双核Cortex-A7 rt-thread移植笔记

发布于 2021-04-10 17:34:42

Cortex-A7 rt-thread移植笔记

前言

公司安排在双核Cortex-A7平台上移植rt-thread,并测试在开启SMP后测试一下各项性能。
从拿到这个任务开始,到移植结束用了一个多月,超过了预期的时间。主要还是一些原理没有弄清楚,如双核SMP的运行原理、cp15协处理的各项配置等。还有被以前的经验所误导,认知也不够,耽误了蛮久的时间。下面就听我聊一下,这一个月的踩坑记录。

软硬件介绍

  • CPU:双核Coretx-A7 + Cortex-M33的多核异构芯片
  • Cortex-M33:已移植好了rt-thread,相关的硬件外设由Cortex-M33负责初始化。
  • Cortex-M33负责启动Cortex-A7

目标

  • 支持浮点 FPU
  • 支持 NEON
  • 支持MMU
  • rt-thread 开启SMP能正常运行
  • coremark、内存读写等跑分测试达到相同等级硬件平台的水平

移植步骤

一、参考合适的BSP

  1. 选择参考bsp目录下qemu-vexpress-a9的代码,因为cortex-A9体和cortex-A7差异不大,而且这个bsp默认开启了SMP,对移植有一定的价值。
  2. 这个bsp 是在qemu下运行的,外设相关的依赖比较少,可以避免一些坑。

二、编译运行

  1. 找到参考的bsp后,首先要修改link.lds脚本中的链接地址,因为每个平台的代码运行空间不一样。
将. = 0x60010000; 
修改为. = 0x3c000000;  //这个地址类似于STM32H750的0x08000000
  1. MMU配置,MMU的初始化大佬们已经整好了,我只需要在board.c中改一下页表配置,将原来qemu A9的改成现在平台的
struct mem_desc platform_mem_desc[] = {
    {0x00000000,  0xFFFFFFFF-1,  0x00000000,   NORMAL_MEM},
    {0x50000000, 0x50300000-1, 0x50000000, DEVICE_MEM},  // SRAM
    {0x3C000000, 0x3C800000-1, 0x3C000000, NORMAL_MEM}, // PSRAM 代码空间
    {0x40000000, 0x40100000-1, 0x40000000, DEVICE_MEM}, // peripheral 外设空间
};
  1. 添加打印日志的代码,开始采用写共享内存,由M33轮训并输出的方式,事实证明这种方式没有串口输出更直接,对调试效率造成了一定的影响, 所以一种好用得调试手段很重要。如果有JTAG或者SWD这种接口,就先把这种调试环境搭建起来,不要想偷懒,人生没有捷径。因为后面遇到棘手的问题,这些高级的调试手段,会事半功倍。
  2. 去除掉一些和外设相关的代码,编译运行(满怀欢喜的等待),结果没跑起来,继续查原因,这里折腾了一周多。

三、踩坑过程

本来想着大佬们已经把体系相关的整好了,如GIC初始化、中断的压栈,进程调度等,到我这里就是简单的改改,事实证明,工资不是那么好拿的,需要掉点头发。

1. 通过日志查看,代码能运行到rtthread_startup中,但是有个很诡异的情况。增加或者删除日志打印的代码后,有时能进入,有时候不能,这玩意颠覆认知啊。事实证明这世界没有鬼的,这种问题一般都是和硬件有关,经过一周多的时间终于发现了,原来我手上的cortex-a7双核启动与qemu-vexpress-a9不一样。
  • qemu-vexpress-a9启动流程

qemu-vexpress-a9.png

关键代码实现:

void rt_hw_secondary_cpu_up(void)
{
    extern void set_secondary_cpu_boot_address(void);

    set_secondary_cpu_boot_address();
    __asm__ volatile ("dsb":::"memory");
    rt_hw_ipi_send(0, 1 << 1);
}

void secondary_cpu_c_start(void)
{
    rt_hw_vector_init();

    rt_hw_spin_lock(&_cpus_lock);

    arm_gic_cpu_init(0, REALVIEW_GIC_CPU_BASE);
    arm_gic_set_cpu(0, IRQ_PBA8_TIMER0_1, 0x2);

    timer_init(0, 10000);
    rt_hw_interrupt_install(IRQ_PBA8_TIMER0_1, rt_hw_timer2_isr, RT_NULL, "tick");
    rt_hw_interrupt_umask(IRQ_PBA8_TIMER0_1);

    rt_system_scheduler_start();
}
  • 友商双核cortex-A7启动流程

cortex-a7启动.png

从启动流程可以看出,我手上的这个cortex-a7在启动时,两个核心启动时是运行的同一份代码,导致了整个代码运行全乱了,出现了很多诡异的现象,所以需要修改启动代码。

  • 修改启动代码,插入对cpu 编号的判断
    //读取cpu编号
    @ get cpu id, and subtract the offset from the stacks base address
    MRC     P15, 0, R5, C0, C0, 5               @ read multiprocessor affinity register
    AND     R5, R5, #3                          @ mask off, leaving CPU ID field

    CMP     R5, #0 
    BEQ     normal_setup   //CPU0 跳转到正常启动流程
    
    //下面是cpu1的流程
#ifdef RT_USING_SMP
    ldr r0, =secondary_cpu_entry
    mov r1, #0
    str r1, [r0] /* clean secondary_cpu_entry */
#endif /* RT_USING_SMP */

secondary_loop:
    @ cpu core 1 goes into sleep until core 0 wakeup it
    wfe    //休眠,等待cpu0 发送sev事件
#ifdef RT_USING_SMP
    ldr r1, =secondary_cpu_entry
    ldr r0, [r1]
    cmp r0, #0
    //跳转到CPU0设置的入口地址
    blxne r0 /* if(secondary_cpu_entry) secondary_cpu_entry(); */
#endif /* RT_USING_SMP */
    b secondary_loop
2. 解决完了启动问题后,每次都能正常进入到rt_hw_board_init中了,这个函数一般在board.c中实现,每个芯片会有一些差异。
/**
 * This function will initialize beaglebone board
 */
void rt_hw_board_init(void)
{
    /* initialize hardware interrupt */
    rt_hw_interrupt_init();
    /* initialize system heap */
    rt_system_heap_init(heap_start, &heap_start[heap_len/sizeof(uint32_t)]);
    //rt_system_heap_init(HEAP_BEGIN, HEAP_END);
    rt_components_board_init();
    //SystemClock_Init();
    rt_console_set_device(RT_CONSOLE_DEVICE_NAME);

    rt_thread_idle_sethook(idle_wfi);
    rt_hw_systick_init(RT_TICK_PER_SECOND);
#ifdef RT_USING_SMP
    /* install IPI handle */
    rt_hw_ipi_handler_install(RT_SCHEDULE_IPI, rt_scheduler_ipi_handler);
#endif
}

结果在调用rt_hw_interrupt_init 时挂了,继续加日志打印呗。

/**
 * This function will initialize hardware interrupt
 */
void rt_hw_interrupt_init(void)
{
    rt_uint32_t gic_cpu_base;
    rt_uint32_t gic_dist_base;
    rt_uint32_t gic_irq_start;

    /* initialize vector table */
    rt_hw_vector_init();

    /* initialize exceptions table */
    rt_memset(isr_table, 0x00, sizeof(isr_table));

    /* initialize ARM GIC */
    gic_dist_base = platform_get_gic_dist_base();
    gic_cpu_base = platform_get_gic_cpu_base();

    gic_irq_start = GIC_IRQ_START;

    arm_gic_dist_init(0, gic_dist_base, gic_irq_start);
    arm_gic_cpu_init(0, gic_cpu_base);
}

日志显示,是在arm_gic_dist_init(0, gic_dist_base, gic_irq_start); 这句代码挂了,是访问了GIC控制器的基地址,等等GIC控制器基地址不应该都是一样的吗,我的第一反应这些属于arm的应该都一样。既然挂了,就得查资料, 最后在Cortex-A7 MPCore Technical Reference Manual中看到描述。
*
The GIC registers are memory-mapped, and the base address is specified by PERIPHBASE[39:15]. This input must be tied to a constant value. The PERIPHBASE value is sampled during reset into the Configuration Base Address (CBAR) for each processor in the cluster. See Configuration Base Address Register on page 4-83.

果然这里有毒,需要读取CBAR寄存器获取GIC基地址,bsp引用的是realview.h中定义好的。

#define REALVIEW_GIC_CPU_BASE       0x1E000100  /* Generic interrupt controller CPU interface */
#define REALVIEW_GIC_DIST_BASE      0x1E001000  /* Generic interrupt controller distributor */

CBAR寄存器是通过CP15协处理访问,具体指令如下:

MRC p15, 4, r0, c15, c0, 0 ;

读出来果然不是0x1E000100,更新GIC基地址后能过了。到这里两个周过去了,emmm,忍无可忍了,我终于把串口打印整上了,出现异常是又可以看到熟悉的打印了,真香。

3. 到这里串口能看到熟悉的shell了。
 \ | /
- RT -     Thread Operating System
 / | \     4.0.3 build Apr 10 2021
 2006 - 2021 Copyright by rt-thread team
hello rt-thread
msh>
4. 前面都是在单核下运行的,现在使能SMP。惴惴不安,使能SMP后,main线程不运行,代码如下:
int main(void)
{
    int count = 0;
    printf("hello rt-thread\n");
    rt_kprintf("a7 mian up %s\r\n", __TIME__);

    while (1)
    {
        rt_kprintf("a7 mian loop %d\r\n", count);
        count++;
        rt_thread_delay(1000);
        if(count > 10000)
        {
            break;
        }
    }

    return 0;
}

 \ | /
- RT -     Thread Operating System
 / | \     4.0.3 build Apr 10 2021
 2006 - 2021 Copyright by rt-thread team
hello rt-thread
a7 mian up 06:1s: 3>
a7 mian loop 0

没有实现1s打印一次的效果,直觉是中断没有触发,卡死在rt_thread_delay中了。经过几天的排查,发现在mmu配置忘记把定时器相关的地址段配置成设备内存属性,导致定时器有时候没有初始化成功。
细节很重要, 这个问题在移植前期MMU配置就有异常,粗心把问题放过去了,后面导致涉及的点太多,排查了很久。

  • 内存段宏定义
#define DESC_SEC       (0x2)
#define MEMWBWA        ((1<<12)|(3<<2))     /* write back, write allocate */
#define MEMWB          (3<<2)  /* write back, no write allocate */
#define MEMWT          (2<<2)  /* write through, no write allocate */
#define SHAREDEVICE    (1<<2)  /* shared device */
#define STRONGORDER    (0<<2)  /* strong ordered */
#define XN             (1<<4)  /* eXecute Never */
#define AP_RW          (3<<10) /* supervisor=RW, user=RW */
#define AP_RO          (2<<10) /* supervisor=RW, user=RO */
#define SHARED         (1<<16) /* shareable */

#define DOMAIN_FAULT   (0x0)
#define DOMAIN_CHK     (0x1)
#define DOMAIN_NOTCHK  (0x3)
#define DOMAIN0        (0x0<<5)
#define DOMAIN1        (0x1<<5)

#define DOMAIN0_ATTR   (DOMAIN_CHK<<0)
#define DOMAIN1_ATTR   (DOMAIN_FAULT<<2)

/* device mapping type */
#define DEVICE_MEM     (SHARED|AP_RW|DOMAIN0|SHAREDEVICE|DESC_SEC|XN)
/* normal memory mapping type */
#define NORMAL_MEM     (SHARED|AP_RW|DOMAIN0|MEMWBWA|DESC_SEC)
  • mmu页表配置, rt_hw_init_mmu_table初始化时使用
struct mem_desc platform_mem_desc[] = {
    {0x00000000,  0xFFFFFFFF-1,  0x00000000,   NORMAL_MEM},
    {0x50000000, 0x50300000-1, 0x50000000, DEVICE_MEM},  // SRAM
    {0x3C000000, 0x3C800000-1, 0x3C000000, NORMAL_MEM},  // PSRAM
    {0x40000000, 0x40100000-1, 0x40000000, DEVICE_MEM},  // peripheral
    {0x58000000, 0x58100000-1, 0x58000000, DEVICE_MEM},  // 定时器地址空间的内存描述
    {0x58300000, 0x58400000-1, 0x58300000, DEVICE_MEM}, 
};

经过增加0x58000000 定时器外设基地址的的描述后,在每次都能稳定运行了。

5. neon使能

a7是需要跑算法的,所以neon必须打开,将rtconfig.py编译参数修改下。

  • qemu A9 BSP中的原参数
import os

DEVICE = ' -march=armv7-a -marm -msoft-float'
  • 使能neon后的参数
import os

DEVICE = ' -march=armv7-a -mtune=cortex-a7 -mfpu=neon-vfpv4 -ftree-vectorize -mfloat-abi=softfp -ffunction-sections -fdata-sections '
Cortex-A7
-mcpu=cortex-a7
-mfpu=vfpv4
-mfpu=vfpv4-d16
-mfpu=neon-vfpv4  // 使能neon与vfp编译
-fp16表明支持16bit半精度浮点操作

运行就炸了,哎,仰天长叹。

 \ | /
- RT -     Thread Operating System
 / | \     4.0.3 build Apr  6 2021
 2006 - 2020 Copyright by rt-thread team
undefined instruction:
Execption:
r00:0x00000000 r01:0x3c0447a8 r02:0x3c03bbac r03:0x3c0446dc
r04:0xdeadbeef r05:0xdeadbeef r06:0xdeadbeef r07:0xdeadbeef
r08:0xdeadbeef r09:0xdeadbeef r10:0xdeadbeef
fp :0x3c044704 ip :0xfefefeff
sp :0x3c0446d8 lr :0x3c002228 pc :0x3c0360bc
cpsr:0x80000013
thread   pri  status      sp     stack size max used left tick  error
-------- ---  ------- ---------- ----------  ------  ---------- ---
sys_work  23  ready   0x00000044 0x00000800    03%   0x0000000a 000
mmcsd_de  22  ready   0x00000044 0x00000400    06%   0x00000014 000
tidle0    31  ready   0x00000048 0x00000400    07%   0x00000020 000
timer      4  suspend 0x0000007c 0x00000400    12%   0x0000000a 000
main      10  running 0x00000044 0x00000800    15%   0x00000014 000
shutdown...

反汇编看看挂掉的地方是啥,反汇编关键代码:

3c0360a8 <rt_soft_rtc_init>:
3c0360a8:    e92d4800     push    {fp, lr}
3c0360ac:    e28db004     add    fp, sp, #4
3c0360b0:    e24dd028     sub    sp, sp, #40    ; 0x28
3c0360b4:    e24b3028     sub    r3, fp, #40    ; 0x28
3c0360b8:    f2c00050     vmov.i32    q8, #0    ; 0x00000000   //大致在这个位置挂掉了
3c0360bc:    f4430a0f     vst1.8    {d16-d17}, [r3]
3c0360c0:    f26021b0     vorr    d18, d16, d16
3c0360c4:    edc32b04     vstr    d18, [r3, #16]
3c0360c8:    f26021b0     vorr    d18, d16, d16
3c0360cc:    edc32b06     vstr    d18, [r3, #24]
3c0360d0:    edc30b07     vstr    d16, [r3, #28]

pc :0x3c0360bc 向上查找可疑地方(arm流水线的原因PC值总比执行的值大一些),看到3c0360b8处的代码是一个vmov指令,带V开头的是neon与fpu的指令。这里证明neon编译参数是OK的,那就是系统配置选项可能没开,看了下RT_USING_FPU定义了。

  • 日志显示进入未定义指令中断了,去看看
void rt_hw_trap_undef(struct rt_hw_exp_stack *regs)
{
#ifdef RT_USING_FPU
    {
        uint32_t ins;
        uint32_t addr;

        if (regs->cpsr & (1 << 5))
        {
            /* thumb mode */
            addr = regs->pc - 2;
            ins = (uint32_t)*(uint16_t*)addr;
            if ((ins & (3 << 11)) != 0)
            {
                /* 32 bit ins */
                ins <<= 16;
                ins += *(uint16_t*)(addr + 2);
            }
        }
        else
        {
            addr = regs->pc - 4;
            ins = *(uint32_t*)addr;
        }
        if ((ins & 0xe00) == 0xa00)
        {
            /* float ins */
            uint32_t val = (1U << 30);

            asm volatile ("vmsr fpexc, %0"::"r"(val):"memory");
            regs->pc = addr;
            return;
        }
    }
#endif
    rt_kprintf("undefined instruction:\n");
    rt_hw_show_register(regs);
#ifdef RT_USING_FINSH
    list_thread();
#endif
    rt_hw_cpu_shutdown();
}

这段代码我找大佬问了下,大佬解释说这是rt-thread为了节约栈空间和压栈的时间,默认没开FPU,当产生异常时通过指令判断是不是neon或者vfp指令造成的异常,是就开启FPU,没有用到neon和vfp的任务就不会开启,确实是一个很巧妙的操作。

3c0360b8:    f2c00050     vmov.i32    q8, #0    ; 0x00000000 //异常的指令
//指令判断条件
if ((ins & 0xe00) == 0xa00)
{
    /* float ins */
    uint32_t val = (1U << 30);  //使能FPU

    asm volatile ("vmsr fpexc, %0"::"r"(val):"memory");
    regs->pc = addr;
    return;
}

通过对比原判段条件不能覆盖到所有的neon和fpu指令,所以直接改成判断FPU是否开启,如果开启过还有异常,那就是真的未定义指令。优化后的代码:

uint32_t val;
asm volatile ("vmrs %0, fpexc" : "=r"(val)::"memory");

if (!(val & 0x40000000))
{
    /* float ins */
    val = (1U << 30);

    asm volatile ("vmsr fpexc, %0"::"r"(val):"memory");
    regs->pc = addr;
    return;
}

修改完毕后能正常启动了。

6. 内存性能测试

基础系统的移植完成后,还需要性能测试保证移植的质量,这里又搞了接近两周。

  • 内存读写测试,使用的是软件包中的MemoryPerf
  • 因为开始SMP使能后有问题,第一轮测试时没有开启SMP,测试结果为。
平台8bit读写(M/s)16bit读写(M/s)32bit读写(M/s)
其他rtos同平台303.9 / 725.6533.6 / 765.4595.8 / 765.4
rt-thread5.3 / 15.910.6 / 31.921.2 / 63.7

与相同平台的其他rtos的测试结果对比差距很大,没办法只能对比原厂bsp,无数次测试后没有效果。最后在cortex-A7的手册中发现,开启SMP是使能dcache的前提条件,然后开启SMP。

mrc p15, 0, r1, c1, c0, 1
mov r0, #(1<<6)
orr r1, r0
mcr p15, 0, r1, c1, c0, 1 //enable smp
平台SMP8bit读写(M/s)16bit读写(M/s)32bit读写(M/s)
其他rtos同平台ON303.9 / 725.6533.6 / 765.4595.8 / 765.4
rt-threadON140.8 / 144.9268.9 / 290.1515.3 / 579.3
rt-threadOFF5.3 / 15.910.6 / 31.921.2 / 63.7

开启后性能提升20多倍,但还有差距,原因后面慢慢到来。现在有点查不动了,跑个coremark。

7. coremark跑分测试

coremark跑分也是使用的软件包的工具Coremark,居然还有这么多小工具,不用自己撸了,对rt-thread爱意增加100分。

  • rt-thread跑分结果
Benchmark started, please make sure it runs for at least 10s.

2K performance run parameters for coremark.
CoreMark Size    : 666
Total ticks      : 16865
Total time (secs): 16
Iterations/Sec   : 625
Iterations       : 10000
Compiler version : GCC5.4.1 20160919 (release) [ARM/embedded-5-branch revision 240496]
Compiler flags   : 
Memory location  : STACK
seedcrc          : 0xe9f5
[0]crclist       : 0xe714
[0]crcmatrix     : 0x1fd7
[0]crcstate      : 0x8e3a
[0]crcfinal      : 0x988c
Correct operation validated. See README.md for run and reporting rules.
CoreMark 1.0 : 625 / GCC5.4.1 20160919 (release) [ARM/embedded-5-branch revision 240496]  / STACK
msh />

结果625分,总感觉哪里不对,去coremark网站看看其他的平台跑分,居然还没有stm32H7的跑分高,瑟瑟发抖,感觉无法说服领导了。还是需要对比下,在同平台的其他rtos也跑一下。

Benchmark started, please make sure it runs for at least 10s.
2K performance run parameters for coremark.
CoreMark Size    : 666
Total ticks      : 35038
Total time (secs): 35
Iterations/Sec   : 2857
Iterations       : 100000
Compiler version : GCC5.4.1 20160919 (release) [ARM/embedded-5-branch revision 240496]
Compiler flags   : 
Memory location  : STACK
seedcrc          : 0xe9f5
[0]crclist       : 0xe714
[0]crcmatrix     : 0x1fd7
[0]crcstate      : 0x8e3a
[0]crcfinal      : 0xd340
Correct operation validated. See README.md for run and reporting rules.
CoreMark 1.0 : 2857 / GCC5.4.1 20160919 (release) [ARM/embedded-5-branch revision 240496]

2875分,这个分数就很正常了,下面就查原因了,这时第4周已经要结束了。

  • 原因分析: 这时候系统已经能稳定运行,只是性能比较差,那是不是CPU主频设置不对呢?接下来询问原厂的兄弟,等待回复中。
  • 这时不能闲着,反汇编代码看看bsp在cortex-a7里面干了什么,屏蔽掉时钟部分的设置,最后将测试代码提前到汇编启动代码中,测出来分数还是2857分。完犊子了,路子完全走错了。
  • 最后想到,会不会是代码优化等级的问题,现在用的是-o0编译,改成-o2整一把,跑分2941分了。没想到优化等级会造成5倍差距,认知不够,被领导狠批了一顿(两周前就叫看看的)。
Benchmark started, please make sure it runs for at least 10s.

2K performance run parameters for coremark.
CoreMark Size    : 666
Total ticks      : 17287
Total time (secs): 17
Iterations/Sec   : 2941
Iterations       : 50000
Compiler version : GCC5.4.1 20160919 (release) [ARM/embedded-5-branch revision 240496]
Compiler flags   :
Memory location  : STACK
seedcrc          : 0xe9f5
[0]crclist       : 0xe714
[0]crcmatrix     : 0x1fd7
[0]crcstate      : 0x8e3a
[0]crcfinal      : 0xa14c
Correct operation validated. See README.md for run and reporting rules.
CoreMark 1.0 : 2941 / GCC5.4.1 20160919 (release) [ARM/embedded-5-branch revision 240496]  / STACK
  • rt-thread 编译等级的修改在rtconfig.py,设置BUILD = 'release'就可以了。
import os

BUILD = 'debug'

# generate debug info in all cases
AFLAGS += ' -gdwarf-2'
CFLAGS += ' -g -gdwarf-2'

if BUILD == 'debug':
    CFLAGS += ' -O0'
else:
    CFLAGS += ' -O2'
7. 收尾

上面还留了一个小尾巴,内存性能测试有差距,最后在开启SMP, -O2编译的情况下测试一下。

平台SMP优化等级8bit读写(M/s)16bit读写(M/s)32bit读写(M/s)
其他rtos同平台ON-o2303.9 / 725.6533.6 / 765.4595.8 / 765.4
rt-threadON-o2304.8 / 725.7536.4 / 765.4606.1 / 765.4
rt-threadON-o0140.8 / 144.9268.9 / 290.1515.3 / 579.3
rt-threadOFF-o05.3 / 15.910.6 / 31.921.2 / 63.7

内存读写也正常了,到现在移植完成,所有的指标完成。

总结

  1. 必要的调试工具是不可少的,会缩短解决问题的时间,比如调时序就要上逻辑分析仪,调代码就要整GDB这些。
  2. 调试过程中要做记录,将可能项列成表格,最后逐个排查,免得后面都晕菜了,不知道调了啥,这个要被吊的。
  3. 养成良好的工作习惯,多学习计算机原理,学会分析问题。
10 条评论

发布
问题