一、项目概述
本文介绍如何通过Linux系统的串口通信,驱动工业级LED显示屏实现动态数据展示。项目采用C语言开发,包含气象数据显示和实时时钟两大核心功能,涉及以下关键技术点:
串口通信协议配置
自定义数据帧封装
CRC16校验算法
UTF-8到GB2312编码转换
多线程数据刷新
二、环境准备
硬件配置
树莓派4B(或其他Linux开发板)
BX-Y08A LED控制卡(支持RS232/RS485)
USB转串口模块(CH340/CP2102等)
软件依赖
sudo apt install gcc build-essential # 编译工具
sudo chmod 666 /dev/ttyUSB0 # 串口权限设置
三、核心代码解析
3.1 主程序逻辑
int main(void) {
// 气象参数初始化
int fengxiang=20;
float fengsu=20.5, wendu=20.5, yuliang=20.5;
// 格式化气象信息
char buffer[100];
sprintf(buffer, "\\F0040\\C1%5d°\n%4.1lfm/s\n%5.1lf℃\n%5.1lfmm",
fengxiang, fengsu, wendu, yuliang);
// 发送到显示屏区域0
Chinese_Show_String(buffer, 0);
// 实时时钟循环
while (1) {
time_t t = time(NULL);
struct tm *tm_info = localtime(&t);
sprintf(buffer, "\\F0040\\C1%d/%02d/%02d %02d:%02d:%02d",
tm_info->tm_year+1900, tm_info->tm_mon+1, tm_info->tm_mday,
tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec);
Chinese_Show_String(buffer, 1);
sleep(1);
}
}
3.2 串口配置函数
int send_data_over_serial(uint8_t *data, size_t length, const char *port) {
// 打开串口设备
int fd = open(port, O_RDWR | O_NOCTTY);
// 配置串口参数
struct termios tty;
cfsetospeed(&tty, B9600); // 波特率
tty.c_cflag &= ~PARENB; // 无校验
tty.c_cflag &= ~CSTOPB; // 1位停止位
tty.c_cflag |= CS8; // 8位数据位
// 发送数据
write(fd, data, length);
close(fd);
return 0;
}
四、关键技术实现
4.1 数据帧结构设计
帧结构组成:
| 帧头(8字节) | 包头(14字节) | 数据区 | CRC校验(2字节) | 结束符(0x5A) |
协议示例:
uint8_t n2s_show[] = {
// 帧头
0xA5,0xA5,0xA5,0xA5,0xA5,0xA5,0xA5,0xA5,
// 包头
0xFE,0xFF,0x00,0x80,0x00,0x00,0x00,0x00,
0x00,0x01,0xFE,0x02, [长度字段],
// 数据区
0xA3,0x06, [坐标参数], [显示内容],
// CRC16校验
crc_low, crc_high,
// 结束符
0x5A
};
4.2 编码转换实现
void replace_utf8_to_gb2312(char *input) {
// °符号转换:C2 B0 → A1 E3
char *pos = strstr(input, "\xC2\xB0");
if(pos) {
pos[0] = 0xA1;
pos[1] = 0xE3;
}
// ℃符号转换:E2 84 83 → A1 E6
pos = strstr(input, "\xE2\x84\x83");
if(pos) {
memmove(pos+2, pos+3, strlen(pos+3)+1);
pos[0] = 0xA1;
pos[1] = 0xE6;
}
}
五、常见问题排查
5.1 串口通信失败
检查设备权限:
ls -l /dev/ttyUSB*
验证波特率匹配:
stty -F /dev/ttyUSB0 speed
使用调试工具:
minicom -D /dev/ttyUSB0
5.2 显示乱码处理
确认控制卡字体编码
检查替换函数是否生效
使用十六进制查看器分析数据:
hexdump -C send_buffer.bin
六、性能优化建议
双缓冲机制:预先构建下一帧数据
CRC预计算:对固定帧头部分提前计算
异步发送:使用线程池处理串口通信
数据压缩:对重复内容进行行程编码
七、完整代码获取
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h> // 添加此行
#include <time.h> // 用于获取本地时间
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
uint8_t Bx_header[8] = {0xA5,0xA5,0xA5,0xA5,0xA5,0xA5,0xA5,0xA5};//帧头
size_t array_len = 0;
/******************************************************************************
* @函数名: send_data_over_serial(uint8_t *data, size_t length, const char *port)
* @功 能: 发送数据到指定的串口
* @输 入: data--要发送的数据数组 length-数据长度 port-串口设备路径
* @返 回: 成功返回0,失败返回-1
******************************************************************************/
int send_data_over_serial(uint8_t *data, size_t length, const char *port) {
int fd;
struct termios tty;
// 打开串口设备
fd = open(port, O_RDWR | O_NOCTTY);
if (fd < 0) {
perror("Error opening serial port");
return -1;
}
// 获取当前串口设置
if (tcgetattr(fd, &tty) != 0) {
perror("Error getting serial port attributes");
close(fd);
return -1;
}
// 设置波特率
cfsetospeed(&tty, B9600); // 设置输出波特率为115200
cfsetispeed(&tty, B9600); // 设置输入波特率为115200
// 设置其他串口参数
tty.c_cflag &= ~PARENB; // 无奇偶校验位
tty.c_cflag &= ~CSTOPB; // 1个停止位
tty.c_cflag &= ~CSIZE; // 清除数据位掩码
tty.c_cflag |= CS8; // 8个数据位
#ifdef CRTSCTS
tty.c_cflag &= ~CRTSCTS; // 禁用硬件流控
#endif
tty.c_cflag |= CREAD | CLOCAL; // 启用接收器并忽略控制线状态变化
tty.c_lflag &= ~ICANON; // 非规范模式
tty.c_lflag &= ~ECHO; // 关闭回显
tty.c_lflag &= ~ECHOE; // 关闭擦除字符时的回显
tty.c_lflag &= ~ECHONL; // 关闭换行符回显
tty.c_lflag &= ~ISIG; // 禁用信号
tty.c_iflag &= ~(IXON | IXOFF | IXANY); // 禁用软件流控
tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // 输入处理
tty.c_oflag &= ~OPOST; // 原始输出模式
tty.c_cc[VTIME] = 10; // 读超时时间(单位为100ms)
tty.c_cc[VMIN] = 0; // 最小字节数
// 应用新的串口设置
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
perror("Error setting serial port attributes");
close(fd);
return -1;
}
// 发送数据
ssize_t bytes_written = write(fd, data, length);
if (bytes_written < 0) {
perror("Error writing to serial port");
close(fd);
return -1;
}
printf("Successfully sent %zd bytes over serial.\n", bytes_written);
// 关闭串口设备
close(fd);
return 0;
}
/******************************************************************************
* @函数名: crc16_modbus(uint8_t *data, size_t length)
* @功 能: 生成crc16校验位
* @输 入: data--要校验的数组 length-长度
* @返 回: 校验结果
* @备 注:
******************************************************************************/
uint16_t crc16_ibm(const uint8_t* data, uint16_t length) {
uint16_t crc = 0x0000;
uint16_t polynomial = 0xA001;
uint16_t i, j;
for (i = 0; i < length; i++) {
crc ^= data[i];
for (j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ polynomial;
} else {
crc = crc >> 1;
}
}
}
return crc;
}
/******************************************************************************
* @函数名: replace_utf8_to_gb2312(char *input)
* @功 能: 替换特定的UTF-8字符为GB2312编码
* @输 入: input--要替换的字符串
* @返 回: NULL
******************************************************************************/
void replace_utf8_to_gb2312(char *input) {
char *pos = input;
while ((pos = strstr(pos, "\xC2\xB0")) != NULL) { // 查找°符号
pos[0] = '\xA1';
pos[1] = '\xE3';
// pos += 2; // 跳过已替换的字符
break;
}
// pos = input;
while ((pos = strstr(pos, "\xE2\x84\x83")) != NULL) { // 查找℃符号
// 将后部内容向前移动1字节(覆盖原第三字节\x83)
memmove(pos + 2, pos + 3, strlen(pos + 3) + 1);
pos[0] = '\xA1';
pos[1] = '\xE6';
// pos += 2; // 跳过已替换的字符
break;
}
}
/******************************************************************************
* @函数名: Chinese_Show_String(char *string_show, uint16_t Dstaddr)
* @功 能: 显示指定信息 -- 要显示指定颜色的时候记得添加转义字符
* @输 入: string_show--要显示的字符串 Dstaddr-屏地址
* @返 回: NULL
******************************************************************************/
uint8_t Chinese_Show_String(char *String_show, uint16_t Dstaddr) {
if(Dstaddr == 0) replace_utf8_to_gb2312(String_show); // 替换特定的UTF-8字符为GB2312编码
uint8_t *data = (uint8_t *)String_show; // 直接使用输入字符串
int crc16_L, crc16_H = 0;
int len = 0;
uint8_t n2s_show[200] = {0};
array_len = strlen((char *)data); // 获取字符串长度
memcpy(n2s_show, Bx_header, 8); // 帧头
len += 8; // 偏移的长度
// 包头数据
n2s_show[len++] = 0xfe;
n2s_show[len++] = 0xff;
n2s_show[len++] = 0x00;
n2s_show[len++] = 0x80;
n2s_show[len++] = 0x00;
n2s_show[len++] = 0x00;
n2s_show[len++] = 0x00;
n2s_show[len++] = 0x00;
n2s_show[len++] = 0x00;
n2s_show[len++] = 0x01;
n2s_show[len++] = 0xFE; // 设备型号
n2s_show[len++] = 0x02;
n2s_show[len++] = (array_len + 36) & 0xff; // 数据区长度
n2s_show[len++] = (array_len + 36) >> 8; // 数据区长度
// 数据区
n2s_show[len++] = 0xa3; // 命令分组编号
n2s_show[len++] = 0x06; // 命令编号
n2s_show[len++] = 0x00; // 是否要求控制器回复
n2s_show[len++] = 0x00; // 不清区域
n2s_show[len++] = 0x00; // 保留
n2s_show[len++] = 0x00; // 删除区域个数
n2s_show[len++] = 0x01; // 更新区域个数
n2s_show[len++] = (array_len + 27) & 0xff; // 区域0数据长度
n2s_show[len++] = (array_len + 27) >> 8; // 区域0数据长度
// 区域0数据
n2s_show[len++] = 0x00; // 区域类型
if(Dstaddr == 0){// 四要素
n2s_show[len++] = 0x20; // 区域X坐标,默认以字节(8个像素点)为单位
n2s_show[len++] = 0x00;
n2s_show[len++] = 40; // 区域Y坐标,以像素点为单位
n2s_show[len++] = 0x00;
n2s_show[len++] = 0x14; // 区域宽度,默认以字节(8个像素点)为单位
n2s_show[len++] = 0x00;
n2s_show[len++] = 160; // 区域高度,以像素点为单位
n2s_show[len++] = 0x00;
}else if(Dstaddr == 1){// 时钟
n2s_show[len++] = 0x00; // 区域X坐标,默认以字节(8个像素点)为单位
n2s_show[len++] = 0x00;
n2s_show[len++] = 0x00; // 区域Y坐标,以像素点为单位
n2s_show[len++] = 0x00;
n2s_show[len++] = 0x32; // 区域宽度,默认以字节(8个像素点)为单位
n2s_show[len++] = 0x00;
n2s_show[len++] = 40; // 区域高度,以像素点为单位
n2s_show[len++] = 0x00;
}
n2s_show[len++] = Dstaddr; // 动态区域编号
n2s_show[len++] = 0X00; // 行间距
n2s_show[len++] = 0X00; // 动态区数据循环显示
n2s_show[len++] = 0X02; // 超时时间
n2s_show[len++] = 0X00; // 超时时间
n2s_show[len++] = 0x00; // 不使能语音模块
n2s_show[len++] = 0x00; // 拓展位数
n2s_show[len++] = 0x00; // 字体对齐方式 --上下居中 左右右对齐
n2s_show[len++] = 0x02; // 是否多行显示
n2s_show[len++] = 0x01; // 不自动换行
n2s_show[len++] = 0x01; // 静止显示01
n2s_show[len++] = 0x00; // 退出方式
n2s_show[len++] = 0x00; // 显示速度
n2s_show[len++] = 0x0a; // 特技显示时间
n2s_show[len++] = array_len & 0xff; // 显示内容长度
n2s_show[len++] = array_len >> 8;
n2s_show[len++] = array_len >> 16;
n2s_show[len++] = array_len >> 24;
int i, j;
for (i = 0; i < array_len; i++) {
n2s_show[len++] = data[i]; // 显示内容
}
crc16_L = crc16_ibm(&n2s_show[8], len - 8) & 0xff;
crc16_H = crc16_ibm(&n2s_show[8], len - 8) >> 8;
n2s_show[len++] = crc16_L;
n2s_show[len++] = crc16_H;
n2s_show[len++] = 0x5a;
// printf("%d:", len);
// for (j = 0; j < len; j++) {
// printf("%02x ", n2s_show[j]);
// }
// printf("\n");
// 发送数据
if (send_data_over_serial(n2s_show, len, "/dev/ttyUSB0") != 0) {
printf("Failed to send data over serial.\n");
return -1;
}
return 0;
}
int main(void){
int fengxiang=20;
float fengsu=20.5;
float wendu=20.5;
float yuliang=20.5;
char iniid[100];
// // 数字左对齐
// sprintf(iniid, "\\F0040\\C1%d°\n%lfm/s\n%lf℃\n%lfmm", fengxiang, fengsu, wendu, yuliang);
// 单位右对齐
sprintf(iniid, "\\F0040\\C1%5d°\n%4.1lfm/s\n%5.1lf℃\n%5.1lfmm", fengxiang, fengsu, wendu, yuliang);
Chinese_Show_String(iniid, 0);
while (1) {
time_t t = time(NULL);
struct tm *tm_info = localtime(&t);
int year = tm_info->tm_year + 1900;
int month = tm_info->tm_mon + 1;
int day = tm_info->tm_mday;
int hour = tm_info->tm_hour;
int minute = tm_info->tm_min;
int second = tm_info->tm_sec;
sprintf(iniid, "\\F0040\\C1%d/%02d/%02d %02d:%02d:%02d", year, month, day, hour, minute, second);
Chinese_Show_String(iniid, 1);
sleep(1); // 每秒更新一次
}
}