简介:这份资源面向单片机初学者与嵌入式入门开发者,围绕51单片机、舵机、PWM与独立按键展开,帮助读者理解如何用定时器产生可变占空比的PWM信号,并通过按键实时调整舵机转角,解决从理论到硬件控制的落地问题。压缩包共14个文件,约24KB,包含C语言源码、A51启动汇编、uvproj与uvopt工程配置、hex烧录文件,以及obj、lst、m51等编译中间与列表文件,覆盖从源码到可执行固件的完整工程链路,可直接导入Keil打开学习。目前已有2597人学习下载,适合作为课程设计、实训作业或自学练手参考。读者可从中掌握定时器配置、IO口输入输出分配、按键扫描与占空比更新逻辑,并理解舵机位置反馈与PWM占空比之间的对应关系,为后续机器人控制、模型制作等场景打下基础。
1. 从一次舵机抖动说起:这套资源到底能帮你省掉多少试错
舵机这玩意儿,说简单也简单,三根线一接,给个 PWM 就能转;说玄学也真玄学,同一个型号,有人写 50Hz 稳如老狗,有人照抄 50Hz 结果抖得像筛糠。我见过太多人卡在同一个地方:定时器频率算错、占空比公式记反、按键一按舵机就抽风。这套「单片机+舵机+PWM+独立按键」的资源包,就是冲着这些高频翻车点来的。
它本质上是一套完整的工程模板:用单片机定时器产生 PWM 波驱动舵机,再用独立按键做角度调节,代码里把定时器配置、占空比换算、按键消抖、角度限幅这几块都拆开写清楚了。适合两类人:一类是刚上手单片机、想找个能直接跑通的舵机控制例程的新手;另一类是做云台、机械臂、小车转向,需要一套稳定按键调角逻辑的熟手。资源里给的是可编译的源码和接线说明,不是那种只贴几行伪代码的教程。
2. 定时器与 PWM 的底层账:为什么你的舵机总在抖
2.1 先搞懂舵机要的到底是什么信号
舵机不是普通电机,它内部有个电位器做位置反馈,控制板只认一种信号:周期 20ms 的方波,高电平宽度在 0.5ms 到 2.5ms 之间对应 0 到 180 度。这个 20ms 就是 50Hz,是绝大多数模拟舵机和数字舵机的通用标准。很多人第一次翻车就翻在这:拿 1kHz 的 PWM 去驱动舵机,舵机要么不动,要么疯狂抖动,因为它的控制芯片根本锁不住这么快的信号。
所以第一步不是写代码,是算账。假设单片机主频 12MHz,定时器用 16 位模式,要产生 20ms 周期,就得让定时器每 20ms 溢出一次。如果预分频设为 12,那定时器每 1us 计一个数,20ms 就是 20000 个计数。这个 20000 就是自动重装载值。高电平时间对应 0.5ms 到 2.5ms,也就是 500 到 2500 个计数。这两个数就是后面占空比换算的全部依据。
提示:不同单片机定时器位数和预分频写法不一样,但「先算周期计数、再算脉宽计数」这个顺序是通用的,别一上来就抄别人的寄存器值。
2.2 占空比换算:把角度变成比较值
角度到脉宽的映射是线性的。0 度对应 0.5ms,180 度对应 2.5ms,那么每度对应 (2500-500)/180 ≈ 11.11 个计数。写代码时我一般不用浮点,直接用整数运算:脉宽计数 = 500 + 角度 * 2000 / 180。这样 0 度得 500,180 度得 2500,中间角度也能落在合理区间。
下面这段是定时器 PWM 初始化和角度设置的核心逻辑,以常见的 8 位单片机风格伪代码呈现,换成 STM32 或 51 只是寄存器名不同:
// 假设定时器时钟 1MHz,计数周期 1us // 周期 20ms = 20000 计数,脉宽 500~2500 计数 #define PWM_PERIOD 20000 #define PULSE_MIN 500 #define PULSE_MAX 2500 void pwm_init(void) { // 设置定时器周期为 20000,预分频使计数频率为 1MHz timer_set_prescaler(12); // 12MHz / 12 = 1MHz timer_set_period(PWM_PERIOD); // 自动重装载值 timer_pwm_mode_enable(); // 开启 PWM 输出模式 timer_start(); } void servo_set_angle(uint8_t angle) { uint16_t pulse; if (angle > 180) angle = 180; // 角度限幅,防止超程 pulse = PULSE_MIN + (uint16_t)angle * (PULSE_MAX - PULSE_MIN) / 180; timer_set_compare(pulse); // 更新比较值,改变占空比 }逻辑说明:pwm_init里预分频和周期决定了 50Hz 这个基准,servo_set_angle把角度先限幅再线性映射到 500 到 2500 的脉宽区间。参数上,PULSE_MIN和PULSE_MAX是最容易出问题的地方——有些舵机实际范围是 500 到 2500,有些是 1000 到 2000,如果你发现舵机转到两端有异响或堵转,先把这两个值往中间收一收。
2.3 独立按键的扫描与消抖
独立按键接法简单,一个引脚一个按键,按下接地或接电源,代码里轮询判断电平。但机械按键有抖动,通常 5ms 到 20ms 不等,不消抖的话按一次角度可能跳好几度。我一般用状态机做消抖:检测到电平变化后等 10ms 再确认,确认后执行动作,松手也要等 10ms 再确认释放。
#define KEY_DEBOUNCE_MS 10 uint8_t key_scan(void) { static uint8_t key_state = 0; static uint16_t key_timer = 0; uint8_t key_pressed = read_key_pin(); // 读按键引脚,按下为 1 switch (key_state) { case 0: // 空闲 if (key_pressed) { key_state = 1; key_timer = 0; } break; case 1: // 消抖确认 if (++key_timer >= KEY_DEBOUNCE_MS) { if (key_pressed) { key_state = 2; return 1; } // 确认按下 else key_state = 0; } break; case 2: // 等待释放 if (!key_pressed) { key_state = 3; key_timer = 0; } break; case 3: // 释放消抖 if (++key_timer >= KEY_DEBOUNCE_MS) { if (!key_pressed) key_state = 0; else key_state = 2; } break; } return 0; }这段状态机的好处是不会因为一次抖动误触发多次。KEY_DEBOUNCE_MS这个参数根据按键质量调,便宜按键调到 15 到 20 更稳。返回 1 表示一次有效按下,主循环里拿到这个 1 再去改角度。
3. 按键调角度的完整链路:从按下到舵机转到位
3.1 主循环怎么组织才不卡顿
很多新手把消抖写成delay(10),结果舵机 PWM 更新被阻塞,舵机一顿一顿的。正确做法是主循环里非阻塞地轮询按键,同时定时器中断在后台持续输出 PWM。主循环只负责改角度变量,PWM 比较值的更新放在定时器中断里或者主循环里快速执行。
int main(void) { uint8_t angle = 90; // 上电默认中位 pwm_init(); servo_set_angle(angle); while (1) { if (key_scan()) { // 有有效按键 if (read_key_up()) { if (angle < 180) angle += 5; // 每次加 5 度 } else if (read_key_down()) { if (angle > 0) angle -= 5; } servo_set_angle(angle); } // 这里可以放其他任务,不会被消抖阻塞 } }逻辑上,按键扫描返回有效值才改角度,改完立刻更新比较值。步进 5 度是个折中,太小按半天转不到位,太大不好微调。如果你要做云台微调,可以改成 1 度或 2 度,但按键响应会变慢,需要配合长按加速逻辑。
3.2 角度限幅和舵机保护
舵机内部有机械限位,但软件不限幅的话,你给 200 度它也会硬顶,时间长了齿轮就扫了。上面代码里if (angle > 180) angle = 180就是干这个的。另外上电初始化时最好先让舵机回中位,再进入主循环,避免上电瞬间舵机猛甩。
注意:有些舵机 0 度和 180 度位置并不是标准脉宽,如果你发现转到头有嗡嗡声,把软件限幅收到 5 到 175 度,牺牲一点行程换寿命。
3.3 多路舵机时的资源分配
如果你要控两个以上舵机,一个定时器通常不够用,因为每个舵机都要独立的比较通道。常见做法是用一个定时器产生 20ms 基准,然后在中断里轮流输出不同通道的脉宽,这叫软件 PWM 分时复用。但这样会占 CPU,通道多了抖动明显。更稳的是用带多通道 PWM 的定时器,每个通道独立比较值。资源包里如果有多路版本,重点看它怎么分配通道和中断优先级。
4. 避坑排查:舵机抖动、按键失灵、角度不准的常见病根
4.1 舵机抖动不停,但代码看着没问题
现象:舵机上电后持续高频抖动,角度不受控。 原因:PWM 频率不是 50Hz,或者周期不稳定。常见是定时器预分频算错,实际输出 100Hz 甚至 1kHz。 解决:用示波器或逻辑分析仪抓 PWM 波形,确认周期是不是 20ms。没有仪器就临时把周期改成 10ms 和 40ms 对比,看舵机反应,反推实际频率。
4.2 按键按一次角度跳好几度
现象:按一下按键,舵机转了好几步。 原因:消抖没做,或者消抖时间太短,一次按下被扫描到多次。 解决:把消抖状态机加上,KEY_DEBOUNCE_MS调到 15 以上。另外检查主循环里是不是在别的地方也调用了角度增加。
4.3 舵机转到某个角度就卡住或异响
现象:小角度正常,转到 0 度或 180 度附近有嗡嗡声或卡顿。 原因:脉宽超出了舵机实际可接受范围,舵机内部顶到机械限位。 解决:把PULSE_MIN和PULSE_MAX各向内收 100 到 200 个计数,或者软件限幅改成 5 到 175 度。
4.4 上电瞬间舵机猛甩一下
现象:每次上电舵机都先转到一边再回中。 原因:初始化时比较值寄存器默认是 0 或最大值,在pwm_init之后、servo_set_angle之前有一小段输出异常脉宽。 解决:先设置比较值为中位对应值,再开启 PWM 输出。顺序是:配置定时器 → 设比较值 → 使能输出 → 启动定时器。
4.5 按键和舵机共用电源时舵机一动按键就误触发
现象:舵机转动时按键会自己触发,或者单片机复位。 原因:舵机启动电流大,拉低电源电压,导致单片机供电不稳。 解决:舵机电源和单片机电源分开走,共地但不共电。舵机电源并一个大电容(470uF 以上)吸收瞬时压降。
5. 进阶技巧:用示波器验证 PWM 和按键长按加速
5.1 用示波器抓一次完整波形
调舵机最靠谱的验证手段就是示波器。把探头接 PWM 输出脚,地夹共地,触发方式设成下降沿,时基打到 5ms/格。正常应该看到周期 4 格(20ms),高电平在 0.5ms 到 2.5ms 之间变化。如果周期不是 4 格,回去查预分频和重装载值。如果没有示波器,用逻辑分析仪也行,抓几秒数据看脉宽分布。
5.2 长按加速:让按键既微调又快速到位
固定步进 5 度的问题是:想从 0 度到 180 度要按 36 次。加长按加速逻辑后,短按走 2 度,长按超过 500ms 后每 100ms 走 10 度。实现方式是在按键状态机里加一个按住计时器,主循环根据按住时长决定步进值。
// 在主循环里根据按住时长动态调整步进 static uint16_t hold_time = 0; if (key_scan()) { hold_time = 0; // 新按下,重置 } else if (key_is_held()) { hold_time++; if (hold_time > 50) { // 约 500ms 后加速 step = 10; // 加速步进 } else { step = 2; // 微调步进 } }参数上,50 这个阈值对应 500ms(假设主循环 10ms 一次),你可以根据手感调。加速步进 10 度也别太大,否则容易冲过头。
5.3 用表格对比不同参数下的手感
| 参数 | 微调步进 | 加速阈值 | 加速步进 | 适用场景 |
|---|---|---|---|---|
| 精细云台 | 1 度 | 800ms | 5 度 | 摄像头微调 |
| 机械臂关节 | 2 度 | 500ms | 10 度 | 快速定位 |
| 小车转向 | 5 度 | 300ms | 15 度 | 响应优先 |
这张表是我自己调手感时记的,你可以直接抄,也可以根据舵机速度改。舵机本身转速慢的话,加速步进太大反而会过冲,那就把加速步进降下来。
从那以后我每次调舵机项目,都强制先用示波器确认 50Hz 周期,再上按键逻辑,最后才调手感参数。顺序反了,后面全是玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取