七、基于HAL库,实现串口+DMA+状态机通信实现
基于HAL库的串口+DMA+状态机通信实现
在嵌入式系统中,实现可靠的串口通信至关重要。结合STM32 HAL库的串口、DMA功能和状态机设计模式,可以高效处理上位机发送的指令帧。下面我将详细介绍如何实现这个功能。
一、系统架构设计
我们将构建一个基于以下组件的通信系统:
- UART:负责物理层数据传输
- DMA:实现数据的零拷贝接收,减少CPU干预
- 状态机:解析和验证指令帧格式
- 环形缓冲区:存储接收到的数据,提供线程安全访问
系统工作流程:
- 配置UART和DMA,启动连续接收
- 当接收到数据时,DMA自动将数据存入缓冲区
- 状态机在后台解析缓冲区数据,识别完整指令帧
- 应用层处理解析后的指令
二、代码实现
以下是完整的实现代码:
#include "stm32f4xx_hal.h"
#include <string.h>/* 定义指令帧相关常量 */
#define FRAME_HEADER1 0xA5
#define FRAME_HEADER2 0x5A
#define FRAME_TAIL1 0x0D
#define FRAME_TAIL2 0x0A
#define MAX_FRAME_LENGTH 64
#define RX_BUFFER_SIZE 128/* 帧状态机枚举 */
typedef enum {STATE_HEADER1, // 等待帧头1 (0xA5)STATE_HEADER2, // 等待帧头2 (0x5A)STATE_LENGTH, // 等待长度字节STATE_DATA, // 接收数据STATE_TAIL1, // 等待帧尾1 (0x0D)STATE_TAIL2 // 等待帧尾2 (0x0A)
} FrameState;/* 全局变量 */
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;uint8_t rx_dma_buffer[RX_BUFFER_SIZE]; // DMA接收缓冲区
uint8_t rx_frame_buffer[MAX_FRAME_LENGTH]; // 帧解析缓冲区uint32_t rx_dma_index = 0; // DMA缓冲区当前索引
uint32_t rx_dma_prev_index = 0; // 上次处理的DMA缓冲区索引
uint8_t frame_received = 0; // 帧接收完成标志
uint8_t frame_length = 0; // 当前帧长度
FrameState current_state = STATE_HEADER1; // 当前状态机状态/* 函数声明 */
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_USART1_UART_Init(void);
void process_received_frame(void);/* 初始化函数 */
int main(void)
{HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_DMA_Init();MX_USART1_UART_Init();/* 启动DMA接收 */HAL_UART_Receive_DMA(&huart1, rx_dma_buffer, RX_BUFFER_SIZE);while (1){/* 处理接收到的帧 */if (frame_received) {process_received_frame();frame_received = 0;}/* 其他主循环任务 */// ...}
}/* 处理接收到的完整帧 */
void process_received_frame(void)
{/* 验证帧长度 */if (frame_length < 4) { // 至少包含帧头、长度和帧尾return;}/* 解析指令类型 */uint8_t command = rx_frame_buffer[0];uint8_t param = rx_frame_buffer[1];/* 根据指令类型执行相应操作 */switch (command) {case 0x02:// 处理指令0x02break;// 其他指令处理...default:// 未知指令break;}
}/* 串口接收回调函数 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{if (huart->Instance == USART1) {/* 获取当前DMA缓冲区索引 */uint32_t current_index = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);/* 处理新接收到的数据 */while (rx_dma_prev_index != current_index) {uint8_t data = rx_dma_buffer[rx_dma_prev_index];/* 状态机处理接收到的字节 */switch (current_state) {case STATE_HEADER1:if (data == FRAME_HEADER1) {current_state = STATE_HEADER2;frame_length = 0;}break;case STATE_HEADER2:if (data == FRAME_HEADER2) {current_state = STATE_LENGTH;} else {current_state = STATE_HEADER1; // 帧头错误,重新开始}break;case STATE_LENGTH:if (data <= MAX_FRAME_LENGTH - 4) { // 确保帧长度合理frame_length = data;current_state = STATE_DATA;} else {current_state = STATE_HEADER1; // 长度错误,重新开始}break;case STATE_DATA:if (frame_length > 0) {rx_frame_buffer[frame_length - data] = data; // 存储数据frame_length--;if (frame_length == 0) { // 数据接收完毕,等待帧尾current_state = STATE_TAIL1;}} else {current_state = STATE_HEADER1; // 数据长度异常,重新开始}break;case STATE_TAIL1:if (data == FRAME_TAIL1) {current_state = STATE_TAIL2;} else {current_state = STATE_HEADER1; // 帧尾错误,重新开始}break;case STATE_TAIL2:if (data == FRAME_TAIL2) {/* 完整帧接收成功 */frame_received = 1;}current_state = STATE_HEADER1; // 无论是否成功,都重新开始break;}/* 更新索引 */rx_dma_prev_index = (rx_dma_prev_index + 1) % RX_BUFFER_SIZE;}/* 重新启动DMA接收 */HAL_UART_Receive_DMA(&huart1, rx_dma_buffer, RX_BUFFER_SIZE);}
}/* 错误处理回调 */
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{if (huart->Instance == USART1) {/* 处理UART错误 */uint32_t error = HAL_UART_GetError(huart);/* 清除错误并重新启动DMA接收 */__HAL_UART_CLEAR_OREFLAG(huart);HAL_UART_Receive_DMA(&huart1, rx_dma_buffer, RX_BUFFER_SIZE);}
}/* 初始化函数实现 (由CubeMX生成) */
static void MX_USART1_UART_Init(void)
{huart1.Instance = USART1;huart1.Init.BaudRate = 115200;huart1.Init.WordLength = UART_WORDLENGTH_8B;huart1.Init.StopBits = UART_STOPBITS_1;huart1.Init.Parity = UART_PARITY_NONE;huart1.Init.Mode = UART_MODE_RX;huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;huart1.Init.OverSampling = UART_OVERSAMPLING_16;if (HAL_UART_Init(&huart1) != HAL_OK){Error_Handler();}
}static void MX_DMA_Init(void)
{__HAL_RCC_DMA2_CLK_ENABLE();HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 0, 0);HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);hdma_usart1_rx.Instance = DMA2_Stream2;hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;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_LOW;hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK){Error_Handler();}__HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_rx);
}
三、实现要点解析
-
DMA环形缓冲区配置:
- 使用DMA_CIRCULAR模式实现无限接收
- 通过比较当前DMA索引和上次处理索引来确定新数据
-
状态机设计:
- 6个状态:等待帧头、接收长度、接收数据、验证帧尾
- 每个状态处理特定字节并转移到下一状态
- 任何错误都会导致状态机重置
-
帧解析逻辑:
- 帧头验证:连续接收到0xA5和0x5A
- 长度验证:确保数据长度在合理范围内
- 帧尾验证:确保以0x0D和0x0A结束
-
错误处理:
- 实现UART错误回调函数处理溢出等错误
- 重置DMA接收以恢复通信
四、优化建议
-
线程安全:
- 如果在中断和主循环中访问共享资源,建议添加临界区保护
- 例如在处理frame_received标志时禁用中断
-
扩展功能:
- 添加CRC校验增强数据可靠性
- 实现指令超时检测
- 支持多指令并发处理
-
性能优化:
- 使用双缓冲区减少数据处理延迟
- 优化状态机转移逻辑提高解析效率
这种设计结合了DMA的高效数据传输能力和状态机的灵活解析能力,能够可靠地接收和解析上位机发送的指令帧,同时最大限度减少CPU占用,是嵌入式系统串口通信的理想解决方案。