构建操作系统时如何用C语言向640x480(模式0x12)屏幕写文本?
在VGA模式0x12(640x480 16色)下用C实现文本输出
核心原理回顾
VGA模式0x12采用平面显存架构:
- 显存起始地址:
0xA0000(需确保32位引导加载器已将该区域映射到线性地址空间) - 4个颜色位面(Plane 0-3),每个位面对应16色中的1个二进制位,组合后实现16种颜色输出
- 像素修改需通过端口指定操作的位面,索引端口
0x3C4、数据端口0x3C5用于位面选择
第一步:实现端口读写函数
32位保护模式下C无法直接操作IO端口,需内嵌汇编实现:
// 写8位数据到指定端口 static inline void outb(uint16_t port, uint8_t data) { __asm__ __volatile__ ("outb %0, %1" : : "a"(data), "Nd"(port)); } // 从指定端口读取8位数据 static inline uint8_t inb(uint16_t port) { uint8_t ret; __asm__ __volatile__ ("inb %1, %0" : "=a"(ret) : "Nd"(port)); return ret; }
第二步:实现位面选择函数
通过端口设置位面掩码,指定当前要操作的位面:
// 选择要操作的位面,bitmask为对应位设1(如选位面0传0x01,选所有位面传0x0F) void select_plane(uint8_t bitmask) { outb(0x3C4, 0x02); // 索引0x02对应位面掩码寄存器 outb(0x3C5, bitmask); }
第三步:定义字符点阵字库
以8x16 ASCII字符为例,先定义基础字符的点阵(可扩展为完整字库):
// 每个字符占16字节,每字节对应一行的8个像素(1=亮,0=暗) const uint8_t font_8x16[] = { // 'A'的点阵 0x00, 0x18, 0x24, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 'B'的点阵 0x00, 0x7C, 0x42, 0x42, 0x7C, 0x42, 0x42, 0x42, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 'H'的点阵 0x00, 0x42, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // 获取字符对应的点阵起始地址 const uint8_t* get_char_bitmap(char c) { if (c < 'A' || c > 'H') return NULL; // 暂时支持A-H,可扩展范围 return &font_8x16[(c - 'A') * 16]; }
第四步:实现单个字符绘制函数
计算字符在屏幕的显存位置,逐行逐位写入对应位面:
// 在(x,y)位置绘制字符c,颜色为color(0-15对应VGA 16色) void draw_char(int x, int y, char c, uint8_t color) { const uint8_t* bitmap = get_char_bitmap(c); if (!bitmap) return; uint8_t* vram = (uint8_t*)0xA0000; // 显存线性地址 for (int row = 0; row < 16; row++) { uint8_t row_data = bitmap[row]; // 处理当前行的8个像素 for (int col = 0; col < 8; col++) { if (row_data & (0x80 >> col)) { // 当前像素需要点亮 int pixel_x = x + col; int pixel_y = y + row; if (pixel_x >= 640 || pixel_y >= 480) continue; // 超出屏幕范围跳过 // 计算像素对应的显存字节偏移和位偏移 uint32_t byte_offset = (pixel_y * (640 / 8)) + (pixel_x / 8); uint8_t bit_offset = 7 - (pixel_x % 8); // 根据颜色的4个位,设置对应位面的像素位 for (int plane = 0; plane < 4; plane++) { select_plane(1 << plane); if (color & (1 << plane)) { vram[byte_offset] |= (1 << bit_offset); } else { vram[byte_offset] &= ~(1 << bit_offset); } } } } } }
第五步:实现字符串输出函数
循环调用字符绘制,支持自动换行:
void print_string(int x, int y, const char* str, uint8_t color) { int current_x = x; int current_y = y; while (*str) { if (*str == '\n') { current_x = x; current_y += 16; // 字符高度16,换行下移16像素 str++; continue; } draw_char(current_x, current_y, *str, color); current_x += 8; // 字符宽度8,右移8像素 if (current_x >= 640) { // 超出屏幕宽度自动换行 current_x = x; current_y += 16; } str++; } }
测试代码
在内核入口函数中调用即可:
void kernel_main() { // 清除屏幕(填充黑色) select_plane(0x0F); // 选中所有4个位面 uint8_t* vram = (uint8_t*)0xA0000; for (int i = 0; i < 640*480/8; i++) { vram[i] = 0x00; } // 输出测试字符串,颜色为白色(0x0F)、黄色(0x0E) print_string(20, 20, "Hello, My OS!", 0x0F); print_string(20, 40, "A B H", 0x0E); }
关键注意事项
- 确保引导加载器已将
0xA0000起始的显存区域正确映射到32位线性地址 - 操作完位面后建议恢复掩码为
0x0F(选中所有位面),避免后续操作异常 - VGA 16色对应值:0=黑、1=蓝、2=绿、3=青、4=红、5=品红、6=棕、7=浅灰、8=深灰、9=亮蓝、10=亮绿、11=亮青、12=亮红、13=亮品红、14=黄、15=白
内容的提问来源于stack exchange,提问作者joshjkk
相关产品推荐
相关产品推荐

