资讯动态

基于STC90C516的智能药盒定时提醒系统:定时器中断与LCD1602设计

发布时间:2026/9/19 1:04:43 来源:尧图企业网站定制
简介这是一份基于单片机STC90C516的智能药盒设计说明文档面向电子设计、嵌入式开发初学者及课程设计人群旨在解决老年人用药不便、容易漏服或错服的问题提供一套清晰的智能化用药提醒方案。文档从硬件结构、电路原理到软件模块化设计依次展开详细说明LCD1602液晶显示、定时/计数器、蜂鸣器报警以及系统实时时间与记录时间比较触发提醒的机制并给出系统调试结果、工作原理与应用前景可直接用于课程设计参考、项目开发或论文撰写。资源为单个doc文件压缩包大小3.74MB内容为完整设计说明书包含中英文摘要、目录及各个模块的运作流程便于读者对照学习整体架构与关键细节。已有416人学习下载适合需要快速理解智能药盒实现逻辑、梳理单片机项目文档结构的读者能有效节省自行查阅与整理资料的时间。1. 智能药盒拆解为什么用STC90C516做定时提醒核心智能药盒说到底是一台“走时准、能到点叫”的单片机小系统核心不是药盒而是定时与提醒。这个项目用 STC90C516 做主控没有外挂 RTC 芯片而是靠单片机内部的定时/计数器中断来维护时钟再用 LCD1602 显示时间和用药量到点后驱动蜂鸣器和 LED 报警。它给出的完整链路——最小系统、显示、按键、声光报警、软件流程——正好覆盖了一个典型 51 单片机课程设计需要的全部环节。对正在做类似毕业设计或想快速搭一个家用药盒原型的人来说这个方案里最有价值的部分不是某个电路而是怎样用 8 个按键完成“每日四次、每次四种药”的参数设置以及怎样在连续运行中不丢秒、不重报警。2. 从需求到定时时钟STC90C516总控方案与初值计算2.1 需求拆解四次定时、四种药量如何落到引脚上先把原始需求翻译成工程语言。系统要显示实时时间要支持每天 4 次定时每次提醒 4 种药的服用量提醒入口是 8 个独立按键其中 2 个用于调节实时时间6 个用于设置定时时间与药量。报警时既要让 LCD1602 显示本次药量又要让蜂鸣器和 LED 同时动作持续约 1 分钟后自动退出。我把这些模块映射成一张表方便对照原文档的电路图看功能模块常用器件典型接法说明主控STC90C516RD40 pin DIPP0 口作 LCD 数据总线中断引脚留给定时器显示LCD1602RS/RW/E 接 P2 口D0-D7 接 P0 口并加上拉电阻设置输入8 个独立按键接 P1 口一端到 I/O一端接地按下为低电平声音提示有源蜂鸣器经 S8050 三极管驱动I/O 输出低电平触发光提示LED串 1k 限流电阻后接 I/O低电平点亮这样设计的好处是每个模块都在独立的 I/O 上软件里可以直接读端口判断按键不需要矩阵扫描逻辑简单。代价是占用了较多 I/O但 51 单片机 P0-P3 四组口足够用不用扩展芯片整体成本可以压得很低。2.2 用定时器做时钟还是外挂 RTC很多人看到“时钟”第一反应是上 DS1302。这份设计主动绕开了 RTC 芯片原因是需求精度并不高。药盒提醒的粒度是“分钟”一级系统只需要秒级走时五十毫秒一次的中断完全能撑住。反过来看加 DS1302 虽然停电后时间不丢但要多一片晶振、多一组通信时序、多一份 PCB 面积。对家用药盒来说整机并不依赖断电保持所以用单片机内部定时器中断做时钟是更直接的选型。STC90C516RD 属于 STC90C51RC/RD 系列指令集兼容传统 8051支持 12 时钟和 6 时钟机器周期切换内部带 Flash、RAM、EEPROM 和看门狗串口能直接 ISP 下载程序。这几点对做原型非常重要改代码不需要买仿真器串口几秒就能烧完一片看门狗防止程序跑飞EEPROM 以后存定时参数和校准值都用得上。2.3 定时器初值计算与代码实现系统把定时器作为秒信号的来源。以 12MHz 晶振、12 时钟模式为例一个机器周期是 12/12MHz 1us。要产生 50ms 定时中断需要的机器周期数是 5000016 位定时器向上计数到 65536 溢出所以初值取 65536 - 50000 15536也就是 0x3CB0。每次中断里重装初值再把 20 次 50ms 累加成 1 秒。写成代码就是#include reg51.h #define RELOAD_H 0x3C // 50ms 中断初值高字节, 12MHz/12T #define RELOAD_L 0xB0 // 初值低字节 void timer0_init(void) { TMOD 0xF0; // 只改动定时器0保留定时器1的配置 TMOD | 0x01; // 定时器0方式116位定时模式 TH0 RELOAD_H; // 装入高字节 TL0 RELOAD_L; // 装入低字节 ET0 1; // 允许定时器0中断 EA 1; // 开总中断 TR0 1; // 启动定时器0 } void timer0_isr(void) interrupt 1 { TH0 RELOAD_H; // 重装初值尽可能早做减少误差 TL0 RELOAD_L; tick50ms 1; // 告诉主循环又一个50ms到了 }这段代码里的 TMOD 0xF0 很关键。TMOD 低 4 位属于定时器0高 4 位属于定时器1如果直接赋 0x01会把定时器1原本的设置冲掉。tick50ms 是一个全局状态标志在第 5 章会给出定义和调度方式。重装初值放在中断最前面是因为进入中断后到重装之间还有几条压栈指令放前的误差比放后小一两个机器周期虽然对分钟级提醒无感但积累一天会少几百微秒。若改用 11.0592MHz 晶振机器周期约 1.085us同样的 50ms 需要 46080 个机器周期初值要重算为 19456。所以后来调误差优先改晶振而不是改代码里的魔法数字。3. 最小系统之外STC90C516与LCD1602硬件电路解剖3.1 单片机最小系统晶振、复位、供电STC90C516RD 的 40 脚封装里第 40 脚 VCC 接 5V第 20 脚 GND 接地。晶振接在 XTAL1(19) 和 XTAL2(18) 之间两个 20~30pF 瓷片电容分别从两端接到地作用是微调频率并帮助起振。上电后 CPU 从片内 Flash 取指令所有操作都在这个时钟节拍下推进。复位电路用 10uF 电容加 10k 电阻组成上电复位上电瞬间电容充电RST 脚维持几十毫秒高电平满足单片机复位要求。STC90C51 系列内部有 MAX810 复位电路12MHz 晶振以下复位脚可以直接接地但工程上还是保留外部阻容复位更稳妥调试时按键复位很好用。PCB 布局时晶振和电容要尽量靠近单片机引脚走线尽量短避免因寄生电容过大出现“程序能烧进去但跑不起来”的现象。3.2 LCD1602接口与显示原理LCD1602 是 16 脚字符型液晶。第 1 脚 VSS 接地第 2 脚 VCC 接 5V第 3 脚 VO 是对比度调节端。VO 直接接地对比度最高接正电源对比度最弱实际用 1k 电位器从 VCC 到地中间抽头接 VO。对比度调太高会出现一排黑块太低则白屏看不出字这是 1602 最常见的“没显示”原因。数据端口 D0-D7 接 P0 口P0 是开漏输出作为通用 I/O 时必须外接上拉排阻否则高电平带不动液晶数据线。控制线 RS、RW、E 分别接 P2 口三个 I/O。RS 决定访问的是指令寄存器还是数据寄存器RW 决定读还是写E 端下降沿时液晶锁存数据。接口定义我按常见习惯整理如下1602引脚功能单片机接法1 VSS电源地GND2 VCC5VVCC3 VO对比度1k电位器中点4 RS寄存器选择P2.55 RW读/写P2.66 E使能P2.77-14 D0-D7数据P0.0-P0.7加10k上拉15-16 A/K背光串电阻接5V/GND显示原理并不复杂向指令寄存器写 0x38 设置 8 位数据、2 行、5x7 点阵写 0x0C 开显示且不显示光标写 0x01 清屏之后写字符对应的 ASCII字符就会依次出现在液晶上。控制 RS/RW/E 的高低电平顺序是驱动代码的核心后面会给出具体实现。3.3 按键、蜂鸣器与LED电路8 个独立按键直接接 P1 口。每个按键一端接 P1.x另一端接地单片机上电后 P1 口为高电平按下变成低电平。独立按键的好处是程序里一读端口就能知道哪个键被按下不需要扫描矩阵也不会因为多个键同时按下导致编码冲突。这个项目里 8 个按键分别承担调时功能键、调时调节键、定时设置功能键、定时设置调节键和 4 个定时选择键键位固定后软件状态机就跟着定下来。蜂鸣器不能直接挂在 I/O 上I/O 高电平驱动能力有限。常见做法是 P2.0 通过 1k 电阻接到 S8050 三极管基极发射极接地集电极接有源蜂鸣器到 VCC。P2.0 输出低电平时三极管导通蜂鸣器通电发声。LED 则简单一些P2.1 串一个 1k 限流电阻接到 LED 阳极阴极接地低电平点亮。两个器件共用“低电平有效”的逻辑软件里只需要把 P2 口对应位清零声音和光就同时出现。4. 按键扫描、状态机和1602驱动把设置信息变成显示4.1 主程序结构与状态机到了软件部分先把流程拆成两个平面中断平面负责走时主循环平面负责按键、显示和比较。中断里只维护 second/minute/hour 以及 tick 标志不做液晶操作主循环里轮询按键响应设置刷新 1602。这样中断服务函数很短时钟稳定性和显示刷新互不拖累。设置过程靠状态机管理。每按一次功能键state 加一从“调时-分”“调时-时”切换到“定时1药量”“定时2药量”等按调节键修改当前 state 对应的值。最后一次按功能键再按下定时键保存整套参数。为了防止设置过程中秒跳变干扰用户看到的数值进入设置状态后建议先关闭定时器中断保存完再开。这个“关中断-设置-开中断”的顺序是文档里强调过的实际调试时非常有用。4.2 LCD1602读写驱动驱动 1602 本质是按时序往指令寄存器或数据寄存器写字节。控制线 RS0 且 RW0 时写指令RS1 且 RW0 时写数据。E 脚先拉高再拉低在下降沿液晶完成锁存。驱动代码我习惯写成两条函数#include reg51.h sbit LCD_RS P2^5; sbit LCD_RW P2^6; sbit LCD_E P2^7; void lcd_write_cmd(unsigned char cmd) { LCD_RS 0; // 选择指令寄存器 LCD_RW 0; // 写模式 P0 cmd; // 数据放到总线上 LCD_E 1; // E拉高让液晶准备采样 delay_us(40); LCD_E 0; // E拉低下降沿锁存命令 delay_us(40); } void lcd_write_data(unsigned char dat) { LCD_RS 1; // 选择数据寄存器 LCD_RW 0; P0 dat; LCD_E 1; delay_us(40); LCD_E 0; delay_us(40); } void lcd_init(void) { lcd_write_cmd(0x38); // 8位数据、2行、5x7点阵 lcd_write_cmd(0x0C); // 开显示不显示光标 lcd_write_cmd(0x06); // 写入数据后地址自动加一 lcd_write_cmd(0x01); // 清屏 }参数说明0x38 是最常用的初始化命令前提是硬件接成 8 位数据线0x0C 中的 0x0D 会显示光标0x0E 显示闪烁光标药盒界面不需要光标所以用 0x0C。0x06 决定了字符写入后光标向右移动这样连续写字符串时不用手工指定每个字符地址。delay 不能省1602 执行清屏指令需要 1.5ms 以上写数据最快也要几十微秒间隔否则会出现乱码。用软件延时最简单不必读忙标志。4.3 按键扫描与消抖按键的机械触点按下和释放时会有 5~20ms 的抖动直接判断电平下降沿会把一次按下识别成多次。我用的消抖方式是“连续两次读到相同电平才算有效”一次扫描返回无效主循环在几毫秒间隔内再扫第二次。代码如下unsigned char key_read(void) { static unsigned char last 0xFF; // 上次读到的电平 unsigned char now; now ~P1 0xFF; // 按下的位为1未按为0 if (now last) { return now; // 连续两次相同认为是稳定值 } last now; return 0xFF; // 第一次变化丢弃 }这里把 P1 取反让按下的键对应位为 1返回值直接可以按位判断是哪个键。第一次进入时 last 初始化为 0xFF如果某键第一次读到 1会在第二次扫描才返回等效于延时消抖。这样消抖不需要阻塞式 delay主循环还能继续刷屏。要注意如果按键按住不放函数会每两次扫描都返回同样的值所以外层还要处理“按下后必须释放才允许再次响应”避免功能键按住时状态一直往下跳。4.4 显示刷新时间与药量的格式时间显示放在第一行格式HH:MM:SS药量信息放第二行。刷新函数先写 DDRAM 地址 0x80再连续写字符串void lcd_display_time(void) { char buf[16]; unsigned char i; sprintf(buf, %02d:%02d:%02d, hour, minute, second); lcd_write_cmd(0x80); // DDRAM首地址第一行第1列 for (i 0; i 8 buf[i] ! \0; i) { lcd_write_data(buf[i]); } }参数说明0x80 是 DDRAM 地址 0 带最高位置 1 的写法1602 的数据地址都是这个格式第二行首地址是 0xC0。sprintf 在 C51 里占用空间较大如果 Flash 紧张可以用手写整型转字符串代替。unsigned char 变量直接%02d输出时要注意高位补零这样秒从 0 到 59 时显示不会少一位。显示刷新不需要每秒都重写整个字符串只在 second 变化时调用一次即可省下的时间留给按键处理。5. 定时器中断与用药提醒算法从秒信号到一条不早不晚的提醒5.1 用定时器0产生秒信号前面写到了定时器初值 0x3CB0 和中断重装这里补上完整的秒计时逻辑。系统时间实际上靠一个全局 tick 计数生成主循环里每次发现 tick50ms 被置位就累加。20 次 50ms 是 1 秒秒满 60 进分分满 60 进时时满 24 归零这是 24 小时制。代码结构如下volatile unsigned char tick50ms 0; unsigned int tick_count 0; unsigned char second 0, minute 0, hour 0; void clock_update(void) { if (!tick50ms) return; tick50ms 0; if (tick_count 20) { // 20 * 50ms 1s tick_count 0; if (second 60) { second 0; if (minute 60) { minute 0; if (hour 24) hour 0; } } } }为什么不用定时器方式 2方式 2 是 8 位自动重装最大定时 256us12MHz 下中断频率接近 4kHz对 51 来说太频繁。方式 1 用 16 位计数器一个中断周期可以压到 50ms主循环每次循环只做一次比较CPU 占用很低。另一个细节是 tick50ms 是 volatile 变量因为中断和主循环都会访问它不加 volatile 时编译器可能把if (!tick50ms) return;优化成只读一次导致某个时间段一直认为没有 tick。5.2 用药提醒时间相等触发一分钟提醒的核心是比较逻辑。系统每秒钟检查一次当前hour:minute是否和 4 个定时项之一相等。为了避免每秒都重复触发我用alarm_active锁存报警状态并用alarm_minute记录触发时刻的分钟值。等到当前时间不再是那分钟就自动退出。数据结构用一个结构体数组存四个定时项struct med_reminder { unsigned char hour; unsigned char minute; unsigned char dose[4]; // 第0~3种药的服用量 } med[4];比较函数放在主循环里void check_alarm(void) { unsigned char i; if (!alarm_active) { for (i 0; i 4; i) { if (hour med[i].hour minute med[i].minute) { alarm_active 1; // 进入报警状态 alarm_index i; alarm_minutes minute; alarm_hours hour; P2_0 0; // 蜂鸣器响 P2_1 0; // LED亮 lcd_show_dose(i); // 液晶显示第 i 种定时对应的药量 break; } } } else { // 当前分钟已经离开触发分钟退出报警 if (hour ! alarm_hours || minute ! alarm_minutes) { alarm_active 0; P2_0 1; P2_1 1; lcd_show_time(); } } }这段逻辑覆盖了文档里“一分钟后自动退出”的需求。用户设置 8:00 提醒系统在 8:00:00 与设定值相等时立刻报警走到 8:01:00 时 minute 变为 1不再等于触发时的 0报警关闭。报警持续 60 秒符合预期。alarm_index用于查询应该显示哪个定时项的药量因为 4 个定时项各有一组 dose[4]显示函数里直接读出对应数组就行。这里没有用秒参与比较是为了防止报警提前或延迟秒范围同时也避免在 8:00:59 到 8:01:00 之间因为 second 进位而少报。5.3 设置过程与关中断临时冻结时钟设置过程最容易被忽略的是中断对共享变量的写。用户在调小时如果中断恰好把 minute 从 59 进位到 0下一次显示可能刷新回原来值。文档里给的做法是检测到设置功能键按下后先关中断调节过程结束后开中断再按下定时键保存当前值。在 C 代码里可以直接操作 TR0void enter_setting(void) { TR0 0; // 停掉定时器0秒信号停走 ET0 0; // 不允许中断重入 } void exit_setting(void) { TR0 1; // 重新启动定时器 ET0 1; }注意 TR0 停掉后定时器计数器停在当前值重新启动时仍然是上次的计数值并不会重置所以时间不会跳变。如果只关 ET0 而开着 TR0定时器依然在跑只是不产生中断这也能冻结时间但计数器会在重启后先溢出一次产生一次性额外 tick。我的习惯是同时关 TR0 和 ET0彻底冻结时钟源。保存设置时要把临时变量里的 hour、minute 和 dose 数组整体拷到 med 结构体再开中断避免开中断瞬间被新 tick 覆盖。6. 调试方法与三个绕不开的坑6.1 Keil Proteus 的先行验证先把电路在 Proteus 里搭起来用 Keil C51 编译出的 HEX 文件仿真。仿真环境里最容易验证的是 LCD 背光和字符加载 0x38、0x0C、0x01 后如果屏幕出现黑影或乱码第一先查命令时序第二查数据端口是不是接上了上拉。秒信号可以通过在仿真里加一个虚拟示波器看 P1 口翻转波形确认中断在跑。6.2 实物调试三个坑第一个坑是 LCD1602 白屏。多半不是代码问题而是 VO 对比度没调好或是 P0 口缺上拉电阻。第二个坑是蜂鸣器不响或声音小。把无源蜂鸣器当成有源软件只给高/低电平蜂鸣器只会嗒一声而不会长鸣要么换成有源蜂鸣器要么用 2kHz PWM 驱动。第三个坑是时间偏快或偏慢。12MHz 晶振带 30pF 电容会产生频率偏移一天差十几秒很常见。解决方法是把晶振两脚电容从 30pF 换成 20pF 试试或者改用 11.0592MHz 晶振同时按65536 - (定时时间 / 机器周期)重新计算初值。6.3 不拆代码的秒级校准如果试了几组电容误差还是固定可以在中断里做补偿。每累计到 3600 秒若实测快 2 秒就让秒进位逻辑跳过 2 次也就是把if (tick_count 20)的阈值临时改成 22持续一分钟再改回来。更简单的方法是用 LED 每秒翻转一次引脚拿手机秒表对 10 分钟内的翻转次数算出每天误差再把补偿值写进 STC 的 EEPROM这样每台机器都能单独校准不依赖晶振个体精度。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价