之前讲过用 利用IDLE空闲中断来接收不定长数据 ,但是没有用到DMA,其实用DMA会更加的高效,MCU也可以腾出更多的性能去处理应该做的事情。
IDLE顾名思义,就是空闲的意思,即当监测到串口空闲超过1个串口的数据帧时,会使状态寄存器(SR或ISR)的IDLE位置位,如果此时控制寄存器(CR或CR1)的IDLEIE为1,则会触发IDLE中断。
DMA搬运数据,则是一边接收数据,一边将串口接收到的数据搬运到内存中,这个过程不需要MCU参与,等到IDLE中断到来的时候,直接去内存中取数据即可。
DMA中断在CubeMX中是默认开启的,可以手动将其关闭,等IDLE中断到来的时候,直接操作读取数据即可。当DMA设置为NORMAL模式时,这个中断是完全用不到的,因为当IDLE到来时,置标志位,然后数据处理,此时需要重启DMA,而DMA的接收缓冲区又大于不定长的数据帧,这样DMA中断就永远不会触发了。
知道原理了,就好操作了。
开启USART1异步方式,波特率、数据长度和校验方式根据需要设置
开启DMA,内存自增,循环方式
开启串口中断
设置好时钟
然后是 Generate Code生成代码
中断函数中,置标志位,读取SR和DR寄存器,以清除IDLE中断标识,并获得本次接收的数据长度
//485发送数据
//由于485发送数据时,接收端会同步接收到发送的数据,这会造成数据解析的错乱。
//所在在485换向之前,先关闭DMA,等发送完成,再转换到输出状态以后,再开启DMA
//也就是说,作为从机来说,不应当主动发起数据传输请求,否则当主机开始发送数据时,从机会丢失数据帧
void LL_myuart_send(u8 *pdata, u16 len)
{
HAL_UART_DMAStop(&huart1); //先关闭DMA
RS485_OUT();
HAL_Delay(1);
HAL_UART_Transmit(&huart1, pdata, len, 1000);
RS485_IN();
HAL_Delay(1);
rs485_receive_pos = 0;
rs485_receive_len = 0;
HAL_UART_Receive_DMA(&huart1,rs485_receive_data, RS485_BUF_LEN); //等发送完成,再转换到输出状态以后,再开启DMA
}
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
u8 temp;
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
//长时间未接收到数据时,会发生IDLE中断,此时意味着数据接收完成
//不同的内核,清除IDLEIE的方式不同,请查阅手册
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
{
//读SR和DR寄存器,以清除IDLE和RXNE中断标志
temp = huart1.Instance->SR;
temp = huart1.Instance->DR;
rs485_idle_flag = 1;
rs485_receive_pos += rs485_receive_len;
if(rs485_receive_pos >= RS485_BUF_LEN) //如果起始位置超出缓冲区,则减去缓冲区长度,也就意味着数据从头开始循环记录
rs485_receive_pos -= RS485_BUF_LEN;
//DMA接收串口数据,
//设置为NORMAL方式时,需要在每一次接收完成后,再次使能,否则DMA的接收缓冲区满了以后就不会再接收
//设置为CIRCLAR方式时,不需要再次使能,但编程会麻烦些
//如果长度大于缓冲区长度
if(RS485_BUF_LEN < (__HAL_DMA_GET_COUNTER(&hdma_usart1_rx) + rs485_receive_pos))
rs485_receive_len = RS485_BUF_LEN*2 - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx) - rs485_receive_pos;
else
rs485_receive_len = RS485_BUF_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx) - rs485_receive_pos;
}
/* USER CODE END USART1_IRQn 1 */
}
在主循环开始前进行初始设置
开启串口和DMA,使能IDLE中断
HAL_UART_Receive_DMA(&huart1,rs485_receive_data,RS485_BUF_LEN);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); //启动IDLE中断
主循环内,清标志位,然后发送 receive OK,由于我用的是485总线,需要进行接收和发送方式的切换,故将整个发送过程做了封装,不再具体列出。
while (1)
{
if(rs485_idle_flag != 0) //接收到一帧数据
{
rs485_idle_flag = 0;
//HAL_UART_Receive_DMA(&huart1,rs485_receive_data,RS485_BUF_LEN); //如果DMA采用NORMAL方式,需要激活本句,以使能DMA
LL_myuart_send((u8*)"receive OK\r\n", 12);
rs485_idle_flag = 0;
}
/* USER CODE END WHILE */
}
下图为缓冲区写满时,从头开始循环写入的情况
在主程序中,从rs485_receive_pos开始,读取rs485_receive_len长度的数据即可,如果超出缓冲区,则减去缓冲区长度,从头开始读取,不再列出代码。
设置为NORMAL方式时,需要在每一次接收完成后,再次使能,否则DMA只接收一次就不会再接收
设置为CIRCLAR方式时,不需要再次使能,但可能会由于各种延时导致数据来不及处理的问题
在CubeMX中需要开启串口的总中断,否则会不进中断。
对于F1系列来说,清IDLE中断标志需要读取SR和DR寄存器,否则会一直进IDLE中断。
不同的内核,清IDLE标志的方法不同,这个需要查询芯片手册
亲测航顺芯片就不兼容,需要改代码。
DMA的计数是连续的,每次接收都是在上次接收完成的位置,继续进行下一次接收,缓冲区装满时,继续从头开始接收,这给编程带来一些麻烦。
还有一种方式简单粗暴:首先接收缓冲区的长度 > 2倍的最大帧长度,然后在每次接收完成后,停止并重新开启DMA,这个过程可以放在主程序中,处理数据时进行。这样每次数据都是从缓冲区的0偏移开始接收数据,所以编程时缓冲区数据不会循环写入,编程上有便利,如果数据传输量不大,而且传输间隔也比较长,可以用这种方式。