尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

STM32F407 寄存器编程点亮 LED—— 从零搭建纯裸机工程

STM32F407 寄存器编程点亮 LED—— 从零搭建纯裸机工程
📅 发布时间:2026/6/20 22:26:07

前言

在 STM32 的开发中,HAL 库和标准库为我们屏蔽了大量的底层细节,让开发者可以快速上手。但如果你想真正理解 MCU 是如何工作的,或者在某些资源受限的场景下追求极致的代码效率,寄存器编程是绕不开的一课。

本篇文章就以DShanMCU-F407 开发板(STM32F407ZGT6)为例,结合之前学习的 GPIO 操作和 LED 硬件知识,通过纯寄存器操作点亮连接到 PF9 引脚的一个 LED,帮助你彻底搞懂 RCC 时钟使能、GPIO 寄存器配置以及 ODR 寄存器控制输出的原理。文中代码可直接在 Keil / IAR 等环境下编译运行,且不依赖任何库文件。

更特别的是,我们将完全从零搭建工程——整个 Keil 工程只需要两个源文件:main.c和start.s,没有任何 SDK、启动文件或头文件依赖,让你看清单片机从复位到 main 函数运行的每一行代码。


目录

前言

一、建立最精简的 Keil 工程

二、寄存器的指针操作

1. 定义指向寄存器地址的指针

2. 读写寄存器

3. 和普通变量操作的核心区别

4. 裸机编程必备:volatile 修饰符

三、代码编写与解析

1. 类型定义和延时函数

2. 使能 GPIOF 时钟

3. 配置 PF9 为输出模式

4. 配置输出类型为推挽输出

5. 配置输出速度为低速

6. 通过 ODR 寄存器点亮/熄灭 LED

7. 源码

源码下载:


一、建立最精简的 Keil 工程

想让代码跑起来,首先要有一个正确的启动文件,它负责初始化堆栈指针并跳转到main。在新建 Keil 工程时,选择芯片为STM32F407ZGTx,但不勾选任何 CMSIS 或 HAL 组件。然后向工程中添加两个文件:

  • main.c—— 包含我们自己的寄存器操作代码

  • start.s—— 最简启动汇编文件,直接参考官方startup_stm32f407xx.s的核心逻辑

start.s 内容如下:

PRESERVE8 THUMB ; Vector Table Mapped to Address 0 at Reset AREA RESET, DATA, READONLY EXPORT __Vectors __Vectors DCD 0 DCD Reset_Handler ; Reset Handler AREA |.text|, CODE, READONLY ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT main LDR SP, =(0x20000000+0x20000) BL main ENDP END

逐行解读:

  • PRESERVE8与THUMB:确保 8 字节对齐并使用 Thumb2 指令集。

  • AREA RESET, DATA, READONLY:定义一个只读数据段,存放向量表。这里仅保留了必不可少的初始栈顶值(写0表示临时占位,真正的栈地址在后续汇编里手动设置)和复位向量Reset_Handler。

  • AREA |.text|, CODE, READONLY:切换到代码段。

  • Reset_Handler是复位后第一个执行的函数:
    先通过LDR SP, =(0x20000000+0x20000)将栈指针设置在 SRAM 的顶部(F407 的 SRAM 共 128 KB,0x20000000 + 0x20000是末尾地址,可以看看魔术棒->Target)。
    然后BL main跳转到我们的 C 入口main函数。

这个启动文件虽然短小,但完全满足运行 C 代码的需求。工程中不需要任何 stm32f4xx.h 或 core_cm4.h,所有寄存器地址由我们手动计算并直接操作。至此,一个“纯裸机”的工程就诞生了。


二、寄存器的指针操作

寄存器是芯片厂商提前映射到固定地址的硬件单元,我们只需要把地址直接赋值给指针,就能像读写变量一样操作寄存器了。

1. 定义指向寄存器地址的指针

// 直接把寄存器的物理地址赋值给指针p // 示例地址0x40010800,对应STM32的某个外设寄存器 unsigned int *pReg = (unsigned int *)0x40010800;

注意:地址必须强制转换为对应类型的指针,这里用unsigned int *(32 位无符号指针),和寄存器的 32 位位宽匹配。

2. 读写寄存器

// 写寄存器:给寄存器地址赋值,相当于修改寄存器的值 *pReg = 0x00000001; // 读寄存器:读取寄存器地址的值,相当于获取寄存器的当前状态 unsigned int reg_val = *pReg;

3. 和普通变量操作的核心区别

操作对象地址来源读写效果
普通变量a编译器自动分配的内存地址仅修改 RAM 中的变量值,不影响硬件
寄存器芯片手册规定的固定物理地址直接修改外设硬件状态(如 GPIO 电平、时钟使能)

4. 裸机编程必备:volatile 修饰符

在实际操作寄存器时,必须给指针加上volatile修饰符,防止编译器优化掉寄存器的读写操作:

// 正确写法:用volatile修饰,确保每次都直接读写硬件地址 #define __IO volatile __IO unsigned int *pReg = (__IO unsigned int *)0x40010800

三、代码编写与解析

下面我们将完整的main.c代码分段解释。

1. 类型定义和延时函数

#define __IO volatile typedef unsigned int uint32_t void delay(int time) { while(time--); }

延时函数通过简单的循环实现,精度不高,仅供演示。

2. 使能 GPIOF 时钟

我们先查看F407的参考手册,这里我们可以不用去网上下载和查找,直接在keil5里打开Help里的Open Books Window。

打开Reference

在 F407 中,GPIOA~GPIOI 都挂在AHB1总线上,对应的时钟使能位在 RCC->AHB1ENR 寄存器中,先找到存储器映射,查看RCC时钟控制寄存器的基地址。

RCC时钟控制寄存器的基地址为:0x40023800。

然后找到RCC AHB1 外设时钟使能寄存器 (RCC_AHB1ENR)

0x40023800 是 RCC 基地址,加上偏移0x30得到 AHB1ENR 的地址。

Bit5对应 GPIOF 的时钟使能位(GPIOFEN),写入 1 即可使能 GPIOF 的时钟。

__IO uint32_t *pReg; /* 使能GPIOF */ pReg = (__IO uint32_t *)(0x40023800 + 0x30); *pReg |= (1 << 5);

3. 配置 PF9 为输出模式

查看GPIOF的基地址。

GPIOF的基地址为:0x40021400。

查看GPIO端口模式寄存器,GPIOF的基地址加上偏移0x00就是GPIOF_MODER的地址了。

欲将PF9设为输出模式,需将寄存器的18位、19位设为0、1,即通用输出模式。

/* 设置PF9的端口模式(输出模式)*/ pReg = (__IO uint32_t *)(0x40021400 + 0x00); *pReg &= ~(3 << 18); *pReg |= (1 << 18);

GPIOF_MODER每 2 位控制一个引脚的模式,先清零复位,再写入1到bit18。

4. 配置输出类型为推挽输出

查看GPIO 端口输出类型寄存器。

GPIOF的基地址加上偏移0x04就是GPIOF_OTYPER的地址。

/* 设置PF9的输出模式(推挽输出) */ pReg = (__IO uint32_t *)(0x40021400 + 0x04); *pReg &= ~(1 << 9);

OTYPER的 bit9 控制 PF9 的输出类型:0 为推挽,1 为开漏。推挽可输出确定的高/低电平,驱动 LED 完全足够。

5. 配置输出速度为低速

查看GPIO 端口输出速度寄存器。

GPIOF的基地址加上偏移0x08就是GPIOF_OSPEEDR的地址。

/* 设置PF9的输出速度(低速) */ pReg = (__IO uint32_t *)(0x40021400 + 0x08); *pReg &= ~(3 << 18);

OSPEEDR同样每 2 位控制一个引脚的速度。清零即为最低速(2 MHz 左右)。

6. 通过 ODR 寄存器点亮/熄灭 LED

查看GPIO 端口输出数据寄存器。

GPIOF的基地址加上偏移0x14就是GPIOF_ODR的地址。

将对应的bitx写入0/1,硬件输出低电平/高电平。

在我的开发板上,LED正极接了3.3V,负极接单片机的PF9引脚。

PF9输出低电平,二极管导通,LED亮;输出高电平,二极管截止,LED灭。

代码如下:

pReg = (__IO uint32_t *)(0x40021400 + 0x14); while(1) { /* LED亮 */ *pReg &= ~(1 << 9); delay(1000000); /* LED灭 */ *pReg |= (1 << 9); delay(1000000); }

7. 源码

#define __IO volatile typedef unsigned int uint32_t; void delay(int d) { while(d--); } int main(void) { __IO unsigned int *pReg; /* 使能GPIOF */ pReg = (__IO uint32_t *)(0x40023800 + 0x30); *pReg |= (1 << 5); /* 设置PF9的端口模式(输出模式)*/ pReg = (__IO uint32_t *)(0x40021400 + 0x00); *pReg &= ~(3 << 18); *pReg |= (1 << 18); /* 设置PF9的输出模式(推挽输出) */ pReg = (__IO uint32_t *)(0x40021400 + 0x04); *pReg &= ~(1 << 9); /* 设置PF9的输出速度(低速) */ pReg = (__IO uint32_t *)(0x40021400 + 0x08); *pReg &= ~(3 << 18); pReg = (__IO uint32_t *)(0x40021400 + 0x14); while(1) { /* LED亮 */ *pReg &= ~(1 << 9); delay(1000000); /* LED灭 */ *pReg |= (1 << 9); delay(1000000); } }

实验现象:LED500ms左右闪烁一次。

源码下载:

通过网盘分享的文件:STM32F407_LED_Register.zip
链接:http:// https://pan.baidu.com/s/1dswThMdzCLUmGPNRsVKIiQ?pwd=taoq 提取码: taoq
--来自百度网盘超级会员v2的分享

如果觉得本文对你有帮助,欢迎点赞、收藏、关注,后续将继续更新ARM架构与编程相关的内容。

相关新闻

  • 纠结智己LS6和问界M7,两款车选哪款车更值得买?2026中大型SUV对比参考 - 外贸老黄
  • 2026年高考排名2000-3000区间,国内AI人工智能专业中南大学适配性深度分析 - 温茶叙旧
  • 想搞定长沙全屋定制?这家专业团队千万别错过! - 资讯速览

最新新闻

  • 告别复杂图表工具!用Mermaid.js轻松创建专业数据可视化的终极指南
  • 深度学习可视化:从Grad-CAM到训练监控,打开模型黑箱的完整指南
  • 【楼长修楼防水案例】青岛业主自主报修,单人房屋漏水维修全过程 - 青岛防水品牌推荐
  • CANN/ge GE图引擎API验证算子属性
  • 手把手教你构建统计局地区经济数据爬虫:从环境搭建到数据持久化全指南
  • Visual C++运行库修复终极指南:5分钟快速解决Windows软件启动错误

日新闻

  • Visual C++运行库修复终极指南:5分钟快速解决Windows软件启动错误
  • 手把手教你构建统计局地区经济数据爬虫:从环境搭建到数据持久化全指南
  • 2026多Agent深度解析:用AI团队替代单一模型,四种架构实战落地

周新闻

  • Visual C++运行库修复终极指南:5分钟快速解决Windows软件启动错误
  • 手把手教你构建统计局地区经济数据爬虫:从环境搭建到数据持久化全指南
  • 2026多Agent深度解析:用AI团队替代单一模型,四种架构实战落地

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号