
简介这是一套面向嵌入式初学者与STM32项目开发者的HAL库标准工程模板专为STM32F103系列单片机设计覆盖I2C、SPI、UART、USART、ADC、TIM、SD、MMC、IRDA等核心外设驱动例程助力快速搭建可移植的底层开发框架。资源共160个文件以83个头文件.h定义寄存器映射与接口规范69个源文件.c实现模块化功能逻辑辅以KEIL工程配置文件.uvprojx/.uvoptx、调试脚本keilkilll.bat、固件镜像.hex及说明文档.txt整体压缩包仅930KB轻量易导入。已有99人学习下载适合正在开展物联网终端、传感器节点或基础外设实验的开发者。代码全程采用ST官方HAL库编写含详尽中文注释各模块引脚定义清晰写入源码接线与适配说明完备支持F103全系芯片仅需在KEIL中调整目标型号与Flash容量即可复用同时明确提示J-Link/ST-Link调试器选择要点显著降低入门门槛与移植成本。1. 这不是“例程压缩包”而是 HAL 库工程结构的最小可信基线你解压STM32F103单片机HAL库例程-HAL标准模板.rar后看到的.ioc文件、Core/Inc和Src目录、main.c里那几行HAL_Init()和SystemClock_Config()表面是“能跑的模板”实则是 STM32F103 工程启动链的显式契约它强制暴露了时钟树配置、中断向量表重定向、HAL 初始化顺序、外设句柄生命周期管理这四层隐性依赖。很多开发者在移植 DHT11 或 OLED 驱动时卡在HAL_TIM_Base_Start_IT()返回HAL_ERROR根本原因不是代码写错而是这个模板里RCC_OscConfig中 HSE 启用失败未被HAL_RCC_GetOscConfig()检出导致后续所有外设时钟门控失效——而标准模板恰恰把这类校验逻辑封装在HAL_RCC_OscConfig()的返回值处理中而非默认忽略。它适合两类人刚从标准外设库StdPeriph转过来、需要厘清 HAL 封装边界的老手以及正在搭建量产项目基础框架、必须规避 CubeMX 自动生成代码中冗余宏定义的新团队。不适用于直接抄函数改引脚的速成学习者。2. 从.ioc到main.cHAL 标准模板的三层初始化契约HAL 标准模板的核心价值不在功能代码而在其强制约定的初始化次序与资源绑定关系。它把 STM32F103 的启动过程拆解为三个不可跳过的阶段系统级初始化RCC SysTick、外设句柄注册hxxx结构体实例化、运行时上下文建立中断优先级 DMA 绑定。这三步若顺序错乱或缺失校验会导致HAL_GPIO_WritePin()无响应、HAL_UART_Transmit()卡死在HAL_TIMEOUT等典型问题。2.1 第一层HAL_Init()与SystemClock_Config()的耦合陷阱标准模板中main()开头必有HAL_Init(); SystemClock_Config();但HAL_Init()并非仅初始化 SysTick——它还执行HAL_MspInit()由用户实现而SystemClock_Config()内部调用HAL_RCC_OscConfig()和HAL_RCC_ClockConfig()。关键在于HAL_Init()必须在SystemClock_Config()之前调用否则HAL_GetTick()返回值恒为 0所有带超时的 HAL 函数如HAL_UART_Receive()将立即超时。常见误操作是把HAL_Init()放到SystemClock_Config()之后尤其在移植旧代码时。验证方法是在HAL_Init()后插入uint32_t tick_start HAL_GetTick(); // 必须 0 HAL_Delay(1); // 若卡住说明 SysTick 未启用 uint32_t tick_end HAL_GetTick(); if (tick_end tick_start) { Error_Handler(); // SysTick 初始化失败 }提示HAL_Init()内部调用HAL_MspInit()该函数需用户实现。若未重写HAL_MspInit()则HAL_Init()不会配置任何 GPIO 或中断仅初始化 SysTick 和 NVIC 基础寄存器。2.2 第二层外设句柄hxxx的静态分配与MX_xxx_Init()的绑定规则模板中每个外设如 UART1、TIM2对应一个全局UART_HandleTypeDef huart1;和TIM_HandleTypeDef htim2;。这些句柄必须在MX_xxx_Init()函数内完成全部字段初始化包括Instance指向寄存器基地址如USART1Init结构体配置波特率、模式等pTxBuffPtr/pRxBuffPtrDMA 缓冲区指针若启用 DMALock互斥锁HAL 自动管理错误做法是仅声明句柄而不调用MX_USART1_Init()此时huart1.Instance为NULLHAL_UART_Transmit()直接触发 HardFault。正确流程如下// 在 main.c 中 UART_HandleTypeDef huart1; void MX_USART1_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; if (HAL_UART_Init(huart1) ! HAL_OK) { Error_Handler(); // 必须检查返回值 } }注意HAL_UART_Init()内部调用HAL_UART_MspInit(huart1)后者由 CubeMX 生成负责 GPIO 时钟使能、引脚复用配置__HAL_RCC_GPIOA_CLK_ENABLE()GPIO_InitStruct.Alternate GPIO_AF7_USART1。若手动修改引脚必须同步更新HAL_UART_MspInit()中的GPIO_InitStruct.Pin和GPIO_InitStruct.Alternate。2.3 第三层中断优先级与HAL_NVIC_SetPriority()的硬编码风险标准模板中MX_GPIO_Init()和MX_USART1_Init()均调用HAL_NVIC_SetPriority()设置中断优先级。例如HAL_NVIC_SetPriority(USART1_IRQn, 0, 1); HAL_NVIC_EnableIRQ(USART1_IRQn);此处0是抢占优先级Preemption Priority1是子优先级Subpriority。STM32F103 使用 Cortex-M3 的 NVIC共支持 4 位优先级分组通过NVIC_PriorityGroupConfig()设置默认为NVIC_PriorityGroup_22 位抢占 2 位子优先级。若未显式调用NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)而HAL_NVIC_SetPriority()传入(0,1)实际生效的优先级可能因分组不同而错乱——例如在NVIC_PriorityGroup_0下(0,1)会被截断为(0,0)导致多个中断无法嵌套。标准模板通常在HAL_Init()后、SystemClock_Config()前插入HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);这是必须项而非可选项。3. 外设驱动落地以 UART1 与 UART3 的差异为例验证模板健壮性STM32F103 的 UART1 和 UART3 物理资源不同UART1 挂载在 APB2 总线最高 72MHzUART3 挂载在 APB1 总线最高 36MHz且 UART3 的 TX/RX 引脚复用功能编号AF与 UART1 不同。标准模板若未区分处理会导致HAL_UART_Transmit()在 UART3 上发送失败。本节以实际调试过程说明如何基于模板修正。3.1 引脚复用与时钟使能的物理约束UART1 默认使用 PA9TX、PA10RX复用功能为GPIO_AF7_USART1UART3 默认使用 PB10TX、PB11RX复用功能为GPIO_AF7_USART3。关键点在于APB1 总线时钟必须在HAL_RCC_ClockConfig()中显式使能否则__HAL_RCC_USART3_CLK_ENABLE()无效。标准模板中SystemClock_Config()通常只使能 APB2 时钟需手动添加// 在 SystemClock_Config() 的 RCC_PeriphCLKInitTypeDef 结构体配置后 __HAL_RCC_USART3_CLK_ENABLE(); // 必须添加同时在MX_USART3_Init()的HAL_UART_MspInit()中// 错误复用功能写成 GPIO_AF7_USART1 GPIO_InitStruct.Alternate GPIO_AF7_USART1; // ❌ // 正确UART3 对应 GPIO_AF7_USART3 GPIO_InitStruct.Alternate GPIO_AF7_USART3; // ✅3.2 波特率计算公式的硬件差异UART1 的最大波特率受 APB2 频率限制72MHzUART3 受 APB1 频率限制36MHz。HAL 库计算波特率时使用公式USARTDIV (fPCLK / (16 * BaudRate))当fPCLK为 36MHzUART3时若设置BaudRate115200USARTDIV ≈ 19.53需取整为 19 或 20误差约 0.3%若误用 APB2 频率 72MHz 计算则USARTDIV ≈ 39.06取整后误差翻倍。标准模板中huart3.Init.BaudRate的值必须与实际 APB1 频率匹配。验证方法用逻辑分析仪抓取 UART3 波形测量实际比特时间若偏离理论值 3%需重新计算USARTDIV并手动设置huart3.Instance-BRR寄存器绕过 HAL。3.3 中断服务函数ISR的模板适配标准模板生成的stm32f103xe_it.c包含USART1_IRQHandler()但不会自动生成USART3_IRQHandler()。必须手动添加void USART3_IRQHandler(void) { HAL_UART_IRQHandler(huart3); // 必须传入对应句柄 }且需确保huart3在全局作用域声明并初始化。若遗漏此函数UART3 接收中断将无法触发HAL_UART_RxCpltCallback()表现为HAL_UART_Receive_IT()后无回调。4. HAL 库 Flash 操作在标准模板中安全擦除与写入STM32F103 的 Flash 操作如保存参数、固件升级极易因未遵循 HAL 库的原子性约束而损坏扇区。标准模板未包含 Flash 示例但其初始化结构为安全操作提供了基础——必须利用HAL_FLASH_Unlock()、HAL_FLASH_Lock()和HAL_FLASHEx_Erase()的严格配对。4.1 Flash 擦除前的三重校验Flash 擦除不可逆标准模板要求在调用HAL_FLASHEx_Erase()前完成三项检查地址对齐目标地址必须是扇区起始地址F103 扇区大小为 1KB起始地址为0x08000000,0x08000400, ...电压范围HAL_PWREx_GetVoltageRange()必须返回PWR_VOLTAGE_RANGE_22.7–3.6V否则HAL_FLASHEx_Erase()返回HAL_ERROR写保护状态FLASH_OB_GetWriteProtection()必须为OB_WRP_AllPages即未写保护示例代码FLASH_EraseInitTypeDef pEraseInit; uint32_t PageError 0; // 1. 地址校验 if ((0x08004000 (FLASH_PAGE_SIZE - 1)) ! 0) { Error_Handler(); // 地址未对齐 } // 2. 电压校验 if (HAL_PWREx_GetVoltageRange() ! PWR_VOLTAGE_RANGE_2) { Error_Handler(); // 电压不足 } // 3. 写保护校验 if (FLASH_OB_GetWriteProtection() ! OB_WRP_AllPages) { Error_Handler(); // 存在写保护 } pEraseInit.TypeErase TYPEERASE_PAGES; pEraseInit.PageAddress 0x08004000; pEraseInit.NbPages 1; if (HAL_FLASHEx_Erase(pEraseInit, PageError) ! HAL_OK) { Error_Handler(); // 擦除失败 }4.2 Flash 写入的时序约束与缓存刷新HAL 库写入 Flash 时必须禁用指令缓存ICache和数据缓存DCache否则写入数据可能被缓存延迟刷新导致读取时仍为旧值。标准模板中HAL_FLASH_Unlock()后需立即执行// 禁用缓存F103 无 DCache但 ICache 必须关闭 __HAL_FLASH_INSTRUCTION_CACHE_DISABLE(); __HAL_FLASH_DATA_CACHE_DISABLE(); // 写入 32 位数据 if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, 0x08004000, 0x12345678) ! HAL_OK) { Error_Handler(); } // 重新启用缓存 __HAL_FLASH_INSTRUCTION_CACHE_ENABLE(); __HAL_FLASH_DATA_CACHE_ENABLE();提示HAL_FLASH_Program()的TypeProgram参数必须与目标地址对齐方式匹配。FLASH_TYPEPROGRAM_WORD要求地址 4 字节对齐若写入字节数组需用FLASH_TYPEPROGRAM_BYTE并循环调用。4.3 Flash 操作失败的诊断路径当HAL_FLASH_Program()返回HAL_TIMEOUT时常见原因有Flash 未解锁HAL_FLASH_Unlock()缺失地址超出 Flash 范围F103 最大地址为0x0801FFFF电源电压低于 2.7VHAL_PWREx_GetVoltageRange()返回PWR_VOLTAGE_RANGE_1正在执行其他 Flash 操作如擦除未完成诊断命令// 查询 Flash 状态寄存器 uint32_t status FLASH-SR; if (status FLASH_FLAG_BSY) { // 正忙等待 } else if (status FLASH_FLAG_PGERR) { // 编程错误 } else if (status FLASH_FLAG_WRPRTERR) { // 写保护错误 }5. 多路输入捕获在 HAL 标准模板中配置 TIM2 的 4 通道同步采样STM32F103 的 TIM2 支持最多 4 路输入捕获IC1–IC4常用于电机编码器或超声波测距。标准模板默认只配置 TIM2 的 IC1需扩展至多通道。关键在于共用一个计数器CNT但独立捕获寄存器CCR1–CCR4且必须保证所有通道使用相同预分频器PSC和自动重装载值ARR。5.1 TIM2 输入捕获的引脚映射与复用配置TIM2 的输入捕获通道对应引脚IC1PA0AF1、PA15AF1IC2PA1AF1IC3PA2AF1IC4PA3AF1标准模板中MX_TIM2_Init()仅配置 IC1需扩展为htim2.Instance TIM2; htim2.Init.Prescaler 71; // 72MHz / (711) 1MHz 计数频率 htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0xFFFF; // 自动重装载值 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_IC_Init(htim2) ! HAL_OK) { Error_Handler(); } // 配置 IC1–IC4 sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_BOTHEDGE; sConfigIC.ICSelection TIM_ICSELECTION_DIRECTTI; sConfigIC.ICPrescaler TIM_ICPSC_DIV1; sConfigIC.ICFilter 0; HAL_TIM_IC_ConfigChannel(htim2, sConfigIC, TIM_CHANNEL_1); HAL_TIM_IC_ConfigChannel(htim2, sConfigIC, TIM_CHANNEL_2); HAL_TIM_IC_ConfigChannel(htim2, sConfigIC, TIM_CHANNEL_3); HAL_TIM_IC_ConfigChannel(htim2, sConfigIC, TIM_CHANNEL_4); // 启动所有通道 HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_2); HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_3); HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_4);5.2 中断服务函数的通道判别逻辑TIM2_IRQHandler()中需根据HAL_TIM_GetITSource()判别触发通道void TIM2_IRQHandler(void) { HAL_TIM_IRQHandler(htim2); } void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { uint32_t flag __HAL_TIM_GET_FLAG(htim, __HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_CC1)); if (flag __HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_CC1)) { uint32_t ic1_val HAL_TIM_ReadCapturedValue(htim2, TIM_CHANNEL_1); // 处理 IC1 } // 类似处理 IC2–IC4 } }注意HAL_TIM_IC_CaptureCallback()是通用回调必须通过htim-Instance和__HAL_TIM_GET_FLAG()精确识别通道避免误判。5.3 多通道同步性的硬件保障TIM2 的 4 个捕获通道共享同一计数器因此理论上可实现纳秒级同步。但实际同步性受以下因素影响引脚电气特性差异如 PA0 与 PA3 的走线长度不同输入滤波器ICFilter设置值不同中断响应延迟HAL_TIM_IC_Start_IT()启动后首次捕获可能有 1–2 个计数周期偏差验证方法用示波器同时观测 4 路输入信号和 TIM2 的CNT值确认CCR1–CCR4的差值等于信号边沿时间差。若偏差 100ns需检查 PCB 走线长度匹配及ICFilter是否统一设为 0。本文还有配套的精品资源点击获取