简介这份资源提供基于C语言实现的AES 128位CBC模式加密解密代码并已在MSP430F149超低功耗单片机上验证通过面向嵌入式开发、安全通信及密码学入门的学习者与工程师帮助解决资源受限环境下数据加密与移植适配的问题。压缩包共4个文件包含2个c源文件与2个h头文件整体约8KB源码分别承担AES核心算法、CBC模式加解密及接口声明等职责结构精简便于阅读与二次开发。目前已有1447人学习下载说明其在嵌入式加密方向具有一定参考价值。读者可从中掌握字节代换、行移位、列混淆与轮密钥加等AES核心步骤理解CBC模式中初始化向量与密文异或的工作机制并借鉴将密码学算法移植到MSP430F149等低功耗平台的实现思路为安全通信与数据保护类项目提供可复用的代码基础。1. 从一块跑在 MSP430F149 上的 AES 128 CBC 代码说起如果你正在用 MSP430F149 做低功耗采集终端又恰好需要把上行数据做一次对称加密那大概率绕不开 AES 128 CBC。这块 16 位 MCU 只有 2KB RAM、60KB Flash主频通常跑在 8MHz 上下没有硬件加密外设也没有 FPU所有轮运算都得靠软件在寄存器里抠出来。网上能搜到的 AES 代码大多是 PC 端或者带硬件加速的 ARM 版本直接搬到 MSP430 上要么编译不过要么跑一次要几百毫秒要么 RAM 直接爆掉。这份基于 C 语言的 AES 128 CBC 加密解密代码就是针对 MSP430F149 这种资源受限场景写的已经在真实板子上验证通过加解密结果和标准测试向量对得上。它适合谁适合做低功耗无线传感节点、工业现场采集器、电池供电仪表这类产品的嵌入式工程师尤其是那些不想引入庞大加密库、只想把一份干净 C 代码塞进工程里的人。下面我按“这是什么、怎么用、坑在哪”的顺序把这份代码拆开讲清楚。2. AES 128 CBC 在 MSP430F149 上的实现原理与资源账2.1 为什么选 CBC 而不是 ECB以及 128 位密钥的取舍AES 本身是分组密码分组长度固定 128 位密钥长度可选 128、192、256。在 MSP430F149 上密钥长度直接决定轮数和密钥扩展的 RAM 占用。AES 128 是 10 轮密钥扩展后需要 176 字节的轮密钥空间AES 256 是 14 轮轮密钥要 240 字节。对于只有 2KB RAM 的芯片176 字节已经不算小再叠上 CBC 的 IV、输入输出缓冲和栈空间余量就很紧张了。所以这份代码选 AES 128 是合理的工程妥协。工作模式上ECB 对相同明文块产生相同密文块在传感数据这种有大量重复结构比如固定帧头、递增计数值的场景里等于把规律直接暴露出去。CBC 通过前一块密文和当前明文异或把相同明文打散代价只是多一个 16 字节 IV 和一次异或操作。在 MSP430 上这次异或几乎不增加多少周期却能把模式安全性提一个档次所以 CBC 是这类低功耗节点的常见选择。需要提醒的是CBC 只提供机密性不提供完整性。如果你需要防篡改还得额外加 MAC 或者用 CCM/GCM 模式但那些模式在 MSP430F149 上开销更大这份代码没有包含属于边界之外的事。2.2 轮运算在 16 位 MCU 上的展开方式与查表策略AES 的四个基本操作是 SubBytes、ShiftRows、MixColumns、AddRoundKey。在 PC 上通常用 T 表或者 AES-NI 指令但在 MSP430 上没有这些。这份代码采用的是字节代换查表加列混合直接计算的方式。S 盒是一个 256 字节的常量数组放在 Flash 里用const修饰不占 RAM。列混合涉及 GF(2^8) 上的乘 2 和乘 3代码里用xtime函数配合异或实现避免再开一张 256 字节的表。轮运算的循环结构是先 AddRoundKey然后 9 轮完整的 SubBytes、ShiftRows、MixColumns、AddRoundKey最后一轮去掉 MixColumns。这种写法比全展开省 Flash比全循环省时间属于折中。在 8MHz 主频下一次 128 位分组加密大约在 1.5 到 2 毫秒之间具体取决于编译器优化等级和是否开-O2。解密时因为要逆列混合和逆 S 盒会稍慢一点但差距不大。2.3 内存布局栈、全局缓冲和 IV 的存放位置MSP430F149 的 RAM 只有 2KB栈通常放在 RAM 顶端向下增长。这份代码里轮密钥RoundKey[176]和状态数组State[16]如果都放局部变量栈很容易溢出。常见做法是把轮密钥和状态数组声明为全局静态变量或者放在一个固定的 RAM 段里。IV 是 16 字节CBC 加密时每次会话应该随机生成但在 MSP430 上没有硬件随机数发生器通常用 ADC 采样噪声或者定时器抖动来凑这份代码只负责使用 IV不负责生成 IV生成部分需要你自己补。输入输出缓冲方面如果你要加密的数据长度不是 16 的整数倍需要做填充。PKCS#7 填充是最常见的但会增加代码量。这份代码假设输入已经是 16 字节对齐的或者由调用者自己处理填充。这一点在移植时要特别注意否则最后一块会读越界。3. 把代码跑起来从工程配置到第一个测试向量3.1 文件结构与移植到 IAR/CCS 的最小步骤这份代码通常包含三个文件aes.h、aes.c和main.c里的测试入口。aes.h里声明了AES_Init、AES_Encrypt、AES_Decrypt三个函数以及 S 盒和逆 S 盒的extern声明。aes.c里是具体实现包括密钥扩展、列混合、逆列混合等。移植到 IAR Embedded Workbench 或者 CCS 时先把aes.c和aes.h加入工程然后在项目设置里把优化等级调到-O2或者-O3同时注意把const数据放到 Flash 段不要被链接器塞进 RAM。具体步骤是新建一个 MSP430F149 工程把aes.c和aes.h复制到工程目录在main.c里#include aes.h然后调用AES_Init传入 16 字节密钥再调用AES_Encrypt传入明文和输出缓冲。编译前检查一下链接器配置文件确保.const段映射到 Flash 地址范围RAM 段没有溢出。如果用的是 CCS还要在预定义宏里加上__MSP430F149__否则寄存器定义可能对不上。3.2 密钥扩展与 CBC 初始化的代码走读密钥扩展是 AES 的第一步把 16 字节密钥扩展成 176 字节轮密钥。代码里通常用一个KeyExpansion函数完成核心是RotWord、SubWord和Rcon异或。下面这段是密钥扩展的关键部分我加了注释说明每一步在做什么。// 密钥扩展从 16 字节密钥生成 176 字节轮密钥 void KeyExpansion(const uint8_t *key, uint8_t *roundKey) { uint8_t temp[4]; uint8_t i, j; // 前 16 字节直接复制原始密钥 for (i 0; i 16; i) { roundKey[i] key[i]; } // 后续 160 字节按 4 字节一组扩展 for (i 4; i 44; i) { // 取上一组 4 字节 temp[0] roundKey[(i - 1) * 4 0]; temp[1] roundKey[(i - 1) * 4 1]; temp[2] roundKey[(i - 1) * 4 2]; temp[3] roundKey[(i - 1) * 4 3]; if (i % 4 0) { // 每 4 组做一次 RotWord SubWord Rcon uint8_t t temp[0]; temp[0] Sbox[temp[1]] ^ Rcon[i / 4]; temp[1] Sbox[temp[2]]; temp[2] Sbox[temp[3]]; temp[3] Sbox[t]; } // 与 i-4 组异或得到新组 roundKey[i * 4 0] roundKey[(i - 4) * 4 0] ^ temp[0]; roundKey[i * 4 1] roundKey[(i - 4) * 4 1] ^ temp[1]; roundKey[i * 4 2] roundKey[(i - 4) * 4 2] ^ temp[2]; roundKey[i * 4 3] roundKey[(i - 4) * 4 3] ^ temp[3]; } }这段代码里Sbox是 256 字节的常量数组Rcon是 10 个轮常量的数组。i % 4 0的判断决定了什么时候做 RotWord 和 SubWord。注意Rcon的下标是i / 4从 1 开始所以Rcon[0]通常不用或者置 0。密钥扩展只需要做一次结果存在roundKey数组里后续加解密都复用不要每次加密都重新扩展否则在 MSP430 上会白白多花几毫秒。CBC 初始化时除了密钥扩展还要准备 IV。IV 是 16 字节加密时和第一个明文块异或解密时和第一个密文块异或后得到第一个明文块。代码里通常把 IV 作为参数传入AES_Encrypt和AES_Decrypt或者存在一个全局数组里。注意加密和解密必须用同一个 IV否则第一个块就解错了。3.3 用标准测试向量验证加解密结果移植完成后第一件事不是直接上业务数据而是用 NIST 的标准测试向量跑一遍。AES 128 CBC 有一个经典的测试向量密钥2b7e151628aed2a6abf7158809cf4f3cIV000102030405060708090a0b0c0d0e0f明文6bc1bee22e409f96e93d7e117393172a期望密文7649abac8119b246cee98e9b12e9197d。你可以在main.c里写一个测试函数把这几组数据传进去然后通过串口打印结果或者用调试器看内存。// 标准测试向量验证 void TestAES(void) { uint8_t key[16] {0x2b,0x7e,0x15,0x16,0x28,0xae,0xd2,0xa6, 0xab,0xf7,0x15,0x88,0x09,0xcf,0x4f,0x3c}; uint8_t iv[16] {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07, 0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f}; uint8_t plain[16] {0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96, 0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a}; uint8_t cipher[16]; uint8_t decrypt[16]; uint8_t roundKey[176]; KeyExpansion(key, roundKey); AES_Encrypt(plain, cipher, roundKey, iv); AES_Decrypt(cipher, decrypt, roundKey, iv); // 比较 cipher 和期望值decrypt 和 plain // 如果一致说明移植成功 }跑完这个测试如果cipher和期望密文一致decrypt和原始明文一致说明代码逻辑没问题。如果不一致先检查 S 盒和逆 S 盒有没有写错再检查列混合的xtime实现最后看字节序有没有搞反。MSP430 是小端模式但 AES 标准里字节是按大端顺序处理的这一点在写状态数组时容易翻车。4. 避坑与排查MSP430F149 上跑 AES 的五个血泪经验4.1 现象加密结果对解密结果错一半原因逆列混合的xtime实现和正列混合共用了一个函数但逆列混合需要乘 9、11、13、14不是简单的乘 2 和乘 3。如果直接复用xtime而没有做多次异或解密时中间轮的状态就错了表现为前几个字节对后面全乱。解决单独写一个InvMixColumns函数用xtime组合出乘 9、11、13、14 的运算。或者直接查逆列混合表但那样会多占 256 字节 Flash。在 MSP430F149 上Flash 比 RAM 宽裕如果实在算不过来查表也是可接受的。4.2 现象程序跑几次就死机看门狗复位原因RoundKey[176]和State[16]都声明成了局部变量栈空间不够。MSP430F149 的栈默认只有几十到一百多字节176 字节的数组直接压栈必溢出。解决把轮密钥和状态数组改成全局静态变量或者用static修饰放在.bss段。如果多个任务共用还要加互斥保护但裸机环境下一般不需要。改完之后用调试器看一下 RAM 使用量留出至少 200 字节余量给栈。4.3 现象串口打印的密文和 PC 端工具对不上原因PC 端工具通常按十六进制字符串显示而 MSP430 串口打印的是原始字节。如果你直接把字节通过printf(%02x)打印看起来应该一样但如果串口配置成了 7 位数据位或者奇偶校验高位可能被截掉导致显示不一致。解决先确认串口是 8 位数据位、无校验、波特率匹配。然后在打印前把密文转成十六进制字符串再逐字节发送。另外注意 MSP430 的 UART 发送缓冲如果连续发送太快可能会丢字节加一点延时或者用中断发送。4.4 现象加密速度慢一次要十几毫秒原因编译时没有开优化或者 S 盒没有加const被链接器放到了 RAM 里每次查表都要从 RAM 读速度比 Flash 慢。另外如果xtime函数没有内联函数调用开销在 16 位 MCU 上也很可观。解决在 IAR 或 CCS 里把优化等级调到-O2以上给 S 盒加const关键字给xtime加inline或者用宏展开。如果还慢可以把列混合也做成查表但会多占 Flash。实测在 8MHz 下优化后一次加密可以压到 1.5 毫秒左右。4.5 现象IV 每次都用同一个被质疑安全性原因CBC 的 IV 不需要保密但必须不可预测。如果每次加密都用固定的 IV相同明文在相同密钥下会产生相同密文和 ECB 的弱点类似。解决每次会话生成一个随机 IV随密文一起发送。在 MSP430F149 上可以用 ADC 采样悬空引脚的热噪声或者用定时器捕获外部抖动的值凑出 16 字节。如果实在没有随机源至少用计数器加时间戳做种子通过一个简单的哈希函数生成 IV。接收方拿到 IV 后先解密第一个块再拿它和后续块异或。5. 进阶技巧把 AES 128 CBC 塞进低功耗采集流程的实操细节5.1 分块加密与 DMA 配合减少 CPU 占用在低功耗采集节点里CPU 大部分时间在 LPM3 睡觉只有采集和发送时醒来。如果你一次要加密几百字节的数据连续调用AES_Encrypt会让 CPU 长时间处于活动状态功耗上去了。常见做法是把数据分成 16 字节一块每加密完一块就触发一次 DMA 把密文搬到 UART 发送缓冲然后 CPU 回去睡觉等 DMA 中断再醒来处理下一块。这样 CPU 活动时间被切碎平均功耗能降不少。具体实现时AES_Encrypt每次只处理一个块CBC 的链式关系要求上一块的密文作为下一块的 IV。你可以在全局维护一个currentIV[16]每加密完一块就把密文复制进去下一块调用时传入这个currentIV。注意解密时顺序相反要先保存当前密文块再用它解密下一块。5.2 用 Flash 常量表换 RAM 的取舍MSP430F149 的 Flash 有 60KBRAM 只有 2KB。S 盒和逆 S 盒各 256 字节如果都放 RAM直接吃掉四分之一。所以一定要加const让链接器把它们放到 Flash 的.const段。列混合的乘 2 和乘 3 可以用xtime算也可以查表但查表要多 512 字节 Flash。在 Flash 充裕、RAM 紧张的情况下查表换速度是划算的。你可以根据实际工程剩余空间决定。5.3 验证方法用已知明文攻击自己的实现写完代码后除了跑标准测试向量还可以自己构造一组数据用 PC 端的 OpenSSL 或者 Python 的cryptography库加密同样的明文和密钥然后把密文和 MSP430 的输出逐字节对比。如果一致说明实现正确。如果不一致先检查填充方式再检查 IV 和密钥的字节序。Python 里可以用AES.new(key, AES.MODE_CBC, iv)来生成参考密文注意 Python 默认用 PKCS#7 填充而你的代码可能没有填充所以明文长度要凑成 16 的整数倍。# PC 端参考实现用于对比 MSP430 输出 from Crypto.Cipher import AES key bytes.fromhex(2b7e151628aed2a6abf7158809cf4f3c) iv bytes.fromhex(000102030405060708090a0b0c0d0e0f) plain bytes.fromhex(6bc1bee22e409f96e93d7e117393172a) cipher AES.new(key, AES.MODE_CBC, iv) print(cipher.encrypt(plain).hex()) # 输出应该和 MSP430 串口打印的密文一致从那以后我每次移植加密代码都强制先跑标准测试向量再用 PC 端工具交叉验证一遍两关都过了才敢往业务数据上接。希望帮到你。本文还有配套的精品资源点击获取