IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> SPI接口及驱动 -> 正文阅读

[嵌入式]SPI接口及驱动

1. 简介

  • SPI接口是Motorola 首先提出的全双工三线同步串行外围接口,采用主从模式(Master Slave)架构。支持多slave模式应用,一般仅支持单Master。时钟由Master控制,在时钟移位脉冲下,数据按位传输,高位在前,低位在后(MSB first)。SPI接口有2根单向数据线,为全双工通信,目前应用中的数据速率可达几Mbps的水平。
  • SPI接口优点:
    • 支持全双工操作
    • 操作简单
    • 数据传输速率较高(相对的)
  • SPI接口缺点:
    • 多个spi设备需要占用主机较多的管脚(每个从机都需要一根片选线)
    • 只支持单个主机
    • 没有指定的流控制,没有应答机制确认是否接收到数据

2. 接口

2.1 总线结构

在这里插入图片描述

2.2 硬件接口

在这里插入图片描述

  • SPI接口共有4根信号线,分别是:设备选择线、时钟线、串行输出数据线、串行输入数据线。
    • MOSI:主器件数据输出,从器件数据输入
    • MISO:主器件数据输入,从器件数据输出
    • SCLK: 时钟信号,由主器件产生
    • S S ˉ \bar{SS} SSˉ: 从器件使能信号,由主器件控制

2.3 SPI工作模式

在这里插入图片描述

  • SPI有四种工作模式,具体由CPOL(Clock Polarity 时钟极性),CPHA(Clock Phase时钟相位)决定
    • 当CPOL为0时,空闲的时候SCLK电平是低电平
    • 当CPOL为1时,空闲的时候SCLK电平是高电平
    • 当CPHA为0时,采集数据发生在时钟周期的前边缘(第一个边缘,可能是上升缘也可能是下降缘,由CPOL决定),这同时意味着输出数据发生在后边缘
    • 当CPHA为1时,采集数据发生在时钟周期的后边缘(第二个边缘,可能是上升缘也可能是下降缘,由CPOL决定),这同时意味着输出数据发生在前边缘

在这里插入图片描述

3. Linux驱动代码

  • Linux SPI驱动框架主要分为

    • 核心层
    • 控制器驱动层
    • 设备驱动层
      在这里插入图片描述
  • 最下层是硬件空间,SPI总线控制器,总线控制器负责硬件上的数据交互。内核空间中,需要有对应的控制器驱动,对硬件进行操作。

  • 核心层的作用:向控制器驱动层提供注册SPI控制器驱动的接口,并提供一些需要控制器驱动实现的回调函数。核心层向上,对SPI设备驱动,提供标准的SPI收发API,以及设备注册函数。

  • 当有SPI设备驱动发起一次传输时,设备驱动会调用SPI核心层的收发函数(spi_sync/spi_async),核心层的收发函数,会回调控制器驱动层实现的硬件相关的发送回调函数。从而实现SPI数据的收发。

  • 我们编写SPI接口的设备驱动程序的时候,最需要关心的就是SPI控制器的部分和SPI设备采用的是那种模式,确定模式后,我们得将SPI控制器配置成一样的模式才能正常工作

  • SPI驱动代码:位于 kernel/drivers/spi目录下 (这个目录和一些层次比较明显的驱动目录布局不同,全放在这个文件夹下,因此还是只好通过看Kconfig 和 Makefile来找找思路)

  • 相关数据结构

    • spi_master:SPI控制器
    • spi_driver:SPI设备驱动
    • spi_device:SPI设备
    • spi_message:SPI传输数据结构体
    • spi_transfer:该结构体是spi_message下的子单元
  • spi_driver和spi_device的关系

    • spi_driver对应一套驱动方法,包含probe,remove等方法。spi_device对应真实的物理设备,每个spi设备都需要一个spi_device来描述。spi_driver与spi_device是一对多的关系,一个spi_driver上可以支持多个同类型的spi_device
  • spi_master和spi_device

    • spi_master 与 spi_device 的关系和硬件上控制器与设备的关系一致,即spi_device依附于spi_master
  • spi_message和spi_transfer

    • spi传输数据是以 spi_message 为单位的,我们需要传输的内容在 spi_transfer 中。spi_transfer是spi_message的子单元
    • 将本次需要传输的 spi_transfer 以 spi_transfer->transfer_list 为链表项,连接成一个transfer_list链表,挂接在本次传输的spi_message spi_message->transfers链表下
    • 将所有等待传输的 spi_message 以 spi_message->queue 为链表项,连接成个链表挂接在queue下
      在这里插入图片描述
  • 相关API函数

