ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F103 入门实战:流水灯、蜂鸣器与传感器代码的结构化理解

STM32F103 入门实战:流水灯、蜂鸣器与传感器代码的结构化理解 TL;DR太长不看版本文面向刚接触 STM32F103 的开发者用流水灯、蜂鸣器和传感器三个经典实验帮你建立一套可复用的嵌入式代码理解框架。核心观点是所有外设初始化都遵循时钟 → 模式 → 初始状态的统一模板流水灯和蜂鸣器属于输出型外设代码骨架同构写引脚 延时/频率控制传感器属于输入型外设多出触发 → 读取 → 解析 → 输出的链路。读完你就能快速读懂任何 STM32 外设的初始化与主循环代码为后续学习串口、定时器中断和 FreeRTOS 打好基础。1. 引言对于刚接触 STM32F103 的开发者来说流水灯、蜂鸣器和传感器是三个最经典的入门实验。它们看似简单却几乎覆盖了嵌入式开发的核心要素GPIO 输出控制、定时器延时、外部中断、ADC 采集以及传感器数据的读取与处理。本文不打算逐行贴出完整工程那会淹没重点而是从代码构成与基本结构的角度帮你建立一套可复用的理解框架。读完你会明白为什么流水灯和蜂鸣器的代码骨架几乎一样传感器代码又为什么多出初始化—读取—转换—输出这几步理解了这些后续学习串口、定时器中断、FreeRTOS 都会顺畅很多。2. 开发环境与工程结构在分析代码之前先明确我们面对的是怎样的工程。STM32F103 的开发通常有两种主流方式标准外设库StdPeriph_Lib经典、资料多适合学习寄存器与库函数的对应关系。HAL 库STM32CubeMX 生成图形化配置代码可读性好适合快速上手和工程化开发。无论用哪种库一个完整的工程都包含以下固定部分Project/ ├── Core/ │ ├── Inc/ # 头文件main.h、gpio.h、tim.h 等 │ └── Src/ # 源文件main.c、gpio.c、tim.c 等 ├── Drivers/ │ ├── CMSIS/ # 内核相关定义 │ └── STM32F1xx_HAL_Driver/ # HAL 库源码 └── main.c # 主函数入口理解这个结构很重要外设的初始化和使用是分离的。初始化代码通常由 CubeMX 生成在gpio.c、tim.c等文件中而业务逻辑写在main.c的while(1)循环里。这也是初学者最容易困惑的地方——明明在main.c里没看到引脚配置为什么灯能亮因为配置在MX_GPIO_Init()里被调用了。3. 流水灯代码的结构化理解3.1 初始化阶段流水灯的本质是多个 GPIO 引脚按顺序输出高低电平。初始化代码的核心是MX_GPIO_Init()它做的事情可以归纳为三步voidMX_GPIO_Init(void){GPIO_InitTypeDef GPIO_InitStruct{0};// 1. 使能 GPIO 端口时钟__HAL_RCC_GPIOC_CLK_ENABLE();// 2. 配置引脚模式推挽输出、无上下拉、低速GPIO_InitStruct.PinLED1_Pin|LED2_Pin|LED3_Pin|LED4_Pin;GPIO_InitStruct.ModeGPIO_MODE_OUTPUT_PP;GPIO_InitStruct.PullGPIO_NOPULL;GPIO_InitStruct.SpeedGPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOC,GPIO_InitStruct);// 3. 初始状态全部熄灭高电平熄灭取决于硬件接法HAL_GPIO_WritePin(GPIOC,LED1_Pin|LED2_Pin|LED3_Pin|LED4_Pin,GPIO_PIN_SET);}这段代码的结构可以抽象为**“时钟 → 模式 → 初始状态”**三步曲。几乎所有 GPIO 外设按键、蜂鸣器、传感器使能脚的初始化都是这个套路。3.2 主循环中的流水逻辑流水效果的核心在while(1)循环里常见写法有两种写法一逐个点亮最直观while(1){HAL_GPIO_WritePin(GPIOC,LED1_Pin,GPIO_PIN_RESET);// 亮HAL_Delay(200);HAL_GPIO_WritePin(GPIOC,LED1_Pin,GPIO_PIN_SET);// 灭HAL_GPIO_WritePin(GPIOC,LED2_Pin,GPIO_PIN_RESET);HAL_Delay(200);// ... 依次类推}写法二移位法更优雅适合多灯uint16_tled_mask0x0001;// 从第一盏灯开始while(1){HAL_GPIO_WritePin(GPIOC,LED_ALL_Pin,GPIO_PIN_SET);// 先全灭HAL_GPIO_WritePin(GPIOC,led_mask,GPIO_PIN_RESET);// 点亮当前位HAL_Delay(200);led_mask1;// 左移一位if(led_mask0x0008)led_mask0x0001;// 循环回第一盏}理解要点流水灯代码的骨架是写引脚 → 延时 → 换下一个。这里的HAL_Delay(200)是阻塞式延时CPU 在延时期间空转。这是入门阶段可以接受的但当你需要同时处理多个任务比如流水灯的同时还要响应按键时就要改用定时器中断或非阻塞延时了——这是后话。写法三定时器中断驱动非阻塞进阶推荐当需要同时处理多个任务时HAL_Delay()的阻塞特性就成了瓶颈。改用定时器中断后CPU 在等待期间可以继续执行其他逻辑如响应按键、刷新 OLED流水灯的节奏完全由硬件定时器驱动。// 1. 定时器初始化回调CubeMX 生成在 tim.c 中voidMX_TIM2_Init(void){TIM_ClockConfigTypeDef sClockSourceConfig{0};TIM_MasterConfigTypeDef sMasterConfig{0};htim2.InstanceTIM2;htim2.Init.Prescaler7199;// 72MHz / 7200 10kHzhtim2.Init.CounterModeTIM_COUNTERMODE_UP;htim2.Init.Period1999;// 10kHz / 2000 5Hz即 200ms 中断一次htim2.Init.ClockDivisionTIM_CLOCKDIVISION_DIV1;HAL_TIM_Base_Init(htim2);}// 2. 启动定时器并开启中断在 main() 中调用HAL_TIM_Base_Start_IT(htim2);// 3. 中断回调函数HAL 库弱函数在 main.c 中重写voidHAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef*htim){staticuint16_tled_mask0x0001;if(htim-InstanceTIM2){HAL_GPIO_WritePin(GPIOC,LED_ALL_Pin,GPIO_PIN_SET);// 先全灭HAL_GPIO_WritePin(GPIOC,led_mask,GPIO_PIN_RESET);// 点亮当前位led_mask1;// 左移一位if(led_mask0x0008)led_mask0x0001;// 循环回第一盏}}// 4. 主循环不再需要任何延时可以处理其他任务while(1){// 响应按键、刷新 OLED、读取传感器……CPU 完全空闲}对比小结对比维度HAL_Delay()阻塞式定时器中断驱动CPU 利用率延时期间空转浪费等待期间可执行其他任务多任务支持差难以同时响应按键好主循环可自由处理其他逻辑代码复杂度简单易上手稍复杂需理解中断与回调时序精度依赖 SysTick受中断影响由硬件定时器保证更精确适用场景入门学习、单任务多任务、实时性要求高的工程下面用一张 Mermaid 时序图直观对比两种方案下 CPU 的占用情况。横轴为时间纵轴标注「主循环任务」与「定时器中断」两条泳道色块表示 CPU 正在执行的内容定时器中断主循环任务定时器中断主循环任务HAL_Delay() 阻塞式定时器中断驱动非阻塞点亮 LED1写引脚1HAL_Delay(200) 空转等待2点亮 LED2写引脚3HAL_Delay(200) 空转等待4点亮 LED3写引脚5HAL_Delay(200) 空转等待6点亮 LED1写引脚7执行其他任务按键/OLED/传感器8200ms 到触发中断9点亮 LED2写引脚10执行其他任务按键/OLED/传感器11200ms 到触发中断12点亮 LED3写引脚13执行其他任务按键/OLED/传感器14读图要点上半部分灰色区域中主循环在HAL_Delay(200)期间完全空转CPU 被白白占用下半部分绿色区域中主循环只在写引脚瞬间占用 CPU其余时间片都可用于响应按键、刷新 OLED 等业务流水节奏完全由定时器中断驱动。理解要点定时器中断方案的本质是把延时等待从主循环中剥离出来交给硬件定时器去计时到点后通过中断回调自动切换下一盏灯。主循环因此被解放出来可以专心处理其他业务。这也是从会抄代码走向会写代码的关键一步——理解了中断驱动模型后续学习 FreeRTOS 的任务调度会轻松很多。4. 蜂鸣器代码的结构化理解蜂鸣器与流水灯在代码结构上几乎完全一致区别只在于蜂鸣器通常只有一个 GPIO 引脚有源蜂鸣器自带振荡源只需给高低电平即可发声无源蜂鸣器需要PWM 方波驱动才能发出不同频率的声音。4.1 有源蜂鸣器与流水灯同构// 初始化与 LED 完全相同的三步曲// 主循环按节奏翻转电平即可产生滴滴声while(1){HAL_GPIO_WritePin(BUZZER_Pin,GPIO_PIN_RESET);// 响HAL_Delay(100);HAL_GPIO_WritePin(BUZZER_Pin,GPIO_PIN_SET);// 停HAL_Delay(100);}4.2 无源蜂鸣器引入 PWM无源蜂鸣器需要定时器产生 PWM 信号。初始化代码多了一个MX_TIM3_Init()结构变为// 1. GPIO 初始化同上// 2. 定时器初始化设置预分频和自动重载值决定 PWM 频率// 3. 配置 PWM 通道并启动HAL_TIM_PWM_Start(htim3,TIM_CHANNEL_1);// 主循环通过修改比较寄存器改变占空比从而控制音量__HAL_TIM_SET_COMPARE(htim3,TIM_CHANNEL_1,500);理解要点蜂鸣器代码的骨架是GPIO 初始化 定时器初始化→ 输出控制 → 延时/频率控制。它与流水灯共享同一套 GPIO 初始化模板多出来的定时器部分本质上是把延时控制升级为频率控制。5. 传感器代码的结构化理解传感器以常见的 DHT11 温湿度传感器为例的代码结构与前面两个有本质区别。它不再是简单的输出控制而是**“输入读取 → 数据解析 → 结果输出”**的完整链路。5.1 传感器代码的通用四段式结构// 第一步初始化GPIO 配置为输出模式用于发送起始信号voidDHT11_Init(void){GPIO_InitTypeDef GPIO_InitStruct{0};// 1. 使能 GPIO 端口时钟__HAL_RCC_GPIOB_CLK_ENABLE();// 2. 配置引脚为推挽输出模式初始用于发送起始信号GPIO_InitStruct.PinDHT11_Pin;GPIO_InitStruct.ModeGPIO_MODE_OUTPUT_PP;GPIO_InitStruct.PullGPIO_NOPULL;GPIO_InitStruct.SpeedGPIO_SPEED_FREQ_HIGH;// 高速保证时序边沿陡峭HAL_GPIO_Init(DHT11_Port,GPIO_InitStruct);// 3. 初始状态拉高保持总线空闲HAL_GPIO_WritePin(DHT11_Port,DHT11_Pin,GPIO_PIN_SET);}// 第二步发送起始信号并切换为输入模式voidDHT11_Start(void){// 1. 主机拉低总线至少 18ms作为起始信号HAL_GPIO_WritePin(DHT11_Port,DHT11_Pin,GPIO_PIN_RESET);HAL_Delay(20);// 拉低 20ms要求 ≥18ms// 2. 拉高总线释放总线控制权HAL_GPIO_WritePin(DHT11_Port,DHT11_Pin,GPIO_PIN_SET);delay_us(30);// 拉高 30us要求 20~40us// 3. 切换为输入模式等待 DHT11 响应GPIO_InitTypeDef GPIO_InitStruct{0};GPIO_InitStruct.PinDHT11_Pin;GPIO_InitStruct.ModeGPIO_MODE_INPUT;// 输入模式读取传感器数据GPIO_InitStruct.PullGPIO_PULLUP;// 上拉保证空闲时为高电平HAL_GPIO_Init(DHT11_Port,GPIO_InitStruct);}// 第三步读取数据逐位读取 40 位数据uint8_tDHT11_ReadByte(void){uint8_tdata0;for(inti0;i8;i){// 等待低电平结束每位数据以 50us 低电平开始while(HAL_GPIO_ReadPin(DHT11_Port,DHT11_Pin)GPIO_PIN_RESET);// 高电平持续时间决定 0 或 1// - 26~28us 表示逻辑 0// - 70us 表示逻辑 1delay_us(40);// 采样点高电平开始后 40usif(HAL_GPIO_ReadPin(DHT11_Port,DHT11_Pin)GPIO_PIN_SET){data|(0x80i);// 高电平仍为高 → 逻辑 1}// 等待该位高电平结束进入下一位while(HAL_GPIO_ReadPin(DHT11_Port,DHT11_Pin)GPIO_PIN_SET);}returndata;}// 第四步数据校验与结果输出voidDHT11_ReadData(uint8_t*humidity,uint8_t*temperature){uint8_tdata[5]{0};// 40 位数据湿度整/小、温度整/小、校验和// 1. 等待 DHT11 响应信号低电平 80us 高电平 80uswhile(HAL_GPIO_ReadPin(DHT11_Port,DHT11_Pin)GPIO_PIN_SET);// 等待拉低while(HAL_GPIO_ReadPin(DHT11_Port,DHT11_Pin)GPIO_PIN_RESET);// 等待拉高while(HAL_GPIO_ReadPin(DHT11_Port,DHT11_Pin)GPIO_PIN_SET);// 等待数据开始// 2. 连续读取 5 个字节40 位for(inti0;i5;i){data[i]DHT11_ReadByte();}// 3. 校验和验证前 4 字节之和的低 8 位应等于第 5 字节if((uint8_t)(data[0]data[1]data[2]data[3])data[4]){*humiditydata[0];// 湿度整数部分*temperaturedata[2];// 温度整数部分}else{// 校验失败返回无效值由调用方决定是否重试*humidity0xFF;*temperature0xFF;}}// 微秒级延时基于 DWT 或定时器HAL_Delay 最小精度为 1ms无法满足 DHT11 时序voiddelay_us(uint32_tus){// 以 DWT 为例使能 DWT 周期计数器实现精确微秒延时CoreDebug-DEMCR|CoreDebug_DEMCR_TRCENA_Msk;DWT-CYCCNT0;DWT-CTRL|DWT_CTRL_CYCCNTENA_Msk;uint32_tstartDWT-CYCCNT;uint32_tticksus*(SystemCoreClock/1000000);// 72MHz → 每微秒 72 个时钟周期while((DWT-CYCCNT-start)ticks);}5.2 主循环中的调用方式while(1){DHT11_Start();DHT11_ReadData(humidity,temperature);// 将结果通过串口打印或显示在 OLED 上HAL_Delay(1000);// 传感器采样间隔通常需要 1s 以上}理解要点传感器代码的骨架是初始化 → 触发 → 读取 → 解析 → 输出。与流水灯/蜂鸣器最大的不同在于数据流向相反从写引脚变为读引脚引入了时序协议需要精确测量高低电平的持续时间来解析数据引入了校验机制防止读取错误引入了采样间隔传感器本身有响应时间限制。6. 三类代码的统一抽象把三个实验放在一起对比你会发现它们其实共享同一个底层骨架┌─────────────────────────────────────────────────┐ │ STM32F103 外设代码通用骨架 │ ├─────────────────────────────────────────────────┤ │ 1. 时钟使能RCC │ │ 2. GPIO 初始化模式/速度/上下拉 │ │ 3. 可选定时器/ADC/中断等外设初始化 │ │ 4. 主循环 │ │ ├─ 流水灯写引脚 → 延时 → 换下一个 │ │ ├─ 蜂鸣器写引脚/PWM → 频率控制 │ │ └─ 传感器触发 → 读取 → 解析 → 输出 │ └─────────────────────────────────────────────────┘用 Mermaid 表示三类实验的代码结构对比STM32F103 代码结构时钟与 GPIO 初始化流水灯写引脚 延时蜂鸣器写引脚 / PWM 频率传感器触发 读取 解析while(1) 循环核心结论流水灯和蜂鸣器是输出型外设代码结构简单、同构传感器是输入型外设多出读取与解析环节。但它们的初始化部分完全一致都遵循时钟 → 模式 → 初始状态的模板。掌握了这个模板你就能快速读懂任何 STM32 外设的初始化代码。7. 常见问题与调试建议7.1 灯不亮 / 蜂鸣器不响检查 GPIO 初始化是否被调用MX_GPIO_Init()是否在main()中执行检查引脚号是否与硬件接线一致检查电平逻辑有些板子是低电平点亮有些是高电平点亮。7.2 传感器读数异常确认采样间隔大于 1 秒检查起始信号时序是否准确延时是否被优化掉用示波器或逻辑分析仪观察数据线波形这是最有效的排查手段。7.3 延时不准HAL_Delay()依赖 SysTick 中断确认中断未被关闭需要更精确的延时或并行任务时改用定时器。8. 总结通过流水灯、蜂鸣器和传感器三个实验我们实际上掌握了 STM32F103 开发的最小知识闭环GPIO 初始化模板时钟 → 模式 → 初始状态适用于所有外设输出型外设流水灯、蜂鸣器写引脚 延时/频率控制输入型外设传感器触发 → 读取 → 解析 → 输出多出时序协议与校验环节。这三个实验的价值不在于让灯亮起来本身而在于让你建立起初始化与业务分离、外设分输入输出、主循环驱动一切的嵌入式思维。带着这个框架去读任何 STM32 代码你都会觉得清晰很多。下一步建议尝试把流水灯改为定时器中断驱动非阻塞再把传感器数据通过串口打印出来——这两个练习能帮你从会抄代码进阶到会写代码。
返回列表