如何开发与位置无关的 STM32 完整工程

发布者:JoyousJourney最新更新时间:2023-09-30 来源: elecfans关键字:STM32  完整工程  编译工具 手机看文章 扫描二维码
随时随地手机看文章

1、前言

最近有客户询问,能否使用 STM32CubeIDE 在编译时通过设置某个编译选项,让STM32 应用与存储位置无关。这样的优势是能使同一个固件被烧在 STM32 Flash 里的不同位置, 而在系统 Bootloader 里只需要跳到相应的位置就可以正常执行固件代码。客户希望STM32 代码从 Flash 里执行,不复制到 RAM 里;客户希望是一个完整的映像,而不仅仅是其中某个函数做到了位置无关。


2、分析

在嵌入式场景下,不一定有操作系统。即使有操作系统,一般也是 RTOS。一般 RTOS没有一个通用的程序加载器。因此,存储位置无关的需求,在这时可以说无关紧要。但是,如果客户需要进行在线固件更新,例如 IoT 应用的固件升级,那么位置无关就存在价值了。位置无关之后,对于不同的软件版本,不需要频繁的为烧写位置的反复改变而修改编译链接脚本。也不需要在代码里显式的在两个 Bank 之间进行切换。


最简单的情况是所有的代码都复制到内存执行。因为 Flash 的功能只是进行存储,自然对 Flash 的位置没有任何要求。但大部分 MCU 用户面临的真实案例都是 Flash 比较大,例如 ,1M 字节 ;RAM 比较小,例如,128K 字节。在这种情况下,代码在 Flash 原地执行就是一个必须的选择。Flash 位置改变,会影响从 Bootloader 跳转之后的固件执行时的 PC 指针,也就是 PC指针值会发生相应的变化。位置无关的原理,是让应用程序经过编译后所生成的映像,其中的代码和数据,都是基于相对代码的位置进行引用。那么,当应用被搬到不同位置时,他们的相对位置不变,从而执行不受影响。


代码和数据基于绝对地址还是基于相对地址,是由编译器所决定。以客户要求的

STM32CubeIDE 编译工具为例,我们可以看到在[Project]->[Properties]->[C/C++ Build]->[Settings]->[Tool Settings]->[MCU GCC Compiler]->[Miscellaneous]已经有一项[Position Independent Code (-fPIC)]。

是否只要选一下-fPIC 选项就大功告成了呢?答案是没有那么简单。

f79506e8-341f-11ed-ba43-dac502259ad0.png

事实上,对于完整应用程序工程,用户应该经过这些步骤将其变成位置无关:• 选择正确的编译器选项

• 去掉或者替换掉那些包含绝对位置的库文件

• 修改代码中的 Flash 绝对地址(这里以 STM32H7 的 CRC_Example 例程为例,

其他情况下有可能要修改更多) o 在 startup_xxx.s 汇编代码里的 sidata

o 在 system_xxx.c 里的 SCB->VTOR 以及中断向量表内容

o GOT

对于完整工程,要正确的跳转到应用程序进行执行,还需要由 Bootloader 向应用程序提供或者由应用程序在链接时自身解析计算,得到以下信息:

• Flash 偏移量

• 中断向量表的开始以及结束地址

• GOT 的开始以及结束地址

我们接下来就举例说明这些步骤。


3、步骤

3.1. 选择正确的编译器选项

如果我们不使用任何编译选项,编出来的代码会怎么样?我们可以通过.list 文件进行查看。.list 文件在 STM32 例程中默认生成,如果没有请勾选如下选项, 在 [Project]->[Properties]->[C/C++ Build]->[Settings]->[Tool Settings]->[MCU Post Build outputs]->[Generate list file],可参考下图。

f7a7d3a4-341f-11ed-ba43-dac502259ad0.png

f7bb0e42-341f-11ed-ba43-dac502259ad0.png

f7d2fee4-341f-11ed-ba43-dac502259ad0.png

