Linux 驱动入门(8)—— SPI驱动

一、编译替换内核和设备树

在编译驱动程序之前要先编译内核,原因有三点:

  • 驱动程序要用到内核文件
  • 编译驱动时用的内核、开发板上运行到内核,要一致
  • 更换板子上的内核后,板子上的其他驱动也要更换

编译内核步骤看我之前写过的文章:

二、SPI协议介绍

1. SPI硬件知识

1.1 硬件连线

常见示例:

image.png

引脚含义如下:

引脚含义
DO(MOSI)Master Output, Slave Input,
SPI主控用来发出数据,SPI从设备用来接收数据
DI(MISO)Master Input, Slave Output,
SPI主控用来发出数据,SPI从设备用来接收数据
SCKSerial Clock,时钟
CSChip Select,芯片选择引脚

1.2 SPI控制器内部结构

image.png

分析如下:

image.png

2. SPI协议

2.1 传输示例

假设现在主控芯片要传输一个0x56数据给SPI Flash,时序如下:

image.png

首先我们要先拉低 CS0 选中 SPI Flash,0x56的二进制为 0b0101 0110,因此我们在每个 SCK 时钟
周期,DO 输出对应的电平。SPI Flash 会在每个时钟周期的上升沿读取 DO 上的电平。

2.2 SPI模式

在 SPI 协议中,有两个值来确定 SPI 的模式。

CPOL:表示 SPICLK 的初始电平,0 为低电平,1 为高电平。

CPHA:表示相位,即第一个还是第二个时钟沿采样数据,0 为第一个时钟沿,1 为第二个时钟沿。

CPOLCPHA模式含义
000SPICLK初始电平为低电平,在第一个时钟沿采样数据
011SPICLK初始电平为低电平,在第二个时钟沿采样数据
102SPICLK初始电平为高电平,在第一个时钟沿采样数据
113SPICLK初始电平为高电平,在第二个时钟沿采样数据

在这里我们常用的是模式0和模式3,因为它们都是在上升沿采集数据,不用在乎时钟电平的初始电平是什么,只要在上升沿采集数据就行。

极性选什么?格式选什么?

通常去参考外接的模块的芯片手册。比如对于OLED,查看它的芯片手册时序部分:

image.png

SCLK的初始电平我们并不需要关心,只要保证在上升沿采集数据就行。

2.3 特点

    1. 采用主-从模式(Master-Slave) 的控制方式

​ SPI 规定了两个 SPI 设备之间通信必须由主设备 (Master) 来控制次设备 (Slave). 一个 Master 设备可以通过提供 Clock 以及对 Slave 设备进行片选 (Slave Select) 来控制多个 Slave 设备, SPI 协议还规定 Slave 设备的 Clock 由 Master 设备通过 SCK 管脚提供给 Slave 设备, Slave 设备本身不能产生或控制 Clock, 没有 Clock 则 Slave 设备不能正常工作。

    1. 采用同步方式(Synchronous)传输数据

​ Master 设备会根据将要交换的数据来产生相应的时钟脉冲(Clock Pulse), 时钟脉冲组成了时钟信号(Clock Signal) , 时钟信号通过时钟极性 (CPOL) 和 时钟相位 (CPHA) 控制着两个 SPI 设备间何时数据交换以及何时对接收到的数据进行采样, 来保证数据在两个设备之间是同步传输的。

    1. 数据交换(Data Exchanges)

​ SPI 设备间的数据传输之所以又被称为数据交换, 是因为 SPI 协议规定一个 SPI 设备不能在数据通信过程中仅仅只充当一个 “发送者(Transmitter)” 或者 “接收者(Receiver)”. 在每个 Clock 周期内, SPI 设备都会发送并接收一个 bit 大小的数据, 相当于该设备有一个 bit 大小的数据被交换了。

​ 一个 Slave 设备要想能够接收到 Master 发过来的控制信号, 必须在此之前能够被 Master 设备进行访问 (Access)。所以, Master 设备必须首先通过 SS/CS pin 对 Slave 设备进行片选, 把想要访问的 Slave 设备选上。

​ 在数据传输的过程中, 每次接收到的数据必须在下一次数据传输之前被采样.。如果之前接收到的数据没有被读取, 那么这些已经接收完成的数据将有可能会被丢弃, 导致 SPI 物理模块最终失效。因此, 在程序中一般都会在 SPI 传输完数据后, 去读取 SPI 设备里的数据, 即使这些数据(Dummy Data)在我们的程序里是无用的。

三、SPI总线设备驱动模型

1. 平台总线设备驱动模型

Linux驱动程序开始基于"平台总线设备驱动"模型,把驱动程序分成2边:

  • 左边注册一个platform_driver结构体,里面是比较固定的、通用的代码

  • 右边注册一个platform_device结构体,里面是硬件资源

    • 可以在C文件中注册platform_device
    • 也可以使用设备树创建一个节点,内核解析设备树时注册platform_device

