《嵌入式开发实战:基于Linux串口的LED屏显系统设计与实现》

发布于:2025-04-12 ⋅ 阅读:(18) ⋅ 点赞:(0)

一、项目概述

本文介绍如何通过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 显示乱码处理
  1. 确认控制卡字体编码

  2. 检查替换函数是否生效

  3. 使用十六进制查看器分析数据:

hexdump -C send_buffer.bin

六、性能优化建议

  1. 双缓冲机制:预先构建下一帧数据

  2. CRC预计算:对固定帧头部分提前计算

  3. 异步发送:使用线程池处理串口通信

  4. 数据压缩:对重复内容进行行程编码


七、完整代码获取

#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); // 每秒更新一次
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到