目录
[1. 编译 + 链接,生成 .hex 或 .bin 文件](#1. 编译 + 链接,生成 .hex 或 .bin 文件)
[1. CPU通过 I-Bus(指令总线)从Flash取指令](#1. CPU通过 I-Bus(指令总线)从Flash取指令)
[2. CPU通过 D-Bus(数据总线)从Flash取常量](#2. CPU通过 D-Bus(数据总线)从Flash取常量)
[3. 变量在SRAM中,CPU通过 S-Bus 读写](#3. 变量在SRAM中,CPU通过 S-Bus 读写)
[4. 控制外设:操作"外设寄存器",而不是"内存"](#4. 控制外设:操作“外设寄存器”,而不是“内存”)
[全局变量 / 静态变量](#全局变量 / 静态变量)
[常量(const修饰的变量 / 字符串字面量)](#常量(const修饰的变量 / 字符串字面量))
[下拉输入(Pull-down Input)](#下拉输入(Pull-down Input))
[模拟输入(Analog Input)](#模拟输入(Analog Input))
MCU内部的总线和统一编址
我们来看一下MCU内部的情况

可以看到MCU和外设之间都有线去相互连接的,他们的彼此之间都是相互联系的,但是根据速度的需求,他们拉线的需求就不一样了。
总线矩阵
总线矩阵 = 芯片内部的"立交桥",把CPU、内存、各个外设全部连接在一起,让数据能从一个地方传到另一个地方。
所以要把CPU和外设都有联系就有了下图理解,还有总线主从设备的概念。

但是显而易见,在原理图上面,不能把所有的主设备和从设备两两连接, 那样就太复杂了。未来解决这样的方法就有了总线矩阵作为枢纽。

总线矩阵的特性
1、支持并行访问:
主设备 A 访问从设备 B 时, 主设备 C 访问从设备 D. 此时两者是完全并行的, 不会相互影响.
2、支持动态优先级调度 (智能仲裁)
主设备 A 和 主设备 B 同时访问从设备 C 的时候
如果是相同优先级的请求 (例如 DMA1 和 DMA2 发出的请求), 此时总线矩阵会按照轮询的方式协调访问顺序.
如果是不同优先级的请求 (例如 DMA 和 CPU 发出的请求, DMA 的请求优先级要更⾼), ⾼优先级请求会抢占低优先级请求, 保证实时性.
3、支持专用路径直连
比如 CPU 和 Flash 之间, 专门架设 ICode 总线. 读取指令这个操作⾮常频繁, 使⽤专⻔的路线可以有效降低和其他设备访问操作的冲突, 提高效率.
4、支持按需连通
仅当主设备实际访问从设备时,矩阵才动态建⽴物理连接,减少无效功耗
因此就有下面的基本结构

不同的线的区别和理解

stm32程序执行过程
第一阶段:编译 (Compilation)
C代码(main.c),最终被工具链(如Keil的ARMCC或GCC)处理成芯片能看懂的二进制文件。
1. 编译 + 链接,生成 .hex 或 .bin 文件
-
编译:把每一行C语言,翻译成对应的ARM汇编指令,再转成机器码(0和1)。
-
链接 :把所有编译好的机器码,按照你在"链接脚本"里规定的地址,组织起来。
-
关键产出物 :
.hex(带地址信息的文本格式)或.bin(纯二进制)。其中明确指定了:-
哪些二进制要放在Flash的起始地址
0x08000000(中断向量表)。 -
哪些二进制要放在Flash的
0x08000200以后(main函数代码)。 -
哪些只读常量要放在Flash的特定位置(
.rodata段)。
-
第二阶段:烧录 (Programming)
你可以想象成把一张写满了"操作说明"的SD卡,插进一个播放器里。
-
方式 :通过SWD(串行调试接口) 或 JTAG,或者通过系统Bootloader(串口/USB)。
-
本质 :烧录器按照Flash的编程手册,通过特定的时序和指令,把
.bin文件的内容,一个字节一个字节地写入 到0x08000000开始的Flash物理存储单元中。 -
结果:掉电不会丢,程序"固化"在芯片里了。
第三阶段:运行 (Execution)
1. CPU通过 I-Bus(指令总线)从Flash取指令
-
具体过程 :CPU内部有一个指令指针寄存器(PC, Program Counter) ,它指着
0x08000200(你的main函数入口)。 -
动作 :CPU通过 I-Bus 发出读请求:"地址
0x08000200的数据给我。" -
路径 :I-Bus → 总线矩阵 → Flash接口控制器 → Flash存储阵列。
-
后续 :Flash接口控制器从Flash阵列中读出一串32位的机器码(比如
0x2000F002),通过I-Bus原路返回给CPU。CPU的解码器认出这是一条"将R0寄存器加1"的指令,于是执行它。 -
流水线 :为了快,CPU不是等执行完一条才去取下一条。它通过I-Bus预取 后面几条指令,放到指令缓冲区里,这叫指令预取缓冲区,能有效减少等待Flash的慢速延迟。
2. CPU通过 D-Bus(数据总线)从Flash取常量
常量 (比如 const char* str = "Hello",或者一个大数组查找表)是跟着程序一起,被烧录在Flash里的。
-
具体过程 :当指令需要读取一个常量时(例如
uint16_t val = const_array[5];),指令会把常量所在的Flash地址(比如0x08010020)告诉CPU。 -
动作 :CPU通过 D-Bus 发出读请求:"地址
0x08010020的数据给我。" -
区别 :I-Bus和D-Bus是两条独立的物理总线! 所以在CPU通过I-Bus取第N+1条指令的同时,可以通过D-Bus去取第N条指令需要的常量,两者完全并行,不冲突。这就是哈佛架构(改进型)的优势。
3. 变量在SRAM中,CPU通过 S-Bus 读写
你的笔记写的是"SBus",在STM32F103的总线矩阵里,CPU访问SRAM确实主要是通过 System Bus(系统总线,简称S-Bus)。
-
变量本质 :你写的
int counter = 0;,系统会在SRAM(0x20000000区域)中划出4个字节(32位)来专门存放counter。 -
读变量 :CPU执行
if(counter == 10)时,会通过S-Bus发出读请求:"把地址0x20000010的4个字节读给我"。总线矩阵将请求路由到SRAM控制器,取回数据。 -
写变量 :CPU执行
counter++时,会通过S-Bus发出写请求:"把数值11写到地址0x20000010去"。总线矩阵将请求路由到SRAM控制器,执行写入。 -
特殊情况(你笔记提到"通过DBus读SRAM") :在F103内部,CPU的D-Bus也可以访问SRAM(这属于总线矩阵的一个交叉连接)。但主线依然是S-Bus,除非你通过某些特殊指令强制走D-Bus,否则可以统一理解为变量都在SRAM里,通过S-Bus访问。
4. 控制外设:操作"外设寄存器",而不是"内存"
-
具体过程 :你写代码:
GPIOA->BSRR = 0x0001; -
翻译 :这行代码被编译成一条写指令,目标地址是
0x40010810(BSRR寄存器在F103中的物理地址),要写入的值是0x0001。 -
总线矩阵识别 :CPU通过S-Bus发出写请求:"我要把
0x0001写到地址0x40010810去!" -
关键转折 :总线矩阵拿到地址
0x40010810一看,发现它在 0x40000000 ~ 0x40020000 这个范围内(这是F103的APB1/APB2外设地址映射区域)。 -
物理动作 :总线矩阵不会把数据发往SRAM控制器 ,而是瞬间启动对应的"地址译码器",并锁定目标:这是APB2总线上的GPIOA外设!
-
最终结果 :总线矩阵将这笔"写事务"翻译成APB2总线上的一次操作,直接把
0x0001这个值,写入了GPIOA模块内部的BSRR寄存器(一个由锁存器组成的物理存储单元)。 -
效果:这个存储单元的值变化,立刻驱动了GPIOA的驱动电路,PA0引脚的电平随之改变。
例子演示
局部变量
cpp
void func(void)
{
int a = 10; // 局部变量
int b = a + 5; // 局部变量
}
CPU ──DCode总线──> Flash(读常量10)
CPU ──System总线──> SRAM(把10写入变量a)
| 变量类型 | 存放在哪里 | CPU通过哪条总线访问 | 说明 |
|---|---|---|---|
局部变量 a、b |
SRAM(栈区) | System总线 | 函数被调用时,在栈上分配空间;读写变量时,CPU通过System总线访问SRAM |
局部变量 a 的初始值 10 |
这个值是常量 ,放在Flash里 | DCode总线 | 编译时,10被放入Flash的只读区。CPU执行int a = 10;时,先用DCode总线从Flash读出10,再通过System总线写入SRAM里的a |
全局变量 / 静态变量
cpp
int global_var = 100; // 已初始化的全局变量
static int static_var = 200; // 已初始化的静态变量
int uninit_var; // 未初始化的全局变量(默认0)
//上电过程
步骤1:CPU通过DCode总线从Flash读出"100"(初始值)
步骤2:CPU通过System总线把"100"写到SRAM中global_var的位置
步骤3:程序后续读写global_var时,都走System总线
| 变量类型 | 存放在哪里 | CPU通过哪条总线访问 | 说明 |
|---|---|---|---|
已初始化的全局/静态变量 (如global_var、static_var) |
SRAM(数据区) | System总线 (读写变量) DCode总线(读取初始值) | 编译时,初始值100、200被放在Flash里。上电后,启动代码通过DCode总线从Flash读出初始值,再通过System总线写入SRAM。之后程序读写变量时,都通过System总线访问SRAM |
未初始化的全局变量 (如uninit_var) |
SRAM(数据区) | System总线 | 启动代码会把它们所在的内存区域清零(也走System总线)。之后读写都走System总线 |
常量(const修饰的变量 / 字符串字面量)
cpp
const int const_var = 999; // 常量
char* str = "Hello"; // 字符串字面量(也是常量)
| 变量类型 | 存放在哪里 | CPU通过哪条总线访问 | 说明 |
|---|---|---|---|
| const修饰的变量 | Flash(只读区) | DCode总线 | 它不在SRAM里!CPU读取它时,走DCode总线直接访问Flash |
字符串字面量 "Hello" |
Flash(只读区) | DCode总线 | 同上。指针str本身是变量,存放在SRAM里,但它指向的内容在Flash里 |
指针变量
cpp
char* ptr = (char*)0x20000100; // 指针变量本身在SRAM里
*ptr = 0x55; // 通过指针往地址0x20000100写数据
| 变量类型 | 存放在哪里 | CPU通过哪条总线访问 | 说明 |
|---|---|---|---|
指针变量 ptr 本身 |
SRAM | System总线 | 它是个普通变量,存了一个地址值 |
指针指向的内容 *ptr |
取决于指针指向的地址 | 取决于地址落在哪个区域 | 如果地址是0x20000000(SRAM),走System总线;如果地址是0x40010800(外设),也走System总线(但外设会识别这个地址,不真的写入SRAM) |
关键: 指针不挑总线,地址指向哪里,就走哪条总线。
总线的理解
说明:上面所说的ICode,DCode,Sys,AHB,APB他们图上显示的是一根,其实这一根是含有三类线的,这三类线又有很多根。其中这三类线分别是地址总线、数据总线和控制总线。
-
数据总线:一次可以传输32位(4个字节)的数据。
-
地址总线 :有32根,这让CPU能访问
2^32 = 4GB的地址空间,这也是为什么STM32的存储器映射有4GB的范围。 -
控制总线 :在内部,这主要是一组控制信号线,负责协调读写、中断等操作。其核心信号线数量也与32位架构的设计配套。
System总线------内存------外设的联系
统一编址的理解
统一编址,又称存储器映射方式,是指把I/O端口(外设寄存器)与存储器(Flash、SRAM)在同一个地址空间内进行统一分配和管理,给每个单元分配唯一的地址。
具体是什么线理解一下下面的图,我们先看看MCU内部线的情况和读取内存数据的信息流。

再来看看操控外设的情况

因此从上篇文章电灯的过程中,访问GPIOF就是统一对应的地址访问的。
怎么访问,就来看看源码了。
源码------点亮LED4(上篇提到)
这里讲解原理
GPIOx和寄存器地址的本质
这是在HAL库点亮LED生成主函数的代码。
cpp
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 */
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_8,0);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
MX_GPIO_Init();
HAL_GPIO_WritePin(GPIOF,GPIO_PIN_8,0);
HAL_GPIO_Init();
这里重点讲解这两个函数。
MX_GPIO_Init()
HAL库的源码
cpp
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USER CODE BEGIN MX_GPIO_Init_1 */
/* USER CODE END MX_GPIO_Init_1 */
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, GPIO_PIN_SET);
/*Configure GPIO pin : PF8 */
GPIO_InitStruct.Pin = GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
/* USER CODE BEGIN MX_GPIO_Init_2 */
/* USER CODE END MX_GPIO_Init_2 */
}
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();这两行代码先不说明,这涉及到时钟的知识。
cpp
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitTypeDef是什么?就是一个结构体变量。
cpp
typedef struct
{
uint32_t Pin;
uint32_t Mode;
uint32_t Pull;
uint32_t Speed;
} GPIO_InitTypeDef;
-
本质:在栈上分配一个结构体,全部清零。
-
作用:准备一张"空配置单",等会儿往里填内容。

HAL_GPIO_Init
cpp
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init)
由于这里面的代码过多有些是冗余的,但是关键的代码不多,只看关键的。
cpp
while (((GPIO_Init->Pin) >> position) != 0x00u)
{
uint32_t position = 0x00u;
uint32_t ioposition;
uint32_t iocurrent;
uint32_t temp;
uint32_t config = 0x00u;
__IO uint32_t *configregister;
uint32_t registeroffset;
ioposition = (0x01uL << position);
iocurrent = (uint32_t)(GPIO_Init->Pin) & ioposition;
if (iocurrent == ioposition)
{
// ... 配置这个引脚
}
position++;
}
cpp
while (((GPIO_Init->Pin) >> position) != 0x00u)

>> position 是右移操作
每循环一次,position 加1(从0开始)。
| position | (GPIO_Init->Pin) >> position |
结果 |
|---|---|---|
| 0 | 0x0100 >> 0 = 0x0100 |
0000 0001 0000 0000 |
| 1 | 0x0100 >> 1 = 0x0080 |
0000 0000 1000 0000 |
| 2 | 0x0100 >> 2 = 0x0040 |
0000 0000 0100 0000 |
| 3 | 0x0100 >> 3 = 0x0020 |
0000 0000 0010 0000 |
| 4 | 0x0100 >> 4 = 0x0010 |
0000 0000 0001 0000 |
| 5 | 0x0100 >> 5 = 0x0008 |
0000 0000 0000 1000 |
| 6 | 0x0100 >> 6 = 0x0004 |
0000 0000 0000 0100 |
| 7 | 0x0100 >> 7 = 0x0002 |
0000 0000 0000 0010 |
| 8 | 0x0100 >> 8 = 0x0001 |
0000 0000 0000 0001 |
| 9 | 0x0100 >> 9 = 0x0000 |
0000 0000 0000 0000 |
找到要配置的针脚
cpp
ioposition = (0x01uL << position);
iocurrent = (uint32_t)(GPIO_Init->Pin) & ioposition;
ioposition 是"当前要检查的位的掩码"(只有一个1),iocurrent 是"这个掩码在 Pin 中是否也存在"(非0表示存在)。它们配合起来判断当前引脚需不需要配置。

这里只截取了部分的重点的代码,刚刚知道了要配置那个引脚,那么就要知道这个引脚所对应模式的配置了。
cpp
switch (GPIO_Init->Mode)
{
/* If we are configuring the pin in OUTPUT open-drain mode */
case GPIO_MODE_OUTPUT_OD:
/* Check the GPIO speed parameter */
assert_param(IS_GPIO_SPEED(GPIO_Init->Speed));
config = GPIO_Init->Speed + GPIO_CR_CNF_GP_OUTPUT_OD;
break;
}
configregister = (iocurrent < GPIO_PIN_8) ? &GPIOx->CRL : &GPIOx->CRH;
registeroffset = (iocurrent < GPIO_PIN_8) ? (position << 2u) : ((position - 8u) << 2u);
MODIFY_REG((*configregister), ((GPIO_CRL_MODE0 | GPIO_CRL_CNF0) << registeroffset), (config << registeroffset));

决定把配置写到哪个寄存器、写到哪个位置的核心

GPIO输入部分
有了输出的理解,输入部分轻松很多。



上拉输入 (Pull-up Input)
闭合上拉电阻开关,断开下拉电阻开关

下拉输入(Pull-down Input)
闭合下拉电阻开关,断开上拉电阻开关

浮空输入 (Floating Input)
断开下拉电阻开关,断开上拉电阻开关
开关在 MCU内部,配置的时候可以看到。
引脚输入高电平,GPIO接收到高电平,输入数据寄存器就读入1。
引脚输入低电平,GPIO接收到低电平,输入数据寄存器就读入0。
针脚悬空,干扰信号经过触发器后就会变成随机的高低电平,因此浮空输入模式下
电平高、低是不确定的
模拟输入(Analog Input)
直接理解:引脚外部是什么,模拟输入就获取什么
