VxWorks串口通信实战:从设备模型到超时收发完整示例

发布时间:2026/10/16 10:49:11
VxWorks串口通信实战:从设备模型到超时收发完整示例 简介这份资源面向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.mdserial_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没数据会立刻返回ERRORerrno是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 短接发什么收什么跑一晚上看有没有丢包或内存泄漏。这个习惯是从一次现场故障后养成的当时串口跑了几小时就死机最后查出来是环形缓冲区指针溢出。从那以后我每次改完串口代码都强制走一遍回环加长时间运行测试。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询