对象式单片机外部模块驱动编写详解——DAC8552为例
具体的代码和例程请参照以下GitHub仓库,记得给我star哦!
https://github.com/xdu-zhangfan/drivers
对象式驱动原理
对象式驱动,借鉴面向对象编程的思想,是将一个具体的外部器件抽象为一个模块,并使用一个函数族实现对于这个外部器件的操作。
——沃兹基硕德(1970.1.1-?),当代面向HelloWorld编程宗师级人物
上面这段话看上去很高级,其实说人话就是:写个驱动,让“我操作具体的器件”变成“用几个函数操作一个结构体”。如果把这几个函数全部写在一个文件里面,加一定的标志体现他们的同一性(比如一个共同的函数前缀),这就叫函数族;然后定义一个结构体,里面包含所有操作器件的时候需要用到的参数,这就叫描述结构体。
DAC8552基本介绍
DAC8552是由TI推出的一款16位DAC模块,支持不同下拉等级的掉电模式(不知道有啥用),使用SPI兼容的协议通信,又是阴间24位SPI,具体时序如下:(出自官方数据手册第5页)
明显,这就是一个SYNC(FPGA)兼容SPI(MCU)的协议。SCLK
对应SPI的时钟线,在空闲的时候(也就是DIN
没有数据的时候),SCLK保持高电平。由此可见,SPI协议的时钟极性应该为高电平(CPOL=1)。同样地,在SYNC
拉低之后,DAC8552在时钟的下降沿进行数据采样(如图所示,DIN
应该在SCLK
下降沿的时候保持稳定),因此实在第一个时钟沿进行采样(CPHA=0)。
至于这24个位到底啥意思,就参照官方数据手册第14页:
咋用,同样在第14页:
还有一个对于Power Down模式下输出阻抗选择的表,也在第14页:
OK,现在让我们分析一下,这个DAC在干啥。
首先,DAC的作用是“将离散信号用某种方式转换成模拟信号”,说人话就是把某个值转成某个电压,值和电压之间一一对应。那么,如果要实现一个DAC模块的驱动,我们需要关注的就是:怎么样把这个值传进去这个模块?
带着这个疑问,我们在数据手册的这几张图表里面找我们需要的信息。第一张时序图里面,只告诉了我们这几根信号线的时序:SPI,24位,MSB First,时钟极性为1(时钟关闭时高电平),时钟相位为0(第一个边沿进行数据读取)。但是传输的这24位的数据中,每一位都代表了什么内容?这个问题可是一点儿也没有提到;第二张,Figure-43
,给了一张简表,但是详细内容还是放在了Table-1
。
我:Table-1,让我康康!
Table-1:哥不要呀!!!
首先,D23
和D22
都是保留位,也就是置0就可以了;D21
和D20
是Load A
和Load B
。根据Description
的内容,哪个位被置1,就装载哪个通道的DAC。也就是说,如果想要一个通道输出一个模拟值,不仅要写这个通道的Buffer(缓冲区),还要Load这个通道,具体做法就是在Load A
或者Load B
这个位置1。D19
是“Don‘t care”,不用管,置0就可以了。D18
是缓冲区选择,也就是说,当这个位是0的时候,Data的值就会被装到A通道的缓冲区中;当这个位是1的时候,Data的值就会被装到B通道的缓冲区之中。到现在为止,怎么把值传给DAC8552就已经很明确了:构造一个24位的数据,作为一次SPI的发送数据,在这个24位数据的相应位置填入相应的控制位和16位数据,然后通过SPI发送给DAC8552就可以了。
Table-2:哥,还有我……
我:那就连你也一起康了!!!(喜
等等,怎么还有两个位没有讲:D17
和D16
?因为这两个位不重要。观察Table-1
可知,发送和装载DAC数据的时候,这俩都置0就可以了。那如果不是0呢?OK,现在看Table-2
:Power Down输出阻抗。这两个位决定了一个通道Power Down(十分抱歉,本人翻译能力有限,实在找不到很好的翻译)时的输出阻抗是10K、100K还是高阻。而且,在进行这两个位的设置的时候是不能传数据的。说人话就是:如果这两个位不同时为0,则Data是无效的。
DAC8552驱动抽象
在正式开始驱动编写的庞大工程之前,我们首先发出写驱动的哲学三问:
我们从哪里来?
我们是谁?
我们要到哪里去?
我要干嘛?
模块有啥功能?
我要实现模块的啥功能?
一般来说,写驱动实现自己想要的功能即可。比如在这个DAC8552的驱动编写过程中,明显,写入A通道和B通道的缓冲区之后可以不进行Load
。也就是说,在写入缓冲区之后的某个时间再进行Load
也是可以被允许的。但是,有必要吗?写都写进去了,不输出算个啥操作?一般这种外挂的DAC模块,写入和输出都是同时进行的。如果需要A通道和B通道在装载之后同一时间输出,也可以。也就是说,这个驱动要实现的基本功能就是:写入一个值并且立即输出、写入两个值并且同时输出。
那么,要怎么实现三个基本功能呢?我的方法是直接写三个函数,分别写入A、B和AB通道的值。当然,写入A、B通道的值也可以用同一个函数,然后用一个独立值选择到底是写入A还是写入B。
但是,首先咱们得有对于SPI的抽象封装:
// Abstract HAL library to DAC8552 driver space
static uint32_t dac8552_send_recv_data(dac8552_HandleTypeDef *dev_handle, uint32_t tx_data)
{
uint32_t rx_data;
HAL_SPI_TransmitReceive(dev_handle->dac8552_hspi, (uint8_t *)&tx_data, (uint8_t *)&rx_data, 1, HAL_MAX_DELAY);
return rx_data;
}
为啥要有这个封装?因为这个封装表明了HAL_SPI_TransmitReceive
函数并不是我这个DAC8552驱动库里面的函数,但是这个函数是底层的接口(HAL库函数),所以需要用一个函数将底层接口包装到我的驱动库,作为调用底层驱动的唯一途径。
这样做主要有两个好处:减少重复代码和便于移植。
对系统底层的操作进行抽象化封装,可以减少重复的代码。试想一下,如果操作不是这么简单的SPI通信,而是在进行SPI通信的时候还要设置一大堆管脚区分系统状态的话(点名AD9910,一堆管脚,复杂得很),这个底层接口函数必然不会这么简单。如果在进行每一个对器件的操作都写这么多代码,你绝对活不过35岁,就更别说面临什么中年危机了好伐……
第二个理由同样是为了让本就寿元无多的硬件工程师们多保留一点寿元。如果发现一个器件,底层协议用的是IIC,但是高层的数据抽象很像。如果有一个底层封装,那么,在移植驱动的时候就不需要把所有的对于SPI的读写换成IIC的读写,而是直接修改这个函数就可以了。这样大大减少了移植的时间成本和出错可能。
然后就是对于DAC8552设备的抽象。这里直接放代码:
// dac8552.h
// for output impedance power down selection, independent codes
#define DAC8552_OUTIMP_PDN_NONE 0
#define DAC8552_OUTIMP_PDN_10K 1
#define DAC8552_OUTIMP_PDN_100K 2
#define DAC8552_OUTIMP_PDN_HZ 3
typedef struct
{
SPI_HandleTypeDef *dac8552_hspi; // HAL SPI interface of DAC8552
unsigned char out_imp_pdn; // output impedance, independent
unsigned short int out_value_a; // output value, effective when [out_imp_pdn == 0]
unsigned short int out_value_b; // output value, effective when [out_imp_pdn == 0]
} dac8552_HandleTypeDef;
在这个描述结构体中,包含了A、B两个通道的值和Power Down模式输出阻抗的选择,还有一个SPI的HAL库描述结构体指针。这几个值构成了最基本的对于DAC8552的描述,解决了两个大问题:传到哪里?传什么数据?
如下是一个函数的实例:
int dac8552_set_value_a(dac8552_HandleTypeDef *dev_handle)
{
dac8552_send_recv_data(dev_handle, (unsigned int)(((1 << 20) |
(0 << 18) |
(dev_handle->out_imp_pdn << 16) |
(dev_handle->out_value_a))));
return 0;
}
这个函数肉眼可见地简洁。其中,(1 << 20)
是设置Load A,(0 << 18)
是指定写入A通道缓冲区。(dev_handle->out_imp_pdn << 16)
是进行Power Down的模式更改, (dev_handle->out_value_a)
是要写入的数据。等等,为啥Power Down和数据同时设置?一方面,根据数据手册,在设置这两个位不同时为零的时候,数据无效。也就是说,这个在硬件逻辑上已经可以实现了,不需要软件在进行处理。另一方面,从逻辑上讲,也应该大概率不会出现Power Down和数据同时设置的时候数据有效的情况,除非编程者对这个器件真的很不熟悉很不熟悉才会这样做的。综上所述,就直接在函数里面把这两位加上就可以了,没必要再做判断啥的,纯纯给自己找事儿。
源码文件及其解释
// dac8552.h
#ifndef DAC8552
#define DAC8552
#include "main.h"
// for output impedance power down selection, independent codes
#define DAC8552_OUTIMP_PDN_NONE 0
#define DAC8552_OUTIMP_PDN_10K 1
#define DAC8552_OUTIMP_PDN_100K 2
#define DAC8552_OUTIMP_PDN_HZ 3
typedef struct
{
SPI_HandleTypeDef *dac8552_hspi; // HAL SPI interface of DAC8552
unsigned char out_imp_pdn; // output impedance, independent
unsigned short int out_value_a; // output value, effective when [out_imp_pdn == 0]
unsigned short int out_value_b; // output value, effective when [out_imp_pdn == 0]
} dac8552_HandleTypeDef;
int dac8552_init(dac8552_HandleTypeDef *dev_handle, SPI_HandleTypeDef *dev_hspi);
int dac8552_set_value_a(dac8552_HandleTypeDef *dev_handle);
int dac8552_set_value_b(dac8552_HandleTypeDef *dev_handle);
int dac8552_set_value_ab(dac8552_HandleTypeDef *dev_handle);
#endif
对于Power Down模式的选择,提供了4个值。DAC8552_OUTIMP_PDN_NONE
对应正常模式,也就是不进行Power Down;DAC8552_OUTIMP_PDN_10K
、DAC8552_OUTIMP_PDN_100K
和DAC8552_OUTIMP_PDN_HZ
则是分别对应了10K、100K和高阻的输出阻抗。当设置这三个输出阻抗的时候,Data的值是无效的。
// dac8552.c
#include "dac8552.h"
// Initialize DAC8552
int dac8552_init(dac8552_HandleTypeDef *dev_handle, SPI_HandleTypeDef *dev_hspi)
{
if (dev_handle == NULL)
{
return -1;
}
memset(dev_handle, 0, sizeof(dac8552_HandleTypeDef));
dev_handle->dac8552_hspi = dev_hspi;
return 0;
}
// Abstract HAL library to DAC8552 driver space
static uint32_t dac8552_send_recv_data(dac8552_HandleTypeDef *dev_handle, uint32_t tx_data)
{
uint32_t rx_data;
HAL_SPI_TransmitReceive(dev_handle->dac8552_hspi, (uint8_t *)&tx_data, (uint8_t *)&rx_data, 1, HAL_MAX_DELAY);
return rx_data;
}
// send DAC value to DAC8552
int dac8552_set_value_a(dac8552_HandleTypeDef *dev_handle)
{
dac8552_send_recv_data(dev_handle, (unsigned int)(((1 << 20) |
(0 << 18) |
(dev_handle->out_imp_pdn << 16) |
(dev_handle->out_value_a))));
return 0;
}
int dac8552_set_value_b(dac8552_HandleTypeDef *dev_handle)
{
dac8552_send_recv_data(dev_handle, (unsigned int)((2 << 20) |
(1 << 18) |
(dev_handle->out_imp_pdn << 16) |
(dev_handle->out_value_b)));
return 0;
}
int dac8552_set_value_ab(dac8552_HandleTypeDef *dev_handle)
{
dac8552_send_recv_data(dev_handle, (unsigned int)(((0 << 20) |
(0 << 18) |
(dev_handle->out_imp_pdn << 16) |
(dev_handle->out_value_a))));
dac8552_send_recv_data(dev_handle, (unsigned int)(((3 << 20) |
(1 << 18) |
(dev_handle->out_imp_pdn << 16) |
(dev_handle->out_value_b))));
return 0;
}
dac8552_set_value_ab
中,先写入A的数据,但是不进行Load A,所以有(0 << 20)
;之后写B通道并且同时Load A和Load B,因此有(3 << 20)
,即两位同时设置。