1. 为什么STM32F1到今天还值得花时间学
如果你最近在选型或者刚入嵌入式的坑,大概率会听到两种声音:一种是“F1太老了,换F4或者国产替代吧”,另一种是“F1是经典,学会了走遍天下都不怕”。我两种声音都听过,也都带过用F1入门和用F1做量产项目的团队。我的结论很直接:STM32F1系列不是最先进的,但它是最适合建立嵌入式系统完整认知的芯片之一。原因不复杂——它的外设够全、资料够多、坑够典型,而且价格和供货在国产替代浪潮下依然有它自己的生态位。
这篇文章不打算给你复述数据手册,而是把我这些年用F1做项目、带新人、踩坑填坑的经验整理出来。从选型思路、时钟树配置、GPIO的八种模式到底怎么选、中断优先级怎么排、串口和DMA怎么配合、ADC采样怎么稳住、低功耗怎么做、再到常见问题排查,我会尽量把“为什么这么做”讲透。适合刚接触STM32的开发者,也适合用F1做过一两个项目但总觉得心里没底的朋友。看完你至少能明白:F1的每一个配置项背后,到底在解决什么问题。
2. STM32F1系列的整体设计与选型思路
2.1 从内核到外设:F1的定位到底在哪
STM32F1系列基于ARM Cortex-M3内核,这个内核是ARMv7-M架构,带硬件除法器和中断控制器NVIC。主频常见的是72MHz,这个数字不是随便定的——它来自外部晶振8MHz经过PLL九倍频得到。为什么是72MHz而不是100MHz?因为F1的Flash访问速度限制,当主频超过24MHz时,读取Flash需要插入等待周期。72MHz下需要2个等待周期,这是芯片设计时权衡性能和功耗的结果。你如果硬超频到128MHz,大概率能跑起来,但Flash等待周期和总线时序会变得不可靠,工业现场温度一变化就可能死机。
F1的产品线大致分几个档:低密度(如F103x4/x6,16-32KB Flash)、中密度(F103x8/xB,64-128KB Flash)、高密度(F103xC/xD/xE,256-512KB Flash)、互联型(F105/F107,带以太网和USB OTG)。选型时第一个要看的不是主频,而是Flash和RAM够不够,以及你需要的外设有没有。我见过太多人为了省几块钱选了低密度型号,结果代码写到一半发现Flash不够,只能换芯片重新画板。
2.2 选型时最容易忽略的三个参数
第一个是RAM大小。F103C8T6有20KB RAM,听起来不少,但你如果跑一个带协议栈的项目,比如Modbus TCP或者文件系统,RAM会非常紧张。我的经验是:裸机项目RAM占用控制在70%以内,带RTOS控制在60%以内,留出余量给栈和堆。第二个是定时器数量。F1的定时器分高级定时器(TIM1/TIM8)、通用定时器(TIM2-TIM5)、基本定时器(TIM6/TIM7)。高级定时器带死区控制和互补输出,做电机控制必须用;通用定时器做PWM输出和输入捕获;基本定时器只能做时基。选型时数一数你需要几路PWM、几路捕获,别等到画板了才发现定时器不够。
第三个是封装和引脚复用。F1的引脚复用非常灵活,但也不是随便映射的。比如USART1的TX/RX固定在PA9/PA10,而USART2可以映射到PA2/PA3或者PD5/PD6。如果你画板时没注意,把USART2接到了PA2/PA3,但代码里开了重映射,那就收不到数据。我的习惯是:画原理图之前先把引脚分配表列出来,每个功能占哪些引脚、有没有冲突、是否需要重映射,全部确认一遍。这个表后面写代码时直接当参考,能省很多调试时间。
2.3 最小系统板的设计要点
F1的最小系统其实很简单:电源、晶振、复位、启动模式、调试接口。但就是这几个部分,我见过太多人翻车。电源部分,F1的VDD范围是2.0V到3.6V,但如果你用3.3V供电,每个VDD引脚都要接100nF去耦电容,VDDA还要额外接1uF加10nF。我见过有人只在一个VDD引脚旁边放了一个电容,结果ADC采样值跳得厉害,查了两天才发现是电源纹波。
晶振部分,外部高速晶振(HSE)通常用8MHz,负载电容根据晶振规格书选,一般20pF左右。但要注意:晶振的负载电容不是随便选20pF就完事,你要看晶振手册里的CL值,然后按公式C1=C2=2*(CL-Cstray)来算,Cstray是PCB走线寄生电容,一般3-5pF。如果负载电容不对,晶振起振时间会变长,甚至不起振。启动模式引脚BOOT0和BOOT1决定芯片从哪里启动:BOOT0=0从Flash启动,BOOT0=1从系统存储器启动(用于串口下载)。我的习惯是BOOT0接10k下拉电阻,同时留一个跳线帽或者按键,需要下载时再拉高。
调试接口,SWD只需要SWDIO和SWCLK两根线,比JTAG省引脚。但要注意:SWDIO和SWCLK默认是PA13和PA14,这两个引脚上不要接大电容或者强上拉,否则调试器识别不到芯片。我一般在这两个引脚上只留测试点,不接任何外部电路。
3. 核心细节解析与实操要点
3.1 时钟树配置:72MHz到底怎么来的
F1的时钟树是很多新手的第一个坎。外部8MHz晶振进来,先经过HSE振荡器,然后到PLL。PLL的配置是:HSE先除以一个预分频(PLLXTPRE),然后乘以PLLMUL,得到PLLCLK。要得到72MHz,就是8MHz乘以9。但这里有个细节:PLLMUL的配置值不是直接写9,而是写0x07,因为寄存器定义里0x07对应9倍频。这个在标准库和HAL库里都被封装好了,但如果你直接操作寄存器,一定要查参考手册的PLLMUL位定义。
系统时钟SYSCLK选PLLCLK后,还要分频给AHB、APB1、APB2。AHB和APB2最高72MHz,APB1最高36MHz。为什么APB1只有36MHz?因为APB1上挂的外设(如USART2/3、I2C、SPI2)设计时就是按36MHz上限做的,你如果超频到72MHz,这些外设可能工作不正常。我见过有人把APB1也设成72MHz,结果I2C通信时好时坏,查了半天才发现是时钟超了。
配置时钟的代码,用HAL库的话大概是这样的:
RCC_OscInitTypeDef osc = {0}; osc.OscillatorType = RCC_OSCILLATORTYPE_HSE; osc.HSEState = RCC_HSE_ON; osc.HSEPredivValue = RCC_HSE_PREDIV_DIV1; osc.PLL.PLLState = RCC_PLL_ON; osc.PLL.PLLSource = RCC_PLLSOURCE_HSE; osc.PLL.PLLMUL = RCC_PLL_MUL9; HAL_RCC_OscConfig(&osc); RCC_ClkInitTypeDef clk = {0}; clk.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; clk.AHBCLKDivider = RCC_SYSCLK_DIV1; clk.APB1CLKDivider = RCC_HCLK_DIV2; clk.APB2CLKDivider = RCC_HCLK_DIV1; HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_2);注意最后的FLASH_LATENCY_2,这就是前面说的Flash等待周期。72MHz下必须设2,设1会跑飞,设3会降性能。这个参数在标准库里的宏是FLASH_ACR_LATENCY_2,HAL库里就是FLASH_LATENCY_2。
3.2 GPIO的八种模式到底怎么选
F1的GPIO有八种模式:输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、开漏复用、推挽复用。很多人配置GPIO时就是抄例程,推挽输出50MHz到处用。但模式选错了,轻则信号不稳,重则烧芯片。
我整理了一个选择逻辑:先问信号方向,再问电平特性,最后问速度。如果是输入,先看信号源是推挽还是开漏。推挽信号源(比如另一个MCU的推挽输出)可以配浮空输入或者上拉/下拉输入;开漏信号源(比如I2C总线)必须配上拉输入,因为开漏只能拉低不能拉高,需要外部上拉电阻提供高电平。如果是模拟信号(比如ADC采样),必须配模拟输入,这时候GPIO的数字部分被禁用,引脚直接连到ADC。
输出模式,推挽输出能输出强高和强低,适合驱动LED、继电器控制;开漏输出只能输出强低,高电平靠外部上拉,适合I2C、电平转换、多个输出并联的场合。复用模式是给外设用的,比如USART的TX引脚要配推挽复用,I2C的SCL/SDA要配开漏复用。速度选择,不是越快越好。速度越高,引脚翻转时的电磁干扰越大,功耗也越高。我的经验是:低速信号(如LED、按键)用2MHz,中速信号(如SPI、USART)用10MHz,高速信号(如FSMC、SDIO)用50MHz。
注意:配置GPIO之前一定要先使能对应端口的时钟。F1的GPIO时钟挂在APB2上,用
__HAL_RCC_GPIOA_CLK_ENABLE()这样的宏使能。忘了使能时钟,GPIO配置不生效,这是新手最常见的错误之一。
3.3 中断优先级:NVIC的分组和抢占
F1的NVIC支持16个优先级,但实际用几位是可以配置的。优先级分组决定了抢占优先级和响应优先级的位数分配。比如分组2,就是2位抢占优先级、2位响应优先级,各有4个级别。抢占优先级高的可以打断抢占优先级低的中断,响应优先级只在同时挂起时决定谁先执行,不能打断。
我见过很多人把所有中断都设成最高优先级,结果系统里中断嵌套乱七八糟,栈溢出都不知道怎么死的。我的建议是:按实时性要求排优先级。比如电机控制的PWM中断、编码器捕获中断,实时性要求最高,设抢占优先级0或1;串口接收中断、定时器时基中断,设中间级别;按键扫描、LED刷新这种,设最低级别。响应优先级一般不用太纠结,除非你有两个同抢占优先级的中断需要严格排序。
配置中断的代码:
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2); HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM2_IRQn);这里NVIC_PRIORITYGROUP_2就是分组2,SetPriority的第二个参数是抢占优先级,第三个是响应优先级。注意:优先级数值越小,级别越高。这个和很多人的直觉相反,我第一次用的时候也搞混过。
3.4 串口DMA:怎么让CPU从搬运工变成指挥官
串口是F1上最常用的外设之一,但很多人用串口就是“发一个字节等一个字节”,CPU利用率极低。DMA的作用就是把CPU从数据搬运中解放出来。比如你要发1000个字节,不用DMA的话,CPU要进1000次中断,每次搬一个字节;用DMA的话,CPU配置好源地址、目标地址、传输长度,然后就可以去干别的事,DMA搬完了再通知CPU。
配置串口DMA发送的步骤:先使能DMA时钟和串口时钟,配置DMA通道的源地址(内存缓冲区)、目标地址(串口数据寄存器)、传输方向、传输长度、优先级。然后使能串口的DMA发送请求。代码大概这样:
// DMA配置 hdma_usart1_tx.Instance = DMA1_Channel4; hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE; hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart1_tx.Init.Mode = DMA_NORMAL; hdma_usart1_tx.Init.Priority = DMA_PRIORITY_MEDIUM; HAL_DMA_Init(&hdma_usart1_tx); // 关联DMA到串口 __HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx); // 启动DMA发送 HAL_UART_Transmit_DMA(&huart1, buffer, length);这里有个坑:DMA传输完成中断和串口发送完成中断是两回事。DMA传输完成只表示数据全部写进了串口数据寄存器,但最后一个字节可能还在移位寄存器里没发出去。如果你在DMA完成中断里直接切换RS485方向或者关闭串口,最后一个字节会丢。正确的做法是在DMA完成中断里使能串口发送完成中断(TC),在TC中断里再做收尾操作。
4. 实操过程与核心环节实现
4.1 从零搭建一个多任务调度框架
F1的资源有限,跑RTOS不是必须的,但裸机写大项目容易乱。我常用的方案是时间片轮询加状态机。用一个定时器产生1ms时基中断,在中断里给每个任务计数器加一,主循环里判断计数器是否到达任务周期,到了就执行任务函数。这个方案简单可靠,RAM占用小,适合大多数F1项目。
具体实现:TIM2配置成1ms中断,中断里维护一个全局的volatile uint32_t tick_count。每个任务定义一个结构体,包含周期、上次执行时间、任务函数指针。主循环遍历任务数组,如果tick_count - last_run >= period,就执行任务并更新last_run。这个框架的关键是任务函数必须是非阻塞的,不能在里面死等。比如按键消抖,不能在任务里delay_ms(20),而是用状态机:第一次检测到按下记录时间,下次执行时判断时间差。
typedef struct { uint32_t period; uint32_t last_run; void (*task)(void); } task_t; task_t tasks[] = { {10, 0, task_key_scan}, {100, 0, task_led_refresh}, {500, 0, task_sensor_read}, }; void main_loop(void) { for (int i = 0; i < sizeof(tasks)/sizeof(task_t); i++) { if (tick_count - tasks[i].last_run >= tasks[i].period) { tasks[i].last_run = tick_count; tasks[i].task(); } } }这个框架我用了很多年,比RTOS轻量,比纯裸机有条理。唯一要注意的是任务执行时间不能超过任务周期,否则会阻塞其他任务。我一般会在任务函数里加一个GPIO翻转,用示波器看执行时间,确保没有超时。
4.2 ADC多通道采样的稳定方案
F1的ADC是12位逐次逼近型,最高采样率1MHz(ADC时钟14MHz时)。但实际用的时候,采样值跳动是常态。原因有几个:参考电压不稳、采样时间不够、通道切换后的建立时间不足、数字电路干扰。我的解决方案是:硬件上VDDA和VSSA单独走线,靠近芯片放1uF和10nF电容;软件上采样时间设长一点(比如239.5个周期),多次采样取平均或者中值滤波。
多通道采样时,规则组用DMA搬运是最稳的。配置ADC为扫描模式,连续转换,DMA循环搬运。这样ADC自动按顺序采所有通道,DMA自动把结果搬到数组里,CPU只需要读数组就行。代码:
// ADC配置 hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 4; HAL_ADC_Init(&hadc1); // 配置4个通道 ADC_ChannelConfTypeDef ch = {0}; ch.Rank = ADC_REGULAR_RANK_1; ch.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; for (int i = 0; i < 4; i++) { ch.Channel = ADC_CHANNEL_0 + i; ch.Rank = i + 1; HAL_ADC_ConfigChannel(&hadc1, &ch); } // 启动DMA搬运 HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_values, 4);这里adc_values是uint16_t数组,长度4。DMA循环模式下,ADC会不停地采,DMA不停地搬,数组里的值始终是最新的。注意ADC的DMA通道和串口的DMA通道不能冲突,F1的DMA1有7个通道,每个通道固定映射到特定外设,选的时候要查手册。
4.3 低功耗模式的实测数据
F1支持三种低功耗模式:睡眠、停止、待机。睡眠模式只关CPU时钟,外设还在跑,唤醒最快,功耗大概十几mA。停止模式关所有时钟,保留RAM和寄存器,唤醒后继续执行,功耗大概几十uA。待机模式关所有电源域,只保留备份域,唤醒相当于复位,功耗最低,大概几uA。
我实测过F103C8T6在停止模式下的功耗:3.3V供电,关闭所有外设时钟,只留一个外部中断唤醒,电流大概20uA左右。如果开了RTC唤醒,大概25uA。这个数据供你参考,实际值跟你的板子设计有关。进入停止模式的代码:
// 关闭外设时钟 __HAL_RCC_GPIOA_CLK_DISABLE(); // ... 关闭其他外设 // 配置唤醒源 HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1); // 进入停止模式 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后重新配置时钟 SystemClock_Config();这里有个大坑:从停止模式唤醒后,系统时钟会切回HSI(内部8MHz),必须重新配置时钟。如果你忘了这一步,唤醒后所有基于72MHz的定时器、串口波特率全乱。我第一次用停止模式时就被这个坑了两天,串口唤醒后收到的全是乱码,后来才发现是时钟没恢复。
4.4 串口IAP升级的实现要点
IAP就是通过串口给芯片更新固件,不用拆机接调试器。F1的Flash可以自己擦写自己,所以IAP完全可行。思路是:Flash分成Bootloader区和App区,Bootloader负责接收新固件并写入App区,App区运行用户程序。上电时先跑Bootloader,判断有没有升级请求,有就升级,没有就跳转到App。
关键点有几个:第一,App程序要设置正确的起始地址和中断向量表偏移。在Keil里,App工程的ROM起始地址改成0x8004000(假设Bootloader占16KB),然后在main函数开头调用NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x4000)。第二,跳转前要关闭所有中断和外设,用__disable_irq()关总中断,然后设置栈指针和跳转地址。第三,Bootloader和App的串口协议要约定好,包括波特率、数据格式、校验方式、升级命令。
// 跳转到App typedef void (*pFunction)(void); pFunction jump_to_app; uint32_t app_addr = 0x08004000; __disable_irq(); SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0; jump_to_app = (pFunction)(*(volatile uint32_t*)(app_addr + 4)); __set_MSP(*(volatile uint32_t*)app_addr); jump_to_app();这段代码里,app_addr是App区的起始地址,前4个字节是栈顶地址,接下来4个字节是复位向量。跳转前一定要关中断和SysTick,否则App里的中断向量还没重映射,中断一来就跑到Bootloader的向量表里去了。
5. 常见问题与排查技巧实录
5.1 芯片识别不到、下载失败怎么办
这是新手遇到最多的问题。排查顺序我一般是这样:先查硬件,再查软件,最后查环境。硬件方面:量VDD是不是3.3V,量VDDA是不是3.3V,量BOOT0是不是低电平,量复位引脚是不是高电平。如果都正常,查SWDIO和SWCLK有没有接反,有没有被其他电路拉死。我见过有人把SWDIO接到了LED上,结果调试器识别不到,因为LED把电平拉偏了。
软件方面:检查下载算法选的对不对,F1的Flash算法是STM32F10x_128K或者256K,选错了会提示擦除失败。检查芯片型号选的对不对,F103C8和F103CB的Flash大小不一样,选错了下载地址会不对。环境方面:调试器固件是不是太老,换一个USB口试试,降低SWD速度试试。我遇到过一次是USB线的问题,线太长导致信号质量差,换根短线就好了。
5.2 串口乱码的六种可能
串口乱码是另一个高频问题。我总结下来大概六种原因:波特率不对、时钟配置不对、数据位/停止位/校验位不匹配、TX/RX接反、电平不匹配、干扰。波特率不对最常见,比如你代码里设115200,但串口助手设9600,那肯定是乱码。时钟配置不对也常见,比如你外部晶振是8MHz,但代码里按12MHz算波特率,那实际波特率就偏了。
TX/RX接反,这个不用多说,调换一下就好。电平不匹配,F1的串口是3.3V TTL电平,你如果直接接RS232的±12V,芯片可能已经烧了。干扰问题,如果线太长或者旁边有电机,加屏蔽或者降低波特率。我的排查习惯是:先用示波器量TX引脚,看有没有波形,波形周期对不对,然后再查其他。有波形但乱码,基本是波特率或者数据格式问题;没波形,查引脚配置和时钟。
5.3 中断进不去或者频繁进中断
中断进不去,先查中断使能了没有(NVIC_EnableIRQ),再查外设的中断允许位开了没有(比如USART的RXNEIE),最后查中断标志清了没有。如果标志没清,中断会一直触发,CPU一直在中断里出不来。我见过有人串口接收中断里忘了清RXNE标志,结果程序卡死在中断里,主循环根本不执行。
频繁进中断,一般是中断标志没清或者中断源没处理干净。比如按键中断,如果没有消抖,按键抖动会产生几十次中断。我的做法是按键用轮询加状态机,不用中断,或者中断里只置标志,实际处理放主循环。另外,中断服务函数里不要做耗时操作,比如浮点运算、长循环、串口打印,这些都会阻塞其他中断。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 下载失败 | BOOT0电平不对 | 量BOOT0引脚 | 拉低BOOT0 |
| 下载失败 | SWD引脚被占用 | 查原理图 | 断开外部电路 |
| 串口乱码 | 波特率不匹配 | 核对代码和助手 | 统一波特率 |
| 串口乱码 | 时钟配置错误 | 量TX波形周期 | 修正时钟树 |
| 中断不进 | NVIC未使能 | 查代码 | 加EnableIRQ |
| 中断频繁 | 标志未清 | 查中断服务函数 | 清标志 |
| ADC跳动 | 参考电压不稳 | 量VDDA | 加去耦电容 |
| ADC跳动 | 采样时间太短 | 查ADC配置 | 加长采样时间 |
| 停止模式唤醒后异常 | 时钟未恢复 | 查唤醒代码 | 重新配置时钟 |
| IAP跳转失败 | 中断未关 | 查跳转代码 | 关总中断和SysTick |
5.5 几个我踩过的坑和独家技巧
第一个坑:Flash写保护。F1的Flash默认可能有写保护,如果你用IAP写Flash发现写不进去,先查选项字节的写保护位。用ST-Link Utility或者CubeProgrammer可以看和改。第二个坑:堆栈溢出。F1的默认栈大小是0x400(1KB),如果你用了递归或者大局部数组,很容易溢出。溢出的表现是程序跑飞或者HardFault,查起来很痛苦。我的习惯是在启动文件里把栈改成0x800(2KB),堆改成0x200(512B),一般项目够用。
第三个技巧:用GPIO翻转测执行时间。在任务函数开头拉高一个空闲GPIO,结尾拉低,用示波器看高电平时间,就知道任务执行了多久。这个方法比用定时器计数还直观,而且不影响程序逻辑。第四个技巧:HardFault定位。F1进入HardFault后,可以通过查LR寄存器和栈里的PC值定位到出错代码。具体做法是在HardFault_Handler里把MSP和PSP的值读出来,然后从栈里找到PC。这个技巧网上有详细教程,我就不展开了,但学会看HardFault是嵌入式开发的必修课。
第五个技巧:批量生产时用唯一ID加密。F1每个芯片有一个96位的唯一ID,读出来可以做简单的加密校验。虽然不能防高手,但能防住大部分抄板的人。读取唯一ID的代码:
uint32_t id[3]; id[0] = *(volatile uint32_t*)0x1FFFF7E8; id[1] = *(volatile uint32_t*)0x1FFFF7EC; id[2] = *(volatile uint32_t*)0x1FFFF7F0;这个ID在芯片出厂时固化,不可修改。你可以用它生成一个密钥,和Flash里的校验值比对,不匹配就不运行。
6. 工具链与开发环境的选择建议
6.1 Keil、IAR还是GCC
Keil MDK是国内F1开发最常用的环境,优点是资料多、配置简单、调试器支持好。缺点是收费,而且编辑器功能弱。IAR的编译效率比Keil高,代码优化更好,但同样收费,而且界面更复杂。GCC加VSCode或者Eclipse是免费方案,灵活度高,但配置麻烦,调试体验不如前两者。
我的建议是:新手用Keil,团队协作看公司习惯,个人项目想折腾就用GCC。Keil的AC5编译器已经停止更新了,AC6编译器优化更好但兼容性有些问题,比如某些内联汇编语法变了。如果你用AC6,注意把C标准改成C99或者C11,否则有些语法不支持。
6.2 标准库、HAL库还是LL库
ST官方推的是HAL库,但F1时代标准库用得最多。标准库直接操作寄存器,代码效率高,但可移植性差。HAL库抽象层度高,换芯片方便,但代码体积大、执行效率低。LL库是HAL的底层版本,兼顾效率和可移植性,但F1的LL库支持不如F4完整。
我的选择是:新项目用HAL库,老项目维护用标准库,对性能有极致要求的用LL库或者直接寄存器。HAL库的代码体积比标准库大30%左右,如果你Flash紧张,可以用CubeMX生成LL库代码,或者手动优化。不管用哪个库,都建议把关键寄存器的操作搞明白,因为库函数出问题时,你最终还是要查寄存器。
6.3 调试工具的选择
ST-Link是最常用的,便宜、支持SWD、能下载能调试。J-Link性能更好,支持更多芯片,但价格贵。DAPLink是开源方案,性价比高,但稳定性看做工。我的建议是:个人学习用ST-Link或者DAPLink,公司项目用J-Link。调试器固件记得定期更新,老固件可能不支持新芯片或者有bug。
逻辑分析仪和示波器也是必备的。逻辑分析仪看数字信号(SPI、I2C、串口)很方便,示波器看模拟信号和电源纹波。我买过一个几十块的逻辑分析仪,配合开源软件,抓串口和SPI波形完全够用。示波器不用太高端,100MHz带宽、1GSa/s采样率就能应付大多数F1项目。
7. 从F1延伸到其他芯片的学习路径
F1学会之后,往哪个方向走?我的建议是先横向扩展同系列,比如F0(低成本)、F4(高性能)、F7(高性能加图形)。F1到F4的迁移主要是时钟树更复杂、外设更多、寄存器更丰富,但基本概念是通的。再纵向深入,比如学RTOS(FreeRTOS、RT-Thread)、学协议栈(lwIP、FatFs)、学电机控制(FOC)、学USB。这些在F1上都能跑,只是资源紧张一点。
如果你F1用得很熟,我建议花时间学一下RTOS。F1跑FreeRTOS完全没问题,20KB RAM可以跑几个任务。RTOS能让你从“超级循环加状态机”的思维升级到“任务加信号量加队列”的思维,对做复杂项目很有帮助。再往后可以学一下Bootloader和OTA,这是产品化的必备技能。F1的IAP虽然简单,但原理和高端芯片是一样的,学会了迁移过去很快。
最后说一句我的真实体会:F1不是最强的芯片,但它是最适合打基础的芯片。你把F1的时钟、GPIO、中断、串口、DMA、ADC、Flash这些都搞透了,换任何芯片都是查手册加改配置的事。我见过太多人直接上F4或者国产高端芯片,结果遇到问题连时钟树都看不懂,只能到处问人。基础不牢,地动山摇,这句话在嵌入式领域特别适用。