简介:压缩包内是面向51单片机小车的舵机控制示例程序,聚焦如何借助蓝牙模块无线操控舵机,适合电子爱好者、嵌入式初学者以及正在搭建遥控小车项目的开发者参考。资源共包含3个C语言源文件,整体仅3KB,文件虽小但覆盖三种典型PWM产生方式:基于PCA的16位软件定时器、单个T0定时器、T0与T1双定时器,可对照学习不同硬件资源下的舵机驱动思路,理解脉冲宽度调制原理及定时器配置差异。目前已有277人学习下载。通过阅读和移植这些代码,读者能快速掌握51单片机接收蓝牙指令、解析控制数据并输出PWM信号的基本流程,同时也可作为进一步扩展多路舵机、接入其他无线模块的基础模板。
1. 舵机测试C51程序:把蓝牙控制小车的第一步验证做扎实
看到「舵机测试C51程序.zip」这个项目名,大多数场景并不是只为了看舵机转一个角度。你大概率已经把 51 单片机、SG90 或 MG996R 舵机、HC-05 蓝牙模块和一套小车底盘凑齐,目标就是做一台能手机控制的蓝牙控制小车,想用最简洁的程序先验证三件事:舵机 PWM 驱动是否正确、蓝牙链路是否通、供电是否扛得住,然后再往循迹或避障方向扩展。标题里的「蓝牙控制小车」才是最终目标,舵机测试只是验证链路上的一环。这个程序里的串口中断接收、定时器产生 PWM、角度解析和转向执行四段逻辑,在后续换成 STM32、ESP32 平台时依然能整体平移。若你手头还是 keil c51 旧工程,或者刚装好 keil5 想同时做 C51 和 STM32,这条从舵机测试起步、逐步加蓝牙指令的路径会比较顺。
2. 从舵机原理到 C51 定时器输出 PWM
2.1 舵机测试先确认 50Hz 周期和脉宽映射
舵机转动的信号是周期 20ms、高电平脉宽 0.5ms 到 2.5ms 的 PWM。1.5ms 对应 90 度中位,0.5ms 接近 0 度,2.5ms 接近 180 度。绝大多数模拟舵机在 45Hz 到 70Hz 都能工作,但把周期稳定在 20ms 最不容易出问题。跑舵机测试程序时,先从 1.5ms 开始,确认舵机回中位,再按 0.1ms 步进观察角度增量,这一手能同时验证角度换算和机械限位范围。
第一个常见误区是用 delay 翻转 IO 模拟 PWM。delay 版本在舵机空转时没问题,一旦加入蓝牙接收,主循环被占住,串口缓存里的字节读不过来,舵机响应就会出现肉眼可见的卡顿。正确做法是定时器中断产生时基,主循环只做指令解析和状态更新。
2.2 用 C51 定时器 T0 产生 0.1ms 时基
以 12MHz 晶振为例,机器周期是 1μs。把定时器 T0 配置为方式 1(16 位),目标溢出周期取 100μs,则初值是:
TH0 = (65536 - 100) >> 8 TL0 = (65536 - 100) & 0xFF
每进一次中断,软件计数器 timer_tick 加一。0.1ms×200 等于 20ms,在 timer_tick 等于脉宽计数时拉低 IO,等于 200 时重新拉高,就得到连续 PWM 波形。为什么选 0.1ms 而不是 1ms?两者都能产生 50Hz 信号,但 0.1ms 时基在之后加斜坡限幅时能提供更细的角度步进,不会出现一次跳 9 度的粗粒度。
// 12MHz 晶振, 定时器0方式1, 每次中断0.1ms #include <REG51.H> unsigned int target_pulse = 15; // 目标脉宽, 单位0.1ms, 15=1.5ms unsigned int timer_tick = 0; sbit servo_pin = P1^0; // 舵机信号线 void timer0_init(void) { TMOD &= 0xF0; TMOD |= 0x01; // T0 方式1 (16位) TH0 = (65536 - 100) >> 8; TL0 = (65536 - 100) & 0xFF; ET0 = 1; TR0 = 1; EA = 1; } void timer0_isr(void) interrupt 1 { TH0 = (65536 - 100) >> 8; // 手动重装 TL0 = (65536 - 100) & 0xFF; timer_tick++; if (timer_tick == target_pulse) { servo_pin = 0; // 高电平结束, 脉宽到 } else if (timer_tick >= 200) { servo_pin = 1; // 周期20ms到, 新一轮开始 timer_tick = 0; } }逻辑说明:target_pulse是全局变量,串口中断或主循环都可以直接改它。每次进入定时器中断,先把计数器和脉宽比较,相等就输出低电平;当计数到 200,说明一个 20ms 周期结束,重新拉高并清零。servo_pin接到舵机信号线,地线必须与单片机共地。初次测试时把target_pulse改成 5、15、25,分别观察舵机是否停在 0 度、90 度、180 度附近,确认接线和脉宽计算都正确。
中间要格外注意两点。第一,重装语句要靠近中断入口,不要在中断尾部做过多判断后再重装,否则定时器溢出后继续计数的时间会被拉长,周期误差累加到舵机上就是抖动。第二,65536 - 100在 Keil C51 里会被优化成常量,不要写成每次中断都用 16 位除法计算的表达式,那样中断耗时会被拉长到 10μs 以上。初次运行发现舵机角度偏大或偏小,先核对晶振实际频率,再核对重装值。
| 参数 | 值 | 说明 |
|---|---|---|
| 晶振 | 12MHz | 机器周期 1μs |
| 定时器 | T0 方式1 | 16 位 |
| 中断周期 | 100μs | 0.1ms 时基 |
| 重装值 | 65436 | 65536 - 100 |
| PWM 周期 | 200 × 0.1ms | 20ms,50Hz |
2.3 角度到脉宽的换算参数
角度换算成脉宽计数的公式:
pulse_unit = 5 + (angle * 20) / 180
pulse_unit单位是 0.1ms,5 对应 0.5ms,25 对应 2.5ms。90 度算出来是 15,也就是 1.5ms。整数运算顺序要注意:angle * 20必须先于/ 180执行,否则先除再乘会把中间结果截成 0。角度上限按舵机实际规格写死,避免蓝牙指令传入 200 度把舵机顶到机械限位。
2.3.1 MG996R 与 SG90 的参数差异
SG90 和 MG996R 的脉宽范围基本一致,但电流特性差异很大。SG90 空载电流约 200mA,堵转接近 700mA;MG996R 堵转标称 2.5A 左右。测试程序跑空载时两者都能正常转,一旦接上小车转向机构,负载变大,舵机抖动往往不是程序问题而是供电问题。顺序建议是先让舵机空转验证程序,再接联轴器和转向机构,这样可以把信号问题和机械负载问题拆开。
3. 蓝牙接收指令与舵机动作映射
3.1 HC-05 与 C51 的接线和电平处理
蓝牙控制小车普遍用 HC-05 或 HC-06。HC-05 支持主从一体,能通过 AT 指令切换角色;HC-06 只能做从机,价格更低。两者都是经典蓝牙协议,Android 手机直接搜,iPhone 用第三方串口 App 支持度有限,因此做这类小车优先选 HC-05,配置灵活,排查时也方便用 AT 指令把模块恢复出厂设置。
接线表:
| 信号 | C51 引脚 | HC-05 引脚 | 说明 |
|---|---|---|---|
| 供电 | 5V | VCC | 模块上有稳压 |
| 地 | GND | GND | 和舵机共地 |
| 数据 | P3.0/RXD | TXD | 蓝牙发往单片机 |
| 数据 | P3.1/TXD | RXD | 需要分压到 3.3V |
| 模式 | P1.1 | EN/KEY | 上电前拉高进入 AT 模式 |
C51 的 TXD 输出 5V 高电平,HC-05 的 RXD 额定 3.3V,直接长连虽然多数模块不烧,但长期运行不稳定。常见做法是用两个电阻分压,串一个 1kΩ 电阻再并 2kΩ 到地,把电平降到约 3.3V。HC-05 的 TXD 输出 3.3V,接 C51 RXD 不需要升压,因为 51 单片机识别高电平的门限约为 2.0V。
3.2 HC-05 的 AT 指令配置参数与进入透传模式
先用 USB-TTL 连接 HC-05,模块的 EN 引脚拉高或按住板载按键再上电,串口助手选用 38400 波特率(部分版本是 9600),发送以下指令,换行后等待 OK 返回:
AT+NAME=Car-BT AT+UART=9600,0,0 AT+ROLE=0
AT+NAME 修改广播名,AT+UART 把通信波特率设为 9600,AT+ROLE=0 指定从机模式。配置完后断电,把 EN 拉低重新上电进入透传模式。手机端搜索到 Car-BT 并配对,配对码通常是 1234 或 0000。配对成功后手机串口 App 发送的字节会原样出现在 HC-05 的 TXD 引脚上,C51 用串口中断读 SBUF 即可。
3.3 C51 串口中断接收与指令解析
串口配置为方式 1,8 位数据,波特率由定时器 1 产生。下面代码用的是 11.0592MHz 晶振,TH1=0xFD对应 9600bps。协议格式定义为T加三位角度数字,例如 T090 表示转向 90 度,T180 表示右满舵。固定长度协议比不定长字符串更容易容错,也不会出现收到 T90 之后程序还在等第三位数字的情况。
#include <REG51.H> unsigned char rx_buf[3]; unsigned char rx_index = 0; unsigned int target_pulse = 15; void uart_init(void) { SCON = 0x50; // 方式1, REN=1 TMOD &= 0x0F; TMOD |= 0x20; // 定时器1 方式2 自动重装 TH1 = 0xFD; // 9600bps @ 11.0592MHz TL1 = 0xFD; TR1 = 1; ES = 1; EA = 1; } void uart_isr(void) interrupt 4 { unsigned char ch; if (RI) { RI = 0; ch = SBUF; if (ch == 'T') { rx_index = 0; return; } if (ch >= '0' && ch <= '9' && rx_index < 3) { rx_buf[rx_index++] = ch; if (rx_index == 3) { unsigned int angle; angle = (rx_buf[0] - '0') * 100 + (rx_buf[1] - '0') * 10 + (rx_buf[2] - '0'); if (angle > 180) angle = 180; target_pulse = 5 + (angle * 20) / 180; } } } }逻辑说明:中断服务程序里只做最少的字符判断和换算,不调用任何库函数,避免在中断里等待外围设备。rx_buf存三位 ASCII 数字,凑齐后一次性转成整数并更新target_pulse。target_pulse与定时器中断是“一个写、一个读”的关系,读的一侧只复制到局部变量再判断,不会出现数据撕裂。串口收错一个字节时,下一条以 T 开头的完整指令会覆盖之前的内容,整个状态机可以自我恢复。
3.4 主循环框架和测试指令映射
主循环不需要再轮询串口,它的任务变成处理蓝牙指令对应的整车行为。以蓝牙控制小车为例,定义如下动作映射表:
| 指令 | 含义 | target_pulse 变化 |
|---|---|---|
| F | 前进 | 保持 90 度中位 |
| L | 左转 | 从 90 降到 45 |
| R | 右转 | 从 90 升到 135 |
| B | 后退 | 保持 90 度中位 |
| S | 停止/回正 | 回到 90 |
实际执行时,控制后轮电机的前进后退由另一个 IO 驱动,例如 P2_0 控制电机使能、P2_1 控制方向。舵机只负责转向角度,电机只负责动力。这样拆分的边界越清晰,排错的时候越容易判断是哪一侧出了问题。每个指令字符建议同时驱动一个 LED 或测试点,跟舵机响应做对比,独立确认蓝牙链路正常。
4. 舵机测试程序实战中的 3 个坑与排查
4.1 舵机抖动或转到一半停住
舵机在 0 度和 90 度之间来回抖动,第一个要查的是电源。C51 开发板上的 78L05 最大输出 100mA 左右,能带单片机却带不动舵机。SG90 空载电流约 200mA,MG996R 堵转瞬间 2A 以上。正确做法是给舵机单独一路 5V 供电,电源地和单片机共地,信号线单独接 IO。
舵机抖动还有一个常见来源是中位失准:目标脉宽 1.5ms,但舵机实际中位在 1.52ms,程序没有死区,导致它不断在附近来回找位置。测试时把脉宽比较条件改成带死区,例如if (timer_tick + 2 == target_pulse),或者在角度解析后做 2 度左右的迟滞判断,都可以避免这种高频抖动。若执行过程中出现咔咔声,优先用手触摸舵机外壳,若明显发烫,基本就是机械负载过大或供电不足。
按照从简到繁的排查顺序,独立电源、共地、死区判断这三步做完,电动机部分通常就稳定了。有些人一上来就怀疑 keil c51 优化有问题,实际上大多数抖动都来自电源和机械连接,程序反而没有大毛病。
4.2 hc05 蓝牙模块连接不上、乱码和收发不同步
hc05 蓝牙模块连接不上时,按顺序排查:
- 看指示灯闪烁频率。约 1 秒 2 次快闪是 AT 模式,约 2 秒 1 次慢闪是透传模式待配对。不能配对时先确认没有停在 AT 模式。
- 检查 TXD 和 RXD 是否交叉。HC-05 的 TXD 必须接 C51 的 RXD,同端相接收不到任何数据。
- 用 USB-TTL 连接模块,串口助手分别试 9600 和 38400 两个波特率,发一条 AT 指令,能返回 OK 的就是模块当前参数。
- 手机端要选 SPP 蓝牙串口应用,不是低功耗 BLE 扫描。
乱码问题更多出在晶振频率。12MHz 晶振配 9600bps 时波特率误差约 2.2%,短数据看不出,连续几十个字节时最后一个字符容易解析错。解决方式是换 11.0592MHz 晶振,或者把数据帧设计成固定长度并带结束符,即使中间有个别位错误,长度检查也能把坏帧滤掉。若模块能连上但发指令没反应,先把 RX 和 TX 短接做回环测试,串口助手能收到自己发出的字符,证明 USB-TTL 链路没问题。
4.3 keil c51 工程配置的 3 个隐性坑
用 keil c51 打开这类工程,编译通过但舵机不动,优先检查下面三项。第一,interrupt关键字的中断号,定时器 0 是 1,定时器 1 是 3,串口是 4,写错后 Keil 不报错,但中断服务函数进不去,现象就是彻底没反应。第二,TMOD的高四位和低四位,低四位配置定时器 0,高四位配置定时器 1,如果工程里同时用定时器 1 做波特率生成,TMOD &= 0xF0这行不能省。第三,优化级别,Keil C51 默认 Level 8 优化对局部变量有效,但全局变量在中断和主循环之间共享时可能被优化成寄存器缓存,把共享变量声明为volatile最直接:
volatile unsigned int target_pulse = 15; volatile unsigned char timer_tick = 0;想观察 PWM 波形却没有示波器,Keil μVision 的 Logic Analyzer 窗口可以当简易舵机仿真器用,添加 P1_0 端口后能看到高电平宽度是否符合预期,不过它依赖仿真配置里的晶振频率,频率填错波形肯定不对。
| 现象 | 可能原因 | 定位手段 |
|---|---|---|
| 编译通过但舵机不动 | 中断号错误 / TR0 未置位 | 检查 ES、ET0、EA |
| 示波器看到固定 1.5ms 与指令无关 | 串口中断没进入 | 在 uart_isr 里设断点 |
| 蓝牙一发数据小车就复位 | 电机和舵机共用一个电源 | 独立供电,加 470μF 电容 |
| 转度达不到 180 度 | 机械限位或脉宽上限超了 | 手动确认舵机实际行程 |
5. 用斜坡限幅让舵机测试程序适配小车的转向动作
验证舵机测试程序是否真正可用,一个有效动作是发送 T180 再快速发 T045,观察舵机是否直接“甩”过去。对于前轮转向小车,舵机是转角执行器;如果是两轮差速小车,可以不用舵机,直接靠左右轮速度差转弯。这里的斜坡限幅只针对舵机转向这种情况。
若小车前轮转向机构直接从最左打到最右,机械冲击会很快磨损舵机齿轮,也会让小车转向时出现明显侧滑。改进方法是在脉宽目标值和实际输出值之间加一段步进限幅。把上一节的target_pulse拆成target_pulse(目标值)和current_pulse(实际输出值),在定时器中断里做斜坡:
unsigned int current_pulse = 15; // 实际输出给比较器的脉宽 unsigned int target_pulse = 15; // 串口解析得到的目标脉宽 unsigned char ramp_cnt = 0; void timer0_isr(void) interrupt 1 { // 重装语句省略, 参考 2.2 节 if (target_pulse > current_pulse) { ramp_cnt++; if (ramp_cnt >= 20) { ramp_cnt = 0; current_pulse++; } } else if (target_pulse < current_pulse) { ramp_cnt++; if (ramp_cnt >= 20) { ramp_cnt = 0; current_pulse--; } } else { ramp_cnt = 0; } timer_tick++; if (timer_tick == current_pulse) { servo_pin = 0; } else if (timer_tick >= 200) { servo_pin = 1; timer_tick = 0; } }参数说明:ramp_cnt控制逼近速度,20 个 tick 就是 2ms,每次只改变 1 个单位。从 90 度目标转到 45 度需要 5 个单位,总耗时约 10ms,对于响应速度 60 度/100ms 左右的舵机,这个速度已经足够温和。如果觉得太快,把ramp_cnt提高到 50,脉宽变化延长到 25ms;如果做总线舵机机械臂那种需要精确位置插补的场合,就把定时器中断周期缩小到 50μs,斜坡粒度会细得多。
验证方法:在 P1_0 上用示波器观察高电平宽度,连续发送 T180 和 T045,脉宽应逐步变化而不是跳变;同时听舵机声音,若有尖锐的持续电流声说明步进过频,加大ramp_cnt即可。这套限幅逻辑在小车前进中改变方向时,能明显减小离心侧倾。后续若把程序移植到 keil5 的 STM32 工程或 stm32cubemx 生成的代码里,斜坡限幅部分可以原样复制,只需把定时器中断周期换成 HAL 定时器的回调周期。
本文还有配套的精品资源,点击获取