3. 字符设备驱动框架

在 VxWorks 中,字符设备驱动并不像 Linux 那样依赖统一的 VFS 层和 cdev 注册机制,而是通过 设备节点(device node) 与 文件操作接口(file operation interface) 的组合,把驱动能力挂接到 I/O 系统上。理解这一框架的关键,是分清“设备如何被命名和找到”与“用户调用 read/write/ioctl 时驱动里究竟发生了什么”这两件事。

一、设备节点创建:iosDevAdd 与设备名

VxWorks 的 I/O 系统维护一张设备列表,每个设备节点由 DEV_HDR 作为头部,后接驱动私有数据结构。驱动初始化时调用 iosDevAdd() 把设备注册进系统,用户随后就能用设备名打开它。典型写法如下:

typedef struct {
    DEV_HDR devHdr;      /* 必须放在结构体首位 */
    int     chan;        /* 通道号 */
    UINT32  baseAddr;    /* 寄存器基地址 */
    SEM_ID  sem;         /* 互斥信号量 */
} MY_DEV;

LOCAL MY_DEV myDev;

STATUS myDevCreate(char *name, UINT32 base)
{
    myDev.baseAddr = base;
    myDev.sem = semMCreate(SEM_Q_PRIORITY | SEM_INVERSION_SAFE);
    if (myDev.sem == NULL) return ERROR;
    if (iosDevAdd(&myDev.devHdr, name, 0) != OK) return ERROR;
    return OK;
}

要点有三:第一,DEV_HDR 必须是私有结构体的第一个成员,因为 I/O 系统通过设备名找到 DEV_HDR 后,会把它强制转换为驱动自己的结构体指针;第二,设备名要唯一,通常形如 "/mydev0";第三,注册前应完成信号量、缓冲区等资源初始化,避免 open 时才发现资源缺失。

二、文件操作接口:把函数指针装进驱动表

VxWorks 通过 iosDrvInstall() 安装一组函数指针,形成驱动操作表。该函数返回一个驱动号(drvNum),设备节点在 iosDevAdd 时与之关联。核心接口包括 create、delete、open、close、read、write、ioctl 七项,字符设备通常只实现后五项,create/delete 可传 NULL。

LOCAL int drvNum;

drvNum = iosDrvInstall(
    NULL,            /* create */
    NULL,            /* delete */
    myOpen,          /* open   */
    myClose,         /* close  */
    myRead,          /* read   */
    myWrite,         /* write  */
    myIoctl          /* ioctl  */
);

安装顺序很重要:必须先 iosDrvInstall 得到 drvNum,再在 iosDevAdd 中把设备与 drvNum 绑定。若顺序颠倒,设备虽然出现在设备列表中,但调用 open 时会因找不到驱动表而失败。

VxWorks 字符设备驱动框架:注册链与调用链 用户层:open / close / read / write / ioctl 通过设备名(如 "/mydev0")访问,不直接接触驱动函数 VxWorks I/O 系统(iosLib) 按设备名查 DEV_HDR → 取 drvNum → 查驱动表 → 调用对应函数指针 设备列表 + 驱动表(iosDrvInstall 安装) 驱动操作表(drvNum 对应) myOpen myClose myRead myWrite myIoctl create/delete=NULL 驱动私有结构体 MY_DEV DEV_HDR(首位) chan / baseAddr SEM_ID 互斥 寄存器读写 / 环形缓冲区 调用链 注册链

三、open 与 close:资源绑定与释放

open 的职责不是“打开硬件”,而是为本次访问建立上下文。典型实现中,open 会检查设备是否已被占用、初始化读写位置、必要时申请缓冲区,并把私有结构体指针返回给 I/O 系统,作为后续 read/write 的句柄。若设备只允许单次打开,可用信号量或标志位实现互斥:

LOCAL int myOpen(MY_DEV *pDev, char *name, int mode, int flags)
{
    if (semTake(pDev->sem, NO_WAIT) != OK)
        return ERROR;          /* 已被占用 */
    pDev->openCnt++;
    return (int)pDev;          /* 返回句柄 */
}

close 则相反:递减打开计数,计数归零时释放缓冲区、复位设备状态并释放信号量。注意 close 不应关闭中断或销毁设备节点,否则再次 open 会失败。若驱动在 open 中申请了内存,close 必须成对释放,否则反复打开会造成内存泄漏。

四、read 与 write:数据搬运与阻塞语义

read/write 的参数是句柄、缓冲区指针和字节数。字符设备常见两种模式:查询式直接读写寄存器并立即返回;阻塞式在数据未就绪时挂起调用任务,等中断或轮询到数据后再唤醒。下面是一个带超时的查询式读示例:

LOCAL int myRead(MY_DEV *pDev, char *buf, int nBytes)
{
    int i;
    for (i = 0; i < nBytes; i++) {
        if (myDataReady(pDev) == FALSE) {
            if (semTake(pDev->sem, sysClkRateGet()) != OK)
                return i;      /* 超时,返回已读字节数 */
        }
        buf[i] = (char)CSR_READ(pDev->baseAddr, DATA_REG);
    }
    return nBytes;
}

要点:返回值必须是实际读写的字节数,出错返回 ERROR;阻塞等待要设置合理超时,避免任务永久挂起;若驱动可能被多个任务并发调用,需用互斥信号量保护共享寄存器或缓冲区。write 的对称实现中,还要注意在数据发送完成后判断硬件是否真正接受,而不是写完寄存器就认为成功。

五、ioctl:把控制类操作收进一个入口

read/write 只适合数据流,配置波特率、设置工作模式、读取状态寄存器等控制操作应通过 ioctl 完成。ioctl 的第二个参数是命令码,第三个参数是可变参数,通常传指针或整型值。命令码建议用 _IO/_IOR/_IOW 宏构造,避免不同驱动之间冲突:

#define MY_SET_BAUD   _IOW('m', 1, int)
#define MY_GET_STATUS _IOR('m', 2, int)

LOCAL int myIoctl(MY_DEV *pDev, int cmd, int arg)
{
    switch (cmd) {
    case MY_SET_BAUD:
        CSR_WRITE(pDev->baseAddr, BAUD_REG, *(int *)arg);
        return OK;
    case MY_GET_STATUS:
        *(int *)arg = CSR_READ(pDev->baseAddr, STAT_REG);
        return OK;
    default:
        return ERROR;          /* 未知命令必须返回错误 */
    }
}

实现 ioctl 时,务必对未知命令返回 ERROR,不要静默忽略;对指针参数要做有效性检查;涉及硬件状态切换的命令,应在切换前后加必要的延时或状态确认。这样用户程序才能通过返回值判断控制是否真正生效。

调试建议:在 open/close/read/write/ioctl 入口处打印设备名、命令码和返回值,配合 iosDevShow() 查看设备是否注册成功,可快速定位“设备找不到”还是“函数指针为空”两类问题。

六、实现检查清单

接口必须完成常见错误
iosDevAddDEV_HDR 置于结构体首位,设备名唯一结构体首成员不是 DEV_HDR
iosDrvInstall先安装驱动表,再注册设备顺序颠倒导致 open 失败
open建立上下文、互斥占用、返回句柄未做占用检查,重复打开
close释放资源、递减计数、释放信号量只减计数不释放内存
read/write返回实际字节数,设置超时返回 OK/ERROR 而非字节数
ioctl命令码唯一,未知命令返回 ERROR静默忽略未知命令

把上述六项落实到位,字符设备驱动就具备了可被用户程序稳定调用的基本形态。后续无论是增加中断驱动、DMA 搬运还是多通道扩展,都可以在这个框架上增量演进,而不必推翻设备节点与文件操作接口的既有结构。