搞嵌入式软件开发的我们在程序代码里面会经常与全局变量global variable打交道在有实时操作系统参与的代码里面甚至有时候可能还会因为全局变量的使用不当带来Bug等问题。另外我们平时也主要是手动定义和使用全局变量一般很少关注其在MCU内部的运行情况除非是出现与其相关的问题才会去深入排查。综上情况我们其实很有必要深入了解一下全局变量在MCU内部的底层运行逻辑也就是MCU究竟是怎么操纵全局变量来为我们所用的别着急跟着我一步一步来看这里所说的全局变量指的是可以应用于工程内所有文件的全局变量以及只作用于作用域某个源文件的静态static全局变量。1、我们先来看下代码里怎么定义全局变量的。下面我们以两个全局变量为例进行阐述第一个是是可以应用于工程内所有文件的全局变量在app_do.h文件里进行原型声明目的是可以让其他源文件使用它复制externu8 module_type;在app_do.c文件里进行定义和初始化复制u8module_type 0;第二个是只作用于app_do.c源文件的静态static全局变量这里我们用数组变量来描述原理上和基本变量一样在app_do.c文件里进行定义和初始化复制staticu8 sgm42406_device[6] {0};由于只用在app_do.c文件因此不需要再在app_do.h进行声明。这里要讲一下其实初始化全局变量有两种方法一种就是在定义的时候直接给其赋值初始值默认值缺省值如上所示另外一种就是在初始化函数接口里面给其赋值类似下面这样那这两种有什么区别吗可以根据自身需求进行操作直接初始化‌适用于初始值在编译时可知的情况如配置常量、固定阈值等等‌函数内赋值‌适用于需要根据运行时条件动态设置初始值的场景。不过个人建议可以在对应模块的初始化函数接口里统一初始化全局变量增强代码可管理性。2、我们再来看下程序编译后全局变量的一些情况程序编译通过后我们可以到map映像文件里面看下module_type和sgm42406_device的定义情况和内存映射情况打开map文件搜索两个全局变量另外两种不同类型的全局变量其实被划分在不同位置里面其中0x20000034和0x20000047其实是编译器为两个全局变量分配的内存存储地址总得需要一个地方来存变量严格意义上说应该是内存起始地址Data表示程序中的‌数据符号‌如全局变量或静态变量其大小字段表示该变量占用的字节数分别是1个字节和6个字节.data即表示.data段为已初始化的全局变量。那它们的地址为什么类似呢即偏移地址都是0x20000000呢这就又涉及到MCU存储器地址映射的知识了。我们知道全局变量是存储在RAM里面的有些MCU也写为SRAM而RAM存储器的地址映射的起始地址一般就是0x20000000其结束地址取决于MCU的RAM大小比如如下是我所用工程对应的MCU系列的用户手册里的信息另外在Keil魔术棒里面也可以查看到RAM的信息主要包括起始地址和大小如上所述起始地址为0x20000000大小为0x3800字节即224KB与用户手册所描述的一样。3、我们继续来看下MCU运行阶段的变量操作情况MCU操作全局变量读写操作归根结底是通过汇编语言指令操作内存地址的方式来实现的我们可以仿真看下MCU复位情况下Register菜单的开启和显示如下图所示常用的寄存器R0和R1等的值都是为0x00000000这里说一下对于Cortex-M3和Cortex-M4处理器的寄存器来说有16个寄存器包括13个通用目的寄存器和3个特殊用途寄存器如下图所示在对应的汇编代码Disassembly中可以看到是如何通过汇编指令操作这些寄存器的。我们在全局变量module_type赋值语句后面打个断点然后运行程序看下R1寄存器的值已经变成0x20000034了这不就是全局变量module_type的存储地址嘛。从C语言代码对应的汇编语言代码也可以看出一条给全局变量赋值的语句是如何通过汇编指令LDRBLDR和STRB来实现的这三条汇编代码的主要作用大概如下所述复制/*LDRB Load Register Byte加载字节到寄存器 从栈指针(sp)偏移0x04的地址读取1个字节 读取的字节零扩展到32位后存入r0寄存器 源地址 sp 4*/LDRB r0,[sp,#0x04]/*LDR Load Register加载字到寄存器 从程序计数器(pc)偏移32的地址读取4个字节一个字 读取的值直接存入r1寄存器 源地址 pc 32*/LDR r1,[pc,#32] ; 0x08026D7C/*STRB Store Register Byte存储字节到内存 将r0寄存器的最低字节8位存储到内存 目标地址 r1 0即r1指向的地址 */STRB r0,[r1,#0x00]这段代码实现的功能主要是从栈上读取一个字节数据加载一个内存地址0x08026D7C到r1将读取的字节存储到这个地址处其实本质上是在将一个字节数据从栈复制到某个固定内存地址即将局部变量id_value的值赋给全局变量module_type。再看下通过环形队列给数组sgm42406_device拷贝数据的操作过程虽然我们现在基本很少通过汇编语言来直接开发软件但汇编语言的指令操作其实是最接近硬件底层的直接通过操作寄存器的方式会显得更加直观不过汇编语言的平台差异性比较大可移植比较差所以现在搞嵌入式基本上都用C语言了真正的牛人也许还在用汇编语言还是很厉害的~~当我们读取全局变量module_type的值的时候汇编代码同样会进行一些操作即通过LDRLDRB和SUBS等指令进行操作其中的SUBS指令即减法指令 将 r0 减去立即数 0xF0十进制 240这个值其实就是module_type的值结果存回 r0所以可以看出来C语言里面的对变量的读操作其实和汇编语言里面的读操作的实现机制其实是不太一样的。而读取全局数组sgm42406_device的值的汇编代码可以看到是如下所示综上所述当我们查看反汇编代码的时候其实可以间接的看到内核寄存器的一些变化而其变化通常与变量的内存存储地址等相关这就是底层逻辑。另外我们可以看出来每次访问全局变量的时候不管是读操作还是写操作常用的汇编指令一般都是LDRLDRBSTR和STRB等等。4、我们最后再来看下Keil里怎么监控全局变量如果需要通过仿真来实时监控查看全局变量的值可以按照下面这样进行。进入仿真后首先要勾选“Periodic Window Update”这样才能在Watch窗口里看到实时变化的全局变量的值对于非static的全局变量直接将变量名称module_type添加到Watch窗口里然后运行程序即可看到其值如果其值发生改变其窗口会变成蓝色对于static全局变量直接用上面的操作方式是不行的会报错对于static全局变量要通过类似监控局部变量的打断点单步调试运行一次后才能用全速运行的方式查看先设断点单步调试一次然后全速运行即可实时监控其状态另外如果你想直接在Watch窗口里查看全局变量的存储地址也是可以的使用”全局变量”的方式就可以通过以上操作就可以同时看到变量的内存存储地址和变量值了。对于全局数组因为数组名本身就代表其起始地址的含义因此不需要再在前面加运算符。或者通过Memory Windows也可以查看变量的内存地址即在Memory窗口的地址栏输入 ”全局变量”后按回车键窗口就会跳转到该变量的内存地址起始处并显示该地址存储的十六进制数据以上所有便是MCU定义和使用全局变量的一些过程和底层逻辑描述有时候了解这些东西确实是有助于我们反向排查问题的~~来吧一起学习吧~~