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

STM32MP1 M4裸机CubeIDE开发指南——STM32基础知识入门

[复制链接]
STMCU小助手 发布时间:2022-9-24 22:33
STM32基础知识入门5 m0 D' I( o1 w4 W7 k
本章,我们着重介绍STM32的一些基础知识,让大家对STM32开发有一个初步的了解,为后面STM32的学习做铺垫,方便后面的学习。本章内容大家第一次看的时候可以只了解一个大概,后面需要用到这方面的知识的时候再回过头来仔细看看。7 l' O- `8 w3 a" L. g
本章将分为如下几个小节:
* E/ U( }" c% `4 p+ d: A! [0 g: [$ T, u7 d1 T: ?
5.1 C语言基础复习9 q( v! z: {9 m" Q3 g. N  f. Z
本小节我们讲解C语言的基础知识。C 语言知识博大精深,也不是我们三言两语能讲解清楚的,我们这里就列举部分STM32学习中会遇见的C 语言基础知识点,引导那些 C 语言基础知识不是很扎实的用户能够快速开发 STM32 程序,同时希望这些用户能够多去复习一下 C 语言基础知识,毕竟C 语言是单片机开发中的必备基础知识。对于 C 语言基础比较扎实的用户,这部分知识可以忽略不看。
) H& `- Z6 @- c5.1.1 位操作
! d* s* l; G% L+ B5 w& YC语言位操作相信学过C语言的人都不陌生了,简而言之,就是对基本类型变量可以在位级别进行操作。这节的内容很多朋友都应该很熟练了,我这里也就点到为止,不深入探讨。下面我们先讲解几种位操作符,然后讲解位操作使用技巧。C语言支持如下6中位操作:
# }8 [. Y+ @# d& n, j. d' }( t+ R, t( i( s
6e52864db0ad423a836079168f388536.png
4 H2 N0 t+ t. L2 h7 q4 j- _& s( K( d1 q* \
表5.1.1.1 六种位操作
1 B3 Y$ k" {3 A" Z4 b" W) Y这些与或非,取反,异或,右移,左移这些到底怎么回事,这里我们就不多做详细,相信大家学C语言的时候都学习过了。如果不懂的话,可以百度一下,非常多的知识讲解这些操作符。下面我们想着重讲解位操作在单片机开发中的一些实用技巧。
1 W4 H4 B- V# s0 E/ z1,在不改变其他位的值的状况下,对某几个位进行设值。& }) Z4 B  a) e) l
这个场景在单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作,然后用|操作符设值。比如我要改变GPIOA的状态,可以先对寄存器的值进行&清零操作:1 z+ p5 Q7 y) V. Q- |
GPIOA->CRL &= 0XFFFFFF0F; /* 将第4~7位清0 /9 r) _' T  Y/ }7 @! u1 ^! ~$ U
然后再与需要设置的值进行|或运算:4 g: N1 g. `  f3 |  y; p3 S8 `* g
GPIOA->CRL |= 0X00000040; / 设置相应位的值(4),不改变其他位的值 /& z) A8 o$ l; e% U
2,移位操作提高代码的可读性。
1 K: {1 \9 w: q  s6 {4 |移位操作在单片机开发中非常重要,下面是delay_init函数的一行代码:
, T/ b* z2 D  T5 u  nSysTick->CTRL |= 1 << 1;
* C3 B1 Q7 D9 E3 c这个操作就是将CTRL寄存器的第1位(从0开始算起)设置为1,为什么要通过左移而不是直接设置一个固定的值呢?其实这是为了提高代码的可读性以及可重用性。这行代码可以很直观明了的知道,是将第1位设置为1。如果写成:. Z: |1 w; i" G& Q( R1 u5 g! h
SysTick->CTRL |= 0X0002;: G# M, h+ k/ y+ ?* B5 C# x. y1 B
这个虽然也能实现同样的效果,但是可读性稍差,而且修改也比较麻烦。
; f$ {9 \- C0 X6 N0 u1 d& \3,~按位取反操作使用技巧1 T$ C5 G8 J0 u$ h- B4 I
按位取反在设置寄存器的时候经常被使用,常用于清除某一个/某几个位。下面是delay_us函数的一行代码:+ p" z$ c: b8 p
SysTick->CTRL &= ~(1 << 0) ; / 关闭SYSTICK /
* @  X5 ^( l+ I7 `" x+ e: j& U该代码可以解读为 仅设置CTRL寄存器的第0位(最低位)为0,其他位的值保持不变。同样我们也不使用按位取反,将代码写成:6 G) S+ R4 u; j! P( d+ {) }& m
SysTick->CTRL &= 0XFFFFFFFE; / 关闭SYSTICK */
  D% ?" ~  y; x6 L- |# ?! I0 N可见前者的可读性,及可维护性都要比后者好很多。
7 L" k- y2 M# Y3 z# G8 x4,^按位异或操作使用技巧9 d  o0 C* r+ M" p
该功能非常适合用于控制某个位翻转,常见的应用场景就是控制LED闪烁,如:2 q) P/ d# h; z
GPIOB->ODR ^= 1 << 5;
  m9 D3 g) a6 F执行一次该代码,就会使PB5的输出状态翻转一次,如果我们的LED接在PB5上,就可以看到LED闪烁了。
