第一個問題:啟動Linux的時候LCD會全屏花屏大約0.5秒,然后左上角出現一塊不明花斑。
這個問題相對簡單。因為我在Bootloader里面打開了液晶顯示,緩沖區映射在某個地址上,當內核初始化MMU的時候,LCD控制寄存器里 緩沖區的位置信息就不對了,或者是Bootloader使用的緩沖區被內核的數據或代碼覆蓋,導致在內核初始化LCD之前,LCD花屏。
那個不明花斑其實是Linux的可愛小企鵝圖片,但是可能因為緩沖區像素位寬和格式、LCD調色板設置等問題顯示不出來。
花屏解決方法:在Bootloader加載系統之前關掉LCD控制器或者關掉LCD的背光,這樣做比較簡單。復雜的,就得改MMU映射部分代 碼,在修改了MMU映射之后,立即修改LCD緩沖的位置。反正我就關掉LCD控制器了,因為內核很快就會初始化LCD,Windows啟動都黑屏呢,我們 黑那么2秒鍾也沒有什么大不了的。
花斑解決方法:相信如果做產品的話,不需要顯示什么企鵝給用戶看,所以可以在內核選項里將這個企鵝logo關掉。具體位置在Device Drivers>Graphics support>Logo configuration,對應的宏相信在.config里面很容易找到。如果非要顯示這個企鵝,那么可以在/drivers/video /console/fbcon.c里面找找,我也不知道怎么弄。
第二個問題:Linux啟動之后,只要一段時間不動鍵盤(開發板上用IO擴展出來的鍵盤),LCD就會自動關閉(黑屏、顯示慢慢消失之類),只要按下鍵盤就能恢復。
這個問題讓我花了一天多的時間。其實如果是手持設備,這樣也沒有什么。但是我們公司的產品是要一直顯示東西的,必須解決這個問題。我看了很多論 壇,有不少人也遇到了這個問題,但是我剛才是搜索的時候,關鍵詞不對,總找不到正確的答案。如果你遇到了同樣的問題,而且不想看我的三腳貓分析,那么就在 百度上搜索“blankinterval”、“setterm -blank 0”之類的,馬上你就能找到簡單的解決方案。
這個問題很容易讓人想到屏幕保護和電源管理。的確,這是一種電源管理。但是,你卻無法從Linux內核選項的電源管理中解決這個問題。我們一步步來。
首先,我測量到LCD的PCLK時鍾消失了,這意味着內核把LCD控制器關掉了。於是,從LCD驅動程序着手。我用的是S3C2440,這是 2410的升級版,但是LCD控制器是一樣的,在我拿到的開發板廠商給我做好驅動的內核里,驅動的位置在/drivers/video /s3c2410fb.c。為什么后面有個fb呢?這是Framebuffer的縮寫,百度下你能找到很多關於它的解釋。Framebuffer是所有 Linux下GUI程序對硬件操作的設備接口,位於/dev中,一般為fb0。在s3c2410fb.c中可以找到一個類似 s3c2410_disable_controller()這樣名稱的函數,我的驅動里叫pxafb_disable_controller(),可以看 出這個驅動是從pxa處理器改的,當然廠家不一樣名字也叫得不一樣。里面有一句類似這樣寫的 __raw_writel(fbi->reg.lcdcon1 & ~S3C2410_LCDCON1_ENVID, S3C2410_LCDCON1);,把這句話刪掉LCD就不會關掉了。這是第一個層次,我也看到有人是這樣做的。但是,這有問題,按鍵盤恢復后,原本顯 示在屏幕上的東西如果你不重畫會消失,就算你重畫了,也會看到屏幕的某些部分先黑了下,然后恢復了。當然如果你可以接受,那么就這樣吧。
然后,可以很自然的想到是誰調用了這個函數,從源頭把這個問題消除掉。但是情況卻不是這樣的。我搜索這個函數名,找到了一個 set_ctrlr_state()的函數調用了pxafb_disable_controller(),搜索set_ctrlr_state(),發現 有個pxafb_task()調用了set_ctrlr_state(),但是到了pxafb_task()就沒有辦法再往上找了,因為這是提供給內核的 一個任務,以指針傳遞函數入口。我對內核了解太不夠了,花了很多時間看了很多論壇上的文章,機緣巧合之下,我找到了/drivers/char/vt.c 這個文件。vt.c我感覺應該是2.4內核的console.c和vt.c的結合體,應為它集成了console基本上所有功能函數,就ioctl在 vt_ioctl.c這個文件里。這個文件的主要作用是負責管理控制台,如控制台的模式(圖形、字符)、向控制台輸出等等。其中能找到一些如 do_blank_screen(),blank_screen_t()這樣的函數,就是這些函數關閉了LCD控制器,修改任意一個都可以起作用。網上的 一個解決方案是把blank_screen_t()變成空函數,但是我沒有這樣試過,我覺得已經來到了問題的根源附近,應該能從根本上解決。
我們先看下屏幕關閉問題的真正起因,看這個控制台初始化函數
static int __init con_init(void) { const char *display_desc = NULL; struct vc_data *vc; unsigned int currcons = 0; acquire_console_sem(); if (conswitchp) display_desc = conswitchp->con_startup(); if (!display_desc) { fg_console = 0; release_console_sem(); return 0; } init_timer(&console_timer); console_timer.function = blank_screen_t; if (blankinterval) { blank_state = blank_normal_wait; mod_timer(&console_timer, jiffies + blankinterval); }
// 這是對控制台定時器的初始化,定時器事件函數被連接到了blank_screen_t() /* * kmalloc is not running yet - we use the bootmem allocator. */ for (currcons = 0; currcons < MIN_NR_CONSOLES; currcons++) { vc_cons[currcons].d = vc = alloc_bootmem(sizeof(struct vc_data)); visual_init(vc, currcons, 1); vc->vc_screenbuf = (unsigned short *)alloc_bootmem(vc->vc_screenbuf_size); vc->vc_kmalloced = 0; vc_init(vc, vc->vc_rows, vc->vc_cols, currcons || !vc->vc_sw->con_save_screen); } currcons = fg_console = 0; master_display_fg = vc = vc_cons[currcons].d; set_origin(vc); save_screen(vc); gotoxy(vc, vc->vc_x, vc->vc_y); csi_J(vc, 0); update_screen(vc); printk("Console: %s %s %dx%d", vc->vc_can_do_color ? "colour" : "mono", display_desc, vc->vc_cols, vc->vc_rows); printable = 1; printk("/n"); release_console_sem(); #ifdef CONFIG_VT_CONSOLE register_console(&vt_console_driver); #endif return 0; }
其中引用了一個叫blankinterval的全局變量和一個console_time,我不知道內核的定時器是具體是怎么工作,但是 這樣的代碼已經很明顯了。這個定時器和電源管理宏PM_CONFIG沒有任何關系,它是控制台的一部分。再看下blank_screen_t():
static void blank_screen_t(unsigned long dummy)
{
blank_timer_expired = 1;
schedule_work(&console_work);
}
{
blank_timer_expired = 1;
schedule_work(&console_work);
}
可以發現vt.c開頭的宏,static DECLARE_WORK(console_work, console_callback, NULL);,找到了console_callback()這個函數:
static void console_callback(void *ignored) { acquire_console_sem(); if (want_console >= 0) { if (want_console != fg_console && vc_cons_allocated(want_console)) { hide_cursor(vc_cons[fg_console].d); change_console(vc_cons[want_console].d); /* we only changed when the console had already been allocated - a new console is not created in an interrupt routine */ } want_console = -1; } if (do_poke_blanked_console) { /* do not unblank for a LED change */ do_poke_blanked_console = 0; poke_blanked_console(); } if (scrollback_delta) { struct vc_data *vc = vc_cons[fg_console].d; clear_selection(); if (vc->vc_mode == KD_TEXT) vc->vc_sw->con_scrolldelta(vc, scrollback_delta); scrollback_delta = 0; } if (blank_timer_expired) { do_blank_screen(0); blank_timer_expired = 0; } release_console_sem(); }
再看do_blank_screen(),隨着struct vc_data中的與fops類似指針跟蹤下去,就可以找到驅動里面的相應代碼了。寫出來太麻煩,讓我偷懶把。
小總結下,其實在控制台內部就有一個定時器,它負責在一定時間之后將顯示關閉,而無視是否打開了電源管理功能。那這和Framebuffer有什么關系呢?我從一個很弱智的角度解釋,內核剛啟動的時候有這樣一句輸出:
Console: colour dummy device 80x30
在初始化LCD控制器(Framebuffer)之后,有這樣一句輸出:
Console: switching to colour frame buffer device 80x30
我就理解:這時候,內核把控制台(也不知道是console還是tty)切換到了Framebuffer上,大蝦們趕快跳出來批判我吧,呵呵。
回到正題,從代碼可以發現,根本的解決之道是讓blankinterval = 0,blank_state就不會是blank_off之外的值,也就不會關閉屏幕了。
但是問題到這里還是沒有完全解決,如果用戶程序希望改變blankinterval來實現屏保(當然在我的系統上用不着);另外,一些程序改變 了blankinterval,程序退出之后,屏幕在一段時間之后還是會關閉的。怎么才能在用戶程序那頭解決這個問題呢,這又耗費了我很多時間。
我在追查代碼的過程中走了個彎路,認為修改控制台的模式可以不讓黑屏現象出現,但是后來發現,這樣可能會使控制台沒有辦法畫圖,不知道對不對。
忽略彎路,直接正解。vt.c中不是有很多操作控制台的函數么?看看是誰修改了blankinterval。於是搜索 blankinterval,發現setterm_command()修改了它,然后搜索setterm_command,找到了 do_con_trol()函數,搜索do_con_trol,找到了do_con_write()函數,搜索do_con_write,終於最終 BOSS現身了:con_write()函數。為什么說它是最終BOSS呢?看看這段:
static struct tty_operations con_ops = { .open = con_open, .close = con_close, .write = con_write, .write_room = con_write_room, .put_char = con_put_char, .flush_chars = con_flush_chars, .chars_in_buffer = con_chars_in_buffer, .ioctl = vt_ioctl, .stop = con_stop, .start = con_start, .throttle = con_throttle, .unthrottle = con_unthrottle, };
熟悉fops的話你就能看出來了,這是對tty設備的文件操作函數的表。也就是說,在用戶程序里,通過open函數打開/dev/tty,然后 再用write函數就可以修改blankinterval了。原理是找到了,實踐上有很大困難,那么多重函數調用,再看看do_con_trol()里面 的switch語句,正常人都要發暈。好在偉大的百度為我們提供了很多信息:在命令行下,可以使用setterm -blank 0指令來設置blankinterval。哈哈,救星來了,趕快看看setterm的源代碼。setterm屬於util-linux包,搜索一下很容易 找到。其中的perform_sequence()函數里有這樣一段:
/* -blank [0-60]. */
if (opt_blank && vcterm)
printf("/033[9;%d]", opt_bl_min);
if (opt_blank && vcterm)
printf("/033[9;%d]", opt_bl_min);
真得很神奇啊,用個printf就可以在用戶程序里解決這個問題,本來我是打算只說用printf解決的,看到原理我想會更舒服一些;況且,在我的系統上用printf是不行的。
但是!問題還沒有完,往往在我們的系統中,LCD的虛擬控制台和控制台TTY不是同一個設備,也就是說,如果在程序里單純的printf是不行的!這樣只能修改你正在使用的TTY的blankinterval,而你用的卻是文本方式的設備,不存在黑屏問題。
於是,就需要仔細比較/dev/console、/dev/tty、/dev/ttyn的設備號,在我的系統里,用戶程序里/dev /console和/dev/tty都是5,說明他們是一個東西,/dev/ttyn是4,這才是FB上的虛擬控制台。但是/dev/ttyn不是正在使 用的TTY,那么怎么printf呢?只好用write函數來解決了。
寫這樣一段代碼:
#include <fcntl.h> #include <stdio.h> #include <sys/ioctl.h> void some_function() { int f; f = open("/dev/tty0", O_RDWR); write(f, "/033[9;0]", 8); close(f); }
還有一種差不多的方法,就是在應用程序中寫入
system("echo -e \"\033[9;0]\" > /dev/tty0");
另外在控制台上
echo 0 > /sys/class/graphics/fb0/blank//打開LCD echo 1 > /sys/class/graphics/fb0/blank//關閉LCD
問題終於解決了。
總結下,第二個問題有很多種解決方法:
1.修改LCD驅動,把關閉LCD控制器的函數變為空(不推薦)
2.修改vt.c中的blank_screen_t()函數,讓其為空(在系統不需要使用關閉顯示功能時推薦)
3.修改vt.c中的blankinterval,讓其為0(系統可能需要使用關閉顯示功能,而且希望系統上電后正常狀態下不會關閉顯示時推薦)
4.修改用戶程序,加入設置blankinterval的代碼(推薦)
