OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 4957|回复: 0

《STM32MP157嵌入式Linux驱动开发指南》第四十四章Linux SPI总线框架

[复制链接]

1061

主题

1072

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
4407
金钱
4407
注册时间
2019-5-8
在线时间
1196 小时
发表于 2021-7-2 11:51:53 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2021-7-2 11:51 编辑

1)实验平台:正点原子STM32MP157开发板
2)  章节摘自【正点原子】《STM32MP157嵌入式Linux驱动开发指南》
3)购买链接:https://item.taobao.com/item.htm?&id=629270721801
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/arm-linux/zdyzmp157.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子STM32MP157技术交流群:691905614   QQ群.png


原子哥.jpg

微信公众号.png


第四十四章Linux SPI总线框架



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


44.1 SPI & ICM-20608简介
44.1.1 SPI简介
        上一章我们讲解了I2C,I2C是串行通信的一种,只需要两根线就可以完成主机和从机之间的通信,但是I2C的速度最高只能到400KHz,如果对于访问速度要求比价高的话I2C就不适合了。本章我们就来学习一下另外一个和I2C一样广泛使用的串行通信:SPI,SPI全称是Serial Perripheral Interface,也就是串行外围设备接口。SPI是Motorola公司推出的一种同步串行接口技术,是一种高速、全双工的同步通信总线,SPI时钟频率相比I2C要高很多,最高可以工作在上百MHz。SPI以主从方式工作,通常是有一个主设备和一个或多个从设备,一般SPI需要4根线,但是也可以使用三根线(单向传输),本章我们讲解标准的4线SPI,这四根线如下:
        ①、CS/SS,Slave Select/Chip Select,这个是片选信号线,用于选择需要进行通信的从设备。I2C主机是通过发送从机设备地址来选择需要进行通信的从机设备的,SPI主机不需要发送从机设备,直接将相应的从机设备片选信号拉低即可。
        ②、SCK,Serial Clock,串行时钟,和I2C的SCL一样,为SPI通信提供时钟。
        ③、MOSI/SDO,Master Out Slave In/Serial Data Output,简称主出从入信号线,这根数据线只能用于主机向从机发送数据,也就是主机输出,从机输入。
        ④、MISO/SDI,Master In Slave Out/Serial Data Input,简称主入从出信号线,这根数据线只能用户从机向主机发送数据,也就是主机输入,从机输出。
        SPI通信都是由主机发起的,主机需要提供通信的时钟信号。主机通过SPI线连接多个从设备的结构如图44.1.1.1所示:
图片1.jpg
图44.1.1.1 SPI设备连接图
SPI有四种工作模式,通过串行时钟极性(CPOL)和相位(CPHA)的搭配来得到四种工作模式:
①、CPOL=0,串行时钟空闲状态为低电平。
②、CPOL=1,串行时钟空闲状态为高电平,此时可以通过配置时钟相位(CPHA)来选择具体的传输协议。
③、CPHA=0,串行时钟的第一个跳变沿(上升沿或下降沿)采集数据。
④、CPHA=1,串行时钟的第二个跳变沿(上升沿或下降沿)采集数据。
这四种工作模式如图44.1.1.2所示:
图片2.jpg
图44.1.1.2 SPI四种工作模式
        和I2C一样,SPI也是有时序图的,以CPOL=0,CPHA=0这个工作模式为例,SPI进行全双工通信的时序如图44.1.1.3所示:
图片3.jpg
图44.1.1.3 SPI时序图
        从图44.1.1.3可以看出,SPI的时序图很简单,不像I2C那样还要分为读时序和写时序,因为SPI是全双工的,所以读写时序可以一起完成。图44.1.1.3中,CS片选信号先拉低,选中要通信的从设备,然后通过MOSI和MISO这两根数据线进行收发数据,MOSI数据线发出了0XD2这个数据给从设备,同时从设备也通过MISO线给主设备返回了0X66这个数据。这个就是SPI时序图。
        关于SPI就讲解到这里,接下来我们看一下STM32MP1自带的SPI外设。
44.1.2 STM32MP1 SPI简介
        STM32MP1自带的SPI全称为:Serial peripheral interface。SPI特性如下:
①、全双工同步串口接口。
②、半双工模式。
        ③、可配置的主/从模式。
④、支持I2S协议。
        ⑤、在达到 FIFO 阈值、超时、操作完成以及发生访问错误时产生中断。
        ⑥、允许16位,24位或者32位数据长度。
        ⑦、支持软件片选和硬件片选。
        STM32MP1的SPI可以工作在主模或从模式,本章我们使用主模式,此芯片有6个SPI,其中SPI1~3是支持I2S协议。在主模式下,可以选择硬件片选和软件片选,如果使用了硬件片选,那么每一个SPI只支持一个外设,软件片选就可以支持无数个外设,本章实验我们不使用硬件片选信号,因为硬件片选信号只能使用指定的片选IO,软件片选的话可以使用任意的IO。
44.1.3 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,±4g,±8g和±16g。陀螺仪和加速度计都是16位的ADC,并且支持I2C和SPI两种协议,使用I2C接口的话通信速度最高可以达到400KHz,使用SPI接口的话通信速度最高可达到8MHz。开发板上的ICM-20608通过SPI接口和STM32MP157连接在一起。ICM-20608特性如下:
        ①、陀螺仪支持X,Y和Z三轴输出,内部集成16位ADC,测量范围可设置:±250,±500,±1000和±2000°/s。
        ②、加速度计支持X,Y和Z轴输出,内部集成16位ADC,测量范围可设置:±2g,±4g,±4g,±8g和±16g。
        ③、用户可编程中断。
        ④、内部包含512字节的FIFO。
        ⑤、内部包含一个数字温度传感器。
        ⑥、耐10000g的冲击。
        ⑦、支持快速I2C,速度可达400KHz。
        ⑧、支持SPI,速度可达8MHz。
        ICM-20608的3轴方向如图44.1.3.1所示:
第四十四章Linux SPI总线框架2581.png
图44.1.3.1 ICM-20608检测轴方向和极性
        ICM-20608的结构框图如图44.1.3.2所示:
第四十四章Linux SPI总线框架2640.png
图44.1.3.2 ICM-20608框图
        如果使用IIC接口的话,ICM-20608的AD0引脚决定I2C设备从地址的最后一位,如果AD0为0的话ICM-20608从设备地址是0X68,如果AD0为1的话ICM-20608从设备地址为0X69。本章我们使用SPI接口,跟上一章使用AP3216C一样,ICM-20608也是通过读写寄存器来配置和读取传感器数据,使用SPI接口读写寄存器需要16个时钟或者更多(如果读写操作包括多个字节的话),第一个字节包含要读写的寄存器地址,寄存器地址最高位是读写标志位,如果是读的话寄存器地址最高位要为1,如果是写的话寄存器地址最高位要为0,剩下的7位才是实际的寄存器地址,寄存器地址后面跟着的就是读写的数据。表44.1.3.1列出了本章实验用到的一些寄存器和位,关于ICM-20608的详细寄存器和位的介绍请参考ICM-20608的寄存器手册:
表1-1.png
表1-2.png
表44.1.3.1 ICM-20608寄存器表
ICM-20608的介绍就到这里,关于ICM-20608的详细介绍请参考ICM-20608的数据手册和寄存器手册。
44.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就有六个SPI,那么就有六个SPI控制器,每个控制器都有一条特定的SPI总线的读写。SPI子系统使用struct spi_master数据结构体来描述SPI控制器。在内核源码drivers/spi目录下有很多以spi-xxxx.c命名的源文件,如图44.2.1:
第四十四章Linux SPI总线框架5383.png
图44.2.1 SPI控制器驱动源码
这些文件就是具体平台对应的SPI控制器驱动程序,使用SPI核心层提供的接口向SPI子系统注册SPI控制器。
SPI设备驱动层:SPI从设备对应的驱动程序,比如一些SPI接口的芯片器件对应的驱动程序。接下来我们详细的聊聊SPI子系统。
44.2.1 SPI主机驱动
SPI主机驱动就是SoC的SPI控制器驱动,类似I2C总线的适配器驱动。SPI子系统使用spi_master结构体来描述SPI控制器,其实spi_master是一个宏,这个宏定义在include/linux/spi/spi.h文件中,如下所示:
  1. #define spi_master                        spi_controller
