uboot打印框架
- 前言
- 要点
- uboot启动大致流程
- 打印
- 剖析串口打印
- printf
- puts
- stdiodev
- stdio 设备的插入添加
- stdio设备的一些接口
- stdiodev初始化
- list操作
- stdio_register
- 线索梳理
- 再次梳理整理已知的线索
- 分析名为"serial"stdio设备
- serial_devices
- serial_pl01x驱动
- 终极大法
前言
本文主要探索uboot下的打印是怎么走的,是如何调用到串口驱动的。以及探索下在uboot下的打印框架。本文中的uboot
版本是2016.11,其他版本的话大体框架应该都差不多。
要点
uboot启动大致流程
uboot 启动从汇编文件开始到C语言环境执行init_sequence_f和init_sequence_r,
init_sequence_f一般初始化时钟,串口,cpu,定时器,环境变量等一些最基本的东西。
init_sequence_r,一般初始化些额外的功能初始化,比如控制台,stdio,logbuf,pci,nor,nand等。以及最后main_loop功能,命令行交互功能。
打印
我们知道一个设备对外输出的是串口,所以最终的输出硬件介质是串口,必然和串口驱动有关,
而我们一般打印的函数不会自己封装串口打印函数,都是用printf用来打印输出。
所以我们抓住一头一尾,printf------…—串口驱动----串口控制器寄存器,然后在细细浏览中间过程。
剖析串口打印
printf
我们在uboot下用的都是printf,看其实现,利用va 解析可变参数并格式化通过puts输出字符串。
printf–》lib/vsprintf
int printf(const char *fmt, ...)
{
va_list args;
uint i;
char printbuffer[CONFIG_SYS_PBSIZE];
va_start(args, fmt);
/*
* For this to work, printbuffer must be larger than
* anything we ever want to print.
*/
i = vscnprintf(printbuffer, sizeof(printbuffer), fmt, args);
va_end(args);
/* Print the string */
puts(printbuffer);
return i;
}
puts
puts --》common\console.c
------------------------
void puts(const char *s)
{
...
if (gd->flags & GD_FLG_DEVINIT) {
/* Send to the standard output */
fputs(stdout, s);
}
...
}
------------------------
#define stdin 0
#define stdout 1
#define stderr 2
------------------------
fputs=>console_puts
static inline void console_puts(int file, const char *s)
{
stdio_devices[file]->puts(stdio_devices[file], s);
}
由此可见通过类似标准输出的方式将字符串打出去
uboot下开来也分层了,所有应用打印通过给console层,由console来虚拟出标准输出、标准输入、标准错误。
标准输入输出其实就是stdio设备数组的索引。
这里可以得知printf打印输出的数据最终是到stdio设备的puts接口上输出。
stdiodev
代码路径\common\stdio.c
struct stdio_dev {
int flags; /* Device flags: input/output/system */
int ext; /* Supported extensions */
char name[32]; /* Device name */
/* GENERAL functions */
int (*start)(struct stdio_dev *dev); /* To start the device */
int (*stop)(struct stdio_dev *dev); /* To stop the device */
/* OUTPUT functions */
/* To put a char */
void (*putc)(struct stdio_dev *dev, const char c);
/* To put a string (accelerator) */
void (*puts)(struct stdio_dev *dev, const char *s);
/* Other functions */
void *priv; /* Private extensions */
struct list_head list;
};
从结构体定义上看有start那么一般是这个设备的初始化接口
有一个list那么说明该stdio dev是通过链表链接的,说明stdio dev可以有多个,并通过链表管理,
那么本文件肯定还有一个链表头,并且肯定有一系列链表操作的接口,比如stdio dev的插入,遍历等。
stdio 设备的插入添加
既然printf是由stdio_devices的puts接口发送出去的那必然有stdio_devices初始化的地方
stdio_devices[] 这个数组一定有赋值的地方,全局搜索stdio_devices,以及通过宏判断
我们找到这条调用关系的线索
线索一
- printf的打印最终有stdio设备的puts函数输出
- console_setfile => stdio_devices[file] = dev;
- console_setfile <= console_init_r <= init_sequence_r
stdio设备的一些接口
拿到上面的线索后不着急看console_setfile谁调用了,按照这种内核或者uboot开发者的风格,抽象出stdio设备层,通过stdio设备的接口函数去调用,面向对象思想,封装性比较强,内聚比较高,
况且可以看到struct stdio_dev *stdio_devices[] = { NULL, NULL, NULL };
刚开始这个数组初始化为null,那么一定有注册的地方才能用。所以可以先大致的浏览下该文件stdio.c
stdiodev初始化
一般模块都有初始化在本文件中搜索init
int stdio_init(void)
{
stdio_init_tables();
stdio_add_devices();
return 0;
}
既然初始化里面没有看到链表插入的动作只有初始化链表头的函数那么接接下来尝试搜索下链表操作的相关接口。
list操作
static struct stdio_dev devs;
devs被设计成static说明只有本文使用,所以搜索本文中devs.list被使用的地方即可。
搜索结果
线索二
list_add_tail(&(_dev->list), &(devs.list));<=stdio_register_dev<=stdio_register,用于注册stdio设备挂到链表上
list_for_each(pos, &(devs.list)) {<=stdio_get_by_name 也很明显用于通过name便利整条链表里面找到匹配的stdio设备
return &(devs.list);<=struct list_head* stdio_get_list(void)很明显用于拿到整条链表
由此可见stdio_dev的注册尤为重要,这个决定了当前系统stdio_list上有多少个stdio设备
stdio_devices[] 这个数组中的值肯定为stdio_list中的一个或者几个。
stdio_register
仍然搜索注册的地方
从搜索情况来看stdio设备作为标准输入输出设备其实体不一定是串口,也有可能是usb设备也有可能是lcd设备
这样符合设计,代码中print不管底层只往控制台打,控制台只往标准输出打,标准输出是串口那么数据体现在串口上,标准输出是lcd那么就往lcd打。分层思想,stdio设备更像usb转串口,lcd,串口的抽象类。
根据编译文件分析
stdio_register (&dev);有这几个地方调用到,且都是由一个地方调过来的
且先调用drv_system_init 后调用serial_stdio_init
drv_system_init ;;;<=stdio_add_devices()<=init_sequence_r
serial_stdio_init;;;<=stdio_add_devices()<=init_sequence_r
static void drv_system_init (void)
{
struct stdio_dev dev;
memset (&dev, 0, sizeof (dev));
strcpy (dev.name, "serial");
dev.flags = DEV_FLAGS_OUTPUT | DEV_FLAGS_INPUT;
dev.putc = stdio_serial_putc;
dev.puts = stdio_serial_puts;
dev.getc = stdio_serial_getc;
dev.tstc = stdio_serial_tstc;
stdio_register (&dev);
}
-----------------------
void serial_stdio_init(void)
{
struct stdio_dev dev;
struct serial_device *s = serial_devices;
while (s) {
memset(&dev, 0, sizeof(dev));
strcpy(dev.name, s->name);
dev.flags = DEV_FLAGS_OUTPUT | DEV_FLAGS_INPUT;
dev.start = serial_stub_start;
dev.stop = serial_stub_stop;
dev.putc = serial_stub_putc;
dev.puts = serial_stub_puts;
dev.getc = serial_stub_getc;
dev.tstc = serial_stub_tstc;
dev.priv = s;
stdio_register(&dev);
s = s->next;
}
}
由线索一得知打印最终由stdio的puts输出所以这两个函数要注意下。
serial_stub_puts
stdio_serial_puts
线索三
由上面注册的地方可以发现注册了一个serial的stdio设备,然后从serial_devices里面读取串口设备挨个注册到stdio设备
这里又拿到几个线索三
1.有一个serial的stdio设备
2.serial_devices设备注册进stdio的设备
3.这里还有一个serial_devices需要扒拉
4. 最终打印输出由serial_stub_puts或者stdio_serial_puts输出,后面确定。
5. drv_system_init 优选于serial_stdio_init调用,根据std注册代码得知“”serial“在serial_devices前面
也就是链表中的位置serial靠前
6. 因为std注册的链表方式为tail插入,后注册的放在链表末尾
线索梳理
通过线索一得知标准输入输出设备stdio在console_setfile中被设置,而console_setfile被console_init_r所调用.
通过线索三得知std设备有一个serial和serial_devices链表中的1个或几个。
继续接上线索一进行分析console_init_r函数如下
int console_init_r(void)
{ struct stdio_dev *dev;
struct list_head *list = stdio_get_list();
/* Scan devices looking for input and output devices */
list_for_each(pos, list) {
dev = list_entry(pos, struct stdio_dev, list);
if ((dev->flags & DEV_FLAGS_INPUT) && (inputdev == NULL)) {
inputdev = dev;
}
if ((dev->flags & DEV_FLAGS_OUTPUT) && (outputdev == NULL)) {
outputdev = dev;
}
if(inputdev && outputdev)
break;
}
/* Initializes output console first */
if (outputdev != NULL) {
console_setfile(stdout, outputdev);
console_setfile(stderr, outputdev);
从中能看到一些线索二的影子。
1. 拿到整条stdio设备链表,轮询这个设备的flag有output的设置为stdout指向的stdio设备。
2. console_setfile就是用来设置控制台标准输入输出指向的stdout指针。
3. 根据list_for_each说只要stdio设备的flag同时包含DEV_FLAGS_INPUT和DEV_FLAGS_OUTPUT那么停止检索跳出循环
根据 线索三serial的stdio设备在链表中比较靠前所以最终标准输出指向drv_system_init 注册的stdio设备
再次梳理整理已知的线索
- uboot下printf最终调用puts,在深入调用stdio标准输出设备的puts函数
- stdio标准输出指向的stdio设备来自于最后一次调用console_setfile函数的地方
- console_init_r函数便利stdio设备链表,并把最后一次注册进链表的stdio设备(已有output标志的)当做标准输出设备
- 目前已知的注册进链表的有一个serial以及还未分析的serial_devices链表中的1个或几个
由上梳理得知标准输出设备为名为serial的stdio设备,那么标准输出的打印也就是serial的puts函数 dev.puts = stdio_serial_puts;。
分析名为"serial"stdio设备
static void drv_system_init (void)
{
struct stdio_dev dev;
strcpy (dev.name, "serial");
dev.flags = DEV_FLAGS_OUTPUT | DEV_FLAGS_INPUT;
dev.putc = stdio_serial_putc;
dev.puts = stdio_serial_puts;
stdio_register (&dev);
}
由上面信息可以得到终极线索
线索四
1. printf最终由名为serial的stdio设备的puts输出
2. 而这个设备的puts指向serial_current的puts输出。
serial_devices
\drivers\serial\serial.c
static struct serial_device *serial_current;
文中可以看到serial_current是个指针
那么什么是serial_current指针呢,他代表什么含义呢 由上分析需要看下serial_devices类型
struct serial_device *s = serial_devices;本文件找drivers\serial\serial.c
struct serial_device {
/* enough bytes to match alignment of following func pointer */
char name[16];
int (*start)(void);
int (*stop)(void);
void (*setbrg)(void);
int (*getc)(void);
int (*tstc)(void);
void (*putc)(const char c);
void (*puts)(const char *s);
#if CONFIG_POST & CONFIG_SYS_POST_UART
void (*loop)(int);
#endif
struct serial_device *next; //这里有一个同样的类型说明是个单项链表
};
//同时在本文件看到serial设备的注册函数
//从注册函数上能看到新注册的设备永远在最前的头上,也就是serial_devices表示的是最新的serial设备
void serial_register(struct serial_device *dev)
{
dev->next = serial_devices;
serial_devices = dev;
}
serial_register函数的调用就有很多了,一般和编译选项绑定平台有关,
那么去该目录 下去看哪个平台编译了就那个平台调用的,这个串口就和平台的控制器相关了。
我这里用的是serial_pl01x.c 驱动
serial_pl01x驱动
static struct serial_device pl01x_serial_drv = {
.name = "pl01x_serial",
.start = pl01x_serial_init,
.setbrg = pl01x_serial_setbrg,
.putc = pl01x_serial_putc,
.puts = default_serial_puts,
.getc = pl01x_serial_getc,
.tstc = pl01x_serial_tstc,
};
void pl01x_serial_initialize(void)
{
serial_register(&pl01x_serial_drv);
}
确定调用关系
pl01x_serial_initialize <= serial_initialize <= init_sequence_r[]
void serial_initialize(void)
{
s3c24xx_serial_initialize();
pl01x_serial_initialize();
...
serial_assign(default_serial_console()->name);
}
serial_initiali这个函数也是一堆的初始化,有其他各个平台的初始化,按照顺序注册serial设备
注意的是最后一个调用的serial_initialize(),就是serial_devices指向的设备。
注意这个函数serial_assign,看代码也能看明白
serial_current变量指向的是serial_assign指定名字的那个serial设备。
而我当前编译环境配置的平台只有serial_pl01x.c 驱动中定义了default_serial_console
那么自然serial_current指向的是pl01x中注册的serial设备
int serial_assign(const char *name)
{
struct serial_device *s
for (s = serial_devices; s; s = s->next) {
if (strcmp(s->name, name))
continue;
serial_current = s;
return 0;
}
return -EINVAL;
}
那么这里产生第5个线索五
1. serial_current指向的是pl01x中注册的serial设备
2. serial_devices中只有一个设备就是pl01x设备
3. 那么中合线索四也就知道puts最终指向pl01x设备的puts,
4. 而这个puts最后调用putc,putc直接控制寄存器,也就符合了我们最初的理论最终是会体现到硬件控制器的寄存器上。
终极大法
1.printf调用puts函数
2.puts函数最后调用标准输出的puts(stdio_devices[file]->puts(stdio_devices[file])
3.stdio的设备有名为serial的stdio设备和serial_devices注册后的stdio设备,经分析标准输出设备指向"serial"的stdio设备
4.最终的打印体现在"serial"的stdio设备的puts函数,而"serial"d puts又等于serial_current的puts函数
5.serial_current设备指向的就是当前编译中配置的设备目前我的平台用的是pl01x设备,所以是pl01x的puts。
6.而pl0x的pus最后调用的是pl01X的putc函数,最后通过寄存器发送出去。
图如下