//分配一个spi_master
struct spi_master *spi_alloc_master(struct device *dev, unsigned size)

//注册和注销spi_master
int spi_register_master(struct spi_master *master)
void spi_unregister_master(struct spi_master *master)

//注册和注销spi_driver
int spi_register_driver(struct spi_driver *sdrv)
void spi_unregister_driver(struct spi_driver *sdrv)

//初始化spi_message
void spi_message_init(struct spi_message *m)
//向spi_message添加transfers
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
//异步发送spi_message
int spi_async(struct spi_device *spi, struct spi_message *message)
//同步发送spi_message
int spi_sync(struct spi_device *spi, struct spi_message *message)

//spi同步写(封装了上面的函数)
int spi_write(struct spi_device *spi, const void *buf, size_t len)
//spi同步读(封装了上面的函数)
int spi_read(struct spi_device *spi, void *buf, size_t len)
//同步写并读取(封装了上面的函数)
int spi_write_then_read(struct spi_device *spi,
    const void *txbuf, unsigned n_tx,
    void *rxbuf, unsigned n_rx)

3.1 SPI核心层

  • 实现代码:kernel/drivers/spi/spi.c (头文件位于: kernel/include/linux/spi/spi.h)
  • 功能:(实现函数:spi_init)
    • 对SPI子系统进行初始化工作
    • 注册SPI总线
    • 注册一个spi master(控制器)类
    • 提供SPI设备驱动对SPI总线进行操作的API
struct bus_type spi_bus_type = {
	.name		= "spi",
	.dev_groups	= spi_dev_groups,
	.match		= spi_match_device,
	.uevent		= spi_uevent,
};

static struct class spi_master_class = {
	.name		= "spi_master",
	.owner		= THIS_MODULE,
	.dev_release	= spi_master_release,
	.dev_groups	= spi_master_groups,
};
  • spi_bus_type为spi总线类型,通过bus_register()函数将SPI 总线注册进总线,成功注册后,在/sys/bus 下即可找到spi 文件目录
  • spi_master_class为spi控制器设备类,通过调用class_register()函数注册设备类,成功注册后,在/sys/class目录下即可找到spi_master文件目录
  • spi子系统初始化函数spi_init,仅仅是注册了spi bus,以及spi_master class。
static int __init spi_init(void)
{
	int	status;

	buf = kmalloc(SPI_BUFSIZ, GFP_KERNEL);
	if (!buf) {
		status = -ENOMEM;
		goto err0;
	}
	status = bus_register(&spi_bus_type);			/*注册spi bus*/
	if (status < 0)
		goto err1;
	status = class_register(&spi_master_class);		/* 注册spi_master类 */
	if (status < 0)
		goto err2;
	if (IS_ENABLED(CONFIG_OF_DYNAMIC))
		WARN_ON(of_reconfig_notifier_register(&spi_of_notifier));
	if (IS_ENABLED(CONFIG_ACPI))
		WARN_ON(acpi_reconfig_notifier_register(&spi_acpi_notifier));
	return 0;
err2:
	bus_unregister(&spi_bus_type);
err1:
	kfree(buf);
	buf = NULL;
err0:
	return status;
}

