26.2Linux中SPI的驱动实验(编程)_csdn
我尽量讲的更详细,为了关注我的粉丝!!!
这里我们用到的是stm32mp157的板子,所以我们看一下SPI用到的引脚。
1、硬件原理图分析
- SPI1_MOSI(对应芯片引脚 SDA/SDI ):主机输出从机输入线,主机通过此线向从机发送数据
- SPI1_MISO(对应芯片引脚 AD0/SDO ):主机输入从机输出线,从机通过此线向主机返回数据
其中这个ICM20608也会使用I2C来通信传输数据,所以有SDA/AD0。
现在我们已经知道所要用的IO口,所以要修改设备树和镜像文件!
2、修改镜像
2.1、添加或者查找 ICM20608 所使用的 IO 的 pinmux 配置
这里的4个IO口不是普通的GPIO口,而是传输数据的。所以要配置pinctrl。
打开stm32mp15-pinctrl.dtsi 文件:
在stm32mp15-pinctrl.dtsi 文件中,默认是没有编写片选信号NSS的!
所以要添加!
我们之前讲为GPIO口的不需要编写PINMUX信息!
但是:
对于 SPI 从机选择信号这类非一般 GPIO 功能的引脚,pinmux
配置是必不可少的。它能确保引脚被正确配置为所需的复用功能,同时还能对引脚的其他属性(如驱动模式、上拉电阻、初始电平、压摆率等)进行设置,从而保证 SPI 通信的稳定和可靠。
当然:
在 SPI 通信中,当 SPI 从机选择信号引脚不需要复用其他功能,且芯片默认该引脚的功能就是作为 SPI 从机选择信号时,通常不需要进行pinmux
配置。我们这里就配置为复用功能!
编译:
make uImage LOADADDR=0XC2000040 -j8 //编译内核
复制给开发板:(可以像我这样复制)
sudo cp arch/arm/boot/uImage /home/chensir/linux/tfboot/ -f
3、修改设备树
3.1、在 SPI1 节点下添加 pinmux 并追加 icm20608 子节点
在镜像中,还有东西没有配置完,需要弄节点,让驱动代码匹配,并获取地址。
打开stm32mp157d-atk.dts 文件。
你看上面都有cs-gpios了其实也没必要编写NSS/CS的PINMUX配置!
编译:
make dtbs
复制给开发板:
sudo cp arch/arm/boot/dts/stm32mp157d-atk.dtb /home/chensir/linux/tfboot/ -f
4、编写 ICM20608 驱动
之前的博客也是跟大家按照肌肉记忆来编写程序!一步一步按照思路来编写!
总代码会放在最后。
为了让大家更能明白,可以先对着总代码,进行对我的写代码流程更加详细得当!
在这里我们可以先讲一下spi设备驱动的流程:(主机驱动是原SOC厂商写的)
- 注册驱动:调用
spi_register_driver
向内核 SPI 子系统注册驱动。 - 设备匹配:内核遍历设备,按规则找匹配设备,匹配上则调用
probe
函数。 probe
处理:分配内存存设备信息,初始化 SPI 设备模式等,按需分配其他资源。- 数据传输:定义
spi_transfer
和spi_message
,用spi_sync
或spi_async
传输。 remove
处理:设备移除或驱动卸载时,释放probe
分配的资源。- 注销驱动:调用
spi_unregister_driver
注销驱动。
先在 icm20608reg.h 中定义好icm20608的寄存器,icm20608reg.h没什么好讲的,就是一些寄存器宏定义。
输入如下内容:(有点长,就直接放代码了!不放图片)
#ifndef ICM20608_H
#define ICM20608_H
#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
这个不就跟裸机代码keil一样啦!所以这里没什么好讲的!
接下来就是讲编写驱动代码了!
4.1、头文件
可以看到下面的头文件比以前的多了spi.h和icm20608reg.h。
#include <linux/spi/spi.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/errno.h>
#include <linux/platform_device.h>
#include "icm20608reg.h"
#include <linux/gpio.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
4.2、驱动注册和注销
对于 SPI驱动而言,没有像 misc
子系统、input子系统那样直接提供完全自动创建设备号、cdev
、device
、class
的机制。
但是我们可以像之前的文章中module_platform_driver类似写module_spi_driver来实现init和exit一体化。也就是不需要自己编写init和exit了。其中之前写I2C驱动也是module_i2c_driver;
这里的icm20608_driver是定义为i2c_driver的结构体,类似platform_driver。
举例:(这里并不要我们写!)
把spi_register_driver和spi_unregister_driver都封装了!
4.2.1、编写SPI_driver驱动结构体
其中流程是:
22行代码:设备树中的compatible值会与icm20608_of_match下的compatible相匹配。
如下图icm20608_of_match的代码:
其中MODULE_DEVICE_TABLE是声明一下而已!
21行代码:会在driver目录下生成icm20608。这个是驱动开发者自己编写的!
和设备树中的compatible没有关系。
20行代码:一般将其设置为 THIS_MODULE
,这表明该驱动属于当前的内核模块。
24行代码:id_table
是一个设备 ID 表,用于传统的设备匹配方式(不使用设备树时)。icm20608_id
是一个预先定义好的 ID 表。
17~18行代码:compatible值一旦匹配成功,就会执行probe和remove。(这些其实之前的驱动程序讲解已经讲过很多遍了)
4.2.2、编写probe和remove函数
这里就用于之前的设备注册和注销,它并不能像MISC和INPUT子系统一样自动注册和注销设备号、cdev、class、device。它这里和I2C的差不多!
这里可以很明显发现和platform驱动、I2C驱动不同;
static int miscbeep_probe(struct platform_device *pdev)static int miscbeep_remove(struct platform_device *dev)static int ap3216c_probe(struct i2c_client *client,const struct i2c_device_id *id)static int ap3216c_remove(struct i2c_client *client)
但是道理都一样,传递进去的数据是设备,区别就是数据分别是I2C设备和platform设备,还有本次spi设备驱动用到的spi_device。
4.2.3、注册和注销字符驱动设备
在这之前需要定义相关的设备结构体,为了统一用到的数据。
1、定义设备结构体
这里我们不用:
struct icm20608_dev icm20608dev
因为如果有多个从设备的话,难以处理多个设备实例的情况;会增加代码复杂度。
用上面这个传递会使系统会在栈上创建该结构体的一个副本。这样的拷贝操作会带来额外的性能开销。
==当然如果驱动代码全文没有用struct icm20608_dev icm20608dev这种形式的,就一定要为它分配内存!==这种形式已经是全局的 struct icm20608_dev
结构体实例,指针 dev
直接指向它,无需额外分配内存。
后面可以通过:
例如:
struct icm20608_dev *icm20608dev;
通过指针变量来修改设备结构体的子类内容。传递指针(即结构体的地址),只需要传递一个固定大小(通常是 4 字节或 8 字节,取决于系统的架构)的地址值,避免了大量数据的拷贝,提高了程序的运行效率。
这里对于结构体指针不熟练的同学尤为重要的是:
例如struct icm20608_dev *dev=&my_device;
其中dev存储的是my_device变量的地址,当使用printf展示dev和&my_device时地址是一样的,但是&dev是存储的dev变量自身的地址!
分配内存:
2、申请设备号
其中宏定义:
同理注销设备号:
3、配置cdev
配置设备结构体:
初始化cdev:
这些都是之前的文章讲遍了的!
THIS_MODULE:让dev在本模块执行。(cdev本身就是执行到dev)
&icm20608_ops:字符操作集,与用户空间信息交互。
举例:(这里了解一下即可)
对字符操作集进行操作:
添加cdev:
同理注销cdev:
4、配置class和device
配置相关设备结构体:
创建class和device:
这里之前也讲过:作用就是在dev目录下生成icm20608。
同理注销class和device:
当然到了这里下面怎么写代码呢?
如果以前的文章,同学们也在跟的话,就不难发现:
我们在设备树下定义了片选信号CS/NSS的gpio信息。
按照以前我们是要获取这个节点信息下的gpio号的,然后对这个PZ3口进行配置的!
但是我看了总的驱动代码并没有获取节点的内容!
问题出现在哪呢???我百思不得其解;
终于我找到答案了!
CS 通过标注 “DNP”(不安装、不连接)的电阻 R43 接地,实际电路中若 R43 未安装,CS 引脚悬空,片选信号不会永久为 0 ;若 R43 安装,CS 引脚接地,片选信号则永久为 0。
不接地那么是驱动是怎么获取它的属性呢?
可以看到前面:
按照图片中的想法那就是主机驱动,就找spi.c和spi.h代码。按照逻辑,那么这个spi.c就应该存在获取片选信号cs-gpios的函数。
打开主机驱动代码,drivers/spi/spi.c 文件中:、
请注意这里都是分析而已,不需要你编写!
发现确实有of_gpio_named_count,这个都是老朋友了。
这段代码主要用于为 spi_device
结构体中的片选(Chip Select,CS)GPIO 相关成员赋值,具体是根据 spi_controller
结构体中是否存在 GPIO 描述符(gpiod
)或 GPIO 编号(gpio
)来进行选择。
==到这里就分析完了!==总之不需要我们编写片选信号的时序,什么拉高拉低电平什么的!主机驱动全部给做好了!
接下来就是做字符操作集的相关工作和spi协议的内容!
4.3、配置字符操作集
在完成写完字符操作集之前,我们先来编写SPI协议,发送和接收单个字节或多个字节,并且进阶完成icm20608中陀螺仪和加速度计传感器数据的收集!陀螺仪和加速度计分别有3个寄存器需要配置!
因为我们有2个传感器的收集,所以我们要对一个从设备的6个寄存器进行读取和发送数据!
4.4、初始化spi_device
因为我们会用到spi_device,这个很重要!
在 SPI 通信里,主设备(一般是微控制器或者处理器)和一个或多个从设备相连,spi_device
数据结构能对每个从设备的具体信息进行描述。
spi_device
包含了一系列字段,这些字段用于存储与从设备相关的信息,例如:
- 设备地址:用来标识总线上的特定从设备。
- 时钟频率:明确 SPI 通信时所采用的时钟速率。
- 模式:涵盖 SPI 的工作模式(像 CPOL 和 CPHA)。
- 片选信号:表明用于选择该从设备的片选线。
配置设备结构体:
看上面的spi设备是指针类型的,目前的没有进行结构体实例化,并没有显示初始化!
它是一个指针成员。定义时它只是预留了一个存储地址的空间,本身不是结构体实例,只有在被赋值后才指向实际的struct spi_device
实例
分析!
在上图中,这里probe函数中传过来的spi_device是已经显示初始化的,是由SPI总线动态检测总线上的设备。当检测到新设备时,也会创建并初始化一个spi_device
实例。
所以我们要赋值!
继续写代码!
正点原子教程有好多知识没有提到!
这里跟i2c协议不一样的是,spi协议有4种工作模式!主机驱动只是提供了api函数接口,供驱动开发者去配置,去初始化spi设备!
我们这里可以分析主机驱动!
分析!
大概配置这两种相关的即可!
在probe
函数中,从设备驱动设置spi_device
结构体的参数(如工作模式、时钟频率等),然后调用spi_setup
函数,该函数会调用主机驱动的接口来完成从设备的实际配置。
这里我们选择时钟线高电平且第一个跳变沿(上升沿或下降沿)采集数据!
接下来我们就写spi协议!
4.5、从icm20608读取多个寄存器数据
接下来 定义spi_transfer
和 spi_message
,用 spi_sync
或 spi_async
传输。
因为主要的信息数据在spi_transfer
下的rx和tx,以及len中。所以可以申请内存或者结构体实例化!这里我们选择申请内存!
同时我们要用收集传进来的数据,如寄存器地址和真正的数据结合。
同样定义指针类型并且申请内存!
读取数据当然是rx,写数据当然是tx。
接下来就是读取寄存器的数据了!
写读取寄存器数据代码:
其中真正要的数据就是向外传递的buf指针变量下的数据!
4.6、向 icm20608 多个寄存器写入数据
写入数据就不用rx了。
ret = -1
是预设错误返回值。==默认函数执行失败,==后续若操作成功会更新 ret
;若遇内存分配、SPI 传输等异常,保持或更新为错误码,方便调用者判断函数执行情况,增强代码健壮性与可读性。
4.7、读取 icm20608 指定寄存器值,读取一个寄存器
4.8、向 icm20608 指定寄存器写入指定的值,写一个寄存器
可以看到上面已经把读取数据和写入数据已经封装好了!
5、编写icm20608的加速度计、陀螺仪以及内部温度传感器数据
我们这里只编写读取数据,观察传感器数据!
我们先配置相关的设备结构体:
5.1、读取 icm20608 的数据,包括 陀螺仪、加速度计、内部温度
因为这里我们要读取7个数据。如上一个文章所说:
这些都是icm20608上7个寄存器的不同地址。
在之前icm20608reg.h编写时就可以发现:
我们要针对这些地址进行读取数据!
这里有个东西非常重要!
ICM20608 这类传感器芯片的寄存器通常是连续排列的。当对某个寄存器地址进行读写操作时,若通信协议支持,在完成该寄存器的操作后,地址会自动递增,从而能够连续访问后续的寄存器。
以 ICM20608 为例,加速度计、陀螺仪,内部温度等数据分别存于连续的寄存器中。ICM20_ACCEL_XOUT_H
是加速度 X 轴数据高位寄存器的地址,后续 13 个寄存器依次存储着加速度 X 轴数据低位、加速度 Y 轴数据、加速度 Z 轴数据、陀螺仪 X 轴数据、陀螺仪 Y 轴数据、陀螺仪 Z 轴数据等。所以,从 ICM20_ACCEL_XOUT_H
开始,可以连续读取 14 个寄存器的数据。
原理讲了这么多,现在我们就开始写代码!
代码的解释都在上面写了噢!
5.2、ICM20608 内部寄存器初始化
我们回顾其他的ICM20608 内部寄存器。看它们的地址!
接下来我们编写初始化代码:
FIFO 是一种数据缓冲机制,在 ICM20608 中表现为一个具有特定深度(通常可容纳一定数量的数据帧)的存储区域。它遵循先进先出的原则,即先存入的数据会先被取出,如同排队一样,先到先得。这里我们关掉FIFO!
6、编写字符操作集
我们现在其实已经完成了关于SPI的读取和写入!但是为了可以通过执行用户程序得到传感器数据,所以得编写字符操作集,完成传感器数据的信息交互。
6.1、编写open函数
其实我们可以把初始化其它寄存器的代码放在open的操作子函数中!
跟i2c驱动那里学的代码一样!
例如类似下面回顾I2C驱动代码:
这里只是回顾!不需要写代码!
驱动开发者通常会将设备的私有数据封装在自定义的设备结构体中,而 struct cdev
是内核可见的公共部分。通过 cdev
来间接访问设备结构体,可以避免在通用的字符设备操作流程中直接暴露设备的私有数据,提高了代码的安全性和可维护性。
这种方式使得驱动的各个部分可以更好地进行模块化设计。例如,内核的字符设备管理模块只需要处理 cdev
相关的操作==,而驱动开发者可以在自定义设备结构体中添加任意的私有数据和状态,两者之间通过 cdev
进行关联,互不干扰==。
在设备驱动的生命周期中,设备结构体可能是动态分配和释放的。内核只需要管理 cdev
的注册和注销,而具体的设备结构体可以由驱动自行管理。通过 cdev
来关联设备结构体,使得设备的动态管理更加灵活。
因为我们已经将icm20608初始化了!
这里就不用写了!
当然也可以放私有数据、原子变量或者自旋锁来保证驱动的独立性和保护!
6.2、编写read函数
这里就要将3个传感器数据读取了!
6.2、编写release函数
大家注意,在内核中尽量不要使用浮点运算,所以不要在驱动将 icm20608 的原始值转换为对应的实际值,因为会涉及到浮点计算。我们在应用程序中使用浮点运算即可!
这个驱动例程还不是很完美,在 icm20608_read 没有加锁。如果多个程序去读取这个驱动的时候就会出现读取数据出错,有能力的可以把这一点补充完整。
发现我们还没在probe中执行初始化ICM20608 内部寄存器。
还要将SPI设备中的spi_device与自己定义的设备结构体关联起来!
这里跟I2C驱动出不多!忘记了的同学往回看!
因为我们用的设备结构体函数是没有直接实例化的!所以要在remove中定义icm20608设备结构体的地址,这里我们spi_get_drvdata(spi);
补充错误类型:
驱动代码这里就全部讲完了!
下面我们开始写APP.代码!
7、编写测试 APP
这里其实也简单,就是传递7个数据!
核心代码:
7.1、SPI 控制器的驱动编译成模块
我们还有一些准备工作没有准备好!
就是SPI控制器还没有使能!并没有编译进内核!
在 Linux 内核模块 Makefile 中,M
设置为某目录(如 M=/home/chensir/linux/linux_Drivers/22_spi
),表示将该目录下模块源码编译成可加载的外部 .ko
模块,方便开发调试。改为 *
一般指把相关代码直接编译进内核,成为内核一部分,减少加载开销但修改更新需重编整个内核。
为了方便,我把代码放这里!
arm-none-linux-gnueabihf-gcc -march=armv7-a -mfpu=neon -mfloat-abi=hard icm20608App.c -o icm20608App
arm-none-linux-gnueabihf-readelf -A icm20608App
8、效果
正点原子的STM32MP157的板子,icm20608芯片的位置并不好找!
如下图:
当我们改变位置:
9、总代码:
icm20608.c:
#include <linux/spi/spi.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/errno.h>
#include <linux/platform_device.h>
#include "icm20608reg.h"
#include <linux/gpio.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>#define ICM20608_CNT 1
#define ICM20608_NAME "icm20608"/*icm20608设备结构体*/
struct icm20608_dev{dev_t devid;int major;int minor;struct cdev cdev;struct class *class;struct device *device;struct spi_device *spi; /* spi 设备 */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; /* 温度原始值 */
};/*从 icm20608 读取多个寄存器数据*/
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg,void *buf, int len)
{int ret = -1;unsigned char *rxdata;unsigned char txdata[1];struct spi_message m;struct spi_transfer *t;/*若设备结构体中spi为void类型则需要按照下面强转类型,不是void类型情况下按照下面的代码是为了增强可读性,更加简洁明了每次都写 dev->spi->xxx 会显得代码冗长*/struct spi_device *spi = (struct spi_device *)dev->spi;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; /* 要读取的数据,从机会给主机发送信息给spi_transfer下的rx_buf子类*/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;}/* 只需要读取的数据 第一个字节可能代表从机的地址、设备 ID 或者其他标识信息。*/memcpy(buf , rxdata+1, len);
out2:kfree(rxdata); /* 释放内存 */
out1:kfree(t); /* 释放内存 */return ret;
}/*向 icm20608 多个寄存器写入数据*/
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->spi;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_message 队列 */ret = spi_sync(spi, &m); /* 同步发送 */if(ret) {goto out2;}
out2:kfree(txdata); /* 释放内存 */
out1:kfree(t); /* 释放内存 */return ret;
}/*读取 icm20608 指定寄存器值,读取一个寄存器*/
static unsigned char icm20608_read_onereg(struct icm20608_dev *dev,u8 reg)
{u8 data = 0;icm20608_read_regs(dev, reg, &data, 1);return data;
}/*向 icm20608 指定寄存器写入指定的值,写一个寄存器*/
static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg,u8 value)
{u8 buf = value;icm20608_write_regs(dev, reg, &buf, 1);
}/*读取 ICM20608 的数据,读取原始数据,包括三轴陀螺仪、三轴加速度计和内部温度*/
void icm20608_readdata(struct icm20608_dev *dev)
{unsigned char data[14];icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);/*icm20608寄存器访问机制中有自动地址递增功能,并且数据高8位地址比低8位地址小*/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]);
}/*ICM20608 内部寄存器初始化函数*/
void icm20608_reginit(struct icm20608_dev *dev)
{u8 value = 0;// 复位设备icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x80);mdelay(50);// 唤醒设备并选择时钟源/*PWR_MGMT_1 寄存器的第 2、1、0 位(CLKSEL[2:0])用于选择时钟源,不同的组合对应不同的时钟源选项,0x01是陀螺仪X轴参考时钟*/icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x01);mdelay(50);// 读取设备 ID 以验证设备连接value = icm20608_read_onereg(dev, ICM20_WHO_AM_I);printk("ICM20608 ID = %#X\r\n", value);// 设置采样率分频寄存器 表示不分频,即使用最高采样率icm20608_write_onereg(dev, ICM20_SMPLRT_DIV, 0x00);// 配置陀螺仪 0x18 通常表示 ±2000°/s 的满量程范围icm20608_write_onereg(dev, ICM20_GYRO_CONFIG, 0x18);// 配置加速度计 0x18 通常表示 ±16g 的满量程范围icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG, 0x18);// 配置数字低通滤波器 配置数字低通滤波器,设置滤波器的截止频率icm20608_write_onereg(dev, ICM20_CONFIG, 0x04);// 配置加速度计的 FIFO 和数字低通滤波器icm20608_write_onereg(dev, ICM20_ACCEL_CONFIG2, 0x04);// 配置电源管理 2 寄存器icm20608_write_onereg(dev, ICM20_PWR_MGMT_2, 0x00);// 配置低功耗模式icm20608_write_onereg(dev, ICM20_LP_MODE_CFG, 0x00);// 禁用 FIFOicm20608_write_onereg(dev, ICM20_FIFO_EN, 0x00);
}static int icm20608_open(struct inode *inode, struct file *filp)
{return 0;
}static ssize_t icm20608_read(struct file *filp, char __user *buf,size_t cnt, loff_t *off)
{signed int data[7];long err = 0;/*read 函数中,通过 filp->f_path.dentry->d_inode->i_cdev 获取到字符设备的 struct cdev 指针*通过Linux 内核采用分层架构设计,各个子系统和模块之间需要保持一定的独立性和抽象性在字符设备操作函数(如 open、read、write 等)中,内核传递给驱动的参数是通用的,比如 struct inode 和 struct file 指针。这些参数中只包含了与文件系统和字符设备通用管理相关的信息,并不直接包含驱动自定义设备结构体的指针。内核通过 inode->i_cdev 提供了一个统一的接口,让驱动可以获取到字符设备的核心管理结构,然后再由驱动自己去关联到具体的设备数据*内核是分层架构,用 cdev 统一管理字符设备,操作函数参数通用,只能通过 cdev 关联具体设备数据。*/struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;struct icm20608_dev *dev = container_of(cdev, struct icm20608_dev,cdev);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;
}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,
};/*spi 驱动的 probe 函数*/
static int icm20608_probe(struct spi_device *spi)
{int ret;/*分配内存*/struct icm20608_dev *icm20608dev;icm20608dev = devm_kzalloc(&spi->dev, sizeof(*icm20608dev),GFP_KERNEL);if(!icm20608dev){return -ENOMEM;}/* 注册字符设备驱动 *//*1->申请设备号*/icm20608dev->major=0;if(icm20608dev->major){//若给定主设备号icm20608dev->devid=MKDEV(icm20608dev->major,0);ret=register_chrdev_region(icm20608dev->devid,ICM20608_CNT,ICM20608_NAME);}else {//若未给定主主设备号ret=alloc_chrdev_region(&icm20608dev->devid,0,ICM20608_CNT,ICM20608_NAME);icm20608dev->major=MAJOR(icm20608dev->devid);icm20608dev->minor=MINOR(icm20608dev->devid);}if(ret < 0){//注册字符设备驱动失败goto fail_devid;}printk("major = %d,minor = %d,CNT = %d,NAME = %s\r\n",icm20608dev->major,icm20608dev->minor,ICM20608_CNT,ICM20608_NAME);/*2->初始化cdev*/icm20608dev->cdev.owner=THIS_MODULE;cdev_init(&icm20608dev->cdev,&icm20608_ops);/*3->添加cdev*/ret=cdev_add(&icm20608dev->cdev,icm20608dev->devid,ICM20608_CNT);if(ret < 0) {goto fail_cdev;}/*4->创建类class*/icm20608dev->class = class_create(THIS_MODULE,ICM20608_NAME);//创建/dev/icm20608if(IS_ERR(icm20608dev->class)){ret = PTR_ERR(icm20608dev->class);goto fail_class;}/*5->创建设备节点device*/icm20608dev->device = device_create(icm20608dev->class,NULL,icm20608dev->devid,NULL,ICM20608_NAME);//创建/dev/icm20608if(IS_ERR(icm20608dev->device)){ret = PTR_ERR(icm20608dev->device);goto fail_device;}/*6->将设备结构体中的spi设备实例化赋值*/icm20608dev->spi = spi;/*7->初始化 spi_device*/spi->mode = SPI_MODE_0; /*MODE0, CPOL=0, CPHA=0*/spi_setup(spi);/*8->初始化ICM20608 内部寄存器*/icm20608_reginit(icm20608dev);/*9->保存 icm20608dev 结构体 */spi_set_drvdata(spi, icm20608dev);return 0;
fail_device:class_destroy(icm20608dev->class);
fail_class:cdev_del(&icm20608dev->cdev);
fail_cdev:unregister_chrdev_region(icm20608dev->devid,ICM20608_CNT);
fail_devid:return ret;
}/*spi 驱动的 remove 函数*/
static int icm20608_remove(struct spi_device *spi)
{/*获取与spi_device关联的自定义数据指针*/struct icm20608_dev *icm20608dev = spi_get_drvdata(spi);/*注销字符设备驱动*//*注销设备device*/device_destroy(icm20608dev->class,icm20608dev->devid);/*注销类class*/class_destroy(icm20608dev->class);/*注销字符设备对象*/cdev_del(&icm20608dev->cdev);/*注销设备号*/unregister_chrdev_region(icm20608dev->devid,ICM20608_CNT);return 0;
}/* 传统匹配方式 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 */ }
};
MODULE_DEVICE_TABLE(of,icm20608_of_match);/*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,
};/*驱动注册和注销一体化*/
module_spi_driver(icm20608_driver);MODULE_LICENSE("GPL");
MODULE_AUTHOR("chensir");
MODULE_INFO(intree, "Y");
icm20608reg.h:
#ifndef ICM20608_H
#define ICM20608_H#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
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>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;
}
makefile:
KERNELDIR := /home/chensir/linux/atk-mp1/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)
obj-m := icm20608.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean