DeviceDriver(13):SPIドライバー

1つ:SPIドライバーフレームワークの概要

SPIドライバーフレームワークはI2Cに似ており、ホストコントローラードライバーとデバイスドライバーに分かれています。

1.SPIホストドライバー

SPIホストドライバーはSOCのSPIコントローラードライバーです。Linuxカーネルはspi_masterを使用してSPIホストドライバーを表します。

struct spi_master {
	struct device	dev;
	struct list_head list;
	s16			bus_num;
	u16			num_chipselect;
	u16			dma_alignment;
	/* spi_device.mode flags understood by this controller driver */
	u16			mode_bits;
	/* bitmask of supported bits_per_word for transfers */
	u32			bits_per_word_mask;
... ...
	/* limits on transfer speed */
	u32			min_speed_hz;
	u32			max_speed_hz;
... ...
	/* other constraints relevant to this driver */
	u16			flags;


	/* lock and mutex for SPI bus locking */
	spinlock_t		bus_lock_spinlock;
	struct mutex		bus_lock_mutex;

	bool			bus_lock_flag;

	int			(*setup)(struct spi_device *spi);

	int			(*transfer)(struct spi_device *spi,
						struct spi_message *mesg);

... ...
	int (*transfer_one_message)(struct spi_master *master,
				    struct spi_message *mesg);
... ...
};

伝達関数は、コントローラーデータ伝達関数としてのi2cのmaster_xfer関数と同じです。transfer_one_message関数は、SPIデータ送信にも使用されます。spi_messageの送信に使用されます。SPIデータはspi_messageにパッケージ化されてから、キューに送信されます。

SPIホストドライバーのコアは、spi_masterを申請し、spi_masterを初期化し、最後にspi_masterをカーネルに登録することです。

(1)spi_masterのアプリケーションとリリース

struct spi_master *spi_alloc_master(struct device *dev, unsigned size)

static inline void spi_master_put(struct spi_master *master)

(2)spi_masterの登録とキャンセル

int spi_register_master(struct spi_master *master)

void spi_unregister_master(struct spi_master *master)

2.SPIデバイスドライバー

Linuxカーネルは、spi_driver構造体を使用してspiデバイスドライバーを表します。これには、以下を実装する必要があります。

struct spi_driver {
	const struct spi_device_id *id_table;
	int			(*probe)(struct spi_device *spi);
	int			(*remove)(struct spi_device *spi);
	void			(*shutdown)(struct spi_device *spi);
	struct device_driver	driver;
};

spiデバイスとドライバが正常に一致すると、プローブ機能が実行されることがわかります。

spi_driverが初期化された後、Linuxカーネルに登録する必要があります。

int spi_register_driver(struct spi_driver *sdrv)

ログアウト機能:

void spi_unregister_driver(struct spi_driver *sdrv)

spi_driver登録の例:

/* probe 函数 */
static int xxx_probe(struct spi_device *spi)
{
	/* 具体函数内容 */
	return 0;
}

/* remove 函数 */
static int xxx_remove(struct spi_device *spi)
{
	/* 具体函数内容 */
	return 0;
}
/* 传统匹配方式 ID 列表 */
static const struct spi_device_id xxx_id[] = {
	{"xxx", 0},
	{}
};

/* 设备树匹配列表 */
static const struct of_device_id xxx_of_match[] = {
	{ .compatible = "xxx" },
	{ /* Sentinel */ }
};

/* SPI 驱动结构体 */
static struct spi_driver xxx_driver = {
	.probe = xxx_probe,
	.remove = xxx_remove,
	.driver = {
		.owner = THIS_MODULE,
		.name = "xxx",
		.of_match_table = xxx_of_match,
	},
	.id_table = xxx_id,
};

/* 驱动入口函数 */
static int __init xxx_init(void)
{
	return spi_register_driver(&xxx_driver);
}

/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
	spi_unregister_driver(&xxx_driver);
}

module_init(xxx_init);
module_exit(xxx_exit);

3.SPIデバイスとドライバーのマッチングプロセス

spiデバイスとドライバーのマッチングプロセスは、spiバスによって完了します。

struct bus_type spi_bus_type = {
	.name		= "spi",
	.dev_groups	= spi_dev_groups,
	.match		= spi_match_device,
	.uevent		= spi_uevent,
};

spiのマッチング関数:

static int spi_match_device(struct device *dev, struct device_driver *drv)
{
	const struct spi_device	*spi = to_spi_device(dev);
	const struct spi_driver	*sdrv = to_spi_driver(drv);

	/* Attempt an OF style match */
	if (of_driver_match_device(dev, drv))
		return 1;

	/* Then try ACPI */
	if (acpi_driver_match_device(dev, drv))
		return 1;

	if (sdrv->id_table)
		return !!spi_match_id(sdrv->id_table, spi);

	return strcmp(spi->modalias, drv->name) == 0;
}

2:SPIホストドライバー分析

ハードウェア情報によると、デバイスはimx6ullシリーズ開発ボードのspi3インターフェイスに接続され、共通デバイスツリーファイルでspi3ノードにクエリを実行します。

ecspi3: ecspi@02010000 {
	#address-cells = <1>;
	#size-cells = <0>;
	compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
	reg = <0x02010000 0x4000>;
	interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
	clocks = <&clks IMX6UL_CLK_ECSPI3>,
		 <&clks IMX6UL_CLK_ECSPI3>;
	clock-names = "ipg", "per";
	dmas = <&sdma 7 7 1>, <&sdma 8 7 2>;
	dma-names = "rx", "tx";
	status = "disabled";
};

互換性のある属性によると、LinuxカーネルでSPI3ホストコントローラードライバーを見つけることができます。

static struct platform_device_id spi_imx_devtype[] = {
	 ... ...
        {
		.name = "imx51-ecspi",
		.driver_data = (kernel_ulong_t) &imx51_ecspi_devtype_data,
	}, {
		.name = "imx6ul-ecspi",
		.driver_data = (kernel_ulong_t) &imx6ul_ecspi_devtype_data,
	}, {
		/* sentinel */
	}
};

static const struct of_device_id spi_imx_dt_ids[] = {
    ... ...
	{ .compatible = "fsl,imx51-ecspi", .data = &imx51_ecspi_devtype_data, },
	{ .compatible = "fsl,imx6ul-ecspi", .data = &imx6ul_ecspi_devtype_data, },
	{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, spi_imx_dt_ids);

static struct platform_driver spi_imx_driver = {
	.driver = {
		   .name = DRIVER_NAME,
		   .of_match_table = spi_imx_dt_ids,
		   .pm = IMX_SPI_PM,
	},
	.id_table = spi_imx_devtype,
	.probe = spi_imx_probe,
	.remove = spi_imx_remove,
};
module_platform_driver(spi_imx_driver);

その中で、spi_imx_devtypeデータはSPIデバイスツリーマッチングテーブルではなく、spi_imx_dt_idsはSPIデバイスツリーマッチングテーブルであり、前の章のI2Cについても同じことが言えます。

     spi_imx_probe関数は、デバイスツリーから対応するノード属性値を読み取り、spi_masterを適用して初期化し、最後にspi_bitbang_start(登録関数はspi_register_master)関数を呼び出してspi_masterをLinuxカーネルに登録します。

-->>static int spi_imx_probe(struct platform_device *pdev)
    -->>spi_bitbang_start(&spi_imx->bitbang);
        -->>spi_register_master(spi_master_get(master));

       spi_imx_probe関数には2つの重要な設定があります。1つはSPIホストの最終的なデータ送受信関数spi_imx_transferであり、もう1つは送受信関数のポイントを設定する関数spi_imx_setupxferです。

spi_imx_probe:

spi_imx->bitbang.setup_transfer = spi_imx_setupxfer;
spi_imx->bitbang.txrx_bufs = spi_imx_transfer;

    -->>spi_imx_transfer
        -->>spi_imx_pio_transfer
            -->>spi_imx_push
                -->>spi_imx->tx(spi_imx);

    -->>spi_imx_setupxfer
        -->>
        /* Initialize the functions for transfer */
	if (config.bpw <= 8) {
		spi_imx->rx = spi_imx_buf_rx_u8;
		spi_imx->tx = spi_imx_buf_tx_u8;
		spi_imx->tx_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
		spi_imx->rx_config.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
	} else if (config.bpw <= 16) {
		spi_imx->rx = spi_imx_buf_rx_u16;
		spi_imx->tx = spi_imx_buf_tx_u16;
		spi_imx->tx_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES;
		spi_imx->rx_config.src_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES;
	} else {
		spi_imx->rx = spi_imx_buf_rx_u32;
		spi_imx->tx = spi_imx_buf_tx_u32;
		spi_imx->tx_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
		spi_imx->rx_config.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
	}

#define MXC_SPI_BUF_TX(type)						\
static void spi_imx_buf_tx_##type(struct spi_imx_data *spi_imx)		\
{									\
	type val = 0;							\
									\
	if (spi_imx->tx_buf) {						\
		val = *(type *)spi_imx->tx_buf;				\
		spi_imx->tx_buf += sizeof(type);			\
	}								\
									\
	spi_imx->count -= sizeof(type);					\
									\
	writel(val, spi_imx->base + MXC_CSPITXDATA);			\
}

MXC_SPI_BUF_RX(u8)
MXC_SPI_BUF_TX(u8)
MXC_SPI_BUF_RX(u16)
MXC_SPI_BUF_TX(u16)
MXC_SPI_BUF_RX(u32)
MXC_SPI_BUF_TX(u32)

3:SPIデバイスドライバー分析

1.ドライブデータを初期化します

前述のように、spi_driverを登録すると、デバイスの読み取りと書き込みができるようになります。次の2つの構造に精通する必要があります。

spi_transfer:tx_bufは送信されるデータを保持し、rx_bufは受信されたデータを保持し、lenは転送されるデータの長さです。

struct spi_transfer {

	const void	*tx_buf;
	void		*rx_buf;
	unsigned	len;

	dma_addr_t	tx_dma;
	dma_addr_t	rx_dma;
	struct sg_table tx_sg;
	struct sg_table rx_sg;

	unsigned	cs_change:1;
	unsigned	tx_nbits:3;
	unsigned	rx_nbits:3;
#define	SPI_NBITS_SINGLE	0x01 /* 1bit transfer */
#define	SPI_NBITS_DUAL		0x02 /* 2bits transfer */
#define	SPI_NBITS_QUAD		0x04 /* 4bits transfer */
	u8		bits_per_word;
	u16		delay_usecs;
	u32		speed_hz;

	struct list_head transfer_list;
};

spi_message:使用する前にspi_messageを初期化する必要があります。初期化関数はspi_message_initです。

struct spi_message {
	struct list_head	transfers;

	struct spi_device	*spi;

	unsigned		is_dma_mapped:1;

	void			(*complete)(void *context);
	void			*context;
	unsigned		frame_length;
	unsigned		actual_length;
	int			status;

	struct list_head	queue;
	void			*state;
};

void spi_message_init(struct spi_message *m)

初期化が完了したら、spi_transferをspi_messageキューに追加する必要があります:spi_message_add_tail

void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)

