☰
VxWorks串口通信实战:从设备模型到超时收发完整示例
2026/10/16 10:48:52 网站建设 项目流程

简介:这份资源面向VxWorks嵌入式开发者与实时操作系统学习者,提供串口通信的完整示例程序,帮助解决串口驱动调用、参数配置与数据收发等实际问题。压缩包共25个文件,约26KB,包含C源码、头文件、编译中间文件与工程配置等类型,其中.c与.h文件承载核心逻辑,.obj、.lst、.map等记录编译链接过程,.cce、.sdb等为工程环境相关文件,便于直接导入或参考。示例围绕TestUart展开,演示了serialOpen、serialSetSpeed、serialSetParity、serialWrite、serialRead、serialClose等标准Serial Driver API的用法,并涉及中断处理、taskSpawn多线程接收与sysLog日志调试等进阶内容。目前已有764人学习下载,适合希望掌握VxWorks串口通信基本流程、理解驱动模型与实时任务协作的读者参考借鉴。

1. 串口通信在 VxWorks 下的真实开发场景

很多做嵌入式实时系统的开发者,第一次在 VxWorks 上写串口通信程序时,都会遇到一个反直觉的情况:明明在 Linux 下几行termios就能搞定的串口收发,换到 VxWorks 上却要重新理解一套完全不同的设备模型。VxWorks 没有open("/dev/ttyS0")这种路径式设备节点,它的串口设备是通过驱动挂载到 I/O 系统里的,名字通常是/tyCo/0、/tyCo/1这类形式。这份 VxWorks 下串口通信示例程序,就是围绕这个设备模型,把串口初始化、参数配置、数据收发、异常处理串成一条完整链路,适合正在做嵌入式底层通信、需要快速跑通串口链路的开发者参考。

它解决的核心问题很具体:在 VxWorks 的实时任务环境里,如何用标准 I/O 接口打开串口、配置波特率和数据位、用read/write完成阻塞或非阻塞收发,以及如何把串口读写封装成可复用的模块。如果你手上有 VxWorks 的交叉编译环境,或者正在 Tornado / Workbench 下调试目标板,这份示例能直接作为起点。

2. 项目结构与技术选型:为什么用标准 I/O 而不是直接操作寄存器

2.1 整体目录与模块划分

这份示例程序的结构不复杂,但每一层都有明确职责。常见的组织方式是这样:

vxworks_serial_demo/ ├── src/ │ ├── serial_init.c // 串口打开与参数配置 │ ├── serial_send.c // 发送封装 │ ├── serial_recv.c // 接收封装(含超时处理) │ └── serial_test.c // 主任务与测试入口 ├── include/ │ └── serial_demo.h // 结构体与函数声明 ├── Makefile // 交叉编译规则 └── README.md

serial_init.c负责把/tyCo/0这类设备名转成文件描述符,并设置波特率、数据位、停止位、校验位。serial_send.c和serial_recv.c分别封装写和读,serial_test.c里创建一个独立任务,循环调用收发接口。头文件里通常定义一个serial_config_t结构体,把波特率、设备名、超时时间这些参数集中管理。

这种划分的好处是:换一块目标板、换一个串口号,只需要改配置结构体,不用动收发逻辑。对于需要同时管理多个串口的项目,可以把serial_config_t做成数组,每个串口一个实例。

2.2 技术选型:标准 I/O 与 ioctl 的配合

VxWorks 下操作串口有两条路:一条是直接操作硬件寄存器,另一条是走 VxWorks 的 I/O 系统,用open、read、write、ioctl这套标准接口。这份示例选的是后者,原因很实际。

直接操作寄存器意味着你要自己处理中断、FIFO、DMA,代码和芯片强绑定,换芯片就得重写。而走 I/O 系统,VxWorks 的串口驱动已经帮你处理了底层细节,你拿到的是一个文件描述符,读写就像操作文件一样。代价是灵活性稍低,但对绝大多数应用场景来说,标准 I/O 足够用,而且代码可移植性更好。

配置串口参数用的是ioctl,命令码通常是FIOBAUDRATE设置波特率,FIOSETOPTIONS设置数据位和校验。这里有个容易翻车的点:VxWorks 的ioctl命令码和 Linux 完全不同,不能照搬 Linux 的TCSETS。示例里会把用到的命令码和参数结构体写清楚。

