先從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等不能顯示的原因了.