Linux内核readl和writel函数的用法

在 Linux 内核中,readl()writel() 是用于访问 内存映射 I/O(Memory-Mapped I/O, MMIO) 的函数,专门处理 32 位宽度的寄存器或内存操作。它们提供了一种安全和可移植的方式与硬件设备寄存器进行交互。


一、基本概念

  • readl(addr): 从虚拟地址 addr 读取 32 位数据。
  • writel(val, addr): 将 32 位值 val 写入虚拟地址 addr

这两个函数的主要目的是:

  1. 保证原子性:确保 32 位操作在目标平台上是一次性完成的。
  2. 处理字节序:根据 CPU 和设备的不同,自动处理字节序问题。
  3. 内存屏障:隐式添加内存屏障(Memory Barrier),防止编译器或 CPU 乱序执行导致的问题。

二、函数原型

1
2
3
4
#include <linux/io.h>

u32 readl(const volatile void __iomem *addr);
void writel(u32 val, volatile void __iomem *addr);
  • volatile 关键字告诉编译器不要优化此操作(确保每次访问都是真实的硬件访问)。
  • __iomem 表示该地址指向 I/O 内存空间,而非普通内存。

三、使用步骤

  1. 映射物理地址到虚拟地址
    硬件寄存器的物理地址需要先通过 ioremap() 映射到内核虚拟地址空间:

    1
    void __iomem *reg_base = ioremap(phy_addr, size);
  2. 计算寄存器偏移量
    假设寄存器偏移为 reg_offset,则实际访问地址为:

    1
    void __iomem *reg_addr = reg_base + reg_offset;
  3. 读写寄存器

    1
    2
    u32 value = readl(reg_addr);        // 读取寄存器
    writel(new_value, reg_addr); // 写入寄存器
  4. 解除映射
    在驱动卸载时,使用 iounmap() 释放映射:

    1
    iounmap(reg_base);

四、完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <linux/io.h>
#include <linux/module.h>

#define PHY_BASE 0x10000000 // 设备物理基地址
#define REG_OFFSET 0x10 // 寄存器偏移量

static void __iomem *reg_base;

static int __init my_init(void)
{
// 1. 映射物理地址到内核虚拟地址
reg_base = ioremap(PHY_BASE, 0x100);
if (!reg_base) {
pr_err("ioremap failed\n");
return -ENOMEM;
}

// 2. 读取寄存器值
u32 val = readl(reg_base + REG_OFFSET);
pr_info("Original value: 0x%08x\n", val);

// 3. 修改并写回寄存器
val |= 0x1; // 设置最低位
writel(val, reg_base + REG_OFFSET);

return 0;
}

static void __exit my_exit(void)
{
// 4. 解除映射
iounmap(reg_base);
}

module_init(my_init);
module_exit(my_exit);

五、注意事项

  1. 地址对齐
    确保访问的地址是 32 位对齐的(即地址是 4 的倍数),否则某些架构会导致异常。

  2. 内存屏障

    • 若需要显式控制内存屏障,可使用 mmiowb() 或在读写后调用 rmb()/wmb()
    • writel() 自身包含写屏障,但某些场景可能需要额外处理。
  3. 错误处理
    始终检查 ioremap() 的返回值,映射失败可能导致内核崩溃。

  4. 并发访问
    如果多个 CPU 或线程可能同时访问同一寄存器,需使用自旋锁(spin_lock)保护。

  5. 设备特性
    某些设备的寄存器可能需要特定的访问顺序(例如先写命令寄存器再写数据寄存器)。


六、其他相关函数

  • 8 位访问: readb() / writeb()
  • 16 位访问: readw() / writew()
  • 64 位访问: readq() / writeq()

通过合理使用 readl()writel(),可以确保在内核中安全、高效地操作硬件寄存器。

Linux内核中写完数据将其写入到flash中,确保不再缓存中

Linux内核中写完数据将其写入到flash中,确保不再缓存中的接口

在 Linux 内核中,若要将数据写入 Flash 并确保数据彻底落盘(即不在缓存中残留),需要根据具体的存储架构和访问方式选择合适的方法。以下是几种典型场景的解决方案:


一、块设备层(如通过 MTD 转换为块设备)

若 Flash 被抽象为块设备(如 /dev/mtdblockX 或通过 FTL 转换的块设备),需确保数据绕过缓存或主动刷写缓存:

1. 直接 I/O(绕过页缓存)

在打开设备时使用 O_DIRECT 标志,强制绕过内核页缓存,直接操作物理存储:

1
int fd = open("/dev/mtdblock0", O_WRONLY | O_DIRECT);
  • 注意:需要对齐的缓冲区(通常按 512 字节对齐)。

2. **主动刷写缓存

Linux内核中写数据将其写入到DDR中而不经过缓存的接口

在 Linux 内核中,若要将数据直接写入 DDR 内存 并确保不经过 CPU 缓存(即绕过缓存,直接操作物理内存),需要根据具体场景选择合适的方法。以下是几种典型场景的实现方案:


一、直接操作非缓存内存(Non-Cached Memory)

如果目标内存区域需要被硬件(如 DMA 控制器或外设)直接访问,或需要避免缓存一致性问题,可以通过以下接口分配或映射 非缓存内存

