700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > 【正点原子Linux连载】第六十二章 Linux SPI驱动实验 -摘自【正点原子】I.MX6U嵌入

【正点原子Linux连载】第六十二章 Linux SPI驱动实验 -摘自【正点原子】I.MX6U嵌入

时间:2019-02-15 18:40:29

相关推荐

【正点原子Linux连载】第六十二章 Linux SPI驱动实验 -摘自【正点原子】I.MX6U嵌入

1)实验平台:正点原子阿尔法Linux开发板

2)平台购买地址:/item.htm?id=603672744434

2)全套实验源码+手册+视频下载地址:/thread-300792-1-1.html

3)对正点原子Linux感兴趣的同学可以加群讨论:935446741

4)关注正点原子公众号,获取最新资料更新

第六十二章 Linux SPI驱动实验

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

62.1 Linux下SPI驱动框架简介

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

62.1.1 SPI主机驱动

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

示例代码62.1.1.1 spi_master结构体315 struct spi_master {316struct device dev;317 318struct list_head list;......326s16 bus_num;327 328/* chipselects will be integral to many controllers; some others329* might use board-specific GPIOs.330*/331u16 num_chipselect;332 333/* some SPI controllers pose alignment requirements on DMAable334* buffers; let protocol drivers know about these requirements.335*/336u16 dma_alignment;337 338/* spi_device.mode flags understood by this controller driver */339u16 mode_bits;340 341/* bitmask of supported bits_per_word for transfers */342u32 bits_per_word_mask;......347/* limits on transfer speed */348u32 min_speed_hz;349u32 max_speed_hz;350 351/* other constraints relevant to this driver */352u16 flags;......359/* lock and mutex for SPI bus locking */360spinlock_tbus_lock_spinlock;361struct mutex bus_lock_mutex;362 363 /* flag indicating that the SPI bus is locked for exclusive use */364bool bus_lock_flag;......372int (*setup)(struct spi_device *spi);373 ......393int (*transfer)(struct spi_device *spi,394struct 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、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。

返回值:无。

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。

62.1.2 SPI设备驱动

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

示例代码62.1.1.2 spi_driver结构体180 struct spi_driver {181const struct spi_device_id *id_table;182int (*probe)(struct spi_device *spi);183int (*remove)(struct spi_device *spi);184void (*shutdown)(struct spi_device *spi);185struct 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注册示例程序如下:

示例代码62.1.1.3 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。

62.1.3 SPI设备和驱动匹配过程

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

示例代码62.1.3.1 spi_bus_type结构体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,函数内容如下:

示例代码62.1.3.2 spi_match_device函数99 static int spi_match_device(struct device *dev, struct device_driver *drv)100 {101const struct spi_device *spi = to_spi_device(dev);102const struct spi_driver *sdrv = to_spi_driver(drv);103 104/* Attempt an OF style match */105if (of_driver_match_device(dev, drv))106 return 1;107 108/* Then try ACPI */109if (acpi_driver_match_device(dev, drv))110 return 1;111 112if (sdrv->id_table)113 return !!spi_match_id(sdrv->id_table, spi);114 115return 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成员变量是否相等。

62.2 I.MX6U SPI主机驱动分析

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

示例代码62.2.1 imx6ull.dtsi文件中的ecspi3节点内容1 ecspi3: ecspi@0000 {2#address-cells = <1>;3#size-cells = <0>;4compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";5reg = <0x0000 0x4000>;6interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;7clocks = <&clks IMX6UL_CLK_ECSPI3>,8 <&clks IMX6UL_CLK_ECSPI3>;9clock-names = "ipg", "per";10dmas = <&sdma 7 7 1>, <&sdma 8 7 2>;11dma-names = "rx", "tx";12status = "disabled";13 };

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

示例代码62.2.2 spi_imx_driver结构体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文件中找到如下所示内容:

示例代码62.2.3 spi_imx_buf_tx_u8函数152 #define MXC_SPI_BUF_TX(type)\153 static void spi_imx_buf_tx_##type(struct spi_imx_data *spi_imx) \154 {\155type val = 0;\156 \157if (spi_imx->tx_buf) {\158 val = *(type *)spi_imx->tx_buf; \159 spi_imx->tx_buf += sizeof(type);\160} \161 \162spi_imx->count -= sizeof(type); \163 \164writel(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的适配器驱动程序类似。

62.3 SPI设备驱动编写流程

62.3.1 SPI设备信息描述

1、IO的pinctrl子节点创建与修改

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

2、SPI设备节点的创建与修改

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

示例代码62.3.1.1 m25p80设备节点308 &ecspi1 {309fsl,spi-num-chipselects = <1>;310cs-gpios = <&gpio4 9 0>;311pinctrl-names = "default";312pinctrl-0 = <&pinctrl_ecspi1>;313status = "okay";314 315flash: 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中的内容即可。

62.3.2 SPI设备数据收发处理流程

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

示例代码62.3.2.1 spi_transfer结构体603 struct spi_transfer {604/* it's ok if tx_buf == rx_buf (right?)605* for MicroWire, one buffer must be null606* buffers must work with dma_*map_single() calls, unless607* spi_message.is_dma_mapped reports a pre-existing mapping608*/609const void *tx_buf;610void *rx_buf;611unsigned len;612 613dma_addr_t tx_dma;614dma_addr_t rx_dma;615struct sg_table tx_sg;616struct sg_table rx_sg;617 618unsigned cs_change:1;619unsigned tx_nbits:3;620unsigned rx_nbits:3;621 #define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */622 #define SPI_NBITS_DUAL0x02 /* 2bits transfer */623 #define SPI_NBITS_QUAD0x04 /* 4bits transfer */624u8bits_per_word;625u16delay_usecs;626u32speed_hz;627 628struct list_head transfer_list;629 };

第609行,tx_buf保存着要发送的数据。第610行,rx_buf用于保存接收到的数据。第611行,len是要进行传输的数据长度,SPI是全双工通信,因此在一次通信中发送和接收的字节数都是一样的,所以spi_transfer中也就没有发送长度和接收长度之分。spi_transfer需要组织成spi_message,spi_message也是一个结构体,内容如下:

示例代码62.3.2.2 spi_message结构体660 struct spi_message {661struct list_head transfers;662 663struct spi_device *spi;664 665unsigned is_dma_mapped:1;......678/* completion is reported through a callback */679void (*complete)(void *context);680void *context;681unsigned frame_length;682unsigned actual_length;683int status;684 685/* for optional use by whatever driver currently owns the686* spi_message ... between calls to spi_async and then later687* complete(), that's the spi_master controller driver.688*/689struct list_head queue;690void *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函数,此函数原型如下:

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

示例代码62.3.2.3 SPI数据读写操作/* SPI多字节发送 */static int spi_send(struct spi_device *spi, u8 *buf, int len){int ret;struct spi_message m;struct spi_transfer t = {.tx_buf = buf,.len = len,};spi_message_init(&m); /* 初始化spi_message */spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */ret = spi_sync(spi, &m); /* 同步传输 */return ret;}/* SPI多字节接收 */static int spi_receive(struct spi_device *spi, u8 *buf, int len){int ret;struct spi_message m;struct spi_transfer t = {.rx_buf = buf,.len = len,};spi_message_init(&m); /* 初始化spi_message */spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */ret = spi_sync(spi, &m); /* 同步传输 */return ret;}

62.4 硬件原理图分析

本章实验硬件原理图参考26.2小节即可。

62.5 试验程序编写

本实验对应的例程路径为:开发板光盘-> 2、Linux驱动例程-> 22_spi。

62.5.1 修改设备树

1、添加ICM20608所使用的IO

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

示例代码62.5.1.1 icm20608 IO节点信息1 pinctrl_ecspi3: icm20608 {2fsl,pins = < 3 MX6UL_PAD_UART2_TX_DATA__GPIO1_IO200x10b0/* CS */4 MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 0x10b1 /* SCLK */5 MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO0x10b1 /* MISO */6 MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI0x10b1 /* MOSI */7>;8};

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

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

示例代码62.5.1.2 向ecspi3节点加入icm20608信息1 &ecspi3 {2fsl,spi-num-chipselects = <1>;3cs-gpio = <&gpio1 20 GPIO_ACTIVE_LOW>; /* cant't use cs-gpios! */4pinctrl-names = "default";5pinctrl-0 = <&pinctrl_ecspi3>;6status = "okay";7 8spidev: icm20608@0 {9compatible = "alientek,icm20608";10 spi-max-frequency = <8000000>;11 reg = <0>;12};13 };

第2行,设置当前片选数量为1,因为就只接了一个ICM20608。第3行,注意!这里并没有用到“cs-gpios”属性,而是用了一个自己定义的“cs-gpio”属性,因为我们要自己控制片选引脚。如果使用“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系统。

62.5.2 编写ICM20608驱动

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

示例代码62.5.2.1 icm20608reg.h文件内容1 #ifndef ICM20608_H2 #define ICM20608_H3 /***************************************************************4 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.5 文件名 : icm20608reg.h6 作者 : 左忠凯7 版本 : V1.08 描述 : ICM20608寄存器地址描述头文件9 其他 : 无10 论坛 : 11 日志 : 初版V1.0 /9/2 左忠凯创建12 ***************************************************************/13 #define ICM20608G_ID 0XAF /* ID值 */14 #define ICM20608D_ID 0XAE /* ID值 */15 16 /* ICM20608寄存器 17 *复位后所有寄存器地址都为0,除了18 *Register 107(0X6B) Power Management 1 = 0x4019 *Register 117(0X75) WHO_AM_I = 0xAF或0xAE20 */21 /* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */22 #define ICM20_SELF_TEST_X_GYRO 0x0023 #define ICM20_SELF_TEST_Y_GYRO 0x0124 #define ICM20_SELF_TEST_Z_GYRO 0x0225 #define ICM20_SELF_TEST_X_ACCEL 0x0D26 #define ICM20_SELF_TEST_Y_ACCEL 0x0E27 #define ICM20_SELF_TEST_Z_ACCEL 0x0F......80 /* 加速度静态偏移 */81 #define ICM20_XA_OFFSET_H 0x7782 #define ICM20_XA_OFFSET_L 0x7883 #define ICM20_YA_OFFSET_H 0x7A84 #define ICM20_YA_OFFSET_L 0x7B85 #define ICM20_ZA_OFFSET_H 0x7D86 #define ICM20_ZA_OFFSET_L 0x7E87 88 #endif

接下来继续编写icm20608.c文件,因为icm20608.c文件内容比较长,因此这里就将其分开来讲解。1、icm20608设备结构体创建首先创建一个icm20608设备结构体,如下所示:

示例代码62.5.2.2 icm20608设备结构体创建1 #include <linux/types.h>2 #include <linux/kernel.h>3 #include <linux/delay.h>......22 #include <asm/io.h>23 #include "icm20608reg.h"24 /***************************************************************25 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.26 文件名 : icm20608.c27 作者 : 左忠凯28 版本 : V1.029 描述 : ICM20608 SPI驱动程序30 其他 : 无31 论坛 : 32 日志 : 初版V1.0 /9/2 左忠凯创建33 ***************************************************************/34 #define ICM20608_CNT 135 #define ICM20608_NAME "icm20608"36 37 struct icm20608_dev {38 dev_t devid;/* 设备号*/39 struct cdev cdev; /* cdev*/40 struct class *class; /* 类*/41 struct device *device;/* 设备 */42 struct device_node *nd; /* 设备节点 */43 int major; /* 主设备号 */44 void *private_data; /* 私有数据 */45 int cs_gpio;/* 片选所使用的GPIO编号*/46 signed int gyro_x_adc;/* 陀螺仪X轴原始值 */47 signed int gyro_y_adc;/* 陀螺仪Y轴原始值 */48 signed int gyro_z_adc;/* 陀螺仪Z轴原始值 */49 signed int accel_x_adc;/* 加速度计X轴原始值 */50 signed int accel_y_adc;/* 加速度计Y轴原始值 */51 signed int accel_z_adc;/* 加速度计Z轴原始值 */52 signed int temp_adc; /* 温度原始值*/53 };54 55 static struct icm20608_dev icm20608dev;

icm20608的设备结构体icm20608_dev没什么好讲的,重点看一下第44行的private_data,对于SPI设备驱动来讲最核心的就是spi_device。probe函数会向驱动提供当前SPI设备对应的spi_device,因此在probe函数中设置private_data为probe函数传递进来的spi_device参数。2、icm20608的spi_driver注册与注销对于SPI设备驱动,首先就是要初始化并向系统注册spi_driver,icm20608的spi_driver初始化、注册与注销代码如下:

示例代码62.5.2.3 icm20608的spi_driver初始化、注册与注销1 /* 传统匹配方式ID列表 */2 static const struct spi_device_id icm20608_id[] = {3 {"alientek,icm20608", 0}, 4 {}5 };6 7 /* 设备树匹配列表 */8 static const struct of_device_id icm20608_of_match[] = {9 {.compatible = "alientek,icm20608" },10 {/* Sentinel */ }11 };12 13 /* SPI驱动结构体 */ 14 static struct spi_driver icm20608_driver = {15 .probe = icm20608_probe,16 .remove = icm20608_remove,17 .driver = {18.owner = THIS_MODULE,19.name = "icm20608",20.of_match_table = icm20608_of_match, 21 },22 .id_table = icm20608_id,23 };24 25 /*26 * @description: 驱动入口函数27 * @param : 无28 * @return : 无29 */30 static int __init icm20608_init(void)31 {32 return spi_register_driver(&icm20608_driver);33 }34 35 /*36 * @description: 驱动出口函数37 * @param : 无38 * @return : 无39 */40 static void __exit icm20608_exit(void)41 {42 spi_unregister_driver(&icm20608_driver);43 }44 45 module_init(icm20608_init);46 module_exit(icm20608_exit);47 MODULE_LICENSE("GPL");48 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。3、probe&remove函数icm20608_driver中的probe和remove函数内容如下所示:

示例代码62.5.2.4 probe和remove函数1 /*2 * @description: spi驱动的probe函数,当驱动与3 * 设备匹配以后此函数就会执行4 * @param - client : spi设备5 * @param - id: spi设备ID6 * 7 */ 8 static int icm20608_probe(struct spi_device *spi)9 {10 int ret = 0;11 12 /* 1、构建设备号 */13 if (icm20608dev.major) {14icm20608dev.devid = MKDEV(icm20608dev.major, 0);15register_chrdev_region(icm20608dev.devid, ICM20608_CNT, ICM20608_NAME);16 } else {17alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT, ICM20608_NAME);18icm20608dev.major = MAJOR(icm20608dev.devid);19 }20 21 /* 2、注册设备 */22 cdev_init(&icm20608dev.cdev, &icm20608_ops);23 cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);24 25 /* 3、创建类 */26 icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);27 if (IS_ERR(icm20608dev.class)) {28return PTR_ERR(icm20608dev.class);29 }30 31 /* 4、创建设备 */32 icm20608dev.device = device_create(icm20608dev.class, NULL, icm20608dev.devid, NULL, ICM20608_NAME);33 if (IS_ERR(icm20608dev.device)) {34return PTR_ERR(icm20608dev.device);35 }36 37 /* 获取设备树中spi节点 */38 icm20608dev.nd = of_find_node_by_path("/soc/aips-bus@02000000/spba-bus@02000000/ecspi@0000");39 if(icm20608dev.nd == NULL) {40printk("ecspi3 node not find!\r\n");41return -EINVAL;42 } 43 44 /* 2、 获取设备树中的gpio属性,得到CS片选所使用的GPIO编号 */45 icm20608dev.cs_gpio = of_get_named_gpio(icm20608dev.nd, "cs-gpio", 0);46 if(icm20608dev.cs_gpio < 0) {47printk("can't get cs-gpio");48return -EINVAL;49 }50 51 /* 3、设置GPIO1_IO20为输出,并且输出高电平 */52 ret = gpio_direction_output(icm20608dev.cs_gpio, 1);53 if(ret < 0) {54printk("can't set gpio!\r\n");55 }56 57 /*初始化spi_device */58 spi->mode = SPI_MODE_0; /*MODE0,CPOL=0,CPHA=0*/59 spi_setup(spi);60 icm20608dev.private_data = spi; /* 设置私有数据 */61 62 /* 初始化ICM20608内部寄存器 */63 icm20608_reginit();64 return 0;65 }66 67 /*68 * @description : spi驱动的remove函数,移除spi驱动的时候此函数会执行69 * @param – client: spi设备70 * @return : 0,成功;其他负值,失败71 */72 static int icm20608_remove(struct spi_device *spi)73 {74 /* 删除设备 */75 cdev_del(&icm20608dev.cdev);76 unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);77 78 /* 注销掉类和设备 */79 device_destroy(icm20608dev.class, icm20608dev.devid);80 class_destroy(icm20608dev.class);81 return 0;82 }

第8~65行,probe函数,当设备与驱动匹配成功以后此函数就会执行,第13~55行都是标准的注册字符设备驱动。其中在第38~49行获取设备节点中的“cs-gpio”属性,也就是获取到设备的片选IO。第58行,设置SPI为模式0,也就是CPOL=0,CPHA=0。第59行,设置好spi_device以后需要使用spi_setup配置一下。第60行,设置icm20608dev的private_data成员变量为spi_device。第63行,调用icm20608_reginit函数初始化ICM20608,主要是初始化ICM20608指定寄存器。第72~81行,icm20608_remove函数,注销驱动的时候此函数就会执行。4、icm20608寄存器读写与初始化SPI驱动最终是通过读写icm20608的寄存器来实现的,因此需要编写相应的寄存器读写函数,并且使用这些读写函数来完成对icm20608的初始化。icm20608的寄存器读写以及初始化代码如下:

示例代码62.5.2.5 icm20608寄存器读写以及出初始化1 /*2 * @description : 从icm20608读取多个寄存器数据3 * @param – dev: icm20608设备4 * @param – reg: 要读取的寄存器首地址5 * @param – buf: 读取到的数据6 * @param – len: 要读取的数据长度7 * @return: 操作结果8 */9 static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)10 {11int ret;12unsigned char txdata[len];13struct spi_message m;14struct spi_transfer *t;15struct spi_device *spi = (struct spi_device *)dev->private_data;16 17gpio_set_value(dev->cs_gpio, 0); /* 片选拉低,选中ICM20608 */18t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);19 20/* 第1次,发送要读取的寄存地址 */21txdata[0] = reg | 0x80;/* 写数据的时候寄存器地址bit7要置1 */22t->tx_buf = txdata; /* 要发送的数据 */23t->len = 1; /* 1个字节 */24spi_message_init(&m); /* 初始化spi_message */25spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message */26ret = spi_sync(spi, &m); /* 同步发送 */27 28/* 第2次,读取数据 */29txdata[0] = 0xff; /* 随便一个值,此处无意义 */30t->rx_buf = buf; /* 读取到的数据 */31t->len = len;/* 要读取的数据长度 */32spi_message_init(&m); /* 初始化spi_message */33spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message*/34ret = spi_sync(spi, &m); /* 同步发送 */35 36kfree(t);/* 释放内存 */37gpio_set_value(dev->cs_gpio, 1); /* 片选拉高,释放ICM20608 */38 39return ret;40 }41 42 /*43 * @description: 向icm20608多个寄存器写入数据44 * @param – dev: icm20608设备45 * @param – reg: 要写入的寄存器首地址46 * @param – buf: 要写入的数据缓冲区47 * @param – len: 要写入的数据长度48 * @return : 操作结果49 */50 static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg,u8 *buf, u8 len)51 {52int ret;53 54unsigned char txdata[len];55struct spi_message m;56struct spi_transfer *t;57struct spi_device *spi = (struct spi_device *)dev->private_data;58 59t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); 60gpio_set_value(dev->cs_gpio, 0);/* 片选拉低 */61 62/* 第1次,发送要读取的寄存地址 */63txdata[0] = reg & ~0x80; /* 写数据的时候寄存器地址bit8要清零*/64t->tx_buf = txdata; /* 要发送的数据 */65t->len = 1; /* 1个字节 */66spi_message_init(&m); /* 初始化spi_message */67spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message */68ret = spi_sync(spi, &m); /* 同步发送 */69 70/* 第2次,发送要写入的数据 */71t->tx_buf = buf; /* 要写入的数据 */72t->len = len;/* 写入的字节数 */73spi_message_init(&m); /* 初始化spi_message */74spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message*/75ret = spi_sync(spi, &m); /* 同步发送 */76 77kfree(t); /* 释放内存 */78gpio_set_value(dev->cs_gpio, 1);/* 片选拉高,释放ICM20608 */79return ret;80 }81 82 /*83 * @description: 读取icm20608指定寄存器值,读取一个寄存器84 * @param – dev: icm20608设备85 * @param – reg: 要读取的寄存器86 * @return : 读取到的寄存器值87 */88 static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)89 {90u8 data = 0;91icm20608_read_regs(dev, reg, &data, 1);92return data;93 }94 95 /*96 * @description : 向icm20608指定寄存器写入指定的值,写一个寄存器97 * @param – dev: icm20608设备98 * @param – reg: 要写的寄存器99 * @param – data: 要写入的值100 * @return : 无101 */ 102 103 static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)104 {105u8 buf = value;106icm20608_write_regs(dev, reg, &buf, 1);107 }108 109 /*110 * @description : 读取ICM20608的数据,读取原始数据,包括三轴陀螺仪、111 * : 三轴加速度计和内部温度。112 * @param - dev : ICM20608设备113 * @return: 无。114 */115 void icm20608_readdata(struct icm20608_dev *dev)116 {117unsigned char data[14];118icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);119 120dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]); 121dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]); 122dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]); 123dev->temp_adc = (signed short)((data[6] << 8) | data[7]); 124dev->gyro_x_adc = (signed short)((data[8] << 8) | data[9]); 125dev->gyro_y_adc = (signed short)((data[10] << 8) | data[11]);126dev->gyro_z_adc = (signed short)((data[12] << 8) | data[13]);127 }128 /*129 * ICM20608内部寄存器初始化函数 130 * @param : 无131 * @return : 无132 */133 void icm20608_reginit(void)134 {135u8 value = 0;136137icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x80);138mdelay(50);139icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x01);140mdelay(50);141 142value = icm20608_read_onereg(&icm20608dev, ICM20_WHO_AM_I);143printk("ICM20608 ID = %#X\r\n", value); 144 145icm20608_write_onereg(&icm20608dev, ICM20_SMPLRT_DIV, 0x00); 146icm20608_write_onereg(&icm20608dev, ICM20_GYRO_CONFIG, 0x18); 147icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG, 0x18); 148icm20608_write_onereg(&icm20608dev, ICM20_CONFIG, 0x04); 149icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG2, 0x04); 150icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_2, 0x00); 151icm20608_write_onereg(&icm20608dev, ICM20_LP_MODE_CFG, 0x00); 152icm20608_write_onereg(&icm20608dev, ICM20_FIFO_EN, 0x00); 153 }

