当前位置:   article > 正文

正点原子imx6ull-mini-Linux驱动之Linux SPI 驱动实验(22)

正点原子imx6ull-mini-Linux驱动之Linux SPI 驱动实验(22)

跟上一章一样,其实这些设备驱动,无非就是传感器对应寄存器的读写。而这个读写是建立在各种通信协议上的,比如上一章的i2c,我们做了什么呢,就是把设备注册成一个i2c平台驱动,这个i2c驱动怎么搞的呢,有个i2c_client 从机的地址放在这个结构体内还有个适配器和算法两个结构体。其他的就是正常的i2c的操作。i2c通信的驱动,已经被SOC厂商写好了,我们做的就是调用其API函数去实现对设备的驱动撰写。等于驱动分层,假如第一层是i2c寄存器的物理属性,那么它在设备树内。第二层是通信协议,被写好了,我们写的是基于这个通信协议的设备驱动。并不直接跟寄存器打交道

上一章我们讲解了如何编写 Linux 下的 I2C 设备驱动,SPI 也是很常用的串行通信协议, 本章我们就来学习如何在 Linux 下编写 SPI 设备驱动。本章实验的最终目的就是驱动 I.MX6UALPHA 开发板上的 ICM-20608 这个 SPI 接口的六轴传感器,可以在应用程序中读取 ICM-20608 的原始传感器数据

1:Linux 下 SPI 驱动框架简介

SPI 驱动框架和 I2C 很类似,都分为主机控制器驱动和设备驱动,主机控制器也就是 SOC 的 SPI 控制器接口。比如在裸机篇中的《第二十七章 SPI 实验》,我们编写了 bsp_spi.c 和 bsp_spi.h 这两个文件,这两个文件是 I.MX6U 的 SPI 控制器驱动,我们编写好 SPI 控制器驱动以后就可 以直接使用了,不管是什么 SPI 设备,SPI 控制器部分的驱动都是一样,我们的重点就落在了 种类繁多的 SPI 设备驱动。

1.1:SPI 主机驱动

SPI 主机驱动就是 SOC 的 SPI 控制器驱动,类似 I2C 驱动里面的适配器驱动。Linux 内核 使用 spi_master 表示 SPI 主机驱动,spi_master 是个结构体,定义在 include/linux/spi/spi.h 文件 中,内容如下(有缩减):

  1. 315 struct spi_master {
  2. 316 struct device dev;
  3. 317
  4. 318 struct list_head list;
  5. ......
  6. 326 s16 bus_num;
  7. 327
  8. 328 /* chipselects will be integral to many controllers; some others
  9. 329 * might use board-specific GPIOs.
  10. 330 */
  11. 331 u16 num_chipselect;
  12. 332
  13. 333 /* some SPI controllers pose alignment requirements on DMAable
  14. 334 * buffers; let protocol drivers know about these requirements.
  15. 335 */
  16. 336 u16 dma_alignment;
  17. 337
  18. 338 /* spi_device.mode flags understood by this controller driver */
  19. 339 u16 mode_bits;
  20. 340
  21. 341 /* bitmask of supported bits_per_word for transfers */
  22. 342 u32 bits_per_word_mask;
  23. ......
  24. 347 /* limits on transfer speed */
  25. 348 u32 min_speed_hz;
  26. 349 u32 max_speed_hz;
  27. 350
  28. 351 /* other constraints relevant to this driver */
  29. 352 u16 flags;
  30. ......
  31. 359 /* lock and mutex for SPI bus locking */
  32. 360 spinlock_t bus_lock_spinlock;
  33. 361 struct mutex bus_lock_mutex;
  34. 362
  35. 363 /* flag indicating that the SPI bus is locked for exclusive use */
  36. 364 bool bus_lock_flag;
  37. ......
  38. 372 int (*setup)(struct spi_device *spi);
  39. 373
  40. ......
  41. 393 int (*transfer)(struct spi_device *spi,
  42. 394 struct spi_message *mesg);
  43. ......
  44. 434 int (*transfer_one_message)(struct spi_master *master,
  45. 435 struct spi_message *mesg);
  46. ......
  47. 462 };

第 393 行,transfer 函数,和 i2c_algorithm 中的 master_xfer 函数一样,控制器数据传输函 数。 第 434 行,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.1.1:spi_master 申请与释放

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

  1. struct spi_master *spi_alloc_master(struct device *dev,
  2. unsigned size)

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

dev:设备,一般是 platform_device 中的 dev 成员变量。

size:私有数据大小,可以通过 spi_master_get_devdata 函数获取到这些私有数据。

返回值:申请到的 spi_master。 spi_master 的释放通过 spi_master_put 函数来完成,

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

void spi_master_put(struct spi_master *master)

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

master:要释放的 spi_master。

返回值:无。

1.1.2:spi_master 的注册与注销

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

int spi_register_master(struct spi_master *master)

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

master:要注册的 spi_master。

返回值:0,成功;负值,失败。

I.MX6U 的 SPI 主机驱动会采用 spi_bitbang_start 这个 API 函数来完成 spi_master 的注册, spi_bitbang_start 函数内部其实也是通过调用 spi_register_master 函数来完成 spi_master 的注册。 如果要注销 spi_master 的话可以使用 spi_unregister_master 函数,此函数原型为:

void spi_unregister_master(struct spi_master *master)

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

master:要注销的 spi_master。

返回值:无。

如果使用 spi_bitbang_start 注册 spi_master 的话就要使用 spi_bitbang_stop 来注销掉 spi_master。

1.2:SPI 设备驱动

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

  1. 180 struct spi_driver {
  2. 181 const struct spi_device_id *id_table;
  3. 182 int (*probe)(struct spi_device *spi);
  4. 183 int (*remove)(struct spi_device *spi);
  5. 184 void (*shutdown)(struct spi_device *spi);
  6. 185 struct device_driver driver;
  7. 186 };

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

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 注册示例程序如下:

  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。

1.3:SPI 设备和驱动匹配过程

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

  1. 131 struct bus_type spi_bus_type = {
  2. 132 .name = "spi",
  3. 133 .dev_groups = spi_dev_groups,
  4. 134 .match = spi_match_device,
  5. 135 .uevent = spi_uevent,
  6. 136 };

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

  1. 99 static int spi_match_device(struct device *dev,
  2. struct device_driver *drv)
  3. 100 {
  4. 101 const struct spi_device *spi = to_spi_device(dev);
  5. 102 const struct spi_driver *sdrv = to_spi_driver(drv);
  6. 103
  7. 104 /* Attempt an OF style match */
  8. 105 if (of_driver_match_device(dev, drv))
  9. 106 return 1;
  10. 107
  11. 108 /* Then try ACPI */
  12. 109 if (acpi_driver_match_device(dev, drv))
  13. 110 return 1;
  14. 111
  15. 112 if (sdrv->id_table)
  16. 113 return !!spi_match_id(sdrv->id_table, spi);
  17. 114
  18. 115 return strcmp(spi->modalias, drv->name) == 0;
  19. 116 }

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

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

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

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

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

2:I.MX6U SPI 主机驱动分析

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

  1. 1 ecspi3: ecspi@02010000 {
  2. 2 #address-cells = <1>;
  3. 3 #size-cells = <0>;
  4. 4 compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
  5. 5 reg = <0x02010000 0x4000>;
  6. 6 interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
  7. 7 clocks = <&clks IMX6UL_CLK_ECSPI3>,
  8. 8 <&clks IMX6UL_CLK_ECSPI3>;
  9. 9 clock-names = "ipg", "per";
  10. 10 dmas = <&sdma 7 7 1>, <&sdma 8 7 2>;
  11. 11 dma-names = "rx", "tx";
  12. 12 status = "disabled";
  13. 13 };

重点来看一下第 4 行的 compatible 属性值,compatible 属性有两个值“fsl,imx6ul-ecspi”和 “fsl,imx51-ecspi”,在 Linux 内核源码中搜素这两个属性值即可找到 I.MX6U 对应的 ECSPI(SPI) 主机驱动。I.MX6U 的 ECSPI 主机驱动文件为 drivers/spi/spi-imx.c,在此文件中找到如下内容:

  1. 694 static struct platform_device_id spi_imx_devtype[] = {
  2. 695 {
  3. 696 .name = "imx1-cspi",
  4. 697 .driver_data = (kernel_ulong_t) &imx1_cspi_devtype_data,
  5. 698 }, {
  6. 699 .name = "imx21-cspi",
  7. 700 .driver_data = (kernel_ulong_t) &imx21_cspi_devtype_data,
  8. ......
  9. 713 }, {
  10. 714 .name = "imx6ul-ecspi",
  11. 715 .driver_data = (kernel_ulong_t) &imx6ul_ecspi_devtype_data,
  12. 716 }, {
  13. 717 /* sentinel */
  14. 718 }
  15. 719 };
  16. 720
  17. 721 static const struct of_device_id spi_imx_dt_ids[] = {
  18. 722 { .compatible = "fsl,imx1-cspi", .data =
  19. &imx1_cspi_devtype_data, },
  20. ......
  21. 728 { .compatible = "fsl,imx6ul-ecspi", .data =
  22. &imx6ul_ecspi_devtype_data, },
  23. 729 { /* sentinel */ }
  24. 730 };
  25. 731 MODULE_DEVICE_TABLE(of, spi_imx_dt_ids);
  26. ......
  27. 1338 static struct platform_driver spi_imx_driver = {
  28. 1339 .driver = {
  29. 1340 .name = DRIVER_NAME,
  30. 1341 .of_match_table = spi_imx_dt_ids,
  31. 1342 .pm = IMX_SPI_PM,
  32. 1343 },
  33. 1344 .id_table = spi_imx_devtype,
  34. 1345 .probe = spi_imx_probe,
  35. 1346 .remove = spi_imx_remove,
  36. 1347 };
  37. 1348 module_platform_driver(spi_imx_driver);

第 714 行,spi_imx_devtype 为 SPI 无设备树匹配表。

第 721 行,spi_imx_dt_ids 为 SPI 设备树匹配表。

第 728 行,“fsl,imx6ul-ecspi”匹配项,因此可知 I.MX6U 的 ECSPI 驱动就是 spi-imx.c 这个 文件。

第 1338~1347 行,platform_driver 驱动框架,和 I2C 的适配器驱动一样,SPI 主机驱动器采 用了 platfom 驱动框架。当设备和驱动匹配成功以后 spi_imx_probe 函数就会执行。 spi_imx_probe 函数会从设备树中读取相应的节点属性值,申请并初始化 spi_master,最后 调用 spi_bitbang_start 函数(spi_bitbang_start 会调用 spi_register_master 函数)向 Linux 内核注册 spi_master。 对于 I.MX6U 来讲,SPI 主机的最终数据收发函数为 spi_imx_transfer,此函数通过如下层 层调用最终实现 SPI 数据发送:

  1. spi_imx_transfer
  2. -> spi_imx_pio_transfer
  3. -> spi_imx_push
  4. -> spi_imx->tx

spi_imx 是个 spi_imx_data 类型的机构指针变量,其中 tx 和 rx 这两个成员变量分别为 SPI 数据发送和接收函数。I.MX6U SPI 主机驱动会维护一个 spi_imx_data 类型的变量 spi_imx,并 且使用 spi_imx_setupxfer 函数来设置 spi_imx 的 tx 和 rx 函数。根据要发送的数据数据位宽的不 同,分别有 8 位、16 位和 32 位的发送函数,如下所示:

  1. spi_imx_buf_tx_u8
  2. spi_imx_buf_tx_u16
  3. spi_imx_buf_tx_u32

同理,也有 8 位、16 位和 32 位的数据接收函数,如下所示:

  1. spi_imx_buf_rx_u8
  2. spi_imx_buf_rx_u16
  3. spi_imx_buf_rx_u32

