C51驱动L298实现直流电机PWM调速与Proteus仿真详解
2026/9/16 20:30:30 网站建设 项目流程

简介:一份面向51单片机初学者的直流电机PWM控制完整工程,基于AT89C51/AT89C52与L298驱动芯片,实现电机加速、减速、正转、反转及停止控制,并配有Proteus仿真,适合课程设计、电子竞赛或入门实践。包内共21个文件,涵盖C语言源程序、Keil工程文件、Hex烧录文件、Proteus仿真工程及电路截图等,压缩包仅116KB,结构简洁,C源文件方便逻辑修改,仿真工程便于电路验证,图片则可快速预览整体接线。已有415人学习下载。资料完整覆盖了从独立键盘命令输入、单片机定时产生PWM波形,到L298驱动输出、1602液晶实时显示转速的完整信号链路,读者可据此理解PWM调速原理、L298控制方式及Proteus联调方法,并可直接烧录运行或修改参数扩展功能,快速完成同类直流电机控制项目。

1. C51 驱动 L298 的标题里,真正值得复现的是哪几件事

标题很长,但拆开看只有三个角色:C51 单片机负责判断方向和速度,L298 负责把控制逻辑放大成功率输出,直流电机是最终负载。加速减速不是改电压,而是调 PWM 占空比;正转反转不是换电源极性,而是切换 H 桥的对角导通;停止则有刹车停和惯性停两种做法。标题中出现的“软件源程序”和“Proteus 仿真”意味着这套逻辑全在 8051 内部用软件定时器解决,没有一个硬件 PWM 外设。放在学习路径里,这个组合刚刚好覆盖了单片机驱动功率器件最常见的接口问题:IO 电流不足、逻辑电平匹配、电源共地、定时中断产生稳定波形。下面按先电路、再代码、后仿真的顺序把整个流程铺开,你可以直接拿同一份代码改引脚和周期。

2. 驱动链路上的电平问题:L298 引脚、C51 IO 和电源地线怎么配合

先别急着写代码。C51 的 P1 和 P3 引脚输出高电平约 4.2V,电流只有几十毫安,直接接直流电机会因为负载电流过大导致输出电压跌落,甚至损坏单片机。L298 在这里做的事不是简单放大电流,它的每一路输出是一个 H 桥,内部四个功率管两两组合,把电机两端接到电源正或负。逻辑输入端需要很小的电流,信号电平兼容 5V TTL,因此 C51 可以直连,不需要再经过 ULN2003 这类中间驱动。

2.1 L298 双 H 桥和直流电机的四个状态

直流电机两端接上正电压就正转,反一反就反转,L298 的 IN1、IN2、ENA 组合起来正好表达了这几种状态。ENA 是使能端,为高电平时功率输出才生效;为低电平时无论方向引脚怎么设置,电机两端都不会被驱动。IN1 和 IN2 控制 H 桥的四个功率管,一组对角开通时电机正向通电,另一组对角开通时反向通电。

ENAIN1IN2电机行为应用场景
110正转前进,方向之一
101反转倒退,方向之二
111刹车制动线圈两端经功率管短路
100自由滑行断开驱动,惯性转动
0XX强制停止功率输出关闭

加速减速时不改 IN1、IN2,而是让 ENA 上出现 PWM 方波。占空比越高,平均输出电压越高,电机转速越快。这个表在 Proteus 仿真里可以直接验证:把 ENA 接高电平,切换 IN1、IN2,观察电机转向;再把 ENA 接到单片机 IO,用程序调制占空比,电机会随着脉宽改变转速。需要提醒的是,IN1 和 IN2 同为 1 叫有源刹车,电机内部线圈被短路,机械上会明显顿一下;同为 0 则是自由停车,电机靠惯性转。项目里常默认用同样的停止逻辑,但实际机械控制中两者差别很大。

2.2 C51 引脚分配和 P0 口的坑

仿真和实物调试之间最容易在第一版引脚定义上出问题。常见接法是:P1.0 和 P1.1 接 IN1、IN2,P1.2 接 ENA,P3.0 到 P3.3 接四个按键。Keil C51 里用 sbit 做位定义,代码非常直观:

sbit IN1 = P1^0; // 电机方向引脚 A sbit IN2 = P1^1; // 电机方向引脚 B sbit ENA = P1^2; // 使能脚,软件 PWM 输出 sbit KEY_UP = P3^0; // 加速 sbit KEY_DOWN = P3^1; // 减速 sbit KEY_REVERSE = P3^2; // 正反转切换 sbit KEY_STOP = P3^3; // 停止

这段位定义的意义是让代码和原理图对应起来。P1^0 是 Keil C51 的位寻址写法,8051 的 P1.0 是一个可直接独立操作的位,不需要为宏定义写复杂的寄存器赋值。注意如果手头板子把控制脚放在 P0,就要在外部加 10kΩ 上拉排阻,否则漏极开路输出无法稳定拉高。Proteus 仿真里很多人不加上拉一直调不通,往往就是 P0 口在高电平状态下电位悬空,L298 逻辑端读到随机电平。

2.3 Proteus 里 L298 的电源和共地设置

L298 在 Proteus 原理图里一般有 VCC 和 VS 两个电源入口,VCC 给内部逻辑电路供电,接 5V;VS 给电机功率供电,仿真里常用 12V。两个电源的负极和单片机的 GND 必须连到同一个网络,电机电流从 VS 流出,经过 H 桥、电机、功率管回到 GND,如果逻辑地和功率地分开,只能看到仿真波形却带不动电机。还要注意 L298 芯片内部虽然有续流二极管模型,但实物设计中电机两端仍应并联反向快速恢复二极管,防止关断瞬间感性电动势损坏芯片或单片机。这个设计在仿真中体现不出来,落到电路板时必须补上。

3. 用软件 PWM 实现加速和减速:定时器中断与状态机怎么写

8051 系列里很多型号没有硬件 PWM 外设,所以“加速减速”在代码层面就是做一个可以随时改变占空比的方波输出。常见做法是用一个定时器产生固定频率中断,中断里维护一个计数器,每计到一定次数就完成一个 PWM 周期,再拿计数器的值与目标占空比比较,决定 ENA 引脚输出高还是低。

3.1 定时器初值怎么算

以 12MHz 晶振为例,8051 的机器周期是 12 个时钟周期,所以定时器每计数一次需要 1μs。想让 PWM 频率在 1kHz 左右,可以设置每次中断 10μs,100 次中断构成 1ms 周期。定时器 0 工作在模式 1,16 位初值计算方式为 65536 - 10 = 65526,十六进制就是 0xFFF6。定时器溢出后进入中断函数,重新装载初值。

#define PWM_PERIOD 100 // 一个 PWM 周期包含 100 个时间片 volatile unsigned char pwm_threshold = 0; // 0~100,对应占空比 volatile unsigned char pwm_cnt = 0; // 时间片计数器 volatile unsigned char key_tick = 0; volatile unsigned char key_flag = 0; void timer0_isr(void) interrupt 1 { TH0 = 0xFF; TL0 = 0xF6; // 初值对应 10us 中断一次 if (++pwm_cnt >= PWM_PERIOD) pwm_cnt = 0; ENA = (pwm_cnt < pwm_threshold) ? 1 : 0; if (++key_tick >= 100) { // 10us * 100 = 1ms 扫描标志 key_tick = 0; key_flag = 1; } }

代码里 pwm_cnt 从 0 加到 99,pwm_threshold 决定高电平宽度。比如 pwm_threshold = 30,则一周期内前 30 个时间片输出高电平,后 70 个输出低电平,占空比就是 30%。TH0 和 TL0 的初值决定了定时中断频率,改小初值会让中断更频繁、PWM 频率上升,但单片机会把更多时间花在中断服务上,留给按键扫描和逻辑处理的时间变少。

3.2 正转、反转、停止这三组逻辑组合

方向控制不需要在中断里做,主循环里根据按键状态改 IN1、IN2 即可。可以维护一个方向变量,把三态映射写清楚,避免每次按下都写一堆 if 判断:

#define MOTOR_STOP 0 #define MOTOR_FORWARD 1 #define MOTOR_REVERSE 2 void set_direction(unsigned char dir) { switch (dir) { case MOTOR_FORWARD: IN1 = 1; IN2 = 0; break; case MOTOR_REVERSE: IN1 = 0; IN2 = 1; break; default: // 停止 IN1 = 0; IN2 = 0; break; } }

这里选择 IN1、IN2 都为 0 的停止方式,相当于把电机两个端子都从功率电源上断开,让电机靠惯性停下来。如果想要快速停车,可以把默认分支改成 IN1 = 1、IN2 = 1,让两组下管构成短路制动,电机立刻抱住。程序上只需要改两个赋值,差别可以对照表 2.1 去理解。

参数数值含义
定时器中断周期10μs时间片最小粒度
PWM 周期100 × 10μs = 1ms频率 1kHz
pwm_threshold0~100占空比百分比

3.3 把按键转成目标速度和运行方向

按键不能直接连到中断里做加减速,因为按下时间不确定,必须扫描后转换为目标值。下面这一段在 1ms 的 key_flag 有效之后执行,每次按键改变一个档位,停止键直接清空目标速度:

void main(void) { unsigned char target_speed = 0; unsigned char direction = MOTOR_FORWARD; TMOD = 0x01; TH0 = 0xFF; TL0 = 0xF6; ET0 = 1; EA = 1; TR0 = 1; set_direction(direction); while (1) { if (!key_flag) continue; key_flag = 0; if (KEY_UP == 0 && target_speed < 100) { target_speed += 10; // 每次加 10%,连续按 10 次到最大 } if (KEY_DOWN == 0 && target_speed > 0) { target_speed -= 10; } if (KEY_REVERSE == 0) { direction = (direction == MOTOR_FORWARD) ? MOTOR_REVERSE : MOTOR_FORWARD; set_direction(direction); } if (KEY_STOP == 0) { target_speed = 0; } pwm_threshold = target_speed; } }

这个循环的关键在于 key_flag 由中断置位,主循环只在标志到来时读取一次按键电平。每次加 10 这个步进是人为选的,想细腻一点可以改成加 2,但把按键消抖时间计算进去后,实际手感会变得很“跳”。加速键按下后每毫秒加一格,松开后立即停止,所以不会出现按住一次就自动跑到最大速度的问题。若需要长按连续加速,应该改成按下后延时 500ms 再每隔 20ms 加一格,这部分的延时逻辑还是复用 key_flag。

4. Proteus 仿真从搭建到跑起来:元件连线、HEX 加载和速度观察

Proteus 仿真在这个项目里的作用是提前看清 L298 逻辑输入能不能正确驱动电机。你不需要一开始就在硬件上烧片子,先把 AT89C51、L298 和直流电机模型放上去,验证程序逻辑足够,再转到实物。

4.1 在原理图里放哪些元件,引脚连到哪

新建 Proteus 项目后,从元件库中选出的常用器件如下表,连接关系尽量和代码里的 sbit 定义保持一致,否则程序烧进去方向不对。

元件库关键词说明
AT89C51AT89C5112MHz 晶振,P1/P3 作为控制脚
L298L298双 H 桥驱动
DC MotorMOTOR-DC用默认 12V 模型
按钮BUTTON接 P3 口,按下为低电平
电阻RESP3 口上拉到 VCC,取 10kΩ
电解电容CAP-ELEC电机电源两端滤波

连接时的顺序影响调试体验。P3 口作为输入必须先接上拉电阻到 5V,再让按键一端接地。P1 口直接连到 L298 的 IN1、IN2、ENA,不需要上拉。电机接在 L298 输出 OUT1 和 OUT2 之间,电源端 VS 接 12V,VSS 接 5V,所有负极端连到一个 GND 网络。P0 口如果空着就不用管,Proteus 不会把悬空引脚当成故障。

4.2 用 Keil 生成 HEX 文件后怎么载入 L298 项目

仿真依赖 HEX 文件,C51 项目编译后才能写进单片机模型。Keil 中需要在 Options for Target 的 Output 标签页勾选 Create HEX File,然后用常规方式构建项目。不使用 Keil 时,也可以用 SDCC 这类开源编译器,命令大致如下:

sdcc -mmcs51 --iram-size 256 -c motor.c sdcc -mmcs51 motor.rel packihx motor.ihx > motor.hex

这里第一步把 motor.c 编译成 8051 的重定位目标文件,第二步链接,第三步把 Intel HEX 文本写出来。生成后在 Proteus 里点击 AT89C51,在 Edit Properties 的 Program File 中选择 motor.hex,把 Clock Frequency 设成 12MHz,点击运行仿真。不要忘记给单片机加电源符号,Proteus 默认隐藏电源引脚,但 AT89C51 的 VCC 和 GND 必须接入电源网络,否则程序根本不会跑。

4.3 用虚拟示波器看加速减速是否在发生

运行仿真后,电机能转不代表 PWM 正确,因为加速减速从外观上可能只是一瞬间的变化。Proteus 左侧工具条的 Virtual Instruments 里有 Oscilloscope,把探头 A 接到 ENA 引脚,探头 B 接到电机一端,然后对比加速前后波形。加速后 ENA 的方波高电平时间明显变宽,也就是占空比变大;减速时高电平时间变窄。电机两端是经过 H 桥调制的方波,不是干净的直流,用示波器观察时要把 Channel 的耦合方式改成 DC,否则看不到低频方波的真实高度。如果 ENA 波形一直是 0V,通常是没有给小电动机驱动的 VS 电源,或者 pwm_threshold 始终为 0,先暂停仿真,检查按键加速分支是否进入了。

5. 参数选型和仿真调试:PWM 频率、按键消抖和常见故障

到了这一步,整个系统已经能在 Proteus 里动起来,但离稳定运行还有一段距离。L298 这类双极型驱动芯片对 PWM 频率敏感,按键扫描也需要消抖,仿真和实物的差异更常见。

5.1 PWM 频率选 1kHz 还是 10kHz

上一章把 PWM 频率设计成 1kHz,这是学习项目常用的折中。频率太低,比如 50Hz,电机转动会一顿一顿,声音也能明显听到“咔咔咔”;频率太高,比如 20kHz,L298 的功率管开关损耗变大,芯片升温快,Proteus 仿真时间步长变小,运行速度也会拖慢。L298 的数据手册里并没有强制规定最高开关频率,但从实际项目看,2kHz 以内比较稳,10kHz 以上就需要注意 H 桥上下管直通保护和续流二极管恢复时间。如果你打算把 L298 换成 TB6612FNG 这类 MOSFET 驱动,可以把频率提到 10kHz,因为 MOSFET 开关损耗低,而且这个电子转换并不影响方向控制逻辑,只需要改 ENA 的 IO 连接。

5.2 按键消抖和单次触发

3.3 节的按键判断没有消抖,Proteus 里按钮模型是理想开关,看不出来问题,实物上按下和释放会产生几毫秒抖动,一次按下可能被读成多次加档。可以在按键扫描里加一个简单的移位寄存器滤波器,连续 8 次读到低电平才认为按下:

unsigned char read_key_filtered(unsigned char raw) { static unsigned char filter = 0xFF; filter = (filter << 1) | raw; return (filter == 0x00) ? 1 : 0; }

filter 初始为 0xFF,低电平有效按键按下时 raw = 0,移位寄存器会逐步变成 0x00,表示按键被稳定读取到。这段代码的代价是按键响应会慢 8ms 左右,在 C51 的 1kHz 扫描周期里可以接受。实际项目中还需要在按下和释放沿加入计数,避免一次按下被重复处理,但 Proteus 仿真中先用移位滤波已经够用。

5.3 常见仿真现象和解决清单

仿真失败大多发生在电源、地线、HEX 加载这三类问题上,整理成表可以节省很多排查时间:

仿真现象可能原因排查方向
电机完全不转ENA 没接到程序控制的引脚用示波器看 ENA 是否有 PWM
电机只有一个方向IN1、IN2 接反但程序只看一个键对调 L298 输入端接线
加速键没反应P3 口上拉电阻缺失,按键电平读不到用电压表量 P3 引脚电压
一上电电机就猛转target_speed 初始值不为 0main 开头显式清零
电机转速固定不能减PWM 输出接在 IN1 而不是 ENA确认程序控制的是使能脚
仿真速度极慢PWM 频率设置过高降到 1kHz 再观察

大多数情况下,把代码里的参数和原理图连线逐一对照就能定位。Proteus 仿真模型不会烧芯片,所以可以大胆尝试把不同输出引脚接到 L298 输入端,直到找出正反转方向一致的那组配置,再固化到 PCB 连线里。注意仿真中 L298 的电源模型比较容易忽略 VSS,也就是逻辑电源,如果 VSS 悬空,即使 VS 有 12V,IN1 和 IN2 也不起作用。

6. 给 L298 加一个软启动斜坡,把占空比变化做平滑

前面直接把 target_speed 赋给 pwm_threshold,按键按下去时占空比瞬间从 30% 跳到 40%,连接轴和齿轮的电机很容易产生冲击。实际控制里更常见的做法是让实际 PWM 阈值以一定速度向目标值靠近,也就是加一个斜坡函数。这样加速曲线是斜线而不是台阶,机械结构的应力会小很多。

unsigned char motor_ramp(unsigned char current, unsigned char target, unsigned char step) { if (current < target) { if (target - current < step) return target; else return current + step; } if (current > target) { if (current - target < step) return target; else return current - step; } return current; }

函数里 step 是每毫秒允许改变的占空比幅度。如果当前值是 20,目标值是 60,step 取 2,主循环需要 20 次扫描才能把速度推上去,也就是大约 20ms 完成加速。step 设成 1 时坡更缓,适合带负载启动;step 设成 10 后响应快,但和直接赋值差别不大。将这函数放到 3.3 节主循环的 key_flag 处理之后,按键事件里修改 target_speed,while 循环里执行pwm_threshold = motor_ramp(pwm_threshold, target_speed, 2);,就能在不改变任何硬件连接的情况下消除起步冲击。

斜坡函数还有一个好处是自然限制了停止时的电流变化。按下停止键时,target_speed 置 0,pwm_threshold 从高占空比逐步降低,电机在减速过程中不会突然被切断电源或短路,因此抖动明显减小。要更快停车,可以单独做一个刹车分支:当 target_speed = 0 且当前速度小于某个阈值时,直接把 IN1、IN2 设为 1、1,用线圈短路制动收尾。这个组合是 L298 控制里比较完整的策略,后面如果换用 TB6612FNG 或者其他电机驱动板,只需把频率提高到 10kHz 左右,斜坡逻辑原样保留,这也是从直流电机调速迁移到无刷直流电机驱动原理时值得保留的写法。

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

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

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

立即咨询