我们通过io和串口的软件开发,已经体验了嵌入式软件开发。不知道大家有没有疑惑,为什么软件能控制硬件?反正当年我学习51的时候,有这个疑惑。今天我们就暂停软件开发,分析单片机到底是如何软硬件结合的。并通过一个基本的程序,分析单片机程序的编译,运行。
软硬件结合 初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写p1=0x55,就可以在io口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。
寻址空间 什么是地址空间呢?所谓的地址空间,就是pc指针的寻址范围,因此也叫寻址空间。
大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,pc指针就是一个32位的二进制数,也就是0xffffffff,范围只有4g寻址空间。现在内存越来越大,4g根本不够,所以需要扩展,为了能访问超出4g范围的内存,就有了64位系统。stm32是多少位的?是32位的,因此pc指针也是32位,寻址空间也就是4g。
我们来看看stm32的寻址空间是怎么样的。在数据手册《stm32f407_数据手册.pdf》中有一个图,这个图,就是stm32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫memory map,用一个新芯片,就先看这个图。
最左边,8个block,每个block 512m,总共就是4g,也就是芯片的寻址空间。
block 0 里面有一段叫做flash,也就是内部flash,我们的程序就是下载到这个地方,起始地址是0x800 0000,大家注意,这个只有1m空间。现在stm32已经有2m flash的芯片了,超出1m的flash放在哪里呢?请自行查看对应的芯片手册。
3 在block 1 内,有两段sram,总共128k,这个空间,也就是我们前面说的内存,存放程序使用的变量。如果需要,也可以把程序放到sram中运行。407不是有196k吗?
其实407有196k内存,但是有64k并不是普通的sram,而是放在block 0 内的ccm。这两段区域不连续,而且,ccm只能内核使用,外设不能使用,例如dma就不能用ccm内存,否则就死机。
block 2,是peripherals,也就是外设空间。我们看右边,主要就是apb1/apb2、ahb1/ahb2,什么东西呢?回头再说。
block 3、block4、block5,是fsmc的空间,fsmc可以外扩sram,nand falsh,lcd等外设。
好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。在io口输出的例程中,我们配置io口是调用库函数,我们看看库函数是怎么做的。
例如:
gpio_setbits(gpiog, gpio_pin_0 | gpio_pin_1 | gpio_pin_2| gpio_pin_3); 这个函数其实就是对一个变量赋值,对gpiox这个结构体的成员bsrrl赋值。
void gpio_setbits(gpio_typedef* gpiox, uint16_t gpio_pin){ /* check the parameters */ assert_param(is_gpio_all_periph(gpiox)); assert_param(is_gpio_pin(gpio_pin)); gpiox->bsrrl = gpio_pin;} assert_param:这个是断言,用于判断输入参数是否符合要求gpiox是一个输入参数,是一个gpio_typedef结构体指针,所以,要用->获取其成员
gpiox是我们传入的参数gpiog,具体是啥?在stm32f4xx.h中有定义。
#define gpiog ((gpio_typedef *) gpiog_base) gpiog_base同样在文件中有定义,如下:
#define gpiog_base (ahb1periph_base + 0x1800)ahb1periph_base,ahb1地址,有点眉目了吧?在进一步看看/*!< peripheral memory map */#define apb1periph_base periph_base#define apb2periph_base (periph_base + 0x00010000)#define ahb1periph_base (periph_base + 0x00020000)#define ahb2periph_base (periph_base + 0x10000000) 再找找periph_base的定义
#define periph_base ((uint32_t)0x40000000)
到这里,我们可以看出,操作io口g,其实就是操作0x40000000+0x1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是sram空间地址,0x40000000+0x1800是外设空间地址。
u32 i;i = 0x55aa55aa; 这个外设空间地址的寄存器是io口硬件的一部分。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0x40000000+0x1800+0x14.
控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。
寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或ram。寄存器为什么能控制硬件外设呢?因为,初略的说,一个寄存器的一个bit,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。
纯软件-包罗万象的小程序 我们已经完成了串口和io口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?代码到底放在那里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。
分析启动代码 函数从哪里开始运行?
每个芯片都有复位功能,复位后,芯片的pc指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,pc可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在stm32中,复位到0x08000004。因此复位后运行的第一条代码就是0x08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。
芯片复位时,芯片从中断表中将reset_handler这个值(函数指针)加载到pc指针,芯片就会执行reset_handler函数了。(一个函数入口就是一个指针)
; vector table mapped to address 0 at reset area reset, data, readonly export __vectors export __vectors_end export __vectors_size__vectors dcd __initial_sp ; top of stack dcd reset_handler ; reset handler dcd nmi_handler ; nmi handler dcd hardfault_handler ; hard fault handler dcd memmanage_handler ; mpu fault handler dcd busfault_handler ; bus fault handler dcd usagefault_handler ; usage fault handler reset_handler函数,先执行systeminit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?
是我们在main.c中定义的main函数吗?后面我们再说这个问题。
; reset handlerreset_handler proc export reset_handler [weak] import systeminit import __main ldr r0, =systeminit blx r0 ldr r0, =__main bx r0 endp 芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujiqueprjobjects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。
在mdk软件options菜单linker下有关于这个菜单的设置。
把use memory layout from target dialog前面的勾去掉,之前不可设置的框都可以设置了。点击edit进行编辑。
在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。
其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定flash跟ram的大小于起始位置,当我们把程序分成boot、core、app,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到ram中运行。
从这个基本的分散加载文件我们可以看出:
第6行 er_irom1 0x08000000 0x00080000定义了er_irom1,也就是我们说的内部flash,从0x08000000开始,大小0x00080000。
第7行.o (reset, +first)从0x08000000开始,先放置一个.o文件, 并且用(reset, +first)指定reset块优先放置,reset块是什么?请查看启动代码,中断向量就是一个area,名字叫reset,属于readonly。这样编译后,reset块将放在0x08000000位置,也就是说,中断向量就放在这个地方。dcd是分配空间,4字节,第一个就是__initial_sp,第二个就是reset_handler函数指针。也就是说,最后编译后的程序,将reset_handler这个函数的指针(地址),放在0x800000+4的地方。所以芯片在复位的时候,就能找到复位函数reset_handler。
第8行 *(inroot$$sections)什么鬼?google啊!回头再说。
第9行 .any (+ro)意思就是其他的所有ro,顺序往后放。就是说,其他代码,跟着启动代码后面。
第11行 rw_iram1 0x20000000 0x00020000定义了ram大小。
第12行 .any (+rw +zi)所有的rw zi,全部放到ram里面。rw,zi,也就是变量,这一行指定了变量保存到什么地址。
分析用户代码 到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。1 程序跳转到main函数后:rcc_getclocksfreq获取rcc时钟频率;systick_config配置systick,在这里打开了systick中断,10毫秒一次。
delay(5);延时50毫秒。
int main(void){ gpio_inittypedef gpio_initstructure; /*! testtmp1) test_tmp1 = 10; else test_tmp1 = 5; testtmp2 +=testtmp3[test_tmp1]; return test_tmp1;} 然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。我们查看delay函数,uwtimingdelay不等于0就死等?谁会将uwtimingdelay改为0?
/** * @brief inserts a delay time. * @param ntime: specifies the delay time length, in milliseconds. * @retval none */void delay(__io uint32_t ntime){ uwtimingdelay = ntime; while(uwtimingdelay != 0);} 搜索uwtimingdelay变量,函数timingdelay_decrement会将变量一直减到0。
/** * @brief decrements the timingdelay variable. * @param none * @retval none */void timingdelay_decrement(void){ if (uwtimingdelay != 0x00) { uwtimingdelay--; }} 这个函数在哪里执行?经查找,在systick_handler函数中运行。谁用这个函数?
/** * @brief this function handles systick handler. * @param none * @retval none */void systick_handler(void){ timingdelay_decrement();} 经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。
__vectors dcd __initial_sp ; top of stack dcd reset_handler ; reset handler dcd nmi_handler ; nmi handler dcd hardfault_handler ; hard fault handler dcd memmanage_handler ; mpu fault handler dcd busfault_handler ; bus fault handler dcd usagefault_handler ; usage fault handler dcd 0 ; reserved dcd 0 ; reserved dcd 0 ; reserved dcd 0 ; reserved dcd svc_handler ; svcall handler dcd debugmon_handler ; debug monitor handler dcd 0 ; reserved dcd pendsv_handler ; pendsv handler dcd systick_handler ; systick handler 余下问题 1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(inroot$$sections)是什么?3 zi段,也就是初始化为0的数据段,什么时候初始化?谁初始化?
为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!
通过map文件了解代码构成 编译结果 程序编译后,在下方的build output窗口会输出信息:
*** using compiler 'v5.06 update 5 (build 528)', folder: 'c:keil_v5armarmccbin'build target 'wujique'compiling stm32f4xx_it.c......assembling startup_stm32f40_41xxx.s...compiling misc.c......compiling mcu_uart.c...linking...program size: code=9038 ro-data=990 rw-data=40 zi-data=6000 fromelf: creating hex file....objectswujique.axf - 0 error(s), 0 warning(s).build time elapsed: 00:00:32 编译目标是wujique c文件compiling,汇编文件assembling,这个过程叫编译 编译结束后,就进行link,链接。 最后得到一个编译结果,9038字节code,ro 990,rw 40,zi 6000。code,是代码,很好理解,那ro、rw、zi都是什么? fromelf,创建hex文件,fromelf是一个好工具,需要自己添加到option中才能用 map文件配置 更多编译具体信息在map文件中,在mdk options中我们可以看到,所有信息都放在listingswujique.map
默认很多编译信息可能没钩,钩上所有信息会增加编译时间。
map文件 打开map文件,好乱?习惯就好。我们抓重点就行了。
map 总信息
从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。
有多少ro?ro到底是什么?
有多少rw?rw又是什么?
rom为什么不包括zi data?为什么包含rw data?
image component sizes
往上,看看image component sizes,这个就比刚刚的总体统计更细了。
这部分内容,说明了每个源文件的概况
首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些stm32的库文件。
第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1m flash上的程序压缩到能在512k上运行。
第3部分也是库,暂时没去分析这两个是什么东西。
库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:
#include #include #include 这些就是库的头文件。这些头文件保存在mdk开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。
文件map
再往上,就是文件map了,也就时每个文件中的代码段(函数)跟变量在rom跟ram中的位置。首先是rom在0x08000000确实放的是startup_stm32f40_41xxx.o中的reset
库文件是什么?
库文件就是别人已经别写好的代码库。
在代码中,我们经常会包含一些头文件,例如:
#include #include #include 这些就是库的头文件。这些头文件保存在mdk开发工具的安装目录下。
我们经常用的库函数有:
memcpy、memcmp、strcmp等。 只要代码中包含了这些函数,就会链接库文件。
文件map 再往上,就是文件map了,也就时每个文件中的代码段(函数)跟变量在rom跟ram中的位置。首先是rom在0x08000000确实放的是startup_stm32f40_41xxx.o中的reset
每个文件有有多行,例如串口,4个函数。
然后是ram的,main.o中的变量,放在0x20000000,总共有0x0000000c,类型是data、rw。串口有两种变量,data和bss,什么是bss?这两个名称,是section name,也就是段的意思。看前面type和attr,
rw data,放在.data段;rw zero放在.bss段,rw zero,其实就是zi。到底哪些变量是rw,哪些是zi?
image symbol table
再往上就是image symbol table,就更进一步到每个函数或者变量的信息了
例如,全局变量testtmp1,是data,4字节,分配的位置是0x20000004。
testtmp3数组放在哪里?放在0x080024e0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几k几十k大,不可能也没必要放到ram区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。
const的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习
那局部变量存放在哪里呢?我们找到了test_tmp3,
没找到test_tmp1/test_tmp2,为什么呢?在定义时,test_tmp3增加了static定义,意思就是静态局部变量,功能上,相当于全局变量,定义在函数内,限制了这个全局变量只能在这个函数内使用。哪test_tmp1、test_tmp2放在哪里呢? 局部变量,在编译链接时,并没有分配空间,只有在运行时,才从栈分配空间。
u8 testfun(u32 x)//函数,带一个参数,并返回一个u8值{ u8 test_tmp1 = 4;//局部变量,初始化 u8 test_tmp2;//局部变量,未初始化 static u8 test_tmp3 = 0;//静态局部变量 上一部分,我们留了一个问题,哪些变量是rw,哪些是zi?我们看看串口变量的情况,uartbuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文block started by symbol的简称。
到这里,我们可解释下面几个概念了:
code就是代码,函数。
ro data,就是只读变量,例如用const修饰的数组。
rw data,就是读写变量,例如全局变量跟static修饰的局部变量。
zi data,就是系统自动初始化为0的读写变量,大部分是数组,放在bss段。
ro size等于代码加只读变量。
rw size等于读写变量(包括自动初始化为0的),这个也就是ram的大小。
rom size,也就是我们编译之后的目标文件大小,也就是flash的大小。但是?为什么会包含rw data呢?因为所有全局变量都需要一个初始化的值(就算没有真正初始化,系统也会分配一个初始化空间),例如我们定义一个变量u8 i = 8;这样的全局变量,8,这个值,就需要保存在falsh区。
我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579
main是main,放在0x08000189
__main到main之间发生了什么?还记得分散加载文件中的这句吗?
*(inroot$$sections) __main就在这个段内。下图是__main的地址,在0x08000189。__vectors就是中断向量,放在最开始。
在分散加载文件中,紧跟reset的就是*(inroot$$sections)。
而且,reset段正好大小0x00000188。
巧合?参考ppt文档《arm嵌入式软件开发.ppt》,或自行google。
这一段代码都完成什么功能呢?主要完成zi代码的初始化,也就是将一部分ram初始化为0。其他环境初始化,通常,我们不用管这一部分。
其他再往上,就是其他信息了,例如优化了哪些东西,移除了哪些函数。
最后 到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。不过,对于中断,后面还会进行详细说明。
ipv4和ipv6有什么区别
Altera发布FPGA业界第一款SoC FPGA软件开发虚拟目标平台
五大最实用的Jupyter Notebook扩展插件
铝PCB在LED灯领域的应用
构建物联网的多种不同实现方式
深度:单片机到底是如何软硬件结合的
人类智慧如此伟大,感受超智慧的卡通机器人
成斥资新台币500亿建首座最先进的面板级扇出型封测生产线
vivoX20最新消息:vivoX20怎么样值得购买吗?vivoX20价格遭吐槽你怎么看?
小米12系列以大小尺寸屏幕对标苹果iPhone
微芯低功耗FPGA视频、图像处理解决方案
带清除水雾功能的水表的原理及设计
2020年车联网市场将达千亿 车载终端达2500万辆
55V高效降压升压电源管理器及多化学电池充电器简介
社区电商APP开发公司
关于智能汽车环境感测三种主流传感器的解析
移动通信3G技术介绍
ATA-7000系列高压放大器
迅龙软件荣获2022年深圳市“专精特新”企业认定
便携式农药残留速测仪的应用、性能及参数