Linux 驱动开发(三)SPI

2021/11/21 7:10:45

本文主要是介绍Linux 驱动开发(三)SPI,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

spi 驱动框架和 iic 驱动框架类似,都分为主机控制器驱动和设备驱动。

1 SPI 主机驱动

SOC 的spi外设驱动是半导体产商写好的,SPI 主机驱动器采用了 platfom 驱动框架。我们可以从内核中文件中找到 spi_imx_driver 结构体:

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,
};

当控制器的设备和驱动匹配以后,spi_imx_probe 函数就会执行,spi_imx_probe 函数会从设备树中读取相应的节点属性值,申请并初始化 spi_master,最后调用 spi_bitbang_start 函数(spi_bitbang_start 会调用 spi_register_master 函数)向 Linux 内核注册
spi_master。spi 控制器驱动的核心就是 spi_master 结构体。

struct spi_master {
	struct device	dev;
	struct list_head list;
	s16			bus_num;
	u16			num_chipselect, dma_alignment, mode_bits;
	u32			bits_per_word_mask;
#define SPI_BPW_MASK(bits) BIT((bits) - 1)
#define SPI_BIT_MASK(bits) (((bits) == 32) ? ~0U : (BIT(bits) - 1))
#define SPI_BPW_RANGE_MASK(min, max) (SPI_BIT_MASK(max) - SPI_BIT_MASK(min - 1))
	u32			min_speed_hz, max_speed_hz;
	u16			flags;
#define SPI_MASTER_HALF_DUPLEX	BIT(0)		/* can't do full duplex */
#define SPI_MASTER_NO_RX	BIT(1)		/* can't do buffer read */
#define SPI_MASTER_NO_TX	BIT(2)		/* can't do buffer write */
#define SPI_MASTER_MUST_RX      BIT(3)		/* requires rx */
#define SPI_MASTER_MUST_TX      BIT(4)		/* requires tx */
	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);   
	void		(*cleanup)(struct spi_device *spi);
	bool		(*can_dma)(struct spi_master *master, struct spi_device *spi,struct spi_transfer *xfer);
	bool				queued;
	struct kthread_worker		kworker;
	struct task_struct		*kworker_task;
	struct kthread_work		pump_messages;
	spinlock_t			queue_lock;
	struct list_head		queue;
	struct spi_message		*cur_msg;
	bool	idling,busy,running,rt,auto_runtime_pm, cur_msg_prepared,cur_msg_mapped;
	struct completion   xfer_completion;
	size_t				max_dma_len;
	int (*prepare_transfer_hardware)(struct spi_master *master);
	int (*transfer_one_message)(struct spi_master *master,struct spi_message *mesg);
	int (*unprepare_transfer_hardware)(struct spi_master *master);
	int (*prepare_message)(struct spi_master *master,struct spi_message *message);
	int (*unprepare_message)(struct spi_master *master,struct spi_message *message);
	void (*set_cs)(struct spi_device *spi, bool enable);
	int (*transfer_one)(struct spi_master *master, struct spi_device *spi, struct spi_transfer *transfer);
	void (*handle_err)(struct spi_master *master, struct spi_message *message);
	int			*cs_gpios;
	struct dma_chan		*dma_tx;
	struct dma_chan		*dma_rx;
	void			*dummy_rx;
	void			*dummy_tx;
};

这里的内容有很多,我们重点关注几个函数
int (*transfer)(struct spi_device *spi, struct spi_message *mesg);
transfer 函数,和 i2c_algorithm 中的 master_xfer 函数一样,控制器数据传输函数。
int (*transfer_one_message)(struct spi_master *master,struct spi_message *mesg);
transfer_one_message 函数,也用于 SPI 数据发送,用于发送一个 spi_message,SPI 的数据会打包成 spi_message,然后以队列方式发送出去。

也就是 SPI 主机端最终会通过 transfer 函数与 SPI 设备进行通信,因此对于 SPI 主机控制器的驱动编写者而言 transfer 函数是需要实现的,因为不同的 SOC 其 SPI 控制器不同,寄存器都不一样。
SPI 主机驱动的核心就是申请 spi_master,然后初始化 spi_master,最后向 Linux 内核注册spi_master。

2 SPI 设备驱动

spi设备驱动的核心内容就是 spi_driver 结构体,它和 i2c_driver、 platform_driver 基本一样,内容包括 probe、remove函数等,当 spi 设备和驱动匹配成功以后 probe 函数就会执行。spi_driver 初始化完成以后需要向 Linux 内核注册, spi_driver 注册函数为
spi_register_driver,注销 SPI 设备驱动以后也需要注销掉前面注册的 spi_driver,使用 spi_unregister_driver 函数完成 spi_driver 的注销

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_driver 驱动框架

