4、GPU硬件抽象层(HAL)设计:寄存器映射、MMU与IOMMU、中断与时钟管理
各位好,咱们今天聊点硬核的——GPU驱动里的硬件抽象层,也就是HAL。说白了,HAL就是给上层驱动和底层硬件之间搭一座桥。桥搭得稳不稳,直接决定了你的驱动能不能在各种奇葩硬件上跑起来。
我个人习惯把HAL设计分成四个核心模块:寄存器怎么访问、内存怎么管理、中断怎么处理、时钟和电源怎么控制。咱们一个一个来拆解。
4.1 寄存器映射:ioremap 的正确打开方式
GPU的寄存器都在物理地址上。用户态程序不能直接碰,内核态也得先映射一下。这个映射操作就是 ioremap。
我见过不少新手直接拿 ioremap 返回的地址当普通内存指针用,结果踩坑踩得鼻青脸肿。为什么?因为GPU寄存器可能有缓存一致性要求,或者有读写顺序约束。
readl / writel 这类访问函数,别用 memcpy 或直接解引用。我曾经在一个项目里,因为用了 *(volatile uint32_t *)addr 的方式写寄存器,结果在ARM平台上死活刷不进去数据——编译器优化把写操作给合并了。
代码示例:
void __iomem *gpu_regs;
// 映射GPU寄存器基地址,长度通常为4KB或更多
gpu_regs = ioremap(GPU_BASE_ADDR, GPU_REG_SIZE);
if (!gpu_regs) {
pr_err("ioremap failed\n");
return -ENOMEM;
}
// 写寄存器:使能GPU核心
writel(0x1, gpu_regs + GPU_ENABLE_OFFSET);
// 读寄存器:检查状态
uint32_t status = readl(gpu_regs + GPU_STATUS_OFFSET);
这里有个小技巧:映射长度最好按页对齐(4KB)。很多SoC的MMU只支持页粒度映射,不对齐的话 ioremap 会返回错误。
4.2 MMU与IOMMU:内存访问的守门员
GPU有自己的MMU,叫IOMMU。它和CPU的MMU功能类似,但管的是设备访问内存。
你想想看,GPU要访问系统内存里的纹理、顶点缓冲、命令队列。如果没有IOMMU,GPU就得直接操作物理地址。那问题来了:如果用户态程序给了一个错误的地址,GPU直接写挂了整个系统怎么办?
IOMMU就是干这个的。它把GPU看到的“设备虚拟地址”映射到真正的物理地址。这样即使GPU跑飞了,也最多破坏它自己的地址空间。
| 特性 | CPU MMU | GPU IOMMU |
|---|---|---|
| 管理对象 | CPU访问内存 | 设备(GPU)访问内存 |
| 页表格式 | 通常4级页表 | 可定制,常见2~3级 |
| TLB刷新 | 由OS管理 | 由驱动管理,开销大 |
| 典型用途 | 进程隔离 | DMA安全、大页支持 |
我在项目中遇到过一个问题:GPU IOMMU的TLB刷新太频繁,导致性能下降。后来我改成批量刷新——把多个地址的映射更新攒到一起,一次性刷TLB。嗯,效果立竿见影。
4.3 中断处理注册:request_irq 的坑与解
GPU干活的时候,CPU不能一直轮询吧?太浪费了。所以GPU完成一个任务后,会发一个中断通知CPU。驱动里注册中断处理函数,用的就是 request_irq。
代码示例:
static irqreturn_t gpu_irq_handler(int irq, void *dev_id)
{
struct gpu_device *gpu = (struct gpu_device *)dev_id;
uint32_t status;
// 读中断状态寄存器
status = readl(gpu->regs + GPU_INT_STATUS);
// 处理具体中断源
if (status & GPU_INT_FENCE) {
// 处理fence信号
gpu_fence_signal(gpu);
}
if (status & GPU_INT_ERROR) {
// 处理错误,比如页面错误
gpu_handle_error(gpu);
}
// 写中断清除寄存器
writel(status, gpu->regs + GPU_INT_CLEAR);
return IRQ_HANDLED;
}
// 注册中断
ret = request_irq(gpu->irq_num, gpu_irq_handler,
IRQF_TRIGGER_HIGH | IRQF_SHARED,
"gpu", gpu);
if (ret) {
pr_err("request_irq failed: %d\n", ret);
return ret;
}
这里有个容易忽略的点:中断处理函数里不能调用 ioremap、kmalloc 这些可能睡眠的函数。为什么?因为中断上下文不允许睡眠。我曾经在中断里调了 kmalloc(GFP_KERNEL),结果系统直接死锁——内核调度器都卡住了。
正确的做法是:中断里只做最轻量的工作,比如读状态、清中断、唤醒一个工作队列。真正的处理放到 workqueue 或 tasklet 里。
IRQF_SHARED 标志,结果GPU的中断线和别的设备共享,导致中断冲突。系统日志里全是“irq handler mismatch”的报错。排查了两天才发现是共享中断的问题。
4.4 时钟与电源管理接口:别让GPU饿死
GPU是个电老虎。不干活的时候得关掉时钟,甚至断电。干活的时候得把频率提上去。这些操作都通过时钟和电源管理接口完成。
Linux内核里,时钟管理用 clk 框架,电源管理用 regulator 和 runtime PM。
代码示例:
// 获取时钟
struct clk *gpu_clk = clk_get(dev, "gpu_core");
if (IS_ERR(gpu_clk)) {
dev_err(dev, "failed to get gpu clock\n");
return PTR_ERR(gpu_clk);
}
// 设置频率
clk_set_rate(gpu_clk, 500000000); // 500MHz
// 使能时钟
clk_prepare_enable(gpu_clk);
// 获取电源域
struct regulator *gpu_reg = regulator_get(dev, "gpu_vdd");
if (IS_ERR(gpu_reg)) {
dev_err(dev, "failed to get regulator\n");
return PTR_ERR(gpu_reg);
}
// 设置电压
regulator_set_voltage(gpu_reg, 900000, 900000); // 0.9V
// 使能电源
regulator_enable(gpu_reg);
我个人习惯把时钟和电源的初始化放到 probe 函数里,把关闭放到 remove 里。但光这样不够——你得考虑动态调频。GPU负载低的时候降频省电,负载高的时候升频提性能。
Linux内核提供了 devfreq 框架来做这件事。你只需要实现一个 devfreq_simple_ondemand 的governor,或者自己写一个策略。我建议用 devfreq 而不是自己写定时器,因为内核已经帮你处理了上下限、延迟、采样频率这些细节。
好了,HAL设计的四个核心模块就聊到这儿。寄存器映射要小心缓存问题,IOMMU要关注TLB刷新和一致性,中断处理要轻量快速,时钟电源要按顺序操作。这些细节看起来琐碎,但每一个都可能导致驱动不稳定甚至系统崩溃。
下一章咱们聊聊GPU命令提交和调度——那又是另一片战场了。