image.png

2. 数据结构

SPI 子系统中涉及2类硬件:SPI控制器、SPI设备。

image.png

SPI 控制器有驱动程序,提供 SPI 的传输能力。
SPI 设备也有自己的驱动程序,提供 SPI 的访问能力:

  • 它怎么知道访问这个设备,它知道这个设备的数据含义是什么
  • 它会调用 SPI 控制器的函数来收发数据

2.1 SPI控制器数据结构

在 Linux 中使用 spi_master 结构体描述 SPI 控制器,里面最重要的成员就是transfer函数指针:

image.png

2.2 SPI设备数据结构

在 Linux 中使用 spi_device 结构体描述 SPI 设备,里面记录有设备的片选引脚、频率、挂在哪个SPI控制器下面:

image.png

2.3 SPI设备驱动

Linux 中使用 spi_driver 结构体描述SPI设备驱动:

image.png

3. SPI驱动框架

image.png

3.1 SPI控制器驱动程序

SPI 控制器的驱动程序可以基于"平台总线设备驱动"模型来实现:

  • 在设备树里描述 SPI 控制器的硬件信息,在设备树子节点里描述挂在下面的SPI设备的信息
  • 在platform_driver中提供一个probe函数
    • 它会注册一个spi_master
    • 还会解析设备树子节点,创建spi_device结构体

3.2 SPI设备驱动程序

跟"平台总线设备驱动模型"类似,Linux中也有一个"SPI总线设备驱动模型":

  • 左边是spi_driver,使用C文件实现,里面有id_table表示能支持哪些 SPI 设备,有probe函数
  • 右边是spi_device,用来描述SPI设备,比如它的片选引脚、频率
    • 可以来自设备树:比如由SPI控制器驱动程序解析设备树后创建、注册spi_device
    • 可以来自C文件:比如使用spi_register_board_info创建、注册spi_device

四、DAC 驱动编写

1. DAC 简介

数模转换器 (DAC) 是一种与模数转换器功能相反的器件,可以将数字形式的数据转换为相应的模拟电压信号。
通用 DAC 模块是 12 位字转换器,带有两个支持立体声音频的输出通道。
DAC 可用于多种音频应用中,例如:安全警报、蓝牙耳机、发声玩具、答录机、人机接口以及低成本的音乐播放器。
不同类型的 DAC 有不同的通讯方式,这里我们选择 SPI 接口 DAC 芯片TLC5615。TLC5615 是一个10位的DAC,具有如下特性:
在这里插入图片描述
TLC5615 使用简单,只需要提供5V供电和外部参考电压即可,无需其他配置。主控通过三线SPI连接TLC5615,将 10bit 的数据发送给TLC5615;TLC5615将数值转换为电压输出。

1.1 内部框图

TLC5615 内部框图如下图所示:
在这里插入图片描述
操作过程为:

  • CS为低
  • 在SCLK的上升沿,从DIN采集16位数据,存入上图中的16-Bit Shift Register
  • 在CS的上升沿,把16-Bit Shift Register中的10位数据传入10-Bit DAC Register,作为模拟量在OUT引脚输出

注意

  • 传输的16位数据中,高4位是无意义的
  • 中间10位才被转换为模拟量
  • 最低2位必须是0

1.2 时序图

在这里插入图片描述
使用SPI传输的细节:

  • SCLK初始电平为低
  • 使用16个SCLK周期来传输16位数据
  • 在SCLK上升沿读取DIN电平
  • 在SCLK上升沿发出DOUT信号
  • DOUT数据来自16-Bit Shift Register
    • 第1个数据是上次数据遗留下的LSB位
    • 其余15个数据来自16-Bit Shift Register的高15位
    • 16-Bit Shift Register的LSB在下一个周期的第1个时钟传输
    • LSB必定是0,所以当前的周期里读出16-Bit Shift Register的15位数据也足够了

1.3 DAC公式

在这里插入图片描述

  • 输出电压 = 2 * VREFIN * n / 1024 = 2 * 2.048 * n / 1024
  • 其中: n为10位数值

2. 修改设备树

通过查看手册,确认SPI时钟最大频率:在这里插入图片描述

T = 25 + 25 = 50ns
F = 20000000 = 20MHz

修改设备树:
在这里插入图片描述
修改内容:0 代表第一个片选 gpio

dac: dac {
    compatible = "zgl,dac";
    reg = <0>;
    spi-max-frequency = <20000000>;
};

3. 驱动层代码

dac_drv.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/ioctl.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/err.h>
#include <linux/list.h>
#include <linux/errno.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/compat.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/acpi.h>

#include <linux/spi/spi.h>
#include <linux/spi/spidev.h>

#include <linux/uaccess.h>

#define SPI_IOC_WR 123

/*-------------------------------------------------------------------------*/

static struct spi_device *dac;
static int major;

static long
spidev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	int val;
	int err;
	unsigned char tx_buf[2];	
	unsigned char rx_buf[2];	

	struct spi_message	msg;
	struct spi_transfer	xfer[1];
	int status;

	memset(&xfer[0], 0, sizeof(xfer));
	
	/* copy_from_user */
	err = copy_from_user(&val, (const void __user *)arg, sizeof(int));

	printk("spidev_ioctl get val from user: %d\n", val);

	/* 发起SPI传输:     */

	/* 1. 把val修改为正确的格式 */
	val <<= 2;     /* bit0,bit1 = 0b00 */
	val &= 0xFFC;  /* 只保留10bit */

	tx_buf[0] = (val>>8) & 0xff;	
	tx_buf[1] = val & 0xff;


	/* 2. 发起SPI传输同时写\读 */
	/* 2.1 构造transfer
	 * 2.2 加入message
	 * 2.3 调用spi_sync
	 */
	xfer[0].tx_buf = tx_buf;
	xfer[0].rx_buf = rx_buf;
	xfer[0].len = 2;

	spi_message_init(&msg);
	spi_message_add_tail(&xfer[0], &msg);
	
	status = spi_sync(dac, &msg);

	/* 3. 修改读到的数据的格式 */
	val = (rx_buf[0] << 8) | (rx_buf[1]);
	val >>= 2;

	/* copy_to_user */
	err = copy_to_user((void __user *)arg, &val, sizeof(int));
	
	return 0;
}


static const struct file_operations spidev_fops = {
	.owner =	THIS_MODULE,
	/* REVISIT switch to aio primitives, so that userspace
	 * gets more complete API coverage.  It'll simplify things
	 * too, except for the locking.
	 */
	.unlocked_ioctl = spidev_ioctl,
};

/*-------------------------------------------------------------------------*/

/* The main reason to have this class is to make mdev/udev create the
 * /dev/spidevB.C character device nodes exposing our userspace API.
 * It also simplifies memory management.
 */

static struct class *spidev_class;

static const struct of_device_id spidev_dt_ids[] = {
	{ .compatible = "zgl,dac" },
	{},
};


/*-------------------------------------------------------------------------*/

static int spidev_probe(struct spi_device *spi)
{
	/* 1. 记录spi_device */
	dac = spi;

	/* 2. 注册字符设备 */
	major = register_chrdev(0, "zgl_dac", &spidev_fops);
	spidev_class = class_create(THIS_MODULE, "zgl_dac_class");
	device_create(spidev_class, NULL, MKDEV(major, 0), NULL, "mydac");	// /dev/mydac

	return 0;
}

static int spidev_remove(struct spi_device *spi)
{
	/* 反注册字符设备 */
	device_destroy(spidev_class, MKDEV(major, 0));
	class_destroy(spidev_class);
	unregister_chrdev(major, "zgl_dac");

	return 0;
}

static struct spi_driver spidev_spi_driver = {
	.driver = {
		.name =		"zgl_spi_dac_drv",
		.of_match_table = of_match_ptr(spidev_dt_ids),
	},
	.probe =	spidev_probe,
	.remove =	spidev_remove,

	/* NOTE:  suspend/resume methods are not necessary here.
	 * We don't do anything except pass the requests to/from
	 * the underlying controller.  The refrigerator handles
	 * most issues; the controller driver handles the rest.
	 */
};

/*-------------------------------------------------------------------------*/

static int __init spidev_init(void)
{
	int status;

	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);

	status = spi_register_driver(&spidev_spi_driver);
	if (status < 0) {
	}
	return status;
}

static void __exit spidev_exit(void)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	spi_unregister_driver(&spidev_spi_driver);
}

module_init(spidev_init);
module_exit(spidev_exit);

MODULE_LICENSE("GPL");



4. 应用层代码

/* 参考: tools\spi\spidev_fdx.c */

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <linux/types.h>

#define SPI_IOC_WR 123

/* dac_test /dev/mydac <val> */

int main(int argc, char **argv)
{
	int fd;
	unsigned int val;
	int status;

	unsigned char tx_buf[2];	
	unsigned char rx_buf[2];	
	
	if (argc != 3)
	{
		printf("Usage: %s /dev/mydac <val>\n", argv[0]);
		return 0;
	}

	fd = open(argv[1], O_RDWR);
	if (fd < 0) {
		printf("can not open %s\n", argv[1]);
		return 1;
	}

	val = strtoul(argv[2], NULL, 0);

	status = ioctl(fd, SPI_IOC_WR, &val);
	if (status < 0) {
		printf("SPI_IOC_WR\n");
		return -1;
	}

	/* 打印 */
	printf("Pre val = %d\n", val);
	
	
	return 0;
}

5. 上机测试

开发板上电,装载驱动,运行程序测试:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

热爱嵌入式的小佳同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值