2.データの送受信

spiデータの準備ができたら、データ送信を行うことができます。データ送信は、同期送信と非同期送信に分けられます。
同期送信:SPIデータ送信の完了待ちをブロックします

int spi_sync(struct spi_device *spi, struct spi_message *message)

非同期送信:ブロックされません。spi_messageで完全なメンバー変数を設定する必要があります。これはコールバック関数です。この関数は、SPI非同期送信が完了すると呼び出されます。

int spi_async(struct spi_device *spi, struct spi_message *message)

3.読み取りおよび書き込み操作の例

/* SPI 多字节发送 */
static int spi_send(struct spi_device *spi, u8 *buf, int len)
{
    int ret;
    struct spi_message m;
    struct spi_transfer t = {
        .tx_buf = buf,
        .len = len,
    };
    spi_message_init(&m); /* 初始化 spi_message */
    spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */
    ret = spi_sync(spi, &m); /* 同步传输 */
    return ret;
}

/* SPI 多字节接收 */
static int spi_receive(struct spi_device *spi, u8 *buf, int len)
{
    int ret;
    struct spi_message m;
    struct spi_transfer t = {
        .rx_buf = buf,
        .len = len,
    };
    spi_message_init(&m); /* 初始化 spi_message */
    spi_message_add_tail(t, &m);/* 将 spi_transfer 添加到 spi_message 队列 */
    ret = spi_sync(spi, &m); /* 同步传输 */
    return ret;
}

4:例

1.デバイスツリーを変更します

(1)ICM20608デバイスのIOピンを追加します

pinctrl_ecspi3: icm20608 {
	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
	>;
};

(2)icm20608子ノードをecspi3ノードに追加します

