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. 注意事项

  1. 必须在 CubeMX 中将 RX DMA 设为 Circular,否则需要重复开关DMA
  2. RINGBUFF_LEN 必须是 2 的幂(256/512/1024),虽然代码支持任意值,但性能最优。
  3. 处理完数据之后必须调用 HMI_Delete,否则环形缓冲区会“假满”,后续数据丢失。

8.CubeMX 配置及完整代码

  • CubeMX 配置

alt text

  • 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

posted @ 2026-03-29 19:58  基米不语  阅读(7)  评论(0)    收藏  举报