/* serial_demo.h - 串口配置结构体与函数声明 */ #ifndef SERIAL_DEMO_H #define SERIAL_DEMO_H #include <ioLib.h> #include <tyLib.h> /* 串口配置参数集中管理,换串口只改这里 */ typedef struct { char dev_name[32]; /* 设备名,如 "/tyCo/0" */ int baud_rate; /* 波特率,如 115200 */ int data_bits; /* 数据位,通常 8 */ int stop_bits; /* 停止位,通常 1 */ char parity; /* 校验位:'N' 无,'O' 奇,'E' 偶 */ int timeout_ms; /* 读超时,单位毫秒 */ } serial_config_t; int serial_open(serial_config_t *cfg); int serial_send(int fd, const char *buf, int len); int serial_recv(int fd, char *buf, int len, int timeout_ms); void serial_close(int fd); #endif

这段头文件定义了配置结构体和四个核心接口。dev_name用字符数组而不是指针,是为了避免在嵌入式环境里动态内存分配带来的不确定性。timeout_ms单独拿出来,是因为串口读操作如果没有超时,任务可能永久阻塞,这在实时系统里是致命的。

2.3 编译与运行环境准备

VxWorks 的编译和普通 Linux 程序不一样,它需要交叉编译工具链。常见做法是在 Makefile 里指定CC为对应架构的编译器,比如ccppc、ccarm,然后链接 VxWorks 的目标库。

# Makefile - 交叉编译规则示例 CC = ccppc CFLAGS = -Wall -O2 -I./include -I$(WIND_HOME)/target/h LDFLAGS = -r TARGET = serial_demo.o OBJS = src/serial_init.o src/serial_send.o src/serial_recv.o src/serial_test.o $(TARGET): $(OBJS) $(LD) $(LDFLAGS) -o $@ $(OBJS) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f $(OBJS) $(TARGET)

这里WIND_HOME是 VxWorks 安装目录的环境变量,-r表示生成可重定位目标文件,方便后续和 VxWorks 镜像一起链接。如果你的环境是 Workbench,通常不用手写 Makefile,直接建一个 Downloadable Kernel Module 工程,把源文件加进去就行。编译产物通过目标板调试通道下载运行。

注意:不同 VxWorks 版本的头文件路径和库名可能有差异,6.9 和 7.x 的ioLib.h位置就不完全一样。编译报找不到头文件时,先确认WIND_HOME指向的版本和你的目标板镜像版本一致。

3. 核心模块拆解:串口初始化、收发与超时处理

3.1 串口打开与参数配置

串口初始化的第一步是open,但 VxWorks 的open有个细节:设备名必须和驱动注册的名字完全一致。常见的是/tyCo/0,但有些 BSP 里可能是/tyCo/1甚至自定义名字。打开之后立刻用ioctl设置参数,顺序不能反。

/* serial_init.c - 串口打开与参数配置 */ #include "serial_demo.h" #include <stdio.h> #include <string.h> int serial_open(serial_config_t *cfg) { int fd; int options = OPT_RAW; /* 原始模式,不做字符转换 */ /* 打开串口设备,O_RDWR 表示读写模式 */ fd = open(cfg->dev_name, O_RDWR, 0); if (fd == ERROR) { printf("open %s failed, errno=%d\n", cfg->dev_name, errno); return ERROR; } /* 设置波特率,FIOBAUDRATE 是 VxWorks 专用命令码 */ if (ioctl(fd, FIOBAUDRATE, cfg->baud_rate) == ERROR) { printf("set baudrate %d failed\n", cfg->baud_rate); close(fd); return ERROR; } /* 设置原始模式和校验位,OPT_RAW 关闭行缓冲和回显 */ if (cfg->parity == 'N') { options |= OPT_RAW; } else if (cfg->parity == 'O') { options |= OPT_ODD; } else if (cfg->parity == 'E') { options |= OPT_EVEN; } if (ioctl(fd, FIOSETOPTIONS, options) == ERROR) { printf("set options failed\n"); close(fd); return ERROR; } return fd; }

open的第三个参数在 VxWorks 里通常传 0,因为串口设备不涉及文件权限。FIOBAUDRATE直接传整数波特率,不需要像 Linux 那样用B115200宏。FIOSETOPTIONS设置的是串口的工作模式,OPT_RAW表示原始模式,数据不做任何加工直接透传,这是串口通信最常用的模式。

如果ioctl返回ERROR,一定要close(fd)再返回,否则文件描述符泄漏,多次调用后系统资源耗尽。这个坑在长时间运行的任务里特别隐蔽,表现是运行几小时后串口突然打不开。

3.2 发送封装与阻塞行为

