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

Linux下串口与TCP双向实时透传工具,纯C实现免依赖

本文还有配套的精品资源,点击获取

简介:一个轻量级Linux串口转TCP透传程序,支持/dev/ttyS0、/dev/ttyUSB0等串口设备与远端TCP服务器(指定IP和端口)之间双向透明转发原始字节流。数据从串口读入后立即发往网络,网络收到的数据也直接写入串口,无协议解析、无缓存延迟、无格式转换。核心功能分两部分:comdev.c封装串口初始化、波特率配置、非阻塞读写及错误恢复逻辑;main.c负责TCP socket连接建立、重连机制、以及单线程内轮询串口与socket的双向转发。整个项目用标准C编写,不依赖libserial、libusb等第三方库,仅需POSIX系统调用,可直接在ARM/x86嵌入式Linux平台交叉编译运行。附带Makefile实现一键编译,.gitignore和完整头文件(comdev.h)便于集成进现有工程。test目录含简易验证脚本,方便快速确认透传通路是否正常。适用于PLC、传感器、工控模块等串口设备接入以太网场景,也适合远程串口调试、AT指令透传、Modbus TCP桥接等低开销需求。

1. 项目概述:为什么一个“不做事”的程序反而最难写?

你有没有遇到过这种场景:现场一台老式PLC只留了一个RS-485串口,但客户要求它能被云平台远程读取数据;或者调试一款新模组时,手边只有USB转串口线,却想用PC上的Python脚本实时发AT指令——但又不想装SecureCRT、minicom、socat这些带一堆依赖的工具?我去年在给一家做智能电表集抄系统的客户做现场支持时,就卡在了这个问题上:他们的ARM Cortex-A7嵌入式板子跑的是精简版OpenWrt,glibc版本老旧,连nc(netcat)都因为缺少libreadline被裁掉了,更别说socat这种动辄十几MB的重型工具。最后我们硬是用不到300行标准C代码搭了个透传桥,三天内完成部署,至今还在产线上稳定跑着。

这个项目说白了就干一件事:让/dev/ttyUSB0和192.168.1.100:502之间变成一根“看不见的串口线”。它不做任何协议解析——不识别Modbus帧头、不校验CRC、不拆解AT指令、不转换换行符;它也不做缓存优化——收到一个字节就发一个字节,网络侧发来一个字节就立刻往串口塞一个字节;它甚至不处理粘包或分包——TCP流是啥样,串口输出就是啥样,反过来也一样。听起来很简单?恰恰相反,正是这种“什么都不做”,让它成了嵌入式现场最考验基本功的程序之一。

核心关键词“串口转TCP”“linux串口通信”“透传工具”“嵌入式联网”,背后对应的是四重硬约束:
-零依赖:不能链接libseriallibusblibev,连pthread都得慎用(有些RTOS兼容层根本不支持);
-低资源:静态编译后体积要压到100KB以内,内存常驻占用低于32KB;
-强实时:从串口RX引脚收到电平变化,到数据出现在远端TCP socket缓冲区,端到端延迟必须控制在20ms内(工业现场PLC轮询周期通常是50ms);
-高鲁棒:串口线被老鼠咬断、网线被保洁阿姨拔掉、远端服务器宕机重启——程序必须自己检测、自动重连、不丢数据、不卡死。

它不是玩具,而是工业现场的“数字管道工”。你不会天天盯着它看,但一旦它堵了,整条产线的数据就断了。所以它的代码风格和设计哲学,和普通应用软件完全不同:没有花哨的面向对象封装,没有异步回调树,没有配置文件解析器——只有对POSIX系统调用的精准拿捏,对termios结构体每个字段的敬畏,对select()超时精度的反复实测,以及对EINTREAGAINEIO这些错误码像老朋友一样的熟悉。

