8. 程序各种要素说明

这节课我们用一个最简单的程序跟大家讲清楚程序的构成。(请看视频)

8.1. 概述

  • 硬件

首先要知道硬件的组成。

在前面章节我们说过,芯片包含FlashRAM

他们虽然不是相同的东西,但是都属于同一个地址空间,32位芯片的地址空间大小是4G。

比如ST32,FLASH通常从0X8000000开始,而RAM就从0x20000000开始。

高级点的芯片,可能会有外部SDRAM,内核也会为这SDRAM分配一段地址。

地址,就是地址,比如你们家的门牌号,酒店的房间号。

TODO添加STM32芯片地址映射图。

  • 程序

程序包含什么?

写代码的时候包含函数过程变量

编译得到的目标文件包含函数过程和变量的初始化值

  • 变量

变量有很多种:全局变量,局部变量、静态变量。。。

变量保存在哪里?

下面我们就从一个简单的程序来分析上面问题。

8.2. 包罗万象的小程序

8.2.1. 程序入口

程序入口,程序启动执行的第一条代码就叫程序入口。

或者说,芯片上电开始执行的第1条用户代码。

这条代码在哪?

我们写代码,通常都是从main函数开始写,我们也会把main函数叫做函数入口。

那么main函数是芯片复位的第一条代码吗?

实际不是,在执行main函数之前,已经执行了很多代码了。

其中最早执行的,也就是芯片复位的第一条代码,就是我们经常说的启动代码。

在我们的STM32工程中,启动代码就是startup_stm32f10x_hd.s。

这是一个汇编文件。我们一起来看看这个启动代码。这个文件是一个汇编文件。

; 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
                DCD     0                          ; Reserved

这就是启动代码的入口。但是这里放的并不是代码,而是函数指针,这些函数指针就是中断向量。

DCD的意思是分配一个空间来保存后面的值。

__Vectors是一个标号,等下在分散加载文件中会提到。

现在我们只要知道这里保存的是中断向量,并且,复位也是一个中断。

当芯片复位时,芯片从这里找到对应的函数指针Reset_Handler,然后跳到这个函数执行。

这个函数同样在启动文件中,如下:

; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0               
                LDR     R0, =__main
                BX      R0
                ENDP

复位后芯片做了什么呢?

  1. 调用SystemInit函数。
  2. 调用__main。

SystemInit在system_stm32f10x.c文件中,这个函数完成芯片的时钟配置。

__main函数在哪呢?在工程中找不到的。是不是main?不是。

这是一个编译系统根据不同芯片生成的一个库函数。

在这个库函数中完成变量(RAM)的初始化,居然后跳到真正的main函数执行。

8.2.2. 函数

int main(void)是我们接触的第一个函数。

函数的定义包含名称、参数、返回值。

我们可以定义一些子函数。

8.2.3. 变量

  • 全局变量

在函数外定义的叫做全局变量。

比如main函数中,SegTab就是一个全局变量,这个变量的类型是一个uint16_t数组。

/*
	定义一个全局数组SegTab,数组成员类型是uint16_t
	并初始化数组。
	这个数组是数码管显示0-9的段定义。
	请看seg_display函数,
	例如,第一个值是0x3f00
	在seg_display,取这个数,输出到IO口,LED就能显示0。
*/
uint16_t SegTab[10]={0x3f00, 0x0600, 0x5b00, 0x4f00, 0x6600, 0x6d00, 0x7d00, 0x0700, 0x7f00, 0x6700};

变量是保存在RAM中的,我们都知道RAM是易失性存储,掉电数据就没了,那数组的些值是如何赋值给数组的呢?

这问题有两个方面:

  1. 编译的时候,这些值会保存在代码中。同时还保存这些值和变量的关系。(细节暂时不研究)
  2. 在启动代码中,执行__main函数时,会根据这些关系执行初始化变量的过程,然后才执行用户的main函数。
  3. 这个过程就是编译器生成的,如果你用一些很便宜的单片机,比如台湾的一些小单片机,这个过程就需要自己写代码实现,通常是用汇编写。
  • 局部变量

