【stm32】GPIO(下篇)——统一编址、源码理解和输入模式

目录

MCU内部的总线和统一编址

总线矩阵

总线矩阵的特性

不同的线的区别和理解

stm32程序执行过程

第一阶段:编译 (Compilation)

[1. 编译 + 链接,生成 .hex 或 .bin 文件](#1. 编译 + 链接,生成 .hex 或 .bin 文件)

第二阶段:烧录 (Programming)

第三阶段:运行 (Execution)

[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修饰的变量 / 字符串字面量))

指针变量

总线的理解

System总线------内存------外设的联系

统一编址的理解

源码------点亮LED4(上篇提到)

GPIOx和寄存器地址的本质

MX_GPIO_Init()

HAL_GPIO_Init

GPIO输入部分

上拉输入 (Pull-up Input)

[下拉输入(Pull-down Input)](#下拉输入(Pull-down Input))

浮空输入 (Floating 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通过哪条总线访问 说明
局部变量 ab 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_varstatic_var SRAM(数据区) System总线 (读写变量) DCode总线(读取初始值) 编译时,初始值100200被放在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)

直接理解:引脚外部是什么,模拟输入就获取什么

相关推荐
youyou-06061 小时前
AUTOSAR‑COM 常见问题 QA (1)
java·linux·服务器·单片机·汽车
云边有个稻草人3 小时前
Tera Term 鸿蒙 PC 适配全记录:用 ArkUI 与 Native C++ 重建多协议终端工作流
c++·stm32·harmonyos
wuyk5553 小时前
从零吃透 MQTT 通信|第 10 章 阿里云 / 腾讯云 MQTT 设备完整对接实战,三元组、签名、设备上云调试
c语言·开发语言·stm32·学习·阿里云·云计算·腾讯云
白搞电子3 小时前
ESP-Mosaico 桌宠为什么会误睡?AS312 PIR 原理、休眠逻辑与排错方法
单片机·嵌入式硬件·动画
youcans_4 小时前
【嵌入式软件AI编程】08. 第一个STM32工程
stm32·单片机·ai编程·嵌入式软件·claude code
zhangzhangkeji5 小时前
stm32 ,3-2:led 闪烁、流水灯、蜂鸣器
stm32·arm
崇山峻岭之间5 小时前
HC32F460PETB的PH2的TIM4_2_CLK
单片机
Brilliantwxx5 小时前
【STM32】 I2C协议和设备主从( 图解 + 面试题 )
stm32·单片机·嵌入式硬件·架构
威嵌神州5 小时前
星载计算机产品方案:商业卫星国产化平台
嵌入式硬件