从零开始学嵌入式之STM32——3.使用寄存器点亮一盏LED灯

一、需求

点亮电路板上的黄灯

二、分析

阅读原理图,找到黄灯相关的电路


从硬件原理图中可以明确:黄色 LED 灯(标识为 LES-1)的控制引脚与芯片的PA0 端口 相连。当 PA0 端口被配置为推挽输出模式 ,且输出低电平时,该 LED 灯(LES-1)即可被点亮。

三、创建工程

  1. 创建所需目录结构根据实际项目需求搭建文件夹体系,为保证兼容性与规范性,建议统一使用英文命名文件夹。
  2. 准备芯片启动文件适配目标 STM32 芯片型号,准备对应的启动文件,该文件是程序初始化的基础,需确保与芯片内核、闪存配置匹配。(具体的方法参考上一篇博客)

三、写代码

在 STM32 单片机中,GPIO、时钟等均属于片上外设,且这些外设会挂载到 APB1 或 APB2 这两条外设总线上。其中,用于连接 LED 的端口,就是我们常说的 GPIO 端口。

与 51 单片机相比,STM32 的一大区别在于时钟管理机制:它为不同的片上外设配置了不同频率的时钟。这就意味着,想要使用某一个外设(比如 GPIO),必须先开启该外设对应的时钟,否则外设无法正常工作。

而二者的相似之处在于:对片上外设的操作,本质上都是通过配置对应的寄存器来实现的

基于以上特性,通过寄存器操作点亮一颗 LED 的核心步骤可梳理为以下四步:

  1. 操作时钟寄存器,开启目标 GPIO 外设对应的时钟
  2. 操作 GPIO 配置寄存器,设置目标 GPIO 端口的工作模式(如推挽输出模式)
  3. 操作 GPIO 数据寄存器,配置目标端口输出低电平(需匹配硬件电路的点亮逻辑)
  4. 保持端口的低电平输出状态,维持 LED 点亮

1.时钟配置

为何要对 STM32 的时钟进行配置?

核心原因在于 STM32 各片上外设(如 GPIO、串口、定时器等)的工作特性与需求不同,所需的工作时钟频率也存在差异。若所有外设都统一使用系统最高主频运行,一方面会造成不必要的资源浪费(如低速外设无需高频时钟,高频运行反而增加功耗),另一方面也可能超出部分外设的工作频率上限,导致外设无法稳定工作甚至损坏。

总结

  1. 适配外设特性:不同外设(如 GPIO、串口、ADC)的设计工作频率不同,时钟配置可精准匹配其运行需求;
  2. 优化资源利用:避免所有外设都以高频运行,减少不必要的功耗与资源占用,提升系统效率。
    控制GPIO的寄存器:


    要访问 STM32 的时钟寄存器,我们需要通过基地址 + 偏移地址 的方式来确定其物理地址:首先从芯片数据手册中获取目标外设的基地址 (本示例中时钟相关外设的基地址为 0x40021000,该地址可在手册对应章节中查阅);再结合寄存器的偏移量 (本示例中时钟使能寄存器的偏移地址为 0x18),将二者相加即可得到寄存器的实际物理地址

基于这两个关键地址信息,就能通过如下方式直接访问该时钟寄存器:

*((uint32_t *)(0x40021000 + 0x18))

补充说明

  • 这里使用uint32_t类型是因为 STM32 的寄存器宽度为 32 位,确保地址访问的位数匹配;
  • 括号(0x40021000 + 0x18)先计算出寄存器的物理地址(0x40021018),再通过(uint32_t *)将其转换为 32 位指针类型,最后用*解引用指针,实现对寄存器的读写操作。

总结

  1. STM32 寄存器访问的核心是通过基地址(外设起始地址)+ 偏移地址(寄存器相对位置) 确定物理地址;
  2. 需先从芯片手册中获取基地址,再结合寄存器偏移量计算,最后通过指针解引用实现寄存器读写。
    这里有个小疑问:这里的基地址假偏移量访问stm32寄存器为什么不写成:
    *(((uint32_t *)(0x40021000) + 0x18))
    解答:
    先拆解两种写法的本质区别
    我们先通过计算对比两种写法的实际访问地址,就能直观看到差异:
    1.正确写法:*( (uint32_t *)(0x40021000 + 0x18) )
    第一步:先做数值相加 → 0x40021000 + 0x18 = 0x40021018
    第二步:把结果转成uint32_t类型(32 位无符号整数指针)
    第三步:解引用,访问0x40021018这个物理地址的 32 位数据最终访问地址:0x40021018(正是你要的寄存器地址)
  3. 疑问的写法:( ((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灯的点亮,但是,这样的写法并不是最终的形态,不能更好的适应于大型项目的开发,在下一篇博客中,将代码进一步完善。

相关推荐
浩子智控3 小时前
商业航天计算机抗辐射设计
单片机·嵌入式硬件
独处东汉6 小时前
freertos开发空气检测仪之输入子系统结构体设计
数据结构·人工智能·stm32·单片机·嵌入式硬件·算法
czy87874757 小时前
机智云 MCU OTA可以对MCU程序进行无线远程升级。
单片机·嵌入式硬件
A9better9 小时前
嵌入式开发学习日志52——二值与计数信号量
单片机·嵌入式硬件·学习
日更嵌入式的打工仔10 小时前
(实用向)中断服务程序(ISR)的优化方向
笔记·单片机
想放学的刺客11 小时前
单片机嵌入式试题(第25)嵌入式系统可靠性设计与外设驱动异常处理
stm32·单片机·嵌入式硬件·mcu·物联网
wotaifuzao11 小时前
STM32+FreeRTOS 长期可维护架构设计(事件驱动篇)-- 告别“屎山”代码
c语言·stm32·嵌入式硬件·freertos·状态机·事件驱动·嵌入式架构
淘晶驰AK11 小时前
大学如何自学嵌入式开发?
单片机·嵌入式硬件
yantaohk11 小时前
【2025亲测】中兴B860AV3.2M完美刷机包ATV版本安卓9-解决1G运存BUG,开ADB已ROOT
android·嵌入式硬件·adb·云计算