STM32 GPIO与定时器深度解析:从基础配置到实战应用
1. 项目概述:从点灯到心跳,STM32的基石
如果你刚开始接触STM32,或者已经用库函数点过灯,但总觉得对GPIO和定时器的理解还浮在表面,那这篇分享就是为你准备的。我见过太多项目,初期功能跑得飞快,一到后期就出现按键失灵、PWM输出抖动、定时不准等“玄学”问题,根源往往就在于对这两个最基础外设的掌握不够扎实。GPIO是单片机与外界沟通的物理桥梁,而定时器则是单片机协调内部运作、精准计时的“心脏”。搞懂它们,你才能真正驾驭STM32,而不是仅仅在调用API。
很多人觉得GPIO不就是配置个输入输出嘛,定时器不就是计个数嘛。但实际项目中,你需要考虑GPIO的驱动能力是否足够点亮你的LED阵列,输入模式的上拉电阻要不要加、加多大才能稳定读取按键,中断触发沿的噪声容限是多少。定时器更是如此,如何用最少的资源实现多路不同频率的PWM?输入捕获测量高频信号时如何避免溢出?这些细节,数据手册不会直接告诉你答案,但却是项目稳定性的关键。接下来,我会结合我踩过的坑和实战经验,带你深入STM32的GPIO和定时器世界,不仅知道怎么配置,更明白为什么要这样配置,以及如何应对各种复杂场景。
2. GPIO深度解析:不仅仅是“开”和“关”
2.1 GPIO的8种工作模式及其本质
STM32的GPIO功能强大,绝不止简单的数字输入输出。其8种工作模式(输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、推挽式复用功能、开漏式复用功能)常常让新手困惑。其实,理解它们的关键在于从单片机内部电路结构出发。
推挽输出是最常用的输出模式。你可以把它想象成一对“推”和“挽”的MOS管。当输出高电平时,上管导通,下管关闭,IO口被“推”到VDD电压(如3.3V);输出低电平时,上管关闭,下管导通,IO口被“拉”到VSS(0V)。这种结构优点是驱动能力强,高低电平明确。但注意,它不能直接用于“线与”逻辑,因为如果两个推挽输出引脚一个输出高一个输出低并短接,会形成短路,产生大电流可能损坏芯片。
开漏输出则只包含下拉MOS管,没有上拉管。当输出逻辑‘1’时,下管关闭,引脚相当于断开(高阻态);输出逻辑‘0’时,下管导通,引脚拉低。因此,开漏输出要产生高电平,必须外接上拉电阻。它的优点是便于实现“线与”(多个开漏输出接在一起,只要有一个为低,总线就是低),并且可以方便地实现电平转换(比如用3.3V单片机通过开漏输出控制5V器件,外接一个5V的上拉电阻即可)。
输入模式的选择至关重要。浮空输入模式下,引脚内部既不上拉也不下拉,电平完全由外部电路决定。如果外部是悬空的(比如一个未按下的独立按键),引脚电平就是不确定的,极易受干扰。因此,对于按键、开关等输入,务必使用上拉或下拉输入模式,给引脚一个确定的默认状态。上拉电阻通常内置,约40kΩ,对于一般按键足够,但如果线路很长或环境干扰大,可能需要额外加强硬件滤波。
模拟输入模式会断开内部的施密特触发器和上下拉电阻,信号直接通往ADC或比较器。这是进行ADC采样或OPAMP输入的唯一正确模式。如果误配置为数字输入模式,轻则采样不准,重则可能因为内部钳位二极管导致信号失真甚至损坏前端传感器电路。
注意:GPIO模式切换不是瞬间完成的。在高速切换GPIO模式(比如从输出瞬间切换到输入捕获)时,需要插入少量延时或检查相关寄存器标志位,确保切换稳定,否则可能读到错误电平。
2.2 GPIO初始化参数详解与配置实战
使用HAL库或标准库时,初始化结构体里的几个参数直接决定了GPIO的行为特性,不能想当然。
GPIO_Pin: 这个简单,选择引脚。但要注意,STM32的GPIO是分组的(GPIOA, GPIOB…),每组最多16个引脚(0-15)。使用
GPIO_PIN_All来初始化整组端口时要格外小心,通常只在硬件复位后统一初始化时使用。GPIO_Mode: 如上节所述的工作模式。这里有一个常见误区:复用功能模式(Alternate Function)。当你想用引脚作为USART的TX、定时器的CH1等非通用IO功能时,必须选择对应的复用模式(推挽复用或开漏复用)。仅仅配置成普通输出模式,外设是无法控制这个引脚的。
GPIO_Speed: 输出速度配置。这个参数控制的是IO口内部驱动电路的压摆率(Slew Rate),即电平翻转的速度。速度越高,边沿越陡峭,信号高频分量越丰富,但带来的电磁干扰(EMI)也越大,功耗也略高。
- 低速(2MHz): 适用于LED、按键等低速设备,有利于降低EMI。
- 中速(10MHz/25MHz): 通用选择,适用于USART、I2C(标准模式)等中速通信。
- 高速(50MHz/100MHz): 必须用于SPI、SDIO、高频PWM、外部中断等对信号边沿要求高的场合。如果SPI通信速率设置很高,但GPIO速度配置为低速,会导致波形畸变,通信失败。
GPIO_Pull: 上拉/下拉电阻选择。对于输入模式,这是消除不确定状态的关键。对于输出模式,在开漏输出时必须外接(或内部若支持则配置)上拉电阻;推挽输出模式下,此配置通常无效,但有些型号为了功耗,在进入睡眠模式时可能会用到。
一个完整的按键输入(带上拉)与LED输出配置示例(HAL库):
// 按键GPIO初始化 (假设接在PC13,低电平有效) GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_13; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; // 输入模式 GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉,默认高电平 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 输入模式速度可配低 HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); // LED GPIO初始化 (假设接在PA5,高电平点亮) GPIO_InitStruct.Pin = GPIO_PIN_5; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出 GPIO_InitStruct.Pull = GPIO_NOPULL; // 推挽输出无需上下拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM; // LED用中速即可 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);2.3 外部中断(EXTI)与事件唤醒
GPIO的另一个高级功能是触发外部中断或事件,这对于低功耗和实时响应至关重要。STM32的EXTI控制器可以将任意GPIO引脚(通常每组只能选一个同编号引脚,如PA0/PB0/PC0共享EXTI0)配置为中断源。
关键配置点:
- 触发沿: 上升沿、下降沿或双边沿触发。对于按键消抖,通常采用下降沿(按下时)或上升沿(释放时)触发,然后在中断服务函数中延时消抖。更可靠的做法是使用定时器配合GPIO状态进行软件消抖。
- 中断与事件的区别:
- 中断: 触发后,会跳转到中断服务函数(ISR)执行代码,需要CPU介入。
- 事件: 触发后,直接唤醒处于低功耗模式的CPU,或者触发其他外设(如DMA、ADC)开始工作,不经过中断服务函数,不占用CPU时间。这在需要超低功耗或精确定时触发ADC采样时非常有用。
- 中断优先级(NVIC): 必须配置好NVIC,否则中断无法响应。对于实时性要求高的按键或信号,可以设置较高的抢占优先级。
一个常见错误:在中断服务函数中执行耗时操作(如HAL_Delay、打印日志)。这会导致其他低优先级中断被阻塞,系统响应变慢。正确的做法是在中断中仅设置标志位,在主循环中处理具体逻辑。
3. 定时器(TIM)核心原理与应用拆解
3.1 定时器的基础架构与时钟树
STM32的定时器种类繁多(基本定时器、通用定时器、高级定时器),但核心架构相通:一个计数器、一个预分频器、一个自动重载寄存器构成了主心骨。
时钟源是定时器运行的动力。它可能来自:
- 内部时钟(CK_INT): 即APB总线时钟。这是最常用的源。这里有个关键点:当APB预分频器系数不为1时,定时器实际得到的时钟是APB时钟的2倍。例如,系统时钟72MHz,APB1时钟36MHz(二分频),挂载在APB1上的定时器(如TIM2)实际时钟是72MHz。这个细节在计算定时精度时非常重要。
- 外部时钟模式1: 来自特定的外部输入引脚(TIx)。
- 外部时钟模式2: 来自ETR引脚。
- 内部触发输入(ITRx): 一个定时器可以同步另一个定时器。
**预分频器(PSC)**是一个16位或更宽的分频器,用于对时钟源进行分频。定时器时钟 = 时钟源频率 / (PSC + 1)。PSC寄存器决定了计数器的“滴答”速度。
**计数器(CNT)**在定时器时钟驱动下,从0开始向上、向下或向上/向下计数。
自动重载寄存器(ARR)设定了计数器的周期。当CNT计数到ARR值(向上计数)或从ARR值减到0(向下计数)时,会产生一个更新事件(UEV),并可选地产生更新中断。此时CNT会被重置(或从0开始重新计数)。定时周期 = (ARR + 1) * (PSC + 1) / 时钟源频率。
3.2 定时器的四大核心功能模式
3.2.1 定时与中断
这是最基本的功能。配置好PSC和ARR,使能更新中断,就可以实现周期性定时。例如,要生成一个1ms的中断(系统时钟72MHz):
- 目标定时器时钟频率 =
1 / 1ms = 1000Hz。 - 分频后计数频率 =
72MHz / (PSC+1) = 1000Hz * (ARR+1)。 - 为了得到整数,通常先设定ARR(决定分辨率),再反算PSC。设ARR=999,则计数频率需为
1000Hz * (999+1) = 1MHz。 - 所以 PSC =
72MHz / 1MHz - 1 = 71 - 1 = 70。 - 验证:
(70+1)*(999+1)/72MHz = 71*1000/72,000,000 ≈ 0.000986s ≈ 1ms(存在微小误差,因72MHz不能被完美分频到1MHz,实际为1.00056ms,对于大多数应用可接受)。
在中断服务函数中,可以执行周期任务,如扫描按键、更新显示数据。务必记得清除中断标志位,否则会连续进入中断。
3.2.2 PWM输出
PWM是控制LED亮度、电机速度、舵机角度的利器。STM32的定时器通过捕获/比较寄存器(CCR)和输出比较单元来生成PWM。
工作原理:计数器CNT不断与ARR和CCRx寄存器比较。
- 当CNT < CCRx时,输出一种电平(如高电平)。
- 当CNT >= CCRx时,输出另一种电平(如低电平)。
- 如此循环,就产生了占空比 =
CCRx / (ARR+1)的PWM波。
关键配置:
- PWM模式: PWM模式1或2,决定了有效电平是高还是低。
- 输出极性: 可以设置空闲状态或反转输出。
- 预装载使能: 通常要使能CCRx寄存器的预装载功能,这样你可以在任何时候更新CCRx的值,但新值只在下一个更新事件时才生效,避免了PWM周期中产生毛刺。
高级技巧: 使用一个定时器的多个通道输出同频率、不同占空比的PWM,或者使用主从定时器同步,输出严格同步的多路PWM。
3.2.3 输入捕获
输入捕获用于测量脉冲宽度、频率或占空比。其原理是:当捕获引脚上出现指定的边沿(上升沿/下降沿)时,硬件会将当前计数器CNT的值锁存到对应的捕获/比较寄存器(CCRx)中,并可以产生中断。
测量频率的常用方法(测周法):
- 配置为上升沿捕获。
- 第一次捕获到上升沿,记录CCRx值T1,并切换为下降沿捕获。
- 捕获到下降沿,记录值T2,切换为上升沿捕获。
- 第二次捕获到上升沿,记录值T3。
- 高电平时间 =
(T2 - T1) * 计数周期,周期 =(T3 - T1) * 计数周期,频率 =1 / 周期。
注意事项:
- 溢出处理: 如果测量的脉冲周期很长,计数器可能溢出多次。必须在捕获中断中检查更新中断标志,并对溢出次数进行补偿。
总计数 = 溢出次数 * (ARR+1) + 本次捕获值。 - 噪声滤波: 定时器通常内置数字滤波器,可以配置为对输入信号进行N个时钟周期的采样,只有连续N次采样值一致才认为边沿有效,这能有效滤除毛刺。
3.2.4 编码器接口模式
这是读取正交编码器的绝佳方式,硬件自动根据A、B两相的边沿关系,控制计数器CNT向上或向下计数,无需软件干预。配置时需要注意极性和滤波设置,确保计数方向与实际旋转方向一致。
3.3 高级定时器特性与互补PWM
高级定时器(如TIM1, TIM8)除了具备通用定时器的所有功能,还增加了:
- 互补输出: 可以输出一对带死区控制的互补PWM(CHx和CHxN),这是驱动三相电机、H桥电路的核心。死区时间是为了防止上下桥臂同时导通造成短路而插入的短暂全关断时间,高级定时器硬件支持可编程死区生成。
- 刹车输入: 紧急情况下,通过特定引脚(BKIN)可以快速关闭所有输出,保护功率器件。
- 更灵活的主从模式: 可以实现多个定时器之间复杂的事件同步。
4. 实战:构建一个多功能信号发生器与测量仪
让我们用一个综合项目来串联GPIO和定时器的知识:设计一个基于STM32的简易DDS信号发生器,并能测量外部输入信号的频率。
4.1 系统设计与外设分配
- MCU: STM32F103C8T6(通用型,资源足够)。
- 信号输出: 使用TIM2的通道1(PA0)输出PWM,后接一个低通滤波器(RC电路)将其平滑为正弦波、三角波等模拟信号。通过改变PWM的占空比表(即DDS中的相位累加器输出幅度),可以生成不同波形。
- 波形选择与参数设置: 使用三个GPIO按键(带上拉输入,外部中断模式)分别用于选择波形(正弦、三角、方波)、增加频率、减少频率。
- 频率测量输入: 使用TIM3的通道1(PA6)工作在输入捕获模式,测量外部信号的频率。
- 显示: 使用一个OLED屏幕(I2C接口)显示当前波形类型、设定频率、实测频率。
4.2 关键代码实现与解析
1. PWM DDS波形生成核心:
// 预计算一个正弦波表,256点,值范围对应CCR寄存器值(0-ARR) #define WAVE_TABLE_SIZE 256 uint16_t sin_wave_table[WAVE_TABLE_SIZE]; uint32_t phase_accumulator = 0; // 相位累加器 uint32_t tuning_word; // 调谐字,决定频率 // 在定时器更新中断中更新PWM占空比 void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET) { __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE); // 更新相位累加器 phase_accumulator += tuning_word; // 取高8位作为波形表索引(相当于相位截断) uint8_t index = (phase_accumulator >> 24) & 0xFF; // 更新CCR值,改变PWM占空比 __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, sin_wave_table[index]); } } // 频率计算:f_out = (f_tim * tuning_word) / (2^32) // f_tim是定时器更新频率(ARR+1分频后的频率)这里利用了定时器周期中断作为DDS的时钟基准。tuning_word决定了每个周期相位增加的量,从而控制了输出频率。通过更换sin_wave_table为三角波或方波表,即可切换波形。关键点:PWM频率(即定时器更新频率)必须远高于要生成的信号频率(奈奎斯特定律),并且低通滤波器的截止频率要设置合理,才能滤除PWM载波,留下平滑的波形。
2. 输入捕获测频(带溢出处理):
volatile uint32_t capture_count = 0; volatile uint32_t overflow_count = 0; volatile uint32_t period_ticks = 0; volatile uint8_t capture_done_flag = 0; // TIM3中断处理(同时处理更新溢出和捕获) void TIM3_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(&htim3, TIM_FLAG_UPDATE) != RESET) { __HAL_TIM_CLEAR_FLAG(&htim3, TIM_FLAG_UPDATE); overflow_count++; // 记录溢出次数 } if (__HAL_TIM_GET_FLAG(&htim3, TIM_FLAG_CC1) != RESET) { __HAL_TIM_CLEAR_FLAG(&htim3, TIM_FLAG_CC1); static uint32_t last_capture = 0; uint32_t current_capture = HAL_TIM_ReadCapturedValue(&htim3, TIM_CHANNEL_1); if (last_capture != 0) { // 计算周期计数值,考虑溢出 period_ticks = (overflow_count * (htim3.Init.Period + 1)) + (current_capture - last_capture); capture_done_flag = 1; overflow_count = 0; // 重置为下一次测量准备 } last_capture = current_capture; } }在主循环中检测到capture_done_flag置位后,即可计算频率:f_measured = f_tim_clock / period_ticks。其中f_tim_clock是定时器计数器的时钟频率(经过PSC分频后的频率)。这里使用了静态变量last_capture来保存上一次捕获值,利用两次上升沿捕获的时间差计算周期。溢出处理是保证测量范围的关键。
4.3 调试心得与避坑指南
- GPIO配置冲突: 当PA0用于PWM输出(复用推挽输出)时,不能再将其初始化为普通输入或模拟输入用于其他用途。务必检查数据手册的“复用功能映射”表。
- 定时器中断优先级: 波形生成的定时器中断(TIM2)和捕获测量的定时器中断(TIM3)如果同时发生,需要合理设置优先级。通常让生成波形的中断优先级更高,以保证输出波形的连续性。测量中断可以容忍微小延迟。
- PWM滤波器的设计: RC低通滤波器的截止频率
f_c = 1/(2πRC)。要滤除PWM载波频率(比如72MHz/(PSC+1)/(ARR+1)),f_c必须远小于载波频率,但大于你希望生成的最大信号频率。通常需要计算和实验结合。 - 输入捕获的噪声: 测量高频或带噪声的信号时,务必启用定时器输入滤波功能(TIMx_CCMRx寄存器中的ICxF位),并适当设置滤波采样数。否则,噪声边沿会导致错误的捕获。
- 功耗考虑: 如果使用电池供电,在不需要测量时,可以关闭输入捕获定时器的时钟(
__HAL_TIM_DISABLE()),以节省功耗。
5. 进阶话题与性能优化
5.1 使用DMA解放CPU
在上述DDS例子中,每次更新中断都需要CPU介入计算并更新CCR寄存器。当需要生成高频、高精度波形时,这会消耗大量CPU资源。此时可以使用DMA。
思路:将预计算的整个波形表(或多个周期)存放在内存中,配置DMA为循环模式,由定时器的更新事件触发DMA,自动将波形表中的下一个数据搬运到TIMx_CCRy寄存器中。这样,整个波形生成过程无需CPU参与,CPU可以完全处理其他任务(如用户界面、通信等)。配置DMA时,需要注意数据宽度(波形表数据宽度需与CCR寄存器匹配)、传输方向(内存到外设)和循环模式。
5.2 定时器主从同步与级联
对于需要多个严格同步的PWM输出(如三相逆变器),可以使用一个定时器作为主(Master),其他定时器作为从(Slave)。
- 主定时器: 配置为输出PWM,并使其更新事件(UEV)作为触发输出(TRGO)。
- 从定时器: 配置为从模式,触发源(TS)选择为来自主定时器的ITR信号(内部连接)或外部触发输入。从定时器的计数器将在主定时器的每个更新事件时被复位或启动,从而实现完全同步。
这种硬件同步的精度远高于软件同步,避免了因中断延迟造成的相位误差。
5.3 低功耗模式下的定时器与GPIO
在电池供电设备中,合理配置GPIO和定时器对续航影响巨大。
- GPIO: 在进入睡眠、停机等低功耗模式前,将未使用的GPIO配置为模拟输入模式(如果支持)通常是最省电的,因为它断开了内部上拉/下拉电阻和施密特触发器。对于需要保持状态的引脚,则根据外部电路配置为合适的输出状态或带上拉的输入。
- 定时器: 使用低功耗定时器(如LPTIM)或通用定时器配合唤醒中断。可以将定时器配置为在特定时间后产生更新事件,并连接该事件到EXTI线,从而将CPU从深度睡眠中唤醒。唤醒后,定时器计数器依然保持正确值,可用于记录休眠时间。
6. 常见问题排查与解决方案实录
在实际开发中,GPIO和定时器的问题千奇百怪,但大多有迹可循。下面是我整理的一些典型问题及排查思路。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| GPIO输出无反应,电平不变 | 1. 时钟未使能。 2. 引脚被重映射到其他功能。 3. 配置为输入模式。 4. 引脚处于复位状态(JTAG/SWD占用)。 | 1. 检查__HAL_RCC_GPIOx_CLK_ENABLE()是否调用。2. 查数据手册“Alternate function mapping”,确认当前所用功能是否与复用冲突。 3. 检查 GPIO_InitStruct.Mode。4. 对于PA13, PA14, PA15, PB3, PB4等引脚,默认用于JTAG/SWD,需先禁用JTAG( __HAL_AFIO_REMAP_SWJ_DISABLE()或 配置AFIO寄存器)才能作为普通GPIO使用。 |
| 按键读取不稳定,偶尔误触发 | 1. 未启用内部/外部上拉下拉,引脚悬空。 2. 机械按键抖动未处理。 3. 外部干扰严重。 | 1. 确保配置了GPIO_PULLUP或PULLDOWN。2. 采用硬件消抖(RC电路)或软件消抖(检测到变化后延时10-20ms再读)。 3. 增加外部滤波电容(如104瓷片电容并联在按键两端),或启用GPIO内部数字滤波器(如果型号支持)。 |
| PWM输出频率或占空比不对 | 1. 定时器时钟源频率计算错误。 2. PSC或ARR值设置错误。 3. CCRx值超出ARR范围。 4. 输出模式(PWM1/2)或极性理解错误。 | 1. 使用HAL_RCC_GetPCLK1Freq()等函数确认实际APB时钟,并考虑倍频因素。2. 重新计算: 实际频率 = 定时器时钟 / ((PSC+1)*(ARR+1))。3. 确保 CCRx <= ARR。4. 用逻辑分析仪抓取波形,对照数据手册检查极性配置。 |
| 输入捕获测量值跳动大,不准 | 1. 信号本身有噪声或振铃。 2. 未处理计数器溢出。 3. 中断处理耗时过长,丢失边沿。 4. 定时器时钟精度不够(如使用内部RC振荡器)。 | 1. 在信号输入端增加RC低通滤波,或启用定时器输入捕获滤波器。 2. 在捕获中断中检查更新中断标志,并累加溢出计数器(如4.2节所示)。 3. 优化中断服务函数,只做最必要的标志位设置,将复杂计算移出中断。 4. 对于高精度测量,使用外部晶振作为系统时钟源。 |
| 定时器中断无法进入 | 1. NVIC未配置或未使能。 2. 中断标志未清除,导致连续进入一次后锁死。 3. 全局中断未开启。 4. 中断服务函数名与启动文件中的向量表名不一致。 | 1. 检查HAL_NVIC_SetPriority()和HAL_NVIC_EnableIRQ()是否调用。2. 在中断函数开头或结尾用 __HAL_TIM_CLEAR_IT()清除对应标志位。3. 确保在main函数早期调用了 __enable_irq()(HAL库初始化后通常已开启)。4. 对照启动文件(如 startup_stm32f103xb.s)中的向量表,确保函数名完全一致。例如TIM2_IRQHandler。 |
| 使用高级定时器互补PWM无输出 | 1. 刹车功能未禁用或配置错误。 2. 主输出使能(MOE)未开启。 3. 死区时间设置过大,导致有效脉冲完全消失。 4. 互补通道的GPIO未正确配置为复用推挽输出。 | 1. 检查TIMx_BDTR寄存器,确保刹车输入无效且刹车功能被禁用或正确配置。2. 设置 TIMx_BDTR寄存器中的MOE位为1(__HAL_TIM_MOE_ENABLE())。3. 调整死区时间寄存器 DTG的值,从0开始逐步增加,用示波器观察死区效果。4. 检查CHx和CHxN对应引脚的GPIO模式是否为 GPIO_MODE_AF_PP。 |
排查问题的黄金法则:化繁为简,分步验证。遇到复杂功能不正常,先回到最基本的功能测试:单独测试GPIO输出高低电平是否正常,单独测试定时器能否进入中断,单独测试PWM输出一个固定占空比是否有波形。使用逻辑分析仪或示波器观察实际引脚波形,是定位硬件相关问题的终极手段。寄存器视图调试也能帮你确认软件配置是否真正写入了芯片。