我们就以 spi_imx_buf_tx_u8 这个函数为例,看看,一个自己的数据发送是怎么完成的,在 spi-imx.c 文件中找到如下所示内容:

  1. 152 #define MXC_SPI_BUF_TX(type) \
  2. 153 static void spi_imx_buf_tx_##type(struct spi_imx_data *spi_imx) \
  3. 154 { \
  4. 155 type val = 0; \
  5. 156 \
  6. 157 if (spi_imx->tx_buf) { \
  7. 158 val = *(type *)spi_imx->tx_buf; \
  8. 159 spi_imx->tx_buf += sizeof(type); \
  9. 160 } \
  10. 161 \
  11. 162 spi_imx->count -= sizeof(type); \
  12. 163 \
  13. 164 writel(val, spi_imx->base + MXC_CSPITXDATA); \
  14. 165 }
  15. 166
  16. 167 MXC_SPI_BUF_RX(u8)
  17. 168 MXC_SPI_BUF_TX(u8)

从示例代码 62.2.3 可以看出,spi_imx_buf_tx_u8 函数是通过 MXC_SPI_BUF_TX 宏来实现 的。第 164 行就是将要发送的数据值写入到 ECSPI 的 TXDATA 寄存器里面去,这和我们 SPI 裸 机实验的方法一样。将第 168 行的 MXC_SPI_BUF_TX(u8)展开就是 spi_imx_buf_tx_u8 函数。 其他的 tx 和 rx 函数都是这样实现的,这里就不做介绍了。关于 I.MX6U 的主机驱动程序就讲 解到这里,基本套路和 I2C 的适配器驱动程序类似。

3:SPI 设备驱动编写流程

3.1:SPI 设备信息描述

3.1.1:IO 的 pinctrl 子节点创建与修改

首先肯定是根据所使用的 IO 来创建或修改 pinctrl 子节点,这个没什么好说的,唯独要注意的就是检查相应的 IO 有没有被其他的设备所使用,如果有的话需要将其删除掉

3.1.2:SPI 设备节点的创建与修改

采用设备树的情况下,SPI 设备信息描述就通过创建相应的设备子节点来完成,我们可以 打开 imx6qdl-sabresd.dtsi 这个设备树头文件,在此文件里面找到如下所示内容:

  1. 308 &ecspi1 {
  2. 309 fsl,spi-num-chipselects = <1>;
  3. 310 cs-gpios = <&gpio4 9 0>;
  4. 311 pinctrl-names = "default";
  5. 312 pinctrl-0 = <&pinctrl_ecspi1>;
  6. 313 status = "okay";
  7. 314
  8. 315 flash: m25p80@0 {
  9. 316 #address-cells = <1>;
  10. 317 #size-cells = <1>;
  11. 318 compatible = "st,m25p32";
  12. 319 spi-max-frequency = <20000000>;
  13. 320 reg = <0>;
  14. 321 };
  15. 322 };

示例代码 62.3.1.1 是 I.MX6Q 的一款板子上的一个 SPI 设备节点,在这个板子的 ECSPI 接 口上接了一个 m25p80,这是一个 SPI 接口的设备。

第 309 行,设置“fsl,spi-num-chipselects”属性为 1,表示只有一个设备。

第 310 行,设置“cs-gpios”属性,也就是片选信号为 GPIO4_IO09。

第 311 行,设置“pinctrl-names”属性,也就是 SPI 设备所使用的 IO 名字。

第 312 行,设置“pinctrl-0”属性,也就是所使用的 IO 对应的 pinctrl 节点。

第 313 行,将 ecspi1 节点的“status”属性改为“okay”。

第 315~320 行,ecspi1 下的 m25p80 设备信息,每一个 SPI 设备都采用一个子节点来描述 其设备信息。

第 315 行的“m25p80@0”后面的“0”表示 m25p80 的接到了 ECSPI 的通道 0 上。这个要根据自己的具体硬件来设置。

第 318 行,SPI 设备的 compatible 属性值,用于匹配设备驱动。

第 319 行,“spi-max-frequency”属性设置 SPI 控制器的最高频率,这个要根据所使用的 SPI 设备来设置,比如在这里将 SPI 控制器最高频率设置为 20MHz。

第 320 行,reg 属性设置 m25p80 这个设备所使用的 ECSPI 通道,和“m25p80@0”后面的 “0”一样。 我们一会在编写 ICM20608 的设备树节点信息的时候就参考示例代码 62.3.1.1 中的内容即 可。

3.2:SPI 设备数据收发处理流程

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

  1. 603 struct spi_transfer {
  2. 604 /* it's ok if tx_buf == rx_buf (right?)
  3. 605 * for MicroWire, one buffer must be null
  4. 606 * buffers must work with dma_*map_single() calls, unless
  5. 607 * spi_message.is_dma_mapped reports a pre-existing mapping
  6. 608 */
  7. 609 const void *tx_buf;
  8. 610 void *rx_buf;
  9. 611 unsigned len;
  10. 612
  11. 613 dma_addr_t tx_dma;
  12. 614 dma_addr_t rx_dma;
  13. 615 struct sg_table tx_sg;
  14. 616 struct sg_table rx_sg;
  15. 617
  16. 618 unsigned cs_change:1;
  17. 619 unsigned tx_nbits:3;
  18. 620 unsigned rx_nbits:3;
  19. 621 #define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */
  20. 622 #define SPI_NBITS_DUAL 0x02 /* 2bits transfer */
  21. 623 #define SPI_NBITS_QUAD 0x04 /* 4bits transfer */
  22. 624 u8 bits_per_word;
  23. 625 u16 delay_usecs;
  24. 626 u32 speed_hz;
  25. 627
  26. 628 struct list_head transfer_list;
  27. 629 };

