资讯动态

STM32寄存器实战:从手册阅读到定时器、ADC、串口高频坑解析

发布时间:2026/10/26 17:49:58 来源:尧图企业网站定制
我在带新人的时候经常被问到一个问题为什么网上讲STM32寄存器的教程不少自己一上手还是懵后来我发现大多数人不是看不懂C语言也不是不会操作寄存器而是被“对着手册抄配置”这种学法带偏了。手册是字典不是教材你把所有位都查一遍反而记不住几个关键的东西。所以这章“白话手册”我想换个路子不按芯片外设的目录顺序一章一章念而是挑实战里真正高频踩坑的场景带着你一起读寄存器、改寄存器、排查寄存器。这一章的内容定位很明确默认你已经会新建标准库工程不会的话先回去补第一章知道GPIO怎么拉高拉低现在我们要进入那些“似懂非懂”的环节——微秒级延时怎么做、PWM频率怎么测、ADC多通道为什么老出怪问题、串口printf怎么才能不卡死、GBK中文乱码到底卡在哪。围绕这些场景我会把涉及的寄存器一个一个拆开讲清楚它们为什么这么设计、配置顺序错一个会有什么后果以及我实测下来的坑和心得。因为内容偏工程所以我会直接用RCC、TIM、ADC、USART、DWT这些外设模块来做例子配合寄存器名、位名和代码尽量做到你拿着手册和这份文档能对着芯片把问题解决掉。1. 学寄存器之前先学会“反着读手册”1.1 寄存器手册不是让你从头读到尾的很多人学寄存器第一个错误动作就是把STM32 Reference Manual参考手册当小说啃。从RM0008或者RM0033第一页开始翻翻到GPIO就已经累了看到AFIO、EXTI这些名字就开始犯困最后不了了之。我在实际带项目时总结了一套“反着读”的办法先看功能描述的第一段明白这个外设是干什么的。直接跳到寄存器描述只看复位值那几列。再回到功能描述里的“初始化步骤”Initialization and configuration小节。最后看“注意事项”Note或者带有“小心”字样的段落。为什么是这个顺序因为外设的功能框图画得再漂亮最后落到代码里就是一个一个寄存器的读写。你只要知道“要启动这个功能必须先设哪个寄存器、后设哪个寄存器”剩下的位描述都可以在有疑问的时候回来查。比如定时器你不需要背下TIM2的所有寄存器但你要知道TIM2-PSC、TIM2-ARR、TIM2-CCMR1、TIM2-CCER、TIM2-CR1这几个是核心DIER是用中断时才碰。把核心五个搞清楚定时器就已经能干活了。1.2 复位值是你最好的排障工具寄存器手册里有一列叫“Reset value”很多初学者直接跳过。大多数时候确实可以直接跳过但一旦遇到“为什么这个外设上电后行为不正常”复位值就是最关键的证据。举个例子GPIOx-CRH的复位值是0x44444444意思是上电后所有引脚都是输入模式CNF01MOD00。如果你的外部电路默认要靠引脚拉低而代码里忘了把引脚配置成推挽输出上电瞬间就可能出现意外电平变化。我做一块板子时遇到过继电器上电误动作查到最后就是IO复位状态问题不是代码逻辑问题。还有USART_CR1的复位值是0x0000所有位默认都是0包括UEUSART使能位。你写了寄存器一大堆漏了置UE位串口就是不工作而且代码编译一点问题都没有。这种“静默失败”用仿真器都难抓只有靠复位值去反推。1.3 从库函数反推寄存器的学习法如果你对寄存器实在怵头我建议换个路径先用标准外设库把功能跑通再打开库函数源码去看它到底是操作了哪些寄存器。比如GPIO_Init函数源码里无非就是读GPIOx-CRL或GPIOx-CRH然后按参数配置修改对应的位段最后写回去。你看完就会发现所谓寄存器编程就是“库函数帮你干的那件事你自己重复一遍”。这个方法见效极快因为你不必先背寄存器再写代码而是先有正确代码再反推原理。等到你哪天觉得自己不该一直依赖库函数了寄存器操作自然就会了。2. 从DWT下手给代码插上“微秒级秒表”2.1 为什么SysTick不够用很多教程教延时都是“用SysTick实现毫秒延时”这没问题。但遇到传感器时序、步进电机脉冲、超声波测距回波计时这些场合毫秒级延时根本不够看。超声波测距声波在空气中的速度大概是340米/秒1微秒走0.34毫米你要测几厘米到几米的目标分辨率至少得做到微秒级。DS18B20这种单总线器件时序要求是微秒级的比如复位脉冲至少480微秒读时隙要精确到几微秒差一点就读不出正确数据。SysTick虽然可以做到微秒级但如果你跑的是裸机代码还好说一旦上了FreeRTOSSysTick就是操作系统的“心跳”你再去占用它做精确延时系统就直接跑飞了。DWTData Watchpoint and Trace模块的CYCCNT计数器就是为了解决这个问题存在的。2.2 DWT相关寄存器配置DWT是内核调试组件的一部分不占用任何外设定时器。在Cortex-M3/M4上它有一个32位的自由运行计数器DWT-CYCCNT每过1个内核时钟周期它就加1。如果你的主频是72MHz那这个计数器每秒钟数72000000次每次计数代表约13.89纳秒。要在代码里用起来只需三步使能DWT模块CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk;复位CYCCNTDWT-CYCCNT 0;启动计数DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk;三步做完你就能在任意位置读取DWT-CYCCNT的值两次读值的差值就是间隔的CPU周期数除以主频就是秒数。2.3 封装成好用的延时和时间戳函数我常用的封装思路是static void DWT_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } static uint32_t DWT_GetCycles(void) { return DWT-CYCCNT; } void DWT_Delay_us(uint32_t us) { uint32_t start DWT_GetCycles(); uint32_t ticks us * (SystemCoreClock / 1000000UL); while ((DWT_GetCycles() - start) ticks); }这里有个非常关键的点ticks这个中间变量必须用uint32_t。如果系统主频是72MHz做1秒延时就是72000000已经超过uint16_t上限了。更隐蔽的问题是如果编译器把这个乘法优化成了更窄的类型运算延时时间会莫名其妙地翻倍或者变成乱跳的值。我踩过这个坑排查了很久才发现是变量类型的问题。还有一点值得注意DWT_GetCycles() - start在计数器回绕时依然正确。32位计数器在72MHz下大概59.6秒回绕一次但在“取差值”而不是“比大小”的写法下回绕不会造成问题。这种写法比current start ticks安全得多因为后者在回绕瞬间会直接死循环。2.4 DWT测时间戳的额外用途除了延时DWT还能用来测代码某段函数的执行时间。方法是uint32_t t1 DWT_GetCycles(); func_to_measure(); uint32_t t2 DWT_GetCycles(); printf(elapsed: %u cycles\n, (unsigned int)(t2 - t1));比如测SPI一次完整读写用了多少周期比逻辑分析仪抓波形还快。我用这个方法定位过不少性能问题比如发现某段浮点运算在未开启FPU的工程里慢到离谱就是因为编译选项没有启用FPU硬指令。3. 定时器捕获测频率从配置到中断的一整条链路3.1 为什么测频率要先搞懂时钟树热词里反复出现“stm32定时器捕获测频率”新人配置定时器最常见的问题就是“计数速度跟想象的不一样”。原因基本都出在PSC预分频的源头定时器的输入时钟不是简单等于主频。以STM32F103为例系统主频72MHzAPB1总线时钟是36MHz但挂在APB1上的定时器TIM2-TIM7的内部时钟是APB1时钟的2倍也就是还是72MHz。这个“2倍频”机制是很多新人栽跟头的地方你以为从RCC那里查APB1是36MHz就直接用结果定时器实际时钟是72MHzARR算出来的溢出时间差了一倍。解决办法很直接查看RCC_CFGR寄存器的PPRE1位段。如果APB1预分频系数不是1即PPRE1非0定时器时钟就等于APB1时钟的2倍如果APB1预分频是1定时器时钟就等于APB1时钟。这是参考手册里明确写的但很多人都没注意到。我有一次用定时器测量PWM频率怎么算怎么对不上最后就是这里出的问题。3.2 输入捕获寄存器配置到底要写什么假设我们用TIM2的CH1也就是PA0引脚来捕获外部方波信号的频率。寄存器层面的配置顺序是RCC-APB1ENR | RCC_APB1ENR_TIM2EN;打开TIM2时钟。RCC-APB2ENR | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN;打开GPIOA和AFIO时钟。GPIOA-CRL设置PA0为输入浮空模式CNF01MOD00或带上拉CNF10MOD00具体看外部信号特性。TIM2-PSC 71;这样计数器时钟就变成1MHz也就是1次计数代表1微秒。TIM2-ARR 0xFFFF;用满16位自动重装载。TIM2-CCMR1 | TIM_CCMR1_CC1S_0;把CH1配置为输入模式映射到TI1。TIM2-CCER | TIM_CCER_CC1E;使能捕获通道。TIM2-DIER | TIM_DIER_CC1IE;开启捕获中断。TIM2-CR1 | TIM_CR1_CEN;最后启动定时器。这里有一个初学者容易犯的原则性问题寄存器使能的顺序不能乱。很多人为了少写几行直接先开CR1再配CCMR1结果捕获功能不生效或者第一次捕获到的值是奇怪的数字。我的理解是定时器的主开关CR1一旦打开内部的预装载、比较、捕获逻辑就开始按旧配置运行了你再改配置部分位会等到下一次更新事件才生效就会造成“配了但没完全配”的尴尬状态。CC1S位段要设为01才能把通道映射到TI1。如果不设置默认是00也就是通道被禁用为输出比较模式捕获根本不会发生。这类寄存器位的语义如果不查手册光靠拼凑代码是猜不出来的。3.3 计算频率时的边界情况配置完捕获中断处理函数里常见写法是void TIM2_IRQHandler(void) { if (TIM2-SR TIM_SR_CC1IF) { uint16_t capture TIM2-CCR1; // 处理capture TIM2-SR ~TIM_SR_CC1IF; } }这里有个细节读取TIM2-CCR1的动作本身就会清除CC1IF标志手册里叫“read to CCR1 clears capture event”。如果你再手动清一次SR反而可能把刚捕获的事件误清掉。我在调试时遇到过一种奇怪现象捕获中断总是丢掉第一个上升沿后来发现就是因为我在读CCR之后又多写了一行清标志的代码导致捕获事件被重复确认了一次。如果测量的是低频信号你还需要考虑计数器溢出。比如1MHz计数速率下16位定时器65.535毫秒就会溢出。如果你测的是50Hz方波周期20毫秒还在范围内但如果测的是5Hz方波周期200毫秒等你捕获到下一次上升沿时计数器可能已经翻了好几轮。这种情况就需要开启更新中断在溢出中断里记录溢出次数用公式总周期 溢出次数 * (ARR 1) 当前CCR值来还原真实周期。3.4 从单通道捕获扩展到多通道测频率如果想要同时测两路PWM的频率配置第二个通道的方法几乎一样只是把寄存器操作换成TIM2-CCMR1里对应CH2的位段以及TIM2-CCER里的CC2E位。注意CCMR1这个寄存器同时管理CH1和CH2的配置位段[7:4]是CH2的CC2S、IC2F、IC2PSC位段[3:0]是CH1的。你不能只设置CH1的位就假装CH2不受影响修改时要读-改-写否则会把CH1误改掉。多通道测频率时还有一个常见的坑两个通道如果接的都是同一个信号源可能会出现两个捕获值相差一个固定值。这通常不是因为配置错了而是因为两个通道的输入滤波器和预分频设置不同造成信号经过内部路径时的延迟不同。一般不影响使用但如果你要精确定相位差就得把IC1F和IC2F滤波设置成一致。4. ADC多通道采集切换通道的几个坑与避坑方案4.1 ADC规则组和注入组的区别以及为什么要知道这个热词里有一个“stm32 adc切换通道”说明关注的人不少。这个需求本身不难难的是“切换通道后值不对”。要理解这个问题先要分清ADC的两种转换组。规则组Regular Group这就是你日常用的那套一次可以配置最多16个通道依次转换转换结果放在ADC_DR数据寄存器里。规则组的特点是如果同时转换多个通道数据寄存器只有一个硬件会依次把结果放到同一个寄存器里软件必须及时读走否则会被下一次转换覆盖。注入组Injected Group类似规则组的“高优先级版”它的结果是存放在独立的ADC_JDR1到ADC_JDR4寄存器里最多4个通道不会被规则组的结果覆盖。注入组适合那些“需要紧急插入一次采样”的场景比如电机控制中突然需要采集一次母线电压。“切换通道”的常规做法是关闭ADC转换改ADC_SQR3的SQ1位段再重新启动。很多教程只说“能这么干”但没说实际操作时为什么要关闭再启动。因为ADC的通道配置是在转换开始时锁存的你改了SQR3下一次转换才会按新配置执行。如果不关转换直接改可能会让正在进行的转换按旧的配置完成后再启动时反而用了新配置行为不一致调试起来很费劲。4.2 第一个采样值不准是正常现象ADC刚启动转换时采样电容还没有充到外部信号的电压尤其是外部信号源阻抗较大时第一次转换结果会明显偏低或偏高。这不是芯片坏了是模拟电路的建立时间问题。解决办法有两种启动后先丢弃第一次转换结果把后续值当作有效数据。在硬件上靠近ADC引脚放一个0.1uF的滤波电容降低信号源阻抗。我倾向于两种都做。代码上启动后读两三次都丢掉第三次之后的数据才参与运算。硬件上如果板子空间允许0.1uF电容几乎是标配。不要只依赖代码丢弃因为外部信号本身可能带高频噪声ALE、CIC滤波做得再好模拟前端脏最后还是白搭。4.3 切换通道后的稳定时序怎么处理实际项目里我常碰到这种代码写法ADC1-SQR3 ~ADC_SQR3_SQ1; ADC1-SQR3 | channel_code; // 切换通道到CHx ADC1-CR2 | ADC_CR2_SWSTART; // 启动转换这段代码有一个隐蔽的问题你改的是SQR3的位段但没有确认上一次转换是否已经完成。如果上一次转换还在进行中这次的SWSTART可能会被忽略或者转换的还是旧通道。稳妥的写法是while (!(ADC1-SR ADC_SR_EOC)); // 等待上次转换完成 ADC1-SQR3 ~ADC_SQR3_SQ1; ADC1-SQR3 | channel_code; ADC1-CR2 | ADC_CR2_SWSTART;如果你在中断里做切换还要注意EOC和OVR标志的关系。若在上一次结果还没读走之前又开启了新转换就会触发溢出标志OVR此时数据寄存器里是最新一次的结果早期数据已经丢失。有人会在开启下一次转换前读一次ADC_DR来清空这个操作在规则组多通道扫描模式下尤其重要。我见过新生写的代码开了扫描模式却不读数据最后所有通道的采样值都挤在一个寄存器里看起来像乱码。4.4 ADC的参考电压和校准另一个隐藏大坑很多项目用ADC测电池电压算出来的值总是比万用表低零点几伏。排查到最后发现是VREF引脚的参考电压不准。ST芯片内部有一个出厂时校准的VREFINT通道可以用来反推实际参考电压。做法是在ADC配置完成后先切换通道到VREFINT读出一个校准值然后用公式算出当前VDDA再用这个VDDA做后续电压计算的比例系数。不同系列的公式不一样要看对应参考手册的“Reference voltage”章节。还有一个点ADC在STM32F1系列上有一个校准步骤在CR2里置CAL位等待CAL位清零完成。很多人用标准库时发现ADC_Calibrate这个函数要自己调用而用寄存器版时完全不记得还有这回事。我在做一款电池电量显示产品时发现满电电压在不同板子上偏差高达0.15V后来加了校准流程后偏差降到0.02V以内。这个步骤不是可选项在精度要求高的场景必须做。5. 串口printf背后那些事重定向、GBK和LD文件5.1 printf重定向的寄存器级解释几乎所有STM32新手都会遇到“printf明明写了串口就是不输出”的问题。标准做法是把fputc重定向到USARTint fputc(int ch, FILE *f) { while (!(USART1-SR USART_SR_TXE)); USART1-DR ch; return ch; }这里涉及底层寄存器TXE是发送数据寄存器空标志只有当SR里的TXE为1时你才能往DR写下一个字节否则会覆盖掉还没发完的数据。这个等待循环是必须的不是可有可无。但很多人不知道用MicroLIB和不用MicroLIB重定向的方式有差异。在Keil里勾选Use MicroLIBfputc直接就能被printf调用不勾MicroLIB需要额外处理半主机模式否则编译能过但运行时会进入HardFault或者卡死。这就是为什么搜“printf stm32”时能看到一堆“HardFault”的求助帖本质上是底层I/O机制没对上。5.2 GBK转UTF8中文乱码到底卡在哪一步热词里有“stm32 gbk转utf8”说明大家在实际产品的串口日志中遇到过中文乱码。这个问题根源在于三方的编码不一致源码文件保存的编码。编译器编译时认定的字符编码。串口终端软件显示的编码。Keil默认源码编码是ANSI也就是GBK而很多串口工具默认用UTF-8解码。你在代码里写温度%d\n编译后内存里存的是GBK字节序列终端按UTF-8去解析就出现问号或乱码。解决办法有几个层次统一源码和终端编码最省事源码存成GBK终端解码也选GBK。想用UTF-8显示把源码存成UTF-8并确认Keil能正确解析还要注意英文字符串不受影响只有中文出问题。运行时转换在日志系统里把所有中文字符串统一转成UTF-8再输出这个适合产品需要固定UTF-8协议的场景。我以前写过一版串口日志模块做法是把所有中文字符串常量直接写成UTF-8的字节数组比如\xE6\xB8\xA9\xE5\xBA\xA6这样的形式再交给终端显示彻底绕开编译器编码问题。缺点是人没法直接阅读源码里的中文字符但输出绝对不乱码。5.3 LD文件它并不神秘但你不能当它不存在热词里还有“stm32 ld文件”。很多从Keil转到GCC或PlatformIO的人第一次看到.ld文件会觉得很陌生。简单说LD文件是链接脚本告诉链接器“代码放到哪个地址、哪些段放哪里、RAM从哪里开始”。在GCC工具链里如果不小心把Flash起始地址设错固件下载进去后要么跑不起来要么在调试器里看到的PC指针完全错乱。我调试过一块使用外部Flash的板子Bootloader写好后App死活跳转不成功最后就是LD文件里FLASH区域地址设错了App链接到了从0x08000000开始的地址而实际上Bootloader占用的前8KB应该被跳过App应该链接到0x08002000。LD文件里常见的几段MEMORY声明Flash和RAM的起始地址和长度。SECTIONS把代码、只读数据、初始化数据等按段放进不同内存区域。_estack栈顶地址通常是SRAM起始地址加长度。你不需要背LD文件的语法但至少要会看MEMORY段确认芯片型号和你的实际容量匹配。如果贴片时把STM32F103C864KB Flash当成了CB128KB FlashLD文件里写的长度大于实际型号容量固件能烧进去但跑到尾巴上就会触发硬件错误这类问题查起来非常隐蔽。5.4 串口波特率偏差一个容易忽略的时钟树问题串口通信里还有个典型问题本机和对方设备波特率标称都是9600但数据偶尔错位。原因多半是主时钟频率和波特率寄存器配置不匹配。USART的波特率计算公式是BRR 时钟频率 / (16 * 目标波特率)我之前调试一个和蓝牙模块通信的项目蓝牙模块默认9600而我配置USART时用的是外部8MHz晶振但板子上实际贴的是12MHz晶振BRR算出来的数值自然不对。时钟不对串口收发全是乱码这个坑和寄存器本身无关却最让人头大。所以第一步永远是确认SystemCoreClock变量的值和实际晶振一致。6. 常见问题排查速查表从现象到寄存器级原因6.1 一张表理清“为什么没反应”现象可能原因需要检查的寄存器/位排查顺序GPIO输出不变化时钟未开启、模式配成输入RCC-APB2ENR的IOPAEN位GPIOx-CRL/CRH先查时钟再查模式定时器不动PSC/ARR写入方式有误、CR1未置位TIMx-CR1的CEN位TIMx-DIER查CEN是否置1串口完全无输出UE未使能、TXE标志误判、时钟不对USARTx-CR1的UE位SR的TXE和TC位查UE位和SR复位值ADC值跳变未等待EOC、通道切换无稳定时间ADCx-SR的EOCADCx-SQR3的SQ1加稳定延时确认EOC中断不触发NVIC未使能、中断标志未清NVIC-ISER外设SR标志先确认中断服务函数是否进入这张表的重点是排查顺序比排查项本身更重要。我见过太多人一上来怀疑寄存器配置结果其实是忘了开RCC时钟。RCC是STM32一切外设的大水闸你在调试时第一个动作永远是确认这个外设的时钟树已经打开。6.2 从“SWD无法连接”说起IO复用和调试口的冲突热词里有“stm32禁用jtag”这是很多人烧录完第一版固件后突然发现“再也连不上调试器”的经典场景。原因很简单你把PA13/PA14/PA15或者PB3/PB4这些默认的SWD/JTAG引脚重新配置成了普通GPIO或者更严重的是把这些引脚外接的设备在运行时把它们拉到了异常电平。这不算什么大问题但确实很吓人。如果你还能通过Boot0拉高进入ISP模式那还能用串口把固件清掉如果连ISP口都没有预留就只能用ST-Link的Connect Under Reset模式在复位期间把内核拉住再连。讲这个案例是想说寄存器开发不光是“配功能”也是“配安全”。把调试引脚让出来之前先确认产品已经不需要调试了或者至少留一个恢复固件的后门。我现在的习惯是把PA15、PB3、PB4当普通GPIO用时一定会在产品板上留三个焊盘万一出问题还能飞线救回来。6.3 Keil和GCC下寄存器工程的结构差异标准库新建工程这个热词背后很多人关心的其实是为什么同一个寄存器代码在Keil里能跑在GCC下编译后跑不出来除了LD文件问题外最常见的是启动文件差异。Keil的启动文件比如startup_stm32f10x_hd.s和GCC的启动文件在堆栈初始化和中断向量表布局上有些微差别。启动文件决定中断向量表放在哪默认是在Flash起始地址。如果你在GCC工程里忘了链接启动文件或者向量表偏移设错了调试器能看到程序在跑但一进中断就飞到未知地址。另外一个跨工具链的坑是volatile。寄存器地址在C语言中是通过指针访问的如果你把外设指针定义成uint32_t*而不是volatile uint32_t*在开优化等级后编译器可能把连续的寄存器读写优化掉一部分看起来就是“寄存器写了但没生效”。标准做法是#define RCC ((RCC_TypeDef *)RCC_BASE)这个宏展开出来的指针本身就是volatile的因为RCC_TypeDef结构体里的成员都是__IO uint32_t。如果你图省事直接用#define RCC_BASE 0x40021000再去访问很容易丢失volatile属性。6.4 Proteus仿真和实物板的差异别在仿真上浪费太多时间热词里出现了“proteus仿真时用正点原子的延迟函数如何设置”我想多说一句。Proteus仿真对学习逻辑很有帮助但它和实物芯片有两个大差异一是仿真模型的外设时序和真实芯片不完全一致尤其ADC的采样时间、Flash读取速度仿真里几乎都是“理想化”的二是仿真里没有真实晶振的起振过程、没有电源纹波、没有任何模拟噪声。我的建议是把Proteus当成逻辑验证工具别当成时序验证工具。尤其做PWM、测频、ADC采样这类和时序强相关的功能在仿真里调好了不等于实物能跑但在实物上调问题时先怀疑自己代码再怀疑硬件别一上来就抱怨“仿真都没问题怎么会错”——仿真本来就不负责这些问题。如果你是用正点原子的Delay函数去仿真超声波、DS18B20这类时序敏感的器件可以把延时函数里的循环变量改成大一点的值因为仿真器的指令执行效率和实物不同。但这种“调参式适配”没有迁移价值实物调试时记得改回来。7. 写在最后的一点实操体会真要说这一章教了什么我希望你记住的不只是某个寄存器的配置顺序而是一种习惯每次配置外设前先打开参考手册找到对应寄存器的复位值和位描述再对照代码检查一遍而不是凭记忆写代码。我见过太多“百度来的代码”能跑但不知道原因换一颗芯片、改一个引脚就彻底瘫痪。有一个我常用的技巧分享给你把所有寄存器操作集中到一个文件里比如hw_config.c在每个外设初始化函数开头加一段类似DEBUG_PRINTF(TIM2 Init, PSC%d, ARR%d\n, ...)的日志。成品出货时不用删只要把调试宏关掉即可。这样每次换板子、换芯片型号排查差异时日志能帮你少踩一半的坑。寄存器开发这条路前期确实比库函数多几行代码但等你熟悉以后猜问题和查问题的速度会明显比翻库快。我第一次用寄存器做ADC多通道扫描时折腾了整整一个晚上后来发现只是没等待EOC标志。那之后我养成了一个习惯每个外设配置完先读一遍状态寄存器确认硬件真的接受了配置再继续往下写。这个习惯救了我很多次。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