单片机+舵机+PWM+独立按键:经典控制闭环实战解析
2026/9/8 2:56:13 网站建设 项目流程

简介:基于51单片机的舵机控制项目资源,演示了通过PWM脉宽调制与独立按键实现舵机角度调节的完整流程,适合入门嵌入式开发、学习定时器与IO控制的学习者参考。压缩包共14个文件,大小约24KB,内含C语言源文件、Keil工程配置、启动代码、编译生成的hex与lst清单,以及辅助设计文件等,便于对照工程结构和烧录验证。目前已有2595人学习浏览。项目详细说明了定时器配置生成可变占空比PWM、按键扫描修改占空比、进而控制舵机转角的实现思路,同时涉及独立按键消抖与中断轮询等常见处理。运用其中源码与工程配置,读者可掌握51单片机控制舵机的硬件连接与软件编写方法,理解PWM控制原理,并了解简单人机交互设计,对提升嵌入式系统综合开发能力有实用价值。 单片机加舵机加PWM加独立按键,这个组合我这两年反复搭过好几遍。最早是给课程设计做个太阳能追光演示,后来帮朋友做仿生机械臂的关节控制,再到现在带新人入门单片机,这套东西几乎是最经典的一套“输入控制执行”闭环:独立按键是人机交互入口,PWM数值变化是控制信号,舵机角度变化是最终执行结果。它不高深,但非常值得展开说,因为整个链条里每一个环节都有讲究,玩明白了,单片机外设调度能力基本就过关了。

这篇文章我按自己实际搭项目的思路来写,从方案选型到硬件接线,再到代码实现和踩坑记录,尽量把为什么这么做讲透。适合刚啃完51单片机点亮LED、正想往“稍微有点实际用处”的方向走一步的初学者,也适合拿来做课程设计或者毕业设计基础框架的同学。

1. 项目概述与整体方案设计

1.1 这个项目到底解决什么问题

很多人学单片机都是从点灯开始,点完灯就不知道该干嘛了。这个项目高级在它逼着你同时处理三件事:读按键状态、产生精确的PWM信号、让舵机转动到指定角度。三件事看似独立,实际调起来环环相扣,尤其是按键和PWM之间的配合,只要有一个地方想漏了,舵机就会乱跳。

从控制角度说,舵机是典型的“角度伺服执行器”,它靠内部电位器反馈角度误差,驱动电机往目标角度修正。你只需要给它一个占空比变化的方波,它自己会转到位。这个“输入信号简单、内部机制复杂”的特性,正好适合我们不需要深入了解舵机结构,就能做出看起来很专业的效果。所以这个项目特别适合拿来建立“通过外设控制现实物理设备”的完整认知。

1.2 为什么选独立按键而不是矩阵键盘

不少初学者一上来就想着用矩阵键盘,觉得按键多显得厉害。但在这个项目里,独立按键才是最优解。原因很简单:矩阵键盘是为了节省IO资源才采用行列扫描的方式,你得处理上拉电阻、逐行逐列扫描、按键状态保持与释放判断,这些逻辑一旦写不好,轻则按键失灵,重则扫描期间PWM信号被延时影响导致舵机抖动。

而独立按键直接一个IO接一个按键,按下就是低电平,释放就是高电平,逻辑非常干净。在这个项目里,我们需要的控制逻辑大概是“切换角度档位”或者“步进加减角度”,用两三个按键就够。省下来的IO和中断资源全部用在PWM精度和舵机控制上,整体鲁棒性高很多。

另外,矩阵键盘还有一个隐藏问题:行列扫描期间如果恰好碰上舵机需要刷新PWM脉冲,非常容易出现抖动或丢脉冲。独立按键配合外部中断或者定时扫描,可以做到对PWM信号干扰最小。如果你项目后面确实需要很多按键,建议优先考虑用移位寄存器扩展IO,或者直接上矩阵键盘,但那属于另外一个话题了。

1.3 舵机选型与PWM控制原理

目前市面上最常见、也最适合学习的是SG90舵机,价格便宜,扭矩在1.8kg·cm左右,工作电压4.8V~6V,重量只有9克左右。虽然它力气不大,但拿来驱动云台、小型机械臂关节、太阳能板追光机构完全够用。