第 609 行,tx_buf 保存着要发送的数据。(指针前面加了一个const表示指针指向一个常量,这个值不能改。也就是固定指向

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

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

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

  1. 660 struct spi_message {
  2. 661 struct list_head transfers;
  3. 662
  4. 663 struct spi_device *spi;
  5. 664
  6. 665 unsigned is_dma_mapped:1;
  7. ......
  8. 678 /* completion is reported through a callback */
  9. 679 void (*complete)(void *context);
  10. 680 void *context;
  11. 681 unsigned frame_length;
  12. 682 unsigned actual_length;
  13. 683 int status;
  14. 684
  15. 685 /* for optional use by whatever driver currently owns the
  16. 686 * spi_message ... between calls to spi_async and then later
  17. 687 * complete(), that's the spi_master controller driver.
  18. 688 */
  19. 689 struct list_head queue;
  20. 690 void *state;
  21. 691 };

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

void spi_message_init(struct spi_message *m)

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

m:要初始化的 spi_message。

返回值:无。

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

跟i2c的msg很像是吧,i2c要指定读还是写,从机地址,发送长度,读取或发送的地址

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,函数原型如下:

  1. 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 个字节的数据发送和接收的示例代码如下所示:

  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. struct spi_transfer t = {
  7. .tx_buf = buf,
  8. .len = len,
  9. };
  10. spi_message_init(&m); /* 初始化 spi_message */
  11. spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */
  12. ret = spi_sync(spi, &m); /* 同步传输 */
  13. return ret;
  14. }
  15. /* SPI 多字节接收 */
  16. static int spi_receive(struct spi_device *spi, u8 *buf, int len)
  17. {
  18. int ret;
  19. struct spi_message m;
  20. struct spi_transfer t = {
  21. .rx_buf = buf,
  22. .len = len,
  23. };
  24. spi_message_init(&m); /* 初始化 spi_message */
  25. spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */
  26. ret = spi_sync(spi, &m); /* 同步传输 */
  27. return ret;
  28. }

4:试验程序编写

其实mini也没有这个传感器,可以自己用杜邦线外扩一个,当然要在设备树内保证所用IO没被其他外设占用。片选的化就默认接地,然后就是那两根数据线,和一个VCC了。

原来学stm32的w25q64模块就可以接。i2c是那个ATc02好像,一个是flash一个是eeprom

4.1:修改设备树

4.1.1:添加 ICM20608 所使用的 IO

首先在 imx6ull-alientek-emmc.dts 文件中添加 ICM20608 所使用的 IO 信息,在 iomuxc 节点 中添加一个新的子节点来描述 ICM20608 所使用的 SPI 引脚,子节点名字为 pinctrl_ecspi3,节 点内容如下所示:

  1. 1 pinctrl_ecspi3: icm20608 {
  2. 2 fsl,pins = <
  3. 3 MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20 0x10b0 /* CS */
  4. 4 MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 0x10b1 /* SCLK */
  5. 5 MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO 0x10b1 /* MISO */
  6. 6 MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 0x10b1 /* MOSI */
  7. 7 >;
  8. 8 };

UART2_TX_DATA 这个 IO 是 ICM20608 的片选信号,这里我们并没有将其复用为 ECSPI3 的 SS0 信号,而是将其复用为了普通的 GPIO。因为我们需要自己控制片选信号,所以将其复 用为普通的 GPIO。

4.1.2:在 ecspi3 节点追加 icm20608 子节点

在 imx6ull-alientek-emmc.dts 文件中并没有任何向 ecspi3 节点追加内容的代码,这是因为 NXP 官方的 6ULL EVK 开发板上没有连接 SPI 设备。在 imx6ull-alientek-emmc.dts 文件最后面 加入如下所示内容:

  1. 1 &ecspi3 {
  2. 2 fsl,spi-num-chipselects = <1>;
  3. 3 cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;
  4. 4 pinctrl-names = "default";
  5. 5 pinctrl-0 = <&pinctrl_ecspi3>;
  6. 6 status = "okay";
  7. 7
  8. 8 spidev: icm20608@0 {
  9. 9 compatible = "alientek,icm20608";
  10. 10 spi-max-frequency = <8000000>;
  11. 11 reg = <0>;
  12. 12 };
  13. 13 };

第 2 行,设置当前片选数量为 1,因为就只接了一个 ICM20608。

第 3 行,一定要使用 “cs-gpios”属性来描述片选引脚,SPI 主机驱动就会控制片选引脚。

第 5 行,设置 IO 要使用的 pinctrl 子节点,也就是我们在示例代码 62.5.1.1 中新建的 pinctrl_ecspi3。

第 6 行,imx6ull.dtsi 文件中默认将 ecspi3 节点状态(status)设置为“disable”,这里我们要将 其改为“okay”。

第 8~12 行,icm20608 设备子节点,因为 icm20608 连接在 ECSPI3 的第 0 个通道上,因此 @后面为 0。第 9 行设置节点属性兼容值为“alientek,icm20608”,

第 10 行设置 SPI 最大时钟频 率为 8MHz,这是 ICM20608 的 SPI 接口所能支持的最大的时钟频率。

第 11 行,icm20608 连接 在通道 0 上,因此 reg 为 0。 imx6ull-alientek-emmc.dts 文件修改完成以后重新编译一下,得到新的 dtb 文件,并使用新 的 dtb 启动 Linux 系统。

4.2:编写 ICM20608 驱动

