概述

  linux SPI驱动框架主要分为核心层,控制器驱动层以及设备驱动层。具体结构可参考下图

  图中,最下层是硬件空间,SPI总线控制器,总线控制器负责硬件上的数据交互。内核空间中,需要有对应的控制器驱动,对硬件进行操作。
  核心层的作用是,向控制器驱动层提供注册SPI控制器驱动的接口,并提供一些需要控制器驱动实现的回调函数。
  核心层向上,对SPI设备驱动,提供标准的spi收发API,以及设备注册函数。
  所以当有SPI设备驱动发起一次传输时,设备驱动会调用SPI核心层的收发函数(spi_sync/spi_async),核心层的收发函数,会回调控制器驱动层实现的硬件相关的发送回调函数。从而实现SPI数据的收发。
  本文主要介绍SPI核心层相关API及数据结构。

SPI核心层

核心层目录说明

  SPI核心层代码位于(drivers/spi/spi.c),头文件位于(include/linux/spi/spi.h)
  spi.c一方面对SPI子系统进行初始化工作,注册spi bus,注册spi_master class,同事提供spi设备驱动对spi总线进行操作的API。
  spi.h包含了spi核心层的一些重要数据结构,struct spi_master; struct spi_transfer; struct spi_message,以及一些实现比较简单的函数等。

子系统初始化

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 bus,以及spi_master class。

spi_init函数

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

  再来看一下spi从设备和从设备驱动匹配函数,也即是spi_bus_type.match函数。

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))                                (1)return 1;/* Then try ACPI */if (acpi_driver_match_device(dev, drv))                                (2)return 1;if (sdrv->id_table)                                                    (3)return !!spi_match_id(sdrv->id_table, spi);return strcmp(spi->modalias, drv->name) == 0;                        (4)
}

说明:
(1)比较驱动中的of_match_table的compatible和device的of_node的compatible,判断两者是否相等
(2)比较驱动中的acpi_match_table的compatible和device的of_node的compatible,判断两者是否相等
(3)判断驱动中是否支持id数组,如果支持,查找匹配此id的spi_device。
(4)比较设备的名字的和驱动的名字是否相同。

核心数据结构

struct spi_master

struct spi_master {struct device dev;                                                    /* SPI设备的device数据结构 */s16           bus_num;                                                    /* SPI总线序号 */u16            num_chipselect;                                             /* 片选信号数量 */u16         dma_alignment;                                              /* SPI控制器DMA缓冲区对齐定义 */u16           mode_bits;                                                  /* 工作模式位,由驱动定义 */u32         min_speed_hz;                                               /* 最小速度 */u32           max_speed_hz;                                               /* 最小速度 */u16           flags;                                                      /* 限制条件标志 */int         (*setup)(struct spi_device *spi);                           /* 设置SPI设备的工作参数 */int           (*transfer)(struct spi_device *spi,                         /* SPI发送函数1 */struct spi_message *mesg);void        (*cleanup)(struct spi_device *spi);                         /* SPI清除函数,当spi_master被释放时调用 */int (*transfer_one_message)(struct spi_master *master,                    /* SPI发送函数2 */int (*transfer_one)(struct spi_master *master, struct spi_device *spi,    /* SPI发送函数3 */struct spi_transfer *transfer);...
};

  上述为struct spi_master数据结构,结构体本身比较大,挑选了一些个人认为比较核心的成员。
  该结构体为spi控制器驱动核心数据结构。控制器驱动需要配置其中一些参数,填充一些回调函数。
  需要注意的是,上述代码段中,有三个transfer函数。这里先简单的说明一下,在spi_master注册时,会首先判断transfer函数是否实现,如果没实现,核心层会自动填充一个默认的transfer函数。当填充了默认的transfer函数后,会判断控制器驱动是否实现transfer_one_message,如果没实现,则核心层会填充一个默认的transfer_one_message函数,最后控制器驱动只需要实现transfer_one回调函数,就可以让驱动层实现spi数据收发了。
  当然,有些厂商会自己实现transfer或transfer_one_message函数,替代内核默认的逻辑。不过总结来说,三个transfer函数只需要实现其中一个就可以了。

struct 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比较简单,根据上文介绍的spi_match_device逻辑,只需要实现of_match_table或acpi_match_table或id_table,或者最简单的driver->name,能让spi_match_device返回match成功即可。match成功会调用probe函数。remove函数在驱动卸载时调用。
  spi_device数据结构就不介绍了,一般用设备树,会自动生成。

struct spi_transfer & struct spi_message

struct spi_transfer {const void  *tx_buf;void        *rx_buf;unsigned    len;...u8       bits_per_word;u16       delay_usecs;u32     speed_hz;...struct list_head transfer_list;
};struct spi_message {struct list_head  transfers;...struct spi_device  *spi;...void            (*complete)(void *context);void         *context;...struct list_head    queue;
};

