一、STM32F103C8T6 最小系统核心板介绍
STM32F103C8T6 是 ST 意法半导体推出的Cortex‑M3 内核 32 位高性能单片机,LQFP‑48 封装,是学生嵌入式学习最常用的最小系统核心板。

(一)核心参数
(1)内核:ARM Cortex-M3,最高工作频率 72MHz
(2)存储:Flash 64KB(存放程序代码),SRAM 20KB(程序运行临时变量)
(3)供电:工作电压 2.0~3.6V,一般外部使用 3.3V 供电;支持 VBAT 纽扣电池给 RTC 实时时钟备份供电
(4)外设资源:GPIO 通用 IO 口、ADC、定时器、USART 串口、I2C、SPI、CAN、USB 等
(二)最小系统板必备电路
(1)电源电路:外部 5V 输入,AMS1117-3.3V 稳压芯片输出 3.3V 给主芯片供电
(2)复位电路 NRST:RC 复位,按键按下低电平,芯片系统复位
(3)高速外部晶振 HSE:8MHz 无源晶振,经 PLL 倍频得到 72MHz 系统时钟
(4)低速外部晶振 LSE:32.768KHz,为 RTC 实时时钟提供时钟
(5)BOOT 启动选择引脚:
- BOOT0=0:从Flash 启动(正常下载程序模式,日常使用)
- BOOT0=1:从系统存储器启动(串口 ISP 下载)
(6)下载调试电路:集成 SWD 调试接口(PA13-SWDIO、PA14-SWCLK),使用 ST-Link 下载调试
(7)板载 LED::核心板自带 LED,LED 阳极接 3.3V,引脚输出低电平点亮 LED,高电平熄灭
二、相关概念的学习
(一)GPIOx端口
GPIO = General‑Purpose Input Output,通用输入输出端口。
GPIOx 中,x 是端口标识符,代表不同端口组,在 STM32F103 芯片中可以是 A、B、C、D、E,于是就有 GPIOA、GPIOB、GPIOC、GPIOD、GPIOE。而且每一个 GPIOx 端口,固定拥有最多 16 个 IO 引脚:Px0 ~ Px15。GPIOA、GPIOB、GPIOC 挂载在 APB2 外设总线上。所以必须操作RCC_APB2ENR寄存器,把对应端口时钟打开;时钟不开启,GPIO 完全失效,读写寄存器无效。bit2 置 1 → 开启GPIOA时钟,bit4 置 1 → 开启GPIOC时钟。每组端口拥有自己独立的寄存器,地址互不相同,GPIOA 的寄存器只会控制 A 组引脚,不能控制 GPIOC 引脚。

(二)寄存器
在 STM32 单片机里,寄存器是 MCU 芯片内部、拥有固定物理地址的硬件存储单元,是 CPU 和硬件外设(GPIO、定时器、串口等)沟通的唯一桥梁。普通 C 语言变量是放在 RAM 内存里,只是软件临时数据;寄存器是真实硬件电路,对寄存器读写,直接改变芯片硬件行为。
核心特点
a.每个寄存器都有唯一固定物理地址
例如:
- GPIOA_CRL 地址:0x40010800
- GPIOA_ODR 地址:0x4001080C
CPU 通过访问这个地址,读写寄存器里面的数据。
b.寄存器由很多二进制位(bit)组成 STM32 的 GPIO 寄存器大多是 32 位,也就是 32 个 0/1 开关。 每一个 bit 或者连续几个 bit,专门控制硬件某一项功能。 比如GPIOA_CRL中,每 4 个 bit 控制 1 个 PA 端口引脚,用来设置引脚模式、输出速度。
c.每组 GPIOx 端口,都自带一套独立寄存器 GPIOA、GPIOB、GPIOC 各自拥有一套 CRL、CRH、ODR、IDR 寄存器。
- 写 GPIOA 的寄存器,只会修改 PA 引脚,不会影响 PB、PC 引脚;
- 写 GPIOC 寄存器,只修改 PC 引脚(本实验 PC13 板载 LED 就在 GPIOC)。

1、地址查找
每个 GPIO 端口都有一个基地址,CRL寄存器的偏移 = 0x00,真实物理地址 = GPIO端口基地址 + 偏移0x00

2、端口配置低寄存器
是GPIO 通用输入输出口,总线APB2,只控制该端口的 Px0 ~ Px7(低 8 个 IO 引脚),Px8‑Px15 由GPIOx_CRH端口配置高寄存器控制。x 代表端口A/B/C/D/E,例如 GPIOA_CRL 控制 PA0‑PA7;GPIOB_CRL控制 PB0‑PB7。修改单个引脚配置时,先把该引脚对应的 4 位清零 &= ~(0x0F << (4*n)),再写入新配置,不能直接给 CRL 整个寄存器赋值,否则会覆盖其他引脚;复用功能模式下,ODR 寄存器不再控制引脚电平,由片内外设(定时器、串口、SPI)控制引脚输出;输入上拉 / 下拉:CNF 只开启功能选择,真正电阻开关由GPIOx_ODR输出寄存器对应位控制。

3、GPIOx_CRH 端口配置高寄存器
CRH 只管 Px8‑Px15,Px0‑Px7 交给 CRL,不要搞混寄存器,修改单个高引脚应该先把该引脚对应的 4 位掩码0x0F移位清零,再写配置,禁止直接GPIOA->CRH = xxx,会覆盖 PA8‑PA15 全部引脚。上拉 / 下拉时CNF 只选择模式,必须配合 ODR 寄存器对应位,内部电阻才生效。复用功能输出时,ODR 不再控制引脚电平,由外设(TIM/USART/SPI)接管引脚。

4、APB2ENR
属于 RCC(复位和时钟控制)模块,必须先开启 GPIO 时钟,GPIO 寄存器才能正常工作。相对 RCC 基地址偏移地址是0x18,RCC 基地址为0x40021000所以 RCC_APB2ENR 物理地址 = 0x40021000 + 0x18 = 0x40021018,可读可写,每一位控制一个 APB2 总线上外设的时钟开关,写1时打开该外设时钟(使能),写0时关闭该外设时钟(复位默认,省电)

5、IDR、ODR
| 对比项 | GPIOx_IDR | GPIOx_ODR |
|---|---|---|
| 偏移 | 0x08 | 0x0C |
| 读写属性 | 只读,写无效 | 可读可写 |
| 含义 | 读外部引脚实际电平 | 读写内部输出锁存值 |
| 用途 | 读取按键、外部输入信号 | 设置输出电平;输入模式控制上下拉电阻 |
| 风险 | 无,只管读 | 修改单 bit 存在中断干扰风险 |


三、基于寄存器实现流水灯
(一)实验目的
理解 STM32 GPIO 外设工作原理,学习直接操作寄存器地址方式控制 IO 口,掌握 RCC 时钟使能,懂得 STM32 外设必须开启时钟才可以工作读懂 CRL、CRH、ODR 寄存器功能,掌握位操作 ,使用GPIOA、GPIOB、GPIOC这3种端口的任意引脚控制3个LED灯(这里用到的是PA1,PA3,PA5),结合最小系统板自带 PC13 板载 LED,实现 4 个 LED 轮流流水闪烁,间隔 1 秒
(二)软硬件准备
1.硬件
STM32F103C8T6 开发板,ST‑Link 下载调试器,USB 数据线、杜邦线,LED灯
2.软件
Keil MDK5 用写、编译、调试代码,初学 STM32,第一步就是搞定 Keil MDK5 开发环境。这块步骤繁琐,截图多,这里就不重复写长篇安装图文了,网上成熟教程资源非常多,要学会利用资源,跟着一步步操作效率更高。
STM32 入门教程‑2023 版 细致讲解 中文字幕‑哔哩哔哩
3.程序设计与实现
(1)代码思路
A.寄存器地址宏定义:
使用volatile uint32_t强制映射硬件寄存器真实物理地址。volatile告诉编译器:该地址是硬件寄存器,不要做缓存优化,每次都要真实读写内存。把RCC_APB2ENR、GPIOA_CRL、GPIOA_ODR、GPIOC_CRH、GPIOC_ODR定义成宏,后面代码直接写名字访问寄存器。
B.delay_1s 软件延时函数:
两层 for 空循环,靠 CPU 循环消耗时间,粗略实现 1 秒延时。循环变量加volatile,防止 Keil 高优化等级直接把空循环删掉,导致延时失效。
C.LED_GPIO_Init () 初始化函数:
操作RCC_APB2ENR,置位 bit2 打开 GPIOA 时钟,置位 bit4 打开 GPIOC 时钟。配置GPIOA_CRL:PA1、PA3、PA5 属于 PA0‑PA7,CRL 每 4 位控制 1 个引脚。先&= ~掩码:把对应引脚的 4 位全部清零;再|= 配置值:写入0001,设置为推挽输出,速度 2MHz。配置GPIOC_CRH:PC13 属于 PC8‑PC15,在 CRH 寄存器,同样先清 4 位,再写 0001 推挽输出。配置初始电平:操作 ODR,所有 LED 初始熄灭。PA1/PA3/PA5 外接 LED输出低电平熄灭;PC13 板载 LED:输出高电平熄灭。
D.main 主函数
先调用LED_GPIO_Init()完成端口初始化。进入while(1)死循环,循环是单片机程序主体,永远不会退出。分 4 个步骤轮流点亮单个 LED,其余全部熄灭:修改GPIOA_ODR、GPIOC_ODR控制引脚高低电平;每点亮一组,调用delay_1s()停留 1 秒;依次:PA1 亮 → PA3 亮 → PA5 亮 → PC13 亮,往复循环。
(2)点亮3个LED灯
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
//开启GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// PA1 PA3 PA5 推挽输出 50MHz
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_3 | GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//初始全部熄灭(输出高电平,低电平点亮LED)
GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_3 | GPIO_Pin_5);
while(1)
{
//点亮PA1,其余熄灭
GPIO_ResetBits(GPIOA, GPIO_Pin_1);
GPIO_SetBits(GPIOA, GPIO_Pin_3);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
Delay_ms(1000);
//点亮PA3,其余熄灭
GPIO_SetBits(GPIOA, GPIO_Pin_1);
GPIO_ResetBits(GPIOA, GPIO_Pin_3);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
Delay_ms(1000);
//点亮PA5,其余熄灭
GPIO_SetBits(GPIOA, GPIO_Pin_1);
GPIO_SetBits(GPIOA, GPIO_Pin_3);
GPIO_ResetBits(GPIOA, GPIO_Pin_5);
Delay_ms(1000);
}
}
(3)用PC13验证自己烧录的代码是否正常运行
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
//开启GPIOA、GPIOC时钟(PA1 PA3 PA5属于GPIOA,PC14属于GPIOC)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC, ENABLE);
// PA1 PA3 PA5 推挽输出 50MHz
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_3 | GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// PC14 推挽输出 50MHz
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_Init(GPIOC, &GPIO_InitStructure);
//初始全部熄灭(输出高电平,低电平点亮LED)
GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_3 | GPIO_Pin_5);
GPIO_SetBits(GPIOC, GPIO_Pin_14);
while(1)
{
//点亮PA1,其余熄灭
GPIO_ResetBits(GPIOA, GPIO_Pin_1);
GPIO_SetBits(GPIOA, GPIO_Pin_3);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
GPIO_SetBits(GPIOC, GPIO_Pin_14);
Delay_ms(1000);
//点亮PA3,其余熄灭
GPIO_SetBits(GPIOA, GPIO_Pin_1);
GPIO_ResetBits(GPIOA, GPIO_Pin_3);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
GPIO_SetBits(GPIOC, GPIO_Pin_14);
Delay_ms(1000);
//点亮PA5,其余熄灭
GPIO_SetBits(GPIOA, GPIO_Pin_1);
GPIO_SetBits(GPIOA, GPIO_Pin_3);
GPIO_ResetBits(GPIOA, GPIO_Pin_5);
GPIO_SetBits(GPIOC, GPIO_Pin_14);
Delay_ms(1000);
//点亮PC14,其余熄灭
GPIO_SetBits(GPIOA, GPIO_Pin_1);
GPIO_SetBits(GPIOA, GPIO_Pin_3);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
Delay_ms(1000);
}
}
4.实验现象
LED灯
LED灯
四、总结
本次STM32裸寄存器流水灯实验,可以掌握了GPIO相关寄存器的配置方法,明白STM32外设上电后时钟默认关闭,使用前必须开启对应时钟。实验中学会使用Keil调试功能,单步运行观察寄存器数值变化,直观验证代码效果。也认识到软件空循环延时精度较差,仅适合实验演示。编写代码不能直接复制照搬,要弄懂每一行代码对应的硬件操作。
4128



被折叠的 条评论
为什么被折叠?



