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

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

[复制链接]
攻城狮Melo 发布时间:2024-7-29 15:25
本文分析STM32单片机到底是如何软硬件结合的,分析单片机程序如何编译,运行。
6 Y) b/ ]" L+ S* s( \
1 c, D  ^0 w6 e3 }软硬件结合5 i6 X: I5 N0 `' ~/ Q3 M" n
初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写P1=0X55,就可以在IO口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。
+ R' O# C) L' E

: ~# G3 I( z+ |  h8 \8 \; }, M, f) O寻址空间$ o3 Z+ l4 j3 r7 z
什么是地址空间呢?所谓的地址空间,就是PC指针的寻址范围,因此也叫寻址空间。
2 M! @9 S2 R, |$ j0 }
1 k( z+ A. }% d: w$ Z大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,PC指针就是一个32位的二进制数,也就是0xffffffff,范围只有4G寻址空间。现在内存越来越大,4G根本不够,所以需要扩展,为了能访问超出4G范围的内存,就有了64位系统。STM32是多少位的?是32位的,因此PC指针也是32位,寻址空间也就是4G。# H4 ?, U5 R5 [4 S

7 v: V3 e9 y6 ^: X我们来看看STM32的寻址空间是怎么样的。在数据手册《STM32F407_数据手册.pdf》中有一个图,这个图,就是STM32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫Memory map,用一个新芯片,就先看这个图。
; w! T  q# _" O( B5 m! m0 v' A" H
微信图片_20240729152315.png ( u6 x3 }" o& `5 J1 B+ x" J
1 P% s% K7 b. [4 o
0 f2 j+ w. p8 A) `
最左边,8个block,每个block 512M,总共就是4G,也就是芯片的寻址空间。
* ?& z+ s+ W/ _, |" b& ^/ P8 G. M* x/ s
block 0 里面有一段叫做FLASH,也就是内部FLASH,我们的程序就是下载到这个地方,起始地址是0X800 0000,大家注意,这个只有1M空间。现在STM32已经有2M flash的芯片了,超出1M的FLASH放在哪里呢?请自行查看对应的芯片手册。+ |1 J) I! [5 M5 L2 E
5 j) B3 P# Q& P- R6 J9 d
3 在block 1 内,有两段SRAM,总共128K,这个空间,也就是我们前面说的内存,存放程序使用的变量。如果需要,也可以把程序放到SRAM中运行。407不是有196K吗?. _3 C$ |* V$ k7 n/ w/ l6 m1 C/ I% d
) a" K5 |" `2 ?/ B
其实407有196K内存,但是有64k并不是普通的SRAM,而是放在block 0 内的CCM。这两段区域不连续,而且,CCM只能内核使用,外设不能使用,例如DMA就不能用CCM内存,否则就死机。
+ e1 Z4 `; Q8 F+ V' z1 M8 S7 B: p6 ?- i9 U8 H
block 2,是Peripherals,也就是外设空间。我们看右边,主要就是APB1/APB2、AHB1/AHB2,什么东西呢?回头再说。8 U8 W1 g4 D5 s  C" {: @  D$ _* m1 S

1 Y1 Q$ ~: r( c( j$ p0 \/ N7 w# Fblock 3、block4、block5,是FSMC的空间,FSMC可以外扩SRAM,NAND FALSH,LCD等外设。
' o% Q- i$ z6 P+ ], w3 |  R. g: W% T$ `3 v% T
好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。对于这个疑惑,也可以看此文:代码是如何控制硬件的?在IO口输出的例程中,我们配置IO口是调用库函数,我们看看库函数是怎么做的。2 R4 e! ]! {* D# P
2 S+ K0 h# L# _5 \# K
例如:
! X3 u/ G+ q2 L# \# Q
  1. GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
复制代码
- H# @+ ?- _+ Z. z8 O5 B: {5 P- T
这个函数其实就是对一个变量赋值,对GPIOx这个结构体的成员BSRRL赋值。5 f6 @7 a3 K5 e3 t- E4 a9 z
  1. void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)+ }& L  X5 N% _, |
  2. {; Z1 T' @  I, p, {$ V$ n
  3. /* Check the parameters */4 Y2 B; L$ w1 ~! v. I- Z8 _$ |
  4. assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
    $ b4 F% M. |( f" A1 C/ p& |
  5. assert_param(IS_GPIO_PIN(GPIO_Pin));
    % s' C8 i8 T8 f0 f
  6. / O# _7 K, r' R- a+ z7 n' }

  7. & H# k; c) r9 e$ q" t
  8. GPIOx->BSRRL = GPIO_Pin;! @# X0 ~, |4 [& R- B: \2 g
  9. }
复制代码
  @, a" d% g8 T, q8 [0 q& l
assert_param:这个是断言,用于判断输入参数是否符合要求GPIOx是一个输入参数,是一个GPIO_TypeDef结构体指针,所以,要用->获取其成员9 O- |( j7 R. s: g# a

) I) ~4 J8 b: _7 M: j% o4 q) uGPIOx是我们传入的参数GPIOG,具体是啥?在stm32f4xx.h中有定义。
. O& Z( I( \% [& p
  1. #define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
复制代码

0 m% m5 `, M, n1 J8 NGPIOG_BASE同样在文件中有定义,如下:
/ A: u" ^. g1 ]& ~+ A& \
  1. /*!< Peripheral memory map */
    : {0 }6 v- @# ]! E
  2. #define APB1PERIPH_BASE       PERIPH_BASE% m# G% H5 U+ f. V7 |
  3. #define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)0 K- d/ t& k' G3 ^4 S; C! |
  4. #define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)
    " Y5 A7 o( e& \
  5. #define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)
复制代码

( B1 z2 ?  P/ s; ]$ u& g1 b6 n再找找PERIPH_BASE的定义
) t$ G  m/ i# j1 B, T
  1. #define PERIPH_BASE           ((uint32_t)0x40000000)
复制代码
到这里,我们可以看出,操作IO口G,其实就是操作0X40000000+0X1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是SRAM空间地址,0X40000000+0X1800是外设空间地址。# o9 [' o9 K7 l7 Q2 s
  1. u32 i;1 C* p! A4 ]. \! c
  2. i = 0x55aa55aa;
复制代码

. E( d7 X5 z' s* w这个外设空间地址的寄存器是IO口硬件的一部分。关于如下图STM32的GPIO文章推荐:STM32中GPIO工作原理详解。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0X40000000+0X1800+0x14.6 u2 g' u! w+ \
! R1 ~6 b5 o0 Y
微信图片_20240729152319.png
9 }9 ~5 P! n8 _1 W
- y  Q1 g( d7 A: T( h+ r, V- b
控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。* d1 S3 v/ I$ `* k& s
* Y# O; a6 q2 _, Z( v8 J$ O/ w
寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或RAM。寄存器为什么能控制硬件外设呢?因为,初略的说,一个寄存器的一个BIT,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。# ~1 j' [- R$ p" @3 e7 F; d

% o! x, w  t/ k: @. o0 k纯软件-包罗万象的小程序# G1 m, O' u  c7 G# t4 e
我们已经完成了串口和IO口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?关于程序是怎么在单片机运行的,也可以看此视频:动画演示单片机是如何跑程序的。代码到底放在那里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。* ^$ V, S$ c$ D9 v/ V, D4 \: Z5 X
4 R. V) U: l7 }1 l  D1 y) R  S
分析启动代码) d% ?& C9 p3 S  g. W. ~$ ^/ A; a
函数从哪里开始运行?( g9 P, l& c  V  Z, w2 b
每个芯片都有复位功能,复位后,芯片的PC指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,PC可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在STM32中,复位到0X08000004。因此复位后运行的第一条代码就是0X08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。. `1 D( @7 N! ^2 f% p) Q

* J! u( o# T7 B( u5 k1 R芯片复位时,芯片从中断表中将Reset_Handler这个值(函数指针)加载到PC指针,芯片就会执行Reset_Handler函数了。(一个函数入口就是一个指针)
  1. ; Vector Table Mapped to Address 0 at Reset
    - s6 Q" D# w4 N" A0 G
  2.                 AREA    RESET, DATA, READONLY4 f: ?2 T2 ^" R$ O; C
  3.                 EXPORT  __Vectors, p- r  z( q0 d) ?' u1 f$ B
  4.                 EXPORT  __Vectors_End
    1 U! J6 K2 R+ |2 p
  5.                 EXPORT  __Vectors_Size
    ( {8 w+ b" n" d! u0 }, Q8 D

  6. 6 q4 V6 U0 N% t6 _$ Z& }) Q4 c& y
  7. __Vectors       DCD     __initial_sp               ; Top of Stack, S* [0 e. y( W1 O( U2 e$ `
  8.                 DCD     Reset_Handler              ; Reset Handler
    # ^/ H+ O3 S! N; O5 j
  9.                 DCD     NMI_Handler                ; NMI Handler# y& X( @+ X/ i; O6 B2 H
  10.                 DCD     HardFault_Handler          ; Hard Fault Handler
    % a  e& e" v9 r
  11.                 DCD     MemManage_Handler          ; MPU Fault Handler5 O. f5 ]$ x/ Q: r; T
  12.                 DCD     BusFault_Handler           ; Bus Fault Handler5 ~7 i5 T* z" E% O
  13.                 DCD     UsageFault_Handler         ; Usage Fault Handler
复制代码
( V# g0 n2 ?4 U8 Q" b4 U( ]
Reset_Handler函数,先执行SystemInit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?
2 V  ]3 v  N% F9 r/ S
, L: F! W/ C" `5 L8 M2 H是我们在main.c中定义的main函数吗?后面我们再说这个问题。
9 [' b$ i$ W3 a/ N1 Z7 p, {6 o8 s9 b; ^0 ^4 I
微信图片_20240729152323.png
" r+ J9 k8 d! N) i

' g- F) U( T3 T" Q芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujique\prj\Objects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。  w  `  z, B* L7 K/ _

& ]4 d  _/ Z4 G& n" |在MDK软件Options菜单Linker下有关于这个菜单的设置。
9 _, r- W; z4 x" P% ]" n# D  q9 E& O) H
微信图片_20240729152326.png # B$ j5 Q8 W9 |9 G
8 [( j; U- T! Y) ^3 U( B
& a# H+ {  k% h" B$ l

/ o% k' y1 r. y. w把Use Memory Layout from Target Dialog前面的勾去掉,之前不可设置的框都可以设置了。点击Edit进行编辑。- m- c" n* x; w5 f  E0 S

8 k3 e0 K/ z- T  L, t
2.png

( |4 E+ b/ P& V; {) D, o1 s  l
' z: K, |- _* Y, z' L' A' r" L

) Q6 Q1 ^+ ~( r7 f5 h在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。9 V1 ]4 y0 |- }* d) @

( G& D& X, k' `2 K8 q/ b. q其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定FLASH跟RAM的大小于起始位置,当我们把程序分成BOOT、CORE、APP,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到RAM中运行。
5 g7 Z( X, f2 q3 w0 z8 Q2 u
' N5 _7 R; S6 ^9 h& i5 e
3.png
/ ?& i) n$ M  p( g

9 L3 b- n# U5 _9 q( w从这个基本的分散加载文件我们可以看出:
2 M5 K: X) L! ]9 V1 [3 D: j5 s5 x; }
, U- e$ ]+ M( ^第6行 ER_IROM1 0x08000000 0x00080000定义了ER_IROM1,也就是我们说的内部FLASH,从0x08000000开始,大小0x00080000。
' T4 @9 C8 W# M. _( ~! ]( @# z
& ]- Y- t6 o* v# [! j3 \$ P# c第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。8 f- i+ z2 J' H+ [) \0 U
3 B0 ^& O! \! B! N/ K
第8行 *(InRoot$$Sections)什么鬼?GOOGLE啊!回头再说。2 y. r/ [0 y+ m- ~" G) ]1 U! l/ C, U

5 k3 `" T: H! H2 G6 k  g第9行 .ANY (+RO)意思就是其他的所有RO,顺序往后放。就是说,其他代码,跟着启动代码后面。* R+ A: R$ a; u, v7 h9 d
7 D# {  C8 _* g6 R8 P! f; N
第11行 RW_IRAM1 0x20000000 0x00020000定义了RAM大小。
2 ~# _* u2 g% O
" H) `! ?6 s9 f
第12行 .ANY (+RW +ZI)所有的RW ZI,全部放到RAM里面。RW,ZI,也就是变量,这一行指定了变量保存到什么地址。2 q: k6 Q5 Q+ n; a/ v* ~
- g0 N% c2 }7 t: `6 S
分析用户代码
) S$ H6 f3 o2 l: _2 p2 T到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。
; {+ T6 i9 Q7 ^7 S& r4 c3 H6 q5 G4 |5 E3 B
1 程序跳转到main函数后:RCC_GetClocksFreq获取RCC时钟频率;SysTick_Config配置SysTick,在这里打开了SysTick中断,10毫秒一次。Delay(5);延时50毫秒。
  1. int main(void)
    5 A$ [; n; r4 i: g& \) b
  2. {
    8 P8 s3 h0 G: ]- x) {
  3.   GPIO_InitTypeDef GPIO_InitStructure;; Q0 V6 O5 k6 @/ X

  4. & }9 }2 o6 a9 ?: G$ {: S
  5. /*!< At this stage the microcontroller clock setting is already configured,
    : b% j- m8 Q' ?  S$ a; I8 c/ w% N+ P1 t
  6.        this is done through SystemInit() function which is called from startup+ [  @& L# o* t+ E) {
  7.        files before to branch to application main.
    4 N1 m* K) G7 D; k0 Y0 Y
  8.        To reconfigure the default setting of SystemInit() function,$ V$ v0 c) V8 D. r- r1 `  C
  9.        refer to system_stm32f4xx.c file */6 `  a% d+ s' R4 Q6 F5 a

  10. ( R, _! }) H! f4 [3 [
  11.   /* SysTick end of count event each 10ms */
    0 e* h$ E+ u, o0 Z* u" N: E" U
  12.   RCC_GetClocksFreq(&RCC_Clocks);
    " C6 i$ l  C/ w0 ?
  13.   SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);% a6 j! a* _1 q% Z  M7 ]

  14. * o, P5 P# B' x: U
  15.   /* Add your application code here */
    # V( d6 O( X* U, v: q9 a- \5 ?9 t7 a0 J
  16.   /* Insert 50 ms delay */5 i1 ]3 ^3 ]$ i2 x6 Z
  17.   Delay(5);
复制代码

( X& q- z! r. a2 初始化IO就不说了,进入while(1),也就是一个死循环,嵌入式程序,都是一个死循环,否则就跑飞了。
  1. /*初始化LED IO口*/
    " M5 S& e" I, c% g  M! w9 [
  2. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);# F8 w0 z8 H9 r* \, o0 h( G6 n. `
  3. 7 _0 a: G' I, B8 F0 ^9 V$ a" R
  4. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;5 I! h. f8 q3 c9 n. I
  5. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    9 i: ]7 E% O! F7 Y. V% J$ `

  6. ! Y2 g9 t8 y1 o% r
  7. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;, H! a! T, r$ C. X# t) q5 `
  8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    2 s/ ~" S! k: T! v. m" g. i
  9. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;2 S% v# ^8 s' H3 }; L' w2 k
  10. GPIO_Init(GPIOG, &GPIO_InitStructure);   
    . T, ]' M" V* w: x9 Q, G: r
  11. $ E. Z2 x1 I: l! `
  12. /* Infinite loop */# a# n1 y$ p# u, b4 O1 D- L- e3 Q* E2 O
  13. mcu_uart_open(3);
    0 T$ O3 F: z6 l: O9 @
  14. while (1)# `( \: ?3 h; \% h- i7 V" f
  15. {% A: U* Z, f+ F$ [8 G- j7 |
  16.   GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);' a) Y/ M. N2 N) Y& [1 X! n
  17.   Delay(100);# O5 F3 v# K6 V
  18.   GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
    6 E1 [0 k$ X* {7 q0 K/ }. ]
  19.   Delay(100);
    ! V0 e# M  C# N) S0 i; x( ^9 L
  20.   mcu_uart_test();
    2 I. y3 O6 S+ l$ b+ M! y

  21. # g( F; r* {0 k% U, e
  22.   TestFun(TestTmp2);3 Z* x( g* x& k( M9 v/ }
  23. }
复制代码
! _: _9 k# a+ Y1 Q, e
3 在while(1)中调用TestFun函数,这个函数使用两个全局变量,两个局部变量。
  1. /* Private functions ---------------------------------------------------------*/$ P3 |( z) W: h% U' ?: K# J
  2. u32 TestTmp1 = 5;//全局变量,初始化为5. t5 A5 H3 t. x
  3. u32 TestTmp2;//全局变量,未初始化, S2 V4 c3 x# @- |* ]/ w3 s' z9 F( \
  4. , |2 y* r1 l$ i" ?$ D2 Z7 |
  5. const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};( o9 {* Z. r7 t6 V, a

  6. 9 L( j# Z" i7 _2 G/ U/ F/ H2 u
  7. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
    / G2 }# d- u, u; t6 i
  8. {
      u$ s3 L8 y7 j
  9. u8 test_tmp1 = 4;//局部变量,初始化7 j' @' I7 D# p7 w: R* R8 l: B
  10. u8 test_tmp2;//局部变量,未初始化
    $ b# s# n! @5 _: `9 y
  11. 3 b3 M  ^& h+ {- d& j. L% N
  12. static u8 test_tmp3 = 0;//静态局部变量3 |7 @' q( G' y% R

  13. + C$ r7 c1 |4 O0 v: G9 I0 N! C$ s
  14. test_tmp3++;3 k$ ^4 I9 ^$ V0 s# u- r* i; [

  15. 8 w$ h8 h4 a% r* x2 n
  16. test_tmp2 = x;
    % p: {- U$ S5 k1 B# u
  17. . x  c2 p5 V" \. j$ ?6 V
  18. if(test_tmp2> TestTmp1)  ?' F' X( f3 h, O
  19.   test_tmp1 = 10;
    2 |, c9 {3 T; U+ Q& ~
  20. else% u9 Y4 J. L( H: q0 _, }$ a
  21.   test_tmp1 = 5;
    " o& C& ^3 v; T- [# l! J
  22. 4 w6 x- ?- w7 @. _- @" Y2 `
  23. TestTmp2 +=TestTmp3[test_tmp1];# g6 V. L7 }; o& h3 Q. a

  24. , A7 I; D8 N1 q9 f- C+ j: F4 c1 I
  25. return test_tmp1;1 Z* R6 X5 ?* B+ H4 S) K8 @! D" Z
  26. }
复制代码
3 R# ^; E! G* H" }* f! E
然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。相关文章:STM32中断系统。我们查看Delay函数,uwTimingDelay不等于0就死等?谁会将uwTimingDelay改为0?
  1. /**
    # H2 C) ]# N! U, l# J1 M
  2.   * @brief  Inserts a delay time.
    ; x1 t- ~  I0 c* U
  3.   * @param  nTime: specifies the delay time length, in milliseconds.
    6 F& [2 Z5 a% @4 Y
  4.   * @retval None7 B0 l1 k4 f, ]" D  b. S
  5.   */# C1 W5 y( E) I6 l0 d% ^+ D
  6. void Delay(__IO uint32_t nTime)* s8 R9 y) g& V- ^9 Q' K( j3 B5 |
  7. {
    5 I" f3 e) S$ C$ M8 M
  8.   uwTimingDelay = nTime;
    " f8 Z2 f1 n- g1 l

  9. ; H5 L4 X* c3 J/ |9 k; u
  10.   while(uwTimingDelay != 0);' D/ q4 l% i  _5 c* H
  11. }
复制代码

" U. p6 {( Z% Z搜索uwTimingDelay变量,函数TimingDelay_Decrement会将变量一直减到0。7 T( m" V7 o2 t1 I7 m
  1. /**, i5 P# U" t$ o0 T6 j
  2.   * @brief  Decrements the TimingDelay variable./ L/ l" |% Q8 q* R% y: p
  3.   * @param  None
    ' d2 O/ Y) X7 o) f
  4.   * @retval None0 p5 V' N# l- J, d# M# Y5 T2 Q
  5.   */; ]' }. l. [1 X9 f; |) J/ a
  6. void TimingDelay_Decrement(void)
    # `: L' |) G5 s( r
  7. {4 d- I0 m  Z% N7 N; i! d, E" o
  8.   if (uwTimingDelay != 0x00)
    , H+ N4 |8 |& r
  9.   {& V: R( @& Z0 A2 }' g- f
  10.     uwTimingDelay--;9 ]# C7 p& R5 ?! ]' b, N
  11.   }. v$ M# L+ g) u0 o4 C; Y
  12. }
复制代码

6 ]% B& h4 ~& V3 ?' `) f9 w& A; r/ @这个函数在哪里执行?经查找,在SysTick_Handler函数中运行。谁用这个函数?
  1. /**
    6 L! X2 A$ A" z+ {; f
  2.   * @brief  This function handles SysTick Handler.
    ' P" @" e+ K$ Y+ M0 a2 x; A. ?
  3.   * @param  None% g3 r/ w# ]2 r2 x
  4.   * @retval None
      i7 Q0 G# w! R; I2 }. w
  5.   */
    8 A, P3 ]/ ?. j6 I: Q! |
  6. void SysTick_Handler(void)
    8 l/ {& x6 `) n/ [, Y9 k
  7. {
      k6 d$ |7 v4 q- N
  8.   TimingDelay_Decrement();
    % N* }1 z1 I2 E, o
  9. }
复制代码

/ b5 J5 c) _/ U. l, E# _/ {经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。
  1. __Vectors       DCD     __initial_sp               ; Top of Stack  E, A1 J! o7 T" z' `
  2.                 DCD     Reset_Handler              ; Reset Handler
    4 Y+ y0 M. T/ a( ]
  3.                 DCD     NMI_Handler                ; NMI Handler
    7 Z, d+ h5 x+ S9 w
  4.                 DCD     HardFault_Handler          ; Hard Fault Handler
    ) D/ A4 U$ T9 d9 }3 W
  5.                 DCD     MemManage_Handler          ; MPU Fault Handler; _; _7 k- E/ Z4 ?
  6.                 DCD     BusFault_Handler           ; Bus Fault Handler
    - z( B9 q! V  f6 V5 e
  7.                 DCD     UsageFault_Handler         ; Usage Fault Handler; ]* k( Z9 H* g4 K6 r
  8.                 DCD     0                          ; Reserved$ g' w/ r! f$ R6 T1 P
  9.                 DCD     0                          ; Reserved
    ! d# W; L, d: n5 t# H6 C) d
  10.                 DCD     0                          ; Reserved
    5 D# ?& E; g9 o8 l' Q* \: z
  11.                 DCD     0                          ; Reserved
    1 p9 G* ~% ]; g; c! O
  12.                 DCD     SVC_Handler                ; SVCall Handler5 L/ r, T. `2 _- k0 S
  13.                 DCD     DebugMon_Handler           ; Debug Monitor Handler' V' ^; O9 b# c. }5 J" u# Q
  14.                 DCD     0                          ; Reserved
    3 w' l, Y- P; c2 Q0 _# M# ^. ^! V8 {7 H
  15.                 DCD     PendSV_Handler             ; PendSV Handler
    & J5 R" I) E4 r! Q
  16.                 DCD     SysTick_Handler            ; SysTick Handler
复制代码
$ L* y' c6 h& o% H
余下问题
4 x/ a3 V8 Q; z9 `; d  n1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(InRoot$$Sections)是什么?3 ZI段,也就是初始化为0的数据段,什么时候初始化?谁初始化?
$ t* _1 x3 l2 M) y# x6 {# @
+ t% l" Z: t* T7 ]+ c' ]
为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!6 A' r! T! m2 v
, [  V, W  B/ }, o( P
# G7 T! B1 L4 Y0 N
通过MAP文件了解代码构成
4 Y6 A8 o! z& i* V+ E/ D编译结果
, B# ^" e) b3 {2 G
程序编译后,在下方的Build Output窗口会输出信息:
  1. *** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'
    ! u1 `3 P6 r6 \0 B& e$ z
  2. Build target 'wujique'
    1 a/ a/ ^- O' }$ q1 g' z) d  \3 l
  3. compiling stm32f4xx_it.c.../ ~4 d, S7 t' ^( Z4 B
  4. ...
    & M1 c5 j3 ]; F5 ]
  5. assembling startup_stm32f40_41xxx.s...
    ( w3 _) ?1 O$ V1 k3 }: t% {. J+ x
  6. compiling misc.c...9 k+ i' h( y) U8 n) x; Y* A
  7. ...* T' g" B9 \4 J# p! I( v5 [
  8. compiling mcu_uart.c...
    8 T+ a( `# W' M
  9. linking...
    & G- _; [! }; q  F9 w
  10. Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  
    ) k+ O% b$ q3 u5 q6 `* K
  11. FromELF: creating hex file...
    + \2 {7 X0 v) [9 Y& W* P5 ?
  12. ".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s)./ H' e  [* i; e2 R0 Q; |
  13. Build Time Elapsed:  00:00:32
复制代码
# t2 s4 @8 j2 s5 y8 g( F
编译目标是wujique4 U0 J" Z2 R  q. S
C文件compiling,汇编文件assembling,这个过程叫编译. A  `3 W2 h+ r
编译结束后,就进行link,链接。
% s8 v: e. M$ _3 J% l最后得到一个编译结果,9038字节code,RO 990,RW 40,ZI 6000。CODE,是代码,很好理解,那RO、RW、ZI都是什么?2 n1 O0 D8 [0 i4 a2 D
FromELF,创建hex文件,FromELF是一个好工具,需要自己添加到option中才能用
& J) }; a& c7 B) p1 O, S
) b( O! C/ ^  o% k9 _$ R, B, b2 h
map文件配置0 ]. x$ O0 _0 I( s7 W
更多编译具体信息在map文件中,在MDK Options中我们可以看到,所有信息都放在\Listings\wujique.map" [8 }+ R" Z* x( `7 g$ r

# e6 N8 \! ?9 p0 e, L$ f默认很多编译信息可能没钩,钩上所有信息会增加编译时间。
" r' N- m7 D6 X$ ]7 @4 O+ }( ^6 F* ]" m( o; E
4.png
: L; _- a; d$ c& q( ~* S
- d8 [$ |: Y; S  ~. y
map文件
" R  o' C9 s7 n, V
打开map文件,好乱?习惯就好。我们抓重点就行了。9 X" T9 m. T" J" Q; T9 V3 m

. C- h2 B7 Q4 u1 @* G) X 5.png
" @3 l) _' N/ n, t- R

: n6 s! u3 |1 t! @: x4 \" Rmap 总信息
' M! Q# ~* e. a! U  p* K/ B
, I0 C3 Z0 m# B  j# ]9 U, L从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。& _/ q$ ?/ l1 s( [7 x
& G2 t2 y. |" [1 T8 x" U
有多少RO?RO到底是什么?
" s- w5 q" q2 M8 u3 h! q" n: }0 H3 i, h: x! ~7 @
有多少RW?RW又是什么?
8 {- n% ^+ o) E7 t9 y
& |, V/ H* A/ t/ PROM为什么不包括ZI Data?为什么包含RW Data?& Z, ~, _/ r, S- B" N" [

. Y. l+ @* G, t; B, M
6.png ; v( N: T$ I* a5 M" d
Image component sizes
9 V+ P( a. k8 z
# i# i% f) D4 N# `往上,看看Image component sizes,这个就比刚刚的总体统计更细了。% @" L5 |1 I5 Y8 T; ]
8 [6 o0 h7 e6 l3 u1 z
这部分内容,说明了每个源文件的概况
0 m  m/ L9 @( R  q4 L% h

% x4 D& G) J  ]7 e. t8 ?1 N首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些STM32的库文件。3 b& Q2 R$ \; O! N" ~4 b

9 ~" ^2 G6 U# u, s: V3 l
7.png
5 P. T3 ]$ L! V' {( ^8 G) @

5 \3 j' {1 w8 \' c2 a第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1M flash上的程序压缩到能在512K上运行。
. j7 c$ \0 F! u2 O6 m. ]3 X  {
8 D5 |4 C; X) B) b$ n1 c; y
8.png
# I6 C* f% K3 F" t% |4 Q9 _5 A6 x

. P! M$ \6 w. f第3部分也是库,暂时没去分析这两个是什么东西。7 D; S9 a- |- o! e! o
) }' m/ ~! Q' X- G" P
9.png

8 @3 A* @9 ?5 F* B7 i0 q1 B2 \7 L

* t0 W5 ?! t2 M库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:
2 f8 Z& h2 u; s/ _% x7 o#include <stdarg.h>
- ]  I1 z( R2 y#include <stdlib.h># R; H* E+ \9 l' B! h3 U1 W
#include <string.h>  ; S+ H' f( ?0 o! w
0 I. [: C% E2 j! R5 L4 F- r3 u1 Z
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。; e+ B$ m/ J2 h5 C& ~7 u
9 X( C6 F* n  Z- F! Z/ j' ?

: O2 C% G, H( g3 G# _/ r3 P文件map
& r; z3 p& @, X再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET, F% r1 S7 _- w' k6 o4 E2 a0 F

( D, w% {  [6 z6 n2 l) Y库文件是什么?0 J  |+ n( d, p) D" x3 V
库文件就是别人已经别写好的代码库。
, F! M$ b9 `5 T
  v3 h0 _. J, X在代码中,我们经常会包含一些头文件,例如:
! t6 Z, w; o& r4 B( Z4 j3 T1 j
  1. #include <stdarg.h>4 V+ t+ q- U% T, S" a+ H; I
  2. #include <stdlib.h>
    " g7 B7 `; u% B- X
  3. #include <string.h>
复制代码

/ V: T0 r/ A+ J0 H这些就是库的头文件。相关文章:C语言中的头文件。这些头文件保存在MDK开发工具的安装目录下。
  g7 \. l( h! W
  ?$ e; L7 w1 V8 y我们经常用的库函数有:memcpy、memcmp、strcmp等。  ~1 {2 E" M3 ?
4 Y: U' b1 J9 a$ u" @/ T5 a# O# X; X
只要代码中包含了这些函数,就会链接库文件。9 e" g" B5 K1 }' m+ t" _( @! F
% w& U6 |1 m" R: t% l! i
文件map
  j) a8 q+ D- @2 R) Y# F# q再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET
! A: f- g0 m( R0 R' L8 M7 h0 _( N, Y) K- {, J2 S
10.png

6 P% f+ j3 `& y' y
- O. i( s9 b0 D* _5 M. _
每个文件有有多行,例如串口,4个函数。
: i' ^1 G4 B8 `! k$ R4 A8 c
9 q9 A& Q$ J/ h
11.png

' y5 k- T7 P- p+ _! I

) f; H8 p/ W( s. _8 q% {$ m/ C然后是RAM的,main.o中的变量,放在0x20000000,总共有0x0000000c,类型是Data、RW。串口有两种变量,data和bss,什么是bss?这两个名称,是section name,也就是段的意思。看前面type和Attr,* A3 Q( t5 C) O$ C: C

1 M: h5 ?' R* T6 S( V6 J0 IRW Data,放在.data段;RW Zero放在.bss段,RW Zero,其实就是ZI。到底哪些变量是RW,哪些是ZI?. a% ]$ W0 h* ?" L6 V: {0 Y+ S
9 W% \( N) W2 r) A" A" l& n
12.png
* M" \  A+ C* d* N  ^3 @
7 H: J* S& ?7 c# i; D
Image Symbol Table
4 |& K4 N9 Q, h4 s8 E( E9 S: V$ e/ R: v! `3 O+ U* T! v+ W
再往上就是Image Symbol Table,就更进一步到每个函数或者变量的信息了" s, I3 I7 Z" ^) z6 P& V5 Z0 b
& L5 }  P: A; X2 M
13.png
* B# S2 w9 Y' ~# s% v

; Z+ y) N) c2 V- x1 t例如,全局变量TestTmp1,是Data,4字节,分配的位置是0x20000004。% F; G( b: B7 R

; g! V  q, o; K
15.png

6 h% G& ?9 S  Y/ y
9 B% e/ C4 i8 e' u" |$ H) V" B! C8 A
TestTmp3数组放在哪里?放在0X080024E0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几K几十K大,不可能也没必要放到RAM区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。
3 b( r! t# T) x: J: G& r, A1 u* V* ?
const的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习, g$ r- X5 K8 T% V& [1 f

& q0 Y) @! I" R$ Z
16.png
' \0 O/ f6 w8 n2 H
% q) K7 K( S4 F! q/ ^" J
那局部变量存放在哪里呢?我们找到了test_tmp3,
8 k6 O& B4 E, ^8 q# n$ ^2 N- p0 W# o" c& d9 @# T! t
17.png

9 U+ I: S% e+ Q! S& r; t
. U) s) F; ^6 g  Y; C: i2 [  U' H. Z
没找到test_tmp1/test_tmp2,为什么呢?在定义时,test_tmp3增加了static定义,意思就是静态局部变量,功能上,相当于全局变量,定义在函数内,限制了这个全局变量只能在这个函数内使用。哪test_tmp1、test_tmp2放在哪里呢? 局部变量,在编译链接时,并没有分配空间,只有在运行时,才从栈分配空间。
  1. <blockquote>u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
复制代码

8 I/ z& @( b' S+ d; x上一部分,我们留了一个问题,哪些变量是RW,哪些是ZI?我们看看串口变量的情况,UartBuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文Block Started by Symbol的简称。6 `# r% M5 ?$ ?; n! }6 q; k* G
6 K& ~4 m3 h1 P! v6 F/ s
18.png

, F  _1 \- j8 [# {$ k, f( Y* e1 z& z  K
到这里,我们可解释下面几个概念了:. s% o* Q) f6 b" Y
Code就是代码,函数。
- N8 X/ W7 I5 x+ Y* nRO Data,就是只读变量,例如用const修饰的数组。/ T' l0 W/ i6 P
RW Data,就是读写变量,例如全局变量跟static修饰的局部变量。3 `1 F8 \: W+ f, I  w
ZI Data,就是系统自动初始化为0的读写变量,大部分是数组,放在bss段。
( `4 s2 B4 t2 Y; n  {" `RO Size等于代码加只读变量。
4 P+ S! m7 I6 m1 tRW Size等于读写变量(包括自动初始化为0的),这个也就是RAM的大小。) x6 F& N' E% P7 g9 M
ROM Size,也就是我们编译之后的目标文件大小,也就是FLASH的大小。但是?为什么会包含RW Data呢?因为所有全局变量都需要一个初始化的值(就算没有真正初始化,系统也会分配一个初始化空间),例如我们定义一个变量u8 i = 8;这样的全局变量,8,这个值,就需要保存在FALSH区。  Q/ U' @0 _+ t! H7 c& j

0 T8 p& `0 U0 ~* V: K
19.png

+ h- S/ F; o& e3 v  d+ J2 {1 h
( t# r: I+ k& g0 O% n
我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579
! I: Y4 o5 W; R. _1 c  R9 O1 G9 y: ~; ]" h3 w
20.png
) g5 `/ s4 d1 [3 c8 k# i
, m) q0 L; _' U+ k9 ]
main是main,放在0x08000189
% l) I/ a3 [6 u/ S! \0 b
% V, u! m6 Z6 u2 m) x! \9 C
21.png

: ~, |: X0 ]- H+ i
( q, {: o9 E6 I
__main到main之间发生了什么?还记得分散加载文件中的这句吗?
% G$ r8 C8 C; ~/ s*(InRoot$$Sections)' F$ Y- j, s2 h, V; T: X' p& O
7 {# W# B3 g& a* L
__main就在这个段内。下图是__main的地址,在0x08000189。__Vectors就是中断向量,放在最开始。' D: P3 }1 C$ C' X( ?
+ v0 y. H8 T8 g8 N) V7 m8 t
22.png

* I1 c8 P- d* w6 T4 w! w' b. z3 L$ H  s8 N
在分散加载文件中,紧跟RESET的就是*(InRoot$$Sections)。
3 l% ]0 U4 [, U" n& m) r9 f) G7 A6 c( n
23.png
6 j6 c7 l% F$ h

: O; Z' v* L$ ^8 N3 \而且,RESET段正好大小0x00000188。
) {$ ?+ b* t9 u8 H$ O3 V8 e6 J* K
# L. Q2 W3 @+ J5 q( I. O& D
24.png

( q8 T0 r/ t- j/ D( r# T& c3 J1 U5 q4 Q- ^
巧合?可以参考PPT文档《ARM嵌入式软件开发.ppt》。
) x8 L% u9 B9 S  I+ W6 ~+ I' {/ v" c; X5 \0 v5 r/ h
25.png
9 ]- B" T% k' _1 O0 Z# o( k" a
: Z8 P! K9 }' x( M: x4 t; B
这一段代码都完成什么功能呢?主要完成ZI代码的初始化,也就是将一部分RAM初始化为0。其他环境初始化……
  g0 w# @* h; i3 l0 ^8 ]; c# ^. e) p: x6 \
7 m' j, o: X/ H; T' U. z
最后6 B' b- Z. U% a  J- y( \, c! S
到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。8 R' q2 ^% |2 \

( {3 R+ N& y2 [# B
* z4 K- B' P* \5 k/ @
转载自: [color=var(--weui-LINK)][url=]STM32嵌入式开发[/url]+ L2 F. l: M1 h* e# J" ^3 q( L
如有侵权请联系删除
! Y" _2 g* Y7 V& O9 A2 x
6 w1 K$ v8 S4 C4 ]% j4 U* Y
/ _) i1 u( N6 g
收藏 评论0 发布时间:2024-7-29 15:25

举报

0个回答
关于意法半导体
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
招聘信息
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
关注我们
st-img 微信公众号
st-img 手机版