UCosIII 在 Tang Nano 20K 的 SparrowRV 软核移植

一直想完整的完成从FPGA到RTOS再到基础APP完成整个流程的理解,于是,尝试在开源的小麻雀RISC-V SOC上移植最简单的UCos-III操作系统,但由于不懂Verilog,所以磕磕碰碰,以下是其记录:

准备工作

由于使用的是一块小小的 Tang Nano 20K 开发板,其芯片为高云的 GW2AR-18C,且 SparrowRV 的作者已经提供了该开发板的移植实例工程,只需要在上面改一改就可以用了。

  1. 搭建高云的开发环境,从官网下载最新版即可;
  2. 在Gitee上拉取 SparrowRV ;
  3. 搭建Bsp的开发环境,这里使用的是Windows上的 MounRiver Studio,这个商业版或社区版都行;
  4. 拉取UCos-III的源码,去UCos-III GitHub这里,一般只需要拉uC-OS3、uC-CPU、uC-LIB这三个即可;

使用的 MounRiver Studio 版本为 v2.2.0
使用的高云教育版软件版本为 v1.9.11.03
使用的小麻雀处理器代码版本为 b460a44

移植准备

  1. 首先,得保证FPGA工程可以综合的过,由于高云教育版软件的更新,fpga/gowin_tang_nano_20k 目录下以前的工程直接综合会报错,提示FPIOA引脚电压错误,将其改为3.3V,即LVCMOS33;
  2. 此时可以发现很多FPIOA依旧没有连接到物理引脚上,由于本次只用到了Uart0的Tx口,更具原理图可以得到实际Uart0的Tx引脚是69,将FPIOA的Location改为69,并将Pull Mode改为None;
  3. 此时综合将不再报错;
  4. 打开bsp/bsp_app里的MounRiver Studio工程,最新版本是基于VsCode开发的,不是以前的Eclipse开发的;
  5. 直接编译工程,编译通过即可确保拉下来的是没有问题的;
  6. 将拉取下来的三个文件夹放到工程根目录下开发环境将会自动添加这些文件夹到项目内;
  7. 删除工程内除RISCV架构相关的其他架构的文件(注意不要把Cfg文件夹删了);

开始移植

  1. 配置工程,在设置工程属性里添加头文件目录和汇编文件目录并关闭优化:

Snipaste_2025-10-05_11-46-41

Snipaste_2025-10-05_11-47-08

Snipaste_2025-10-05_11-47-18

  1. 修改Ports文件夹下的相关文件,这里是关于移植的东西:

上下文切换

os_cpu_a.S要改以下内容:

  • 修改清除软中断的代码并把它放到入口处,屏蔽自动清除软件中断的FPGA代码(具体代码在 csr.v 有一行 msip <= 1'b0;//自动清除软件中断位);
  • 关于软中断切换和主动触发的地方要改,因为触发软件中断的寄存器是一个自定义的CSR;
  • 因为没有 entry.S 所以添加保存上下文的代码;
  • 保存栈指针到TCB的地方也要改,示例代码注释里说 entry.Ssp 保存到了 a2 寄存器,但是实际上代码里没有 entry.S 那么 a2 的值便是不确定的,因此直接将当前 sp 的值赋给TCB就好;

小麻雀的手册说,可以给函数添加 __attribute__((interrupt("machine"))) 来编译器自动添加代码保存恢复上下文,刚开始我给软中断函数添加了,结果通过反汇编发现,这个只管 C 函数的,汇编标签它是不加保存上下文的代码的;


    .equ  RISCV_CSR_MSIP,            0x345           # 添加自定义CSR软中断寄存器地址(写1触发软中断)

#-------------------------------------------------------------------------------------------------

OSCtxSw:
OSIntCtxSw:
# MIE_MSIE -- 启用软件中断位
    li     t0, RISCV_MIE_MSIE
    csrrs  zero, mie, t0

# 这将触发一个软件中断 MSIP = 0x01;
    csrrsi x0, RISCV_CSR_MSIP, 1
    ret

#-------------------------------------------------------------------------------------------------

Software_IRQHandler:
# 清除 hart0 的软件中断,MSIP = 0x01;
    csrw   RISCV_CSR_MSIP, zero

# 保存所有寄存器到堆栈(按照OSTaskStkInit的顺序)
    addi   sp, sp, -32 * 4           # 分配堆栈空间

# 保存MEPC寄存器(程序计数器)到堆栈顶部(偏移量 31 * 4)
    csrr   t0, mepc
    sw     t0, 31 * 4(sp)

# 保存通用寄存器 x1-x31 (x0 是硬连线0,不需要保存)
    sw     ra,   0 * 4(sp)          # x1 (ra)
    sw     sp,   1 * 4(sp)          # x2 (sp)
    sw     gp,   2 * 4(sp)          # x3 (gp)
    sw     tp,   3 * 4(sp)          # x4 (tp)
    sw     t0,   4 * 4(sp)          # x5 (t0)
    sw     t1,   5 * 4(sp)          # x6 (t1)
    sw     t2,   6 * 4(sp)          # x7 (t2)
    sw     s0,   7 * 4(sp)          # x8 (s0/fp)
    sw     s1,   8 * 4(sp)          # x9 (s1)
    sw     a0,   9 * 4(sp)          # x10 (a0)
    sw     a1,  10 * 4(sp)          # x11 (a1)
    sw     a2,  11 * 4(sp)          # x12 (a2)
    sw     a3,  12 * 4(sp)          # x13 (a3)
    sw     a4,  13 * 4(sp)          # x14 (a4)
    sw     a5,  14 * 4(sp)          # x15 (a5)
    sw     a6,  15 * 4(sp)          # x16 (a6)
    sw     a7,  16 * 4(sp)          # x17 (a7)
    sw     s2,  17 * 4(sp)          # x18 (s2)
    sw     s3,  18 * 4(sp)          # x19 (s3)
    sw     s4,  19 * 4(sp)          # x20 (s4)
    sw     s5,  20 * 4(sp)          # x21 (s5)
    sw     s6,  21 * 4(sp)          # x22 (s6)
    sw     s7,  22 * 4(sp)          # x23 (s7)
    sw     s8,  23 * 4(sp)          # x24 (s8)
    sw     s9,  24 * 4(sp)          # x25 (s9)
    sw     s10, 25 * 4(sp)          # x26 (s10)
    sw     s11, 26 * 4(sp)          # x27 (s11)
    sw     t3,  27 * 4(sp)          # x28 (t3)
    sw     t4,  28 * 4(sp)          # x29 (t4)
    sw     t5,  29 * 4(sp)          # x30 (t5)
    sw     t6,  30 * 4(sp)          # x31 (t6)

# 禁用全局中断并防止上下文切换期间的中断
    li     t0, RISCV_MSTATUS_MIE
    csrrc  zero, mstatus, t0

# 保存当前堆栈指针
# OSTCBCurPtr->StkPtr = SP;
    la     t0, OSTCBCurPtr
    lw     t1, 0(t0)
    sw     sp, 0(t1)

# 执行 OS 任务切换钩子
    jal    OSTaskSwHook

...

image

安装中断

想要UCos-III跑起来,主要需要安装两个中断函数,它们都在Ports内,分别是 Software_IRQHandlerSysTick_Handler

  • 由于 SysTick_Handler 和示例Bsp里的中断服务函数重名了,这里直接注释掉 trap_handle.c 里的定义和声明就好,然后给Uos里的实现加个带 __attribute__((interrupt("machine"))); 的声明自动恢复上下文。
  • 在这里需要复位定时器值,不然一直 mtime > mtimecmp 一直触发定时器中断;
  • 然后再修改中断向量表,改成 Software_IRQHandler
void SysTick_Handler(void) __attribute__((interrupt("machine")));
void  SysTick_Handler (void)
{
    mtime_en_ctr(0);//暂停定时器
    mtime_value_set(0);//设置定时器值
    mtime_en_ctr(1);//启动定时器

    CPU_SR_ALLOC();  // 分配存储以保存 CPU 状态寄存器

    CPU_CRITICAL_ENTER();  // 进入临界区
    OSIntEnter();  // 告诉 uC/OS-III 我们正在开始一个 ISR
    CPU_CRITICAL_EXIT();  // 退出临界区

    OSTimeTick();  // 调用 uC/OS-III 的 OSTimeTick() 函数

    OSIntExit();  // 告诉 uC/OS-III 我们正在离开 ISR

}
/*----中断向量表----
入口基地址(trap_vector_tab)可以放在程序存储器的任何地方,但必须32bit对齐,初始化阶段需要写入CSR_mtvec。
每个异常和中断都有独立的trap编码,每个trap编码对应中断向量表的一个32bit表项,每个表项必须按照trap编码在内存上连续分布
发生中断后,跳转至中断向量表的对应表项,即PC = 入口基地址 + trap编码*4
每个表项存放了一条跳转指令,可以跳转至相应的中断服务程序。也可以放一条其他指令,但必须是RV32I指令。 */
trap_vector_tab: 
    j   _start                      /*trap编码 0,保留,软件复位*/
    j   HardFault_Handler           /*trap编码 1,硬件错误异常*/
    j   Software_IRQHandler         /*trap编码 2,软件中断*/

调用系统

  • 配置并启动一下定时器,让它 1ms 中断一次;
  • main 里调用 OSInitOSStart ,顺带创建个任务;
  • 打开时间片轮转调度,注意 OS_CFG_SCHED_ROUND_ROBIN_EN 宏也得开着;
int main()
{
    init_uart0_printf(115200,0);//设置波特率

    uint64_t count;
    mtime_en_ctr(DISABLE);                             //暂停定时器
    trap_en_ctrl(TRAP_TCMP, DISABLE);                  //关闭定时器中断
    count = OS_CFG_TICK_RATE_HZ * system_cpu_freqM;    //计算计数值
    mtimecmp_value_set(count);                         //设置比较值
    mtime_value_set(0);                                //设置定时器值
    mtime_en_ctr(ENABLE);                              //启动定时器

    trap_global_ctrl(ENABLE);                          //打开全局中断
    
    OS_ERR  err;

    OSInit(&err);

    OSSchedRoundRobinCfg(OS_TRUE, 100, &err);          //打开一下时间片轮转调度,设置默认的时间片大小

    // 创建第一个任务
    OSTaskCreate((OS_TCB     *)&AppTaskStartTCB_1,
                (CPU_CHAR   *)"App Task Start1",
                (OS_TASK_PTR )AppTaskStart_1,
                (void       *)0,
                (OS_PRIO     )5,
                (CPU_STK    *)&AppTaskStartStk_1[0],
                (CPU_STK_SIZE)STACK_SIZE/10,
                (CPU_STK_SIZE)STACK_SIZE,
                (OS_MSG_QTY  )0,
                (OS_TICK     )10,
                (void       *)0,
                (OS_OPT      )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
                (OS_ERR     *)&err);
    
    // 创建第二个任务
    OSTaskCreate((OS_TCB     *)&AppTaskStartTCB_2,
                (CPU_CHAR   *)"App Task Start2",
                (OS_TASK_PTR )AppTaskStart_2,
                (void       *)0,
                (OS_PRIO     )5,
                (CPU_STK    *)&AppTaskStartStk_2[0],
                (CPU_STK_SIZE)STACK_SIZE/10,
                (CPU_STK_SIZE)STACK_SIZE,
                (OS_MSG_QTY  )0,
                (OS_TICK     )10,
                (void       *)0,
                (OS_OPT      )(OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR),
                (OS_ERR     *)&err);

    trap_en_ctrl(TRAP_TCMP, ENABLE);                   //打开定时器中断;

    OSStart(&err);
    
    _putchar('E');

    while(1);
}

到这里移植就基本结束了,接下来就是写两个简单的任务。

基础应用

编写两个任务,这里本来用的是小麻雀作者bsp里提供的 printf ,但发现好像需要的栈空间有点大,故直接把里面的 _putchar 拿出来用于打印调试,毕竟中断里加 printf 超长的处理时间会干扰调试所需的结果:

unsigned char mux = 0;

static void AppTaskStart_1(void *p_arg)
{
    mux = 1;
    while(1)
    {
        if(mux == 1)
        {
            _putchar('F');
            mux = 2;
        }
    }
}

static void AppTaskStart_2(void *p_arg)
{
    mux = 2;
    while(1)
    {
        if(mux == 2)
        {
            _putchar('O');
            mux = 1;
        }
    }
}

运行

按照小麻雀作者给出的方式直接综合到FPGA里去,然后烧写即可:

image

基础功能就算是完成了,后续的信号量、互斥量、消息、串口控制台后续再看。

posted @ 2025-10-05 12:24  炽杨  阅读(17)  评论(0)    收藏  举报