返回列表 发新帖我要提问本帖赏金: 50.00元(功能说明)

[嵌入式linux] RA8D1B-CPKCOR开发板移植linux -- 第一个在Cortex-M85上跑linux的憨憨出品

[复制链接]
 楼主| xhackerustc 发表于 2024-10-21 20:59 | 显示全部楼层 |阅读模式
<
#申请原创#
@21小跑堂

国庆前有一块RA生态工作室出品的RA8D1B-CPKCOR开发板到了,主控瑞萨RA8D1B MCU,基于Arm Cortex-M85,支持Helium 和 TrustZone,CPU频率高达480MHZ。似乎是世界第一款使用Cortex-M85的MCU?RA8D1B集成2MB flash和1MB的SRAM,I/D-cache 32KB。集成一堆丰富外设主控,比如usb、eth、i2c、i3c、spi、sdhci、ospi、canfd、MIPI-DSI、CEU摄像头、2D图形、TFT显示控制等等,MCU功能已经非常强大。更有意思的是RA8D1B-CPKCOR板载了32MB SDRAM,16MB的QSPI flash,看到这些尤其是32MB的SDRAM笔者觉得这板子足够跑linux系统了,虽然Cortex-M85无MMU,但主线linux早已经支持NOMMU了。要说生态没有哪个RTOS能和linux比生态,在不要求硬实时的应用场景中利用linux系统的丰富生态能大大降低软件工作量。一个复杂MCU平台的软件工作量非常大,特别是集成M7、M85这样高性能的core(M7 coremark/MHZ超过A7)的MCU,外设通常也是比较丰富的,大多数系统还板载了SDRAM或PSRAM,如果再以裸机思维去做软件,软件复杂度大到不可想象。而RTOS的生态因种种原因远远不如linux。

那咱们就开始RA8D1B移植linux的旅程了:

bootloader初始化SDRAM

