STM32 陶晶驰串口屏驱动设计文档
1. 特点
- 基于STM32HAL实现
- 零拷贝、非阻塞接收
- TJC 协议支持
- Ring Buffer + DMA Idle
2. 整体架构
上层应用 (app_main)
│
├── HMI_Printf() ──→ 格式化 + 追加 0xFF 0xFF 0xFF ──→ HAL_UART_Transmit (阻塞)
│
└── 轮询 tjc_rx_flag / ble_rx_flag
│
└── HMI_Get_Count() + HMI_Peek() + HMI_Delete()
│
└── 环形缓冲区 (DMA 自动填充)
- 接收路径:DMA Circular → Idle 事件 → RxEventCallback → 置标志 → 上层轮询处理
- 发送路径:HMI_Printf → vsnprintf → 追加结束符 → 阻塞发送
- 缓冲区:两个独立 Ring Buffer(tjc_rb / ble_rb),完全隔离,避免相互干扰。
3. 环形缓冲区数据结构
typedef struct {
volatile uint16_t head; // 软件读指针
uint8_t data[RINGBUFF_LEN]; // 实际缓冲区
} HMI_Buffer_t;
DMA_BUFFER HMI_Buffer_t tjc_rb; // 放在 .dma_buffer 段
DMA_BUFFER uint8_t tjc_tx_buf[TX_TEMP_LEN];
head为 volatile:防止编译器优化,导致上层读不到最新值。
4. 核心函数设计与代码深度解析
4.1 HMI_Init() —— 初始化
void HMI_Init(void)
{
tjc_rb.head = 0;
ble_rb.head = 0;
HAL_UARTEx_ReceiveToIdle_DMA(tjc_huart, tjc_rb.data, RINGBUFF_LEN);
HAL_UARTEx_ReceiveToIdle_DMA(ble_huart, ble_rb.data, RINGBUFF_LEN);
__HAL_DMA_DISABLE_IT(tjc_huart->hdmarx, DMA_IT_HT);
__HAL_DMA_DISABLE_IT(ble_huart->hdmarx, DMA_IT_HT);
}
设计亮点:
- 使用
ReceiveToIdle_DMA而不是普通Receive_DMA:只有总线空闲(帧结束)才产生事件,可以处理不定长数据帧。 - 主动禁用 Half Transfer 中断:HT 中断在环形缓冲区中完全无用,反而会频繁触发浪费 CPU。代码显式关闭,减少中断噪声。
- 只清
head,不清data:DMA 会持续覆盖,无需浪费时间。
4.2 HMI_Get_Count() —— 环形缓冲区长度计算
uint16_t HMI_Get_Count(HMI_Buffer_t *rb, UART_HandleTypeDef *huart)
{
uint16_t tail = RINGBUFF_LEN - __HAL_DMA_GET_COUNTER(huart->hdmarx);
return (tail - rb->head + RINGBUFF_LEN) % RINGBUFF_LEN;
}
- DMA 计数器
NDTR是剩余要接收的字节数(从 RINGBUFF_LEN 倒计数)。 - 所以
tail = RINGBUFF_LEN - NDTR就是 DMA 当前写入的位置。 (tail - head + RINGBUFF_LEN) % RINGBUFF_LEN是经典环形缓冲区可用长度计算公式,同时处理 head > tail 的回绕情况。
4.3 HMI_Delete() 与 HMI_Peek() —— 零拷贝协议解析基础
HMI_Delete:仅移动head,不拷贝任何数据。HMI_Peek:通过(head + index) % RINGBUFF_LEN实现逻辑连续读取。- 上层可以直接在环形缓冲区上做状态机解析(例如查找 TJC 的 0xFF 0xFF 0xFF),无需先 memcpy 到线性缓冲区。
4.4 HMI_Printf() —— 发送核心
void HMI_Printf(UART_HandleTypeDef* huart, const char* fmt, ...)
{
uint8_t* target_buf = (huart->Instance == tjc_huart->Instance) ? tjc_tx_buf : ble_tx_buf;
if (huart->gState == HAL_UART_STATE_BUSY_TX) return; // 防重入
va_list args;
va_start(args, fmt);
int len = vsnprintf((char*)target_buf, TX_TEMP_LEN - 3, fmt, args);
va_end(args);
if (len <= 0) return;
if (len > TX_TEMP_LEN - 3) len = TX_TEMP_LEN - 3;
target_buf[len++] = 0xFF; // TJC 协议强制要求
target_buf[len++] = 0xFF;
target_buf[len++] = 0xFF;
HAL_UART_Transmit(huart, target_buf, len, HAL_MAX_DELAY);
}
- TX_TEMP_LEN-3 预留:保证 0xFF 结束符永远有空间。
- BUSY_TX 检查:TJC 屏幕对指令连续性要求极高,半途被打断会导致指令解析失败。
- 阻塞式发送:对于指令类通信(几十字节)足够,简单可靠。
- 支持浮点:需要开启
-u _printf_float
5. 接收回调机制
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef* huart, uint16_t Size)
{
if (huart->Instance == tjc_huart->Instance){
tjc_rx_flag = 1;
}
else if (huart->Instance == ble_huart->Instance){
ble_rx_flag = 1;
}
}
回调函数仅仅修改标志位,操作放到主函数完成
7. 注意事项
- 必须在 CubeMX 中将 RX DMA 设为 Circular,否则需要重复开关DMA
- RINGBUFF_LEN 必须是 2 的幂(256/512/1024),虽然代码支持任意值,但性能最优。
- 处理完数据之后必须调用 HMI_Delete,否则环形缓冲区会“假满”,后续数据丢失。
8.CubeMX 配置及完整代码
- CubeMX 配置