3.2 SPI控制器驱动

  • SPI控制器驱动:即SPI硬件控制器对应的驱动,核心部分需要实现硬件SPI数据收发功能。这样SPI设备驱动,才能通过SPI读写数据
  • SPI是一种平台特定的资源,所以它是以platform device的方式注册进内核的,因此它的struct platform_device结构是已经静态定义好了的,现在只待它的struct platform_driver注册,然后和platform_device匹配。
  • 描述SPI控制器的设备树节点:
		spi0: spi@0xA5004000 {
			compatible = "my,my-spi";   /* 描述,和驱动匹配 */
			reg = <0 0xA5004000 0 0x1000>; /* 控制器IO地址 */
			clocks = <&spi0_mclk>;  /* 控制器时钟 */
			clock-names = "spi_mclk";  /* 时钟名 */
			interrupt-parent = <&gic>;
			interrupts = <0 33 4>;   /* 控制器中断 */
			resets = <&rst 0x50 4>;  /* 复位寄存器 */
			reset-names = "spi0";     
			pinctrl-names = "default";
			pinctrl-0 = <&spi0_func>;   /* 引脚复用功能配置 */
			status = "disabled";     /* 暂时disable */
			#address-cells = <1>;
			#size-cells = <0>;
		};
  • 实现一个platform_driver
static struct platform_driver my_spi_driver = {
    .probe = my_spi_probe,
    .remove = my_spi_remove,
    .driver = {
        .name = MY_SPI_NAME,
        .of_match_table = my_spi_of_match,
		.pm = &my_spi_dev_pm_ops,
    },
};

3.2.1 probe函数功能

  • 申请struct spi_master内存以及私有数据内存(struct my_spi)
  • 将struct spi_master设置为platform_device的private_data
  • 从设备树获取IO地址,对应设备树reg节点
  • 将IO内存映射为虚拟地址
  • 从设备树获取中断,对应设备树interrupts节点
  • 申请中断,设置中断服务函数
  • 设置核心层回调的片选使能函数,设置spi_master的transfer_one函数,如果控制器驱动不实现transfer和transfer_one_message,内核会自动填充默认的,最终控制器驱动只需要实现transfer_one。
  • 根据时钟名,从设备树获取时钟
  • 根据设备树的reset节点,操作寄存器spi控制器对应的BIT进行复位
  • 调用devm_spi_register_master,把spi_master->device注册到设备模型中。核心层篇中以及详细介绍了devm_spi_register_master函数,

3.2.2 数据收发

  • 数据收发部分主要有两个函数
  • 1)控制收发的函数,即spi_master的spi_transfer_one函数
    - 以看到全志这里代码的逻辑,一个是初始化完成量。然后进行一系列硬件操作后,睡眠等待完成信号,同时设置超时时间,超时了没有得到信号量,证明硬件出问题了,不应该一直等待,应该返回错误。
    - 这个完成信号由中断来发送,硬件上会有发送完成中断。
  • 2)配合发送的中断服务函数
    • 读取中断状态寄存器,判断发送完成的BIT是否置位,如果置位,则表示硬件已经发送完数据了,那么发送完成信号量,给正在睡眠等待的spi_transfer_one函数。spi_transfer_one函数接收到信号量后被唤醒,知道硬件以及发送完数据了,返回0(表示发送成功)

3.3 SPI设备驱动

  • 实现一个spi_driver, 并注册到SPI核心层
  • 直接调用spi核心层提供的函数注册,也就是说它不需要关心是哪个控制器来实现最终的spi数据传输。从这里也可以看出核心层的作用,分隔了控制器和设备驱动的关联性,主要两边的驱动都容易实现
  • 以内核中spidev设备驱动为例,对基于设备树的SPI设备驱动进行说明
  • spidev驱动代码位于kernel/drivers/spi/spidev.c
  • 设备树部分
dac0: dh2228@2 {
	compatible = "rohm,dh2228fv";
	reg = <2>;
	spi-max-frequency = <100000>;
};
  • 设备树部分很简单,只是写了compatible描述,用于和驱动匹配。这里reg的意思是spi cs引脚序号。并不像其他平台设备一样是IO地址,或者像I2C一样是从机地址

3.3.1 spidev_init