SG90这类模拟舵机的控制信号,是一个周期20ms即频率50Hz、高电平脉宽在0.5ms到2.5ms之间变化的方波。脉宽0.5ms对应0度,1.5ms对应90度,2.5ms对应180度,中间近似线性对应。

注意:有些舵机标称脉宽范围是0.5ms到2.5ms,但实际使用中我遇到过部分批次SG90在2.3ms以上就超出机械限位,表现为电机堵转、齿轮咔咔响,这时候必须减小上限脉宽。这是非常常见的坑。

转换成占空比概念就是:周期20ms,高电平0.5ms对应占空比2.5%,2.5ms对应12.5%。如果你用单片机定时器生成50Hz的PWM,本质就是把占空比控制在2.5%到12.5%之间。理解这一点,后面所有代码都变得非常直觉化。

2. 核心硬件与接线方案

2.1 单片机的选择:51单片机还是STM32

这个项目用51单片机和STM32都能做,区别在于PWM生成的代码路径完全不同。51单片机内部没有硬件PWM输出模块,只能用定时器中断配合IO翻转来模拟PWM,好处是把PWM底层的时序原理暴露得干干净净,做完一遍你对“周期”“占空比”“中断服务函数耗时”的理解会非常深刻。坏处是51主频只有12MHz左右,定时器中断频繁触发,如果代码里有其他耗时操作,PWM脉冲宽度就会跑偏,舵机就会抖。

STM32则直接用通用定时器的PWM模式,设置好ARR、PSC、CCR三个寄存器,硬件自动输出方波,CPU完全不用管。精确度、稳定性、可扩展性都远强于51,代价是你得先理解定时器的预分频、计数周期、比较值这一套概念,对上位新手有个学习曲线。

我的建议是:如果是课程设计或者第一次做,直接用51单片机把整个流程跑通,代码量和调试难度相对低;如果基础不错想一步到位,或者后面要接多个舵机做机械臂这类更复杂的项目,直接上STM32用CubeMX生成工程,半年后你回头看会感谢自己当初这个决定。

2.2 舵机供电:永远别直接让单片机喂舵机

这是这个项目里最重要的一条硬件铁律:舵机绝对不能靠单片机开发板的3.3V或者USB口供电硬扛。SG90在空载转动时电流大约是150mA到250mA,堵转时电流能飙到600mA以上。一块STC89C52开发板的稳压芯片或者STM32板载的AMS1117-3.3,在这种电流下会发热严重甚至烧毁,更致命的是电压跌落会导致单片机复位,表现就是舵机一启动,整个系统白屏重启。

正确接法是这样的:舵机红线接5V电源正极,棕线接GND,橙线接单片机IO口。这个5V电源可以是你电脑USB的5V经过单独一根线引出,也可以是一块18650电池或者两节干电池,最稳定的是用一块单独的5V稳压模块供电,同时把电源负极和单片机开发板的GND连在一起。共地是必须的,因为PWM信号是相对GND的电压差,地不共,控制信号就完全乱套。

实测经验:我曾经用面包板直接用一个USB转TTL模块的5V引脚给SG90供电,同时给STC89C52的VCC也接在这个5V上,结果舵机一转,单片机就复位,现象就是LED重新闪起来像上电一样。后来把舵机供电分开、只共地,问题瞬间消失。

2.3 独立按键的硬件处理:上拉、下拉与消抖

独立按键的接线方式看起来简单,一个按键一端接IO,另一端接GND,但IO口的电平状态完全取决于你这个引脚有没有配置上拉。51单片机的P1、P2、P3端口内部有上拉电阻,但P0口是没有内部上拉的,如果你把按键接在P0口还不接外部上拉,按下与否电平都是不确定的。STM32则需要在CubeMX里把GPIO配置为输入模式,并启用内部上拉电阻,按下秒变低电平。

