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

【经验分享】STM32是如何软硬件结合,编译后怎么样一步步运行起来的

[复制链接]
STMCU小助手 发布时间:2022-5-27 21:34
不知道大家有没有疑惑,为什么软件能控制硬件?本文分析STM32单片机到底是如何软硬件结合的,分析单片机程序如何编译,运行。
; C/ L) ?* a. Q+ m; u软硬件结合( c" Q: g! t& F$ `5 \0 g. |
& f' O. W. O: B2 O

6 R- a& C0 R3 }2 ^: U* G, W初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写P1=0X55,就可以在IO口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。
! I4 Z. Z  w% I
7 j! z2 G# S$ l7 R; Z  `+ l

' J; k8 r( R% N" O: E1 F寻址空间
7 u% Y  i8 U3 H1 _) n! n( h; x7 [0 [  ~6 v$ |! U9 q% ]; W' b
什么是地址空间呢?所谓的地址空间,就是PC指针的寻址范围,因此也叫寻址空间。
大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,PC指针就是一个32位的二进制数,也就是0xffffffff,范围只有4G寻址空间。现在内存越来越大,4G根本不够,所以需要扩展,为了能访问超出4G范围的内存,就有了64位系统。STM32是多少位的?是32位的,因此PC指针也是32位,寻址空间也就是4G。
我们来看看STM32的寻址空间是怎么样的。在数据手册《STM32F407_数据手册.pdf》中有一个图,这个图,就是STM32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫Memory map,用一个新芯片,就先看这个图。
% Y! L4 `  |7 [4 C) [0 z  {; S
/ a+ ?9 w- a7 X( |1 H) [' j( a

! z0 g) S* x9 R8 }0 w7 _6 ~, y 640 (25).png ! m( f3 O# M* b/ a9 `* {
. A! E3 r) D: L
  • 最左边,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等外设。
      r& S" X& @+ W# A. M

    ( ^- a6 r3 h4 s. y% ]5 X
好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。在IO口输出的例程中,我们配置IO口是调用库函数,我们看看库函数是怎么做的。, T, T% L0 l! G" F1 P; L
' p# X1 ]1 S' t. G2 C9 ^) o
例如:
( ^* z2 A; a5 C0 N
  1. GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
复制代码

6 u: ]5 K5 L6 ?) A$ z这个函数其实就是对一个变量赋值,对GPIOx这个结构体的成员BSRRL赋值。
  1. void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
    9 l4 s5 H! _3 y  k! J' R! V
  2. {
    ' [* q$ u& f2 P$ O+ Q& @
  3. /* Check the parameters */; q/ G4 @3 c4 ~7 K3 H2 ~
  4. assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
    1 W1 q- U/ R" Z6 o9 W" ]
  5. assert_param(IS_GPIO_PIN(GPIO_Pin));
    1 m: y9 j8 Q  x8 b, H! J  |- j

  6. " j% N  l' c5 `$ Z
  7. GPIOx->BSRRL = GPIO_Pin;: c! B# {4 ]. R) w
  8. }
复制代码

: M5 V2 c/ _1 j# D
assert_param:这个是断言,用于判断输入参数是否符合要求GPIOx是一个输入参数,是一个GPIO_TypeDef结构体指针,所以,要用->获取其成员

1 N* w5 f7 t( j0 V: X
GPIOx是我们传入的参数GPIOG,具体是啥?在stm32f4xx.h中有定义
: |5 \: |9 e) u4 E& ^3 C9 A
  1. #define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
复制代码
: t) P: V/ W, v- J5 s6 g
GPIOG_BASE同样在文件中有定义,如下:
( ?# _& s; i$ ]& q1 A1 t0 s8 H
  1. #define GPIOG_BASE           (AHB1PERIPH_BASE + 0x1800)
复制代码
% U; A& ]) I' k2 Y) G4 Q4 ]
AHB1PERIPH_BASE,AHB1地址,有点眉目了吧?在进一步看看
  1. /*!< Peripheral memory map */
      K5 ^2 `. |- I( I6 M" A
  2. #define APB1PERIPH_BASE       PERIPH_BASE
    2 m" x7 a! W0 K2 ~
  3. #define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)
    2 v: e' D2 F5 R
  4. #define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)
    ! }# k! u& k, D$ [3 c! L& S
  5. #define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)
复制代码

9 ]3 A: j$ i3 z7 w: v

- Q9 \. `6 R/ p: f# i再找找PERIPH_BASE的定义
  1. #define PERIPH_BASE           ((uint32_t)0x40000000)   
复制代码

