Linux SPi总线使用

1. 简介

SPI总线是很常用的总线类型,有好多设备的数据接口都采用了SPI总线,比如ICM20608加速度陀螺仪传感器。在Linux中,SPI设备是采用了Linux惯用的设备模型:driver->bus->device,此总线类型是SPI总线,driver就是我们针对某一SPI设备所编写的设备驱动,使用bus给我们提供的SPI总线操控函数,然后根据SPI设备信息来编写SPI设备驱动。当我们编写SPI设备驱动时,需要将SPI设备驱动(spi_driver)使用设备注册函数(spi_register_driver)注册到Linux内核中,然后我们就可以使用bus所提供的的SPI操控函数了。SPI操控函数是由SPI控制器提供的。
SPI控制器其实也是一个driver->bus->device模型,只不过这个SPI控制器驱动由芯片厂商人员编写,提供给我们操控SPI控制器。这个总线是platform总线类型,device设备是在设备树中获取的SPI控制器的信息,然后芯片厂商编写driver,最后将SPI控制器注册到内核中,这样我们编写SPI设备驱动时就可以使用厂商提供的SPI控制器操控函数了。
其简单的模型如下:
在这里插入图片描述

2. SPI数据类型

1). SPI控制器驱动结构体

这个结构体里面主要是包含了某个SPI控制器的所有课操控信息结构体,比如这个SPI控制器的传输函数等信息。spi_master结构体在include/linux/spi/spi.h的第315行左右定义,其具体内容如下:

struct spi_master {
	......
	int (*transfer)(struct spi_device *spi, struct spi_message *mesg);	/* 次函数就是SPI传输数据函数	*/
	......
	int (*transfer_one_message)(struct spi_master *master, struct spi_message *mesg); /* 这个也是 */
	......

其实这个结构体我们不需要关心,这个结构体在芯片厂商人员编写SPI控制器驱动时,注册后,这里面的信息就已经有了,我们不需要去干预。结构体具体的信息很复杂,可以不去了解,也可以通过315行上面的注释来了解。

2). SPI设备信息结构体

spi_device结构体里面储存着这个SPI设备从设备树中获取的信息,在include/linux/spi/spi.h中的第72行左右有定义,其内容如下:

struct spi_device {
	struct device		dev;	/* SPI设备信息的父类	*/
	struct spi_master	*master;	/* 所使用的SPI控制器结构体*/
	u32			max_speed_hz;	/* spi设备所支持的最大时钟频率	*/
	u8			chip_select;	/* 片选选中电平	*/
	u8			bits_per_word;	/* 每个字的bit数,默认8位,可以被驱动使用者修改	*/
	u16			mode;	/* 数据传输模式,cpol,chpa */
/* mode参数	*/
#define	SPI_CPHA	0x01			/* clock phase */
#define	SPI_CPOL	0x02			/* clock polarity */
#define	SPI_MODE_0	(0|0)			/* (original MicroWire) */
#define	SPI_MODE_1	(0|SPI_CPHA)
#define	SPI_MODE_2	(SPI_CPOL|0)
#define	SPI_MODE_3	(SPI_CPOL|SPI_CPHA)
#define	SPI_CS_HIGH	0x04			/* chipselect active high? */
#define	SPI_LSB_FIRST	0x08			/* per-word bits-on-wire */
#define	SPI_3WIRE	0x10			/* SI/SO signals shared */
#define	SPI_LOOP	0x20			/* loopback mode */
#define	SPI_NO_CS	0x40			/* 1 dev/bus, no chipselect */
#define	SPI_READY	0x80			/* slave pulls low to pause */
#define	SPI_TX_DUAL	0x100			/* transmit with 2 wires */
#define	SPI_TX_QUAD	0x200			/* transmit with 4 wires */
#define	SPI_RX_DUAL	0x400			/* receive with 2 wires */
#define	SPI_RX_QUAD	0x800			/* receive with 4 wires */
	int			irq;	/* 所使用的的irq号	*/
	void			*controller_state;
	void			*controller_data;
	char			modalias[SPI_NAME_SIZE];
	int			cs_gpio;	/* 片选所使用的的gpio号 */

	/*
	 * likely need more hooks for more protocol options affecting how
	 * the controller talks to each chip, like:
	 *  - memory packing (12 bit samples into low bits, others zeroed)
	 *  - priority
	 *  - drop chipselect after each word
	 *  - chipselect delays
	 *  - ...
	 */
};

