STM32|STM32 串口总线空闲检测

主机环境:Windows XPSP3
开发环境:MDK 5.20
目标芯片:STM32F030C8T6
前两天在群里看到有人在询问有关STM32 串口总线空闲检测的事情,根据串口总线是否空闲来判断一帧数据是否发送完成,之前使用串口一直没怎么注意过这一串口特性,所以后来特意去看了下手册中有关总线空闲检测的指示,发现它的确是个好特性,之前都只是在串口中断中接收数据在主循环中不断的读取数据然后检测是否是一帧完整的数据,之后再进行后续处理。这样处理有一个不是很好的问题就是在主循环读取串口数据时需要有个超时计数器来避免无串口数据时死等在那里,但如果使用串口总线空闲检测的话,我们就不需要超时计数器了,只需要在检测到串口总线空闲时把收到的数据全部读走,然后检测是否满足一定的格式进而处理,这样是的主循环的时间进一步减少,加速了系统的处理速度。
在STM32F030C8T6的参考手册中串口中断状态寄存器USARTx_ISR中有一个IDLE位来表明是否检测到总线空闲,如下图所示:
STM32|STM32 串口总线空闲检测
文章图片


并且给出了如何清除该标识,STM32F1系列芯片清除该标识的方法不同,可根据参考手册来查询,且该标识置位后就不再置位除非RXNE位再次置位,如果上位机一次性发送了1个字节数据则RXNE置位1次,IDLE置位1次,而如果上位机一次性发送了6个字节数据,则RXNE置位6次,IDLE依然置位1次,只要在CR1寄存器中使能了串口总线空闲检测就可以使用该特性了,使用标准库编辑了一下测试代码,uart头文件如下

#ifndef __UART_H__ #define __UART_H__ #include #include "stm32f0xx.h" #include #define USARTxUSART1 #define USARTx_GPIO_PORTGPIOA #define USARTx_GPIO_CLKRCC_AHBPeriph_GPIOA #define USARTx_TX_PINGPIO_Pin_9 #define USARTx_TX_SOURCEGPIO_PinSource9 #define USARTx_TX_AFGPIO_AF_1 #define USARTx_RX_PINGPIO_Pin_10 #define USARTx_RX_SOURCEGPIO_PinSource10 #define USARTx_RX_AFGPIO_AF_1 #define USARTx_IRQnUSART1_IRQn #define USARTx_IRQHandlerUSART1_IRQHandler #define USARTx_CLK_ENABLE()RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE)void uart_init (uint32_t baud); #endif


