返回博客
STM32 串口 DMA + 空闲中断实现不定长数据接收

STM32 串口 DMA + 空闲中断实现不定长数据接收

2025年3月22日STM32, UART, DMA

为什么 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 等协议。