实验一 寄存器方式实现四路流水灯
实验名称
STM32F103C8T6寄存器编程实现四路LED流水灯
一、实验目的
1. 熟悉STM32F103最小系统板硬件,理解GPIO端口功能与APB2外设时钟作用。
2. 掌握直接操作寄存器配置GPIO,理解CRL、CRH、BSRR寄存器功能,学会位操作。
3. 使用PA0、PB0、PB1外接LED,结合板载PC13 LED,实现四路流水灯,间隔约1s轮流点亮。
4. 掌握Keil‑MDK工程创建、编译、ST‑Link下载调试流程。
5. 理解 volatile 关键字作用;练习将工程上传Gitee,撰写实验博客。
二、实验器材
1. STM32F103C8T6最小系统板
2. ST‑Link仿真下载器
3. 面包板,红、绿、蓝LED各1只,220Ω限流电阻,杜邦线若干
4. 笔记本电脑,Keil uVision5
三、实验原理
GPIO外设使用前必须开启对应总线时钟,GPIOA、GPIOB、GPIOC挂载在APB2总线,由 RCC->APB2ENR 控制时钟。
- GPIOx‑>CRL :管理引脚0‑7,配置模式与输出速度,PA0、PB0、PB1由CRL控制。
- GPIOx‑>CRH :管理引脚8‑15,PC13使用CRH寄存器。
- BSRR 置位复位寄存器:低16位写1引脚输出高电平;高16位写1引脚输出低电平,无读改写风险。
硬件电路:
外接LED:PA0、PB0、PB1串联220Ω电阻接LED阳极,阴极接GND,高电平点亮。
板载PC13 LED:硬件反向驱动,低电平点亮,高电平熄灭。
延时采用双层for空循环软件延时,会受编译器优化影响,延时并不严格准确。
表格
寄存器 基地址 偏移地址
RCC 0x40021000 0x18(APB2ENR)
GPIOA 0x40010800 0x00(CRL)、0x10(BSRR)
GPIOB 0x40010C00 0x00(CRL)、0x10(BSRR)
GPIOC 0x40011000 0x04(CRH)、0x10(BSRR)
四、程序设计思路
1. 使用寄存器指针访问硬件寄存器, volatile 防止编译器优化读写。
2. 开启GPIOA、GPIOB、GPIOC的APB2外设时钟。
3. CRL、CRH对应位先清零再赋值,配置PA0、PB0、PB1、PC13为50MHz推挽输出。
4. 初始化所有LED为熄灭状态。
5. 编写软件延时函数。
6. while死循环,使用BSRR寄存器依次点亮单路LED,其余熄灭,延时后切换,流水顺序PA0→PB0→PB1→PC13循环。
五、实验代码
while(1)
#include "stm32f10x.h"
//软件粗略延时,volatile防止编译器优化
void Soft_Delay(uint32_t t)
{
volatile uint32_t i,j;
for(i = 0; i < t; i++)
{
for(j = 0; j < 7200; j++);
}
}
int main(void)
{
//开启GPIOA GPIOB GPIOC时钟
RCC->APB2ENR |= (1U << 2) | (1U << 3) | (1U << 4);
//PA0 50MHz推挽输出
GPIOA->CRL &= ~(0x0FU << 0);
GPIOA->CRL |= (0x03U << 0);
//PB0 50MHz推挽输出
GPIOB->CRL &= ~(0x0FU << 0);
GPIOB->CRL |= (0x03U << 0);
//PB1 50MHz推挽输出
GPIOB->CRL &= ~(0x0FU << 4);
GPIOB->CRL |= (0x03U << 4);
//PC13,CRH寄存器,50MHz推挽输出
GPIOC->CRH &= ~(0x0FU << 20);
GPIOC->CRH |= (0x03U << 20);
//全部LED初始熄灭
GPIOA->BSRR = (1U << 0) << 16;
GPIOB->BSRR = ((1U << 0)|(1U << 1)) << 16;
GPIOC->BSRR = (1U << 13);
while(1)
{
//点亮PA0
GPIOA->BSRR = 1U << 0;
GPIOB->BSRR = ((1U<<0)|(1U<<1)) <<16;
GPIOC->BSRR = 1U <<13;
Soft_Delay(1000);
//点亮PB0
GPIOA->BSRR = (1U<<0)<<16;
GPIOB->BSRR = 1U << 0;
GPIOB->BSRR = (1U<<1)<<16;
GPIOC->BSRR = 1U <<13;
Soft_Delay(1000);
//点亮PB1
GPIOA->BSRR = (1U<<0)<<16;
GPIOB->BSRR = (1U<<0)<<16;
GPIOB->BSRR = 1U << 1;
GPIOC->BSRR = 1U <<13;
Soft_Delay(1000);
//点亮PC13板载LED
GPIOA->BSRR = (1U<<0)<<16;
GPIOB->BSRR = ((1U<<0)|(1U<<1)) <<16;
GPIOC->BSRR = (1U << 13) <<16;
Soft_Delay(1000);
}
}
六、实验步骤
1. 硬件接线:PA0、PB0、PB1串联220Ω电阻接LED,LED阴极接GND;ST‑Link连接SWDIO、SWCLK、GND、3.3V。
2. Keil新建工程,选择芯片STM32F103C8,添加启动文件,新建main.c粘贴代码。
3. 调试器选择ST‑Link,设置下载后自动复位运行。
4. 编译工程,无报错后下载程序。
5. 上电观察LED流水现象,截图Keil界面、硬件实物。
6. 将工程Git上传Gitee仓库。
七、实验现象
上电后LED按照PA0→PB0→PB1→PC13顺序依次点亮,同一时刻只亮一盏灯,每盏灯维持约1s循环流水。软件延时存在误差,间隔不是严格1秒。
八、遇到的问题与解决
1. 没有加 volatile 修饰延时变量,编译器直接优化掉延时,灯切换飞快。添加volatile后延时正常。
2. 一开始用ODR寄存器,出现引脚互相干扰,改用BSRR置位复位寄存器解决。
3. PC13错误操作CRL寄存器,板载LED无反应,修改操作CRH寄存器后正常。
4. ST‑Link下载失败,是SWCLK杜邦线接触不良,重新插紧即可下载。
九、实验总结
寄存器编程直接操作硬件地址,可以深入理解GPIO底层工作原理,但是要记住寄存器地址与每一位功能,写代码比较繁琐。
必须开启外设时钟GPIO才可以工作;PC13属于CRH寄存器;BSRR比ODR更加安全。软件延时只能粗略等待,不能用于高精度时序。
实验二 标准外设库实现四路流水灯
实验名称
STM32F103标准外设库流水灯实验
一、实验目的
1. 掌握Keil中搭建标准外设库工程,添加库文件,配置头文件路径与编译宏。
2. 学会使用标准库RCC、GPIO函数,理解库对寄存器的封装。
3. 实现PA0、PB0、PB1、PC13四路流水灯,间隔约1s轮流点亮。
4. 使用Keil逻辑分析仪仿真,观察引脚波形,分析软件延时误差。
5. 工程上传Gitee,记录实验过程。
二、实验器材
1. STM32F103C8T6最小系统板
2. ST‑Link仿真下载器
3. 面包板,LED×3,220Ω电阻,杜邦线
4. 电脑,Keil uVision5,STM32F10x标准外设库
三、实验原理
标准外设库将底层寄存器操作封装成C函数,不用手动操作寄存器地址。
- RCC_APB2PeriphClockCmd() :使能GPIO端口时钟。
- GPIO_InitTypeDef 结构体:配置引脚模式、输出速度。
- GPIO_SetBits() 引脚输出高电平; GPIO_ResetBits() 输出低电平。
硬件接线和实验一完全一致:外接LED高电平点亮,PC13板载LED低电平点亮。延时依旧使用软件双层循环,定时精度有限。Keil逻辑分析仪可以仿真观察引脚电平变化,测量实际延时。
四、程序设计思路
1. 工程导入标准库 stm32f10x_rcc.c 、 stm32f10x_gpio.c ,配置编译宏 USE_STDPERIPH_DRIVER 。
2. 编写GPIO初始化函数,开启GPIOA/GPIOB/GPIOC时钟,配置引脚为50MHz推挽输出。
3. 编写软件延时函数。
4. main死循环,依次点亮PA0→PB0→PB1→PC13,其余熄灭,延时循环。
五、实验代码
#include "stm32f10x.h"
void Delay_ms(uint32_t ms)
{
uint32_t i,j;
for(i = 0; i < ms; i++)
for(j = 0; j < 7200; j++);
}
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB,ENABLE);
//PA0??
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
//PB0 PB1??
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1;
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_SetBits(GPIOA,GPIO_Pin_0);
GPIO_SetBits(GPIOB,GPIO_Pin_0|GPIO_Pin_1);
}
六、实验步骤
1. 硬件保持实验一接线不变,ST‑Link连接开发板。
2. 新建Keil工程,复制标准库inc、src文件夹到工程目录。
3. 新建工程分组User、StdPeriph_Driver、Startup;添加main.c、rcc.c、gpio.c、启动文件。
4. 魔术棒配置头文件路径,添加宏定义 USE_STDPERIPH_DRIVER,STM32F10X_MD 。
5. 调试器选择ST‑Link,编译下载程序。
6. 打开Keil软件仿真,调出逻辑分析仪,添加PA0、PB0、PB1、PC13引脚,观察波形。
7. 记录现象,截图,工程上传Gitee。
七、实验现象
下载完成,LED按PA0→PB0→PB1→PC13顺序轮流点亮,每个灯约1秒切换。逻辑分析仪观察引脚周期性翻转,软件延时实际时间和理论1秒存在误差。
八、遇到的问题与解决
1. 编译报大量头文件找不到,没有配置inc头文件路径,添加路径后解决。
2. 忘记添加 USE_STDPERIPH_DRIVER 宏,库函数全部未定义报错。
3. PC13电平逻辑搞反,调用 GPIO_SetBits() 点亮板载LED,灯反而熄灭,修改为 GPIO_ResetBits() 。
4. 逻辑分析仪看不到波形,没有切换到软件仿真模式,修改工程仿真设置。
5. 把全部库文件加入工程,编译速度很慢,只添加rcc.c与gpio.c即可。
九、实验总结
标准外设库对寄存器进行封装,不用记忆寄存器地址,代码可读性高。使用结构体配置GPIO很方便,但同样必须开启外设时钟。
仿真波形直观看到软件延时精度不足,如果需要精准定时,需要使用定时器外设。
实验三 HAL库外部中断:按键控制流水灯暂停/继续
实验名称
HAL库+外部中断实验,按键控制四路流水灯暂停恢复
一、实验目的
1. 熟悉STM32CubeMX图形化配置,生成HAL库工程。
2. 掌握外部中断配置,理解中断回调函数运行机制。
3. 在四路流水灯基础上,按键触发外部中断,实现流水灯运行与暂停切换。
4. 理解中断编程规范:回调函数只修改标志位,耗时代码放在主循环。
5. 工程上传Gitee,整理实验记录。
二、实验器材
1. STM32F103C8T6最小系统板
2. ST‑Link下载器
3. 面包板,LED,220Ω电阻,按键,杜邦线
4. 电脑,STM32CubeMX,Keil‑MDK
三、实验原理
外部中断EXTI:引脚电平跳变触发中断,CPU暂停主循环,执行中断服务函数,最终调用 HAL_GPIO_EXTI_Callback() 回调函数。
本实验按键接PA0,下降沿触发外部中断;设置全局标志位 run_flag ,中断中翻转标志位。
主循环判断标志位: run_flag=1 执行流水灯; run_flag=0 停止流水,LED保持当前状态。
重要:中断回调函数不能放置延时、循环等耗时代码,仅允许简单变量修改。
LED硬件电路和前面实验保持一致。按键一端接PA0,一端接GND,开启内部上拉。
四、程序设计思路
1. CubeMX配置:系统时钟72MHz;PA0配置外部中断下降沿触发,开启上拉;PA0、PB0、PB1、PC13配置为GPIO输出。
2. 生成Keil HAL工程。
3. 定义全局标志位 run_flag=1 ,代表流水灯运行。
4. 在外部中断回调函数翻转run_flag。
5. while主循环判断标志位,标志位为1执行流水灯逻辑,为0不执行。
6. 软件延时,下载调试观察现象。
五、核心代码
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2026 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
uint8_t run_flag = 1; //1=流水灯运行,0=暂停
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
//外部中断回调函数,PA2触发中断自动进入
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == GPIO_PIN_2)
{
run_flag = !run_flag; //翻转状态:运行 ↔ 暂停
}
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if(run_flag == 1)
{
//PA0亮1秒
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
HAL_Delay(1000);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
//PB0亮1秒
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
HAL_Delay(1000);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
//PB1亮1秒
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET);
HAL_Delay(1000);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET);
}
//run_flag=0时,循环空转,流水灯保持暂停
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief GPIO Initialization Function
* @param None
* @retval None
*/
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0|GPIO_PIN_1, GPIO_PIN_SET);
/*Configure GPIO pin : PA0 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/*Configure GPIO pin : PA2 */
GPIO_InitStruct.Pin = GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/*Configure GPIO pins : PB0 PB1 */
GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI2_IRQn);
}
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
六、实验步骤
1. 保留LED接线;按键接PA0与GND,ST‑Link连接开发板。
2. CubeMX选择芯片STM32F103C8T6;配置外部晶振,调试接口Serial Wire。
3. PA0设置为 GPIO_EXTI0 下降沿触发,开启上拉;PA0、PB0、PB1、PC13设置输出。时钟树配置72MHz。
4. 生成Keil工程。
5. 打开工程,替换main.c业务代码,编译。
6. ST‑Link下载程序,按下按键观察流水灯暂停与继续效果。
7. 记录现象截图,工程上传Gitee。
七、实验现象
八、遇到的问题与解决
1. 按键偶尔多次触发,缺少硬件消抖,后续可以增加软件消抖代码。
2. 一开始在中断回调写延时,程序运行异常,老师提醒中断不能放耗时代码,只修改标志位。
3. CubeMX没有开启引脚内部上拉,按键电平不稳定,开启上拉配置后按键识别正常。
4. 重新生成CubeMX工程把main.c代码覆盖,需要保留用户代码段。
九、实验总结
HAL库配合CubeMX图形配置硬件,不需要手写大量初始化代码,开发效率高。
外部中断不需要CPU轮询查询按键,电平变化立刻响应。中断回调函数要尽量简短,业务逻辑放到主循环处理。
对比寄存器、标准库、HAL三种开发方式,对GPIO、时钟、中断有了更加完整理解。