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 命令码定义
typedef enum {GET_TEMP = 1, // 获取温度GET_HUMI, // 获取湿度GET_ALL // 获取全部(温度+湿度)
} CMD;
2.3 核心函数设计原则
- 封装与发送分离:打包函数只负责生成字节流,不直接调用
send()。 - 模块化:便于扩展(如增加设备 ID、时间戳等字段)。
- 边界检查:防止缓冲区溢出、空指针等。
3. TCP 模拟测试程序详解(ser.c / cli.c)
💡 用途:在无硬件条件下验证协议逻辑。使用 TCP 模拟串口通信。
3.1 公共定义(两端一致)
#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 主机
数据结构
typedef struct __data {char temp; // 温度(0~99)char humi; // 湿度(0~99)
} DATA;
函数1:根据命令组织有效数据
// 输入:原始数据、命令类型;输出:填充后的数据缓冲区;返回:有效数据长度
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:按协议封装完整数据包
// 输入:待封装的有效数据及其长度;输出:完整协议包;返回:总包长度
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 服务并循环发送
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次篡改校验和// 发送并休眠50mssend(conn, send_buf, len, 0);usleep(50 * 1000);}close(listfd);close(conn);return 0;
}
理想运行结果:
- 每 50ms 输出一行温湿度值(如
temp = 45 humi = 78)。 - 客户端能正确解析大部分数据包,并过滤掉被篡改的无效包。
3.3 客户端程序(cli.c)—— 模拟 485 从机解析器
主循环:接收并解析数据
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+tailmemcpy(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 串口配置函数
#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 校验
// 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)
#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 主函数:查询并解析光照值
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 连接与登录
# PC设置IP(与开发板同网段)
sudo ifconfig eth0 192.168.0.100# SSH登录(默认IP: 192.168.0.232)
ssh root@192.168.0.232
5.3 基础服务测试
# 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 开机自启动配置
# 创建启动脚本(数字越小优先级越高)
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(适合小型程序)。 - 交叉编译:
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 开发流程建议
- 先模拟后实测:用 TCP 程序验证协议逻辑。
- 逐步集成:先通串口 → 再解析单个传感器 → 最后多设备轮询。
- 文档先行:记录每个传感器的协议细节(寄存器地址、单位等)。
✅ 核心思想:利用 485 主从模式简化解析逻辑,通过校验和+超时机制保障可靠性。
