淘晶驰(TJC)串口屏开发指南
文档目标:让AI快速理解如何驱动淘晶驰串口屏
核心内容:HMI_APP函数库 + 数据解析方法 + 注意事项
更新日期:2025-12-17
1. 通信协议核心规则(必读)
三条铁律
铁律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.c 和 hmi_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
常见错误:一次性读取导致数据包丢失
问题场景:串口屏快速发送多个数据包(如连续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
本项目解决方案:逐包读取
核心思想:每次只从环形缓冲区读取一个完整数据包,处理后再读取下一个。
关键代码(来自 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函数库 + 环形缓冲区逐包解析 + 注意事项
本文档基于实际项目经验总结,重点解决数据包丢失和中文编码问题。