我把它叫做“哑管道”——它不说话,不思考,不记忆,只忠实地搬运每一个字节。而恰恰是这种极致的克制,让它能在资源紧张的嵌入式Linux上,扛住7×24小时不间断运行的压力。下面我们就一层层剥开这个“哑管道”的肌肉与神经。

2. 整体架构与设计思路:单线程阻塞模型为何是工业现场的最优解?

很多人第一反应是:“单线程?还阻塞?这不早该淘汰了吗?”——在Web服务或桌面应用里确实如此。但在工业串口透传这个特定场景下,单线程阻塞模型反而是经过十年现场验证的“黄金方案”。让我用三个真实踩过的坑来解释为什么。

2.1 为什么不用多线程?

早期我们试过为串口和网络各开一个线程,用pthread_mutex保护共享缓冲区。结果在某款国产RK3328工控板上,连续运行48小时后必现死锁。抓取/proc/[pid]/stack发现:串口线程卡在tcdrain()等待发送完成,网络线程正持有互斥锁准备往缓冲区写数据,而串口线程又需要锁来读缓冲区……典型的AB-BA死锁。更麻烦的是,某些ARM平台的pthread实现对SCHED_FIFO实时调度支持不完整,线程优先级翻车后,串口接收中断响应延迟飙升到150ms,直接导致串口FIFO溢出丢帧。

提示:工业现场串口波特率常为115200甚至921600,按115200算,每字节传输耗时约87μs。若中断响应延迟超过1ms,10字节就可能溢出。多线程引入的上下文切换开销(ARMv7典型值1.2μs/次)在此场景下不是优化,而是负担。

2.2 为什么不用epoll或libuv?

epoll当然高效,但它要求文件描述符必须是非阻塞模式。而Linux串口设备(/dev/ttyS0等)在非阻塞模式下有个致命缺陷:当串口硬件FIFO未满时,write()会立即返回成功,但实际数据可能还卡在UART控制器的TX FIFO里。此时若网络侧突然断连,程序在epoll_wait()返回后准备close()串口,却因TX FIFO未空而触发EAGAIN错误——更糟的是,tcdrain()在非阻塞fd上会直接失败。我们曾因此导致一批传感器在断网瞬间丢失最后3帧关键数据。

而阻塞式select()则天然规避了这个问题:它只在fd真正可读/可写时才返回,且write()阻塞直到数据进入内核TX缓冲区(注意:不是硬件FIFO,但已足够可靠)。虽然select()有1024fd数量限制,但透传工具永远只监控2个fd(串口+socket),完全无压力。

2.3 为什么坚持纯C+POSIX,拒绝一切高级封装?

客户现场的交叉编译环境五花八门:有的用Buildroot自建toolchain,glibc版本2.23;有的用Yocto,musl libc;还有用裸机uCLibc的。某次给风电变流器厂商交付时,他们要求静态链接,结果libserial依赖的libudev又依赖libblkid,最终二进制膨胀到2.1MB,刷写进SPI Flash都困难。而纯POSIX方案,用arm-linux-gnueabihf-gcc -static编译,开启-Os优化后,成品仅86KB,且在glibc/musl/uClibc三大libc上全部原生兼容。

整个架构就一张图(文字描述):

[串口硬件] → [内核TTY层] → [用户态comdev.c] ↓ [main.c主循环] ← select()轮询 → [TCP socket] → [远端服务器]
  • comdev.c是“串口管家”:专注把/dev/ttyS0变成一个听话的字节流管道,暴露com_open()com_read()com_write()三个接口;
  • main.c是“交通调度员”:用select()同时监听串口fd和socket fd,哪个就绪就处理哪个,绝不越界;
  • Makefile是“一键施工队”:预置ARM/x86交叉编译规则,make ARCH=arm即可生成目标平台可执行文件。

