本文章相关专栏往期内容,SPI子系统专栏:
- SPI通信协议与Linux设备驱动框架解析
- SPI传输与驱动框架的实现
- spidev.c:SPI设备驱动的核心实现逻辑
PCI/PCIe子系统专栏:
- 专栏地址:PCI/PCIe子系统
- PCIe设备MSI/MSI-X中断源码分析与驱动编写
– 末片,有专栏内容观看顺序Uart子系统专栏:
- 专栏地址:Uart子系统
- Linux内核早期打印机制与RS485通信技术
– 末片,有专栏内容观看顺序interrupt子系统专栏:
- 专栏地址:interrupt子系统
- Linux 链式与层级中断控制器讲解:原理与驱动开发
– 末片,有专栏内容观看顺序pinctrl和gpio子系统专栏:
专栏地址:pinctrl和gpio子系统
编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用
– 末片,有专栏内容观看顺序
input子系统专栏:
- 专栏地址:input子系统
- input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
– 末片,有专栏内容观看顺序I2C子系统专栏:
- 专栏地址:IIC子系统
- 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
– 末篇,有专栏内容观看顺序总线和设备树专栏:
- 专栏地址:总线和设备树
- 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
– 末篇,有专栏内容观看顺序
目录
- 1.spi_dac设备的应用程序
- 1.2 测试示例
- 1.2.1 使用方法
- 1.2.2 代码讲解
- 1.3 编写操作spi_dac设备的应用程序
- 1.3.1 DAC介绍
- 1.3.2 时序图
- 1.3.3 DAC公式
- 1.3.4 编写
- 3.3.5 上机
- 2.编写DAC的驱动程序
- 2.1 编写设备树
- 2.2 代码
1.spi_dac设备的应用程序
1.2 测试示例
内核提供的测试程序:tools\spi\spidev_fdx.c
📎spidev_fdx.c(应用程序)
1.2.1 使用方法
编译该程序并生成可执行文件 spidev_test
,你可以使用以下命令来与 SPI 设备交互。
- 查看设备状态:
./spidev_test /dev/spidev0.0
- 发送 SPI 消息:
./spidev_test -m 16 /dev/spidev0.0
- 读取数据:
./spidev_test -r 8 /dev/spidev0.0
- 启用详细模式:
./spidev_test -v /dev/spidev0.0
1.2.2 代码讲解
(1)显示设备属性
static void dumpstat(const char *name, int fd)
{
__u8 lsb, bits;
__u32 mode, speed;
if (ioctl(fd, SPI_IOC_RD_MODE32, &mode) < 0) { //读取当前的 SPI 模式。
perror("SPI rd_mode");
return;
}
if (ioctl(fd, SPI_IOC_RD_LSB_FIRST, &lsb) < 0) { //读取传输是 LSB 先还是 MSB 先。
perror("SPI rd_lsb_fist");
return;
}
if (ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits) < 0) { //读取每字传输的位数。
perror("SPI bits_per_word");
return;
}
if (ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed) < 0) { //读取最大传输速度。
perror("SPI max_speed_hz");
return;
}
printf("%s: spi mode 0x%x, %d bits %sper word, %d Hz max\n",
name, mode, bits, lsb ? "(lsb first) " : "", speed); //printf打印出设备的参数
}
通过 ioctl 调用,读取当前 SPI 设备的配置信息(模式、LSB/MSB 顺序、每字传输位数、最大速度等),并将其打印到控制台。
(2)读设备数据
static void do_read(int fd, int len)
{
unsigned char buf[32], *bp;
int status;
/* read at least 2 bytes, no more than 32 */
if (len < 2)
len = 2;
else if (len > sizeof(buf))
len = sizeof(buf);
memset(buf, 0, sizeof buf);
status = read(fd, buf, len);
//检查并确保读取的数据长度在 2 到 32 字节之间,并通过 read(fd, buf, len) 来从设备读取数据到 buf。
if (status < 0) {
perror("read");
return;
}
if (status != len) {
fprintf(stderr, "short read\n");
return;
}
printf("read(%2d, %2d): %02x %02x,", len, status,
buf[0], buf[1]);
status -= 2;
bp = buf + 2;
while (status-- > 0)
printf(" %02x", *bp++); // 将读取到的数据打印到控制台。
printf("\n");
}
(3)发送消息
static void do_msg(int fd, int len)
{
struct spi_ioc_transfer xfer[2];
unsigned char buf[32], *bp;
int status;
memset(xfer, 0, sizeof xfer);
memset(buf, 0, sizeof buf);
if (len > sizeof buf)
len = sizeof buf;
buf[0] = 0xaa;
xfer[0].tx_buf = (unsigned long)buf;
xfer[0].len = 1;
xfer[1].rx_buf = (unsigned long) buf;
xfer[1].len = len;
status = ioctl(fd, SPI_IOC_MESSAGE(2), xfer);
if (status < 0) {
perror("SPI_IOC_MESSAGE");
return;
}
printf("response(%2d, %2d): ", len, status);
for (bp = buf; len; len--)
printf(" %02x", *bp++);
printf("\n");
}
它不是通过调用write函数,进而去调用spidev驱动中的spidev_write,而是通过Ioctl函数来实现,这是可以的,ioctl函数在之前就介绍过是可以用来执行一些复杂的数据传输操作。
(4)主函数
int main(int argc, char **argv)
{
int c;
int readcount = 0;
int msglen = 0;
int fd;
const char *name;
while ((c = getopt(argc, argv, "hm:r:v")) != EOF) {
switch (c) {
case 'm':
msglen = atoi(optarg);
if (msglen < 0)
goto usage;
continue;
case 'r':
readcount = atoi(optarg);
if (readcount < 0)
goto usage;
continue;
case 'v':
verbose++;
continue;
case 'h':
case '?':
usage:
fprintf(stderr,
"usage: %s [-h] [-m N] [-r N] /dev/spidevB.D\n",
argv[0]);
return 1;
}
}
if ((optind + 1) != argc)
goto usage;
name = argv[optind];
fd = open(name, O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
dumpstat(name, fd);
if (msglen)
do_msg(fd, msglen);
if (readcount)
do_read(fd, readcount);
close(fd);
return 0;
}
-
解析命令行参数,支持的选项包括:
-
-m N
:发送 N 字节的消息。-r N
:读取 N 字节的数据。-v
:启用详细模式。-h
或-?
:打印帮助信息。
-
打开指定的 SPI 设备文件并调用
dumpstat
打印设备的状态。 -
根据命令行参数调用
do_msg
(发送消息)和do_read
(读取数据)函数进行 SPI 传输。 -
最后关闭设备文件。
1.3 编写操作spi_dac设备的应用程序
使用的驱动程序是内核提供的spidev.
DAC芯片手册:📎TLC5615.pdfc
1.3.1 DAC介绍
DAC(Digital-to-Analog Converter,数字-模拟转换器)是一种将数字信号转换为模拟电压或电流输出的器件。DAC模块常用于音频输出、信号处理、自动控制系统等场合。
一个典型的 DAC 模块有以下常见的引脚:
-
DIN(Data In,数据输入):
-
- 这是数据输入引脚,通过它传入要转换的数字信号。
- 数据通常以串行方式传输,例如 SPI(Serial Peripheral Interface)通信协议。每次传输的数字信号代表希望生成的模拟输出。
-
SCLK(Serial Clock,串行时钟):
-
- 时钟信号引脚,用于同步数据的传输。
- 通过时钟的上升沿或下降沿(取决于 DAC 的工作模式)将数据从控制器(例如微控制器)发送到 DAC 的数据输入引脚 DIN。SCLK 控制数据传输的时序。
-
CS(Chip Select,片选):
-
- 片选信号引脚,用于启用或禁用 DAC。
- 当 CS 处于低电平时,DAC 会被选中并开始接收数据;当 CS 拉高时,数据传输结束,DAC 停止通信并执行相应的操作(如更新输出电压)。
-
DOUT(Data Out,数据输出):
-
- 这是数据输出引脚,一些高级 DAC 模块带有此引脚,用于反馈通信或级联多个 DAC。
- DOUT 通常用于从 DAC 向主设备返回状态信息,也可以将多个 DAC 模块级联,形成串行数据链。
- 级联下(也就是多个DAC串行),传输的16bit数据
-
Vdd(电源输入):
-
- 为 DAC 提供工作电压的引脚。
- 典型的工作电压范围为 2.7V 到 5.5V 或 3.3V/5V,具体取决于 DAC 的型号。Vdd 为内部数字和模拟电路供电。
-
OUT(模拟输出):
-
- DAC 的模拟电压输出引脚。
- 根据输入的数字信号,这个引脚会输出相应的模拟信号,通常是电压信号(例如 0V-3.3V 或 0V-5V)。在某些情况下,DAC 可以直接输出电流信号。
- 其中传入的是16bit,但是输出的10bit,高四位无效,第两位为0
-
REFIN(Reference Input,基准电压输入):
-
- 基准电压输入引脚,用于提供 DAC 模块的基准电压。
- 基准电压决定了 DAC 的输出范围。例如,如果参考电压为 2.5V,DAC 的输出范围可能会是 0V 到 2.5V。外部基准电压可以提高 DAC 的精度和稳定性。
-
AGND(Analog Ground,模拟地):
-
- 模拟电路的地线。
- 与 Vdd 共同提供 DAC 的电源参考电压,确保 DAC 输出的稳定性。AGND 通常用于模拟部分电路,以减少数字噪声的干扰。
需要特别注意的是OUT和DOUT,因为这两个的差异,输入的16bit数据其高4位和低2位得都为0才能保证传输正确的数据。这个在时序图讲解的时候再提。
1.3.2 时序图
时序图,假设SCLK上升沿的时候读取电平 :
记住SPI其实就是彼此数据的置换:
在CS
有效(低电平)期间,DIN
(MISO)数据会随着SCLK
的每次时钟脉冲被移入从机的移位寄存器,从高位(MSB)开始依次传输到低位(LSB)。同时,DOUT
(MOSI)也会将从机中保存的前一次数据从高位(MSB)开始依次移出,传输到主机的移位寄存器中。
在TLC5615的SPI通信中,**DOUT**
的第一位对应上一帧(previous frame)的LSB。这是由TLC5615的工作原理和SPI协议的移位寄存器设计决定的。
CS
信号拉低后,TLC5615进入通信状态,其移位寄存器不会被清零,而是保留上一帧的数据内容。- 在通信开始时,从机的移位寄存器会直接输出当前内容中的最高位(MSB)。
- 但是,因为SPI通信是连续的,上一帧数据的LSB早已移位到移位寄存器的最高位(MSB位置)。
- 假设上一帧完整的16位数据已经移位完成,寄存器中的最后一个位(LSB)自然会移到最高位(MSB)。
- 当新的时钟信号(SCLK)到来时,从机移位寄存器的最高位(previous LSB)会最先通过
DOUT
传出。
SPI协议和TLC5615设计中,移位寄存器在时钟脉冲驱动下始终保持连续性,导致上一帧数据循环至当前帧的最高位。
我觉得主要是得看SPI选择的通信模式吧,像模式0,第一个上升沿就要移入数据,但是此时根本还没有数据,在此之前应该就进行了一次数据的移除。
1.3.3 DAC公式
数字信号怎么转换为模拟信号
OUT引脚输出电压:2 * V****refin ***** (n / 1024),其中n为输出的10bit的10进制值
1.3.4 编写
📎dac_test.c
编写设备树:
&ecspi1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi1>;
fsl,spi-num-chipselects = <2>;
cs-gpios = <&gpio4 26 GPIO_ACTIVE_LOW>, <&gpio4 24 GPIO_ACTIVE_LOW>;
status = "okay";
dac: dac {
compatible = "spidev";
reg = <0>;
spi-max-frequency = <20000000>;
};
};
3.3.5 上机
编译应用程序:
arm-buildroot-linux-gnueabihf-gcc -o dac_test dac_test.c
编译驱动:
2.编写DAC的驱动程序
使用之前讲到的编写SPI设备的驱动框架
2.1 编写设备树
&ecspi1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi1>;
fsl,spi-num-chipselects = <2>;
cs-gpios = <&gpio4 26 GPIO_ACTIVE_LOW>, <&gpio4 24 GPIO_ACTIVE_LOW>;
status = "okay";
dac: dac {
compatible = "spidev";
reg = <0>;
spi-max-frequency = <20000000>;
};
};
2.2 代码
📎dac_test.c
📎dac_drv.c
如果spidev没有被编译进内核,那么先执行:
insmod dac_drv.ko
确定设备节点:
ls /dev/100ask_dac
假设设备节点为/dev/100ask_dac
,执行测试程序:
./dac_test /dev/100ask_dac 500
./dac_test /dev/100ask_dac 600
./dac_test /dev/100ask_dac 1000