Linux 驱动入门(8)—— SPI驱动
文章目录
- 一、编译替换内核和设备树
- 二、SPI协议介绍
- 1. SPI硬件知识
- 1.1 硬件连线
- 1.2 SPI控制器内部结构
- 2. SPI协议
- 2.1 传输示例
- 2.2 SPI模式
- 2.3 特点
- 三、SPI总线设备驱动模型
- 1. 平台总线设备驱动模型
- 2. 数据结构
- 2.1 SPI控制器数据结构
- 2.2 SPI设备数据结构
- 2.3 SPI设备驱动
- 3. SPI驱动框架
- 3.1 SPI控制器驱动程序
- 3.2 SPI设备驱动程序
- 四、DAC 驱动编写
- 1. DAC 简介
- 1.1 内部框图
- 1.2 时序图
- 1.3 DAC公式
- 2. 修改设备树
- 3. 驱动层代码
- 4. 应用层代码
- 5. 上机测试
一、编译替换内核和设备树
在编译驱动程序之前要先编译内核,原因有三点:
- 驱动程序要用到内核文件
- 编译驱动时用的内核、开发板上运行到内核,要一致
- 更换板子上的内核后,板子上的其他驱动也要更换
编译内核步骤看我之前写过的文章:
- 编译替换内核_设备树_驱动_IMX6ULL
二、SPI协议介绍
1. SPI硬件知识
1.1 硬件连线
常见示例:
引脚含义如下:
引脚 | 含义 |
---|---|
DO(MOSI) | Master Output, Slave Input, SPI主控用来发出数据,SPI从设备用来接收数据 |
DI(MISO) | Master Input, Slave Output, SPI主控用来发出数据,SPI从设备用来接收数据 |
SCK | Serial Clock,时钟 |
CS | Chip Select,芯片选择引脚 |
1.2 SPI控制器内部结构
分析如下:
2. SPI协议
2.1 传输示例
假设现在主控芯片要传输一个0x56数据给SPI Flash,时序如下:
首先我们要先拉低 CS0 选中 SPI Flash,0x56的二进制为 0b0101 0110,因此我们在每个 SCK 时钟
周期,DO 输出对应的电平。SPI Flash 会在每个时钟周期的上升沿读取 DO 上的电平。
2.2 SPI模式
在 SPI 协议中,有两个值来确定 SPI 的模式。
CPOL:表示 SPICLK 的初始电平,0 为低电平,1 为高电平。
CPHA:表示相位,即第一个还是第二个时钟沿采样数据,0 为第一个时钟沿,1 为第二个时钟沿。
CPOL | CPHA | 模式 | 含义 |
---|---|---|---|
0 | 0 | 0 | SPICLK初始电平为低电平,在第一个时钟沿采样数据 |
0 | 1 | 1 | SPICLK初始电平为低电平,在第二个时钟沿采样数据 |
1 | 0 | 2 | SPICLK初始电平为高电平,在第一个时钟沿采样数据 |
1 | 1 | 3 | SPICLK初始电平为高电平,在第二个时钟沿采样数据 |
在这里我们常用的是模式0和模式3,因为它们都是在上升沿采集数据,不用在乎时钟电平的初始电平是什么,只要在上升沿采集数据就行。
极性选什么?格式选什么?
通常去参考外接的模块的芯片手册。比如对于OLED,查看它的芯片手册时序部分:
SCLK的初始电平我们并不需要关心,只要保证在上升沿采集数据就行。
2.3 特点
-
- 采用主-从模式(Master-Slave) 的控制方式
SPI 规定了两个 SPI 设备之间通信必须由主设备 (Master) 来控制次设备 (Slave). 一个 Master 设备可以通过提供 Clock 以及对 Slave 设备进行片选 (Slave Select) 来控制多个 Slave 设备, SPI 协议还规定 Slave 设备的 Clock 由 Master 设备通过 SCK 管脚提供给 Slave 设备, Slave 设备本身不能产生或控制 Clock, 没有 Clock 则 Slave 设备不能正常工作。
-
- 采用同步方式(Synchronous)传输数据
Master 设备会根据将要交换的数据来产生相应的时钟脉冲(Clock Pulse), 时钟脉冲组成了时钟信号(Clock Signal) , 时钟信号通过时钟极性 (CPOL) 和 时钟相位 (CPHA) 控制着两个 SPI 设备间何时数据交换以及何时对接收到的数据进行采样, 来保证数据在两个设备之间是同步传输的。
-
- 数据交换(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
2. 数据结构
SPI 子系统中涉及2类硬件:SPI控制器、SPI设备。
SPI 控制器有驱动程序,提供 SPI 的传输能力。
SPI 设备也有自己的驱动程序,提供 SPI 的访问能力:
- 它怎么知道访问这个设备,它知道这个设备的数据含义是什么
- 它会调用 SPI 控制器的函数来收发数据
2.1 SPI控制器数据结构
在 Linux 中使用 spi_master 结构体描述 SPI 控制器,里面最重要的成员就是transfer
函数指针:
2.2 SPI设备数据结构
在 Linux 中使用 spi_device 结构体描述 SPI 设备,里面记录有设备的片选引脚、频率、挂在哪个SPI控制器下面:
2.3 SPI设备驱动
Linux 中使用 spi_driver 结构体描述SPI设备驱动:
3. SPI驱动框架
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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
", 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
", __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
", __FILE__, __FUNCTION__, __LINE__);
spi_unregister_driver(&spidev_spi_driver);
}
module_init(spidev_init);
module_exit(spidev_exit);
MODULE_LICENSE("GPL");
4. 应用层代码
/* 参考: toolsspispidev_fdx.c */
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SPI_IOC_WR 123
/* dac_test /dev/mydac */
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
" , argv[0]);
return 0;
}
fd = open(argv[1], O_RDWR);
if (fd < 0) {
printf("can not open %s
", argv[1]);
return 1;
}
val = strtoul(argv[2], NULL, 0);
status = ioctl(fd, SPI_IOC_WR, &val);
if (status < 0) {
printf("SPI_IOC_WR
");
return -1;
}
/* 打印 */
printf("Pre val = %d
", val);
return 0;
}
5. 上机测试
开发板上电,装载驱动,运行程序测试:
本文地址:https://www.vps345.com/5775.html