【DMA】FreeRTOS下STM32串口接收不定长数据并结合DMA实现环形缓冲区:2 串口DMA不定长接收代码实践
本文介绍了在FreeRTOS下使用STM32串口结合DMA实现环形缓冲区接收不定长数据的方法。系统采用分层设计:硬件中断层负责数据接收并存入环形缓冲区,BSP层驱动程序处理缓冲区管理并通知APP层,APP层进行数据解包处理。通过串口空闲中断、DMA半满和全满中断动态调整缓冲区head指针位置,实现高效数据接收。相比动态内存分配方案,环形缓冲区能避免内存碎片问题,保证系统实时性。文章还解答了环形缓冲
目录
3 串口空闲中断+DMA半满&全满中断+环形缓冲区实现不定长接收
BSP层:串口驱动程序(bsp_uart_driver.c)
APP层:串口解包程序(uart_parse_task.c)
3. 是否可以根据每次数据量大小来在每次接收时临时malloc内存?
4. 如果想要每次通过malloc来实现,应该怎么判断每次分配多少大小空间的内存?
0 书接上文
【DMA】FreeRTOS下STM32串口接收不定长数据并结合DMA实现环形缓冲区:1 相关函数理论部分与环形缓冲区设计
3 串口空闲中断+DMA半满&全满中断+环形缓冲区实现不定长接收
前提:
配置DMA

再使用 HAL_UARTEx_ReceiveToIdle_DMA() 开启串口空闲中断(串口驱动程序中)。

流程总览
一般来说,RTOS中设计一个串口解包过程:
数据帧格式(一个简单的协议):
帧头0xFE ... 净荷数据 ... 校验和(1字节) 帧尾:0xFF
中断(系统硬件)将数据传输完成的事情告诉串口驱动程序(前端),
串口驱动程序再通知APP解包(后端)
信息传递过程:系统硬件(中断)->前端(BSP层串口驱动程序)->后端(APP层串口解包程序)
中断(系统硬件):bsp_uart_driver.c
1、将数据存入环形缓冲区(中断内强烈不建议数据的拷贝,这里只是学习环形缓冲区演示)
2、通知前端,数据已经就绪(队列)
前端(BSP层串口驱动程序):bsp_uart_driver.c
1、buffer是否已满->停下串口(在下面的代码中没有实现这个功能)
2、将当前数据就绪的事件发送给后端
后端(APP层串口解包程序):uart_parse_task.c
1、对数据进行解包(解包状态机的实现:switch)
2、如果解包数据正确,则打印(对数据进行操作,这里仅打印)
中断部分(bsp_uart_driver.c)
自定义调用的回调函数
根据前面的知识,我们知道首先我们将串口空闲中断、DMA半满中断、DMA全满中断最终都会调用 HAL_UARTEx_RxEventCallback() 回调函数,我们可以去 stm32f4xx_hal_uart.c 文件中的串口中断服务函数、DMA半满中断服务函数、DMA全满中断服务函数那里将 HAL_UARTEx_RxEventCallback() 回调函数换成我们自定义的回调函数。



回调函数实现
环形缓冲区head指针动态偏移逻辑:

按照上面的逻辑,我们可以写出对应的回调函数:
DMA半满中断回调函数
void dma_half_irq_callback(UART_HandleTypeDef* huart, uint32_t number_of_data)
{
if (&huart1 == huart)
{
// log_i("-> dma_half_irq_callback(), data_number = [%d]", number_of_data);
// adjust head pos
uint32_t head_pos = 0;
CIR_BUF_RETURN_VALUE ret = CIR_BUF_OK;
// 获取当前head位置
ret = get_head_pos(g_cir_buffer_irq_thread, &head_pos);
// log_i("[h]head_pos = [%d]", head_pos);
if (CIR_BUF_OK != ret) {
log_e("error:[h]get_head_pos error!");
}
// 获取进入半满中断时,数据已经到达的位置:(CIRCULAR_BUFFER_SIZE / 2) - 1
// 下一个存放数据的位置:(CIRCULAR_BUFFER_SIZE / 2)
uint32_t target_data_pos = (CIRCULAR_BUFFER_SIZE / 2);
// log_i("[h]target_data_pos = [%d]", target_data_pos);
// 对head取余数,相减得到head与数据下一个位置之间的距离(即head需要偏移的距离)
// head指向下一个存放数据的位置
uint32_t pos_in_buffer = head_pos % (CIRCULAR_BUFFER_SIZE / 2);
// log_i("[h]pos_in_buffer = [%d]", pos_in_buffer);
uint32_t dis_need_increase = target_data_pos - pos_in_buffer;
// log_i("[h]dis_need_increase = [%d]", dis_need_increase);
// 偏移head
ret = head_pos_increment(g_cir_buffer_irq_thread, \
dis_need_increase);
if (CIR_BUF_OK != ret) {
log_e("[h]error:head_pos_increment error!");
}
// notify front thread
// 通知前端,数据已经就绪(消息邮箱(队列))
BaseType_t ret_1 = 0;
uint32_t send_data_to_rec_A = IRQ_SEND_TO_THREAD;
ret_1 = xQueueGenericSendFromISR(queue_uart_irq_thread, \
&send_data_to_rec_A, \
NULL, \
queueOVERWRITE);
if (pdTRUE != ret_1) {
log_e("error:dma_half_irq_callback() xQueueSendFromISR() to front queue_uart_irq_thread failed!");
}
else {
log_d("dma_half_irq_callback() xQueueSendFromISR() to front queue_uart_irq_thread success.");
}
}
}
DMA全满中断回函数
void dma_cplt_irq_callback(UART_HandleTypeDef* huart, uint32_t number_of_data)
{
if (&huart1 == huart)
{
// log_i("-> dma_cplt_irq_callback(), data_number = [%d]", number_of_data);
/* adjust head pos */
uint32_t head_pos = 0;
CIR_BUF_RETURN_VALUE ret = CIR_BUF_OK;
// 获取当前head位置
ret = get_head_pos(g_cir_buffer_irq_thread, &head_pos);
// log_i("[c]head_pos = [%d]", head_pos);
if (CIR_BUF_OK != ret) {
log_e("error:[c]get_head_pos error!");
}
// 获取进入全满中断时,数据已经到达的位置:(CIRCULAR_BUFFER_SIZE) - 1
// 下一个存放数据的位置:(CIRCULAR_BUFFER_SIZE)
uint32_t target_data_pos = (CIRCULAR_BUFFER_SIZE);
// log_i("[c]target_data_pos = [%d]", target_data_pos);
// 对head取余数,相减得到head与下一个存放数据位置之间的距离(即head需要偏移的距离)
// head指向下一个存放数据的位置
uint32_t pos_in_buffer = head_pos % (CIRCULAR_BUFFER_SIZE);
// log_i("[c]pos_in_buffer = [%d]", pos_in_buffer);
uint32_t dis_need_increase = target_data_pos - pos_in_buffer;
// log_i("[c]dis_need_increase = [%d]", dis_need_increase);
// 偏移head
ret = head_pos_increment(g_cir_buffer_irq_thread, \
dis_need_increase);
if (CIR_BUF_OK != ret) {
log_e("error:[c]head_pos_increment error!");
}
// notify front thread
// 通知前端,数据已经就绪(消息邮箱(队列))
BaseType_t ret_1 = 0;
uint32_t send_data_to_rec_A = IRQ_SEND_TO_THREAD;
ret_1 = xQueueGenericSendFromISR(queue_uart_irq_thread, \
&send_data_to_rec_A, \
NULL, \
queueOVERWRITE);
if (pdTRUE != ret_1) {
log_e("error:dma_cplt_irq_callback() xQueueSendFromISR() to front queue_uart_irq_thread failed!");
}
else {
log_d("dma_cplt_irq_callback() xQueueSendFromISR() to front queue_uart_irq_thread success.");
}
}
}
串口空闲中断回调函数
void uart_idle_irq_callback(UART_HandleTypeDef* huart, uint32_t number_of_data)
{
if (&huart1 == huart)
{
// log_i("-> uart_idle_irq_callback(), data_number = [%d]", number_of_data);
/* adjust head pos */
uint32_t head_pos = 0;
CIR_BUF_RETURN_VALUE ret = CIR_BUF_OK;
// 获取当前head位置
ret = get_head_pos(g_cir_buffer_irq_thread, &head_pos);
// log_i("[I]head_pos = [%d]", head_pos);
if (CIR_BUF_OK != ret) {
log_e("error:[I]get_head_pos error!");
}
// 获取进入空闲中断时,数据已经到达的位置:number_of_data - 1
// 下一个存放数据位置:number_of_data
uint32_t target_data_pos = number_of_data;
// log_i("[I]target_data_pos = [%d]", target_data_pos);
// 对head取余数,相减得到head与下一个存放数据位置之间的距离(即head需要偏移的距离)
uint32_t pos_in_buffer = head_pos % (CIRCULAR_BUFFER_SIZE);
// log_i("[I]pos_in_buffer = [%d]", pos_in_buffer);
uint32_t dis_need_increase = 0;
if (target_data_pos < pos_in_buffer)
{
dis_need_increase = (target_data_pos + CIRCULAR_BUFFER_SIZE) - pos_in_buffer;
}
else
{
dis_need_increase = target_data_pos - pos_in_buffer;
}
// log_i("[I]dis_need_increase = [%d]", dis_need_increase);
// 偏移head
ret = head_pos_increment(g_cir_buffer_irq_thread, \
dis_need_increase);
if (CIR_BUF_OK != ret) {
log_e("error:[I]head_pos_increment error!");
}
// notify front thread
// 通知前端,数据已经就绪(消息邮箱(队列))
BaseType_t ret_1 = 0;
uint32_t send_data_to_rec_A = IRQ_SEND_TO_THREAD;
ret_1 = xQueueGenericSendFromISR(queue_uart_irq_thread, \
&send_data_to_rec_A, \
NULL, \
queueOVERWRITE);
if (pdTRUE != ret_1) {
log_e("error:uart_idle_irq_callback() xQueueSendFromISR() to front queue_uart_irq_thread failed!");
}
else {
log_d("uart_idle_irq_callback() xQueueSendFromISR() to front queue_uart_irq_thread success.");
}
}
}
BSP层:串口驱动程序(bsp_uart_driver.c)
串口驱动程序作为一个靠近中断的任务,需要较高的任务优先级来及时响应中断的通知。
static circular_buffer_t* g_cir_buffer_irq_thread = NULL; // circular buffer
static QueueHandle_t queue_uart_irq_thread = NULL; // queue: uart(hardware) send message to front-end thread
extern QueueHandle_t queue_irq_rec_A; // queue: front-end notify back-end APP
void uart_driver_func(void* argument)
{
/* Variable */
uint32_t receive_data = 0;
// 0.alloc the ring buffer
circular_buffer_t* p_cir_buffer = creatEmptyCircularBuffer();
if (NULL == p_cir_buffer){
log_e("error: create p_cir_buffer failed!");
}
else {
log_i("create p_cir_buffer success.");
g_cir_buffer_irq_thread = p_cir_buffer;
}
// 1.create queue for UART(hardware) sending message to me
queue_uart_irq_thread = xQueueCreate(1, 4);
if (NULL == queue_uart_irq_thread) {
log_e("error:queue_uart_irq_thread create failed!");
}
else {
log_i("queue_uart_irq_thread create success.");
}
// 2.start uart receive
#if 1 // receive_DMA
if (HAL_OK == HAL_UARTEx_ReceiveToIdle_DMA(&huart1, g_cir_buffer_irq_thread->data, 10)){
log_i("HAL_UART_rec_Idle_DMA init success.");
}
else {
log_e("error:HAL_UART_rec_Idle_DMA init failed!");
}
#endif // end of receive_DMA
for (;;)
{
// 3.判断是否有中断发来队列通知环形缓冲区已有数据
xQueueReceive(queue_uart_irq_thread, &receive_data, portMAX_DELAY);
log_i("irq2front queue_uart_irq_thread receive_data = [%x]", receive_data);
if (IRQ_SEND_TO_THREAD == receive_data)
{
// 4.将当前数据就绪的事件发送给后端
BaseType_t ret_queue = 0;
uint32_t send_to_end = FRONT_SEND_TO_END;
ret_queue = xQueueGenericSend(queue_irq_rec_A, \
& send_to_end, \
0, \
queueOVERWRITE);
if (pdTRUE != ret_queue) {
log_e("error:xQueueSend() front to end queue_irq_rec_A failed!");
}
else {
log_d("xQueueSend() front to end queue_irq_rec_A success.");
}
}
//osDelay(1);
}
}
环形缓冲区指针 g_cir_buffer_irq_thread 作为一个定义在 bsp_uart_drivere.c 的静态变量,我们需要在 uart_parse_task.c 文件中拿到环形缓冲区指针对缓冲区内数据进行解包,但是强烈不建议直接在 bsp_uart_drivere.h 头文件中将 g_cir_buffer_irq_thread 变量 extern 出去,我们要遵循最小可见原则,用一个公共接口将 g_cir_buffer_irq_thread 的值传递出去:

/* pass the circular_buffer pointer to external caller */
/* return:
HAL_OK,
HAL_ERROR */
HAL_StatusTypeDef get_circular_buffer(circular_buffer_t** circular_buffer)
{
if (NULL != g_cir_buffer_irq_thread)
{
*circular_buffer = g_cir_buffer_irq_thread;
return HAL_OK;
}
else {
*circular_buffer = NULL;
return HAL_ERROR;
}
}
APP层:串口解包程序(uart_parse_task.c)
extern UART_HandleTypeDef huart1;
QueueHandle_t queue_irq_rec_A = NULL; // queue: front-end notify back-end APP
static circular_buffer_t* g_circular_buffer_from_driver = NULL; // circular buffer
void uart_rec_A_func(void* argument)
{
/* Variable */
uint32_t receive_data = 0;
// create queue
queue_irq_rec_A = NULL;
queue_irq_rec_A = xQueueCreate(1, 4);
if (NULL == queue_irq_rec_A){
log_i("queue_irq_rec_A init failed!");
}
else{
log_i("queue_irq_rec_A init success.");
log_i("p_queue_irq_rec_A = [%x]",queue_irq_rec_A);
}
get_front_circular_buffer:
if (HAL_ERROR == get_circular_buffer(&g_circular_buffer_from_driver)) {
log_e("error:get_circular_buffer failed!");
osDelay(1);
goto get_front_circular_buffer;
}
log_i("get_circular_buffer success.");
for (;;)
{
xQueueReceive(queue_irq_rec_A, &receive_data, portMAX_DELAY);
log_i("queue_irq_rec_A receive_data = [%x]", receive_data);
if (FRONT_SEND_TO_END == receive_data)
{
/*
APP解析固件包过程中,进行校验和检查
零、进入帧内净荷数据
一、从包头开始进行叠加每个数据
二、运行数据解析状态机
*/
// 零、进入环形缓冲区进行帧内净荷数据
if (NULL == g_circular_buffer_from_driver) {
log_e("error: NULL pointer");
return;
}
while (BUFFER_IS_EMPTY != buffer_is_empty(g_circular_buffer_from_driver))
{
static uint8_t temp_data_array[20] = 0;
static uint8_t data_counter = 0;
uint8_t cir_buf_data = 0;
if (CIR_BUF_OK == get_data(g_circular_buffer_from_driver, &cir_buf_data)) {
log_i("read out data success from APP, cir_buf_data = [%d]", cir_buf_data);
}
// Parse data
// 检测到帧尾之前,将数据全部暂存
// 检测到帧尾之后,开始计算校验和,若相等,则输出
static FRAME_STATUS status = FRAME_NOT_DETECTED; // parse data status
switch (status)
{
case FRAME_NOT_DETECTED:
// check if FRAME_HEAD is comming
if (FRAME_HEAD_FLAG == cir_buf_data)
{
status = FRAME_HEAD;
log_i("start parse uart data");
}
break;
case FRAME_HEAD:
if (FRAME_END_FLAG == cir_buf_data)
{
status = FRAME_END;
log_i("END");
// 计算校验和
uint32_t data_rec_sum = temp_data_array[data_counter - 1];
uint32_t data_cal_sum = 0;
log_i("data_rec_sum = [%d]", data_rec_sum);
for (int i = 0;i < data_counter - 1;++i)
{
data_cal_sum += temp_data_array[i];
}
log_i("data_cal_sum = [%d]", data_cal_sum);
if (data_cal_sum == data_rec_sum)
{
// 打印净荷数据
for (int i = 0;i < data_counter - 1;++i)
{
log_i("Payload[%d] = [%d]", i, temp_data_array[i]);
}
}
else
{
log_e("error:data_cal_sum error!");
}
for (int i = 0;i < data_counter;++i)
{
temp_data_array[i] = 0;
}
data_counter = 0;
status = FRAME_NOT_DETECTED;
}
else
{
temp_data_array[data_counter] = cir_buf_data;
++data_counter;
log_i("data_counter = [%d]", data_counter);
//log_i("Payload: [%d]", cir_buf_data);
}
break;
// case FRAME_END:
// break;
// default:
// break;
}
}
}
osDelay(1);
}
}
总结
创建一个环形缓冲区,两个指针( head 和 tail )可以在环形缓冲区上一直向一个方向移动,启用串口DMA连续接收,数据会被不断地、连续地接收到缓冲区内,在DMA半满中断、DMA全满中断和串口空闲中断中根据DMA当前搬运数量调整 head 指针的位置,而 APP(串口解包程序) 控制 tail 指针,持续解包数据。
在这个过程中,head 指针的移动需要借助中断回调函数,所以 head 指针的位置是 “一抽一抽” 地动的,而 tail 指针是 APP 解一个字节的数据就递增一次,是 “连续” 地动的。
4 Q&A
1. 为什么串口需要环形缓冲区?
串口接收数据的速度可能比处理数据的速度更快,使用环形缓冲区可以让处理器有足够的时间来处理已接收的数据,否则可能导致串口数据丢失,并且环形缓冲区是重复使用同一块内存,不需要频繁的内存分配和释放,利用率更高
2. 串口的单次最大接收数据量是否需要限制?
需要限制,因为串口接收缓冲区大小有限,如果单次接收数据量大于缓冲区大小,也可能会丢失数据,并且处理器处理能力也有限,即使数据量小于缓冲区,但是较大的数据量也会占用处理器较长的时间,如果处理数据任务优先级较高,则会让较低优先级任务得不到运行,影响系统实时性
3. 是否可以根据每次数据量大小来在每次接收时临时malloc内存?
可以,但是这种方法存在风险,动态内存分配和释放操作的时间是不确定的,较长的时间可能影响系统实时性,并且频繁动态内存分配和释放可能会导致内存碎片问题,严重情况下无法分配到内存,这将会导致导致数据全部丢失
4. 如果想要每次通过malloc来实现,应该怎么判断每次分配多少大小空间的内存?
通过包头和数据长度来计算需要的空间大小,再malloc。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)