当前位置: 首页 > news >正文

STM32F103唯一ID读取实战:原理、代码实现与高级应用

1. 项目概述:为什么需要读取STM32的“身份证”?

在嵌入式开发中,尤其是涉及产品量产、软件授权、设备身份识别或防抄板等场景时,我们常常需要一个独一无二、无法篡改的硬件标识。这个标识,就像是芯片的“身份证”。对于STM32系列单片机而言,这个“身份证”就是它的唯一设备标识符。今天,我们就来深入聊聊如何在STM32F103这款经典且应用广泛的芯片上,准确、可靠地读取这个ID。

你可能会有疑问:我直接用个EEPROM存个序列号不就行了?为什么非要读芯片内部的ID?这里面的区别和意义可大了。首先,芯片内部的唯一ID是在生产过程中由ST公司激光刻录或固化在芯片内部的,对于每一颗STM32都是全球唯一的,无法被用户修改或擦除。这保证了标识的绝对唯一性和不可伪造性。其次,它不占用任何用户存储空间(Flash或RAM),是芯片的“原生属性”。基于这个ID,我们可以实现许多高级功能:比如生成绑定此硬件的软件激活码,防止程序被复制到其他板子上运行;在物联网应用中,作为设备的唯一MAC地址或设备标识符;在量产测试中,自动记录每块板子对应的芯片信息,实现精准溯源。

STM32F103的ID存储在一个特定的内存地址区域,读取它本质上就是去访问这个只读的内存区域。听起来简单,但实际操作中,从地址的理解、数据的读取到格式的转换,每一步都有需要注意的细节。接下来,我将带你从原理到实践,完整走一遍这个过程,并分享我在实际项目中积累的实操经验和避坑指南。

2. 核心原理与存储结构深度解析

要正确读取ID,首先得知道它放在哪里、长什么样。STM32F103的唯一ID是一个96位(即12字节)的数据。根据ST官方参考手册,这个ID的存储地址是0x1FFFF7E80x1FFFF7F4(不同STM32系列地址可能不同,F103是这个)。这96位数据并不是随意排列的,它包含了芯片的多种信息。

2.1 ID地址映射与数据含义

我们通过指针直接访问这个地址区域。具体来说:

  • 0x1FFFF7E8开始的4个字节(32位)通常与芯片的晶圆批次相关。
  • 0x1FFFF7EC开始的4个字节是芯片在晶圆上的X坐标。
  • 0x1FFFF7F0开始的4个字节是芯片在晶圆上的Y坐标。

这X、Y坐标的组合,保证了在同一个晶圆批次内,每颗芯片的ID也是唯一的。当我们读取时,通常将这12个字节作为一个整体,即一个96位的唯一标识符。在程序中,我们常将其转换为一个16字节的十六进制字符串(因为每个字节需要两个十六进制字符表示)或者一个24字节的ASCII字符串(如果你用%02X格式输出)。

注意:虽然手册给出了地址,但强烈建议使用ST标准外设库或HAL库中定义的宏来访问,而不是直接写死0x1FFFF7E8。例如,在标准外设库中,这个地址通常被定义为(uint32_t*)0x1FFFF7E8或通过UID_BASE这样的宏。这样做的好处是,如果你的项目未来需要更换到另一款STM32(如F4系列,其ID地址不同),只需更新库文件,你的代码无需修改,可移植性大大增强。

2.2 读取操作的硬件与软件本质

从硬件层面看,这个ID存储在一块特殊的OTP(一次性可编程)存储器或类似的只读存储单元中。CPU通过内部总线(通常是AHB或APB总线)访问这个物理地址。这个过程是只读的,任何写操作都是无效的,这从硬件上保证了ID的安全性。

从软件层面看,我们的代码需要做以下几件事:

  1. 声明指针:定义一个指向uint32_tuint8_t类型的指针,并将其指向ID的起始地址。由于内存访问最好以32位对齐,通常使用uint32_t*指针一次读取4字节效率更高。
  2. 解引用读取:通过指针解引用,获取该地址存储的数据。
  3. 数据重组:将读取到的多个32位数据,按照我们需要的顺序(通常是大端或小端)组合成最终的96位ID。
  4. 格式化输出:将二进制数据转换为便于显示、存储或传输的格式,如十六进制字符串。

这里有一个关键点:STM32的内存系统是小端模式。也就是说,一个32位数据(例如0x12345678)存储在内存中,低字节0x78在低地址。当我们用uint32_t*指针从0x1FFFF7E8读取时,读到的就是0x?? ?? ?? ??这个完整的32位值,顺序是符合我们直觉的。但如果我们用uint8_t*指针逐个字节读取,就需要关注字节序问题。不过,在将ID转换为字符串时,我们通常按照地址递增的顺序依次处理每个字节,这样得到的字符串顺序是固定的、可预期的。

