news 2026/9/3 2:49:53

基于51单片机与ADS1110的4路高精度电压采集系统设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于51单片机与ADS1110的4路高精度电压采集系统设计与实现

简介:本资源是一套完整的基于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,主要基于以下几点考虑:

  1. 精度与速度的平衡:ADS1110是16位分辨率,对于测量0-5V的电压,理论分辨率能达到5V / 65536 ≈ 0.076mV,这比单片机自带的10位ADC(约4.9mV)精度高两个数量级。它的采样率最高到240SPS(每秒采样次数),对于监测电池电压、电源纹波、传感器输出等变化不快的信号完全足够。
  2. 极简的外围电路:ADS1110内部集成了基准源、振荡器和PGA(可编程增益放大器)。这意味着你不需要外接精密基准电压芯片,也不需要复杂的滤波电路,基本上VCC、GND、I2C接口加上输入信号,芯片就能工作,极大简化了PCB设计和BOM成本。
  3. 单通道与多路复用:ADS1110本身是单通道ADC。要实现4路测量,就需要4颗ADS1110。这看起来是缺点,但实际上带来了一个巨大优势:4路可以同时采样。如果使用ADS1115这种多路复用的ADC,虽然只用一颗芯片,但4个通道是轮流采样的,存在时间差。在某些需要同步比较4路电压的应用中(比如平衡充),4颗独立的ADS1110反而是更好的选择。
  4. 成本与封装: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-U4ADC芯片ADS1110A0IDBVR (SOT23-6)4注意后缀A0,地址为GND配置
Y1晶振11.0592MHz1
C1, C6-C9等瓷片电容100nF (104)9+单片机电源去耦1个,每个ADS1110电源去耦1个,输入滤波4个
C2电解电容10uF/16V1电源储能
C3电解电容10uF/16V1复位电路
C4, C5瓷片电容22pF2晶振负载电容
R1电阻10kΩ1复位电路上拉电阻
R3, R4电阻4.7kΩ2I2C总线上拉电阻
R2, R5, R8, R11电阻100Ω4输入限流/滤波电阻
R6, R7, R9, R10等电阻1kΩ8MOS管栅极限流电阻(每路2个)
Q1-Q8MOS管2N7002 (SOT-23)8N沟道,用于I2C总线门控,每路需要2个(SDA和SCL)
S1轻触开关6x6x5mm1复位按钮
USB转串口CH340G模块1用于程序下载和调试输出
电源接口DC-005 / Micro USB1提供5V输入

注意:MOS管2N7002的栅极阈值电压较低(约2.1V),用5V单片机IO口驱动完全没问题。焊接时注意SOT-23封装的方向。如果买不到2N7002,也可以用其他通用的N沟道小信号MOS管替代,如SI2302。

3. 程序设计流程图与核心逻辑剖析

画流程图不是走过场,而是把复杂的程序逻辑视觉化,帮助自己理清思路,也方便后续调试和他人理解。这个项目的程序流程图可以分为三个层次:主程序流程、单次测量流程、I2C通信流程。

3.1 主程序流程图与状态机设计

主程序的核心是一个简单的顺序执行+定时循环结构,但用状态机的思想来理解会更清晰。

  1. 初始化:上电后,首先初始化单片机硬件。包括:

    • 设置定时器(如Timer0)为16位自动重装模式,用于产生精确的延时和采样间隔。
    • 初始化串口,波特率设为9600,用于向电脑发送数据。
    • 初始化用于控制4路I2C总线“门控”MOS管的GPIO(如P1.0-P1.3),并全部设置为低电平(关闭所有通路)。
    • 初始化一些状态变量和数组,用于存储4路电压值。
  2. 主循环:进入一个无限的while(1)循环。循环体内不是一股脑地执行,而是由定时器中断来驱动“采样”这个事件。

    • 主循环大部分时间在“空闲”状态,可以执行一些低优先级的任务,比如检查是否有串口命令(如修改采样率)。
    • 定时器中断(比如每100ms一次)发生时,会设置一个标志位,如sampling_flag = 1
  3. 采样状态:在主循环中不断检查sampling_flag。一旦发现其被置位,立即清除该标志,并进入“采样状态”。

    • 在采样状态下,程序会依次对4路电压进行测量。注意,是“依次”,不是“同时”,但因为我们的“门控”机制,在逻辑上可以认为在打开某一路MOS管的瞬间,该路ADC开始工作,其他路与总线隔离,互不干扰。
    • 测量完一路,将结果存入对应的数组,然后立即关闭该路的“门”,打开下一路的“门”,进行下一路测量。这个切换速度很快(微秒级),对于慢变信号,可以近似认为是同步采样。
    • 4路全部测量完毕后,退出“采样状态”。
  4. 数据处理与发送状态:采样结束后,程序进入“数据处理状态”。

    • 将ADC读取到的原始数字量(一个16位有符号整数,因为ADS1110是差分输入)根据PGA增益和基准电压,换算成实际的电压值(单位:伏特)。
    • 可以选择进行简单的软件滤波,比如取最近几次测量的平均值。
    • 最后,将4路电压值通过串口,按照一定的格式(例如CH1:3.301V, CH2:2.485V, CH3:4.998V, CH4:0.123V)发送给上位机(电脑),或者驱动本地显示(如LCD1602)。
  5. 返回空闲:数据处理发送完毕后,程序回到主循环的起点,等待下一个定时中断,开启新一轮的采样。