发送相对简单,write把缓冲区数据写到串口。但要注意write的返回值可能小于请求长度,尤其是在缓冲区满的时候。健壮的写法是循环写直到全部发完。

/* serial_send.c - 发送封装,处理部分写 */ #include "serial_demo.h" int serial_send(int fd, const char *buf, int len) { int total = 0; int n; while (total < len) { /* write 返回实际写入字节数,可能小于 len */ n = write(fd, buf + total, len - total); if (n == ERROR) { printf("write failed, errno=%d\n", errno); return ERROR; } total += n; } return total; }

write在 VxWorks 下默认是阻塞的,如果串口发送缓冲区满,任务会挂起等待。这在实时系统里需要评估:如果发送任务优先级很高,阻塞可能导致其他任务得不到调度。常见做法是给发送也加超时,或者用select判断可写再发。示例里保持简单,用循环写保证数据完整。

3.3 接收与超时处理

接收是串口通信里最容易出问题的地方。read默认阻塞,如果对方不发数据,任务就卡死。解决办法是用select加超时,或者用ioctl设置FIONREAD先查缓冲区有多少字节可读。

/* serial_recv.c - 带超时的接收封装 */ #include "serial_demo.h" #include <selectLib.h> int serial_recv(int fd, char *buf, int len, int timeout_ms) { fd_set read_fds; struct timeval tv; int n; FD_ZERO(&read_fds); FD_SET(fd, &read_fds); /* 把毫秒转成 timeval 结构 */ tv.tv_sec = timeout_ms / 1000; tv.tv_usec = (timeout_ms % 1000) * 1000; /* select 返回可读的描述符数量,0 表示超时 */ n = select(fd + 1, &read_fds, NULL, NULL, &tv); if (n == ERROR) { printf("select failed, errno=%d\n", errno); return ERROR; } if (n == 0) { return 0; /* 超时,没有数据 */ } /* 有数据可读,read 不会阻塞 */ n = read(fd, buf, len); if (n == ERROR) { printf("read failed, errno=%d\n", errno); return ERROR; } return n; }

select的第一个参数是最大文件描述符加一,这是 POSIX 标准的要求。timeval结构把毫秒拆成秒和微秒。select返回 0 表示超时,返回大于 0 表示有描述符可读,返回ERROR表示出错。超时后返回 0 而不是ERROR,调用方可以根据返回值区分「没数据」和「出错」。

这个封装的好处是接收任务不会永久阻塞,可以定期检查其他标志位,或者做心跳检测。在实时系统里,任何可能永久阻塞的操作都要有超时兜底,这是血泪经验。

3.4 主任务与测试入口

最后把所有模块串起来,创建一个独立任务循环收发。

/* serial_test.c - 主任务与测试入口 */ #include "serial_demo.h" #include <taskLib.h> #include <stdio.h> #define RECV_BUF_SIZE 256 void serial_task(void) { serial_config_t cfg = { .dev_name = "/tyCo/0", .baud_rate = 115200, .data_bits = 8, .stop_bits = 1, .parity = 'N', .timeout_ms = 500 }; char recv_buf[RECV_BUF_SIZE]; int fd; int n; fd = serial_open(&cfg); if (fd == ERROR) { printf("serial open failed\n"); return; } printf("serial ready: %s @ %d\n", cfg.dev_name, cfg.baud_rate); while (1) { /* 先发一条测试数据 */ serial_send(fd, "PING\r\n", 6); /* 等待接收,超时 500ms */ n = serial_recv(fd, recv_buf, RECV_BUF_SIZE - 1, cfg.timeout_ms); if (n > 0) { recv_buf[n] = '\0'; /* 补字符串结束符,方便打印 */ printf("recv %d bytes: %s\n", n, recv_buf); } else if (n == 0) { printf("recv timeout\n"); } else { printf("recv error\n"); break; } taskDelay(sysClkRateGet()); /* 延时 1 秒 */ } serial_close(fd); }

taskDelay(sysClkRateGet())延时一秒,sysClkRateGet()返回系统时钟频率。recv_buf[n] = '\0'是为了用printf打印时不会越界读到脏数据。这个任务可以用taskSpawn创建,也可以直接在目标板 shell 里调用serial_task函数。

4. 避坑与排查:串口调试中最容易翻车的几个点

4.1 打开设备返回 ERROR

现象是open("/tyCo/0")直接失败,errno可能是ENODEV或ENOENT。原因通常是设备名不对,或者串口驱动没有加载到内核。VxWorks 的串口驱动需要在 BSP 的sysSerial.c里初始化,如果 BSP 没配这个串口,设备节点就不存在。解决办法是先在目标板 shell 里执行devs命令,列出所有已注册设备,确认串口设备名到底是什么。有些 BSP 默认只注册一个调试串口,第二个串口需要手动在sysSerialHwInit里打开。

4.2 波特率设置成功但收发乱码

现象是ioctl返回成功,但收到的数据全是乱码。原因通常是波特率不匹配,或者数据位、校验位设置和对方不一致。VxWorks 的FIOBAUDRATE设置的是驱动分频值,如果目标板晶振频率和驱动里假设的不一致,实际波特率会有偏差。解决办法是用示波器量一下 TX 引脚的实际波特率,或者换一个标准波特率试试。另外注意FIOSETOPTIONS里如果没设OPT_RAW,驱动可能会对特殊字符做转义,导致数据被篡改。

4.3 read 永久阻塞导致任务卡死

现象是接收任务调用read后不再返回,系统其他任务正常但串口任务无响应。原因是read默认阻塞,对方没发数据时任务挂起。如果这个任务还持有互斥锁,整个系统可能死锁。解决办法是像示例里那样用select加超时,或者用ioctl(fd, FIONBIO, &on)设置非阻塞模式。非阻塞模式下read没数据会立刻返回ERROR,errno是EWOULDBLOCK,调用方需要轮询。两种方式各有取舍,select更优雅,非阻塞更简单。

4.4 发送数据丢失或截断

现象是write返回成功,但对方只收到部分数据。原因是write返回值小于请求长度,而代码没有循环写。串口发送缓冲区大小有限,大数据量时必然出现部分写。解决办法就是示例里的循环写,直到total等于len。另外如果发送任务优先级太低,缓冲区可能被其他任务抢占,导致发送间隔过大,对方按帧解析时可能断帧。这种情况需要调整任务优先级或加流控。

4.5 多次打开关闭后资源耗尽

现象是程序运行一段时间后open开始失败,errno是EMFILE。原因是每次open后没有对应的close,文件描述符泄漏。VxWorks 的文件描述符数量有限,默认可能只有几十个。解决办法是确保每个open都有close,尤其是在错误处理分支里。示例里serial_open在ioctl失败时会close(fd)再返回,这个细节不能省。

5. 二次开发与改造:从示例到可复用串口模块

把这份示例改造成项目里能直接用的串口模块,我一般会做三件事:参数外置、多串口支持、收发分离。

参数外置是把serial_config_t从代码里挪到配置文件或启动参数里。VxWorks 下可以用sysSymTbl或者环境变量,但更简单的做法是定义一个全局配置数组,在usrInit或应用启动时从 Flash 里读。这样换板子不用重新编译。

多串口支持是把serial_open、serial_send、serial_recv都改成接受serial_config_t *参数,内部用fd区分不同串口。如果串口数量固定,可以定义一个serial_dev_t数组,每个元素包含配置和文件描述符,再包一层按索引访问的接口。

收发分离是把接收做成独立任务,收到数据后放到环形缓冲区,发送任务从缓冲区取数据发送。这样收发互不阻塞,适合协议栈这种需要异步处理的场景。环形缓冲区用数组加读写指针实现,注意加锁保护,VxWorks 下可以用semMCreate创建互斥信号量。

/* ring_buffer.h - 环形缓冲区,用于串口异步收发 */ typedef struct { char buf[512]; int head; /* 写指针 */ int tail; /* 读指针 */ SEM_ID lock; /* 互斥信号量 */ } ring_buf_t; int ring_put(ring_buf_t *rb, const char *data, int len) { int i; semTake(rb->lock, WAIT_FOREVER); for (i = 0; i < len; i++) { int next = (rb->head + 1) % sizeof(rb->buf); if (next == rb->tail) { /* 缓冲区满 */ semGive(rb->lock); return i; } rb->buf[rb->head] = data[i]; rb->head = next; } semGive(rb->lock); return len; }

这个环形缓冲区用head和tail判断空满,head追上tail表示满,tail追上head表示空。semTake和semGive保证多任务访问安全。实际项目里缓冲区大小要根据最大帧长和任务调度周期来定,太小会丢数据,太大浪费内存。

验证改造是否成功,我习惯用回环测试:把串口的 TX 和 RX 短接,发什么收什么,跑一晚上看有没有丢包或内存泄漏。这个习惯是从一次现场故障后养成的,当时串口跑了几小时就死机,最后查出来是环形缓冲区指针溢出。从那以后我每次改完串口代码,都强制走一遍回环加长时间运行测试。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询