模块驱动指南--串口屏

淘晶驰(TJC)串口屏开发指南

文档目标:让AI快速理解如何驱动淘晶驰串口屏
核心内容:HMI_APP函数库 + 数据解析方法 + 注意事项
更新日期:2025-12-17


1. 通信协议核心规则(必读)

:warning: 三条铁律

铁律1:所有指令必须以 0xFF 0xFF 0xFF 结尾

// ✅ 正确:发送HEX字节
uint8_t end[3] = {0xFF, 0xFF, 0xFF};
HAL_UART_Transmit(&huart1, (uint8_t*)"page 1", 6, 100);
HAL_UART_Transmit(&huart1, end, 3, 100);

// ❌ 错误:使用字符串字面量
char cmd[] = "page 1\xFF\xFF\xFF";  // 这不是HEX字节!

铁律2:字符串属性必须加双引号

// ✅ 正确
t0.txt="Hello"

// ❌ 错误
t0.txt=Hello

铁律3:控件名称严格区分大小写

// ✅ 正确
t0.txt

// ❌ 错误
T0.txt

数据包格式

MCU → 串口屏

[指令内容] + [0xFF 0xFF 0xFF]

串口屏 → MCU

返回类型 起始码 数据部分 终结符 总长度
触摸事件 0x65 PageID(1B) + CompID(1B) + Event(1B) 0xFF×3 7字节
字符串 0x70 字符串字节流 0xFF×3 可变长度
数值 0x71 4字节整数(小端序) 0xFF×3 8字节

2. HMI_APP 函数库

本项目封装了3个常用函数,位于 hmi_app.chmi_app.h

2.1 HMI_send_string()

函数原型

void HMI_send_string(char* name, char* showdata);

功能:向文本框控件发送字符串

底层实现

void HMI_send_string(char* name, char* showdata)
{
    my_printf(&huart1, "%s=\"%s\"\xff\xff\xff", name, showdata);
    // 生成指令:name="showdata"[0xFF×3]
}

使用示例

HMI_send_string("t0.txt", "温度正常");
HMI_send_string("t1.txt", "设备运行中");
HMI_send_string("get", "t0.txt");  // 发送get指令

