H39基于51单片机自动灌装系统,用AT89C51控制传送带与灌装两个执行器:LCD1602显示设定瓶数与实际计数,五按键设定0~255,确认后继电器驱动送瓶。数量检测到位即停带开灌装,装满触发后关阀续送,构成四步工序循环,够数自动停机并蜂鸣提示。

01 设计任务
功能介绍:
使用51单片机设计一个自动灌装系统,LCD1602 显示设定值与实际计数,传送带 + 灌装两个执行器协同工作,要求如下:
1、参数设定:五个按键"设置/增加/减少/确认",设定需要灌装的瓶数(0~255),LCD 第一行显示设定值、第二行显示实际计数;
2、传送带运行:确认后传送带电机启动,继电器吸合带动模拟传送带送瓶;
3、到位停带 + 灌装:数量检测到瓶子到位,立即停带并打开灌装执行器(仿真用红色 LED 指示);
4、装满检测续行:灌装完成(装满检测触发)后关闭灌装、传送带重新启动,进入下一瓶;
5、够数收工:计数达到设定值自动停机,蜂鸣器响约 0.5 秒提示完成。
特别注意:前面 H26(汽车里程)、H30(传送带计数)、H31(带存储传送带计数器)都是"测"——数了多少、跑了多远;这一篇是"控"——数到几个就执行什么动作。它有两个执行器(传送带电机、灌装设备)和两个检测信号(到位、装满),本质上是一个四步循环的工序状态机。看懂 moto_control() 那二十几行,工业流水线的控制逻辑就入门了。



