当前位置: 首页 > news >正文

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_HEAD1B包头:0x55
Length1B有效数据长度(含命令码 + 数据)
CMD1B命令类型(见下文枚举)
DatanB实际数据(如温度、湿度)
Checksum1B校验和 = sum(CMD + Data)
PACK_TAIL1B包尾: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 开发流程建议

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

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

http://www.dtcms.com/a/557382.html

相关文章:

  • 中文编码、乱码问题解析处理
  • 如何设计一款百兆网络监控器H81220S
  • 2025年ASOC SCI2区TOP,双重防御网络阻断模型下的供给路线优化,深度解析+性能实测
  • seo关键词教程国外seo综合查询
  • 郑州网站建设饣汉狮网络wordpress重置主题
  • 算法——二叉树、dfs、bfs、适配器、队列练习
  • Linux_Socket_浅谈UDP
  • dfs|位运算
  • 网站开发内容商用图片的网站
  • 凡客建站免费的可以用多久win优化大师有用吗
  • DevOps的实现路径与关键实践
  • 开发实战 - ego商城 - 6 购物车模块
  • 虚幻引擎5 GAS开发俯视角RPG游戏 P06-28 构建属性菜单小部件控制器
  • 线程协作——生产者消费者问题:
  • ROS2系列 (14) : 服务通信介绍——双向通信的核心机制
  • C语言入门(十三):操作符详解(1)
  • 化妆品设计网站家用宽带做网站
  • 雄安建设集团 网站湖北做网站教程哪家好
  • 晋城市 制作网站织梦网站文章发布模板下载
  • Microsoft Speech TTS微软语音识别ISpeechRecoGrammar,ISpeechRecoResult
  • 【Java 开发日记】运行时有出现过什么异常?
  • 企业门户网站设计扬州网页设计培训
  • 从大模型中的chat_template了解jinja模板语法
  • Pandas--数据选择与索引
  • Linux下编译WebSocket++
  • 淄博哪家公司做网站最好莱钢吧贴吧
  • 调试的艺术:从崩溃到洞察的全面指南
  • 深入洞察:大模型服务之MaaS平台
  • 1024.5不是数位和--------题解
  • 加强门户网站建设的通知博客和网站的区别