7、CV1800B小核移植
CV1800B 中的小核是比较特殊的 RISC-V 64 位 无 MMU 的平头哥 C906,应该是为了节省成本将 MMU 裁剪掉了。
本文通过循序渐进的方式一步一步完成适配 RT-Thread,本章节相关工作均在 bsp 目录下对应的项目工程目录下进行。
7.1、相关寄存器
MIE
MTVEC
7.2、第一阶段:bsp工程创建与编译
第一阶段目标:可以正常编译 RT-Thread,生成 rtthread.bin 后通过脚本合并生成可运行的 bin 文件。
bsp 目录
包括:
链接脚本: 复制 duo-buildroot-sdk/freertos/cvitek/scripts/cv180x_lscript.ld 及 build/output/cv1800b_milkv_duo_sd/cvi_board_memmap.ld。
在项目根据路下添加 Kconfig 文件: 由于 cv 1800b 小核是 c906-nommu,在 Kconfig 文件中必须添加以下配置:
config BSP_USING_C906_LITTLE bool select ARCH_RISCV64 select ARCH_RISCV_FPU_D select RT_USING_COMPONENTS_INIT select RT_USING_USER_MAIN default y
添加 scons 编译文件 SConscript 和 SConstruct。
建立 board 目录,复制 freertos/cvitek/arch/riscv64 目录,并修改 start.S 文件中硬件初始化完成后跳转到 RT-Thread 内置 C 语言的入口函数
entry
。#include "riscv-virt.h" .org 0 .section .vectors, "ax" .globl _start .type _start,@function _start: .cfi_startproc .cfi_undefined ra .option push .option norelax // la gp, __global_pointer$ .option pop // Continue primary hart csrr a0, mhartid li a1, PRIM_HART bne a0, a1, secondary li x1, 0 li x2, 0 li x3, 0 li x4, 0 li x5, 0 li x6, 0 li x7, 0 li x8, 0 li x9, 0 li x10, 0 li x11, 0 li x12, 0 li x13, 0 li x14, 0 li x15, 0 li x16, 0 li x17, 0 li x18, 0 li x19, 0 li x20, 0 li x21, 0 li x22, 0 li x23, 0 li x24, 0 li x25, 0 li x26, 0 li x27, 0 li x28, 0 li x29, 0 li x30, 0 li x31, 0 // enable interrupt // li x3, 0x880 // csrw mie, x3 csrw mie, 0 csrw mip, 0 la t0, trap_entry csrw mtvec, t0 #ifndef RISCV_QEMU // invalidate all memory for BTB,BHT,DCACHE,ICACHE li x3, 0x30013 csrs mcor, x3 // enable ICACHE,DCACHE,BHT,BTB,RAS,WA li x3, 0x7f csrs mhcr, x3 // enable data_cache_prefetch, amr li x3, 0x610c csrs mhint, x3 #mhint #endif # enable fp li x3, 0x1 << 13 csrs mstatus, x3 // Primary hart la sp, _stack_top // Load data section la a0, _data_lma la a1, _data la a2, _edata bgeu a1, a2, 2f 1: LOAD t0, (a0) STOR t0, (a1) addi a0, a0, REGSIZE addi a1, a1, REGSIZE bltu a1, a2, 1b 2: // Clear bss section la a0, _bss la a1, _ebss bgeu a0, a1, 2f 1: // reduce branch time, be sure about bss alignment in linker script STOR zero, 0x00 (a0) STOR zero, 0x08 (a0) STOR zero, 0x10 (a0) STOR zero, 0x18 (a0) STOR zero, 0x20 (a0) STOR zero, 0x28 (a0) STOR zero, 0x30 (a0) STOR zero, 0x38 (a0) addi a0, a0, REGSIZE * 8 bltu a0, a1, 1b 2: // argc, argv, envp is 0 li a0, 0 li a1, 0 li a2, 0 jal entry 1: wfi j 1b secondary: // TODO: Multicore is not supported wfi j secondary .cfi_endproc
修改 rtconfig.py 中的编译参数,相关参数参考 freertos/cvitek/scripts/toolchain-riscv64-elf.cmake 文件。
import os # toolchains options ARCH ='risc-v' CPU ='rv64' CROSS_TOOL ='gcc' # bsp lib config BSP_LIBRARY_TYPE = None if os.getenv('RTT_ROOT'): RTT_ROOT = os.getenv('RTT_ROOT') else: RTT_ROOT = r'../../..' if os.getenv('RTT_CC'): CROSS_TOOL = os.getenv('RTT_CC') if CROSS_TOOL == 'gcc': PLATFORM = 'gcc' EXEC_PATH = r'/opt/Xuantie-900-gcc-elf-newlib-x86_64-V2.8.1/bin' else: print('Please make sure your toolchains is GNU GCC!') exit(0) if os.getenv('RTT_EXEC_PATH'): EXEC_PATH = os.getenv('RTT_EXEC_PATH') BUILD = 'debug' if PLATFORM == 'gcc': # toolchains PREFIX = 'riscv64-unknown-elf-' CC = PREFIX + 'gcc' CXX = PREFIX + 'g++' AS = PREFIX + 'gcc' AR = PREFIX + 'ar' LINK = PREFIX + 'gcc' TARGET_EXT = 'elf' SIZE = PREFIX + 'size' OBJDUMP = PREFIX + 'objdump' OBJCPY = PREFIX + 'objcopy' DEVICE = ' -march=rv64imafdc -mabi=lp64d -mcmodel=medany' CFLAGS = DEVICE + ' -std=gnu11 -ffunction-sections -fdata-sections -Wl,--gc-sections -Wno-pointer-to-int-cast -fno-builtin -Wno-missing-field-initializers -fdiagnostics-color=always' CFLAGS += ' -Wall -nostdlib' CFLAGS += ' -DCONFIG_64BIT' LINKER_SCRIPTS = r'cv180x_lscript.ld' AFLAGS = ' -c' + DEVICE + ' -x assembler-with-cpp' LFLAGS = DEVICE + ' -nostartfiles -fms-extensions -ffunction-sections -fdata-sections -Wl,--gc-sections,-Map=rtthread.map,-cref,-u,_start -T ' + LINKER_SCRIPTS CPATH = '' LPATH = '' if BUILD == 'debug': CFLAGS += ' -O2 -g' AFLAGS += ' -g' else: CFLAGS += ' -O3' CXXFLAGS = CFLAGS + ' -std=gnu++17 -Wno-multichar -Wno-parentheses' DUMP_ACTION = OBJDUMP + ' -D -S $TARGET > rtt.asm\n' POST_ACTION = OBJCPY + ' -O binary $TARGET rtthread.bin\n' + SIZE + ' $TARGET \n' POST_ACTION += 'cd .. && ./combine-fip.sh c906_little/rtthread.bin\n'
添加 application 目录,并创建 main.c 文件,添加 main 函数,不然编译会报错。
#include <rtthread.h> #include <stdio.h> int main(void) { rt_kprintf("Hello, RISC-V!\n"); return 0; }
drivers 目录
创建 drivers 目录,添加 board.c 、board.h,在 board.c 中实现
rt_hw_board_init()
函数,该文件在rtthread_startup()
函数中被调用。并需在该函数中先实现串口初始化及rt_console
设备注册,用于日志输出。
void rt_hw_board_init(void)
{
#ifdef RT_USING_SERIAL
rt_hw_uart_init();
#endif
/* Set the shell console output device */
#if defined(RT_USING_CONSOLE) && defined(RT_USING_DEVICE)
rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
}
串口驱动
串口驱动适配 参考 驱动框架与适配 完成串口驱动适配。
进入 menuconfig,配置 console 串口设备。在 RT-Thread Kernel 菜单下配置。
选中
Using console for rt_kprintf
,默认已选中。将
the device name for console
名字修改为与上诉驱动名一致,当前配置为uart0
。可以修改
the buffer size for console log printf
值,默认为128
,可能会影响某些特别长的打印正常显示,可根据当前芯片内存情况修改为256
或512
。
$ scons --menuconfig RT-Thread Kernel ---> [*] Using console for rt_kprintf (512) the buffer size for console log printf (uart0) the device name for console
堆栈配置
查看 ld 文件,获取到内存 section 分布情况。
MEMORY { psu_ddr_0_MEM_0 : ORIGIN = CVIMMAP_FREERTOS_ADDR , LENGTH = CVIMMAP_FREERTOS_SIZE }
CVIMMAP_FREERTOS_ADDR 和 CVIMMAP_FREERTOS_SIZE 定义在 cvi_board_memmap.ld 文件中,该文件在原厂 SDK 是自动生成的,这里我们用的是自动生成完成后的文件。
CVIMMAP_FREERTOS_ADDR = 0x83f40000; CVIMMAP_FREERTOS_RESERVED_ION_SIZE = 0x0; CVIMMAP_FREERTOS_SIZE = 0xc0000;
代码和数据都放在 RAM 上运行, 地址为
0x83f40000: 0x84000000
总共0xc0000
字节。stack 设置
修改 ld 文件,添加 __rt_rvstack 为栈顶地址。
/* Generate Stack and Heap definitions */ .stack (NOLOAD) : { . = ALIGN(64); _stack_end_end = .; . += _STACK_SIZE; _stack_top = .; __rt_rvstack = .; } > psu_ddr_0_MEM_0
heap 设置
我们将程序运行空间、全局变量、栈空间等之外所有剩余的空间都作为 heap 使用,并会根据程序/数据的大小自动动态调整。
修改 ld 文件。原 ld 文件中 heap 定义在 stack 前,FreeRTOS 的 heap 是通过全局数据来定义的属于 .bss 段,所以定义没有问题。在适配 RT-Thread 时需要做一定的修改,将 stack 放在 heap 前面,并将所有的剩余空间都作为 heap 使用。
/* Generate Stack and Heap definitions */ .stack (NOLOAD) : { . = ALIGN(64); _stack_end_end = .; . += _STACK_SIZE; _stack_top = .; __rt_rvstack = .; } > psu_ddr_0_MEM_0 .heap (NOLOAD) : { . = ALIGN(64); _heap = .; HeapBase = .; _heap_start = .; *(.heap*) /*. += _HEAP_SIZE;*/ /*_heap_size = _HEAP_SIZE; */ _heap_end = .; HeapLimit = .; } > psu_ddr_0_MEM_0 HeapLimit = ORIGIN(psu_ddr_0_MEM_0) + LENGTH(psu_ddr_0_MEM_0); _end = .;
修改 board.h,添加 heap 的起、始地址。
extern rt_uint8_t HeapBase; extern rt_uint8_t HeapLimit; #define RT_HW_HEAP_BEGIN (void *)&HeapBase #define RT_HW_HEAP_END (void *)&HeapLimit
修改 board.c 在
rt_hw_board_init()
函数最前面加入 heap 初始化代码。
#ifdef RT_USING_HEAP /* initialize memory system */ rt_system_heap_init(RT_HW_HEAP_BEGIN, RT_HW_HEAP_END); #endif
7.4、第二阶段:文件打包
第二阶段目标:将第一阶段生成的 bin 文件与其他相关的文件合并,生成芯片需要的相关文件后可以正常启动运行,可以在启动后显示 RT-Thread 启动 LOG。
根据前文分析,cv1800b 小核生成的文件是打包在 fip.bin 文件内的,fip.bin 由 BL2、opensbi、uboot、小核 bin 文件等工作合并而成。将需要的相关文件和打包脚本整合进入 RT-Thread bsp 工程,打包脚本命名为 combine.sh。
将上诉生成的 rtthread.bin 文件通过 combine.sh 脚本和 BL2、opensbi、uboot 合并生成 fip.bin,更新至 SD 卡,即可正常运行。
完成以上工作后,可以在串口中看到如下启动日志:
\ | /
- RT - Thread Operating System
/ | \ 5.1.0 build Dec 18 2023 17:29:19
2006 - 2022 Copyright by RT-Thread team
RT-Thread 已经启动起来了,但是由于对中断相关工作进行配置处理,所以调度器不能正常运行,其实是处于裸机工作状态。
7.5、第三阶段:中断
我们知道操作系统是需要靠硬件定时器定时中断做任务调度的,所以中断是移植 RT-Thread 最重要的工作之一。
中断初始化
在原 start.S 文件中,启动函数 _start
中通过配置 MIE 寄存器使能相关中断。
// enable interrupt
// li x3, 0x880
// csrw mie, x3
其中:
MEIE = 1:使能机器模式外部中断使能位
MTIE = 1:使能机器模式定时器中断
在 RT-Thread 中 MEIE 和 MTIE 中断使能均在对应模块中使能,这部分代码可以注释。
然后将中断和异常统一处理函数 trap_entry()
设置至 MTVEC 机器模式向量基址寄存器。
csrw mie, 0
csrw mip, 0
la t0, trap_entry
csrw mtvec, t0
MTVEC 寄存器 bit[1:0] 为 向量入口模式位 MODE,当 MODE[1:0] 为 2’ b00 时,异常和中断都统一使用 BASE 地址作为异常入口地址,BASE 即为 trap_entry()
的函数地址。
当异常/中断触发后,CPU 就会将跳转至 trap_entry()
函数处执行。
中断与异常处理
trap_entry()
函数位于 libcpu/risc-v/common/interrupt_gcc.S
文件。
.section .text.trap_entry
.align 2
.global trap_entry
trap_entry:
#ifdef ARCH_RISCV_FPU
addi sp, sp, -32 * FREGBYTES
FSTORE f0, 0 * FREGBYTES(sp)
FSTORE f1, 1 * FREGBYTES(sp)
FSTORE f2, 2 * FREGBYTES(sp)
FSTORE f3, 3 * FREGBYTES(sp)
FSTORE f4, 4 * FREGBYTES(sp)
FSTORE f5, 5 * FREGBYTES(sp)
FSTORE f6, 6 * FREGBYTES(sp)
FSTORE f7, 7 * FREGBYTES(sp)
FSTORE f8, 8 * FREGBYTES(sp)
FSTORE f9, 9 * FREGBYTES(sp)
FSTORE f10, 10 * FREGBYTES(sp)
FSTORE f11, 11 * FREGBYTES(sp)
FSTORE f12, 12 * FREGBYTES(sp)
FSTORE f13, 13 * FREGBYTES(sp)
FSTORE f14, 14 * FREGBYTES(sp)
FSTORE f15, 15 * FREGBYTES(sp)
FSTORE f16, 16 * FREGBYTES(sp)
FSTORE f17, 17 * FREGBYTES(sp)
FSTORE f18, 18 * FREGBYTES(sp)
FSTORE f19, 19 * FREGBYTES(sp)
FSTORE f20, 20 * FREGBYTES(sp)
FSTORE f21, 21 * FREGBYTES(sp)
FSTORE f22, 22 * FREGBYTES(sp)
FSTORE f23, 23 * FREGBYTES(sp)
FSTORE f24, 24 * FREGBYTES(sp)
FSTORE f25, 25 * FREGBYTES(sp)
FSTORE f26, 26 * FREGBYTES(sp)
FSTORE f27, 27 * FREGBYTES(sp)
FSTORE f28, 28 * FREGBYTES(sp)
FSTORE f29, 29 * FREGBYTES(sp)
FSTORE f30, 30 * FREGBYTES(sp)
FSTORE f31, 31 * FREGBYTES(sp)
#endif
/* save thread context to thread stack */
#ifndef __riscv_32e
addi sp, sp, -32 * REGBYTES
#else
addi sp, sp, -16 * REGBYTES
#endif
STORE x1, 1 * REGBYTES(sp)
csrr x1, mstatus
STORE x1, 2 * REGBYTES(sp)
csrr x1, mepc
STORE x1, 0 * REGBYTES(sp)
STORE x4, 4 * REGBYTES(sp)
STORE x5, 5 * REGBYTES(sp)
STORE x6, 6 * REGBYTES(sp)
STORE x7, 7 * REGBYTES(sp)
STORE x8, 8 * REGBYTES(sp)
STORE x9, 9 * REGBYTES(sp)
STORE x10, 10 * REGBYTES(sp)
STORE x11, 11 * REGBYTES(sp)
STORE x12, 12 * REGBYTES(sp)
STORE x13, 13 * REGBYTES(sp)
STORE x14, 14 * REGBYTES(sp)
STORE x15, 15 * REGBYTES(sp)
#ifndef __riscv_32e
STORE x16, 16 * REGBYTES(sp)
STORE x17, 17 * REGBYTES(sp)
STORE x18, 18 * REGBYTES(sp)
STORE x19, 19 * REGBYTES(sp)
STORE x20, 20 * REGBYTES(sp)
STORE x21, 21 * REGBYTES(sp)
STORE x22, 22 * REGBYTES(sp)
STORE x23, 23 * REGBYTES(sp)
STORE x24, 24 * REGBYTES(sp)
STORE x25, 25 * REGBYTES(sp)
STORE x26, 26 * REGBYTES(sp)
STORE x27, 27 * REGBYTES(sp)
STORE x28, 28 * REGBYTES(sp)
STORE x29, 29 * REGBYTES(sp)
STORE x30, 30 * REGBYTES(sp)
STORE x31, 31 * REGBYTES(sp)
#endif
/* switch to interrupt stack */
move s0, sp
#ifdef RT_USING_SMP
/* get cpu id */
csrr t0, mhartid
/* switch interrupt stack of current cpu */
la sp, __stack_start__
addi t1, t0, 1
li t2, __STACKSIZE__
mul t1, t1, t2
add sp, sp, t1 /* sp = (cpuid + 1) * __STACKSIZE__ + __stack_start__ */
#endif
/* handle interrupt */
call rt_interrupt_enter
csrr a0, mcause
csrr a1, mepc
mv a2, s0
call handle_trap
call rt_interrupt_leave
#ifdef RT_USING_SMP
/* s0 --> sp */
mv sp, s0
mv a0, s0
call rt_scheduler_do_irq_switch
tail rt_hw_context_switch_exit
#else
/* switch to from_thread stack */
move sp, s0
/* need to switch new thread */
la s0, rt_thread_switch_interrupt_flag
lw s2, 0(s0)
beqz s2, spurious_interrupt
sw zero, 0(s0)
la s0, rt_interrupt_from_thread
LOAD s1, 0(s0)
STORE sp, 0(s1)
la s0, rt_interrupt_to_thread
LOAD s1, 0(s0)
LOAD sp, 0(s1)
#endif
spurious_interrupt:
tail rt_hw_context_switch_exit
RISC-V 架构规定,进入异常和退出异常中没有硬件自动保存和恢复上下文的操作,因此需要软件明确地使用指令进行上下文的保存和恢复。
所以 trap_entry() 函数完成以下工作:
保存现场,
调用 RT-Thread 所有中断处理函数都需要调用的
rt_interrupt_enter();
通知系统进入中断调用真正的 trap 处理函数
handle_trap()
函数处理trap,并将 MCAUSE,MEPC,SP 这 3 个寄存器通过a0/a1/a2 寄存器转至handle_trap()
函数。调用
rt_interrupt_leave();
通知系统离开中断判断是否有更高优先级的任务需要调度(rt_thread_switch_interrupt_flag标志),有则进行任务切换
调用 rt_hw_context_switch_exit 恢复现场。
handle_trap()
函数位于libcpu/risc-v/rv64/trap.c
文件。
rt_weak rt_size_t handle_trap(rt_size_t cause, rt_size_t epc, rt_size_t *sp)
{
if (cause & (1UL << (__riscv_xlen - 1))) //interrupt
{
if ((cause & 0x1f) == IRQ_M_SOFT)
{
rt_hw_soft_irq_isr();
}
else if ((cause & 0x1f) == IRQ_M_TIMER)
{
rt_hw_tick_isr();
}
else if ((cause & 0x1f) == IRQ_M_EXT)
{
rt_hw_irq_isr();
}
}
else
{
rt_thread_t tid;
#if defined(RT_USING_FINSH) && defined(MSH_USING_BUILT_IN_COMMANDS)
extern long list_thread();
#endif
rt_hw_interrupt_disable();
tid = rt_thread_self();
rt_kprintf("\nException:\n");
switch (cause)
{
case CAUSE_MISALIGNED_FETCH:
rt_kprintf("Instruction address misaligned");
break;
case CAUSE_FAULT_FETCH:
rt_kprintf("Instruction access fault");
break;
case CAUSE_ILLEGAL_INSTRUCTION:
rt_kprintf("Illegal instruction");
break;
case CAUSE_BREAKPOINT:
rt_kprintf("Breakpoint");
break;
case CAUSE_MISALIGNED_LOAD:
rt_kprintf("Load address misaligned");
break;
case CAUSE_FAULT_LOAD:
rt_kprintf("Load access fault");
break;
case CAUSE_MISALIGNED_STORE:
rt_kprintf("Store address misaligned");
break;
case CAUSE_FAULT_STORE:
rt_kprintf("Store access fault");
break;
case CAUSE_USER_ECALL:
rt_kprintf("Environment call from U-mode");
break;
case CAUSE_SUPERVISOR_ECALL:
rt_kprintf("Environment call from S-mode");
break;
case CAUSE_HYPERVISOR_ECALL:
rt_kprintf("Environment call from H-mode");
break;
case CAUSE_MACHINE_ECALL:
rt_kprintf("Environment call from M-mode");
break;
default:
rt_kprintf("Uknown exception : %08lX", cause);
break;
}
rt_kprintf("\n");
print_stack_frame(sp);
rt_kprintf("exception pc => 0x%08x\n", epc);
rt_kprintf("current thread: %.*s\n", RT_NAME_MAX, tid->parent.name);
#if defined(RT_USING_FINSH) && defined(MSH_USING_BUILT_IN_COMMANDS)
list_thread();
#endif
while(1);
}
return epc;
}
主要完成以下工作:
判断 MCAUSE 最高位是否被置位,置位则表示当前位中断,否则为异常
如当前为中断则判断 MCASUE 的 bit[4:0],通过判断异常向量表对应的中断号,跳转置对应的系统计时器、外部中断、软件中断函数处理。
如当前为异常,则通过异常号做对应处理
中断配置
CV1800B 小核是 C906-NOMMU,所以只能工作在机器模式下,超级用户模式下的中断都作为异常处理。 涉及到的中断有 3 种:
定时器中断
外部中断
软件中断
定时器中断
定时器中断配置是在
board/board.c
文件中rt_hw_board_init()
函数中完成初始化。在 board/board.c 文件中
rt_hw_board_init()
函数中添加硬件定时器初始化函数rt_hw_tick_init()
。外部中断
外部中断配置是在
board/board.c
文件中rt_hw_board_init()
函数中完成初始化。在 board/board.c 文件中
rt_hw_board_init()
函数中添加 负责外部rt_hw_interrupt_init()
。软件中断主要用于线程切换,RT-Thread 在线程切换中未使用软件中断。
完成后的 rt_hw_board_init()
函数如下:
void rt_hw_board_init(void)
{
#ifdef RT_USING_HEAP
/* initialize memory system */
rt_system_heap_init(RT_HW_HEAP_BEGIN, RT_HW_HEAP_END);
#endif
/* initalize interrupt */
rt_hw_interrupt_init();
/* init rtthread hardware */
rt_hw_tick_init();
#ifdef RT_USING_SERIAL
rt_hw_uart_init();
#endif
/* Set the shell console output device */
#if defined(RT_USING_CONSOLE) && defined(RT_USING_DEVICE)
rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
#ifdef RT_USING_HEAP
/* initialize memory system */
rt_kprintf("RT_HW_HEAP_BEGIN:%x RT_HW_HEAP_END:%x size: %d\r\n", RT_HW_HEAP_BEGIN, RT_HW_HEAP_END, RT_HW_HEAP_END - RT_HW_HEAP_BEGIN);
#endif
}
系统计时器
按照 RISC-V 定义,RV32 和 RV64 均需要提供一个 64 位系统定时器 MTIME,该定时器在处理器核局部中断控制器(Core Local Interruptor 简称 CLINT)中实现。
系统计时器定义了两个 64 位宽的寄存器 MTIME 和 MTIMECMP:
MTIME 是一个可读可写的计数器,其中的数值以一定的时间间隔递增,计数器计满后会回绕。
一个机器模式计时器比较寄存器(MTIMECMPH,MTIMECMPL)和一个 64 位的超级用户模式计时器比较寄存器(STIMECMPH,STIMECMPL)。
通过比较系统计时器的当前值(MTIME)与比较值寄存器(MTIMECMP/STIMECMP)的值来确定是否产生计时器中断。
当系统计时器的值小于比较寄存器(MTIMECMP/STIMECMP)的值时不产生中断。
当系统计时器的值大于等于比较寄存器(MTIMECMP/STIMECMP)的值时产生对应的计时器中断。
在机器模式下,只有当 MTIMECMP 寄存器被重新写入后,MIP 寄存器中的时钟中断标志位才会被清除。
机器模式下可以访问/修改所有计时器中断相关的寄存器;带超级用户模式的架构超级用户模式下只具有访问/修改超级用户模式计时器比较寄存器的权限。
RISC-V 特权 ISA 规范 https://github.com/riscv/riscv-isa-manual/releases/download/riscv-isa-release-056b6ff-2023-10-02/riscv-privileged.pdf 中 3.2.1 Machine Timer Registers (mtime and mtimecmp) 中可以找到系统计时器的规范。
在 RISC-V 规范中并没有定义 MTIME 寄存器和 MTIMECMP/STIMECMP 寄存器为 CSR 寄存器,而是定义其为存储器地址映射(Machine-Level Memory-Mapped Registers)的系统寄存器,具体的存储器音声地址 RISC-V 架构没有规定,是有 SOC 设计厂商自己定义的。
MTIME 和 MTIMECMP 无论是 RV32E、RV32I、RV64、RV128 总是 64 位的。对用作计数的这两个寄存器而言,64 位已经是天文数字,在产品的整个生命周期内都不会产生溢出。假设驱动 MTIME 的时钟频率是 10GHz(目前还没有哪个CPU主频能到 10GHz 的吧),那么要让 64 位的 MTIME 溢出,那么需要 0x10000000000000000 / 10000000000 / 60 / 60 / 24 / 365 年,也就是 58 年。
C906中的系统计时器
在 C906 中 CLINT 占据 64KB 内存空间,其高 13 位地址由 SoC 集成 C906 时硬件指定,低 27 位地址映射如下表所示。所有寄存器仅支持字对齐的访问。
CLINT 寄存器存储器映射地址表: | 地址 | 名称 | 属性 | 初始值 | 描述 | | —- | — | — | —— | —- | | 0x4000000 | MSIP0 | 读/写 | 0x00000000 | 机器模式软件中断配置寄存器高位绑 0, bit[0] 有效 | | Reserved | - | - | - | - | | 0x4004000 | MTIMECMPL0 | 读/写 | 0xFFFFFFFF | 机器模式系统计时器比较值寄存器 (低 32 位) | | 0x4004004 | MTIMECMPH0 | 读/写 | 0xFFFFFFFF | 机器模式系统计时器比较值寄存器 (高 32 位) | | Reserved | - | - | - | - | | 0x400C000 | SSIP0 | 读/写 | 0x00000000 | 超级用户模式软件中断配置寄存器高位绑 0, bit[0] 有效 | | Reserved | - | - | - | - | | 0x400D000 | STIMECMPL0 | 读/写 | 0xFFFFFFFF | 超级用户模式系统计时器比较值寄存器 (低 32 位) | | 0x400D004 | STIMECMPH0 | 读/写 | 0xFFFFFFFF | 超级用户模式系统计时器比较值寄存器 (高 32 位) | | Reserved | - | - | - | - |
定义了 M-Mode(机器模式)的软件中断和计时器比较中断,S-Mode(超级用户模式)下的软件中断和计时器比较中断。
在 C906 的 CLINT 寄存器存储器有映射地址表并没有看到上面所诉的 MTIME 寄存器。在网上查询到相关信息为:C906 不是完全按照 CLIC 的标准定义这一块的,它使用了 CLIC+PLIC,并没有把 MTIME 的地址给映射出来,而是通过 rdtime 伪指令,这条指令编译的时候会扩展为 CSR 指令读取 C906 的扩展寄存器,而这个扩展寄存器就是和 MTIME 绑定的。
RT-Thread 中的硬件定时器
硬件定时器初始化
static volatile rt_uint64_t time_elapsed = 0; static volatile unsigned long tick_cycles = 0; #define CLINT_BASE (PLIC_PHY_ADDR + 0x4000000UL) static volatile rt_uint32_t *mtimecmp_l = (volatile rt_uint32_t *)(CLINT_BASE + 0x4000UL); static volatile rt_uint32_t *mtimecmp_h = (volatile rt_uint32_t *)(CLINT_BASE + 0x4004UL); rt_inline void set_ticks(rt_uint64_t value) { *mtimecmp_l = 0xFFFFFFFF; *mtimecmp_h = (rt_uint32_t)(value >> 32); *mtimecmp_l = (rt_uint32_t)(value & 0xFFFFFFFF); } /* Sets and enable the timer interrupt */ int rt_hw_tick_init(void) { /* Clear the Machine-Timer bit in MIE */ clear_csr(mie, MIP_MTIP); tick_cycles = TIMER_CLK_FREQ / RT_TICK_PER_SECOND; set_ticks(get_ticks() + tick_cycles); rt_kprintf("[rt_hw_tick_init] time_elapsed: %d tick_cycles:%d\n", time_elapsed, tick_cycles); /* Enable the Machine-Timer bit in MIE */ set_csr(mie, MIP_MTIP); return 0; }
其中:
TIMER_CLK_FREQ 为 MTIME 时钟频率,CV1800B 为 25M,定义在 bsp/cvitek/c906_little/Kconfig 文件中,通过 menuconfig 自动生成。
config TIMER_CLK_FREQ int default 25000000
RT_TICK_PER_SECOND 为 RT-Thread 系统时钟节拍,默认值为 1000,定义在 rtconfig.h 中,可通过 menuconfig 修改,并自动生成。
$ scons --menuconfig RT-Thread Kernel ---> (1000) Tick frequency, Hz
硬件定时器中断函数处理
调用
rt_tick_increase()
通知内核调用
set_ticks(get_ticks() + tick_cycles)
重新设置 mtimer 值
int rt_hw_tick_isr(void) { rt_tick_increase(); set_ticks(get_ticks() + tick_cycles); return 0; }
中断处理函数
rt_hw_tick_isr()
会在 CLINT 中断中被调用。
外部中断管理
外部中断管理使用 RT-Thread 自带的外部中断管理接口,包括:
rt_hw_interrupt_init()
rt_hw_interrupt_install()
rt_hw_interrupt_mask()
rt_hw_interrupt_umask()
外部中断管理接口在驱动中会经常用到。