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

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

[复制链接]
攻城狮Melo 发布时间:2024-7-29 15:25
本文分析STM32单片机到底是如何软硬件结合的,分析单片机程序如何编译,运行。: v9 G8 Z$ l! B# `) ]
' q! H* a8 J: d: [2 j6 F+ D! j  R
软硬件结合
4 J- Z$ a! r# T- l. z9 a, N初学者,通常有一个困惑,就是为什么软件能控制硬件?就像当年的51,为什么只要写P1=0X55,就可以在IO口输出高低电平?要理清这个问题,先要认识一个概念:地址空间。6 `. \2 H+ g1 q' g! z' |
; Q/ E# e9 `9 h/ ]# j" E
寻址空间
+ b/ D; w( {: C什么是地址空间呢?所谓的地址空间,就是PC指针的寻址范围,因此也叫寻址空间。) h: m3 d9 g, O4 e6 ~2 b# G. B0 ^
( b: `1 Q9 J+ ?+ g7 @6 _, w
大家应该都知道,我们的电脑有32位系统和64位系统之分,为什么呢?因为32位系统,PC指针就是一个32位的二进制数,也就是0xffffffff,范围只有4G寻址空间。现在内存越来越大,4G根本不够,所以需要扩展,为了能访问超出4G范围的内存,就有了64位系统。STM32是多少位的?是32位的,因此PC指针也是32位,寻址空间也就是4G。
: E/ H+ O  K! x. o5 ~9 e9 M0 y5 r: h: l3 E7 E5 U+ b  K
我们来看看STM32的寻址空间是怎么样的。在数据手册《STM32F407_数据手册.pdf》中有一个图,这个图,就是STM32的寻址空间分配。所有的芯片,都会有这个图,名字基本上都是叫Memory map,用一个新芯片,就先看这个图。
. Q9 R: \+ |2 E2 X
5 ^7 |! G! G% T% A  S, @' C
微信图片_20240729152315.png * g/ Y6 Q+ ^+ I! @9 y6 V
8 C  W, U0 ?& ^9 u) \; o1 Z
* C  o; `) I7 \' a
最左边,8个block,每个block 512M,总共就是4G,也就是芯片的寻址空间。5 o4 [- f1 w( e7 `6 {* I
0 Y* v4 r$ Y6 L& b1 K( J7 E
block 0 里面有一段叫做FLASH,也就是内部FLASH,我们的程序就是下载到这个地方,起始地址是0X800 0000,大家注意,这个只有1M空间。现在STM32已经有2M flash的芯片了,超出1M的FLASH放在哪里呢?请自行查看对应的芯片手册。, t" E9 P7 r3 x& Q! t6 D" [
0 o7 s6 |4 Q7 k8 l# d4 ]
3 在block 1 内,有两段SRAM,总共128K,这个空间,也就是我们前面说的内存,存放程序使用的变量。如果需要,也可以把程序放到SRAM中运行。407不是有196K吗?/ P& L8 y, p! W" m

% {' i8 [* R, V其实407有196K内存,但是有64k并不是普通的SRAM,而是放在block 0 内的CCM。这两段区域不连续,而且,CCM只能内核使用,外设不能使用,例如DMA就不能用CCM内存,否则就死机。/ A  t( w: T4 Q9 p( s* p3 Q

$ U) G% y4 K7 a5 Bblock 2,是Peripherals,也就是外设空间。我们看右边,主要就是APB1/APB2、AHB1/AHB2,什么东西呢?回头再说。
( d+ x/ x' p! F6 D9 f
2 M' w& t/ l% K5 D8 pblock 3、block4、block5,是FSMC的空间,FSMC可以外扩SRAM,NAND FALSH,LCD等外设。
# H( g' X  f' q# M
; C8 W" Y/ E' _4 Z- d8 {% g! U3 p好的,我们分析了寻址空间,我们回过头看看,软件是如何控制硬件的。对于这个疑惑,也可以看此文:代码是如何控制硬件的?在IO口输出的例程中,我们配置IO口是调用库函数,我们看看库函数是怎么做的。
4 L2 Z3 U0 A7 S7 W% L7 c% J9 B
4 y( @0 ^4 r8 [- r: \% F9 l! U# X. m例如:% }4 _8 C- p5 l, j& Z, g& ]
  1. GPIO_SetBits(GPIOG, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3);
复制代码

. U4 w. x8 F+ ~9 ]2 ~这个函数其实就是对一个变量赋值,对GPIOx这个结构体的成员BSRRL赋值。& y0 i' n8 I& h  G9 n8 y
  1. void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)+ x5 ?3 c+ {9 ~" q# I  _# ^
  2. {" I# `6 E* J1 l0 g) c0 z! n
  3. /* Check the parameters */
    5 V4 ]) {  y. z) G- s# V; h
  4. assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
    6 }6 H% Z' c1 h
  5. assert_param(IS_GPIO_PIN(GPIO_Pin));
    ; F% n% R' z0 ]1 \$ p3 @! P2 A6 k
  6. ) O, _4 E! c, N& ]
  7. 1 w9 f6 v6 X/ @; }. j
  8. GPIOx->BSRRL = GPIO_Pin;4 E9 _2 [- S! M! Y! C  u+ O! H; e
  9. }
复制代码

