在内核启动的最早期(在此阶段,内存管理、中断甚至设备树解析都尚未完成),为了能够打印调试信息,SimpleKernel 提供了 Early Console 机制。
通过 CMake 预设变量 SIMPLEKERNEL_EARLY_CONSOLE 启用。该变量定义了串口控制器的物理基地址。
- RISC-V 64: 0x10000000 (QEMU virt machine)
- AArch64: 0x9000000 (QEMU virt machine)
利用 C++ 静态全局对象的构造函数在 main 函数之前执行的特性,EarlyConsole 类的构造函数会初始化一个最小可用的串口驱动,并设置 sk_putchar 函数指针,从而使得 klog 等日志函数能够立即工作。
struct EarlyConsole {
EarlyConsole() {
// 初始化串口
// 设置 console_putchar
}
};
// 静态实例,其构造函数会在 main 之前运行
EarlyConsole early_console;此阶段的串口驱动通常采用轮询模式,不依赖中断。
RISC-V64 架构使用 OpenSBI 固件提供的调试控制台扩展 (Debug Console Extension) 进行调试输出。这种方式通过 SBI 调用直接与 Machine 模式固件通信,无需直接操作硬件串口。
基于 RISC-V SBI 标准的 Debug Console Extension (EID #0x4442434E "DBCN"):
// SBI 扩展ID和功能ID
#define SBI_EXT_DBCN 0x4442434E
#define SBI_EXT_DEBUG_CONSOLE_WRITE_BYTE 0x2
/**
* @brief Console Write Byte (FID #2)
* @param byte 要写入的字节
* @return sbiret.error SBI_SUCCESS, SBI_ERR_DENIED, SBI_ERR_FAILED
* @return sbiret.value 设置为0
*/
struct sbiret sbi_debug_console_write_byte(uint8_t byte);在 src/arch/riscv64/arch_main.cpp 中直接调用 SBI 接口:
// 基本输出实现
extern "C" void sk_putchar(int c, [[maybe_unused]] void *ctx) {
sbi_debug_console_write_byte(c);
}SBI 调用通过 ecall 指令实现,具体在 3rd/opensbi_interface/src/opensbi_interface.c:
struct sbiret sbi_debug_console_write_byte(uint8_t byte) {
return ecall(byte, 0, 0, 0, 0, 0,
SBI_EXT_DEBUG_CONSOLE_WRITE_BYTE, SBI_EXT_DBCN);
}
// 通用SBI调用函数
static inline struct sbiret ecall(unsigned long arg0, unsigned long arg1,
unsigned long arg2, unsigned long arg3,
unsigned long arg4, unsigned long arg5,
unsigned long fid, unsigned long eid) {
struct sbiret ret;
register unsigned long a0 asm("a0") = arg0;
register unsigned long a1 asm("a1") = arg1;
register unsigned long a2 asm("a2") = arg2;
register unsigned long a3 asm("a3") = arg3;
register unsigned long a4 asm("a4") = arg4;
register unsigned long a5 asm("a5") = arg5;
register unsigned long a6 asm("a6") = fid;
register unsigned long a7 asm("a7") = eid;
asm volatile("ecall"
: "+r"(a0), "+r"(a1)
: "r"(a2), "r"(a3), "r"(a4), "r"(a5), "r"(a6), "r"(a7)
: "memory");
ret.error = a0;
ret.value = a1;
return ret;
}- Supervisor 模式:内核运行在 S 模式
- Machine 模式:OpenSBI 固件运行在 M 模式
- 调用流程:S 模式 →
ecall→ M 模式 → 串口硬件
RISC-V64 的调试输出无需额外初始化,因为:
- 固件支持:OpenSBI 固件在启动时已初始化串口
- 标准接口:通过标准 SBI 调用访问
- 自动配置:固件根据设备树自动配置串口参数
// 调试控制台扩展支持的功能
#define SBI_EXT_DEBUG_CONSOLE_WRITE 0x0 // 批量写入
#define SBI_EXT_DEBUG_CONSOLE_READ 0x1 // 批量读取
#define SBI_EXT_DEBUG_CONSOLE_WRITE_BYTE 0x2 // 单字节写入
// 错误码
enum {
SBI_SUCCESS = 0,
SBI_ERR_FAILED = -1,
SBI_ERR_NOT_SUPPORTED = -2,
SBI_ERR_INVALID_PARAM = -3,
SBI_ERR_DENIED = -4,
SBI_ERR_INVALID_ADDRESS = -5
};- 标准化:基于 RISC-V SBI 标准
- 简洁性:无需硬件串口驱动
- 可移植性:适用于不同 RISC-V 实现
- 固件管理:串口配置由固件处理
- 安全性:通过固件统一管理硬件访问
// 直接SBI调用
auto ret = sbi_debug_console_write_byte('H');
if (ret.error != SBI_SUCCESS) {
// 处理错误
}
// 通过内核日志输出(会调用sk_putchar)
klog::Info("Hello RISC-V64 Debug Output\n");AArch64 架构使用 ARM PrimeCell UART (PL011) 控制器进行调试输出。PL011 是 ARM 设计的标准 UART 控制器,广泛应用于 ARM 系统中,支持完整的 UART 功能和中断处理。
位于 src/driver/pl011/ 目录,提供完整的 PL011 UART 驱动:
/**
* @brief PL011 串口驱动
* @see https://developer.arm.com/documentation/ddi0183/g/
*/
class Pl011 {
public:
explicit Pl011(uint64_t dev_addr, uint64_t clock = 0, uint64_t baud_rate = 0);
void PutChar(uint8_t c);
private:
// 寄存器偏移定义
static constexpr uint32_t kRegDR = 0x00; // 数据寄存器
static constexpr uint32_t kRegRSRECR = 0x04; // 接收状态/错误清除
static constexpr uint32_t kRegFR = 0x18; // 标志寄存器
static constexpr uint32_t kRegIBRD = 0x24; // 整数波特率分频器
static constexpr uint32_t kRegFBRD = 0x28; // 小数波特率分频器
static constexpr uint32_t kRegLCRH = 0x2C; // 线路控制寄存器
static constexpr uint32_t kRegCR = 0x30; // 控制寄存器
static constexpr uint32_t kRegIMSC = 0x38; // 中断屏蔽设置/清除
// 控制位定义
static constexpr uint32_t kFRTxFIFO = (1 << 5); // 发送FIFO满
static constexpr uint32_t kLCRHWlen8 = (3 << 5); // 8位数据
static constexpr uint32_t kCREnable = (1 << 0); // UART使能
static constexpr uint32_t kCRTxEnable = (1 << 8); // 发送使能
static constexpr uint32_t kCRRxEnable = (1 << 9); // 接收使能
static constexpr uint32_t kIMSCRxim = (1 << 4); // 接收中断屏蔽
};Pl011::Pl011(uint64_t dev_addr, uint64_t clock, uint64_t baud_rate)
: base_addr_(dev_addr), base_clock_(clock), baud_rate_(baud_rate) {
// 清除所有错误
io::Out<uint32_t>(base_addr_ + kRegRSRECR, 0);
// 禁用所有功能
io::Out<uint32_t>(base_addr_ + kRegCR, 0);
// 设置波特率(如果提供了时钟和波特率)
if (baud_rate_ != 0) {
uint32_t divisor = (base_clock_ * 4) / baud_rate_;
io::Out<uint32_t>(base_addr_ + kRegIBRD, divisor >> 6);
io::Out<uint32_t>(base_addr_ + kRegFBRD, divisor & 0x3f);
}
// 配置为8位数据,1个停止位,无奇偶校验,禁用FIFO
io::Out<uint32_t>(base_addr_ + kRegLCRH, kLCRHWlen8);
// 启用接收中断
io::Out<uint32_t>(base_addr_ + kRegIMSC, kIMSCRxim);
// 启用UART和收发功能
io::Out<uint32_t>(base_addr_ + kRegCR, kCREnable | kCRTxEnable | kCRRxEnable);
}void Pl011::PutChar(uint8_t c) {
// 等待发送FIFO有空间或设备被禁用
while (io::In<uint32_t>(base_addr_ + kRegFR) & kFRTxFIFO) { ; }
// 写入数据寄存器
io::Out<uint32_t>(base_addr_ + kRegDR, c);
}在 src/arch/aarch64/arch_main.cpp 中实现:
// 基本输出实现
namespace {
Pl011 *pl011 = nullptr;
}
extern "C" void sk_putchar(int c, [[maybe_unused]] void *ctx) {
if (pl011) {
pl011->PutChar(c);
}
}通过设备树获取串口配置,在 src/include/kernel_fdt.hpp 中实现:
[[nodiscard]] auto GetSerial() const -> std::tuple<uint64_t, size_t, uint32_t> {
// 1. 查找 /chosen 节点
int chosen_offset = fdt_path_offset(fdt_header_, "/chosen");
// 2. 获取 stdout-path 属性
const auto *prop = fdt_get_property(fdt_header_, chosen_offset, "stdout-path", &len);
const char *stdout_path = reinterpret_cast<const char *>(prop->data);
// 3. 解析路径并查找对应的串口节点
int stdout_offset = fdt_path_offset(fdt_header_, path_buffer.data());
// 4. 获取寄存器基址和大小
const auto *reg_prop = fdt_get_property(fdt_header_, stdout_offset, "reg", &len);
uint64_t base = fdt64_to_cpu(reinterpret_cast<const uint64_t *>(reg_prop->data)[0]);
size_t size = fdt64_to_cpu(reinterpret_cast<const uint64_t *>(reg_prop->data)[1]);
// 5. 获取中断号
const auto *irq_prop = fdt_get_property(fdt_header_, stdout_offset, "interrupts", &len);
uint32_t irq = fdt32_to_cpu(reinterpret_cast<const uint32_t *>(irq_prop->data)[1]);
return {base, size, irq};
}/ {
chosen {
stdout-path = "/uart@9000000";
};
uart@9000000 {
compatible = "arm,pl011", "arm,primecell";
reg = <0x0 0x9000000 0x0 0x1000>;
interrupts = <0x0 0x1 0x4>;
clock-names = "uartclk", "apb_pclk";
clocks = <&clk24mhz>, <&clk24mhz>;
};
};
在 src/arch/aarch64/arch_main.cpp 的 ArchInit() 中:
void ArchInit(int argc, const char **argv) {
// 1. 解析设备树
KernelFdtSingleton::create(strtoull(argv[2], nullptr, 16));
// 2. 获取串口信息
auto [serial_base, serial_size, irq] =
KernelFdtSingleton::instance().GetSerial();
// 3. 初始化PL011串口
Pl011Singleton::create(serial_base);
pl011 = &Pl011Singleton::instance();
// 4. 串口现在可用于调试输出
klog::Info("Hello aarch64 ArchInit\n");
}// 数据寄存器 (DR) - 0x00
// 位[7:0]: 发送/接收数据
// 位[11:8]: 错误标志 (仅读取时)
// 标志寄存器 (FR) - 0x18
// 位[7]: TXFE - 发送FIFO空
// 位[6]: RXFF - 接收FIFO满
// 位[5]: TXFF - 发送FIFO满
// 位[4]: RXFE - 接收FIFO空
// 位[3]: BUSY - UART忙
// 线路控制寄存器 (LCRH) - 0x2C
// 位[6:5]: WLEN - 字长选择 (11=8位)
// 位[4]: FEN - FIFO使能
// 位[1]: PEN - 奇偶校验使能
// 控制寄存器 (CR) - 0x30
// 位[9]: RXE - 接收使能
// 位[8]: TXE - 发送使能
// 位[0]: UARTEN - UART使能// 波特率计算公式:
// BAUDDIV = (UARTCLK * 4) / BAUD_RATE
// IBRD = BAUDDIV >> 6
// FBRD = BAUDDIV & 0x3F
// 例如: UARTCLK=24MHz, BAUD_RATE=115200
// BAUDDIV = (24000000 * 4) / 115200 = 833.33
// IBRD = 833 >> 6 = 13
// FBRD = 833 & 0x3F = 1- ARM标准:符合ARM PrimeCell规范
- 功能完整:支持中断、FIFO、流控等
- 设备树驱动:通过设备树动态配置
- 可扩展性:支持多种波特率和配置
- 调试友好:与ARM开发工具兼容
// 中断相关寄存器
static constexpr uint32_t kRegRIS = 0x3C; // 原始中断状态
static constexpr uint32_t kRegMIS = 0x40; // 屏蔽中断状态
static constexpr uint32_t kRegICR = 0x44; // 中断清除
// 中断类型
static constexpr uint32_t kIMSCRTIM = (1 << 6); // 接收超时中断
static constexpr uint32_t kIMSCRxim = (1 << 4); // 接收中断// 初始化PL011串口
auto [base, size, irq] = GetSerial();
Pl011 uart(base);
// 直接字符输出
uart.PutChar('H');
uart.PutChar('i');
// 通过内核日志输出(会调用sk_putchar)
klog::Info("Hello AArch64 Debug Output\n");
// 检查串口状态
uint32_t status = io::In<uint32_t>(base + Pl011::kRegFR);
bool tx_ready = !(status & Pl011::kFRTxFIFO);// 错误状态检查
uint32_t status = io::In<uint32_t>(base_addr_ + kRegRSRECR);
if (status & 0x0F) { // 检查错误位
// 清除错误
io::Out<uint32_t>(base_addr_ + kRegRSRECR, 0);
}