写点什么

STM32L051 测试 (五、串口测试 — 与 Enocean 模块通讯问题)

作者:矜辰所致
  • 2022-10-14
    江苏
  • 本文字数:7582 字

    阅读完需:约 25 分钟

STM32L051测试 (五、串口测试 — 与Enocean模块通讯问题)
STM32L051测试 第五课,串口的使用      ....  by 矜辰所致                  ..调整文章格式,增加串口接收卡死处理说明             
复制代码


前言

早些时候写的一些测试记录的文章,并没有完全是以那种教材的方式写的,所以相对于最新的文章,整体格式框架可能没那么好。


好消息是,博主是实打实的实际应用记录,每次遇到问题会来重新更新文章,做新的记录!


经过前面一段时间的测试,我们把 STM32L051 的需要用到的基本功能都测试过了,这次我们得把产品替换成 L051 了。


基本的 IO 使用都没问题,数据存储 EEPROM 和 flash 也没有问题,测试过正常用就可以了(实际上后来 EEPROM 折腾了好久),在串口的使用上,也需要测试一下。


这里贴的代码是我测试流程使用过的代码,当时的文章以记录各种测试数据为主,仅供参考!


本系列博文目录:

STM32L051 测试 (一、使用 CubeMX 生成工程文件 — ST 系列芯片通用)

https://xie.infoq.cn/article/fc5e31ea41e0b33f64b9ee537

STM32L051 测试 (二、开始添加需要的代码)

https://xie.infoq.cn/article/26e83bbc53646042a66e6fb8f

STM32L051 测试 (三、I2C 协议设备的添加测试)

https://xie.infoq.cn/article/76cbd45207fe8f4a6d2d9b99d

STM32L051 测试 (四、Flash 和 EEPROM 的读写)

https://xie.infoq.cn/article/8049c2e853c99348b66fc8c72

一、串口接收处理的几种方式

1.1 串口接收发送不定长度的数据(非 DMA 方式)

以前在在标准库 STM32F103 标准库的使用上用到的 IDLE 中断接收一帧数据,测试用起来确实好用


void USART2_IRQHandler(void)                  //串口2中断服务程序  {  u8 clear=clear;     //消除编译器没有用到的提醒
//USART_ClearFlag(USART2,USART_FLAG_TC); if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { USART_RX_BUF[RX_Data++] = USART2->DR; } else if(USART_GetITStatus(USART2, USART_IT_IDLE) != RESET) { clear=USART2->SR; //读SR寄存器 可以清空寄存器 clear=USART2->DR; //读DR寄存器(先读SR寄存器,再读DR,为了清除IDLE中断) ReceiveState=1; //标记接收到了一帧数据 } }
复制代码


不需要自己做延时处理,在某些情况下,比如通讯模块的串口 3 使用中,还是用到了延时处理,具体就是判断串口是否接收到一个字节的数据?如果接收到一个字节的数据,那么延时一定时间,一般是几毫秒(这个延时时间就是干等,等待这一串数据全部接收完成,所以这个时间需要实际使用中不同的测试优化,干等时间太长了不太合理,太短数据接收不完全处理起来会出错),如下:


