做嵌入式这行,硬盘里堆着十几个版本的固件包几乎是常态,但真正能一次跑通的却没有几个。STM32 HAL库 F1 系列资源下载这件事听起来像"点个链接、解个压缩包"这么简单,实际操作过的人都知道,从找到对的包、认清自己板子上那颗芯片的容量档次、到把工程编译通过、串口打印出第一行字符,中间隔着一堆没人写进说明书的细节。我这几年带过几批新人,几乎每个人都在同一个地方卡过:包下错了版本、器件支持包没装、时钟宏和板上晶振对不上,然后对着一个"能编译但跑不起来"的工程怀疑人生。这篇内容就是把这条链路完整走一遍——从认清资源包里有什么,到下载路径怎么选,到工程怎么落地,再到那些反复出现的坑怎么绕。适合刚接触 F1 系列、准备用 HAL 库起第一个工程的人,也适合已经用了一阵、但工程结构一直比较随意、想整理一下资源管理方式的人。
1. 一份"HAL库资源"到底包含哪些东西
很多人对"下载一个 HAL 库"的理解是"拿到几个 .c 和 .h 文件",这个理解会让你在后续配置里处处被动。实际上官方发布的 F1 系列固件包是一个完整的分层工程集合,里面既有驱动源码,也有可直接编译的示例工程,还有配套的中间件和文档。你不清楚它的组织方式,就不知道哪些文件该拷进自己的工程、哪些只是参考样例。
1.1 HAL 库与早期标准外设库的根本差异
早期 F1 系列流行的是标准外设库(SPL),它的思路是"直接操作寄存器的一层薄封装":你要开一个串口,就调USART_Init(),函数内部把寄存器一位一位配好,配置项全部塞进一个结构体,没有状态、没有句柄、没有回调。HAL 库换了一套模型:每个外设有一个句柄结构体(比如UART_HandleTypeDef),里面除了配置参数,还保存了工作状态、错误码、DMA 关联指针;操作方式从"配置一次就完事"变成"初始化 → 启动 → 中断/回调里处理事件"。
这个转变带来的直接后果是:HAL 库代码量比 SPL 大不少,一个最小工程编译出来占用的 Flash 明显更多。以 F103C8T6 这颗 64KB Flash 的芯片为例,SPL 点灯工程通常几 KB,HAL 库点灯工程在开启串口和基本外设后,10KB 上下很常见。所以如果你的板子是小容量型号(16KB 或 32KB Flash),用 HAL 库时要格外注意裁剪,把stm32f1xx_hal_conf.h里用不到的模块宏直接关掉——这个头文件里的每个#define HAL_XXX_MODULE_ENABLED只要注释掉,对应模块的源码就不会参与编译。
另一个差异是中断处理的组织方式。SPL 时代你在stm32f10x_it.c里直接写业务逻辑;HAL 库里中断入口只负责调用HAL_XXX_IRQHandler(),真正的处理放在HAL_XXX_Callback弱函数里,你重写回调即可。这个设计对初学者很友好,但也埋了一个坑:很多人自己在stm32f1xx_it.c里又写了一份USART1_IRQHandler,结果和 HAL 的入口重复定义,链接时报 multiply defined。后面第 5 章会专门讲这类问题。
1.2 F1 系列内部分档:先认清手上是哪一档
F1 系列不是一个型号,而是一整个家族,按 Flash 容量通常分成几档,不同档次在启动文件、链接脚本、部分外设支持上都不一样。判断错了档次,最常见的结果是程序能编译、能下载,但一进中断就跑飞,或者堆栈莫名溢出。
| 档次 | 典型 Flash 容量 | 常见代表型号 | 启动文件选择 |
|---|---|---|---|
| 小容量 | 16–32KB | 尾缀 6/8 的部分型号 | startup_stm32f10x_ld.s |
| 中容量 | 64–128KB | STM32F103C8T6、F103RBT6 | startup_stm32f10x_md.s |
| 大容量 | 256–512KB | F103ZET6、F103VCT6 | startup_stm32f10x_hd.s |
| 互联型 | 64–256KB | F105/F107 系列 | startup_stm32f10x_cl.s |
更麻烦的是同型号不同尾缀。比如 C8T6 和 CBT6 外观一样、引脚一样,Flash 一个是 64KB 一个是 128KB。如果你按 64KB 配置却写了 100KB 的程序,Keil 会在链接阶段报 region overflow,这个还算友好;真正阴险的是 RAM 判断错——中容量型号一般 20KB RAM,大容量 64KB,堆栈大小按错的来配,程序跑几分钟才崩,排查起来非常费劲。
我一般建议:拿到板子第一件事不是下载资源包,而是把芯片丝印拍照放大看清楚,然后去原厂的产品选型页面查这颗料的确切参数,把 Flash、RAM、主频、封装记在工程目录的 README 里。这一步花五分钟,后面能省几小时。
1.3 资源包里真正会被反复打开的文件就那么几个
一个完整的 F1 固件包解压后目录层级不浅,但日常开发真正高频接触的其实集中在Drivers/STM32F1xx_HAL_Driver下面:
Inc/与Src/:每个外设一对文件,如stm32f1xx_hal_gpio.c/h、stm32f1xx_hal_uart.c/h,以及公共核心stm32f1xx_hal.cstm32f1xx_hal_conf.h:模块开关、晶振频率、断言开关都在这,是整个库的"总闸"stm32f1xx_hal_msp.c:MSP 意为 MCU Support Package,负责外设的底层引脚、时钟、中断优先级初始化,CubeMX 生成的代码里这部分是你需要关注的重点stm32f1xx_it.c:中断向量入口system_stm32f1xx.c:系统时钟初始化,SystemInit()在这里
另外Projects/目录下按开发板分类的示例工程价值极高,但用法要讲究:把它当参考,不要当依赖。直接拿示例工程改,很容易因为板子引脚定义不同而调不通,而且示例工程往往按某一款特定开发板的晶振配置,换板子就出问题。
2. 下载之前先定三件事,能省掉后面一半的返工
资源下载这件事的返工率之所以高,是因为大部分人是在"还没想清楚要什么"的状态下开始找包的。先把三件事定下来,你会发现可选的路径其实很窄,选择困难自然消失。
2.1 型号、封装与容量:从丝印到宏定义
前面说了要认清档次,这里讲怎么把这个认知落到代码里。以中容量 F103C8T6 为例,工程里通常有两个地方要显式声明容量:
- 启动文件的选择(
startup_stm32f10x_md.s) - 预编译宏,比如 Keil 的 C/C++ 选项里定义
STM32F10X_MD,同时在system_stm32f1xx.c或工程的全局宏中定义USE_HAL_DRIVER
用 CubeMX 生成工程时这些会自动配好,但如果你的工程是从别处拷来的模板,或者从示例工程改的,这两处一定要核对。我见过一个案例:模板来自 F103ZE(大容量),启动文件用的hd,换到 C8T6 上编译通过、下载成功,但中断进不去——因为向量表偏移和中断数量按大容量配置,实际芯片的中断向量表不一样。查了两天才发现是启动文件的事。
封装虽然不影响固件包的选择,但影响你的引脚规划。LQFP48 和 LQFP64 可用的 GPIO 数量差不少,做引脚分配前先把封装确定下来,避免 Pinout 图画完才发现想要的引脚压根没引出来。
2.2 版本号怎么选:不要无脑追最新
F1 系列固件包的版本号形如STM32Cube_FW_F1_V1.8.x,主版本号变化通常伴随 API 调整或新增器件支持,小版本号是缺陷修复。选版本的建议很朴素:
- 新项目:选当前最新的稳定版,享受 bug 修复
- 已有量产工程:能不动就不动,除非你遇到了在新版本里被修复的具体问题
- 跟着教程学:跟教程作者用同一个大版本,避免 API 不一致导致"照抄跑不通"
版本差异造成的最典型问题是某些 HAL 函数签名变化。早期版本的HAL_UART_Transmit和后期在超时参数处理上有细微差别,还有一些回调函数的调用时机调整过。这些差异在官方每个版本的 Release Note 里都写得很清楚,但 Release Note 是最没人看的文档。我个人的习惯是:换版本前至少扫一遍 Release Note 的 "Breaking changes" 部分,五分钟的事。
2.3 开发环境先定,再决定下哪种形态的包
同一个固件包,在不同环境下的"下载形态"是不一样的:
| 使用环境 | 需要的形态 | 说明 |
|---|---|---|
| CubeMX + Keil/IAR | 完整固件包(Firmware Package) | CubeMX 里指定本地包路径即可调用 |
| CubeIDE | 通过内置管理器在线拉取 | 也可指向本地包,适合内网环境 |
| 纯手工工程 | 只需要 HAL Driver 目录 | 整个包里拷Drivers一部分就够 |
| 只想看示例 | 完整包 + 对应开发板目录 | Projects/下按板子分类 |
如果你的电脑不能联网,或者公司网络对下载有限制,那就要提前把完整的离线包拿到手,并且想清楚放在哪个固定路径。CubeMX 有个"本地包"选项,指过去之后就不需要每次联网校验,这个对反复重装环境的场景特别有用。
3. 官方固件包的获取方式与离线备份
这一节讲实际操作。我把获取路径按可靠性排个序,然后讲拿到手之后怎么存放,最后澄清一个高频混淆点:固件包和器件支持包不是一回事。
3.1 三条获取路径的取舍
第一条,集成开发环境内置的包管理器。CubeMX 和 CubeIDE 都自带固件包管理界面,能列出所有系列、所有版本的包,点一下就开始下载,装完自动放到统一目录。这是最省心的方式,优点是路径自动管理、版本一目了然,缺点是依赖网络、首次下载体积不小(完整包通常在几百 MB 量级,因为包含了所有示例工程和中间件)。
第二条,官方发布页的独立压缩包。适合需要离线分发、团队内共享、或者要做版本归档的场景。拿到的是一个压缩包,解压即用,不依赖任何工具。我倾向于至少保留一份这种形态的包在做长期备份的目录里,理由很简单:工具会升级、管理器的下载源会变,但一个躺在硬盘上的压缩包十年后还能解压。
第三条,第三方渠道。国内很多资源站会转存这些包,好处是下载速度快、不需要账号,坏处是你无法确认包内文件是否被改动过。这个风险不是危言耸听:我见过压缩包里被塞了额外的工程模板、被替换过的启动文件,甚至有把示例工程的晶振配置改成某个特定开发板参数的。对初学者来说,这类改动极难察觉,因为它不报错,只是"行为有点怪"。
提示:任何来源的资源包,解压后建议先核对目录结构和文件数量是否与官方发布说明一致,重点看
Drivers目录下文件是否齐全、Projects目录是否完整。发现多出来的可疑目录或脚本,直接丢弃整个包,不要抱着"删掉就行"的心态继续用。
3.2 离线包解压后该放在哪
路径规划这件事,吃过一次亏就会记一辈子。核心原则三条:全英文、无空格、层级浅。
具体的目录结构我通常这样组织:
D:/mcu/ ├── fw_packages/ │ ├── STM32Cube_FW_F1_V1.8.0/ │ ├── STM32Cube_FW_F1_V1.8.5/ │ └── STM32Cube_FW_F4_V1.27.0/ ├── packs/ │ └── (器件支持包归档) ├── projects/ │ ├── f103c8t6_blink/ │ └── f103rbt6_uart/ └── docs/ └── release_notes/为什么不放在桌面或者"文档"目录?因为这类路径在中文系统里常常带用户名中文、带空格(C:/Users/张三/Desktop/新建文件夹),而部分编译工具链对空格和中文路径的处理并不完善,尤其是调用外部 make、链接脚本、以及某些版本的头文件搜索逻辑。表现出来就是"明明文件在那儿,编译器说找不到"。这种问题排查起来极耗时间,因为报错信息往往指向一个看起来完全正常的路径。
版本目录名我坚持用官方的原始名(带版本号),不改成latest之类的别名。原因很实际:半年后你打开硬盘,只看到latest根本不知道那是哪个版本,而工程里引用的头文件路径是写死的,想回退都找不到对应关系。
3.3 器件支持包(DFP)是另外一件事
这是初学者最容易混淆的点。固件包(Firmware Package)是 HAL 库源码 + 示例工程;器件支持包(Device Family Pack,DFP)是给 IDE 用的器件描述文件,里面包含 Flash 烧写算法、寄存器定义、调试配置、芯片选型信息。
两者解决的是完全不同的问题:
- 固件包缺失 → 编译报找不到
stm32f1xx_hal.h - 器件包缺失 → 选型列表里找不到你的芯片,或者下载时报"Flash algorithm not found"
所以一个完整可用的环境需要两者都到位。器件包一般通过 IDE 自带的 Pack Installer 安装,也能下载独立的离线包文件(后缀通常是.pack)双击安装。内网环境或者多台机器部署时,把.pack文件和固件包一起归档,是新机器环境搭建最快的方式,比一台台去联网装快得多。
4. 从压缩包到第一个能点亮LED的工程
资源到位之后,落地成工程这一步是分水岭。有人十分钟跑通,有人卡两天。差别不在手速,在于是否知道每个配置项背后的含义。这一节按实际操作顺序走一遍。
4.1 用 .ioc 建立最小工程
CubeMX 新建工程的流程:选芯片型号(直接搜型号比按系列翻快得多)→ 配置调试接口 → 配时钟源 → 配引脚 → 配时钟树 → 生成代码。
第一步选型时有个细节值得强调:一定要确认选中的具体型号尾缀,不要看到"STM32F103C8"就点,因为列表里可能有 C8T6、C8T6TR、C8T6A 等多个条目,封装和温度等级不同。选错影响不大但会在生成代码的宏定义里体现,后期看着别扭。
调试接口配置是新手最容易漏的一步,也是最容易把板子"锁死"的一步。F1 系列支持 SWD 和 JTAG,日常用 SWD 两根线就够。CubeMX 里System Core → SYS → Debug选Serial Wire。如果你不配这一项,生成代码后调试口会被当成普通 GPIO,第一次下载之后就可能连不上了,需要靠复位时序去救,很麻烦。我在带新人的时候会把这条写在第一页:先配 Debug,再配别的。
4.2 时钟树配置:72MHz 是怎么算出来的
F103 系列最高主频 72MHz,这个数字不是直接来的,而是"外部晶振 × PLL 倍频"算出来的。以板上 8MHz 晶振为例:
- HSE = 8MHz(外部高速晶振)
- HSE 作为 PLL 输入,PLL 倍频系数 = 9
- SYSCLK = 8 × 9 = 72MHz
- AHB 预分频 = 1 → HCLK = 72MHz
- APB1 预分频 = 2 → PCLK1 = 36MHz(APB1 最高 36MHz,超了会不稳定)
- APB2 预分频 = 1 → PCLK2 = 72MHz
CubeMX 的时钟树界面会自动校验这些约束,超频会标红。但有个陷阱:如果你板上晶振不是 8MHz 而是 12MHz,而你在 CubeMX 里仍然填 8MHz,界面显示 72MHz 一切正常,实际芯片跑的是 108MHz。表现是串口波特率全错、延时不准、某些外设时好时坏。这类问题的排查难度在于"代码完全正确,就是行为不对"。
所以配时钟树之前,先确认板上晶振丝印。常见的有 8MHz 和 12MHz 两种,也有用内部 RC(HSI,8MHz)的方案。用内部 RC 时精度差一些,串口高波特率下容易出错,一般只在对成本极敏感的场合用。
4.3 生成代码后先看这四个文件
CubeMX 生成代码后,不要急着写业务逻辑,先花两分钟看四个文件,能避免 80% 的"莫名其妙":
main.c:看SystemClock_Config()里的 PLL 参数和你配置的是否一致;看MX_GPIO_Init()里引脚模式对不对stm32f1xx_hal_conf.h:确认HSE_VALUE是否等于板上晶振频率(这个后面第 5 章细说)stm32f1xx_hal_msp.c:看外设时钟使能、引脚复用、中断优先级有没有按预期配置stm32f1xx_it.c:看中断入口是否存在、是否调用了 HAL 的处理函数
特别是第三个文件,很多人遇到"外设初始化了但不工作"的问题,根因就在 MSP 里某个时钟没使能。CubeMX 一般会自动生成,但如果你后来手动改了引脚或者换了外设实例,MSP 里的内容可能没跟着更新。
4.4 编译下载与 printf 重定向
代码生成完,编译前检查工程设置:目标芯片型号、启动文件、预定义宏(USE_HAL_DRIVER、STM32F10X_MD)、头文件搜索路径(指向Drivers/STM32F1xx_HAL_Driver/Inc和Drivers/CMSIS/Device/.../Include)。这几项 CubeMX 生成时通常自动配好,但如果你在多个 IDE 之间搬工程,一定要逐项核对。
串口打印是调试的命脉,重定向printf需要两步。第一步,重写底层输出函数:
#include <stdio.h> int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }第二步,在 Keil 的 Target 选项里勾选Use MicroLIB。不勾选的话,标准库的printf会依赖半主机(semihosting)机制,而半主机在独立运行(不接调试器)时没有响应,程序会卡死在第一次打印上——这个现象非常有迷惑性:接调试器跑得好好的,拔了调试器一上电就不动了。
如果你的工程用的是 GCC 工具链,重定向方式不一样,需要实现_write()而不是fputc():
int _write(int fd, char *ptr, int len) { HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, 0xFFFF); return len; }工具链不同、重定向函数不同,这一条在跨环境搬工程时特别容易踩。
5. 下载和使用环节里那些反复出现的坑
前面讲的都是"顺利路径",这一节专门讲不顺利的情况。这些问题我在不同项目里反复见过,基本覆盖了初学者 90% 的卡壳场景。
5.1 第三方打包资源的隐性成本
先说下载环节的风险。第三方资源站的包之所以"看起来能用",是因为它们绝大多数确实是从官方包原样转存的,问题出在少数情况:
- 包内被追加了推广内容:常见形式是多出来的工程模板目录、Readme 里塞的链接、批处理脚本。这些本身无恶意,但会干扰你判断哪些是官方内容
- 文件被"优化"过:有的打包者会删掉自认为无用的目录(比如
Projects下的示例)来减小体积,结果你按教程去找示例工程找不到 - 版本标注与实际不符:标称 V1.8.0 实际是更早的版本,导致 API 和文档对不上
判断方法很土但有效:打开包内根目录的版本说明文件,对比里面的版本号和你以为的版本号;看Drivers/STM32F1xx_HAL_Driver/Src下的文件数量是否与你预期相符;随便打开一个.c文件看头部的版本注释。这三步两分钟,能过滤掉绝大多数问题包。
注意:如果包内出现
setup.exe、注册机、破解补丁、一键配置.bat之类的文件,直接整包删除。正常的固件包就是纯源码目录,不会有任何可执行安装程序。
5.2 路径、中文、空格:编译器最不喜欢的组合
这个问题前面提过,这里展开讲表现形式和排查方法。
典型症状:编译报 "cannot open source input file xxx.h",但你打开文件管理器,那个路径下文件明明在。或者链接阶段报找不到某个.o,但源文件存在且已被加入工程。再或者,代码能编译通过,但调试时断点对不上行号。
根因通常是路径里含空格或中文,经过工具链的多层转义后变成非法路径。有些工具链会静默截断路径,有些会报错,有些(最气人的)会在某些环节正常工作、某些环节失败。判断方法:把工程整体拷到一个纯英文、无空格、层级较浅的路径下(比如D:/work/test1),重新编译。如果问题消失,那就是路径问题。
工程内部也不要出现中文文件名。我见过有人把外设驱动文件命名为串口驱动.c,在自己的机器上跑得好好的,发给同事后一堆报错——因为对方的工具链编码设置不同。文件命名统一用英文加下划线,是个看起来琐碎但能省掉很多跨机器问题的习惯。
5.3 HSE_VALUE 与实际晶振不一致导致的"玄学"
这是 F1 + HAL 库里最经典的一个坑,值得单独拎出来。
stm32f1xx_hal_conf.h里有一个宏:
#if !defined(HSE_VALUE) #define HSE_VALUE 8000000U /*!< Value of the External oscillator in Hz */ #endif这个宏告诉库"外部晶振是多少赫兹"。库用它来计算 SysTick 的重装载值(进而影响HAL_Delay的准确性)、串口波特率的分频系数、以及某些外设的时序。
如果板上是 12MHz 晶振但这个宏还是 8000000,会发生什么?
HAL_Delay(1000)实际延时约 667ms(比例 8/12)- 串口波特率偏差 50%,通信全是乱码
- 定时器计数值不准
- 一切"看起来在工作但数据不对"
最坑的地方在于它不报错。程序正常运行、外设正常初始化、就是数据不对。新手会怀疑是通信协议的问题、是对方设备的问题、是硬件的问题,唯独想不到是这一行宏。
排查方法:一旦发现"延时不准"或"波特率对不上",第一步就去核HSE_VALUE。如果你用的是 CubeMX 生成的工程,这个值一般会跟着时钟树配置自动改,但如果你手动改过时钟树、或者工程是从别处拷来的,一定要手动确认。另外注意,SystemClock_Config()里的 PLL 参数和HSE_VALUE必须匹配,两者任一错了,结果都是错的。
5.4 HAL_Delay 卡死与 SysTick 优先级
HAL_Delay的实现依赖 SysTick 中断:它记录当前 tick,然后在一个 while 循环里等 tick 差值达到目标。所以如果 SysTick 中断不响应,这个函数就永远不会返回。
两种典型卡死场景:
场景一,在中断服务函数里调用HAL_Delay。如果你的中断优先级高于 SysTick(数字更小),进入中断后 SysTick 无法抢占,tick 不增长,HAL_Delay死等。这在串口接收中断里写延时是最常见的诱因。正确做法是中断里只做标记,延时和业务处理放到主循环。
场景二,HAL_Init()之前调用了HAL_Delay。SysTick 还没配置,tick 永远是 0。这个一般不会犯,但如果在main()开头写了自定义的初始化函数并调了延时,就容易踩。
顺带说一句,HAL_Delay的精度受中断响应影响,不适合做精确定时。需要微秒级或者高精度延时,用定时器 + 计数,或者用 DWT 周期计数器,别指望HAL_Delay。
5.5 重新生成代码把用户代码冲掉
CubeMX 生成代码时会在特定位置插入注释标记:
/* USER CODE BEGIN 2 */ // 你自己的代码写在这里面 /* USER CODE END 2 */只有夹在这对标记之间的内容会被保留。写在标记外面的代码,下次点"Generate Code"就没了。这个机制设计得很合理,但新人常常忽略,写了半天逻辑,改了个引脚重新生成,全没了。
我的习惯是:任何手写的业务代码都不放在生成文件里,而是单独建app_xxx.c/h,在main()的 USER CODE 区里调用入口函数。这样即使重新生成、即使换了生成工具,业务逻辑都安全。工程结构也更清晰,后期交接或者复用都方便。
另外一个相关的问题:.ioc文件要纳入版本管理。它是工程配置的唯一真相来源,丢了之后想重新生成出同样的配置基本不可能,只能一个个选项重新点。
6. 多版本共存与工程长期维护
资源管理和工程维护是两件容易被忽视的事。刚开始做一个工程时感觉不到,手上同时有三五个工程、跨越两三个固件包版本时,问题就集中爆发了。
6.1 目录规划:一个版本一个坑位
前面给的目录结构里,固件包按版本号平铺存放。这样做的好处是:每个工程的依赖关系是明确的,工程 A 用 V1.8.0,工程 B 用 V1.8.5,互不干扰。坏处是占硬盘——一个完整固件包几百 MB,存五六个版本就是几个 GB。
占硬盘和"某天想回退版本但找不到包"之间,我选前者。硬盘便宜,时间贵。如果真的空间紧张,可以做一次裁剪归档:只保留Drivers目录 + 版本说明文件,示例工程删掉。Drivers目录通常只有几十 MB,压缩后更小,而且这才是工程真正依赖的部分。
工程目录的组织上,我倾向于"一个工程一个目录,目录内自带依赖副本"。也就是说,工程目录下有一份Drivers的拷贝,而不是通过相对路径去引用全局的固件包目录。代价是每个工程多占几十 MB,收益是工程可以整体打包发给别人、可以整体归档、换机器直接能编译。做团队协作时这个收益远大于成本。
6.2 .ioc 与生成代码要不要进版本库
如果工程用 Git 管理,建议这样处理:
| 文件/目录 | 是否入库 | 理由 |
|---|---|---|
.ioc配置文件 | 必须 | 配置的唯一来源,丢失无法重建 |
Core/下的.c/.h | 入库 | 含 USER CODE 区的手写逻辑 |
Drivers/固件库 | 建议入库 | 保证任何人克隆后版本一致 |
编译中间产物(Objects/、Debug/) | 不入库 | 体积大、可重新生成 |
.uvprojx等工程文件 | 入库 | 工程配置含大量手工设置 |
烧录产物(.hex、.bin) | 视情况 | 发布版本可单独归档,不必每次提交 |
把Drivers入库这个选择有争议,有人觉得应该用子模块或者包管理器。我的看法是:这个领域的大部分团队规模不大、协作流程不复杂,直接把依赖入库最省心,克隆下来就能编译,不需要额外步骤。等团队规模上去了再考虑更精细的方案。
6.3 例程的正确用法:当参考不当依赖
Projects/目录下的示例工程是固件包里价值最高的部分,但用法要对。
正确的用法是"读"而不是"改"。遇到某个外设不知道怎么配,找到对应示例,读它的初始化流程、看它怎么处理中断、看它怎么组织状态机。这些示例都是经过验证的,能帮你避开很多低级错误。
错误的用法是直接拿示例工程当项目起点。原因有三:示例工程按特定开发板配置(晶振、引脚、外设实例都可能不同);示例往往为了演示某个特性而做了精简,缺少错误处理和资源管理;示例的工程设置里可能包含调试用的特殊配置,直接拿去用会有隐患。
我通常的做法是:新建一个干净的工程,把示例工程里对应的外设初始化代码抄过来,然后逐项核对引脚和时钟配置。抄的过程本身就是理解的过程,比跑通一个现成工程收获大得多。
最后分享一个我在实际项目里养成的小习惯:每接手一个工程,先在根目录写一个README,记录四件事——用的哪个固件包版本、板上晶振频率、芯片型号和容量档次、工程跑起来需要的环境版本。这四条信息在半年后回头看的时候,价值比任何注释都大,因为它们决定了这个工程还能不能被重新编译出来。