printk打印到LCD

来源:互联网 发布:网络红歌2015流行歌曲下载 编辑:程序博客网 时间:2024/05/16 09:18

先从ppcboot传过来的启动命令参数说起,ppcboot把它放在内的存固定地址,参数如下
char linux_cmd[] = "initrd=0x30800000,0x440000 root=/dev/ram init=/linuxrc console=ttyS0";
在内核start_kernel函数中调用了setup_arch(&command_line)取到命令参数并保存到
saved_command_line字符数组里,同时command_line字符指针指向一个全局的字符数组
command_line(同名,不过这是全局的),其中存放的同样是命令行参数。
为了方便,我们可以在此就另给command_line赋值,而不需要去改ppcboot了。
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等不能显示的原因了.