Skip to content

Latest commit

 

History

History
409 lines (320 loc) · 11.7 KB

File metadata and controls

409 lines (320 loc) · 11.7 KB

调试输出

早期控制台 (Early Console)

在内核启动的最早期(在此阶段,内存管理、中断甚至设备树解析都尚未完成),为了能够打印调试信息,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;

此阶段的串口驱动通常采用轮询模式,不依赖中断。

RISCV64

概述

RISC-V64 架构使用 OpenSBI 固件提供的调试控制台扩展 (Debug Console Extension) 进行调试输出。这种方式通过 SBI 调用直接与 Machine 模式固件通信,无需直接操作硬件串口。

关键组件

1. SBI 调试控制台扩展

基于 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);

2. 调试输出接口

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 调用机制

1. SBI 调用实现

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;
}

2. 权限级别转换

  • Supervisor 模式:内核运行在 S 模式
  • Machine 模式:OpenSBI 固件运行在 M 模式
  • 调用流程:S 模式 → ecall → M 模式 → 串口硬件

初始化流程

RISC-V64 的调试输出无需额外初始化,因为:

  1. 固件支持:OpenSBI 固件在启动时已初始化串口
  2. 标准接口:通过标准 SBI 调用访问
  3. 自动配置:固件根据设备树自动配置串口参数

支持的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

概述

AArch64 架构使用 ARM PrimeCell UART (PL011) 控制器进行调试输出。PL011 是 ARM 设计的标准 UART 控制器,广泛应用于 ARM 系统中,支持完整的 UART 功能和中断处理。

关键组件

1. PL011 驱动实现

位于 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);    // 接收中断屏蔽
};

2. 初始化实现

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);
}

3. 字符输出实现

void Pl011::PutChar(uint8_t c) {
  // 等待发送FIFO有空间或设备被禁用
  while (io::In<uint32_t>(base_addr_ + kRegFR) & kFRTxFIFO) { ; }

  // 写入数据寄存器
  io::Out<uint32_t>(base_addr_ + kRegDR, c);
}

4. 调试输出接口

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);
  }
}

设备树配置

1. 串口信息获取

通过设备树获取串口配置,在 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};
}

2. 典型设备树配置

/ {
  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.cppArchInit() 中:

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");
}

寄存器详解

1. 核心寄存器

// 数据寄存器 (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使能

2. 波特率设置

// 波特率计算公式:
// 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);
}