3. 三种实战代码实现与对比

理解了原理,我们来看代码。我将展示三种不同开发环境下的实现方法:标准外设库、HAL库以及纯寄存器操作。你可以根据你的项目情况选择。

3.1 基于标准外设库的实现

如果你的项目使用的是经典的STM32标准外设库(StdPeriph Lib),实现如下。首先,确保你的工程包含了stm32f10x.h和相关头文件。

#include "stm32f10x.h" #include <stdio.h> // 用于sprintf /** * @brief 读取STM32F103的唯一ID(96位) * @param uidArray: 用于存储ID的数组,至少12字节 * @retval None */ void Get_STM32F103_UID(uint8_t *uidArray) { uint32_t *uid_addr = (uint32_t*)0x1FFFF7E8; // 使用宏更好,如 `(uint32_t*)UID_BASE` // 读取3个32位数据(共96位) uint32_t uid_part0 = uid_addr[0]; // 地址: 0x1FFFF7E8 uint32_t uid_part1 = uid_addr[1]; // 地址: 0x1FFFF7EC uint32_t uid_part2 = uid_addr[2]; // 地址: 0x1FFFF7F0 // 将32位数据分解为字节存入数组 // 注意内存布局,这里按小端顺序,将低地址字节放在数组前端是常见做法 uidArray[0] = (uid_part0 >> 0) & 0xFF; uidArray[1] = (uid_part0 >> 8) & 0xFF; uidArray[2] = (uid_part0 >> 16) & 0xFF; uidArray[3] = (uid_part0 >> 24) & 0xFF; uidArray[4] = (uid_part1 >> 0) & 0xFF; uidArray[5] = (uid_part1 >> 8) & 0xFF; uidArray[6] = (uid_part1 >> 16) & 0xFF; uidArray[7] = (uid_part1 >> 24) & 0xFF; uidArray[8] = (uid_part2 >> 0) & 0xFF; uidArray[9] = (uid_part2 >> 8) & 0xFF; uidArray[10] = (uid_part2 >> 16) & 0xFF; uidArray[11] = (uid_part2 >> 24) & 0xFF; } /** * @brief 将UID数组转换为十六进制字符串 * @param uidArray: UID数组 * @param strBuffer: 输出字符串缓冲区,至少需要25字节(12字节*2 + 结束符) * @retval None */ void UID_To_HexString(uint8_t *uidArray, char *strBuffer) { for(int i = 0; i < 12; i++) { sprintf(&strBuffer[i*2], "%02X", uidArray[i]); // 格式化为大写十六进制 } strBuffer[24] = '\0'; // 添加字符串结束符 } // 使用示例 int main(void) { uint8_t uid[12]; char uidStr[25]; Get_STM32F103_UID(uid); UID_To_HexString(uid, uidStr); // 现在可以通过串口打印 uidStr,或者用于其他逻辑 // printf("Chip UID: %s\r\n", uidStr); while(1) { // 主循环 } }

实操心得:在标准库中,直接使用地址指针是最快的方式。但务必注意,0x1FFFF7E8这个地址位于系统内存区域,不是Flash也不是RAM,你的调试器在正常运行时可能无法直接查看这个地址的内容。我建议将读取到的UID通过串口打印出来,这是最可靠的验证方式。另外,数组uid的字节顺序(即哪个字节算作UID的最高位)取决于你的应用协议,上述代码将uid_part0的最低字节放到了uid[0],这是按内存地址递增的常见顺序,与用uint8_t*指针逐个读取的结果一致。

3.2 基于HAL库的实现

对于使用STM32CubeMX和HAL库的新项目,过程类似,但我们可以利用HAL库可能已经定义好的宏(尽管HAL库本身没有专门读取UID的函数)。通常,我们需要自己定义地址,或者查看stm32f1xx_hal.h中是否有相关定义。

