在 Linux 设备驱动开发中,I/O 端口和I/O 内存是两种与硬件设备通信的核心机制。它们对应不同的硬件访问方式,适用于不同的设备类型(如传统 ISA 设备或现代 PCI 设备)。以下是两者的详细对比及使用方法。
1. I/O 端口(Port I/O)
基本概念
- 适用场景:传统 ISA 设备或需要直接通过端口号访问的硬件(如串口、PS/2 控制器)。
- 特点:
- 通过独立的 I/O 地址空间访问(x86 架构的
in
/out
指令)。 - 地址范围通常较小(0x0000 ~ 0xFFFF)。
- 需要申请端口资源,防止冲突。
- 通过独立的 I/O 地址空间访问(x86 架构的
操作流程
-
申请端口资源:
#include <linux/ioport.h> struct resource *request_region(unsigned long start, unsigned long len, const char *name);
- 示例:
if (!request_region(0x3F8, 8, "my_serial")) { printk(KERN_ERR "Failed to request I/O ports\n"); return -EBUSY; }
- 示例:
-
端口读写函数:
- 8 位操作:
unsigned char inb(unsigned int port); void outb(unsigned char value, unsigned int port);
- 16 位操作:
unsigned short inw(unsigned int port); void outw(unsigned short value, unsigned int port);
- 32 位操作:
unsigned int inl(unsigned int port); void outl(unsigned int value, unsigned int port);
- 8 位操作:
-
释放端口资源:
void release_region(unsigned long start, unsigned long len);
代码示例
// 向串口端口 0x3F8 写入数据
outb('A', 0x3F8);
// 从端口 0x3F8 读取数据
unsigned char data = inb(0x3F8);
2. I/O 内存(Memory-Mapped I/O)
基本概念
- 适用场景:现代 PCI 设备、SoC 外设等,通过将硬件寄存器映射到内存地址空间访问。
- 特点:
- 硬件寄存器映射到系统内存地址空间。
- 支持大范围地址访问。
- 需要映射到内核虚拟地址才能访问。
操作流程
-
申请内存资源:
struct resource *request_mem_region(resource_size_t start, resource_size_t len, const char *name);
-
映射到内核虚拟地址:
void __iomem *ioremap(phys_addr_t offset, size_t size);
-
访问内存映射寄存器:
- 使用内核提供的访问函数(避免直接指针操作):
- 8 位操作:
unsigned char ioread8(const void __iomem *addr); void iowrite8(u8 value, void __iomem *addr);
- 16 位操作:
unsigned short ioread16(const void __iomem *addr); void iowrite16(u16 value, void __iomem *addr);
- 32 位操作:
unsigned int ioread32(const void __iomem *addr); void iowrite32(u32 value, void __iomem *addr);
- 8 位操作:
- 使用内核提供的访问函数(避免直接指针操作):
-
解除映射并释放资源:
void iounmap(void __iomem *addr); void release_mem_region(resource_size_t start, resource_size_t len);
代码示例
// 1. 申请内存资源(假设设备寄存器物理地址为 0xFED00000)
if (!request_mem_region(0xFED00000, 0x100, "my_device")) {
return -EBUSY;
}
// 2. 映射到内核虚拟地址
void __iomem *regs = ioremap(0xFED00000, 0x100);
if (!regs) {
release_mem_region(0xFED00000, 0x100);
return -ENOMEM;
}
// 3. 读写寄存器
u32 value = ioread32(regs + 0x10); // 读取偏移 0x10 处的 32 位寄存器
iowrite32(0x12345678, regs + 0x10); // 写入数据
// 4. 清理
iounmap(regs);
release_mem_region(0xFED00000, 0x100);
3. 对比与选择
特性 | I/O 端口 | I/O 内存 |
---|---|---|
地址空间 | 独立的 I/O 地址空间 | 系统内存地址空间 |
适用设备 | 传统 ISA 设备 | 现代 PCI/PCIe、SoC 外设 |
操作函数 | inb() /outb() 等 | ioread8() /iowrite8() 等 |
性能 | 较低(需通过端口指令) | 较高(直接内存访问) |
跨平台兼容性 | x86 架构为主 | 通用(ARM、x86 等均支持) |
选择原则:
- 优先使用 I/O 内存(现代设备普遍支持)。
- 仅在传统设备(如旧 ISA 设备)必须时使用 I/O 端口。
4. 高级用法
(1) 内存屏障(Memory Barriers)
确保硬件寄存器访问顺序:
iowrite32(0x55AA, regs + CTRL_REG); // 写入控制寄存器
mb(); // 内存屏障,确保写入完成
(2) 直接指针访问(慎用)
仅在内核明确允许时使用:
u32 __iomem *reg = (u32 __iomem *)regs;
writel(0x12345678, reg + 0x10); // 等效于 iowrite32()
(3) 用户空间映射(mmap
)
允许用户空间直接访问设备内存:
// 驱动中的 mmap 实现
static int mydev_mmap(struct file *filp, struct vm_area_struct *vma) {
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long pfn = virt_to_phys(regs_base + offset) >> PAGE_SHIFT;
return remap_pfn_range(vma, vma->vm_start, pfn, vma->vm_end - vma->vm_start, vma->vm_page_prot);
}
5. 调试与工具
(1) 查看资源分配
- I/O 端口:
cat /proc/ioports
- I/O 内存:
cat /proc/iomem
(2) 直接读写工具
- I/O 端口:
sudo setpci -s 00:02.0 0x10.L=0x12345678 # 修改 PCI 配置空间
- I/O 内存:
sudo busybox devmem 0xFED00000 32 # 读取物理地址 0xFED00000 的 32 位值
6. 常见问题
(1) 资源冲突
- 表现:
request_region
或request_mem_region
失败(返回EBUSY
)。 - 解决:检查
/proc/ioports
和/proc/iomem
,确保地址未被占用。
(2) 地址映射失败
- 原因:物理地址未正确映射或超出范围。
- 验证:使用
iounmap
前检查ioremap
返回值是否为NULL
。
(3) 字节序问题
- 注意:某些设备使用大端字节序,需使用
__raw_readl()
或字节序转换函数:u32 value = __raw_readl(regs); // 直接读取,不进行字节序转换
总结
- I/O 端口:通过
in
/out
指令操作独立地址空间,适用于传统设备。 - I/O 内存:将硬件寄存器映射到内存地址空间,通过
ioread
/iowrite
访问,适用于现代设备。 - 核心步骤:资源申请 → 地址映射 → 安全访问 → 清理释放。
- 调试工具:
/proc/ioports
、/proc/iomem
、devmem
。
正确使用 I/O 端口和 I/O 内存,是驱动与硬件交互的基础能力。
参考: