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 主从模式简化解析逻辑,通过校验和+超时机制保障可靠性。

相关推荐
Yana.nice1 小时前
openssl将证书从p7b转换为crt格式
java·linux
AI逐月1 小时前
tmux 常用命令总结:从入门到稳定使用的一篇实战博客
linux·服务器·ssh·php
小白跃升坊2 小时前
基于1Panel的AI运维
linux·运维·人工智能·ai大模型·教学·ai agent
跃渊Yuey2 小时前
【Linux】线程同步与互斥
linux·笔记
舰长1152 小时前
linux 实现文件共享的实现方式比较
linux·服务器·网络
zmjjdank1ng2 小时前
Linux 输出重定向
linux·运维
路由侠内网穿透.2 小时前
本地部署智能家居集成解决方案 ESPHome 并实现外部访问( Linux 版本)
linux·运维·服务器·网络协议·智能家居
VekiSon3 小时前
Linux内核驱动——基础概念与开发环境搭建
linux·运维·服务器·c语言·arm开发
zl_dfq3 小时前
Linux 之 【进程信号】(signal、kill、raise、abort、alarm、Core Dump核心转储机制)
linux
Ankie Wan3 小时前
cgroup(Control Group)是 Linux 内核提供的一种机制,用来“控制、限制、隔离、统计”进程对系统资源的使用。
linux·容器·cgroup·lxc