static int __init spidev_init(void)
{
	int status;

	/* Claim our 256 reserved device numbers.  Then register a class
	 * that will key udev/mdev to add/remove /dev nodes.  Last, register
	 * the driver which manages those device numbers.
	 */
	BUILD_BUG_ON(N_SPI_MINORS > 256);
	status = register_chrdev(SPIDEV_MAJOR, "spidev_test", &spidev_fops);
	if (status < 0)
		return status;

	spidev_class = class_create(THIS_MODULE, "spidev");
	if (IS_ERR(spidev_class)) {
		unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
		return PTR_ERR(spidev_class);
	}

	status = spi_register_driver(&spidev_spi_driver);
	if (status < 0) {
		class_destroy(spidev_class);
		unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
	}
	return status;
}
module_init(spidev_init);

static void __exit spidev_exit(void)
{
	spi_unregister_driver(&spidev_spi_driver);
	class_destroy(spidev_class);
	unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
}
module_exit(spidev_exit);
  • 这里设置的spidev的file_operations(spidev_fops),设备操作函数后文详细介绍。然后创建了spidev的class,创建完成后在用户空间/sys/class/下可以看到spidev目录结构。然后调用spi_register_driver注册spi从设备。

3.3.2 spi_driver

static const struct of_device_id spidev_dt_ids[] = {
	{ .compatible = "rohm,dh2228fv" },
	{ .compatible = "lineartechnology,ltc2488" },
	{ .compatible = "ge,achc" },
	{ .compatible = "semtech,sx1301" },
	{},
};
MODULE_DEVICE_TABLE(of, spidev_dt_ids);
static struct spi_driver spidev_spi_driver = {
	.driver = {
		.name =		"spidev",
		.of_match_table = of_match_ptr(spidev_dt_ids),
		.acpi_match_table = ACPI_PTR(spidev_acpi_ids),
	},
	.probe =	spidev_probe,
	.remove =	spidev_remove,

	/* NOTE:  suspend/resume methods are not necessary here.
	 * We don't do anything except pass the requests to/from
	 * the underlying controller.  The refrigerator handles
	 * most issues; the controller driver handles the rest.
	 */
};
  • 描述,与设备树相对应,会调用probe函数
  • probe函数,匹配时调用

3.3.3 probe

static int spidev_probe(struct spi_device *spi)
{
	struct spidev_data	*spidev;
	int			status;
	unsigned long		minor;

	/*
	 * spidev should never be referenced in DT without a specific
	 * compatible string, it is a Linux implementation thing
	 * rather than a description of the hardware.
	 */
	if (spi->dev.of_node && !of_match_device(spidev_dt_ids, &spi->dev)) {
		dev_err(&spi->dev, "buggy DT: spidev listed directly in DT\n");
		WARN_ON(spi->dev.of_node &&
			!of_match_device(spidev_dt_ids, &spi->dev));
	}

	spidev_probe_acpi(spi);

	/* Allocate driver data */
	spidev = kzalloc(sizeof(*spidev), GFP_KERNEL);
	if (!spidev)
		return -ENOMEM;

	/* Initialize the driver data */
	spidev->spi = spi;
	spin_lock_init(&spidev->spi_lock);
	mutex_init(&spidev->buf_lock);

	INIT_LIST_HEAD(&spidev->device_entry);

	/* If we can allocate a minor number, hook up this device.
	 * Reusing minors is fine so long as udev or mdev is working.
	 */
	mutex_lock(&device_list_lock);
	minor = find_first_zero_bit(minors, N_SPI_MINORS);
	if (minor < N_SPI_MINORS) {
		struct device *dev;

		spidev->devt = MKDEV(SPIDEV_MAJOR, minor);
		dev = device_create(spidev_class, &spi->dev, spidev->devt,
				    spidev, "spidev%d.%d",
				    spi->master->bus_num, spi->chip_select);
		status = PTR_ERR_OR_ZERO(dev);
	} else {
		dev_dbg(&spi->dev, "no minor number available!\n");
		status = -ENODEV;
	}
	if (status == 0) {
		set_bit(minor, minors);
		list_add(&spidev->device_entry, &device_list);
	}
	mutex_unlock(&device_list_lock);

	spidev->speed_hz = spi->max_speed_hz;

	if (status == 0)
		spi_set_drvdata(spi, spidev);
	else
		kfree(spidev);

	return status;
}
  • spidev的probe函数,申请了私有结构体spidev的内存空间,初始化了spidev的一些成员,自旋锁、互斥锁等。并将spidev指针设置为struct spi_device的私有数据(private_data)
  • 调用device_create创建了/dev/下的spidev节点,如spi总线0上cs1设备,则设备名为/dev/spidev0.1,其他以此类推
  • 在编写spi、i2c等驱动时,所做的操作也和spidev相差无几。初始化一些自己需要的资源等。但是一般情况下,这类涉及外设的驱动,会读取一下SPI、I2C从设备的ID寄存器等,来判断硬件是否就位,spi总线能否读写数据

