集成式单片机外部模块驱动编写详解——AD5689为例

集成式单片机外部模块驱动编写详解——AD5689为例

具体的代码和例程请参照以下GitHub仓库,记得给我star哦!
https://github.com/xdu-zhangfan/drivers

集成式驱动原理

集成式驱动,亦即将驱动文件作为一个单独的模块,将具体的期间抽象到抽象的文件集合。
——沃兹基硕德(1970.1.1-?),当代面向HelloWorld编程宗师级人物

上面这段话看上去很高级,其实说人话就是:写个驱动,让“我操作具体的器件”变成“用几个函数操作一个函数族”。如果把这几个函数全部写在一个文件里面,加一定的标志体现他们的同一性(比如一个共同的函数前缀),这就叫函数族。在操作这个函数族时候,就相当于在操作具体的器件。

那么,为什么要用集成式驱动,或者说集成式驱动有什么好处?以下主要从两方面谈一谈这个问题:集成式驱动更加贴近具体的嵌入式环境、集成式驱动可以实现更好地控制连续性。

集成式驱动更加贴近具体的嵌入式环境。在一般的嵌入式开发之中,一般来说每种模块都只用一个。如果在这个时候仍然使用对象式驱动,有一点点说不过去。

集成式驱动可以实现更好地控制连续性。在具体的单片机运行环境之中,程序的控制流总要时不时转移一下:或者是DMA缓冲区满了,或者是有个定时器每秒中断一次……但是在程序的控制流发生转移的时候,集成式驱动内部的变量值不会发生改变。换一句学术一点的话来说就是:集成式驱动的内部状态和运行上下文不会发生改变。那么,控制流在每一次发生转移的时候,都会有一个已经设定好的环境,供控制流操作外部模块。

AD5689基本介绍

ADI官方对这个产品的定位是:

The AD5689/AD5687 members of the nanoDAC+™ family are low power, dual, 16-/12-bit, buffered voltage output digital-to-analog converters (DACs). The devices include a gain select pin giving a full-scale output of 2.5 V (gain = 1) or 5 V (gain = 2). The AD5689/AD5687 operate from a single 2.7 V to 5.5 V supply, are guaranteed monotonic by design, and exhibit less than 0.1% FSR gain error and 1.5 mV offset error performance. Both devices are available in a 3 mm × 3 mm LFCSP and a TSSOP package.
——ADI官方手册

看不懂的同志们可以参考一下百度翻译的结果:

纳米DAC的AD5689/AD5687成员+™ 该系列是低功耗、双、16/12位、缓冲电压输出数模转换器(DAC)。这些器件包括一个增益选择引脚,提供2.5 V(增益=1)或5 V(增益=2)的满刻度输出。AD5689/AD5687在单个2.7V至5.5V电源范围内工作,设计保证单调,并表现出小于0.1%的FSR增益误差和1.5mV的偏移误差性能。这两种设备都有3毫米×3毫米LFCSP和TSSOP封装。
——百度翻译ADI官方手册

简而言之,这就是一个高级一点、体积小一点的16位DAC(我这个是AD5689,如果是AD5687的话就是12位)。那就直接放时序图:

写时序图

菊花链时序图

回读时序图

AD5689的时序图相对比较复杂,但是我们不需要一点点看。同样地,DAC的根本任务是把一个数字值转换成模拟信号。那么,首要任务还是把值送到DAC模块里面。

明显,这就是一个SYNC(FPGA)兼容SPI(MCU)的协议。SCLK对应SPI的时钟线,在空闲的时候(也就是DIN没有数据的时候),SCLK保持高电平。由此可见,SPI协议的时钟极性应该为高电平(CPOL=1)。同样地,在SYNC拉低之后,DAC8552在时钟的下降沿进行数据采样(如图所示,DIN应该在SCLK下降沿的时候保持稳定),因此实在第一个时钟沿进行采样(CPHA=0)。

因此,纵观三幅时序图,我们可以获得DAC5689的基本时序要素:使用SPI,时钟极性为1(高电平),时钟相位为0(第一个边沿),24位数据,MSB First

