【STM32MP157 异核通信框架学习篇】(7)Remoteproc 相关驱动简析 ---- 链接脚本
1 概要
使用正点原子的stm32mp157mini进行RPMSG多核通信试验。
硬件:STM32MP157
软件:STM32CubeIDE / STM32CubeMX
远程处理器(Remoteproc)框架负责根据固件资源表中可用的信息激活 Linux 端的进程间通信(IPC),本节我们来分析 Linux 下的 Remoteproc 相关驱动,了解 Remoteproc 是怎样控制远程处理器的。本小节的内容会涉及到分析代码,会比较枯燥乏味,如果只想了解 Remotepro使用方法,可以直接看本章的最后一小节。
本章分为如下几部分(文章末尾会附上链接):
- 资源表
- 存储和系统资源分配
- Linux 下 Remoteproc 相关 API
- 链接脚本
- Remoteproc 的使用
2 stm32mp15xx_m4.ld 链接脚本
我们通过启动文件了解了系统复位后做了些什么工作,但我们并不知道内存的分配信息是怎样的,当然很多时候我们不需关心这些,只要确保程序能正常运行就可以。关于内存排布,我们这里会介绍一个重要的文件:链接脚本。
链接脚本是用于描述文件应该如何被链接在一起形成最终的可执行文件的脚本,其主要目的是描述输入文件中的段(section)如何被映射到输出文件中,并且控制在输出文件中的内存排布。利用链接脚本我们可以控制代码的加载区以及执行区的位置。
本小节中,在介绍链接脚本的时候,我们也会介绍两个和链接脚本关系比较重要的文件,一个是链接时产生的 map 文件,另一个是编译后生成的反汇编文件。本小节只是作为一个了解性的内容,如感兴趣可以了解一下,也可以跳过本小节。
2.1 链接脚本
链接脚本路径:Device\ST\STM32MP1xx\Source\Templates\gcc\linker\stm32mp15xx_m4.ld

在Device\ST\STM32MP1xx\Source\Templates\下的arm、gcc和iar下均有一个文件夹linker,里边放的就是 STM32MP1 系列的链接描述文件,其中,在 STM32CubeIDE 下,链接脚本为.ld文件,在 KEIL 中,链接脚本为.sct 文件,在 IAR 中,链接脚本为.icf 文件。
当构建工程的时候,STM32CubeIDE 会按照我们选择的芯片型号生成一个.ld 的链接脚本, 链接脚本是用于描述文件应该如何被链接在一起形成最终的可执行文件的脚本,其主要目的是描述输入文件中的段(section)如何被映射到输出文件中,并且控制在输出文件中的内存排布。利用链接脚本我们可以控制代码的加载区以及执行区的位置。
程序的编译一般分为预处理、汇编、编译和链接这 4 个步骤,我们在 STM32CubeIDE 上只需点击编译图标就一次性完成了这 4 个步骤,其中的操作细节 IDE 已经通过层层封装屏蔽掉了。在编译过程中,编译器将.c 和.s 源文件编译生成很多以.o 结尾的中间文件,这些中间文件包含了只读数据段、代码段、数据段、未初始化数据段等机器码信息,但是这些信息是放在最终可执行文件的哪个位置并没有确定下来,于是,链接脚本会告诉链接器,把所有的中间文件链接起来,并重定向它们的数据,然后链接生成可以被单片机运行的.elf 文件。如果要生成.bin 格式的文件,只需要通过格式转换就可以完成。

我们先回顾一下前面的 STM32CubeIDE 第一个工程编译信息,可以打开工程重新编译,然后将编译的信息拷贝到一个.txt 文本文件中,这样方便浏览信息。如下图是删除部分编译信息后的内容:

编译生成的文件在工程的 CM4\Debug 下,在里边的文件夹中有生成的中间文件。编译结束后提示 Build Finished. 0 errors, 0 warnings,没有报错,最终生成 HAL_LED_CM4.elf 二进制文件。如果有配置编译生成其它格式的执行文件,那么编译器会执行相应的指令以生成对应的文件,这些配置可以在 STM32CubeIDE 中设置。

