STM32无RNG单元时,巧用ADC噪声与SysTick生成高随机性数值
1. 当你的STM32没有“骰子”时怎么办玩过单片机开发的朋友都知道随机数在很多场景里都扮演着关键角色。比如你想做一个抽奖小游戏或者让设备每次启动时生成一个唯一的ID又或者在一些简单的加密场景里需要一个不可预测的密钥。这时候一个靠谱的随机数来源就至关重要了。对于高端的STM32芯片比如F4、F7、H7系列它们内部集成了一个叫做**硬件随机数发生器RNG**的单元。这玩意儿就像芯片内置的一个“真·骰子”利用模拟电路的固有噪声来产生真正的随机数用起来特别省心直接调用HAL库的HAL_RNG_GetRandomNumber()函数就能拿到一个高质量的随机数。但问题来了我们手头大量使用的、性价比极高的中低端STM32比如经典的F1系列、G0系列它们为了控制成本和功耗往往没有这个“豪华配置”。没有硬件RNG难道就没办法了吗当然不是。这就好比你想做饭没有电饭煲难道就不吃米饭了用锅在炉子上也能煮出来。在嵌入式世界里我们有的是办法“创造”随机性。最常被提起的就是标准C库里的srand()和rand()这对组合拳。但我要先给你泼盆冷水只用它们得到的几乎是“假随机”。因为rand()本质上是一个确定的数学公式只要给srand()的“种子”一样它后面吐出来的数列就一模一样完全可预测。这用在游戏里可能每次重启游戏敌人的行动路线都相同那就太没意思了。所以核心矛盾就变成了如何为一个伪随机数发生器找到一个真随机的种子以及有没有办法直接获取真随机数这篇文章我就结合自己踩过的坑和实战经验给你分享两个在无RNG的STM32上特别实用的“土办法”一个是巧用系统滴答定时器SysTick来“制造”随机种子另一个是挖掘ADC模数转换器的噪声来“提取”真随机数。更妙的是我们还能把这两者结合起来形成一个既保证随机性、又兼顾效率的混合方案。无论你是做物联网设备、智能硬件还是简单的电子玩具这套思路都能让你摆脱随机数匮乏的困境。2. 剖析两种核心的随机数生成原理在动手写代码之前我们得先搞清楚我们要用的这两个“原料”到底是怎么回事。知其然更要知其所以然这样你才能在不同的项目里灵活变通而不是死记硬背代码。2.1 SysTick一个永不停止的“微妙计时器”SysTick是Cortex-M内核自带的一个24位递减计数器。它通常被配置为每1毫秒或一个你设定的固定时间间隔产生一次中断为操作系统提供心跳。但除了这个中断功能我们随时可以读取它的当前计数值SysTick-VAL。它为什么能作为随机种子呢想象一下你让用户去按一个按键从程序开始运行到用户按下按键中间经过的时间是毫秒甚至微秒级的。这个时间长度完全取决于用户的手速、反应对于单片机程序来说是不可预测的。SysTick-VAL这个值就在以系统时钟频率高速递减比如72MHz下每13.9纳秒减1你在“随机”的时刻去读取它得到的值在很大概率上也是随机的。但是这里有个大坑我踩过如果你的程序启动流程是固定的比如没有等待用户输入那么每次上电复位后程序运行到读取SysTick的那行代码所花费的机器周期数可能相差无几。这会导致你每次得到的“种子”都非常接近甚至在某些简单循环中可能会周期性地出现相同值随机性大打折扣。所以更靠谱的做法是结合一个非确定性的用户事件。比如在程序启动后先在一个循环里等待用户按下某个按键或者等待一个外部传感器的不稳定信号。在检测到事件的那个“瞬间”立刻读取SysTick-VAL。这个“瞬间”的计时器值就具备了很好的随机性。这相当于把人的行为或物理世界的噪声引入了随机种子的生成过程。2.2 ADC噪声芯片内部的“微观风暴”如果说SysTick是借用了时间的不可预测性那么ADC噪声就是直接挖掘芯片模拟世界的本质随机性。STM32的ADC分辨率至少是12位这意味着它能把一个0到3.3V的电压分成4096个等级2^124096来测量。关键点来了ADC的最后几位通常是LSB最低有效位是非常不稳定的。即使你给ADC引脚接一个理论上非常稳定的电压比如通过两个精密电阻对电源进行分压得到一个精确的1.65V你连续读取ADC转换结果也会发现数值会在一个很小的范围内跳动比如在2048附近上下波动几个数字。这个跳动主要来源于芯片内部的热噪声和量化噪声。热噪声是电子器件中电子的热运动产生的而量化噪声是ADC转换过程本身固有的。这两种噪声在物理层面上都是真随机的。这就给我们提供了一个绝佳的真随机数来源我们可以故意去读取这个噪声。方法就是设置一个ADC通道接一个稳定的电压源比如分压电路然后高速、连续地采样ADC。每次采样我们只取转换结果的最低1位或2位因为高位是稳定的电压值只有低位在随机波动。然后我们把多次采样得到的这些低位“碎片”拼接起来就能组成一个多位数的、高质量的真正随机数。我实测过用这个方法产生的随机数序列通过专业的随机性测试如NIST测试套件的表现确实可以接近硬件RNG的水平。但它的代价也很明显速度慢耗时间。ADC一次转换需要几个到几十个微秒要采集足够多的位来组成一个32位的随机数可能需要上百次转换耗时数毫秒。在实时性要求高的场合这可能是无法接受的。3. 实战构建一个混合随机数生成器理解了原理我们就可以动手搭建一个实用的方案了。单独使用SysTick种子或ADC噪声都有其局限性最好的办法是“强强联合”。3.1 第一步用ADC噪声生成高质量的“种子池”我们首先利用ADC噪声来创建一个一次性的、高质量的随机种子。这个种子不需要在每次需要随机数时都生成可以在系统初始化时生成一次用于播种伪随机数发生器或者作为后续混合生成的基石。这里有一个提升效率的技巧使用DMA直接存储器访问来批量采集ADC数据。这样CPU就可以解放出来去做其他事情等DMA搬运完一批数据比如256个后再来处理它们。// 假设使用STM32CubeMX/HAL库以下为示例代码框架 #define ADC_SAMPLE_COUNT 256 uint32_t adc_buffer[ADC_SAMPLE_COUNT]; void ADC_RNG_Init(void) { // 1. 初始化ADC设置一个通道例如接在稳定的分压电压上 // 2. 启用DMA配置为循环模式将数据搬运到 adc_buffer // 3. 启动ADC } uint32_t GetTrueRandomSeed(void) { uint32_t seed 0; uint8_t bit_position 0; // 等待DMA采集到足够的数据可以通过DMA中断或查询标志位 while(/* DMA传输未完成 */) { // 可以加入超时机制 } // 处理采集到的数据取每个样本的低2位拼接到seed中 for(int i 0; i ADC_SAMPLE_COUNT bit_position 32; i) { // 取ADC值的低2位 (adc_buffer[i] 0x03) uint8_t noise_bits adc_buffer[i] 0x03; // 将这两个位放入seed的当前空闲位置 seed | (noise_bits bit_position); bit_position 2; // 每次增加2位 // 如果ADC噪声只取最低1位则用 (adc_buffer[i] 0x01)且 bit_position 1 } // 如果采集了256个样本每个取2位我们就能得到512位的原始数据 // 但seed只有32位这里我们实际上是用前16个样本16*232位就填满了seed。 // 更多的样本可以用来做后处理比如异或混合增强随机性。 // 一个简单的增强将整个buffer的数据进行异或混合 for(int i 0; i ADC_SAMPLE_COUNT; i) { seed ^ adc_buffer[i]; } return seed; }这个GetTrueRandomSeed()函数虽然耗时可能需要几毫秒但它只在系统启动时调用一次生成一个“种子池”或初始种子这个代价是完全可以接受的。3.2 第二步用SysTick的动态值作为快速补充生成了高质量的初始种子后我们的伪随机数发生器rand()就有了一个很好的起点。但是如果我们需要连续、快速地生成大量随机数rand()的序列仍然是确定性的。为了在每次调用时都增加不可预测性我们可以引入SysTick的当前值进行“扰动”。思路是将SysTick的瞬态值与伪随机数发生器的输出进行混合。这里不直接用SysTick作为种子因为我们已经用更好的ADC种子初始化了而是把它作为一个动态的“搅拌器”。// 使用ADC种子初始化标准库随机数发生器 srand(GetTrueRandomSeed()); // 定义一个增强型的随机数获取函数 uint32_t GetEnhancedRandom(void) { uint32_t sysTickVal SysTick-VAL; // 获取当前SysTick值 uint32_t pseudoRand rand(); // 获取伪随机数 // 将两者进行异或混合。异或操作能很好地保留随机性。 // 也可以尝试更复杂的混合比如乘以一个奇数后相加。 uint32_t finalRandom pseudoRand ^ sysTickVal; return finalRandom; }这样做的好处是基础随机性好rand()序列的起点种子是真随机的。动态扰动强每次调用GetEnhancedRandom都会混入一个高速变化的SysTick值即使攻击者知道了rand()的内部状态也很难预测下一次输出因为SysTick值取决于函数被调用的精确时刻。速度快一次rand()调用加上一次内存读取和一次异或运算开销极小适合在循环或中断中频繁调用。3.3 第三步设计一个完整的、可配置的随机数模块在实际项目中我们最好把它封装成一个模块根据不同的应用场景对随机性要求高 vs 对速度要求高来灵活配置。// random_utils.h #ifndef __RANDOM_UTILS_H #define __RANDOM_UTILS_H #include stdint.h #include stdbool.h typedef enum { RNG_MODE_FAST, // 快速模式仅使用播种后的rand()SysTick扰动 RNG_MODE_STANDARD, // 标准模式如上文所述的混合模式 RNG_MODE_TRUE // 真随机模式每次调用都使用ADC噪声非常慢 } RNG_Mode_t; void RNG_Init(void); bool RNG_SeedWithADC(uint32_t sampleCount); // 用ADC噪声重新播种 uint32_t RNG_Get(RNG_Mode_t mode); #endif// random_utils.c #include random_utils.h #include main.h // 包含你的ADC、SysTick相关头文件 static bool isSeeded false; static uint32_t adc_seed 0; void RNG_Init(void) { // 默认使用ADC种子初始化一次 if(RNG_SeedWithADC(128)) { // 采样128次 isSeeded true; srand(adc_seed); } else { // 如果ADC种子失败降级使用SysTick随机性较差但好过固定值 srand(SysTick-VAL); isSeeded true; // 标记已播种但记录质量 } } bool RNG_SeedWithADC(uint32_t sampleCount) { // 实现ADC采样并生成种子的逻辑更新 adc_seed // 返回成功或失败 // ... (具体实现参考上文GetTrueRandomSeed思路) adc_seed generatedSeed; return true; } uint32_t RNG_Get(RNG_Mode_t mode) { if(!isSeeded) { RNG_Init(); // 懒初始化 } switch(mode) { case RNG_MODE_FAST: // 最简单快速适合对随机性要求不高的游戏、简单调度 return rand(); case RNG_MODE_STANDARD: // 推荐的混合模式兼顾性能与质量 return rand() ^ SysTick-VAL; case RNG_MODE_TRUE: // 用于密钥生成等安全场景每次调用都慢 // 可以设计为采集少量ADC样本与当前rand()和SysTick混合 uint32_t fastPart rand() ^ SysTick-VAL; uint32_t truePart 0; // 快速采集几个ADC样本例如4次取低位 for(int i0; i4; i) { truePart (truePart 2) | (ReadSingleADC() 0x03); } return fastPart ^ truePart; default: return rand(); } }这样在你的主程序中需要快速随机数时调用RNG_Get(RNG_MODE_FAST)需要高随机性时调用RNG_Get(RNG_MODE_STANDARD)在极少数需要最高安全性的环节如生成初始密钥使用RNG_Get(RNG_MODE_TRUE)或直接调用RNG_SeedWithADC。4. 不同应用场景下的选择与优化建议方案有了但具体怎么用还得看你的项目需求。我根据常见的几种嵌入式场景给你一些具体的建议。4.1 场景一设备唯一ID或启动标识生成很多物联网设备需要在第一次启动时生成一个全球唯一的标识符UUID的一部分或者一个随机的设备编号。这种场景对随机性的质量要求最高因为它直接关系到设备的身份唯一性。我的建议是必须使用ADC噪声作为核心熵源。在设备出厂首次启动的初始化流程中调用RNG_SeedWithADC函数并使用较大的采样次数比如512次甚至1024次确保采集到足够的熵。生成足够长的随机数。一个32位的随机数仍然有重复的概率。你可以用这个高质量的种子初始化srand()然后连续调用rand()获取多个32位数拼接成一个128位或更长的标识符。考虑结合芯片唯一ID。STM32都有一个96位的唯一设备标识符Unique Device ID。你可以将ADC生成的随机数与这个唯一ID进行哈希运算比如简单的SHA-256如果资源允许这样生成的标识符既包含了物理不可克隆的随机性又绑定了芯片本身几乎不可能重复。4.2 场景二简单游戏或动态效果如LED随机闪烁做一个呼吸灯流水灯太普通了想让你做的智能台灯或者玩具的灯光效果每次都不一样、更灵动这种场景对随机数的生成速度要求高但对随机性的质量要求相对较低。攻击者不会去预测你的下一个LED是哪一颗亮起。我的建议是在程序开始时用一次ADC种子或SysTick种子初始化srand()即可。甚至可以直接用HAL_GetTick()基于SysTick的毫秒计数器的返回值作为种子虽然随机性一般但足够简单。在需要随机数的循环中直接调用rand()。速度极快效果也完全能满足视觉上的“随机”感。如果想效果更好一点可以采用RNG_MODE_FAST模式。如果连初始化时的一次ADC采样都觉得耗时可以退而求其次在循环中结合SysTick-VAL的低位进行微调也能打破rand()序列的周期性。4.3 场景三简单的通信协议或数据混淆有些轻量级的通信协议为了降低连续数据被误判为命令的风险或者只是想给数据包做个简单的混淆需要用到随机数。这种场景需要一定的随机性来避免模式可被轻易识别但又不能引入太大的延迟。我的建议是采用标准的混合模式RNG_MODE_STANDARD。这是性价比最高的选择。它用ADC种子打下了良好的基础确保了不同设备、不同启动周期下序列的起始差异很大。同时每次调用混入SysTick值使得同一序列内部的关联性被大大削弱外部观察者很难找出规律。可以定期例如每小时或每天用ADC重新播种一次。这能进一步降低长期运行后随机数序列可能被分析破解的风险。你可以在SysTick中断里设置一个软件计数器时间到了就置位一个标志在主循环中检查这个标志并执行RNG_SeedWithADC(64)使用较小的采样次数以降低对主循环的阻塞。4.4 性能与随机性的权衡表格为了更直观我把几种方法的权衡点总结成下面这个表格你可以在选型时快速参考方法随机性质量生成速度CPU占用适用场景关键注意事项纯rand()极低极快极低对随机性无要求的模拟、演示必须提供变化的种子否则序列固定SysTick作种子低快低简单的游戏、非关键动态效果依赖不可预测的用户/外部事件来读取种子ADC噪声作种子高慢仅初始化一次初始化时高所有需要较好随机性的场景的基础需稳定电压源采样次数影响质量混合模式 (ADC种子SysTick扰动)中高快低通用推荐通信、中等安全需求标识、复杂效果在速度和质量间取得最佳平衡纯ADC噪声连续生成极高极慢持续高密钥生成、安全启动、唯一ID严重牺牲实时性通常只用于初始化5. 避坑指南与进阶思考最后分享几个我在实际项目中踩过的坑和一点进阶想法希望能帮你少走弯路。第一个坑ADC参考电压的稳定性。我们依赖ADC噪声的前提是输入的电压本身是稳定的。如果你直接用电源电压VCC作为ADC的参考电压同时又用电阻对VCC分压作为输入那么当电源波动时ADC读数的整个值都会漂移噪声就被淹没了。尽量使用STM32内部稳定的参考电压如VREFINT通道作为基准或者确保你的分压源是独立于VCC的精密基准源。第二个坑SysTick的周期性。在极其规律的任务中比如每1毫秒执行一次的定时任务中读取SysTick可能会读到规律的值。避免在周期性的中断服务程序ISR中直接读取SysTick作为唯一随机源。如果非要用可以结合中断发生时的某个外部IO状态即使未连接读取的电平也有噪声或者多个定时器的计数值进行异或。第三个坑伪随机数发生器的算法。标准库的rand()函数通常实现为线性同余发生器LCG其随机性质量一般且周期有限。对于要求更高的应用可以考虑实现一个更复杂的算法比如梅森旋转算法Mersenne Twister虽然它占用的内存稍多但周期极长随机性分布更好。你可以用ADC噪声初始化它的状态向量。进阶思考熵池Entropy Pool的概念。在专业的密码学随机数生成器中会维护一个“熵池”不断收集各种不可预测的事件按键时间、网络数据包间隔、ADC噪声等作为熵源然后通过一个密码学安全的哈希函数如SHA-256从熵池中提取随机数。我们在STM32上也可以模仿这种思想开辟一小块内存作为熵池在系统空闲时用ADC慢速采集噪声存入池中在外部中断发生时将事件发生时的精确时间多个定时器计数值混入池中。当需要高质量随机数时对整个熵池做哈希运算输出。这能极大地提升随机数的安全上限适合对安全有严苛要求的边缘设备。说到底在资源受限的单片机上做随机数就是在物理世界的不可预测性和有限的计算资源与时间之间做精巧的平衡。希望我今天分享的这套结合ADC与SysTick的混合方案能成为你工具箱里一件趁手的武器。下次当你的项目需要一点“不确定性”的魔法时不妨试试看。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2408363.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!