简介:面向单片机入门及电子设计人员,资源基于C51单片机与L298驱动芯片,实现直流电机PWM调速、加速减速、正反转与停止控制,并配有protue仿真工程,可用于课程设计、毕业设计或实物调试参考。全包共21个文件,约116KB,主要包含C源码与Keil工程文件、可烧录HEX文件、protue仿真工程及说明图片和备份文件,覆盖程序编写、编译烧录到仿真验证的完整链路。已有415人学习浏览。通过源码和仿真可理解定时器产生PWM波形、L298使能与方向控制的配合逻辑,掌握独立键盘输入和1602液晶显示转速的实现思路。源码与仿真联合使用便于修改占空比或方向参数后观察电机运转变化,适合动手完成直流电机控制项目的读者参考,整体设计思路清晰,也可作为二次开发基础模板。
1. C51+L298 驱控直流电机,为什么“加减速”的难点不在 H 桥而在 PWM 时序
直流电机调速看起来是硬件问题,但真正让初学者卡住的往往是那句“我想让它加速减速”。给 L298 的两个方向引脚 IN1/IN2 置高低电平,正转、反转、停止三行代码就能跑通;一旦加上“快一点、慢一点”的需求,就必须让使能端 ENA 上出现占空比可变的 PWM 波形。对 AT89C52 这类没有硬件 PWM 的传统 C51 单片机,这个 PWM 只能靠定时器中断软件生成,而中断时基、周期计数、按键步进三者关系理不清,电机就会出现要么不转、要么乱抖的现象。本文按 Proteus 仿真为主线,把 L298 输出逻辑、C51 软 PWM、按键调速和示波器验证串成一条可以复现的调试链路,适合课程设计、毕设入门和第一次接触驱动电路的工程师对照修改。
2. 先看懂 L298 的输出逻辑:正转、反转、停止真值表与 C51 引脚映射
2.1 L298 内部 H 桥和三个电源引脚怎么接
L298 是一颗双 H 桥驱动芯片,内部每个 H 桥由四个功率管组成,外部只需要给方向输入和使能信号,就能控制一路直流有刷电机。和无刷直流电机需要三相桥式换相的逻辑不同,L298 面对的是直流有刷电机,双 H 桥就足够完成正反向切换。如果项目功率不大,也可以换用 TB6612FNG 驱动直流电机,它的 MOSFET 桥压降更小;但 L298 的逻辑输入电平只要 5V 就能直接兼容 C51、驱动能力余量大,Proteus 元件库里也一直保留,所以课设和教学项目至今还在大量使用。
L298 的引脚看起来多,但单电机方案只需要关心 Channel A 这一组。VS 是电机电源,VSS 是芯片逻辑电源,二者必须分开供电,但最终都必须共地。很多 Proteus 仿真“电机不转”,第一原因就是只给 VS 接了 5V,或者 VSS 和 VS 共用一个电源却没接 GND。下面是单电机方案的接线表:
| 引脚 | 功能 | 本设计接法 |
|---|---|---|
| VS | 电机电源,6~46V | 接 12V |
| VSS | 逻辑电源,5V | 接开发板 5V |
| GND | 电源地 | 与单片机、电源全部共地 |
| ENA | A 桥使能端 | 接单片机 P1.2,用于 PWM |
| IN1 / IN2 | A 桥方向输入 | 接 P1.0 / P1.1 |
| OUT1 / OUT2 | A 桥输出 | 接直流电机两端 |
| SENSEA | 电流采样 | Proteus 中直接接地 |
注意不要从 VS 引一根线到 VSS,L298 不是稳压芯片。更常见的做法是 12V 电源给 VS,同时经过一个 5V 稳压模块给 VSS 和单片机供电,三路 GND 连在一起。
2.2 四象限输出真值表:滑行停止和刹车停止不是一回事
L298 的每个 H 桥可以看作由 ENA 和 IN1/IN2 共同控制的开关组合。ENA 为高时桥臂导通,IN1/IN2 决定电流方向;ENA 为低时输出级进入高阻状态。这里最容易混淆的是“停止”的两种实现:
| 模式 | ENA | IN1 | IN2 | OUT1/OUT2 行为 | 电机状态 |
|---|---|---|---|---|---|
| 正转 | H 或 PWM | 1 | 0 | VS / GND | 正转 |
| 反转 | H 或 PWM | 0 | 1 | GND / VS | 反转 |
| 刹车停止 | H | 0 | 0 | GND / GND | 两端短接,立即制动 |
| 滑行停止 | L | x | x | 高阻 | 惯性滑行 |
正转和反转好理解,关键是停止。把 ENA 置高、IN1=IN2=0,输出两端同时被拉到低电平,电机两端相当于被短路,会很快停下来,这叫刹车停止;把 ENA 置低,输出级变成高阻,电机靠惯性继续转,这叫滑行停止。课设里“按下停止键电机停住”通常用刹车停止,因为效果直观;如果做的是风机会要求断电后自然减速,那就用滑行。
2.3 用 C51 的 sbit 定义把真值表写成函数
有了真值表,C51 侧就是一组引脚定义和几个函数。下面是最小实现:
#include <reg52.h> sbit L298_IN1 = P1^0; sbit L298_IN2 = P1^1; sbit L298_ENA = P1^2; void motor_forward(void) // 正转 { L298_IN1 = 1; L298_IN2 = 0; L298_ENA = 1; } void motor_reverse(void) // 反转 { L298_IN1 = 0; L298_IN2 = 1; L298_ENA = 1; } void motor_brake(void) // 刹车停止 { L298_IN1 = 0; L298_IN2 = 0; L298_ENA = 1; } void motor_coast(void) // 滑行停止 { L298_ENA = 0; }这段代码对应上面的真值表,正转和反转只差 IN1/IN2 的电平组合。刹车时必须让 ENA 保持为 1,输出端才会被拉低形成短接制动;滑行只要把 ENA 置 0 即可。注意一个关键设计:后续加入 PWM 调速后,ENA 这个引脚会被定时器中断持续翻转,主函数里不要再单独执行 L298_ENA = 1 或 = 0 去覆盖它,否则 PWM 波形会被主流程打断,电机就会一顿一顿地转。
3. 用定时器中断写软件 PWM,把加减速做成 10%~90% 占空比步进
3.1 为什么不用延时循环调占空比
网上很多 C51 直流电机 demo 直接用延时函数写 PWM:ENA=1 后延时一段,再 ENA=0 延时一段,循环往复。这种做法在“只让电机转起来”的场景下能用,但一碰到按键就露馅:主循环扫描按键时如果执行了 10ms 消抖延时,PWM 波形就中断 10ms,电机转速会瞬间跌落,松手后又突然恢复。按键按得越快,电机抖得越厉害。
正确做法是把 PWM 的产生放进定时器中断,主循环只负责按键检测和档位设置。中断每次进入时给一个计数器加 1,然后用计数器的值和占空比阈值比较,决定 ENA 输出高还是低。这样按键消抖、方向切换再耗时,也不影响 PWM 波形的连续性。对 Proteus 仿真和课程设计来说,这是稳定性和可读性最均衡的方案。
3.2 软 PWM 的时基、周期和分辨率怎么定
软件 PWM 的核心参数有三个:定时器时基、PWM_PERIOD、pwm_duty。定时器时基决定多长进一次中断;PWM_PERIOD 决定多少个时基组成一个完整 PWM 周期;pwm_duty 是计数阈值,当前计数小于它时输出高电平。实际 PWM 频率等于 1 /(时基 x PWM_PERIOD)。
本方案推荐参数如下:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 晶振频率 | 12MHz | 机器周期 1us,计算初值方便 |
| 定时器 | T0 方式 1,16 位 | 软件重装初值 |
| 时基 | 50us | 给 TL0 装 0xCE |
| PWM_PERIOD | 100 | 分辨率 1% |
| PWM 频率 | 200Hz | 50us x 100 = 5ms |
| pwm_duty 范围 | 10~90 | 防止 0% 和 100% 极端占空比 |
有同学问为什么不用 100Hz 或者 1kHz。100Hz 接近人耳可听范围,实际电机容易发出“嗡嗡”声;1kHz 又要求 PWM_PERIOD 很小,步进 10% 时只有 20 个计数点,档位感太弱。200Hz 是 Proteus 仿真和多数玩具电机都能接受的折中值,如果实物出现明显啸叫,把 PWM_PERIOD 改成 20、PWM_STEP 改成 2,PWM 频率就能到 1kHz,代价是调速分辨率降到 5%。
3.3 按键加速减速、正反转和停止的完整 C51 程序
下面的代码可以直接在 Keil C51 中编译,烧录到 AT89C52 后配合 Proteus 仿真。五个独立按键接在 P3.0 到 P3.4,按下为低电平:
#include <reg52.h> #define PWM_PERIOD 100 // 100 个 50us 时基组成一个周期 #define PWM_STEP 10 // 每次按键改变 10% 占空比 #define PWM_MAX 90 // 最高 90% #define PWM_MIN 10 // 最低 10% sbit IN1 = P1^0; // L298 IN1 sbit IN2 = P1^1; // L298 IN2 sbit ENA = P1^2; // L298 ENA,由中断输出 PWM sbit KEY_ACC = P3^0; // 加速 sbit KEY_DEC = P3^1; // 减速 sbit KEY_FWD = P3^2; // 正转 sbit KEY_REV = P3^3; // 反转 sbit KEY_STOP = P3^4; // 停止 unsigned char pwm_tick = 0; // 中断计数 unsigned char pwm_duty = 50; // 当前占空比阈值 unsigned char motor_mode = 0; // 0=停止 1=正转 2=反转 void delay_10ms(void) { unsigned int i; for (i = 0; i < 1200; i++); } void set_direction(unsigned char mode) { switch (mode) { case 0: IN1 = 0; IN2 = 0; break; // 停 case 1: IN1 = 1; IN2 = 0; break; // 正转 case 2: IN1 = 0; IN2 = 1; break; // 反转 } } void timer0_init(void) { TMOD &= 0xF0; TMOD |= 0x01; // T0 方式 1,16 位定时 TH0 = 0xFF; TL0 = 0xCE; // 50us @ 12MHz ET0 = 1; EA = 1; TR0 = 1; } void timer0_isr(void) interrupt 1 { TH0 = 0xFF; TL0 = 0xCE; // 软件重装初值 if (motor_mode == 0) { ENA = 0; pwm_tick = 0; return; } pwm_tick++; if (pwm_tick >= PWM_PERIOD) pwm_tick = 0; if (pwm_tick < pwm_duty) ENA = 1; else ENA = 0; } void key_scan(void) { if (KEY_ACC == 0) { delay_10ms(); if (KEY_ACC == 0) { if (pwm_duty < PWM_MAX) pwm_duty += PWM_STEP; while (KEY_ACC == 0); } } if (KEY_DEC == 0) { delay_10ms(); if (KEY_DEC == 0) { if (pwm_duty > PWM_MIN) pwm_duty -= PWM_STEP; while (KEY_DEC == 0); } } if (KEY_FWD == 0) { delay_10ms(); if (KEY_FWD == 0) { if (motor_mode != 1) { set_direction(0); // 先刹车 delay_10ms(); // 换向死区 motor_mode = 1; set_direction(1); } while (KEY_FWD == 0); } } if (KEY_REV == 0) { delay_10ms(); if (KEY_REV == 0) { if (motor_mode != 2) { set_direction(0); delay_10ms(); motor_mode = 2; set_direction(2); } while (KEY_REV == 0); } } if (KEY_STOP == 0) { delay_10ms(); if (KEY_STOP == 0) { motor_mode = 0; set_direction(0); ENA = 0; while (KEY_STOP == 0); } } } void main(void) { timer0_init(); set_direction(0); while (1) { key_scan(); } }代码里最关键的是timer0_isr这一段。中断每 50us 进入一次,把pwm_tick加 1,当它小于pwm_duty时输出高电平,否则输出低电平。这样 ENA 上就形成了频率 200Hz、占空比可变的方波。main函数只做按键扫描,完全不参与电平翻转,因此即使按键消抖占用 10ms,PWM 波形也不会断。
按键处理里加入了 PWM_MAX 和 PWM_MIN 的限幅,避免占空比到 0% 或 100%。正转和反转按键内部先set_direction(0)再延时 10ms,这是方向切换死区,防止电机在高速反转时电流瞬间倒灌损坏 L298。
3.4 中断重装初值算错,是“电机转一会就停”的头号原因
代码里TH0 = 0xFF; TL0 = 0xCE;对应 50us 时基。计算方法是:12MHz 晶振的机器周期是 1us,定时 50 个机器周期需要计数 50 次;16 位定时器从 65536 开始向下计数,所以初值等于 65536 - 50 = 65486,换算成十六进制就是 0xFFCE。中断服务函数里每次必须重新给 TH0 和 TL0 赋值,否则定时器会从 0 开始重走 65536 个数,PWM 周期瞬间拉长,电机会出现明显的周期性停顿。
| 晶振 | 机器周期 | 定时 50us 的初值 |
|---|---|---|
| 12MHz | 1.000us | 0xFFCE |
| 11.0592MHz | 1.085us | 0xFFD2(约 49.9us) |
如果 Proteus 里晶振用的是 11.0592MHz,就不要照抄 0xFFCE,否则实际时基约 47us,PWM 频率会偏到 212Hz 左右,虽然不影响仿真演示,但示波器读出来的频率和理论值对不上时会干扰判断。
4. 在 Proteus 8 里复现整个驱动链路:元件接线、加载 HEX、示波器验证
4.1 Proteus 元件清单与 AT89C52 最小系统
Proteus 8 的元件库可以直接搜到本设计需要的所有元件。元件关键字如下:
| 元件 | Proteus 搜索关键字 | 数量 |
|---|---|---|
| 单片机 | AT89C52 | 1 |
| 电机驱动 | L298 | 1 |
| 直流电机 | MOTOR-DC | 1 |
| 独立按键 | BUTTON | 5 |
| 晶振 | CRYSTAL | 1 |
| 瓷片电容 | CAP | 2 |
| 电解电容 | CAP-ELEC | 1 |
| 电阻 | RES | 2 |
先放 AT89C52,把 12MHz 晶振接在 XTAL1 和 XTAL2 之间,两个 33pF 电容分别从晶振两端接地。复位电路用 10uF 电解电容和 10k 电阻接到 RST 脚即可。这部分和实际单片机最小系统完全一致,Proteus 里不画也能运行,但画出来更接近教材截图。
4.2 L298 与直流电机的连线关系
按照第 2 章的引脚表连线。单片机这边只用 P1 口三个引脚控制 L298,P3.0 到 P3.4 接五个按键,按键另一端接地,利用 P3 口内部上拉,按下时输入为低电平。
| 单片机引脚 | L298 引脚 | 说明 |
|---|---|---|
| P1.0 | IN1 | 方向控制 1 |
| P1.1 | IN2 | 方向控制 2 |
| P1.2 | ENA | PWM 使能输入 |
| GND | GND | 共地 |
电源在网络标签层面处理:L298 的 VS 接 12V,VSS 接 5V,GND 接地。Proteus 左侧工具栏选择 Terminals Mode,放置 POWER 和 GROUND。12V 电源可以用 DC POWER 元件直接接到 VS,也可以把电源元件的 Voltage 属性改成 12V。SENSEA 引脚直接接地,避免悬空导致 Proteus 仿真报错。
电机两端接 OUT1 和 OUT2,注意电机是交流符号还是直流符号并不影响,MOTOR-DC 就是直流有刷电机模型。若画面里电机反转方向和你预期相反,把两根输出线对调即可,软件无需改动。
4.3 加载 HEX 文件并观察占空比变化
在 Keil 里编译工程前,先到 Options for Target -> Output,勾选 Create HEX File,重新编译生成 .hex 文件。然后回到 Proteus:
- 双击 AT89C52 芯片,在 Program File 一栏选择刚才生成的 .hex 文件,Clock Frequency 改成 12MHz。
- 点击左下角运行按钮,先按 KEY_FWD,电机开始正转。
- 打开菜单 Debug -> Digital Oscilloscope,打开虚拟示波器。
- 把示波器探头 A 接到 ENA 网络标签上,可以看到约 200Hz 的方波。
- 按 KEY_ACC,方波高电平宽度变宽;按 KEY_DEC,高电平宽度变窄。
- 按 KEY_STOP,示波器波形直接归零,电机停止。
如果按下按键没反应,先检查按键属性。Proteus 的 BUTTON 元件默认是按下闭合,一端接单片机、另一端接 GND,初始状态为断开,这样才能读到低电平。如果按键接的是 VCC,程序里所有== 0的判断都要反过来。
虚拟示波器的横轴时间基准建议放到 10ms/div 或 5ms/div,这样能完整看到 5ms 的 PWM 周期。时间基准太短只能看到一条平线,容易误判成没有波形。
4.4 Proteus 仿真与实物调试的三个差异
仿真跑通后拿到实物上,往往会遇到几个 Proteus 里看不到的问题。第一个是 L298 的管压降,实际输出到电机的电压接近 VS 减去 2V,12V 供电时电机端大概只有 10V,Proteus 的电机模型不会体现这个压降,所以实物调试可以把 VS 提高到 13~14V。第二个是电机模型的反电动势和负载惯量过于理想,实物电机的加速减速有过渡过程,Proteus 里转速数字变化得非常快,这属于正常现象。第三个是 48V 无刷直流电机的应用场景不要套用 L298,L298 最高支持到 46V,但它只能驱动有刷电机,无刷电机需要三相六拍换相电路,两者驱动原理完全不同。
5. 把加减速手感调顺的三个边界细节:限幅、换向死区、斜坡步进
5.1 占空比限幅到 10~90,避免堵转和过流
代码上线后第一件事是确认 PWM_MAX 和 PWM_MIN。占空比为 10% 时,电机得到的平均电压很低,带负载可能转不动;占空比为 90% 时接近满压,L298 长时间工作发热明显。限幅的本质是把调速范围限定在电机能稳定运行且驱动芯片不过热的区间。如果发现 10% 电机完全不动,把 PWM_MIN 调到 15 或 20,不需要改其他逻辑。
5.2 正反转切换必须加死区,这是实物烧芯片最常见的坑
第 3 章代码里按 KEY_FWD 时先set_direction(0)再delay_10ms(),这 10ms 就是换向死区。电机从正转切反转时,转子还在正向旋转,驱动桥立刻反向输出会产生很大的反向电流。Proteus 不模拟这种瞬态,但实物中反复快速切换正反转,L298 的过热保护或短路保护就可能被触发。死区时间建议取 10~50ms,判断标准是切换瞬间电机不发“咔”的金属撞击声。
5.3 目标占空比与当前占空比分离,实现斜坡加减速
按键一次跳 10% 的控制适合演示“加速减速”逻辑,但手感偏硬。更接近真实产品的做法是把目标占空比和实际输出占空比分开,按键只修改目标值,主循环每 10ms 让实际占空比向目标值靠近 2%:
unsigned char target_duty = 50; if (target_duty > pwm_duty) { pwm_duty += 2; } else if (target_duty < pwm_duty) { pwm_duty -= 2; } delay_10ms();这段代码放在主循环里,每执行一次实际占空比变化 2%。比如目标从 50% 改到 90%,要经历 20 次循环约 200ms 的爬坡过程,电机电流冲击小,正反转切换时再配合死区延时,整个驱动的可靠性会比直接跳档高一个台阶。需要更陡的加速曲线就把步进从 2 改成 4,需要更平缓就把循环间隔从 10ms 加长到 20ms。
本文还有配套的精品资源,点击获取