如上图,设置编译生成.bin 格式文件和反汇编文件,那么编译器在生成 HAL_LED_CM4.elf文件以后,还会再执行以下指令:
arm-none-eabi-objdump -h -S HAL_LED_CM4.elf > "HAL_LED_CM4.list" arm-none-eabi-objcopy -O binary HAL_LED_CM4.elf "HAL_LED_CM4.bin"
arm-none-eabi-objdump 是反汇编指令,将 HAL_LED_CM4.elf 文件进行反汇编,并生成HAL_LED_CM4.list 文件。arm-none-eabi-objcopy 命令表示复制一个目标文件的内容到另一个文件 中 , 可 用 于 不 同 文 件 之 间 的 格 式 转 换 , 最 后 将 HAL_LED_CM4.elf 格 式 转 换 成HAL_LED_CM4.bin 文件。

C 语言程序编译完成以后,编译出来的代码一般都包含 text、data、bss 和 rodata 这四个段(section)。已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。text 和 data 段都在可执行文件中,程序运行的时候,由系统从可执行文件中加载。而 bss 段不在可执行文件中,由系统初始化并清零。四个段以及堆和栈的简单说明如下:


前面有看到通过 STM32MP157DAAX_RAM.ld 链接脚本,生成了 HAL_LED_CM4.elf 文件,这个链接脚本是怎样工作的呢?我们先看几个链接脚本的语法,然后再去查看链接脚本的代码实现过程。
1)入口地址
ENTRY(SYMBOL)
ENTRY(SYMBOL) ,表示将符号 SYMBOL 的值设置成入口地址,即程序执行的第一条指令的地址。
2)内存区域定义
链接器在默认状态下可以为 section 分配任意位置的存储区域,使用 MEMORY 命令可以用它来描述哪些内存区域可以被链接器使用,哪些内存区域避免使用。一个链接脚本最多可以包含一次 MEMORY 命令。
1 MEMORY
2 {/* 名称 权限 起始地址 长度 */
3 NAME [(ATTR)] : ORIGIN = ORIGIN, LENGTH = LEN 4 }
NAME 是在链接脚本中引用内存区域的名字,每块内存区域有一个唯一的名字。ORIGIN
是起始地址,LENGTH 是地址的长度。ATTR 字符串是该内存区域的属性,其中:
① r 表示读 section
② w 表示写 section
③ x 表示执行 section
④5 a 可表示分配的 section
⑤ l(L)表示初始化了的 section
⑥ ! 表示不满足该字符之后的任何一个属性的 section
一旦定义了一个内存区域,就可以指示链接器把指定的输出段放入到这个内存区域中,方法是:通过使用’>region 区域’。例如已经描述一个名为’mem’的内存区域,可以在输出段定义中使用’>mem’。
3)段链接定义
SECTIONS 命令是链接脚本里非常重要的命令,它的作用是:告诉链接器,如何把输入文件的 sections 映射到输出文件的各个 section,如何把输出 section 放入地址空间。跟 MEMORY
命令一样,一个链接脚本里只有一个 SECTIONS 命令。如果整个链接脚本内没有 SECTIONS
命令, 那么链接器将所有同名输入 section 合成一个输出 section 内, 各输入 section 的顺序为它们被链接器发现的顺序。
1 SECTIONS
2 {
3 .text :
4 {
5 start.o (.text) 6 *(.text*)
7 } >region
8 }
第 1 行先写了一个关键字―SECTIONS‖,后面跟了一个大括号,这个大括号和第 8 行 的大括号是一对,这是必须的。看起来就跟 C 语言里面的函数一样。
第 3 行,―.text‖表示段名,段名后面先空 2 个空格,然后再有一个冒号,表示段定义。例如.text :表示定义一个.text 段,这个段定义可以自己定,段名可以自己取。
第 4 行到第 7 行就是段的内容,这部分内容比较复杂。start.o (.text)表示将工程文件中的start.o 的.text 段(即代码段)链接到 MEMORY 定义的 region 中。(.text)中的是通配符,表示将工程中所有目标文件的.text 段链接到 region 中,在链接(.text*) 时,不会重复链接 start.o的.text 段。
第 7 行的’>region’就表示指示链接器把上面花括号中指定的输出段放入到这个 region 内存区域中(内存区域是在前面的 MEMORY 中定义的)
段的内容比较复杂,下面是一些常见的用法:
. = ALIGN(4):表示 4 字节地址对齐。也就是说段的起始地址要能被 4 整除,一般常见的都是 ALIGN(4)或者 ALIGN(8),也就是 4 字节或者 8 字节对齐。
PROVIDE 和 PROVIDE_HIDDEN 关键字:表示在链接脚本文件中定义一个符号,这个符号没有被目标文件定义,但是被目标文件引用。
KEEP()关键字:KEEP() 的作用是当启用连接器的–gc-sections 垃圾回收选项时,这部分不能被回收。如 KEEP(*(.text))表示不能将所有的.text 段当做垃圾回收。
/DISCARD/:是一个特殊的段名,如果使用这个段名作为输出,那么所有符合条件的段都被丢弃。
下面我们将 stm32mp15xx_m4.ld 文件的代码分为几个部分,查看链接脚本是怎样实现链接的。
1 /* 程序入口,程序将从 Reset Handler 开始执行 */
2 ENTRY(Reset_Handler)
3
4 /* 用户模式栈的最高地址,声明内存末尾地址 */
5 _estack = 0x10040000; /* 堆栈末尾 = RAM 起始地址 + RAM 空间大小 */ 6 /* 定义了堆和栈的最小空间大小 */
7 _Min_Heap_Size = 0x200; /* 堆大小 */
8 _Min_Stack_Size = 0x400; /* 栈大小 */
9
10 /* 内存定义,„MEMORY‟ 命令描述目标平台上内存块的位置与长度 */
11 MEMORY
12 {/* 名称 权限(读 R/写 W/执行 X) 起始地址 长度 */
13 m_interrupts (RX) : ORIGIN = 0x00000000, LENGTH = 0x00000298
14 m_text (RX) : ORIGIN = 0x10000000, LENGTH = 0x00020000
15 m_data (RW) : ORIGIN = 0x10020000, LENGTH = 0x00020000
16 m_ipc_shm (RW) : ORIGIN = 0x10040000, LENGTH = 0x00008000
17 }
18
19 /* OpenAMP 启用 rpmsg 所需的符号 */
20 __OPENAMP_region_start__ = ORIGIN(m_ipc_shm);
21 __OPENAMP_region_end__ = ORIGIN(m_ipc_shm)+LENGTH(m_ipc_shm);
第 2 行,程序入口,程序将从 Reset Handler 函数开始执行,该函数在启动文件startup_stm32mp15xx.s 中有定义。
第 5 行,设置堆栈的最高地址为 0x10040000,这里注意了,它决定了 SP 的位置,0x10040000
就是我们前面分析 startup_stm32mp15xx.s 文件时系统启动流程小节里仿真时得到的 SP 地址,即 0x0000 0000 映射到内部 SRAM 的地址。
第 7、8 行,定义了堆和栈的最小空间大小,其中,设置堆大小为 512B,栈大小为 1KB。
第 11 到 17 行,以 MEMORY 命令定义了系统中可用于放置代码和数据的内存区域:
①区域名为 m_interrupts 的地址范围是 0x00000000~0x00000298,这个范围也就是 M4 内核中断向量表的范围,对应前面内存映射关系图中的 RETRAM 区域;
②m_text 区域的地址范围是 0x10000000~0x10020000,刚好对应内存映射关系图中的SRAM1 区域,链接的是 tex 代码段(Code);
③m_data 区域的范围是 0x10020000~0x10040000,对应内存映射关系图中的 SRAM2区域,链接的是数据段(Data);
以上的链接地址就是程序的执行地址,找到链接地址了就知道程序是在哪里执行了。我们前面有提到内存映射关系图,在 IDE 中调试程序的时候,M4 的代码其实是放到了 SRAM 中运行 了 , 其中 M4 可 用的 SRAM 是 SRAM1(128kB) 、 SRAM2(128kB) 、SRAM3(64kB)和SRAM4(64kB),地址范围是 0x10000000~0x1005FFFF,共 384KB,如果只是 M4 跑裸机或者RTOS,不运行 A7 的话,这 SRAM1~SRAM4 可以全部分配给 M4,那如果要同时运行 M4 和A7 的话,这些地址分配就要注意了:根据 MEMORY 命令定义的地址范围,我们知道 m_text、m_data 和 m_ipc_shm 占用了 SRAM1~SRAM3,SRAM1、SRAM2 是完全分配给 M4 了,这里注意,如果要运行 A7 的话,M4 并不是完全占用 SRAM3,具体占用多少需要根据 Linux 下的设备树配置来决定,在 A7 和 M4 双核通信中,默认 A7 和 M4 共同占用 SRAM3 的0x10040000 ~ 0x10046000,这部分地址作为 A7 和 M4 通信的内存交换区,内核下的设备树如下:

而 m_interrupts 其实是在 RETRAM 里,剩下的 SRAM4 用于做什么呢?如果不跑 Linux 操作系统,只是跑 M4 裸机程序的话,M4 内核完全可以使用这部分区域,由用户来指定。如果跑 Linux操作系统,在 Linux 设备树下已经默认将 SRAM4 当做了 Linux 功能的 DMA 了,如果要释放这部分区域,在设备树下将对应节点删除释放即可(但不建议这么做,A7 可能会异常)
根据上述描述,这几个区域对应关系如下图,从图中可以明显看出 SRAM 的地址分配情况:

综合以上分析,总结如下:
如果不跑 A7,只运行 M4(M4 可以跑裸机、RTOS):SRAM1~SRAM4 可以完全分配给M4;
如果同时跑 A7 和 M4(例如双核通信):SRAM1 和 SRAM2 是单独给 M4 用的,SRAM3的部分地址是 M4 和 A7 一起使用的,SRAM4 在 Linux 下单独配置了 DMA,即被 A7 占用了。 如果要修改 MEMORY 中的地址区域范围,一定要联系内存映射表的地址范围来修改。
接下来是 SECTIONS 段链接定义,内容比较多已经省略部分:
1 /* SECTIONS 关键字,用来描述输出文件各个 section 的布局 */
2 SECTIONS
3 {
4 /* 将中断向量的内容链接到 m_interrupts 地址 */
5 .isr_vector : /*.isr_vector 在 startup_stm32mp15xx.s 文件中有定义 */ 6 {
7 . = ALIGN(4); /* 4 字节对齐,要求段的起始地址要能被 4 整除 */
8 KEEP(*(.isr_vector)) /* 此处 KEEP 保证 isr_vector 的输出段不会被删除 */ 9 . = ALIGN(4);
10 } > m_interrupts /* 地址为 0x00000000-0x00000298*/
11 /* 将程序代码和其他数据链接到 m_text 区域 */
12 .text : /* .text 对应程序的可执行代码 */
13 {
14 . = ALIGN(4);
15 *(.text) /* 将所有输入文件.o 的代码段链接到 MEMORY 定义的 m_text */
16 *(.text*) /*将所有目标文件的.text 段链接到 m_text 中 */ 17 *(.glue_7) /* glue arm to thumb code */
18 *(.glue_7t) /* glue thumb to arm code */
19 *(.eh_frame) /* .eh_frame 用于处理异常 */
20 KEEP (*(.init))
21 KEEP (*(.fini))
22 . = ALIGN(4);
23 /* _etext 是链接器的预定义变量,代表程序正文段结束的第一个地址 */ 24 _etext = .; /* 在代码末尾定义一个全局符号 */
25 } > m_text
26 /* 将常量数据链接到 m_text */
27 .rodata :
28 {
29 . = ALIGN(4);
30 *(.rodata) /* .rodata 段 (常亮、字符串等) */
31 *(.rodata*) /* .rodata* 段 (常亮、字符串等) */
32 . = ALIGN(4);
33 } > m_text
34 /******* 省略部分代码 *******/
35 /* 初始化数据段 */
36 .data : AT(__DATA_ROM)
37 {
38 . = ALIGN(4);
39 _sdata = .; /* 创建全局符号 */
40 *(.data) /* .data 段 */
41 *(.data*) /* .data* 段 */
42 . = ALIGN(4);
43 _edata = .;
44 } > m_data
45 /******* 省略部分代码 *******/
46 /* 将程序中未初始化的全局变量链接到 m_data 中 */
47 . = ALIGN(4);
48 .bss :
49 {
50 /******* 省略部分代码 *******/
51 } > m_data
52 /*用于检查剩余的 RAM 是否足够 */
53 ._user_heap_stack :
54 {
55 /******* 省略部分代码 *******/
56 } > m_data
57 /* 从编译器库中删除以下信息 */
58 /DISCARD/ :
59 {
60 /******* 省略部分代码 *******/
61 }
62 .ARM.attributes 0 : { *(.ARM.attributes) } 63 }
根据注释以及前面讲解的语法,整个链接脚本也很容易看懂。
第 5 到第 10 行,表示中断向量的内容,地址为 0x00000000~0x00000298,这个区域是RETRAM。
第 24 行,_etext = .;中的小数点.表示当前地址,意思是_etext =的地址就是.text 段的地址。
一般的程序中包含常见的几个段:text、rodata、data 和 bss 段,这一部分实际上指定了程序的各个内容该如何放置在 SRAM 上。
在 STM32CubeIDE 上可以直接查看这些段的信息。例如第一个工程实验 HAL_LED,编译完成以后,点击 Window 菜单Show ViewBuild Analyzer 打开 Build Analyzer 窗口查看(注意,此时不是在 Debug 调试看的,是编译后查看的):