#include "main.h" #include "stm32f1xx_hal.h" // 如果HAL库未定义,我们自己定义UID基地址(对于F103) #define UID_BASE_ADDRESS 0x1FFFF7E8U void Get_Chip_UID_HAL(uint8_t *buffer) { uint32_t *pUID = (uint32_t *)UID_BASE_ADDRESS; // 一次性读取三个32位字 uint32_t word0 = pUID[0]; uint32_t word1 = pUID[1]; uint32_t word2 = pUID[2]; // 使用联合体(union)或内存拷贝进行转换,这里使用更直观的移位操作 buffer[0] = (uint8_t)(word0 >> 0); buffer[1] = (uint8_t)(word0 >> 8); buffer[2] = (uint8_t)(word0 >> 16); buffer[3] = (uint8_t)(word0 >> 24); buffer[4] = (uint8_t)(word1 >> 0); buffer[5] = (uint8_t)(word1 >> 8); buffer[6] = (uint8_t)(word1 >> 16); buffer[7] = (uint8_t)(word1 >> 24); buffer[8] = (uint8_t)(word2 >> 0); buffer[9] = (uint8_t)(word2 >> 8); buffer[10] = (uint8_t)(word2 >> 16); buffer[11] = (uint8_t)(word2 >> 24); } // 或者,一个更简洁的版本,直接按字节指针访问 void Get_Chip_UID_HAL_Simple(uint8_t *buffer) { uint8_t *pUID = (uint8_t *)UID_BASE_ADDRESS; for(int i = 0; i < 12; i++) { buffer[i] = pUID[i]; } }

注意事项Get_Chip_UID_HAL_Simple函数更加简洁,它直接以字节为单位遍历内存。这两种方法在结果上是等效的。在HAL库项目中,我更喜欢第二种方法,因为它意图更清晰——我们就是要12个连续的字节。同时,建议将UID_BASE_ADDRESS的定义放在一个独立的头文件(如chip_id.h)中,方便不同源文件引用和管理。

3.3 纯寄存器操作实现

如果你在写裸机程序或者追求极致的效率和可控性,可以直接操作寄存器。对于读取固定地址内存来说,这和上面的指针操作没有本质区别,但可以让你更清楚地看到发生了什么。

// 假设在一个没有标准库的环境下 #define UID_START_ADDR 0x1FFFF7E8 typedef volatile uint32_t vu32; typedef volatile uint8_t vu8; void Read_Chip_ID_Register(uint8_t uid_out[12]) { // 方法1:使用32位访问 vu32* uid_reg = (vu32*)UID_START_ADDR; vu32 w0 = uid_reg[0]; vu32 w1 = uid_reg[1]; vu32 w2 = uid_reg[2]; // 将字数据分解到字节数组 *((vu32*)(&uid_out[0])) = w0; // 注意:这依赖于编译器的字节序!不推荐。 // 更安全的方法仍是逐字节赋值 uid_out[0] = (w0 >> 0) & 0xFF; // ... 省略后续赋值,同前例 // 方法2:直接使用8位指针访问(推荐,清晰无歧义) // vu8* uid_byte = (vu8*)UID_START_ADDR; // for(int i=0; i<12; i++) { // uid_out[i] = uid_byte[i]; // } }

踩坑记录:在早期的一次项目中,我曾试图用*((uint32_t*)(uid_out)) = w0;这样的内存拷贝来快速赋值,结果在不同优化等级的编译下,出现了字节序错乱的问题。这是因为这种操作直接操作了内存,而编译器的行为可能不一致。血的教训是:在处理这种需要精确字节顺序的数据时,显式地使用移位和掩码操作,或者逐字节拷贝,是最安全、可移植性最好的方法,不要为了所谓的“优化”而引入不确定性。

4. 高级应用与功能扩展

仅仅读出ID还不够,关键在于怎么用。下面分享几个我在实际产品中用到的扩展应用。

4.1 生成设备唯一序列号

96位的ID很长,直接使用不方便。我们可以通过一个哈希函数(如CRC32、MD5或简单的自定义算法)将其压缩成一个固定长度的短序列号,例如4字节或8字节。

#include <stdint.h> // 一个简单的哈希函数示例(FNV-1a变种),用于从UID生成一个32位序列号 uint32_t Generate_Serial_From_UID(uint8_t *uid, uint8_t len) { uint32_t hash = 2166136261u; // FNV偏移基础值 for(int i=0; i<len; i++) { hash ^= uid[i]; hash *= 16777619u; } return hash; } // 使用示例 uint8_t my_uid[12]; Get_STM32F103_UID(my_uid); uint32_t device_sn = Generate_Serial_From_UID(my_uid, 12); // device_sn 可以作为设备的短序列号

这个device_sn可以烧录到Flash的某个固定位置,作为设备出厂信息的一部分,比直接存储96位ID更节省空间。

4.2 实现简易软件授权与防复制

利用UID可以实现简单的软件绑定。思路是:在程序初始化时读取UID,然后与一个预先设定的“密钥”或算法进行计算,得到一个结果。将这个结果与存储在Flash中的另一个值(激活码)进行比较,只有匹配时,软件才进入正常工作模式。

