Day75 RS-485 通信协议设计、串口编程与嵌入式系统部署实践

day75 RS-485 通信协议设计、串口编程与嵌入式系统部署实践


1. RS-485 接口与通信基础

1.1 物理接口说明

  • 位置 :开发板上网口旁两个蓝色接口中,远离网口的那个为 485 接口。
  • 标识:标有 "AB" 字样,对应差分信号线 A/B。
  • 供电要求 :标准 RS-485 传感器(如 FCU1104)需 10~30V DC 电源;开发板仅提供 5V,不可直接驱动,必须外接合适电源。
  • 安全规范
    • 严禁带电插拔(历史教训:已烧毁两块开发板)。
    • 所有设备应共地(GND),避免电位差导致通信失败。

1.2 通信特性

  • 差分信号传输:通过 A/B 线之间的电压差表示数据,抗干扰能力强。
  • 主从架构(Master-Slave)
    • 主机主动轮询,从机被动响应。
    • 从机不会主动发送数据。
  • 解析优势
    • 每次通信包独立且完整
    • 解析时可从缓冲区起始位置处理,无需拼接碎片。
    • 若数据不足一个完整包,可直接丢弃,下次重新接收。

✅ 对比:自动上报型传感器需处理粘包/分包,逻辑更复杂。


2. 自定义通信协议设计与实现

2.1 协议格式(字节顺序)

字段 长度 说明
PACK_HEAD 1B 包头:0x55
Length 1B 有效数据长度(含命令码 + 数据)
CMD 1B 命令类型(见下文枚举)
Data nB 实际数据(如温度、湿度)
Checksum 1B 校验和 = sum(CMD + Data)
PACK_TAIL 1B 包尾:0xAA

🔍 示例(GET_ALL):
[0x55][0x03][0x03][temp][humi][checksum][0xAA]

2.2 命令码定义

c 复制代码
typedef enum {
    GET_TEMP = 1,  // 获取温度
    GET_HUMI,      // 获取湿度
    GET_ALL        // 获取全部(温度+湿度)
} CMD;

2.3 核心函数设计原则

  • 封装与发送分离 :打包函数只负责生成字节流,不直接调用 send()
  • 模块化:便于扩展(如增加设备 ID、时间戳等字段)。
  • 边界检查:防止缓冲区溢出、空指针等。

3. TCP 模拟测试程序详解(ser.c / cli.c)

💡 用途:在无硬件条件下验证协议逻辑。使用 TCP 模拟串口通信。

3.1 公共定义(两端一致)

c 复制代码
#define PACK_HEAD 0x55  // packet header
#define PACK_TAIL 0xAA  // packet tail

// 计算校验和:对指定长度的字节数组求和,返回低8位
unsigned char check_sum(unsigned char *data, int len) {
    unsigned char sum = 0;
    for (int i = 0; i < len; i++)
        sum += data[i];
    return sum;
}

功能:用于验证数据完整性。发送端计算并附加,接收端重新计算并比对。


3.2 服务器程序(ser.c)------ 模拟 485 主机

数据结构
c 复制代码
typedef struct __data {
    char temp;  // 温度(0~99)
    char humi;  // 湿度(0~99)
} DATA;
函数1:根据命令组织有效数据
c 复制代码
// 输入:原始数据、命令类型;输出:填充后的数据缓冲区;返回:有效数据长度
int get_data(DATA *data, CMD cmd, unsigned char *out_data) {
    int len = 0;
    out_data[len++] = cmd;  // 第一字节为命令码
    switch (cmd) {
        case GET_TEMP:
            out_data[len++] = data->temp;  // 仅温度
            break;
        case GET_HUMI:
            out_data[len++] = data->humi;  // 仅湿度
            break;
        case GET_ALL:
            out_data[len++] = data->temp;  // 温度
            out_data[len++] = data->humi;  // 湿度
            break;
        default:
            break;
    }
    return len;  // 返回 cmd + data 的总字节数
}

功能:将结构体数据按命令要求序列化为字节数组。

