AbydOS开发日记 (2) - 驱动程序框架和系统参数配置
设备与系统参数
所有连接到系统上的部件,都可以视为设备,从而并入设备驱动框架统一管理。
但是在现代操作系统中,如果没有 ACPI,如何发现设备呢?主线 Linux 已经给出了答案,利用设备树 (Device Tree) 描述。
在设备树中,我们可以描述平台的特性、外设的接入情况、CPU 和 Memory 的数量,还可以携带配置参数,如命令行参数。
由于树状结构不好存储,所以 Linux 推出了一个工具 DTC (Device Tree Compiler),用于将设备树源码 (DTS) 编译成扁平化的设备树 (Flattened Device Tree,FDT),并且提供了一个 libfdt 库进行操作,可以进行增删改查。
本篇记录设备发现、驱动框架搭建和系统参数配置的设计和过程。关于设备树的详细解释,请参阅 Linux设备树 和 设备树的解析 LibFDT。
驱动框架搭建
用 CS 的思维来考虑,设备都存在共同之处,也就可以抽象成一个基类。这里给出定义:
#define DRV_INSTALL_FUNC(V) __attribute__((constructor(V)))
typedef enum
{
DEV_TYPE_SYS = 1,
DEV_TYPE_CHAR = 0x80,
DEV_TYPE_BLOCK,
} dev_type_t;
class DriverBase
{
public:
// Returns 1 if driver can handle this device, or 2 for cover-all driver (will stop probing for sub nodes)
virtual int probe(const char *name, const char *compatible) = 0;
virtual long addDevice(const void *fdt) = 0; // returns handler
virtual void removeDevice(long handler) = 0; // unregister device by handler
virtual dev_type_t getDeviceType() = 0;
virtual ~DriverBase() = default;
};
通过这样的基类,我们就可以通过 probe()
函数进行设备驱动程序枚举,并在枚举成功时调用 addDevice()
请求驱动程序添加设备,使用 removeDevice()
删除设备。
比如,考虑一个系统根设备驱动,这个设备驱动继承 DriverBase
并重写其中的虚函数:
static void drv_register();
class SysDev : public DriverBase
{
public:
int probe(const char *name, const char *compatible) override
{
std::string id = name;
if (id == "chosen") // Node to storage cmdline arguments
return 2;
if (id == "" && std::string(compatible).find("riscv") != std::string::npos)
return 1;
return 0;
}
long addDevice(const void *fdt) override
{
return ++hdl_count;
}
void removeDevice(long handler) override
{
}
dev_type_t getDeviceType() override
{
return DEV_TYPE_SYS;
}
private:
static int hdl_count;
friend void drv_register();
};
int SysDev::hdl_count = -1;
static DRV_INSTALL_FUNC(200) void drv_register()
{
static SysDev drv;
drv.hdl_count = 0;
DriverManager::addDriver(drv);
printf("Driver SysRoot installed\n");
}
这里还有一个 drv_register()
函数,用于注册驱动到系统中。
设备发现
无论是 OpenSBI 还是 UBoot ,都将设备树加载到内存中并进行修补,随后通过一个寄存器传递到下一级。我们的系统目前从 OpenSBI 直接跳到内核的 _start 处开始执行,根据其文档,设备树地址通过寄存器 a1 传递。所以我们可以直接从 C 环境下的第二个参数接收设备树地址,并加以解析:
long k_early_boot(const unsigned long hart_id, const void *dtb_addr, void **sys_stack_base)
{
int ret = fdt_check_header(dtb_addr);
if (ret != 0)
{
puts("[EBOOT] FDT header check failed\n");
return -1;
}
...
}
这里要明确一点,由于内存的描述也在设备树中,所以我们需要先于 k_cstart()
将设备树解析并正确设置系统栈。这里我们通过修改汇编代码增加一个对 k_early_boot()
的调用,在该函数中调整系统栈底的地址 (sys_stack_base),并将设备树数据全部转移到不会冲突的系统内存区域,供后续正常环境使用,随后返回到汇编中调整系统栈。这项工作稍后再讲述。
进入 k_main()
之后,我们就可以开始进行设备发现了。定义这样一个驱动管理类:
class DriverManager
{
public:
static void addDriver(DriverBase &drv)
{
_drvlist.push_back(&drv);
}
static void removeDriver(DriverBase &drv);
protected:
static int probe(const void *fdt,int node = 0);
friend int k_main(int argc, const char *argv[]);
private:
static std::vector<DriverBase *> _drvlist;
static char _depth[32];
static int _try(const void *fdt, int node);
};
其中的 probe()
函数将递归地解析设备树,然后调用 _try()
依次调用注册的设备驱动的 probe()
函数,根据其返回值决定是否进行安装和跳过子节点解析。
到这里,我们只要在 k_main()
中调用 DriverManager::probe()
,就可以完成设备发现了。
不过还有一个问题要解决,就是驱动程序要如何将自己在 k_main()
之前注册到 DriverManager
中,也就是由谁来调用 drv_register()
。相信有经验的同志已经注意到上面的一个宏定义:
#define DRV_INSTALL_FUNC(V) __attribute__((constructor(V)))
这里的 __attribute__((constructor(V)))
是一个 GCC 支持的函数属性描述宏,指示 GCC 将函数当作全局构造器 (Global Constructor),其中的 V
是优先级,越小的越早执行。通过这个修饰,函数指针将被放入 .init_array
段中,并在 k_before_main()
被执行。
此外,我们还需保证 _drvlist
能够较早地初始化,使用下面的定义:
__attribute__((init_priority(K_PR_INIT_DRV_LIST))) std::vector<DriverBase *> DriverManager::_drvlist;
指示 GCC 将这个全局对象的构造函数优先级提升到 K_PR_INIT_DRV_LIST
。
系统参数配置
前文提到要检测系统内存并设置栈,这里首先修改汇编启动代码,设置临时堆栈并调用 k_early_boot()
:
/* Setup boot C stack */
lla a3, _kernel_end
lla a4, _KERNEL_BOOT_STACK_SIZE /* this is fake a address provided from ld */
add sp, a3, a4
lla a3, _sys_stack_base
REG_S sp, 0(a3)
/* Early boot in C */
lla a3, _boot_a0
REG_L a0, 0(a3)
lla a3, _boot_a1
REG_L a1, 0(a3)
lla a2, _sys_stack_base
call k_early_boot
bne a0, zero, _start_hang
_start_warm:
...
随后在 k_early_boot 中解析内存节点:
// Detect system memory size
ret = fdt_path_offset(dtb_addr, "/memory");
if (ret < 0)
{
puts("[EBOOT] FDT memory node not found\n");
return -3;
}
int regsize = 0;
const void *memreg = fdt_getprop(dtb_addr, ret, "reg", ®size);
if (!memreg || regsize < 16)
{
puts("[EBOOT] FDT memory reg invalid\n");
return -4;
}
unsigned long mem_start = be2le64(((const unsigned long *)memreg)[0]);
unsigned long mem_len = be2le64(((const unsigned long *)memreg)[1]);
printf("[EBOOT] Memory Range: base= 0x%lx, len= 0x%lx\n", mem_start, mem_len);
- 通过调试发现设备树的数据是大端序的,通过函数简单转换一下即可。
系统命令行参数也包含在设备树中,通过节点 /chosen
下的 bootargs
属性提供。所以我们还可以先在这里解析了命令行参数,允许手动指定系统可用内存:
// Get the kernel command line
ret = fdt_path_offset(dtb_addr, "/chosen");
if (ret < 0)
{
puts("[EBOOT] FDT chosen node not found\n");
return -5;
}
const void *kcmdline = fdt_getprop(dtb_addr, ret, "bootargs", ®size);
if (!kcmdline || regsize < 1)
{
puts("[EBOOT] FDT bootargs invalid\n");
return -6;
}
memcpy(kernel_args, kcmdline, regsize);
puts("[EBOOT] Kernel command line: ");
puts(kernel_args);
// Parse Kerenl command line args for memory size (force override if specified in cmdline, only the first one is
// used!)
kernel_cmdargc = split_args((char *)kernel_args, kernel_args_array);
unsigned long mem_len_cmdarg = 0;
char mem_suffix = 'M';
for (int i = 0; i < kernel_cmdargc; ++i)
{
if (strncmp(kernel_args_array[i], "-mem", 4) == 0)
{
if (i < kernel_cmdargc - 1)
{
int res = sscanf(kernel_args_array[i + 1], "%ld %c", &mem_len_cmdarg, &mem_suffix);
if (res == 0)
{
puts("[EBOOT] Invalid memory size specified, skipping\n");
mem_len_cmdarg = 0;
break;
}
if (res == 1)
{
puts("[EBOOT] Memory size specified without suffix, assuming MB\n");
mem_len_cmdarg *= 1024 * 1024;
break;
}
if (res == 2)
{
switch (mem_suffix)
{
case 'K':
case 'k':
mem_len_cmdarg *= 1024;
break;
case 'M':
case 'm':
mem_len_cmdarg *= 1024 * 1024;
break;
case 'G':
case 'g':
mem_len_cmdarg *= 1024 * 1024 * 1024;
break;
default:
puts("[EBOOT] Invalid memory size suffix, skipping\n");
mem_len_cmdarg = 0;
break;
}
break;
}
}
else
{
puts("[EBOOT] Memory size not specified, skipping\n");
mem_len_cmdarg = 0;
break;
}
}
}
随后计算系统栈底:
k_mem_size = (mem_len_cmdarg > 0) ? mem_len_cmdarg : mem_len;
*sys_stack_base = (void *)(mem_start + k_mem_size);
printf("[EBOOT] Set SYS_SP: 0x%lx\n", (unsigned long)*sys_stack_base);
至此,这一部分宣告结束,来一段 Log:
详情
[EBOOT] Memory Range: base= 0x80000000, len= 0x10000000
[EBOOT] Kernel command line:
[EBOOT] Set SYS_SP: 0x90000000
===== Entered Test Kernel =====
a0: 0x0
Calling init_array...
Driver SysMem installed
Driver SysRoot installed
Driver UART8250 installed
cmd args: ''
Finding driver for #0 [riscv-virtio] ... Found! Installing... OK, handler: 1
Finding driver for #100 reserved-memory [] ... Found! Installing... OK, handler: 1
Finding driver for #164 mmode_resv1@80000000 [] ... Failed
Finding driver for #236 mmode_resv0@80040000 [] ... Failed
Finding driver for #312 flash@20000000 [cfi-flash] ... Failed
Finding driver for #420 chosen [] ... Found! Installing... OK, handler: 2
Driver has cover the node with offset: 420
Finding driver for #480 uart@10000000 [ns16550a] ... Found! Installing... OK, handler: 1
Finding driver for #604 test@100000 [sifive,test1] ... Failed
Finding driver for #692 virtio_mmio@10008000 [virtio,mmio] ... Failed
Finding driver for #808 virtio_mmio@10007000 [virtio,mmio] ... Failed
Finding driver for #924 virtio_mmio@10006000 [virtio,mmio] ... Failed
Finding driver for #1040 virtio_mmio@10005000 [virtio,mmio] ... Failed
Finding driver for #1156 virtio_mmio@10004000 [virtio,mmio] ... Failed
Finding driver for #1272 virtio_mmio@10003000 [virtio,mmio] ... Failed
Finding driver for #1388 virtio_mmio@10002000 [virtio,mmio] ... Failed
Finding driver for #1504 virtio_mmio@10001000 [virtio,mmio] ... Failed
Finding driver for #1620 cpus [] ... Failed
Finding driver for #1680 cpu-map [] ... Failed
Finding driver for #1692 cluster0 [] ... Failed
Finding driver for #1708 core0 [] ... Failed
Finding driver for #1748 cpu@0 [riscv] ... Failed
Finding driver for #1900 interrupt-controller [riscv,cpu-intc] ... Failed
Finding driver for #2012 memory@80000000 [] ... Found! Installing... OK, handler: 2
Finding driver for #2084 soc [simple-bus] ... Failed
Finding driver for #2160 pci@30000000 [pci-host-ecam-generic] ... Failed
Finding driver for #2852 interrupt-controller@c000000 [riscv,plic0] ... Failed
Finding driver for #3048 clint@2000000 [riscv,clint0] ... Failed
Reached k_after_main, clearing up...
===== Test Kernel exited with 0 =====