新建名为“22_spi”的文件夹,然后在 22_spi 文件夹里面创建 vscode 工程,工作区命名为 “spi”。工程创建好以后新建 icm20608.c 和 icm20608reg.h 这两个文件,icm20608.c 为 ICM20608 的驱动代码,icm20608reg.h 是 ICM20608 寄存器头文件。先在 icm20608reg.h 中定义好 ICM20608 的寄存器,输入如下内容(有省略,完整的内容请参考例程):

  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. /* ICM20608寄存器
  16. *复位后所有寄存器地址都为0,除了
  17. *Register 107(0X6B) Power Management 1 = 0x40
  18. *Register 117(0X75) WHO_AM_I = 0xAF或0xAE
  19. */
  20. /* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
  21. #define ICM20_SELF_TEST_X_GYRO 0x00
  22. #define ICM20_SELF_TEST_Y_GYRO 0x01
  23. #define ICM20_SELF_TEST_Z_GYRO 0x02
  24. #define ICM20_SELF_TEST_X_ACCEL 0x0D
  25. #define ICM20_SELF_TEST_Y_ACCEL 0x0E
  26. #define ICM20_SELF_TEST_Z_ACCEL 0x0F
  27. /* 陀螺仪静态偏移 */
  28. #define ICM20_XG_OFFS_USRH 0x13
  29. #define ICM20_XG_OFFS_USRL 0x14
  30. #define ICM20_YG_OFFS_USRH 0x15
  31. #define ICM20_YG_OFFS_USRL 0x16
  32. #define ICM20_ZG_OFFS_USRH 0x17
  33. #define ICM20_ZG_OFFS_USRL 0x18
  34. #define ICM20_SMPLRT_DIV 0x19
  35. #define ICM20_CONFIG 0x1A
  36. #define ICM20_GYRO_CONFIG 0x1B
  37. #define ICM20_ACCEL_CONFIG 0x1C
  38. #define ICM20_ACCEL_CONFIG2 0x1D
  39. #define ICM20_LP_MODE_CFG 0x1E
  40. #define ICM20_ACCEL_WOM_THR 0x1F
  41. #define ICM20_FIFO_EN 0x23
  42. #define ICM20_FSYNC_INT 0x36
  43. #define ICM20_INT_PIN_CFG 0x37
  44. #define ICM20_INT_ENABLE 0x38
  45. #define ICM20_INT_STATUS 0x3A
  46. /* 加速度输出 */
  47. #define ICM20_ACCEL_XOUT_H 0x3B
  48. #define ICM20_ACCEL_XOUT_L 0x3C
  49. #define ICM20_ACCEL_YOUT_H 0x3D
  50. #define ICM20_ACCEL_YOUT_L 0x3E
  51. #define ICM20_ACCEL_ZOUT_H 0x3F
  52. #define ICM20_ACCEL_ZOUT_L 0x40
  53. /* 温度输出 */
  54. #define ICM20_TEMP_OUT_H 0x41
  55. #define ICM20_TEMP_OUT_L 0x42
  56. /* 陀螺仪输出 */
  57. #define ICM20_GYRO_XOUT_H 0x43
  58. #define ICM20_GYRO_XOUT_L 0x44
  59. #define ICM20_GYRO_YOUT_H 0x45
  60. #define ICM20_GYRO_YOUT_L 0x46
  61. #define ICM20_GYRO_ZOUT_H 0x47
  62. #define ICM20_GYRO_ZOUT_L 0x48
  63. #define ICM20_SIGNAL_PATH_RESET 0x68
  64. #define ICM20_ACCEL_INTEL_CTRL 0x69
  65. #define ICM20_USER_CTRL 0x6A
  66. #define ICM20_PWR_MGMT_1 0x6B
  67. #define ICM20_PWR_MGMT_2 0x6C
  68. #define ICM20_FIFO_COUNTH 0x72
  69. #define ICM20_FIFO_COUNTL 0x73
  70. #define ICM20_FIFO_R_W 0x74
  71. #define ICM20_WHO_AM_I 0x75
  72. /* 加速度静态偏移 */
  73. #define ICM20_XA_OFFSET_H 0x77
  74. #define ICM20_XA_OFFSET_L 0x78
  75. #define ICM20_YA_OFFSET_H 0x7A
  76. #define ICM20_YA_OFFSET_L 0x7B
  77. #define ICM20_ZA_OFFSET_H 0x7D
  78. #define ICM20_ZA_OFFSET_L 0x7E
  79. #endif

接下来继续编写 icm20608.c 文件,因为 icm20608.c 文件内容比较长,因此这里就将其分开 来讲解。

4.2.1:icm20608 设备结构体创建

首先创建一个 icm20608 设备结构体,如下所示:

  1. #include <linux/types.h>
  2. #include <linux/kernel.h>
  3. #include <linux/delay.h>
  4. #include <linux/ide.h>
  5. #include <linux/init.h>
  6. #include <linux/module.h>
  7. #include <linux/errno.h>
  8. #include <linux/gpio.h>
  9. #include <linux/cdev.h>
  10. #include <linux/device.h>
  11. #include <linux/of_gpio.h>
  12. #include <linux/semaphore.h>
  13. #include <linux/timer.h>
  14. #include <linux/i2c.h>
  15. #include <linux/spi/spi.h>
  16. #include <linux/of.h>
  17. #include <linux/of_address.h>
  18. #include <linux/of_gpio.h>
  19. #include <linux/platform_device.h>
  20. #include <asm/mach/map.h>
  21. #include <asm/uaccess.h>
  22. #include <asm/io.h>
  23. #include "icm20608reg.h"
  24. /***************************************************************
  25. Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
  26. 文件名 : icm20608.c
  27. 作者 : 左忠凯
  28. 版本 : V1.0
  29. 描述 : ICM20608 SPI驱动程序
  30. 其他 : 无
  31. 论坛 : www.openedv.com
  32. 日志 : 初版V1.0 2019/9/2 左忠凯创建
  33. ***************************************************************/
  34. #define ICM20608_CNT 1
  35. #define ICM20608_NAME "icm20608"
  36. struct icm20608_dev {
  37. dev_t devid; /* 设备号 */
  38. struct cdev cdev; /* cdev */
  39. struct class *class; /* 类 */
  40. struct device *device; /* 设备 */
  41. struct device_node *nd; /* 设备节点 */
  42. int major; /* 主设备号 */
  43. void *private_data; /* 私有数据 */
  44. signed int gyro_x_adc; /* 陀螺仪X轴原始值 */
  45. signed int gyro_y_adc; /* 陀螺仪Y轴原始值 */
  46. signed int gyro_z_adc; /* 陀螺仪Z轴原始值 */
  47. signed int accel_x_adc; /* 加速度计X轴原始值 */
  48. signed int accel_y_adc; /* 加速度计Y轴原始值 */
  49. signed int accel_z_adc; /* 加速度计Z轴原始值 */
  50. signed int temp_adc; /* 温度原始值 */
  51. };
  52. static struct icm20608_dev icm20608dev;

icm20608 的设备结构体 icm20608_dev 没什么好讲的,重点看一下第 44 行的 private_data, 对于 SPI 设备驱动来讲最核心的就是 spi_device。probe 函数会向驱动提供当前 SPI 设备对应的 spi_device,因此在 probe 函数中设置 private_data 为 probe 函数传递进来的 spi_device 参数。

