
简介基于51单片机的舵机转动实验设计资源面向正在学习51单片机定时器、中断及PWM信号控制的初学者也可作为电子设计课程或机器人入门项目的配套参考。压缩包约125KB共15个文件主要包含C语言源程序、Proteus仿真工程、可烧录的HEX文件及工程备份其中Proteus工程可直接打开进行仿真源程序则便于逐行理解PWM生成逻辑。实验围绕用定时器产生舵机所需的PWM脉冲展开程序包含初始化、控制函数与中断服务程序能够根据目标角度计算并更新脉宽在Proteus仿真环境中可直观观察舵机角度变化无需搭建硬件即可验证代码效果降低学习门槛。目前已有1639人学习/下载适合希望快速掌握舵机角度控制原理、完成课程设计或进入机器人控制领域的学习者。通过该实验可同时熟悉51单片机的定时器/中断机制和Proteus仿真方法为后续复杂控制项目打下基础。1. 舵机转动实验设计的三个前提程序、时钟与仿真一致性把舵机接在51单片机上是电子课程设计里的高频题目但多数资料停在空转演示程序烧进去舵机偏一下就不动了。标题里的程序加Proteus仿真本质要回答三件事脉冲怎么生成、角度怎么映射、仿真怎么验证。舵机选用SG90单片机选用AT89C51是这个组合里最常见的搭配也是下文所有参数围绕的原型。51单片机没有硬件PWM舵机控制恰好是练定时器中断的最佳场景。普通I/O模拟20ms周期和1ms到2ms高电平脉宽原理只有一句话真正翻车点集中在定时器初值算错、中断函数处理太久、Proteus晶振频率与实物板不一致这三处。这三处单拎出来都不难但叠在一起时角度偏差和波形抖动会让整个实验看起来像舵机本身坏了。下文按一条能下板、能交课的流程展开Keil C51程序怎么写Proteus 8元件怎么摆虚拟示波器怎么验最后补一个多舵机扩展的边角处理。新手能照着跑完老手也能在初值计算和脉宽校准里看到一些平时不太会写进笔记的边界条件。2. 舵机转动原理与51单片机定时器选型从20ms周期到脉宽计数2.1 20ms周期脉冲与角度映射先弄清舵机内部在比什么模拟舵机的内部结构可以简化成三块基准电路、直流电机、电位器反馈。外部输入的PWM信号和内部基准信号做差值比较差值方向决定电机转向差值归零时舵机停在对应角度。注意一个容易被忽略的事实决定角度的不是占空比百分比而是高电平持续的实际时间。对SG90这类小型舵机20ms固定周期下0.5ms高电平对应0°1.5ms对应90°2.5ms对应180°。不少教学资料习惯用1ms、1.5ms、2ms这组映射本质是给机械零点和硬件死区留余量两种映射都能转但代码注释里必须写清用的是哪一组。如果试图用main循环里的delay来拼脉宽角度很快就会飘。按键扫描、数码管刷新、串口发送任何一样都会拉长高电平时间轻则角度偏移重则脉宽超过2.5ms舵机直接进入连续转动区间。可靠做法是把电平翻转放进定时器中断主循环只负责修改脉宽对应的变量值。这样主循环里无论写多复杂的逻辑波形层面始终保持独立。2.2 12MHz晶振下定时器初值计算与寄存器配置51单片机接12MHz晶振并工作在12T模式时一个机器周期刚好1us这是课程设计里最省心的时钟组合。选用定时器0方式1也就是16位计数模式从初值加到65536溢出一次。要得到1ms定时中断需要计数1000次初值等于65536减1000即64536十六进制写作0xFC18拆给两个寄存器就是TH00xFC、TL00x18。下面这段初始化代码可以直接放进Keil工程。#include reg51.h // 12MHz晶振, 12T模式, 1个机器周期1us // 定时器0方式1(16位), 定时1ms // 初值: 65536 - 1000 64536 0xFC18 #define T1MS_H 0xFC #define T1MS_L 0x18 void timer0_init(void) { TMOD 0xF0; // 保留定时器1原有配置 TMOD | 0x01; // 定时器0工作在方式1 TH0 T1MS_H; // 先写高字节 TL0 T1MS_L; // 再写低字节 ET0 1; // 使能定时器0中断 EA 1; // 使能总中断 TR0 1; // 启动定时器 }这段代码逐行看其实信息量不小。TMOD的低4位控制定时器0高4位控制定时器1。用“先清后置”的写法而不是直接赋值TMOD0x01是为了后面加串口波特率初始化时不破坏定时器1的配置。TH0和TL0的写入顺序从高字节到低字节能避免16位重装时出现半个周期组合错误这个问题在旧版Keil和某些盗版优化环境下会被放大。ET0、EA、TR0三个控制位分别对应中断允许、总开关、计数运行开关少写任何一个定时器都会表现得很奇怪最常见症状是程序停在main里毫无反应。2.3 初值参数表与常见晶振的换算差异作为对比把几种常见晶振频率下列出的1ms定时参数整理成表。实操时先确认板子丝印再填代码里的宏定义。晶振频率机器周期1ms初值(十进制)1ms初值(HEX)说明12.000MHz1us645360xFC18课程设计的默认值11.0592MHz约1.085us646140xFC66串口常用晶振每毫秒有约0.4us舍入误差6.000MHz2us650360xFE0C脉宽按2us步进计数要另外换算熟手在这里容易踩一个隐蔽的坑开发板标注是11.0592MHz代码却沿用12MHz的初值结果1ms时基偏了8%以上。角度偏差在示波器上肉眼可见但很多人第一反应是去改舵机死区而不是检查晶振。Proteus仿真里的情况类似原理图上CRYSTAL组件的默认频率如果没改成12MHz程序烧进去照样转只是脉宽和角度对不上。排查时把Keil里“Options for Target”中的晶振、Proteus原理图里的晶振、代码注释里的机器周期三者统一能省掉大半联调时间。如果只看舵机“转不转”初值差一点也会转但偏差会在角度上线性放大。课程设计答辩里最常被问到的“为什么你的90°实际停在95°”答案十有八九是晶振频率没有统一。2.4 为什么用1ms时基而不是直接20ms中断一次另一种写法是把定时器做成20ms中断一次在中断函数里先输出高电平再软件延时拉低。逻辑上更直接缺点也很明显调整角度必须改延时时间而且这个延时期间中断被其他任务打断后波形占空比会立刻抖动。1ms时基的做法是让中断函数每次都执行一遍“计数值小于脉宽就输出高否则输出低”的比较脉宽本身来自全局变量改角度只是写一次变量波形层完全不受主循环影响。我一般把这种方案作为默认选型只有当IO资源特别紧张时才去考虑定时器1做第二路PWM。3. Keil C51程序定时器中断累加脉宽主循环只改角度值3.1 中断累加的PWM实现从1ms时基细化到0.5ms1ms时基下脉宽只能取整数毫秒SG90只能得到0°、45°、90°、135°、180°五个位置。大多数课程设计够用但如果想把代码延伸到云台或两自由度机械臂角度分辨率会不够。常见做法是压缩时基到0.5ms20ms周期被分成40个时间片中断里的比较逻辑完全不变只改初值和周期计数上限。#include reg51.h #define T0_5MS_H 0xFE // 0.5ms时基初值高8位: 65536-50065036 #define T0_5MS_L 0x0C // 65036 0xFE0C #define PWM_MAX 40 // 20ms / 0.5ms 40 个时间片 #define SERVO_PIN P2_0 // 舵机信号接P2.0 volatile unsigned char angle_level 3; // 当前脉宽档位 unsigned char pwm_count 0; // 周期内计数值 void timer0_isr(void) interrupt 1 { TH0 T0_5MS_H; // 重装初值, 保证中断间隔一致 TL0 T0_5MS_L; if(pwm_count angle_level) // 前N个时间片输出高电平 SERVO_PIN 1; else SERVO_PIN 0; if(pwm_count PWM_MAX) // 计满40次代表20ms周期结束 pwm_count 0; } void set_servo_angle(unsigned char angle) { if(angle 0) angle_level 1; // 0.5ms, 0度 else if(angle 180) angle_level 5; // 2.5ms, 180度 else angle_level (unsigned char)(angle / 45 1); }中断里先重装初值再比较pwm_count和angle_level。angle_level等于3时前三个0.5ms时间片是高电平累计1.5ms正好对应90°。set_servo_angle把角度量化成1到5的档位避开了在8位变量里做浮点运算。角度与档位的对应关系如下。目标角度angle_level 档位高电平时间对应周期0°10.5ms20ms45°21.0ms20ms90°31.5ms20ms135°42.0ms20ms180°52.5ms20ms这段ISR编译后的执行时间只有几十个机器周期按12MHz算不到几十微秒远小于0.5ms的时基中断完全拖不垮主循环。angle_level和pwm_count都声明成volatile是因为它们同时被中断函数和主循环访问。8位变量在这个场景下不会有半个字的问题但保留volatile是后面扩展16位变量时避免踩坑的习惯。3.2 main循环里的按键角度切换与去抖动按键控制角度是课程设计里最常演示的功能。两个独立按键分别做加45°和减45°整个过程分为去抖动、二次确认、更新目标角度、等待松手四步。sbit key1 P3^2; // 独立按键, 低电平有效 sbit key2 P3^3; void delay_ms(unsigned int ms) { unsigned int i, j; for(i 0; i ms; i) for(j 0; j 123; j); } void main(void) { unsigned char target 90; timer0_init(); set_servo_angle(target); while(1) { if(key1 0) { delay_ms(20); // 跳过机械抖动 if(key1 0) // 二次确认确实按下 { target 45; if(target 180) target 180; set_servo_angle(target); while(key1 0); // 等待松手 } } if(key2 0) { delay_ms(20); if(key2 0) { if(target 45) target - 45; else target 0; set_servo_angle(target); while(key2 0); } } } }按键代码里的target是目标角度set_servo_angle只更新angle_level实际PWM波形始终由中断维护。因此即使按键扫描里的delay_ms拖到20ms舵机脉冲也不会丢。阻塞式的while(key10)优点是逻辑清晰缺点是按住按键期间主循环无法做别的事。如果后续要加数码管显示角度建议去掉这个等待改成置一个“按键生效”标志由主循环下一轮统一处理。3.3 定时器初值重装的时序与中断嵌套边界ISR里重装TH0和TL0的顺序值得强调。如果先写TL0再写TH0低字节更新后高字节还停留在旧值计数器的组合值在极短时间内是不确定的相当于多出几微秒的误差。对舵机来说单次几微秒无感但会累积成周期抖动。重装时保持先高后低是标准写法也是规范写法。51的中断没有硬件优先级抢占只要ISR里不主动重开EA就不会出现嵌套所以这段ISR不需要进入时关中断、退出时开中断处理时间可以被压得很短。4. Proteus仿真文件搭建与51单片机联调元件放置、时钟一致性与排错4.1 从Proteus元件库拉AT89C51和MOTOR-SERVO的接线顺序Proteus 8 Professional里搜索AT89C51在Microprocessor ICs分类下可以找到。舵机模型最稳妥的搜索词是MOTOR-SERVO而不是SG90后者在不同版本元件库中的命名很不一致。我一般先把单片机最小系统搭完再接舵机负载避免线缆缠在一起时漏掉复位电路。元件Proteus搜索名参数作用单片机AT89C51DIP40主控单元舵机模型MOTOR-SERVO三线接口接收PWM晶振CRYSTAL12MHz时钟源负载电容CAP22pF晶振起振复位电阻RES10k复位下拉复位电容CAP-ELEC10uF上电复位延时连线时最容易犯的一个错误是EA引脚悬空。AT89C51的EA必须接高电平否则单片机会从外部程序存储器取指HEX烧进去也不会执行仿真里表现为P2.0完全没波形。舵机信号线接P2.0电源线接5V和GND。不要图方便把信号接在P0口P0是开漏结构仿真模型可能看不出来实物板必须外加上拉电阻才能驱动这种“仿真通过、实物失效”的案例在答辩现场很常见。4.2 仿真文件里的晶振频率必须与Keil程序一致Proteus默认放置的晶振频率不一定是12MHz双击CRYSTAL组件查看频率属性改成12MHz后保存。如果这里保持默认值而Keil代码按12MHz计算初值仿真时舵机照样转用示波器一看P2.0的高电平时长全部按比例放大角度完全对不上。这个现象和实物板换了晶振不换代码是一样的只是Proteus更隐蔽因为它连波形都给你画好了。提示Proteus不同版本中MOTOR-SERVO的引脚标注略有差异接反信号线和电源线不会烧模型但你会看到舵机朝一个方向持续转动这通常不是程序问题先检查接线。4.3 仿真不转、抖动、角度不准的排查顺序先说“不转”。检查AT89C51上是否有红色运行标记确认仿真没停在暂停状态。再用虚拟示波器看P2.0有没有脉冲。没有波形时优先确认两方面Keil是否成功生成HEX文件以及AT89C51元件属性里的Program File是否指向了这个HEX。Proteus不会自动关联Keil输出很多人在这里卡了一下午。再说“抖动”。仿真里的抖动大多是动画步长引起的假象不是真正的信号噪声。Proteus仿真时把动画速度调快或改用“运行”而非“单步”模式波形毛刺会明显减少。0.5ms时基本身会产生阶梯状上升沿这是PWM的分辨率不要当成故障去改代码。最后是“角度不准”。不同版本的MOTOR-SERVO模型对脉宽范围的解释不同有的按0.5ms到2.5ms有的按1ms到2ms。双击模型查看属性说明按模型的有效范围去改角度映射宏比照着真机参数硬调更快。为了排除这种不确定性可以用下面这段测试代码让舵机在0°和180°之间往复摆动。// 测试步进: 每隔1s切换一次0度和180度 // 用于确认定时器初值、角度映射、Proteus模型三者一致 void main(void) { unsigned char dir 0; timer0_init(); while(1) { if(dir 0) { set_servo_angle(0); dir 1; } else { set_servo_angle(180); dir 0; } delay_ms(1000); } }如果两个极限位置都能到达并停留说明中断和脉宽映射正常工作。如果只有一个方向到位多半是脉宽在单侧超出了模型的有效范围此时不需要质疑舵机直接查模型参数表。5. 波形验证与多舵机扩展把单路实验改成复用性更好的PWM组5.1 在Proteus里用虚拟示波器读脉宽和周期Proteus右侧工具栏打开OSCILLOSCOPEA通道接到P2.0地线接到GND。运行仿真后按空格键暂停画面用示波器面板上的光标测量两个下降沿间隔应该是20ms左右再单独测量高电平持续时间0°、90°、180°三档应分别对应0.5ms、1.5ms、2.5ms。如果高电平时长明显偏大并且切换角度时变化幅度异常优先回查定时器初值重装而不是怀疑舵机模型。5.2 多舵机共用一个定时器的累加规则扩展到四路舵机时不要复制四套定时器初始化代码51单片机的定时器资源撑不起这种写法。可靠做法是使用一个0.5ms时基中断函数里依次比较四路level变量四路共享同一个pwm_count。角度更新只在主循环里写全局变量中断层保持不变。#define SERVO0 P2_0 #define SERVO1 P2_1 #define SERVO2 P2_2 #define SERVO3 P2_3 volatile unsigned char level0 3; volatile unsigned char level1 3; volatile unsigned char level2 3; volatile unsigned char level3 3; void timer0_isr(void) interrupt 1 { TH0 T0_5MS_H; TL0 T0_5MS_L; SERVO0 (pwm_count level0) ? 1 : 0; SERVO1 (pwm_count level1) ? 1 : 0; SERVO2 (pwm_count level2) ? 1 : 0; SERVO3 (pwm_count level3) ? 1 : 0; if(pwm_count PWM_MAX) pwm_count 0; }四路PWM的相位完全对齐周期仍是20ms。中断里四个比较操作合计多出几微秒开销在0.5ms时基下完全可接受。实物调试时注意四路舵机同时启动瞬间电流很大5V供电会被明显拉低仿真模型不会展示这个现象。在舵机电源端并一个470uF以上的电解电容且舵机电源与单片机电源不要共用同一根细线。5.3 一个常被忽视的同步校准技巧多舵机项目里把各通道的角度档位统一做成一个数组修改时只动一处映射表比分散在多个set函数里好维护。校准舵机零点时让某一路停在90°档位用示波器游标量出实际高电平时长把偏差值记录成常量在set_servo_angle里追加补偿。若偏差超过0.2ms优先检查舵机电源质量以及继电器信号线上是否误接了限流电阻。仿真阶段这类故障不会出现但实物调试时它比程序逻辑更常见。最后记住Proteus验证的是逻辑正确性替代不了机械限位和负载电流测试把仿真文件的角度变化换成真实舵机前先用手转动舵机臂确认机械范围防止程序角度叠加机构限位导致堵转。本文还有配套的精品资源点击获取