【STM32】两轮自平衡小车学习笔记2


前言

上周讲到的消除按键抖动代码用的是阻塞式等待
这次用第二种方式——非阻塞等待。就是判断当前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);    // 发送接收到的数据

参考博客——STM32 Uart 接收不定长数据

四、定时器编码器模式读取脉冲数据

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)设为 72Counter Period 设为 100,即计数周期(自动加载值 TIMx_ARR)设为 100Pulse 设为 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(); 函数


总结

好多好多……冲冲冲!

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页