简介面向嵌入式开发者的 STM32F4 AES 加解密测试程序基于 Cortex-M4 内核演示 AES 对称加密并集成 PKCS7 填充与去填充逻辑保证数据加解密前后的完整性该算法效率高、安全性强常用于金融交易、移动通信等敏感数据场景。程序通过串口一PB6、PB7接收不定长数据能够灵活处理外部异步通信适合正在学习 STM32 加密外设或需要快速落地数据保护方案的开发者。压缩包共 775 个文件、21.3MB包含 368 个 C 源码、142 个头文件以及汇编启动文件、HAL 库、Keil 工程配置、链接脚本和可直接烧录的 hex/axf 镜像目录与标准工程一致。资源已有 258 人学习下载工程内涵盖 CRYP 加解密驱动、定时器与 FFT 算法代码可帮助读者理清 AES 初始化、数据填充、串口中断接收的完整链路并作为实际项目中加密通信的可复用参考。1. 串口明文裸奔的日子该结束了一个F4 AES加解密测试工程够不够用做嵌入式联调时拿逻辑分析仪抓一下串口配置命令、固件升级包全是明文地址和协议内容一目了然。某开发者手上正好有一块 STM32F4 开发板于是配了一个 AES-128 加解密测试程序串口先接收不定长数据再对数据做 AES 加密或解密把结果从串口发回去。这个工程同时覆盖了 STM32F4 串口接收不定长数据与 AES 加解密测试两个场景测试帧里带了帧头、命令字、长度和 CRC8 校验拿到手可以直接用串口助手发十六进制帧验证。适合刚开始接触嵌入式加密、想把 AES 流程在 F4 上完整跑一遍的开发者。2. 技术选型与工程骨架为什么是软件AES加DMA空闲中断2.1 F4 上没有硬件 AES软件实现是绕不开的选择STM32F4 系列F405/F407/F427 这一代内部没有 AES 硬件外设硬件加解密单元从 F7、H7 系列才开始集成。也就是说在这块板子上做 AES 只能走软件实现。工程里用的是一份非常轻量的开源 AES 实现整个库只有aes.h和aes.c两个文件支持 AES-128/192/256接口极简编译后只占几 KB Flash很适合单片机环境。对比另外两个常见方案方案体积速度上手难度适用场景mbedTLS 完整移植较大快中需要 TLS、证书等完整加密栈轻量 AES 单文件库小较快低只需要 AES-ECB/CBC 加解密自己按 FIPS-197 实现可控取决于优化高学习算法原理生产不推荐工程选的是第二个方案。测试程序的核心诉求是快速验证加解密链路不需要 TLS 那套复杂依赖。AES 轮变换是固定流程——SubBytes、ShiftRows、MixColumns、AddRoundKey轻量库的代码量大约两三百行调试起来也直观出问题可以直接翻到具体轮函数。用软件 AES 有一个心理准备速度远不如硬件外设。F4 主频 168MHz 时软件跑 AES-128 加密 16 字节大约在几十微秒量级做测试和低频业务完全够用。如果将来业务要求每秒处理几十 MB 数据再考虑换带硬件 AES 的芯片。2.2 串口不定长接收三种方案选 DMA 加 IDLE 中断串口接收不定长数据是另一个坑。最笨的办法是每收一个字节进一次接收中断在中断里判断是否空闲数据量大时 CPU 占用率直接拉满。常见的做法有三种方案实现方式优点缺点逐字节中断 超时判断每字节进中断超时认为帧结束逻辑简单CPU 占用高高速率下易丢字节定时器超时检测定时器配合接收中断帧边界准确定时器配置和中断交互复杂DMA UART IDLE 空闲中断DMA 连续接收总线空闲触发中断CPU 几乎零负担需要处理 DMA 与 IDLE 的配合细节这个工程用的是第三种DMA 把串口数据搬到内存缓冲区当串口总线上出现空闲一个字节时间没有新数据时硬件触发 IDLE 中断。在 IDLE 中断里通过 DMA 剩余计数算出这一帧收了多少字节置标志位后交给主循环处理。CPU 只在帧结束时参与一次效率远高于逐字节中断方案。这里有个容易踩的细节DMA 接收模式要配成 Normal单次不能配 Circular。Normal 模式在收到一帧后 DMA 传输完成并停止方便在 IDLE 回调里用__HAL_DMA_GET_COUNTER计算剩余字节数Circular 模式计数器会自动重装长度计算会出错。2.3 目录结构和一帧数据的完整命运工程是标准 STM32CubeMX 生成的结构加解密和校验模块单独拎出来放STM32F4_AES_Test/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── usart.h │ │ ├── aes_wrapper.h │ │ └── crc8.h │ └── Src/ │ ├── main.c │ ├── usart.c │ ├── aes_wrapper.c │ └── crc8.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32F4xx_HAL_Driver/ ├── aes_lib/ │ ├── aes.h │ └── aes.c ├── MDK-ARM/ └── STM32F4_AES_Test.iocaes_wrapper.c是自封装的加解密接口屏蔽了底层 AES 库的细节crc8.c负责帧校验usart.c管 DMA 配置和 IDLE 回调main.c里跑主循环协议解析。aes_lib目录单独放第三方 AES 实现方便以后整体替换成硬件加密库。一帧数据从进入到返回的完整链路是串口硬件收到字节流并写入 DMA 缓冲区总线空闲触发 IDLE 中断中断里计算帧长度并停止 DMA主循环读到rx_frame_flag后解析帧头、命令字、长度调用 AES 模块对数据区加密或解密最后组帧走 DMA 发送回上位机。3. 核心代码拆解从IDLE回调到AES输出的完整链路3.1 AES 封装层把第三方库藏在一层接口后面先看aes_wrapper.c。之所以不直接在主循环里调 AES 库函数是为了以后替换加密库时只改这一个文件// aes_wrapper.c #include aes_wrapper.h #include aes.h static AES_CTX aes_ctx; // 测试工程固定使用全 0 密钥拿到工程可自行修改 static const uint8_t test_key[16] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; void aes_wrapper_init(void) { AES_init_ctx(aes_ctx, test_key); } // 注意len 必须是 16 的倍数调用方负责补齐 void aes_wrapper_ecb_encrypt(uint8_t *buf, uint32_t len) { for (uint32_t i 0; i len; i 16) { AES_ECB_encrypt(aes_ctx, buf i); } } void aes_wrapper_ecb_decrypt(uint8_t *buf, uint32_t len) { for (uint32_t i 0; i len; i 16) { AES_ECB_decrypt(aes_ctx, buf i); } }AES_CTX是第三方库定义的上下文结构体内部保存密钥扩展后的轮密钥。AES_init_ctx做密钥扩展把 16 字节密钥展开成 11 组轮密钥供加密轮函数使用AES_ECB_encrypt对传入的单个 16 字节块做加密。循环里每次步进 16 字节把所有块依次加密。测试密钥写死在代码里方便对照网上在线的 AES 计算工具验证结果。实际产品里密钥一般不会这样放后面二次开发会讨论如何把密钥改成动态下发。函数注释里强调了一点调用方必须保证len是 16 的倍数。AES 是分组算法明文不足 16 字节时要么补零要么补 PKCS7 填充否则循环会漏掉尾部数据。测试程序里加密命令收到数据后统一补零到 16 的倍数解密命令则要求数据本身已经是合法的 16 字节倍数。3.2 串口 DMA 与 IDLE 回调一帧数据如何被完整接住串口初始化由 CubeMX 生成关键代码在main.c和usart.c里。启动接收的代码这样写// main.c初始化完成后立即启动 DMA 接收 uint8_t rx_buf[512]; volatile uint16_t rx_len 0; volatile uint8_t rx_frame_flag 0; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_USART1_UART_Init(); // 使能 UART 空闲中断并在 DMA 模式下开始接收 __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); HAL_UART_Receive_DMA(huart1, rx_buf, sizeof(rx_buf)); while (1) { if (rx_frame_flag) { rx_frame_flag 0; frame_process(); memset(rx_buf, 0, sizeof(rx_buf)); HAL_UART_Receive_DMA(huart1, rx_buf, sizeof(rx_buf)); } } }HAL_UART_Receive_DMA启动后串口的每个字节都由 DMA 自动搬到rx_buf不需要 CPU 介入。UART_IT_IDLE是串口空闲中断当最后一个字节接收完成后总线上超过一个字节时间没有新数据硬件就认为这一帧结束了并触发中断。HAL 库的 IDLE 中断回调是个弱函数在usart.c里重写// usart.c void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { // DMA 剩余未搬运的字节数 uint16_t remain __HAL_DMA_GET_COUNTER(huart-hdmarx); rx_len sizeof(rx_buf) - remain; rx_frame_flag 1; // 通知主循环有完整帧 __HAL_UART_CLEAR_IDLEFLAG(huart); // 清 IDLE 标志否则不会再触发 HAL_UART_DMAStop(huart1); // 停 DMA等主循环处理完再重启 } }__HAL_DMA_GET_COUNTER返回 DMA 控制器还剩多少字节没搬。缓冲区总大小减去剩余值就是实际收到的字节数。这个技巧比在中断里逐字节计数省事得多。回调里只做两件事记长度、置标志位然后停 DMA 等主循环处理。不能在中断里直接处理数据因为 AES 加解密耗时较长会拖住中断导致下一次接收丢失。主循环看到标志位置位后调用frame_process()完成协议解析和加解密清空缓冲区再重新启动 DMA 接收。注意memset和HAL_UART_Receive_DMA的顺序不能反先清缓冲区再重启 DMA否则旧数据还没清空 D本文还有配套的精品资源点击获取