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

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

[复制链接]
攻城狮Melo 发布时间:2024-7-29 15:25
本文分析STM32单片机到底是如何软硬件结合的,分析单片机程序如何编译,运行。
# V; n* b2 q/ X8 w9 z6 s2 F( O# {4 A& ?" Q: Q
软硬件结合
/ K# t9 k! y7 ]$ ], n' @3 k/ ?初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写P1=0X55,就可以在IO口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。6 L& f. ?* Q& ^6 w
2 ~! \5 j( R& Y
寻址空间# B/ P" B7 c& Y
什么是地址空间呢?所谓的地址空间,就是PC指针的寻址范围,因此也叫寻址空间。
' Y/ ~7 `* v+ F5 ^9 b  |2 h% g
% O# E% B# v, t7 J( y大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,PC指针就是一个32位的二进制数,也就是0xffffffff,范围只有4G寻址空间。现在内存越来越大,4G根本不够,所以需要扩展,为了能访问超出4G范围的内存,就有了64位系统。STM32是多少位的?是32位的,因此PC指针也是32位,寻址空间也就是4G。& k: X* r1 [% t' S0 ~2 F2 m! Q8 N2 h
7 s1 }- X( S+ F1 U' f
我们来看看STM32的寻址空间是怎么样的。在数据手册《STM32F407_数据手册.pdf》中有一个图,这个图,就是STM32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫Memory map,用一个新芯片,就先看这个图。
# s' j; T" _" ~3 P5 f
+ b( W6 F, W" N+ n7 C& C7 r
微信图片_20240729152315.png / G9 g! O" M( E( a
3 y$ v7 C. n& `, E
' G; h- N  }/ O  A9 w4 f; }% K
最左边,8个block,每个block 512M,总共就是4G,也就是芯片的寻址空间。
$ R0 v4 R' R* T5 A4 F. Y
  J: y! u% N+ ]  mblock 0 里面有一段叫做FLASH,也就是内部FLASH,我们的程序就是下载到这个地方,起始地址是0X800 0000,大家注意,这个只有1M空间。现在STM32已经有2M flash的芯片了,超出1M的FLASH放在哪里呢?请自行查看对应的芯片手册。
( E4 I9 x" e6 m2 U' W$ L  [& G$ e" ^! C
3 在block 1 内,有两段SRAM,总共128K,这个空间,也就是我们前面说的内存,存放程序使用的变量。如果需要,也可以把程序放到SRAM中运行。407不是有196K吗?0 E7 Z, H3 E$ i$ b/ x4 G5 S3 V% r
8 J, V9 ~  t5 E6 q' L* `
其实407有196K内存,但是有64k并不是普通的SRAM,而是放在block 0 内的CCM。这两段区域不连续,而且,CCM只能内核使用,外设不能使用,例如DMA就不能用CCM内存,否则就死机。
+ O; L1 y* U5 K, b7 C2 M3 s6 {2 \: U
block 2,是Peripherals,也就是外设空间。我们看右边,主要就是APB1/APB2、AHB1/AHB2,什么东西呢?回头再说。# y- p4 D! |' U' y+ Y  E; N7 b4 o4 F
9 |) {, {" e' G$ ]/ j3 b3 Y
block 3、block4、block5,是FSMC的空间,FSMC可以外扩SRAM,NAND FALSH,LCD等外设。8 t- v: @9 ?! G" G$ H- `, x$ U' ^
6 s% h7 Y0 L  j5 i# S% t5 y
好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。对于这个疑惑,也可以看此文:代码是如何控制硬件的?在IO口输出的例程中,我们配置IO口是调用库函数,我们看看库函数是怎么做的。2 ~' Z& y1 j7 J8 [* P

; D8 z) ~1 S5 n% b9 m$ H例如:) q( O0 g0 e, f/ w, K, ]
  1. GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
复制代码

: J9 m/ N2 D2 w: N% T. o这个函数其实就是对一个变量赋值,对GPIOx这个结构体的成员BSRRL赋值。: r( |% A' A" l: E( f. Q
  1. void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)" U. E: z) P" U( L" ]) d
  2. {
    & @6 O9 N( I% w3 S
  3. /* Check the parameters */
    - X9 F2 {4 p" N. g' U
  4. assert_param(IS_GPIO_ALL_PERIPH(GPIOx));+ s& |. S" S# N# F: k+ G
  5. assert_param(IS_GPIO_PIN(GPIO_Pin));4 z7 S% @7 O4 U. I
  6. * s  C# M5 [: j6 Q5 X6 v$ M/ ~% R

  7. 2 C$ ~; w+ r& T3 p) O8 g5 g6 l
  8. GPIOx->BSRRL = GPIO_Pin;  U% h3 o$ W1 `: O
  9. }
复制代码
: w2 Y. |. _8 y
assert_param:这个是断言,用于判断输入参数是否符合要求GPIOx是一个输入参数,是一个GPIO_TypeDef结构体指针,所以,要用->获取其成员7 L" b& @7 I( ?1 Z
0 D& F( ?8 r8 X& f* C3 _
GPIOx是我们传入的参数GPIOG,具体是啥?在stm32f4xx.h中有定义。) X) B. {. E# n- B  k, `/ O) i
  1. #define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
复制代码

, Y" H! A' |' _, _9 tGPIOG_BASE同样在文件中有定义,如下:
& W* d" }! l$ ?( _! H: w
  1. /*!< Peripheral memory map */. P/ ?  {* D; J+ s$ Q
  2. #define APB1PERIPH_BASE       PERIPH_BASE
    4 Y! K! w' Q/ u. m, n( ~1 ?3 F/ }3 z
  3. #define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)# V7 }$ o- }+ [
  4. #define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)/ b0 [- m" ?4 E1 Q; P
  5. #define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)
复制代码

/ n- n! C! c6 s2 K再找找PERIPH_BASE的定义
  d9 H9 O# O1 q/ k! ]
  1. #define PERIPH_BASE           ((uint32_t)0x40000000)
复制代码
到这里,我们可以看出,操作IO口G,其实就是操作0X40000000+0X1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是SRAM空间地址,0X40000000+0X1800是外设空间地址。; y2 a& ~/ w1 h) P3 o4 l; s
  1. u32 i;: `+ Y7 @9 ], e5 _0 |- d9 M
  2. i = 0x55aa55aa;
复制代码
% z4 V' k( ~! l9 \% h
这个外设空间地址的寄存器是IO口硬件的一部分。关于如下图STM32的GPIO文章推荐:STM32中GPIO工作原理详解。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0X40000000+0X1800+0x14.- w1 t! [1 M, J& H4 I8 S- M
) ?' ^- A; T3 Y6 f4 s" G. E) V
微信图片_20240729152319.png

" J" }( V5 Y7 {' t- ^0 e0 |

( i& U2 h9 e% L4 C控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。
" [+ [, z# M; r2 F2 a/ j8 }0 t$ A) E+ t4 m6 W) p7 G
寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或RAM。寄存器为什么能控制硬件外设呢?因为,初略的说,一个寄存器的一个BIT,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。+ |# ^8 W4 h, @0 R
: b* j6 W" p4 R$ B* U7 L+ d
纯软件-包罗万象的小程序! F  j2 r3 V, F# `# K3 ^
我们已经完成了串口和IO口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?关于程序是怎么在单片机运行的,也可以看此视频:动画演示单片机是如何跑程序的。代码到底放在那里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。  L) e' Z' ]  j- G

, Z4 P1 m: m+ p% w+ s分析启动代码) K9 y, }/ L& ]3 M6 m
函数从哪里开始运行?! S8 K6 k& r& F$ [
每个芯片都有复位功能,复位后,芯片的PC指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,PC可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在STM32中,复位到0X08000004。因此复位后运行的第一条代码就是0X08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。
2 y) @" o, y6 w- P. M) M
9 x. ]  j# |. T6 u6 G! T芯片复位时,芯片从中断表中将Reset_Handler这个值(函数指针)加载到PC指针,芯片就会执行Reset_Handler函数了。(一个函数入口就是一个指针)
  1. ; Vector Table Mapped to Address 0 at Reset
    , e) P) O* a! s: |* x* F9 F+ I4 A
  2.                 AREA    RESET, DATA, READONLY5 \% u  b! H3 a* ~+ Y; ^! b" \; n
  3.                 EXPORT  __Vectors
    # n$ s1 q/ E& \
  4.                 EXPORT  __Vectors_End7 u* Q. S$ S: Y( h7 @
  5.                 EXPORT  __Vectors_Size* J( @- h& Z1 S6 D1 \
  6. 3 v- p/ r& S  x, i1 c9 V
  7. __Vectors       DCD     __initial_sp               ; Top of Stack
    7 r) }8 X6 P. b' i  d% H  Y
  8.                 DCD     Reset_Handler              ; Reset Handler
    * {0 Y3 J' q, h' N8 \
  9.                 DCD     NMI_Handler                ; NMI Handler
    ; J0 X$ s0 C, |. j3 _  d
  10.                 DCD     HardFault_Handler          ; Hard Fault Handler7 Y4 e3 `, B  _  j, Y) d5 J
  11.                 DCD     MemManage_Handler          ; MPU Fault Handler
    " ^$ y# O! @8 H8 k9 Q! n1 l
  12.                 DCD     BusFault_Handler           ; Bus Fault Handler
    4 h) z7 h8 {5 q# q
  13.                 DCD     UsageFault_Handler         ; Usage Fault Handler
复制代码
% c' j8 V( P' ?# x' k
Reset_Handler函数,先执行SystemInit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?0 Z% o, p# C5 ^1 i" u

" L: c7 I. ?. I是我们在main.c中定义的main函数吗?后面我们再说这个问题。  t5 V# b8 }/ m! H& h4 A# n

: Q) }% d) s" Q# u5 b" I
微信图片_20240729152323.png
, {! \8 y0 f( b* F

' z( S" c9 Z' z芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujique\prj\Objects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。
, Q& [  x8 [. x+ k; Y6 H- ]$ d
9 J, L( M" N# W& T  h0 g
在MDK软件Options菜单Linker下有关于这个菜单的设置。
) {4 D/ k7 `: l( l, j" g# e
/ O( ]8 h( D" t! F: v! b
微信图片_20240729152326.png * D8 u  a4 ?+ q  e8 |

: L3 z% f: m! y! o5 k
# t. o8 S7 S3 ~+ t! N

$ V9 T$ Z2 K5 A3 n- |3 D把Use Memory Layout from Target Dialog前面的勾去掉,之前不可设置的框都可以设置了。点击Edit进行编辑。& T3 r9 B6 R) T( W

4 n% n$ t+ ?% O
2.png
0 R9 v5 Q. P9 j7 x5 p  D

$ Z( p& V0 s0 l0 V0 Y

# t. X- ~( P) H& c% V在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。0 b- u5 K) l: D6 e

% P: {. \3 B- Q( {7 i其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定FLASH跟RAM的大小于起始位置,当我们把程序分成BOOT、CORE、APP,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到RAM中运行。4 ?3 F- y* f& _2 Y

: O0 K' z( R) M. Q2 N
3.png
5 `) e) S/ c7 w  ?% D3 t

0 C* T1 w, S! n4 u# f从这个基本的分散加载文件我们可以看出:
, r8 c3 W. ?  d3 ^
3 ?! ]. y5 @4 B6 s0 m/ y5 T4 f$ ]第6行 ER_IROM1 0x08000000 0x00080000定义了ER_IROM1,也就是我们说的内部FLASH,从0x08000000开始,大小0x00080000。
( d  t9 N, G) p$ K- @( |" V& `2 q0 ~) O) W7 u. 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。
) r1 Z# F6 w. m
" ^3 g6 _0 W: _2 ]
第8行 *(InRoot$$Sections)什么鬼?GOOGLE啊!回头再说。" m. w; ?" M" a7 ?* d' m9 q% Y9 Y: d

/ _8 ~6 M9 J5 H5 I0 x! b第9行 .ANY (+RO)意思就是其他的所有RO,顺序往后放。就是说,其他代码,跟着启动代码后面。
+ F1 L5 L' i1 Q

3 Z4 f! e  x' u4 x9 o3 s" j第11行 RW_IRAM1 0x20000000 0x00020000定义了RAM大小。
4 E; u, k3 T" d/ m2 x  m

- [/ Q- e+ X$ x5 x0 D; k第12行 .ANY (+RW +ZI)所有的RW ZI,全部放到RAM里面。RW,ZI,也就是变量,这一行指定了变量保存到什么地址。
8 @" H; a. X8 r1 a( U
3 b1 [# c8 J5 k. s6 I分析用户代码9 y, a- D2 X% I3 t( U' ~
到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。
7 E8 C3 w4 h4 \& y" Z( e/ H/ k8 j, @6 }4 X4 T3 W
1 程序跳转到main函数后:RCC_GetClocksFreq获取RCC时钟频率;SysTick_Config配置SysTick,在这里打开了SysTick中断,10毫秒一次。Delay(5);延时50毫秒。
  1. int main(void)0 `" n! @: b- k6 G/ d) n, \
  2. {
    6 m( Q+ V. C2 `& |9 N& [0 M
  3.   GPIO_InitTypeDef GPIO_InitStructure;
    + B# {( I/ d5 i% X+ z; Y
  4. 4 I. ~' E7 l$ Q/ a; A: @" \2 K4 [
  5. /*!< At this stage the microcontroller clock setting is already configured,
    ; D6 \5 @6 {+ N5 l! R4 K
  6.        this is done through SystemInit() function which is called from startup+ Z: K! z, s0 c* b
  7.        files before to branch to application main.
    9 r7 N! k7 \7 w0 l+ }6 f$ i7 c
  8.        To reconfigure the default setting of SystemInit() function,
    ' A& R( I8 E4 [) @
  9.        refer to system_stm32f4xx.c file */
    / c( A3 a& g% {
  10. % I0 X  P9 F- X0 m
  11.   /* SysTick end of count event each 10ms */
    : u- f3 J0 n: i% d: T* [$ b/ r
  12.   RCC_GetClocksFreq(&RCC_Clocks);
    ' L! b' |) U  d6 A
  13.   SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);
    : b) f& Q4 u; y- o* g. Z3 B# ]
  14. 0 M" ]# U  v8 U; k7 O9 Z) b
  15.   /* Add your application code here */- T) }& ^' ?& V0 ~; O! l/ D" O
  16.   /* Insert 50 ms delay */7 H' p. Y5 e  y
  17.   Delay(5);
复制代码

0 t" J# g4 s2 b9 T2 初始化IO就不说了,进入while(1),也就是一个死循环,嵌入式程序,都是一个死循环,否则就跑飞了。
  1. /*初始化LED IO口*/! w8 j4 B% N) @5 l- O
  2. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);+ G) }- E# ]  I7 u
  3. ) I+ Q# x/ _2 f( p5 ?% ?
  4. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;4 ^& h6 _+ W% u' M$ N
  5. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    * f6 l! c( ?, s# x+ x

  6. 1 a9 }  z) `* O' S0 q
  7. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    # u. A: }4 h% U
  8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;0 b  k. @+ Y% q+ E6 C
  9. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    6 D- M9 M& \& w, ?$ x0 {5 q
  10. GPIO_Init(GPIOG, &GPIO_InitStructure);    9 `: }2 w/ m7 ]" A' F- r

  11. 8 f7 j' ]% Y* `( C5 d) v- G
  12. /* Infinite loop */
    , l' ?, k. Z( N2 t  v) b, z
  13. mcu_uart_open(3);: j8 f; t; s& L! {3 u  G1 D
  14. while (1)+ E, K2 k) g% @
  15. {
    . a3 y5 s9 x; ?/ Q0 q) E. m
  16.   GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
    3 H7 Z5 E8 G7 h2 C4 L
  17.   Delay(100);
    : P# U/ K0 W/ M2 N- e* f
  18.   GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);8 j' z# h7 F- V2 _
  19.   Delay(100);1 d! B$ z+ i/ X4 l5 S# T
  20.   mcu_uart_test();
    + X& C8 N) m4 s3 I6 h% ?

  21. 8 h+ V6 \6 d* \& T2 @7 i
  22.   TestFun(TestTmp2);
    4 v! U1 v+ Y0 g0 K. e4 H0 A
  23. }
复制代码
- k  Q0 W4 l4 l$ `7 S0 Y5 k
3 在while(1)中调用TestFun函数,这个函数使用两个全局变量,两个局部变量。
  1. /* Private functions ---------------------------------------------------------*/
    ; D) K# M% h' E, y5 E, |! t* m
  2. u32 TestTmp1 = 5;//全局变量,初始化为5
    - u9 g: P- b! Q6 X
  3. u32 TestTmp2;//全局变量,未初始化% s1 ^. w# N" `0 P( S# Y! O, m% X$ g, x
  4. " r! }' `6 {: F% b9 l/ P
  5. const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};
    + V8 _# f+ m' T' U
  6. ; E, U3 s& G! `3 X# J. `% w) X
  7. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
    4 z# x8 o6 L5 {& l0 D
  8. {' [: ~& z$ d7 A5 l. M9 P
  9. u8 test_tmp1 = 4;//局部变量,初始化
    ) a: G; U) h/ T4 A
  10. u8 test_tmp2;//局部变量,未初始化9 f: x8 s) y' Z- Z# G

  11. 6 g9 D% L6 @% h% K0 h/ `: f: G
  12. static u8 test_tmp3 = 0;//静态局部变量
    , k0 u1 v0 u2 g% U! b1 P
  13. . Q1 M$ L" B9 S6 K( ]6 _. F1 f
  14. test_tmp3++;
    & ^1 H$ c* ^* W! L5 V
  15. 0 K+ t; b  n5 a) ]
  16. test_tmp2 = x;3 ?  y+ b9 `9 ?. f/ D

  17. 3 _" E& N* |! R) H
  18. if(test_tmp2> TestTmp1)
    0 D) Q$ T" E- d+ f" S: A
  19.   test_tmp1 = 10;
    ! p% Y+ C: O* V1 M7 Q( S& x
  20. else! {7 h6 F& l" ^0 r
  21.   test_tmp1 = 5;  k$ z3 q) q  h  w( K( e7 S

  22. : d( y5 o' a# c( k# v( D# S
  23. TestTmp2 +=TestTmp3[test_tmp1];/ s1 E3 g- n% x5 \6 t' q
  24. * k& P% }7 m) F
  25. return test_tmp1;
      D! ^$ Y# g0 Z' h5 G
  26. }
复制代码
7 Q& N+ E: R6 t( b8 K
然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。相关文章:STM32中断系统。我们查看Delay函数,uwTimingDelay不等于0就死等?谁会将uwTimingDelay改为0?
  1. /**
    + l6 c8 s* ~+ r# f1 f$ c+ C
  2.   * @brief  Inserts a delay time.
    % d  G( q: |. P, g# {/ C1 B3 t
  3.   * @param  nTime: specifies the delay time length, in milliseconds.
    9 u8 ~1 S- Z2 `" X- I
  4.   * @retval None
    % j5 L: D9 w6 j; S8 ?* I3 g; ^
  5.   */
    ( o  I1 T( n7 _, v7 o. v* j( f$ i
  6. void Delay(__IO uint32_t nTime)4 c6 y- ^( |0 O
  7. {
    ' y$ z$ G; s) @% c
  8.   uwTimingDelay = nTime;5 P# ?0 n9 w: A  j$ K9 [3 k
  9. # J5 `( z' w, Y! Q# V9 j7 T$ u3 F
  10.   while(uwTimingDelay != 0);
    % H; z7 l! W; G$ e9 q8 i
  11. }
复制代码
4 l$ [0 d/ L1 s
搜索uwTimingDelay变量,函数TimingDelay_Decrement会将变量一直减到0。/ s. e( ?: \# Z) Q0 [& d
  1. /**
    ! E, f# a% t0 \# D
  2.   * @brief  Decrements the TimingDelay variable.
    & X9 v( c, U8 x( J
  3.   * @param  None
    2 c8 n) x! e9 s2 G1 d
  4.   * @retval None  }. ]0 ]5 ^1 {# f* ^6 l* r
  5.   */' j4 H: N/ p) Y
  6. void TimingDelay_Decrement(void)
    : q, v! ?* R( Q& g& Q1 u
  7. {
    ! o. M2 c6 p; C
  8.   if (uwTimingDelay != 0x00)3 p. u' I' e" E' h+ F0 F5 g% {2 L4 Q
  9.   {7 L* E7 C( l( E! Q
  10.     uwTimingDelay--;6 z" p3 }" d+ R+ }* y* s( ]
  11.   }
    ' D# |2 e0 k+ Q( k& W/ h
  12. }
复制代码

* G' `' S+ Y9 X4 M这个函数在哪里执行?经查找,在SysTick_Handler函数中运行。谁用这个函数?
  1. /**
    / ?. l; g- q" M& h% H4 h
  2.   * @brief  This function handles SysTick Handler.
    % Y- `8 P( D: k/ b. `( d* \
  3.   * @param  None3 X3 D7 u# [/ o( ~
  4.   * @retval None
    $ `- X. X- g) p9 T9 x
  5.   */
    : k$ H( ~, ]- h/ o
  6. void SysTick_Handler(void)7 ~5 a/ u, v' y/ [8 k- z
  7. {
    7 E# Q! z  H) J% q7 b" ^
  8.   TimingDelay_Decrement();- Q* }" |" f! o1 S) ^
  9. }
复制代码

. C; E- p- ]  G' j/ m* m经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。
  1. __Vectors       DCD     __initial_sp               ; Top of Stack
      E8 F- V( e7 E: B
  2.                 DCD     Reset_Handler              ; Reset Handler
    ) ~& ?) F/ E2 i) _! }
  3.                 DCD     NMI_Handler                ; NMI Handler3 \8 |# B% F& D( r' I. w
  4.                 DCD     HardFault_Handler          ; Hard Fault Handler
    - D9 R0 |1 g3 S2 j( @+ ^/ i: e
  5.                 DCD     MemManage_Handler          ; MPU Fault Handler
    2 y% M1 R. [7 t. I8 R6 Q' Z7 j
  6.                 DCD     BusFault_Handler           ; Bus Fault Handler
    1 U2 T. f4 F! C2 H. x
  7.                 DCD     UsageFault_Handler         ; Usage Fault Handler, `* `  A& j& Z
  8.                 DCD     0                          ; Reserved
    ( j0 M" U. b. @5 u/ B
  9.                 DCD     0                          ; Reserved
    * Q( ^+ ~* p. L$ R* A
  10.                 DCD     0                          ; Reserved9 k2 O9 J8 N! O
  11.                 DCD     0                          ; Reserved
    " k5 h6 {( M  s8 d3 p8 f! j
  12.                 DCD     SVC_Handler                ; SVCall Handler
    6 Y/ \( ]/ n; Y9 k
  13.                 DCD     DebugMon_Handler           ; Debug Monitor Handler1 x$ R; d' j, q  n# ^' G0 h
  14.                 DCD     0                          ; Reserved
    ( |8 E: E: @  g' F2 n' b" `
  15.                 DCD     PendSV_Handler             ; PendSV Handler
    7 n: T/ }. b. U! u: S7 ~. I
  16.                 DCD     SysTick_Handler            ; SysTick Handler
复制代码

2 n6 S, ^( s* W余下问题! `, s/ s* P' j5 D. `
1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(InRoot$$Sections)是什么?3 ZI段,也就是初始化为0的数据段,什么时候初始化?谁初始化?' V: K% l$ C# D
8 B+ H. j* ?8 m! W
为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!
" l1 v* K* j  Q% ~
# z4 m8 J. N& D; r0 v. f
8 }: X- e7 f( L: J3 F
通过MAP文件了解代码构成2 I; m6 o$ L6 b7 f! a' _0 w: s
编译结果
; R7 E5 D4 a3 Y7 M9 O
程序编译后,在下方的Build Output窗口会输出信息:
  1. *** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'
    & w! r* U$ M# ?
  2. Build target 'wujique'
    ) P) A' h! p1 l, n3 O4 C8 }
  3. compiling stm32f4xx_it.c.../ _3 ]- I) y2 ^" c- ~6 _3 k4 j& X
  4. ...
    : e8 a- U3 o0 Z' h
  5. assembling startup_stm32f40_41xxx.s...1 n' Z6 s0 u* j: D
  6. compiling misc.c...8 g2 U. T, o) d
  7. ...
    + x7 X, n2 s" ~* Y, P' N
  8. compiling mcu_uart.c...
    2 C- K6 _/ }6 B4 `# G
  9. linking...6 n' L3 B' A  d/ ^1 J
  10. Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  
    . T) b( H1 n$ x/ l, F( I
  11. FromELF: creating hex file...% v! Y3 _: c" L7 `. V- n: G
  12. ".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s).
    + u! t$ H7 L0 C/ B) S% T5 y+ K9 ]
  13. Build Time Elapsed:  00:00:32
复制代码

2 d- @9 v& \! b) D( @5 @编译目标是wujique- a" M) v9 I+ ?" @! _/ N2 J6 s
C文件compiling,汇编文件assembling,这个过程叫编译+ C! }2 k  D% @3 ?
编译结束后,就进行link,链接。
( v  U3 \1 m4 Y* A$ `1 c: r/ L最后得到一个编译结果,9038字节code,RO 990,RW 40,ZI 6000。CODE,是代码,很好理解,那RO、RW、ZI都是什么?2 o% U4 P0 H2 ?$ V
FromELF,创建hex文件,FromELF是一个好工具,需要自己添加到option中才能用5 {0 |& O  b" P1 H8 R9 i' A
# Y0 g$ K* h2 k: L
map文件配置
& v/ c* y: J+ m更多编译具体信息在map文件中,在MDK Options中我们可以看到,所有信息都放在\Listings\wujique.map
! r8 J* A) K# k) x. P  ^  J) t% V
) a: v  K2 r/ I6 J# g  C默认很多编译信息可能没钩,钩上所有信息会增加编译时间。2 l% r7 t4 j4 z- [2 I* ?5 `# o) M

. |0 O5 _7 S4 x" \9 r& p8 H5 v
4.png
$ v: P! i" a. I$ y' h  V6 u

. V+ f& N  {* G; [( ]' Jmap文件

$ c0 O1 @  K% y/ q打开map文件,好乱?习惯就好。我们抓重点就行了。1 ^5 f( Z7 A3 U; r  C1 ?, k( M. Z/ @
' x/ B1 J* A& Q  l& }1 C
5.png . ?" v+ k" S; P6 {9 a- B6 P. Z
3 W1 O- O: L1 w- M& m- F
map 总信息0 W) r  {7 H0 r/ _
  H/ P2 x# E& ]
从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。' a+ [1 f* x) o5 i( b, p

% M& }2 Q6 k. n6 U有多少RO?RO到底是什么?, T) T* m" u2 i! g  `
3 ~2 \6 r- {( K9 ?3 c$ K# j: A
有多少RW?RW又是什么?& P0 Z8 m2 w  p: j% m. B' l

5 K8 t% z1 @5 c0 B. O" G' y' ?4 IROM为什么不包括ZI Data?为什么包含RW Data?
  f8 `* Q# {: |" s
5 o  k( W" N8 X$ g
6.png
4 C6 z& _' d1 W( I( s* s( wImage component sizes
9 L+ F9 _) q5 `/ E8 `: n) s0 r) K! T: P3 S% y
往上,看看Image component sizes,这个就比刚刚的总体统计更细了。9 U/ c3 p2 S" A9 ]9 }
8 y2 B3 b5 i6 \
这部分内容,说明了每个源文件的概况
! |2 n% o- i9 G( V6 q" g

( L& o  t& U; \/ @6 q& z+ w6 e首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些STM32的库文件。
9 k$ Z2 V! L0 j* d  z$ o/ ]9 ~, y- |7 l! ^3 t: O1 e- y- D/ Y
7.png

! E  \, B  K. t$ f" a" b( b

/ z5 B; \. Y! t8 i/ v' T0 p第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1M flash上的程序压缩到能在512K上运行。5 Q% r" V3 G' s8 ^$ d5 \

1 R9 R2 n0 M  e7 Y: C6 R
8.png

+ }1 u# u  y% E& a0 g

4 I/ R* w- V! r2 d$ Z; L第3部分也是库,暂时没去分析这两个是什么东西。
0 L  C& F0 r0 z4 U: j
2 @0 @: i  T+ h+ a6 L0 @/ |% [4 T
9.png

" m) d/ d# A/ h, P& ]  ?

  g! h; q- G# d( G% h库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:; N+ e: c: E' C6 m4 b- `
#include <stdarg.h>
! Y$ k, p7 ~3 ]9 u0 p# ^% H6 X#include <stdlib.h>
! K9 p; F' M8 y#include <string.h>  2 R0 M2 j: w8 ?  v

+ d2 c5 x( q" ]9 n* D+ Y; [
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。! y& C1 i" L6 U' d

/ k! `7 T. `" B6 E) d. ]* G. T. P
" [& t' w4 @' s0 x) D  e' q- |1 @
文件map: e# Y. u0 F% y2 {, u# b
再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET3 B" k6 r. Y" o0 R, A
5 o) \% F0 y- G; ^* f- V
库文件是什么?8 l& ^; h' ~5 S- c" q# I
库文件就是别人已经别写好的代码库。
/ J. Q7 `9 f5 W! F! o0 ?' r% W; M( y( A3 f; l
在代码中,我们经常会包含一些头文件,例如:- t, G% y2 Z3 \, W$ r9 N
  1. #include <stdarg.h>
    % n6 ?( L1 w' F$ s
  2. #include <stdlib.h>
    # V2 H; `5 J0 o( E( |; L
  3. #include <string.h>
复制代码
. G6 I0 A4 f" C7 C, b* X
这些就是库的头文件。相关文章:C语言中的头文件。这些头文件保存在MDK开发工具的安装目录下。9 U2 g. y  T. x! s, Z- j
, Z/ m# ]/ Z: s# n3 {3 N
我们经常用的库函数有:memcpy、memcmp、strcmp等。, J2 ?; S  f8 d1 N' E- O( M
! O: U% x. N" ~2 X1 [4 k4 o" \
只要代码中包含了这些函数,就会链接库文件。
. n" V9 b, Q1 a7 ^+ ]6 J) r0 B- L' ]  V) C+ s$ O+ c2 M( G
文件map# ]  g: [/ d  A! y' h$ }
再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET6 i, j. a+ p' ~( O
2 A+ D* m" D( h. X$ K1 k) L
10.png

0 H3 O; n+ [* _4 }& ]0 K
, m4 Z' b( i) s% R) B
每个文件有有多行,例如串口,4个函数。3 [" X+ {- C: |6 N3 x2 S$ Q

4 m! s7 L  o# D
11.png
1 T, p0 u2 H6 ]; C
6 g( v0 W: n& k# U* X
然后是RAM的,main.o中的变量,放在0x20000000,总共有0x0000000c,类型是Data、RW。串口有两种变量,data和bss,什么是bss?这两个名称,是section name,也就是段的意思。看前面type和Attr,
3 A; i- q7 S/ {6 v2 A# z  c3 R4 ]. H
RW Data,放在.data段;RW Zero放在.bss段,RW Zero,其实就是ZI。到底哪些变量是RW,哪些是ZI?
+ _) q" t2 E) N8 N, g5 ?" h5 Z$ a1 i* J' Y
12.png

1 Z( X8 X7 j, ^" ?" Y$ E

1 q6 {/ G0 C2 rImage Symbol Table/ @; G2 }. T* ^" \2 V

' Q0 @4 e6 s0 a* ~; v再往上就是Image Symbol Table,就更进一步到每个函数或者变量的信息了' z$ N) `  r; P. g' a( R

$ Y$ N* T  H0 x5 c& ]0 p
13.png
' ^% C9 O% c5 f- P0 u! }

: m2 }5 D0 \1 S& Y+ Y/ ]3 b例如,全局变量TestTmp1,是Data,4字节,分配的位置是0x20000004。
+ W1 w  Q7 `8 ^) ^' r( Y+ b5 F9 h  J4 B5 w
15.png
& ]" Q# @; B) {2 h- N* b$ y5 u! V

+ B6 K# h" s6 S7 XTestTmp3数组放在哪里?放在0X080024E0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几K几十K大,不可能也没必要放到RAM区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。' I) q# K5 Q8 ?( @, L- }
/ b1 R# m2 S6 X) o; d, ~" F
const的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习' T% A9 G9 N' v' `+ u
2 v9 ^2 h1 o" L: A3 S
16.png

. k* K4 k, L7 p0 V. @, h: E0 I1 Z: @
那局部变量存放在哪里呢?我们找到了test_tmp3,
; x9 E+ C+ ^5 C: I) Z
! s# t! q( \3 [
17.png
, m/ x7 X9 q+ s, n" S* [: t0 U5 J

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

8 c1 N* t/ |0 i- g6 a上一部分,我们留了一个问题,哪些变量是RW,哪些是ZI?我们看看串口变量的情况,UartBuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文Block Started by Symbol的简称。. `0 S' f& I( E* Y' z

2 @( X: |, Q$ @1 ?+ H5 J
18.png

7 U0 Q3 m; ~* K. n; Z
% w# d- B6 S% R% {$ H0 f到这里,我们可解释下面几个概念了:6 D3 Q7 w2 b& W0 ?
Code就是代码,函数。" j' m" x$ i" K
RO Data,就是只读变量,例如用const修饰的数组。
  v" ?- A0 I* E- r6 Y6 RRW Data,就是读写变量,例如全局变量跟static修饰的局部变量。+ H, {9 f0 k% Z+ a& g) n  [
ZI Data,就是系统自动初始化为0的读写变量,大部分是数组,放在bss段。$ H7 c' I+ {7 s3 J+ k
RO Size等于代码加只读变量。
7 x) ]& D6 w+ \RW Size等于读写变量(包括自动初始化为0的),这个也就是RAM的大小。) H6 |# |5 O( t& q* O+ @% X0 u
ROM Size,也就是我们编译之后的目标文件大小,也就是FLASH的大小。但是?为什么会包含RW Data呢?因为所有全局变量都需要一个初始化的值(就算没有真正初始化,系统也会分配一个初始化空间),例如我们定义一个变量u8 i = 8;这样的全局变量,8,这个值,就需要保存在FALSH区。: R. s( J, m# m8 `% a" L& R* \) }

( s! ?5 z1 A7 D. W: k6 e4 @
19.png
* @1 z( f. T* e& H8 f
$ K/ X7 P: p7 r# a1 }: S% n! a$ C1 y
我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579
4 o$ o9 w" d% s6 N/ Y
$ Q$ }/ Z9 L, J+ j& W! D
20.png
" X) [/ C3 }: `  s+ K) R
7 {( w' g- b0 X& c9 j2 E
main是main,放在0x08000189  e" m+ r1 o) F& z1 X- {; Y3 r

7 V: `& E! c- z
21.png
; X9 d7 E: a8 S

8 t" H# [4 M* ~7 X% q; i__main到main之间发生了什么?还记得分散加载文件中的这句吗?
1 [# {& T1 }4 e  @" ?*(InRoot$$Sections)1 d% X7 ^; W  q1 [, s7 T# s0 f) l/ Q3 }
2 `4 W7 O' e8 x5 S4 P
__main就在这个段内。下图是__main的地址,在0x08000189。__Vectors就是中断向量,放在最开始。
7 M- ^" A: d# M+ y4 |( F* i* c& o5 {9 m% u3 b$ `9 I! ]
22.png
! t5 m# ^* D7 W
  M- S- e7 E: V% K( w- v
在分散加载文件中,紧跟RESET的就是*(InRoot$$Sections)。1 t/ D9 a: }; D, f: G2 O

. {1 f& _! L1 i7 ]! J) N9 Z  i0 K* X
23.png

- c) B3 L; T# n/ {

7 z7 q; `: h$ t# S, t" e而且,RESET段正好大小0x00000188。, ]/ W' I+ k" P
; c& b; g: F/ j6 [' H8 Z0 h1 ?2 l
24.png
- E5 B7 ^: T* _* ^0 c; a

8 ~" a$ z9 N- J' B8 X
巧合?可以参考PPT文档《ARM嵌入式软件开发.ppt》。: _2 [- F+ f$ `

4 i7 J! D3 v; F, h$ @/ ~
25.png

$ f! h6 z$ W& |; K0 n! x$ p# }4 p# n: U3 N
这一段代码都完成什么功能呢?主要完成ZI代码的初始化,也就是将一部分RAM初始化为0。其他环境初始化……
4 c  N& c/ ?! O
$ \$ m5 m( [9 w$ h4 m
# y/ L  t" y/ L3 l' V: Q+ r- m- \
最后
# v# I5 l4 @5 t0 i到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。
0 n) {- H, b: r0 Z4 u
9 D2 Z8 K; x' g1 s% v

+ b: ]" Q/ m2 M转载自: [color=var(--weui-LINK)][url=]STM32嵌入式开发[/url]
( g( w9 S: s  e8 ~9 n$ D& T如有侵权请联系删除
3 o$ b8 I3 G# d5 @/ w8 j4 C& P
: Z( F! s  ~0 g4 M" d
, s. y# P. T5 N6 b  T
收藏 评论0 发布时间:2024-7-29 15:25

举报

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