51单片机L298直流电机PWM调速:原理、Keil工程与Proteus仿真实现
2026/9/16 16:32:44 网站建设 项目流程

简介:这是一套专门用于直流电机控制的PWM调速设计方案,采用51单片机作为主控芯片,搭配L298驱动芯片完成电机的加速、减速、正转、反转以及停止控制。资源包总共包含21个文件,压缩后体积只有116KB,内容涵盖C语言程序源码、Keil工程文件、Proteus仿真电路、Hex烧录文件以及3张运行效果截图,其中程序源码与仿真工程还分别打包成两个zip压缩包,便于独立下载和对照使用。目前已有415人学习浏览,适合单片机初学者、相关课程设计以及电子设计竞赛备赛者参考学习。方案中通过独立式键盘输入控制命令,单片机定时产生脉宽可调的PWM波形并送入L298驱动芯片,从而调整电机的转速与转向,同时利用1602液晶显示屏实时显示当前转速,整个过程涵盖PWM信号生成、电机驱动、按键交互和数据显示,逻辑清晰,非常适合用来理解直流电机控制的基本流程。所有源码和仿真工程都已调试完成,下载后可以直接用Proteus打开运行,也可以根据C语言源程序修改参数,进而移植到实际硬件电路中进行功能扩展。

1. C51单片机配L298驱动直流电机的第一关:先分清占空比和方向逻辑

很多人第一次拿51单片机控制L298直流电机时,会以为让电机转慢一点就得把控制电压调低,结果代码写出来电机要么不动,要么直接就全速飞转。真正稳的做法是用PWM调占空比,而且要把“方向逻辑”和“速度逻辑”分开处理:L298的INA和INB管高电平/低电平组合决定正反转,EN管脚上的PWM波形决定平均电压。这个基于AT89C51/AT89C52的直流电机控制工程把按键输入、PWM生成、L298驱动、1602显示全部串起来,源文件里既有Keil工程也有Proteus仿真,适合应付课程设计和快速搭建实物原型。对做电机控制的工程师来说,它也是一个很好用的开环调速参考模板。

2. PWM调速原理与L298接口设计:为什么占空比决定转速

2.1 直流电机调速为什么不直接改模拟电压

直流电机的转速和电枢两端平均电压近似成正比,但从单片机角度直接输出一个可变的模拟电压很不划算。AT89C51/AT89C52的I/O口只有高电平和低电平,想要模拟电压要么外接DAC,要么用运放加RC滤波,成本和复杂度都上去了。PWM相当于用一个固定周期的数字脉冲去控制平均电压,占空比越高,等效电压越高,转速越快。

PWM周期要选得合适。频率太低电机会一顿一顿,噪音也大;频率太高超过电机换向和电路驱动能力又不一定响应。常见做法是选1kHz到20kHz,这个项目用定时器0做1ms周期,也就是1kHz基频,实际驱动小直流电机手感很稳。计算初值时按12MHz晶振、方式1、12分频来算:12MHz除以12得到1MHz,即1个机器周期是1微秒,1ms需要计数1000次,所以初值是65536-1000=64536,换成十六进制就是0xFC18。

2.2 L298输入逻辑与四个状态的映射

L298内部是H桥结构,IN1和IN2控制电流方向,ENA使能端控制整个通道是否供电。要转速可调,就把PWM信号送到ENA,把方向信号送到IN1/IN2。注意:即便ENA为高,如果IN1和IN2同为高电平,也会形成内部刹车,这和停止状态不一样。下面是整个调速逻辑的核心状态表。

ENAIN1IN2电机行为
0任意任意励磁关断,自由滑行
110正转,速度受ENA占空比控制
101反转,速度受ENA占空比控制
111急停/刹车,两个下桥同时导通

实际接线时,把电机两端接到OUT1和OUT2,电源正极接L298的VS(电机电源),逻辑电源接VSS。如果主控板是5V,电机是12V,也要保证两边共地,否则IN1/IN2电平参考点不一致,单片机说的高电平在L298看来可能完全不成立。这个共地问题在Proteus仿真里不点破很容易被忽略,因为仿真默认所有地是同一个地。

2.3 最小控制代码:单定时器产生可调PWM

在C51里生成PWM不需要复杂外设,一个定时器中断配合计数器变量就够了。下面这一段是完整调速的基础,后续按键加减速度也是改这个变量。

sbit IN1 = P1^0; sbit IN2 = P1^1; sbit ENA = P2^0; unsigned char speed_level = 50; // 占空比等级 0~100 unsigned char pwm_counter = 0; void Timer0_Init(void) { TMOD &= 0xF0; // 保留 T1 设置 TMOD |= 0x01; // T0 方式1:16位定时器 TH0 = 0xFC; // 12MHz 下 1ms 定时初值高字节 TL0 = 0x18; // 初值低字节,补充到 64536 ET0 = 1; // 打开 T0 中断 EA = 1; // 打开总中断 TR0 = 1; // 启动定时器 } void Timer0_ISR(void) interrupt 1 { TH0 = 0xFC; // 重新装入初值 TL0 = 0x18; if (++pwm_counter >= 100) // 周期分成 100 份 pwm_counter = 0; if (pwm_counter < speed_level) ENA = 1; // 前 speed_level 份为高 else ENA = 0; // 剩余部分为低 }