函数2:按协议封装完整数据包
c 复制代码
// 输入:待封装的有效数据及其长度;输出:完整协议包;返回:总包长度
int package(unsigned char *data, int len_data, unsigned char *out_data) {
    int len = 0;
    out_data[len++] = PACK_HEAD;          // 包头
    out_data[len++] = len_data;           // 有效数据长度(cmd + payload)
    for (int i = 0; i < len_data; i++)    // 复制有效数据
        out_data[len++] = data[i];
    out_data[len++] = check_sum(data, len_data);  // 校验和(仅对有效数据计算)
    out_data[len++] = PACK_TAIL;          // 包尾
    return len;  // 总长度 = 1(head) + 1(len) + len_data + 1(sum) + 1(tail)
}

功能:将有效数据包装成符合自定义协议的完整帧。

主函数:启动 TCP 服务并循环发送
c 复制代码
int main(int argc, char **argv) {
    // 1. 创建监听套接字
    int listfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == listfd) { perror("socket error\n"); return 1; }

    // 2. 绑定地址(任意IP,端口50000)
    struct sockaddr_in ser;
    bzero(&ser, sizeof(ser));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;
    bind(listfd, (struct sockaddr*)&ser, sizeof(ser));

    // 3. 开始监听(队列长度3)
    listen(listfd, 3);

    // 4. 接受客户端连接
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);
    int conn = accept(listfd, (struct sockaddr*)&cli, &len);
    if (-1 == conn) { perror("accept"); return 1; }

    // 5. 循环生成并发送数据
    DATA data;
    int i = 0;
    while (1) {
        // 生成随机温湿度(0~99)
        data.temp = rand() % 100;
        data.humi = rand() % 100;
        printf("temp = %d  humi = %d\n", data.temp, data.humi);

        // 打包
        unsigned char buf[20];
        unsigned char send_buf[100];
        int len = get_data(&data, GET_ALL, buf);     // 组织有效数据
        len = package(buf, len, send_buf);           // 封装完整包

        // 故意引入错误用于测试鲁棒性
        if (i % 5 == 0) buf[1] += 1;                 // 每5次篡改数据
        if (i++ % 6 == 0) send_buf[len - 2] += 1;    // 每6次篡改校验和

        // 发送并休眠50ms
        send(conn, send_buf, len, 0);
        usleep(50 * 1000);
    }

    close(listfd);
    close(conn);
    return 0;
}

理想运行结果

  • 每 50ms 输出一行温湿度值(如 temp = 45 humi = 78)。
  • 客户端能正确解析大部分数据包,并过滤掉被篡改的无效包。

3.3 客户端程序(cli.c)------ 模拟 485 从机解析器

主循环:接收并解析数据
c 复制代码
int main(int argc, char **argv) {
    // 1. 创建连接套接字并连接服务器(127.0.0.1:50000)
    int conn = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in ser;
    bzero(&ser, sizeof(ser));
    ser.sin_family = AF_INET;
    ser.sin_port = htons(50000);
    ser.sin_addr.s_addr = INADDR_ANY;
    connect(conn, (struct sockaddr*)&ser, sizeof(ser));

    // 2. 接收缓冲区管理
    unsigned char buf[1024] = {0};
    int cur_len = 0;  // 当前缓冲区中有效数据长度

    while (1) {
        // 追加新接收的数据到缓冲区末尾
        int ret = recv(conn, &buf[cur_len], sizeof(buf) - cur_len, 0);
        cur_len += ret;

        // 3. 从缓冲区开头尝试解析完整包
        int pos = 0;
        while (cur_len - pos >= 6) {  // 最小包长:head(1)+len(1)+cmd(1)+data(1)+sum(1)+tail(1)=6
            // 检查包头和包尾是否匹配
            if (buf[pos] == PACK_HEAD && 
                buf[pos + buf[pos + 1] + 3] == PACK_TAIL) {

                // 验证校验和:重新计算 vs 接收到的值
                if (buf[pos + buf[pos + 1] + 2] == 
                    check_sum(&buf[pos + 2], buf[pos + 1])) {

                    // 校验成功:打印有效数据(跳过cmd字节)
                    for (int i = 0; i < buf[pos + 1]; i++)
                        printf("%d\t", buf[pos + i + 2]);
                    printf("\n~~~~~~~~~~~\n");

                    // 移除已处理的数据包(滑动窗口)
                    int size = pos + buf[pos + 1] + 4;  // head+len+data+sum+tail
                    memcpy(buf, &buf[size], cur_len - size);
                    cur_len -= size;
                } else {
                    pos++;  // 校验失败,尝试下一个起始位置
                }
            } else {
                pos++;  // 包头/包尾不匹配,尝试下一个起始位置
            }

            // 防止pos过大导致效率低下:定期清理无效前缀
            if (pos >= 20) {
                memcpy(buf, &buf[pos], cur_len - pos);
                cur_len -= pos;
                pos = 0;
            }
        }
    }

    close(conn);
    return 0;
}