在函数内定义的变量就是局部变量,例如seg_display函数中的tmp就是一个局部变量。

/*
   定义一个seg_display
   输入参数有2个,分别是char型的num,char型的dot
   没有返回值。
*/
void seg_display(char num, char dot)
{
   	uint16_t tmp;

局部变量同样也是在RAM上。但是具体在哪呢?地址是哪里?

局部变量的地址是不固定的。当调用函数时,从栈上分配。函数退出后就释放了。

  • 变量有效域

    局部变量只在函数中有效。

    全局变量呢?

    这个不是芯片的知识,是C语言的知识。和编译系统也有关系,在MDK中,全局变量在声明之后的C代码中都可以调用。

    还可以通过EXTERN在外部文件中声明后调用。

    局部变量可以通过static定义成类似全局变量,但仅限本函数使用。

    static还可以限制全局变量只在本文件有效。

8.3. 分散加载文件

为什么启动代码就是上电执行的第一条指令呢?

因为我们用分散加载文件(链接文件)指定启动代码保存在芯片复位时指向的位置。

分析分散加载文件

; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; *************************************************************

LR_IROM1 0x08000000 0x00080000 { ; load region size_region ER_IROM1 0x08000000 0x00080000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00010000 { ; RW data .ANY (+RW +ZI) } }

定义了IROM1,地址和范围就是芯片Flash的定义。

其中,放在最全面的是reset,也就是启动代码中定义的AREA RESET, DATA, READONLY

紧接则放置的是InRoot$$Sections,这些代码是编译器链接时根据芯片和内核自动添加的。

我们可以认为这就是__main。

最后放其他代码,也就是RO段。

定义IRAM1,就是RAM,内存。

所有的RW段和ZI段都放在RAM中。

8.4. 编译结果如何看?

  1. 在MDK IDE界面有编译过程和最终结果:
compiling stm32f10x_sdio.c...
compiling stm32f10x_rcc.c...
compiling stm32f10x_usart.c...
compiling stm32f10x_spi.c...
compiling stm32f10x_tim.c...
compiling main.c...
compiling stm32f10x_wwdg.c...
linking...
Program Size: Code=1336 RO-data=336 RW-data=24 ZI-data=1632  
FromELF: creating hex file...
".\Objects\stm32_tech.axf" - 0 Error(s), 0 Warning(s).
Build Time Elapsed:  00:00:19

Program Size: Code=1336 RO-data=336 RW-data=24 ZI-data=1632

这一句说明生成的目标文件大小,代码1336字节,RO(只读变量)336字节,RW(读写变量)24字节,ZI数据1632字节。

  1. 更细的情况,可以通过map文件查看。map文件在Listings\目录下,名字叫stm32_tech.map

    用文件编辑器打开就能看到内容。

    map文件最开始是最细的地方,最后是整体情况。拖到最后,就能看到下面内容:

    ==============================================================================
    
    
          Code (inc. data)   RO Data    RW Data    ZI Data      Debug   
    
          1336         96        336         24       1632     236688   Grand Totals
          1336         96        336         24       1632     236688   ELF Image Totals
          1336         96        336         24          0          0   ROM Totals
    
    ==============================================================================
    
        Total RO  Size (Code + RO Data)                 1672 (   1.63kB)
        Total RW  Size (RW Data + ZI Data)              1656 (   1.62kB)
        Total ROM Size (Code + RO Data + RW Data)       1696 (   1.66kB)
    
    ==============================================================================
    

    这些内容跟IDE中看到的基本类似。这是程序的总体情况。有一个地方需要注意:

    Total RO Size (Code + RO Data):

    Total RW Size (RW Data + ZI Data)

    Total ROM Size (Code + RO Data + RW Data)

    Total ROM Size就是最终的目标文件,也就是写到FLASH上的内容,请问,为什么包含RW Data的大小?

    因为RW数据需要一个初始化值,这个值并不是凭空而来,而是代码中定义了,编译后保存在ROM中。

    所以ROM会包含RW。

    往回看,则是Image component sizes。map文件每个大段之间用等号分开。

    ==============================================================================
    
    Image component sizes
    
    
          Code (inc. data)   RO Data    RW Data    ZI Data      Debug   Object Name
    
           304         20          0         24          0       1705   main.o
             0          0          0          0          0     203136   misc.o
            64         26        304          0       1536        792   startup_stm32f10x_hd.o
           298          0          0          0          0      12407   stm32f10x_gpio.o
            26          0          0          0          0      16706   stm32f10x_it.o
            32          6          0          0          0        557   stm32f10x_rcc.o
           328         28          0          0          0       1845   system_stm32f10x.o
    
        ----------------------------------------------------------------------
          1058         80        336         24       1536     237148   Object Totals
             0          0         32          0          0          0   (incl. Generated)
             6          0          0          0          0          0   (incl. Padding)
    
        ----------------------------------------------------------------------
    
          Code (inc. data)   RO Data    RW Data    ZI Data      Debug   Library Member Name
    
             8          0          0          0          0         68   __main.o
    

    本段说明了组成程序的各个文件的信息,每一个.o文件对应一个.c文件。

    从这我们还能看到程序暗地里使用了多少个函数库。

    再往上:

    Memory Map of the image,说明各文件使用的RAM分类情况。

    Image Symbol Table,这是个文件中使用的函数和RAM情况。

    在往上的内容我们基本也不会看了。

  2. 这里我们关键看下函数入口的情况。

     RESET                                    0x08000000   Section      304  startup_stm32f10x_hd.o(RESET)
        !!!main                                  0x08000130   Section        8  __main.o(!!!main)
        !!!scatter                               0x08000138   Section       52  __scatter.o(!!!scatter)
        !!handler_copy                           0x0800016c   Section       26  __scatter_copy.o(!!handler_copy)
        !!handler_zi                             0x08000188   Section       28  __scatter_zi.o(!!handler_zi)
    

    在0x08000000,放的确实是向量表。芯片复位时就会从这里开始执行代码。

    除了__main,还有一些我们不知道是什么东西的代码放在启动代码后面。

8.5. 为什么能控制外设?

因为有外设寄存器。

外设寄存器是跟RAM一样的存在。(RAM是可以读写的,外设寄存器有些不能写)。

这些寄存器链接到对应的硬件。

TOTO请看规格书地址空间map图

我们只要写这些寄存器,就能实现对应外设的功能。

我们看ST提供的库,比如下面函数

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->BSRR = GPIO_Pin;
}

要设置一个GPIO的输出,就是配置GPIOx->BSRR = GPIO_Pin;

这时什么意思呢?

typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;

我们可以看到,BSRR 是结构体GPIO_TypeDef的内容。

GPIO_SetBits(GPIOE, GPIO_Pin_7);

使用这个函数的时候我们会传入一个GPIOE,这是一个GPIO_TypeDef结构体指针。

而GPIOE的定义是下面这些宏定义:

#define PERIPH_BASE           ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */

#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)

#define GPIOE_BASE            (APB2PERIPH_BASE + 0x1800)

#define GPIOE               ((GPIO_TypeDef *) GPIOE_BASE)

意思是:

0x40000000这个地址上,是PERIPH_BASE,也就是PERIPH外设的基地址,起始地址。

PERIPH_BASE偏移0x10000的地方,放的是APB2PERIPH,也就是APB2总线上的外设。

APB2PERIPH_BASE偏移0x1800的地方,是GPIOE外设寄存器地址。

第四行则是把一个uint32_t的值强行类型转换为GPIO_TypeDef指针。

如此,就能通过GPIOE这个宏定义找到 GPIOE外设的相关寄存器。


end

2020-04-18