&ecspi3 {
	fsl,spi-num-chipselects = <1>;              /* 设置当前片选数量为1 */
	cs-gpio = <&gpio1 20 GPIO_ACTIVE_LOW>;      /* 选用自定义“cs-gpio”属性,而非系统片选属性*/
	pinctrl-names = "default";                  
	pinctrl-0 = <&pinctrl_ecspi3>;              /* 设置IO要使用的pinctrl子节点 */
	status = "okay";

	spidev: icm20608@0 {                        /* 设备连接在ecspi3的第0个通道上 */
		compatible = "alientek, icm20608";
		spi-max-frequency = <8000000>;          /* SPI最大时钟频率为8MHz */
		reg = <0>;
	};
};

2.ドライブ

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <linux/spi/spi.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "icm20608reg.h"


#define ICM20608_CNT    1
#define ICM20608_NAME   "icm20608"

struct icm20608_dev {
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct device_node *nd;
    int major;
    void *private_data;
    int cs_gpio;
    signed int gyro_x_adc;
    signed int gyro_y_adc;
    signed int gyro_z_adc;
    signed int accel_x_adc;
    signed int accel_y_adc;
    signed int accel_z_adc;
    signed int temp_adc;
};

static struct icm20608_dev icm20608dev;

static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{
	int ret;
	unsigned char txdata[len];
	struct spi_message m;
	struct spi_transfer *t;
	struct spi_device *spi = (struct spi_device *)dev->private_data;

	gpio_set_value(dev->cs_gpio, 0);				/* 片选拉低,选中ICM20608 */
	t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);	/* 申请内存 */

	/* 第1次,发送要读取的寄存地址 */
	txdata[0] = reg | 0x80;		/* 写数据的时候寄存器地址bit8要置1 */
	t->tx_buf = txdata;			/* 要发送的数据 */
	t->len = 1;					/* 1个字节 */
	spi_message_init(&m);		/* 初始化spi_message */
	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
	ret = spi_sync(spi, &m);	/* 同步发送 */

	/* 第2次,读取数据 */
	txdata[0] = 0xff;			/* 随便一个值,此处无意义 */
	t->rx_buf = buf;			/* 读取到的数据 */
	t->len = len;				/* 要读取的数据长度 */
	spi_message_init(&m);		/* 初始化spi_message */
	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
	ret = spi_sync(spi, &m);	/* 同步发送 */

	kfree(t);									/* 释放内存 */
	gpio_set_value(dev->cs_gpio, 1);			/* 片选拉高,释放ICM20608 */

	return ret;
}

static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len)
{
    uint8_t char txdata[len];
    struct spi_message m;
    struct spi_transfer *t;
    struct spi_device *spi = (struct spi_device *)dev->private_data;

    t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
    gpio_set_value(dev->cs_gpio, 0);

	/* 第1次,发送要读取的寄存地址 */
    txdata[0] = reg & ~0x80;
    t->tx_buf = txdata;
    t->led = 1;
    spi_message_init(&m);
    spi_message_add_tail(t, &m);
    ret = spi_sync(spi, &m);

	/* 第2次,发送要写入的数据 */
	t->tx_buf = buf;			/* 要写入的数据 */
	t->len = len;				/* 写入的字节数 */
	spi_message_init(&m);		/* 初始化spi_message */
	spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
	ret = spi_sync(spi, &m);	/* 同步发送 */

	kfree(t);					/* 释放内存 */
	gpio_set_value(dev->cs_gpio, 1);/* 片选拉高,释放ICM20608 */
	return ret;
}

static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
{
	u8 data = 0;
	icm20608_read_regs(dev, reg, &data, 1);
	return data;
}

static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
{
	u8 buf = value;
	icm20608_write_regs(dev, reg, &buf, 1);
}

void icm20608_readdata(struct icm20608_dev *dev)
{
    uint8_t data[14];

    icm20608_read_onereg(dev, ICM20_ACCEL_XOUT_H, data, 14);

    dev->accel_x_adc = (signed short)((data[0] << 8) | data[1]); 
	dev->accel_y_adc = (signed short)((data[2] << 8) | data[3]); 
	dev->accel_z_adc = (signed short)((data[4] << 8) | data[5]); 
	dev->temp_adc    = (signed short)((data[6] << 8) | data[7]); 
	dev->gyro_x_adc  = (signed short)((data[8] << 8) | data[9]); 
	dev->gyro_y_adc  = (signed short)((data[10] << 8) | data[11]);
	dev->gyro_z_adc  = (signed short)((data[12] << 8) | data[13]); 
}