#define ACTIVATION_CODE_ADDR 0x0800FC00 // Flash中存储激活码的地址(示例) uint8_t Check_Software_License(void) { uint8_t uid[12]; uint32_t calculated_key; uint32_t stored_key; // 1. 读取芯片UID Get_Chip_UID_HAL_Simple(uid); // 2. 使用一个算法和主密钥计算本机的“激活码” calculated_key = Your_License_Algorithm(uid, MASTER_SECRET_KEY); // 3. 从Flash指定地址读取之前存储的激活码 stored_key = *(uint32_t*)ACTIVATION_CODE_ADDR; // 4. 比较 if(calculated_key == stored_key) { return 1; // 授权成功 } else { return 0; // 授权失败,可进入演示模式或限制功能 } }

这里的Your_License_Algorithm是你自己设计的算法,可以结合UID和一个只有你知道的MASTER_SECRET_KEY(这个密钥不能出现在客户端代码中,否则会被反编译找到)进行计算。在量产时,你通过一个离线工具,根据每颗芯片的UID计算出对应的激活码,然后通过烧录器写入到Flash的ACTIVATION_CODE_ADDR地址。这样,程序就被绑定到了这块具体的硬件上。

重要安全提示:这种方法只能防君子不防小人,对于有经验的破解者,通过反汇编找到算法和比较点,仍然可以绕过。但它能有效防止普通的程序复制粘贴,显著提高了抄袭的门槛。对于高安全需求,建议使用带有硬件加密功能的STM32系列(如STM32L5)。

4.3 在物联网中作为设备标识

在MQTT、CoAP等物联网协议中,设备需要一个Client ID或Endpoint Name。直接使用UID的十六进制字符串就是一个绝佳的选择,保证了全球唯一性。

void Get_Device_ClientID(char *clientIdBuffer) { uint8_t uid[12]; char uidStr[25]; Get_STM32F103_UID(uid); UID_To_HexString(uid, uidStr); // 可以添加前缀,如 "DEV_" sprintf(clientIdBuffer, "DEV_%s", uidStr); } // 得到的clientIdBuffer类似:"DEV_1234567890ABCDEF12345678"

将这个Client ID用于连接阿里云、AWS IoT、腾讯云等物联网平台,可以免去你手动分配和管理设备标识的麻烦。平台后台可以直接用这个ID来区分设备。

5. 调试技巧与常见问题排查实录

即使原理和代码都清楚了,第一次实操时还是可能遇到各种问题。下面是我总结的常见“坑点”和解决方法。

5.1 问题一:读出来的ID全是0xFF或0x00

  • 现象:通过调试器查看内存地址0x1FFFF7E8,或者串口打印出来的UID,全部是0xFFFFFFFF0x00000000
  • 排查思路
    1. 地址错误:首先确认芯片型号是STM32F103,并且你使用的地址是正确的。不同STM32系列的UID地址不同,F1是0x1FFFF7E8,F4是0x1FFF7A10,一定要核对《参考手册》。
    2. 调试器访问限制:在调试模式下,某些调试器(特别是通过SWD接口)可能无法直接访问系统内存区域。最可靠的验证方法不是看调试器的Memory窗口,而是通过你的代码将数据打印到串口。在代码中读取UID后,通过printf发送到串口,用串口助手查看。
    3. 代码优化:检查编译器优化等级。如果读取UID的代码被编译器认为是“无副作用”的无效代码,可能会被优化掉。确保存储UID的变量是volatile的,或者将读取UID的函数返回值用于实际操作(如打印、计算),避免被优化。
    4. 芯片问题:极少数情况下,可能是芯片本身的问题。换一颗同型号芯片测试一下。

5.2 问题二:每次上电读出的ID不一致

  • 现象:程序运行中,多次读取UID,或者复位后重新读取,得到的值不一样。
  • 排查思路
    1. 指针或数组越界:这是最常见的原因。检查你的数组是否足够大(至少12字节),检查指针操作是否越界,破坏了相邻内存的数据。
    2. 栈或堆溢出:如果读取UID的函数使用了局部数组,并且栈空间不足,或者程序其他部分有内存溢出,可能会覆盖这块内存。确保栈空间设置合理。
    3. 缓存问题:STM32F103没有数据缓存,所以不是缓存一致性问题。但如果你是在有Cache的高端芯片(如F7、H7)上遇到此问题,则需要考虑Cache的影响,在访问这类特定内存地址前清空或禁用Cache。
    4. 软件逻辑错误:仔细检查你的读取和转换代码,特别是字节序处理部分。用一个已知的模拟UID(例如,在内存中定义一个常量数组)测试你的转换和打印函数,确保逻辑正确。

5.3 问题三:UID转换的字符串格式不对

  • 现象:读出的字节数据看起来是随机的,但转换成字符串后,与别人读出的格式不同(比如字节顺序反了)。
  • 解决方案
    • 明确你的需求。你需要的是大端序还是小端序的字符串?通常,按照内存地址从低到高依次排列字节得到的字符串,是一种最自然、最常用的格式。
    • 编写一个测试函数,用一组固定的字节数组(如{0x12, 0x34, 0x56, 0x78, ...})输入给你的UID_To_HexString函数,看输出是否符合你的预期。
    • 记录下你确定的转换规则,并在整个项目(包括上位机软件)中保持一致。

5.4 调试与验证最佳实践

  1. 串口打印是王道:无论你用什么调试器,将UID以十六进制字符串形式通过串口打印出来,是最直观、最不容易出错的方法。用串口助手或逻辑分析仪捕获这个字符串。
  2. 编写单元测试:为你的UID读取和转换函数编写简单的测试代码,在开发初期就验证其正确性。
  3. 对比官方工具:如果条件允许,使用ST官方的编程软件(如ST-LINK Utility)连接芯片,它通常有一个“芯片信息”或“Unique ID”的查看界面,将你读出的ID与工具显示的进行对比,这是终极验证。
  4. 注意供电稳定性:在极端不稳定的电源环境下,读取系统内存区域可能会出错。确保你的板子在读取ID时供电充足、稳定。

读取STM32的芯片ID是一个小而精的必备技能,它连接着硬件身份与软件逻辑。掌握它,不仅能让你轻松完成设备标识、软件授权等任务,更能加深你对单片机内存布局和底层访问的理解。希望这篇详尽的拆解和实战记录,能让你在下次需要用到这个功能时,信手拈来,游刃有余。在实际项目中,我习惯将UID读取函数封装成一个独立的模块,并提供获取字符串、哈希值等不同格式的接口,这样在任何需要的地方都能方便地调用,让代码更加整洁和模块化。

http://www.jsqmd.com/news/1292411/

相关文章:

  • AI搜索时代企业营销困局:流量悄悄流失?全域GEO优化破局新思路
  • C#十六进制格式化ToString(X)与ToString(X2)详解:从原理到实战应用
  • 中山口碑好的贴标机厂家靠谱之选,双诚智能如何?
  • C++高并发在线判题系统架构:负载均衡与微服务实践
  • VMware macOS解锁工具完全指南:在Windows/Linux上运行苹果系统的终极解决方案
  • 从阿波罗到现代:计算能力 8 个数量级飞跃,RAM 见证计算机发展历程!
  • 2026 年现阶段焉耆回族自治专业的毛细管空调系统施工公司怎么联系,夏天在家像裹棉被?这玩意儿竟让你体感比开风扇还舒服-博力久能暖通 - 行业推荐官[官方】--
  • C#配置管理:App.config与.settings文件的原理、实践与演进
  • 深入OWASP ZAP源码:结对编程解析Web安全工具核心架构与插件开发
  • AI与数学定理证明:LongCat-Flash-Prover技术解析
  • PLC编程指令核心解析:从位逻辑到数据操作,构建自动化控制基石
  • 基于Django与人脸识别的智能考勤系统开发实践
  • 照片大小修改:打印店说文件太大时先答三道判断题 - AI测评专家
  • Unity游戏开发实战:BFS、DFS与A*寻路算法性能深度对比与优化
  • Harris角点检测算法原理与Matlab实战实现
  • CODESYS + UaExpert + Qt OPC UA 入门指南
  • 保研全流程实战指南:从信息战到九推系统填报
  • TPFanCtrl2终极指南:ThinkPad风扇智能控制完整解决方案
  • 基于Springboot+Vue的高校竞赛管理系统(源码+lw+部署文档+讲解等)
  • Nginx安装与启动全指南:从源码编译到systemd服务管理
  • CPU、GPU、TPU/NPU/XPU:从架构原理到场景适配的处理器全解析
  • 重测信度:测量工具稳定性的核心评估方法与工程实践
  • 易潮解晶体加工注意事项
  • 相场法模拟金属定向凝固枝晶生长技术解析
  • 【面试题-多线程】什么是 ABA 问题,怎么产生的,怎么解决?
  • 静电旋杯喷枪厂商有哪些
  • Electron与WebView2实战:将网页快速打包为Windows桌面应用
  • 磁吸易装+稳定传输,赋能机房U位资产高效智能管理
  • 网络工程师转型指南:Python自动化核心工具与实战案例解析
  • RC电路充电时间计算:从理论公式到工程实践的全面解析