一个SPi控制器可能挂了很多个SPI设备,有可能每个SPI设备所需要的的SPI控制器的参数不同,比如SPI速率,片选引脚,等每个SPI特有的信息,这个特有的信息不能放在共有的结构体中,需要针对某个SPI初始化,所以这个spi_device中除了保存从设备树获取的设备树信息,还保存着自己特有的信息,如果设备树里面没有添加,我们也可以修改这个结构体,然后设置这个SPI控制器。
一般都需要修改了cpol,csph,这个是按照SPI设备芯片所配置的,本次全部配置为0,然后使用spi_setup函数设置。
其中master结构体就是在设备树中设备指定所使用的的SPI控制器,让kernel读取设备树时,就会将所指定的SPI控制器的地址保存在这个master变量中,当进行设备传输时,就需要将spi_device结构体传入进去,用来寻找所使用的的哪个SPI控制器。

3). SPI设备驱动结构体

spi_driver是需要我们针对SPI设备所写驱动所使用的的结构体,当我们将其内容写完时,需要注册到内核进去,然后这个SPI设备就可以自由的使用。spi_driver结构体在/include/linux/spi/spi.h的第180行左右,其内容如下:

struct spi_driver {
	const struct spi_device_id *id_table;	/* 传统设备匹配表	*/
	int			(*probe)(struct spi_device *spi);	/* 设备和驱动匹配后会自动运行的函数	*/
	int			(*remove)(struct spi_device *spi);	/* 驱动注销时会自动运行的函数	*/
	void			(*shutdown)(struct spi_device *spi);	/* 系统的某些状态所需要的的回调函数	*/
	struct device_driver	driver;	/* SPI驱动的父类	*/
};

传统设备匹配表一般都是未使用设备树是,采用的设备匹配方法,一般都是根据of_table中的name进行匹配,必须实现,name的最大空间是32个字节(可以修改),然后id_table中的另外一个变量driver_data是这个硬件的特有信息,可以在有需要的时候实现。probe,和remove是必须实现的,当probe自动运行的时候,可以在里面进行字符设备处理的框架和初始化SPI设备,设备操作函数在file_operations中保存,提供APP使用,remove一般是将probe注册的东西给注销。
driver是SPI驱动的父类,所有的驱动函数都有此父类,这是每个设备都必须有的信息,在里面最重要的变量是of_match_table变量,里面保存了采用设备树方式的驱动兼容性,必须实现compatible变量,它要和设备树中的compatible一样,才能匹配,*data可以在有需要的时候实现,这里面一般保存此设备的特有信息。
当此结构体实现完成,就可以将其注册到Linux内核中,然后上层APP就可以进行SPI设备控制了。
如果某些信息不准确,可以参考第180行上面的注释,以注释为准!

4). SPI传输信息结构体

这是SPi控制器进行数据传输时,保存设备数据信息的结构体,有两个设备结构体,其具体保存传输数据信息的地方是spi_transfer,然后通过spi_message_add_tail将spi_transfer添加到spi_message中,然后调用传输函数进行数据传送。其中spi_transfer结构体定义在/include/linux/spi/spi.h的的第603行,其内容如下:

struct spi_transfer {
	/* it's ok if tx_buf == rx_buf (right?)
	 * for MicroWire, one buffer must be null
	 * buffers must work with dma_*map_single() calls, unless
	 *   spi_message.is_dma_mapped reports a pre-existing mapping
	 */
	const void	*tx_buf;	/* 要发送数据的地址	*/
	void		*rx_buf;	/* 要接受数据的地址	*/
	unsigned	len;		/* 数据长度	*/
	
	/* DMA信息,SPI控制器采用的DMA形式	*/
	dma_addr_t	tx_dma;
	dma_addr_t	rx_dma;
	struct sg_table tx_sg;
	struct sg_table rx_sg;
	......
	struct list_head transfer_list;	/* 传输数据时需要将此结构体添加到spi_message就是使用的这个	*/
};

其内容中 tx_buf, rx_buf, len是我们传输信息必须实现的,len只有一个是因为SPI数据传输模式所决定的,SPI数据传输是每发送一个数据的同时也接收一个数据的全双工数据形式。transfer_list变量是将此结构体添加到spi_message结构体中的链表,采用此链表的原因是,当我们发送多条信息时,此结构体可以被spi_message排序。

spi_message结构体就是我们在发送数据时送到发送函数的一个结构体,其内容在/include/linux/spi/spi.h的的第660行,其内容如下:

struct spi_message {
	struct list_head	transfers;	/*保存添加的spi_transfer结构体数据 */

	struct spi_device	*spi;	/* spi设备信息 */

