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;
}
⚠️ 注意:ioremap之前一定要先调用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;
}
💡 小技巧:有些SoC的TRNG时钟频率有范围限制,比如不能低于1MHz、不能高于100MHz。如果时钟频率不对,TRNG可能输出周期性的伪随机序列。我建议你在使能时钟后,用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");
}
⚠️ 避坑指南:我曾经在一个项目里遇到TRNG初始化后死活不产生随机数。折腾了半天,发现是复位释放后没有等足够的时间。有些TRNG内部有自校准电路,复位释放后需要几十微秒才能稳定。如果你读状态寄存器发现“忙”标志一直为1,多半是复位后等待时间不够。

4.4 初始化序列:按手册的剧本走

好了,地址映射了、时钟开了、复位也释放了。接下来就是按手册规定的初始化序列,把TRNG模块配置到正常工作状态。不同厂家的TRNG初始化步骤差异很大,但大体上逃不出这几步:

  1. 配置噪声源:设置采样时钟分频、环路振荡器偏置电流等。
  2. 配置后处理模块:选择健康测试使能、去偏算法、抽取率等。
  3. 启动熵池收集:写入启动位,让TRNG开始累积熵。
  4. 等待初始化完成:轮询状态寄存器,直到“初始化完成”或“数据就绪”标志置位。
  5. 执行一次试读:读一个随机数,确认模块工作正常。

下面是一个典型的初始化序列代码示例:

#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;
}
🔑 关键点:初始化序列里的每一步,最好都加上超时机制。TRNG是模拟电路,受温度、电压影响很大。同一个芯片,冬天和夏天的初始化时间可能差好几倍。别用固定延时,用轮询+超时才是正道。

4.5 整体初始化流程

把上面几块拼起来,就是完整的TRNG驱动初始化流程。我习惯画个图来理清依赖关系,这样写代码的时候不容易漏步骤。

TRNG驱动初始化流程 1. 获取资源 (platform_get_resource) 2. 申请IO内存 (request_mem_region) 3. ioremap 物理地址 → 虚拟地址 4. 时钟使能 (clk_prepare_enable) 5. 释放复位 (deassert reset) 6. 初始化序列 (配置+启动+等待) 7. 试读随机数,验证功能 注意:步骤4(时钟使能)必须在步骤5(释放复位)之前完成,否则TRNG可能进入未知状态

这个流程看起来步骤多,但其实每一步都有明确的检查点。我个人习惯在每个步骤后打印一条调试信息,这样如果初始化失败,从日志里一眼就能看出卡在哪一步。

4.6 初始化中的常见陷阱

陷阱 现象 解决方法
ioremap前未request_mem_region 驱动加载成功,但其他驱动访问同一地址时崩溃 严格按顺序:先request,再ioremap
时钟使能顺序错误 TRNG状态寄存器读回全0或全F 先使能时钟,再释放复位
复位释放后等待不足 初始化完成标志一直不置位 用轮询+超时,别用固定延时
配置寄存器写错位域 TRNG输出全是0或固定模式 读回验证,用readl检查写入值
忽略电源域依赖 TRNG时钟使能成功但模块不工作 检查TRNG是否依赖其他电源域先上电
💡 我的经验:调试TRNG初始化时,准备一个逻辑分析仪或示波器,直接量TRNG模块的时钟引脚和复位引脚。有时候代码看起来没问题,但硬件上时钟根本没送过来——这种问题看寄存器是看不出来的。我遇到过两次,都是因为SoC内部的时钟门控被Bootloader关掉了,驱动里使能了但硬件没反应。

嗯,到这里TRNG驱动的初始化部分就讲完了。记住,初始化是驱动的基础,基础不牢,后面所有功能都是空中楼阁。写代码的时候多留几个心眼,多打几条日志,出问题的时候能省下大把排查时间。