x86平台串口IO端口0x2f8能否mmap到用户态?求实现步骤
可以在x86平台下将UART IO端口mmap到用户态,但你的实现存在几个关键错误
先明确:x86的IO端口(比如传统UART的0x2f8)属于独立的IO地址空间,不是物理内存空间,所以你当前用virt_to_phys处理ioport_map返回的地址是完全错误的——这也是你得到诡异物理地址0x6587800102f8的原因。下面是问题分析和正确实现步骤:
你的代码中的核心错误
- 错误的物理地址计算:
ioport_map返回的是内核虚拟地址,对应IO端口空间而非物理内存,virt_to_phys仅适用于映射到物理内存的内核虚拟地址,对IO空间地址无效,所以得到的物理地址完全不可用。 - 用户态访问地址错误:你在用户态中尝试访问
address[0x2f8 + i],但mmap返回的address是映射区域的基地址,若你映射的是0x2f8开始的端口,正确的访问方式应该是address[i]对应0x2f8+i的端口;如果映射的是整个IO页(0x0000-0x0FFF),则应该是*(unsigned char*)(address + 0x2f8 + i)。 - 未正确设置VMA属性:你没有添加
VM_PFNMAP标志,这对于映射非物理内存的IO空间是必须的。
正确的实现步骤
1. 驱动端mmap函数修正
针对IO端口的mmap,核心是利用x86的IO空间映射机制,正确设置VMA属性并使用ioport_to_pfn获取IO端口对应的页帧号(PFN),而非错误的virt_to_phys。以下是修正后的mmap函数:
static int my_dev_mmap(struct file *filep, struct vm_area_struct *vma) { SERIAL_DEV *my_dev = (SERIAL_DEV*)filep->private_data; unsigned long base_port = my_dev->base_addr; // 0x2f8 size_t map_size = vma->vm_end - vma->vm_start; unsigned long pfn; unsigned long port_page = base_port & PAGE_MASK; // 获取端口所在的IO页起始地址(0x0000) // 限制映射大小为包含目标端口的整页,或你需要的8字节 if (map_size != PAGE_SIZE && map_size != 8) { printk("GNA: Invalid map size\n"); return -EINVAL; } // 设置VMA属性:VM_IO标记IO空间,VM_PFNMAP表示映射的是PFN而非物理内存,禁用扩展和转储 vma->vm_flags |= VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP; // 设置非缓存属性,IO端口访问不能缓存 vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); // 获取IO页对应的PFN pfn = ioport_to_pfn(port_page); // 映射整个IO页到用户态 if (remap_pfn_range(vma, vma->vm_start, pfn, PAGE_SIZE, vma->vm_page_prot)) { printk("GNA: remap_pfn_range failed\n"); return -EAGAIN; } printk("GNA: mmap success, user base: 0x%lx, IO page port: 0x%lx\n", vma->vm_start, port_page); return 0; }
2. 用户态代码修正
如果映射的是整个IO页(0x0000-0x0FFF),用户态需要通过偏移0x2f8来访问目标UART端口,并且要先写入scratch pad寄存器再读取(该寄存器是可读写的,默认值不确定):
#include <stdio.h> #include <sys/mman.h> #include <fcntl.h> #include <unistd.h> #include <stdint.h> #define PAGE_SIZE 4096 #define UART_BASE_PORT 0x2F8 #define SCRATCH_PAD_OFFSET 7 int main() { int configfd = open("/dev/custom_serial_device", O_RDWR); if (configfd < 0) { perror("open failed"); return 1; } uint8_t *address = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, configfd, 0); if (address == MAP_FAILED) { perror("mmap failed"); close(configfd); return 1; } // 获取scratch pad寄存器的用户态地址 uint8_t *scratch_reg = address + UART_BASE_PORT + SCRATCH_PAD_OFFSET; // 先写入0x5A到scratch pad寄存器 *scratch_reg = 0x5A; // 读取验证 uint8_t val = *scratch_reg; printf("Scratch pad value: 0x%x\n", val); // 也可以遍历测试端口 for (int i = 0; i < 8; i++) { uint8_t *port = address + UART_BASE_PORT + i; printf("Port 0x%x value: 0x%x\n", UART_BASE_PORT + i, *port); } munmap(address, PAGE_SIZE); close(configfd); return 0; }
3. 额外注意事项
- 串口配置验证:确保你已经正确配置了UART的软件回环模式,并且scratch pad寄存器是可访问的(有些UART硬件可能不支持该寄存器,需要确认你的硬件规格)。
- 权限问题:驱动需要正确设置设备文件的权限(比如
chmod 666 /dev/custom_serial_device),否则用户态程序可能无法打开或mmap设备。 - 内核版本兼容性:上述代码基于主流Linux内核(5.x+),不同版本的内核API可能略有差异,需根据你的内核版本调整。
内容的提问来源于stack exchange,提问作者Haswell
相关产品推荐
相关产品推荐

