一、需求
点亮电路板上的黄灯
二、分析
阅读原理图,找到黄灯相关的电路


从硬件原理图中可以明确:黄色 LED 灯(标识为 LES-1)的控制引脚与芯片的PA0 端口 相连。当 PA0 端口被配置为推挽输出模式 ,且输出低电平时,该 LED 灯(LES-1)即可被点亮。
三、创建工程
- 创建所需目录结构根据实际项目需求搭建文件夹体系,为保证兼容性与规范性,建议统一使用英文命名文件夹。
- 准备芯片启动文件适配目标 STM32 芯片型号,准备对应的启动文件,该文件是程序初始化的基础,需确保与芯片内核、闪存配置匹配。(具体的方法参考上一篇博客)
三、写代码
在 STM32 单片机中,GPIO、时钟等均属于片上外设,且这些外设会挂载到 APB1 或 APB2 这两条外设总线上。其中,用于连接 LED 的端口,就是我们常说的 GPIO 端口。
与 51 单片机相比,STM32 的一大区别在于时钟管理机制:它为不同的片上外设配置了不同频率的时钟。这就意味着,想要使用某一个外设(比如 GPIO),必须先开启该外设对应的时钟,否则外设无法正常工作。
而二者的相似之处在于:对片上外设的操作,本质上都是通过配置对应的寄存器来实现的。
基于以上特性,通过寄存器操作点亮一颗 LED 的核心步骤可梳理为以下四步:
- 操作时钟寄存器,开启目标 GPIO 外设对应的时钟
- 操作 GPIO 配置寄存器,设置目标 GPIO 端口的工作模式(如推挽输出模式)
- 操作 GPIO 数据寄存器,配置目标端口输出低电平(需匹配硬件电路的点亮逻辑)
- 保持端口的低电平输出状态,维持 LED 点亮
1.时钟配置
为何要对 STM32 的时钟进行配置?
核心原因在于 STM32 各片上外设(如 GPIO、串口、定时器等)的工作特性与需求不同,所需的工作时钟频率也存在差异。若所有外设都统一使用系统最高主频运行,一方面会造成不必要的资源浪费(如低速外设无需高频时钟,高频运行反而增加功耗),另一方面也可能超出部分外设的工作频率上限,导致外设无法稳定工作甚至损坏。
总结
- 适配外设特性:不同外设(如 GPIO、串口、ADC)的设计工作频率不同,时钟配置可精准匹配其运行需求;
- 优化资源利用:避免所有外设都以高频运行,减少不必要的功耗与资源占用,提升系统效率。
控制GPIO的寄存器:


要访问 STM32 的时钟寄存器,我们需要通过基地址 + 偏移地址 的方式来确定其物理地址:首先从芯片数据手册中获取目标外设的基地址 (本示例中时钟相关外设的基地址为 0x40021000,该地址可在手册对应章节中查阅);再结合寄存器的偏移量 (本示例中时钟使能寄存器的偏移地址为 0x18),将二者相加即可得到寄存器的实际物理地址。
基于这两个关键地址信息,就能通过如下方式直接访问该时钟寄存器:
*((uint32_t *)(0x40021000 + 0x18))
补充说明
- 这里使用
uint32_t类型是因为 STM32 的寄存器宽度为 32 位,确保地址访问的位数匹配; - 括号
(0x40021000 + 0x18)先计算出寄存器的物理地址(0x40021018),再通过(uint32_t *)将其转换为 32 位指针类型,最后用*解引用指针,实现对寄存器的读写操作。
总结
- STM32 寄存器访问的核心是通过基地址(外设起始地址)+ 偏移地址(寄存器相对位置) 确定物理地址;
- 需先从芯片手册中获取基地址,再结合寄存器偏移量计算,最后通过指针解引用实现寄存器读写。
这里有个小疑问:这里的基地址假偏移量访问stm32寄存器为什么不写成:
*(((uint32_t *)(0x40021000) + 0x18))
解答:
先拆解两种写法的本质区别
我们先通过计算对比两种写法的实际访问地址,就能直观看到差异:
1.正确写法:*( (uint32_t *)(0x40021000 + 0x18) )
第一步:先做数值相加 → 0x40021000 + 0x18 = 0x40021018
第二步:把结果转成uint32_t类型(32 位无符号整数指针)
第三步:解引用,访问0x40021018这个物理地址的 32 位数据最终访问地址:0x40021018(正是你要的寄存器地址) - 疑问的写法:( ((uint32_t *)(0x40021000)) + 0x18 )
第一步:先把基地址0x40021000转成uint32_t类型(指针类型)
第二步:做指针加法 → 指针 + N 等价于 指针地址 + N× 指针类型大小uint32_t是 4 字节(32 位),所以实际地址是:0x40021000 + 0x18×4 = 0x40021060
第三步:解引用,访问0x40021060这个地址最终访问地址:0x40021060(完全偏离了目标寄存器)
关键原理:C 语言的指针运算规则在 C 语言中,指针的加减运算不是按 "字节" 算,而是按 "指针指向的类型大小" 算:对于char(1 字节):ptr + 1 → 地址 + 1 字节
对于uint16_t*(2 字节):ptr + 1 → 地址 + 2 字节
对于uint32_t*(4 字节):ptr + 1 → 地址 + 4 字节
STM32 的寄存器都是 32 位(4 字节)对齐的,手册里给出的 "偏移量" 是相对于基地址的字节偏移,不是 "寄存器个数偏移"。所以必须先把基地址和偏移量(字节数)做数值相加,再转成指针,才能准确指向目标寄存器。
2.GPIO工作模式配置
GPIO的寄存器基地址:

偏移地址:


寄存器配置为0011
*(uint32_t *)(0x40010800 + 0x00) = 3;
3.PA0输出低电平
端口输出数据寄存器GPIOx_ODR
*(uint32_t *)(0x40010800 + 0x0c) = 0xfffe;
4.用一个死循环保持状态
while(1)
{}
5.全部代码
cpp
include <stdint.h>
int main(void)
{
//设置端口时钟
*((uint32_t*)(0x40021000 + 0x18)) = 0x4;
//打开GPIO端口A0、A1、A8
*((uint32_t*)(0x40010800 + 0x00)) = 0x33;
*((uint32_t*)(0x40010800 + 0x04)) = 0x03;
//端口输出数据寄存器GPIOx_ODR
*(uint32_t *)(0x40010800 + 0x0c) = 0xfefc;
//用一个死循环保持状态
while(1)
{}
}
烧写入程序后,需要按复位键重新运行,或者在keil中的debug中选择下载后重启


注意:这里需要将pack选项卡中的使能选项框取消,否则下载后重启功能不生效。

四、总结
通过寄存器的操作,我们完成了一个LED灯的点亮,但是,这样的写法并不是最终的形态,不能更好的适应于大型项目的开发,在下一篇博客中,将代码进一步完善。