首页 / 资讯中心 / 文章详情

VxWorks串口通信例程:从设备名到read超时的实现与避坑指南

VxWorks串口通信例程:从设备名到read超时的实现与避坑指南 ★ FEATURED ARTICLE
简介VxWorks是一款广泛应用于航空航天、工业自动化等领域的实时操作系统串口通信在嵌入式开发中承担着设备调试与数据交换的重要角色。这份示例程序TestUart完整演示了VxWorks下串口驱动的调用流程面向嵌入式开发者及VxWorks初学者可直接参考或迁移到实际项目中。资源包共25个文件压缩包仅26KB包含C源码.c/.h及编译生成的obj、hex、map、sym等中间文件还有mcp/mcw工程文件与lst列表文件便于在Tornado等环境中查看、编译和调试。已有765人学习下载。代码涵盖串口初始化、参数配置波特率、数据位、校验位、停止位、数据收发、错误处理、关闭串口并涉及中断服务例程注册与多任务同步展示了sysLog日志输出辅助调试的方法。理解后能掌握VxWorks串口编程的基本套路结合实例理解串口资源管理、超时处理与多任务协作方式为硬件调试和设备通信打下扎实基础。1. 串口收不到数据这套VxWorks通信例程替我把坑都踩平了有一次我在调试一台基于VxWorks的采集设备板子上电后姿态数据始终传不上来串口调试助手一片空白。我花了一下午换接线、查波特率最后才发现是设备节点名写错了。VxWorks的串口设备不像Linux那样统一叫/dev/ttyS0它由BSP决定有的叫/tyCo/0有的叫/sio/0名字挂错open直接失败。这份串口通信示例程序解决的就是这类问题它把VxWorks下最常用的串口初始化、收发、超时处理和任务化封装成一段能直接跑通的代码明确告诉你在哪里配置设备名、波特率、校验位以及read为什么会卡死。适合刚转到VxWorks的嵌入式工程师也适合想在项目里快速接入串口通信的开发者。下面我从技术底座开始拆最后给出可以直接抄走的封装框架。2. VxWorks串口通信的技术底座设备模型与open/read/write/ioctl2.1 串口设备从哪来设备名和BSP绑定关系VxWorks的设备管理沿用了类UNIX的文件描述符思路串口被抽象成字符设备用户空间用open拿到fd再用read、write、ioctl操作。但设备名不是全局统一而是由BSP板级支持包里的驱动类型决定。常见的有TTY字符设备/tyCo/0这是老牌驱动也有用/ttyS0或/sio/0的POSIX风格节点取决于你在config.h里怎么配置。拿到一套新板子我第一件事是在VxWorks shell里敲devs命令看设备列表里实际注册了哪些串口节点而不是猜。如果devs显示/tyCo/0和/tyCo/1那open(/tyCo/0, ...)才靠谱如果板子跑的是VxWorks 7还可能要查vxb层绑定关系。这里最容易犯的错是把Linux的/dev/ttyS0带过来结果open返回ERROR还找不到原因。2.2 打开串口open参数与返回值的正确检查VxWorks的open声明在fcntl.h典型用法是#include vxWorks.h #include fcntl.h #include ioLib.h int fd; fd open(/tyCo/0, O_RDWR, 0); if (fd ERROR) { printf(open /tyCo/0 failed, errno 0x%x\n, errnoGet()); return ERROR; }打开之后errno会告诉你失败细节。比如S_ioLib_UNKNOWN_DEV表示设备名不存在S_ioLib_TIMED_OUT表示资源被占用超时。flag参数用O_RDWR足够第三个参数在VxWorks里通常填0不用像传统UNIX那样传权限位。2.3 关键ioctl波特率、raw模式和缓冲区清空VxWorks串口控制主要靠ioctl下面的表是示例程序里最常用的几个命令全部来自ioLib.h和sioLib.h命令作用参数说明FIOBAUDRATE设置或查询波特率arg为0时查询当前值arg非0时设置数值例如115200FIOSETOPTIONS设置串口工作模式OPT_RAW表示原始模式不做回显和行缓冲FIOGETOPTIONS获取当前模式返回int型选项字FIOFLUSH清空输入输出缓冲arg传0即可FIOFIFO设置或查询FIFO深度部分驱动支持arg为FIFO大小我一般在open之后立刻做两件事设置波特率打开raw模式。注意FIOBAUDRATE第三个参数必须传整数本身而不是指针。传指针会静默失败驱动读到垃圾值波特率看起来没变实际是没设置成功。int baudRate 115200; if (ioctl(fd, FIOBAUDRATE, baudRate) ERROR) { printf(set baud rate failed\n); close(fd); return ERROR; } int opt OPT_RAW; if (ioctl(fd, FIOSETOPTIONS, opt) ERROR) { printf(set raw mode failed\n); close(fd); return ERROR; }如果不设置OPT_RAW串口可能运行在行缓冲模式接收数据要等到换行符才返回导致你发的二进制数据一直收不齐这是新手最常见的翻车点。设置完模式后用ioctl(fd, FIOFLUSH, 0)清掉之前积攒的脏数据保证后续read从干净的缓冲区开始。2.4 read、write和select超时阻塞与超时的平衡VxWorks的read在fd阻塞模式下会一直挂起当前任务直到串口有数据。这在某些场合可以但如果你希望接收函数在指定时间内没数据就返回就需要select来做轮询。示例代码里这样用#include selectLib.h #include sys/types.h #include sys/select.h int waitForSerialData(int fd, int timeoutMs) { struct timeval timeout; fd_set readSet; int ret; timeout.tv_sec timeoutMs / 1000; timeout.tv_usec (timeoutMs % 1000) * 1000; FD_ZERO(readSet); FD_SET(fd, readSet); ret select(fd 1, readSet, NULL, NULL, timeout); if (ret ERROR) { printf(select error, errno 0x%x\n, errnoGet()); return ERROR; } if (ret 0) { printf(select timeout\n); return 0; } if (FD_ISSET(fd, readSet)) { return 1; } return ERROR; }select返回0表示超时返回1表示fd可读返回值ERROR表示参数出错或中断。注意VxWorks里timeout被修改后可能变成剩余时间但这不影响我们因为每次调用都重新赋值。这个函数是我所有串口接收例程的基础。3. 核心模块拆解一个串口收发任务从初始化到数据返回的完整链路3.1 初始化函数串口启动五步走示例程序把初始化收敛成一个serialInit函数这样main函数里只调用一次逻辑清楚后面换板子只改参数。五步分别是先open设备节点再设置波特率接着设置raw模式清空缓冲区最后把fd存进全局句柄。#include vxWorks.h #include fcntl.h #include ioLib.h #include sioLib.h #include stdio.h static int serialFd ERROR; int serialInit(const char *devName, int baudRate) { serialFd open(devName, O_RDWR, 0); if (serialFd ERROR) { printf(open %s failed, errno 0x%x\n, devName, errnoGet()); return ERROR; } if (ioctl(serialFd, FIOBAUDRATE, baudRate) ERROR) { printf(set baud rate failed, errno 0x%x\n, errnoGet()); close(serialFd); return ERROR; } if (ioctl(serialFd, FIOSETOPTIONS, OPT_RAW) ERROR) { printf(set raw mode failed, errno 0x%x\n, errnoGet()); close(serialFd); return ERROR; } ioctl(serialFd, FIOFLUSH, 0); printf(serial %s initialized, baud %d\n, devName, baudRate); return serialFd; }这里devName传入/tyCo/0baudRate传入115200。open返回的fd不是固定值所以我用静态变量保存后面的收发函数直接用serialFd。如果open成功但ioctl失败必须记得close防止fd泄漏。调试时观察errno比println靠谱得多。3.2 发送数据write返回值的严格校验发送接口看起来就是一层write封装但直接把串口当管道怼容易出问题。VxWorks的write可能返回部分字节也可能是ERROR不能想当然认为一次调用就全发出去。示例代码用循环保证所有字节都写出int serialSend(const unsigned char *buf, int len) { int written 0; int ret; if (serialFd ERROR || buf NULL || len 0) { return ERROR; } while (written len) { ret write(serialFd, buf[written], len - written); if (ret ERROR) { printf(serial write error, errno 0x%x\n, errnoGet()); return ERROR; } written ret; } return written; }循环里buf[written]从上次断点接着写len - written表示剩余字节数。write在串口驱动里很少会阻塞但底层缓冲区满时会返回ERROR或中断这时候加个taskDelay再重发更稳妥。我通常在循环里放一条if (ret 0) { taskDelay(1); }taskDelay(1)让出1个tick的CPU避免忙等。3.3 接收数据用select超时模式构建非阻塞read接收比发送复杂因为不知道对端什么时候发数据。示例提供两种模式一种是直接read阻塞另一种是配合select超时读取。后者更实用我把它们组合成一个函数int serialRecv(unsigned char *buf, int maxLen, int timeoutMs) { int ret; int len; if (serialFd ERROR || buf NULL || maxLen 0) { return ERROR; } ret waitForSerialData(serialFd, timeoutMs); if (ret 0) { return 0; } if (ret ERROR) { return ERROR; } len read(serialFd, buf, maxLen); if (len ERROR) { printf(serial read error, errno 0x%x\n, errnoGet()); return ERROR; } return len; }调用时传入接收缓冲区和期望的最大长度超时时间按下发数据周期来定。比如下位机每100ms发一帧timeoutMs就设200留出余量。read返回的实际长度可能小于maxLen这是正常的串口分帧现象上层做协议解析时必须处理粘包和半包不能假设一次read就是完整的一帧。3.4 任务化taskSpawn的参数和栈大小估算串口接收需要常驻任务示例用taskSpawn创建#include taskLib.h static void serialReceiveTask(void *arg) { unsigned char buf[256]; int len; while (1) { len serialRecv(buf, sizeof(buf), 200); if (len 0) { /* 这里把数据交给协议层处理示例只做打印 */ printf(recv %d bytes: , len); for (int i 0; i len; i) { printf(%02x , buf[i]); } printf(\n); } } } int serialTaskStart(void) { int taskId; taskId taskSpawn(serialRecv, 100, 0, 20000, (FUNCPTR)serialReceiveTask, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); if (taskId ERROR) { printf(taskSpawn failed, errno 0x%x\n, errnoGet()); return ERROR; } return taskId; }taskSpawn第一个参数是任务名第二个是优先级100数字越小优先级越高第三个是状态选项0第四个是栈大小20000字节。串口任务里有printf和select栈不能给太小否则会栈溢出直接导致任务挂死系统表现为这个任务消失但不影响其他任务。第五个参数必须强转成FUNCPTR后面的0是函数参数VxWorks最多支持10个int参数这里只用了1个。我见过有人第五个参数直接写函数名编辑器报警告但不报错结果运行后任务起不来排查半天发现是类型不对。任务里while(1)配合200ms超时即使没有数据任务也会每秒回来5次不会卡死整个系统。如果你希望更低功耗可把超时调大些。4. 避坑与排查串口通信的四次翻车现场4.1 open失败errno一直报S_ioLib_UNKNOWN_DEV现象open串口节点时返回ERROR打印errno是S_ioLib_UNKNOWN_DEV代码检查了几遍没发现拼写问题设备名对着devs抄的还是失败。原因设备名写对了但devs输出里可能有多个同名设备比如/tyCo/0出现在两行说明串口驱动实例NVRAM配置不同另外一些BSP在系统启动阶段没把串口驱动加载到I/O系统devs里根本看不到。解决先确认devs输出然后用iosDrvShow查驱动是否注册再看config.h里关于串口控制器的宏定义。如果确认驱动没加载在usrConfig.c的usrIosExtraInit里补充驱动初始化而不是死磕open参数。4.2 read永远不返回数据进了串口却没动静现象read阻塞调用串口助手数据显示发送成功但接收任务一直卡死输出没有新增。原因没有设置OPT_RAW串口设备运行在规范模式驱动要等到回车换行符才把数据交给用户态。二进制帧里没有换行符于是数据一直积压在驱动缓冲区。解决在初始化里加上ioctl(fd, FIOSETOPTIONS, OPT_RAW)并且设置完后用ioctl(fd, FIOFLUSH, 0)清掉残留数据。从那以后我每次初始化串口都把raw模式和flush写在一起成了固定搭配。4.3 波特率设了等于没设实际通信速率还是9600现象调用ioctl(fd, FIOBAUDRATE, 115200)没报错但示波器测出来的波形速率还是9600或者对端收到的码流乱码。原因我在代码里写了ioctl(fd, FIOBAUDRATE, baudRate)把指针传给了ioctl。驱动内部把第三个参数当int处理指针被强转成整数数值远大于合法波特率范围驱动判定非法后保持原值。解决直接传int类型的波特率值不要传地址。检查ioctl实现源码的话会发现FIOBAUDRATE的参数语义是值传递没有间接寻址。这也是VxWorks和Linuxtermios的区别Linux用cfsetospeed传结构体指针VxWorks这个接口就是直接传值。4.4 taskSpawn返回OK但接收任务一启动就消失现象taskSpawn返回了合法任务ID但i命令查任务列表时找不到srialRecv任务名也拼错了系统里残留僵尸任务。原因任务函数内部发生了栈溢出或者未定义异常。串口任务里如果开一个大数组比如unsigned char buf[4096]放在栈上加上select的fd_set20KB栈可能不够或者任务函数入口参数类型不是FUNCPTR导致跳转地址错误。解决先减小局部数组改成malloc放在堆上然后把taskSpawn的栈大小从20KB加到40KB。我习惯在taskSpawn任务入口第一行打印taskID和taskName确认任务进了函数再往下排查。现在我的默认做法是所有串口任务里不用大栈数组统一用静态或malloc。5. 从示例到驱动框架把单一收发流程封装成可复用模块5.1 抽象串口对象结构体安排得明明白白基础示例能跑通但换一个串口就得改全局变量维护起来难受。我通常把它重构成一个SerialPort结构体把fd、设备名、回调和错误状态收拢在一起typedef struct { char deviceName[16]; int fd; int baudRate; int timeoutMs; int (*onData)(unsigned char *buf, int len); void *userArg; } SerialPort; int serialPortOpen(SerialPort *port, const char *devName, int baudRate); int serialPortSend(SerialPort *port, const unsigned char *buf, int len); int serialPortRecv(SerialPort *port, unsigned char *buf, int maxLen);这样同一进程可以管理两路串口/tyCo/0和/tyCo/1各创建一个SerialPort实例。回调onData用来在收数任务里抛出完整的帧协议层不在串口模块里写死方便你移植到自己的通信协议。5.2 接收侧加环形缓冲解决粘包和半包示例里一次read拿到的数据可能只是半个包也可能包含两三个包。我一般会在串口驱动层加一个环形缓冲把read到的原始字节全部塞进去另写一个serialParseFrame按帧头帧尾提取完整帧。环形缓冲代码不复杂关键是头和尾指针的处理#define RING_BUFFER_SIZE 2048 static unsigned char ringBuf[RING_BUFFER_SIZE]; static volatile int ringHead 0; static volatile int ringTail 0; int ringWrite(const unsigned char *data, int len) { int i; for (i 0; i len; i) { int next (ringHead 1) % RING_BUFFER_SIZE; if (next ringTail) { return i; } ringBuf[ringHead] data[i]; ringHead next; } return len; }ringHead指向下一个写入位置ringTail指向下一个读取位置。当(head1)%size tail时表示缓冲区满直接返回已经写入的字节数避免覆盖还没消费的数据。这个环是单生产者单消费者的VRxWorks任务里只要不多个任务同时调ringWrite就不需要加锁。解析帧时从ringTail开始扫描一旦找到帧头就尝试凑齐一帧长度凑不齐就等下一次写入后再扫。5.3 二次开发时的验证方法我固定的三遍检查改完代码不是直接下载到板子我会先跑三遍验证第一遍用串口回环测试把TXD和RXD短接代码每发一个字节就收一个字节校验收发一致这一步能发现波特率、raw模式的问题第二遍用另一台串口卡模拟下位机主动发已知的测试帧比如55 AA 00 FF看解析层能不能正确拼帧第三遍才接到实际设备上观察长时间跑一小时后有没有内存增长、任务卡死。这三遍下来90%的串口坑都能提前暴露。我印象最深的是第一次做VxWorks串口以为波特率设对就万事大吉结果回环测试直接暴露了raw mode没设数据在驱动里排队等换行符。从那以后我每次串口通信开发都强制走一遍回环验证再进入协议联调。希望这套示例程序和拆解思路能帮你在VxWorks下少走几圈弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站