这段代码的思路是让pwm_counter从0循环到99,每个周期100份。speed_level代表高电平持续份数,50就是50%占空比。以后加速减速只需要在按键代码里修改speed_level。这种方法比在每个中断里重新计算重装值稳定,因为定时器周期始终固定,只是高电平时长变化。代码里TMOD先与0xF0清除T0位,再或0x01设置方式1,这种写法避免了影响T1,在多定时器工程里比较规范。

3. Keil C51工程逐段拆解:按键状态机、双定时器PWM和1602显示

3.1 打开工程与源文件组成

下载解压后,拿着直流电机.uvproj直接双击,Keil uVision4/uVision5都能识别这个工程文件。源程序主体是main.c,另外那些.lst.m51.plg是编译过程产物,不用提交,留着也不影响。如果打开时提示找不到设备,去Keil的Pack Installer里补装Atmel AT89C52芯片包。这里有个容易踩的坑:Keil 5在安装C51编译器时如果只选了ARM,打开51工程就会报toolchain not installed,切到Manage Project Items把Toolchain改成C51 Compiler,再指定C51路径即可。

3.2 按键输入与运行状态机

独立键盘在Proteus仿真里就是几个BUTTON,引脚默认高电平,按下为低。扫描代码要做去抖,否则一次按下可能被当成多次。正常项目里按键处理不会放在主循环while里空转,但这个51工程主要是教学用途,直接延时去抖是最容易复现的写法。

sbit KEY_ACC = P3^0; // 加速 sbit KEY_DEC = P3^1; // 减速 sbit KEY_FWD = P3^2; // 正转 sbit KEY_REV = P3^3; // 反转 sbit KEY_EN = P3^4; // 启/停 void delay10ms(void) { unsigned char i, j; for (i = 0; i < 20; i++) for (j = 0; j < 250; j++); } void Key_Scan(void) { if (KEY_ACC == 0) { delay10ms(); if (KEY_ACC == 0) { if (speed_level < 90) speed_level += 5; while (!KEY_ACC); // 等待松开 } } if (KEY_DEC == 0) { delay10ms(); if (KEY_DEC == 0) { if (speed_level > 10) speed_level -= 5; while (!KEY_DEC); } } if (KEY_FWD == 0) { delay10ms(); if (KEY_FWD == 0) { IN1 = 1; IN2 = 0; while (!KEY_FWD); } } if (KEY_REV == 0) { delay10ms(); if (KEY_REV == 0) { IN1 = 0; IN2 = 1; while (!KEY_REV); } } if (KEY_EN == 0) { delay10ms(); if (KEY_EN == 0) { IN1 = 0; IN2 = 0; ENA = 0; speed_level = 0; while (!KEY_EN); } } }

这里把速度范围限制在10到90,是为了避免占空比过低时电机因死区电压不转,也避免过高时电流冲击。按键后加上等待释放循环,虽然会占用主循环,但在这种单任务工程里是稳定可靠的。如果是真实产品,建议改成检测下降沿加短去抖,不等待释放。

3.3 1602显示转速与PWM等级同步

摘要里说1602要实时显示转速,这个工程源码实际上是显示speed_level这个百分比数值,也就是PWM占空比等级,而不是真实RPM。如果后续要显示真实转速,可以在电机轴上装编码器,用计数器测速后换算。这里先看显示逻辑,把speed_level拆成个位、十位、百位再写进1602。

