当前位置:首页>Linux>Linux驱动学习日记(26) Linux SPI 驱动

Linux驱动学习日记(26) Linux SPI 驱动

  • 2026-09-10 10:56:08
Linux驱动学习日记(26) Linux SPI 驱动

Linux 下的 SPI 驱动框架简介

  SPI 驱动框架和 I2C 很类似,都分为主机控制器驱动和设备驱动,主机控制器也就是 SOC 的 SPI 控制器接口。

SPI 主机驱动

  SPI 主机驱动也就是 SOC 的 SPI 控制器驱动,类似 I2C 驱动里面的适配器驱动。Linux 使用 spi_controller 表示 SPI 主机驱动,spi_controller 是个结构体,定义在 include/linux/spi/spi.h:

structspi_controller {structdevicedev;structlist_headlist;/* Other than negative (== assign one dynamically), bus_num is fully        * board-specific.  usually that simplifies to being SOC-specific.        * example:  one SOC has three SPI controllers, numbered 0..2,        * and one board's schematics might show it using SPI-2.  software        * would normally use bus_num=2 for that controller.        */    s16  bus_num;/* chipselects will be integral to many controllers; some others        * might use board-specific GPIOs.        */    u16  num_chipselect;/* Some SPI controllers pose alignment requirements on DMAable        * buffers; let protocol drivers know about these requirements.        */    u16  dma_alignment;/* spi_device.mode flags understood by this controller driver */    u32  mode_bits;/* spi_device.mode flags override flags for this controller */    u32  buswidth_override_bits;/* Bitmask of supported bits_per_word for transfers */    u32  bits_per_word_mask;······

  SPI 主机驱动的核心就是申请 spi_controller,然后初始化,最后向 Linux 内核注册。

1、spi_master 申请与释放  spi_alloc_master 函数用于申请 spi_master,函数原型如下:

staticinline struct spi_controller *spi_alloc_master(struct device *host, unsignedint size)

  函数参数和返回值含义如下   dev:设备,一般是 platform_device 中的 dev 成员变量。   size:私有数据大小,可以通过 spi_maser_get_devdata 函数获取到这些私有数据。   返回值:申请到的 spi_controller。

  spi_master 的释放通过 spi_controller_put 来完成,当我们删除一个 SPI 主机驱动的时候就需要释放掉前面申请的 spi_master,其函数原型如下:

staticinlinevoidspi_controller_put(struct spi_controller *ctlr)

  其中,ctrl 就是要释放的 spi_master。

2、spi_master 的注册与注销  当 spi_master 初始化完成以后就要将其注册到 Linux 内核,spi_master 注册函数为 spi_register_controller,函数原型如下:

intspi_register_controller(struct spi_controller *ctlr);

  其中 ctlr 表示要注册的 spi_controller,返回值为 0 时成功,为负值时失败。   如果要注销的话可以使用 spi_unregister_controller 函数,此函数原型如下

voidspi_unregister_controller(struct spi_controller *ctlr);

  其中,ctlr 就是要注销的 spi_controller。

SPI 设备驱动

  spi 设备驱动和 i2c 设备驱动也很类似,Linux 内核使用 spi_driver 来表示 spi 设备驱动,其原型如下:

structspi_driver {conststructspi_device_id *id_table;int      (*probe)(struct spi_device *spi);void      (*remove)(struct spi_device *spi);void      (*shutdown)(struct spi_device *spi);structdevice_driverdriver;};

  可以看出,spi_driver 和 i2c_driver、platform_driver 基本一样,当 SPI 设备和驱动匹配成功以后 probe 函数就会执行。   同样的,spi_driver 初始化完成以后也需要向 Linux 内核注册,spi_driver 注册函数为 spi_register_driver,其原型如下:

#define spi_register_driver(driver) \    __spi_register_driver(THIS_MODULE, driver)

  driver 就表示要注册的 spi_driver,返回值为 0 表示注册成功,为负值表示失败。   注销 SPI 设备驱动以后也需要注销掉前面注册的 spi_driver,使用 spi_unregister_driver 函数完成 spi_driver 的注销,函数原型如下:

voidspi_unregister_driver(struct spi_driver *sdrv)

  其中,sdrv 就表示需要注销的 spi_driver。

SPI 设备驱动编写流程

SPI 设备信息概述

1、IO 的 pinctrl 子节点的创建与修改  首先肯定是根据所使用的 IO 来创建和修改 pinctrl 子节点,一定要注意检查相应的 IO 有没有被其他设备所占用!!!

2、SPI 设备节点的创建与修改  采用设备树的情况下,SPI 设备信息描述就通过创建相应的设备子节点来完成,我们可以打开 imx6ull-14x14-evk.dtsi 文件,找到 spi 相关的配置:

spi-4 {    compatible = "spi-gpio";    pinctrl-names = "default";    pinctrl-0 = <&pinctrl_spi4>;    status = "okay";    gpio-sck = <&gpio5 11 0>;               // 配置时钟线    gpio-mosi = <&gpio5 10 0>;              // 配置数据线(主到从)    cs-gpios = <&gpio5 7 GPIO_ACTIVE_LOW>;  // 配置片选线,设置为低有效     num-chipselects = <1>;                  // 配置设备数量,表示只有 1 个设备    #address-cells = <1>;    #size-cells = <0>;    gpio_spi: gpio@0 {                      // 表示通道 0        compatible = "fairchild,74hc595";   // 设备类型        gpio-controller;                            #gpio-cells = <2>;        reg = <0>;        registers-number = <1>;        spi-max-frequency = <100000>;       // spi最大频率        enable-gpios = <&gpio5 8 GPIO_ACTIVE_LOW>;  // 使能控制引脚,低有效    };};

SPI 设备数据收发处理流程

  SPI 设备驱动的核心时 spi_driver,这个我们之前学习过了。当我们向Linux内核注册成功以后就可以使用 SPI 核心层提供的 API 函数来对设备进行读写操作了。首先是 spi_transfer 结构体,此结构体用于描述 SPI 传输信息,其内容如下:

structspi_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    */constvoid     *tx_buf;void          *rx_buf;unsigned     len;dma_addr_t     tx_dma;dma_addr_t     rx_dma;structsg_tabletx_sg;structsg_tablerx_sg;unsigned     dummy_data:1;unsigned     cs_off:1;unsigned     cs_change:1;unsigned     tx_nbits:3;unsigned     rx_nbits:3;#define     SPI_NBITS_SINGLE     0x01 /* 1bit transfer */#define     SPI_NBITS_DUAL          0x02 /* 2bits transfer */#define     SPI_NBITS_QUAD          0x04 /* 4bits transfer */    u8          bits_per_word;structspi_delaydelay;structspi_delaycs_change_delay;structspi_delayword_delay;    u32          speed_hz;    u32          effective_speed_hz;unsignedint     ptp_sts_word_pre;unsignedint     ptp_sts_word_post;structptp_system_timestamp *ptp_sts;bool          timestamped;structlist_headtransfer_list;#define SPI_TRANS_FAIL_NO_START     BIT(0)    u16          error;};

  其中,tx_buf 保存着要发送的数据,rx_buf 保存接收到的数据。len 是要进行传输的数据长度,由于 SPI 是全双工通信,因此在一次通信中接收的字节数都是一样的。   spi_transfer 需要组织成 spi_message,spi_message 也是一个结构体,内容如下:

structspi_message {structlist_headtransfers;structspi_device    *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_controller controller driver.     */structlist_headqueue;void            *state;/* List of spi_res reources when the spi message is processed */structlist_headresources;/* spi_prepare_message() was called for this message */bool            prepared;};

  在使用 spi_message 之前需要对其进行初始化,spi_message 初始化函数为 spi_message_init,函数原型如下:

voidspi_message_init(struct spi_message *m)

  其中,m 就表示要初始化的 spi_message。   spi_message 准备好以后就可以进行数据传输了,数据传输分为同步传输和异步传输,同步传输会阻塞的等待 SPI 数据传输完成,同步传输函数为 spi_sync,函数原型如下:

voidspi_message_add_tail(struct spi_transfer *t, struct spi_message *m)

  其中,t 表示要添加到队列中的 spi_transfer,m 表示要加入的 spi_message。

  spi_message 准备好以后就可以进行数据传输了,数据传输分为同步传输和异步传输,同步传输会阻塞的等待 SPI 数据传输完成,同步传输函数为 spi_sync,函数原型如下:

intspi_sync(struct spi_device *spi, struct spi_message *message)

  其中,spi 就是要进行数据传输的 spi_device,message 表示要传输的 spi_message。

  异步传输不会阻塞的等待 SPI 数据传输完成,异步传输需要设置 spi_message 中的 complete 成员变量,其是一个回调函数,当 SPI 异步传输完成以后此函数就会被调用。SPI 异步传输函数为 spi_async,函数原型如下:

intspi_async(struct spi_device *spi, struct spi_message *message)

  其中,spi 就是要进行数据传输的 spi_device,message 就是要传输的 spi_message。

  综上所述,SPI 数据传输步骤如下:   ①、申请并初始化 spi_transfer,设置 spi_transfer 的 tx_buf 成员变量,然后设置 rx_buf 成员变量,最后设置 len 成员变量。   ②、使用 spi_message_init 函数初始化 spi_message。   ③、使用 spi_message_add_tail 函数将前面设置好的 spi_transfer 添加到 spi_message 队列中。   ④、使用 spi_sync 函数完成 SPI 数据同步传输。

硬件分析

  我们使用 SPI 通信的 W25Q64 完成本次学习。查询原理图,得知可以将以下几个引脚复用为 SPI 功能。

图 1 spi原理图

代码编写

修改设备树

  打开 imx6ull-hcw-emmc.dtsi 文件,在 iomuxc 节点中添加一个新的子节点来描述 W25Q64 所使用的 SPI 引脚。内容如下:

pinctrl_ecspi3: w25q64{    fsl,pins = <        MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20     0x10b0        MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK    0x10b1        MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO      0x10b1        MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI      0x10b1    >;};

  接下来,我们在设备树文件中添加以下代码

&ecspi3 {    /delete-property/ dmas;    /delete-property/ dma-names;    fsl,spi-num-chipselects = <1>;    cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;    pinctrl-names = "default";    pinctrl-0 = <&pinctrl_ecspi3>;    status = "okay";    w25q64@0 {        compatible =  "hcw,w25q64";        reg = <0>;        spi-max-frequency = <1000000>;    };};

  检查引脚是否冲突,将冲突的设备设为 disabled,编译设备树,并使用新的 dtb 文件启动 Linux 内核。

编写代码

  新建文件,写入以下代码。   1、设备结构体

#define     W25Q64_CNT      1#define     W25Q64_NAME     "w25q64"structw25q64_dev{dev_t devid;structcdevcdev;structclass *class;structdevice *device;structdevice_node *nd;int major;void *private_data;int cs_gpio;};staticstructw25q64_devw25q64dev;

2、w25q64 spi 设备的注册与注销  对于 spi 设备驱动,首先就是要初始化并向系统注册 spi_driver,w25q64 的 spi_driver 初始化、注册、注销代码如下:

staticconststructof_device_idw25q64_of_match[] = {    {.compatible = "hcw,w25q64"},    {}};staticstructspi_driverw25q64_drv = {    .driver = {        .name = "w25q64_drv",        .owner = THIS_MODULE,        .of_match_table = w25q64_of_match,    }};staticint __init w25q64_init(void){return spi_register_driver(&w25q64_drv);}staticvoid __exit w25q64_exit(void){    spi_unregister_driver(&w25q64_drv);}module_init(w25q64_init);module_exit(w25q64_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("hcw");

3、probe&remove 函数

staticintw25q64_probe(struct spi_device *dev){if(w25q64dev.major){        w25q64dev.devid = MKDEV(w25q64dev.major, 0);        register_chrdev_region(w25q64dev.devid, W25Q64_CNT, W25Q64_NAME);    }else{        alloc_chrdev_region(&w25q64dev.devid, 0, W25Q64_CNT, W25Q64_NAME);        w25q64dev.major = MAJOR(w25q64dev.devid);    }    cdev_init(&w25q64dev.cdev, &w25q64_fops);    cdev_add(&w25q64dev.cdev, w25q64dev.devid, W25Q64_CNT);    w25q64dev.class = class_create(THIS_MODULE, W25Q64_NAME);if(IS_ERR(w25q64dev.class))    {return PTR_ERR(w25q64dev.class);    }    w25q64dev.device = device_create(w25q64dev.class, NULL, w25q64dev.devid, NULL, W25Q64_NAME);if(IS_ERR(w25q64dev.device))    {return PTR_ERR(w25q64dev.device);    }    dev->mode = SPI_MODE_0;    spi_setup(dev);    w25q64dev.private_data = dev;return0;}staticvoidw25q64_remove(struct spi_device *dev){    cdev_del(&w25q64dev.cdev);    unregister_chrdev_region(w25q64dev.devid, W25Q64_CNT);    device_destroy(w25q64dev.class, w25q64dev.devid);    class_destroy(w25q64dev.class);}

4、w25q64 读取 ID 函数  SPI 驱动最终是通过读写 w25q64 寄存器来实现的,因此需要编写对应的寄存器读写函数。

staticintw25q64_readID(struct w25q64_dev *dev, void *buf){int ret = -1;unsignedchar txdata[1];unsignedchar *rxdata;structspi_messagem;structspi_transfer *t;structspi_device *spi = (structspi_device *)dev->private_data;    t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);if(!t)    {return -ENOMEM;    }    rxdata = kzalloc(sizeof(char) * 4, GFP_KERNEL);if(!rxdata)goto out1;    txdata[0] = 0x90;    t->tx_buf = txdata;    t->rx_buf = rxdata;    t->len = 4;    spi_message_init(&m);    spi_message_add_tail(t, &m);    ret = spi_sync(spi, &m);if(ret)goto out2;memcpy(buf, rxdata + 1, 3); // 从 rxdata 的首地址往后偏移 1 个字节开始拷贝,用来过滤掉第一个无效数据out2:    kfree(rxdata);out1:    kfree(t);return ret;}

修复错误

  写好后,编译模块装载,运行测试程序,但是并没有如预想中的一样输出设备 ID。是怎么回事呢?我们查阅 STM32 平台下的标准代码可以得知,我们应该依次发送 90 00 00 00 ff ff。最终代码放在最后

uint16_tw25qxx_read_id(void){uint16_t id = 0;//片选有效    SPI_CS = 0;//发送0x90,读取厂商ID和设备ID    spi_read_writeByte(0x90);//发送24位地址(3个字节)  前面两个字节可以任意,第三个字节必须是0x00    spi_read_writeByte(0x00);    spi_read_writeByte(0x00);    spi_read_writeByte(0x00);//一定是0x00//随便发2个字节的数据    id |= spi_read_writeByte(0xFF)<<8; //id:0xEF17  厂商ID:0xEF        id |= spi_read_writeByte(0xFF);    //设备ID:0x17//片选无效    SPI_CS = 1;return id;}

图 2 读到ID

/* * @Author: 胡城玮 * @FilePath: w25q64.c * @Date: 2026-03-22 * @Description:  * @Version: 0.1 */#include<linux/types.h>#include<linux/kernel.h>#include<linux/delay.h>#include<linux/init.h>#include<linux/module.h>#include<linux/errno.h>#include<linux/gpio.h>#include<linux/cdev.h>#include<linux/device.h>#include<linux/of_gpio.h>#include<linux/semaphore.h>#include<linux/timer.h>#include<linux/i2c.h>#include<linux/spi/spi.h>#include<linux/of.h>#include<linux/of_address.h>#include<linux/of_gpio.h>#include<linux/platform_device.h>#include<asm/mach/map.h>#include<asm/uaccess.h>#include<asm/io.h>#define     W25Q64_CNT      1#define     W25Q64_NAME     "w25q64"structw25q64_dev{dev_t devid;structcdevcdev;structclass *class;structdevice *device;structdevice_node *nd;int major;void *private_data;int cs_gpio;};staticstructw25q64_devw25q64dev;staticconststructof_device_idw25q64_of_match[] = {    {.compatible = "hcw,w25q64"},    {}};staticconststructspi_device_idw25q64_id[] = {    { "w25q64", 0 },    { }};MODULE_DEVICE_TABLE(of, w25q64_of_match);staticintw25q64_readID(struct w25q64_dev *dev, void *buf){int ret = -1;unsignedchar txdata[6] = {0x90, 0x00, 0x00, 0x00,0xff,0xff};unsignedchar *rxdata;structspi_messagem;structspi_transfer *t;structspi_device *spi = (structspi_device *)dev->private_data;    t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);if(!t)    {return -ENOMEM;    }    rxdata = kzalloc(sizeof(char) * 6, GFP_KERNEL);if(!rxdata)goto out1;    t->tx_buf = txdata;    t->rx_buf = rxdata;    t->len = 6;    spi_message_init(&m);    spi_message_add_tail(t, &m);    ret = spi_sync(spi, &m);if(ret)goto out2;memcpy(buf, rxdata + 4, 2); // 从 rxdata 的首地址往后偏移 4 个字节开始拷贝,用来过滤掉4个无效数据out2:    kfree(rxdata);out1:    kfree(t);return ret;}staticintw25q64_open(struct inode *nd, struct file *filp){    filp->private_data = &w25q64dev;return0;}staticssize_tw25q64_read(struct file *filp, char __user *buf, size_t cnt, loff_t *oft){int ret = 0;uint8_t data[3];    ret = w25q64_readID(&w25q64dev, data);if(ret)return ret;    printk("read id = %02x %02x %02x\n", data[0], data[1], data[2]);int err = copy_to_user(buf, data, 3);if(err)    {return -EFAULT;    }return3;}staticintw25q64_release(struct inode *inode, struct file *filp){return0;}staticstructfile_operationsw25q64_fops = {    .owner = THIS_MODULE,    .read = w25q64_read,    .open = w25q64_open,    .release = w25q64_release};staticintw25q64_probe(struct spi_device *dev){    printk("matched!!!\r\n");if(w25q64dev.major){        w25q64dev.devid = MKDEV(w25q64dev.major, 0);        register_chrdev_region(w25q64dev.devid, W25Q64_CNT, W25Q64_NAME);    }else{        alloc_chrdev_region(&w25q64dev.devid, 0, W25Q64_CNT, W25Q64_NAME);        w25q64dev.major = MAJOR(w25q64dev.devid);    }    cdev_init(&w25q64dev.cdev, &w25q64_fops);    cdev_add(&w25q64dev.cdev, w25q64dev.devid, W25Q64_CNT);    w25q64dev.class = class_create(THIS_MODULE, W25Q64_NAME);if(IS_ERR(w25q64dev.class))    {return PTR_ERR(w25q64dev.class);    }    w25q64dev.device = device_create(w25q64dev.class, NULL, w25q64dev.devid, NULL, W25Q64_NAME);if(IS_ERR(w25q64dev.device))    {return PTR_ERR(w25q64dev.device);    }    dev->mode = SPI_MODE_0;    spi_setup(dev);    w25q64dev.private_data = dev;return0;}staticvoidw25q64_remove(struct spi_device *dev){    cdev_del(&w25q64dev.cdev);    unregister_chrdev_region(w25q64dev.devid, W25Q64_CNT);    device_destroy(w25q64dev.class, w25q64dev.devid);    class_destroy(w25q64dev.class);}staticstructspi_driverw25q64_drv = {    .driver = {        .name = "w25q64_drv",        .owner = THIS_MODULE,        .of_match_table = w25q64_of_match,    },    .probe = w25q64_probe,    .remove = w25q64_remove,    .id_table = w25q64_id,};staticint __init w25q64_init(void){return spi_register_driver(&w25q64_drv);}staticvoid __exit w25q64_exit(void){    spi_unregister_driver(&w25q64_drv);}module_init(w25q64_init);module_exit(w25q64_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("hcw");

最新文章

随机文章