为了在屏幕上显示颜色,必须通过桢缓冲存储器完成。桢缓冲存储器(Frame Buffer),简称桢缓存或桢存。它是屏幕显示画面的一个内存映像,桢缓存的每个存储单元对应屏幕的一个像素,整个桢缓存对应一幅图像。桢缓存的特点是可对每个像素点进行操作,不仅可以借助它在屏幕上画出屏幕上色彩,还可以在屏幕上用像素点绘制文字以及图片。

此前设置说显示芯片的显示模式(模式号:0x180,分辨率:1400*900,颜色深度:32bit),而且内核执行头程序还将桢缓存的物理基地址映射到线性地址0xffff8000000000000xa00000处。

屏幕上显示色彩

桢缓存格式:一个像素点能够显示的颜色值位宽。Loader引导加载程序设置的显示模式可支持32位颜色深度的像素点,其中0~7位代表蓝色,8~15位代表吝啬,16~23位代表红色,24~31位是保留位。

如果想设置屏幕上某个像素点的颜色,必须知道这个点在屏幕上的位置,并计算处该点距离屏幕原点的偏移值。屏幕坐标位于左上角。

Screenshot_20220717_202647

显示屏幕色带如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void start_kernel(void) {
int *addr = (int *)0xffff800000a00000;
int i;

for (i = 0; i < 1440 * 20; i++) {
*((char *)addr + 0) = (char)0x00;
*((char *)addr + 1) = (char)0x00;
*((char *)addr + 2) = (char)0xff;
*((char *)addr + 3) = (char)0x00;
addr += 1;
}
for (i = 0; i < 1440 * 20; i++) {
*((char *)addr + 0) = (char)0x00;
*((char *)addr + 1) = (char)0xff;
*((char *)addr + 2) = (char)0x00;
*((char *)addr + 3) = (char)0x00;
addr += 1;
}
for (i = 0; i < 1440 * 20; i++) {
*((char *)addr + 0) = (char)0xff;
*((char *)addr + 1) = (char)0x00;
*((char *)addr + 2) = (char)0x00;
*((char *)addr + 3) = (char)0x00;
addr += 1;
}
for (i = 0; i < 1440 * 20; i++) {
*((char *)addr + 0) = (char)0xff;
*((char *)addr + 1) = (char)0xff;
*((char *)addr + 2) = (char)0xff;
*((char *)addr + 3) = (char)0x00;
addr += 1;
}
while (1) {
;
}
}

桢缓存区被映射的线性地址是0xffff800000a00000,在显示模式的过程中,有个寄存器位可以在设置显示模式后清除屏幕上的数据。Loader引导加载程序已将该寄存器位置位,所以先前在屏幕上显示的信息已经被清除。

在屏幕上显示LOG

在一个固定像素方块内用像素点画出字符,即可实现屏幕上的字符显示功能。

ASCII字符库

ASCII字符集共有256个字符,其中包括字母、数字、符号和一些非显示信息。当前只显示一些常用的显示字符。

Screenshot_20220717_204028

数字0和一个8*16的像素点矩阵,像素点矩阵中的黑色像素点在屏幕上映射组成了数字0,它们是数字0的字体颜色。只要根据像素点矩阵的映射原理,计算出每行的16进制数值,再将这16行数值组合起来就构成了字符像素位图。一行刚好一个字节,所以只需要保留16字节的位图即可。

显示彩色字符

实现color_printk函数前,需要先准备一个用于屏幕信息的结构体struct position。该结构体记录这当前屏幕的分辨率、字符光标所在位置、字符像素矩阵尺寸、桢缓冲区起始地址和桢缓冲区容量大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct position {
int XResolution; // 配置屏幕的分辨率
int YResolution;

int XPosition; // 光标所在列
int YPosition; // 光标所在行

int XCharSize; //一个字符的宽度
int YCharSize; // 一个字符的高度

unsigned int *FB_addr; //帧缓冲区起始线性地址
unsigned long FB_length; //每个像素点需要4字节的值进行控制
};

初始化全局屏幕信息描述:

kernel/main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
struct position pos;
void start_kernel(void) {
int *addr = (int *)0xffff800000a00000;
int i;
// 配置屏幕的分辨率/光标位置/字符矩阵的尺寸/帧缓冲区起始线性地址/缓冲区长度
pos.x_resolution = 1440; // 从左到右横向1440
pos.y_resolution = 900; // 从上到下纵向900
pos.x_position = 0; // 光标所在列
pos.y_position = 0; // 光标所在行
pos.x_char_size = 8; //一个字符的宽度
pos.y_char_size = 16; // 一个字符的高度
pos.fb_addr = (unsigned int *)0xffff800000a00000; //帧缓冲区起始线性地址
pos.fb_length = (pos.x_resolution * pos.y_resolution * 4); //每个像素点需要4字节的值进行控制

for (i = 0; i < 1440 * 20; i++) {
*((char *)addr + 0) = (char)0x00;
*((char *)addr + 1) = (char)0x00;
*((char *)addr + 2) = (char)0xff;
*((char *)addr + 3) = (char)0x00;
addr += 1;
}
for (i = 0; i < 1440 * 20; i++) {
*((char *)addr + 0) = (char)0x00;
*((char *)addr + 1) = (char)0xff;
*((char *)addr + 2) = (char)0x00;
*((char *)addr + 3) = (char)0x00;
addr += 1;
}
for (i = 0; i < 1440 * 20; i++) {
*((char *)addr + 0) = (char)0xff;
*((char *)addr + 1) = (char)0x00;
*((char *)addr + 2) = (char)0x00;
*((char *)addr + 3) = (char)0x00;
addr += 1;
}
for (i = 0; i < 1440 * 20; i++) {
*((char *)addr + 0) = (char)0xff;
*((char *)addr + 1) = (char)0xff;
*((char *)addr + 2) = (char)0xff;
*((char *)addr + 3) = (char)0x00;
addr += 1;
}
color_printk(YELLOW, BLACK, "Hello\t\t World!\n");
color_printk(GREEN, BLACK, "Hello %c World!\n", 'h');
while (1) {
;
}
}

打印字符函数实现:

kernel/printk.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* print character of paramters required
* @param fb 帧缓存线性地址
* @param x_size 行分辨率
* @param x 列像素点位置
* @param y 行像素点位置
* @param fb_color 颜色
* @param bk_color 背景
* @param font 字符位图
*/
void putchar(unsigned int *fb, int x_size, int x, int y, unsigned int fb_color,
unsigned bk_color, unsigned char font) {
int i = 0, j = 0;
unsigned int *addr = NULL; //指向32位内存空间的指针,用于写入一个像素点的颜色
unsigned char *fontp = NULL;
fontp = font_ascii[font]; //字符位图中的一行
int testval; //用来测试比特位是否有效(是否要填充)
//这段程序使用到了帧缓存区首地址,将该地址加上字符首像素位置(首像素位置是指字符像素矩阵左上角第一个像素点)的偏移( Xsize * ( y + i ) + x ),可得到待显示字符矩阵的起始线性地址
for (i = 0; i < 16; i++) {
addr = fb + x_size * (y + i) + x;
testval = 0x100; //256=>1 0000 0000
//for循环从字符首像素地址开始,将字体颜色和背景色的数值按字符位图的描绘填充到相应的线性地址空间中,每行8个像素点,每个像素点写入32比特位控制颜色
for (j = 0; j < 8; j++) {
testval = testval >> 1; //右移1位
if (*fontp & testval) { //如果对应的位是1则填充字体颜色,如果字体有颜色,则该位置将不再填充背景
*addr = fb_color;
} else {
*addr = bk_color; //对应的位为0填充背景颜色
}
addr++; //填充完一位自增到下一个位置,addr是int类型32位
}
fontp++; //字符位图下一个char序列
}
}

只能将数值字母转换成整数值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define is_digit(c) ((c) >= '0' && (c) <= '9')