3.3.4 spidev_fops

static const struct file_operations spidev_fops = {
	.owner =	THIS_MODULE,
	.write =	spidev_write,
	.read =		spidev_read,
	.unlocked_ioctl = spidev_ioctl,
	.compat_ioctl = spidev_compat_ioctl,
	.open =		spidev_open,
	.release =	spidev_release,
	.llseek =	no_llseek,
};

  • open和release:判断是否申请txbuff和rxbuff内存,如果没申请就申请。并维护了一个user count,用户空间每打开一次就+1,关闭一次就-1。所有都关闭后release释放申请的内存。

3.3.4.1 spidev_write

static ssize_t
spidev_write(struct file *filp, const char __user *buf,
		size_t count, loff_t *f_pos)
{
	struct spidev_data	*spidev;
	ssize_t			status = 0;
	unsigned long		missing;

	if (count > bufsiz)
		return -EMSGSIZE;
	spidev = filp->private_data;
	
	mutex_lock(&spidev->buf_lock);
	missing = copy_from_user(spidev->tx_buffer, buf, count);		(1)
	if (missing == 0)
		status = spidev_sync_write(spidev, count);					(2)
	else
		status = -EFAULT;
	mutex_unlock(&spidev->buf_lock);

	return status;
}

static inline ssize_t
spidev_sync_write(struct spidev_data *spidev, size_t len)
{
	struct spi_transfer	t = {
			.tx_buf		= spidev->tx_buffer,
			.len		= len,
			.speed_hz	= spidev->speed_hz,
		};
	struct spi_message	m;

	spi_message_init(&m);
	spi_message_add_tail(&t, &m);
	return spidev_sync(spidev, &m);									(3)
}

static ssize_t
spidev_sync(struct spidev_data *spidev, struct spi_message *message)
{
	int status;
	struct spi_device *spi;
	spin_lock_irq(&spidev->spi_lock);
	spi = spidev->spi;
	spin_unlock_irq(&spidev->spi_lock);
	if (spi == NULL)
		status = -ESHUTDOWN;
	else
		status = spi_sync(spi, message);							(4)
	if (status == 0)
		status = message->actual_length;
	return status;
}

  • copy_from_user:从用户空间拷贝要发送的数据到内核空间
  • 调用spidev_sync_write函数发送数据,这个函数实现代码也在上面贴出来了
  • spidev_sync_write代码实现,定义spi_transfer和spi_message,然后通过spidev_sync发送
  • spidev_sync中仅仅是判断了spi_device是否为空,不为空则调用spi_sync函数将spi_message发送出去
  • spidev_read函数与write函数如出一辙,不同的是write先从用户空间拷贝数据,然后调用发送函数发出去。struct spi_transfer填充的是tx_buff。而read则是先调用接受函数,struct spi_transfer填充的是rx_buff,然后将接收到的rx_buff,通过copy_to_user拷贝给用户空间。

3.3.4.2 ioctl

#define SPI_IOC_MAGIC			'k'

/* Read / Write of SPI mode (SPI_MODE_0..SPI_MODE_3) (limited to 8 bits) */
#define SPI_IOC_RD_MODE			_IOR(SPI_IOC_MAGIC, 1, __u8)
#define SPI_IOC_WR_MODE			_IOW(SPI_IOC_MAGIC, 1, __u8)

/* Read / Write SPI bit justification */
#define SPI_IOC_RD_LSB_FIRST		_IOR(SPI_IOC_MAGIC, 2, __u8)
#define SPI_IOC_WR_LSB_FIRST		_IOW(SPI_IOC_MAGIC, 2, __u8)

