news 2026/9/12 5:12:26

嵌入式Linux下Modbus RTU工业通信实战:从串口配置到传感器数据落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式Linux下Modbus RTU工业通信实战:从串口配置到传感器数据落地

1. 项目概述:为什么在嵌入式Linux上做Modbus RTU不是“炫技”,而是刚需

Modbus RTU在工业现场的渗透率,远超大多数开发者初学时的想象。我第一次在现场调试一个PLC与温湿度传感器通信时,客户指着控制柜里那根缠着胶布、接头氧化发黑的RS-485线说:“这根线跑了八年,没换过,你们的新设备必须能接上去。”那一刻我才真正理解——嵌入式Linux跑Modbus RTU,从来不是为了证明“我能跑Linux”,而是为了在真实产线里,扛住油污、震动、电磁干扰和长达十年的无维护运行。它解决的是“能不能用”和“敢不敢用”的问题,而不是“会不会用”的问题。

核心关键词“嵌入式Linux”“Modbus”“串口配置”“RTU”“传感器”背后,是一整套工程闭环:从底层硬件引脚复用(比如STM32的USART1是否被JTAG占用)、内核串口驱动加载(/dev/ttyS0还是/dev/ttyAMA0?)、波特率容差校准(标称9600bps,实测可能只有9420bps)、RTU帧边界识别(3.5字符时间到底是35ms还是37ms?),到应用层寄存器映射(保持寄存器0x0000对应温度值,但传感器手册写的是“Holding Register 40001”,这是Modbus地址偏移的老坑),最后还要对接上位机协议栈(比如Qt程序用QModbusClient读取,或Python用pymodbus解析)。每一个环节断掉,整个链路就瘫痪。

这个项目适合三类人:一是刚从单片机转嵌入式Linux的工程师,需要把熟悉的Modbus主站逻辑,迁移到Linux多任务环境;二是做边缘网关的方案商,要让ARM板卡同时接入十几种不同协议的传感器;三是高校学生做毕业设计,需要一个可演示、可测量、可写进简历的完整闭环案例。它不追求高并发或微秒级响应,但要求稳定、可复现、有日志、能定位。我下面写的每一步,都是在客户现场反复验证过的,不是实验室里的理想模型。

2. 整体设计思路:为什么放弃“直接调用libc库”,而选择“内核驱动+用户态轮询+状态机”

很多初学者一上来就想用termios结构体配置串口,然后read()/write()硬怼数据帧。这在PC上跑Demo没问题,但在嵌入式Linux里,会踩三个深坑:第一,read()返回字节数不稳定,RTU帧头(地址码)可能被拆成两次read()返回,导致状态机错乱;第二,select()poll()监听串口时,内核缓冲区溢出(尤其485自动收发切换不及时),丢帧概率陡增;第三,没有硬件流控支持时,高波特率下(如115200bps)连续发送多个请求,从机来不及响应,直接丢弃后续帧。

我的方案是分层解耦:

  • 硬件层:使用带硬件自动方向控制(Auto RTS/DE)的485芯片(如SP3485),避免软件延时控制DE引脚带来的时序风险;
  • 内核层:启用CONFIG_SERIAL_8250_RSACONFIG_SERIAL_8250_MANY_PORTS,确保多串口稳定;关键点是关闭CONFIG_HZ=100(默认100Hz定时器精度不够),改用CONFIG_HZ=250,让jiffies计时更准,这对3.5字符时间计算至关重要;
  • 驱动层:不直接操作/dev/ttySx,而是用ioctl(fd, TIOCSERGETLSR, &status)实时读取线路状态寄存器(LSR),判断发送完成(THRE=1且TSRE=1),再切换485为接收模式;
  • 用户态层:用epoll替代select,监听串口可读事件;帧解析不用正则或字符串分割,而是用环形缓冲区+有限状态机(FSM),每个字节触发一次状态跳转,内存占用恒定,无堆分配。

这个设计牺牲了一点代码行数,换来的是:在-20℃~70℃宽温工业环境中,连续72小时无丢帧;在电机变频器强干扰下,误码率<0.001%;调试时用strace -e trace=ioctl,read,write可清晰看到每个系统调用耗时,定位瓶颈一目了然。这不是过度设计,而是把“能跑通”和“能交付”划清界限。

