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

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

[复制链接]
攻城狮Melo 发布时间:2024-7-29 15:25
本文分析STM32单片机到底是如何软硬件结合的,分析单片机程序如何编译,运行。" K7 d6 n0 b* [5 d

3 Z1 ?' i+ m& q% T软硬件结合/ a2 h8 ^$ P, g; e+ e( ^2 b( E- B
初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写P1=0X55,就可以在IO口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。
' o; x! N* `* g& a

. S/ `; h9 t4 Y4 o0 Y1 U# O, N0 M寻址空间5 h& f0 D; b5 G/ ~9 _1 G
什么是地址空间呢?所谓的地址空间,就是PC指针的寻址范围,因此也叫寻址空间。
5 O* b4 M4 a4 {2 j! n6 S" H- u* w
$ j9 K: f. |$ J! c- ?大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,PC指针就是一个32位的二进制数,也就是0xffffffff,范围只有4G寻址空间。现在内存越来越大,4G根本不够,所以需要扩展,为了能访问超出4G范围的内存,就有了64位系统。STM32是多少位的?是32位的,因此PC指针也是32位,寻址空间也就是4G。3 u8 W6 P5 Q' f4 b
$ I$ [% ]1 x1 u
我们来看看STM32的寻址空间是怎么样的。在数据手册《STM32F407_数据手册.pdf》中有一个图,这个图,就是STM32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫Memory map,用一个新芯片,就先看这个图。% @" @: K% x$ P( s+ E

7 S/ m% a5 f2 W& S) Q! n
微信图片_20240729152315.png
. n- V" Z1 ?" P0 M9 E

4 ^# o" n0 s: b. g' q7 T5 i& Q( T$ K2 ~- L( E9 |
最左边,8个block,每个block 512M,总共就是4G,也就是芯片的寻址空间。
$ g0 V" I1 j- P, K$ U1 _3 W  h& B$ Y) f1 Y
block 0 里面有一段叫做FLASH,也就是内部FLASH,我们的程序就是下载到这个地方,起始地址是0X800 0000,大家注意,这个只有1M空间。现在STM32已经有2M flash的芯片了,超出1M的FLASH放在哪里呢?请自行查看对应的芯片手册。
5 r( b; n. E) X, C6 F; [% a, s8 o1 F/ W0 u
3 在block 1 内,有两段SRAM,总共128K,这个空间,也就是我们前面说的内存,存放程序使用的变量。如果需要,也可以把程序放到SRAM中运行。407不是有196K吗?: C2 ]" b8 b: Z
; t' s( \  y1 Z  Z% J+ u9 v
其实407有196K内存,但是有64k并不是普通的SRAM,而是放在block 0 内的CCM。这两段区域不连续,而且,CCM只能内核使用,外设不能使用,例如DMA就不能用CCM内存,否则就死机。9 D5 M4 g: a, F  G+ s
; o* a7 M6 O, }% x0 c7 G+ [1 q
block 2,是Peripherals,也就是外设空间。我们看右边,主要就是APB1/APB2、AHB1/AHB2,什么东西呢?回头再说。/ R( _  F& [' D6 S
8 g6 M, P) ^; J0 g
block 3、block4、block5,是FSMC的空间,FSMC可以外扩SRAM,NAND FALSH,LCD等外设。4 v* k5 [$ t0 d3 Z

+ |5 K7 d# b9 i9 s) f1 Y好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。对于这个疑惑,也可以看此文:代码是如何控制硬件的?在IO口输出的例程中,我们配置IO口是调用库函数,我们看看库函数是怎么做的。
7 Z. H7 H% }8 h. n( }6 A; V* f, o' L2 m0 e: p
例如:
" M( ~9 ]/ @& n% e! T- m0 M$ b
  1. GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
复制代码

+ a1 V8 P8 u) d8 W) d/ O这个函数其实就是对一个变量赋值,对GPIOx这个结构体的成员BSRRL赋值。
9 @  H2 Q& R+ {6 g0 o+ H
  1. void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
    % R! o& ]3 s% }4 H* Z' Z
  2. {2 D% K8 q. c% h. B6 _
  3. /* Check the parameters */9 C9 ?. l: L: ~- L+ z! ~, }8 w0 s& M' t
  4. assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
    + }# }  w4 H- Y4 L7 H
  5. assert_param(IS_GPIO_PIN(GPIO_Pin));
    7 I8 h- N  Z7 X6 u2 G1 K  U4 z
  6. - f5 z- c9 d/ a% y
  7. " I& o2 V2 m$ C# B# ~( E
  8. GPIOx->BSRRL = GPIO_Pin;5 x% o7 z7 I  @! j
  9. }
复制代码
+ P& q+ f# V: a0 K
assert_param:这个是断言,用于判断输入参数是否符合要求GPIOx是一个输入参数,是一个GPIO_TypeDef结构体指针,所以,要用->获取其成员, L& X9 z# F) ^8 |
5 x' V0 g5 V" \9 g
GPIOx是我们传入的参数GPIOG,具体是啥?在stm32f4xx.h中有定义。" o+ A0 f, I. T! u! u9 Q: w* B: Q* S
  1. #define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
复制代码

# O8 D$ m: T, S- M; W5 d+ N7 oGPIOG_BASE同样在文件中有定义,如下:. l& k$ h  ]+ y& B/ Y
  1. /*!< Peripheral memory map */
    * g2 V) V4 c, p. E" A0 t
  2. #define APB1PERIPH_BASE       PERIPH_BASE
    6 k5 {; _0 I& t
  3. #define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)- b5 r9 Q, {# x5 ^  A, `
  4. #define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)' g1 q- Z# x- q1 x- N0 @
  5. #define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)