注意

  • name 参数需要包含完整属性名(如 "t0.txt"
  • showdata 是要显示的字符串内容

2.2 HMI_send_number()

函数原型

void HMI_send_number(char* name, int num);

功能:向数字框控件发送整数

底层实现

void HMI_send_number(char* name, int num)
{
    my_printf(&huart1, "%s=%d\xff\xff\xff", name, num);
    // 生成指令:name=num[0xFF×3]
}

使用示例

HMI_send_number("x0.val", 1234);   // 显示整数1234
HMI_send_number("x1.val", 56);     // 显示整数56

注意

  • name 参数需要包含完整属性名(如 "x0.val"
  • 只能发送整数,浮点数需要使用 HMI_send_float()

2.3 HMI_send_float()

函数原型

void HMI_send_float(char* name, float num);

功能:向数字框控件发送浮点数(转换为×100倍数的整数)

底层实现

void HMI_send_float(char* name, float num)
{
    my_printf(&huart1, "%s=%d\xff\xff\xff", name, (int)(num * 100));
    // 生成指令:name=(int)(num*100)[0xFF×3]
}

使用示例

float voltage = 3.14f;
HMI_send_float("x2.val", voltage);  // 发送314

// ⚠️ 串口屏端必须设置 vvs1=2(2位小数),才能显示为"3.14"

注意

  • 硬编码×100倍数:函数内部将浮点数乘以100后转为整数发送
  • 串口屏必须设置小数位:在TJC HMI编辑器中,选中数字框控件,设置 vvs1=2(2位小数)
  • 仅适用于2位小数:如果需要1位或3位小数,需要修改函数

如何修改为其他小数位

// 修改为1位小数(×10)
void HMI_send_float_1(char* name, float num)
{
    my_printf(&huart1, "%s=%d\xff\xff\xff", name, (int)(num * 10));
}

// 修改为3位小数(×1000)
void HMI_send_float_3(char* name, float num)
{
    my_printf(&huart1, "%s=%d\xff\xff\xff", name, (int)(num * 1000));
}

2.4 him_task()

函数原型

void him_task(void);

功能:HMI任务函数(当前为空函数,预留接口)

使用场景

  • 定时更新显示内容
  • 轮询按键状态
  • 处理HMI相关的周期性任务

3. 串口屏数据解析方法(重点)

3.1 :warning: 常见错误:一次性读取导致数据包丢失

问题场景:串口屏快速发送多个数据包(如连续6个get指令的返回值),MCU只接收到第一个包,后续包全部丢失。

错误代码

// ❌ 错误做法:一次性读取所有数据,只解析第一个包
uint16_t length = rt_ringbuffer_data_len(&uart_ringbuffer);
rt_ringbuffer_get(&uart_ringbuffer, uart_dma_buffer, length);

// 只解析了第一个包
if(uart_dma_buffer[0] == 0x65) { /* ... */ }

// 清空缓冲区,后续包全部丢失!
memset(uart_dma_buffer, 0, sizeof(uart_dma_buffer));

3.2 :white_check_mark: 本项目解决方案:逐包读取

核心思想:每次只从环形缓冲区读取一个完整数据包,处理后再读取下一个。

关键代码(来自 uart_app.c 第185-227行):

void uart_task(void)
{
    uint16_t length = rt_ringbuffer_data_len(&uart_ringbuffer);
    if(length == 0) return;

    // ========== 步骤1:读取第一个字节,判断数据包类型 ==========
    uint8_t first_byte;
    rt_ringbuffer_get(&uart_ringbuffer, &first_byte, 1);
    uart_dma_buffer[0] = first_byte;

    // ========== 步骤2:根据类型读取剩余字节 ==========
    if(first_byte == 0x65) {
        // 触摸事件:固定7字节
        rt_ringbuffer_get(&uart_ringbuffer, &uart_dma_buffer[1], 6);
        length = 7;
    }
    else if(first_byte == 0x70) {
        // 字符串:可变长度,逐字节读取直到遇到0xFF 0xFF 0xFF
        uint16_t idx = 1;
        uint8_t end_count = 0;  // 连续0xFF计数器

        while(idx < 127 && rt_ringbuffer_data_len(&uart_ringbuffer) > 0) {
            rt_ringbuffer_get(&uart_ringbuffer, &uart_dma_buffer[idx], 1);

            // 检查是否是终结符
            if(uart_dma_buffer[idx] == 0xFF) {
                end_count++;
                if(end_count == 3) {
                    // 找到完整终结符
                    length = idx + 1;
                    break;
                }
            } else {
                end_count = 0;  // 重置计数器
            }
            idx++;
        }

        // 如果没找到完整终结符,设置length为当前idx
        if(end_count != 3) {
            length = idx;
        }
    }
    else if(first_byte == 0x71) {
        // 数值:固定8字节
        rt_ringbuffer_get(&uart_ringbuffer, &uart_dma_buffer[1], 7);
        length = 8;
    }
    else {
        // 未知数据包,跳过
        return;
    }

    // ========== 步骤3:解析完整数据包 ==========
    // 在这里添加具体的解析逻辑...

    // ⚠️ 注意:不要清空整个RingBuffer,下次调用会继续处理剩余数据
    memset(uart_dma_buffer, 0, sizeof(uart_dma_buffer));
}

3.3 解析触摸事件

数据包格式0x65 + PageID + CompID + Event + 0xFF×3(共7字节)

if(uart_dma_buffer[0] == 0x65) {
    uint8_t page_id = uart_dma_buffer[1];   // 页面ID
    uint8_t comp_id = uart_dma_buffer[2];   // 控件ID
    uint8_t event = uart_dma_buffer[3];     // 事件类型

    // Event值:0x01=按下,0x00=释放
    if(event == 0x01) {
        // 用户按下了控件
        if(comp_id == 0) {
            // 按钮0被按下,执行操作
            tjc_send_cmd("page 1");
        }
    }
}

3.4 解析字符串返回

数据包格式0x70 + 字符串字节流 + 0xFF×3

if(uart_dma_buffer[0] == 0x70) {
    // 计算字符串长度(去掉起始码和终结符)
    uint8_t str_len = length - 4;

    // 提取字符串
    char received_str[64];
    memcpy(received_str, &uart_dma_buffer[1], str_len);
    received_str[str_len] = '\0';

    // 使用字符串
    printf("接收到字符串: %s\n", received_str);
}

3.5 解析数值返回

数据包格式0x71 + 4字节整数(小端序) + 0xFF×3(共8字节)

if(uart_dma_buffer[0] == 0x71) {
    // 解析小端序整数
    uint32_t value = uart_dma_buffer[1] |
                    (uart_dma_buffer[2] << 8) |
                    (uart_dma_buffer[3] << 16) |
                    (uart_dma_buffer[4] << 24);

    // 使用数值
    printf("接收到数值: %lu\n", value);

    // 如果是×100倍数的浮点数(2位小数)
    float actual_value = value / 100.0f;
    printf("实际数值: %.2f\n", actual_value);
}

4. 核心注意事项

注意1:终结符必须是HEX字节

// ✅ 正确
uint8_t end[3] = {0xFF, 0xFF, 0xFF};
HAL_UART_Transmit(&huart1, end, 3, 100);

// ❌ 错误
char end[] = "\xFF\xFF\xFF";  // 这是字符串字面量,不是HEX字节

原因:串口屏硬件识别的是字节值 0xFF,不是字符编码。


注意2:环形缓冲区必须逐包读取

关键要点

  • 逐包读取:每次只读取一个完整数据包
  • 精确长度:根据协议确定每种包的长度
  • 保留剩余:不要清空RingBuffer,让下次调用继续处理
  • 状态机配合:使用状态机记录当前处理到哪个参数

错误做法

// ❌ 一次性读取所有数据,只解析第一个包
length = rt_ringbuffer_data_len(&uart_ringbuffer);
rt_ringbuffer_get(&uart_ringbuffer, uart_dma_buffer, length);
// 后续包全部丢失!

正确做法

// ✅ 先读取第一个字节判断类型,再读取对应长度
uint8_t first_byte;
rt_ringbuffer_get(&uart_ringbuffer, &first_byte, 1);

if(first_byte == 0x65) {
    // 触摸事件:再读取6字节
    rt_ringbuffer_get(&uart_ringbuffer, &uart_dma_buffer[1], 6);
}
else if(first_byte == 0x70) {
    // 字符串:逐字节读取直到遇到0xFF×3
}
else if(first_byte == 0x71) {
    // 数值:再读取7字节
    rt_ringbuffer_get(&uart_ringbuffer, &uart_dma_buffer[1], 7);
}

注意3:浮点数需要倍率转换

问题:串口屏数字框只能存储整数,如何显示小数?

解决方案:倍率转换 + 小数位设置

// MCU端:发送时乘以100
float voltage = 3.14f;
HMI_send_float("x0.val", voltage);  // 内部发送314

// 串口屏端:设置小数位为2
// 在TJC HMI编辑器中,选中x0控件,设置vvs1=2
// 最终显示:3.14

倍率对照表

小数位数 倍率 示例:3.14 发送值 串口屏vvs1设置
1位小数 ×10 3.1 31 vvs1=1
2位小数 ×100 3.14 314 vvs1=2
3位小数 ×1000 3.140 3140 vvs1=3

注意4:中文字符串匹配失败

问题:串口屏发送中文字符串(如"架空"、“电缆”),MCU使用 strstr() 无法匹配。

原因

  • 源代码文件通常是UTF-8编码
  • 串口屏发送的是GB2312编码
  • 两者字节序列不同,strstr() 无法匹配

解决方案

// 方法1:避免中文比较,使用数字编码
typedef enum {
    LINE_TYPE_OVERHEAD = 0,  // 架空
    LINE_TYPE_CABLE = 1      // 电缆
} LineType;

// 方法2:字节序列匹配(同时支持UTF-8和GB2312)
int Parse_LineType(const char *str) {
    // GB2312编码匹配
    if(strstr(str, "\xBC\xDC\xBF\xD5") != NULL) {  // "架空"的GB2312字节
        return 0;
    }
    if(strstr(str, "\xB5\xE7\xC0\xC2") != NULL) {  // "电缆"的GB2312字节
        return 1;
    }

    // UTF-8编码匹配(备用)
    if(strstr(str, "\xE6\x9E\xB6\xE7\xA9\xBA") != NULL) {  // "架空"的UTF-8字节
        return 0;
    }
    if(strstr(str, "\xE7\x94\xB5\xE7\xBC\x86") != NULL) {  // "电缆"的UTF-8字节
        return 1;
    }

    return -1;  // 无法识别
}

注意5:控件名称严格区分大小写

// ✅ 正确
HMI_send_string("t0.txt", "Hello");
HMI_send_number("x0.val", 123);

// ❌ 错误
HMI_send_string("T0.txt", "Hello");  // 大写T
HMI_send_number("x0.Val", 123);      // 大写V

建议:使用宏定义集中管理控件名称

#define HMI_TEXT_STATUS   "t0.txt"
#define HMI_NUM_TEMP      "x0.val"

HMI_send_string(HMI_TEXT_STATUS, "正常");
HMI_send_number(HMI_NUM_TEMP, 36);

注意6:get指令后需要解析返回值

// 发送get指令
HMI_send_string("get", "t0.txt");

// ⚠️ 必须在uart_task()中添加解析逻辑
void uart_task(void)
{
    // ... 逐包读取数据包 ...

    if(uart_dma_buffer[0] == 0x70) {
        // 解析字符串返回
        uint8_t str_len = length - 4;
        char received_str[64];
        memcpy(received_str, &uart_dma_buffer[1], str_len);
        received_str[str_len] = '\0';

        // 使用返回值
        printf("t0.txt内容: %s\n", received_str);
    }
}

5. 快速开发模板

5.1 基础发送函数

// 发送指令(基础)
void tjc_send_cmd(const char *cmd) {
    uint8_t end[3] = {0xFF, 0xFF, 0xFF};
    HAL_UART_Transmit(&huart1, (uint8_t*)cmd, strlen(cmd), 100);
    HAL_UART_Transmit(&huart1, end, 3, 100);
}

// 页面跳转
void tjc_goto_page(uint8_t page_id) {
    char cmd[16];
    snprintf(cmd, sizeof(cmd), "page %d", page_id);
    tjc_send_cmd(cmd);
}

5.2 环形缓冲区解析模板

void uart_task(void)
{
    uint16_t length = rt_ringbuffer_data_len(&uart_ringbuffer);
    if(length == 0) return;

    // 读取第一个字节判断类型
    uint8_t first_byte;
    rt_ringbuffer_get(&uart_ringbuffer, &first_byte, 1);
    uart_dma_buffer[0] = first_byte;

    // 根据类型读取剩余数据
    if(first_byte == 0x65) {
        // 触摸事件:固定7字节
        rt_ringbuffer_get(&uart_ringbuffer, &uart_dma_buffer[1], 6);
        length = 7;

        uint8_t page_id = uart_dma_buffer[1];
        uint8_t comp_id = uart_dma_buffer[2];
        uint8_t event = uart_dma_buffer[3];

        // 处理触摸事件
        if(event == 0x01 && comp_id == 0) {
            tjc_goto_page(1);
        }
    }
    else if(first_byte == 0x70) {
        // 字符串:逐字节读取直到0xFF×3
        uint16_t idx = 1;
        uint8_t end_count = 0;

        while(idx < 127 && rt_ringbuffer_data_len(&uart_ringbuffer) > 0) {
            rt_ringbuffer_get(&uart_ringbuffer, &uart_dma_buffer[idx], 1);
            if(uart_dma_buffer[idx] == 0xFF) {
                end_count++;
                if(end_count == 3) {
                    length = idx + 1;
                    break;
                }
            } else {
                end_count = 0;
            }
            idx++;
        }

        // 提取字符串
        uint8_t str_len = length - 4;
        char received_str[64];
        memcpy(received_str, &uart_dma_buffer[1], str_len);
        received_str[str_len] = '\0';

        // 处理字符串
    }
    else if(first_byte == 0x71) {
        // 数值:固定8字节
        rt_ringbuffer_get(&uart_ringbuffer, &uart_dma_buffer[1], 7);
        length = 8;

        uint32_t value = uart_dma_buffer[1] |
                        (uart_dma_buffer[2] << 8) |
                        (uart_dma_buffer[3] << 16) |
                        (uart_dma_buffer[4] << 24);

        // 处理数值
    }

    memset(uart_dma_buffer, 0, sizeof(uart_dma_buffer));
}

6. 开发检查清单

开发完成后,提交前检查:

  • 所有指令使用HEX字节终结符 {0xFF, 0xFF, 0xFF}
  • 字符串属性加双引号 t0.txt="文本"
  • 控件名称大小写正确
  • 环形缓冲区使用逐包解析(关键)
  • 浮点数正确设置倍率和小数位
  • get指令后添加了返回值解析逻辑
  • 中文字符串使用字节序列匹配
  • 删除所有调试代码

文档版本:v1.0
最后更新:2025-12-17
核心要点:HMI_APP函数库 + 环形缓冲区逐包解析 + 注意事项

本文档基于实际项目经验总结,重点解决数据包丢失和中文编码问题。