外设驱动库开发笔记41:ADS1256 ADC驱动
我们经常会碰到多通道AD采集的需求,有时候甚至需要高精度的ADC器件。本篇我们将来设计并实现ADS1256模数转换器的驱动。并简单讨论该驱动使用方式。
1、功能概述
ADS1256是TI公司推出的一款低噪声高分辨率的24位Sigma-Delta(E-v)模数转换器(ADC)。E-vADC与传统的逐次逼近型和积分型ADC相比有转换误差小而价格低廉的优点,但由于受带宽和有效采样率的限制,E-vADC不适用于高频数据采集的场合。该款ADS1256可适合于采集最高频率只有几千赫兹的模拟数据的系统中,数据输出速率最高可为30K采样点/秒,4路差分或8路伪差分输入,有完善的自校正和系统校正系统,SPI串行数据传输接口。其结构图如下所示:
从结构图可以看出来,ADS1256是模拟区域与数字区域完全独立的ADC,即AVDD给模拟区域供电,DVDD给数字区域供电,在原理图设计方面按照官方指导文档,需要对两个区域做独立的布线与隔离处理,才能让信噪比最佳。
ADS1256采用SSOP的封装形式,具有8个模拟输入通道,共28个引脚,与其类似的2通道产品ADS1255共有20引脚,其实两者操作相同,所以我们设计驱动也会考虑兼容性。其中ADS1256引脚排布和定义如下图所示:
ADS1255和ADS1256的操作是通过一组寄存器来控制的。这些寄存器包含了配置部件所需的所有信息,如数据速率、多路复用器设置、PGA设置、校准等。这些寄存器的地址及结构如下表所述:
我们知道了这些寄存器的定义,那么就可以操作ADS1256了。可是我们怎么来实现对这些寄存器的访问呢?这就涉及到操作命令的问题了。ADS1256有多个操作命令,具体如下表所示:
在以上这些命令中,除了读写寄存器操作需要有第二个字节命令和数据外,其它命令都是独立使用的。
2、驱动设计与实现
我们已经了解了ADS1256的相关结构、寄存器及操作命令。接下来我们就来考虑如何设计ADS1256的驱动程序。
2.1、对象定义
与以往一样,我们依然是基于对象来实现ADS1256的驱动程序,所以我们需要抽象出ADS1256的对象类型。
2.1.1、抽象数据类型
我们先来考虑一下ADS1256对象类型的定义问题。一个对象一般来说主要包括属性和操作两个方面的内容,我们也从这两个方面来分析ADS1256对象。
首先我们来考虑ADS1256模数转换器对象的属性。这些属性必须能够标识ADS1256模数转换器对象的特征,或者是存储ADS1256模数转换器对象的某种状态。对于ADS1256模数转换器对象我们希望可以记录寄存器的状态,所以我们将各个寄存器定义为该对象的属性。
接下来我们再来考虑一下ADS1256模数转换器对象的操作。一个对象有各种各样的操作,或者说他能实现很多的操作,但不是所有的操作都是我们要提取的。我们需要考虑的是那些对象所独有并且同类对象都必不可少的操作,以及那些不能由对象独自完成,依赖于具体平台但又决定对象的行为的必要操作。对于ADS1256莫数转换器,我们需要读写数据,操作片选信号,读取就绪信号等。但这些操作都依赖于具体的软硬件平台,我们将这些操作定义对象的操作,通过函数指针的方式将具体的操作函数传递给对象变量,以便于适用于不同的软硬件平台。此外,由于时序控制的需要我们需要在驱动中使用延时操作,而延时操作的实现依赖于具体的软硬件平台,所以我们也将其抽象为对象的操作。根据上述我们的分析,可以定义ADS1256模数转换器对象的类型如下:
/*定义ADS1256对象类型*/
typedef struct ADS1256Object {
uint8_t Register[11];
void (*ReadWrite)(uint8_t *wData,uint8_t *rData,uint16_t size); //实现读写操作
void (*ChipSelect)(ADS1256CSType cs); //实现片选
uint16_t (*GetReadyInput)(void); //实现Ready状态监视
void (*Delay)(volatile uint32_t nTime); //实现ms延时操作
}ADS1256ObjectType;
2.1.2、对象初始化函数
我们抽象了ADS1256模数转换器的对象类型,使用这一对象类型我们可以获得具体的对象变量,但这一对象变量必须要进行必要的属性和操作设定才能进行正确的操作。为了完成对象变量属性和操作的配置,我们需要一个对象初始化函数。
/*ADS1256初始化配置函数*/
void ADS1256Initialization(ADS1256ObjectType *ads, //待初始化的ADS1256对象
ADS1256OrderType order, //数据顺序
ADS1256ACALType acal, //自动校准使能
ADS1256BufenType bufEn, //模拟量缓存使能
ADS1256ClkoutType clkOut, //时钟输出类型
ADS1256SDCSType sdcs, //传感器检测电流
ADS1256GainType gain, //增益
ADS1256DRateType dataRate, //数据输出速率
ADS1256DIOType *dio, //输入输出配置
ADS1256ReadWriteType readWrite, //读写函数指针
ADS1256ChipSelectType cs, //片选函数指针
ADS1256GetReadyInputType ready, //就绪函数指针
ADS1256DelaymsType delayms //毫秒延时函数指针
)
{
uint8_t Order[]={STATUS_ORDER_MOST,STATUS_ORDER_LEAST};
uint8_t ACAL[]={STATUS_ACAL_DISABLE,STATUS_ACAL_ENABLE};
uint8_t BUFEN[]={STATUS_BUFEN_DISABLE,STATUS_BUFEN_ENABLE};
uint8_t clkck[]={ADCON_CLOCK_OFF,ADCON_CLOCK_FCLKIN,ADCON_CLOCK_HALF,ADCON_CLOCK_QUARTER};
uint8_t sDCS[]={ADCON_SDCS_OFF,ADCON_SDCS_05uS,ADCON_SDCS_2uS,ADCON_SDCS_10uS};
uint8_t gains[]={ADCON_PGA_GAIN1,ADCON_PGA_GAIN2,ADCON_PGA_GAIN4,ADCON_PGA_GAIN8,
ADCON_PGA_GAIN16,ADCON_PGA_GAIN32,ADCON_PGA_GAIN64};
uint8_t dRate[]={DRATE_30000SPS,DRATE_15000SPS,DRATE_7500SPS,DRATE_3750SPS,
DRATE_2000SPS,DRATE_1000SPS,DRATE_500SPS,DRATE_100SPS,
DRATE_60SPS,DRATE_50SPS,DRATE_30SPS,DRATE_25SPS,
DRATE_15SPS,DRATE_10SPS,DRATE_5SPS,DRATE_2_5SPS};
uint8_t dir[4][2]={{GPIO_DIR0_OUTPUT,GPIO_DIR0_INPUT},
{GPIO_DIR1_OUTPUT,GPIO_DIR1_INPUT},
{GPIO_DIR2_OUTPUT,GPIO_DIR2_INPUT},
{GPIO_DIR3_OUTPUT,GPIO_DIR3_INPUT}};
if((ads==NULL)||(readWrite==NULL)||(ready==NULL)||(delayms==NULL))
{
return;
}
ads->ReadWrite=readWrite;
ads->GetReadyInput=ready;
ads->Delay=delayms;
if(cs==NULL)
{
ads->ChipSelect=ADS1256ChipSelect;
}
else
{
ads->ChipSelect=cs;
}
for(int i=0; i<11;i++)
{
ads->Register[i]=0x00;
}
ads->Register[REG_STATUS]=Order[order]||ACAL[acal]||BUFEN[bufEn];
ads->Register[REG_MUX]=0x00;
ads->Register[REG_ADCON]=clkck[clkOut]||sDCS[sdcs]||gains[gain];
ads->Register[REG_DRATE]=dRate[dataRate];
ads->Register[REG_IO]=dir[0][dio[0]]||dir[1][dio[1]]||dir[2][dio[2]]||dir[3][dio[3]];
WriteADS1256Register(ads,REG_STATUS,1);
WriteADS1256Register(ads,REG_MUX,1);
WriteADS1256Register(ads,REG_ADCON,1);
WriteADS1256Register(ads,REG_DRATE,1);
WriteADS1256Register(ads,REG_IO,1);
ADS1256Calibration(ads,SELFCAL);
ReadADS1256Register(ads,REG_STATUS,11);
}
在这个初始化函数中,我们完成两个方面的内容:一是对属性的赋值和对操作函数指针进行初始化;二是对对象变量所代表的对象进行初始化配置。
2.2、对象操作
我们获得了ADS1256模数转换器的对象类型,也编写了初始化对象变量的函数,接下来我们考虑一下ADS1256模数转换器的一些主要的操作过程。
2.2.1、读数据
作为模数转换器,我们首要的目的就是从其获得我们想要的数据。ADS1256读数据分为连续读取和非连续读取,在这里我们考虑单次读取数据的操作。当模数转换完成后,就绪信号下拉到“0”,这个时候可以读取数据,数据读取后就绪信号将上拉到“1”。
根据上述描述和时序图我们可以编写读取数据的操作如下:
/*ADS1256读取数据*/
static uint32_t ADS1256ReadData(ADS1256ObjectType *ads)
{
uint8_t cmd[1]={RDATA};
uint8_t rData[3];
uint32_t result=0;
while(ads->GetReadyInput()==1);
ads->ChipSelect(ADS1256CS_Enable);
ads->ReadWrite(cmd,rData,3);
ads->ChipSelect(ADS1256CS_Disable);
result=rData[0];
result=(result<<8)+rData[1];
result=(result<<8)+rData[2];
return result;
}
2.2.2、读寄存器
我们已经了解ADS1256有11个寄存器,这些寄存器都可读取。读取寄存器的命令由2个字节组成。第一个字节是读寄存器命令0x10与寄存器起始地址合并而成。第二个字节是所要读取的寄存器数量减1。具体的操作时序如下图所示:
根据上述对读寄存器的描述和时序图我们可以编写读寄存器的操作如下:
/*读ADS1256寄存器*/
static void ReadADS1256Register(ADS1256ObjectType *ads,uint8_t regAddr,uint8_t regNum)
{
uint8_t cmd[2];
uint8_t rData[11];
cmd[0]=RREG|regAddr;
cmd[1]=regNum-1;
ads->ChipSelect(ADS1256CS_Enable);
ads->ReadWrite(cmd,rData,2);
cmd[0]=0;
cmd[1]=0;
ads->ReadWrite(cmd,rData,regNum);
ads->ChipSelect(ADS1256CS_Disable);
for(int i=0;i<regNum;i++)
{
ads->Register[regAddr+i]=rData[i];
}
}
2.2.3、写寄存器
在ADS1256的11个寄存器中有一些寄存器用于配置ADS1256的工作特性,可以写这些寄存器。写寄存器的命令也是由2个自己组成。第一个字节是读寄存器命令0x50与寄存器起始地址合并而成。第二个字节是所要写的寄存器数量减1。具体的操作时序如下图所示:
根据上述对写寄存器的描述和时序图我们可以编写写寄存器的操作如下:
/*写ADS1256寄存器*/
static void WriteADS1256Register(ADS1256ObjectType *ads,uint8_t regAddr,uint8_t regNum)
{
uint8_t wData[7];
uint16_t index=0;
uint8_t rData[2];
wData[index++]=WREG|regAddr;
wData[index++]=regNum-1;
for(int i=0;i<regNum;i++)
{
wData[index++]=ads->Register[regAddr+i];
}
ads->ChipSelect(ADS1256CS_Enable);
ads->ReadWrite(wData,rData,index);
ads->ChipSelect(ADS1256CS_Disable);
}
3、驱动的使用
我们已经设计并实现了ADS1256模数转换器的驱动程序。在这一节中我们将设计一个简单的例子,通过这个例子我们将简单的验证驱动程序的正确性,并简要说明驱动的使用方法。
3.1、声明并初始化对象
我们为ADS1256模数转换器设计的驱动是基于对象开发的,所以我们在使用驱动之前需要声明一个ADS1256模数转换器对象变量。使用我们前面定义的ADS1256模数转换器对象类型声明这一变量如下:
ADS1256ObjectType ads1256;
声明了这个对象变量后,我们还需要使用对象初始化函数来将这个对象变量变量进行初始化。我们设计的初始换函数有多个参数:
ADS1256ObjectType *ads, //待初始化的ADS1256对象
ADS1256OrderType order, //数据顺序
ADS1256ACALType acal, //自动校准使能
ADS1256BufenType bufEn, //模拟量缓存使能
ADS1256ClkoutType clkOut, //时钟输出类型
ADS1256SDCSType sdcs, //传感器检测电流
ADS1256GainType gain, //增益
ADS1256DRateType dataRate, //数据输出速率
ADS1256DIOType *dio, //输入输出配置
ADS1256ReadWriteType readWrite, //读写函数指针
ADS1256ChipSelectType cs, //片选函数指针
ADS1256GetReadyInputType ready, //就绪函数指针
ADS1256DelaymsType delayms //毫秒延时函数指针
这些参数中,ADS1256对象我们已经声明。数据顺序、自动校准使能、模拟量缓存使能、时钟输出类型、传感器检测电流、增益、数据输出速率等几个参数均为枚举量,我们们根据需要选择就可,在这里我们均按默认值选择。输入输出配置这个参数,因有4个IO需独立配置,所以我们定义一个数组,并将其传入。剩下的几个函数指针其原型定义如下:
/*定义读写操作函数指针类型*/
typedef void (*ADS1256ReadWriteType)(uint8_t *wData,uint8_t *rData,uint16_t size);
/*实现片选*/
typedef void (*ADS1256ChipSelectType)(ADS1256CSType cs);
/*实现Ready状态监视*/
typedef uint16_t (*ADS1256GetReadyInputType)(void);
/*实现ms延时操作*/
typedef void (*ADS1256DelaymsType)(volatile uint32_t nTime);
我们需要根据函数的原型声明来结合具体的软硬件平台设计这几个函数,并将函数指针以参数的形式传递给初始函数。我们是在STM32平台来实现这个示例,所以延时函数我们直接采用HAL_Delay即可,其他几个函数实现如下:
/*定义片选信号函数*/
void ADS1256CS(ADS1256CSType en)
{
if(ADS1256CS_Enable==en)
{
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_4, GPIO_PIN_RESET);
}
else
{
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_4, GPIO_PIN_SET);
}
}
/* 定义就绪信号读取函数 */
uint16_t ADS1256CheckReady(void)
{
return HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_0);
}
/*定义发送数据函数*/
void ADS1256WriteReadData(uint8_t *wData,uint8_t *rData,uint16_t size)
{
HAL_SPI_TransmitReceive(&ads1256hspi,wData,rxData,size,1000);
}
根据上述这些定义后,我们可以调用初始化函数来实现对ADS1256对象变量进行初始化。具体如下:
ADS1256DIOType dio[4]={ADS1256_DIO_INPUT,ADS1256_DIO_INPUT,ADS1256_DIO_INPUT,ADS1256_DIO_OUTPUT};
/*ADS1256初始化配置函数*/
ADS1256Initialization(&ads1256, //待初始化的ADS1256对象
ADS1256_ORDER_MOST, //数据顺序
ADS1256_ACAL_DISABLE, //自动校准使能
ADS1256_BUFEN_DISABLE, //模拟量缓存使能
ADS1256_CLKOUT_FCLKIN, //时钟输出类型
ADS1256_SDCS_OFF, //传感器检测电流
ADS1256_GAIN1, //增益
ADS1256_DRATE_30000SPS, //数据输出速率
dio, //输入输出配置
ADS1256WriteReadData, //读写函数指针
ADS1256CS, //片选函数指针
ADS1256CheckReady, //就绪函数指针
HAL_Delay //毫秒延时函数指针
);
3.2、基于对象进行操作
在完成对象变量的初始化后,我们就可以通过操作这个对象变量获取采集的数据。这里我们采集8路单端输入的数据。需要注意的是每次读出来的数据并非我们当前设定的通道的数据,而是我们上次设定的通道的数据。据此我们设计简单的数据采集函数如下:
/*获取通道数据*/
void GetADS1256ChannelValue(void)
{
int32_t dataCode[8];
for(ADS1256ChannelType ainP=ADS1256_AIN0;ainP<ADS1256_AINCOM;ainP++)
{
if(ainP==ADS1256_AIN0)
{
dataCode[7]=ADS1256SingleReadData(&ads1256,ainP,ADS1256_AINCOM);
}
else
{
dataCode[ainP]=ADS1256SingleReadData(&ads1256,ainP,ADS1256_AINCOM);
}
}
}
4、应用总结
我们设计了ADS1256模数转换器的驱动程序,并利用一个简单的例子对齐进行了验证,读取数据没有问题。
在使用驱动程序时需要注意,片选信号并非必须实现。因为有些时候我们可能需要在硬件上直接将其选中,此时添加片选操作函数是没有什么意义的,我们可以在初始化时传入NULL来完成。
在使用驱动程序时需要注意,ADS1256模数转换器在设置通道选择然后就可以在转换过程中读取上一个转换周期的数据。所以在驱动程序中,设置通道选择后,没有等待转化完成,而是直接读取了数据,这个数据实际上是上一次转换的数据,这样可以充分利用转换周期。所以在使用驱动程序需要注意读取的数据所对应的通道。
欢迎关注:
如果阅读这篇文章让您略有所得,还请点击下方的【好文要顶】按钮。
当然,如果您想及时了解我的博客更新,不妨点击下方的【关注我】按钮。
如果您希望更方便且及时的阅读相关文章,也可以扫描上方二维码关注我的微信公众号【木南创智】