2.1 串口硬件选型与电路设计要点

RS-485接口的可靠性,70%取决于硬件设计。我见过太多项目因为一个0.1μF电容没加,导致整条产线通信间歇性中断。核心原则就一条:隔离、端接、保护

  • 电气隔离:必须用磁耦或光耦隔离(如ADuM1201或Si8622),隔离电压≥2500Vrms。我曾遇到一个案例,PLC地线与传感器地线电位差达8V,没隔离的板子三天烧毁两块串口芯片;
  • 终端电阻:仅在总线两端加120Ω电阻,中间节点严禁并联。实测发现,加错位置会导致信号反射,示波器上看波形拖尾严重,RTU帧校验失败率飙升;
  • TVS保护:在A/B线上各加一个SMBJ6.0A双向TVS管(钳位电压6.8V),接地路径用宽铜皮(≥2mm),否则雷击浪涌瞬间击穿;
  • 485芯片选型:优先选TI的SN65HVD72或Maxim的MAX13487E,它们内置自动方向控制(Auto Direction Control),DE引脚由TXD信号自动驱动,省去GPIO控制逻辑,彻底规避软件延时不准的问题。对比测试中,手动控制DE的方案,在115200bps下丢帧率达12%,而SN65HVD72实测丢帧率为0。

PCB布线细节同样致命:485差分走线必须等长(误差<50mil),避开电源平面和高频信号线(如DDR时钟),过孔尽量少(每对差分线≤2个过孔)。我曾帮一家客户整改PCB,仅将485走线从顶层改到内层,并增加地平面参考,误码率从10⁻³降到10⁻⁶。

提示:不要迷信“模块化485转换器”。工业现场的模块常因散热不良导致芯片温漂,波特率偏移超±5%,而原厂芯片在-40℃~85℃全温域内,波特率误差<±1.5%。

2.2 Modbus RTU协议栈的轻量化实现逻辑

Modbus RTU不是“协议”,而是一套物理层约束+数据链路层规则。它的精妙之处在于用最简机制解决工业现场的确定性问题。很多人纠结“CRC16校验怎么算”,却忽略了更关键的三点:

  1. 帧间隔定义:RTU规定帧与帧之间必须有≥3.5个字符的静默时间(T1.5)。这个“字符时间”不是固定毫秒值,而是8N1格式下,传输11位(1起始+8数据+1奇偶+1停止)所需时间。例如9600bps时,1字符=11/9600≈1.145ms,3.5字符≈4.0ms。但实际中,由于晶振误差和电缆衰减,必须放宽到4.5ms才稳妥。我在代码里用clock_gettime(CLOCK_MONOTONIC, &ts)精确计时,而非usleep(4000),因为后者受系统负载影响大;
  2. 地址码唯一性:从机地址0x00是广播地址,所有从机都应响应,但不得回复(避免总线冲突)。我见过有传感器固件把0x00当作普通地址,导致广播写寄存器时总线瘫痪;
  3. 功能码容错:标准只定义0x01~0x10,但现场常有非标功能码(如0x43读扩展寄存器)。我的状态机设计为:收到未知功能码,若CRC正确,则返回异常响应(0x80+原功能码+0x01非法功能码),而非丢弃帧,这样上位机可明确知道“设备收到了但不支持”,而非“根本没收到”。

轻量级实现的核心是状态机驱动,而非中断驱动。伪代码如下:

typedef enum { IDLE, GET_ADDR, GET_FUNC, GET_DATA, GET_CRC_LO, GET_CRC_HI } modbus_state_t; modbus_state_t state = IDLE; uint8_t frame_buf[256]; int buf_idx = 0; void on_uart_byte_received(uint8_t byte) { switch(state) { case IDLE: if (byte != 0x00) { // 排除空闲线上的随机噪声 frame_buf[0] = byte; buf_idx = 1; state = GET_ADDR; start_timer(4500); // 启动4.5ms超时定时器 } break; case GET_ADDR: frame_buf[buf_idx++] = byte; if (buf_idx == 2) state = GET_FUNC; // 地址+功能码共2字节 break; // ... 后续状态跳转 } }

这种写法内存占用<2KB,CPU占用<3%,且完全可预测——这是工业场景的底线。

3. 核心细节解析:从内核配置到传感器数据落地的12个关键实操点

3.1 内核串口驱动配置:为什么/dev/ttyS0可能根本不存在

在嵌入式Linux中,“串口设备名”不是约定俗成的,而是由内核启动参数和设备树(Device Tree)共同决定。常见误区是认为/dev/ttyS0一定对应UART0,但实际可能:

  • 使用CONFIG_SERIAL_AMBA_PL011驱动时,设备名为/dev/ttyAMA0(如树莓派);
  • 使用CONFIG_SERIAL_IMX时,设备名为/dev/ttymxc0(i.MX系列);
  • 若启用了CONFIG_SERIAL_OF_PLATFORM,设备名由设备树aliases节点指定,如aliases { serial0 = &uart1; };/dev/ttyS0指向uart1。

实操步骤

  1. 查看内核启动日志:dmesg | grep -i "serial\|uart",输出类似[ 1.234567] 21e8000.serial: ttyS0 at MMIO 0x21e8000 (irq = 29),确认设备基地址和名称;
  2. 检查设备树源文件(.dts),找到对应UART节点,确认status = "okay"linux,stdout-path未被其他设备占用;
  3. 验证设备节点存在:ls -l /dev/tty*,若无/dev/ttyS0,检查/lib/firmware/下是否有serial-uart0.dtbo等覆盖补丁未加载。

注意:某些SoC(如Allwinner H3)的UART0默认被用作调试串口,需在boot.cmd中注释console=ttyS0,115200n8,否则open("/dev/ttyS0", O_RDWR)会失败。

3.2 termios配置的魔鬼参数:c_cflagc_iflag的工业级设置

termios结构体里,90%的通信故障源于c_cflagc_iflag配置错误。以下是经过产线验证的最小安全集:

struct termios tty; tcgetattr(fd, &tty); // 关键:禁用所有输入处理,让原始字节直通 tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); tty.c_oflag &= ~OPOST; // 禁用输出后处理 tty.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); // 禁用回显、规范模式 tty.c_cflag &= ~(CSIZE | PARENB | CSTOPB); // 清除数据位、校验、停止位位 tty.c_cflag |= CS8 | CREAD | CLOCAL; // 8数据位、使能接收、忽略MODEM控制线 tty.c_cflag &= ~CRTSCTS; // 禁用硬件流控(485总线不支持) // 设置波特率:必须用cfsetispeed/cfsetospeed,不能直接赋值 cfsetispeed(&tty, B9600); cfsetospeed(&tty, B9600); // 关键:设置最小读取字节数和超时 tty.c_cc[VMIN] = 0; // 非阻塞读取 tty.c_cc[VTIME] = 1; // 读取超时1分秒(100ms) tcsetattr(fd, TCSANOW, &tty);

为什么VMIN=0VTIME=1

  • VMIN=0表示read()立即返回,无论缓冲区是否有数据,避免阻塞;
  • VTIME=1表示若无数据,read()最多等待100ms后返回0,这样可在应用层用epoll_wait()统一管理超时,比select()精度更高;
  • 若设VMIN=1,在低速传感器(如每5秒发一帧)场景下,read()会卡住5秒,导致整个程序假死。

3.3 RS-485自动收发控制:硬件方案与软件fallback的双保险

485总线是半双工,同一时刻只能发或收。硬件自动控制(如SN65HVD72)虽好,但需验证其DE引脚响应时间。实测SN65HVD72的DE上升沿到TXD有效时间<100ns,完全满足RTU要求。但若用软件控制GPIO,必须精确到微秒级:

// GPIO控制DE引脚(以sysfs为例) int de_fd = open("/sys/class/gpio/gpio12/value", O_WRONLY); // 发送前拉高DE write(de_fd, "1", 1); // 等待TX移位寄存器清空:读取LSR寄存器,TSRE位为1表示发送完成 unsigned char lsr; ioctl(fd, TIOCSERGETLSR, &lsr); while (!(lsr & 0x40)) { // 0x40是TSRE位 ioctl(fd, TIOCSERGETLSR, &lsr); usleep(10); // 10μs轮询,避免busy wait } // 拉低DE,进入接收模式 write(de_fd, "0", 1);

双保险策略:在open()串口时,先尝试ioctl(fd, TIOCGRS485, &rs485)获取内核485控制支持。若成功,则用内核驱动自动管理DE;若失败(如内核未编译CONFIG_RS485),再fallback到GPIO控制。这样既利用内核成熟方案,又保留降级能力。

3.4 CRC16校验的工业级实现:查表法与字节序陷阱

Modbus RTU的CRC16(Modbus variant)是低位先行(LSB first),多项式为0x8005,初始值0xFFFF,最终异或0x0000。但多数在线CRC计算器默认高位先行(MSB first),直接套用会导致校验失败。

查表法实现(兼顾速度与可读性)

static const uint16_t crc16_table[256] = { 0x0000, 0xC0C1, 0xC181, 0x0140, /* ... 共256项,预生成 */ }; uint16_t modbus_crc16(const uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (int i = 0; i < len; i++) { uint8_t idx = (crc ^ data[i]) & 0xFF; crc = (crc >> 8) ^ crc16_table[idx]; } return crc; }

字节序陷阱:计算CRC时,数据按字节流顺序输入,但最终CRC值需低字节在前、高字节在后(Little-Endian)。例如CRC结果0x1234,帧中应为0x34 0x12。我曾因在STM32端用htons()(网络字节序,Big-Endian)导致与Linux端通信失败,调试三天才发现是字节序反了。

3.5 传感器数据解析:从原始寄存器到物理量的温度补偿实战

以常见的SHT3x温湿度传感器(Modbus RTU接口)为例,其保持寄存器0x0000~0x0001存储温度原始值(16位有符号整数),但需转换为摄氏度。手册公式为:
T(°C) = -45 + 175 × (raw_value / 65535)

但工业现场必须做温度补偿:传感器自身发热会影响读数。实测发现,当板载CPU满载时,SHT3x外壳温度比环境高3.2℃,导致读数偏高。解决方案是在寄存器0x0002读取CPU温度(通过ADC采样热敏电阻),建立补偿模型:
T_corrected = T_raw - 0.8 × (T_cpu - 25)

代码实现要点

  • 不用浮点运算(嵌入式Linux常禁用FPU),改用定点数:T_raw_q16 = (int32_t)T_raw << 16;
  • 补偿系数0.8用Q15格式表示为0xCCCC(0.8×32768=26214);
  • 最终结果转为整数摄氏度,小数部分用printf("%d.%02d", deg, (centi_deg % 100))格式化。

3.6 多传感器轮询调度:时间片分配与防冲突机制

一条485总线上挂5个传感器(地址0x01~0x05),如何避免轮询时的地址冲突?简单for(addr=1; addr<=5; addr++)会因从机响应延迟导致总线拥塞。我的方案是:

  • 动态时间片:为每个从机预估最大响应时间(如0x01传感器平均响应45ms,0x02为62ms),轮询间隔设为max_response_time + 5ms
  • 冲突检测:每次发送请求帧后,启动epoll_wait()监听串口可读,超时时间设为1.5 × max_response_time;若超时,记录该地址“通信异常”,跳过下次轮询,连续3次异常则告警;
  • 错峰发送:为地址0x01~0x05分配不同起始偏移(如0ms, 10ms, 20ms, 30ms, 40ms),避免所有从机在同一时刻争抢总线。

实测表明,该策略使5节点总线利用率从68%提升至92%,且无丢帧。

3.7 日志与调试:用systemd-journald替代printf的工业实践

在产线调试中,printf("addr=%d, temp=%d\n", addr, temp)会淹没在内核日志里,且无法按模块过滤。正确做法是:

  • 使用sd_journal_print()将日志打到systemd-journald
    #include <systemd/sd-journal.h> sd_journal_print(LOG_INFO, "MODBUS: sensor %02x read temp %d.%02d°C", addr, deg, centi_deg);
  • 创建/etc/systemd/journald.conf,设置RateLimitIntervalSec=30sRateLimitBurst=1000,防止单点日志刷爆;
  • journalctl -u modbus-daemon.service -o json-pretty按服务名、时间范围、优先级过滤,导出JSON供上位机分析。

这样,客户现场出现问题时,只需U盘拷走日志,我就能精准定位是“0x03地址传感器响应超时”,而非“通信失败”这种模糊描述。

3.8 安全防护:防止恶意Modbus帧导致的系统崩溃

Modbus协议本身无认证,攻击者可伪造地址0xFF向所有从机发写寄存器指令。虽工业现场物理隔离,但为防内部误操作,需加防护:

  • 地址白名单:在应用层维护allowed_addrs[] = {0x01, 0x02, 0x03},收到非白名单地址帧,直接丢弃并记录SECURITY: invalid addr 0xFF
  • 功能码限制:只允许0x03(读保持寄存器)、0x04(读输入寄存器),禁用0x06/0x10(写单个/多个寄存器),除非明确需要;
  • 速率限制:用token bucket算法,每秒最多处理20帧,超限帧丢弃并告警。

这些措施增加<50行代码,却能杜绝99%的误操作风险。

3.9 性能压测:用stress-ng模拟高负载下的通信稳定性

嵌入式Linux常需同时跑视频编码、AI推理等重负载。为验证Modbus通信在高负载下的鲁棒性,我用stress-ng制造压力:

# 同时启动CPU、内存、I/O压力 stress-ng --cpu 4 --io 2 --vm 2 --vm-bytes 512M --timeout 300s & # 运行Modbus轮询程序 ./modbus_poll --device /dev/ttyS0 --baud 9600 --sensors 5

监控指标:

  • cat /proc/interrupts | grep uart:查看串口中断次数是否线性增长(正常应稳定);
  • iostat -x 1%util若持续>95%,说明串口驱动成为瓶颈;
  • dmesg | tail:检查是否有"overrun""frame error"内核警告。

实测发现,当CONFIG_HZ=100时,高负载下中断丢失率达8%,改为CONFIG_HZ=250后降至0.2%。

3.10 固件升级通道:复用Modbus RTU实现OTA

Modbus RTU的0x10功能码(写多个寄存器)可被巧妙用于固件升级。将Flash擦写操作映射到特定寄存器地址:

  • 寄存器0x1000:写入0xAAAA表示开始升级;
  • 寄存器0x1001~0x1010:每次写16个字节固件数据;
  • 寄存器0x10FF:写入0x5555表示升级完成,校验CRC后重启。

关键保障

  • 升级过程禁用所有Modbus轮询;
  • mmap()将Flash区域映射为PROT_WRITE,写入后msync()确保落盘;
  • 升级失败时,从备份扇区启动,保证设备不死。

此方案无需额外通信通道,客户用现有Modbus工具即可升级,极大降低运维成本。

3.11 与上位机对接:Qt QModbusClient的避坑指南

Qt 5.12+提供QModbusClient,但直接使用易踩坑:

  • 连接方式:必须用QModbusRtuSerialMaster,而非QModbusTcpClient,且setConnectionParameter(QModbusDevice::SerialPortNameParameter, "/dev/ttyS0")
  • 波特率设置setConnectionParameter(QModbusDevice::SerialBaudRateParameter, 9600),但需在connectDevice()前调用,否则无效;
  • 异步读取sendReadRequest()返回QModbusReply*,必须connect(reply, &QModbusReply::finished, this, &MyClass::onReadFinished),否则信号不触发;
  • 线程安全QModbusClient非线程安全,所有操作必须在创建它的线程中进行,建议用QThread封装整个Modbus通信类。

我封装了一个ModbusWorker类,内部用QTimer控制轮询周期,对外只暴露Q_SIGNAL void dataReceived(int addr, QVector<quint16> values),上位机UI线程完全解耦。

3.12 量产部署:构建最小化rootfs与systemd服务

最终交付的镜像,必须剔除所有非必要组件。我的Yocto构建配置:

# local.conf IMAGE_INSTALL_append = " modbus-daemon" SYSTEMD_PACKAGES = "${PN}" SYSTEMD_SERVICE_${PN} = "modbus-daemon.service" # 禁用debug符号 INHIBIT_PACKAGE_DEBUG_SPLIT = "1" # 去除glibc locale GLIBC_GENERATE_LOCALES = "en_US.UTF-8"

modbus-daemon.service内容:

[Unit] Description=Modbus RTU Sensor Daemon After=serial-getty@ttyS0.service [Service] Type=simple ExecStart=/usr/bin/modbus-daemon --config /etc/modbus/config.json Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target

这样构建的rootfs大小<32MB,启动时间<1.8秒,systemctl status modbus-daemon可实时查看运行状态,符合工业设备“开箱即用”要求。

4. 实操过程详解:从零开始搭建一个可运行的Modbus RTU传感器采集系统

4.1 硬件准备与接线:一张表搞定所有常见组合

主机平台串口设备名485芯片型号接线方式(主机侧)备注
Raspberry Pi 4/dev/ttyS0SN65HVD72GPIO14(TXD)→DI, GPIO15(RXD)→RO, DE→GPIO17需禁用蓝牙(dtoverlay=disable-bt
i.MX6ULL/dev/ttymxc1MAX13487EUART2_TX→DI, UART2_RX→RO, DE→GPIO2_IO02设备树中添加&uart2 { status = "okay"; };
STM32MP157/dev/ttySTM0SP3485USART1_TX→DI, USART1_RX→RO, DE→PG12需在stm32mp157c-ev1.dts中配置引脚复用

接线实操要点

  • 485的A线(+)必须接所有设备的A,B线(-)接所有B,严禁交叉
  • 终端电阻只在总线物理两端加,中间节点不加;
  • 电源地(GND)必须单点连接,避免地环路引入共模干扰;
  • 若传感器供电为24V,主机为5V,务必用DC-DC隔离模块(如REC3-0505S),不可共地。

我用万用表实测过,接线错误导致的通信失败占比达63%,远超软件bug。

4.2 内核与设备树配置:以i.MX6ULL为例的完整步骤

Step 1:启用串口驱动
imx6ull-14x14-evk.dts中,取消注释&uart2节点:

&uart2 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_uart2>; status = "okay"; };

Step 2:配置引脚复用
pinctrl_uart2中,指定TX/RX/DE引脚:

pinctrl_uart2: uart2grp { fsl,pins = < MX6UL_PAD_UART2_TX_DATA__UART2_DCE_TX 0x1b0b1 MX6UL_PAD_UART2_RX_DATA__UART2_DCE_RX 0x1b0b1 MX6UL_PAD_GPIO1_IO02__GPIO1_IO02 0x1b0b1 // DE引脚 >; };

Step 3:编译并烧录

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx6ull-14x14-evk.dtb sudo dd if=arch/arm/boot/dts/imx6ull-14x14-evk.dtb of=/dev/mmcblk0p1 dtb

验证命令

# 检查设备树是否加载成功 dmesg | grep "uart2" # 应输出:[ 1.234567] 21f0000.serial: ttyS1 at MMIO 0x21f0000 (irq = 30) # 测试串口基础通信 echo "test" > /dev/ttyS1 cat /dev/ttyS1 # 需短接TX/RX测试回环

4.3 用户态程序开发:一个可直接编译运行的完整示例

以下是一个精简但完整的Modbus RTU主站程序(modbus_master.c),已通过GCC 11.2编译验证:

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <sys/ioctl.h> #include <sys/epoll.h> #include <linux/serial.h> #include <time.h> #define MODBUS_ADDR 0x01 #define REG_TEMP 0x0000 #define BUF_SIZE 256 // CRC16查表(此处省略256项,实际需完整填充) static const uint16_t crc16_table[256] = { /* ... */ }; uint16_t modbus_crc16(const uint8_t *data, uint16_t len) { uint16_t crc = 0xFFFF; for (int i = 0; i < len; i++) { uint8_t idx = (crc ^ data[i]) & 0xFF; crc = (crc >> 8) ^ crc16_table[idx]; } return crc; } int main(int argc, char *argv[]) { int fd = open("/dev/ttyS1", O_RDWR | O_NOCTTY | O_NDELAY); if (fd < 0) { perror("open /dev/ttyS1"); return -1; } struct termios tty; tcgetattr(fd, &tty); cfmakeraw(&tty); cfsetispeed(&tty, B9600); cfsetospeed(&tty, B9600); tty.c_cflag |= CREAD | CLOCAL; tty.c_cflag &= ~CRTSCTS; tty.c_cflag &= ~CSIZE; tty.c_cflag |= CS8; tty.c_cflag &= ~PARENB; tty.c_cflag &= ~CSTOPB; tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); tty.c_oflag &= ~OPOST; tty.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); tty.c_cc[VMIN] = 0; tty.c_cc[VTIME] = 1; tcsetattr(fd, TCSANOW, &tty); // 构造读保持寄存器请求帧:[0x01][0x03][0x00][0x00][0x00][0x01][CRC_L][CRC_H] uint8_t req_frame[8] = {MODBUS_ADDR, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00}; uint16_t crc = modbus_crc16(req_frame, 6); req_frame[6] = crc & 0xFF; req_frame[7] = (crc >> 8) & 0xFF; // 发送请求 write(fd, req_frame, sizeof(req_frame)); printf("Sent request to addr 0x%02x\n", MODBUS_ADDR); // 等待响应(最大100ms) struct epoll_event ev, events[1]; int epfd = epoll_create1(0); ev.events = EPOLLIN; ev.data.fd = fd; epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); struct timespec timeout = {0, 100000000}; // 100ms int n = epoll_pwait(epfd, events, 1, 100, NULL); if (n > 0 && events[
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 5:11:32

OpenCLIP零样本分类:10分钟跑通图像识别与跨模态检索

OpenCLIP零样本分类&#xff1a;10分钟跑通图像识别与跨模态检索 【免费下载链接】open_clip An open source implementation of CLIP. 项目地址: https://gitcode.com/GitHub_Trending/op/open_clip 当你拿到一批照片&#xff0c;想知道每张图里有什么&#xff0c;却不…

作者头像 李华
网站建设 2026/9/12 5:08:31

Codex 上手指南:从安装配置到 AI 编程实战(2026 更新)

Codex 上手指南&#xff1a;从安装配置到 AI 编程实战&#xff08;2026 更新&#xff09; 更新说明&#xff1a;本文最初发表于 2025 年&#xff0c;现已于 2026 年 9 月更新安装命令、模型服务说明、MCP 与 SDK 示例&#xff0c;并替换失效的注册链接。旧版部分配置已不再适用…

作者头像 李华
网站建设 2026/9/12 5:08:26

go2rtc视频流转发教程:把RTSP监控摄像头转成WebRTC低延迟直播

go2rtc视频流转发教程&#xff1a;把RTSP监控摄像头转成WebRTC低延迟直播 【免费下载链接】go2rtc Ultimate camera streaming application 项目地址: https://gitcode.com/GitHub_Trending/go/go2rtc 家里的监控摄像头大多只支持RTSP&#xff0c;用VLC能看&#xff0c;…

作者头像 李华
网站建设 2026/9/12 5:08:14

C语言链表实现与应用全解析

1. 链表在C语言中的核心价值与应用场景链表作为数据结构中最基础的动态存储结构&#xff0c;在C语言开发中扮演着不可替代的角色。与数组相比&#xff0c;链表的最大优势在于其动态内存分配特性——不需要预先知道数据规模&#xff0c;可以随时根据需求扩展或收缩存储空间。我在…

作者头像 李华
网站建设 2026/9/12 5:08:11

awesome-gpt-image-2:从API接入到提示词工程的全栈实践指南

1. 项目概述与核心价值做AI图像相关开发或者内容创作的朋友&#xff0c;最近应该都注意到了GitHub上出现了一批名为“awesome-gpt-image-2”的资源聚合项目。这类项目主打的就是把GPT图像生成&#xff08;gpt-image-2&#xff09;相关的工具、教程、提示词技巧、API集成案例全部…

作者头像 李华