驱动开发基础:驱动框架介绍、PCI设备枚举与配置、中断处理机制、DMA传输原理
各位同学,今天我们来啃一块硬骨头——驱动开发的基础。说实话,很多做应用层开发的兄弟一听到「驱动」两个字就头大,觉得那是内核开发者的专利。其实不然,驱动说白了就是让操作系统认识你的硬件,并告诉它怎么用。我在QNX上折腾驱动也有十来年了,踩过的坑比走过的路还多。今天就把这些经验掰开揉碎了讲给你听。
一、QNX驱动框架介绍
QNX的驱动模型和Linux不太一样。它采用的是资源管理器(Resource Manager)架构。什么意思呢?就是每个驱动都是一个独立的进程,通过消息传递和内核通信。你想想看,这样做的好处是什么?
- 隔离性:驱动挂了,不会把整个系统拖死。我遇到过好几次,驱动崩了,重启一下驱动进程就行,系统纹丝不动。
- 可调试性:驱动进程可以像普通程序一样gdb调试。这在Linux上简直是奢望。
- 热插拔:驱动可以动态加载卸载,不用重启系统。
驱动的基本结构是这样的:
int main(int argc, char **argv) {
// 1. 初始化资源管理器
dispatch_t *dpp = dispatch_create();
// 2. 注册设备路径
resmgr_attr_t resmgr_attr;
memset(&resmgr_attr, 0, sizeof(resmgr_attr));
// 3. 绑定IO函数
resmgr_connect_funcs_t connect_funcs;
resmgr_io_funcs_t io_funcs;
memset(&connect_funcs, 0, sizeof(connect_funcs));
memset(&io_funcs, 0, sizeof(io_funcs));
io_funcs.read = my_read;
io_funcs.write = my_write;
io_funcs.devctl = my_devctl;
// 4. 注册设备
resmgr_attach(dpp, &resmgr_attr, "/dev/mydevice",
_FTYPE_ANY, 0, &connect_funcs, &io_funcs, NULL);
// 5. 进入消息循环
dispatch_block(dpp);
return 0;
}
嗯,这里要注意:dispatch_create() 必须在最前面调用,否则后面所有注册都会失败。我曾经在这个坑里爬了整整一个下午。
二、PCI设备枚举与配置
PCI设备枚举,说白了就是让系统找到你的硬件。QNX提供了 pci_* 系列API,用起来还算顺手。
我个人习惯的枚举流程是这样的:
- 调用
pci_attach()初始化PCI子系统 - 遍历总线、设备、功能号
- 读取Vendor ID和Device ID,匹配你的设备
- 读取BAR空间地址,映射到虚拟地址空间
- 配置中断和DMA
来看一个实际的枚举代码:
int find_pci_device(uint16_t vendor_id, uint16_t device_id) {
pci_attach(0); // 初始化PCI子系统
for (int bus = 0; bus < 256; bus++) {
for (int dev = 0; dev < 32; dev++) {
for (int func = 0; func < 8; func++) {
uint16_t vid, did;
pci_read_config16(bus, dev, func, PCI_VENDOR_ID, &vid);
pci_read_config16(bus, dev, func, PCI_DEVICE_ID, &did);
if (vid == vendor_id && did == device_id) {
printf("找到设备: bus=%d, dev=%d, func=%d\n",
bus, dev, func);
return (bus << 16) | (dev << 8) | func;
}
}
}
}
return -1; // 没找到
}
配置BAR空间时,我最常用的做法是:
uint64_t bar_addr;
uint32_t bar_size;
// 读取BAR0
pci_read_config32(bus, dev, func, PCI_BAR0, &bar_addr);
// 计算BAR大小
pci_write_config32(bus, dev, func, PCI_BAR0, 0xFFFFFFFF);
pci_read_config32(bus, dev, func, PCI_BAR0, &bar_size);
bar_size = ~(bar_size & 0xFFFFFFF0) + 1;
// 恢复原始值
pci_write_config32(bus, dev, func, PCI_BAR0, bar_addr);
// 映射到虚拟地址
uintptr_t va = mmap_device_io(bar_size, bar_addr);
if (va == MAP_DEVICE_FAILED) {
perror("mmap_device_io failed");
return -1;
}
mmap_device_io() 和 mmap_device_memory() 是有区别的。前者用于IO空间,后者用于内存空间。搞混了会导致系统崩溃,我亲眼见过同事因为这个把开发板搞死机了。
三、中断处理机制
中断处理是驱动开发中最容易出问题的环节。QNX的中断处理流程是这样的:
- 注册中断处理函数
- 中断到来时,内核调用处理函数
- 处理函数在中断上下文中运行,不能阻塞
- 如果需要做耗时操作,用
InterruptAttachEvent()配合事件通知
来看一个典型的中断注册代码:
int irq = pci_get_irq(bus, dev, func); // 获取中断号
// 注册中断处理函数
int ret = InterruptAttach(irq, my_isr, NULL, 0, 0);
if (ret != 0) {
perror("InterruptAttach failed");
return -1;
}
// 中断处理函数
const struct sigevent *my_isr(void *arg, int id) {
// 读取中断状态寄存器
uint32_t status = in32(INT_STATUS_REG);
// 清除中断
out32(INT_CLEAR_REG, status);
// 返回事件通知
return NULL; // 或者返回事件对象
}
为什么会这样设计?因为中断处理函数必须快进快出。我见过有人在中断里做内存分配,结果系统直接死锁。记住:中断里只做最必要的事。
如果你需要做复杂处理,可以用 InterruptAttachEvent():
struct sigevent event;
SIGEV_PULSE_INIT(&event, connect_id, priority, code, value);
InterruptAttachEvent(irq, &event, 0);
// 在消息循环中接收脉冲
while (1) {
struct _pulse pulse;
MsgReceivePulse(chid, &pulse, sizeof(pulse), NULL);
// 处理中断事件
}
printf()、malloc()、mutex_lock() 等。我曾经在调试时加了个 printf(),结果系统直接卡死,查了三天才发现。
四、DMA传输原理
DMA(直接内存访问)是高性能驱动的必备技能。说白了,就是让硬件自己搬数据,CPU在旁边喝茶。
QNX的DMA编程主要涉及这几个步骤:
- 分配DMA缓冲区(物理连续)
- 设置DMA描述符
- 启动DMA传输
- 等待传输完成(中断或轮询)
分配DMA缓冲区时,一定要用 mmap_device_memory() 或者 posix_memalign() 配合 mlock():
// 分配DMA缓冲区
void *dma_buf = mmap_device_memory(NULL, BUF_SIZE,
PROT_READ | PROT_WRITE, 0,
phys_addr);
if (dma_buf == MAP_DEVICE_FAILED) {
perror("mmap_device_memory failed");
return -1;
}
// 或者用posix_memalign
void *buf;
posix_memalign(&buf, 4096, BUF_SIZE); // 4K对齐
mlock(buf, BUF_SIZE); // 锁定物理内存
设置DMA描述符时,要注意字节对齐和地址边界。很多DMA控制器要求源地址和目的地址按4字节或8字节对齐:
typedef struct {
uint32_t src_addr; // 源地址
uint32_t dst_addr; // 目的地址
uint32_t length; // 传输长度
uint32_t control; // 控制位
} dma_descriptor_t;
dma_descriptor_t desc;
desc.src_addr = (uint32_t)src_phys;
desc.dst_addr = (uint32_t)dst_phys;
desc.length = transfer_size;
desc.control = DMA_CTRL_IE | DMA_CTRL_GO; // 使能中断,启动传输
启动DMA传输后,可以通过中断或轮询来等待完成:
// 中断方式
// 在中断处理函数中检查DMA完成标志
// 轮询方式
while (in32(DMA_STATUS_REG) & DMA_BUSY) {
// 等待,但不要空转
nanospin_ns(100); // 100纳秒的延迟
}
嗯,这里要提醒一下:轮询方式虽然简单,但会占用CPU。如果传输频繁,建议用中断方式。我在一个视频采集驱动里用过轮询,结果CPU占用率直接飙到80%,后来改成中断方式,降到了5%以下。
总结
驱动开发其实没那么神秘。你只要掌握了资源管理器框架、PCI枚举、中断处理和DMA传输这四板斧,大部分设备驱动都能搞定。记住:
- 驱动是进程,不是内核模块
- PCI枚举要遍历所有总线设备功能
- 中断处理要快,别在里面干重活
- DMA缓冲区要物理连续,注意对齐
下一章我们会讲具体的调试工具使用,到时候我会带大家实战分析一个真实的驱动问题。今天就到这里,有问题随时问我。