C51单片机智能水表设计:干簧管脉冲计数与定时器中断应用

发布时间:2026/9/16 10:01:05
C51单片机智能水表设计:干簧管脉冲计数与定时器中断应用 简介基于C51单片机的智能水表设计源代码是一份面向单片机课程设计及项目开发的完整参考工程适合正在完成C51课设或需要实现流量监测、阈值报警功能的初学者。程序通过液晶屏实时显示瞬时流量、累计流量、实时温度以及设定的累计流量阈值并支持按键调整阈值当累计流量超出设定值时蜂鸣器发出报警音同时继电器自动断开起到安全保护与控制作用。代码中包含了脉冲量到流量值的换算、定时器计数、温度读取等关键功能模块每个模块的变量定义和逻辑结构都清晰完整便于读者理解智能水表的测量与处理流程。压缩包共七十六个文件以PNG、JPEG图片和XML、RELS等文档配置类文件为主整体大小约四兆字节涵盖工程代码、设计截图与说明文档方便查看和学习。目前已有二百二十九人在线学习下载适合需要参考完整单片机工程源码、理解液晶显示与继电器控制逻辑的开发者直接复用或二次开发。1. 一个干簧管怎么变成一块智能水表第一次拆开机械水表里面就是转轮、磁铁和干簧管。所谓智能水表本质是把干簧管通断次数数清楚再换算成瞬时流量和累计流量。真正麻烦的是三件事低速时脉冲间隔几百毫秒高速时又容易抖动误触发LCD1602 与 DS18B20 各有严格时序累计流量超阈值后继电器还要真的断开水路。这套基于 C51 单片机的智能水表设计用外部中断累计脉冲、定时器算流量、液晶显示温度与流量、按键设置累计阈值超限就蜂鸣器报警并断开继电器。做 C51 课设或要快速搭一个可读数的水量计量原型解压 .rar 后拿里面的 Word 设计文档对照接线可以直接改成自己的工程。2. C51 定时器与外部中断把脉冲数换算成 L/min2.1 干簧管/霍尔脉冲怎么进单片机智能水表的机械部分通常由一个叶轮和磁钢组成水流推动叶轮旋转磁钢每经过干簧管或霍尔开关一次就输出一个低电平脉冲。脉冲频率与流量成正比但干簧管是机械触点闭合和断开瞬间会产生几毫秒抖动如果直接进单片机 I/O 口主循环可能把一次抖动读成几次计数。我一般会在信号线上并联一个 0.1μF 陶瓷电容顺带把 P3.2 配置成下降沿触发的外部中断。电容容值不要超过 1μF否则叶轮高速旋转时脉冲会被削得太平滑反而丢脉冲。表头信号C51 引脚说明干簧管/霍尔输出P3.2 / INT0下降沿触发外接 10kΩ 上拉地GND与单片机共地VCC5V霍尔开关需要供电干簧管不需要外部中断方式的优点是主循环里即使正在刷 LCD也不会错过脉冲。C51 的 INT0 在下降沿时置中断标志只要IT0 1、EX0 1、EA 1每次脉冲都能进一次中断服务函数。需要留意的是中断服务函数要足够短不能在中断里做 LCD 写操作或sprintf否则高速水流下会阻塞下一次中断。2.2 定时器 0 生成 20ms 时间基准瞬时流量的分母是时间没有时间基准脉冲数只是计数。这里用定时器 0 工作在方式 116 位定时以 12MHz 晶振、12T 模式为例机器周期为 1μs20ms 需要计数 20000所以初值是65536 - 20000 45536也就是0xB1E0。初始化代码可以写成这样#define FOSC 12000000UL #define TIMER0_RELOAD (65536UL - (FOSC / 12UL / 1000UL) * 20UL) void Init_Timer0(void) { TMOD 0xF0; TMOD | 0x01; // 定时器0方式1 TH0 (unsigned char)(TIMER0_RELOAD 8); TL0 (unsigned char)(TIMER0_RELOAD 0xFF); ET0 1; // 允许定时器0中断 TR0 1; // 启动定时器 EA 1; } unsigned int time_ms 0; unsigned char dealFlag 0; void Timer0_ISR(void) interrupt 1 { TH0 (unsigned char)(TIMER0_RELOAD 8); TL0 (unsigned char)(TIMER0_RELOAD 0xFF); time_ms 20; // 毫秒累计 dealFlag 1; // 通知主循环该做周期任务 }TIMER0_RELOAD用宏在编译期算好比在中断里写65536 - 20000直观dealFlag是“20ms 到了”的标志主循环查询到 1 后再做流量计算、按键扫描和显示刷新。这样做的原因是把耗时操作移出中断服务程序。如果你的板子是 11.0592MHz 晶振FOSC / 12 / 1000 * 20不是整数会带来千分之几的计时误差对水表这种慢速计量影响不大如果追求精确可以把定时周期改成 10ms用65536 - (11059200 / 12 / 1000) * 10计算初值。和定时器配套的还有外部中断计数void Ext0_ISR(void) interrupt 0 { PluNum; // 只做计数不处理其他事 }PluNum是unsigned long外部中断里只做自增不做任何计算这样中断占用时间只有几条指令信号频率再高一些也不会丢数。2.3 瞬时流量与累计流量的换算公式脉冲数本身没有物理单位必须乘以一个“每升多少脉冲”的系数通常写成K 脉冲数/L。假设表头标称 450 脉冲对应 1L 水那么PluNum / 450.0f就是累计升数PluNum / 450.0f / 1000.0f就是立方米。瞬时流量的计算则要在一个固定时间窗里统计脉冲增量#define PULSE_PER_LITER 450UL unsigned long PluNum 0; // 外部中断累计的脉冲量 float ShunShi 0; // 瞬时流量单位 L/min float LeiJi 0; // 累计流量单位 L static unsigned long last_plu 0; static unsigned long sum_pulse 0; static unsigned char cnt 0; void Calc_Flow(void) { unsigned long diff; diff PluNum - last_plu; // 本次20ms内的脉冲增量 last_plu PluNum; sum_pulse diff; LeiJi (float)PluNum / (float)PULSE_PER_LITER; if (cnt 5) // 累计5次100ms窗口 { ShunShi (float)sum_pulse * 600.0f / (float)PULSE_PER_LITER; sum_pulse 0; cnt 0; } }这里 600 的来历是1000ms / 20ms 50即 1 秒有 50 个 20ms 窗口再乘 60 秒得到每分钟脉冲数因此100ms 内脉冲数 × 10 × 60 每分钟脉冲数。如果 100ms 内读到 5 个脉冲瞬时流量就是5 × 600 / 450 ≈ 6.67 L/min。PULSE_PER_LITER需要和表头实标一致常见小口径表有 300、450、1000 等不同系数选错会导致显示值和实际水量差好几倍。表头常见系数K 值imp/L备注小口径旋翼式300 ~ 450家庭用较多纯净水表1000 左右脉冲密度更高工业远传水表100 ~ 300流量大时用低系数还要说明一点LeiJi这里用float保存累计升数主要给显示和阈值比较用真正断电不丢失的累计量应该保存PluNum这个unsigned long因为 450 脉冲/L 时16 位计数器只能数到约 146L 就会溢出。裸机程序里定成unsigned long是必要的中断里PluNum在 C51 编译后不是单指令极端情况下主循环读它可能读到“前一半后一半”更严谨的做法是读之前关一下 EA读后再开。2.4 为什么不用主循环轮询主循环轮询if(P3_2 0) PluNum;的问题在于 LCD 写一个字节要几十微秒显示一行要数百微秒机械触点的低电平宽度最短只有 1ms 左右轮询代码路径稍微长一点就会漏数。外部中断把计数从“主循环里找时间”变成“硬件帮你记账”每秒几千个脉冲以下都不会丢。对于水表这种低频信号外部中断绰绰有余如果换到更高频的流量计就要改用定时器 1 做计数器方式把脉冲接到 T1P3.5引脚由硬件自动计数主循环只负责定期读取计数器的值。3. LCD1602 与 DS18B20显示和温度采集的读写时序3.1 LCD1602 的 4 线驱动LCD1602 可以接 8 线和 4 线两种方式C51 课设里普遍用 4 线省 4 个 I/O 口。4 线模式下数据脚只接 D4~D7RS 选择命令/数据E 脚做下降沿锁存。RW 脚可以直接接地这样省掉读忙判断代价是每次写后必须延时等待液晶内部处理完成。初始化时序要先按数据手册两次发送 0x03、再设置 0x28、0x0C、0x01、0x06不能只发一次命令。LCD1602 引脚C51 引脚说明RSP2.00命令1数据RWGND只写模式EP2.2下降沿锁存D4-D7P0.4-P0.7数据高四位#define LCD_RS P2_0 #define LCD_RW P2_1 #define LCD_E P2_2 #define LCD_DATA P0 // 使用 P0.4~P0.7 void LCD_WriteNibble(unsigned char n) { LCD_DATA (LCD_DATA 0x0F) | (n 0xF0); // 只改高4位 LCD_E 1; LCD_E 0; } void LCD_WriteCmd(unsigned char cmd) { LCD_RS 0; LCD_WriteNibble(cmd); LCD_WriteNibble(cmd 4); delay_us(50); } void LCD_WriteData(unsigned char dat) { LCD_RS 1; LCD_WriteNibble(dat); LCD_WriteNibble(dat 4); delay_us(50); }LCD_WriteNibble里先取n 0xF0放到 P0 高四位再把原来的低四位保留下来这样不会干扰接到 P0.0~P0.3 的其他器件cmd 4是把命令字的低四位移到高四位发送。E 脚一个高电平脉冲就是一次数据锁存必须在数据稳定后触发否则液晶收到乱码。初始化时建议在发送 0x28 前至少延时 15ms因为 LCD1602 上电内部复位需要时间匆忙发送第一条命令经常导致屏幕只亮不显示。3.2 DS18B20 单总线时序DS18B20 只占一根 I/O 线但时序非常敏感。每次操作都要经过“复位脉冲 → 存在应答 → ROM 命令 → 功能命令”的流程。这里用跳过 ROM 的 0xCC 命令适合总线上只挂一个传感器的情况。代码里要严格保持 15μs、60μs、480μs 这几个时间窗口最好用_nop_()或短延时函数不要用会被中断打断的长延时去凑。sbit DS18B20_DQ P3^7; bit DS18B20_Reset(void) { bit presence; DS18B20_DQ 0; delay_us(480); DS18B20_DQ 1; delay_us(60); presence DS18B20_DQ; // 低电平表示器件应答 delay_us(420); return presence; } void DS18B20_WriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DS18B20_DQ 0; delay_us(2); DS18B20_DQ dat 0x01; delay_us(60); DS18B20_DQ 1; dat 1; } } unsigned char DS18B20_ReadByte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { DS18B20_DQ 0; dat 1; DS18B20_DQ 1; if (DS18B20_DQ) dat | 0x80; delay_us(60); } return dat; }读取温度的完整流程是复位、写 0x44 启动转换、等待转换完成、再次复位、写 0xBE 读取两个字节。把low和high拼成 16 位整数再乘以 0.0625 就是实际温度。int temp1; // 原始温度寄存器拼接值 float temperature; // 实际温度 void Read_Temperature(void) { unsigned char low, high; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); while (DS18B20_DQ 0); // 等待转换结束DQ拉高表示完成 DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); low DS18B20_ReadByte(); high DS18B20_ReadByte(); temp1 ((unsigned int)high 8) | low; temperature temp1 * 0.0625f; }temp1是带符号 16 位整数负数代表零下温度乘以 0.0625 后同样得到带符号浮点数。DS18B20 上电默认值是 85℃如果显示一直 85℃多半是 DQ 引脚的 4.7kΩ 上拉电阻没接或初始化时序不对。while (DS18B20_DQ 0)这段要加超时计数否则传感器没接好时程序会一直卡在等待里。3.3 显示缓冲与刷新节奏原始工程里定义了char dis0[16]; char dis1[16];两行显示缓冲区这是一个好习惯把要显示的内容先组装到缓冲区再一次写入 LCD避免在计算过程中直接操作液晶产生中间态。第一行通常显示F:xx.x L/M T:xx.xC第二行显示L:xx.xx M3 SET:xx.x。可以用sprintf(dis0, F:%4.1f T:%4.1f, ShunShi, temperature);但 C51 的sprintf会引入较大代码量且对float支持依赖库配置如果编译后代码空间紧张就把浮点乘以 10 转成整数自己拼数字字符。显示刷新不建议和 20ms 计算同步。LCD1602 写满两行大约需要几毫秒持续刷新反而干扰 DS18B20 的 1-Wire 时序也容易看到屏幕字符闪烁。我一般把刷新放到主循环里计数每累计 25 个dealFlag刷一次也就是 500ms 更新一次。瞬时流量变化不剧烈500ms 刷新看起来仍然连续温度更是没必要快刷。4. 阈值报警联动按键、蜂鸣器、继电器的配合4.1 按键扫描与 rekey 防重入原工程里有几个变量值得注意rekey和dealFlag。rekey的作用是按键去抖后在按键释放之前只响应一次否则一次按压会因为主循环多次扫描而把阈值连续加好几次。按键扫描我一般写成延时消抖 等待释放的方式#define KEY_SET P1_0 #define KEY_ADD P1_1 bit rekey 0; unsigned char Key_Scan(void) { if (KEY_SET 0) { delay_ms(10); if (KEY_SET 0 !rekey) { rekey 1; return 1; // KEY_SET 按下 } } else if (KEY_ADD 0) { delay_ms(10); if (KEY_ADD 0 !rekey) { rekey 1; return 2; // KEY_ADD 按下 } } else { rekey 0; // 所有按键释放才允许下一次触发 } return 0; }这里把消抖延时放在主循环而不是定时器中断里主要原因是按键是人的操作10ms 延时不会影响 20ms 水表计算周期。rekey在按下期间保持 1直到两个按键全部释放才清 0能避免“一次按键多次触发”。阈值增减可以使用setNum 0.1f在累加到上限后回绕到下限如果需要在设置时看到当前值可以加一个setMode变量让 LCD 第二行在设置模式下显示SET:xx.x而不是累计值。按键功能范围KEY_SET进入/退出设置模式-KEY_ADD阈值 0.1 m³0.1 ~ 99.9KEY_SUB阈值 -0.1 m³0.1 ~ 99.9KEY_SAVE保存到 EEPROM断电恢复如果原工程只有两个按键可以把“保存”合并到“退出设置”时自动执行这样最少两个按键也能完成完整流程。4.2 累计流量越限与滞回控制原工程的执行逻辑很明确累计流量超过设置阈值蜂鸣器报警、继电器断开否则蜂鸣器不响、继电器闭合。如果把比较直接写在主循环里系统在阈值附近会因为累计量一点一点增长而出现继电器反复开关。更稳的做法是加入滞回区间#define HYSTERESIS 0.05f // 单位 m3 bit alarm 0; void Alarm_Control(void) { float curM3 LeiJi / 1000.0f; // LeiJi 单位是 L转成立方米比较 if (!alarm curM3 setNum) { alarm 1; BEEP 1; RELAY 0; // 断开继电器切断负载 } else if (alarm curM3 setNum - HYSTERESIS) { alarm 0; BEEP 0; RELAY 1; // 低于阈值且回落 0.05 m3 才恢复 } }setNum在原始工程里默认 1.5按 m³ 理解就是 1.5 立方米这和显示端的L:xx.xx M3是一致的。alarm标志用于锁存报警状态避免主循环每次进入都重新触发。继电器是感性负载线圈两端必须并联一个反向续流二极管例如 1N4007否则断开瞬间的反电动势轻则干扰单片机复位重则打坏 I/O 口。蜂鸣器如果是无源蜂鸣器需要给一个 2kHz~4kHz 的方波驱动不能直接拉高有源蜂鸣器则可以直接给高电平。4.3 阈值和累计量的掉电保持传统 AT89C51 没有片内 EEPROM掉电后setNum会回到默认 1.5如果要求“设置一次长期有效”需要外挂 AT24C02 或者改用带 IAP 的 STC 单片机。常见做法是把setNum放大 10 倍转成整数写入 EEPROM读出来再除以 10避免浮点数存储的字节序问题void EEPROM_WriteSet(void) { unsigned int value; value (unsigned int)(setNum * 10.0f); AT24C02_WriteByte(0x00, value 8); AT24C02_WriteByte(0x01, value 0xFF); } void EEPROM_ReadSet(void) { unsigned int value; value ((unsigned int)AT24C02_ReadByte(0x00) 8) | AT24C02_ReadByte(0x01); if (value 0 value 65535) { setNum value / 10.0f; } }这里value用的是 16 位无符号整数最大能存 65535除以 10 后阈值最大 6553.5对水表完全够用。读取后做合法性判断是为了防止 EEPROM 全 0xFF 时把阈值设成一个天文数字。AT24C02_WriteByte和AT24C02_ReadByte按 24C02 的 I2C 时序实现即可包含起始、应答、停止。如果中断里累计的PluNum也要保存建议每秒把当前值写入 EEPROM 一次并同时在 RAM 里保存上一份备份上电时比较两份取合理值这样能减少掉电瞬间数据没写全造成的累计量丢失。5. 计量标定与三种典型现场故障5.1 用容器法重新标定 K 系数K 系数不能只看表头标称。最实用的标定方法是拿一个 5L 量杯开启水表放水记录放水前后PluNum差值和实际水量多测三次取平均。假设 5L 水对应PluNum增加 2263那么实际 K 系数是2263 / 5.0 452.6 imp/L直接改宏定义#define PULSE_PER_LITER 452.6f改完重新编译烧录再放 5L 水验证一次如果显示累计量偏差超过 2%优先怀疑每次放水体积测量不准而不是程序问题。容器法标定尽量保持水流稳定水流忽大忽小虽然会让瞬时流量波动但总脉冲数仍然准确。5.2 现场最常见的三个坑第一低速滴水不出数。多数不是算法问题而是干簧管抖动或上拉太弱。检查IT0 1是否配置信号输入端有无 10kΩ 上拉RC 滤波电容是否超过 1μF。第二DS18B20 显示 85℃。这是传感器上电默认值说明单片机没读到温度重点查 DQ 上拉电阻和读写时序延时特别是while (DS18B20_DQ 0)等待转换完成这一段如果传感器没接会死循环最好加 800ms 超时退出。第三继电器断开瞬间单片机重启。几乎所有 C51 驱动继电器都会遇到原因在反电动势解决办法是在继电器线圈两端反向并联二极管并在单片机电源入口加 100μF 电解电容。5.3 快速验证清单与验收参数测试项做法通过标准脉冲计数信号发生器输出 10Hz 方波到 INT0显示瞬时流量约 1.33 L/min累计精度5L 量杯放水PluNum增量 / 5 等于 K 系数阈值报警阈值设为当前累计值附近继电器断开、蜂鸣器响掉电恢复断电 5 分钟再上电累计量和阈值与断电前一致温度显示用冰水/热水对比DS18B20 误差约 ±0.5℃10Hz 方波对应的期望值是每 100ms 采到 1 个脉冲1 × 600 / 450 ≈ 1.33 L/min如果显示值偏差明显先查PULSE_PER_LITER是否被改成其他数字。完成标定后把新的 K 系数连同阈值一起写入 EEPROM以后每次上电都从 EEPROM 恢复现场维护只需要看第二行当前累计量有没有按预期增长。本文还有配套的精品资源点击获取