我们看到代码中直接使用了变量的绝对地址,例如 0x2000 0028。我们不要被 literal pool 文字池的使用所迷惑,那个基于 PC 的操作只是为了取变量的绝对地址,例如, 0x2000 0028,并没有将绝对地址变成相对地址。


当然大家说这里是 RAM 地址,没有关系。我们选择这个函数来说明,是因为位置无关的编译器选项是不区分 RAM 还是 Flash 里的变量,而这个函数最简单容易理解。如果我们查看另外一个复杂一点的函数,例如,HAL_RCC_ClockConfig,我们可以看到以下对Flash 里变量的直接使用。这就不妙了,因为一旦改变了 Flash 下载的位置,在绝对地址处就取不出变量的真实内容了。

f7e3c436-341f-11ed-ba43-dac502259ad0.png

我们没有办法一个一个查找修改所有的变量。当然这里的变量是指全局变量。如果要修改,我们希望编译器能把他们集中在一起。对于此,编译器提供了多个编译选项。例如,PIC 是位置无关代码, PIE 是位置无关执行。PIC 和 PIE 这两者类似,但是存在一个显著的差异是 PIE 会对部分全局变量优化。我们可以观察到用两种不同编译选项的效果。

f7f47cf4-341f-11ed-ba43-dac502259ad0.png

其中 80004C0 地址处包含的是 GOT 自身的偏移量,存在 r2 里,要在两次取全局变量 uwTickFreq 和 uwTick 时引用。GCC 编译器引入 GOT 全局偏移量表来解决全局变量的绝对地址的问题。在之前对绝对地址的直接使用,现在被转化成先取得 GOT 入口相对于 PC 的偏移,再获得实际变量相对于 GOT 入口的偏移,从而得到实际变量的地址。计算公式如下:

实际变量的绝对地址=PC + GOT 相对于 PC 的偏移 + 变量地址相对于 GOT 的偏移

GOT 只有一个,如果代码放在不同的位置,代码自身就可以根据 Bootloader 传递过来的信息,或者自行计算来对 GOT 进行更新。这样变量的地址就和新的 Flash 偏移相匹配。

f807ca2a-341f-11ed-ba43-dac502259ad0.png

这里可以看到 80004c0 对应的 uwTick(可以从 str 指令结合 C 语言源代码快速知道它对应于 uwTick)不再使用 GOT 偏移,而是相对于 PC 的偏移(与前文相比,多了一条指令 “add r3,pc”)。换句话说,PIE 对局部的全局变量做了优化。这个优化显然不是我们所需要的。因为如此以来,RAM 变量的地址就会随着 PC 的不同而不同。而我们则希望所有对RAM 的用法不发生变化。


为了能够修改 GOT 内容,我们选择将 GOT 最终存放在 RAM 中,导致代码中对 GOT的寻址也是使用了相对于 PC 的偏移。而因为 RAM 有限,或者因为没有虚拟内存的缘故,我们不希望 RAM 的用法有所不同,否则,可能代价很大。这时,一旦 Flash 代码位置发生变化引起 PC 指针变化,GOT 就无法找到。因此,即使我们不使用 PIE,PIC 也没有办法单独使用。为了确保没有任何存放在 RAM 里的变量的位置是相对于 PC 的偏移。我们应该使用如下所有编译选项,single-pic-base 让系统只使用一个 PIC 基址,就是下文反汇编中看到

r9;no-pic-data-is-text-relative 则让编译器不要让任何变量相对于 PC 寻址。

f81762be-341f-11ed-ba43-dac502259ad0.png

f8276736-341f-11ed-ba43-dac502259ad0.png

这样实际变量的绝对地址,就变成实际变量的绝对地址=PIC 基址 + GOT 相对于 PIC 基址的偏移 + 变量地址相对于 GOT的偏移使用以上编译选项,这样我们看到 HAL_IncTick 就如下所示:

f83fb610-341f-11ed-ba43-dac502259ad0.png

这样所有在 RAM 里的全局变量都是相对于 GOT 的偏移。注意,这个时候你编译出来的代码现在没有办法进行测试,尽管你只是改了编译选项。这是因为 PIC 的基址需要你通过寄存器 r9 显式指定。在本例中,我们在链接脚本里如下定义 GOT 的位置:

f8542d3e-341f-11ed-ba43-dac502259ad0.png

因此,我们可以很容易的从.map 文件中获得 GOT_START 的 RAM 地址,0x2000 0000,它就是 PIC 的基址。如果想测试编译器选项是否如我们所期望,我们可以在Reset_Handler 开始部分加上如下语句(参考后文内存布局的代码):

f86b1986-341f-11ed-ba43-dac502259ad0.png

经过测试,我们可以确信,编译器选项的改动对我们最终执行结果没有影响。

值得注意的是,STM32 用户的代码,例如 RTOS 的移植, 也可能使用寄存器 r9。在这种情况,用户应当解决冲突。一般情况寄存器 r9 对应用程序并不是必要的。

3.2. 去掉或者替换掉那些包含绝对位置的库文件

我们要将位置无关的库去掉或者替换掉。在 STM32 参考代码里,我们需要

startup_xxx.s 里 C 库调用去掉。示例如下:

f883fc62-341f-11ed-ba43-dac502259ad0.png

3.3. 修改 Flash 绝对地址

3.3.1. 内存布局

如果要对代码中的 Flash 绝对地址进行修改,我们需要知道存放 Flash 绝对地址的 RAM起始和结束地址,以及需要增加或减少的 Flash 偏移量。存放 Flash 绝对地址的 RAM 起始和结束地址,在编译时可以让应用代码本身借助自身链接脚本在链接时导出的变量得到,然后由应用程序在运行时存放在 RAM 中的固定位置;也可以在编译后从.map 文件或使用工具解析 elf 文件获得,然后作为应用程序一部分的元信息,例如,给应用程序加个头部存放元信息,由 Bootloader 下载并解析,将其放入到 RAM 固定位置。


我们规划在一段 RAM 里按如下顺序存放如下元信息,它可以是应用程序本身在最初阶段自我存放在这里,也可以简单的由 Bootloader 解析元信息后,跳转到应用程序之前就存放在这里。

f893b15c-341f-11ed-ba43-dac502259ad0.png

我们在前文已经在链接脚本中定义了 GOT_START 和 GOT_END,我们还需要在链接脚本中定义 VT_START 和 VT_END。如下图所示:

f8a1f0dc-341f-11ed-ba43-dac502259ad0.png

如果我们希望 Bootloader 仅仅是做简单的跳转,我们可以将规划这段内存的工作,交给应用程序的初始化部分(在 “ldr sp, =_estack”之前)。假定 0x0 处对应为 0x2400 0000,参考代码如下:

f8b484fe-341f-11ed-ba43-dac502259ad0.png

3.3.2. 汇编代码

3.3.2.1. _sidata

在默认的 STM32 工程中,还有一些对变量绝对地址的使用。在 startup_xxx.s 有许多地方使用绝对地址,它们不能被编译器收集到 GOT 中。其中,默认在链接脚本里的_sidata,标志 flash 里 RAM 数据区的 Flash 位置,需要修改。

f8d1235c-341f-11ed-ba43-dac502259ad0.png

注意,变量绝对地址本身不是个问题,而对它解应用,取它的内容才会发生错误。而这里的 _sidata 是要被初始化代码使用,目的是将 Flash 的内容搬移到 RAM 里。我们显然要对_sidata 进行修改,否则无法取得正确的内容到 RAM 里。

根据前文的内存布局,我们可以把 Flash 的偏移量从内存中放置在寄存器 r8 里,例如:

f8e20532-341f-11ed-ba43-dac502259ad0.png

则我们只需要一行简单的代码 “add r3,r8” 就可以修正_sidata 的地址。

f8f6f37a-341f-11ed-ba43-dac502259ad0.png

3.3.3. C 代码

3.3.3.1. 公共函数

如果一段内存的数据都是硬编码,我们只需要一个公共函数就可以对其循环进行修正。我们需要知道什么样的地址之外不是 Flash 地址,那么就对这样的值不做修改。例如,我们定义 0x1fff ffff 之外的就不是 Falsh 地址,相应的宏定义如下:

f901f82e-341f-11ed-ba43-dac502259ad0.png

3.3.3.2. SCB->VTOR

在 C 语言中如果使用赋值语句进行硬编码,编译器也无法进行收集。例如在

system_stm32xxxx.c 中的 SystemInit 有如下语句:

f9172eba-341f-11ed-ba43-dac502259ad0.png

中断向量表相关的内容需要修改,包括两部分:

• 中断向量表的内存位置

• 中断向量表的内容

我们应该将中断向量表复制到 RAM 里,通过 UpdateOffset 函数修正其中包含的所有Flash 绝对地址的值,同时通过对 SCB->VTOR 赋值来将中断向量表的位置指向我们修改过内容的 RAM 地址。注意,VTOR 所指向的地址 VT_RAM_START 要按照 ARM 要求,根据中断总大小向上进行 2 的幂次对齐,例如,37 个字大小要使用 64 个字对齐。另外,中断向量表的内容,也包含有 RAM 地址,对此,我们并不需要修改。当然,UpdateOffset 函数已

经考虑到这一点,所以我们可以直接使用它。更新中断向量表以及 VTOR 的参考代码如下:

f932eb96-341f-11ed-ba43-dac502259ad0.png

3.3.3.3. GOT

编译器已经将 C 语言中所有全局变量的地址都收集到 GOT 中,因此我们很容易对其Flash 地址的内容进行修正,参考代码如下:

f94ba028-341f-11ed-ba43-dac502259ad0.png

4、总结

除非你仅仅是运行一小块代码,否则开发位置无关的 STM32 完整工程,不仅仅要设置正确的编译器选项,还要保证它所链接的预编译的库不含有绝对地址引用,要保证所有源代码里没有对绝对地址的硬编码,包括修改 data 区的 Flash 起始地址,中断向量表的内容与位置,以及 GOT 的内容。

关键字:STM32  完整工程  编译工具 引用地址:如何开发与位置无关的 STM32 完整工程

上一篇:STM32启动文件简介、详细步骤及代码讲解
下一篇:如何用STM32CubeIDE软件实现STM32外部中断

推荐阅读最新更新时间:2024-11-17 01:48

