你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【经验分享】STM32单片机是如何软硬件结合的

[复制链接]
STMCU小助手 发布时间:2022-6-2 14:15
不知道大家有没有疑惑,为什么软件能控制硬件?
本文分析STM32单片机到底是如何软硬件结合的,分析单片机程序如何编译,运行。

9 s. L( v/ l  O, u/ ]% d" U9 t" G/ x9 i
软硬件结合
初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写P1=0X55,就可以在IO口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。

; }. d( [  F; b" C' E
寻址空间
什么是地址空间呢?所谓的地址空间,就是PC指针的寻址范围,因此也叫寻址空间。
大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,PC指针就是一个32位的二进制数,也就是0xffffffff,范围只有4G寻址空间。现在内存越来越大,4G根本不够,所以需要扩展,为了能访问超出4G范围的内存,就有了64位系统。STM32是多少位的?是32位的,因此PC指针也是32位,寻址空间也就是4G。
我们来看看STM32的寻址空间是怎么样的。在数据手册《STM32F407_数据手册.pdf》中有一个图,这个图,就是STM32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫Memory map,用一个新芯片,就先看这个图。
640 (25).png
  • 最左边,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等外设。
    ( D: d" K, Y0 b9 ^" E* b& O
    2 j0 O' i# h4 S/ p9 @  S
好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。在IO口输出的例程中,我们配置IO口是调用库函数,我们看看库函数是怎么做的。
6 `( ~; s/ J: H
例如:
  1. GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
复制代码
6 {6 Z( x$ U- P6 L% {# \$ k
这个函数其实就是对一个变量赋值,对GPIOx这个结构体的成员BSRRL赋值。
  1. void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)2 _6 q& Y8 y9 E: ]
  2. {8 z8 c/ Z( T: @( A$ K
  3. /* Check the parameters */
    & c3 }7 y4 A% k1 U8 C+ r. {
  4. assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
    " j2 U, j7 S, O( j# F' Y
  5. assert_param(IS_GPIO_PIN(GPIO_Pin));
    ; G) w$ v' @' w$ R/ i' ]: ?, S" M

  6. 6 M+ i; i' W! U& ]
  7. GPIOx->BSRRL = GPIO_Pin;
    ; W0 m5 i- M5 _; |# J7 h. |
  8. }
复制代码
4 }% `8 k/ u7 y& T& z# ]9 }
assert_param:这个是断言,用于判断输入参数是否符合要求GPIOx是一个输入参数,是一个GPIO_TypeDef结构体指针,所以,要用->获取其成员
GPIOx是我们传入的参数GPIOG,具体是啥?在stm32f4xx.h中有定义。
  1. #define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
复制代码
* x! k% [: d( Z/ u) D2 Y
GPIOG_BASE同样在文件中有定义,如下:
  1. #define GPIOG_BASE           (AHB1PERIPH_BASE + 0x1800)
复制代码
/ N9 W4 K7 O4 B; [
AHB1PERIPH_BASE,AHB1地址,有点眉目了吧?在进一步看看
  1. /*!< Peripheral memory map */+ C" v& W6 f+ q" n4 a+ m' R: R
  2. #define APB1PERIPH_BASE       PERIPH_BASE
    + S: G4 i7 y" x; a. p. K
  3. #define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)! \% u$ J2 @# T% V2 d" d; E
  4. #define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)2 J: x! k: _1 D
  5. #define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)
复制代码
' a9 r" P! B1 R. Q
再找找PERIPH_BASE的定义
  1. #define PERIPH_BASE           ((uint32_t)0x40000000)   
复制代码

/ p& I# r: G8 ^
到这里,我们可以看出,操作IO口G,其实就是操作0X40000000+0X1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是SRAM空间地址,0X40000000+0X1800是外设空间地址。
  1. u32 i;
    & r7 H  Q9 d+ Y. Q" V6 {5 ?0 z
  2. i = 0x55aa55aa;
复制代码
' [9 m( m5 l- h; k% K
这个外设空间地址的寄存器是IO口硬件的一部分。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0X40000000+0X1800+0x14.
640 (24).png
控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。
寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或RAM。寄存器为什么能控制硬件外设呢?因为,初略的说,一个寄存器的一个BIT,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。
' Z" r4 J9 T5 d3 d; v& u* v5 j
纯软件-包罗万象的小程序
我们已经完成了串口和IO口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?代码到底放在那里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。

3 _* X3 n! A: Y3 F  k+ ]% v
分析启动代码
  • 函数从哪里开始运行?
    2 a  O+ t5 E7 f  X% j0 Z, z
    & o0 V0 \( L1 [9 Z' e
每个芯片都有复位功能,复位后,芯片的PC指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,PC可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在STM32中,复位到0X08000004。因此复位后运行的第一条代码就是0X08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。
芯片复位时,芯片从中断表中将Reset_Handler这个值(函数指针)加载到PC指针,芯片就会执行Reset_Handler函数了。(一个函数入口就是一个指针)
  1. ; Vector Table Mapped to Address 0 at Reset
    % W5 Y1 B) |5 G  f- e$ q* w( Z
  2.                 AREA    RESET, DATA, READONLY
    $ s/ |. Y: q& z7 p/ _
  3.                 EXPORT  __Vectors
    7 N/ v/ k* I$ |2 Z( }- P5 V
  4.                 EXPORT  __Vectors_End
    % q' W& Q. H6 ?, I3 {
  5.                 EXPORT  __Vectors_Size+ K  G; P! Q8 E( e
  6. / R  A* Z  _' O8 F
  7. __Vectors       DCD     __initial_sp               ; Top of Stack2 }, P% E$ \2 G% W
  8.                 DCD     Reset_Handler              ; Reset Handler4 i' v4 g" r- K' {5 w
  9.                 DCD     NMI_Handler                ; NMI Handler" \3 d( o! J& T
  10.                 DCD     HardFault_Handler          ; Hard Fault Handler" j; R3 n* L, r6 l. C  a: _
  11.                 DCD     MemManage_Handler          ; MPU Fault Handler- A( o; }) P; Y9 U( f, P2 c
  12.                 DCD     BusFault_Handler           ; Bus Fault Handler2 @3 N" k4 y  i2 [' I
  13.                 DCD     UsageFault_Handler         ; Usage Fault Handler
复制代码

/ I8 v2 `6 G& B
Reset_Handler函数,先执行SystemInit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?
是我们在main.c中定义的main函数吗?后面我们再说这个问题。
2VJM~PT_%[{HWF97~ABT(RE.png
芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujique\prj\Objects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。
在MDK软件Options菜单Linker下有关于这个菜单的设置。

. y# U' J% z) b; P+ z5 A8 M# L
640 (23).png

; W& ^' U% p& e1 _6 n
把Use Memory Layout from Target Dialog前面的勾去掉,之前不可设置的框都可以设置了。点击Edit进行编辑。
640 (22).png
在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。
其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定FLASH跟RAM的大小于起始位置,当我们把程序分成BOOT、CORE、APP,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到RAM中运行。
640 (21).png
从这个基本的分散加载文件我们可以看出:
  • 第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,也就是变量,这一行指定了变量保存到什么地址。
    & X0 ]" r5 Z2 o* W* V4 X# `* M4 o7 |
    ' w# |$ Y  w* o4 [# f# ]
分析用户代码
到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。
1 程序跳转到main函数后:RCC_GetClocksFreq获取RCC时钟频率;SysTick_Config配置SysTick,在这里打开了SysTick中断,10毫秒一次。Delay(5);延时50毫秒。
  1. int main(void)
    ! l& A( G$ H8 T
  2. {9 p# \; |" o& S$ l( G
  3.   GPIO_InitTypeDef GPIO_InitStructure;
    # ~8 v% V$ ^' R1 ?! t% y
  4. + i/ o& e7 o$ e( S" h1 H
  5. /*!< At this stage the microcontroller clock setting is already configured,3 j( M1 E# b* c- S3 d5 `
  6.        this is done through SystemInit() function which is called from startup
    ( o  _& P& }0 K$ I) l' e, P
  7.        files before to branch to application main.5 G8 v/ K# u7 R' F- }3 v
  8.        To reconfigure the default setting of SystemInit() function,# ]3 G. l- f2 N' O2 H' ~
  9.        refer to system_stm32f4xx.c file */! S& ]7 i4 U. i* g7 d
  10. + q/ Y5 e/ \  M/ X
  11.   /* SysTick end of count event each 10ms */
    + `- ]0 T) @3 m/ n. V1 n  [
  12.   RCC_GetClocksFreq(&RCC_Clocks);8 G3 p! f" O% a& H9 ^
  13.   SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);
    5 k4 a0 M8 ?# v

  14. ' i: d$ ?. p+ C- e
  15.   /* Add your application code here */
    - \9 O8 b: p, _; v1 S8 O
  16.   /* Insert 50 ms delay */
    ; B+ f8 S3 p+ y0 H( Z3 F
  17.   Delay(5);
复制代码
8 h' X1 v, O7 i& y( k/ ?7 w
2 初始化IO就不说了,进入while(1),也就是一个死循环,嵌入式程序,都是一个死循环,否则就跑飞了。
  1. /*初始化LED IO口*/
    . I& Y" g* A- I( o
  2. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);  }4 N) M. ]5 o
  3. $ E+ J. o# b/ F  _. j9 S$ U9 K
  4. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;
    1 ^0 f3 ?5 B/ y# K
  5. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    # _! K+ `5 n) p. j3 ]

  6. ( }1 J: Q! F3 h; y4 J' N, S
  7. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    , {- d- Y, |/ @3 p1 b
  8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;3 }, \# I- j% ^! }' F) o7 F
  9. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;2 J7 U! y$ S. t9 k
  10. GPIO_Init(GPIOG, &GPIO_InitStructure);    $ i6 Y2 B: E! `0 q4 q+ W+ \

  11. 7 o3 D7 x/ U9 F' C# v1 x& e! C. o
  12. /* Infinite loop */
    , p) F7 E" l9 S  `- l# ~- @5 Q
  13. mcu_uart_open(3);
    ( T5 |3 L( ]. B% J% }) ^# L. E
  14. while (1)
    9 B& f$ O; q0 ^5 r* j" l* q8 J* ]
  15. {
    1 C4 |# ~7 w/ L. Q* Z$ i
  16.   GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);# W6 N" [% k/ W% [4 U8 e  O" R3 q' n
  17.   Delay(100);0 i6 h. Y' m" J* B6 b
  18.   GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);7 N/ D) _) ~$ y  U' A) G: R7 X" f
  19.   Delay(100);, ]/ J, \- h  j' X4 F% v+ J  M
  20.   mcu_uart_test();2 I/ O8 B. q1 _/ T
  21. , C: P* a. W; d% T. D& D
  22.   TestFun(TestTmp2);
    " E+ W+ `: o* |7 L( \
  23. }
复制代码
* I; S# {1 U9 |; ?1 V# S) F
3 在while(1)中调用TestFun函数,这个函数使用两个全局变量,两个局部变量。
  1. /* Private functions ---------------------------------------------------------*/
    / G) G, F) I4 l! M) M; H) X
  2. u32 TestTmp1 = 5;//全局变量,初始化为5  [: o! ]' \* @. N( p. ]1 X
  3. u32 TestTmp2;//全局变量,未初始化+ A8 [. [6 l1 Y+ q4 P3 o

  4. + f$ B9 U; c# K/ w1 V
  5. const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};8 }1 `: {1 J" V% @5 W

  6. 0 ?3 a# \9 f; P0 g
  7. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值& L8 m% O& C& |; x) d1 ~5 N
  8. {6 p9 I5 \: G. t7 g: D4 }
  9. u8 test_tmp1 = 4;//局部变量,初始化
    4 W* A, J2 t9 q3 h
  10. u8 test_tmp2;//局部变量,未初始化
    . ]0 T- |: ^/ \! ^& j

  11. * T* Q3 b7 U  Y1 X  O, H, b
  12. static u8 test_tmp3 = 0;//静态局部变量
    % A$ N  Z/ z# c4 y( k3 m  l

  13. : t  K1 b3 L4 i7 A0 ]9 K& U- v
  14. test_tmp3++;2 d* r4 j$ r8 L' u3 p

  15. & q2 T# c, ^) B$ [
  16. test_tmp2 = x;
    / A# @3 _' [" {: Q& o8 h
  17. - v9 U! y8 X, Z# v* B/ a9 h
  18. if(test_tmp2> TestTmp1)" j. c7 j4 T1 o! D4 J7 x) K
  19.   test_tmp1 = 10;* `5 l/ g+ G: Z
  20. else4 @; g) _; |% P- W8 M+ O
  21.   test_tmp1 = 5;
    $ b- ^2 \% p; w& |/ s$ b
  22. 2 r5 O+ i9 ^. p, V$ _
  23. TestTmp2 +=TestTmp3[test_tmp1];
    . @" p7 w# n# S! v$ \: H$ g: S

  24. 3 L, |( T1 E; h
  25. return test_tmp1;
    , m) `' f4 W0 K( a9 L* P
  26. }
复制代码
. }5 s* O* X2 R
然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。我们查看Delay函数,uwTimingDelay不等于0就死等?谁会将uwTimingDelay改为0?
  1. /**3 L' }) I1 q# l% {8 u& `1 N$ P. e# c+ E
  2.   * @brief  Inserts a delay time.4 w: E2 D2 W' X( a
  3.   * @param  nTime: specifies the delay time length, in milliseconds." ~+ B7 [9 B% E- M+ O
  4.   * @retval None
    1 S4 t, N! T# F+ F! r7 v! ^. c5 \" }
  5.   */0 `7 z1 K: U# Y
  6. void Delay(__IO uint32_t nTime)
    % y) x: R' p4 v5 c3 e+ O
  7. {
    2 o2 C/ I/ I0 f1 O" J  o0 r& y
  8.   uwTimingDelay = nTime;2 d- O1 b; O$ O  r; h( @/ x- U

  9. * _6 X0 E" T1 N; _2 s1 r
  10.   while(uwTimingDelay != 0);5 x3 |, q+ Z7 f: a; Z
  11. }
复制代码
! e, F6 n- _, p! @
搜索uwTimingDelay变量,函数TimingDelay_Decrement会将变量一直减到0。
  1. /**
    , x, L- ~4 h/ V* W0 g) L
  2.   * @brief  Decrements the TimingDelay variable.
    $ g. a/ @4 Q  X9 n/ O9 @5 w
  3.   * @param  None. l) k# f5 l8 D+ z2 B5 g" s
  4.   * @retval None
    ! y1 `% n5 B2 z
  5.   */
    ; `( t8 v0 _. @
  6. void TimingDelay_Decrement(void)
    ' |8 e: C: P: f& j9 K
  7. {' K/ F9 C; E' o$ }, t
  8.   if (uwTimingDelay != 0x00)0 ?& N3 E/ i4 U6 p. W1 R6 p
  9.   {# ?7 l) J5 m: _+ G: ]4 Z) [7 k
  10.     uwTimingDelay--;  e  L% X1 r" Q! L- m. W  D
  11.   }2 }+ f' ~( j% R8 p% [. ~
  12. }
复制代码

% k! A  w5 C: i
这个函数在哪里执行?经查找,在SysTick_Handler函数中运行。谁用这个函数?
  1. /**
    6 c4 U0 t4 v, [4 x- }3 l8 ]0 I
  2.   * @brief  This function handles SysTick Handler.: T& k, q8 f2 K% D( @+ U
  3.   * @param  None
    7 Q& R" h  ~; w" w$ N0 M
  4.   * @retval None
    & g' ~8 S8 r8 W( Q' m* E
  5.   */
    4 ^) ]/ y* ~6 K. D
  6. void SysTick_Handler(void)
    ' s+ j8 B3 x2 {9 C% S
  7. {4 j( I1 u. v9 e2 F
  8.   TimingDelay_Decrement();
    " ?8 N0 }7 d( a; J" z+ U
  9. }
复制代码
6 I) U  r$ [! Y: B' g8 E0 d. L1 z
经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。
  1. __Vectors       DCD     __initial_sp               ; Top of Stack9 ~7 j; o! ]% p) s" w  [% n
  2.                 DCD     Reset_Handler              ; Reset Handler
      m7 X6 I- k' q5 W; S. r
  3.                 DCD     NMI_Handler                ; NMI Handler
    4 e5 a/ ]' s, G- C* z
  4.                 DCD     HardFault_Handler          ; Hard Fault Handler
    $ m9 E, `% k, k: h
  5.                 DCD     MemManage_Handler          ; MPU Fault Handler. j0 s* m# I$ p4 s, F
  6.                 DCD     BusFault_Handler           ; Bus Fault Handler9 `/ k# O# c& u* x
  7.                 DCD     UsageFault_Handler         ; Usage Fault Handler! r, f6 g% A3 D9 `
  8.                 DCD     0                          ; Reserved
    % d0 t6 N/ O6 g. S/ Q
  9.                 DCD     0                          ; Reserved
    : P( @4 y. H2 {1 _/ d% B) z
  10.                 DCD     0                          ; Reserved
    ; A# f( J" L) `) J6 u0 d& S
  11.                 DCD     0                          ; Reserved
    0 w) c2 J' v4 w( C* O& y
  12.                 DCD     SVC_Handler                ; SVCall Handler
    ) w, v  d+ c4 u1 o" m+ i. N
  13.                 DCD     DebugMon_Handler           ; Debug Monitor Handler
    0 A- O6 V# v$ _9 j, H* |
  14.                 DCD     0                          ; Reserved. ?# S- n2 j  B$ I# r! w. O; E
  15.                 DCD     PendSV_Handler             ; PendSV Handler
      r9 N1 P( Z: u6 t+ X
  16.                 DCD     SysTick_Handler            ; SysTick Handler
复制代码
% T/ m! y" t6 Z% l" \9 ?5 w: W

. N8 b4 Q5 P, r' Z; w余下问题
1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(InRoot$$Sections)是什么?3 ZI段,也就是初始化为0的数据段,什么时候初始化?谁初始化?
为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!
# l6 J' F% P  s" V) d' h
通过MAP文件了解代码构成
/ _. G" B9 X  M) w2 ]6 N编译结果

程序编译后,在下方的Build Output窗口会输出信息:

  1. *** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'
    ( Q) K8 J9 q: v" W1 p: v* ]! e9 T% q
  2. Build target 'wujique'! v5 M% {# o" W3 u; |: e
  3. compiling stm32f4xx_it.c...
    3 G0 U: x6 K0 ?' q8 L7 r6 K
  4. ...
    4 B+ T) j8 H( N9 s0 W% @
  5. assembling startup_stm32f40_41xxx.s...
    1 R0 v4 X4 M; w  {1 M
  6. compiling misc.c...
    / Q- ]8 p9 Y2 a9 t8 l+ L' {4 g
  7. ...
    . Y$ J) A9 j4 A( [6 p6 {/ v
  8. compiling mcu_uart.c...- n9 z6 f% o" ^3 v( d- A
  9. linking...
    2 `( u. _- ?4 [6 G
  10. Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  
    1 ^: n4 H$ L7 L1 ?3 T8 R( Q
  11. FromELF: creating hex file...7 K- b  a5 x" N0 Q3 V! G
  12. ".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s).
    - a$ i  v) V- b9 r& I
  13. Build Time Elapsed:  00:00:32
复制代码


- m. m0 T% ]3 \

  • 编译目标是wujique
  • C文件compiling,汇编文件assembling,这个过程叫编译
  • 编译结束后,就进行link,链接。
  • 最后得到一个编译结果,9038字节code,RO 990,RW 40,ZI 6000。CODE,是代码,很好理解,那RO、RW、ZI都是什么?
  • FromELF,创建hex文件,FromELF是一个好工具,需要自己添加到option中才能用2 }% z$ C2 o( c5 K/ p
    7 i9 v1 a$ S. S1 H2 z- Z
map文件配置

更多编译具体信息在map文件中,在MDK Options中我们可以看到,所有信息都放在\Listings\wujique.map

默认很多编译信息可能没钩,钩上所有信息会增加编译时间。

640 (20).png
map文件
打开map文件,好乱?习惯就好。我们抓重点就行了。
640 (19).png
  • map 总信息

    ( k) Q4 u9 T2 t) A% _# T
    " g0 E6 y9 z$ b3 {& M
从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。
有多少RO?RO到底是什么?
有多少RW?RW又是什么?
ROM为什么不包括ZI Data?为什么包含RW Data?
640 (18).png
  • Image component sizes

    . X7 y. Q# z* k7 u* B: ]$ [

    . [3 r( c1 Z/ {4 w. M/ z5 {
往上,看看Image component sizes,这个就比刚刚的总体统计更细了。
这部分内容,说明了每个源文件的概况
首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些STM32的库文件。
640 (17).png
第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1M flash上的程序压缩到能在512K上运行。
640 (16).png
第3部分也是库,暂时没去分析这两个是什么东西。
640 (15).png
库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:
#include <stdarg.h>. R* V' S) j( C0 M
#include <stdlib.h>2 E' a, j+ t" J. K2 Y0 w( v
#include <string.h>  
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。
  • 文件map

    ( j: y2 J7 V' T& H2 A

    / `" U& o* v# H/ {" n
再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET

库文件是什么?

库文件就是别人已经别写好的代码库。
在代码中,我们经常会包含一些头文件,例如:
  1. #include <stdarg.h>% Y- m/ \& F" l9 ~0 j
  2. #include <stdlib.h>
    ( O, r) A. \0 a' }2 L+ A
  3. #include <string.h>  
复制代码

% [) P% s3 L9 B  [* P/ O; w- J
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。
我们经常用的库函数有:memcpy、memcmp、strcmp等。
只要代码中包含了这些函数,就会链接库文件。
  • 文件map
    ( Y$ f1 Q' i- t( j$ o

    % h( e; P# b$ x- W7 S2 _* f, u
再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET
640 (14).png
每个文件有有多行,例如串口,4个函数。
640 (13).png
然后是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?
640 (12).png
  • Image Symbol Table
    ! X: T1 c# d% [7 \# B- i7 C! i

    + w( X# _; l7 x; Y5 s% r8 G  p! q
再往上就是Image Symbol Table,就更进一步到每个函数或者变量的信息了
640 (11).png
例如,全局变量TestTmp1,是Data,4字节,分配的位置是0x20000004。
640 (10).png
TestTmp3数组放在哪里?放在0X080024E0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几K几十K大,不可能也没必要放到RAM区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。
const的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习
640 (9).png
那局部变量存放在哪里呢?我们找到了test_tmp3,
640 (8).png
没找到test_tmp1/test_tmp2,为什么呢?在定义时,test_tmp3增加了static定义,意思就是静态局部变量,功能上,相当于全局变量,定义在函数内,限制了这个全局变量只能在这个函数内使用。哪test_tmp1、test_tmp2放在哪里呢? 局部变量,在编译链接时,并没有分配空间,只有在运行时,才从栈分配空间。
  1. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
      e. ~  |" h9 w
  2. {
    2 K/ Q$ L1 W6 k4 N  a0 H% }
  3. u8 test_tmp1 = 4;//局部变量,初始化8 ~- J0 k) h7 v: O  M! S
  4. u8 test_tmp2;//局部变量,未初始化
    3 @) v3 Q- `7 ]- H# S
  5. 7 I) L. I9 z- i$ i0 u, A
  6. static u8 test_tmp3 = 0;//静态局部变量
复制代码

7 d" Z* t& B- x! s
上一部分,我们留了一个问题,哪些变量是RW,哪些是ZI?我们看看串口变量的情况,UartBuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文Block Started by Symbol的简称。
640 (7).png
到这里,我们可解释下面几个概念了:
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区。

* s' C+ d' o2 `, r% c
640 (6).png
我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579
640 (5).png
main是main,放在0x08000189
640 (4).png
__main到main之间发生了什么?还记得分散加载文件中的这句吗?
  • ( y! _3 E0 f9 `2 p. |9 Z+ o$ I0 n/ A

    9 g& ]$ u+ |2 }9 F8 p1 ]# o
*(InRoot$$Sections)
__main就在这个段内。下图是__main的地址,在0x08000189。__Vectors就是中断向量,放在最开始。
640 (3).png
在分散加载文件中,紧跟RESET的就是*(InRoot$$Sections)。
640 (2).png
而且,RESET段正好大小0x00000188。
640 (1).png
巧合?可以参考PPT文档《ARM嵌入式软件开发.ppt》。
640.png
这一段代码都完成什么功能呢?主要完成ZI代码的初始化,也就是将一部分RAM初始化为0。其他环境初始化……
; `" a8 X* D( H. O
最后
到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。

" \' V8 c, N9 h: u  O% L: _4 C& |- T0 j- d  Z2 M) n2 ?. g+ ?
赞 收藏 评论0 发布时间:2022-6-2 14:15

举报

0个回答

所属标签

相似技术帖

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版