- tjc_usart_hmi.c
#include "tjc_usart_hmi.h"
#include "usart.h"
#include <stdio.h>
#include <stdarg.h>
UART_HandleTypeDef *tjc_huart = &huart4;
UART_HandleTypeDef *ble_huart = &huart2;
DMA_BUFFER HMI_Buffer_t tjc_rb;
DMA_BUFFER HMI_Buffer_t ble_rb;
volatile uint8_t tjc_rx_flag = 0;
volatile uint8_t ble_rx_flag = 0;
DMA_BUFFER uint8_t tjc_tx_buf[TX_TEMP_LEN];
DMA_BUFFER uint8_t ble_tx_buf[TX_TEMP_LEN];
/**
* @brief 初始化 HMI UART 和环形缓冲区
*/
void HMI_Init(void)
{
tjc_rb.head = 0;
ble_rb.head = 0;
// RX 必须是 Circular 模式
HAL_UARTEx_ReceiveToIdle_DMA(tjc_huart, tjc_rb.data, RINGBUFF_LEN);
HAL_UARTEx_ReceiveToIdle_DMA(ble_huart, ble_rb.data, RINGBUFF_LEN);
__HAL_DMA_DISABLE_IT(tjc_huart->hdmarx, DMA_IT_HT);
__HAL_DMA_DISABLE_IT(ble_huart->hdmarx, DMA_IT_HT);
}
/**
* @brief 获取环形缓冲区中的可用数据数量
*/
uint16_t HMI_Get_Count(HMI_Buffer_t *rb, UART_HandleTypeDef *huart)
{
// 计算当前 DMA 接收到的数据尾部位置
uint16_t tail = RINGBUFF_LEN - __HAL_DMA_GET_COUNTER(huart->hdmarx);
// 返回可用数据数量,考虑环形缓冲区的循环特性
return (tail - rb->head + RINGBUFF_LEN) % RINGBUFF_LEN;
}
/**
* @brief 从环形缓冲区中删除指定数量的数据
*/
void HMI_Delete(HMI_Buffer_t* rb, uint16_t size)
{
rb->head = (rb->head + size) % RINGBUFF_LEN;
}
/**
* @brief 查看环形缓冲区中指定位置的数据
*/
uint8_t HMI_Peek(HMI_Buffer_t *rb, uint16_t index)
{
return rb->data[(rb->head + index) % RINGBUFF_LEN];
}
/**
* @brief 阻塞式打印 ,要发送浮点数需要开启浮点编译
*/
void HMI_Printf(UART_HandleTypeDef* huart, const char* fmt, ...)
{
uint8_t* target_buf = (huart->Instance == tjc_huart->Instance) ? tjc_tx_buf : ble_tx_buf;
if (huart->gState == HAL_UART_STATE_BUSY_TX)
return;
va_list args;
va_start(args, fmt);
int len = vsnprintf((char*)target_buf, TX_TEMP_LEN - 3, fmt, args);
va_end(args);
if (len <= 0)
return;
if (len > TX_TEMP_LEN - 3)
len = TX_TEMP_LEN - 3;
target_buf[len++] = 0xFF;
target_buf[len++] = 0xFF;
target_buf[len++] = 0xFF;
HAL_UART_Transmit(huart, target_buf, len,HAL_MAX_DELAY);
}
/**
* @brief UART 接收事件回调函数
*/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef* huart, uint16_t Size)
{
if (huart->Instance == tjc_huart->Instance)
tjc_rx_flag = 1;
else if (huart->Instance == ble_huart->Instance)
ble_rx_flag = 1;
}
- 头文件 tjc_usart_hmi.h
#ifndef HMI_UART_H
#define HMI_UART_H
#include "main.h"
#define RINGBUFF_LEN 256
#define TX_TEMP_LEN 128
#ifndef DMA_BUFFER
#define DMA_BUFFER __attribute__((section(".dma_buffer"), aligned(32)))
#endif
typedef struct
{
volatile uint16_t head;
uint8_t data[RINGBUFF_LEN];
} HMI_Buffer_t;
extern HMI_Buffer_t tjc_rb;
extern HMI_Buffer_t ble_rb;
extern UART_HandleTypeDef *tjc_huart;
extern UART_HandleTypeDef *ble_huart;
extern volatile uint8_t tjc_rx_flag;
extern volatile uint8_t ble_rx_flag;
extern uint8_t tjc_tx_buf[TX_TEMP_LEN];
extern uint8_t ble_tx_buf[TX_TEMP_LEN];
void HMI_Init(void);
uint16_t HMI_Get_Count(HMI_Buffer_t *rb, UART_HandleTypeDef *huart);
void HMI_Delete(HMI_Buffer_t *rb, uint16_t size);
uint8_t HMI_Peek(HMI_Buffer_t *rb, uint16_t index);
void HMI_Printf(UART_HandleTypeDef *huart, const char *fmt, ...);
#endif

浙公网安备 33010602011771号