1. 使用 dma_alloc_coherent(一致性 DMA 内存)

此函数分配的内存对 CPU 和设备是 一致性(Coherent) 的,CPU 写入的数据会直接反映到物理内存,无需手动刷新缓存。
适用场景:需要与 DMA 控制器或其他硬件共享内存时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <linux/dma-mapping.h>

// 分配非缓存内存(物理连续)
void *cpu_addr;
dma_addr_t dma_handle;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
// 错误处理
}

// 写入数据到 DDR(不经过缓存)
memcpy(cpu_addr, src_data, size);

// 释放内存
dma_free_coherent(dev, size, cpu_addr, dma_handle);

关键特性

  • 内存物理连续,适合 DMA 传输。
  • 自动保证 CPU 和设备的内存视图一致。

2. 使用 ioremap 映射非缓存物理内存

如果已知某段物理地址需要直接访问(如预留的硬件缓冲区),可通过 ioremap 将其映射为内核虚拟地址,并指定非缓存属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <linux/io.h>

// 映射物理地址到内核虚拟地址(非缓存)
void __iomem *vaddr;
vaddr = ioremap(phy_addr, size);
if (!vaddr) {
// 错误处理
}

// 写入数据到 DDR(使用 writel 等接口避免缓存)
writel(val, vaddr + offset); // 32 位写入

// 解除映射
iounmap(vaddr);

关键特性

  • 需手动指定物理地址(通常配合设备树预留内存使用)。
  • ioremap 默认可能映射为设备内存(非缓存),但具体行为依赖架构和 flags。

3. 使用 memremap 显式指定缓存策略

在较新内核中,memremap 允许更灵活地控制内存映射属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <linux/io.h>

// 映射物理地址为非缓存内存
void *vaddr = memremap(phy_addr, size, MEMREMAP_WC); // MEMREMAP_WB(缓存)/ MEMREMAP_WT(透写)/ MEMREMAP_WC(非缓存)
if (!vaddr) {
// 错误处理
}

// 直接操作内存(不经过缓存)
memcpy(vaddr, src_data, size);

// 解除映射
memunmap(vaddr);

二、强制绕过 CPU 缓存的写入操作

若已有一段普通内存(如通过 kmalloc 分配),但需要确保某次写入直接到达 DDR,可以手动禁用缓存或刷新缓存:

1. 使用 non-temporal 写入指令(如 movnti

通过 SSE/AVX 指令直接写入内存而不污染缓存(需在用户态或内核模块中内联汇编):

1
2
3
4
5
6
7
// 示例:使用 movnti 写入 32 位数据(x86 架构)
static inline void write_nt(int *dest, int val) {
asm volatile("movnti %1, %0" : "=m" (*dest) : "r" (val));
}

// 调用
write_nt(dest_ptr, value);

注意

  • 需处理内存对齐(通常 16 字节对齐)。
  • 平台依赖性强,通常仅用于性能关键代码。

2. 手动刷新缓存

写入普通缓存内存后,强制刷新缓存使数据落盘:

1
2
3
4
5
6
7
#include <asm/cacheflush.h>

// 写入数据到缓存内存
memcpy(dest, src, size);

// 刷新缓存范围,确保数据写入 DDR
clflush_cache_range(dest, size);

适用场景

  • 临时性强制刷新,但频繁操作会严重影响性能。

三、保留内存(Reserved Memory)

通过设备树预留一段物理内存,确保内核不会将其分配给普通内存池,然后映射为直接访问的非缓存内存:

1. 设备树中预留内存

1
2
3
4
5
6
7
/reserved-memory {
my_region: my_region@10000000 {
compatible = "my-reserved-memory";
reg = <0x10000000 0x100000>; // 物理地址和大小
no-map; // 内核不映射此区域
};
};

2. 驱动中映射预留内存

1
2
3
4
5
6
7
8
9
10
11
// 获取预留内存信息
struct resource *res;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
phy_addr = res->start;
size = resource_size(res);

// 映射为非缓存内存
void __iomem *vaddr = ioremap(phy_addr, size);

// 直接写入 DDR
writel(val, vaddr + offset);

四、注意事项

  1. 缓存一致性

    • 若内存被多个 CPU 核心或设备共享,需确保使用正确的同步机制(如内存屏障 wmb()/rmb())。
    • 避免混合使用缓存和非缓存方式访问同一内存区域。
  2. 性能影响

    • 绕过缓存会降低 CPU 写入速度(无缓存加速)。
    • 频繁的非缓存操作可能增加总线带宽压力。
  3. 平台依赖性

    • 非缓存内存的实现细节(如 ioremap 属性)因架构而异(ARM、x86、RISC-V 等需区别处理)。

五、总结

方法 适用场景 特点
dma_alloc_coherent DMA 设备共享内存 自动维护一致性,物理连续,需释放
ioremap/memremap 访问预留的物理地址 灵活指定缓存策略,需手动管理映射
保留内存 + 设备树 固定物理地址的硬件缓冲区 避免内核占用,需设备树配合
手动刷新缓存 临时强制数据落盘 性能开销大,仅适合低频操作

根据具体需求选择合适方法,确保数据直接写入 DDR 且避免缓存干扰。