RETURN_TYPE blue_getTelegram(TEL_RADIO_TYPE *pu8RxRadioTelegram, TEL_PARAM_TYPE *pu8TelParam){//  uint8 i=0;  uint8 u8CRC = 0;  uint8 u8Count = 0;   uint8 TmpVal = 0;  uint8 DATA_LEN = 0;  uint8 n = 0;  uint8 HEADER_BYTES[4];  uint8 DatBuf[30] = {0};  u8state = GET_SYNC_STATE;  if (Read_pt != Enocean_Data){    delay_ms(7);  //这里的等待就是干等!从收到第一个字节开始干等    while (Read_pt != Enocean_Data)    {      TmpVal = USART_Enocean_BUF[Read_pt++];      if(Read_pt >= 100)                        //定义缓存大小,测试时候用100直接替代        Read_pt = 0;      switch(u8state)      ...
复制代码


自己也使用过环形缓冲区等一些方式进行优化,可能目前的处理方式能够满足大部分功能需求,所以也没有确实的花心思正真去测试优化一个环形缓冲区的使用。


那么现在再 L051 下面,我们怎么来使用 IDLE 中断呢? 当然我先去网上查找了大神们的各种帖子,然后还是得自己修改一下程序,来一步一步做测试,先回到串口接收简单的测试:


if(test_data != Enocean_Data){        HAL_Delay(7);        // printf("Enocean_Data ID is: 0x %d  test_data is : 0x%d \r\n",Enocean_Data,test_data);        if(Enocean_Data > 10){          HAL_UART_Transmit(&huart1,USART_Enocean_BUF, Enocean_Data,0xFFFF); //将串口3接收到的数据通过串口1传出         }              memset(USART_Enocean_BUF, 0, sizeof(USART_Enocean_BUF));   //清空缓存区         Enocean_Data=0;        // (&hlpuart1)->pRxBuffPtr = &USART_Enocean_BUF[Enocean_Data];//用下面的简洁,数组名就可以当做指针用        (&hlpuart1)->pRxBuffPtr = USART_Enocean_BUF;//这一句很重要,没有这一句,后面接收会出错    }    }
复制代码


在主函数初始化串口后,有这么一句,打开串口中断(这里的中断可不可以理解为打开 IT 中断)


 HAL_UART_Receive_IT(&hlpuart1, (uint8_t *)&USART_Enocean_BUF[0], 1);
复制代码


现在我们要开启 IDLE 中断


 __HAL_UART_ENABLE_IT(&hlpuart1,UART_IT_IDLE);  HAL_UART_Receive_IT(&hlpuart1, (uint8_t *)&USART_Enocean_BUF[0], 1);
复制代码


在 stm32l0xx_it.c 中找到 LPUART1_IRQHandler 函数,因为所有中断先是进入 stm32l0xx_it.c 中相应的 IRQHandler 函数中,调用对应函数,最后才会进入到 自己设置的 Callback 函数中,我们这次测试直接在 void LPUART1_IRQHandler(void)函数中系统设置的函数前写一个 实现 IDLE 中断后操作的函数:


void LPUART1_IRQHandler(void){  /* USER CODE BEGIN LPUART1_IRQn 0 */  if((__HAL_UART_GET_FLAG(&hlpuart1,UART_FLAG_IDLE) != RESET))  {        __HAL_UART_CLEAR_IDLEFLAG(&hlpuart1);        ReceiveState = 1;     }  /* USER CODE END LPUART1_IRQn 0 */  HAL_UART_IRQHandler(&hlpuart1);  /* USER CODE BEGIN LPUART1_IRQn 1 */
/* USER CODE END LPUART1_IRQn 1 */}
复制代码


用 ReceiveState 来标志是否接受到了一串数据,然后打印函数变成


 if(ReceiveState == 1){        ReceiveState = 0;              HAL_UART_Transmit(&huart1,USART_Enocean_BUF, Enocean_Data,0xFFFF); //将串口3接收到的数据通过串口1传出                    memset(USART_Enocean_BUF, 0, sizeof(USART_Enocean_BUF));   //清空缓存区         Enocean_Data=0;        (&hlpuart1)->pRxBuffPtr = USART_Enocean_BUF;//这一句很重要,没有这一句,后面接收会出错    }
复制代码


结果, 数据异常,就是收不全,结尾会有问题,明天得继续测试优化了,先上个正常的接收,这是上面用 HAL_Delay(7)测试得到的正常结果。




实际测试,数据会断接收不全,考虑了一下,是否本身我的通讯模组串口给 MCU 的时候一包数据就是分两次发送(其实就是一包数据中间有个时间会长一点,然后 MCU 判定为 2 帧数据,每次能够收到开头的一段),我测试的打印函数每次收到一包数据,都会把数据全部打印出来,然后清空缓存区,所以下一包数据直接被清掉了,因为在进行输出的过程,可能下一包的数据中断进来了,为了验证一下,还是加上了一个延时,代码改成如下:


if(ReceiveState == 1){        HAL_Delay(7); //测试,重要        ReceiveState = 0;              HAL_UART_Transmit(&huart1,USART_Enocean_BUF, Enocean_Data,0xFFFF); //将串口3接收到的数据通过串口1传出                    memset(USART_Enocean_BUF, 0, sizeof(USART_Enocean_BUF));   //清空缓存区         Enocean_Data=0;        (&hlpuart1)->pRxBuffPtr = USART_Enocean_BUF;//这一句很重要,没有这一句,后面接收会出错    }
复制代码


发现加上延时后,数据就正常了……,其实中途测试的时候在 IDLE 中断处理时候,我打印过 测试语句,发现我正常的一包数据,都会打印 2 次测试语句,也进一步的证实了,测试模块给 MCU 的正常的一包数据会被 MCU 认为是 2 帧数据。那么我们怎么来解决这个问题呢,还是直接加一个 ms 延时吗? 至少 IDLE 中断在这里用不了,一帧数据会触发 2 次中断,可能和通讯模块有关,因为以前测试的时候也遇到过类似情况。


但至少 IDLE 中断我们可以用起来,在这里做一个小结:1、正常初始化串口以后,打开 IDLE 中断:


__HAL_UART_ENABLE_IT(&hlpuart1,UART_IT_IDLE);
复制代码


这个语句可以放在串口初始化函数 MX_LPUART1_UART_Init 中,也可以放在 main 函数后面,看个人习惯;


2、在 stm32l0xx_it.c 文件中响应的中断相应函数 LPUART1_IRQHandler 中加入关于 IDLE 中断的处理于语句:


void LPUART1_IRQHandler(void){  /* USER CODE BEGIN LPUART1_IRQn 0 */    /* USER CODE END LPUART1_IRQn 0 */  HAL_UART_IRQHandler(&hlpuart1);  /* USER CODE BEGIN LPUART1_IRQn 1 */  if((__HAL_UART_GET_FLAG(&hlpuart1,UART_FLAG_IDLE) != RESET))  {    usartreceive_IDLE(&hlpuart1);  }  /* USER CODE END LPUART1_IRQn 1 */}
/* USER CODE BEGIN 1 */void usartreceive_IDLE(UART_HandleTypeDef *huart){ __HAL_UART_CLEAR_IT(&hlpuart1,UART_CLEAR_IDLEF); //清除中断 // HAL_UART_AbortReceive_IT(huart); //终止接收 ReceiveState = 1;}
复制代码


用一个标志位表示收到了一帧数据,即便这个一帧不完全,也至少表示收到一段数据了,这里不能加 HAL_UART_AbortReceive_IT(huart); //终止接收 ,因为 IT 中断可以继续接收,如果终止了,数据处理不够及时,下一帧数据就收不到了。


3、最后通过判断标志位 ReceiveState 来处理数据,一般情况下直接处理,特除情况,在标志位至 1 后,刻意的等待几个毫秒,等待数据接收完全再处理。


!!!最后移植又发现一个问题


代码中 #define ID_blueNum 44


  printf("Enocean_Data is :%d Read_pt is :%d\n\r",Enocean_Data,Read_pt);  printf("send command !\r\n");  COMMAND_GetmoduleID();  // HAL_Delay(10);  //7ms不够    while ((Enocean_Data - Read_pt) < ID_blueNum);//加了这句不需要加上面的延时,这句就是等待,这句很重要,能够过滤掉不相关的报文  // while ((Enocean_Data - Read_pt) < ID_blueNum){  //   HAL_UART_Receive_IT(&hlpuart1, &USART_Enocean_BUF[Enocean_Data], 1);  // }//2021/8/11  不是很懂为什么这里这个while等待必须得加点东西  printf("Enocean_Data is :%d Read_pt is :%d\n\r",Enocean_Data,Read_pt);
复制代码



如果按照上面的代码,会一直卡在这里过不去,其实就是 while ((Enocean_Data - Read_pt) < ID_blueNum);过不去,开始怀疑是不是通讯模块本来就是坏的,我们有多种方式测试:


测试代码 1(好理解,发送命令了,等待一会,让串口把数据收完):


  printf("Enocean_Data is :%d Read_pt is :%d\n\r",Enocean_Data,Read_pt);  printf("send command !\r\n");  COMMAND_GetmoduleID();  HAL_Delay(10);//7ms不够    while ((Enocean_Data - Read_pt) < ID_blueNum);//加了这句不需要加上面的延时,这句就是等待,这句很重要,能够过滤掉不相关的报文  printf("Enocean_Data is :%d Read_pt is :%d\n\r",Enocean_Data,Read_pt);
复制代码



测试代码 2(本来是想看看等待时候打印什么东西,加个条件怕一直循环):


  printf("Enocean_Data is :%d Read_pt is :%d\n\r",Enocean_Data,Read_pt);  printf("send command !\r\n");  COMMAND_GetmoduleID();  while ((Enocean_Data - Read_pt) < ID_blueNum){    if(Enocean_Data < 10)printf("测试过随便打印点东西都可以!\r\n");  }//2021/8/11  不是很懂为什么这里这个while等待必须得加点东西  printf("Enocean_Data is :%d Read_pt is :%d\n\r",Enocean_Data,Read_pt);
复制代码



测试代码 3 , 4(不加条件随便在等待中打印 , 延时 ):


printf("Enocean_Data is :%d Read_pt is :%d\n\r",Enocean_Data,Read_pt);  printf("send command !\r\n");  COMMAND_GetmoduleID();  while ((Enocean_Data - Read_pt) < ID_blueNum){    printf("不要条件乱打都可以测试过随便打印点东西都可以!\r\n");    //HAL_Delay(1);//延时可以,1ms就可以  }//2021/8/11  不是很懂为什么这里这个while等待必须得加点东西  printf("Enocean_Data is :%d Read_pt is :%d\n\r",Enocean_Data,Read_pt);
复制代码



经过测试发现,while 中虽然是干等,但是没有语句就会有问题,不是很明白,因为以前都可以,虽然知道解决办法,但是不知道问题原因,所以也不知道哪种是更优的解决办法!= =!

1.2 串口接收发送不定长度的数据(DMA 方式)

在实际使用中,我没有在 L051 下面测试使用 DMA 利用 IDLE 中断进行 DMA 接收发送,实际上,通过我们上面做的实验和测试,对于目前使用的通讯模块,一包数据会触发 2 次 IDLE 中断的情况,也不太适合。本例程是以前 STM32L1 系列中使用的例程,以前也是在网上参考过其他人的设计然后自己用起来的,正好也在这里做个笔记


#define USART3_DMA_REC_SIE 256   //DMAbuf空间大小#define USART3_REC_SIE 512       //接收区空间大小两倍,以保持两条历史数据typedef struct{       uint8_t UsartRecFlag;   //接收数据Flag    uint16_t UsartDMARecLen; //DMA接收到数据的长度    uint16_t UsartRecLen;    //RecBuffer中已经存储数据的长度    uint8_t  Usart3DMARecBuffer[USART3_DMA_REC_SIE];  //dma    uint8_t  Usart3RecBuffer[USART3_REC_SIE];         //RecBuffer} Usart3DMAbufstr;extern Usart3DMAbufstr Usart3type;  
复制代码


void USART3_IRQHandler(void){  /* USER CODE BEGIN USART3_IRQn 0 */
/* USER CODE END USART3_IRQn 0 */ HAL_UART_IRQHandler(&huart3); /* USER CODE BEGIN USART3_IRQn 1 */ if(__HAL_UART_GET_FLAG(&huart3,UART_FLAG_IDLE) == SET) //判断空闲中断 { u16 res = 0; __HAL_UART_CLEAR_IDLEFLAG(&huart3); //清楚空闲中断标志 HAL_UART_DMAStop(&huart3); //暂时关闭DMA中断 res = huart3.Instance->ISR; res = huart3.Instance->RDR; res = hdma_usart3_rx.Instance->NDTR; //读取DMA接收的那个buf还剩多少空间 Usart3type.UsartDMARecLen = USART3_DMA_REC_SIE - temp; //总空间减去还剩下的空间等于存入数据的空间大小 HAL_UART_RxCpltCallback(&huart3); //手动调用中断回调,因为空闲中断不会主动调用 } /* USER CODE END USART3_IRQn 1 */}
复制代码


数据最初都存储在 DMARecBuffer 中,然后转存到 RecBuffer 中。DMARecBuffer 中的数据每接收到新的数据都会清空。


void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){      //可行选着是否开启多条数据缓存,默认不开启,开启删除下划线    if(huart->Instance == USART3)    {      //  if(Usart3type.UsartRecLen>0)//判断RecLen是否清0,如果没有清零代表上一个数据没有读取       //  {        //    memcpy(&Usart3type.Usart3RecBuffer[Usart3type.UsartRecLen],Usart3type.Usart3DMARecBuffer,Usart3type.UsartDMARecLen); //把数据顺延       //     Usart3type.UsartRecLen +=  Usart3type.UsartDMARecLen;//数据长度增加相应的位数        //  }      //  else     //   {            memcpy(Usart3type.Usart3RecBuffer,Usart3type.Usart3DMARecBuffer,Usart3type.UsartDMARecLen);                          //把输入放入buf开头            Usart3type.UsartRecLen =  Usart3type.UsartDMARecLen;                //记录数据长度                                           // }                memset(Usart3type.Usart3DMARecBuffer, 0x00, sizeof(Usart3type.Usart3DMARecBuffer));                                     //把DMA缓存中的数据清空,方便下一次接收        Usart3type.UsartRecFlag = 1;                                                                                            //RecFlag置1,代表RecBuffer中有数据      HAL_UART_Receive_DMA(&huart3,Usart3type.Usart3DMARecBuffer,USART3_DMA_REC_SIE);                                         //重新开启DMA中断,方便再一次接收    }}
复制代码


在相应的程序中把上面的代码添加好,然后在主函数循环中使用,RecBuffer 中的数据会一直增加,直到用户读取以后才会清空:


if(Usart3type.UsartRecFlag == 1)//如果Recbuffer中有数据{  HAL_UART_Transmit(&huart1,Usart3type.Usart3RecBuffer,256,0xffff);//把RecBuffer中的数据发送到串口1  memset(Usart3type.Usart3RecBuffer, 0x00, sizeof(Usart3type.Usart3RecBuffer));//读取完数据后记得一定要把RecBuffer中的数据清除  Usart3type.UsartRecFlag = 0;//标志位清0  Usart3type.UsartRecLen = 0;//已有数据长度清0}
复制代码

1.3 环形缓冲区

因为单单靠数组方式,接收处理,总感觉不是那么聪明,有时候需要干等,所以还是得花时间研究下环形缓冲区。。。

二、串口接收卡死处理

在使用了了段时间后,测试部反馈偶尔会有串口卡死说明,最终就是接收不到串口数据,但是轮询发送是正常,后来查阅了一些资料,找到了需要处理的地方,这里特此来记录一下。

2.1 清除错误标志位

在使用 HAL 库的时候,有 4 个错误 flag,如下图:



出错的时候,HAL 库会把以上 flag 置位如果不清除就再也接收不到数据了。


所以我们可以在需要的时候使用,下面的语句清除错误标志:


  __HAL_UART_CLEAR_FLAG(&hlpuart1, UART_FLAG_PE);//清标志  __HAL_UART_CLEAR_FLAG(&hlpuart1, UART_FLAG_FE);  __HAL_UART_CLEAR_FLAG(&hlpuart1, UART_FLAG_NE);  __HAL_UART_CLEAR_FLAG(&hlpuart1, UART_FLAG_ORE);
复制代码


比如,在我清除串口接收缓存的函数中,我加上了这几句代码:



这是实际使用的情况,我并没有详细的测试到底是哪一个错误置位了,在自己了解的简单产品上,直接一步到位也是一种方式。

2.2 HAL 库函数问题

产品使用了上面的串口清除错误标志,在压力测试下下面还是有问题:


现象就是串口发送正常,但是永远接收不到数据了。


实在是没办法,后来继续找答案。


最后确实发现网上也有小伙伴遇到过相同的问题,我使用的是 HAL_UART_Receive_IT 开启中断:



在这个函数中有这么一段:



里面有个加锁操作,正式因为这个加锁操作,如果收发同时进行,会有概率卡死。


这个时候的处理方式就是手动解锁,每次接收到一个字节后需要再次开启中断接收,判断一下是否有错误:



判断的语句如下:


if(huart->Instance == LPUART1){    Enocean_Data++;    if(Enocean_Data > 98)Enocean_Data = 0;    while(HAL_UART_Receive_IT(&hlpuart1, (uint8_t *)&USART_Enocean_BUF[Enocean_Data], 1) != HAL_OK){      hlpuart1.RxState = HAL_UART_STATE_READY;      __HAL_UNLOCK(&hlpuart1);    }  }
复制代码


结语


具体我后期会单独使用一篇文章来说明这个问题吗,本文只是为了说明基本的串口使用,这个问题的详细说明以及解决方法我还需要一点时间。


发布于: 刚刚阅读数: 3
用户头像

矜辰所致

关注

CSDN、知乎、微信公众号: 矜辰所致 2022-08-02 加入

不浮夸,不将就,认真对待学知识的我们,矜辰所致,金石为开! 为了活下去的嵌入式工程师,画画板子,敲敲代码,玩玩RTOS,搞搞Linux ...

评论

发布
暂无评论
STM32L051测试 (五、串口测试 — 与Enocean模块通讯问题)_stm32_矜辰所致_InfoQ写作社区