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

基于STM32单片机软硬件结合经验分享

[复制链接]
攻城狮Melo 发布时间:2024-7-29 15:25
本文分析STM32单片机到底是如何软硬件结合的,分析单片机程序如何编译,运行。( {/ _0 `5 M2 U9 J& C* G, Q" w
/ E/ v$ W, i8 O/ K8 N+ O
软硬件结合; f  J5 K# q6 N" H2 P- O! D$ q5 u
初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写P1=0X55,就可以在IO口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。# y$ `& z5 F( ^/ {& j4 k- _' P: w% S
, F8 L: {/ p8 S. n
寻址空间1 D" ?/ W/ \$ I4 u
什么是地址空间呢?所谓的地址空间,就是PC指针的寻址范围,因此也叫寻址空间。: y+ {8 O7 u* w' d# ^- k1 [* Y

/ m4 h, F2 i% X大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,PC指针就是一个32位的二进制数,也就是0xffffffff,范围只有4G寻址空间。现在内存越来越大,4G根本不够,所以需要扩展,为了能访问超出4G范围的内存,就有了64位系统。STM32是多少位的?是32位的,因此PC指针也是32位,寻址空间也就是4G。
$ R" I# ~  n3 B$ f( n# I
+ T/ t: g6 k  _: j# C1 T4 u. A我们来看看STM32的寻址空间是怎么样的。在数据手册《STM32F407_数据手册.pdf》中有一个图,这个图,就是STM32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫Memory map,用一个新芯片,就先看这个图。2 u0 a: B, f  G4 L8 c0 g/ o
8 v5 b: ]* m" V1 W  U4 F
微信图片_20240729152315.png 7 T4 A' w+ f9 Q" }
8 O6 f, v7 g" y* x
# I! H6 i0 C* s% f, R2 R$ }" u
最左边,8个block,每个block 512M,总共就是4G,也就是芯片的寻址空间。
, t- T) R/ p$ A# F! p3 p, w: s0 R6 J2 @) a6 d3 `# Y
block 0 里面有一段叫做FLASH,也就是内部FLASH,我们的程序就是下载到这个地方,起始地址是0X800 0000,大家注意,这个只有1M空间。现在STM32已经有2M flash的芯片了,超出1M的FLASH放在哪里呢?请自行查看对应的芯片手册。
( J( e' q, g! M& r$ s, n' o9 |  l* p. Y* J" q* a' P3 Y
3 在block 1 内,有两段SRAM,总共128K,这个空间,也就是我们前面说的内存,存放程序使用的变量。如果需要,也可以把程序放到SRAM中运行。407不是有196K吗?, G  L2 ^3 g5 e: E; l

0 q6 \* J7 h3 M, H其实407有196K内存,但是有64k并不是普通的SRAM,而是放在block 0 内的CCM。这两段区域不连续,而且,CCM只能内核使用,外设不能使用,例如DMA就不能用CCM内存,否则就死机。
$ ?/ s( X4 d; {5 }4 `! [7 H  _3 O  C% d- o5 w
block 2,是Peripherals,也就是外设空间。我们看右边,主要就是APB1/APB2、AHB1/AHB2,什么东西呢?回头再说。
! X- \% W1 h; G- ^
& r2 C; ^/ ?9 _( D* W* Sblock 3、block4、block5,是FSMC的空间,FSMC可以外扩SRAM,NAND FALSH,LCD等外设。3 Y3 W- D4 M! |, Q* A; T6 p# ~+ e

8 g* ]2 C6 V1 E' j2 j好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。对于这个疑惑,也可以看此文:代码是如何控制硬件的?在IO口输出的例程中,我们配置IO口是调用库函数,我们看看库函数是怎么做的。. g, }2 U$ y. q* ]

) }: z7 ?5 _% B2 V* ^例如:
8 M5 u! ^" s" e) j% f3 r
  1. GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
复制代码

6 v, d. }. X( s这个函数其实就是对一个变量赋值,对GPIOx这个结构体的成员BSRRL赋值。+ C! M  y! C, |  I: B* ?
  1. void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)$ `/ G4 H2 L5 H! h0 J' j7 G
  2. {
    5 f3 [# S+ A( B# c% q' m
  3. /* Check the parameters */
    / ]5 X8 |5 Y4 U, H) M, e# C
  4. assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
    : j; P. c. x, I
  5. assert_param(IS_GPIO_PIN(GPIO_Pin));
    ! \, M4 h6 n! x2 F4 l8 Z! _; ?2 u  N

  6. & a' s, W' Y2 }" R4 J. k0 F' R

  7. . p: I0 }6 k9 U: ^- \6 D( j" ~; d
  8. GPIOx->BSRRL = GPIO_Pin;( a" w: \( C1 m: W5 L5 F
  9. }
复制代码

3 \6 Q- `4 X3 Y% Oassert_param:这个是断言,用于判断输入参数是否符合要求GPIOx是一个输入参数,是一个GPIO_TypeDef结构体指针,所以,要用->获取其成员
, f5 _4 u5 K/ x% M8 r: b- j

3 b8 v) m6 Q% x& n3 SGPIOx是我们传入的参数GPIOG,具体是啥?在stm32f4xx.h中有定义。, B$ Y" k; x( N% j
  1. #define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
复制代码

& \4 K0 F* {# {3 NGPIOG_BASE同样在文件中有定义,如下:+ Y" y7 u/ [, V" X+ G, ?
  1. /*!< Peripheral memory map */
    ( y% q7 Z, _* \# |
  2. #define APB1PERIPH_BASE       PERIPH_BASE+ A: M% D: n1 h  f/ P/ o$ o9 x* q& }
  3. #define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)5 Q9 [$ Y: f7 P5 F" Y
  4. #define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)( @$ H6 `" Q' @: Z
  5. #define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)
复制代码
2 g, r- i) E. N! Y. I0 b
再找找PERIPH_BASE的定义
9 X  }' b% W, N2 ~9 o# i  T
  1. #define PERIPH_BASE           ((uint32_t)0x40000000)
复制代码
到这里,我们可以看出,操作IO口G,其实就是操作0X40000000+0X1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是SRAM空间地址,0X40000000+0X1800是外设空间地址。$ Y" y0 f# c4 ~9 k" }
  1. u32 i;
    9 B( h3 _" ^5 d! I5 z3 b$ ~1 W
  2. i = 0x55aa55aa;
复制代码
' G4 h* J$ k' R! E# z. \, d) X
这个外设空间地址的寄存器是IO口硬件的一部分。关于如下图STM32的GPIO文章推荐:STM32中GPIO工作原理详解。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0X40000000+0X1800+0x14.( \9 R' U# B' i* O$ F
9 h2 M: ~- u% D
微信图片_20240729152319.png
; F$ F  H4 G' @. l  U6 R( Z
% J2 @; ]7 H2 r0 U. R/ ^
控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。, U. |1 I0 M* Y2 a/ d

: V/ f8 J8 `' A0 ]1 z) L4 Q; W寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或RAM。寄存器为什么能控制硬件外设呢?因为,初略的说,一个寄存器的一个BIT,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。
% N; y- }, N5 A% S* [  @7 R
, n( Z& i: H% [- P7 K
纯软件-包罗万象的小程序& b: ^6 \* ^) E. Y
我们已经完成了串口和IO口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?关于程序是怎么在单片机运行的,也可以看此视频:动画演示单片机是如何跑程序的。代码到底放在那里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。
$ o+ _- _5 _+ J, c% X4 {( [0 @0 c' O
分析启动代码4 b/ l" m$ [3 e! X0 o$ d
函数从哪里开始运行?4 B  o; w1 d9 C0 H& r1 X
每个芯片都有复位功能,复位后,芯片的PC指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,PC可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在STM32中,复位到0X08000004。因此复位后运行的第一条代码就是0X08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。- u' {5 j( ]% U# b, E6 A4 ~$ a7 w  S
* t1 K8 }; h2 u/ {# b
芯片复位时,芯片从中断表中将Reset_Handler这个值(函数指针)加载到PC指针,芯片就会执行Reset_Handler函数了。(一个函数入口就是一个指针)
  1. ; Vector Table Mapped to Address 0 at Reset
    ; h& U% T- }$ E+ E- e) i( F1 Q
  2.                 AREA    RESET, DATA, READONLY2 B- U6 V% X' o  j# u" |) w
  3.                 EXPORT  __Vectors3 ]% a4 E9 V; d' Z) ^
  4.                 EXPORT  __Vectors_End
    . d! w* V5 d0 u& B
  5.                 EXPORT  __Vectors_Size
    / O! {5 }( \9 f$ z

  6. $ ^! x/ g! L; l+ W: j6 f
  7. __Vectors       DCD     __initial_sp               ; Top of Stack  j" b$ X5 P2 O+ n
  8.                 DCD     Reset_Handler              ; Reset Handler! x' m; S% T1 u
  9.                 DCD     NMI_Handler                ; NMI Handler
    ) c5 |4 y, v1 X
  10.                 DCD     HardFault_Handler          ; Hard Fault Handler
    + D; @7 y3 R# l" s
  11.                 DCD     MemManage_Handler          ; MPU Fault Handler4 p/ D$ b1 ~) o+ x3 A+ v4 l
  12.                 DCD     BusFault_Handler           ; Bus Fault Handler
    0 y% k& r& ]5 f' M8 |
  13.                 DCD     UsageFault_Handler         ; Usage Fault Handler
复制代码
, F0 f1 Q. \1 D7 i- ?
Reset_Handler函数,先执行SystemInit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?
  A0 u4 ^) x6 v3 w. x6 H8 f1 d* w& |% Y! t5 N. F" R4 X
是我们在main.c中定义的main函数吗?后面我们再说这个问题。3 X) _* W7 `& N1 |9 W7 h

. U! E$ ?% U5 F, q1 s
微信图片_20240729152323.png

, }% h: H3 R- e9 w
3 X4 I8 g1 }9 z) `/ a3 G) [# X$ p* _
芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujique\prj\Objects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。
6 F4 ]9 a2 ]; P5 g

) J* M/ b+ h3 ~在MDK软件Options菜单Linker下有关于这个菜单的设置。, V! o# R# B' D

; J6 w% f' H, ~1 F! T
微信图片_20240729152326.png
5 o/ Y& B. \1 }# W
/ _2 m( R1 s- Y9 C# F
3 V/ q6 t* Q. O5 w% _
6 \; [! |, X  P% [0 n
把Use Memory Layout from Target Dialog前面的勾去掉,之前不可设置的框都可以设置了。点击Edit进行编辑。/ u1 _4 O6 Q- t

+ h2 M3 U9 @& g7 ^6 Q
2.png

9 _* b7 K1 _: \2 A
0 \2 z' x! N( X# t

# P% [3 y  V/ j* ?在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。2 v, t4 O& I2 s, s

) f: Z) g' v- D9 R4 H' u其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定FLASH跟RAM的大小于起始位置,当我们把程序分成BOOT、CORE、APP,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到RAM中运行。
3 f% a) l* w) ]" L/ R8 A& f. H
8 V+ x! j9 D% l( h
3.png
* I% v2 N  t/ |$ M: e: F
  Q2 l, J3 T% |6 d- M- q
从这个基本的分散加载文件我们可以看出:
8 |5 D0 M8 n5 W! P, a6 K" ?  ]
6 D- {  v1 m3 n! ]" L# F7 h( k第6行 ER_IROM1 0x08000000 0x00080000定义了ER_IROM1,也就是我们说的内部FLASH,从0x08000000开始,大小0x00080000。9 ?; X$ d. s2 u/ h7 Y* P

/ @3 R& l' X5 ^- O5 p; ^第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。
* o8 ^+ G3 x5 {) q+ h! f- d

# Y! ^5 t1 J8 D1 x/ A第8行 *(InRoot$$Sections)什么鬼?GOOGLE啊!回头再说。# {5 p4 Q" M1 D/ \
' H% q1 Q0 Y( p* q& T
第9行 .ANY (+RO)意思就是其他的所有RO,顺序往后放。就是说,其他代码,跟着启动代码后面。
2 j* p7 V+ W& ^
+ f( l2 Z* q' w: u: l3 k
第11行 RW_IRAM1 0x20000000 0x00020000定义了RAM大小。! y8 G  |# w: D6 o+ `; a

% S% Q- U5 z# N/ d0 ]第12行 .ANY (+RW +ZI)所有的RW ZI,全部放到RAM里面。RW,ZI,也就是变量,这一行指定了变量保存到什么地址。
1 [# l& |8 V0 ~& b  h
  Y; X5 p; n* F3 p% W/ ?+ ~- m分析用户代码
; c; ]4 _1 P+ `0 ?到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。; i  ]! q5 \) `/ C5 r! X

/ L$ `  Y) B# j8 f$ e; J9 l' E, a1 程序跳转到main函数后:RCC_GetClocksFreq获取RCC时钟频率;SysTick_Config配置SysTick,在这里打开了SysTick中断,10毫秒一次。Delay(5);延时50毫秒。
  1. int main(void)
    6 z( g0 S  B5 j$ S; ]! W1 y7 k
  2. {+ t( R! Q5 {9 D5 z* ]  w2 @
  3.   GPIO_InitTypeDef GPIO_InitStructure;% u8 Y* F0 n/ `8 w
  4. 4 n9 v: j& ~2 K. z- `9 O4 {( o6 w. E
  5. /*!< At this stage the microcontroller clock setting is already configured,4 H& R6 s) X4 r+ f+ j9 c5 g
  6.        this is done through SystemInit() function which is called from startup8 o6 |% L( m: B/ J
  7.        files before to branch to application main.
    ) W2 [6 r. A0 Q$ ^/ ?
  8.        To reconfigure the default setting of SystemInit() function,
    + m% s: }4 b4 \4 H
  9.        refer to system_stm32f4xx.c file */
    6 m! Y2 ?9 t- g6 {- H# G. H

  10. 4 O/ B  M" \3 Y# Q3 a1 v7 A
  11.   /* SysTick end of count event each 10ms */
    7 f3 Z0 i9 u$ H" r0 V
  12.   RCC_GetClocksFreq(&RCC_Clocks);/ g# d! X7 y+ ^9 S1 ?
  13.   SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);  P- h. U# W( Z# f" z, i

  14. # r+ N/ h- f4 B$ o5 w2 r  o5 ^* P
  15.   /* Add your application code here */3 _  A$ ^2 D8 v1 _0 Q# H
  16.   /* Insert 50 ms delay */) a1 Y! I& K2 x, H6 G, `
  17.   Delay(5);
复制代码

2 e2 t7 C! d9 t3 x$ p: E- l8 V2 _2 初始化IO就不说了,进入while(1),也就是一个死循环,嵌入式程序,都是一个死循环,否则就跑飞了。
  1. /*初始化LED IO口*/
    : O% K2 @, X0 `/ }
  2. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);8 d# x5 G8 C& {: }* B6 z/ u

  3. ) l! M1 K( \$ ~0 c- Z
  4. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;- L  C5 e9 ^1 {1 K
  5. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;- {9 A) x" u/ J6 Y) \
  6. 7 E2 i9 z( l) d! g" _5 K* A
  7. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    ) ?  Q1 C  \7 t3 k
  8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    0 |+ T  s+ A' @. D( l! a
  9. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    ' ~6 W  ?* @% n  {& j
  10. GPIO_Init(GPIOG, &GPIO_InitStructure);      V3 t* v/ y: l7 P9 _5 s/ ^

  11. " r; _  @3 v- K6 \5 t
  12. /* Infinite loop */
    ) @, N; o7 N% \- h+ r/ E8 }# I
  13. mcu_uart_open(3);$ T0 R9 T  l1 S' W: {
  14. while (1)
    " Q; W: [* _/ c
  15. {( X+ D$ Z) l8 \! H6 u. T1 h9 R
  16.   GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);8 @/ C% M" |* `9 A- ?4 ]( B
  17.   Delay(100);
    0 t4 |- ~! s7 [3 d" Z7 k& w
  18.   GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);/ z5 U! k% E; n
  19.   Delay(100);; }% A/ K. i  _6 H0 r# T. U; r& _% L5 Z
  20.   mcu_uart_test();
    7 w( ~9 D- C9 q7 a" y# P

  21. 9 \' s/ Y# N5 j! E+ l1 h
  22.   TestFun(TestTmp2);: H- ~! ]$ A( Y! N
  23. }
复制代码

+ }  {! U) t# q2 h# i9 m2 {3 在while(1)中调用TestFun函数,这个函数使用两个全局变量,两个局部变量。
  1. /* Private functions ---------------------------------------------------------*/3 {2 a: _/ V$ m6 o4 d7 @
  2. u32 TestTmp1 = 5;//全局变量,初始化为5
    / e# _9 u  Y0 c: p  Q8 ~/ c
  3. u32 TestTmp2;//全局变量,未初始化$ G: l" g  w% H

  4. + x8 s# O) r: X
  5. const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};. W" Q0 A' S- v' d  t

  6. ) i; m! W; k& m
  7. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值+ @: s' B1 v, J2 q8 G' G
  8. {
    7 a, N9 E" S1 f( _0 `1 B
  9. u8 test_tmp1 = 4;//局部变量,初始化' z" c; h+ P" G& x
  10. u8 test_tmp2;//局部变量,未初始化$ q8 u" X/ r/ o  s3 x1 M4 a3 T. ~
  11. 1 o3 ?# T# D# w
  12. static u8 test_tmp3 = 0;//静态局部变量
    8 g) q5 t; U' x4 x6 b

  13. % G$ `3 i1 s, d
  14. test_tmp3++;- W6 g+ N& X4 g$ A0 |. {1 d; V

  15. 1 u% O& J0 f7 T% S. J
  16. test_tmp2 = x;7 P, f& Z/ c0 v: p& N: z
  17. % I- E9 J" P2 g
  18. if(test_tmp2> TestTmp1)
    6 z- N  B1 X; [" E
  19.   test_tmp1 = 10;9 [* K  x+ ~+ I5 y: z  ^( ^! `
  20. else4 U! `+ T' O% N" h/ `" ]
  21.   test_tmp1 = 5;
    ( U( B+ [* I! v8 W
  22. * a+ e2 c" z/ Y
  23. TestTmp2 +=TestTmp3[test_tmp1];
    " k. T- U; {/ D7 I* ~

  24. 7 d1 x% E/ n1 G9 m" v
  25. return test_tmp1;
    , L1 n! q% Y9 i4 R' {
  26. }
复制代码
$ {2 F0 n' v# J
然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。相关文章:STM32中断系统。我们查看Delay函数,uwTimingDelay不等于0就死等?谁会将uwTimingDelay改为0?
  1. /**
    0 i7 J6 J% S7 c6 z
  2.   * @brief  Inserts a delay time.% I( O! t; T- q( f  C. R, y7 R
  3.   * @param  nTime: specifies the delay time length, in milliseconds.
    ; u7 F7 g1 ?8 O
  4.   * @retval None9 c& ?$ w  v# \5 a5 M
  5.   */4 f0 H. c2 |% V9 `
  6. void Delay(__IO uint32_t nTime)7 o+ Z0 ~1 y8 C+ \
  7. {/ T  \0 a$ B7 [, V& b" U
  8.   uwTimingDelay = nTime;
    / G$ K( @+ Q# ^

  9. 0 y2 Q7 R  r3 i" R0 F
  10.   while(uwTimingDelay != 0);
    * W, n* i% _9 U, {& V! f
  11. }
复制代码

6 ?2 b& w- r0 T0 z2 r- h4 Z搜索uwTimingDelay变量,函数TimingDelay_Decrement会将变量一直减到0。
; A) V$ t7 T/ B9 U% ^9 D+ V
  1. /**
    3 b" K& U, ~) E  z5 l+ W( W2 i
  2.   * @brief  Decrements the TimingDelay variable.
    # @# _$ P7 V! j
  3.   * @param  None3 N1 `! J" T/ l# l! F; o
  4.   * @retval None
    - _* a$ v; M; \: o5 x9 `: P
  5.   */. R. L. `$ R3 `# E$ G( |
  6. void TimingDelay_Decrement(void)3 d) X& o, L: M* O
  7. {
    & K+ D+ l# L1 h  ?+ l! s+ h
  8.   if (uwTimingDelay != 0x00)
    1 ?  B/ t$ K9 j3 k7 H$ p
  9.   {
    " \7 A' M2 Y7 ^' I( Z: T
  10.     uwTimingDelay--;$ y# s4 R; c  I
  11.   }
    # n  e! Y$ }. C0 S# U4 I& M
  12. }
复制代码
+ Y( U  Y% E5 z) w  w3 j
这个函数在哪里执行?经查找,在SysTick_Handler函数中运行。谁用这个函数?
  1. /**
    3 v- L5 T9 v* a: O5 B( Y
  2.   * @brief  This function handles SysTick Handler.
    1 E2 n& s; I: p6 k; V4 Q2 w# u; G! U
  3.   * @param  None5 N! z' s8 b% c+ R2 f) Q6 f
  4.   * @retval None
    7 z- D7 U! X# Z
  5.   */: w; G: ^  T! ?2 v5 l6 R: W4 X: Y
  6. void SysTick_Handler(void)% x8 w0 Q8 l3 m8 x* g$ B4 _/ A( g. a
  7. {
    4 `% o: j* d" K8 S  @1 ~! c
  8.   TimingDelay_Decrement();
    " q. D) A( [. H9 ]8 g) N* G
  9. }
复制代码

$ K) l# }' f6 Z7 K经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。
  1. __Vectors       DCD     __initial_sp               ; Top of Stack4 @5 I7 N6 _$ T4 o1 ]. r
  2.                 DCD     Reset_Handler              ; Reset Handler
    2 }; X. B! c2 y$ \2 b1 ~6 ]- @8 L
  3.                 DCD     NMI_Handler                ; NMI Handler8 ]& q7 ^  T! D8 }: F* U0 n
  4.                 DCD     HardFault_Handler          ; Hard Fault Handler
    + l& b) }4 @+ F, r% P& p
  5.                 DCD     MemManage_Handler          ; MPU Fault Handler: g( t. x% ]( d# e3 n( w0 q
  6.                 DCD     BusFault_Handler           ; Bus Fault Handler
    , [6 D9 A) x: i1 L# i! S  f
  7.                 DCD     UsageFault_Handler         ; Usage Fault Handler
    9 V* P0 z  l. S. f7 @1 b
  8.                 DCD     0                          ; Reserved
    3 d" J( P" i+ m: d; F
  9.                 DCD     0                          ; Reserved$ G& K: u$ Y' T  d2 U9 w. l, c
  10.                 DCD     0                          ; Reserved
    * s+ P6 G8 @7 d* Q! L
  11.                 DCD     0                          ; Reserved
    2 `& I" }3 T! F3 ?  n0 a
  12.                 DCD     SVC_Handler                ; SVCall Handler
    - k7 G* k$ P# V
  13.                 DCD     DebugMon_Handler           ; Debug Monitor Handler
    , G0 O% n9 U$ v$ ]/ f4 g! E; A# u8 V
  14.                 DCD     0                          ; Reserved
    " J; q+ Q( k- j: Q( N
  15.                 DCD     PendSV_Handler             ; PendSV Handler7 y! P5 R- Y( p
  16.                 DCD     SysTick_Handler            ; SysTick Handler
复制代码

. G! n: T: u# @% z* e  G+ v/ D余下问题
% c4 t: ^; }6 N- ]1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(InRoot$$Sections)是什么?3 ZI段,也就是初始化为0的数据段,什么时候初始化?谁初始化?
; Q( B4 h# d9 ?! Q

0 s) b' D2 L$ j9 D: W6 m为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!
9 I4 c: q' \2 l& i' p3 X; {
. d' ~; d, V7 `( H3 U

+ f* F- V/ Q+ w6 ?通过MAP文件了解代码构成+ z) c' h1 ?9 H9 O2 J
编译结果
# u" E/ Y6 ~" q# P
程序编译后,在下方的Build Output窗口会输出信息:
  1. *** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'
    : S; ]4 a, K. ^, o" W& B, V& ^2 v$ q
  2. Build target 'wujique'
    , R4 |8 f, @; C( s- ^/ L: ?
  3. compiling stm32f4xx_it.c...
    : B: B" w3 ?$ Q' l6 ?
  4. ...6 [" T4 {2 r' _4 V- b! ]# p9 [6 X+ x  \
  5. assembling startup_stm32f40_41xxx.s...  U& E7 U5 c* i" T$ Y1 T
  6. compiling misc.c...
    6 L) P" P/ z4 i  C; Q
  7. ...
    : @" x% h$ `' J0 X( z% a! N
  8. compiling mcu_uart.c...
    ! |# @! ^3 V5 R9 i+ w
  9. linking...8 G" I2 ^! r) ~  o
  10. Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  
    8 v& G2 ?3 U/ A4 T. k
  11. FromELF: creating hex file...
    3 _, r" C( m1 E- g
  12. ".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s).
    5 d6 J  _% Q$ Y/ C. r
  13. Build Time Elapsed:  00:00:32
复制代码

4 K4 y/ S3 C' I( Y9 x% j/ F编译目标是wujique
+ |! v3 t$ `( }9 s7 E% |C文件compiling,汇编文件assembling,这个过程叫编译9 k, v2 F2 Y. T% ^6 i7 y
编译结束后,就进行link,链接。* K) q; k& m% q6 A8 t3 v8 k, X( {
最后得到一个编译结果,9038字节code,RO 990,RW 40,ZI 6000。CODE,是代码,很好理解,那RO、RW、ZI都是什么?
+ o) H6 c9 N0 Y: g3 t; `, p3 M- bFromELF,创建hex文件,FromELF是一个好工具,需要自己添加到option中才能用2 B/ E( c: Q3 A7 g5 Y- j8 W

+ i4 C1 M( q5 N+ c5 i. x7 \
map文件配置
" L4 ~- \. X, X更多编译具体信息在map文件中,在MDK Options中我们可以看到,所有信息都放在\Listings\wujique.map( e/ }5 [% I. ~1 `7 u3 F

  A( V5 K5 F  V5 Y; I6 A, w; x8 B5 q  Q/ J默认很多编译信息可能没钩,钩上所有信息会增加编译时间。
- \6 S* R! d' j& {& R* `+ m+ E: R' x- W8 H. o7 m
4.png
0 y# D. M- K7 `: r, R

. v* N" Z$ B, e+ K% `+ Zmap文件

& i% }( X0 j  d8 g6 r打开map文件,好乱?习惯就好。我们抓重点就行了。* |- z1 j* ~7 Q; F6 K, ]2 ^3 n
& z" V9 y# a7 @% r* G% Z) J
5.png
  u' Y) i' e9 ^; K7 @
' ]. @  f; g: m8 z& n& s# }# C: o3 l0 z
map 总信息8 [% W" {4 I. k4 s5 O. T

  I" ^- S; A0 o9 e从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。; \: |4 o( U( b# d9 F9 L# m2 e5 W

5 L8 {6 N) h2 D+ [5 O1 z有多少RO?RO到底是什么?
  S" Y( m; `3 R3 X! r; t4 V; T% s: x+ {) O' F) V
有多少RW?RW又是什么?
, b) _' L$ r+ h% M  @6 t0 a' t4 R' c' G9 m' Z& [/ ^0 ?
ROM为什么不包括ZI Data?为什么包含RW Data?
" \4 }; A. S2 }8 n
+ G, ^' \& K! h4 I6 g7 p  D
6.png + E( }6 m% @' Z( d; w
Image component sizes, O3 ^' k4 J' _

4 p" R4 u8 Y: V往上,看看Image component sizes,这个就比刚刚的总体统计更细了。
! V9 A8 Z: n1 B9 S; Y9 h! X# S0 y' E" r# G
这部分内容,说明了每个源文件的概况
+ K$ Q5 }5 `# }* T5 B
' A- @. D$ `. g. ^
首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些STM32的库文件。
3 g; R- {- Q& C7 F; g4 E2 _# @8 m( J' R
7.png
4 x! e) q6 V( o- B  Q% g
5 M0 b9 B- I: `' Q
第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1M flash上的程序压缩到能在512K上运行。1 L0 g; U8 I: ~6 E$ S' l

) v# {% Q! |0 o- w% S& u- Q% w
8.png

5 I5 k9 s; X" x

- n( O, @% M; R: r4 a' o第3部分也是库,暂时没去分析这两个是什么东西。7 A* E/ M: H: M" `* W4 I

) {+ B1 E, }7 F9 Q. `5 r
9.png
5 z& O% Y/ O4 T) k  q6 X9 s/ R
6 x" R. d7 r# R9 n& K3 \
库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:
6 Z/ r+ L; [2 {& e( ^# f#include <stdarg.h>  |2 F6 ?$ @1 L
#include <stdlib.h>0 Q2 O% a; E- ^8 R) K1 p+ J# b2 p
#include <string.h>  
. Q& M* s2 i. ~2 }. Z; M# b. m# h1 a( [3 J# z4 G5 J! I+ i: ^
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。
* L" m- o5 c+ C; Y9 L
9 P& g+ ?9 a$ {7 |  b

$ Y$ ^% S- t- H. y: F; n文件map
' \* G; Y$ w+ S! k/ s  q再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET
0 a5 Z% P; p& X: o3 z" D% k4 A) ?/ R$ {, U8 }) `& P
库文件是什么?
' N+ J, a% j/ L. j) Z库文件就是别人已经别写好的代码库。
, s+ N% \0 m  |* k8 a# i& {  a! G. I5 E+ w, @! e. O, |9 `# i
在代码中,我们经常会包含一些头文件,例如:) P6 v4 l+ U3 w; Z1 J
  1. #include <stdarg.h>
    0 N! b; g% J* F* l& {6 h$ f5 i
  2. #include <stdlib.h>
    & N' t" ~; }1 q" E% Z
  3. #include <string.h>
复制代码

5 i& P2 e% K9 D( j这些就是库的头文件。相关文章:C语言中的头文件。这些头文件保存在MDK开发工具的安装目录下。+ \7 c/ L- K( W5 N! W5 a" z7 U

; l4 V: n) G: I! H6 T我们经常用的库函数有:memcpy、memcmp、strcmp等。# ~9 `1 Z" r% D& A6 |) X1 ^
, M! F* {5 m$ ~
只要代码中包含了这些函数,就会链接库文件。4 [8 U" _( z1 z. F" E

7 z+ l! I# i' ~3 }文件map
' i' L  ?5 C* b5 s再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET
3 [5 {/ Y1 }5 K) C  u8 u+ a" L4 O( G+ s2 h  L
10.png
1 n, X# {& @. T# B

6 t0 g4 T* \0 w6 P6 A% }8 o9 R每个文件有有多行,例如串口,4个函数。
" c  q, Q, z9 ~( m
) v3 x! U9 U7 S0 p( H$ J
11.png

0 n: S2 u9 o1 O9 w0 R) Z' `$ K( K
- N8 Y8 ^! E* Z; R- q" z# C% J
然后是RAM的,main.o中的变量,放在0x20000000,总共有0x0000000c,类型是Data、RW。串口有两种变量,data和bss,什么是bss?这两个名称,是section name,也就是段的意思。看前面type和Attr,
. H% a$ Y( T7 ^, `' V6 }. r" b; E! n3 ^5 O% g) T! J$ d" ?" l
RW Data,放在.data段;RW Zero放在.bss段,RW Zero,其实就是ZI。到底哪些变量是RW,哪些是ZI?( o4 _8 ?/ c/ E# D, w
/ s' G& R& K. a2 t% M2 M( k
12.png

3 S: x, Q" ?2 ^  A% [7 {  h

# T$ P3 F6 ?( hImage Symbol Table
4 Y6 O8 B& ^$ C! {, l
; `, L9 J; ~5 l再往上就是Image Symbol Table,就更进一步到每个函数或者变量的信息了& T' f6 @" h+ M3 k, @, ]' z$ P

, l+ k$ @5 D+ @+ }  _
13.png
; q% C- X$ _4 T8 S; m% o1 _8 S
7 S, I9 @  f1 m% Q
例如,全局变量TestTmp1,是Data,4字节,分配的位置是0x20000004。: E6 v& E/ J5 z- z+ l3 Q

7 F5 L" Y& U4 h+ V0 B% Q9 O5 I' ~8 L
15.png

; D+ U% d; }: ]; e4 Y& D

# x6 v- \( N0 s* J. c7 NTestTmp3数组放在哪里?放在0X080024E0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几K几十K大,不可能也没必要放到RAM区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。
5 O' }! C# E  ~
" H: n" y' }! S: v  O+ F8 ?2 hconst的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习
7 z- S( V4 R8 M) m6 C4 n' O* f0 K4 J7 V7 n2 J
16.png
- e5 ], `3 N$ F

$ C* d9 @: W. n" ^3 c
那局部变量存放在哪里呢?我们找到了test_tmp3,
, k' C4 I5 V& c! U1 L# |& H1 k9 b* P6 H$ a& y# t
17.png
+ F0 ^1 l/ j5 F5 D8 M9 f
0 E  v7 f3 q; G1 N9 u* w( c2 s. W
没找到test_tmp1/test_tmp2,为什么呢?在定义时,test_tmp3增加了static定义,意思就是静态局部变量,功能上,相当于全局变量,定义在函数内,限制了这个全局变量只能在这个函数内使用。哪test_tmp1、test_tmp2放在哪里呢? 局部变量,在编译链接时,并没有分配空间,只有在运行时,才从栈分配空间。
  1. <blockquote>u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
复制代码
/ g, v$ o( H% [1 G3 e
上一部分,我们留了一个问题,哪些变量是RW,哪些是ZI?我们看看串口变量的情况,UartBuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文Block Started by Symbol的简称。% B/ I* `" z* n
0 `, W& q' k- m: ?, b! Z
18.png

( P7 `* y1 r' f( G5 a
, G( d) N4 K6 S. N到这里,我们可解释下面几个概念了:
5 H- [0 O+ @: H# tCode就是代码,函数。
" K3 ]6 U( C. @+ _8 eRO Data,就是只读变量,例如用const修饰的数组。. j, Q* Y! U6 L1 |
RW Data,就是读写变量,例如全局变量跟static修饰的局部变量。7 H# @6 Q, N( D( m
ZI Data,就是系统自动初始化为0的读写变量,大部分是数组,放在bss段。
, K. b5 h& _9 J, LRO Size等于代码加只读变量。( z: P1 q& `8 z4 [3 o
RW Size等于读写变量(包括自动初始化为0的),这个也就是RAM的大小。
4 z- c' p  t4 K6 t1 `$ VROM Size,也就是我们编译之后的目标文件大小,也就是FLASH的大小。但是?为什么会包含RW Data呢?因为所有全局变量都需要一个初始化的值(就算没有真正初始化,系统也会分配一个初始化空间),例如我们定义一个变量u8 i = 8;这样的全局变量,8,这个值,就需要保存在FALSH区。% x/ X0 U, \1 j

( n4 w9 H' }- l
19.png

3 G4 ]# Q, M- t$ N, z: E

, u' D3 U8 ?1 X& \我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579
8 l1 A% p6 l8 T: L- h' T2 o, w* Y$ F: M
20.png
8 N8 I2 s7 E' t8 W
7 H. p8 I. d: `
main是main,放在0x08000189
! D& D( B+ d- _1 Y% I5 C
' ?0 M! L7 X( V% q! e% X
21.png

/ h: A+ I1 L, }% ~$ w; M0 R; e
4 ^( E# p  J5 z: s1 o! K
__main到main之间发生了什么?还记得分散加载文件中的这句吗?. z5 p* R2 W+ I6 V+ e7 F
*(InRoot$$Sections)3 z) c+ w1 s5 c  h6 D- u% b8 ~
9 R1 L; j2 _+ V5 A- [! h7 V+ p
__main就在这个段内。下图是__main的地址,在0x08000189。__Vectors就是中断向量,放在最开始。( ]) A: Y: |0 P! K" P
1 G3 y1 ^% |5 a8 z7 H
22.png

; t2 n' I& K1 f# B
6 A  e& w- {7 Y
在分散加载文件中,紧跟RESET的就是*(InRoot$$Sections)。/ h- n9 E! h% K
, g' a" `. x( L6 c' m- ~
23.png

' l" _) s4 e. i) M2 K; z

8 {5 [4 g1 ^) R2 O9 V8 y而且,RESET段正好大小0x00000188。) F6 D: S% e" I+ b$ j% z- S$ p

+ e2 _3 O0 A5 g7 D1 Q0 k; q, l
24.png
1 C% C' C* L3 i( L& Y7 [& L- S" w
. M4 J1 @- B) e; \5 l
巧合?可以参考PPT文档《ARM嵌入式软件开发.ppt》。
4 P, J; s5 r, d/ ]2 k4 ]* m( h5 _* U
% C2 _& M+ r0 \/ T- T( I9 I# q2 f
25.png
# ]( c8 Z; d6 \8 r4 e& L; X

8 w& d: c0 G' h; o4 Q* s7 p) b- c3 T
这一段代码都完成什么功能呢?主要完成ZI代码的初始化,也就是将一部分RAM初始化为0。其他环境初始化……
  I, y$ z/ ?0 ]$ `/ k2 [/ z) h
: H  N! S3 k* Q3 U, d
2 a5 C' P( N' L0 }" a2 [
最后. G5 \3 X! j1 ^2 [5 |: {' e( X
到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。# A8 o) f6 E, \( g9 x
# Z8 R' n" H& \& e8 `

3 b0 A- ^# R6 ]0 q转载自: [color=var(--weui-LINK)][url=]STM32嵌入式开发[/url]2 B  M) ?2 E! j% g
如有侵权请联系删除
6 T5 k; A1 p# m4 b9 A0 `2 K3 d" _

: x  D2 c1 @0 z! E2 @5 D. o$ x: v+ r% [  z9 ~' r
收藏 评论0 发布时间:2024-7-29 15:25

举报

0个回答
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版