02 硬件:两个检测、两个执行器
先把引脚关系理清楚,这张图上的器件比前几篇都多:
检测输入(两个按钮模拟传感器)
| 信号 | 引脚 | 上拉 | 含义 |
| 装满检测 Check_full | P1.5 | R4 10K | 瓶子灌满了(按下=0) |
| 数量检测 Check_num | P1.6 | R5 10K | 有瓶子经过/到位(按下=0) |
执行输出(两个)
| 执行器 | 引脚 | 驱动电路 |
| 传送带电机 MOTO | P3.6 | R9 100Ω → Q2(PNP)→ 继电器 RL1 → +12V |
| 灌装设备(LED 模拟) | P2.4 | R10 100Ω → D2 红色 LED |
人机界面
· LCD1602:数据口 P0(配 RP1 排阻上拉),RS/RW/E 接 P2.0~P2.2;
· 五个按键:设置 P3.0、增加 P3.1、减少 P3.2、确认 P3.3;
· 蜂鸣器:P1.7 → R11 1K → Q1(NPN)→ 模拟警铃。
两个细节值得单独说:
一是继电器用 PNP 驱动。 P3.6 输出低电平时 Q2 导通、继电器吸合——所以程序里 #define ON 0 / OFF 1,MOTO=OFF 才是停。这种"低电平有效"的写法必须靠宏名把极性遮住,否则后面读代码的人会被 MOTO=1 到底是开还是停搞晕。这个习惯在 H35、H36 都强调过,工业控制里尤其重要。
二是灌装用 LED 模拟。 真实设备的灌装阀是电磁阀,仿真里一个 LED 就够了——重点是时序:停带 → 开阀 → 装满 → 关阀 → 启动传送带。LED 亮灭就是阀的开闭,看仿真时盯着那盏红灯的节奏,就知道程序有没有写对。
03 五个标志位,一个工序状态机
这份程序的全部"记忆"都在五个 bit 变量里:
五个位变量各管一件事:设定模式标志表示正在改参数;装满标志是定义了但全程没用上的空位;到位标志表示瓶子已经停在灌装位;启动标志表示确认后系统正在跑;蜂鸣器标志表示任务做完了要响一声。几个按键分别负责置位、清零这些标志,主循环再靠它们判断该走哪一步。
对照按键就明白每个的职责:
· 设置键:set_flag=1,同时 start_flag=0——进入设定模式并停掉运行,防止边改参数边跑;
· 增加/减少键:只在 set_flag 为真时生效,Set_num 在 0~255 之间循环;
· 确认键:set_flag=0、Count_num=0、start_flag=1——退出设定、计数清零、系统开跑。"确认"这个动作顺手把计数器归零了,所以每次重新确认都是一轮新的生产任务,不用另外按清零键。
真正干活的是 moto_control(),它每轮主循环都跑一遍,是一个三层判断的工序状态机:
第一层:够数了吗?
到达设定值后的处理是这样走的:先把传送带停掉,再判断设定值是否为 0——为 0 就直接收工、不再做最后一瓶,避免「设定 0 瓶却仍然打开灌装阀」的荒唐动作;设定值非 0 时打开灌装,等装满信号到来才关灌装、清掉到位标志、结束本轮任务,并置起蜂鸣器标志响一声。
这里有个防呆守卫:if(Set_num)。如果设定值是 0,按确认后直接停机、不灌装——否则会出现"设定 0 瓶,但流程还是会打开灌装阀"的荒唐动作。参数为 0 时要有单独的守门条件,这个习惯在 H29(设定 0 分钟按开始无效)也提过。
第二层:没够数,瓶子到位了吗?
到位标志为真就停带、开灌装,等装满信号到来后关灌装、清到位标志、恢复启动标志准备下一瓶;到位标志为假说明瓶子还没送到,传送带保持运行继续送。
第三层:都没触发——MOTO=ON,传送带继续送。
传送带运行、瓶子到位触发数量检测、停带、开灌装、装满检测触发、关灌装、传送带重新启动,如此循环,直到计数等于设定值才整体停机。
注意 check_arrive_flag 的清除时机:它在"装满"之后才清,而不是在"停带"之后清。为什么?因为停带之后程序还要靠这个标志维持"正在灌装"的认知——如果停带就清标志,下一轮循环会认为"没到位",直接把传送带又开起来,灌装阀还没开完就被打断。这类"标志清早了"的 bug 在流水线程序里特别常见,看到设备动作一顿一顿地抖,先查标志清除位置。
04 中断里的边沿检测:怎么"数一下"
计数发生在 T0 中断里(10ms 一次):
用一个静态变量记住上一拍的电平:上一拍已经记下「按下」、这一拍仍然按着,就把静态变量清零(相当于宣告这次按下已经处理过了),此时才计一次数并置到位标志;上一拍没记下、这一拍检测到按下,只把静态变量置一,本拍不计数。这样一次按下只累加一次,等效于给按钮做了软件消抖。
这段代码的本质是用两个状态记住"上一次是什么电平",从而只对"变化"作出反应。把它拆开看:
· temp_check == 0 且检测到按下 → 记下 temp_check = 1,这一拍不计数;
· 下一拍 temp_check == 1 且仍然按着 → 把 temp_check 清零(相当于宣告"这次按下已经处理过了"),此时才计数。
因为 10ms 才进一次中断,人手按一下至少几十毫秒,所以"按下"这件事会被 interrupt 看到多次——如果直接 if(Check_num==0) Count_num++,按住一秒就能加几十次。加了这层边沿状态机,一次按下只计一次,等效于给按钮做了硬件消抖。
这就是软件边沿检测,和 H30 的"T0 计数器初值 0xFFFF 单脉冲捕捉"正好是两条路:
| 方案 | 实现 | 优点 | 代价 |
| 硬件计数器(H30) | T0 设为计数器模式,初值差 1 就溢出 | 不占 CPU、响应快 | 要占用一个定时器/计数器,且只能接特定引脚(T0/T1) |
| 软件边沿(H39) | 定时中断里存上一次电平,比对变化 | 任意 IO 都能用、逻辑可扩展 | 采样周期决定最小脉冲宽度(这里 10ms) |
"任意 IO 都能用"是软件方案的最大价值:Check_num 接的是 P1.6,根本不是计数器的输入脚,硬件方案压根用不上。所以实际工程里,几个传感器同时计数,大多是软件扫描 + 边沿判断。
顺带说一个隐患:Count_num 是 int,在中断里 Count_num++、主循环里读它比较。51 是 8 位机,int 的读写分两条指令,理论上主循环可能在"高低字节刚改了一半"时读到错值。这份程序里因为计数变化慢、主循环又一直在读,出问题的概率极低;但严谨的做法是在主循环读之前先关中断、读完再开(EA=0; x=Count_num; EA=1;),或者干脆用 uchar 并保证不超过 255。这种"跨中断共享变量"的原子性问题,答辩时被问到的概率不低。
05 LCD1602:设定界面怎么摆
显示部分没什么花活,但有两个细节:
一是定长三位补零显示。Display_data(hang, lie, num) 把数字拆成百/十/个三位,每位的 ASCII 码是"数字 + 0x30",然后连续写三个字符:
三次写入按百位、十位、个位的顺序来,行号是同一个参数、列号依次加一;定位怎么算的,放到本节后面的定位函数里一起看更清楚。
固定三位、前导补零,所以屏幕上是 Setnum: 003、Measure: 012 这种格式。好处是刷新时永远是三个字符,不用考虑"位数变了要不要清残留"——如果做"消前导零"的格式,从 100 掉到 99 时会剩个"0"在最后一位(H24 讲数显消隐时踩过)。界面定宽定长,是省心的第一原则。
二是忙检测被注释掉了。 看 Busy() 函数:
函数里先把寄存器选择置为命令、读写选择置为读,拉高使能,等两段短延时过去,读回状态位那行已经被注掉,使能拉低后固定返回「不忙」。
这是 Proteus 仿真必须做的妥协:仿真里的 LCD1602 模型在某些时序下不会正确响应忙标志,while(Busy()) 会死等,程序直接卡死。所以仿真版一律屏蔽忙检测、改成固定延时(delay1602() 两次)。
但上实机必须把那一行还原:真 LCD1602 在清屏、写入等操作期间确实会忙,不判忙硬写会丢字符、甚至显示乱码。H33 也讲过这个坑,这里再确认一遍:"仿真屏蔽、实机打开"这两份要留个开关(比如 #define SIM_LCD 1 包一层),别只留注释。另外这段 wdata() 里还有一句多余的 P0 = 0XFF; 紧跟着 P0 = del;——一次写数据送两遍,属于改程序时的残留,不影响功能。
06 按键与 1602 的行列定位
按键扫描用的是 H21 那个通用 KEY_Scan(mode):key_up 标志 + 10ms 延时消抖,mode=1 支持连按、mode=0 不连按。这里是 KEY_Scan(0)——设置参数时按一下加一,按住不放不会疯跑,符合"设定值要精确"的需求。如果想加速调参,把增加/减少改成 KEY_Scan(1) 就能长按连加(H24 的 INC/DEC 长按连调就是另一套更完整的写法)。
1602 的光标定位函数收拾得很干净,值得抄走:
函数主体是两个判断加一次加法:先按行号定基址,再加上列号偏移,把算出的地址当命令发出去,最后发要显示的那个字符。
第一行基址 0x80、第二行 0xc0,列号减一就是偏移——这套地址是 1602 的硬规矩(DDRAM 第一行 0x00~0x27,第二行 0x40 起,命令里再加 0x80 的"写地址"位)。L1602_string 在此基础上按字符串逐字写,b==16 兜住 16 字符宽度,防止写到行尾越界绕回行首。
07 排错清单
1、按确认后电机不转:先看 Set_num 是否为 0(设定 0 是停机状态,不是故障);再查 P3.6 → Q2 基极电阻是否接、继电器是否接 +12V——PNP 驱动时 IO 输出低才导通,用万用表量一下 P3.6 有没有变低;
2、LED 一闪就灭,看不到灌装过程:装满检测(P1.5)如果在仿真里初始就是低电平,程序判断"已经装满"会立刻关阀。平时该是高电平(未满),按下才变低;按钮没接上拉或接反就会出这个现象;
3、传送带一直跑,从不停:查数量检测 P1.6 有没有信号进来。中断里 temp_check 的状态机要求"先看到按下、下一拍再确认",如果信号一直为低(按钮常按/接反),反而刚好触发一次后就卡住了;用 LED 跟着 Check_num 的电平闪一下最快定位;
4、计数按住一次加好几个:边沿检测没生效——检查 temp_check 是否被重复初始化(static 丢了会被每次进中断重置),或者中断周期被改得太长(10ms 合适,100ms 时人手快按可能漏检);
5、灌装完不动了/卡死在灌装:check_arrive_flag 没清或清早了。按 03 节的时序核对:它是"装满后清",不是"停带时清";
6、LCD 花屏或无显示:仿真是 RP1 排阻上拉 P0 没接;实机是忙检测还没打开(//busy_flag = ... 那行还注释着);另外 RV1 对比度电位器没调也会白屏;
7、源码三处小问题,改不改都能跑:① full_flag 定义了全程没用,删掉;② wdata() 里 P0=0XFF; P0=del; 连写两次,留后一句;③ moto_control() 的 else 分支是空的,意味着"未启动"时不会主动把 MOTO 置 OFF,靠上一次状态维持——想更稳就在 else 里补一句 MOTO=OFF; LED=OFF;。
08 总结
把 H26、H30、H31、H39 这四篇连起来看,是一条很清晰的升级路线:
| 篇目 | 干的事 | 关键能力 |
| H26 里程计数 | 数脉冲、算里程 | 定时器"一开双用" |
| H30 传送带计数 | 数件数、到值停机 | 单脉冲捕捉 + 定值控制 |
| H31 带存储计数 | 设定值掉电不丢 | 手写 I2C + EEPROM |
| H39 自动灌装 | 数件数 + 控制执行器动作 | 工序状态机 + 多信号协同 |
前三篇是"看",这一篇是"做"。多出来的东西只有一个——把计数结果翻译成执行器动作,而这件事靠的是标志位和判断顺序,不是新器件、不是新协议。工业控制的门槛往往就在这里:逻辑不难,顺序很重要。
后面还会继续做类似"流程控制"的设计,把检测、判断、执行这三段的套路走熟。
欢迎关注,持续更新!
09 文件日期修改
如果你想把文件修改为最新的日期,又不想新建工程,那么你可以参考下面的文章。
"如何不打开程序的情况下更改proteus和keil程序的日期?"
工具和仿真文件在一起,见下面的下载链接。
10 资料下载
全套资料包含 Keil 工程(C 语言源码 + hex 文件)、Proteus 仿真工程、设计报告、程序流程图、讲解视频,以及编译截图和仿真截图。下载后可直接打开运行,也可用于课程设计、毕业设计参考。
也可以关注公众号「单片机仿真」,回复 H39 获取下载入口。公众号内还有 A / T / H 全系列 400 多个单片机仿真实例,回复「目录」可查看完整清单。

