
简介基于STC15F2K60S2单片机读取ADS1256高精度模数转换芯片并通过LCD1602液晶屏显示电压值的驱动源码包。资源面向单片机学习者和嵌入式开发人员适合需要掌握SPI通信、ADC数据采集与字符液晶显示联合调试的场合也可作为课程设计和毕设的参考资料。压缩包内共有20个文件包括C程序源码、头文件、Keil工程文件、可直接烧录的Hex固件以及编译生成的目标文件和列表文件整个压缩包仅59KB体积小巧便于快速下载与查阅。已有1082人学习/下载说明其实用性得到初步验证。源码中给出了ADS1256的初始化、通道选择读取、电压系数校准以及通过四线方式驱动LCD1602显示电压值的具体实现主函数轮询采集AIN0/AIN1等多通道信号并延时刷新关键代码注释清晰便于理解原始ADC数据到电压值的换算过程也方便移植到其他单片机开发板上。1. 从 ADS1256 到 STC15F2K60S2这个高精度组合值得拆一遍把 ADS1256 接到 STC15F2K60S2 上乍看是跨代搭配8 位 1T 内核带着一颗 24 位 Σ-Δ ADC 跑很多第一次接触的人会怀疑 51 系列是不是扛不住。实际调过之后你会发现在 100SPS 以下的采集场景里这个组合非常稳定STC15F2K60S2 的 IO 翻转速度和 Keil C51 的生成代码完全能应付 ADS1256 的 SPI 协议。真正麻烦的不是读数据而是初始化顺序、MUX 切换时机、自校准的执行以及 LCD1602 的 4 位刷新会不会打断 DRDY 握手。这套驱动源码把这些环节串成了一条完整链路读 AIN0 和 AIN1 对 AINCOM换算成电压后由 LCD1602 显示适合做高精度仪表、传感器采集前端或者毕业设计里的数据采集板。2. ADS1256 的 SPI 时序与 STC15F2K60S2 接口2.1 STC15F2K60S2 驱动 ADS1256 的可行性边界ADS1256 是德州仪器的 24 位 ADC内置 8 通道多路复用器、可编程增益放大器和数字滤波器最高 30kSPS 采样率。STC15F2K60S2 虽然是 8051 内核但属于 1T 架构在 11.0592MHz 晶振下普通 IO 翻转指令只需要几个时钟周期驱动 SPI 最高跑到 1MHz 以上没有问题。这里先说结论ADS1256 的 SCLK 并不需要飞快10SPS 到 100SPS 的采集场景下1MHz 的软件 SPI 足够消耗每一次 DRDY 事件。很多人会纠结要不要用 STC15 自带的硬件 SPI 模块。从源码实操角度看软件模拟 SPI 更容易控制时序而且 ADS1256 的命令字节和寄存器操作并不复杂手写字节收发函数比配置硬件 SPI 的 CPOL/CPHA 组合更直观。下表给出了这个设计里 STC15F2K60S2 和传统 STC89C52 的差异帮助你判断为什么这个资源选的是 STC15。对比项STC89C52STC15F2K60S2指令周期12T1T常见主频12MHz11.0592MHz / 22.1184MHz软件 SPI 能力勉强可用余量充足RAM256B256B 2KB SRAMFLASH8K 到 64K60KB内部 ADC无10 位但本设计不用注意STC15F2K60S2 内部自带 10 位 ADC但你要做的是 24 位信号链所以外部 ADS1256 才是采样核心。内部的 10 位 ADC 在这里最多用来监控电源电压或者给 LCD1602 背光做亮度调节。2.2 引脚分配与接线方式ADS1256 需要一个晶振常见是 7.68MHz 的无源晶振晶振起振后大概需要几十毫秒稳定。单片机侧的接口只需要 5 根线CS 可以直接接地但为了调试方便源码里通常还是用 IO 控制。以下是一套很常见的接法和源码里的宏定义一致。信号功能ADS1256 引脚STC15F2K60S2 IO说明SCLKSCLKP1.5SPI 时钟软件模拟DINDINP1.6主机输出到 ADCDOUTDOUTP1.7ADC 输出到主机CSCSP1.4片选低有效DRDYDRDYP3.2数据就绪低有效RESETRESETP1.3上电拉高可接 RC 复位注意ADS1256 的数字电源 DVDD 接 5V 时逻辑电平兼容 STC15 的 5V IO不需要电平转换。模拟电源 AVDD 建议单独用 LDO 供电避免 LCD1602 的背光电流在电源上产生毛刺。DOUT 是推挽输出可以直接接 STC15 的准双向 IODRDY 是开漏输出外部需要上拉电阻通常 10kΩ 到 3.3V 或者 5V 都行。2.3 软件 SPI 的字节读写实现ADS1256 手册推荐的 SPI 模式是空闲高电平也就是 CPOL1数据在下降沿被 ADS1256 采样。下面这段代码用 STC15 的位变量模拟这个时序同时处理写命令和读数据。sbit ADS_SCLK P1^5; sbit ADS_DIN P1^6; sbit ADS_DOUT P1^7; sbit ADS_CS P1^4; sbit ADS_DRDY P3^2; void ADS1256_WriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { ADS_SCLK 1; // 回到空闲高电平 if (dat 0x80) ADS_DIN 1; else ADS_DIN 0; // 在下降沿前准备好数据 ADS_SCLK 0; // 下降沿ADS1256 锁存 DIN dat 1; } } unsigned char ADS1256_ReadByte(void) { unsigned char i, val 0; for (i 0; i 8; i) { ADS_SCLK 1; // 上升沿之后 DOUT 稳定 _nop_(); _nop_(); val (val 1) | ADS_DOUT; // 读取一个 bit ADS_SCLK 0; } return val; }这段代码的关键在于读写方向不能搞反。写字节时把 DIN 准备好以后再产生下降沿ADS1256 在 SCLK 下降沿采集输入数据读字节时在 SCLK 上升沿之后延时两个_nop_()再读 DOUT 引脚能避开边沿附近的毛刺。实际的 ADS1256 例程里也有把 SCLK 先拉低再拉高的写法那是因为不同例程对 CPOL 的初始状态理解不同只要保证数据建立时间足够通信都能成功。2.4 控制和状态寄存器速查ADS1256 的寄存器操作比一般 SPI ADC 啰嗦但也没到复杂的地步。常见命令码如下。命令操作码用途RDATA0x01读出一次转换结果占用 24 个 SCLKRREG0x10 (addr1)读取某个寄存器WREG0x50 (addr1)写入某个寄存器SYNC0xFC暂停转换修改 MUX 或增益后使用WAKEUP0xFE唤醒转换器继续工作SELFCAL0xF0执行自校准校准偏移和增益写寄存器时先发送 WREG 命令再发送寄存器数量减一最后发送数据字节。读寄存器则是 RREG 命令后跟着寄存器数量减一然后连续读。ADS1256 有一个很容易踩的细节修改通道 MUX 或 PGA 增益之后不是马上生效而是要让转换器重新同步。常见的做法是先发 SYNC再发几个 NOP最后发 WAKEUP。这个顺序会直接影响你读回来的数据到底属于哪个通道后面的驱动代码里会专门体现。3. 驱动初始化与读取把寄存器顺序变成可复现的代码3.1 初始化函数REESET 后必须等待 DRDYADS1256 的上电过程比较慢晶振起振、内部数字滤波器和调制器建立都需要时间。很多调不通的板子都是因为初始化前没有等够 DRDY 信号。下面是一份标准的初始化流程源码包里的ADS1256_Init()就是按照这个顺序写的。void ADS1256_Init(void) { unsigned char id; DelayMs(50); // 上电稳定等待晶振起振 ADS_CS 0; // 读 STATUS 寄存器最低位 BIT3 为 BUFFER 状态 // ADS1256 的 ID 位在 STATUS 的 BIT6-BIT4读出来应该是 001 id ADS1256_ReadReg(0x00); if ((id 0x70) ! 0x10) // 常见值具体以手册为准 { while (1); // SPI 不通时停在错误状态 } // 写 MUX初始通道 AIN0-AINCOM ADS1256_WriteReg(0x01, ADS1256_MUXP_AIN0 | ADS1256_MUXN_AINCOM); // ADCON 寄存器PGA1CLKOUT 关闭 ADS1256_WriteReg(0x02, 0x00); // DRATE 寄存器0x80 对应 100SPS0x20 对应 10SPS ADS1256_WriteReg(0x03, 0x80); // STATUS 寄存器禁止自动校准手动触发 ADS1256_WriteReg(0x00, 0x00); // 执行自校准校准期间 DRDY 会保持高电平 ADS1256_WriteByte(0xF0); // SELFCAL while (ADS_DRDY); // 校准完成DRDY 拉低 }这段代码里的ADS1256_ReadReg和ADS1256_WriteReg是在字节收发函数的基础上包了一层地址解析就不展开贴了。注意读取 STATUS 寄存器并判断 ID 位并不是必须的但能帮你快速判断 SPI 线有没有接反。如果程序卡死在while(1)最先检查的是 SCLK、DIN、DOUT 三根线是否与标号一一对应以及 DOUT 上是否有 10kΩ 上拉。3.2 单通道读取函数与 MUX 切换实际工程里不会只读一个通道至少会在 AIN0 和 AIN1 之间切。ADS1256 的 MUX 寄存器是由高四位选择正输入低四位选择负输入。源码里的宏ADS1256_MUXP_AIN0和ADS1256_MUXN_AINCOM就是用来拼 MUX 寄存器值的。读取函数必须在发出 RDATA 命令之前确保 MUX 已经切到目标通道。signed long ADS1256ReadData(unsigned char mux) { unsigned char b0, b1, b2; unsigned long raw; while (ADS_DRDY); // 等待上一轮转换完成 ADS_CS 0; ADS1256_WriteReg(0x01, mux); // 切换通道 ADS1256_WriteByte(0xFC); // SYNC停止内部转换 ADS1256_WriteByte(0xFF); // 空读等待 24 个 SCLK ADS1256_WriteByte(0xFE); // WAKEUP重新启动转换 while (ADS_DRDY); // 等待新通道数据就绪 ADS1256_WriteByte(0x01); // RDATA 命令 b0 ADS1256_ReadByte(); // 高位 b1 ADS1256_ReadByte(); // 中位 b2 ADS1256_ReadByte(); // 低位 ADS_CS 1; raw ((unsigned long)b0 16) | ((unsigned long)b1 8) | b2; // 24 位补码转符号数负数时高位补 1 if (raw 0x800000UL) raw | 0xFF000000UL; return (signed long)raw; }这段代码有一点非常重要SYNC 之后不能立刻读数据必须发至少 24 个 SCLK 周期让 ADS1256 内部完成同步。这里的ADS1256_WriteByte(0xFF)就是充当 NOP 指令发送 8 个 SCLK但实际需要 24 个所以严格来说还得再补两个空字节。不少例程偷懒只发一个空字节结果就是每次切换通道后第一帧数据仍然是上一个通道的值。正确做法应当是在 SYNC 后循环发三个0xFF或者发0xFF后再发两次0xFF。源码包里的写法可能略有不同但你在移植时一定要保证有足够多的 SCLK 空脉冲。3.3 增益、基准电压与 0.00059453 系数来源ADS1256 的输出是一个 24 位有符号数范围从 -8388608 到 8388607对应差分输入的负满量程到正满量程。满量程电压由公式决定FullScale ±VREF / PGA当 VREF 是 5VPGA 是 1 时满量程约为 ±5V。每个 LSB 对应电压是VREF / (2^23 - 1) 5 / 8388607 ≈ 0.000596 V源码里注释写的0.00059453并不是理论值而是把 ADS1256 实际测到的基准电压和增益误差校准后的系数。也就是说这块板子的等效满量程约等于 4.988V。这个系数在每块板上都会有小幅差异正确做法是先用万用表量一个接近满量程的直流电压再反推 LSB 系数而不是把源程序里的常数直接复制到所有项目里。后面第五章还会提到用 SELFCAL 校准后如何标定这个系数。4. LCD1602 的 4 位模式接入显示刷新与采样节奏的平衡4.1 4 位并行接口接线LCD1602 可以用 8 位模式但那样要占 11 个 IO在 STC15F2K60S2 上会显得很紧张。4 位模式只使用 D4-D7总共占用 6 个 IO适合和 ADS1256 的 SPI 引脚错开。下面是一套常见接法。LCD1602 引脚STC15F2K60S2 IO说明RSP2.0寄存器选择RWP2.1读写选择接地可简化EP2.2使能脉冲D4P2.4数据位 4D5P2.5数据位 5D6P2.6数据位 6D7P2.7数据位 7RW 引脚可以直接通过一个 10kΩ 电阻接地这样省掉一次 IO 操作也不需要每次写命令前把 RW 拉低。代价是无法读取 LCD 忙标志位只能用延时等待。4 位模式初始化时必须先把 D4-D7 接到高四位不然第一条初始化命令根本发不出去。4.2 4 位模式的初始化和写字符LCD1602 的 4 位初始化序列比 8 位繁琐。上电后要连续发送三次0x03再发送0x02进入 4 位模式最后才设置显示行数和光标。下面这段代码是标准的 C51 写法。sbit LCD_RS P2^0; sbit LCD_RW P2^1; sbit LCD_E P2^2; #define LCD_DATA P2 void LCD_WriteNibble(unsigned char c) { LCD_DATA 0x0F; // 保留低四位 LCD_DATA | (c 0x0F) 4; // 数据放到高四位 LCD_E 1; DelayMs(1); LCD_E 0; } void LCD_WriteByte(unsigned char dat) { LCD_RS 1; // 数据模式 LCD_WriteNibble(dat 4); LCD_WriteNibble(dat); DelayMs(1); } void LCD_Init(void) { DelayMs(15); LCD_WriteNibble(0x03); DelayMs(5); LCD_WriteNibble(0x03); DelayMs(5); LCD_WriteNibble(0x03); LCD_WriteNibble(0x02); // 进入 4 位模式 LCD_RS 0; // 命令模式 LCD_WriteNibble(0x02); // 4 位2 行5x7 字库 LCD_WriteNibble(0x08); LCD_WriteNibble(0x00); // 关显示 LCD_WriteNibble(0x0C); LCD_WriteNibble(0x00); // 清屏 LCD_WriteNibble(0x01); LCD_WriteNibble(0x00); // 光标右移 LCD_WriteNibble(0x06); }LCD_WriteNibble函数里的关键点是LCD_DATA 0x0F因为 LCD 的 D4-D7 接在 P2 口的高四位如果不清掉高四位上一次的旧数据会残留在 D4-D7 上导致写错命令。源码包里如果用的是P2 (P2 0x0F) | (x4)效果完全相同。4.3 把电压值转换成可显示的字符串ADS1256 读出来的原始值是 signed long 类型直接显示会看到一长串数字。更实用的做法是先把换算后的电压值放大成整数再按位转 ASCII 字符。比如要显示到小数点后四位可以先把电压乘以 10000 转成 int然后拆出每一位。void DisplayVoltage(float volts) { int value; unsigned char i; unsigned char buf[6]; if (volts 0) { LCD_WriteChar(0x00, -); volts -volts; } else { LCD_WriteChar(0x00, ); } value (int)(volts * 10000.0); // 保留 4 位小数 buf[0] value / 10000 0; // 个位 buf[1] .; buf[2] value % 10000 / 1000 0; buf[3] value % 1000 / 100 0; buf[4] value % 100 / 10 0; buf[5] value % 10 0; for (i 0; i 6; i) { LCD_WriteChar(0x01 i, buf[i]); } }这段代码没有用sprintf原因很简单Keil C51 的sprintf会引入较大的代码体积而且浮点格式化需要额外处理。手动拆分虽然看起来笨但在 51 上运行效率反而更高。显示负数时只占一个字符位置所以正数前面加一个空格保持整数位对齐。4.4 显示刷新与 ADC 采样的时序协调把 LCD1602 和 ADS1256 放在同一个主循环里容易出现一个隐蔽问题LCD 写命令和写数据时都是关中断的如果此时 ADS1256 的 DRDY 刚好拉低你晚了几十微秒去读数据并不会丢失因为 ADS1256 会在内部保持当前转换结果直到你真正发出 RDATA 命令。真正要避免的是在 LCD 写数据的过程中去执行while(ADS_DRDY)等待那会造成 LCD 写时序被拉长液晶显示出现毛刺。常见的处理办法有几种第一种是在主循环里先扫描 ADS1256 是否准备好如果准备好了再切换通道和读取第二种是给 LCD 的写操作加上一个忙标志位在 LCD 写数据期间不允许进入 ADS1256 读取流程。源码包里一般不会有复杂的状态机但你在改成 Modbus 数据上传或者多通道采集时一定要把这两个设备当作两个互不阻塞的外设。5. 调试 ADS1256 读数从异常值到稳定数据的排查顺序5.1 读回 0xFFFFFF 或 0x000000 的排查方向如果读回来的数据一直正满码或者负满码首先要排除的是输入超出量程。ADS1256 是差分输入AIN0 对 AINCOM 的电压差如果超过 ±5V输出就会被钳位在满量程。其次要检查 MUX 寄存器是不是把正输入和负输入接反了常见错误是把ADS1256_MUXP_AIN0和ADS1256_MUXN_AINCOM当成两个独立位直接拼接但实际上高位是正输入低位是负输入组合值要按寄存器文档去对照。如果读回来的是全 0 或者完全随机的大数大概率是 SPI 字节顺序没有对齐。ADS1256 的数据输出是高字节在前而很多 51 例程在读取时会把第一个字节当成低字节导致最终的 raw 值完全错乱。调试时先固定输入一个已知电压用示波器看 DOUT 输出的 24 位数据核对最高位是不是符号位。5.2 利用 SELFCAL 和系统校准消除偏移ADS1256 的偏移误差主要来自输入多路复用器和放大器的失调电压通过SELFCAL可以自动校准。初始化时已经执行过ADS1256_WriteByte(0xF0)但这只能消除内部偏移。如果你想做系统级校准可以先把 IN 引脚短接到 AINCOM读取一个偏移值然后把实际电压值减去该偏移。这个偏移原始值通常只在几十到几百个 LSB但要在微伏级别测量时不能忽视。校准系数0.00059453也应该在系统校准之后重新计算。常见做法是用一个高精度的 3.000V 基准源接到 AIN0读取原始码值 N那么实际系数就是3.000 / N。这样得到的系数比直接套公式更准而且能够把基准电压源和 ADS1256 的内部增益误差一起覆盖掉。5.3 采样率、数字滤波和长线布局的联合处理把 ADS1256 的 DRATE 寄存器从 100SPS 降到 10SPS读回来的噪声会明显减小但这个代价是响应变慢。如果只是测缓慢变化的电池电压10SPS 完全够如果要做高速数据采集就不要在模拟前端省抗混叠滤波电容。ADS1256 内部本身有数字滤波器能抑制 50Hz 工频干扰但前提是采样率选择要匹配10SPS、50SPS、60SPS 这些档位对工频衰减效果最好。对于长线连接DOUT 和 SCLK 必须尽量短最好使用杜邦线时缠绕在一起或者直接在 PCB 上走底层。ADS1256 的时钟引脚附近不要走高噪声信号。如果发现读到的数在某个固定值附近上下跳先在 AVDD 和 AVSS 之间加一个 10µF 钽电容和 0.1µF 陶瓷电容再检查 AIN 引脚的源阻抗源阻抗过高会导致输入采样电容充不满表现为数值随采样率变化而漂移。最后还有一个非常实用的技巧在使用ADS1256ReadData时对同一通道连续读 8 次去掉最大值和最小值后取平均能有效抑制由 DRDY 抖动引入的单次异常值。这个操作在 51 上不会增加太多时间开销却能让 LCD1602 上显示的最后一位稳定下来。本文还有配套的精品资源点击获取