  一个spi_message是一次数据交换的原子请求,而spi_message由多个spi_transfer结构组成,这些spi_transfer通过一个链表组织在一起。

  spi_message通过spi_sync函数或spi_async函数发送。

核心API

spi_register_master函数

int spi_register_master(struct spi_master *master)
{...status = of_spi_register_master(master);                           (1)if (status)return status;...dev_set_name(&master->dev, "spi%u", master->bus_num);status = device_add(&master->dev);                                  (2)if (status < 0)goto done;dev_dbg(dev, "registered master %s%s\n", dev_name(&master->dev),dynamic ? " (dynamic)" : "");if (master->transfer)dev_info(dev, "master is unqueued, this is deprecated\n");else {status = spi_master_initialize_queue(master);                   (3)if (status) {device_del(&master->dev);goto done;}}...mutex_lock(&board_lock);list_add_tail(&master->list, &spi_master_list);list_for_each_entry(bi, &board_list, list)spi_match_master_to_boardinfo(master, &bi->board_info);           (4)mutex_unlock(&board_lock);...of_register_spi_devices(master);                                    (5)...
done:return status;
}

  spi_register_master是控制器驱动中最核心的api,控制器驱动通过该api注册到系统中。
说明:
(1)of_spi_register_master函数内部,根据设备树节点中的"cs-gpios",向struct spi_master添加gpio cs引脚。
(2)将device注册到设备模型中。
(3)如果控制器驱动没有自己实现transfer函数,则初始化发送队列。(核心层填充默认transfer函数)
(4)老的方式,遍历所有spi_board_info数据结构,并注册spi_device
(5)新的设备树方式,遍历spi控制器节点下所有子节点,并注册成对应的spi_device设备。
  再来仔细看下spi_master_initialize_queue函数

static int spi_master_initialize_queue(struct spi_master *master)
{......master->queued = true;master->transfer = spi_queued_transfer;if (!master->transfer_one_message)master->transfer_one_message = spi_transfer_one_message;/* Initialize and start queue */ret = spi_init_queue(master);......ret = spi_start_queue(master);......
}

  该函数把master->transfer回调字段设置为默认的实现函数:spi_queued_transfer,如果控制器驱动没有实现transfer_one_message回调,用默认的spi_transfer_one_message函数进行赋值。然后分别调用spi_init_queue和spi_start_queue函数初始化队列并启动工作线程。spi_init_queue函数最主要的作用就是建立一个内核工作线程。
  然后来一起看下spi_init_queue和spi_start_queue函数

static int spi_init_queue(struct spi_master *master)
{......kthread_init_worker(&master->kworker);master->kworker_task = kthread_run(kthread_worker_fn,       &master->kworker, "%s", dev_name(&master->dev));......kthread_init_work(&master->pump_messages, spi_pump_messages);......return 0;
}static int spi_start_queue(struct spi_master *master)
{......master->running = true;master->cur_msg = NULL;......kthread_queue_work(&master->kworker, &master->pump_messages);......return 0;
}

  spi_init_queue函数先初始化kthread_worker,为kthread_worker创建一个内核线程来处理work,随后初始化kthread_work,设置work执行函数,work执行函数为spi_pump_messages
  spi_start_queue就相对简单了,只是唤醒该工作线程而已;自此,队列化的相关工作已经完成,系统等待message请求被发起,然后在工作线程中处理message的传送工作。

spi_message_init函数

static inline void spi_message_init_no_memset(struct spi_message *m)
{INIT_LIST_HEAD(&m->transfers);INIT_LIST_HEAD(&m->resources);
}
static inline void spi_message_init(struct spi_message *m)
{memset(m, 0, sizeof *m);spi_message_init_no_memset(m);
}