观察Figure-2,这个图阐明了写命令的时序。这个图相对于菊花链和回读的时序多了LDACRESET两个信号引脚。这两个引脚会在之后驱动编写的时候再慢慢说。

观察Figure-4,这个图是菊花链时序图,先不管这个东西。

观察Figure-5,这个图是回读的时序图。这个图的意思其实是:当发送了一次回读指令的时候,需要隔一段时间发送一次空白数据(即NOP CONDITION),为SDO提供时钟。SDO会在主机发送空白数据时返回数据。

继续上图:

指令结构

可以看到,在Figure-38Figre-39中,分别给出了AD5689和AD5687的SPI数据位含义。在COMMAND BITSADDRESS BITS这两部分来说,这两个器件并没有什么不同;但是AD5687DATA BITS的低四位时不考虑的。也就是说,AD5689的驱动也可以用于AD5687,只是数据步进变成了2的4次方,有效位数也变成了12位。

命令列表

Table-9给出了四位指令的具体数据。现在挑几个重要的数值讲一讲。0000代表了空操作,应该是在回读的时候,给SDO信号提供时钟用的。0001代表了写寄存器,至于写那个寄存器,则参照ADDRESS BITS:如果ADDRESS BITS的第0位是1,则将值写入到DAC的通道A;如果ADDRESS BITS的第3位是1,则将值写入到DAC的通道B。由此可见,通道A和通道B应该可以被同时写入同一个值。

但是我们还要看一看这两个表:

写命令详情

Table-15中,将寄存器分为了输入寄存去和DAC寄存器。如果命令为0001,则只将数据写到输入寄存器之中;如果命令为0010,则将输入寄存器的数据更新到DAC寄存器(也就是改变DAC的输出电压)。也就是说,在0001指令之后,必须跟上一条0010指令,才能完成一次DAC输出电压更新。

但是,如果命令指令为0011,则会直接值更新到DAC寄存去,这就说明了,我们的驱动大概率只用这一条命令就够了,因为目前并没有先写入再择机更新的需求(既然都不用立刻更新,那就不用立刻写,写的时候更新就行了,反正耗费的时间都是一样的)。

但是,Table-14中也提供了一种不用拉低LDAC信号引脚的方法:通过指令0101设置LDAC寄存器。这样的话,可以不用在每一次写数据的时候都要拉低LDAC信号引脚,而是在全部写数据操作之前,先发送这个指令把LDAC长期拉低。那么,无论写什么数据,都不用管LDAC的状态了,直接写就可以了。这样可以给单片机省下一个LDAC引脚。

另外,AD5689还提供了Power Down模式的指令0100和软件复位指令0110。但是一般这些模式都不会用到。

AD5689驱动抽象及源码解释

如果要实现一个简单的驱动程序(也就是把值给到单片机),其实我们只需要关注LDAC引脚和0011指令就可以了。但是既然模块上都有LDACRESET两个引脚,其实我们也可以在单片机上也开启两个引脚的输出模式,分别对应LDACRESET

在进行器件的初始化操作的时候,不仅要对芯片进行初始化,也要对驱动程序进行初始化。具体就是要对驱动程序的一些变量进行赋值,包括以下的变量:

// ad5689.c
// configure spi interface 
static SPI_HandleTypeDef *ad5689_hspi;

// LDAC GPIO 
static GPIO_TypeDef *ad5689_ldac_gpio_group;
static uint16_t ad5689_ldac_gpio_pin;

// RESET GPIO
static GPIO_TypeDef *ad5689_rst_gpio_group;
static uint16_t ad5689_rst_gpio_pin;

在这一段代码中,分别定义了SPI的HAL接口、LDACRESETIO grouppin。这些变量是驱动程序进行所有操作的基础。

在这些变量的声明中,都使用了static前缀。这个前缀在C语言中,如果用在函数定义外的函数或者变量声明之中,是指“被声明的函数或者全局变量只能在本文件内被调用”,这个特性可以用于保护本文件内的局部变量和局部函数不被错误地外部调用,也可以避免不同文件的函数名或者变量名冲突;如果用在函数定义之内的变量声明之中,是指“被定义的局部变量被静态储存”,也就是说,在函数结束运行之后,这个局部变量的值还是被储存在内存空间中,而不会被堆栈回收。这样,在下一次运行这个函数的时候,这个变量的值还是上一次运行这个函数的时候的最后一次赋值时所获得的值。这个特性可以被单函数用作状态机编写。

