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

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

[复制链接]
攻城狮Melo 发布时间:2024-7-29 15:25
本文分析STM32单片机到底是如何软硬件结合的,分析单片机程序如何编译,运行。
) m6 u. V  N+ c4 K9 w/ X% V( C- P) x5 q( X1 m( ~
软硬件结合# r2 T& a# E' y  }' I. e
初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写P1=0X55,就可以在IO口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。
2 [% M8 D. {! B" v+ k# `, U

/ w% r6 Y& H, l' r; L  r1 N寻址空间  \& m8 a& C% ]
什么是地址空间呢?所谓的地址空间,就是PC指针的寻址范围,因此也叫寻址空间。
/ r. o) {% L  l9 i2 c2 m1 }  l1 x: N% E2 s4 w1 s9 x9 D, Y; I
大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,PC指针就是一个32位的二进制数,也就是0xffffffff,范围只有4G寻址空间。现在内存越来越大,4G根本不够,所以需要扩展,为了能访问超出4G范围的内存,就有了64位系统。STM32是多少位的?是32位的,因此PC指针也是32位,寻址空间也就是4G。4 y0 N+ p# k. |- [/ B
! N. g2 T  B  n7 \4 U0 z( Q
我们来看看STM32的寻址空间是怎么样的。在数据手册《STM32F407_数据手册.pdf》中有一个图,这个图,就是STM32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫Memory map,用一个新芯片,就先看这个图。
  ~4 \; n/ H4 x3 {) W- [8 X4 [8 n" a9 r: O- s
微信图片_20240729152315.png $ C$ `& I, x  N) @
! Z/ q6 [( X( J; p

8 t  P9 V8 F- |& z' o2 `$ B( ~最左边,8个block,每个block 512M,总共就是4G,也就是芯片的寻址空间。3 ~* ^1 @5 F& D7 P% P2 Q

2 `$ R, c0 _4 \% U" `+ B7 G! _block 0 里面有一段叫做FLASH,也就是内部FLASH,我们的程序就是下载到这个地方,起始地址是0X800 0000,大家注意,这个只有1M空间。现在STM32已经有2M flash的芯片了,超出1M的FLASH放在哪里呢?请自行查看对应的芯片手册。2 c  u0 k6 Q0 t# M
" P$ b8 R' G/ Q
3 在block 1 内,有两段SRAM,总共128K,这个空间,也就是我们前面说的内存,存放程序使用的变量。如果需要,也可以把程序放到SRAM中运行。407不是有196K吗?
# o+ N6 U  }, p" s! _; l5 @8 b9 o2 A* g* W2 w7 g1 n
其实407有196K内存,但是有64k并不是普通的SRAM,而是放在block 0 内的CCM。这两段区域不连续,而且,CCM只能内核使用,外设不能使用,例如DMA就不能用CCM内存,否则就死机。% j& c( D8 q, q6 }
) f7 `# d5 u$ s
block 2,是Peripherals,也就是外设空间。我们看右边,主要就是APB1/APB2、AHB1/AHB2,什么东西呢?回头再说。& k: M1 T8 S* [

3 T: m. Q) h3 P4 dblock 3、block4、block5,是FSMC的空间,FSMC可以外扩SRAM,NAND FALSH,LCD等外设。3 A7 R' [) Z+ h0 B) W
; w  J  T; |/ A& ~7 O: `+ F
好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。对于这个疑惑,也可以看此文:代码是如何控制硬件的?在IO口输出的例程中,我们配置IO口是调用库函数,我们看看库函数是怎么做的。
7 J5 O9 i* y5 z& k) n  i, m2 S! o
! o- `+ s- F2 K8 k( x. G# ~  t例如:* S  G0 a3 Q  C/ V% H1 s
  1. GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
复制代码

  D1 f, }3 @7 e' ]这个函数其实就是对一个变量赋值,对GPIOx这个结构体的成员BSRRL赋值。& l" s' T( M1 R  U
  1. void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)" r3 B2 N0 R  U! ^8 H: F
  2. {
    ' G5 k/ y) `; P3 e& d+ i% r
  3. /* Check the parameters */
    $ _/ d* }( o5 ^- k& S" [
  4. assert_param(IS_GPIO_ALL_PERIPH(GPIOx));4 h8 b5 C6 v3 x  l6 o
  5. assert_param(IS_GPIO_PIN(GPIO_Pin));
    & @. I) D( K% {- J0 X

  6. ( V% H, H4 Z0 B# m& F+ Y( u
  7. 5 S* ?" ~& w* `. P  q8 V
  8. GPIOx->BSRRL = GPIO_Pin;
    " g, [$ T4 b: h/ C1 E
  9. }
复制代码
3 v8 `* p! u# ^: {+ p- C# L8 d9 l
assert_param:这个是断言,用于判断输入参数是否符合要求GPIOx是一个输入参数,是一个GPIO_TypeDef结构体指针,所以,要用->获取其成员
2 U! U  j- u/ q( w' c1 r6 }
. K: i" S5 D% ^% X* x( u
GPIOx是我们传入的参数GPIOG,具体是啥?在stm32f4xx.h中有定义。
$ O' ~$ |: l% M- D
  1. #define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
复制代码

  U0 k; g" G: b, Q* EGPIOG_BASE同样在文件中有定义,如下:. D$ l* u% q9 q. W2 n' S# p
  1. /*!< Peripheral memory map */# y1 H8 s- D! u( Y7 i9 L5 A+ [, a
  2. #define APB1PERIPH_BASE       PERIPH_BASE, D$ Z; ?/ n' v' o( X. Q2 w
  3. #define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)/ l  b, K) q5 d. @
  4. #define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000): i( w& e  K5 o# \9 Z8 D/ j
  5. #define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)
复制代码

" n2 k, ]" K1 M7 w; m* t再找找PERIPH_BASE的定义
3 W) ^1 O0 m8 F2 E- \
  1. #define PERIPH_BASE           ((uint32_t)0x40000000)
复制代码
到这里,我们可以看出,操作IO口G,其实就是操作0X40000000+0X1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是SRAM空间地址,0X40000000+0X1800是外设空间地址。" r( S; F4 z" \2 ~
  1. u32 i;
    8 J8 C8 X* w/ p
  2. i = 0x55aa55aa;
复制代码

! a  g6 m( C5 j/ X这个外设空间地址的寄存器是IO口硬件的一部分。关于如下图STM32的GPIO文章推荐:STM32中GPIO工作原理详解。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0X40000000+0X1800+0x14.7 I( e* }8 W6 r+ D2 a# D! Y& z
+ o6 O; g1 L# D: l" L9 P2 {/ [, ?
微信图片_20240729152319.png
9 U& c3 Y0 O: D% B( e: c. L

% J/ N- e2 X/ ~# D- G控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。) M& ~  u2 {; }6 W. t

2 t* m% G6 U. j2 o& w8 V) k寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或RAM。寄存器为什么能控制硬件外设呢?因为,初略的说,一个寄存器的一个BIT,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。: C; r4 j9 ~( |( [2 U4 [
0 n9 L- C+ [9 ~( \
纯软件-包罗万象的小程序
# F: ?7 r* [0 t6 a! k我们已经完成了串口和IO口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?关于程序是怎么在单片机运行的,也可以看此视频:动画演示单片机是如何跑程序的。代码到底放在那里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。
( ~3 _* j: @! \, E6 W' c5 l$ E+ C1 B: r1 }+ g
分析启动代码7 v% p4 E4 V0 B  }1 Q6 y! i
函数从哪里开始运行?
, X$ b7 i4 R' m3 }/ D3 \每个芯片都有复位功能,复位后,芯片的PC指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,PC可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在STM32中,复位到0X08000004。因此复位后运行的第一条代码就是0X08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。( {* e* {# h4 W4 Z0 W- N

0 T( ~! f( K% i# V* l芯片复位时,芯片从中断表中将Reset_Handler这个值(函数指针)加载到PC指针,芯片就会执行Reset_Handler函数了。(一个函数入口就是一个指针)
  1. ; Vector Table Mapped to Address 0 at Reset; Z: s/ h1 d1 R, @* \+ h. b
  2.                 AREA    RESET, DATA, READONLY
    . p  v" C6 \0 k
  3.                 EXPORT  __Vectors: `& G. W6 V# Q  a7 q
  4.                 EXPORT  __Vectors_End( d+ V2 z- X9 ~  d1 y/ s3 q8 D. W
  5.                 EXPORT  __Vectors_Size5 c) ]. b3 i3 [) T9 ^. V, W
  6. 7 G! N) n, ]5 s# W: J
  7. __Vectors       DCD     __initial_sp               ; Top of Stack# B2 o; _' q) T0 {, b1 s
  8.                 DCD     Reset_Handler              ; Reset Handler
    1 a$ ~& |8 r" Y
  9.                 DCD     NMI_Handler                ; NMI Handler
    8 |% \5 F* s6 B, r1 X
  10.                 DCD     HardFault_Handler          ; Hard Fault Handler5 z# t7 Z1 }& o+ }6 K
  11.                 DCD     MemManage_Handler          ; MPU Fault Handler- D, d) ]1 m. R6 N2 T0 X% n- R& E
  12.                 DCD     BusFault_Handler           ; Bus Fault Handler
    ( y9 h8 B/ P' b' C
  13.                 DCD     UsageFault_Handler         ; Usage Fault Handler
复制代码

: K, w( y( n2 [9 i+ uReset_Handler函数,先执行SystemInit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?. \, }  a4 y2 W* v9 ~4 X: O; N
! j9 i* q9 p, t4 A' Y3 W2 @
是我们在main.c中定义的main函数吗?后面我们再说这个问题。
$ w& u" A' t5 x2 |! s
) ?" v% @. ]4 c9 j4 x
微信图片_20240729152323.png
4 ^& q# P, ~6 S& I! @# w8 g

9 _4 _4 o" C8 J) G芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujique\prj\Objects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。7 _* b& c5 v3 ]0 ~

/ N( V/ @1 I9 K. e/ T, ?在MDK软件Options菜单Linker下有关于这个菜单的设置。- _2 U6 s$ @. J! m7 q

+ |8 U* h" ~  u8 ?* j( ]4 T
微信图片_20240729152326.png
: I1 m# k+ ^; b

. e& w$ f: `; t  c' z& {: [+ |* U, J* N4 O- j' V: w
/ o6 l; {5 T; x7 d8 q
把Use Memory Layout from Target Dialog前面的勾去掉,之前不可设置的框都可以设置了。点击Edit进行编辑。* Y/ {- t/ G% h4 m& }, a) n. q

1 e! k" P- |# @7 d
2.png

4 E4 G4 n, W* L8 s0 ]
: x' }0 P6 x* u' c2 H: D6 A  x

: K. `; g; Y9 |$ m" u4 U/ `( ~在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。! c  ~* Q6 F2 }2 P) d1 P6 m

$ ?' h0 a" f; A& A9 |其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定FLASH跟RAM的大小于起始位置,当我们把程序分成BOOT、CORE、APP,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到RAM中运行。+ ~2 ?" r5 g% u3 a" a) e
: |# f1 L' T6 w* i5 |6 B0 V+ i! t
3.png
9 R  [! m5 q% W: E* r/ [
( b! x, n: i  Z3 x
从这个基本的分散加载文件我们可以看出:, f  P" q' K5 N0 H

% s. @3 }+ @- t第6行 ER_IROM1 0x08000000 0x00080000定义了ER_IROM1,也就是我们说的内部FLASH,从0x08000000开始,大小0x00080000。
9 r1 k' ~  Y. z( D% ~
7 w% ]4 B- ~% s- b$ v) m第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。
' r1 c/ D, @5 G3 N/ G
2 Z* c; {7 a% q! B
第8行 *(InRoot$$Sections)什么鬼?GOOGLE啊!回头再说。
$ R* i6 |8 a; `. Z6 C& ]2 H2 L- o2 D. E* L3 x9 m# N( E
第9行 .ANY (+RO)意思就是其他的所有RO,顺序往后放。就是说,其他代码,跟着启动代码后面。
( W9 O) y, \7 y+ e7 B
5 U4 @+ I) w* o1 Y+ a
第11行 RW_IRAM1 0x20000000 0x00020000定义了RAM大小。: I4 |1 t( D! c9 R7 _# W

. G* M. v" e2 v6 P第12行 .ANY (+RW +ZI)所有的RW ZI,全部放到RAM里面。RW,ZI,也就是变量,这一行指定了变量保存到什么地址。
4 F3 s& u8 M, |6 U  Q9 m) A, s$ R3 _0 k
分析用户代码* O' E: M) [+ W- d! i
到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。
/ q5 ~' V5 ]2 s: B+ c$ O0 ~4 i0 }! y
1 程序跳转到main函数后:RCC_GetClocksFreq获取RCC时钟频率;SysTick_Config配置SysTick,在这里打开了SysTick中断,10毫秒一次。Delay(5);延时50毫秒。
  1. int main(void)
    3 C3 Q/ r* N- J+ @  x2 F/ t& p
  2. {
    5 V" d$ Z' B* j3 N
  3.   GPIO_InitTypeDef GPIO_InitStructure;
    4 ]/ F4 d. c( J4 g$ Q" P0 g

  4. . p( W4 v+ i& A# A) h3 I, q& I
  5. /*!< At this stage the microcontroller clock setting is already configured,# `* F5 z% l' }7 [2 v% w
  6.        this is done through SystemInit() function which is called from startup  D5 ^- a7 G6 r: C& C; l1 W
  7.        files before to branch to application main.
    , t5 ~5 |& a3 C+ d- i8 Q$ k- W) a. o- `
  8.        To reconfigure the default setting of SystemInit() function,
    : ]& i. a4 }/ q, B- s$ T  w' \! p
  9.        refer to system_stm32f4xx.c file */
    9 T+ o2 e, h6 p

  10. 2 ^: P6 @1 Y- G: Y" @6 R: A
  11.   /* SysTick end of count event each 10ms */
    + Y+ A3 a% c4 K, A
  12.   RCC_GetClocksFreq(&RCC_Clocks);. O& p4 t! m# e, L& d3 L
  13.   SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);
    0 O+ o' Z5 r. P( a& c" x8 {% d1 q

  14. . ?2 n4 B0 [/ j, E
  15.   /* Add your application code here */
    $ e4 U8 Z3 a0 t+ Y0 Y& e
  16.   /* Insert 50 ms delay */& w$ f* S  ~9 A  y, H9 J# H
  17.   Delay(5);
复制代码
' q" k; W0 _) R; u1 q
2 初始化IO就不说了,进入while(1),也就是一个死循环,嵌入式程序,都是一个死循环,否则就跑飞了。
  1. /*初始化LED IO口*/% h* q- Y' A+ w7 `% V& O
  2. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);
    ' Y$ P  @8 G+ V2 q% e

  3. 6 \% o4 L6 g4 }1 X( l/ r' s0 P
  4. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;
    3 f1 x! p" ^% M6 B
  5. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    1 s" c9 X7 e% O) H, o  V7 R

  6. # J" x+ }% `7 _: o& m
  7. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;2 ^: X; i1 o; R; Y: W$ }
  8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    0 ^4 ?4 X! i& z* K3 s4 f7 ]' c2 F
  9. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;) z% q9 R2 i: }! _
  10. GPIO_Init(GPIOG, &GPIO_InitStructure);   
    - K, x- _/ c* P0 ?' p
  11. * K* P+ |8 ]) h9 K
  12. /* Infinite loop */
    3 |" _7 h' C. D  X* i- W5 h) @
  13. mcu_uart_open(3);
    5 O  M/ }' E: h* I3 y* Y
  14. while (1)* v8 D  x& Y& O& G
  15. {
    / v; s! E; ~. s  |
  16.   GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);' e- p1 E1 u/ k4 Q" ~* R+ ?
  17.   Delay(100);8 n% }6 T. p" `# I: s0 _3 u; `
  18.   GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);4 y0 M+ p  i0 e/ H* Q8 k
  19.   Delay(100);8 G6 e& S3 f  B" X+ q  N/ v
  20.   mcu_uart_test();  X6 `$ U/ z6 `; h5 o; N) V7 p9 K

  21. ! u) v9 E4 V" z+ O' Q
  22.   TestFun(TestTmp2);' |+ i0 \$ t. j0 n
  23. }
复制代码

7 j1 X/ V( ~$ F4 ~' S8 U8 G0 W3 在while(1)中调用TestFun函数,这个函数使用两个全局变量,两个局部变量。
  1. /* Private functions ---------------------------------------------------------*/- X3 y/ t; O. P# b$ U% H2 e, J* ~
  2. u32 TestTmp1 = 5;//全局变量,初始化为5
    $ \$ |9 u0 I* l4 h9 k: ?6 R, ]& D
  3. u32 TestTmp2;//全局变量,未初始化3 R' k, u& a( s

  4. 0 c0 t; N: d# ^1 T
  5. const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};1 B% a" o: m; V: c4 o
  6. 8 ?  ^4 r+ a8 q) L8 C3 ]0 A/ }+ Q* G
  7. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值. Y8 X" P+ M8 P1 q2 x
  8. {
    ; m; p& V. C5 c2 w# Y" {. m
  9. u8 test_tmp1 = 4;//局部变量,初始化: @& n* n* r5 k2 h
  10. u8 test_tmp2;//局部变量,未初始化
    2 [5 Y2 p. Y& ]
  11. 5 q  q5 E( F( Y( M" x
  12. static u8 test_tmp3 = 0;//静态局部变量
    9 C- `8 p& Q+ o) X& b

  13. 1 ]( @) b. j% C1 L. q8 _6 |1 h) q
  14. test_tmp3++;$ m, n3 Q- u: _3 L* p1 E6 C

  15. / O# O: H* W! R/ M7 [4 Y1 m
  16. test_tmp2 = x;( c8 w4 G' h6 M9 S; x0 t' P; ]. \9 N; c
  17. 4 C5 V3 D% o  `, b- B8 X& k6 u
  18. if(test_tmp2> TestTmp1)4 h0 [; }- X0 t0 q
  19.   test_tmp1 = 10;
    ! ~- X3 W0 w& D0 y7 N! e5 j
  20. else8 t/ }' t  U1 `% A* y7 H: ?, R
  21.   test_tmp1 = 5;
    2 f5 o9 v2 u& J! e3 K$ e

  22. ; r7 m" ^( l0 _" T, i' ~# P
  23. TestTmp2 +=TestTmp3[test_tmp1];# x+ N8 o& l1 J
  24. , M1 x# _  z. `* o9 k/ x5 p
  25. return test_tmp1;/ ?" x" Z& b) g5 Q% x+ ~( _
  26. }
复制代码
, M5 T  Z) A1 E8 y& v0 u) w
然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。相关文章:STM32中断系统。我们查看Delay函数,uwTimingDelay不等于0就死等?谁会将uwTimingDelay改为0?
  1. /**
    ( f# x  l$ W9 B, b; i
  2.   * @brief  Inserts a delay time.! K% y$ _: b0 M* U
  3.   * @param  nTime: specifies the delay time length, in milliseconds.
    ) G# H  R3 l4 Z# |6 }
  4.   * @retval None
      E9 I2 f2 _* v3 U  f% e6 k
  5.   */
    $ y& P, J, e/ x) b3 z3 t% `" o
  6. void Delay(__IO uint32_t nTime)% Z8 z( }% J6 [4 b
  7. {  ~- ]6 D* ]% n. o; B5 A$ c$ u
  8.   uwTimingDelay = nTime;
    ! D# u( L3 \7 Y4 q

  9. % z1 ]6 H' ~6 l$ M; l, N
  10.   while(uwTimingDelay != 0);
    8 ?2 Q0 y6 ~2 M5 b9 y8 Z# O2 J
  11. }
复制代码

* `% Y+ r# j# N8 R搜索uwTimingDelay变量,函数TimingDelay_Decrement会将变量一直减到0。
5 o+ }8 _1 V2 \5 A
  1. /**
    2 u+ x7 T" w9 C9 @  D3 _
  2.   * @brief  Decrements the TimingDelay variable.4 {- d$ ^. ?7 s9 D7 S
  3.   * @param  None- p2 H0 I0 ~4 e. {+ E6 H3 g% U
  4.   * @retval None
    7 m& q+ s; }' Q
  5.   */
    6 H& J, D+ f) e# v0 j  K, @
  6. void TimingDelay_Decrement(void)
    8 J. w) w; p8 U* v
  7. {1 P3 G  f( S/ z# \2 o4 Z
  8.   if (uwTimingDelay != 0x00)' i* L% C$ q1 F6 I4 `& }
  9.   {
    + U, f8 e1 M/ N6 A: Q: a4 d
  10.     uwTimingDelay--;: i$ J: J/ m" R! ^' A5 f4 @
  11.   }
    $ R( q. G4 m4 f# I3 S
  12. }
复制代码
2 u! d- I7 K" J  E' Y
这个函数在哪里执行?经查找,在SysTick_Handler函数中运行。谁用这个函数?
  1. /**; L6 g: l4 ?5 E: I9 b
  2.   * @brief  This function handles SysTick Handler.& G9 ]: Q2 Y9 h2 P! w
  3.   * @param  None) [1 @7 u8 p: }, b
  4.   * @retval None9 _6 m- V- G+ B& u9 G
  5.   */  u; V! I% z% |' D7 R9 L
  6. void SysTick_Handler(void)1 o4 F4 ]$ L3 S  I: N2 R
  7. {
    ! Q, C. s% @8 L
  8.   TimingDelay_Decrement();
    4 [- M* J% D1 V9 x# ?. t; ^
  9. }
复制代码
" w  j3 ]& s( V# T, [
经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。
  1. __Vectors       DCD     __initial_sp               ; Top of Stack
    # P7 {3 A6 ^( G) T+ d
  2.                 DCD     Reset_Handler              ; Reset Handler
    * V- H3 C3 [( C- a  c; q
  3.                 DCD     NMI_Handler                ; NMI Handler" X( R2 f& S- l/ u
  4.                 DCD     HardFault_Handler          ; Hard Fault Handler7 ~- B& t! d6 [6 [6 {
  5.                 DCD     MemManage_Handler          ; MPU Fault Handler
    5 s' Y/ a% V8 _: Q
  6.                 DCD     BusFault_Handler           ; Bus Fault Handler* i; L/ Y7 {' Z
  7.                 DCD     UsageFault_Handler         ; Usage Fault Handler
    , C0 C% d3 _& y& K) ?5 K
  8.                 DCD     0                          ; Reserved2 z0 k$ Q" I6 R0 g
  9.                 DCD     0                          ; Reserved
    7 ?7 l1 k4 i% T+ s
  10.                 DCD     0                          ; Reserved' m: y* M0 T# `& L
  11.                 DCD     0                          ; Reserved# V! n* q3 m/ r- B, B
  12.                 DCD     SVC_Handler                ; SVCall Handler7 ^; q8 p+ j; y; F- n3 L
  13.                 DCD     DebugMon_Handler           ; Debug Monitor Handler
    2 T$ R6 h- I4 n
  14.                 DCD     0                          ; Reserved
    & e/ {" |& Q" R. n: Y
  15.                 DCD     PendSV_Handler             ; PendSV Handler* F, L, B) J! y# V1 P
  16.                 DCD     SysTick_Handler            ; SysTick Handler
复制代码

, v, i, |# z9 I" M9 }$ s, K' Q: v余下问题
9 z2 ?0 E6 {7 h# o* N# \1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(InRoot$$Sections)是什么?3 ZI段,也就是初始化为0的数据段,什么时候初始化?谁初始化?
. L0 C; \2 k: m$ S: R' s
( z+ p1 O6 G$ Z4 @6 X3 E9 X. U
为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!. Y  S( W6 Q5 j+ [7 @$ D' E

  u& o+ A: V+ _7 Z$ Y
. ^. c! {9 W1 e/ Y; d. i
通过MAP文件了解代码构成
# }& H# _- D! c: ]' V% K, H7 o, V% W编译结果

1 L) g7 Z2 z! T8 K* a8 B1 Q6 x* N程序编译后,在下方的Build Output窗口会输出信息:
  1. *** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'0 \- G# L' l! ]  w) S
  2. Build target 'wujique'
    : @0 ~% J9 k3 s/ _( A8 N: k$ |
  3. compiling stm32f4xx_it.c..., t6 k+ J! D% @  A7 e" w9 J
  4. .... L, G7 F1 c* a2 n
  5. assembling startup_stm32f40_41xxx.s...' P2 z$ x" g0 |0 q- T4 Y
  6. compiling misc.c...- x) ]5 s1 ~9 W3 a- m% y
  7. .../ ^6 H4 }1 T7 [7 V) \
  8. compiling mcu_uart.c...
    & a4 v+ V  c. ^8 a
  9. linking...+ y; \  Z1 K7 `+ U8 L& W: _; o( Q5 a
  10. Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  / g: X- Y7 F$ c
  11. FromELF: creating hex file...
    3 v1 h$ K2 M( ~# P/ [  Q
  12. ".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s).
    , I/ S) u, I; G' b) B
  13. Build Time Elapsed:  00:00:32
复制代码

1 S+ G" C, s# G) x0 a# q编译目标是wujique
$ d$ W0 h- r# s5 D1 kC文件compiling,汇编文件assembling,这个过程叫编译1 o% I& t6 C6 L6 B
编译结束后,就进行link,链接。2 a) ~! @: t% |& m. ~8 \0 h
最后得到一个编译结果,9038字节code,RO 990,RW 40,ZI 6000。CODE,是代码,很好理解,那RO、RW、ZI都是什么?
6 U1 a/ J+ j5 G; m, [* FFromELF,创建hex文件,FromELF是一个好工具,需要自己添加到option中才能用  I0 e  C- t) N( a. O5 c7 W
  x+ c4 O8 ]  y. K2 z% u- Y5 U7 |
map文件配置
: y  o0 l" a6 k& p更多编译具体信息在map文件中,在MDK Options中我们可以看到,所有信息都放在\Listings\wujique.map- ^4 z% I7 r% D7 X' u# p0 c  A
! @: S, ?& E4 H& r  E$ b- t; M
默认很多编译信息可能没钩,钩上所有信息会增加编译时间。" G; [$ F* q) g+ }- s  u8 i

/ X6 U8 |# m6 i' }
4.png $ m7 x, S+ w  |
9 S- L; {% s) B7 z# i' Y; C& \! R" N
map文件

; c7 e) d( m3 ]# j( ^打开map文件,好乱?习惯就好。我们抓重点就行了。3 X4 @) `/ }, n. a7 O
1 i6 i& p4 G3 k( q& n: V' @
5.png
" F. q6 {  [5 Q' X. \$ {2 L
- O, k' i7 |" X* o
map 总信息
: Y2 d  @) u' d
5 N/ s/ `3 c& z从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。, H8 J5 ?, F/ h/ i; Q$ H
; Z% U6 ?2 s: s6 ^
有多少RO?RO到底是什么?/ `5 |9 H5 X5 P+ N2 S7 b5 z+ Y( a

$ o. U, t4 U. C8 K! K$ f有多少RW?RW又是什么?& h+ i) w% s) T6 \0 U
$ s9 v+ O6 T. B! @" E2 l! Y5 J+ F
ROM为什么不包括ZI Data?为什么包含RW Data?: ^; d. k3 _0 K& K8 I; L" P
# _3 t5 m7 d; N- a
6.png
4 y3 q+ {. l. W( \Image component sizes
) j  V( }1 e8 v# R$ Y* @- n* m: B( C# F- w. p8 f, c
往上,看看Image component sizes,这个就比刚刚的总体统计更细了。
% n; y& g" a# U7 Q- G' B1 O/ F* F- M( O. {- B, w2 l6 n0 T
这部分内容,说明了每个源文件的概况
' d6 L/ {' p# |+ L, B5 I7 j. N$ f; R

' i6 r8 T3 \) D/ a& x( J$ ~首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些STM32的库文件。1 O4 b' T& F; M* I0 G' Q& z
  I: j7 v5 y! t) w( X
7.png
* |# K0 q$ p- S( m( C( S3 x
) r; b3 t: P8 `+ l* Q: V6 J, N
第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1M flash上的程序压缩到能在512K上运行。2 R; w* Y! z( h, e$ C" {7 H. b5 O7 A
& w2 C0 w  Z6 `( ]
8.png
7 a6 b% b" C1 B* j2 G+ |

0 _* O6 |) j( }  _( X, n- s第3部分也是库,暂时没去分析这两个是什么东西。. r6 c1 s# m1 ~6 u3 h* c

* ]$ n6 P  y" ^" Q) V" q
9.png
/ R- g, \2 t; f1 ^: s5 |
7 X7 [) l- u' e
库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:9 v6 k8 Y3 K. N" o% `
#include <stdarg.h># O. I6 e- A) J8 Q* F4 t
#include <stdlib.h>7 I: h& [  \9 L% x" }
#include <string.h>    H. P7 H4 x1 s0 v) q3 B+ n

9 Y- m  ^: V* u
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。
/ c0 p, x2 U, o/ u/ c1 X" @0 J
: [% I$ O& M! }, u/ @* H
( z! v* Y: W) y, y
文件map- p, r9 h2 {: O# s) h  ~
再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET) r4 \9 o9 @, C: I; A( J0 i
  H! X+ B' N  L  |/ T; @
库文件是什么?3 L3 _7 ~( C  Q$ ~# R* C
库文件就是别人已经别写好的代码库。
( [+ I* J! \5 n8 v
; g6 e" o) j- @! F# ?在代码中,我们经常会包含一些头文件,例如:
& C, w9 u1 J1 s
  1. #include <stdarg.h>
    8 |  U8 c, t3 P; X
  2. #include <stdlib.h>; W- G1 Y3 ?6 l5 e! A' j) w
  3. #include <string.h>
复制代码

, D' _1 D3 |! V0 m6 y: @$ K这些就是库的头文件。相关文章:C语言中的头文件。这些头文件保存在MDK开发工具的安装目录下。
9 ~& {- Y* i) }' S2 T- c8 w, @& ^" t% n
我们经常用的库函数有:memcpy、memcmp、strcmp等。
4 }, ~0 T& [1 z3 Q: v1 W/ z, m$ Y' `4 U$ \. c
只要代码中包含了这些函数,就会链接库文件。
, b, L7 V: o, I( W7 z& n' }9 `' U' E. M( }( _
文件map
" V! C2 D6 h* o再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET
$ j, I. S; H7 k1 l! m3 S3 W) c: c9 U" W/ x9 `0 H9 {& `
10.png

+ v8 j& O" C; A5 }" R2 c

: F- G* }0 w7 Y1 o+ u; Q每个文件有有多行,例如串口,4个函数。. Y, Z8 y% h, s' x- J
$ t( e+ W2 t7 ?( R4 N& I# W' q) W
11.png

- X7 j7 y# t/ m/ u3 j
3 z: X- r3 a3 X' C  r
然后是RAM的,main.o中的变量,放在0x20000000,总共有0x0000000c,类型是Data、RW。串口有两种变量,data和bss,什么是bss?这两个名称,是section name,也就是段的意思。看前面type和Attr,
8 K; v8 h) n2 D: e! p2 A" L- `, A' L9 g8 ]4 I) O% q
RW Data,放在.data段;RW Zero放在.bss段,RW Zero,其实就是ZI。到底哪些变量是RW,哪些是ZI?$ ?9 t  {5 H& P( V& V
; Z! N2 ?8 t8 {4 W: |6 F2 v+ P! C
12.png

# D; a! m% {" Y& \% T
2 `9 g: k/ U1 k* D6 }! {& Y. n
Image Symbol Table' V+ u/ }4 o& e1 \5 d% d

: a, ~: F* f5 x7 T* W% R. g& [. q再往上就是Image Symbol Table,就更进一步到每个函数或者变量的信息了" H  o- X( X0 ]9 X! S
/ \( M3 f; o/ P2 x2 a7 a) p# N
13.png
4 q) S7 X( W2 j5 F* r$ L
7 y5 o7 s+ [  R. W8 M" f
例如,全局变量TestTmp1,是Data,4字节,分配的位置是0x20000004。3 ?3 X6 r& y5 o! Z# z" s
3 N4 I' I3 u/ D- A/ \9 g
15.png

" F- C+ y* j( |$ Q% C2 ^3 ^% k

- S( d4 ^7 [, U+ g4 A) lTestTmp3数组放在哪里?放在0X080024E0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几K几十K大,不可能也没必要放到RAM区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。& e) u" t0 X& n* N/ T5 G# r

; Z2 A- e7 }* z! i' u7 w, Rconst的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习
; K  l7 n4 }3 a% w9 ~: Q3 B3 O$ `- H; [5 V: q8 Z8 L: J1 g, H
16.png
% X" g% n- T9 Y0 b
& o# U, o0 c2 z0 H
那局部变量存放在哪里呢?我们找到了test_tmp3,
+ o( C) s- m+ l8 @9 A' ~
4 B8 b  H% W! O) i, ^
17.png

% r0 }0 `: b8 W% G
1 U; F8 L/ @7 y9 Y
没找到test_tmp1/test_tmp2,为什么呢?在定义时,test_tmp3增加了static定义,意思就是静态局部变量,功能上,相当于全局变量,定义在函数内,限制了这个全局变量只能在这个函数内使用。哪test_tmp1、test_tmp2放在哪里呢? 局部变量,在编译链接时,并没有分配空间,只有在运行时,才从栈分配空间。
  1. <blockquote>u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
复制代码

0 N' u4 s6 m- r% M! d+ M! r上一部分,我们留了一个问题,哪些变量是RW,哪些是ZI?我们看看串口变量的情况,UartBuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文Block Started by Symbol的简称。
- r. [( u  A! z1 w/ I2 g* Y* B; p9 k3 {& M/ E8 _+ `
18.png
& G- C$ U# G0 H1 a7 s

! Y3 D' A) z3 D: v' U9 n7 A5 S到这里,我们可解释下面几个概念了:5 m" A# ]9 b8 T/ |8 ?
Code就是代码,函数。
! |1 I, j9 [) w5 |RO Data,就是只读变量,例如用const修饰的数组。0 W  R5 {3 ^. w3 s5 X. t  i6 |
RW Data,就是读写变量,例如全局变量跟static修饰的局部变量。0 g: \$ b0 `) X, k  l
ZI Data,就是系统自动初始化为0的读写变量,大部分是数组,放在bss段。
* P6 o6 Y4 @$ s; G, HRO Size等于代码加只读变量。8 S  q% V) T5 e* y2 R7 N9 Q& N+ w
RW Size等于读写变量(包括自动初始化为0的),这个也就是RAM的大小。
; v7 \$ n4 t( S& p1 [ROM Size,也就是我们编译之后的目标文件大小,也就是FLASH的大小。但是?为什么会包含RW Data呢?因为所有全局变量都需要一个初始化的值(就算没有真正初始化,系统也会分配一个初始化空间),例如我们定义一个变量u8 i = 8;这样的全局变量,8,这个值,就需要保存在FALSH区。
* _# P# Q5 Y% B, R& l4 ^
9 |6 }. l7 ^( B: R
19.png

8 S% Y' t; L) W5 K

; b7 {' s& s5 U, L* C我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579
! c* p8 L, V/ J: \% O- u+ e- b; y2 U* L- g% x) V7 c
20.png

# O1 H5 f$ C! J% l/ _
/ u9 t+ \- O- X4 i' B; V) t8 }
main是main,放在0x08000189* h+ y# ~& i! p9 V; n, V, E  Z6 P

6 U' a1 n# r1 R
21.png

& u7 e# x5 p" r  T& x6 R  C
8 W% M! u- H) h; z
__main到main之间发生了什么?还记得分散加载文件中的这句吗?
! y8 l- F( T. [- z: h*(InRoot$$Sections)& c/ @2 j: l2 N  I
' E/ J( I' j2 k7 N* M
__main就在这个段内。下图是__main的地址,在0x08000189。__Vectors就是中断向量,放在最开始。
4 H( r. \4 |/ I6 G; F- A
4 M0 F6 i% L* v" B( ~0 t
22.png

3 U- Z3 g0 B/ H' K& @! m1 M. {8 Z! d7 L
在分散加载文件中,紧跟RESET的就是*(InRoot$$Sections)。' C- Z' `* u1 d$ t/ k6 n

  b3 \+ B+ f/ L1 K2 g% r
23.png
# \+ r' K- R% S
) l9 M5 }8 r# W, Y7 c) j
而且,RESET段正好大小0x00000188。! X! z# H! E) |
0 \" I5 {0 j* O9 E! x% k
24.png

( W9 ^& S0 c  @+ `
" g- R' T) ~! _
巧合?可以参考PPT文档《ARM嵌入式软件开发.ppt》。
  m7 C* A* n7 q8 f5 b, v
2 n* t# b" C# b, \
25.png

/ B' w5 U7 L; @4 F! j
2 J% ^/ T  B; Q  `- b; W' o
这一段代码都完成什么功能呢?主要完成ZI代码的初始化,也就是将一部分RAM初始化为0。其他环境初始化……
2 ?- f/ J3 Q' N4 @: g2 R4 T7 H! W' y9 h2 k8 y

- w' p: L. f5 W& Y& Z! W( }最后
: a: c) H2 o4 Z+ Q- C1 V到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。; \8 s! s! b( {2 F  ^" p+ ~) r

9 `3 K- }2 \; X' V. w; v

: D( J3 a" Y+ p$ R4 G转载自: [color=var(--weui-LINK)][url=]STM32嵌入式开发[/url]
8 @1 l2 D% F5 e0 E" N/ `如有侵权请联系删除; n  U7 z7 q9 s2 i! {5 |
  a4 R" |" s9 M/ Y  [6 `  {; @

8 c" Q- \0 a8 S" V
收藏 评论0 发布时间:2024-7-29 15:25

举报

0个回答
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32Cube扩展软件包
意法半导体边缘AI套件
ST - 理想汽车豪华SUV案例
ST意法半导体智能家居案例
STM32 ARM Cortex 32位微控制器
关注我们
st-img 微信公众号
st-img 手机版