功能详解

  • 缓冲区追加:应对 TCP 粘包/分包。
  • 滑动窗口解析 :通过 pos 指针在缓冲区内查找有效包。
  • 鲁棒性设计
    • 校验和验证确保数据正确。
    • pos >= 20 时强制归零,避免因连续错误数据导致死循环。
  • 内存安全 :每次解析前检查 cur_len - pos >= 6

理想运行结果

  • 正常包:输出两列数字(温度、湿度)并打印分隔线。

  • 错误包:静默跳过,不影响后续解析。

  • 示例输出:

    复制代码
    45	78	
    ~~~~~~~~~~~
    12	34	
    ~~~~~~~~~~~

4. 真实传感器通信:Modbus RTU 与串口编程

📌 应用于雨水/光照传感器(FCU1104 等)

4.1 串口配置函数

c 复制代码
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>

// 打开并配置串口(波特率4800, 8N1)
int open_serial_port(const char *port) {
    // 以读写模式打开设备,不作为控制终端
    int fd = open(port, O_RDWR | O_NOCTTY);
    if (fd == -1) { perror("Error opening serial port"); return -1; }

    struct termios options;
    tcgetattr(fd, &options);  // 获取当前配置

    // 设置波特率
    cfsetispeed(&options, B4800);
    cfsetospeed(&options, B4800);

    // 8位数据位、无校验、1位停止位
    options.c_cflag &= ~CSIZE;
    options.c_cflag |= CS8;
    options.c_cflag &= ~PARENB;
    options.c_cflag &= ~CSTOPB;

    // 启用接收和本地模式
    options.c_cflag |= (CLOCAL | CREAD);

    // 原始输入/输出模式(禁用回显、行缓冲等)
    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
    options.c_oflag &= ~OPOST;

    // 设置读超时:至少1字节,最长等待1秒
    options.c_cc[VMIN] = 1;
    options.c_cc[VTIME] = 10;  // 10 * 0.1s = 1s

    // 应用配置并清空缓冲区
    tcsetattr(fd, TCSANOW, &options);
    tcflush(fd, TCIOFLUSH);
    return fd;
}

4.2 Modbus RTU CRC16 校验

c 复制代码
// Modbus标准CRC16算法(多项式0xA001)
unsigned short crc16(const unsigned char *buf, int len) {
    unsigned short crc = 0xFFFF;
    for (int i = 0; i < len; i++) {
        crc ^= buf[i];
        for (int j = 0; j < 8; j++)
            crc = (crc & 1) ? ((crc >> 1) ^ 0xA001) : (crc >> 1);
    }
    return crc;
}

4.3 带超时的读取函数(使用 select)

c 复制代码
#include <sys/select.h>

// 使用select实现读超时(避免阻塞)
int read_timeout(int fd, void *buf, int len, struct timeval time) {
    fd_set rd_set;
    FD_ZERO(&rd_set);
    FD_SET(fd, &rd_set);

    int sel_ret = select(fd + 1, &rd_set, NULL, NULL, &time);
    if (sel_ret == 0) return 0;         // 超时
    if (sel_ret > 0 && FD_ISSET(fd, &rd_set))
        return read(fd, buf, len);      // 有数据可读
    return -1;                          // 错误
}

4.4 主函数:查询并解析光照值

c 复制代码
int main(void) {
    int fd = open_serial_port("/dev/ttymxc1");
    if (-1 == fd) { printf("open failed\n"); return -1; }

    while (1) {
        // Modbus指令:地址01, 功能码03, 起始寄存器0002, 读2个寄存器
        unsigned char buf[8] = {0x01, 0x03, 0x00, 0x02, 0x00, 0x02, 0x65, 0xCB};
        write(fd, buf, sizeof(buf));

        // 等待2秒超时
        struct timeval tv = {.tv_sec = 2, .tv_usec = 0};
        unsigned char data[100] = {0};
        int ret = read_timeout(fd, data, sizeof(data), tv);

        if (ret <= 0) {
            printf(ret < 0 ? "read error\n" : "timeout\n");
            continue;
        }

        // 打印原始数据(调试用)
        for (int i = 0; i < ret; i++) printf("0x%02x\t", data[i]);
        printf("\n");

        // 验证CRC:计算值 vs 接收值(注意字节序)
        unsigned short c16 = crc16(data, data[2] + 3);
        unsigned short recv_crc = (data[data[2] + 4] << 8) | data[data[2] + 3];
        if (c16 == recv_crc) {
            // 解析4字节光照值(大端序)
            int light = (data[3] << 24) | (data[4] << 16) | (data[5] << 8) | data[6];
            printf("light = %dlux\n", light);
        }

        sleep(3);
    }

    close_serial_port(fd);
    return 0;
}