4.2.2:icm20608 的 spi_driver 注册与注销

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

  1. /* 传统匹配方式ID列表 */
  2. static const struct spi_device_id icm20608_id[] = {
  3. {"alientek,icm20608", 0},
  4. {}
  5. };
  6. /* 设备树匹配列表 */
  7. static const struct of_device_id icm20608_of_match[] = {
  8. { .compatible = "alientek,icm20608" },
  9. { /* Sentinel */ }
  10. };
  11. /* SPI驱动结构体 */
  12. static struct spi_driver icm20608_driver = {
  13. .probe = icm20608_probe,
  14. .remove = icm20608_remove,
  15. .driver = {
  16. .owner = THIS_MODULE,
  17. .name = "icm20608",
  18. .of_match_table = icm20608_of_match,
  19. },
  20. .id_table = icm20608_id,
  21. };
  22. /*
  23. * @description : 驱动入口函数
  24. * @param : 无
  25. * @return : 无
  26. */
  27. static int __init icm20608_init(void)
  28. {
  29. return spi_register_driver(&icm20608_driver);
  30. }
  31. /*
  32. * @description : 驱动出口函数
  33. * @param : 无
  34. * @return : 无
  35. */
  36. static void __exit icm20608_exit(void)
  37. {
  38. spi_unregister_driver(&icm20608_driver);
  39. }
  40. module_init(icm20608_init);
  41. module_exit(icm20608_exit);
  42. MODULE_LICENSE("GPL");
  43. MODULE_AUTHOR("zuozhongkai");

第 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。

4.2.3:probe&remove 函数

icm20608_driver 中的 probe 和 remove 函数内容如下所示:

  1. /*
  2. * @description : spi驱动的probe函数,当驱动与
  3. * 设备匹配以后此函数就会执行
  4. * @param - client : i2c设备
  5. * @param - id : i2c设备ID
  6. *
  7. */
  8. static int icm20608_probe(struct spi_device *spi)
  9. {
  10. /* 1、构建设备号 */
  11. if (icm20608dev.major) {
  12. icm20608dev.devid = MKDEV(icm20608dev.major, 0);
  13. register_chrdev_region(icm20608dev.devid, ICM20608_CNT, ICM20608_NAME);
  14. } else {
  15. alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT, ICM20608_NAME);
  16. icm20608dev.major = MAJOR(icm20608dev.devid);
  17. }
  18. /* 2、注册设备 */
  19. cdev_init(&icm20608dev.cdev, &icm20608_ops);
  20. cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);
  21. /* 3、创建类 */
  22. icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);
  23. if (IS_ERR(icm20608dev.class)) {
  24. return PTR_ERR(icm20608dev.class);
  25. }
  26. /* 4、创建设备 */
  27. icm20608dev.device = device_create(icm20608dev.class, NULL, icm20608dev.devid, NULL, ICM20608_NAME);
  28. if (IS_ERR(icm20608dev.device)) {
  29. return PTR_ERR(icm20608dev.device);
  30. }
  31. /*初始化spi_device */
  32. spi->mode = SPI_MODE_0; /*MODE0,CPOL=0,CPHA=0*/
  33. spi_setup(spi);
  34. icm20608dev.private_data = spi; /* 设置私有数据 */
  35. /* 初始化ICM20608内部寄存器 */
  36. icm20608_reginit();
  37. return 0;
  38. }
  39. /*
  40. * @description : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行
  41. * @param - client : i2c设备
  42. * @return : 0,成功;其他负值,失败
  43. */
  44. static int icm20608_remove(struct spi_device *spi)
  45. {
  46. /* 删除设备 */
  47. cdev_del(&icm20608dev.cdev);
  48. unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);
  49. /* 注销掉类和设备 */
  50. device_destroy(icm20608dev.class, icm20608dev.devid);
  51. class_destroy(icm20608dev.class);
  52. return 0;
  53. }

第 8~43 行,probe 函数,当设备与驱动匹配成功以后此函数就会执行,第 10~33 行都是标 准的注册字符设备驱动