  spi_message_init函数用于对spi_massage进行初始化,并初始化链表头。

spi_message_add_tail函数

static inline void
spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
{list_add_tail(&t->transfer_list, &m->transfers);
}

  spi_message_add_tail函数用于把spi_transfer添加到spi_message传输链表中。
一般spi设备驱动的发送函数,会定义spi_message和若干个spi_transfer,设置spi_transfer的txbuff/rxbuff,len等,调用spi_message_init函数初始化spi_message,调用spi_message_add_tail把spi_transfer添加到spi_message链表中。最后调用spi_sync或spi_async发送。

spi_async

  spi_async是异步执行的,不会等待传输是否完成,就直接返回。调用关系为:

spi_async->__spi_async->master->transfer

  在上文中的spi_register_master函数中,我们已经介绍过,当控制器驱动没有实现transfer函数时,内核会自己填充默认的transfer函数(spi_queued_transfer)。这里我们假设控制器驱动既没有实现transfer,也没有实现transfer_one_message,只实现了transfer_one函数。来看一下内核默认transfer函数的逻辑。

spi_queued_transfer(struct spi_device *spi, struct spi_message *msg)->__spi_queued_transfer(spi, msg, true)static int __spi_queued_transfer(struct spi_device *spi,struct spi_message *msg,bool need_pump)
{struct spi_master *master = spi->master;unsigned long flags;spin_lock_irqsave(&master->queue_lock, flags);...list_add_tail(&msg->queue, &master->queue);if (!master->busy && need_pump)kthread_queue_work(&master->kworker, &master->pump_messages);spin_unlock_irqrestore(&master->queue_lock, flags);return 0;
}

  这里我们可以看到,__spi_queued_transfer(spi, msg, true)函数中,注意这里第三个参数是true,把message添加到master的发送链表中。随后仅仅是唤醒工作线程,完成该步后,spi_async即返回,不等待发送完成。

spi_sync

  与spi_async不同,spi_sync是同步执行的,会等待传输完成,随后返回。调用关系为:

spi_sync->__spi_sync

  然后看一下__spi_sync函数代码,这里我们也假设控制器驱动既没有实现transfer,也没有实现transfer_one_message,只实现了transfer_one函数:

static void spi_complete(void *arg)
{complete(arg);
}static int __spi_sync(struct spi_device *spi, struct spi_message *message)
{DECLARE_COMPLETION_ONSTACK(done);int status;struct spi_master *master = spi->master;unsigned long flags;...message->complete = spi_complete;                                   (1)message->context = &done;message->spi = spi;SPI_STATISTICS_INCREMENT_FIELD(&master->statistics, spi_sync);SPI_STATISTICS_INCREMENT_FIELD(&spi->statistics, spi_sync);/* If we're not using the legacy transfer method then we will* try to transfer in the calling context so special case.* This code would be less tricky if we could remove the* support for driver implemented message queues.*/if (master->transfer == spi_queued_transfer) {spin_lock_irqsave(&master->bus_lock_spinlock, flags);trace_spi_message_submit(message);status = __spi_queued_transfer(spi, message, false);            (2)spin_unlock_irqrestore(&master->bus_lock_spinlock, flags);} else {status = spi_async_locked(spi, message);}if (status == 0) {/* Push out the messages in the calling context if we* can.*/if (master->transfer == spi_queued_transfer) {SPI_STATISTICS_INCREMENT_FIELD(&master->statistics,spi_sync_immediate);SPI_STATISTICS_INCREMENT_FIELD(&spi->statistics,spi_sync_immediate);__spi_pump_messages(master, false);                          (3)}wait_for_completion(&done);                                     (4)status = message->status;}message->context = NULL;return status;
}

说明:
(1)设置spi_message完成后回调函数,回调函数中仅仅调用complete函数,唤醒等待唤醒的线程。意思就是,当发送spi_message的工作线程完成发送后,唤醒在等待的spi发送完成的spi_sync函数所在的线程。
(2)由于使用内核默认的spi_queued_transfer,所以会调用__spi_queued_transfer(spi, message, false);这个函数上文介绍spi_async时已经介绍过了,注意这里第三个传参是false。所以所做的操作仅仅是把spi_message添加到master的发送链表中。随即返回
(3)__spi_pump_messages(master, false),false意思是标记不在工作线程中执行该函数。调用该函数,把spi_message发出去,该函数中会判断当前状态,如果可以直接发,则直接在当前线程中发送,如果不能直接发,则唤醒工作线程,稍后发送。
(4)睡眠等待发送完成函数释放的完成信号量,当接收到信号量时,证明发送完成唤醒并结束spi_sync函数。

