最近在做一个基于51单片机的智能小车项目需要实时监测电机的驱动电流。最初尝试用简单的采样电阻加运放发现精度和稳定性都难以满足要求尤其是在小电流测量时噪声很大。经过一番选型最终锁定了TI的ADS1110这款高精度、低功耗的16位ADC芯片。本文将完整分享如何利用51单片机驱动ADS1110并通过串口将精确的电流值发送到电脑进行显示和记录的全过程。无论你是学生正在做课程设计还是工程师需要为设备添加电流监控功能这套从硬件连接到软件编程再到数据可视化的闭环方案都能直接复用。1. 项目背景与核心器件选型在嵌入式开发中电流测量是一个常见但颇具挑战的需求。无论是电池供电设备的功耗分析、电机驱动的过流保护还是电源管理的负载监测都需要一个稳定可靠的电流测量方案。1.1 为什么需要专门的ADC芯片很多初学者会尝试使用单片机自带的ADC如果具备或简单的电压比较器来测量电流。但这种方法通常面临几个问题分辨率低很多8位或10位ADC的分辨率不足以分辨mA甚至uA级的电流变化。噪声大电源噪声、数字信号干扰会直接影响采样值。需要外围电路需要设计滤波、放大电路增加了复杂度和不确定性。因此对于精度要求较高的场合使用一款集成度高、性能优秀的专用ADC芯片是更优的选择。1.2 ADS1110芯片简介ADS1110是德州仪器TI推出的一款超小型、低功耗、16位精度的Σ-Δ模数转换器。它特别适合电池供电的便携式设备。核心特性高分辨率16位无失码提供高达65,536个输出码。内置可编程增益放大器PGA增益可选1、2、4、8倍能灵活适配不同量程的输入电压。内部基准集成了2.048V的精密基准电压源温漂低简化了外部电路。I2C接口采用标准的I2C通信协议仅需两根线与单片机连接。超低功耗连续转换模式下电流仅为240μA单次转换后自动进入休眠模式功耗可低至0.5μA。小封装采用SOT23-6封装占用PCB面积极小。在本项目中我们利用ADS1110测量采样电阻两端的压降再根据欧姆定律I V / R计算出电流值。1.3 系统整体框架整个电流表系统的工作流程如下电流信号采集待测电流流过一颗精密采样电阻如0.1Ω产生一个微弱的电压信号。信号放大与转换该电压信号送入ADS1110芯片内部的PGA对其进行放大并由Σ-Δ ADC转换为数字量。数据处理51单片机通过I2C总线读取ADS1110转换后的数字值。计算与换算单片机将数字值根据芯片配置增益、基准电压换算成实际电压值再除以采样电阻阻值得到电流值。数据输出51单片机通过UART串口将格式化后的电流数据发送给PC。上位机显示PC端的串口调试助手如SSCOM、XCOM接收并显示数据也可用其他软件进行记录和分析。2. 硬件电路设计与连接“工欲善其事必先利其器”。正确的硬件连接是项目成功的基础。2.1 所需材料清单元件/模块型号/参数数量说明单片机最小系统STC89C52RC1或其他兼容的51内核单片机ADC芯片ADS11101注意是I2C接口版本采样电阻0.1Ω / 1W1阻值小功率大建议使用精密合金电阻USB转TTL模块CH340G1用于单片机与电脑通信晶振11.0592MHz1用于产生精确的串口波特率电容22pF, 10uF, 0.1uF若干滤波、退耦杜邦线公对公母对母若干用于连接面包板或PCB-1搭建电路2.2 核心电路原理图分析下图展示了ADS1110与51单片机及采样电阻的核心连接方式5V/VCC | ---||------ AIN0 (ADS1110 Pin1) | | 采样电阻 R_sense (0.1Ω) | | ---|------ AIN1 (ADS1110 Pin2) | | GND 待测电流 I 流入/出ADS1110 (SOT23-6) 引脚连接详解AIN0 (Pin1)模拟输入正端。连接到采样电阻的高电位端电流流入端。AIN1 (Pin2)模拟输入负端。连接到采样电阻的低电位端电流流出端。ADS1110测量的是AIN0与AIN1之间的差分电压。GND (Pin3)芯片地。与单片机、采样电阻的GND共地。SCL (Pin4)I2C时钟线。连接到单片机任意I/O口如P2.0需接上拉电阻通常4.7kΩ~10kΩ至VCC。SDA (Pin5)I2C数据线。连接到单片机任意I/O口如P2.1需接上拉电阻至VCC。VDD (Pin6)芯片电源2.7V~5.5V。连接到5V或3.3V需并联一个0.1uF的陶瓷电容到GND进行退耦。采样电阻的选择与计算这是影响测量范围和精度的关键。假设我们使用ADS1110的内部2.048V基准PGA增益设为1。最大测量电压V_max V_ref / Gain 2.048V / 1 2.048V。若采样电阻 R_sense 0.1Ω则最大可测电流 I_max V_max / R_sense 2.048V / 0.1Ω 20.48A。分辨率16位ADC总码值65536。电流分辨率 I_max / 65536 ≈ 0.0003125A 0.3125mA。这意味着理论上可以分辨出约0.3mA的电流变化。注意采样电阻的功率要足够。P I² * R。测量20A时功率为(20A)² * 0.1Ω 40W因此在实际中应根据预期最大电流选择合适的电阻功率或使用更小的阻值如0.01Ω配合更高的PGA增益如8倍来降低电阻压降和功耗。2.3 单片机串口连接51单片机通过P3.0 (RXD) 和 P3.1 (TXD) 与USB转TTL模块连接单片机TXD (P3.1)- USB转TTL模块RXD单片机RXD (P3.0)- USB转TTL模块TXD单片机GND- USB转TTL模块GND单片机VCC- 5V电源可从USB模块取电但需注意电流负载3. 软件编程51单片机驱动ADS1110硬件搭建好后我们来编写单片机的C语言程序。程序主要包含三个部分I2C模拟驱动、ADS1110读写函数、主循环数据处理与串口发送。3.1 工程建立与基础配置我们使用Keil uVision进行开发。首先创建一个新工程选择正确的单片机型号如STC89C52RC。系统基础配置时钟频率设置为11.0592MHz。这个频率可以非常精确地产生9600、19200等标准串口波特率。内存模型一般选择Small即可。3.2 I2C总线模拟驱动由于标准51单片机没有硬件I2C外设我们需要用两个普通I/O口来模拟I2C时序。这是一个非常经典且必须掌握的技能。// 文件i2c.c #include i2c.h #include intrins.h // 用于_nop_()延时 sbit I2C_SCL P2^0; // 定义I2C时钟线引脚 sbit I2C_SDA P2^1; // 定义I2C数据线引脚 /** * brief I2C起始信号 * param 无 * retval 无 */ void I2C_Start(void) { I2C_SDA 1; I2C_SCL 1; I2C_Delay(); I2C_SDA 0; // SDA在SCL高电平期间由高变低表示起始 I2C_Delay(); I2C_SCL 0; // 钳住总线准备发送数据 I2C_Delay(); } /** * brief I2C停止信号 * param 无 * retval 无 */ void I2C_Stop(void) { I2C_SDA 0; I2C_SCL 1; I2C_Delay(); I2C_SDA 1; // SDA在SCL高电平期间由低变高表示停止 I2C_Delay(); } /** * brief I2C发送一个字节 * param dat: 要发送的数据 * retval 从机应答位0-应答1-非应答 */ bit I2C_SendByte(unsigned char dat) { unsigned char i; bit ack_bit; for(i0; i8; i) // 循环8次发送8位数据 { I2C_SDA (bit)(dat 0x80); // 取出最高位 dat 1; I2C_SCL 1; I2C_Delay(); I2C_SCL 0; I2C_Delay(); } // 读取应答位第9个时钟脉冲 I2C_SDA 1; // 释放SDA线准备接收 I2C_SCL 1; I2C_Delay(); ack_bit I2C_SDA; // 读取SDA电平0为应答1为非应答 I2C_SCL 0; I2C_Delay(); I2C_SDA 0; // 发送完拉低SDA为下次操作做准备 return ack_bit; } /** * brief I2C接收一个字节 * param ack: 是否发送应答信号1-发送非应答0-发送应答 * retval 接收到的数据 */ unsigned char I2C_RecvByte(bit ack) { unsigned char i, dat 0; I2C_SDA 1; // 确保主机释放SDA线 for(i0; i8; i) { dat 1; I2C_SCL 1; I2C_Delay(); if(I2C_SDA) dat | 0x01; // 读取SDA数据位 I2C_SCL 0; I2C_Delay(); } // 发送应答/非应答信号 I2C_SDA ack; // ack0:应答 ack1:非应答 I2C_SCL 1; I2C_Delay(); I2C_SCL 0; I2C_Delay(); I2C_SDA 1; // 释放SDA线 return dat; } // 简单的微秒级延时函数具体延时需根据主频调整 void I2C_Delay(void) { _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); }// 文件i2c.h #ifndef __I2C_H__ #define __I2C_H__ #include reg52.h // 引脚定义 sbit I2C_SCL P2^0; sbit I2C_SDA P2^1; // 函数声明 void I2C_Delay(void); void I2C_Start(void); void I2C_Stop(void); bit I2C_SendByte(unsigned char dat); unsigned char I2C_RecvByte(bit ack); #endif3.3 ADS1110驱动程序接下来基于I2C驱动编写ADS1110的专用读写函数。ADS1110的I2C地址是固定的0x487位地址。它内部有三个寄存器转换结果寄存器16位、配置寄存器8位。// 文件ads1110.c #include ads1110.h #include i2c.h #include intrins.h #define ADS1110_ADDR_W 0x90 // ADS1110的I2C写地址 (0x48 1) | 0 #define ADS1110_ADDR_R 0x91 // ADS1110的I2C读地址 (0x48 1) | 1 /** * brief 向ADS1110写入配置寄存器 * param config: 配置字节 * retval 0-成功1-失败无应答 */ bit ADS1110_WriteConfig(unsigned char config) { I2C_Start(); if(I2C_SendByte(ADS1110_ADDR_W)) // 发送器件地址写 { I2C_Stop(); return 1; // 无应答失败 } if(I2C_SendByte(config)) // 发送配置字节 { I2C_Stop(); return 1; } I2C_Stop(); return 0; // 成功 } /** * brief 从ADS1110读取转换结果和配置 * param result: 指向存放16位结果的数组高字节在前 * param config: 指向存放配置字节的变量 * retval 0-成功1-失败 */ bit ADS1110_ReadData(unsigned char *result, unsigned char *config) { I2C_Start(); if(I2C_SendByte(ADS1110_ADDR_R)) // 发送器件地址读 { I2C_Stop(); return 1; } // ADS1110在读取时会先发送2字节结果再发送1字节配置 result[0] I2C_RecvByte(0); // 读取结果高字节并发送应答 result[1] I2C_RecvByte(0); // 读取结果低字节并发送应答 *config I2C_RecvByte(1); // 读取配置字节并发送非应答结束 I2C_Stop(); return 0; } /** * brief 初始化ADS1110 * param 无 * retval 无 * note 配置为连续转换模式增益1采样率15SPS */ void ADS1110_Init(void) { unsigned char config 0x8C; // 二进制1000 1100 // BIT7: ST/DRDY (0) - 无关 // BIT6-5: 采样率 (00) - 15 SPS // BIT4-3: 增益 (01) - 增益1 // BIT2-0: 模式 (100) - 连续转换模式 // 更常用的单次转换模式配置可能是 0x8A (1000 1010) while(ADS1110_WriteConfig(config)); // 循环写入直到成功 }// 文件ads1110.h #ifndef __ADS1110_H__ #define __ADS1110_H__ // ADS1110 I2C 地址定义 #define ADS1110_ADDR 0x48 // 函数声明 bit ADS1110_WriteConfig(unsigned char config); bit ADS1110_ReadData(unsigned char *result, unsigned char *config); void ADS1110_Init(void); #endif3.4 串口通信程序为了将数据发送到电脑我们需要配置并使用51单片机的串口。// 文件uart.c #include uart.h #include reg52.h /** * brief 串口初始化波特率9600模式1允许接收 * param 无 * retval 无 */ void UART_Init(void) { SCON 0x50; // 串口模式18位数据允许接收 TMOD 0x0F; // 清零定时器1模式位 TMOD | 0x20; // 定时器1模式28位自动重装 TH1 0xFD; // 波特率9600 (11.0592MHz晶振) TL1 0xFD; TR1 1; // 启动定时器1 EA 1; // 开启总中断如果不用接收可不开启 // ES 1; // 开启串口中断本例查询发送未使用中断 } /** * brief 串口发送一个字符 * param dat: 要发送的数据 * retval 无 */ void UART_SendByte(unsigned char dat) { SBUF dat; // 将数据写入发送缓冲区 while(TI 0); // 等待发送完成 TI 0; // 清除发送完成标志 } /** * brief 串口发送一个字符串 * param str: 要发送的字符串指针 * retval 无 */ void UART_SendString(unsigned char *str) { while(*str ! \0) { UART_SendByte(*str); str; } } /** * brief 串口发送一个16位整数十进制 * param num: 要发送的整数 * retval 无 */ void UART_SendInt(int num) { unsigned char buf[6]; unsigned char i 0; unsigned char len 0; if(num 0) { UART_SendByte(-); num -num; } else if(num 0) { UART_SendByte(0); return; } // 将整数逐位分解到缓冲区 while(num 0) { buf[len] num % 10 0; num / 10; } // 逆序发送即从最高位开始 for(i len; i 0; i--) { UART_SendByte(buf[i-1]); } }3.5 主程序逻辑最后我们将所有模块整合到主函数中实现周期性的电流采样、计算和串口上报。// 文件main.c #include reg52.h #include ads1110.h #include uart.h #include stdio.h // 用于sprintf如果内存紧张可不用 #define SAMPLE_RESISTOR 0.1f // 采样电阻阻值单位欧姆 #define VREF 2.048f // ADS1110内部基准电压单位伏特 #define PGA_GAIN 1.0f // 可编程增益放大器增益本例设为1 /** * brief 将ADS1110的原始数据转换为电压值 * param msb: 转换结果高字节 * param lsb: 转换结果低字节 * retval 计算出的差分电压值单位伏特 */ float ADS1110_ConvertToVoltage(unsigned char msb, unsigned char lsb) { int raw_data; float voltage; // 组合16位有符号整数ADS1110输出为二进制补码 raw_data (msb 8) | lsb; // 处理负数 if(raw_data 0x8000) { raw_data raw_data - 65536; } // 计算电压V (raw_data / 32768) * (Vref / Gain) // 对于16位满量程输出码为32767到-32768 voltage ((float)raw_data / 32768.0f) * (VREF / PGA_GAIN); return voltage; } void main(void) { unsigned char adc_result[2]; // 存放ADC结果adc_result[0]MSB, [1]LSB unsigned char adc_config; // 存放配置寄存器值 float voltage; // 采样电阻两端电压 float current; // 计算出的电流 char str_buf[30]; // 用于格式化的字符串缓冲区 // 初始化 UART_Init(); // 串口初始化波特率9600 ADS1110_Init(); // ADS1110初始化 UART_SendString(System Start...\r\n); while(1) { // 1. 读取ADS1110的转换数据和当前配置 if(ADS1110_ReadData(adc_result, adc_config) 0) { // 2. 将原始数据转换为电压值伏特 voltage ADS1110_ConvertToVoltage(adc_result[0], adc_result[1]); // 3. 根据欧姆定律计算电流I V / R current voltage / SAMPLE_RESISTOR; // 4. 通过串口发送数据格式电压电流 // 方法一使用sprintf格式化需要开启float支持会增大代码 // sprintf(str_buf, V:%.4fV, I:%.4fA\r\n, voltage, current); // UART_SendString(str_buf); // 方法二分步发送更节省资源推荐 UART_SendString(Voltage: ); // 发送电压整数部分 UART_SendInt((int)voltage); UART_SendByte(.); // 发送电压小数部分放大10000倍取整 UART_SendInt((int)((voltage - (int)voltage) * 10000)); UART_SendString( V, Current: ); // 发送电流值单位A if(current 0.001 current -0.001) // 电流很小显示为mA { current current * 1000; // 转换为mA UART_SendInt((int)current); UART_SendByte(.); UART_SendInt((int)((current - (int)current) * 1000)); UART_SendString( mA); } else { UART_SendInt((int)current); UART_SendByte(.); UART_SendInt((int)((current - (int)current) * 1000)); UART_SendString( A); } UART_SendString(\r\n); // 换行 } else { UART_SendString(ADS1110 Read Error!\r\n); } // 延时一段时间再进行下一次读取避免串口数据过快 // 使用简单的延时循环实际项目中建议用定时器 { unsigned int i, j; for(i0; i1000; i) for(j0; j1000; j); } } }4. 系统调试与数据验证代码编写完成后需要编译、下载到单片机并进行实际测试。4.1 编译与下载在Keil中编译整个工程确保0错误0警告。使用STC-ISP等下载软件将生成的.hex文件烧录到STC89C52RC单片机中。选择正确的单片机型号。选择正确的串口号你的USB转TTL模块对应的COM口。设置合适的波特率通常使用115200或9600进行下载。点击“下载/编程”按钮然后给单片机上电复位完成下载。4.2 串口调试助手配置打开串口调试助手如SSCOM、XCOM。选择与单片机连接的COM口。设置波特率为9600与程序UART_Init()中设置一致。数据位8停止位1无校验位。点击“打开串口”。4.3 测试与结果分析正确连接硬件并上电后你应该能在串口调试助手的接收区看到类似以下格式的数据流System Start... Voltage: 0.0123 V, Current: 123.4 mA Voltage: 0.0125 V, Current: 125.0 mA Voltage: -0.0010 V, Current: -10.0 mA ...如何验证精度零点校准在不接入任何电流断开负载时读取的电流值应在零点附近小幅波动。这个偏移量可以在软件中减去软件调零。已知负载测试使用一个已知阻值的功率电阻作为负载连接到可调电源上。用万用表测量实际电流与串口输出的电流值进行对比。调整程序中的SAMPLE_RESISTOR微调参数使两者匹配。线性度测试改变电源电压观察电流测量值是否与万用表测量值保持良好的线性关系。5. 常见问题与排查思路在实践过程中你可能会遇到以下问题问题现象可能原因排查思路与解决方案串口无任何数据输出1. 电源未接通或电压不对。2. 单片机未正常启动晶振问题。3. 串口线接反TXD与RXD交叉。4. 波特率设置错误。1. 检查电源电压是否为5V测量单片机VCC引脚。2. 用示波器检查晶振引脚是否有11.0592MHz波形。3. 确认单片机TXD接USB模块RXD单片机RXD接USB模块TXD。4. 确认程序与串口助手的波特率、数据位、停止位完全一致。串口输出乱码1. 波特率不匹配最常见。2. 单片机时钟频率不是11.0592MHz。3. 串口助手编码格式问题。1. 重点检查波特率。11.0592MHz晶振对应9600波特率时TH10xFD。2. 确认单片机使用的晶振频率并重新计算定时器重装值。3. 尝试以十六进制显示看是否是规律数据。读取ADS1110始终失败返回非01. I2C引脚SDA SCL未接上拉电阻。2. ADS1110电源或地未接好。3. I2C时序速度过快51单片机跟不上。4. ADS1110器件地址错误或损坏。1. 确认SCL和SDA线上有4.7kΩ~10kΩ的上拉电阻到VCC。2. 测量ADS1110的VDD引脚是否为正确电压2.7V-5.5V。3. 在I2C_Delay()函数中增加_nop_()数量降低I2C速度。4. 用逻辑分析仪或示波器抓取I2C波形看起始信号、地址、应答是否正常。测量值跳动大噪声明显1. 采样电阻两端走线过长引入干扰。2. 电源噪声大。3. 数字地单片机与模拟地ADS1110、采样电阻未做隔离。4. 未在ADS1110电源引脚附近放置0.1uF退耦电容。1. 采样电阻的走线应尽量短且粗采用开尔文接法四线制为佳。2. 为模拟部分使用LDO等干净的电源并与数字电源用磁珠或0Ω电阻隔离。3. 在PCB布局上将模拟地和数字地单点连接。4. 在ADS1110的VDD和GND引脚间紧挨着焊接一个0.1uF陶瓷电容。测量值有固定偏差1. 采样电阻阻值不精确。2. ADS1110内部基准电压有微小偏差。3. 运放若使用存在失调电压。1. 使用更高精度的采样电阻如0.1%精度。2. 在软件中进行校准测量一个已知精确电流计算出一个校准系数进行乘法补偿。3. 在程序初始化时测量短路输入AIN0AIN1时的输出将此值作为零点偏移量存储并后续减去。电流很大时测量值不准确或发热1. 采样电阻功率不足阻值因发热而漂移。2. 输入电压超过ADS1110的允许范围PGA增益设置过小。1. 根据公式PI²R计算电阻功耗选择功率余量足够的电阻建议2倍以上。2. 根据最大待测电压选择合适的PGA增益。例如若最大压降0.5V可设置增益为4量程为2.048V/40.512V充分利用ADC量程以提高精度。6. 优化与进阶应用基础功能实现后可以考虑以下优化方向让这个电流表更实用、更专业。6.1 软件滤波算法ADC采样值难免会有噪声简单的软件滤波能极大提升读数稳定性。均值滤波连续采样N次取平均值。简单有效但会引入延迟。#define FILTER_NUM 10 float Filter_Average(float new_value) { static float buf[FILTER_NUM] {0}; static int index 0; float sum 0; buf[index] new_value; index (index 1) % FILTER_NUM; for(int i0; iFILTER_NUM; i) sum buf[i]; return sum / FILTER_NUM; }一阶低通滤波惯性滤波Y(n) α * X(n) (1-α) * Y(n-1)。α为滤波系数0α1α越小滤波效果越强响应越慢。非常适合嵌入式系统。6.2 量程自动切换为了同时测量小电流和大电流可以设计自动量程功能。初始化时使用高增益如Gain8测量小电流。如果读数值接近满量程例如超过90%则通过ADS1110_WriteConfig函数动态将增益调低如改为Gain1。同时在软件计算中根据当前的增益值来修正电压计算公式。反之如果读数很小则尝试切换到更高增益以提高分辨率。6.3 上位机软件开发使用串口调试助手只能看数据无法记录和分析。你可以用PythonPySerial库、C#SerialPort控件或LabVIEW等工具开发一个简单的上位机实现实时曲线绘制将电流值以波形图形式显示。数据记录将数据保存到CSV或TXT文件便于后续用Excel或MATLAB分析。报警功能设置电流上下限超限时声音或颜色提示。校准界面提供输入实际电流值的界面自动计算并保存校准系数到单片机需扩展EEPROM操作。6.4 低功耗优化如果用于电池供电设备需考虑功耗。使用单次转换模式将ADS1110配置为单次转换模式。需要读数时单片机发送启动转换命令读取结果后芯片自动进入休眠1μA。在ADS1110_Init中将配置字节改为0x8A单次转换增益115SPS。单片机休眠在两次采样间隔让51单片机进入空闲模式或掉电模式通过定时器中断唤醒。这需要配置相应的低功耗模式和中断。降低系统电压在满足性能的前提下将系统电压从5V降至3.3V可以显著降低功耗。通过这个项目你不仅完成了一个高精度数字电流表更深入掌握了51单片机驱动I2C设备、模拟I2C时序、串口通信、数据处理等嵌入式开发核心技能。这套方法同样适用于ADS1110的姐妹型号如ADS1115带多路输入和比较器或其他I2C传感器如温度、压力传感器。