/**
* convert numeric letters to integer values
* @param s string address of numeric
* @return number after conversion
*/
int skip_atoi(const char **s) {
int i = 0;
while (is_digit(**s)) { //判断是否是数值字母
i = i * 10 + *((*s)++) - '0'; // 将当前字符转换成数值
}
return i;
}

字符串长度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* get string length
* @param str the point of string
* @return string length
*/
//repne不相等则重复,重复前判断ecx是否为零,不为零则减1,scasb查询di中是否有al中的字符串结束字符0,如果有则退出循环,将ecx取反后减去1得到字符串长度
static inline int strlen(char *str) {
register int __res;
__asm__ __volatile__("cld \n\t"
"repne \n\t"
"scasb \n\t"
"notl %0 \n\t"
"decl %0 \n\t"
: "=c"(__res)
: "D"(str), "a"(0), "0"(0xffffffff)); //输入约束:寄存器约束D令String的首地址约束在edi,立即数约束令al=0,通用约束0令ecx=0xffff ffff
return __res; //ecx=0xffff ffff,al=0,edi指向String首地址,方向从低到高
}

strlen先将AL寄存器赋值为0,随后借助scasb汇编指令逐字节扫描字符串,每次扫描都会与AL寄存器进行对比,并根据对比结果置位相应的标志位,如果扫描的数值与AL寄存器的数值相等,ZF标志位被置位。repne对一直重复执行scasb指令,知道ecx寄存器递减为0或ZF标志位被置位。又因为ecx寄存器的初始值是负值(0xffffffff),repne指令执行结束后,ecx寄存器依然是负值(ecx寄存器在函数执行过程中递减,使用负值可统计出扫描次数),对ECX寄存器取反减1后得到字符串长度。

