为什么 Modbus RTU 不能用定长接收
Modbus RTU 帧长度不固定,取决于功能码和数据量。功能码 03 读取 1 个寄存器时帧长 8 字节,读取 10 个寄存器时帧长 23 字节。若使用定长接收,需要预设最大帧长,接收后判断实际长度,效率低且浪费缓冲区。
传统中断接收方式每字节触发一次中断,高波特率下 CPU 占用率高。19200 波特率接收 23 字节需 12ms,触发 23 次中断,影响其他任务执行。
DMA(Direct Memory Access,直接存储器访问)可自动搬运数据,无需 CPU 干预。配合空闲中断(IDLE)检测帧结束,实现高效的不定长接收。
USART 空闲中断原理
USART 空闲中断(IDLE Line Interrupt)在检测到总线空闲时触发。空闲定义为:在完整字节接收后,检测到连续 1 个字符时间的高电平(停止位后无起始位)。
IDLE 中断标志位(ISR 寄存器 bit 4)由硬件置位,软件需向其写 1 清除(写 0 无效)。注意与普通中断标志位(读数据寄存器自动清除)不同。
IDLE 中断特性:
- 仅在 RXNE(接收寄存器非空)为 1 时可能触发
- 触发后不会自动清除,必须手动清除
- 可配合 DMA 使用,DMA 完成后自动触发 IDLE
DMA 循环模式配置
STM32F103 的 DMA1 支持 USART1/2/3 的 DMA 请求。循环模式(Circular Mode)下,DMA 传输完成后自动重新开始,适合连续接收场景。
UART1 使用 DMA1 通道 4(RX)和通道 5(TX)。配置步骤:
#include "stm32f1xx_hal.h"
#define UART_RX_BUFFER_SIZE 256
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
uint8_t uart_rx_buffer[UART_RX_BUFFER_SIZE];
volatile uint16_t uart_rx_len = 0;
void MX_USART1_UART_Init(void) {
huart1.Instance = USART1;
huart1.Init.BaudRate = 19200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
}
void MX_DMA_Init(void) {
__HAL_RCC_DMA1_CLK_ENABLE();
HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
}
void HAL_UART_MspInit(UART_HandleTypeDef* huart) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (huart->Instance == USART1) {
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// PA9 -> USART1_TX, PA10 -> USART1_RX
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// USART1 RX DMA 配置
hdma_usart1_rx.Instance = DMA1_Channel4;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_CIRCULAR; // 循环模式
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_usart1_rx);
__HAL_LINKDMA(huart, hdmarx, hdma_usart1_rx);
// USART1 中断配置
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
}
HAL_UARTEx_ReceiveToIdle_DMA 用法
HAL 库提供 HAL_UARTEx_ReceiveToIdle_DMA 函数,自动配置 DMA 并启用 IDLE 中断。函数原型:
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(
UART_HandleTypeDef *huart,
uint8_t *pData,
uint16_t Size
);
初始化代码:
void UART_DMA_Start(void) {
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_buffer, UART_RX_BUFFER_SIZE);
// 启用 IDLE 中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
}
双缓冲策略
DMA 循环模式下,缓冲区会被覆盖。双缓冲策略使用两个缓冲区交替接收,避免数据处理期间数据丢失。
#define UART_RX_BUFFER_SIZE 256
uint8_t uart_rx_buffer[UART_RX_BUFFER_SIZE];
uint8_t uart_process_buffer[UART_RX_BUFFER_SIZE];
volatile uint16_t uart_rx_len = 0;
volatile uint8_t uart_frame_ready = 0;
// 在 IDLE 中断回调中处理
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
if (huart->Instance == USART1) {
uart_rx_len = Size;
// 复制数据到处理缓冲区
memcpy(uart_process_buffer, uart_rx_buffer, uart_rx_len);
uart_frame_ready = 1;
// 重新启动 DMA(循环模式下可省略,但确保指针复位)
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_buffer, UART_RX_BUFFER_SIZE);
}
}
接收完成回调中的帧处理
在主循环中检查帧就绪标志,处理完整帧:
void UART_Process_Frame(void) {
if (uart_frame_ready) {
uart_frame_ready = 0;
// CRC 校验
uint16_t crc_calc = CRC16_Modbus_Table(uart_process_buffer, uart_rx_len - 2);
uint16_t crc_recv = (uart_process_buffer[uart_rx_len - 1] << 8) |
uart_process_buffer[uart_rx_len - 2];
if (crc_calc == crc_recv) {
// 调用 Modbus 协议栈处理
uint8_t tx_buffer[256];
uint16_t tx_len = 0;
if (Modbus_Process_Request(uart_process_buffer, uart_rx_len,
tx_buffer, &tx_len)) {
// 发送响应
RS485_Set_Mode_TX();
HAL_UART_Transmit(&huart1, tx_buffer, tx_len, 1000);
RS485_Set_Mode_RX();
}
}
}
}
// 主循环
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
UART_DMA_Start();
while (1) {
UART_Process_Frame();
// 其他任务
}
}
中断服务函数
HAL 库的中断服务函数需在 stm32f1xx_it.c 中实现:
#include "stm32f1xx_it.h"
extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_usart1_rx;
void USART1_IRQHandler(void) {
HAL_UART_IRQHandler(&huart1);
}
void DMA1_Channel4_IRQHandler(void) {
HAL_DMA_IRQHandler(&hdma_usart1_rx);
}
完整代码示例
以下是完整的初始化和处理代码:
#include "stm32f1xx_hal.h"
#include <string.h>
#define UART_RX_BUFFER_SIZE 256
#define RS485_DE_RE_PIN GPIO_PIN_1
#define RS485_DE_RE_PORT GPIOA
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
uint8_t uart_rx_buffer[UART_RX_BUFFER_SIZE];
uint8_t uart_process_buffer[UART_RX_BUFFER_SIZE];
volatile uint16_t uart_rx_len = 0;
volatile uint8_t uart_frame_ready = 0;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_USART1_UART_Init(void);
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
// 启动 DMA 接收
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, uart_rx_buffer, UART_RX_BUFFER_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
while (1) {
if (uart_frame_ready) {
uart_frame_ready = 0;
// 处理接收到的帧
// ...
}
}
}
void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
}
static void MX_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
// RS485 DE/RE 控制
GPIO_InitStruct.Pin = RS485_DE_RE_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(RS485_DE_RE_PORT, &GPIO_InitStruct);
HAL_GPIO_WritePin(RS485_DE_RE_PORT, RS485_DE_RE_PIN, GPIO_PIN_RESET);
}
static void MX_DMA_Init(void) {
__HAL_RCC_DMA1_CLK_ENABLE();
HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
}
static void MX_USART1_UART_Init(void) {
huart1.Instance = USART1;
huart1.Init.BaudRate = 19200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
HAL_UART_Init(&huart1);
}
void HAL_UART_MspInit(UART_HandleTypeDef* huart) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (huart->Instance == USART1) {
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
hdma_usart1_rx.Instance = DMA1_Channel4;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_usart1_rx);
__HAL_LINKDMA(huart, hdmarx, hdma_usart1_rx);
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
}
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
if (huart->Instance == USART1) {
uart_rx_len = Size;
memcpy(uart_process_buffer, uart_rx_buffer, uart_rx_len);
uart_frame_ready = 1;
}
}
void RS485_Set_Mode_TX(void) {
HAL_GPIO_WritePin(RS485_DE_RE_PORT, RS485_DE_RE_PIN, GPIO_PIN_SET);
HAL_Delay(1);
}
void RS485_Set_Mode_RX(void) {
HAL_GPIO_WritePin(RS485_DE_RE_PORT, RS485_DE_RE_PIN, GPIO_PIN_RESET);
HAL_Delay(1);
}
常见问题
IDLE 中断不触发
原因:未启用 IDLE 中断或未清除标志位。
解决:
// 启用 IDLE 中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
// 在中断处理中清除标志位(HAL 库自动处理)
// 若使用寄存器操作,需手动清除:
// __HAL_UART_CLEAR_IDLEFLAG(&huart1);
DMA 接收数据不完整
原因:缓冲区大小不足或数据处理时间过长。
解决:增大缓冲区或使用双缓冲策略,确保处理时间小于帧间隔。
循环模式下数据覆盖
原因:DMA 循环写入,旧数据被新数据覆盖。
解决:在 IDLE 回调中及时复制数据,或使用双缓冲。
性能对比
| 方式 | CPU 占用 | 中断次数 | 适用场景 |
|---|---|---|---|
| 轮询 | 高 | 0 | 简单应用 |
| 字节中断 | 中 | N 字节 | 低波特率 |
| DMA+IDLE | 低 | 1 次/帧 | 高波特率、不定长 |
DMA+IDLE 方式在 19200 波特率下接收 23 字节仅触发 1 次中断,CPU 占用率降低 95% 以上,适合 Modbus RTU 等协议。