ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32寄存器编程白话手册:从GPIO到中断彻底搞懂外设底层

STM32寄存器编程白话手册:从GPIO到中断彻底搞懂外设底层

1. 为什么我要写一本STM32寄存器的“白话手册”

先聊点实在的。市面上的STM32教程铺天盖地,但大多数都停留在“复制代码、改引脚、跑起来”的阶段。尤其是刚接触STM32的人,一上来就被开发环境的向导工具牵着走,点几下鼠标,一个能点灯的工程就生成了。灯亮是亮了,但你问自己一句:GPIOA的ODR寄存器到底是怎么把电平送出去的?恐怕很多人答不上来。

我最早也是从库函数入门的,说句不好听的,那叫“照猫画虎”。库函数把底层细节包得太严实,看起来是省事了,可一旦遇到芯片供电异常、引脚复用冲突、外设时钟没打开这类问题,你会发现自己连从哪儿排查都不知道。后来我下决心把寄存器手册硬啃了一遍,配合芯片参考手册一个寄存器一个寄存器地翻,才慢慢把整个外设的工作过程理顺。这是一条绕不开的路,也是我对“寄存器”这三个字最深的体会。

写这本“白话手册”的初衷特别简单:把参考手册里那些文绉绉、堆满专业术语的寄存器描述,翻译成“人话”。参考手册是芯片原厂写的,严谨是严谨,但读起来像在天书里找线索,比如“复位后该寄存器处于复位值”,听起来毫无波澜,实际上这条信息能救你一命。所以我就想按自己的理解,把最常用的时钟、GPIO、定时器、串口、中断这几块内容,用大白话总结出来,顺便把踩过的坑也一并写进去。

这本手册适合谁看?我觉得最适合三类人:一是刚跳进STM32坑、被各种函数接口绕晕的新手;二是用库函数开发但遇到疑难杂症、想往下挖一下原理的进阶用户;三是准备自己画板子、做底层固件、或者要移植系统的开发者。不管你属于哪一类,读完之后再去翻原厂参考手册,你会觉得那些寄存器突然变得不再面目可憎了。

2. 上手之前,先把这几个概念搞清楚

2.1 寄存器到底是个什么东西

很多教程开篇就甩出一大堆寄存器名字,却不说清楚它为什么存在。我打个比方。假设一个外设就是一间办公室,办公室里有很多带编号的抽屉,每个抽屉职能各不相同,有的抽屉是放“开关指令”的,你往里面塞一张写着“打开电源”的纸条,电源就接通了;有的抽屉是用来“读取状态”的,你打开抽屉看一眼,就知道设备现在忙不忙。这些抽屉,就是寄存器。

从计算机角度看,寄存器就是挂在总线上一段连续的存储单元,每个单元有固定的地址。CPU写数据到某个地址,外设就收到命令;CPU从某个地址读数据,就能得到外设的状态。唯一的区别是,普通内存里的数据是“死”的,你存多少就是多少,但寄存器中的数据会直接影响硬件行为,或者反过来反映硬件当前的真实状态。

这也是为什么在C语言里操作寄存器时,一定要用volatile修饰。定义某个寄存器指针,如果不加volatile,编译器可能会自作聪明,把多次读取合并成一次,或者把明明没变的变量给优化掉,结果就是你的代码在调试器里怎么看怎么对,到了现场就是不行。我用它踩过一次大坑,后面在调试部分详细说。

2.2 总线结构,决定你该先开谁的时钟

STM32内部不是把所有外设都挂在一条总线上,而是分了几层。理解这个分层的意义非常大,因为你对任何外设操作之前,第一件事永远是“打开对应总线的时钟”。时钟没开,寄存器写了就跟没写一样,这是新手遇到最多的灵异事件。

拿常见的F1系列举例:最顶层的是AHB总线,往下又分出APB1和APB2两条。APB2是“高速外设总线”,挂了GPIO、串口1、定时器1这类对速度要求比较高的外设;APB1是“低速外设总线”,挂了定时器2到定时器5、串口2、串口3这些。你点灯之前,操作的是某个GPIO口,高位的GPIO挂在APB2上,低位的GPIO也挂在APB2上,所以必须先操作RCC_APB2ENR这个寄存器,把对应GPIO口的时钟位置1。定时器2就不一样了,它挂在APB1上,你得操作RCC_APB1ENR。

很多人在网上抄来的代码里看到RCC->APB2ENR |= 1 << 2;这种句子,完全不明白在干什么。现在我告诉你,这行操作放在GPIOA的场景里,就是“打开挂在APB2总线上的GPIOA的时钟开关”。后面我给的代码,你一眼就能看懂自己做了什么。

2.3 外设基地址 + 偏移量,寄存器地址是这样来的

每个外设都有一块专用的寄存器空间,这空间的起始地址叫“基地址”。比如GPIOA基地址是0x40010800,串口1基地址是0x40013800,片上外设的寄存器,就是从基地址开始往后排的内存区域。具体某个寄存器,比如GPIOA的ODR寄存器,它的地址就是“基地址+偏移量”。ODR这个寄存器的偏移量是0x0C,那GPIOA->ODR的绝对地址就是0x40010800 + 0x0C。

在代码里,我们通常不做这种“基地址+偏移量”的算术,而是用芯片厂商提供的头文件,里面已经把外设定义成结构体了。定义大致是这样的:

typedef struct { __IO uint32_t CRL; __IO uint32_t CRH; __IO uint32_t IDR; __IO uint32_t ODR; __IO uint32_t BSRR; __IO uint32_t BRR; __IO uint32_t LCKR; } GPIO_TypeDef;

然后定义#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)。这样你写GPIOA->ODR = 0xFFFF;时,编译器会自动算出GPIOA基地址加ODR偏移量,最后生成一条向该地址写数据的指令。讲这个不是为了让你背地址,是为了让你遇到“往寄存器写值没反应”时,能主动怀疑是不是偏移量算错、是不是地址串了。

3. 从点亮LED到读取按键,GPIO寄存器全解

3.1 打开时钟,点亮LED的第一步

点灯是嵌入式界的“Hello World”,但寄存器版的点灯和库函数版完全不同。先明确我要做的事情: 把PA1引脚配置成推挽输出,输出高电平,让接在PA1上的LED亮起来。听起来是三步,实际要做的事有四步,顺序不能乱。

第一步,打开GPIOA时钟。前面说了GPIOA挂在APB2总线上,那操作的是RCC_APB2ENR寄存器。看代码:

RCC->APB2ENR |= 1 << 2;

寄存器里第2位对应GPIOA,把这一位置1,时钟就开始跑起来了。这里用|=而不是=,是因为APB2ENR里面还有其他位,比如GPIOB是第3位,ADC1是第9位,你不应该把其他位清零。新手最爱犯的错误就是直接写一个等号,把整个寄存器清掉了,结果其他外设全部闹罢工。

第二步,配置引脚模式。GPIOA的配置寄存器分为CRL和CRH两个。CRL管PA0到PA7,CRH管PA8到PA15。每个引脚占4位,这4位又分成MODE位和CNF位。MODE位用来设置输入还是输出以及输出速度,CNF位用来设置具体模式——比如是推挽输出、开漏输出,还是上拉输入、模拟输入。以PA1为例,它是CRL寄存器的第4到第7位,其中MODE[1:0]在第5到第6位,CNF[1:0]在第4到第5位。我写个更直白的计算:

GPIOA->CRL &= ~(0xF << 4); // 先把第4到7位清零,避免历史值干扰 GPIOA->CRL |= (0x1 << 4); // MODE=01,输出模式,速度10MHz GPIOA->CRL |= (0x0 << 6); // CNF=00,推挽输出

如果你刚接触位操作,可能会被这个搞迷糊。0xF是二进制1111,左移4位以后就是覆盖第4到7位的掩码。先按位取反再按位与,是把这几位清零;后面再按位或,是往这几位写具体值。这就像拿一块橡皮擦干净黑板上的旧内容,再写新的。不清零就直接或上去,旧数据就会和新生效的数据打架,引脚模式会是乱的。

第三步,设置输出电平。输出数据寄存器ODR,每一位对应一个引脚。往第1位写1,PA1输出高电平;写0,输出低电平:

GPIOA->ODR |= (1 << 1);

到这里灯就能亮了。不过我还想顺带提一下BSRR这个寄存器。手册里BSRR是端口位设置/清除寄存器,用BSRR去操作输出更优雅:高16位是清位,低16位是置位,一次写入就能完成设置。比如:

GPIOA->BSRR = (1 << 1); // PA1输出高电平 GPIOA->BSRR = (1 << (1 + 16)); // PA1输出低电平

BSRR的好处是不会被打断,在中断和DMA场景里用得更放心。你以后读别人的代码,看到BSRR这种写法,就知道它在干同一件事,只是更贴近寄存器手册的本意。

3.2 读取按键,输入模式配置与电平判断

读按键跟配置输出刚好反过来。以PA0接一个按键为例,按键按下接地,松开接高电平。那我们需要把PA0配置成上拉输入,就是默认被拉到高电平,按下时变成低电平。

RCC->APB2ENR |= 1 << 2; // 打开GPIOA时钟 GPIOA->CRL &= ~(0xF << 0); // 清零PA0的4个配置位 GPIOA->CRL |= (0x8 << 0); // CNF=10, MODE=00,上拉/下拉输入 GPIOA->ODR |= 1 << 0; // 写1到ODR,相当于选择上拉

这里CNF位是10表示输入模式的浮空输入或上下拉输入,到底选上拉还是下拉,要看ODR这个位怎么设置:写1是上拉,写0是下拉。从配置寄存器的字面来看,好像输入模式下MODE位应该全是0,但也有人误把MODE写成输出模式,结果读回来的电平永远不对,这个问题值得警惕。

读到按键状态用的是IDR寄存器,每一位对应一个引脚的电平状态。判断方法需要注意一点:IDR里一个引脚只占1位,不能把整个寄存器值拿来做等值比较,标准写法是用掩码:

if ((GPIOA->IDR & (1 << 0)) == 0) { // 按键按下 }

这里有个硬件抖动处理问题:机械按键按下和松开时,电平会伴随几毫秒到几十毫秒的抖动,肉眼看不出来,但CPU读得到。寄存器操作没有现成的防抖接口,得自己在代码里加消抖,比如读到一个稳定状态后延时10ms再确认一次。我习惯是用一个标志变量配合简易的延时函数来处理,别在中断里直接做长延时,那会把整个系统的实时性拖垮。

3.3 复用功能与重映射,引脚不够用时的选择

GPIO还有一个衍生话题:复用和重映射。很多外设的引脚不是固定的,芯片厂家为了提高布线灵活性,允许你用软件把某个外设功能映射到不同的引脚上。比如串口1的发送引脚,默认是PA9,也可以重映射到PB6。这种操作在寄存器层面要动两个地方:一是配置引脚的CNF位为复用推挽输出,二是配置重映射寄存器AFIO。

重映射之前,有一件新手容易漏掉的事:AFIO时钟也要先打开。很多人记得开GPIO时钟,记得开USART时钟,唯独忘了AFIO时钟,结果重映射怎么都不生效。具体是操作RCC_APB2ENR的第0位:

RCC->APB2ENR |= 1 << 0; // 使能AFIO时钟 AFIO->MAPR |= 1 << 2; // 以USART1重映射为例,把引脚从PA9/PA10切到PB6/PB7

我个人的经验是:能用默认引脚就尽量用默认引脚,重映射只是备选方案。因为一旦开了重映射,原本的默认引脚也不再绑到该外设上,如果不仔细看原理图,很容易出现两个外设抢同一个引脚的冲突,排查起来非常痛苦。

4. 定时器寄存器:精确时基是怎么算出来的

4.1 预分频器、自动重装载与计数器

定时器是STM32里比较能体现寄存器编程优势的外设。库函数封装了一堆结构体和初始化函数,把逻辑藏得很深,而寄存器操作把每一步都摊在明面上。

以定时器2为例,它的核心寄存器有三个:TIM2_CR1是控制寄存器,决定定时器启动、停止、计数方向;TIM2_PSC是预分频寄存器;TIM2_ARR是自动重装载寄存器。整个定时过程的原理一句话就能讲明白:定时器内部有一个计数器,每个时钟脉冲来了就加1,当计数值跟ARR寄存器里的值相等时,就产生一个更新事件,计数器清零重新开始。

那这个时钟脉冲从哪里来?来自定时器时钟源,它通常是总线时钟经过预分频后的信号。PSC不是分频系数本身,而是“分频系数减1”,这是新手最容易算错的地方。假设定时器时钟是72MHz,你想得到1微秒一次的计数脉冲,分频系数就是71,写到PSC里的值是71。计数器从0加到ARR,ARR里的值就是你要计多少个脉冲,同样也要减1。比如要1毫秒溢出一次,那就是数1000个脉冲,ARR写999。

RCC->APB1ENR |= 1 << 0; // 打开定时器2时钟(挂在APB1) TIM2->PSC = 71; // 72MHz / 72 = 1MHz,即1微秒计一次 TIM2->ARR = 999; // 计数1000次,溢出周期1毫秒 TIM2->CR1 |= 1 << 0; // 启动定时器

溢出后,状态寄存器SR的更新标志位会被硬件置1。这个标志位你可以用轮询的方式去等待,也可以用它触发中断。如果选择轮询,注意读完之后要清标志位。很多寄存器位是RC(读清除)类型,意思是读一次就自动清零;而SR里的更新标志却是写0清除的,这是一个大坑。你读它100次,它可能依然是1,只有写入0才真正清零。我刚开始就犯过这个错,导致中断服务里死循环。

正确的清标志做法是:

TIM2->SR &= ~(1 << 0); // 清除更新标志位

4.2 定时中断里能做什么不能做什么

让定时器产生中断,需要把DIER寄存器里的更新中断使能位置1,同时在NVIC里把中断通道打开。这一步在寄存器操作中的编码量不小,但逻辑很清晰。

中断服务函数是另一个发挥空间。我见过有人在定时器中断里写延时循环、写串口打印、甚至跑算法,系统动不动就卡顿。定时中断服务函数应该短小精悍,它的职责只是“标记一下时间到了”,真正耗时的处理应该放在主循环里。

我个人惯用的写法是这样的:中断服务函数里只做一个标志位置1,主循环里检测这个标志,再去做数据采集、显示刷新这类事。这样既不会漏掉中断事件,也不会阻塞其他中断响应。你可能会看到网上有人直接在中断里做完所有事,看起来好像没问题,那是因为他的系统负载低,不代表这是好习惯。身边真实发生过的案例:某同学在一个1毫秒定时中断里加了一段打印函数,结果打印一次就要占用几百微秒,加上其他处理,整个中断的基本盘被拖到了接近2毫秒,定时自然就不准了。

4.3 实测频率计算,别被库函数骗了

这里我给你一个实际的计算题目。芯片系统主频是72MHz,APB1分频系数默认是2,APB1上的定时器时钟是72MHz还是36MHz,取决于APB1预分频值。按F1系列的特殊规则,APB1不分频时定时器时钟等于APB1时钟;如果APB1分频了,定时器时钟反而翻倍。换句话说,实际上APB1定时器时钟就是72MHz。如果你不看手册里这条规则,直接用36MHz去算PSC,你的定时时间会差整整一倍。

我把这个规律总结成一句话: 先把外设时钟树从头捋一遍,再说配PSC和ARR的事,否则全是白算。

5. 串口寄存器:数据进出的大门

5.1 发送和接收,核心就那三四个寄存器

串口是嵌入式系统里最重要的调试手段,也是很多传感器通信的基础。寄存器操作串口,实际上要掌握的寄存器数量比想象中少。以USART1为例,挂在APB2上,记得把RCC_APB2ENR第14位置1。

配置串口波特率,需要一个关键的寄存器——BRR。波特率寄存器里面存的是“分频值”,这个值的计算公式在原厂手册里有,但看着头疼。我直接给你常用的结果:当外设时钟是72MHz,想要115200波特率时,分频值是0x2710,实际写入时要用一些技巧。F1系列的BRR分两部分,高4位是USARTDIV的整数部分,低12位是小数部分,所以不能简单地把算出的分频值整个写进去。很多人在这里栽跟头。

比较省心的办法是调用一个函数去算,参考芯片参考手册里关于BRR设置的说明。很多示例代码里都有这个换算式子,用移位和取余把整数和小数拆开。从核心逻辑上看,就是“把波特率分频值转成16位寄存器值”,你可以照着参考手册的公式自己推导一遍,比复制代码有用得多。

发送一个字节的流程是: 先读SR寄存器,看TXE位是否置1,表示发送数据寄存器为空;然后把要发的字节写入DR寄存器。DR寄存器低8位就是数据位,写进去后硬件自动把数据移位送出去。接收则是看RXNE位,如果置1,表示接收数据寄存器里有新数据,直接读DR即可。

// 发送一个字节 while (!(USART1->SR & (1 << 7))); // 等待TXE为空 USART1->DR = ch;

接收的时候要注意: 读SR之后再读DR,顺序不能反。因为在某些芯片版本里,SR的RXNE标志在读DR之前要先读一次,否则标志清除不及时,下一次会读到同一个旧数据。这属于硬件设计的历史遗留,实际编程就是要养成“先判标志,再读数据”的习惯。

5.2 使能串口,一个寄存器三条位

USART1的CR1寄存器像一个总开关面板,里面包含UE(串口使能)、TE(发送使能)、RE(接收使能)三个位。很多新手写了半天,忘了置UE位,串口一点反应都没有。配置顺序上,一般建议先设置波特率、数据位格式,最后再打开UE、TE、RE这几个开关位。

USART1->CR1 |= (1 << 13); // UE=1,开启串口 USART1->CR1 |= (1 << 3); // TE=1,允许发送 USART1->CR1 |= (1 << 2); // RE=1,允许接收

串口的中断使能则分布在CR1、CR2等多个位置,比如接收中断是RXNEIE位在CR1第5位,发送完成中断是TCIE在CR1第6位。我在项目里常用的是“接收中断+轮询发送”组合,既保证接收不丢数据,又简化逻辑,你可以参考这个套路。

5.3 波特率误差,被忽略的时钟源问题

串口通信经常遇到乱码,十有八九是波特率对不上。寄存器编程时,要注意串口时钟来源可能是系统主时钟,也可能是外部低速时钟。如果你换了时钟源但没改BRR的计算依据,波特率就直接偏了,接收端自然解码出错。

我在实际测试中用过一种土办法:把发送端固定发一串0x55字节,0x55的二进制是01010101,波形上就是明显的方波。用示波器看方波的周期,能反推出实际波特率。这样几分钟就能定位到底是哪一端时钟配置错了,比反复改软件快得多。

6. 中断与NVIC寄存器,让CPU响应外部事件

6.1 使能外部中断时的三处配置

外部中断的寄存器配置,要比定时器中断多几步。比如要使用PA0引脚的外部中断,需要做三件事:一是配置GPIO模式为输入,二是配置外部中断线,三是设置NVIC使能。

GPIO配置为输入倒是简单,关键是外部中断线怎么选。F1系列的外部中断控制器通过AFIO的EXTICR寄存器来选择中断线对应哪个端口。EXTICR1的第0到第3位对应EXTI0,这4位填0000表示选择PA0,填0001是PB0,以此类推。这一层映射很容易被忽略,我在项目里配置PB0外部中断时,就因为忘了改EXTICR的值,中断始终不触发,查了半天才发现中断线还绑在PA0上。

然后还有EXTI本身的寄存器组:IMR是中断屏蔽寄存器,把对应位置1才会放行;RTSR和FTSR分别设置上升沿和下降沿触发;PR是挂起寄存器,中断发生后标志位置1,需要写1清除。一串操作连起来,是新手眼中最“寄存器感”的一段代码。

以PA0上升沿触发为例:

AFIO->EXTICR[0] &= ~(0xF << 0); // 选择PA0 EXTI->IMR |= 1 << 0; // 不屏蔽0号线 EXTI->RTSR |= 1 << 0; // 上升沿触发 EXTI->FTSR &= ~(1 << 0); // 关闭下降沿触发

中断响应之后,系统会跳转到中断服务函数。很多初学寄存器编程的人在这里困惑:为什么中断服务函数的名字是固定的?因为启动文件中已经把中断向量表填好了,中断来了CPU自动跳到对应的函数地址。你要是函数名写错,链接时不会报错,但中断永远进不去,这是一个极其隐蔽的错误。

6.2 中断服务函数里的清标志顺序

外部中断服务函数里,清标志的顺序有讲究。个人习惯是进入函数后立刻读一次中断状态,把对应的清除操作先做完,再开始处理业务逻辑。千万不能先花大几十微秒去做别的操作,然后才清标志。在这期间如果又来了一次同样的中断,理论上会被挂起,但因为间隔极短,极易丢失。

曾经接手过一段代码,中断里做了一堆数据处理,最后一步才清标志,结果有一次外部传感器连续发脉冲时,系统经常掉事件。我把清标志提前到第一行,问题立刻消失了。这个改动小,但效果立竿见影,是我最想分享的一条实战经验。

7. 寄存器调试的常用套路与实操心得

7.1 我在写寄存器代码时遇到过的最蠢bug

先说一个最蠢又最典型的错误:GPIO配置寄存器写入时因为掩码不对,把相邻引脚的模式也改了。当时目标是配置PA3为输出,但掩码计算成0xF左移8位,结果不小心把PA2的配置也清了。LED没亮,反而把另一个引脚给锁住了。这种位操作的边界错误,靠肉眼盯代码是很难发现的,需要对照寄存器手册一位一位数清楚。

另一类高频bug是“数组指针越界”。外设寄存器结构体里有的数组,比如EXTICR[4]、AFIO->MAPR,访问时下标越界不会报错,只是悄悄写到了相邻地址上。你写的可能是下一个外设的寄存器区域,硬件行为千奇百怪。排查这类问题,最简单的是检查结构体指针的偏移量是否和手册一致。

寄存器编程的优势在于直观,代价也同样明显:你需要对每一个操作的后果有清晰预期。我的建议是每次配置完外设后,先通过调试器读一遍寄存器实际值,对照手册里的描述确认无误再继续往下写,这个习惯能救回很多时间。

7.2 调试器与寄存器的侦查工具箱

除了常规的IDE调试,我强烈推荐使用调试器的寄存器窗口。调试器可以在程序暂停时读出所有外设寄存器的当前值,你一眼就能看出:时钟有没有开、当前引脚输入电平是多少、标志位有没有置起。这种“冻结现场”的能力,是寄存器编程排查问题的第一大杀器。

排查时我的固定套路是:先看时钟寄存器,确认外设时钟已使能;再看配置寄存器,确认模式和方向正确;再看数据寄存器,确认数据和状态值是否跟预期一致;最后看标志寄存器,确认中断和数据操作走到哪一步了。按这个顺序走一遍,多数“仿佛不生效”的问题都能找到方向。

如果调试器都不好使,还有一个土办法:写一个极简测试代码,只干一件事。比如外设不动,只把引脚输出高低电平,用示波器或万用表量电平。把复杂问题拆成最小可复现单元,逐步排除变量,比憋着代码猜快得多。

7.3 关于优化等级与编译器的坑

寄存器编程和编译器优化之间,有一个常被忽视的陷阱。当你开启较高优化等级时,编译器可能改变代码的执行顺序,也可能把连续多次读同一个寄存器的操作合并。前面说到volatile修饰就是预防这个的,但即便加了volatile,有些顺序问题仍需要小心。

C语言标准不保证volatile变量的读写顺序不会被改变,这在嵌入式里表现为:你写了一个寄存器,然后马上读另一个相关寄存器,理论上应该按顺序发生,但高级优化下可能被打乱。实践中我的经验是,涉及硬件时序的操作,要么用编译器提供的内存屏障,要么暂时关闭这一块的某个优化选项。更粗暴的办法是把这个项目的优化等级调低一档,大多数情况下不影响运行速度,但换来了极大的可预测性。

8. 一点个人总结,送给正在学寄存器的人

我做寄存器编程这一路下来,最真切的感受是:寄存器不复杂,复杂的是被层层的封装隔断了心智。你怕它,是因为看不见内部,但寄存器恰恰把整个硬件摊在你面前,每一个位都是一个明确的信号。与其怕它,不如花一个下午把GPIOA的CRL和ODR反复玩熟,那种“CPU在控制硬件”的实感,比任何教学视频都来得强烈。

练习的时候我的建议很直接:找一个带LED和按键的最小开发板,先点亮一颗灯,再去读按键,然后加上定时器让灯闪烁,最后用串口和电脑互发数据。这四步走完,你使用寄存器编程的基础就已经很扎实了。往后去接ADC、控制舵机、驱动显示屏,你会发现底层逻辑全都相通。

最后分享一个学习的小技巧:注册一个云笔记,把你每配置一个外设时用到的关键寄存器、关键位、踩过的坑,用自己的话记录下来。这其实就是你个人版的“白话手册”。手册这东西,最适合的手法是边用边写,跟着项目成长的笔记,永远是排错时最趁手的参考。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进