uart的源码文件如下
【STM32|STM32 串口总线空闲检测】
#include "uart.h" uint8_t buffer[100]; uint8_t cnt = 0,idle_detect = 0; void uart_init (uint32_t baud) { USART_InitTypeDef USART_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; //初始化串口时钟以及串口端口时钟 RCC_AHBPeriphClockCmd(USARTx_GPIO_CLK, ENABLE); USARTx_CLK_ENABLE(); GPIO_PinAFConfig(USARTx_GPIO_PORT, USARTx_TX_SOURCE, USARTx_TX_AF); GPIO_PinAFConfig(USARTx_GPIO_PORT, USARTx_RX_SOURCE, USARTx_RX_AF); GPIO_InitStructure.GPIO_Pin = USARTx_TX_PIN| USARTx_RX_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_Level_3; GPIO_Init(USARTx_GPIO_PORT, &GPIO_InitStructure); USART_InitStructure.USART_BaudRate= baud ; //设置波特率 USART_InitStructure.USART_WordLength= USART_WordLength_8b; //8位数据位 USART_InitStructure.USART_StopBits= USART_StopBits_1; //1位停止位 USART_InitStructure.USART_Parity= USART_Parity_No; //无校验位 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件控制 USART_InitStructure.USART_Mode= USART_Mode_Rx | USART_Mode_Tx; //发送与接收两种方式 USART_Init(USARTx, &USART_InitStructure); USART_ITConfig(USARTx,USART_IT_RXNE,ENABLE); //使能接收中断,在接收移位寄存器中有数据时产生 USART_ITConfig(USARTx,USART_IT_PE,ENABLE); USART_ITConfig(USARTx,USART_IT_ERR,ENABLE); USART_ITConfig(USARTx,USART_IT_IDLE,ENABLE); //使能总线空闲检测中断 /* 使能 USARTx 中断 */ NVIC_InitStructure.NVIC_IRQChannel = USARTx_IRQn; NVIC_InitStructure.NVIC_IRQChannelPriority=0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); USART_Cmd(USARTx, ENABLE); }int fputc(int ch, FILE *f) { USART_SendData(USARTx,(uint8_t)ch); while(USART_GetFlagStatus(USARTx,USART_FLAG_TXE) != SET); return ch; }void USARTx_IRQHandler(void) { uint8_t temp = 0; if(USART_GetFlagStatus(USARTx,USART_FLAG_ORE) != RESET) { temp = USART_ReceiveData(USARTx); (void)temp; USART_ClearFlag(USARTx,USART_FLAG_ORE); } if(USART_GetFlagStatus(USARTx,USART_FLAG_NE) != RESET) { USART_ClearFlag(USARTx,USART_FLAG_NE); } if(USART_GetFlagStatus(USARTx,USART_FLAG_FE) != RESET) { USART_ClearFlag(USARTx,USART_FLAG_FE); } if(USART_GetFlagStatus(USARTx,USART_FLAG_PE) != RESET) { USART_ClearFlag(USARTx,USART_FLAG_PE); } if(USART_GetITStatus(USARTx, USART_IT_RXNE) != RESET)//判断寄存器中是否有数据 { buffer[cnt++]=USART_ReceiveData(USARTx); //读取数据,读数据的同时清空了接收中断标志; if(cnt >= 100) { cnt = 0; } USART_ClearITPendingBit(USARTx, USART_IT_RXNE); } if(USART_GetITStatus(USARTx, USART_IT_IDLE) != RESET) { //清除总线空闲中断标志位 USART_ClearITPendingBit(USARTx, USART_IT_IDLE); idle_detect = 1; } return; }


这里检测到IDLE标识置位后置位idle_detect变量,cnt变量标记了本次接收到的字节个数,主函数测试代码如下:

#include "uart.h"extern uint8_t idle_detect,cnt; extern uint8_t buffer[100]; int main (void) { uint8_t i = 0; uart_init(115200); while(1) { if(1 == idle_detect) { idle_detect = 0; printf("\r\ncnt:%X,ctx:",cnt); for(i = 0; i < cnt; i++) { printf("%02X ",buffer[i]); } cnt = 0; } } }

代码比较简单,这里使用printf来输出本次接收到的字节数以及字节内容,运行结果如下:
STM32|STM32 串口总线空闲检测
文章图片

可以看到检测结果是正常的,只是在开机后有一次cnt为0的结果,应该是上电之后总线默认就是空闲的,的确也没有收到数据,因此就想把cnt为0的结果去掉,这里我遇到了一个很纠结的问题,在判断idle_detect变量的同时也检测cnt是否大于0,测试代码更改如下:

#include "uart.h"extern uint8_t idle_detect,cnt; extern uint8_t buffer[100]; int main (void) { uint8_t i = 0; uart_init(115200); while(1) { if(1 == idle_detect && cnt > 0) { idle_detect = 0; printf("\r\ncnt:%X,ctx:",cnt); for(i = 0; i < cnt; i++) { printf("%02X ",buffer[i]); } cnt = 0; } } }


感觉逻辑是对的,再次运行,结果如下:
STM32|STM32 串口总线空闲检测
文章图片


这个时候发现cnt变量的值输出一直为1,但输出的字节内容却是对的,一直想不通这是为啥?思来想去逻辑是没啥问题的,后来又把cnt是否为0的条件判断不放在跟idle_detect变量检测同一水平,而是放在它里面,更改测试代码如下:

#include "uart.h"extern uint8_t idle_detect,cnt; extern uint8_t buffer[100]; int main (void) { uint8_t i = 0; uart_init(115200); while(1) { if(1 == idle_detect) { idle_detect = 0; if(cnt == 0) continue; printf("\r\ncnt:%X,ctx:",cnt); for(i = 0; i < cnt; i++) { printf("%02X ",buffer[i]); } cnt = 0; } } }


这个时候再次运行代码,结果如下:
STM32|STM32 串口总线空闲检测
文章图片


这个时候输出的结果是正确的,难道两个条件检测放在一起会有问题吗?这个是不应该的,因为idle_detect为0时CPU是不会再去检测cnt变量的,只有idle_detect为1时才去检测cnt变量,因此cnt的条件检测放在里面和放在外面应该是一样的效果才对。后来想是否是printf引起的问题,就把printf去掉改成了自己的输出函数,更改测试代码如下:

#include "uart.h"extern uint8_t idle_detect,cnt; extern uint8_t buffer[100]; int main (void) { uint8_t i = 0; uart_init(115200); while(1) { if(1 == idle_detect && cnt > 0) { idle_detect = 0; uart_puts("\r\ncnt:"); uart_char(0x30+cnt); uart_puts(",ctx:"); uart_write(buffer,cnt); cnt = 0; } } }


这里cnt的输出是有问题的,但我测试时保证cnt不会大于10,因此不影响测试结果,在编译时把微库去掉,运行结果如下:
STM32|STM32 串口总线空闲检测
文章图片


结果发现在数据小于等于6时结果是正确的,而当数据大于6时cnt一直为6但字节内容同样是正确的,很是费解,在后来我又测试过当发送的字节为16时,输出的数据内容会少几个字节,总而言之把cnt变量的判断放在idle_detect变量检测的后面就会导致结果不对,而把cnt变量的判断放在里面就不会有问题,所以应该不是printf引起的问题,感觉像是if条件语句引起的问题,尝试过很多方法也没搞定该问题,虽然最后我们也能回避这个问题,但没找到原因真纠结,如果有人知道是啥问题的话,麻烦告知一下。
最后串口总线空闲检测这一特性的确很有用,尤其是对收到的数据是不定长时更有效果,大家以后可以尝试使用一下该特性。
PS:
今天下午不死心又进行了一些测试,还怀疑过是否是时序的问题,对比了一下cnt判断所在位置不同的汇编代码,对比结果如下:
STM32|STM32 串口总线空闲检测
文章图片


左侧是cnt判断和idle_detect判断在同一水平上,右侧是cnt判断放在了idle_detect条件检测里面,两者差别不大,再次证明我们的代码逻辑是没有问题的,后来又想起使用printf和使用我们自己的输出函数cnt的值会有变化,在使用printf时cnt一直为1,而使用我们自己的输出函数时cnt在数据长度小于等于6时正确,两者的区别就是printf会使用微库,延迟不同,所以增加了一个延迟函数,如下:

void delay(void) { uint32_t t = 5000; while(t) { __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); t-=1; } }


更改主函数的测试代码,delay()函数可以有两处放置,如下:
int main (void) { uint8_t i = 0; uart_init(115200); while(1) { delay(); if(1 == idle_detect && cnt > 0) { idle_detect = 0; //delay(); printf("\r\ncnt:%X,ctx:",cnt); for(i = 0; i < cnt; i++) { printf("%02X ",buffer[i]); } cnt = 0; } } }


加上delay()函数后代码运行的结果就正确了,区别是delay()函数如果放在if条件语句里面时当上位机发送的数据量多时需要的延迟就会长些才能保证结果的正确性,而delay()函数放在if条件语句的外面则不会这样,总算找到问题了,但还是推荐把cnt的判断放在if条件的里面,这样就不需要增加延迟函数了,就这样吧。

    推荐阅读