1. 为什么STM32F1至今仍是嵌入式入门的首选
搞嵌入式这行十几年,带过的新人少说也有几十个。每次有人问我“想学单片机从哪块板子开始”,我基本都会先问一句:你是想快速做出东西,还是想彻底搞懂底层。如果答案是前者,我通常直接推荐STM32F1系列。原因很简单,这颗芯片的资料密度、社区沉淀和成本控制,在入门级32位MCU里几乎没有对手。
STM32F1是意法半导体基于ARM Cortex-M3内核推出的经典产品线,最早一批型号在2007年前后问世。它把当时动辄几十块钱的32位单片机价格拉到了十元级别,同时保留了丰富的外设:多路USART、SPI、I2C、ADC、定时器、CAN,还有DMA控制器。对于刚接触32位开发的人来说,这些外设足够覆盖绝大多数练手项目,又不会像高端系列那样寄存器复杂到让人劝退。
我见过太多人一上来就买最高配的开发板,结果光看参考手册就放弃了。STM32F1的好处在于,它的参考手册虽然也有一千多页,但社区里针对它的教程、例程、踩坑记录是最全的。你遇到的90%问题,网上都有人问过并且给出了答案。这种“被验证过的学习路径”对新手极其重要。
这篇文章主要面向三类人:第一类是电子、自动化、计算机相关专业的学生,正在做课程设计或准备电赛;第二类是转行做嵌入式开发的工程师,需要快速上手一颗能实际干活的芯片;第三类是老手,想找一颗便宜稳定的芯片做小批量产品。我会从选型、工具链、外设配置、调试技巧几个维度,把STM32F1的开发流程完整拆一遍,中间穿插我这些年踩过的坑和总结的经验。
2. STM32F1系列选型与核心架构拆解
2.1 型号命名规则与常见型号对比
STM32F1的型号命名有一套固定逻辑,看懂之后选型会快很多。以“STM32F103C8T6”为例:STM32代表产品家族,F代表基础型,103是子系列,C表示引脚数(48脚),8表示Flash容量(64KB),T表示封装(LQFP),6表示温度范围(-40到85度)。这套规则适用于整个F1系列,记住之后基本不会买错。
下面这张表是我整理的最常用几款型号对比,都是市场上货源稳定、价格透明的选择:
| 型号 | 引脚数 | Flash | RAM | 主频 | 典型用途 |
|---|---|---|---|---|---|
| STM32F103C8T6 | 48 | 64KB | 20KB | 72MHz | 入门学习、小项目 |
| STM32F103RCT6 | 64 | 256KB | 48KB | 72MHz | 中等复杂度产品 |
| STM32F103VET6 | 100 | 512KB | 64KB | 72MHz | 多外设、带LCD |
| STM32F103ZET6 | 144 | 512KB | 64KB | 72MHz | 开发板常用 |
| STM32F101C8T6 | 48 | 64KB | 10KB | 36MHz | 低成本低功耗 |
选型时最容易纠结的是Flash和RAM够不够。我的经验是,用标准外设库开发时,一个带串口、定时器、ADC的完整工程大约占用15到25KB Flash,RAM占用在5KB以内。如果你用HAL库,代码体积会膨胀30%到50%。所以C8T6的64KB Flash对大多数练手项目是够的,但如果你要移植文件系统、GUI或者网络协议栈,直接上RCT6或VET6更稳妥。
注意:市面上有一些翻新或打磨过的F1芯片,价格明显低于市场均价。买的时候尽量选正规渠道,尤其是做产品时。翻新片在高温或高负载下容易出现随机死机,排查起来非常痛苦。
2.2 Cortex-M3内核与总线结构
STM32F1用的是ARM Cortex-M3内核,哈佛架构,三级流水线。主频最高72MHz,这个数字在现在看不算高,但配合它的总线矩阵,实际吞吐能力比很多同频8位机强一个数量级。内核通过ICode总线取指令,DCode总线取数据,系统总线连接外设,三条总线并行工作,减少了取指和取数据的冲突。
理解总线结构对写高效代码有帮助。比如你把频繁访问的变量放在SRAM里,把常量表放在Flash里,内核可以同时从两条总线取数据,速度会快一些。另外F1的Flash访问需要等待周期,72MHz下需要插入2个等待周期。如果你在中断里执行Flash中的代码,而同时有DMA在访问Flash,可能会出现总线仲裁延迟。这个细节在高速ADC采样加DMA传输的场景下要特别注意。
2.3 时钟树与启动流程
F1的时钟树是很多新手第一个卡住的地方。简单说,芯片上电后默认使用内部8MHz RC振荡器(HSI),经过分频后给系统时钟。你要跑到72MHz,需要配置外部晶振(HSE),然后经过PLL倍频。典型配置是:HSE 8MHz,预分频1,PLL倍频9,得到72MHz系统时钟。AHB不分频,APB1分频2得到36MHz,APB2不分频得到72MHz。
这里有个坑:APB1上的外设最高只能跑36MHz,APB2上的可以跑72MHz。如果你把挂在APB1的定时器配置成72MHz计数,实际频率会不对。我见过有人调PWM时发现频率差一倍,查了半天才发现是总线频率搞错了。
启动流程方面,F1上电后从0x00000000地址取栈顶指针,从0x00000004取复位向量。实际映射的是Flash起始地址0x08000000。如果你用Bootloader方案,需要修改中断向量表偏移,这个后面会细说。
3. 开发环境搭建与工具链选择
3.1 IDE与编译器方案对比
STM32F1的开发环境选择很多,我按使用场景分三类来说。
第一类是Keil MDK。这是国内最流行的方案,优点是上手快、调试方便、教程多。缺点是正版授权费用不低,而且Keil的编辑器体验一般。如果你只是学习,用评估版也够,但要注意32KB代码限制。很多F1的工程编译出来会超过这个限制,尤其是带HAL库的。
第二类是STM32CubeIDE。这是官方推出的免费方案,基于Eclipse,集成了CubeMX配置工具。优点是免费、跨平台、官方支持好。缺点是Eclipse系IDE对电脑配置要求高,启动慢,偶尔会有索引卡顿。我个人的做法是:用CubeMX生成初始化代码,然后用Makefile加VSCode来写和编译,这样既轻量又灵活。
第三类是平台IO加VSCode。这是近几年越来越流行的方案,用platformio.ini管理依赖和编译选项,跨平台体验很好。缺点是调试配置需要手动写,对新手有一定门槛。
| 方案 | 费用 | 上手难度 | 调试体验 | 适合人群 |
|---|---|---|---|---|
| Keil MDK | 收费 | 低 | 好 | 学生、企业 |
| STM32CubeIDE | 免费 | 中 | 好 | 个人、跨平台 |
| VSCode+PlatformIO | 免费 | 中高 | 中 | 有经验开发者 |
| Makefile+GCC | 免费 | 高 | 需配置 | 喜欢折腾的人 |
3.2 标准外设库、HAL库与LL库的取舍
F1系列支持三种库:标准外设库(SPL)、HAL库、LL库。标准外设库是早期官方推出的,代码直接操作寄存器,效率高,但ST已经停止维护。HAL库是现在主推的,抽象层次高,跨系列移植方便,但代码体积大、执行效率略低。LL库是底层库,介于寄存器和HAL之间,效率接近SPL,但覆盖的外设不如HAL全。
我的建议是:如果你只是做F1的项目,不打算换芯片,用标准外设库最省资源。如果你以后可能换到F4、F7或者G0系列,直接用HAL库,虽然前期学习曲线陡一点,但长期看省事。LL库适合对性能有要求又不想直接写寄存器的场景,比如高频PWM或者高速ADC采样。
实操心得:HAL库的
HAL_Delay()函数依赖SysTick中断,如果你在中断服务函数里调用它,会导致死锁。正确做法是在中断里用简单的循环延时,或者自己维护一个毫秒计数器。
3.3 烧录与调试工具
烧录工具主要分两类:ST-Link和串口ISP。ST-Link是官方调试器,支持SWD接口,可以在线调试、单步、看变量。山寨版ST-Link价格在十几块钱,功能基本够用,但固件升级时可能遇到问题。串口ISP只需要一个USB转TTL模块,通过BOOT0和BOOT1引脚进入系统存储器启动模式,用官方Flash Loader工具烧录。这种方式便宜,但不能在线调试。
调试接口我强烈建议用SWD而不是JTAG。SWD只需要两根线(SWCLK和SWDIO),占用引脚少,速度也够。F1的SWD引脚是PA13和PA14,如果你在代码里把这两个引脚配置成普通GPIO,下次就烧不进去了。这时候需要用串口ISP擦除,或者把BOOT0拉高重新上电。
4. 核心外设配置与实操要点
4.1 GPIO配置的八种模式与常见误区
F1的GPIO有八种模式:输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、开漏复用、推挽复用。新手最容易搞混的是“复用”和“普通输出”的区别。复用模式是给外设用的,比如USART的TX引脚要配置成推挽复用,SPI的SCK也是复用。如果你把USART的TX配置成普通推挽输出,虽然也能发数据,但时序可能不标准。
另一个常见误区是输出速度配置。F1的GPIO输出速度有三个档:2MHz、10MHz、50MHz。速度越高,功耗和EMI越大。我一般的原则是:普通LED、继电器控制用2MHz;SPI、USART用10MHz或50MHz;高速SPI或者外部总线用50MHz。不要无脑全设50MHz,那样EMI测试容易出问题。
// 标准外设库配置PA5为推挽输出,50MHz GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);注意:F1的GPIO在复位后默认是浮空输入,但JTAG引脚(PA13、PA14、PA15、PB3、PB4)默认是复用功能。如果你要把它们当普通GPIO用,需要先关闭JTAG功能,只保留SWD。这个操作在
GPIO_PinRemapConfig()里做。
4.2 USART串口通信的完整配置流程
串口是调试和通信最常用的外设。F1的USART支持波特率范围很宽,常用9600到115200。配置步骤分四步:使能时钟、配置GPIO、配置USART参数、使能USART。
// 以USART1为例,PA9为TX,PA10为RX RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // TX配置为复用推挽 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // RX配置为浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // USART参数配置 USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); USART_Cmd(USART1, ENABLE);波特率计算有个公式:波特率 = fCK / (16 * USARTDIV)。fCK是APB2时钟72MHz,要得到115200,USARTDIV约等于39.0625。寄存器里整数部分是39,小数部分是0.0625乘以16等于1。所以BRR寄存器的值是0x271。标准库会自动算,但你要知道这个原理,遇到波特率偏差大的时候能自己排查。
串口接收中断是另一个重点。F1的USART支持接收非空中断(RXNE),每收到一个字节触发一次。如果你用中断接收不定长数据,需要配合空闲中断(IDLE)。空闲中断在一帧数据接收完后触发,这时候把缓冲区数据取走就行。这个方案比定时器超时判断更可靠。
4.3 定时器与PWM输出的参数计算
F1的定时器分高级定时器(TIM1、TIM8)、通用定时器(TIM2到TIM5)、基本定时器(TIM6、TIM7)。PWM输出一般用通用定时器。配置PWM需要算三个参数:预分频器(PSC)、自动重装载值(ARR)、比较值(CCR)。
假设系统时钟72MHz,你要输出1kHz、占空比50%的PWM。计算过程:定时器时钟72MHz,先分频到1MHz,PSC设为71。周期1ms对应1000个计数,ARR设为999。占空比50%,CCR设为500。
// TIM3通道1输出PWM,PA6引脚 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 999; TIM_TimeBaseStructure.TIM_Prescaler = 71; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 500; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE);实操心得:高级定时器TIM1和TIM8输出PWM时,需要额外使能主输出(
TIM_CtrlPWMOutputs),否则引脚没有波形。这个坑我当年调了半天才发现。
4.4 ADC多通道采样与DMA传输
F1的ADC是12位逐次逼近型,最高采样率1MHz。多通道采样时,用DMA搬运数据是最省CPU的方案。配置步骤:使能ADC和DMA时钟、配置GPIO为模拟输入、配置DMA、配置ADC规则组、使能ADC和DMA。
// ADC1多通道采样,PA0到PA3,DMA搬运 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_values; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 4; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel1, &DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = ENABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 4; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE);采样时间的选择很关键。F1的ADC输入阻抗有限,采样时间太短会导致采样值偏低。一般建议采样时间不低于55.5个周期,对应约1.2微秒。如果你测的是高阻抗信号源,采样时间还要加长,或者加电压跟随器。
5. 常见问题排查与避坑经验实录
5.1 程序烧录失败与芯片锁死处理
烧录失败最常见的原因是SWD引脚被占用。如果你在代码里把PA13或PA14配置成了普通GPIO,下次上电后SWD接口就失效了。解决办法有两个:一是把BOOT0拉高,让芯片从系统存储器启动,然后用串口ISP擦除;二是用ST-Link的“Connect under reset”模式,在复位期间连接。
还有一种情况是芯片读保护被意外开启。这时候ST-Link会提示“Flash read protection”。解决方法是先用ST-Link Utility解除读保护,但这会擦除整个Flash。所以平时调试时不要随便开读保护,除非产品要量产了。
注意:F1的BOOT0引脚内部有下拉,但为了可靠,建议外部加一个10k下拉电阻。BOOT1和BOOT0的组合决定启动模式:00是Flash启动,01是系统存储器,11是SRAM启动。正常运行时BOOT0接地。
5.2 中断优先级与嵌套问题
F1的中断优先级分抢占优先级和响应优先级,各4位,但实际只用高4位。抢占优先级高的可以打断低的,响应优先级只在同时挂起时决定谁先执行。新手常犯的错误是把所有中断都设成最高优先级,结果中断嵌套混乱,栈溢出。
我的建议是:SysTick设最低优先级,串口接收中断设中等,定时器控制中断设较高,紧急保护中断设最高。另外,中断服务函数里尽量只做标志位处理和简单数据搬运,复杂计算放到主循环。
// 中断优先级配置示例 NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 2位抢占,2位响应 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);5.3 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 烧录失败 | SWD引脚被占用 | 检查PA13/PA14配置 | 用Connect under reset或ISP擦除 |
| 串口乱码 | 波特率不匹配 | 核对时钟和BRR值 | 重新计算波特率 |
| PWM无输出 | 高级定时器未使能主输出 | 检查TIM_CtrlPWMOutputs | 调用该函数使能 |
| ADC值跳动大 | 采样时间太短 | 示波器看输入信号 | 加长采样时间或加滤波 |
| 程序跑飞 | 栈溢出 | 查看栈使用情况 | 增大栈空间或减少局部变量 |
| 复位后不运行 | BOOT引脚状态错误 | 测量BOOT0电平 | 确保BOOT0接地 |
| 定时器频率不对 | 总线频率搞错 | 确认APB1/APB2分频 | 重新计算PSC和ARR |
| 中断不触发 | 优先级配置错误 | 检查NVIC配置 | 调整优先级分组 |
5.4 低功耗模式与唤醒源配置
F1支持睡眠、停止、待机三种低功耗模式。睡眠模式只关CPU时钟,外设还在跑,唤醒最快。停止模式关掉所有时钟,保留SRAM和寄存器,唤醒后需要重新配置时钟。待机模式最省电,但唤醒后相当于复位。
实际项目中,如果你用电池供电,待机模式可以把功耗降到2微安左右。唤醒源可以是WKUP引脚、RTC闹钟或者外部中断。配置待机模式的步骤:使能PWR时钟、设置WKUP引脚、调用PWR_EnterSTANDBYMode()。注意进入待机前要清除WKUP标志位,否则会立刻唤醒。
// 进入待机模式 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); PWR_WakeUpPinCmd(ENABLE); PWR_ClearFlag(PWR_FLAG_WU); PWR_EnterSTANDBYMode();实操心得:停止模式下,如果开了DMA,DMA传输会暂停。唤醒后DMA会继续,但如果你在停止前改了DMA配置,唤醒后可能不生效。稳妥的做法是唤醒后重新初始化DMA。
6. 从练手到产品的进阶路线
6.1 Bootloader与在线升级方案
产品化之后,在线升级是刚需。F1的Flash分成多个扇区,你可以把Bootloader放在0x08000000开始的扇区,应用程序放在后面的扇区。Bootloader负责接收新固件并写入Flash,然后跳转到应用程序。
跳转前需要做三件事:关闭所有中断、设置栈顶指针、设置中断向量表偏移。中断向量表偏移用NVIC_SetVectorTable()设置,偏移量是应用程序起始地址减去0x08000000。
// 跳转到应用程序 typedef void (*pFunction)(void); pFunction Jump_To_Application; uint32_t JumpAddress; if (((*(__IO uint32_t*)ApplicationAddress) & 0x2FFE0000) == 0x20000000) { JumpAddress = *(__IO uint32_t*)(ApplicationAddress + 4); Jump_To_Application = (pFunction)JumpAddress; __set_MSP(*(__IO uint32_t*)ApplicationAddress); Jump_To_Application(); }注意:跳转前一定要关掉SysTick中断和所有外设中断,否则应用程序里重新配置中断时会出问题。另外,应用程序的链接脚本里要修改Flash起始地址,否则编译出来的向量表位置不对。
6.2 硬件设计中的电源与晶振注意事项
F1对电源要求不算苛刻,但有几个细节要注意。VDDA和VSSA是模拟电源,即使你不用ADC,也建议接上滤波电容。如果VDDA不接,ADC读数会不准。退耦电容每对电源引脚配一个100nF,靠近引脚放置。
晶振部分,F1支持4到16MHz的外部晶振。8MHz是最常用的,配合PLL倍频到72MHz。晶振的负载电容要根据晶振规格书选,一般20pF左右。如果起振困难,可以减小负载电容或者换低ESR的晶振。PCB走线尽量短,包地处理。
复位电路用一个10k上拉加100nF电容到地就行。如果环境干扰大,可以加一个复位芯片,比如MAX809。BOOT0引脚用10k下拉,需要ISP时再拉高。
6.3 代码优化与执行效率提升
F1的Flash等待周期和总线仲裁会影响执行效率。几个实用的优化技巧:把频繁调用的函数放到RAM里执行,用__attribute__((section(".ramfunc")))声明;把中断服务函数放到RAM里,减少Flash等待;用DMA搬运数据,减少CPU干预;把常量表放到Flash,变量放SRAM,利用双总线并行。
编译器优化选项也有讲究。Keil里用-O2或-O3,但要注意volatile变量的使用。如果你在中断里修改的变量没有加volatile,编译器可能优化掉主循环里的读取操作。这个坑我踩过,变量值明明变了,主循环里读到的还是旧值。
// 中断和主循环共享的变量必须加volatile volatile uint32_t g_tick_count = 0; void SysTick_Handler(void) { g_tick_count++; }6.4 量产测试与固件加密
小批量生产时,我建议做一个简单的产测固件。产测固件跑一遍所有外设:串口回环、GPIO翻转、ADC读基准、定时器计数。测试结果通过串口输出,产线工人看OK或NG就行。这样能把硬件问题挡在出厂前。
固件加密方面,F1支持读保护,开启后通过调试接口读不出Flash内容。但读保护可以解除,解除时会擦除Flash。如果你需要更强的保护,可以用外部加密芯片或者把关键算法放到带唯一ID的芯片里。F1有96位的唯一ID,可以用它做简单的授权绑定。
实操心得:开启读保护后,如果你用ST-Link烧录新固件,会提示需要先解除保护。解除保护会擦除整个Flash,所以量产时最好先烧录再开保护。另外,读保护开启后,SRAM里的数据还是可以通过调试接口读到的,敏感数据不要长期放SRAM。
7. 一些个人体会
STM32F1这颗芯片我用了快十年,从学生时代的课程设计到后来的小批量产品,它一直是个可靠的选择。它的资料多到几乎任何问题都能搜到答案,这对新手来说太重要了。但我也要提醒一句:不要因为F1简单就停留在舒适区。F1的架构和现在主流的M0+、M4有差异,比如F1没有硬件浮点,没有Cache,外设寄存器布局也不一样。如果你打算长期做嵌入式,学完F1之后建议再摸一颗M4或者M0+的芯片,把HAL库和CubeMX的流程走一遍,视野会宽很多。
另外,F1的供货前几年有过波动,价格一度涨得很离谱。如果你做产品,选型时最好准备一个Pin-to-Pin兼容的替代方案,比如同系列的F103其他型号,或者国产的兼容芯片。这样万一缺货,不至于停产。
最后分享一个小技巧:调试时如果怀疑是硬件问题,先写一个最简单的GPIO翻转程序,用示波器看波形。如果波形正常,说明时钟和GPIO配置没问题,再往上加外设。这个“最小系统法”能帮你快速定位问题范围,比盲目改代码高效得多。