复制代码


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


        第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 的使用者,这一部分的驱动就不用操心了,除非你是在 SOC 原厂工作,内容就是写 SPI 主机驱动。
SPI 主机驱动的核心就是申请 spi_master,然后初始化 spi_master,最后向 Linux 内核注册spi_master。
1、spi_master申请与释放
        spi_alloc_master函数用于申请spi_master,函数原型如下:
  1. struct spi_controller *spi_alloc_master(struct device         *host,
  2.                                                                                unsigned int                 size)
复制代码


函数参数和返回值含义如下:
        host:设备,一般是platform_device中的dev成员变量。
        size:私有数据大小,可以通过spi_master_get_devdata函数获取到这些私有数据。
        返回值:申请到的spi_controller,也就是spi_master。
        spi_master的释放通过spi_master_put函数来完成,当我们删除一个SPI主机驱动的时候就需要释放掉前面申请的spi_master,spi_master_put本质上是个宏:
  1. #define spi_master_put(_ctlr)                spi_controller_put(_ctlr)
复制代码


spi_master_put函数最终通过调用spi_controller_put函数来完成spi_master释放,原型如下:
  1. void spi_master_put(struct spi_controller *ctlr)
复制代码


函数参数和返回值含义如下:
        ctlr:要释放的spi_master。
        返回值:无。
        2、spi_master的注册与注销
        当spi_master初始化完成以后就需要将其注册到Linux内核,spi_master注册函数为spi_register_master,函数原型如下:
  1. int spi_register_master(struct spi_controller *ctlr)
复制代码


函数参数和返回值含义如下:
        ctlr:要注册的spi_master。
        返回值:0,成功;负值,失败。
        如果要注销spi_master的话可以使用spi_unregister_master函数,此函数原型为:
  1. void spi_unregister_master(struct spi_controller *ctlr)
复制代码


函数参数和返回值含义如下:
ctlr:要注销的spi_master。
        返回值:无。
44.2.2 SPI设备驱动
        spi设备驱动和i2c设备驱动也很类似,Linux内核使用spi_driver结构体来表示spi设备驱动,我们在编写SPI设备驱动的时候需要实现spi_driver。spi_driver结构体定义在include/linux/spi/spi.h文件中,结构体内容如下:
示例代码44.2.2.1 spi_driver结构体
  1. 259 struct spi_driver {
  2. 260     const struct spi_device_id *id_table;
  3. 261     int         (*probe)(struct spi_device *spi);
  4. 262     int         (*remove)(struct spi_device *spi);
  5. 263     void              (*shutdown)(struct spi_device *spi);
  6. 264     struct device_driver    driver;
  7. 265 };
  8. 可以看出,spi_driver和i2c_
复制代码

driver、platform_driver基本一样,当SPI设备和驱动匹配成功以后probe函数就会执行。
        同样的,spi_driver初始化完成以后需要向Linux内核注册,spi_driver注册函数为spi_register_driver,函数原型如下:
  1. int spi_register_driver(struct spi_driver *sdrv)
复制代码


函数参数和返回值含义如下:
sdrv:要注册的spi_driver。
        返回值:0,注册成功;赋值,注册失败。
        注销SPI设备驱动以后也需要注销掉前面注册的spi_driver,使用spi_unregister_driver函数完成spi_driver的注销,函数原型如下:
  1. void spi_unregister_driver(struct spi_driver *sdrv)
复制代码


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


        第1~36行,spi_driver结构体,需要SPI设备驱动人员编写,包括匹配表、probe函数等。和i2c_driver、platform_driver一样,就不详细讲解了。
        第39~42行,在驱动入口函数中调用spi_register_driver来注册spi_driver。
        第45~48行,在驱动出口函数中调用spi_unregister_driver来注销spi_driver。
44.2.3 SPI设备和驱动匹配过程
        SPI设备和驱动的匹配过程是由SPI总线来完成的,这点和platform、I2C等驱动一样,SPI总线为spi_bus_type,定义在drivers/spi/spi.c文件中,内容如下:
示例代码44.2.3.1 spi_bus_type结构体
  1. 377 struct bus_type spi_bus_type = {
  2. 378     .name               = "spi",
  3. 379     .dev_groups         = spi_dev_groups,
  4. 380     .match              = spi_match_device,
  5. 381     .uevent             = spi_uevent,
  6. 382 };
复制代码


        可以看出,SPI设备和驱动的匹配函数为spi_match_device,函数内容如下:
示例代码44.2.3.2 spi_match_device函数
  1. 342 static int spi_match_device(struct device *dev,
  2. struct device_driver *drv)
  3. 343 {
  4. 344     const struct spi_device *spi = to_spi_device(dev);
  5. 345     const struct spi_driver *sdrv = to_spi_driver(drv);
  6. 346
  7. 347     /* Check override first, and if set, only use the named driver */
  8. 348     if (spi->driver_override)
  9. 349         return strcmp(spi->driver_override, drv->name) == 0;
  10. 350
  11. 351     /* Attempt an OF style match */
  12. 352     if (of_driver_match_device(dev, drv))
  13. 353         return 1;
  14. 354
  15. 355     /* Then try ACPI */
  16. 356     if (acpi_driver_match_device(dev, drv))
  17. 357         return 1;
  18. 358
  19. 359     if (sdrv->id_table)
  20. 360         return !!spi_match_id(sdrv->id_table, spi);
  21. 361
  22. 362     return strcmp(spi->modalias, drv->name) == 0;
  23. 363 }
复制代码


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成员变量是否相等。
44.3 STM32MP1 SPI主机驱动分析
        和I2C的适配器驱动一样,SPI主机驱动一般都由SOC厂商编写好了,打开stm32mp151.dtsi文件,找到如下所示内容:
示例代码44.3.1 stm32mp151.dtsi文件中的spi1节点内容
  1. 1   spi1: spi@44004000 {
  2. 2       #address-cells = <1>;
  3. 3       #size-cells = <0>;
  4. 4       compatible = "st,stm32h7-spi";
  5. 5       reg = <0x44004000 0x400>;
  6. 6       interrupts = <GIC_SPI 35 IRQ_TYPE_LEVEL_HIGH>;
  7. 7       clocks = <&rcc SPI1_K>;
  8. 8       resets = <&rcc SPI1_R>;
  9. 9       dmas = <&dmamux1 37 0x400 0x01>,
  10. 10            <&dmamux1 38 0x400 0x01>;
  11. 11      dma-names = "rx", "tx";
  12. 12      power-domains = <&pd_core>;
  13. 13      status = "disabled";
  14. 14  };
复制代码


        重点来看一下第4行的compatible属性值,compatible属性为“st,stm32h7-spi”,在Linux内核源码中搜素这两个属性值即可找到STM32MP1对应的SPI主机驱动。STM32MP1的SPI主机驱动文件为drivers/spi/spi-stm32.c,在此文件中找到如下内容:
示例代码44.3.2 stm32_spi_driver结构体
  1. 1859    static const struct of_device_id stm32_spi_of_match[] = {
  2. 1860                 {         .compatible = "st,stm32h7-spi",
  3.                                 .data = (void *)&stm32h7_spi_cfg },
  4. 1861                {        .compatible = "st,stm32f4-spi",
  5.                                 .data = (void *)&stm32f4_spi_cfg },
  6. 1862        {},
  7. 1863    };
  8. ......
  9. 2154    static struct platform_driver stm32_spi_driver = {
  10. 2155        .probe = stm32_spi_probe,
  11. 2156        .remove = stm32_spi_remove,
  12. 2157        .driver = {
  13. 2158            .name = DRIVER_NAME,
  14. 2159            .pm = &stm32_spi_pm_ops,
  15. 2160            .of_match_table = stm32_spi_of_match,
  16. 2161        },
  17. 2162    };
  18. 2163
  19. 2164    module_platform_driver(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函数做了些什么,函数如下所示:
示例代码44.3.3 stm32_spi_probe函数
  1. 1866    static int stm32_spi_probe(struct platform_device *pdev)
  2. 1867    {
  3. 1868        struct spi_master *master;
  4. 1869        struct stm32_spi *spi;
  5. 1870        struct resource *res;
  6. 1871        struct reset_control *rst;
  7. 1872        int i, ret, num_cs, cs_gpio;
  8. 1873
  9. 1874        master = spi_alloc_master(&pdev->dev,
  10. sizeof(struct stm32_spi));
  11. 1875        if (!master) {
  12. 1876            dev_err(&pdev->dev, "spi master allocation failed\n");
  13. 1877            return -ENOMEM;
  14. 1878        }
  15. .....
  16. 1892        res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
  17. 1893        spi->base = devm_ioremap_resource(&pdev->dev, res);
  18. 1894        if (IS_ERR(spi->base)) {
  19. 1895            ret = PTR_ERR(spi->base);
  20. 1896            goto err_master_put;
  21. 1897        }
  22. 1898
  23. 1899        spi->phys_addr = (dma_addr_t)res->start;
  24. 1900
  25. 1901        spi->irq = platform_get_irq(pdev, 0);
  26. 1902        if (spi->irq <= 0) {
  27. 1903            ret = spi->irq;
  28. 1904            if (ret != -EPROBE_DEFER)
  29. 1905                dev_err(&pdev->dev, "failed to get irq: %d\n", ret);
  30. 1906            goto err_master_put;
  31. 1907        }
  32. 1908        ret = devm_request_threaded_irq(&pdev->dev, spi->irq,
  33. 1909                        spi->cfg->irq_handler_event,
  34. 1910                        spi->cfg->irq_handler_thread,
  35. 1911                        IRQF_ONESHOT, pdev->name, master);
  36. ......
  37. 1963        master->dev.of_node = pdev->dev.of_node;
  38. 1964        master->auto_runtime_pm = true;
  39. 1965        master->bus_num = pdev->id;
  40. 1966        master->mode_bits = SPI_CPHA | SPI_CPOL | SPI_CS_HIGH |
  41. 1967                                                    SPI_LSB_FIRST | SPI_3WIRE;
  42. 1968        master->bits_per_word_mask = spi->cfg->get_bpw_mask(spi);
  43. 1969                master->max_speed_hz =         spi->clk_rate /
  44.                                        spi->cfg->baud_rate_div_min;
  45. 1970                master->min_speed_hz = spi->clk_rate /
  46.                                                                   spi->cfg->baud_rate_div_max;
  47. 1971        master->setup = stm32_spi_setup;
  48. 1972        master->prepare_message = stm32_spi_prepare_msg;
  49. 1973        master->transfer_one = stm32_spi_transfer_one;
  50. 1974        master->unprepare_message = stm32_spi_unprepare_msg;
  51. ......
  52. 2026        ret = spi_register_master(master);
  53. ......

  54. 2050    }
复制代码


        第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函数的内容,如下所示(有省略):
示例代码44.3.4 stm32_spi_transferZ_one函数
  1. 1683    static int stm32_spi_transfer_one(struct spi_master *master,
  2. 1684                      struct spi_device *spi_dev,
  3. 1685                      struct spi_transfer *transfer)
  4. 1686    {
  5. 1687        struct stm32_spi *spi = spi_master_get_devdata(master);
  6. 1688        u32 xfer_time, midi_delay_ns;
  7. 1689        unsigned long timeout;
  8. 1690        int ret;
  9. 1691
  10. 1692        spi->tx_buf = transfer->tx_buf;
  11. 1693        spi->rx_buf = transfer->rx_buf;
  12. 1694        spi->tx_len = spi->tx_buf ? transfer->len : 0;
  13. 1695        spi->rx_len = spi->rx_buf ? transfer->len : 0;
  14. 1696
  15. 1697        spi->cur_usedma = (master->can_dma &&
  16. 1698                   master->can_dma(master, spi_dev, transfer));
  17. 1699
  18. 1700        ret = stm32_spi_transfer_one_setup(spi, spi_dev, transfer);
  19. 1701        if (ret) {
  20. 1702            dev_err(spi->dev, "SPI transfer setup failed\n");
  21. 1703            return ret;
  22. 1704        }
  23. 1705
  24. 1706        reinit_completion(&spi->xfer_completion);
  25. 1707        spi->xfer_status = 0;
  26. 1708
  27. 1709        if (spi->cur_usedma)
  28. 1710            ret = stm32_spi_transfer_one_dma(spi, transfer);
  29. 1711        else
  30. 1712            ret = spi->cfg->transfer_one_irq(spi);
  31. 1713
  32. 1714        if (ret)
  33. 1715            return ret;
  34. ......
  35. 1739    }
复制代码


        第1709~1715行,如果启动了DMA那么就使用stm32_spi_transfer_one_dma来进行传输数据,没有用DMA的话就调用transfer_one_irq函数。这两个函数也就是控制寄存器来进行收发数据。
        关于STM32MP1 SPI主机程序的分析就到这里了。
44.4 SPI设备驱动编写流程
44.4.1 SPI设备信息描述
        1、IO的pinctrl子节点创建与修改
        首先肯定是根据所使用的IO来创建或者修改pinctrl子节点,这个没有什么好说的。唯独要注意的是检查相应的IO有没有被其它的设备所使用,如果有多个pinctrl配置相同的IO是没有关系的,只要保证没有被设备调用就行。
        2、SPI设备节点的创建与修改        
采用设备树方式的情况下,SPI从机设备信息描述就通过创建相应的设备子节点来完成,我们可以打开stm32mp157d-atk.dts这个设备树文件,然后在里边创建一个SPI从机设备节点,描述该设备的相关信息,我们后面再创建。
44.4.2 SPI从机设备数据收发处理流程
        SPI设备驱动的核心是spi_driver,这个我们已经在44.2.2小节讲过了。当我们向Linux内核注册成功spi_driver以后就可以使用SPI核心层提供的API函数来对设备进行读写操作了。首先是spi_transfer结构体,此结构体用于描述SPI传输信息,结构体内容如下:
示例代码44.4.2.1 spi_transfer结构体
  1. 811 struct spi_transfer {
  2. ......
  3. 817     const void  *tx_buf;
  4. 818     void        *rx_buf;
  5. 819     unsigned    len;
  6. 820
  7. 821     dma_addr_t  tx_dma;
  8. 822     dma_addr_t  rx_dma;
  9. 823     struct sg_table tx_sg;
  10. 824     struct sg_table rx_sg;
  11. 825
  12. 826     unsigned    cs_change:1;
  13. 827     unsigned    tx_nbits:3;
  14. 828     unsigned    rx_nbits:3;
  15. 829 #define SPI_NBITS_SINGLE    0x01 /* 1bit transfer */
  16. 830 #define SPI_NBITS_DUAL      0x02 /* 2bits transfer */
  17. 831 #define SPI_NBITS_QUAD      0x04 /* 4bits transfer */
  18. 832     u8      bits_per_word;
  19. 833     u8      word_delay_usecs;
  20. 834     u16     delay_usecs;
  21. 835     u16     cs_change_delay;
  22. 836     u8      cs_change_delay_unit;
  23. 837 #define SPI_DELAY_UNIT_USECS    0
  24. 838 #define SPI_DELAY_UNIT_NSECS    1
  25. 839 #define SPI_DELAY_UNIT_SCK  2
  26. 840     u32     speed_hz;
  27. 841     u16     word_delay;
  28. 842
  29. 843     u32     effective_speed_hz;
  30. 844
  31. 845     struct list_head transfer_list;
  32. 846 };
复制代码


第817行,tx_buf保存着要发送的数据。
        第818行,rx_buf用于保存接收到的数据。
        第819行,len是要进行传输的数据长度,SPI是全双工通信,因此在一次通信中发送和接收的字节数都是一样的,所以spi_transfer中也就没有发送长度和接收长度之分。
        spi_transfer需要组织成spi_message,spi_message也是一个结构体,内容如下:
示例代码44.4.2.2 spi_message结构体
  1. 878 struct spi_message {
  2. 879     struct list_head    transfers;
  3. 880
  4. 881     struct spi_device   *spi;
  5. 882
  6. 883     unsigned        is_dma_mapped:1;
  7. ......
  8. 897     void            (*complete)(void *context);
  9. 898     void            *context;
  10. 899     unsigned        frame_length;
  11. 900     unsigned        actual_length;
  12. 901     int         status;
  13. ......
  14. 907     struct list_head    queue;
  15. 908     void            *state;
  16. 909
  17. 910     /* list of spi_res reources when the spi message is processed */
  18. 911     struct list_head        resources;
  19. 912 };
复制代码


在使用spi_message之前需要对其进行初始化,spi_message初始化函数为spi_message_init,函数原型如下:
  1. void spi_message_init(struct spi_message *m)
复制代码


函数参数和返回值含义如下:
m:要初始化的spi_message。
        返回值:无。
        spi_message初始化完成以后需要将spi_transfer添加到spi_message队列中,这里我们要用到spi_message_add_tail函数,此函数原型如下:
  1. void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
复制代码


函数参数和返回值含义如下:
t:要添加到队列中的spi_transfer。
m:spi_transfer要加入的spi_message。
        返回值:无。
        spi_message准备好以后就可以进行数据传输了,数据传输分为同步传输和异步传输,同步传输会阻塞的等待SPI数据传输完成,同步传输函数为spi_sync,函数原型如下:
  1. int spi_sync(struct spi_device *spi, struct spi_message *message)
复制代码


函数参数和返回值含义如下:
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)
函数参数和返回值含义如下:
spi:要进行数据传输的spi_device。
message:要传输的spi_message。
        返回值:无。
        在本章实验中,我们采用同步传输方式来完成SPI数据的传输工作,也就是spi_sync函数。
        综上所述,SPI数据传输步骤如下:
        ①、申请并初始化spi_transfer,设置spi_transfer的tx_buf成员变量,tx_buf为要发送的数据。然后设置rx_buf成员变量,rx_buf保存着接收到的数据。最后设置len成员变量,也就是要进行数据通信的长度。
        ②、使用spi_message_init函数初始化spi_message。
        ③、使用spi_message_add_tail函数将前面设置好的spi_transfer添加到spi_message队列中。
        ④、使用spi_sync函数完成SPI数据同步传输。
        通过SPI进行n个字节的数据发送和接收的示例代码如下所示:
示例代码44.4.2.3 SPI数据读写操作
  1. /* SPI多字节发送 */
  2. static int spi_send(struct spi_device *spi, u8 *buf, int len)
  3. {
  4.     int ret;
  5.     struct spi_message m;
  6.    
  7.     struct spi_transfer t = {
  8.         .tx_buf = buf,
  9.         .len = len,
  10.     };

  11.     spi_message_init(&m);               /* 初始化spi_message */
  12.     spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
  13.     ret = spi_sync(spi, &m);            /* 同步传输 */
  14.     return ret;
  15. }

  16. /* SPI多字节接收 */
  17. static int spi_receive(struct spi_device *spi, u8 *buf, int len)
  18. {
  19.     int ret;
  20.     struct spi_message m;
  21.    
  22.     struct spi_transfer t = {
  23.         .rx_buf = buf,
  24.         .len = len,
  25.     };

  26.     spi_message_init(&m);               /* 初始化spi_message */
  27.     spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
  28.     ret = spi_sync(spi, &m);            /* 同步传输 */
  29.     return ret;
  30. };
复制代码


44.5 硬件原理图分析
        STM32MP1开发板上ICM20608原理如图44.5.1所示:
第四十四章Linux SPI总线框架22835.png
图44.5.1 ICM-20608 原理图
        图44.5.1这是ICM-20608的硬件原理图。正点原子STM32MP1开发板的PZ0~3分别连接到ICM-20608的SCK、SDA、AD0和CS。其中6D_INT为ICM20608的中断引脚,连接到PA14引脚上,在本章实验上没有用到ICM20608的中断引脚。
44.6 实验程序编写
        本实验对应的例程路径为:开发板光盘1、程序源码2、Linux驱动例程22_spi。
44.6.1 修改设备树
        1、添加或者查找ICM20608所使用的IO的pinmux配置
        首先在stm32mp15-pinctrl.dtsi文件中添加IO配置信息,ICM20608连接到了STM32MP157的SPI1接口,因此先在stm32mp15-pinctrl.dtsi里面搜索一下,看看有没有SPI1接口引脚配置(在本教程中,默认是有的)。如果没有的话就自行添加,有的话就检查一下SPI1接口的引脚配置是否和自己所使用的硬件一致,不一致的话就要修改。修改后的引脚信息如下所示:
示例代码44.6.1.1 spi1的pinmux配置
  1. 1  spi1_pins_a: spi1-0 {
  2. 2   pins1 {
  3. 3       pinmux =         <STM32_PINMUX('Z', 0, AF5)>,         /* SPI1_SCK         */
  4. 4                             <STM32_PINMUX('Z', 2, AF5)>;        /* SPI1_MOSI         */
  5. 5       bias-disable;
  6. 6       drive-push-pull;
  7. 7       slew-rate = <1>;
  8. 8   };
  9. 9  
  10. 10  pins2 {
  11. 11      pinmux = <STM32_PINMUX('Z', 1, AF5)>;                 /* SPI1_MISO         */
  12. 12      bias-disable;
  13. 13  };
  14. 14
  15. 15  pins3 {
  16. 16      pinmux = <STM32_PINMUX('Z', 3, GPIO)>;          /* SPI1_NSS         */
  17. 17          drive-push-pull;
  18. 18          bias-pull-up;
  19. 19      output-high;
  20. 20          slew-rate = <0>;
  21. 21  };
  22. 22 };
  23. 23
  24. 24 spi1_sleep_pins_a: spi1-sleep-0 {
  25. 25  pins {
  26. 26      pinmux = <STM32_PINMUX('Z', 0, ANALOG)>,         /* SPI1_SCK         */
  27. 27           <STM32_PINMUX('Z', 1, ANALOG)>,                 /* SPI1_MISO         */
  28. 28           <STM32_PINMUX('Z', 2, ANALOG)>,                 /* SPI1_MOSI         */
  29. 29           <STM32_PINMUX('Z', 3, ANALOG)>;                 /* SPI1_NSS         */
  30. 30  };
  31. 31 };
复制代码


        示例代码44.6.3.1里15~21行是设置ICM20608的片选信号,直接复用为GPIO,也就是使用软件片选。
2、在SPI1节点下添加pinmux并追加icm20608子节点
        在stm32mp157d-atk.dts文件,追加SPI1节点,追加如下所示内容:
示例代码44.6.1.2追加内容的spi1节点
  1. 1   &spi1 {
  2. 2       pinctrl-names = "default", "sleep";
  3. 3       pinctrl-0 = <&spi1_pins_a>;
  4. 4       pinctrl-1 = <&spi1_sleep_pins_a>;
  5. 5       cs-gpios = <&gpioz 3 GPIO_ACTIVE_LOW>;
  6. 6       status = "okay";
  7. 7
  8. 8       spidev: icm20608@0 {
  9. 9           compatible = "alientek,icm20608";
  10. 10          reg = <0>; /* CS #0 */
  11. 11          spi-max-frequency = <80000000>;
  12. 12      };
  13. 13  };
复制代码


        第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>;
