STM32 嵌入式学习笔记(一):点个灯泡的疑问汇总
STM32 嵌入式学习笔记(一):点个灯泡的疑问汇总
这是我在看江协科技的stm32入门教程的学习过程记录,是学习过程中我产生的问题和deepseek给我的回答再让deepseek总结后的内容。大致完整可能缺少一些细节。主要用作个人回头复习,若能真够帮到可能的读者就更好了。这里面没有具体步骤。
一、存储体系:RAM、ROM 与寄存器
1.1 RAM 是什么?
全称:Random Access Memory,随机存取存储器。
核心特征:
- 掉电丢失(易失性)
- 读写速度极快(纳秒级)
- 可随机访问任意地址
在 STM32 里的身份:内存。你写 int a = 5;,这个 a 在通电运行时的物理存放位置就是 RAM。
编译输出的对应关系:
| 编译字段 | 存放内容 | 物理位置 |
|---|---|---|
| Code | 函数的机器码 | Flash |
| RO-data | const 常量 |
Flash |
| RW-data | 已初始化全局变量(如 int b=5;) |
先存 Flash 备份,运行时复制到 RAM |
| ZI-data | 未初始化全局变量(如 int c;) |
RAM(上电清零,不占 Flash) |
1.2 ROM 是什么?
全称:Read-Only Memory,只读存储器。
核心特征:
- 掉电保留(非易失性)
- 在常规运行中不易改写或不能改写
在 STM32 里的身份:Flash(闪存)。
关键区分:传统 ROM 是“出厂固化只读”。但在 STM32 里,ROM 就是 Flash——可以通过烧录器擦除重写。之所以还叫“ROM”,是因为程序固化后,CPU 主要去读它来执行指令。
1.3 Flash 为什么叫“闪存”?与硬盘有什么区别?
为什么叫“闪”:发明者发现它擦除时能瞬间整块清空,像闪光灯一样“唰”一下。命名来源是东芝同事觉得这个动作很像相机的 Flash。
与硬盘的区别:
| 对比维度 | STM32 里的 Flash | 电脑机械硬盘 | 电脑固态硬盘 |
|---|---|---|---|
| 物理形态 | 芯片内部硅片 | 磁头+旋转磁盘 | 独立 Flash 颗粒+主控 |
| 能否就地执行代码 | 能(XIP) | 不能,必须读到 RAM | 不能,必须读到 RAM |
| 容量 | 几十KB–2MB | 1TB–20TB | 128GB–4TB |
| 读写单位 | 字节/字 | 扇区(512字节) | 页/块 |
核心区别:STM32 的 Flash 支持 XIP(eXecute In Place,就地执行)。CPU 的程序计数器直接指向 Flash 的物理地址(0x08000000)取指令执行,不需要先把程序搬到 RAM。
1.4 为什么已经有了能写的 Flash,还要区分 RAM 和 ROM?
因为物理定律的限制:
| 对比维度 | SRAM(单片机里的 RAM) | Flash(单片机里的 ROM) |
|---|---|---|
| 构成原理 | 6 个晶体管组成锁存器 | 1 个带浮栅的特殊晶体管 |
| 写入机制 | 纯电路翻转,瞬间完成 | 隧道效应,需要高压轰击绝缘层 |
| 写入耗时 | 纳秒级 | 毫秒级(慢几百万倍) |
| 写入寿命 | 无限次 | 1万–10万次 |
| 成本/容量比 | 极高(1KB 占很大面积) | 极低(1MB 很便宜) |
结论:如果把 Flash 当 RAM 用,每次 i++ 都要等毫秒级擦除,跑几万次芯片就物理磨损报废。所以必须分工:程序睡在 Flash,运行时变量在 RAM 里快速翻腾。活干完了,关键数据再慢慢写回 Flash。
这个设计的专业术语:缓存-持久化模型,嵌入式里叫 Shadow RAM(影子内存)。
1.5 寄存器到底是什么?
寄存器不是 Flash,也不是 RAM。寄存器是一排被映射到地址空间的微型闸刀开关。
它的物理本质是 D 触发器——用两个非门首尾相接形成正反馈环,一个电平一旦进去就会永远在环里转圈,从而被“锁存”住。
| 对比项 | Flash / RAM(存储器) | 寄存器(D 触发器) |
|---|---|---|
| 角色比喻 | 仓库货架 | 控制台闸刀开关面板 |
| 核心目的 | 存数据 | 控制电路 |
| 写入行为 | “把货放到第3排第5列” | “把第3号闸刀推上去” |
寄存器如何控制硬件:不是“触发”,而是持续施加影响。寄存器对应位为 1,电平立刻持续输出,硬件电路根据这个电平决定工作状态。你改成 0,状态立刻改变。
1.6 电脑里有寄存器吗?
有。
第一类:CPU 内核寄存器(EAX、EBX、R0、R1 等)。仅在 CPU 内部,用于运算,对外不可见。
第二类:外设配置寄存器。和 STM32 一模一样,控制 USB、SATA、网卡等外设。只是这些操作被封装在操作系统内核驱动里,应用程序接触不到。
为什么电脑的驱动看起来像软件:因为 Windows/Linux 有内核分层。应用程序要操作硬件必须通过系统调用,由内核验证权限后让驱动去写寄存器。这层层封装让你感觉像是在调用软件服务,而不是在扳闸刀。STM32 裸机上没有这层管家,你就是上帝,直接写物理地址。
1.7 从软件地址到硬件电平的完整链路
问题:你写 *((volatile uint32_t *)0x4001080C) = 1;,这个“地址”究竟靠什么找到物理寄存器?
答案:地址译码器。
完整流程:
- 编译:你的 C 代码变成一条
STR指令,地址和数据分别加载到寄存器。 - CPU 执行:把地址放到地址总线,把数据放到数据总线,发出“写”控制信号。
- 地址译码器:一堆组合逻辑电路监视地址总线。当地址落在
0x40010800–0x40010FFF范围时,输出片选信号选中 GPIOA 外设;再根据偏移选中具体寄存器(如 ODR)。 - 数据写入:数据总线上的 32 位值直接拍进对应寄存器的 32 个 D 触发器。
- 电平改变:触发器的输出端物理电平改变,通过输出驱动器送到引脚焊盘。
整个流程从软件指令到物理电平,只需一个总线时钟周期(约几十纳秒)。
1.8 RAM、Flash、寄存器的地址形式
形式完全一样——都是 32 位二进制数。 Cortex-M3 把 4GB 寻址空间画成一张地图:
| 地址范围 | 区域 | 用途 |
|---|---|---|
0x00000000–0x1FFFFFFF |
Code 区 | Flash 在这里 |
0x20000000–0x3FFFFFFF |
SRAM 区 | RAM 在这里 |
0x40000000–0x5FFFFFFF |
外设区 | 寄存器在这里 |
0xE0000000–0xFFFFFFFF |
系统区 | NVIC、SysTick 等 |
传递渠道:CPU 访问这三者用同一套指令(LDR/STR),走同一套总线(地址总线+数据总线)。区别只在于地址落在哪个区间,地址译码器根据高位自动选通对应设备。
二、时钟系统:芯片的心跳
2.1 为什么点灯要先“开时钟”?
STM32 把所有外设的时钟都默认关着——为了省电。每个外设门口有一个总闸(RCC->APB2ENR 的对应位)。你写 RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; 就是推上 GPIOC 的时钟总闸。闸一开,系统时钟涌入 GPIOC 内部,所有寄存器被激活。闸一关,外设内部时钟被封锁在门外,电路纹丝不动,零功耗。这称为时钟门控。
2.2 时钟树:时钟是怎么分到各外设的
外部晶振 (8MHz)
│
HSE 输入
│
┌─ PLL ──┐ ← 树干分叉,倍频到 72MHz
│ │
▼ ▼
SYSCLK 其他时钟源
│
├─ AHB 总线 ──┐ ← 分到各大分支
│ │
HCLK(CPU核) HCLK(DMA等)
│
▼
APB1 / APB2 ← 再分到外设总线
│ │
TIM2, GPIOA,
USART2 USART1
│
▼
RCC->APB2ENR 的每一位 ← 每根树枝末端都有独立总闸
这棵树是芯片出厂时在硅片上铺设好的。你能做的就是控制每个末端的开关。
2.3 系统时钟本身的功耗能优化吗?
能,有三个维度:
| 优化维度 | 具体手段 | 省电原理 |
|---|---|---|
| 降频 | 切换系统时钟源,降低 HCLK | 频率越低,动态功耗线性下降 |
| 关核 | 执行 WFI/WFE 让 CPU 内核休眠 |
CPU 自身时钟被门控切断,外设时钟仍在 |
| 换源 | 从 PLL 切换到 HSI 或 HSE 直接分频 | PLL 是模拟电路,功耗远高于晶振直出 |
三种低功耗模式:
| 模式 | 关了什么 | 唤醒方式 | 功耗 |
|---|---|---|---|
| 睡眠 | 只关 CPU 内核时钟 | 任意中断或事件 | CPU 部分省电 |
| 停机 | 系统时钟树大面积停摆 | 外部中断或 RTC | 微安级 |
| 待机 | 几乎全关,只保留备份域 | WKUP 引脚或复位 | 最低,唤醒等同复位 |
三、GPIO 引脚:从丝印到寄存器
3.1 从 PC13 找到该操作的寄存器和位
命名规则:P = Port(端口),C = 端口编号,13 = 引脚号。
翻译步骤:
- 外设:
PC13→GPIOC。基地址0x40011000。 - 时钟使能:
RCC->APB2ENR里的IOPCEN位。时钟控制的最小单位是整个端口,不能单开一个引脚。 - 配置寄存器:引脚 13 在 8–15 范围,归
CRH管,占第 23–20 位。 - 数据寄存器:
ODR的第 13 位直接对应该引脚的高低电平。
为什么配置寄存器只分 CRL 和 CRH:一个端口 16 个引脚,每个引脚需要 4 个配置位。一个 32 位寄存器只能装 8 个引脚(32÷4=8)。所以 CRL 管 0–7,CRH 管 8–15。
4 位配置位拆解:
- 低 2 位:MODE(输入/输出,输出时决定速度)
- 高 2 位:CNF(具体电路模式)
为什么 PC 口只引出 PC13、PC14、PC15:STM32F103C8T6 只有 48 个引脚,不可能把四组 64 个引脚全引出。C 口只引了这三个,因为它们属于备份域,由 VBAT 独立供电。
3.2 为什么要区分这么多 GPIO 模式?
输入模式(4 种)——只管读电平,引脚呈高阻态:
| 模式 | 电路状态 | 应用场景 |
|---|---|---|
| 模拟输入 | 切断数字电路,直连 ADC | 采集电压、麦克风信号 |
| 浮空输入 | 引脚悬空,由外部完全决定 | 读取数字传感器 |
| 上拉输入 | 内部接约 40kΩ 电阻到 3.3V,默认读 1 | 按键(省外部电阻) |
| 下拉输入 | 内部接约 40kΩ 电阻到 GND,默认读 0 | 需要默认低电平的开关 |
输出模式(4 种)——驱动外部负载:
| 模式 | 电路特征 | 应用场景 |
|---|---|---|
| 推挽输出 | 上管推高、下管拉低。驱动能力强 | 点亮 LED、给外设供电 |
| 开漏输出 | 只有下管能拉低。要输出高必须外接上拉电阻 | I2C 总线、电平转换 |
| 复用推挽输出 | 推挽电路,由内部外设(定时器、串口)自动控制 | USART_TX、PWM 输出 |
| 复用开漏输出 | 开漏电路,由内部外设自动控制 | I2C 的 SDA/SCL |
为什么需要上拉/下拉:浮空引脚什么都不接时,电平是飘的,受电磁干扰乱跳。上下拉电阻给引脚一个确定的默认状态。
为什么 I2C 必须用开漏:多设备共用一根数据线。如果都用推挽,一个设备拉高一个拉低,直接短路烧坏。开漏电路线只拉低不推高,高电平靠公用的上拉电阻提供,天然防冲突。
推挽和开漏的点灯接法:
- LED 另一端接地 → 必须推挽输出(需要主动输出电流)
- LED 另一端接 3.3V → 推挽和开漏都能亮(只要能把低电平拉下来灌电流即可)
3.3 外设复用引脚的两个时钟使能
复用功能引脚需要两个时钟使能:
- GPIO 端口的时钟:让引脚电路通电
- 外设本身的时钟:让外设内部逻辑跑起来
配置要点:
- 输出复用(TIM1 的 PWM、USART_TX):
CNF配成复用推挽输出 - 输入复用(USART_RX、SPI_MISO):配成浮空输入或上拉输入
- 控制权交给外设后,
ODR不再由你的代码直接控制
四、通信总线:USART、I2C、SPI
4.1 总览
这三者都是串行通信(数据一位一位走单线),但机制完全不同:
| 对比维度 | USART | I2C | SPI |
|---|---|---|---|
| 线数 | 2 根(TX/RX) | 2 根(SDA/SCL) | 4 根(SCK/MOSI/MISO/CS) |
| 时钟线 | 无(靠波特率硬猜) | 有(SCL) | 有(SCK) |
| 设备选择 | 点对点(天然) | 软件寻址(发地址) | 硬件片选(拉低 CS) |
| 速度 | 慢(<2Mbps) | 较慢(100k–3.4Mbps) | 快(可达 50Mbps) |
| 全双工 | 是(同时收发) | 否(半双工) | 是 |
| 比喻 | 打电话 | 老师点名 | 流水线拉电闸 |
4.2 为什么 UART 不能像 I2C 那样多条并联?
电气层面:UART 的 TX 是推挽输出,两个 TX 并在一起一个拉高一个拉低会短路。而 I2C 的 SDA 是开漏输出,所有设备只拉低不推高,高电平靠公用上拉电阻提供,天然防冲突。
协议层面:UART 没有地址机制。多个从机同时回数据,主机不知道谁在说。
STM32 推荐做法:挂多个 UART 设备时,用硬件串口数量解决问题(芯片有 3–5 个 USART),而不是并联。
唯一例外:加 RS485 收发芯片后可以实现一主多从。
4.3 I2C 怎么挂多个设备?
同一总线上所有设备的 SDA 接在一起,SCL 接在一起。主机发起通信时先发地址。所有从机都收到,但只有地址匹配的那个才响应,其他保持沉默。
物理上怎么连:用面包板的电源轨(横向连通的长条孔),单片机的 I2C 引脚接过去,再从电源轨分出杜邦线接到各从设备。PCB 上就是铜线走线分叉。
4.4 SPI 怎么挂多个设备?
SCK、MOSI、MISO 三根线所有设备共用。每个从机单独一根 NSS(片选线)连到主机的不同 GPIO。主机拉低谁的 NSS 就和谁通信。未被选中的从机其 MISO 呈高阻态,不干扰总线。
4.5 引脚缩写全称
I2C:
- SDA:Serial Data Line
- SCL:Serial Clock Line
SPI:
- SCK:Serial Clock
- MOSI:Master Output, Slave Input
- MISO:Master Input, Slave Output
- NSS(CS):Negative Slave Select,低电平有效的从机选择
4.6 同步与异步
- 异步通信:没有独立的时钟线。双方约定好波特率,接收方靠起始位的下降沿启动内部定时器,按固定节奏在比特中心点采样。
- 同步通信:有一根专门的时钟线(SCL/SCK)传递节拍信号。
为什么 USART 有 S(同步)却总当 UART 用:同步模式需要额外占用 CLK 引脚,且电脑、GPS、蓝牙等外设本身不带时钟线。所以绝大多数场景把 USART 配置为异步模式,只用 TX/RX。
五、中断:CPU 的“紧急响应机制”
5.1 中断是什么?
中断就是让 CPU 能响应外部事件的硬件机制。
没有中断时,CPU 必须不停轮询(反复问“按键按了吗?”),效率极低。有了中断,CPU 可以忙自己的事甚至休眠,外设事件发生硬件自动通知,CPU 暂停当前工作转去处理,处理完返回,仿佛什么都没发生。
5.2 中断全流程
- 中断请求:外设通过内部连线向 NVIC 发信号。
- 优先级仲裁:NVIC 按优先级排队,选出最高的发给 CPU。
- 硬件压栈:CPU 自动把 xPSR, PC, LR, R12, R3–R0 共 8 个寄存器压入栈。
- 查向量表跳转:硬件根据中断号,从向量表取出对应中断服务函数的入口地址,装入 PC。
- 执行中断服务函数:你写在
stm32f10x_it.c里的代码。 - 硬件出栈返回:函数末尾执行
BX LR,硬件自动把 8 个寄存器值弹回,PC 恢复原位。
你做的工作只是往空壳函数里填业务逻辑,剩下全是硬件自动完成。
5.3 堆栈和中断
为什么中断必须用栈:只存一个“状态”不够,必须把当前运行中用到的寄存器全部原封不动保存(PC、状态寄存器、中间结果等)。Cortex-M3 硬件自动完成 8 个寄存器的压栈。
栈存在哪里:RAM。启动文件第一行 Stack_Size EQU 0x00000400 就是给它划空间。不能放 Flash,因为 Flash 写入太慢。
5.4 中断向量表
本质:一张“跳转地址表”,是 Flash 最开头的一个地址数组。物理位置在 0x08000000。
内容:
- 第一项:栈顶地址(SP 初始值)
- 第二项:复位中断入口(
Reset_Handler) - 第三项:NMI 中断入口
- ……后面是几十个外设中断入口
工作原理:发生中断时,硬件根据中断编号自动计算偏移 中断号 × 4,从向量表取出地址装入 PC。这是纯硬件电路写死的逻辑,没有软件参与。
向量表和普通函数调用的类比:普通函数调用是在编译时把函数地址填进 BL 指令;中断响应是运行时由硬件触发查表跳转。本质都是“取一个地址,跳过去执行”。
5.5 复位中断
复位中断(Reset_Handler)是启动文件里的第一个中断服务函数。特殊之处在于:芯片刚刚上电,所有寄存器不确定,根本没有“现场”需要保存。它的工作是:
- 把 RW-data 从 Flash 搬到 RAM
- 清零 ZI-data 段
- 调用
SystemInit()(配置时钟) - 调用
main()
把它算进中断体系是为了利用统一的中断向量表机制,避免额外设计一套“启动入口”。
5.6 中断服务函数的规范
- 不能有返回值
- 不能有参数
- 必须短小精悍(不宜做耗时操作)
- 函数名必须与启动文件里向量表定义的符号完全一致
5.7 主函数和中断的协作模式
“中断是速记员,主函数是业务后厨。”
- 中断里:只做最少的事(收字节放缓冲区、把标志位置 1、清中断标志),立刻退出。
- 主循环里:轮询标志位和缓冲区,做耗时处理(解析命令、计算、更新屏幕)。
为什么主函数必须有死循环:单片机裸机没有操作系统。main() 一旦返回,CPU 程序计数器不知道该指向哪里,会执行到非法区域跑飞死机。while(1) 是硬件必须的兜底机制,不是偷懒。
六、启动文件与编译系统
6.1 启动文件(startup_stm32f10x_md.s)
后缀 .s:汇编语言源文件。必须用汇编写,因为要操作寄存器、设栈指针、建向量表,这些 C 语言不能直接表达。
启动文件的完整功能清单:
| 序号 | 功能 | 具体做什么 | 为什么必须由它做 |
|---|---|---|---|
| ① | 分配栈空间 | Stack_Size EQU 0x00000400 |
C 语言运行前提 |
| ② | 分配堆空间 | Heap_Size EQU 0x00000200 |
malloc 需要的内存池 |
| ③ | 构建中断向量表 | 按 Cortex-M3 规范排列所有中断入口地址 | CPU 硬件强制从 0x08000000 读取 |
| ④ | 实现 Reset_Handler | 搬数据、清零、调 SystemInit()、调 main() |
搭建 C 语言运行环境 |
| ⑤ | 声明默认中断函数(弱定义) | [WEAK] 关键字定义空占位符 |
防止意外中断死机,允许用户重写覆盖 |
本质:从硬件复位到 main() 被调用之间,唯一在跑的代码。没有它,STM32 就是一坨硅片。
6.2 .h 与 .c 的编译链接模型
.h 文件:说明书(函数声明)。告诉编译器有哪些函数可用,长什么样。不参与编译,不进入最终固件。
.c 文件:车间(函数实体)。存放真正的代码逻辑,需要被编译成 .o 文件。
分开的原因:方便多人协作和代码复用。你只需给别人说明书(.h)和编译好的库(.lib),他就能调用你的函数。
编译链接全流程:
- 预处理:
#include把.h内容原封不动复制粘贴进.c。 - 编译:编译器看到函数声明,做类型检查,生成一条带空白地址的
BL指令。 - 链接:链接器在所有
.o文件里按函数名字匹配,找到实体地址,填进空白。
.h 文件在链接阶段完全不参与,已经丢弃。
普通函数调用 vs 中断响应:
| 对比项 | 库函数调用 | 中断响应 |
|---|---|---|
| 触发方式 | 代码主动写调用语句 | 硬件信号被动触发 |
| 地址填入时机 | 链接时 | 链接时(固化在向量表) |
| 运行时行为 | 直接跳转 | 硬件压栈 → 查表 → 跳转 |
6.3 Keil 工程管理:物理文件夹 vs 逻辑分组
物理文件夹:文件在硬盘的真实位置,编译器不关心。
Keil 工程分组:告诉编译器“这个项目包含哪些源文件”,不在分组里的文件不会被编译。
添加 .c 进分组:告诉 Keil“这个文件要编译”。
配置 Include Paths:告诉编译器“去这些文件夹里找头文件”。这是 Keil 使用的强制标准操作,不是可选项。因为库文件在独立的文件夹(如 Library/inc),和 main.c 不在同一目录。
如果不声明路径会怎样:#include "stm32f10x.h" 时编译器先在 main.c 所在文件夹找,找不到就报错 cannot open source input file。
6.4 USE_STDPERIPH_DRIVER 宏
作用:条件编译的总开关。
库头文件 stm32f10x.h 里有:
#ifdef USE_STDPERIPH_DRIVER
#include "stm32f10x_conf.h"
#endif
这个暗号是库作者预先写死的。只有定义了这个特定字符串,锁才打开,所有外设驱动头文件才被包含。
在魔术棒 Define 框里填一次,等于告诉编译器:“编译本工程所有 .c 文件时,都自动定义好 USE_STDPERIPH_DRIVER。” 这样每个文件都单独满足条件编译的条件,步调一致。
能否用代码实现:可以在每个源文件最开头写 #define USE_STDPERIPH_DRIVER,但必须写在 #include "stm32f10x.h" 之前,且每个文件都得写一遍。Define 框省去了这个麻烦。
6.5 stm32f10x_conf.h 的作用
这是一个“总道具清单”。它内部用 #include 包含了所有外设驱动的头文件,默认全部注释掉。你需要哪个外设,就把哪行的注释去掉。本质上,它就是一条条普通的 #include 语句,只是起集中配置文件的作用。
最终决定固件体积的:不是你引入了多少头文件,而是你实际调用了哪些函数。链接器按需提取,没调用的函数不会被塞进最终二进制文件。
6.6 Build、Rebuild、Translate
| 操作 | 做什么 | 什么时候用 |
|---|---|---|
| Translate | 只编译当前激活的单个 .c 文件,不链接 |
快速检查语法 |
| Build | 只编译修改过的文件,然后链接(增量编译) | 日常开发 |
| Rebuild | 不管改没改,全部从头编译再链接 | 改魔术棒配置、交作业前 |
Build 为什么快:库文件编译一次后生成 .o,只要你没改它就不再重新编译,只编译你改动的文件然后直接链接。这就是“增量编译”。
6.7 编译生成的中间文件
| 扩展名 | 作用 | 能否删除 |
|---|---|---|
.o |
编译后的二进制机器码+符号表,链接器拼成固件的原料 | 不能手动删 |
.d |
记录该源文件 #include 了哪些头文件,用于增量编译判断 |
可删,自动重建 |
.crf |
交叉引用文件,支持“跳转到定义”功能 | 可删,自动重建 |
.s |
C 代码对应的汇编代码,给你看的 | 可删,自动重建 |
.hex/.bin |
最终烧录固件 | 成品,要留着 |
七、寄存器操作与 C 语言相关
7.1 RCC->APB2ENR 的本质
RCC 是一个指向结构体的指针常量:
#define RCC ((RCC_TypeDef *) 0x40021000)
RCC->APB2ENR 的地址 = 0x40021000 + 结构体成员 APB2ENR 的偏移量(0x18) = 0x40021018。
你写 RCC->APB2ENR = 0x00000010;,编译器把它翻译成 STR R1, [0x40021018],数据总线上的 32 位值直接拍进对应寄存器。
7.2 位掩码宏
#define RCC_APB2ENR_IOPCEN ((uint32_t)0x00000010)
就是 0x00000010 的宏替身。IOPC = IO Port C,EN = Enable。
为什么不用十进制写:手册上所有寄存器位掩码都是十六进制标注,用十六进制可直接对照手册。
7.3 位运算操作符
| 写法 | 效果 |
|---|---|
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; |
第 4 位置 1(开时钟),其余位不变 |
RCC->APB2ENR &= ~RCC_APB2ENR_IOPCEN; |
第 4 位清 0(关时钟),其余位不变 |
RCC->APB2ENR ^= RCC_APB2ENR_IOPCEN; |
第 4 位翻转 |
|= 是“按位或后赋值”,&= ~ 是“按位与取反后赋值”。它们只改动目标位,其余位安全保留。
位运算符只适用于整型。但所有整型数据在内存中都以二进制存储,编译器自动把十六进制常量换算成二进制位模式参与运算。
7.4 uint32_t 是什么
无符号 32 位整数的标准化写法(定义在 <stdint.h>)。STM32 所有寄存器都是 32 位宽,必须用它保证位宽精确匹配。
7.5 枚举(enum)的作用
给整数常量起有意义的名字。GPIO_Speed_50MHz 在代码里看到就知道是 50MHz 速度,而没有枚举就得查手册才知道数字 3 代表什么。
自动递增规则:枚举成员如果不显式赋值,值等于前一个成员的值 + 1。
参数校验宏:IS_GPIO_SPEED(SPEED) 这种宏专门用于判断传入参数值是否在合法范围内,配合 assert_param() 在函数开头做参数合法性检查。
八、调试、烧录与其他
8.1 ST-Link 和 SWD
ST-Link:连接电脑 USB 和芯片调试口的硬件工具。
SWD(Serial Wire Debug):两线调试协议(SWDIO + SWCLK)。JTAG 是它的老前辈,占 5 个脚,在 STM32 上已基本被 SWD 取代。
Keil 里的 Port 选项:选择用 SW 还是 JTAG 协议通信。直接选 SW 就好,大多数报错是因为 JTAG 兼容问题。
烧录和调试共用同一接口:点 Download 是把程序灌进 Flash;点 Debug 是在程序跑起来后能随时暂停、单步、看变量值。这两样走的是同一条物理通路。
8.2 软件工程调试 vs 嵌入式调试
纯软件 C 程序:点运行,结果在黑窗口里显示;操作系统和驱动帮你把硬件全包办了。
STM32 程序:Keil 只编译并告诉你语法对不对。灯亮不亮、电平对不对,必须烧进芯片看实际效果。可以借助 Debug 模式看寄存器值辅助排查,但终究要硬件验证。编译通过只是第一步,硬件跑通才是真本事。
九、核心哲学:从砂子到软件的抽象栈
硅片上刻出固定的逻辑电路 → 指令编码是这些电路的触发口令 → 程序是一串口令序列 → 编译器把你的想法翻译成这些口令。
整个数字系统设计思想就是分层封装、逐层抽象:
物理电路 → 指令集 → 汇编 → 编译器 → 高级语言 → 操作系统 → 应用软件
每一层向上提供人类易于理解的接口,向下屏蔽底层硬件细节。你学的 STM32 裸机卡在 C 语言和寄存器之间,所以你能同时看到高级语言的便利性和底层硬件的赤裸真相。
模块化设计的代价:你看到的“性能浪费”,是工程设计里用空间和效率换来的通用性、可维护性与可靠性。没有这些浪费,就没有 STM32 这种人人都能玩得起的通用单片机。
笔记终。持续更新。

浙公网安备 33010602011771号