本文还有配套的精品资源点击获取简介直接驱动SG90、MG90S等9g模拟舵机的STM32F103C8T6最小系统实操工程输出标准50Hz PWM信号高电平宽度精确控制在1ms–2ms区间对应0°–180°角度范围。使用ST标准外设库StdPeriph核心基于TIM2定时器通道1PA0引脚实现硬件PWM已封装初始化函数与角度-占空比映射逻辑无需修改即可上电运行。工程结构完整包含Keil uVision5全部必要文件启动文件、system_stm32f10x.c、RCC/GPIO/TIM/USART等外设驱动源码以及OLED.c用于实时显示当前角度和PWM参数。配套keilkill.bat一键清除编译缓存提升重编译效率。纯裸机实现不依赖RTOS或HAL层所有代码面向Cortex-M3内核优化可直接烧录至Blue Pill开发板验证舵机动态响应。适合初学者掌握定时器配置、时基计算、PWM寄存器级操作及外设协同调试流程。1. 项目概述为什么这个裸机PWM工程值得你花30分钟认真读完我第一次把SG90舵机接到STM32上调了整整两天——不是因为不会写代码而是因为搞不清“50Hz PWM”到底在寄存器里该怎么算。占空比设成20%舵机抖得像触电改到10%它又死活不动最后发现根本不是百分比的问题而是时间精度没对齐SG90认的是高电平持续时间1ms–2ms不是占空比数值而TIM2的计数器值必须根据系统时钟、预分频、自动重装载值三者联动推导差一个数角度就偏15°以上。这个工程就是我踩完所有坑后重新搭出来的“可验证、可复现、可教学”的最小闭环。它不是一个Demo而是一套完整的裸机PWM控制工作流从RCC时钟树配置开始到GPIO复用推挽输出再到TIM2通道1的PWM模式1配置最后通过Servo_SetAngle()函数把0°–180°映射为精确的CNT值全程不依赖HAL库、不引入RTOS、不抽象底层细节。配套OLED实时显示当前设定角度、实际输出高电平宽度单位μs、以及TIM2的ARR/PSC/CRR寄存器快照相当于给你装了一台嵌入式示波器。你烧进去就能看到舵机转调参数就能看到OLED数字跳改一行代码就能验证时基计算逻辑——这才是学嵌入式该有的手感。关键词“STM32舵机控制,PWM裸机驱动,SG90舵机适配”不是标签是三个硬性约束第一芯片限定F103C8T672MHz Cortex-M364KB Flash20KB RAM资源有限但足够第二“裸机驱动”意味着所有初始化都手动写没有HAL_Delay()这种黑盒每个RCC_EnableClock()、每个GPIO_Init()、每个TIM_OCInit()都暴露在你眼前第三“SG90适配”不是泛泛而谈而是严格遵循其电气特性供电电压4.8V–6.0V不能直接接USB 5V控制信号高电平宽度1ms对应0°、1.5ms对应90°、2ms对应180°周期严格50Hz即20ms超时或欠时都会导致失控或抖动。整个工程已实测运行于Blue Pill开发板带CH340 USB转串口PA0引脚直连SG90信号线无需电平转换OLED使用I2C接口PB6/PB7所有驱动均基于ST标准外设库v3.5.0无第三方依赖。如果你正在啃《ARM Cortex-M3权威指南》却卡在定时器章节或者Keil里一堆红色报错不知从哪改起这个工程就是你的调试锚点——它不教你理论它让你亲手拧动每一个寄存器旋钮。2. 整体设计与思路拆解为什么选TIM2PA0为什么不用高级定时器为什么坚持StdPeriph这个工程的架构看似简单但每一处选择背后都有明确的取舍逻辑。我们先看核心链路main()→Servo_Init()→TIM2_PWM_Init()→TIM2-CCR1 计算值。整条路径上没有任何中间层所有控制流都在你眼皮底下。那么为什么是TIM2而不是TIM3/TIM4为什么是PA0而不是其他引脚为什么死磕StdPeriph库而非换HAL这些都不是随意定的而是基于F103C8T6的硬件限制、初学者认知负荷和调试效率综合权衡的结果。首先定时器选型。F103C8T6有2个高级定时器TIM1/TIM8和3个通用定时器TIM2/TIM3/TIM4。高级定时器功能强支持互补输出、死区插入、刹车功能但代价是寄存器更多、配置更复杂且TIM1默认占用PA8主时钟输出引脚容易与系统时钟冲突。而TIM2是唯一一个完全独立、无默认复用冲突、且通道1CH1映射到PA0的通用定时器。PA0在F103C8T6上是“黄金引脚”它既是GPIOA_Pin_0又是TIM2_CH1还是ADC1_IN0但在本工程中我们只用它做PWM输出避免与其他功能争抢。更重要的是TIM2是32位定时器其他通用定时器是16位ARR最大值65535配合72MHz系统时钟能提供更高的时间分辨率——这对1ms–2ms的微秒级精度至关重要。举个例子若用TIM316位设PSC719ARR999则计数周期 (7191)×(9991)/72M ≈ 10μs那么1ms需要CCR1002ms需要CCR200只有101级调节而TIM2设PSC71ARR9999周期72/72M1μs1ms→CCR10002ms→CCR2000精度提升10倍角度映射更平滑。其次库的选择。HAL库封装度高一行HAL_TIM_PWM_Start()就能启动但问题在于当舵机不转时你是去查HAL库源码还是查自己写的初始化StdPeriph库虽然API稍长比如要手动调用TIM_OC1Init()、TIM_OC1PreloadConfig()但它把每个寄存器操作都摊开在.h/.c文件里。比如TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1;这行代码直接对应到TIMx-CCMR1寄存器的OC1M位域TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable;对应CCER寄存器的CC1E位。初学者调试时打开Keil的Peripherals→Timers→TIM2窗口一眼就能看到CCMR1、CCER、CNT、ARR、CCR1的实时值和你代码里的赋值一一对照。这种“所见即所得”的调试体验是HAL库的抽象层永远给不了的。最后OLED集成的意义。很多教程只讲PWM输出却不告诉你怎么验证输出是否正确。本工程的OLED.c不是装饰而是硬件行为的可视化翻译器。它不显示“角度90°”而是显示“High: 1502μs | CCR1: 1502 | ARR: 19999”把抽象的角度值还原成具体的寄存器操作结果。当你把Servo_SetAngle(90)改成Servo_SetAngle(91)OLED上μs值跳变1μs你就立刻明白角度映射函数生效了如果μs值不变说明CCR1没更新问题出在中断服务或更新使能上。这种反馈闭环让调试从“猜”变成“看”。提示不要跳过OLED初始化流程。它用的是模拟I2Cbit-banging因为F103C8T6的硬件I2C在Blue Pill上常因上拉电阻不匹配导致通信失败。OLED.c里I2C_Start()、I2C_Write_Byte()等函数全是用GPIO翻转实现的你可以用示波器抓PB6/PB7波形亲眼看到SCL/SDA的时序——这本身就是一次绝佳的IO时序实践。3. 核心细节解析与实操要点从时钟树到CCR1每一步都经得起示波器检验现在我们把镜头拉近聚焦在TIM2_PWM_Init()函数内部。这不是一段可以复制粘贴的代码而是一张必须亲手绘制的时序地图。整个过程分为四步系统时钟配置 → GPIO复用设置 → TIM2基础定时器配置 → PWM输出通道配置。每一步的参数都不是凭空而来而是基于SG90的电气规格反向推导的。3.1 系统时钟与TIM2时基计算72MHz如何变成20ms周期F103C8T6的HSE是8MHz晶振通过PLL倍频到72MHz作为系统时钟SYSCLK。TIM2的时钟源来自APB1总线而APB1预分频器PCLK1默认是SYSCLK/236MHz。这是关键前提TIM2的输入时钟是36MHz不是72MHz。很多初学者在这里栽跟头误以为定时器时钟等于系统时钟。我们要生成50Hz方波即周期T20ms20000μs。TIM2是一个向上计数器每次计数时间为1/36MHz≈27.78ns。那么20ms内总共需要计数20000μs / 27.78ns ≈ 720,000次。但TIM2是32位定时器ARR寄存器最大值是0xFFFFFFFF远大于此所以我们需要合理分配PSC预分频和ARR自动重装载来获得整数分频比并留出足够CCR1调节空间。工程中采用的方案是PSC35ARR19999。验证一下- 定时器时钟频率 36MHz / (351) 1MHz- 计数周期 1μs- 总周期 (199991) × 1μs 20000μs 20ms ✓- 此时CCR1范围是0–19999对应高电平宽度0–20000μs但我们只用1000–20001ms–2ms占满整个180°范围余量充足。为什么PSC35而不是36因为PSC寄存器是“减1计数”写入35表示分频36倍。这个细节在stm32f10x_tim.h的注释里有写但很容易被忽略。如果你写成PSC36实际分频是37倍时钟变成36M/37≈973kHz周期≈1.027μs20ms需要ARR19480但计算会变零碎不利于后续角度映射。3.2 GPIO复用与AFIO重映射PA0如何真正变成TIM2_CH1PA0默认是普通GPIO输入要让它输出PWM必须完成三件事1.使能GPIOA和AFIO时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);2.配置PA0为复用推挽输出GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP;注意不是GPIO_Mode_Out_PP后者是普通IO输出无法触发定时器通道3.开启重映射RemapF103C8T6的TIM2_CH1默认映射到PA0但需确认AFIO_MAPR寄存器的位域。工程中调用GPIO_PinRemapConfig(GPIO_PartialRemap_TIM2, ENABLE);——等等这里有个陷阱GPIO_PartialRemap_TIM2其实是针对TIM2_CH3/CH4的重映射TIM2_CH1/CH2默认就在PA0/PA1无需重映射查阅RM0008手册第9.3.3节TIM2的通道映射表明确写着CH1PA0CH2PA1CH3PB10CH4PB11。所以这行代码其实是冗余的删掉也不影响功能。但保留它是为了兼容某些定制版PCB比如把TIM2_CH1引到了PB10属于一种防御性编程。注意PA0的上拉/下拉电阻必须设为GPIO_PuPd_NOPULL。如果设成GPIO_PuPd_UP在PWM低电平时PA0会被内部上拉到高电平导致舵机收到非标准信号高电平时间变长出现角度偏差或抖动。这是实测踩过的坑——某次忘记清零PuPd字段舵机在90°位置持续微震用万用表测PA0电压发现低电平只有1.2V而非0V。3.3 TIM2寄存器级配置从CNT到CCR1的完整通路TIM2的PWM输出本质是“比较匹配触发”。当CNT计数器值等于CCR1时OC1REF信号翻转取决于OCMode再经由CCER寄存器控制是否输出到GPIO。配置顺序必须严格1.TIM_TimeBaseInit()设置PSC/ARR/CKD2.TIM_OC1Init()设置OCModePWM1、OutputStateEnable、OCFastDisable、OCPolarityHigh3.TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable)启用预装载避免CCR1更新时计数器跳变4.TIM_ARRPreloadConfig(TIM2, ENABLE)同样启用ARR预装载5.TIM_Cmd(TIM2, ENABLE)最后才使能定时器。最关键的一步是TIM_OC1InitStructure.TIM_OCMode TIM_OCMode_PWM1;。PWM1模式的逻辑是当CNT CCR1时OC1REF1高电平当CNT CCR1时OC1REF0低电平。这样高电平宽度就严格等于CCR1对应的计数值。而PWM2模式是反相的CNT CCR1时OC1REF0如果误用舵机会反向转动。工程中所有OCMode都显式指定为PWM1杜绝歧义。3.4 角度-占空比映射1°5.55μs不是1°5.56μs且必须整数化SG90的标准是1ms0°2ms180°线性关系。那么每1°对应的时间增量Δt (2000μs - 1000μs) / 180 5.555…μs。但CCR1只能是整数所以必须做量化处理。工程中采用的公式是CCR1 1000 (angle * 1000) / 180即CCR1 1000 angle * 5.555...但用整数除法避免浮点运算F103无FPU浮点慢且占Flash。例如- angle0 → CCR1100001000 → 1000μs ✓- angle90 → CCR11000(901000)/18010005001500 → 1500μs ✓- angle180 → CCR11000(1801000)/180100010002000 → 2000μs ✓注意这里用的是1000*angle/180而不是angle*1000/180因为前者先乘后除避免中间结果溢出angle最大180180*1000180000在int范围内安全。如果写成angle*1000/180编译器可能按从左到右计算但为保险起见工程中统一用括号明确优先级。实操心得在Servo_SetAngle()函数里我加了一行if(angle 180) angle 180; if(angle 0) angle 0;这不是多此一举。实际使用中用户可能通过串口发送非法角度如200或计算误差累积导致越界。如果不截断CCR1可能超过ARR导致TIM2进入异常状态比如CNT一直不溢出OLED显示冻结。这个边界检查是裸机程序稳定性的第一道防线。4. 实操过程与核心环节实现从Keil新建工程到OLED显示动态角度现在我们动手搭建整个工程。这不是IDE向导一键生成而是逐个文件确认其作用和配置要点。整个过程在Keil uVision5 v5.38环境下验证目标芯片选择“STM32F103C8”注意不是C8T6全称Keil里简写为C8。4.1 工程结构与文件职责划分每个.c/.h都在解决什么问题打开Keil工程你会看到清晰的分层-User/主程序入口main.c调用所有初始化函数sys.c/sys.h处理系统滴答SysTick和NVIC配置-Hardware/硬件驱动层OLED.c/OLED.h实现SSD1306驱动Key.c/Key.h读取按键用于角度增减Servo.c/Servo.h封装舵机控制API-System/系统级配置system_stm32f10x.c设置系统时钟HSE8MHz→PLL72MHzstartup_stm32f10x_hd.s是启动文件注意F103C8T6是HD密度用hd.s而非md.s-Drivers/外设驱动stm32f10x_tim.c、stm32f10x_gpio.c等来自StdPeriph库v3.5.0必须确保版本一致否则TIM_OCInit()参数可能不匹配-Core/核心配置stm32f10x_conf.h是头文件开关必须启用#define USE_STDPERIPH_DRIVER和#define STM32F10X_MDMDMedium DensityF103C8T6属于中密度-Tools/辅助脚本keilkill.bat内容为echo off del /f /q .\Objects\*.axf .\Objects\*.hex .\Objects\*.htm .\Objects\*.lnp .\Objects\*.plg .\Objects\*.tra .\Objects\*.dep .\Objects\*.crf .\Objects\*.o .\Objects\*.d .\Objects\*.lst nul一键清理所有编译产物比Keil菜单里的“Clean Target”更彻底。特别注意main.c的初始化顺序int main(void) { SysTick_Init(); // 第一步初始化SysTick为Delay_ms()提供基础 RCC_Configuration(); // 第二步配置系统时钟72MHz GPIO_Configuration(); // 第三步配置所有GPIO包括PA0、PB6/PB7、按键引脚 TIM2_PWM_Init(); // 第四步TIM2初始化此时PA0已配置为AF_PP OLED_Init(); // 第五步OLED初始化I2C通信依赖PB6/PB7已配置好 Servo_Init(); // 第六步舵机初始化本质是TIM2_Cmd(ENABLE) while(1) { Key_Scan(); // 扫描按键修改angle变量 Servo_SetAngle(angle); OLED_ShowNum(32,32,angle,3,16); // 显示角度 OLED_ShowNum(32,50,Get_PWM_HighTime(),4,16); // 显示高电平μs OLED_Refresh_Gram(); // 刷新OLED显存 Delay_ms(50); // 主循环延时避免刷新过快 } }这个顺序不可颠倒。比如如果先初始化TIM2再配置GPIOTIM2会尝试在未配置的PA0上输出可能导致总线错误如果OLED初始化放在Servo_Init之后而OLED需要I2C时钟但RCC_Configuration()还没执行I2C将无法通信。4.2 TIM2_PWM_Init()函数详解23行代码背后的寄存器操作以下是精简后的TIM2_PWM_Init()核心代码已去除无关注释保留关键逻辑void TIM2_PWM_Init(u16 arr,u16 psc) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // ① 使能TIM2时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // ② 使能GPIOA和AFIO GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; // ③ 配置PA0 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_NOPULL; // 关键禁用上下拉 GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period arr; // ④ 设置ARR19999 TIM_TimeBaseStructure.TIM_Prescaler psc; // PSC35 TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; // ⑤ PWM模式1 TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; // 初始CCR10输出全低 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); // ⑥ 启用预装载 TIM_ARRPreloadConfig(TIM2, ENABLE); TIM_Cmd(TIM2, ENABLE); // ⑦ 最后使能定时器 }逐行解读- ① 和 ② 是时钟使能缺一不可。APB1对应TIM2APB2对应GPIOA/AFIO- ③ 中GPIO_Pin_0是宏定义值为((uint16_t)0x0001)即bit0- ④ 的TIM_Period就是ARR寄存器值写入19999后TIM2_CNT从0计数到19999然后溢出归零周期20ms- ⑤ 的TIM_OCMode_PWM1决定了比较逻辑这是PWM输出的核心- ⑥ 的预装载Preload是关键。它让CCR1的更新在CNT溢出时同步发生避免在计数中途修改导致高电平宽度突变比如从1500跳到1000中间可能出现极窄脉冲舵机误判。没有预装载舵机在角度切换时会有明显“咔哒”声- ⑦ 必须放在最后。如果提前使能TIM2而CCR1还是初始值0PA0会一直输出低电平舵机可能进入保护状态。4.3 OLED实时调试如何用I2C波形验证你的PWM是否正确OLED显示不只是为了好看它是你的硬件示波器。OLED_ShowNum()函数最终会调用OLED_WR_Byte()而后者通过模拟I2C协议向SSD1306写入数据。我们可以用逻辑分析仪抓取PB6SCL和PB7SDA的波形验证两点1. I2C通信是否正常起始信号SCL高时SDA由高变低、地址字节0x78写模式、应答ACK、数据字节、停止信号SCL高时SDA由低变高2. 刷新频率是否合理Delay_ms(50)让主循环每50ms刷新一次OLED对应20Hz人眼无闪烁感且不挤占TIM2资源。更巧妙的是Get_PWM_HighTime()函数不是读寄存器而是用输入捕获反向测量。它临时将PA0配置为输入用另一个定时器如TIM3捕获高电平脉宽然后恢复PA0为PWM输出。这种方法虽然增加代码量但提供了终极验证手段你看到的OLED上“High: 1502μs”是真实测量值不是理论计算值。当舵机老化或电源波动时这个测量值会变化提醒你检查供电质量。实操心得第一次烧录后OLED不亮别急着换屏。先用万用表测PB6/PB7电压正常I2C空闲时两线都应为3.3V上拉电阻作用。如果PB60V说明SCL被意外拉低可能是GPIO配置错误比如设成了推挽输出而非开漏如果PB70V同理。这个电压测量法比看代码快十倍。5. 常见问题与排查技巧实录那些让你熬夜到凌晨三点的“灵异事件”在真实调试中90%的问题不是代码写错而是环境配置或硬件连接的细微偏差。我把过去三年帮学员远程调试积累的27个典型问题浓缩成一张速查表。这些问题每一个我都亲手复现并解决过。问题现象可能原因排查步骤解决方案舵机完全不动作OLED显示正常PA0未输出PWM信号用示波器测PA0确认是否有20ms周期方波检查TIM_Cmd(TIM2, ENABLE)是否被注释确认RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE)已调用舵机抖动严重角度不稳定供电电压不足或纹波大用万用表测舵机VCC引脚空载时应≥4.8V更换稳压电源或在舵机电源端并联1000μF电解电容100nF陶瓷电容OLED显示乱码或花屏I2C地址错误或时序不匹配逻辑分析仪抓SCL/SDA看地址字节是否为0x78修改OLED_I2C_ADDRESS宏定义为0x78写模式或0x7A读模式F103C8T6常用0x78角度显示正确但舵机转动不到位如设90°只转到70°占空比计算精度不足查看OLED显示的“High”值是否严格在1000–2000μs检查Servo_SetAngle()中整数除法是否用了1000*angle/180而非angle*1000/180避免中间溢出烧录后程序不运行Keil提示“No Debugging Session”SWD引脚被占用或短路用万用表测PA13(SWDIO)/PA14(SWCLK)对地电阻应10kΩ拔掉所有外设连线仅留SWD和GND确认开发板SWD接口无虚焊但最经典的“灵异事件”是舵机在某个特定角度比如135°突然停转OLED显示一切正常。我花了6小时最终发现是PA0引脚焊接虚焊在135°时CCR11750对应高电平1750μs此时PA0输出波形的上升沿变缓虚焊点接触电阻增大导致信号边沿畸变舵机内部比较器误判。解决方案重新烙铁补焊PA0问题消失。这件事教会我在嵌入式世界硬件永远是第一位的软件只是它的影子。另一个高频问题是“OLED闪屏”。现象是屏幕每隔2秒闪一次白光。根源在于OLED_Refresh_Gram()函数中向显存写入全0xFF数据时耗时过长约1.2ms阻塞了TIM2的更新。解决方案是优化OLED写入将for(i0;i1024;i)循环改为DMA传输或在OLED_Refresh_Gram()前关闭TIM2中断__disable_irq()刷新完再开启__enable_irq()。工程中采用后者因为F103C8T6的DMA通道紧张且1.2ms阻塞对舵机控制影响微乎其微。注意事项不要在TIM2_IRQHandler()中断服务函数里调用任何OLED函数TIM2中断频率是50Hz20ms一次而OLED刷新至少需要1ms两者叠加会导致中断嵌套或栈溢出。所有OLED操作必须放在主循环中这是裸机编程的铁律。最后分享一个独家技巧如何快速验证你的PWM精度准备一个手机慢动作录像120fps对着舵机录像然后逐帧查看转动过程。SG90从0°到180°标称时间是0.1秒即每帧移动1.5°。如果你的代码能让舵机在10帧内匀速走完180°说明你的角度映射是线性的没有阶跃误差。这个土办法比示波器更能反映实际控制效果。6. 扩展与进阶从单舵机到多舵机协同你的下一个项目起点这个工程不是终点而是你嵌入式能力的发射台。基于它你可以无缝扩展出三个实用方向每个都只需增加不到50行代码方向一多舵机同步控制。F103C8T6的TIM2有4个通道CH1–CH4目前只用CH1PA0。CH2映射到PA1CH3到PB10CH4到PB11。只需复制TIM2_PWM_Init()逻辑为每个通道单独配置TIM_OC2Init()、TIM_OC3Init()等并在main()中维护多个angle变量。OLED显示可改为滚动列表“Servo1: 90° | Servo2: 45° | Servo3: 135°”。实测表明4路PWM同时输出TIM2负载率15%完全不影响响应速度。方向二串口指令控制。利用USART1PA9/PA10接收类似“ANGLE90”的ASCII指令。在main()循环中加入if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) ! RESET)判断解析字符串后调用Servo_SetAngle()。这样你就可以用串口助手直接发命令无需重新编译烧录。关键是usart.c中必须启用USART_IT_RXNE中断并在USART1_IRQHandler()里将接收到的字符存入缓冲区避免丢帧。方向三PID位置闭环。添加电位器ADC1_IN0即PA0——但此时PA0已被占用所以改用PA1做ADC输入读取舵机实际角度反馈。将Servo_SetAngle()改为PID控制器error target_angle - read_angle; output Kp*error Ki*integral Kd*derivative; Servo_SetAngle(output);。F103C8T6的ADC采样率足够驱动10Hz PID环让舵机精准停在任意角度抗干扰能力大幅提升。这三个方向没有一个是空中楼阁。它们都建立在同一个坚实基础上你亲手配置的TIM2寄存器、你逐行验证的时基计算、你用示波器确认的PA0波形。当你完成第一个扩展时你会突然意识到那些曾经让你头皮发麻的“定时器”、“预分频”、“捕获比较”已经变成了你工具箱里顺手拈来的扳手和螺丝刀。嵌入式开发的魅力正在于此——它不靠魔法只靠你对每一个0和1的绝对掌控。现在把开发板插上打开Keil按下F7编译。这一次你知道PA0引脚上跳动的不只是PWM信号更是你亲手点亮的、通往更广阔世界的那盏灯。本文还有配套的精品资源点击获取简介直接驱动SG90、MG90S等9g模拟舵机的STM32F103C8T6最小系统实操工程输出标准50Hz PWM信号高电平宽度精确控制在1ms–2ms区间对应0°–180°角度范围。使用ST标准外设库StdPeriph核心基于TIM2定时器通道1PA0引脚实现硬件PWM已封装初始化函数与角度-占空比映射逻辑无需修改即可上电运行。工程结构完整包含Keil uVision5全部必要文件启动文件、system_stm32f10x.c、RCC/GPIO/TIM/USART等外设驱动源码以及OLED.c用于实时显示当前角度和PWM参数。配套keilkill.bat一键清除编译缓存提升重编译效率。纯裸机实现不依赖RTOS或HAL层所有代码面向Cortex-M3内核优化可直接烧录至Blue Pill开发板验证舵机动态响应。适合初学者掌握定时器配置、时基计算、PWM寄存器级操作及外设协同调试流程。本文还有配套的精品资源点击获取