  工作线程的工作函数是spi_pump_messages来看一下该函数实现:

static void spi_pump_messages(struct kthread_work *work)
{struct spi_master *master =container_of(work, struct spi_master, pump_messages);__spi_pump_messages(master, true);
}

  上文中不管是spi_sync,还是spi_async中,不管是直接调用该函数中的__spi_pump_messages(sync)还是通过工作线程,作为线程服务函数调用,最终发送数据都是通过__spi_pump_messages实现的。
  与__spi_sync中不同的是工作函数中调用的__spi_pump_messages(master, true);第二个参数为true,标记为是否在工作线程中。
  最后来看一下__spi_pump_messages函数的实现:

static void __spi_pump_messages(struct spi_master *master, bool in_kthread)
{unsigned long flags;bool was_busy = false;int ret;/* Lock queue */spin_lock_irqsave(&master->queue_lock, flags);/* Make sure we are not already running a message */if (master->cur_msg) {spin_unlock_irqrestore(&master->queue_lock, flags);return;}/* If another context is idling the device then defer */if (master->idling) {kthread_queue_work(&master->kworker, &master->pump_messages);spin_unlock_irqrestore(&master->queue_lock, flags);return;}/* Check if the queue is idle */if (list_empty(&master->queue) || !master->running) {if (!master->busy) {spin_unlock_irqrestore(&master->queue_lock, flags);return;}/* Only do teardown in the thread */if (!in_kthread) {kthread_queue_work(&master->kworker,&master->pump_messages);spin_unlock_irqrestore(&master->queue_lock, flags);return;}master->busy = false;master->idling = true;spin_unlock_irqrestore(&master->queue_lock, flags);kfree(master->dummy_rx);master->dummy_rx = NULL;kfree(master->dummy_tx);master->dummy_tx = NULL;if (master->unprepare_transfer_hardware &&master->unprepare_transfer_hardware(master))dev_err(&master->dev,"failed to unprepare transfer hardware\n");if (master->auto_runtime_pm) {pm_runtime_mark_last_busy(master->dev.parent);pm_runtime_put_autosuspend(master->dev.parent);}trace_spi_master_idle(master);spin_lock_irqsave(&master->queue_lock, flags);master->idling = false;spin_unlock_irqrestore(&master->queue_lock, flags);return;}/* Extract head of queue */master->cur_msg =list_first_entry(&master->queue, struct spi_message, queue);list_del_init(&master->cur_msg->queue);if (master->busy)was_busy = true;elsemaster->busy = true;spin_unlock_irqrestore(&master->queue_lock, flags);mutex_lock(&master->io_mutex);if (!was_busy && master->auto_runtime_pm) {ret = pm_runtime_get_sync(master->dev.parent);if (ret < 0) {dev_err(&master->dev, "Failed to power device: %d\n",ret);mutex_unlock(&master->io_mutex);return;}}if (!was_busy)trace_spi_master_busy(master);if (!was_busy && master->prepare_transfer_hardware) {ret = master->prepare_transfer_hardware(master);if (ret) {dev_err(&master->dev,"failed to prepare transfer hardware\n");if (master->auto_runtime_pm)pm_runtime_put(master->dev.parent);mutex_unlock(&master->io_mutex);return;}}trace_spi_message_start(master->cur_msg);if (master->prepare_message) {ret = master->prepare_message(master, master->cur_msg);if (ret) {dev_err(&master->dev,"failed to prepare message: %d\n", ret);master->cur_msg->status = ret;spi_finalize_current_message(master);goto out;}master->cur_msg_prepared = true;}ret = spi_map_msg(master, master->cur_msg);if (ret) {master->cur_msg->status = ret;spi_finalize_current_message(master);goto out;}ret = master->transfer_one_message(master, master->cur_msg);if (ret) {dev_err(&master->dev,"failed to transfer one message from queue\n");goto out;}out:mutex_unlock(&master->io_mutex);/* Prod the scheduler in case transfer_one() was busy waiting */if (!ret)cond_resched();
}

