• 正点原子嵌入式linux驱动开发——Linux SPI驱动


    到目前为止的学习笔记,已经介绍了Linux下的platform总线框架、I2C总线框架,本篇笔记将介绍Linux下的SPI总线框架。与I2C总线一样,SPI是物理总线,也是一种很常用的串行通信协议。本章就来学习如何在Linux下编写SPI总线接口的设备驱动。本章实验的最终目的就是驱动STM32MP1开发板上的ICM-20608这个SPI接口的六轴传感器,可以在应用程序中读取ICM-20608的原始传感器数据。

    SPI&ICM-20608简介

    SPI简介

    SPI的基础知识在之前学习裸机开发的时候学过了,这里就不再赘述。

    SPI设备连接图

    SPI四种工作模式

    第一种工作模式下的SPI时序图

    STM32MP1 SPI简介

    STM32MP1自带的SPI全称为:Serial peripheral interface。SPI特性如下:

    1. 全双工同步串口接口。
    2. 半双工模式。
    3. 可配置的主/从模式。
    4. 支持I2S协议。
    5. 在达到FIFO阈值、超时、操作完成以及发生访问错误时产生中断。
    6. 允许16位,24位或者32位数据长度。
    7. 支持软件片选和硬件片选。

    STM32MP1的SPI可以工作在主模或从模式,本章使用主模式,此芯片有6个SPI
    ,其中SPI1-3是支持I2S协议
    。在主模式下,可以选择硬件片选和软件片选,如果使用了硬件片选,那么每一个SPI只支持一个外设,软件片选就可以支持无数个外设,本章实验不使用硬件片选信号,因为硬件片选信号只能使用指定的片选IO,软件片选的话可以使用任意的IO

    ICM-20608简介

    ICM-20608是InvenSense出品的一款6轴MEMS传感器,包括 3轴加速度和3轴陀螺仪。

    ICM-20608尺寸非常小,只有3x3x0.75mm,采用16P的LGA封装。ICM-20608内部有一个512字节的FIFO。陀螺仪的量程范围可以编程设置,可选择±250,±500,±1000和±2000°/s,加速度的量程范围也可以编程设置,可选择±2g,±4g,±8g和±16g。陀螺仪和加速度计都是16位的ADC,并且支持I2C和SPI两种协议,使用I2C接口的话通信速度最高可以达到400KHz,使用SPI接口的话通信速度最高可达到8MHz。开发板上的ICM-20608通过SPI接口和STM32MP157连接在一起。ICM-20608特性如下:

    1. 陀螺仪支持X,Y和 Z三轴输出,内部集成16位ADC,测量范围可设置±250,±500,±1000和±2000°/s。
    2. 加速度计支持X,Y和Z轴输出,内部集成16位ADC,测量范围可设置±2g,±4g,±8g和±16g。
    3. 用户可编程中断。
    4. 内部包含512字节的FIFO。
    5. 内部包含一个数字温度传感器。
    6. 耐10000g的冲击。
    7. 支持快速I2C,速度可达400KHz。
    8. 支持SPI,速度可达8MHz。

    ICM-20608的3轴方向如下图所示:

    ICM-20608检测轴方向和极性

    ICM-20608的结构框图如下图所示:

    ICM-20608框图

    如果使用IIC接口的话,ICM-20608的AD0引脚决定I2C设备从地址的最后一位,如果AD0为0的话ICM-20608从设备地址是0X68,如果AD0为1的话ICM-20608从设备地址为0X69。

    本章使用SPI接口,与之前的I2C驱动芯片AP3216C一样,ICM-20608也是通过读写寄存器来配置和读取传感器数据,使用SPI接口读写寄存器需要16个时钟或者更多 (如果读写操作包括多个字节的话),第一个字节包含要读写的寄存器地址,寄存器地址最高位是读写标志位,如果是读的话寄存器地址最高位要为1,如果是写的话寄存器地址最高位要为0,剩下的7位才是实际的寄存器地址,寄存器地址后面跟着的就是读写的数据。下图列出了本章实验用到的一些寄存器和位:

    ICM-20608寄存器图1
    ICM-20608寄存器图2

    Linux下SPI驱动框架

    SPI总线框架和I2C总线框架很类似,都采用了主机控制器驱动和设备驱动分离的思想;
    主机控制器也就是SoC的SPI控制器,例如STM32MP1的SPI控制器;而设备驱动对应的则是挂在SPI总线下的从机设备驱动程序。主机控制器针对具体的SOC平台,例如STM32MP1对于同一个SOC平台来说,SPI控制器驱动程序是不用动的,不管外接的是什么SPI从机设备,对应的控制器驱动程序都一样,所以重点就落在了种类繁多的SPI从机设备驱动开发了。SPI控制器驱动程序一般是不需要驱动开发工程师自己编写,SOC厂商会提供相应的主机驱动程序。

    Linux内核当中,与I2C总线框架一样,SPI总线框架(也可以叫做SPI子系统)也可以分为三个部分

    • SPI核心层:SPI核心层是Linux的SPI子系统的核心代码部分,提供了核心数据结构的定义、SPI控制器驱动和设备驱动的注册、注销、管理等API。其为硬件平台无关层,向下屏蔽了物理总线控制器的差异,定义了统一的访问策略和接口;其向上提供了统一的接口,以便SPI设备驱动通过总线控制器进行数据收发。在Linux系统中,SPI核心层的代码位于drivers/spi/spi.c。
    • SPI控制器驱动层:每种处理器平台都有自己的SPI控制器驱动程序,它的职责是为系统中的SPI总线实现相应的读写方法。例如 STM32MP1就有6个SPI,那么就有6个SPI控制器,每个控制器都有一条特定的SPI总线的读写。SPI子系统使用struct spi_master数据结构体来描述SPI控制器。在内核源码drivers/spi目录下有很多以spi-xxxx.c命名的源文件,如下图所示:

    SPI控制器驱动源码

    这些文件就是具体平台对应的SPI控制器驱动程序,使用SPI核心层提供的接口向SPI子系统注册SPI控制器。

    • SPI设备驱动层:SPI从设备对应的驱动程序,比如一些SPI接口的芯片器件对应的驱动程序。

    SPI主机驱动

    SPI主机驱动就是SoC的SPI控制器驱动,类似I2C总线的适配器驱动。SPI子系统使用spi_master结构体来描述SPI控制器,其实spi_master是一个宏,这个宏定义在include/linux/spi/spi.h文件中,如下所示:

    #define spi_master spi_controller
    
    • 1

    所以由此可以知道,spi_master就是spi_controller结构体,该结构体定义在include/linux/spi/spi.h文件中,如下所示:

    示例代码 45. 2.1 spi_controller 结构体
    424 struct spi_controller {
    425     struct device dev; /* device 对象 */
    426
    427     struct list_head list;
    ......
    435     s16 bus_num; /* SPI 总线编号 */
    ......
    440     u16 num_chipselect; /* 片选 */
    441
    442     /* some SPI controllers pose alignment requirements on DMAable
    443      * buffers; let protocol drivers know about these requirements.
    444      */
    445     u16 dma_alignment;
    446
    447     /* spi_device.mode flags understood by this controller driver */
    448     u32 mode_bits /* 模式位 */
    ......
    455     /* limits on transfer speed */
    456     u32 min_speed_hz; /* SPI 控制器支持的最小传输速率 */
    457     u32 max_speed_hz; /* SPI 控制器支持的最大传输速率 */
    458
    459     /* other constraints relevant to this driver */
    460     u16 flags; /* 传输类型标志 */
    468
    469     /* flag indicating this is an SPI slave controller */
    470     bool slave; /* 标志该控制器是否为 SPI 从设备存在 */
    471
    472 /*
    473  * on some hardware transfer / message size may be constrained
    474  * the limit may depend on device transfer settings
    475  */
    476     size_t (*max_transfer_size)(struct spi_device *spi);
    477     size_t (*max_message_size)(struct spi_device *spi);
    478
    479     /* I/O mutex */
    480     struct mutex io_mutex
    481
    482     /* lock and mutex for SPI bus locking */
    483     spinlock_t bus_lock_spinlock;
    484     struct mutex bus_lock_mutex;
    485
    486     /* flag indicating that the SPI bus is locked for exclusive use */
    487     bool bus_lock_flag;
    ......
    495     int (*setup)(struct spi_device *spi)
    ......
    527     int (*transfer)(struct spi_device *spi,
    528                     struct spi_message *mesg);
    567     int (*prepare_transfer_hardware)(struct spi_controller *ctlr);
    568     int (*transfer_one_message)(struct spi_controller *ctlr,
    569                                 struct spi_message *mesg);
    ......
    607 };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    第495行,SPI控制器的setup函数,类似于初始化函数。

    第527行,SPI控制器的transfer函数,和i2c_algorithm中的master_xfer函数一样,控制器数据传输函数。

    第568行,transfer_one_message函数,也用于SPI数据发送,用于发送一个spi_message,SPI的数据会打包成spi_message,然后以队列方式发送出去

    SPI主机端最终会通过transfer函数与SPI设备进行通信,因此对于SPI主机控制器的驱动编写者而言transfer函数是需要实现的,因为不同的SOC其SPI控制器不同,寄存器都不一样。和I2C适配器驱动一样,SPI主机驱动一般都是SOC厂商去编写的,所以作为SOC的使用者,这一部分的驱动就不用操心了。

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

    spi_master申请与释放

    spi_alloc_master函数用于申请spi_master,函数原型如下:

    struct spi_controller *spi_alloc_master(struct device *host, 
    										unsigned int size)
    
    • 1
    • 2

    函数参数和返回值含义如下:

    • host:设备,一般是platform_device中的dev成员变量。
    • size:私有数据大小,可以通过spi_master_get_devdata函数获取到这些私有数据。
    • 返回值: 申请到的spi_controller,也就是spi_master。

    spi_master的释放通过spi_master_put函数来完成,当删除一个SPI主机驱动的时候就需要释放掉前面申请的spi_master,spi_master_put本质上是个宏

    #define spi_master_put(_ctlr) spi_controller_put(_ctlr)
    
    • 1

    spi_master_put函数最终通过调用spi_controller_put函数来完成spi_master释放,原型如下:

    void spi_master_put(struct spi_controller *ctlr) 
    
    • 1

    函数参数和返回值含义如下:

    • ctlr:要释放的spi_master。
    • 返回值:无。

    spi_master的注册与注销

    当spi_master初始化完成以后就需要将其注册到Linux内核,spi_master注册函数为spi_register_master,函数原型如下:

    int spi_register_master(struct spi_controller *ctlr) 
    
    • 1

    函数参数和返回值含义如下:

    • ctlr:要注册的spi_master。
    • 返回值:0,成功;负值,失败。

    如果要注销spi_master的话可以使用 spi_unregister_master函数,此函数原型为:

    void spi_unregister_master(struct spi_controller *ctlr) 
    
    • 1

    函数参数和返回值含义如下:

    • ctlr:要注销的spi_master。
    • 返回值:无。

    SPI设备驱动

    spi设备驱动和i2c设备驱动也很类似,Linux内核使用spi_driver结构体来表示spi设备驱
    ,在编写SPI设备驱动的时候需要实现spi_driver。spi_driver结构体定义在include/linux/spi/spi.h文件中,结构体内容如下:

    spi_driver结构体

    可以看出,spi_driver和i2c_driver、platform_driver基本一样,当SPI设备和驱动匹配成功以后probe函数就会执行。

    同样的,spi_driver初始化完成以后需要向Linux内核注册,spi_driver注册函数为spi_register_driver,函数原型如下:

    int spi_register_driver(struct spi_driver *sdrv) 
    
    • 1

    函数参数和返回值含义如下:

    • sdrv:要注册的spi_driver。
    • 返回值:0,注册成功;赋值,注册失败。

    注销SPI设备驱动以后也需要注销掉前面注册的spi_driver,使用spi_unregister_driver函数完成spi_driver的注销,函数原型如下:

    void spi_unregister_driver(struct spi_driver *sdrv) 
    
    • 1

    函数参数和返回值含义如下:

    • sdrv:要注销的spi_driver。
    • 返回值:无。

    spi_driver注册示例程序如下:

    示例代码 45.2.2.2 spi_driver 注册示例程序
    1  /* probe 函数 */
    2  static int xxx_probe(struct spi_device *spi)
    3  {
    4      /* 具体函数内容 */
    5      return 0;
    6  }
    7
    8  /* remove 函数 */
    9  static int xxx_remove(struct spi_device *spi)
    10 {
    11     /* 具体函数内容 */
    12     return 0;
    13 }
    14 /* 传统匹配方式 ID 列表 */
    15 static const struct spi_device_id xxx_id[] = {
    16     {"xxx", 0},
    17     {}
    18 };
    19
    20 /* 设备树匹配列表 */
    21 static const struct of_device_id xxx_of_match[] = {
    22     { .compatible = "xxx" },
    23     { /* Sentinel */ }
    24 };
    25
    26 /* SPI 驱动结构体 */
    27 static struct spi_driver xxx_driver = {
    28     .probe = xxx_probe,
    29     .remove = xxx_remove,
    30     .driver = {
    31         .owner = THIS_MODULE,
    32         .name  = "xxx",
    33         .of_match_table = xxx_of_match,
    34         },
    35     .id_table = xxx_id,
    36 };
    37
    38 /* 驱动入口函数 */
    39 static int __init xxx_init(void)
    40 {
    41     return spi_register_driver(&xxx_driver);
    42 }
    43
    44 /* 驱动出口函数 */
    45 static void __exit xxx_exit(void)
    46 {
    47     spi_unregister_driver(&xxx_driver);
    48 }
    49
    50 module_init(xxx_init);
    51 module_exit(xxx_exit);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    第1-36行,spi_driver结构体,需要SPI设备驱动人员编写,包括匹配表、 probe函数等。和i2c_driver、platform_driver一样,就不详细讲解了。

    第39-42行,在驱动入口函数中调用spi_register_driver来注册spi_driver。

    第45-48行,在驱动出口函数中调用spi_unregister_driver来注销spi_driver。

    SPI设备和驱动匹配过程

    SPI设备和驱动的匹配过程是由SPI总线来完成的,这点和platform、I2C等驱动一样,SPI总线为spi_bus_type,定义在drivers/spi/spi.c文件中,内容如下:

    spi_bus_type结构体

    可以看出,SPI设备和驱动的匹配函数为spi_match_device,函数内容如下:

    spi_match_device函数

    spi_match_device函数和i2c_match_device函数的对于设备和驱动的匹配过程基本一样。

    第352行,of_driver_match_device函数用于完成设备树设备和驱动匹配。比较SPI设备节
    点的compatible属性和of_device_id中的compatible属性是否相等,如果相当的话就表示SPI设备和驱动匹配。

    第356行,acpi_driver_match_device函数用于ACPI形式的匹配。

    第360行,spi_match_id函数用于传统的、无设备树的SPI设备和驱动匹配过程。比较SPI设备名字和spi_device_id的name字段是否相等,相等的话就说明SPI设备和驱动匹配。

    第362行,比较spi_device中modalias成员变量和device_driver中的name成员变量是否
    相等。

    STM32MP1 SPI主机驱动分析

    和I2C的适配器驱动一样,SPI主机驱动一般由SOC厂商编写好,打开stm32mp151.dtsi文件,找到如下内容:

    stm32mp151.dtsi文件中的spi1节点

    重点来看一下第4行的compatible属性值,compatible属性为“st,stm32h7-spi”,在Linux内核源码中搜素这两个属性值即可找到STM32MP1对应的SPI主机驱动。STM32MP1的SPI主机驱动文件为drivers/spi/spi-stm32.c,在此文件中找到如下内容:

    stm32_spi_driver结构体

    第1860行,“st,stm32h7-spi”匹配项,因此可知STM32MP1主机驱动就是spi-stm32.c这个文件。

    第2154-2164行,从这里可以知道,该主机驱动程序是基于platform总线框架编写,platform_driver结构体变量为stm32_spi_driver,当platform总线下设备和设备驱动匹配成功之后就会执行stm32_spi_probe函数,同样当驱动模块卸载的时候就会执行stm32_spi_remove函数。

    接下来重点来看下stm32_spi_probe函数做了些什么,函数如下所示:

    示例代码 45. 3.3 stm32_spi_probe 函数
    1866 static int stm32_spi_probe(struct platform_device *pdev)
    1867 {
    1868     struct spi_master *master;
    1869     struct stm32_spi *spi;
    1870     struct resource *res;
    1871     struct reset_control *rst;
    1872     int i, ret, num_cs, cs_gpio;
    1873
    1874     master = spi_alloc_master(&pdev->dev,
                        sizeof(struct stm32_spi));
    1875     if (!master) {
    1876         dev_err(&pdev->dev, "spi master allocation failed\n");
    1877         return -ENOMEM;
    1878     }
    ......
    1892     res p= latform_get_resource(&pdev, IORESOURCE_MEM, 0);
    1893     spi->base = devm_ioremap_resource(&pdev->dev, res);
    1894     if (IS_ERR(spi->base)) {
    1895         ret = PTR_ERR(spi->base);
    1896         goto err_master_put;
    1897     }
    1898
    1899     spi->phys_addr = (dma_addr_t)res->start;
    1900
    1901     spi->irq = platform_get_irq(pdev, 0);
    1902     if (spi->irq <= 0) {
    1903         ret = spi->irq;
    1904         if (ret != -EPROBE_DEFER)
    1905             dev_err(&pdev->dev, "failed to get irq: %d\n", ret);
    1906         goto err_master_put;
    1907     }
    1908     ret = devm_request_threaded_irq(&pdev->dev, spi->irq,
    1909                 spi->cfg->irq_handler_event,
    1910                 spi->cfg->irq_handler_thread,
    1911                 IRQF_ONESHOT, pdev->name, master);
    1963     master->dev.of_node = pdev->dev.of_node;
    1964     master->auto_runtime_pm = true;
    1965     master->bus_num = pdev->id;
    1966     master->mode_bits = SPI_CPHA | SPI_CPOL | SPI_CS_HIGH |
    1967                         SPI_LSB_FIRST | SPI_3WIRE;
    1968     master->bits_per_word_mask = spi->cfg->get_bpw_mask(spi);
    1969     master->max_speed_hz = spi clk_rate /
                                    spi->cfg->baud_rate_div_min;
    1970     master->min_speed_hz = spi->clk_rate /
                                    spi->cfg->baud_rate_div_max;
    1971     master->setup = stm32_spi_setup;
    1972     master->prepare_message = stm32_spi_prepare_msg;
    1973     master->transfer_one = stm32_spi_transfer_one;
    1974     master->unprepare_message = stm32_spi_unprepare_msg;
    ......
    2026     ret = spi_register_master(master);
    ......
    2050 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    第1874行,通过调用spi_alloc_master函数为master指针申请内存,也就是实例化master。

    第1901-1911行,获取中断号和注册中断函数。

    第1963-1974行,对master变量进行初始化和赋值,从结果可以看到,master结构体中并没有设置transfer和transfer_one_message这两个用于SPI数据传输的函数,而是使用了transfer_one作为SPI数据传输的函数,对应的函数为stm32_spi_transfer_one,也就是该函数是STM32MP1 SPI数据传输的函数。

    第2026行,调用spi_register_master函数向SPI子系统注册一个master。

    这里简单看看stm32_spi_transfer_one函数的内容,如下所示(有省略):

    stm32_spi_transfer_one函数

    第1709-1715行,如果启动了DMA那么就使用stm32_spi_transfer_one_dma来进行传输数据,没有用DMA的话就调用transfer_one_irq函数。这两个函数也就是控制寄存器来进行收发数据。

    SPI设备驱动编写流程

    SPI设备信息描述

    IO的pinctrl子节点创建与修改

    首先肯定是根据所使用的IO来创建或者修改pinctrl子节点。唯独要注意的是检查相应的IO有没有被其它的设备所使用,如果有多个pinctrl配置相同的IO是没有关系的,只要保证没有被设备调用就行

    SPI设备节点的创建与修改

    采用设备树方式的情况下,SPI从机设备信息描述就通过创建相应的设备子节点来完成,可以打开stm32mp157d-atk.dts这个设备树文件,然后在里边创建一个SPI从机设备节点,描述该设备的相关信息。

    SPI从机设备数据收发处理流程

    SPI设备驱动的核心是spi_driver。当向Linux内核注册成功spi_driver以后就可以使用SPI核心层提供的API函数来对设备进行读写操作了

    首先是spi_transfer结构体,此结构体用于描述SPI传输信息,结构体内容如下:

    spi_transfer结构体

    第817行,tx_buf保存着要发送的数据。

    第818行,rx_buf用于保存接收到的数据。

    第819行,len是要进行传输的数据长度,SPI是全双工通信,因此在一次通信中发送和接收的字节数都是一样的,所以spi_transfer中也就没有发送长度和接收长度之分。

    spi_transfer需要组织成spi_message,spi_message也是一个结构体,内容如下:

    spi_message结构体

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

    void spi_message_init(struct spi_message *m) 
    
    • 1

    函数参数和返回值含义如下:

    • m:要初始化的spi_message。
    • 返回值:无。

    spi_message初始化完成以后需要将spi_transfer添加到spi_message队列中,这里要用到spi_message_add_tail函数,此函数原型如下:

    void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m) 
    
    • 1

    函数参数和返回值含义如下:

    • t:要添加到队列中的spi_transfer。
    • m:spi_transfer要加入的spi_message。
    • 返回值:无。

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

    int spi_sync(struct spi_device *spi, struct spi_message *message)
    
    • 1

    函数参数和返回值含义如下:

    • spi:要进行数据传输的spi_device。
    • message:要传输的spi_message。
    • 返回值:无。

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

    int spi_async(struct spi_device *spi, struct spi_message *message)
    
    • 1

    函数参数和返回值含义如下:

    • spi:要进行数据传输的spi_device。
    • message:要传输的spi_message。
    • 返回值:无。

    在本章实验中,采用同步传输方式来完成SPI数据的传输工作,也就是spi_sync函数。

    综上所述,SPI数据传输步骤如下:

    1. 申请并初始化spi_transfer,设置spi_transfer的tx_buf成员变量,tx_buf为要发送的数据。然后设置rx_buf成员变量,rx_buf保存着接收到的数据。最后设置len成员变量,也就是要进行数据通信的长度。
    2. 使用spi_message_init函数初始化spi_message。
    3. 使用spi_message_add_tail函数将前面设置好的spi_transfer添加到spi_message队列中。
    4. 使用spi_sync函数完成SPI数据同步传输。

    通过SPI进行n个字节的数据发送和接收的示例代码如下所示:

    示例代码 45.4.2.3 SPI 数据读写操作
    /* SPI 多字节发送 */
    static int spi_send(struct spi_device *spi, u8 *buf, int len)
    {
    	int ret;
    	struct spi_message m;
    	struct spi_transfer t = {
    		.tx_buf = buf,
    		.len = len,
    	};
    			
    	spi_message_init(&m); /* 初始化 spi_message */
    	spi_message_add_tail(t, &m); /* 将 spi_transfer 添加到 spi_message 队列 */
    	ret = spi_sync(spi, &m); /* 同步传输 */
    	return ret;
    };
    
    /* SPI 多字节接收 */
    static int spi_receive(struct spi_device *spi, u8 *buf, int len)
    {
    	int ret;
    	struct spi_message m;
    	
    	struct spi_transfer t = {
    		.rx_buf = buf,
    		.len = len,
    	};
    	
    	spi_message_init(&m); /* 初始化 spi_message */
    	spi_message_add_tail(t, &m); /* 将 spi_transfer 添加到 spi_message 队列 */
    	ret = spi_sync(spi, &m); /* 同步传输 */
    	return ret;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    硬件原理图分析

    STM32MP1开发板上ICM20608原理如下图所示:

    ICM-20608原理图

    上图就是ICM-20608的硬件原理图。正点原子STM32MP1开发板的PZ0-3分别连接到ICM-20608的SCK、SDA、AD0和CS。其中6D_INT为ICM20608的中断引脚,连接到PA14引脚上,在本章实验上没有用到ICM20608的中断引脚。

    实验程序编写

    修改设备树

    添加/查找ICM20608使用的IO的pinmux配置

    首先在stm32mp15-pinctrl.dtsi文件中添加IO配置信息,ICM20608连接到了STM32MP157的SPI1接口,因此先在stm32mp15-pinctrl.dtsi里面搜索一下,看看有没有SPI1接口引脚配置(在正点原子的linux驱动开发的教程中是默认有的)。如果没有的话就自行添加,有的话就检查一下SPI1接口的引脚配置是否和自己所使用的硬件一致,不一致的话就要修改。修改后的引脚信息如下所示:

    示例代码 45. 6.1.1 spi1 的 pinmux 配置
    1  spi1_pins_a: spi1-0 { 
    2      pins1 { 
    3          pinmux = <STM32_PINMUX('Z', 0, AF5)>, /* SPI1_SCK */ 
    4                  <STM32_PINMUX('Z', 2, AF5)>; /* SPI1_MOSI */ 
    5          bias-disable; 
    6          drive-push-pull; 
    7          slew-rate = <1>; 
    8      }; 
    9 
    10     pins2 { 
    11         pinmux = <STM32_PINMUX('Z', 1, AF5)>; /* SPI1_MISO */ 
    12         bias-disable; 
    13     }; 
    14 
    15     pins3 { 
    16         pinmux = <STM32_PINMUX('Z', 3, GPIO)>; /* SPI1_NSS */ 
    17         drive-push-pull; 
    18         bias-pull-up; 
    19         output-high; 
    20         slew-rate = <0>; 
    21     }; 
    22 }; 
    23 
    24 spi1_sleep_pins_a: spi1-sleep-0 { 
    25     pins { 
    26         pinmux = <STM32_PINMUX('Z', 0, ANALOG)>, /* SPI1_SCK */ 
    27                 <STM32_PINMUX('Z', 1, ANALOG)>, /* SPI1_MISO */ 
    28                 <STM32_PINMUX('Z', 2, ANALOG)>, /* SPI1_MOSI */ 
    29                 <STM32_PINMUX('Z', 3, ANALOG)>; /* SPI1_NSS */ 
    30     }; 
    31 };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    示例代码45.6.3.1里15-21行是设置ICM20608的片选信号,直接复用为GPIO,也就是使用软件片选。

    在SPI1节点下添加pinmux并追加icm20608子节点

    在stm32mp157d-atk.dts文件,追加SPI1节点,追加如下所示内容:

    示例代码 45. 6.1.2 追加内容的 spi 1 节点
    1  spi1 {
    2      pinctrl-names = "default", "sleep";
    3      pinctrl-0 = <&spi1_pins_a>;
    4      pinctrl-1 = <&spi1_sleep_pins_a>;
    5      cs-gpios = <&gpioz 3 GPIO_ACTIVE_LOW>;
    6      status = "okay";
    7
    8      spidev: icm20608@0 {
    9          compatible = "alientek,icm20608";
    10         reg = <0>; /* CS #0 */
    11         spi-max-frequency = <8000000>;
    12     };
    13 };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    第2-4行,设置IO要使用的pinmux配置。

    第5行,“cs-gpios”属性是用来设置SPI的片选引脚。SPI主机驱动就会根据此属性去控制设备的片选引脚,本实验使用PZ3作为片选引脚。关于cs-gpios属性的详细描述可以参考
    绑定文档:Documentation/devicetree/bindings/spi/spi-controller.yaml。如果一个SPI接口下连接了多个SPI芯片,那么cs-gpios属性里面就要添加所有SPI芯片的片选信号,比如:

    cs-gpios = <&gpio1 0 0>, <&gpio1 1 0>, <&gpio1 2 0>, <&gpio1 3 0>;
    
    • 1

    上述描述说明此时SPI节点下有4个SPI芯片,第一个SPI芯片的片选引脚为gpio1_0,依次类推。

    第8-12行,icm20608设备子节点,从第5行的cs-gpios节点可以看出,此时SPI接口下只有一个ICM20608,而且ICM20608的片选索引为0,因此@后面为0。注意,@后面的数字就是对应SPI芯片片选信号在cs-gpios中的索引值。第9行设置节点属性兼容值为“alientek,icm20608”,第10行reg属性表示icm20608所使用的片选,和第8行@后面的数字含义相同,这里也设置为0,也就是cs-gpios属性中的第一个片选信号。第11行设置SPI最大时钟频率为8MHz,这是ICM20608的SPI接口所能支持的最大的时钟频率。

    编写ICM20608驱动

    工程创建好以后新建icm20608.c和icm20608reg.h这两个文件,icm20608.c为ICM20608的驱动代码,icm20608reg.h是ICM20608寄存器头文件。先在icm20608reg.h中定义好ICM20608的寄存器,输入如下内容(有省略):

    示例代码 45. 6.2.1 icm20608reg.h 文件内容
    1  #ifndef ICM20608_H 
    2  #define ICM20608_H 
    3  /*************************************************************** 
    4  Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 
    5  文件名 : icm20608reg.h 
    6  作者 : 左忠凯 
    7  版本 : V1.0
    8  描述 : ICM20608寄存器地址描述头文件 
    9  其他 : 无 
    10 论坛 : www.openedv.com 
    11 日志 : 初版V1.0 2019/9/2 左忠凯创建 
    12 ***************************************************************/ 
    13 #define ICM20608G_ID 0XAF /* ID值 */ 
    14 #define ICM20608D_ID 0XAE /* ID值 */ 
    15 
    16 /* ICM20608寄存器 
    17 *复位后所有寄存器地址都为0,除了 
    18 *Register 107(0X6B) Power Management 1 = 0x40 
    19 *Register 117(0X75) WHO_AM_I = 0xAF或0xAE 
    20 */ 
    21 /* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */ 
    22 #define ICM20_SELF_TEST_X_GYRO 0x00 
    23 #define ICM20_SELF_TEST_Y_GYRO 0x01 
    24 #define ICM20_SELF_TEST_Z_GYRO 0x02 
    25 #define ICM20_SELF_TEST_X_ACCEL 0x0D 
    26 #define ICM20_SELF_TEST_Y_ACCEL 0x0E 
    27 #define ICM20_SELF_TEST_Z_ACCEL 0x0F 
    ...... 
    88 #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    接下来编写icm20608.c文件。

    icm20608设备结构体创建

    这一部分跟之前的字符设备的设备结构体没有什么区别,主要需要加上struct spi_device结构体指针*spi,然后就是加上一系列的signed int类型的变量来存储从传感器中读取的数据。

    icm20608的spi_driver注册与注销

    对于SPI设备驱动,首先就是要初始化并向系统注册spi_driver。

    对于设备树而言,就是要添加一个of_device_id结构体类型的icm20608_of_match[]数组,里面设置.compatible用来匹配。

    然后需要写一个spi_driver结构体类型的icm20608_driver,作为SPI驱动结构体,里面要设置.probe,.remove,.driver里面设置.owner(这个一般就是THIS_MODULE),.name以及.of_match_table,就可以了。

    然后就是驱动入口和驱动出口函数,这里就是在icm20608_init里面return出来spi_register_driver;在icm20608_exit里面调用spi_unregister_driver函数。

    最后就是五个常规的操作就可以了。

    probe&remove函数

    probe函数,就在其中先通过devm_kzalloc分配内存空间,然后就是字符设备的老一套,alloc_chrdev_region创建设备号,cdev_init初始化cdev,然后cdev_add添加一个cdev,最后就是class_create以及device_create创建类和设备。

    之后就是SPI的内容,首先spi->mode来设置SPI的四种工作模式的一种,然后spi_setup;之后就是调用自己写的icm20608_reginit来初始化内部寄存器,之后spi_set_drvdata来保存设置好的设备结构体。

    remove函数跟之前的基本是一样的,除了要先通过spi_get_drvdata来获取一下SPI设备,然后就是4套操作来cdev_del,unregister_chrdev_region,device_destroy以及class_destroy。

    icm20608寄存器读写与初始化

    SPI驱动最终是通过读写icm20608的寄存器来实现的,因此需要编写相应的寄存器读写函数,并且使用这些读写函数来完成对icm20608的初始化。 具体的读写就是参考操作手册。

    这里的话我就直接把代码贴上来,比较长,需要注意的就是在读写多个寄存器的时候,因为SPI是全双工,没有发送和接收长度的分别,同时读写N个字节需要封装N+1个字节(第1个是区分读写的,后面N个才是真实的传输数据)。

    /*
     * @description	: 从icm20608读取多个寄存器数据
     * @param - dev:  icm20608设备
     * @param - reg:  要读取的寄存器首地址
     * @param - val:  读取到的数据
     * @param - len:  要读取的数据长度
     * @return 		: 操作结果
     */
    static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
    {
    
    	int ret = -1;
    	unsigned char txdata[1];
    	unsigned char * rxdata;
    	struct spi_message m;
    	struct spi_transfer *t;
    	struct spi_device *spi = (struct spi_device *)dev->spi;
        
    	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);	/* 申请内存 */
    	if(!t) {
    		return -ENOMEM;
    	}
    	rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL);	/* 申请内存 */
    	if(!rxdata) {
    		goto out1;
    	}
    	/* 一共发送len+1个字节的数据,第一个字节为
    	寄存器首地址,一共要读取len个字节长度的数据,*/
    	txdata[0] = reg | 0x80;		/* 写数据的时候首寄存器地址bit8要置1 */			
    	t->tx_buf = txdata;			/* 要发送的数据 */
        t->rx_buf = rxdata;			/* 要读取的数据 */
    	t->len = len+1;				/* t->len=发送的长度+读取的长度 */
    	spi_message_init(&m);		/* 初始化spi_message */
    	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
    	ret = spi_sync(spi, &m);	/* 同步发送 */
    	if(ret) {
    		goto out2;
    	}
    	
        memcpy(buf , rxdata+1, len);  /* 只需要读取的数据 */
    
    out2:
    	kfree(rxdata);					/* 释放内存 */
    out1:	
    	kfree(t);						/* 释放内存 */
    	
    	return ret;
    }
    
    /*
     * @description	: 向icm20608多个寄存器写入数据
     * @param - dev:  icm20608设备
     * @param - reg:  要写入的寄存器首地址
     * @param - val:  要写入的数据缓冲区
     * @param - len:  要写入的数据长度
     * @return 	  :   操作结果
     */
    static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len)
    {
    	int ret = -1;
    	unsigned char *txdata;
    	struct spi_message m;
    	struct spi_transfer *t;
    	struct spi_device *spi = (struct spi_device *)dev->spi;
    	
    	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);	/* 申请内存 */
    	if(!t) {
    		return -ENOMEM;
    	}
    	
    	txdata = kzalloc(sizeof(char)+len, GFP_KERNEL);
    	if(!txdata) {
    		goto out1;
    	}
    	
    	/* 一共发送len+1个字节的数据,第一个字节为
    	寄存器首地址,len为要写入的寄存器的集合,*/
    	*txdata = reg & ~0x80;	/* 写数据的时候首寄存器地址bit8要清零 */
        memcpy(txdata+1, buf, len);	/* 把len个寄存器拷贝到txdata里,等待发送 */
    	t->tx_buf = txdata;			/* 要发送的数据 */
    	t->len = len+1;				/* t->len=发送的长度+读取的长度 */
    	spi_message_init(&m);		/* 初始化spi_message */
    	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
    	ret = spi_sync(spi, &m);	/* 同步发送 */
        if(ret) {
            goto out2;
        }
    	
    out2:
    	kfree(txdata);				/* 释放内存 */
    out1:
    	kfree(t);					/* 释放内存 */
    	return ret;
    }
    
    /*
     * @description	: 读取icm20608指定寄存器值,读取一个寄存器
     * @param - dev:  icm20608设备
     * @param - reg:  要读取的寄存器
     * @return 	  :   读取到的寄存器值
     */
    static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
    {
    	u8 data = 0;
    	icm20608_read_regs(dev, reg, &data, 1);
    	return data;
    }
    
    /*
     * @description	: 向icm20608指定寄存器写入指定的值,写一个寄存器
     * @param - dev:  icm20608设备
     * @param - reg:  要写的寄存器
     * @param - data: 要写入的值
     * @return   :    无
     */	
    
    static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
    {
    	u8 buf = value;
    	icm20608_write_regs(dev, reg, &buf, 1);
    }
    
    /*
     * @description	: 读取ICM20608的数据,读取原始数据,包括三轴陀螺仪、
     * 				: 三轴加速度计和内部温度。
     * @param - dev	: ICM20608设备
     * @return 		: 无。
     */
    void icm20608_readdata(struct icm20608_dev *dev)
    {
    	unsigned char data[14];
    	icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);
    
    	dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]); 
    	dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]); 
    	dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]); 
    	dev->temp_adc    = (signed short)((data[6] << 8) | data[7]); 
    	dev->gyro_x_adc  = (signed short)((data[8] << 8) | data[9]); 
    	dev->gyro_y_adc  = (signed short)((data[10] << 8) | data[11]);
    	dev->gyro_z_adc  = (signed short)((data[12] << 8) | data[13]);
    }
    
    /*
     * ICM20608内部寄存器初始化函数 
     * @param - spi : 要操作的设备
     * @return 	: 无
     */
    void icm20608_reginit(struct icm20608_dev *dev)
    {
    	u8 value = 0;
    	
    	icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x80);
    	mdelay(50);
    	icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x01);
    	mdelay(50);
    
    	value = icm20608_read_onereg(dev, ICM20_WHO_AM_I);
    	printk("ICM20608 ID = %#X\r\n", value);	
    
    	icm20608_write_onereg(dev, ICM20_SMPLRT_DIV, 0x00); 	/* 输出速率是内部采样率					*/
    	icm20608_write_onereg(dev, ICM20_GYRO_CONFIG, 0x18); 	/* 陀螺仪±2000dps量程 				*/
    	icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG, 0x18); 	/* 加速度计±16G量程 					*/
    	icm20608_write_onereg(dev, ICM20_CONFIG, 0x04); 		/* 陀螺仪低通滤波BW=20Hz 				*/
    	icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG2, 0x04); /* 加速度计低通滤波BW=21.2Hz 			*/
    	icm20608_write_onereg(dev, ICM20_PWR_MGMT_2, 0x00); 	/* 打开加速度计和陀螺仪所有轴 				*/
    	icm20608_write_onereg(dev, ICM20_LP_MODE_CFG, 0x00); 	/* 关闭低功耗 						*/
    	icm20608_write_onereg(dev, ICM20_FIFO_EN, 0x00);		/* 关闭FIFO						*/
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168

    字符设备驱动框架

    这里的驱动框架就比较常规。

    在icm20608_read里面,与IIC一样,需要先读取filp的cdev首地址,再通过container_of来获取设备的首地址;然后调用已经写好的icm20608_readdata读取寄存器的值,然后把数据对应存入到自定义的data[7]数组之中,最后copy_to_user来传输。

    open和release函数就是直接return 0就可以了。

    file_operations这个操作函数集就是.open,.read以及.release。

    编写测试APP

    argc是传入了2个参数。

    这里主要就是获取字符设备之后,filename=argv[1],然后open打开,在while死循环里面,通过read把数据读入databuf[7]数组里面,然后把数组的对应位置的值传入自己定义的变量里面;再根据寄存器设置的量程,把这些直接读取的数据转换成对应的真实值然后打印出来,最后usleep(100000)间隔100ms重复读取;跳出死循环外面close设备。

    运行测试

    内核使能SPI控制器

    ST官方系统把SPI控制器的驱动编译成模块,需要把SPI控制器驱动编译进内核,这
    样就可以在启动Linux内核的时候自动加载SPI控制器驱动,无需手动加载,方便使用。打开Linux内核图形化配置界面,按下路径找到对应的配置项:

    -> Device Drivers
    -> SPI support (SPI [=y])
    -> <*> STMicroelectronics STM32 SPI controller //编译进内核

    如下图所示:

    SPI控制器配置项

    上图本来是选择为“M””,要改为“*”,也就是编译进内核。接着重新编译设备树和内核,运行以下命令进行编译:

    make dtbs uImage LOADADDR=0XC2000040 -j32

    使用新编译好的stm32mp157d-atk.dtb和uImage镜像启动系统,如果SPI控制器驱动工作正常就会有如下图所示提示信息:

    SPI控制器初始化

    如果没有输出上图中“spi_stm32 44004000.spi: driver initialized”这句话,那就要检查一下设备树和内配配置是否有问题,通过查看/sys/bus/spi/devices/下有没有spi相关的设备,就能够知道设备树配置是否正确,比如本例程如下图所示:

    SPI设备

    编译驱动程序

    老样子,Makefile里面的obj-m改成icm20608.o,然后“make”就可以了。

    编译测试APP

    在icm20608App.c这个测试APP中用到了浮点计算,而STM32MP1是支持硬件浮点的,因此在编译icm20608App.c的时候就可以使能硬件浮点,这样可以加速浮点计算。使
    能硬件浮点很简单,在编译的时候加入如下参数即可:

    -march-armv7-a -mfpu-neon -mfloat=hard

    输入如下命令使能硬件浮点编译icm20608App.c这个测试程序:

    arm-none-linux-gnueabihf-gcc -march=armv7-a -mfpu=neon -mfloat-abi=hard icm20608App.c -o icm20608App

    编译成功以后就会生成icm20608App这个应用程序,使用arm-linux-gnueabihf-readelf查看一下编译出来的icm20608App就知道了,输入如下命令:

    arm-none-linux-gnueabihf-readelf -A icm20608App

    结果如下图所示:

    icm20608App文件信息

    从上图可以看出FPU架构为VFPv3,SIMD使用了NEON,说明icm20608App这个应用程序使用了硬件浮点。

    运行测试

    将上一小节编译出来icm20608.ko和icm20608App这两个文件拷贝到rootfs/lib/modules/5.3.41目录中,重启开发板,进入到目录lib/modules/5.3.41中。输入如下命令加载icm20608.ko这个驱动模块:

    depmod //第一次加载驱动的时候需要运行此命令
    modprobe icm20608.ko //加载驱动模块

    当驱动模块加载成功以后使用icm20608App来测试,输入如下命令:

    ./icm20608App /dev/icm20608

    测试APP会不断的从ICM20608中读取数据,然后输出到终端上,如下图所示:

    获取到的ICM20608数据

    可以看出,开发板静止状态下,Z轴方向的加速度为0.97g,这个就是重力加速度。对于陀螺仪来讲,静止状态下三轴的角速度应该在0°/S左右。ICM20608内温度传感器采集到的温度在39.51度,可以晃动一下开发板,这个时候陀螺仪和加速度计的值就会有变化。

    总结

    跟之前的I2C驱动是很类似的,具体SPI主机驱动是不需要我们自己写的,我们只需要写从机驱动,也就是对应的传感器芯片的驱动就可以了。

    首先需要修改设备数,根据SPI的接口,在stm32mp15-pinctrl.dtsi中,修改电气属性,添加复用功能;然后在stm32mp157d-atk.dts设备树文件中,添加对应的spi节点,并与pinctrl关联。

    然后要编写SPI驱动传感器或芯片的头文件,按照数据手册定义好寄存器的地址。

    对于驱动程序而言,首先是设备结构体,这里就是要添加spi_device结构体的*spi,来完成对spi的设置。然后就是通过设备树,写一个SPI的驱动结构体spi_driver,定义probe和remove函数,然后在.driver里面写好.owner,.name和.of_match_table。of_match_table就是of_device_id结构体的数组,设置好.compatible就可以了。驱动的入口和出口就是调用spi_register_driver和spi_unregister_driver。

    probe函数,首先来通过devm_kzalloc分配内存之后,走字符设备的流程,然后是初始化spi_device,通过spi->mode选定SPI工作模式,然后spi_setup初始化,之后调用自行编写的初始化寄存器的函数,并通过spi_set_drvdata保存spi结构体。

    remove函数,就是通过spi_get_drvdata获取到spi结构体,然后注销字符设备驱动的四件套就可以了。

    这里SPI实验是使用ICM20608,所以根据他的手册和寄存器,来编写对寄存器的读写函数,这里因为全双工就不区别收发的长度,只要注意长度是len+1就可以了。

    字符设备的驱动框架就是老样子,这里注意在read里面,要跟I2C一样,先读cdev首地址,再container_of获取设备首地址。

  • 相关阅读:
    java农家乐旅游管理系统springboot+vue
    MSP430F5529时钟系统配置
    十、ThreadLocal
    【计算机视觉 | 目标检测】目标检测常用数据集及其介绍(八)
    代码规范问题,“static“ base class members should not be accessed via derived types
    OSIRISV4.1使用教程(最新可用版)
    python代码服务汇总
    QQ2 微信红包
    SpringMVC基础源码分析(一)
    一、MySQL.pratice.search
  • 原文地址:https://blog.csdn.net/xhj12138/article/details/134053521