硬件接口与寄存器:TRNG 模块的寄存器映射详解

搞驱动开发,说白了就是跟寄存器打交道。TRNG 模块也不例外。

你拿到一份芯片手册,第一件事就是找到 TRNG 的寄存器基地址,然后搞清楚每个偏移量对应什么功能。我刚开始做 TRNG 驱动时,就吃过一次亏——手册上写的是 32 位寄存器,结果我按 16 位去读,数据死活不对。嗯,从那以后我养成了一个习惯:先确认总线宽度,再动手写代码。

寄存器映射概览

TRNG 模块通常挂载在 APB 或 AHB 总线上。基地址由芯片设计决定,比如 0x4000_1000。偏移量从 0x00 开始,依次排列。下面这张表是我从多个项目里总结出来的典型寄存器布局,不同厂商会有些差异,但核心寄存器基本一致。

偏移地址 寄存器名称 读写属性 功能描述
0x00 TRNG_CR R/W 控制寄存器:使能、复位、模式选择
0x04 TRNG_SR R 状态寄存器:数据就绪、错误标志、忙标志
0x08 TRNG_DR R 数据寄存器:读取随机数
0x0C TRNG_IMR R/W 中断屏蔽寄存器:控制中断触发条件
0x10 TRNG_ISR R/W1C 中断状态寄存器:读清零或写1清零
0x14 TRNG_CFGR R/W 配置寄存器:时钟分频、采样周期
我的习惯:写驱动前,先把这些偏移量定义成宏。比如 #define TRNG_CR 0x00。这样代码可读性高,后期移植也方便。

控制寄存器(TRNG_CR)

控制寄存器是 TRNG 模块的「总开关」。你写驱动时,第一步就是操作它。

典型的位域定义如下:

  • Bit 0 — RNGEN:随机数发生器使能。写 1 启动,写 0 关闭。我建议在初始化时先写 0 复位,再写 1 使能,避免残留状态影响。
  • Bit 1 — RST:软件复位。写 1 复位整个 TRNG 模块,复位后自动清零。我在调试时经常用这个位来恢复模块到已知状态。
  • Bit 2 — MODE:模式选择。0 表示连续模式,1 表示单次模式。连续模式下,TRNG 会持续产生随机数;单次模式需要每次手动触发。
  • Bit 3 — IE:中断使能。写 1 允许 TRNG 产生中断,写 0 禁用。

避坑指南:我曾经在一个项目里,使能 TRNG 后立即去读数据寄存器,结果读回来全是 0。后来才发现,TRNG 需要一段启动时间才能稳定输出。正确的做法是:使能后等待状态寄存器的「数据就绪」位被置位,再去读数据。

代码示例——初始化控制寄存器:

void trng_init(void)
{
    uint32_t base = TRNG_BASE_ADDR;

    // 先复位模块
    writel(base + TRNG_CR, 0x02);
    // 等待复位完成
    while (readl(base + TRNG_CR) & 0x02);

    // 使能 TRNG,连续模式,开启中断
    writel(base + TRNG_CR, 0x09);  // RNGEN=1, MODE=0, IE=1
}

状态寄存器(TRNG_SR)

状态寄存器告诉你 TRNG 模块「现在怎么样了」。驱动里大部分时间都在轮询它。

关键位域:

  • Bit 0 — DRDY:数据就绪位。1 表示数据寄存器中有有效随机数。这是你最常用的位。
  • Bit 1 — ERR:错误标志位。1 表示检测到错误,比如时钟异常或熵源故障。我建议在驱动里加一个错误计数器,连续出错就上报。
  • Bit 2 — BUSY:忙标志位。1 表示 TRNG 正在生成随机数。此时不能写入控制寄存器。

你想想看,如果 DRDY 一直为 0,说明 TRNG 可能没工作。这时候就要检查时钟和使能位了。

代码示例——轮询等待数据就绪:

int trng_wait_for_data(uint32_t timeout_us)
{
    uint32_t base = TRNG_BASE_ADDR;
    uint32_t timeout = timeout_us;

    while (timeout--) {
        if (readl(base + TRNG_SR) & 0x01) {
            return 0;  // 数据就绪
        }
        udelay(1);
    }

    return -1;  // 超时
}
注意:轮询超时一定要加。我曾经见过一个驱动,没有超时机制,结果 TRNG 硬件挂了,整个系统卡死在 while 循环里。加个超时,至少能优雅地报错。

数据寄存器(TRNG_DR)

数据寄存器是最终产出随机数的地方。每次读取,TRNG 都会返回一个新的 32 位随机数。

读取时要注意几点:

  • 必须先确认 DRDY 为 1,再读 DR。否则读到的可能是旧数据或无效数据。
  • 读 DR 后,DRDY 会自动清零。硬件就是这么设计的。
  • 有些芯片的 DR 是 32 位,有些是 64 位。我遇到过一款芯片,手册写 32 位,实际读出来只有低 16 位有变化。后来确认是勘误——嗯,芯片手册也有坑。

代码示例——读取随机数:

uint32_t trng_get_random(void)
{
    uint32_t base = TRNG_BASE_ADDR;

    if (trng_wait_for_data(1000) != 0) {
        // 超时处理
        return 0xFFFFFFFF;
    }

    return readl(base + TRNG_DR);
}

中断相关寄存器

轮询方式简单,但浪费 CPU。实际产品里,我更喜欢用中断方式。

中断屏蔽寄存器(TRNG_IMR)控制哪些事件能触发中断:

  • Bit 0 — DRDY_IE:数据就绪中断使能
  • Bit 1 — ERR_IE:错误中断使能

中断状态寄存器(TRNG_ISR)记录中断事件。读它就能知道是数据就绪还是错误触发了中断。注意,这个寄存器通常是 W1C(写 1 清零),处理完中断后要手动清除标志位。

配置寄存器(TRNG_CFGR)用来调整采样参数。比如时钟分频系数、采样窗口长度。这些参数直接影响随机数的质量和生成速度。我一般会留一个调试接口,方便在测试阶段调整。

寄存器操作流程图

下面这张图是我画的一个典型 TRNG 驱动流程,从初始化到读取随机数,再到中断处理。你写驱动时可以照着这个思路来。

TRNG 驱动寄存器操作流程图 TRNG 驱动寄存器操作流程 初始化 TRNG_CR 轮询 TRNG_SR.DRDY 读取 TRNG_DR 检查 TRNG_SR.ERR 有错误 错误处理/复位 无错误 返回随机数 重新使能 TRNG 说明 中断方式可替代轮询 ISR 中读取 DR 并清标志

寄存器操作的注意事项

最后,我总结几个实际项目中踩过的坑:

  • 字节序问题:有些芯片的 TRNG 寄存器是小端,有些是大端。如果你的驱动跑在不同平台上,记得用 readl/writel 这类接口,它们会帮你处理字节序。
  • 时钟门控:TRNG 模块的时钟可能被门控了。我遇到过驱动写寄存器没反应,查了半天发现是时钟没开。初始化前先确认时钟树配置。
  • 多核访问:如果 TRNG 被多个核共享,需要加锁保护。否则两个核同时读 DR,数据可能乱掉。
  • 电源管理:系统休眠时,TRNG 寄存器内容可能丢失。唤醒后要重新初始化。
调试小技巧:在驱动里加一个寄存器 dump 函数,把 TRNG_CR、TRNG_SR、TRNG_DR 的值打印出来。遇到问题先看寄存器状态,比猜代码快多了。

嗯,寄存器这部分就讲到这里。你把这些寄存器操作搞清楚了,TRNG 驱动的基本骨架就有了。下一层就是怎么把这些寄存器操作封装成可靠的 API,以及怎么处理各种边界情况。