Robotics - Embedding 2 探索 STM32G4
什么是 STM32G4 ?
(子问题: 什么是MCU)
子问题:什么是 MCU?什么是引脚?什么是 GPIO、PWM 和 ADC?Rust HAL 中的
GPIOA、PA5和split()又代表什么?
目标: 每天加几个内容到库里
STM32G4 是意法半导体 STM32 系列中的一类 MCU(微控制器),全称通常指 STM32G4 系列,不是某一块具体开发板。
特别擅长“实时控制 + 电机控制 + 模拟信号处理”的 STM32。
一颗 STM32G4 通常集成:
- Arm Cortex-M4 CPU:部分型号最高约 170 MHz
- Flash 和 SRAM:存放程序及运行数据
- GPIO:控制 LED、继电器、使能信号等
- PWM 定时器:控制电机、电源和舵机
- ADC:读取电流、电压、温度和传感器模拟信号
- DAC、运算放大器、比较器:处理模拟信号
- UART、SPI、I²C、CAN/FDCAN、USB:与传感器、电机驱动器、电脑通信
- DSP/FPU:快速进行滤波、PID、坐标变换和浮点计算
能理解的东西: 生活中日常实体或者隐喻, 比如动物, 盒子
可以先把它想成一只装在芯片里的“小型控制动物”:
- CPU 是它的大脑;
- Flash 是它记住的程序;
- SRAM 是它思考时使用的短期记忆;
- GPIO 是它最简单的触觉和动作接口;
- ADC 是它测量外部电压的感觉器官;
- PWM 是它有节奏地控制外部设备的方式;
- UART、SPI、I²C、CAN 和 USB 是它与其他设备交流的通信系统。
<用stm32-rs/stm32g4xx-hal> 来分辨好定义
引脚 (pin)
- 物理实体 - Pin 就是芯片外面的金属脚、金属触点
- rust HAL里面的一个类型
引脚(pin)就是芯片用来连接外部电路的金属端子。
pub struct Pin<
const P: char,
const N: u8,
MODE = DefaultMode,
{
_mode: PhantomData<MODE>,
}
引脚(pin)就是芯片用来连接外部电路的金属端子。
芯片内部的电路被塑料/陶瓷封装包住,需要通过引脚才能与 PCB、传感器、电源等连接。
芯片内部电路
→ 芯片内部焊盘
→ 很细的连接线
→ 芯片外部引脚
→ PCB铜线
→ 外部设备
引脚和电线的区别
- 引脚:一个设备的金属连接端点
- 电线/PCB 铜线:把不同设备的引脚连接起来
就像:
插座孔 = 引脚/接口
电源线 = 电线
引脚本身一般只是一块金属,它之所以能输入、输出,是因为后面连接了芯片内部的 GPIO 电路。
并非所有引脚都是 GPIO
STM32 的引脚可能有不同用途:
| 引脚类型 | 用途 |
|---|---|
| GPIO | 输入或输出数字电平 |
| VDD | 给芯片提供正电源 |
| GND/VSS | 接地 |
| RESET | 复位芯片 |
| OSC | 连接外部晶振 |
| ADC | 接收模拟电压 |
| USB | USB 通信 |
| 调试引脚 | 烧录和调试程序 |
因此:
GPIO 是引脚背后的功能之一,引脚是把这个功能连接到外部的物理端子。
PA5 是什么?
PA5 不是芯片外壳上的第 5 根金属腿,而是逻辑名称:
P = Port,引脚端口
A = A组
5 = A组中的第5号
它对应芯片内部 GPIOA 控制器里的第 5 路 GPIO 电路。
但它在芯片外壳上的实际位置取决于封装:
逻辑名称:PA5
封装编号:可能是第21脚,也可能是其他位置
开发板接口:又可能标成 D13 等名称
需要通过具体芯片的 datasheet 和开发板原理图确认。
完整关系是:
GPIOA内部电路的第5路
→ PA5芯片引脚
→ PCB铜线
→ 开发板排针
→ 杜邦线
→ LED或传感器
所以最准确的一句话是:
引脚是芯片与外界电路连接的物理金属端点;PA5 是其中某个可由 GPIOA 控制的引脚的逻辑名字。
Port
Port A
├── PA0 → 一个金属 pin
├── PA1 → 一个金属 pin
├── PA2 → 一个金属 pin
└── PA5 → 一个金属 pin
GPIO
| 说法 | 实际含义 |
|---|---|
| GPIO 电路 | MCU 内部控制电压的硬件电路 |
| GPIO 功能 | 读取或输出数字电平的能力 |
| GPIO 引脚 | 连接这套内部电路的芯片金属引脚 |
- 电线 = 水管
- GPIO 引脚 = 水管接口
- GPIO 输出电路 = 可以开关、推动水流的水泵
- GPIO 输入电路 = 检测有没有水压的传感器
- 固件 = 决定何时开泵、何时检测的控制员
GPIO = General-Purpose Input/Output,通用输入输出引脚。
它是 MCU 用来和外部电路交换最简单数字信号的“电线接口”。
每个 GPIO 通常可以配置成两种基本模式:
- 输入 Input:MCU 读取外部是高电平还是低电平
- 输出 Output:MCU 主动输出高电平或低电平
同一根 GPIO 引脚可以通过程序配置成:
- 输入模式:读取外部电压,得到
0或1 - 输出模式:主动产生低电平或高电平
例如一根叫 PA5 的引脚:
PA5 配置为输入:读取按钮是否按下
PA5 配置为输出:控制 LED 是否点亮
能同时输入和输出吗?
一般来说,一根引脚在某一时刻主要选择一种模式:
现在作为输入 → 听
之后切换成输出 → 说
可以在程序运行时快速切换,但不能随意让外部设备和 MCU 同时向同一根线输出,否则可能发生冲突:
MCU 想输出 3.3V
外部设备想输出 0V
→ 类似电源正负直接打架,可能损坏电路
不过,一个 MCU 有很多 GPIO,所以通常是:
PA0:输入,读取按钮
PA1:输入,读取传感器
PA5:输出,控制 LED
PA6:输出,控制电机驱动器
因此 MCU 整体当然可以同时输入和输出,只是使用不同的引脚。
某些协议会让同一根线双向通信,例如 I²C,但它使用了特殊的“开漏输出 + 上拉电阻”,大家遵守规则,避免两个设备在电线上打架。
(不好理解, 高低电平是语言吗, mcu是一种会说0, 1 的动物?)
GOIPA
对,我现在发现了。你前面连续问“GPIO 是电路还是电线”“PA5 是不是类型”,其实说明我跳过了中间最关键的 GPIOA。
最简单地说:
GPIOA 是一组 GPIO 引脚的控制器,PA5 是这一组里的第 5 号引脚。
STM32 把大量 GPIO 分组管理,每组最多约 16 个:
GPIOA 这一组
├── PA0
├── PA1
├── PA2
├── ...
├── PA5
└── PA15
GPIOB 这一组
├── PB0
├── PB1
└── ...
命名规则是:
PA5
││└── 第5号
│└── A组
└── Pin(引脚)
- Pin:一只金属脚
- Port:一组可以一起管理的金属脚
- PA5:Port A 这组里的第 5 个 pin
- 线路:从金属 pin 焊出去的 PCB 铜线
GPIOA 到底是硬件还是软件?
首先它是 MCU 内部的真实硬件模块,负责控制 A 组引脚:
GPIOA控制器
├── 输入状态记录
├── 输出状态记录
├── 模式配置
└── PA0~PA15的控制电路
可以把它想象成一个有 16 路的控制柜:
GPIOA = A号控制柜
PA5 = A号控制柜的第5路
控制柜内部有一些寄存器:
MODER:每个引脚是输入还是输出
IDR:读取引脚电平
ODR:记录输出电平
BSRR:把某个引脚设为高或低
例如对 GPIOA 的第 5 位进行操作,就是控制 PA5。
Rust 代码中的 GPIOA
dp.GPIOA
它是代表整个 GPIOA 硬件模块的软件句柄。
然后:
let gpioa = dp.GPIOA.split(&mut rcc);
HAL 把整个 A 组拆成多个独立的软件对象:
gpioa.pa0
gpioa.pa1
gpioa.pa2
gpioa.pa5
所以:
| 名称 | 意思 |
|---|---|
GPIO |
通用输入输出这种硬件功能 |
GPIOA |
管理 A 组 GPIO 的硬件模块 |
PA5 |
A 组第 5 个具体引脚 |
dp.GPIOA |
Rust 中整个 GPIOA 的软件句柄 |
gpioa.pa5 |
Rust 中代表 PA5 的具体值 |
完整关系就是:
STM32
→ GPIOA控制模块
→ A组第5路控制电路
→ PA5物理引脚
→ PCB铜线
→ 外部LED或传感器
所以此前的代码:
gpioa.pa5.into_push_pull_output()
真正的意思是:
**可以说 split() 完成了 GPIOA 的基础初始化,但它不只是初始化。**它同时做了三件事:
let gpioa = dp.GPIOA.split(&mut rcc);
1. 开启 GPIOA 的时钟
STM32 为了省电,GPIOA 上电后可能没有时钟。split() 内部先执行类似:
GPIOA::enable(rcc);
没有时钟,GPIOA 的寄存器和控制电路就不能正常工作。
这里的:
&mut rcc
是对 RCC(Reset and Clock Control,复位与时钟控制器)的可变借用。
2. 复位 GPIOA
它接着执行类似:
GPIOA::reset(rcc);
让 GPIOA 回到已知的初始状态。
3. 拆出每个引脚的句柄
它最后返回:
Parts {
pa0: PA0::new(),
pa1: PA1::new(),
// ...
pa5: PA5::new(),
}
因此:
let gpioa = ...
得到的 gpioa 大致是:
gpioa.pa0
gpioa.pa1
gpioa.pa5
// ...
真实源码的结构可以在 gpio.rs 中看到。
为什么叫 split?
因为它把对整个 GPIOA 的控制权:
dp.GPIOA
拆成了 16 个相对独立的引脚句柄:
dp.GPIOA
↓ split
gpioa.pa0
gpioa.pa1
gpioa.pa2
...
gpioa.pa15
而且 split(self, ...) 会取得 dp.GPIOA 的所有权,所以不能重复拆分:
let gpioa1 = dp.GPIOA.split(&mut rcc);
let gpioa2 = dp.GPIOA.split(&mut rcc);
// 编译错误:dp.GPIOA 已经被消耗
这样可以防止程序中出现两套对象同时声称自己控制同一个 GPIOA。
但这时 PA5 还不是输出
split() 只完成 GPIOA 整体的启用、复位和句柄拆分。这个 HAL 中普通引脚默认是 Analog:
gpioa.pa5 // 类型大致是 PA5<Analog>
还需要单独配置:
let mut led = gpioa.pa5.into_push_pull_output();
现在才变成:
Pin<'A', 5, Output<PushPull>>
随后才能:
led.set_high();
所以完整过程是:
split()
→ 启用并复位整个 GPIOA
→ 取得 PA0~PA15 的软件句柄
into_push_pull_output()
→ 配置具体 PA5 的硬件模式
set_high()
→ 让 PA5 输出高电平
因此最准确的说法是:
split() 是“GPIOA 基础初始化 + 将整个外设拆成独立引脚资源”,而不是完成每个引脚的具体功能配置。