ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

QNX串口程序实战:termios原始模式、环形缓冲与帧状态机

QNX串口程序实战:termios原始模式、环形缓冲与帧状态机

简介:这是一份面向QNX嵌入式开发初学者与串口通信学习者的源码实例包,围绕实时操作系统下的串口编程展开,可用于理解设备节点访问、参数配置与数据收发流程,适合需要快速搭建串口实验并对照源码排查配置问题的自学者。压缩包为rar格式,共13个文件、约13KB,包含C语言源码、makefile与mk构建脚本、工程配置文件、目标文件及可执行文件,便于在QNX工程环境中查阅和编译实践。源码覆盖打开串口、通过termios与ioctl设置波特率、数据位、停止位和校验位、read/write收发以及关闭设备等关键环节,并涉及返回值检查与错误处理思路。已有642人学习,可作为嵌入式调试、设备控制及底层通信机制的入门参考,也能帮助读者理解QNX串口程序的组织方式、工程结构与底层I/O机制。

1. 一份能上板就跑的 QNX 串口程序,省掉的到底是哪段功夫

不少人在 QNX 上第一次写串口,卡住的地方和算法无关,而是open("/dev/ser1")之后那几百毫秒:数据要么读不到,要么读出来是乱的,stty敲了一遍又一遍还是老样子。这份 QNX 串口程序源码包针对的就是这一段——把驱动启动参数、termios 原始模式、环形缓冲、帧状态机、多线程收发这几层按顺序摆好,提供一份能编译、能上板、能对着排查的骨架。它适合已经拿到 BSP、手里有目标板、要在上面接传感器或上位机私有协议的嵌入式开发者。如果你只想弄明白串口在 QNX 里是怎么挂到文件系统上的,看到第二章也就够了;想直接拿去改协议、调实时性的,后面几章的参数和坑都能直接用。

2. 从 /dev/ser1 到 termios:驱动、终端、应用三层参数怎么摆对

QNX 上串口不通,九成不是应用代码写错,而是这三层里有一层没对上。驱动层负责把硬件寄存器变成设备节点,终端层负责把字节流解释成「行」还是「原始字节」,应用层负责搬数据。这三层任意一层用了默认值,后面怎么调都是白费。

2.1 驱动先起来:devc-ser* 的启动参数与设备节点

在 QNX Neutrino 里,串口不是一个静态的/dev节点,而是由devc-ser*这一族资源管理器进程注册出来的路径。进程没起,/dev/ser1就不存在;进程起了但参数不对,节点在,波特率却是错的。所以排查永远从「驱动进程」开始,而不是从应用开始。

典型的启动命令长这样,放在启动脚本的[session]段或者 build 文件里:

# -b 指定驱动默认波特率,-u 后面跟 单元号,端口基址,中断号(x86 ISA 风格) # SoC 平台一般不用手工给端口,驱动从 hwinfo 里拿,具体参数用 -h 查 devc-ser8250 -b115200 -u1 3f8,4 & # 起来之后先确认三件事 pidin | grep -i ser # 1) 驱动进程在不在、启动参数对不对 ls -l /dev/ser* # 2) 节点注册出来没有、权限够不够 stty < /dev/ser1 # 3) 终端当前实际的波特率/数据位/流控

-b只是驱动的默认值,应用打开设备后一旦调tcsetattr,以应用设的为准——这一点经常被误解成「驱动设了 115200 应用就不用管了」。-u的格式在不同驱动上不一样,devc-ser8250是「单元号,端口,中断」,SoC 上的devc-seromap之类往往只需要单元号或者干脆自动枚举,拿不准就直接devc-serxxx -h看它自己的用法,别照着别的平台抄。

编译侧同样要对齐目标架构,主机上编译出来跑不动是最常见的低级翻车:

# -V 选目标变体,必须和板子上跑的系统一致,aarch64le 与 armle 不能混 qcc -Vgcc_ntoaarch64le -O2 -Wall -Wextra -o ser_demo ser_open.c ser_io.c parser.c main.c

提示:/dev/ser1的权限默认常常只给 root,应用以普通用户跑就会open失败返回EACCES。要么在启动脚本里chmod 666 /dev/ser1,要么把应用放进有相应权限的账户下跑,别在代码里加setuid那种歪招。

2.2 termios:把终端从「编辑模式」掰成原始模式

