搜索
bottom↓
回复: 1

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

[复制链接]

出0入234汤圆

发表于 2020-7-9 10:26:01 | 显示全部楼层 |阅读模式
本帖最后由 正点原子 于 2020-10-26 12:03 编辑

1)实验平台:正点原子阿尔法Linux开发板
2)平台购买地址:https://item.taobao.com/item.htm?id=603672744434
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html  
4)对正点原子Linux感兴趣的同学可以加群讨论:
876919289 QQ群头像.png
5)关注正点原子公众号,获取最新资料


100846rel79a9p4uelap24.jpg

100846f1ce1fg14zbg0va4.png

第六十二章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结构体
  1. 315struct spi_master {
  2. 316struct 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. 361struct mutex        bus_lock_mutex;
  34. 362
  35. 363/* flag indicating that the SPI bus is locked for exclusive use */
  36. 364bool            bus_lock_flag;
  37. ......
  38. 372int(*setup)(struct spi_device *spi);
  39. 373
  40. ......
  41. 393int(*transfer)(struct spi_device *spi,
  42. 394struct spi_message *mesg);
  43. ......
  44. 434        int(*transfer_one_message)(struct spi_master *master,
  45. 435struct 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、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函数原型如下:
  1. 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函数,此函数原型为:
  1. 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结构体
  1. 180struct spi_driver {
  2. 180conststruct spi_device_id *id_table;
  3. 180int(*probe)(struct spi_device *spi);
  4. 180int(*remove)(struct spi_device *spi);
  5. 180void(*shutdown)(struct spi_device *spi);
  6. 180struct device_driver    driver;
  7. 180};
复制代码

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

函数参数和返回值含义如下:
sdrv:要注册的spi_driver。
        返回值:0,注册成功;赋值,注册失败。
        注销SPI设备驱动以后也需要注销掉前面注册的spi_driver,使用spi_unregister_driver函数完成spi_driver的注销,函数原型如下:
void spi_unregister_driver(struct spi_driver *sdrv)
函数参数和返回值含义如下:
sdrv:要注销的spi_driver。
        返回值:无。
        spi_driver注册示例程序如下:
示例代码62.1.1.3 spi_driver注册示例程序
  1. 1 /* probe函数 */
  2. 2staticint xxx_probe(struct spi_device *spi)
  3. 3{
  4. 4        /* 具体函数内容 */
  5. 5        return0;
  6. 6}
  7. 7
  8. 8/* remove函数 */
  9. 9staticint xxx_remove(struct spi_device *spi)
  10. 10{
  11. 11        /* 具体函数内容 */
  12. 12                return0;
  13. 13}
  14. 14/* 传统匹配方式ID列表 */
  15. 15staticconststruct spi_device_id xxx_id[]={
  16. 16        {"xxx",0},
  17. 17        {}
  18. 18};
  19. 19
  20. 20/* 设备树匹配列表 */
  21. 21staticconststruct of_device_id xxx_of_match[]={
  22. 22        {.compatible ="xxx"},
  23. 23        {/* Sentinel */}
  24. 24};
  25. 25
  26. 26/* SPI驱动结构体 */
  27. 27staticstruct 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. 39staticint __init xxx_init(void)
  40. 40{
  41. 41        return spi_register_driver(&xxx_driver);
  42. 42}
  43. 43
  44. 44/* 驱动出口函数 */
  45. 45staticvoid __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。
62.1.3 SPI设备和驱动匹配过程
        SPI设备和驱动的匹配过程是由SPI总线来完成的,这点和platform、I2C等驱动一样,SPI总线为spi_bus_type,定义在drivers/spi/spi.c文件中,内容如下:
示例代码62.1.3.1 spi_bus_type结构体
  1. 131struct 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,函数内容如下:
示例代码62.1.3.2 spi_match_device函数
  1. 99staticint spi_match_device(struct device *dev,
  2. struct device_driver *drv)
  3. 100{
  4. 101conststruct spi_device *spi = to_spi_device(dev);
  5. 102conststruct spi_driver *sdrv = to_spi_driver(drv);
  6. 103
  7. 104/* Attempt an OF style match */
  8. 105if(of_driver_match_device(dev, drv))
  9. 106return1;
  10. 107
  11. 108/* Then try ACPI */
  12. 109if(acpi_driver_match_device(dev, drv))
  13. 110return1;
  14. 111
  15. 112if(sdrv->id_table)
  16. 113return!!spi_match_id(sdrv->id_table, spi);
  17. 114
  18. 115return 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行,i2c_match_id函数用于传统的、无设备树的I2C设备和驱动匹配过程。比较I2C设备名字和i2c_device_id的name字段是否相等,相等的话就说明I2C设备和驱动匹配。
        第115行,比较spi_device中modalias成员变量和device_driver中的name成员变量是否相等。
62.2 I.MX6U SPI主机驱动分析
        和I2C的适配器驱动一样,SPI主机驱动一般都由SOC厂商编写好了,打开imx6ull.dtsi文件,找到如下所示内容:
示例代码62.2.1 imx6ull.dtsi文件中的ecspi3节点内容
  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 =<0x020100000x4000>;
  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 771>,<&sdma 872>;
  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,在此文件中找到如下内容:
示例代码62.2.2 spi_imx_driver结构体
  1. 694staticstruct 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. 721staticconststruct 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. 1338staticstruct 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文件中找到如下所示内容:
示例代码62.2.3 spi_imx_buf_tx_u8函数
  1. 152 #define MXC_SPI_BUF_TX(type)                \
  2. 153staticvoid spi_imx_buf_tx_##type(struct spi_imx_data *spi_imx)  \
  3. 154{                                                \
  4. 155     type val =0;                                                \
  5. 156                                                \
  6. 157if(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的适配器驱动程序类似。
62.3 SPI设备驱动编写流程
62.3.1 SPI设备信息描述
        1、IO的pinctrl子节点创建与修改
        首先肯定是根据所使用的IO来创建或修改pinctrl子节点,这个没什么好说的,唯独要注意的就是检查相应的IO有没有被其他的设备所使用,如果有的话需要将其删除掉!
        2、SPI设备节点的创建与修改
        采用设备树的情况下,SPI设备信息描述就通过创建相应的设备子节点来完成,我们可以打开imx6qdl-sabresd.dtsi这个设备树头文件,在此文件里面找到如下所示内容:
示例代码62.3.1.1 m25p80设备节点
  1. 308&ecspi1 {
  2. 309     fsl,spi-num-chipselects =<1>;
  3. 310     cs-gpios =<&gpio4 90>;
  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中的内容即可。
62.3.2 SPI设备数据收发处理流程
        SPI设备驱动的核心是spi_driver,这个我们已经在62.1.2小节讲过了。当我们向Linux内核注册成功spi_driver以后就可以使用SPI核心层提供的API函数来对设备进行读写操作了。首先是spi_transfer结构体,此结构体用于描述SPI传输信息,结构体内容如下:
示例代码62.3.2.1 spi_transfer结构体
  1. 603struct 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. 609constvoid*tx_buf;
  8. 610void*rx_buf;
  9. 611unsigned    len;
  10. 612
  11. 613     dma_addr_t  tx_dma;
  12. 614     dma_addr_t  rx_dma;
  13. 615struct sg_table tx_sg;
  14. 616struct sg_table rx_sg;
  15. 617
  16. 618unsigned    cs_change:1;
  17. 619unsigned    tx_nbits:3;
  18. 620unsigned    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. 628struct list_head transfer_list;
  27. 629};
复制代码

        第609行,tx_buf保存着要发送的数据。
        第610行,rx_buf用于保存接收到的数据。
        第611行,len是要进行传输的数据长度,SPI是全双工通信,因此在一次通信中发送和接收的字节数都是一样的,所以spi_transfer中也就没有发送长度和接收长度之分。
        spi_transfer需要组织成spi_message,spi_message也是一个结构体,内容如下:
示例代码62.3.2.2 spi_message结构体
  1. 660struct spi_message {
  2. 661struct list_head    transfers;
  3. 662
  4. 663struct spi_device   *spi;
  5. 664
  6. 665unsigned        is_dma_mapped:1;
  7. ......
  8. 678/* completion is reported through a callback */
  9. 679void(*complete)(void*context);
  10. 680void*context;
  11. 681unsigned        frame_length;
  12. 682unsigned        actual_length;
  13. 683int         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. 689struct list_head    queue;
  20. 690void*state;
  21. 691};
复制代码

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

函数参数和返回值含义如下:
m:要初始化的spi_message。
        返回值:无。
        spi_message初始化完成以后需要将spi_transfer添加到spi_message队列中,这里我们要用到spi_message_add_tail函数,此函数原型如下:
voidspi_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个字节的数据发送和接收的示例代码如下所示:
示例代码62.3.2.3 SPI数据读写操作
  1. /* SPI多字节发送 */
  2. staticint 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. staticint 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. }
复制代码

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. 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。
        2、在ecspi3节点追加icm20608子节点
在imx6ull-alientek-emmc.dts文件中并没有任何向ecspi3节点追加内容的代码,这是因为NXP官方的6ULL EVK开发板上没有连接SPI设备。在imx6ull-alientek-emmc.dts文件最后面加入如下所示内容:
示例代码62.5.1.2 向ecspi3节点加入icm20608信息
  1. 1&ecspi3 {
  2. 2      fsl,spi-num-chipselects =<1>;
  3. 3      cs-gpio =<&gpio1 20 GPIO_ACTIVE_LOW>;/* cant't use cs-gpios! */
  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”属性,而是用了一个自己定义的“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. 1  #ifndef ICM20608_H
  2. 2  #define ICM20608_H
  3. 3/***************************************************************
  4. 4  Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
  5. 5 文件名                : icm20608reg.h
  6. 6作者        : 左忠凯
  7. 7版本        : V1.0
  8. 8描述        : ICM20608寄存器地址描述头文件
  9. 9其他        : 无
  10. 10论坛        : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  11. 11日志        : 初版V1.0 2019/9/2 左忠凯创建
  12. 12 ***************************************************************/
  13. 13 #define ICM20608G_ID                                 0XAF/* ID值 */
  14. 14 #define ICM20608D_ID                                 0XAE/* ID值 */
  15. 15
  16. 16/* ICM20608寄存器
  17. 17  *复位后所有寄存器地址都为0,除了
  18. 18  *Register 107(0X6B) Power Management 1          = 0x40
  19. 19  *Register 117(0X75) WHO_AM_I                            = 0xAF或0xAE
  20. 20  */
  21. 21/* 陀螺仪和加速度自测(出产时设置,用于与用户的自检输出值比较) */
  22. 22 #define  ICM20_SELF_TEST_X_GYRO           0x00
  23. 23 #define  ICM20_SELF_TEST_Y_GYRO            0x01
  24. 24 #define  ICM20_SELF_TEST_Z_GYRO            0x02
  25. 25 #define  ICM20_SELF_TEST_X_ACCEL           0x0D
  26. 26 #define  ICM20_SELF_TEST_Y_ACCEL           0x0E
  27. 27 #define  ICM20_SELF_TEST_Z_ACCEL           0x0F
  28. ......
  29. 80/* 加速度静态偏移 */
  30. 81 #define  ICM20_XA_OFFSET_H                   0x77
  31. 82 #define  ICM20_XA_OFFSET_L                   0x78
  32. 83 #define  ICM20_YA_OFFSET_H                   0x7A
  33. 84 #define  ICM20_YA_OFFSET_L                   0x7B
  34. 85 #define  ICM20_ZA_OFFSET_H                   0x7D
  35. 86 #define  ICM20_ZA_OFFSET_L                   0x7E
  36. 87
  37. 88 #endif
复制代码

        接下来继续编写icm20608.c文件,因为icm20608.c文件内容比较长,因此这里就将其分开来讲解。
        1、icm20608设备结构体创建
        首先创建一个icm20608设备机构体,如下所示:
示例代码62.5.2.2 icm20608设备结构体创建
  1. 1  #include <linux/types.h>
  2. 2  #include <linux/kernel.h>
  3. 3  #include <linux/delay.h>
  4. ......
  5. 22 #include <asm/io.h>
  6. 23 #include "icm20608reg.h"
  7. 24/***************************************************************
  8. 25 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
  9. 26文件名                : icm20608.c
  10. 27作者        : 左忠凯
  11. 28版本        : V1.0
  12. 29描述        : ICM20608 SPI驱动程序
  13. 30其他        : 无
  14. 31论坛        : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  15. 32日志        : 初版V1.0 2019/9/2 左忠凯创建
  16. 33 ***************************************************************/
  17. 34 #define ICM20608_CNT 1
  18. 35 #define ICM20608_NAME    "icm20608"
  19. 36
  20. 37struct icm20608_dev {
  21. 38        dev_t devid;        /* 设备号                        */
  22. 39        struct cdev cdev;        /* cdev                             */
  23. 40        struct class *class;        /* 类                                */
  24. 41        struct device *device;        /* 设备                                */
  25. 42        struct device_node  *nd;        /* 设备节点                                */
  26. 43        int major;        /* 主设备号                                */
  27. 44        void*private_data;        /* 私有数据                */
  28. 45        int cs_gpio;        /* 片选所使用的GPIO编号*/
  29. 46                signedint gyro_x_adc;        /* 陀螺仪X轴原始值        */
  30. 47        signedint gyro_y_adc;        /* 陀螺仪Y轴原始值        */
  31. 48        signedint gyro_z_adc;        /* 陀螺仪Z轴原始值        */
  32. 49        signedint accel_x_adc;        /* 加速度计X轴原始值        */
  33. 50        signedint accel_y_adc;        /* 加速度计Y轴原始值        */
  34. 51        signedint accel_z_adc;        /* 加速度计Z轴原始值        */
  35. 52        signedint temp_adc;        /* 温度原始值        */
  36. 53};
  37. 54
  38. 55staticstruct 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. 1        /* 传统匹配方式ID列表 */
  2. 2staticconststruct spi_device_id icm20608_id[]={
  3. 3        {"alientek,icm20608",0},
  4. 4        {}
  5. 5};
  6. 6
  7. 7        /* 设备树匹配列表 */
  8. 8staticconststruct of_device_id icm20608_of_match[]={
  9. 9        {.compatible ="alientek,icm20608"},
  10. 10        {/* Sentinel */}
  11. 11};
  12. 12
  13. 13        /* SPI驱动结构体 */
  14. 14staticstruct spi_driver icm20608_driver ={
  15. 15        .probe = icm20608_probe,
  16. 16        .remove = icm20608_remove,
  17. 17        .driver ={
  18. 18        .owner = THIS_MODULE,
  19. 19        .name ="icm20608",
  20. 20        .of_match_table = icm20608_of_match,
  21. 21        },
  22. 22        .id_table = icm20608_id,
  23. 23};
  24. 24
  25. 25/*
  26. 26  * @description        : 驱动入口函数
  27. 27  * @param                : 无
  28. 28  * @return               : 无
  29. 29  */
  30. 30staticint __init icm20608_init(void)
  31. 31{
  32. 32        return spi_register_driver(&icm20608_driver);
  33. 33}
  34. 34
  35. 35/*
  36. 36  * @description        : 驱动出口函数
  37. 37  * @param                : 无
  38. 38  * @return               : 无
  39. 39  */
  40. 40staticvoid __exit icm20608_exit(void)
  41. 41{        
  42. 42        spi_unregister_driver(&icm20608_driver);
  43. 43}
  44. 44
  45. 45 module_init(icm20608_init);
  46. 46 module_exit(icm20608_exit);
  47. 47 MODULE_LICENSE("GPL");
  48. 48 MODULE_AUTHOR("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. 1/*
  2. 2    * @description             : spi驱动的probe函数,当驱动与
  3. 3    *                            设备匹配以后此函数就会执行
  4. 4    * @param - client          : spi设备
  5. 5    * @param - id             : spi设备ID
  6. 6    *
  7. 7    */
  8. 8staticint icm20608_probe(struct spi_device *spi)
  9. 9{
  10. 10        int ret =0;
  11. 11
  12. 12        /* 1、构建设备号 */
  13. 13        if(icm20608dev.major){
  14. 14        icm20608dev.devid = MKDEV(icm20608dev.major,0);
  15. 15        register_chrdev_region(icm20608dev.devid, ICM20608_CNT,
  16. ICM20608_NAME);
  17. 16        }else{
  18. 17        alloc_chrdev_region(&icm20608dev.devid,0, ICM20608_CNT,
  19. ICM20608_NAME);
  20. 18        icm20608dev.major = MAJOR(icm20608dev.devid);
  21. 19        }
  22. 20
  23. 21        /* 2、注册设备 */
  24. 22        cdev_init(&icm20608dev.cdev,&icm20608_ops);
  25. 23        cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);
  26. 24
  27. 25        /* 3、创建类 */
  28. 26                icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);
  29. 27        if(IS_ERR(icm20608dev.class)){
  30. 28        return PTR_ERR(icm20608dev.class);
  31. 29                }
  32. 30
  33. 31        /* 4、创建设备 */
  34. 32        icm20608dev.device = device_create(icm20608dev.class,NULL,
  35. icm20608dev.devid,NULL, ICM20608_NAME);
  36. 33        if(IS_ERR(icm20608dev.device)){
  37. 34        return PTR_ERR(icm20608dev.device);
  38. 35        }
  39. 36
  40. 37        /* 获取设备树中cs片选信号 */
  41. 38        icm20608dev.nd = of_find_node_by_path("/soc/aips-bus@02000000/
  42. spba-bus@02000000/ecspi@02010000");
  43. 39        if(icm20608dev.nd ==NULL){
  44. 40        printk("ecspi3 node not find!\r\n");
  45. 41        return-EINVAL;
  46. 42        }
  47. 43
  48. 44        /* 2、获取设备树中的gpio属性,得到BEEP所使用的BEEP编号 */
  49. 45        icm20608dev.cs_gpio = of_get_named_gpio(icm20608dev.nd,
  50. "cs-gpio",0);
  51. 46                if(icm20608dev.cs_gpio <0){
  52. 47        printk("can't get cs-gpio");
  53. 48        return-EINVAL;
  54. 49        }
  55. 50
  56. 51        /* 3、设置GPIO1_IO20为输出,并且输出高电平 */
  57. 52        ret = gpio_direction_output(icm20608dev.cs_gpio,1);
  58. 53        if(ret <0){
  59. 54        printk("can't set gpio!\r\n");
  60. 55        }
  61. 56
  62. 57        /*初始化spi_device */
  63. 58        spi->mode = SPI_MODE_0;                        /*MODE0,CPOL=0,CPHA=0        */
  64. 59        spi_setup(spi);
  65. 60        icm20608dev.private_data = spi;/* 设置私有数据                        */
  66. 61
  67. 62        /* 初始化ICM20608内部寄存器 */
  68. 63        icm20608_reginit();
  69. 64        return0;
  70. 65}
  71. 66
  72. 67/*
  73. 68  * @description          : spi驱动的remove函数,移除spi驱动的时候此函数会执行
  74. 69  * @param – client        : spi设备
  75. 70  * @return                 : 0,成功;其他负值,失败
  76. 71  */
  77. 72staticint icm20608_remove(struct spi_device *spi)
  78. 73{
  79. 74        /* 删除设备 */
  80. 75        cdev_del(&icm20608dev.cdev);
  81. 76        unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);
  82. 77
  83. 78        /* 注销掉类和设备 */
  84. 79        device_destroy(icm20608dev.class, icm20608dev.devid);
  85. 80        class_destroy(icm20608dev.class);
  86. 81        return0;
  87. 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. 1/*
  2. 2    * @description         : 从icm20608读取多个寄存器数据
  3. 3    * @param – dev        :  icm20608设备
  4. 4    * @param – reg        :  要读取的寄存器首地址
  5. 5    * @param – val        :  读取到的数据
  6. 6    * @param – len        :  要读取的数据长度
  7. 7    * @return              : 操作结果
  8. 8    */
  9. 9staticint icm20608_read_regs(struct icm20608_dev *dev, u8 reg,
  10. void*buf,int len)
  11. 10{
  12. 11int ret;
  13. 12unsignedchar txdata[len];
  14. 13struct spi_message m;
  15. 14struct spi_transfer *t;
  16. 15struct spi_device *spi =(struct spi_device *)dev->private_data;
  17. 16
  18. 17      gpio_set_value(dev->cs_gpio,0);/* 片选拉低,选中ICM20608 */
  19. 18      t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
  20. 19
  21. 20/* 第1次,发送要读取的寄存地址 */
  22. 21      txdata[0]= reg |0x80;        /* 写数据的时候寄存器地址bit8要置1 */
  23. 22      t->tx_buf = txdata;        /* 要发送的数据                                                */
  24. 23      t->len =1;        /* 1个字节                                                        */
  25. 24      spi_message_init(&m);        /* 初始化spi_message                                 */
  26. 25      spi_message_add_tail(t,&m);/* 将spi_transfer添加到spi_message */
  27. 26      ret = spi_sync(spi,&m);        /* 同步发送                                                        */
  28. 27
  29. 28/* 第2次,读取数据 */
  30. 29      txdata[0]=0xff;        /* 随便一个值,此处无意义                        */
  31. 30      t->rx_buf = buf;        /* 读取到的数据                                                */
  32. 31      t->len = len;        /* 要读取的数据长度                                        */
  33. 32      spi_message_init(&m);        /* 初始化spi_message                                 */
  34. 33      spi_message_add_tail(t,&m);/* 将spi_transfer添加到spi_message*/
  35. 34      ret = spi_sync(spi,&m);        /* 同步发送                                                        */
  36. 35
  37. 36      kfree(t);        /* 释放内存                                        */
  38. 37      gpio_set_value(dev->cs_gpio,1);        /* 片选拉高,释放ICM20608         */
  39. 38
  40. 39return ret;
  41. 40}
  42. 41
  43. 42/*
  44. 43   * @description        : 向icm20608多个寄存器写入数据
  45. 44   * @param – dev        : icm20608设备
  46. 45   * @param – reg        : 要写入的寄存器首地址
  47. 46   * @param – val        : 要写入的数据缓冲区
  48. 47   * @param – len        : 要写入的数据长度
  49. 48   * @return                    : 操作结果
  50. 49   */
  51. 50static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg,
  52.                                      u8 *buf, u8 len)
  53. 51{
  54. 52int ret;
  55. 53
  56. 54unsignedchar txdata[len];
  57. 55struct spi_message m;
  58. 56struct spi_transfer *t;
  59. 57struct spi_device *spi =(struct spi_device *)dev->private_data;
  60. 58
  61. 59      t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
  62. 60      gpio_set_value(dev->cs_gpio,0);        /* 片选拉低 */
  63. 61
  64. 62/* 第1次,发送要读取的寄存地址 */
  65. 63      txdata[0]= reg &~0x80;        /* 写数据的时候寄存器地址bit8要清零        */
  66. 64      t->tx_buf = txdata;        /* 要发送的数据                                                */
  67. 65      t->len =1;        /* 1个字节                                                        */
  68. 66      spi_message_init(&m);        /* 初始化spi_message                                 */
  69. 67      spi_message_add_tail(t,&m);/* 将spi_transfer添加到spi_message*/
  70. 68      ret = spi_sync(spi,&m);        /* 同步发送                                                        */
  71. 69
  72. 70/* 第2次,发送要写入的数据 */
  73. 71      t->tx_buf = buf;        /* 要写入的数据                                                */
  74. 72      t->len = len;        /* 写入的字节数                                                */
  75. 73      spi_message_init(&m);        /* 初始化spi_message                                 */
  76. 74      spi_message_add_tail(t,&m);/* 将spi_transfer添加到spi_message*/
  77. 75      ret = spi_sync(spi,&m);        /* 同步发送                                                        */
  78. 76
  79. 77      kfree(t);        /* 释放内存                                                        */
  80. 78      gpio_set_value(dev->cs_gpio,1);/* 片选拉高,释放ICM20608                 */
  81. 79return ret;
  82. 80}
  83. 81
  84. 82/*
  85. 83   * @description        : 读取icm20608指定寄存器值,读取一个寄存器
  86. 84   * @param – dev        : icm20608设备
  87. 85   * @param – reg        : 要读取的寄存器
  88. 86   * @return                    : 读取到的寄存器值
  89. 87   */
  90. 88staticunsignedchar icm20608_read_onereg(struct icm20608_dev *dev,
  91. u8 reg)
  92. 89{
  93. 90      u8 data =0;
  94. 91      icm20608_read_regs(dev, reg,&data,1);
  95. 92return data;
  96. 93}
  97. 94
  98. 95/*
  99. 96   * @description         : 向icm20608指定寄存器写入指定的值,写一个寄存器
  100. 97   * @param – dev        : icm20608设备
  101. 98   * @param – reg        : 要写的寄存器
  102. 99   * @param – data        : 要写入的值
  103. 100  * @return                   : 无
  104. 101  */
  105. 102
  106. 103staticvoid icm20608_write_onereg(struct icm20608_dev *dev, u8 reg,
  107. u8 value)
  108. 104{
  109. 105     u8 buf = value;
  110. 106     icm20608_write_regs(dev, reg,&buf,1);
  111. 107}
  112. 108
  113. 109/*
  114. 110  * @description         : 读取ICM20608的数据,读取原始数据,包括三轴陀螺仪、
  115. 111  *                              : 三轴加速度计和内部温度。
  116. 112  * @param - dev         : ICM20608设备
  117. 113  * @return              : 无。
  118. 114  */
  119. 115void icm20608_readdata(struct icm20608_dev *dev)
  120. 116{
  121. 117unsignedchar data[14];
  122. 118     icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data,14);
  123. 119
  124. 120     dev->accel_x_adc =(signedshort)((data[0]<<8)| data[1]);
  125. 121     dev->accel_y_adc =(signedshort)((data[2]<<8)| data[3]);
  126. 122     dev->accel_z_adc =(signedshort)((data[4]<<8)| data[5]);
  127. 123     dev->temp_adc    =(signedshort)((data[6]<<8)| data[7]);
  128. 124     dev->gyro_x_adc  =(signedshort)((data[8]<<8)| data[9]);
  129. 125     dev->gyro_y_adc  =(signedshort)((data[10]<<8)| data[11]);
  130. 126     dev->gyro_z_adc  =(signedshort)((data[12]<<8)| data[13]);
  131. 127}
  132. 128/*
  133. 129  * ICM20608内部寄存器初始化函数
  134. 130  * @param           : 无
  135. 131  * @return          : 无
  136. 132  */
  137. 133void icm20608_reginit(void)
  138. 134{
  139. 135     u8 value =0;
  140. 136
  141. 137     icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1,0x80);
  142. 138     mdelay(50);
  143. 139     icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1,0x01);
  144. 140     mdelay(50);
  145. 141
  146. 142     value = icm20608_read_onereg(&icm20608dev, ICM20_WHO_AM_I);
  147. 143     printk("ICM20608 ID = %#X\r\n", value);
  148. 144
  149. 145     icm20608_write_onereg(&icm20608dev, ICM20_SMPLRT_DIV,0x00);
  150. 146     icm20608_write_onereg(&icm20608dev, ICM20_GYRO_CONFIG,0x18);
  151. 147     icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG,0x18);
  152. 148     icm20608_write_onereg(&icm20608dev, ICM20_CONFIG,0x04);
  153. 149     icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG2,0x04);
  154. 150     icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_2,0x00);
  155. 151     icm20608_write_onereg(&icm20608dev, ICM20_LP_MODE_CFG,0x00);
  156. 152     icm20608_write_onereg(&icm20608dev, ICM20_FIFO_EN,0x00);
  157. 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. 1/*
  2. 2   * @description           : 打开设备
  3. 3   * @param – inode        : 传递给驱动的inode
  4. 4   * @param - filp         : 设备文件,file结构体有个叫做pr似有ate_data的成员变量
  5. 5   *                     一般在open的时候将private_data似有向设备结构体。
  6. 6   * @return                : 0 成功;其他失败
  7. 7   */
  8. 8staticint icm20608_open(struct inode *inode,struct file *filp)
  9. 9{
  10. 10        filp->private_data =&icm20608dev;/* 设置私有数据 */
  11. 11        return0;
  12. 12}
  13. 13
  14. 14/*
  15. 15  * @description        : 从设备读取数据
  16. 16  * @param - filp         : 要打开的设备文件(文件描述符)
  17. 17  * @param - buf          : 返回给用户空间的数据缓冲区
  18. 18  * @param - cnt          : 要读取的数据长度
  19. 19  * @param - offt          : 相对于文件首地址的偏移
  20. 20  * @return                 : 读取的字节数,如果为负值,表示读取失败
  21. 21  */
  22. 22static ssize_t icm20608_read(struct file *filp,char __user *buf,size_t cnt, loff_t *off)
  23. 23{
  24. 24        signedint data[7];
  25. 25        long err =0;
  26. 26        struct icm20608_dev *dev =(struct icm20608_dev *
  27. )filp->private_data;
  28. 27
  29. 28        icm20608_readdata(dev);
  30. 29                data[0]= dev->gyro_x_adc;
  31. 30        data[1]= dev->gyro_y_adc;
  32. 31        data[2]= dev->gyro_z_adc;
  33. 32        data[3]= dev->accel_x_adc;
  34. 33        data[4]= dev->accel_y_adc;
  35. 34        data[5]= dev->accel_z_adc;
  36. 35        data[6]= dev->temp_adc;
  37. 36        err = copy_to_user(buf, data,sizeof(data));
  38. 37        return0;
  39. 38}
  40. 39
  41. 40/*
  42. 41  * @description          : 关闭/释放设备
  43. 42  * @param - filp         : 要关闭的设备文件(文件描述符)
  44. 43  * @return                : 0 成功;其他失败
  45. 44  */
  46. 45staticint icm20608_release(struct inode *inode,struct file *filp)
  47. 46{
  48. 47        return0;
  49. 48}
  50. 49
  51. 50/* icm20608操作函数 */
  52. 51staticconststruct file_operations icm20608_ops ={
  53. 52        .owner = THIS_MODULE,
  54. 53        .open = icm20608_open,
  55. 54        .read = icm20608_read,
  56. 55        .release = icm20608_release,
  57. 56};