7 {& T5 y1 G' f5 p# e% `' r7 U& ^* u9 P2 _! e) _
6 Y$ o) t9 R9 |. v8 t) N
到这里,我们可以看出,操作IO口G,其实就是操作0X40000000+0X1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是SRAM空间地址,0X40000000+0X1800是外设空间地址。. a* F+ W" m+ ]9 r
  1. u32 i;% h2 C$ \& y3 q- P, L
  2. i = 0x55aa55aa;
复制代码

( I9 g: X* b* a, i8 w
7 E. r, B. `) H7 d* V& n
" s1 N  j+ E$ `% b; J
这个外设空间地址的寄存器是IO口硬件的一部分。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0X40000000+0X1800+0x14./ ]% K8 |! p9 I3 ?6 D

: A6 q! v( V: ]1 G  G

" I: l4 [4 ^6 U, ?8 q2 Y 640 (24).png
! c$ R$ L, Z1 `4 F
5 G- F" V$ _( M9 \& H控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。
寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或RAM。寄存器为什么能控制硬件外设呢?因为,初略的说,一个寄存器的一个BIT,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。
8 T0 L% I7 X) p4 e( ?4 ^+ I
纯软件-包罗万象的小程序
! M! b- A- a% l- y3 Y' E- v/ c# S  U1 O! C9 _
& b* T- m# `: Z& `7 ^
我们已经完成了串口和IO口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?代码到底放在那里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。2 w# |$ E  F/ F  R$ ~) S
" R) D6 e7 \* q# S& r/ B
7 G6 x1 {% x; d$ J1 B+ m5 Q
分析启动代码
  • 函数从哪里开始运行?

    5 A0 g9 e- q! }3 a0 u& v
每个芯片都有复位功能,复位后,芯片的PC指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,PC可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在STM32中,复位到0X08000004。因此复位后运行的第一条代码就是0X08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。) T1 @6 U6 y( y! B' @
芯片复位时,芯片从中断表中将Reset_Handler这个值(函数指针)加载到PC指针,芯片就会执行Reset_Handler函数了。(一个函数入口就是一个指针), j6 @/ i- o4 j* F3 V- R8 o
  1. ; Vector Table Mapped to Address 0 at Reset: O7 z4 a  F" f( ?; j# }
  2.                 AREA    RESET, DATA, READONLY1 R: d) K# j3 E9 H" I# E
  3.                 EXPORT  __Vectors4 V$ T, u+ f& B# {& S
  4.                 EXPORT  __Vectors_End
    % F9 ^7 }! [" T" X
  5.                 EXPORT  __Vectors_Size
    ; z3 e6 K% n+ _+ W8 @4 Y. [
  6. ! ]) `5 k# L  r, z
  7. __Vectors       DCD     __initial_sp               ; Top of Stack
    , W2 M; `# K8 u' z; ?: \
  8.                 DCD     Reset_Handler              ; Reset Handler3 z! [' i; U* t7 }1 p/ U$ z
  9.                 DCD     NMI_Handler                ; NMI Handler3 z: b# ?: Y8 T
  10.                 DCD     HardFault_Handler          ; Hard Fault Handler
    # O! l+ d0 X9 b; n
  11.                 DCD     MemManage_Handler          ; MPU Fault Handler
    - w/ c7 A. L+ z5 E( Q2 U
  12.                 DCD     BusFault_Handler           ; Bus Fault Handler
    6 n4 o! j9 a4 z& Q) V8 K+ s; V
  13.                 DCD     UsageFault_Handler         ; Usage Fault Handler
复制代码

( H' q2 W8 a0 \4 i) R- b7 W
4 d' u( [1 r1 h! @+ \' |0 O  W6 U

2 y# ]8 L( z/ G# ~1 \- o# MReset_Handler函数,先执行SystemInit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?
! ]% O# {! H7 u$ E( b4 ^; w7 R
/ l4 Y4 ~3 v2 y1 V' w* p
8 x. {& v% ]$ C% f
是我们在main.c中定义的main函数吗?后面我们再说这个问题。  j; B9 `6 \- @3 q1 E/ ?4 m0 t8 _; K
" c+ `( j9 C: v/ F9 ?' K7 V
2 c, R8 i  [+ N; A* O! C, t
`K9@]PZ]{27KB)L05F~Z(QV.png
9 L: t. V) j! z3 X9 b6 `+ w  f1 G/ n3 {
芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujique\prj\Objects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。
  H9 {6 I1 Q7 l- @" {/ h; ^2 U1 q0 A4 v# w1 ?8 s
1 O4 L6 p# [, J) g; ]
在MDK软件Options菜单Linker下有关于这个菜单的设置。
0 ^& U( E% a/ Y) _
6 v  ?6 ]7 m3 x& l& H/ \- Z

/ z$ s! B. Q0 S7 i8 n$ o( o 640 (23).png 0 J" p! [, I. @5 k8 [; L1 C

* \6 L( E2 H% ]把Use Memory Layout from Target Dialog前面的勾去掉,之前不可设置的框都可以设置了。点击Edit进行编辑。
& `/ Q2 ^' G! T8 x6 [) e, y  }; |; M+ ~& D9 d! D

8 I* O8 v# S( M3 u) u- h 640 (22).png . ?/ D: l: G. D, R, g( d
4 h4 D! t! h1 f) S. Y" s
在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。
其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定FLASH跟RAM的大小于起始位置,当我们把程序分成BOOT、CORE、APP,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到RAM中运行。
6 o' x4 A  R8 ?- n" B; x
640 (21).png ! P$ i: F% ?  F# Q9 }# V

3 F! `  G; B. Z
, D/ n  e( j  s; x* r. k, e
从这个基本的分散加载文件我们可以看出:
  • 第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,也就是变量,这一行指定了变量保存到什么地址。
      ?0 _' }/ w' B1 ]1 J
    4 W5 L. D- d9 f

    # [- }! ?  J. H2 v5 R3 g
分析用户代码1 Z* Z7 K2 H: @) o* c+ U. l

3 m5 B, \# _' u# r9 z; |

% t% X/ p8 u5 {$ y到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。1 程序跳转到main函数后:RCC_GetClocksFreq获取RCC时钟频率;SysTick_Config配置SysTick,在这里打开了SysTick中断,10毫秒一次。Delay(5);延时50毫秒。
7 T7 G/ @+ L' C9 a; E
  1. int main(void)% D5 w; ?0 ?5 g$ x# E9 b/ {! T0 X6 M! x
  2. {  s6 u8 C' b- g4 d
  3.   GPIO_InitTypeDef GPIO_InitStructure;4 t. A# Y; ]9 h4 f. Q

  4. ) w% Q% o8 F$ S+ X, X' R3 f
  5. /*!< At this stage the microcontroller clock setting is already configured,5 m$ V4 X& z8 x5 C: t3 N
  6.        this is done through SystemInit() function which is called from startup+ h1 `; N! j7 m4 X6 T2 h& b
  7.        files before to branch to application main., C0 s/ F9 v% F
  8.        To reconfigure the default setting of SystemInit() function,
    , B: t2 K* w5 j2 n
  9.        refer to system_stm32f4xx.c file */" D* R3 L& U; H* d6 j1 x6 i: ^

  10. ) A- \+ ~1 x5 i% i/ P) |) v
  11.   /* SysTick end of count event each 10ms */% A8 b8 \& D+ u2 |
  12.   RCC_GetClocksFreq(&RCC_Clocks);' P! [8 p( K1 V1 c1 C
  13.   SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);. O& x" [: j! L5 O6 D* H0 R

  14. 4 _- }: m/ u% |8 ?8 x
  15.   /* Add your application code here */
    9 |8 ]! Q* K2 p3 C" h3 F
  16.   /* Insert 50 ms delay */( d0 N0 c" l) L
  17.   Delay(5);
复制代码

. Z* |, y3 \9 j. j3 G" Z5 B
, \1 ^+ v* Z- L/ z5 C) i0 N

2 j* Y6 t9 D# f; \5 u) l2 初始化IO就不说了,进入while(1),也就是一个死循环,嵌入式程序,都是一个死循环,否则就跑飞了。
9 J8 g  k% ~6 q
  1. /*初始化LED IO口*/
    2 r. Q5 F6 ^; e  z5 y0 H
  2. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);
    % `9 ^9 z5 \2 j0 E9 S5 T. Q! z4 U

  3. ' r  q/ d$ _  }) I4 \7 p/ q
  4. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;! P2 n7 J) ^9 y5 R0 {+ M0 O  u+ G) t
  5. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;& r4 w. u" f, e3 f7 O9 ]) c+ S7 w
  6. 2 Z- v1 D% m. u, i/ e
  7. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;* E% d. {6 k. s) h4 g- u; j
  8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;2 M. }# [7 K) H+ y
  9. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    ( Y& s; A$ d* S3 X; k8 z
  10. GPIO_Init(GPIOG, &GPIO_InitStructure);   
    % L' O/ i& x1 y

  11. ( x+ V, [0 \  x- ?4 \9 R
  12. /* Infinite loop */2 x) P3 Q6 A  i0 F# z  B
  13. mcu_uart_open(3);
    ' E+ ~. t- E' D0 N
  14. while (1)) Z+ G8 N% {& O
  15. {
    . S  C( P& J. S* j( E
  16.   GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
    6 j: l/ ~( ~/ x3 Z! Y5 |
  17.   Delay(100);0 `: P. O4 O+ [4 h' q8 d7 ?
  18.   GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
    " R0 I2 c, N/ d& q# N: l
  19.   Delay(100);" Y. p3 L+ Y' @& m- r& e5 I
  20.   mcu_uart_test();6 v( `' m* ~7 ~# t  F4 [
  21. 2 h- g. s* Y1 w( i# O% n& z
  22.   TestFun(TestTmp2);" n' C5 I0 d  O9 P+ X  w9 M7 S% Z7 Y5 I
  23. }
复制代码

2 U0 N7 g3 l* n+ Y; @9 j: |  f- z$ ^# R. }' B1 C
% d, I+ u6 B* \/ h; J
3 在while(1)中调用TestFun函数,这个函数使用两个全局变量,两个局部变量。
7 R, T" A. i: A. D3 r* [  s
  1. /* Private functions ---------------------------------------------------------*/8 ^3 W& {* o5 D& b1 B% ]
  2. u32 TestTmp1 = 5;//全局变量,初始化为5* _' p* C; @" o" O2 a! Q
  3. u32 TestTmp2;//全局变量,未初始化
    , p7 C3 E* ?. A9 z
  4. : D- _! S2 t2 F3 h8 i
  5. const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};
    3 G% g/ n$ j0 n" o/ _+ g+ i

  6. 0 d& G) g( C5 F% y' |& n
  7. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值! F3 T5 o* @+ V& t; ?; S
  8. {
    0 [8 J9 s8 a! r. V# j
  9. u8 test_tmp1 = 4;//局部变量,初始化
    * L2 X5 o9 w$ h9 J. H
  10. u8 test_tmp2;//局部变量,未初始化
    , @; B; \6 q& d6 B. ?# Q- W
  11. + ~# B& L5 U  `
  12. static u8 test_tmp3 = 0;//静态局部变量
    % X4 q5 K/ {6 }7 ]6 z& g

  13. $ t6 x7 h( V2 q1 ^. L/ L7 ^, i" y6 |
  14. test_tmp3++;
      \4 P: [9 G, q+ r

  15. 0 q! F6 e  E6 Q  A2 q' ^5 G2 R5 w9 [2 j
  16. test_tmp2 = x;: p' }. J  T0 p& N0 H% Y
  17. 5 v) t$ }- w/ ]) w9 l
  18. if(test_tmp2> TestTmp1)* t; u5 @) t8 Z% \! `9 y! R  z) N" ~
  19.   test_tmp1 = 10;
      N0 l, l( E; |. P% l
  20. else; T6 }  k+ S4 ?! M& W9 P$ S) S
  21.   test_tmp1 = 5;
    ; ~( d3 e8 V9 H6 h7 V

  22. , a1 }5 N) v/ h& }+ f3 v3 T
  23. TestTmp2 +=TestTmp3[test_tmp1];
    # ]2 x# z- m8 O, z+ y
  24. 4 k- E/ E) a! z7 a! A5 C: ?- h7 B
  25. return test_tmp1;' i. o1 b: W% u$ Y2 C& p% U, ]$ Y
  26. }
复制代码
4 Z/ a* S- V' ~7 K

- o$ L$ ~: l' ?4 E

7 ^2 e$ ]: z3 W3 Y然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。我们查看Delay函数,uwTimingDelay不等于0就死等?谁会将uwTimingDelay改为0?
: m+ P, ~$ v: B0 @  e

  1. - j* ^& d, h; Z" d
  2. /**( c0 j1 \5 B2 K, b( W) Z
  3.   * @brief  Inserts a delay time.
    4 e1 o8 T* U' E/ I% j
  4.   * @param  nTime: specifies the delay time length, in milliseconds.5 S/ J* t. C. o' V/ y; a
  5.   * @retval None5 r3 y# z: x; \% X+ O) M) y, p2 b' i8 S
  6.   */4 `: G2 ~4 @7 [% s, A( [. q5 o) }
  7. void Delay(__IO uint32_t nTime)
    4 l; s: y: Y# J! o
  8. {; J3 j* I1 V1 E$ y
  9.   uwTimingDelay = nTime;3 r4 Z; L- s  _# H6 h# m

  10. % D7 G0 b/ {( n$ y
  11.   while(uwTimingDelay != 0);
    9 {5 K+ f- e! \/ D; W9 y) v$ Z1 c  @
  12. }
复制代码
# l" d/ G) z6 s6 o

; b: t/ u: P. m( x; S搜索uwTimingDelay变量,函数TimingDelay_Decrement会将变量一直减到0。
6 ?" R3 r- F7 r% d
  1. /**
    " N7 p/ l( z0 O: {0 \( Y: P
  2.   * @brief  Decrements the TimingDelay variable.
    4 S. `1 l5 c% P
  3.   * @param  None
    8 u$ u# }% y  [% c8 D
  4.   * @retval None7 l* K7 x4 a: v3 S
  5.   */
    6 S; @3 ^$ r+ H8 ~. z
  6. void TimingDelay_Decrement(void)
    & f' ?8 M+ G, h. }* O) U
  7. {5 r# U1 \' D2 y& ?( F
  8.   if (uwTimingDelay != 0x00)
    % a. g/ Z3 o7 W. p0 `# C5 C3 f) ?( B+ o
  9.   {
    5 W$ K$ O  _8 g: F5 e  X
  10.     uwTimingDelay--;2 w4 G! L' e7 ]7 e: _/ \
  11.   }' R7 ^- D' O9 @
  12. }
复制代码

& v0 s, N& y: f& ~2 `9 ?) g9 _) N1 f9 B- Y6 ^  K; s; A# Q, D
# ~7 r5 v' N" `) j/ P: A
这个函数在哪里执行?经查找,在SysTick_Handler函数中运行。谁用这个函数?
, p  I0 B. z0 g! A0 U, B. G
  1. /**
    9 \* A% R. ?. H& R4 k$ m4 U/ W
  2.   * @brief  This function handles SysTick Handler.
    4 v+ _* ~: ?: C5 l( [
  3.   * @param  None
    + X8 y* n5 u3 |+ f
  4.   * @retval None1 t: _  q* Z0 K  [* K
  5.   */
    0 d6 d2 q! V- r4 r0 p: G# F1 Y
  6. void SysTick_Handler(void)# L: u. b1 D. @  z6 K) r
  7. {
    , Q2 L$ p8 {) V, A$ b5 `
  8.   TimingDelay_Decrement();. W( Q( f* e7 B0 n) `+ {
  9. }
复制代码

6 B' S7 a6 y3 L6 s6 N% I, N. b
' V) i4 R& ^7 a; c) O0 w" a; a4 X' N

, W! H) H, i. y, d7 x# X3 A3 y7 y经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。6 M2 }. I$ `* V7 y6 W& ^4 Q
  1. __Vectors       DCD     __initial_sp               ; Top of Stack
    6 ]$ n1 ?+ v' [. C4 X
  2.                 DCD     Reset_Handler              ; Reset Handler
    ! \4 n0 q) d7 v* A7 @
  3.                 DCD     NMI_Handler                ; NMI Handler. }  X: o1 K7 l( |
  4.                 DCD     HardFault_Handler          ; Hard Fault Handler
    0 D0 a6 `' o) U( f# w+ }
  5.                 DCD     MemManage_Handler          ; MPU Fault Handler3 H, w  f) d; x5 q) X3 r9 C
  6.                 DCD     BusFault_Handler           ; Bus Fault Handler, g+ z2 k" n% O, O5 u
  7.                 DCD     UsageFault_Handler         ; Usage Fault Handler
      `# u7 R+ P% D) F+ z
  8.                 DCD     0                          ; Reserved
    # B1 [+ n2 C& o
  9.                 DCD     0                          ; Reserved' \0 u0 y# P% Z2 W  E
  10.                 DCD     0                          ; Reserved7 p! @& f. g+ Y4 W+ _( v
  11.                 DCD     0                          ; Reserved. p4 V- C# U: @, Q4 r& Z+ x& t! R
  12.                 DCD     SVC_Handler                ; SVCall Handler
    3 L  i2 t3 {2 r3 k) ~& Q" Q" f; l
  13.                 DCD     DebugMon_Handler           ; Debug Monitor Handler
    ' z% I( H2 N1 E
  14.                 DCD     0                          ; Reserved
    5 P. w+ a% L* a% l, d0 r" C
  15.                 DCD     PendSV_Handler             ; PendSV Handler
    5 l" U: k; h( H+ f
  16.                 DCD     SysTick_Handler            ; SysTick Handler
复制代码
% A% n8 }2 V( o% @/ {3 E/ Z
1 |" g5 Z8 g# M

3 d+ C8 a- V" a+ S+ {. F+ s, k* }/ q
. z  |  h# G- K& N
余下问题
; r/ ]3 o! c0 }- V) ^# C3 ]6 d: [4 N$ y  P' A2 `6 Q