这是 QNX 串口程序最容易翻车的一层。QNX 的终端默认是规范(canonical,文档里也叫 edited)模式:输入按行缓存、回显、0x03会被解释成中断字符、0x11/0x13会被当成软件流控吃掉。你的协议里只要出现这些字节,数据就在驱动层被「加工」没了,应用侧再怎么写都收不回来。

要传二进制帧,必须逐位清干净。下表是这份源码包里ser_open_raw()实际做的事:

termios 字段要清掉的位不清会怎样
c_iflagIXON IXOFF IXANY ICRNL INLCR IGNCR ISTRIP INPCK BRKINT PARMRK负载里的0x11/0x13/0x0D被当控制字符吞掉或改写
c_oflagOPOST发出去的0x0A被扩成0x0D 0x0A,帧长直接错
c_lflagICANON ECHO ECHOE ECHOK ISIG IEXTEN行缓冲导致read一直等换行符,短帧永远读不出来
c_cflagCSIZE PARENB CSTOPB数据位/校验位/停止位不是 8N1,对端全乱码
c_cflag置位CS8 CREAD CLOCAL不使能接收、被 modem 控制线挂住
c_ccVMIN=0、VTIME=0配合 select 使用时行为不符合预期
#include <termios.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <stdio.h> /* 以原始模式打开串口;baud 用 B115200 这类常量,不要传裸数字 */ int ser_open_raw(const char *dev, speed_t baud) { /* O_NOCTTY:别让这个口变成控制终端,否则收到控制字符会影响本进程 */ int fd = open(dev, O_RDWR | O_NOCTTY); if (fd < 0) { perror("open"); return -1; } struct termios t; if (tcgetattr(fd, &t) != 0) { perror("tcgetattr"); close(fd); return -1; } /* 输入:关软件流控、关回车换行互转、关奇偶校验与 break 处理 */ t.c_iflag &= ~(IXON | IXOFF | IXANY | ICRNL | INLCR | IGNCR | ISTRIP | INPCK | BRKINT | PARMRK); /* 输出:关掉 \n -> \r\n 这类加工,帧是二进制,一个字节都不能动 */ t.c_oflag &= ~OPOST; /* 本地:非规范模式,不回显,不产生 SIGINT/SIGQUIT */ t.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHOK | ISIG | IEXTEN); /* 控制:8N1,使能接收,忽略 modem 控制线 */ t.c_cflag &= ~(CSIZE | PARENB | CSTOPB); t.c_cflag |= (CS8 | CREAD | CLOCAL); /* 需要 RTS/CTS 硬件流控时打开这两位;QNX 用的是 IHFLOW/OHFLOW, 不是别的系统里常见的 CRTSCTS,抄错就编译不过 */ /* t.c_cflag |= (IHFLOW | OHFLOW); */ cfsetispeed(&t, baud); cfsetospeed(&t, baud); /* VMIN=0/VTIME=0:read 立即返回当前可用字节,可能为 0,节奏交给 select */ t.c_cc[VMIN] = 0; t.c_cc[VTIME] = 0; /* TCSADRAIN 等输出队列排空再改,避免把正在发的半帧截断; 刚打开、还没发过数据时用 TCSANOW 也可以 */ if (tcsetattr(fd, TCSADRAIN, &t) != 0) { perror("tcsetattr"); close(fd); return -1; } return fd; }

几个参数值得单独说清楚。VMIN/VTIME只在非规范模式下有意义,四种组合里只有0/0是「有就读、没有立刻返回」,这正是配合select想要的语义;1/0是「至少等到 1 个字节」,会让你的读线程在空闲时死等,超时逻辑全部失效。TCSANOW、TCSADRAIN、TCSAFLUSH的区别也别忽略:TCSAFLUSH会把已收到但还没读的数据丢掉,如果应用是「先跑一会儿再切换参数」,用它会莫名其妙丢帧。

最后提醒一句,HUPCL不要随手加。它的语义是关闭设备时拉低 DTR,某些对接的单片机把 DTR 接到复位脚上,你程序一退出,对端就重启了,现象是「每次重连设备都要初始化半天」。

2.3 读写循环:select 超时 + 短读短写处理

原始模式配好只是让字节「原样进出」,什么时候读、读多少、写不完整怎么办,得靠读写循环兜住。这份程序里读侧用select等可读,写侧用循环续写。

