本文主要是介绍printk打印到LCD,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
command_line="initrd=0x30800000,0x440000 root=/dev/ram init=/linuxrc console=tty0";
这样就可以改变系统的控制台了. 还是在setup_arch函数中找到了以下代码,以后会用到的.
#ifdef CONFIG_VT
#if defined(CONFIG_VGA_CONSOLE)
conswitchp = &vga_con;
#elif defined(CONFIG_DUMMY_CONSOLE)
conswitchp = &dummy_con;
#endif
#endif
CONFIG_VT,在menuconfig的Character devices中选上Virtual terminal
CONFIG_VGA_CONSOLE,在menuconfig的Console drivers中选上VGA text console
CONFIG_DUMMY_CONSOLE,在menuconfig的Console drivers的Frame-buffer support中
选上Support for frame buffer devices,参看driver/video中的Config.in文件
意思是如果选择VGA文本控制台,那么缺省的显示设备接口是vga_con,如果是LCD之类的
Frame-buffer设备,则缺省的显示设备接口是dummy_con.
我这边是用的LCD,所以重点关注dummy_con.
const struct consw *conswitchp;
const struct consw dummy_con = {
con_startup: dummycon_startup,
con_init: dummycon_init,
con_deinit: DUMMY,
con_clear: DUMMY,
con_putc: DUMMY,
con_putcs: DUMMY,
con_cursor: DUMMY,
con_scroll: DUMMY,
con_bmove: DUMMY,
con_switch: DUMMY,
con_blank: DUMMY,
con_font_op: DUMMY,
con_set_palette: DUMMY,
con_scrolldelta: DUMMY,
};
还是回到start_kernel中,接下来调用了parse_options(command_line)来处理命令行
它会以空格对命令行进行分解,比如分解后使得line="console=ttyS0",然后调用
checksetup(line),下面分析一下这个函数
static int __init checksetup(char *line)
{
struct kernel_param *p;
p = &__setup_start; //setup段开始地址
do {
int n = strlen(p->str);
if (!strncmp(line,p->str,n)) //比较line和p->str的n个字符
{
if (p->setup_func(line+n))//相等则调用函数
return 1;
}
p++;
} while (p < &__setup_end);
return 0;
}
需要理解的是这个setup段,这和驱动的初始化init_call段类似,在include/linux/init.h
中可以找到
#define __setup(str, fn) /
static char __setup_str_##fn[] __initdata = str; /
static struct kernel_param __setup_##fn __attribute__((unused)) __initsetup = { __setup_str_##fn, fn }
#define __initsetup __attribute__ ((unused,__section__ (".setup.init")))
另外我在kernel/printk.c中找到了__setup("console=", console_setup);
通过这个函数,在链接时就会在段.setup.init中建立一个kernel_param结构,其中一个成员
是字符串"console="的指针,另一个则是console_setup这个函数指针,于是这样在checksetup
中就必然会调用到console_setup这个函数了.
该函数的作用是把传入的字符串进行处理并保存在全局结构中.我们以console=ttyS0 console=tty0
为例,这样就会调用console_setup两次,第一次是console_setup("ttyS0"),第二次是console_setup("tty0").
全局结构struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES];
struct console_cmdline
{
char name[8]; /* Name of the driver */
int index; /* Minor dev. to use */
char *options; /* Options for the driver */
};
第一次就会使console_cmdline[0]的name="ttyS",index=0,options=NULL
第二次就会使console_cmdline[1]的name="tty",index=0,options=NULL
并且最后preferred_console = 1 ,这些结构会在注册控制台时用到.
接着下来还是回到start_kernel函数中,该函数调用了console_init()来初始化console
在该函数中调用了con_init()函数来初始化tty控制台,uart_console_init()初始化ttyS控制台
在con_init()函数中有
#ifdef CONFIG_VT_CONSOLE //menuconfig的Character devices中选上Support for console...
register_console(&vt_console_driver);
#endif
struct console vt_console_driver = {
name: "tty",
write: vt_console_print,
device: vt_console_device,
wait_key: keyboard_wait_for_keypress,
flags: CON_PRINTBUFFER,
index: -1,
};
在register_console函数中会把name和前面赋值的console_cmdline[1]中的name比较,
console->flags |= CON_ENABLED;
console->index = console_cmdline[i].index;
if (i == preferred_console) console->flags |= CON_CONSDEV;
这样其实在命令行最后的console的i才会等于preferred_console,所以它会是主控制台
if ((console->flags & CON_CONSDEV) || console_drivers == NULL) {
console->next = console_drivers;
console_drivers = console;
} else {
console->next = console_drivers->next;
console_drivers->next = console;
}
会把控制台驱动组织成单链表格式,头结点是全局指针struct console *console_drivers
它指向命令行参数最后的console,以后注册的回插在其后
比如在我这里就是console_drivers == vt_console_driver --> s3c2410_cons --> NULL
前面已经说过printk和register_console这两个函数最后都会调用release_console_sem函数,
该函数最终调用了printk.c中的__call_console_drivers函数来打印字符。
static void __call_console_drivers(unsigned long start, unsigned long end)
{
struct console *con;
for (con = console_drivers; con; con = con->next) {
if ((con->flags & CON_ENABLED) && con->write)
con->write(con, &LOG_BUF(start), end - start);
}
} 这个函数又调用了console_drivers中各个成员的write方法
对于vt_console_driver,其write方法就是vt_console_print,到这里我们还需要回去看几个
特别重要的结构,以便更好的理解这个函数。
在console.c的con_init函数中有如下代码
for (currcons = 0; currcons < MIN_NR_CONSOLES; currcons++) {
vc_cons[currcons].d = (struct vc_data *)
alloc_bootmem(sizeof(struct vc_data));
vt_cons[currcons] = (struct vt_struct *)
alloc_bootmem(sizeof(struct vt_struct));
visual_init(currcons, 1);
screenbuf = (unsigned short *) alloc_bootmem(screenbuf_size);
kmalloced = 0;
vc_init(currcons, video_num_lines, video_num_columns,
currcons || !sw->con_save_screen); //sw=dummy_con
}
currcons = fg_console = 0;
master_display_fg = vc_cons[currcons].d;
看看定义
#define MIN_NR_CONSOLES 1
struct vc vc_cons [MAX_NR_CONSOLES];
struct vt_struct *vt_cons[MAX_NR_CONSOLES];
int fg_console; fg_console is the current virtual console
static struct vc_data *master_display_fg;
这样我们可以知道这段代码首先分配了vc_cons[0].d的内存,vt_cons[0]的内存,
然后调用了visual_init来填充vc_cons[0].d结构
static void visual_init(int currcons, int init)
{
/* ++Geert: sw->con_init determines console size */
sw = conswitchp; //#define sw (vc_cons[currcons].d->vc_sw)
#ifndef VT_SINGLE_DRIVER //这里sw就被赋值为上面介绍过的dummy_con了
if (con_driver_map[currcons]) //第一次调用时是NULL
sw = con_driver_map[currcons];
#endif
cons_num = currcons;
display_fg = &master_display_fg;
vc_cons[currcons].d->vc_uni_pagedir_loc = &vc_cons[currcons].d->vc_uni_pagedir;
vc_cons[currcons].d->vc_uni_pagedir = 0;
hi_font_mask = 0;
complement_mask = 0;
can_do_color = 0;
sw->con_init(vc_cons[currcons].d, init); //调用了dummycon_init
if (!complement_mask) //can_do_color=1
complement_mask = can_do_color ? 0x7700 : 0x0800;
s_complement_mask = complement_mask;
video_size_row = video_num_columns<<1;
screenbuf_size = video_num_lines*video_size_row;
}这里的video_num_lines=80 video_num_columns=30是在dummycon_init调用中赋值的。
接下来分配一个80*30*2的内存screenbuf,是用来存放需要显示到屏幕上的数据的,
当然由于刚开始我们的sw指向的是dummy_con,是丢弃控制台,其结构方法基本都是空操作,
所以并不会向屏幕上打印什么。只有在后来初始化了lcd的驱动,sw会指向fb_con结构,那
才能真正在lcd屏上显示。具体的在下面接着介绍。
回到vt_console_print函数中看,其中一段代码
while (count--) {
c = *b++;
if (c == 10 || c == 13 || c == 8 || need_wrap) {
if (cnt > 0) {
if (IS_VISIBLE)
sw->con_putcs(vc_cons[currcons].d, start, cnt, y, x);
x += cnt;
if (need_wrap)
x--;
cnt = 0;
}
if (c == 8) { /* backspace */
bs(currcons);
start = (ushort *)pos;
myx = x;
continue;
}
if (c != 13)
lf(currcons);
cr(currcons);
start = (ushort *)pos;
myx = x;
if (c == 10 || c == 13)
continue;
}
scr_writew((attr << 8) + c, (unsigned short *) pos);
cnt++;
if (myx == video_num_columns - 1) {
need_wrap = 1;
continue;
}
pos+=2;
myx++;
}
仔细分析一下可以知道,要显示的字符会通过scr_writew保存到vc_cons[0].d->vc_screenbuf
中pos指向的位置,当然保存的是(attr << 8) + c的值,而如果字符是10 13 8或者一行满的时候
则会调用sw->con_putcs方法把该行显示至屏幕,对于dummy_con当然是什么也不做了,而对于
fb_con则会在lcd上显示。看到有个条件是 IS_VISIBLE
#define IS_VISIBLE CON_IS_VISIBLE(vc_cons[currcons].d)
#define CON_IS_VISIBLE(conp) (*conp->vc_display_fg == conp)
是要求*(vc_cons[currcons].d->vc_display_fg) == vc_cons[currcons].d
而上面在visual_init中有display_fg = &master_display_fg;
出来在con_init中又有master_display_fg = vc_cons[currcons].d;
于是其实master_display_fg = vc_cons[0].d
这样对于currcons=0时 IS_VISIBLE 为真。
在没有注册vt_console_driver前调用printk函数,要打印的字符只会保存到全局数组log_buf中,
而在注册vt_console_driver时同样会把以前保存在log_buf中的打印出来.看代码就知道了
接下来看看真正的lcd驱动是如何与控制台接轨的.
在drivers/video/s3c2410fb.c中找到lcd初始化的代码
int __init s3c2410fb_init(void)
{
struct s3c2410fb_info *fbi;
int ret;
fbi = s3c2410fb_init_fbinfo();
ret = -ENOMEM;
if (!fbi)
goto failed;
ret = s3c2410fb_map_video_memory(fbi);
if (ret)
goto failed;
s3c2410_lcd_init();
s3c2410fb_set_var(&fbi->fb.var, -1, &fbi->fb);
ret = register_framebuffer(&fbi->fb);
if (ret < 0)
goto failed;
printk("Installed S3C2410 frame buffer/n");
MOD_INC_USE_COUNT ;
return 0;
failed:
if (fbi)
kfree(fbi);
return ret;
}
主要是分配和填充struct s3c2410fb_info结构体,其中细节的地方下一次再分析,
这里主要看一下lcd驱动和控制台的关系.
在register_framebuffer(&fbi->fb)中调用了
take_over_console(&fb_con, first_fb_vc, last_fb_vc, fbcon_is_default);
这个函数会用fb_con替换dummy_con,具体看其中的代码
void take_over_console(const struct consw *csw, int first, int last, int deflt)
{
int i, j = -1;
const char *desc;
desc = csw->con_startup();
if (!desc) return;
if (deflt)
conswitchp = csw; //这里把fb_con赋给全局变量conswitchp
for (i = first; i <= last; i++) {
int old_was_color;
int currcons = i;
con_driver_map[i] = csw;
if (!vc_cons[i].d || !vc_cons[i].d->vc_sw)
continue;
j = i;
if (IS_VISIBLE)
save_screen(i);
old_was_color = vc_cons[i].d->vc_can_do_color;
vc_cons[i].d->vc_sw->con_deinit(vc_cons[i].d);
visual_init(i, 0); //这里又调用visual_init
update_attr(i);
if (old_was_color != vc_cons[i].d->vc_can_do_color)
clear_buffer_attributes(i);
if (IS_VISIBLE)
update_screen(i);
}
printk("Console: switching ");
if (!deflt)
printk("consoles %d-%d ", first+1, last+1);
if (j >= 0)
printk("to %s %s %dx%d/n",
vc_cons[j].d->vc_can_do_color ? "colour" : "mono",
desc, vc_cons[j].d->vc_cols, vc_cons[j].d->vc_rows);
else
printk("to %s/n", desc);
}
再次调用visual_init的时候把vc_cons[0].d->vc_sw赋值为fb_con,同时通过
sw->con_init(vc_cons[currcons].d, init)调用了fbcon_init函数
该函数又调用了fbcon_setup函数fbcon_setup(unit, init, !init)
在fbcon_setup中如果logo=1的话就会把screenbuf空间中的上面一块
留给logo就是那个蜻蜓,又调用了vc_resize_con(nr_rows, nr_cols, con),
vc_resize_con会重新调整vc_cons[0].d->vc_screenbuf并把原来screenbuf中
对应的数据拷过来,原来是80*30,现在成了40*30,而上面的10*30留给了logo,
这样只把原来最下面的30*30复制过来,通过调用update_screen(currcons)来
更新屏幕,update_screen调用fbcon_switch显示logo,并显示screen_buf中的数据.
take_over_console以后再调用printk函数就会调用fbcon_putcs真正实现把
屏幕显示.
接下来看看几个显示设备的区别吧,分别是tty,tty0,console,vc/0
在drivers/char/mem.c函数中找到chr_dev_init函数,其调用的tty_init在
tty_io.c中,该函数注册了/dev/tty,/dev/console,/dev/vc/0这几个设备,
而/dev/tty0是通过mknod建立的节点,并不是devfs设备.
他们都是通过tty_register_driver注册的,而且其设备方法集都是tty_fops
tty 5 0
console 5 1
tty0 4 0
vc/0 4 0
major和minor如上所示,这样看来关键还是看tty_fops中的方法,先看tty_open
device = inode->i_rdev;
if (device == TTY_DEV) {
if (!current->tty)
return -ENXIO;
device = current->tty->device; //打开tty时用current->tty->device
filp->f_flags |= O_NONBLOCK; /* Don't let /dev/tty block */
/* noctty = 1; */
}
#ifdef CONFIG_VT
if (device == CONSOLE_DEV) { //打开tty0和vc/0时用device=0x401
extern int fg_console;
device = MKDEV(TTY_MAJOR, fg_console + 1);
noctty = 1;
}
#endif
if (device == SYSCONS_DEV) {
struct console *c = console_drivers;
while(c && !c->device)
c = c->next;
if (!c)
return -ENODEV;
device = c->device(c);
filp->f_flags |= O_NONBLOCK; /* Don't let /dev/console block */
noctty = 1;
}
而打开console时,调用了vt_console_device,使用串口控制台的话当然返回的是ttyS0的设备号,这里只介绍LCD控制台。
static kdev_t vt_console_device(struct console *c)
{
return MKDEV(TTY_MAJOR, c->index ? c->index : fg_console + 1);
}
在我这里返回的也是0x401
这样看来tty0和vc/0和console甚至vc/1其device都是0x401
接下来调用了init_dev(device, &tty)分配填充tty结构,在init_dev函数中
driver = get_tty_driver(device)获取对应设备的驱动,0x401获取的并不是
在tty_init中注册的dev_tty_driver或dev_syscons_driver,而是在console.c
con_init中注册的console_driver,可以看到这里还同时要注册vc/1-63设备,不过
由于开始没有支持devfs,其实是在tty_init中调用con_init_devfs来注册的.
于是driver=&console_driver,这样tty->driver=&console_driver,在tty_open
中有filp->private_data = tty,并通过tty->driver.open(tty, filp)调用了con_open函数.
重点就是要知道filp->private_data = tty,tty这个结构,在tty_read和tty_write中都用到
再来看tty_write函数
tty = (struct tty_struct *)file->private_data;
do_tty_write(tty->ldisc.write, tty, file,(const unsigned char *)buf, count);
而tty结构中的ldisc其实是在console_init函数中注册的
tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY),就是tty_ldisc_N_TTY
其write方法就是write_chan,在drivers/char/N_tty.c中,事实上
在write_chan中还是通过tty->driver.write(tty, 1, b, nr)调用了console_driver的
write方法con_write,其又调用了do_con_write(tty, from_user, buf, count)
其中很关键的两句代码
struct vt_struct *vt = (struct vt_struct *)tty->driver_data;
currcons = vt->vc_num;
这样就必须回去看看tty->driver_data是怎么赋值的,这是在con_open中给出的
currcons = MINOR(tty->device) - tty->driver.minor_start;
i = vc_allocate(currcons);
if (i)
return i;
vt_cons[currcons]->vc_num = currcons;
tty->driver_data = vt_cons[currcons];
而console_driver.minor_start = 1
于是乎对于0x401设备来说currcons=0,vt_cons[0]->vc_num = 0
tty->driver_data = vt_cons[0]
回到do_con_write中定义了
#define FLUSH if (draw_x >= 0) { /
sw->con_putcs(vc_cons[currcons].d, (u16 *)draw_from, (u16 *)draw_to-(u16 *)draw_from, y, draw_x); /
draw_x = -1; /
}
通过FLUSH调用了fb_con的con_putcs方法把数据写到lcd上去.
当然注意了这里
if (DO_UPDATE && draw_x < 0) {
draw_x = x;
draw_from = pos;
}
#define DO_UPDATE IS_VISIBLE
如果IS_VISIBLE为0的话就没法调用con_putcs更新屏幕了,
这也是为什么写vc/2等不能显示的原因了.
这篇关于printk打印到LCD的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!