
1. TRNG硬件随机数生成器从物理噪声到安全密钥的工程实践在嵌入式安全领域随机数不是“随便”的数而是系统信任的基石。无论是生成一个会话密钥、初始化一个加密向量还是为一个协议生成一个临时的Nonce其安全性都完全依赖于背后那个随机数生成器的“不可预测性”。软件伪随机数生成器PRNG依赖算法和种子一旦种子被猜中或算法被逆向整个安全大厦就可能倾覆。而真正的随机必须根植于物理世界的混沌与噪声之中这正是硬件真随机数生成器TRNG的价值所在。我接触过不少基于微控制器的安全项目从智能卡到物联网网关TRNG模块往往是那颗“定心丸”。它不像外设那么简单更像一个精密的物理熵源采集与调理系统。今天我就以德州仪器TI某款芯片中的TRNG模块为蓝本结合手册中的核心内容拆解一下它的工作原理、配置玄学以及低层编程的那些“坑”。这不仅仅是读寄存器更是理解如何将一个物理过程驯服为稳定、可靠的安全服务。如果你正在为你的嵌入式设备寻找或实现一个靠谱的随机数源那么这篇深度解析或许能帮你避开不少弯路。2. TRNG核心原理物理熵源的采集与提纯要理解TRNG的配置和编程首先得明白它到底在“制造”什么。TRNG的核心思想是利用微观物理过程的固有随机性例如半导体中的热噪声、自由运行振荡器FRO的相位抖动等。这些现象在宏观上可能微不足道但在微观和时间尺度上是完全不可预测的。2.1 熵源自由运行振荡器FRO的奥秘在TI的这个TRNG实现中熵源是多个自由运行振荡器。你可以把它们想象成一群各自为政、不受约束的“野马”。每个FRO都是一个独立的环形振荡器其振荡频率受制程、电压、温度PVT的影响并且自身存在固有的相位噪声和抖动。关键点在于这些FRO的振荡与系统主时钟是异步的。为什么需要多个FRO单个物理熵源可能出问题比如因为某种原因如电磁干扰、特定的电压条件陷入一个稳定的模式停止产生有效的随机性。使用多个例如24个独立的FRO相当于引入了多个独立的随机性来源。即使其中一两个FRO因为“时钟锁定”后面会详细讲而失效其他FRO仍然能贡献熵值整个系统的鲁棒性大大增强。熵的积累速度也直接与启用的FRO数量正相关FRO越多单位时间内采集到的随机“素材”就越多。2.2 采样与熵积累从模拟噪声到数字随机比特有了FRO这群“野马”下一步是如何“驯服”它们提取出随机的比特流。这个过程分为两步采样和熵积累。采样系统时钟对TRNG模块而言以固定的频率去“窥探”每个FRO的输出电平0或1。由于FRO的振荡与采样时钟不同步且存在抖动采样点落在FRO信号上升沿或下降沿的哪个位置是完全随机的。这个随机的位置决定了采样到的值是0还是1。TRNG:CFG0.SMPL_DIV寄存器就是用来控制采样频率的它定义了每多少个系统时钟周期采样一次。手册强调这个值必须尽可能小通常设为0即每个时钟周期都采样以确保最慢的FRO在其两个振荡周期内至少被采样一次避免丢失随机性事件。熵积累单个采样位的随机性熵是很低的可能只有零点几个比特。为了得到高熵值的随机数需要将大量这样的采样位进行混合和“搅拌”。这里用到了一个关键的硬件模块线性反馈移位寄存器。LFSR本身是确定性的但它的妙处在于它将来自多个FRO的随机采样位进行异或XOR后作为输入喂给自己。LFSR不断地移位、反馈这个过程就像一个混沌系统将输入的微弱随机性充分扩散到其整个状态中。你可以把LFSR想象成一个不断被注入各种颜色墨水随机采样位的搅拌机。即使每次注入的墨水颜色很淡熵低但经过长时间、多次的注入和搅拌最终搅拌机里的颜色会变得完全无法预测。TRNG:CTL.STARTUP_CYCLES和TRNG:CFG0.MIN/MAX_REFILL_CYCLES这些寄存器本质上就是在控制“搅拌”的时间——需要积累多少采样才认为LFSR里的“颜色”即状态达到了足够的随机性例如64位熵可以输出作为一个随机数种子。2.3 熵的量化与安全等级手册里反复提到了“64位熵”这个概念。这不是一个随便的数字。在密码学中熵是衡量不可预测性的单位。一个具有64位熵的随机数意味着攻击者平均需要尝试2^63次才能猜中它这在计算上是不可行的。TRNG通过控制启动周期和重填周期来确保每个输出的64位随机数都达到或超过这个熵值。这里有一个非常重要的安全实践STARTUP_CYCLES启动周期定义了TRNG模块从完全关闭状态启动后需要积累多少样本才能产生第一个随机数。MIN_REFILL_CYCLES最小重填周期定义了在输出一个随机数后需要再积累多少样本才能产生下一个。为了确保每个输出随机数都具有相同的、可预测的高熵值最佳实践是将MIN_REFILL_CYCLES设置为0。这样系统将强制使用MAX_REFILL_CYCLES的值作为固定的重填周期使得每个随机数的“酿造”时间一致。注意手册明确警告STARTUP_CYCLES和MIN_REFILL_CYCLES设置过低只能用于生成非安全用途的随机数例如同步字、CRC初始化值。对于密钥生成等安全用途必须确保最小熵值达到64位或更高。一个经验值是当所有24个FRO都启用时大约5ms的启动时间可以生成一个具备64位熵的随机数。在实际产品中你需要根据安全认证如FIPS 140-2, Common Criteria的要求来校准和验证这个时间。3. TRNG警报机制诊断与恢复TRNG不是一劳永逸的模块它内部的物理过程可能会“生病”最主要的表现就是警报。理解并妥善处理警报是保证TRNG长期可靠运行的关键。3.1 警报的根源频率锁定警报最可能的原因是FRO时钟与采样时钟之间的频率锁定。想象一下如果一个FRO的振荡频率恰好是采样时钟频率的整数倍那么采样点每次都会落在FRO波形的相同相位上。这就好比用每秒闪一次的快门采样时钟去拍一个每秒转整数的电风扇FRO拍出来的叶片看起来总是静止的。在这种情况下采样到的将是一个固定的模式如连续多个0或1而不是随机序列。TRNG内部有重复模式检测器会持续监视每个FRO的采样序列。当检测到某个FRO的输出出现过于规律的模式时内部计数器就会增加。一旦这个计数超过了TRNG:ALARMCNT.ALARM_THR寄存器设定的阈值默认是255这是一个比较宽松的阈值旨在减少误报就会针对该FRO触发一个警报事件。3.2 警报处理两种策略当警报事件发生时硬件会自动采取一个保护动作关闭那个“有问题”的FRO阻止它继续向熵池贡献可能劣质的“随机性”。接下来就需要软件工程师介入了你有两个选择尝试挽救Detune这是首选方案。通过设置TRNG:FRODETUNE寄存器中对应此FRO的位为1可以让该FRO的振荡频率提升大约5%。这个微小的频率偏移目的就是打破它与采样时钟之间的谐波锁定关系。关键操作顺序由于FRO已被警报关闭你需要先确保TRNG:FROEN寄存器中对应位为0然后设置FRODETUNE最后再重新在FROEN中启用该FRO。这相当于给那匹“野马”轻轻抽一鞭子让它跑得节奏稍微不一样。永久弃用如果上述“微调”操作后该FRO仍然频繁触发警报可能意味着它本身已经失效或不稳定。此时你应该记录下这个状态例如在软件中设置一个标志位并保持其在FROEN寄存器中的禁用状态将其从熵源集合中永久移除。3.3 关机阈值与系统健康度监控单个FRO失效或许可以接受但如果失效的FRO太多总的熵产生速率就会下降可能无法在规定时间内达到所需熵值影响系统性能甚至安全。为此TRNG提供了关机溢出监控机制。TRNG:ALARMCNT.SHUTDOWN_THR寄存器允许你设置一个FRO关机数量的阈值。当因为警报而被关闭的FRO数量记录在ALARMSTOP.FRO_FLAGS和ALARMCNT.SHUTDOWN_CNT中超过这个阈值时会触发SHUTDOWN_OVF中断或状态标志。这是一个非常重要的系统健康信号。在你的软件设计中必须监控这个事件。一旦发生意味着TRNG的熵源健康度已低于安全冗余水平。处理策略可以是提升系统警报级别通知上层应用或运维人员。尝试自动恢复在中断服务程序中批量对所有被关闭的FRO执行Detune操作并重新启用。降级运行或进入安全状态如果可用的FRO数量已低于某个绝对下限例如少于总数的一半应考虑停止依赖TRNG的高安全级操作。4. 低层编程实战从初始化到数据读取理解了原理和异常处理我们来看如何用代码“驱动”这个模块。低层编程的核心就是与一系列内存映射的寄存器打交道。下面我将基于手册提供的流程给出更贴近实战的C语言代码片段和解释。4.1 模块初始化与配置序列在设备复位后首次使用TRNG前必须完成一系列初始化。这个过程必须严格按照顺序进行。// 假设我们有必要的硬件抽象层HAL函数来读写寄存器 #define TRNG_BASE 0x40084000 // 示例基地址 // 步骤1: 启用外围模块时钟依赖具体芯片的PRCM模块 PRCM-PDCTL0_PERIPH_ON | (1 TRNG_PERIPH_BIT); // 开启TRNG电源域 PRCM-SECDMACLKGR | (1 TRNG_CLK_EN_BIT); // 使能TRNG接口时钟 // 通常还需要等待时钟稳定具体见芯片手册 // 步骤2: 执行软件复位确保TRNG处于已知状态 TRNG_REG(TRNG_SWRESET) 0x1; // 写入1触发复位 while(TRNG_REG(TRNG_SWRESET) 0x1); // 轮询等待复位完成 // 步骤3: 配置采样和熵积累参数必须在TRNG_EN0时配置 TRNG_REG(TRNG_CFG0) 0; // SMPL_DIV 0每个时钟周期采样一次 // 设置启动周期例如目标5ms启动时间系统时钟48MHz // 所需周期数 5ms * 48e6 Hz 240000 cycles // STARTUP_CYCLES寄存器值 ceil(所需周期数 / 256) uint32_t startup_cycles_val (240000 255) 8; // 除以256并向上取整 TRNG_REG(TRNG_CTL) (startup_cycles_val 16); // 设置STARTUP_CYCLES字段 // 设置重填周期为了确保每个随机数熵值一致将MIN_REFILL设为0使用MAX_REFILL TRNG_REG(TRNG_CFG0) | (0x00 0); // MIN_REFILL_CYCLES 0 // 设置MAX_REFILL_CYCLES例如也使用5ms对应的值 TRNG_REG(TRNG_CFG0) | (startup_cycles_val 16); // 设置MAX_REFILL_CYCLES字段 // 步骤4: 配置FRO和警报 TRNG_REG(TRNG_FRODETUNE) 0x0; // 初始不进行频率微调 TRNG_REG(TRNG_FROEN) 0x00FFFFFF; // 启用所有24个FRO假设芯片有24个 TRNG_REG(TRNG_ALARMCNT) 0xFF; // 设置警报阈值ALARM_THR为默认值255 TRNG_REG(TRNG_ALARMCNT) | (0x08 16); // 设置SHUTDOWN_THR8即超过8个FRO关闭则报警 // 步骤5: 使能TRNG模块 TRNG_REG(TRNG_CTL) | (1 10); // 设置TRNG_EN位为1关键点解析时钟使能是前提TRNG模块本身和访问它的总线都需要时钟这一步常被遗忘导致后续寄存器读写失败。复位是必要的确保所有内部状态机、LFSR和计数器都回到初始状态。时序约束修改STARTUP_CYCLES、MIN/MAX_REFILL_CYCLES、SMPL_DIV等关键配置寄存器时必须确保TRNG_EN位为0否则写操作会被忽略。这是一个硬性规定。FRO启用默认启用所有FRO能获得最大熵产生速率。在产品后期可以根据长期稳定性测试结果考虑禁用个别表现不佳的FRO。4.2 轮询模式读取随机数轮询模式是最简单直接的方式适用于对实时性要求不高、或不想处理中断的场景。/** * 通过轮询方式获取一个64位随机数 * return 成功返回0并填充random_num失败返回错误码如超时 */ int trng_polling_read(uint64_t *random_num) { uint32_t timeout 1000000; // 设置一个超时计数器防止死等 // 等待RDY标志置位 while (!(TRNG_REG(TRNG_IRQFLAGSTAT) 0x1)) { timeout--; if (timeout 0) { // 超时处理检查TRNG是否仍在运行NEED_CLOCK位检查警报等 if (!(TRNG_REG(TRNG_IRQFLAGSTAT) (1 31))) { return -1; // TRNG未运行或已停止 } return -2; // 熵积累超时可能配置周期太短或FRO大量失效 } } // 读取64位随机数分两个32位寄存器 uint32_t lsw TRNG_REG(TRNG_OUT0); // 低32位 uint32_t msw TRNG_REG(TRNG_OUT1); // 高32位 *random_num ((uint64_t)msw 32) | lsw; // 清除RDY标志告知硬件可以准备下一个随机数 TRNG_REG(TRNG_IRQFLAGCLR) 0x1; // 检查并处理警报在轮询模式中需要主动检查 uint32_t alarm_mask TRNG_REG(TRNG_ALARMMASK); uint32_t alarm_stop TRNG_REG(TRNG_ALARMSTOP); if (alarm_mask ! 0 || alarm_stop ! 0) { handle_trng_alarm(alarm_mask, alarm_stop); // 调用警报处理函数 } return 0; // 成功 }轮询模式注意事项必须超时永远不要在一个无限循环中等待RDY标志。如果TRNG因为配置错误或硬件故障无法产生随机数程序会卡死。清除标志读取数据后必须通过写IRQFLAGCLR.RDY1来清除RDY标志。这是通知硬件“我已取走数据你可以开始准备下一个”的必要步骤。不清除标志RDY会一直为1但OUT0/OUT1中的值不会更新。主动健康检查在轮询模式下没有中断来通知你发生了警报。因此在每次成功读取随机数后或定期地应该检查ALARMMASK和ALARMSTOP寄存器并调用相应的处理函数以防FRO被静默关闭导致熵产生能力下降。4.3 中断模式与服务例程对于需要高效、异步获取随机数的应用中断模式是更好的选择。它允许CPU在TRNG积累熵的过程中处理其他任务。// 首先在初始化阶段额外配置中断 void trng_interrupt_init(void) { // ... 前述的初始化步骤1-5 ... // 使能TRNG模块中断连接到NVIC TRNG_REG(TRNG_IRQFLAGMASK) 0x3; // 同时使能 RDY 和 SHUTDOWN_OVF 中断 // 配置NVIC启用TRNG中断中断号需查芯片手册 NVIC_EnableIRQ(TRNG_IRQn); NVIC_SetPriority(TRNG_IRQn, 5); // 设置合适优先级 } // TRNG中断服务例程 (ISR) void TRNG_IRQHandler(void) { uint32_t status TRNG_REG(TRNG_IRQFLAGSTAT); uint32_t int_clear 0; // 处理随机数就绪中断 if (status 0x1) { // RDY 位 uint32_t lsw TRNG_REG(TRNG_OUT0); uint32_t msw TRNG_REG(TRNG_OUT1); uint64_t random_val ((uint64_t)msw 32) | lsw; // 将随机数存入软件队列供上层应用读取 enqueue_random_number(random_val); int_clear | 0x1; // 标记需要清除RDY标志 } // 处理关机溢出中断太多FRO被关闭 if (status 0x2) { // SHUTDOWN_OVF 位 uint32_t shutdown_cnt (TRNG_REG(TRNG_ALARMCNT) 24) 0x3F; // 记录严重错误日志或尝试恢复 log_error(TRNG FRO Shutdown Overflow! Count: %lu, shutdown_cnt); // 可以尝试批量恢复被关闭的FRO try_recover_shutdown_fros(); int_clear | 0x2; // 标记需要清除SHUTDOWN_OVF标志 } // 检查并处理FRO警报警报本身不直接产生中断但需要在ISR中处理其后果 uint32_t alarm_mask TRNG_REG(TRNG_ALARMMASK); if (alarm_mask ! 0) { handle_alarms_in_isr(alarm_mask); // 在ISR中快速处理警报 } // 一次性清除所有已处理的中断标志 if (int_clear ! 0) { TRNG_REG(TRNG_IRQFLAGCLR) int_clear; } } // 警报处理函数在ISR中调用需快速执行 static void handle_alarms_in_isr(uint32_t alarm_mask) { // 1. 读取当前被停止的FRO uint32_t alarm_stop TRNG_REG(TRNG_ALARMSTOP); // 2. 对每个触发警报的FRO尝试Detune uint32_t fro_detune_new TRNG_REG(TRNG_FRODETUNE); uint32_t fro_en_new TRNG_REG(TRNG_FROEN); for (int i 0; i 24; i) { if (alarm_mask (1 i)) { // 该FRO触发了警报 // 确保它被禁用硬件应已设置但再确认 fro_en_new ~(1 i); // 尝试对其Detune提速5% fro_detune_new | (1 i); } } // 3. 清除警报掩码和停止标志向对应位写1清零具体看寄存器是W1C还是其他 TRNG_REG(TRNG_ALARMMASK) alarm_mask; // 假设是W1C TRNG_REG(TRNG_ALARMSTOP) alarm_stop; // 假设是W1C // 4. 应用新的Detune设置并重新启用FRO TRNG_REG(TRNG_FRODETUNE) fro_detune_new; // 注意需要先写FRODETUNE再重新启用FROEN TRNG_REG(TRNG_FROEN) fro_en_new | (~alarm_stop 0x00FFFFFF); // 重新启用未被永久关闭的FRO }中断模式编程要点中断使能两步走不仅要设置TRNG模块内部的中断掩码IRQFLAGMASK还要在ARM Cortex-M的NVIC中使能对应的中断线。ISR要快进快出中断服务例程中不宜进行复杂操作。对于随机数通常只是读取并存入一个环形缓冲区。对于警报处理也应是简单的寄存器操作。复杂的恢复逻辑或日志记录最好设置标志位退出ISR后在主循环中处理。中断标志清除在ISR结束前必须清除已处理的中断源标志否则会立即再次进入中断。IRQFLAGCLR寄存器是写1清零W1C。共享数据保护如果中断产生的随机数会被多个任务或线程读取必须使用互斥锁、队列等机制保护共享缓冲区防止竞态条件。5. 常见问题排查与实战经验在实际项目中TRNG模块可能会遇到各种稀奇古怪的问题。下面是我总结的一些典型故障场景和排查思路。5.1 问题一读不到随机数RDY标志永不置位现象轮询IRQFLAGSTAT.RDY标志始终为0或中断从未发生。排查步骤检查时钟和电源确认PRCM中TRNG的时钟和电源域已正确使能。用调试器读取TRNG:CTL.TRNG_EN位确认其为1。检查NEED_CLOCK位读取IRQFLAGSTAT.NEED_CLOCK位31。如果为0表示TRNG处于空闲状态可能根本没有启动熵积累过程。这通常是因为TRNG_EN位未被成功置1或者模块处于某种错误状态。检查警报状态读取ALARMSTOP寄存器。如果所有FRO都被关闭例如因为频繁警报且达到了关机阈值那么熵积累会停止永远无法达到RDY条件。此时SHUTDOWN_OVF标志可能被置位。验证配置寄存器确认STARTUP_CYCLES和MAX_REFILL_CYCLES没有被误设为极大值例如0xFFFF导致需要天文数字般的采样周期才能完成。也要确认SMPL_DIV设置合理通常为0。检查FRO启用情况读取FROEN寄存器确认有足够多的FRO被启用例如大部分位应为1。如果HWOPT.NR_OF_FROS显示有24个但FROEN里只有少数几个是1熵积累速度会极慢。5.2 问题二随机数质量不佳或序列有规律现象生成的随机数通过简单的统计测试如卡方检验、游程检验失败或者在实际加密应用中导致可预测的弱点。排查与解决熵源不足这是最常见的原因。确保STARTUP_CYCLES和MAX_REFILL_CYCLES设置得足够大。不要为了追求速度而牺牲安全性。对于安全应用必须保证足够的熵积累时间。可以参考手册给出的“5ms for 64-bit entropy with all FROs”作为起点但强烈建议在目标硬件和实际工作环境温度、电压下进行熵评估。FRO大量失效频繁的警报导致许多FRO被关闭。检查ALARMSTOP和SHUTDOWN_CNT。如果数量很多需要检查电源噪声是否过大或者芯片是否存在缺陷。尝试在初始化时对所有FRO进行一次性的、小幅度的Detune设置FRODETUNE为某个非零值有时可以改善FRO的稳定性。采样时钟问题SMPL_DIV设置过大导致采样率过低无法捕捉到FRO的快速抖动。务必遵循手册建议将其设置为尽可能小的值确保最慢的FRO周期也小于两倍采样周期。软件使用错误在轮询模式下读取随机数后没有清除RDY标志导致后续读取的一直是同一个旧值。或者在中断模式下ISR读取数据太慢导致新的随机数覆盖了旧的但硬件通常有双缓冲此问题不常见仍需注意。5.3 问题三系统进入低功耗模式后TRNG失效现象设备从睡眠或深度睡眠模式唤醒后TRNG无法再产生随机数或产生速度极慢。原因与解决时钟被关闭在低功耗模式下TRNG模块的时钟可能被PRCM关闭。唤醒后必须重新初始化TRNG模块执行软件复位和配置序列而不仅仅是重新使能TRNG_EN。FRO需要重新稳定FRO是模拟电路从断电或深度省电状态恢复后其振荡特性可能需要一段时间才能稳定。在唤醒后增加一个额外的延时例如几毫秒再开始使用TRNG或者适当增加STARTUP_CYCLES的值。寄存器上下文丢失有些低功耗模式不会保持外设寄存器状态。唤醒后所有TRNG配置寄存器都需要重新写入。最好的做法是在每次从可能丢失上下文的低功耗模式唤醒后都完整地执行一遍TRNG初始化流程。5.4 实战经验安全关键系统的TRNG设计启动时自检在系统启动阶段不要立即使用TRNG生成的第一个随机数。应该先让TRNG运行一段时间例如完成数次MAX_REFILL_CYCLES并监控是否有异常警报。可以设计一个简单的健康检查函数连续获取多个随机数进行基本的随机性统计如0/1平衡性通过后再将TRNG投入正式使用。持续健康监控在后台任务中定期例如每分钟检查ALARMSTOP和SHUTDOWN_CNT。如果被关闭的FRO数量持续增长或超过了预警阈值例如SHUTDOWN_THR/2应产生系统日志告警。熵池混合对于极高安全要求的应用不要直接使用TRNG输出的64位作为最终密钥。应该将多个连续的TRNG输出或者将TRNG输出与一个由安全种子初始化的DRBG确定性随机比特生成器如HMAC-DRBG或CTR-DRBG进行混合。这样即使TRNG在某个瞬间输出质量略有下降最终的随机数仍有DRBG的算法强度作为保障。这是NIST SP 800-90A/B等标准推荐的做法。寄存器访问安全确保只有可信的、高权限的软件组件如安全服务层才能访问TRNG的配置寄存器。防止恶意软件修改SMPL_DIV或ALARM_THR等参数降低随机数质量。