#include <sys/select.h> /* 等可读,最多等 timeout_ms;返回 >0 读到的字节数,0 超时,-1 出错 */ static int ser_read_wait(int fd, unsigned char *buf, size_t cap, int timeout_ms) { fd_set rfds; FD_ZERO(&rfds); FD_SET(fd, &rfds); struct timeval tv; tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000; int n = select(fd + 1, &rfds, NULL, NULL, &tv); if (n < 0) return (errno == EINTR) ? 0 : -1; /* 被信号打断按超时处理 */ if (n == 0) return 0; /* 超时,交给上层做帧超时判断 */ ssize_t r = read(fd, buf, cap); if (r < 0) return (errno == EINTR) ? 0 : -1; return (int)r; /* 关键:r 常常远小于 cap,也可能为 0 */ } /* 写满 len 个字节,处理短写与输出缓冲满 */ static int ser_write_all(int fd, const unsigned char *buf, size_t len) { size_t off = 0; while (off < len) { ssize_t w = write(fd, buf + off, len - off); if (w < 0) { if (errno == EINTR) continue; /* 重试即可 */ if (errno == EAGAIN) { delay(1); continue; } /* 输出队列满,让出 1ms */ return -1; } off += (size_t)w; /* 短写:只进去一部分,必须续写 */ } return 0; }

select的返回值有三种,代码里必须分开处理:>0表示至少一个字节可读,0是超时(这里正是做「帧内超时重同步」的时机),-1且errno == EINTR说明被信号打断,不能当错误直接退出,否则程序跑着跑着就被一个无关信号干掉。

read返回 0 也不要慌,在VMIN=0下它只表示「这次没拿到数据」,不是对端关闭。真正需要担心的是短写:write返回小于len是正常现象,驱动输出队列满了就会这样,不续写就会发出半帧,对端校验全错,现象看起来像「偶发丢包」,实际是自己把帧截断了。

3. 帧状态机与环形缓冲:把「半截包」变成可校验的完整报文

驱动 FIFO 的深度通常只有十几到几十字节,select唤醒到read真正执行之间还有调度延迟。结果是:一次read拿到 0.5 个帧、1.5 个帧、或者一个完整帧外加下一帧的帧头,全都正常。指望「一次 read 一帧」的写法,在实验室能跑,上现场必翻车。

3.1 为什么要在应用层再做一层缓冲

因为读侧不能被解析拖慢。解析里可能有 CRC 计算、回调、日志,耗时不确定;一旦读线程在里面多待几毫秒,驱动 FIFO 就溢出了,而溢出丢的字节是找不回来的。常见做法是读线程只做「read + 塞进环形缓冲」,解析放到另一个执行体里,两边用单生产者单消费者模型,连锁都不用加。

#define RB_SIZE 4096 /* 必须是 2 的幂,取模用 & 而不是 % */ #define RB_MASK (RB_SIZE - 1) typedef struct { unsigned char buf[RB_SIZE]; volatile unsigned int head; /* 只由写者(读线程)修改 */ volatile unsigned int tail; /* 只由读者(解析线程)修改 */ } ring_t; /* 写者侧:返回实际写入字节数,满了就返回写了多少,绝不阻塞 */ static size_t rb_put(ring_t *r, const unsigned char *d, size_t n) { unsigned int h = r->head, t = r->tail; size_t space = RB_SIZE - (size_t)((h - t) & RB_MASK) - 1; /* 留一个空位区分满/空 */ if (n > space) n = space; for (size_t i = 0; i < n; i++) { r->buf[(h + i) & RB_MASK] = d[i]; } r->head = (h + (unsigned int)n) & RB_MASK; /* 最后一次性更新 head,保证可见性顺序 */ return n; } /* 读者侧:有多少取多少 */ static size_t rb_get(ring_t *r, unsigned char *out, size_t n) { unsigned int h = r->head, t = r->tail; size_t avail = (size_t)((h - t) & RB_MASK); if (n > avail) n = avail; for (size_t i = 0; i < n; i++) { out[i] = r->buf[(t + i) & RB_MASK]; } r->tail = (t + (unsigned int)n) & RB_MASK; return n; }

head和tail用无符号整数的自然溢出配合掩码,省掉每次取模;head必须在数据全部写完之后再更新,否则读者可能看到「head 已经前进但数据还没落地」的窗口。缓冲大小 4096 是按「最高波特率下 100ms 的数据量」估的:115200 下 100ms 约 1152 字节,留四倍余量足够扛住一次调度抖动。如果你的波特率上到 921600,这个值要跟着涨,别照抄。

