ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STC89C52自动售货机设计:硬件选型、C语言状态机与Keil调试

STC89C52自动售货机设计:硬件选型、C语言状态机与Keil调试 简介基于STC89C52单片机设计的自动售货机系统资料面向单片机/嵌入式课程设计、毕业设计及入门学习者。整套方案围绕商品选购、货币识别、余额计算、找零与LCD实时显示等功能展开采用3×3矩阵键盘作为输入并用C/C实现核心控制逻辑文档从绪论、总体设计方案到关键芯片选型、硬件电路连接、软件模块划分、系统联调与结论均按课程设计报告思路完整呈现还单独附有源码可直接参考移植或二次开发。资源内容集中在一个docx文档中压缩包约427KB体量虽小但结构完整便于快速通读和打印归档。当前已有339人学习适合需要完成同类课题、理解自动售货机工作流程或希望掌握STC89C52外设控制的读者重点参考尤其可借鉴矩阵键盘扫描、货币识别处理、货道驱动和显示刷新等模块化设计方式。1. 一台STC89C52售货机值不值得做自动售货机的课设项目做了这么多年从 51 到 STM32 再到 ESP32 方案都有人在传但回到 8 种商品、3 档价格、投币累计、余额不足退回这一组需求STC89C52 依然是门槛最低、最容易把逻辑讲清楚的选择。8K Flash 装下全部 C 源码512B RAM 跑状态机够用32 个 I/O 接完矩阵键盘、LCD1602 和出货指示之后还有剩余。很多人觉得 51 太老但真正做产品时低路数售货机、扭蛋机、盲盒机里这类单片机仍然大量存在。这篇按硬件选型、C 代码组织、Keil 编译调试这条线把整台机器从原理图到源码怎么落地拆开讲附录里的主函数、显示驱动和提示程序三份源码可以直接拼起来跑。2. 硬件选型与STC89C52周边电路2.1 为什么是STC89C52资源盘点与选型逻辑STC89C52 的本质是增强型 MCS-51指令集和引脚与 Intel 8052 兼容但内部整合了 ISP 引导、看门狗、4KB EEPROM 和三路 16 位定时器。8K Flash 装这个项目的 C 代码绰绰有余512B RAM 中 data 区虽然只有 128B但配合code段放常量表跑一个售货机交易状态机完全够用。相比直接上 STM32F103STC89C52 的成本优势非常明显散买几块钱一片直插封装手工洞洞板就能搭不需要画四层 PCB这对课程设计和前期原型验证都是实打实的便利。选型上有一个容易被忽略的点I/O 数量。这个系统要占用的引脚包括 3×3 键盘 6 个、LCD1602 的数据线 8 个加 RS/RW/EN 三个控制线P2.0~P2.7 还要留给出货指示总计超过 20 个 I/O。STC89C52 提供 32 个 I/O接完之后还能剩下 P3.0/P3.1 给串口做调试和 ISP 下载不用像某些小封装 MCU 那样在引脚复用上做取舍。2.2 3×3矩阵键盘逐行扫描的电路与电平逻辑键盘模块是用户唯一的输入途径。3×3 矩阵键盘有 9 个交叉点0~7 号对应 8 种商品8 号对应退币。行线接 P1.0~P1.2列线接 P1.3~P1.5每个交叉点放一个轻触按键。STC89C52 的 P1 口内部有准双向上拉但排线稍长时建议在列线上再并 10K 外部上拉避免按下时电平抖动导致误判。扫描原理是逐行输出低电平先把第 0 行拉低其余行保持高再读列线。如果某一列读到低电平说明该行该列的交叉点被按下。三行各扫一遍就能唯一确定按键位置。这个方案用 6 个 I/O 换来 9 个按键比独立按键省了 3 个引脚。具体实现代码在第 3 章给出这里只需要理解一个关键点行线必须逐行拉低不能同时把三行都拉低否则只能判断有无按键无法定位行号。2.3 TC1602AL显示模块8位并口接线与对比度调整TC1602AL 是标准的 16×2 字符型液晶模块接口在原文表 3.1 中列得很清楚。D0~D7 接 P0 口RS 接 P2.6 选择数据/指令寄存器RW 接 P2.5 选择读/写EN 接 P2.7 作为使能信号下降沿锁存数据。Vss 接地Vdd 接 5VV0 是液晶驱动电压输入串一个 10K 电位器到地用于调节对比度。这里有一个非常常见的坑P0 口是开漏输出内部没有上拉直接接 LCD 数据线会出现高电平驱动不足表现为屏幕亮但显示乱码或完全不显示。解决方法是给 P0 外接 4.7K~10K 上拉排阻这是焊接阶段就要确认的细节等代码写完再查硬件就浪费时间了。2.4 蜂鸣器提示电路与出货指示提示模块是一个 NPN 三极管驱动的蜂鸣器基极串 330Ω~1KΩ 保护电阻接 I/O集电极接蜂鸣器负极发射极接地。R4 的阻值决定基极电流330Ω 时约 10mA1KΩ 时约 4mAS8050 或 BC547 都能饱和导通。需要注意的是这个电路驱动的是有源蜂鸣器I/O 给高电平就响如果换用无源蜂鸣器必须输出 2~4kHz 方波才有声音代码逻辑完全不同。出货状态用发光二极管代替真实货道P2.0~P2.7 每个引脚接一个 LED 串 470Ω 限流电阻到 GND对应 8 个商品位。商品被选中且余额足够时对应引脚拉低LED 点亮表示货道动作。这样做的意义在于先把控制逻辑跑通后续换真实电机驱动模块时I/O 语义不用改。2.5 MDB货币识别接口接线与使用边界货币识别模块按原文设计只处理 1 元硬币采用 MDB 接口。MDB 是全双工主从式串行接口标准 6 针插头4 号口接 P3.0 作为数据接收5 号口接 P3.1 作为数据发送单片机是主机硬币器是从机。顾客投入硬币后硬币器通过 MDB 上报币值和数量单片机累加余额退币时单片机发退币命令硬币器执行退币动作后返回结果。硬币识别的物理原理是高频振荡器的电感变化不同材质和直径的硬币通过线圈时引起振荡频率偏移与预设值比对后确定面值。这些判断在硬币器内部完成51 这边只需要处理 MDB 协议帧。原文特别提到货币识别器核心内容保密可参考资料少所以软件设计里不做协议栈展开只把它当作一个串口数据源处理。提示P3.0/P3.1 同时又是 ISP 下载用的串口引脚。调试阶段如果发现下载失败先确认硬币器没有占用这两个引脚或者下载时把 MDB 插头拔掉。3. C语言状态机与键盘、显示程序设计3.1 工程文件划分与主循环结构C 源码按模块拆分是比较合理的做法main.c 放主循环和交易状态机keypad.c 放键盘扫描lcd1602.c 放显示驱动beep.c 放提示音控制头文件统一声明接口。这样后面替换货道驱动或者改 LCD 型号时不需要在整个 main.c 里翻找。主程序结构是上电初始化 LCD、定时器和余额变量然后进入 while(1) 死循环。循环体只做三件事扫描键盘、根据按键更新交易状态、刷新显示。这个结构看似简单但它是状态机的最简形态交易逻辑集中在主循环的按键分支里不会出现中断服务函数改全局变量导致的竞态问题。原文 4.2 节的流程图展示的正是这个流程投币后显示余额选货后比较金额不足则提示足够则出货并找零。3.2 矩阵键盘扫描与消抖实现键盘扫描的核心是逐行拉低 回读列线。下面的代码把行接 P1.0~P1.2列接 P1.3~P1.5返回值 0~8 对应 9 个按键0xFF 表示无按键。// keypad.c #include REG52.H #define KEY_MASK 0x38 // 0b00111000对应 P1.3~P1.5 // 返回0~8商品按键0~7按键8为退币0xFF表示无按键 unsigned char KeyScan(void) { unsigned char row, col, key 0xFF; // 三行对应的行输出值P1.0/P1.1/P1.2 分别拉低 unsigned char code row_low[3] {0xFE, 0xFD, 0xFB}; for (row 0; row 3; row) { P1 row_low[row]; // 当前行拉低其余保持高 if ((P1 KEY_MASK) ! KEY_MASK) { // 有列被拉低说明有按键 delay_ms(10); // 软件消抖 if ((P1 KEY_MASK) KEY_MASK) continue; // 抖动重新扫描 for (col 0; col 3; col) { if ((P1 (0x08 col)) 0) { key row * 3 col; // 按行优先编号 break; } } break; // 一次只认一个键 } } return key; }逻辑说明row_low数组中 0xFE 对应 1111 1110表示 P1.0 输出低电平0xFD 对应 1111 1101P1.1 拉低0xFB 对应 1111 1011P1.2 拉低。KEY_MASK为 0x38 用于屏蔽列位(P1 KEY_MASK) ! KEY_MASK成立说明至少有一列被拉低。列判断用0x08 col依次检查 P1.3、P1.4、P1.5 是否读到 0。消抖时间取 10ms 是机械按键的常见值轻触开关的抖动周期一般在 5~20ms。delay_ms用定时器或空循环实现空循环要注意 Keil C51 在 O0 优化级别下延时时间符合预期开高优化后空循环可能被优化掉。如果后续把键盘扫描放到定时器中断里做可以省掉阻塞式消抖改为连续采样 3 次一致的边沿触发。3.3 交易状态机投币、选货、退币的流转交易逻辑用状态变量来组织。定义两个关键数据price_tab是 8 种商品的价格常量表balance是当前投币累计金额。主循环根据按键值分流处理。// main.c #include REG52.H #include lcd1602.h #include keypad.h #include beep.h // 8种商品价格3个1元、3个2元、2个3元对应按键0~7 unsigned char code price_tab[8] {1, 1, 1, 2, 2, 2, 3, 3}; unsigned char balance 0; // 投币累计金额单位元 // 出货指示P2.0~P2.7对应8个货道LED亮表示出货 void GoodsOut(unsigned char idx) { P2 ~(0x01 idx); // 对应位拉低 delay_ms(300); // 模拟出货过程 P2 0xFF; // 复位LED熄灭 } void main(void) { unsigned char key; LcdInit(); // LCD初始化为8位模式、2行 LcdWriteStr(0, VENDING MACHINE); LcdWriteStr(1, MONEY:0 RMB); balance 0; P2 0xFF; // 出货口全部置高LED全灭 while (1) { key KeyScan(); if (key 8) { // 商品按键0~7 if (balance price_tab[key]) { // 余额足够 balance - price_tab[key]; GoodsOut(key); // 对应货道动作 BeepOnce(200); // 短响交易成功 } else { BeepOnce(500); // 长响余额不足 LcdWriteStr(0, NOT ENOUGH); } } else if (key 8) { // 退币键 // 退币执行后余额清零硬币器通过MDB接口驱动 balance 0; LcdWriteStr(0, COIN RETURN); } // 刷新余额显示只更新数字部分避免全屏闪烁 LcdWriteStr(1, MONEY:); LcdWriteNum(6, balance); LcdWriteStr(11, RMB); delay_ms(2); } }逻辑说明Price_tab用code关键字放在 Flash不占宝贵的 data 区这是 C51 与标准 C 的一个关键差异。按键 0~7 直接作为商品索引余额足够时扣款并拉低对应 P2 口模拟出货余额不足时长响提示并刷新屏幕提示信息。8 号键是退币操作这里只做余额清零和界面切换真实退币动作依赖 MDB 硬币器执行。状态流转的核心是余额比较那一行balance price_tab[key]。这个判断覆盖了三种情况余额恰好等于价格、余额大于价格此时差额就是找零、余额不足。找零逻辑在原文流程图中对应投入货币数多于设定价格时实现找钱功能此处没有真实硬币器时用余额清零模拟退币即可。3.4 LCD 驱动初始化、写指令与刷新策略TC1602AL 的驱动本质上是对三个控制引脚和一个数据口进行操作。初始化序列固定为 8 位模式、2 行显示、5×7 点阵。写指令时 RS 拉低写数据时 RS 拉高EN 产生一个下降沿把数据锁存进液晶模块。// lcd1602.c #include REG52.H sbit RS P2^6; // 数据/指令选择0指令1数据 sbit RW P2^5; // 读/写选择0写1读 sbit EN P2^7; // 使能信号下降沿锁存 void LcdWriteCmd(unsigned char cmd) { RW 0; RS 0; // 指令寄存器 P0 cmd; EN 1; delay_us(2); EN 0; // 下降沿锁定指令 delay_us(100); } void LcdWriteData(unsigned char dat) { RW 0; RS 1; // 数据寄存器 P0 dat; EN 1; delay_us(2); EN 0; delay_us(100); } void LcdInit(void) { delay_ms(20); // 上电等待液晶内部复位 LcdWriteCmd(0x38); // 8位总线2行5×7点阵 LcdWriteCmd(0x08); // 显示关 LcdWriteCmd(0x01); // 清屏 LcdWriteCmd(0x06); // 写入后地址自动加1 LcdWriteCmd(0x0C); // 显示开光标关 } void LcdWriteStr(unsigned char line, unsigned char *str) { unsigned char addr (line 0) ? 0x80 : 0xC0; LcdWriteCmd(addr); // 设置DDRAM地址第一行0x80第二行0xC0 while (*str) { LcdWriteData(*str); } } void LcdWriteNum(unsigned char col, unsigned char num) { LcdWriteCmd(0x80 | 0x40 | col); // 第二行第col列 LcdWriteData(0 num); // 单位数直接转字符 }逻辑说明LCD 的 DDRAM 地址从 0x00 开始第一行对应 0x80~0x8F第二行对应 0xC0~0xCF这是 1602 的行首地址规则。LcdWriteCmd中的 EN 下降沿是数据锁存的关键时序要求 EN 高电平保持至少 450nsdelay_us(2)在这个频率下足够。LcdWriteStr用 while 循环逐个输出字符遇到字符串结束符\0停止。LcdWriteNum只刷新单个字符避免整屏重写造成闪烁这在余额变化频繁的售货场景里体验差异明显。提示如果显示第一行正常、第二行乱码先检查0xC0这条地址指令。很多移植问题不是时序而是第二行首地址写错。4. Keil C51下的编译、烧录与常见坑4.1 C51和标准C的几个关键差异Keil C51 编译器和桌面端 GCC/Clang 的 C 标准有差异5 年以上 C 经验的人第一次写 C51 最容易踩这几个点整理成下表。C51 关键字/概念含义与标准C的差异code常量存放在程序 Flash标准 C 无此概念等价于只读数据段data/xdata内部 RAM / 外部扩展 RAM决定变量访问速度data 快但只有 128Bbit/sbit位变量 / 特殊功能寄存器位标准 C 没有位寻址类型sfr特殊功能寄存器定义用于访问定时器、串口等硬件寄存器中断函数void Timer0_ISR(void) interrupt 1不能有参数和返回值不能直接调用重入函数函数默认不可重入需要加reentrant中断和主循环共享函数时必须处理其中data区只有 128B局部变量和全局变量都在里面。一个常见报错是DATA SEGMENT TOO LARGE解决办法是把大数组或常量表挪到code段或者改小内存模型。原项目里 8 个商品价格、LCD 字符串常量都用code修饰根本原因就在这里。4.2 EEPROM做价格表持久化STC89C52 内部 4KB EEPROM 可以用来存价格表、库存数量和累计营收掉电不丢失。这比外部接 24C02 省一根 I2C 线。EEPROM 操作要用 STC 特有的 IAP 寄存器组先定义再调用。// eeprom.c #include REG52.H // STC89C52 系列 IAP 寄存器地址定义 sfr IAP_DATA 0xC2; // IAP数据寄存器 sfr IAP_ADDRH 0xC3; // 地址高字节 sfr IAP_ADDRL 0xC4; // 地址低字节 sfr IAP_CMD 0xC5; // 命令寄存器 sfr IAP_TRIG 0xC6; // 触发寄存器 sfr IAP_CONTR 0xC7; // 控制寄存器 // 扇区擦除EEPROM写前必须擦除所在扇区STC89C52扇区大小512B void EepromErase(unsigned char sector) { IAP_CONTR 0x80; // 使能IAP IAP_CMD 0x03; // 扇区擦除命令 IAP_ADDRH 0x00; IAP_ADDRL sector * 2; // 每个扇区512B地址按扇区取 IAP_TRIG 0x5A; // 触发序列 IAP_TRIG 0xA5; delay_ms(20); // 等待擦除完成 IAP_CONTR 0; // 关闭IAP } // 读取一个字节 unsigned char EepromRead(unsigned int addr) { unsigned char dat; IAP_CONTR 0x80; IAP_CMD 0x01; // 字节读 IAP_ADDRH (addr 8) 0xFF; IAP_ADDRL addr 0xFF; IAP_TRIG 0x5A; IAP_TRIG 0xA5; dat IAP_DATA; IAP_CONTR 0; return dat; }逻辑说明IAP 操作统一走0x5A和0xA5触发序列这是 STC 单片机的保护机制防止误写。写字节前必须先擦除整个扇区这是一个非常容易踩的坑改一个字节要重写整个扇区。实际应用时通常把价格表放在固定地址上电先从 EEPROM 读读不到或校验失败再用默认价格表初始化这样运营人员改价只需要通过串口下发新表。4.3 串口打印调试printf重定向LED 指示和 LCD 显示能看到的运行状态有限串口打印是单片机调试效率最高的手段。STC89C52 的 UART 在模式 1 下用定时器 1 产生波特率11.0592MHz 晶振配合 TH10xFD 得到 9600bps这是最常用的组合。// uart.c #include REG52.H // 初始化串口模式18位UART波特率960011.0592MHz void UartInit(void) { SCON 0x50; // 0101 0000模式1REN1使能接收 TMOD 0x0F; TMOD | 0x20; // 定时器1方式28位自动重装 TH1 0xFD; // 9600波特率重装值 TL1 0xFD; TR1 1; // 启动定时器1 } void UartSendByte(unsigned char dat) { SBUF dat; // 写入发送缓冲硬件自动发送 while (!TI); // 等待发送完成标志 TI 0; // 手动清标志 } // 重定向putcharprintf即可直接使用 char putchar(char c) { UartSendByte((unsigned char)c); return c; }逻辑说明SCON 0x50中的 0x50 拆开看是 0101 0000SM0/SM1 为 01 即模式 1REN 置 1 使能接收。TH1 0xFD这个值是固定套路换晶振要重算波特率 晶振频率 / 12 / 32 / (256 - TH1)。putchar重定向之后printf(balance%d\r\n, balance)就能直接往串口助手输出按键状态、余额变化、状态机跳转全部可视。注意 C51 的 printf 默认支持%d和%x但%f需要额外配置嵌入式调试里尽量不用浮点格式。5. 从课设到产品看门狗、真实货道与ISP升级5.1 看门狗兜底售货机是无人值守设备程序跑飞没人按复位键。STC89C52 内置看门狗定时器通过WDT_CONTR寄存器控制一旦使能主循环必须周期性喂狗否则芯片自动复位。喂狗代码放在主循环末尾溢出时间按最大交易流程估算一般 1~2 秒足够。sfr WDT_CONTR 0xE1; #define EN_WDT 0x30 // 使能看门狗 清看门狗 WDT_CONTR 0x35; // EN1, CLR1, PS101 约2秒溢出5.2 从LED到ULN2003驱动真实货道原设计用 LED 模拟出货真实货道通常是 12V 或 24V 直流电机推杆51 的 I/O 直接驱动不了。常规方案是 ULN2003 达林顿管阵列I/O 高电平经内部达林顿管放大后驱动继电器或电机P2 口的 8 个引脚正好对应 8 路货道软件层GoodsOut函数不用改只换硬件驱动板。电机动作时间要单独测实物出货加上限位开关后GPIO 控制逻辑需要从延时改成检测到位信号。5.3 STC-ISP串口下载与量产STC89C52 支持 ISP 串口下载不需要专用编程器。下载时先点 STC-ISP 软件的下载按钮再给板上电芯片上电瞬间进入引导区完成烧录。量产时把 P3.0/P3.1 引出到测试点配合 STC-ISP 的序列号加密功能可以防止固件被直接读走。下载速度不是越快越好遇到下载失败先降到最低波特率同时确认 MDB 硬币器没有占用串口引脚。本文还有配套的精品资源点击获取
返回列表