第9~40行,icm20608_read_regs函数,从icm20608中读取连续多个寄存器数据。第50~80行,icm20608_write_regs函数,向icm20608连续写入多个寄存器数据。第88~83行,icm20608_read_onereg函数,读取icm20608指定寄存器数据。第103~107行,icm20608_write_onereg函数,向icm20608指定寄存器写入数据。第115~126行,icm20608_readdata函数,读取icm20608六轴传感器和温度传感器原始数据值,应用程序读取icm20608的时候这些传感器原始数据就会上报给应用程序。第133~153行,icm20608_reginit函数,初始化icm20608,和我们spi裸机实验里面的初始化过程一样。5、字符设备驱动框架icm20608的字符设备驱动框架如下:

示例代码62.5.2.6 icm20608字符设备驱动1 /*2 * @description : 打开设备3 * @param – inode: 传递给驱动的inode4 * @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 /*15 * @description: 从设备读取数据 16 * @param - filp : 要打开的设备文件(文件描述符)17 * @param - buf : 返回给用户空间的数据缓冲区18 * @param - cnt : 要读取的数据长度19 * @param - offt : 相对于文件首地址的偏移20 * @return : 读取的字节数,如果为负值,表示读取失败21 */22 static ssize_t icm20608_read(struct file *filp, char __user *buf,size_t cnt, loff_t *off)23 {24 signed int data[7];25 long err = 0;26 struct icm20608_dev *dev = (struct icm20608_dev *)filp->private_data;27 28 icm20608_readdata(dev);29 data[0] = dev->gyro_x_adc;30 data[1] = dev->gyro_y_adc;31 data[2] = dev->gyro_z_adc;32 data[3] = dev->accel_x_adc;33 data[4] = dev->accel_y_adc;34 data[5] = dev->accel_z_adc;35 data[6] = dev->temp_adc;36 err = copy_to_user(buf, data, sizeof(data));37 return 0;38 }39 40 /*41 * @description : 关闭/释放设备42 * @param - filp : 要关闭的设备文件(文件描述符)43 * @return : 0 成功;其他 失败44 */45 static int icm20608_release(struct inode *inode, struct file *filp)46 {47 return 0;48 }49 50 /* icm20608操作函数 */51 static const struct file_operations icm20608_ops = {52 .owner = THIS_MODULE,53 .open = icm20608_open,54 .read = icm20608_read,55 .release = icm20608_release,56 };

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

62.5.3 编写测试APP

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

示例代码62.5.3.1 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.c17 作者 : 左忠凯18 版本 : V1.019 描述 : icm20608设备测试APP。20 其他 : 无21 使用方法 :./icm20608App /dev/icm2060822 论坛 : 23 日志 : 初版V1.0 /9/20 左忠凯创建24 ***************************************************************/25 26 /*27 * @description : main主程序28 * @param - argc : argv数组元素个数29 * @param - argv : 具体参数30 * @return : 0 成功;其他 失败31 */32 int main(int argc, char *argv[])33 {34 int fd;35 char *filename;36 signed int databuf[7];37 unsigned char data[14];38 signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;39 signed int accel_x_adc, accel_y_adc, accel_z_adc;40 signed int temp_adc;41 42 float gyro_x_act, gyro_y_act, gyro_z_act;43 float accel_x_act, accel_y_act, accel_z_act;44 float temp_act;45 46 int ret = 0;47 48 if (argc != 2) {49printf("Error Usage!\r\n");50return -1;51 }52 53 filename = argv[1];54 fd = open(filename, O_RDWR);55 if(fd < 0) {56printf("can't open file %s\r\n", filename);57return -1;58 }59 60 while (1) {61ret = read(fd, databuf, sizeof(databuf));62if(ret == 0) {/* 数据读取成功 */63gyro_x_adc = databuf[0];64gyro_y_adc = databuf[1];65gyro_z_adc = databuf[2];66accel_x_adc = databuf[3];67accel_y_adc = databuf[4];68accel_z_adc = databuf[5];69temp_adc = databuf[6];70 71/* 计算实际值 */72gyro_x_act = (float)(gyro_x_adc) / 16.4;73gyro_y_act = (float)(gyro_y_adc) / 16.4;74gyro_z_act = (float)(gyro_z_adc) / 16.4;75accel_x_act = (float)(accel_x_adc) / 2048;76accel_y_act = (float)(accel_y_adc) / 2048;77accel_z_act = (float)(accel_z_adc) / 2048;78temp_act = ((float)(temp_adc) - 25 ) / 326.8 + 25;79 80printf("\r\n原始值:\r\n");81printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc, gyro_y_adc, gyro_z_adc);82printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc, accel_y_adc, accel_z_adc);83printf("temp = %d\r\n", temp_adc);84 printf("实际值:");85printf("act gx = %.2f°/S, act gy = %.2f°/S, act gz = %.2f°/S\r\n", gyro_x_act, gyro_y_act, gyro_z_act);86printf("act ax = %.2fg, act ay = %.2fg, act az = %.2fg\r\n", accel_x_act, accel_y_act, accel_z_act);87printf("act temp = %.2f°C\r\n", temp_act);88}89usleep(100000); /*100ms */90 }91 close(fd); /* 关闭文件 */ 92 return 0;93 }

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

62.6 运行测试

62.6.1 编译驱动程序和测试APP

1、编译驱动程序

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

示例代码62.6.1.1 Makefile文件

1 KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek

4 obj-m := icm20608.o

11 clean:

12 $(MAKE) -C (KERNELDIR)M=(KERNELDIR) M=(KERNELDIR)M=(CURRENT_PATH) clean

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

输入如下命令编译出驱动模块文件:

make -j32

编译成功以后就会生成一个名为“icm20608.ko”的驱动模块文件。

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 icm20608App文件信息

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

62.6.2 运行测试

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

depmod //第一次加载驱动的时候需要运行此命令

modprobe icm20608.ko //加载驱动模块

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

./icm20608App /dev/icm20608

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

图62.6.2.1 获取到的ICM20608数据

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

【正点原子Linux连载】第六十二章 Linux SPI驱动实验 -摘自【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.0

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。