! b/ V( F8 @: h9 ?: \5.1.2 define宏定义
, X8 R/ q8 w$ T) L% ldefine是C语言中的预处理命令,它用于宏定义(定义的是常量),可以提高源代码的可读性,为编程提供方便。常见的格式:
6 b: z  k7 [# r( Y#define 标识符 字符串
2 v/ l) D+ y1 D  B“标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。例如:4 ]7 Y5 o( m: V- s( i
#define HSE_VALUE 8000000U
, g) n% C! {0 s& j定义标识符HSE_VALUE的值为8000000,数字后的U表示unsigned的意思。
- h4 P% i0 h2 V# |6 i- ]至于define宏定义的其他一些知识,比如宏定义带参数这里我们就不多讲解。
3 |" d# V8 n% Q- `/ Q+ J5.1.3 ifdef条件编译
3 C" _2 Z6 |: |8 p) e0 Y% ~. |单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。条件编译命令最常见的形式为:
  H3 `( g+ U  f5 ?7 r3 g, ?7 }#ifdef 标识符
: j# s! l/ d7 w3 G1 `/ D5 i! w" ^$ k程序段1
9 Y/ L8 l3 q/ F7 `# E, L" n#else
  g+ f/ h# E0 j3 i程序段2
9 a' j5 t) v, H: Z7 T6 T7 v#endif# ]4 A% L: k. Y
它的作用是:当标识符已经被定义过(一般是用#define命令定义),则对程序段1进行编译,否则编译程序段2。 其中#else部分也可以没有,即:2 n/ S% w8 Y! Y' v: \9 T. l: Z
#ifdef; e6 |. s% I- s2 T
程序段1" J: a/ I' ]6 I; x+ p
#endif+ K7 n/ D8 j9 X- }) m3 P$ @$ _
条件编译在HAL库里面是用得很多,在stm32mp1xx_hal_conf.h这个头文件中经常会看到这样的语句:
& R* H% ^8 r- k2 |* I4 S4 _#if !defined (HSE_VALUE)
" q# J& S9 R: Q0 d#define HSE_VALUE 24000000U7 Q( V' W% e, S
#endif/ O/ {. e& v$ A4 B/ ~9 K9 H
如果没有定义HSE_VALUE这个宏,则定义HSE_VALUE宏,并且HSE_VALUE的值为24000000U。条件编译也是C语言的基础知识,这里也就点到为止吧。+ o# \0 M8 R1 r% [7 r! O
这里提一下,24000000U中的U表示无符号整型,常见的,UL表示无符号长整型,F表示浮点型。这里加了U以后,系统编译时就不进行类型检查,直接以U的形式把值赋给某个对应的内存,如果超出定义变量的范围,则截取。- y! \! C, a+ w3 l% y9 ?5 B3 Q
5.1.4 extern变量申明5 |' ~# @6 w; m1 k5 ?
C语言中extern可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。这里面要注意,对于extern申明变量可以多次,但定义只有一次。在我们的代码中你会看到看到这样的语句:
. @+ U( f5 B, o( `: ~extern uint16_t g_usart_rx_sta;* q6 ~  x) b* ]. g, m; J9 W+ x
这个语句是申明g_usart_rx_sta变量在其他文件中已经定义了,在这里要使用到。所以,你肯定可以找到在某个地方有变量定义的语句:7 `1 |( e; J/ O+ D
uint16_t g_usart_rx_sta;( m7 C: f' T- H6 _- v
extern的使用比较简单,但是也会经常用到,需要掌握。% y+ _0 [- e0 C: e( |3 z3 I
5.1.5 typedef类型别名
; h2 J1 R+ K" W5 n% ~4 r. n% _' l- ctypedef用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。typedef在HAL库用得最多的就是定义结构体的类型别名和枚举类型了。* m4 t1 T' e; x$ S- ?# q

, K! |" I- u) P" qstruct _GPIO4 d0 ~9 ?* S- ?4 V
{
; o" M9 g  }7 }9 I  `__IO uint32_t CRL;3 G: s" I1 J1 R  Z2 ~5 R
__IO uint32_t CRH;2 W; G) K: w$ a2 K+ b: w' E0 r" J
; q6 x: _( O$ k( [9 K+ T
};
( E% b8 S$ ~4 v
/ S! k$ x- j$ q( b定义了一个结构体GPIO,这样我们定义结构体变量的方式为:/ I1 Y1 A0 M2 U
struct _GPIO gpiox; /* 定义结构体变量gpiox */- P0 R: {' K2 d0 l, O2 v
但是这样很繁琐,HAL库中有很多这样的结构体变量需要定义。这里我们可以为结体定义一个别名GPIO_TypeDef,这样我们就可以在其他地方通过别名GPIO_TypeDef来定义结构体变量了,方法如下:
+ i8 R  v# I: Y
7 n# F3 b! A  o* `2 j# W+ k0 [typedef struct
" ~4 Y( w8 L+ V! a{
4 n, ]8 x( ]$ O' g+ }7 U0 y+ M3 s! ^__IO uint32_t CRL;
8 z1 Q* x3 Z4 X: m__IO uint32_t CRH;/ d3 i4 s3 m# o+ b' ~, X
# J1 \8 X. G, ?0 t
} GPIO_TypeDef;
) W7 G0 J3 F7 v0 j8 x+ ^) e3 P% P" a
Typedef为结构体定义一个别名GPIO_TypeDef,这样我们可以通过GPIO_TypeDef来定义结构体变量:4 |6 l+ a+ p4 g5 z
GPIO_TypeDef gpiox;
8 [0 f$ M" N* ]1 z) E这里的GPIO_TypeDef就跟struct _GPIO是等同的作用了,但是GPIO_TypeDef使用起来方便很多。
# L% `9 m  s4 d* i/ d5.1.6 结构体
2 n) K$ g4 D/ `4 u+ D% |1 X7 i1 [( z$ J
结构体的声明和定义' X; r/ `5 Q& {( Y% V! t
经常很多用户提到,他们对结构体使用不是很熟悉,但是HAL库中太多地方使用结构体以及结构体指针,这让他们一下子摸不着头脑,学习STM32的积极性大大降低,其实结构体并不是那么复杂,这里我们稍微提一下结构体的一些知识。
* l! J* A5 \6 @声明结构体类型:
* {) W/ o: E" j: @
  1.     struct 结构体名0 m" ?8 n! Q. {! J! }- Q: l+ p
  2.     {
    : J/ H$ P5 F+ }0 A
  3.         成员列表;
    3 ]7 {/ y5 H" S' E
  4.     }变量名列表;" I; d% H7 B  y7 ]) Q+ x+ P" n
  5. 例如:
    . Y/ G" S! {7 T# @: R
  6.     struct U_TYPE
    4 }4 X, Z& J' B& ]3 L8 ?, [
  7.     {9 R, U0 u# C" P' O4 M( c+ m
  8.         int BaudRate 3 V: y0 q5 S8 p! p7 N
  9.         int WordLength;
    & O. l! X: {$ F/ t. l7 T
  10.     }usart1, usart2;
复制代码
% b  m7 M% L, O& C6 O% w. ?2 N
在结构体申明的时候可以定义变量,也可以申明之后定义,方法是:6 l+ q# l7 t; V; Y
struct 结构体名字 结构体变量列表 ;) I3 d7 Y& A+ X: g$ X" J  \  i1 [% G
例如:
( G# A8 F* b" Q/ G* ^6 g; \3 E6 Ystruct U_TYPE usart1,usart2;% Z. S4 |4 v; j+ o4 e' @
2. 引用结构体成员变量% n) D! }" j) A) a5 l8 E) K; t  Q
结构体成员变量的引用方法是:  T6 Q2 y4 O9 t5 ]" D/ A
结构体变量名字.成员名
9 K- A& E" N, I" {/ [2 ?比如要引用usart1的成员BaudRate,方法是:usart1.BaudRate; 结构体指针变量定义也是一样的,跟其他变量没有啥区别。9 S  ?# x+ W& Q! a3 w& B
例如:
) [" m' D% w2 q5 Zstruct U_TYPE usart3; / 定义结构体指针变量usart3 */: T9 x8 t- u" s' Y
结构体指针成员变量引用方法是通过“->”符号实现,比如要访问usart3结构体指针指向的结构体的成员变量BaudRate,方法是:2 A1 H) n6 m/ c- \( m* y1 |9 {
usart3->BaudRate;, u4 `3 b- W# ~) Y  s
3. 结构体的作用1 O- l2 |4 q  @) ^" n$ r* @
上面讲解了结构体和结构体指针的一些知识,其他的什么初始化这里就不多讲解了。讲到这里,有人会问,结构体到底有什么作用呢?为什么要使用结构体呢?下面我们将简单的通过一个实例回答一下这个问题。6 `4 l/ k, S1 F0 V" [6 u' _% @% k5 x
在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如串口,它的初始化状态是由几个属性来决定的,比如串口号,波特率,极性,以及模式。对于这种情况,在我们没有学习结构体的时候,我们一般的方法是:( T* r) Z1 e2 @, Y
void usart_init(uint8_t usartx, uiut32_t BaudRate, uint32_t Parity,
$ R/ s% H4 u; Muint32_t Mode);$ p; D) J' K5 `$ R( Q5 X
这种方式是有效的同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函数里面再传入一个/几个参数,那么势必我们需要修改这个函数的定义,重新加入新的入口参数,随着开发不断的增多,那么是不是我们就要不断的修改函数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢?
  S/ I# }% z+ j7 k我们使用结构体参数,就可以在不改变入口参数的情况下,只需要改变结构体的成员变量,就可以达到改变入口参数的目的。, U6 L) b9 F) r% a+ l3 l/ n1 [8 W
结构体就是将多个变量组合为一个有机的整体,上面的函数,usartx,BaudRate,
! J% K* E5 ~$ {9 @7 {* z7 `Parity, Mode等这些参数,他们对于串口而言,是一个有机整体,都是来设置串口参数的,所以我们可以将他们通过定义一个结构体来组合在一个。HAL库中是这样定义的:! r6 b4 i# ^8 r
' p/ h: u# Q3 |( b
typedef struct" u) X  B3 ~, t( A3 Y( f- e
{
  y) {; j1 ?6 T) ]' [uint32_t BaudRate;
% U$ e0 R; p4 }uint32_t WordLength;
& [  t( T$ U7 k- T8 luint32_t StopBits;
* u: |' o; S* a8 a7 x8 W3 d- huint32_t Parity;
' ]+ ^* F4 }, L  Q) G. J+ f# cuint32_t Mode;5 l0 C! O! p3 M& S# Q; g
uint32_t HwFlowCtl;% K" S2 V3 x- i& h- ^1 h0 m
uint32_t OverSampling;
" {# `5 e- V& h} UART_InitTypeDef;; I9 n( q" f. }

9 Y" U, `0 K6 u  Z- Y* l这样,我们在初始化串口的时候入口参数就可以是USART_InitTypeDef类型的变量或者指针变量了,于是我们可以改为:
* ?) `7 O: Z, H" O3 I! j4 xvoid usart_init(UART_InitTypeDef *huart);
1 J3 v9 K6 a0 X7 i这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义就可以达到增加变量的目的。
% S3 X8 [0 R: T/ O8 R% p' U: c- Y理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多,如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可以提高你的代码的可读性。
# V5 x/ |' P9 r使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作用就远远不止这个了,同时,HAL库中用结构体来定义外设也不仅仅只是这个作用,这里我们只是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲解结构体的一些其他知识。+ Y1 [) |2 g/ H5 N% p
4. 结构体成员的内存分布与对齐
6 y- K& w$ {' v1 D0 G首先我们明白下面几点:) w' ^( Q! O0 Q( U
(1)声明一个结构体类型的时候是没有为它分配任何存储空间的,只有在定义结构体变量的时候,才会为变量分配存储空间。
7 N+ E/ t0 n0 l(2)结构体中可以有不同的数据类型成员,成员在定义时依次存储在内存连续的空间中,结构体变量的首地址就是第一个成员的地址,内存偏移量就是各个成员相对于第一个成员地址的差(即,把低位内存分配给最先定义的变量)。
7 ?: y# \' s+ ?5 u(3)理论上,结构体所占用的存储空间是各个成员变量所占的存储空间之和,但是为了提高CPU的访问效率,采用了内存对齐方式:2 ?! H5 w( e" o: D7 N1 O4 |( H% I
①结构体的每一个成员起始地址必须是自身类型大小的整数倍,若不足,则不足部分用数据填充至所占内存的整数倍。8 D/ e! [, s# t) N
②结构体大小必须是结构体占用最大字节数成员的整数倍,这样在处理数组时可以保证每一项都边界对齐$ N! p* K! N' C- M2 N- b
根据上面的说明,我们举例子分析如下:
/ O( Q0 s' b! A: P5 y; B* E
/ U/ K* [9 T6 Z( z+ P) w$ _& p% Ustruct test
1 }; _7 p! _9 T7 |* j& H{
1 M+ a& `5 v6 \5 f3 U- Pchar a;
1 f  N7 K% Y' @  r4 t$ tint b;
2 ^% ]! P6 S( h" }# Ofloat c;( h: G, S: |5 g: g  W2 G' q
double d;! {: l8 h1 _4 M% f5 g
}mytest;
4 I2 G9 ~7 W' }: d) j
" ?- b% k2 R) r+ Q2 z3 }这个结构体所占用的内存怎么算呢?理论结果为17,实际上并不是17,而是24。为什么会这样呢?这个就是前面我们说的内存对齐。& z$ `) g3 i# n# ^0 w9 V0 S, v
char型变量占1个字节,所以它的起始地址是0。int类型占用4个字节,它的起始地址要求是4的整数倍数,那么内存地址1、2、3就需要被填充(被填充的内存不适于变量),b从4开始。float类型也是占用4个字节,起始地址要求是4的倍数,所以c的起始地址就是8。double类型变量占用8个字节,起始地址为16,12~15被填充。这里,第一个成员a的地址首地止,第二个成员b的偏移量为4,第三个成员c的偏移量是8,以此类推,是如下图所示:
) K/ V* G  b& }
, O' n1 R0 L* K dc6600b08fad4ebea4068446402a2b7e.png
& X) z! ?2 G6 w# Q, _
9 N2 N/ H. H& i8 Y- ?* S图5.1.6. 1结构地地址内存分配
! V; S# b% g* K1 n# S1 X& J上面的结构体成员a、b、c、d的类型各不相同,但在HAL库中,有很多的结构体,结构体中的成员类型基本上是一样的,例如GPIO的结构体,都是定义为uint32_t,即32位,每个成员占用4个字节,以第一个成员MODER为首地止0,第二个成员OTYPER相对于MODER偏移为4个字节,依次类推。关于HAL库,我们后面会分析。( h$ j- C7 t4 e/ G* q# @
- r% r# B. M% f  m1 R
typedef struct+ K1 G  K, r" f! J) `, s3 I
{
" J8 R& _7 a7 J  H__IO uint32_t MODER;
; `. D( u  n8 z: r, T: |$ {__IO uint32_t OTYPER;8 T4 w# H) j( I' L* E% c2 q0 I
__IO uint32_t OSPEEDR;2 o  f5 f* e- R$ T9 g
__IO uint32_t PUPDR;
% r! d9 r; T6 k/******此处省略部分代码 */
; B  ?1 c* _( l__IO uint32_t VERR;
5 j5 q; ~) A$ u$ g/ R+ u- i__IO uint32_t IPIDR;
  E$ W+ X8 L9 K! p8 {& b. R__IO uint32_t SIDR;; x) Q9 T3 o& K! I7 h- W9 k2 j
} GPIO_TypeDef;
# p+ ?8 U7 n; w: C1 E3 Z+ O, F- N# T4 D+ f5 k9 Q6 F
5.1.7 关键字- I; B8 V2 X; O$ X& b8 u- z+ Z) q  z
在core_cm4.h头文件中会看到有如下定义:
4 |+ G2 r, n/ {3 k, D' T
8 Q4 K# w3 {7 V6 H9 G
  1. #define   __I     volatile: L4 }0 H# j" {8 e
  2. #define   __O     volatile   , T/ A9 J8 h0 x
  3. #define   __IO    volatile           4 ?: P5 f9 o1 K5 ^- W
  4. #define   __IM     volatile const   
    & u4 L9 c5 `& \
  5. #define   __OM     volatile           
    0 C9 X- Z  `9 J/ L: y
  6. #define   __IOM    volatile  
复制代码

0 f  E; ]) V% s0 G& s8 m5 A& R在HAL库中,大家会看到部分这样的宏定义,表示将volatile或者volatile const来代替某一个符号。( {8 B: r% Q1 B6 e  a- z/ F1 N

% @4 M5 y1 G7 A6 e! X+ W1.volatile4 ?% m* ^: h" f
volatile表示强制编译器减少优化,告诉编译器必须每次去内存中取变量值。8 {1 \, B2 M  S. i3 K
程序运行时数据是存储在主内存(物理内存)中的,每个线程先从主内存拷贝变量到对应的寄存器中。对没有加volatile的变量进行读写时,为了提高读取速度,编译器进行优化时,会先把主内存中的变量读取到一个寄存器中,以后,再读取此变量的值时,就直接从该寄存器中读取,而不是直接从内存中读取了,这样的读写速度比较快。如果其它程序改变了内存中变量的值,上面已经保存到寄存器中的值不会跟着改变,从而造成应用程序读取的值和实际的变量值不一致。加了volatile以后的变量,表示不想被编译器优化掉,每次都要从内存中读取该变量的数据,不会用寄存器里的值,这样确保了数据的准确性,但影响了效率。; l$ |, |- _% x7 b
2.const
9 |4 d( s) [3 c7 M' b  H& l7 Uconst叫常量限定符,用来限定特定变量为只读属性,如果修改此变量,编译器会报错。const修饰的变量存储在只读数据段,在程序结束时释放,而const局部变量存储在栈中,代码块结束时释放。用const定义变量时就要初始化该变量:: t  T( K; P0 a
const int a = 4;
& f4 J7 y/ l7 p( ^& U0 p+ ?3.static
- N$ K5 m5 I! I9 x6 Lstatic 关键字修饰的变量称为静态变量,如果该变量在声明时未赋初始值,则编译器自动初始化为0,静态变量存储在全局区(静态区)。
' p: w- m. K; j$ i在函数内被static声明的变量,仅能在本函数中使用,也叫静态局部变量。
! g6 I" a! B' C! ~在文件内(函数体外)被static声明的变量,仅能被本文件内的函数访问,不能被其他文件中的函数访问,也叫静态全局变量。
2 O5 W. z2 B+ h+ \" o  k/ W( L静态全局变量和普通的全局变量不同,静态全局变量仅限于本文件中使用,在其它文件中可以定义一个与静态全局变量名字相同的变量。普通的全局变量可以通过extern外部声明后被其他文件使用,也就是整个工程可见,而且其他文件不能再定义一个与普通全局变量名字相同的变量了。' G4 j5 S$ b* n- I% \' ^' H
用static修饰的函数和用static修饰的变量类似。9 L' z2 C# x$ g7 A, q
5.1.8 指针( c# H7 h* v( K8 X6 x
指针是一个值指向地址的变量(或常量),其本质是指向一个地址,从而可以访问一片内存区域。在编写STM32代码的时候,或多或少都要用到指针,它可以使不同代码共享同一片内存数据,也可以用作复杂的链接性的数据结构的构建,比如链表,链式二叉树等,而且,有些地方必须使用指针才能实现,比如内存管理等。
1 ?* d/ Z9 n$ n" j3 U. ~申明指针我们一般以p开头,如:
$ P2 x% x  Q( a' @3 j. k% Dchar * p_str = “This is a test!”;5 Z1 J+ s0 s# v7 m5 f
这样,我们就申明了一个p_str的指针,它指向This is a test!这个字符串的首地址。我们编写如下代码:; d* V5 R: |9 H
  1. int main(void)
    % F. I/ \- d( c6 x# X: v
  2.     {
    * e" d7 F4 O- g  U
  3.         HAL_Init();                                             /* 初始化HAL库 */. ^; L# Y& w  D- [3 N+ p
  4.         sys_stm32_clock_init(RCC_PLL_MUL9);         /* 设置时钟,72M */8 y) g+ ^; r3 K; I$ ?& B
  5.         usart_init(115200);                             /* 初始化串口 */
    0 F: e% Q% A- N. e/ U8 B
  6. / f8 x3 ?' x- x; m
  7.         uint8_t temp = 0X88;                        /* 定义变量 temp */
    7 R  v4 G" U# l( j8 d# |1 E
  8.         uint8_t *p_num = &temp;                /* 定义指针p_num,指向temp的地址 */
    * M. A* u7 d9 l7 h: h$ m' k) I
  9.         printf("temp:0X%X\r\n", temp);                   /* 打印temp的值 */$ W3 y0 i: W9 z, x# A8 W8 E( h
  10.         printf("*p_num: 0X %X\r\n", *p_num);            /* 打印*p_num的值 */
    & L# w  ]) v7 O. }  e* Y
  11.         printf("p_num: 0X %X\r\n", (uint32_t)p_num);  /* 打印p_num的值 */
    $ z2 U( L8 X# y% Q9 R# H; p
  12.         printf("&p_num: 0X %X\r\n", (uint32_t)&p_num);/* 打印&p_num的值 */. Z0 `3 N# _" Z6 t3 Y1 j

  13. ( b7 U2 H: @( @  f) P  Q& K/ t
  14.         while (1);  
    * @- R) u2 n, r# {. k  a( r
  15.     }