上述描述说明此时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接口所能支持的最大的时钟频率。
44.6.2 编写ICM20608驱动
新建名为“22_spi”的文件夹,然后在22_spi文件夹里面创建vscode工程,工作区命名为“spi”。工程创建好以后新建icm20608.c和icm20608reg.h这两个文件,icm20608.c为ICM20608的驱动代码,icm20608reg.h是ICM20608寄存器头文件。先在icm20608reg.h中定义好ICM20608的寄存器,输入如下内容(有省略,完整的内容请参考例程):
示例代码44.6.2.1 icm20608reg.h文件内容
  1. 1  #ifndef ICM20608_H
  2. 2  #define ICM20608_H
  3. 3  /***************************************************************
  4. 4  Copyright &#169; ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
  5. 5  文件名         : icm20608reg.h
  6. 6  作者            : 左忠凯
  7. 7  版本            : V1.0
  8. 8  描述           : ICM20608寄存器地址描述头文件
  9. 9  其他             : 无
  10. 10 论坛            : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  11. 11 日志            : 初版V1.0 2019/9/2 左忠凯创建
  12. 12 ***************************************************************/
  13. 13 #define ICM20608G_ID         0XAF    /* ID值 */
  14. 14 #define ICM20608D_ID         0XAE    /* ID值 */
  15. 15
  16. 16 /* ICM20608寄存器
  17. 17  *复位后所有寄存器地址都为0,除了
  18. 18  *Register 107(0X6B) Power Management 1          = 0x40
  19. 19  *Register 117(0X75) WHO_AM_I                     = 0xAF或0xAE
  20. 20  */
  21. 21 /* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
  22. 22 #define  ICM20_SELF_TEST_X_GYRO            0x00
  23. 23 #define  ICM20_SELF_TEST_Y_GYRO             0x01
  24. 24 #define  ICM20_SELF_TEST_Z_GYRO            0x02
  25. 25 #define  ICM20_SELF_TEST_X_ACCEL           0x0D
  26. 26 #define  ICM20_SELF_TEST_Y_ACCEL            0x0E
  27. 27 #define  ICM20_SELF_TEST_Z_ACCEL            0x0F
  28. ......
  29. 88 #endif
复制代码


        接下来继续编写icm20608.c文件,因为icm20608.c文件内容比较长,因此这里就将其分开来讲解。
1、icm20608设备结构体创建
        首先创建一个icm20608设备结构体,如下所示:
示例代码44.6.2.2 icm20608设备结构体创建
  1. 1   #include <linux/spi/spi.h>
  2. 2   #include <linux/kernel.h>
  3. 3   #include <linux/module.h>
  4. ......
  5. 14
  6. 15  #define ICM20608_CNT    1
  7. 16  #define ICM20608_NAME   "icm20608"
  8. 17
  9. 18  struct icm20608_dev {
  10. 19      struct spi_device *spi;           /* spi设备                         */
  11. 20      dev_t devid;                        /* 设备号                             */
  12. 21      struct cdev cdev;                   /* cdev                             */
  13. 22      struct class *class;               /* 类                                      */
  14. 23      struct device *device;              /* 设备                                    */
  15. 24      struct device_node  *nd;            /* 设备节点                                 */
  16. 25      signed int gyro_x_adc;              /* 陀螺仪X轴原始值           */
  17. 26      signed int gyro_y_adc;              /* 陀螺仪Y轴原始值            */
  18. 27      signed int gyro_z_adc;              /* 陀螺仪Z轴原始值             */
  19. 28      signed int accel_x_adc;             /* 加速度计X轴原始值          */
  20. 29      signed int accel_y_adc;             /* 加速度计Y轴原始值          */
  21. 30      signed int accel_z_adc;             /* 加速度计Z轴原始值          */
  22. 31      signed int temp_adc;                /* 温度原始值                  */
  23. 32  };
复制代码


        icm20608的设备结构体icm20608_dev没什么好讲的,重点看一下第19行的spi,对于SPI设备驱动来讲最核心的就是spi_device。probe函数会向驱动提供当前SPI设备对应的spi_device,因此把这个结构体赋值到spi成员里,我们可以在字符串设备里操作spi成员就可以操作对应的SPI设备。
        2、icm20608的spi_driver注册与注销
        对于SPI设备驱动,首先就是要初始化并向系统注册spi_driver,icm20608的spi_driver初始化、注册与注销代码如下:
示例代码44.6.2.3 icm20608的spi_driver初始化、注册与注销
  1. 1   /* 传统匹配方式ID列表 */
  2. 2   static const struct spi_device_id icm20608_id[] = {
  3. 3       {"alientek,icm20608", 0},
  4. 4       {}
  5. 5   };
  6. 6
  7. 7   /* 设备树匹配列表 */
  8. 8   static const struct of_device_id icm20608_of_match[] = {
  9. 9       { .compatible = "alientek,icm20608" },
  10. 10      { /* Sentinel */ }
  11. 11  };
  12. 12
  13. 13  /* SPI驱动结构体 */
  14. 14  static struct spi_driver icm20608_driver = {
  15. 15      .probe = icm20608_probe,
  16. 16      .remove = icm20608_remove,
  17. 17      .driver = {
  18. 18              .owner = THIS_MODULE,
  19. 19              .name = "icm20608",
  20. 20              .of_match_table = icm20608_of_match,
  21. 21             },
  22. 22      .id_table = icm20608_id,
  23. 23  };
  24. 24
  25. 25  /*
  26. 26   * @description         : 驱动入口函数
  27. 27   * <a href="home.php?mod=space&uid=271674" target="_blank">@param</a>               : 无
  28. 28   * @return              : 无
  29. 29   */
  30. 30  static int __init icm20608_init(void)
  31. 31  {
  32. 32      return spi_register_driver(&icm20608_driver);
  33. 33  }
  34. 34
  35. 35  /*
  36. 36   * @description         : 驱动出口函数
  37. 37   * @param               : 无
  38. 38   * @return              : 无
  39. 39   */
  40. 40  static void __exit icm20608_exit(void)
  41. 41  {
  42. 42      spi_unregister_driver(&icm20608_driver);
  43. 43  }
  44. 44
  45. 45  module_init(icm20608_init);
  46. 46  module_exit(icm20608_exit);
  47. 47  MODULE_LICENSE("GPL");
  48. 48  MODULE_AUTHOR("ALIENTEK");
  49. 49  MODULE_INFO(intree, "Y");
复制代码


        第2~5行,传统的设备和驱动匹配表。
        第8~11行,设备树的设备与驱动匹配表,这里只有一个匹配项:“alientek,icm20608”。
        第14~23行,icm20608的spi_driver结构体变量,当icm20608设备和此驱动匹配成功以后第15行的icm20608_probe函数就会执行。同样的,当注销此驱动的时候icm20608_remove函数会执行。
        第30~33行,icm20608_init函数为icm20608的驱动入口函数,在此函数中使用spi_register_driver向Linux系统注册上面定义的icm20608_driver。
        第40~43行,icm20608_exit函数为icm20608的驱动出口函数,在此函数中使用spi_unregister_driver注销掉前面注册的icm20608_driver。
3、probe&remove函数
        icm20608_driver中的probe和remove函数内容如下所示:
示例代码44.6.3.4 probe和remove函数
  1. 1  /*
  2. 2    * @description         : spi驱动的probe函数,当驱动与设备匹配以后此函数
  3. 3    *                    就会执行
  4. 4    * @param - spi          : spi设备
  5. 5    */
  6. 6  static int icm20608_probe(struct spi_device *spi)
  7. 7  {
  8. 8           int ret;
  9. 9           struct icm20608_dev *icm20608dev;
  10. 10  
  11. 11          /* 分配icm20608dev对象的空间 */
  12. 12          icm20608dev = devm_kzalloc(&spi->dev, sizeof(*icm20608dev),
  13.                                                                   GFP_KERNEL);
  14. 13          if(!icm20608dev)
  15. 14              return -ENOMEM;
  16. 15      
  17. 16          /* 注册字符设备驱动 */
  18. 17          /* 1、创建设备号 */
  19. 18                  ret = alloc_chrdev_region(&icm20608dev->devid, 0, ICM20608_CNT,
  20.                                                                                  ICM20608_NAME);
  21. 19          if(ret < 0) {
  22. 20              pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n",
  23.                                 ICM20608_NAME, ret);
  24. 21                 return 0;
  25. 22          }
  26. 23
  27. 24          /* 2、初始化cdev */
  28. 25          icm20608dev->cdev.owner = THIS_MODULE;
  29. 26          cdev_init(&icm20608dev->cdev, &icm20608_ops);
  30. 27  
  31. 28          /* 3、添加一个cdev */
  32. 29          ret = cdev_add(&icm20608dev->cdev, icm20608dev->devid,
  33.                                                  ICM20608_CNT);
  34. 30          if(ret < 0) {
  35. 31              goto del_unregister;
  36. 32          }
  37. 33  
  38. 34          /* 4、创建类 */
  39. 35          icm20608dev->class = class_create(THIS_MODULE, ICM20608_NAME);
  40. 36          if (IS_ERR(icm20608dev->class)) {
  41. 37              goto del_cdev;
  42. 38          }
  43. 39
  44. 40          /* 5、创建设备 */
  45. 41          icm20608dev->device = device_create(icm20608dev->class, NULL,
  46.                                                         icm20608dev->devid, NULL, ICM20608_NAME);
  47. 42          if (IS_ERR(icm20608dev->device)) {
  48. 43              goto destroy_class;
  49. 44          }
  50. 45          icm20608dev->spi = spi;
  51. 46  
  52. 47          /*初始化spi_device */
  53. 48          spi->mode = SPI_MODE_0; /*MODE0,CPOL=0,CPHA=0*/
  54. 49          spi_setup(spi);
  55. 50  
  56. 51          /* 初始化ICM20608内部寄存器 */
  57. 52          icm20608_reginit(icm20608dev);  
  58. 53          /* 保存icm20608dev结构体 */
  59. 54          spi_set_drvdata(spi, icm20608dev);
  60. 55
  61. 56          return 0;
  62. 57         destroy_class:
  63. 58          device_destroy(icm20608dev->class, icm20608dev->devid);
  64. 59         del_cdev:
  65. 60          cdev_del(&icm20608dev->cdev);
  66. 61         del_unregister:
  67. 62          unregister_chrdev_region(icm20608dev->devid, ICM20608_CNT);
  68. 63          return -EIO;
  69. 64 }
  70. 65
  71. 66 /*
  72. 67  * @description        : spi驱动的remove函数,移除spi驱动的时候此函数会执行
  73. 68  * @param - spi         : spi设备
  74. 69  * @return                 : 0,成功;其他负值,失败
  75. 70  */
  76. 71 static int icm20608_remove(struct spi_device *spi)
  77. 72 {
  78. 73          struct icm20608_dev *icm20608dev = spi_get_drvdata(spi);
  79. 74          /* 注销字符设备驱动 */
  80. 75          /* 1、删除cdev */
  81. 76          cdev_del(&icm20608dev->cdev);
  82. 77          /* 2、注销设备号 */
  83. 78          unregister_chrdev_region(icm20608dev->devid, ICM20608_CNT);
  84. 79          /* 3、注销设备 */
  85. 80          device_destroy(icm20608dev->class, icm20608dev->devid);
  86. 81          /* 4、注销类 */
  87. 82          class_destroy(icm20608dev->class);
  88. 83          return 0;
  89. 84 }
