STM32 | Cube | HAL库 | UART接收中断应用

前言

能看到这里的话,就默认你不是那种新手阶段的小白,是那种有一定的C语言基础,在工作或者学习生涯中的某一段时间突然要接触STM32的HAL库这种,并且之前是用过标准库开发过的。
很久没有写博客了,距离上一篇正式发布的好像已经过去一年,虽然中间陆陆续续写了一些,但是一直是草稿不想发布,因为那个时间段算是正式学习32BitMCU,之前学8BitMCU的方法明显不适用了,所以没有办法把32BitMCU的功能尽可能地写出来(主要是用的人多,万一写得不好怕挨骂🤣)。
最近为什么突然想写了呢?最近工作需要,我要开始重新接触STM32并且用HAL库开发,虽然已经做好心理准备这是一个稍微有点麻烦的过程,算是开发思维的一个大的转变,但是接触下来感觉还是状况百出😵
光看程序还是有点晕的,于是我决定上网看看别人是怎么用的,看了一圈下来还是要感慨一下,好多人写的内容真的差不多,顶多就是给自己的变量换个名字,流程都是一样的,甚至有些人代码不知道谁抄的谁,一模一样然后自己标个原创。不过有的博客内容还是有一点水平的,哈哈,但不多,不是完全没有。
而且感觉好像没有写到我想要看到的重点哈,那就我自己来写吧🧐

HAL库的中断机制

真的超级麻烦,我的第一印象,而且多层函数嵌套,光是跳转都要不少时间开销了,但是后面想想,正常的开发也不太可能频繁进出中断,另外就是72M的主频,这些跳转的开销也算是可以勉强接受?
就拿UART的接收中断来举例,光是HAL库的程序就是各种函数嵌套😲

  1. void USART1_IRQHandler(void);
  2. HAL_UART_IRQHandler(&huart1);
  3. UART_Receive_IT(huart);
  4. HAL_UART_RxCpltCallback(huart);

列举出来感觉也还好哈哈🤔其实也没有必要去把这个顺序或者涉及到的所有程序理清楚,我的建议是用到哪个中断就去看对应的分支,另外就是上面提到的前面3个函数基本不用改,除非是有很特殊的需求,主要也是因为改了也没有用,Cube重新配置一下的话,你改动的地方不会被保留🙃
接收中断一整个流程的分支就是我上面写的那样,一般要改的话就是第四个,回调函数是HAL库中断比较特殊的机制,如果没有记错的话标准库应该是没有这种东西的。
本身程序就有地方预留了一个用关键字__weak修饰的函数,具体原理没有仔细研究过,但是可以粗糙地认为,如果程序其他地方没有同名的函数就使用这个虚函数的程序,如果有的话,就使用不带__weak关键字修饰的函数。
扯远了,大概意思就是,这个回调函数要自己复制一下函数名然后放到自己方便修改的地方来重新写,好像还没有见过直接在官方预留的那个函数里面写的。

不同模块使用同一个回调函数

思路跟标准库的中断分支有点像,假如之前在标准库进入串口中断,怎么知道是哪个行为触发的中断呢?所以一般在中断里面判断是接收中断,发送完成中断还是接收空闲中断等。
但是HAL也算是帮我们把不同触发中断的分支判断写好了,不过因为预留的回调函数机制,只要是UART模块触发的接收中断,不管是UART1还是UART2都是同一个回调函数,所以在进入回调函数的时候去判断到底是UART1还是UART2触发,如果不做判断的话,就会导致一个很明显的问题,我UART1和UART2接收数据之后的操作完全不一样,写在一个回调函数里面不做判断区分,那就写不了。
有些教程居然没提到这一点,我是有点吃惊的,这么不严谨的吗😰
区分的方式也很简单,我简单试了一下,下面两种都是可以的。

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART1)
    {
    
    }
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart == (&huart1))
    {
    
    }
}

接收空闲中断