将长整型变量值转换成指定进制规格的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* converts the value of a long integer variable to a string with a spacified
* hexadecimal spacifition
*
* @param str buffer of save parse result
* @param num convert variable
* @param base base number
* @param field_width
* @param precision dipaly precision
* @param flags
* @return
*/
// 不管number函数将整数值转换成大写还是小写字母,它最高支持36进制的数值转换
static char *number(char *str, long num, int base, int field_width,
int precision, int flags) {
char c, sign, tmp[50]; // c用来存放填充的进制前缀后前导字符,sign正负
const char *digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; //默认大写共36个符号(36进制)
int i;
if (base < 2 || base > 36) { //不支持小于2或大于36的进制
return NULL;
}
if (flags & SMALL) { // 定义了使用小写字母
digits = "0123456789abcdefghijklmnopqrstuvwxyz";
}
if (flags & LEFT) { // 对齐后字符放在左边则字符前面将不填充0,而在后面填充空格,填充0将破坏数据大小,则令ZEROPAD位域为0表示不填充0
flags &= ~ZEROPAD; // 显示的字符前面填充0取代空格,对于所有的数字格式用前导零而不是空格填充字段宽度,如果出现-标志或者指定了精度(对于整数)则忽略该标志
}
c = (flags & ZEROPAD) ? '0' : ' '; //c用来存放填充的进制前缀后前导字符,如果左对齐,则前面已经令ZEROPAD位域为0,这个表达式c=空格,如果右对齐且ZEROPAD位域为1,这个表达式c=0,表示显示的字符前面填充0取代空格,因为右对齐的话字符显示在右边,前导用0填充不会破坏数据
sign = 0;
if (flags & SIGN && num < 0) { //如果是有符号数且获取到的数小于0
sign = '-';
num = -num; //将负数转换成一般正数
} else { //如果是无符号数或者获取到的是正数再根据对齐方式决定前导填充什么符号
sign = (flags & PLUS) ? '+' : ((flags & SPACE) ? ' ' : 0);
}
if (sign) { //如果要在正数前面补充显示的是+号或者空格,这个符号占用一个宽度,则数据域宽度减去1,如果是0符号则留给后面的来填充,此次不管
field_width--;
}
if (flags & SPECIAL) {
if (base == 16) {
field_width -= 2; // 十六进制的0x占用两个宽度
} else if (base == 8) {
field_width--; // 八进制的0占用一个宽度
}
}
i = 0;
if (num == 0) { //如果取出的数等于0,将字符串结束符'0'存入临时数组tmp第一位中
tmp[i++] = '0';
} else {
while (num != 0) { //如果取出的数不等于0则除去进制直到num=0
tmp[i++] = digits[do_div(num, base)]; //将整数值转换成字符串(按数值倒序排列),然后再将tmp数组中的字符倒序插入到显示缓冲区,do_div(num,base)的值等于num除以base的余数,余数部分即是digits数组的下标索引值
}
}
if (i > precision) { //进制转换完的数值的长度大于精度,则精度等于进制转换完的数值的长度,比如%5.3限定,1000转十进制=>1000,i=4大于精度3,精度变成4,再由剩下的数据域宽度 = 剩下的数据域宽度 - 精度,得到剩下的数据域宽度等于1,也就是要填充的位数1
precision = i; //否则精度不变,比如%5.3限定,10转十进制=>10,i=2小于精度3,精度还是3,再由剩下的数据域宽度 = 剩下的数据域宽度 - 精度,得到剩下的数据域宽度等于2,也就是要填充的位数2
}
field_width -= precision; //剩下的数据域宽度 = 数据域宽度 - 精度,至此得到数据域宽度,也就是要填充的位数
if (!(flags & (ZEROPAD + LEFT))) { //如果显示的字符前面填充的是空格且对齐后字符放在右边才可以在前导填充空格,否则放到后边if (!(flags & LEFT))再填充,因为如果对齐后字符放在左边还有进制前缀未填充
while (field_width-- > 0) {
*str++ = ' ';
}
}
if (sign) { //填充前面决定的前导符号,空格或者+号,如果既不是空格也不是+号,而是0就不填充避免破坏数据,前面的if(sign)也没有执行,至此已经填充完毕(包括0 + 空格)
*str++ = sign;
}
if (flags & SPECIAL) {
if (base == 8) {
*str++ = '0'; //八进制前缀0
} else if (base == 16) {
*str++ = '0'; //十六进制前缀0x或0X
*str++ = digits[33];
}
}
if (!(flags & LEFT)) { //对齐完字符在右边的情况可以用前导0填充而不破坏数据
while (field_width-- > 0) { //c用来存放填充的进制前缀后前导字符,如果左对齐,则前面已经令ZEROPAD位域为0,这个表达式c=空格,如果右对齐且ZEROPAD位域为1,这个表达式c=0,表示显示的字符前面填充0取代空格,因为右对齐的话字符显示在右边,前导用0填充不会破坏数据
*str++ = c;
}
}
//比如%5.3限定,10转十进制=>10,i=2小于精度3,精度还是3,再由剩下的数据域宽度 = 剩下的数据域宽度 - 精度,得到剩下的数据域宽度等于2,也就是要填充的位数2,至此要填充进制前缀后的数值部分了
while (i < precision--) { //i等于除去进制得到几位,precision精度
*str++ = '0';
}
while (i-- > 0) { //填充完前缀后的前导符,逆序复制有效数值过去
*str++ = tmp[i];
}
while (field_width-- > 0) { //前面会改变field_width数据的条件是(填充空格且右对齐/填充0且右对齐),而此处的条件是剩下的条件左对齐,则填充空格
*str++ = ' ';
}
return str;
}

用于解析color_printk函数所提供的格式化字符串及其参数,vsprintf函数会将格式化后(就像汇编语言用db定义的一串字符那样,可以直接搬运到显存输出那样)的字符串结果保存到一个4096B的缓冲区中buf,并返回字符串长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
/**
* parse the formatted string and paramters provided by color_printk function
* @param buf buffer of save parse result
* @param fmt the format
* @param args the paramters
* @return string length
*/
int vsprintf(char *buf, const char *fmt, va_list args) {
char *str, *s;
int flags;
int field_width; //宽度
int precision;
int len, i; //len用在格式符s,i用在临时循环

int qualifier; //'h', 'l', 'L' or 'Z' for integer fields//基本上只实现了l

for (str = buf; *fmt; fmt++) { //str直接指向缓冲区buf
if (*fmt != '%') { //该循环体会逐个解析字符串,如果字符不为%就认为它是个可显示字符,直接将其存入缓冲区buf中,否则进一步解析其后的字符串格式
*str++ = *fmt;
continue;
}
flags = 0;//按照字符串规定,符号%后面可接 - + 空格 # 0等格式符,如果下一个字符是上述格式符,则设置标志变量flags的标志位(标志位定义在printk.h),随后计算出数据区域的宽度
repeat:
fmt++; //后续的自增都是指向下一地址
switch (*fmt) {
case '-':
flags |= LEFT; //对齐后字符放在左边
goto repeat;
case '+':
flags |= PLUS; //有符号的值若为正则显示带加号的符号,若为负则显示带减号的符号
goto repeat;
case ' ':
flags |= SPACE;//有符号的值若为正则显示时带前导空格但是不显示符号,若为负则带减号符号,+标志会覆盖空格标志
goto repeat;
case '#':
flags |= SPECIAL; //0x
goto repeat;
case '0':
flags |= ZEROPAD; //显示的字符前面填充0取代空格,对于所有的数字格式用前导零而不是空格填充字段宽度。如果出现-标志或者指定了精度(对于整数)则忽略该标志
goto repeat;
}
//这部分程序可提取出后续字符串中的数字,并将其转化为数值以表示数据区域的宽度,如果下一个字符不是数字而是字符*,那么数据区域的宽度将由可变参数提供,根据可变参数值亦可判断数据区域的对齐显示方式(左/右对齐)
// get fied with
field_width = -1; //不限定宽度默认为-1
if (is_digit(*fmt)) {
field_width = skip_atoi(&fmt); //得到数据域的宽度
} else if (*fmt == '*') { //如果下一个字符不是数字而是字符*,那么数据区域的宽度将由可变参数提供,例如printf("dnumber = %*.*f\n", width, precision, dnumber);
fmt++; //有限定*号则指向下一位
field_width = va_arg(args, int); //通过可变参数取得数据域宽度
if (field_width < 0) { //数据域宽度是负数
field_width = -field_width; //数据域宽度必须是正数,负负得正
flags |= LEFT; //字符放在左边
}
}
//获取数据区宽度后,下一步提取出显示数据的精度,如果数据区域的宽度后面跟有字符.,说明其后的数值是显示数据的精度,这里采用与计算数据区域宽度相同的方法计算出显示数据的精度,随后还要获取显示数据的规格
precision = -1; //不限定精度默认为-1,如果有写.但没有后续精度限定则前两个if都不执行,精度设置为0
if (*fmt == '.') {
fmt++; //有限定小数点则指向下一位
if (is_digit(*fmt)) {
precision = skip_atoi(&fmt); //int skip_atoi(const char **s)//将数值字母转换成整数值
} else if (*fmt == '*') {
fmt++;
field_width = va_arg(args, int); //通过可变参数取得数据域精度
}
if (field_width < 0) { //如果精度小于0则精度设置为0
precision = 0;
}
}
//获取显示数据的规格,比如%ld格式化字符串中的字母l,就表示显示数据的规格是长整型
qualifier = -1; //不限定规格默认为-1
if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L' || *fmt == 'Z') {
qualifier = *fmt; //获得数据规格
fmt++; //有限定规格则指向下一位
}
//经过逐个格式符的解析,数据区域的宽度和精度等信息皆已获取,现在将遵照这些信息把可变参数格式化成字符串,并存入buf缓冲区,下面进入可变参数的字符串转化过程,目前支持的格式符有c s o p x X d i u n %等
switch (*fmt) {
// char
case 'c':
if (!(flags & LEFT)) { //如果对齐后字符要放在右边
// on the right
while (--field_width > 0) { //比如宽度是2,那么将会填充1个空格凑成两个宽度
*str++ = ' '; //补齐前面得空格
}
}
*str++ = (unsigned char)va_arg(args, int); //从可变参数列表取出一个字符结合填充到缓冲区
while (--field_width > 0) { //如果对齐后字符要放在左边,补齐后面的空格
*str++ = ' ';
}
break;
// string
case 's': //整个显示过程会把字符串的长度与显示精度进行比对,根据数据区的宽度和精度等信息截取待显示字符串的长度并补齐空格符,涉及到内核通用库函数strlen
s = va_arg(args, char *);
if (!s) { //如果字符串只有结束符
*s = '\0';
}
len = strlen(s); //字符串不是只有一个结束符则获取长度
if (precision < 0) { //如果不限定精度默认为-1,改写成字符串长度
precision = len;
} else if (len > precision) { //如果限定了精度并且len大于精度则精度由len决定
len = precision;
}

if (!(flags & LEFT)) { //如果对齐后字符要放在右边则先填充空格
while (len < field_width--) {
*str++ = ' ';
}
}
for (i = 0; i < len; i++) { //将字符串依次复制到缓冲区buf
// copy the string to the buffer
*str++ = *s++;
}
while (len < field_width--) { //如果对齐后字符要放在左边则最后填充空格
*str++ = ' ';
}
break;
// octal number
case 'o':
if (qualifier == 'l') { //如果规格是长整型
str = number(str, va_arg(args, unsigned long), 8, field_width,
precision, flags);
} else {
str = number(str, va_arg(args, unsigned int), 8, field_width, precision,
flags);
}
break;
// address
case 'p':
if (field_width == -1) { //如果未限定数据域宽度
field_width = 2 * sizeof(void *);
flags |= ZEROPAD; //显示的字符前面填充0取代空格,对于所有的数字格式用前导零而不是空格填充字段宽度,如果出现-标志或者指定了精度(对于整数)则忽略该标志
}
str = number(str, (unsigned long)va_arg(args, void *), 16, field_width,
precision, flags);
break;
case 'x': //十六进制的处理过程x设置为小写,其余跟X一样
flags |= SMALL;
case 'X':
if (qualifier == 'l') { //如果规格是长整型
str = number(str, va_arg(args, unsigned long), 16, field_width,
precision, flags);
} else {
str = number(str, va_arg(args, unsigned int), 16, field_width,
precision, flags);
}
break;
case 'd': //十进制
case 'i': //有符号处理过程跟无符号差别在于flags的SIGN域
flags |= SIGN;
case 'u':
if (qualifier == 'l') { //如果规格是长整型
str = number(str, va_arg(args, unsigned long), 10, field_width,
precision, flags);
} else {
str = number(str, va_arg(args, unsigned int), 10, field_width,
precision, flags);
}
break;
//函数的最后一部分代码负责格式化字符串的扫尾工作
case 'n': //格式符%n的功能是把目前已格式化的字符串长度返回给函数的调用者,意思是把刚刚接收的数据的字符个数赋给对应的变量
if (qualifier == 'l') { //如果规格是长整型
{
long *ip = va_arg(args, long *); //对应的变量,CLanguageMINEOS项目里有标准库printf的测试程序
*ip = (str - buf);
} else {
int *ip = va_arg(args, int *);
*ip = (str - buf);
}
break;
// if appear %%, the first % as an escape character, after formatting, only
// one character %
//如果格式化字符串中出现字符%%,则把第一个格式符%视为转义符,经过格式化解析后,最终只显示一个字符%
case '%':
*str++ = '%';
break;
// if not support, direct output without any processing
//如果在格式符解析过程中出现任何不支持的格式符,比如f,则不做任何处理,直接将其视为字符串输出到buf缓冲区
default:
*str++ = '%';
if (*fmt) {
*str++ = *fmt;
} else {
fmt--;
}
//碰到字符串结束符fmt指针回退并break,退出外层循环后又因为碰到结束符,for循环结束,整个函数结束
break;
}
}
*str = '\0';
return str - buf;
}

color_printk函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
int color_printk(unsigned int fr_color, unsigned int bk_color, const char *fmt,
...) {
int i = 0;
int count = 0;
int line = 0;
va_list args;
va_start(args, fmt);
i = vsprintf(buf, fmt, args);
va_end(args);
//缓冲区里存放的是已经格式化后的字符串,接下来color_printk开始检索buf缓冲区里的格式化字符串,从中找出\n \b \t等转义字符,并在打印过程中解析这些转义字符
//通过for语句逐个字符检测格式化后的字符串,<运算符优先级高于逻辑或||,i是buf缓冲区的字符串的长度
for (count = 0; count < i || line; count++) { //字符串还未输出完或者当前光标距下一个制表位还需要填充空格
if (line > 0) { //line保存当前光标距下一个制表位需要填充的空格符数量,如果要填充的空格数大于0
count--; //因为这里处理的是填充空格符,但是会影响到循环变量count,此处将count自减避免影响
goto label_tab;
}
if ((unsigned char)*(buf + count) == '\n') { //如果发现某个待显示字符是\n转移字符,则将光标行数加1,列数设置为0,否则判断待显示字符是否为\b转义符
pos.y_position++; //将光标行数加1
pos.x_position = 0; //列数设置为0
} else if ((unsigned char)*(buf + count) == '\b') { //如果确定待显示字符是\b转义字符,那么调整列位置并调用putchar函数打印空格符覆盖之前的字符,如果既不是\n也不是\b,则继续判断其是否为\t转义字符
{
pos.x_position--; //列数减去1
if (pos.x_position < 0) { //列数小于0(本来就在第0列),则回退到上一行的最后一列
pos.x_position = pos.x_resolution / pos.x_char_size - 1; //列数 = 从左到右横向1440 / 从左到右横向8 - 1 =>最后一列
pos.y_position--;
if (pos.y_position < 0) {
pos.y_position = pos.y_position * pos.x_char_size - 1; //行数 = 从上到下纵向900 / 从上到下纵向16 - 1 =>最后一行
}
}
//因为这里在给putchar传递光标位置时是乘上了位图尺寸的,所以上面的不可以再乘
putchar(pos.fb_addr, pos.x_resolution, pos.x_position * pos.x_char_size,
pos.y_position * pos.y_char_size, fr_color, bk_color, ' ');
} else if ((unsigned char)*(buf + count) == '\t') { //如果确定待显示字符是\t转义字符,则计算当前光标距下一个制表位需要填充的空格符数量,将计算结果保存到局部变量line中,再结合for循环和if判断,把显示位置调整到下一个制表位,并使用空格填补调整过程中占用的字符显示空间
//8表示一个制表位占用8个显示字符
line = ((pos.x_position + 8) & ~(8 - 1)) - pos.x_position;
//需要填充的空格符数量 = ((列数 + 8) & ~(8 - 1)) - 列数),比如现在光标在(2, 2),则需要填充的空格符数量 = ((2 + 8) & ~(8 - 1)) - 2) = (8 - 2) = 6
label_tab:
line--;
putchar(pos.fb_addr, pos.x_resolution, pos.x_position * pos.x_char_size,
pos.y_position * pos.y_char_size, fr_color, bk_color, ' ');
pos.x_position++;
} else { //排除待显示字符是\n\b\t转义字符之后,那么它就是一个普通字符,使用putchar函数将字符打印在屏幕上,参数帧缓存线性地址/行分辨率/屏幕列像素点位置/屏幕行像素点位置/字体颜色/字体背景色/字符位图
putchar(pos.fb_addr, pos.x_resolution, pos.x_position * pos.x_char_size,
pos.y_position * pos.y_char_size, fr_color, bk_color,
(unsigned char)*(buf + count));
pos.x_position++;
}
//收尾工作:字符显示结束还要为下次字符显示做准备,更新当前字符的显示位置,此处的字符显示位置可理解为光标位置,下面这段程序负责调整光标的列位置和行位置
if (pos.x_position >= (pos.x_resolution / pos.x_char_size)) { //列数 >= (1440 / 8),也就是最后一列了,则换到下一行并令列数=0,相当于回车换行
pos.y_position++;
pos.x_position = 0;
}
if (pos.y_position >= (pos.y_resolution / pos.y_char_size)) { //行数 >= (900 / 16),也就是最后一行了,则行数=0,列数不变,相当于换行
pos.y_position = 0;
}
}
return i;
}