初始化sdram,其实RA生态工作室其实做好了,直接把他们的board_sdram.c和board_sdram.h拷贝过来即可。这一步最好做下内存读写的压力测试,我没用他们的测试code,自己写了一份,为啥呢:linux下读写内存1字节、2字节、4字节都可能的,我想都测到,测试代码如下:
  1. #define SDRAM_BASE_ADDRESS (0x68000000U)

  2. static void sdram_test(void)
  3. {
  4.         int i;
  5.         uint32_t addr;
  6.         uint32_t t1;
  7.         uint8_t testbuf[64];
  8.         void *p = (void *)SDRAM_BASE_ADDRESS;

  9.         APP_PRINT("Writing SDRAM...\n");
  10.         printf("Writing SDRAM...\n");
  11.         for (addr = 0; addr < 32 * 1024 * 1024; ++addr) {
  12.                 uint8_t data = addr & 0xff;
  13.                 *(uint8_t *)(p + addr) = data;
  14.         }

  15.         APP_PRINT("Reading SDRAM...\n");
  16.         printf("Reading SDRAM...\n");
  17.         for (addr = 0; addr < 32 * 1024 * 1024; ++addr) {
  18.                 uint8_t data, expected;

  19.                 expected = addr & 0xff;
  20.                 data = *(uint8_t *)(p + addr);
  21.                 if (data != expected)
  22.                         APP_PRINT("SDRAM 8bit read failed at %lx (%x != %x)\n", addr, data, expected);
  23.         }
  24.         APP_PRINT("SDRAM 8bit read pass.\n");
  25.         printf("SDRAM 8bit read pass.\n");

  26.         for (addr = 0; addr < 32 * 1024 * 1024; addr += 2) {
  27.                 uint16_t data, expected;
  28.                 expected = (((addr + 1) & 0xff) << 8) |
  29.                            (addr & 0xff);
  30.                 data = *(uint16_t *)(p + addr);
  31.                 if (data != expected)
  32.                         APP_PRINT("SDRAM 16bit read failed at %lx (%x != %x)\n", addr, data, expected);
  33.         }
  34.         APP_PRINT("SDRAM 16bit read pass.\n");
  35.         printf("SDRAM 16bit read pass.\n");

  36.         for (addr = 0; addr < 32 * 1024 * 1024; addr += 4) {
  37.                 uint32_t data, expected;
  38.                 expected = (((addr + 3) & 0xff) << 24) |
  39.                            (((addr + 2) & 0xff) << 16) |
  40.                            (((addr + 1) & 0xff) << 8) |
  41.                            (addr & 0xff);
  42.                 data = *(uint32_t *)(p + addr);
  43.                 if (data != expected)
  44.                         APP_PRINT("SDRAM 32bit read failed at %lx (%lx != %lx)\n", addr, data, expected);
  45.         }
  46.         APP_PRINT("SDRAM 32bit read pass.\n");
  47.         printf("SDRAM 32bit read pass.\n");

  48.         memset(testbuf, 0x5a, sizeof(testbuf));
  49.         DWT_init();
  50.         t1 = DWT_get_count();
  51.         for (i = 0; i < 10000; ++i) {
  52.                 memcpy(p, testbuf, 64);
  53.         }
  54.         t1 = DWT_get_count() - t1;
  55.         t1 = DWT_count_to_us(t1);
  56.         t1 /= 1000;
  57.         APP_PRINT("SDRAM write speed: %ld B/s.\n", 64 * 10000 * 1000 / t1);
  58.         printf("SDRAM write speed: %ld B/s.\n", 64 * 10000 * 1000 / t1);

  59.         t1 = DWT_get_count();
  60.         for (i = 0; i < 10000; ++i) {
  61.                 memcpy(testbuf, p, 64);
  62.         }
  63.         t1 = DWT_get_count() - t1;
  64.         t1 = DWT_count_to_us(t1);
  65.         t1 /= 1000;
  66.         APP_PRINT("SDRAM read speed: %ld B/s.\n", 64 * 10000 * 1000 / t1);
  67.         printf("SDRAM read speed: %ld B/s.\n", 64 * 10000 * 1000 / t1);

  68.         for (;;) {
  69.                 APP_PRINT("SDRAM test done\n");
  70.                 printf("SDRAM test done\n");
  71.                 __WFI();
  72.         }
  73. }



这一步其实还有用FSP配置并生成项目工程,本人因习惯于Linux下开发,所以生成的是CMake工程。这一步网上测评很多铺天盖地,且FSP使用并非本次主目标,这里不再赘述。

bootloader加载并跳转linux内核入口
加载内核image、dtb等可以从sd卡加载也可以从flash上加载,但bootloader不是本次主目标,能否偷懒呢?不要忘记板载的jlink,完全可以用jlink把内核image和dtb加载到SDRAM指定位置,jlink加载命令语法如下:

  1. loadfile FILE address noreset


比如
  1. loadfile /tmp/Image.bin 0x68008000 noreset


noreset意思是只加载后不要reset目标板,jlink loadfile默认是reset的。

加载解决了,跳转内核入口呢?easy,使用jlink的wreg命令即可,语法如下:
  1. wreg rN value


不过这里碰到jlink一个比较搞笑的地方,它命令行里32位arm的pc寄存器,你写r15或pc都不行,必须是"R15 (PC)"才行,比如
  1. wreg "R15 (PC)" 0x68008000

建议segger这里做下简单更新,方便用户,毕竟打双引号再空格再括号相当繁琐。

linux cortex-m85的支持补丁
32位arm对每款cpu都有一个所谓proc_info的结构体,成员包含cpu setup函数,cache操作函数,hwcaps变量等等,cortex-m85呢笔者做这个事情的时候主线linux中还没有这个结构体。经阅读cortex-m85 TRM可以知道就上述操作函数等来讲它和cortex-m55是兼容的,所以笔者打了个补丁如下:
  1. --- a/arch/arm/mm/proc-v7m.S
  2. +++ b/arch/arm/mm/proc-v7m.S
  3. @[url=home.php?mod=space&uid=72445]@[/url] -194,6 +194,16 @[url=home.php?mod=space&uid=72445]@[/url] ENDPROC(__v7m_setup)
  4.         .long   \cache_fns
  5. .endm

  6. +       /*
  7. +        * Match ARM Cortex-M85 processor.
  8. +        */
  9. +       .type   __v7m_cm85_proc_info, #object
  10. +__v7m_cm85_proc_info:
  11. +       .long   0x410fd230              /* ARM Cortex-M85 0xD23 */
  12. +       .long   0xff0ffff0              /* Mask off revision, patch release */
  13. +       __v7m_proc __v7m_cm85_proc_info, __v7m_cm7_setup, hwcaps = HWCAP_EDSP, cache_fns = v7m_cache_fns, proc_fns = cm7_processor_functions
  14. +       .size   __v7m_cm85_proc_info, . - __v7m_cm85_proc_info
  15. +
  16.         /*
  17.          * Match ARM Cortex-M55 processor.
  18.          */

fix一个exc_ret的bug
不多说了看代码即可
  1. +++ b/arch/arm/include/asm/v7m.h
  2. @[url=home.php?mod=space&uid=72445]@[/url] -51,6 +51,7 @@
  3.   */
  4. #define EXC_RET_STACK_MASK                     0x00000004
  5. #define EXC_RET_THREADMODE_PROCESSSTACK                (3 << 2)
  6. +#define EXC_RET_FTYPE                          (1 << 4)

  7. /* Cache related definitions */

  8. diff --git a/arch/arm/mm/proc-v7m.S b/arch/arm/mm/proc-v7m.S
  9. index ed7781c84341..fdae077d2654 100644
  10. --- a/arch/arm/mm/proc-v7m.S
  11. +++ b/arch/arm/mm/proc-v7m.S
  12. @[url=home.php?mod=space&uid=72445]@[/url] -138,6 +138,7 @[url=home.php?mod=space&uid=72445]@[/url] __v7m_setup_cont:
  13. 1:     cpsid   i
  14.         /* Calculate exc_ret */
  15.         orr     r10, lr, #EXC_RET_THREADMODE_PROCESSSTACK
  16. +       orr     r10, #EXC_RET_FTYPE
  17.         ldmia   sp, {r0-r3, r12}
  18.         str     r5, [r12, #11 * 4]      @ restore the original SVC vector entry
  19.         mov     lr, r6                  @ restore LR


clockevent和clocksource
linux系统必有至少一个clockevent提供心跳中断,一个clocksource提供计时。其中clockevent必须的,clocksource可以由内核的jiffies代替,详细笔者就不赘述了,因为前前后后讲清楚它们可以长篇大论写两三篇大文章,linux内核这块其实还蛮复杂的,但对clockevent和clocksource驱动要提供的接口来说变简单了,以后有机会再写吧。对应到MCU来说,其实就是timer嘛,RA8D1B里集成的timer多的是,各种口味的都有。对着RA8D1B的手册写clockevent/clocksource驱动难度不大,但是,笔者这次想换个**,咱能不能利用arm自由的资源呢?我们知道arm M类cpu都有一个systick且支持中断,所以理论上可以把它抽象成一个clockevent的。目前linux内核主线中把这个systick当clocksource用的,不支持clockevent,它的代码在drivers/clocksource/armv7m_systick.c。咱给它改造改造整个容,经笔者整容之后的armv7m_systick.c眉清目秀,盘亮条顺。


tty console
要看基本linux启动成功可用,最简单的还是一个tty console。console用uart做比较简单,这个RA8D1B里也有好几个uart,而且特性丰富功能强大,而且似乎内核中有一个同出一门众芯片的串口驱动drivers/tty/serial/sh-sci.c,但似乎寄存器不是太一致,所以接下来咱就对着手册改写串口驱动吗?NoNoNo,一般来说特性丰富功能强大就暗示着还是蛮复杂的,不信你去读一读sh-sci.c,咱能否粗糙猛方式搞定linux呢?咱又看上jlink了,它不是有个RTT (Real Time Terminal)么,既然号称Real Time Terminal,代替个串口做console那是小菜一碟阿。但是Segger的RTT代码不能直接用,因为SDRAM有32MB,这么大空间都搜一遍么?而且别忘记Cortex-M85是可以带有D-cache的,恰好RA8D1B里的这个M85就配置了D-Cache, linux肯定开启cache了,RTT代码注释和文档里对使能了cache这块其实有很多的说明。笔者这>里换了一个思路,不要用SDRAM而是SRAM阿或者DTCM一部分来做RTT控制块,DTCM是不经cache的。另外为了通用性,咱们这里还需要使用DT(Device Tree)技术而不是hardcoding来设置RTT控制块的地址,然后通过如下命令告诉jlink到哪里去找RTT控制块:

  1. exec SetRTTAddr 0x20000000
笔者也把Segger的RTT给整了个容,更像linux家的大**了。

dts
最后根据MCU和板级具体情况做个简单的dts,主要就是告诉SDRAM起始地址阿大小阿什么的,还有上面说的rtt的设备节点。

至此,linux内核层的事情搞定了,咱来搞定linux用户层。


利用buildroot构造工具链和rootfs

buildroot对NOMMU Linux支持得非常好,基本是拿来就用,buildroot使用方法网上也是很多的,前后>讲完也是很费时的,这里笔者也不多着笔墨了,因为linux userspace也不是主要目标。这个步骤的输>出是一个用busybox做的简单rootfs,可以在内核编译时builtin进去省去加载initrd/rootfs的过程。

编译内核和dtb
上述dts编译成一个dtb,重命名为dtb.bin,内核(带rootfs builtin)编译成一个Image,重命名为Image.bin。

万事具备,可以吹东风啦,咱上机测试一把
板子上电,烧录前面讲的bootloader,重启板子,运行jlink挂上去,运行如下命令:

  1. halt
  2. exec SetRTTAddr 0x20000000
  3. loadfile /tmp/Image.bin 0x68008000 noreset
  4. loadfile /tmp/dtb.bin 0x68004000 noreset
  5. wreg "R15 (PC)" 0x68008001
  6. wreg r0 0
  7. wreg r2 0x68004000
  8. go


第一个命令停止cpu,第二个命令是告诉jlink RTT控制块地址,后面是加载内核Image和跳转内核入口,至于为啥这么设置请参考arm linux内核启动协议,网上很多,笔者不再赘述了。
再起一个shell运行如下命令获得RA8D1B的linux shell
  1. telnet 127.0.0.1 19021


最后运行NOMMU Linux的视频供参考: https://www.bilibili.com/video/BV1FU2zYqEVQ/

咱可能是世界上第一个在cortex-m85平台上跑linux的憨憨 :D

最后说一句:笔者有一个预感,NOMMU Linux可能又会流行起来了。


打赏榜单

21小跑堂 打赏了 50.00 元 2024-10-31
理由:恭喜通过原创审核!期待您更多的原创作品~

评论

在RA8D1B-CPKCOR开发板上一直linux的尝鲜体验,测试过程完善,实现效果较好  发表于 2024-10-31 17:09
oxlm 发表于 2024-12-21 10:16 | 显示全部楼层
一直不明白,M核跑linux的意义何在,没有mmu,感觉很容易出状况
 楼主| xhackerustc 发表于 2024-12-21 15:53 | 显示全部楼层
如这么说把linux换成任何RTOS一样出状况阿。这里无非是把nommu linux当成一般的rtos用了,不运行外来程序。早年嵌入式cpu有mmu并不多,nommu linux非常合适,见过nommu linux做的路由器、监控设备等等
goyhuan 发表于 2024-12-24 13:44 | 显示全部楼层
这个系统能安装什么APP?还是只是模拟一下系统?
 楼主| xhackerustc 发表于 2024-12-24 20:19 | 显示全部楼层
nommu linux基本上是全系统定制,所有应用都是随着系统一起编译打包
您需要登录后才可以回帖 登录 | 注册

本版积分规则

42

主题

165

帖子

2

粉丝
快速回复 在线客服 返回列表 返回顶部