详解stm32定时器的编码器模式
前言: 增量式编码器在实际应用中还是很常见。stm32的定时器带的也有编码器模式。所用的编码器是有ABZ三相,其中ab相是用来计数,z相输出零点信号。AB相根据旋转的方向不同,输出的波形如下图所示: 第一步:具体配置如下图: 重点说明: 一、如果编码模式设置为 Encoder Mode TI1 and TI2 则会默认检测AB相的上升沿与下降沿; 每一个上升沿和下降沿都触发计数,所以每转一格计数器就会+4;那么上图计数周期设置为400;也就是编码器转100格,计数器就会置零,重新开始计数! 二、关于Polarity参数:这个参数的意思是在检测到上升沿的时候就触发encoder捕获AB相的值,而并不是这里设置的是上升沿就只检
[单片机]
详解<font color='red'>stm32</font>定时器的编码器模式
STM32标准库中DMA配置详解 (标准库版)
写博客Mark下自己对STM32中DMA功能的一些分析: 先看上图中下左侧的偏移地址,偏移地址是相对于DMA1_BASE的相对地址。查表可得DMA1_BASE的实际物理地址是:0x4002 0000 .故我们可以得出这些寄存器的实际地址是什么。实际上我们对寄存器配置的时候也是对这几个寄存器值进行修改。 DMA_ISR: 0x4002 0000 DMA_IFCR 0x4002 0004 DMA_CCR1: 0x4002 0008 DMA_CNDTR1: 0x4002 000C DMA_CPAR1: 0x4002 0010 DMA_CMAR1: 0x4002 0014 接下来,我们去STM32的程序
[单片机]
<font color='red'>STM32</font>标准库中DMA配置详解 (标准库版)
实现stm32在FSK调制解调器的综合设计
大致要求:设计一个FSK调制解调器,基带信号码速率为2000B/s,载波速率为4khz和8khz,解调信号要能完整还原基带信号。实现方法多种多样,通信领域内调制解调器的设计大多数用的都是硬件电路,鉴于笔者对编程情有独钟(其实笔者还是懂一点电路设计知识的~),所以最终决定用stm32来设计,纯编程实现。看起来高大上,但实际做起来不难,不过有挺多东西要考虑的。 总的设计思路如下: 首先是基带信号的产生,它也是我们要调制和解调的目标。基带信号由一连串随机的码元序列构成,为了模拟随机的码元序列,笔者用定时器设计8位的PN码序列,码元速率为2000B/s。定时器3定时0.5ms,每进入一次中断,变量num加一,设置一次I
[单片机]
实现<font color='red'>stm32</font>在FSK调制解调器的综合设计
STM32单片机SPI极性和相位的设置方法
SPI分主设备和从设备,两者通过SPI协议通讯。而设置SPI的模式,是从设备的模式,决定了主设备的模式。所以要先去搞懂从设备的SPI是何种模式,然后再将主设备的SPI的模式,设置和从设备相同的模式,即可正常通讯。对于从设备的SPI是什么模式,有两种:(1)固定的,有SPI从设备硬件决定的SPI从设备,具体是什么模式,相关的datasheet中会有描述,需要自己去datasheet中找到相关的描述,即:关于SPI从设备,在空闲的时候,是高电平还是低电平,即决定了CPOL是0还是1;然后再找到关于设备是在上升沿还是下降沿去采样数据,这样就是,在定了CPOL的值的前提下,对应着可以推算出CPHA是0还是1了。举例1:CC2500-Low
[单片机]
<font color='red'>STM32</font>单片机SPI极性和相位的设置方法
STM32入门笔记1
STM32入门笔记:STM32+ST-Link ST-link仿真器跟一块最小系统的STM32开发板都买回来好久了,到最近终于有空开始动手学习。 --|||在ST-Link配的光盘里有个J-Link的介绍文档跟驱动程序,结果我就看完把驱动一装,连上开发板按着教程开始想烧录个程序进去试试。结果一直无法识别到J-Link设备…我居然还上旺旺找买仿真器的那个卖家问为什么我发现不了设备,我的仿真器上面的灯不亮?卖家问了下情况后很纳闷:你买的是ST-Link当然用J-Link的驱动程序连接不到设备啦…当时自己也被自己这个乌龙给雷到了。 好吧,又接着问了一下,原来ST-Link不用装驱动,但是用ST-Link无法像使用J-Link
[单片机]
stm32串口发送16进制字符和16进制数的区别和具体实现方法
在调试一个stm32串口通信时,下位机设置好的发送方式采用串口助手接收后已经可以正常离线解包。但是由于需要实时解包并存储,因此写了一个实时解包存储的上位机,通信正常后接收的数据一直有误。经过单步调试,发现单片机发送的数据帧是字符串,并不是16进制数,而在线解包程序直接是对16进制数进行处理的,这就导致的比如16进制的数0xAA(170),下位机发送的是“AA (字符串),上位机处理的16进制数是41 41(10进制为65*100+65)。查询‘A’的ASCII码可知,‘A’的ASCII码对应的10进制数为65,16进制数为41,也就是说上位机把接收到的16进制字符串当16进制数处理了,所以出错。 解决方法两个,一个是修改下位机
[单片机]
STM32学习笔记 — 之GPIO端口篇
最近刚开始学习STM32,所以从最基本的GPIO开始学起;首先看看STM32的datasheet上对GPIO口的简单介绍: 每个GPI/O 端口有两个32 位配置寄存器(GPIOx_CRL,GPIOx_CRH),两个32位数据寄存器(GPIOx_IDR,GPIOx_ODR),一个32 位置位/复位寄存器(GPIOx_BSRR),一个16 位复位寄存器(GPIOx_BRR)和一个32 位锁定寄存器(GPIOx_LCKR)。 GPIO 端口的每个位可以由软件分别配置成多种模式。每个I/O 端口位可以自由编程,然而I/0 端口寄存器必须按32 位字被访问(不允许半字或字节访问)。GPIOx_BSRR 和GPIOx_BRR 寄存器允许对
[单片机]
STM32中结构体的学习
在STM32中经常遇到一些结构体的设置,结构体的好处不用多少了,仔细看《C语言设计》谭浩强版本,这样设计让项目很有层次,一目了然,这里介绍一下结构体指针在STM32库函数的应用,如下 例子: (GPIO_TypeDef *) 这里的作用是 把GPIO_BASE地址转换为GPIO_TypeDef结构体指针类型, 在C语言中经常遇到结构体指针,这样定义 GPIO_TyepeDef * a; 其中,a为结构体指针,而不是 * a是结构体指针,
[单片机]
<font color='red'>STM32</font>中结构体的学习

