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

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

[复制链接]
攻城狮Melo 发布时间:2024-7-29 15:25
本文分析STM32单片机到底是如何软硬件结合的,分析单片机程序如何编译,运行。, p/ E; U/ ^. S  M' {( [' v
% b( e% D# g9 C+ G( N9 ^+ A
软硬件结合
9 v2 U" v7 R" b初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写P1=0X55,就可以在IO口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。' C9 u1 t0 p5 M9 U, }
, s- v' }+ S5 D" Q
寻址空间  D. m; E+ b5 V! R0 h
什么是地址空间呢?所谓的地址空间,就是PC指针的寻址范围,因此也叫寻址空间。
/ O" c5 a; S3 [! o
( w! V7 ?5 I5 ^- B% I7 K2 z大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,PC指针就是一个32位的二进制数,也就是0xffffffff,范围只有4G寻址空间。现在内存越来越大,4G根本不够,所以需要扩展,为了能访问超出4G范围的内存,就有了64位系统。STM32是多少位的?是32位的,因此PC指针也是32位,寻址空间也就是4G。- k3 X. d6 E$ [$ |1 {/ ~- K

; Z' @% Q5 o# k6 e* D$ Y6 F& `我们来看看STM32的寻址空间是怎么样的。在数据手册《STM32F407_数据手册.pdf》中有一个图,这个图,就是STM32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫Memory map,用一个新芯片,就先看这个图。
3 @$ R3 p! F& x2 [3 u
; ^$ F+ {6 S7 A; i" c( G5 H
微信图片_20240729152315.png 6 V0 ?8 z& ^8 C! Y8 m/ t/ ^9 y

( g0 |0 \' [$ a% f" f8 i6 q& ~: J) r* N
5 ]3 f3 g7 S, J5 s' M最左边,8个block,每个block 512M,总共就是4G,也就是芯片的寻址空间。! f  f+ q( k, r% t8 F) m" U: j

4 i" o4 o8 p4 t- dblock 0 里面有一段叫做FLASH,也就是内部FLASH,我们的程序就是下载到这个地方,起始地址是0X800 0000,大家注意,这个只有1M空间。现在STM32已经有2M flash的芯片了,超出1M的FLASH放在哪里呢?请自行查看对应的芯片手册。0 Z9 L% G4 h# l& }7 A

8 I  J* H) F: q3 在block 1 内,有两段SRAM,总共128K,这个空间,也就是我们前面说的内存,存放程序使用的变量。如果需要,也可以把程序放到SRAM中运行。407不是有196K吗?; ~6 r* o% ^' l
) g  f: u. _  B- G! w( i
其实407有196K内存,但是有64k并不是普通的SRAM,而是放在block 0 内的CCM。这两段区域不连续,而且,CCM只能内核使用,外设不能使用,例如DMA就不能用CCM内存,否则就死机。% J$ `: f  q& {& ^6 U! o6 z: N9 d
6 N& ]6 k1 d1 O5 C# B- M0 P! u
block 2,是Peripherals,也就是外设空间。我们看右边,主要就是APB1/APB2、AHB1/AHB2,什么东西呢?回头再说。
3 R! f; M! |+ T3 E% T' H8 r8 A9 W: z" e
block 3、block4、block5,是FSMC的空间,FSMC可以外扩SRAM,NAND FALSH,LCD等外设。. {) C0 w9 u7 H/ t7 ^2 V

7 N" g1 J% [- _; I好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。对于这个疑惑,也可以看此文:代码是如何控制硬件的?在IO口输出的例程中,我们配置IO口是调用库函数,我们看看库函数是怎么做的。0 @& Z8 k$ Q- y, L6 }9 I/ l6 W) G

) f4 H  X2 _* X% @& g8 y6 v& H例如:
4 B  G' h3 n$ ]! V0 o
  1. GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
复制代码
4 ^6 y* B: w: G: q, I2 A# E7 R
这个函数其实就是对一个变量赋值,对GPIOx这个结构体的成员BSRRL赋值。
! c6 n6 C4 S/ Y0 c4 A& j: I
  1. void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)# H% G# \* c( R2 N( P$ V7 V
  2. {# y% L/ T% G7 A! e
  3. /* Check the parameters */
    / E4 [* I! U7 H) o( {
  4. assert_param(IS_GPIO_ALL_PERIPH(GPIOx));4 p* Z3 B* r; D4 r
  5. assert_param(IS_GPIO_PIN(GPIO_Pin));
    0 @; z& T9 g" H/ w! X, P
  6. 1 K7 f) v5 q8 T

  7. * F, D! k5 z* Z3 k+ ~, |$ @! k
  8. GPIOx->BSRRL = GPIO_Pin;
    ( i" d9 B/ D( p/ R% F9 a
  9. }
复制代码

0 F1 w8 F( M9 W. ?assert_param:这个是断言,用于判断输入参数是否符合要求GPIOx是一个输入参数,是一个GPIO_TypeDef结构体指针,所以,要用->获取其成员7 J  K+ z6 \5 H/ ?% M

# W1 ~8 D$ ^0 H$ Z# EGPIOx是我们传入的参数GPIOG,具体是啥?在stm32f4xx.h中有定义。6 {+ ]/ l0 u5 |# G- D
  1. #define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
复制代码

1 F2 n6 X' f1 Z8 J: P$ mGPIOG_BASE同样在文件中有定义,如下:
0 ^3 G% o/ B" G, ]" R8 T8 `
  1. /*!< Peripheral memory map */
    9 J5 D9 N& E+ A( R
  2. #define APB1PERIPH_BASE       PERIPH_BASE
    % y/ i" w: d$ e- m$ F- |
  3. #define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)
    + k% ^( f; E# Z7 U! D# e1 Z2 t
  4. #define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)6 ]; W/ r& ?+ L1 R
  5. #define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)
复制代码
* S% u* F0 m$ E4 M3 {. Q# l. [( z
再找找PERIPH_BASE的定义
! R7 m' |; e; F
  1. #define PERIPH_BASE           ((uint32_t)0x40000000)
复制代码
到这里,我们可以看出,操作IO口G,其实就是操作0X40000000+0X1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是SRAM空间地址,0X40000000+0X1800是外设空间地址。7 \  m8 H9 L7 R: G) \# k. n
  1. u32 i;& W# i$ x; G0 ]3 p2 M2 N- o
  2. i = 0x55aa55aa;
复制代码

4 r: K3 F7 t/ S. J$ }% Z# j这个外设空间地址的寄存器是IO口硬件的一部分。关于如下图STM32的GPIO文章推荐:STM32中GPIO工作原理详解。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0X40000000+0X1800+0x14.
3 W5 w$ C- z) l0 U7 _* u/ P. Z+ C2 }1 Z, e# j6 W; r# c
微信图片_20240729152319.png

9 J3 d! b% D7 X& C( X

. M3 @. U! [* A5 S7 M5 f0 g3 u控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。! \& b" b6 s* g7 q, D6 h$ \
' H8 j9 ~1 z* \) F( b6 {
寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或RAM。寄存器为什么能控制硬件外设呢?因为,初略的说,一个寄存器的一个BIT,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。- C9 ]) a& p! }, {
, Z/ N: _( F( R: q6 E+ r" a
纯软件-包罗万象的小程序
) h' K7 Q9 j5 u6 y我们已经完成了串口和IO口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?关于程序是怎么在单片机运行的,也可以看此视频:动画演示单片机是如何跑程序的。代码到底放在那里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。& l+ Q& Q: u/ W7 g4 a9 l
; Y+ Q" m3 G( U6 s  L3 \5 V4 c: g
分析启动代码/ ]1 ^& F2 x5 D
函数从哪里开始运行?
+ B. T+ y5 g% j8 t6 i每个芯片都有复位功能,复位后,芯片的PC指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,PC可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在STM32中,复位到0X08000004。因此复位后运行的第一条代码就是0X08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。. n4 l* _& Y+ ], D% x% O! G
4 o/ K" B, d# P. I, g- F/ |& g2 ^
芯片复位时,芯片从中断表中将Reset_Handler这个值(函数指针)加载到PC指针,芯片就会执行Reset_Handler函数了。(一个函数入口就是一个指针)
  1. ; Vector Table Mapped to Address 0 at Reset
    ) a* `& F( v+ ~. m3 [
  2.                 AREA    RESET, DATA, READONLY
    ( b# y* ]+ o# Y3 L- o; @
  3.                 EXPORT  __Vectors
    ! ~2 w; ]) Q+ P+ K/ L
  4.                 EXPORT  __Vectors_End/ [# b; ?3 ]$ P5 m4 S) t% y
  5.                 EXPORT  __Vectors_Size
    3 F# K- }& e' B% N8 s8 e
  6. ' R+ V. i- w2 [% t9 l+ u# j0 A
  7. __Vectors       DCD     __initial_sp               ; Top of Stack
    / m5 u/ a: F& A9 W& i
  8.                 DCD     Reset_Handler              ; Reset Handler
    : C) S/ r. |4 K2 B  Z
  9.                 DCD     NMI_Handler                ; NMI Handler8 O( t% I- l6 |/ i# }* j
  10.                 DCD     HardFault_Handler          ; Hard Fault Handler
    9 T) Q3 e! C: y7 b' q& j
  11.                 DCD     MemManage_Handler          ; MPU Fault Handler6 X7 [' ]: _0 p+ _  K" J
  12.                 DCD     BusFault_Handler           ; Bus Fault Handler
    ' x! l9 S" U  {) ^9 b+ B, q$ E
  13.                 DCD     UsageFault_Handler         ; Usage Fault Handler
复制代码

+ R4 a# E6 L5 w: {4 d: @Reset_Handler函数,先执行SystemInit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?) u, `+ J/ B, Z) J' H8 q, b
) I$ J; @& N' o; c! q3 L/ ]
是我们在main.c中定义的main函数吗?后面我们再说这个问题。0 z) v* q' r7 p* |' @6 `
% \  v! j0 @5 [" u. M, T6 w7 I) G
微信图片_20240729152323.png

$ T/ A* E/ c$ Q0 r
. R( e3 {1 L# E) q" A! u% L& g5 x
芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujique\prj\Objects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。5 s5 C8 q4 I6 I. a: b1 j; B

3 Q; m0 W1 b3 K. y2 f在MDK软件Options菜单Linker下有关于这个菜单的设置。* a! O# C/ F9 U! @& {( \# K! G
* h2 H; j1 E/ I' A/ r
微信图片_20240729152326.png + L9 K+ E- t- z
: ]- u$ ]# o- m

$ I! \) r& R2 ^" w( [/ q. D

2 b/ {5 H0 t& a4 _' I+ o2 k$ z3 b3 v把Use Memory Layout from Target Dialog前面的勾去掉,之前不可设置的框都可以设置了。点击Edit进行编辑。( U1 j+ o" J/ n' ]" S0 P" j5 v
( z7 I9 B) Z" N. c- E9 U  a9 _1 O' l9 }
2.png
+ n/ k+ m. J; L9 h$ n. D. y2 d) Z
/ r; n( ]" j5 n( ]
+ O0 t0 n# Z2 e( D- k6 N9 y
在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。
5 `. v  k" o2 ~$ g
% r- f$ R9 I1 B其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定FLASH跟RAM的大小于起始位置,当我们把程序分成BOOT、CORE、APP,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到RAM中运行。
$ r" {9 t  {$ p" `; x7 x9 f- ]: G/ A/ X/ {& ~
3.png
, T. ^5 }: w! `. E
/ z) P' F3 }, C: b& W4 N3 j
从这个基本的分散加载文件我们可以看出:; ?9 e/ x( b  ]* n( B1 r" w
" G; V. t5 Q+ |1 Z( `; z4 P3 L' k
第6行 ER_IROM1 0x08000000 0x00080000定义了ER_IROM1,也就是我们说的内部FLASH,从0x08000000开始,大小0x00080000。
& g+ y/ B8 G" S0 P9 y7 c. T; @6 y4 Y. x, v% k9 E5 U
第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。' c" `" {6 o3 G; {/ u" H. m/ f) I( X
  `: I5 F. A# [4 S
第8行 *(InRoot$$Sections)什么鬼?GOOGLE啊!回头再说。
* y* f7 ?( K8 C9 ^' C
0 e0 C2 d+ n# ~. ?- P: B! X8 x第9行 .ANY (+RO)意思就是其他的所有RO,顺序往后放。就是说,其他代码,跟着启动代码后面。' l8 a6 ^- e9 Y& h. A

1 p) L: S. E: C第11行 RW_IRAM1 0x20000000 0x00020000定义了RAM大小。9 f7 Y3 u9 u  p5 W

  E  G# |( V7 [  M/ X* Y' t1 `# u第12行 .ANY (+RW +ZI)所有的RW ZI,全部放到RAM里面。RW,ZI,也就是变量,这一行指定了变量保存到什么地址。
: ^# ]2 g" q7 A
# I$ P( A/ V2 \# p, S9 _分析用户代码
* l# ^% a7 d. I( d4 h% H1 h到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。2 {5 t" C8 A+ V# V, m( r$ U# t
( s# b  W- m% |) ?# C
1 程序跳转到main函数后:RCC_GetClocksFreq获取RCC时钟频率;SysTick_Config配置SysTick,在这里打开了SysTick中断,10毫秒一次。Delay(5);延时50毫秒。
  1. int main(void)
    3 F& ?4 p* j. N, ^! x& j; c% R" P
  2. {
      p- Y1 J9 U7 a: u
  3.   GPIO_InitTypeDef GPIO_InitStructure;9 a0 m! p. L) F# a: _8 O8 l$ q# m# w

  4. 1 N; r' E- c( Q0 _- v  D0 h/ Z! o
  5. /*!< At this stage the microcontroller clock setting is already configured,& v! k4 w0 r. q5 v9 {2 g
  6.        this is done through SystemInit() function which is called from startup2 H* C! V$ {% v  l+ `& n
  7.        files before to branch to application main.& N/ e$ M) L7 y, s* e( e( ]
  8.        To reconfigure the default setting of SystemInit() function,
    . l( ?0 P  A9 }3 k" }8 N
  9.        refer to system_stm32f4xx.c file */* ^, S8 N; W! N& D; f1 g( E& E

  10. ) @1 @; @7 ~! U# \+ E* }: a* [
  11.   /* SysTick end of count event each 10ms */1 E2 W% l: M/ D4 q9 ^
  12.   RCC_GetClocksFreq(&RCC_Clocks);0 T5 A2 A. ?4 o* Y2 Q- h
  13.   SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);+ Q8 }0 e8 e* n

  14. / ], O& z, j' L
  15.   /* Add your application code here */9 V/ X7 y7 s4 ?& E3 Y
  16.   /* Insert 50 ms delay */1 d2 O5 n! U, s- Z, y
  17.   Delay(5);
复制代码
# {/ l$ _/ @1 r9 `0 h' v. ^6 v
2 初始化IO就不说了,进入while(1),也就是一个死循环,嵌入式程序,都是一个死循环,否则就跑飞了。
  1. /*初始化LED IO口*/' a9 G7 m& q: M  x$ C" i
  2. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);/ p3 B4 ]" L+ n) o
  3. ( F' n: O! o$ L0 S/ b( v' F
  4. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;
    3 N! V% Q% _" x1 t8 i/ b
  5. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;% n8 Y1 r! T7 X- K: i

  6. % U/ b% @6 r7 w/ k' ~
  7. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;% O2 ?5 b9 t2 d- y+ O
  8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    # n0 N  u) \1 C
  9. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;. _+ O) }" S/ ~
  10. GPIO_Init(GPIOG, &GPIO_InitStructure);    " ]' D! J8 n- x7 H" o$ H* Y: p

  11. 9 t3 P" d' f" E
  12. /* Infinite loop */  a+ u! d- K( v# R
  13. mcu_uart_open(3);
    % n2 C3 v& a  X9 e
  14. while (1)7 |) x0 o9 P+ r4 ~
  15. {
    " c" L/ {) y; |8 I  K/ C; G" W" z' v
  16.   GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
    - r8 f' R1 T; h4 }1 A
  17.   Delay(100);  d( y! X" Y- J- m
  18.   GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
    & V4 O/ ^, {  x7 [
  19.   Delay(100);9 i7 S; A" Y  u
  20.   mcu_uart_test();
    $ C: w& m" Q& m4 z3 `
  21. , j6 L6 ~; A! u& _4 d9 A8 i  \7 t
  22.   TestFun(TestTmp2);# w7 K- V& W( a% D  o& m
  23. }
复制代码

3 s# F" |# S3 k* k6 e9 s+ u- I2 n3 在while(1)中调用TestFun函数,这个函数使用两个全局变量,两个局部变量。
  1. /* Private functions ---------------------------------------------------------*/3 w/ G9 Y2 E. V# v
  2. u32 TestTmp1 = 5;//全局变量,初始化为52 J' {3 v: K+ C  T+ v! ~4 t' @+ S
  3. u32 TestTmp2;//全局变量,未初始化
    " n" j2 E- k+ _8 U9 i) l

  4. 4 M( `$ V  E5 O+ T3 l5 k' Z& }
  5. const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};
    # i; Z5 s7 y. c/ W

  6. ) Y$ ^  F0 P6 g6 n
  7. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
    % B3 K7 W- ^6 @2 s& Z+ {% P
  8. {% c; e* g' `5 s2 @
  9. u8 test_tmp1 = 4;//局部变量,初始化/ W) }" [7 U  z- \9 K4 _
  10. u8 test_tmp2;//局部变量,未初始化# W- q( A" ?- p( T/ T4 k
  11. - |. \, u1 I; @3 e3 S& F6 d
  12. static u8 test_tmp3 = 0;//静态局部变量
    % }! m! `" }# i
  13. 2 R' C4 Y' v; v0 @2 Z0 H7 W
  14. test_tmp3++;" q" v4 c4 D4 A! h" `) a7 f( I

  15. 9 {- f, @2 s& l. N; h/ P7 b
  16. test_tmp2 = x;  B9 v) f9 I7 n1 a

  17. 9 q6 j. }8 V( }6 d& y1 Y. G
  18. if(test_tmp2> TestTmp1)$ N9 i2 @8 z" o" {4 [& J
  19.   test_tmp1 = 10;* Z/ ~0 L5 \" @$ E- s% H. v- [
  20. else- V. s, W4 {+ V# P
  21.   test_tmp1 = 5;
    6 I2 `, Q0 b0 p6 P: q9 Z
  22. ' c5 V) [# }! h. o" N6 l' w$ D$ O
  23. TestTmp2 +=TestTmp3[test_tmp1];
    ) h9 ]4 ]1 N( r3 I- p6 Y2 [6 r
  24. 2 V' a$ w3 E5 h" l
  25. return test_tmp1;
    * Q! A, j2 k: S% O/ m
  26. }
复制代码
2 p" v- L* o0 J
然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。相关文章:STM32中断系统。我们查看Delay函数,uwTimingDelay不等于0就死等?谁会将uwTimingDelay改为0?
  1. /**7 }5 O6 @1 B) j9 l$ W- h. ?& ?
  2.   * @brief  Inserts a delay time.
    ; j2 J/ N5 A- @+ H! ^
  3.   * @param  nTime: specifies the delay time length, in milliseconds.
    6 @) `/ Y1 Z) V  Y' o4 H4 R
  4.   * @retval None
    5 T5 B: ]# J( F0 Y
  5.   */
    : R0 x* i9 d8 U$ \
  6. void Delay(__IO uint32_t nTime)
    : Z) `" R/ y6 J9 Z; f, ?
  7. {, @7 E. @. E- e' {! J5 ?- W
  8.   uwTimingDelay = nTime;
    6 q/ |+ G8 O( K& |6 H' F- t
  9. $ B/ g* O6 H7 l6 k
  10.   while(uwTimingDelay != 0);; G/ G4 ?, J- A5 ~0 L& i
  11. }
复制代码
# T# i9 B4 _6 a: H% w& V, l- I! }
搜索uwTimingDelay变量,函数TimingDelay_Decrement会将变量一直减到0。
5 u2 B8 {$ g+ x! H2 R
  1. /**
    % a2 l  L. K$ T  b9 K$ H1 @
  2.   * @brief  Decrements the TimingDelay variable.5 p' H9 ]( S" J1 n- `6 ]! E9 `
  3.   * @param  None  G2 ^& H; E% m  ?% u- h
  4.   * @retval None# b. d0 l" r6 ^1 R9 ?! }  \
  5.   */0 h) o7 N5 A' E: {8 s
  6. void TimingDelay_Decrement(void)
    . C5 H3 S  n$ U+ B
  7. {
    % {# ]: w# a) n1 n
  8.   if (uwTimingDelay != 0x00)
    1 u) L, J& I* H
  9.   {
    * u1 ^- w7 l, g* h- u: u: ^1 f
  10.     uwTimingDelay--;6 F3 J. z7 G) @" l8 b) z6 f
  11.   }8 Z, ~+ Y% ?7 v8 `% U
  12. }
