本文章的代码来自于rt-smart中针对qemu-virt-riscv的bsp
仓库地址 https://gitee.com/rtthread/rt-thread/tree/rt-smart/
commit ID:d28249c08a152bcf0e1a076cf5b4b082c0a84add
qemu-virt-riscv介绍简介Virt板不对应于任何真实硬件的平台;它是为虚拟机设计的。如果你只是想运行Linux等客户机,而不关心重现真实世界硬件的特殊性和局限性,那么它是推荐的板卡类型。(摘自https://www.qemu.org/docs/master/system/riscv/virt.html)
内存空间布局(包括外设地址)static const MemMapEntry virt_memmap[] = {
[VIRT_DEBUG] = { 0x0, 0x100 },
[VIRT_MROM] = { 0x1000, 0xf000 },
[VIRT_TEST] = { 0x100000, 0x1000 },
[VIRT_RTC] = { 0x101000, 0x1000 },
[VIRT_CLINT] = { 0x2000000, 0x10000 },
[VIRT_ACLINT_SSWI] = { 0x2F00000, 0x4000 },
[VIRT_PCIE_PIO] = { 0x3000000, 0x10000 },
[VIRT_PLIC] = { 0xc000000, VIRT_PLIC_SIZE(VIRT_CPUS_MAX * 2) },
[VIRT_APLIC_M] = { 0xc000000, APLIC_SIZE(VIRT_CPUS_MAX) },
[VIRT_APLIC_S] = { 0xd000000, APLIC_SIZE(VIRT_CPUS_MAX) },
[VIRT_UART0] = { 0x10000000, 0x100 }, /*串口设备*/
[VIRT_VIRTIO] = { 0x10001000, 0x1000 },
[VIRT_FW_CFG] = { 0x10100000, 0x18 },
[VIRT_FLASH] = { 0x20000000, 0x4000000 },
[VIRT_IMSIC_M] = { 0x24000000, VIRT_IMSIC_MAX_SIZE },
[VIRT_IMSIC_S] = { 0x28000000, VIRT_IMSIC_MAX_SIZE },
[VIRT_PCIE_ECAM] = { 0x30000000, 0x10000000 },
[VIRT_PCIE_MMIO] = { 0x40000000, 0x40000000 },
[VIRT_DRAM] = { 0x80000000, 0x0 }, /*DDR空间*/
};
rt-smart针对virt board的ddr空间规划参考链接脚本
bsp\qemu-virt64-riscv\link.lds
以及board.h中的相关定义
bsp\qemu-virt64-riscv\driver\board.h
得到ddr的空间规划如下
内容
地址空间
代码段数据段栈空间以及bss段0x80200000 ~ __bss_end
堆空间__bss_end ~ __bss_end + 100M
页分配空间__bss_end + 100M ~ __bss_end + 200Mrt-smart针对virt board的初始化整体初始化rt_hw_board_init定义了与qemu-virt-riscv相关的板级初始化的全部内容,包括内存系统,plic中断子系统,定时器系统以及串口设备等。它由rtthread_startup调用,完整的调用路径如下。
(libcpu\risc-v\virt64\startup_gcc.S)_start->primary_cpu_entry->entry->rtthread_startup->rt_hw_board_init
源码如下
void rt_hw_board_init(void)
{
#ifdef RT_USING_USERSPACE
rt_page_init(init_page_region);
/* init mmu_info structure */
rt_hw_mmu_map_init(&mmu_info, (void *)(USER_VADDR_START - IOREMAP_SIZE), IOREMAP_SIZE, (rt_size_t *)MMUTable, 0);
// this API is reserved currently since PLIC etc had not been porting completely to MMU version
rt_hw_mmu_kernel_map_init(&mmu_info, 0x00000000UL, 0x80000000);
/* setup region, and enable MMU */
rt_hw_mmu_setup(&mmu_info, platform_mem_desc, NUM_MEM_DESC);
#endif
#ifdef RT_USING_HEAP
/* initialize memory system */
rt_system_heap_init(RT_HW_HEAP_BEGIN, RT_HW_HEAP_END);
#endif
plic_init();
rt_hw_interrupt_init();
rt_hw_uart_init();
#ifdef RT_USING_CONSOLE
/* set console device */
rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif /* RT_USING_CONSOLE */
rt_hw_tick_init();
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
#ifdef RT_USING_HEAP
rt_kprintf("heap: [0x%08x - 0x%08x]\n", (rt_ubase_t)RT_HW_HEAP_BEGIN, (rt_ubase_t)RT_HW_HEAP_END);
#endif /* RT_USING_HEAP */
}
rt_page_initrt-smart中使用了buddy算法管理了一部分内存区域,系统使用page_alloc来向buddy管理的内存区域申请内存资源,像linux一样每个page是4k的大小。rt-smart采用buddy算法将系统中部分可用的物理内存页面按照每1个页面、2个页面、4个页面等等划分为了不同的单元。详情可参考这篇文章https://club.rt-thread.org/ask/article/3e3a9a0b6d3e2105.html
void rt_page_init(rt_region_t reg)
{
int i;
LOG_D("split 0x%08x 0x%08x\n", reg.start, reg.end);
reg.start += ARCH_PAGE_MASK;
reg.start &= ~ARCH_PAGE_MASK;
reg.end &= ~ARCH_PAGE_MASK;
{
int nr = ARCH_PAGE_SIZE / sizeof(struct page);
int total = (reg.end - reg.start) >> ARCH_PAGE_SHIFT;
int mnr = (total + nr) / (nr + 1);
LOG_D("nr = 0x%08x\n", nr);
LOG_D("total = 0x%08x\n", total);
LOG_D("mnr = 0x%08x\n", mnr);
RT_ASSERT(mnr < total);
page_start = (struct page*)reg.start;
reg.start += (mnr << ARCH_PAGE_SHIFT);
page_addr = (void*)reg.start;
page_nr = (reg.end - reg.start) >> ARCH_PAGE_SHIFT;
}
这里rt-smart直接将一部分页表空间分配给struct page去使用,有可能会造成页面的浪费。例如当total=7, nr=5时,mnr=2,也就是俩个页表用于存储page,五个页表是真正可以被alloc_page申请的。但实际上五个页表只需要一个页表的空间就可以存放page结构体了,相当于浪费了一个页表。
rt_hw_mmu_map_init#define USER_VADDR_START 0x100000000UL
#define IOREMAP_SIZE (1ul << 30)
int rt_hw_mmu_map_init(rt_mmu_info *mmu_info, void *v_address, rt_size_t size, rt_size_t *vtable, rt_size_t pv_off)
{
/*代码省略*/
mmu_info->vtable = vtable;
mmu_info->vstart = va_s;
mmu_info->vend = va_e;
mmu_info->pv_off = pv_off;
return 0;
}
mmu_info是一个全局变量,在调用rt_hw_mmu_map_init后,(USER_VADDR_START - IOREMAP_SIZE) ~ USER_VADDR_START 这片虚拟地址空间将来专门提供给ioremap来使用。也就是ioremap返回的虚拟地址区间就是 0xc0000000 ~ 0xFFFFFFFF
rt_hw_mmu_kernel_map_initvoid rt_hw_mmu_kernel_map_init(rt_mmu_info *mmu_info, rt_size_t vaddr_start, rt_size_t size)
{
rt_size_t paddr_start = __UMASKVALUE(VPN_TO_PPN(vaddr_start, mmu_info->pv_off), PAGE_OFFSET_MASK);
rt_size_t va_s = GET_L1(vaddr_start);
rt_size_t va_e = GET_L1(vaddr_start + size - 1);
rt_size_t i;
for (i = va_s; i <= va_e; i++)
{
mmu_info->vtable = COMBINEPTE(paddr_start, PAGE_ATTR_RWX | PTE_G | PTE_V);
paddr_start += L1_PAGE_SIZE;
}
rt_hw_cpu_tlb_invalidate();
}
这里将0x0 ~ 0x80000000的物理地址空间做了offset为0的一比一映射,且只使用了一级页表。之后0x80000000之下的地址CPU都可以直接访问了。从页表的属性配置上看,这片区域是nocache的。
rt_hw_mmu_setup#define KERNEL_VADDR_START 0x80000000
#define PV_OFFSET 0
struct mem_desc platform_mem_desc[] = {
{KERNEL_VADDR_START, KERNEL_VADDR_START + 0x10000000 - 1, KERNEL_VADDR_START + PV_OFFSET, NORMAL_MEM},
};
void rt_hw_mmu_setup(rt_mmu_info *mmu_info, struct mem_desc *mdesc, int desc_nr)
{
void *err;
for (size_t i = 0; i < desc_nr; i++)
{
size_t attr;
switch (mdesc->attr)
{
case NORMAL_MEM:
attr = MMU_MAP_K_RWCB;
break;
case NORMAL_NOCACHE_MEM:
attr = MMU_MAP_K_RWCB;
break;
case DEVICE_MEM:
attr = MMU_MAP_K_DEVICE;
break;
default:
attr = MMU_MAP_K_DEVICE;
}
rt_kprintf("vaddr start:%lx paddr_start:%lx\n", mdesc->vaddr_start, mdesc->paddr_start);
err = _rt_hw_mmu_map(mmu_info, (void *)mdesc->vaddr_start, (void *)mdesc->paddr_start,
mdesc->vaddr_end - mdesc->vaddr_start + 1, attr);
mdesc++;
}
rt_hw_mmu_switch((void *)MMUTable);
}
这里首先将0x80000000 ~ 0x90000000这片区域做了offset为0的线性映射,映射使用的是三级页表一页一页映射的,相当于page的区域也被映射好了。之后调用rt_hw_mmu_switch配置SATP配置MMU的地址翻译模式为SV39。STAP的mode被配置后,MMU就相当于开启了。
将rtconfig.h中的PV_OFFSET改为非0值后系统无法启动,对比bsp/qemu-vexpress-a9中board.c里关于页表的配置这块儿应该还是有问题的。
rt_hw_tick_initint rt_hw_tick_init(void)
{
/* Read core id */
// unsigned long core_id = current_coreid();
unsigned long interval = 1000/RT_TICK_PER_SECOND;
/* Clear the Supervisor-Timer bit in SIE */
clear_csr(sie, SIP_STIP);
/* calculate the tick cycles */
// tick_cycles = interval * sysctl_clock_get_freq(SYSCTL_CLOCK_CPU) / CLINT_CLOCK_DIV / 1000ULL - 1;
tick_cycles = 40000;
/* Set timer */
sbi_set_timer(get_ticks() + tick_cycles);
/* Enable the Supervisor-Timer bit in SIE */
set_csr(sie, SIP_STIP);
return 0;
}
这里使用的是riscv中的mtime。mtime是riscv中定义的一个64位的系统计时器,它被要求工作在常开的时钟域下。内核中使用以下指令可以读取mtime的值
static uint64_t get_ticks()
{
__asm__ __volatile__(
"rdtime %0"
: "=r"(time_elapsed));
return time_elapsed;
}
补充知识,在qemu中这个时钟的获取来源如下
static inline int64_t get_clock_realtime(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000000000LL + (tv.tv_usec * 1000);
}
sbi_set_timer并不是设置timer本身的值,而是设置机器模式计时器比较值寄存器MTIMECMPH, MTIMECMPL的值, 当系统计时器的值小于等于 {M/STIMECMPH[31:0],M/STIMECMPL[31:0]}的值时不产生中断;当系统计时器的值大于 {M/STIMECMPH[31:0],M/STIMECMPL[31:0]} 的值时 CLINT产生对应的计时器中断。它的配置过程为 rt-smart将比较寄存器的配置按规则组织为sbi_call的指令,将指令类型指令参数等放入cpu的a0~a7的寄存器,然后调用ecall指令使cpu陷入M态。
sbi_set_timer->SBI_CALL1(SBI_SET_TIMER, 0, val)->sbi_call
static __inline struct sbi_ret
sbi_call(uint64_t arg7, uint64_t arg6, uint64_t arg0, uint64_t arg1,
uint64_t arg2, uint64_t arg3, uint64_t arg4)
{
struct sbi_ret ret;
register uintptr_t a0 __asm("a0") = (uintptr_t)(arg0);
register uintptr_t a1 __asm("a1") = (uintptr_t)(arg1);
register uintptr_t a2 __asm("a2") = (uintptr_t)(arg2);
register uintptr_t a3 __asm("a3") = (uintptr_t)(arg3);
register uintptr_t a4 __asm("a4") = (uintptr_t)(arg4);
register uintptr_t a6 __asm("a6") = (uintptr_t)(arg6);
register uintptr_t a7 __asm("a7") = (uintptr_t)(arg7);
__asm __volatile(\
"ecall" \
: "+r"(a0), "+r"(a1) \
: "r"(a2), "r"(a3), "r"(a4), "r"(a6), "r"(a7) \
: "memory");
ret.error = a0;
ret.value = a1;
return (ret);
}
CPU陷入M态后,opensbi会处理这个ecall产生的异常。获取内核放到寄存器中参数,把新的值赋值给比较值寄存器,并清除计时器中断
void sbi_timer_event_start(u64 next_event)
{
if (timer_dev && timer_dev->timer_event_start)
timer_dev->timer_event_start(next_event);
csr_clear(CSR_MIP, MIP_STIP);
csr_set(CSR_MIE, MIP_MTIP);
}
其他之后的初始化都是原先rt-thread中的内容了,感兴趣的读者可以自行查阅rt-thread官方的《RT-THREAD 编程指南》手册来学习。另外需要注意的点在plic_init中,plic的寄存器的基地址没有使用ioremap就直接使用了,这是因为上面描述的0x0 ~ 0x80000000的物理地址空间被做了offset为0的一比一映射。
rt-smart的ioremap实现void *rt_ioremap(void *paddr, size_t size)
{
return _ioremap_type(paddr, size, MM_AREA_TYPE_PHY);
}
void *rt_ioremap_nocache(void *paddr, size_t size)
{
return _ioremap_type(paddr, size, MM_AREA_TYPE_PHY);
}
void *rt_ioremap_cached(void *paddr, size_t size)
{
return _ioremap_type(paddr, size, MM_AREA_TYPE_PHY_CACHED);
}
rt-smart中的ioremap实际上只分了俩种映射方式,分别是cache和nocache。在当前的qemu-virt64-riscv里,cache的属性没有配置到页表中,我也没有查qemu的页表支不支持配置cache,感兴趣的读者请参考C906的相关代码 libcpu/risc-v/t-head/c906/riscv_mmu.h
/* C-SKY extend */
#define PTE_SEC (1UL << 59) /* Security */
#define PTE_SHARE (1UL << 60) /* Shareable */
#define PTE_BUF (1UL << 61) /* Bufferable */
#define PTE_CACHE (1UL << 62) /* Cacheable */
#define PTE_SO (1UL << 63) /* Strong Order */
#define MMU_MAP_K_DEVICE (PAGE_ATTR_RWX | PTE_V | PTE_G | PTE_SO | PTE_BUF | PTE_A | PTE_D)
#define MMU_MAP_K_RWCB (PAGE_ATTR_RWX | PTE_V | PTE_G | PTE_SHARE | PTE_BUF | PTE_CACHE | PTE_A | PTE_D)
static void *_ioremap_type(void *paddr, size_t size, int type)
{
void *v_addr = NULL;
size_t attr;
switch (type)
{
case MM_AREA_TYPE_PHY:
attr = MMU_MAP_K_DEVICE;
break;
case MM_AREA_TYPE_PHY_CACHED:
attr = MMU_MAP_K_RWCB;
break;
default:
return v_addr;
}
rt_mm_lock();
v_addr = rt_hw_mmu_map(&mmu_info, 0, paddr, size, attr);
if (v_addr)
{
int ret = lwp_map_area_insert(&k_map_area, (size_t)v_addr, size, type);
if (ret != 0)
{
_iounmap_range(v_addr, size);
v_addr = NULL;
}
}
rt_mm_unlock();
return v_addr;
}
__ioremap_type中会记录页表要配置的属性然后调用rt_hw_mmu_map进行映射。之后会将映射得到的虚拟地址插入到k_map_area中。
void *_rt_hw_mmu_map(rt_mmu_info *mmu_info, void *v_addr, void *p_addr, rt_size_t size, rt_size_t attr)
{
/*代码省略*/
if (v_addr)
{
/*代码省略*/
}
else
{
vaddr = find_vaddr(mmu_info, pages);
}
if (vaddr)
{
ret = __rt_hw_mmu_map(mmu_info, (void *)vaddr, p_addr, pages, attr);
if (ret == 0)
{
rt_hw_cpu_tlb_invalidate();
return (void *)(vaddr | GET_PF_OFFSET((rt_size_t)p_addr));
}
}
return 0;
}
ioremap传入的虚拟地址是0,所以这里先需要调用find_vaddr得到一个可用的虚拟地址。另一个传入find_vaddr的参数pages代表要要映射的物理内存区域需要多少个page(4K).
static size_t find_vaddr(rt_mmu_info *mmu_info, int pages)
{
size_t loop_pages;
size_t va;
size_t find_va = 0;
int n = 0;
size_t i;
loop_pages = (mmu_info->vend - mmu_info->vstart) ? (mmu_info->vend - mmu_info->vstart) : 1;
loop_pages <<= (ARCH_INDEX_WIDTH * 2);
va = mmu_info->vstart;
va <<= (ARCH_PAGE_SHIFT + ARCH_INDEX_WIDTH * 2);
for (i = 0; i < loop_pages; i++, va += ARCH_PAGE_SIZE) {
if (_rt_hw_mmu_v2p(mmu_info, (void *)va)) {
n = 0;
find_va = 0;
continue;
}
if (!find_va) {
find_va = va;
}
n++;
if (n >= pages) {
return find_va;
}
}
return 0;
}
这里会从mmu_info->vstart的虚拟地址开始找,这个地址就是最前面提到的0xC0000000。从0XC0000000开始一个page一个page的去找,看对应的虚拟地址有没有被映射。如果没有,那么将va赋值给find_va。之后会继续往后查找看能不能找到连续的虚拟内存空间大小可以满足ioremap需要的大小。如果满足大小最终就返回找到的虚拟地址。总结这个过程就是寻找一块连续的没有被映射的大小满足的虚拟地址空间。