在 Build Analyzer 窗口中可以查看内存使用情况。其中在 Memory Regions 处可以看到m_interrupts、m_text、m_data 和 m_ipc_shm 的起始地址和结束地址,范围大小,以及使用量和剩余量。

在 Memory Details 处可以查看更详细的信息,VMA 是虚拟地址,一般是 RAM 位置这里指运行地址,LMA 一般是加载地址,即 ROM 位置。

通过链接脚本的内容,我们大概知道每段链接到了哪段内存区域,程序的链接地址一般也就等于运行地址(这点大家要记住)。其实我们也可以通过查看.map 文件和反汇编文件来进一步研究我们链接脚本和代码的实现,下面我们分两个部分来讲解这两个文件。
3 总结
本章节介绍了STM32mp157中M核会用到的链接脚本。
4 其他章节
【STM32MP157 异核通信框架学习篇】(1)SMP和AMP架构
【STM32MP157 异核通信框架学习篇】(2)IPCC通信框架
【STM32MP157 异核通信框架学习篇】(3)OpenAMP框架
【STM32MP157 异核通信框架学习篇】(4)驱动文件介绍
【STM32MP157 异核通信框架学习篇】(5)Remoteproc 相关驱动简析 ---- 资源表
【STM32MP157 异核通信框架学习篇】(6)Remoteproc 相关驱动简析 ---- 存储和系统资源分配
【STM32MP157 异核通信框架学习篇】(7)Remoteproc 相关驱动简析 ---- 链接脚本
【STM32MP157 异核通信框架学习篇】(8)Remoteproc 相关驱动简析 ---- 重新划分存储区域
【STM32MP157 异核通信框架学习篇】(10)Linux下Remoteproc相关API (上)
【STM32MP157 异核通信框架学习篇】(10)Linux下Remoteproc相关API (下)
【STM32MP157 异核通信框架学习篇】(11)链接脚本
【STM32MP157 异核通信框架学习篇】(11)链接脚本 – [额外补充: MP157板子存储分配]
【STM32MP157 异核通信框架学习篇】(12)Remoteproc 的使用 [加载和启动M4固件] (上)
【STM32MP157 异核通信框架学习篇】(12)Remoteproc 的使用 [加载和启动M4固件] (下)
更多推荐



所有评论(0)