复制代码
* R$ w) B9 w6 p9 P
再找找PERIPH_BASE的定义
" A4 A* `1 U$ d1 w5 K, T
  1. #define PERIPH_BASE           ((uint32_t)0x40000000)
复制代码
到这里,我们可以看出,操作IO口G,其实就是操作0X40000000+0X1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是SRAM空间地址,0X40000000+0X1800是外设空间地址。" b4 h8 H$ C9 m; R1 ?" ?  \
  1. u32 i;
    & f0 i/ m4 B& h5 I# D4 d+ i
  2. i = 0x55aa55aa;
复制代码

7 v0 W1 W5 k9 P这个外设空间地址的寄存器是IO口硬件的一部分。关于如下图STM32的GPIO文章推荐:STM32中GPIO工作原理详解。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0X40000000+0X1800+0x14.
2 i+ c% n; k0 M: Z
+ |! D4 b. z& N
微信图片_20240729152319.png
( X( w% f5 B* T9 c

3 Q* _9 ]% G& K% P% m* H控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。  k! Y# }! C0 R3 ~- h
9 R1 h: O" G# C, c: J. j
寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或RAM。寄存器为什么能控制硬件外设呢?因为,初略的说,一个寄存器的一个BIT,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。2 z! z, v8 L, |

) V- x  T, f7 }7 {纯软件-包罗万象的小程序3 W' I) _) G- Q) C3 ?) F5 `, O
我们已经完成了串口和IO口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?关于程序是怎么在单片机运行的,也可以看此视频:动画演示单片机是如何跑程序的。代码到底放在那里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。8 j0 L6 w! V* W& ?

6 @! M1 G2 m3 l/ u) q; e# X分析启动代码
6 {- _* L7 P) D9 {3 F' r. m, W3 q函数从哪里开始运行?$ D8 n- |# T5 V! b
每个芯片都有复位功能,复位后,芯片的PC指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,PC可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在STM32中,复位到0X08000004。因此复位后运行的第一条代码就是0X08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。3 r. {5 c7 s2 C# B/ t

" l) `7 l: v9 @8 j0 t& F芯片复位时,芯片从中断表中将Reset_Handler这个值(函数指针)加载到PC指针,芯片就会执行Reset_Handler函数了。(一个函数入口就是一个指针)
  1. ; Vector Table Mapped to Address 0 at Reset
    / ?' E) Y; p- h' z
  2.                 AREA    RESET, DATA, READONLY
    4 Y0 Y+ [' Q0 n  x% p% X) X
  3.                 EXPORT  __Vectors0 n- I& {* I8 ~
  4.                 EXPORT  __Vectors_End) A$ ~4 V' S$ `$ A6 H. ~: x9 Z
  5.                 EXPORT  __Vectors_Size/ C9 j6 I: I9 P& v" ?: y8 F0 ?8 H3 X

  6. 9 w- s* x. q9 x4 q8 r
  7. __Vectors       DCD     __initial_sp               ; Top of Stack
    / I0 Q# d- [: V9 j
  8.                 DCD     Reset_Handler              ; Reset Handler, f4 u& K6 a- x+ f# g" V% e
  9.                 DCD     NMI_Handler                ; NMI Handler0 W, c( d  t2 T; a2 T$ n" A- i
  10.                 DCD     HardFault_Handler          ; Hard Fault Handler/ |4 m& M1 S% F% z5 b8 z6 e
  11.                 DCD     MemManage_Handler          ; MPU Fault Handler+ n6 m* g9 b% B  ?8 K/ i
  12.                 DCD     BusFault_Handler           ; Bus Fault Handler  T; @; H- S0 b4 ?% I* \
  13.                 DCD     UsageFault_Handler         ; Usage Fault Handler
复制代码
, n1 ^" |# F- ^0 P: D
Reset_Handler函数,先执行SystemInit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?  ]* E5 \9 P) r8 ^8 P& w7 B3 d
* P, p: u3 k- j4 v
是我们在main.c中定义的main函数吗?后面我们再说这个问题。4 @1 f, f2 q  P7 a: E7 X
$ A9 ~1 B+ v1 e+ G
微信图片_20240729152323.png
# P; X, t* X/ A

* n+ P* e; p1 X: B( l* k芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujique\prj\Objects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。
7 x, L7 d& [  ~
' K; ~0 m* o* ^! N0 A" s
在MDK软件Options菜单Linker下有关于这个菜单的设置。. B6 g) ~7 W) E3 u" f3 Q
& g, G# |4 l, r$ P" Z. r; x3 b6 [
微信图片_20240729152326.png & H6 D7 S- F1 y# I2 n& A7 `! h
6 J) }# @# q  O1 V" _

4 P; J3 Z- N3 B& F: n2 r  y

  b3 b; E# c$ g. Q把Use Memory Layout from Target Dialog前面的勾去掉,之前不可设置的框都可以设置了。点击Edit进行编辑。
" L# P' ?- ~$ v: t: u. j* X
& a( v4 K( ?! ]: f
2.png
" m: c' C5 d  Z/ H" s3 z7 M2 u

4 n( F( R% u* u3 R
+ T' Z9 o8 s: |/ J5 ^8 I
在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。
( ]0 ]/ H7 ?" p  l. S- B
: i! f' W2 d1 |5 V其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定FLASH跟RAM的大小于起始位置,当我们把程序分成BOOT、CORE、APP,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到RAM中运行。
9 o$ p  |8 n! A) A9 e6 j9 K- y' n2 c8 y
3.png

2 X# c4 ?4 L+ \8 |& N; B, Z# `

: ^/ T" \0 R7 Z7 b( A( k, O- z7 m从这个基本的分散加载文件我们可以看出:
1 L6 S) [) M0 J6 f/ M5 x3 ]
8 F  W( V* I% ]. U$ D( R  X第6行 ER_IROM1 0x08000000 0x00080000定义了ER_IROM1,也就是我们说的内部FLASH,从0x08000000开始,大小0x00080000。
) J7 @2 u" b0 g  A2 j/ l
( b% m  w( U* w第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。
: n7 \$ E; y4 p3 Z
, i* ~  C7 S+ q* G, n: O
第8行 *(InRoot$$Sections)什么鬼?GOOGLE啊!回头再说。3 C' @. a# X. f$ B

6 z3 G: B  _7 p, n" w4 ^第9行 .ANY (+RO)意思就是其他的所有RO,顺序往后放。就是说,其他代码,跟着启动代码后面。% i0 c* |5 G) \: s3 P4 v, c
% e( s3 F9 {, G0 ^7 Z2 T
第11行 RW_IRAM1 0x20000000 0x00020000定义了RAM大小。
  e5 P7 L  f, ]4 a2 a# B6 r
( u6 s, M5 l- u- C/ h9 C
第12行 .ANY (+RW +ZI)所有的RW ZI,全部放到RAM里面。RW,ZI,也就是变量,这一行指定了变量保存到什么地址。
0 X0 W" s  R: `7 y- T7 T
/ R; x! R: h; P0 k' Y% W% C分析用户代码
/ f" W+ u# D# j& A, J, o4 L到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。# u' V0 s- F$ h7 Q) ]) i3 Z$ ]

9 j7 E5 K, c) X% Q  `5 S1 程序跳转到main函数后:RCC_GetClocksFreq获取RCC时钟频率;SysTick_Config配置SysTick,在这里打开了SysTick中断,10毫秒一次。Delay(5);延时50毫秒。
  1. int main(void)
    & Q6 M+ I0 W, H% z: E; e
  2. {
    + C" h$ H$ }4 k
  3.   GPIO_InitTypeDef GPIO_InitStructure;& o, [) s7 h8 N0 Y* U6 \

  4. # }- w8 R* L5 R
  5. /*!< At this stage the microcontroller clock setting is already configured,
    5 u* L" u* \/ g" N, Q: g( s5 }+ I
  6.        this is done through SystemInit() function which is called from startup
    5 c+ |) ~8 x- x' }) ]; L1 n. F9 p0 O4 o
  7.        files before to branch to application main.5 o* m* ~$ m: r0 k6 @- W
  8.        To reconfigure the default setting of SystemInit() function,/ R8 ?% b% m# o2 T! m
  9.        refer to system_stm32f4xx.c file */
    ' l0 y- `8 [. p$ q
  10. 8 G1 u2 Y3 S3 c
  11.   /* SysTick end of count event each 10ms */3 }: X% J5 |: r9 ~8 {$ `; g2 X
  12.   RCC_GetClocksFreq(&RCC_Clocks);" J9 r3 v3 ^3 h! u( B' u3 h0 s
  13.   SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);) I+ B5 L; m# D0 E- Z

  14. ! S+ D0 O8 c. ]/ g1 @
  15.   /* Add your application code here */% c- P9 b' `6 j
  16.   /* Insert 50 ms delay */8 O, X1 t$ @: I1 S0 \
  17.   Delay(5);
复制代码
  L/ Y6 ?$ I, K4 u' S
2 初始化IO就不说了,进入while(1),也就是一个死循环,嵌入式程序,都是一个死循环,否则就跑飞了。
  1. /*初始化LED IO口*/
    8 m$ W5 `! [9 e4 C+ `% ]* B
  2. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);; h) L- n! q4 I  m& _3 b

  3. ' F2 ]/ X7 \9 O& w6 `/ Z
  4. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;
    " {" j. s( I  H* p9 a6 ?
  5. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;$ R9 I. _; Y; p1 Z# |$ x
  6. 6 ?5 y, U& c& M! e8 p
  7. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    / z9 Y8 M! g8 `3 S- _8 U* h# ^- u
  8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;( `( R# {; P* `0 u+ d: {( K6 x6 x
  9. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    * R; e" O  f! O( l8 N
  10. GPIO_Init(GPIOG, &GPIO_InitStructure);   
    , _& d( S% s4 T% q- X

  11. 7 I* n' m4 a& P
  12. /* Infinite loop */
    : Z1 S% [  E- Y( Z) f  K9 `) f8 T; Q- a
  13. mcu_uart_open(3);! t. g" d6 ^3 z! n
  14. while (1)& M& K! F& L1 M. h
  15. {
      p' T% u8 G1 x! b* d  h. s
  16.   GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);  K  G; g* h/ M( z* v, G; g2 a
  17.   Delay(100);: T2 v, E7 ^$ U: ?
  18.   GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);0 Q7 ^* i7 c8 n2 W" \* d/ Q6 z
  19.   Delay(100);
    2 N. ]0 w: ^3 `) V' e( w
  20.   mcu_uart_test();
    5 Y. h: T2 O% _; T3 f4 Z4 T

  21. : j& C! L& s3 K* g* _4 C& w
  22.   TestFun(TestTmp2);% a7 N' G8 \- B6 i5 o6 L, y* X
  23. }
复制代码
/ y; n9 K1 q# c! z& {1 W0 f
3 在while(1)中调用TestFun函数,这个函数使用两个全局变量,两个局部变量。
  1. /* Private functions ---------------------------------------------------------*// o8 t' V/ R% w9 B" F
  2. u32 TestTmp1 = 5;//全局变量,初始化为5( x3 Y& e% E8 |' h
  3. u32 TestTmp2;//全局变量,未初始化
      p! S2 K- E! b* }

  4. 2 |& {: \  a  [3 ^# U, P) [- \
  5. const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};/ T3 o5 z( ~/ L' S+ `' x# [

  6. - B  B, i  q8 b, J* `) |; C7 S
  7. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值, g7 @) p/ d5 C, I6 [
  8. {
    # T4 [6 l* a8 z: f, f, ]2 m2 F5 e) a# H
  9. u8 test_tmp1 = 4;//局部变量,初始化
    5 c3 J  H: C  ~6 x  ^
  10. u8 test_tmp2;//局部变量,未初始化: }- F1 V- F% }! P1 D0 J7 a
  11. $ N; T( u% E( h
  12. static u8 test_tmp3 = 0;//静态局部变量
    + g; u, G/ h$ K! K

  13. : f! O# a- \6 l. i
  14. test_tmp3++;
    / r+ k! ?9 E; N
  15. ' S! x& w! a0 f+ u9 J
  16. test_tmp2 = x;
    ; |+ j& f$ Q* F" B

  17. ; u, y6 K% f5 a! N7 J, d4 a' q2 y
  18. if(test_tmp2> TestTmp1)" o& E2 r8 l" l* D; w
  19.   test_tmp1 = 10;1 i( d/ h' ^6 }! K5 C( x, \
  20. else
    9 h" i, g+ e1 k7 G( `, F1 t
  21.   test_tmp1 = 5;
    8 y+ R: y, X5 e
  22. ) p  x& N) o( W5 Z
  23. TestTmp2 +=TestTmp3[test_tmp1];) b4 u1 x5 {- Z6 ^
  24. ! m9 W4 S% e" v! `
  25. return test_tmp1;
      b2 J, n( e- E# m
  26. }
复制代码
* l' a* R8 {% ?# x
然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。相关文章:STM32中断系统。我们查看Delay函数,uwTimingDelay不等于0就死等?谁会将uwTimingDelay改为0?
  1. /**
    7 T8 A- m8 q) A& S0 Z
  2.   * @brief  Inserts a delay time.
    9 X; D1 ^. I, ~
  3.   * @param  nTime: specifies the delay time length, in milliseconds.
    3 s5 c3 h7 }% m" S9 O2 S& y
  4.   * @retval None
    8 @$ m, p% L' |, J( K; X
  5.   */
    ' s# T' @: V& c9 |
  6. void Delay(__IO uint32_t nTime)
    , h5 H5 @/ J  K
  7. {
    3 v& H) d" F* z  z
  8.   uwTimingDelay = nTime;
    ( M% A6 F! Q/ p: E- ]

  9. - @$ C3 a2 u+ L& i$ ?% d& C
  10.   while(uwTimingDelay != 0);4 M3 }0 @+ K. U
  11. }
复制代码

& i6 q- B$ V- j7 ~( f搜索uwTimingDelay变量,函数TimingDelay_Decrement会将变量一直减到0。1 i* y- V: o! K' d" V
  1. /**
    7 d* d1 ?& N( a* b# W& k: E5 B
  2.   * @brief  Decrements the TimingDelay variable.; i* D5 b8 x; ?+ i
  3.   * @param  None
    % O, f0 N$ e! O; f7 v1 h
  4.   * @retval None
    : k  L/ V2 V7 m. w6 a+ |
  5.   */0 s% T& l7 ?' _
  6. void TimingDelay_Decrement(void)1 W4 b' G( o2 n# _9 x" \1 ]5 u
  7. {2 W4 g, C  [- W& ~, X( l, F
  8.   if (uwTimingDelay != 0x00)
    8 R1 X- B3 Q& {& w3 _
  9.   {
    # N: ]4 h9 r. }% Q& \+ v
  10.     uwTimingDelay--;4 f( L% @6 u: z9 y* [2 O+ [
  11.   }. h9 a; r, J: v$ m; p$ g
  12. }
复制代码

" f) s) O: g/ Q$ \, N3 W2 o这个函数在哪里执行?经查找,在SysTick_Handler函数中运行。谁用这个函数?
  1. /**: k, j( |% o7 G  B3 x. C  R
  2.   * @brief  This function handles SysTick Handler.
    - h' u3 ^2 ^/ ?7 j- A& Q
  3.   * @param  None
    5 z& l; O" J  N4 N9 x
  4.   * @retval None
    " R: d2 J8 P' |, }" U3 \
  5.   */
    ( [4 J- g6 D$ B* m+ Z
  6. void SysTick_Handler(void)) [: v4 ]- Z  ]0 F
  7. {/ j4 c1 j$ Q1 `9 s: F, ^9 p
  8.   TimingDelay_Decrement();6 E. J- G* C' D* b2 \& I+ t
  9. }
复制代码

+ a+ I- \8 a8 N( r+ ?5 E经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。
  1. __Vectors       DCD     __initial_sp               ; Top of Stack
    7 O2 y/ O1 |/ b# ?; ]8 N; \2 p
  2.                 DCD     Reset_Handler              ; Reset Handler
    $ @0 d0 d6 ?4 ?
  3.                 DCD     NMI_Handler                ; NMI Handler
    2 d% U/ h! W9 E3 K: {
  4.                 DCD     HardFault_Handler          ; Hard Fault Handler
    8 L0 \  a5 W$ F$ [+ ~; K+ i& }, r
  5.                 DCD     MemManage_Handler          ; MPU Fault Handler  n9 p: {0 U* f/ m
  6.                 DCD     BusFault_Handler           ; Bus Fault Handler& J$ u9 y+ G) o4 W% `5 g
  7.                 DCD     UsageFault_Handler         ; Usage Fault Handler
    . }) ^# D/ X- G/ H
  8.                 DCD     0                          ; Reserved
    & d+ @. h: o+ S/ k4 ?. I" l
  9.                 DCD     0                          ; Reserved
    ' k" [" J" H( I; r5 S& M
  10.                 DCD     0                          ; Reserved4 ]& _6 B& w. @4 g# |" B8 l
  11.                 DCD     0                          ; Reserved. u' u" _+ t' K0 W4 Y7 Y
  12.                 DCD     SVC_Handler                ; SVCall Handler
    ) }8 C& [( h8 R3 r: S+ j
  13.                 DCD     DebugMon_Handler           ; Debug Monitor Handler6 d8 k+ }( i5 e1 X
  14.                 DCD     0                          ; Reserved/ c$ ?5 x& ~% U# [# e" i
  15.                 DCD     PendSV_Handler             ; PendSV Handler
    # j9 N. C& c; j$ m* N8 L
  16.                 DCD     SysTick_Handler            ; SysTick Handler
复制代码
+ |) X) ]  }& N/ K1 @9 T9 i
余下问题
" P4 L+ _( h2 `1 I* }! \2 }4 H4 M1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(InRoot$$Sections)是什么?3 ZI段,也就是初始化为0的数据段,什么时候初始化?谁初始化?4 x2 Z5 a; S# D2 e/ d% d0 z

* N0 w8 R4 X7 S为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!$ h0 o; |; D5 M" {9 N
  }- M& L  l0 @5 V; G

7 O  {3 B! L5 F通过MAP文件了解代码构成: X; N! J+ `: z* D9 l
编译结果
5 g- {0 y" j! ^+ x& T
程序编译后,在下方的Build Output窗口会输出信息:
  1. *** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'
    ( P( c. |1 G- ^' u  r% ]
  2. Build target 'wujique'
    3 q) `. t0 W' L' A0 k8 O) F
  3. compiling stm32f4xx_it.c...3 F+ f7 |- F# m5 o
  4. .../ d! e1 I5 C+ P5 g( f% Y5 j3 m
  5. assembling startup_stm32f40_41xxx.s...
    : F& o! i( \$ _: Q# ^# C9 w$ B
  6. compiling misc.c...  G6 `+ @9 N9 C* F* H
  7. ...
    % u, Z- X; K; j: m' V& `& U
  8. compiling mcu_uart.c...& e% O/ s3 l% v: z
  9. linking...+ ?& c7 \4 V- ^: r7 ~
  10. Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  # `/ \! j. v& {& X) x+ h3 l
  11. FromELF: creating hex file...
    / k+ I% M& t; S% z2 Q3 S8 T( Q3 p% \
  12. ".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s).: N0 R- V* d* i6 q
  13. Build Time Elapsed:  00:00:32
复制代码

: M. O2 b+ [1 A' X) I编译目标是wujique+ l+ C3 y: s9 _+ B* m+ a" k
C文件compiling,汇编文件assembling,这个过程叫编译
) m$ s! a. i! n. r编译结束后,就进行link,链接。
( u% u; V' T/ @- K0 h$ [1 w: i7 A最后得到一个编译结果,9038字节code,RO 990,RW 40,ZI 6000。CODE,是代码,很好理解,那RO、RW、ZI都是什么?
: V  Q5 k2 b, b. U1 ~FromELF,创建hex文件,FromELF是一个好工具,需要自己添加到option中才能用
: F, @! p  y% J% [/ k- \* i; V" c' T/ F0 Q1 F2 b! N0 ^
map文件配置
. g% P. ~& X  b4 Q$ H更多编译具体信息在map文件中,在MDK Options中我们可以看到,所有信息都放在\Listings\wujique.map4 g8 I0 Y/ O$ k) m. [. r
" b( ?. }1 s: d
默认很多编译信息可能没钩,钩上所有信息会增加编译时间。1 p) F5 O- u4 N8 N
9 s5 n0 c+ |7 K6 ~  G
4.png
- n0 I/ K* P. x9 ^0 `0 B
3 H3 d% j5 k4 e4 ?
map文件
3 Q3 r( z1 Z3 {! ~9 c2 Q
打开map文件,好乱?习惯就好。我们抓重点就行了。1 Q# f1 W! W+ A- v* t5 N
/ q! Q+ o) t1 M8 }! f9 S
5.png 3 u! `4 m8 M1 F, M

# x' n; f2 |9 T0 |* F0 c; dmap 总信息
' s, G( m; _' j; l& I, B3 X! [  W3 N
从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。( T$ U8 Q6 ~/ Y( |! A
, }4 m7 X0 j, a8 a
有多少RO?RO到底是什么?9 Q6 f  x# k/ s4 m% D" ?

' J9 o* ], c) _% L% Z# A- H有多少RW?RW又是什么?
6 H! J* g- C$ L- h/ O
  `' d3 ]4 k1 Y% NROM为什么不包括ZI Data?为什么包含RW Data?
3 @) j! A+ A% [# [9 s  V3 z8 D2 v- {: I) G$ ]. {) E% V+ Q, W& a4 Q
6.png / T) C/ Y: s4 s+ ^7 q
Image component sizes( ^$ f; }' ^. @; x5 y

$ p; y7 Q, L, ^' n- L" Q& s: t+ }往上,看看Image component sizes,这个就比刚刚的总体统计更细了。0 {& z4 I- x9 M6 i: O
# K7 s0 V+ d6 z* k$ V8 H( I
这部分内容,说明了每个源文件的概况
5 `6 ?" W* W7 p! q' A& S$ p! [+ S
- D4 o  c5 x& b  X! Q% ~  L* \
首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些STM32的库文件。
+ n0 U0 G! |2 ~* f) b
0 ]' b$ a1 j" m
7.png
5 Y. |* p8 d4 J

5 e3 M! ?4 t& {# w5 f" r第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1M flash上的程序压缩到能在512K上运行。$ S. E0 ^2 a3 e: a! G" _7 R  X
& Q" t* Z0 w! f% N; Q6 E$ t
8.png

" L! I- {2 [6 j! S
3 U3 S7 J8 a/ H- \9 B0 c; P& N7 O
第3部分也是库,暂时没去分析这两个是什么东西。
- \5 f1 g0 J4 @4 t4 N+ B/ @* g2 t. v+ S3 `# H
9.png

) S1 _$ t1 F  h9 {

/ `9 @" R  K) \- t, U3 B  c& _9 M8 i库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:
* ?! S2 ~* \$ B, s- c. g#include <stdarg.h>
0 r* M; S& [- c# k4 |#include <stdlib.h>8 v- {: k, h) [, e6 E
#include <string.h>  ' Q9 x: q: [- {' K
  o/ j: q2 p" ?7 Y0 M* c4 x
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。' o) i- e$ b& Y' ~7 _' Q' m+ ^  L

% @3 y8 W" ~# B; s$ O7 u, O4 U
7 C/ Y) o1 Q2 F* q
文件map$ R( `0 B& b; r8 }3 k
再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET8 C- F; L8 L# y& S+ n

( A9 r( z% e  f( n- a% _库文件是什么?
% b* ?5 o% e7 @) R/ c8 P库文件就是别人已经别写好的代码库。
# ~' o1 k# W) Z+ Z, |
1 q  q2 @' R/ E3 ^, w在代码中,我们经常会包含一些头文件,例如:$ h! A2 P) r# N: X5 F# _
  1. #include <stdarg.h>
    ' T  W4 h# Z+ K0 g' ~" `
  2. #include <stdlib.h>; H6 d5 [( ?6 s2 Q  Z
  3. #include <string.h>
复制代码
' j& p! E5 b$ T6 p
这些就是库的头文件。相关文章:C语言中的头文件。这些头文件保存在MDK开发工具的安装目录下。* J, A$ l3 ]' K9 W
+ [# A& z# r7 r/ p
我们经常用的库函数有:memcpy、memcmp、strcmp等。' ]  m3 `' J1 l5 r. U: k
9 [$ N; Y3 [  |2 {
只要代码中包含了这些函数,就会链接库文件。
3 ]# W5 _, Y/ D4 m3 j0 j
7 ^) Q4 c; H! Q+ H" L文件map7 h( G0 V2 A# Q- f
再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET
' d5 T, a6 p. j) n* ?  {
; |8 s$ Q  B. E- P5 A3 F( G
10.png
+ k/ ^: k7 s2 w( }5 \. c2 {. @
" ~! ?: C& I* Q3 I$ X
每个文件有有多行,例如串口,4个函数。( [4 B: m, P2 f# _& Y
  Q* y! L8 z" p0 o2 c
11.png

5 t  k% _3 i$ _) P6 u' s# [

# J9 m5 L# a2 O8 V+ g% j4 B- r9 |9 Q4 S/ y然后是RAM的,main.o中的变量,放在0x20000000,总共有0x0000000c,类型是Data、RW。串口有两种变量,data和bss,什么是bss?这两个名称,是section name,也就是段的意思。看前面type和Attr,$ K& Y% B# w: [2 `; @* {
6 s7 @% W6 k( W" J! \3 H
RW Data,放在.data段;RW Zero放在.bss段,RW Zero,其实就是ZI。到底哪些变量是RW,哪些是ZI?$ o& r# a) E0 [! U# ?
- M+ m5 M0 e1 o$ Q: u
12.png
# a4 B8 o& T% p" o5 D0 ]* G' `

- p- ?) H9 T2 F( M. j4 z/ L1 xImage Symbol Table( w9 a) L3 n- @  p4 x& u3 p# [
4 K" ?* L2 o: z. b) Z8 ?* {0 j0 `
再往上就是Image Symbol Table,就更进一步到每个函数或者变量的信息了
7 h; D1 n. p. C7 A0 X4 q
8 {! b5 \5 d! u* }6 k
13.png

1 [& h  u, k7 q  d6 x3 F) `; c9 E7 p. \- H4 `
例如,全局变量TestTmp1,是Data,4字节,分配的位置是0x20000004。
( Z+ K; q4 b" S& D. }4 d$ x/ S
/ ?! H0 [( O+ X0 D
15.png
. R1 O  K1 M( {! H) k3 W' c

0 `# b; u* y+ W5 W0 L4 `7 Z2 ^4 jTestTmp3数组放在哪里?放在0X080024E0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几K几十K大,不可能也没必要放到RAM区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。1 M. K5 |1 [' R1 U4 y( q) {% }6 M

( G+ m0 c$ [8 W7 K( {const的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习
7 A$ I& D2 C1 q
9 x2 w6 Y% {" Z: j
16.png
8 U3 ^7 E/ P2 h# V0 w8 a9 S$ ?

& \5 T0 e4 X" f- o3 p( X# r( @
那局部变量存放在哪里呢?我们找到了test_tmp3,% S  W3 |* |( z

" G5 V* U8 E- U; Z" m
17.png

3 T5 N9 o# a" ?. i" T: q6 i) u
. w0 k1 A* H& z1 i% _
没找到test_tmp1/test_tmp2,为什么呢?在定义时,test_tmp3增加了static定义,意思就是静态局部变量,功能上,相当于全局变量,定义在函数内,限制了这个全局变量只能在这个函数内使用。哪test_tmp1、test_tmp2放在哪里呢? 局部变量,在编译链接时,并没有分配空间,只有在运行时,才从栈分配空间。
  1. <blockquote>u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
复制代码

4 k7 b- m' Q+ V/ F9 h! h上一部分,我们留了一个问题,哪些变量是RW,哪些是ZI?我们看看串口变量的情况,UartBuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文Block Started by Symbol的简称。: B% P# H) j+ o
0 \% w$ m: `& M7 S8 J; u3 b
18.png

, s! m3 b, i8 d+ f9 d6 Z% Z$ L7 G% [% i
到这里,我们可解释下面几个概念了:4 ~% f1 i# c" b- p2 Q' ]4 m" `
Code就是代码,函数。, ?* C) V/ c& g7 p
RO Data,就是只读变量,例如用const修饰的数组。+ ~% S- W2 K5 w1 C. \* ]- b
RW Data,就是读写变量,例如全局变量跟static修饰的局部变量。
/ d: ~4 a5 C; w' v3 p/ Q3 U) W* jZI Data,就是系统自动初始化为0的读写变量,大部分是数组,放在bss段。2 [: a  f1 i# X5 S7 [
RO Size等于代码加只读变量。" `9 v1 x- Q- o9 t* j. r
RW Size等于读写变量(包括自动初始化为0的),这个也就是RAM的大小。: g/ p! w+ m/ I% D. w/ X
ROM Size,也就是我们编译之后的目标文件大小,也就是FLASH的大小。但是?为什么会包含RW Data呢?因为所有全局变量都需要一个初始化的值(就算没有真正初始化,系统也会分配一个初始化空间),例如我们定义一个变量u8 i = 8;这样的全局变量,8,这个值,就需要保存在FALSH区。
/ X% a# _4 M( ?; {* Q. C
7 ?- W( \& G  t9 W
19.png

- r9 ~2 L; d& `+ w# I2 I

# E8 o) x, O& q. t8 q8 M( p+ ~我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579
5 [$ U2 k9 j; [5 q/ t7 J% T5 X5 @! _4 z& F$ r: {' u5 S  P/ n2 s! t+ F
20.png
1 y& @! L% o% u  J2 }* _5 o

0 S. @* _- c& X8 a. V) ?% H
main是main,放在0x080001899 N' ~- K3 M, t" E- x& _( r
3 V2 g' b3 ?' [; d$ b. U& A
21.png

8 M) M% g& W( D: R( H& k
( r6 b' I  z5 z8 B+ r
__main到main之间发生了什么?还记得分散加载文件中的这句吗?. C- e+ V+ V% L+ ^' x
*(InRoot$$Sections)
4 a6 d/ B& s4 T  F# X9 v! ~. ^* c7 q6 k% Z+ Q
__main就在这个段内。下图是__main的地址,在0x08000189。__Vectors就是中断向量,放在最开始。
6 W- Z+ s$ C1 U
! G9 ~- ?4 w+ Y
22.png
% n4 y7 C; }7 O1 i" L

  g+ N: Z9 v8 ?
在分散加载文件中,紧跟RESET的就是*(InRoot$$Sections)。
4 p8 l# T; V, d! a. m" u! T+ r# k* K" H% ~" N- i; q
23.png
. x* }  v6 b4 L0 l

- v$ S) w. M; M+ d& v- x; P! G8 F而且,RESET段正好大小0x00000188。3 l# k/ }) h0 i# }# {

0 y! C2 t+ x6 J
24.png
# B4 w5 y( L9 u
2 u4 J; c6 n. b
巧合?可以参考PPT文档《ARM嵌入式软件开发.ppt》。
1 T9 O; q# G: _' n" p& o
4 ?3 g7 j2 A1 {& [0 ?; i
25.png

4 o/ r' o) Q5 s" P+ N
, r) o- e, |! B2 l0 @! R; Y
这一段代码都完成什么功能呢?主要完成ZI代码的初始化,也就是将一部分RAM初始化为0。其他环境初始化……( H( `# Y( U; a. L+ N6 |
8 W3 f+ a7 Q: {
% @8 Q+ ]3 M1 L, r' m
最后
& m. N( J* |- K% x! c: m' Z到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。$ b6 B( X0 P: k2 M4 c5 V
9 M3 ]- c: f8 h3 B

, B. _1 |1 a, J/ }4 A) @/ U6 x% F转载自: [color=var(--weui-LINK)][url=]STM32嵌入式开发[/url]" z0 n/ e, F6 S8 Y: V1 |7 I
如有侵权请联系删除7 g. u) @, o+ ~- W/ Y9 ^
6 v. l8 g4 M7 y5 b! r

+ d8 ]6 d$ h' n
收藏 评论0 发布时间:2024-7-29 15:25

举报

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