" @9 u+ E1 Q/ v5 Dassert_param:这个是断言,用于判断输入参数是否符合要求GPIOx是一个输入参数,是一个GPIO_TypeDef结构体指针,所以,要用->获取其成员
" S3 o# f  ?) X+ |: w$ k" e6 {+ j* v
, j4 t1 W5 ~6 x: J5 D! B- j8 {
GPIOx是我们传入的参数GPIOG,具体是啥?在stm32f4xx.h中有定义。
% j: v9 S: `( Y# m
  1. #define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
复制代码

1 t5 p9 P5 H& ?$ l$ lGPIOG_BASE同样在文件中有定义,如下:
* R9 E  e/ i: g! x$ S
  1. /*!< Peripheral memory map */
      ~2 A& W* q6 [" Y% o
  2. #define APB1PERIPH_BASE       PERIPH_BASE( M7 E. w, E( |* p
  3. #define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000)) ]& m1 {( M* c" y& U0 L4 @8 _
  4. #define AHB1PERIPH_BASE       (PERIPH_BASE + 0x00020000)$ f! S+ y8 R3 f8 Y% L
  5. #define AHB2PERIPH_BASE       (PERIPH_BASE + 0x10000000)
复制代码
$ q6 R, d1 B0 v- Q, M
再找找PERIPH_BASE的定义
1 u; X7 ]! A# T  o( I3 l
  1. #define PERIPH_BASE           ((uint32_t)0x40000000)
复制代码
到这里,我们可以看出,操作IO口G,其实就是操作0X40000000+0X1800这个地址上的一个结构体里面的成员。说白了,就是操作了这个地方的寄存器。实质跟我们操作普通变量一样,就像下面的两句代码,区别就是变量i是SRAM空间地址,0X40000000+0X1800是外设空间地址。
! j. P# W$ B; P, B+ G
  1. u32 i;
    : D- J  G* a1 ^( P& X
  2. i = 0x55aa55aa;
复制代码

& w" n5 a6 J7 X; K# E  r* P; N这个外设空间地址的寄存器是IO口硬件的一部分。关于如下图STM32的GPIO文章推荐:STM32中GPIO工作原理详解。如下图,左边的输出数据寄存器,就是我们操作的寄存器(内存、变量),它的地址就是0X40000000+0X1800+0x14.9 s5 d2 |. c# P* M3 j) q2 M( {

! ]8 P; b8 Q6 Q8 {9 q- v
微信图片_20240729152319.png

  Q+ a# Q6 t5 f1 E4 u

5 d) _1 r1 @- ^+ \8 V5 Q1 D  f# {+ K控制其他外设也类似,就是将数据写到外设寄存器上,跟操作内存一样,就可控制外设了。) B. E# L! q+ H

6 F' [. J' s; ~: y. X3 \寄存器,其实应该是内存的统称,外设寄存器应该叫做特殊寄存器。慢慢的,所有人都把外设的叫做寄存器,其他的统称内存或RAM。寄存器为什么能控制硬件外设呢?因为,初略的说,一个寄存器的一个BIT,就是一个开关,开就是1,关就是0。通过这个电子开关去控制电路,从而控制外设硬件。0 V( H: F6 b3 h9 b" W$ U% Y
3 i8 @5 v7 H, N
纯软件-包罗万象的小程序! s. E- F6 o* }
我们已经完成了串口和IO口的控制,但是我们仅仅知道了怎么用,对其他一无所知。程序怎么跑的?关于程序是怎么在单片机运行的,也可以看此视频:动画演示单片机是如何跑程序的。代码到底放在那里?内存又是怎么保存的?下面,我们通过一个简单的程序,学习嵌入式软件的基本要素。& _% N' Q5 E' j8 |# t9 N  k4 B
2 {) ?9 t( P: t9 m% e
分析启动代码
4 |0 ?8 h; B2 S# T0 Q9 C' j$ V# P' E/ \函数从哪里开始运行?5 O3 L$ U+ v6 b/ o, T
每个芯片都有复位功能,复位后,芯片的PC指针(一个寄存器,指示程序运行位置,对于多级流水线的芯片,PC可能跟真正执行的指令位置不一致,这里暂且认为一致)会复位到固定值,一般是0x00000000,在STM32中,复位到0X08000004。因此复位后运行的第一条代码就是0X08000004。前面我们不是拷贝了一个启动代码文件到工程吗?startup_stm32f40_41xxx.s,这个汇编文件为什么叫启动代码?因为里面的汇编程序,就是复位之后执行的程序。在文件中,有一段数据表,称为中断向量,里面保存了各个中断的执行地址。复位,也是一个中断。
* |+ o% v( ]5 q& P* W$ Y2 X" f- k, `/ u9 `- H
芯片复位时,芯片从中断表中将Reset_Handler这个值(函数指针)加载到PC指针,芯片就会执行Reset_Handler函数了。(一个函数入口就是一个指针)
  1. ; Vector Table Mapped to Address 0 at Reset' Q+ s/ O3 u) q6 d
  2.                 AREA    RESET, DATA, READONLY
    - d$ M. d: S  Y5 |8 ~+ D
  3.                 EXPORT  __Vectors
    ; N9 M) v: e. ~1 b( P
  4.                 EXPORT  __Vectors_End
    ! \6 K3 p$ ~0 v+ I2 h5 E
  5.                 EXPORT  __Vectors_Size. s6 z* w; O( x# I& T% w

  6. - _6 A" l/ j/ O7 c
  7. __Vectors       DCD     __initial_sp               ; Top of Stack: U/ n! R2 }( M+ C6 f* _" E( k9 K
  8.                 DCD     Reset_Handler              ; Reset Handler2 D5 M. y0 `7 P' g, A. P
  9.                 DCD     NMI_Handler                ; NMI Handler8 V7 b- B. L# v7 N
  10.                 DCD     HardFault_Handler          ; Hard Fault Handler
    % f) m* ]% h1 }2 O3 l5 n
  11.                 DCD     MemManage_Handler          ; MPU Fault Handler. q7 @% X: W5 ^  x+ R9 Q
  12.                 DCD     BusFault_Handler           ; Bus Fault Handler6 C9 Q6 U1 W* O$ Q
  13.                 DCD     UsageFault_Handler         ; Usage Fault Handler