复制代码

; X) e  G* k/ J7 ^! ?* t6 g此代码的输出结果为:
. F& H9 P$ ]& l3 B1 n+ w
% k+ Z# H& G2 ?  d3 h 6031866737a84f088189581587908cb8.png
* f( g* f2 Y! s. e0 W- U6 x0 K
  V+ R8 d1 k8 ~图5.1.6. 2输出结果
  @/ a$ m# Z/ }2 ]$ H$ Mp_num:是uint8_t类型指针,指向temp变量的地址,其值等于temp变量的地址。( i& L5 |$ v! e% ]
*p_num:取p_num指向的地址所存储的值,即temp的值。
4 ]5 ?+ l( V* M9 N9 c; X/ N&p_num:取p_num指针的地址,即指针自身的地址。, Q' g0 M7 e9 i) q  ^4 u
以上,就是指针的简单使用和基本概念说明,指针的详细知识和使用范例大家可以百度学习,网上有非常多的资料可供参考。指针是C语言的精髓,在后续的代码中我们将会大量用到各种指针,大家务必好好学习和了解指针的使用。
6 G4 K- p  n: ~- w# B' y5.2 STM32MP157存储系统. ^6 C+ N1 q+ O
5.2.1 寄存器基本概念! {' B$ n1 h+ Y# h; R
我们在接触51单片机的时候就经常听到寄存器(Register)这个词,笔者认为,寄存有“寄养、临时存放”之意。寄存器是CPU内部的元件,其实也就是由锁存器或触发器构成的,是CPU内部用来暂时存放参与运算的数据的一些小型存储区域,在数字电路中这些数据就是0或者1的二进制数据或代码。. ?, V8 p$ ^) w7 S& f$ K- m
寄存器拥有非常高的读写速度。当CPU在计算时,先把要用的数据从存储模块读到内存,然后再把即将参与运算的数据写到寄存器中,当要用这些数据进行运算的时候,再从这些寄存器中读取出数据,运算的结果也可以从内存中写到寄存器,这样一来,CPU就不用每次从内存中读取数据了,这使CPU的计算能力加快了。
. L, s; a/ k. G5 O$ rSTM32单片机的寄存器是32位的,而且寄存器数量比51单片机的寄存器数量还要多。CPU中有内核外设和片上外设,每种外设都有其对应的寄存器,通过配置这些寄存器可以控制对应的外设实现复杂的功能。内核外设(核外设),如我们后面会遇见的SysTick(系统滴答定时器),片上外设,如我们常见的GPIO、SPI、IIC、UART、I2C等。) O) j8 }- v8 L9 g% ~
STM32的寄存器那么多,我们不可能把每个寄存器的配置都记住,在开发过程中,我们用到什么外设就查询对应外设的寄存器。从大方向来区分,STM32寄存器分为两类,如下表所示:
8 g6 R6 w7 x5 H. O8 @' O
$ u' I/ t4 B& A% n$ T 9c2864287e474bd6aaf3a1b66277679a.png 5 a3 q- c& b/ h: B

) G1 G, }8 Z2 Y$ Z8 q1 V4 v表5.2.1. 1寄存器分类
% w  N8 B( v* p其中,内核寄存器,我们一般只需要关心中断控制寄存器和SysTick寄存器即可,如果要深入研究RTOS操作系统的话就需要了解R0~R15、xPSR这些内核寄存器。外设寄存器则是学到哪个外设,就了解哪个外设相关寄存器即可,所以整体来说,我们需要关心的寄存器并不是很多,而且很多都是有共性的,比如STM32MP157 M4内核有6个IIC控制器,我们只需要学习其中一个IIC控制器相关寄存器即可,其他5个都是一样的。- j1 b2 U/ T2 y5 O: j, S
5.2.2 STM32MP157总线结构) w1 x7 E# }3 j% y
本小节我们对总线结构做一个了解性的介绍。6 \9 t: Q$ ]6 S& \' h
AMBA (Advanced Microcontroller Bus Architecture) 高级处理器总线架构,是由ARM公司推出的一种通用的、开放的片上通信标准,用于片上系统中功能模块的连接和管理。其中,AHB、APB和AXI总线是目前的SOC中经常用到的总线结构:
* [( z& }( t: FAXI(Advanced eXtensible Interface),即高级的可扩展接口,是一种面向高性能和高时钟频率的系统设计的总线,目前AMBA4.0 包括AXI4.0、AXI4.0-lite、ACE4.0、AXI4.0-stream等。
: n' V% V2 a7 ?4 bAHB(Advanced High performance Bus),即高级高性能总线,主要用于高性能模块 (如CPU、DMA和DSP等)之间的连接,也可以称AHB为“系统总线”。
" \* }' D8 x; A. \$ ?7 ^: rAPB(AdvancedPeripheral Bus),即高级外围总线,主要用于低带宽的周边外设(如UART、USB、SPI、I2C等)之间的连接,也称之为外围总线。% U7 q7 G5 ^4 D7 m6 @0 ~- {6 _  I3 T3 X$ \
如下图是STM32MP157的总线架构图,总线架构围绕两个互连矩阵进行组织。其中,AXI互联矩阵主要用于Cortex-A7 CPU子系统和高带宽的设备(USBH、ETH、SDMMC1/2、MDMA、GPU和 LTDC)。主AHB互联矩阵主要用于Cortex-M4子系统和相关的设备(OTG、DMA1/2和SDMMC3)。3 I9 m% L  d* i2 @6 F
5 W4 f/ f. N/ u  l0 d
a09ff64e14794363ad52c53bb402bf01.png / I: i4 s' T) ?; v' R

3 s+ r; a& ]1 g5 o0 L8 W5 s图5.2.2. 1 STM32MP157总线架构图8 Y3 q9 B4 j0 p
如上图中,M表示主端口,S表示从端口。两根线交叉线相连有小圆点的地方表示可以进行通信,没有小圆点的地方表示不能通信。在AXI互联矩阵中,M0M10共接了11个主机,S0S11共接了12个从机层。其中,M0端口连接到了MCU互连端口,MCU可以通过此端口访问MPU的模块,不过M0和S7线没有交叉圆点,表示MCU无法访问MPU的ROM。M1和M2接了USBH端口,它们仅限于DDR和SYSRAM存储器的访问。% K/ Q5 H: y% E5 Q* O
主AHB互联矩阵是有关于Cortex-M4子系统的AHB互联部分,其结构继承了以前的MCU系列,包括10个主机和9个从机层,并由 32 位多层 AHB 总线矩阵构成。
/ L+ w# q  J2 u3 a对于STM32MP157的总线架构图,这里就不做重点讲解,我们主要能大概看明白此图的含义就可以了,如果对AMBA总线架构感兴趣,也可以在ARM官网获取更加详细的信息。
/ E6 ~+ _* `" m; l5.2.3 存储器映射
' F+ B  s% P; }6 k2 u; P7 \  d' w- NSTM32MP157是一个32位ARM芯片,它可以很方便的访问4GB以内的存储空间(2^32 = 4GB),这4GB的存储空间不是杂乱无章的,而是在芯片设计的时候规划好了不同外设所占用的存储范围,比如内部SRAM、DDR、外设等,这些外设模块全部组织在同一个4GB的线性地址空间内。数据字节以小端格式(小端模式)存放在存储器中,数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。STM32MP157的内存映射如下图所示:* N9 ^8 ~  L( ]' [: ?* f; f3 T! e& l
) k/ e. [% O. Q* E+ r6 o
cdd299eee5224fc08b9de47f8a37482e.png ! A* W  t1 Q1 ^4 X$ a
4 d3 b/ L. x: m9 T0 g
图5.2.3. 1内存映射
" y' F9 s$ l6 R& T& `; M从上图中左侧部分可以看到,STM32MP157整个4GB内存空间被分为13块,这13块存储区域对应不同的功能。因为芯片厂商是不可能把4GB空间用完的,同时,为了方便后续型号升级,会将一些空间预留(Reserved),图中灰色框就是未使用的预留存储块,13个存储块的功能如下表所示:' x1 o7 }+ `$ z7 B/ i) O
: O! ~" Q8 V" g, m5 W6 O" j
7c837724510a404daf8744913234f30e.png
. T: e# M- N- @% Y0 L; n, y: B: g1 c0 z
表5.2.3. 1存储块功能及地址范围
  J5 n6 s  J5 s/ ~根据STM32MP157官方文档描述,STM32MP157有SRAM1~4,但是现在SRAMs和RAM aliases都有SRAM1~4,实际测试M4代码放到SRAMs和RAM aliases区域都可以运行。但是ST官方M4例程都是放到SRAMs中的SRAM1~4中运行,不清楚RAM aliases中的SRAM1~4有什么作用,ST官方的资料也没有明确说明。& G7 h. g; r2 K, L$ \
我们来看几个比较重要的存储块,首先是BOOT存储区域,这是STM32MP157内部BOOT ROM,用来存储ST自己编写的启动代码,用户不可以使用。6 D0 l4 e$ t5 q
第二个就是SRAMs区域,这个区域里面一共有4块SRAM:SRAM1~SRAM4,这四块SRAM地址范围如下表所示:; `6 e) g* }+ I6 q2 R9 ?* ]
1 K, x3 D% a( Z- _/ n
2bf9479223c442cda8084d6f7d03375a.png
, {- E- n3 {5 k# W6 F6 b+ ?3 ?
! X, Q5 O/ U; P5 I: y表5.2.3. 2存储区域
/ {# y! H5 a' f: x: g' O2 m  RSRAM1SRAM4的地址空间是连续的,地址范围为0X100000000X1005FFFF,总大小为384KB。STM32MP157 M4内核就使用这384KB内存来运行程序,RAM和ROM全部都在这384KB内存范围内。, M; \8 V7 t8 m. C
但是,如果既要启动A7内核,又要启动M4内核,并且A7内核和M4内核之间又要进行通信,那么M4就不能使用这全部的384KB内存。此时ST给出的SRAM1~SRAM4内存分配如下图所示:
0 k3 Q4 L0 Q/ {5 D3 \3 y' M6 @
9 S- `! t# q% I 97b4a9c306dd44c087d5f4e658f90980.png
: s+ V& v  |/ J" `% C  ^2 c# K. A) g3 {) D; C  C" Q4 t( w
图5.2.3. 2 内存分配! Y& \" W2 H7 L* e
从上图可以看出,当A7和M4一起启动,而且A7要和M4之间进行通信的时候,SRAM4作为DMA缓冲区,SRAM3作为内部IPC缓冲区。此时,M4内核只能使用SRAM1来存放代码,使用SRAM2存放数据,这个时候M4的代码和数据都不能大于128KB!大家打开ST官方的M4裸机例程的时候就会发现默认的代码区为SRAM1,数据区为SRAM2。7 g' t# G8 `$ `. ~" I
图中的RETRAM仅能用于存放M4内核的中断向量表!关于中断向量表我们在后面外部中断实验 章节会进行介绍。
  Q5 U; L& e% F  r' i; }  ^6 I第三个就是Peripherals 1区域,也就是外设内存区域1,外设都是挂在对应的总线上,这些总线都有对应的内存空间,Perpherals 1区域内存分配如下表所示:
8 G$ ~: i9 P$ s9 L2 k- S) g* u+ t& m; r; n) T
9fd169b01aed4cbfb16af2e30773d636.png # a/ U; h9 G  B

