STM32HAL库-UID实战:从读取到应用加密与设备标识
1. STM32芯片唯一码(UID)基础解析
第一次接触STM32的UID功能时,我盯着Datasheet里那串十六进制地址发懵——0x1FFFF7E8?这看起来像某种神秘代码。后来才发现,每个STM32芯片出厂时都内置了全球唯一的96位身份证号码,就藏在这些地址里。这串数字对嵌入式开发者来说简直是宝藏,从防抄袭加密到设备组网标识都离不开它。
不同STM32系列的UID存放位置就像不同品牌的保险箱,位置和开锁方式各有特点。F1系列藏在0x1FFFF7E8,F4系列躲在0x1FFF7A10,H7系列则位于0x1FF0F420。我整理了个地址对照表方便查阅:
| 芯片系列 | UID起始地址 | 数据宽度 |
|---|---|---|
| STM32F0 | 0x1FFFF7AC | 96位 |
| STM32F1 | 0x1FFFF7E8 | 96位 |
| STM32F4 | 0x1FFF7A10 | 96位 |
| STM32H7 | 0x1FF0F420 | 96位 |
读取时要注意STM32采用小端模式存储数据,就像倒着吃甘蔗——低字节在前,高字节在后。我第一次读取时没注意这点,结果打印出来的UID完全对不上号,调试了整整一个下午才发现问题。
2. 两种UID读取方法实战
2.1 直接地址访问法
最原始的读取方式就像直接撬开保险箱——通过指针操作访问特定内存地址。以STM32F103为例,代码简单粗暴:
uint32_t uid[3]; uid[0] = *(__IO uint32_t *)(0x1FFFF7E8); // 第一部分 uid[1] = *(__IO uint32_t *)(0x1FFFF7EC); // 第二部分 uid[2] = *(__IO uint32_t *)(0x1FFFF7F0); // 第三部分这种方法性能极高,但存在明显缺陷。去年我在一个多系列兼容项目中踩过坑:当代码从F1移植到F4平台时,由于地址不同导致读取失败。后来我改进成动态地址查询:
uint32_t GetUIDBase(MCUType type) { static const uint32_t addrTable[] = { [STM32F1] = 0x1FFFF7E8, [STM32F4] = 0x1FFF7A10, //...其他系列地址 }; return addrTable[type]; }2.2 HAL库API封装法
ST官方提供的HAL库就像给UID访问装了把智能钥匙,使用起来优雅多了:
void PrintUID_HAL(void) { printf("UID: %08X-%08X-%08X\n", HAL_GetUIDw0(), HAL_GetUIDw1(), HAL_GetUIDw2()); }实测对比两种方法:
- 执行效率:直接访问快3-5个时钟周期
- 代码可读性:HAL库完胜
- 跨平台兼容性:HAL库自动适配各系列
在STM32CubeIDE环境下,我推荐优先使用HAL库方案。但如果是极端追求性能的场景(如bootloader阶段),直接地址访问仍是首选。
3. UID在程序加密中的应用
3.1 防抄袭基础方案
最简单的加密思路就是比对UID白名单。我曾给客户做过这样的保护方案:
const uint32_t AUTHORIZED_UID[] = {0x12345678, 0x9ABCDEF0, 0x13579BDF}; void CheckLicense() { uint32_t currentUID[3]; HAL_GetUID(currentUID); for(int i=0; i<3; i++) { if(currentUID[i] != AUTHORIZED_UID[i]) { HAL_NVIC_SystemReset(); // 不匹配则复位 } } }但这种方式太容易被破解——反编译找到UID数组就完蛋。后来我升级成动态校验方案:
void AdvancedCheck() { uint32_t uid = HAL_GetUIDw0(); uint32_t key = (uid ^ 0x55AA55AA) + 0x12345678; if(key != 0x89ABCDEF) { // 示例密钥 EraseFlash(); // 自毁程序 } }3.2 结合加密算法进阶方案
更安全的做法是结合加密算法。我在工业控制器项目中使用SHA-256哈希方案:
#include "mbedtls/sha256.h" void GenerateDeviceKey(uint8_t* output) { uint32_t uid[3]; HAL_GetUID(uid); mbedtls_sha256_context ctx; mbedtls_sha256_init(&ctx); mbedtls_sha256_starts(&ctx, 0); mbedtls_sha256_update(&ctx, (uint8_t*)uid, 12); mbedtls_sha256_finish(&ctx, output); mbedtls_sha256_free(&ctx); }这样生成的256位密钥可以作为AES加密的种子,实现固件分片解密等高级功能。有个坑要注意:STM32的UID在某些系列中可能存在连续重复问题,建议混合其他芯片特征值(如Flash大小)作为盐值。
4. 生成设备唯一标识实战
4.1 MAC地址生成技巧
物联网设备常需要唯一MAC地址。我从UID派生MAC的标准做法:
void GenerateMAC(uint8_t mac[6]) { uint32_t uid = HAL_GetUIDw0(); mac[0] = 0x02; // 本地管理地址 mac[1] = (uid >> 16) & 0xFF; mac[2] = (uid >> 8) & 0xFF; mac[3] = uid & 0xFF; mac[4] = (uid >> 24) & 0x7F; // 确保最高位为0 mac[5] = (mac[1] + mac[2] + mac[3]) & 0xFF; }这种算法能保证:
- 符合IEEE 802标准
- 同一批芯片MAC不会冲突
- 通过最后一位校验防止规律性重复
4.2 设备序列号方案
给产品贴序列号标签时,我常用这种转换方法:
void UIDToSerial(char* serial) { uint32_t uid[3]; HAL_GetUID(uid); snprintf(serial, 20, "ST%08X%04X%04X", uid[0], uid[1] & 0xFFFF, uid[2] & 0xFFFF); }输出示例:"ST5A3F8E21003B4D2C" 既包含厂商代码又保证唯一性。有个实用技巧:对于需要打印的场合,可以转成Base64编码缩短长度:
#include "base64.h" char* GetShortSerial(void) { uint8_t uid[12]; memcpy(uid, (void*)UID_BASE, 12); return base64_encode(uid, 12); }5. 跨平台开发注意事项
5.1 系列兼容性处理
在多系列项目中,我总结出这套兼容方案:
typedef enum { STM32_UNKNOWN = 0, STM32F1, STM32F4, STM32H7 } MCU_Series; MCU_Series DetectMCU(void) { if(*(uint16_t*)0x1FFF7A10 == 0x1000) return STM32F4; if(*(uint16_t*)0x1FFFF7E8 == 0x2000) return STM32F1; return STM32_UNKNOWN; } uint32_t GetUIDAddress(void) { switch(DetectMCU()) { case STM32F1: return 0x1FFFF7E8; case STM32F4: return 0x1FFF7A10; default: return 0; } }5.2 安全读取建议
直接操作内存地址时要注意:
- 先检查地址是否合法
- 关闭中断防止被打断
- 对于H7系列需要先Cache无效化
uint32_t SafeReadUID(uint32_t addr) { __disable_irq(); #if defined(STM32H7) SCB_InvalidateDCache_by_Addr((uint32_t*)addr, 4); #endif uint32_t val = *(__IO uint32_t*)addr; __enable_irq(); return val; }在RTOS环境中更要小心竞争条件,建议使用互斥锁:
osMutexId_t uidMutex; void RTOS_UID_Init(void) { uidMutex = osMutexNew(NULL); } uint32_t RTOS_ReadUID(void) { osMutexAcquire(uidMutex, osWaitForever); uint32_t val = HAL_GetUIDw0(); osMutexRelease(uidMutex); return val; }6. 常见问题排查指南
6.1 UID读取异常排查
遇到过最诡异的BUG是UID读取全为0,后来发现是:
- 芯片未正确初始化时钟
- 地址写错成0x1FFFFFFF
- 优化等级过高导致读取被跳过
推荐调试步骤:
- 先验证能否读取Flash大小寄存器(0x1FFF7A22)
- 检查反汇编确认读取指令被正确生成
- 尝试-O0优化等级编译
6.2 加密方案失效分析
有个客户反馈加密突然失效,排查发现:
- 使用了低质量的克隆芯片,UID区域被改写
- 电源不稳定导致UID读取错误
- 工程中混用了不同系列的HAL库
解决方案:
- 增加UID校验和检查
- 添加重试机制
- 关键操作前先读取DEVID校验芯片型号
bool ValidateUID(void) { uint32_t uid[3]; HAL_GetUID(uid); uint32_t checksum = uid[0] ^ uid[1] ^ uid[2]; return (checksum != 0); // 简单校验示例 }7. 工程实践建议
7.1 CubeMX配置技巧
在CubeMX中配置UID相关功能时:
- 启用CRC模块(某些加密方案需要)
- 配置RTC作为随机数种子源
- 启用写保护功能防止UID被篡改
对于需要网络功能的项目,可以在MX中直接生成基于UID的MAC地址:
void MX_LWIP_Init(void) { uint8_t macaddr[6]; GenerateMAC(macaddr); netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, &tcpip_input); netif_set_hostname(&gnetif, "stm32_device"); netif_set_up(&gnetif); }7.2 代码架构设计
推荐的项目文件结构:
/Drivers /BSP bsp_uid.c bsp_uid.h /CMSIS /STM32xx_HAL_Driver /Middlewares /Src main.c在bsp_uid.h中声明统一接口:
typedef struct { uint8_t mac[6]; char serial[20]; uint32_t uid[3]; } DeviceInfo_t; void BSP_GetDeviceInfo(DeviceInfo_t* info); bool BSP_VerifyLicense(void);这种封装方式方便跨平台移植,后续更换芯片系列时只需修改底层实现。