复制代码

        字符设备驱动框架没什么好说的,重点是第22~38行的icm20608_read函数,当应用程序调用read函数读取icm20608设备文件的时候此函数就会执行。此函数调用上面编写好的icm20608_readdata函数读取icm20608的原始数据并将其上报给应用程序。大家注意,在内核中尽量不要使用浮点运算,所以不要在驱动将icm20608的原始值转换为对应的实际值,因为会涉及到浮点计算。
62.5.3编写测试APP
新建icm20608App.c文件,然后在里面输入如下所示内容:
示例代码62.5.3.1 icm20608App.c文件代码
  1. 1  #include "stdio.h"
  2. 2  #include "unistd.h"
  3. 3  #include "sys/types.h"
  4. 4  #include "sys/stat.h"
  5. 5  #include "sys/ioctl.h"
  6. 6  #include "fcntl.h"
  7. 7  #include "stdlib.h"
  8. 8  #include "string.h"
  9. 9  #include <poll.h>
  10. 10 #include <sys/select.h>
  11. 11 #include <sys/time.h>
  12. 12 #include <signal.h>
  13. 13 #include <fcntl.h>
  14. 14/***************************************************************
  15. 15 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
  16. 16 文件名                : icm20608App.c
  17. 17作者        : 左忠凯
  18. 18版本        : V1.0
  19. 19描述        : icm20608设备测试APP。
  20. 20其他        : 无
  21. 21使用方法                :./icm20608App /dev/icm20608
  22. 22论坛        : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
  23. 23日志        : 初版V1.0 2019/9/20 左忠凯创建
  24. 24 ***************************************************************/
  25. 25
  26. 26/*
  27. 27  * @description         : main主程序
  28. 28  * @param - argc         : argv数组元素个数
  29. 29  * @param - argv          : 具体参数
  30. 30  * @return                 : 0 成功;其他失败
  31. 31  */
  32. 32int main(int argc,char*argv[])
  33. 33{
  34. 34        int fd;
  35. 35        char*filename;
  36. 36                signedint databuf[7];
  37. 37        unsignedchar data[14];
  38. 38        signedint gyro_x_adc, gyro_y_adc, gyro_z_adc;
  39. 39        signedint accel_x_adc, accel_y_adc, accel_z_adc;
  40. 40        signedint temp_adc;
  41. 41
  42. 42        float gyro_x_act, gyro_y_act, gyro_z_act;
  43. 43        float accel_x_act, accel_y_act, accel_z_act;
  44. 44        float temp_act;
  45. 45
  46. 46        int ret =0;
  47. 47
  48. 48        if(argc !=2){
  49. 49        printf("Error Usage!\r\n");
  50. 50        return-1;
  51. 51        }
  52. 52
  53. 53        filename = argv[1];
  54. 54        fd = open(filename, O_RDWR);
  55. 55        if(fd <0){
  56. 56        printf("can't open file %s\r\n", filename);
  57. 57        return-1;
  58. 58        }
  59. 59
  60. 60        while(1){
  61. 61        ret = read(fd, databuf,sizeof(databuf));
  62. 62                if(ret ==0){/* 数据读取成功 */
  63. 63                gyro_x_adc = databuf[0];
  64. 64                gyro_y_adc = databuf[1];
  65. 65                gyro_z_adc = databuf[2];
  66. 66                accel_x_adc = databuf[3];
  67. 67                accel_y_adc = databuf[4];
  68. 68                accel_z_adc = databuf[5];
  69. 69                temp_adc = databuf[6];
  70. 70
  71. 71                /* 计算实际值 */
  72. 72                gyro_x_act =(float)(gyro_x_adc)/16.4;
  73. 73                gyro_y_act =(float)(gyro_y_adc)/16.4;
  74. 74                gyro_z_act =(float)(gyro_z_adc)/16.4;
  75. 75                accel_x_act =(float)(accel_x_adc)/2048;
  76. 76                accel_y_act =(float)(accel_y_adc)/2048;
  77. 77                accel_z_act =(float)(accel_z_adc)/2048;
  78. 78                temp_act =((float)(temp_adc)-25)/326.8+25;
  79. 79
  80. 80                printf("\r\n原始值:\r\n");
  81. 81                printf("gx = %d, gy = %d, gz = %d\r\n", gyro_x_adc,
  82. gyro_y_adc, gyro_z_adc);
  83. 82                printf("ax = %d, ay = %d, az = %d\r\n", accel_x_adc,
  84. accel_y_adc, accel_z_adc);
  85. 83                printf("temp = %d\r\n", temp_adc);
  86. 84                printf("实际值:");
  87. 85                printf("act gx = %.2f°/S, act gy = %.2f°/S,
  88. act gz = %.2f°/S\r\n", gyro_x_act, gyro_y_act,
  89. gyro_z_act);
  90. 86                printf("act ax = %.2fg, act ay = %.2fg,
  91. act az = %.2fg\r\n", accel_x_act, accel_y_act,
  92. accel_z_act);
  93. 87                printf("act temp = %.2f°C\r\n", temp_act);
  94. 88        }
  95. 89        usleep(100000);/*100ms */
  96. 90        }
  97. 91        close(fd);/* 关闭文件 */
  98. 92        return0;
  99. 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. 1  KERNELDIR:= /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
  2. ......
  3. 4  obj-m := icm20608.o
  4. ......
  5. 11 clean:
  6. 12$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
复制代码

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

        编译成功以后就会生成一个名为“icm20608.ko”的驱动模块文件。
2、编译测试APP
        在icm20608App.c这个测试APP中我们用到了浮点计算,而I.MX6U是支持硬件浮点的,因此我们在编译icm20608App.c的时候就可以使能硬件浮点,这样可以加速浮点计算。使能硬件浮点很简单,在编译的时候加入如下参数即可:
  1. -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 icm20608App
复制代码

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

        结果如图62.6.1.1所示:
image002.jpg

图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这个驱动模块。
  1. depmod                                //第一次加载驱动的时候需要运行此命令
  2. modprobe icm20608.ko        //加载驱动模块
复制代码

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

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

图62.6.2.1 获取到的ICM20608数据

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


出16170入6148汤圆

发表于 2020-7-9 19:44:20 来自手机 | 显示全部楼层
打赏!

庆祝论坛“打赏”功能实施, 现在开始发技术主题,可以获得打赏
https://www.amobbs.com/thread-5735948-1-1.html
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-19 10:38

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

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