第 36 行,设置 SPI 为模式 0,也就是 CPOL=0,CPHA=0。(空闲状态低电平,第一个前沿采样(也就是上升沿)

第 37 行,设置好 spi_device 以后需要使用 spi_setup 配置一下。 第 38 行,设置 icm20608dev 的 private_data 成员变量为 spi_device。

第 41 行,调用 icm20608_reginit 函数初始化 ICM20608,主要是初始化 ICM20608 指定寄 存器。

第 50~60 行,icm20608_remove 函数,注销驱动的时候此函数就会执行。

4.2.4:icm20608 寄存器读写与初始化

  1. /*
  2. * @description : 从icm20608读取多个寄存器数据
  3. * @param - dev: icm20608设备
  4. * @param - reg: 要读取的寄存器首地址
  5. * @param - val: 读取到的数据
  6. * @param - len: 要读取的数据长度
  7. * @return : 操作结果
  8. */
  9. static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
  10. {
  11. int ret = -1;
  12. unsigned char txdata[1];
  13. unsigned char * rxdata;
  14. struct spi_message m;
  15. struct spi_transfer *t;
  16. struct spi_device *spi = (struct spi_device *)dev->private_data;
  17. t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); /* 申请内存 */
  18. if(!t) {
  19. return -ENOMEM;
  20. }
  21. rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL); /* 申请内存 */
  22. if(!rxdata) {
  23. goto out1;
  24. }
  25. /* 一共发送len+1个字节的数据,第一个字节为
  26. 寄存器首地址,一共要读取len个字节长度的数据,*/
  27. txdata[0] = reg | 0x80; /* 写数据的时候首寄存器地址bit8要置1 */
  28. t->tx_buf = txdata; /* 要发送的数据 */
  29. t->rx_buf = rxdata; /* 要读取的数据 */
  30. t->len = len+1; /* t->len=发送的长度+读取的长度 */
  31. spi_message_init(&m); /* 初始化spi_message */
  32. spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
  33. ret = spi_sync(spi, &m); /* 同步发送 */
  34. if(ret) {
  35. goto out2;
  36. }
  37. memcpy(buf , rxdata+1, len); /* 只需要读取的数据 */
  38. out2:
  39. kfree(rxdata); /* 释放内存 */
  40. out1:
  41. kfree(t); /* 释放内存 */
  42. return ret;
  43. }
  44. /*
  45. * @description : 向icm20608多个寄存器写入数据
  46. * @param - dev: icm20608设备
  47. * @param - reg: 要写入的寄存器首地址
  48. * @param - val: 要写入的数据缓冲区
  49. * @param - len: 要写入的数据长度
  50. * @return : 操作结果
  51. */
  52. static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len)
  53. {
  54. int ret = -1;
  55. unsigned char *txdata;
  56. struct spi_message m;
  57. struct spi_transfer *t;
  58. struct spi_device *spi = (struct spi_device *)dev->private_data;
  59. t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); /* 申请内存 */
  60. if(!t) {
  61. return -ENOMEM;
  62. }
  63. txdata = kzalloc(sizeof(char)*len, GFP_KERNEL);
  64. if(!txdata) {
  65. goto out1;
  66. }
  67. /* 一共发送len+1个字节的数据,第一个字节为
  68. 寄存器首地址,len为要写入的寄存器的集合,*/
  69. *txdata = reg & ~0x80; /* 写数据的时候首寄存器地址bit8要清零 */
  70. memcpy(txdata+1, buf, len); /* 把len个寄存器拷贝到txdata里,等待发送 */
  71. t->tx_buf = txdata; /* 要发送的数据 */
  72. t->len = len+1; /* t->len=发送的长度+读取的长度 */
  73. spi_message_init(&m); /* 初始化spi_message */
  74. spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
  75. ret = spi_sync(spi, &m); /* 同步发送 */
  76. if(ret) {
  77. goto out2;
  78. }
  79. out2:
  80. kfree(txdata); /* 释放内存 */
  81. out1:
  82. kfree(t); /* 释放内存 */
  83. return ret;
  84. }
  85. /*
  86. * @description : 读取icm20608指定寄存器值,读取一个寄存器
  87. * @param - dev: icm20608设备
  88. * @param - reg: 要读取的寄存器
  89. * @return : 读取到的寄存器值
  90. */
  91. static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
  92. {
  93. u8 data = 0;
  94. icm20608_read_regs(dev, reg, &data, 1);
  95. return data;
  96. }
  97. /*
  98. * @description : 向icm20608指定寄存器写入指定的值,写一个寄存器
  99. * @param - dev: icm20608设备
  100. * @param - reg: 要写的寄存器
  101. * @param - data: 要写入的值
  102. * @return : 无
  103. */
  104. static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
  105. {
  106. u8 buf = value;
  107. icm20608_write_regs(dev, reg, &buf, 1);
  108. }
  109. /*
  110. * @description : 读取ICM20608的数据,读取原始数据,包括三轴陀螺仪、
  111. * : 三轴加速度计和内部温度。
  112. * @param - dev : ICM20608设备
  113. * @return : 无。
  114. */
  115. void icm20608_readdata(struct icm20608_dev *dev)
  116. {
  117. unsigned char data[14] = { 0 };
  118. icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);
  119. dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]);
  120. dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]);
  121. dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]);
  122. dev->temp_adc = (signed short)((data[6] << 8) | data[7]);
  123. dev->gyro_x_adc = (signed short)((data[8] << 8) | data[9]);
  124. dev->gyro_y_adc = (signed short)((data[10] << 8) | data[11]);
  125. dev->gyro_z_adc = (signed short)((data[12] << 8) | data[13]);
  126. }

第 9~50 行,icm20608_read_regs 函数,从 icm20608 中读取连续多个寄存器数据;注意: 在本实验中,SPI 为全双工通讯没有所谓的发送和接收长度之分。要读取或者发送 N 个字节就 要封装 N+1 个字节,第 1 个字节是告诉设备我们要进行读还是写,后面的 N 个字节才是我们 要读或者发送的数据。因为是读操作,因此在第 31 行设置第一个数据 bit7 位 1,表示读操作。

第 60~96 行,icm20608_write_regs 函数,向 icm20608 连续写入多个寄存器数据。此函数和 icm20608_read_regs 函数区别不大。

第 104~109 行,icm20608_read_onereg 函数,读取 icm20608 指定寄存器数据。

第 119~123 行,icm20608_write_onereg 函数,向 icm20608 指定寄存器写入数据。

第 131~143 行,icm20608_readdata 函数,读取 icm20608 六轴传感器和温度传感器原始数 据值,应用程序读取 icm20608 的时候这些传感器原始数据就会上报给应用程序。

第 150~170 行,icm20608_reginit 函数,初始化 icm20608,和我们 spi 裸机实验里面的初始 化过程一样。

4.2.5:字符设备驱动框架

icm20608 的字符设备驱动框架如下:

  1. /*
  2. * @description : 打开设备
  3. * @param - inode : 传递给驱动的inode
  4. * @param - filp : 设备文件,file结构体有个叫做pr似有ate_data的成员变量
  5. * 一般在open的时候将private_data似有向设备结构体。
  6. * @return : 0 成功;其他 失败
  7. */
  8. static int icm20608_open(struct inode *inode, struct file *filp)
  9. {
  10. filp->private_data = &icm20608dev; /* 设置私有数据 */
  11. return 0;
  12. }
  13. /*
  14. * @description : 从设备读取数据
  15. * @param - filp : 要打开的设备文件(文件描述符)
  16. * @param - buf : 返回给用户空间的数据缓冲区
  17. * @param - cnt : 要读取的数据长度
  18. * @param - offt : 相对于文件首地址的偏移
  19. * @return : 读取的字节数,如果为负值,表示读取失败
  20. */
  21. static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
  22. {
  23. signed int data[7];
  24. long err = 0;
  25. struct icm20608_dev *dev = (struct icm20608_dev *)filp->private_data;
  26. icm20608_readdata(dev);
  27. data[0] = dev->gyro_x_adc;
  28. data[1] = dev->gyro_y_adc;
  29. data[2] = dev->gyro_z_adc;
  30. data[3] = dev->accel_x_adc;
  31. data[4] = dev->accel_y_adc;
  32. data[5] = dev->accel_z_adc;
  33. data[6] = dev->temp_adc;
  34. err = copy_to_user(buf, data, sizeof(data));
  35. return 0;
  36. }
  37. /*
  38. * @description : 关闭/释放设备
  39. * @param - filp : 要关闭的设备文件(文件描述符)
  40. * @return : 0 成功;其他 失败
  41. */
  42. static int icm20608_release(struct inode *inode, struct file *filp)
  43. {
  44. return 0;
  45. }
  46. /* icm20608操作函数 */
  47. static const struct file_operations icm20608_ops = {
  48. .owner = THIS_MODULE,
  49. .open = icm20608_open,
  50. .read = icm20608_read,
  51. .release = icm20608_release,
  52. };