推荐帖子

LM324四运放的应用
LM324四运放的应用看看了啊,呵呵很好,不错的说很好很不错,希望能多分享些运放方面的资料!很不错,谢谢楼主
pontiff 模拟电子
用汇编如何将多个8位数据累加到16位R寄存器中
用汇编语言如何实现多个8位数据累加到16位R寄存器中的操作?谢谢!用汇编如何将多个8位数据累加到16位R寄存器中
yuguyu 微控制器 MCU
各位大侠请给小弟指点迷津?
我一直很喜欢单片机,也想往这方面发展,可是我找工作却到处碰壁,我现在只会51单片机,不过在自学avr,编程仿真都还行,用c和汇编,也会ptotel,数电模电都行,但去找工作人家不是嫌我没经验,就是要求高,像得会dsp啊,arm啊,所以我现在的水平找不到与单片机相关的工作,我真的很受伤,可是我知道如果找了一份与这不相关的,以后就很难再跨进单片机的世界了,我是说干单片机的工作,我现在是得放弃随便找一份工作,还是一边找,一边坚持学习。我真的很犹豫,希望能得到大侠们的指点。小弟在此先谢了。各位大侠
假面的告白 微控制器 MCU
单片机复位种类和故障
外部复位(ExternalReset)它是影响时钟模块和所有内部电路,属于同步复位,但外部Reset引脚为逻辑低电平。在引脚变为低电平后,CPU的复位控制逻辑单元确认复位状态直到Reset释放。复位控制逻辑保持复位低电平状态,在额外512个时钟周期内。因为当复位引脚为低电平时与MCU执行复位命令是相互冲突的,因此复位引脚必须保证520时间周期内低电平才能保证外部复位被外部总线辨识出来。上电复位(Power-onreset)它是由外部总线产生
Jacktang 微控制器 MCU
TDK-Lambda电源在系留无人机及水下机器人中的应用
近来,在与客户工程师的沟通中了解了一些系留无人机和水下机器人等方面的系统应用,两种设备虽然完全属于不同的行业领域,但是供电的构架却有很多相似之处,这里粗略整理一下,分享给大家。系留无人机电源构架系留无人机系统一般分为地面部分和空中部分,两部分通过线缆连接,地面部分一般由电源和控制等部分组成,空中部分一般由蓄电池,机身,传感器等部分组成。相对于其他类型的无人机,系留无人机因为有线缆的存在,可不受电能限制
安泰测试设备 测试/测量
从技术角度分析,汽车生物识别技术的未来
随着科技的进步,汽车行业正在经历一场显著的转变,生物识别技术在这一转变中扮演了关键角色。从增强车辆安全性开始,生物识别技术还可以为电子支付、健康监测、个性化设置等其他应用领域开辟了新的可能性。本文综合了指纹识别、面部识别及其他生物识别技术在汽车中的应用现状,探讨其对安全、便利和个性化体验的影响,并展望这些技术在未来的潜力。传统的基于钥匙和无钥匙的车辆进入系统长期以来是车辆访问控制的标准。然而,这些系统存在固有的局限性,尤其是随着技术漏洞(如中继攻击和信号拦截)的增加,车辆盗窃和
eric_wang 汽车电子
小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved