我的博客startcraft
昨天寫的內核在屏幕上沒有我們輸出的東西,今天就來想辦法顯示點什麼
文字的顯示
要顯示東西就涉及到顯卡了,顯卡有兩種模式,文本模式和圖形模式,現在基本都是圖形模式用得多,但是我們這個就用文本模式了,畢竟不涉及ui啥的
文本的顯示規則
顯卡通電後就自動初始化了80\ * 25分辨率的文本模式,即一屏25行,一行80個字符
之前說過內存地址空間不是全部映射到主存的,有一部分映射到外部設備,0xB8000~0xBFFFF就是映射到顯卡文本模式的顯存的地址空間
從0xB8000開始,每兩個字節對應屏幕上的一個字符,從第一行開始,字符在內存中的存儲形式叫內碼,第一個字符對應字符的ASCII碼,第二個控制字符的顏色等等信息,每一位都有不同的含義,如下圖: (圖片來自教程)
除了顯存之外,顯卡還有一些控制單元,這些單元沒有映射在cpu尋址的4GB空間裏,需要使用in / out命令來讀寫,這部分寄存器的訪問規則是:
- 通過0x3D4端口來設置寄存器索引,就是要訪問哪一個寄存器
- 通過0x3D5端口來設置寄存器的值
端口讀寫函數
這些要用in / out命令訪問的寄存器用c的代碼顯然是不行的,只能用匯編實現,這裏選擇在c裏面內嵌彙編代碼參考彙編語言和C語言混合編程和內聯彙編
libs/common.c
#include "common.h"
//往端口寫一個字節
inline void outb (uint16_t port, uint8_t value)
{
asm volatile ("outb %1,%0"::"dN"(port), "a"(value));
}
//從端口讀一個字節
inline uint8_t inb (uint16_t port)
{
uint8_t ret;
asm volatile ("inb %1,%0":"=a"(ret):"dN"(port));
return ret;
}
//從端口讀一個字
inline uint16_t inw (uint16_t port)
{
uint16_t ret;
asm volatile ("inw %1,%0":"=a"(ret):"dN"(port));
return ret;
}
include/common.h
#ifndef INCLUDE_COMMON_H_
#define INCLUDE_COMMON_H_
#include "types.h"
//往端口寫一個字節
void outb (uint16_t port, uint8_t value);
//從端口讀一個字節
uint8_t inb (uint16_t port);
//從端口讀一個字
uint16_t intw (uint16_t port);
#endif
頭文件前面的那個ifndef和define是爲了防止在不同的文件中include了同一個頭文件會出現的重複定義錯誤
函數前面的inline是建議編譯器把函數當成內聯函數來編譯,即在函數的調用處進行代碼展開,而不是傳統函數調用
顏色定義和屏幕操作函數
是顏色定義的枚舉和一些屏幕控制函數的聲明
include/console.h
#ifndef INCLUDE_CONSOLE_H_
#define INCLUDE_CONSOLE_H_
#include "types.h"
typedef
enum real_color {
rc_black = 0,
rc_blue = 1,
rc_green = 2,
rc_cyan = 3,
rc_red = 4,
rc_magenta = 5,
rc_brown = 6,
rc_light_grey = 7,
rc_dark_grey = 8,
rc_light_blue = 9,
rc_light_green = 10,
rc_light_cyan = 11,
rc_light_red = 12,
rc_light_magenta = 13,
rc_light_brown = 14, // yellow
rc_white = 15
} real_color_t;
// 清屏操作
void console_clear();
// 屏幕輸出一個字符帶顏色
void console_putc_color(char c, real_color_t back, real_color_t fore);
// 屏幕打印一個以 \0 結尾的字符串默認黑底白字
void console_write(char *cstr);
// 屏幕打印一個以 \0 結尾的字符串帶顏色
void console_write_color(char *cstr, real_color_t back, real_color_t fore);
// 屏幕輸出一個十六進制的整型數
void console_write_hex(uint32_t n, real_color_t back, real_color_t fore);
// 屏幕輸出一個十進制的整型數
void console_write_dec(uint32_t n, real_color_t back, real_color_t fore);
#endif // INCLUDE_CONSOLE_H_
drivers/console.c
定義一些變量,static修飾代表只在該文件內生效
static uint16_t *video_memory = (uint16_t *)0xB8000; //顯存的起始地址,每兩個字節表示一個字符所以是16位
//屏幕光標的座標
static uint8_t cursor_x = 0;
static uint8_t cursor_y = 0;
輸入光標的移動
//移動光標的位置
static void move_cursor ()
{
uint16_t cursorLocation = cursor_y * 80 + cursor_x; //一行有80個字符
/*控制光標位置的寄存器爲14號和15號寄存器,
* 分別存儲位置的高八位和低八位*/
outb(0x3D4, 14); //要設置14號寄存器,即位置信息的高八位
outb(0x3D5, cursorLocation >> 8);
outb(0x3D4, 15); //要設置15號寄存器,即位置信息的低八位
outb(0x3D5, cursorLocation);
}
清屏操作
清屏就是把所有字符用黑底白字的空格填充,然後把光標移動到最前面就行了
void console_clear()
{
uint8_t attribute_byte = (0 << 4) | (15 & 0x0f); //黑底白字
uint16_t blank = 0x20 | (attribute_byte << 8);
int i;
for (i = 0; i < 80 * 25; i++)
{
video_memory[i] = blank;
}
cursor_x = cursor_y = 0;
move_cursor();
}
屏幕滾動
只需要將所有行內容往上移動一格,最後一行填充空格
//屏幕的滾動
static void scroll()
{
int i;
uint8_t attribute_byte = (0 << 4) | (15 & 0x0f); //黑底白字
uint16_t blank = 0x20 | (attribute_byte << 8);
//大於25行就該滾動屏幕了
if (cursor_y >= 25)
{
//米一行向上移動一行
for (i = 0; i < 24 * 80; i++)
{
video_memory[i] = video_memory[i + 80];
}
//最後一行填充空格
for (i = 24 * 80; i < 25 * 80; i++)
video_memory[i] = blank;
//光標向上一行
cursor_y--;
}
}
顯示字符串
注意對換行符等等一些符號的處理
// 屏幕輸出一個字符帶顏色
void console_putc_color(char c, real_color_t back, real_color_t fore)
{
//設置字體顏色
uint8_t back_color = (uint8_t) back;
uint8_t fore_color = (uint8_t) fore;
uint8_t attribute_byte = (back_color << 4) | (fore_color & 0x0f);
uint16_t attirbute = attribute_byte << 8;
// 0x08 是退格鍵的 ASCII 碼
// 0x09 是tab 鍵的 ASCII 碼
if (c == 0x08 && cursor_x)
cursor_x--;
else if (c == 0x09)
cursor_x = (cursor_x + 4) & ~(4 - 1); //即當前位置移動4下後的最高位的1所表示的值
else if (c == '\r')
cursor_x = 0;
else if (c == '\n')
{
cursor_x = 0;
cursor_y++;
} else if (c >= ' ')
{
video_memory[cursor_y * 80 + cursor_x] = c | attirbute;
cursor_x++;
}
//一行滿了換行
if (cursor_x >= 80)
{
cursor_x = 0;
cursor_y++;
}
// 如果需要的話滾動屏幕顯示
scroll();
// 移動硬件的輸入光標
move_cursor();
}
// 屏幕打印一個以 \0 結尾的字符串默認黑底白字
void console_write(char *cstr)
{
while (*cstr)
{
console_putc_color(*cstr++, rc_black, rc_white);
}
}
// 屏幕打印一個以 \0 結尾的字符串帶顏色
void console_write_color(char *cstr, real_color_t back, real_color_t fore)
{
while (*cstr)
{
console_putc_color(*cstr++, back, fore);
}
}
輸出數字
做一下進制轉換當字符串輸出就行了
/ 屏幕輸出一個十六進制的整型數
void console_write_hex(uint32_t n, real_color_t back, real_color_t fore)
{
uint8_t data[8];
int16_t len = 0;
while (n)
{
data[len++] = n % 16;
n /= 16;
}
len--;
console_putc_color('0', back, fore);
console_putc_color('x', back, fore);
while (len >= 0)
{
if (data[len] < 10)
console_putc_color(data[len] + '0', back, fore);
else
console_putc_color(data[len] - 10 + 'A', back, fore);
len--;
}
}
// 屏幕輸出一個十進制的整型數
void console_write_dec(uint32_t n, real_color_t back, real_color_t fore)
{
uint8_t data[10];
int16_t len = 0;
while (n)
{
data[len++] = n % 10;
n /= 10;
}
len--;
while (len >= 0)
{
console_putc_color(data[len] + '0', back, fore);
len--;
}
}
測試
修改一下init / entry.c
#include "console.h"
int kern_entry()
{
console_clear();
console_write_color("Hello, OS kernel!\n", rc_black, rc_green);
console_write_dec(128, rc_black, rc_green);
console_putc_color('\n', rc_black, rc_green);
console_write_hex(128, rc_black, rc_green);
return 0;
}
make
make qemu
最終的成果