摘要:在实际项目中经常用到串口接收一些不定长的数据,怎么判断这一帧数据接收完成了呢?通常使用UART非空中断配合简单的数据协议,在数据中加入帧头、帧尾,在程序中判断是否接收到帧尾来确定数据接收完毕,对每个字节数据都要进行判断,比较消耗系统资源,尤其是在一些实时性要求较高的场合。而串口空闲中断可以大大简化数据接收过程的判断,在这一块起到非常重要的作用。
关于这种方法,可以看以前STM32第五章-串口通讯详解。下面说一下空闲中断+DMA。
一、什么是空闲中断?
空闲中断(IDLE),俗称帧中断,即第一帧数据接收完毕到第二帧数据开始接收期间存在一个空闲状态(每接收一帧数据后空闲标志位置1),检测到此空闲状态后即执行中断程序。空闲中断的优点在于省去了帧头帧尾的检测,进入中断程序即意味着已经接收到一组完整数据,仅需即时对数据处理或将数据转移出缓冲区即可。
串口空闲中断在串口无数据接收的情况下,是不会产生的,产生的条件是当清除空闲标志位后,必须有接收到第一个数据后,才开始触发,一旦接收的数据断流,没有接收到数据,即产生空闲中断。
MM32F3277系列的UART支持空闲中断功能。相关的寄存器包括中断状态寄存器(UART_ISR)、 中断使能寄存器(UART_IER)、 中断清除寄存器(UART_ICR),对应的位如下:
中断状态寄存器(UART_ISR)
中断使能寄存器(UART_IER)
中断清除寄存器(UART_ICR)
二、串口初始化
这里以串口1为例,串口1一般就这打印调试使用。
static void RS485_Init(uint32_t baudrate)
{
GPIO_InitTypeDef GPIO_InitStruct;
UART_InitTypeDef UART_InitStruct;
NVIC_InitTypeDef NVIC_InitStructure;
//打开串口和GPIO的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART3, ENABLE);
//打开GPIO的复用功能,具体复用到哪一个线,要看数据手册
GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_7);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource11, GPIO_AF_7);
//UART3_TX GPIOB.10
GPIO_StructInit(&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOB, &GPIO_InitStruct);
//UART3_RX GPIOB.11
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOB, &GPIO_InitStruct);
//串口参数初始化
UART_StructInit(&UART_InitStruct);
UART_InitStruct.BaudRate = baudrate;
UART_InitStruct.WordLength = UART_WordLength_8b;
UART_InitStruct.StopBits = UART_StopBits_1;
UART_InitStruct.Parity = UART_Parity_No;
UART_InitStruct.HWFlowControl = UART_HWFlowControl_None;
UART_InitStruct.Mode = UART_Mode_Rx | UART_Mode_Tx;
UART_Init(UART3, &UART_InitStruct);
//使能串口空闲中断
UART_ITConfig(UART3, UART_IER_RXIDLE, ENABLE);
//使能串口
UART_Cmd(UART3, ENABLE);
//因为要使用中断,所以必须配置中断优先级
NVIC_InitStructure.NVIC_IRQChannel= UART3_IRQn; //指定中断源
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd= ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
中断服务函数
void UART3_IRQHandler(void)
{
if(UART_GetITStatus(UART3, UART_ISR_RXIDLE) != RESET)
{
/*清除串口空闲中断标志位*/
UART_ClearITPendingBit(UART3, UART_ICR_RXIDLE);
/*
数据处理
*/
}
}
这样就可以了。但是串口空闲中断一般是配合DMA的方式。
三、串口DMA
一般串口空闲中断配合MM32F0270 UART使用DMA方式接收数据可以减小CPU的开销。
对于接收定长数据,可以将DMA接收缓冲区的长度设定为待接收数据的长度,这样利用DMA的传输完成中断就可以知道已经接收了一帧数据。
对于接收不定长数据,由于内核在串口接收数据到空闲这段时间,是不受理串口数据的,所以可以使用DMA来协助我们把数据传送到指定的地方,当数据传输完成后,通知内核去处理。
注意每一款单片机的串口DMA通道不一样,一定要看数据手册。MM32F3277的DMA各通道请求映像表如下,其中UART3_RX对应通道3。
再来看以下MM32F0270的DMA各通道请求映像,两款芯片串口对应的通道是不一样的。
通过DMA方式把数据传送到指定的缓存区。当UART接收完一帧数据后,会产生一个空闲中断。这个中断在UART其他任何状态都不产生,只会在接收完一帧数据后才会产生,一帧数据可以是1个字节或者多个字节。
四、DMA初始化
void UART_Receive_DMA(UART_TypeDef* uart, uint32_t cmar, uint16_t cndtr)
{
DMA_InitTypeDef DMA_StructInit;
//UART1_RX DMA1_Channel5
//UART2_RX DMA1_Channel6
//UART3_RX DMA1_Channel3
RCC_AHBPeriphClockCmd(RCC_AHBENR_DMA1, ENABLE);
/* DMA1 Channel3 (triggered by USART3 Rx event) Config */
DMA_DeInit(DMA1_Channel3);
DMA_StructInit.DMA_PeripheralBaseAddr = (uint32_t)&UART3->RDR;
DMA_StructInit.DMA_MemoryBaseAddr = cmar;
DMA_StructInit.DMA_DIR = DMA_DIR_PeripheralSRC; //外设到内存
DMA_StructInit.DMA_BufferSize = cndtr; //DMA 通道的 DMA 缓存的大小
DMA_StructInit.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址不变
DMA_StructInit.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址递增
DMA_StructInit.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //8 位
DMA_StructInit.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //8 位
DMA_StructInit.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式
DMA_StructInit.DMA_Priority = DMA_Priority_Medium; //DMA 通道 x 拥有中优先级
DMA_StructInit.DMA_M2M = DMA_M2M_Disable;
DMA_StructInit.DMA_Auto_reload = DMA_Auto_Reload_Disable;
DMA_Init(DMA1_Channel3, &DMA_StructInit);
/* Enable USART3 DMA Rx request */
UART_DMACmd(UART3, UART_DMAReq_EN, ENABLE);
DMA_Cmd(DMA1_Channel3, ENABLE);
}
关于DMA的就不用多说了,数据从内存到外设,或者外设到内存,或者内存到内存都不需要CPU的参与,直接存储器访问即可,减轻了CPU的压力。
既然有了增加了DMA,所以我们的串口终端服务函数也要修改一下了。
void UART3_IRQHandler(void)
{
if(UART_GetITStatus(UART3, UART_ISR_RXIDLE) != RESET)
{
/*清除串口空闲中断标志位*/
UART_ClearITPendingBit(UART3, UART_ICR_RXIDLE);
DMA_Cmd(DMA1_Channel3, DISABLE);//停止DMA传输
/*
数据处理
*/
DMA_Cmd(DMA1_Channel3, ENABLE); //使能DMA传输
}
}
在中断服务函数中,当发生了空闲中断时要关闭DMA,停止DMA数据传输,然后进行处理数据,在数据处理完成之后在打开DMA,继续接收数据。
调试
打开串口调试助手发送一帧数据,如:666
,程序运行至断点1,
UART中断状态寄存器UART_ISR
的RXIDLE_INTF
位置1,表示进入UART空闲中断。
程序运行至断点2,UART中断状态寄存器UART_ISR
的RXIDLE_INTF
位清0。
这时串口接收到的数据就自动保存到了DMA初始化中你填写的那个内存基地址所在的内存区域了,想什么时候用就什么时候用。