STM32-内存五区
局 部 变 量、全 局 变 量 、堆 、堆 栈 、静 态 和 全局一个由C/C++编译的程序占用的内存分为以下几个部分栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。堆区(heap) — 由程序员分配和释放,若程序员不释放,程序结束时可能由OS回收。全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量、未初始化的静态变量在相邻的另一块区域。文字常量区 — 常量字符串就是放在这里的。程序代码区 — 存放函数体的二进制代码。堆栈(stack)是内存中的一个连续的块。一个叫堆栈指针的寄存器(SP)指向堆栈的栈顶。堆栈的底部是一个固定地址。堆栈有一个特点就是,后进先出。也就是说,后放入的数据第一个取出。堆(heap)是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。栈(stack)空间,用于局部变量,函数调时现场保护和返回地址,函数的形参等。堆(heap)空间,主要用于动态内存分配,也就是说用 malloc,calloc, realloc 等函数分配的变量空间是在堆上。以 STM32F429 为例,堆栈是在 startup_stm32f429xx.s 文件里面设置:在高级语言中,函数调用、函数中定义的变量都用到栈(stack)。用malloc, calloc, realloc等函数分配得到变量空间是在堆(heap)上。所有函数体外定义的是全局量。加了static修饰符后不管放在在哪里都属于静态变量,存放在全局区(静态区)在所有函数体外定义的static变量表示在该文件中有效,不能extern到别的文件用。在函数体内定义的static表示只在该函数体内有效。函数中的"xiaoshidi"这样的字符串存放在常量区。举个具体的例子加深大家的理解。main.c 文件:int a = 0; //全局初始化区, 可以被其他c文件 extern 引用
static int ss = 0; //静态变量,只允许在本文件使用
char *p1; //全局未初始化区
void main(void)
{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456\0在常量区,p3在栈上。
static int c =0; //全局(静态)初始化区
p1 = (char *)malloc(10); //在堆区申请了10个字节空间
p2 = (char *)malloc(20); //在堆区申请了20个字节空间
strcpy(p1, "123456"); /* 123456字符串(结束符号是0,总长度7)放在常量区,编译器可能会
将它与p3所指向的"123456"优化成一个地方 */
}局部变量在一个函数内部定义的变量是内部变量,它只在本函数范围内有效,也就是说只有在本函数内才能使用它们,在此函数以外是不能使用这些变量的,这称为局部变量。使用局部变量注意以下问题:不同函数中可以使用相同名字的变量,它们代表不同的对象,互不干扰。形式参数也是局部变量。局部变量的作用域在函数内部。全局变量在函数内部定义的变量是局部变量,而在函数之外定义的变量称为外部变量,也就是全局变量。使用全局变量的注意事项:全局变量可以为本文件中其他函数所共用。它的有效范围为从定义变量的位置开始到本源文件结束。设置全局变量的作用是增加了函数间数据联系的渠道。如果在同一个源文件中,外部变量和局部变量同名,则在局部变量的作用范围内,外部变量被“屏蔽”,即外部变量将不起作用。使用全局变量的缺点程序设计中,建议不要创建太多的全局变量,主要是出于以下三点考虑:全局变量在程序的执行过程中都占用存储单元,而不是仅在需要时才占用存储单元。函数的通用性降低了,因为函数在执行时要依赖于其所在的外部变量。如果将一个函数移植到另一个文件中,还要将有关的外部变量及其值一起移植过去。使用全局变量过多,会降低程序的清晰性,特别是多个函数都调用此变量时。变量的存储类别从变量的作用域来分,可以分为全局变量和局部变量,而从变量值存在的时间来看,可以分为静态存储方式和动态存储方式。静态存储方式:指在程序运行期间由系统分配固定的存储空间方式。动态存储方式:在程序运行期间根据需要进行动态的分配存储空间方式。全局变量存储在静态存储区中,动态存储区可以存放以下数据:函数形式参数,在调用函数时给形参分配存储空间。局部变量(未加 static 声明的局部变量)。函数调用时的现场保护和返回地址等。用 static 声明局部或者全局变量有时候希望函数中的局部变量的值在函数调用结束后不消失而保留原值,即占用的存储单元不释放,在下一次该函数调用时,该变量已有值,就是上一次函数调用结束时的值。这时可以使用关键字 static 进行声明。用 static 声明一个变量的作用:对局部变量用 static 声明,则使用该变量在整个程序执行期间不释放,为其分配的的空间始终存在。全局变量用 static 声明,则该变量的作用域只限于本文件模块(即被声明的文件中)。关注微信公众号:[<font=red>果果小师弟],获取更多精彩内容!
STM32的内存管理相关(内存架构,内存管理,map文件分析)
把以前看过的做过的笔记,还有网上参考的部分好文章,利用十一假期好好梳理了一遍,希望对大家也对自己以后查看有帮助
..梳理章节架构,完善内容
..5.1 STM32启动方式完善
..5.2 用户bootloader简要说明
..GCC环境下部分相关图例补充
..增加第六节GCC下生成的.map文件
..增加 5.4 ARM 和 x86cpu 指令集RISC和CISC说明
..更新地址说明错误问题0x0800 0000
..更新部分细节错误 使用一个STM32芯片,对于内存而言有两个直观的指标就是 RAM 大小,FLASH大小,比如STM32F103系列(其他系列也是如此):那么着两个大小意味着什么?怎么去理解这两个内存,那就得从什么是Flash,什么是RAM说起。一、FLASH 和 RAM基本概念先来看一张图:1.1 FLASH是什么通过上图我们可以知道,FLASH属于 非易失性存储器:扩展一点说,FLASH又称为闪存,不仅具备电子可擦除可编程(EEPROM)的性能,还不会断电丢失数据同时可以快速读取数据,U盘和MP3里用的就是这种存储器。在以前的嵌入式芯片中,存储设备一直使用ROM(EPROM),随着技术的进步,现在嵌入式中基本都是FLASH,用作存储Bootloader以及操作系统或者程序代码或者直接当硬盘使用(U盘)。然后 Flash 主要有两种NOR Flash和NADN Flash。(对于这两者的区别,下面的话供参考,因为这些介绍都是基于早些年的技术了)NOR Flash的读取和我们常见的SDRAM的读取是一样,用户可以直接运行装载在NOR FLASH里面的代码,这样可以减少SRAM的容量从而节约了成本。NAND Flash没有采取内存的随机读取技术,它的读取是以一次读取一块的形式来进行的,通常是一次读取512个字节,采用这种技术的Flash比较廉价。用户不能直接运行NAND Flash上的代码,因此好多使用NAND Flash的开发板除了使用NAND Flah以外,还作上了一块小的NOR Flash来运行启动代码。STM32单片机内部的FLASH为 NOR FLASH。Flash 相对容量大,掉电数据不丢失,主要用来存储 代码,以及一些掉电不丢失的用户数据。1.2 RAM是什么RAM 属于易失性存储器:RAM随机存储器(Random Access Memory)表示既可以从中读取数据,也可以写入数据。当机器电源关闭时,存于其中的数据就会丢失。比如电脑的内存条。RAM有两大类,一种称为静态RAM(Static RAM/SRAM),SRAM速度非常快,是目前读写最快的存储设备了,但是它也非常昂贵,所以只在要求很苛刻的地方使用,譬如CPU的一级缓冲,二级缓冲。另一种称为动态RAM(Dynamic RAM/DRAM),DRAM保留数据的时间很短,速度也比SRAM慢,不过它还是比任何的ROM都要快,但从价格上来说DRAM相比SRAM要便宜很多,计算机内存就是DRAM的。DRAM分为很多种,常见的主要有FPRAM/FastPage、EDORAM、SDRAM、DDR RAM、RDRAM、SGRAM以及WRAM等,这里介绍其中的一种DDR RAM。DDR RAM(Date-Rate RAM)也称作DDR SDRAM,这种改进型的RAM和SDRAM是基本一样的,不同之处在于它可以在一个时钟读写两次数据,这样就使得数据传输速度加倍了。这是目前电脑中用得最多的内存,而且它有着成本优势,事实上击败了Intel的另外一种内存标准-Rambus DRAM。在很多高端的显卡上,也配备了高速DDR RAM来提高带宽,这可以大幅度提高3D加速卡的像素渲染能力。为什么需要RAM,因为相对FlASH而言,RAM的速度快很多,所有数据在FLASH里面读取太慢了,为了加快速度,就把一些需要和CPU交换的数据读到RAM里来执行(注意这里不是全部数据,只是一部分需要的数据,这个在后面介绍STM32的内存管理中会提到)。STM32单片机内部的 RAM 为 SRAM。RAM相对容量小,速度快,掉电数据丢失,其作用是用来存取各种动态的输入输出数据、中间计算结果以及与外部存储器交换的数据和暂存数据。二、STM32的内存架构2.1 Cortex-M3的存储器映射分析在《ARM Cotrex-M3权威指南》中有关 M3的存储器映射表:存储器映射 是用 地址来表示 对象,因为Cortex-M3是32位的单片机,因此其PC指针可以指向2^32=4G的地址空间,也就是图中的 0x00000000到0xFFFFFFFF的区间,也就是将程序存储器、数据存储器、寄存器和输入输出端口被组织在同一个4GB的线性地址空间内,数据字节以小端格式存放在存储器中。2.2 STM32 的存储器映射分析STM32存储器映射表(选用的是STM32F103VE的,不同的型号Flash 和 SRAM 的地址空间不同,起始地址都是一样的):那么我们所需要分析的STM32 内存,就是图中 0X0800 0000开始的 Flash 部分 和 0x2000 0000 开始的SRAM部分,这里还要介绍一个和Flash模块相关的部分:2.3 STM32的 Flash 组织参考博文:深入理解STM32内存管理STM32的Flash,严格说,应该是Flash模块。该Flash模块包括:Flash主存储区(Main memory)、Flash信息区(Informationblock),以及Flash存储接口寄存器区(Flash memory interface)。主存储器,该部分用来存放代码和数据常数(如加const类型的数据)。对于大容量产品,其被划分为256页,每页2K,小容量和中容量产品则每页只有1K字节。主存储起的起始地址为0X08000000,B0、B1都接GND的时候,就从0X08000000开始运行代码。信息块,该部分分为2个部分,其中启动程序代码,是用来存储ST自带的启动程序,用于下载,当B0接3.3V,B1接GND时,运行的就这部分代码,用户选择字节,则一般用于配置保护等功能。闪存储器块,该部分用于控制闪存储器读取等,是整个闪存储器的控制机构。对于主存储器和信息块的写入有内嵌的闪存编程管理;编程与擦除的高压由内部产生。在执行闪存写操作时,任何对闪存的读操作都会锁定总线,在写完成后才能正确进行,在进行读取或擦除操作时,不能进行代码或者数据的读取操作。三、STM32 的内存管理STM32 的内存管理起始就是对0X0800 0000 开始的 Flash 部分 和 0x2000 0000 开始的 SRAM 部分使用管理3.1 C/C++ 程序编译后的存储数据段参考博文:STM32内存结构介绍在了解如何使用内存管理之前,先得理解一下 6 个储存数据段 和 3种存储属性区 的概念:.data数据段,储存已初始化且不为0的全局变量和静态变量(全局静态变量和局部静态变量)。static声明的变量放在data段。数据段属于静态内存分配,所以放在RAM里,准确来说,是在程序运行的时候需要在RAM中运行。.BSSBlock Started by Symbol。储存未初始化的,或初始化为0的全局变量和静态变量。BSS段属于静态内存分配,所以放在RAM里。.text(CodeSegment/Text Segment)代码段,储存程序代码。也就是存放CPU执行的机器指令(machineinstructions)。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。放在Flash里。.constdata储存只读常量。const修饰的常量,不管是在局部还是全局放在Flash 里。所以为了节省 RAM,把常量的字符串,数据等 用const声明heap(堆)堆是用于存放进程运行中被动态分配的内存段。他的大小并不固定,可动态扩张或者缩减,由程序员使用malloc()和free()函数进行分配和释放。当调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。放在RAM里其可用大小定义在启动文件startup_stm32fxx.s中。stack(栈)栈又称堆栈,是用户存放程序临时创建的局部变量,由系统自动分配和释放。可存放局部变量、函数的参数和返回值(但不包括static声明的变量,static意味着 放在 data 数据段中)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。???由于栈的先进先出(FIFO)特点???上面这句话正确吗?Cortex-M3/M4的堆栈是向下生长,第一个入栈的元素应该是最后一个才能出来??所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。放在RAM里其大小定义在启动文件startup_stm32fxx.s中。3.2 STM32 程序编译后的内存占用情况3.2.1 MDK 编译MDK编译后的结果:Code:程序代码部分。 .text 段放在ROM里面,就是Flash,需要占用flash空间RO-data(Read Only)只读数据程序定义的常量,只读数据,字符串常量(const修饰的).constdata 段放在flash里面,需要占用flash空间RW-data(Read Write)可读可写数据已经初始化的全局变量和静态变量(就是static修饰的变量); .data 段需要在 RAM里面运行,但是起初需要保存在 Flash里面,程序运行后复制到 RAM里面运行,需要占用Flash空间ZI-data(Zero Initialize)未初始化的全局变量和静态变量,以及初始化为0的变量;.BSS段ZI的数据全部是0,没必要开始就包含,只要程序运行之前将ZI数据所在的区域(RAM里面)一律清 0,不占用Flash,运行时候占用RAM.heap 和 stack 其实也属于 ZI,只不过他不是程序编译就能确定大小的,必须在运行中才会有大小,而是是变化的因为RAM掉电丢失,所以 RW-data 数据也得下载到ROM(flash) 中,在运行的时候复制到 RAM中运行,如下图所示(图中的地址也是错的,应该是从0x0800 0000 开始):由上我们得知:程序占用 Flash = Code + RO data + RW data 程序运行时候占用 RAM = RW data + ZI data。 Code + RO data + RW data 的大小也是生成的 bin 文件的大小3.2.1 GCC 编译GCC编译结果:GCC编译,图中红色的部分是占用 Flash 的大小:Flash = text + data 。蓝色部分是运行时候占用 RAM大小:RAM = data + bss。3.3 STM32 程序的内存分配我们前面说到的 stack(栈) 和 heap(堆),程序编译完成以后并不能知道运行时候实际占用RAM 大小。但是我们可以知道的是 stack(栈) 和 heap(堆)的起始地址,和能够使用的最大空间,我们先看能够使用的空间大小。3.3.1 MDK 环境MDK是在 startup_stm32fxxx.s 中定义的:在 startup_stm32fxxx.s 中我们可以看到关于 Stack_Size 和 Heap_Size的定义,图中的定义就是规定本程序中 栈 的大小为 1K, 堆的大小为 0.5K。startup_stm32fxxx.s文件是系统的启动文件,startup_stm32fxxx.s主要完成三个工作:栈和堆的初始化、定位中断向量表、调用Reset Handler。(关于STM32的启动文件,我们有单独的一篇文章,请查看另一篇博文 STM32的启动过程(startup_xxxx.s文件解析))我们在生成的.map文件可以看到 HEAP 和 STACK的起始地址(不懂的可以先看博文下面一节的内容——四、MDK生成的.map文件简析),我们要注意的是:堆使用时候从起始地址开始,往上加栈使用时候从结束地址,就是__initial_sp(栈顶指针的地址)开始,往下减他们的空间大小定义好了,如果入栈元素过大,使得元素减到了堆的地址范围,就是栈溢出,这会导致改变堆中相应地址元素的值。同样的,当申请的动态内存过大,使得堆的变量加到了栈的地址范围,就是堆溢出。在实际项目中,如果程序复杂,中断嵌套太多,栈需要多设置一点空间如果你使用裸机程序,从来不使用标准库的malloc,heap可以没有,因为永远不会用到,还一直占用着一片RAM区。所以在我们知道了以上的知识后,我们可以按照自己的工程需求,定义Stack_Size 和 Heap_Size。3.3.2 GCC 环境如果是在GCC编译器下面,关于 Stack_Size 和 Heap_Size的定义如下图:四、MDK生成的.map文件简析为了更深层次的理解上述内容,我们还有必要分析一下MDK生成的 .map 文件,那么既然要分析,除了我们关注的flash 和 ram部分的内容,其他的地方也稍微做一下笔记:4.1 Section Cross References主要是不同文件中函数的调用关系:我们查看一下 clock.c 文件下的 rt_tick_increase函数:4.2 Removing Unused input sections from the image被删除的冗余函数:删除函数功能在MDK的配置中可以设置,勾选以后删除得多,不勾选删除得少,如下图:在 Removing Unused input sections from the image 的最后会列出删除的冗余函数的大小,如果在MDK上改变上图所示的配置,下图中的删除总代码可以看到变化:4.3 Image Symbol Table4.3.1 Local Symbols局部标号,用Static声明的全局变量地址和大小,C文件中函数的地址和用static声明的函数代码大小,汇编文件中的标号地址(作用域限本文件):我们找个占用内存地址的地方看一下:我们在 startup_stm32f103xg.s ,可以看到RESET函数:我们继续往下看:上图中的 i.RCC_Delay 下面跟了一个 RCC_Delay,说明这个函数是用static修饰的,我们找到 stm32f1xx_hal_rcc.c下的RCC_Delay 如图:我们接着看到 SRAM区:我们继续看到后面的.bss段,包括了 HEAP 和 STACK区域:通过上图中我们可以看到 HEAP 的起始地址为 0x20002338,ram从 0x2000 0000开始存放的依次为 .data、.bss、HEAP、STACK。HEAP在 startup_stm32fxxx.s 中定义过大小为 0x200,所以结束地址为0x20002538, HEAP 是和 STACK连接在一起的,所以STACK的起始地址为 0x20002538,大小 0X400,结束地址为 0x20002938。 最后我们可以看到 __initial_sp 指向的是 0x20002938,入栈从高地址开始入栈,地址越来越小。4.3.2 Global Symbols全局标号,全局变量的地址和大小,C文件中函数的地址及其代码大小,汇编文件中的标号地址(作用域全工程):我们找到Flash地址开始的部分:在 startup_stm32fxxx.s 能看到对应的部分:看到最后的 RAM区,注意下图中标出的两行的功能:4.4 Memory Map of the image映像文件可以分为加载域(Load Region)和运行域(Execution Region):加载域反映了ARM可执行映像文件各个段存放在存储器中时的位置关系。其中还能看出来 Flash中存放的都是 code,和 RO_Data:我们看看SRAM部分:需要注意一下,我们前面代码he 数据部分都是4字节对齐,PAD一般都是补充2个字节,到了栈部分,需要8字节对齐:4.5 Image component sizes存储组成大小这部分的内容就比较直观最后就是我们熟悉的部分:五、一些相关的补充内容5.1 STM32 的启动方式BOOT0BOOT1启动模式0XUser Flash memory(从闪存存储器启动)10System memory(从系统存储器启动)11Embedded SRAM(从内嵌SRAM启动)第一种启动方式是最常用的用户FLASH启动,正常工作就在这种模式下,STM32的FLASH可以擦出10万次,所以不用担心芯片哪天会被擦爆!第二种启动方式是系统存储器启动方式,即我们常说的串口下载方式(ISP),不建议使用这种,速度比较慢。STM32 中自带的BootLoader就是在这种启动方式中,如果出现程序硬件错误的话可以切换BOOT0/1到该模式下重新烧写Flash即可恢复正常。第三种启动方式是STM32内嵌的SRAM启动。该模式用于调试。 用jlink在线仿真,则是下载到SRAM中。以上三种启动方式我们都很熟悉,但是他的究竟是如何实现的呢?我们先来看看《Cortex-M3权威指南》关于CM3复位后的动作:当选择相应的启动方式时,对应的存储器空间被映射到启动空间(0x00000000)。从闪存存储器启动:主闪存存储器被映射到启动空间(0x0000 0000) ,也就是0x08000000被映射到0x00000000。从内嵌SRAM启动 :SRAM起始地址 0x2000 0000 被映射到0x00000000。从系统存储器启动:系统存储器被映射到启动空间(0x0000 0000),也就是0x1FFF F000被映射到0x00000000。 (为什么是0x1FFF F000 可以查阅上文中的 2.2小节 STM32 的存储器映射分析,STM32互联型产品这个地址不一样,此地址由ST官方写入了一段BootLoader代码,可以通过官方BootLoader升级MCU固件,无法修改)。5.2 STM32启动地址和 Bootloader 说明通过上面的内容,我们现在知道STM32 从Flash程序启动以后会从 0X08000000 开始运行,那么他这个地址是否可以修改,答案是当然的!但是单独的改他的启动地址,没有任何意义,一般都是需要使用到 Bootloader 才会使得应用程序的地址发生变化。在 0x1FFF F000 这个地址上官方写入了一段 BootLoader 用户使用,我们也可以自己写一段 BootLoader 程序方便自己使用,因为是自己写的,他还是用户程序,只是我们自己把程序分成了 BootLoader部分和 应用程序部分,大概的意思如下图所示: 为什么要使用用户 BootLoader :在有些项目中,可能因为某些原因需要经常更换 程序,如果每次都是重新烧录,特别的麻烦,那么我们就可以自己设计一个 BootLoader,通过 SD卡进行升级:上电后先运行 BootLoader,BootLoader主要工作是检测是否有SD卡,SD卡中是否有需要的BIn文件,如果检测到就将其复制到 应用程序区域 使得程序得以更新,更新结束以后跳转到应用程序执行;如果没检测到相应的SD卡,就说明程序不需要更新,也跳转到应用程序执行;以上主要是说明使用 BootLoader 的思路与适用场合,至于具体的实现其实网上有很多教程,如果有机会我也会补充或者单独写一篇文章总结一下(说明:下面是零碎的笔记,还没整理完,仅供参考stm32 FLASH的起始地址是0x08000000,当然也可以自定义起始地址,不过记得在main函数中定义变量后加一句SCB->VTOR=FLASH_BASE | OFFSET;OFFSET是想要偏移的量,可宏定义或直接0xXX。当然也可以调用库函数 NVIC_SetVectorTable()进行偏移,效果一样。IAP升级这样用的多)5.3 单片机和 x86cpu运行程序的不同参考博文:cpu运行时程序是在flash中还是在RAM呢?x86的pc机cpu在运行的时候程序是存储在RAM中的,而单片机等嵌入式系统则是存于flash中 x86cpu和单片机读取程序的具体途径: pc机在运行程序的时候将程序从外存(硬盘)中,调入到RAM中运行,cpu从RAM中读取程序和数据 而单片机的程序则是固化在flash中,cpu运行时直接从flash中读取程序,从RAM中读取数据 原因分析 :x86构架的cpu是基于冯.诺依曼体系的,即数据和程序存储在一起,而且pc机的RAM资源相当丰富,从几十M到几百M甚至是几个G,客观上能够承受大量的程序数据。 单片机的构架大多是哈弗体系的,即程序和数据分开存储,而且单片的片内RAM资源是相当有限的,内部的RAM过大会带来成本的大幅度提高。 冯.诺依曼体系与哈佛体系的区别:二者的区别就是程序空间和数据空间是否是一体的。 早期的微处理器大多采用冯诺依曼结构,典型代表是Intel公司的X86微处理器。取指令和取操作数都在同一总线上,通过分时复用的方式进行的。缺点是在高速运行时,不能达到同时取指令和取操作数,从而形成了传输过程的瓶颈。 哈佛总线技术应用是以DSP和ARM为代表的。采用哈佛总线体系结构的芯片内部程序空间和数据空间是分开的,这就允许同时取指令和取操作数,从而大大提高了运算能力。5.4 ARM 和 x86cpu 指令集RISC和CISC说明(此部分是学习了韦东山老师的视频后来记录的,原视频可以在韦东山老师官网里面找到:百问网)ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:① 对内存只有读、写指令② 对于数据的运算是在CPU内部实现③ 使用RISC指令的CPU复杂度小一点,易于设计在ARM架构中,对于乘法运算a = a * b,在RISC中要使用4条汇编指令:① 读内存a② 读内存b③ 计算a*b④ 把结果写入内存x86属于复杂指令集计算机(CISC:Complex Instruction Set Computing),它所用的指令比较复杂,比如某些复杂的指令,它是通过“微程序”来实现的。比如执行乘法指令时,实际上会去执行一个“微程序”,一样是去执行这4步操作:① 读内存a② 读内存b③ 计算a*b④ 把结果写入内存RISC和CISC的区别:CISC的指令能力强,单多数指令使用率低却增加了CPU的复杂度,指令是可变长格式;RISC的指令大部分为单周期指令,指令长度固定,操作寄存器,对于内存只有Load/Store操作CISC支持多种寻址方式;RISC支持的寻址方式CISC通过微程序控制技术实现;RISC增加了通用寄存器,硬布线逻辑控制为主,采用流水线CISC的研制周期长RISC优化编译,有效支持高级语言六、 GCC下生成的.map文件我们知道了MDK下的.map文件,GCC下的.map文件基本上也可以看懂了,这里添加GCC下.map文件关于RAM的部分:6.1 RAM部分RAM部分从0x2000 0000开始,首先存放的是.data部分:接下来是.bss段:如果使用了FreeRTOS,那么在.bbs段后面会有关于FreeRTOS部分的数据:最后才到了heap和stack:
ARM汇编中伪指令的介绍
伪指令没有相对应的操作码,他们所完成的操作称为伪操作。
伪指令在源程序中的作用是为完成汇编程序作各种准备工作的,这些伪指令仅在汇编过程中起作用,一旦汇编结束,伪指令的使命就完成(就如你睡觉她为你铺床,铺在哪儿和铺床技巧由她安排,但她不陪你睡觉,铺好了就走 :-( )。
在 ARM 的汇编程序中,有如下几种伪指令:符号定义伪指令、数据定义伪指令、汇编控制伪指令、宏指令以及其他伪指令。
符号定义( Symbol Definition )伪指令
符号定义伪指令用于定义 ARM 汇编程序中的变量、对变量赋值以及定义寄存器的别名等操作。
常见的符号定义伪指令有如下几种:
—用于定义全局变量的 GBLA 、 GBLL 和 GBLS 。
—用于定义局部变量的 LCLA 、 LCLL 和 LCLS 。
—用于对变量赋值的 SETA 、 SETL 、 SETS 。
—为通用寄存器列表定义名称的 RLIST 。
1、 GBLA、GBLL 和GBLS
语法格式:
GBLA ( GBLL 或 GBLS )全局变量名
GBLA 、 GBLL 和 GBLS 伪指令用于定义一个 ARM 程序中的全局变量,并将其初始化。其中:
GBLA 伪指令用于定义一个全局的数字变量,并初始化为 0 ;
GBLL 伪指令用于定义一个全局的逻辑变量,并初始化为 F (假);
GBLS 伪指令用于定义一个全局的字符串变量,并初始化为空;
由于以上三条伪指令用于定义全局变量,因此在整个程序范围内变量名必须唯一。
使用示例:
GBLA Test1 ;定义一个全局的数字变量,变量名为 Test1
Test1 SETA 0xaa ;将该变量赋值为 0xaa
GBLL Test2 ;定义一个全局的逻辑变量,变量名为 Test2
Test2 SETL {TRUE} ;将该变量赋值为真
GBLS Test3 ;定义一个全局的字符串变量,变量名为 Test3
Test3 SETS “ Testing ”;将该变量赋值为“ Testing ”
2、 LCLA、LCLL 和LCLS
语法格式:
LCLA ( LCLL 或 LCLS )局部变量名
LCLA 、 LCLL 和 LCLS 伪指令用于定义一个 ARM 程序中的局部变量,并将其初始化。其中:
LCLA 伪指令用于定义一个局部的数字变量,并初始化为 0 ;
LCLL 伪指令用于定义一个局部的逻辑变量,并初始化为 F (假);
LCLS 伪指令用于定义一个局部的字符串变量,并初始化为空;
以上三条伪指令用于声明局部变量,在其作用范围内变量名必须唯一。
使用示例:
LCLA Test4 ;声明一个局部的数字变量,变量名为 Test4
Test3 SETA 0xaa ;将该变量赋值为 0xaa
LCLL Test5 ;声明一个局部的逻辑变量,变量名为 Test5
Test4 SETL {TRUE} ;将该变量赋值为真
LCLS Test6 ;定义一个局部的字符串变量,变量名为 Test6
Test6 SETS “ Testing ”;将该变量赋值为“ Testing ”
3、 SETA、SETL 和SETS
语法格式:
变量名 SETA ( SETL 或 SETS )表达式
伪指令 SETA 、 SETL 、 SETS 用于给一个已经定义的全局变量或局部变量赋值。
SETA 伪指令用于给一个数学变量赋值;
SETL 伪指令用于给一个逻辑变量赋值;
SETS 伪指令用于给一个字符串变量赋值;
其中,变量名为已经定义过的全局变量或局部变量,表达式为将要赋给变量的值。
使用示例:
LCLA Test3 ;声明一个局部的数字变量,变量名为 Test3
Test3 SETA 0xaa ;将该变量赋值为 0xaa
LCLL Test4 ;声明一个局部的逻辑变量,变量名为 Test4
Test4 SETL {TRUE} ;将该变量赋值为真
4 、 RLIST
语法格式:
名称 RLIST { 寄存器列表 }
RLIST 伪指令可用于对一个通用寄存器列表定义名称,使用该伪指令定义的名称可在 ARM 指令 LDM/STM 中使用。在 LDM/STM 指令中,列表中的寄存器访问次序为根据寄存器的编号由低到高,而与列表中的寄存器排列次序无关。
使用示例:
RegList RLIST {R0-R5 , R8 , R10} ;将寄存器列表名称定义为 RegList ,可在 ARM 指令 LDM/STM中通过该名称访问寄存器列表。
数据定义( Data Definition )伪指令
数据定义伪指令一般用于为特定的数据分配存储单元,同时可完成已分配存储单元的初始化。
常见的数据定义伪指令有如下几种:
— DCB 用于分配一片连续的字节存储单元并用指定的数据初始化。
— DCW ( DCWU )用于分配一片连续的半字存储单元并用指定的数据初始化。
— DCD ( DCDU )用于分配一片连续的字存储单元并用指定的数据初始化。
— DCFD ( DCFDU )用于为双精度的浮点数分配一片连续的字存储单元并用指定的数据初始
化。
— DCFS ( DCFSU )用于为单精度的浮点数分配一片连续的字存储单元并用指定的数据初
始化。
— DCQ ( DCQU )用于分配一片以 8 字节为单位的连续的存储单元并用指定的数据初始
化。
— SPACE 用于分配一片连续的存储单元
— MAP 用于定义一个结构化的内存表首地址
— FIELD 用于定义一个结构化的内存表的数据域
1、 DCB
语法格式:
标号 DCB 表达式
DCB 伪指令用于分配一片连续的字节存储单元并用伪指令中指定的表达式初始化。其中,表达式可以为 0 ~ 255 的数字或字符串。 DCB 也可用“ = ”代替。
使用示例:
Str DCB “ This is a test !” ;分配一片连续的字节存储单元并初始化。
2、 DCW(或DCWU)
语法格式:
标号 DCW (或 DCWU )表达式
DCW (或 DCWU )伪指令用于分配一片连续的半字存储单元并用伪指令中指定的表达式初始化。
其中,表达式可以为程序标号或数字表达式。。
用 DCW 分配的字存储单元是半字对齐的,而用 DCWU 分配的字存储单元并不严格半字对齐。
使用示例:
DataTest DCW 1 , 2 , 3 ;分配一片连续的半字存储单元并初始化。
3、 DCD(或DCDU)
语法格式:
标号 DCD (或 DCDU )表达式
DCD (或 DCDU )伪指令用于分配一片连续的字存储单元并用伪指令中指定的表达式初始化。其中,表达式可以为程序标号或数字表达式。 DCD 也可用“ & ”代替。
用 DCD 分配的字存储单元是字对齐的,而用 DCDU 分配的字存储单元并不严格字对齐。
使用示例:
DataTest DCD 4 , 5 , 6 ;分配一片连续的字存储单元并初始化。
4、 DCFD(或DCFDU)
语法格式:
标号 DCFD (或 DCFDU )表达式
DCFD (或 DCFDU )伪指令用于为双精度的浮点数分配一片连续的字存储单元并用伪指令中指定的表达式初始化。每个双精度的浮点数占据两个字单元。用 DCFD 分配的字存储单元是字对齐的,而用 DCFDU 分配的字存储单元并不严格字对齐。
使用示例:
FDataTest DCFD 2E115 , -5E7 ;分配一片连续的字存储单元并初始化为指定的双精度数。
5、 DCFS(或DCFSU)
语法格式:
标号 DCFS (或 DCFSU )表达式
DCFS (或 DCFSU )伪指令用于为单精度的浮点数分配一片连续的字存储单元并用伪指令中指定的表达式初始化。每个单精度的浮点数占据一个字单元。用 DCFS 分配的字存储单元是字对齐的,而用 DCFSU 分配的字存储单元并不严格字对齐。
使用示例:
FDataTest DCFS 2E5 , -5E - 7 ;分配一片连续的字存储单元并初始化为指定的单精度数。
6、 DCQ(或DCQU)
语法格式:
标号 DCQ (或 DCQU )表达式
DCQ (或 DCQU )伪指令用于分配一片以 8 个字节为单位的连续存储区域并用伪指令中指定的表达式初始化。
用 DCQ 分配的存储单元是字对齐的,而用 DCQU 分配的存储单元并不严格字对齐。
使用示例:
DataTest DCQ 100 ;分配一片连续的存储单元并初始化为指定的值。
7、 SPACE
语法格式:
标号 SPACE 表达式
SPACE 伪指令用于分配一片连续的存储区域并初始化为 0 。其中,表达式为要分配的字节数。
SPACE 也可用“ % ” 代替。
使用示例:
DataSpace SPACE 100 ;分配连续 100 字节的存储单元并初始化为 0 。
8、 MAP
语法格式:
MAP 表达式 { ,基址寄存器 }
MAP 伪指令用于定义一个结构化的内存表的首地址。 MAP 也可用“ ^ ” 代替。
表达式可以为程序中的标号或数学表达式,基址寄存器为可选项,当基址寄存器选项不存在时,表达式的值即为内存表的首地址,当该选项存在时,内存表的首地址为表达式的值与基址寄存器的和。
MAP 伪指令通常与 FIELD 伪指令配合使用来定义结构化的内存表。
使用示例:
MAP 0x100 , R0 ;定义结构化内存表首地址的值为 0x100 + R0 。
9、 FILED
语法格式:
标号 FIELD 表达式
FIELD 伪指令用于定义一个结构化内存表中的数据域。 FILED 也可用“ # ”代替。
表达式的值为当前数据域在内存表中所占的字节数。
FIELD 伪指令常与 MAP 伪指令配合使用来定义结构化的内存表。 MAP 伪指令定义内存表的首地址, FIELD 伪指令定义内存表中的各个数据域,并可以为每个数据域指定一个标号供其他的指令引用。
注意 MAP 和 FIELD 伪指令仅用于定义数据结构,并不实际分配存储单元。
使用示例:
MAP 0x100 ;定义结构化内存表首地址的值为 0x100 。
A FIELD 16 ;定义 A 的长度为 16 字节,位置为 0x100
B FIELD 32 ;定义 B 的长度为 32 字节,位置为 0x110
S FIELD 256 ;定义 S 的长度为 256 字节,位置为 0x130
汇编控制( Assembly Control )伪指令
汇编控制伪指令用于控制汇编程序的执行流程,常用的汇编控制伪指令包括以下几条:
— IF 、 ELSE 、 ENDIF
— WHILE 、 WEND
— MACRO 、 MEND
— MEXIT
1、 IF、ELSE、ENDIF
语法格式:
IF 逻辑表达式
指令序列 1
ELSE
指令序列 2
ENDIF
IF 、 ELSE 、 ENDIF 伪指令能根据条件的成立与否决定是否执行某个指令序列。当 IF 后面的逻辑表达式为真,则执行指令序列 1 ,否则执行指令序列 2 。其中, ELSE 及指令序列 2 可以没有,此时,当 IF 后面的逻辑表达式为真,则执行指令序列 1 ,否则继续执行后面的指令。
IF 、 ELSE 、 ENDIF 伪指令可以嵌套使用。
使用示例:
GBLL Test ;声明一个全局的逻辑变量,变量名为 Test……
IF Test = TRUE
指令序列 1
ELSE
指令序列 2
ENDIF
2、 WHILE、WEND
语法格式:
WHILE 逻辑表达式
指令序列
WEND
WHILE 、 WEND 伪指令能根据条件的成立与否决定是否循环执行某个指令序列。当 WHILE 后面的逻辑表达式为真,则执行指令序列,该指令序列执行完毕后,再判断逻辑表达式的值,若为真则继续执行,一直到逻辑表达式的值为假。
WHILE 、 WEND 伪指令可以嵌套使用。
使用示例:
GBLA Counter ;声明一个全局的数学变量,变量名为 Counter
Counter SETA 3 ;由变量Counter 控制循环次数
……
WHILE Counter &lt; 10
指令序列
WEND
3、 MACRO、MEND
语法格式:
$ 标号宏名 $ 参数 1 , $ 参数 2 ,……
指令序列
MEND
MACRO 、 MEND 伪指令可以将一段代码定义为一个整体,称为宏指令,然后就可以在程序中通过宏指令多次调用该段代码。其中, $ 标号在宏指令被展开时,标号会被替换为用户定义的符号,宏指令可以使用一个或多个参数,当宏指令被展开时,这些参数被相应的值替换。
宏指令的使用方式和功能与子程序有些相似,子程序可以提供模块化的程序设计、节省存储空间并提高运行速度。但在使用子程序结构时需要保护现场,从而增加了系统的开销,因此,在代码较短且需要传递的参数较多时,可以使用宏指令代替子程序。
包含在 MACRO 和 MEND 之间的指令序列称为宏定义体,在宏定义体的第一行应声明宏的原型(包含宏名、所需的参数),然后就可以在汇编程序中通过宏名来调用该指令序列。在源程序被编译时,汇编器将宏调用展开,用宏定义中的指令序列代替程序中的宏调用,并将实际参数的值传递给宏定义中的形式参数。
MACRO 、 MEND 伪指令可以嵌套使用。
4、 MEXIT
语法格式:
MEXIT
MEXIT 用于从宏定义中跳转出去。
其他常用的伪指令
还有一些其他的伪指令,在汇编程序中经常会被使用,包括以下几条:
— AREA
— ALIGN
— CODE16 、 CODE32
— ENTRY
— END
— EQU
— EXPORT (或 GLOBAL )
— IMPORT
— EXTERN
— GET (或 INCLUDE )
— INCBIN
— RN
— ROUT
1、 AREA
语法格式:
AREA 段名属性 1 ,属性 2 ,……
AREA 伪指令用于定义一个代码段或数据段。其中,段名若以数字开头,则该段名需用“ | ”括起来,如 |1_test| 。
属性字段表示该代码段(或数据段)的相关属性,多个属性用逗号分隔。常用的属性如下:
— CODE 属性:用于定义代码段,默认为 READONLY 。
— DATA 属性:用于定义数据段,默认为 READWRITE 。
— READONLY 属性:指定本段为只读,代码段默认为 READONLY 。
— READWRITE 属性:指定本段为可读可写,数据段的默认属性为 READWRITE 。
— ALIGN 属性:使用方式为 ALIGN 表达式。在默认时, ELF (可执行连接文件)的代码段和数据段是按字对齐的,表达式的取值范围为 0 ~ 31 ,相应的对齐方式为 2 表达式次方,比如“ALIGN=3”表示按一个字节(8位)对齐。
— COMMON 属性:该属性定义一个通用的段,不包含任何的用户代码和数据。各源文件中同名的 COMMON 段共享同一段存储单元。
一个汇编语言程序至少要包含一个段,当程序太长时,也可以将程序分为多个代码段和数据段。
使用示例:
AREA Init , CODE , READONLY
该伪指令定义了一个代码段,段名为 Init ,属性为只读
2、 ALIGN
语法格式:
ALIGN { 表达式 { ,偏移量 }}
ALIGN 伪指令可通过添加填充字节的方式,使当前位置满足一定的对其方式 | 。其中,表达式的值用于指定对齐方式,可能的取值为 2 的幂,如 1 、 2 、 4 、 8 、 16 等。若未指定表达式,则将当前位置对齐到下一个字的位置。偏移量也为一个数字表达式,若使用该字段,则当前位置的对齐方式为: 2 的表达式次幂+偏移量。
使用示例:
AREA Init , CODE , READONLY , ALIEN = 3 ;指定后面的指令为 8 字节对齐。
指令序列
END
3、 CODE16、CODE32
语法格式:
CODE16 (或 CODE32 )
CODE16 伪指令通知编译器,其后的指令序列为 16 位的 Thumb 指令。
CODE32 伪指令通知编译器,其后的指令序列为 32 位的 ARM 指令。
若在汇编源程序中同时包含 ARM 指令和 Thumb 指令时,可用 CODE16 伪指令通知编译器其后的指令序列为 16 位的 Thumb 指令, CODE32 伪指令通知编译器其后的指令序列为 32 位的 ARM 指令。因此,在使用 ARM 指令和 Thumb 指令混合编程的代码里,可用这两条伪指令进行切换,但注意他们只通知编译器其后指令的类型,并不能对处理器进行状态的切换。
使用示例:
AREA Init , CODE , READONLY
……
CODE32 ;通知编译器其后的指令为 32 位的 ARM 指令
LDR R0 ,= NEXT + 1 ;将跳转地址放入寄存器 R0
BX R0 ;程序跳转到新的位置执行,并将处理器切换到 Thumb 工作状态
……
CODE16 ;通知编译器其后的指令为 16 位的 Thumb 指令
NEXT LDR R3,=0x3FF
……
END ;程序结束
4、 ENTRY
语法格式:
ENTRY
ENTRY 伪指令用于指定汇编程序的入口点。在一个完整的汇编程序中至少要有一个 ENTRY (也可以有多个,当有多个 ENTRY 时,程序的真正入口点由链接器指定),但在一个源文件里最多只能有一个 ENTRY (可以没有)。
使用示例:
AREA Init , CODE , READONLY
ENTRY ;指定应用程序的入口点
……
5、 END
语法格式:
END
END 伪指令用于通知编译器已经到了源程序的结尾。
使用示例:
AREA Init , CODE , READONLY
……
END ;指定应用程序的结尾
6、 EQU
语法格式:
名称 EQU 表达式 { ,类型 }
EQU 伪指令用于为程序中的常量、标号等定义一个等效的字符名称,类似于 C 语言中的# define 。
其中 EQU 可用“ * ”代替。
名称为 EQU 伪指令定义的字符名称,当表达式为 32 位的常量时,可以指定表达式的数据类型,可以有以下三种类型:
CODE16 、 CODE32 和 DATA
使用示例:
Test EQU 50 ;定义标号 Test 的值为 50
Addr EQU 0x55 , CODE32 ;定义 Addr 的值为 0x55 ,且该处为 32 位的 ARM 指令。
7、 EXPORT(或GLOBAL)
语法格式:
EXPORT 标号 {[WEAK]}
EXPORT 伪指令用于在程序中声明一个全局的标号,该标号可在其他的文件中引用,意为输出变量到整个但前工程中,与Linux shell中的export指令类似。 EXPORT可用 GLOBAL 代替。标号在程序中区分大小写, [WEAK] 选项声明其他的同名标号优先于该标号被引用。
使用示例:
AREA Init , CODE , READONLY
EXPORT Stest ;声明一个可全局引用的标号Stest……
END
8、 IMPORT
语法格式:
IMPORT 标号 {[WEAK]}
IMPORT 伪指令用于通知编译器要使用的标号在其他的源文件中定义,但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。
标号在程序中区分大小写, [WEAK] 选项表示当所有的源文件都没有定义这样一个标号时,编译器也不给出错误信息,在多数情况下将该标号置为 0 ,若该标号为 B 或 BL 指令引用,则将 B 或 BL指令置为 NOP 操作。
使用示例:
AREA Init , CODE , READONLY
IMPORT Main ;通知编译器当前文件要引用标号Main,但Main 在其他源文件中定义……
END
9、 EXTERN
语法格式:
EXTERN 标号 {[WEAK]}
EXTERN 伪指令用于通知编译器要使用的标号在其他的源文件中定义,但要在当前源文件中引用,如果当前源文件实际并未引用该标号,该标号就不会被加入到当前源文件的符号表中。标号在程序中区分大小写, [WEAK] 选项表示当所有的源文件都没有定义这样一个标号时,编译器也不给出错误信息,在多数情况下将该标号置为 0 ,若该标号为 B 或 BL 指令引用,则将 B 或 BL指令置为 NOP 操作。
使用示例:
AREA Init , CODE , READONLY
EXTERN Main ;通知编译器当前文件要引用标号Main,但Main 在其他源文件中定义……
END
10、 GET(或INCLUDE)
语法格式:
GET 文件名
GET 伪指令用于将一个源文件包含到当前的源文件中,并将被包含的源文件在当前位置进行汇编处理。可以使用 INCLUDE 代替 GET 。
汇编程序中常用的方法是在某源文件中定义一些宏指令,用 EQU 定义常量的符号名称,用 MAP和 FIELD 定义结构化的数据类型,然后用 GET 伪指令将这个源文件包含到其他的源文件中。使用方法与 C 语言中的“ include ”相似。
GET 伪指令只能用于包含源文件,包含目标文件需要使用 INCBIN 伪指令
使用示例:
AREA Init , CODE , READONLY
GET a1.s ;通知编译器当前源文件包含源文件a1.s
GE T C:\a2.s ;通知编译器当前源文件包含源文件C:\ a2.s ……
END
11、 INCBIN
语法格式:
INCBIN 文件名
INCBIN 伪指令用于将一个目标文件或数据文件包含到当前的源文件中,被包含的文件不作任何变动的存放在当前文件中,编译器从其后开始继续处理。
使用示例:
AREA Init , CODE , READONLY
INCBIN a1.dat ;通知编译器当前源文件包含文件a1.dat
INCBIN C:\a2.txt ;通知编译器当前源文件包含文件C:\a2.txt……
END
12、 RN
语法格式:
名称 RN 表达式
RN 伪指令用于给一个寄存器定义一个别名。采用这种方式可以方便程序员记忆该寄存器的功能。其中,名称为给寄存器定义的别名,表达式为寄存器的编码。
使用示例:
Temp RN R0 ;将R0 定义一个别名Temp
13、 ROUT
语法格式:
{ 名称 } ROUT
ROUT 伪指令用于给一个局部变量定义作用范围。在程序中未使用该伪指令时,局部变量的作用范围为所在的 AREA ,而使用 ROUT 后,局部变量的作为范围为当前 ROUT 和下一个 ROUT 之间。
函数内部分配的buffer过大导致堆栈溢出
最近在调试一个模块的时候,先是IAR配置的环境,操作很正常,在keil简单移植操作以后老是在函数调用的时候,导致log实时数据出问题,找了好多天,修改了好几个版本,最后面终于确认在局部变量的内存溢出,具体细节是函数体内部分配一个2K+2byte的局部变量,超过启动文件分配的栈空间的大小导致了内存溢出,程序有时候会进入hardfault有时候正差运行但都伴随log数据失败。溢出的原因我们找到了,但是为什么2k的buffer分配内存就会溢出呢,要知道手里用的MCU的FLASH有128k,RAM有16K大小,理论上使用是足够的这要从程序编译成代码的原理进行分析,程序的几个预编译、编译、汇编、链接几个步骤就不多做赘述,不了解的大家可以自行搜索,言归正传,程序在转为二进制的代码时候不仅在逻辑上语言类型上进行转换,在一些变量上也通过不同的修饰符来进行分配其在代码中或者在程序运行的时候所处的位置,这个时候出现了ROM区以及RAM区的变量。先用一个例子进行展示编译器如何识别定位变量的位置:int *p1,*p2;
int val_b = 2; //全局变量 栈(程序自动分配 自动释放)
int val_e ; // 未初始化的全局变量 栈
char s[]="abc"; //全局变量 栈
char *p3= "1234567"; //全局变量 在文字常量区ROM(FALSH)
const int val_c = 3 ; // const只读数据段 全局变量 ROM(FALSH)
static int val_a=0 ; //全局变量 初始化的静态初始化区 栈
static int val_d ; // 未初始化的静态变量 栈
p1= (char *)malloc(10); //全局变量 堆区(由程序员分配)
int main() //此处按照普通函数进行解释
{
static int val_f = 5; // 初始化的局部静态变量 栈
static int val_g; //未初始化局部静态变量 栈
int val_h = 6; //初始化局部变量 栈
int val_i; //未初始化局部变量 栈
const int val_j = 7; //const局部变量 栈
p2= (char *)malloc(10); //局部变量堆区(由程序员分配)
while(1)
{
}
free(p2);
return 0;
}
free(p1);
除去ROM区变量剩下的都在RAM区,其余变量在芯片上电之后在RAM执行。①static无论是全局变量还是局部变量都存储在全局/静态区域,在编译期就为其分配内存,在程序结束时释放,例如:val_a、val_d、val_f、val_g。②const全局变量存储在只读数据段,编译期最初将其保存在符号表中,第一次使用时为其分配内存,在程序结束时释放,例如:val_c;const局部变量存储在栈中,代码块结束时释放,例如:val_j。③全局变量存储在全局/静态区域,在编译期为其分配内存,在程序结束时释放,例如:val_b、val_e。④局部变量存储在栈中,代码块结束时释放,例如:val_h、val_i。注:当全局变量和静态局部变量未赋初值时,系统自动置为0编译之后内存的情况(1)栈区(stack):由编译器自动分配和释放,存放函数的参数值、局部变量的值等,其操作方式类似于数据结构中的栈。(2)堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。分配方式类似于数据结构中的链表。(3)全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统自动释放。(4)文字常量区:常量字符串就是存放在这里的。(5)程序代码区:存放函数体的二进制代码。单片机的堆和栈是分配在RAM里的,有可能是内部也有可能是外部,可以读写;栈:存函数的临时变量,即局部变量,函数返回时随时有可能被其他函数栈用。所以栈是一种分时轮流使用的存储区,编译器里定义的Stack_Size,是为了限定函数的局部数据活动的范围,超过这个范围,程序就会跑飞,也就是栈溢出;Stack_Size不影响Hex,更不影响Hex怎么运行的,只是在Debug调试时会提示错。栈溢出也有是超过了边界进行所以单片机代码在函数里定义一个大数组 int buf[2048],栈要是小于2K= 2048=0x400会导致内存溢出,导致很多不可知的bug,例如溢出到其他外设的寄存器进行操作。堆:存的是全局变量,这变量理论上是所有函数都可以访问的,全局变量有的有初始值,但这个值不是存在RAM里的,是存在Hex里,下载到Flash里,上电由代码(编译器生成的汇编代码)搬过去的。上电的时候就分配一块很大内存大的RAM(Heap_Size),作为(malloc_init),别人用只能通过 (malloc)去分配,用完还得(free),否则一不小心内存都使用光了。所以一旦有需要分配很大的内存是编译器里必须定义Heap_Size,否则malloc 也没有实际效果。从这里可以想到函数体内部分配临时变量超过限制,可能需要调整 STACK SIZE。而ST系列的堆栈一般都在在头文件或者在IDE进行配置(IAR),本人习惯在在头文件去看堆栈的大小,一般在在STM32Fxxxxx.s文件中(对应的头文件)修改Stack_Size EQU 0x00000200 (keil 默认1k)修改Heap_Size EQU 0x00000400 (keil 默认2k)在IDE开发的时候我们有一处可以很方便的看到我们程序使用内存空间的大小就是在下方的编译窗口信息。eg:keilIAReclipse其中Code 或者 .text为程序代码部分RO-data 表示 程序定义的常量 const temp;RW-data 或者 .data表示 已初始化的全局变量ZI-data 或者 .bss表示 未初始化的全局变量Total RO Size (Code + RO Data)Total RW Size (RW Data + ZI Data)Total ROM Size (Code + RO Data + RW Data)**这块map文件详解以及数据分配地方的详解推荐大家看哈[strongerhuang的CSDN或者公众号]**讲的很详细。也可以关注公众号嵌入式Linux,这些大佬都写的很详细。前情提要就是这些帮助大家理解问题我也是通过看文章以及和几位大佬聊天测试之后发现了问题:通过以上了解我用了三种方法来解决我的问题。1. 通过const 修饰,将分配的内存置于FLASH空间执行2. 扩大堆区分配最小值实现3. 扩大栈区分配最小值实现注释:前两种都是在RAM分配实现,后一种是在FLASH空间实现(本次解决问题的测试环境都在keil下进行)初始化程序什么都没有操作的情况下 程序容量大小第一种方法: 我还是依旧在函数内部定义一个buffer,首先判断buffer被分配到哪个区(在ROM 区)发现RO Data明显增加2K,所以确认是在ROM区。放置于ROM(FLASH)区一般要需要程序读写要求不高的数据,因为成本的问题,一般MCU 的ROM(FLASH)区远大于RAM,放置于FLASH基本不需要担心溢出等的问题。由于我的数据是固定的,所以我用const也可以进行成功操作。但是切记放于ROM代码只读不可写,如果你的变量需要读写操作,此种操作是不适合的,可以看接下来的其他两种实现方法。设置完也可以在调试call+stack或者watch窗口看数据的地址:ST一般0x08000000这样0x8打头的地址为FASH区(ROM),0x20000000 这样0x2打头的地址为RAM区,不同的芯片请看对应的数据手册第二种方法: 通过在堆区进行设置大小,从而使空间足够用(RAM区)发现其中ZI-data明显变大置于RAM 区的空间记得前面提到过,在stm32的启动文件中有堆栈大小的分配,但是默认比较小一个1k一个2k,当我们分配2K大小的一个buffer的时候空间是不够的,所以我们要去把堆的空间变大。启动文件的堆栈默认值:此次需要malloc一个2k 的内存,属于heap区,而初始化默认的堆区的分配大小只有1K,所以需要需要进行安排需要修改,此处堆区所需的内存大家可以设置稍微大些,不要刚刚好,也容易出现问题。第三种方法:通过在栈区进行设置大小,从而使空间足够用(RAM区)同样的char定义一个buffer之后可以看到RAM空间变大,而定义的变量属于栈区所以我们需要增加栈的空间分配。此次需要一个2k 的buffer,属于stack区,而初始化默认的堆区的分配大小只有2K,所以需要需要进行安排需要修改,此处栈区所需的内存大家也可以设置稍微大些按照空间大小修改之后情况:程序成功运行,希望对大家有用,谢谢。
STM32F103实现IAP在线升级应用程序
一、环境介绍MCU: STM32F103ZET6编程IDE: Keil5.25工程附加源码包下载地址: CSDNhttps://download.csdn.net/download/xiaolong1126626497/25652410二、 IAP介绍 IAP,全称是“In-Application Programming”,中文解释为“在程序中编程”。IAP是一种对通过微控制器的对外接口(如USART,IIC,CAN,USB,以太网接口甚至是无线射频通道)对正在运行程序的微控制器进行内部程序的更新的技术(注意这完全有别于ICP或者ISP技术)。 ICP(In-Circuit Programming)技术即通过在线仿真器对单片机进行程序烧写,而ISP技术则是通过单片机内置的bootloader程序引导的烧写技术。无论是ICP技术还是ISP技术,都需要有机械性的操作如连接下载线,设置跳线帽等。若产品的电路板已经层层密封在外壳中,要对其进行程序更新无疑困难重重,若产品安装于狭窄空间等难以触及的地方,更是一场灾难。但若进引入了IAP技术,则完全可以避免上述尴尬情况,而且若使用远距离或无线的数据传输方案,甚至可以实现远程编程和无线编程。这绝对是ICP或ISP技术无法做到的。某种微控制器支持IAP技术的首要前提是其必须是基于可重复编程闪存的微控制器。STM32微控制器带有可编程的内置闪存,同时STM32拥有在数量上和种类上都非常丰富的外设通信接口,因此在STM32上实现IAP技术是完全可行的。 实现IAP技术的核心是一段预先烧写在单片机内部的IAP程序。这段程序主要负责与外部的上位机软件进行握手同步,然后将通过外设通信接口将来自于上位机软件的程序数据接收后写入单片机内部指定的闪存区域,然后再跳转执行新写入的程序,最终就达到了程序更新的目的。 在STM32微控制器上实现IAP程序之前首先要回顾一下STM32的内部闪存组织架构和其启动过程。STM32的内部闪存地址起始于0x8000000,一般情况下,程序文件就从此地址开始写入。此外STM32是基于Cortex-M3内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动。而这张“中断向量表”的起始地址是0x8000004,当中断来临,STM32的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。最后还需要知道关键的一点,通过修改STM32工程的链接脚本可以修改程序文件写入闪存的起始地址。 在STM32微控制器上实现IAP方案,除了常规的串口接收数据以及闪存数据写入等常规操作外,还需注意STM32的启动过程和中断响应方式。下图显示了STM32常规的运行流程: 图解读如下: 1、 STM32复位后,会从地址为0x8000004处取出复位中断向量的地址,并跳转执行复位中断服务程序。2、 复位中断服务程序执行的最终结果是跳转至C程序的main函数,而main函数应该是一个死循环,是一个永不返回的函数。 3、 在main函数执行的过程中,发生了一个中断请求,此时STM32的硬件机制会将PC指针强制指回中断向量表处。 4、 根据中断源进入相应的中断服务程序。 5、 中断服务程序执行完毕后,程序再度返回至main函数中执行。 若在STM32中加入了IAP程序: 1、 STM32复位后,从地址为0x8000004处取出复位中断向量的地址,并跳转执行复位中断服务程序,随后跳转至IAP程序的main函数。2、 执行完IAP过程后(STM32内部多出了新写入的程序,地址始于0x8000004+N+M)跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main函数。新程序的main函数应该也具有永不返回的特性。同时应该注意在STM32的内部存储空间在不同的位置上出现了2个中断向量表。 3、 在新程序main函数执行的过程中,一个中断请求来临,PC指针仍会回转至地址为0x8000004中断向量表处,而并不是新程序的中断向量表,注意到这是由STM32的硬件机制决定的。 4、 根据中断源跳转至对应的中断服务,注意此时是跳转至了新程序的中断服务程序中。 5、 中断服务执行完毕后,返回main函数。 二、hex文件与bin文件区别 Intel HEX文件是记录文本行的ASCII文本文件,在Intel HEX文件中,每一行是一个HEX记录,由十六进制数组成的机器码或者数据常量。Intel HEX文件经常被用于将程序或数据传输存储到ROM、EPROM,大多数编程器和模拟器使用Intel HEX文件。 很多编译器的支持生成HEX格式的烧录文件,尤其是Keil c。但是编程器能够下载的往往是BIN格式,因此HEX转BIN是每个编程器都必须支持的功能。HEX格式文件以行为单位,每行由“:”(0x3a)开始,以回车键结束(0x0d,0x0a)。行内的数据都是由两个字符表示一个16进制字节,比如”01”就表示数0x01;”0a”,就表示0x0a。对于16位的地址,则高位在前低位在后,比如地址0x010a,在HEX格式文件中就表示为字符串”010a”。hex和bin文件格式 Hex文件,这里指的是Intel标准的十六进制文件,也就是机器代码的十六进制形式,并且是用一定文件格式的ASCII码来表示。具体格式介绍如下: Intel hex 文件常用来保存单片机或其他处理器的目标程序代码。它保存物理程序存储区中的目标代码映象。一般的编程器都支持这种格式。 hex和bin文件格式Hex文件,这里指的是Intel标准的十六进制文件,也就是机器代码的十六进制形式,并且是用一定文件格式的ASCII码来表示。具体格式介绍如下: Intel hex 文件常用来保存单片机或其他处理器的目标程序代码。它保存物理程序存储区中的目标代码映象。一般的编程器都支持这种格式。 三、使用Keil软件完成hex文件转bin文件选项框里的代码:C:\app_setup\for_KEIL\ARM\ARMCC\bin\fromelf.exe --bin -o ./OBJECT/STM32_MD.bin ./OBJECT/STM32_MD.axf解析如下:C:\app_setup\for_KEIL\ARM\ARMCC\bin\fromelf.exe:是keil软件安装目录下的一个工具,用于生成bin--bin -o ./OBJECT/STM32_MD.bin :指定生成bin文件的目录和名称./OBJECT/STM32_MD.axf :指定输入的文件. 生成hex文件需要axf文件新工程的编译指令:C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin -o ./obj/STM32HD.bin ./obj/STM32HD.axf 将该文件下载到STM32内置FLASH,复位开发板,即可启动程序。四、 使用win hex软件将bin文件搞成数组 生成数组之后,可以直接将数组编译到程序里,然后使用STM32内置FLASH编程代码,将该程序烧写到内置FLASH里,再复位开发板即可运行新的程序。五、 Keil编译程序大小计算 Program Size: Code=x RO-data=x RW-data=x ZI-data=x 的含义Code(代码): 程序所占用的FLASH大小,存储在FLASH. 2. RO-data(只读的数据): Read-only-data,程序定义的常量,如const型,存储在FLASH中。 3. RW-data(有初始值要求的、可读可写的数据): 4. Read-write-data,已经被初始化的变量,存储在FLASH中。初始化时RW-data从flash拷贝到SRAM。 5. ZI-data:Zero-Init-data,未被初始化的可读写变量,存储在SRAM中。ZI-data不会被算做代码里因为不会被初始化。ROM(Flash) size = Code + RO-data + RW-data;RAM size = RW-data + ZI-data简单的说就是在烧写的时候是FLASH中的被占用的空间为:Code+RO Data+RW Data程序运行的时候,芯片内部RAM使用的空间为: RW Data + ZI Data六、工程编译信息与堆栈信息查看对于没有OS的程序,堆栈大小是在 startup.s 里设置的: Stack_Size EQU 0x00000800 对于使用用 uCos 的系统,OS自带任务的堆栈,在 os_cfg.h 里定义:/* ——————— TASK STACK SIZE ———————- */
#define OS_TASK_TMR_STK_SIZE 128 /* Timer task stack size (# of OS_STK wide entries) */
#define OS_TASK_STAT_STK_SIZE 128 /* Statistics task stack size (# of OS_STK wide entries) */
#define OS_TASK_IDLE_STK_SIZE 128 /* Idle task stack size (# of OS_STK wide entries) */ 用户程序的任务堆栈,在 app_cfg.h 里定义:#define APP_TASK_MANAGER_STK_SIZE 512
#define APP_TASK_GSM_STK_SIZE 512
#define APP_TASK_OBD_STK_SIZE 512
#define OS_PROBE_TASK_STK_SIZE 128 总结:1, 合理设置堆栈很重要 2, 多种方法结合,相互核对、校验 3, 尽量避免大数组,如果一定要用,尽量定义为 全局变量,使其不占用堆栈空间, 如果函数有重入可能性,则要注意保护。七、实现STM32在线升级程序 7.1 升级的思路与步骤1. 首先得完成STM32内置FLASH编程操作2. 将(升级的程序)新的程序编译生成bin文件(编译之前需要在Keil软件里设置FLASH的起始位置)3. 创建一个专门用于升级的boot程序(IAP Bootloader)4. 使用网络、串口、SD卡等方式接收到bin文件,再将bin文件烧写到STM32内置FLASH里5. 设置主堆栈指针6. 将用户代码区第二个字(第4个字节)为程序开始地址(强制转为函数指针)7. 执行函数,进行程序跳转7.2 待升级的程序FLASH起始设置Bootloader的程序大小先固定为: 20KB,最好是越小越好,可以预留更加多的空间给APP程序使用。STM32内置FLASH闪存的起始地址是: 0x08000000 ,大小是512KB。现在将内置FLASH闪存前20KB的空间留给Bootloader程序使用,后面剩下的空间就给APP程序使用。设置FLASH的起始位置(APP主程序):中断向量表偏移量设置设置编译bin文件 7.3 Bootloader的程序设置//设置写入的地址,必须偶数,因为数据读写都是按照2个字节进行
#define FLASH_APP_ADDR 0x08005000 //应用程序存放到FLASH中的起始地址
int main()
{
printf("UART1 OK.....\n");
printf("进入IAP Bootloader程序!\n");
while(1)
{
key=KEY_Scanf();
if(key==1) //KEY1按下,写入STM32 FLASH
{
printf("正在更新IAP程序...............\n");
iap_write_appbin(FLASH_APP_ADDR,(u8*)app_bin_data,sizeof(app_bin_data));//烧写新的程序到内置FLASH
printf("程序更新成功....\n");
iap_load_app(FLASH_APP_ADDR);//执行FLASH APP代码
}
}
}
/*
函数功能:跳转到应用程序段
appxaddr:用户代码起始地址.
*/
typedef void (*iap_function)(void); //定义一个函数类型的参数.
void IAP_LoadApp(u32 app_addr)
{
//给函数指针赋值合法地址
jump2app=(iap_function)*(vu32*)(app_addr+4);//用户代码区第二个字为程序开始地址(复位地址)
__set_MSP(*(vu32*)app_addr); //设置主堆栈指针
jump2app(); //跳转到APP.
}
Android ARM常用的汇编指令合集
ARM 处理器的指令集可以分为 跳转指令、数据处理指令、程序状态寄存器(PSR)处理指令、加载/存储指令、协处理器指令和异常产生指令 六大指令,这里把其它几个指令一起发了出来,可以查看具体的目录,请点击头部左上角
一、跳转指令
跳转指令用于实现程序流程的跳转,在ARM程序中有以下两种方法可以实现程序流程的跳转。Ⅰ.使用专门的跳转指令;Ⅱ.直接向程序计数器PC写入跳转地址值,通过向程序计数器PC写入跳转地址值,可以实现在4GB的地址空间中的任意跳转,在跳转之前结合使用MOV LR,PC等类似指令,可以保存将来的返回地址值,从而实现在4GB连续的线性地址空间的子程序调用。
ARM指令集中的跳转指令可以完成从当前指令向前或向后的32MB的地址空间的跳转,包括以下4条指令:
1、B指令
B指令的格式为:B{条件} 目标地址B指令是最简单的跳转指令。一旦遇到一个B指令,ARM处理器将立即跳转到给定的目标地址,从那里继续执行。注意存储在跳转指令中的实际值是相对当前PC 值的一个偏移量,而不是一个绝对地址,它的值由汇编器来计算(参考寻址方式中的相对寻址)。它是24位有符号数,左移两位后有符号扩展为32 位,表示的有效偏移为26 位(前后32MB的地址空间)。以下指令:B Label ;程序无条件跳转到标号Label处执行CMP R1,#0 ;当CPSR寄存器中的Z条件码置位时,程序跳转到标号Label处执行BEQ Label
2、BL指令
BL指令的格式为:BL{条件} 目标地址BL是另一个跳转指令,但跳转之前,会在寄存器R14中保存PC的当前内容,因此,可以通过将R14的内容重新加载到PC中,来返回到跳转指令之后的那个 指令处执行。该指令是实现子程序调用的一个基本但常用的手段。以 下指令:BL Label ;当程序无条件跳转到标号Label处执行时,同时将当前的 PC值保存到;R14(LR)中
3、BLX指令
BLX指令的格式为: BLX 目标地址BLX指令从ARM指令集跳转到指令中所指定的目标地址,并将处理器的工作状态有ARM状态切换到Thumb状态,该指令同时将PC的当前内容保存到寄存 器R14中。因此,当子程序使用Thumb指令集,而调用者使用ARM指令集时,可以通过BLX指令实现子程序的调用和处理器工作状态的切换。同时,子程 序的返回可以通过将寄存器R14值复制到PC中来完成。
4、BX指令
BX指令的格式为:BX{条件} 目标地址BX指令跳转到指令中所指定的目标地址,目标地址处的指令既可以是ARM指令,也可以是Thumb指令。二、数据处理指令
数据处理指令可分为数据传送指令、算术逻辑运算指令 和比较指令等。数据传送指令用于在寄存器和存储器之间进行数据的双向传输;算术逻辑运算指令完成常用的算术与逻辑的运算,该类指令不但将运算结果保存在目的寄存器中,同时更新CPSR中的相应条件标志位;比较指令不保存运算结果,只更新CPSR中相应的条件标志位。数据处理指令共以下16条。
1、MOV指令(传送)
MOV指令的格式为:MOV{条件}{S} 目的寄存器,源操作数MOV指令可完成从另一个寄存器、被移位的寄存器或将一个立即数加载到目的寄存器。其中S选项决定指令的操作是否影响CPSR中条件标志位的值,当没有S 时指令不更新CPSR中条件标志位的值。指令示例:MOV R1,R0 ;将寄存器R0的值传送到寄存器R1MOV PC,R14 ;将寄存器R14的值传送到 PC,常用于子程序返回MOV R1,R0,LSL#3 ;将寄存器R0的值左移3位后传送到R1
2、MVN指令(求反)
MVN指令的格式为:MVN{条件}{S} 目的寄存器,源操作数MVN指令可完成从另一个寄存器、被移位的寄存器、或将一个立即数加载到目的寄存器。与MOV指令不同之处是在传送之前按位被取反了,即把一个被取反的值 传送到目的寄存器中。其中S决定指令的操作是否影响CPSR中条件标志位的值,当没有S时指令不更新CPSR中条件标志位的值。指令示例:MVN R0,#0 ;将 立即数0取反传送到寄存器R0中,完成后R0=-1
3、CMP指令(比较)
CMP指令的格式为:CMP{条件} 操作数1,操作数2CMP指令用于把一个寄存器的内容和另一个寄存器的内容或立即数进行比较,同时更新CPSR中条件标志位的值。该指令进行一次减法运算,但不存储结果,只 更改条件标志位。 标志位表示的是操作数1与操作数2的关系(大、小、相等),例如,当操作数1大于操作操作数2,则此后的有GT后缀的指令将可以执行。指令示例:CMP R1,R0 ;将寄存器R1的值与寄存器R0的值相减,并根据 结果设置CPSR的标 ;志位CMP R1,#100 ;将寄存器R1的值与立即数100相减,并根 据结果设置CPSR的标志位
4、CMN指令(负数比较)
CMN指令的格式为:CMN{条件} 操作数1,操作数2CMN指令用于把一个寄存器的内容和另一个寄存器的内容或立即数取反后进行比较,同时更新CPSR中条件标志位的值。该指令实际完成操作数1和操作数2相 加,并根据结果更改条件标志位。指令示例:CMN R1,R0 ;将寄存器R1的值与寄存器R0的值相加,并根据 结果设置CPSR ;的标志位CMN R1,#100 ;将寄存器R1的值与立即数100相加,并根据 结果设置CPSR的标志位
5、TST指令(测试)
TST指令的格式为:TST{条件} 操作数1,操作数2TST指令用于把一个寄存器的内容和另一个寄存器的内容或立即数进行按位的与运算,并根据运算结果更新CPSR中条件标志位的值。操作数1是要测试的数 据,而操作数2是一个位掩码,该指令一般用来检测是否设置了特定的位。指令示例:TST R1,#%1 ;用于测试在寄存器R1中是否设置了最低位(%表 示二进制数)TST R1,#0xffe ;将寄存器R1的值与立即数0xffe按位与,并根据 结果设置CPSR ;的标志位
6、TEQ指令(测试相等)
TEQ指令的格式为:TEQ{条件} 操作数1,操作数2TEQ指令用于把一个寄存器的内容和另一个寄存器的内容或立即数进行按位的异或运算,并根据运算结果更新CPSR中条件标志位的值。该指令通常用于比较操作数1和操作数2是否相等。指令示例:TEQ R1,R2 ;将寄存器R1的值与寄存器R2的值按位异或,并根据结果 设置CPSR ;的标志位
7、ADD指令(相加)
ADD指令的格式为:ADD{条件}{S} 目的寄存器,操作数1,操作数2ADD指令用于把两个操作数相加,并将结果存放到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个立即数。指令示例:ADD R0,R1,R2 ; R0 = R1 + R2ADD R0,R1,#256 ; R0 = R1 + 256ADD R0,R2,R3,LSL#1 ; R0 = R2 + (R3 << 1)
8、ADC指令(带进位相加)
ADC指令的格式为:ADC{条件}{S} 目的寄存器,操作数1,操作数2ADC指令用于把两个操作数相加,再加上CPSR中的C条件标志位的值,并将结果存放到目的寄存器中。它使用一个进位标志位,这样就可以做比32位大的数 的加法,注意不要忘记设置S后缀来更改进位标志。操作数1应是一个寄存器,操作数2可以是一 个寄存器,被移位的寄存器,或一个立即数。以下指令序列完成两个128位数的加法,第一个数由高到低存放在寄存器R7~R4,第二个数由高到低存放在寄存器R11~R8,运算结果由高到低存放在寄 存器R3~R0:ADDS R0,R4,R8 ; 加低端的字ADCS R1,R5,R9 ; 加第二个字,带进位ADCS R2,R6,R10 ; 加第三个字,带进位ADC R3,R7,R11 ; 加第四个字,带进位
9、SUB指令(相减)
SUB指令的格式为:SUB{条件}{S} 目的寄存器,操作数1,操作数2SUB指令用于把操作数1减去操作数2,并将结果存放到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个立即 数。该指令可用于有符号数或无符号数的减法运算。指令示例:SUB R0,R1,R2 ; R0 = R1 - R2SUB R0,R1,#256 ; R0 = R1 - 256SUB R0,R2,R3,LSL#1 ; R0 = R2 - (R3 << 1)
10、~~~~C指令
~~~~C指令的格式为:~~~~C{条件}{S} 目的寄存器,操作数1,操作数2~~~~C指令用于把操作数1减去操作数2,再减去CPSR中的C条件标志位的反码,并将结果存放到目的寄存器中。操作数1应是一个寄存器,操作数2可以 是一个寄存器,被移位的寄存器,或一个立即数。该指令使用进位标志来表示借位,这样就可以做大于32位的减法,注意不要忘记设置S后缀来更改进位标志。该指令可用于有符号数或无符号数的减法运算。指令示例:SUBS R0,R1,R2 ;R0 = R1 - R2 - !C,并根据结果设置CPSR的进位标志位
11、R~~~~指令
R~~~~指令的格式为:R~~~~{条件}{S} 目的寄存器,操作数1,操作数2R~~~~指令称为逆向减法指令,用于把操作数2减去操作数1,并将结果存放到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位 的寄存器,或一个立即数。该指令可用于有符号数或无符号数的减法运算。指令示例:R~~~~ R0,R1,R2 ; R0 = R2 – R1R~~~~ R0,R1,#256 ; R0 = 256 – R1R~~~~ R0,R2,R3,LSL#1 ; R0 = (R3 << 1) - R2
12、RSC指令(反向带进位减)
RSC指令的格式为:RSC{条件}{S} 目的寄存器,操作数1,操作数2RSC指令用于把 操作数2减去操作数1,再减去CPSR中的C条件标志位的反码,并将结果存放到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位 的寄存器,或一个立即数。该指令使用进位标志来表示借位,这样就可以做大于32位的减法,注意不要忘记设置S后缀来更改进位标志。该指令可用于有符号数或 无符号数的减法运算。指令示例:RSC R0,R1,R2 ;R0 = R2 – R1 - !C
13、AND指令(逻辑位 与)
AND指令的格式为:AND{条件}{S} 目的寄存器,操作数1,操作数2AND指令用于在两个操作数上进行逻辑与运算,并把结果放置到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个 立即数。该指令常用于屏蔽操作数1的某些位。指令示例:AND R0,R0,#3 ;该指令保持R0的0、1位,其余位清零。
14、ORR指令(逻辑位 或)
ORR指令的格式为:ORR{条件}{S} 目的寄存器,操作数1,操作数2ORR指令用于在两个操作数上进行逻辑或运算,并把结果放置到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个 立即数。该指令常用于设置操作数1的某些位。指令示例:ORR R0,R0,#3 ;该指令设置R0的0、1位,其余位保持不变。
15、EOR指令(逻辑位 异或)
EOR指令的格式为:EOR{条件}{S} 目的寄存器,操作数1,操作数2EOR指令用于在两个操作数上进行逻辑异或运算,并把结果放置到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一 个立即数。该指令常用于反转操作数1的某些位。指令示例:EOR R0,R0,#3 ;该指令反转R0的0、1位,其余位保持不变。
16、BIC指令(位清零)
BIC指令的格式为:BIC{条件}{S} 目的寄存器,操作数1,操作数2BIC指令用于清除操作数1的某些位,并把结果放置到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个立即数。 操作数2为32位的掩码,如果在掩码中设置了某一位,则清除这一位。未设置的掩码位保持不 变。指令示例:BIC R0,R0,#%1011 ;该指令清除R0中的位 0、1、和 3,其余的位保持不变。三、乘法指令与乘加指令
ARM 微处理器支持的乘法指令与乘加指令共有6条,可分为运算结果为32位和运算结果为64位两类,与前面的数据处理指令不同,指令中的所有操作数、目的寄存器 必须为通用寄存器,不能对操作数使用立即数或被移位的寄存器,同时,目的寄存器和操作数1必须是不同的寄存器。 乘法指令与乘加指令共有以下6条:
1、MUL指令(相乘)
MUL指令的格式为:MUL{条件}{S} 目的寄存器,操作数1,操作数2MUL指令完成将操作数1与操作数2的乘法运算,并把结果放置到目的寄存器中,同时可以根据运算结果设置CPSR中相应的条件标志位。其中,操作数1和操 作数2均为32位的有符号数或无符号数。指令示例:MUL R0,R1,R2 ;R0 = R1 × R2MULS R0,R1,R2 ;R0 = R1 × R2,同时设置CPSR中的相关条件标志位
2、MLA指令(带累加的相乘)
MLA指令的格式为:MLA{条件}{S} 目的寄存器,操作数1,操作数2,操作数3MLA指令完成将操作数1与操作数2的乘法运算,再将乘积加上操作数3,并把结果放置到目的寄存器中,同时可以根据运算结果设置CPSR中相应的条件标志 位。其中,操作数1和操作数2均为32位的有符号数或无符号数。指令示例:MLA R0,R1,R2,R3 ;R0 = R1 × R2 + R3MLAS R0,R1,R2,R3 ;R0 = R1 × R2 + R3,同时设置CPSR中的相关条件标志位
3、SMULL指令
SMULL指令的格式为:SMULL{条件}{S} 目的寄存器Low,目的寄存器High,操作数1,操作数2SMULL指令完成将操作数1与操作数2的乘法运算,并把结果的低32位放置到目的寄存器Low中,结果的高32位放置到目的寄存器High中,同时可以 根据运算结果设置CPSR中相应的条件标志位。其中,操作数1和操作数2均为32位的有符号数。指令示例:SMULL R0,R1,R2,R3 ;R0 = (R2 × R3)的低32位;R1 = (R2 × R3)的高32位
4、SMLAL指令
SMLAL指令的格式为:SMLAL{条件}{S} 目的寄存器Low,目的寄存器High,操作数1,操作数2SMLAL指令完成将操作数1与操作数2的乘法运算,并把结果的 低32位同目的寄存器Low中的值相加后又放置到目的寄存器Low中,结果的高32位同目的寄存器High中的值相加后又放置到目的寄存器High中,同 时可以根据运算结果设置CPSR中相应的条件标志位。其中,操作数1和操作数2均为32位的有符号数。对于目的寄存器Low,在指令执行前存放64位加数的低32位,指令执行后存放结果的低32位;对于目的寄存器High,在指令执行前存放64位加数的高32位,指令执行后存放结果的高32位。指令示例:SMLAL R0,R1,R2,R3 ;R0 = (R2 × R3)的低32位 + R0;R1 = (R2 × R3)的高32位 + R1
5、UMULL指令
UMULL指令的格式为:UMULL{条件}{S} 目的寄存器Low,目的寄存器High,操作数1,操作数2UMULL指令完成将操作数1与操作数2的乘法运算,并把结果的低32位放置到目的寄存器Low中,结果的高32位放置到目的寄存器High中,同时可以 根据运算结果设置CPSR中相应的条件标志位。其中,操作数1和操作数2均为32位的无符号数。指令示例:UMULL R0,R1,R2,R3 ;R0 = (R2 × R3)的低32位;R1 = (R2 × R3)的高32位
6、UMLAL指令
UMLAL指令的格式为:UMLAL{条件}{S} 目的寄存器Low,目的寄存器High,操作数1,操作数2UMLAL指令完成将操作数1与操作数2的乘法运算,并把结果的 低32位同目的寄存器Low中的值相加后又放置到目的寄存器Low中,结果的高32位同目的寄存器High中的值相加后又放置到目的寄存器High 中,同 时可以根据运算结果设置CPSR中相应的条件标志位。其中,操作数1和操作数2均为32位的无符号数。对于目的寄存器Low,在指令执行前存放64位加数的低32位,指令执行后存放结果的低32位;对于目的寄存器High,在指令执行前存放64位加数的高32位,指令执行后存放结果的高32位。指令示例:UMLAL R0,R1,R2,R3 ;R0 = (R2 × R3)的低32位 + R0;R1 = (R2 × R3)的高32位 + R1
四、程序状态寄存器访问指令
1、MRS指令
MRS指令的格式为:MRS{条件} 通用寄存器 程序状态寄存器(CPSR或SPSR)MRS指令用于将程序状态寄存器的内容传送到通用寄存器中。该指令一般用在以下两种情况:Ⅰ.当需要改变程序状态寄存器的内容时,可用MRS将程序状态寄存器的内容读入通用寄存器,修改后再写回程序状态寄存器。Ⅱ.当在异常处理或进程切换时,需要保存程序状态寄存器的值,可先用该指令读出程序状态寄存器的值,然后保存。指令示例:MRS R0,CPSR ;传送CPSR的内容到R0MRS R0,SPSR ;传送 SPSR的内容到R0
2、MSR指令
MSR指令的格式为:MSR{条件} 程序状态寄存器(CPSR或SPSR)_<域>,操作数MSR指令用于将操作数的内容传送到程序状态寄存器的特定域中。其中,操作数可以为通用寄存器或立即数。<域>用于设置程序状态寄存器中需要 操作的位,32位的程序状态寄存器可分为4个域:位[31:24]为条件位域,用f表示;位[23:16]为状态位域,用s表示;位[15:8] 为扩展位域,用x表示;位[7:0] 为控制位域,用c表示;该指令通常用于恢复或改变程序状态寄存器的内容,在使用时,一般要在MSR指令中指明将要操作的域。指令示例:MSR CPSR,R0 ;传送R0的内容到CPSRMSR SPSR,R0 ;传送R0的内容到SPSRMSR CPSR_c,R0 ;传送R0的内容到SPSR,但仅仅修改CPSR中的控制位域
五、加载/存储指令
ARM微处理器支持加载/存储指令用于在寄存器和存储器之间传送数据,加载指令用于将存储器中的数据传送到寄存器,存储 指令则完成相反的操作。常用的加载存储指令如下:
1、LDR指令
LDR指令的格式为:
LDR{条件} 目的寄存器,<存储器地址>LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取32位的字数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为 目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。该指令在程序设计 中比较常用,且寻址方式灵活多样,请读者认真掌握。指令示例:LDR R0,[R1] ;将存储器地址为R1的字数据读入寄存器R0。LDR R0,[R1,R2] ;将存储器地址为R1+R2的字数据读入寄存器R0。LDR R0,[R1,#8] ;将存储器地址为R1+8的字数据读入寄存器R0。LDR R0,[R1,R2] ! ;将存储器地址为R1+R2的字数据读入寄存器R0,并将新地 址;R1+R2写入R1。LDR R0,[R1,#8] ! ;将存储器地址为R1+8的字数据读入寄存器R0,并将新地址 R1;+8写入R1。LDR R0,[R1],R2 ;将存储器地址为R1的字数据读入寄存器R0,并将新地址 R1+;R2写入R1。LDR R0,[R1,R2,LSL#2]! ;将存储器地址为R1+R2×4的字数据读入寄存器R0,并;将新地址R1+R2×4写入R1。LDR R0,[R1],R2,LSL#2 ;将存储器地址为R1的字数据读入 寄存器R0,并将新地;址R1+R2×4写入R1。
2、LDRB指令
LDRB指令的格式为:LDR{条件}B 目的寄存器,<存储器地址>LDRB指令用于从存储器中将一个8位的字节数据传送到目的寄存器中,同时将寄存器的高24位清零。 该指令通常用于从存储器中读取8位的字节数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目 的地址,从而可以实现程序流程的跳转。指令示例:LDRB R0,[R1] ;将存储器地址为R1的字节数据读入寄存器 R0,并将R0的高24 ;位清零。LDRB R0,[R1,#8] ;将存储器地址为R1+8的字节数据读入寄存器R0,并将 R0的;高24位清零。
3、LDRH指令
LDRH指令的格式为:LDR{条件}H 目的寄存器,<存储器地址>LDRH指令用于从存储器中将一个16位的半字数据传送到目的寄存器中,同时将寄存器的高16位清零。 该指令通常用于从存储器中读取16位的半字数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作 目的地址,从而可以实现程序流程的跳转。指令示例:LDRH R0,[R1] ;将存储器地址为R1的半字数据读入寄存器 R0,并将R0的高;16位清零。LDRH R0,[R1,#8] ;将存储器地址为R1+8的半字数据读入寄存器R0,并将R0 的;高16位清零。LDRH R0,[R1,R2] ;将存储器地址为R1+R2的半字数据读入寄存器R0,并将 R0的;高16位清零。
4、STR指令
STR指令的格式为:STR{条件} 源寄存器,<存储器地址>STR指令用于从源寄存器中将一个32位的字数据传送到存储器中。 该指令在程序设计中比较常用,且寻址方式灵活多样,使用方式可参考指令LDR。指令示例:STR R0,[R1],#8 ;将R0中的字数据写入以R1为地址的存储器中,并 将新地址;R1+8写入R1。STR R0,[R1,#8] ;将R0中的字数据写入以R1+8为地址的存储器中。
5、STRB指令
STRB指令的格式为:STR{条件}B 源寄存器,<存储器地址>STRB指令用于从源寄存器中将一个8位的字节数据传送到存储器中。该字节数据为源寄存器中的低8位。指令示例:STRB R0,[R1] ;将寄存器R0中的字节数据写入以R1为地 址的存储器中。STRB R0,[R1,#8] ;将寄存器R0中的字节数据写入以R1+8为地址的存 储器中。
6、STRH指令
STRH指令的格式为:STR{条件}H 源寄存器,<存储器地址>STRH指令用于从源寄存器中将一个16位的半字数据传送到存储器中。该半字数据为源寄存器中的低16位。指令示例:STRH R0,[R1] ;将寄存器R0中的半字数据写入以R1为地址的 存储器中。STRH R0,[R1,#8] ;将寄存器R0中的半字数据写入以R1+8 为地址的存储器中。
六、批量数据加载/存储指令。
ARM微处理器所支持批量数据加载/存储指令可以一次在一片连续的存储器单元和多个寄存器之间传送数据,批量加载指令 用于将一片连续的存储器中的数据传送到多个寄存器,批量数据存储指令则完成相反的操作。
常用的加载存储指令如下:
LDM(或STM)指令
LDM(或STM)指令的格式为:LDM(或STM){条件}{类型} 基址寄存器{!},寄存器列表{∧}LDM(或STM)指令用于从由基址寄存器所指示的一片连续存储器到寄存器列表所指示的多个寄存器之间传送数据,该指令的常见用途是将多个寄存器的内容入栈或出栈。其中,{类型}为 以下几种情况:IA 每次传送后地址加1;IB 每次传送前地址加1;DA 每次传送后地址减1;DB 每次传送前地址减1;FD 满递减堆栈;ED 空递减堆栈;FA 满递增堆栈;EA 空递增堆栈;{!}为可选后缀,若选用该后缀,则当数据 传送完毕之后,将最后的地址写入基址寄存器,否则基址寄存器的内容不改变。基址寄存器不允许为R15,寄存器列表可以为R0~R15的任意组合。{∧}为可选后缀,当指令为LDM且寄存器列表中包含R15,选用该后缀时表示:除了正常的数据传送之外,还将SPSR复制到CPSR。同时,该后缀还表 示传入或传出的是用户模式下的寄存器,而不是当前模式下的寄存器。指令示例:
STMFD R13!,{R0,R4-R12,LR} ;将寄存器列表中的寄存器(R0,R4到R12,LR)存入堆栈。LDMFD R13!, {R0,R4-R12,PC} ;将堆栈内容恢复到寄存器(R0,R4到R12,LR)。七、数据交换指令
1、SWP指令
SWP指令的格式为:SWP{条件} 目的寄存器,源寄存器1,[源寄存器2]SWP指令用于将源寄存器2所指向的存储器中的字数据传送到目的寄存器中,同时将源寄存器1中的字数据传送到源寄存器2所指向的存储器中。显然,当源寄存 器1和目的寄存器为同一个寄存器时,指令交换该寄存器和存储器的内容。指令示例:SWP R0,R1,[R2] ;将R2所指向的存储器中的字数据传送到R0,同时将R1 中的字数据传送到R2所指向的存储单元。SWP R0,R0,[R1] ;该指令完成将R1所指向的存储器中的字数 据与R0中的数据交换。
2、SWPB指令
SWPB指令的格式为:SWP{条件}B 目的寄存器,源寄存器1,[源寄存器2]SWPB指令用于将源寄存器2所指向的存储器中的字节数据传送到目的寄存器中,目的寄存器的高24清零,同时将源寄存 器1中的字节数据传送到源寄存器2所指向的存储器中。显然,当源寄存器1和目的寄存器为同一个寄存器时,指令交换该寄存器和存储器的内容。指令示例:SWPB R0,R1,[R2] ;将R2所指向的存储器中的字节数据传送到 R0,R0的高24位清零,同时将R1中的低8位数据传送到R2所指向的存储单元。SWPB R0,R0,[R1] ;该指令完成将R1所指向的存储器中的 字节数据与R0中的低8位数据交换。
八、移位指令
1、LSL(或ASL)
LSL(或ASL)的格式为:通用寄存器,LSL(或ASL) 操作数 LSL(或ASL)可完成对通用寄存器中的内容进行逻辑(或算术)的左移操作,按操作数所指定的数量向左移位,低位用零来填充。 其中,操作数可以是通用寄存器,也可以是立即数(0~31)。操作示例MOV R0, R1, LSL #2 ;将R1中的内容左移两位后传送到R0 中。2、LSR
LSR的格式为:通用寄存器,LSR 操作数 LSR可完成对通用寄存器中的内容进行右移的操作,按操作数所指定的数量向右移位,左端用零来填充。其中,操作数可以 是通用寄存器,也可以是立即数(0~31)。操作示例: MOV R0, R1, LSR #2 ;将R1中的内容右移两位后传送到R0 中,左端用零来填充。3、ASR
ASR的格式为:通用寄存器,ASR 操作数 ASR可完成对通用寄存器中的内容进行右移的操作,按操作数所指定的数量向右移位,左端用第31位的值来填充。其中,操作数可以是通用寄存器,也可以是立 即数(0~31)。操作示例:MOV R0, R1, ASR #2 ;将R1中的内容右移两位后传送到R0 中,左端用第31位的值来填充。4、ROR
ROR的格式为:通用寄存器,ROR 操作数 ROR可完成对通用寄存器中的内容进行循环右移的操作,按操作数所指定的数量向右循环移位,左端用右端移出的位来填充。其中,操作数可以是通用寄存器,也 可以是立即数(0~31)。显然,当进行32位的循环右移操作时,通用寄存器中的值不改变。操作示例:MOV R0, R1, ROR #2 ;将R1中的内容循环右移两位后传送到R0 中。5、RRX
RRX的格式为:通用寄存器,RRX 操作数 RRX可完成对通用寄存器中的内容进行带扩展的循环右移的操作,按操作数所指定的数量向右循环移位,左端用进位标志位C来填充。其中,操作数可以是通用寄 存器,也可以是立即数(0~31)。操作示例:MOV R0, R1, RRX #2 ;将R1中的内容进行带扩展的循环右移两位 后传送到R0中。九、协处理器指令
1、CDP指令
CDP指令的格式为:CDP{条件} 协处理器编码,协处理器操作码1,目的寄存器,源寄存器1,源寄存器2,协处理器操作码2。CDP指令用于ARM处理器通知ARM协处理器执行特定的操作,若协处理器不能成功完成特定的操作,则产生未定义指令异常。其中协处理器操作码1和协处理 器操作码2为协处理器将要执行的操作,目的寄存器和源寄存器均为协处理器的寄存器,指令不涉及ARM处理器的寄存器和存储器。指令示例:CDP P3,2,C12,C10,C3,4 ;该指令完成协处理器P3的初始化2、LDC指令
LDC指令的格式为:LDC{条件}{L} 协处理器编码,目的寄存器,[源寄存器]LDC指令用于将源寄存器所指向的存储器中的字数据传送到目的寄存器中,若协处理器不能成功完成传送操作,则产生未定义指令异常。其中,{L}选项表示指 令为长读取操作,如用于双精度数据的传输。指令示例:LDC P3,C4,[R0] ;将ARM处理器的寄存器R0所指向的存储器中的字数 据传送到协处理器P3的寄存器C4中。
3、STC指令
STC指令的格式为:STC{条件}{L} 协处理器编码,源寄存器,[目的寄存器]STC指令用于将源寄存器中的字数据传送到目的寄存器所指向的存储器中,若协处理器不能成功完成传送操作,则产生未定义指令异常。其中,{L}选项表示指 令为长读取操作,如用于双精度数据的传输。指令示例:STC P3,C4,[R0] ;将协处理器P3的寄存器C4中的字数据传送到ARM处理 器的寄存器R0所指向的存储器中。
4、MCR指令
MCR指令的格式为:MCR{条件} 协处理器编码,协处理器操作码1,源寄存器,目的寄存器1,目的寄存器2,协处理器操作码2。MCR指令用于将ARM处理器寄存器中的数据传送到协处理器寄存器中,若协处理器不能成功完成操作,则产生未定义指令异常。其中协处理器操作码1和协处理 器操作码2为协处理器将要执行的操作,源寄存器为ARM处理器的寄存器,目的寄存器1和目的寄存器2均为协处理器的寄 存器。指令示例:MCR P3,3,R0,C4,C5,6 ;将ARM处理器寄存器R0中的数据传送到协处 理器P3的寄存器C4和C5中。5、MRC指令
MRC指令的格式为:MRC{条件} 协处理器编码,协处理器操作码1,目的寄存器,源寄存器1,源寄存器2,协处理器操作码2。MRC指令用于将协处理器寄存器中的数据传送到ARM处理器寄存器中,若协处理器不能成功完成操作,则产生未定义指令异常。其中协处理器操作码1和协处理 器操作码2为协处理器将要执行的操作,目的寄存器为ARM处理器的寄存器,源寄存器1和源寄存器2均为协处理器的寄存器。指令示例:MRC P3,3,R0,C4,C5,6 ;该指令将协处理器P3的寄存器中的数据传送到 ARM处理器寄存器中。十、异常产生指令
1、SWI指令
SWI指令的格式为:SWI{条件} 24位的立即数SWI指令用于产生软件中断,以便用户程序能调用操作系统的系统例程。操作系统在SWI的异常处理程序中提供相应的系统服务,指令中24位的立即数指定用 户程序调用系统例程的类型,相关参数通过通用寄存器传递,当指令中24位的立即数被忽略时,用户程序调用系统例程的类型由通用寄存器R0的内容决定,同 时,参数通过其他通用寄存器传递。 指令示例:SWI 0x02 ;该指令调用操作系统编号位02的系统例程。2、BKPT指令
BKPT指令的格式为:BKPT 16位的立即数BKPT指令产生软件断点中断,可用于程序的调试。ARM汇编伪指令在ARM汇编语言程序里,有一些特殊指令助记符,这些助记符与指令系统的助记符不同,没有相对应的操作码,通常称这些特殊指令助记符为伪指令,他们所完成 的操作称为伪操作。伪指令在源程序中的作用是为完成汇编程序作各种准备工作的,这些伪指令仅在汇编过程中起作用,一旦汇编结束,伪指令的使命就完 成。 在ARM 的汇编程序中,有如下几种伪指令:符号定义伪指令、数据定义伪指令、汇编控制伪指令、宏指令以及其他伪指令。一、符号定义(Symbol Definition)伪指令
符号定义伪指令用于定义ARM 汇编程序中的变量、对变量赋值以及定义寄存器的别名等操作。 常见的符号定义伪指令有如下几种: — 用于定义全局变量的GBLA 、GBLL 和GBLS 。 — 用于定义局部变量的LCLA 、LCLL 和LCLS 。 — 用于对变量赋值的SETA 、SETL 、SETS 。 — 为通用寄存器列表定义名称的RLIST 。1、GBLA、GBLL 和GBLS 语法格式: GBLA (GBLL 或GBLS )全局变量名 GBLA 、GBLL 和GBLS 伪指令用于定义一个ARM 程序中的全局变量,并将其初始化。其中: GBLA 伪指令用于定义一个全局的数字变量,并初始化为0 ; GBLL 伪指令用于定义一个全局的逻辑变量,并初始化为F(假); GBLS 伪指令用于定义一个全局的字符串变量,并初始化为空; 由于以上三条伪指令用于定义全局变量,因此在整个程序范围内变量名必须唯一。 使用示例: GBLA Test1 ; 定义一个全局的数字变量,变量名为 Test1。 Test1 SETA 0xaa ; 将该变量赋值为0xaa。 GBLL Test2 ; 定义一个全局的逻辑变量,变量名为 Test2。 Test2 SETL {TRUE} ;将该变量赋值为真。 GBLS Test3 ; 定义一个全局的字符串变量,变量名为 Test3。 Test3 SETS “Testing” ;将该变量赋值为"Testing”。 2、LCLA、LCLL 和LCLS 语法格式: LCLA (LCLL 或 LCLS )局部变量名 LCLA 、LCLL 和LCLS 伪指令用于定义一个ARM 程序中的局部变量,并将其初始化。其中: LCLA伪指令用于定义一个局部的数字变量,并初始化为0 ; LCLL伪指令用于定义一个局部的逻辑变量,并初始化为F(假); LCLS伪指令用于定义一个局部的字符串变量,并初始化为空; 以上三条伪指令用于声明局部变量,在其作用范围内变量名必须唯一。 使用示例: LCLA Test4 ; 声明一个局部的数字变 量,变量名为Test4。 Test3 SETA 0xaa ; 将该变量赋值为0xaa。 LCLL Test5 ; 声明一个局部的逻辑变 量,变量名为Test5。 Test4 SETL {TRUE} ;将该变量赋值为真。 LCLS Test6 ; 定义一个局部的字 符串变量,变量名为Test6。 Test6 SETS “Testing” ;将该变量赋值为 "Testing”。 3、SETA、SETL 和SETS 语法格式: 变量名 SETA (SETL 或 SETS )表达式 伪指令 SETA 、SETL 、SETS 用于给一个已经定义的全局变量或局部变量赋值。 SETA伪指令用于给一个数学变量赋值; SETL伪指令用于给一个逻辑变量赋值; SETS伪指令用于给一个字符串变量赋值; 其中,变量名为已经定义过的全局变量或局部变量,表达式为将要赋给变量的值。 使用示例: LCLA Test3 ; 声明一个局部的数字变量,变量名为 Test3。 Test3 SETA 0xaa ; 将该变量赋值为0xaa。 LCLL Test4 ; 声明一个局部的逻辑变量,变量名为 Test4。 Test4 SETL {TRUE} ;将该变量赋值为真。 4 、RLIST 语法格式: 名称 RLIST { 寄存器列表 } RLIST伪指令可用于对一个通用寄存器列表定义名称,使用该伪指令定义的名称可在ARM 指令 LDM/STM中使用。在LDM/STM指令中,列表中的寄存器访问次序为根据寄存器的编号由低到高,而与列表中的寄存器排列次序无关。 使用示例: RegList RLIST {R0-R5 ,R8 ,R10} ;将寄存器列表名称定义为 RegList ,可在ARM指令LDM/STM中通过该名称访问寄存器列表。二、数据定义(Data Definition)伪指令
数据定义伪指令一般用于为特定的数据分配存储单元,同时可完成已分配存储单元的初始化。 常见的数据定义伪指令有如下几种: — DCB 用于分配一片连续的字节存储单元并用指定的数据初始化。 — DCW(DCWU)用于分配一片连续的半字存储单元并用指定的数据初始化。 — DCD (DCDU)用于分配一片连续的字存储单元并用指定的数据初始化。 — DCFD(DCFDU)用于为双精度的浮点数分配一片连续的字存储单元并用指定的数据初始化。 — DCFS(DCFSU)用于为单精度的浮点数分配一片连续的字存储单元并用指定的数据初始化。 — DCQ(DCQU)用于分配一片以8字节为单位的连续的存储单元并用指定的数据初始化。 — SPACE 用于分配一片连续的存储单元。 — MAP 用于定义一个结构化的内存表首地址。 — FIELD 用于定义一个结构化的内存表的数据域。1、DCB 语法格式: 标号 DCB 表达式 DCB伪指令用于分配一片连续的字节存储单元并用伪指令中指定的表达式初始化。其中,表达式可以为0~255的数字或字符串。DCB 也可用“=”代替。 使用示例: Str DCB “This is a test” ;分配一片连续的字节存储单元并初始化。 2、DCW(或DCWU) 语法格式: 标号 DCW (或DCWU) 表达式 DCW(或DCWU)伪指令用于分配一片连续的半字存储单元并用伪指令中指定的表达式初始化。 其中,表达式可以为程序标号或数字表达式。 用DCW分配的字存储单元是半字对齐的,而用DCWU分配的字存储单元并不严格半字对齐。 使用示例: DataTest DCW 1 ,2 ,3 ;分配一片连续的半字存储单元并初始化。 3、DCD(或DCDU) 语法格式: 标号 DCD(或DCDU) 表达式 DCD(或DCDU)伪指令用于分配一片连续的字存储单元并用伪指令中指定的表达式初始化。其中,表达式可以为程序标号或数字表达式。DCD也可 用"&” 代替。 用DCD分配的字存储单元是字对齐的,而用DCDU分配的字存储单元并不严格字对齐。 使用示例: DataTest DCD 4 ,5 ,6 ;分配一片连续的字存储单元并初始化。
4、DCFD(或DCFDU) 语法格式: 标号 DCFD(或DCFDU) 表达式 DCFD(或DCFDU)伪指令用于为双精度的浮点数分配一片连续的字存储单元并用伪指令中指定的表达式初始化。每个双精度的浮点数占据两个字单元。用 DCFD分配的字存储单元是字对齐的,而用DCFDU分配的字存储单元并不严格字对齐。 使用示例: FDataTest DCFD 2E115 ,-5E7 ;分配一片连续的字存储单元并初始化 为指定的双精度数。 5、DCFS(或DCFSU) 语法格式: 标号 DCFS(或DCFSU) 表达式 DCFS(或DCFSU)伪指令用于为单精度的浮点数分配一片连续的字存储单元并用伪指令中指定的表达式初始化。每个单精度的浮点数占据一个字单元。用 DCFS分配的字存储单元是字对齐的,而用DCFSU分配的字存储单元并不严格字对齐。 使用示例: FDataTest DCFS 2E5 ,-5E -7 ;分配一片连续的字存储单元并初始化为 指定的单精度数。 6、DCQ(或DCQU) 语法格式: 标号 DCQ(或DCQU) 表达式 DCQ(或DCQU)伪指令用于分配一片以8个字节(双字)为单位的连续存储区域并用伪指令中指定的表达式 初始化。 用DCQ分配的存储单元是字对齐的,而用DCQU 分配的存储单元并不严格字对齐。 使用示例: DataTest DCQ 100 ;分配一片连续的存储单元并初始化为指定的值。 7、SPACE 语法格式: 标号 SPACE 表达式 SPACE伪指令用于分配一片连续的存储区域并初始化为0 。其中,表达式为要分配的字节数。 SPACE也可用“ % ”代替。 使用示例: DataSpace SPACE 100 ;分配连续100字节的存储单元并初始化为0 。 8、MAP 语法格式: MAP 表达式 { ,基址寄存器 } MAP伪指令用于定义一个结构化的内存表的首地址。MAP也可用“^” 代替。 表达式可以为程序中的标号或数学表达式,基址寄存器为可选项,当基址寄存器选项不存在时,表达式的值即为内存表的首地址,当该选项存在时,内存表的首地址 为表达式的值与基址寄存器的和。 MAP伪指令通常与FIELD伪指令配合使用来定义结构化的内存表。 使用示例: MAP 0x100 ,R0 ;定义结构化内存表首地址的值为0x100+R0 。 9、FILED 语法格式: 标号 FIELD 表达式 FIELD伪指令用于定义一个结构化内存表中的数据域。FILED 也可用“#” 代替。 表达式的值为当前数据域在内存表中所占的字节数。 FIELD伪指令常与MAP伪指令配合使用来定义结构化的内存表。MAP伪指令定义内存表的首地址,FIELD伪指令定义内存表中的各个数据域,并可以为 每个数据域指定一个标号供其他的指令引用。 注意MAP和FIELD伪指令仅用于定义数据结构,并不实际分配存储单元。 使用示例: MAP 0x100 ; 定义结构化内存表首地址的值为0x100。 A FIELD 16 ; 定义A的长度为16字节,位置为0x100。 B FIELD 32 ; 定义B的长度为32字节,位置为0x110。 S FIELD 256 ;定义S的长度为256字节,位置为0x130。 三、汇编控制(Assembly Control)伪指令 汇编控制伪指令用于控制汇编程序的执行流程,常用的汇编控制伪指令包括以下几条: — IF 、ELSE 、ENDIF — WHILE 、WEND — MACRO 、MEND — MEXIT1、IF、ELSE、ENDIF 语法格式: IF 逻辑表达式 指令序列 1 ELSE 指令序列 2 ENDIF IF 、ELSE 、ENDIF伪指令能根据条件的成立与否决定是否执行某个指令序列。当IF后面的逻辑表达式为真,则执行指令序列1 ,否则执行指令序列2 。其中,ELSE及指令序列2可以没有,此时,当IF后面的逻辑表达式为真,则执行指令序列1 ,否则继续执行后面的指令。 IF 、ELSE 、ENDIF伪指令可以嵌套使用。 使用示例: GBLL Test ;声明一个全局的逻辑变量,变量名为Test IF Test = TRUE 指令序列 1 ELSE 指令序列 2 ENDIF 2、WHILE、WEND 语法格式: WHILE 逻辑表达式 指令序列 WEND WHILE 、WEND伪指令能根据条件的成立与否决定是否循环执行某个指令序列。当WHILE后面的逻辑表达式为真,则执行指令序列,该指令序列执行完毕后,再判断 逻辑表达式的值,若为真则继续执行,一直到逻辑表达式的值为假。 WHILE 、WEND伪指令可以嵌套使用。 使用示例: GBLA Counter ; 声明一个全局的数学变量,变量名为Counter Counter SETA 3 ;由变量Counter 控制循环次数 …… WHILE Counter < 10 指令序列 WEND
3、MACRO、MEND 语法格式: $ 标号 宏名 $ 参数 1 ,$ 参数 2 ,…… 指令序列 MEND MACRO 、MEND伪指令可以将一段代码定义为一个整体,称为宏指令,然后就可以在程序中通过宏指令多次调用该段代码。其中,$标号在宏指令被展开时,标号会被替 换为用户定义的符号,宏指令可以使用一个或多个参数,当宏指令被展开时,这些参数被相应的值替换。 宏指令的使用方式和功能与子程序有些相似,子程序可以提供模块化的程序设计、节省存储空间并提高运行速度。但在使用子程序结构时需要保护现场,从而增加了 系统的开销,因此,在代码较短且需要传递的参数较多时,可以使用宏指令代替子程序。 包含在MACRO和MEND之间的指令序列称为宏定义体,在宏定义体的第一行应声明宏的原型(包含宏名、所需的参数),然后就可以在汇编程序中通过宏名来 调用该指令序列。在源程序被编译时,汇编器将宏调用展开,用宏定义中的指令序列代替程序中的宏调用,并将实际参数的值传递给宏定义中的形式参数。 MACRO、MEND伪指令可以嵌套使用。 4、MEXIT 语法格式: MEXIT MEXIT用于从宏定义中跳转出去。四、其他常用的伪指令
还有一些其他的伪指令,在汇编程序中经常会被使用,包括以下几条: — AREA— ALIGN — CODE16 、CODE32 — ENTRY — END — EQU — EXPORT(或GLOBAL ) — IMPORT — EXTERN — GET(或INCLUDE ) — INCBIN — RN — ROUT1、AREA 语法格式: AREA 段名 属性1 ,属性2 ,…… AREA伪指令用于定义一个代码段或数据段。其中,段名若以数字开头,则该段名需用“|”括起来,如:|1_test| 。属性字段表示该代码段(或数据段)的相关属性,多个属性用逗号分隔。常用的属性如下: — CODE 属性:用于定义代码段,默认为READONLY 。 — DATA 属性:用于定义数据段,默认为READWRITE 。 — READONLY 属性:指定本段为只读,代码段默认为READONLY 。 — READWRITE 属性:指定本段为可读可写,数据段的默认属性为READWRITE 。 — ALIGN 属性:使用方式为ALIGN表达式。在默认时,ELF(可执行连接文件)的代码段和数据段是按字对齐的,表达式的取值范围为0~31,相应的对齐方式为2 表达式次方。 — COMMON 属性:该属性定义一个通用的段,不包含任何的用户代码和数据。各源文件中同名的COMMON段共享同一段存储单元。 一个汇编语言程序至少要包含一个段,当程序太长时,也可以将程序分为多个代码段和数据段。 使用示例: AREA Init ,CODE ,READONLY ; 该伪指令定义了一个代码段,段 名为Init ,属性为只读。 2、ALIGN 语法格式: ALIGN { 表达式 { ,偏移量 }} ALIGN伪指令可通过添加填充字节的方式,使当前位置满足一定的对齐方式。其中,表达式的值用于指定对齐方式,可能的取值为2的幂,如1 、2 、4 、8 、16 等。若未指定表达式,则将当前位置对齐到下一个字的位置。偏移量也为一个数字表达式,若使用该字段,则当前位置的对齐方式为:2的表达式次幂+偏移 量。 使用示例: AREA Init ,CODE ,READONLY ,ALIEN=3 ;指定后面的指令为8 字节对齐。 指令序列 END 3、CODE16、CODE32 语法格式: CODE16(或CODE32) CODE16伪指令通知编译器,其后的指令序列为16位的Thumb指令。 CODE32伪指令通知编译器,其后的指令序列为32位的ARM指令。 若在汇编源程序中同时包含ARM指令和Thumb指令时,可用CODE16伪指令通知编译器其后的指令序列为16位的Thumb指令,CODE32伪指令 通知编译器其后的指令序列为32位的ARM指令。因此,在使用ARM指令和Thumb指令混合编程的代码里,可用这两条伪指令进行切换,但注意他们只通知 编译器其后指令的类型,并不能对处理器进行状态的切换。 使用示例: AREA Init ,CODE ,READONLY …… CODE32 ; 通知编译器其后的指令为32位的 ARM指令 LDR R0 ,=NEXT+1 ;将跳转地址放入寄存器R0 BX R0 ; 程序跳转到新的位置执行, 并将处理器切换到Thumb工作状态 …… CODE16 ; 通知编译器其后的指令为16位的 Thumb指令 NEXT LDR R3,=0x3FF …… END ;
4、ENTRY 语法格式: ENTRY ENTRY伪指令用于指定汇编程序的入口点。在一个完整的汇编程序中至少要有一个ENTRY(也可以有多个,当有多个ENTRY时,程序的真正入口点由链 接器指定),但在一个源文件里最多只能有一个ENTRY(可以没有)。 使用示例: AREA Init ,CODE ,READONLY ENTRY ; 指定应用程序的入口点 …… 5、END 语法格式: END END伪指令用于通知编译器已经到了源程序的结尾。 使用示例: AREA Init ,CODE ,READONLY …… END ;指定应用程序的结尾
6、EQU 语法格式: 名称 EQU 表达式 { ,类型 } EQU伪指令用于为程序中的常量、标号等定义一个等效的字符名称,类似于C语言中的#define 。其中EQU可用“*”代替。名称为EQU伪指令定义的字符名称,当表达式为32位的常量时,可以指定 表达式的数据类型,可以有以下三种类型: CODE16 、CODE32 和DATA 使用示例: Test EQU 50 ; 定义标号Test 的值为50。 Addr EQU 0x55 ,CODE32 ; 定义Addr的值为0x55 ,且该处为32位的ARM指令。
7、EXPORT(或GLOBAL) 语法格式: EXPORT 标号 {[WEAK]} EXPORT伪指令用于在程序中声明一个全局的标号,该标号可在其他的文件中引用。EXPORT 可用GLOBAL代替。标号在程序中区分大小写,[WEAK] 选项声明其他的同名标号优先于该标号被引用。 使用示例: AREA Init ,CODE ,READONLY EXPORT Stest ;声明一个可全局引用的标号Stest END
8、IMPORT 语法格式: IMPORT 标号 {[WEAK]} IMPORT伪指令用于通知编译器要使用的标号在其他的源文件中定义,但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。标 号在程序中区分大小写,[WEAK] 选项表示当所有的源文件都没有定义这样一个标号时,编译器也不给出错误信息,在多数情况下将该标号置为0 ,若该标号为B或BL指令引用,则将B或BL指令置为NOP操作。 使用示例: AREA Init ,CODE ,READONLY IMPORT Main ;通知编译器当前文件要引用标号Main,但Main在其他源文件中定 义。 END
9、EXTERN 语法格式: EXTERN 标号 {[WEAK]} EXTERN伪指令用于通知编译器要使用的标号在其他的源文件中定义,但要在当前源文件中引用,如果当前源文件实际并未引用该标号,该 标号就不会被加入到当前源文件的符号表中。标号在程序中区分大小写, [WEAK] 选项表示当所有的源文件都没有定义这样一个标号时,编译器也不给出错误信息,在多数情况下将该标号置为0 ,若该标号为B或BL指令引用,则将B或BL指令置为NOP操作。 使用示例: AREA Init ,CODE ,READONLY EXTERN Main ;通知编译器当前文件要引用标号Main,但Main在其他源文件中定 义。 END
10、GET(或INCLUDE) 语法格式: GET 文件名 GET伪指令用于将一个源文件包含到当前的源文件中,并将被包含的源文件在当前位置进行汇编处理。可 以使用INCLUDE代替GET。 汇编程序中常用的方法是在某源文件中定义一些宏指令,用EQU定义常量的符号名称,用MAP和FIELD定义结构化的数据类型,然后用GET伪指令将这个 源文件包含到其他的源文件中。使用方法与C 语言中的"include” 相似。GET伪指令只能用于包含源文件,包含目标文件需要使用INCBIN伪指令 使用示例: AREA Init ,CODE ,READONLY GET a1.s ; 通知编译器当前源文件包含源文件a1.s GET C:\a2.s ; 通知编译器当前源文件包含源文件C:\a2.s END
11、INCBIN 语法格式: INCBIN 文件名 INCBIN伪指令用于将一个目标文件或数据文件包含到当前的源文件中,被包含的文件不作任何变动的存放在当前文件中,编译器从其后开始继续处理。 使用示例: AREA Init ,CODE ,READONLY INCBIN a1.dat ; 通知编译器当前源文件包含文件a1.dat INCBIN C:\a2.txt ;通知编译器当前源文件包含文件C:\a2.txt END
12、RN 语法格式: 名称 RN 表达式 RN伪指令用于给一个寄存器定义一个别名。采用这种方式可以方便程序员记忆该寄存器的功能。其中,名称为给寄存器定义的别名,表达式为寄存器的编码。 使用示例: Temp RN R0 ;将R0定义一个别名Temp
13、ROUT 语法格式: {名称} ROUT ROUT伪指令用于给一个局部变量定义作用范围。在程序中未使用该伪指令时,局部 变量的作用范围为所在的AREA,而使用ROUT后,局部变量的作为范围为当前ROUT和下一个ROUT之间。14. BNE 与 BEQTST R0, #0X8BNE SuspendUp ;BNE指令是“不相等(或不为0)跳转指令”: LDR R1,#0x00000000先进行and运算,如果R0的第四位不为1,则结果为零,则设置zero=1(继续下面的LDR指令);否则,如果R0的第四位为1,zero=0(跳到SuspendUp处执行)。tst 和bne连用: 先是用tst进行位与运算,然后将位与的结果与0比较,如果不为0,则跳到bne紧跟着的标记(如bne sleep,则跳到sleep处)。tst 和beq连用: 先是用tst进行位与运算,然后将位与的结果与0比较,如果为0,则跳到beq紧跟着的标记(如bne AAAA,则跳到AAAA处)
干货|单片机的指针怎么学?
摘要:大家想过没有我们用keil写单片机的代码,你的函数啊、变量啊最终都放在了哪里?我们一直说的内存五区,到底是哪五区?到底放在芯片的那个地方呢?还有为什么你学完C语言指针和结构体了,32单片机里面的关于结构体指针的内容还是搞不清楚呢?如果你有这些问题,今天就带你研究研究!这张图学过STM32单片机的小伙伴应该都不陌生,我们看到的STM32芯片已经是已经封装好的成品,主要由内核和片上外设组成。若与电脑类比,内核与外设就如同电脑上的CPU与主板、内存、显卡、硬盘的关系。芯片和外设之间通过各种总线连接。连接被控总线的是FLASH,RAM和片上外设,这些功能部件共同排列在一个4GB的地址空间内。上面这些张图是STM32F40XXX系列单片机的内存地址映射图。我们的代码就是放在Flash里面(0x8000000~0x80FFFFF)。代码就是你写得各种函数,而在程序或者中声明的各种变量都放在RAM中,局部变量就是在函数运行玩空间就释放了,全局变量就是程序运行完了再释放,可以这样简单的理解。CPU使用的变量是存储在RAM里面的,要问我RAM是啥,RAM就是个芯片。就是上图的Block1的SRAM区。CPU是通过导线和RAM芯片连接的,然后可以通过导线往RAM芯片里面存储数据和读数据。首先呢RAM需要有个一开始的地址,对于STM32单片机来说开始地址是0x20000000,要问我为啥要规定地址。只有规定了地址CPU才好对数据进行存储,要是没有地址,瞎几把存,瞎几把取......1、变量1.定义了一个int型的变量,通过打印可以看到这个变量存储的地址是:0x20000000。这也证明了我们内存的首地址是0x20000000。我们定义的value变量就放在这里。2.再定义一个变量通过打印可以看到这个变量存储的地址是:0x20000004。因为int类型在内存中占据4个字节,所以第二个变量就存放在0x20000004这个地方。综上所述,定义的两个变量在内存里面是下面这样子。0x2000 0000地址里面存储的是 00x2000 0004地址里面存储的是 12、指针变量定义指针其实和定义变量一样一样的,只不过呢变量名子前头有个*下面就定义了一个int型的指针变量,变量的名字是p。然后有人会问,为啥变量名字前面加个*就是指针了?答:搞C语言那帮家伙们规定的。定义指针和定义变量一样哈,然后可以定义各种类型的。然后记住一句话:指针这个变量是存变量的地址的!指针这个变量是存变量的地址的!指针这个变量是存变量的地址的!所以给指针赋值自然是把变量的地址给它。#include "sys.h"
#include "led.h"
#include "delay.h"
#include "usart.h"
int value = 0;
int value2 = 1;
int *p;
int main(void)
{
uart_init(115200);
delay_init();
p=&value;//把变量value的地址复制给这个指针
printf("Address of a: %p\n",p);//打印下这个指针指向的地址
while(1)
{
}
}一般什么类型的指针变量就应该赋值什么类型变量的地址。如再定义个 char 型#include "sys.h"
#include "led.h"
#include "delay.h"
#include "usart.h"
int value = 0;
int value2 = 1;
int *p;//定义一个指针
char value3=1;
char *q;
int main(void)
{
uart_init(115200);//串口初始化
delay_init();
p=&value;//把变量value的地址复制给这个指针
q=&value3;//把变量value的地址复制给这个指针
printf("Address of a: %p\n",q);//打印下这个指针指向的地址
while(1)
{
}
}那些规定C语言的大佬弄出来指针这个玩意有啥用?3、指针有啥用?1.咱先使用感受下指针,然后具体有啥用就自己体会了。前面咱把一个变量的地址赋值给了指针了,然后搞C语言的那帮家伙们又规定。*{指针变量名} :代表了这个指针所指向的变量。啥意思呢?对照下面的程序 p=&value, p记录的就是变量value的地址, 然后*p就代表value。#include "sys.h"
#include "led.h"
#include "delay.h"
#include "usart.h"
int value = 0;
int *p;//定义一个指针
int main(void)
{
uart_init(115200);//串口初始化
delay_init();
p=&value;//把变量value的地址复制给指针变量p
printf("Address of a: %d\n",value);
printf("Address of b: %d\n",*p);
while(1)
{
}
}有人会想......就这??? 这不是脱了裤子放屁多此一举???其实我一开始也是这样想的.......既然 * p就代表value那么 * p=XXXX,不就是相当于value=XXXX看看下面这个例子#include "sys.h"
#include "led.h"
#include "delay.h"
#include "usart.h"
int value = 0;
int *p;//定义一个指针
int main(void)
{
uart_init(115200);//串口初始化
delay_init();
p=&value;//把变量value的地址复制给指针变量p
printf("value of a: %d\n",value);
*p=520;
printf("value of b: %d\n",value);
while(1)
{
}
}
还是没感觉到指针有啥用?别着急,先把基本的知识点学完哈。没有最基本的知识储备是不可以的,因为厚积而薄发!见过返回值是指针的函数没?4、函数指针先看一下,如果感觉不理解就接着往下看#include "sys.h"
#include "led.h"
#include "delay.h"
#include "usart.h"
int value = 0;
int *p;//定义一个指针
int *function(void)
{
return &value;//把value的地址返回
}
int main(void)
{
uart_init(115200);//串口初始化
delay_init();
p=function();//调用函数,其实就是把value的地址赋值给了p
printf("Address1 of a: %p\n",&value);//打印value的地址
printf("Address2 of a: %p\n",p);//打印p所代表的地址
while(1)
{
}
}很多人用过返回值是int、char 等等的函数,但是在int,char 后面加个*估计对于初学者没有用过。看下面的哈,其实就是指针之间赋值。下面就是把p(int*类型的指针) 代表的地址赋值给q变量之间可以互相赋值吧,指针之间也一样,可以互相之间赋值。其实和上面是一样的道理,那个函数function返回值是一个 int * 类型的指针, 然后赋值给了p而已#include "sys.h"
#include "led.h"
#include "delay.h"
#include "usart.h"
int value = 0;
int *p;//定义一个指针
int *q;//定义一个指针
int main(void)
{
uart_init(115200);//串口初始化
delay_init();
p=&value;//把value的地址赋值给了p
q=p;//把p代表的地址给q
printf("Address1 of a: %p\n",&value);//打印value的地址
printf("Address2 of a: %p\n",q);//打印p所代表的地址
while(1)
{
}
}姑且再问一句,函数名字是啥?咱们都知道这样调用函数#include "sys.h"
#include "led.h"
#include "delay.h"
#include "usart.h"
void function()
{
printf("zhiguoxin\n");
}
int main(void)
{
uart_init(115200);//串口初始化
delay_init();
function();
while(1)
{
}
}但是这样的见过没#include "sys.h"
#include "led.h"
#include "delay.h"
#include "usart.h"
void (*fun)();
void function()
{
printf("zhiguoxin\n");
}
int main(void)
{
uart_init(115200);//串口初始化
delay_init();
fun = function;
fun();
while(1)
{
}
}这里采用了函数指针先记住一句话函数名就是这个函数的地址!函数名就是这个函数的地址!函数名就是这个函数的地址!既然是地址,那么这个地址应该可以赋值给一个指针。因为是函数的地址,所以咱定义的指针也一定是一个函数类型的。上面的函数 void function() 是一个没有返回值,没有形参的函数。那么咱需要定义一个这种的指针类型,其实就是void (指针变量名字,随意写) () 。上面写的是 void (fun)(); fun就是一个函数类型的指针, 是一个没有返回值,没有形参的函数指针。咱可以把这种函数赋值给这个指针变量。就是上面的fun = function。那么这个函数指针便代表了那个函数fun就等同于function。所以调用 fun(); 就等同于调用 function()。如果函数有形参怎么办? 好办,他有咱就加#include "sys.h"
#include "led.h"
#include "delay.h"
#include "usart.h"
void (*fun)(int a);
void function(int value)
{
printf("value= %d\r\n",value);
}
int main(void)
{
uart_init(115200);//串口初始化
delay_init();
fun = function;//把function赋值给fun
fun(520);//fun就等同于function
while(1)
{
}
}如果函数有返回值怎么办#include "sys.h"
#include "led.h"
#include "delay.h"
#include "usart.h"
int res;
int (*fun)(int a);
int function(int value)
{
return value;
}
int main(void)
{
uart_init(115200);//串口初始化
delay_init();
fun = function;//把function赋值给fun
res = fun(520);//fun就等同于function
printf("res = %d",res);
while(1)
{
}
}小总结一下指针呢其实基本的也就是上面那些,指针就是用来记录变量的地址的。或是做地址之间的传递的。&代表取地址符。*代表取数据。&{变量名} :就是把这个变量的地址取出来。*{指针变量名} :就是把这个指针所代表的地址里面的存的值取出来下面看一些比较常见的应用。把数组的地址赋值给指针,然后用指针操作数组#include "sys.h"
#include "led.h"
#include "delay.h"
#include "usart.h"
char temp[3]={1,2,3};
char *p;
int main(void)
{
uart_init(115200);//串口初始化
delay_init();
p=temp;//将数组名赋值给指针变量p,p就指向数组temp的首地址
printf("value0 = %d\r\n",*p); //p就代表数组的第一个数据的地址
printf("value1 = %d\r\n",*(p+1));//p+1就代表数组的第二个数据的地址
printf("value2 = %d\r\n",*(p+2));//p+2就代表数组的第三个数据的地址
printf("temp[0] = %d\r\n",p[0]);//p[0]等同于temp[0]
printf("temp[1] = %d\r\n",p[1]);//p[1]等同于temp[1]
printf("temp[2] = %d\r\n",p[2]);//p[2]等同于temp[2]
while(1)
{
}
}5、函数的形参是一个指针#include "sys.h"
#include "led.h"
#include "delay.h"
#include "usart.h"
char temp[3]={1,2,3};
void function(char *value)
{
printf("value0 = %d\r\n",value[0]);
printf("value1 = %d\r\n",value[1]);
printf("value2 = %d\r\n",value[2]);
}
int main(void)
{
uart_init(115200);//串口初始化
delay_init();
function(temp);
while(1)
{
}
}以上的指针的基本知识多练习几遍就可以。指针真正的应用是在于代码的封装。可能对于初学者感受不到其作用,但是当你成为真正的开发人员。你会发现把好多功能函数封装起来,然后留出接口来调用是以后必不可少的。封装的时候会大量的使用指针,函数指针、结构体指针等,怎么说呢!90%的程序员敲的是字母,写的是代码。当你开始封装的时候,你写的便是思想,但是需要一定的基础知识储备才能达到。关注微信公众号:[果果小师弟],获取更多精彩内容!智果芯—服务于百万大学生和电子工程师
单片机如何从上电复位执行到main函数?
目录1、启动文件2、系统初始化过程3、全局变量的初始化从事嵌入式开发的伙伴可能会思考过一个问题,我们一般都是使用芯片厂商提供的驱动库和初始化文件,直接从main函数开始写程序,那么系统上电之后,程序怎么引导进main函数执行的呢?还有,系统上电之后RAM的数据是随机的,那么定义的全局变量的初始值又是怎么实现的呢?下面我将带着这两个问题,以Cortex-M架构为例,采用IAR EWARM作为编译工具链,从系统上电之后执行的第一条代码开始,梳理系统的启动过程,了解编译器在此期间所做的工作。其他的工具链,如Keil和GCC在系统初始化过程所做的工作也是相似的,但具体的实现有所差异。1、启动文件芯片厂商提供的启动文件,一般是采用汇编语言编写,少数用C语言。在启动文件中一般至少存在下面两个部分内容:1、向量表2、默认的中断和异常处理程序向量表实际上是一个数组,放置在存储器的零地址,每个元素存储的是各个中断或异常处理程序的入口地址。以STM32F107芯片基于IAR工具的启动文件为例:文件的开头定义了一个名为__vector_table的全局符号,“DATA”的作用是在代码段中定义一个数据区,用作向量表。数据区的内容是使用DCD指令定义的32位宽度常量,除了第一个sfe(CSTACK)比较特殊以为,其他的常量都是异常和中断服务程序的地址(在编译时函数名会被替换成函数的入口地址)。sfe(CSTACK)是IAR汇编器段操作,用于获取段(section)的结束地址,在这里意欲何为呢?实际上这是获取堆栈基地址的操作。IAR在链接器脚本(*.icf)文件中定义堆栈,实际是定义了一个名为“CSTACK”的空闲块(block),如下图的脚本命令所示。所谓的块就是保留一段连续的地址空间,用来作为堆栈或者堆。当然,块也可以是用内容的,例如可以用来管理段,但不在今天的讨论范围。我们知道Cortex-M架构的堆栈模型是满减栈,堆栈从高地址向低地址增长,因此堆栈的基地址是CSTACK的结束地址。向量表的第一个元素是栈基址这是由Cortex-M架构定义的。系统上电后硬件自动从向量表中获取,并设置主堆栈指针MSP,而不是像其他ARM架构,堆栈指针需要通过软件来设置。向量表中第二个元素是复位异常(Reset_Handler)的入口地址。系统上电后,硬件自动从__vector_table + 4的位置读取,并从读取到的地址开始执行。系统上电后CPU执行的第一条是Reset_Handler函数的第一条语句。上面的THUMB命令表示接下来的代码采用THUMB模式(Cortex-M只支持Thumb-2指令集);SECTION用于定义一个段,段名为“.ResetHandler”,段的类型是代码(CODE);REODER指示用给定的名称开启一个新的段;ROOT指示链接器,当段内的符号没有被引用,链接器也不可以丢弃这个段。PUBWEAK是弱定义,如果用户在其他位置编写了中断处理函数,在连接时实际链接用户所编写的,启动文件中用汇编写的服务函数会忽略。之所以要在启动文件中以弱定义的方式编写全部的异常和中断服务函数,是为了防止用户在没有编写服务函数的情况下开启并触发了中断,导致系统的不确定。2、系统初始化过程在EWARM的工程Options > Debugger > Setup中将“Run to”勾选取消,这样在进入调试之后就会停第一条要执行的代码的位置:进入调试之后会停在启动文件Reset_Handler函数第一条汇编指令位置:此时,通过寄存器观察窗口查看SP的值为0x20009820。通过链接时生成的map文件,查看CSTACK的地址范围,0x20009820正好是CSTACK的结束地址。有了MSP,C代码就能运行了。SystemInit函数是芯片厂商根据ARM的CMSIS标准提供的一个系统基础配置函数,配置基础的时钟系统和向量表重定位等。这里的LDR是伪指令,它将SystemInit函数的地址加载到寄存器R0,实际上是通过PC偏移寻址来获取SystemInit的地址。从上面的图可以发现一个问题,在反汇编窗口可以观察到SystemInit的地址是0x20000150,但加载到R0寄存器后却是0x20000151。这是因为在使用跳转指令更新PC时,需要置PC的LSB为1,以表示THUMB模式,由于Cortex-M不支持ARM模式,因此LSB总是1。执行完芯片厂商提供的SystemInit函数之后,跳转到__iar_program_start,这是IAR编译器提供的初始化代码的入口。__iar_program_start首先会执行两个函数:__iar_init_core和__iar_init_vfp,可以完成一些CPU和FPU相关的初始化操作,在某些ARM架构打包好的运行时库会有这两个函数,用户也可以重写这两个函数来自己实现一些相关的操作。之后,跳转到__cmain函数执行。在__cmain中调用了一个__low_level_init函数,该函数专门用于提供给用户编写一个初阶的初始化操作,它在全局变量初始化之前执行,例如可用在__low_level_init中初始化SDRAM,这样就可以将全局变量定义到SDRAM中使用。__low_level_init可以在任意的C文件中编写,注意它的返回值,如果返回0,后续就会跳过变量初始化操作,正常一般都是返回1。3、全局变量的初始化此后进入到__iar_data_init3函数,在这里会完成所有具有初始值的全局/静态变量的赋值,以及零初始化全局/静态变量的清零操作,分别调用__iar_copy_init3和__iar_zero_init3,将保存在ROM区由链接器生成的变量初始值复制到变量的地址。注意,新的EWARM版本默认变量初始化操作可能会采用压缩算法,实际变量初始化调用的函数可能有区别。在全局变量未初始化之前,通过watch窗口可以看到,变量的值都是随机数。在__iar_data_init3执行完成后,全部变量的初值赋值已经完成。在__cmain函数的最后,跳转到用户的main函数,最终开始用户的代码执行。了解了编译器所提供的初始化过程和处理器架构,我们可以根据自己的需求定制系统的初始化。例如,在进入__iar_program_start之前,就可以执行必要的硬件初始化操作,可以用汇编写,也可以用C写。还可以手动控制变量的初始化操作,自己实现变量的初始化。甚至,完全不采用IAR编译器提供的初始化操作,自己从复位序列引导至main函数那也是可以的。
ARM架构与编程(基于I.MX6ULL): 代码重定位(八)(上)
1.段的概念_重定位的引入1.1. 问题的引入led.imx = 头部 + led.bin 或 led.stm32 = 头部 + led.bin头部里含有位置信息(addr):固件要把led.bin复制到哪里去链接程序时,指定了链接地址,一般来说头部信息的addr就等于链接地址如果,偏要修改头部信息的addr,让它不等于链接地址,会发生什么是?头部里含有长度信息(len):led.bin多大在串口程序中添加全局变量,把它打印出来,看看会发生什么事。1.2. 段的概念1.2.1 程序直接烧写在ROM上代码段、只读数据段、可读可写的数据段、BSS段。char g_Char = 'A'; // 可读可写,不能放在ROM上,应该放在RAM里
const char g_Char2 = 'B'; // 只读变量,可以放在ROM上
int g_A = 0; // 初始值为0,干嘛浪费空间保存在ROM上?没必要
int g_B; // 没有初始化,干嘛浪费空间保存在ROM上?没必要所以,程序分为这几个段:代码段(RO-CODE):就是程序本身,不会被修改可读可写的数据段(RW-DATA):有初始值的全局变量、静态变量,需要从ROM上复制到内存只读的数据段(RO-DATA):可以放在ROM上,不需要复制到内存BSS段或ZI段:初始值为0的全局变量或静态变量,没必要放在ROM上,使用之前清零就可以未初始化的全局变量或静态变量,没必要放在ROM上,使用之前清零就可以局部变量:保存在栈中,运行时生成堆:一块空闲空间,使用malloc函数来管理它,malloc函数可以自己写1.2.2 片内固件功能强大,理解段的概念麻烦一点看视频1.3. 重定位保存在ROM上的全局变量的值,在使用前要复制到内存,这就是数据段重定位。想把代码移动到其他位置,这就是代码重定位。2.重定位要做的事2.1. 程序中含有什么?代码段:如果它不在链接地址上,就需要重定位只读数据段:如果它不在链接地址上,就需要重定位可读可写的数据段:如果它不在链接地址上,就需要重定位BSS段:不需要重定位,因为程序里根本不保存BSS段,使用前把BSS段对应的空间清零即可2.2. 谁来做重定位?程序本身:它把自己复制到链接地址去一开始,程序可能并不位于它的链接地址上,为什么它可以执行重定位的操作?因为重定位的代码是使用“位置无关码”写的什么叫位置无关码:这段代码扔在任何位置都可以运行,跟它所在的位置无关怎么写出位置无关码:跳转:使用相对跳转指令,不能使用绝对跳转指令只能使用branch指令(比如bl main),不能给PC直接复制,比如ldr pc, =main不要访问全局变量、静态变量不使用字符串2.3. 怎么做重定位和清除BSS段?核心:复制复制的三要素:源、目的、长度怎么知道代码段/数据段保存在哪?(加载地址)怎么知道代码段/数据段要被复制到哪?(链接地址)怎么知道代码段/数据段的长度?怎么知道BSS段的地址范围:起始地址、长度?这一切在keil中使用散列文件(Scatter File)来描述在GCC中使用链接脚本(Link Script)来描述2.4. 加载地址和链接地址的区别程序运行时,应该位于它的链接地址处,因为:使用函数地址时用的是"函数的链接地址",所以代码段应该位于链接地址处去访问全局变量、静态变量时,用的是"变量的链接地址",所以数据段应该位于链接地址处但是: 程序一开始时可能并没有位于它的"链接地址":比如对于STM32F103,程序被烧录器烧写在Flash上,这个地址称为"加载地址"比如对于IMX6ULL/STM32MP157,片内ROM根据头部信息把程序读入内存,这个地址称为“加载地址”当加载地址 != 链接地址时,就需要重定位。3.链接脚本使用与分析参考手册:Using LD, the GNU linker3.1. 重定位的实质: 移动数据把代码段、只读数据段、数据段,移动到它的链接地址处。也就是复制!数据复制的三要素:源、目的、长度。数据保存在哪里?加载地址数据要复制到哪里?链接地址长度这3要素怎么得到?在GCC中,使用链接脚本来描述。在keil中,跟链接脚本对应的是散列文件,散列的意思就是"分散排列",在STM32F103这类资源紧缺的单片机芯片中:代码段保存在Flash上,直接在Flash上运行(当然也可以重定位到内存里)数据段保存在Flash上,使用前被复制到内存里但是,在资源丰富的MPU板子上:内存很大,几十M、几百M,甚至几G可能没有XIP设备(XIP: eXecute In Place,原地执行)没有类似STM32F103上的Flash,代码无法在存储芯片上直接运行基于这些特点,在MPU板子上代码段、数据段、BSS段等等,运行时没有必要分开存放重定位时,把整个程序(包括代码段、数据段等),一起复制到它的链接地址去3.2. 链接脚本示例3.2.1 链接脚本示例SECTIONS {
. = 0xC0200000; /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}3.2.2 链接脚本语法3.2.2.1 完整的语法一个链接脚本由一个SECTIONS组成。一个SECTIONS里面,含有一个或多个section。SECTIONS {
...
secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill
...
}section是链接脚本的核心,它的语法如下:secname start BLOCK(align) (NOLOAD) : AT ( ldadr )
{ contents } >region :phdr =fill3.2.2.2 几个例子实际上不需要那么复制,不需要把语法里各项都写完。示例1SECTIONS {
.text : { *(.text) } /* secname为".text",里面是所有文件的".text"段 */
.data : { *(.data) } /* secname为".data",里面是所有文件的".data"段 */
.bss : { *(.bss) *(.COMMON) } /* secname为".bss",里面是所有文件的".bss"段和".COMMON"段 */
}示例2还可以按文件指定SECTIONS {
outputa 0x10000 : /* secname为"outputa",链接地址为0x10000 */
{
first.o /* 把first.o整个文件放在前面 */
second.o (.text) /* 接下来是second.o的".text"段 */
}
outputb : /* secname为"outputb",链接地址紧随outputa */
{
second.o (.data) /* second.o的".data"段 */
}
outputc : /* secname为"outputc",链接地址紧随outputb */
{
*(.bss) /* 所有文件的".bss"段 */
*(.COMMON) /* 所有文件的".COMMON"段 */
}
}示例3SECTIONS {
.text 0x10000 : AT (0) /* secname为".text",链接地址是0x10000,加载地址是0 */
{ *(.text) }
.data 0x20000 : AT (0x1000) /* secname为".data",链接地址是0x20000,加载地址是0x1000 */
{ *(.data) }
.bss : /* secname为".bss",链接地址紧随.data段,加载地址紧随.data段 */
{ *(.bss) *(.COMMON) }
}3.3. 怎么获得各个段的信息数据复制3要素:源、目的、长度。怎么知道某个段的加载地址、链接地址、长度?3.3.1 怎么确定源?可以用ADR伪指令获得当前代码的地址,对于这样的代码:.text
.global _start
_start:
......
adr r0, _startadr是伪指令,它最终要转换为真实的指令。它怎么获得_start代码的当前所处地址呢?实际上,adr r0, _start指令的本质是r0 = pc - offset,offset是在链接时就确定了。3.3.2 怎么确定目的地址?也就是怎么确定链接地址?可以用LDR伪指令。对于这样的代码:.text
.global _start
_start:
......
ldr r0, =_startldr是伪指令,它最终要转换为真实的指令。它怎么获得_start的链接地址呢?_start的链接地址在链接时,由链接脚本确定。3.3.3 如何获得更详细的信息在链接脚本里可以定义各类符号,在代码里读取这些符号的值。比如对于下面的链接脚本,可以使用__bss_start、__bss_end得到BSS段的起始、结束地址:__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;上述代码里,有一个".“,它被称为"Location Counter”,表示当前地址:可读可写。它表示的是链接地址。. = 0xABC; /* 设置当前地址为0xABC */
_abc_addr = . ; /* 设置_abc_addr等于当前地址 */
. = . + 0x100; /* 当前地址增加0x100 */
. = ALIGN(4); /* 当前地址向4对齐 */注意:"Location Counter"只能增大,不能较小。3.4. 编写程序重定位数据段3.4.1 修改链接脚本我们故意只重定位数据段,在后面的课程再来重定位代码段并引入更多知识。数据段要被复制到哪去?需要在链接脚本里确定一下:增加了__data_startSECTIONS {
. = 0xC0200000; /* 对于STM32MP157设置链接地址为0xC0200000, 对于IMX6ULL设为0x80200000 */
. = ALIGN(4);
.text :
{
*(.text)
}
. = ALIGN(4);
__data_start = .;
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) *(.COMMON) }
__bss_end = .;
}3.4.2 编写程序修改start.S:ldr r0, =__data_start /* 目的: 链接地址 */
/* 计算data段的当前地址:
* _start的链接地址 - _start的当前地址 = __data_start的链接地址 - data段的当前地址
* data段的当前地址 = __data_start的链接地址 - (_start的链接地址 - _start的当前地址)
*/
adr r1, _start
ldr r2, =_start
sub r2, r2, r1
sub r1, r0, r2
/* 计算data段的长度 */
ldr r2, =__bss_start
ldr r3, =__data_start
sub r2, r2, r3
bl memcpy /* 需要3个参数: dest, src, len */
基于STM32的心率计(一)DMA方式获取传感器数据
前言最近利用下班后的时间,使用STM32做了个心率计,从单片机程序到上位机开发,到现在为止完成的差不多了,实现很简单,STM32开发板外加一个PulseSensor传感器就行,这里我选择的是uFUN开发板,又开发了配套的串口上位机,实现数据的解析和显示,运行界面如下:其实PulseSensor官方已经配备的了Processing语言编写的上位机软件,串口协议的,界面还蛮好看,只要按照它的通信协议,就可以实现心跳波形和心率的显示。刚好最近学习了Qt,所以就用这个小软件来练手了。传感器介绍PulseSensor 是一款用于脉搏心率测量的光电反射式模拟传感器。将其佩戴于手指、耳垂等处,利用人体组织在血管搏动时造成透光率不同来进行脉搏测量。传感器对光电信号进行滤波、放大,最终输出模拟电压值。单片机通过将采集到的模拟信号值转换为数字信号,再通过简单计算就可以得到心率数值。信号输出引脚连接到示波器,看一下是什么样的信号:可以看出信号随着心跳起伏变化,周期大概为:1.37/2 = 0.685s。计算出心率值为:600 / 0.685 = 87,我的心率在正常范围内(废话!),这个传感器测心率还是可以的。手头上没有传感器的朋友,可以看一下这篇自制心率传感器的教程:手指检测心跳设计——传感器制作篇,这篇文章介绍的使用一个红外发射管和一个红外接收管,外加放大滤波电路,效果还是挺不错的。AD采集电路的分析大家在使用ADC接口的时候要注意了,线别插错了。我第一次使用就是测不到电压值,后来用万用表量了一下,才发现是入门指南中引脚功能标示错了,要采集AD电压,输入脚应该接DCIN这个,对应的是PC3-ADC_IN13。如下图。可能是由于原理图版本的迭代,入门指南没有来得及更新吧!手动@管理员 更改一下。从原理图中可以看出,直流电压采集电路前级采用双T陷波滤波器滤除50Hz工频干扰,后级为运放电路:关于前级的双T陷波滤波器S域分析,可以参考这篇文章:双T陷波器s域计算分析(纯手算,工程版!)大学期间学得信号与系统都忘了,所以这部分计算我没有看懂。其实了解电路的S域分析,更有利于理解电路的特性,大家还是要掌握好理论基础。后面的运放电路,还是大概能看懂的,下面来分析一下直流通路,把电容看作断路:所有的运放电路分析,就记住两个要点就行了:虚短和虚断。(感觉又回到了大学。。。。)虚短:理解成短路,运放处于线性状态时,把两输入端视为等电位,即运放正输入端和负输入端的电压相等,即U+ = U-。虚断:理解成断路,运放处于线性状态时,把两输入端视为开路,即流入正负输入端的电流为零。总结一句话:虚短即U+=U-;虚断即净输入电流为0。好了,有了这两把利器,我们来看一下这部分电路的分析,直流通路可进一步简化为:很明显,可计算出U+ = 0.5 * VCC = 1.65v应用虚短:U- = U+ = 1.65v应用虚断,即没有电流流入运放,根据串联电流相等:以上三式联立,可得:Uo = 3.368 - 1.205*Ui即:Ui = 3 - 0.83 * Uo只要得到单片机采集到的电压值Uo,就可以反推出实际的传感器电压值Ui。通过使用示波器测量Ui和Uo的波形,近似可以认为是反向的,但是明显可以看出,Uo的峰值比Ui的峰值小一点。而且通过绘制 Ui=3-0.83*Uo和 Ui=3.3-Uo的曲线,也可以看出,两条直线几乎重合,即输入和输出近似为反向。DMA简介DMA,即直接存储器,用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须 CPU任何干预,通过DMA数据可以快速地移动。这就节省了CPU的资源来做其他操作。STM32共有两个DMA控制器有12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。关于DMA通道和外设的对应,可以查看STM32参考手册,心率传感器使用的PC3-ADC_IN13,对应的是DMA1的通道1STM32 DMA程序配置获取ADC通道的电压值主要有两种方式,一种是直接使用ADC,然后在需要使用的地方,先启动AD转换,然后读取AD值。另一种更好的方式是使用DMA方式,就是先定义一个保存AD值的全局变量,而全局变量是对应内存中的一个地址的。只要初始时,把DMA和ADC配置好了,DMA会自动把获取到的AD值,存入这个地址中,我们在需要的时候,直接读取这个值就可以了。0.定义一个全局变量必须是全局变量,用于存放AD值。uint16_t ADC_ConvertedValue;1.配置GPIO和使能时钟使能外设对应的时钟,注意时钟总线的不同:RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOC, ENABLE);引脚配置成模拟输入模式:GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //设置为模拟输入
GPIO_Init(GPIOC, &GPIO_InitStructure);2.配置DMA配置ADC对应的DAM1通道1:DMA_DeInit(DMA1_Channel1);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&(ADC1->DR)); //设置源地址
DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&ADC_ConvertedValue; //设置内存地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 设置传输方向
DMA_InitStructure.DMA_BufferSize = 1;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //循环模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High; //高优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
DMA_Cmd(DMA1_Channel1, ENABLE); //使能DMA1通道13.配置ADC由于只有1个通道,不需要配置成扫描模式:ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE ;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);PC3对应ADC输入通道13,注意采样周期不能太短:ADC_RegularChannelConfig(ADC1, ADC_Channel_13, 1, ADC_SampleTime_55Cycles5);
ADC_DMACmd(ADC1, ENABLE);
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
ADC_SoftwareStartConvCmd(ADC1, ENABLE);4.主程序调用DMA和ADC配置好之后,只需要初始化一次。然后就可以随时获取电压值了。int main(void)
{
float Sensor_Voltage;
float Uo_Voltage;
delay_init();
UART1_Config(115200);
ADC1_Init();
while(1)
{
Uo_Voltage = ADC_ConvertedValue * 3.3 / 4096;
Sensor_Voltage = 3.3 - Uo_Voltage; //近似值
// Sensor_Voltage = 3 - 0.83 * Uo_Voltage; //实际传感器输出电压值
ANO_SendFloat(0xA1, Sensor_Voltage);
delay_ms(10);
}
}为了方便查看数据的波形,这里直接使用了匿名上位机来显示电压值的波形。函数实现//匿名上位机,波形显示一个浮点型数据ANO_SendFloat(0xA1, ad);
void ANO_SendFloat(int channel, float f_dat)
{
u8 tbuf[8];
int i;
unsigned char* p;
for(i = 0; i <= 7; i++)
tbuf[i] = 0;
p = (unsigned char*)&f_dat;
tbuf[0] = 0x88;
tbuf[1] = channel; //0xA1
tbuf[2] = 4;
tbuf[3] = (unsigned char)(*(p + 3)); //取float类型数据存储在内存中的四个字节
tbuf[4] = (unsigned char)(*(p + 2));
tbuf[5] = (unsigned char)(*(p + 1));
tbuf[6] = (unsigned char)(*(p + 0));
for(i = 0; i <= 6; i++)
tbuf[7] += tbuf[i]; //校验和
printf("%s", tbuf);
}实际的显示没有调试器,如何下载程序呢?可以参考我之前发的一篇帖子:如何使用串口来给STM32下载程序,详细介绍了如何通过串口来给uFUN开发板下载程序。匿名上位机的帧格式配置实际的显示效果:总结传感器数据的获取,只是心率计实现的第一步,传感器放置位置的不同,波形的振幅也会不同,所以,对获得数据的处理、分析,才是最关键的部分。