	unsigned		is_dma_mapped:1;

	/* REVISIT:  we might want a flag affecting the behavior of the
	 * last transfer ... allowing things like "read 16 bit length L"
	 * immediately followed by "read L bytes".  Basically imposing
	 * a specific message scheduling algorithm.
	 *
	 * Some controller drivers (message-at-a-time queue processing)
	 * could provide that as their default scheduling algorithm.  But
	 * others (with multi-message pipelines) could need a flag to
	 * tell them about such special cases.
	 */

	/* completion is reported through a callback */
	void			(*complete)(void *context);	/* 采用非阻塞的方式发送数据需要的实现的回调函数	*/
	void			*context;	
	unsigned		frame_length;
	unsigned		actual_length;
	int			status;

	/* for optional use by whatever driver currently owns the
	 * spi_message ...  between calls to spi_async and then later
	 * complete(), that's the spi_master controller driver.
	 */
	struct list_head	queue;	/* 当阻塞时 代表本身的结点	*/
	void			*state;
};

这个结构体就是最终使用spi数据发送函数所需要的一个结构体变量,一般数据发送时没需要先初始化spi_message结构体数据,然后将spi_transfer结构体数据添加到spi_message结构体中的transfers变量中(将spi_transfer添加到transfers中可以点此,我的另一篇笔记),最后再执行数据发送函数。发送函数有阻塞方式和非阻塞的方式,如果使用非阻塞的方式,需要将complete此回调函数实现。

3. SPI设备树

现在我们一般使用设备树来表示某个设备信息,SPI设备也不例外,在设备树中,一般SPI控制器的设备树信息不需要我们来写,这是有半导体厂商的开发人员已经写好了,并且SPI控制器驱动已经 继承到了Linux内核中,不需要我们管,我们只需要将修改某个我们需要的值即可,一般我们都在厂商定义好的SPI结点下添加我们自己的SPI设备结点。比如我使用的iMX6ull芯片,我添加icm20608SPI设备只需要在ecspi3结点下添加即可。
在板子设备树中(xxx-xxx.dts):

&iomux结点中有如下:
pinctrl_ecspi3: icm20608{
			fsl,pins = <
				MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20		0x10b0	/* CS片选	*/
				MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 	0x10b1	/* SCLK	*/
				MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI		0x10b1	/* MOSI	*/
				MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO		0x10b1	/* MISO	*/
			>;
		};

这是spi控制器所使用的的io复用,此引脚连接着SPI设备,在添加io复用时,一般要注意此io引脚是否被使用,一定要注意未使用,可以通过搜索UART2_TX_DATA来判断是否被使用,如果被使用,如果使用的这个节点无可厚非,可以将其注释掉。


添加设备结点信息:
&ecspi3 {
	fsl,spi-num-chipselects = <1>;	/* 此SPI设备上的片选芯片数量	*/
	cs-gpio = <&gpio1 20 GPIO_ACTIVE_LOW>;	/* 不能使用cs-gpios!因为使用这个片选信号会被硬件接管	*/
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_ecspi3>;
	status = "okay";

	spidev:	icm20608@0	{	/* 0的意思是icm20608连接在低第0个通道	*/
		compatible = "cui,icm20608";
		spi-max-frequency = <8000000>;	/* 此芯片最高支持到8Mhz*/
		reg = <0>;
	};
};

**此节点的具体描述信息可参考Documentation\devicetree\bindings\spi\fsl-imx-cspi.txt查看,**文档中规定了cs-gpios节点,可是在上面使用的是cs-gpio,原因是我们要自己手动控制此设备的片选信号,如果使用cs-gpios,就会由spi控制器接管cs-gpios可以添加多个片选引脚,则此时fsl,spi-num-chipselects的内容就要写成添加了几个片选引脚。
pinctrl-names的dafault意思是此设备所使用的的iomux是pinctrl-0,如果有多个比如pinctrl-1 pinctrl-2 可以根据default在第几个位置来判断使用哪个iomux。
在子设备结点中,icm20608@0中的0是所使用的的第0个通道(其实也就是第0个片选引脚),有compatible设备兼容性,reg代表所使用的的是上面cs-gpios所定义的第几个片选引脚。

4. SPI设备驱动编写方法

1). SPI设备驱动注册和注销

设备的注册与注销要使用spi_driver结构体,所以要先定义此结构体并实现其内容,本次我使用的是icm20608陀螺仪设备,所以一下有大量的icm20608的名字

