
调STM32WL33x的AES-GCM踩过坑的人应该都熟悉一个问题我明明按手册配了寄存器密钥、IV也都填了为什么输出来的ciphertext和tag不是乱码就是和预期对不上我第一次调GCM时也在这个问题上磨了一个晚上最后发现根本不是算法理解的问题而是硬件外设自己有一套“相位”状态机任何一个步骤的顺序错了输出的东西就是不对的。这篇东西我把STM32WL33x的AES外设跑GCM模式的完整思路拆开讲包括寄存器怎么配、AAD要在什么时候送进去、tag到底从哪个阶段读出来、字节序和最后一个不完整块怎么处理。适合正在用STM32WL3x做LoRa/Sub-GHz无线安全通信、固件升级校验、设备配对的嵌入式工程师也适合已经能跑通HAL例程但想弄明白内部到底是什么逻辑的人。我会尽量用实际调试验证过的顺序来讲而不是抄一遍参考手册。1. 为什么STM32WL33x的GCM会让好几个人怀疑人生1.1 GCM不是“CTR加密加一个MAC”这么简单很多人最开始理解AES-GCM会把它拆成“CTR模式加密数据再算个GHASH得到认证tag”。这个理解没错软件实现里也确实是这样两个并行链路。但在硬件加速器上事情就变得不太一样了因为片上AES外设只有一个数据通道它必须通过状态机来分时处理加密和认证两条链路。STM32WL33x的AES外设把整个GCM操作强行切成了几个固定阶段软件必须跟着这个节奏一步步走跳一步、少一步结果就乱了。我见过不少人用软件mbedTLS跑GCM跑得很熟换到硬件外设以后第一反应是去找“GCM Encrypt”这种一步到位函数。但硬件外设给的不是完整AEAD封装而是一组操作原语。你需要自己负责密钥加载、IV写入、AAD送入、分段切换和tag读出。缺了任何一环输出就不会是有效结果而且不会给你任何报错只能自己排查。1.2 硬件外设把一次GCM操作切成了四个阶段STM32WL33x的AES外设里有一个GCMPH字段用来表示当前处于GCM的哪个阶段阶段00初始阶段写入密钥和IV完成GHASH子密钥的预计算。阶段01头部阶段送入附加认证数据AADAAD只参与GHASH计算不产生密文。阶段10载荷阶段送入明文或密文每块数据经过AES运算后从数据寄存器读回。阶段11最终阶段处理完所有数据后从数据寄存器连续读出16字节tag。每个阶段的切换都必须在前一个阶段彻底完成后进行。判断“彻底完成”的标志是状态寄存器里的CCF位置位同时软件要把CCF清掉才能改GCMPH字段进入下一阶段。这里就是大多数问题的源头很多人写完AAD或者写完最后一个数据块忘了清标志就急着切换阶段结果硬件状态直接乱掉。1.3 为什么这套流程和PC上跑OpenSSL完全不一样在PC上用OpenSSL或者Python调AES-GCM只需要传一个key、iv、aad和明文一行代码就返回密文和tag。MCU的硬件外设做不到这一点因为硬件是给人“一步一块”操作设计的。你要自己维护状态自己处理块边界自己决定在哪一步读DOUTR。这种思维从“调用完整函数”切换到“操作状态机”是很多人最不适应的部分。如果你已经理解了上面这些后面的内容就顺了。接下来我从寄存器级开始把每个细节展开帮你在STM32WL33x上真正产出有效的密文和tag。2. 开始之前搞清AES外设的字节序、寄存器图和相位2.1 先把AES外设的寄存器地图对齐STM32WL33x的AES外设寄存器并不复杂主要就是下面这几个寄存器作用AES_CR控制寄存器配置模式、数据宽度、密钥长度、相位、使能AES_SR状态寄存器核心标志是CCF计算完成AES_DINR数据输入寄存器32位写入待处理数据AES_DOUTR数据输出寄存器32位读取处理结果AES_KEYR0~3密钥寄存器128位密钥用0~3256位密钥用0~7AES_IVR0~3初始化向量寄存器GCM通常只使用96位即IVR0~2配置流程上第一步是打开AES外设时钟然后设置控制寄存器的基本参数。要注意的是不同系列甚至不同型号的AES外设寄存器字段名长得差不多但位置可能不完全一样所以代码里的宏名务必以当前工程对应的头文件为准。比如AES_CR_CHMOD、AES_CR_GCMPH这些字段在STM32Cube的头文件里一定有定义但取值本身不要拿网上的老代码直接套。2.2 字节序问题你的uint8_t数组和寄存器之间的映射这是GCM调不通的另一个重灾区。STM32内核是32位小端而AES运算本身的标准字节序是大端。说得再直白一点你在代码里定义了一个uint8_t key[16]内存里低地址放的是数组第0个字节但当你用uint32_t指针去强转时低地址字节会成为这个uint32_t的最低有效字节。AES外设的数据寄存器是32位的所以数据进去以后怎么解释完全取决于AES_CR里的DATATYPE字段。我在实际调试中的建议是先保持一个最简单的配置把DATATYPE设为不做任何交换的那档然后老老实实按内存顺序写入。什么意思呢就是用memcpy把uint8_t数组拷贝进临时uint32_t变量再把变量写给寄存器。不要自己手写“大端转小端”的宏因为你以为的转换方向很可能和硬件预期刚好相反。调完一个标准测试向量之后如果发现输出和你参考结果逐字相反再去翻DATATYPE的另一个选项。大多数时候问题不在DATATYPE而在你写数据之前做了多余的手工字节交换。2.3 IV、AAD、数据块、Tag谁该在什么阶段进哪个寄存器IV96位IV分3次写入AES_IVR0、AES_IVR1、AES_IVR2AES_IVR3固定写0。不要把12字节IV用标准大端方式“先转成3个uint32”再写这里和上面字节序逻辑一样直接memcpy。AAD在头部阶段写入AES_DINR。AAD按16字节一块处理长度不足16字节时需要补0。你可能会担心这样会不会影响认证结果不用担心硬件内部计算GHASH时会按实际的AAD长度处理补0只是为了让外设按完整块工作。明文/密文在载荷阶段写入AES_DINR每写一块就等待CCF置位然后从AES_DOUTR读回一块结果。加密时写明文读密文解密时写密文读明文。Tag在所有数据处理完后把GCMPH切到最终阶段等CCF置位然后连续读4次AES_DOUTR得到16字节tag。如果只需要8字节或12字节tag从输出里截断即可硬件永远给你算完整的128位。2.4 用NIST测试向量作为唯一的“有效判据”调GCM的时候最忌讳用随机数据或者自己随便编的报文来核对结果。因为一旦结果不对你根本分不清是密钥字节序错、IV配置错、AAD漏送还是tag读取顺序错。我每次都先用NIST公开测试向量做最小验证。这里给一个最简单的例子密钥和IV全是0参数值Key00000000000000000000000000000000IV000000000000000000000000AAD空明文16字节00000000000000000000000000000000预期密文0388dace60b6a392f328c2b971b2fe78预期Tagab6e47d42cec13bdf53a67b21257bddf这个向量在NIST官方文档里是公开的不用联网也能查。你先拿这个例子把整个流程跑通再去处理自己业务里的真实数据。如果连这个固定向量都跑不对后面就不用往下测了老老实实对照寄存器流程一条条查。3. 实操从0到1产出有效密文和Tag3.1 用寄存器实现GCM加密的核心流程下面这段代码是寄存器级操作目标是把NIST那个固定向量跑通。我需要强调一下这个代码里的宏名我在写的时候用的是当前STM32Cube环境中的常见命名但不同版本可能有细微差异你拿到自己工程里字段名一律以头文件为准。#include stm32wl33x.h #include string.h static void aes_wait_ccf(void) { /* 等待CCF置位 */ while (!(AES-SR AES_SR_CCF)) ; /* 清CCF。不同系列清法可能不同这里使用CR里的CCFC位 */ AES-CR | AES_CR_CCFC; } void gcm_encrypt_nist_test(void) { /* 测试向量里的字节按内存数组顺序定义 */ uint8_t key[16] {0}; uint8_t iv[12] {0}; uint8_t plain[16] {0}; uint8_t cipher[16] {0}; uint8_t tag[16] {0}; uint32_t tmp; /* 1. 使能AES时钟并复位外设 */ __HAL_RCC_AES_CLK_ENABLE(); AES-CR 0; /* 2. 配置GCM模式、128位密钥并直接使能外设 * GCMPH默认是00即初始化阶段 */ AES-CR AES_CR_CHMOD_GCM | AES_CR_DATATYPE_0 | AES_CR_EN; /* 3. 写入密钥用memcpy保持内存字节序 */ memcpy(tmp, key 0, 4); AES-KEYR0 tmp; memcpy(tmp, key 4, 4); AES-KEYR1 tmp; memcpy(tmp, key 8, 4); AES-KEYR2 tmp; memcpy(tmp, key 12, 4); AES-KEYR3 tmp; /* 4. 写入96位IV */ memcpy(tmp, iv 0, 4); AES-IVR0 tmp; memcpy(tmp, iv 4, 4); AES-IVR1 tmp; memcpy(tmp, iv 8, 4); AES-IVR2 tmp; AES-IVR3 0; /* 初始化阶段结束 */ aes_wait_ccf(); /* 5. 本测试向量AAD为空直接进入载荷阶段 */ AES-CR (AES-CR ~AES_CR_GCMPH) | (2U 8); /* GCMPH 10 */ /* 明文刚好16字节写一块读一块 */ memcpy(tmp, plain 0, 4); AES-DINR tmp; memcpy(tmp, plain 4, 4); AES-DINR tmp; memcpy(tmp, plain 8, 4); AES-DINR tmp; memcpy(tmp, plain 12, 4); AES-DINR tmp; aes_wait_ccf(); tmp AES-DOUTR; memcpy(cipher 0, tmp, 4); tmp AES-DOUTR; memcpy(cipher 4, tmp, 4); tmp AES-DOUTR; memcpy(cipher 8, tmp, 4); tmp AES-DOUTR; memcpy(cipher 12, tmp, 4); /* 6. 切换到最终阶段并读tag */ AES-CR (AES-CR ~AES_CR_GCMPH) | (3U 8); /* GCMPH 11 */ aes_wait_ccf(); tmp AES-DOUTR; memcpy(tag 0, tmp, 4); tmp AES-DOUTR; memcpy(tag 4, tmp, 4); tmp AES-DOUTR; memcpy(tag 8, tmp, 4); tmp AES-DOUTR; memcpy(tag 12, tmp, 4); }跑完这段cipher应该是0388dace60b6a392f328c2b971b2fe78tag应该是ab6e47d42cec13bdf53a67b21257bddf。如果你看到的字节顺序刚好是反的那多半是DATATYPE设置问题或者你读取后将每个32位字内的字节顺序理解反了。3.2 最后一个不完整数据块的NBLW处理测试向量都是整块数据但业务数据几乎不可能永远是16的倍数。STM32WL33x的AES外设对最后一块的处理要求你在送最后一个块之前把该块的有效字节数告诉外设这就要用到控制寄存器里的NBLW字段。这里的细节不同系列略有差异有的手册里NBLW表示“最后一个块有效字节数减一”有的表示“有效位减一”所以我给不了你一个通吃所有型号的固定值。你一定要打开当前芯片的参考手册找到NBLW这一位段的说明再结合最后一个块的实际长度计算。一个比较稳的做法是先把aes_wait_ccf()做完在准备写最后一块数据前先把NBLW字段设好再写DINR。如果