% c8 z5 C9 V  c0 i8 B' y  b" G. D1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(InRoot$$Sections)是什么?3 ZI段,也就是初始化为0的数据段,什么时候初始化?谁初始化?
; K: l7 V! ?5 D; u% T# a
7 d8 x% L8 f" ^0 K2 d, v* J

/ M( @9 r( O3 [/ x. Q! m为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!9 r* M% P: q8 D$ b
" o' o  \1 J, [, N) T
) S) v5 a' X- d  c' D# v( a
通过MAP文件了解代码构成
- K: X7 ]6 M. v, A8 Z. X% I  c" E

0 d& c5 ]/ y4 _% k5 R% _/ t3 m3 [编译结果  c0 o) u+ Q0 H0 i) E8 s
程序编译后,在下方的Build Output窗口会输出信息:- K5 ]5 r7 w( }1 s+ g0 e
  1. *** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'
    6 n7 ?6 e' |% O% n4 V) M7 c9 X9 @
  2. Build target 'wujique'+ X' [+ L0 B2 t1 L1 Y
  3. compiling stm32f4xx_it.c...
    8 w# x7 i$ V- x. p! j: B
  4. ...
    & i1 N6 J* O# i0 }6 h) h5 o/ d$ w
  5. assembling startup_stm32f40_41xxx.s...6 P/ @# J. M  C  }1 o
  6. compiling misc.c...8 X$ L; D) u+ G& j' |3 G
  7. ...! L. y, _+ x6 ~" e. O9 L
  8. compiling mcu_uart.c...* m# S! y* ~2 k# [
  9. linking...
    0 x: _# g1 j! v
  10. Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  
    5 i* c* c% D$ Z  O- ^' t8 f% e
  11. FromELF: creating hex file...
    9 T( Y( ?$ x" @3 n
  12. ".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s).( b$ t* n. S+ V  {, \+ D& F, U- Q
  13. Build Time Elapsed:  00:00:32
复制代码

3 P1 T# ~! H6 g1 V) e0 S" i2 x" ~; {0 ]. q8 V2 B1 p) i/ O/ D3 j8 c
1 d# q! i& K' {8 C6 D0 `
  • 编译目标是wujique
  • C文件compiling,汇编文件assembling,这个过程叫编译
  • 编译结束后,就进行link,链接。
  • 最后得到一个编译结果,9038字节code,RO 990,RW 40,ZI 6000。CODE,是代码,很好理解,那RO、RW、ZI都是什么?
  • FromELF,创建hex文件,FromELF是一个好工具,需要自己添加到option中才能用
    % p/ }4 |( l6 E0 j6 W+ O
    " \5 T6 Z: m! [/ Z: J
map文件配置' V" F" m8 e7 L" K& ^* e5 u, a5 J

6 T' |6 ]) }  X. T
  V9 q& k, t/ y  k
更多编译具体信息在map文件中,在MDK Options中我们可以看到,所有信息都放在\Listings\wujique.map
/ ~% K/ ?' j6 Q9 K1 i& r
9 D3 i1 I- j1 R
默认很多编译信息可能没钩,钩上所有信息会增加编译时间。

$ O; Y' @5 \4 A/ s; l+ y/ m
640 (20).png
: `# t& q& k/ |- N
7 m! ]: M* h5 e2 f$ R: @4 D: ]map文件% A+ q8 t* e7 W0 }7 Q" b9 G9 K
打开map文件,好乱?习惯就好。我们抓重点就行了。
. W9 m+ l6 x' T+ O+ d1 Z* f4 l( k( B. P. }" ~& ?
% g; l$ E0 Z% a: N/ N$ U
640 (19).png * H& Q% H9 T1 M) u9 F% t. T* u( N

. a  u. H6 m& K2 p5 R% ~$ }5 R
  • map 总信息

    $ c, n* k7 K; X
从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。
8 J' R  ]0 [+ e1 O有多少RO?RO到底是什么?3 F# z9 @5 W. l! H- |
有多少RW?RW又是什么?7 }) C+ e  L( y3 ]9 ?
ROM为什么不包括ZI Data?
1 w4 i# J, ^5 Z- f! b- p6 ~" x7 U为什么包含RW Data?7 u9 _8 M7 L5 z! `' {6 N9 H
3 ]3 @0 i' G' R' L

" S* a9 S3 l2 l' Y. f# l' a1 q 640 (18).png
$ u' r$ Q4 K/ H  t$ X) [- L6 h7 Y) L) Z/ p
  • Image component sizes

    1 n. Y4 o! n, u1 k
    , V8 u0 u# i# ^! X; g! f5 I
往上,看看Image component sizes,这个就比刚刚的总体统计更细了。这部分内容,说明了每个源文件的概况首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些STM32的库文件。
& \* H' i0 @6 x
: a+ \/ o9 H: T

8 H4 i* G0 _/ d6 p1 { 640 (17).png & u1 c* o& p/ h/ K0 [
第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1M flash上的程序压缩到能在512K上运行。
8 n( W% G7 T: w9 I. ?; D& E& s. Q2 h+ `6 t& N

) x% x* {  n1 ?; w 640 (16).png - H8 |* x# A' ^4 d

/ _, S6 m) B( u+ v. V9 N第3部分也是库,暂时没去分析这两个是什么东西。
* P& J0 E' `) E" p9 T2 B4 D2 ^* W4 A1 E* I* y/ z' s9 ~
4 r# p7 M# H  _& U% T
640 (15).png
) g2 [4 {9 N% {5 p5 r  U% f
; v" t. N. b' K0 T) E* o& _! _
库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:
#include <stdarg.h>* u4 L! `" T0 E4 h
#include <stdlib.h>) {9 q4 Z. n/ \2 x9 g6 V- D
#include <string.h>  
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。
6 E9 P. d. m8 R+ O0 U7 w5 J+ d! m
  • 文件map

    : ]6 T9 b- q8 M3 F/ ~( z
再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET+ H8 k' o+ i8 \* q. D

2 A: v  f) ]) g+ i) {
库文件是什么?

9 m% O# a# L" e! O% Q
库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:6 j% {- z0 k7 a5 a( ?4 `
  1. #include <stdarg.h>
    & [8 l% D( J) q; I# z) Z  k6 F
  2. #include <stdlib.h>3 p$ r1 }* {" Z
  3. #include <string.h>
复制代码

/ ~% e5 ?; t# t$ O. Y# q( k2 ^) h
# w3 d; y. B9 T0 R4 @7 d* m
" D4 [. N" v2 s- z6 c8 r
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。- y: P9 h9 c+ d8 C7 W/ p
我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。
3 h. z+ q2 ]$ m' X1 ?1 p* Y  T0 @; `0 t. `( |' G
  • 文件map6 {8 o0 N. i4 p: n& l
    # r& }/ y5 C+ G  W. V2 _+ D- a
再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET$ {8 G8 \1 |# G1 D9 x* O
/ K' k$ ]4 v3 X& ~; f4 d

5 l$ f5 y' |+ E4 S" J 640 (14).png ' t2 ]6 S6 L9 b. `
每个文件有有多行,例如串口,4个函数。3 O( C8 h9 P; U' a# }0 u5 I- t
/ Z+ O, z8 M+ W8 a7 u3 x- c$ q6 Z$ p
+ s* d/ s0 L  f/ j) D! N
640 (13).png 6 B2 J' @; b$ }7 t! B- f
3 a2 n! A- Q7 k+ [4 F

" I& {# U9 h7 {0 F7 ?! k' K然后是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?
" W, r9 F+ K0 \  x9 D5 L& Q2 e6 m
* e9 h; P" A: X% I

( f0 z( r3 V$ J6 o  g" G6 | 640 (12).png
: t( W; t$ k& q1 b1 p% o( t
$ j! l) C2 N0 s6 D8 U
  • Image Symbol Table
    % @' _! E$ ^% J3 O: Y9 U9 y
再往上就是Image Symbol Table,就更进一步到每个函数或者变量的信息了
" ^/ Y! K# G2 q" t* t
# Z2 V3 Z3 F% F5 x1 a" Q
/ X9 b: ^, ^/ I4 @6 y/ W, I! C
640 (11).png " ]% R+ D1 o" v3 d  a
; j6 F4 t2 y, a& o8 q- R( T
9 t/ g4 _* c8 Y5 L
例如,全局变量TestTmp1,是Data,4字节,分配的位置是0x20000004。* B6 v: A! N3 ]: U0 e0 v, O
& s  ^" q; y2 ?: m2 \5 r0 A

7 r) _$ [/ N. G# f 640 (10).png
8 u3 p) |  c( ]  ]
. p% t8 a* w6 \/ I
6 v% n& Y' I5 X+ B$ A
TestTmp3数组放在哪里?放在0X080024E0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几K几十K大,不可能也没必要放到RAM区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。
+ V; I4 a4 L$ ~/ c3 f6 Y* s8 c0 m+ P/ i9 E# i
const的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习
/ M% K! ?9 T$ R' w# N- Z4 k
640 (9).png
- B3 b3 n4 K% p" ^, a
/ v8 L9 e! Z/ a
0 }3 w. J3 K0 j7 `# X6 M
那局部变量存放在哪里呢?我们找到了test_tmp3,7 D; C& c' x' E0 A& h

7 z  \0 A  W' g

/ p. {  G" K% R" p; D. S; F) s! K$ `3 S& M 640 (8).png ; Y$ F1 q5 I( f' U# U3 I3 j% U/ ?6 H
! z4 _' i7 Y9 g2 j# D. @
# o) m$ H) t: R5 ?" h) U% [
没找到test_tmp1/test_tmp2,为什么呢?在定义时,test_tmp3增加了static定义,意思就是静态局部变量,功能上,相当于全局变量,定义在函数内,限制了这个全局变量只能在这个函数内使用。哪test_tmp1、test_tmp2放在哪里呢? 局部变量,在编译链接时,并没有分配空间,只有在运行时,才从栈分配空间。7 T" |9 ]5 a! D$ \9 }1 E" d
  1. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值% \- g- ^3 n% ~  J* l
  2. {7 P' w3 m9 I% T+ U7 E9 }( z
  3. u8 test_tmp1 = 4;//局部变量,初始化2 g/ C$ [( V" j/ H5 c* h
  4. u8 test_tmp2;//局部变量,未初始化
    / N9 x5 A5 K7 C1 @8 n8 @
  5. 6 f8 c3 C# [+ {! [& ?( S0 U
  6. static u8 test_tmp3 = 0;//静态局部变量
复制代码
5 U$ F5 b+ v7 }/ v
+ @% ^4 C) J, K
6 s* P$ L* y5 n! \. g/ f2 W
上一部分,我们留了一个问题,哪些变量是RW,哪些是ZI?我们看看串口变量的情况,UartBuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文Block Started by Symbol的简称。- S$ P; P2 m7 x. T& u. ]; s

5 {% V! ^! g+ v' k6 f: Q4 x
5 p5 Q6 Y* |$ c. O
640 (7).png 7 q3 D7 {3 _+ m& |- `9 c
0 x- \, {% S& t- D
- k# ?  O: B6 i$ c# L3 l
到这里,我们可解释下面几个概念了:
* r, `' j0 m& s: `- Q/ G; U$ u
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区。
& \0 E. c5 S  z  ]6 g- ~2 j
640 (6).png & t4 _2 j* l/ g; d

! m5 d+ F, Q2 s我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579
3 k6 f$ c* J/ q% A6 M- V
) ]% U2 u* B2 z3 y1 c7 \

, ~: e8 t, S1 M 640 (5).png # L0 \" N5 x5 d* V6 V) V
* l# g% ~1 S9 C6 z# o- D( O
main是main,放在0x08000189
& U  k9 ~! V  w3 R) E+ b  m  D. P% Q  I; B  o% ]

5 e) T5 h) D6 `  E& m 640 (4).png
& F- \' ?* _. v) v* A5 R7 V
" Y, y$ S8 H4 K7 M

- d3 q; x2 _/ }  f1 Q0 j6 h4 k! K3 U__main到main之间发生了什么?还记得分散加载文件中的这句吗?
& K& q7 v3 f) D
  1. *(InRoot$Sections)
复制代码

8 I4 w* I( _  y( Q% j__main就在这个段内。下图是__main的地址,在0x08000189。__Vectors就是中断向量,放在最开始。
6 A3 b5 E$ D5 N: x2 I1 E* Z7 i. Z( d* I6 \
3 b- r  @* {# o& B& c8 s0 w
640 (3).png
4 `% x$ [8 W) m; d( \4 j9 V; b( j; F4 F; A& q
1 l- H0 q  i* Y
在分散加载文件中,紧跟RESET的就是*(InRoot$$Sections)。% N/ q4 f: I0 l

$ ^6 j# H( W& Q) w" z8 k

; P2 a2 \  d  R- G 640 (2).png
& @0 U9 }! g2 G  q. B8 @# ^9 E% f
# T( j8 O, s" J3 m2 w
1 b; V+ ^3 U% E8 y9 u7 J2 z
而且,RESET段正好大小0x00000188。
) m* k1 z  ]( Z+ \% {3 _9 Z# ^* d; B: D
( a3 \: T# f' F: U) [
640 (1).png
6 n9 r0 C6 ^0 K- R% R
3 l: g* Y8 t1 R

  w/ m" J) j6 d7 X巧合?
9 B! ^+ Y% M- o  L) z
% y+ q) w* u+ v

* n* ?0 d/ h  w; h( y6 v# V3 e 640.png : }& I; ?# L9 b6 U+ \/ W; R

0 A' [; }; \* e$ c

' r3 `1 `% u; K( \9 q' s6 e这一段代码都完成什么功能呢?主要完成ZI代码的初始化,也就是将一部分RAM初始化为0。其他环境初始化……最后到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。" Q3 D6 @! s9 Z  d: B* l, Z  V/ y1 Y
2 I' X+ x3 ~" k" k9 p0 ^' f* I

  L  g+ P( L: ]1 i" W: B- m
8 _6 v# i, z- O8 G
9 ]# d- A6 E1 B$ q) z
/ x8 L8 N0 ~6 j' m9 O9 R
. r0 q  k( A* @/ F7 _. W/ ~
赞 收藏 评论0 发布时间:2022-5-27 21:34

举报

0个回答

所属标签

相似技术帖

官网相关资源

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