复制代码


        第6~64行,probe函数,当设备与驱动匹配成功以后此函数就会执行。第12行就是给自定义的结构体申请空间。第25~44行都是标准的注册字符设备驱动。注意我们这边驱动没有去读取“cs-gpios”属性,那是因为SPI的核心会自动控制的,我们不用关心它。
        第45行,将probe函数的spi_device参数赋值给我们自定义的spi成员变量,也就是保存spi_device结构体对象。
        第48行,设置SPI为模式0,也就是CPOL=0,CPHA=0。
        第49行,设置好spi_device以后需要使用spi_setup配置一下。
        第52行,调用icm20608_reginit函数初始化ICM20608,主要是初始化ICM20608指定寄存器。
        第54行,icm20608dev变量是在probe函数里申请的内存空间,在remove函数需要释放掉。这里使用spi_set_drvdata函数将icm20608dev地址保存起来,后面再remove函数里面可以通过spi_get_drvdata函数获取到icm20608dev地址。
        第71~84行,icm20608_remove函数,注销驱动的时候此函数就会执行。我们可以使用spi_get_drvdata函数来获取icm20608dev的地址。
4、icm20608寄存器读写与初始化
        SPI驱动最终是通过读写icm20608的寄存器来实现的,因此需要编写相应的寄存器读写函数,并且使用这些读写函数来完成对icm20608的初始化。icm20608的寄存器读写以及初始化代码如下:
示例代码44.6.2.5 icm20608寄存器读写以及初始化
  1. 1   /*
  2. 2    * @description         : 从icm20608读取多个寄存器数据
  3. 3    * @param – dev        : icm20608设备
  4. 4    * @param – reg        : 要读取的寄存器首地址
  5. 5    * @param – val        : 读取到的数据
  6. 6    * @param – len        : 要读取的数据长度
  7. 7    * @return              : 操作结果
  8. 8    */
  9. 9   static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg,
  10. void *buf, int len)
  11. 10  {
  12. 11
  13. 12      int ret = -1;
  14. 13      unsigned char txdata[1];
  15. 14      unsigned char * rxdata;
  16. 15      struct spi_message m;
  17. 16      struct spi_transfer *t;
  18. 17      struct spi_device *spi = (struct spi_device *)dev->spi;
  19. 18      
  20. 19      t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);/* 申请内存*/
  21. 20      if(!t) {
  22. 21          return -ENOMEM;
  23. 22      }
  24. 23      rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL);   /* 申请内存 */
  25. 24      if(!rxdata) {
  26. 25          goto out1;
  27. 26      }
  28. 27      /* 一共发送len+1个字节的数据,第一个字节为
  29. 28      寄存器首地址,一共要读取len个字节长度的数据,*/
  30. 29      txdata[0] = reg | 0x80;         /* 写数据的时候首寄存器地址bit8要置1         */           
  31. 30      t->tx_buf = txdata;              /* 要发送的数据                                                         */
  32. 31      t->rx_buf = rxdata;             /* 要读取的数据                                                         */
  33. 32      t->len = len+1;                  /* t->len=发送的长度+读取的长度                 */
  34. 33      spi_message_init(&m);           /* 初始化spi_message                                         */
  35. 34      spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message*/
  36. 35      ret = spi_sync(spi, &m);        /* 同步发送                                                                 */
  37. 36      if(ret) {
  38. 37          goto out2;
  39. 38      }
  40. 39      
  41. 40      memcpy(buf , rxdata+1, len);          /* 只需要读取的数据         */
  42. 41
  43. 42  out2:
  44. 43      kfree(rxdata);                          /* 释放内存                         */
  45. 44  out1:   
  46. 45      kfree(t);                               /* 释放内存                         */
  47. 46      
  48. 47      return ret;
  49. 48  }
  50. 49
  51. 50  /*
  52. 51   * @description : 向icm20608多个寄存器写入数据
  53. 52   * @param - dev:  icm20608设备
  54. 53   * @param - reg:  要写入的寄存器首地址
  55. 54   * @param - val:  要写入的数据缓冲区
  56. 55   * @param - len:  要写入的数据长度
  57. 56   * @return    :   操作结果
  58. 57   */
  59. 58  static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg,
  60.                                                                                 u8 *buf, u8 len)
  61. 59  {
  62. 60      int ret = -1;
  63. 61      unsigned char *txdata;
  64. 62      struct spi_message m;
  65. 63      struct spi_transfer *t;
  66. 64      struct spi_device *spi = (struct spi_device *)dev->spi;
  67. 65      
  68. 66      t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);/* 申请内存*/
  69. 67      if(!t) {
  70. 68          return -ENOMEM;
  71. 69      }
  72. 70      
  73. 71      txdata = kzalloc(sizeof(char)+len, GFP_KERNEL);
  74. 72      if(!txdata) {
  75. 73          goto out1;
  76. 74      }
  77. 75      
  78. 76      /* 一共发送len+1个字节的数据,第一个字节为
  79. 77      寄存器首地址,len为要写入的寄存器的集合,*/
  80. 78      *txdata = reg & ~0x80;                  /* 写数据的时候首寄存器地址bit8要清零 */
  81. 79      memcpy(txdata+1, buf, len); /* 把len个数据拷贝到txdata里        */
  82. 80      t->tx_buf = txdata;                 /* 要发送的数据                                         */
  83. 81      t->len = len+1;                     /* t->len=发送的长度+读取的长度        */
  84. 82      spi_message_init(&m);               /* 初始化spi_message                         */
  85. 83      spi_message_add_tail(t, &m);/*添加到spi_message队列                 */
  86. 84      ret = spi_sync(spi, &m);            /* 同步发送                                                 */
  87. 85      if(ret) {
  88. 86          goto out2;
  89. 87      }
  90. 88      
  91. 89  out2:
  92. 90      kfree(txdata);                      /* 释放内存 */
  93. 91  out1:
  94. 92      kfree(t);                           /* 释放内存 */
  95. 93      return ret;
  96. 94
  97. 95  }
  98. 96
  99. 97  /*
  100. 98   * @description         : 读取icm20608指定寄存器值,读取一个寄存器
  101. 99   * @param – dev        : icm20608设备
  102. 100  * @param – reg        : 要读取的寄存器
  103. 101  * @return                    : 读取到的寄存器值
  104. 102  */
  105. 103 static unsigned char icm20608_read_onereg(struct icm20608_dev *dev,
  106. u8 reg)
  107. 104 {
  108. 105     u8 data = 0;
  109. 106     icm20608_read_regs(dev, reg, &data, 1);
  110. 107     return data;
  111. 108 }
  112. 109
  113. 110 /*
  114. 111  * @description         : 向icm20608指定寄存器写入指定的值,写一个寄存器
  115. 112  * @param – dev        : icm20608设备
  116. 113  * @param – reg        : 要写的寄存器
  117. 114  * @param – data        : 要写入的值
  118. 115  * @return                   : 无
  119. 116  */
  120. 117
  121. 118 static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg,
  122. u8 value)
  123. 119 {
  124. 120     u8 buf = value;
  125. 121     icm20608_write_regs(dev, reg, &buf, 1);
  126. 122 }
  127. 123
  128. 124 /*
  129. 125  * @description         : 读取ICM20608的数据,读取原始数据,包括三轴陀螺仪、
  130. 126  *                              : 三轴加速度计和内部温度。
  131. 127  * @param - dev         : ICM20608设备
  132. 128  * @return              : 无。
  133. 129  */
  134. 130 void icm20608_readdata(struct icm20608_dev *dev)
  135. 131 {
  136. 132     unsigned char data[14];
  137. 133     icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);
  138. 134
  139. 135     dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]);
  140. 136     dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]);
  141. 137     dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]);
  142. 138     dev->temp_adc    = (signed short)((data[6] << 8) | data[7]);
  143. 139     dev->gyro_x_adc  = (signed short)((data[8] << 8) | data[9]);
  144. 140     dev->gyro_y_adc  = (signed short)((data[10] << 8) | data[11]);
  145. 141     dev->gyro_z_adc  = (signed short)((data[12] << 8) | data[13]);
  146. 142 }
  147. 143
  148. 144 /*
  149. 145  * ICM20608内部寄存器初始化函数
  150. 146  * @param - spi         : 要操作的设备
  151. 147  * @return                  : 无
  152. 148  */
  153. 149 void icm20608_reginit(struct icm20608_dev *dev)
  154. 150 {
  155. 151     u8 value = 0;
  156. 152     
  157. 153     icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x80);
  158. 154     mdelay(50);
  159. 155     icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x01);
  160. 156     mdelay(50);
  161. 157
  162. 158     value = icm20608_read_onereg(dev, ICM20_WHO_AM_I);
  163. 159     printk("ICM20608 ID = %#X\r\n", value);
  164. 160
  165. 161     icm20608_write_onereg(dev, ICM20_SMPLRT_DIV, 0x00);
  166. 162     icm20608_write_onereg(dev, ICM20_GYRO_CONFIG, 0x18);
  167. 163     icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG, 0x18);
  168. 164     icm20608_write_onereg(dev, ICM20_CONFIG, 0x04);
  169. 165     icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG2, 0x04);
  170. 166     icm20608_write_onereg(dev, ICM20_PWR_MGMT_2, 0x00);
  171. 167     icm20608_write_onereg(dev, ICM20_LP_MODE_CFG, 0x00);
  172. 168     icm20608_write_onereg(dev, ICM20_FIFO_EN, 0x00);
  173. 169 }
复制代码


        第9~48行,icm20608_read_regs函数,从icm20608中读取连续多个寄存器数据;注意:在本实验中,SPI为全双工通讯没有所谓的发送和接收长度之分。要读取或者发送N个字节就要封装N+1个字节,第1个字节是告诉设备我们要进行读还是写,后面的N个字节才是我们要读或者发送的数据。
        第58~95行,icm20608_write_rege函数,向icm20608连续写入多个寄存器数据。此函数和icm20608_read_regs函数区别不大。
        第103~108行,icm20608_read_onereg函数,读取icm20608指定一个寄存器数据。
        第118~122行,icm20608_write_onereg函数,向icm20608指定一个寄存器写入数据。
        第130~142行,icm20608_readdata函数,读取icm20608六轴传感器和温度传感器原始数据值,应用程序读取icm20608的时候这些传感器原始数据就会上报给应用程序。
        第149~169行,icm20608_reginit函数,初始化icm20608。
5、字符设备驱动框架
        icm20608的字符设备驱动框架如下:
示例代码44.6.2.6 icm20608字符设备驱动
  1. 1   /*
  2. 2    * @description           : 打开设备
  3. 3    * @param – inode        : 传递给驱动的inode
  4. 4    * @param - filp         : 要打开的设备文件(文件描述符)
  5. 5    * @return                : 0 成功;其他 失败
  6. 6    */
  7. 7   static int icm20608_open(struct inode *inode, struct file *filp)
  8. 8   {
  9. 9       return 0;
  10. 10  }
  11. 11
  12. 12  /*
  13. 13   * @description         : 从设备读取数据
  14. 14   * @param – filp        : 要打开的设备文件(文件描述符)
  15. 15   * @param - buf          : 返回给用户空间的数据缓冲区
  16. 16   * @param - cnt         : 要读取的数据长度
  17. 17   * @param – offt        : 相对于文件首地址的偏移
  18. 18   * @return                : 读取的字节数,如果为负值,表示读取失败
  19. 19   */
  20. 20  static ssize_t icm20608_read(struct file *filp, char __user *buf,
  21. size_t cnt, loff_t *off)
  22. 21  {
  23. 22      signed int data[7];
  24. 23      long err = 0;
  25. 24      struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;
  26. 25      struct icm20608_dev *dev = container_of(cdev, struct icm20608_dev,
  27.                                      cdev);
  28. 26              
  29. 27      icm20608_readdata(dev);
  30. 28      data[0] = dev->gyro_x_adc;
  31. 29      data[1] = dev->gyro_y_adc;
  32. 30      data[2] = dev->gyro_z_adc;
  33. 31      data[3] = dev->accel_x_adc;
  34. 32      data[4] = dev->accel_y_adc;
  35. 33      data[5] = dev->accel_z_adc;
  36. 34      data[6] = dev->temp_adc;
  37. 35      err = copy_to_user(buf, data, sizeof(data));
  38. 36      return 0;
  39. 37  }
  40. 38
  41. 39  /*
  42. 40   * @description          : 关闭/释放设备
  43. 41   * @param – filp        : 要关闭的设备文件(文件描述符)
  44. 42   * @return                : 0 成功;其他 失败
  45. 43   */
  46. 44  static int icm20608_release(struct inode *inode, struct file *filp)
  47. 45  {
  48. 46      return 0;
  49. 47  }
  50. 48
  51. 49  /* icm20608操作函数 */
  52. 50  static const struct file_operations icm20608_ops = {
  53. 51      .owner = THIS_MODULE,
  54. 52      .open = icm20608_open,
  55. 53      .read = icm20608_read,
  56. 54      .release = icm20608_release,
  57. 55  };
复制代码


        字符设备驱动框架没什么好说的,重点是第20~37行的icm20608_read函数,当应用程序调用read函数读取icm20608设备文件的时候此函数就会执行。其中的24和25行里,通过probe注册的cdev变量,获取到我们自定义的icm20608_dev结构体的首地址。此函数调用上面编写好的icm20608_readdata函数读取icm20608的原始数据并将其上报给应用程序。大家注意,在内核中尽量不要使用浮点运算,所以不要在驱动将icm20608的原始值转换为对应的实际值,因为会涉及到浮点计算。
        这个驱动例程还不是很完美,在icm20608_read没有加锁。如果多个程序去读取这个驱动的时候就会出现读取数据出错,有能力的可以把这一点补充完整。
