去年做一套带OTA升级的传感终端,MCU选的是PIC18F57Q43,要在CAN总线上跑AES-128-CBC加密。第一版为了快速验证,直接用XC8编译软件AES库,结果一个16字节块加密耗时跑到毫秒级,整个通信周期被拖得没法看,中断响应抖动也冒出来了。后来把加密整体搬到片内集成的Crypto Engine上,同样一个块,硬件路径只占软件耗时的一个零头。这件事给我的触动不只是那点性能提升,而是:在MCU上做安全,硬件集成引擎不是可选加分项,而是当你面对真实业务时最值得优先考虑的一条路。
这篇文章想跟你聊的,就是Microchip PIC系列单片机里内置的加密引擎。它不是某个特定型号独有,而是散在一批PIC18、AVR DB、PIC32CM等芯片里,以AES硬件加速器、真随机数发生器、安全启动模块、代码保护逻辑等形式存在。我会从软件加密的痛点讲起,盘一盘哪些芯片带这些外设,然后给出一套完整的MPLAB X + MCC配置实操,最后分享我在真实项目里踩过的一些坑和排查思路。适合正在做安全通信、安全OTA升级、固件防抄板,或者单纯想在低成本MCU上把加密做扎实的开发者参考。
1. 从一次“加密一个包要等半天”的现场说开去——为什么我把目光转向硬件加密引擎
1.1 软件AES在8位MCU上有多吃力
很多人觉得AES-128是小意思,C代码几百行,调用一个函数就能完成加解密。在PC或者Cortex-M4上确实如此,但在8位PIC18这类MCU上,情况完全不同。
我实测过在PIC18F57Q43上跑一个标准软件AES-128,16字节块的加密大约需要几千个时钟周期。这颗芯片主频64MHz,算下来几个毫秒。单次看似乎还能忍,但在实际协议栈里,一个上报帧可能要分多个块加密,再加上CBC模式的链式依赖,一个完整报文往往要数十毫秒。更要命的是,软件AES执行期间CPU被占满,中断延迟直接拉高,定时器、通信外设的实时性全部受影响。如果系统里还挂了电机控制或采样任务,这种延迟不可接受。
代码空间的代价也不小。完整AES-128实现,包括S盒、密钥扩展、加解密逻辑,轻松吃下几KB Flash。对一颗Flash总量32KB的PIC18来说,这个占比有点心疼。而硬件引擎只需要配置几个寄存器,占用几乎可以忽略。
1.2 硬件引擎真正解决的三个问题不止是速度
把加密放到硬件外设之后,第一个变化是CPU几乎不用参与运算。你只需要把明文写入数据寄存器,置位启动位,等待完成标志位,然后读回密文。整个过程CPU可以去做别的,或者进入低功耗模式,等加密完成中断唤醒。
第二个变化是功耗。软件AES意味着CPU高速运转很长时间,动态功耗自然高。硬件引擎用专用状态机完成运算,通常能在远少于软件路径的周期内结束,平均功耗明显下降。对电池供电的终端设备来说,这是实打实的收益。
第三个变化是时序稳定性。硬件外设的运算时间是固定的,不依赖编译器优化等级、不依赖中断嵌套、不依赖当前CPU负载。这对通信协议设计非常友好,你可以精确预算一条报文的处理耗时。软件实现一旦被中断打断,执行时间就会漂移,这在安全协议里尤其让人头疼。
1.3 我用它做过什么、它适合什么项目
我目前做过两种典型场景。一个是前面说的OTA升级固件加密,固件包在Bootloader里用硬件AES解密,校验通过后写入App区。另一个是传感器节点和网关之间的会话密钥加密,用硬件AES加密每一条上行报文,网关用预共享密钥解密。
适合用这类方案的,首先是电池供电的IoT设备,功耗敏感且需要加密通信;其次是现场仪表、工业控制器,需要定期安全升级固件但不能接受停机太久;还有是消费类产品做防抄板,用安全启动和代码保护把固件保护起来。如果项目里已经有了CAN、LIN或者无线通信,加密引擎基本都能无缝接入。
2. Microchip集成加密引擎的硬件家底:从PIC18到AVR DB,哪些芯片能直接“抄作业”
2.1 当前带着硬件加密外设的MCU家族盘点
Microchip在PIC这个品牌下铺了不少产品线,带加密引擎的主要集中在几个家族,我按我接触过的先后顺序说一下。
PIC18 Q系列是我最先用的。PIC18F57Q43、PIC18F56Q43这一批,内置Crypto模块,支持AES-128加解密,同时带了真随机数发生器(TRNG)。这个组合在8位MCU里相当难得。Crypto模块支持ECB和CBC两种模式,基本覆盖了常见的对称加密需求。
AVR DB系列是另一个值得关注的家族,比如AVR128DB48。它内置硬件AES加速器,支持AES-128加密和解密,还带CCL可配置自定义逻辑,能在硬件层面做一些简单的布尔逻辑组合。AVR DB整体定位比PIC18 Q系列更偏向工业控制和实时控制,但加密能力一样实用。
再往上就是PIC32CM系列和PIC32MZ系列。PIC32CM LE是基于Cortex-M0+核心,主打安全启动和安全密钥存储,适合对身份认证有要求的应用。PIC32MZ DA/EF系列带有RNG和安全Flash区域(SAF,Secure Asset Flash),可以用来保存密钥、证书这类敏感资产,适合需要跑复杂协议栈和图形界面的中高端设备。
2.2 核心外设拆解:Crypto模块、TRNG、CCL各自管哪一段
很多同学拿到芯片手册,看到Crypto、TRNG、CCL一堆缩写就懵了。其实它们分工很清晰。
Crypto模块,或者说AES加速器,负责对称加解密。你把密钥和明文给它,它输出密文。以PIC18F57Q43为例,密钥是128位,数据块是128位,你通过几个寄存器把数据喂进去,等待完成标志,然后取结果。ECB模式简单直接,CBC模式需要你多管一个IV向量,但安全性更好。
TRNG,真随机数发生器,负责生成真正的随机数。它利用芯片内部的模拟噪声源产生熵,而不是软件伪随机算法。这个外设的关键用途是生成加密密钥、生成CBC模式的IV、生成挑战-响应认证里的随机挑战值。很多安全协议卡在“随机数不够随机”上,硬件TRNG正好解决这个问题。
CCL是AVR系列上的可配置自定义逻辑,严格来说不是加密外设,但它在安全场景里有点用处。你可以用CCL做一个简单的硬件状态机,用于检测篡改事件、锁定某个外设,或者配合外部传感器做一个快速响应的物理防护逻辑。
PIC32MZ的SAF安全Flash区则起到保险箱的作用。你可以把密钥、证书放到SAF里,它独立于主Flash,有单独的访问控制,即使主Flash被读出来,SAF里的内容也读不到。
2.3 选型建议:不同加密需求对应哪颗芯片
芯型比较直接,我列了一个表格,是我给项目选型时常用的对照表:
| 芯片系列 | 加密能力 | 典型应用场景 |
|---|---|---|
| PIC18F Q系列 | Crypto模块(AES-128 ECB/CBC)、TRNG | 中小型IoT节点、传感器、消费电子 |
| AVR DB系列 | 硬件AES加速器(AES-128)、CCL | 工业控制、车载周边、实时控制 |
| PIC32CM LE/LS | 安全启动、安全密钥存储、AES引擎 | 医疗设备、安全认证、网络节点 |
| PIC32MZ DA/EF | RNG、SAF安全Flash区、AES | 带HMI的高端设备、网关、复杂协议栈 |
如果产品只有几千台量级,成本敏感,PIC18F57Q43这颗就非常合适。如果要做嵌入式安全认证,比如医疗设备或者金融终端,PIC32CM系列更专业。如果主控需要跑TCP/IP协议栈或者图形界面,PIC32MZ是不错的选择。选型时不要只看有没有AES,还要看有没有配套的TRNG、安全存储、调试保护,这些都是安全链路上不可缺的一环。
3. 实操记录:用MPLAB X + MCC把一个AES-128加解密跑起来
3.1 工程与MCC配置的完整过程
我以PIC18F57Q43为例,一步步说一下怎么配置。先准备好MPLAB X IDE和XC8编译器,这两个在Microchip官网都能下载,版本用新的就行。MCC(MPLAB Code Configurator)现在集成在MPLAB X里,不需要单独安装插件。
新建工程时选择芯片型号PIC18F57Q43,编译器选XC8,工程名自己定。工程建好后,在IDE右侧工具栏找到MCC图标,点击进入图形化配置界面。首次打开MCC会加载设备数据包,等它跑完就行。
在MCC的Device Resources面板里展开Crypto,选中Crypto模块,点Add。右侧配置界面会出现AES相关的选项。这里有几项要重点设置:
- Crypto Enable:勾选,启用模块
- Mode:ECB或CBC,按需选择。我建议能用CBC就用CBC,安全性更高
- Key Length:128位,这是当前模块支持的长度
- Key:可以直接在界面里填入16字节密钥,也可以留空,在代码里动态加载
你还可以在Device Resources里找到TRNG模块,一并添加进来。TRNG的配置更简单,主要就是Enable勾选,时钟源用默认的内部振荡器即可。
配置完成后,点击MCC界面的Generate按钮,MCC会生成一套外设初始化代码,放在工程目录下的mcc_generated_files文件夹里。这个过程会自动把引脚冲突、时钟配置、中断向量都处理好,比自己手写寄存器省太多事。
3.2 引擎初始化与加解密调用的代码骨架
MCC生成的代码,主函数需要按固定套路来。你不需要手动调用每个外设的初始化函数,MCC已经在SYSTEM_Initialize()里把它们串起来了。看代码骨架:
#include "mcc_generated_files/mcc.h" static const uint8_t test_key[16] = { 0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, 0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C }; static uint8_t plaintext[16] = { 0x6B, 0xC1, 0xBE, 0xE2, 0x2E, 0x40, 0x9F, 0x96, 0xE9, 0x3D, 0x7E, 0x11, 0x73, 0x93, 0x17, 0x2A }; static uint8_t ciphertext[16]; void main(void) { SYSTEM_Initialize(); // 把密钥写入Crypto引擎 CRYPTO_LoadKey(test_key); // 执行一次AES-128 ECB加密 CRYPTO_Encrypt(plaintext, ciphertext); while (1) { // 业务代码 } }注意,不同型号的MCC生成代码,API名会略有不同。比如有些型号的密钥加载函数叫CRYPTO_SetKey(),有些叫CRYPTO_LoadKey()。你以自己MCC生成的头文件里的声明为准。我上面这段用的是通用写法,逻辑是一样的。
如果你想验证加密结果对不对,可以在线调试,把ciphertext数组加入Watch窗口,跟标准AES-128测试向量比对。我喜欢用OpenSSL命令行生成一组测试数据,然后把同样的密钥和明文灌进MCU,两边结果一致才算跑通。这个习惯帮我排掉了很多配置问题。
3.3 用TRNG生成随机数时要留意的细节
TRNG在MCC里配置好之后,调用方式很简单。以PIC18F57Q43为例,MCC生成的API大概是这种形式:
uint32_t rng_Value; rng_Value = RNG_GetRandom();每次调用返回一个32位真随机数。听起来很简单,实际使用时我踩过几个细节问题,值得重视。
第一个是上电后的熵稳定时间。TRNG内部依赖模拟噪声源,芯片刚上电时,电源电压和内部振荡器还没完全稳定,前几次采样出来的随机数质量可能不理想。我的做法是上电后先连续读8次,全部丢弃,然后才开始正式使用。这个操作本质上是给熵源一个“热身”时间。
第二个是不要用TRNG直接生成大块随机数据后不加处理。TRNG的每个比特都来自物理噪声,硬件上会做一些后处理,但为了保险起见,我通常会再做一次哈希或者异或混合。比如生成16字节密钥时,连续读4次32位随机数,然后做异或混合再作为密钥。这样做能有效降低极端情况下的偏置风险。
第三个是接口的阻塞问题。TRNG的硬件采样需要一定时间,通常几微秒到几十微秒。如果业务代码里频繁调用RNG_GetRandom(),要注意可能阻塞主循环。我会在需要随机数的场景里集中生成,并放在对时序不敏感的位置,避免影响协议栈。
4. 硬件安全不止加解密:Secure Boot、密钥存储与调试锁的完整链路
4.1 从上电到App执行的Secure Boot链路
加解密只是安全的一部分。如果你的固件能被随意替换,那加密算法再强也没意义。Secure Boot(安全启动)的核心目标,是保证MCU上电后只运行经过授权的固件。
一条典型的安全启动链路是这样的。MCU上电后,先执行Boot区里的引导代码,这段代码放在Boot Block区,受代码保护,正常情况下不会被覆盖。引导代码首先验证App区的固件完整性,可以是CRC校验,也可以是更严格的AES解密验证。校验通过,跳转到App区执行;校验失败,停留在Bootloader,等待固件恢复。部分PIC32系列芯片支持在硬件层面通过安全密钥存储和独立的安全处理器来辅助这个验证过程,把根信任放到硬件里。
关键一点是,Secure Boot本身不会增加多少成本,但它需要一个可信的根。这个根就是存放在受保护存储区里的密钥。密钥被读取出来的可能性越低,整个信任链就越稳固。所以,Secure Boot和密钥存储在项目里必须一起规划,不能只做其中一半。
4.2 密钥放在哪里才算靠谱
密钥存放是硬件加密项目里最容易被低估的问题。很多开发者习惯把密钥定义成const数组直接放在代码里,然后打开反汇编工具就能看到16字节的密钥数据。这对安全产品来说是致命的。
比较靠谱的做法是,利用芯片本身的代码保护能力,把密钥所在Flash区域锁定。PIC18和AVR系列都有代码保护位,使能之后,调试器无法通过编程接口读取Flash内容。PIC32MZ的SAF区域更彻底,它可以独立配置访问权限,密钥、证书这类敏感资产被锁在里面,即使应用代码本身都没法随意读取,只能通过特定寄存器和密码访问。
生产环节上,我建议密钥单独烧录。也就是在产线上先烧录一份密钥文件到指定Flash区域,再烧录应用固件,最后设置保护位。如果反过来,先烧固件再烧密钥,密钥数据可能会暴露在编程器缓存里,被中途截获。这个顺序问题我见过不止一次,值得注意。
4.3 调试口与量产保护:别让攻击者比你先拿到密钥
MCC和MPLAB X调试方便,但调试接口也往往是攻击者最喜欢的地方。一个没有关闭调试口的设备,插上调试器就能读出全片Flash,加密算法再强也形同虚设。
量产产品需要做几件事。第一,在配置位里关闭调试功能,设置Debug禁用。第二,使能代码保护,让外部无法通过ICSP接口读取Flash内容。第三,如果支持,设置安全Flash区域的访问密码。这三步做完,攻击者能拿到的只剩下一颗锁死的芯片,而不是一份可分析的固件。
这里有个比较反直觉的点:代码保护一旦使能,你自己也读不回Flash。调试器连上芯片后只能执行全片擦除,擦除后代码保护和密钥全部消失。所以量产烧录流程一定要想清楚,先烧什么、后烧什么、什么时候锁定,锁定之后就不要再指望能回读调试了。我自己的习惯是,烧录脚本里把“锁定”这一步放在最后,而且设成人机双重确认,避免误锁导致批量返工。
5. 我在实际项目里踩过的四个坑(附完整排查思路)
5.1 加密引擎“罢工”了:时钟与初始化顺序问题
第一次用Crypto模块时,我遇到的现象是,加密启动位写1之后,完成标志位迟迟不置位,加密结果读出来全是0xFF。排查过程很曲折,最后定位到是时钟问题。PIC18F Q系列的Crypto模块必须依赖系统时钟工作,如果芯片跑在外部晶振下,而Crypto模块的时钟门没打开,它就一直处于挂起状态。
这个问题的排查思路是,先确认模块时钟使能位,再确认系统时钟源,最后确认初始化顺序。用MCC生成代码时,Crypto模块的时钟配置通常会被自动处理,但如果你手动改过系统时钟配置,或者把主时钟切换到了外部晶振,就要额外检查一下。另一个隐蔽细节是,MCC生成的初始化代码里,SYSTEM_Initialize()内部会先初始化时钟,再初始化Crypto。如果你在main里自己调用了外设初始化函数,放在SYSTEM_Initialize()之前,时钟可能还没起来,模块自然不工作。
5.2 密钥锁死后的恢复流程:量产烧录顺序必须想清楚
我出过一次比较大的问题。当时给一批设备烧录固件,为了图省事,在烧录脚本里直接启用了代码保护。第二天发现有个功能需要调参数,需要重新连接调试器,结果芯片完全不响应。用MPLAB X连接,提示必须Erase All才能继续。全片擦除之后,之前烧进去的密钥也跟着没了,只能返工重烧。
这件事给我的教训很直接:代码保护必须在固件和密钥都烧录完成、并且验证完毕之后,才最后开启。量产阶段的验证流程应该是,先烧录密钥和固件,不要开保护,做一轮整机测试,测试通过后再通过烧录工具执行锁定操作。而且锁定操作要放到独立步骤,不能跟固件烧录混在一个脚本里,防止误触发。
返修场景还要多考虑一层。如果设备返修需要重新烧录,而芯片已经被锁定,最可靠的做法是EEPROM区域单独保存一份可覆盖的密钥副本,但这样又降低了安全性。我现在的折中方案是,返修设备直接换新芯片,旧芯片报废处理。虽然成本高一点,但安全链路不会被破坏。
5.3 TRNG随机数质量不稳:时钟源与采样策略
有一阵子我发现,网关经常报告设备上行的会话密钥存在重复。排查到最后,问题出在TRNG的时钟源选择上。当时为了低功耗,我把系统时钟切换到了外部低频晶振。TRNG虽然名义上支持内部振荡器,但对外部晶振的适配并不好,导致随机数输出的熵明显下降。
解决方案是,把TRNG的时钟源固定为内部高频振荡器,并且保证使用TRNG时系统时钟不掉到某个阈值以下。采样策略上,我会在每轮会话开始前连续读TRNG多次,取其中一部分做异或混合,作为会话密钥。由于真随机数生成本身不可预测,即使攻击者拿到了其中几次采样值,也无法推断出最终的混合结果。
5.4 编译器优化把寄存器访问“优化没了”
这个坑比较有意思,而且很容易被忽略。XC8编译器在-O2及以上优化等级时,会对代码做激进的指令重排。如果你直接操作硬件寄存器的地址,编译器可能认为某些写入操作没有实际效果,把它优化掉,或者调整执行顺序,导致Crypto模块收到的寄存器序列不对。
我第一次遇到这个现象时,加密结果偶尔正确、偶尔错误。后来打开反汇编一看,密钥写入寄存器的语句被重排到了加密启动位之后,相当于引擎还没拿到完整密钥就开始工作了。解决办法是,使用MCC生成的官方API来操作Crypto外设。官方API里,外设寄存器地址被正确映射为volatile,编译器不会对其做危险的优化。如果你确实需要自己操作寄存器,一定要在类型定义里加上volatile关键字,并且把加密启动位写入放在一个单独的函数里,用内存屏障隔开。
我还养成了一个习惯:写完加密相关代码后,主动用-O2和-O0各编译一次,跑同一组测试向量,确保两种优化等级下结果一致。如果出现不一致,就去查寄存器访问代码,而不是怀疑加密算法本身。
最后再分享一个小技巧
项目里如果要快速验证硬件加密正确性,我用得最多的方法是拿OpenSSL当对拍工具。在电脑上执行一条AES-128-ECB加密命令,输入同样的密钥和明文,把结果和MCU的输出做比对。这个做法不用买任何分析仪,几分钟就能排查出是硬件问题还是代码问题。
echo -n "k\xbc\x1e\xb2..." | openssl enc -aes-128-ecb -K 2B7E151628AED2A6ABF7158809CF4F3C | xxd我个人在实际项目里的体会是,Microchip这些带集成加密引擎的PIC,真正值钱的地方不是省了那几毫秒CPU时间,而是把安全这件事从“算法”变成了“系统工程”。AES跑通只是第一步,后续的密钥管理、安全启动、调试保护、量产流程,每一环都要跟上。如果你正打算在PIC上做安全方案,建议不要只盯着加解密函数去抄资料,先把密钥从哪里来、放在哪里、怎么保护这三件事想清楚,再动工程,后面的路会顺很多。