简介:面向单片机课程设计、毕业设计与电子入门实践的51单片机汽车胎压监测报警系统资料包,围绕轮胎压力实时采集、阈值判断与声光报警展开,帮助读者搭建从传感器信号处理、51单片机程序控制到显示更新和报警联动的完整方案。标题信息显示包内以程序源码、仿真工程、电路原理图和元件清单为主,可用于对照理解系统组成、接口设计与仿真验证流程;文件总数暂未标注,类型明细上游未提供,压缩包约9.87MB。现有319人学习,适合需要快速获取胎压监测参考实现、梳理硬件连接与程序逻辑的读者。资料覆盖初始化配置、数据采集滤波、压力计算、报警判断及显示刷新等关键环节,也便于在仿真环境中提前排查问题、优化硬件配置与代码,作为课程设计或项目原型的起步材料较为实用。
1. 四个轮子的压力同时盯:这套 51 单片机胎压监测报警系统解决的是什么问题
四个轮胎的压力值同时挂在一块 1602 屏上,任意一路掉到 1.8 bar 以下或者窜到 3.2 bar 以上,蜂鸣器就开始叫,对应那一路的 LED 闪起来。这套基于 STC89C52 的汽车胎压监测报警系统,本质上就是把「采样 → 换算 → 比较 → 报警」这条链路在一个 8 位单片机上完整跑通,压缩包里把程序、Proteus 仿真、原理图和元件清单一次性给全了,属于拿到手能编译、能仿真、能照着焊的那一类资源。
它适合三类人:课程设计要交活的、想练手 51 外设驱动(ADC0832、LCD1602、独立按键、蜂鸣器)的初学者,以及要给老设备加个简易胎压巡检的工程开发人员。它不追求 CAN 总线和无线组网,胜在链路完整——每一行代码都能对上原理图上的一个网络标号,这对刚开始啃单片机的人来说,比一个功能花哨但看不透的工程有价值得多。
2. 系统架构与元件清单:从压力传感器到四路轮询的完整链路
2.1 硬件分层:主控、采样、人机、报警四块怎么分工
把原理图摊开,整个系统其实就四块,分工特别清楚。
第一块是主控最小系统:STC89C52RC 加晶体振荡电路、上电复位电路和电源去耦。这块没任何花样,DIP40 的芯片、两个 22pF 瓷片电容配一个晶振、10uF 电解加 10k 电阻做复位,再加一个复位按键,齐活。
第二块是压力采样链。四路模拟输出的压力传感器,各自经一级 RC 低通,进多路开关,再进 ADC0832 转成数字量,最后从 P3 口的几个引脚进单片机。这条链是整个系统里唯一需要认真对待的模拟通路,后面精度崩不崩,八成看这一段。
第三块是人机接口:LCD1602 负责显示四个轮子的实时压力值和当前阈值,四个独立按键分别对应「设置」「加」「减」「切轮位」。
第四块是报警输出:一个有源或无源蜂鸣器加四路 LED,每路 LED 对应一个轮胎。哪路异常,哪路亮。
Proteus 仿真里,真实的压力传感器是用四个电位器替代的——仿真模型库里没有 MPX 系列的 spice 模型,用可调电阻分压模拟 0~5V 的传感器输出,调一下旋钮就相当于给轮胎放气或打气,调试起来反而比实物方便。原理图上留的是标准三针接口(VCC / GND / OUT),实物上换成真传感器直接插就行,这也是这套图纸能同时兼顾仿真和实物的关键设计。
2.2 元件清单逐项过:每个器件在电路里干什么活
压缩包里的元件清单我按电路功能重新梳理了一遍,把关键项和选型理由放在下面:
| 器件 | 型号/参数 | 数量 | 在电路里的作用 | 选型注意 |
|---|---|---|---|---|
| 主控 | STC89C52RC(DIP40) | 1 | 全部逻辑运算与外设调度 | 兼容 AT89C52,Keil 里可直接选 Atmel 器件 |
| 晶振 | 11.0592MHz | 1 | 系统时钟 | 要跑串口就选它,12MHz 除不尽波特率 |
| 复位电容 | 10uF / 16V 电解 | 1 | 上电复位延时 | 配 10k 下拉电阻 |
| 模数转换 | ADC0832 | 1 | 8 位串行 A/D | 只有 2 个单端通道,四路要扩展 |
| 多路开关 | CD4051 / CD4052 | 1 | 四路传感器切换 | 4051 是 8 选 1,4052 是双 4 选 1 |
| 压力传感器 | 模拟电压输出型 | 4 | 感受气压并输出电压 | 量程要覆盖 0~250kPa |
| 液晶 | LCD1602 | 1 | 显示四路压力与状态 | 带背光版更好调 |
| 对比度电位器 | 10k | 1 | 调 LCD 的 V0 | 少了它大概率白屏 |
| 独立按键 | 6×6 轻触 | 4 | 设置阈值、切换轮位 | 一端接地,靠内部上拉 |
| 蜂鸣器 | 5V 有源/无源 | 1 | 声音报警 | 无源的要方波驱动 |
| 驱动三极管 | S8550(PNP) | 1 | 放大蜂鸣器电流 | 单片机 IO 直接驱动带不动 |
| 指示 LED | 5mm 红/绿 | 4 | 分轮位指示 | 串 1k 限流电阻 |
| 上拉排阻 | 10k(9 脚) | 1 | P0 口上拉 | 51 的 P0 是开漏,不加驱动不了 |
| 滤波电容 | 100uF + 0.1uF | 各若干 | 电源去耦 | 蜂鸣器电源脚必须加 |
有两点值得单独说一下。晶振为什么用 11.0592MHz 而不是 12MHz:12MHz 看着整数好看,但 51 的定时器算波特率时除不尽,一旦后面要加串口打印调试信息就会翻车;11.0592 是 9600 波特率在 51 上的整数解,即便当前这套程序不用串口,留着也是给自己留退路。
P0 口的排阻容易被忽略。很多人焊完发现 LCD 有背光但一个字符都不显示,折腾半天代码,最后发现是 P0 没上拉——51 的 P0 口内部没有上拉电阻,输出不了高电平,数据线一直是低,LCD 收到的全是 0x00。
2.3 信号链与供电:为什么参考电压决定了一半的精度
ADC0832 是 8 位分辨率,参考电压直接取单片机 VCC,5V 满量程对应 255 个码值,一个 LSB 就是 5000mV / 255 ≈ 19.6mV。按 45mV/kPa 的传感器灵敏度折算,一个码值大约对应 0.44kPa,也就是 0.0044 bar。这个分辨率用来判断「低于 1.8 bar 就报警」绰绰有余,显示到小数点后一位完全没有问题。
真正的问题不在分辨率,在稳定性。蜂鸣器吸合瞬间电流能到 30mA 以上,如果它和 ADC 共用同一路 5V 走线,VCC 会被拉低几十毫伏,换算下来所有通道的读数一起往下掉 2~3 个码值。这个现象在仿真里看不出来,因为 Proteus 的电源是理想的,只有焊出实物才会暴露。
我一般的处理是三件事:蜂鸣器电源脚并一个 100uF 电解加一个 0.1uF 瓷片,走线从电源入口单独拉一路;模拟地和数字地在电源入口处单点汇合,别在板子上到处乱连;每路传感器输出串一个 1k 电阻加一个 0.1uF 电容到地,做成截止频率约 1.6kHz 的低通,把开关电源和电机带来的高频噪声挡在 ADC 外面。
还有一件事容易漏:多路开关切换通道之后,ADC 内部的采样电容需要建立时间。CD4051 的导通电阻在 5V 供电下大概一两百欧,配上后面的采样电容,稳定下来要几十微秒。代码里切完地址立刻启动转换,读回来的值会带着上一路的残留,四路读数看起来「互相串」。仿真里因为模型理想不明显,实物上就是灾难。
提示:这套图纸的传感器接口是通用模拟电压输入,量程系数写在代码的宏里。换上不同型号的传感器,只需要改
SENSOR_K和SENSOR_B两个宏,不用动采样流程。
3. 环境搭起来:Keil C51 编译、Proteus 联调、实物上电的三段式跑通流程
3.1 Keil C51 工程配置与一次成功编译
压缩包里的程序是标准 C51 工程,用 Keil uVision4 或 uVision5 加 C51 编译器打开都能编。打开之后先做三件事,缺一件就编不出能跑的 hex。
第一,确认器件选的是 Atmel 的 AT89C52(STC89C52 跟它指令兼容,Keil 的器件库里没有 STC)。第二,在 Target Options 的 Output 页把「Create HEX File」勾上,否则编译只生成 obj 不生成 hex,Proteus 那边就会一直加载不到新程序。第三,Target 页的 Xtal 填 11.0592,跟实际晶振对上,不然所有基于delay_ms()的时序都会偏。
工程目录整理之后大概是这么个结构:
TPMS_51/ ├── Project/ │ └── TPMS.uvproj # Keil 工程文件 ├── User/ │ ├── main.c # 主循环与定时调度 │ ├── adc0832.c # 采样时序与压力换算 │ ├── lcd1602.c # 显示驱动 │ └── key.c # 按键扫描 ├── Inc/ │ └── tpms.h # 阈值宏、结构体、引脚定义 ├── Sim/ │ └── TPMS.pdsprj # Proteus 仿真工程 ├── Doc/ │ ├── 原理图.pdf │ └── 元件清单.xlsx └── Output/ └── TPMS.hex # 编译产物,仿真和烧录都用它编译完看 Build Output 窗口,必须是0 Error(s), 0 Warning(s)。带 Warning 也能生成 hex,但那种工程往往埋着隐式类型转换的雷,比如把unsigned int的压力值塞进unsigned char变量,超过 255 直接截断,屏幕上的数会莫名其妙归零。
3.2 Proteus 仿真加载:hex、时钟频率与虚拟终端
仿真工程打开后,双击那颗单片机,弹出属性框里有两个值必须改:Program File 指向Output/TPMS.hex,Clock Frequency 填11.0592M。
Program File 这一项最容易出错。Keil 每次编译生成的 hex 会覆盖旧文件,但 Proteus 如果已经加载过一次,双击进去看到的路径是对的,实际跑的却可能是内存里的旧副本。稳妥做法是每次重新编译后,在 Proteus 里把 Program File 那一栏重新选一遍,或者直接关掉仿真工程重开。
Clock Frequency 这一项更隐蔽。Proteus 默认是 12MHz,跟 11.0592 差 8%。LCD1602 的初始化时序对时间敏感,差这 8% 有时能跑有时不能跑,表现出来就是「昨天还好好的今天一打开就白屏」,属于典型的玄学问题。
跑起来之后,四个电位器就是四个轮胎。往上拧增加电压相当于打气,往下拧相当于放气。把某一路拧到底,对应的 LED 应该亮、蜂鸣器应该响、屏幕上那一格应该出现异常提示。这一步能全过,说明逻辑没问题,可以进实物阶段了。
3.3 实物焊接与首次上电检查顺序
实物阶段最忌讳一次焊完再上电。我的习惯是分四步,每步都有一个可验证的输出。
第一步只焊最小系统:单片机座、晶振、两个 22pF、复位电路、电源座和去耦电容。烧一个最简单的闪灯程序进去,P1.0 接个 LED 加 1k 电阻,看到灯一闪一闪,说明供电、时钟、复位、下载四件事全对了。这一步不过,后面全是白费。
第二步焊 LCD1602 和对比度电位器。先不接数据线,只接电源和背光,确认背光亮。再接上数据线和控制线,烧一个只显示固定字符串的程序,一边显示一边拧那个 10k 电位器,直到字符最清晰。很多人卡在「背光亮但没字」,九成是这个电位器拧在极端位置上。
第三步焊 ADC0832 和传感器接口。这时候先不接传感器,把每个通道的输入脚用杜邦线接到 GND 和 VCC,看读数是不是稳定在 0 和满量程附近。这一步是在验证采样链路本身,把传感器排除在外。
第四步才焊蜂鸣器和 LED。蜂鸣器一定要接三极管,单片机 IO 的灌电流能力有限,直推要么声音小得像蚊子,要么把 IO 口烧了。S8550 是 PNP,做高边驱动时 IO 输出低电平导通,这个极性搞反了蜂鸣器永远不响。
4. 核心代码拆解:ADC0832 采样、压力换算、阈值状态机四段主逻辑
4.1 定时调度与主循环骨架
整套程序不用 RTOS,也不需要。一个定时器中断做时基,主循环按标志位干活,对 51 这种资源来说是最省的做法。
#include <reg52.h> unsigned int g_tick = 0; /* 1ms 时基计数 */ unsigned char g_scan_flag = 0; /* 200ms 采样标志 */ void Timer0_Init(void) { TMOD &= 0xF0; TMOD |= 0x01; /* 定时器0,工作方式1,16位 */ TH0 = (65536 - 922) / 256; /* 11.0592MHz/12 = 921.6kHz,1ms 约 922 个计数 */ TL0 = (65536 - 922) % 256; ET0 = 1; EA = 1; TR0 = 1; } void Timer0_ISR(void) interrupt 1 { TH0 = (65536 - 922) / 256; TL0 = (65536 - 922) % 256; g_tick++; if (g_tick % 200 == 0) { g_scan_flag = 1; /* 每 200ms 置一次采样请求 */ } } void main(void) { System_Init(); /* IO 方向、LCD 初始化、阈值赋默认值 */ Timer0_Init(); while (1) { Key_Scan(); /* 按键随时响应,不放在中断里 */ if (g_scan_flag) { g_scan_flag = 0; Wheel_Sample_All(); /* 四路采样 + 换算 + 判断 */ Alarm_Update(); /* 蜂鸣器与 LED 输出 */ LCD_Refresh(); /* 刷屏 */ } } }为什么是 200ms:轮胎压力是个慢变量,机械系统的响应时间在秒级,采样频率再高也只是把噪声一起采进来。200ms 一轮,人眼看着数字是连续变化的,又给了每路传感器足够的建立时间和软件平均的样本量。
按键为什么不放中断:按键要处理的是「短按」「长按」「连按」这些语义,放在中断里会让中断服务函数变得又长又难维护,而且按键抖动会在中断里被放大成多次触发。放主循环里靠状态机消抖,逻辑清晰得多。
4.2 ADC0832 读时序与压力换算
ADC0832 是串行接口,DI 和 DO 共用一根线,靠分时切换方向。这段时序写错了,读出来就是一堆随机数。
sbit ADC_CS = P3^5; sbit ADC_CLK = P3^6; sbit ADC_DIO = P3^7; /* ch: 0 或 1,对应 ADC0832 的两个单端通道 */ static unsigned char Adc0832_Read(unsigned char ch) { unsigned char i, dat = 0; ADC_CS = 0; /* 片选拉低,一次转换开始 */ ADC_CLK = 0; ADC_DIO = 1; /* 第1个时钟:起始位 */ ADC_CLK = 1; ADC_CLK = 0; ADC_DIO = 1; /* 第2个时钟:SGL=1,单端输入模式 */ ADC_CLK = 1; ADC_CLK = 0; ADC_DIO = ch; /* 第3个时钟:ODD 位选择通道 */ ADC_CLK = 1; ADC_CLK = 0; ADC_DIO = 1; /* 释放数据线,交给 ADC 驱动 */ for (i = 0; i < 8; i++) { /* 高位在前读 8 位 */ ADC_CLK = 1; ADC_CLK = 0; dat = (dat << 1) | (unsigned char)ADC_DIO; } ADC_CS = 1; /* 片选拉高,本次转换结束 */ return dat; }这段代码最容易翻车的点是ADC_DIO = 1;这一句。51 的 IO 口做输入之前必须先写 1(准双向口的特性),否则内部的下拉会把线拉死在低电平,读回来永远是 0。CS 拉高之后也要记得给 DIO 写 1,否则下一轮起始位发不出去。
另一个点是时钟速度。实物上 ADC0832 最快能跑到 400kHz,51 用软件翻转 IO 差不多就是几百 kHz,刚好卡在边缘。如果读出来的值不稳定,在两个ADC_CLK = 1; ADC_CLK = 0;之间加一两个空操作_nop_();,把时钟压到 200kHz 以下,稳定性立刻上来。
原始码值要变成压力值,中间隔着一个线性换算:
/* 传感器手册给的线性关系:Vout = Vcc * (K * P + B),P 单位 kPa 这里把 K 折算成 mV/kPa,B 折算成 mV,全部用整数算,避开浮点 */ #define VREF_MV 5000L /* ADC 参考电压,直接取 VCC */ #define ADC_FULL 255L /* 8 位满量程 */ #define SENSOR_K 45L /* 45 mV/kPa */ #define SENSOR_B (-475L) /* 零点偏移 -475 mV */ unsigned int Adc_To_Press10(unsigned char adc) { long vout_mv; long p10; vout_mv = (long)adc * VREF_MV / ADC_FULL; /* ADC 码值 → 毫伏 */ p10 = (vout_mv - SENSOR_B) * 10L / SENSOR_K; /* 反解压力,放大 10 倍 */ if (p10 < 0) p10 = 0; if (p10 > 9999) p10 = 9999; return (unsigned int)p10; /* 单位 0.1 kPa */ }参数怎么改:SENSOR_K和SENSOR_B完全来自器件手册的传输函数。上面这组系数对应的是 15~115kPa 量程的气压传感器,用在实验室演示没问题,真要装车测胎压必须换成量程覆盖 0~250kPa 的型号,然后把手册里的斜率和截距重新折算进去。这一步不做,屏幕上的数字永远是六十几 kPa,怎么调都上不去。
p10用long而不是int是有原因的:vout_mv - SENSOR_B最大能到 5000 + 475 = 5475,乘 10 就是 54750,超过 16 位有符号整数的 32767,用int直接溢出成负数。
采样还要做平均。单次采样的抖动在 ±2 个 LSB 左右,不处理的话屏幕末位一直在跳,看着像坏了:
#define SAMPLE_NUM 8 static unsigned int Wheel_ReadRaw(unsigned char idx) { unsigned char i; unsigned long sum = 0; for (i = 0; i < SAMPLE_NUM; i++) { sum += Adc0832_Read(Channel_Map[idx]); /* 通道映射表,兼容 4051 扩展 */ } return (unsigned int)(sum / SAMPLE_NUM); /* 8 次算术平均 */ }8 次平均能把随机噪声压到原来的 1/2.8 左右,代价是每路多花几百微秒。四路加起来不到 5ms,相对于 200ms 的采样周期完全可以接受。要更狠一点可以改成去掉最大最小值再平均,抗脉冲干扰更好,但代码量翻倍,这个场景没必要。
4.3 阈值判断、消抖与报警状态机
判断逻辑本身很简单,难的是让它别乱叫。车过个减速带,压力瞬间波动十几个 kPa,如果直接用瞬时值比较,蜂鸣器会跟着路面节奏响。
#define ST_OK 0 #define ST_LOW 1 #define ST_HIGH 2 #define ST_SENSOR_ERR 3 typedef struct { unsigned int press; /* 当前压力,单位 0.1kPa */ unsigned int hi; /* 上限 */ unsigned int lo; /* 下限 */ unsigned char raw_st; /* 本次瞬时判断结果 */ unsigned char st; /* 消抖后的稳定状态 */ unsigned char cnt; /* 连续一致计数 */ } Wheel_t; Wheel_t g_wheel[4]; static unsigned char Wheel_RawJudge(Wheel_t *w) { if (w->press < 50 || w->press > 9000) return ST_SENSOR_ERR; /* 断线或短路 */ if (w->press < w->lo) return ST_LOW; if (w->press > w->hi) return ST_HIGH; return ST_OK; } void Wheel_Judge_All(void) { unsigned char i, now; for (i = 0; i < 4; i++) { now = Wheel_RawJudge(&g_wheel[i]); if (now == g_wheel[i].raw_st) { if (g_wheel[i].cnt < 250) g_wheel[i].cnt++; } else { g_wheel[i].cnt = 0; g_wheel[i].raw_st = now; } /* 连续 3 轮(约 600ms)结论一致才认,滤掉过坎瞬间的毛刺 */ if (g_wheel[i].cnt >= 3) { g_wheel[i].st = now; } } }600ms 这个数不是拍脑袋来的。轮胎压力变化的物理时间常数在秒级,而路面冲击引起的压力尖峰通常在 100~300ms 内衰减完。3 轮确认刚好卡在两者中间:真的漏气一定撑得过 600ms,假的尖峰一定撑不过。
报警输出单独抽成一个函数,把「判断」和「执行」解耦,后面要加分级报警或者换报警方式,只动这一个函数:
void Alarm_Update(void) { unsigned char i, any_alarm = 0; static unsigned char beep_cnt = 0; for (i = 0; i < 4; i++) { if (g_wheel[i].st != ST_OK) any_alarm = 1; } if (any_alarm) { /* 无源蜂鸣器靠方波驱动,这里每 1ms 翻转一次 ≈ 500Hz */ if (++beep_cnt >= 1) { beep_cnt = 0; BEEP = !BEEP; } } else { BEEP = 0; beep_cnt = 0; } for (i = 0; i < 4; i++) { LED[i] = (g_wheel[i].st != ST_OK) ? 0 : 1; /* 低电平点亮 */ } }这段里BEEP = !BEEP放在主循环里其实不够准,主循环一圈的时间不固定,实际频率会飘。严谨做法是把翻转动作挪到 1ms 定时器中断里,主循环只负责置一个beep_enable标志。仿真里听不出差别,实物上声音会明显更干净。
4.4 LCD1602 显示与按键设置
显示部分没什么技术含量,关键是排版。四个轮子分两行,每行两个,左边前轮右边后轮,中间留个空格。
/* press 单位 0.1kPa;1 bar = 100kPa = 1000 个计数 所以 bar 的整数位是 press/1000,小数位是 press/100%10 */ void LCD_ShowPress(unsigned char pos, unsigned int press) { LCD_ShowChar(pos + 0, '0' + press / 1000 % 10); LCD_ShowChar(pos + 1, '.'); LCD_ShowChar(pos + 2, '0' + press / 100 % 10); LCD_ShowChar(pos + 3, 'B'); }单位换算这里有个小陷阱:0.1kPa 的计数单位下,1 bar 正好是 1000 个计数,所以取整数位是除以 1000,取第一位小数是除以 100 再模 10。搞混了显示出来就是「2.25B」这种一看就不对的数。
按键用状态机消抖,不用延时:
/* K1 设置 K2 加 K3 减 K4 切换轮位,低电平有效 */ void Key_Scan(void) { static unsigned char last = 0xFF, cnt = 0; unsigned char cur = (K1 << 0) | (K2 << 1) | (K3 << 2) | (K4 << 3); if (cur == last) { if (cnt < 10) cnt++; } else { cnt = 0; last = cur; } if (cnt == 3) { /* 连续 3 次读到同一状态才认 */ if (!(cur & 0x01)) Key_SetMode(); if (!(cur & 0x02)) Key_Inc(); if (!(cur & 0x04)) Key_Dec(); if (!(cur & 0x08)) Key_NextWheel(); } }cnt == 3这个写法保证了「一次按下只触发一次」。cnt 会继续涨到 10,== 3这个条件之后就再不成立,直到松开按键把 cnt 清零。用>= 3就会变成按住不放疯狂连加,设个阈值一路飙到底。
5. 避坑与排查:从仿真到实物最容易翻车的五条记录
5.1 编译与仿真阶段的三条
现象一:Keil 编译 0 错误,Proteus 里跑起来 LCD 只有第一行黑块,程序像没跑。
原因是 hex 没更新。Proteus 的 Program File 指向的路径是对的,但它在启动仿真时已经把这个 hex 读进内存了,Keil 重新编译覆盖
本文还有配套的精品资源,点击获取