4. 字符设备驱动框架与第一个驱动
在 INTEGRITY 实时操作系统上开发字符设备驱动,核心工作是围绕 DEV_DRIVER 结构体注册一组入口函数,让内核在应用层调用 open、close、read、write 时能够路由到驱动实现。与 Linux 的 file_operations 不同,INTEGRITY 使用 DEV_DRIVER 与 DEV_DEV 两层结构:前者描述驱动能力(函数指针表),后者描述具体设备实例(设备名、句柄、私有数据)。理解这两层关系,是写出最小可用驱动的第一步。
一、驱动框架的两层结构
INTEGRITY 的字符设备模型可以概括为“驱动注册 + 设备实例化”。DEV_DRIVER 是驱动级对象,包含 open、close、read、write、ioctl 等函数指针,以及驱动名和标志位;DEV_DEV 是设备级对象,包含设备名、指向所属 DEV_DRIVER 的指针、设备句柄和私有数据指针。应用通过设备名打开设备时,内核先找到 DEV_DEV,再通过其 driver 字段找到 DEV_DRIVER,最后调用对应的函数指针。
这种分层带来的直接好处是:同一份驱动代码可以实例化多个设备,每个设备拥有独立的私有数据,而函数实现完全复用。对于最小驱动,我们只需要一个驱动对象和一个设备对象。
二、最小驱动的实现步骤
下面以 INTEGRITY 的 ghs 工具链为例,给出一个最小字符设备驱动的骨架。该驱动不操作真实硬件,而是维护一个 256 字节的内核缓冲区,用于演示 read 与 write 的数据通路。
第一步:定义私有数据结构。每个设备实例需要保存自己的状态。对于最小驱动,私有数据只需一个缓冲区、一个写指针和一个读指针。
typedef struct {
char buffer[256];
uint32_t write_pos;
uint32_t read_pos;
uint32_t count;
} MyDevPrivate;
static MyDevPrivate g_mydev_priv;
第二步:实现四个入口函数。函数签名必须与 DEV_DRIVER 中的函数指针类型一致。INTEGRITY 的 open 返回 Error 类型,read/write 返回实际传输的字节数,close 返回 Error。
static Error MyDev_Open(DEV_DEV *dev, const char *name, int mode, void **pCookie)
{
MyDevPrivate *priv = (MyDevPrivate *)dev->privateData;
priv->read_pos = 0;
priv->write_pos = 0;
priv->count = 0;
*pCookie = priv;
return NO_ERROR;
}
static Error MyDev_Close(DEV_DEV *dev, void *cookie)
{
(void)dev; (void)cookie;
return NO_ERROR;
}
static int MyDev_Read(DEV_DEV *dev, void *cookie, char *buf, int len)
{
MyDevPrivate *priv = (MyDevPrivate *)cookie;
int n = (len < (int)priv->count) ? len : (int)priv->count;
for (int i = 0; i < n; i++) {
buf[i] = priv->buffer[priv->read_pos];
priv->read_pos = (priv->read_pos + 1) % sizeof(priv->buffer);
}
priv->count -= n;
return n;
}
static int MyDev_Write(DEV_DEV *dev, void *cookie, const char *buf, int len)
{
MyDevPrivate *priv = (MyDevPrivate *)cookie;
int n = (len < (int)(sizeof(priv->buffer) - priv->count))
? len : (int)(sizeof(priv->buffer) - priv->count);
for (int i = 0; i < n; i++) {
priv->buffer[priv->write_pos] = buf[i];
priv->write_pos = (priv->write_pos + 1) % sizeof(priv->buffer);
}
priv->count += n;
return n;
}
第三步:填充 DEV_DRIVER 并注册。将四个函数指针填入 DEV_DRIVER 结构体,调用 CreateDevice 或 RegisterDevice 完成注册。注册时需指定设备名,应用层通过该名称打开设备。
static DEV_DRIVER MyDev_Driver = {
.driverName = "MyCharDev",
.open = MyDev_Open,
.close = MyDev_Close,
.read = MyDev_Read,
.write = MyDev_Write,
.ioctl = NULL,
.flags = 0
};
static DEV_DEV MyDev_Device = {
.name = "/dev/mydev",
.driver = &MyDev_Driver,
.privateData = &g_mydev_priv
};
Error MyDev_Init(void)
{
return RegisterDevice(&MyDev_Device);
}
三、加载到 INTEGRITY 目标系统验证
驱动编译为 .a 或 .o 后,需要链接进 INTEGRITY 内核镜像或作为可加载模块。典型流程是:在 integrity.gpj 工程文件中加入驱动源文件,重新构建内核,然后通过 MULTI 调试器或串口终端启动目标板。
验证时,在应用任务中调用 open("/dev/mydev", ...),若返回有效文件描述符,说明 DEV_DEV 查找和 open 入口均正常。随后写入一段字符串,再读回并比较内容。以下是一个最小验证任务:
void TestTask(void)
{
int fd = open("/dev/mydev", O_RDWR);
if (fd < 0) { printf("open failed\n"); return; }
const char *msg = "INTEGRITY";
int w = write(fd, msg, 9);
printf("write %d bytes\n", w);
char buf[16] = {0};
int r = read(fd, buf, 9);
printf("read %d bytes: %s\n", r, buf);
close(fd);
}
预期输出为 write 9 bytes 和 read 9 bytes: INTEGRITY。若 open 失败,优先检查设备名是否与注册名一致、RegisterDevice 是否在应用启动前调用;若 read 返回 0,检查 write 是否成功以及 count 是否被正确更新。
open 入口中 pCookie 是输出参数,用于把私有数据指针传递给后续 read/write。若忘记赋值,后续函数将收到空指针,导致异常。建议在 open 中先校验 dev->privateData 非空。
四、常见问题与排查
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| open 返回错误 | 设备名不匹配或未注册 | 核对 DEV_DEV.name 与调用名;确认 RegisterDevice 返回值 |
| read 返回 0 | 缓冲区为空或 count 未更新 | 检查 write 返回值;在 write 后打印 count |
| write 返回 0 | 缓冲区已满 | 检查 count 是否等于 256;先读后写 |
| 系统挂起 | 函数指针为空或签名不匹配 | 确认 DEV_DRIVER 中所有指针已赋值;核对参数类型 |