字符设备驱动框架没什么好说的,重点是第 22~38 行的 icm20608_read 函数,当应用程序 调用 read 函数读取 icm20608 设备文件的时候此函数就会执行。此函数调用上面编写好的 icm20608_readdata 函数读取 icm20608 的原始数据并将其上报给应用程序。大家注意,在内核 中尽量不要使用浮点运算,所以不要在驱动将 icm20608 的原始值转换为对应的实际值,因为会 涉及到浮点计算。

4.3:编写测试 APP

新建 icm20608App.c 文件,然后在里面输入如下所示内容:

  1. #include "stdio.h"
  2. #include "unistd.h"
  3. #include "sys/types.h"
  4. #include "sys/stat.h"
  5. #include "sys/ioctl.h"
  6. #include "fcntl.h"
  7. #include "stdlib.h"
  8. #include "string.h"
  9. #include <poll.h>
  10. #include <sys/select.h>
  11. #include <sys/time.h>
  12. #include <signal.h>
  13. #include <fcntl.h>
  14. /***************************************************************
  15. Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
  16. 文件名 : icm20608App.c
  17. 作者 : 左忠凯
  18. 版本 : V1.0
  19. 描述 : icm20608设备测试APP。
  20. 其他 : 无
  21. 使用方法 :./icm20608App /dev/icm20608
  22. 论坛 : www.openedv.com
  23. 日志 : 初版V1.0 2019/9/20 左忠凯创建
  24. ***************************************************************/
  25. /*
  26. * @description : main主程序
  27. * @param - argc : argv数组元素个数
  28. * @param - argv : 具体参数
  29. * @return : 0 成功;其他 失败
  30. */
  31. int main(int argc, char *argv[])
  32. {
  33. int fd;
  34. char *filename;
  35. signed int databuf[7];
  36. unsigned char data[14];
  37. signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;
  38. signed int accel_x_adc, accel_y_adc, accel_z_adc;
  39. signed int temp_adc;
  40. float gyro_x_act, gyro_y_act, gyro_z_act;
  41. float accel_x_act, accel_y_act, accel_z_act;
  42. float temp_act;
  43. int ret = 0;
  44. if (argc != 2) {
  45. printf("Error Usage!\r\n");
  46. return -1;
  47. }
  48. filename = argv[1];
  49. fd = open(filename, O_RDWR);
  50. if(fd < 0) {
  51. printf("can't open file %s\r\n", filename);
  52. return -1;
  53. }
  54. while (1) {
  55. ret = read(fd, databuf, sizeof(databuf));
  56. if(ret == 0) { /* 数据读取成功 */
  57. gyro_x_adc = databuf[0];
  58. gyro_y_adc = databuf[1];
  59. gyro_z_adc = databuf[2];
  60. accel_x_adc = databuf[3];
  61. accel_y_adc = databuf[4];
  62. accel_z_adc = databuf[5];
  63. temp_adc = databuf[6];
  64. /* 计算实际值 */
  65. gyro_x_act = (float)(gyro_x_adc) / 16.4;
  66. gyro_y_act = (float)(gyro_y_adc) / 16.4;
  67. gyro_z_act = (float)(gyro_z_adc) / 16.4;
  68. accel_x_act = (float)(accel_x_adc) / 2048;
  69. accel_y_act = (float)(accel_y_adc) / 2048;
  70. accel_z_act = (float)(accel_z_adc) / 2048;
  71. temp_act = ((float)(temp_adc) - 25 ) / 326.8 + 25;
  72. printf("\r\n原始值:\r\n");
  73. printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc, gyro_y_adc, gyro_z_adc);
  74. printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc, accel_y_adc, accel_z_adc);
  75. printf("temp = %d\r\n", temp_adc);
  76. printf("实际值:");
  77. printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", gyro_x_act, gyro_y_act, gyro_z_act);
  78. printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", accel_x_act, accel_y_act, accel_z_act);
  79. printf("act temp = %.2f°C\r\n", temp_act);
  80. }
  81. usleep(100000); /*100ms */
  82. }
  83. close(fd); /* 关闭文件 */
  84. return 0;
  85. }

第 60~91 行,在 while 循环中每隔 100ms 从 icm20608 中读取一次数据,读取到 icm20608 原始数据以后将其转换为实际值,比如陀螺仪就是角速度、加速度计就是 g 值。注意,我们在 icm20608 驱动中将陀螺仪和加速度计的测量范围全部设置到了最大,分别为±2000 和±16g。 因此,在计算实际值的时候陀螺仪使用 16.4,加速度计使用 2048。最终将传感器原始数据和得 到的实际值显示在终端上。

5:运行测试

5.1:编写 Makefile 文件

编写 Makefile 文件,本章实验的 Makefile 文件和第四十章实验基本一样,只是将 obj-m 变 量的值改为“icm20608.o”,Makefile 内容如下所示:

  1. KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
  2. CURRENT_PATH := $(shell pwd)
  3. obj-m := icm20608.o
  4. build: kernel_modules
  5. kernel_modules:
  6. $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
  7. clean:
  8. $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

第 4 行,设置 obj-m 变量的值为“icm20608.o”。

5.2:编译测试 APP

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

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

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

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

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

arm-linux-gnueabihf-readelf -A icm20608App

结果如图 62.6.1.1 所示:

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

6:运行测试

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

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

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

./icm20608App /dev/icm20608

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

本文仅在记录学习正点原子imx6ull-mini开发板的过程,不做他用。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/煮酒与君饮/article/detail/948878
推荐阅读
相关标签
  

闽ICP备14008679号