44.6.3 编写测试APP
新建icm20608App.c文件,然后在里面输入如下所示内容:
示例代码44.6.3.1 icm20608App.c文件代码
  1. 12  #include "stdio.h"
  2. 13  #include "unistd.h"
  3. 14  #include "sys/types.h"
  4. 15  #include "sys/stat.h"
  5. 16  #include "sys/ioctl.h"
  6. 17  #include "fcntl.h"
  7. 18  #include "stdlib.h"
  8. 19  #include "string.h"
  9. 20  #include <poll.h>
  10. 21  #include <sys/select.h>
  11. 22  #include <sys/time.h>
  12. 23  #include <signal.h>
  13. 24  #include <fcntl.h>
  14. 25  /*
  15. 26   * @description          : main主程序
  16. 27   * @param – argc        : argv数组元素个数
  17. 28   * @param - argv         : 具体参数
  18. 29   * @return               : 0 成功;其他 失败
  19. 30   */
  20. 31  int main(int argc, char *argv[])
  21. 32  {
  22. 33      int fd;
  23. 34      char *filename;
  24. 35      signed int databuf[7];
  25. 36      unsigned char data[14];
  26. 37      signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;
  27. 38      signed int accel_x_adc, accel_y_adc, accel_z_adc;
  28. 39      signed int temp_adc;
  29. 40
  30. 41      float gyro_x_act, gyro_y_act, gyro_z_act;
  31. 42      float accel_x_act, accel_y_act, accel_z_act;
  32. 43      float temp_act;
  33. 44
  34. 45      int ret = 0;
  35. 46
  36. 47      if (argc != 2) {
  37. 48          printf("Error Usage!\r\n");
  38. 49          return -1;
  39. 50      }
  40. 51
  41. 52      filename = argv[1];
  42. 53      fd = open(filename, O_RDWR);
  43. 54      if(fd < 0) {
  44. 55          printf("can't open file %s\r\n", filename);
  45. 56          return -1;
  46. 57      }
  47. 58
  48. 59      while (1) {
  49. 60          ret = read(fd, databuf, sizeof(databuf));
  50. 61          if(ret == 0) {          /* 数据读取成功 */
  51. 62              gyro_x_adc = databuf[0];
  52. 63              gyro_y_adc = databuf[1];
  53. 64              gyro_z_adc = databuf[2];
  54. 65              accel_x_adc = databuf[3];
  55. 66              accel_y_adc = databuf[4];
  56. 67              accel_z_adc = databuf[5];
  57. 68              temp_adc = databuf[6];
  58. 69
  59. 70              /* 计算实际值 */
  60. 71              gyro_x_act = (float)(gyro_x_adc)  / 16.4;
  61. 72              gyro_y_act = (float)(gyro_y_adc)  / 16.4;
  62. 73              gyro_z_act = (float)(gyro_z_adc)  / 16.4;
  63. 74              accel_x_act = (float)(accel_x_adc) / 2048;
  64. 75              accel_y_act = (float)(accel_y_adc) / 2048;
  65. 76              accel_z_act = (float)(accel_z_adc) / 2048;
  66. 77              temp_act = ((float)(temp_adc) - 25 ) / 326.8 + 25;
  67. 78
  68. 79
  69. 80                      printf("\r\n原始值:\r\n");
  70. 81                         printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc,                                         gyro_y_adc, gyro_z_adc);
  71. 82                       printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc,
  72.                                 accel_y_adc, accel_z_adc);
  73. 83                      printf("temp = %d\r\n", temp_adc);
  74. 84                      printf("实际值:");
  75. 85                     printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz
  76.                  = %.2f°/S\r\n", gyro_x_act, gyro_y_act, gyro_z_act);
  77. 86                            printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n",
  78.                          accel_x_act, accel_y_act, accel_z_act);
  79. 87                      printf("act temp = %.2f°C\r\n", temp_act);
  80. 88          }
  81. 89          usleep(100000);         /*100ms         */
  82. 90      }
  83. 91      close(fd);                          /* 关闭文件         */  
  84. 92      return 0;
  85. 93  }
复制代码


        第60~91行,在while循环中每隔100ms从icm20608中读取一次数据,读取到icm20608原始数据以后将其转换为实际值,比如陀螺仪就是角速度、加速度计就是g值。注意,我们在icm20608驱动中将陀螺仪和加速度计的测量范围全部设置到了最大,分别为±2000和±16g。因此,在计算实际值的时候陀螺仪使用16.4,加速度计使用2048。最终将传感器原始数据和得到的实际值显示在终端上。
44.7 运行测试
44.7.1 编译驱动程序和测试APP
        1、内核使能SPI控制器
        ST官方系统把SPI控制器的驱动编译成模块,我们需要把SPI控制器驱动编译进内核,这样就可以在启动linux内核的时候自动加载SPI控制器驱动,无需我们手动加载,方便我们使用。打开Linux内核图形化配置界面,按下路径找到对应的配置项:
  1. Device Drivers                                                                                         
  2.    SPI support (SPI [=y])
  3.         <*> STMicroelectronics STM32 SPI controller         //编译进内核
复制代码


        如图44.7.1.1所示:
第四十四章Linux SPI总线框架44243.png
图44.7.1.1 SPI控制器配置项
        图44.7.1.1本来是选择为“M”,我们要改为“*”,也就是编译进内核。接着我们重新编译设备树和内核,运行以下命令进行编译:
  1. make dtbs uImage LOADADDR=0XC2000040 -j32
复制代码


使用新编译好的stm32mp157d-atk.dtb和uImage镜像启动系统,如果SPI控制器驱动工作正常就会有图44.7.1.2所示提示信息:
第四十四章Linux SPI总线框架44446.png
图44.7.1.2 SPI控制器初始化
如果没有输出图44.7.1.2中“spi_stm32 44004000.spi: driver initialized”这句话,那就要检查一下设备树和内配配置是否有问题,通过查看/sys/bus/spi/devices/下有没有spi相关的设备,就能够知道设备树配置是否正确,比如本例程如图44.7.1.3所示:
第四十四章Linux SPI总线框架44625.png
图44.7.1.3 SPI设备
2、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第四十章实验基本一样,只是将obj-m变量的值改为“icm20608.o”,Makefile内容如下所示:
示例代码44.7.1.1 Makefile文件
  1. 1  KERNELDIR := /home/zuozhongkai/linux/my_linux/linux-5.4.31
  2. ......
  3. 4  obj-m := icm20608.o
  4. ......
  5. 11 clean:
  6. 12  $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
复制代码


        第4行,设置obj-m变量的值为“icm20608.o”。
        输入如下命令编译出驱动模块文件:
  1. make -j16
复制代码


        编译成功以后就会生成一个名为“icm20608.ko”的驱动模块文件。
3、编译测试APP
        在icm20608App.c这个测试APP中我们用到了浮点计算,而STM32MP1是支持硬件浮点的,因此我们在编译icm20608App.c的时候就可以使能硬件浮点,这样可以加速浮点计算。使能硬件浮点很简单,在编译的时候加入如下参数即可:
  1. -march-armv7-a -mfpu-neon -mfloat=hard
复制代码


输入如下命令使能硬件浮点编译icm20608App.c这个测试程序:
  1. arm-none-linux-gnueabihf-gcc -march=armv7-a -mfpu=neon -mfloat-abi=hard icm20608App.c -o icm20608App
复制代码


        编译成功以后就会生成icm20608App这个应用程序,那么究竟有没有使用硬件浮点呢?使用arm-linux-gnueabihf-readelf查看一下编译出来的icm20608App就知道了,输入如下命令:
  1. arm-none-linux-gnueabihf-readelf -A icm20608App
复制代码


        结果如图44.7.1.4所示:

第四十四章Linux SPI总线框架45494.png
        图44.7.1.4 icm20608App文件信息
        从图44.7.1.4可以看出FPU架构为VFPv3,SIMD使用了NEON,说明icm20608App这个应用程序使用了硬件浮点。
44.7.2 运行测试
        将上一小节编译出来icm20608.ko和icm20608App这两个文件拷贝到rootfs/lib/modules/5.3.41目录中,重启开发板,进入到目录lib/modules/5.3.41中。输入如下命令加载icm20608.ko这个驱动模块。
  1. depmod                                //第一次加载驱动的时候需要运行此命令
  2. modprobe icm20608.ko        //加载驱动模块
复制代码


        当驱动模块加载成功以后使用icm20608App来测试,输入如下命令:
  1. ./icm20608App /dev/icm20608
复制代码


        测试APP会不断的从ICM20608中读取数据,然后输出到终端上,如图44.7.2.1所示:
第四十四章Linux SPI总线框架45903.png
图44.7.2.1获取到的ICM20608数据
        可以看出,开发板静止状态下,Z轴方向的加速度为0.97g,这个就是重力加速度。对于陀螺仪来讲,静止状态下三轴的角速度应该在0°/S左右。ICM20608内温度传感器采集到的温度在39.51度,大家可以晃动一下开发板,这个时候陀螺仪和加速度计的值就会有变化。
正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2024-5-29 14:52

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表