/* probe function */
static int xxx_probe(struct spi_driver *spi)
{
	/* specific content */
	return 0;
}

/* remove function */
static void xxx_remove(struct spi_driver *spi)
{
	/* specific content */
	return 0;
}

/* traditional match table */
static const struct spi_driver_id xxx_id[] = {
	{"xxx",0},
	{}
};

/* device_tree match table */
static const struct spi_driver_id xxx_of_match[] = {
	{.compatible = "xxx"},
	{}
};

/* spi_driver structure */
static struct spi_driver xxx_driver = {
	.probe = xx_probe,
	.remove = xxx_remove,
	.driver = {
		.owner = THIS_MODULE,
		.name = "xxx",
		.of_match_table = xxx_of_match,
	},
	.id_table = xxx_id,
};

/* module entrance */
static int __init xxx_init(void)
{
	return spi_register_driver(&xxx_driver);
}

/* module exit */
static void __exit xxx_exit(void)
{
	spi_unregister_driver(&xxx_driver);
}

module_init(xx_init);
module_exit(xxx_exit);

3 spi 设备和驱动匹配过程

SPI 设备和驱动的匹配过程是由 SPI 总线来完成的,SPI 总线为 spi_bus_type

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

可以看出, SPI 设备和驱动的匹配函数为 spi_match_device,函数内容如下:

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;
}

of_driver_match_device 函数用于完成设备树设备和驱动匹配。比较 SPI 设备节点的 compatible 属性和 of_device_id 中的 compatible 属性是否相等,如果相当的话就表示 SPI 设备和驱动匹配。spi_match_id 函数用于传统的、无设备树的 SPI 设备和驱动匹配过程。比较 SPI设备名字和 spi_device_id 的 name 字段是否相等,相等的话就说明 SPI 设备和驱动匹配。

4 spi 数据收发流程

spi 的收发关键是两个结构体,spi_transfer 和 spi_message

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;
};

tx_buf 保存着要发送的数据, rx_buf 用于保存接收到的数据, len 是要进行传输的数据长度

truct 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;
};

