ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

VxWorks串口通信实战:从termios配置到Zynq平台部署

VxWorks串口通信实战:从termios配置到Zynq平台部署 简介VxWorks广泛部署于航空航天、通信、工业自动化等实时性要求极高的场景串口通信则是设备交互与调试环节中最常用也最基础的手段之一。这份示例程序以TestUart项目为载体面向嵌入式初学者与工程开发人员重点演示VxWorks标准串口驱动API的完整调用流程包括端口打开、波特率及数据位/校验位/停止位配置、数据收发、中断服务注册、任务同步与异常返回处理等关键分支。压缩包共25个文件整体仅26KB以main.c、usart.c等C源码为核心同时附有Tornado环境下的.mcp工程文件、.map链接映射、.hex烧写文件以及.lst、.sym等调试文件便于从源码到运行镜像逐层对照。目前该资源已有762人学习使用。通过研读并调试这些代码读者能掌握serialOpen、serialWrite、serialRead等接口的实际用法理解VxWorks串口驱动在中断与多线程环境中的配合要点进而快速移植到自己的硬件平台或应用项目中提升嵌入式系统开发效率。1. 项目概述与核心思路拆解1.1 这个示例到底解决什么问题搞过嵌入式开发的朋友都清楚VxWorks作为一款硬实时操作系统在航空、航天、工业控制、高端装备这些领域里依然是难以替代的存在。而串口通信恰恰是嵌入式系统里最基础、最常用的调试和数据交互手段——小到启动时的控制台输出大到设备间的指令下发、状态回传串口几乎贯穿整个产品生命周期。我接触VxWorks有几年时间从最开始在模拟器上跑通一个Hello World到后来在真实板卡上调试驱动踩过的坑不少。其中串口通信这块网上资料不算少但大多停留在调用open/read/write这种API层面的简单示范真正涉及VxWorks特有的设备驱动框架、串口参数配置细节、以及多任务环境下串口使用的注意事项讲清楚的并不多。这篇博文就基于一个实际可运行的VxWorks串口通信示例程序展开核心目标有三个第一讲清楚VxWorks下串口设备从打开、配置到读写关闭的完整流程第二把容易踩坑的细节和背后的原理说透比如波特率配置为什么有的板子不生效、中断模式下read返回时机怎么控制第三给出一份能直接编译运行的参考代码让初学者能快速搭建起自己的串口通信测试环境。1.2 为什么选择标准串口驱动框架而不是直接操作寄存器有个常见误区很多从裸机开发转过来的工程师习惯直接读寄存器。在VxWorks上这么做短期看好像效率很高实际上代价很大。VxWorks的设备驱动架构里串口被抽象成了标准的tty设备通过tyCoDrv、uart驱动层和具体芯片驱动三层结构来管理。应用层只需要用open/read/write/ioctl这套POSIX接口操作设备文件即可完全不用关心底层是16550还是Zynq上的UART控制器。选择这套标准框架的好处很明显。首先是可移植性同一份应用层代码从x86平台换到ARM平台只需要改驱动配置应用层一行不用动。其次是系统级的资源管理中断处理、环形缓冲区、任务调度这些事情驱动层已经处理好了应用层介入过多反而容易引入竞态问题。还有一点VxWorks的调试工具和可视化工具对标准设备节点的支持更友好用i命令可以快速查看设备列表排查问题方便很多。当然这也不是说寄存器操作就一无是处比如在启动早期、驱动还没有加载完成的时候通过串口输出启动日志确实需要直接在BSP层操作寄存器。但那属于系统移植范畴和本文讨论的应用层通信不是一回事。应用层开发老老实实走标准驱动框架才是投入产出比最高的选择。2. 串口通信的关键API与参数配置详解2.1 设备节点与open操作的正确理解在VxWorks里串口设备节点一般默认叫/tyCo/0、/tyCo/1这样对应系统的第0个、第1个串口。这里容易有个混淆点VxWorks控制台串口console用的也是这个设备节点如果在目标机上把控制台重定向到了/tyCo/0那再打开/tyCo/0做数据通信就可能会和shell输出打架。所以实际项目里有个约定俗成的做法调试串口和业务串口分开业务串口用/tyCo/1或更高编号。如果一定要用同一个串口就需要把控制台重定向到其他设备比如网络虚拟控制台或者干脆关掉shell对串口的占用。代码层面open操作很直观int fd; fd open(/tyCo/1, O_RDWR | O_NOCTTY, 0); if (fd ERROR) { perror(open /tyCo/1 fail); return -1; }这里O_NOCTTY标志在VxWorks里并非所有版本都强制需要但加上是良好习惯——防止该串口成为控制终端后受到shell信号干扰。我在一次实际调试中就遇到过不加O_NOCTTY程序跑一段时间后莫名其妙收到SIGINT终止排查半天才发现是控制终端信号问题。2.2 串口参数配置波特率、数据位、停止位、校验位open之后第一件事就是配置参数。VxWorks里最常用的是ioctl配合FIOBAUD设置波特率或者用tcsetattr配合termios结构体做完整配置。后者更标准可配置项更多我建议统一用termios方式。一个完整配置的代码片段如下#include termios.h struct termios tty; int baudrate B115200; memset(tty, 0, sizeof(tty)); if (tcgetattr(fd, tty) ! OK) { perror(tcgetattr fail); close(fd); return -1; } tty.c_cflag CLOCAL | CREAD | CS8; tty.c_cflag ~PARENB; // 无校验 tty.c_cflag ~CSTOPB; // 1位停止位 tty.c_cflag ~CRTSCTS; // 禁用硬件流控 tty.c_iflag ~(IXON | IXOFF | IXANY); // 禁用软件流控 tty.c_iflag ~(ICRNL | INLCR | IGNCR); // 不做回车换行转换 tty.c_oflag ~OPOST; // 原始输出模式不做换行转换 tty.c_lflag ~(ICANON | ECHO | ECHOE | ISIG); // 原始输入模式 cfsetispeed(tty, baudrate); cfsetospeed(tty, baudrate); tcsetattr(fd, TCSANOW, tty);这里特别强调一下c_lflag里的ICANON。很多新手在这块翻车不关闭ICANONread会工作在线路缓冲模式必须等收到换行符才会返回导致明明设备发来了数据程序却一直阻塞。我在Zynq平台上调一个传感器数据采集时就因为这个参数没配置对浪费了一整天。关闭ICANON后read就能立刻返回当前缓冲区已有的数据。2.3 非阻塞模式与超时控制串口通信场景里阻塞式read一把梭能应付简单需求但real-world里的设备往往不是有问必答对端可能宕机、可能没上电、可能协议交互有延迟。如果read无限期阻塞整个任务就卡死了。解决思路有两种第一种是设置非阻塞模式。用fcntl或者VxWorks的ioctl(fd, FIONBIO, on)来开启之后read会立即返回如果没有数据就返回ERROR然后查errno去判断是EAGAIN还是其他错误。这种方式需要自己在循环里做轮询或配合select使用。第二种是termios里的VTIME和VMIN组合。VMIN表示最少读取字节数VTIME表示超时时间单位是0.1秒。比如设置tty.c_cc[VTIME] 10tty.c_cc[VMIN] 0read会在收到任何数据字节后等待1秒没有新数据到来就返回实现读完当前数据就超时返回的效果。这在处理变长帧、不定长响应时非常好用。从实际经验看select加非阻塞是最稳妥的方案尤其在多任务环境下既能保证及时响应又不会忙等消耗CPU。VxWorks的select实现和POSIX标准兼容性很好代码写法和Linux下几乎一致。3. 完整的串口通信示例程序3.1 程序功能设定和文件组织为了让示例有实际参考价值我把它设计成这样一个场景目标板通过串口外接一个温度传感器模块传感器上电后每2秒主动上报一帧数据帧格式是帧头0xAA 0x55加1字节温度值加1字节校验和。程序要做的是打开串口配置好参数循环接收数据帧解析出温度值同时支持用户通过控制台输入命令主动发送查询指令。整个程序分成三个文件uart_demo.h定义接口和常量uart_demo.c是核心实现uart_demo_main.c是入口任务代码。这样组织方便后续扩展——如果要把接收到的数据转发到网络只需要在解析回调里加逻辑核心串口代码不用动。3.2 核心代码实现先看头文件/* uart_demo.h */ #ifndef __UART_DEMO_H__ #define __UART_DEMO_H__ #include vxWorks.h #include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include errno.h #include termios.h #include ioLib.h #define UART_DEVICE /tyCo/1 #define UART_BAUDRATE B115200 #define FRAME_HEAD0 0xAA #define FRAME_HEAD1 0x55 typedef struct { char devName[32]; int baudrate; int isOpen; } UartConfig; int uart_open(const char *dev, int baudrate); int uart_config(int fd, int baudrate); int uart_send(int fd, const char *buf, int len); int uart_recv(int fd, char *buf, int maxLen, int timeoutMs); void uart_close(int fd); void uart_demo_task(void); #endif再是核心实现文件。这里重点展示打开、配置、发送和带超时接收的逻辑/* uart_demo.c */ #include uart_demo.h int uart_open(const char *dev, int baudrate) { int fd; fd open(dev, O_RDWR | O_NOCTTY, 0); if (fd ERROR) { printf([UART] open %s failed, errno 0x%x\n, dev, errno); return ERROR; } if (uart_config(fd, baudrate) ! OK) { printf([UART] config %s failed\n, dev); close(fd); return ERROR; } printf([UART] open and config %s success\n, dev); return fd; } int uart_config(int fd, int baudrate) { struct termios tty; int speed; switch (baudrate) { case 9600: speed B9600; break; case 19200: speed B19200; break; case 38400: speed B38400; break; case 115200: speed B115200; break; default: speed B115200; break; } memset(tty, 0, sizeof(tty)); if (tcgetattr(fd, tty) ! OK) { return ERROR; } tty.c_cflag CLOCAL | CREAD | CS8; tty.c_cflag ~PARENB; tty.c_cflag ~CSTOPB; tty.c_cflag ~CRTSCTS; tty.c_iflag ~(IXON | IXOFF | IXANY); tty.c_iflag ~(ICRNL | INLCR | IGNCR); tty.c_oflag ~OPOST; tty.c_lflag ~(ICANON | ECHO | ECHOE | ISIG); tty.c_cc[VMIN] 0; tty.c_cc[VTIME] 1; cfsetispeed(tty, speed); cfsetospeed(tty, speed); if (tcsetattr(fd, TCSANOW, tty) ! OK) { return ERROR; } tcflush(fd, TCIOFLUSH); return OK; } int uart_send(int fd, const char *buf, int len) { int written 0; int ret; while (written len) { ret write(fd, buf written, len - written); if (ret ERROR) { printf([UART] write error, errno 0x%x\n, errno); return ERROR; } written ret; } return written; } int uart_recv(int fd, char *buf, int maxLen, int timeoutMs) { fd_set rfds; struct timeval tv; int ret; FD_ZERO(rfds); FD_SET(fd, rfds); tv.tv_sec timeoutMs / 1000; tv.tv_usec (timeoutMs % 1000) * 1000; ret select(fd 1, rfds, NULL, NULL, tv); if (ret ERROR) { printf([UART] select error, errno 0x%x\n, errno); return ERROR; } if (ret 0) { return 0; // 超时无数据 } ret read(fd, buf, maxLen); return ret; }我在uart_recv里用了select加非阻塞思维的超时控制而不是直接依赖VTIME/VMIN。原因是这样更灵活VTIME的最小粒度是0.1秒如果调用方想设置50ms超时termios方式就做不到。而select的时间粒度更细配合循环还能实现每次最多等N毫秒这种更复杂的逻辑。代码中uart_send做了写满才算完的处理这也是实际项目中容易忽略的。写串口不一定一次write就能把整个缓冲区写出去尤其底层缓冲区剩余空间不足时write会返回部分写入字节数。不做循环处理的话发送长帧可能出现数据截断。3.3 应用入口任务和数据帧解析看入口任务怎么组织/* uart_demo_main.c */ #include uart_demo.h #define RECV_BUF_LEN 256 void uart_demo_task(void) { int fd; char recvBuf[RECV_BUF_LEN]; int recvLen; int i; char tempRaw; char checksum; char sum; float temperature; char cmd[16]; fd uart_open(UART_DEVICE, UART_BAUDRATE); if (fd ERROR) { return; } while (1) { recvLen uart_recv(fd, recvBuf, RECV_BUF_LEN, 1000); if (recvLen 0) { /* 简单帧解析找帧头0xAA 0x55 */ if (recvLen 4) { for (i 0; i recvLen - 3; i) { if ((unsigned char)recvBuf[i] FRAME_HEAD0 (unsigned char)recvBuf[i1] FRAME_HEAD1) { tempRaw recvBuf[i2]; checksum recvBuf[i3]; sum (char)(FRAME_HEAD0 FRAME_HEAD1 tempRaw); if (sum checksum) { temperature (float)tempRaw; printf([UART] temperature report: %.1f C\n, temperature); } else { printf([UART] checksum error\n); } break; } } } } /* 检测控制台是否有输入支持发送查询指令 */ if (fgets(cmd, sizeof(cmd), stdin) ! NULL) { cmd[strcspn(cmd, \n)] \0; if (strlen(cmd) 0) { uart_send(fd, cmd, strlen(cmd)); } } taskDelay(10); } uart_close(fd); }入口任务做了三件事接收数据、解析帧、检查控制台输入并发送指令。需要注意fgets在VxWorks的shell环境下读的是标准输入这里在循环里调用如果shell没有给这个任务分配标准输入fgets会阻塞。实际调试中更常见的做法是把指令下发做成单独的命令函数在shell里直接敲命令触发发送。帧解析上这个示例用了最朴素的遍历查找帧头方式。真实项目里如果数据量很大、波特率很高这种逐字节遍历的方式可能有性能问题更好的做法是维护一个环形缓冲区或者状态机逐字节解析。但作为示例思路表达清楚就行了——核心是想说明收到的是字节流不是完整帧一定要自己做帧边界识别和校验。4. 常见问题与排查技巧实录4.1 read一直阻塞不返回怎么办这应该是VxWorks串口开发里最高频的问题。前面在配置部分其实已经埋了伏笔绝大多数情况是termios里c_lflag没有关闭ICANON。最佳排除顺序是先用tcgetattr把当前配置打出来确认ICANON、ECHO这些位的状态再确认是否设置了VTIME/VMIN如果两个参数都是0非阻塞模式下read会立即返回而阻塞模式下则永远等待最后确认是不是底层驱动本身的配置有问题这种一般发生在自己修改过BSP串口驱动的情况下。我曾经在一个项目里遇到非常隐蔽的情况板子上电后串口工作正常但只要运行过一段网络相关的初始化代码串口read就再也不返回了。后来排查发现是网络初始化时修改了全局中断优先级和CPU中断使能状态影响了串口接收中断。这类问题已经超出应用层范围只能通过查看中断状态寄存器定位。但这也提醒我们遇到诡异现象时不要只盯着应用层代码多想想周围环境的变化。4.2 数据接收内容是乱码乱码问题一般集中在波特率、电平、数据格式三个层面。先排除波特率不匹配这是最常见的原因比如传感器默认9600而程序配了115200。其次是线路电平问题RS232和TTL电平不能直接对接中间要有电平转换芯片工业现场还要考虑地线是否共地。最后是数据格式比如设备是8位数据位1位停止位程序配置成了7位数据位接收自然错乱。有一个实用排查技巧把示波器接到TX/RX线上直接看波形。从起始位和停止位的宽度能够反推实际波特率用1个数据位的宽度算一下这样就能确定是配置问题还是硬件问题不用猜。我在现场调试时经常用这个办法区分程序没配对和线接错了往往就在一瞬间。4.3 串口发送出去了但对端没反应先确认数据真的从引脚上发出去了示波器一量就能验证。如果量不到信号说明程序可能根本没执行到write或者设备文件打开失败。如果量到了波形但对端没反应重点查对端设备的手册确认它的通信方式——有的设备是先听后说必须收到特定指令才上报数据有的设备是纯主动上报不需要任何指令还有的设备用的是RS485需要控制方向引脚单纯发数据不够还得把收发方向切换成发送模式这个在很多串口转RS485的硬件上是个隐蔽的坑。VxWorks下还有一种特殊情况如果串口被系统控制台占用了应用层可以正常write但数据可能被控制台相关的处理逻辑干扰。比如你printf输出调试信息也走这个串口那应用数据和控制台输出就混在一起了。这就是我在前文强调为什么业务串口要和调试串口分开的原因。4.4 高频收发时丢数据高频收发场景下丢数据通常是接收缓冲区溢出或者中断响应不及时。VxWorks的串口驱动内部有缓冲区但如果应用层读取不够频繁、处理耗时太长数据就会在驱动缓冲区里堆积溢出。关键在于调整调度策略让接收任务有足够的优先级缩短从中断产生到应用层read之间的时间。还有一个容易忽视的点应用层处理完一帧数据后最好不要在任务上下文里做大量计算和打印操作打印耗时会拉低整体接收吞吐正确做法是先把数据拷贝到自己的接收队列交给低优先级任务慢慢处理接收任务继续保持高速。我在Zynq平台上遇到过莫名其妙丢开头几个字节的情况后来发现是DMA配置问题——DMA接收模式和串口中断模式配合不良。如果程序需要高可靠性收发建议认真阅读芯片的UART控制器手册确认DMA描述符长度和中断触发阈值设置合理。4.5 常见问题速查表为了方便以后排查我把自己遇到过的典型问题整理成一张速查表基本覆盖了串口开发90%的坑现象可能原因排查方法read一直阻塞未关闭ICANONVTIME/VMIN配置不对tcgetattr打印配置检查c_lflag数据乱码波特率不匹配电平不对数据位格式错误示波器量波形核对设备手册发送无反应设备文件打开失败RS485方向未控制对端需指令触发示波器确认输出检查ioctl方向控制高频收发丢数据缓冲区溢出任务调度不及时打印耗时太长提高接收任务优先级减少打印开销打开设备失败设备节点不存在驱动未加载设备被占用i命令查看设备列表检查BSP配置打印和业务数据混在一起控制台复用同一串口shell重定向控制台换成独立业务串口偶发帧校验错误线路干扰地电位差波特率微小偏差加屏蔽线共地处理降低波特率验证5. 进阶实践与部署建议5.1 如何把示例程序部署到VxWorks镜像里示例代码写好后很多人卡在怎么把它编译进VxWorks工程。这里补充一个最小操作路径在VxWorks Workbench里创建或者打开一个bootable工程把三个源文件加进工程源码目录然后通过Workbench的构建配置把源文件编译进vxWorks镜像的usrAppInit或自定义启动任务里。具体路径不同版本略有差异但思路一样——把uart_demo_task挂到系统启动任务序列中。如果是VxWorks 7底层是基于Yocto的VSBVxWorks Source Build和VIPVxWorks Image Project两层构建方式。VSB里需要确保包含了串口驱动组件比如IPNET_UART或DRV_SIO这类组件不同BSP的名字可能不同。确认方法是在VSB配置界面里搜16550或uart关键字。IP组件配置不对是最常见的启动后串口无输出的原因之一。5.2 与Zynq平台相关的特殊性热词里出现了不少vxworks移植到正点原子zynq 7100之类的内容。Zynq系列是Xilinx现在是AMD的异构SoC内部是ARM Cortex-A9双核加FPGA架构。VxWorks跑在ARM核上串口硬核用的是Zynq的UART控制器在VxWorks的BSP里已经有对应驱动支持。实际上手时有一点和纯粹ARM平台不同——Zynq的UART时钟由PS侧的时钟配置决定如果FSBL或者引导程序里设置的UART时钟和VxWorks BSP编译时的默认时钟不一致就会出现波特率偏移。有次我们的板子串口输出全是乱码查了很久最后发现是FSBL里改了UART时钟频率VxWorks BSP里却是按另一个频率计算的。这类问题的排查方法也很直接量波形确认实际波特率然后去改BSP里的时钟宏定义重新编译VSB。5.3 多任务环境下串口使用的设计模式VxWorks的多任务环境是它的核心优势但也是串口程序设计的难点所在。如果多个任务同时对一个串口做read/write没有协调机制的话会出现严重的资源竞争问题。我常用的设计模式是单读写任务消息队列分发一个专门的串口接收任务负责read和解析解析出来的数据通过消息队列发给各个业务任务发送方向则是所有任务都把要发的数据放入发送队列由串口发送任务统一write。这样做的好处是串口只有一个任务在操作天然避免了竞态而且接收任务优先级可以设得较高保证不丢数据的同时其他任务不会被长时间打断。如果系统里有强实时性要求可以在接收任务里直接做信号量同步中断服务程序收到一帧数据就释放信号量接收任务立刻被唤醒去处理。这种中断信号量任务的组合拳是VxWorks下比较高效的模式比裸轮询省电比纯中断上下文处理安全中断上下文里不允许调用printf这类非中断安全的操作。5.4 收尾经验上值得留意的几点最后补充几个我实际项目中沉淀下来的小习惯虽然不能保证每个项目都适用但多数情况下能帮你少走弯路第一收到数据先存后解析不要把解析逻辑直接写在read返回后的处理里尤其是涉及浮点运算、打印格式化这些耗时操作时尽量和接收路径解耦。第二串口配置做完后一定要调用tcflush清空缓冲区否则上电残留的脏数据会影响第一帧的解析。第三程序的异常分支一定要打印errnoVxWorks的errno能直接反映底层错误类型比返回-1这种信息有价值得多。第四多留调试接口比如通过shell命令动态修改波特率、打开或关闭帧解析的时间统计这些看似多余的功能在现场问题上能省下大量时间。我始终觉得串口通信在嵌入式开发里不算高深技术但恰恰是这种基础模块最考验工程师对系统的整体把握能力。一个小小串口牵扯到BSP、设备驱动、中断机制、任务调度、应用层逻辑每一层都可能出问题。把这份示例程序跑通只是第一步更重要的是理解每一层之间的关系这样就算以后遇到复杂问题也能顺着这条技术链路一步步定位到根因。希望这篇内容对正打算在VxWorks上做串口开发的你有所帮助。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进