void LCD_WriteSpeed(void) { unsigned char buf[4]; buf[0] = speed_level / 100; // 百位 buf[1] = speed_level % 100 / 10; // 十位 buf[2] = speed_level % 10; // 个位 LCD_WriteChar(0, 7, '0' + buf[0]); LCD_WriteChar(0, 8, '0' + buf[1]); LCD_WriteChar(0, 9, '0' + buf[2]); LCD_WriteChar(0, 10, '%'); }

写入位置是第0行第7列,也就是1602屏幕第一行的Speed: 后三位显示。用LCD_WriteChar这个自定义函数的原因是为了避免在C51里使用sprintf,后者会引入很重的库代码,浪费51单片机本就不宽裕的Flash。如果确实需要格式化输出,用指针逐步拼字符会更可控。

3.4 编译生成hex文件的必须设置

Keil里编译源文件后,默认输出hex文件。如果没有生成,需要在Options for Target 的 Output 页勾选Create HEX File,否则Proteus仿真里加载不了编译结果。勾选后点击Rebuild,工程目录下会出现直流电机.hex。那个.uvopt.bak文件是环境配置和备份,不是代码,不用理会。

3.5 一个容易被忽略的定时器冲突点

如果同时用定时器0产生PWM、定时器1做测速,主循环里千万不要对TMOD整体赋值,比如TMOD = 0x11。这种写法会同时覆盖T0和T1的工作方式,假如T1已经被设置为计数模式,就会瞬间变成定时器模式,导致外部脉冲计数异常。规范化写法是在初始化里分别保留不修改的位,就像前面第2章的TMOD &= 0xF0

4. Proteus仿真搭建与L298电机调试:从原理图到虚拟示波器

4.1 元件清单与引脚连接

仿真文件里已经有完整的原理图,这里给出从零搭建时需要用的元件和位置逻辑,方便自己重画或排查接线。

Proteus关键字元件说明接线要点
AT89C52主控单片机Program File里加载hex文件,Clock Frequency改成12MHz
L298双H桥驱动器VS接电机电源正极,VSS接5V逻辑电源
MOTOR-DC直流电机两个引脚接L298的OUT1、OUT2
1602 LCD液晶屏RS、RW、E接P2.0~P2.2,数据D4~D7接P0.4~P0.7
BUTTON按键一端接P3口,另一端接GND,另一边接10k上拉到VCC
CRYSTAL晶振12MHz,两个震荡电容对地

连接L298时特别注意:ENA要接到P2.0上,IN1和IN2分别接P1.0和P1.1。如果ENA直接接VCC,PWM调速就失效了,只剩方向控制,这是仿真是最容易出现的“能转不能调速”问题。

4.2 加载hex和设置时钟频率

双击Proteus原理图里的AT89C52元件,在Program File中选择工程目录下编译好的直流电机.hex。还要把Internal Clock改为12MHz,这个值必须和Keil里定时器初值计算时用的晶振一致。如果这里保持默认的1MHz,定时器1ms中断会变成6ms左右,电机转速明显变慢,按键加减速也像延迟了一样。

4.3 用虚拟示波器检查ENA引脚波形

在Proteus左侧工具栏选择“Virtual Instruments”,找到模式中的Oscilloscope,把探头接到ENA网络。运行时按下加速按键,应该能看到波形高电平宽度逐渐变大,占空比从10%向90%增加。如果按下按键后波形跳变过快,可能是按键去抖时间不够,或者主循环里没有等待释放导致一次按下扫描了多次。

4.4 仿真失败时的排查顺序

  • 电机完全不动:先看L298的VS和VSS是否都接电源,再看AT89C52是否加载了hex文件。
  • 能转但不能调速:ENA没有接单片机引脚,或者PWM信号没从ENA进。
  • 按键按下没反应:P3口是否接了上拉电阻,Proteus里有些按键模型需要外部上拉才能保证高电平稳定。
  • 1602只亮不显示:对比度引脚V0悬空导致的,接一个10k电位器,中心抽头接V0,调节到第三行清晰即可。
  • L298发热严重:检查电机电源极性,或者IN1/IN2是不是出现了同时为高的情况。

排查的时候先从电源和共地开始,再看信号网络标号是否一一对应,最后才考虑代码逻辑。仿真里的问题绝大多数是网络连接错误,而不是代码编译错。

5. 把开环PWM闭环化的收尾:编码器测速与占空比限幅技巧

5.1 用外部中断做真实转速测量

如果要把占空比显示改成真实电机转速,最简洁的路子是给电机加一个测速码盘,用光电开关输出脉冲,把脉冲送到AT89C52的外部中断INT0引脚。每来一个脉冲进入一次外部中断,对计数器加1,再用定时器1做固定1秒的时间闸门,每秒读一次计数并清零。

unsigned long pulse_count = 0; unsigned int current_rpm = 0; void External0_ISR(void) interrupt 0 { pulse_count++; // 码盘每转一圈产生的脉冲会被多次累计 } void Timer1_ISR(void) interrupt 3 { static unsigned char second_count = 0; if (++second_count >= 50) // 参数以实际定时器中断周期为准 { second_count = 0; // 假设码盘单圈20个脉冲,则每分钟转速为 秒计数*60/20 current_rpm = (unsigned int)(pulse_count * 3); pulse_count = 0; } }

这里假设码盘是20线,如果改为每圈200线的编码器,把修正系数换成pulse_count * 60 / 200。实测时用转速表对比,然后微调这个常数。因为这个工程原本是开环PWM控制,加上测速后可以用一个最简单的比例控制:实际转速低于目标值时提高speed_level,高于目标值时降低speed_level。

5.2 占空比限幅为什么比简单归零更实用

按键减速把speed_level减到0,L298输出完全没有PWM信号,电机进入励磁关断状态,如果此时电机还在转,它会靠惯性滑行很久。更实用的急停是“制动”而不是“断电”,也就是让IN1和IN2同时为高,把电机两端短路,利用电机制动效应迅速停下。如果只在代码里把speed_level归零,就丢掉了这个刹车特性。

5.3 最后的验证方法

仿真里跑通后,把AT89C52换成AT89C51也不需要改外围,因为两个芯片在这个工程里兼容。唯一要重查的是AT89C51的RAM比AT89C52少了128字节,如果显示模块用了数组缓冲区,要留意内存占用。没有避免不了的问题,只有不该省的定时器初值:占空比分辨率如果用200份,定时器初值和中断里的比较阈值都要同步改,否则显示刷新速度会和PWM周期错位,导致1602上的数字跳动明显不稳定。

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

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

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

立即咨询