/* Read / Write SPI device word length (1..N) */
#define SPI_IOC_RD_BITS_PER_WORD	_IOR(SPI_IOC_MAGIC, 3, __u8)
#define SPI_IOC_WR_BITS_PER_WORD	_IOW(SPI_IOC_MAGIC, 3, __u8)

/* Read / Write SPI device default max speed hz */
#define SPI_IOC_RD_MAX_SPEED_HZ		_IOR(SPI_IOC_MAGIC, 4, __u32)
#define SPI_IOC_WR_MAX_SPEED_HZ		_IOW(SPI_IOC_MAGIC, 4, __u32)

/* Read / Write of the SPI mode field */
#define SPI_IOC_RD_MODE32		_IOR(SPI_IOC_MAGIC, 5, __u32)
#define SPI_IOC_WR_MODE32		_IOW(SPI_IOC_MAGIC, 5, __u32)

3.3.5 spidev总结

  • spidev是内核中用来测试spi的驱动,不是专门针对某一SPI硬件设备做的驱动,只是简单的注册字符设备,用户空间通过read、write、ioctl,直接对spi进行操作,相当于是用户空间和spi设备的桥梁
  • 用户空间操作时,打开设备节点,然后通过IOCTL设置模式、速度等参数,然后就可以调用read、write进行操作了
  • 根据设备树获取节点信息
spi_register_master->
	of_register_spi_devices->
		of_register_spi_device->
			of_spi_parse_dt
static int of_spi_parse_dt(struct spi_controller *ctlr, struct spi_device *spi,
			   struct device_node *nc)
{
	u32 value;
	int rc;

	/* Mode (clock phase/polarity/etc.) */
	if (of_property_read_bool(nc, "spi-cpha"))
		spi->mode |= SPI_CPHA;
	if (of_property_read_bool(nc, "spi-cpol"))
		spi->mode |= SPI_CPOL;
	if (of_property_read_bool(nc, "spi-cs-high"))
		spi->mode |= SPI_CS_HIGH;
	if (of_property_read_bool(nc, "spi-3wire"))
		spi->mode |= SPI_3WIRE;
	if (of_property_read_bool(nc, "spi-lsb-first"))
		spi->mode |= SPI_LSB_FIRST;

	/* Device DUAL/QUAD mode */
	if (!of_property_read_u32(nc, "spi-tx-bus-width", &value)) {
		switch (value) {
		case 1:
			break;
		case 2:
			spi->mode |= SPI_TX_DUAL;
			break;
		case 4:
			spi->mode |= SPI_TX_QUAD;
			break;
		default:
			dev_warn(&ctlr->dev,
				"spi-tx-bus-width %d not supported\n",
				value);
			break;
		}
	}

	if (!of_property_read_u32(nc, "spi-rx-bus-width", &value)) {
		switch (value) {
		case 1:
			break;
		case 2:
			spi->mode |= SPI_RX_DUAL;
			break;
		case 4:
			spi->mode |= SPI_RX_QUAD;
			break;
		default:
			dev_warn(&ctlr->dev,
				"spi-rx-bus-width %d not supported\n",
				value);
			break;
		}
	}

	if (spi_controller_is_slave(ctlr)) {
		if (strcmp(nc->name, "slave")) {
			dev_err(&ctlr->dev, "%pOF is not called 'slave'\n",
				nc);
			return -EINVAL;
		}
		return 0;
	}

	/* Device address */
	rc = of_property_read_u32(nc, "reg", &value);
	if (rc) {
		dev_err(&ctlr->dev, "%pOF has no valid 'reg' property (%d)\n",
			nc, rc);
		return rc;
	}
	spi->chip_select = value;

	/* Device speed */
	rc = of_property_read_u32(nc, "spi-max-frequency", &value);
	if (rc) {
		dev_err(&ctlr->dev,
			"%pOF has no valid 'spi-max-frequency' property (%d)\n", nc, rc);
		return rc;
	}
	spi->max_speed_hz = value;

	return 0;
}
  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2022-04-04 12:27:28  更:2022-04-04 12:28:40 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 4:36:50-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码