简介:本资源是一套完整的基于51单片机的4通道高精度电压测量系统开发资料,面向嵌入式初学者、课程设计学生及电子类实训教师,解决多路模拟信号采集、阈值报警与人机显示等典型工程问题。资源包共41个文件,涵盖原理图(SchDoc/PDF)、Keil工程(uvproj/uvopt/hex)、C语言源码(main.c/ADS1110.c/lcd1602.c)、头文件、编译中间文件(lst/obj)、流程图(bmp)、物料清单(xlsx)及Proteus仿真项目(pdsprj/pdsbak),全面支撑从硬件设计、软件编程到系统联调的全流程学习。压缩包仅577KB,结构清晰、模块解耦,含上下限电压报警(蜂鸣器驱动)、0–10V量程适配、LCD1602实时显示等核心功能实现。目前已有132人下载学习,配套代码注释详尽、硬件接口定义明确,特别适合单片机课程实践、毕业设计参考及ADS1110芯片入门应用。
1. 项目缘起:为什么用51单片机做4路电压测量?
最近在整理工作室的旧项目,翻出了一个基于STC89C52RC和ADS1110做的4路电压采集板。这玩意儿现在看有点“复古”,毕竟现在STM32、ESP32满天飞,谁还用51单片机啊?但恰恰是这种“复古”项目,最能体现一个嵌入式工程师的基本功。很多朋友,尤其是刚入行的学生,一上来就想搞FreeRTOS、搞LVGL,结果连个ADC都调不通,I2C时序都搞不明白。这个项目麻雀虽小,五脏俱全:从原理图设计、器件选型、程序流程图到代码实现,完整地走了一遍一个简单数据采集系统的开发流程。今天我就把这个项目的所有细节,包括当时踩过的坑、做的取舍,都掰开揉碎了讲清楚。无论你是想复现一个课程设计,还是想理解如何将一颗简单的ADC芯片用好,这篇文章都能给你提供一份可以直接“抄作业”的完整方案。
2. 核心器件选型与电路设计思路
做硬件项目,第一步永远是选型和画图。这一步走对了,后面写代码、调试才能事半功倍。
2.1 为什么是STC89C52RC和ADS1110?
先说单片机。选择STC89C52RC,理由很直接:经典、便宜、资料多。对于这种低速、低精度的数据采集任务,51内核完全够用。它的GPIO可以直接模拟I2C时序驱动ADS1110,内部定时器也足够处理采样间隔。虽然它没有硬件I2C,但软件模拟反而更灵活,便于我们理解I2C通信的本质。如果你手头有STC12、STC15或者AT89S52,也完全通用,程序几乎不用改。
再说ADC芯片。这是本项目的核心。市面上ADC芯片很多,比如PCF8591(8位,I2C)、ADS1115(16位,4通道,I2C)。我最终选择ADS1110,主要基于以下几点考虑:
- 精度与速度的平衡:ADS1110是16位分辨率,对于测量0-5V的电压,理论分辨率能达到5V / 65536 ≈ 0.076mV,这比单片机自带的10位ADC(约4.9mV)精度高两个数量级。它的采样率最高到240SPS(每秒采样次数),对于监测电池电压、电源纹波、传感器输出等变化不快的信号完全足够。
- 极简的外围电路:ADS1110内部集成了基准源、振荡器和PGA(可编程增益放大器)。这意味着你不需要外接精密基准电压芯片,也不需要复杂的滤波电路,基本上VCC、GND、I2C接口加上输入信号,芯片就能工作,极大简化了PCB设计和BOM成本。
- 单通道与多路复用:ADS1110本身是单通道ADC。要实现4路测量,就需要4颗ADS1110。这看起来是缺点,但实际上带来了一个巨大优势:4路可以同时采样。如果使用ADS1115这种多路复用的ADC,虽然只用一颗芯片,但4个通道是轮流采样的,存在时间差。在某些需要同步比较4路电压的应用中(比如平衡充),4颗独立的ADS1110反而是更好的选择。
- 成本与封装:ADS1110是SOT23-6的小封装,价格也相对低廉。在板卡面积有限或者需要分布式测量时,小封装优势明显。
所以,这个组合(51单片机 + 多颗独立高精度ADC)的核心设计思想就是:用经典、易用的主控,搭配“傻瓜式”但性能专一的外设,通过简单的总线(I2C)扩展,实现一个高精度、可同步的多通道数据采集系统。
2.2 原理图设计详解与关键细节
原理图是硬件设计的蓝图。这里我直接给出核心部分的电路设计,并解释每一个元件的作用。
1. 单片机最小系统:这是51单片机工作的基础,必须稳定可靠。
- 电源:VCC接5V。我在VCC和GND之间放置了一个100nF的瓷片电容(C1)和一个10uF的电解电容(C2)。100nF的电容负责滤除高频噪声,紧靠单片机电源引脚放置;10uF的电容负责缓冲低频波动。这是保证数字电路稳定的标准做法。
- 复位电路:采用经典的RC复位(R1=10kΩ, C3=10uF)。当上电时,电容充电,RST引脚保持一段时间高电平,完成复位。手动复位按钮S1并联在电容两端,按下时电容放电,RST变高。
- 时钟电路:使用11.0592MHz的晶振(Y1)和两个22pF的负载电容(C4, C5)。这个频率非常经典,能让串口波特率非常准确,方便调试时通过串口打印数据。
- EA/VPP引脚:接VCC,表示使用片内程序存储器。
2. ADS1110接口电路:这是设计的重中之重。4颗ADS1110(U1-U4)的电路完全一致。
- 电源去耦:每个ADS1110的VDD引脚都必须就近放置一个100nF的去耦电容(如C6-C9)。这是必须的,否则ADC的读数会跳动得非常厉害,因为模拟电路对电源噪声极其敏感。
- 信号输入:AIN0和AIN1是差分输入引脚。本项目测量对地的单端电压,因此将AIN1接地(GND),待测电压Vin接入AIN0。在AIN0引脚前,我串联了一个100Ω的电阻(R2)并并联了一个100nF的电容(C10)到地,构成一个简单的RC低通滤波器,可以滤除输入信号中的高频干扰。对于变化缓慢的直流或低频信号,这个滤波器非常有效。
- I2C总线:所有ADS1110的SCL和SDA引脚分别并联在一起,然后通过4.7kΩ的上拉电阻(R3, R4)接到单片机的5V电源。这是I2C总线的标准接法。特别注意:每颗ADS1110都有一个唯一的I2C地址,通过ADDR引脚配置。ADS1110的ADDR引脚可以接GND、VDD、SDA或SCL,对应不同的地址。为了简化,我将4颗芯片的ADDR都接地,这样它们的默认地址都是0x48。那如何区分它们呢?答案是:无法通过地址区分,只能通过分时复用总线。也就是说,同一时间,总线上只能有一颗ADS1110的地址是0x48。我们需要用单片机的GPIO来控制每颗ADS1110的电源VDD或使能端。但ADS1110没有专门的使能引脚,所以更常见的做法是:控制其I2C总线的上拉电阻。具体电路设计见下文。
- 地址冲突的解决方案:我采用了一个非常实用且成本低廉的方案——用单片机的IO口控制I2C总线的通断。具体来说,我为每一路ADS1110的SDA和SCL线都设置了一对“门控”MOS管(如N沟道MOSFET 2N7002)。单片机的一个GPIO(如P1.0)控制MOS管的栅极。当P1.0输出高电平时,MOS管导通,对应的ADS1110的SDA/SCL线被连接到主I2C总线上;当输出低电平时,MOS管关闭,该ADS1110就从总线上“隔离”开来。这样,4颗ADS1110可以永远配置为相同的地址(0x48),我们通过程序依次打开其中一路的“门”,与其通信,读取数据后再关闭,然后打开下一路的“门”。这种方法比用模拟开关芯片(如74HC4052)更简单,成本也更低。
3. 电平转换与通信接口(可选但推荐):单片机是5V系统,ADS1110的工作电压是2.7V-5.5V,所以可以直接连接。但如果你未来想连接3.3V的设备(如电脑的USB转串口芯片CP2102、ESP8266等),就需要考虑电平转换。我预留了一个CH340G USB转TTL串口芯片的接口,它可以将5V TTL电平转换为USB信号,同时它的TX/RX引脚是3.3V电平。因此,在单片机的RXD/TXD和CH340G的TXD/RXD之间,我加了两个1kΩ的电阻做简单限流,实践证明在低速通信下(9600波特率)是可行的。更稳妥的做法是使用电平转换芯片(如TXS0102)或电阻分压。
2.3 物料清单(BOM)与备选方案
一份清晰的BOM表是采购和焊接的依据。以下是核心物料清单,括号内为可选或备选型号。
| 位号 | 名称 | 型号/参数 | 数量 | 备注 |
|---|---|---|---|---|
| U0 | 单片机 | STC89C52RC (DIP-40) | 1 | 可用STC12C5A60S2、AT89S52替代 |
| U1-U4 | ADC芯片 | ADS1110A0IDBVR (SOT23-6) | 4 | 注意后缀A0,地址为GND配置 |
| Y1 | 晶振 | 11.0592MHz | 1 | |
| C1, C6-C9等 | 瓷片电容 | 100nF (104) | 9+ | 单片机电源去耦1个,每个ADS1110电源去耦1个,输入滤波4个 |
| C2 | 电解电容 | 10uF/16V | 1 | 电源储能 |
| C3 | 电解电容 | 10uF/16V | 1 | 复位电路 |
| C4, C5 | 瓷片电容 | 22pF | 2 | 晶振负载电容 |
| R1 | 电阻 | 10kΩ | 1 | 复位电路上拉电阻 |
| R3, R4 | 电阻 | 4.7kΩ | 2 | I2C总线上拉电阻 |
| R2, R5, R8, R11 | 电阻 | 100Ω | 4 | 输入限流/滤波电阻 |
| R6, R7, R9, R10等 | 电阻 | 1kΩ | 8 | MOS管栅极限流电阻(每路2个) |
| Q1-Q8 | MOS管 | 2N7002 (SOT-23) | 8 | N沟道,用于I2C总线门控,每路需要2个(SDA和SCL) |
| S1 | 轻触开关 | 6x6x5mm | 1 | 复位按钮 |
| USB转串口 | CH340G模块 | 1 | 用于程序下载和调试输出 | |
| 电源接口 | DC-005 / Micro USB | 1 | 提供5V输入 |
注意:MOS管2N7002的栅极阈值电压较低(约2.1V),用5V单片机IO口驱动完全没问题。焊接时注意SOT-23封装的方向。如果买不到2N7002,也可以用其他通用的N沟道小信号MOS管替代,如SI2302。
3. 程序设计流程图与核心逻辑剖析
画流程图不是走过场,而是把复杂的程序逻辑视觉化,帮助自己理清思路,也方便后续调试和他人理解。这个项目的程序流程图可以分为三个层次:主程序流程、单次测量流程、I2C通信流程。
3.1 主程序流程图与状态机设计
主程序的核心是一个简单的顺序执行+定时循环结构,但用状态机的思想来理解会更清晰。
初始化:上电后,首先初始化单片机硬件。包括:
- 设置定时器(如Timer0)为16位自动重装模式,用于产生精确的延时和采样间隔。
- 初始化串口,波特率设为9600,用于向电脑发送数据。
- 初始化用于控制4路I2C总线“门控”MOS管的GPIO(如P1.0-P1.3),并全部设置为低电平(关闭所有通路)。
- 初始化一些状态变量和数组,用于存储4路电压值。
主循环:进入一个无限的
while(1)循环。循环体内不是一股脑地执行,而是由定时器中断来驱动“采样”这个事件。- 主循环大部分时间在“空闲”状态,可以执行一些低优先级的任务,比如检查是否有串口命令(如修改采样率)。
- 定时器中断(比如每100ms一次)发生时,会设置一个标志位,如
sampling_flag = 1。
采样状态:在主循环中不断检查
sampling_flag。一旦发现其被置位,立即清除该标志,并进入“采样状态”。- 在采样状态下,程序会依次对4路电压进行测量。注意,是“依次”,不是“同时”,但因为我们的“门控”机制,在逻辑上可以认为在打开某一路MOS管的瞬间,该路ADC开始工作,其他路与总线隔离,互不干扰。
- 测量完一路,将结果存入对应的数组,然后立即关闭该路的“门”,打开下一路的“门”,进行下一路测量。这个切换速度很快(微秒级),对于慢变信号,可以近似认为是同步采样。
- 4路全部测量完毕后,退出“采样状态”。
数据处理与发送状态:采样结束后,程序进入“数据处理状态”。
- 将ADC读取到的原始数字量(一个16位有符号整数,因为ADS1110是差分输入)根据PGA增益和基准电压,换算成实际的电压值(单位:伏特)。
- 可以选择进行简单的软件滤波,比如取最近几次测量的平均值。
- 最后,将4路电压值通过串口,按照一定的格式(例如
CH1:3.301V, CH2:2.485V, CH3:4.998V, CH4:0.123V)发送给上位机(电脑),或者驱动本地显示(如LCD1602)。
返回空闲:数据处理发送完毕后,程序回到主循环的起点,等待下一个定时中断,开启新一轮的采样。
这个流程的关键在于通过“门控”GPIO和顺序查询,用一套I2C总线资源分时复用了4颗地址相同的ADC芯片,逻辑清晰,资源占用少。
3.2 单路ADS1110测量子流程详解
这是程序最核心的部分,它封装了与一颗ADS1110通信并读取数据的全过程。流程图如下:
- 开启通道:单片机将对应通道的“门控”GPIO置高(例如测量第1路,则置高P1.0),使对应的两个MOS管导通,将该路ADS1110接入I2C总线。
- 发送配置字节:通过I2C向ADS1110(地址0x48)写入配置寄存器。这是关键一步!ADS1110的配置寄存器(8位)决定了它的工作模式。
- Bit7-6 (ST/DR1/DR0): 数据速率选择。我们设为
11,对应240SPS,这是最高速率。 - Bit5-4 (PGA1/PGA0): 可编程增益放大器设置。这决定了输入电压的范围。例如:
00: PGA=1, 输入范围 ±4.096V01: PGA=2, 输入范围 ±2.048V10: PGA=4, 输入范围 ±1.024V11: PGA=8, 输入范围 ±0.512V这里有个大坑:ADS1110的基准电压是内部固定的2.048V。当PGA=1时,放大倍数为1,它所能测量的差分输入电压最大范围是 -2.048V 到 +2.048V。但数据手册上写的“±4.096V”是指在PGA=1时,AIN0和AIN1之间的电压差范围。因为我们把AIN1接地了,所以AIN0对地的电压(单端输入)范围实际上是 0V 到 2.048V(当差分输入为正时)。如果你想测量0-5V的电压,必须使用PGA=1,并在外部用电阻分压,将0-5V分压到0-2.048V以内。否则,输入超过2.048V,ADC输出会饱和(达到最大值32767)。在本项目中,我假设测量电压在0-2.048V内,因此配置PGA=1 (00)。
- Bit3-2 (Reserved): 保留位,必须写
00。 - Bit1-0 (SC): 转换模式控制。
00代表单次转换模式(上电后需要触发一次转换);11代表连续转换模式。我们使用单次转换模式(00),更省电,控制也更直接。 - 因此,我们的配置字节可以定为
0x8C(二进制1000 1100),即单次转换、PGA=1、240SPS。
- Bit7-6 (ST/DR1/DR0): 数据速率选择。我们设为
- 等待转换完成:写入配置字节后,ADS1110会立即开始一次转换。转换时间取决于数据速率,240SPS下约4.17ms。程序需要延时至少这么长时间。更专业的做法是反复读取配置寄存器的最高位(Bit15),这一位在转换期间为
0,转换完成后为1。通过查询这一位可以精确知道转换何时结束,避免盲目延时。 - 读取转换结果:转换完成后,通过I2C读取两个字节的数据(MSB先送)。这两个字节组成一个16位有符号整数。
- 关闭通道:读取数据后,立即将“门控”GPIO置低,断开该路ADS1110与总线的连接,为下一路测量做准备。
- 数据换算:将读取到的16位有符号整数转换为电压值。公式为:
电压(V) = (读取值 / 32767) * (PGA对应的满量程电压)由于是单端输入(AIN1=0V),且PGA=1,满量程电压为2.048V。公式简化为:电压(V) = (读取值 / 32767) * 2.048如果读取值是正数,表示AIN0 > AIN1;如果是负数(在单端接法下很少见,除非有负压或干扰),则表示AIN0 < AIN1。
3.3 I2C软件模拟流程的精髓
51单片机没有硬件I2C,我们必须用GPIO口模拟。模拟I2C的代码网上很多,但真正稳定可靠需要关注以下几点:
- 时序严格性:I2C的起始条件(SDA在SCL高电平时由高变低)、停止条件(SDA在SCL高电平时由低变高)、数据有效性(SDA在SCL高电平期间必须保持稳定)的时序必须严格遵守。每个上升沿、下降沿后要加适当的延时(
_nop_()空指令),这个延时时间决定了I2C的速率。对于100kHz的标准模式,一个位周期是10us,半周期是5us。我们的单片机主频11.0592MHz,一个_nop_()大约1us,所以延时5个_nop_()左右。 - 应答(ACK)处理:主机每发送完一个字节(8位),必须释放SDA线(设为输入模式),并产生一个额外的时钟脉冲(SCL高电平),在这个脉冲期间,从机(ADS1110)需要将SDA拉低作为应答。主机程序必须去读取SDA线的状态,检查是否为低电平,以判断从机是否正常应答。同样,主机读取完一个字节后,需要在下一个时钟脉冲发出应答(或非应答)信号。
- 端口模式切换:这是软件I2C最容易出错的地方。51单片机的GPIO在作为输出(写
0或1)和输入(读取引脚状态)时,需要对端口进行不同的操作。对于传统的51单片机(如STC89C52),读取引脚前需要先向端口锁存器写1(即Px = 0xFF;),这样才能正确读取外部电平。而对于一些新型的51单片机(如STC15系列),IO口有专门的模式寄存器需要配置。在代码中,必须封装好SDA_Out()和SDA_In()这样的函数,来正确处理方向切换。 - 总线异常恢复:在实际调试中,I2C总线可能因为干扰或从机异常而卡住(SCL被拉低)。一个健壮的I2C驱动应该包含总线恢复函数:连续发送9个或更多时钟脉冲,直到SDA被释放,然后再发送一个停止条件。
4. 源代码逐行解析与关键函数实现
理论讲完了,是时候上代码了。这里我将核心函数拆解出来,并附上详细的注释。完整工程可以在文末的链接找到。
4.1 宏定义与全局变量
#include <reg52.h> // 包含51单片机寄存器定义头文件 #include <intrins.h> // 包含_nop_()函数 // 硬件引脚定义 sbit SCL = P2^0; // I2C时钟线 sbit SDA = P2^1; // I2C数据线 sbit CH1_EN = P1^0; // 通道1使能(控制MOS管) sbit CH2_EN = P1^1; // 通道2使能 sbit CH3_EN = P1^2; // 通道3使能 sbit CH4_EN = P1^3; // 通道4使能 // ADS1110地址及配置字 #define ADS1110_ADDR_W 0x90 // 写地址 (0x48 << 1) | 0 #define ADS1110_ADDR_R 0x91 // 读地址 (0x48 << 1) | 1 #define ADS1110_CONFIG 0x8C // 配置字:单次转换,PGA=1, 240SPS // 全局变量 unsigned int adc_value[4]; // 存储4路ADC原始值 float voltage[4]; // 存储4路换算后的电压值 bit sampling_flag = 0; // 采样标志位注意:I2C的7位地址是0x48,但实际传输时,需要左移一位,并在最低位加上读写位(0写,1读)。所以写地址是
0x48<<1 = 0x90,读地址是0x91。这是一个常见的易错点。
4.2 软件I2C底层驱动
这是项目的基石,务必稳定。
// 微秒级延时函数,用于I2C时序 void I2C_Delay(unsigned char t) { while(t--); } // I2C起始信号:SCL高电平期间,SDA产生一个下降沿 void I2C_Start(void) { SDA = 1; SCL = 1; I2C_Delay(5); // 延时约5us,建立时间 SDA = 0; I2C_Delay(5); SCL = 0; // 钳住总线,准备发送数据 } // I2C停止信号:SCL高电平期间,SDA产生一个上升沿 void I2C_Stop(void) { SDA = 0; SCL = 1; I2C_Delay(5); SDA = 1; I2C_Delay(5); } // I2C发送一个字节,并返回从机应答位 bit I2C_SendByte(unsigned char dat) { unsigned char i; bit ack; for(i=0; i<8; i++) { SDA = (dat & 0x80) ? 1 : 0; // 取最高位 dat <<= 1; SCL = 1; I2C_Delay(5); SCL = 0; I2C_Delay(5); } // 发送完8位后,主机释放SDA,读取从机应答 SDA = 1; // 51单片机,先写1才能正确读取引脚 SCL = 1; I2C_Delay(5); ack = SDA; // 读取应答位,0为应答,1为非应答 SCL = 0; I2C_Delay(5); return ack; } // I2C读取一个字节,并发送应答或非应答信号 unsigned char I2C_ReadByte(bit ack) { unsigned char i, dat = 0; SDA = 1; // 释放SDA,准备读取 for(i=0; i<8; i++) { SCL = 1; I2C_Delay(5); dat <<= 1; if(SDA) dat |= 0x01; // 读取SDA线状态 SCL = 0; I2C_Delay(5); } // 读取完毕,主机发送应答信号 SDA = ack ? 1 : 0; // ack=1发送非应答(NACK),ack=0发送应答(ACK) SCL = 1; I2C_Delay(5); SCL = 0; I2C_Delay(5); SDA = 1; // 释放SDA return dat; }4.3 ADS1110读取函数
这个函数封装了与一颗ADS1110通信的全过程,是核心中的核心。
/** * @brief 读取指定通道的ADS1110数据 * @param ch_en_bit: 指向通道使能引脚的控制位(如&CH1_EN) * @retval 读取到的16位ADC原始值,如果读取失败返回0xFFFF */ unsigned int Read_ADS1110(sbit *ch_en_bit) { unsigned int value = 0; unsigned char config_reg; // 1. 开启对应通道的I2C总线门控 *ch_en_bit = 1; I2C_Delay(100); // 稍作延时,等待MOS管完全导通,总线稳定 // 2. 发送起始信号,写入地址和配置寄存器 I2C_Start(); if(I2C_SendByte(ADS1110_ADDR_W)) { // 发送写地址,并检查应答 I2C_Stop(); *ch_en_bit = 0; return 0xFFFF; // 从机无应答,返回错误值 } if(I2C_SendByte(0x00)) { // 写入指针寄存器,0x00指向配置寄存器 I2C_Stop(); *ch_en_bit = 0; return 0xFFFF; } if(I2C_SendByte(ADS1110_CONFIG)) { // 写入配置字节,启动单次转换 I2C_Stop(); *ch_en_bit = 0; return 0xFFFF; } I2C_Stop(); // 3. 等待转换完成(查询方式) // 单次转换模式下,可以查询配置寄存器的最高位(DRDY) // 但为了代码简单,这里采用固定延时。对于240SPS,延时5ms足够。 // 更精确的做法是下面被注释掉的查询代码。 // Delay_ms(5); // --- 精确等待转换完成的代码(推荐)--- do { I2C_Start(); I2C_SendByte(ADS1110_ADDR_W); I2C_SendByte(0x00); // 指向配置寄存器 I2C_Start(); // 重复起始条件 I2C_SendByte(ADS1110_ADDR_R); config_reg = I2C_ReadByte(0); // 读取配置寄存器,发送非应答 I2C_Stop(); // 检查最高位(bit7),为1表示转换完成 } while((config_reg & 0x80) == 0); // --- 结束查询 --- // 4. 读取转换结果 I2C_Start(); I2C_SendByte(ADS1110_ADDR_W); I2C_SendByte(0x00); // 指针寄存器仍指向配置寄存器,但读操作会自动返回转换结果 I2C_Start(); // 重复起始条件 I2C_SendByte(ADS1110_ADDR_R); value = I2C_ReadByte(1); // 读取高8位,并发送应答(ACK) value = (value << 8) | I2C_ReadByte(0); // 读取低8位,并发送非应答(NACK) I2C_Stop(); // 5. 关闭通道,释放总线 *ch_en_bit = 0; I2C_Delay(50); // 短暂延时,确保MOS管完全关断,避免总线冲突 return value; }关键点解析:
- 通道控制:函数通过
sbit指针接收通道使能引脚,这样同一个函数可以用于4个通道,代码复用性高。- 启动转换:向配置寄存器写入
0x8C后,转换立即开始。在单次转换模式下,芯片会忙直到转换完成。- 等待转换:强烈建议使用查询方式(
do...while循环)。固定延时不精确,且如果ADS1110因为某些原因(如输入超量程)转换时间变长,固定延时会导致读取到旧数据或错误数据。查询配置寄存器的最高位(数据就绪位)是最可靠的方法。- 读取数据:注意I2C的“重复起始条件”用法。先发送写地址和指针,然后不发送停止条件,而是直接发送重复起始条件和读地址,开始读取数据。这是I2C标准操作。
- 错误处理:在每一步发送地址或数据后,都检查了应答位。如果无应答,则立即停止并返回错误码
0xFFFF。在实际产品中,可以加入重试机制。
4.4 主函数与数据处理流程
void main() { unsigned char i; // 初始化 Timer0_Init(); // 初始化定时器,用于产生采样间隔,例如100ms UART_Init(); // 初始化串口,9600波特率 // 初始化通道使能引脚为输出低电平 CH1_EN = 0; CH2_EN = 0; CH3_EN = 0; CH4_EN = 0; printf("System Start...\r\n"); // 通过串口发送启动信息 while(1) { if(sampling_flag) { // 定时器中断置位了这个标志 sampling_flag = 0; // 依次读取4路ADC adc_value[0] = Read_ADS1110(&CH1_EN); adc_value[1] = Read_ADS1110(&CH2_EN); adc_value[2] = Read_ADS1110(&CH3_EN); adc_value[3] = Read_ADS1110(&CH4_EN); // 数据换算与处理 for(i=0; i<4; i++) { // 判断是否读取成功 if(adc_value[i] != 0xFFFF) { // 将16位有符号数转换为电压 // 注意:adc_value是unsigned int,但ADS1110输出的是有符号数 // 我们需要先将其视为有符号short来处理 short signed_val = (short)adc_value[i]; voltage[i] = (signed_val / 32767.0) * 2.048; // PGA=1, Vref=2.048V // 如果输入是经过分压的,这里需要乘以分压系数 // 例如,用两个10k电阻分压,测量0-5V,则系数为 2.0 // voltage[i] = voltage[i] * 2.0; } else { voltage[i] = -1.0; // 标记错误 } } // 通过串口发送结果 printf("CH1:%.3fV, CH2:%.3fV, CH3:%.3fV, CH4:%.3fV\r\n", voltage[0], voltage[1], voltage[2], voltage[3]); } // 这里可以添加其他任务,如按键扫描、显示刷新等 } } // 定时器0中断服务函数,用于产生固定采样间隔 void Timer0_ISR() interrupt 1 { static unsigned int t_count = 0; TH0 = 0xDC; // 重装初值,11.0592MHz下,10ms中断一次 TL0 = 0x00; t_count++; if(t_count >= 10) { // 10 * 10ms = 100ms t_count = 0; sampling_flag = 1; // 置位采样标志 } }5. 仿真调试、实测问题与优化建议
代码写完了,原理图也画好了,但离真正能用还差最后一步:调试。这里分享我在仿真和实测中遇到的关键问题及解决方法。
5.1 Proteus仿真中的注意事项
如果你用Proteus做仿真,需要注意以下几点:
- ADS1110模型:Proteus元件库可能没有ADS1110。你可以用ADS1115模型替代,但需要注意引脚和地址的差异。更好的方法是,用两个电压表和一个“ADC”器件来模拟,但这只能验证逻辑,无法模拟I2C通信细节。
- I2C调试器:Proteus自带的“I2C Debugger”工具是神器。把它连接到你的I2C总线上,运行仿真时,它可以实时显示总线上所有的起始、停止、地址、数据、应答信号。当你发现程序卡住时,首先看这里的数据流是否正常,能快速定位是起始信号没发出、地址错误还是无应答。
- 上拉电阻:在仿真中,必须给SCL和SDA线加上上拉电阻(如4.7kΩ到VCC),否则总线永远是低电平。
- 电压测量:用Proteus的电压探针或电压表,检查ADS1110输入引脚的电压是否在你的设定范围内(例如0-2.048V)。如果超限,ADC读数会饱和。
5.2 实物制作与调试踩坑记录
把电路焊出来,才是挑战的开始。
- 电源噪声是第一大敌:我最开始焊接的板子,ADC读数总是在最后几位跳动。用示波器一看,ADS1110的VDD引脚上有几十毫伏的高频毛刺。解决方法就是前面强调的:每个ADS1110的VDD引脚,都必须就近焊接一个100nF的瓷片电容到地。电容的引脚要尽量短。加了之后,噪声立刻变得平滑,读数稳定。
- I2C总线被意外拉低:有一次,所有通道都读不出数据。用逻辑分析仪抓取I2C波形,发现SCL线一直被拉低,无法产生上升沿。这通常是总线冲突或某个器件故障导致的。我的问题出在“门控”MOS管上。当单片机IO口输出低电平时,MOS管应该关断,但其漏极(D)和源极(S)之间可能存在微弱的漏电流,或者关断不彻底,导致总线被部分拉低。我在每个MOS管的栅极(G)和地之间,增加了一个10kΩ的下拉电阻,确保在单片机IO口初始化(高阻态)时,MOS管能被可靠关断。问题解决。
- 地址冲突的隐性BUG:我的方案是分时复用。但在程序切换通道的瞬间,如果时序没控制好,可能会出现两路MOS管同时导通哪怕几微秒的情况,导致两个ADS1110同时响应地址,造成总线冲突。我的解决方法是:在关闭当前通道后,增加一个几十微秒的延时(
I2C_Delay(50);),再打开下一个通道。确保物理上的绝对隔离。 - 测量范围与分压电阻:最初我想直接测量0-5V,结果输入超过2.048V后,读数永远都是32767。这就是量程问题。后来我在每路输入前加了两个精密电阻(10kΩ和20kΩ)进行2/3分压,这样0-5V输入就变成了0-3.33V,仍然超过了2.048V。于是调整电阻为15kΩ和10kΩ,进行2/5分压,将0-5V映射到0-2.0V,完美匹配ADC量程。分压电阻要选用1%精度的金属膜电阻,否则会引入增益误差。
- 软件滤波提升读数稳定性:即使硬件滤波做好了,读数最后一位可能还是会轻微跳动。可以在软件中做滑动平均滤波。例如,为每路电压维护一个长度为8的数组,每次读取新值后,替换最旧的值,然后计算平均值。这样既能平滑噪声,又不会引入太大延迟。
5.3 项目优化与扩展思路
这个基础框架可以有很多玩法和优化空间:
- 提高测量速率:目前是100ms测4路,平均一路25ms,其中大部分时间在等待ADC转换(约4ms)。如果想更快,可以尝试连续转换模式。在连续模式下,ADS1110会以设定速率自动连续转换,你只需要定期去读取数据即可,省去了启动和等待转换的时间。但需要注意数据同步问题。
- 增加量程自动切换:ADS1110的PGA可以编程设置。可以写一个算法:先以PGA=1(小量程)测量,如果读数接近满量程,则自动切换到PGA=2或更大量程重新测量,以实现更宽范围的测量,同时在小信号时保持高分辨率。
- 添加LCD本地显示:接一个LCD1602或OLED屏幕,实时显示4路电压值,脱离电脑也能工作。
- 改为从机模式:将51单片机作为从机,通过串口接收上位机(如电脑上的Python程序)的指令,按需测量指定通道并返回数据,实现远程监控。
- 低功耗设计:如果用于电池供电设备,可以将ADS1110配置为单次转换模式,测量完成后让其进入休眠。单片机也可以在两次测量间隙进入空闲模式或掉电模式,由定时器中断唤醒,大幅降低系统功耗。
这个基于51单片机和ADS1110的4路电压测量系统,从设计到实现,完整地展示了一个嵌入式数据采集项目的基本流程。它不复杂,但涵盖了硬件选型、电路设计、通信协议、软件架构、调试排错等核心环节。希望这份超详细的拆解,能帮你不仅做出这个作品,更能理解背后每一个设计决策的原因。
本文还有配套的精品资源,点击获取