硬件上还需要考虑抖动问题。机械按键在按下和释放的瞬间,触点会反复通断几毫秒到十几毫秒,如果你的程序只检测一次电平,可能把一次按下识别成多次触发。硬件消抖最常用的是一个RC低通滤波电路,一个10kΩ电阻串联后接一个10μF电容到地,把高频抖动毛刺滤掉。不过我们做单片机项目,更常见也更灵活的方式是在软件里做消抖,10到20毫秒延时或者状态机判断,效果完全够用,还能省元件。

3. 软件设计与PWM实现

3.1 PWM产生的三种方式与选型

软件延时翻转IO是最暴力的方式,用delay延时控制高电平和低电平的时间,优点是代码直观到不能再直观,缺点是一旦有其他代码执行,延时不准确,PWM频率就飘了,舵机就跟着抖。这种方式我只建议用来验证舵机本身好坏,比如写个循环让它来回扫,不适合做稳定控制。

定时器中断翻转是我最推荐初学者掌握的。设定一个定时器中断周期比如50μs,每次中断计数器加一,当计数器小于某个阈值时IO输出高电平,大于阈值输出低电平,一个周期内高电平时间由阈值决定,这就是软件PWM。这种方法占CPU资源,但逻辑清晰,51单片机没有PWM外设时这是最优解。

STM32则直接使用硬件PWM。CubeMX里配置一个通用定时器,输出通道设置为PWM Generation,PSC预分频器把72MHz时钟分频到1MHz,ARR自动重装载值设1999,这样输出频率就是1MHz除以2000等于500Hz吗?等等,这里要再除以分频,实际上我配置ARR=1999时,如果PSC设成71,即预分频后计数时钟是1MHz,频率就是1MHz/2000=500Hz,这不符合50Hz要求。正确配置是PSC=719,计数时钟变100kHz,再算ARR=1999,频率100000/2000=50Hz。这个计算过程特别容易搞混,我单独列出来:

// STM32F103,系统时钟72MHz // 目标PWM频率50Hz,即周期20ms // 预分频PSC = 72000000 / (100000 * (1999 + 1)) - 1 // 如果PSC = 719,则计数时钟 = 72MHz / (719+1) = 100kHz // ARR = 100kHz / 50Hz - 1 = 1999 // CCR值对应脉宽:0.5ms->50,1.5ms->150,2.5ms->250

3.2 51单片机完整代码示例

51单片机没有硬件PWM模块,我用定时器0做了一个20ms周期、占空比可调的软PWM。核心思想是定时器每50μs中断一次,用一个变量累加计数值,一个周期需要400次中断,占空比比较变量决定IO高电平持续多少次中断。

// 51单片机:独立按键控制SG90舵机 // 定时器0,50us中断一次,PWM周期20ms(400次中断) // P1.0接舵机信号线,P3.2接按键1,P3.3接按键2 #include <REGX52.H> #define PWM_PERIOD 400 // 20ms周期分成400份,每份50us #define MIN_PWM 10 // 0.5ms对应角度0度 #define MAX_PWM 50 // 2.5ms对应角度180度 unsigned int pwm_count = 0; unsigned int pwm_high = 15; // 初始对应90度,即1.5ms高电平 void Timer0_Init(void) { TMOD &= 0xF0; TMOD |= 0x01; // 定时器0,模式1(16位) TH0 = 0xFF; // 12MHz,定时50us初值 TL0 = 0x9C; // 65536 - 600 = 64936 ET0 = 1; EA = 1; TR0 = 1; } void Timer0_ISR(void) interrupt 1 { TH0 = 0xFF; TL0 = 0x9C; pwm_count++; if (pwm_count > PWM_PERIOD) { pwm_count = 0; } if (pwm_count < pwm_high) { P1_0 = 1; } else { P1_0 = 0; } } void Delay10ms(void) { unsigned char i, j; i = 20; j = 113; do { while(--j); } while(--i); } void main(void) { Timer0_Init(); while(1) { if (P3_2 == 0) // 按键1按下:角度减10 { Delay10ms(); if (P3_2 == 0) { if (pwm_high > MIN_PWM) { pwm_high -= 2; // 每次减2份,对应角度减小约18度 } while(P3_2 == 0); // 等待释放,防止连击 } } if (P3_3 == 0) // 按键2按下:角度加10 { Delay10ms(); if (P3_3 == 0) { if (pwm_high < MAX_PWM) { pwm_high += 2; } while(P3_3 == 0); } } } }

