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 时会因找不到驱动表而失败。
三、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,不要静默忽略;对指针参数要做有效性检查;涉及硬件状态切换的命令,应在切换前后加必要的延时或状态确认。这样用户程序才能通过返回值判断控制是否真正生效。
iosDevShow() 查看设备是否注册成功,可快速定位“设备找不到”还是“函数指针为空”两类问题。六、实现检查清单
| 接口 | 必须完成 | 常见错误 |
|---|---|---|
| iosDevAdd | DEV_HDR 置于结构体首位,设备名唯一 | 结构体首成员不是 DEV_HDR |
| iosDrvInstall | 先安装驱动表,再注册设备 | 顺序颠倒导致 open 失败 |
| open | 建立上下文、互斥占用、返回句柄 | 未做占用检查,重复打开 |
| close | 释放资源、递减计数、释放信号量 | 只减计数不释放内存 |
| read/write | 返回实际字节数,设置超时 | 返回 OK/ERROR 而非字节数 |
| ioctl | 命令码唯一,未知命令返回 ERROR | 静默忽略未知命令 |
把上述六项落实到位,字符设备驱动就具备了可被用户程序稳定调用的基本形态。后续无论是增加中断驱动、DMA 搬运还是多通道扩展,都可以在这个框架上增量演进,而不必推翻设备节点与文件操作接口的既有结构。