有人在使用STM32的UART收发并开启空闲中断时,有时会发现空闲中断相比预期多进一次的情况。比方,本来以为只会进3次空闲中断的结果进了4次;或者说根本没开启接收,一使能空闲中断就立即进一次中断服务程序;有时即使在使能空闲中断之前还特意做了空闲事件标志的清零也会发生类似情况。
下面我找了块STM32开发板,选择USART1做自发自收的测试。也的确可以重现问题。
下面是我的测试代码的main程序:
#define Length (25)
uint8_t Data_RX[Length]={0};
uint32_t UART_Rx_Len; //the Number of received data by DMA
uint32_t UART_Rx_Count_IDLE;//Counting IDLE interrupt times
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
//HAL_Delay(20);
__HAL_UART_CLEAR_FLAG(&huart1, UART_CLEAR_IDLEF);
__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart1, Data_RX, Length);
HAL_UART_Transmit(&huart1,(uint8_t *)"ABC",3,0xffff);//1st TX
HAL_Delay(20);
HAL_UART_Transmit(&huart1,(uint8_t *)"BDEF",4,0xffff);//2nd TX
HAL_Delay(20);
HAL_UART_Transmit(&huart1,(uint8_t *)"CGHIJ",5,0xffff);//3rd TX
HAL_Delay(20);
HAL_UART_Transmit(&huart1,(uint8_t *)"DKLMNP",6,0xffff);//4th TX
HAL_Delay(20);
/* USER CODE END 2 */
while(1)
{ }
}
从代码里不难看出,这里做了4帧数据的发送,帧间加了20ms的延时。每发送一帧数据之后应会产生一个空闲帧。
下面是IDLE中断处理代码
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE)!=0)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
UART_Rx_Count_IDLE++;//counting idle interrupt times
UART_Rx_Len=Length- huart1.hdmarx->Instance->CNDTR;
HAL_UART_DMAStop(&huart1);
HAL_UART_Receive_DMA(&huart1, Data_RX, Length); //Receive again
}
/* USER CODE END USART1_IRQn 0 */
/* HAL_UART_IRQHandler(&huart1);*/
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
中断处理代码很简单。这里没有开启 UART其它相关中断,仅仅针对IDLE事件做处理,其它UART事件的中断就不用理睬。检测到空闲事件后,清除空闲中断请求标志,统计收到的数据个数和进入空闲中断的次数,然后重新开启新的UART的DMA方式接收。
变量UART_Rx_Count_IDLE表示CPU进入空闲中断的次数。
变量UART_Rx_Len表示UART通过DMA接收到内存的数据个数。
我们基于上面代码进行验证测试。
我们先发送第1帧“ABC”3个字母出去,看看UART接收和IDLE事件响应的情况。
从发送和接收的情况来看,收发是正常的、IDLE中断里统计到数据个数也正确,但统计到IDLE中断次数UART_Rx_Count_IDLE明显不对,似乎多计了1次。因为现在才发送1帧数据出去,应该只会有1次IDLE事件,怎么进了2次IDLE中断呢?【注:我将上图中右下角放大后截图放在图中间便于查看。】
我们不妨继续发送第2帧"BDEF"4个字母出去,看看UART接收和IDLE事件的情况。
同样,收发结果一致,统计到接收数据个数也正确,就是进IDLE中断的次数多了1次。【注:我依然将上图中右下角放大后截图放在图中间便于查看。】
基于上面代码,当我把4帧数据都发送完毕的话,按理只应该进4次空闲中断,可是却进了5次空闲中断。
不论发到第几帧数据,收发的结果正常,就是进空闲中断的次数比预想的多了1次。这是怎么回事呢?
这里多出来的1次中断有时可能会导致些麻烦,尤其在不知情的情况下。因为我们常常根据空闲中断来做些判断及处理,如果像这种不清不楚地多1次中断可能会给我们的应用带来些隐患或困惑。
。。。。。。
查看STM32手册UART章节相关内容。
空闲帧是一个特殊的通信帧,全帧是包含起始位、停止位在内的全“1”帧。
在UART每次接收到数据后,紧接着若通信线上出现不短于1个字符传输时间的高电平时则被硬件判为空闲通信帧并可以触发空闲中断。【对于UART传输,每个传输字的起始位是低电平,停止位是高电平】
另外,我们还可以从STM32手册中看到,对于STM32片内的UART,在使能其发送功能时,具体操作就是在对USART_CR1寄存器的TE位置位时,硬件会自动发送1个空闲帧出去。【下图是来自STM32手册相关描述】
现在是基于USART自发自收,难道前面多出来的那次空闲中断是因为在做UART初始化时对TE位置1操作所导致的?
细想起来,这种可能性的确存在。如果代码里在使能UART的发送功能,即对USART_CR1寄存器的TE位置位时产生空闲帧,UART接收端也感受到了,这样的话,若使能IDLE中断时若先不做空闲事件标志清零的话,是会立即进入中断一次。显然,此时还并没有真正的数据发送或接收。
但是,我们从前面main()函数代码里看到了在使能IDLE中断之前已经先做对IDLE事件标志的清零,莫非这个清零操作太早?此时,IDLE事件或许还没真正生成呢!以下面示意图为例,黄色区域表示空闲帧持续时间,清除空闲事件标志的操作显然不能太着急,清得太早也没意义。
那么,我们不妨先研究下代码,看看是哪个地方对TE@USART_CR1实现置位的。
我们不难追查到对TE置位是发生在 MX_USART1_UART_Init()函数;在这个初始化代码里,最终在 UART_SetConfig()这个函数里完成。
既然这样,如果我们在MX_USART1_UART_Init();执行之后稍作延时后再对IDLE事件标志清零,然后使能IDLE事件中断,按理应该就可以避免上面提到的多进一次空闲中断的情况了。
我们把前面的main()代码稍作调整,修改成下面样子,实际上就是在做IDLE事件标志清零之前加了个延时。至于IDLE中断响应代码保持不变。【下面延时所加的20ms延时可能有点夸张,这里只为演示和验证结果。】
#define Length (25)
uint8_t Data_RX[Length]={0};
uint32_t UART_Rx_Len; //the Number of received data by DMA
uint32_t UART_Rx_Count_IDLE;//Counting IDLE interrupt times
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
HAL_Delay(20); //Newly added
__HAL_UART_CLEAR_FLAG(&huart1, UART_CLEAR_IDLEF);
__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart1, Data_RX, Length);
HAL_UART_Transmit(&huart1,(uint8_t *)"ABC",3,0xffff);//1st TX
HAL_Delay(20);
HAL_UART_Transmit(&huart1,(uint8_t *)"BDEF",4,0xffff);//2nd TX
HAL_Delay(20);
HAL_UART_Transmit(&huart1,(uint8_t *)"CGHIJ",5,0xffff);//3rd TX
HAL_Delay(20);
HAL_UART_Transmit(&huart1,(uint8_t *)"DKLMNP",6,0xffff);//4th TX
HAL_Delay(20);
/* USER CODE END 2 */
while(1)
{ }
}
基于上面修改后的代码,验证结果都是正常的,不再有多进一次中断的情况了。下图是UART发送2次后的接收及IDLE中断响应的情况。
下图是UART发送4次后的接收及IDLE中断响应的情况。
基于修改后的代码经过反复验证,最终可以实现发送几次就对应几次IDLE中断【每次发送后保留了足够延时,以保证数据发送后的空闲帧产生】的目的,这也说明了上面的分析和判断是正确的。
前面刚开始测试时遇到多出1次IDLE中断的情形,是因为使能UART发送功能时发送了一个空闲帧并触发了中断。解决办法就是等待该空闲帧发送完毕后直接清除IDLE事件标志,然后才使能IDLE中断,这样就避免了UART硬件使能发送功能时的空闲帧触发中断的问题,进而可以避免个别应用时的麻烦或困惑。
不过,在具体应用中是否会产生多次1次空闲中断的问题还要具体问题具体分析。比方当完成UART初始化并使能发送功能后,并不立刻使能IDLE中断,而是优哉游哉地做了其它诸多事情后才来使能IDLE中断【注:假定此时空闲帧早已发送完毕也被接收到】,并在使能IDLE中断前做了IDLE事件标志的清零,这时也不会产生多进1次IDLE中断的问题。
个人觉得这里的重点是我们要知道有这么回事,在具体应用时我们可以灵活处理。比方,即使在开启UART接收空闲帧中断前不做任何延时也可以,我们可以在IDLE中断里检查数据的接收情况,因为使能UART发送功能时发送的空闲帧之前是没有数据接收的。
我们还是以前面的测试代码为例,在UART初始化之后不做任何延时就开启UART的接收并使能IDLE中断。我们只需将IDLE中断响应代码稍微调整也可以规避启动UART发送功能时发出的空闲帧对我们程序判断的影响。
下面是调整后的IDLE中断响应代码之截图。
前面的测试是将4帧不同长度的数据分4批发送,不同发送帧间保持了足够的延时以产生空闲事件,如果将这4帧数据发送间的延时取消掉,即将上面代码中几个UART发送函数间的Delay(20)屏蔽掉,并给UART采用DMA方式的接收安排足够长度的接收缓冲【这里只设置为25,具体应用时视情况而定】,看看结果怎么样。
测试代码是下面的样子,就是在前面修改过的main()代码基础上,屏蔽掉4次发送操作间的Delay(20)延时。中断处理还是最初的代码。
#define Length (25)
uint8_t Data_RX[Length]={0};
uint32_t UART_Rx_Len; //the Number of received data by DMA
uint32_t UART_Rx_Count_IDLE;//Counting IDLE interrupt times
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
HAL_Delay(20); //Newly added
__HAL_UART_CLEAR_FLAG(&huart1, UART_CLEAR_IDLEF);
__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart1, Data_RX, Length);
HAL_UART_Transmit(&huart1,(uint8_t *)"ABC",3,0xffff);//1st TX
// HAL_Delay(20);
HAL_UART_Transmit(&huart1,(uint8_t *)"BDEF",4,0xffff);//2nd TX
// HAL_Delay(20);
HAL_UART_Transmit(&huart1,(uint8_t *)"CGHIJ",5,0xffff);//3rd TX
// HAL_Delay(20);
HAL_UART_Transmit(&huart1,(uint8_t *)"DKLMNP",6,0xffff);//4th TX
HAL_Delay(20);
/* USER CODE END 2 */
while(1)
{ }
}
我们来看看运行结果。
从结果不难看出4帧数据都被完整接收到一批缓存了,即作为一次性DMA接收的结果。尽管数据分4帧发送,由于发送间隔较短不足以触发空闲事件,也就不会重新开启新的DMA接收,都尽收在1批内存区了,共18个字符,全部接收完毕后进了一次空闲中断,并做好了下次接收的准备。这也是基于空闲事件接收不定长数据的常见处理方式。
关于UART空闲事件中断多进一次的话题就聊到这里,供君参考。知道怎么回事了在具体应用时灵活处理即可。