复制代码

! j9 ]+ T1 L* z( S/ K这个函数在哪里执行?经查找,在SysTick_Handler函数中运行。谁用这个函数?
  1. /**. X- t/ F9 O4 T* d
  2.   * @brief  This function handles SysTick Handler." q+ H2 X0 B2 ~2 H8 n( B5 {$ K1 a' n. y
  3.   * @param  None
    % Y2 f# V. d' o
  4.   * @retval None
    * \( _, }2 @  j' y: Q
  5.   */( y' N1 b4 [+ e+ \/ }
  6. void SysTick_Handler(void)
    / L: f4 u5 d4 S  @% H
  7. {
    9 f: m' B. K; {8 b1 s
  8.   TimingDelay_Decrement();
    - E7 F* A1 f: k( R  [" P2 n7 Y1 ~
  9. }
复制代码

- Y+ W! o. z! R经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。
  1. __Vectors       DCD     __initial_sp               ; Top of Stack5 ]1 T. w# T  p9 ?+ P, r0 N
  2.                 DCD     Reset_Handler              ; Reset Handler. s. z1 r% N6 _2 K
  3.                 DCD     NMI_Handler                ; NMI Handler
    + J- A8 Z6 R. U
  4.                 DCD     HardFault_Handler          ; Hard Fault Handler
    # @1 z+ I" N* N) b+ P- M9 |* Q* R6 L
  5.                 DCD     MemManage_Handler          ; MPU Fault Handler1 a3 o1 T6 Q, x6 o, ^8 e* C
  6.                 DCD     BusFault_Handler           ; Bus Fault Handler
    . j! y( }. u& \* X3 y2 r  `
  7.                 DCD     UsageFault_Handler         ; Usage Fault Handler
    5 @9 }2 r. n6 p/ s1 J( P- i) ~
  8.                 DCD     0                          ; Reserved5 M3 R" M* }! y0 a4 T! G: D% ]
  9.                 DCD     0                          ; Reserved
    : _% C! ?% R3 ]3 A7 Q1 x% `) u
  10.                 DCD     0                          ; Reserved
    ; }' |& @+ p' t, K# {
  11.                 DCD     0                          ; Reserved; x9 f- R+ Y2 j4 w$ W
  12.                 DCD     SVC_Handler                ; SVCall Handler3 b% E& r, Z* X/ V: N3 Z$ d1 F
  13.                 DCD     DebugMon_Handler           ; Debug Monitor Handler
    8 u( w& ^) K7 B* ^5 v
  14.                 DCD     0                          ; Reserved
    6 h2 J# q5 U5 M3 N/ w9 J
  15.                 DCD     PendSV_Handler             ; PendSV Handler
    6 G2 Q# U% Q8 U# }( p
  16.                 DCD     SysTick_Handler            ; SysTick Handler
复制代码
  n! j& X- C. M1 b% [( ]
余下问题: |, n" h) z5 ^: S/ D( M- D
1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(InRoot$$Sections)是什么?3 ZI段,也就是初始化为0的数据段,什么时候初始化?谁初始化?4 M; @" u( C1 ?+ c5 t" D
: _+ g9 ]9 E+ U  O# B5 @; X2 I( m* W
为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!; a  d4 C" Z4 L8 q6 j4 k

* `3 d, R. ]9 T! `1 b
& i; w5 C' [( \1 v% d) C
通过MAP文件了解代码构成
/ [  _4 `  f9 c# K编译结果
( o0 ^, c$ E2 h
程序编译后,在下方的Build Output窗口会输出信息:
  1. *** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'
    . X* ?& n8 t2 h8 y4 `7 L5 g
  2. Build target 'wujique'# n5 j: t0 X6 o3 r  m% X
  3. compiling stm32f4xx_it.c...+ o  u* y1 \% L+ C0 j% C/ z) H6 T2 F
  4. ...
    + _2 v2 {) A  z
  5. assembling startup_stm32f40_41xxx.s...
    0 x) G7 M2 c6 f1 F# U
  6. compiling misc.c..." {8 t1 {9 i$ f& ]* {/ X$ ?
  7. .../ }! J0 |# Y  B2 C; S; s0 M
  8. compiling mcu_uart.c...
    8 D8 o/ _6 u1 @  M6 t+ O
  9. linking...) o* N  b2 @" o, S5 W
  10. Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  $ ~; g7 H; t! e
  11. FromELF: creating hex file...; s7 U8 D0 @/ ?
  12. ".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s).
    ) b# i8 r+ x9 K: ^% Q
  13. Build Time Elapsed:  00:00:32
复制代码
6 U4 |/ y# ^$ v" c
编译目标是wujique7 R: `$ c0 X" ]0 x# r9 i4 ~2 k
C文件compiling,汇编文件assembling,这个过程叫编译8 B( f2 H& l+ a4 O! f+ w
编译结束后,就进行link,链接。* w9 }3 N7 `( {/ |; `3 k% J2 X
最后得到一个编译结果,9038字节code,RO 990,RW 40,ZI 6000。CODE,是代码,很好理解,那RO、RW、ZI都是什么?! s/ W+ a# R/ _7 L5 H8 P4 ]
FromELF,创建hex文件,FromELF是一个好工具,需要自己添加到option中才能用( B/ ?" e( z" m& ~& P
( W  i4 }+ r" p$ z/ R" X
map文件配置
  v! k( }1 L5 i9 G/ q  l8 L1 E更多编译具体信息在map文件中,在MDK Options中我们可以看到,所有信息都放在\Listings\wujique.map
. Z: u- S+ ]5 u
8 T$ b" P" o+ I) |. }默认很多编译信息可能没钩,钩上所有信息会增加编译时间。& d7 g1 z  A, b+ P
* g$ }- z+ n2 B5 H" _
4.png
" ?& B/ [7 a' B, S4 M- c

5 z# c7 E$ W# dmap文件

; `5 Y& A: V8 I打开map文件,好乱?习惯就好。我们抓重点就行了。0 i+ z1 j. J: h$ w3 t; ?9 O6 c

( A6 _( P6 H2 u: x 5.png
. I/ }9 z' R$ H% ^. k9 Y

# y7 L- z4 D( x/ _2 S# R! \map 总信息/ K8 O  e" o+ j! M9 J. C% r

, A3 P; P- P) W% u从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。: k. ~" ]% O5 N  }7 R% o- X, U1 ?

/ k0 v3 E7 b- S: i7 o有多少RO?RO到底是什么?
5 A: y8 w. V) T* D( n2 m" ?9 A
4 H2 e0 p5 z4 s  s% X( d) o有多少RW?RW又是什么?
( o: x" d7 l; ~5 W+ T% ~
+ s, a( w) j+ \ROM为什么不包括ZI Data?为什么包含RW Data?
& M, l# U8 }; G( ]2 F0 A9 {+ ^& U) _6 m
6.png
! K) D# w" u! w& `$ u( p; WImage component sizes, k& K# Y9 G% i' q1 R7 V
* L/ Q$ [, G, @# q+ \
往上,看看Image component sizes,这个就比刚刚的总体统计更细了。
$ p" _: Y4 }5 I: E2 f' ?5 p4 c" c( r. t
这部分内容,说明了每个源文件的概况
1 L8 P- Z. M8 F8 M

7 {2 _: ~; M- x" J4 k首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些STM32的库文件。4 m& G0 ?' n( f6 x! p) S

8 m4 x8 u4 i6 y
7.png
# a' D  v/ }% Z
7 j. g& u2 N1 o' l4 }/ |$ Q5 e
第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1M flash上的程序压缩到能在512K上运行。
$ C" T9 G+ T+ h3 k2 X( c0 i
2 U. V% e3 ~  D
8.png

. J. Z. G2 e9 t$ K7 w- v; O4 x$ T

' s0 e$ [& y7 f& g9 x) G& s第3部分也是库,暂时没去分析这两个是什么东西。
9 [1 ~% f9 c6 V6 F8 L
, ^8 g  `0 z9 M+ Q: }' h( P. U
9.png

# R1 E6 }! c6 X% [" w$ @4 W
" D9 d8 T" e1 o0 u$ c- [+ K/ [
库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:7 L" M; P3 x0 M9 X
#include <stdarg.h>
7 {8 ~& I/ x1 S#include <stdlib.h>
$ M; I1 Q/ _  R0 E& A#include <string.h>  
% `. z8 s0 G; h, c) F6 `/ p0 M
: k5 I5 g( O- D
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。
% f4 H- B6 h" I" t7 B1 p+ \& x& x
4 U! s, _* ]) @

$ b6 a& f: C; S- q$ @文件map$ n+ |6 r7 B1 l8 ^; v% C/ Q
再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET
4 B+ U7 O, F! ?: ?5 S
0 L2 U' S2 K' k' u! E$ j库文件是什么?
/ D$ F$ {5 O2 }- Y6 E& F" |* C3 q3 T库文件就是别人已经别写好的代码库。
! v  Q5 J1 M5 D1 e) A  n" b5 M% |: {6 U
在代码中,我们经常会包含一些头文件,例如:
/ g5 e6 h! d; M4 s  W  n- c; X% Y
  1. #include <stdarg.h>* {0 j0 Z! k1 v. e4 L
  2. #include <stdlib.h>
    " G: ~. R3 ?$ s' M% K' B. I/ ]
  3. #include <string.h>
复制代码

. i6 {; D2 l* X) x! e2 o这些就是库的头文件。相关文章:C语言中的头文件。这些头文件保存在MDK开发工具的安装目录下。
0 T( T: i2 I. D3 ]4 ?
) O7 n& S0 i( |+ Z我们经常用的库函数有:memcpy、memcmp、strcmp等。: d# b( D6 k( y) l; o

/ D" \7 Z) U" H* g% F3 @只要代码中包含了这些函数,就会链接库文件。
4 v, ]( B( V: b* v$ r) ^1 N: a! Z0 D5 p4 C3 M
文件map7 F) t. X5 f' |8 y, B+ i: b
再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET, j& j: c8 S2 a; W0 X: y" ^$ ^

0 k, U) ]' c1 j% G5 m* I( F
10.png

* R1 I6 z* F0 B, a+ C
3 p& Y; Z! f0 F5 k- k0 |
每个文件有有多行,例如串口,4个函数。
0 R# C* \+ @6 x8 t; W
! N: q2 }6 k: {5 B2 ~0 q3 [
11.png

1 G! a& U3 i; r! R9 L; s
" |% b& Y6 n3 q& H
然后是RAM的,main.o中的变量,放在0x20000000,总共有0x0000000c,类型是Data、RW。串口有两种变量,data和bss,什么是bss?这两个名称,是section name,也就是段的意思。看前面type和Attr,' m8 x2 f5 C1 T6 T$ w+ ]

  f0 F. h* V/ M8 n# {/ ZRW Data,放在.data段;RW Zero放在.bss段,RW Zero,其实就是ZI。到底哪些变量是RW,哪些是ZI?/ V) q2 S& Q5 H  }3 j! K) w
: R! Q2 D: Z9 f9 \" x9 }% q
12.png

; L& d' X. J  H# m- q) _3 T% ^

0 m0 d' _9 O9 f/ G2 zImage Symbol Table8 V. r4 |) k7 m$ l- B7 `

# B; `4 q% ]7 |- w' t2 M# F: M3 ^再往上就是Image Symbol Table,就更进一步到每个函数或者变量的信息了
! @! T6 e: c. o' p$ ~- g1 ^, r8 k1 k7 P2 P: U# v! m
13.png

7 A; m7 E4 s+ a' z- K. s$ D& ^7 u. p
例如,全局变量TestTmp1,是Data,4字节,分配的位置是0x20000004。4 a  f  O- v0 z% V5 t8 i9 L6 _

. G8 [+ T- t4 A: A! _0 a
15.png
) C* c: A8 K4 x
( e/ s* g' b% k9 W. i% D2 \
TestTmp3数组放在哪里?放在0X080024E0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几K几十K大,不可能也没必要放到RAM区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。  r5 ~% V' S! X" K: p

9 k* H0 j1 ?7 i# |const的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习, q3 }( |2 Y7 [3 g: n7 H# f

: i9 c& c! ?9 }7 w
16.png

# }7 n% N/ ]. y) C9 `  {1 Q" B
, ]" v7 |! H; }$ k# @  V0 u
那局部变量存放在哪里呢?我们找到了test_tmp3," [: g6 e& `0 r( F

: M5 G, Z* Q# Q0 f3 d
17.png

- Y/ p7 @' \* F" u+ g0 ~

$ N4 {) e3 k: j. d没找到test_tmp1/test_tmp2,为什么呢?在定义时,test_tmp3增加了static定义,意思就是静态局部变量,功能上,相当于全局变量,定义在函数内,限制了这个全局变量只能在这个函数内使用。哪test_tmp1、test_tmp2放在哪里呢? 局部变量,在编译链接时,并没有分配空间,只有在运行时,才从栈分配空间。
  1. <blockquote>u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
复制代码

$ S2 _5 ?6 `! K& l. {9 P上一部分,我们留了一个问题,哪些变量是RW,哪些是ZI?我们看看串口变量的情况,UartBuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文Block Started by Symbol的简称。
1 Z& a, y3 e9 |1 X* `0 T
" V7 Y4 }5 K3 f6 l+ P
18.png

% i: e6 K% Y3 e: I! k" n% Y
& E. y7 P& Z5 C2 N到这里,我们可解释下面几个概念了:
) m3 i3 X( |3 rCode就是代码,函数。
; y4 @3 i- h* [; vRO Data,就是只读变量,例如用const修饰的数组。# Z) Y3 i) o: V+ s
RW Data,就是读写变量,例如全局变量跟static修饰的局部变量。
& P$ `' ^. [( l0 H" s, YZI Data,就是系统自动初始化为0的读写变量,大部分是数组,放在bss段。
3 M* L( i6 V7 a4 C( l' XRO Size等于代码加只读变量。9 J# l" ^1 u7 y! W
RW Size等于读写变量(包括自动初始化为0的),这个也就是RAM的大小。
$ {3 X# E; o9 Y1 u) F' z* H8 j9 xROM Size,也就是我们编译之后的目标文件大小,也就是FLASH的大小。但是?为什么会包含RW Data呢?因为所有全局变量都需要一个初始化的值(就算没有真正初始化,系统也会分配一个初始化空间),例如我们定义一个变量u8 i = 8;这样的全局变量,8,这个值,就需要保存在FALSH区。
# Q/ e$ R$ ?/ O2 J
* O8 c+ e5 ~; C2 u9 ]( M
19.png
2 a( \3 y5 s+ ^- C% I6 m1 L

. G2 }4 s. |# r% `2 a3 p( g我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579
+ P8 s! s( y% `5 ]5 o
( H' X7 U5 w2 x0 S! d
20.png
0 b! x4 p- f7 j! t& u$ ?2 R

) S! B2 I* H+ B- a- }( H/ S9 Y& p
main是main,放在0x08000189! ]# N% ]: L3 I8 t  v" r
6 z* z8 k4 A: f$ `. H
21.png
) C  t" T8 k; ], `  L
9 K' t) T( e, `1 r( P9 u6 E
__main到main之间发生了什么?还记得分散加载文件中的这句吗?
6 F9 A! a  c3 Z* o) j+ m*(InRoot$$Sections)
! C) G8 ~5 l" u! q9 R8 f/ R( n- ^  m6 r2 c  h
__main就在这个段内。下图是__main的地址,在0x08000189。__Vectors就是中断向量,放在最开始。. ]/ g! {' E: }% A) T1 \
" ^2 e0 ?, f, `6 f; r* h
22.png

0 j% O! R0 B* [6 F7 T1 o5 m) ~0 [0 r$ R0 }
在分散加载文件中,紧跟RESET的就是*(InRoot$$Sections)。
2 F/ k2 M% }$ U0 O- c# V3 F4 f# D; p% ~' }. }, P
23.png

: ~# g# }$ [- {! ]7 q% m

  \1 t% d0 x$ z3 }2 \/ [& c' X, o而且,RESET段正好大小0x00000188。
. v4 `& j$ z' q/ E* M# i. o% U' [! Y  G6 d$ V
24.png
! B' l; l/ w  j% b( ]1 _4 y$ S. \
( `8 i8 t! R1 l2 C6 o! q
巧合?可以参考PPT文档《ARM嵌入式软件开发.ppt》。
% U: X1 g3 `+ G/ W. @, p- l; [* w" K# M
25.png
" h) I  U# `2 e& r0 L1 n

2 m( _3 [4 x+ m5 X& R6 |% `5 f
这一段代码都完成什么功能呢?主要完成ZI代码的初始化,也就是将一部分RAM初始化为0。其他环境初始化……5 {3 V( o' g5 e5 H2 O

  W& N8 a; O& i/ S8 ~
' P5 x- q+ w$ K( t; h
最后- S$ R' p0 K+ r1 v% A
到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。
, `* y" R# g% i' H' U
, ~5 q8 u2 |0 L8 D, Z9 |
& l7 t7 e/ B( O# G4 O, F4 y
转载自: [color=var(--weui-LINK)][url=]STM32嵌入式开发[/url]- e! ~0 ~- G) f' F2 w$ b
如有侵权请联系删除- f" O' ?2 B& m6 w# Q

1 F9 R0 J$ U/ }6 g5 F) ]  J4 e, v) h8 V2 F3 d. V
收藏 评论0 发布时间:2024-7-29 15:25

举报

0个回答

所属标签

相似技术帖

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