You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

构建操作系统时如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 22:47:53