上周调一块工控板,系统启动后网卡死活不认。查了半天发现是I/O空间冲突------新加的FPGA逻辑占用了网卡的端口范围。这种问题在嵌入式开发里太常见了,今天咱们就聊聊Linux驱动里怎么管好这些硬件资源。
从一次真实踩坑说起
那天看到内核日志里连续打印"eth0: probe failed",第一反应是硬件问题。用逻辑分析仪抓PCIe配置周期,发现网卡BAR空间分配正常。接着查I/O访问,才发现问题出在request_region失败。原来同事在FPGA驱动里写死了0x1000-0x1FFF的端口范围,正好跟网卡的I/O窗口重叠。内核可不会主动告诉你"这两个驱动在抢地盘",它只是默默让后加载的驱动申请失败。
I/O端口:老派但必须懂
x86架构保留着独立的I/O空间,用专门的in/out指令访问。虽然现在很多外设都走内存映射了,但串口、GPIO控制器这些老伙计还在用端口。
c
/* 正确姿势:先申请再使用 */
if (!request_region(0x3F8, 8, "my_serial")) {
printk("端口被占用了!查查谁干的\n"); // 这里踩过坑,不申请直接用的驱动都是耍流氓
return -EBUSY;
}
/* 操作端口 */
outb(0x80, 0x3F8); // 设置波特率
val = inb(0x3F8 + 5); // 读线路状态
/* 用完记得还 */
release_region(0x3F8, 8);
/proc/ioports文件能看到系统里所有登记的端口范围。调试时先看这个,能省两小时。曾经有个项目,USB控制器突然不工作了,最后发现是有人把DMA控制器的端口范围改大了4个字节,刚好覆盖了USB的配置寄存器。
内存映射:现代外设的主流选择
内存映射I/O把外设寄存器映射到物理内存空间,CPU用普通访存指令就能操作。ARM、PowerPC这些架构压根没有独立的I/O空间,全靠MMIO。
c
/* 映射物理地址到内核虚拟地址 */
void __iomem *regs = ioremap(0xFE000000, 0x1000);
if (!regs) {
printk("映射失败,检查地址是不是太野了\n");
return -ENOMEM;
}
/* 用专用接口读写 */
u32 val = ioread32(regs + 0x10);
iowrite32(0x12345678, regs + 0x20);
/* 带屏障的版本,确保执行顺序 */
iowrite32_rep(regs, buffer, count);
/* 取消映射 */
iounmap(regs);
这里有个细节:ioremap返回的是void __iomem *类型,编译器会阻止你直接用指针解引用。必须用ioread32这类接口,为什么?因为有些架构(比如Alpha)的I/O空间有特殊的内存序要求,而且PCI设备可能有写合并限制。我见过有人用*(u32 *)regs直接操作,在x86上跑得好好的,一到PowerPC就数据错乱。
资源管理框架
内核提供了统一的资源管理机制,把端口和内存映射都抽象成struct resource:
c
static struct resource my_dev_resources[] = {
[0] = {
.start = 0xFE000000,
.end = 0xFE000FFF,
.name = "dev_regs",
.flags = IORESOURCE_MEM, // 内存资源
},
[1] = {
.start = 0x1000,
.end = 0x1007,
.name = "dev_io",
.flags = IORESOURCE_IO, // 端口资源
}
};
/* 在probe函数里申请 */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res); // 这个带错误处理和自动释放
if (IS_ERR(base))
return PTR_ERR(base);
用devm_开头的托管函数能自动释放资源,驱动卸载或probe失败时不用手动清理。但注意:devm_ioremap_resource会自己申请资源,别在前面再调request_region。
调试技巧
cat /proc/iomem和cat /proc/ioports是首选工具,一眼看出资源分配情况- 用
/sys/kernel/debug/ioports能看到更详细的占用者信息 - 怀疑资源冲突时,临时注释掉可疑驱动的
request_region,看问题是否消失 - 对于PCI设备,
lspci -vv显示的BAR空间信息比内核日志更直观
去年调一个四网口板卡,第三个网口时好时坏。最后发现是第二个网卡驱动在remove函数里漏了iounmap,内核重启后物理地址被重复映射,两个虚拟地址操作同一套寄存器,时序全乱了。
个人经验
-
永远别写死资源地址,用设备树或ACPI传递。我们有个项目换了芯片型号,地址偏移变了,改设备树比重新编译驱动方便多了
-
先申请后使用,哪怕你知道这个地址肯定没人用。有个客户在驱动里偷懒没申请,后来系统升级加了个看门狗驱动,正好地址重叠,设备半年才死一次机,查了三个月
-
MMIO访问用屏障 ,特别是多核处理器。曾经在ARM Cortex-A9上遇到寄存器配置不生效,加了
wmb()就好了 -
释放资源要配对 ,
ioremap对iounmap,request_region对release_region。最好用devm_托管,让内核帮你记着 -
查冲突从启动日志看起,内核启动时打印的资源分配信息很有用,特别是那句"resource conflict"
资源管理就像开车系安全带,平时觉得麻烦,出事时能救命。好的驱动代码不仅要功能正确,还要和其他驱动和睦相处。毕竟内核是大家的,不能只顾自己跑得欢。