资讯详情

QNX串口通信工程实践:termios、select与粘包组帧

📅 2026/11/5 4:33:15 | 华诺云谱 👁 阅读
QNX串口通信工程实践:termios、select与粘包组帧
简介面向 QNX 嵌入式开发初学者的一份串口通信入门示例围绕实时操作系统下通过 /dev/term/* 设备节点操作串口展开帮助读者理解波特率、数据位、停止位、校验位与握手协议等参数配置并建立从打开设备到读写的完整认识。源码围绕串口打开、参数配置、数据收发和关闭等环节组织重点体现 ioctl、read、write 的调用关系以及 struct termios 与 tcsetattr 的设置方式和返回值检查思路对掌握配置流程与错误处理较有帮助。压缩包共 13 个文件约 13KB以 makefile、.mk 构建脚本和 C 源码为主辅以工程配置及编译产物规模精简便于在 QNX 环境中对照阅读与编译。已有 642 人学习可作为嵌入式调试、设备控制等场景的入门参考也能为后续理解中断、流控与多线程同步等问题打下基础。1. 调试串口被占用后业务串口该怎么写手上这块工控板只有一个调试串口业务口全被 RS485 占着偏偏现场要求把一路传感器数据原样转发出去。不少人的第一反应是写个while(read())循环结果在 QNX 上跑起来不是读不到数据就是连续运行十几个小时后写端阻塞卡死。原因在于 QNX 的串口本质上是一个资源管理器/dev/ser1背后是devc-ser*驱动你用的是一套 POSIX 字符设备接口加 termios 的既有规则只是它的消息传递模型、线程调度和readcond这类私有接口把配置不匹配暴露得特别明显。这份 qnx 串口程序资源做的就是把这套东西封成一个能直接编译、能上板、能改协议的最小工程适合刚接手 QNX BSP、需要用串口对接外设的开发者。2. 架构与目录QNX 串口程序的四层结构和文件分工QNX 上的串口程序和 Linux 下的写法七成相似、三成要命。相似的部分是open/read/write/tcsetattr那一套要命的部分集中在线程优先级、超时语义和驱动的启动方式上。所以这个工程的分层不是为了好看而是把会变的部分和几乎不改的部分切开驱动层和接口层是 BSP 和系统给的封装层和组帧层才是你真正要动手的地方。先把层次划清楚后面调参、换协议、加第二路串口才不会牵一发动全身。一个请求从传感器到业务回调走的链路是这样的UART 收到字节触发中断devc-ser8250在中断服务里把数据搬进输入队列然后唤醒正在read()上阻塞的客户端线程客户端线程被唤醒后从内核态拷出数据交给组帧状态机状态机攒够一整帧、校验通过之后回调业务函数。这条链路上任何一环的优先级低于同机上的计算线程在 QNX 的抢占式调度下都会表现为偶发丢字节而不是报错。这是动手前必须建立的第一个认知。2.1 四层结构从内核资源管理器到业务回调驱动层由 BSP 提供x86 平台一般是devc-ser8250ARM SoC 平台常见devc-serminiuart、devc-serpl011这类。它负责 UART 寄存器读写、FIFO 管理、中断处理、波特率分频向上注册/dev/ser1、/dev/ser2这样的设备节点。这一层几乎不需要改但必须会启动它、会看它的启动参数、会在节点没出现时判断是驱动没跑还是硬件没上电。接口封装层对应工程里的serial_port.c它把 fd、termios 配置、超时等待、全量读写包成几个函数。之所以单独抽一层是因为 termios 的字段又多又互相关联散在业务代码里改一次错一次。集中到一处之后换波特率、开关流控、改校验位都只动一个地方。组帧层对应proto_frame.c处理帧头识别、长度域解析、CRC 校验、半包缓存和粘包拆分。串口是字节流一次read()拿到 1 个字节还是 200 个字节完全看时序如果你在业务代码里假设一次读就是一帧那上线之后一定会遇到半包。把这层独立出来业务侧就只面对完整帧回调。业务层就是main.c解析命令行、创建读线程和写线程、注册帧回调、打印日志。这一层尽量薄薄到换一个传感器协议只需要改回调函数体。2.2 目录结构与各文件职责工程目录我一般这么组织文件不多但每个都有明确边界文件职责改动频率src/main.c参数解析、线程创建、回调注册、退出清理中src/serial_port.c/.hopen、termios 配置、select超时、全量读写低src/proto_frame.c/.h帧头、长度域、CRC16、半包状态机高src/ring_buffer.c/.h读写线程之间的缓冲带互斥保护低include/*.h对外接口和帧结构体定义中build/Makefileqcc交叉编译规则低scripts/run_on_target.sh上板之后的启动与日志重定向中改动频率那一列很能说明问题真正需要反复迭代的是proto_frame.c因为每家传感器的帧格式都不一样serial_port.c写对一次之后基本不动。如果你的代码里serial_port.c天天在改多半是把协议逻辑漏进去了。2.3 编译与环境qcc、QNX_TARGET 和上板部署QNX 的交叉编译器是qcc本质是 gcc 的包装靠-V参数选择目标变体。SDP 装好之后先 source 环境脚本把QNX_HOST、QNX_TARGET指到位qcc才能找到头文件和库# build/Makefile —— 先 source qnxsdp-env.sh再执行 make CC qcc # -V 后面跟目标变体aarch64 用 gcc_ntoaarch64learmv7 用 gcc_ntoarmv7lex86_64 用 gcc_ntox86_64 CFLAGS -Vgcc_ntoaarch64le -Wall -O2 -I../include LDFLAGS -Vgcc_ntoaarch64le TARGET qnx_serial_demo OBJS main.o serial_port.o proto_frame.o ring_buffer.o $(TARGET): $(OBJS) $(CC) $(LDFLAGS) -o $ $(OBJS) %.o: ../src/%.c $(CC) $(CFLAGS) -c $ -o $ clean: rm -f $(OBJS) $(TARGET)-V这一项漏了的话qcc会用宿主机默认变体编译出来的二进制拷到板子上会直接报无法执行或者链接阶段找不到libc.so。判断方法很简单file qnx_serial_demo看一眼架构和板子的uname -m对得上再拷。部署环节有个血泪经验如果板子上的系统镜像裁剪过缺运行时库最省事的办法是加-static静态链接二进制会大几 MB但省掉一堆ldd排查。动态链接的话把libc.so、libgcc_s.so一起拷到目标机的/lib或者用LD_LIBRARY_PATH指过去。# 目标机上确认架构、赋执行权限、前台跑一次看日志 uname -m chmod x /tmp/qnx_serial_demo /tmp/qnx_serial_demo -d /dev/ser1 -b 1152003. 初始化链路termios 原始模式与 devc-ser* 驱动的对接细节串口程序的第一次失败九成发生在初始化阶段而且症状都是open 成功但读不到数据。这是因为open()成功只说明设备节点存在、权限够不代表驱动已经正确绑定了硬件、更不代表波特率和对面一致。把初始化拆成驱动侧和应用侧两段看问题就很好定位驱动侧决定/dev/ser1这个节点能不能用、默认波特率是多少应用侧决定这次通信实际用什么参数。3.1 驱动先起来/dev/ser1 是怎么出现的串口驱动不是内核自动加载的得在启动脚本里显式拉起来。QNX 的启动脚本一般写在 build 文件里是个[script]段落。驱动跑起来之后设备节点才存在所以后面要跟一个waitfor否则业务进程抢在前面启动open()直接 ENOENT# build 文件里的启动脚本片段先拉驱动再等节点最后起业务进程 devc-ser8250 -e -b115200 -u1 0x3f8,4 waitfor /dev/ser1 /tmp/qnx_serial_demo -d /dev/ser1 -b 115200 -u1 0x3f8,4表示第 1 个单元用 I/O 端口0x3f8、中断号 4对应的就是/dev/ser1-b115200是驱动侧的默认波特率-e打开硬件流控 RTS/CTS。这里有个坑要说清楚驱动侧的-b和你程序里cfsetospeed()设的波特率是两回事后者会覆盖前者但两者不一致时中间会有一小段以错误波特率通信的窗口对端如果已经在发数据那几帧就是乱码。不同 BSP 的devc-ser*参数名不完全统一devc-serminiuart和devc-ser8250的选项就不完全一样。最靠得住的办法是上板子敲use devc-ser8250看当前镜像里这个驱动的真实用法不要照抄网上的参数表。3.2 应用侧open 加 termios 把串口切成原始模式应用侧第一件事是打开设备并切到原始模式。原始模式的意思是关掉行编辑、回显、信号处理这些终端特性让内核把收到的字节原样交给你。不切的话0x03会被解释成中断信号0x0D会被转成0x0A二进制协议直接崩掉/* serial_port.c —— 打开串口并切成原始模式 */ #include stdio.h #include fcntl.h #include unistd.h #include termios.h #include string.h #include errno.h int serial_open(const char *dev, speed_t baud) { /* O_NOCTTY不要把自己变成控制终端O_NONBLOCKopen 本身不阻塞 */ int fd open(dev, O_RDWR | O_NOCTTY | O_NONBLOCK); if (fd 0) { fprintf(stderr, open %s failed: %s\n, dev, strerror(errno)); return -1; } struct termios tio; /* 必须先用 tcgetattr 取当前值直接 memset 会把 BSP 预置的 c_cflag 一起清掉 */ if (tcgetattr(fd, tio) ! 0) { close(fd); return -1; } cfmakeraw(tio); /* 清 ICANON/ECHO/ISIG/IXON 等等价于手工清位 */ cfsetispeed(tio, baud); cfsetospeed(tio, baud); tio.c_cflag | (CS8 | CLOCAL | CREAD); /* 8 数据位、忽略调制解调器控制线、允许接收 */ tio.c_cflag ~(PARENB | CSTOPB); /* 无校验、1 位停止位 */ tio.c_cflag ~CRTSCTS; /* 硬件流控默认关需要时再单独打开 */ tio.c_iflag ~(IXON | IXOFF | IXANY); /* 软件流控关掉否则收到 0x11/0x13 会被吞 */ tio.c_cc[VMIN] 0; /* 配合 select/O_NONBLOCK 用读不到就返回 */ tio.c_cc[VTIME] 0; /* 不在驱动层做超时超时交给 select 控制 */ if (tcsetattr(fd, TCSANOW, tio) ! 0) { close(fd); return -1; } tcflush(fd, TCIOFLUSH); /* 清掉打开前残留在收发 FIFO 里的脏数据 */ return fd; }几个参数值得单独说。O_NOCTTY不加的话如果当前进程没有控制终端这个串口会变成它的控制终端之后收到某些字符会往进程组发信号很难查。CLOCAL表示忽略 DCD/DSR 这些调制解调器状态线做设备对接时基本都要开不开的话某些板子上read()会一直返回 0。CREAD是允许接收某些老代码忘了置这一位现象就是只能发不能收。VMIN0、VTIME0是一种常见组合含义是有多少读多少没有就立刻返回。这时候read()会返回 0 或者 -1/EAGAIN所以超时控制必须由select()或者readcond()来做不能指望read()自己等。顺带提一句如果目标平台的 BSP 头文件里没有cfmakeraw()不常见但存在手工把c_lflag里的ICANON、ECHO、ECHOE、ISIG四位清掉效果一样。3.3 参数怎么改波特率、校验、流控的对应关系改参数的时候别一个一个试按这张表对号入座改完拿示波器或者另一台机器回环验证一次比反复烧板子快得多需求要改的字段常见错误改波特率cfsetispeed/cfsetospeed或c_cflag的CBAUD位只改了驱动-b没改 termios7 位数据 偶校验c_cflag ~CSIZE; c_cflag | CS7 | PARENB;忘了先清CSIZE8 位和 7 位叠加成未定义组合2 位停止位c_cflag | CSTOPB;对端是 1 位停止位表现为固定位置乱码硬件流控c_cflag | CRTSCTS;且驱动加-e只改了一边发送 FIFO 满后write永久阻塞二进制透传清IXON/IXOFF清ICANON/ISIG数据里的 0x11/0x13 被当成流控字符吞掉速度常量用B115200这种宏别直接写数字。非标准波特率比如 250000需要用c_cflag里的CBAUDEXT加自定义分频这时候必须查驱动支持的最大时钟频率devc-ser8250是通过-c指定输入时钟的时钟给错波特率会整体偏移现象是能通信但误码率很高。4. 收发核心select 超时、readcond 定长读取与粘包状态机初始化对了之后真正决定程序稳不稳的是收发那几百行。串口的问题有很强的欺骗性短时间跑没事连续跑三天开始偶发丢包单步调试没事全速跑就粘包。这些都不是玄学是读超时策略和缓冲策略没设计好。这一章把读线程、判包、写线程三段拆开讲每段都给能直接抄的代码。4.1 读线程用 select 做超时别在 read 上死等最原始的写法是阻塞read()等一个字节。这种写法在单串口小工具里能用一旦要做多路串口或者需要定时上报心跳就会互相拖死。正确做法是把 fd 设成非阻塞用select()统一等超时时间自己定/* serial_port.c —— 等串口可读单位毫秒0 有数据0 超时-1 出错 */ #include sys/select.h int serial_wait_readable(int fd, int timeout_ms) { fd_set rfds; struct timeval tv; FD_ZERO(rfds); FD_SET(fd, rfds); tv.tv_sec timeout_ms / 1000; tv.tv_usec (timeout_ms % 1000) * 1000; /* EINTR 要单独重试QNX 上定时器信号打断 select 是常态 */ int rc; do { rc select(fd 1, rfds, NULL, NULL, tv); } while (rc 0 errno EINTR); return rc; }select()在 QNX 上对字符设备是支持的底层走的是资源管理器的select回调驱动在中断里唤醒等待队列不会忙等。超时值不要设成 0那就是纯轮询白烧 CPU也不要设太大读线程的超时周期通常就是你的心跳周期50 到 200 毫秒比较合适。返回 -1 且errno EINTR时必须重试否则定时器信号一来这一轮数据就漏了。4.2 QNX 特有的 readcond一次读够 N 个字节select()加read()的组合有个小缺陷select告诉你有数据但read可能只拿到 1 个字节剩下的还在路上。对定长协议来说这意味着一堆状态机判断。QNX 提供了一个别的系统上没有的接口readcond()可以指定最少读够多少字节和两段超时一次调用解决/* 至少读够 min 字节或等够 timeout 个十分之一秒才返回 */ ssize_t n readcond(fd, buf, sizeof(buf), 16, /* min攒够 16 字节就返回 */ 1, /* time字节间隔超时单位 0.1 秒 */ 10); /* timeout总超时单位 0.1 秒 */三个参数的含义min是最少字节数time是字节间超时收到第一个字节后隔多久没新字节就算这一段结束timeout是总超时。返回值可能小于min表示超时了但确实收到了一些。这个接口特别适合帧头 定长载荷的协议直接读够一帧长度比状态机拆包省事。用之前建议在目标机上use readcond确认当前版本的确切语义不同版本在边界条件上略有差别。4.3 组帧状态机粘包和半包都在这里消化不管用不用readcond最终都要有一层状态机兜底因为串口只保证字节顺序不保证边界。下面这段是按帧头 0xAA55 小端长度域 CRC16设计的换成你自己的协议只需要改这三个常量/* proto_frame.c —— 半包缓存 粘包拆分上层只拿完整帧 */ #include stdint.h #define FRAME_HEAD0 0xAA #define FRAME_HEAD1 0x55 #define FRAME_MAX 256 static uint8_t s_buf[FRAME_MAX]; static uint16_t s_len; /* 已收到的字节数 */ static uint16_t s_need; /* 本帧声明的总长度0 表示还没解析出来 */ /* 喂一个字节返回 1 表示攒够一整帧*out/*out_len 指向 s_buf 里的数据 */ int frame_feed(uint8_t byte, uint8_t **out, uint16_t *out_len) { /* 找帧头帧头之前的垃圾数据直接丢避免错位后一直对不齐 */ if (s_len 0 byte ! FRAME_HEAD0) return 0; if (s_len 1 byte ! FRAME_HEAD1) { s_len (byte FRAME_HEAD0) ? 1 : 0; /* 0xAA 0xAA 0x55 这种要能重新对齐 */ return 0; } if (s_len FRAME_MAX) { s_buf[s_len] byte; } else { s_len 0; s_need 0; /* 超长帧丢弃防止缓冲区溢出 */ return 0; } /* 第 3、4 字节是长度域小端到齐就能算整帧长度 */ if (s_len 4 s_need 0) { s_need (uint16_t)(s_buf[2] | (s_buf[3] 8)); if (s_need 6 || s_need FRAME_MAX) { /* 长度荒谬整帧丢掉重新找头 */ s_len 0; s_need 0; return 0; } } if (s_need s_len s_need) { uint16_t crc (uint16_t)(s_buf[s_need - 2] | (s_buf[s_need - 1] 8)); if (crc16_modbus(s_buf, s_need - 2) ! crc) { s_len 0; s_need 0; /* 校验错丢弃整帧等下一个帧头 */ return 0; } *out s_buf; *out_len s_need; s_len 0; s_need 0; /* 复位接着处理粘在后面的下一帧 */ return 1; } return 0; }有个细节必须写进注释里返回 1 的时候*out指向静态缓冲区调用方必须在下一次frame_feed之前把数据拷走或者处理完否则会被后续字节覆盖。这是静态缓冲区方案最大的隐患也是我见过最多的一类数据莫名其妙变了的问题。稳妥一点的做法是上层回调里立刻做一次memcpy或者干脆改成传入外部缓冲区。配套的读线程骨架是这样每次read到多少喂多少粘包由状态机自己迭代/* main.c —— 读线程一次读到 N 字节就全部喂给状态机靠循环消化粘包 */ uint8_t buf[128]; for (;;) { if (serial_wait_readable(fd, 100) 0) continue; /* 超时就回去做心跳 */ ssize_t n read(fd, buf, sizeof(buf)); if (n 0) continue; for (ssize_t i 0; i n; i) { uint8_t *frame; uint16_t flen; if (frame_feed(buf[i], frame, flen)) { handle_frame(frame, flen); /* 业务回调内部立刻拷走数据 */ } } }4.4 写侧全量写循环与 tcdrain写侧最容易翻车的地方是把write()的返回值当成写完了。非阻塞模式下write可能只写一部分硬件流控打开且对端不读时甚至一直返回 EAGAIN。必须循环到全部写完并且给一个总超时别让线程永远卡在那儿/* serial_port.c —— 循环写直到写完超时返回 -1 */ int serial_write_all(int fd, const uint8_t *buf, size_t len, int timeout_ms) { size_t off 0; while (off len) { ssize_t n write(fd, buf off, len - off); if (n 0) { off (size_t)n; continue; } if (n 0 (errno EAGAIN || errno EINTR)) { /* 发送队列满或者被信号打断等可写再试等不到就放弃这一帧 */ if (serial_wait_writable(fd, timeout_ms) 0) return -1; continue; } return -1; } return 0; }write()返回成功只代表数据进了驱动的发送队列不代表已经上了线。如果后面的逻辑是发完就关串口或者发完就重启设备中间必须加tcdrain(fd)它会阻塞到发送队列真正排空。少了这一步最后几个字节经常会丢而且丢得毫无规律。多线程同时写同一个串口的话加一把互斥锁并且把锁的属性设成优先级继承pthread_mutexattr_t attr; pthread_mutexattr_init(attr); pthread_mutexattr_setprotocol(attr, PTHREAD_PRIO_INHERIT); /* 防优先级反转 */ pthread_mutex_init(s_tx_lock, attr);QNX 是抢占式实时调度低优先级的写线程持有锁的时候被高优先级线程抢占不开优先级继承就会出现高优先级线程等一把低优先级线程的锁等了很久的经典反转现象是偶发性的写延迟尖峰。5. 避坑与排查收不到、乱码、写卡死的五类现场串口问题的排查顺序比技巧重要。上来就改代码是大忌先确认物理层和设备节点再确认驱动参数最后才怀疑应用逻辑。下面这个顺序是我踩了足够多次之后固定下来的基本能在十分钟内把问题圈定到某一层。5.1 上板之后的排查顺序第一步看设备节点ls -l /dev/ser*。节点不存在说明驱动没起来或者硬件没被识别去slog2info里搜驱动名看有没有资源冲突或者中断申请失败的记录。第二步看进程pidin ar | grep devc-ser确认驱动进程活着顺便看它的启动参数是不是你以为的那组。第三步做物理回环把 TX 和 RX 短接然后cat /dev/ser1同时在另一个终端往里写数据能收到就说明驱动和硬件都通问题一定在应用层。第四步才是跑业务程序同时开着slog2info -w看有没有异常日志。# 目标机上的四步确认顺序不要跳 ls -l /dev/ser* pidin ar | grep devc-ser cat /dev/ser1 # 回环测试TX/RX 短接后往串口写什么就该看到什么 slog2info -w | grep -i ser第三步的cat特别有用它绕过了你所有的应用代码是判断到底是驱动问题还是我的问题的分水岭。我见过好几次是应用层把 termios 配错了cat一切正常。5.2 五类高频现场现象、原因、解决第一类open 成功read 一直返回 0。现象是程序不报错但就是没数据日志里连一行超时都没有。原因通常是c_cflag里没置CREAD或者CLOCAL没开导致驱动在等 DCD 信号另一个常见原因是驱动侧波特率和 termios 设置的波特率不一致对端发过来的全被当成噪声丢掉了。解决方式是先用cat /dev/ser1验证链路再打印tcgetattr的结果和驱动参数逐项对比。别信我记得配了打印出来看。第二类数据里固定少几个字节且少的总是 0x11 或 0x13。这是软件流控没关的典型特征XON0x11和XOFF0x13被驱动当成流控字符消费掉了不会交给应用。二进制协议里这两个字节出现频率不低所以症状是大部分帧正常偶尔校验错。解决办法就是c_iflag ~(IXON | IXOFF | IXANY)同时确认cfmakeraw确实被调用了。第三类高位数据错乱或者中文变乱码。原因多半是数据位没设对。有的人图省事直接写tio.c_cflag CS8 | CREAD | CLOCAL;一行覆盖看着是 8 位但把驱动预置的时钟选择位、CBAUD位一起清了波特率就变成了默认值。正确姿势永远是先tcgetattr、再改位、再tcsetattr。另外 7 位数据加校验的组合如果忘了先清CSIZE会拼出一个未定义的组合行为完全看驱动实现。第四类连续跑十几个小时后 write 卡死。现象是进程还在pidin能看到但业务停了用调试器 attach 上去发现线程停在write里。原因是硬件流控开着对端有一段时间不读发送 FIFO 满了之后write就阻塞如果代码里没设超时那就是永久阻塞。解决办法是open用O_NONBLOCK写之前select等可写并给超时超时就丢帧并记日志别让一个不听话的对端拖死整个进程。第五类多线程收发时数据错乱或者延迟尖峰。现象是偶发性的帧内容对不上或者某次写操作延迟从 1 毫秒跳到几十毫秒。原因有两个一是收发共用了一个缓冲区却没加锁二是互斥量没开优先级继承导致反转。解决办法是固定单读线程 单写线程的模型中间用环形缓冲解耦锁统一用PTHREAD_PRIO_INHERIT。这个模型还有个好处读线程可以设成较高优先级保证不丢字节写线程优先级低一点业务计算再低一点整条链路的时延就可控了。提示把上面五类的判断依据做成一张检查表贴在工位上比每次重新推理快得多。尤其是第二类和第三类症状相似但改的地方完全不同。6. 二次开发把示例协议层换成你自己的帧格式拿到这份 qnx 串口程序之后真正要动的只有proto_frame.c和main.c里的回调其余部分基本可以原样保留。改协议的时候按这个顺序来能少走弯路先把帧结构体定义清楚帧头几个字节、长度域是大端还是小端、有没有序列号、CRC 是 Modbus 还是 CCITT然后只改frame_feed里的三个常量和一个校验函数最后在handle_frame里写业务。长度域大小端搞反是最常见的错误现象是帧头对得上但长度算出来几百上千直接被FRAME_MAX挡掉所以调试期把s_need算出来的值打出来看一眼比什么都快。如果要接第二路串口不要复制一份serial_port.c。把 fd 和配置收进一个结构体serial_open返回这个结构体指针读线程按fd区分组帧状态机也改成每个实例一份把s_buf/s_len/s_need搬进结构体。这样加第三路、第四路都是改配置的事。同理handle_frame建议做成函数指针注册/* 按设备号注册不同的处理函数新增一路串口只加一行注册 */ typedef void (*frame_cb_t)(const uint8_t *frame, uint16_t len); int serial_register_cb(int port_idx, frame_cb_t cb);验证方法上我习惯先在 PC 侧用脚本模拟对端把各种边界情况打一遍半包一次只发 3 个字节、粘包两帧拼在一个write里、错帧故意改 CRC、超长帧长度域写 0xFFFF。这四种都过了再上板子联调实物。板子上验证时优先看两个指标连续跑 24 小时的 CRC 错误计数以及write超时的次数。这两个数都是 0才算真的稳。还有个容易忽略的点QNX 上可以用devctl()拿驱动的线路状态判断发送队列是不是排空了比单纯依赖tcdrain更灵活具体命令和返回结构以目标机上use devctl和对应驱动源码为准别照抄。从那以后我每次动串口代码上板第一件事都是先cat /dev/ser1做一遍回环确认物理链路没问题再谈应用逻辑。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