文章目录
前言
上周讲到的消除按键抖动代码用的是阻塞式等待。
这次用第二种方式——非阻塞等待。就是判断当前uwTick与基准uwTick之间的差值是否大于目标“阈值”,使用这种结构相比较阻塞式等待能大幅提升效率
所用工具:
1、芯片: STM32F103C8T6
2、STM32CubeMx软件
3、IDE: MDK-Keil软件
4、STM32F1xx/STM32F4xxHAL库
提示:以下是本篇文章正文内容,下面案例可供参考
一、外部中断按键消抖
1、工程配置
- 1、设置RCC
- 2、设置串口
1、设置按键PA8为GPIO_EXTI8
2、设置GPIO mode 为 External lnterrupt Mode with Falling edge trigger detection(具有下降沿触发检测的外部中断模式)
3、设置GPIO Pull-up
1、设置按键PB12为GPIO_Output
2、设置GPIO mode 为 Output Push Pull(输出推拉)
3、设置GPIO NO pull-up and no pull-down
4、设置Maximum output speed为High
- 3、打开中断
点击NVIC打开对应的Enabled按钮
- 4、创建工程
点击 GENERATE CODE,重新生成代码。
2、工程代码
点开工程的stm32f1xx_it.c可以看到下面的代码
void EXTI9_5_IRQHandler(void)
{
/* USER CODE BEGIN EXTI9_5_IRQn 0 */
/* USER CODE END EXTI9_5_IRQn 0 */
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_8);
/* USER CODE BEGIN EXTI9_5_IRQn 1 */
/* USER CODE END EXTI9_5_IRQn 1 */
}
右键 HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_8)
点击GO to Definition
可以看见下面的代码
__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(GPIO_Pin);
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_GPIO_EXTI_Callback could be implemented in the user file
*/
}
复制void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)到main.c的下面USER CODE BEGIN 4 和 USER CODE END 4 之间,然后加入下面的代码。代码中的HAL_GetTick() 也可以替换为 uwTick
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
static uint32_t TickBase=0;
if(GPIO_Pin==Button_Pin && HAL_GetTick() - TickBase >= 30)
{
TickBase = HAL_GetTick();
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
}
}
二、 Usart与Printf函数重定向
1、工程配置
- 1、设置
在 Pinout&Configuration 界面中点击左侧 Connectivity 选择 USART1,然后在 USART1 Mode and Configuration 的 Mode 中选择 Asynchronous。Asynchronous 为异步通信的意思。Parameter Setting(基础设置)为默认设置,其中波特率为 115200 Bits/s,字节长度为 8 Bits。
- 2、开启中断
- 选择 NVIC Setting,将Enable打钩 ,启用 USART1 中断。
- 3、创建工程
点击 GENERATE CODE,重新生成代码。
2、工程代码
在 MDK-ARM 工程里要把 USE MicroLIB 选上
打开 usart.c,在 /* USER CODE BEGIN 0 / 和 / USER CODE END 0 */ 把标准输入输入头文件 stdio.h 加进去,并且加入以下代码
#include "stdio.h"
#ifdef __GNUC__
/* With GCC/RAISONANCE, small printf (option LD Linker->Libraries->Small printf
set to 'Yes') calls __io_putchar() */
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */
继续在 /* USER CODE BEGIN 1 / 和 / USER CODE END 1 */ 之间加入以下代码:
/**
* @brief Retargets the C library printf function to the USART.
* @param None
* @retval None
*/
PUTCHAR_PROTOTYPE
{
/* Place your implementation of fputc here */
/* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
至此,我们已经完成了Printf函数的重定向,之后的代码里面就可以直接使用Print函数了
三、STM32 Uart 接收数据
1、工程代码
打开usart.c,在 /* USER CODE BEGIN 0 / 和 / USER CODE END 0 */ 之间加入以下代码:
uint8_t uart1Rx[1024];//申请1024个字节的数组
uint8_t *pUart1Rx =uart1Rx;//定义一个指针,用来指向存放数据的数组
uint16_t uart1Size;//接收到数据的长度
在 stm32f1xx_it.c 中右键 HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_8);
GO to Definition
在HAL_UART_IRQHandler() 函数里加上这一段。
/* UART in mode Idle -------------------------------------------------*/
if(((isrflags & USART_SR_IDLE) != RESET) && ((cr1its & USART_CR1_IDLEIE) != RESET))
{
HAL_UART_IdleCpltCallback(huart);
return;
}
然后在HAL_UART_IRQHandler() 函数外面加上这段
__weak void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_UART_TxCpltCallback could be implemented in the user file
*/
}
至此,整个 HAL_UART_IRQHandler() 函数,是这样的。
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
uint32_t isrflags = READ_REG(huart->Instance->SR);
uint32_t cr1its = READ_REG(huart->Instance->CR1);
uint32_t cr3its = READ_REG(huart->Instance->CR3);
uint32_t errorflags = 0x00U;
uint32_t dmarequest = 0x00U;
/* If no error occurs */
errorflags = (isrflags & (uint32_t)(USART_SR_PE | USART_SR_FE | USART_SR_ORE | USART_SR_NE));
if (errorflags == RESET)
{
/* UART in mode Receiver -------------------------------------------------*/
if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))
{
UART_Receive_IT(huart);
return;
}
}
/* If some errors occur */
if ((errorflags != RESET) && (((cr3its & USART_CR3_EIE) != RESET) || ((cr1its & (USART_CR1_RXNEIE | USART_CR1_PEIE)) != RESET)))
{
/* UART parity error interrupt occurred ----------------------------------*/
if (((isrflags & USART_SR_PE) != RESET) && ((cr1its & USART_CR1_PEIE) != RESET))
{
huart->ErrorCode |= HAL_UART_ERROR_PE;
}
/* UART noise error interrupt occurred -----------------------------------*/
if (((isrflags & USART_SR_NE) != RESET) && ((cr3its & USART_CR3_EIE) != RESET))
{
huart->ErrorCode |= HAL_UART_ERROR_NE;
}
/* UART frame error interrupt occurred -----------------------------------*/
if (((isrflags & USART_SR_FE) != RESET) && ((cr3its & USART_CR3_EIE) != RESET))
{
huart->ErrorCode |= HAL_UART_ERROR_FE;
}
/* UART Over-Run interrupt occurred --------------------------------------*/
if (((isrflags & USART_SR_ORE) != RESET) && (((cr1its & USART_CR1_RXNEIE) != RESET) || ((cr3its & USART_CR3_EIE) != RESET)))
{
huart->ErrorCode |= HAL_UART_ERROR_ORE;
}
/* Call UART Error Call back function if need be --------------------------*/
if (huart->ErrorCode != HAL_UART_ERROR_NONE)
{
/* UART in mode Receiver -----------------------------------------------*/
if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))
{
UART_Receive_IT(huart);
}
/* If Overrun error occurs, or if any error occurs in DMA mode reception,
consider error as blocking */
dmarequest = HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAR);
if (((huart->ErrorCode & HAL_UART_ERROR_ORE) != RESET) || dmarequest)
{
/* Blocking error : transfer is aborted
Set the UART state ready to be able to start again the process,
Disable Rx Interrupts, and disable Rx DMA request, if ongoing */
UART_EndRxTransfer(huart);
/* Disable the UART DMA Rx request if enabled */
if (HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAR))
{
CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAR);
/* Abort the UART DMA Rx channel */
if (huart->hdmarx != NULL)
{
/* Set the UART DMA Abort callback :
will lead to call HAL_UART_ErrorCallback() at end of DMA abort procedure */
huart->hdmarx->XferAbortCallback = UART_DMAAbortOnError;
if (HAL_DMA_Abort_IT(huart->hdmarx) != HAL_OK)
{
/* Call Directly XferAbortCallback function in case of error */
huart->hdmarx->XferAbortCallback(huart->hdmarx);
}
}
else
{
/* Call user error callback */
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
/*Call registered error callback*/
huart->ErrorCallback(huart);
#else
/*Call legacy weak error callback*/
HAL_UART_ErrorCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
}
}
else
{
/* Call user error callback */
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
/*Call registered error callback*/
huart->ErrorCallback(huart);
#else
/*Call legacy weak error callback*/
HAL_UART_ErrorCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
}
}
else
{
/* Non Blocking error : transfer could go on.
Error is notified to user through user error callback */
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
/*Call registered error callback*/
huart->ErrorCallback(huart);
#else
/*Call legacy weak error callback*/
HAL_UART_ErrorCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
huart->ErrorCode = HAL_UART_ERROR_NONE;
}
}
return;
} /* End if some error occurs */
/* UART in mode Transmitter ------------------------------------------------*/
if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
{
UART_Transmit_IT(huart);
return;
}
/* UART in mode Transmitter end --------------------------------------------*/
if (((isrflags & USART_SR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET))
{
UART_EndTransmit_IT(huart);
return;
}
}
开启中断。将 usart.c 中MX_USART1_UART_Init(void) 函数的
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
改写成
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
else
{
HAL_UART_Receive_IT(&huart1,pUart1Rx,1);
}
__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
然后在 /* USER CODE BEGIN 1 / 和 / USER CODE END 1 */ 之间加入以下代码:
/* USER CODE BEGIN 1 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart1)
{
pUart1Rx++;
uart1Size++;
//HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
HAL_UART_Receive_IT(&huart1,pUart1Rx,1);
}
}
void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart)
{
__HAL_UART_CLEAR_IDLEFLAG(huart);
if(huart == &huart1)
{
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
HAL_UART_Transmit(&huart1,uart1Rx,uart1Size,1000);
pUart1Rx = uart1Rx;
uart1Size=0;
}
}
/* USER CODE END 1 */
STM32收到的数据,是这样处理的。
HAL_UART_Transmit(huart, &RxLenHi, 1, 1000); // 发送长度高位
HAL_UART_Transmit(huart, &RxLenlo, 1, 1000); // 发送长度低位
HAL_UART_Transmit(huart, uart4Rx, uart4RxLength, 1000); // 发送接收到的数据
四、定时器编码器模式读取脉冲数据
1、工程配置
- 1、设置
在左侧 Pinout&Configuration 界面中的 Timers 下拉中点击 TIM4,然后在 TIM4 Mode and Configuration 的 Mode 中将 Combined Channels 选择为 Encoder Mode,即编码器模式。
在 Configuration 中选择 Parameter Setting 选项卡,,Counter Mode 默认为 Up,即向上计数。Counter Period 设置为 65535,即计数器周期,这是一个 16 位的自动加载寄存器,填写范围为 0~65535。Encoder Mode 设置为 Encoder Mode TI1 and TI2,即两个输入 TI1 和 TI2 都被用来作为增量编码器的接口。Polarity 默认为 Rising Edge,即为捕获上升沿。其他参数默认。
注意: 当 Encoder Mode 设置为 Encoder Mode TI1 and TI2 模式时,AB 两相的上升沿和下降沿都会计数,所以计数值是实际脉冲值的 4 倍,即四倍频。Channel1 和 Channel2 的 Polarity 参数默认是 Rising Edge,意思是在检测到上升沿的时候就触发编码器模式接口捕获 AB 相的值,并不是指只检测 AB 相的上升沿,下降沿还是同样会计数的。
- 2、创建工程
点击 GENERATE CODE,重新生成代码。
2、工程代码
打开 MDK-ARM 工程,按下组合键 Ctrl+N(按住 Ctrl 键再按 N 键),新建一个文件,再按下组合键 Ctrl+S,文件名改为 encoder.c,保存到 MiaowLabs-DEMO 的 Src 文件夹里,接着在 MDK-ARM 工程界面左侧 Project 栏目双击 Application/User 文件夹,把 encoder.c 加进来。
双击 encoder.c 文件,把下面代码敲进去
#include "tim.h"//包含tim头文件
#include "encoder.h"
int iTim4Encoder;//存放从TIM4定时器读出来的编码器脉冲
int GetTim4Encoder(void)//获取TIM4定时器读出来的编码器脉冲
{
iTim4Encoder = (short)(__HAL_TIM_GET_COUNTER(&htim4));//先读取脉冲数
__HAL_TIM_SET_COUNTER(&htim4,0);//再计数器清零
return iTim4Encoder;//返回脉冲数
}
再新建一个文件 encoder.h 头文件,把文件保存到 Inc 文件夹。然后,把下面代码敲进去。
#ifndef __ENCODER_H
#define __ENCODER_H
int GetTim4Encoder(void);//声明函数
#endif
打开 main.c 文件,在 main 函数中的 /* USER CODE BEGIN 1 / 和 / USER CODE END 1 */ 之间定义一个变量:
int iTempTim4Encoder; //临时存放从TIM4编码器接口捕获到的脉冲数据
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201202001525804.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5fQXJ0aHVy,size_16,color_FFFFFF,t_70)
在主循环 while(1) 里,把以下代码敲进去:
HAL_Delay(5000);//延时5秒
iTempTim4Encoder = GetTim4Encoder();//捕获TIM4脉冲数据
printf("TIM4定时器编码器模式捕获脉冲 = %d \n",iTempTim4Encoder);//把脉冲数据打印出来
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);//翻转指示灯LED的电平
上面这段加入主循环的代码的意思,是 TIM4 定时器的编码器捕获脉冲,每隔 5 秒累计输出一次脉冲数据,通过串口显示在上位机上。
这里补充一个知识点:M 法测速,就是数固定时间内产生的脉冲数,像这里每隔 5 秒数一次累计起来的脉冲数据,就是用了 M 法测速。
代码还没写完,有一句重要的代码必须要加进去,TIM4 的编码器接口模式才会启用。我们在 main 函数的 /* USER CODE BEGIN 2 / 和 / USER CODE END 2 */ 之间加入:
HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL);//开启TIM4的编码器接口模式
五、PWM与TB6612FNG驱动电机
STM32 与 TB6612FNG 的主要接线:
PB0 --> AIN1
PB1 --> AIN2
PA3 --> BIN1
PA4 --> BIN2
TIM3_CH1 --> PWMA
TIM3_CH2 --> PWMB
1、工程配置
- 1、设置
打开工程。在左侧 Pinout&Configuration 界面中的 Timers 下拉中点击 TIM3,然后在 TIM3 Mode and Configuration 的 Mode 中将 Channel2 选择为 PWM Generation CH2,并在下方的参数设置选项卡中将 Prescaler 设为 72,即预分频系数(TIMx_PSC)设为 72;Counter Period 设为 100,即计数周期(自动加载值 TIMx_ARR)设为 100;Pulse 设为 100,即占空比设置为 100%。
回到 STM32CubeMX 软件界面,在右侧界面的芯片中分别点击 PA3、PA4,并将其配置为 GPIO_Output。在 System Core 下拉菜单中选择 GPIO,然后在左侧的 System Core 下拉菜单中选择 GPIO,然后在 GPIO Mode and Configuration 中对 PA3、PA4 引脚进行配置,GPIO output level 代表 GPIO 默认输出电平,在这里设置为低电平;GPIO mode 代表 GPIO 引脚模式,在这里设置为推挽输出;GPIO Pull-up/Pull-down 即 GPIO 上拉或下拉,在这里设置为既不上拉也不下拉;Maximum output speed 即 最大输出速度,在这里设置为低速;User Label 即用户标签,在这里将 PA3 改为 BIN1,PA4 改为 BIN2。
- 2、创建工程
点击 GENERATE CODE,重新生成代码。
2、工程代码
在 main.c 中的 /* USER CODE BEGIN 2 / 和 / USER CODE END 2 */ 之间加入以下代码:
/* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_2);//开启TIM3_CH2的PWM输出
HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, GPIO_PIN_SET);//初始化BIN1引脚为低电平
HAL_GPIO_WritePin(BIN2_GPIO_Port, BIN2_Pin, GPIO_PIN_RESET);//初始化BIN2引脚为高电平
/* USER CODE END 2 */
然后,在主循环中加入以下代码:
ButtonScan();
if(g_iButtonState == 1)//如果按键被按下
{
HAL_GPIO_TogglePin(BIN1_GPIO_Port,BIN1_Pin);//翻转BIN1引脚电平,如果是低电平则翻转为高电平,如果是高电平则翻转为低电平
HAL_GPIO_TogglePin(BIN2_GPIO_Port,BIN2_Pin);//翻转BIN2引脚电平,如果是低电平则翻转为高电平,如果是高电平则翻转为低电平
g_iButtonState = 0;//按键状态归0,代表松开
}
上面这段代码的意思是,每次按下按键,左侧电机的转动方向都会更换一次,默认是全速转动(占空比 100%)。
遇到的问题
1、以CubeMX生成的代码为基础,添加用户代码
使用时发现每一次重新生成代码的时候,自己添加进去的代码就会消失不见。网上查资料发现所有用户自定义添加的代码必须在BEGIN和END之间。 还有工程配置也要修改,若是下面的没有配置,即使把代码写在了USER CODE BEGINE 中也会被覆盖。
2、编码器数据为什么会输出负数
如果 TI1 和 TI2 分别接电机的 A 相和 B 相的话,那么,当电机正转的时候,如下图计数器会向上计数,反转的时候会向下计数,但是向下计数并不会出现负的值,依旧是从(0-ARR)计数。
获取编码器脉冲的代码如下
int GetTim4Encoder(void)//获取TIM4定时器读出来的编码器脉冲
{
iTim4Encoder = (short)(__HAL_TIM_GET_COUNTER(&htim4));//先读取脉冲数
__HAL_TIM_SET_COUNTER(&htim4,0);//再计数器清零
return iTim4Encoder;//返回脉冲数
}
注意看,上面的代码 iTim4Encoder = (short)(__HAL_TIM_GET_COUNTER(&htim4)); 使用了强制类型装换,把寄存器的值读出来了之后,转换成了 short 型(2 字节),范围为(-32768-32767),此时当我们把计数器的初始值设置为 0 之后,如果出现反转,它就会从 0 开始向下计数(0,65535,65534,…)但是经过强制类型转换之后就变成了(0,-1,-2,…)。
但是为什么 65535 会变成 -1 ?此时我们回到 short 的表示范围(-32768-32767),也就是说原来 int 型变量当读出来的值为 32767, 32768, 32769,…,65535,65536,65537… 的时候会因为强制转换成 short 型变量而溢出转换为 32767,-32768,-32767,…,-1,0,1 就这样不断地循环下去。所以电机反转的时候读出的数就是反方向的速度值,不需要用 65535 去减去读出的值再加上负号才可以得到方便观察的值,我们只需要运用一个强制类型转换就可以了。
3、为什么按键按下无法控制电机反转
记得在主循环里添加 ButtonScan(); 函数
总结
好多好多……冲冲冲!