这段代码的思路是:pwm_high每变化2,对应高电平时间变化100μs,角度变化约18度。数值可以根据你清晰度需求自己调整,比如想每次10度,就把步进改为约1.1份,用整数就取1或者2。51单片机上高电平IO驱动能力有限,舵机信号线要求电流很小,所以直接接没问题。

3.3 STM32实现:CubeMX配置一把梭

STM32的做法就优雅很多了。以STM32F103ZET6为例,CubeMX里做以下配置:

  • 系统时钟RCC配置为HSE外部晶振,APB1定时器时钟配置为72MHz
  • 选择一个通用定时器如TIM3,Channel 1配置为PWM Generation CH1
  • 参数配置:Prescaler=719,Counter Period=1999,Pulse设为150,代表初始1.5ms即90度
  • 舵机信号线接对应的GPIO引脚,这里以PB0为例,同时要确认TIM3_CH1的重映射情况,有些引脚需要开启AFIO重映射才能使用

生成的代码里,只需要在主循环里修改比较值就能改变舵机角度:

// 假设TIM3句柄为htim3 // 0度对应CCR=50,90度对应CCR=150,180度对应CCR=250 __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 50); // 转到0度 HAL_Delay(500); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 150); // 转到90度 HAL_Delay(500); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 250); // 转到180度

CubeMX配置的时候有一个特别容易出错的点:如果你本来计划用某个引脚,却发现它已经被别的外设占用了,最常见的原因是USART1的TX/RX默认在PA9/PA10,和TIM1的通道恰好冲突。这时候可以用重映射功能把TIM1的通道映射到别的引脚上,或者干脆换一个定时器。飞特舵机和总线舵机虽然也能用PWM控制,但它们有自己更复杂的通信协议,和普通舵机完全不同,不在本文讨论范围内。

3.4 按键状态机设计:别让延时毁了PWM

按键消抖最经典的方案是“检测到低电平后延时10ms再确认”。这个方案在点灯项目里没问题,但在本项目里要格外小心:如果你用的是51软件PWM,当你在主循环里执行Delay10ms时,定时器中断照样在跑,PWM输出不会断,所以延时消抖是安全的。但如果你把延时函数放在定时器中断里,那就大错特错了,会直接影响PWM周期,舵机瞬间抽搐。

我建议用状态机思路替代延时。按键扫描不做阻塞延时,而是记录按下时刻,利用系统tick判断是否超过10ms,既消了抖又不阻塞主循环。伪代码如下:

// 状态机按键扫描,每2ms调用一次 // 状态0:等待按下;状态1:确认按下 unsigned char key_state_1 = 0; unsigned char key_scan(unsigned char key_input) { unsigned char press_event = 0; switch(key_state_1) { case 0: if (key_input == 0) // 读到低电平,表示按下 { key_state_1 = 1; // 进入确认状态 } break; case 1: if (key_input == 0) // 再次读到低电平 { press_event = 1; // 确认有效按下事件 key_state_1 = 2; // 等待释放 } else { key_state_1 = 0; // 是抖动干扰,回到初始状态 } break; case 2: if (key_input == 1) // 等待释放 { key_state_1 = 0; } break; } return press_event; }

这种方式的好处是按键扫描过程中,PWM不受任何阻塞影响,时间精度稳稳的。后来我把这个状态机移植到了STM32版本里,逻辑完全通用,只是把2ms的调度放到了SysTick中断里。

4. 常见问题与排查技巧

4.1 舵机抖动的真凶:供电和占空比抖动

舵机抖动是最常见的问题,而且绝大多数时候源头不在代码,在供电。前面说的单独供电、共地操作一定要做扎实。另外面包板也是重点怀疑对象:面包板内部金属簧片接触电阻大,SG90连续转动时电流大,容易在簧片上产生电压降,导致舵机供电电压波动,表现为转动过程中啸叫或者抖动。我遇到过好几次这种问题,最后都是把舵机信号线和电源线直接焊接到洞洞板上解决的。