至于为什么要用HAL库,因为这个是ST官方一直在更新的固件库,对于H7这种高性能单片机而言,相比与稳定性,获取最新更新的特性还是很重要的。而且,因为AD5689的通信时序兼容SPI,直接使用HAL库之中的SPI和GPIO定义可以降低底层函数编程难度。

然后就是初始化代码:

int ad5689_init(
    //Configure SPI interface
    SPI_HandleTypeDef *ad5689_hspi_i,
    
    //LDAC GPIO 
    GPIO_TypeDef *ad5689_ldac_gpio_group_i,
    uint16_t ad5689_ldac_gpio_pin_i,

    //RESET GPIO 
    GPIO_TypeDef  *ad5689_rst_gpio_group_i,
    uint16_t ad5689_rst_gpio_pin_i

)
{
    //configure values
    ad5689_hspi = ad5689_hspi_i;


    ad5689_ldac_gpio_group = ad5689_ldac_gpio_group_i;
    ad5689_ldac_gpio_pin = ad5689_ldac_gpio_pin_i;

    ad5689_rst_gpio_group = ad5689_rst_gpio_group_i;
    ad5689_rst_gpio_pin = ad5689_rst_gpio_pin_i;

    //Initialize ad5689
    ad5689_set_pins(AD5689_PIN_RST | AD5689_PIN_LDAC);
    HAL_Delay(1);
    ad5689_clr_pins(AD5689_PIN_RST);
    HAL_Delay(1);
    ad5689_set_pins(AD5689_PIN_RST);

    return 0;
}

这段初始化代码主要解决两个问题:设置驱动程序的变量和进行芯片的初始化。这个集成式驱动程序的变量主要包括SPI和引脚两大部分(虽然引脚不是必须的,但也需要进行实现)。ad5689_init首先将HAL库的SPI描述符和LDACRESET两个引脚存入内部变量,然后拉高LDACRESET(数据手册中,空闲状态下这两个引脚就是高电平)。在拉高1ms过后,对RESET引脚进行1ms的拉低,进行芯片的初始化。

为何在这里不进行软件初始化,而进行硬件的初始化?在IC上电的时候,内部状态一般默认是不确定的;只有在进行一次复位之后,IC内部的所有状态才会回复到初始默认值。虽然很多现代芯片已经可以做到上电初始化,但是这个“古老”的习惯还是被保留了下来。

在初始化代码之后,AD5689就已经进入了正式工作的状态。这时候就可以给AD5689发送数据和指令了。同样地,先实现几个底层封装函数:

// ad5689.c
static uint32_t ad5689_send_recv_data(uint32_t tx_data)
{
    uint32_t rx_data ;
    HAL_SPI_TransmitReceive( ad5689_hspi, (uint8_t *)&tx_data, (uint8_t *)&rx_data, 1,HAL_MAX_DELAY);
    return rx_data ;
    
}

static int ad5689_set_pins(uint16_t pins)
{
    if (HAL_IS_BIT_SET(pins ,AD5689_PIN_LDAC))
{
        HAL_GPIO_WritePin(ad5689_ldac_gpio_group ,ad5689_ldac_gpio_pin, 1);
}
    if (HAL_IS_BIT_SET(pins ,AD5689_PIN_RST))
    {
        HAL_GPIO_WritePin (ad5689_rst_gpio_group ,ad5689_rst_gpio_pin, 1);

    }
    return 0;

}

static int ad5689_clr_pins(uint16_t pins)
{
    if (HAL_IS_BIT_SET(pins, AD5689_PIN_LDAC))
    {
        HAL_GPIO_WritePin(ad5689_ldac_gpio_group , ad5689_ldac_gpio_pin ,0);
    }
    if (HAL_IS_BIT_SET(pins,AD5689_PIN_LDAC))
    {
        HAL_GPIO_WritePin(ad5689_rst_gpio_group, ad5689_rst_gpio_pin,0);
    }
    
    return 0;
}

首先,为了进行调用保护和消除命名冲突,这几个函数都使用了static前缀,以确保只有本文件内的函数能够调用底层封装函数。其次,这几个函数分别封装了SPI操作和GPIO操作,如果需要进行驱动移植的话,可以只修改这几个函数和上面变量定义的内容即可。

SPI封装函数还是中规中矩,实现收发功能即可;GPIO操作函数采用掩码形式,用两个函数分别实现置1(ad5689_set_pins)和置0(ad5689_clr_pins)以下是掩码的宏定义:

// ad5689.h
#define AD5689_PIN_LDAC 0X1
#define AD5689_PIN_RST 0X2

可见,AD5689_PIN_LDAC其实是0001AD5689_PIN_RST其实是0010。在两个GPIO的操作中,用HAL库的HAL_IS_BIT_SET宏检测这两个位是否被设置,如果被设置就进行目标GPIO的置位或者清除置位。

这里有一个小缺陷:如果不用HAL库的话,HAL_IS_BIT_SET宏是无法使用的,这时候就需要自己写一个HAL_IS_BIT_SET宏。一般来说,这种问题是不应该出现的,因为“检测一个位是否被置位”应该是一个算法相关操作,而“使用HAL库宏函数”的行为是一种底层相关操作,底层相关操作和算法相关操作一定要严格分开。从这个角度出发,我应该自己写一个AD5689_IS_BIT_SET宏代替HAL_IS_BIT_SET宏,而不是“拿来主义”用HAL库的宏函数。

在进行完底层操作封装之后,需要再进行一下命令封装:

// ad5689.c
static uint32_t ad5689_send_cmd(uint8_t cmd , uint8_t addr , uint16_t data)
{
    uint32_t send_data =(((uint32_t)cmd) <<(16+4)) | (((uint32_t)addr)<<16) | data;
    return ad5689_send_recv_data(send_data);
}

为什么还要搞这么多花里胡哨的东西,又进行底层封装又进行命令层封装?这里其实是借鉴了OSI(七层网络模型)的设计思想:

OSI的上面四层(应用层、表示层、会话层、传输层)为高层,为应用程序服务;下面三层(网络层、数据链路层、物理层)为低层,由操作系统支持。
建立七层模型的主要目的是为解决各种网络互联时遇到的兼容性问题。其最大的优点是将服务、接口和协议这三个概念明确地区分开来:服务说明某一层为上一层提供一些什么功能,接口说明上一层如何使用下层的服务,而协议则是如何实现本层的服务。如此各层之间就具有很强的独立性,互联网络中各实体采用什么样的协议是没有限制的,只要向上提供服务并且不改变相临层的接口就可以了。网络七层的划分也是为了使用网络的不同功能模块分担起不同的职责,也就带来如下好处:
1、减轻问题的复杂程度,一旦发生网络故障,可迅速定位故障所处层次
2、在各层分别定义标准接口,使具备相同对等层的不同网络设备能实现互操作。各层之间相对独立,一种高层次协议可放在多种低层次协议上运行。
3、能有效刺激网络技术革新,因为每次更新都可以在小范围内进行,不需要更改整个系统
————————————————
版权声明:本文为CSDN博主「mzm1166」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/mzm1166/article/details/122867144

同样地,在进行驱动编写的时候,一般有几个不同的数据层次:最底层是物理层,是SPI的物理和时序实现,但是在这个底层上传输什么数据、为谁传输数据,物理层是不知道的,全靠上一层提供;比物理层高一层的是链路层,负责SPI物理传输的软件接口;中间两层是协议层和传输层,负责进行AD5689的指令和数据传输,但是具体的什么数据和什么指令,则是由最高层:应用层提供。每一层都为上一层提供一个简便而统一的接口。这个接口的具体表现就是每一层的封装函数。

在传输层,数据传输也有相应的封装:

// ad5689.c
uint32_t ad5689_set_dac(uint32_t data, uint8_t dac_ch, uint8_t write_flag, uint8_t update_flag)
{

    uint8_t cmd = 0;
    if (write_flag)
    {
        cmd |= AD5689_CMD_WRITE_INPUT_REG;
    }
    if (update_flag)
    {
        cmd |= AD5689_CMD_UPDATE_DAC_REG;
    }
    uint32_t res = ad5689_send_cmd(cmd, dac_ch, data);

    return res;
}

ad5689_set_dac函数中,调用了协议层的封装函数ad5689_send_cmd。以这个为例子,说明ad5689_set_dac作为传输层的封装函数,“只知道”有协议层的封装函数存在,并且只可以调用协议层函数。如果ad5689_set_dac调用链路层函数,亦即ad5689_send_recv_data,那就是“越级指挥”,是不符合分层思想和代码的基本逻辑的。由此可见,上一层函数只能调用自己的直接下层函数,而不能越级调用更下层的函数。

源码

// ad5689.h
#ifndef _AD5689_H_
#define _AD5689_H_

#include "main.h"

#define AD5689_PIN_LDAC 0X1
#define AD5689_PIN_RST 0X2

#define AD5689_CMD_WRITE_INPUT_REG 0X1
#define AD5689_CMD_UPDATE_DAC_REG 0X2
#define AD5689_CMD_DAC_POWER_CTRL 0X4
#define AD5689_CMD_HM_LDAC 0X5
#define AD5689_CMD_SW_RST 0X6
#define AD5689_CMD_SET_DCEN 0X9
#define AD5689_CMD_SET_READ_BACK 0Xa

// DAC register values (Mask codes) in argument[uint8_t dac_ch] function
#define AD5689_DAC_CH_A_ADDR 0X1
#define AD5689_DAC_CH_B_ADDR 0X8

int ad5689_init(
    // Configure spi interface
    SPI_HandleTypeDef *ad5689_hspi_i,
    // LDAC GPIO
    GPIO_TypeDef *ad5689_ldac_gpio_group_i,
    uint16_t ad5689_ldac_gpio_pin_i,

    // RESET GPIO
    GPIO_TypeDef *ad5689_rst_gpio_i,
    uint16_t ad5689_rst_gpio_pin_i);

uint32_t ad5689_set_dac(uint32_t data, uint8_t dac_ch, uint8_t write_flag, uint8_t update_flag);

#endif // _AD5689_H_
#include "ad5689.h"

// configure spi interface
static SPI_HandleTypeDef *ad5689_hspi;

// LDAC GPIO

static GPIO_TypeDef *ad5689_ldac_gpio_group;
static uint16_t ad5689_ldac_gpio_pin;

// RESET GPIO
static GPIO_TypeDef *ad5689_rst_gpio_group;
static uint16_t ad5689_rst_gpio_pin;

static uint32_t ad5689_send_recv_data(uint32_t tx_data)
{
    uint32_t rx_data;
    HAL_SPI_TransmitReceive(ad5689_hspi, (uint8_t *)&tx_data, (uint8_t *)&rx_data, 1, HAL_MAX_DELAY);
    return rx_data;
}

static int ad5689_set_pins(uint16_t pins)
{
    if (HAL_IS_BIT_SET(pins, AD5689_PIN_LDAC))
    {
        HAL_GPIO_WritePin(ad5689_ldac_gpio_group, ad5689_ldac_gpio_pin, 1);
    }
    if (HAL_IS_BIT_SET(pins, AD5689_PIN_RST))
    {
        HAL_GPIO_WritePin(ad5689_rst_gpio_group, ad5689_rst_gpio_pin, 1);
    }
    return 0;
}

static int ad5689_clr_pins(uint16_t pins)
{
    if (HAL_IS_BIT_SET(pins, AD5689_PIN_LDAC))
    {
        HAL_GPIO_WritePin(ad5689_ldac_gpio_group, ad5689_ldac_gpio_pin, 0);
    }
    if (HAL_IS_BIT_SET(pins, AD5689_PIN_LDAC))
    {
        HAL_GPIO_WritePin(ad5689_rst_gpio_group, ad5689_rst_gpio_pin, 0);
    }

    return 0;
}

static uint32_t ad5689_send_cmd(uint8_t cmd, uint8_t addr, uint16_t data)
{
    uint32_t send_data = (((uint32_t)cmd) << (16 + 4)) | (((uint32_t)addr) << 16) | data;
    return ad5689_send_recv_data(send_data);
}

int ad5689_init(
    // Configure SPI interface
    SPI_HandleTypeDef *ad5689_hspi_i,

    // LDAC GPIO
    GPIO_TypeDef *ad5689_ldac_gpio_group_i,
    uint16_t ad5689_ldac_gpio_pin_i,

    // RESET GPIO
    GPIO_TypeDef *ad5689_rst_gpio_group_i,
    uint16_t ad5689_rst_gpio_pin_i

)
{
    // configure values
    ad5689_hspi = ad5689_hspi_i;

    ad5689_ldac_gpio_group = ad5689_ldac_gpio_group_i;
    ad5689_ldac_gpio_pin = ad5689_ldac_gpio_pin_i;

    ad5689_rst_gpio_group = ad5689_rst_gpio_group_i;
    ad5689_rst_gpio_pin = ad5689_rst_gpio_pin_i;

    // Initialize ad5689
    ad5689_set_pins(AD5689_PIN_RST | AD5689_PIN_LDAC);
    HAL_Delay(1);
    ad5689_clr_pins(AD5689_PIN_RST);
    HAL_Delay(1);
    ad5689_set_pins(AD5689_PIN_RST);

    return 0;
}

uint32_t ad5689_set_dac(uint32_t data, uint8_t dac_ch, uint8_t write_flag, uint8_t update_flag)
{

    uint8_t cmd = 0;
    if (write_flag)
    {
        cmd |= AD5689_CMD_WRITE_INPUT_REG;
    }
    if (update_flag)
    {
        cmd |= AD5689_CMD_UPDATE_DAC_REG;
    }
    uint32_t res = ad5689_send_cmd(cmd, dac_ch, data);

    return res;
}

参考资料

ADI官方AD5689/5687介绍页面
OSI(七层网络协议)

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
STM32F407单片机16bit_DAC_AD5689模拟量(-10V~10V)电压输出实验KEIL工程源码: int main(void) { uint16_t data=0xFFFF/2; double temp,opa; /* 复位所有外设,初始化Flash接口和系统滴答定时器 */ HAL_Init(); /* 配置系统时钟 */ SystemClock_Config(); /* 初始化串口并配置串口中断优先级 */ MX_DEBUG_USART_Init(); KEY_GPIO_Init(); printf("硬石DAC(AD5689模块模拟量电压输出测试\n"); AD5689_Init(); AD5689_WriteUpdate_DACREG(DAC_A,data); AD5689_WriteUpdate_DACREG(DAC_B,0xFFFF-data); printf("data:%d\n",data); opa=OPA_RES_R2/OPA_RES_R1; while(1) { if(KEY1_StateRead()==KEY_DOWN) { if(data>(0xFFFF-1000)) data=(0xFFFF-1000); data +=1000; AD5689_WriteUpdate_DACREG(DAC_A,data); AD5689_WriteUpdate_DACREG(DAC_B,0xFFFF-data); temp=(double)(data*2-0xFFFF)*2500*opa/0xFFFF; //temp为目标电源值,这里先放大1000倍(方便计算而已),等后面显示再还原 //data是数字量DA值,当data取值为:0~0xFFFF对应AD5689输出为0~5V //本例程是输出-10V~10V,这个功能主要是靠运放实现,特殊的电路使得: //AD5689输出0V时对应运放输出-10V,AD5689输出2.5V对应运放输出0V,AD5689输出5V对应运放输出10V //(上面虽说是10V,实际上应该是 2.5V*opa(运放放大倍数),这里opa=40.2K/10K=4.02) //所以使得程序:data值为0时运放输出-10V, data为0xFFFF/2时输出运放输出0V,data为0xFFFF时输出运放输出10V //temp=(data-0xFFFF/2)/(0xFFFF/2)*2.5*1000*opa printf("data:%d->%0.3fV\n",data,temp/1000); } if(KEY2_StateRead()==KEY_DOWN) { if(data<1000) data=1000; data -=1000;

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值