fannifu 发表于 2022-6-7 17:21:12

Raspberry-pico SMP调度移植

Raspberry-pico SMP调度移植原文链接:https://club.rt-thread.org/ask/article/6d5e17fcceb168d9.htmlRaspberry pico 是一款双核cortex-m0的处理器,在RT-Thread提供的bsp中目前是默认采用libcpu/arm/cortex-m0,其并没有对多核进行支持。在Coremark的测试中pico的性能很一般,只用一个核心实在是太浪费了,所以下面用一种不太优雅的方式基本实现Pico的SMP,简单测试没有问题,当然由于萌新对于内核的理解程度有限,总是可能存在一些问题,不过总算跑起来了不是~
移植SMP
在这之前,官方的文件完全没有支持Cortex-M的多核。
首先是几个基本的函数对接:

[*]rt_hw_cpu_id:最首先需要实现的一个也是最容易的实现的一个,直接访问pico的sio就可以

1int rt_hw_cpu_id(void)
2{
3    return sio_hw->cpuid;
4}


[*]rt_hw_interrupt_disable/enable: 在SMP框架当中,关闭中断不只是屏蔽中断,其还要通过spinlock来保证对资源访问的互斥,对于此rtt在rthw通过宏定义将其替换,并且重新命名原来的中断控制函数

1#define rt_hw_interrupt_disable rt_cpus_lock
2#define rt_hw_interrupt_enable rt_cpus_unlock
3
4#ifdef RT_USING_SMP
5    #define rt_hw_interrupt_disable rt_hw_local_irq_disable
6    #define rt_hw_interrupt_enablert_hw_local_irq_enable
7#endif


[*]rt_hw_spin_lock_xxx:自旋锁,用于多核之间的资源保护,在rp2040中芯片提供硬件spinlock使用,这一部分同样使用pico-sdk的api即可,选择unsafe版本

1void rt_hw_spin_lock(rt_hw_spinlock_t *lock)
2{
3    spin_lock_unsafe_blocking((spin_lock_t *)lock->slock);
4}
5void rt_hw_spin_unlock(rt_hw_spinlock_t *lock)
6{
7    spin_unlock_unsafe((spin_lock_t *)lock->slock);
8}


[*]rt_hw_secondary_cpu_up:在主CPU启动后,运行调度器,调度器会调用main线程运行,main线程运行前会首先调用该api来启动第二个核心。Rp2040两个核心其实是上电以后同时启动的,CPU-1会在bootrom中被拦截下来进入等待状态,我们可以通过sio的fifo来唤醒第二个核心,pico-sdk中提供了api,可以直接指定CPU-1唤醒后执行的函数。在唤醒过程中同时使能两个CPU的SIO中断,用来进行IPI_Handler.
1void secondary_cpu_c_start(void) // 其中CPU-1,该函数为入口
2{
3    irq_set_enabled(SIO_IRQ_PROC1,RT_TRUE); // 启动该核心的SIO中断,用于IPI
4
5    systick_config(frequency_count_khz(CLOCKS_FC0_SRC_VALUE_ROSC_CLKSRC)*10000/RT_TICK_PER_SECOND); // 配置该核心的systick
6
7    rt_hw_spin_lock(&_cpus_lock);
8
9    rt_system_scheduler_start();
10}
11
12void rt_hw_secondary_cpu_up(void)
13{
14    multicore_launch_core1(secondary_cpu_c_start); // 启动CPU-1
15
16    irq_set_enabled(SIO_IRQ_PROC0,RT_TRUE); // 打开CPU-0的SIO中断
17}

在需要调度的时候,CPU之间可能会互相通知让其进行调度,该部分通过rt_hw_ipi_send和rt_hw_ipi_handler对接, 1#define IPI_MAGIC 0x5a5a
2
3void rt_hw_ipi_send(int ipi_vector, unsigned int cpu_mask)
4{
5    sio_hw->fifo_wr = IPI_MAGIC; // 通知其他CPU调度
6}
7
8// 两个CPU SIO实际执行的部分,用来进行调度和一些其他需要沟通的事情
9void rt_hw_ipi_handler(void)
10{
11    uint32_t status = sio_hw->fifo_st;
12
13    // 清楚中断标志
14    if ( status & (SIO_FIFO_ST_ROE_BITS | SIO_FIFO_ST_WOF_BITS) )
15    {
16      sio_hw->fifo_st = 0;
17    }
18
19    if ( status & SIO_FIFO_ST_VLD_BITS )
20    {
21      if ( sio_hw->fifo_rd == IPI_MAGIC )
22      {
23            rt_schedule(); // 如果正确接受指令,进行调度
24      }
25    }
26}

上面对接的函数都比较基础,其次是对接上下文的汇编代码部分,这一部分就不是特别顺利了。简单梳理一下Cortex-M的调度流程,rt_schedule获取最高优先级的任务然后使能PendSV中断并在全局变量中保存调度信息,最后在完成高优先级中断(或者直接进行PendSV)后进行实际的上下文切换,在SMP中基本同理,但是由于RT-Thread的SMP是针对Cortex-A提供的,这里出现了一些问题。首先在调度中必须关注一个函数,rt_cpus_lock_status_restore(thread),其将要调度的线程绑定到当前的cpu上,调用该函数的位置是一个关键问题 1void rt_cpus_lock_status_restore(struct rt_thread *thread)
2{
3    struct rt_cpu* pcpu = rt_cpu_self();
4
5    pcpu->current_thread = thread; // 绑定CPU到当前核心
6    if (!thread->cpus_lock_nest)   // 用于第一次调度是解锁spinlock
7    {
8      rt_hw_spin_unlock(&_cpus_lock);
9    }
10}

在Cortex-A中其在rt_hw_context_switch中被调用,这对于Cortex-A是可行的,因为在非中断情况下A核会直接进行线程切换而不需要PendSV,但是对于Cortex-M核心放在这个位置会存在下面一个问题:PendSV是中断,所以需要使能中断才能运行,因此在rt_hw_context_switch后立马就有一个rt_hw_interrupt_enable,如果M核工作在非SMP框架下这是没有问题的,但是在SMP框架下当前的线程已经变了,而rt_hw_interrupt_enable是同当前线程绑定的,所以这里会导致CPU的scheduler_lock_nest,cpus_lock_nest错乱,从而导致调度器不能正常工作 1rt_base_t rt_cpus_lock(void)
2{
3    rt_base_t level;
4    struct rt_cpu* pcpu;
5
6    level = rt_hw_local_irq_disable();
7
8    pcpu = rt_cpu_self();
9    if (pcpu->current_thread != RT_NULL)
10    {
11      register rt_ubase_t lock_nest = pcpu->current_thread->cpus_lock_nest;
12
13      pcpu->current_thread->cpus_lock_nest++; // 会锁的nest加在变量上
14      if (lock_nest == 0)
15      {
16            pcpu->current_thread->scheduler_lock_nest++;
17            rt_hw_spin_lock(&_cpus_lock);
18      }
19    }
20
21    return level;
22}