如果确认供电没问题,再看PWM信号本身。用示波器看舵机信号线的波形,如果高电平脉宽在跳动且没有规律,就是占空比设置不稳定,通常是因为中断服务函数里执行了耗时操作,导致高电平时间被拉长或缩短。尤其是使用软件PWM时,中断函数里只做IO翻转和计数比较,千万别放其他逻辑。

4.2 PWM“占空比100%”的邪门问题

有朋友用STM32调PWM控制电机,遇到一个怪现象:设置占空比到100%时,输出不是常高,而是直接没有输出、或者变成常低。这个问题的根源在于定时器的输出极性设置。在CubeMX里PWM Generation有两个选项:Active High和Active Low。如果你选了Active Low,那么CCR值等于ARR时,输出的是持续低电平,看起来反而是“没有PWM”,舵机完全不转或者转到最低限位。

解决办法是检查定时器输出比较极性,或者检查CCR值是否超过了ARR。另外有些芯片的定时器在CCR和ARR相等时,比较事件触发的行为有特殊规定,需要参考数据手册。遇到这类问题,先用示波器或者逻辑分析仪看波形,别猜。

注意:逻辑分析仪可能是我做单片机调试以来性价比最高的工具,几十块钱就能买一个采样率几十M的,舵机PWM这种50Hz信号绰绰有余。Keil5软件仿真里也能查看PWM波形,但毕竟是仿真,和实测还是有差距。

4.3 按键误触、连击与“优先级”问题

独立按键按下一次,舵机连续跳两个角度,这是消抖没做干净。我见过很多新手只在检测到按下时延时10ms,但没等到按键释放就结束了一次扫描,结果松手时候的抖动又被识别成第二次按下。解决方法是状态机里增加“等待释放”状态,或者按下处理完后加while等待按键释放。

还有一个容易被忽略的点:两个按键同时按下的情况。控制逻辑里如果只是两个独立的if,同时按下时两个条件都成立,舵机就会同时响应两个操作,表现是角度瞬间跑飞。解决方法是设计成if else if结构,给按键设定优先级,或者在入口处用互斥标志,同时只允许一个按键生效。

4.4 PWM故障保护:不合法占空比的处理

PWM信号在设计时就应该考虑故障保护。舵机的合法脉宽范围是0.5ms到2.5ms,也就是我代码里的MIN_PWM到MAX_PWM。一旦程序跑飞或者外界干扰导致CCR值超出这个范围,舵机可能转到机械极限位置堵转,时间长了会烧舵机。

我的做法是在设置占空比的接口函数里加一个限幅判断,超出范围就钳位到边缘值,或者在主循环里周期性地检查当前角度值是否合理,发现非法值就强制恢复到一个安全角度比如90度。STM32的定时器还支持刹车功能Break Function,配合故障输入引脚可以在异常信号时自动封锁PWM输出,这个功能在电机驱动里是标配,做舵机控制系统时可以留个心眼。

我在实际项目里还习惯加一个“开机自检”流程:上电后舵机先回中到90度,再根据按键状态决定后续动作。这样即使上次断电前舵机停在某个极端角度,重新上电也不会猛冲到极限位置,对机械结构和舵机本身都是一种保护。

写在最后

这套“单片机+舵机+PWM+独立按键”玩熟了之后,你会发现它的可扩展性远超预期。把按键换成传感器,就是智能追光系统;把单个舵机扩成六个,加上上位机通信,就是入门级机械臂;把PWM输出接到电机驱动模块,就是小车底盘和云台的核心控制逻辑。也就是说,这个项目最大的价值不是“做出了一个会转的舵机”,而是它帮你把单片机最核心的资源和外设之间如何协同这件事吃透了。

如果让我给新手一句劝告:不要急着上STM32的HAL库和各种封装好的框架,先把定时器中断和PWM的原理在51上摸一遍,把按键消抖的状态机逻辑在纸上画一遍,然后再去用CubeMX生成工程,你会发现自己对每一个寄存器配置都有一种“我知道它在干嘛”的踏实感。这种底层的感觉,是用再高级的库都换不来的。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询