正点原子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 文件 中,内容如下(有缩减):
315 struct spi_master {
316 struct device dev;
317
318 struct list_head list;
......
326 s16 bus_num;
327
328 /* chipselects will be integral to many controllers; some others
329 * might use board-specific GPIOs.
330 */
331 u16 num_chipselect;
332
333 /* some SPI controllers pose alignment requirements on DMAable
334 * buffers; let protocol drivers know about these requirements.
335 */
336 u16 dma_alignment;
337
338 /* spi_device.mode flags understood by this controller driver */
339 u16 mode_bits;
340
341 /* bitmask of supported bits_per_word for transfers */
342 u32 bits_per_word_mask;
......
347 /* limits on transfer speed */
348 u32 min_speed_hz;
349 u32 max_speed_hz;
350
351 /* other constraints relevant to this driver */
352 u16 flags;
......
359 /* lock and mutex for SPI bus locking */
360 spinlock_t bus_lock_spinlock;
361 struct mutex bus_lock_mutex;
362
363 /* flag indicating that the SPI bus is locked for exclusive use */
364 bool bus_lock_flag;
......
372 int (*setup)(struct spi_device *spi);
373
......
393 int (*transfer)(struct spi_device *spi,
394 struct spi_message *mesg);
......
434 int (*transfer_one_message)(struct spi_master *master,
435 struct spi_message *mesg);
......
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,函数原型如下:
struct spi_master *spi_alloc_master(struct device *dev,
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 文件中,结构体内容如下:
180 struct spi_driver {
181 const struct spi_device_id *id_table;
182 int (*probe)(struct spi_device *spi);
183 int (*remove)(struct spi_device *spi);
184 void (*shutdown)(struct spi_device *spi);
185 struct device_driver driver;
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 的注销,函数原型如下:
void spi_unregister_driver(struct spi_driver *sdrv)
函数参数和返回值含义如下:
sdrv:要注销的 spi_driver。
返回值:无。 spi_driver 注册示例程序如下:
1 /* probe 函数 */
2 static int xxx_probe(struct spi_device *spi)
3 {
4 /* 具体函数内容 */
5 return 0;
6 }
7
8 /* remove 函数 */
9 static int xxx_remove(struct spi_device *spi)
10 {
11 /* 具体函数内容 */
12 return 0;
13 }
14 /* 传统匹配方式 ID 列表 */
15 static const struct spi_device_id xxx_id[] = {
16 {"xxx", 0},
17 {}
18 };
19
20 /* 设备树匹配列表 */
21 static const struct of_device_id xxx_of_match[] = {
22 { .compatible = "xxx" },
23 { /* Sentinel */ }
24 };
25
26 /* SPI 驱动结构体 */
27 static struct spi_driver xxx_driver = {
28 .probe = xxx_probe,
29 .remove = xxx_remove,
30 .driver = {
31 .owner = THIS_MODULE,
32 .name = "xxx",
33 .of_match_table = xxx_of_match,
34 },
35 .id_table = xxx_id,
36 };
37
38 /* 驱动入口函数 */
39 static int __init xxx_init(void)
40 {
41 return spi_register_driver(&xxx_driver);
42 }
43
44 /* 驱动出口函数 */
45 static void __exit xxx_exit(void)
46 {
47 spi_unregister_driver(&xxx_driver);
48 }
49
50 module_init(xxx_init);
51 module_exit(xxx_exit);
第 1~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 文件中,内容如下:
131 struct bus_type spi_bus_type = {
132 .name = "spi",
133 .dev_groups = spi_dev_groups,
134 .match = spi_match_device,
135 .uevent = spi_uevent,
136 };
可以看出,SPI 设备和驱动的匹配函数为 spi_match_device,函数内容如下:
99 static int spi_match_device(struct device *dev,
struct device_driver *drv)
100 {
101 const struct spi_device *spi = to_spi_device(dev);
102 const struct spi_driver *sdrv = to_spi_driver(drv);
103
104 /* Attempt an OF style match */
105 if (of_driver_match_device(dev, drv))
106 return 1;
107
108 /* Then try ACPI */
109 if (acpi_driver_match_device(dev, drv))
110 return 1;
111
112 if (sdrv->id_table)
113 return !!spi_match_id(sdrv->id_table, spi);
114
115 return strcmp(spi->modalias, drv->name) == 0;
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 ecspi3: ecspi@02010000 {
2 #address-cells = <1>;
3 #size-cells = <0>;
4 compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
5 reg = <0x02010000 0x4000>;
6 interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
7 clocks = <&clks IMX6UL_CLK_ECSPI3>,
8 <&clks IMX6UL_CLK_ECSPI3>;
9 clock-names = "ipg", "per";
10 dmas = <&sdma 7 7 1>, <&sdma 8 7 2>;
11 dma-names = "rx", "tx";
12 status = "disabled";
13 };
重点来看一下第 4 行的 compatible 属性值,compatible 属性有两个值“fsl,imx6ul-ecspi”和 “fsl,imx51-ecspi”,在 Linux 内核源码中搜素这两个属性值即可找到 I.MX6U 对应的 ECSPI(SPI) 主机驱动。I.MX6U 的 ECSPI 主机驱动文件为 drivers/spi/spi-imx.c,在此文件中找到如下内容:
694 static struct platform_device_id spi_imx_devtype[] = {
695 {
696 .name = "imx1-cspi",
697 .driver_data = (kernel_ulong_t) &imx1_cspi_devtype_data,
698 }, {
699 .name = "imx21-cspi",
700 .driver_data = (kernel_ulong_t) &imx21_cspi_devtype_data,
......
713 }, {
714 .name = "imx6ul-ecspi",
715 .driver_data = (kernel_ulong_t) &imx6ul_ecspi_devtype_data,
716 }, {
717 /* sentinel */
718 }
719 };
720
721 static const struct of_device_id spi_imx_dt_ids[] = {
722 { .compatible = "fsl,imx1-cspi", .data =
&imx1_cspi_devtype_data, },
......
728 { .compatible = "fsl,imx6ul-ecspi", .data =
&imx6ul_ecspi_devtype_data, },
729 { /* sentinel */ }
730 };
731 MODULE_DEVICE_TABLE(of, spi_imx_dt_ids);
......
1338 static struct platform_driver spi_imx_driver = {
1339 .driver = {
1340 .name = DRIVER_NAME,
1341 .of_match_table = spi_imx_dt_ids,
1342 .pm = IMX_SPI_PM,
1343 },
1344 .id_table = spi_imx_devtype,
1345 .probe = spi_imx_probe,
1346 .remove = spi_imx_remove,
1347 };
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 数据发送:
spi_imx_transfer-> spi_imx_pio_transfer-> spi_imx_push-> 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 位的发送函数,如下所示:
spi_imx_buf_tx_u8
spi_imx_buf_tx_u16
spi_imx_buf_tx_u32
同理,也有 8 位、16 位和 32 位的数据接收函数,如下所示:
spi_imx_buf_rx_u8
spi_imx_buf_rx_u16
spi_imx_buf_rx_u32
我们就以 spi_imx_buf_tx_u8 这个函数为例,看看,一个自己的数据发送是怎么完成的,在 spi-imx.c 文件中找到如下所示内容:
152 #define MXC_SPI_BUF_TX(type) \
153 static void spi_imx_buf_tx_##type(struct spi_imx_data *spi_imx) \
154 { \
155 type val = 0; \
156 \
157 if (spi_imx->tx_buf) { \
158 val = *(type *)spi_imx->tx_buf; \
159 spi_imx->tx_buf += sizeof(type); \
160 } \
161 \
162 spi_imx->count -= sizeof(type); \
163 \
164 writel(val, spi_imx->base + MXC_CSPITXDATA); \
165 }
166
167 MXC_SPI_BUF_RX(u8)
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 这个设备树头文件,在此文件里面找到如下所示内容:
308 &ecspi1 {
309 fsl,spi-num-chipselects = <1>;
310 cs-gpios = <&gpio4 9 0>;
311 pinctrl-names = "default";
312 pinctrl-0 = <&pinctrl_ecspi1>;
313 status = "okay";
314
315 flash: m25p80@0 {
316 #address-cells = <1>;
317 #size-cells = <1>;
318 compatible = "st,m25p32";
319 spi-max-frequency = <20000000>;
320 reg = <0>;
321 };
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 传输信息,结构体内容如下
603 struct spi_transfer {
604 /* it's ok if tx_buf == rx_buf (right?)
605 * for MicroWire, one buffer must be null
606 * buffers must work with dma_*map_single() calls, unless
607 * spi_message.is_dma_mapped reports a pre-existing mapping
608 */
609 const void *tx_buf;
610 void *rx_buf;
611 unsigned len;
612
613 dma_addr_t tx_dma;
614 dma_addr_t rx_dma;
615 struct sg_table tx_sg;
616 struct sg_table rx_sg;
617
618 unsigned cs_change:1;
619 unsigned tx_nbits:3;
620 unsigned rx_nbits:3;
621 #define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */
622 #define SPI_NBITS_DUAL 0x02 /* 2bits transfer */
623 #define SPI_NBITS_QUAD 0x04 /* 4bits transfer */
624 u8 bits_per_word;
625 u16 delay_usecs;
626 u32 speed_hz;
627
628 struct list_head transfer_list;
629 };
第 609 行,tx_buf 保存着要发送的数据。(指针前面加了一个const表示指针指向一个常量,这个值不能改。也就是固定指向)
第 610 行,rx_buf 用于保存接收到的数据。
第 611 行,len 是要进行传输的数据长度,SPI 是全双工通信,因此在一次通信中发送和 接收的字节数都是一样的,所以 spi_transfer 中也就没有发送长度和接收长度之分。
spi_transfer 需要组织成 spi_message,spi_message 也是一个结构体,内容如下:
660 struct spi_message {
661 struct list_head transfers;
662
663 struct spi_device *spi;
664
665 unsigned is_dma_mapped:1;
......
678 /* completion is reported through a callback */
679 void (*complete)(void *context);
680 void *context;
681 unsigned frame_length;
682 unsigned actual_length;
683 int status;
684
685 /* for optional use by whatever driver currently owns the
686 * spi_message ... between calls to spi_async and then later
687 * complete(), that's the spi_master controller driver.
688 */
689 struct list_head queue;
690 void *state;
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,函数原型如下:
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 个字节的数据发送和接收的示例代码如下所示:
/* SPI 多字节发送 */
static int spi_send(struct spi_device *spi, u8 *buf, int len)
{int ret;struct spi_message m;struct spi_transfer t = {.tx_buf = buf,.len = len,};spi_message_init(&m); /* 初始化 spi_message */spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */ret = spi_sync(spi, &m); /* 同步传输 */return ret;
}
/* SPI 多字节接收 */
static int spi_receive(struct spi_device *spi, u8 *buf, int len)
{int ret;struct spi_message m;struct spi_transfer t = {.rx_buf = buf,.len = len,};spi_message_init(&m); /* 初始化 spi_message */spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */ret = spi_sync(spi, &m); /* 同步传输 */return ret;
}
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 pinctrl_ecspi3: icm20608 {
2 fsl,pins = <
3 MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20 0x10b0 /* CS */
4 MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 0x10b1 /* SCLK */
5 MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO 0x10b1 /* MISO */
6 MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 0x10b1 /* MOSI */
7 >;
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 &ecspi3 {
2 fsl,spi-num-chipselects = <1>;
3 cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;
4 pinctrl-names = "default";
5 pinctrl-0 = <&pinctrl_ecspi3>;
6 status = "okay";
7
8 spidev: icm20608@0 {
9 compatible = "alientek,icm20608";
10 spi-max-frequency = <8000000>;
11 reg = <0>;
12 };
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 的寄存器,输入如下内容(有省略,完整的内容请参考例程):
#ifndef ICM20608_H
#define ICM20608_H
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : icm20608reg.h
作者 : 左忠凯
版本 : V1.0
描述 : ICM20608寄存器地址描述头文件
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2019/9/2 左忠凯创建
***************************************************************/
#define ICM20608G_ID 0XAF /* ID值 */
#define ICM20608D_ID 0XAE /* ID值 *//* ICM20608寄存器 *复位后所有寄存器地址都为0,除了*Register 107(0X6B) Power Management 1 = 0x40*Register 117(0X75) WHO_AM_I = 0xAF或0xAE*/
/* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
#define ICM20_SELF_TEST_X_GYRO 0x00
#define ICM20_SELF_TEST_Y_GYRO 0x01
#define ICM20_SELF_TEST_Z_GYRO 0x02
#define ICM20_SELF_TEST_X_ACCEL 0x0D
#define ICM20_SELF_TEST_Y_ACCEL 0x0E
#define ICM20_SELF_TEST_Z_ACCEL 0x0F/* 陀螺仪静态偏移 */
#define ICM20_XG_OFFS_USRH 0x13
#define ICM20_XG_OFFS_USRL 0x14
#define ICM20_YG_OFFS_USRH 0x15
#define ICM20_YG_OFFS_USRL 0x16
#define ICM20_ZG_OFFS_USRH 0x17
#define ICM20_ZG_OFFS_USRL 0x18#define ICM20_SMPLRT_DIV 0x19
#define ICM20_CONFIG 0x1A
#define ICM20_GYRO_CONFIG 0x1B
#define ICM20_ACCEL_CONFIG 0x1C
#define ICM20_ACCEL_CONFIG2 0x1D
#define ICM20_LP_MODE_CFG 0x1E
#define ICM20_ACCEL_WOM_THR 0x1F
#define ICM20_FIFO_EN 0x23
#define ICM20_FSYNC_INT 0x36
#define ICM20_INT_PIN_CFG 0x37
#define ICM20_INT_ENABLE 0x38
#define ICM20_INT_STATUS 0x3A/* 加速度输出 */
#define ICM20_ACCEL_XOUT_H 0x3B
#define ICM20_ACCEL_XOUT_L 0x3C
#define ICM20_ACCEL_YOUT_H 0x3D
#define ICM20_ACCEL_YOUT_L 0x3E
#define ICM20_ACCEL_ZOUT_H 0x3F
#define ICM20_ACCEL_ZOUT_L 0x40/* 温度输出 */
#define ICM20_TEMP_OUT_H 0x41
#define ICM20_TEMP_OUT_L 0x42/* 陀螺仪输出 */
#define ICM20_GYRO_XOUT_H 0x43
#define ICM20_GYRO_XOUT_L 0x44
#define ICM20_GYRO_YOUT_H 0x45
#define ICM20_GYRO_YOUT_L 0x46
#define ICM20_GYRO_ZOUT_H 0x47
#define ICM20_GYRO_ZOUT_L 0x48#define ICM20_SIGNAL_PATH_RESET 0x68
#define ICM20_ACCEL_INTEL_CTRL 0x69
#define ICM20_USER_CTRL 0x6A
#define ICM20_PWR_MGMT_1 0x6B
#define ICM20_PWR_MGMT_2 0x6C
#define ICM20_FIFO_COUNTH 0x72
#define ICM20_FIFO_COUNTL 0x73
#define ICM20_FIFO_R_W 0x74
#define ICM20_WHO_AM_I 0x75/* 加速度静态偏移 */
#define ICM20_XA_OFFSET_H 0x77
#define ICM20_XA_OFFSET_L 0x78
#define ICM20_YA_OFFSET_H 0x7A
#define ICM20_YA_OFFSET_L 0x7B
#define ICM20_ZA_OFFSET_H 0x7D
#define ICM20_ZA_OFFSET_L 0x7E#endif
接下来继续编写 icm20608.c 文件,因为 icm20608.c 文件内容比较长,因此这里就将其分开 来讲解。
4.2.1:icm20608 设备结构体创建
首先创建一个 icm20608 设备结构体,如下所示:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "icm20608reg.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : icm20608.c
作者 : 左忠凯
版本 : V1.0
描述 : ICM20608 SPI驱动程序
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2019/9/2 左忠凯创建
***************************************************************/
#define ICM20608_CNT 1
#define ICM20608_NAME "icm20608"struct icm20608_dev {dev_t devid; /* 设备号 */struct cdev cdev; /* cdev */struct class *class; /* 类 */struct device *device; /* 设备 */struct device_node *nd; /* 设备节点 */int major; /* 主设备号 */void *private_data; /* 私有数据 */signed int gyro_x_adc; /* 陀螺仪X轴原始值 */signed int gyro_y_adc; /* 陀螺仪Y轴原始值 */signed int gyro_z_adc; /* 陀螺仪Z轴原始值 */signed int accel_x_adc; /* 加速度计X轴原始值 */signed int accel_y_adc; /* 加速度计Y轴原始值 */signed int accel_z_adc; /* 加速度计Z轴原始值 */signed int temp_adc; /* 温度原始值 */
};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 初 始化、注册与注销代码如下:
/* 传统匹配方式ID列表 */
static const struct spi_device_id icm20608_id[] = {{"alientek,icm20608", 0}, {}
};/* 设备树匹配列表 */
static const struct of_device_id icm20608_of_match[] = {{ .compatible = "alientek,icm20608" },{ /* Sentinel */ }
};/* SPI驱动结构体 */
static struct spi_driver icm20608_driver = {.probe = icm20608_probe,.remove = icm20608_remove,.driver = {.owner = THIS_MODULE,.name = "icm20608",.of_match_table = icm20608_of_match, },.id_table = icm20608_id,
};/** @description : 驱动入口函数* @param : 无* @return : 无*/
static int __init icm20608_init(void)
{return spi_register_driver(&icm20608_driver);
}/** @description : 驱动出口函数* @param : 无* @return : 无*/
static void __exit icm20608_exit(void)
{spi_unregister_driver(&icm20608_driver);
}module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");
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 函数内容如下所示:
/** @description : spi驱动的probe函数,当驱动与* 设备匹配以后此函数就会执行* @param - client : i2c设备* @param - id : i2c设备ID* */
static int icm20608_probe(struct spi_device *spi)
{/* 1、构建设备号 */if (icm20608dev.major) {icm20608dev.devid = MKDEV(icm20608dev.major, 0);register_chrdev_region(icm20608dev.devid, ICM20608_CNT, ICM20608_NAME);} else {alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT, ICM20608_NAME);icm20608dev.major = MAJOR(icm20608dev.devid);}/* 2、注册设备 */cdev_init(&icm20608dev.cdev, &icm20608_ops);cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);/* 3、创建类 */icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);if (IS_ERR(icm20608dev.class)) {return PTR_ERR(icm20608dev.class);}/* 4、创建设备 */icm20608dev.device = device_create(icm20608dev.class, NULL, icm20608dev.devid, NULL, ICM20608_NAME);if (IS_ERR(icm20608dev.device)) {return PTR_ERR(icm20608dev.device);}/*初始化spi_device */spi->mode = SPI_MODE_0; /*MODE0,CPOL=0,CPHA=0*/spi_setup(spi);icm20608dev.private_data = spi; /* 设置私有数据 *//* 初始化ICM20608内部寄存器 */icm20608_reginit(); return 0;
}/** @description : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行* @param - client : i2c设备* @return : 0,成功;其他负值,失败*/
static int icm20608_remove(struct spi_device *spi)
{/* 删除设备 */cdev_del(&icm20608dev.cdev);unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);/* 注销掉类和设备 */device_destroy(icm20608dev.class, icm20608dev.devid);class_destroy(icm20608dev.class);return 0;
}
第 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 寄存器读写与初始化
/** @description : 从icm20608读取多个寄存器数据* @param - dev: icm20608设备* @param - reg: 要读取的寄存器首地址* @param - val: 读取到的数据* @param - len: 要读取的数据长度* @return : 操作结果*/
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{int ret = -1;unsigned char txdata[1];unsigned char * rxdata;struct spi_message m;struct spi_transfer *t;struct spi_device *spi = (struct spi_device *)dev->private_data;t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); /* 申请内存 */if(!t) {return -ENOMEM;}rxdata = kzalloc(sizeof(char) * len, GFP_KERNEL); /* 申请内存 */if(!rxdata) {goto out1;}/* 一共发送len+1个字节的数据,第一个字节为寄存器首地址,一共要读取len个字节长度的数据,*/txdata[0] = reg | 0x80; /* 写数据的时候首寄存器地址bit8要置1 */ t->tx_buf = txdata; /* 要发送的数据 */t->rx_buf = rxdata; /* 要读取的数据 */t->len = len+1; /* t->len=发送的长度+读取的长度 */spi_message_init(&m); /* 初始化spi_message */spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */ret = spi_sync(spi, &m); /* 同步发送 */if(ret) {goto out2;}memcpy(buf , rxdata+1, len); /* 只需要读取的数据 */out2:kfree(rxdata); /* 释放内存 */
out1: kfree(t); /* 释放内存 */return ret;
}/** @description : 向icm20608多个寄存器写入数据* @param - dev: icm20608设备* @param - reg: 要写入的寄存器首地址* @param - val: 要写入的数据缓冲区* @param - len: 要写入的数据长度* @return : 操作结果*/
static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len)
{int ret = -1;unsigned char *txdata;struct spi_message m;struct spi_transfer *t;struct spi_device *spi = (struct spi_device *)dev->private_data;t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); /* 申请内存 */if(!t) {return -ENOMEM;}txdata = kzalloc(sizeof(char)*len, GFP_KERNEL);if(!txdata) {goto out1;}/* 一共发送len+1个字节的数据,第一个字节为寄存器首地址,len为要写入的寄存器的集合,*/*txdata = reg & ~0x80; /* 写数据的时候首寄存器地址bit8要清零 */memcpy(txdata+1, buf, len); /* 把len个寄存器拷贝到txdata里,等待发送 */t->tx_buf = txdata; /* 要发送的数据 */t->len = len+1; /* t->len=发送的长度+读取的长度 */spi_message_init(&m); /* 初始化spi_message */spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */ret = spi_sync(spi, &m); /* 同步发送 */if(ret) {goto out2;}out2:kfree(txdata); /* 释放内存 */
out1:kfree(t); /* 释放内存 */return ret;
}/** @description : 读取icm20608指定寄存器值,读取一个寄存器* @param - dev: icm20608设备* @param - reg: 要读取的寄存器* @return : 读取到的寄存器值*/
static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
{u8 data = 0;icm20608_read_regs(dev, reg, &data, 1);return data;
}/** @description : 向icm20608指定寄存器写入指定的值,写一个寄存器* @param - dev: icm20608设备* @param - reg: 要写的寄存器* @param - data: 要写入的值* @return : 无*/ static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
{u8 buf = value;icm20608_write_regs(dev, reg, &buf, 1);
}/** @description : 读取ICM20608的数据,读取原始数据,包括三轴陀螺仪、* : 三轴加速度计和内部温度。* @param - dev : ICM20608设备* @return : 无。*/
void icm20608_readdata(struct icm20608_dev *dev)
{unsigned char data[14] = { 0 };icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]); dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]); dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]); dev->temp_adc = (signed short)((data[6] << 8) | data[7]); dev->gyro_x_adc = (signed short)((data[8] << 8) | data[9]); dev->gyro_y_adc = (signed short)((data[10] << 8) | data[11]);dev->gyro_z_adc = (signed short)((data[12] << 8) | data[13]);
}
第 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 的字符设备驱动框架如下:
/** @description : 打开设备* @param - inode : 传递给驱动的inode* @param - filp : 设备文件,file结构体有个叫做pr似有ate_data的成员变量* 一般在open的时候将private_data似有向设备结构体。* @return : 0 成功;其他 失败*/
static int icm20608_open(struct inode *inode, struct file *filp)
{filp->private_data = &icm20608dev; /* 设置私有数据 */return 0;
}/** @description : 从设备读取数据 * @param - filp : 要打开的设备文件(文件描述符)* @param - buf : 返回给用户空间的数据缓冲区* @param - cnt : 要读取的数据长度* @param - offt : 相对于文件首地址的偏移* @return : 读取的字节数,如果为负值,表示读取失败*/
static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{signed int data[7];long err = 0;struct icm20608_dev *dev = (struct icm20608_dev *)filp->private_data;icm20608_readdata(dev);data[0] = dev->gyro_x_adc;data[1] = dev->gyro_y_adc;data[2] = dev->gyro_z_adc;data[3] = dev->accel_x_adc;data[4] = dev->accel_y_adc;data[5] = dev->accel_z_adc;data[6] = dev->temp_adc;err = copy_to_user(buf, data, sizeof(data));return 0;
}/** @description : 关闭/释放设备* @param - filp : 要关闭的设备文件(文件描述符)* @return : 0 成功;其他 失败*/
static int icm20608_release(struct inode *inode, struct file *filp)
{return 0;
}/* icm20608操作函数 */
static const struct file_operations icm20608_ops = {.owner = THIS_MODULE,.open = icm20608_open,.read = icm20608_read,.release = icm20608_release,
};
字符设备驱动框架没什么好说的,重点是第 22~38 行的 icm20608_read 函数,当应用程序 调用 read 函数读取 icm20608 设备文件的时候此函数就会执行。此函数调用上面编写好的 icm20608_readdata 函数读取 icm20608 的原始数据并将其上报给应用程序。大家注意,在内核 中尽量不要使用浮点运算,所以不要在驱动将 icm20608 的原始值转换为对应的实际值,因为会 涉及到浮点计算。
4.3:编写测试 APP
新建 icm20608App.c 文件,然后在里面输入如下所示内容:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : icm20608App.c
作者 : 左忠凯
版本 : V1.0
描述 : icm20608设备测试APP。
其他 : 无
使用方法 :./icm20608App /dev/icm20608
论坛 : www.openedv.com
日志 : 初版V1.0 2019/9/20 左忠凯创建
***************************************************************//** @description : main主程序* @param - argc : argv数组元素个数* @param - argv : 具体参数* @return : 0 成功;其他 失败*/
int main(int argc, char *argv[])
{int fd;char *filename;signed int databuf[7];unsigned char data[14];signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;signed int accel_x_adc, accel_y_adc, accel_z_adc;signed int temp_adc;float gyro_x_act, gyro_y_act, gyro_z_act;float accel_x_act, accel_y_act, accel_z_act;float temp_act;int ret = 0;if (argc != 2) {printf("Error Usage!\r\n");return -1;}filename = argv[1];fd = open(filename, O_RDWR);if(fd < 0) {printf("can't open file %s\r\n", filename);return -1;}while (1) {ret = read(fd, databuf, sizeof(databuf));if(ret == 0) { /* 数据读取成功 */gyro_x_adc = databuf[0];gyro_y_adc = databuf[1];gyro_z_adc = databuf[2];accel_x_adc = databuf[3];accel_y_adc = databuf[4];accel_z_adc = databuf[5];temp_adc = databuf[6];/* 计算实际值 */gyro_x_act = (float)(gyro_x_adc) / 16.4;gyro_y_act = (float)(gyro_y_adc) / 16.4;gyro_z_act = (float)(gyro_z_adc) / 16.4;accel_x_act = (float)(accel_x_adc) / 2048;accel_y_act = (float)(accel_y_adc) / 2048;accel_z_act = (float)(accel_z_adc) / 2048;temp_act = ((float)(temp_adc) - 25 ) / 326.8 + 25;printf("\r\n原始值:\r\n");printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc, gyro_y_adc, gyro_z_adc);printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc, accel_y_adc, accel_z_adc);printf("temp = %d\r\n", temp_adc);printf("实际值:");printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", gyro_x_act, gyro_y_act, gyro_z_act);printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", accel_x_act, accel_y_act, accel_z_act);printf("act temp = %.2f°C\r\n", temp_act);}usleep(100000); /*100ms */}close(fd); /* 关闭文件 */ return 0;
}
第 60~91 行,在 while 循环中每隔 100ms 从 icm20608 中读取一次数据,读取到 icm20608 原始数据以后将其转换为实际值,比如陀螺仪就是角速度、加速度计就是 g 值。注意,我们在 icm20608 驱动中将陀螺仪和加速度计的测量范围全部设置到了最大,分别为±2000 和±16g。 因此,在计算实际值的时候陀螺仪使用 16.4,加速度计使用 2048。最终将传感器原始数据和得 到的实际值显示在终端上。
5:运行测试
5.1:编写 Makefile 文件
编写 Makefile 文件,本章实验的 Makefile 文件和第四十章实验基本一样,只是将 obj-m 变 量的值改为“icm20608.o”,Makefile 内容如下所示:
KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)obj-m := icm20608.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modulesclean:$(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 这个测试程序:
arm-linux-gnueabihf-gcc -march=armv7-a -mfpu=neon -mfloat-abi=hard icm20608App.c -o
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 这个驱动模块。
depmod //第一次加载驱动的时候需要运行此命令
modprobe icm20608.ko //加载驱动模块
当驱动模块加载成功以后使用 icm20608App 来测试,输入如下命令:
./icm20608App /dev/icm20608

可以看出,开发板静止状态下,Z 轴方向的加速度在 1g 左右,这个就是重力加速度。对于 陀螺仪来讲,静止状态下三轴的角速度应该在 0°/S 左右。ICM20608 内温度传感器采集到的温 度在 30 多度左右,大家可以晃动一下开发板,这个时候陀螺仪和加速度计的值就会有变化。
本文仅在记录学习正点原子imx6ull-mini开发板的过程,不做他用。
相关文章:
正点原子imx6ull-mini-Linux驱动之Linux SPI 驱动实验(22)
跟上一章一样,其实这些设备驱动,无非就是传感器对应寄存器的读写。而这个读写是建立在各种通信协议上的,比如上一章的i2c,我们做了什么呢,就是把设备注册成一个i2c平台驱动,这个i2c驱动怎么搞的呢ÿ…...
TypeScript 函数
函数是JavaScript应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块。 在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义 行为 的地方。 TypeScript为JavaScript函数添加了额外的功能&…...
C++ : namespace,输入与输出,函数重载,缺省参数
一,命名空间(namespace) 1.1命名空间的作用与定义 我们在学习c的过程中,经常会碰到命名冲突的情况。就拿我们在c语言中的一个string函数来说吧: int strncat 0; int main() {printf("%d", strncat);return 0; } 当我们运行之后&…...
目标检测 | yolov1 原理和介绍
1. 简介 论文链接:https://arxiv.org/abs/1506.02640 时间:2015年 作者:Joseph Redmon 代码参考:https://github.com/abeardear/pytorch-YOLO-v1 yolo属于one-stage算法,仅仅使用一个CNN网络直接预测不同目标的类别与…...
excel中有些以文本格式存储的数值如何批量转换为数字
一、背景 1.1 文本格式存储的数值特点 在平时工作中有时候会从别地方导出来表格,表格中有些数值是以文本格式存储的(特点:单元格的左上角有个绿色的小标)。 1.2 文本格式存储的数值在排序时不符合预期 当我们需要进行排序的时候…...
原神升级计划数据表:4个倒计时可以修改提示信息和时间,可以点击等级、命座、天赋、备注进行修改。
<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><title>原神倒计时</title><style>* {margin: 0;padding: 0;box-sizing: border-box;body {background: #0b1b2c;}}header {width: 100vw;heigh…...
YoloV10 论文翻译(Real-Time End-to-End Object Detection)
摘要 近年来,YOLO因其在计算成本与检测性能之间实现了有效平衡,已成为实时目标检测领域的主流范式。研究人员对YOLO的架构设计、优化目标、数据增强策略等方面进行了探索,并取得了显著进展。然而,YOLO对非极大值抑制࿰…...
第R1周:RNN-心脏病预测
本文为🔗365天深度学习训练营 中的学习记录博客 原作者:K同学啊 要求: 1.本地读取并加载数据。 2.了解循环神经网络(RNN)的构建过程 3.测试集accuracy到达87% 拔高: 1.测试集accuracy到达89% 我的环境&a…...
Golang | Leetcode Golang题解之第321题拼接最大数
题目: 题解: func maxSubsequence(a []int, k int) (s []int) {for i, v : range a {for len(s) > 0 && len(s)len(a)-1-i > k && v > s[len(s)-1] {s s[:len(s)-1]}if len(s) < k {s append(s, v)}}return }func lexico…...
远程连接本地虚拟机失败问题汇总
前言 因为我的 Ubuntu 虚拟机是新装的,并且应该装的是比较纯净的版本(纯净是指很多工具都尚未安装),然后在使用远程连接工具 XShell 连接时出现了很多问题,这些都是我之前没遇到过的(因为之前主要使用云服…...
WebRTC 初探
前言 项目中有局域网投屏与文件传输的需求,所以研究了一下 webRTC,这里记录一下学习过程。 WebRTC 基本流程以及概念 下面以 1 对 1 音视频实时通话案例介绍 WebRTC 的基本流程以及概念 WebRTC 中的角色 WebRTC 终端,负责音视频采集、编解码、NAT 穿…...
Python:read,readline和readlines的区别
在Python中,read(), readline(), 和 readlines() 是文件操作中常用的三个方法,它们都用于从文件中读取数据,但各自的使用方式和适用场景有所不同。 read() 方法: read(size-1) 方法用于从文件中读取指定数量的字符。如果指定了si…...
重生之我学编程
编程小白如何成为大神?大学新生的最佳入门攻略 编程已成为当代大学生的必备技能,但面对众多编程语言和学习资源,新生们常常感到迷茫。如何选择适合自己的编程语言?如何制定有效的学习计划?如何避免常见的学习陷阱&…...
如何将PostgreSQL的数据实时迁移到SelectDB?
PostgreSQL 作为一个开源且功能强大的关系型数据库管理系统,在 OLTP 系统中得到了广泛应用。很多企业利用其卓越的性能和灵活的架构,应对高并发事务、快速响应等需求。 然而对于 OLAP 场景,PostgreSQL 可能并不是最佳选择。 为了实现庞大规…...
关于c语言的const 指针
const * type A 指向的数据是常量 如上所示,运行结果如下,通过解引用的方式,改变了data的值 const type * A 位置是常量,不能修改 运行结果如下 type const * A 指针是个常量,指向的值可以改变 如上所示,…...
万能门店小程序开发平台功能源码系统 带完整的安装代码包以及安装搭建教程
互联网技术的迅猛发展和用户对于便捷性需求的不断提高,小程序以其轻量、快捷、无需安装的特点,成为了众多商家和开发者关注的焦点。为满足广大商家对于门店线上化、智能化管理的需求,小编给大家分享一款“万能门店小程序开发平台功能源码系统…...
C#初级——字典Dictionary
字典 字典是C#中的一种集合,它存储键值对,并且每个键与一个值相关联。 创建字典 Dictionary<键的类型, 值的类型> 字典名字 new Dictionary<键的类型, 值的类型>(); Dictionary<int, string> dicStudent new Dictionary<int, str…...
git版本控制的底层实现
目录 前言 核心概念串讲 底层存储形式探测 本地仓库的详细解析 提交与分支的深入解析 几个问题的深入探讨 前言 Git的重要性 Git是一个开源的版本控制工具,广泛用于编程开发领域。它极大地提高了研发团队的开发协作效率。对于开发者来说,Git是一个…...
深入解析数据处理的技术与实践
欢迎来到我的博客,很高兴能够在这里和您见面!欢迎订阅相关专栏: 工💗重💗hao💗:野老杂谈 ⭐️ 全网最全IT互联网公司面试宝典:收集整理全网各大IT互联网公司技术、项目、HR面试真题. ⭐️ AIGC时代的创新与未来:详细讲解AIGC的概念、核心技术、应用领域等内容。 ⭐…...
python-调用c#代码
环境: win10,net framework 4,python3.9 镜像: C#-使用IronPython调用python代码_ironpython wpf-CSDN博客 https://blog.csdn.net/pxy7896/article/details/119929434 目录 hello word不接收参数接收参数 其他例子 hello word 不…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...
JVM虚拟机:内存结构、垃圾回收、性能优化
1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...
面向无人机海岸带生态系统监测的语义分割基准数据集
描述:海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而,目前该领域仍面临一个挑战,即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...
Spring AI Chat Memory 实战指南:Local 与 JDBC 存储集成
一个面向 Java 开发者的 Sring-Ai 示例工程项目,该项目是一个 Spring AI 快速入门的样例工程项目,旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 项目采用模块化设计,每个模块都专注于特定的功能领域,便于学习和…...
ubuntu22.04 安装docker 和docker-compose
首先你要确保没有docker环境或者使用命令删掉docker sudo apt-get remove docker docker-engine docker.io containerd runc安装docker 更新软件环境 sudo apt update sudo apt upgrade下载docker依赖和GPG 密钥 # 依赖 apt-get install ca-certificates curl gnupg lsb-rel…...
【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...
高端性能封装正在突破性能壁垒,其芯片集成技术助力人工智能革命。
2024 年,高端封装市场规模为 80 亿美元,预计到 2030 年将超过 280 亿美元,2024-2030 年复合年增长率为 23%。 细分到各个终端市场,最大的高端性能封装市场是“电信和基础设施”,2024 年该市场创造了超过 67% 的收入。…...
Excel 怎么让透视表以正常Excel表格形式显示
目录 1、创建数据透视表 2、设计 》报表布局 》以表格形式显示 3、设计 》分类汇总 》不显示分类汇总 1、创建数据透视表 2、设计 》报表布局 》以表格形式显示 3、设计 》分类汇总 》不显示分类汇总...
Ubuntu 安装 Mysql 数据库
首先更新apt-get工具,执行命令如下: apt-get upgrade安装Mysql,执行如下命令: apt-get install mysql-server 开启Mysql 服务,执行命令如下: service mysql start并确认是否成功开启mysql,执行命令如下&am…...
jieba实现和用RNN实现中文分词的区别
Jieba 分词和基于 RNN 的分词在技术路线、实现机制、性能特点上有显著差异,以下是核心对比: 1. 技术路线对比 维度Jieba 分词RNN 神经网络分词范式传统 NLP(规则 统计)深度学习(端到端学习)核心依赖词典…...