3.2 状态机:帧头、长度、负载、校验、超时重同步

协议是0xAA 0x55 LEN BODY[LEN] SUM这种最常见的小端私有帧。解析用逐字节喂的状态机,好处是断帧、粘帧、噪声插入全都能自愈,不需要缓存整段再做memmem找帧头。

typedef enum { S_H1, S_H2, S_LEN, S_BODY, S_SUM } pstate_t; #define FRAME_MAX 64 #define FRAME_GAP_MS 50 /* 帧内允许的最大字节间隔 */ typedef struct { pstate_t st; unsigned char len, idx, sum; unsigned char body[FRAME_MAX]; uint64_t last_ms; /* 上一个字节的到达时刻,用于超时重同步 */ unsigned int good, bad_csum, bad_len, resync; } parser_t; /* now_ms() 用 CLOCK_MONOTONIC,别用 CLOCK_REALTIME,否则校时会让超时判断抽风 */ static void parser_feed(parser_t *p, const unsigned char *d, size_t n) { for (size_t i = 0; i < n; i++) { unsigned char c = d[i]; uint64_t now = now_ms(); /* 半帧卡住超过 FRAME_GAP_MS 没后续字节,直接丢弃回帧头状态 */ if (p->st != S_H1 && (now - p->last_ms) > FRAME_GAP_MS) { p->st = S_H1; p->resync++; } p->last_ms = now; switch (p->st) { case S_H1: if (c == 0xAA) p->st = S_H2; break; case S_H2: /* 收到 0xAA 0xAA 0x55 时保持在 S_H2,不能直接回 S_H1 */ if (c == 0x55) p->st = S_LEN; else if (c != 0xAA) p->st = S_H1; break; case S_LEN: p->len = c; p->idx = 0; p->sum = 0; if (c == 0 || c > FRAME_MAX) { p->bad_len++; p->st = S_H1; } else p->st = S_BODY; break; case S_BODY: p->body[p->idx++] = c; p->sum = (unsigned char)(p->sum + c); if (p->idx >= p->len) p->st = S_SUM; break; case S_SUM: if (c == p->sum) { p->good++; on_frame(p->body, p->len); } else p->bad_csum++; p->st = S_H1; /* 无论对错都回帧头,绝不停在 SU M 等下一个字节 */ break; } } }

S_H2那个分支是血泪经验:最初的版本写成if (c == 0x55) ... else p->st = S_H1;,结果两帧之间夹一个0xAA噪声时,状态机把噪声当帧头、把真正的帧头0xAA当数据丢掉,后面整段错位,表现为「连续丢好几十帧然后自己恢复」。修法就是上面那句else if (c != 0xAA)。

FRAME_GAP_MS = 50这个值不是拍脑袋:在 115200 下传一个 64 字节的帧约 5.5ms,帧内任何字节间隔超过 10 倍就基本可以判定是断帧。如果你的上位机是「攒够一批再发」的,这个值要按它的最大内部间隔来调,设太小会把正常的长帧切断,设太大则断帧要等很久才自愈。

3.3 校验、长度上限与错误计数

校验用累加和还是 CRC16,取决于对端,不是你能选的;但长度上限必须自己加。LEN字段是一个字节,恶意或者错位的数据能给你一个 255,body[64]直接越界,这种内存踩踏在目标板上表现为「跑几个小时后莫名重启」,查起来极痛苦。

错误计数同理,四个计数器(good、bad_csum、bad_len、resync)看着不起眼,现场排障时全靠它们区分「是对端没发」「是线路干扰」还是「是我自己解析错位」。常见做法是每 10 秒往日志打一行,比值能直接把问题定性:bad_csum高而good也有,多半是线路或波特率偏差;resync高而bad_csum几乎为零,大概率是自己状态机或超时参数的问题。

4. 实时性调优:收、解、发三个执行体的优先级和唤醒方式

串口程序在实验室跑通和在现场跑稳,中间隔的就是这一章。默认优先级下,你的读线程会被系统里任何一个稍忙的进程挤开几十毫秒,而驱动 FIFO 只能缓冲十几字节,挤开一次就丢一次数据。

4.1 收、解、发拆成三个执行体