复制代码

5 B% e4 ?" @% H. R9 ^Reset_Handler函数,先执行SystemInit函数,这个函数在标准库内,主要是初始芯片时钟。然后跳到__main执行,__main函数是什么函数?; g2 ^7 P) G' Q

7 B3 I3 M  w5 W$ k5 z( M是我们在main.c中定义的main函数吗?后面我们再说这个问题。
: n8 l9 W% Y5 h+ \, p/ F4 M' B7 }! r% O; s& ~3 `9 S
微信图片_20240729152323.png
1 r2 e% [- a, h, ?2 ^( Q

9 F! ]1 v6 ]: o8 j* |3 J4 X* K& e芯片是怎么知道开始就执行启动代码的呢?或者说,我们如何把这个启动代码放到复位的位置?这就牵涉到一个一般情况下不关注的文件wujique.sct,这个文件在wujique\prj\Objects目录下,通常把这个文件叫做分散加载文件,编译工具在链接时,根据这个文件放置各个代码段和变量。" p6 H  h5 B- x4 ^  k
+ F4 J- M* N" u  c
在MDK软件Options菜单Linker下有关于这个菜单的设置。
+ N& D# ^  K4 K% K5 i) B* ^! S/ n$ i% D7 R
微信图片_20240729152326.png - z0 [( L6 T  Y% P: t
, y- r$ N7 K, A+ `* \. g2 ^2 o

3 @, x& [- x( _1 l# X8 h, V

; L$ t; b2 `/ v, W把Use Memory Layout from Target Dialog前面的勾去掉,之前不可设置的框都可以设置了。点击Edit进行编辑。! i+ a/ c) ?. ^$ R# y3 G

  ]( a8 D1 {4 w& [( z
2.png
& {) q2 c" ~; e2 B
$ U' u+ l, z7 R% Q. }
( L9 ^9 V2 `% ?+ ^  R9 `
在代码编辑框出现了分散加载文件内容,当前文件只有基本的内容。. }% h: i/ I7 q3 I: W% O) c% {
) q' h/ H9 n! D) q
其实这个文件功能很强大,通过修改这个文件可以配置程序的很多功能,例如:1 指定FLASH跟RAM的大小于起始位置,当我们把程序分成BOOT、CORE、APP,甚至进行驱动分离的时候,就可以用上了。2 指定函数与变量的位置,例如把函数加载到RAM中运行。
/ R8 Q3 ^3 o/ z- z1 z
, x' C0 Q' R' U7 R( F0 v
3.png

: }/ N: S! T; E/ p5 g" y

" |+ B7 B! I  f- q& y从这个基本的分散加载文件我们可以看出:( U2 \: c% j6 e4 x+ \. d
8 X9 p% |" @- g1 g% Z- C) V
第6行 ER_IROM1 0x08000000 0x00080000定义了ER_IROM1,也就是我们说的内部FLASH,从0x08000000开始,大小0x00080000。
3 S' N- D5 H* w+ c/ s4 t: E$ ~, o
第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。
3 \+ t& ?3 \. o( b3 U" ]# X! f

& n% Y( c# K/ J! T9 S第8行 *(InRoot$$Sections)什么鬼?GOOGLE啊!回头再说。
$ n, M! i6 u; m
8 ^0 C9 s2 }7 H8 P* Z, \+ \第9行 .ANY (+RO)意思就是其他的所有RO,顺序往后放。就是说,其他代码,跟着启动代码后面。
; F6 r6 S1 W0 s6 [4 Y

1 z, X" g7 p' K第11行 RW_IRAM1 0x20000000 0x00020000定义了RAM大小。3 Y. s8 _# g8 Q7 L- Z
; G& A3 S9 v; o* ~- q* I8 i
第12行 .ANY (+RW +ZI)所有的RW ZI,全部放到RAM里面。RW,ZI,也就是变量,这一行指定了变量保存到什么地址。/ C4 Q9 D8 {$ H( x4 `( j: k

$ w% U2 f# E& ~# T* n分析用户代码
5 s2 a9 Y9 f- L到此,基本启动过程已经分析完。下一步开始分析用户代码,就从main函数开始。
7 t0 @" r4 ~5 }3 V8 k; N7 J9 G3 Z/ B6 Z# q2 s: v/ V" b$ t
1 程序跳转到main函数后:RCC_GetClocksFreq获取RCC时钟频率;SysTick_Config配置SysTick,在这里打开了SysTick中断,10毫秒一次。Delay(5);延时50毫秒。
  1. int main(void)
    5 _8 j: C+ U0 S; Q: Y. S
  2. {$ ~% p. L/ _0 J
  3.   GPIO_InitTypeDef GPIO_InitStructure;
    4 {1 u1 s# E" J# v; t

  4. + J) q0 S9 _( e0 r
  5. /*!< At this stage the microcontroller clock setting is already configured,# Z) Z5 F! y' S
  6.        this is done through SystemInit() function which is called from startup
    % T0 _/ w; [' {( |$ p
  7.        files before to branch to application main.0 ?0 d( L* W* ^5 _; x
  8.        To reconfigure the default setting of SystemInit() function,# W' o$ h  C0 c. z3 _
  9.        refer to system_stm32f4xx.c file */% e% v* h3 G9 B4 C

  10. / D# [3 V2 p6 S% T% w5 _5 i  T
  11.   /* SysTick end of count event each 10ms */
    $ V% L  V- k! d9 g9 u9 w' Y+ \
  12.   RCC_GetClocksFreq(&RCC_Clocks);
    , ]) {! D# J9 c3 D  d5 i5 f
  13.   SysTick_Config(RCC_Clocks.HCLK_Frequency / 100);1 |) O0 e( J# i' Z( h6 V
  14.   `. d$ @- B3 R8 f# `9 O' L( c
  15.   /* Add your application code here */
    & a! U2 {) p: {1 Z9 e; `0 j9 u
  16.   /* Insert 50 ms delay */
    " l& D# Y4 n8 J# ?3 I; A
  17.   Delay(5);
复制代码
, c" K$ ~" t! W9 I
2 初始化IO就不说了,进入while(1),也就是一个死循环,嵌入式程序,都是一个死循环,否则就跑飞了。
  1. /*初始化LED IO口*/! ]4 Q1 W# n# W
  2. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOG, ENABLE);  y. Q* V/ n; Q* F7 s, x' \

  3. & j% w# o, ]! \5 D: G
  4. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2| GPIO_Pin_3;/ {8 e6 M4 }& c1 J/ u2 k0 U6 E
  5. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;4 @: M9 X  P, Y
  6. ; |, [; \" U6 u
  7. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;& r1 Y* C, O+ @; S0 W
  8. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;* L. y7 [8 U4 e4 P" q8 F1 C
  9. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
      L, u( S7 v  T/ p8 K
  10. GPIO_Init(GPIOG, &GPIO_InitStructure);    & R2 m' L0 ~, o/ q4 ?3 v6 P
  11. 3 z8 z) Y2 t8 a
  12. /* Infinite loop */
    0 f, l9 t6 g+ s. q
  13. mcu_uart_open(3);* g& Q4 g& l) n( G$ a
  14. while (1)
    1 z$ j2 [3 M7 i
  15. {/ o$ [0 S7 t+ P
  16.   GPIO_ResetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
    ) l, U4 P9 ~& c+ |, q" f
  17.   Delay(100);" D& A6 B/ D( g
  18.   GPIO_SetBits(GPIOG, GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);7 Z) C6 B0 x2 s) L; k1 a& u
  19.   Delay(100);
      N$ b" s8 m& \' j4 V9 h
  20.   mcu_uart_test();
    7 s# _/ a0 H& a8 q7 s

  21. 1 @. g$ c* m, v  e
  22.   TestFun(TestTmp2);
    6 X8 w" N! A7 q0 A, X9 T
  23. }
复制代码

  U) I" h7 U3 k$ G/ K3 在while(1)中调用TestFun函数,这个函数使用两个全局变量,两个局部变量。
  1. /* Private functions ---------------------------------------------------------*/3 [0 W' |' a0 A( F* K
  2. u32 TestTmp1 = 5;//全局变量,初始化为5
    + C9 ?* e) n! D3 Y; k
  3. u32 TestTmp2;//全局变量,未初始化
    ! U4 o/ d! ?: t

  4. 4 _& d4 X: N6 A
  5. const u32 TestTmp3[10] = {6,7,8,9,10,11,12,13,12,13};) L' q( \4 v! O3 s2 F
  6. 4 ?9 F. t& B2 C
  7. u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值: x3 u! a2 h1 O: P
  8. {
    ' b4 A5 o; _  u: {: \
  9. u8 test_tmp1 = 4;//局部变量,初始化( v" G0 F) p# L8 [2 N
  10. u8 test_tmp2;//局部变量,未初始化
    % j" C$ B- N+ t# C7 o% d3 b/ k1 [

  11. 8 H# g) D8 u) F7 ?
  12. static u8 test_tmp3 = 0;//静态局部变量2 I3 @' t3 r; T% S2 f; ^
  13. 6 t$ A/ r2 F8 R4 T" C* Z  V
  14. test_tmp3++;
    8 K( L; a& i! g8 \# s6 q

  15. 6 a& M2 r: ?, R7 S5 Q
  16. test_tmp2 = x;
      D3 j' M9 H) s- |' M
  17. ( F  P* g/ `: R# q- R6 G
  18. if(test_tmp2> TestTmp1)6 T8 n; ]2 I  S  w
  19.   test_tmp1 = 10;
    9 N1 b+ T# m% _) N5 e% d" x" j9 N
  20. else( ]% y, x9 |4 J+ X
  21.   test_tmp1 = 5;
    ( d5 l4 @. y4 u% \3 X

  22. 4 M: H# `7 x7 k0 t( u& v  ~
  23. TestTmp2 +=TestTmp3[test_tmp1];- F9 m% D  T4 _9 U* q% `
  24. + a0 W# Y, j- d$ N2 q- U5 f
  25. return test_tmp1;$ R/ x- I, T2 Q0 R9 F8 s
  26. }
复制代码
* M; p' y# D5 ^. ^" j
然后程序就一直在main函数的while循环里面执行。中断呢?对,还有中断。中断中断,就是中断正常的程序执行流程。相关文章:STM32中断系统。我们查看Delay函数,uwTimingDelay不等于0就死等?谁会将uwTimingDelay改为0?
  1. /**
    ) |- C" J8 Q% ?
  2.   * @brief  Inserts a delay time.# C( d0 H8 T- f# C
  3.   * @param  nTime: specifies the delay time length, in milliseconds.
    0 b3 t' O' T- n+ ?" A
  4.   * @retval None
    3 p# {6 p& ?/ I9 f
  5.   */5 W7 }3 p% _% K1 L/ T
  6. void Delay(__IO uint32_t nTime)
    3 T; `8 f# }/ g% X( W
  7. {
    1 W: }) D3 @" M4 W# D: o
  8.   uwTimingDelay = nTime;
    3 b; C. H4 [7 b( V7 Z! |

  9. 0 Z$ l* t+ K0 U  C2 a1 x
  10.   while(uwTimingDelay != 0);
    # V2 E: |7 ?- w
  11. }
复制代码
/ u! v! ^2 }- s
搜索uwTimingDelay变量,函数TimingDelay_Decrement会将变量一直减到0。* n; v; _6 J" c1 N1 i, E9 O5 N& }
  1. /**0 x: c! s" }* d0 f
  2.   * @brief  Decrements the TimingDelay variable.% n- r+ v5 g/ C
  3.   * @param  None2 ~6 x5 P1 }* h$ B
  4.   * @retval None
    ' t0 T  |6 n8 k9 W  p% {
  5.   */; B" }5 [. H6 [- I" j
  6. void TimingDelay_Decrement(void)
    / p* O1 R0 ^: p$ H4 O" b
  7. {
    + Y. A. l: H6 o
  8.   if (uwTimingDelay != 0x00): B% I* }3 {( U* C- d* a* B
  9.   {
    # M7 p& A- A) ]. {
  10.     uwTimingDelay--;
    : a9 f1 s( x: N" b6 ?
  11.   }' V! z# ]8 D$ q4 Y) @8 p
  12. }
复制代码

4 E5 o6 t3 x1 r5 R  G这个函数在哪里执行?经查找,在SysTick_Handler函数中运行。谁用这个函数?
  1. /**
    3 o# t5 s8 U; O- [* ]
  2.   * @brief  This function handles SysTick Handler., n- d% I* {6 b
  3.   * @param  None  B$ @. D! O8 m8 d3 }5 ?/ c- l
  4.   * @retval None- ?4 h$ _* M, v; V% g6 h, N
  5.   */
    $ I1 F% p3 q+ `6 e3 a
  6. void SysTick_Handler(void)
    4 U; H7 S$ @  Q; ~
  7. {
    & M, T1 M4 Z* A. q6 T
  8.   TimingDelay_Decrement();
    5 p) M' v5 h) X  T) j: R% P
  9. }
复制代码
5 K% ?/ {! I0 c  I* D" ?$ V9 E
经查找,在中断向量表中有这个函数,也即是说这个函数指针保存在中断向量表内。当发生中断时,就会执行这个函数。当然,在进出中断会有保存和恢复现场的操作。这个主要涉及到汇编,暂时不进行分析了。有兴趣自己研究研究。通常,现在我们开发程序不用关心上下文切换了。
  1. __Vectors       DCD     __initial_sp               ; Top of Stack
    7 b6 E: L; I: s- s. A
  2.                 DCD     Reset_Handler              ; Reset Handler
    + ~: z. Z& s2 H! T7 {! s. T& Q
  3.                 DCD     NMI_Handler                ; NMI Handler  t5 Q! c) A8 s, R4 Z0 n' ^2 b
  4.                 DCD     HardFault_Handler          ; Hard Fault Handler  S5 |( s7 f- W8 i' A/ W3 i. ~# q
  5.                 DCD     MemManage_Handler          ; MPU Fault Handler
    ! V3 G& S/ P0 ^
  6.                 DCD     BusFault_Handler           ; Bus Fault Handler& T2 w" w" R1 g% {. O
  7.                 DCD     UsageFault_Handler         ; Usage Fault Handler
    6 R' o% L- Q- T8 A( ^
  8.                 DCD     0                          ; Reserved
    0 a2 E" b/ P% M! Z" o
  9.                 DCD     0                          ; Reserved& U: A% @, h6 h1 Y2 m' Z* r
  10.                 DCD     0                          ; Reserved) ?6 N" M8 F2 J% M
  11.                 DCD     0                          ; Reserved& ~8 e3 u" ]. V) [
  12.                 DCD     SVC_Handler                ; SVCall Handler
    ) D1 `- w5 Y' t% w  Q& B, i
  13.                 DCD     DebugMon_Handler           ; Debug Monitor Handler
    / ^8 w4 T+ w7 v' Y* s; t6 k8 J
  14.                 DCD     0                          ; Reserved
    2 b7 J4 b. j# l) C  t6 r
  15.                 DCD     PendSV_Handler             ; PendSV Handler
    # N. ]5 v  `: I5 k, k6 r
  16.                 DCD     SysTick_Handler            ; SysTick Handler
复制代码

2 k1 J1 l0 m7 Z6 k余下问题1 S7 d- a0 q* T: W
1 __main函数是什么函数?是我们在main.c中定义的main函数吗?2 分散加载文件中*(InRoot$$Sections)是什么?3 ZI段,也就是初始化为0的数据段,什么时候初始化?谁初始化?' H1 c( X3 W& w" P

5 x' Q. G2 M. T' [. g$ _为什么这几个问题前面留着不说?因为这是同一个问题。顺藤摸瓜!3 b2 F1 I+ l* v0 v
' J% A) \1 Q" H% ?# s, m
( C: z, |+ l: X4 C7 m, ~5 m
通过MAP文件了解代码构成
- s" |6 S( I$ C& J8 s! T% ]编译结果

, E, d) z8 K  u- k. L程序编译后,在下方的Build Output窗口会输出信息:
  1. *** Using Compiler 'V5.06 update 5 (build 528)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'4 u( I+ G- F" f1 c6 F6 @8 }% A
  2. Build target 'wujique'1 q, C9 _) o* {  |3 |% n
  3. compiling stm32f4xx_it.c...
    1 I$ y5 A  e& r/ }
  4. ...
    ( @$ Z/ Z- H' N# O+ j0 N
  5. assembling startup_stm32f40_41xxx.s...  W' V# M6 |( x# k1 j- F; W3 C7 e
  6. compiling misc.c...
    * A9 a# y4 J0 E% T7 @2 e* e5 k
  7. ...
    # [* B7 z7 ~7 X  H9 s( b2 T
  8. compiling mcu_uart.c...3 l, A# _4 H7 {" `
  9. linking...
    ( m7 v* ]  ~, C0 Z8 H
  10. Program Size: Code=9038 RO-data=990 RW-data=40 ZI-data=6000  
    . E# D# z- o2 q9 e
  11. FromELF: creating hex file...8 U3 v. ~8 i' t* y; M9 k' }
  12. ".\Objects\wujique.axf" - 0 Error(s), 0 Warning(s).! t: [( i; L/ x3 U/ z$ q- v4 s
  13. Build Time Elapsed:  00:00:32
复制代码

* @& R; g1 k( y2 |/ f编译目标是wujique
  _, A4 C- g: Z/ K  bC文件compiling,汇编文件assembling,这个过程叫编译
; @' g; o9 y- h6 Q编译结束后,就进行link,链接。" A1 P; E- o, u# i
最后得到一个编译结果,9038字节code,RO 990,RW 40,ZI 6000。CODE,是代码,很好理解,那RO、RW、ZI都是什么?0 C0 O. o- \; f0 O2 [7 h! X
FromELF,创建hex文件,FromELF是一个好工具,需要自己添加到option中才能用
0 u; g1 j; g" T" h: m+ w" o
, f% m0 B2 {4 B5 g3 B
map文件配置! e* y, O9 q4 h6 h9 }
更多编译具体信息在map文件中,在MDK Options中我们可以看到,所有信息都放在\Listings\wujique.map4 L) o' S2 I0 k8 v- A0 h

. c! h. g: ?! [4 s% u默认很多编译信息可能没钩,钩上所有信息会增加编译时间。
$ x& h: ^! M. T2 O6 o: \- a$ f9 f: ]' e, T" c8 Q
4.png
4 K* O3 f4 s' L7 F

$ p$ x+ D5 V: ^0 ~: d. @+ s$ u3 ]map文件
+ \+ q# z  y# M. N4 K
打开map文件,好乱?习惯就好。我们抓重点就行了。
9 T" ~" x; c( |" p% c! S) O; Q: c2 y; N0 \
5.png . D; U1 z; F7 o

! |3 L% a0 D8 d; \/ |map 总信息
5 i. I3 R( n- R  r6 J$ ?* r6 W. r
1 @7 u6 l. p" s从最后看起,看到没?最后的这一段map内容,说明了整个程序的基本概况。- a+ v- U, F' `7 X$ S8 f+ f
7 ?( C0 ~+ s7 w! {
有多少RO?RO到底是什么?
- \! y% ?; a: W& m: l
9 S  M7 L4 D$ E! {( _3 m6 N% F5 H有多少RW?RW又是什么?% q, D# d+ S% K3 x, b

4 K/ n0 R' S9 d4 N6 d/ C! oROM为什么不包括ZI Data?为什么包含RW Data?- B8 V$ B* j* I/ |/ c( J

8 K& {7 G3 \  F; E7 U
6.png
- J7 e! h5 `+ M/ }4 `Image component sizes3 f( {3 |) g* N1 c1 ^
( Q* A% w9 S( p/ T- O
往上,看看Image component sizes,这个就比刚刚的总体统计更细了。
; V" P8 ~* `4 L5 D, {8 l7 a* G0 \
这部分内容,说明了每个源文件的概况
% s, [& Y* q% _3 Z

- E: `9 N" C4 d0 B+ F首先,是我们自己的源码,这个程序我们的代码不多,只有main.o,wujique_log.o,和其他一些STM32的库文件。
0 F+ Q8 `7 I3 ^8 A9 d  K9 f# ?5 h5 ]/ E0 |9 r1 b# m
7.png
$ A8 e" u9 P) `: N3 |& ~
: e- n+ L2 }& D  N9 x, v% t/ y9 M
第2部分是库里面的文件,看到没?里面有一个main.o。main函数是不是我们写的main函数?明显不是,我们的main函数是放在main.o文件。这么小的一个工程,用了这么多库,你以前关注过吗?估计没有,除非你曾经将一个原本在1M flash上的程序压缩到能在512K上运行。
# B4 N6 e5 R6 ^! V9 [, A
' K3 g: ?1 K5 w
8.png

  Z' l: q: v  v. u/ G, R0 M$ _/ F

5 u+ s1 E! n  O- h3 b$ N第3部分也是库,暂时没去分析这两个是什么东西。
( f( a8 B: ?8 I# _3 e0 p* o0 h) {1 w: @4 u' O
9.png
% M6 R0 I7 Y& G$ \1 c$ V! {

0 x# K6 v: e1 G  V! {库文件是什么?库文件就是别人已经别写好的代码库。在代码中,我们经常会包含一些头文件,例如:9 \" E/ ?1 A* V1 t5 G4 S
#include <stdarg.h>
. o  H% ]! x( M# q4 m9 \8 K#include <stdlib.h>
3 J+ K8 F% z0 @; v- m" t#include <string.h>  
7 G; J& N! ]8 M* G! L3 D) w+ v. [9 Z8 e
这些就是库的头文件。这些头文件保存在MDK开发工具的安装目录下。我们经常用的库函数有:memcpy、memcmp、strcmp等。只要代码中包含了这些函数,就会链接库文件。
4 G( j/ K8 C2 \
4 W* [( a! x* X* _3 ?- F

, N" Y! t2 s5 F1 x& U; `0 L文件map
9 V9 g5 W/ p( Q9 j' M2 s再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET
8 ]" W& E( j4 f6 y( d+ a1 n4 G" d" V$ h: P+ k: c1 {& b
库文件是什么?
0 k# i7 o. o, P4 O1 X/ M( m库文件就是别人已经别写好的代码库。# d, {! F' u+ }0 d2 i7 {+ H5 l
& g4 y9 N) r5 V. o2 W9 ~: N' i
在代码中,我们经常会包含一些头文件,例如:3 E& y/ S5 {) l; G
  1. #include <stdarg.h>
    ) g# q: }0 }0 z- `
  2. #include <stdlib.h>9 i$ D' R: Y; `/ N& i4 k3 Y
  3. #include <string.h>
复制代码
. f# V3 F( ~: S- @- {: N
这些就是库的头文件。相关文章:C语言中的头文件。这些头文件保存在MDK开发工具的安装目录下。
7 P( t, ^/ g/ [8 b& [
; y0 o$ P, {; \, ]我们经常用的库函数有:memcpy、memcmp、strcmp等。
; L& d. x3 [, r
# \8 P& c) x3 |* H' m6 g只要代码中包含了这些函数,就会链接库文件。
1 O; [7 {9 Q7 z* [# {0 o
. O6 @  D- k, d( B# B% Z* _文件map
! \  q7 e4 G- N( S9 m3 ?再往上,就是文件MAP了,也就时每个文件中的代码段(函数)跟变量在ROM跟RAM中的位置。首先是ROM在0x08000000确实放的是startup_stm32f40_41xxx.o中的RESET" e' c& `, M* e" v. T) V

' X2 K6 b; O5 D) O" Q
10.png

$ _9 s& o1 E9 k; m
8 y7 C; c6 U* P4 L% a# x
每个文件有有多行,例如串口,4个函数。3 |4 T$ V% Z  s* \

$ @7 M5 F) t! H2 s+ S9 G
11.png
. p9 J- S4 N0 L) J

  L- |$ E* m( f. Q9 C+ s然后是RAM的,main.o中的变量,放在0x20000000,总共有0x0000000c,类型是Data、RW。串口有两种变量,data和bss,什么是bss?这两个名称,是section name,也就是段的意思。看前面type和Attr,+ ^  {" m+ Z% V% k8 D

1 K+ j6 X7 C. t3 j0 @" Q4 _. WRW Data,放在.data段;RW Zero放在.bss段,RW Zero,其实就是ZI。到底哪些变量是RW,哪些是ZI?7 u/ b8 U' x3 w7 Q& s
$ S2 R4 g8 G1 e
12.png

' i" W; Q$ B% `8 a9 T/ D: G

: n: i6 `+ A3 e7 ?, ^Image Symbol Table- F% V  a& E+ a! N4 i1 C; i
2 Z4 a. ^1 B8 z& n: [! t
再往上就是Image Symbol Table,就更进一步到每个函数或者变量的信息了5 J; s/ N# r" r4 j5 C/ A' w

" o  W3 S7 x! y. I' L2 V
13.png

/ g4 Q) n7 C& y* \$ B" f8 r( v' V" w' C) t2 \5 j' b8 }8 a+ g
例如,全局变量TestTmp1,是Data,4字节,分配的位置是0x20000004。
( X, \! n" e  s5 F- x7 _" l$ F/ o- ?* i: t) o" a, R
15.png
' o! N! e0 C& W, O
& b1 R3 n& U8 U6 O
TestTmp3数组放在哪里?放在0X080024E0这个地方,这可是代码区额。因为我们用const修饰了这个全局变量数组,告诉编译器,这个数组是不可以改变的,编译器就将这个数组保存到代码中了。程序中我们经常会使用一些大数组数据,例如字符点阵,通常有几K几十K大,不可能也没必要放到RAM区,整个程序运行过程这些数据都不改变,因此通过const修饰,将其存放到代码区。
( e% Y- Q. b) P. R
7 ~9 u7 _2 T0 j* e- l4 dconst的用处比较多,可以修饰变量,也可以修饰函数。更多用法自行学习
% a* d9 O( e2 m4 I, M3 j* `) X/ c1 A1 f  J! R+ B) U! L
16.png
2 r7 I% x* L/ V% b  C: J, a" p

8 C1 {6 q+ w# O7 S) K
那局部变量存放在哪里呢?我们找到了test_tmp3,, a/ }' v9 g( H, X

% ]6 _- ~- Q( z( ?  u
17.png
3 G: O- R8 J. Q/ u* t: |/ P/ i8 J/ U

1 A1 y  ^& S" X0 m6 J. D' T没找到test_tmp1/test_tmp2,为什么呢?在定义时,test_tmp3增加了static定义,意思就是静态局部变量,功能上,相当于全局变量,定义在函数内,限制了这个全局变量只能在这个函数内使用。哪test_tmp1、test_tmp2放在哪里呢? 局部变量,在编译链接时,并没有分配空间,只有在运行时,才从栈分配空间。
  1. <blockquote>u8 TestFun(u32 x)//函数,带一个参数,并返回一个u8值
复制代码
( e# f* P( Q! p% R- X
上一部分,我们留了一个问题,哪些变量是RW,哪些是ZI?我们看看串口变量的情况,UartBuf3放在bss段,其他变量放在.data段。为什么数组就放在bss?bss是英文Block Started by Symbol的简称。
9 L$ I) a5 ]0 F
2 u; |# }) P, Q) Y
18.png

9 d: V9 T% N2 _( Q  X8 V
. i$ \- ]. ~$ H1 E到这里,我们可解释下面几个概念了:" n/ j, W, O6 l6 @
Code就是代码,函数。+ {/ L, s; `8 A8 T5 E; t9 m
RO Data,就是只读变量,例如用const修饰的数组。! ~, b3 }$ C, i3 S
RW Data,就是读写变量,例如全局变量跟static修饰的局部变量。
: }, {% F& h4 T! z9 d/ IZI Data,就是系统自动初始化为0的读写变量,大部分是数组,放在bss段。) X- r# k0 Y' D! e& d  z' R
RO Size等于代码加只读变量。
3 }* ]( E2 N# \: URW Size等于读写变量(包括自动初始化为0的),这个也就是RAM的大小。1 p# |% Y  I. c; D6 r
ROM Size,也就是我们编译之后的目标文件大小,也就是FLASH的大小。但是?为什么会包含RW Data呢?因为所有全局变量都需要一个初始化的值(就算没有真正初始化,系统也会分配一个初始化空间),例如我们定义一个变量u8 i = 8;这样的全局变量,8,这个值,就需要保存在FALSH区。, k9 Y% Q7 p  P9 V

2 R! T' j! x0 E: `6 e
19.png
& I' J0 T( ^) l- {% W; b& e+ _
" P7 ?; q" g4 u* ^( p: p
我们看看函数的情况,前面我们不是有一个问题吗?__main和main是一个函数吗?查找main后发现,main是main,放在0x08000579
( w: K8 f4 l9 M  |/ U
+ I6 @; T0 C$ E. z) m3 m0 M
20.png
6 e! z/ m% ]9 x* Z7 R
& e2 \1 ]9 f9 h' L* s
main是main,放在0x08000189
$ b, {( |6 t3 q9 i4 x; w% L  }% e3 z( v) _9 N8 L. M
21.png

4 O! s1 J7 u( ^% M+ d
) p8 N* K7 z2 R% f: D3 {  l
__main到main之间发生了什么?还记得分散加载文件中的这句吗?
& L0 T9 E! h# x3 a$ J; p+ p3 y" q*(InRoot$$Sections)% b1 F( _) c6 w5 Q
4 ^! S0 U# P6 m
__main就在这个段内。下图是__main的地址,在0x08000189。__Vectors就是中断向量,放在最开始。
# C- M0 ], j4 L% \! J
/ Q1 z2 Q9 k) a  b4 g0 i, b
22.png
+ J: B7 C5 p" l1 r$ V

# Z6 \! q2 s2 u& H+ r( I& \: H& E+ e
在分散加载文件中,紧跟RESET的就是*(InRoot$$Sections)。
6 R0 I1 q8 q9 h" Q/ i
/ S* n1 |! z+ B$ N
23.png

8 a8 x" }( C0 O9 l+ q
0 i/ C/ [( l0 Z; R. `
而且,RESET段正好大小0x00000188。4 k/ U) Y# R- S( s2 U3 g
1 K# J7 d7 M" p2 b( ~( V
24.png

" u; }; E* f/ L  n/ b
" ]* j9 P/ z5 i5 u6 y6 R3 ~
巧合?可以参考PPT文档《ARM嵌入式软件开发.ppt》。
$ v2 o) k3 U) U8 X( {$ a: X
+ \) t- Y3 l# J) c8 f* ]. x
25.png

" J9 u8 y4 O# Q5 @' A5 w( |; L5 `! Y6 s
这一段代码都完成什么功能呢?主要完成ZI代码的初始化,也就是将一部分RAM初始化为0。其他环境初始化……
: _6 G) }# n. V- U- _
3 j0 S, A6 U# L+ {: [! `
1 T6 |! L+ }7 I/ C1 e
最后
. B- J0 M  q5 t: n- K9 \- }到这里,一个程序,是怎么组成的,程序是如何运行的,基本有一个总体印象了。- z3 j) m+ L* V- A. r( S7 j# T

* F0 \: C' T1 t/ C$ m: u
. @) A6 t. o% }4 n" w2 h
转载自: [color=var(--weui-LINK)][url=]STM32嵌入式开发[/url]
, x/ m* ~& ^9 S9 l2 `6 ~7 r- ]如有侵权请联系删除  G' P8 T8 b9 d0 j+ a  ^. w

1 ?% i6 P3 o6 C% b. T+ _( X$ v7 J8 Z1 }. w4 f+ b, }9 m$ }
收藏 评论0 发布时间:2024-7-29 15:25

举报

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