这种极简分层,让二次开发变得极其直观:想改波特率?只动comdev.ccfsetispeed()那几行;想加重连指数退避?只改main.c里的connect_with_retry()函数;想支持RTS/CTS硬件流控?在comdev.ctermios配置段补两行c_cflag |= CRTSCTS就行。没有抽象泄漏,没有魔法黑盒,每一行代码都直面硬件。

3. 核心细节解析:串口初始化与错误恢复的魔鬼在参数里

comdev.c表面只有200多行,但里面藏着工业现场十年积累的“串口玄学”。很多开发者以为设置波特率就是cfsetispeed(&tty, B115200),然后tcsetattr()完事——这在实验室能跑通,到了现场必然翻车。下面拆解几个关键细节,全是血泪教训。

3.1 波特率设置:BOTHER才是真正的自由

Linux内核对标准波特率(B9600、B115200等)做了特殊优化,但工业设备常用一些非标速率,比如76800(某些老PLC)、460800(高速传感器)、甚至1.5Mbps(激光测距仪)。若强行用BOTHER,需手动计算divisor。以ARM AM335x平台为例,UART模块基准时钟为48MHz,要得到460800bps:

divisor = round(48000000 / (16 * 460800)) = round(6.51) = 7 实际波特率 = 48000000 / (16 * 7) = 428571bps (误差6.8%)

这显然不行。正确做法是启用ASYNC_SPD_CUST标志,并通过ioctl(fd, TIOCSSERIAL, &serinfo)设置custom divisor。我们在comdev.c中封装了com_set_custom_baudrate()函数,内部先ioctl(fd, TIOCGSERIAL, &serinfo)读取当前serial info,修改serinfo.divisor后再次TIOCSSERIAL写入。实测在AM335x上将460800误差压缩到0.1%以内。

3.2 termios配置:七个必须关闭的“安全开关”

默认termios开启了一堆面向人机交互的特性,对机器通信全是干扰:

// comdev.c 关键配置段 tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXOFF | IXANY); tty.c_oflag &= ~(OPOST | ONLCR | OCRNL | ONOCR | ONLRET | OFILL | OFDEL); tty.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN | ECHOE | ECHOK | ECHOKE);

逐个解释危害:
-ICRNL:把\r转成\n——Modbus ASCII帧里\r\n是帧尾标记,一转就废;
-ECHO:串口自发自收回显——AT指令调试时有用,但透传时会导致远端收到重复指令;
-IXON/IXOFF:软件流控——工业设备极少支持XON/XOFF,开了反而让数据流被误判暂停;
-ISTRIP:强制把高位清零——某些设备用第8位传校验位,一清就丢数据。

最隐蔽的是VMINVTIME。很多人设VMIN=1, VTIME=0以为就是“有数据就读”,但实测发现:当串口突发大量数据(如固件升级),内核TTY层可能因调度延迟,导致read()一次只返回几十字节,而select()又没触发(因为缓冲区还有数据),形成“假死”。我们的解法是设VMIN=0, VTIME=1(1分秒超时),确保每次read()要么读满缓冲区,要么超时返回,配合select()的100ms超时,完美平衡实时性与吞吐。

3.3 错误恢复:EIO不是终点,而是起点

串口设备最常见故障是热插拔(USB转串口)或线缆松动,内核会向进程发送SIGIO,但我们的程序没注册信号处理器,所以只会收到read()返回-1并置errno=EIO。如果就此退出,现场就得人工去机柜重启——这不可接受。

comdev.ccom_read()函数对此做了三重防护:
1. 检测到EIO后,先ioctl(fd, TIOCMGET, &status)读取控制线状态(DSR/CD等),确认是否真断连;
2. 若确认断连,执行com_close()清理资源,然后休眠2秒(避免高频重试烧坏USB PHY);
3. 在main.c主循环中,当com_read()返回COM_ERR_DISCONNECT时,触发串口重开逻辑:com_open()com_set_baudrate()com_flush()清空残留数据。