这个流程的关键在于通过“门控”GPIO和顺序查询,用一套I2C总线资源分时复用了4颗地址相同的ADC芯片,逻辑清晰,资源占用少。

3.2 单路ADS1110测量子流程详解

这是程序最核心的部分,它封装了与一颗ADS1110通信并读取数据的全过程。流程图如下:

  1. 开启通道:单片机将对应通道的“门控”GPIO置高(例如测量第1路,则置高P1.0),使对应的两个MOS管导通,将该路ADS1110接入I2C总线。
  2. 发送配置字节:通过I2C向ADS1110(地址0x48)写入配置寄存器。这是关键一步!ADS1110的配置寄存器(8位)决定了它的工作模式。
    • Bit7-6 (ST/DR1/DR0): 数据速率选择。我们设为11,对应240SPS,这是最高速率。
    • Bit5-4 (PGA1/PGA0): 可编程增益放大器设置。这决定了输入电压的范围。例如:
      • 00: PGA=1, 输入范围 ±4.096V
      • 01: PGA=2, 输入范围 ±2.048V
      • 10: PGA=4, 输入范围 ±1.024V
      • 11: 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。
  3. 等待转换完成:写入配置字节后,ADS1110会立即开始一次转换。转换时间取决于数据速率,240SPS下约4.17ms。程序需要延时至少这么长时间。更专业的做法是反复读取配置寄存器的最高位(Bit15),这一位在转换期间为0,转换完成后为1。通过查询这一位可以精确知道转换何时结束,避免盲目延时。
  4. 读取转换结果:转换完成后,通过I2C读取两个字节的数据(MSB先送)。这两个字节组成一个16位有符号整数。
  5. 关闭通道:读取数据后,立即将“门控”GPIO置低,断开该路ADS1110与总线的连接,为下一路测量做准备。
  6. 数据换算:将读取到的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的代码网上很多,但真正稳定可靠需要关注以下几点:

  1. 时序严格性:I2C的起始条件(SDA在SCL高电平时由高变低)、停止条件(SDA在SCL高电平时由低变高)、数据有效性(SDA在SCL高电平期间必须保持稳定)的时序必须严格遵守。每个上升沿、下降沿后要加适当的延时(_nop_()空指令),这个延时时间决定了I2C的速率。对于100kHz的标准模式,一个位周期是10us,半周期是5us。我们的单片机主频11.0592MHz,一个_nop_()大约1us,所以延时5个_nop_()左右。
  2. 应答(ACK)处理:主机每发送完一个字节(8位),必须释放SDA线(设为输入模式),并产生一个额外的时钟脉冲(SCL高电平),在这个脉冲期间,从机(ADS1110)需要将SDA拉低作为应答。主机程序必须去读取SDA线的状态,检查是否为低电平,以判断从机是否正常应答。同样,主机读取完一个字节后,需要在下一个时钟脉冲发出应答(或非应答)信号。
  3. 端口模式切换:这是软件I2C最容易出错的地方。51单片机的GPIO在作为输出(写01)和输入(读取引脚状态)时,需要对端口进行不同的操作。对于传统的51单片机(如STC89C52),读取引脚前需要先向端口锁存器写1(即Px = 0xFF;),这样才能正确读取外部电平。而对于一些新型的51单片机(如STC15系列),IO口有专门的模式寄存器需要配置。在代码中,必须封装好SDA_Out()SDA_In()这样的函数,来正确处理方向切换。
  4. 总线异常恢复:在实际调试中,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; }

关键点解析

  1. 通道控制:函数通过sbit指针接收通道使能引脚,这样同一个函数可以用于4个通道,代码复用性高。
  2. 启动转换:向配置寄存器写入0x8C后,转换立即开始。在单次转换模式下,芯片会忙直到转换完成。
  3. 等待转换强烈建议使用查询方式(do...while循环)。固定延时不精确,且如果ADS1110因为某些原因(如输入超量程)转换时间变长,固定延时会导致读取到旧数据或错误数据。查询配置寄存器的最高位(数据就绪位)是最可靠的方法。
  4. 读取数据:注意I2C的“重复起始条件”用法。先发送写地址和指针,然后不发送停止条件,而是直接发送重复起始条件和读地址,开始读取数据。这是I2C标准操作。
  5. 错误处理:在每一步发送地址或数据后,都检查了应答位。如果无应答,则立即停止并返回错误码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做仿真,需要注意以下几点:

  1. ADS1110模型:Proteus元件库可能没有ADS1110。你可以用ADS1115模型替代,但需要注意引脚和地址的差异。更好的方法是,用两个电压表和一个“ADC”器件来模拟,但这只能验证逻辑,无法模拟I2C通信细节。
  2. I2C调试器:Proteus自带的“I2C Debugger”工具是神器。把它连接到你的I2C总线上,运行仿真时,它可以实时显示总线上所有的起始、停止、地址、数据、应答信号。当你发现程序卡住时,首先看这里的数据流是否正常,能快速定位是起始信号没发出、地址错误还是无应答。
  3. 上拉电阻:在仿真中,必须给SCL和SDA线加上上拉电阻(如4.7kΩ到VCC),否则总线永远是低电平。
  4. 电压测量:用Proteus的电压探针或电压表,检查ADS1110输入引脚的电压是否在你的设定范围内(例如0-2.048V)。如果超限,ADC读数会饱和。

5.2 实物制作与调试踩坑记录

把电路焊出来,才是挑战的开始。

  1. 电源噪声是第一大敌:我最开始焊接的板子,ADC读数总是在最后几位跳动。用示波器一看,ADS1110的VDD引脚上有几十毫伏的高频毛刺。解决方法就是前面强调的:每个ADS1110的VDD引脚,都必须就近焊接一个100nF的瓷片电容到地。电容的引脚要尽量短。加了之后,噪声立刻变得平滑,读数稳定。
  2. I2C总线被意外拉低:有一次,所有通道都读不出数据。用逻辑分析仪抓取I2C波形,发现SCL线一直被拉低,无法产生上升沿。这通常是总线冲突或某个器件故障导致的。我的问题出在“门控”MOS管上。当单片机IO口输出低电平时,MOS管应该关断,但其漏极(D)和源极(S)之间可能存在微弱的漏电流,或者关断不彻底,导致总线被部分拉低。我在每个MOS管的栅极(G)和地之间,增加了一个10kΩ的下拉电阻,确保在单片机IO口初始化(高阻态)时,MOS管能被可靠关断。问题解决。
  3. 地址冲突的隐性BUG:我的方案是分时复用。但在程序切换通道的瞬间,如果时序没控制好,可能会出现两路MOS管同时导通哪怕几微秒的情况,导致两个ADS1110同时响应地址,造成总线冲突。我的解决方法是:在关闭当前通道后,增加一个几十微秒的延时(I2C_Delay(50);),再打开下一个通道。确保物理上的绝对隔离。
  4. 测量范围与分压电阻:最初我想直接测量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%精度的金属膜电阻,否则会引入增益误差。
  5. 软件滤波提升读数稳定性:即使硬件滤波做好了,读数最后一位可能还是会轻微跳动。可以在软件中做滑动平均滤波。例如,为每路电压维护一个长度为8的数组,每次读取新值后,替换最旧的值,然后计算平均值。这样既能平滑噪声,又不会引入太大延迟。

5.3 项目优化与扩展思路

这个基础框架可以有很多玩法和优化空间:

  1. 提高测量速率:目前是100ms测4路,平均一路25ms,其中大部分时间在等待ADC转换(约4ms)。如果想更快,可以尝试连续转换模式。在连续模式下,ADS1110会以设定速率自动连续转换,你只需要定期去读取数据即可,省去了启动和等待转换的时间。但需要注意数据同步问题。
  2. 增加量程自动切换:ADS1110的PGA可以编程设置。可以写一个算法:先以PGA=1(小量程)测量,如果读数接近满量程,则自动切换到PGA=2或更大量程重新测量,以实现更宽范围的测量,同时在小信号时保持高分辨率。
  3. 添加LCD本地显示:接一个LCD1602或OLED屏幕,实时显示4路电压值,脱离电脑也能工作。
  4. 改为从机模式:将51单片机作为从机,通过串口接收上位机(如电脑上的Python程序)的指令,按需测量指定通道并返回数据,实现远程监控。
  5. 低功耗设计:如果用于电池供电设备,可以将ADS1110配置为单次转换模式,测量完成后让其进入休眠。单片机也可以在两次测量间隙进入空闲模式或掉电模式,由定时器中断唤醒,大幅降低系统功耗。

这个基于51单片机和ADS1110的4路电压测量系统,从设计到实现,完整地展示了一个嵌入式数据采集项目的基本流程。它不复杂,但涵盖了硬件选型、电路设计、通信协议、软件架构、调试排错等核心环节。希望这份超详细的拆解,能帮你不仅做出这个作品,更能理解背后每一个设计决策的原因。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 2:49:38

ThinkPHP6+Swoole+UniApp构建高并发仿QQ即时通讯系统

简介&#xff1a;这是一套基于ThinkPHP6与Swoole后端架构、UniApp前端实现的高仿QQ即时通讯全栈项目源码&#xff0c;面向计算机专业学生、全栈初学者及毕业设计/课程设计开发者&#xff0c;解决即时消息收发、用户在线状态、群聊与私聊等核心IM功能的工程化落地难题。资源包共…

作者头像 李华
网站建设 2026/9/3 2:47:59

从普通代码到可开源仓库:完整整理指南

很多开发者手里都有一些不错的项目代码、组件封装、工具脚本或学习笔记&#xff0c;但因为整理不规范、缺少说明文档、协议不清晰&#xff0c;最后只敢放在本地&#xff0c;或者发到网盘里让别人下载后一脸茫然。项目代码明明写完了&#xff0c;却始终迈不出“开源”这一步。 …

作者头像 李华
网站建设 2026/9/3 2:47:25

Android FFmpeg RTSP拉流获取H.264 NALU原始数据实战

简介&#xff1a;本资源是一套面向Android音视频开发者的FFmpeg实战工程&#xff0c;聚焦于在移动端拉取RTSP流并提取原始H.264 NALU数据的核心场景&#xff0c;适用于实时监控、视频分析、自定义解码等中高级开发需求。压缩包共358个文件&#xff0c;涵盖72个XML布局与配置文件…

作者头像 李华
网站建设 2026/9/3 2:44:45

基于Flet框架的全栈文件上传组件开发与实战指南

简介&#xff1a;本资源是一套基于Flet前端框架与FastAPI后端服务协同实现的文件上传系统模板&#xff0c;面向Python全栈初学者及轻量级Web应用开发者&#xff0c;解决前后端联动上传、进度反馈与本地持久化保存的核心问题。适用于文档管理、媒体库搭建、团队项目文件共享等实…

作者头像 李华
网站建设 2026/9/3 2:43:21

西门子PLC与基恩士视觉传感器PROFINET通信实战指南

简介&#xff1a;本资源是面向工业自动化工程师与PLC系统集成人员的实战型通信配置示例&#xff0c;聚焦基恩士IV视觉系统与西门子PLC通过PROFINET协议实现高效数据交互的核心场景&#xff0c;解决跨品牌设备集成中常见的网络配置、IO映射与周期性数据交换难题。压缩包共35个文…

作者头像 李华
网站建设 2026/9/3 2:42:48

MiniMax H3本地部署实战:用ComfyUI搭建可复现的AI视频生成工作流

最近我把一堆“要先有雏形、再往细节里抠”的短片项目&#xff0c;全部搬到本地 ComfyUI 里重跑了一遍。整个过程最让我意外的不是 MiniMax H3 生成的视频效果有多惊艳&#xff0c;而是我发现自己终于摆脱了“在线生成一次、下载一次、重新再来的循环”。以前用网页端或在线接口…

作者头像 李华