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

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

[复制链接]
攻城狮Melo 发布时间:2024-7-29 15:25
本文分析STM32单片机到底是如何软硬件结合的,分析单片机程序如何编译,运行。1 K) E% d0 K- G2 h

8 s# ~1 h3 U2 Q( m! p软硬件结合$ @7 j% y3 I" v
初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写P1=0X55,就可以在IO口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。( A$ Y% P* y# l! m
1 s6 f; w4 V5 H- n; O
寻址空间. f3 D/ P) @  W- f
什么是地址空间呢?所谓的地址空间,就是PC指针的寻址范围,因此也叫寻址空间。
# C: O8 ^  F% q7 p( Q! m$ X
7 c2 C- M" e, K5 z. ~/ S大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,PC指针就是一个32位的二进制数,也就是0xffffffff,范围只有4G寻址空间。现在内存越来越大,4G根本不够,所以需要扩展,为了能访问超出4G范围的内存,就有了64位系统。STM32是多少位的?是32位的,因此PC指针也是32位,寻址空间也就是4G。8 M0 m- f( h2 s: J
" X  L1 A- Y, v/ g5 y% ~
我们来看看STM32的寻址空间是怎么样的。在数据手册《STM32F407_数据手册.pdf》中有一个图,这个图,就是STM32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫Memory map,用一个新芯片,就先看这个图。" v; M; r5 m1 J9 h1 P+ m, J: S" O
% R! ?% J  z# s# f+ p7 |
微信图片_20240729152315.png ! a  {/ @( @0 d+ _0 w& ?

& z2 j: y3 i  x
7 A( W2 S7 t0 Q8 h7 p) o最左边,8个block,每个block 512M,总共就是4G,也就是芯片的寻址空间。
5 z1 ~; J! I7 }3 I
/ b& x' h9 [% fblock 0 里面有一段叫做FLASH,也就是内部FLASH,我们的程序就是下载到这个地方,起始地址是0X800 0000,大家注意,这个只有1M空间。现在STM32已经有2M flash的芯片了,超出1M的FLASH放在哪里呢?请自行查看对应的芯片手册。0 z$ g1 O7 `. K1 q
$ V2 L& U9 V% z# m! l$ _8 H* k3 g
3 在block 1 内,有两段SRAM,总共128K,这个空间,也就是我们前面说的内存,存放程序使用的变量。如果需要,也可以把程序放到SRAM中运行。407不是有196K吗?
0 q  F; d2 G$ p4 N$ Z5 B! C/ b% G6 t3 T& W3 K, ^7 \
其实407有196K内存,但是有64k并不是普通的SRAM,而是放在block 0 内的CCM。这两段区域不连续,而且,CCM只能内核使用,外设不能使用,例如DMA就不能用CCM内存,否则就死机。
$ J5 o/ G5 h( W+ {: P. |+ q. O, M
' ~. N" @* @. [* l" ~block 2,是Peripherals,也就是外设空间。我们看右边,主要就是APB1/APB2、AHB1/AHB2,什么东西呢?回头再说。$ U3 X: J  p+ a' S5 I: `; h

+ w) x: M& n, `' r9 D' L  ^" {block 3、block4、block5,是FSMC的空间,FSMC可以外扩SRAM,NAND FALSH,LCD等外设。( i( c# I$ U# E3 w0 y
% ?# x9 P& ~9 J1 H) ]
好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。对于这个疑惑,也可以看此文:代码是如何控制硬件的?在IO口输出的例程中,我们配置IO口是调用库函数,我们看看库函数是怎么做的。0 c& ]' j9 S6 f8 l( T3 h, u' D
: f+ _* b, `" T% k! U
例如:& Q, s( ^& r5 C4 g( M4 {
  1. GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
复制代码
6 e6 r/ W* f3 I9 a- e
这个函数其实就是对一个变量赋值,对GPIOx这个结构体的成员BSRRL赋值。
; t7 d0 p4 J# t  r7 [
  1. void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)5 H+ n9 C$ U" W
  2. {
    / X2 Z+ f. k$ \7 C1 v+ f
  3. /* Check the parameters */
    7 F$ y3 D; d  _! _0 R
  4. assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
    7 W5 `2 L2 q( K" a/ A3 J7 f
  5. assert_param(IS_GPIO_PIN(GPIO_Pin));
    * `5 G  K5 n: c# y+ X5 a, n% c
  6. 9 |" ~7 I( o3 E5 [; _$ O

  7. $ b, M6 e8 k+ H* r
  8. GPIOx->BSRRL = GPIO_Pin;
    & n3 v6 ]2 ?/ F- l/ Q! ^4 d! O
  9. }
复制代码

* i/ ?% D- ^+ c$ @% passert_param:这个是断言,用于判断输入参数是否符合要求GPIOx是一个输入参数,是一个GPIO_TypeDef结构体指针,所以,要用->获取其成员
( s  j* F9 ^" v2 J) `& D2 H: `) l
/ z. J) {& W; |2 A
GPIOx是我们传入的参数GPIOG,具体是啥?在stm32f4xx.h中有定义。1 }/ E4 x/ W- G4 |# F
  1. #define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
复制代码

& {# O' e% ?$ s2 xGPIOG_BASE同样在文件中有定义,如下:
  v3 \! a5 C8 T# i
  1. /*!< Peripheral memory map */0 f1 L0 g. N' h+ Q$ R2 W8 u
  2. #define APB1PERIPH_BASE       PERIPH_BASE
    1 }& K, D$ y; x% x3 v' U. s2 x& J
  3. #define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)& s! P, G: @6 }/ r' E0 [) i
  4. #define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)7 c3 m3 U2 Y2 |
  5. #define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)
复制代码

9 Q( \( x+ H8 K* ]+ d9 ?再找找PERIPH_BASE的定义; k; d2 Y% D8 r7 @
  1. #define PERIPH_BASE           ((uint32_t)0x40000000)
复制代码
到这里,我们可以看出,操作IO口G,其实就是操作0X40000000+0X1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是SRAM空间地址,0X40000000+0X1800是外设空间地址。3 Q* j, {. b/ Z
  1. u32 i;8 O) P$ O8 f2 v5 D9 i8 ?0 G
  2. i = 0x55aa55aa;
复制代码
. ]( a* B5 E; g) p9 `. y
这个外设空间地址的寄存器是IO口硬件的一部分。关于如下图STM32的GPIO文章推荐:STM32中GPIO工作原理详解。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0X40000000+0X1800+0x14.
  a0 N/ g$ j& S! n
! o" }! M! @  Y, L0 l2 i6 {0 q$ _) |  u
微信图片_20240729152319.png
5 P, w' S3 H5 p' }/ \/ J
/ c- T* T- |% s  Y, a. O
控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。/ L% v2 q" ]' i3 {) A8 u2 P4 _9 W9 `/ ^
; O( E3 v' Z8 `6 d
寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或RAM。寄存器为什么能控制硬件外设呢?因为,初略的说,一个寄存器的一个BIT,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。( n* G2 N, @5 V+ d/ R
* S. \) P$ g3 ?" j. N! S% N
纯软件-包罗万象的小程序1 Q% R* b2 C$ X  b' s5 w, Y+ |
我们已经完成了串口和IO口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?关于程序是怎么在单片机运行的,也可以看此视频:动画演示单片机是如何跑程序的。代码到底放在那里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。
* W* ]: \2 }0 v, ^$ g+ s
! |9 {8 u9 |  s( B9 G6 J分析启动代码8 g' L$ ^7 e1 O6 A3 E
函数从哪里开始运行?
4 U* a, u. E/ t" a: P每个芯片都有复位功能,复位后,芯片的PC指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,PC可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在STM32中,复位到0X08000004。因此复位后运行的第一条代码就是0X08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。: z* d/ [0 \( C% ^! f8 d  G" x

+ t( k  o$ q% G- G; i, y" r! q: s芯片复位时,芯片从中断表中将Reset_Handler这个值(函数指针)加载到PC指针,芯片就会执行Reset_Handler函数了。(一个函数入口就是一个指针)
  1. ; Vector Table Mapped to Address 0 at Reset
    ) Z, K- x3 l5 w" n1 m( Z
  2.                 AREA    RESET, DATA, READONLY
    ; H/ f# Y! o, P: E# h$ L' K0 V
  3.                 EXPORT  __Vectors
    " H+ ?7 g# U9 n0 r1 i3 K& _3 s8 v# q
  4.                 EXPORT  __Vectors_End
    + Y+ I8 r' L# w# l! A
  5.                 EXPORT  __Vectors_Size3 p  C& P2 ?5 T4 U$ f1 Z7 D
  6. : `( b. Q. Y3 V) x7 L  C! T
  7. __Vectors       DCD     __initial_sp               ; Top of Stack
    ) N5 f4 j" k/ |4 M" C
  8.                 DCD     Reset_Handler              ; Reset Handler
    ! G  r' R1 o  q) y% V6 K! W
  9.                 DCD     NMI_Handler                ; NMI Handler
    . }* C: s; I7 k& V1 G
  10.                 DCD     HardFault_Handler          ; Hard Fault Handler+ o- V) R# ]$ T* \' H' O
  11.                 DCD     MemManage_Handler          ; MPU Fault Handler' E! E5 X3 u0 c0 P4 ~
  12.                 DCD     BusFault_Handler           ; Bus Fault Handler3 S' F9 j' U: T% a5 z5 ~. L' s0 y+ \
  13.                 DCD     UsageFault_Handler         ; Usage Fault Handler
复制代码

7 i3 U4 y& |4 rReset_Handler函数,先执行SystemInit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?
0 w! O' W, T' d9 {2 ~) d0 g: l8 T* `
是我们在main.c中定义的main函数吗?后面我们再说这个问题。# v2 {% a) X9 q7 o8 |9 R

2 ]9 ^1 d+ e" o4 _
微信图片_20240729152323.png
. E& A7 V4 C3 A" j
' Z7 ?* A) z/ ]
芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujique\prj\Objects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。
; ^7 \6 V0 q. E- @. O, C8 p8 h$ D
" A' ]: H6 `, R  O! z
在MDK软件Options菜单Linker下有关于这个菜单的设置。- @9 X& t1 J! @% v* `, f5 G9 v" n
6 W1 Z  g+ x3 b3 f& W- ~. }" Z- t
微信图片_20240729152326.png * Q* b( Q  W: g/ o2 F/ Q% Q

* K. ]( _( E$ w
# O- r  Y8 a3 |2 B, C4 f
* d5 o5 K" P& s) c0 k8 k" l9 H
把Use Memory Layout from Target Dialog前面的勾去掉,之前不可设置的框都可以设置了。点击Edit进行编辑。# ]4 V- X1 t4 j

$ y' G( |8 F/ L) q
2.png

- b8 P) D. u3 E9 g- Q: F  A, K

. s! l* X- q8 U2 _; R" r
: V7 m$ ]9 i3 p9 |2 |% P
在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。
& W. l! v9 P- J/ Y" o0 D. O
2 `  Y2 L9 E6 ?$ j* @# k其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定FLASH跟RAM的大小于起始位置,当我们把程序分成BOOT、CORE、APP,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到RAM中运行。  N/ f6 D& K' S- |+ t0 c
5 U2 k" Y! y7 S- Z4 B" f
3.png
$ Z) N0 h" e* V5 p4 J: D! {

7 F, Q) Y7 N7 O/ z( u' U( P从这个基本的分散加载文件我们可以看出:4 M9 B" X8 m  j0 H! J$ T- l

8 ]5 @# u) A0 F& ]1 ]1 B% s第6行 ER_IROM1 0x08000000 0x00080000定义了ER_IROM1,也就是我们说的内部FLASH,从0x08000000开始,大小0x00080000。  b$ \# I1 B: P

! t! Q% p9 N. ?1 J3 ~4 V3 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。
. Q+ a# V$ C9 U
8 n& j) I7 \  l( d. h5 W
第8行 *(InRoot$$Sections)什么鬼?GOOGLE啊!回头再说。
3 \3 E7 f# k. L1 a" n% Q3 J1 u& ?! _. {! `. g  v( K8 I0 K
第9行 .ANY (+RO)意思就是其他的所有RO,顺序往后放。就是说,其他代码,跟着启动代码后面。4 z: x, Q2 t! M" \0 L

2 i6 x# k# g* U6 z! }% K  u; @* K第11行 RW_IRAM1 0x20000000 0x00020000定义了RAM大小。
& K  R2 c- a9 l% i- b
6 N( _8 o$ E  y$ L
第12行 .ANY (+RW +ZI)所有的RW ZI,全部放到RAM里面。RW,ZI,也就是变量,这一行指定了变量保存到什么地址。4 p) ?/ B9 D7 u$ T0 `

5 ~9 {" z: d- w, r分析用户代码
: m! W1 x# p: l% O/ p" w到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。0 ^- ~% \+ b6 I/ {  s
* v4 ]0 S$ r$ b9 @$ {
1 程序跳转到main函数后:RCC_GetClocksFreq获取RCC时钟频率;SysTick_Config配置SysTick,在这里打开了SysTick中断,10毫秒一次。Delay(5);延时50毫秒。
  1. int main(void)
    ) a! j) X$ T: r$ j
  2. {% `* u# ?/ L1 G: r& r
  3.   GPIO_InitTypeDef GPIO_InitStructure;# S! j' z) J- o0 w% U9 x% X

  4. * k' t8 f3 t" v, w
  5. /*!< At this stage the microcontroller clock setting is already configured,+ w& V+ e4 M4 E5 X! d. z
  6.        this is done through SystemInit() function which is called from startup9 P2 P) G* c2 o( \# ~
  7.        files before to branch to application main.6 |% }! [# f0 ?- B, ~5 Q! v+ J
  8.        To reconfigure the default setting of SystemInit() function,
    0 w: f2 Q1 z1 z9 i
  9.        refer to system_stm32f4xx.c file */
    9 {/ a' X" @4 b# v% X) r

  10. 4 \0 D9 W- x1 G9 Y. |
  11.   /* SysTick end of count event each 10ms */
    , k) }0 K4 o+ {2 e+ b& i% E3 i
  12.   RCC_GetClocksFreq(&RCC_Clocks);
    3 I" \4 o) k! A) k/ o: B% k( b. m5 T
  13.   SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);
      w3 L, H9 \3 b
  14. 6 A/ u8 ]7 Z! H: v: S
  15.   /* Add your application code here */
      o: j. d9 a5 @
  16.   /* Insert 50 ms delay */5 R! N9 l( T3 H9 k: F4 A
  17.   Delay(5);
复制代码

+ [3 j1 T+ y  A# q) S7 v: V2 初始化IO就不说了,进入while(1),也就是一个死循环,嵌入式程序,都是一个死循环,否则就跑飞了。
  1. /*初始化LED IO口*/$ [. G$ A/ [3 d5 O3 d
  2. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);
    ( B1 U# j/ I' v4 o
  3. + ]3 Z4 z- X, K2 ^6 v5 P+ u( P5 b
  4. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;1 ?6 W9 J. m7 L6 y& K+ k# ?
  5. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    ) n! ~  c$ y% t; W& L

  6. 0 l  k; J" G( O. P% [4 l% ~
  7. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;- Z) d$ L% n+ e# ]
  8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;3 g9 m6 ^& S0 f4 B0 d6 }* g
  9. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    - k2 w" D! V8 F) l( K
  10. GPIO_Init(GPIOG, &GPIO_InitStructure);   
    3 n. q! i* A# @5 s; P
  11. 3 |7 |- L( [0 B/ s& R$ a4 G  ^
  12. /* Infinite loop */+ O- A8 e) U+ |$ @( O. r4 k
  13. mcu_uart_open(3);6 [/ u+ x, X. o% h" S
  14. while (1)+ u: e6 i2 i8 S% I+ \
  15. {
    & f4 |  N7 v! J; X/ _# ^
  16.   GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
    5 Z/ x( g1 J9 N# X
  17.   Delay(100);% V% k8 \& d5 n+ t# j
  18.   GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
    3 m. k/ O* d# G
  19.   Delay(100);5 {/ O; B- j5 z5 A
  20.   mcu_uart_test();
    % B, S. o3 ^! t* I7 X
  21. & z$ W- E5 i) T5 z, B4 o8 I
  22.   TestFun(TestTmp2);
    # T) ?. U3 B  R2 a. K, W
  23. }
复制代码

+ y. ?* z; F3 M& ~. O& e5 H2 }3 在while(1)中调用TestFun函数,这个函数使用两个全局变量,两个局部变量。
  1. /* Private functions ---------------------------------------------------------*/
      I: i4 ~  |4 {' m/ ?, R
  2. u32 TestTmp1 = 5;//全局变量,初始化为5
    3 M0 s' h1 i0 y3 `( c: h2 Y7 @
  3. u32 TestTmp2;//全局变量,未初始化6 H' `7 S# T3 w$ j
  4. $ [+ b4 u0 l5 v- Q$ a' V
  5. const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};
    9 ]0 k' l: C8 F# G( J9 u

  6. 6 E2 u; L& C( `8 ?2 ~
  7. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
    8 y& {4 @, p% a9 K9 Y: `* c
  8. {' V( N, z" Y( m4 f3 h8 c/ m
  9. u8 test_tmp1 = 4;//局部变量,初始化
    ' L  g/ b2 m( o4 l
  10. u8 test_tmp2;//局部变量,未初始化% Q$ s- P6 N2 k- ~& a! c
  11. & g1 l' Z& J1 W8 K/ A+ z0 g2 m5 m
  12. static u8 test_tmp3 = 0;//静态局部变量* R/ E* n5 o6 T; Y

  13. " l! O7 z6 N. v1 |5 Z* S
  14. test_tmp3++;4 s! l. d5 J# R+ c0 ~2 J
  15. + i/ b5 G/ Q) {+ M
  16. test_tmp2 = x;) y/ ^+ g, S% I7 D9 V

  17. 6 W! V. B" P) a* _) K/ Q! ~
  18. if(test_tmp2> TestTmp1)# n. J. k+ A/ v$ {% c- i
  19.   test_tmp1 = 10;
    $ c: I# C3 b# u1 F2 x5 ]
  20. else
    + D5 m, ?' _& a
  21.   test_tmp1 = 5;  m7 Y5 {( t( o/ I2 M
  22.   H4 z% j* F+ l/ J; [
  23. TestTmp2 +=TestTmp3[test_tmp1];
    " |+ J  S# b3 C2 U- z
  24. , s! g  C- p! ]# B# d
  25. return test_tmp1;2 ]. ?9 E1 d& d5 K, h/ m* _- ~! p
  26. }
复制代码
! V) o8 P( t6 X4 \7 @
然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。相关文章:STM32中断系统。我们查看Delay函数,uwTimingDelay不等于0就死等?谁会将uwTimingDelay改为0?
  1. /**
    / F$ \+ g5 m% L
  2.   * @brief  Inserts a delay time.
    , u/ A" \1 w8 m' s. C* v6 V
  3.   * @param  nTime: specifies the delay time length, in milliseconds.
    + Y$ L1 q. z4 J9 m( p9 w. A1 R: ]
  4.   * @retval None6 J- ~; f# T* C" a1 g) a
  5.   */
    # U7 y8 u, h& O
  6. void Delay(__IO uint32_t nTime)2 o) ?' K/ C2 ]  {, }' ?  Y# E
  7. {8 K, b! A4 A# F
  8.   uwTimingDelay = nTime;
    5 Q' U: ?; b8 K+ {3 l
  9. / L9 v0 w& `. f- q# ?
  10.   while(uwTimingDelay != 0);7 n# B/ Z- E) g4 r
  11. }
复制代码

: X0 d. [6 }; c1 M; a; K9 h搜索uwTimingDelay变量,函数TimingDelay_Decrement会将变量一直减到0。
+ V$ O" n2 w( ~2 [& \9 {8 r# O
  1. /**
    % Z' I: z0 b% D/ [4 F
  2.   * @brief  Decrements the TimingDelay variable.
    , ^) o" S+ f, Z
  3.   * @param  None
    1 W5 z% g9 K7 L2 z' c# x' K
  4.   * @retval None
    * a* W" z% u) z* F* h
  5.   */8 B: e) Z- B  s  s! O) t: p
  6. void TimingDelay_Decrement(void)
    " p; @% C) z% ~2 B
  7. {% u; G9 [2 x: E3 {/ w
  8.   if (uwTimingDelay != 0x00): R# }* [; o4 v, O" N; X
  9.   {
    ! I1 ~: a. N# f& U$ m% _1 r
  10.     uwTimingDelay--;
    * d# [  E6 g9 h
  11.   }
    1 U& Z! T0 y" _8 b# o# A
  12. }
复制代码
$ X- I6 p6 Y2 ^/ \1 r% v7 q1 O  U
这个函数在哪里执行?经查找,在SysTick_Handler函数中运行。谁用这个函数?
  1. /**
    ' c: }$ ?+ d7 w$ ^6 |( ^, m4 H
  2.   * @brief  This function handles SysTick Handler.
    - `4 z# g3 [$ M! t5 D% `
  3.   * @param  None5 O: S4 G. F$ ^5 Q8 @- o
  4.   * @retval None
    $ c  l1 [" N% A7 U3 c, F5 Q
  5.   */
    ' H" D& J' A: P9 v3 }0 |. m
  6. void SysTick_Handler(void), p: B2 ~  D, n: }- H
  7. {
    - w+ s* o) Q2 V% T! c, E
  8.   TimingDelay_Decrement();% \2 ?  y. Q: ]6 A  C( b; D
  9. }
复制代码
/ n: X: J: _% I( E9 ~8 [
经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。
  1. __Vectors       DCD     __initial_sp               ; Top of Stack; k5 D$ ^4 M; l% P& q0 E- _
  2.                 DCD     Reset_Handler              ; Reset Handler) `4 Z. n2 l& p9 [2 r, O
  3.                 DCD     NMI_Handler                ; NMI Handler
    ' }9 T! w3 q4 o) b
  4.                 DCD     HardFault_Handler          ; Hard Fault Handler
    # d0 F" D3 `4 E# G1 P& T* k6 u" ~7 k
  5.                 DCD     MemManage_Handler          ; MPU Fault Handler
    + S' {* s$ d. E* b
  6.                 DCD     BusFault_Handler           ; Bus Fault Handler) x4 }# K" Z2 O; d. M# }% T1 S5 B
  7.                 DCD     UsageFault_Handler         ; Usage Fault Handler2 \4 j" O" N/ P  U6 ^
  8.                 DCD     0                          ; Reserved
    & [+ G6 W+ C6 e. k1 Y/ @
  9.                 DCD     0                          ; Reserved
    ! r' x+ S: C' `; ^- O0 D  s
  10.                 DCD     0                          ; Reserved) `2 O9 ?+ e7 f& i" q" G6 V. @
  11.                 DCD     0                          ; Reserved1 g* `. s' P7 f: Q$ T
  12.                 DCD     SVC_Handler                ; SVCall Handler( c) D# ^, g8 H, [
  13.                 DCD     DebugMon_Handler           ; Debug Monitor Handler
    4 D/ C# z3 P7 [
  14.                 DCD     0                          ; Reserved! C$ Y9 S  ?6 b" h6 e1 V
  15.                 DCD     PendSV_Handler             ; PendSV Handler
    ( M1 W# a* s$ w& F" ?
  16.                 DCD     SysTick_Handler            ; SysTick Handler
复制代码

) l6 m3 w) W* o* c余下问题: t) b4 q, h9 L* ~. i# F
1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(InRoot$$Sections)是什么?3 ZI段,也就是初始化为0的数据段,什么时候初始化?谁初始化?
* o1 }' W% s7 u! T8 B

- }2 {9 o+ r' q7 R6 B; Q* J为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!
% y+ ~1 n2 Y. W2 A" }
8 y/ z; Q2 d+ S/ v( Z: x

! j$ A. E5 d2 E5 @: j$ O- ~5 z通过MAP文件了解代码构成
- i4 C, [! G7 Y, N/ n5 k% D9 G编译结果
' Z* [/ W& e6 o5 o
程序编译后,在下方的Build Output窗口会输出信息:
  1. *** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'( F# C; t9 i5 E" f# x7 F' B
  2. Build target 'wujique'5 H6 W. y# G" K
  3. compiling stm32f4xx_it.c...# U- N6 X8 g& f+ N3 Z
  4. ...; S) X* F% @7 q. o
  5. assembling startup_stm32f40_41xxx.s...2 n9 ?2 R- `2 I/ U* N& {2 V1 Z
  6. compiling misc.c.../ [0 c$ w9 n" T. ]0 s4 a! v4 q
  7. ..., J4 \; n8 N4 l2 J% k. Y3 ?
  8. compiling mcu_uart.c...) b5 q# D& s/ V! m& r
  9. linking...
    * B( C# x: i+ G+ j5 r! D
  10. Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  . L: o& x9 p' Z. o5 q1 p
  11. FromELF: creating hex file...6 W  b9 b; B* W- N9 P
  12. ".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s).
    " j3 f0 F  k& p, A1 N- f
  13. Build Time Elapsed:  00:00:32
复制代码

# A8 y, L5 @- T9 |! M0 o. G编译目标是wujique
+ |! R7 ?% O2 z0 R( W- ~+ uC文件compiling,汇编文件assembling,这个过程叫编译8 z3 y: J: I! E. Y5 y
编译结束后,就进行link,链接。
# q* F. z3 T: c; S6 K最后得到一个编译结果,9038字节code,RO 990,RW 40,ZI 6000。CODE,是代码,很好理解,那RO、RW、ZI都是什么?6 ~6 ~3 g$ [* z; D
FromELF,创建hex文件,FromELF是一个好工具,需要自己添加到option中才能用) U% ]& b( \0 V! y: `
, ~4 ]3 q1 M/ a/ |- ~: H
map文件配置, Y4 O! B( B) Q) y0 V# }4 p/ {
更多编译具体信息在map文件中,在MDK Options中我们可以看到,所有信息都放在\Listings\wujique.map
+ O  P% x% S4 m& ?* V( z& W
- c/ b. W/ I9 _; o/ t/ }默认很多编译信息可能没钩,钩上所有信息会增加编译时间。" R, W! v& t; g$ @! E

& @! i; L3 _# h) n
4.png
1 Y0 {8 p* E. J' o- N
1 O" d- Z# [' j: v, @2 t  b; h9 A
map文件
; y0 P1 [' d# l4 r8 S; s! _
打开map文件,好乱?习惯就好。我们抓重点就行了。) I  |8 q/ z8 ?% v+ P4 F
$ C& y+ M7 F( h  T$ D: ]4 G8 o/ k
5.png
# M& ^5 G6 R4 d% M, T
$ [8 B+ @. [) w
map 总信息2 Q) C. a4 X& A2 A/ [2 @
7 }0 ^) L4 g3 a8 n) r. B
从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。
+ J3 U, F  g: G' ^: ^9 k- P* T4 u8 A8 W0 L, T& I
有多少RO?RO到底是什么?
5 m- \& ~8 R& a6 V, i# d3 u( T! n1 d( t) x: X; W2 h
有多少RW?RW又是什么?
4 c( }5 q5 l) b. `- e  y
/ s9 e: J- D/ M+ N. ]1 Z3 jROM为什么不包括ZI Data?为什么包含RW Data?
/ e# S* n" B( W# Y( Z  I) y9 M7 X. ]! q0 F6 o7 P# d) X
6.png
) K8 B% ^" b# v5 QImage component sizes
3 Q/ i  ?; t  F1 c* p/ B( m, O5 Q4 P& m. @! o4 B* ^7 i. [
往上,看看Image component sizes,这个就比刚刚的总体统计更细了。
0 y6 r( i$ M* k/ r" o2 F
' X+ Y- L( v1 q1 a/ n: h4 C3 b这部分内容,说明了每个源文件的概况/ J- t5 f4 ^3 V$ f7 t3 ]8 M, \

9 G9 u5 T, m# X6 D9 T/ g首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些STM32的库文件。1 R+ s4 z; A, h* _" Y! D
$ b/ W9 n* ?, i0 G, w0 h
7.png
; a1 u9 B% m& K1 k4 \& E

8 q$ R6 v$ E+ a( j" N/ `5 y3 ^第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1M flash上的程序压缩到能在512K上运行。# {/ W$ H7 V2 a/ ^8 j% u  j( B
& D  [! I; o) f2 q2 P6 O9 |: N  s
8.png

2 o1 Z7 E% o4 c4 U4 K1 }! A0 y0 a

+ d  U4 ]/ K' H) [8 a% V第3部分也是库,暂时没去分析这两个是什么东西。
. @; J8 h7 v( t- y( r3 s3 R- D" Y, P) ?& Z/ e7 {
9.png

5 Z6 ~: B! g9 c3 o3 o8 @: U- T
# ~7 H5 s( C0 `8 A
库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:
5 \0 n/ ^8 e+ |2 [! Q/ C#include <stdarg.h>
5 t4 _6 l6 ^! T4 ]7 M4 P#include <stdlib.h>
& x! Q* ]9 b$ V' b5 R0 x#include <string.h>  
1 q4 v7 I  y2 J$ a
9 M- g! a' b# Y% Y
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。
2 m' h$ I) Y5 J" U2 E
! L7 x+ y. R3 v% A8 ]( E( e+ e
1 S8 {! C" m% T9 _* }/ z) ~
文件map
9 F; Z3 \& Z) c再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET+ h1 b3 c8 b; W$ _! X

/ X8 z! a( ]  `" ^库文件是什么?  e! q( R; G! q' Z" L
库文件就是别人已经别写好的代码库。
: z# M" l1 V+ k* [9 J9 l* m& _$ e9 q2 Y4 K! r* x
在代码中,我们经常会包含一些头文件,例如:
+ [% p9 a' |  q2 L# S2 h5 g  z; k
  1. #include <stdarg.h>) I0 ^& U' V  X+ I) ~
  2. #include <stdlib.h>% b( ?. o/ d6 E8 o1 V2 x
  3. #include <string.h>
复制代码

8 F9 @- y' Y8 U* o: Y' [% [这些就是库的头文件。相关文章:C语言中的头文件。这些头文件保存在MDK开发工具的安装目录下。% S/ L) g. S5 w8 I9 o0 R5 k" C

2 ^' j% K! ^' I2 N2 O/ {- Y我们经常用的库函数有:memcpy、memcmp、strcmp等。8 t6 c: H3 U! u0 t. O4 Y
! |. N7 i" O. f2 C# T# C
只要代码中包含了这些函数,就会链接库文件。
9 ~" X1 U# Y4 s8 w3 M  E, n+ W+ M/ O; Q2 y! M
文件map
* y6 i! s/ a4 V- m  J再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET) }6 B9 Q+ K9 {3 y9 {, V

. [/ C8 L1 d# O* W) @$ T7 N' x
10.png

( L  b0 J$ z; l, K

! _) J$ j' i( H! R6 ~" |每个文件有有多行,例如串口,4个函数。5 G: ~0 G& H3 C2 ~
8 Y7 p! s: n$ ^& `# _+ Q
11.png
) t3 [+ H5 v8 U& h& c

  x, j+ |0 s' Z+ o然后是RAM的,main.o中的变量,放在0x20000000,总共有0x0000000c,类型是Data、RW。串口有两种变量,data和bss,什么是bss?这两个名称,是section name,也就是段的意思。看前面type和Attr,
7 v/ j4 T5 @9 J) c3 X3 N6 h3 a5 c, x, {+ d' L* ]8 L/ E
RW Data,放在.data段;RW Zero放在.bss段,RW Zero,其实就是ZI。到底哪些变量是RW,哪些是ZI?  p6 D8 s7 S/ Z  }4 Y

! o# C" ^2 `, d5 W: |# p$ F
12.png
% C" U( `) [  A5 B: f( R  p
% l9 P# m7 W! y, n
Image Symbol Table
  A5 y9 Q5 p% L( q  c9 ]
' L# I/ M, i, ]5 M+ m4 T3 d/ {3 @再往上就是Image Symbol Table,就更进一步到每个函数或者变量的信息了% z, X) Z7 I# `; Z) T. o( f: X

  w4 ^0 g' h: t( J! R; Z4 z
13.png

1 W/ |8 b6 |! u4 J; D7 I/ l9 \" n
, h; H7 g1 f3 J6 Z5 w& f例如,全局变量TestTmp1,是Data,4字节,分配的位置是0x20000004。9 H! i% F4 l; j! P% h' E; J" b
7 }$ n1 Z7 I1 g) l; ^# o
15.png
) g; U4 x! C3 v9 O/ s" `, `
& {, b. e6 b, R/ j. |
TestTmp3数组放在哪里?放在0X080024E0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几K几十K大,不可能也没必要放到RAM区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。
: ]! B; a9 |' @) d4 u! t  C0 \
. y7 F) \( e. {const的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习/ P( y# W! p/ W% P6 y+ x

- f, p3 x: f! e- i
16.png
' x* u! P( W/ l* p" h: F; O% h2 }

$ i: X! g( w: i* p/ B
那局部变量存放在哪里呢?我们找到了test_tmp3," q5 g9 I0 R5 G' C

: f0 p( L+ e$ g5 e3 u
17.png
9 s$ C5 {3 s% q% m
' {$ j* d# _$ H( M5 ?( q5 a
没找到test_tmp1/test_tmp2,为什么呢?在定义时,test_tmp3增加了static定义,意思就是静态局部变量,功能上,相当于全局变量,定义在函数内,限制了这个全局变量只能在这个函数内使用。哪test_tmp1、test_tmp2放在哪里呢? 局部变量,在编译链接时,并没有分配空间,只有在运行时,才从栈分配空间。
  1. <blockquote>u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
复制代码
, k  c) F. d3 Z+ D5 U- R
上一部分,我们留了一个问题,哪些变量是RW,哪些是ZI?我们看看串口变量的情况,UartBuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文Block Started by Symbol的简称。& j; D( J8 b( D4 B+ R

: ^( S1 b1 L: n* c& K- N) Y) F3 |
18.png

, a- z  U+ k% s, V! N, d0 m% |# {' `" g0 I: I
到这里,我们可解释下面几个概念了:) @1 y/ X2 K+ N- I6 U+ d' B/ L9 K
Code就是代码,函数。8 [8 p! w# |4 G% G4 u
RO Data,就是只读变量,例如用const修饰的数组。
& o/ q) |# ?8 ~! a" B( C. \, rRW Data,就是读写变量,例如全局变量跟static修饰的局部变量。
, a) s2 J' ~& t. @! wZI Data,就是系统自动初始化为0的读写变量,大部分是数组,放在bss段。
, h* V0 q2 a) n2 J5 P8 z0 Y4 |- n1 ORO Size等于代码加只读变量。3 y+ N! |, z7 ^. k5 a
RW Size等于读写变量(包括自动初始化为0的),这个也就是RAM的大小。4 ]8 E. H( d1 [
ROM Size,也就是我们编译之后的目标文件大小,也就是FLASH的大小。但是?为什么会包含RW Data呢?因为所有全局变量都需要一个初始化的值(就算没有真正初始化,系统也会分配一个初始化空间),例如我们定义一个变量u8 i = 8;这样的全局变量,8,这个值,就需要保存在FALSH区。  Y3 G6 K1 [) v3 J: w6 z' c. b# P% N5 f

) x( i$ l* N" o3 J& }2 w. b
19.png
+ k  i; a3 a; P( h9 T0 ]. G+ O

; Q5 f3 H( ^+ [( B; b! z我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579
* W4 ^  _% M! a  y& l2 t
5 O6 _8 f  g& ^  G4 k
20.png
: _0 Y/ R) N3 l. X3 b, [* t7 ~

% D* G- e8 r. {2 ^, H* L1 w
main是main,放在0x08000189
, _" }6 {4 f4 B9 S
& R( ?4 N" d. X3 F
21.png

9 |* F4 v* W' z, x

# y; A" w1 {% p2 b6 U  h__main到main之间发生了什么?还记得分散加载文件中的这句吗?
9 p% W+ [$ u9 s' b9 [5 s*(InRoot$$Sections)
7 Q* {, z0 }7 x+ |
: \. p! U, o4 O# b1 U
__main就在这个段内。下图是__main的地址,在0x08000189。__Vectors就是中断向量,放在最开始。- B9 J) w' V* X" b% ?; o" K
8 s  j, I* X! E" K# n8 g' y  U
22.png
3 z. T  B' m0 |( t$ J# Z
7 p" X9 j% t; L3 H% u+ G, H
在分散加载文件中,紧跟RESET的就是*(InRoot$$Sections)。5 E& e1 R. N' X8 b! k
# Z& J- ]6 {( o
23.png

- ?( {8 }& F1 J

- U& h% x3 A/ q( J" _而且,RESET段正好大小0x00000188。) E% Q6 F+ f: c/ i8 ^4 i4 ~! a: s

  f. _9 U$ R+ n5 @) e: M7 I
24.png

+ }) D: [4 S- I7 b9 |& c2 E4 M* X3 s- d- q/ D/ G3 z) d
巧合?可以参考PPT文档《ARM嵌入式软件开发.ppt》。% ^0 o8 e' }4 f
8 @. v8 k6 ~5 R5 W, t) E( _. h2 @
25.png

6 C- [3 h- \' l" @+ h  Z
  [; j% {2 @' T* ^# W! V
这一段代码都完成什么功能呢?主要完成ZI代码的初始化,也就是将一部分RAM初始化为0。其他环境初始化……
# \4 p1 H  ]' @! ~7 `9 I
$ t& @$ X9 O6 c1 U6 n1 s

4 q) _+ y% S2 D' p9 h# {最后
" X5 C1 Y: B5 F- v3 B" N# J到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。+ p3 q! l) }/ R1 O; n

! }$ w4 x6 T; A  T

9 B6 O3 l5 B: ]/ g转载自: [color=var(--weui-LINK)][url=]STM32嵌入式开发[/url]$ m& ], u  a; g
如有侵权请联系删除' V: B7 P2 G, K/ j3 g

# ], w2 B8 t, ^( ]
. c: N/ P! g, ~* |. ?- p/ T0 m
收藏 评论0 发布时间:2024-7-29 15:25

举报

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