§1.1 ioremap:寄存器为什么能 readl / writel
平台:QEMU virt,UART 物理基址常见 0x09000000,RAM 从 0x40000000 起。
0. 这条要明白什么
CPU 取指、load/store 用的是 虚拟地址 。设备树 reg 给的是 物理地址。两者不是一回事。
所以驱动不能写 *(u32 *)0x09000000。必须先在内核页表里开一扇窗:虚拟地址 → 这块 MMIO,属性是 Device / 非缓存。readl/writel 打的是这扇窗,MMU 再翻译到物理寄存器。
DT / 手册:UART @ 0x09000000 物理地址
ioremap 在页表里开窗
返回 void __iomem * 内核虚拟地址
readl/writel(base + offset) 访存指令打虚拟地址
MMU → 0x09000000 硬件寄存器
没有这扇窗,readl 没有合法的内核地址可用。这就是清单那句「寄存器为什么能 readl/writel」。
1. 两份 ioremap.c 各干什么
ARM64 选了 GENERIC_IOREMAP。驱动看到的 ioremap() 是头文件里的 inline。
| 文件 | 函数 | 角色 |
|---|---|---|
include/asm-generic/io.h |
ioremap() |
皮:补上默认 prot = _PAGE_IOREMAP |
arch/arm64/mm/ioremap.c |
ioremap_prot() |
架构门禁(地址宽度、禁止映射 RAM) |
mm/ioremap.c |
generic_ioremap_prot() |
占虚拟窗口 + 填页表 |
mm/vmalloc.c |
__get_vm_area_caller / ioremap_page_range |
被通用层调用;看到职责即可,不跟 PGD 循环 |
arch/arm64/include/asm/io.h:_PAGE_IOREMAP = PROT_DEVICE_nGnRE(设备内存、非缓存、写可 posted)。变体:ioremap_wc → PROT_NORMAL_NC,ioremap_np → PROT_DEVICE_nGnRnE。
正规驱动优先 devm_ioremap_resource() / devm_platform_ioremap_resource(),最后仍进 ioremap_prot。virt 上 PL011:amba-pl011.c 里 devm_ioremap_resource(dev, mmiobase)。
2. 调用链(probe 之后)
probe
devm_ioremap_resource() / ioremap(phys, size)
include/asm-generic/io.h
ioremap_prot(phys, size, _PAGE_IOREMAP)
arch/arm64/mm/ioremap.c ★ 门禁
generic_ioremap_prot() ★ mm/ioremap.c
1. __get_vm_area_caller() 占 VA(页表还空)
2. ioremap_page_range() 填 phys → vaddr
返回 (vaddr + 页内 offset)
readl(base + 0x18) 下一条 io.h 才展开
ioremap() 本体:
c
/* include/asm-generic/io.h,CONFIG_GENERIC_IOREMAP */
static inline void __iomem *ioremap(phys_addr_t addr, size_t size)
{
return ioremap_prot(addr, size, _PAGE_IOREMAP);
}
3. 架构门禁:ioremap_prot
arch/arm64/mm/ioremap.c 自己不建页表,只拦非法请求。
-
last_addr & ~PHYS_MASK超出芯片物理地址宽度(
PHYS_MASK,跟CONFIG_ARM64_PA_BITS)。无法映射,返回NULL。 -
pfn_is_map_memory()这段已经是内核线性映射里的 RAM。禁止再 ioremap 成 Device 属性(别名、缓存错乱)。会
WARN_ON。virt:RAM
@0x40000000,UART@0x09000000,后者不是 memory,能过。
通过后:generic_ioremap_prot(phys, size, __pgprot(prot))。
同文件 early_ioremap_init() 是 另一条路 :setup_arch 里 early_fixmap_init 之后调用,给启动早期用。DTB 的 fixmap_remap_fdt 走 fixmap,不是本函数。
4. 真正开窗:generic_ioremap_prot
mm/ioremap.c。依赖 slab 已可用 (vmalloc 要分配 vm_struct)。!slab_is_available() 则 WARN 并返回 NULL------早期平台代码误闯到这里会失败,应走 early_ioremap。
4.1 页对齐
页表只能按页映射。若物理地址不是页头:
offset = phys_addr & ~PAGE_MASK 页内偏移
phys_addr -= offset 从页头开始映射
size = PAGE_ALIGN(size + offset) 覆盖到请求末尾
返回时加回 offset,驱动拿到的仍是「寄存器那个字节」,不是页头。virt 上 0x09000000 通常已页对齐,offset 为 0。
长度为 0,或 phys + size 溢出绕回,直接 NULL。
4.2 两步
第一步:占虚拟地址(还不译地址)
__get_vm_area_caller(size, VM_IOREMAP, IOREMAP_START, IOREMAP_END, caller)
mm/vmalloc.c:3139
→ __get_vm_area_node() :3090
kzalloc(struct vm_struct)
alloc_vmap_area() 在 [start, end) 抠一段 VA
ARM64 未另定义 IOREMAP_START,就是 vmalloc 区(VMALLOC_START ... VMALLOC_END)。VM_IOREMAP 只是标记这块给 ioremap 用。此时 load 这个 vaddr 仍会出错。
alloc_vmap_area 内部红黑树不必跟。
第二步:填页表
ioremap_page_range(vaddr, vaddr+size, phys_addr, prot)
mm/vmalloc.c:329
find_vm_area() 必须是刚占的那块 VM_IOREMAP
→ vmap_page_range() :315
→ vmap_range_noflush() :286 ★ 写 PGD...PTE
flush_cache_vmap()
vmap_range_noflush() 把 phys_addr 按 prot 写进内核页表。再往下 vmap_p4d_range / set_pte 是同一件事的分层,看到 286 行停。
失败则 free_vm_area,返回 NULL。
成功:(void __iomem *)(vaddr + offset)。之后 readl 用的就是这个指针。
5. 用 virt 的 UART 对号
物理 0x09000000(DT uart0.reg)
→ ioremap_prot 门禁通过(不是 RAM)
→ vmalloc 区某个 vaddr(例如 ffff8000xxxxxxxx,具体随运行变化)
→ PTE:vaddr → 0x09000000,PROT_DEVICE_nGnRE
→ 驱动 pl011->base = vaddr
→ readl(base + UARTDR) 访问虚拟地址,MMU 打到 UART 数据寄存器
gdb:在 PL011 probe 的 devm_ioremap_resource 返回后看指针;再断 generic_ioremap_prot 看 phys_addr == 0x09000000。-s -S + nokaslr 方便对地址。
6. 生命周期、失败、和早期映射
| 正规 ioremap | 早期 early_ioremap / fixmap | |
|---|---|---|
| 何时 | slab / vmalloc 起来之后(驱动 probe) |
start_kernel 很早(扫 DTB、earlycon) |
| 窗口 | vmalloc 区,按需分配 | 固定的 fixmap 槽,数量有限 |
| 释放 | iounmap / devm_* 设备卸载时自动 |
用完要 early_iounmap,槽要让出来 |
| 本条 | 精读这条 | 已在 setup_arch DTB 笔记里见过 |
失败都是返回 NULL(或 devm_ioremap_resource 返回 ERR_PTR)。驱动必须查返回值,不能对空指针 readl。
generic_iounmap():把地址按页对齐,确认落在 ioremap 区后 vunmap,拆页表、还虚拟窗口。新驱动用 devm_*,一般不手写 iounmap。