static struct spi_driver icm20608_driver = {
    .id_table = icm20608_id_table,
    .probe = xxx_probe,
    .remove = xxx_remove,
    .driver = {
        .name = "icm20608_driver",
        .owner = THIS_MODULE,
        .of_match_table = icm20608_match_table,
    },
};

probe是设备和驱动匹配时自动加载运行的函数,我们必须实现,一般在这个函数进行驱动的初始化,然后定义一个字符设备的框架,remove函数是设备移除时自动运行的函数,一般将probe所定义的字符设备框架给卸载掉。
icm20608_id_table是传统设备的匹配方法,当我们编写驱动是,也应尽量兼容传统设备的匹配方法,增加驱动的适用性,其具体内容如下:

/* 传统设备匹配方法 */
static struct spi_device_id icm20608_id_table[] = {
    {.name = "cui,icm20608",},
    {   },
};

driver是spi_driver结构体的父类,它里面是每个驱动必须有的基本数据,所以每种驱动都会包含,此驱动里面的name就是当被加载以后在/sys/bus/spi/driver/的名字,of_match_table一般定义采用设备树方法的设备兼容性,其内容如下:

/* 采用设备树匹配方法   */
static struct of_device_id	icm20608_match_table[] = {
    {.compatible = "cui,icm20608",},
    {   },	/* 必须要有一个空的*/
};

当实现完成spi_driver设备数据时,我们需要将其注册到Linux内核中,这样所上层APP就可以使用了。注册如下:

static int __init icm20608_init(void)
{
    /* 当注册后次spi的操作函数就可以使用了  */
    return spi_register_driver(&icm20608_driver);
}

static void __exit icm20608_exit(void)
{
    spi_unregister_driver(&icm20608_driver);
}
module_init(icm20608_init);
module_exit(icm20608_exit);

就是基本的注册框架也可以使用include/linux/spi/spi.h的214行左右的宏定义来初始化:

module_spi_driver(icm20608_driver);

关于此初始化原理可以点此访问(我的另一篇关于module_xxx_driver宏定义的笔记)

2). probe和remove

设备和驱动匹配时这个probe函数就会自动运行,其内容如下:

static int	xxx_probe(struct spi_device *spi)
{
	...... /* 基本字符初始化过程,比如dev_t cdev class device等等*/
	
	/*因为我的SPI设备片选未使用SPI控制器接管,所以需要我手动的进行初始化*/
	icm20608dev->nd = of_find_node_by_path("/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02010000");
	icm20608dev->cs_gpio = of_get_named_gpio(icm20608dev->nd,
                                             "cs-gpio", 0);
	ret = gpio_direction_output(icm20608dev->cs_gpio, 1);
	
	/* 然后对我使用SPI控制器的spol和chpa进行修改	*/
	spi->mode = SPI_MODE_0; /* cpol=0,cpha=0 */
    spi_setup(spi);
    
    /* 将spi_device保存起来供发送函数使用	*/
    icm20608dev->private_data = spi;    /* 设置私有数据 */

	/* 进行SPI设备初始化	*/
	xxx_reginit();
	......	/* 其他处理,比如错误处理*/
}

remove函数是设备注销时自动运行的就是将probe函数初始化的框架等东西进行注销

static int	xxx_remove(struct spi_device *spi)
{
	......	/* 字符设备框架注销	*/
}

3). SPI设备读写函数

当将probe和remove框架完成了以后就需要将SPI控制函数进行封装,封装成针对ICM20608所需要的函数,便于操作的函数,其中写icm20608寄存器函数是:

/* 向icm20608写寄存器   */
static s32 xxx_write_regs(struct icm20608_dev *dev,
                                u8 reg, u8 *buf, int len)
{
    int ret;
    unsigned char txdata = 0;
    struct spi_message m;
    struct spi_transfer *t;
    struct spi_device *spi = (struct spi_device *)dev->private_data;

    t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);   /* kzalloc是申请内存然后顺便将内存清零  */
    gpio_set_value(dev->cs_gpio, 0);    /* 拉低片选 */

    /* 1. 写要写的地址    */
    txdata = reg & (~0x80); /* 写入要将bit8清零 */
    t->tx_buf = &txdata;
    t->len = 1;
    spi_message_init(&m);       /* 初始化spi_message*/
    spi_message_add_tail(t, &m);    /* 将spi_transfer添加到spi_message*/
    ret = spi_sync(spi, &m);    /* 同步发送 */

    /* 2. 写内容到寄存器    */
    t->tx_buf = buf,
    t->len = len;
    spi_message_init(&m);
    spi_message_add_tail(t, &m);
    ret = spi_sync(spi, &m);

    kfree(t);
    gpio_set_value(dev->cs_gpio, 1);
    return ret;
}

