一、综述
本文通过如何通过编写特定板子的spi master驱动从而识别到spi norflash设备,完成norflash设备的读写。
二、UCLASS架构解析
2.1 uclass
uclass可以理解为一些具有相同属性的udevice对外操作的接口,uclass的驱动是uclass_driver,主要为上层提供接口。
udevice的是指具体设备的抽象,对应驱动是driver,driver主要负责和硬件通信,为uclass提供实际的操作集。
udevice找到对应的uclass的方式主要是通过:udevice对应的driver的id和uclass对应的uclass_driver的id是否匹配。
udevice会和uclass绑定。driver会和udevice绑定。uclass_driver会和uclass绑定。
uclass和udevice都是动态生成的。在解析fdt中的设备的时候,会动态生成udevice。
然后找到udevice对应的driver,通过driver中的uclass id得到uclass_driver id。从uclass链表中查找对应的uclass是否已经生成,没有生成的话则动态生成uclass。
2.2 udevice
连接到对应uclass中
也就是会连接到uclass->dev_head中
连接到父设备的子设备链表中
也就是会连接到udevice->child_head中,并且最终的根设备是gd->dm_root这个根设备。
struct uclass {
void *priv; //uclass的私有数据
struct uclass_driver *uc_drv; //uclass类的操作函数集合
struct list_head dev_head; //该uclass的所有设备
struct list_head sibling_node; //下一个uclass的节点
};
2.3 uclass driver
主要函数:
struct uclass_driver {
const char *name; // 该uclass_driver的命令
enum uclass_id id; // 对应的uclass id
/* 以下函数指针主要是调用时机的区别 */
int (*post_bind)(struct udevice *dev); // 在udevice被绑定到该uclass之后调用
int (*pre_unbind)(struct udevice *dev); // 在udevice被解绑出该uclass之前调用
int (*pre_probe)(struct udevice *dev); // 在该uclass的一个udevice进行probe之前调用
int (*post_probe)(struct udevice *dev); // 在该uclass的一个udevice进行probe之后调用
int (*pre_remove)(struct udevice *dev);// 在该uclass的一个udevice进行remove之前调用
int (*child_post_bind)(struct udevice *dev); // 在该uclass的一个udevice的一个子设备被绑定到该udevice之后调用
int (*child_pre_probe)(struct udevice *dev); // 在该uclass的一个udevice的一个子设备进行probe之前调用
int (*init)(struct uclass *class); // 安装该uclass的时候调用
int (*destroy)(struct uclass *class); // 销毁该uclass的时候调用
int priv_auto_alloc_size; // 需要为对应的uclass分配多少私有数据
int per_device_auto_alloc_size; //
int per_device_platdata_auto_alloc_size; //
int per_child_auto_alloc_size; //
int per_child_platdata_auto_alloc_size; //
const void *ops; //操作集合
uint32_t flags; // 标识为
};
spi-uclass驱动:
UCLASS_DRIVER(spi) = {
.id = UCLASS_SPI,
.name = "spi",
.flags = DM_UC_FLAG_SEQ_ALIAS,
#if CONFIG_IS_ENABLED(OF_REAL)
.post_bind = dm_scan_fdt_dev,
#endif
.post_probe = spi_post_probe,
.child_pre_probe = spi_child_pre_probe,
.per_device_auto = sizeof(struct dm_spi_bus),
.per_child_auto = sizeof(struct spi_slave),
.per_child_plat_auto = sizeof(struct dm_spi_slave_plat),
#if CONFIG_IS_ENABLED(OF_REAL)
.child_post_bind = spi_child_post_bind,
#endif
};
//存放在段._u_boot_list_2_uclass_2_spi中,也就是section段的内容可以在uboot.map可以查看
想要获取uclass_driver需要先获取uclass_driver table。
struct uclass_driver *uclass =
ll_entry_start(struct uclass_driver, uclass);
// 会根据.u_boot_list_2_uclass_1的段地址来得到uclass_driver table的地址
const int n_ents = ll_entry_count(struct uclass_driver, uclass);
// 获得uclass_driver table的长度
struct uclass_driver *lists_uclass_lookup(enum uclass_id id)
// 从uclass_driver table中获取uclass id为id的uclass_driver。
2.4 driver
主要函数:
struct driver {
char *name; // 驱动名
enum uclass_id id; // 对应的uclass id
const struct udevice_id *of_match; // compatible字符串的匹配表,用于和device tree里面的设备节点匹配
int (*bind)(struct udevice *dev); // 用于绑定目标设备到该driver中
int (*probe)(struct udevice *dev); // 用于probe目标设备,激活
int (*remove)(struct udevice *dev); // 用于remove目标设备。禁用
int (*unbind)(struct udevice *dev); // 用于解绑目标设备到该driver中
int (*ofdata_to_platdata)(struct udevice *dev); // 在probe之前,解析对应udevice的dts节点,转化成udevice的平台数据
int (*child_post_bind)(struct udevice *dev); // 如果目标设备的一个子设备被绑定之后,调用
int (*child_pre_probe)(struct udevice *dev); // 在目标设备的一个子设备被probe之前,调用
int (*child_post_remove)(struct udevice *dev); // 在目标设备的一个子设备被remove之后,调用
int priv_auto_alloc_size; //需要分配多少空间作为其udevice的私有数据
int platdata_auto_alloc_size; //需要分配多少空间作为其udevice的平台数据
int per_child_auto_alloc_size; // 对于目标设备的每个子设备需要分配多少空间作为父设备的私有数据
int per_child_platdata_auto_alloc_size; // 对于目标设备的每个子设备需要分配多少空间作为父设备的平台数据
const void *ops; /* driver-specific operations */ // 操作集合的指针,提供给uclass使用,没有规定操作集的格式,由具体uclass决定
uint32_t flags; // 一些标志位
};
2.4.1 spi master driver
static const struct dm_spi_ops noru_spi_ops = {
.claim_bus = winbond_spi_claim_bus,
.release_bus = winbond_spi_release_bus,
.xfer = noru_spi_master_xfer,
.set_speed = noru_spi_set_speed,
.set_mode = noru_spi_set_mode,
};
static const struct udevice_id noru_spi_ids[] = {
{ .compatible = "noru,spi_master" },
{ }
};
U_BOOT_DRIVER(noru_spi_master) = {
.name = "noru_spi_master",
.id = UCLASS_SPI,
.of_match = noru_spi_ids,
.ops = &noru_spi_ops,
.bind = winbond_sf_bind_emul,
.ofdata_to_platdata = noru_spi_ofdata_to_platdata,
.platdata_auto_alloc_size = sizeof(struct noru_spi_platdata),
.priv_auto_alloc_size = sizeof(struct noru_spi_priv),
.probe = noru_spi_probe,
};
由上面结构体可得,其定义之后都被存放在了段._u_boot_list_2_driver_2_noru_spi_master中,那么去哪里可以看到呢?
在u-boot.map文件中搜索,._u_boot_list_2_driver,就可以查到程序中定义的所有驱动程序。
**注意:**spi master驱动中读写函数noru_spi_master_xfer需要根据设备具体spi协议进行编写代码
三、uboot代码解析
3.1 DM的初始化
DM的初始化分为两个部分,一个是在relocate重定向之前的初始化:initf_dm,一个是在relocate重定向之后的初始化:initr_dm
创建根设备root的udevice,存放在gd->dm_root中。
根设备其实是一个虚拟设备,主要是为uboot的其他设备提供一个挂载点。
初始化uclass链表gd->uclass_root
DM中udevice和uclass的解析
1、udevice的创建和uclass的创建
2、udevice和uclass的绑定
3、uclass_driver和uclass的绑定
4、driver和udevice的绑定
5、部分driver函数的调用
static int initf_dm(void)
{
#if defined(CONFIG_DM) && CONFIG_VAL(SYS_MALLOC_F_LEN)
int ret;
bootstage_start(BOOTSTAGE_ID_ACCUM_DM_F, "dm_f");
ret = dm_init_and_scan(true); //这里为true
bootstage_accum(BOOTSTAGE_ID_ACCUM_DM_F);
if (ret)
return ret;
#endif
#ifdef CONFIG_TIMER_EARLY
ret = dm_timer_init();
if (ret)
return ret;
#endif
return 0;
}
static int initr_dm(void)
{
int ret;
/* Save the pre-reloc driver model and start a new one */
gd->dm_root_f = gd->dm_root;
gd->dm_root = NULL;
#ifdef CONFIG_TIMER
gd->timer = NULL;
#endif
bootstage_start(BOOTSTAGE_ID_ACCUM_DM_R, "dm_r");
ret = dm_init_and_scan(false); //这里为false
bootstage_accum(BOOTSTAGE_ID_ACCUM_DM_R);
if (ret)
return ret;
return 0;
}
首先说明一下dts节点中的“u-boot,dm-pre-reloc”属性,当设置了这个属性时,则表示这个设备在relocate之前就需要使用。当dm_init_and_scan的参数为true时,只会对带有“u-boot,dm-pre-reloc”属性的节点进行解析,如在board_f.c文件中调用。而当参数为false的时候,则会对所有节点都进行解析,如在board_r.c文件中调用,重定位的意义,uboot重定位之前运行在SDRAM中,重定位运行在ddr中,运行空间更大。
代码解析:
int dm_init_and_scan(bool pre_reloc_only)
{
int ret;
ret = dm_init(); // DM的初始化
ret = dm_scan_platdata(pre_reloc_only); //从平台设备中解析udevice和uclass,uboot一般不用该方式,采用dts方式
if (CONFIG_IS_ENABLED(OF_CONTROL)) {//CONFIG_OF_CONTROL注意打开
ret = dm_scan_fdt(gd->fdt_blob, pre_reloc_only); // 从dtb中解析udevice和uclass
}
ret = dm_scan_other(pre_reloc_only);
return 0;
}
#define DM_ROOT_NON_CONST (((gd_t *)gd)->dm_root) // 宏定义根设备指针gd->dm_root
#define DM_UCLASS_ROOT_NON_CONST (((gd_t *)gd)->uclass_root) // 宏定义gd->uclass_root,uclass的链表
int dm_init(void)
{
int ret;
if (gd->dm_root) {// 根设备已经存在,说明DM已经初始化过了
dm_warn("Virtual root driver already exists!\n");
return -EINVAL;
}
INIT_LIST_HEAD(&DM_UCLASS_ROOT_NON_CONST);// 初始化uclass链表
ret = device_bind_by_name(NULL, false, &root_info, &DM_ROOT_NON_CONST);
// DM_ROOT_NON_CONST是指根设备udevice,root_info是表示根设备的设备信息
// device_bind_by_name会查找和设备信息匹配的driver,然后创建对应的udevice和uclass并进行绑定,最后放在DM_ROOT_NON_CONST中。
// device_bind_by_name后续我们会进行说明,这里我们暂时只需要了解root根设备的udevice以及对应的uclass都已经创建完成。
#if CONFIG_IS_ENABLED(OF_CONTROL)
DM_ROOT_NON_CONST->of_offset = 0;
#endif
ret = device_probe(DM_ROOT_NON_CONST);// 对根设备执行probe操作,在initf_dm执行(未重定位之前)只是创建root节点和uclass。
return 0;
}
dm_init创建root和uclass设备后开始解析设备树内容,从而创建相应的udevice
int dm_scan_fdt(const void *blob, bool pre_reloc_only)
// 此时传进来的参数blob=gd->fdt_blob, pre_reloc_only=0,即initr_dm才会扫dts所有内容
{
return dm_scan_fdt_node(gd->dm_root, blob, 0, pre_reloc_only);
// 直接调用dm_scan_fdt_node
}
int dm_scan_fdt_node(struct udevice *parent, const void *blob, int offset,
bool pre_reloc_only)
// 此时传进来的参数
// parent=gd->dm_root,表示以root设备作为父设备开始解析
// blob=gd->fdt_blob,指定了对应的dtb
// offset=0,从偏移0的节点开始扫描
// pre_reloc_only=0,不只是解析relotion之前的设备
{
int ret = 0, err;
/* 以下步骤相当于是遍历每一个dts节点并且调用lists_bind_fdt对其进行解析 */
for (offset = fdt_first_subnode(blob, offset);
// 获得blob设备树的offset偏移下的节点的第一个子节点
offset > 0;
offset = fdt_next_subnode(blob, offset)) {
// 循环查找下一个子节点
if (!fdtdec_get_is_enabled(blob, offset)) {
// 判断节点状态是否是disable,如果是的话直接忽略,注意dts中节点status = “ok”
dm_dbg(" - ignoring disabled device\n");
continue;
}
err = lists_bind_fdt(parent, blob, offset, NULL);
// 解析绑定这个节点,dm_scan_fdt的核心,下面具体分析
if (err && !ret) {
ret = err;
debug("%s: ret=%d\n", fdt_get_name(blob, offset, NULL),
ret);
}
}
return ret;
}
lists_bind_fdt是从dtb中解析udevice和uclass的核心,通过读取dts内容读取相应的driver生成相应的 udevice,再根据driver中对应的UCLASS_ID绑定对应的uclass,如dts中定义一个spi master驱动,对应的驱动名字为mxc_spi,id为UCLASS_SPI,则该udevice会追加到spi uclass节点下,同时也会加到gd->root下。
int lists_bind_fdt(struct udevice *parent, const void *blob, int offset,
struct udevice **devp)
// parent指定了父设备,通过blob和offset可以获得对应的设备的dts节点,对应udevice结构通过devp返回
{
struct driver *driver = ll_entry_start(struct driver, driver);
// 获取driver table地址
const int n_ents = ll_entry_count(struct driver, driver);
// 获取driver table长度
const struct udevice_id *id;
struct driver *entry;
struct udevice *dev;
bool found = false;
const char *name;
int result = 0;
int ret = 0;
dm_dbg("bind node %s\n", fdt_get_name(blob, offset, NULL));
// 打印当前解析的节点的名称
if (devp)
*devp = NULL;
for (entry = driver; entry != driver + n_ents; entry++) {
// 遍历driver table中的所有driver,即通过U_BOOT_DRIVER(mxc_spi)声明的驱动
ret = driver_check_compatible(blob, offset, entry->of_match,
&id);
// 判断driver中的compatibile字段和dts节点是否匹配
name = fdt_get_name(blob, offset, NULL);
// 获取节点名称
if (ret == -ENOENT) {
continue;
} else if (ret == -ENODEV) {
dm_dbg("Device '%s' has no compatible string\n", name);
break;
} else if (ret) {
dm_warn("Device tree error at offset %d\n", offset);
result = ret;
break;
}
dm_dbg(" - found match at '%s'\n", entry->name);
ret = device_bind(parent, entry, name, NULL, offset, &dev);
// 找到对应的driver,调用device_bind进行绑定,会在这个函数中创建对应udevice,然后根据id和uclass并且进行绑定
if (ret) {
dm_warn("Error binding driver '%s': %d\n", entry->name,
ret);
return ret;
} else {
dev->driver_data = id->data;
found = true;
if (devp)
*devp = dev;
// 将udevice设置到devp指向的地方中,进行返回
}
break;
}
if (!found && !result && ret != -ENODEV) {
dm_dbg("No match for node '%s'\n",
fdt_get_name(blob, offset, NULL));
}
return result;
}
这里会将udevice相关的信息进行填充,获取对应的parent、uclass以及它们对应的平台属性,然后将该udevice绑定到parent和uclass链表中。
int device_bind(struct udevice *parent, const struct driver *drv,
const char *name, void *platdata, int of_offset,
struct udevice **devp)
// parent:父设备
// drv:设备对应的driver
// name:设备名称
// platdata:设备的平台数据指针
// of_offset:在dtb中的偏移,即代表了其dts节点
// devp:所创建的udevice的指针,用于返回
{
struct udevice *dev;
struct uclass *uc;
int size, ret = 0;
ret = uclass_get(drv->id, &uc);
// 获取driver id对应的uclass,如果uclass原先并不存在,那么会在这里创建uclass并其uclass_driver进行绑定
dev = calloc(1, sizeof(struct udevice));
// 分配一个udevice
dev->platdata = platdata; // 设置udevice的平台数据指针
dev->name = name; // 设置udevice的name
dev->of_offset = of_offset; // 设置udevice的dts节点偏移
dev->parent = parent; // 设置udevice的父设备
dev->driver = drv; // 设置udevice的对应的driver,相当于driver和udevice的绑定
dev->uclass = uc; // 设置udevice的所属uclass
dev->seq = -1;
dev->req_seq = -1;
if (CONFIG_IS_ENABLED(OF_CONTROL) && CONFIG_IS_ENABLED(DM_SEQ_ALIAS)) {
/*
* Some devices, such as a SPI bus, I2C bus and serial ports
* are numbered using aliases.
*
* This is just a 'requested' sequence, and will be
* resolved (and ->seq updated) when the device is probed.
*/
if (uc->uc_drv->flags & DM_UC_FLAG_SEQ_ALIAS) {
if (uc->uc_drv->name && of_offset != -1) {
fdtdec_get_alias_seq(gd->fdt_blob,
uc->uc_drv->name, of_offset,
&dev->req_seq);
}
// 设置udevice的alias请求序号
}
}
if (!dev->platdata && drv->platdata_auto_alloc_size) {
dev->flags |= DM_FLAG_ALLOC_PDATA;
dev->platdata = calloc(1, drv->platdata_auto_alloc_size);
// 为udevice分配平台数据的空间,由driver中的platdata_auto_alloc_size决定
}
size = uc->uc_drv->per_device_platdata_auto_alloc_size;
if (size) {
dev->flags |= DM_FLAG_ALLOC_UCLASS_PDATA;
dev->uclass_platdata = calloc(1, size);
// 为udevice分配给其所属uclass使用的平台数据的空间,由所属uclass的driver中的per_device_platdata_auto_alloc_size决定
}
/* put dev into parent's successor list */
if (parent)
list_add_tail(&dev->sibling_node, &parent->child_head);
// 添加到父设备的子设备链表中
ret = uclass_bind_device(dev);
// uclass和udevice进行绑定,主要是实现了将udevice链接到uclass的设备链表中
/* if we fail to bind we remove device from successors and free it */
if (drv->bind) {
ret = drv->bind(dev);
// 执行udevice对应driver的bind函数
}
if (parent && parent->driver->child_post_bind) {
ret = parent->driver->child_post_bind(dev);
// 执行父设备的driver的child_post_bind函数
}
if (uc->uc_drv->post_bind) {
ret = uc->uc_drv->post_bind(dev);
if (ret)
goto fail_uclass_post_bind;
// 执行所属uclass的post_bind函数
}
if (devp)
*devp = dev;
// 将udevice进行返回
dev->flags |= DM_FLAG_BOUND;
// 设置已经绑定的标志
// 后续可以通过dev->flags & DM_FLAG_ACTIVATED或者device_active宏来判断设备是否已经被激活
return 0;
当udevie和driver、uclass绑定之后开始最后的激活阶段
查找是否有对应的设备分配设备的私有数据
对父设备进行probe
执行probe device之前uclass需要调用的一些函数
调用driver的ofdata_to_platdata,将dts信息转化为设备的平台数据
调用driver的probe函数
执行probe device之后uclass需要调用的一些函数
int device_probe(struct udevice *dev)
{
const struct driver *drv;
int size = 0;
int ret;
int seq;
if (dev->flags & DM_FLAG_ACTIVATED)
return 0;
// 表示这个设备已经被激活了
drv = dev->driver;
assert(drv);
// 获取这个设备对应的driver
/* Allocate private data if requested and not reentered */
if (drv->priv_auto_alloc_size && !dev->priv) {
dev->priv = alloc_priv(drv->priv_auto_alloc_size, drv->flags);
// 为设备分配私有数据
}
/* Allocate private data if requested and not reentered */
size = dev->uclass->uc_drv->per_device_auto_alloc_size;
if (size && !dev->uclass_priv) {
dev->uclass_priv = calloc(1, size);
// 为设备所属uclass分配私有数据
}
// 这里过滤父设备的probe
seq = uclass_resolve_seq(dev);
if (seq < 0) {
ret = seq;
goto fail;
}
dev->seq = seq;
dev->flags |= DM_FLAG_ACTIVATED;
// 设置udevice的激活标志
ret = uclass_pre_probe_device(dev);
// uclass在probe device之前的一些函数的调用
if (drv->ofdata_to_platdata && dev->of_offset >= 0) {
ret = drv->ofdata_to_platdata(dev);
// 调用driver中的ofdata_to_platdata将dts信息转化为设备的平台数据
}
if (drv->probe) {
ret = drv->probe(dev);
// 调用driver的probe函数,到这里设备才真正激活了
}
ret = uclass_post_probe_device(dev);
return ret;
}
3.2 spi norflash设备识别
通过dts中子节点匹配的驱动进行创建:
static const struct dm_spi_flash_ops spi_flash_std_ops = {
.read = spi_flash_std_read,
.write = spi_flash_std_write,
.erase = spi_flash_std_erase,
};
static const struct udevice_id spi_flash_std_ids[] = {
{ .compatible = "noru,spi-flash" },
{ }
};
U_BOOT_DRIVER(spi_flash_std) = {
.name = "spi_flash_std",
.id = UCLASS_SPI_FLASH,
.of_match = spi_flash_std_ids,
.probe = spi_flash_std_probe,
.priv_auto_alloc_size = sizeof(struct spi_flash),
.ops = &spi_flash_std_ops,
};
3.3 设备树内容
在设备树添加设备信息
在对应的dts文件中添加相应的设备节点和信息,注意simple-bus创建,代码解析:
如果dts中没有设置clocks节点直接回退出,原因是spi通信是基于clock进行数据传输的
chosen节点在scan dts内容是需要的,可以加入
/dts-v1/;
/ {
#address-cells = <2>;
#size-cells = <2>;
compatible = "evb,noru";
model = "NORU vu440";
aliases {
spi0 = &spi;
} ;
chosen {
bootargs = "console=ttyS0,115200n8 loglevel=7";
stdout-path = "uart0:115200n8";
};
clocks {
compatible = "simple-bus"; //bus总线驱动和clock
u-boot,dm-pre-reloc;
spiclk: virt_100mhz {
#clock-cells = <0>;
compatible = "fixed-clock";
clock-frequency = <100000000>;
};
};
spi: spi@c100000 {
compatible = "noru,spi_master"; //spi master 驱动
status = "okay";
reg = <0x0 0xc100000 0x0 0x10000>;
#address-cells = <1>;
#size-cells = <0>;
num-cs = <1>;
clocks = <&spiclk>;
flash@0 {
compatible = "noru,spi-flash";//flash 驱动
spi-max-frequency = <50000000>;
reg = <0>;
spi-cpol;
spi-cpha;
};
};
};
3.4 .config配置
CONFIG_DEFAULT_DEVICE_TREE="xxxxx" //指定设备树名
CONFIG_OF_EMBED=y //那么uboot会把设备树编译进镜像内
CONFIG_OF_CONTROL=y //使能设备树的支持
CONFIG_SPI_FLASH_BAR=y //支持4B模式,大于16MB的spi打开
```c
//执行命令
sf probe
'spi@c100000'
- found
spi_find_chip_select: plat=00000089ff6bd4c0, cs=0
spi_flash_probe_bus_cs==========bus:0,===cs:0,===speed:1000000,====mode:3
spi_flash@0:0,the str is spi_flash@0:0
uclass_get_device_by_seq===
'spi@c100000'
- found
spi_find_chip_select: plat=00000089ff6bd4c0, cs=0
==============spi_flash_std_probe
winbond_spi_set_speed: ==========eric=======winbond_spi_claim_bus
SF: Detected w25q256fw with page size 256 Bytes, erase size 4 KiB, total 32 MiB
SF: Warning - Only lower 16MiB accessible, Full access #define CONFIG_SPI_FLASH_BAR
上面Warning因为现在用的winbod spi支持32MB,设置的3B模式只支持16MB,需要在config中打开CONFIG_SPI_FLASH_BAR=y文章来源:https://www.toymoban.com/news/detail-728379.html
注意加入该norflash相关id
drivers/mtd/spi/spi_flash_ids.c
文章来源地址https://www.toymoban.com/news/detail-728379.html
3.5 spi读写测试
//初始化spi
sf probe
//tftp传入需要烧录的文件
tftp 0x8801000000 pkg_bin/noru_preloader_pkt.bin
tftp 0x8802000000 pkg_bin/noru_loader_pkt.bin
tftp 0x8803000000 pkg_bin/fw_pkt_compress.image
//擦除原有数据
sf erase 0 0x200000
//从ddr写入数据
sf write 0x8801000000 0 0x38000
sf write 0x8802000000 0x70000 0x80000
sf write 0x8803000000 0xF1000 0x80000
//读数据到ddr
sf read 0x8801000000 0 0x38000
sf read 0x8802000000 0x70000 0x80000
sf read 0x8803000000 0xF1000 0x80000
四、其他相关链接
1、SPI协议详细总结附实例图文讲解通信过程
2、Linux下spi网卡dm9051驱动移植及驱动调试分析总结
3、Linux下设备树dts内容总结及示例解析
到了这里,关于uboot下UCLASS框架详解---结合项目工作中spi master和flash驱动开发的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!