  函数源码比较多,总结一下大概可以概括为,作为工作线程服务函数时,会调用master->transfer_one_message函数。当__spi_sync中调用该函数时,如果资源没有冲突,可以直接在当前线程中调用transfer_one_message则调用,如果不能,则添加到工作线程队列中,通过工作线程再次调用__spi_pump_messages,把数据发送出去。
  在上文介绍spi_register_master->spi_master_initialize_queue中,我们已经看到,内核默认填充的master->transfer_one_message函数为spi_transfer_one_message,下面来看一下源码:

static int spi_transfer_one_message(struct spi_master *master,struct spi_message *msg)
{struct spi_transfer *xfer;...spi_set_cs(msg->spi, true);                                    /* cs引脚拉高 */SPI_STATISTICS_INCREMENT_FIELD(statm, messages);SPI_STATISTICS_INCREMENT_FIELD(stats, messages);list_for_each_entry(xfer, &msg->transfers, transfer_list) {trace_spi_transfer_start(msg, xfer);spi_statistics_add_transfer_stats(statm, xfer, master);spi_statistics_add_transfer_stats(stats, xfer, master);if (xfer->tx_buf || xfer->rx_buf) {reinit_completion(&master->xfer_completion);ret = master->transfer_one(master, msg->spi, xfer);      (1)if (ret < 0) {SPI_STATISTICS_INCREMENT_FIELD(statm,errors);SPI_STATISTICS_INCREMENT_FIELD(stats,errors);dev_err(&msg->spi->dev,"SPI transfer failed: %d\n", ret);goto out;}...}
out:if (ret != 0 || !keep_cs)spi_set_cs(msg->spi, false);                       /* 拉低cs */spi_finalize_current_message(master);                         (2)return ret;
}void spi_finalize_current_message(struct spi_master *master)
{...kthread_queue_work(&master->kworker, &master->pump_messages);...mesg->state = NULL;if (mesg->complete)mesg->complete(mesg->context);
}

  提炼了一下代码,(1)可以看到函数中先是遍历了message中的transfer链表,然后一个个调用控制器驱动的transfer_one函数。随后(2)调用spi_finalize_current_message函数,函数代码也在代码段中贴出来了。重要的两个操作一个是,唤醒下一次工作线程,然后判断message的回调函数是否填充,如果存在,则回调。对于spi_sync来说,message的回调函数是complete(&done),spi_sync阻塞的wait_for_completion(&done)接收到信号量会被唤醒,意味着数据以及收发完成,结束本次spi_sync。

spi_sync&spi_async总结

  spi_sync和spi_async是spi设备驱动通过spi总线发送数据的重要函数。经过上文的分析。简单的总结下。
  spi_sync函数是同步的,把spi_message发送出去,会等待发送完成,发送完成才会返回,正常情况下调用的都是spi_sync函数。
  spi_async函数本身的异步的,把spi_message推到工作线程中,就不管了,所以试想一下,当发送数据时,可以不管是否发送完成。但是读取数据时,如果一次发送(数据交换)没有完成就返回了,那么rx_buff中是否有数据时不能保障的。那么,spi_async是不是就不能用来读取数据了呢?
  答案是否定的,可以仿照spi_sync,在spi_async调用之前,手动设置spi_message的逻辑,不过有点麻烦就是了,还不如直接调用spi_sync。。

static void spi_complete(void *arg)
{complete(arg);
}{...
message->complete = spi_complete;
message->context = &done;
...
spi_async(spi, message);
...
wait_for_completion(&done);
...
}

SPI驱动框架链接:
Linux SPI驱动框架(2)——控制器驱动层
Linux SPI驱动框架(3)——设备驱动层

Linux SPI驱动框架(1)——核心层相关推荐

  1. Linux SPI驱动框架(2)——控制器驱动层

    SPI控制器驱动层   上节中,讲了SPI核心层的东西,这一部分,以全志平台SPI控制器驱动为例,对SPI控制器驱动进行说明. SPI控制器驱动,即SPI硬件控制器对应的驱动,核心部分需要实现硬件SP ...

  2. Linux SPI驱动框架(3)——设备驱动层

    SPI设备驱动层   Linux SPI驱动框架(1)和(2)中分别介绍了SPI框架中核心层,和控制器驱动层.其实实际开发过程中,不是IC原厂工程师比较少会接触控制器驱动层,设备驱动层才是接触比较多的 ...

  3. i.MX6ULL驱动开发 | 13 - Linux SPI 驱动框架

    Linux SPI 驱动框架分为两部分: SPI总线控制器驱动:SOC的 SPI 控制器外设驱动 SPI设备驱动:基于SPI总线控制器驱动编写,针对具体的SPI从机设备 一.SPI总线控制器驱动 基于 ...

  4. linux spi不使用框架,Linux spi驱动框架之执行流程

    Linux spi驱动架构由三部分构成:SPI核心层.SPI控制器驱动层.和SPI设备驱动程序. 1.SPI核心层: SPI核心层是Linux的SPI核心部分,提供了核心数据结构的定义.SPI控制器驱 ...

  5. Linux spi驱动框架之执行流程-nuc970-att7022

    转载地址:http://blog.csdn.net/chenliang0224/article/details/51236499 Linux spi驱动架构由三部分构成:SPI核心层.SPI控制器驱动 ...

  6. i.MX6ULL驱动开发 | 14 - 基于 Linux SPI 驱动框架读取ICM-20608传感器

    本系列文章驱动源码仓库,欢迎Star~ https://github.com/Mculover666/linux_driver_study. 一.ICM20608 1. 简介 InvenSense 的 ...

  7. SPI驱动框架源码分析

     SPI驱动框架源码分析 2013-04-12 16:13:08 分类: LINUX SPI驱动框架源码分析 SPI协议是一种同步的串行数据连接标准,由摩托罗拉公司命名,可工作于全双工模式.相关通讯设 ...

  8. Linux驱动修炼之道-SPI驱动框架源码分析(上)

    Linux驱动修炼之道-SPI驱动框架源码分析(上)   SPI协议是一种同步的串行数据连接标准,由摩托罗拉公司命名,可工作于全双工模式.相关通讯设备可工作于m/s模式.主设备发起数据帧,允许多个从设 ...

  9. imx6 通过移植XRM117x(SPI转串口)对Linux中的SPI驱动框架进行分析

    最近分析了一下Linux 中的SPI驱动框架,将自己的理解总结一下,不足之处还请斧正! 1.SPI通信基础知识 SPI(Serial Peripheral Interface)是一种串行(一次发送1b ...

最新文章

  1. mac下安装libpng环境
  2. [微信开发] 开发指南笔记
  3. NFS 网络挂载问题 解决
  4. Navicat连接Mysql 8.0.16报错:Client does not support authentication protocol requested by server?
  5. 卢伟冰疑似用上Redmi K30S:今年最后一款骁龙865旗舰
  6. SVN missing 解决
  7. python ttk separator_Python3 tkinter基础 Menu add_cascade 多级菜单 add_separator 分割线
  8. IE6.0中js优化
  9. QQ自动发送消息——维持群聊炽焰
  10. android 播放assets下视频,安卓播放assets文件里视频文件相关问题分析
  11. 极小化极大;292Nim 游戏;bitset容器;464我能赢吗;486预测赢家
  12. 学生用计算机怎么去掉,怎样把学生使用的计算器关掉
  13. 小米手机刷android one,小米手机(Mi One)刷机教程详解完整版 (刷MIUI官方刷机包)...
  14. 分数加减法混合计算机,新苏教版小学五年级下册数学《5.2 分数加、减法混合运算》教案教学设计...
  15. 数据挖掘ID3算法详解
  16. 冷战背景下的计算机,袁岚峰:鼓吹科技冷战,格调太低
  17. 工业企业数字化转型--设备管理运维系统
  18. DAEMON Tools Ultra(虚拟光驱超级版)v5.5.0.1046免费版
  19. 诸葛亮是刘备最器重的人才么
  20. Spring 实现屏幕捕获-屏幕共享

热门文章

  1. 随机生成游戏角色昵称(使用Excel配置XML文件)上
  2. 至强秘笈 | 英特尔
  3. 基于Opencv+Mediapipe实现手势追踪
  4. 乐pro3刷LineageOS 出现错误07
  5. 一个java程序员的非全日制软工硕士之路
  6. 微信小程序原生集成vant weapp注意点 (https://youzan.github.io/vant-weapp/#/intro)
  7. CSS字体样式属性汇总
  8. Win10《芒果TV》更新v3.5.2星玥版:修复电视台直播异常,优化添加下载提示
  9. 2019 蓝桥杯省赛 B 组模拟赛(一)
  10. 前端工程师需要了解的知识点