我们还在test/目录下放了个stress_test.sh脚本,模拟每30秒拔插USB串口线,连续运行72小时验证——程序从未漏帧,重连平均耗时1.8秒。

注意:com_flush()不是简单的tcflush(fd, TCIOFLUSH)。后者只清内核缓冲区,而硬件FIFO里的数据还在。我们额外调用ioctl(fd, TIOCMBIS, &bits)拉高RTS/DSR线制造握手失败,再tcdrain()确保TX FIFO清空,这才是真正的“物理级刷新”。

4. 实操过程与核心环节实现:从Makefile到双向转发的每一行代码

现在我们动手把设计落地。整个流程分为四步:环境准备→编译部署→配置运行→验证调优。所有操作均在Ubuntu 22.04 x86_64主机上完成,目标平台为ARM Cortex-A9(使用arm-linux-gnueabihf-gcc)。

4.1 环境准备与交叉编译链配置

首先确认交叉编译工具链已安装:

$ arm-linux-gnueabihf-gcc --version arm-linux-gnueabihf-gcc (Ubuntu 11.4.0-1ubuntu1~22.04.1) 11.4.0

若未安装,执行:

sudo apt update && sudo apt install gcc-arm-linux-gnueabihf

关键点:不要用--static直接链接所有库glibc静态链接会引入大量无关符号,导致体积暴增。正确姿势是只静态链接libc,动态链接其他(但本项目实际无需其他库):

# Makefile 中的关键编译规则 CC_arm = arm-linux-gnueabihf-gcc CFLAGS_arm = -Os -Wall -Wextra -std=gnu99 -ffunction-sections -fdata-sections LDFLAGS_arm = -Wl,--gc-sections -static-libgcc -static-libc

-static-libc是GCC 11+新增的选项,比传统-static更精准,实测可将ARM二进制从142KB降至86KB。

4.2 Makefile详解:一行命令解决所有编译问题

Makefile设计遵循“零配置”原则,所有平台适配通过变量注入:

# 默认构建x86_64本地版 ARCH ?= x86_64 CC = $(CC_$(ARCH)) CFLAGS = $(CFLAGS_$(ARCH)) LDFLAGS = $(LDFLAGS_$(ARCH)) # 平台专用配置 CC_x86_64 = gcc CFLAGS_x86_64 = -Os -Wall -Wextra -std=gnu99 LDFLAGS_x86_64 = -static CC_arm = arm-linux-gnueabihf-gcc CFLAGS_arm = -Os -Wall -Wextra -std=gnu99 -ffunction-sections -fdata-sections LDFLAGS_arm = -Wl,--gc-sections -static-libgcc -static-libc # 构建目标 all: serial2tcp serial2tcp: main.o comdev.o $(CC) $(LDFLAGS) -o $@ $^ %.o: %.c comdev.h $(CC) $(CFLAGS) -c -o $@ $< clean: rm -f *.o serial2tcp .PHONY: all clean

执行make ARCH=arm即可生成ARM可执行文件。我们特意避免使用autoconfcmake,因为客户现场的Buildroot环境往往禁用这些高级构建系统。

4.3 main.c核心逻辑:select()轮询的精确时序控制

main.cmain()函数是整个透传的心脏,其主循环结构如下:

int main(int argc, char *argv[]) { int serial_fd = -1, sock_fd = -1; fd_set read_fds, write_fds; struct timeval timeout; // 解析命令行:./serial2tcp /dev/ttyUSB0 192.168.1.100 502 115200 parse_args(argc, argv, &serial_dev, &server_ip, &server_port, &baudrate); while (1) { // 步骤1:确保串口已打开 if (serial_fd == -1) { serial_fd = com_open(serial_dev, baudrate); if (serial_fd < 0) { log_error("Failed to open %s", serial_dev); sleep(2); continue; } } // 步骤2:确保socket已连接 if (sock_fd == -1) { sock_fd = connect_to_server(server_ip, server_port); if (sock_fd < 0) { log_warn("Connect failed, retry in 3s..."); close(serial_fd); serial_fd = -1; sleep(3); continue; } } // 步骤3:select轮询(关键!) FD_ZERO(&read_fds); FD_ZERO(&write_fds); FD_SET(serial_fd, &read_fds); // 监听串口是否有数据可读 FD_SET(sock_fd, &read_fds); // 监听socket是否有数据可读 FD_SET(serial_fd, &write_fds); // 监听串口是否可写(防TX FIFO满) FD_SET(sock_fd, &write_fds); // 监听socket是否可写(防send缓冲区满) timeout.tv_sec = 0; timeout.tv_usec = 100000; // 100ms超时 int ret = select(FD_SETSIZE, &read_fds, &write_fds, NULL, &timeout); if (ret < 0) { if (errno == EINTR) continue; // 被信号中断,重试 log_error("select error: %s", strerror(errno)); break; } // 步骤4:处理就绪事件(重点:严格按优先级处理) if (FD_ISSET(serial_fd, &read_fds)) { handle_serial_read(serial_fd, sock_fd); } if (FD_ISSET(sock_fd, &read_fds)) { handle_socket_read(sock_fd, serial_fd); } // 写就绪检查放在最后,避免读写竞争 if (FD_ISSET(serial_fd, &write_fds)) { handle_serial_write(serial_fd, sock_fd); } if (FD_ISSET(sock_fd, &write_fds)) { handle_socket_write(sock_fd, serial_fd); } } cleanup(serial_fd, sock_fd); return 0; }

这里有两个极易被忽略的细节:
-写就绪检查顺序:必须放在读就绪之后。因为handle_serial_read()可能已把数据写入socket,若此时socket send缓冲区满,handle_socket_write()会阻塞,导致整个循环卡死。而handle_serial_write()本质是清空本地发送队列,不涉及网络IO,更安全;
-超时时间100ms的由来:太短(如10ms)会导致CPU空转率过高(实测达35%);太长(如500ms)会使串口响应延迟超标。我们用逻辑分析仪实测了不同超时下的端到端延迟分布,100ms是吞吐与实时性的最佳平衡点。

4.4 双向转发实现:如何保证字节流零失真

handle_serial_read()handle_socket_read()看似简单,但暗藏玄机:

void handle_serial_read(int serial_fd, int sock_fd) { uint8_t buf[1024]; ssize_t n = com_read(serial_fd, buf, sizeof(buf)); if (n > 0) { // 关键:用send()而非write(),避免TCP Nagle算法合并小包 ssize_t sent = send(sock_fd, buf, n, MSG_NOSIGNAL | MSG_DONTWAIT); if (sent != n) { log_warn("Partial send to socket: %zd/%zd", sent, n); // 未发送完的数据暂存到本地缓冲区,下次write就绪时再发 enqueue_to_socket_buf(buf + sent, n - sent); } } else if (n == COM_ERR_DISCONNECT) { log_info("Serial disconnected"); close(serial_fd); serial_fd = -1; } }

MSG_NOSIGNAL防止SIGPIPE终止进程(socket断连时send()会触发);MSG_DONTWAIT确保非阻塞发送,避免卡死。而enqueue_to_socket_buf()实现了一个环形缓冲区,容量16KB,专门应对网络瞬时拥塞。

同理,handle_socket_read()中:

ssize_t n = recv(sock_fd, buf, sizeof(buf), MSG_DONTWAIT); if (n > 0) { // 关键:用write()而非send(),绕过socket层缓冲,直通串口驱动 ssize_t written = write(serial_fd, buf, n); if (written != n) { log_warn("Partial write to serial: %zd/%zd", written, n); // 未写入数据暂存到串口缓冲区 enqueue_to_serial_buf(buf + written, n - written); } }

这里用write()而非send(),是因为write()会直接调用tty_write(),而send()还要走socket协议栈,多一层拷贝。实测在115200波特率下,write()send()平均快1.2ms。

5. 常见问题与排查技巧实录:现场工程师的私藏笔记

再完美的设计,到了现场也会遇到千奇百怪的问题。以下是我在23个工业现场记录的真实案例及解决方案,整理成速查表:

问题现象根本原因排查命令解决方案
串口数据乱码,但用minicom测试正常comdev.c未关闭ISTRIP,第8位被清零stty -F /dev/ttyUSB0 -parenb -parodd cs8 -cstopbcom_set_attr()中添加tty.c_cflag &= ~CS8; tty.c_cflag |= CS8;强制8位数据
连接远端服务器后,串口数据发不出去远端服务器未开启SO_KEEPALIVE,TCP连接静默断开ss -tnp \| grep :502观察连接状态connect_to_server()中添加int keepalive = 1; setsockopt(sock_fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
USB转串口热插拔后,程序无法重连udev规则未配置,设备节点名从/dev/ttyUSB0变为/dev/ttyUSB1udevadm monitor --subsystem-match=tty编写/etc/udev/rules.d/99-serial.rulesSUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="myplc",代码中用/dev/myplc
高负载时串口丢帧严重select()超时设置过大,导致串口RX中断响应延迟cat /proc/interrupts \| grep ttyS0观察中断频率timeout.tv_usec从100000改为50000,并在com_read()中增加ioctl(serial_fd, TIOCGICOUNT, &counts)监控overrun计数
程序启动后CPU占用率持续30%select()超时设为0(轮询模式)top -p $(pgrep serial2tcp)检查Makefile是否误加了-DDEBUG宏,导致日志打印过多;关闭所有log_debug()调用

5.1 独家避坑技巧:三招搞定“幽灵丢帧”

所谓“幽灵丢帧”,是指用逻辑分析仪抓到串口硬件确有数据发出,但远端服务器收不到——这通常不是程序问题,而是硬件握手异常。我们总结出三招:

第一招:强制硬件流控同步
某些USB转串口芯片(如CH340)在驱动加载时会随机初始化RTS/CTS状态。在com_open()末尾添加:

int status = TIOCM_RTS | TIOCM_DTR; ioctl(fd, TIOCMBIS, &status); // 强制拉高RTS/DTR usleep(10000); // 等待10ms稳定

第二招:规避USB枚举风暴
当多个USB串口设备同时插入,内核可能因枚举顺序混乱导致/dev/ttyUSB*编号错乱。解决方案是在/etc/default/grub中添加内核参数:

usbcore.autosuspend=-1 usb-storage.delay_use=0

然后update-grub && reboot

第三招:内核TTY缓冲区调优
默认/sys/class/tty/ttyUSB0/device/bInterfaceNumber对应的缓冲区太小。实测将/sys/module/usbcore/parameters/autosuspend设为-1后,再执行:

echo 4096 > /sys/class/tty/ttyUSB0/device/bInterfaceNumber

可将USB批量传输最大包长提升至4KB,大幅降低中断频率。

5.2 验证脚本实战:test目录下的黄金组合

test/目录不只是摆设,而是经过产线验证的“三件套”:

  • test_loopback.sh:用socat创建本地回环TCP服务,验证透传通路
    bash # 启动回环服务 socat TCP4-LISTEN:502,fork SYSTEM:"cat" # 启动透传程序 ./serial2tcp /dev/ttyUSB0 127.0.0.1 502 115200 # 发送测试数据 echo -ne "\x01\x03\x00\x00\x00\x02\xc4\x0b" > /dev/ttyUSB0
    此时socat终端应立即打印相同十六进制数据。

  • test_stress.sh:模拟工业现场最严苛的断连场景
    bash # 每30秒拔插USB线,持续1小时 for i in {1..120}; do echo "Test cycle $i" ./serial2tcp /dev/ttyUSB0 192.168.1.100 502 115200 & PID=$! sleep 30 udevadm trigger --subsystem-match=tty --action=remove sleep 2 udevadm trigger --subsystem-match=tty --action=add kill $PID 2>/dev/null sleep 1 done

  • test_latency.py:用Python量化端到端延迟
    ```python
    import serial, socket, time
    ser = serial.Serial(“/dev/ttyUSB0”, 115200, timeout=1)
    sock = socket.socket()
    sock.connect((“192.168.1.100”, 502))

for _ in range(100):
start = time.time_ns()
sock.send(b”\x01\x03\x00\x00\x00\x01\x84\x0a”)
resp = ser.read(5) # Modbus响应
end = time.time_ns()
print(f”Latency: {(end-start)//1000000}ms”)
```

实测在AM335x平台上,95%的请求延迟≤18ms,完全满足工业PLC实时性要求。

6. 扩展与定制:如何把它变成你的专属工具

这个工具的价值不仅在于开箱即用,更在于它是一块“乐高底板”。根据我的经验,80%的定制需求集中在以下三类:

6.1 协议增强:在透传之上加一层薄胶水

有些场景需要轻量协议处理,比如:
-AT指令透传+自动心跳:在handle_socket_read()中检测到AT\r\n,则先发AT+CREG?\r\n,再转发原始数据;
-Modbus RTU转ASCII:在com_read()后插入转换函数,将\x01\x03\x00\x00\x00\x02\xc4\x0b转为:010300000002C40B\r\n
-数据加密透传:在enqueue_to_socket_buf()前调用AES-128-CBC加密。

关键原则:所有增强必须保持单线程,且处理时间<5ms(否则影响实时性)。我们建议用查表法替代实时计算,比如CRC16校验用预生成的256项表。

6.2 多端口支持:从单通道到N通道

客户常问:“能同时透传4个串口吗?”答案是肯定的,只需微调main.c
- 将serial_fd改为int serial_fds[MAX_PORTS]数组;
-select()nfds参数改为max(serial_fds)+1
- 轮询时用for(i=0; i<MAX_PORTS; i++)遍历所有串口fd;
- 为每个串口分配独立socket连接(或复用同一连接,用首字节标识端口ID)。

我们已在某地铁信号系统中实现8通道透传,静态编译后体积仍<120KB。

6.3 运维集成:让运维人员也能轻松掌控

现场运维最怕黑盒子。我们在main.c中预留了SIGUSR1信号处理器:

void sigusr1_handler(int sig) { log_info("Received SIGUSR1: dumping stats"); log_info("Serial RX: %llu bytes", g_stats.serial_rx_bytes); log_info("Socket TX: %llu bytes", g_stats.socket_tx_bytes); log_info("Reconnect count: %u", g_stats.reconnect_count); } signal(SIGUSR1, sigusr1_handler);

运维只需执行kill -USR1 $(pgrep serial2tcp),日志中就会打印实时统计。配合logrotate,可实现无人值守长期运行。

最后分享一个小技巧:在Makefile中加入VERSION变量,每次make自动写入Git commit hash到二进制中:

VERSION := $(shell git describe --always --dirty 2>/dev/null || echo "unknown") CFLAGS += -DVERSION=\"$(VERSION)\"

这样现场报障时,一句“你们用的是哪个版本?”就能精准定位问题。

这个工具没有炫酷的UI,没有云平台对接,甚至没有一行注释提到“物联网”——但它像一枚精密的齿轮,在无数个无人值守的机柜里,沉默地转动着数据洪流。当你看到产线上PLC的数据实时跳动在云端大屏上,那背后很可能就是这段不到500行的C代码,在某个ARM芯片上,以最朴素的方式,完成了最可靠的使命。


本文还有配套的精品资源,点击获取


简介:一个轻量级Linux串口转TCP透传程序,支持/dev/ttyS0、/dev/ttyUSB0等串口设备与远端TCP服务器(指定IP和端口)之间双向透明转发原始字节流。数据从串口读入后立即发往网络,网络收到的数据也直接写入串口,无协议解析、无缓存延迟、无格式转换。核心功能分两部分:comdev.c封装串口初始化、波特率配置、非阻塞读写及错误恢复逻辑;main.c负责TCP socket连接建立、重连机制、以及单线程内轮询串口与socket的双向转发。整个项目用标准C编写,不依赖libserial、libusb等第三方库,仅需POSIX系统调用,可直接在ARM/x86嵌入式Linux平台交叉编译运行。附带Makefile实现一键编译,.gitignore和完整头文件(comdev.h)便于集成进现有工程。test目录含简易验证脚本,方便快速确认透传通路是否正常。适用于PLC、传感器、工控模块等串口设备接入以太网场景,也适合远程串口调试、AT指令透传、Modbus TCP桥接等低开销需求。


本文还有配套的精品资源,点击获取

http://www.jsqmd.com/news/1111448/

相关文章:

  • 24槽19极外置V型永磁游标电机全套设计资料:含参数化模型、6张结构图与技术说明文档
  • 昇腾NPU部署MindIE推理服务实战与避坑指南
  • 48tools:一站式跨平台媒体内容自动化管理工具
  • 3分钟搞定音乐解密:Unlock Music让你重获音乐自由
  • MATLAB黄金分割法动态演示脚本:实时显示区间缩放、函数值对比与收敛过程
  • 1.2B小模型如何实现高可靠Agent工作流
  • 【计算机Java毕业设计案例】基于 SpringBoot 的中药仓库物资流转管理系统的设计与实现 基于 SpringBoot 的中药材过期预警与库存维护系统(程序+文档+讲解+定制)
  • Windows一键运行的Unity飞机射击游戏成品包(含源资源与可执行文件)
  • Matlab一键识别硬币数量的图形化工具(含示例图片和界面文件)
  • TinyMCE格式刷插件(formatpainter)轻量版,含配置教程与实战调用示例
  • 深入解析Java:HashMap扩容机制全过程深度剖析
  • Three.js IndexedDB使用教程
  • 线粒体氧化应激精准定量 线粒体活性氧(ROS)产生速率检测试剂盒
  • SPA模式全链路利润计算器,输入设计,生产,门店成本,对比传统分销模式收益。
  • AI搜索,找哪些务商好
  • TIA Portal V15可用的西门子PLC随机数生成LGF库(V4.0.2)
  • 变压器铁心叠片逐级张角数值求解工具(C++开源可编译)
  • 科研绘图不用多款软件折腾!paperxie AI 科研绘图一键搞定全学科期刊配图
  • LV3296与STM32G474RE构建高效二维条码扫描系统
  • 拖到就转:Windows下免安装的HEX转BIN小工具,支持中文路径和长文件名
  • MATLAB一键运行的单/双/四孤子动态演化仿真工具包(含图形输出与多作者版本)
  • 中小企业还在用 Excel 管库存?该上进销存系统的 6 个信号
  • 思源宋体TTF:开源中文字体如何彻底改变你的中文排版体验?
  • LV3296与PIC32MX795F512L构建高效条码采集系统
  • Matlab做的语音识别小工具:点一下录音,自动提取MFCC特征,用DTW比对识别孤立词
  • 非洲54国及一级行政区SHP矢量地图数据,WGS84坐标系,开箱即用
  • Tabletop Simulator本地存档+Mod资源一键打包工具(含模型/图片的完整ZIP备份)
  • 别再被参数迷住眼!收藏这份小白指南,轻松看懂AI大模型
  • STM32F103用AT指令通过ESP8266直连OneNET云(TCP透传+自动重连)
  • VC6.0实现的NetBot双端远控工程:含图形客户端、IOCP服务端及FTP/广播/日志等完整模块