嵌入式基础作业三:STM32F103C8T6 三种方式实现流水灯
课程作业范围:实验1(寄存器方式)、实验2(标准外设库方式及 Keil 逻辑分析仪)、实验3(HAL 库 + 按键外部中断暂停/恢复)。
目标器件:STM32F103C8T6,Cortex-M3,72 MHz 系统时钟,64 KB Flash,20 KB SRAM。
文档用途:本文可直接作为博客正文。图片和视频均按本文第一次出现的顺序编号,文件与本文位于同一目录。
摘要
本次作业围绕 STM32F103C8T6 的 GPIO、RCC、SysTick、EXTI 和 NVIC 展开,分别采用三种抽象层级完成流水灯:第一部分绕过库函数,直接访问外设寄存器;第二部分使用 STM32F10x 标准外设库(SPL)封装 GPIO/RCC 操作,并用 Keil 的 Logic Analyzer 观察软件逻辑波形;第三部分迁移到 STM32F1 HAL 库,用 PA0 按键产生下降沿外部中断控制流水灯运行状态。
实验1建立了“地址—寄存器位—引脚电平”的底层认识;实验2将配置过程封装为库函数,并增加逻辑分析仪变量,便于测量每一路 LED 的亮、灭时间和周期;实验3进一步把中断服务函数、HAL 回调、消抖和主循环状态机组合起来,实现按键暂停、再次按键恢复,并在暂停期间保持当前 LED 状态。
1. 作业要求与资料边界
1.1 附件中真正需要完成的内容
附件作业要求分为三项:
- 实验任务1:用 STM32 最小系统板和若干红/绿/蓝 LED,使用 GPIOA/GPIOB/GPIOC 控制流水灯,间隔约 1 s;要求说明 GPIO 端口寄存器地址和参数,并用 C 语言寄存器方式实现。
- 实验任务2:在实验1基础上改用标准外设库,通过软件循环延时实现流水灯;使用 Keil Logic Analyzer 观察 GPIO 或逻辑变量,分析高低电平和流水灯周期。
- 实验任务3:在前两项基础上改用 HAL 库和按键中断,实现流水灯的暂停、恢复和继续运行。
附件中给出的博客链接、标准外设库下载链接和参考资料属于学习线索,不替代上述三个任务。本文中的“实验现象”分为代码可推出的理论结果、工程已有 Build 结果和需要用户在板上/仿真器上填写的实测结果三类,避免把理论计算误写成测量值。
1.2 本文使用的本地工程
| 实验 | 工程目录 | 主要实现 | 可核对产物 |
|---|---|---|---|
| 实验1 | D:\Keil files\text\1 | 寄存器直写 GPIO + SysTick 轮询延时 | Objects\text1.hex、Build Log |
| 实验2 | D:\Keil files\text\2 | STM32F10x SPL + 软件循环延时 + Logic Analyzer 变量 | Project\Objects\Experiment2.hex、README_实验2.md |
| 实验3 | D:\Keil files\text\3\Experiment3_HAL_EXTI_LED | STM32F1 HAL + EXTI0 + 按键消抖 + 状态机 | MDK-ARM\Experiment3_HAL.uvprojx、Core\Src\main.c |
2. 器件、接线与安全约定
2.1 STM32F103C8T6 基本信息
STM32F103C8T6的命名可以拆解为:STM32 表示 ARM 32 位微控制器,F103 表示增强型 F1 子系列,C 表示 48 引脚封装,8 表示 64 KB Flash,T 表示 LQFP 封装,6 表示工业级温度范围。参考手册第 3 页给出了命名规则;本文截图图2与芯片时钟相关的参考手册页用于说明芯片资料来源。
2.2 三个实验采用的 LED 映射
实验1和实验2来自已有工程,使用的 LED 引脚不同;实验3是本次 HAL 工程的独立接线约定,不能把三个实验的引脚表混接。
| 实验 | LED 顺序 | 引脚 | 有效电平 | 备注 |
|---|---|---|---|---|
| 实验1 | 红、绿、蓝、板载 | PA0、PB0、PC0、PC13 | 外接三路高有效,PC13 低有效 | 直接写 BSRR/BRR |
| 实验2 | 板载、红、绿、蓝 | PC13、PA0、PB0、PC15 | 四路均按低有效逻辑处理 | PC15 与 LSE 晶振复用,启用 LSE 时不要占用 |
| 实验3 | LED1、LED2、LED3 | PA8、PB0、PC13 | PA8/PB0 高有效,PC13 低有效 | PA0 为按键,不再作为 LED |
外接 LED 必须串联限流电阻,建议 220 Ω~1 kΩ。PC13、PC15 的输出驱动能力较弱,不应直接带大电流负载;必要时使用三极管或 MOSFET。按键 PA0 采用内部上拉,按下时将引脚接地,形成下降沿。
2.3 参考手册截图
图1说明:参考手册指出,GPIO 输出可以通过GPIOx_ODR改变,也可以通过置位/复位寄存器GPIOx_BSRR和GPIOx_BRR单独改变某一位,且不需要为一次 APB2 写操作额外禁止中断。实验1使用 BSRR/BRR,实验3由 HAL 的HAL_GPIO_WritePin()封装了相同的输出思想。
图2说明:RCC 将 HSE/HSI 送入 PLL,并通过 AHB、APB1、APB2 分频得到各总线时钟;SysTick 可以使用处理器时钟或分频后的外部时钟。实验1假设SYSCLK_HZ=72 MHz,实验3在 HAL 初始化后把 HCLK 配置为 72 MHz。
图3说明:EXTI 的每条线具有独立触发和屏蔽状态。实验3把 PA0 映射到 EXTI0,配置为下降沿触发,再由EXTI0_IRQHandler()进入 HAL 中断分发函数。
3. 实验1:寄存器方式控制四路流水灯
3.1 实验目的
- 认识 STM32F1 GPIO 和 RCC 的外设基地址及寄存器偏移。
- 直接配置 GPIOx_CRL/CRH 的 MODE、CNF 位,理解推挽输出的底层编码。
- 使用 SysTick 控制与状态寄存器实现毫秒级轮询延时。
- 通过 BSRR/BRR 的写入实现“先全灭、再点亮一路”的单灯流水效果。
3.2 寄存器地址和关键位
| 外设 | 寄存器 | 地址 | 本实验用途 |
|---|---|---|---|
| RCC | RCC_APB2ENR | 0x40021018 | bit2/bit3/bit4 分别打开 GPIOA/GPIOB/GPIOC 时钟 |
| GPIOA | CRL/CRH | 0x40010800/0x40010804 | 配置 PA0 为 2 MHz 推挽输出 |
| GPIOA | BSRR/BRR | 0x40010810/0x40010814 | PA0 置高/置低 |
| GPIOB | CRL/CRH | 0x40010C00/0x40010C04 | 配置 PB0 |
| GPIOB | BSRR/BRR | 0x40010C10/0x40010C14 | PB0 置高/置低 |
| GPIOC | CRL/CRH | 0x40011000/0x40011004 | 配置 PC0、PC13 |
| GPIOC | BSRR/BRR | 0x40011010/0x40011014 | PC0、PC13 置位/复位 |
| Cortex-M3 SysTick | CTRL/LOAD/VAL | 0xE000E010/0xE000E014/0xE000E018 | 启动计数、装载重装值、清零当前值 |
GPIO 配置寄存器每个引脚占 4 bit。低 8 个引脚使用CRL,高 8 个引脚使用CRH;MODE=10b表示 2 MHz 输出,CNF=00b表示通用推挽输出,所以实验1的配置半字节为0x2。例如 PA0 的字段位于GPIOA_CRL[3:0],PC13 的字段位于GPIOC_CRH[(13-8)*4+3 : (13-8)*4]。
3.3 程序设计思路
程序启动后依次完成:
- 向
RCC_APB2ENR的 GPIOA/GPIOB/GPIOC 使能位写 1。 - 清除目标引脚原有 4 bit 配置,再写入
0x2,得到 2 MHz 推挽输出。 - 配置 SysTick:若系统时钟为 72 MHz,则
LOAD=72000000/1000-1=71999;若工程实际仍为复位后的 8 MHz,则应使用 7999,否则 1 s 会严重偏差。 LED_All_Off()先关闭全部 LED;LED_On(n)根据编号点亮一路。- 主循环按照 PA0 → PB0 → PC0 → PC13 顺序,每路保持约 1 s。
3.4 关键代码
#defineRCC_APB2ENR(*(volatileuint32_t*)0x40021018UL)#defineGPIOA_CRL(*(volatileuint32_t*)0x40010800UL)#defineGPIOA_BSRR(*(volatileuint32_t*)0x40010810UL)#defineGPIOA_BRR(*(volatileuint32_t*)0x40010814UL)#defineSYSTICK_CTRL(*(volatileuint32_t*)0xE000E010UL)#defineSYSTICK_LOAD(*(volatileuint32_t*)0xE000E014UL)#defineSYSTICK_VAL(*(volatileuint32_t*)0xE000E018UL)#defineSYSCLK_HZ72000000ULstaticvoidGPIO_Clock_Enable(void){RCC_APB2ENR|=(1UL<<2);/* IOPAEN */RCC_APB2ENR|=(1UL<<3);/* IOPBEN */RCC_APB2ENR|=(1UL<<4);/* IOPCEN */}引脚配置函数按照pin<8选择 CRL,否则选择 CRH:
shift=(pin<8U)?(uint32_t)pin*4UL:(uint32_t)(pin-8U)*4UL;*cr&=~(0xFUL<<shift);*cr|=(0x2UL<<shift);/* CNF=00, MODE=10 */SysTick 轮询延时的核心是清零VAL,再等待CTRL的COUNTFLAG(bit16)置位:
staticvoidDelay_ms(uint32_tms){while(ms-->0UL){SYSTICK_VAL=0UL;while((SYSTICK_CTRL&(1UL<<16))==0UL){}}}3.5 运行结果与工程检查
理论上,四路 LED 每路点亮约 1 s,完整一轮约 4 s。PA0/PB0/PC0 是高有效外接 LED,PC13 板载 LED 是低有效,因此关闭全部 LED 时要分别写低电平和高电平。程序的LED_All_Off()正是按这种有效电平差异实现的。
已有D:\Keil files\text\1\Objects\text1.build_log.htm显示:Program Size: Code=1044 RO-data=252 RW-data=0 ZI-data=1632,并且0 Error(s), 0 Warning(s),同时生成了text1.hex。但是源码注释同时提醒SYSCLK_HZ必须与system_stm32f10x.c的实际时钟一致,下载到不同板卡前应再次确认 HSE/PLL 配置。
4. 实验2:标准外设库 + Keil 逻辑分析仪
4.1 实验目的和接线
实验2把寄存器直写替换为 STM32F10x Standard Peripheral Library(SPL)调用:RCC_APB2PeriphClockCmd()打开时钟,GPIO_Init()配置推挽输出,GPIO_WriteBit()改变 LED 电平。流水顺序为:PC13 板载灯 → PA0 红灯 → PB0 绿灯 → PC15 蓝灯。
工程将四个逻辑变量定义为:
volatileuint8_tLA_PC13;volatileuint8_tLA_PA0;volatileuint8_tLA_PB0;volatileuint8_tLA_PC15;这些变量采用1=对应 LED 点亮,0=对应 LED 熄灭的逻辑状态,不一定等于 GPIO 物理电平。当前工程把四路 LED 都按低有效处理:GPIO 输出低电平时 LED 点亮。若外接 LED 是 GPIO→电阻→LED→GND 的高有效接法,应对调EXT_ACTIVE_LEVEL和EXT_INACTIVE_LEVEL。
4.2 Keil 工程建立过程
以下截图是本目录中保留的完整操作证据,按本文出现顺序编号。截图中的界面文字可能因 Keil 版本不同略有差异,但对应的配置目标相同:建立 STM32F103C8 工程、添加 CMSIS/SPL 文件、配置包含路径、设置仿真器和下载算法。
图4说明:这是实验2最重要的观测截图。波形用于检查LA_PC13、LA_PA0、LA_PB0、LA_PC15是否按 one-hot 顺序变化,以及每一路的亮、灭时间和重复周期。由于截图中游标和缩放比例属于当前一次仿真记录,最终报告中的“实测数值”仍应以用户自己的游标读数填写。
4.3 源码结构
工程目录中的关键文件如下:
text\2 ├─Project │ ├─STM32F103C8_Experiment2.uvprojx │ └─User\main.c └─Libraries\STM32F10x_SPL_Source\Libraries ├─CMSIS\CM3\... └─STM32F10x_StdPeriph_Driver\inc、src主循环先调用SystemInit()和GPIO_Config(),然后反复执行四颗 LED 的 one-hot 序列。切换时采用“全部熄灭 → 20 ms 黑场 → 点亮当前 LED → 延时 1000 ms”的 break-before-make 策略,避免两路 LED 在切换瞬间同时点亮。
staticvoidLED_Write(LED_TypeDef*led,uint8_ton){BitAction level;if(on!=0U)level=led->active_level;elselevel=(led->active_level==Bit_SET)?Bit_RESET:Bit_SET;GPIO_WriteBit(led->port,led->pin,level);*(led->logic_level)=on;}while(1){for(i=0U;i<led_count;++i){LED_AllOff();DelayMs(20U);LED_Write(&leds[i],1U);DelayMs(1000U);LED_AllOff();}}4.4 Logic Analyzer 观察步骤
- 打开
Project\STM32F103C8_Experiment2.uvprojx,确认宏包含USE_STDPERIPH_DRIVER和STM32F10X_MD。 - Build,确认
Project\Objects\Experiment2.hex生成。 - 进入 Debug,选择
Use Simulator,运行程序。 - 打开
View → Analysis Windows → Logic Analyzer。 - 添加
LA_PC13、LA_PA0、LA_PB0、LA_PC15四个变量。 - 用游标测量单路高电平/逻辑点亮时间、单路重复周期以及相邻 LED 之间的时间间隔。
已有 Build Log 显示工程使用 ARMCC V5.06,程序大小为Code=1304、RO-data=268、RW-data=52、ZI-data=1636,并以0 Error(s), 0 Warning(s)生成Experiment2.hex。
4.5 理论波形计算
设软件延时D=1 s,黑场切换间隔G=20 ms,四颗 LED 依次点亮。对某一路 LED 来说:
- 点亮时间:约
1.000 s; - 其余三路点亮时间:约
3.000 s; - 三次切换黑场:约
3×0.020=0.060 s; - 单路重复周期:约
1+3+0.060=4.060 s; - 逻辑点亮占空比:约
1/4.060≈24.6%。
实际DelayMs()是软件空循环,编译器版本、优化等级、仿真器速度和系统时钟都会改变 D。因此应把D、3D+3G和周期从 Logic Analyzer 中实测后写入实验记录。图4可以作为波形证据,表格可按下列格式补充:
| 逻辑变量 | 理论亮/灭 | 理论周期 | 实测亮/灭/周期 |
|---|---|---|---|
LA_PC13 | 约 1 s / 约 3.06 s | 约 4.06 s | 用户游标读数:________ |
LA_PA0 | 约 1 s / 约 3.06 s | 约 4.06 s | 用户游标读数:________ |
LA_PB0 | 约 1 s / 约 3.06 s | 约 4.06 s | 用户游标读数:________ |
LA_PC15 | 约 1 s / 约 3.06 s | 约 4.06 s | 用户游标读数:________ |
5. 实验3:HAL 库 + 按键外部中断暂停/恢复
5.1 实验目标
实验3要求把流水灯的控制方式改为 HAL 库,并通过按键中断控制运行状态:按键第一次按下后暂停,当前 LED 保持;再次按下后恢复,从当前序号继续。工程位于D:\Keil files\text\3\Experiment3_HAL_EXTI_LED。
5.2 引脚和 HAL 参数
| 信号 | 引脚 | HAL 模式 | 有效电平/动作 |
|---|---|---|---|
| LED1 | PA8 | GPIO_MODE_OUTPUT_PP、低速 | 高电平点亮 |
| LED2 | PB0 | GPIO_MODE_OUTPUT_PP、低速 | 高电平点亮 |
| LED3 | PC13 | GPIO_MODE_OUTPUT_PP、低速 | 低电平点亮,板载灯 |
| KEY1 | PA0 | GPIO_MODE_IT_FALLING+GPIO_PULLUP | 按下接地,产生下降沿 |
| EXTI | EXTI0 | NVIC 优先级 2/0 | EXTI0_IRQn |
PA0 与实验1的 LED1 不同:在实验3中 PA0 专门用作按键,LED1 改为 PA8。若实际开发板使用其他按键或 LED 引脚,应同步修改Core\Inc\main.h的宏、gpio.c的初始化和中断入口。
5.3 初始化流程
HAL_Init();SystemClock_Config();MX_GPIO_Init();Led_AllOff();Led_Show(g_led_index);last_step_tick=HAL_GetTick();SystemClock_Config()打开 8 MHz HSE,PLL×9 得到 72 MHz;AHB 不分频,APB1 为 HCLK/2,APB2 为 HCLK。MX_GPIO_Init()打开 GPIOA/B/C 和 AFIO 时钟,将三路 LED 配成推挽输出,再将 PA0 配成下降沿 EXTI 输入,最后调用HAL_NVIC_SetPriority()和HAL_NVIC_EnableIRQ()。
5.4 中断、消抖和状态机
中断入口保持短小:
voidEXTI0_IRQHandler(void){HAL_GPIO_EXTI_IRQHandler(KEY1_Pin);}voidHAL_GPIO_EXTI_Callback(uint16_tGPIO_Pin){if(GPIO_Pin==KEY1_Pin)g_key_event=1U;}回调只记录事件,不在中断中HAL_Delay()。主循环检测g_key_event后,等待 50 ms 消抖,并再次读取 PA0:确认仍为低电平才翻转g_run。暂停和恢复的核心代码为:
if((HAL_GetTick()-last_key_tick)>=KEY_DEBOUNCE_MS&&HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin)==GPIO_PIN_RESET){g_run=(uint8_t)!g_run;last_key_tick=HAL_GetTick();last_step_tick=last_key_tick;}if(g_run!=0U&&(HAL_GetTick()-last_step_tick)>=LED_INTERVAL_MS){last_step_tick=HAL_GetTick();g_led_index=(uint8_t)((g_led_index+1U)%LED_COUNT);Led_Show(g_led_index);}这种写法有三个特点:
- 暂停保持状态:暂停时不调用
Led_AllOff(),所以当前 LED 的 GPIO 电平保持不变。 - 恢复从当前位置继续:
g_led_index不清零,再次运行时继续加一。 - 消抖不阻塞 ISR:中断只置位标志,消抖放在主循环中完成,避免按键抖动导致多次翻转。
5.5 HAL 工程代码窗口与运行视频
图22说明:代码窗口用于核对HAL_Init()、HAL_GetTick()、HAL_GPIO_EXTI_Callback()和g_run状态切换。工程中的Core\Src\gpio.c负责硬件初始化,Core\Src\stm32f1xx_it.c负责EXTI0_IRQHandler()和SysTick_Handler()。
下面的视频文件是本次素材目录中的流水灯运行记录。Markdown 博客可直接播放;PDF 不能可靠内嵌本地 MP4,因此 PDF 中保留视频文件名和相对链接,提交时应将23.mp4与文档一起上传。
视频23:HAL + 按键中断流水灯运行记录(请使用支持 HTML5 video 的博客编辑器预览)。
视频23:HAL + 按键中断流水灯运行记录
5.6 验收现象
- 上电后 LED1、LED2、LED3 依次点亮,默认每 1 s 移动一次。
- 按下 KEY1,当前点亮的 LED 保持,流水灯暂停。
- 再次按下 KEY1,流水灯从原位置继续,不跳回 LED1。
- 若出现一次按键跳过两个状态,优先检查按键是否悬空、是否启用上拉、消抖时间是否足够。
- 若 PC13 亮灭相反,检查板载 LED 的低有效特性,不要简单把所有 LED 的有效电平统一处理。
6. 三种实现方式对比
| 对比项 | 实验1:寄存器 | 实验2:SPL | 实验3:HAL + EXTI |
|---|---|---|---|
| GPIO 配置 | 手动地址和位操作 | GPIO_Init() | HAL_GPIO_Init() |
| 时钟配置 | 依赖 CMSIS/system 文件,应用层直接开 APB2 位 | RCC_APB2PeriphClockCmd() | __HAL_RCC_GPIOx_CLK_ENABLE() |
| 延时 | SysTick COUNTFLAG 轮询 | 软件循环空转 | HAL_GetTick()非阻塞计时 |
| 按键 | 无 | 无 | PA0 下降沿 EXTI0 |
| 可维护性 | 最接近硬件,易出错 | 代码量适中 | API 统一,工程可扩展性好 |
| 时序测量 | 需要示波器/仿真底层寄存器 | Logic Analyzer 观察LA_* | 可继续用 tick 或增加逻辑变量 |
| 主要风险 | 时钟常量和寄存器地址写错 | 延时空循环随优化变化 | EXTI 映射、消抖和有效电平配置错误 |
从实验1到实验3,代码抽象层逐步提高,但硬件规律没有改变:打开 GPIO 时钟、配置端口模式、输出正确的有效电平、用可靠的时间基准切换状态。理解第一种方式后,再使用 SPL/HAL 才不会把库函数当成“黑盒”。
7. 常见问题与排查
7.1 LED 完全不亮
- 检查 LED 是否串联电阻、是否接反;
- 检查目标端口时钟是否打开;
- 检查板载 PC13 是否低有效;
- 检查 PA0/PB0/PC0/PC15 是否被其他外设复用;
- 检查下载器的 SWDIO/SWCLK 是否误被重映射关闭。
7. LED 顺序正确但速度不对
- 实验1检查
SYSCLK_HZ与SystemCoreClock是否一致; - 实验2调整
DelayMs()的空循环常数,并记录优化等级; - 实验3优先检查 HSE 晶振是否存在,确认 PLL×9 后为 72 MHz;
- 逻辑分析仪显示的是软件变量时,应说明它代表逻辑状态,不一定是物理引脚电压。
7.3 按键一次按下触发多次
- PA0 必须设置
GPIO_PULLUP,按键另一端接 GND; - EXTI 选择下降沿,不能在按下和释放两个边沿同时触发;
- ISR 中不要使用长延时;
- 主循环采用至少 20~50 ms 消抖,并确认按键仍为低电平后再翻转状态。
7.4 PC15 无法作为实验2 LED
PC15 与 LSE 低速外部晶振功能复用。如果工程启用了 LSE/RTC,PC15 不能同时可靠地当普通 LED GPIO;应关闭 LSE、换用其他空闲引脚,或用外部测试点替代。
8. 结论
实验1验证了寄存器地址和位域配置可以直接驱动 GPIO;实验2验证了 SPL 可以降低工程配置难度,并通过 Logic Analyzer 观察到流水逻辑;实验3在此基础上引入 HAL 和 EXTI,把按键事件与流水灯状态机解耦,实现了暂停、恢复、保持当前位置和软件消抖。
本次实现最需要注意的不是 LED 亮灭语句本身,而是三个一致性问题:系统时钟必须与延时计算一致、LED 有效电平必须与接线一致、逻辑分析变量必须与物理引脚含义区分。这些问题也是从寄存器级代码迁移到库函数和 HAL 工程时最容易出现的错误来源。
9. 提交文件与素材清单
9.1 文档
嵌入式基础作业三_三实验综合报告.md:本文 Markdown 博客稿。嵌入式基础作业三_三实验综合报告.pdf:由本文生成的 PDF 版本。
9.2 图片和视频
1.png~3.png:STM32 参考手册 GPIO、RCC/SysTick、EXTI 截图;4.png~22.png:原作业目录中的 19 张实验截图,按本文出现顺序编号;23.mp4:流水灯运行视频,Markdown 中用<video>和普通链接各保留一次;原始素材备份:保留原始中文文件名,便于追溯数字编号来源。
9.3 参考资料
D:\cxdownload\STM32中文参考手册_V10.pdf.pdf,RM0008 中文参考手册第 10 版译文;D:\Keil files\text\1中的寄存器流水灯工程及 Build Log;D:\Keil files\text\2中的 STM32F10x SPL 工程、Logic Analyzer 说明和 Build Log;D:\Keil files\text\3\Experiment3_HAL_EXTI_LED中的 HAL + EXTI 工程。