很多做单片机课程设计或毕业设计的同学,拿到“基于51单片机的GSM自动售货机”这个题目时,第一反应是不知道从哪里下手。网上能搜到的资料要么只有原理图、没有完整代码,要么只有一段无法跑通的主程序、没有讲解。更麻烦的是,这个题目不仅涉及单片机最小系统,还牵扯到GSM模块的AT指令、LCD1602显示、电机控制、状态机设计,任何一个环节卡住,整个项目都推进不下去。
这篇文章就是想把这个题目一次性讲透。我会从系统架构、硬件选型、电路设计、核心代码、运行验证、常见故障排查到课程设计答辩要点,完整拆解一个基于51单片机STC89C52 + GSM模块的自动售货机项目。你不需要额外去搜几十篇分散的博客,读这一篇,配合本文代码,理论上就可以把实物做出来并跑通流程。
需要先做一个判断:这类题目的核心价值并不在于“自动售货机”本身,而在于它天然覆盖了嵌入式开发最常见的技术点,包括单片机GPIO控制、外部中断、定时器、串口通信、LCD显示、AT指令解析、状态机编程和电机驱动。你把这套项目做完,51单片机最重要的知识点基本就全部串起来了。
1. 这篇文章真正要解决的问题
自动售货机这个题目,在单片机课程设计和电子设计竞赛里出现频率一直很高。原因很简单:它足够综合,但难度又可控。它不要求你掌握复杂的RTOS,不需要Linux驱动开发,只要把裸机编程、模块通信和业务逻辑处理好,就能实现一个功能完整的系统。
但绝大多数同学在做这个题目时,遇到的典型痛点有三个:
第一,GSM模块不会调。很多人卡在“短信发不出去”“收不到AT指令回显”这个问题上,最后发现是波特率不匹配、SIM卡没插好、电源电流不够,或者模块根本没有开机。这类问题不从原理上理解,靠瞎试很难解决。
第二,系统整体逻辑混乱。自动售货机其实是一个典型的状态机系统:待机、投币、选择商品、出货、找零、短信通知,每个状态之间都有转移条件。如果不用状态机的思路写代码,而是全写在while(1)里堆if,一旦功能多起来,程序就彻底失控。
第三,功能验证没有方法。很多人在Proteus里仿真一切正常,做成实物之后LCD不亮、电机不转、短信发不出去,完全不知道问题出在哪。这说明缺少“分模块验证”的工程习惯。
这篇文章的目标是让读者读完能做到三件事:
- 理解自动售货机的完整工作流程,画出系统状态转移图,并能讲解清楚。
- 能独立完成基于STC89C52和GSM模块的售货机代码编写,包括按键扫描、投币检测、出货控制、LCD显示和短信通知。
- 掌握硬件调试和常见故障排查方法,遇到问题知道先查电源、再查信号、最后查逻辑,而不是盲目换模块。
2. 系统架构与核心原理
先讲清楚整体架构,再做硬件和软件设计。
2.1 系统整体框图
基于51单片机的GSM自动售货机,典型架构如下:
- 主控芯片:STC89C52RC,这是最常见的51内核单片机,带8KB Flash、512B RAM、3个定时器、1个全双工串口,课程设计完全够用。
- 人机交互:4x4矩阵键盘或独立按键,用于选择商品;LCD1602液晶用于显示余额、商品价格、出货状态。
- 支付识别:投币检测模块,通过红外对管或微动开关检测货币投入,根据脉冲次数计算金额。
- 出货机构:直流减速电机或舵机,控制货道挡板转动,把商品推出来。
- 远程通知:GSM模块(SIM800C/SIM900A),在出货成功后发送短信通知管理员或用户。
- 辅助模块:蜂鸣器用于声音提示,电源模块用于给主板和GSM模块独立供电。
整个业务流程是:系统上电自检,LCD显示欢迎界面,进入待机状态;用户投入硬币,主控检测到投币信号后计算金额并刷新LCD;用户按下对应商品按键,主控判断余额是否足够;余额满足条件,则点亮电机驱动信号,商品掉落,LCD显示出货成功;同时通过串口向GSM模块发送AT指令,向预设手机号发送一条包含商品和金额的短信;最后系统回到待机状态。
2.2 为什么用51单片机,不用STM32
这是一个很现实的工程选型问题。从性能上讲,STM32当然更强,但课程设计场景下,51单片机有不可替代的优势。
51单片机上手门槛低,教材多、资料多、实验板便宜,STC89C52开发板加下载器几十块钱就能买到;寄存器少,直接操作P0口、P1口就能控制IO;定时器和串口的逻辑非常经典,学一遍就能建立坚实的嵌入式基础。
GSM模块用的是串口AT指令通信,51单片机恰好有完整的UART模块。处理短信发送这种低速任务,51单片机完全够用,不存在性能瓶颈。
更重要的是,51单片机在Proteus里有成熟的仿真模型,课程设计阶段可以先仿真验证逻辑,再去做实物,降低失败成本。STM32虽然在工业界是主流,但在这个特定任务里属于“杀鸡用牛刀”。
2.3 GSM模块的核心机制
GSM模块本质上是把单片机的串口数据转换成手机通信网络能识别的射频信号。单片机通过TTL串口把AT指令发给GSM模块,例如发送AT+CMGS="手机号"表示准备发短信,然后发送短信内容,再发送十六进制的1A作为结束符。
AT指令是嵌入式开发绕不开的一个知识点。对GSM模块来说,最常用的指令包括:
AT:测试模块是否在线,模块应回复OK。AT+CMGF=1:设置短信格式为文本模式,回复OK。AT+CMGS="13800138000":设置短信接收号码,模块回复>。- 短信内容 + 十六进制
1A:发送短信,模块回复+CMGS:xxx和OK。
实际开发中最容易出错的地方是:单片机发送完AT指令后,必须等待GSM模块返回结果,再执行下一步。不然串口缓冲区里全是未读数据,指令顺序一乱,短信就发不出去。
2.4 状态机设计
自动售货机如果用状态机来组织代码,逻辑会非常清晰。建议划分以下状态:
| 状态 | 说明 | 进入条件 | 退出条件 |
|---|---|---|---|
| S0 待机 | LCD显示欢迎词 | 系统上电或出货完成 | 检测到投币 |
| S1 投币 | 实时刷新余额 | 检测到投币信号 | 按下商品按键 |
| S2 选择 | 判断余额是否足够 | 用户在S1按下按键 | 出货成功或取消 |
| S3 出货 | 电机运行、蜂鸣器提示 | 余额足够 | 延时结束,电机停止 |
| S4 通知 | 通过GSM发送短信 | 出货完成 | 短信发送成功 |
| S5 返回 | 回到待机状态 | 短信发送完成 | 执行完毕 |
状态机写法的好处是,每个状态对应一个函数或一段独立代码,状态之间通过变量current_state切换,不容易出现逻辑混乱,也方便后期加功能,比如退币、多货道选择、补货告警。
3. 硬件选型与引脚设计
硬件选型是实物制作的前提。下面列出的模块都是市面上最常见、最便宜的方案。
3.1 主要硬件清单
| 模块 | 推荐型号 | 说明 |
|---|---|---|
| 主控 | STC89C52RC | 51内核,8KB Flash,5V供电 |
| 显示 | LCD1602 | 16x2字符液晶,I2C或并行接口均可 |
| GSM模块 | SIM800C / SIM900A | TTL串口,支持AT指令,需外接天线和SIM卡 |
| 键盘 | 4x4矩阵键盘 | 或者用独立按键,这个项目建议4个独立按键够用 |
| 投币检测 | 红外对管模块或硬币识别器 | 输出脉冲信号,下降沿触发外部中断 |
| 电机 | 5V直流减速电机 | 配合ULN2003或L298N驱动 |
| 蜂鸣器 | 有源蜂鸣器 | GPIO高电平触发 |
| 电源 | 5V/2A适配器 + AMS1117-3.3 | GSM模块需要2A峰值电流,必须独立供电 |
3.2 引脚分配建议
| 功能 | 引脚 | 说明 |
|---|---|---|
| LCD1602 RS | P2.0 | 寄存器选择 |
| LCD1602 RW | P2.1 | 读写选择 |
| LCD1602 EN | P2.2 | 使能信号 |
| LCD1602 D4-D7 | P2.3-P2.6 | 4位数据线 |
| 按键K1-K4 | P1.0-P1.3 | 商品选择,上拉输入 |
| 投币检测 | P3.2(INT0) | 外部中断0,下降沿触发 |
| 蜂鸣器 | P1.4 | 高电平响 |
| 电机控制 | P1.5 | 高电平启动电机 |
| GSM TXD | P3.0(RXD) | 单片机接收GSM数据 |
| GSM RXD | P3.1(TXD) | 单片机发送数据给GSM |
| GSM PWRKEY | P1.6 | 控制GSM模块开机 |
注意:SIM800C模块的逻辑电平是2.8V,如果直接接51单片机5V串口,可能损坏模块或用不了。稳妥的做法是用一个三极管电平转换电路,或者买模块时要求商家带电平转换。若模块已做3.3V/5V兼容处理,则可以直接连接。
3.3 电源设计说明
GSM模块在信号弱或注册网络时,峰值电流可以到1.5A以上,瞬间跌落会直接导致模块重启。因此GSM模块的电源必须单独一路,不能用单片机的稳压器带。建议用5V/2A的适配器供电,GSM模块单独从适配器取电,单片机系统再从适配器经过AMS1117-3.3或直接5V供电。绝对不要共用一个LDO,否则短信发到一半,GSM模块重启,指令全乱。
4. 开发环境与工程结构
4.1 软件环境
- 编译环境:Keil C51,这是51单片机开发的标准IDE,用来编写和编译C代码。
- 烧录工具:STC-ISP,用于把生成的Hex文件下载到STC89C52中。
- 仿真工具:Proteus 8,可用来搭建单片机电路仿真。
- 串口调试助手:SSCOM或XCOM,调试GSM时可以直接通过USB-TTL查看AT指令回显。
如果你的电脑是Windows 10/11,需要注意Keil C51安装完后,需要破解才能正常编译超过2KB的代码。STC-ISP烧录时需要选择正确的单片机型号和串口号,并手动给单片机断电再上电进入下载模式。
4.2 工程文件结构
建议把工程文件按功能拆分,不要全部堆在一个main.c里。
GSM_Vending/ ├── main.c // 主函数和状态机 ├── delay.c / delay.h // 延时函数 ├── lcd1602.c / lcd1602.h // LCD1602驱动 ├── key.c / key.h // 按键扫描 ├── uart.c / uart.h // 串口驱动 ├── gsm.c / gsm.h // GSM短信发送 └── motor.c / motor.h // 电机控制拆分文件的好处是编译速度快、代码结构清晰,答辩时也更容易讲清楚各部分职责。
5. 核心代码实现
下面给出核心代码。先说明,这个项目代码量约500行左右,我在这里按模块拆分讲解,各位可以逐个文件建立并编译。
5.1 主程序与状态机框架
// main.c #include <reg52.h> #include "lcd1602.h" #include "key.h" #include "gsm.h" #include "motor.h" #define uchar unsigned char #define uint unsigned int // 商品价格定义,单位:角 #define PRICE_COLA 15 // 1.5元 #define PRICE_WATER 10 // 1.0元 #define PRICE_SNACK 20 // 2.0元 #define PRICE_JUICE 25 // 2.5元 sbit BEEP = P1^4; // 蜂鸣器 uchar current_state = 0; // 当前状态 uint balance = 0; // 当前余额,单位角 uchar gsm_ready = 0; // GSM是否初始化成功 // 商品价格表,索引0-3对应按键1-4 uchar code price_table[4] = { PRICE_COLA, PRICE_WATER, PRICE_SNACK, PRICE_JUICE }; // 商品名称表 uchar code name_table[4][8] = { {"Cola"}, {"Water"}, {"Snack"}, {"Juice"} }; void state_standby(void); void state_coin(void); void state_select(uchar key_idx); void state_output(uchar key_idx); void state_sms(uchar key_idx); void main(void) { UART_Init(); // 串口初始化,波特率9600 LCD_Init(); // LCD初始化 Motor_Init(); // 电机初始化 Key_Init(); // 按键初始化 INT0_Init(); // 外部中断0初始化,用于投币检测 LCD_WriteString(0, 0, "Vending Machine"); LCD_WriteString(1, 0, "Please Insert Coin"); // 初始化GSM模块 gsm_ready = GSM_Init(); if (gsm_ready) { LCD_WriteString(1, 0, "GSM OK "); } else { LCD_WriteString(1, 0, "GSM ERROR "); } delay_ms(1000); current_state = 0; while (1) { uchar key = Key_Scan(); switch (current_state) { case 0: state_standby(); if (key >= 1 && key <= 4) { // 如果没有投币就按键,提示先投币 LCD_WriteString(1, 0, "Insert Coin First "); } break; case 1: state_coin(); if (key >= 1 && key <= 4) { state_select(key); } break; default: current_state = 0; break; } } } void state_standby(void) { // 检测到投币后进入投币状态 if (coin_flag) { coin_flag = 0; balance += coin_value; current_state = 1; LCD_WriteString(0, 0, "Balance: "); LCD_WriteNum(0, 9, balance / 10); LCD_WriteChar(0, 11, '.'); LCD_WriteNum(0, 12, balance % 10); LCD_WriteChar(0, 13, 'Y'); LCD_WriteString(1, 0, "Select 1-4 "); } } void state_coin(void) { // 持续投币,实时刷新余额 if (coin_flag) { coin_flag = 0; balance += coin_value; LCD_WriteString(0, 0, "Balance: "); LCD_WriteNum(0, 9, balance / 10); LCD_WriteChar(0, 11, '.'); LCD_WriteNum(0, 12, balance % 10); LCD_WriteChar(0, 13, 'Y'); } } void state_select(uchar key_idx) { // key_idx:1-4 uchar idx = key_idx - 1; uchar price = price_table[idx]; if (balance >= price) { balance -= price; LCD_WriteString(0, 0, "Push Out "); LCD_WriteString(1, 0, name_table[idx]); // 进入出货状态 state_output(idx); // 余额显示 LCD_WriteString(0, 0, "Balance: "); LCD_WriteNum(0, 9, balance / 10); LCD_WriteChar(0, 11, '.'); LCD_WriteNum(0, 12, balance % 10); LCD_WriteChar(0, 13, 'Y'); // 发送短信 state_sms(idx); delay_ms(2000); current_state = 0; LCD_WriteString(0, 0, "Vending Machine"); LCD_WriteString(1, 0, "Please Insert Coin"); } else { // 余额不足 LCD_WriteString(0, 0, "No Enough Money! "); BEEP = 1; delay_ms(200); BEEP = 0; delay_ms(1000); LCD_WriteString(0, 0, "Balance: "); LCD_WriteNum(0, 9, balance / 10); LCD_WriteChar(0, 11, '.'); LCD_WriteNum(0, 12, balance % 10); LCD_WriteChar(0, 13, 'Y'); } } void state_output(uchar idx) { Motor_Start(); // 启动电机 BEEP = 1; delay_ms(300); BEEP = 0; delay_ms(1500); // 电机运行1.5秒,模拟出货过程 Motor_Stop(); } void state_sms(uchar idx) { // 组装短信内容,通过GSM发送 uchar msg[32]; sprintf(msg, "Goods:%s Amount:%d", name_table[idx], price_table[idx]); GSM_SendSMS("13800138000", msg); // 替换为你想要通知的手机号 }这个代码框架的关键点在于状态机。current_state控制主循环行为,state_coin和state_select是两个核心函数。当用户投币时,外部中断把coin_flag置1,主循环在合适时机处理这个标志,更新余额,切换到投币状态。选择商品后,判断余额、减扣金额、驱动电机、发送短信,再回到待机状态。
实际你可以根据自己的需要修改商品数量、价格、短信目标号码。如果要做多个货道,可以把电机控制改成多路继电器或ULN2003驱动,每个按键对应一个IO口控制一路电机。
5.2 外部中断处理投币
投币检测常用的方式是红外对管或微动开关。一枚硬币通过时会产生一个下降沿脉冲,单片机外部中断0捕获该脉冲。
// 外部中断0初始化 void INT0_Init(void) { IT0 = 1; // 下降沿触发 EX0 = 1; // 开外部中断0 EA = 1; // 开总中断 } // 外部中断0服务函数 void INT0_ISR(void) interrupt 0 { static uchar cnt = 0; cnt++; coin_flag = 1; coin_value = coin_value_constant; // 默认每枚硬币1元,即10角 }这里有一个小技巧:如果采用1元硬币检测,每检测到一次投币就加10角,balance += 10。如果使用的是1元、5角混投,可以在硬件上加不同的传感器端口来区分。课程设计一般用1元硬币或按键模拟投币即可。
投币检测模块调试时很容易出现抖动,导致一次投币被计数多次。解决方式有两种:
- 硬件上在红外对管输出端并联一个104电容,滤除高频抖动。
- 软件上在中断里加入消抖处理,比如记录上次中断时间,如果间隔小于50ms则不处理。
5.3 LCD1602驱动
LCD1602是51课程设计的常客。初学者最容易犯的错误是忘了延时、没有检测忙标志导致初始化失败,或者数据和使能引脚接错。
下面给出一份可用的4线驱动代码。这里采用4位模式,减少IO占用。注意,各种开发板的1602引脚定义不完全一样,接线时需要对照自己的液晶屏和开发板原理图。
// lcd1602.c #include <reg52.h> #include <intrins.h> #define uchar unsigned char sbit LCD_RS = P2^0; sbit LCD_RW = P2^1; sbit LCD_EN = P2^2; #define LCD_DATA P0 // 延时大约1ms void delay_ms(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) for (j = 0; j < 120; j++); } void lcd_write_cmd(uchar cmd) { LCD_RS = 0; LCD_RW = 0; LCD_DATA = cmd; LCD_EN = 1; _nop_(); LCD_EN = 0; delay_ms(5); } void lcd_write_data(uchar dat) { LCD_RS = 1; LCD_RW = 0; LCD_DATA = dat; LCD_EN = 1; _nop_(); LCD_EN = 0; delay_ms(5); } void LCD_Init(void) { delay_ms(50); lcd_write_cmd(0x38); // 8位模式,2行,5x7点阵 lcd_write_cmd(0x0C); // 开显示,不显示光标 lcd_write_cmd(0x06); // 光标右移 lcd_write_cmd(0x01); // 清屏 delay_ms(10); } void LCD_WriteChar(uchar row, uchar col, uchar ch) { uchar addr; if (row == 0) addr = 0x80 + col; else addr = 0xC0 + col; lcd_write_cmd(addr); lcd_write_data(ch); } void LCD_WriteString(uchar row, uchar col, uchar *str) { uchar addr; if (row == 0) addr = 0x80 + col; else addr = 0xC0 + col; lcd_write_cmd(addr); while (*str) { lcd_write_data(*str); str++; } } void LCD_WriteNum(uchar row, uchar col, unsigned int num) { uchar buf[6]; uchar i = 0; if (num == 0) { LCD_WriteChar(row, col, '0'); return; } while (num > 0) { buf[i++] = '0' + (num % 10); num = num / 10; } while (i > 0) { LCD_WriteChar(row, col, buf[--i]); col++; } }上面这份代码默认采用8位数据模式,LCD_DATA接P0口。如果使用4位模式,代码需要相应调整。建议新手先用8位模式跑通,再考虑优化IO占用。
LCD1602显示乱码或者无显示时,优先检查三个地方:
- 对比度调节电位器是否调好,V0引脚电压应该在0.5V左右。
- 初始化时序是否完整,特别注意延时和清屏。
- 数据线、RS、RW、EN是否接反。
5.4 GSM模块串口通信与短信发送
GSM模块通信是本文的重点。51单片机的串口UART工作在模式1,波特率9600,用定时器1作为波特率发生器。下面是初始化代码和发送函数。
// uart.c #include <reg52.h> #define uchar unsigned char #define uint unsigned int bit uart_busy = 0; uchar rx_buffer[64]; uchar rx_index = 0; bit rx_complete = 0; void UART_Init(void) { SCON = 0x50; // 模式1,8位UART,允许接收 TMOD &= 0x0F; TMOD |= 0x20; // 定时器1,模式2,8位自动重装 TH1 = 0xFD; // 波特率9600,晶振11.0592MHz TL1 = 0xFD; TR1 = 1; ES = 1; EA = 1; } void UART_SendByte(uchar dat) { uart_busy = 1; SBUF = dat; while (uart_busy); } void UART_SendString(uchar *str) { while (*str) { UART_SendByte(*str); str++; } } void UART_ISR(void) interrupt 4 { if (RI) { RI = 0; if (rx_index < 63) { rx_buffer[rx_index++] = SBUF; } } if (TI) { TI = 0; uart_busy = 0; } }GSM模块初始化流程一般如下:
// gsm.c #include <reg52.h> #include "uart.h" #define uchar unsigned char #define uint unsigned int // 等待GSM模块返回"OK",timeout单位:秒 bit GSM_WaitOK(uint timeout) { uint i; uint cnt = 0; rx_index = 0; rx_complete = 0; while (cnt < timeout * 10) { // 在串口接收中查找OK if (rx_index >= 2) { if (rx_buffer[0] == 'O' && rx_buffer[1] == 'K') { rx_index = 0; return 1; } } delay_ms(100); cnt++; } return 0; } bit GSM_Init(void) { // 模块启动等待 delay_ms(2000); // 发送AT测试 UART_SendString("AT\r\n"); if (!GSM_WaitOK(2)) return 0; // 设置文本模式 UART_SendString("AT+CMGF=1\r\n"); if (!GSM_WaitOK(2)) return 0; return 1; } bit GSM_SendSMS(uchar *phone, uchar *msg) { uchar cmd[32]; // 发送AT+CMGS=手机号 cmd[0] = 0; sprintf(cmd, "AT+CMGS=\"%s\"\r\n", phone); UART_SendString(cmd); // 等待模块返回 '>' 字符,简化为等待100ms后直接发送内容 delay_ms(200); // 发送短信内容 UART_SendString(msg); // 发送结束符 0x1A UART_SendByte(0x1A); // 等待发送完成,实际应等待+CMGS和OK delay_ms(3000); return 1; }上面的GSM代码是简化版本,主要演示AT指令的发送流程。更可靠的做法是在GSM_WaitOK里增加对ERROR的检测、对>提示符的等待,以及对短信发送状态+CMGS的解析。
实际调试中,千万不要直接用51单片机连接GSM模块去测试AT指令。推荐的调试顺序是:
- 先把GSM模块用USB-TTL转串口模块连接到电脑,用串口调试助手手动发送AT指令,确认模块能正常工作。
- 确认AT指令OK后,再把GSM模块接到单片机串口上,用单片机程序控制。
这样可以避免“单片机代码和GSM模块坏没坏”同时存在的问题。
5.5 蜂鸣器与电机控制
电机控制相对简单,通过GPIO输出高电平控制三极管或ULN2003导通。注意不要让电机直接接单片机引脚,因为电机工作电流远大于51单片机IO口承受能力。正确的做法是用NPN三极管或者ULN2003达林顿管阵列驱动。
// motor.c #include <reg52.h> sbit MOTOR = P1^5; sbit BEEP = P1^4; void Motor_Init(void) { MOTOR = 0; BEEP = 0; } void Motor_Start(void) { MOTOR = 1; } void Motor_Stop(void) { MOTOR = 0; } void Beep_Once(void) { BEEP = 1; delay_ms(200); BEEP = 0; }5.6 按键扫描
如果是4个独立按键,直接读IO口即可。为了消抖,最简单的办法是检测到按键后延时20ms再确认一次。
// key.c #include <reg52.h> sbit K1 = P1^0; sbit K2 = P1^1; sbit K3 = P1^2; sbit K4 = P1^3; uchar Key_Scan(void) { if (K1 == 0) { delay_ms(20); if (K1 == 0) return 1; } if (K2 == 0) { delay_ms(20); if (K2 == 0) return 2; } if (K3 == 0) { delay_ms(20); if (K3 == 0) return 3; } if (K4 == 0) { delay_ms(20); if (K4 == 0) return 4; } return 0; }如果你用的是4x4矩阵键盘,则需要用行列扫描方式读取按键,代码会更复杂一些。课程设计通常4个独立按键已经足够。
6. 运行结果与效果验证
代码写完后,验证过程可以分为仿真验证、硬件验证和GSM功能验证三个层次。
6.1 Proteus仿真验证
在Proteus中搭好电路,把Hex文件烧录到仿真芯片中,验证纯逻辑流程:
- 按键是否能够切换状态。
- LCD是否显示正确文字。
- 余额计算是否正确。
- 投币模拟后能否进入下一步。
- 电机在余额足够时是否动作。
Proteus中可以用按钮开关模拟投币脉冲,用虚拟终端替代GSM模块,观察单片机串口是否发出发送短信的AT指令。
6.2 实物验证
硬件焊接完成后,先不急着接GSM模块,分三步验收。
第一步,单独测试最小系统。上电后LCD应该能显示欢迎界面,按键按下时蜂鸣器有反应。
第二步,测试投币逻辑。用手按下微动开关模拟投币,观察LCD金额是否累加,选择商品后电机是否转、蜂鸣器是否响。
第三步,连接GSM模块。完成初始化后,执行一次完整的购买流程,观察手机是否能在几秒内收到短信。
如果实物出现“LCD不亮”“按键没反应”的情况,不要怀疑是程序问题,先用万用表量电压。很多制作失败的原因是杜邦线接触不良、电源没共地、芯片忘了复位电路。
6.3 预期效果与判断标准
一个完整可演示的系统,应该满足以下效果:
- 上电后LCD显示“Vending Machine”和“Please Insert Coin”。
- 投入硬币后,LCD刷新余额,例如投入1元显示“Balance: 1.0Y”。
- 按下商品按键1,若余额足够,电机转动约1.5秒,LCD显示商品名称和剩余余额。
- 余额不足时,蜂鸣器响,LCD提示“No Enough Money”。
- 出货完成后,预设手机号收到一条短信,内容类似“Goods:Cola Amount:15”。
- 短信发送完成后,系统自动回到待机界面,可继续下一次购买。
7. 常见问题与排查思路
这个项目在实际调试中问题最多的地方依次是:GSM模块不工作、串口通信乱码、LCD显示异常、电机不转、投币计数不准确。下面按表格整理。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| GSM模块无法启动 | 电源电流不足或PWRKEY未拉低 | 测量模块电源电压,检查启动时序 | 使用独立5V/2A电源,PWRKEY接IO口拉低1秒以上 |
| 发送AT指令无回显 | SIM卡未插好、天线未接、串口TXD/RXD接反 | 用USB-TTL连接电脑,串口调试助手发送AT | 检查SIM卡座、天线,对调TXD和RXD |
| 收到短信内容乱码 | 短信格式模式设置错误或字符编码不对 | 发送AT+CMGF=1设置文本模式 | 确认返回OK,文本模式使用ASCII字符 |
| LCD无显示 | 对比度电位器未调好、初始化失败 | 调节V0电压,检查数据线 | 用万用表量V0电压,检查电源和引脚接线 |
| LCD显示乱码 | 数据引脚接错、时序不对 | 对照原理图逐线检查 | 确认RS、RW、EN、D0-D7连接顺序 |
| 投币一次计多次金额 | 机械抖动或红外信号抖动 | 用示波器观察脉冲波形 | 加RC滤波电容,软件延时消抖 |
| 电机不转 | 驱动管损坏、IO口电流不够、电机供电不足 | 单独给IO高电平看电机状态 | 改用ULN2003,电机使用独立5V电源 |
| 单片机程序能跑但出货无反应 | 电机驱动引脚接错或未初始化 | 用万用表量引脚电平 | 确认Motor_Init()已调用,检查P1.5连接 |
| 短信发不出去且GSM模块重启 | 模块峰值电流拉垮电源 | 观察模块LED是否闪烁或熄灭 | 换成大电流电源,电源线加1000uF电容 |
| 串口收到乱码 | 波特率不匹配、晶振频率不对 | 检查晶振是否为11.0592MHz | 使用11.0592MHz晶振,波特率设为9600 |
8. 最佳实践与工程建议
8.1 从最小系统开始,分模块测试
做单片机项目最大的忌讳是写完所有代码再上电测试,一旦出错根本不知道问题在哪里。正确的做法是“自底向上”验证:先让LED闪烁,确认单片机最小系统和烧录正常;再测LCD显示,确认GPIO操作正确;然后测按键和外部中断;接着测串口通信;最后才把GSM模块集成进来。
每一步都验证无误之后再做整合,整体调试时间会缩短一半以上。
8.2 串口通信使用统一的协议约定
GSM模块和单片机之间的通信,建议约定一个简单的协议,不要裸发字符串。比如:
- 单片机发送
AT+CMGF=1\r\n给GSM。 - GSM模块回复
OK\r\n。 - 单片机等到串口收完再发下一条指令。
提高可靠性的细节是:串口接收采用中断方式,设置环形缓冲区或足够大的接收数组;代码中不要用delay_ms代替等待GSM回复,应该用超时机制,避免程序卡死。
8.3 硬件安全与保护
- 电机和GSM模块都属于大电流设备,必须在电源入口加上保险丝或自恢复保险,防止短路。
- 单片机IO口驱动能力有限,驱动电机、继电器必须经过三极管或ULN2003,不要直接接负载。
- 所有模块必须共地,否则串口通信会不稳定。
- 焊接实物时,先断开电源再改接线,避免烧毁引脚。
8.4 代码规范与程序健壮性
- 每个文件头加注释,说明作者、日期、功能和主要引脚定义。
- 状态机枚举用宏定义或枚举类型,不要用魔法数字。
- 所有延时函数的单位写清楚,比如
delay_ms表示毫秒。 - 串口发送函数最好带超时保护,避免GSM模块没响应时进入死循环。
- GSM初始化失败后,系统应该继续运行本地售卖功能,而不是直接死机等待。可以打印错误标志,让用户知道短信功能不可用,但机器还是可以正常卖货。
这一点在实际答辩中很加分。你可以向老师解释:GSM模块作为异步通知通道,不应该影响主业务流程。这是工程设计中经典的“故障隔离”思想。
8.5 短信内容与号码管理
如果要在多个场景中复用这套代码,建议把接收短信的手机号放在一个固定的宏定义里,方便修改。
#define ADMIN_PHONE "13800138000"短信内容尽量精简,因为中英文和字符编码问题容易乱码。如果发送中文短信,需要使用PDU模式,对课程设计来说过于复杂,建议直接在短信中发送英文字符串。
9. 总结与后续学习方向
这个基于51单片机GSM自动售货机项目,本质上是一个综合性的嵌入式系统练习。它没有太高深的技术难点,但把思路理顺,代码写好,硬件调通,每一步都在锻炼必要的嵌入式基本功。做完这个项目,你应该已经掌握了:
- 51单片机GPIO控制、外部中断、定时器串口的基本用法。
- LCD1602、矩阵按键、蜂鸣器、直流电机等常见模块的驱动方式。
- GSM模块AT指令通信的基本流程。
- 状态机思想在嵌入式裸机程序中的实际应用。
- 从仿真到实物的完整开发流程,以及分模块排错的工程习惯。
如果你的时间允许,可以继续往三个方向扩展。一是增加多个货道,用ULN2003驱动多路电机,配合继电器实现更真实的售货逻辑;二是接入更完整的硬币识别器,支持不同面额混投和退币功能;三是把GSM模块升级为4G模块,如合宙Air724UG或EC200S,短信延迟更低、网络更稳定,也能为后续接入云平台做铺垫。
课程设计答辩时,重点关注以下几个讲解维度:系统整体架构图、状态机设计、GSM模块的AT指令流程、硬件电源设计原因、分模块测试方法,以及故障排查案例。能把这些讲清楚,比单纯说“我实现了所有功能”更有说服力。
这套代码里的商品价格、手机号、电机运行时间、投币面额都是可以改的常量,建议你先把最小系统跑通,再根据自己手头的模块和材料做适配。如果硬件连接和代码细节有差异,优先对照原理图画引脚表,把每个模块的供电、地线、信号线都确认清楚,再上电调试。
最后给一个实用的建议:把这篇博客和你的代码、引脚连接表、Proteus仿真图一起收藏好。课程设计做完以后,面试或考研复试时,这就是一个能完整讲述的项目经历。