2. VxWorks I/O系统与文件描述符

VxWorks 的 I/O 系统本质上是一层“设备抽象层”:上层应用只看到文件描述符(fd)和一组标准调用(open/read/write/ioctl/close),下层则由具体驱动提供实际的数据搬运与控制逻辑。理解这一层的关键,是搞清楚 fd 如何映射到设备、调用如何穿过 I/O 系统落到驱动函数,以及错误如何沿调用链返回。

一、设备抽象:fd、设备节点与驱动函数表

在 VxWorks 中,设备通过 iosDevAdd() 注册到 I/O 系统,形成一个设备节点(DEV_HDR)。每个设备节点携带设备名、驱动号以及指向驱动函数表的指针。应用调用 open("/tyCo/0", O_RDWR, 0) 时,I/O 系统按路径查找设备节点,再调用该设备驱动函数表中的 open 例程,返回一个文件描述符。fd 并不是设备本身,而是 I/O 系统维护的一张描述符表中的索引,表中记录该 fd 对应的设备节点和驱动私有数据指针。

驱动函数表通常包含七个入口:open、close、read、write、ioctl、remove、create(部分驱动可省略后两者)。其中 open 负责初始化设备并返回私有数据指针,read/write 负责数据搬运,ioctl 负责参数配置与状态查询。这种“函数表 + 私有数据”的结构,使同一套 I/O 接口可以适配串口、块设备、网络设备等不同硬件。

VxWorks I/O 调用链与设备抽象 应用层 open / read / write ioctl / close I/O 系统 fd 表 / 设备节点查找 iosDevAdd 注册表 驱动函数表 open / close / read write / ioctl / remove 调用 分发 驱动私有数据 寄存器基址 / 缓冲区 / 状态 硬件设备 UART / Flash / 网口 错误码沿调用链返回(errno) fd 描述符表 fd=3 → /tyCo/0 fd=4 → /ata0/0 fd=5 → /ram0 每项含设备节点指针 与驱动私有数据指针 索引 图例: 应用接口 I/O 系统 驱动函数表 私有数据 硬件 错误返回路径

二、调用链:从 open 到驱动例程

以串口设备 /tyCo/0 为例,一次完整的写操作调用链如下:应用调用 write(fd, buf, n),I/O 系统根据 fd 找到设备节点,取出驱动函数表中的 write 例程并调用,同时把 fd 对应的私有数据指针传入。驱动例程将数据写入 UART 发送寄存器或环形缓冲区,返回实际写入字节数。若设备未就绪,驱动返回 ERROR,I/O 系统将 errno 置为对应错误码,应用层通过 errno 判断失败原因。

这里有一个容易混淆的点:read/write 的返回值是“实际传输字节数”,可能小于请求长度,尤其在非阻塞模式下。应用不能假设一次 write 就写完所有数据,必须循环处理剩余部分。而 ioctl 的返回值通常是 0 表示成功、-1 表示失败,具体命令字(如 FIOBAUDRATE、FIOSETOPTIONS)决定参数含义。

三、错误处理:errno 与返回值约定

VxWorks I/O 系统遵循 POSIX 风格约定:成功返回非负值,失败返回 ERROR(-1),并设置全局 errno。常见错误码包括 S_ioLib_NO_DEVICE(设备不存在)、S_ioLib_DEVICE_BUSY(设备忙)、S_ioLib_IO_ERROR(底层 I/O 错误)、EINVAL(参数无效)。在驱动内部,应通过 errnoSet() 设置错误码,而不是直接返回错误码,否则会破坏调用链的约定。

注意:errno 是任务私有的(在 VxWorks 中通常通过任务变量实现),因此多任务并发调用同一设备时,各任务看到的 errno 互不干扰。但驱动内部的共享状态(如环形缓冲区)仍需自行加锁保护。

四、实操示例:打开串口并配置波特率

下面示例演示打开 /tyCo/0、设置波特率 115200、写入一行数据并读取回显。示例参数为教学假设值,实际波特率需与硬件匹配。

#include <ioLib.h>
#include <tyLib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

void uartDemo(void)
{
    int fd;
    char txBuf[] = "HELLO_VXWORKS\r\n";
    char rxBuf[64];
    int  n;

    /* 1. 打开设备,O_RDWR 表示读写 */
    fd = open("/tyCo/0", O_RDWR, 0);
    if (fd == ERROR) {
        printf("open failed, errno=%d\n", errno);
        return;
    }

    /* 2. 通过 ioctl 设置波特率,参数为整数指针 */
    {
        int baud = 115200;
        if (ioctl(fd, FIOBAUDRATE, (int)&baud) == ERROR) {
            printf("set baudrate failed, errno=%d\n", errno);
            close(fd);
            return;
        }
    }

    /* 3. 写入数据,检查实际写入长度 */
    n = write(fd, txBuf, strlen(txBuf));
    if (n != (int)strlen(txBuf)) {
        printf("write short: %d/%d, errno=%d\n",
               n, (int)strlen(txBuf), errno);
    }

    /* 4. 读取回显,非阻塞模式下可能返回 0 */
    n = read(fd, rxBuf, sizeof(rxBuf) - 1);
    if (n > 0) {
        rxBuf[n] = '\0';
        printf("recv: %s\n", rxBuf);
    } else if (n == 0) {
        printf("no data available\n");
    } else {
        printf("read failed, errno=%d\n", errno);
    }

    /* 5. 关闭设备,释放 fd */
    close(fd);
}

运行结果判断依据:若 open 返回非负 fd,说明设备节点存在且驱动 open 成功;若 ioctl 返回 0,说明波特率命令被驱动接受;write 返回值等于请求长度,说明数据已全部交给驱动;read 返回大于 0 表示收到回显,返回 0 表示当前无数据(非阻塞),返回 -1 则需查看 errno 定位是设备错误还是参数错误。

五、常见错误与排查要点

现象可能原因排查方法
open 返回 -1,errno=S_ioLib_NO_DEVICE设备名拼写错误或未调用 iosDevAdd 注册用 iosDevShow() 列出已注册设备
ioctl 返回 -1,errno=EINVAL命令字不被该驱动支持,或参数指针无效查阅驱动文档确认支持的 ioctl 命令
write 返回短写发送缓冲区满或设备流控生效循环写入剩余数据,或检查硬件流控引脚
read 长期返回 0非阻塞模式且无数据,或对端未发送确认打开标志是否含 O_NONBLOCK,检查对端