1. 为什么还要啃寄存器手册
1.1 从“点灯成功”到“心里没底”的鸿沟
很多人玩STM32的经历都差不多:拿到板子,装好开发环境,找个现成工程,编译下载,LED亮了,串口有输出了,心里一阵激动。然后呢?然后想改个引脚,发现不知道从哪下手;想调个PWM频率,翻遍例程也找不到那个决定频率的寄存器在哪;中断响应慢了一拍,只能凭感觉瞎改优先级。这就是典型的“库函数会用,但底层逻辑一片空白”的状态。
我见过太多人卡在这个阶段。他们能跑通例程,但一旦需求稍微偏离例程,就束手无策。比如某个项目要求PA8输出一个特定频率、特定占空比的方波,同时TIM2的更新中断里要翻转另一个引脚,还要保证中断响应延迟不超过某个值。这种需求,光靠改例程里的宏定义是搞不定的,你必须知道哪个寄存器的哪个位在控制什么。
寄存器手册就是解决这个问题的钥匙。它不是让你抛弃库函数,而是让你在库函数不够用、或者行为不符合预期的时候,有能力直接跟硬件对话。第五章通常涉及的是GPIO、中断、定时器这些最核心的外设,这些恰恰是实际项目里用得最多、也最容易出问题的地方。
1.2 寄存器操作的本质:给内存地址写数据
说穿了,操作寄存器就是往特定的内存地址写特定的值。STM32的每个外设都有一片地址空间,片内每个寄存器都对应一个固定的偏移地址。比如GPIOA的基地址是0x40010800,那么GPIOA_CRL的地址就是基地址加上0x00,GPIOA_CRH就是基地址加上0x04,以此类推。
你写GPIOA->CRL = 0x44444444;,编译器实际上就是往地址0x40010800写入0x44444444。就这么简单。库函数做的事情,无非是帮你算好了地址,加了一些参数检查,让你不用去翻手册查偏移量。但代价是,你失去了对每一个位的精确控制,也失去了对执行效率的把握。
我个人的习惯是:初始化阶段用库函数快速搭建框架,关键路径和性能敏感的地方直接操作寄存器。比如中断服务函数里,为了减少延迟,我会直接写TIM2->SR = 0;来清标志位,而不是调用TIM_ClearITPendingBit(TIM2, TIM_IT_Update);。后者要多执行好几条指令,在高速中断场景下,这点时间差可能就是稳定与不稳定的分界线。
1.3 第五章通常覆盖哪些内容
根据常见的STM32寄存器手册编排,第五章一般会深入讲解以下几个模块:GPIO的八种工作模式及其配置方法、外部中断EXTI的触发机制与NVIC优先级管理、通用定时器TIM的基本定时与PWM输出功能、以及USART串口通信的寄存器级配置。
这些模块之间不是孤立的。比如你要做一个红外遥控解码器,就需要GPIO输入捕获、EXTI边沿触发、TIM精确计时三者配合。如果你只懂库函数调用,遇到信号抖动或者时序偏差,根本无从下手排查。但如果你清楚每个寄存器在干什么,就能像调试电路一样,用示波器配合寄存器状态,一步步定位问题。
提示:不要试图一次性背下所有寄存器的所有位。先掌握每个外设最核心的三五个寄存器,剩下的用到再查。手册是工具书,不是教科书。
2. GPIO寄存器:从点亮LED到精确控制
2.1 端口配置寄存器CRL和CRH的分工
STM32的GPIO端口有16个引脚,每个引脚需要4个配置位,16个引脚就是64位。但STM32的寄存器是32位的,所以需要两个寄存器来存放:CRL负责低8位(Pin0到Pin7),CRH负责高8位(Pin8到Pin15)。每个引脚的4个位分成两部分:低2位是MODE,高2位是CNF。
MODE位决定输出速度或输入模式。具体来说,00是输入模式,01是输出模式最大10MHz,10是输出模式最大2MHz,11是输出模式最大50MHz。CNF位则根据MODE的不同有不同的含义。输入模式下,00是模拟输入,01是浮空输入,10是上拉/下拉输入,11是保留。输出模式下,00是通用推挽输出,01是通用开漏输出,10是复用推挽输出,11是复用开漏输出。
我刚开始学的时候,最困惑的就是“复用”这两个字。后来想明白了:复用就是把这个引脚的控制权从GPIO外设交给其他外设,比如定时器、串口、SPI。你配置成复用推挽输出,引脚就不再受GPIOA->ODR的控制,而是受对应外设的控制。这个理解到位了,后面配置PWM输出、串口TX就顺理成章了。
2.2 输出模式的选择逻辑与实战场景
推挽输出和开漏输出的区别,是面试常问、实战常错的知识点。推挽输出可以主动输出高电平和低电平,驱动能力强,适合直接驱动LED、继电器等负载。开漏输出只能主动拉低,高电平需要外部上拉电阻提供,但好处是可以实现“线与”逻辑,多个开漏输出引脚接在一起,任何一个拉低,总线就是低电平。
我做过一个多主机共享总线的项目,三个STM32通过一根信号线通信,谁有数据谁拉低,空闲时靠上拉电阻保持高电平。这种场景就必须用开漏输出,用推挽的话,一个输出高、一个输出低,直接短路,芯片发热甚至烧毁。当时有个同事不信邪,非要用推挽,结果烧了两个引脚才回头改代码。
输出速度的选择也有讲究。10MHz、2MHz、50MHz不是指输出信号的实际频率,而是指引脚电平翻转的最大速率。速度越高,功耗越大,电磁干扰也越强。驱动LED这种慢速负载,用2MHz就够了。驱动SPI时钟这种高速信号,才需要50MHz。我一般默认用2MHz,除非有明确的高速需求,这样EMI表现会好很多。
2.3 输入模式的上拉下拉与浮空陷阱
输入模式下,浮空输入是最容易出问题的。引脚悬空时,电平不确定,读到的值随机跳变。我早期做按键检测,用浮空输入,发现按键没按下时,程序偶尔会误触发。后来改成上拉输入,按键一端接引脚、一端接地,按下时读到低电平,松开时内部上拉保证高电平,问题就解决了。
上拉和下拉的选择取决于外部电路。如果外部已经有上拉电阻,就配置成浮空或下拉;如果外部是下拉,就配置成上拉。最忌讳的是外部上拉、内部也上拉,或者外部下拉、内部也下拉,造成电平冲突。我见过一个案例,外部用了10k上拉,内部又开了上拉,结果按键按下时,分压导致低电平不够低,MCU识别不到。后来把内部上拉关掉,问题消失。
模拟输入模式用于ADC采样,此时引脚的数字输入功能被禁用,避免数字电路对模拟信号的干扰。如果你用ADC采集电压,但引脚配置成了浮空输入,采到的值会跳得厉害。这个坑我踩过,当时调了一个下午,最后查手册才发现是GPIO模式配错了。
2.4 位操作与原子性:BSRR和BRR的妙用
直接写ODR寄存器可以控制输出电平,但有个问题:读-改-写不是原子操作。比如你想把PA5置高,同时不影响其他引脚,代码可能是GPIOA->ODR |= (1<<5);。这条语句编译成汇编是:先读ODR到寄存器,然后或上(1<<5),再写回ODR。如果在这三步之间来了中断,中断里也修改了ODR,那么中断返回后,主程序的写回操作会覆盖中断的修改,导致中断里的电平变化丢失。
STM32提供了BSRR和BRR两个寄存器来解决这个问题。BSRR是位设置/清除寄存器,写1到低16位,对应引脚置高;写1到高16位,对应引脚置低。BRR是位清除寄存器,写1到某位,对应引脚置低。这两个寄存器的写操作是原子的,硬件保证不会被中断打断。
所以正确的做法是:GPIOA->BSRR = (1<<5);来置高PA5,GPIOA->BRR = (1<<5);来置低PA5。这样既高效又安全。我在所有中断服务函数里都坚持用BSRR/BRR,从来不用ODR的读-改-写。这个习惯让我避免了很多偶发的、难以复现的bug。
3. 中断与EXTI:让MCU学会“察言观色”
3.1 NVIC优先级分组:别让中断打架
STM32的中断优先级分为抢占优先级和响应优先级。抢占优先级高的中断可以打断正在执行的抢占优先级低的中断,这就是中断嵌套。响应优先级只在两个中断同时到达时决定谁先执行,不能嵌套。NVIC的分组决定了抢占优先级和响应优先级各占几位。
常见的分组有NVIC_PriorityGroup_0到NVIC_PriorityGroup_4。Group_0是0位抢占、4位响应,意味着没有中断嵌套,所有中断按响应优先级排队。Group_4是4位抢占、0位响应,意味着抢占优先级决定一切,响应优先级没用。我一般用Group_2,2位抢占、2位响应,这样既有嵌套能力,又有足够的优先级层次。
优先级数值越小,优先级越高。这个和直觉相反,我第一次配的时候搞反了,把最重要的中断配成了15,结果它总是被其他中断打断,数据丢得一塌糊涂。后来改成0,问题解决。记住:0是最高优先级,15是最低。
注意:不要滥用高抢占优先级。抢占优先级高的中断会打断其他中断,如果这个中断执行时间很长,会导致其他中断响应延迟。我一般只给最紧急的几个中断配高抢占优先级,比如电机堵转保护、通信超时检测。
3.2 EXTI触发方式与软件消抖
EXTI支持上升沿触发、下降沿触发、双边沿触发。按键检测一般用下降沿触发,因为按键按下时是下降沿。但机械按键有抖动,一次按下可能产生多个下降沿,导致中断多次触发。硬件消抖用电容电阻,软件消抖可以在中断里加延时确认,或者用定时器扫描。
我常用的软件消抖方案是:EXTI中断触发后,不立即处理,而是启动一个10ms的定时器,定时器到期后再读引脚电平,如果还是低电平,才确认按键按下。这样既不影响实时性,又能有效消抖。具体实现是:EXTI中断里关闭EXTI中断使能,启动定时器;定时器中断里读引脚,确认后处理按键,然后重新使能EXTI中断。
还有一种更简单的方案:在EXTI中断里加一个循环延时,比如for(int i=0;i<10000;i++);,然后再读引脚。但这种方法会阻塞CPU,影响其他中断响应,不推荐在复杂系统里使用。我早期用过,后来系统中断一多,就发现按键响应变慢,甚至丢按键,果断换成了定时器方案。
3.3 中断服务函数的编写规范
中断服务函数要短小精悍,这是铁律。我见过有人在中断里做浮点运算、调用printf、甚至延时,这些都是大忌。中断里应该只做最紧急的事情:清标志位、置标志变量、发信号量。耗时的处理放到主循环里做。
清标志位是必须的,而且要在中断处理的最前面清。STM32的EXTI挂起寄存器EXTI_PR,写1清除对应位。如果你忘了清,中断会反复触发,程序卡死在中断里。我有个学生,调了一整天,说串口一收数据就死机,最后发现是USART中断里没清RXNE标志,导致中断不断重入。
中断服务函数的命名也要注意。STM32的启动文件里定义了中断向量表,函数名必须和向量表里的一致,否则中断触发时会跳转到默认的死循环。比如EXTI0的中断函数名是EXTI0_IRQHandler,你写成EXTI0_Handler,编译能过,但中断一来就死。这个坑很隐蔽,因为编译不报错,运行时才出问题。
3.4 中断与主循环的数据共享问题
中断里修改的变量,主循环里读取,这个变量必须用volatile修饰。否则编译器优化时,可能把变量缓存到寄存器里,主循环读到的永远是旧值。我早期写代码,中断里置了个标志位,主循环里判断这个标志位,结果主循环死活不进if。查了半天,加了volatile就好了。
更复杂的情况是,主循环和中断共享一个多字节变量,比如32位的计数器。主循环读低16位和高16位之间,中断可能修改了这个变量,导致读到的值不一致。解决办法是:读之前关中断,读完开中断;或者用双缓冲,中断写一个缓冲区,主循环读另一个,定期切换。
我一般用关中断的方式,简单可靠。__disable_irq();和__enable_irq();是CMSIS提供的内联函数,执行很快。但要注意,关中断的时间不能太长,否则影响其他中断响应。如果共享变量很多,可以考虑用DMA或者消息队列来传递数据,避免直接共享。
4. 定时器TIM:从精确延时到PWM输出
4.1 时基单元:PSC、ARR和CNT的关系
定时器的核心是三个寄存器:预分频器PSC、自动重装载寄存器ARR、计数器CNT。CNT从0开始计数,每个时钟周期加1或减1。当CNT达到ARR的值时,产生更新事件,CNT重新从0开始。PSC用来分频输入时钟,实际计数频率是时钟频率/(PSC+1)。
定时时间的计算公式是:定时时间 = (PSC+1) * (ARR+1) / 时钟频率。比如时钟频率72MHz,PSC=71,ARR=999,那么定时时间 = 72 * 1000 / 72000000 = 1ms。这个公式要记牢,配定时器的时候直接套。
我见过有人把PSC和ARR搞反,想要1ms定时,结果配出来是1秒。还有人忘了加1,PSC=71实际是72分频,不是71分频。这些细节手册上写得很清楚,但很多人不看,凭感觉配,结果就是不对。
提示:ARR和PSC都是16位寄存器,最大值65535。如果需要更长的定时,可以级联两个定时器,或者用软件计数。我做过一个项目需要30秒定时,单个定时器不够,就用TIM2产生1ms中断,在中断里对一个变量累加,加到30000就是30秒。
4.2 PWM输出的配置步骤与占空比计算
PWM输出是定时器最常用的功能之一。配置步骤是:配置GPIO为复用推挽输出,配置定时器时基单元,配置输出比较模式为PWM模式,使能输出比较通道,使能定时器。
占空比由CCR寄存器决定。PWM模式1下,CNT小于CCR时输出高电平,CNT大于等于CCR时输出低电平。所以占空比 = CCR / (ARR+1)。比如ARR=999,CCR=300,占空比就是30%。频率 = 时钟频率 / ((PSC+1)*(ARR+1))。
我调PWM的时候,习惯先把ARR和PSC算好,确定频率,然后再调CCR调占空比。这样思路清晰,不容易乱。如果频率和占空比同时调,很容易顾此失彼。另外,CCR的值不能大于ARR,否则占空比永远是100%,这个要注意。
4.3 输入捕获:测量脉冲宽度的利器
输入捕获用来测量外部信号的频率和脉宽。原理是:当引脚上出现指定边沿时,当前CNT的值被锁存到CCR寄存器,同时产生捕获中断。通过两次捕获的CNT差值,可以算出脉冲宽度。
配置步骤是:配置GPIO为浮空或上拉输入,配置定时器时基单元,配置输入捕获通道为上升沿或下降沿捕获,使能捕获中断,使能定时器。在捕获中断里读取CCR值,计算差值。
我做过一个红外遥控解码的项目,就是用输入捕获测脉宽。红外信号的高电平和低电平持续时间不同,代表0和1。用输入捕获测出每个电平的持续时间,再根据协议解码。这个方案比用外部中断加延时精确得多,因为输入捕获是硬件自动完成的,不受中断延迟影响。
4.4 定时器中断的优先级与执行时间
定时器中断的优先级要根据用途来定。如果是系统时基,比如1ms滴答,优先级可以低一些,因为它只是累加计数,晚一点执行没关系。如果是电机控制PWM,优先级要高,因为PWM波形不能有抖动。
中断服务函数的执行时间也要控制。我见过有人在定时器中断里做PID运算,浮点乘除一大堆,结果中断执行时间超过定时周期,导致中断丢失,系统失控。正确的做法是:中断里只置标志位,PID运算放到主循环里做。如果主循环来不及,可以用DMA把数据传到外设,减少CPU干预。
5. USART串口:寄存器级的收发控制
5.1 波特率计算:BRR寄存器的配置
USART的波特率由BRR寄存器决定。BRR的值 = 时钟频率 / 波特率。比如时钟72MHz,波特率115200,BRR = 72000000 / 115200 = 625。但BRR不是直接写625,而是分成整数部分和小数部分。整数部分是625的整数部分,小数部分是625的小数部分乘以16。
具体来说,BRR的高12位是整数部分,低4位是小数部分。625的二进制是0x271,小数部分是0,所以BRR = 0x2710。如果波特率是9600,BRR = 72000000 / 9600 = 7500,二进制是0x1D4C,小数部分是0,BRR = 0x1D4C0。
我一般直接用库函数算BRR,因为手动算容易出错。但要知道原理,这样波特率不对的时候,能查出来是BRR配错了。常见的波特率错误表现为:收到的数据全是乱码,或者根本收不到数据。这时候先查BRR,再查时钟使能,最后查引脚配置。
5.2 发送与接收的标志位轮询
发送数据时,要等TXE标志位置1,表示发送数据寄存器空,可以写入下一个数据。接收数据时,要等RXNE标志位置1,表示接收数据寄存器非空,可以读取数据。这两个标志位在SR寄存器里。
轮询方式发送一个字节的代码是:while(!(USART1->SR & (1<<7))); USART1->DR = data;。接收一个字节是:while(!(USART1->SR & (1<<5))); data = USART1->DR;。注意,读DR寄存器会自动清除RXNE标志,写DR寄存器会自动清除TXE标志。
我早期用轮询方式,发现发送大量数据时CPU占用率很高。后来改用中断或DMA,CPU就解放出来了。中断方式是在TXE中断里写下一个数据,RXNE中断里读数据。DMA方式更省事,配置好DMA通道,数据自动搬运,CPU完全不用管。
5.3 中断接收与环形缓冲区
中断接收的典型问题是数据丢失。如果中断里只把数据存到一个变量里,主循环来不及处理,下一个数据来了就覆盖了。解决办法是用环形缓冲区。中断里把数据写入缓冲区,主循环从缓冲区读取。
环形缓冲区的实现是:定义一个数组和两个指针,写指针和读指针。中断里写数据,写指针加1,如果写到数组末尾就回到开头。主循环读数据,读指针加1,同样循环。当写指针等于读指针时,缓冲区空;当写指针加1等于读指针时,缓冲区满。
我一般把缓冲区大小设为2的幂次,比如64、128、256,这样可以用位与运算代替取模运算,提高效率。write_index = (write_index + 1) & (SIZE - 1);比write_index = (write_index + 1) % SIZE;快得多。
5.4 串口通信的常见故障排查
串口不通是最常见的问题。排查顺序是:先查硬件,TX和RX是否接反,地线是否共地;再查时钟,USART时钟和GPIO时钟是否使能;再查引脚配置,TX是否配成复用推挽,RX是否配成浮空或上拉输入;再查波特率,BRR是否正确;最后查中断,NVIC是否使能,中断服务函数名是否正确。
我遇到过一个案例,串口能发不能收。查了半天,发现RX引脚配成了推挽输出,而不是输入。推挽输出时,引脚被MCU驱动,外部信号进不来。改成浮空输入就好了。这个错误很隐蔽,因为TX能发,说明USART基本配置是对的,问题只出在RX引脚上。
还有一个案例,串口收到的数据偶尔出错。用示波器看波形,发现波特率有偏差。算了一下,BRR的小数部分舍入误差导致实际波特率偏离标准值。后来换了晶振频率,让BRR能整除,问题解决。所以选晶振的时候,要考虑常用波特率的分频需求。
6. 寄存器调试的实战心法
6.1 用调试器直接查看寄存器
调试的时候,不要只盯着变量看,要直接看寄存器。Keil和IAR都支持在调试模式下查看外设寄存器。比如你怀疑GPIO没配置对,就直接看GPIOA->CRL的值,对照手册看每一位是否符合预期。这比在代码里加printf快得多。
我习惯在调试时打开寄存器窗口,把常用的几个外设寄存器加到watch里。比如GPIOA->ODR、TIM2->CNT、USART1->SR。程序运行时,这些值实时变化,一眼就能看出问题。比如TIM2->CNT不变化,说明定时器没使能;USART1->SR的RXNE一直是0,说明没收到数据。
6.2 寄存器读改写与位带操作
前面说过,读改写不是原子操作。除了BSRR/BRR,STM32还提供了位带操作。位带区把每个位映射到一个32位地址,写这个地址就是写对应的位,而且是原子的。位带操作的地址计算公式是:别名地址 = 位带基址 + (字节偏移 * 32) + (位号 * 4)。
位带操作的好处是,可以像操作普通变量一样操作单个位,而且编译器会生成原子指令。比如PAout(5) = 1;就是置高PA5。但位带操作的可读性不如BSRR/BRR直观,我一般只在需要频繁操作单个位的时候用。
6.3 寄存器手册的查阅技巧
手册很厚,不要从头看。先看目录,找到对应的外设章节,然后看寄存器描述表。每个寄存器都有一个表,列出每一位的名称、读写权限、复位值、功能描述。重点看功能描述,理解每一位的作用。
我一般把常用的寄存器页打印出来,放在手边。比如GPIO的CRL、CRH、ODR、IDR、BSRR、BRR,TIM的CR1、SR、CNT、PSC、ARR、CCR,USART的SR、DR、BRR、CR1。这些寄存器覆盖了80%的日常操作。剩下的用到再查。
提示:手册的英文版比中文版更新快,而且翻译版偶尔有歧义。如果英文还行,建议直接看英文版。我遇到过中文版描述模糊,看英文版才明白的情况。
6.4 从库函数到寄存器的映射方法
如果你已经会用库函数,想过渡到寄存器操作,有个简单的方法:看库函数的源码。比如GPIO_Init()函数,里面就是根据参数计算CRL/CRH的值,然后写寄存器。你看懂了库函数怎么写的,就知道寄存器该怎么配了。
我当初就是这么学的。把GPIO_Init()、TIM_TimeBaseInit()、USART_Init()这几个函数的源码看了一遍,寄存器配置就全明白了。库函数是很好的教材,它把手册里的规则用代码实现了出来。看懂了库函数,再回头看手册,会有豁然开朗的感觉。
6.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| GPIO输出无变化 | 时钟未使能 | 查RCC->APB2ENR对应位 |
| GPIO输出无变化 | 模式配错 | 查CRL/CRH对应位 |
| 中断不触发 | NVIC未使能 | 查NVIC->ISER对应位 |
| 中断反复触发 | 标志位未清 | 查EXTI->PR或外设SR |
| 定时器不计数 | 定时器未使能 | 查TIM->CR1的CEN位 |
| PWM无输出 | 输出比较未使能 | 查TIM->CCER的CCxE位 |
| 串口乱码 | 波特率不对 | 查BRR值和时钟频率 |
| 串口收不到 | RX引脚模式错 | 查CRL/CRH对应位 |
| 程序卡死 | 中断函数名错 | 查启动文件向量表 |
| 变量值不变 | 未加volatile | 检查变量定义 |
这张表是我这些年踩坑总结出来的,覆盖了大部分常见问题。遇到问题先查表,能省不少时间。当然,具体问题还要具体分析,表只是提供排查方向。
6.6 寄存器操作的性能考量
直接操作寄存器比调用库函数快,这是肯定的。但快多少?我实测过,GPIOA->BSRR = (1<<5);编译成两条指令:加载地址、写数据。而GPIO_SetBits(GPIOA, GPIO_Pin_5);编译成十几条指令,包括参数检查、地址计算、读改写。在72MHz的STM32上,前者约30ns,后者约200ns。差距很明显。
在高速中断或紧密循环里,这个差距会累积。比如一个1MHz的PWM中断,每次中断都要翻转引脚,用库函数可能来不及,用寄存器就绰绰有余。所以性能敏感的地方,一定要用寄存器。
但也不是所有地方都要用寄存器。初始化代码、不频繁执行的代码,用库函数完全没问题,可读性更好,开发效率更高。我的原则是:初始化用库,关键路径用寄存器;可读性优先,性能其次,除非性能确实不够。
6.7 寄存器代码的可维护性
直接写寄存器有个缺点:可读性差。GPIOA->CRL = 0x44444444;这行代码,不查手册根本不知道在干什么。所以我在写寄存器代码时,会加详细的注释,或者用宏定义封装。
比如:
#define GPIOA_PIN5_OUTPUT_PP_2MHZ (GPIOA->CRL &= ~(0xF << 20), GPIOA->CRL |= (0x2 << 20))这样代码里写GPIOA_PIN5_OUTPUT_PP_2MHZ;,既直观又高效。宏定义在编译时展开,没有函数调用开销。我一般把常用的寄存器配置写成宏,放在头文件里,用的时候直接调。
另外,寄存器代码要加版本注释。因为不同型号的STM32,寄存器可能略有差异。比如F1和F4的GPIO配置就不完全一样。注释里写清楚适用的型号和手册版本,以后维护的时候能省很多事。
6.8 从寄存器到整体架构的思考
学寄存器不是为了炫技,而是为了在需要的时候有能力深入底层。一个合格的嵌入式工程师,应该既能用库函数快速开发,又能在关键时刻用寄存器解决问题。这就像开车,平时用自动挡,但要知道手动挡怎么换挡,万一自动挡坏了,还能把车开走。
我在带新人的时候,会让他们先用库函数做项目,等遇到瓶颈了,再引导他们看寄存器。这样他们能体会到寄存器的价值,而不是为了学而学。有个新人,用库函数调PWM,频率总是差一点,怎么改参数都不对。后来我让他看CCR和ARR的计算公式,他自己算了一遍,发现是库函数的参数理解错了。从那以后,他就主动去啃寄存器手册了。
寄存器手册第五章的内容,看起来枯燥,但每一个位背后都是硬件的行为逻辑。理解了这些逻辑,你就不再是“调库侠”,而是真正掌控了MCU。这种掌控感,是嵌入式开发最大的乐趣之一。