查看: 4328|回复: 1

使用IAR从零开始快速搭建LPC5500双核工程

[复制链接]
  • TA的每日心情
    开心
    2024-3-26 15:16
  • 签到天数: 266 天

    [LV.8]以坛为家I

    3302

    主题

    6549

    帖子

    0

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    32045
    最后登录
    2024-4-29
    发表于 2020-5-21 17:27:41 | 显示全部楼层 |阅读模式
    使用IAR从零开始快速搭建LPC5500双核工程

    引言
    LPC55S69微控制器内部集成了两个ARM Cortex-M33内核, 都可以跑在150MHz的主频上. 通常情况下, 使用其中一个内核(core0)就可以完成足够多的工作. 但是, 让一个150MHz的Cortex-M33内核闲在那里实在浪费, 并且在一些对性能有要求的情况下, 使用双核同时工作确实可以简化应用的开发过程, 并提升系统整体的工作效率. 笔者最近就遇到了这么一个案例.


    笔者在LPCXpresso55s69开发板上面做语音关键字识别的项目时, 想在LCD显示屏模块上显示点交互信息, 改善用户体验. 笔者使用的是一块SPI总线320x240像素的LCD屏模块, 使用RGB565的像素格式, 如果使用DMA+SPI的方式刷屏自然可以为主CPU减负, 但需要提前缓存整张图片到内存中, 而存一幅图需要连续的150KB内存, 占用内存空间比较大. 虽然LPC55S69有足够的内存(320KB), 但是使用人工神经网络模型占用的内存规模也比较大, 在不确定内存能否够用的情况下, 笔者觉得仅仅为刷屏分配这么多内存无疑是奢侈的, 并且屏幕交互信息很简单, 基本上就是一个黑色的背景色加几行字而已, 现算现刷的方式可以大幅降低内存和代码的占用量. 但使用主CPU轮询SPI会严重影响人工神经网络的计算实时性. 此时, 使用副CPU在主CPU计算的时候执行刷屏的操作, 哇, 简直不要太香.

    在本文中, 笔者将介绍笔者从零开始搭建双核工程的过程.

    精简MCUX SDK工程, 制作工程模板
    首先, 笔者从MCUX SDK代码包里提取出了一个hello_world工程, 经过一番精简和调整文件组织结构, 改了工程名, 最后变成了这个样子:
    1.png
    根目录下只有"application", "CMSIS", "device"和"drivers", 大部分源文件直接放在一级目录下, 最深的工程组织文件路径也不过只有三层. 一个词, "清爽".
    2.png

    这个工程作为模板, 将成为后续所有新建工程的起点.


    为双核工程筹备足够的源文件

    复制"\application\hello_world"目录, 改"hello_world"为"dualcore_basic", 相当于根据模板创建了一个新工程. 然后在"\application\dualcore_basic\iar"目录下复制一份"my_project.eww"和"my_project.ewp", 将两份工程组织文件改名为"core0_project"和"core1_project", 这两个工程组织文件将分别用于编译生成两个处理器内核上运行的程序.



    从模板创建的工程在默认情况下是支持单核的, 其中并没有包含支持第二个核心的一些必要的源文件. 如此, 笔者又在MCUX SDK软件包中提取了双核版的"hello_world" 工程.
    3.png
    从中复制了与core1相关的相关文件:


    LPC55S69_cm33_core1_ram.icf
    startup/startup_LPC55S69_cm33_core1.s
    startup/LPC55S69_cm33_core1.h
    startup/LPC55S69_cm33_core1_features.h
    startup/system_LPC55S69_cm33_core1.c
    startup/system_LPC55S69_cm33_core1.h
    lib/iar_lib_power_cm33_core1.a
    在新工程中, 只要放置在对应"xxx_core0.xxx"源文件存放的位置就可以了. 两个核心除了启动代码, 链接命令文件和供电库文件之外, 其余的驱动源代码完全复用.


    这里特别强调一个思路, 双核的工程跟单核的工程本质上没有区别, 只是原来生成下载的可执行文件是一次编译, 双核工程需要两次编译(先编core1再编core0). 或者也可以将core1的程序当成core0工程的库文件. 先编译core1的二进制可执行程序, 然后将core1工程编译生成的core1_project.bin包含在core0工程中, 就像平时在单核工程中添加一个预编译库那样简单. 实际上, 在实际使用双核应用的时候, 也就是将core1当成一个运行着的库函数一样使用.


    配置副核core1工程


    1.改头换面从core0到core1



    core1工程中, 将芯片类型, linker文件名, 芯片名的宏定义, 都从core0改到core1. 另外, 由于core1工程文件和core0在同一个目录下, 为了区分同core0生成中间文件, 特别将输出文件目录名中加一个"core1_"的前缀.
    4.png
    5.png
    6.png
    7.png

    2.调整linker文件指定运行时空间


    唯一需要注意的地方就是调整linker文件中的内容.



    双核系统中, 两个内核都是总线主机, 在执行程序和存取变量的时候都需要访问系统总线, 但如果两个内核要同时访问同一个总线从机设备, 那么就会出现访问冲突, 需要通过总线仲裁, 这样就降低了两个内核访问存储设备的速度, 降低了双核系统执行程序的性能. 因此需要合理安排两个内核各自使用的存储区, 尽量不要让两个内核在访问内存的时候"打架".
    8.png
    在上图中可以看到, 将SRAMX内存块分给core1存放代码, 将RAM3内存块分给core1存放数据, 其余的内存块分给core0, 大家各用各的, 相安无事.


    core1工程有下载时空间和运行时空间两个概念. 下载时空间就是把需要将可执行程序下载到flash中, 否则掉电之后程序就没了. 运行时空间是指, 整个系统的启动后, 需要把flash中的程序搬运到ram中, 程序中跳转指令和寻址变量都是在ram中的运行时空间中. 对于core1工程, 怎么下载到flash和在系统启动过程中搬运到ram中, 它都不管, 这将会交给core0工程处理, 由于系统启动过程是单线程的, 就是把core1的程序存放在core0的管辖空间内也无妨. 此处, core1只要告诉自己的程序和变量, 在运行时自己会在内存中就可以. core1的运行时内存就是sramx和ram3.



    对应的linker脚本文件LPC55S69_cm33_core1_ram.icf中, 对应指定代码和数据存放区域的代码如下:
    9.png
    在基本的应用中不用去管下载选项, 因为实际不大可能直接下载core1工程程序到芯片中. 即使可以通过调试器直接将程序写入到芯片的RAM中, 但由于core1的启动开关和时钟系统的初始化过程都需要core0的代码去完成, 单独下载core1的工程不能正常启动, 仍是不能直接调试的. 但这里可以考虑到一种特殊的情况, 也是一个比较有意思的设计, 就是预先在core0的工程中让core0启动后(电路系统的默认启动操作)仅仅启用core1, 之后什么都不做了直接进入休眠或者死等的状态. 此时, 是可以用IAR的调试环境将程序下载到RAM中并调试core1的程序的. 使用这种方式可以用于专门调试运行在副核上的功能, 待代码成熟后, 再集成到有完善功能的主核应用工程中.


    3.生成二进制格式的bin文件



    配置core1工程生成"core0_project.bin"文件(默认只生成core1_project.out文件), 这个二进制文件将在core0工程中将被直接包含.
    10.png
    配置主核core0工程
    终于回到主场了. core0工程就跟普通的单核工程没有不同. 额... 除了需要为core1留一点空间(下载时空间和运行时空间). 那么在工程中的配置都是围绕这个预留空间来的.


    1.在IAR工程中创建新段包容core1的bin程序文件



    在编辑linker文件之前, 先要为core1的一大块程序指定一个在core0程序空间中的标号. core0工程不会关注core1_project.bin里各种细节, 对于core0来说, core1_project.bin只是一块需要烧写在flash中特定位置的数据. 甚至内存搬运的工作都是在代码中完成的, 因此在工程配置中没有更多隐藏的"黑科技".
    11.png
    其中, "Raw binary image"框中的几个字符框的内容分别是:


    File   : "$PROJ_DIR$\core1_debug\core1_project.bin"
    Symbol : "_lpc5500_cm33_core1_image"
    Section: "__sec_core1"
    Align  : "4"
    这里的意思是, 将core1_project.bin文件指定成linker过程中的一个段(section), 段名为"__sec_core1", 并在链接过程中使用"_lpc5500_cm33_core1_image"符号指代. 这个段在linker文件中将被用于安置内存, 在启动代码中将提取段地址从而执行将coer1程序从flash复制到sramx的过程.


    2.调整linker文件包容core1的新段



    首先, 将core0的程序空间和内存空间压缩, 为core1程序的下载时空间和运行时空间让出地方.
    12.png
    从linker脚本代码中可以看到, 从0x0009_0000开始的32KB flash空间就是预留给core1的下载时空间. core0的数据空间也仅仅用到了160KB.



    之后, 在后续的linker脚本代码中将core1的下载时空间包容到core0工程的程序文件中.
    13.png
    此处专门为core1的下载时空间创建了一个域(region)并制定地址, 然后将之前创建的段"__sec_core1"通过块"sec_core1_block"包含在"CORE1_region"域中.


    3.在代码中复制core1的程序到ram中并启动core1


    笔者总是想尽量把关键的部分放在代码中, 因为IDE总是在更新, 配置可能会变, 但代码永恒. 而且代码是程序员的通用语言, 最容易理解.


    使用core1程序的关键部分, 就在于内存复制和启动内核, 一个是软件的活, 一个是硬件的事.



    笔者在"core1_init.c"文件中实现了core1_init()函数:
    14.png
    这里面用了一点IAR编译器专有的"黑魔法", 通过"_section_end()"函数配合提取了"_sec_core1"段的地址空间. 然后就是用memcpy函数直接进行无差别的内存复制. 此处的"CORE1_BOOT_ADDRESS"不是动态提取的, 如果想保持代码风格一致的话, 也可以像"core1_image_start"一样从链接文件中引用. 此处将两种方式都展现出来, 只是为了说明两种方式的作用是一致的.


    start_core1_hardware()函数的实现内容是硬件双核系统的专有设计, 根据LPC55S69手册中的说明, 启动coer1首先需要在"SYSCON->CPUCFG"寄存器中启用core1, 之后在"SYSCON->CPBOOT"寄存器指定可执行程序二进制文件的首地址, 芯片就会自动将其指定为core1的向量表地址(启动地址), 最后在"SYSCON->CPUCTRL"寄存器中执行一波"神操作", 需要在特定验证码的加持下, 提供时钟并复位core1, 才能最终让core1运行起来.


    至此, 为双核程序运行准备的所有配置操作都已经完成, 在core0工程中的main()函数中调用core1_init()即可启动为core1预先写好的程序.


    编写样例程序, 测试运行
    1.测试core1正常启动并运行



    搭建好双核运行环境之后, 笔者先写了一个简单的测试程序, 验证core1能够被正常启动并运行. 具体就是让core1控制板子上的一盏小灯闪烁. core1的main()函数代码如下:
    15.png
    在core0的main()函数中, 只是调用"core1_init()"启动core1而已, 没有同core1有进一步的干预. 下载程序后, 可以看到小灯闪烁, 说明core1已经按照预期正常工作了.


    2.实现简单的双核通信


    实际上, 笔者希望core1能够帮助core0在运行时执行更多的辅助工作, 灵活地根据core0的需求执行多种操作. 然后笔者就基于共享内存的机制, 实现了一个极为简单的纯软件的双核通信组件"shmem". 具体原理很简单, 笔者在LPC55S69芯片上的ram空间中分出来一块内存, 独立于core0和core1工程可自主使用的内存, 而是需要两个内核通过绝对地址访问. 目前实现的就是两个核的分别拥有的事件标志位和两个带锁的单向fifo数据队列, 事件标志位用于同步事件, 两个fifo数据队列就像串口的收发一样建立数据的双向通信通道.


    写好了"shmem"组件之后, 笔者改进了基本的双核测试程序, 让core1在core0的控制下闪烁小灯. 笔者设计了控制小灯的命令码和参数格式, 然后通过从core0到core1的fifo传递控制命令及参数. 当然, 在此之前, 笔者还用了事件标志同步了两个内核的工作步调, 一定确保在core1已经完成了对fifo的初始化, 保证fifo可用之后才能让core0向fifo中下命令.



    这样, core1的main()就增加了shmem的内容:
    16.png
    core0的main()函数在初始化阶段, 耐心等待core1的各项工作准备完成, 然后时不时向core1的fifo发送控制命令及参数.
    18.png
    下载, 运行, core1接收core0的指令控制小灯闪烁, 大功告成.
    19.png
    后记
    MCUX SDK的代码包中已经提供了双核的样例工程, 为什么笔者此处还是要做从零开始的工程搭建. 原因有两个:

    SDK中的双核工程略显臃肿, 像LPC5500这种芯片,由于core0和core1只有CPU不同, 整个系统中的外设是完全一样的, 因此可以使用同一份驱动代码. 本文中将副核作为主核的一个运行库安排在应用工程中, 这个思路跟在单核工程中添加预编译库的思路基本一致, 便于用户理解。而SDK将两个核的工程完全独立出来, 这对于熟悉经典单核开发的用户来说,始终需要按双芯片的系统考虑,但好处是两个工程可以分别用不同版本的SDK库,也可以分别用不同的开发工具,适合大型项目或多人同时开发等。

    SDK中提供样例程序, 对双核通信部分的实现比较高级(无论是"erpc"还是"rpmsg"),  在比较简单的环境时,不必使用这么复杂的组件, 可以考虑使用笔者设计的这个shmem组件用于后期开发.


    笔者的初衷, 是用尽量简单的方式理解双核工程, 然后才能进一步将双核系统用起来. , 笔者希望通过本文的讲述, 能够降低读者使用LPC55S69微控制器双核系统的难度, 让广大的单片机开发者们了解双核开发, 善用双核系统, 充分使用LPC55S69这款性能强大的芯片.

    本文中设计的双核样例工程可以在开源中国的git站点中下载到:点击


    相关推荐:恩智浦开发教程小课堂第一期——LPC5500专题

    作者:苏勇@NXP      文章出处:恩智浦MCU加油站


    签到签到
    回复

    使用道具 举报

  • TA的每日心情
    开心
    2024-4-10 22:38
  • 签到天数: 1335 天

    [LV.10]以坛为家III

    88

    主题

    4292

    帖子

    12

    版主

    Rank: 7Rank: 7Rank: 7

    积分
    9049
    最后登录
    2024-4-13
    发表于 2020-10-15 10:22:42 | 显示全部楼层
    将core1的程序当做是一个线程来处理。
    不过,我倒是觉得还是Cotex-M4 + Cortex-M0+的配置比较好。性价比高很多
    该会员没有填写今日想说内容.
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /4 下一条

    Archiver|手机版|小黑屋|恩智浦技术社区

    GMT+8, 2024-4-29 13:21 , Processed in 0.117077 second(s), 21 queries , MemCache On.

    Powered by Discuz! X3.4

    Copyright © 2001-2024, Tencent Cloud.

    快速回复 返回顶部 返回列表