拆分的依据是「谁的时间约束最硬」。收线程直接对接驱动 FIFO,延迟要求最高;解析线程做协议处理,可以慢一点但不能阻塞收;发送线程负责把上层要发的报文排队发出,顺带解决多线程同时write导致报文交错的问题。

执行体调度策略建议优先级职责不能做的事
收线程SCHED_RR较高(如 30)select+read+rb_put解析、打印日志、申请内存
发送线程SCHED_RR中(如 25)从发送队列取报文,ser_write_all无限等待某个事件
解析线程SCHED_RR略低于收(如 20)rb_get+ 状态机 + 回调在回调里做耗时 IO
主/监控SCHED_OTHER默认统计、日志、命令处理抢占上面三个

优先级的具体数值要按系统里其他关键线程来排,不能孤立地看。判断方法很朴素:现场跑起来之后用pidin观察各线程的 CPU 占用和状态,如果收线程长期处于READY而拿不到 CPU,说明优先级排低了或者有更高优先级的线程在空转。

4.2 优先级与调度策略怎么设

QNX 里新建线程默认继承创建者的调度参数,所以只调pthread_attr_setschedparam是不够的,必须显式关掉继承,否则你设的优先级根本不生效——这个坑几乎每个人都会踩一次。

#include <pthread.h> #include <sched.h> /* prio 在 QNX 里范围是 1..255,普通用户通常只能用到 63,再高需要额外权限 */ static int spawn_rt(pthread_t *tid, void *(*fn)(void *), void *arg, int prio) { pthread_attr_t attr; pthread_attr_init(&attr); /* 关键:不关继承的话,下面设的策略和优先级会被静默忽略 */ pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED); pthread_attr_setschedpolicy(&attr, SCHED_RR); struct sched_param sp; sp.sched_priority = prio; pthread_attr_setschedparam(&attr, &sp); pthread_attr_setstacksize(&attr, 64 * 1024); /* 别用默认 8MB,嵌入式内存紧张 */ int rc = pthread_create(tid, &attr, fn, arg); pthread_attr_destroy(&attr); if (rc != 0) { errno = rc; perror("pthread_create"); } return rc; }

SCHED_RR与SCHED_FIFO的选择:收线程用SCHED_RR,因为它会在select上阻塞,天然让出 CPU,时间片机制只是兜底防止它死循环霸占;如果某个线程里有很长的纯计算段,用SCHED_FIFO更合适,但必须自己保证它有阻塞点,否则同级线程会被饿死。pthread_create返回的是错误码本身,不是-1,所以要errno = rc再perror,直接perror会打出Success,白白浪费一次排查时间。

三个线程共享解析器和发送队列时,互斥锁要带优先级继承:

pthread_mutexattr_t ma; pthread_mutexattr_init(&ma); pthread_mutexattr_setprotocol(&ma, PTHREAD_PRIO_INHERIT); /* 防优先级反转 */ pthread_mutex_init(&g_tx_lock, &ma);

不加PTHREAD_PRIO_INHERIT的后果很典型:低优先级的日志线程拿着锁被中优先级线程抢占,高优先级的收线程在锁上等,等多久完全看运气,表现出来就是「偶发几十毫秒的接收空洞」。

4.3 异步通知:什么时候不用 select

select是主流做法,简单、可控、超时语义明确。但如果你的收线程还要处理别的 fd(比如一个管理通道),或者希望连select那一层唤醒延迟都省掉,可以用异步通知:把 fd 设成O_ASYNC,事件以信号形式投递,用sigwait在专线程里等。

/* 先屏蔽信号,再交给 sigwait 同步等待;顺序反了信号会被默认处理掉 */ sigset_t set; sigemptyset(&set); sigaddset(&set, SIGIO); pthread_sigmask(SIG_BLOCK, &set, NULL); /* 把 fd 的异步事件投给本进程 */ fcntl(fd, F_SETOWN, getpid()); int fl = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, fl | O_ASYNC | O_NONBLOCK); /* 等待线程里 */ for (;;) { int sig; if (sigwait(&set, &sig) != 0) continue; if (sig != SIGIO) continue; /* 信号会合并:来了 10 次可能只收到 1 次,必须循环读到 EAGAIN 为止 */ unsigned char tmp[512]; for (;;) { ssize_t r = read(fd, tmp, sizeof(tmp)); if (r > 0) { rb_put(&g_rb, tmp, (size_t)r); continue; } if (r < 0 && errno == EINTR) continue; break; /* EAGAIN 或者出错,退出内层循环 */ } }

