本系列文章驱动源码仓库,欢迎Star~
https://github.com/Mculover666/linux_driver_study。
一、ICM20608
1. 简介
InvenSense 的 ICM-20608 是一款 6 轴运动跟踪器件(MEMS传感器),也是 MPU-6500 的后续产品,集成了3轴加速度计和3轴陀螺仪。
相比以前的 6 轴器件,Invensense 的 ICM 20608 具有更低的功耗和噪声并采用更薄的封装。 该器件为陀螺仪提供了一种占空比工作模式,相比以前的 6 轴器件能将陀螺仪的功耗降低一半或一半以上(具体视 ODR 而定)。 此外,该器件的噪声比以前的器件降低约 20%,封装薄约 17%。
2. 功能使用
3. Alpha开发板原理图
ICM-20608传感器使用SPI3,引脚连接情况如下:
传感器引脚 | iMX6ULL引脚 |
---|---|
CS | ECSPI3_SS0 |
SCLK | ECSPI3_SCLK |
SDI | ECSPI3_MOSI |
SDO | ECSPI3_MISO |
二、添加设备树节点
1. 设置SPI3引脚
首先设置 SPI3 引脚的复用功能和电气属性,在 iomuxc 节点中添加:
pinctrl_spi3: spi3grp {
fsl,pins = <
MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20 0x10b0
MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK 0x10b1
MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO 0x10b1
MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI 0x10b1
>;
};
其中UART2_TX_DATA这个引脚设置为普通IO是为了手动控制SPI片选信号。
检查这四个引脚有没有复用,以 MX6UL_PAD_UART2_TX_DATA 引脚为例,已经被uart2使用:
所以这里我们要找到 pinctrl_uart2
的引用,将uart2节点先屏蔽:
同样的方法,解决其它引脚 MX6UL_PAD_UART2_RTS_B 和 MX6UL_PAD_UART2_CTS_B 冲突问题。
这两个引脚作为FLEXCAN2的引脚使用,将该节点先注释了:
2. 添加ICM-20608设备节点
添加对ecspi3节点的补充描述:
&ecspi3 {
fsl,spi-num-chipselects = <1>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_spi3>;
status = "okay";
mems_spi: icm20608@0 {
compatible = "atk,icm20608";
spi-max-frequency = <8000000>;
reg = <0>;
cs-gpio = <&gpio1 20 GPIO_ACTIVE_LOW>;
};
};
重新编译设备树,使用新的设备树启动,查看设备树是否有新添加的节点:
三、编写ICM-20608设备驱动
1. 先写个模块
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
static int __init icm20608_module_init(void)
{
return 0;
}
static void __exit icm20608_module_exit(void)
{
}
module_init(icm20608_module_init)
module_exit(icm20608_module_exit)
MODULE_AUTHOR("Mculover666");
MODULE_LICENSE("GPL");
写个Makefile编译一下:
KERNEL_DIR = /home/mculover666/imx6ull/kernel/linux-imx6ull
obj-m = icm20608.o
build: kernel_module
kernel_module:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
2. 再搭个spi驱动框架
包含头文件:
#include <linux/spi/spi.h>
完成 spi 设备驱动基本框架:
static int icm20608_probe(struct spi_device *spi)
{
return 0;
}
static int icm20608_remove(struct spi_device *spi)
{
return 0;
}
/* 设备树匹配 */
static const struct of_device_id icm20608_of_match[] = {
{
.compatible = "atk,icm20608" },
{
},
};
/* 传统id方式匹配 */
static const struct spi_device_id icm20608_id[] = {
{
"atk,icm20608", 0 },
{
},
};
/**
*@brief 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;
};
static int __init icm20608_module_init(void)
{
int ret;
/* 注册spi_driver */
ret = spi_register_driver(&icm20608_driver);
if (ret < 0) {
printk("spi_register_driver fail!\n");
return -1;
}
return 0;
}
static void __exit icm20608_module_exit(void)
{
/* 注销spi_driver */
spi_unregister_driver(&icm20608_driver);
}
3. 再写字符设备驱动框架
引入头文件:
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
添加字符设备驱动框架相关代码:
struct icm20608_dev {
dev_t dev;
struct cdev *cdev;
struct class *class;
struct device *device;
};
static struct icm20608_dev icm20608;
static int icm20608_open(struct inode *node, struct file *fp)
{
return 0;
}
static int icm20608_read(struct file *fp, char __user *buf, size_t size, loff_t *off)
{
return 0;
}
static int icm20608_write(struct file *fp, const char __user *buf, size_t size, loff_t *off)
{
return 0;
}
static int icm20608_release(struct inode *node, struct file *fp)
{
return 0;
}
static struct file_operations icm20608_fops = {
.owner = THIS_MODULE,
.open = icm20608_open,
.read = icm20608_read,
.write = icm20608_write,
.release = icm20608_release,
};
static int icm20608_probe(struct spi_device *spi)
{
int ret;
// 申请设备号
ret = alloc_chrdev_region(&icm20608.dev, 0, 1, "icm20608");
if (ret != 0) {
printk("alloc_chrdev_region fail!");
return -1;
}
// 创建cdev
icm20608.cdev = cdev_alloc();
if (!icm20608.cdev) {
printk("cdev_alloc fail!");
return -1;
}
icm20608.cdev->owner = THIS_MODULE;
icm20608.cdev->ops = &icm20608_fops;
// 注册cdev
cdev_add(icm20608.cdev, icm20608.dev, 1);
// 创建设备类
icm20608.class = class_create(THIS_MODULE, "icm20608");
if (!icm20608.class) {
printk("class_create fail!");
return -1;
}
// 创建设备节点
icm20608.device = device_create(icm20608.class, NULL, icm20608.dev, NULL, "icm20608");
if (IS_ERR(icm20608.device)) {
printk("device_create led_dts_device0 fail!");
return -1;
}
return 0;
}
static int icm20608_remove(struct spi_device *spi)
{
// 将设备从内核删除
cdev_del(icm20608.cdev);
// 释放设备号
unregister_chrdev_region(icm20608.dev, 1);
// 删除设备节点
device_destroy(icm20608.class, icm20608.dev);
// 删除设备类
class_destroy(icm20608.class);
return 0;
}
4. 获取设备树信息
因为本文中我们使用gpio作为片选引脚,手动控制,所以要从设备树中获取gpio引脚信息和spi的一些设置信息,获取信息的代码放到probe函数中。
(1)获取spi节点信息
全局变量中添加node成员:
// 获取设备树中spi节点信息
icm20608.node = of_find_compatible_node(NULL, NULL, "atk,icm20608");
if (!icm20608.node) {
printk("icm20608 node find fail!");
return -1;
}
(2)进一步获取片选引脚gpio信息
全局变量中添加cs_gpio成员:
包含头文件:
#include <linux/of_gpio.h>
#include <linux/gpio.h>
获取gpio引脚信息:
// 进一步获取gpio片选引脚信息
icm20608.cs_gpio = of_get_named_gpio(icm20608.node, "cs-gpio", 0);
if (icm20608.cs_gpio < 0) {
printk("cs-gpio propname in icm20608 node find fail!");
return -1;
}
// 设置gpio引脚方向并默认输出高电平
ret = gpio_direction_output(icm20608.cs_gpio, 1);
if (ret < 0) {
printk("gpio_direction_output fail!");
return -1;
}
5. 封装spi操作代码
(1)spi_device成员
模块全局数据中,添加 spi_device 成员:
在probe函数中,将probe函数传入的spi_device值,存到模块全局数据的spi_devices成员中:
// 存储spi_device值
spi->mode = SPI_MODE_0;
spi_setup(spi);
icm20608.spi = spi;
(2)spi发送然后读取函数封装
static int icm20608_send_then_recv(struct icm20608_dev *dev, uint8_t *send_buf, ssize_t send_len, uint8_t *recv_buf, ssize_t recv_len)
{
int ret;
struct spi_device *spi;
struct spi_message m;
struct spi_transfer *t;
if (!dev) {
return -1;
}
spi = dev->spi;
t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
if (!t) {
printk("spi_transfer kzalloc fail!\n");
return -1;
}
/* 使能片选 */
gpio_set_value(dev->cs_gpio, 0);
/* 发送数据 */
if (send_buf && send_len != 0) {
t->tx_buf = send_buf;
t->len = send_len;
spi_message_init(&m);
spi_message_add_tail(t, &m);
ret = spi_sync(spi, &m);
if (ret < 0) {
printk("spi_sync fail!\n");
goto exit;
}
}
/* 接收数据 */
if (recv_buf && recv_len != 0) {
t->rx_buf = recv_buf;
t->len = send_len;
spi_message_init(&m);
spi_message_add_tail(t, &m);
ret = spi_sync(spi, &m);
if (ret < 0) {
printk("spi_sync fail!\n");
goto exit;
}
}
ret = 0;
/* 禁止片选 */
exit:
gpio_set_value(dev->cs_gpio, 1);
return ret;
}
(3)icm20608寄存器读写
icm20608写寄存器函数:
static int icm20608_write_reg(struct icm20608_dev *dev, uint8_t reg, uint8_t dat)
{
int ret;
uint8_t send_buf[2];
send_buf[0] = reg & (~0x80); // MSB is W(0)
send_buf[1] = dat;
ret = icm20608_send_then_recv(dev, send_buf, 2, NULL, 0);
return ret < 0 ? -1 : 0;
}
icm20608读寄存器函数:
static int icm20608_read_reg(struct icm20608_dev *dev, uint8_t reg, uint8_t *dat)
{
int ret;
uint8_t send_buf;
send_buf = reg | 0x80; // MSB is R(1)
ret = icm20608_send_then_recv(dev, &send_buf, 1, dat, 1);
return ret < 0 ? -1 : 0;
}
6. icm20608板级操作函数
6.1. icm20608初始化函数
需要使用mdelay函数,引入头文件:
#include <linux/delay.h>
操作 PWOER MANAGEMENT 1 寄存器,进行软复位,然后选择时钟:
static int icm20608_board_soft_reset(void)
{
// reset the internal registers and restore the default settings.
icm20608_write_reg(&icm20608, 0x6B, 0x80);
mdelay(50);
// auto select the best available clock source.
icm20608_write_reg(&icm20608, 0x6B, 0x01);
mdelay(50);
return 0;
}
读取芯片ID以检测是否通信正常:
static int icm20608_board_read_id(uint8_t *id)
{
int ret;
ret = icm20608_read_reg(&icm20608, 0x75, id);
return ret < 0 ? -1 : 0;
}
设置芯片内部一些配置:
static int icm20608_board_config(void)
{
icm20608_write_reg(&icm20608, 0x19, 0x00); // SMPLRT_DIV
icm20608_write_reg(&icm20608, 0x1A, 0x04); // CONFIG
icm20608_write_reg(&icm20608, 0x1B, 0x18); // GYRO_CONFIG
icm20608_write_reg(&icm20608, 0x1C, 0x18); // ACCEL_CONFIG
icm20608_write_reg(&icm20608, 0x1D, 0x04); // ACCEL_CONFIG2
icm20608_write_reg(&icm20608, 0x1E, 0x00); // LP_MODE_CFG
icm20608_write_reg(&icm20608, 0x23, 0x00); // FIFO_EN
icm20608_write_reg(&icm20608, 0x6C, 0x00); // PWR_MGMT_2
return 0;
}
整合为Icm20608初始化函数:
static int icm20608_board_init(void)
{
uint8_t id;
if (icm20608_board_soft_reset() < 0) {
printk("icm20608_board_soft_reset fail\n!");
return -1;
}
if (icm20608_board_read_id(&id) < 0) {
printk("icm20608_board_read_id fail\n!");
return -1;
}
if (icm20608_board_config() < 0) {
printk("icm20608_board_config fail\n!");
return -1;
}
printk("icm20608 id: 0x%x\n", id);
return 0;
}
编译,加载,检查是否可以正常读取到芯片id:
6.2. icm20608读取数据函数
首先抽象出icm20608数据结构:
struct icm20608_row_data {
int16_t accel_x;
int16_t accel_y;
int16_t accel_z;
int16_t gyro_x;
int16_t gyro_y;
int16_t gyro_z;
int16_t temperature;
};
添加到模块的全局变量中:
编写数据读取函数:
static int icm20608_board_read_row_data(struct icm20608_dev *dev)
{
int i;
int ret;
uint8_t reg;
uint8_t data[14];
if (!dev) {
return -1;
}
reg = 0x3B;
for (i = 0; i < 14; i++) {
ret = icm20608_read_reg(dev, reg++, &data[i]);
if (ret < 0) {
break;
}
}
if (i < 14) {
printk("icm20608_board_read_row_data fail, i = %d!", i);
return -1;
}
dev->data.accel_x = (int16_t)((data[0] << 8) | data[1]);
dev->data.accel_y = (int16_t)((data[2] << 8) | data[3]);
dev->data.accel_z = (int16_t)((data[4] << 8) | data[5]);
dev->data.gyro_x = (int16_t)((data[8] << 8) | data[9]);
dev->data.gyro_x = (int16_t)((data[10] << 8) | data[11]);
dev->data.gyro_x = (int16_t)((data[12] << 8) | data[13]);
dev->data.temperature = (int16_t)((data[6] << 8) | data[7]);
return 0;
}
7.icm20608驱动实现
open
static int icm20608_open(struct inode *node, struct file *fp)
{
fp->private_data = &icm20608;
return 0;
}
read
static int icm20608_read(struct file *fp, char __user *buf, size_t size, loff_t *off)
{
int ret;
long err = 0;
struct icm20608_dev *dev = (struct icm20608_dev *)fp->private_data;
ret = icm20608_board_read_row_data(dev);
if (ret < 0) {
return -1;
}
err = copy_to_user(buf, &dev->data, sizeof(struct icm20608_row_data));
return 0;
}
write
static int icm20608_write(struct file *fp, const char __user *buf, size_t size, loff_t *off)
{
return 0;
}
close
static int icm20608_release(struct inode *node, struct file *fp)
{
return 0;
}
四、编写驱动测试函数
1. 测试代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
struct icm20608_row_data {
int16_t accel_x;
int16_t accel_y;
int16_t accel_z;
int16_t gyro_x;
int16_t gyro_y;
int16_t gyro_z;
int16_t temperature;
};
struct icm20608_row_data data;
int icm20608_read_data(char *filename)
{
int fd;
int ret;
float gyro_x_act, gyro_y_act, gyro_z_act;
float accel_x_act, accel_y_act, accel_z_act;
float temp_act;
if (!filename) {
return -1;
}
// 打开设备文件
fd = open(filename, O_RDWR);
if (fd < 0) {
printf("open %s error!\n", filename);
return 0;
}
// 读文件
ret = read(fd, &data, sizeof(data));
if (ret != 0) {
printf("read fail!\n");
close(fd);
return -1;
} else {
printf("accel x:%d, y:%d, z:%d\n", data.accel_x, data.accel_y, data.accel_z);
printf("gyro x:%d, y:%d, z:%d\n", data.gyro_x, data.gyro_y, data.gyro_z);
printf("temperature: %d\n", data.temperature);
}
// 计算实际值
gyro_x_act = (float)(data.gyro_x) / 16.4;
gyro_y_act = (float)(data.gyro_y) / 16.4;
gyro_z_act = (float)(data.gyro_z) / 16.4;
accel_x_act = (float)(data.accel_x) / 2048;
accel_y_act = (float)(data.accel_y) / 2048;
accel_z_act = (float)(data.accel_z) / 2048;
temp_act = ((float)(data.temperature) - 25 ) / 326.8 + 25;
printf("act accel x:%.2f, y:%.2f, z:%.2f\n", accel_x_act, accel_y_act, accel_z_act);
printf("act gyro x:%.2f, y:%.2f, z:%.2f\n", gyro_x_act, gyro_y_act, gyro_z_act);
printf("act temperature: %.2f\n", temp_act);
// 关闭文件
close(fd);
return 0;
}
int main(int argc, char *argv[])
{
uint32_t interval;
// 检查参数
if (argc != 3) {
printf("usage: ./test_icm20608 [device] [read interval(s)]\n");
return -1;
}
interval = atoi(argv[2]);
if (interval < 1) {
interval = 1;
}
while (1) {
icm20608_read_data(argv[1]);
sleep(interval);
}
}
2. 测试结果
编译:
arm-linux-gnueabihf-gcc test_icm20608.c -o test_icm20608