基于上面的描述,我考虑把rt_cpus_lock_status_restore放在PendSV中进行调用,这样就可以保证scheduler_lock_nest工作的正确性,但是导致一个更大的问题!!!在rt_schedule函数中,如果中断还没有使能的情况下重复调用rt_schedule(systick中多层中断)会导致已经被标记为RUNNING的线程无法正常被加入到就绪列表中。因为在上一次的rt_schedule中线程已经被移除了,其等待在PendSV中绑定到当前CPU的时候rt_schedule再次到来,其应该被重新加入到就绪列表(如果优先级低的话),但是schudler是基于当前CPU上的线程来管理的,由于之前被调度的线程当前还没有绑定,所以线程变成游离状态而无法被调度,就会出现下面的情况: 1thread   cpu bind pristatus      sp   stack size max used left tickerror
2-------- --- ---- ---------- ---------- -------------------------- ---
3i-7      0   2   17running 0x000000b4 0x00000200    35%   0x00000014 -02
4i-6      0   2   16running 0x000000b4 0x00000200    35%   0x00000014 -02
5i-5      0   2   15running 0x000000b4 0x00000200    35%   0x00000014 -02
6i-4      0   2   14running 0x000000b4 0x00000200    35%   0x00000014 -02
7i-3      0   2   13running 0x000000b4 0x00000200    35%   0x00000014 -02
8i-2      0   2   12running 0x000000b4 0x00000200    35%   0x00000014 -02
9i-1      0   2   11running 0x000000b4 0x00000200    35%   0x00000014 -02
10i-0      0   2   10running 0x000000b4 0x00000200    35%   0x00000014 -02
11tshell   1   2   20running 0x000000e4 0x00001000    17%   0x0000000a 000
12tsystemN/A   2   30suspend 0x000000b4 0x00000100    73%   0x00000020 000
13tidle1   N/A   1   31ready   0x00000060 0x00000100    37%   0x00000020 000
14tidle0   0   0   31running 0x00000058 0x00000100    34%   0x00000005 000
15main   N/A   2   10suspend 0x000000e8 0x00000800    17%   0x00000014 000

所以rt_cpus_lock_status_restore(thread)只能在rt_hw_context_switch中被调用,但这种情况下我们需要处理scheduler_nest和cpus_lock_nest错乱的问题,由于SMP框架将nest绑定到线程上,但实际上锁针对的还是CPU,我也认为将太绑定到CPU上更合适,为了不修改内核源码的情况下实现,我在rt_hw_context_switch中将当前cpu线程的nest绑定到需要调度的线程上,这样就等价于把nest绑定到CPU上,此时就可以正常工作了。 1struct __thread_switch_status
2{
3    uint32_t    from;
4    uint32_t    to;
5    uint32_t    flag;
6}_thread_switch_array[2];
7
8extern void rt_cpus_lock_status_restore(struct rt_thread *thread);
9
10void thread_switch_status_store(uint32_t from, uint32_t to, rt_thread_t thread)
11{
12    int cpu_id = sio_hw->cpuid;
13
14    if ( _thread_switch_array.flag == 0)
15    {
16      _thread_switch_array.from = from;
17      _thread_switch_array.flag = 1;
18    }
19    _thread_switch_array.to   = to;
20
21    if ( from != 0 )
22    {
23      rt_thread_t currrent_cpu_thread = rt_thread_self();
24      thread->cpus_lock_nest      = currrent_cpu_thread->cpus_lock_nest;
25      thread->scheduler_lock_nest = currrent_cpu_thread->scheduler_lock_nest;
26      thread->critical_lock_nest= currrent_cpu_thread->critical_lock_nest;
27    }
28    rt_cpus_lock_status_restore(thread);
29}


1    // rt_hw_context_switch
2    MOV   R4, LR
3    BL      thread_switch_status_store
4    MOV   LR, R4

解决上述问题后知剩下最后一个问题,我们前文的讨论都是基于非中断情况下的,对于Cortex-M而言中断中的调度和非中断中的调度是一致的,都是基于PendSV实现的,所以我们rt_hw_context_switch,rt_hw_context_interrupt_switch用一套一样的代码就可以,但是在SMP框架中这两个部分具有两个调度函数,在中断中调用rt_schedule,SMP框架会直接跳过当前调度并且给当前CPU打上中断调度标记,最后在离开中断的时候调用rt_scheduler_do_irq_switch(void *context)来实现,对于Cortex-A的中断结构来说这是没有问题的,只要保证switch能够在本次调度过程中直接切换就行,但是对于Cortex-M这样就不太合适,我们可以把NVIC弄成统一IRQ的样子,但是我觉得直接废弃rt_scheduler_do_irq_switch更加合适。 1void rt_schedule()
2{
3    ....
4
5    /* whether do switch in interrupt */
6    if (pcpu->irq_nest)
7    {
8      pcpu->irq_switch_flag = 1;
9      rt_hw_interrupt_enable(level);
10      goto __exit;
11    }
12    ...
13}
14
15void rt_scheduler_do_irq_switch(void *context);

为了使得调度器不知道我们在中断状态,我把rt_interrupt_enter/leave注释掉了(应该在涉及内核调度的中断中全部采用这种办法),这样irq_nest就一直是0,调度器也不会去调用do_irq了,其实我们不用这个处理方法也能够工作的,但是中断中就没法调度了,实时性也没法保障。按照我的理解在Cortex-M中这样的处理并不会有太大的问题,但是总不太好是吧~ 1void isr_systick(void)
2{
3    /* enter interrupt */
4    //rt_interrupt_enter();
5
6    rt_tick_increase();
7
8    /* leave interrupt */
9    //rt_interrupt_leave();
10}

最后基于上面全部的修改,RP2040的SMP能够正常工作,小灯能够按照正常闪烁。 1 \ | /
2- RT -   Thread Operating System
3 / | \   4.1.1 build May1 2022 20:00:57
4 2006 - 2022 Copyright by RT-Thread team
5Hello, RT-Thread!
6msh >ps
7thread   cpu bind pristatus      sp   stack size max used left tickerror
8-------- --- ---- ---------- ---------- -------------------------- ---
9i-7      N/A   2   17suspend 0x000000b4 0x00000200    35%   0x00000012 000
10i-6      N/A   2   16suspend 0x000000b4 0x00000200    35%   0x00000014 000
11i-5      N/A   2   15suspend 0x000000b4 0x00000200    35%   0x00000014 000
12i-4      N/A   2   14suspend 0x000000b4 0x00000200    35%   0x00000014 000
13i-3      N/A   2   13suspend 0x000000b4 0x00000200    35%   0x00000013 000
14i-2      N/A   2   12suspend 0x000000b4 0x00000200    35%   0x00000014 000
15i-1      N/A   2   11suspend 0x000000b4 0x00000200    35%   0x00000014 000
16i-0      N/A   2   10suspend 0x00000094 0x00000200    35%   0x00000014 000
17tshell   1   2   20running 0x000002dc 0x00001000    17%   0x00000009 000
18tsystemN/A   2   30suspend 0x000000b4 0x00000100    73%   0x00000020 000
19tidle1   N/A   1   31ready   0x00000060 0x00000100    37%   0x00000020 000
20tidle0   0   0   31running 0x00000058 0x00000100    34%   0x0000000f 000
21main   N/A   2   10suspend 0x000000e8 0x00000800    17%   0x00000014 000
22msh >

最后
我对于RT-Thread的理解还很有限,萌新,有很多问题我可能预料不到,这样的实现方式我也觉得不太优雅,不过总算是跑起来了(肝了两天还是有点累emmm)。后续会优化整理并且再经过一段时间的测试,或许能够喜提自己的第一个RT-Thread PR ~最后是关于SMP,我不明白为什么把nest绑定到thread而不是cpu上,因为总还是在锁cpu,其次rt-thread的smp似乎是专门给A核设计,目前的多核MCU也有蛮多,希望可以提供一些相关支持。Attention please!!(2022-5-12): 评论区有提供bsp的压缩包只是一个可以玩玩的状态,目前可以确定的是存在和调度相关的bug会导致系统崩溃(目前测试在shell反复调用list_thread可能崩溃,可能和kservice有关)。<u>另外如果线程在调度器启动前被创建,即INIT_BORAD_EXPORT方式创建则一切正常(list_thread不会崩溃),在main中创建就可能出现崩溃</u>,希望各位大佬可以给点调试思路。</u>由于最近事情很多比较忙绿,没有时间调试和阅读代码,但会在后续一段时间(六月-七月)调试完善smp调度并在后续添加完善pico的驱动支持,希望感兴趣的同学一起交流哈。(也在看看rtthread v5.0的消息哈)

页: [1]
查看完整版本: Raspberry-pico SMP调度移植