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

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

[复制链接]
攻城狮Melo 发布时间:2024-7-29 15:25
本文分析STM32单片机到底是如何软硬件结合的,分析单片机程序如何编译,运行。9 M! S" X, _$ B+ t" O3 G' D
9 ^+ C) [& N0 N9 r, r7 W
软硬件结合
5 D+ s2 S6 A4 Z% a+ F初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写P1=0X55,就可以在IO口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。
6 f# s0 E% ]& {5 f- ?9 q

! m% |: G# d2 K# u- e0 e% ?寻址空间8 [4 }" Z1 V/ E$ m1 E8 G
什么是地址空间呢?所谓的地址空间,就是PC指针的寻址范围,因此也叫寻址空间。
7 C0 T5 `3 T9 Z, H# L3 l( M5 f
/ }6 @  ?5 T2 J' ~大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,PC指针就是一个32位的二进制数,也就是0xffffffff,范围只有4G寻址空间。现在内存越来越大,4G根本不够,所以需要扩展,为了能访问超出4G范围的内存,就有了64位系统。STM32是多少位的?是32位的,因此PC指针也是32位,寻址空间也就是4G。
- n3 h/ A4 ~# s* T! M' V" B0 ~* G2 V3 r. n
我们来看看STM32的寻址空间是怎么样的。在数据手册《STM32F407_数据手册.pdf》中有一个图,这个图,就是STM32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫Memory map,用一个新芯片,就先看这个图。# u& t3 ^; A. O/ m3 L) Z  A

& u/ D: n+ _( X
微信图片_20240729152315.png   B9 w' \7 ^+ p, Q

) d( b' k/ u! i7 t- K: V9 I! i) @1 }& n) U3 h6 [& c& u( w
最左边,8个block,每个block 512M,总共就是4G,也就是芯片的寻址空间。! U, G$ e) F) M, @6 V2 U
) s& H1 w. P) F+ e9 a: g" ^
block 0 里面有一段叫做FLASH,也就是内部FLASH,我们的程序就是下载到这个地方,起始地址是0X800 0000,大家注意,这个只有1M空间。现在STM32已经有2M flash的芯片了,超出1M的FLASH放在哪里呢?请自行查看对应的芯片手册。
& q" O& I" w# _0 M' f. k
2 Q" _) y4 b& U3 在block 1 内,有两段SRAM,总共128K,这个空间,也就是我们前面说的内存,存放程序使用的变量。如果需要,也可以把程序放到SRAM中运行。407不是有196K吗?
0 J6 C# t, A, b! |$ Q  e6 ~9 v5 [5 n9 K4 e; `  A
其实407有196K内存,但是有64k并不是普通的SRAM,而是放在block 0 内的CCM。这两段区域不连续,而且,CCM只能内核使用,外设不能使用,例如DMA就不能用CCM内存,否则就死机。
* F- |" t: c# V1 L# f- Y0 x
4 R: `0 d% E) |$ {2 F! vblock 2,是Peripherals,也就是外设空间。我们看右边,主要就是APB1/APB2、AHB1/AHB2,什么东西呢?回头再说。
& v( m4 ?: @& h; A0 b+ s" m+ F5 ~7 M7 ?
block 3、block4、block5,是FSMC的空间,FSMC可以外扩SRAM,NAND FALSH,LCD等外设。
# G  ^) g5 D' U5 {" K$ i9 L
+ J- n( E7 o, v好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。对于这个疑惑,也可以看此文:代码是如何控制硬件的?在IO口输出的例程中,我们配置IO口是调用库函数,我们看看库函数是怎么做的。( S3 {, X" x  d- C
  d# S. C9 j$ U5 B: S
例如:, R1 f( m! B  r: h3 h
  1. GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
复制代码

' O' _( q) K, _5 K9 p1 C  [; f这个函数其实就是对一个变量赋值,对GPIOx这个结构体的成员BSRRL赋值。
6 T+ z2 o% T- N( l) l& s
  1. void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)" s9 M; q$ W/ b# N! N% ?) g
  2. {! L; n8 F1 c& x3 Q& Y& ~
  3. /* Check the parameters */9 f% `7 `+ Y. q) Y, J6 E
  4. assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
    - \4 I% H6 A, A8 ~; R7 H8 j
  5. assert_param(IS_GPIO_PIN(GPIO_Pin));- y$ R0 c7 }* b" }( U5 H% j- T

  6. ) p4 v9 G- w' J" d: ]0 S9 Y

  7. 2 I3 s) r5 H! n; ?
  8. GPIOx->BSRRL = GPIO_Pin;2 l8 a( G  H9 L2 @! _
  9. }
复制代码

5 |+ `! o! A' F" z  R" ?assert_param:这个是断言,用于判断输入参数是否符合要求GPIOx是一个输入参数,是一个GPIO_TypeDef结构体指针,所以,要用->获取其成员
" `7 k+ M! g7 s4 S  t& b+ Y
# x2 ?& S" e8 ]$ |' w' |
GPIOx是我们传入的参数GPIOG,具体是啥?在stm32f4xx.h中有定义。, R8 k' }8 `+ V2 d3 N3 @9 a
  1. #define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
复制代码

+ K! j. S' z- X! i5 M0 w* uGPIOG_BASE同样在文件中有定义,如下:8 C# [' ~, z) d7 n
  1. /*!< Peripheral memory map */! j% w$ w' u& `1 M6 w5 {
  2. #define APB1PERIPH_BASE       PERIPH_BASE# u4 O% Y0 F: z- n- S
  3. #define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)% c8 V/ |/ L7 u* y& {; d
  4. #define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)
    + l0 K$ a0 r$ s% R5 h+ A
  5. #define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)
复制代码

* M3 N2 N4 s( z% T7 S0 m再找找PERIPH_BASE的定义
" N3 W! ]# r; T) X+ g
  1. #define PERIPH_BASE           ((uint32_t)0x40000000)
复制代码
到这里,我们可以看出,操作IO口G,其实就是操作0X40000000+0X1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是SRAM空间地址,0X40000000+0X1800是外设空间地址。" v( Y+ Z; h; a  T; t( k
  1. u32 i;
    ! ?" \1 {' Z& U$ }) [
  2. i = 0x55aa55aa;
复制代码

! u+ k3 U4 V+ ]这个外设空间地址的寄存器是IO口硬件的一部分。关于如下图STM32的GPIO文章推荐:STM32中GPIO工作原理详解。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0X40000000+0X1800+0x14.
- |% F: p$ ~: u6 C1 x' p+ v9 J8 p4 g4 h; y
微信图片_20240729152319.png
! O/ g2 W2 L+ l; b" t1 g! O
+ e8 ]0 F# e+ M3 g: T
控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。
5 Q8 A# u4 ?5 f
5 I8 ]% K+ ^7 L5 w% A  j寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或RAM。寄存器为什么能控制硬件外设呢?因为,初略的说,一个寄存器的一个BIT,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。
( M, M( q# J. s1 V/ m- @5 C
! T" A) u6 U8 T
纯软件-包罗万象的小程序( d6 q, c( n# |$ [
我们已经完成了串口和IO口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?关于程序是怎么在单片机运行的,也可以看此视频:动画演示单片机是如何跑程序的。代码到底放在那里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。- ^- n+ b/ n* U* u& ]

7 _. l5 V$ O& G- r! u$ O分析启动代码
: E; O5 U6 U: P1 K5 f函数从哪里开始运行?
$ A5 F9 X' V. A: ?7 z每个芯片都有复位功能,复位后,芯片的PC指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,PC可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在STM32中,复位到0X08000004。因此复位后运行的第一条代码就是0X08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。5 k/ n( k6 n7 [; G; r

& M7 i( G7 }( M4 s" x芯片复位时,芯片从中断表中将Reset_Handler这个值(函数指针)加载到PC指针,芯片就会执行Reset_Handler函数了。(一个函数入口就是一个指针)
  1. ; Vector Table Mapped to Address 0 at Reset) h# i: b4 R7 l0 L; M
  2.                 AREA    RESET, DATA, READONLY
    3 Q+ t! r7 z5 B
  3.                 EXPORT  __Vectors
    , A0 b( H) m/ M! l9 p; ?
  4.                 EXPORT  __Vectors_End3 ?4 L9 B# V" _% O6 e2 K8 g
  5.                 EXPORT  __Vectors_Size0 \& r' B" r" ]4 `
  6. 7 u* f/ d; |+ O
  7. __Vectors       DCD     __initial_sp               ; Top of Stack
      K" a) ?$ V5 q4 d+ N; K3 w. _
  8.                 DCD     Reset_Handler              ; Reset Handler
    . h& R1 ?! C" r, c9 E0 m
  9.                 DCD     NMI_Handler                ; NMI Handler7 J% A; Y/ w+ c. |
  10.                 DCD     HardFault_Handler          ; Hard Fault Handler- l2 S* K1 N, r8 R$ s
  11.                 DCD     MemManage_Handler          ; MPU Fault Handler+ k9 O4 W6 l/ a: n! W. b
  12.                 DCD     BusFault_Handler           ; Bus Fault Handler# U* a9 O; m. ?3 l! N
  13.                 DCD     UsageFault_Handler         ; Usage Fault Handler
复制代码

; \: Z: {2 |( I: R, ?* wReset_Handler函数,先执行SystemInit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?
5 W' F9 h! a( M. a# a& {" M. N$ v2 B
是我们在main.c中定义的main函数吗?后面我们再说这个问题。- E/ C. }/ @: o
6 u" l2 i9 u; p6 x7 U  d+ }; h6 _) [! C
微信图片_20240729152323.png

5 ~  K) U( u- M1 Y8 P& P2 u4 d1 d7 @

) s! l* U; W3 G芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujique\prj\Objects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。
8 ~0 \& W; p/ T  k6 a  D  J

0 A5 K# I/ c; u: Y4 j: D在MDK软件Options菜单Linker下有关于这个菜单的设置。& s9 N1 O5 ^( V* v- U

$ v- [- ~+ T7 F" X: c8 N. ^
微信图片_20240729152326.png . l; d# x8 g# A0 V- t+ i
: R7 [$ t8 U" @
- P9 A9 s' K6 N; A% I1 v( p$ q7 m

. o# _/ `3 e  w3 a% R, u) P把Use Memory Layout from Target Dialog前面的勾去掉,之前不可设置的框都可以设置了。点击Edit进行编辑。
8 L2 ~9 m. P7 t' ~
) [, e1 J& |) J. A* h3 U$ p$ r4 v
2.png
" j, L, r3 A$ c: K2 Q! s  v
- i. e1 j2 _0 P- {
) T7 d  u% h8 Q/ ^
在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。
8 {+ j  i* J- W8 ?1 ~6 b
8 J4 I; W- h/ b, H/ |/ d; p其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定FLASH跟RAM的大小于起始位置,当我们把程序分成BOOT、CORE、APP,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到RAM中运行。6 j1 Q$ z- e& ]4 j3 n4 g7 j

7 C& ^" ]% W& O; h
3.png
1 J, O; \: Y# W7 b% O. J: |
$ k" h$ y! a$ E2 ~; A
从这个基本的分散加载文件我们可以看出:
4 n- e( A( R6 |5 m' M: b) D1 z3 I/ }
8 V- i0 C5 b4 |第6行 ER_IROM1 0x08000000 0x00080000定义了ER_IROM1,也就是我们说的内部FLASH,从0x08000000开始,大小0x00080000。3 W8 c% L% T/ N6 c2 j. y8 a" C

4 e  B" \1 C2 c5 n; M+ m$ M) ]第7行.o (RESET, +First)从0x08000000开始,先放置一个.o文件, 并且用(RESET, +First)指定RESET块优先放置,RESET块是什么?请查看启动代码,中断向量就是一个AREA,名字叫RESET,属于READONLY。这样编译后,RESET块将放在0x08000000位置,也就是说,中断向量就放在这个地方。DCD是分配空间,4字节,第一个就是__initial_sp,第二个就是Reset_Handler函数指针。也就是说,最后编译后的程序,将Reset_Handler这个函数的指针(地址),放在0x800000+4的地方。所以芯片在复位的时候,就能找到复位函数Reset_Handler。/ `0 m5 ~! J* i8 L

6 h+ d& y. p. C7 X" G& B, x( \9 [, N第8行 *(InRoot$$Sections)什么鬼?GOOGLE啊!回头再说。
& s; B" p6 p. G* t0 |) }4 h' E
2 e; Q) G) M' i" {! R第9行 .ANY (+RO)意思就是其他的所有RO,顺序往后放。就是说,其他代码,跟着启动代码后面。
/ o9 I" }8 U1 X' O
, V2 s1 a; m% ?9 N) G
第11行 RW_IRAM1 0x20000000 0x00020000定义了RAM大小。0 J9 @4 ]" c+ ^0 B5 t) [# V

$ [3 |. m# W) h& |- p第12行 .ANY (+RW +ZI)所有的RW ZI,全部放到RAM里面。RW,ZI,也就是变量,这一行指定了变量保存到什么地址。) l4 d& Q/ d  U/ R6 [

) e, Q5 L- d, Y% ?/ O分析用户代码
: O+ X7 g) V4 O+ i到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。, _4 n# C% p+ w7 y/ \

" s8 |. |5 N4 r6 s+ K1 程序跳转到main函数后:RCC_GetClocksFreq获取RCC时钟频率;SysTick_Config配置SysTick,在这里打开了SysTick中断,10毫秒一次。Delay(5);延时50毫秒。
  1. int main(void)7 L: e4 m" b& {  ]3 [# W7 J9 X$ X
  2. {6 f  @0 Q. n& i
  3.   GPIO_InitTypeDef GPIO_InitStructure;- h, P/ W# p# z9 u9 j3 U+ f/ t, `3 L5 ?. [

  4. 6 ]& z) p; t) P/ Z: r1 G
  5. /*!< At this stage the microcontroller clock setting is already configured,
    ) f6 M" ?9 R; r' I
  6.        this is done through SystemInit() function which is called from startup
    7 k7 \: d4 S# l+ d& {& N8 m5 Z; G
  7.        files before to branch to application main.6 s- \$ ]5 e6 F2 E5 O
  8.        To reconfigure the default setting of SystemInit() function,
    % R2 b1 s. d( s
  9.        refer to system_stm32f4xx.c file */+ N6 q& T/ r+ p- o
  10. ( E- O6 [$ E8 x2 a2 v7 q2 t7 R
  11.   /* SysTick end of count event each 10ms */
    " a0 p. M. ^; W6 f
  12.   RCC_GetClocksFreq(&RCC_Clocks);; x; a/ r, y; q* p2 H) o
  13.   SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);) X2 c0 t. l; S# ?1 G% B! h

  14. & L7 g: w2 S0 }' u( f, F( g% I% C
  15.   /* Add your application code here */
    : B5 O8 k' b3 z# w& k, L
  16.   /* Insert 50 ms delay */
    ) ]: q! \- T! R" s# b$ i. s# @+ y
  17.   Delay(5);
复制代码
, d+ V# v/ K9 }7 D
2 初始化IO就不说了,进入while(1),也就是一个死循环,嵌入式程序,都是一个死循环,否则就跑飞了。
  1. /*初始化LED IO口*/6 L- p) z8 ~7 z) e( a2 G
  2. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);
    " m' w  _2 X% `; i- @% r
  3. 1 [/ A. ?$ c& v. p
  4. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;
    4 m6 Q4 h# O* L3 j7 v; g+ {- @
  5. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;8 [" r2 C' D) |' m( }& I3 \

  6. - x6 `$ ^' f- S/ K( p& J
  7. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;* [2 \. I* h' _
  8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
      w6 Q1 e( m- X7 t' }7 \( A
  9. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;% n6 Q5 N' n3 }6 c' H
  10. GPIO_Init(GPIOG, &GPIO_InitStructure);    * M1 Q2 d# `8 R: q4 i

  11. ) i0 R) k+ I% {& P
  12. /* Infinite loop */
    " z9 h( h! v" J; S
  13. mcu_uart_open(3);
    ; o! J/ [  i" A1 |
  14. while (1)
    & ?% y/ v' I  v0 ?" m& x
  15. {
    ' X$ K+ x, L' Q' S3 `; s5 ~( D
  16.   GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
    7 e/ V( F. {! g* a
  17.   Delay(100);
    ! r- z0 l/ T# ~6 _; \
  18.   GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);1 X8 p9 W6 D7 \5 W* P3 S7 ~
  19.   Delay(100);
    * b3 Z1 ~" h& n/ o5 l. q
  20.   mcu_uart_test();- c* ^% v  I2 O. D1 x, F) L
  21. 5 \5 O5 l' t  m* n# J) A
  22.   TestFun(TestTmp2);
    : T: ?4 \" }+ G# u% j
  23. }
复制代码
. ^3 `* C! o8 ^0 y  N5 {
3 在while(1)中调用TestFun函数,这个函数使用两个全局变量,两个局部变量。
  1. /* Private functions ---------------------------------------------------------*// W  f% |7 Z5 `8 r' [& L3 s
  2. u32 TestTmp1 = 5;//全局变量,初始化为53 c3 u/ Q. A8 y3 p' r0 @
  3. u32 TestTmp2;//全局变量,未初始化- f; f% {0 l! S  d& ?( u  g8 V

  4. 5 h# p  S* {3 ^/ o1 @% g1 {
  5. const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};
    5 ~5 @4 E( B7 y
  6. ) G! R5 E0 V0 W+ O- ?: E  W1 O
  7. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
    8 C2 l2 l2 i! n  \
  8. {
    6 K2 k) D7 h1 @/ ?% ]# O
  9. u8 test_tmp1 = 4;//局部变量,初始化# ^7 u4 [' m; \" o
  10. u8 test_tmp2;//局部变量,未初始化
    # B, Y3 n( n$ D2 }1 Y5 {

  11. 2 e' V( S0 W: d* B
  12. static u8 test_tmp3 = 0;//静态局部变量9 J2 {7 \1 ?3 D3 `6 A
  13. ) \( z. U! w' f' x6 Y$ U
  14. test_tmp3++;
    5 Q: Y  [# M: `" x- P: ]7 ~

  15. " q- Y1 D1 u! G
  16. test_tmp2 = x;% O7 R" M% f2 K5 K6 q' z7 S

  17. - b& t4 _4 }0 \1 F% n; J$ U! ]& [! T
  18. if(test_tmp2> TestTmp1)
    : C, i' Z: \/ [. {5 f' L
  19.   test_tmp1 = 10;
    / W4 p! k: Z) [, e/ E5 I1 S- ?
  20. else
    " ~0 y, J0 J( p7 T/ |
  21.   test_tmp1 = 5;
    0 d6 x, U7 Q5 g

  22. ) A9 y# p/ I; R* q8 b7 O) y
  23. TestTmp2 +=TestTmp3[test_tmp1];7 s. B4 j  W. ]; x

  24. ; ^7 Y( X; J2 @. O4 `1 q; @
  25. return test_tmp1;
    . v0 v$ x$ _% t6 N7 p% n
  26. }
复制代码

, D0 V! a: m* P& ^% M然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。相关文章:STM32中断系统。我们查看Delay函数,uwTimingDelay不等于0就死等?谁会将uwTimingDelay改为0?
  1. /**
    6 C: q2 B% k1 W* b
  2.   * @brief  Inserts a delay time.
    / u4 d, M& \7 Z
  3.   * @param  nTime: specifies the delay time length, in milliseconds.
      S* o; j3 @* x6 x! q( _! ^
  4.   * @retval None2 ?* {9 V* ]: W$ _$ N, F" G, |
  5.   */  m2 G- o' U7 j/ H; x! G  {2 S
  6. void Delay(__IO uint32_t nTime)
    ' N) J) M+ {% Z1 I( G- K7 g' X
  7. {6 t- y; v" R: {, ]6 `# v
  8.   uwTimingDelay = nTime;
    / Y: W) s+ `: M1 b/ m: {
  9. # ]% S5 d2 n6 }
  10.   while(uwTimingDelay != 0);
    0 y0 V" C+ [7 j9 e0 H- t/ q
  11. }
复制代码
% h8 d4 M! f' c0 ]# s4 J: j- Q- W
搜索uwTimingDelay变量,函数TimingDelay_Decrement会将变量一直减到0。: K( f6 H9 n3 f( @  A3 f
  1. /**
    $ ~: l0 }1 m6 t
  2.   * @brief  Decrements the TimingDelay variable.7 Q' G2 w; |4 `9 l6 m+ e3 A
  3.   * @param  None* l) A* ?) g$ h$ F' q9 h
  4.   * @retval None
    ) S/ w  `9 A7 a
  5.   */* @# m9 |/ d' f6 `3 g/ |
  6. void TimingDelay_Decrement(void)/ [1 ]3 Q( F3 p; v0 o' `' t/ W, L% `. o
  7. {- J  A/ l" ^, J
  8.   if (uwTimingDelay != 0x00)
    & _( t: I0 s, {. w) C
  9.   {
    % I" Q7 T! Q6 n; X& D: J3 F) z
  10.     uwTimingDelay--;9 F" ]' K: d6 t3 H; o% X
  11.   }8 @9 }+ A' a3 c& w  n4 N) P! _
  12. }
复制代码
  i8 g9 s: d+ a: R8 I# g
这个函数在哪里执行?经查找,在SysTick_Handler函数中运行。谁用这个函数?
  1. /**" H1 m& c: r& R. _0 n9 _9 f6 C
  2.   * @brief  This function handles SysTick Handler.; [6 j( G% ~- T/ i9 \
  3.   * @param  None
    1 a' [5 ?' [3 o. t0 z' O9 V
  4.   * @retval None
    + `7 I. v5 F! y$ X
  5.   */4 j" P" o* C$ T  p/ |6 {  m/ x
  6. void SysTick_Handler(void)8 D- j' S2 |) {0 O: G- r' O
  7. {/ F: v- J; I# Z* ?7 S
  8.   TimingDelay_Decrement();
    + C2 U# L- u7 u$ z$ A8 {
  9. }
复制代码
4 }" w% M& J8 N+ L, p% t$ ?
经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。
  1. __Vectors       DCD     __initial_sp               ; Top of Stack
    ! H" h: p% @* p# I
  2.                 DCD     Reset_Handler              ; Reset Handler
    / A! |* g4 S/ e6 b: B* X& w! n
  3.                 DCD     NMI_Handler                ; NMI Handler
      [5 p2 t6 P$ _, }" q' `" L( o
  4.                 DCD     HardFault_Handler          ; Hard Fault Handler$ K. i% |2 E2 c& t( h+ t/ e" n
  5.                 DCD     MemManage_Handler          ; MPU Fault Handler2 t: v0 _- v# q
  6.                 DCD     BusFault_Handler           ; Bus Fault Handler" |! d! q4 H5 [' u* W7 c/ s
  7.                 DCD     UsageFault_Handler         ; Usage Fault Handler
    0 X, G" J* ?  w. y$ O/ e* z
  8.                 DCD     0                          ; Reserved
    7 N6 j* l5 X' F' j) f+ S
  9.                 DCD     0                          ; Reserved
    % q4 t$ H- m3 ?: s
  10.                 DCD     0                          ; Reserved5 Y6 E* [. E/ }/ B/ N8 F
  11.                 DCD     0                          ; Reserved; E' Q7 R6 \% x3 p9 M( l4 |
  12.                 DCD     SVC_Handler                ; SVCall Handler
    4 W, C5 {. D& M' `
  13.                 DCD     DebugMon_Handler           ; Debug Monitor Handler3 Q3 y1 m2 _' a2 P
  14.                 DCD     0                          ; Reserved
    9 Y$ b, X7 O! n# j
  15.                 DCD     PendSV_Handler             ; PendSV Handler% R- v' ?2 y2 f0 z6 ?2 V5 f& G& r
  16.                 DCD     SysTick_Handler            ; SysTick Handler
复制代码
) ?# }$ p0 T/ A% `  Y# {
余下问题
- e5 N. i2 A& O( N0 e9 ]$ e1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(InRoot$$Sections)是什么?3 ZI段,也就是初始化为0的数据段,什么时候初始化?谁初始化?  Q- ?- X: D. }$ F

" t! ]! u" W8 E3 C: k$ v0 c为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!
' h: f" v: m1 Z! I( ?+ ^8 Y/ n: f' B% A: A# |. A) g, \$ [. Y
" j7 O4 q. Y& |0 w1 ]6 d- o
通过MAP文件了解代码构成
7 j% h! d# r, |' a: K编译结果
+ Q0 q8 K4 S& S1 Y5 e
程序编译后,在下方的Build Output窗口会输出信息:
  1. *** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'/ r& j+ ~/ y: {% Z
  2. Build target 'wujique'
    1 P/ l9 Y+ G, u; }
  3. compiling stm32f4xx_it.c...
    ) P2 _5 J6 m/ p
  4. ...
    4 C. A" C. {; T) Q8 T  t! c
  5. assembling startup_stm32f40_41xxx.s...* W- P" a# M, r) c$ {" u/ K2 b0 A
  6. compiling misc.c...
    2 |6 L" u/ k) I" b6 K3 |8 O
  7. ...2 x" E+ V' w# J2 ?% M1 Q" A
  8. compiling mcu_uart.c...+ g) f; E: b8 |- {* N
  9. linking...8 x- z/ O' j9 M: P: O- e
  10. Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  
    : h- }$ p, h: @/ s. N& `
  11. FromELF: creating hex file...6 l! z, O* |1 F6 w1 ]
  12. ".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s).* L4 J4 w9 R2 r% R4 k
  13. Build Time Elapsed:  00:00:32
复制代码

7 e2 n* T" _/ V编译目标是wujique
5 z. X0 W2 ~) iC文件compiling,汇编文件assembling,这个过程叫编译
$ y8 @* A( h; f9 [/ K. n/ \0 h编译结束后,就进行link,链接。4 S, B+ x1 l' d, \5 N# ?5 ~4 r
最后得到一个编译结果,9038字节code,RO 990,RW 40,ZI 6000。CODE,是代码,很好理解,那RO、RW、ZI都是什么?
% \* p( O( a9 W) W6 O# a! `FromELF,创建hex文件,FromELF是一个好工具,需要自己添加到option中才能用
; w+ ^  l6 O& A* g- u8 O
. K% e& x1 M1 Z4 _
map文件配置8 z1 a5 _6 H  c
更多编译具体信息在map文件中,在MDK Options中我们可以看到,所有信息都放在\Listings\wujique.map' ]# L$ U" [1 t/ t# z' X  N

  V. a0 p7 A+ L( _- M; h- P默认很多编译信息可能没钩,钩上所有信息会增加编译时间。: ~: R0 P! p/ T/ s: N% I: o
, p* t6 ]9 ^( `( l* j0 l
4.png ) m) S4 B# e3 o& T: V+ s, o

" T, r+ {& T  Gmap文件

% v; \; f" r# _* V6 A, j- z打开map文件,好乱?习惯就好。我们抓重点就行了。
! {9 G! M( {* m8 r* k- ~+ S8 E
, k8 r. n+ L, H1 \7 y; ] 5.png 7 i- y9 `& v7 w' F5 e  w
' N& Y1 Y9 Y0 i, I* W) Y0 _& I5 L  ?
map 总信息: w( J) y$ O% W/ Z

0 e2 I; O1 h/ O从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。
( j# W. l- z& B2 K/ |" M$ @" U! g! N2 J+ K, I
有多少RO?RO到底是什么?0 f+ F( h% [7 O" M
  z' ]- f! F) u; K0 ~. |( u! Q( S
有多少RW?RW又是什么?
: g/ Y( D, `" f% c: u: Y7 x! I/ R' i( D
ROM为什么不包括ZI Data?为什么包含RW Data?( f5 R2 J, ?$ |6 [! R: y

. R  w7 h3 V. f( i) T9 p) _
6.png
* _) a1 ^. c1 N0 i! B  T) D1 V/ KImage component sizes
; S0 ]% ~  v  E) h2 \2 q) D/ |8 F6 J! d7 P5 K0 |% d
往上,看看Image component sizes,这个就比刚刚的总体统计更细了。, j& d* D1 W" w6 [5 v

8 x2 o$ m. p7 e$ I这部分内容,说明了每个源文件的概况
6 ?. M2 W2 R4 |6 M* S3 h2 [

8 X$ M! s& J! C1 ]$ r, I首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些STM32的库文件。, E! @. e& l' Y. b
4 v( ~5 y4 X! H3 i) _" g
7.png
- Y' ~0 V3 y; R" v7 W
' w* l% ~0 N. X8 \- Y
第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1M flash上的程序压缩到能在512K上运行。
& ]$ }' P7 {2 @# n8 Y- D9 k, B
( C& `. s' w6 U& I$ U) Y) R! _
8.png
' J! X" r" Y+ l; |' j
$ o5 ~4 p/ g9 z' u
第3部分也是库,暂时没去分析这两个是什么东西。
$ s4 Y0 ~( k6 b7 n# C1 t5 i0 x$ m* m8 ~" J- L
9.png
9 z- q3 _3 d5 ?

! @6 v7 ]5 j8 T! N6 o库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:
6 \7 p" d2 h7 D#include <stdarg.h>1 T: i% m# q9 F5 C/ s5 ~( D
#include <stdlib.h>. T, @5 @+ s0 [8 _4 r& w, n
#include <string.h>  
0 W' Q0 z  N9 z' o" p' T& O4 h5 Q5 e0 C, K
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。, {1 J0 H; u; M- C9 K* T9 D
8 k& I' m( ^0 _1 Y5 U

% r' y1 k, w1 ~: H文件map! m. r# R+ O) h) C9 U
再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET
0 }" a2 v/ z6 v) T2 z: _! O
, ^( M; K6 J) W; x5 X7 B' h库文件是什么?
' M) @  q9 l5 Z5 l库文件就是别人已经别写好的代码库。- I# j3 V" J2 f, x1 Q; p

1 _6 h" D- L+ ?在代码中,我们经常会包含一些头文件,例如:7 `' M& u9 L0 {' O' l! u
  1. #include <stdarg.h>
    ; Q! g/ u$ |5 b( S' ], s3 {( O
  2. #include <stdlib.h>
    8 F7 D. f5 x/ ?: l* ?
  3. #include <string.h>
复制代码

8 Z: l: b- L6 |  n: ^这些就是库的头文件。相关文章:C语言中的头文件。这些头文件保存在MDK开发工具的安装目录下。& v9 k( W. U4 A) n1 x
; P% R& L4 V+ U$ O6 n
我们经常用的库函数有:memcpy、memcmp、strcmp等。3 W* B/ a0 D: Z2 M8 G/ l) p2 a9 A

5 E8 M  J! k& e只要代码中包含了这些函数,就会链接库文件。
' k/ `  v' ]0 w4 ?# y4 g  X' D' J( b
文件map
9 D# h' M2 }0 B# b再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET
3 J% r6 Q5 q! h, L$ b% G+ X$ U" i" H* @' v
10.png
2 r: N# Y7 R& ]- @  T0 W' z

; x% Z8 _& ^$ D每个文件有有多行,例如串口,4个函数。
' z' a2 [8 Y' y8 w( d' o3 D6 ~  S6 d" a$ W3 B/ l9 S
11.png

+ `  }% Z, N% o1 K4 {% d6 c
* Q4 w3 A3 `. c
然后是RAM的,main.o中的变量,放在0x20000000,总共有0x0000000c,类型是Data、RW。串口有两种变量,data和bss,什么是bss?这两个名称,是section name,也就是段的意思。看前面type和Attr,
6 q( ~9 @1 y6 g, _
% _( w9 K2 D) v% D# A7 bRW Data,放在.data段;RW Zero放在.bss段,RW Zero,其实就是ZI。到底哪些变量是RW,哪些是ZI?
, h0 r- y7 m* Y2 y$ o$ s# o8 R4 M# [
12.png
! }6 ^0 H6 X6 a; u
2 a% g" x* g3 x3 q; R
Image Symbol Table9 a" g; {, Z! @5 _
  n* f! f+ ?; @- ?+ u3 q3 h
再往上就是Image Symbol Table,就更进一步到每个函数或者变量的信息了: Q6 E# [& x! w: Y+ l  o
* o# i8 {- ~0 y8 Y2 {1 f+ w. Z0 \
13.png
1 K! W5 \: B; R: |9 z) c

, `5 K. E* I7 h. j. C例如,全局变量TestTmp1,是Data,4字节,分配的位置是0x20000004。
1 R; P3 t& v' \/ m) A" o
2 L6 F: e  \+ ~) ]  X, B# j
15.png

/ C& k: F+ [  l( I: {

6 i) J! n- _! X+ ]' J4 x5 ?7 m0 KTestTmp3数组放在哪里?放在0X080024E0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几K几十K大,不可能也没必要放到RAM区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。
: h4 {& y; ?1 n( }
6 G! X) Y9 T: Rconst的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习: P6 d9 W; l1 z$ l" s, h+ l8 |

; x. a$ ]! D5 H* \. o
16.png

1 a7 ?6 s7 Y$ I" s
# U5 y( j& [4 M( M1 `6 @
那局部变量存放在哪里呢?我们找到了test_tmp3,
2 d1 h% }! C- u1 N$ b& \3 w0 u1 G! F% Y8 @
17.png

8 m: |3 c& q0 M7 J2 B, A; Q2 Y
5 f3 k0 c2 ^$ f# F
没找到test_tmp1/test_tmp2,为什么呢?在定义时,test_tmp3增加了static定义,意思就是静态局部变量,功能上,相当于全局变量,定义在函数内,限制了这个全局变量只能在这个函数内使用。哪test_tmp1、test_tmp2放在哪里呢? 局部变量,在编译链接时,并没有分配空间,只有在运行时,才从栈分配空间。
  1. <blockquote>u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
复制代码
' X: n; x  O3 L  H6 N% f4 i) A8 o) Y
上一部分,我们留了一个问题,哪些变量是RW,哪些是ZI?我们看看串口变量的情况,UartBuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文Block Started by Symbol的简称。
' c8 }# y, d5 Y7 _- _$ D8 r6 R) J/ `
18.png

& @: _0 ^9 R2 d5 C. ?) \1 S% t" c, }( M+ n2 }1 O) E
到这里,我们可解释下面几个概念了:: K/ e9 b& Y+ R  r7 S. Y
Code就是代码,函数。& S. M2 u& @9 m0 L( _+ Q0 O, E! O% @
RO Data,就是只读变量,例如用const修饰的数组。
; n, N" K  T, e/ j$ pRW Data,就是读写变量,例如全局变量跟static修饰的局部变量。
6 d5 Z) F  H; T% J+ _# vZI Data,就是系统自动初始化为0的读写变量,大部分是数组,放在bss段。
3 d% J( Q/ p: t6 v, LRO Size等于代码加只读变量。
0 F" ~5 L* U6 ?1 Y* f  Q3 QRW Size等于读写变量(包括自动初始化为0的),这个也就是RAM的大小。
9 T, }& B3 ^' N, BROM Size,也就是我们编译之后的目标文件大小,也就是FLASH的大小。但是?为什么会包含RW Data呢?因为所有全局变量都需要一个初始化的值(就算没有真正初始化,系统也会分配一个初始化空间),例如我们定义一个变量u8 i = 8;这样的全局变量,8,这个值,就需要保存在FALSH区。3 E7 W& A- l$ r3 n/ E

; ]4 B: R$ e" f% @7 N3 r
19.png

1 x! j$ A  P, K: i1 _

2 m) o+ T' y0 C4 S/ y, ^! R7 B我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579; }: R9 B2 R4 L# r

8 m7 M: B& K+ K9 g
20.png

1 @: n) \0 q$ R2 `
: Q0 Z$ k7 T; C" j
main是main,放在0x08000189% T5 {, t0 n8 E/ r2 j7 |( L

; R5 i7 U! j) U3 J. M2 d* E
21.png

6 h0 ]# y& K+ S% S

0 R9 ]7 j+ u* _% b9 D0 H2 n__main到main之间发生了什么?还记得分散加载文件中的这句吗?
9 s$ e$ o% U; v+ n. V*(InRoot$$Sections)
5 D) \+ c+ @- v6 [# ]3 q$ V3 z& W8 j' M  w; s
__main就在这个段内。下图是__main的地址,在0x08000189。__Vectors就是中断向量,放在最开始。
4 N6 z2 R* i3 g! G8 V% O; U& ]- J! @( K  ^% d2 x' y
22.png

$ ?- N1 L' C+ B! J* ~
0 _( N' B' l  m( N" q. q
在分散加载文件中,紧跟RESET的就是*(InRoot$$Sections)。1 {7 ]! @2 q1 m. A3 u1 J! A% J0 O

- |* I* I4 G, L2 t' _5 W) I) D/ a6 a
23.png

5 j4 U: t3 e1 U$ d- S

/ o. a4 t9 Z& |# M/ [8 o而且,RESET段正好大小0x00000188。9 I! f7 J: I9 F( Y4 t9 d

" P+ s2 S8 I# U% [" p. l" L
24.png
( Q; R+ |& I* M7 |' z

6 V) @5 |3 |& M0 c7 R0 }; S) S
巧合?可以参考PPT文档《ARM嵌入式软件开发.ppt》。
0 q5 u# I" D: Y6 u# m# B
/ d2 c9 E5 t" @# T: M
25.png
7 _& l% O$ ?9 C" B' z

8 J6 L  K2 G  V2 ~* q) M1 c+ c
这一段代码都完成什么功能呢?主要完成ZI代码的初始化,也就是将一部分RAM初始化为0。其他环境初始化……
: ]. ^  ]( H9 D8 F# y4 F( s3 A1 {: P
0 e- @( k6 O. P
5 d  }; N% H3 o3 ~! x* z
最后1 C0 F% x3 j1 a* m) P) @
到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。
- I$ x" ~2 Z% h, [: t7 o2 r- w. s9 \( X
+ K4 A# x9 g2 u% R, M2 y/ G
转载自: [color=var(--weui-LINK)][url=]STM32嵌入式开发[/url]" q1 q' I* ?6 U2 _) s
如有侵权请联系删除# G! L6 w* ]' o3 A' g$ V

* S4 p; f$ |# R8 @+ r) Z- O& n- S2 \
收藏 评论0 发布时间:2024-7-29 15:25

举报

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