两个必须记住的点:一是信号会合并,SIGIO不能当作「有几个字节」的计数,只能当作「可能还有数据」的提示;二是O_ASYNC必须配O_NONBLOCK,否则内层那个read会在读完最后一字节后永久阻塞,线程再也回不到sigwait。这套写法比select少一层轮询,但调试难度高一截,我一般只在收线程确实被别的 fd 拖累时才切过去。

5. 避坑清单:五个让我在目标板上返工的串口问题

下面五条都是现象、原因、解决三段式的实录,症状描述尽量按你实际会看到的样子写,方便对照。

5.1 open 返回 ENOENT 或 EACCES

现象:程序启动就退出,perror打出No such file or directory或者Permission denied,但/dev下明明能看到节点。

原因:ENOENT基本是驱动进程根本没起来,或者设备节点注册在别的路径下(有些 BSP 会把第一个口映射成/dev/ser1也可能是别的编号,取决于-u的单元号);EACCES是节点权限问题,应用以非 root 身份跑,而节点是crw-rw---- root root。

解决:先pidin | grep -i ser确认驱动进程,再ls -l /dev/ser*看节点和权限。驱动没起就去查启动脚本有没有被注释掉、-u参数和硬件对不对;权限问题在启动脚本里chmod 666 /dev/ser1,别在应用里动setuid的心思。这一步花十分钟,比后面猜半小时值。

5.2 改了波特率还是乱码

现象:代码里明明cfsetospeed(&t, B115200)了,收出来还是花屏或者固定几个错误字节。

原因:三种可能。一是只调了cfsetispeed/cfsetospeed却没调tcsetattr,参数根本没下发到驱动;二是tcsetattr的action用了TCSAFLUSH,把配置过程中的数据丢了导致对端超时重发;三是对端压根没跟着改,或者它用的是自己的晶振分频,实际波特率偏差超过 2%。

解决:改完立刻stty < /dev/ser1看驱动侧的实际值,和代码里的常量对一遍。波特率常量必须用B115200这类宏,传裸数字 115200 进去在部分实现上会被当成无效值静默忽略。偏差问题用示波器或者让对方发0x55连续码,看波形周期算实际速率。

5.3 收到的数据被「吞」掉一部分或几帧粘在一起

现象:抓日志发现本该 8 字节的帧只到了 6 字节,或者一次read返回了三帧拼在一起。

原因:前者九成是终端还处在规范模式,0x0D、0x11、0x13、0x03这些字节被输入处理吃掉了;后者是正常的字节流语义,read从来不保证边界。

解决:把 2.2 那张表里的位逐个清干净,清完用一段包含0x00到0xFF全字节的测试数据回环验证一遍——这是最快的自检方法,全字节过一遍,哪个字节被改写一眼就能看出来。粘包靠 3.2 的状态机解决,不要试图用「按固定长度读」绕过去。

5.4 跑几分钟后开始丢字节,重启就好

现象:刚上电一切正常,跑五到十分钟后开始零星丢帧,resync计数缓慢上涨,重启程序又能好一阵。

原因:读线程被抢占导致驱动 FIFO 溢出。默认调度策略下,只要系统里有编译、日志刷盘、文件系统同步这类后台动作,读线程就可能被挤开几十毫秒,而 FIFO 只能存十几字节。

解决:按 4.2 把收线程提到SCHED_RR并显式关闭调度继承,把解析和日志挪出收线程。改完用一个持续高频发送的对端压测半小时,看resync是否还在涨。如果还涨,就要查是不是中断被别的驱动共享拖慢了,那是 BSP 层面的事。

5.5 程序退出再启动,串口打不开或者对端被复位

现象:kill掉进程后重新运行,open卡住或者返回错误;或者每次退出,对接的单片机就重启一次。

原因:前一个进程的 fd 没关干净,资源管理器那边还挂着;对端复位则是HUPCL在关闭时拉低了 DTR,而对方把 DTR 接到了复位电路。

解决:退出路径上一定有close(fd),并且SIGTERM的处理函数里也要走同一条清理路径,别只写正常返回的分支。HUPCL按 2.2 的建议不要置位。如果确实遇到「上一个进程还占着」,用pidin找残留进程,别靠重启板子解决——重启会掩盖真正的资源泄漏。

6. 回环验证与长稳压测

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

返回列表