UART串口高效接收方案:DMA + IDLE中断 + Modbus解析
UART串口高效接收方案:DMA + IDLE中断 + Modbus解析
基于USART+自定义协议+DMA+STM32F407VE+CubeMX+FreeRTOS
1 CubeMX配置设置
1.1 RCC 时钟配置


1.2 SYS 配置

用到了FreeRTOS,采用定时器的方式。
1.3 USART1 配置

添加DMA;
开启中断;
1.4 FREERTOS 配置

Configuration全部默认;
1.5 GPIO 配置
串口使用RS485的通讯方式:PA9(USART1_TX)、PA10(USART_RX)、PC8(RS485_EN)

配置完成生成代码;
2 串口代码 UART + DMA + IDLE 模式
前置代码说明,下面内容会涉及:
#define MODBUS_RX_MAX_LEN 32
typedef struct
{
//作为从机时使用
uint8_t myadd; //本设备从机地址
uint8_t rcbuf[MODBUS_RX_MAX_LEN]; //modbus接受缓冲区
uint8_t recount; //modbus端口接收到的数据个数
uint8_t reflag; //modbus一帧数据接受完成标志位
uint8_t sendbuf[MODBUS_RX_MAX_LEN]; //modbus接发送缓冲区
}MODBUS;
协议由MODBUS的基础上进行的自定义更改,命名保留了modbus,可以更改其它命名;
2.1 usart.h
#define RS485DIR_TX HAL_GPIO_WritePin(RS485_EN_GPIO_Port, RS485_EN_Pin, GPIO_PIN_SET);
#define RS485DIR_RX HAL_GPIO_WritePin(RS485_EN_GPIO_Port, RS485_EN_Pin, GPIO_PIN_RESET);
2.2 usart.c
在MX_USART1_UART_Init中只需要添加两行代码
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 开启IDLE中断
HAL_UART_Receive_DMA(&huart1, modbus.rcbuf, MODBUS_RX_MAX_LEN); // 启动DMA接收
作用解释:__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE)
启用 UART 空闲中断(IDLE),当串口在一段时间内未接收到新数据时会触发此中断,常用于判断一帧数据是否接收完毕。HAL_UART_Receive_DMA
启动 DMA 接收机制,将接收到的数据直接写入 modbus.rcbuf 缓冲区,提高数据传输效率,降低 CPU 占用率。
2.3 stm32f4xx_it.c
在串口1的中断里添加以下代码
代码块
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))// 检测是否是 IDLE 中断
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);// 清除 IDLE 中断标志
HAL_UART_DMAStop(&huart1);// 停止当前 DMA 接收
// 计算接收到的数据长度 = 总长度 - 剩余未传输的长度
uint16_t len = MODBUS_RX_MAX_LEN - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
modbus.recount = len; // 保存接收长度
modbus.reflag = 1; // 设置“接收完成”标志
// 重新启动 DMA 接收,准备接收下一帧
HAL_UART_Receive_DMA(&huart1, modbus.rcbuf, MODBUS_RX_MAX_LEN);
}
/* USER CODE END USART1_IRQn 1 */
}
作用解释:
在 USART1_IRQHandler 中我们处理 UART 的空闲中断,用于识别一帧 Modbus 数据的接收完成。此方式无需在每接收一个字节时触发中断,结合 DMA 可以大幅降低 CPU 占用率,适合实时性要求高的场合。
为什么采用 UART + DMA + IDLE 模式
在初期的串口通信实现中,我们曾使用传统的串口中断方式逐字节接收数据(即每接收一个字节触发一次中断)。然而,在实际运行中发现:
- 当主站发送的数据量较大、发送频率较高时,
- 串口接收中断频繁触发,导致 MCU 负担加重,
- 在某些情况下甚至会出现程序跑飞或串口彻底失效的问题(不再进入中断,数据丢失)。
为了解决这一问题,我们引入了 DMA(直接内存访问)机制 来接管接收过程,配合 UART 的 空闲中断(IDLE) 来判断一帧数据的结束,从而大大提高了通信的稳定性和效率。
此种方式既避免了频繁中断的性能瓶颈,也简化了数据帧的接收判断逻辑,适合用于 Modbus 等典型的一帧一处理通信模式。
3 基于MODBUS自定义协议代码
3.1 rs485.h
为实现 基于MODBUS自定义协议在 RS-485 总线下的从机通信,本模块定义了两个核心结构体:HOLDREG 和 MODBUS,用于数据映射与串口接收处理。
#define MODBUS_RX_MAX_LEN 32
定义最大接收缓冲长度为 32 字节,适用于典型的 Modbus 帧长度(可根据协议指令与寄存器数量调整)。
typedef struct
{
uint16_t HoldRegAddr; //地址
uint16_t HoldRegData; //数据
uint16_t safeLevel; //读写权限
}HOLDREG;
HoldRegAddr:保持寄存器的地址,对应主站读写的目标地址;HoldRegData:该寄存器当前值;safeLevel:权限控制字段,可自定义标记为只读(RO)、可读写(RW)等安全等级。
typedef struct
{
//作为从机时使用
uint8_t myadd; //本设备从机地址
uint8_t rcbuf[MODBUS_RX_MAX_LEN]; //modbus接受缓冲区
uint8_t recount; //modbus端口接收到的数据个数
uint8_t reflag; //modbus一帧数据接受完成标志位
uint8_t sendbuf[MODBUS_RX_MAX_LEN]; //modbus接发送缓冲区
}MODBUS;
extern MODBUS modbus;
extern HOLDREG Reg[];
myadd:本设备的从机地址(Modbus RTU 协议中 1~247);rcbuf[]:DMA 接收缓存区;recount:记录实际接收的数据长度;reflag:当接收到完整的一帧数据后置 1,主循环/任务中处理;sendbuf[]:准备发送的应答数据缓存。
3.2 rs485.c
本项目基于标准 Modbus RTU 协议进行改造,实现了从机设备在 RS-485 总线下的数据接收、校验、写入和回复等功能。
全局变量定义
MODBUS modbus; // 接收处理核心结构体
uint8_t modbus_slave_addr = 0x40; // 本设备的从机地址
uint8_t modbus_Tx_buff[32]; // 数据回复缓冲区
寄存器映射区初始化
HOLDREG Reg[]= {{0x0000, 0x00, 0},
{0x0001, 0x00, 0},
{0x0002, 0x00, 0},
{0x0003, 0x00, 0},
{0x0004, 0x00, 0},
{0x0005, 0x00, 0},
{0x0006, 0x00, 0},
{0x0007, 0x00, 0},
{0x0008, 0x00, 0},
{0x0009, 0x00, 0},
{0x000A, 0x00, 0},
{0x000B, 0x00, 0}
};
系统预定义了一组保持寄存器(HOLDREG),每个寄存器包含地址、值、权限字段。主站通过功能码可以写入这些寄存器值。
从机发送函数
void modbus_send_data(uint8_t *buff, uint8_t len)
{
RS485DIR_TX;
HAL_Delay(1);
HAL_UART_Transmit_DMA(&huart1, buff, len);
HAL_Delay(1);
RS485DIR_RX;
}
RS485DIR_TX/RS485DIR_RX:控制 485 总线方向,GPIO:PC8;
使用 DMA 非阻塞发送数据,提高性能;
发完数据后及时切换为接收模式。
modbus_service:帧接收 + 校验 + 功能码解析
void modbus_service(void)
{
if(modbus.reflag == 0) return;
CRC_check_result = CRC16_XMODEM_T(&modbus.rcbuf[0], modbus.recount-3);
data_CRC_value = ((modbus.rcbuf[modbus.recount-3]<<8)) | modbus.rcbuf[modbus.recount-2];
frame_end = modbus.rcbuf[modbus.rcbuf[2] + 5];
if (CRC_check_result == data_CRC_value && frame_end == 0xEE)
{
if (modbus.rcbuf[1] == modbus_slave_addr)
{
modbus_16_function(); // 功能码 16:写多个寄存器
}
}
modbus.recount = 0;
modbus.reflag = 0;
}
数据格式
| 字节 | 含义 |
|---|---|
| 0 | 帧头 |
| 1 | 从机地址 |
| 2 | 数据长度 |
| 3~n | 有效数据 |
| n+1 | CRC 高位 |
| n+2 | CRC 低位 |
| n+3 | 帧尾 0xEE |
功能码16:写多个保持寄存器
void modbus_16_function(void)
{
uint16_t register_num = modbus.rcbuf[2];
for(uint16_t i = 0; i < register_num; i++)
{
Reg[i].HoldRegData = modbus.rcbuf[3+i]; // 写入数据
}
// 回复帧
modbus_Tx_buff[0] = modbus.rcbuf[0]; // 帧头
modbus_Tx_buff[1] = modbus.rcbuf[1]; // 从机地址
modbus_Tx_buff[2] = modbus.rcbuf[2]; // 长度
uint16_t crc = CRC16_XMODEM_T(modbus_Tx_buff, 3);
modbus_Tx_buff[3] = (crc >> 8) & 0xFF;
modbus_Tx_buff[4] = crc & 0xFF;
modbus_Tx_buff[5] = modbus.rcbuf[register_num + 3 + 2]; // 原始帧尾
modbus_send_data(modbus_Tx_buff, 6);
}
注意 有效数据是8位一个字节的数据,要想16位2个字节,可按需扩展rcbuf[3] << 8 | rcbuf[4]
CRC16_XMODEM //CRC校验方式
4 FreeRTOS代码
由于使用的是CubeMX生成,用起来不是很方便,所以只是生成部分代码内容,接下来要进行修改自动生成的初始代码;
4.1 freertos.c
/* USER CODE BEGIN Header */
/**
******************************************************************************
* File Name : freertos.c
* Description : Code for freertos applications
******************************************************************************
* @attention
*
* Copyright (c) 2025 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "rs485.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* START_TASK 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define START_TASK_PRIO 1 /* 任务优先级 */
#define START_TASK_STACK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t start_task_handler; /* 任务句柄 */
void start_task( void * pvParameters ); /* 任务函数 */
/* TASK1 任务 用来通信
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define TASK1_PRIO 2 /* 任务优先级 */
#define TASK1_STACK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t task1_handler; /* 任务句柄 */
void task1( void * pvParameters ); /* 任务函数 */
/* TASK2 任务 LED_RUN
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define TASK2_PRIO 3 /* 任务优先级 */
#define TASK2_STACK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t task2_handler; /* 任务句柄 */
void task2( void * pvParameters ); /* 任务函数 */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
#if 0
/* USER CODE END Variables */
osThreadId defaultTaskHandle;
/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */
// 在 void StartDefaultTask(void const * argument); 下一行添加 #endif
/* USER CODE END FunctionPrototypes */
void StartDefaultTask(void const * argument);
#endif
void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) */
/* GetIdleTaskMemory prototype (linked to static allocation support) */
void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize );
/* USER CODE BEGIN GET_IDLE_TASK_MEMORY */
static StaticTask_t xIdleTaskTCBBuffer;
static StackType_t xIdleStack[configMINIMAL_STACK_SIZE];
void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer, uint32_t *pulIdleTaskStackSize )
{
*ppxIdleTaskTCBBuffer = &xIdleTaskTCBBuffer;
*ppxIdleTaskStackBuffer = &xIdleStack[0];
*pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
/* place for user code */
}
/* USER CODE END GET_IDLE_TASK_MEMORY */
/**
* @brief FreeRTOS initialization
* @param None
* @retval None
*/
void MX_FREERTOS_Init(void) {
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* USER CODE BEGIN RTOS_MUTEX */
/* add mutexes, ... */
/* USER CODE END RTOS_MUTEX */
/* USER CODE BEGIN RTOS_SEMAPHORES */
/* add semaphores, ... */
/* USER CODE END RTOS_SEMAPHORES */
/* USER CODE BEGIN RTOS_TIMERS */
/* start timers, add new ones, ... */
/* USER CODE END RTOS_TIMERS */
/* USER CODE BEGIN RTOS_QUEUES */
/* add queues, ... */
#if 0
/* USER CODE END RTOS_QUEUES */
/* Create the thread(s) */
/* definition and creation of defaultTask */
osThreadDef(defaultTask, StartDefaultTask, osPriorityNormal, 0, 128);
defaultTaskHandle = osThreadCreate(osThread(defaultTask), NULL);
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
#endif
xTaskCreate((TaskFunction_t ) start_task,
(char * ) "start_task",
(configSTACK_DEPTH_TYPE ) START_TASK_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) START_TASK_PRIO,
(TaskHandle_t * ) &start_task_handler );
/* USER CODE END RTOS_THREADS */
}
/* USER CODE BEGIN Header_StartDefaultTask */
#if 0
/**
* @brief Function implementing the defaultTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */
for(;;)
{
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
#endif
void start_task( void * pvParameters )
{
taskENTER_CRITICAL(); /* 进入临界区 */
xTaskCreate((TaskFunction_t ) task1,
(char * ) "task1",
(configSTACK_DEPTH_TYPE ) TASK1_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) TASK1_PRIO,
(TaskHandle_t * ) &task1_handler );
xTaskCreate((TaskFunction_t ) task2,
(char * ) "task2",
(configSTACK_DEPTH_TYPE ) TASK2_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) TASK2_PRIO,
(TaskHandle_t * ) &task2_handler );
vTaskDelete(start_task_handler);
taskEXIT_CRITICAL(); /* 退出临界区 */
}
void task1( void * pvParameters )
{
while(1)
{
modbus_service(); //从机
vTaskDelay(1000);
}
}
void task2( void * pvParameters )
{
while(1)
{
HAL_GPIO_TogglePin(LED_RUN_GPIO_Port,LED_RUN_Pin);
vTaskDelay(1000);
}
}
/* USER CODE END Application */
注意 每次使用CubeMX生成代码 都要在void StartDefaultTask(void const * argument); 下一行添加 #endif
5 通信测试
5.1 串口调试助手–串口设置

5.2 接收设置

5.3 发送设置

5.3.1 附加位设置

5.4 数据日志

根据 数据格式 分析:
5.4.1 主机发送数据
帧头 0xAA、从机地址 0x40、数据长度 0x05、有效数据 0x68 0x63 0x67 0x69 0xDC、CRC高位 0xF5、CRC低位 0x3F、帧尾 0xEE。
5.4.1从机返还数据
帧头 0xAA、从机地址 0x40、数据长度 0x05、CRC高位 0x27、CRC低位 0x34、帧尾 0xEE。
6 Keil5 调试
使用串口调试助手发送数据,在keil中查看modbus.rcbuf接收缓存
接收到的数据正常
将有效数据保存到Reg.HoldRegData查看
正常保存
附录
crc.c
unsigned short CRC16_XMODEM_T(unsigned char *ptr, unsigned int len)
{
const unsigned int crc_table[256] = {
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
};
unsigned short crc = 0x0000;
while(len--)
{
crc = (crc << 8) ^ crc_table[(crc >> 8 ^ *ptr++) & 0xff];
}
return(crc);
}
END
更多推荐


所有评论(0)