在使用spi_message之前需要对其进行初始化, spi_message 初始化函数为spi_message_init
spi_message 初始化完成以后需要将 spi_transfer 添加到 spi_message 队列中,要用到 spi_message_add_tail 函数
spi_message 准备好以后既可以进行数据传输了,数据传输分为同步传输和异步传输,同步传输会阻塞的等待 SPI 数据传输完成,同步传输函数为 spi_sync
异步传输不会阻塞的等到 SPI 数据传输完成,异步传输需要设置 spi_message 中的 complete 成员变量, complete 是一个回调函数,当 SPI 异步传输完成以后此函数就会被调用。 SPI 异步传输函数为 ```spi_async``

5 以 icm20608 为例构建一个 Linux 下 SPI 驱动框架

  • 修改设备树
    在 iomuxc 节点中添加一个新的子节点来描述 ICM20608 所使用的 SPI 引脚,子节点名字为 pinctrl_ecspi3,内容如下:
pinctrl_ecspi3: icm20608grp {
			fsl,pins = <
				MX6UL_PAD_UART2_TX_DATA__GPIO1_IO20		0x10b0	/* CS */
				MX6UL_PAD_UART2_RX_DATA__ECSPI3_SCLK	0x10b1	/* SCLK */
				MX6UL_PAD_UART2_RTS_B__ECSPI3_MISO		0x10b1	/* MISO */
				MX6UL_PAD_UART2_CTS_B__ECSPI3_MOSI		0x10b1	/* MOSI */
			>;
		};

接着在 ecspi3 节点追加 icm20608 子节点

&ecspi3 {
	fsl,spi-num-chipselects = <1>;
	cs-gpio = <&gpio1 20 GPIO_ACTIVE_LOW>; 
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_ecspi3>;
	status = "okay";

	spidev0: icm20608@0 {  /* @0中的0表示icm20608连接在ECSPI3的第0个通道上 */
		compatible = "alientek,icm20608";
		spi-max-frequency = <8000000>; 
		reg = <0>;
	};	
};

接着开始编写驱动程序

  • 创建一个 icm20608 设备结构体
/* icm20608 device struct*/
struct icm20608_dev {
    int major;
    int minor;
    dev_t devid;
    struct cdev cdev;
    struct device *device;
    struct class *class;
    void *private_data;
    struct device_node *nd;
    int cs_gpio;				/* gpio num of cs */
	signed int gyro_x_adc;		/* initial value of gyro_x */
	signed int gyro_y_adc;		/* initial value of gyro_y */
	signed int gyro_z_adc;		/* initial value of gyro_z */
	signed int accel_x_adc;		/* initial value of accel_x */
	signed int accel_y_adc;		/* initial value of accel_y */
	signed int accel_z_adc;		/* initial value of accel_z*/
	signed int temp_adc;		/* initial tempture value */
};
struct icm20608_dev icm20608dev;
  • icm20608 的 spi_driver 注册与注销
/* no device tree match*/
struct spi_device_id icm20608_id [] = {
    {"alientek,icm20608", 0},
    {}
}; 
/*  device-tree match*/
static const struct of_device_id icm20608_of_match [] = {
    {.compatible = "alientek,icm20608"},
    {}
};
/* spi driver*/ 
static struct spi_driver icm20608_driver = {
    .probe = icm20608_probe,
    .remove = icm20608_remove,
    .driver = {
        .name = "icm20608",
        .owner = THIS_MODULE,
        .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");
MODULE_AUTHOR("jimmy");
  • 由于匹配成功后开始执行 probe 函数。接着编写 probe 和 remove 函数
/* probe function */
static int icm20608_probe(struct spi_device *spi)
{
    int ret =0;
    /* create character device agriculture */
    /* device ID */
    icm20608dev.major = 0;
    if(icm20608dev.major){     /* given major*/
        icm20608dev.devid = MKDEV(icm20608dev.major, 0);
        register_chrdev_region(icm20608dev.devid, ICM20608_CNT, ICM20608_NAME);
    } else {                        /* without major*/
        ret = alloc_chrdev_region(&icm20608dev.devid, 0, ICM20608_CNT, ICM20608_NAME);
        icm20608dev.major = MAJOR(icm20608dev.devid);
        icm20608dev.minor = MINOR(icm20608dev.devid);
    }
    if(ret < 0){
        printk("icm20608 chrdev_region error!!!\r\n");
        goto fail_devid;
    }
    printk("major=%d, minor=%d\r\n",icm20608dev.major, icm20608dev.minor);
    /* create cdev */
    icm20608dev.cdev.owner = THIS_MODULE;
    cdev_init(&icm20608dev.cdev, &icm20608_fops);
    ret = cdev_add(&icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);
    if(ret < 0){
        printk("icm20608 cdev_add error!!\r\n");
        goto fail_cdev;
    }
    /* create device node */
    icm20608dev.class = class_create(THIS_MODULE, ICM20608_NAME);
    if(IS_ERR(icm20608dev.class)){
        ret = PTR_ERR(icm20608dev.class);
        printk("icm20608 class_create error!!\r\n");
        goto fail_class;
    }
    icm20608dev.device = device_create(icm20608dev.class, NULL, icm20608dev.devid, NULL, ICM20608_NAME);
    if(IS_ERR(icm20608dev.device)){
        ret = PTR_ERR(icm20608dev.device);
        printk("icm20608 device_create error!!\r\n");
        goto fail_device;
    }
    /* require shipselect pin */
    icm20608dev.nd = of_get_parent(spi->dev.of_node);
    icm20608dev.cs_gpio = of_get_named_gpio(icm20608dev.nd, "cs_gpio", 0);
    if(icm20608dev.cs_gpio < 0){
        printk("cannot get cs_gpio!\r\n");
        goto fail_gpio;
    }
    /* request gpio */
    ret = gpio_request(icm20608dev.cs_gpio, "cs");
    if(ret < 0){
        printk("gpio requset fail!\r\n");
        goto fail_gpio;
    }
    /* set the output, high level, invalid */
    ret = gpio_direction_output(icm20608dev.cs_gpio, 1); 
    if(ret < 0){
        printk("unable to set output!\r\n");
        goto fail_set_output;
    }
    /* initialize spi_device */
    spi->mode = SPI_MODE_0; /* MODE0, CPOL=0, CPHA=0 */
    spi_setup(spi);
    /* set private data */
    icm20608dev.private_data = spi;
    /* initialize icm20608 */
    icm20608_initialize(&icm20608dev);

    return 0;

fail_set_output:
    gpio_free(icm20608dev.cs_gpio);
fail_gpio:
    device_destroy(icm20608dev.class, icm20608dev.devid);
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;
}

/* remove function */
static int icm20608_remove(struct spi_device *spi)
{
    cdev_del(&icm20608dev.cdev);
    unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);
    device_destroy(icm20608dev.class, icm20608dev.devid);
    class_destroy(icm20608dev.class);
    gpio_free(icm20608dev.cs_gpio);
    return 0;
}
  • 字符设备操作集
static int icm20608_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &icm20608dev;
    return 0;
}

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

static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offset)
{
    signed int 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));
    if(err < 0){
        printk("copy to user error!!\r\n");
        return err;
    }
    return 0;
}

/* device operations struct */
static struct file_operations icm20608_fops = {
    .owner = THIS_MODULE,
    .read = icm20608_read,
    .open = icm20608_open,
    .release = icm20608_release,
};
  • icm20608 初始化以及读写寄存器函数编写
/* icm20608 initial function */
void icm20608_initialize(struct icm20608_dev *dev)
{
    u8 value = 0;

    icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x80);    /* reset, sleep mode */
    mdelay(50);
    icm20608_write_onereg(dev, ICM20_PWR_MGMT_1, 0x01);    /* close sleep mode, select clock automatically */ 
    mdelay(50);

    value = icm20608_read_onereg(dev, ICM20_WHO_AM_I);
    printk("ICM20608 ID = %#x\r\n", value);

    value = icm20608_read_onereg(dev, ICM20_PWR_MGMT_1);
    printk("ICM20_PWR_MGMT_1 = %#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	*/
}
/* read data of icm20608 */
void icm20608_readdata(struct icm20608_dev *dev)
{
    unsigned char data[14];
    icm20608_read_regs(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]);
}
/* icm20608 read a register */
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 write a register */
static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
{
    u8 buf = value;
    icm20608_write_regs(dev, reg, &buf, 1);
}

/* spi write register function */
static int icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, int len)
{
    int ret = 0;
    unsigned char txdata[len];    
    struct spi_message msg;
    struct spi_transfer *t;
    struct spi_device *spi = (struct spi_device *)dev->private_data;
    /* shipselect set low, select icm20608 */
    gpio_set_value(dev->cs_gpio, 0);
    /* create spi_transfer*/
    t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
    /* step 1: send address of register */
    txdata[0] = reg & ~ 0x80;           // when write data, bit 7 need to be cleared
    t->tx_buf = txdata;                 // data needs to be sent
    t->len = 1;                         // len is one byte
    spi_message_init(&msg);             // initialize spi_message
    spi_message_add_tail(t, &msg);      // add spi_trnasfer to spi_message
    ret = spi_sync(spi, &msg);          // set send method as sync

    /* step2: send data needs to be written */
    t->tx_buf = buf;                    // data needs to be written
    t->len = len;
    spi_message_init(&msg);
    spi_message_add_tail(t, &msg);
    ret = spi_sync(spi, &msg);
    kfree(t);
     /* shipselect set high */
    gpio_set_value(icm20608dev.cs_gpio, 1);
    return ret;

}

/* spi read register function */
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{
    int ret = 0;
    unsigned char txdata[len]; 
    struct spi_message msg;
    struct spi_transfer *t;
    struct spi_device *spi = (struct spi_device *)dev->private_data;
    /* shipselect set low, select icm20608 */
    gpio_set_value(icm20608dev.cs_gpio, 0);
    /* create spi_transfer*/
    t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
    /* step 1: send address of register */
    txdata[0] = reg | 0x80;             // if you use icm20608 by spi, set the highest bit of reg's add
    t->tx_buf = txdata;                 // data needs to be sent
    t->len = 1;                         // len is one byte
    spi_message_init(&msg);             // initialize spi_message
    spi_message_add_tail(t, &msg);      // add spi_trnasfer to spi_message
    ret = spi_sync(spi, &msg);          // set send method as sync
    /* step2: read data */
    txdata[0] = 0xff;                   // random number, no meaning
    t->tx_buf = txdata;
    t->len = len;
    spi_message_init(&msg);
    spi_message_add_tail(t, &msg);
    ret = spi_sync(spi, &msg);    
    kfree(t); 
     /* shipselect set high */
    gpio_set_value(icm20608dev.cs_gpio, 1);
    return ret;
}

我们可以看到读写寄存器的操作是很繁琐的,有没有高级的API函数呢显然是有得,上面的两个函数我们可以使用内核中的其他函数替换。

/* spi read register function using linux kernel function */
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{
    u8 data = 0;
    struct spi_device *spi = (struct spi_device *)dev->private_data;
    gpio_set_value(dev->cs_gpio, 0);
    data = reg | 0x80;
    spi_write(spi, &data, 1);   /* send the address of reg you want to read */
    spi_read(spi, buf, len);    /* read data */
    gpio_set_value(dev->cs_gpio, 1);
    return data;
}

/* spi write register function using linux kernel function */
static void icm20608_write_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{
    u8 data = 0;
    struct spi_device *spi = (struct spi_device *)dev->private_data;
    gpio_set_value(dev->cs_gpio, 0);
    data = reg & ~ 0x80;
    spi_write(spi, &data, 1);   /* send the address of reg you want to write*/
    spi_read(spi, buf, len);    /* data you want to write */
    gpio_set_value(dev->cs_gpio, 1);
}


这篇关于Linux 驱动开发(三)SPI的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程