static int icm20608_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &icm20608dev;
    return 0;
}

static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
    int16_t data[7];
    long err = 0;
    struct icm20608_dev *dev = (struct icm20608_dev *)filp->private_data;

    icm20608_readdata(dev);
	data[0] = dev->gyro_x_adc;
	data[1] = dev->gyro_y_adc;
	data[2] = dev->gyro_z_adc;
	data[3] = dev->accel_x_adc;
	data[4] = dev->accel_y_adc;
	data[5] = dev->accel_z_adc;
	data[6] = dev->temp_adc;
	err = copy_to_user(buf, data, sizeof(data));
	return 0;
}

static int icm20608_release(struct inode *inode, struct file *filp)
{
	return 0;
}

static struct file_operations icm20608_fops = {
    .owner = THIS_MODULE,
    .open = icm20608_open,
    .read = icm20608_read,
    .release = icm20608_release,
};

void icm20608_reginit(void)
{
    uint8_t value = 0;

    icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x80);
    mdelay(50);
    icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_1, 0x01);
    mdelay(50);

    value = icm20608_read_onereg(&icm20608dev, ICM20_WHO_AM_I);
	printk("ICM20608 ID = %#X\r\n", value);	

	icm20608_write_onereg(&icm20608dev, ICM20_SMPLRT_DIV, 0x00); 	/* 输出速率是内部采样率					*/
	icm20608_write_onereg(&icm20608dev, ICM20_GYRO_CONFIG, 0x18); 	/* 陀螺仪±2000dps量程 				*/
	icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG, 0x18); 	/* 加速度计±16G量程 					*/
	icm20608_write_onereg(&icm20608dev, ICM20_CONFIG, 0x04); 		/* 陀螺仪低通滤波BW=20Hz 				*/
	icm20608_write_onereg(&icm20608dev, ICM20_ACCEL_CONFIG2, 0x04); /* 加速度计低通滤波BW=21.2Hz 			*/
	icm20608_write_onereg(&icm20608dev, ICM20_PWR_MGMT_2, 0x00); 	/* 打开加速度计和陀螺仪所有轴 				*/
	icm20608_write_onereg(&icm20608dev, ICM20_LP_MODE_CFG, 0x00); 	/* 关闭低功耗 						*/
	icm20608_write_onereg(&icm20608dev, ICM20_FIFO_EN, 0x00);		/* 关闭FIFO						*/
}

static int icm20608_probe(struct spi_device *spi)
{
    int ret = 0;

    if(icm20608dev.major)
    {
        icm20608dev.devid = MKDEV(icm20608dev.major, 0);
        register_chrdev_region(icm20608dev.devid, ICM20608_CNT, ICM20608_NAME);
    }
    else    
    {
        alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT, ICM20608_NAME);
        icm20608dev.major = MAJOR(icm20608dev.devid);
    }

    cdev_init(&icm20608dev.cdev, &icm20608_fops);
    cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);

    icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);
    icm20608dev.device = device_create(icm20608dev.class, NULL, icm20608dev.device, NULL, ICM20608_NAME);

    icm20608dev.nd = of_find_node_by_path("/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02010000");
    icm20608dev.cs_gpio = of_get_named_gpio(icm20608dev.nd, "cs-gpio", 0);

    ret = gpio_direction_output(icm20608dev.cs_gpio, 1);
    if(ret < 0){
        printk("can't set gpio!\r\n");
    }

    spi->mode = SPI_MODE_0;
    spi_setup(spi);
    icm20608dev.private_data = spi;

    icm20608_reginit();
    return 0;
}

static int icm20608_remove(struct spi_device *spi)
{
    device_destroy(icm20608dev.class, icm20608dev.devid);
    class_destroy(icm20608dev.class);

    cdev_del(&icm20608dev.cdev);
    unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);
    
    return 0;
}


static const struct spi_device_id icm20608_id[] = {
    { "alientek, icm20608", 0 },
    {}
};

static const struct of_device_id icm20608_of_match[] = {
    { .compatible = "alientek, icm20608" },
    {}
};

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_init(void)
{
    return spi_register_driver(&icm20608_driver);
}

static void __exit icm20608_exit(void)
{
    spi_unregister_driver(&icm20608_driver);
}

module_init(icm20608_init);
module_exit(icm20608_exit);
MODULE_LICENSE("GPL");

 

おすすめ

転載: blog.csdn.net/qq_34968572/article/details/104920483