简介:本资源是一套面向单片机初学者与毕业设计学生的温度电压双参数检测系统完整开发包,解决低温环境(低至-50℃)下高精度数据采集、AD转换与可视化显示的实际工程问题。资源共49个文件,涵盖Keil工程源码(.c/.a51/.uvproj等)、Proteus 7.5/8.10仿真文件(.dsn/.pdsprj)、LCD12864与传感器驱动说明(PDF/DOC)、硬件选型手册(ZIP器件库)、操作视频(MP4)及答辩指导文档(含常见问题与PPT模板),压缩包大小为7.47MB。已有46人学习下载,内容组织清晰:按“设计说明→源程序→仿真→技术手册”四级目录展开,配套视频教程与必读使用指南显著降低上手门槛。读者可直接导入Keil与Proteus运行调试,复现DS1621测温(I²C通信)、MAX1241电压采样(12位精度)及12864中文界面动态刷新全过程,是单片机课程设计、电子类毕设及嵌入式入门实践的高复用性参考方案。
1. 这个项目到底解决什么问题:先想清楚再动手
前段时间接了个小需求,要用51单片机做一个温度电压检测装置,显示用12864,最关键的一条是温度要能测零下。一开始我的第一反应和大多数人一样——温度传感器用DS18B20,ADC用PCF8591,这两颗芯片便宜、教程多、网上代码一抓一大把。但真正梳理需求后我发现,这个方案对"可零下"这个要求其实存在隐患。
先说DS18B20。它当然支持负温度测量,测温范围-55℃到+125℃,12位分辨率下精度能到0.0625℃,看起来完全够用。但它是单总线协议,时序要求很高,对延时精度特别敏感,尤其当系统里还有其他外设要跑的时候,单总线的时序很容易被中断干扰。更麻烦的是,DS18B20在Proteus里仿真时偶尔会出现ROM搜索不稳定、读出来全是0xFFFFFFFF之类的问题,排查起来很费劲。所以在这类"检测+显示+仿真"的项目里,我反而更倾向于用I2C接口的DS1621。I2C有标准时序,不依赖精确延时,和51单片机的GPIO配合很宽松,Proteus仿真兼容性也更好。
再说电压采集。PCF8591是8位ADC,分辨率只有256级,如果测0-5V电压,每一步代表约19.5mV,显示出来会有明显跳变。MAX1241是12位ADC,分辨率4096级,0-5V下每步约1.22mV,精度差了16倍。MAX1241走的是SPI接口,恰好和DS1621的I2C分开,两条总线互不干扰,从软件架构上说也更清晰。
所以最终方案定为:AT89C51(或AT89C52)作为主控,DS1621负责温度采集,MAX1241负责电压采集,12864液晶(ST7920控制器,带中文字库)做显示。这套组合的核心思路是:温度用I2C总线,电压用SPI总线,显示用并行总线,三条通道完全独立,程序里无论是初始化还是数据刷新都互不牵连,调试起来非常直接。
这篇内容适合正在做单片机课程设计、毕业设计,或者想快速搭一套多通道检测系统的朋友参考。会把这套方案的硬件连接、驱动代码、Proteus仿真步骤一次说透,也会把我在实际调试中踩过的坑一并列出来。
2. 硬件电路设计:三条总线的分工与接口细节
2.1 51最小系统与资源规划
主控我选了AT89C51,12MHz晶振。RST引脚接经典的10uF电容加10k电阻复位电路,P0口必须接上拉电阻(10k排阻),这个不用多说。关键是把IO口规划清楚:
- P3.6 / P3.7 接DS1621的SCL / SDA,这个是硬件I2C的常规位置,方便以后用寄存器模拟或者直接升级成带硬件I2C的STC单片机。
- P1.0 - P1.3 接MAX1241的CS / SCLK / DIN / DOUT,纯GPIO模拟SPI。
- P2.0 - P2.7 接12864的DB0-DB7数据口,P3.4 / P3.5 接RS和E,RW直接接地(少一根控制线)。
这套分配有个好处:P1口完整留给SPI,P2口完整留给LCD数据,P3口的上下半区分别给I2C和LCD控制,没有任何引脚冲突,写代码的时候也容易记忆。
2.2 DS1621的I2C接口与地址设置
DS1621是Maxim(现在的ADI)出的数字温度传感器,I2C接口,测量范围-55℃到+125℃,分辨率和DS18B20一样可以配置成9到12位,12位模式下温度分辨率0.0625℃。和DS18B20最核心的区别是:DS1621不需要精确的单总线时序,I2C的SCL时钟只要在100kHz-400kHz以内都能正常工作,对GPIO模拟的要求低很多,这在Proteus仿真里优势特别明显。
接线很简单:VCC、GND、SCL、SDA,另外有A0、A1、A2三个地址引脚。这三个引脚接GND时I2C从机地址是0x48(7位地址),写地址是0x90,读地址是0x91。如果总线上还要挂别的I2C设备,可以改这三个引脚的接法来避开地址冲突。SDA和SCL必须各接一个4.7k上拉电阻到VCC,这个电阻不接的话I2C通信完全跑不起来,Proteus仿真也一样。DS1621的地址判断在程序里就是这一行:
#define DS1621_WRITE 0x90 // 0x48 << 1 #define DS1621_READ 0x91 // 0x48 << 1 | 1DS1621上电默认工作在连续转换模式,也就是说上电后它会自动周期性地进行温度转换,不需要额外写启动命令。但为了程序逻辑明确,我在初始化时还是写了一次配置寄存器,把工作模式设为连续转换、输出极性为高、12位分辨率,然后再发启动转换命令。
2.3 MAX1241的SPI接口与电压输入范围
MAX1241是12位单通道ADC,SPI接口,最大采样率约73kSPS,供电范围2.7V-5.25V,直接用5V供电没问题。它内部有一个2.5V基准电压,所以VREF引脚不需要接外部参考,只要在VREF引脚对地接一个0.1uF去耦电容,它自己就能稳定输出基准。模拟输入电压范围是0-2.5V。
这里有个特别需要注意的点:如果被测电压范围是0-5V,不能直接把5V接到MAX1241的AIN引脚,否则超量程会损坏芯片。需要在输入前加分压电阻,把0-5V映射到0-2.5V。比如用两个10k电阻分压,实际电压就是ADC读数乘以2。10k这个阻值的选择逻辑是:不能太大,否则信号源内阻会影响采样精度;也不能太小,否则分压电阻本身会消耗被测电路的电流。10k对绝大多数信号源来说都是安全值。
MAX1241的引脚分配:SCLK接P1.1,DIN接P1.2,DOUT接P1.3,CS接P1.0。VDD和VREF各接一个0.1uF电容到地,电容要尽量靠近芯片引脚,这个在实物布局上很关键,在Proteus仿真中影响不大但还是要养成规范习惯。
2.4 12864液晶的ST7920接法
12864液晶有很多种控制器,最常见的是ST7920(带中文字库)和KS0108(不带字库)。建议用ST7920版本,好处是内置中文字库,显示"温度""电压"这些汉字时直接写入GB2312内码就能显示,完全不用自己做字模。而KS0108需要自己提取和存储字模,程序体积大一大截,对51的片内Flash是个考验。
ST7920支持并行和串行两种模式,由PSB引脚决定。把PSB接VCC选并行模式,这样数据线用8根DB0-DB7,控制线只要RS和E两根,R/W接地固定为写模式。CPU读液晶状态的操作就不做了,每次操作前用延时代替。接线表整理如下:
| 12864引脚 | 51引脚 | 说明 |
|---|---|---|
| DB0-DB7 | P2.0-P2.7 | 8位数据总线 |
| RS | P3.4 | 0=指令 1=数据 |
| RW | GND | 固定写模式 |
| E | P3.5 | 使能信号,下降沿锁存 |
| PSB | VCC | 并行模式 |
| V0 | 接电位器 | 调节对比度,仿真中可接地 |
12864的供电是5V,背光A脚经电阻接5V,K脚接地。V0是液晶对比度调节脚,实物中接一个10k电位器,调节范围大概在0-1.5V之间,Proteus仿真里直接接地就能显示清楚。
3. 软件程序设计:三路驱动完整代码思路
3.1 主程序流程:初始化、轮询刷新、数据换算
整个软件结构非常清晰,主循环就是一个"读一遍、换算一遍、显示一遍"的循环,用定时器0做1秒定时,每1秒刷新一次显示。刷新太快反而不好,12864的清屏和写数据操作本身耗时,刷新过于频繁还会造成视觉闪烁,而且MAX1241和DS1621的读数也不会有秒级以下的有效变化。
主程序的逻辑大致是这样:
void main(void) { clk_init(); // 定时器初始化,1秒中断 lcd_init(); // 12864液晶初始化 ds1621_init(); // DS1621配置,启动连续转换 adc_init(); // MAX1241初始化(这里主要是GPIO配置) while(1) { if(flag_1s) // 1秒定时标志 { flag_1s = 0; // 读取DS1621温度 temp_raw = ds1621_read_temp(); // 补码转实际温度,12位分辨率下乘0.0625 temp_value = temp_raw * 0.0625; // 读取MAX1241电压 adc_raw = max1241_read(); // 12位ADC,参考电压2.5V,乘2补偿分压 volt_value = (adc_raw * 2.5 * 2) / 4096.0; // 刷新显示 lcd_display_temp(temp_value); lcd_display_volt(volt_value); } } }注意temp_raw是int类型的16位有符号数据,DS1621读出来是两个字节,高字节在前。如果温度是负的,高字节最高位为1,直接把它当成有符号int来乘0.0625,C语言的整数提升会自动把负数转换成正确的浮点负数。但是有坑:如果把两个字节硬拼成unsigned int再转float,负温度就会变成很大的正值,显示出的温度会极其离谱。
3.2 DS1621驱动:I2C起始、读写与温度换算
51没有硬件I2C,全靠GPIO模拟。DS1621的I2C时序是标准的:起始条件、从机地址、命令字节、重复起始、读取数据、停止条件。下面是核心驱动函数。
先看I2C的底层操作:
void i2c_start(void) { SDA = 1; SCL = 1; _nop_(); SDA = 0; _nop_(); SCL = 0; } void i2c_stop(void) { SCL = 0; SDA = 0; _nop_(); SCL = 1; _nop_(); SDA = 1; } void i2c_write_byte(unsigned char dat) { unsigned char i; for(i = 0; i < 8; i++) { if(dat & 0x80) SDA = 1; else SDA = 0; dat <<= 1; SCL = 1; _nop_(); SCL = 0; _nop_(); } // 释放SDA总线,等待从机应答 SDA = 1; SCL = 1; _nop_(); SCL = 0; _nop_(); } unsigned char i2c_read_byte(void) { unsigned char i, dat = 0; SDA = 1; // 释放总线,让从机驱动SDA for(i = 0; i < 8; i++) { SCL = 1; _nop_(); dat <<= 1; if(SDA) dat |= 0x01; SCL = 0; _nop_(); } return dat; }读取DS1621温度寄存器的完整流程:
int ds1621_read_temp(void) { unsigned char hi, lo; int raw; i2c_start(); i2c_write_byte(DS1621_WRITE); // 写地址 i2c_write_byte(0xAA); // 读温度寄存器命令 i2c_start(); // 重复起始 i2c_write_byte(DS1621_READ); // 读地址 hi = i2c_read_byte(); lo = i2c_read_byte(); // 最后一个字节后不发送应答 i2c_stop(); raw = (int)(hi << 8 | lo); return raw; }这里有个细节必须要解释:读最后一个字节之后要回非应答(NAK),表示主机不再需要更多数据,然后发停止条件。有些代码在读完最后一个字节后还发ACK,从机就会继续发下一字节,导致总线状态混乱。我在第一次调试时就犯了这个错,数据读出来是乱的,排查了半天才发现是多发了一个ACK。
温度换算逻辑是:DS1621在12位分辨率下,低字节只有高4位有效(bit3-bit0恒为0)。原本的16位温度寄存器值乘以0.0625就是实际温度。负温度时是补码表示,比如-20℃,寄存器里存的是0xFF60(即-160),-160 * 0.0625 = -10.0,换算正确。这里顺便验证一下:0xFF60换算成十进制,最高位为1说明是负数,先取反加一得到0x00A0即160,所以原值是-160,乘以0.0625等于-10℃,没毛病。
3.3 MAX1241驱动:模拟SPI时最容易出错的三个细节
MAX1241虽然是一个SPI器件,但它的时序和标准SPI略有不同,需要分两个阶段:第一阶段主机通过DIN发送8位配置字(bit7=1启动转换,bit6=0选择单极性输入),第二阶段主机在SCLK作用下从DOUT读取12位转换结果,MSB在前。
unsigned int max1241_read(void) { unsigned char i; unsigned char control = 0x80; // b7=1启动转换, b6=0单极性 unsigned int val = 0; ADC_CS = 0; _nop_(); // 发送8位配置字 for(i = 0; i < 8; i++) { if(control & 0x80) ADC_DIN = 1; else ADC_DIN = 0; control <<= 1; ADC_SCLK = 1; _nop_(); ADC_SCLK = 0; _nop_(); } // 读取12位转换结果 for(i = 0; i < 12; i++) { ADC_SCLK = 1; _nop_(); val <<= 1; if(ADC_DOUT) val |= 0x01; ADC_SCLK = 0; _nop_(); } ADC_CS = 1; return val; }这里要看懂一个时序问题:发送配置字时,DIN在SCLK上升沿被锁存,所以要先设置DIN数据位,再拉高SCLK。读取数据时,MAX1241是在SCLK的下降沿更新DOUT数据,那么主机可以在SCLK为高电平期间直接读DOUT,也就是上面代码里先置SCLK=1,再读DOUT,然后再拉低。这两个方向搞反了就会得到全0或者错位的数据。
另外两个容易踩的坑:第一,CS拉低后至少要有几微秒的延时等待转换启动,虽然MAX1241的tCSL时序很紧凑,但在51这种慢速单片机上,加_nop_()更稳妥;第二,控制字必须是0x80,不要写成0xC0或别的值。0xC0里bit6为1选择的是双极性差分输入模式,对于单端电压检测来说会得到完全错误的读数。
电压值换算公式:
// VREF内部2.5V,分压2倍,所以满量程对应5.0V float volt = (adc_raw * 5.0) / 4096.0;很多教程写成adc_raw * 2.5 / 4096.0,这是没考虑分压电阻的。我没有用分压电阻的时候,MAX1241的输入范围是0-2.5V,显示出来最大值2.5V,这倒没错;但我测的是0-5V信号,就要在外部加两个10k电阻分压,所以最终满量程对应5V,计算时乘5.0。
3.4 12864驱动:ST7920初始化序列与中文显示
ST7920的初始化顺序是一个固定套路,网上好多版本细节不一致,我实测下来最稳妥的是这个序列:
void lcd_init(void) { delay_ms(50); // 上电等待,ST7920内部复位 lcd_write_cmd(0x30); // 基本指令集,8位模式 delay_ms(5); lcd_write_cmd(0x30); delay_ms(5); lcd_write_cmd(0x0C); // 显示开,光标关 delay_ms(2); lcd_write_cmd(0x01); // 清屏 delay_ms(10); lcd_write_cmd(0x06); // 地址自动加1 delay_ms(2); }注意0x30要连续写两次,第一次是唤醒ST7920,第二次才是真正的功能设置。有人偷懒只写一次,实测在某些屏上会偶发初始化不成功,显示全黑或者全白。这里不省那5ms的延时。
写命令和写数据的底层函数:
void lcd_write_cmd(unsigned char cmd) { RS = 0; LCD_DATA = cmd; E = 1; _nop_(); E = 0; delay_us(50); } void lcd_write_data(unsigned char dat) { RS = 1; LCD_DATA = dat; E = 1; _nop_(); E = 0; delay_us(50); }ST7920的中文显示区域和英文显示区域地址不同。在中文模式下,每行最多8个汉字,四行的起始地址分别是0x80、0x90、0x88、0x98。要注意第二行是0x90而不是0x88,这个地址表很多初学者会搞错。
显示"温度: xx.x C"的完整函数可以这样写:
void lcd_display_temp(float temp) { unsigned char buf[8]; lcd_write_cmd(0x80); // 第一行起始地址 lcd_display_str("温 度"); // 显示中文 lcd_write_cmd(0x87); // 第一行第7个字符位置 // 把float转成字符串 sprintf(buf, "%5.1f", temp); lcd_display_str(buf); }sprintf在51上使用需要包含stdio.h,而且会比较大,但12MHz的51跑这个还是可以的,只是要注意Keil里必须勾选"Use MicroLIB"才能正常使用浮点格式化功能,否则链接时会报错或者程序体积爆炸。如果不想用sprintf,也可以用整数拆分的方式手动转换,后面避坑部分单独说。
4. Proteus仿真调试:从搭电路到跑通全流程
4.1 仿真环境搭建与元件清单
Proteus的仿真步骤分为:新建工程、放置元件、连线、加载HEX文件、运行。元件清单如下,直接搜索这些名字就能找到:
| 元件 | 搜索关键字 | 备注 |
|---|---|---|
| 主控 | AT89C51 | 也可用AT89C52 |
| 温度传感器 | DS1621 | I2C接口 |
| ADC | MAX1241 | SPI接口 |
| 液晶 | LGM12641BS1R | ST7920控制器12864,带中文字库 |
| 分压电阻 | RES | 两个10k |
| 上拉电阻 | RESPACK-8 | P0口排阻,4.7k或10k |
Proteus里的12864模型选LGM12641BS1R,这个型号是ST7920控制器,支持中文显示。在摆放元件时注意:DS1621的SDA和SCL要分别接上拉电阻到VCC,MAX1241的VREF引脚接0.1uF电容到地,AIN引脚接两个10k电阻的分压点。
加载程序:在AT89C51的属性里双击,在Program File一栏选择Keil编译生成的HEX文件,然后点运行。如果看不到效果,先检查有没有加载HEX文件——这是Proteus仿真里出现概率最高的问题,没有之一。
4.2 负温度与电压信号的模拟验证方法
温度验证:Proteus里的DS1621可以直接在元件属性里修改温度值。双击DS1621,在属性对话框里有一个Temperature(范围-55到125)选项,把它改成-20,运行后12864上应该显示-20.0℃。修改后不必重启仿真,继续运行几秒就会更新。
电压验证:在MAX1241的AIN输入引脚接一个电位器(POT-HG),中间抽头接AIN,电位器两端分别接5V和GND。仿真运行时拖动电位器改变输出电压,LCD上的电压读数应该跟着变化。最大到5.0V,最小到0.0V。
这里要注意:PROTEUS里模拟电压源如果直接接到MAX1241的AIN,而MAX1241的VREF是内部2.5V,输入超过2.5V时仿真可能不会报错但数据会饱和在4095。所以用5V供电时一定要加分压电阻。我见过很多人在仿真图里直接把5V电源接AIN,读数永远是4095,还以为是程序问题,其实是输入超量程了。
4.3 仿真中常见的排查思路
如果运行后12864完全不显示,优先查以下四项:第一,LGM12641BS1R的PSB接VCC没有,很多Proteus例程默认PSB接地(串行模式),但程序是按并行模式写的,自然没反应;第二,对比度调节脚V0是否接地,不接地可能全黑;第三,E引脚和RS引脚有没有接反;第四,AT89C51的RST和晶振是否接好。
如果DS1621温度读出来一直是FF或者0,重点抓I2C地址。检查A0-A2是不是全接地了,代码里0x90/0x91的地址就要对应。如果A2接了VCC,地址就变成0x92/0x93,代码不改就不通。
如果MAX1241读出来一直等于4095,除了超量程外,还要检查芯片上有没有其他功率源。有些Proteus版本里MAX1241的SHDN引脚如果悬空,ADC会一直处于关断状态。把SHDN引脚直接接VCC就能排除这个干扰。
5. 实测中遇到的坑与处理经验
5.1 DS1621的负温度显示显示乱码问题
我刚写完温度显示功能时,把int类型直接无符号处理,正温度显示完全正常,但把DS1621设成负温度后,LCD上显示了一个巨大的正数,比如-10℃变成了65526℃,一眼就知道是符号位被当成了数据位。
解决办法有两种:第一种,直接把读回来的(hi<<8|lo)强转成int,C语言会自动识别最高位符号,这个方法最简单;第二种,手动判断最高位,如果是1就先取反加一再加负号:
if(raw & 0x8000) { raw = ~raw + 1; temp = -(float)raw * 0.0625; } else { temp = (float)raw * 0.0625; }两种方法结果一致,但第一种对初学者来说容易在后续格式化输出时犯错。建议采用第二种,逻辑清晰,也好在LCD上拼字符串。
5.2 MAX1241的读数波动与去耦技巧
MAX1241在仿真中读数非常稳定,但在实物中会出现末位跳动的现象。我在调试中发现,跳动的主要原因是VREF引脚的去耦电容离芯片太远。MAX1241内部2.5V基准是通过一个开关电容网络产生的,每一次采样都会从VREF引脚抽取微小电荷,如果去耦电容不足或者离引脚太远,基准电压就会有纹波,反映到ADC结果上就是低位跳动。
解决方法是:VREF引脚对地接一个1uF的钽电容或陶瓷电容,放在尽可能靠近芯片的位置,同时VDD对地再接一个0.1uF电容。实测下来末位跳动从±3个字降到±1个字,效果立竿见影。
另外,MAX1241的AGND和DGND在芯片内部是分开的,Proteus仿真中不需要区分,但实物PCB上如果条件允许,模拟地和数字地要单点连接。
5.3 12864使用sprintf的坑与手动拼串方案
Keil C51里用sprintf(buf, "%5.1f", temp)会把程序体积撑大好几K,而且如果不勾选MicroLIB,链接时会有不可预料的错误。更稳妥的做法是手动拆数拼字符串:
void lcd_display_temp(float temp) { char buf[6]; int t; if(temp < 0) { temp = -temp; buf[0] = '-'; } else { buf[0] = ' '; } t = (int)(temp * 10); // 乘以10得到一位小数 buf[1] = t / 100 + '0'; buf[2] = (t / 10) % 10 + '0'; buf[3] = '.'; buf[4] = t % 10 + '0'; buf[5] = '\0'; lcd_display_str(buf); }这段代码要理解两点:第一,(int)(temp * 10)强制转int是四舍五入效果还是截断效果取决于编译器,Keil里默认是截断,如果你需要四舍五入就加0.5:(int)(temp * 10 + 0.5);第二,温度范围如果是-55到125℃,整数部分最多三位,所以buf[1]存的是百位,buf[2]是十位,如果温度只有几十度,百位会被字符'0'占位,显示成"025.3"这样的格式,可以把buf[1]判断一下,如果等于'0'就替换成空格。
5.4 定时器1秒刷新与按键扩展思路
在实际项目中,检测装置往往不只显示温度电压,还要加报警、按键切换显示。这套程序的主循环已经预留了定时器中断的框架,扩展起来很方便。在定时器0的中断服务函数里累加计数,到达20次(每次50ms)就置flag_1s,主循环检测到这个标志再刷新数据。这样做的好处是:DS1621的温度转换需要时间,MAX1241采样也有建立时间,用固定1秒节奏去读数据,每次读到的都是转换完成后的稳定值,规避了"上一秒还没转换完就读"的时序问题。
如果后续要加报警功能,可以在温度超过阈值时让蜂鸣器响,接一个NPN三极管驱动就行。按键切屏的话,用P3.2和P3.3接两个独立按键,在主循环里扫描,切换"只显温度""只显电压""双显"三种模式。整体框架不用动,只是在显示部分加判断。
6. 最后再分享两个小技巧
第一个是关于Proteus仿真速度。如果12864刷新太频繁,仿真会变得非常慢,因为Proteus对液晶的字符扫描做了逐点模拟,每次写显示数据都会消耗大量计算资源。把刷新率从1秒改成3秒甚至5秒,仿真流畅度会明显提升。对验证功能来说,5秒刷新完全够用,反正数据是实时更新的。
第二个是实际焊接时的排错顺序。如果实物上电后什么都没显示,不要急着怀疑程序,先用万用表量一下晶振两脚电压(正常应该在1.5V-2.5V之间),再量RST引脚电平(正常应该是低电平),最后确认P0口上拉电阻是否焊好。硬件顺序确认无误后再烧程序,这样可以少走很多弯路。这个项目我用这套顺序排查过三次,每次都能快速定位问题是在硬件还是软件。
对了,还有一点忘说了,MAX1241的SHDN引脚如果悬空,芯片默认是不工作的,实物和仿真都要把它接VCC才能正常转换。这个我差点忘记写在上面,算是悄悄藏了一个最容易被人忽略的细节。整套项目从设计到仿真跑通,大约需要两到三个晚上,大部分时间都花在I2C和SPI时序调试上,但只要把上面这几个关键点记牢,整个过程会很顺利。
本文还有配套的精品资源,点击获取