1、SPI协议
SPI(Serial Peripheral Interface)是一种同步串行数据通信协议,通常用于连接微控制器和外部设备,如传感器、存储器、显示器等。SPI协议使用四根线进行通信,包括时钟线(SCLK)、数据输入线(MISO)、数据输出线(MOSI)和片选线(SS)。
SPI通信的基本过程如下:
- 主设备通过片选线选择要与之通信的从设备
- 主设备通过时钟线产生时钟信号,控制数据传输的时序
- 主设备通过数据输出线(MOSI)发送数据到从设备
- 从设备通过数据输入线(MISO)返回响应数据到主设备
时钟线在空闲时可以有高低电平两种状态,数据在采样可以在时钟线的奇数沿或者偶数沿,前者为极性,后者为相位,一共四种组合构成了SPI通信的4种模式:
模式 | 极性 | 相位 | 说明 |
0 | 0 | 0 | clk空闲为低电平,在奇数沿采样 |
1 | 0 | 1 | clk空闲为低电平,在偶数沿采样 |
2 | 1 | 0 | clk空闲为高电平,在奇数沿采样 |
3 | 1 | 1 | clk空闲为高电平,在偶数沿采样 |
以模式0为例,clk空闲为低电平,第一个沿为奇数沿,是从低电平变高电平的上升沿,在此处进行数据采样:
2、SPI驱动子系统
2.1 主机驱动
主机驱动一般由SOC厂商编写,本文示例使用的全志H616的内核代码,源码位于drivers/spi/spi-sunxi.c,主机驱动使用了platform总线驱动模型, platform总线的详细匹配过程可以参考之前的文章:Linux驱动(四)platform总线匹配过程,在设备树中配置好对应节点后,能够与内核配置的of_match_table匹配成功就会调用probe函数:
probe函数主要完成两个任务,一个是根据设备树的配置获取spi通信的相关参数,申请-->初始化-->注册spi_master;二是将设备树配置的子节点转化为spi_device数据结构,供后续的设备驱动使用:
2.2 设备驱动
linux内核中spi设备驱动代码位置:/drivers/spi/spidev.c。驱动的入口函数如下,正常的字符设备驱动需要经过三个步骤:register_chrdev-->class_create-->device_create;但是在入口函数中只进行了前面两步,然后调用spi_register_driver向spi总线进行了驱动注册。
static int __init spidev_init(void)
{int status;BUILD_BUG_ON(N_SPI_MINORS > 256);status = register_chrdev(SPIDEV_MAJOR, "spi", &spidev_fops);if (status < 0)return status;spidev_class = class_create(THIS_MODULE, "spidev");if (IS_ERR(spidev_class)) {unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);return PTR_ERR(spidev_class);}status = spi_register_driver(&spidev_spi_driver);if (status < 0) {class_destroy(spidev_class);unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);}return status;
}
spi_register_driver注册驱动,其本质也是使用总线设备驱动模型,不过其使用的是spi总线,设备树中的节点转换为spi_device与驱动进行匹配,匹配成功后则会调用驱动的probe函数,其具体的匹配过程如下:
设备和驱动通过spi总线匹配成功后执行probe函数,即spidev_probe函数,其内容如下,在31行使用device_create进行字符设备创建,在前面的入口函数中已经完成register_chrdev和class_create,在此处调用device_create创建了spidevxx设备,其中第一个x表示的事第几路SPI,第二个x表示的是该SPI下的第几个片选设备(设备树<reg>属性):
static int spidev_probe(struct spi_device *spi)
{struct spidev_data *spidev;int status;unsigned long minor;if (spi->dev.of_node && !of_match_device(spidev_dt_ids, &spi->dev)) {dev_err(&spi->dev, "buggy DT: spidev listed directly in DT\n");WARN_ON(spi->dev.of_node &&!of_match_device(spidev_dt_ids, &spi->dev));}spidev_probe_acpi(spi);spidev = kzalloc(sizeof(*spidev), GFP_KERNEL);if (!spidev)return -ENOMEM;spidev->spi = spi;spin_lock_init(&spidev->spi_lock);mutex_init(&spidev->buf_lock);INIT_LIST_HEAD(&spidev->device_entry);mutex_lock(&device_list_lock);minor = find_first_zero_bit(minors, N_SPI_MINORS);if (minor < N_SPI_MINORS) {struct device *dev;spidev->devt = MKDEV(SPIDEV_MAJOR, minor);dev = device_create(spidev_class, &spi->dev, spidev->devt,spidev, "spidev%d.%d",spi->master->bus_num, spi->chip_select);status = PTR_ERR_OR_ZERO(dev);} else {dev_dbg(&spi->dev, "no minor number available!\n");status = -ENODEV;}if (status == 0) {set_bit(minor, minors);list_add(&spidev->device_entry, &device_list);}mutex_unlock(&device_list_lock);spidev->speed_hz = spi->max_speed_hz;if (status == 0)spi_set_drvdata(spi, spidev);elsekfree(spidev);return status;
}
在上述代码中定义了struct spidev_data *spidev,其存储了spi设备对应的master结构体和设备本身的设备号,因此当使用open函数打开该设备节点的时候就可以反向查找到该设备对应的master结构体,进而可以使用其中的收发函数实现数据传输。
2.3 应用测试
目前测试的设备中设备树配置了两路SPI,每一路配置了一个设备:
根据上述分析,会生成两个字符设备,查看/dev下相关文件:
内核中spi驱动程序使用示例在/tools/spi/spidev_fdx.c中,重点查看do_msg接口:
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");
}
根据上述示例编写一个简单的测试用例,将SPI的输入和输出进行短接,测试数据发送与数据接收是否一致,测试代码如下:
#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>
#include <linux/spi/spidev.h>int main(int argc, char **argv)
{int fd, ret = 0;struct spi_ioc_transfer xfer = {0};unsigned char sendbuf[6] = "hello";unsigned char recvbuf[6] = {0};fd = open("/dev/spidev1.1", O_RDWR);if (fd < 0){printf("error, can't open /dev/spidev1.1\n");return 0;}xfer.tx_buf = (unsigned long)sendbuf;xfer.rx_buf = (unsigned long)recvbuf;xfer.len = 6;ret = ioctl(fd, SPI_IOC_MESSAGE(1), &xfer);if(ret < 0){printf("err\n");}else{printf("send data : %s\n", sendbuf);printf("recv data : %s\n", recvbuf);}return 0;
}
编译程序进行测试,测试结果正常:
3、总结
文章简单阐述了spi设备的协议和特性,结合内核代码总结了SPI驱动子系统的架构,最后编写了简单测试用例进行测试,其中设备树的详细配置项和数据收发中内核的详细处理后续再另起篇幅阐述。