# z- b  }7 a/ u9 N5 y$ _- c表5.2.3. 3内存区域8 d- r1 ?( q/ p6 }3 K5 N
第四个是Peripherals 2区域,是外设内存区域2,对应的内存分配如表5.2.3.4所示:
+ j' R4 K. x2 A$ f
9 h1 m, O& e9 c" ^: f4 N7 S 6853357016f24874a87186f2331f7562.png 6 Q" ~2 V7 X- y7 o0 u4 o0 W

6 N  d, Z* ^2 b表5.2.3. 4内存区域
; {. z- q6 @; J第三个就是DDR区域,这个区域就是DDR映射的内存区域,这个区域地址范围为0XC0000000~0XFFFFFFFF,一共1GB,所以ST32MP157最大支持1GB的DDR内存。
  ?4 j" \7 `- M% rSTM32MP157存储映射就讲到这里,至于其他存储区域,大家了解一下就行了,对于STM32MP157 M4内核来说,重点要了解的就是SRAM1~4这384KB内存,因为M4内核的程序就是运行在这段内存区域内的。* @9 x6 z# F  K4 P/ ?; B8 w
5.2.4 寄存器地址& j& H( ]; n2 w. c- u5 T& l! Y0 ~
前面我们了解了总线、存储器映射,可以知道总线的地址以及总线上有哪些外设,我们打开开发板光盘A-基础资料\7、STM32MP1参考资料中的 《STM32MP157参考手册》,在整个学习过程中我们需要不断翻看此手册。查看参考手册的寄存器边界地址表格Register boundary addresses,此表中详细记录了每个外设挂在哪个总线上,例如,GPIOZ挂在AHB5上,GPIOA~GPIOK挂在AHB4上,如下图只是部分截图:
/ T9 w/ d/ o3 C0 t) k' I( A
" t2 c4 S2 k5 K0 m( H 27eb96bd51a441619dcd73c82787b708.png . X0 o' i  D0 v  t! ]* S* Q" P