理想运行结果

复制代码
0x01	0x03	0x04	0x00	0x00	0x01	0x2C	0x8F	
c16 = 0x8F2C
light = 300lux

5. 嵌入式开发板功能与系统部署

5.1 设备信息

  • 型号:工业网关(i.MX6ULL)
  • 接口
    • 网络:双以太网、4G(SIM卡槽)、WiFi
    • 串口:4路独立 RS-485(A1/B1 ~ A4/B4)
    • 其他:蜂鸣器、GPS、调试串口
  • 指示灯:4G/RUN/POW/ERR/Tx/Rx(每路485一对)

5.2 连接与登录

bash 复制代码
# PC设置IP(与开发板同网段)
sudo ifconfig eth0 192.168.0.100

# SSH登录(默认IP: 192.168.0.232)
ssh root@192.168.0.232

5.3 基础服务测试

bash 复制代码
# Web服务
curl http://192.168.0.232

# 4G拨号(插入SIM卡后)
./ppp.sh start
ping baidu.com

# 设置系统时间并写入RTC
date -s "2025-11-01 12:00:00"
hwclock -w

5.4 开机自启动配置

bash 复制代码
# 创建启动脚本(数字越小优先级越高)
vi /etc/rc.d/S99myapp.sh

#!/bin/sh
/home/root/my_sensor_app > /tmp/app.log 2>&1 &

# 添加执行权限
chmod 777 /etc/rc.d/S99myapp.sh

5.5 编译环境

  • 板载编译 :直接使用 gcc(适合小型程序)。

  • 交叉编译

    bash 复制代码
    export PATH=/opt/toolchain/bin:$PATH
    arm-linux-gcc --version

6. 关键注意事项与工程建议

6.1 硬件安全

  • 断电操作:所有接线必须在断电状态下进行。
  • 电源匹配:确认传感器电压范围(10~30V),勿直接接5V。

6.2 软件健壮性

  • 异常处理 :对 open/read/select 等系统调用做错误检查。
  • 边界保护:缓冲区操作前检查长度,防止溢出。
  • 日志记录 :重定向输出到文件(> log 2>&1),便于排查。

6.3 开发流程建议

  1. 先模拟后实测:用 TCP 程序验证协议逻辑。
  2. 逐步集成:先通串口 → 再解析单个传感器 → 最后多设备轮询。
  3. 文档先行:记录每个传感器的协议细节(寄存器地址、单位等)。

核心思想:利用 485 主从模式简化解析逻辑,通过校验和+超时机制保障可靠性。

相关推荐
海棠蚀omo9 小时前
Linux基础I/O-打开新世界的大门:文件描述符的“分身术”与高级重定向
linux·操作系统
带土19 小时前
33. 文件IO (4) 二进制文件操作与结构体存储 文件路径与目录操作
linux
无敌最俊朗@9 小时前
C++音视频就业路线
linux·windows
Fr2ed0m9 小时前
Linux 文本处理完整指南:grep、awk、sed、jq 命令详解与实战
linux·运维·服务器
大聪明-PLUS10 小时前
使用 GitLab CI/CD 为 Linux 创建 RPM 包(一)
linux·嵌入式·arm·smarc
边疆.10 小时前
【Linux】自动化构建工具make和Makefile和第一个系统程序—进度条
linux·运维·服务器·makefile·make
2021黑白灰10 小时前
windows11 vscode ssh远程linux服务器/虚拟机 免密登录
linux·服务器·ssh
z2023050810 小时前
linux之PCIE 设备枚举流程分析
linux·运维·服务器
simple_whu10 小时前
编译tiff:arm64-linux-static报错 Could NOT find CMath (missing: CMath_pow)
linux·运维·c++