标准库里面是有预留接收空闲中断的相关标志位的来判断的,但是不知道为什么HAL库好像还没有,我看到别人说F4的程序里面加了,F1暂时还没有。
我搜了一下别人自己加入这个中断分支的操作,有一个博主模仿HAL库的风格,加入了空闲中断分支,内容是真的多😨但还是那个问题,加这些程序除非后续不再用Cube配置,不然还是会被清掉的,所以我就没有仔细看了。
所以也是有点矛盾的,Cube+HAL库开发,除非是官方升级HAL库把大家反馈的内容加进去,不然个人去加一些底层的东西,好像不太合适。
这篇博客重点也不是讨论接收空闲中断的问题,我比较关心是实际开发中遇到的功能。
接收空闲中断用在那种,不等长数据的情况,且没有固定格式的结尾数据,但是我好像还没遇到过,如果真的有这种的话,可能要自己搭一个定时器模块进去计算时间。
不等长数据的话一般会加一个\r\n在数据尾部来标记,不然真的不太好处理。
另一种情况就是,每次通信数据长度都是固定的。
下面就写个代码验证一下这两种情况。

不等长字符接收

初始化配置接收中断

首先,要在主函数用户初始化区域,给使能串口接收中断,Cube配置的程序不包括这一句🙄

HAL_UART_Receive_IT(&huart1, (uint8_t )&uart_data_byte, 1); / 开启串口接收中断 */

接收中断配置函数分析

这个函数里面还是可以着重了解一下的,我把源代码搬过来。
其实也可以理解成打开接收中断,不过因为HAL库加了特殊的功能进去,所以也不仅仅只有打开中断这一个功能。

HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  /* Check that a Rx process is not already ongoing */
  if (huart->RxState == HAL_UART_STATE_READY)
  {
    if ((pData == NULL) || (Size == 0U))
    {
      return HAL_ERROR;
    }

    /* Process Locked */
    __HAL_LOCK(huart);

    huart->pRxBuffPtr = pData;
    huart->RxXferSize = Size;
    huart->RxXferCount = Size;

    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->RxState = HAL_UART_STATE_BUSY_RX;

    /* Process Unlocked */
    __HAL_UNLOCK(huart);

    /* Enable the UART Parity Error Interrupt */
    __HAL_UART_ENABLE_IT(huart, UART_IT_PE);

    /* Enable the UART Error Interrupt: (Frame error, noise error, overrun error) */
    __HAL_UART_ENABLE_IT(huart, UART_IT_ERR);

    /* Enable the UART Data Register not empty Interrupt */
    __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

这个接收中断配置的函数,第二个参数是一个指针,它并不是说一个变量或者一个数组,它就是一个指针,指针的本质是一个地址,函数的实参是一个变量uart_data_byte,用&取地址然后传进函数。
huart->pRxBuffPtr = pData;这一句的意思就是huart->pRxBuffPtr地址指向我刚才传入的那个变量的地址,huart->pRxBuffPtr这个又是什么呢?后面会再分析这个。
有点绕,但是涉及到指针的内容确实没有办法一下子能讲明白的🤒
函数的第三个参数配置的是这两个huart->RxXferSize = Size;huart->RxXferCount = Size;也同样是后面再具体分析。

接收中断函数

下面这个是接收中断的源代码,但还不是回调函数,是回调函数被这个函数调用。

static HAL_StatusTypeDef UART_Receive_IT(UART_HandleTypeDef *huart)
{
  uint16_t *tmp;

  /* Check that a Rx process is ongoing */
  if (huart->RxState == HAL_UART_STATE_BUSY_RX)
  {
    if (huart->Init.WordLength == UART_WORDLENGTH_9B)
    {
      tmp = (uint16_t *) huart->pRxBuffPtr;
      if (huart->Init.Parity == UART_PARITY_NONE)
      {
        *tmp = (uint16_t)(huart->Instance->DR & (uint16_t)0x01FF);
        huart->pRxBuffPtr += 2U;
      }
      else
      {
        *tmp = (uint16_t)(huart->Instance->DR & (uint16_t)0x00FF);
        huart->pRxBuffPtr += 1U;
      }
    }
    else
    {
      if (huart->Init.Parity == UART_PARITY_NONE)
      {
        *huart->pRxBuffPtr++ = (uint8_t)(huart->Instance->DR & (uint8_t)0x00FF);
      }
      else
      {
        *huart->pRxBuffPtr++ = (uint8_t)(huart->Instance->DR & (uint8_t)0x007F);
      }
    }

    if (--huart->RxXferCount == 0U)
    {
      /* Disable the UART Data Register not empty Interrupt */
      __HAL_UART_DISABLE_IT(huart, UART_IT_RXNE);

      /* Disable the UART Parity Error Interrupt */
      __HAL_UART_DISABLE_IT(huart, UART_IT_PE);

      /* Disable the UART Error Interrupt: (Frame error, noise error, overrun error) */
      __HAL_UART_DISABLE_IT(huart, UART_IT_ERR);

      /* Rx process is completed, restore huart->RxState to Ready */
      huart->RxState = HAL_UART_STATE_READY;

#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
      /*Call registered Rx complete callback*/
      huart->RxCpltCallback(huart);
#else
      /*Call legacy weak Rx complete callback*/
      HAL_UART_RxCpltCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */

      return HAL_OK;
    }
    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

一般串口的配置都是,8位数据,无奇偶校验。每次接收到一个字符,程序都会跑到26行这里,也就是这一句:

*huart->pRxBuffPtr++ = (uint8_t)(huart->Instance->DR & (uint8_t)0x00FF);

等号的右边可以理解成,我直接在UART模块的数据寄存器读取数据出来,等号的左边是一个地址,然后我把读到的数据放到这个地址上面来,这个地址是什么?之前配置接收中断的时候,让它指向了一个变量的地址,那个变量是我自己定义的一个全局变量,用来存串口读到的数据。
怎么对一个地址赋值呢?要用这个*运算符,跟指针组合就是要给这个变量赋值的意思。
如果从结果上来看就是,把中断接收到的数据放到我自己定义的一个变量上面。从这一套复杂的流程来说,涉及到了不少指针知识,为什么要用指针呢?

原理概述

有几点原因,先说最基础的一点,在大学里面肯定会遇到的一个知识点,用指针给两个变量互换值的,就算看得明白,自己也会写这种程序,但是遇到实际开发的话,估计也是很容易懵逼,核心点在于,用函数传一个变量进去,本质上进入函数的时候,会生成一个看不到的临时变量然后把参数的值赋给这个临时的变量,不管怎么操作这个变量,都不会对参数本身的那个变量有影响,因为操作的是那个临时变量。
但是如果传的是这个变量的地址,我就可以通过*运算符对指定地址上面的变量进行赋值,来完成一个值传递的操作。
唔😵也不知道怎么表达比较好了,感觉就是了解指针的话一眼能明白,不了解的话,看多几遍也是不明白的。我就不写这么多东西了,万一露馅就完了。
第二点原因是,还记得那个函数的第三个参数吗,是接收到多少个字符,然后怎样怎样的。如果不为1的话,就是多字符接收的情况了,后面再来分析。

代码验证

先来看看单个字符接收的简单测试代码,下面的回调函数就是要自己写的。
只有这一个函数需要改,其他几个函数只是单独拿一些片段出来阅读,并不需要改。

uint8_t uart_rx_index = 0;
uint8_t uart_data_byte;

#define                UART_RX_BUF_LEN                100
uint8_t uart_rx_buffer[UART_RX_BUF_LEN] = {0};

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
        if(huart == (&huart1))
        {
                if(uart_rx_index < UART_RX_BUF_LEN)
                {
                        /* 字符拼接 */
                        uart_rx_buffer[uart_rx_index++] = uart_data_byte;
                }
                else /* 超过最大限定范围直接清掉 */
                {
                        uart_rx_index = 0;

                        memset(uart_rx_buffer, 0x00, UART_RX_BUF_LEN);
                }

                /* 如果接收到换行和回车,即证明是一帧数据完结 */
                if((uart_rx_buffer[uart_rx_index - 2] == '\r') && (uart_rx_buffer[uart_rx_index - 1] == '\n'))
                {
                        printf("Rx Receive: %s\r\n", uart_rx_buffer);

                        uart_rx_index = 0; /* 指针清零 */

                        memset(uart_rx_buffer, 0x00, UART_RX_BUF_LEN);
                }

                HAL_UART_Receive_IT(&huart1, (uint8_t *)&uart_data_byte, 1);   //再开启接收中断
        }
}

注意事项:实际开发过程中不建议在中断里面使用printf函数,这个东西可以理解成比想象中要耗费更多时间。

整个流程大概意思就是,我配置每次接收字符就进入回调函数,其实跟下面这个变量设置的有关系,因为配置的值是1,所以看上去就是每次接收到数据都会调用回调函数。实际情况应该是等计数变量自减到0才会调用一次回调函数。(同样是上面的HAL库源代码,删减了一些东西,方便阅读)

    huart->RxXferSize = Size;
    huart->RxXferCount = Size;
if (--huart->RxXferCount == 0U)
{
    HAL_UART_RxCpltCallback(huart);
}

把接收到的字符全部拼接在一个数组上面,如果收到回车\r换行\n就打印出来,并且开始重新接收。这里溢出没有做额外处理,可以打印一个错误的提示。
还有一个问题,上面提到的函数UART_Receive_IT,这里面有关闭接收中断的操作,所以每次接收到字符就会进入到下面这个分支,再执行回调函数,所以要在回调函数里面重新打开中断。

if (--huart->RxXferCount == 0U)
{
  /* Disable the UART Data Register not empty Interrupt */
  __HAL_UART_DISABLE_IT(huart, UART_IT_RXNE);

  /* Disable the UART Parity Error Interrupt */
  __HAL_UART_DISABLE_IT(huart, UART_IT_PE);

  /* Disable the UART Error Interrupt: (Frame error, noise error, overrun error) */
  __HAL_UART_DISABLE_IT(huart, UART_IT_ERR);

  /* Rx process is completed, restore huart->RxState to Ready */
  huart->RxState = HAL_UART_STATE_READY;

#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
  /*Call registered Rx complete callback*/
  huart->RxCpltCallback(huart);
#else
  /*Call legacy weak Rx complete callback*/
  HAL_UART_RxCpltCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */

  return HAL_OK;
}

验证结果

在这里插入图片描述

固定字符接收

假设我现在固定要每次接收5个字符,数据结构如下:
数据头+数据+校验和+回车+换行

中断配置函数

HAL_UART_Receive_IT(&huart1, (uint8_t *)&uart_rx_buffer, 5);

第二个参数从之前的一个变量的地址变成一个数组的地址,长度暂时固定是100,然后接收字节数为5,可以理解成要接收到5个byte之后才能触发一次回调函数。
假设如果数据发送方出了一点问题,某一次发送4个byte之后,后面恢复正常每次发送5个byte,但是接收方这里如果没有额外的处理机制的话,一定是会出故障的,我自己测试到的一些现象,同时也提供了一个简单的保护机制。

代码验证

#define                UART_RX_DATA_LEN        3
uint8_t sensor_data_buf[UART_RX_DATA_LEN+2] = {0};

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
        if(huart->Instance == USART1)
        {
                /* 能进来就证明已经接收到5byte数据了 */

                /* 这个缓存直接用,不用转换 */
                if(uart_rx_buffer[UART_RX_DATA_LEN] == '\r' && uart_rx_buffer[UART_RX_DATA_LEN + 1] == '\n')
                {
                        /* 简单的校验和,等于数据[0][1]的和,取低8位 */
                        if(((uart_rx_buffer[0] + uart_rx_buffer[1]) & 0xFF) == uart_rx_buffer[2])
                        {
                                printf("Sensor Data:");
                                for(uint8_t i = 0; i < UART_RX_DATA_LEN; i++)
                                {
                                        sensor_data_buf[i] = uart_rx_buffer[i];
                                        if(i == UART_RX_DATA_LEN - 1)
                                                printf("%d\r\n", sensor_data_buf[i]);
                                        else
                                                printf("%d-", sensor_data_buf[i]);
                                }
                        }
                }

                __HAL_UART_FLUSH_DRREGISTER(&huart1); /* 清除串口的数据寄存器,防止多余数据对下一次接收的影响 */
                
                HAL_UART_Receive_IT(&huart1, (uint8_t *)&uart_rx_buffer, 5); /* 重新开启一次接收中断 */
        }
}

前面也提到过,配置接收字节数,其中一个作用就是,接收到指定个数的字符才进入一次回调函数。
后面也有详细测试,少发或者多发数据过来,会有什么影响。
这个回调函数的逻辑就是,接收到5个byte之后,判断最后是不是回车和换行,如果是的话再进一步判断接收到的数据,符不符合简单的校验逻辑,如果不符合,证明数据出问题就不做其他处理,重新配置中断等下一笔数据。
如果数据符合条件,则将收到的数据赋值给自己需要的地方,这里假设了一组sensor_data_buf变量,如果数据正常就接收并且打印出来。

串口助手配置

在这里插入图片描述
勾选HEX发送,然后待发送数据以16进制的形式填写,每一个byte数据之间留一个空格。

验证结果

[图片]
5byte数据,有回车换行且校验和正确才能打印出来。
[图片]
5byte数据,但是校验和出错,也不打印。
[图片]
发送6byte数据,但是程序配置的接收5byte数据符合条件,同时加入了清空数据寄存器的程序,所以也能正常打印。
[图片]
如果少于5byte数据,也不能正常打印。

使用指针开发程序

前面提到的一个事,好像没有说完,刚好也是在写这个日志的时候有些感慨。
大学C语言课程的时候,老师直接都说了,指针是最难的一个知识点,其实也没有说错,但是直接把所有人给吓住了,同时在之后的几年时间里,没有做具体项目的学习过程中,看各种各样的教材去学习指针,印象比较深刻是之前看过的一本书《指针的艺术》,也是学得有点模糊,因为确实不知道什么时候能用得上这种东西。
在工作第二年的时候突然想起要搞点项目,然后在没有任何第三方开源项目参考下,自己开始整合一些常用驱动的框架,比如LCD和OLED等。在这个过程中会经常遇到一个问题的,比如:

  1. 怎么把一个数组作为参数传入给函数?
  2. 一维数组和二维数组的关系是怎样的,可以怎样转换?
  3. 如果一个函数的返回值固定是0/1两种状态,但是我还要返回一个计数型的变量,要怎么处理呢?

新手阶段的绝大部分功能完全可以不使用指针来实现的,都可以选择用一些比较笨的方法来实现。就拿第一点来讲,不知道怎么把数组作为参数传入,那我就不传参数,直接在函数里面对这个数组进行操作,这个其实没有问题,但是如果需要操作的数组不止一个呢?这种方法的应对方式就是,给每一个需要处理的数组配置一个函数。
如果函数内部对于数组处理的操作都是一样的,那就非常不合适,需要处理的数组越多,浪费的Flash就越多。但是如果能理解数组和指针的关系,这样子就比较好解决了,把这个数组的地址也就是指针传给函数,函数内部可以通过for循环的方式给地址递增,然后对每个地址进行读取或者赋值操作,完成对数组的处理,就可以用一个函数来完成多个同类型数组的处理操作。
第三点的话,可能描述得不是很完善,具体的情况就是,返回值是固定的几种状态值,我需要通过函数计算之后,返回把这个值传出来,但是函数的返回值只能有一个,写个简单的代码帮助理解。
像下面这种情况,如果我要把变量Data传入进去,同时经过函数处理再传出来的话,是实现不了的。


#define                STATUS_OK                 1
#define                STATUS_ERROR         2

uint8_t Data;

uint8_t ReadData(uint8_t data)
{
        data >>= 1;
        if(data == 0)
        {
                return STATUS_ERROR;
        }
        else
        {
                return STATUS_OK;
        }
}

int main(void)
{
        uint8_t tmp_status = 0, tmp_data = 0;

        Data = 0x01;

        tmp_status = ReadData(Data);
        
        /* Data被传进去,但是实际上没有任何变化 */

        return 1;
}

前面提到的,传进去的变量再怎么折腾,函数执行完之后,那个被折腾的局部变量被注销了,传进去的变量还是没有任何变化的。但是如果像HAL库的这种方式,把变量的地址传进去,在函数里面对这个地址进行赋值,就可以达成目的了。

int main(void)
{
        uint8_t tmp_status = 0;

        Data = 0x01;

        tmp_status = ReadData(&Data);

        if(tmp_status == STATUS_OK)
        {

        }
        else if(tmp_status == STATUS_ERROR)
        {

        }

        return 1;
}

哈哈,如果还有问题的话大概率就是,既然要判断返回值,那就证明不是所有情况都要能操作到那个变量,又应该怎么处理?就是加一个局部变量的问题🤭

int main(void)
{
        uint8_t tmp_status = 0, tmp_data = 0;

        Data = 0x01;

        tmp_data = Data;

        tmp_status = ReadData(&tmp_data);

        if(tmp_status == STATUS_OK)
        {
               Data = tmp_data ;
        }
        else if(tmp_status == STATUS_ERROR)
        {

        }

        return 1;
}

大概就是这样子🐾

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值