Cortex-M3的杂项知识
必备知识
stm32的框图
![image-20240617195653771](https://img2024.cnblogs.com/blog/3224932/202409/3224932-20240903153227627-1511027890.png)
Cortex-M微控制器复位流程
- 向量表中向量地址的最低为应该为1,这里指的是向量表中存储的地址
如何查看反汇编代码
- 汇编语言:汇编语言是一种低级语言,是针对某种机器而言的。
应用程序的状态
应用程序具有静止状态和运行状态。静止态的程序被存储在非易失存储器中,如 STM32 的内部 FLASH,因而系统掉电后也能正常保存。但是当程序在运行状态的时候,程序常常需要修改一些暂存数据,由于运行速度的要求,这些数据往往存放在内存中(RAM),掉电后这些数据会丢失。因此,程序在静止与运行的时候它在存储器中的表现是不一样的。
ELF格式
ELF 是 Executable and Linking Format 的缩写,译为可执行链接格式,该格式用于记录目标文件的内容。
目标文件主要有如下三种类型:
-
- 可重定位的文件(Relocatable File),包含基础代码和数据,但它的代码及数据都没有指定绝对地址,因此它适合于与其他目标文件链接来创建可执行文件或者共享目标文件。 这种文件一般由编译器根据源代码生成。例如 MDK 的 armcc 和 armasm 生成的.o 文件就是这一类,另外还有 Linux的.o 文件,Windows 的 *.obj 文件。
-
- 可执行文件(Executable File) ,它包含适合于执行的程序,它内部组织的代码数据都有固定的地址(或相对于基地址的偏移),系统可根据这些地址信息把程序加载到内存执行。这种文件一般由链接器根据可重定位文件链接而成,它主要是组织各个可重定位文件,给它们的代码及数据一一打上地址标号,固定其在程序内部的位置,链接后,程序内部各种代码及数据段不可再重定位(即不能再参与链接器的链接)。例如 MDK 的 armlink 生成的.elf 及.axf 文件,(使用 gcc 编译工具可生成.elf 文件,用 armlink 生成的是*.axf 文件,.axf 文件在.elf 之外,增加了调试使用的信息,其余区别不大,后面我们仅讲解.axf 文件),另外还有 Linux 的/bin/bash 文件,Windows 的*.exe 文件。
-
- 共享目标文件(Shared Object File), 它的定义比较难理解,我们直接举例,MDK生成的*.lib 文件就属于共享目标文件,它可以继续参与链接,加入到可执行文件之中。另外,Linux 的.so,如/lib/ glibc-2.5.so,Windows 的 DLL 都属于这一类。
下载到单片机中的程序
- *.axf 文件是由多个*.o 文件链接而成的,而*.o 文件由相应的源文件编译而成,一个源文件对应一个*.o 文件
- 由于都使用 ELF 文件格式,.o 与.axf 文件的结构是类似的,它们包含ELF 文件头、程序头、节区(section)以及节区头部表
- 若编译过程无误,即可把工程生成前面对应的*.axf 文件,而在 MDK 中使用下载器(DAP/JLINK/ULINK 等)下载程序或仿真的时候,MDK 调用的就是*.axf 文件,它解释该文件,然后控制下载器把*.axf 中的代码内容下载到 STM32 芯片对应的存储空间,然后复位后芯片就开始执行代码了.
- 默认情况下 MDK 都不会生成 hex 及 bin 文件,需要配置工程选项或使用 fromelf 命令
- 下载到单片机的程序本质上是机器码
分散加载
-
当工程按默认配置构建时,MDK 会根据我们选择的芯片型号,获知芯片的内部FLASH 及内部 SRAM 存储器概况,生成一个以工程名命名的后缀为*.sct 的分散加载文件(Linker Control File,scatter loading),链接器根据该文件的配置分配各个节区地址,生成分散加载代码,因此我们通过修改该文件可以定制具体节区的存储位置。
-
从静止状态到运行状态,它们之间应有一个转化过程,把存储在 FLASH 中的 RW-data 数据拷贝至 SRAM程序中。程序中具有一段名为“__scatterload”的分散加载代码,它是由 armlink 链接器自动生成的。执行这些指令后数据就会从 FLASH 地址加载到内部 SRAM 的地址。而 “__scatterload ”的代码会被“main”函数调用,见代码清单 43-8,main 在启动文件中的“Reset_Handler”会被调用,因而,在主体程序执行前,已经完成了分散加载过程。
使用fromelf工具
- 使用fromelf工具,可以将ELF格式的文件进行转换
stm32的内存编址
-
遇到的问题:
问题:书中写向量表从0地址开始,以4个字节为一个单位,但是给出的地址增长不过才4个比特位。然后查询到单片机内存的地址访问存储单元是按照字节编址的。
-
编址意味着每一个地址位对应着多大的内存空间
-
有按字节编址 和 按字编址:字节8位,字为系统的数据宽度,则32位,按字节编址的寻址空间更大
-
32位的系统可以生成32位的地址,那么它可以生成 2的32次方 个不同的地址,每个地址指向唯一的内存地址,每个位置存储一个字节。
-
所以2的32次方 = 4,294,967,296 字节,4,294,967,296 /1024/1024/1024 = 4GB,所以stm32的寻址空间为4GB
-
函数名和地址
在编程中,函数名通常是指向函数代码起始地址的指针。这种设计使得函数可以被调用,因为调用函数实际上就是跳转到函数代码的起始地址执行。
函数名和地址的关系
- 函数名:在源代码中,函数名是用来标识函数的,它使得程序员可以方便地调用函数。
- 函数地址:在编译后的程序中,函数名被编译器转换为函数的起始地址,这个地址指向函数代码在内存中的位置。
函数调用的过程
当程序调用一个函数时,实际上发生了以下几个步骤:
- 参数传递:将函数的参数传递给函数,这通常涉及到将参数压入栈中。
- 跳转:将程序的执行流程跳转到函数的起始地址,这个地址就是函数名所指向的地址。
- 执行:执行函数的代码,直到遇到返回语句或函数的末尾。
- 返回:将执行流程返回到调用函数的位置,继续执行后续代码。
函数指针
在某些编程语言中,如C语言,函数名可以被视为指向函数代码起始地址的指针。这意味着可以将函数名赋值给一个函数指针变量,然后通过这个变量来调用函数。
int add(int a, int b) {
return a + b;
}
int main() {
int (*func_ptr)(int, int) = add; // func_ptr是指向add函数的指针
int result = func_ptr(2, 3); // 通过函数指针调用add函数
return 0;
}
在这个例子中,func_ptr
是一个指向add
函数的指针,它指向add
函数的起始地址。通过func_ptr
可以调用add
函数,就像直接调用add
函数一样。
总的来说,函数名本身就是地址,这个地址指向函数代码的起始位置。在编译后的程序中,函数名被转换为这个地址,使得程序可以跳转到这个地址执行函数代码。
大字节序与小字节序(大端存储和小端存储)
- 大字节序的大是怎样解读?
"大字节序"中的"大"字,是用来形容在多字节数据存储时,最高有效字节(Most Significant Byte, MSB)被放置在内存地址的较低位置(或在序列的前面)的特性。这里的"大"并不是指字节的物理大小,而是指数据值的"重要性"或"权重"。在大字节序中,数据的高位部分优先存储,体现了从左到右(或地址低位到高位)的"由大至小"的优先级排序,这也是为什么称为"大字节序"的原因。
以十六进制数
0x12345678
为例,如果这是一个32位整数,在大字节序下,它在内存中的存储布局(假设从低地址到高地址)会是12 34 56 78
,其中12
作为最高有效字节存储在最低地址处,体现了"大"字节(即重要字节)在前的存储规则。这种存储方式符合人类阅读数字的习惯,即从左到右依次是千位、百位、十位、个位,体现了数值大小的递减顺序。
指针加1
在C语言中,对一个指针进行加1操作(如 ptr++
或 ptr += 1
),其意义取决于指针所指向的数据类型。具体来说,对指针加1会使指针向前移动到下一个同类型数据的地址上。这里的“下一个”是按照该数据类型的大小来衡量的。
例如,假设 ptr
是一个指向 int
类型数据的指针,那么 ptr++
就会使 ptr
指向紧随当前 int
值之后的那个 int
值的地址。由于 int
类型通常占用4个字节(在32位系统上),所以 ptr
实际上会向前移动4个字节。
同样,如果 ptr
指向一个 char
类型的数据(通常为1字节),那么 ptr++
就会让 ptr
指向下一个字节,即下一个 char
值的地址。
简而言之,对指针加1的操作是按指针所指向的数据类型的大小来移动指针的,这样设计的目的是为了方便遍历数组或连续内存块中的元素。这种操作在处理数组、内存管理和字符串等领域非常有用。
上电之后ROM和RAM
- 根据RAM的特性,MCU每次上电之后RAM里面的值是随机的
- ROM里面都是FFFF
启动过程
- 上电之后根据boot0、boot1的高低电平选择启动方式,
- 从内部FLASH启动时,内部FLASH的
0x0000 0000
和0x0000 0004
被映射到内部FLASH的首地址0x0800 0000
和0x0800 0004
,CM-3内核硬件上从0x0000 0000
和0x0000 0004
地址分别取出值赋给MSP,和PC。 - 随后跳转到
0x0800 0004
这个地址所在处的Reset_Handler
去执行,接着运行SystemInit
初始化时钟,- 运行
__main
调用分散加载代码_scatterload
,它会把FLASH中的RW-data复制到RAM中,然后在RAM区开辟一块ZI-data的空间,然后初始化为0。最后跳到用户的main函数中执行- 分散加载代码来自于*.sct分散加载文件,链接器根据该文件的配置分配各个节区地址,生成分散加载代码。
- *.sct分散加载文件默认会由MDK为我们生成,也可以自己编写。
- 运行
- 从内部FLASH启动时,内部FLASH的
启动文件做了什么?
- 定义栈 数据段
- 定义堆 数据段
- 定义一个RESET数据段
- 向量表从 FLASH 的 0 地址开始放置,以 4 个字节为一个单位,地址 0 存放的是栈
顶地址,0X04 存放的是复位程序的地址,以此类推。从代码上看,向量表中存放的都
是中断服务函数的函数名,可我们知道 C 语言中的函数名就是一个地址- 定义一个名称为.text 的代码段
- Reset_Handler: 复位子程序是系统上电后第一个执行的程序,调用 SystemInit 函数初始化系统时钟,然后调用 C 库函数_mian,最终调用 main 函数去到 C 的世界,SystemInit 初始化时钟,重定位向量表的地址,__main 是一个标准的 C 库函数,主要作用是初始化用户堆栈
- 在启动文件里面已经帮我们写好所有中断的中断服务函数,跟我们平时写的中断服务函数不一样的就是这些函数都是空的并定义为weak,真正的中断复服务程序需要我们在外部的 C 文件里面重新实现,这里只是提前占了一个位置而已。程序就会跳转到启动文件预先写好的空的中断服务程序中,并且在这个空函数中无限循环,即程序就死在这里
预分频和分频
在数字电路设计和微控制器的时钟管理中,"prescale"(预分频)和"divider"(分频器)这两个术语经常被提及,尽管它们在某些情况下可以互换使用,但它们在概念上有着细微的差别。
Prescale(预分频)
预分频器是位于时钟源和目标模块(如定时器、计数器等)之间的第一个分频级。其主要作用是降低时钟频率,这样可以减慢目标模块的计数速度,延长其周期,避免在高速时钟下过快地溢出或达到最大计数值。预分频器可以显著降低功耗,并且允许使用更长的计数周期,这对于需要较长时间间隔的测量或控制任务尤为重要。
Divider(分频器)
分频器是一个更通用的术语,它可以指代任何将输入时钟频率降低到较低频率的电路。分频器可以在信号路径中的任何位置,不仅限于最开始的阶段。分频器可以是固定的比率,也可以是可编程的,允许用户根据需要调整分频比。
在许多微控制器中,"prescale"通常是指定时器或特定模块的第一级分频,而"divider"可能指的是更广泛的时钟管理操作,包括但不限于总线时钟、外设时钟等的分频。
区别总结
- 位置:预分频器通常是第一个分频级,紧接在时钟源之后;而分频器可以位于信号路径中的任意位置。
- 目的:预分频器主要用于降低输入时钟频率,以便于后续模块处理;分频器的目的更加多样,包括但不限于匹配不同模块的时钟需求、降低功耗、适应不同的通信协议等。
- 灵活性:预分频器可能提供有限的分频比选择,而分频器可能提供更广泛的分频比调整范围。
在实际应用中,预分频和分频往往结合使用,以满足各种时钟管理和信号同步的需求。例如,在配置定时器时,可能首先使用预分频器来降低时钟频率,然后在定时器内部使用额外的分频器来进一步调整时钟,以达到所需的计数速率。