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

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

[复制链接]
攻城狮Melo 发布时间:2024-7-29 15:25
本文分析STM32单片机到底是如何软硬件结合的,分析单片机程序如何编译,运行。, }& _: N/ O2 N$ v, F
# J& n/ j1 @  ]2 X, t6 j
软硬件结合
1 k; G: O' L' ^& m初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写P1=0X55,就可以在IO口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。; P% w( R! r9 I/ G* ]

  ?' _* O* l2 O2 ?# J- O寻址空间9 J& v( p1 x# P: s: o* [
什么是地址空间呢?所谓的地址空间,就是PC指针的寻址范围,因此也叫寻址空间。1 l  k% g& Y! r( o5 \5 W: a+ _

' o+ m7 S& J! Q/ V3 [! e$ m大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,PC指针就是一个32位的二进制数,也就是0xffffffff,范围只有4G寻址空间。现在内存越来越大,4G根本不够,所以需要扩展,为了能访问超出4G范围的内存,就有了64位系统。STM32是多少位的?是32位的,因此PC指针也是32位,寻址空间也就是4G。
' m9 f! K9 E5 [1 [! c$ Q/ T$ k# f* @$ F  ^4 j/ y
我们来看看STM32的寻址空间是怎么样的。在数据手册《STM32F407_数据手册.pdf》中有一个图,这个图,就是STM32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫Memory map,用一个新芯片,就先看这个图。
5 B- X2 _+ P: i6 T- s! r& p- J' ]' S4 z# y, G: x8 p
微信图片_20240729152315.png
& N; a  a7 J; V/ U5 [# Y

9 B/ k5 F( x) o6 Y1 J+ M  l, p1 P. s8 f( f7 U
最左边,8个block,每个block 512M,总共就是4G,也就是芯片的寻址空间。
, o. ^" v- f- V0 R) m; X. c9 x
' k7 Y8 n# g/ C# f4 Kblock 0 里面有一段叫做FLASH,也就是内部FLASH,我们的程序就是下载到这个地方,起始地址是0X800 0000,大家注意,这个只有1M空间。现在STM32已经有2M flash的芯片了,超出1M的FLASH放在哪里呢?请自行查看对应的芯片手册。& p8 \8 Q, g& ^2 B6 p" C! h8 x, @  B

. H: [# d% h. Y: S( v3 在block 1 内,有两段SRAM,总共128K,这个空间,也就是我们前面说的内存,存放程序使用的变量。如果需要,也可以把程序放到SRAM中运行。407不是有196K吗?- `- T+ @8 S% X) D& e

1 g* c! {' E) o+ k6 h! j( z其实407有196K内存,但是有64k并不是普通的SRAM,而是放在block 0 内的CCM。这两段区域不连续,而且,CCM只能内核使用,外设不能使用,例如DMA就不能用CCM内存,否则就死机。
3 b: F/ `* m! O  R. n3 R* J/ I8 f- A/ q5 F9 g/ |
block 2,是Peripherals,也就是外设空间。我们看右边,主要就是APB1/APB2、AHB1/AHB2,什么东西呢?回头再说。
9 `$ f# p) |, q5 k& M( m( h9 B
5 r0 {4 X, e5 t1 H! Xblock 3、block4、block5,是FSMC的空间,FSMC可以外扩SRAM,NAND FALSH,LCD等外设。9 M' N5 n2 L* j) y
) ?$ }5 @9 X7 t/ E
好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。对于这个疑惑,也可以看此文:代码是如何控制硬件的?在IO口输出的例程中,我们配置IO口是调用库函数,我们看看库函数是怎么做的。" N% E9 o+ R2 c7 H
% A2 Q5 F6 D& z% W
例如:
. r, v9 Q0 m8 w, _7 |9 U
  1. GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
复制代码

( C. O" @2 s/ t# v# |, ]这个函数其实就是对一个变量赋值,对GPIOx这个结构体的成员BSRRL赋值。
+ ]( Z$ C! F: Q9 i7 v( s" W* h
  1. void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
    * w3 P- Q/ `  E' D1 T# z6 [; ~% }7 I
  2. {
    8 D) z8 }8 q  _
  3. /* Check the parameters */% M! R1 X: F& U1 V! Z9 n) E9 g
  4. assert_param(IS_GPIO_ALL_PERIPH(GPIOx));% q1 w2 i0 n* `& ?
  5. assert_param(IS_GPIO_PIN(GPIO_Pin));
    + ~- G( \/ F# w+ j; k; }

  6. ; b% A* n3 B$ d. c

  7. $ {% I% [' j( E. Z' w# b
  8. GPIOx->BSRRL = GPIO_Pin;
    ( z! F5 ]) Y5 E; m4 J
  9. }
复制代码
) X6 L" G0 j: k0 V) q; N3 ~) A
assert_param:这个是断言,用于判断输入参数是否符合要求GPIOx是一个输入参数,是一个GPIO_TypeDef结构体指针,所以,要用->获取其成员& g& h/ C9 g8 w

( K* n) O  m. e$ r/ D2 MGPIOx是我们传入的参数GPIOG,具体是啥?在stm32f4xx.h中有定义。2 X* c, d  C0 w# k2 @, }9 B" @& E
  1. #define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
复制代码
7 a' d/ G) F' L# t, V- o4 {+ X
GPIOG_BASE同样在文件中有定义,如下:
0 I% r, z0 H) a- C( C; a
  1. /*!< Peripheral memory map */
    # N* k4 x5 p  {, _
  2. #define APB1PERIPH_BASE       PERIPH_BASE- E$ A5 k8 E! r6 b( N) a4 m& D
  3. #define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)# ?" b" }% }( v$ o* q7 k
  4. #define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)
    % C  g* F7 R& N* v/ R* g' k0 U
  5. #define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)
复制代码
, Q4 w. K1 `+ ?- }1 ?
再找找PERIPH_BASE的定义1 v, |" k1 |; d# i, P
  1. #define PERIPH_BASE           ((uint32_t)0x40000000)
复制代码
到这里,我们可以看出,操作IO口G,其实就是操作0X40000000+0X1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是SRAM空间地址,0X40000000+0X1800是外设空间地址。$ j6 u3 }1 f6 M. l, o9 }
  1. u32 i;  ?  @/ \2 |$ }
  2. i = 0x55aa55aa;
复制代码

/ Q2 v( g5 ^. }9 W8 b' h这个外设空间地址的寄存器是IO口硬件的一部分。关于如下图STM32的GPIO文章推荐:STM32中GPIO工作原理详解。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0X40000000+0X1800+0x14.
. N4 Z8 H$ H! D0 Q% a- J8 d+ C" i7 S8 \# e* x( O* l6 m
微信图片_20240729152319.png

& |; `) ?# X9 O7 x8 a, p" |

* b' J/ L8 C( L' q+ v( _控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。
1 d- ~- `& n9 z% U, I( F
+ `' D, J( L8 {; j寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或RAM。寄存器为什么能控制硬件外设呢?因为,初略的说,一个寄存器的一个BIT,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。
6 b: t3 F5 B& a) `

; r2 }8 i7 f* F: X7 \纯软件-包罗万象的小程序
4 W: p, K7 H" Z我们已经完成了串口和IO口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?关于程序是怎么在单片机运行的,也可以看此视频:动画演示单片机是如何跑程序的。代码到底放在那里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。  J5 v4 L8 X5 {+ B7 R0 y6 R
3 ^) Z6 |5 G% p8 K3 ~
分析启动代码9 j2 N' V3 E$ s
函数从哪里开始运行?8 B1 K* F- Z- T
每个芯片都有复位功能,复位后,芯片的PC指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,PC可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在STM32中,复位到0X08000004。因此复位后运行的第一条代码就是0X08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。
5 n( W+ L1 T+ M7 `9 t, ^/ ?9 j
6 r9 O  [. n) U# G9 {0 C芯片复位时,芯片从中断表中将Reset_Handler这个值(函数指针)加载到PC指针,芯片就会执行Reset_Handler函数了。(一个函数入口就是一个指针)
  1. ; Vector Table Mapped to Address 0 at Reset0 L2 y, \. d5 D2 r6 q
  2.                 AREA    RESET, DATA, READONLY& J. w/ h% ]- m% @$ h' a8 F1 A/ ]
  3.                 EXPORT  __Vectors
    - h( o# ~2 A' h$ r
  4.                 EXPORT  __Vectors_End
    . D: c, {0 f7 |) R- r
  5.                 EXPORT  __Vectors_Size. N" C& }6 D/ w0 p1 B
  6. ! f% D9 y# T9 o' w5 O
  7. __Vectors       DCD     __initial_sp               ; Top of Stack. X% x% A( o$ Y" W8 p
  8.                 DCD     Reset_Handler              ; Reset Handler& ]! \) j1 H! Z- e: u% i0 w# X
  9.                 DCD     NMI_Handler                ; NMI Handler
    / M6 w. o3 h3 s- J
  10.                 DCD     HardFault_Handler          ; Hard Fault Handler" g/ u, o# T7 e
  11.                 DCD     MemManage_Handler          ; MPU Fault Handler
    " o8 j# [+ ?: [8 t" \7 P# [
  12.                 DCD     BusFault_Handler           ; Bus Fault Handler) u2 E6 i+ U$ q9 S
  13.                 DCD     UsageFault_Handler         ; Usage Fault Handler
复制代码
2 H+ q% V1 ]8 S
Reset_Handler函数,先执行SystemInit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?
! k% ?4 k) ?2 _6 ?, f, c
8 n" e# }0 }  n. \( P7 [3 o; s0 S是我们在main.c中定义的main函数吗?后面我们再说这个问题。9 {' f' d3 Q8 `
! p/ }9 v& K6 Z
微信图片_20240729152323.png

8 h) g& L( g% J( M
5 p/ x8 y5 X: P- j) s! j
芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujique\prj\Objects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。# O- W5 O0 Y5 T$ ^0 N4 _
3 t2 j- N0 B6 R/ d
在MDK软件Options菜单Linker下有关于这个菜单的设置。1 {+ m$ B+ G4 f' s3 o3 M
% ^8 P% [- u& c' w2 ?% ~. U7 L5 j
微信图片_20240729152326.png / s+ J4 {, w9 z/ [% W# `

' C: X1 ^5 I! b5 S4 W: ?
7 V! l( Y. p  r2 N( Y6 T
" P- I6 U5 g! ?. M! z" |
把Use Memory Layout from Target Dialog前面的勾去掉,之前不可设置的框都可以设置了。点击Edit进行编辑。
& m1 ^8 x; \" q) U4 X2 u% |$ w& M  K* \, q
2.png

0 R2 ]+ ~9 U, _* f; u* t7 M
5 F- M4 @: \: N
. \8 y5 C5 C: X% \; w
在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。
5 x& S3 h7 z6 B# D2 c" c. C; c* L( }; ?1 j" H- k% J& B
其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定FLASH跟RAM的大小于起始位置,当我们把程序分成BOOT、CORE、APP,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到RAM中运行。4 T) E; M) j( o1 e9 J' Y

9 E9 Q; B2 i, q' w, f4 u- \# h
3.png
+ a6 m4 X. d0 p. o) f
! `9 i; H6 M" ~" S% u
从这个基本的分散加载文件我们可以看出:
$ Z* B* C' w% i1 ~1 m% q8 m8 {& u% {6 Q# ?( t7 U
第6行 ER_IROM1 0x08000000 0x00080000定义了ER_IROM1,也就是我们说的内部FLASH,从0x08000000开始,大小0x00080000。
) p1 X1 a- ?. p: n# L/ E8 |6 k! x. U9 H/ n6 P  y( W, T$ W4 ]& ~6 ?
第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。
4 l$ j! f& A3 h: _
' l5 Y3 T& Q: N, I' n! }  y- j
第8行 *(InRoot$$Sections)什么鬼?GOOGLE啊!回头再说。4 x, |4 W- G) |6 E( W% L
( J! W! y2 i( T2 Y8 s
第9行 .ANY (+RO)意思就是其他的所有RO,顺序往后放。就是说,其他代码,跟着启动代码后面。1 l8 W: ~) l* O8 b( i: n
) I/ ?5 U$ V( Y9 {
第11行 RW_IRAM1 0x20000000 0x00020000定义了RAM大小。
6 n8 C% g' [6 G2 P, f: _% k
2 K7 ~9 C" H3 T2 F
第12行 .ANY (+RW +ZI)所有的RW ZI,全部放到RAM里面。RW,ZI,也就是变量,这一行指定了变量保存到什么地址。% k2 O3 s- W8 ^
  z% x8 V- t# u  F
分析用户代码
/ j! t% r5 M9 T- j( c; ?# u到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。+ j" a1 H- b- _2 i( C
7 L# ?9 l0 V0 c+ D/ ~' _
1 程序跳转到main函数后:RCC_GetClocksFreq获取RCC时钟频率;SysTick_Config配置SysTick,在这里打开了SysTick中断,10毫秒一次。Delay(5);延时50毫秒。
  1. int main(void)# z0 k; P7 o8 t
  2. {+ L9 M9 L3 Y3 T; g
  3.   GPIO_InitTypeDef GPIO_InitStructure;
    3 F* Z2 p: t4 E0 `, ^! m

  4. . T" i( Q& W) G4 @# H
  5. /*!< At this stage the microcontroller clock setting is already configured,( V0 ]- a- }8 N4 g/ y3 p0 w/ q
  6.        this is done through SystemInit() function which is called from startup
    5 e0 l  x3 f4 c" h
  7.        files before to branch to application main.
    6 T- R0 d/ m! j7 z, D/ K
  8.        To reconfigure the default setting of SystemInit() function,
    : _: l% [. i# V% E( ~
  9.        refer to system_stm32f4xx.c file */
    7 {* Z+ ?4 r  w4 r
  10. , C: X2 O: f7 `7 r" [6 y
  11.   /* SysTick end of count event each 10ms */
      r+ y" M  C) F% S
  12.   RCC_GetClocksFreq(&RCC_Clocks);; L! t6 L3 A6 h8 v7 j
  13.   SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);# a% W6 X$ J5 r- K7 D% g1 t) t- ?

  14. 0 [2 ]: r+ j6 Y$ ?
  15.   /* Add your application code here */
    9 e6 y6 ?- f+ `3 ]; x5 i/ ^9 N6 I
  16.   /* Insert 50 ms delay */
    . A; X3 q& A4 |% S/ W- Z
  17.   Delay(5);
复制代码

- h( C4 r4 h- y/ i" C2 初始化IO就不说了,进入while(1),也就是一个死循环,嵌入式程序,都是一个死循环,否则就跑飞了。
  1. /*初始化LED IO口*/' A' B$ a/ {/ B2 h/ J! D* ?* ~
  2. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);; X* x% ]: _2 p' d* U+ }+ B' m
  3. 3 k$ W7 ^1 d& F1 [5 }7 `0 J+ N
  4. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;
    ! [- [/ k& A" L: i
  5. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    ; b0 P: p0 [1 s: D8 `
  6. : o1 L. _/ F( m- L" ^
  7. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    " z. S( c: p0 V2 M4 Z; z
  8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    & O5 H6 p+ i; N! ~
  9. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;6 e* D* E" E1 h4 n6 Q
  10. GPIO_Init(GPIOG, &GPIO_InitStructure);   
    # ~# B8 w$ Q; Q( I0 D9 u) C

  11. ! O$ Y8 m2 r( H( J! Y/ p6 [
  12. /* Infinite loop */7 F% \4 G" ?5 h4 L5 n  w
  13. mcu_uart_open(3);, ]9 X7 N+ o* i* x( N! v
  14. while (1)
    + W+ y! D6 r/ i; ]: u  q
  15. {$ K2 a% v9 c2 v& {( H; R( h  [
  16.   GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);& i1 v9 {. O# G4 |7 ~
  17.   Delay(100);
    $ h! ]2 t3 W# |0 Y& f; J9 B7 c
  18.   GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
    * v) q" k; k2 O; x7 H
  19.   Delay(100);+ Q  b. C: b/ [
  20.   mcu_uart_test();
    : h7 _5 \  s1 F! Y
  21. ' v' j/ W& l- N! b. T  _! o
  22.   TestFun(TestTmp2);
    ; c0 y9 s8 q4 \7 U" t& M% {
  23. }
复制代码
2 B7 p; O7 M- q7 \" y+ z) W: V& E
3 在while(1)中调用TestFun函数,这个函数使用两个全局变量,两个局部变量。
  1. /* Private functions ---------------------------------------------------------*/
    , v- d7 d- d1 M. t, `1 B1 y
  2. u32 TestTmp1 = 5;//全局变量,初始化为5
    # f$ e* _# p7 {5 {' ?2 a
  3. u32 TestTmp2;//全局变量,未初始化' B5 n. B2 j# k/ d

  4. ( U( v. r# N' H( ]# X
  5. const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};
    ! n: r) {1 \: S( d# _5 L
  6. 5 T9 g6 N$ q, e$ b
  7. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值, }% k. S5 O5 W
  8. {) D8 J/ M8 i( P! Q
  9. u8 test_tmp1 = 4;//局部变量,初始化' D  _  F0 O% ]1 g5 R
  10. u8 test_tmp2;//局部变量,未初始化
    ' L1 m2 n& U- O

  11. 6 W  i  u" U  j
  12. static u8 test_tmp3 = 0;//静态局部变量  b. g! Y' ?# z6 Z  m
  13. ; O2 K. ?8 b# ^* J  P* z
  14. test_tmp3++;
    4 B( c- h& p6 n+ n* \$ r
  15. 1 H2 |8 ?% [8 d  \
  16. test_tmp2 = x;; h; p1 Q% d4 C" [% C+ @

  17. / w2 ~* b# }; V1 D1 p/ p  W
  18. if(test_tmp2> TestTmp1)  o6 f; y0 i) Z/ i; D" Z
  19.   test_tmp1 = 10;, O, u0 q/ \4 k1 s+ A
  20. else! W1 E! j  C$ _- [5 m- \
  21.   test_tmp1 = 5;
    ' ^' k9 c3 h5 q6 V% h: B# |

  22. 5 F: S$ C' Q& O6 z9 d8 U! U
  23. TestTmp2 +=TestTmp3[test_tmp1];+ R  V# o# r( v6 ?0 k# O" r' J

  24. 8 e0 L1 E% v: _! s" P
  25. return test_tmp1;, }, S: K" b% s, W- i' _0 B$ N; v
  26. }
复制代码

8 F: W! F/ Z  ~' Z然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。相关文章:STM32中断系统。我们查看Delay函数,uwTimingDelay不等于0就死等?谁会将uwTimingDelay改为0?
  1. /**
      K' G6 D; e0 i  [$ y( _2 l/ c
  2.   * @brief  Inserts a delay time." ^, M4 F7 G" k$ I
  3.   * @param  nTime: specifies the delay time length, in milliseconds.2 W: ^. ]$ b) [  w4 V# A& e
  4.   * @retval None
    1 G$ z' O4 g; _8 P4 Y
  5.   */% y1 j% ?/ s5 P) Q0 t, C
  6. void Delay(__IO uint32_t nTime)9 A# }5 I! @0 D) c9 ^: b4 c- l+ u
  7. {3 B9 q2 C2 M  G4 p0 Z
  8.   uwTimingDelay = nTime;
    , g* ~2 ^# C5 j; w) i) v

  9. 0 p7 k$ E, R2 J/ m6 _- ~
  10.   while(uwTimingDelay != 0);- p& k& t  b" r' N$ p
  11. }
复制代码
, Q9 c5 b. k- c0 Z
搜索uwTimingDelay变量,函数TimingDelay_Decrement会将变量一直减到0。
" H  p; G7 R# k3 Q: T5 O- K5 C2 y6 U
  1. /**
    4 ?7 k0 V- `8 Y8 p0 N% T; G1 j
  2.   * @brief  Decrements the TimingDelay variable.
    ! U5 Q  ]; r+ r5 K4 ~
  3.   * @param  None# g) y3 r+ v) V  l$ J4 {
  4.   * @retval None  a% M5 K4 L/ i7 d8 O
  5.   */
    ( o! k" I  h* n+ T. }' v1 E$ G
  6. void TimingDelay_Decrement(void)) Z# b: a; `1 B3 M$ b
  7. {: Y4 e, h" Z1 i3 ~# {  C
  8.   if (uwTimingDelay != 0x00)) ?3 f, j8 F+ n. M
  9.   {2 s+ V* z+ `# ~) L7 U) \$ y: {; G
  10.     uwTimingDelay--;
    % o7 ]' }/ b" r  F
  11.   }
    0 D1 e7 X8 d9 a7 F
  12. }
复制代码
7 L* p! s2 L- M. S  c8 c. |
这个函数在哪里执行?经查找,在SysTick_Handler函数中运行。谁用这个函数?
  1. /**
    # b7 m+ i# C$ O* q
  2.   * @brief  This function handles SysTick Handler.
    * L% B1 V9 s) z1 N" ?2 h  p
  3.   * @param  None5 N/ g" j5 b& C9 `; h4 r
  4.   * @retval None$ P% ^% @  |6 m  u. m
  5.   */
    4 I" H; }' o# {3 I
  6. void SysTick_Handler(void)
    : A) I# i$ E4 q
  7. {# t; L% I. b8 C" q+ a2 u
  8.   TimingDelay_Decrement();" |8 ^8 G* S# C, F" z( B
  9. }
复制代码

1 k4 o2 ?+ a2 D1 k2 V' t+ V经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。
  1. __Vectors       DCD     __initial_sp               ; Top of Stack0 D6 I+ v' X. e0 C
  2.                 DCD     Reset_Handler              ; Reset Handler
    % V6 N8 F  x) p% _  _
  3.                 DCD     NMI_Handler                ; NMI Handler
    9 U- r7 U9 e" p  B9 _0 |
  4.                 DCD     HardFault_Handler          ; Hard Fault Handler
    0 k) k% B4 ~6 K. t1 K# t
  5.                 DCD     MemManage_Handler          ; MPU Fault Handler, u8 [* H1 x6 q3 v
  6.                 DCD     BusFault_Handler           ; Bus Fault Handler
    , F! n% g1 D' ^. H# Q. s/ Y
  7.                 DCD     UsageFault_Handler         ; Usage Fault Handler
    / i* t  M- G) D0 K
  8.                 DCD     0                          ; Reserved% ^9 t! \- J5 W7 p9 y' i
  9.                 DCD     0                          ; Reserved2 ~; d6 s: I" z0 @, r
  10.                 DCD     0                          ; Reserved/ ]+ |5 T4 N+ F4 ^- X) d. F
  11.                 DCD     0                          ; Reserved
    $ X2 {# V6 ^8 Z9 P6 e. s; K
  12.                 DCD     SVC_Handler                ; SVCall Handler- m0 C( A0 q. R. v! x, j
  13.                 DCD     DebugMon_Handler           ; Debug Monitor Handler; h! ~; K' a6 ~
  14.                 DCD     0                          ; Reserved) l! O1 h9 G9 T) \
  15.                 DCD     PendSV_Handler             ; PendSV Handler$ e, C4 M% P' w0 ^# u' K
  16.                 DCD     SysTick_Handler            ; SysTick Handler
复制代码

! z6 L  {) T# Z- h余下问题
% P5 l: p5 T5 y1 Z1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(InRoot$$Sections)是什么?3 ZI段,也就是初始化为0的数据段,什么时候初始化?谁初始化?
$ ]# T; q3 j! y
/ z2 }. A2 ]8 s" a! J
为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!
  v' u; {$ t+ F# O/ ~; T& D* E% @+ O' q
( ]6 |8 o3 [) e3 F4 n( Z1 h# w3 L
通过MAP文件了解代码构成  m9 K8 D9 c( @1 o5 z- K
编译结果

, [' p5 b5 K2 m$ p0 T程序编译后,在下方的Build Output窗口会输出信息:
  1. *** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'7 z- M: n& w4 B8 f7 Z
  2. Build target 'wujique'
    3 C* b( j$ [0 x( ^/ Q9 m
  3. compiling stm32f4xx_it.c...8 H! v$ p5 i3 w
  4. ...' W/ U. k8 H* ?0 _# q4 {/ ?+ R$ n( G
  5. assembling startup_stm32f40_41xxx.s...3 j6 O- c) ^' t5 S4 T
  6. compiling misc.c...) E4 g0 R$ S/ t" Z5 m
  7. ...
    . @5 Y1 F" j2 w! I
  8. compiling mcu_uart.c...
    " p; z7 ]! v! ~
  9. linking...9 I$ D, b! p, i& U6 Y0 N
  10. Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  
    * d: \7 }) i4 W7 Q
  11. FromELF: creating hex file...
    4 E* m# K6 D/ _
  12. ".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s).  t, P6 k$ q0 x5 D
  13. Build Time Elapsed:  00:00:32
复制代码

8 K) A/ O4 T  s7 |3 ]编译目标是wujique! I! @( y' F- F
C文件compiling,汇编文件assembling,这个过程叫编译
. d- |5 \5 B0 v: f编译结束后,就进行link,链接。
; m. l0 _/ E% l. c: b最后得到一个编译结果,9038字节code,RO 990,RW 40,ZI 6000。CODE,是代码,很好理解,那RO、RW、ZI都是什么?
7 n9 W/ C! ~% \6 uFromELF,创建hex文件,FromELF是一个好工具,需要自己添加到option中才能用0 r  _1 B, s' }" r

( k# q  A/ H/ q4 S+ p! G
map文件配置5 d$ l8 V, a7 k  g# j
更多编译具体信息在map文件中,在MDK Options中我们可以看到,所有信息都放在\Listings\wujique.map
& s; ~/ V1 s& x9 k) t& f& @; \/ {$ `% c0 R6 [6 b
默认很多编译信息可能没钩,钩上所有信息会增加编译时间。
  Y# u0 _* `+ V9 l. G5 V
& p$ J) N6 d6 Z' w. X
4.png # l& V2 G4 m" `

6 l; [) k/ v' t' `, B4 h! f) ?map文件
6 A1 x" c2 H5 I, W& `" U6 y, }5 F9 v
打开map文件,好乱?习惯就好。我们抓重点就行了。# i0 d" n7 c4 G+ x: g4 c

8 O( A0 b1 S* w- u) R3 W# i: S 5.png
- A, j8 l. F) G9 ?# @" N

9 E4 o5 Z; \9 _7 o7 Wmap 总信息2 w; J. v  {( Q! R! j6 U

/ \6 I! C" j% {; i8 M8 F* @, P从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。3 Q( E$ x7 x7 E# Q3 z
3 a( m! e) w9 {) \
有多少RO?RO到底是什么?
2 O; W$ L: O# ~. @2 ~1 }( U, g, u
% t7 ~+ }; X6 _* B- I* c! h有多少RW?RW又是什么?
, q8 c7 P6 P  W, _% U3 E
: Q% [* U6 l* I/ a7 S9 SROM为什么不包括ZI Data?为什么包含RW Data?
: Y# R- j) Q( \8 w1 G  F% a4 }' K5 ?
6.png 7 A* m4 ?; O( @( A
Image component sizes, X3 E; K! x7 W) d
6 e7 [0 k, j* V- J: Z/ Q/ c7 H
往上,看看Image component sizes,这个就比刚刚的总体统计更细了。+ p8 a" K" l2 j) d! ?, ]; ?
0 f1 m$ L0 A( ]4 B
这部分内容,说明了每个源文件的概况
. y+ v* a% v, H. [2 Z

3 L+ M* F* n2 P3 @' y首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些STM32的库文件。
6 H1 |+ ~3 Z! X
- z% e) {0 A$ q
7.png

" N5 i, a& j1 ~- j% M% q0 ]: Z, Y

6 [, T7 s# j2 ?. g6 G第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1M flash上的程序压缩到能在512K上运行。
; Y5 S% G# U9 q, \, S: K$ u7 M
8 o$ d! o. p) N
8.png
' y8 r$ t8 \5 ?  O" ]
( ?9 `. z( V' ~( ?4 f; C. ~
第3部分也是库,暂时没去分析这两个是什么东西。
* ]% Y& Y  ^$ l% k# q% n( A  y3 Y" w/ S7 r" P% z, w9 U. ?
9.png

& `% w! X2 o/ l* ?

. J0 O+ T1 g) J; A! a3 s, N+ p0 f库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:
' Z2 w4 o" F% C4 x6 c#include <stdarg.h>4 E6 ~! Y: L3 N1 G
#include <stdlib.h>- \; w/ j- ?( M4 m* n, M
#include <string.h>  
& G0 ?. w; t" C2 l
' _! r: ]) ?5 n" `" k
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。' |  r; A2 r, [- E0 c& P: v* z

  Z" y# f1 i% `4 q6 x/ Q. x  Y
; ?2 _9 _8 G3 {0 O
文件map
/ J" K9 s" Q) ~  E再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET
' o4 U/ Q7 |: [
! M! V9 `: j( _0 g2 \6 t库文件是什么?
' ]6 M8 p0 l& }9 T+ O& b7 f4 v! r: b库文件就是别人已经别写好的代码库。
( F4 l; O/ n/ f- r2 L2 Q( x) M8 R7 J/ p1 ]  }# y- A8 T
在代码中,我们经常会包含一些头文件,例如:  `0 w% F) e+ j% X& D+ Y! N) Z3 H
  1. #include <stdarg.h>3 k$ J: b) n/ C3 a' A: v5 W5 G
  2. #include <stdlib.h>' a  [7 M' i, }' s( Y
  3. #include <string.h>
复制代码
$ u8 W7 E/ D, e1 y* g, q0 c
这些就是库的头文件。相关文章:C语言中的头文件。这些头文件保存在MDK开发工具的安装目录下。
6 W- |9 H: `* w, l  f2 _0 K0 m8 }" p* D! c( j2 Z( E
我们经常用的库函数有:memcpy、memcmp、strcmp等。
2 E* E) }* Q& V: @+ q) f# D3 l8 \3 \* c' T* M) r: x" l- N) S5 v- {$ y
只要代码中包含了这些函数,就会链接库文件。. I! J; }: G8 W& ^0 I9 ^9 F# @

7 G# M$ e2 A1 i# G: c文件map
' m+ M6 Q5 o5 g* [7 p再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET
% w6 F# O- y& W( Y/ a6 o# M4 o: l7 X( e8 _) z
10.png
/ x3 `* \( y5 z+ f: D) b8 {
/ Q2 ~7 Y7 N5 T3 g, \6 f4 P# v8 s
每个文件有有多行,例如串口,4个函数。; {4 Z; [6 |: Q+ \

$ t, b) w% a. [
11.png

" `7 M% a  h/ o

* z" n) h* Y8 `  [( q0 L然后是RAM的,main.o中的变量,放在0x20000000,总共有0x0000000c,类型是Data、RW。串口有两种变量,data和bss,什么是bss?这两个名称,是section name,也就是段的意思。看前面type和Attr,
9 h0 a) E: h' l: Y) W- P( h9 ?% m
RW Data,放在.data段;RW Zero放在.bss段,RW Zero,其实就是ZI。到底哪些变量是RW,哪些是ZI?
( X) r" `, i  J- M. b4 O& u, ]' h; f: l0 I( G, k+ m( c
12.png
0 o% H0 ?) q5 z) }- X1 ^( p7 m
7 x( b. e6 K* S$ W
Image Symbol Table( |' i7 F# X# {7 e  S

, ?' F. K9 s& ]4 g) z8 l再往上就是Image Symbol Table,就更进一步到每个函数或者变量的信息了
( F. _, Y4 `" B0 r; E" h$ B- I$ h4 E0 C1 i# }
13.png

8 F2 H: {- b: V1 e) o6 x
; O: |. [5 v7 W7 X5 Q例如,全局变量TestTmp1,是Data,4字节,分配的位置是0x20000004。
+ ]9 T3 U3 H) ]
/ y; Q' `7 o; @9 k3 z9 p$ ^
15.png
! V3 O4 Q; H% _0 K
7 K& [) ?9 Q6 @' C; [7 Y$ S
TestTmp3数组放在哪里?放在0X080024E0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几K几十K大,不可能也没必要放到RAM区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。
* M7 X7 }/ ?- o5 }$ ^; w1 W9 o; |
: b. N" f! {) G/ kconst的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习" g2 D( N! b  k; U

3 u) R% [. A8 E6 |  m
16.png

' o  J3 b2 c0 o" k- y7 y
7 ?0 E4 Q6 G5 ~
那局部变量存放在哪里呢?我们找到了test_tmp3,: y6 F9 e4 {- ^2 d) D

; r6 d6 B% u( O+ L7 g
17.png
2 N' N" ^% V, k! X
/ O- I0 d1 C- P* H. d
没找到test_tmp1/test_tmp2,为什么呢?在定义时,test_tmp3增加了static定义,意思就是静态局部变量,功能上,相当于全局变量,定义在函数内,限制了这个全局变量只能在这个函数内使用。哪test_tmp1、test_tmp2放在哪里呢? 局部变量,在编译链接时,并没有分配空间,只有在运行时,才从栈分配空间。
  1. <blockquote>u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
复制代码
5 m4 |! B+ j5 t# P- ]# |( n' N
上一部分,我们留了一个问题,哪些变量是RW,哪些是ZI?我们看看串口变量的情况,UartBuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文Block Started by Symbol的简称。
, o3 [8 z" a" m0 @/ G0 a* O( ~# @  f: d2 R( a6 ^
18.png
; x  H6 d1 m/ v& M- B

0 S( W* V! b. M4 d& i/ t1 V到这里,我们可解释下面几个概念了:& E8 e% \4 L& U$ c6 n
Code就是代码,函数。( B3 I+ u( s9 i+ P5 l* X' R2 K
RO Data,就是只读变量,例如用const修饰的数组。8 W% U5 j' ]/ |5 t3 t, u
RW Data,就是读写变量,例如全局变量跟static修饰的局部变量。1 n+ E, E0 u' I9 y
ZI Data,就是系统自动初始化为0的读写变量,大部分是数组,放在bss段。. ?0 J. _+ @# V
RO Size等于代码加只读变量。  Q) w1 M: w/ ^; y4 i3 B% M
RW Size等于读写变量(包括自动初始化为0的),这个也就是RAM的大小。
# g, l7 X& \$ y5 a  D: ZROM Size,也就是我们编译之后的目标文件大小,也就是FLASH的大小。但是?为什么会包含RW Data呢?因为所有全局变量都需要一个初始化的值(就算没有真正初始化,系统也会分配一个初始化空间),例如我们定义一个变量u8 i = 8;这样的全局变量,8,这个值,就需要保存在FALSH区。" Z& ?4 {: v9 s# v5 X
8 I  `+ s) K% R7 |
19.png
8 i1 \" ]  n: e- s; M  z

2 f, p/ h1 p% N: y我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579
+ J( B; ?' j) Z. x3 Q) P$ x: c& v# u+ L# |
20.png

) K4 P/ k, i" c* X+ @7 V5 c! _. L2 s7 W" L
main是main,放在0x080001899 X: o* s* V4 }( ?2 T
5 Q  U+ l+ Y8 N* e
21.png

' v/ m# F0 @& p& ^' O
  Y( r2 M/ h$ ^
__main到main之间发生了什么?还记得分散加载文件中的这句吗?4 D# Y- P! Y: ?4 T4 Q
*(InRoot$$Sections)
; j1 B; ]5 u8 O9 G. L3 [, K
( B1 @# E2 J9 I: W' |6 ?
__main就在这个段内。下图是__main的地址,在0x08000189。__Vectors就是中断向量,放在最开始。" B5 v" o0 M# E' |: V' K
; l( e1 ]6 ^) m  C
22.png
- P/ \1 C2 p9 [( g7 @# @/ r3 X

! R3 g4 [4 W) k( `2 ]' K* x% q
在分散加载文件中,紧跟RESET的就是*(InRoot$$Sections)。0 E/ s) u8 C( L+ J! W
, \) L& O$ F9 B) H
23.png
% s: ~0 Z0 M* K, e  h* H- B
6 N* R5 M% P& Y  r. C# q
而且,RESET段正好大小0x00000188。
; \  z, a1 t" E) u1 s# s! t! v4 x/ l, Y! y8 X; y: Z* D' R% N( k# E
24.png

  S; ]8 ^7 \  _  d" T+ C& v$ V, d: e0 q" n7 O7 W- M; B
巧合?可以参考PPT文档《ARM嵌入式软件开发.ppt》。, S  s/ z) |+ z, S! b

4 o+ N& h1 H2 I" q* K/ a5 a  G
25.png
) s' C4 E6 A* ~1 z7 T: k

% P2 N" n1 U# ?! X
这一段代码都完成什么功能呢?主要完成ZI代码的初始化,也就是将一部分RAM初始化为0。其他环境初始化……# @% A! [5 i  ~3 z1 Y

5 i7 U- j1 n8 |6 F' R' a+ R- U
3 B; k" v4 M# ^2 @4 @
最后
: @! ^0 c( ^9 e到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。
$ ^) r: F$ ~2 i3 P; A8 c: b' n3 u4 V/ r) a% }7 ~; f2 C
2 K& h9 ]& o7 ~4 H' y( k/ ]
转载自: [color=var(--weui-LINK)][url=]STM32嵌入式开发[/url]
+ I" Q6 C/ e7 [7 h: t: @, O如有侵权请联系删除6 x7 j' Y6 M- L  Z: m  @0 K0 D  G

3 T& z5 u6 W4 F% }8 ~4 U
  m4 e& A. Q: z5 i* G' F; \
收藏 评论0 发布时间:2024-7-29 15:25

举报

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