51单片机控制直流电机:从PWM调速到状态机复位与按键消抖实践
发布时间:2026/9/14 5:22:39 作者:尧图编辑部 阅读量:1,286

简介一套面向51单片机学习者的自动清洗机完整开发资料覆盖proteus仿真、源码、原理图、流程图及元件清单等核心内容。项目以3个直流电机为执行机构实现X/Y向导轨运动控制、清洗喷头抽水并通过开始、结束、暂停按键操作暂停后导轨自动复位这一逻辑尤其适合作为课程设计或毕业设计参照。压缩包共33个文件大小仅591KB包含C语言源代码、hex烧录文件、Proteus仿真工程DSN、原理图SchDoc、PDF预览、物料清单xlsx及流程图bmp等类型完整且便于直接打开学习。已有174人浏览学习。包内还附带功能介绍文本可快速理解设计思路仿真与源码对应读者可逐步调试按键控制直流电机及导轨复位逻辑也可基于现有工程修改扩展用于单片机接口、电机驱动或嵌入式系统等方向的实践训练。1. 一张原理图看清三电机清洗机的控制骨架拿到这套“基于51单片机的清洗机”资料时第一感觉是“三个按键、三个直流电机简单”但真正把Proteus仿真跑起来之后才发现难点不在“转”而在“停下来还要回去”。三个电机里两个分别驱动X、Y方向导轨一个驱动清洗喷头抽水按键只有开始、结束、暂停而暂停后要求X、Y导轨回到初始位置。这个“复位”动作才是整套代码的状态机核心。它涉及IO分配、PWM调速、按键消抖、限位判断和运行状态切换正好适合用来理解51单片机在真实小系统里怎么组织任务。下面按“驱动选型 → 状态机 → Proteus复现 → 按键细节”的顺序拆开讲资料里的原理图、流程图、物料清单、仿真图和源码可以对照着看。2. 51单片机怎么同时管住三个直流电机IO分配、PWM与驱动选型2.1 为什么不能把电机直接接在单片机引脚上常见误区是把直流电机的两根线直接接到P1口觉得给高电平就转。实际51单片机的IO口输出电流很小标准模式下灌电流也就十几毫安推挽模式下也带不动一个几百毫安的直流电机。工作电压也不匹配电机额定6V或12V单片机是5V。所以中间必须加驱动芯片常见的组合是L298N或ULN2003。L298N适合两路电机而且带使能端ENA、ENB可以直接接PWM信号调速ULN2003则是达林顿管阵列适合单向开关控制但调速不方便。这套清洗机的三个电机里X、Y轴导轨需要正反转来控制前进和后退所以每个导轨电机至少需要两个方向IO和一个使能IO。喷头抽水电机只需要单向运转但为了调速和统一管理也建议用相同的驱动方式。最省事的方案是两片L298N第一片管X、Y两个导轨电机第二片只用其中一路管水泵。主控用STC89C52或AT89C51晶振11.0592MHz这样既与Proteus仿真兼容也方便后面用定时器做PWM。2.2 IO分配与驱动连接关系在画原理图之前最好先把IO口规划写死在表格里。下面这张表直接参考了资源里“元件清单”和原理图的常见连接方式实际工程里也建议照这个格式维护一份避免写代码时反复查原理图。功能引脚方向说明X轴电机方向AP2.0输出L298N IN1X轴电机方向BP2.1输出L298N IN2X轴电机使能PWMP1.0输出接L298N ENAY轴电机方向AP2.2输出L298N IN3Y轴电机方向BP2.3输出L298N IN4Y轴电机使能PWMP1.1输出接L298N ENB水泵电机方向AP2.4输出第二片L298N IN1水泵电机方向BP2.5输出第二片L298N IN2水泵电机使能PWMP1.2输出第二片L298N ENA开始按键P3.0输入上拉电阻到VCC结束按键P3.1输入上拉电阻到VCC暂停按键P3.2输入上拉电阻到VCCX轴限位开关P3.3输入低电平触发Y轴限位开关P3.4输入低电平触发注意P1.0到P1.2作为PWM输出负责调速P2口控制方向。电机正转意味着导轨前进反转就是后退。限位开关装在导轨的“零点”位置暂停后复位就是让电机反转直到碰到限位开关。这里不要依赖步数估算因为直流电机会丢步用限位开关才是可靠做法。2.3 用定时器模拟三路PWM代码与参数说明51单片机没有硬件PWM模块所以三路PWM要靠定时器中断软件翻转。常见做法是用定时器0产生1ms节拍再维护一个0到99的计数器让每路比较占空比。下面这段代码就是从实际项目里抽出的核心片段可以直接嵌到Keil工程里。// 用定时器0产生1ms节拍在中断里按占空比输出PWM // P1.0: X轴电机使能P1.1: Y轴电机使能P1.2: 水泵电机使能 unsigned char pwm_count 0; unsigned char pwm_x 80; // X轴占空比 0~100 unsigned char pwm_y 80; // Y轴占空比 0~100 unsigned char pwm_pump 90; // 水泵占空比 void Timer0_Init(void) { TMOD 0xF0; TMOD | 0x01; // 定时器0模式116位 TH0 0xFC; // 12MHz晶振下延时1ms TL0 0x18; ET0 1; TR0 1; EA 1; } void Timer0_ISR(void) interrupt 1 { TH0 0xFC; TL0 0x18; pwm_count; if (pwm_count 100) pwm_count 0; P1_0 (pwm_x pwm_count) ? 1 : 0; P1_1 (pwm_y pwm_count) ? 1 : 0; P1_2 (pwm_pump pwm_count) ? 1 : 0; }这里把PWM周期定为100个1ms也就是100Hz频率偏低但拖动小直流电机足够而且Proteus仿真时肉眼能看得见转速变化。pwm_count从0数到99当pwm_x设为80时一轮周期里80ms高电平、20ms低电平电机得到约80%的占空比。真机上如果电机噪声大可以把这个周期改到10ms也就是把pwm_count上限改成10但那样TH0和TL0要重新计算。要注意这段代码只输出PWM使能信号真正驱动电机的大电流还是要靠L298N输出。如果把电机线直接接在P1.0上轻则转不动重则烧单片机引脚。3. 开始、结束、暂停三个按键的状态机以及暂停复位的实现3.1 状态定义空闲、运行、暂停这个清洗机的交互逻辑看起来是三个按键但暂停后的行为是“导轨复位”所以不能只按电平高低去控制电机。合理做法是定义一个状态机三个状态分别是空闲、运行、暂停。开始键在空闲和暂停时有效结束键在运行和暂停时有效暂停键只在运行时有效。暂停键按下后X、Y电机先停然后执行复位流程复位完成后系统停在暂停状态等开始键重新进入运行。下面是状态转换的表格写代码前先把这个表画出来主循环的调度逻辑就会非常清楚。当前状态按键动作下一状态空闲开始启动三电机运行空闲结束无动作空闲运行暂停停止电机并复位X、Y暂停运行结束停止所有电机空闲暂停开始启动三电机运行暂停结束停止所有电机空闲注意暂停状态下如果复位还没有完成必须屏蔽“开始”按键否则导轨还没回到零点又开始前进坐标就乱了。所以在代码里要额外加一个reset_done标志位。3.2 暂停复位导轨回原点不能只靠“倒转”很多人一看到“暂停后导轨复位”就直接写“让电机反转一段时间”这在真机上非常危险。直流电机反转时间长短无法精确控制导轨可能撞到机械末端或者根本回不到原位。正确做法是靠限位开关确定零点。X轴和Y轴各装一个机械限位开关平时被压住或释放时输出低电平复位时让对应电机反转直到单片机读到限位开关触发信号再立刻停掉该轴。复位顺序也有讲究两个轴不能同时复位因为如果两个导轨运动范围有干涉同时复位可能卡死。常见做法是先复位X轴等X轴限位开关触发后再复位Y轴。水泵电机不参与复位暂停时直接关掉即可。复位完成后把当前坐标变量清零否则再次运行时软件不知道导轨在哪。3.3 主循环里的状态调度代码下面这段代码是主循环的核心包含按键读取、简单消抖和状态切换。实际工程里建议把按键消抖改成第5章的边沿检测这里为了演示状态机先用延时消抖。typedef enum { IDLE, RUNNING, PAUSED } SysState; SysState state IDLE; unsigned char reset_done 0; sbit KEY_START P3^0; sbit KEY_STOP P3^1; sbit KEY_PAUSE P3^2; sbit X_LIMIT P3^3; sbit Y_LIMIT P3^4; void delay_ms(unsigned int t) { unsigned int i, j; for (i 0; i t; i) for (j 0; j 120; j); } unsigned char read_key(void) { unsigned char k 0; if (KEY_START 0) k | 0x01; if (KEY_STOP 0) k | 0x02; if (KEY_PAUSE 0) k | 0x04; return k; } void motor_all(unsigned char on) { P1_0 on; P1_1 on; P1_2 on; } void guide_reset(void) { // 先复位X轴再复位Y轴 P2_0 1; P2_1 0; // X轴反转 P1_0 1; // 使能X电机 while (X_LIMIT ! 0); // 等待限位开关触发 P1_0 0; P2_0 0; P2_1 0; P2_2 1; P2_3 0; // Y轴反转 P1_1 1; while (Y_LIMIT ! 0); P1_1 0; P2_2 0; P2_3 0; reset_done 1; // 复位完成 } void main(void) { unsigned char key; Timer0_Init(); while (1) { key read_key(); delay_ms(10); if (key ! read_key()) { continue; // 两次读不一致认为是抖动 } switch (state) { case IDLE: if (key 0x01) { state RUNNING; reset_done 0; motor_all(1); } break; case RUNNING: if ((key 0x04) reset_done 0) { motor_all(0); guide_reset(); state PAUSED; } if (key 0x02) { motor_all(0); state IDLE; } break; case PAUSED: if (key 0x01 reset_done) { reset_done 0; motor_all(1); state RUNNING; } if (key 0x02) { motor_all(0); state IDLE; } break; } } }这段代码里有个很容易被忽略的细节RUNNING状态判断暂停时加了reset_done 0。因为第一次进入运行时reset_done是0所以暂停可以触发复位复位完成后reset_done变成1如果再按暂停就不会再重复复位。在PAUSED状态按开始时需要判断reset_done是否为1确保导轨已经回到初始位置。这里的while (X_LIMIT ! 0)是等待限位开关被触发如果仿真时没有限位开关代码会死等所以元件清单里必须把限位开关算上。4. Proteus仿真复现原理图、流程图、物料清单与调试4.1 用Proteus 7.8打开.DSN并加载HEX文件资源里的仿真文件是仿真.DSN从文件名看是用Proteus 7.8保存的新版Proteus 8.16也能打开但可能会提示低版本兼容。如果双击.DSN后元件库缺东西通常是Proteus版本自带库路径没设置了。打开工程后第一步不是点运行而是右键单片机AT89C51编辑属性在“Program File”里选择源码工程编译生成的motor.hex。注意每次改完C代码重新编译后需要重新加载HEX文件否则仿真里跑的还是旧程序。代码工程是Keil的.uvproj对应Keil4或Keil5都能直接打开。第一次打开会提示找不到芯片需要在“Device”里选AT89C51或STC89C52。编译前记得勾选“Create HEX File”位置在Options for Target → Output选项卡里不勾的话Proteus加载不到HEX。很多初学者仿真没反应问题几乎都出在这一步。4.2 原理图关键连接与常见画法原理图工程里已经画好了连线但值得对照自己的理解重画一遍。单片机最小系统部分需要晶振电路和复位电路晶振两脚分别接30pF电容到地复位引脚接10uF电解电容和10k电阻。按键部分三个按键一端接地另一端接P3口并接10k上拉电阻到VCC这样按下时单片机读到低电平。L298N部分VS接电机电源正极仿真里用12V直流电源VSS接5V逻辑电源GND和单片机的GND必须共地否则L298N输入端的电平无法被正确识别。仿真的直流电机可以用Proteus自带的MOTOR元件它只有两个引脚所以L298N输出端OUT1、OUT2直接接电机两端即可。如果想让仿真看起来更接近实物可以在电机轴上加一个旋转箭头但这不是必须的。三个电机的负载不同水泵电机可以不用方向切换但为了统一仍然把两个方向引脚都接入L298N只是软件里始终让一个方向有效。4.3 物料清单核对与流程图画法资源里的元件清单.xlsx列的是所有物料我实际核对下来核心元件和数量基本稳定。下面这张表是从常见物料清单里提炼出的最小版本详细清单还是以XLSX为准。元件型号/规格数量用途单片机AT89C51 / STC89C521主控晶振11.0592MHz1时钟瓷片电容30pF2晶振负载电解电容10uF1复位电路电阻10k4上拉/复位按键轻触开关3开始/结束/暂停电机驱动L298N2驱动三个直流电机直流电机6V~12V3X轴/Y轴/水泵限位开关常开型2X/Y轴零点检测电源5V和12V独立2逻辑和电机供电流程图画法在Visio或ProcessOn里都可以。你不用画得像教科书那么细但至少要把“程序启动 → 等待开始 → 运行中 → 是否暂停 → 暂停停止电机 → X轴复位 → Y轴复位 → 等待开始”这条主线画出来。流程图的作用是给答辩和代码维护看所以复位分支必须画成两个判断框分别对应X限位和Y限位而不是一个简单的“复位”框。这样做的好处是检查代码时能直接发现如果复位逻辑里只写了一个限位判断流程图和代码就对不上。4.4 仿真中电机不转、按键失控的排查Proteus仿真最常见的故障是电机一动不动。这时先用Virtual Terminal或电压探针看P1.0有没有PWM波形。如果没有波形说明定时器没启动检查ET0和TR0是否置1。如果有波形但电机不转多半是L298N供电没接对。直流电机在Proteus里需要单独的电源电压不能直接从单片机VCC供电。用L298N时OUT1和OUT2之间的电压由VS决定如果VS没接12V电机两端电压差为0当然不转。按键失控通常是上拉电阻缺失或按键引脚接错。Proteus里按键按下时接地松开时浮空所以必须靠上拉电阻把引脚拉到高电平。如果没有上拉电阻读到的值不稳定按下和松开都可能在高低电平之间抖动。下面这段UART调试代码可以帮你在仿真里快速定位状态有没有切对// 初始化串口波特率9600用于Proteus Virtual Terminal观察状态 void UART_Init(void) { SCON 0x50; // 模式18位UART TMOD 0x0F; TMOD | 0x20; // 定时器1模式2自动重装 TH1 0xFD; // 11.0592MHz下得到9600 TL1 0xFD; TR1 1; } void UART_SendByte(unsigned char dat) { SBUF dat; while (!TI); TI 0; }在状态切换的地方调用UART_SendByte(S)、UART_SendByte(P)这类输出再在Proteus里放置Virtual Terminal虚拟终端就能看到按键按下时状态是否按预期跳转。这样比凭感觉看电机转不转要可靠得多。串口初始化里的SCON 0x50是设置串口工作方式TH1和TL1的初值决定了波特率晶振必须是11.0592MHz换成12MHz会乱码。5. 一个值得抄的细节用下降沿标志位代替电平判断彻底解决按键抖动前面主循环里的按键消抖用的是短延时加二次读取这在Proteus仿真里没问题但真机上还是会出现一次按下触发两次动作的情况。原因很简单机械按键按下和松开的时间点有抖动可能持续几毫秒到十几毫秒延时消抖只能保证“读到稳定值”不能保证“只在按下瞬间动作一次”。比如在运行状态按暂停如果电平持续低电平主循环每次扫描都会进入暂停分支重复调用guide_reset()这就麻烦了。解决办法是把按键扫描从电平判断改成下降沿判断只关心“从没有按下变成按下”的那个瞬间一旦捕获就置一个标志位然后立刻进入处理逻辑。松开键时不做任何处理。下面是我常用的写法直接放在1ms定时器中断里扫描每20ms做一次卷积式滤波。#define KEY_NONE 0x00 #define KEY_START_FLAG 0x01 #define KEY_STOP_FLAG 0x02 #define KEY_PAUSE_FLAG 0x04 volatile unsigned char key_event 0; void Key_Scan_ISR(void) { static unsigned int key_filter 0xFFFF; static unsigned char key_latch 0xFF; unsigned char key_now; key_now 0; if (KEY_START 0) key_now | KEY_START_FLAG; if (KEY_STOP 0) key_now | KEY_STOP_FLAG; if (KEY_PAUSE 0) key_now | KEY_PAUSE_FLAG; key_filter (key_filter 1) | key_now; // 连续16次采样结果一致才认为按键电平稳定 if (key_filter 0x0000) { // 当前检测到任意按键且上一次按键记录为没有按下则产生下降沿事件 if (key_latch ! 0) { key_event key_now; } key_latch key_now; } else if (key_filter 0xFFFF) { key_latch 0xFF; } }这段代码的关键是key_filter。每1ms中断一次把当前按键电平移入16位变量只有连续16次读到0x0000才代表所有按键都稳定为按下状态也就是16ms的消抖窗口。之后再用key_latch记录上一次按键值key_latch ! 0意味着上一次没有任何按键按下这次突然出现按下值就是下降沿。key_event就是需要处理的事件主循环读取后要手动清零。这样改完以后主循环里不再需要延时消抖代码变成if (key_event KEY_PAUSE_FLAG) { key_event ~KEY_PAUSE_FLAG; motor_all(0); guide_reset(); state PAUSED; }因为key_event是一个只触发一次的边沿事件所以即使主循环执行很慢也不会重复进入暂停复位逻辑。这个细节同样适用于矩阵键盘、编码器按键和一切机械触点输入是最低成本的专业级消抖方案。资源里的源码工程是motor.c我建议你直接在它的基础上替换按键扫描函数仿真运行几次暂停和复位会比重新写一遍整个项目更有收获。本文还有配套的精品资源点击获取