
简介面向STM32F4系列嵌入式开发者这份AES加密解密测试程序以C语言工程形式提供了完整的加解密验证方案。程序基于STM32F4 HAL库实现重点演示了PKCS7填充与解填充算法确保数据块满足AES要求的128位长度同时通过串口1PB6/PB7接收不定长数据解决了异步通信中数据长度不固定的处理难题非常适合需要在工业控制、物联网节点等场景中快速实现数据加密的工程师参考。压缩包内共775个文件以368个C源文件、142个H头文件和70个汇编文件为主体辅以ICF链接脚本、UVprojx工程文件及AXF、HEX烧录文件涵盖从源码阅读到编译下载的完整链路整体包体约21.3MB。目前已有257人学习下载对于希望掌握STM32F4平台AES应用和串口不定长接收技巧的开发者而言这份工程具有直接的借鉴与复用价值。 先把话说在前面网上聊STM32加密的资料不少但大多数停在“看别人跑通”的阶段。我这次是真刀真枪在STM32F407上做了一个AES加密解密测试程序从Keil5建工程开始到tiny-AES-c集成再到ECB、CBC、CTR三种模式逐个验证中间踩过的坑比想象中多。这篇东西就是完整的试验记录适合正在给MCU加加密功能的嵌入式工程师参考也适合刚接触AES、想在F4上快速落地的朋友抄作业。我会把工程搭建、核心代码、测试方法、性能测试和故障排查全写出来保证你照着能跑通。1. 项目目标与方案选型背后的逻辑1.1 这个测试程序到底要验证什么很多人以为“测试程序”就是调通一次加解密就结束了但我这次的目标远不止于此。拿到一个加密库你要确认几件事第一算法实现在这块芯片上是不是正确的必须用标准测试向量去比对而不是只看“能加密也能解密”这种自我安慰的结果第二不同分组模式的行为是否符合预期比如CBC模式里上一个密文块会影响下一个明文块这个用打印日志能直观看到第三性能基准要摸清楚后续做协议设计时才能估算每秒能处理多少包数据第四最好留一套通用性强的调试接口以后每个项目都能复用这个测试框架。我把这些目标写成了四条验收标准能用NIST官方测试向量跑出完全一致的密文、能连续加密解密多组数据不出错、能测出加密和解密各自耗时、串口日志能清晰显示每个中间结果。这样测试程序才不是一次性玩具而是能沉淀成通用工具的东西。1.2 为什么用软件AES而不是依赖硬件加密选型时很多人第一反应是“F4不是有硬件AES吗”。这里要澄清一个容易混淆的点STM32F4系列只有少数型号内置了硬件加密处理器CRYP比如STM32F437、STM32F439常用的STM32F405、F407、F411、F427等型号并没有CRYP外设。我手里这块F407就属于没有硬件AES的型号所以软实现是唯一选择除非换芯片。软件AES虽然不如硬件加速器快但有几个实打实的优点第一纯C代码可以在任何F4型号上编译运行代码可移植性极强第二不依赖复杂的寄存器配置和DMA通道出问题好排查第三模式切换非常灵活ECB、CBC、CTR可以随意切换这在硬件加密引擎上反而麻烦得多。另外有些F4型号内置了TRNG真随机数发生器这个好东西可以配合AES生成随机IV或盐测试程序里我顺手把它也用上了。2. 开发环境与Keil5工程搭建2.1 Keil5添加STM32F4器件支持的正确姿势如果你用的是新装的MDK5打开工程前第一件事是确认Device Pack装好了。选芯片时找不到STM32F407IGHx这类型号十有八九是DFP包没装。我习惯在Pack Installer里直接搜索STM32F4然后安装STMicroelectronics出品的STM32F4xx_DFP也可以去ST官网下载Pack文件后双击安装效果一样。如果项目是用STM32CubeMX生成的MDK工程这个包一般会自动带上不用手动折腾。我这边因为要给旧工程加功能所以选择手动添加。踩过一次坑是MDK5打开旧工程后提示找不到器件但Pack Installer里明明显示装了最后发现是版本不兼容。解决方法是把旧版本DFP卸载装和MDK5版本匹配的新包然后再重新选一次Device型号。所以别急着写代码先让编译器能识别芯片否则后面全白搭。2.2 加密库选型tiny-AES-c与mbedTLS怎么选MCU上做AES加密市面上最常用的两个软件库是tiny-AES-c和mbedTLS。我做了个对比表方便你按场景取舍对比项tiny-AES-cmbedTLS代码体积极小核心就两个文件较大含完整密码套件第三方依赖无直接编译需要裁剪配置依赖传送层支持的模式AES128/192/256的ECB、CBC、CTR完整AES且支持TLS/哈希/证书等适用场景裸机MCU快速验证、单一加密需求需要TLS握手、MQTT加密通信等重场景调试难度简单代码一眼能看完较复杂配置项多我的建议是测试程序阶段先上tiny-AES-c。理由很实在代码量小出问题能直接单步看算法细节而如果你只是在做设备传感器数据加密这个库完全够用。等到后面真的需要上TLS再迁移到mbedTLS也不迟因为AES部分已经被验证过迁移成本低。2.3 快速集成加密源文件从GitHub拉取kokke/tiny-AES-c这个仓库把aes.c和aes.h放进工程的自定义目录比如App/Crypto。然后在魔术棒选项的C/C选项卡里把这个目录加到Include Paths。这里有个细节STM32的CMSIS头文件路径最好往前排否则可能编译时先找错了头文件。源文件加入工程后编译若报错“未定义符号”检查一下aes.c是不是真的加进了工程树而不只是放在文件夹里。我见过太多次“文件在目录里但没被工程引用”的情况。还有一个容易被忽略的点优化等级建议先设置到O0等测试全部通过再改成O2验证性能这个后面性能测试部分会展开说。3. 加密库集成与三种模式的实战代码3.1 AES算法原理速成AES是个分组密码算法明文和密文都是固定16字节一块。AES-128表示密钥长度16字节整个加密过程执行10轮AES-192是12轮AES-256是14轮。每一轮会做四个操作字节代换、行移位、列混淆、轮密钥加。用大白话解释字节代换就是把每个字节通过一张S盒表替换成另一个值行移位是把状态矩阵的行错位挪动列混淆是每列做一次数学变换让单个字节的变化扩散到整列轮密钥加则是把当前状态和本轮扩展出来的密钥做异或。10轮下来明文的每个位都受到密钥和所有其他位的影响这就是AES安全性高的重要原因。3.2 ECB模式先跑通最小闭环ECB是最基础的AES模式把16字节明文直接丢进去加密得到16字节密文。为了验证函数调用没问题我写了个最小闭环测试定义一组密钥和明文先调用加密函数再调用解密函数通过串口打印每一组数据。实际使用的tiny-AES-c接口比较直观流程是先用AES_init_ctx初始化上下文然后对缓冲区原地加解密#include aes.h struct AES_ctx ctx; uint8_t key[16] {0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c}; uint8_t buf[16] {0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a}; AES_init_ctx(ctx, key); // 加密 AES_ECB_encrypt(ctx, buf); printf(ECB encrypt: ); print_hex(buf, 16); // 解密 AES_ECB_decrypt(ctx, buf); printf(ECB decrypt: ); print_hex(buf, 16);打印结果那一步特别重要如果密文和用在线工具算出来的一致说明基本函数没问题。但我也要强调ECB模式有个致命缺点相同明文块永远得到相同密文块真实数据里很容易泄露格式信息。这个模式在测试程序里只适合验证基础函数不要直接用到产品里。3.3 CBC模式引入IV消除明文模式残留CBC是产品里最常见的模式之一它的核心思想是每个明文块先和上一个密文块做异或再执行AES加密。第一个明文块没有“上一个密文”所以引入一个16字节的初始化向量IV。这样即使两个明文块内容相同加密后密文块也大概率不同泄露信息的风险大大降低。对应代码里需要多准备一个IV。tiny-AES-c的初始化函数带IV版本是AES_init_ctx_iv然后调用AES_CBC_encrypt_buffer处理一整段数据uint8_t iv[16] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; AES_init_ctx_iv(ctx, key, iv); AES_CBC_encrypt_buffer(ctx, buf, len); // 注意解密前需要重新设置相同的IV AES_init_ctx_iv(ctx, key, iv); AES_CBC_decrypt_buffer(ctx, buf, len);这里有个特别容易踩的坑解密时如果不重新初始化IV而是沿用加密后已经被修改的IV值因为库函数会更新IV解密结果一定是错的。我一开始就是犯了这个错折腾了半小时才反应过来。所以写代码时养成习惯加解密前都重新执行一次AES_init_ctx_iv。3.4 CTR模式与盐的设计思考CTR模式设计思路完全不同它不是把明文直接加密而是先对一个计数器值做AES加密得到一段“密钥流”再把密钥流和明文按位异或从而得到密文。解密时用完全相同的计数器再一次生成密钥流和密文异或就能还原明文。CTR模式最大的优点是不需要补位数据是几个字节就可以处理几个字节非常适合长度不固定的通信包。它还有个特性是加密和解密用的是同一个函数AES_CTR_xcrypt_buffer不像CBC需要区分encrypt和decrypt。代码上自然就把IV替换成了“nonce counter”的概念uint8_t nonce_counter[16] {0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff}; AES_init_ctx_iv(ctx, key, nonce_counter); AES_CTR_xcrypt_buffer(ctx, buf, len);说到“盐”很多人把盐和IV混为一谈其实两者职责不同。盐通常用在密钥派生阶段比如用户输入口令后通过加盐、迭代哈希生成真正的AES密钥用来对抗离线字典攻击而IV是分组算法里用来让一组数据产生随机化效果的nonce。测试程序里我很自然地做了一个组合让F4的TRNG外设生成一个随机数作为计数器初始值再和固定测试密钥一起跑CTR模式模拟真实协议里“随机数随密文一起传输”的场景。至于“盐放后端”这个问题我的理解是盐不应该硬编码在设备固件里。设备端生成随机盐要么把它附加在密文前一起发送要么由服务端生成后下发给设备总之盐要能动态变化。一旦盐被硬编码在固件里逆向人员拿到固件就等于拿到了造盐规则整个加密体系的安全度会大幅缩水。后面安全建议部分我还会再提一句。4. 串口日志设计、测试用例与性能实测4.1 让每个中间结果可观测调试加密程序最怕的就是黑盒。我专门写了几个打印函数把密钥、明文、密文、解密结果全部以HEX格式输出到串口。每条日志带标记前缀比如[AES-TEST] key:、[AES-TEST] cipher:这样用串口助手分析数据时一目了然。日志设计有个原则输出内容不要过多每一条都有明确语义。我留了一个宏定义来控制日志开关比如#define AES_DEBUG_ENABLE 1正式工程里关掉即可不用删代码。如果你用RTT代替串口速度会更快日志接入也非常方便但裸机串口依然是调试最通用的路子。4.2 测试用例设计与标准测试向量测试用例怎么设计直接影响你能发现多少问题。我用了三组用例用例编号测试内容预期结果TC1使用NIST SP 800-38A标准向量验证AES-128 ECB加解密密文与标准值完全一致解密后还原明文TC2连续加密5个数据块验证CBC模式数据块之间的关联性各块密文互不相同解密后数据完全一致TC3只修改明文的最后一个字节对比加密结果密文变化远不止最后一个字节体现雪崩效应第一组用例是底线算法正不正确就看它。第二组用例专门用来验证CBC模式下的块链接行为如果你把某个密文块删掉后续所有解密块都会变乱这就是CBC的特性。第三组用例特别直观你改一个字节明文密文几乎全部变了这在CTR模式下也能看到因为CTR密钥流是固定的但明文参与异或之后改变位置不同。网上有很多开源测试工具可以帮你生成参考密文核心是别自己拍脑袋发明向量优先用标准向量。4.3 性能实测用DWT在MCU上计时测AES性能最直接的方法是用Systick计时但如果系统里其他模块占了Systick就会冲突。我推荐用Cortex-M内核自带的DWT周期计数器它不占用系统定时器精度到CPU周期特别适合这类短耗时测量。初始化DWT的代码很简短CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk;测耗时的时候在加密函数前后分别读DWT-CYCCNT相减后除以主频就得到秒数DWT-CYCCNT 0; AES_CBC_encrypt_buffer(ctx, buf, len); uint32_t cycles DWT-CYCCNT; float ms cycles / 168.0f / 1000.0f;我实测的参考数据是在STM32F407 168MHz、Keil AC5编译器、优化等级O2的条件下使用tiny-AES-c加密512字节数据耗时大概在0.5ms量级换算过来吞吐量接近1MB/s多。这个成绩对于传感器上报、指令加密场景完全够用但如果一分钟要加密几兆字节的视频流或文件流就得评估是否要用硬件AES。测性能前务必确认优化等级O0和O2的差距可能超过3倍。我要不是被O0下的慢速迷惑过也不会特意强调这点。5. 容易踩的坑与排查手册5.1 Keil5编译配置类问题测试程序写完后最常报错的无非这几类找不到头文件、未定义函数、变量被优化掉。找不到头文件就把Include路径重新检查一遍未定义函数则是源文件没加进工程而“变量被优化掉”多半是因为你把优化等级开到了O2但调试时又想观察某个局部变量。处理办法是调试阶段统一O0功能全部验证完再切O2。如果必须保持O2可以给关键变量加volatile修饰但我不建议为了调试去改产品代码结构。还有一个隐性问题是芯片型号选错。你用的是F407结果Device列表里不小心选了F103Cortex-M内核虽然兼容编译但启动文件和寄存器头文件不一致跑起来必出问题。每次新建工程都先看下编译宏里有没有STM32F40XX字样这一步能省很多后面的排查时间。5.2 加解密结果校验失败的排查三板斧加密结果不对九成是下面三点第一密钥和IV长度不对。AES-128要求16字节密钥很多人直接把ASCII字符串当密钥用比如1234567890123456这里每个字符是8位数量也对但取值是ASCII码和你想表达的数字数组完全不是一回事。打印HEX出来一看就知道问题在哪。第二模式调用错误。CTR模式下加密和解密函数其实是同一个但CBC和ECB必须严格区分encrypt和decrypt调错了密文当然还原不回来。第三IV没有重置。CBC模式下库函数会更新内部IV状态解密前必须重新调用初始化函数把IV设置回原值用加密后的IV值去解密结果必然乱七八糟。排查时不要凭感觉猜我把每一步数据都打印出来然后和参考工具对比很快就能定位是哪一环节出了问题。5.3 安全建议与产品化注意点测试程序跑通之后要做成一个真正能上线的加密模块还有几件事必须处理。第一不要使用固定IV第二密钥最好由TRNG随机生成或者按芯片唯一ID派生第三启用STM32的读保护防止固件被直接抄读第四完整的加密协议远比单个AES算法复杂AES只是原语别试图自己发明加密协议。如果产品要走认证或合规路线协议设计和安全评估该请专业的人来做就请专业的人来做。代码层面还有两个优化方向一是开启了编译器O2优化后性能会明显提升二是如果对吞吐量不满意可以把算法里的查表数据放到对齐的静态数组中减少缓存加载次数。最后分享一点个人体会这个测试程序虽然小但它把AES从“听过名字”变成了“手里有数据”的一步。测完三种模式之后我把代码整理成了一个加密服务模块后续给两个项目复用了每次接入新主控只需要改底层打印接口。如果你也想在STM32上跑AES我建议照这个思路先跑通再说。遇到数据对不上不要慌八成是密钥或者IV的理解问题把每个中间结果都打印出来逐字节比对问题很快就能定位。本文还有配套的精品资源点击获取