下图是安装成功后,sys目录下的主要结构,由于目录非常复杂仅仅列出了主要的结构
sys目录下spi子系统结构
接下来将从各struct开始进行分析,struct是构成内核对象的基础,函数是动态的构建和执行的工具。所以梳理脉络就从结构开始。
linux下的设备模型包括几个主要的概念
sysfs (dev是用户空间接口,根据sysfs下的class目录由mdev负责建立)
bus总线,linux下的设备都是建立在总线上的,platform总线是一个虚拟的总线,所有的的片上设备基本上都接在这个虚拟总线上
device是设备
device_driver是设备驱动
class是类别,从功能角度对设备进行分类
注意,在sys/bus目录下有platform目录和spi目录
这两个目录下面的设备分别代表什么呢?
platform下的设备有s3c64xx-spi0和s3c64xx-spi1分别对应了s3c6410上的spi0和spi1接口
一、先说说platformplatform.txt是需要阅读的参考文档platform子系统是linux对不同的架构下设备的抽象归纳,即所有片上的设备都会放在这个子目录下
我们先看一下platform相关的struct
struct platform_device {
const char * name;
int id;
struct device dev;
u32 num_resources;
struct resource * resource;
const struct platform_device_id *id_entry;
/* arch specific additions */
struct pdev_archdata archdata;
};
platform_device从字面理解就是平台设备,注意它内含了一个名为dev的device结构,这有点像C++的类继承的关系,linux内核大量利用了这种类似继承的结构实现了C语言下的面向对象编程。
后面谈到的SPI的设备继承了platform_device的结构。 另外,还包含了一个resource 结构的指针,注意和dev的区别。也就是说资源是需要另外定义然后,将对象指针赋予这里的结构指针。资源包含IOMEM和IRQ。
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver;
const struct platform_device_id *id_table;
};
platform_driver结构就不多说了,主要是用来说明platform驱动的结构。
下面看看对象实例s3c64xx-spi.0的定义
static struct resource s3c64xx_spi0_resource[] = {
[0] = {
.start = S3C64XX_PA_SPI0,
.end = S3C64XX_PA_SPI0 + 0x100 - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = DMACH_SPI0_TX,
.end = DMACH_SPI0_TX,
.flags = IORESOURCE_DMA,
},
[2] = {
.start = DMACH_SPI0_RX,
.end = DMACH_SPI0_RX,
.flags = IORESOURCE_DMA,
},
[3] = {
.start = IRQ_SPI0,
.end = IRQ_SPI0,
.flags = IORESOURCE_IRQ,
},
};
static struct s3c64xx_spi_info s3c64xx_spi0_pdata = {
.cfg_gpio = s3c64xx_spi_cfg_gpio,
.fifo_lvl_mask = 0x7f,
.rx_lvl_offset = 13,
};
static u64 spi_dmamask = DMA_BIT_MASK(32);
struct platform_device s3c64xx_device_spi0 = {
.name = "s3c64xx-spi",
.id = 0,
.num_resources = ARRAY_SIZE(s3c64xx_spi0_resource),
.resource = s3c64xx_spi0_resource,
.dev = {
.dma_mask = &spi_dmamask,
.coherent_dma_mask = DMA_BIT_MASK(32),
.platform_data = &s3c64xx_spi0_pdata,
},
};
从上面的定义可以看出,s3c64xx_device_spi0是s3c64xx-spi.0的定义,系统根据这个变量实现了s3c64xx-spi.0,而它的类型是platform_device,也就是说这是一个平台设备。s3c64xx-spi.1就不说了同理。
下面是驱动s3c64xx_spi_driver 的定义,系统根据这个定义实现了平台驱动s3c64xx-spi
static struct platform_driver s3c64xx_spi_driver = {
.driver = {
.name = "s3c64xx-spi",
.owner = THIS_MODULE,
},
.remove = s3c64xx_spi_remove,
.suspend = s3c64xx_spi_suspend,
.resume = s3c64xx_spi_resume,
};
注意驱动的定义中含有很多的函数指针,这些函数是用来控制设备工作的标准动作。具体功能这里就不解释了,各位看源码。
各位看到这里可能要问了那s3c64xx-spi.0下的spi0.0是在哪里实现的?它的类结构是什么?
首先,我们得明白s3c64xx-spi.0代表了s3c6410下的spi0接口,那么spi0.0代表什么呢?注意spi0.0有两个数字,第一个0代表spi0接口,第2个0呢?它代表这spi0总线上的片外设备,这意味着一个spi0总线可以接多个片外设备,通过cs进行片选。属于分时复用,那么spi0.0的定义在什么地方呢?首先,它和开发板的定义高度相关,也就是说它不会存在于内核代码,也不会存在于芯片厂商三星公司提供的s3c64xx系列代码中,那么它应该在什么地方呢?答案是它应该在开发板的初始化代码中,即在友善之臂的mach_mini6410.c中,很遗憾友善之臂暂时没有支持spi,spi的相关初始化代码需要我们自行定义,如何定义这里暂且不说,先说说这个设备的类型定义在哪里?
下面就是片外设备的定义,s3c64xx_spi_csinfo是片外设备需要的片选信号的定义,这个片选信号的定义是必须的,很多网上的帖子并没有介绍到,这也是我之前很郁闷的地方一直无法成功的加载设备。
/**
* struct s3c64xx_spi_csinfo - ChipSelect description
* @fb_delay: Slave specific feedback delay.
* Refer to FB_CLK_SEL register definition in SPI chapter.
* @line: Custom 'identity' of the CS line.
* @set_level: CS line control.
*
* This is per SPI-Slave Chipselect information.
* Allocate and initialize one in machine init code and make the
* spi_board_info.controller_data point to it.
*/
struct s3c64xx_spi_csinfo {
u8 fb_delay;
unsigned line;
void (*set_level)(unsigned line_id, int lvl);
};
/*
* INTERFACE between board init code and SPI infrastructure.
*
* No SPI driver ever sees these SPI device table segments, but
* it's how the SPI core (or adapters that get hotplugged) grows
* the driver model tree.
*
* As a rule, SPI devices can't be probed. Instead, board init code
* provides a table listing the devices which are present, with enough
* information to bind and set up the device's driver. There's basic
* support for nonstatic configurations too; enough to handle adding
* parport adapters, or microcontrollers acting as USB-to-SPI bridges.
*/
/**
* struct spi_board_info - board-specific template for a SPI device
* @modalias: Initializes spi_device.modalias; identifies the driver.
* @platform_data: Initializes spi_device.platform_data; the particular
* data stored there is driver-specific.
* @controller_data: Initializes spi_device.controller_data; some
* controllers need hints about hardware setup, e.g. for DMA.
* @irq: Initializes spi_device.irq; depends on how the board is wired.
* @max_speed_hz: Initializes spi_device.max_speed_hz; based on limits
* from the chip datasheet and board-specific signal quality issues.
* @bus_num: Identifies which spi_master parents the spi_device; unused
* by spi_new_device(), and otherwise depends on board wiring.
* @chip_select: Initializes spi_device.chip_select; depends on how
* the board is wired.
* @mode: Initializes spi_device.mode; based on the chip datasheet, board
* wiring (some devices support both 3WIRE and standard modes), and
* possibly presence of an inverter in the chipselect path.
*
* When adding new SPI devices to the device tree, these structures serve
* as a partial device template. They hold information which can't always
* be determined by drivers. Information that probe() can establish (such
* as the default transfer wordsize) is not included here.
*
* These structures are used in two places. Their primary role is to
* be stored in tables of board-specific device descriptors, which are
* declared early in board initialization and then used (much later) to
* populate a controller's device tree after the that controller's driver
* initializes. A secondary (and atypical) role is as a parameter to
* spi_new_device() call, which happens after those controller drivers
* are active in some dynamic board configuration models.
*/
struct spi_board_info {
/* the device name and module name are coupled, like platform_bus;
* "modalias" is normally the driver name.
*
* platform_data goes to spi_device.dev.platform_data,
* controller_data goes to spi_device.controller_data,
* irq is copied too
*/
char modalias[SPI_NAME_SIZE];
const void *platform_data;
void *controller_data;
int irq;
/* slower signaling on noisy or low voltage boards */
u32 max_speed_hz;
/* bus_num is board specific and matches the bus_num of some
* spi_master that will probably be registered later.
*
* chip_select reflects how this chip is wired to that master;
* it's less than num_chipselect.
*/
u16 bus_num;
u16 chip_select;
/* mode becomes spi_device.mode, and is essential for chips
* where the default of SPI_CS_HIGH = 0 is wrong.
*/
u8 mode;
/* ... may need additional spi_device chip config data here.
* avoid stuff protocol drivers can set; but include stuff
* needed to behave without being bound to a driver:
* - quirks like clock rate mattering when not selected
*/
};
上面的介绍已经将平台下的各设备及驱动的定义讲清楚了
二、接下来介绍SPI目录下的各对象的定义。spi-summary.txt是必须阅读的参考文档从spi目录可以看出这是与平台无关的,也就是linux对spi接口进行抽象,形成的用户层相关代码,而platform下面的代码是具体进行工作的代码,它们与平台相关,而spi代码是与用户空间接口相关的代码它必须与平台无关,即无论平台如何更换,对用户空间的接口都是一致的,这就是linux设备驱动架构的精髓。
我们注意到spi目录下只有一个spidev的驱动,其他都是指向platform下的设备链接,这里先不说整个spi子系统的运作机制,先搞清楚spi子系统的静态结构
spidev是由spidev_spi_driver 定义的,它的结构类型是spi_driver
static struct spi_driver spidev_spi_driver = {
.driver = {
.name = "spidev",
.owner = THIS_MODULE,
},
.probe = spidev_probe,
.remove = __devexit_p(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.
*/
};
spi_driver 的定义如下:
/**
* struct spi_driver - Host side "protocol" driver
* @id_table: List of SPI devices supported by this driver
* @probe: Binds this driver to the spi device. Drivers can verify
* that the device is actually present, and may need to configure
* characteristics (such as bits_per_word) which weren't needed for
* the initial configuration done during system setup.
* @remove: Unbinds this driver from the spi device
* @shutdown: Standard shutdown callback used during system state
* transitions such as powerdown/halt and kexec
* @suspend: Standard suspend callback used during system state transitions
* @resume: Standard resume callback used during system state transitions
* @driver: SPI device drivers should initialize the name and owner
* field of this structure.
*
* This represents the kind of device driver that uses SPI messages to
* interact with the hardware at the other end of a SPI link. It's called
* a "protocol" driver because it works through messages rather than talking
* directly to SPI hardware (which is what the underlying SPI controller
* driver does to pass those messages). These protocols are defined in the
* specification for the device(s) supported by the driver.
*
* As a rule, those device protocols represent the lowest level interface
* supported by a driver, and it will support upper level interfaces too.
* Examples of such upper levels include frameworks like MTD, networking,
* MMC, RTC, filesystem character device nodes, and hardware monitoring.
*/
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);
int (*suspend)(struct spi_device *spi, pm_message_t mesg);
int (*resume)(struct spi_device *spi);
struct device_driver driver;
};
static inline struct spi_driver *to_spi_driver(struct device_driver *drv)
{
return drv ? container_of(drv, struct spi_driver, driver) : NULL;
}
extern int spi_register_driver(struct spi_driver *sdrv);
/**
* spi_unregister_driver - reverse effect of spi_register_driver
* @sdrv: the driver to unregister
* Context: can sleep
*/
static inline void spi_unregister_driver(struct spi_driver *sdrv)
{
if (sdrv)
driver_unregister(&sdrv->driver);
}
三、下面介绍class目录下的对象定义结构spi_master的定义如下
static struct class spi_master_class = {
.name = "spi_master",
.owner = THIS_MODULE,
.dev_release = spi_master_release,
};
spidev的类定义如下:
/*-------------------------------------------------------------------------*/
/* The main reason to have this class is to make mdev/udev create the
* /dev/spidevB.C character device nodes exposing our userspace API.
* It also simplifies memory management.
*/
static struct class *spidev_class;
文章写到这里,spi子系统主要的设备、驱动、类定义的结构已经介绍清楚了,即静态的结构大家已经明白了。
下面我们开始介绍SPI子系统的初始化过程,这也是我和大伙都很头痛的地方,SPI子系统是如何初始化的?
四、SPI子系统的初始化过程先说一下和SPI子系统初始化相关的主要代码
spi.c是spi子系统初始化的核心代码,由内核负责初始化
spidev.c是spi用户接口初始化的代码,编译的时候需要选择该模块
spi_s3c64xx.c是平台驱动的初始化代码,编译时需要选择spi s3c64xx模块
mach-mini6410.c是开发板初始化的代码
上述核心设备、驱动、类的初始化过程就是在上述代码中实现的,它们分别负责那些对象的初始化过程,次序是什么,需要各位思考一下,我也没有完全搞清楚。
先说说spi.c中的初始化代码
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);
if (status < 0)
goto err1;
status = class_register(&spi_master_class);
if (status < 0)
goto err2;
return 0;
err2:
bus_unregister(&spi_bus_type);
err1:
kfree(buf);
buf = NULL;
err0:
return status;
}
从这段代码,我们可以看出,系统注册及初始化了总线spi和类spi_master
总线spi的定义代码前面没有列出,这里补充如下:
struct bus_type spi_bus_type = {
.name = "spi",
.dev_attrs = spi_dev_attrs,
.match = spi_match_device,
.uevent = spi_uevent,
.suspend = spi_suspend,
.resume = spi_resume,
};
这个初始化过程是内核在初始化过程中,调用spi_init(void)函数执行的,由宏postcore_initcall(spi_init);加入到启动代码。
platform总线的初始化代码由platform.c中的函数platform_bus_init执行
int __init platform_bus_init(void)
{
int error;
early_platform_cleanup();
error = device_register(&platform_bus);
if (error)
return error;
error = bus_register(&platform_bus_type);
if (error)
device_unregister(&platform_bus);
return error;
}
下面是用户空间接口的初始化代码,从代码可以看出,初始化了字符设备spi
注册&初始化类spidev
注册&初始化驱动spidev
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, "spi", &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 int __init s3c64xx_spi_init(void)
{
return platform_driver_probe(&s3c64xx_spi_driver, s3c64xx_spi_probe);
}
int __init_or_module platform_driver_probe(struct platform_driver *drv,
int (*probe)(struct platform_device *))
{
int retval, code;
/* make sure driver won't have bind/unbind attributes */
drv->driver.suppress_bind_attrs = true;
/* temporary section violation during probe() */
drv->probe = probe;
retval = code = platform_driver_register(drv);
/*
* Fixup that section violation, being paranoid about code scanning
* the list of drivers in order to probe new devices. Check to see
* if the probe was successful, and make sure any forced probes of
* new devices fail.
*/
spin_lock(&platform_bus_type.p->klist_drivers.k_lock);
drv->probe = NULL;
if (code == 0 && list_empty(&drv->driver.p->klist_devices.k_list))
retval = -ENODEV;
drv->driver.probe = platform_drv_probe_fail;
spin_unlock(&platform_bus_type.p->klist_drivers.k_lock);
if (code != retval)
platform_driver_unregister(drv);
return retval;
}
上面这段代码注册了平台SPI驱动s3c64xx-spi
看到这里,我们可以发现还有什么活没有干?
s3c64xx-spi.0和s3c64xx-spi.1设备没有建立,它们下面的spi0.0和spi1.0设备也没有建立。
spidev类下面的spidev0.0和spidev1.0也没有建立
spi_master类下面的spi0和spi1也没有建立
这些都是在什么地方建立的?分析一下这些都与具体的开发板有关系,也就是说这部分代码必须由开发板的初始化代码来实现,这里必须由mach-mini6410.c负责实现。
mach-mini6410.c的初始化函数是
static void __init mini6410_machine_init(void)
也就是说上述工作必须在这个函数中实现。
这里到了本文的高潮部分了,也是很多兄弟姐妹们迫切希望直接拷贝然后加入到自己的代码中去的地方。但是请注意,我也会犯错,这里的代码我也许说的并不对,各位看官如果发现了错误请指出及批判。
我们先分析s3c64xx-spi.0和s3c64xx-spi.1是在什么地方定义的,因为它与平台架构相关,所以我们到arch/arm/mach-s3c64xx目录中找到了dev-spi.c,这个代码中定义了s3c64xx_device_spi0和s3c64xx_device_spi1也就是设备s3c64xx-spi.0和s3c64xx-spi.1的实现代码,所以dev-spi.c必须加入到编译中,需要修改相关的编译配置文件。
然后,我们在分析mini6410的初始化代码,注意代码中的这个函数
platform_add_devices(mini6410_devices, ARRAY_SIZE(mini6410_devices));
可以看出该函数是批量初始化了一批设备,所以将
&s3c64xx_device_spi0,
&s3c64xx_device_spi1,
加入到mini6410_devices结构中,就可以将设备s3c64xx-spi.0和s3c64xx-spi.1进行初始化,
但是事情没有做完,s3c64xx_device_spi0和s3c64xx_device_spi1结构中的片选数量和时钟并未定义,需要根据开发板的具体情况进行定义,
函数原型 void __init s3c64xx_spi_set_info(int cntrlr, int src_clk_nr, int num_cs)
这里具体的执行代码如下:
s3c64xx_spi_set_info(0,0,1);
s3c64xx_spi_set_info(1,0,1);
到这里各位已经可以在平台目录中看见s3c64xx_device_spi0和s3c64xx_device_spi1,在类spi_master目录下看见spi0和spi1了
下面介绍如何增加spi0.0和spi1.0设备?各位请仔细分析在spi.h中的这段代码
/*---------------------------------------------------------------------------*/
/*
* INTERFACE between board init code and SPI infrastructure.
*
* No SPI driver ever sees these SPI device table segments, but
* it's how the SPI core (or adapters that get hotplugged) grows
* the driver model tree.
*
* As a rule, SPI devices can't be probed. Instead, board init code
* provides a table listing the devices which are present, with enough
* information to bind and set up the device's driver. There's basic
* support for nonstatic configurations too; enough to handle adding
* parport adapters, or microcontrollers acting as USB-to-SPI bridges.
*/
/**
* struct spi_board_info - board-specific template for a SPI device
* @modalias: Initializes spi_device.modalias; identifies the driver.
* @platform_data: Initializes spi_device.platform_data; the particular
* data stored there is driver-specific.
* @controller_data: Initializes spi_device.controller_data; some
* controllers need hints about hardware setup, e.g. for DMA.
* @irq: Initializes spi_device.irq; depends on how the board is wired.
* @max_speed_hz: Initializes spi_device.max_speed_hz; based on limits
* from the chip datasheet and board-specific signal quality issues.
* @bus_num: Identifies which spi_master parents the spi_device; unused
* by spi_new_device(), and otherwise depends on board wiring.
* @chip_select: Initializes spi_device.chip_select; depends on how
* the board is wired.
* @mode: Initializes spi_device.mode; based on the chip datasheet, board
* wiring (some devices support both 3WIRE and standard modes), and
* possibly presence of an inverter in the chipselect path.
*
* When adding new SPI devices to the device tree, these structures serve
* as a partial device template. They hold information which can't always
* be determined by drivers. Information that probe() can establish (such
* as the default transfer wordsize) is not included here.
*
* These structures are used in two places. Their primary role is to
* be stored in tables of board-specific device descriptors, which are
* declared early in board initialization and then used (much later) to
* populate a controller's device tree after the that controller's driver
* initializes. A secondary (and atypical) role is as a parameter to
* spi_new_device() call, which happens after those controller drivers
* are active in some dynamic board configuration models.
*/
struct spi_board_info {
/* the device name and module name are coupled, like platform_bus;
* "modalias" is normally the driver name.
*
* platform_data goes to spi_device.dev.platform_data,
* controller_data goes to spi_device.controller_data,
* irq is copied too
*/
char modalias[SPI_NAME_SIZE];
const void *platform_data;
void *controller_data;
int irq;
/* slower signaling on noisy or low voltage boards */
u32 max_speed_hz;
/* bus_num is board specific and matches the bus_num of some
* spi_master that will probably be registered later.
*
* chip_select reflects how this chip is wired to that master;
* it's less than num_chipselect.
*/
u16 bus_num;
u16 chip_select;
/* mode becomes spi_device.mode, and is essential for chips
* where the default of SPI_CS_HIGH = 0 is wrong.
*/
u8 mode;
/* ... may need additional spi_device chip config data here.
* avoid stuff protocol drivers can set; but include stuff
* needed to behave without being bound to a driver:
* - quirks like clock rate mattering when not selected
*/
};
这段代码中,spi_board_info 是spi总线上从设备的定义由于和硬件的具体种类有非常大的关系,所以需要针对具体的设备类型进行定义,这里先给一个例子
先在mach-mini6410.c中增加如下代码
//zhuyong add start
static void cs_set_level(unsigned line_id, int lvl) {
gpio_direction_output(line_id, lvl);
};
static struct s3c64xx_spi_csinfo s3c64xx_spi0_csinfo = {
.fb_delay=100,
.line=S3C64XX_GPC(3),
.set_level=cs_set_level,
};
static struct spi_board_info s3c6410_spi0_board[] = {
[0] = {
.modalias = "spidev",
.bus_num= 0,
.chip_select= 0, //必须小于s3c6410_spi0_platdata.num_cs
.irq =IRQ_SPI0,
.max_speed_hz= 500*1000,
.mode=SPI_MODE_0,
.controller_data=&s3c64xx_spi0_csinfo,
},
};
static struct s3c64xx_spi_csinfo s3c64xx_spi1_csinfo = {
.fb_delay=100,
.line=S3C64XX_GPC(7),
.set_level=cs_set_level,
};
static struct spi_board_info s3c6410_spi1_board[] = {
[0] = {
.modalias = "spidev",
.bus_num= 1,//代表使用芯片的第二个spi模块
.chip_select= 0, //必须小于s3c6410_spi1_platdata.num_cs
.irq = IRQ_SPI1,
.max_speed_hz = 500*1000,
.mode=SPI_MODE_0,
.controller_data=&s3c64xx_spi1_csinfo,
},
};
//zhuyong add end
为什么要定义s3c64xx_spi0_csinfo?这是spi从设备必须的片选信号的定义,片选信号中有一个关键的函数set_level需要各位自己进行定义,这个函数由系统在打开设备时调用,即设置该SPI从设备的片选信号,一开始由于没有设置该设备的片选信号,导致spi0.0和spi1.0一直加载不成功。
需要注意是,这里仅仅只是给出了一个例子,s3c64xx_spi0_info中还有很多具体的设置需要在该结构中进行定义。
最后在函数 mini6410_machine_init中,增加如下代码
spi_register_board_info(s3c6410_spi0_board, ARRAY_SIZE(s3c6410_spi0_board));
spi_register_board_info(s3c6410_spi1_board, ARRAY_SIZE(s3c6410_spi1_board));
这段代码必须加在
s3c64xx_spi_set_info(0,0,1);
s3c64xx_spi_set_info(1,0,1);
之后,至于为什么请各位自己思考,到这里所有的spi设备都已经初始化结束,在目录中已经可以看见本文开头的文件结构了。
下面就有两个议题了,
spi用户空间接口如何使用?
spi子系统是如何运行的?在黄埔江畔的星巴克写完这篇文章,已经是华灯初上了,对岸的外滩建筑美丽而宁静,这篇文章也用来纪念前段时间死去的成千上万的脑细胞。希望还能挽救各位兄弟姐妹的脑细胞。
email:
如果本文中有错误,请各位及时指出。
待续