一般都将spi_transfer结构体定义为指针形式(我也不知道为什么,但是其他驱动就是这样写的),然后用kzalloc函数进行动态分配内存,kzalloc函数和kmalloc函数使用方法一样,只不过kzlloc函数会自动将所申请的内存块进行清零,如果使用kmalloc函数,则需要我们使用memset函数手动清零,所以建议使用kzalloc函数。
让后对SPI设备写寄存器数据时,先要写一个要写寄存器数据的地址,因为是写操作,要将寄存器地址的bit8清零,之后先初始化spi_message结构体,将spi_transfer结构体添加到spi_message中,最后使用同步发送的方式发送数据,同步发送方式有可能产生阻塞。发送完成第二次发送所需要发送的数据。发送前先拉低片选才能将数据发送出去,发送完成后拉高片选,取消选中,释放申请的内存。
向icm20608读寄存器数据时和发送差不多:

static int xxx_read_regs(struct icm20608_dev *dev,
                            u8 reg, void *buf, int len)
{
    int ret;
    unsigned char txdata;
    struct spi_message m;
    struct spi_transfer *t = NULL;
    struct spi_device *spi = (struct spi_device *)dev->private_data;

    /* 申请完内存需要将内存清零,否则会出现问题 */
    t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
    memset(t, 0, sizeof(struct spi_transfer));

    /* 拉低片选 */
    gpio_set_value(dev->cs_gpio, 0);

    /* 1. 读数据之前要先写入读取的寄存器地址   */
    txdata = reg | 0x80;    /* 写数据的时候寄存器地址bit8要设置1*/
    t->tx_buf = &txdata;
    t->len = 1; /* 发送一个寄存器字节   */
    spi_message_init(&m);   /* 初始化spi_message    */
    spi_message_add_tail(t, &m);    /* 将spi_transfer添加到spi_message */
    /*
     * 此时用的是同步发送,同步发送是阻塞形式的发送
     * 采用非同步发送的是非阻塞的方法,
     * API:nt spi_async(struct spi_device *spi, struct spi_message *message);
     * 采用非同步发送需要设置spi_message中:void (*complete)(void *context);
     * 此函数是报告发送的情况。
     */
    ret = spi_sync(spi, &m);    /* 同步发送 */

    /*2. 读取数据   */
    txdata = 0xff;   /* 此时无意义,随便定义即可 */
    t->rx_buf = buf;
    t->len = len;
    spi_message_init(&m);
    spi_message_add_tail(t, &m);
    ret = spi_sync(spi, &m);

    kfree(t);
    gpio_set_value(dev->cs_gpio, 1);

    return ret;
}

对过程和发送数据的过程差不多基本上没有什么区别,只不过就是tx_buf和rx_buf的不同。

上面函数是将spi操控函数进行多个数据发送的封装,有时候我们只需要发送或者读取一个数据,这是使用上面的函数会有点麻烦,毕竟要多输入几个字符,所以我们可以将上面的函数进一步封装成发送或接受单个数据的形式。内容如下:

/* 读取icm20608指定寄存器,读取一个寄存器   */
static unsigned char xxx_read_reg(struct icm20608_dev *dev, u8 reg)
{
    u8 data = 0;
    xxx_read_regs(dev, reg, &data, 1);
    return data;
}

/* 写icm20608指定寄存器,写一个数据到寄存器 */
static void xxx_write_reg(struct icm20608_dev *dev, u8 reg, u8 data)
{
    xxx_write_regs(dev, reg, &data, 1);
}

4). 初始化设备函数和访问SPI设备函数

将spi发送接收数据针对我们的SPI设备封装好以后,我们就可以使用来控制SPI设备了,先进性设备初始化:

static void xxx_reginit(void)
{
	...... /* 设备初始化的集体过程 */
}

然后可以读取我们需要的数据:

static void icm20608_readdata(struct icm20608_dev *dev)
{
	...... /* 读取数据的具体过程 将数据保存在了dev中*/
}

5). 对访问设备函数进行封装给上层使用

当我们已经可以直接操作SPI设备时,我们可能需要将SPI设备操控函数按照上层所需要的的API形式进行封装,比如经常使用的struct file_operations结构体形式.等等。

发布了15 篇原创文章 · 获赞 7 · 访问量 273

猜你喜欢

转载自blog.csdn.net/weixin_42397613/article/details/105018016