4. TRNG驱动初始化:ioremap映射物理地址、时钟使能、复位控制、初始化序列编写
驱动初始化这事儿,说白了就是给硬件“通电、唤醒、告诉它该干嘛”。TRNG模块也不例外。你想想看,一个真随机数发生器,本质上是一堆模拟电路和数字逻辑的混合体。如果连地址都访问不到、时钟都没跑起来、复位状态都没释放,它怎么可能给你吐出随机数?
我个人习惯,写TRNG驱动第一步不是敲代码,而是翻芯片手册。你得搞清楚三件事:TRNG模块挂在哪条总线上?它的基地址是多少?时钟源来自哪个PLL或分频器?这些信息通常在芯片的Memory Map和Clock Tree章节里。我见过不少新手上来就对着寄存器一顿操作,结果ioremap返回NULL,时钟使能位写不进去——说白了,就是没把“家底”摸清楚。
4.1 ioremap:把物理地址变成虚拟地址
在Linux内核里,你不能直接拿物理地址去读写寄存器。内核用的是虚拟地址空间。所以第一步,得用ioremap()把TRNG的物理基地址映射到内核虚拟地址空间。
#include <linux/io.h>
#include <linux/ioport.h>
#define TRNG_BASE_ADDR 0xFE800000
#define TRNG_REG_SIZE 0x1000
static void __iomem *trng_base;
static int trng_probe(struct platform_device *pdev)
{
struct resource *res;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
if (!request_mem_region(res->start, resource_size(res), "trng")) {
dev_err(&pdev->dev, "Failed to request mem region\n");
return -EBUSY;
}
trng_base = ioremap(res->start, resource_size(res));
if (!trng_base) {
dev_err(&pdev->dev, "ioremap failed\n");
release_mem_region(res->start, resource_size(res));
return -ENOMEM;
}
dev_info(&pdev->dev, "TRNG mapped at %p (phys 0x%llx)\n",
trng_base, (unsigned long long)res->start);
return 0;
}
request_mem_region()。这不仅是内核规范,更是防止其他驱动跟你抢资源。我曾经在一个项目里忘了这步,结果两个驱动同时操作同一段地址,TRNG输出的随机数全是0xDEADBEEF——排查了整整两天才发现是资源冲突。
映射完成后,你就可以用readl()和writel()来读写寄存器了。记住,别用普通的指针解引用,ARM架构下内存序和总线序可能不一致,readl/writel会帮你处理好这些细节。
4.2 时钟使能:不给饭吃,它就不干活
TRNG模块要工作,时钟必须跑起来。不同芯片的时钟控制方式差异很大。有的挂在CCU(Clock Control Unit)下面,有的直接由PRCM(Power, Reset and Clock Management)管理。
我一般这么干:先查手册找到TRNG对应的时钟门控位,然后通过clk_prepare_enable()来操作。如果芯片用设备树,那就更简单了——在dts里配好clocks属性,驱动里直接拿。
#include <linux/clk.h>
static struct clk *trng_clk;
static int trng_clk_enable(struct device *dev)
{
int ret;
trng_clk = devm_clk_get(dev, "trng_clk");
if (IS_ERR(trng_clk)) {
dev_err(dev, "Failed to get TRNG clock\n");
return PTR_ERR(trng_clk);
}
ret = clk_prepare_enable(trng_clk);
if (ret) {
dev_err(dev, "Failed to enable TRNG clock\n");
return ret;
}
dev_info(dev, "TRNG clock enabled\n");
return 0;
}
clk_get_rate()读一下实际频率,跟手册对比确认。
4.3 复位控制:先按住,再松手
复位控制是初始化里最容易出问题的一环。TRNG模块上电后通常处于复位状态,你得先把它从复位里释放出来。但这里有个顺序问题:先释放时钟,再释放复位。反过来操作,模块可能进入不确定状态。
复位控制位一般在Reset Controller或PRCM的寄存器里。有些芯片甚至把复位和时钟放在同一个寄存器里——写的时候要小心别把其他位搞乱了。
#define TRNG_RST_CTRL 0x00
#define TRNG_RST_BIT (1 << 0)
static void trng_reset_deassert(void)
{
u32 val;
/* 先确认时钟已经使能 */
val = readl(trng_base + TRNG_RST_CTRL);
val &= ~TRNG_RST_BIT; /* 清除复位位 = 释放复位 */
writel(val, trng_base + TRNG_RST_CTRL);
/* 等待复位完成,通常几微秒就够了 */
udelay(10);
dev_info(trng_dev, "TRNG reset deasserted\n");
}
4.4 初始化序列:按手册的剧本走
好了,地址映射了、时钟开了、复位也释放了。接下来就是按手册规定的初始化序列,把TRNG模块配置到正常工作状态。不同厂家的TRNG初始化步骤差异很大,但大体上逃不出这几步:
- 配置噪声源:设置采样时钟分频、环路振荡器偏置电流等。
- 配置后处理模块:选择健康测试使能、去偏算法、抽取率等。
- 启动熵池收集:写入启动位,让TRNG开始累积熵。
- 等待初始化完成:轮询状态寄存器,直到“初始化完成”或“数据就绪”标志置位。
- 执行一次试读:读一个随机数,确认模块工作正常。
下面是一个典型的初始化序列代码示例:
#define TRNG_CTRL 0x04
#define TRNG_STAT 0x08
#define TRNG_DATA 0x0C
#define TRNG_CFG 0x10
#define CTRL_ENABLE (1 << 0)
#define STAT_INIT_DONE (1 << 0)
#define STAT_DATA_READY (1 << 1)
#define CFG_OSC_BIAS 0x3
#define CFG_DIV_RATIO 0x2
static int trng_init_sequence(void)
{
u32 val;
int retry = 100;
/* Step 1: 配置噪声源参数 */
val = (CFG_OSC_BIAS << 4) | (CFG_DIV_RATIO << 0);
writel(val, trng_base + TRNG_CFG);
/* Step 2: 启动TRNG */
val = readl(trng_base + TRNG_CTRL);
val |= CTRL_ENABLE;
writel(val, trng_base + TRNG_CTRL);
/* Step 3: 等待初始化完成 */
do {
val = readl(trng_base + TRNG_STAT);
if (val & STAT_INIT_DONE)
break;
udelay(10);
} while (--retry);
if (!retry) {
dev_err(trng_dev, "TRNG init timeout\n");
return -ETIMEDOUT;
}
/* Step 4: 试读一个随机数 */
if (!(readl(trng_base + TRNG_STAT) & STAT_DATA_READY)) {
dev_err(trng_dev, "No data ready after init\n");
return -EIO;
}
val = readl(trng_base + TRNG_DATA);
dev_info(trng_dev, "TRNG init OK, first sample: 0x%08x\n", val);
return 0;
}
4.5 整体初始化流程
把上面几块拼起来,就是完整的TRNG驱动初始化流程。我习惯画个图来理清依赖关系,这样写代码的时候不容易漏步骤。
这个流程看起来步骤多,但其实每一步都有明确的检查点。我个人习惯在每个步骤后打印一条调试信息,这样如果初始化失败,从日志里一眼就能看出卡在哪一步。
4.6 初始化中的常见陷阱
| 陷阱 | 现象 | 解决方法 |
|---|---|---|
| ioremap前未request_mem_region | 驱动加载成功,但其他驱动访问同一地址时崩溃 | 严格按顺序:先request,再ioremap |
| 时钟使能顺序错误 | TRNG状态寄存器读回全0或全F | 先使能时钟,再释放复位 |
| 复位释放后等待不足 | 初始化完成标志一直不置位 | 用轮询+超时,别用固定延时 |
| 配置寄存器写错位域 | TRNG输出全是0或固定模式 | 读回验证,用readl检查写入值 |
| 忽略电源域依赖 | TRNG时钟使能成功但模块不工作 | 检查TRNG是否依赖其他电源域先上电 |
嗯,到这里TRNG驱动的初始化部分就讲完了。记住,初始化是驱动的基础,基础不牢,后面所有功能都是空中楼阁。写代码的时候多留几个心眼,多打几条日志,出问题的时候能省下大把排查时间。