Uboot驱动框架概述
6. 驱动框架
6.1. 声明
-
方式 A
每个设备在 uboot 中都对应一个驱动结构体变量, uboot 操纵外设都是通过该结构体变量实现的。以 ti 的 cpsw 网卡为例,在
driver/net/cpsw.c
中定义了多个函数实现了网卡的收发、配置、中断处理等操作,然后通过结构体将这些操作集成到操作结构体struct eth_ops
中:static const struct eth_ops cpsw_eth_ops = { .start = cpsw_eth_start, .send = cpsw_eth_send, .recv = cpsw_eth_recv, .free_pkt = cpsw_eth_free_pkt, .stop = cpsw_eth_stop, };
这个结构体包含了网卡的主要操作:启停、收发等操作,但是这个结构体还是太底层了,不应该直接传给上层应用,也不能传给上层,uboot 的外设驱动架构是把全部的驱动放到一个 section 群中,驱动结构体的成员是统一的,cpsw 驱动结构提其组成如下:
U_BOOT_DRIVER(eth_cpsw) = { .name = "eth_cpsw", .id = UCLASS_ETH, .of_match = cpsw_eth_ids, .ofdata_to_platdata = cpsw_eth_ofdata_to_platdata, .probe = cpsw_eth_probe, .ops = &cpsw_eth_ops, .priv_auto_alloc_size = sizeof(struct cpsw_priv), .platdata_auto_alloc_size = sizeof(struct eth_pdata), .flags = DM_FLAG_ALLOC_PRIV_DMA, };
cpsw 驱动的成员包括了驱动名(name)、id、类型(of_match)、探针(probe)、操作(ops)等等。而这些驱动都是保存在同一个 section 群中,这是通过宏
U_BOOT_DRIVER
实现的:/* Declare a new U-Boot driver */ #define U_BOOT_DRIVER(__name) \ ll_entry_declare(struct driver, __name, driver) #define ll_entry_declare(_type, _name, _list) \ _type _u_boot_list_2_##_list##_2_##_name __aligned(4) \ __attribute__((unused, \ section(".u_boot_list_2_"#_list"_2_"#_name))) struct driver { char *name; enum uclass_id id; const struct udevice_id *of_match; int (*bind)(struct udevice *dev); int (*probe)(struct udevice *dev); int (*remove)(struct udevice *dev); int (*unbind)(struct udevice *dev); int (*ofdata_to_platdata)(struct udevice *dev); int (*child_post_bind)(struct udevice *dev); int (*child_pre_probe)(struct udevice *dev); int (*child_post_remove)(struct udevice *dev); int priv_auto_alloc_size; int platdata_auto_alloc_size; int per_child_auto_alloc_size; int per_child_platdata_auto_alloc_size; const void *ops; /* driver-specific operations */ uint32_t flags; };
这和之前 shell command 的实现类似。此处网卡驱动
eth_cpsw
的结构体就是一个定义在段.u_boot_list_2_driver_2_eth_cpsw
的struct driver
结构体(_u_boot_list_2_driver_2_eth_cpsw
),定义好网卡驱动之后,其它应用就知道了结构体的名字和位置,从而可以使用该驱动。 -
方式 B
方式 B 的驱动声明就相对简单多了 , 实现网卡的各种操作接口还是必要的 , 但是此时就不需要再声明定义额外的驱动结构体了,只需要定义一个网卡注册函数
cpsw_register()
:
int cpsw_register(struct cpsw_platform_data *data)
{
struct eth_device *dev;
...
priv->dev = dev;
priv->data = *data;
strcpy(dev->name, "cpsw");
dev->iobase = 0;
dev->init = cpsw_init;
dev->halt = cpsw_halt;
dev->send = cpsw_send;
dev->recv = cpsw_recv;
dev->priv = priv;
eth_register(dev);
ret = _cpsw_register(priv);
if (ret < 0) {
eth_unregister(dev);
free(dev);
free(priv);
return ret;
}
return 1;
}
cpsw_register()
中会网卡的操作函数 dev
关联,并且通过接口 eth_register()
(uboot 提供的网卡统一注册函数)与全局变量 eth_register
关联起来,供上层应用使用。
6.2. uclass
uclass 是一组操作方式相似的设备。一个 uclass 提供了一种在一组设备中操作一个设备的方法,而且使用的是一组相同的接口。
以 uboot 自带的 uclass demo 为例(cmd/demo.c
)。
uclass 的主要的数据结构是 udevice
:
struct udevice {
const struct driver *driver;
const char *name;
void *platdata;
void *parent_platdata;
void *uclass_platdata;
int of_offset;
ulong driver_data;
struct udevice *parent;
void *priv;
struct uclass *uclass;
void *uclass_priv;
void *parent_priv;
struct list_head uclass_node;
struct list_head child_head;
struct list_head sibling_node;
uint32_t flags;
int req_seq;
int seq;
#ifdef CONFIG_DEVRES
struct list_head devres_head;
#endif
};
和 uclass_driver
:
struct uclass_driver {
const char *name;
enum uclass_id id;
int (*post_bind)(struct udevice *dev);
int (*pre_unbind)(struct udevice *dev);
int (*pre_probe)(struct udevice *dev);
int (*post_probe)(struct udevice *dev);
int (*pre_remove)(struct udevice *dev);
int (*child_post_bind)(struct udevice *dev);
int (*child_pre_probe)(struct udevice *dev);
int (*init)(struct uclass *class);
int (*destroy)(struct uclass *class);
int priv_auto_alloc_size;
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;
};
而 U_BOOT_DRIVER
定义的驱动也是属于 uclass 驱动。
demo.c
中定义了一个结构体变量 struct udevice *demo_dev
, do_demo()
实现了命令 demo
:
static int do_demo(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
...
ret = uclass_get_device(UCLASS_DEMO, devnum, &demo_dev);
...
}
uclass_get_device()
完成两件事:
int uclass_get_device(enum uclass_id id, int index, struct udevice **devp)
{
...
ret = uclass_find_device(id, index, &dev);
return uclass_get_device_tail(dev, ret, devp);
}
- 从 uclass 驱动所在的 section 找到找到 uclass driver
uboot 已经在 demo_uclass.c
定义了 demo 的 uclass 驱动结构体:
UCLASS_DRIVER(demo) = {
.name = "demo",
.id = UCLASS_DEMO,
};
do_demo() 要做的就是找到这个结构体变量:
int uclass_find_device(enum uclass_id id, int index, struct udevice **devp)
{
...
ret = uclass_get(id, &uc);
...
list_for_each_entry(dev, &uc->dev_head, uclass_node) {
if (!index--) {
*devp = dev;
return 0;
}
}
...
}
首先根据 uclass id 和设备 id 找到一组 uclass 设备驱动并且从中找到具体的某个设备。而 uclass id 就是上面的 UCLASS_DEMO
,而设备 id 则是在具体设备定义的驱动中,比如 demo hello 驱动的定义 :
static const struct udevice_id demo_shape_id[] = {
{ "demo-simple", 0 },
{ },
};
U_BOOT_DRIVER(demo_simple_drv) = {
.name = "demo_simple_drv",
.of_match = demo_shape_id,
.id = UCLASS_DEMO,
.ofdata_to_platdata = demo_shape_ofdata_to_platdata,
.ops = &simple_ops,
.platdata_auto_alloc_size = sizeof(struct dm_demo_pdata),
};
设备 id 就是该设备在 demo_shape_id
的下标。
- 找到具体的设备之后就是初始化该设备。
int uclass_get_device_tail(struct udevice *dev, int ret,
struct udevice **devp)
{
...
ret = device_probe(dev);
...
}
找到找到设备驱动并进行初始化之后,就该执行设备提供的功能,比如 demo 提供的 hello 功能:
=> demo hello 0
r@@@@@@@
e@@@@@@@
d@@@@@@@
r@@@@@@@
e@@@@@@@
d@@@@@@@
这个功能的实现依赖于函数 demo_hello()
:
int demo_hello(struct udevice *dev, int ch)
{
const struct demo_ops *ops = device_get_ops(dev);
if (!ops->hello)
return -ENOSYS;
return ops->hello(dev, ch);
}
其中参数 dev
就是上面的 demo_dev
,以后的操作只需要调用结构体对应的操作函数即可。
6.2.1. uclass 的内部结构
方式 B 驱动实现方式本质上就是 uclass 。简单来说, uclass 机制就是将外设分为多组,每组不同的设备都有一个相同的 ID (如上面的 UCLASS_DEMO),然后 uboot 在声明驱动时将所有的驱动按顺序连续保存在一起,运行时如果要使用某个设备就需要根据 ID 找到对应组的驱动,然后根据设备 ID 找到具体某个设备的驱动,后面使用设备就是执行这个驱动对应的操作函数。
6.3. 调用
首先注册驱动,将具体设备的驱动和 uboot 的全局结构体(got?)关联起来。
然后通过全局结构体调用具体的设备驱动。
int eth_send(void *packet, int length)
{
...
return eth_current->send(eth_current, packet, length);
}
uboot 通过 eth_current->send
调用了实际网络设备的发送函数。现在要关注的是 eth_current 实际指向的驱动是哪个,以及该变量是何时、何处赋值的。 (详见 6.1 节)
6.4. 设备的使用
6.4.1. 网卡
如上所示 uboot 启动阶段会执行 initr_net
初始化网络:
static int initr_net(void)
{
...
eth_initialize();
...
return 0;
}
int eth_initialize(void)
{
int num_devices = 0;
eth_devices = NULL;
eth_current = NULL;
eth_common_init();
/*
* If board-specific initialization exists, call it.
* If not, call a CPU-specific one
*/
if (board_eth_init != __def_eth_init) {
if (board_eth_init(gd->bd) < 0)
printf("Board Net Initialization Failed\n");
} else if (cpu_eth_init != __def_eth_init) {
if (cpu_eth_init(gd->bd) < 0)
printf("CPU Net Initialization Failed\n");
} else {
printf("Net Initialization Skipped\n");
}
if (!eth_devices) {
puts("No ethernet found.\n");
bootstage_error(BOOTSTAGE_ID_NET_ETH_START);
} else {
...
eth_current = dev;
...
}
...
}
在函数 eth_initialize()
中,会初始化网络地址、参数(eth_common_init()
),接着进入 board_eth_init()
注册网络设备,并给 eth_current
赋值。
int board_eth_init(bd_t *bis)
{
...
rv = cpsw_register(&cpsw_data);
...
}
int cpsw_register(struct cpsw_platform_data *data)
{
eth_register(dev);
...
}
int eth_register(struct eth_device *dev)
{
...
eth_devices = dev;
eth_current = dev;
...
}
以后对网卡的操作都会直接调用 eth_current
。如 ping 操作最终会沿着函数调用链 : do_ping()
->net_loop()
->ping_start()
->ping_send()
->arp_request()
->arp_raw_request()
->->net_send_packet()
->eth_send()
->eth_current->send
进行发包。
注: 网卡的接口主要是收发,以及一些配置或特殊功能,现在都是通过U_BOOT_DRIVER 进行封装的,协议栈(net/net.c)会主动调用网卡接口。
6.4.2. 串口
uboot 启动阶段会执行 initr_serial
初始化串口终端,初始化函数调用链如下:
initr_serial
->serial_initialize
->serial_init
->serial_find_console_or_panic
static void serial_find_console_or_panic(void)
{
...
if (!uclass_get_device_by_of_offset(UCLASS_SERIAL, node,
&dev)) {
gd->cur_serial_dev = dev;
return;
}
...
if (node > 0 &&
!lists_bind_fdt(gd->dm_root, blob, node, &dev)) {
if (!device_probe(dev)) {
gd->cur_serial_dev = dev;
return;
}
...
if (!uclass_get_device_by_seq(UCLASS_SERIAL, INDEX, &dev) ||
!uclass_get_device(UCLASS_SERIAL, INDEX, &dev) ||
(!uclass_first_device(UCLASS_SERIAL, &dev) && dev)) {
gd->cur_serial_dev = dev;
return;
}
...
}
串口驱动使用了方式 B 声明设备结构体,在使用宏 U_BOOT_DRIVER
定义设备结构体变量时,会给赋给设备 .id=UCLASS_SERIAL
, 然后初始化串口时会调用到 uclass_add()
,而这个函数会根据 id 去找设备结构体变量,然后将该设备添加到 uc->dev_head
和相关链表链表,并最后把设备指针传给 gd->cur_serial_dev
。以后 uboot 使用串口进行收发包都是调用 gd->cur_serial_dev
进行的。
6.4.3. 其他
其他外设大多类似,基本要么使用方式 A 通过 init_sequence_r[]
中的初始化函数进行配置的,同时驱动自己还需要提供一个接口进行设备注册;或者使用方式 B 定义设备结构体,然后由 uboot 根据设备 ID 找到对应的结构体。两种方法最终都是要把设备驱动和 gd 全局变量关联起来。
6.5. 设备树
dts,fdt (待补充)
6.6. 小结
uboot 的驱动框架相对于 linux driver 来说很简单,但是相对一般裸板系统(以及 RTOS) 来说还是复杂些。
首先,uboot 的驱动实现和裸板系统的驱动类似,都是直接对设备寄存器、存储空间进行操作,所有的功能都要自己实现,不似 linux 已经提供了现成的同步、互斥接口,也不需要考虑内存申请、释放、地址转换等问题,可以说都很原始;
其次,虽然 uboot 的驱动实现很简单,但是使用方法和裸板驱动还是不同的,常见的裸板(包括 RTOS)驱动程序都是直接将底层操作接口暴露给上层应用,而 uboot 却对这些接口进行了封装,针对多平台的外设使用简化了使用复杂度:通过结构体将驱动操作接口和设备的全局变量(比如网卡相关全局变量为 eth_current
)关联到一起,上层应用只要知道这个全局变量就可以进行设备操作,BSP 工程师要做的就是实现驱动并且在 uboot 初始化外设时将驱动接口和这个全局变量关联起来。
再次,uboot 的驱动还利用了 section 特性,将很多驱动声明在一堆连续的 section 区(如上所述,通过宏 U_BOOT_DRIVER()
将驱动结构体放到段 .u_boot_list_2_driver_2_*
),后续如果要使用这些驱动,则只需要知道驱动名(如网卡 eth_cpsw)。
最后,uboot 的驱动框架是很简单的,既要兼顾多种外设、多种架构,但又要降低驱动的实现难度,提高驱动的执行效率,所以它的框架是针对功能性和复杂度的折中实现。而 uboot 不同设备驱动在具体实现时采用的框架流程并没有统一,不同类型有不同的执行套路,比较烦人。