" y. `, d* v& r! m9 M, u图5.2.4. 1 STM32MP157寄存器边界地址表格
* j- e: t" `( S/ L1 w6 k& f: Y8 \根据Register boundary addresses表,我们以AHB4这根总线为例,0x50000000表示AHB4总线的基地址,AHB4总线地址范围为0x50000000 0x5001FFFF,这一段地址区间被划分为若干的小区域,这些小区域用于保留或者给不同的外设使用。例如0x5000D4000x5001FFFF这段区域用于保留,0x5000A000~0x5000A3FF这段区域给GPIOI使用,其中0x5000A000表示GPIOI的基地址,我们点击GPIOI后面的GPIO registers一栏就进入GPIO相关的寄存器配置介绍页面。+ D9 w5 ]$ c6 R% @" {- }) c- M6 h
如下图,其中,寄存器GPIOx_MODER(x等于A~K和Z,下同)的偏移地址是0x00,复位后默认的实际地址是0xFFFF FFFF,如果我们继续往下看,下一个寄存器GPIOx_OTYPER的偏移地址是0x04,他们是以0x04为间隔逐渐递增的,即相邻的两个寄存器的偏移地址是0x04。STM32地址的编排是按照Byte来编排的,也就是8位,那么32位的数据就占用了4个Byte才构成了一个完整的32位寄存器,所以地址的偏移是相隔4(0x04)Byte。5 O8 A3 ~  ^( K+ }, Y
( j- z! A8 ?4 }: a; Z
59b064bf8e2c49529ced7e6c1aeec4c5.png / H! [% s  t6 [$ ~0 g$ x. S; p

1 Q3 w9 m/ H# _: t- K$ u图5.2.4. 2 GPIO寄存器配置页面1 a8 y9 Q: a6 x3 R& o
上图中,寄存器GPIOx_MODER有031个位,每位都可以进行读(r)和写(w)操作,其中每两位组合成一个MODERy(y等于015),对某位写1表示将该位置位,写0表示将该位复位。根据寄存器位说明,对某一组MODERy写入00表示配置某个IO口(引脚)为输入模式,写入01表示配置某个IO口(引脚)为输出模式。例如控制GPIOI的第0个IO口为输出模式,则配置GPIOI的MODER0[1:0]为01(即第0位为1,第1位为0)。
5 S6 m* C$ `5 x' D9 C% X& E: J我们找到参考手册第13章GPIO介绍部分,章节13.1~13.3主要是GPIO的整体性能介绍,13.4部分是GPIO的各个寄存的每个位的配置介绍,如果要配置GPIO寄存器,就按照这章节的内容来操作,对于其它外设寄存器也是如此。
* Y9 ]' U  r1 n+ w  \$ d/ _* E! ?2 `
058f76183085450bbb56d2ab529bcb7a.png
. {0 F) n$ {0 I9 `0 x) a$ p2 D, b2 S) s# m( k
图5.2.4. 3 GPIO介绍
1 A/ t. Y; c7 |0 Q" t( I' U' D+ Z上面提到偏移地址,偏移地址是相对基址偏移后的另外一个地址量。实际地址(或者说绝对地址)就是基地址加上偏移地址后的值。根据表Register boundary addresses我们已经可以找到基地址,再根据查询到的偏移地址就可以算出某个寄存器的实际地址(基地址+偏移地址)。例如GPIOI这个外设的地址范围是0x5000A000~0x5000A3FF,0x5000A000是GPIOI的基地址,可以算出GPIOI_MODER寄存器的地址为0x5000A000(0x5000A000+0x00),GPIOI_ODR寄存器的地址为0x5000A014(0x5000A000+0x14)。
- X$ h+ V* z5 o( B% T9 k7 y! g5.2.5 寄存器映射
- i% O0 P! ]  O: y( z/ l5 F通过前面存储器映射以及寄存器的介绍,寄存器映射的概念就更好理解了。给有特定功能的内存单元取一个别名,这个别名就是我们经常说的寄存器,给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
" h$ V* Q  m1 `( X& N例如,我们在51单片机中为什么可以用 P0=0xFF 点亮小灯,这是因为在其头文件reg52.h中利用sfr这个关键字定义了一个个的内存地址,sfr(special function register)即特殊功能寄存器,sfr P0=0x80表示把单片机的地址0x80改名字为P0,这就意味这我们可以用P0代表80这个地址单元,如果要操作80这个地址,直接操作P0即可。但是在STM32中没有sfr这个关键字,那它是如何实现的呢?
1 P+ S* [" L1 S' e9 Z6 b5 wGPIOB的地址为0x50003000 - 0x500033FF,在这段地址中有很多的寄存器,包括寄存器GPIOB_ODR(偏移地址位0x14),GPIOB_ODR寄存的地址为0x50003014(绝对地址),如果GPIOB_ODR的低十六位全为1即可点亮16个小灯,那么:
& |/ |, M2 j, N; h  v6 F$ W将这16位置1,可以写成:  V, }2 R" e+ J. s9 d; Z5 p3 {
(unsigned int)(0x50003014) = 0xFFFF;
, A* o: O# y% ]8 H6 Q* c如果项目中外设有很多,按照这种方式,我们操作每个寄存器的话,每个寄存器的地址都需要赋值一遍,而且寄存器的地址还会容易写错,使用起来肯定是不方便的。所以我们可以先将GPIOB_ODR寄存的地址定义为某个宏,然后以后只要是需要操作某个寄存器的话,直接操作该寄存器对应的宏即可,这样不容易出错:! h- U1 f8 g8 w1 x6 u! s

) Z% A) Y- }% l% [9 R  }& h
  1. #define GPIOB_ODR  (unsignedint*)(0x50003014)
    8 g4 k6 R- f1 Z2 E: y$ t9 s
  2. * GPIOB_ODR = 0xFF;4 V) k, @* Z( q) k6 y! {$ S3 k

  3. $ I+ w  B: y  M6 O& m: Z8 d
  4. #define GPIOB_ODR *(unsignedint*)(0x50003014)) G9 @: W( p2 M& @; F
  5. GPIOB_ODR = 0xFF;
复制代码

; C1 R, U6 ~6 ]9 K! ?4 e) K这个过程就是寄存器映射。实际上ST官方已经为我们做好这一部分的内容了,在STM32Cube固件包中的stm32mp157dxx_cm4.h头文件中就有定义,它使用了大量的结构体来对寄存器进行了封装,如果我们要访问某个寄存器,只需要定义一个结构体指针,然后通过指针操作结构体对应的成员即可。ST官方的stm32mp157dxx_cm4.h头文件是怎么实现的,这部分内容我们会在第六章节分析stm32mp157dxx_cm4.h头文件的时候进行详解介绍。最后,GPIOB_ODR的低十六位全为1的写法可以这样:. [+ r0 V; a8 s8 J* ?6 o$ }
GPIOB->ODR = 0XFF;' [9 L2 T. h4 C$ r5 j7 Z! A/ V  ?
————————————————$ X4 _7 M9 H' c3 k# J$ o2 h: N
版权声明:正点原子0 s4 [# d) L8 W: H
2 g/ _" K* m1 H$ m
收藏 评论0 发布时间:2022-9-24 22:33

举报

0个回答

所属标签

相似分享

官网相关资源

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