9.1 初学者重要提示+ S$ ]+ g. |5 f9 Y
1、 如果对C语言不熟练的话,可以阅读C语言书:C Primer Plus(第五版)中文版.pdf
$ j9 `6 B# ]( A2、 为了更好的学习本章知识点,可以看之前做的视频教程第10章% d: o, b) p8 J; v/ s& S
+ L6 d2 m" f$ l
9.2 数据类型
. J+ N, u$ h1 {7 x5 {: |% \了解数据类型之前要对ANSI C和ISO C的发展史有个了解,特别是C89,C99和C11的由来。
/ H/ d! n/ F/ ?) \& v* ~& C
- X" [- Y0 g( L: t' D; N9.2.1 ANSI C和ISO C历史; S& n1 I" W1 J3 b( H$ H+ u, k
1983年,美国国家标准协会(ANSI)组成了一个委员会来创立C语言的标准。因为这个标准是1989年发布的,所以一般简称C89标准。有些人也把C89标准叫做ANSI C。
% l* Z6 ]5 B' ~6 a 在1990年,ANSI C89标准被国际标准化组织(ISO)和国际电工委员会(IEC)采纳为国际标准,名叫ISO/IEC 9899:1990 - Programming languages C,有些人简称C90标准。因此,C89和C90通常指同一个标准,一般更常用C89这种说法。+ R g% j% N* T0 a# x6 Y7 K3 E' O
在2000年3月,国际标准化组织(ISO)和国际电工委员会(IEC)采纳了第二个C语言标准,名叫ISO/IEC 9899:1999 - Programming languages -- C,简称C99标准。. D, Q1 g$ r u! g% G' u
在2011年12月,国际标准化组织(ISO)和国际电工委员会(IEC)采纳了第三个C语言标准,名叫ISO/IEC 9899:2011 - Information technology -- Programming languages -- C,简称C11标准。它是C程序语言的最新标准。
* d- ~4 i4 Q# I1 }% P- I: S, x对于我们常用的编译器MDK和IAR而已,C89,C99和C11均支持。' E! W) [$ Y! {
$ ]8 h" J0 n- r7 }" W9.2.2 ARM架构(含Cortex-M系列)数据类型 % F# Q# x h$ S& a% h k! E
ARM架构(含Cortex-M系列)的数据类型定义如下:% o( P7 u* l1 ^, T+ t; K. y5 J6 A
, M' M g @6 Y% t; Z* W
# Z" A4 }2 P+ V5 k/ z( P6 y
: }2 Z* J n' M0 `) N# L5 l9.2.3 头文件stdint.h对数据类型的定义% o+ v- R- b' |" c. r
stdint.h是C99中引进的一个标准C库的头文件。目前大部分单片机C编译器均支持,IAR和MDK都支持。下面是部分内容(来自MDK)。) H7 r" H8 H/ o! R6 I& ^
; x9 T5 h6 Q; C8 w
- /* exact-width signed integer types */- q1 {6 Q: H% B
- typedef signed char int8_t;
v* |& Q6 g* J* o/ ? - typedef signed short int int16_t;) R( G) S: W8 j$ f
- typedef signed int int32_t;
' c2 {6 p K+ ` - typedef signed __INT64 int64_t;
8 o5 ^/ v3 L+ [+ j0 X1 F
, A* V* r( o; ]/ [' T. {/ k- /* exact-width unsigned integer types */# A' s$ h5 q3 E; V- g0 ^
- typedef unsigned char uint8_t;
4 j; ~4 f4 ] l6 E - typedef unsigned short int uint16_t;
' w a; Y' n. h: d - typedef unsigned int uint32_t;
\$ a! H* j( u6 ]/ X - typedef unsigned __INT64 uint64_t;; A4 y& J; G" x
~% [' v, T: [' v4 }) s W- /* 7.18.1.2 */3 \; ]' R1 J7 F) W
. M& l! t! q. H" l @0 R' n- /* smallest type of at least n bits */ C; w1 y4 G$ K" f8 D m; O
- /* minimum-width signed integer types */
& y( [/ v R- L; q% } - typedef signed char int_least8_t;. Y1 X8 Q( S0 @
- typedef signed short int int_least16_t;
+ Z( _+ ]0 k: _8 p' ?% i& c - typedef signed int int_least32_t;& n: @7 ~! A5 @4 T, [! N2 @7 c
- typedef signed __INT64 int_least64_t;
/ J2 |! U2 u% o) I# f6 `
& m( ~; o. H9 Q( Y1 o0 r$ W' u- /* minimum-width unsigned integer types */
5 j% b! e6 ^! Z" d. a' k. G8 y. a - typedef unsigned char uint_least8_t;& i$ S4 I8 ?7 @6 }; c
- typedef unsigned short int uint_least16_t;
1 m# g- H( ^9 t) f0 a - typedef unsigned int uint_least32_t;
0 c- l' F M# S- C - typedef unsigned __INT64 uint_least64_t;) {! X, X: g4 x; T; C
* b" _: P2 w* |- /* 7.18.1.3 */5 X; X g) h* T2 J/ u+ ]1 X
5 I8 ~' h @: a- /* fastest minimum-width signed integer types */3 T9 z$ o* h- K# N6 n" u5 M! W' J
- typedef signed int int_fast8_t;0 \5 p: z( ?% I
- typedef signed int int_fast16_t;
8 `, t& k2 {0 d Y' o - typedef signed int int_fast32_t;
W) Z' ?& K* M' T, l6 u9 K+ U3 j. } - typedef signed __INT64 int_fast64_t;2 m) b% L) ?) g$ Z" Y9 @
- R) {5 p; }0 I8 b' J- /* fastest minimum-width unsigned integer types */, [0 f! ~9 @9 y
- typedef unsigned int uint_fast8_t;* R4 v+ \; w! B: F; `
- typedef unsigned int uint_fast16_t;
4 l* W4 o C/ D$ f. z5 t7 P6 z - typedef unsigned int uint_fast32_t;
' a4 D2 g5 o1 ?; i - typedef unsigned __INT64 uint_fast64_t;
复制代码 & U5 F+ b: o( Y; u3 H+ D6 T
以MDK5.26为例,stdint.h位于如下路径:
, r2 n- t6 X+ G$ O7 R' E6 Q4 [0 L2 U, P7 d
\Keil_v526\ARM\ARMCC\include3 ], t3 g9 f9 H1 I2 b4 f( I
+ s/ E7 [9 X/ C' [- I# N以IAR8.X为,stdint.h位于如下路径:* v6 R [# Z+ z8 [3 w3 }3 E
& X6 r1 V6 W* c" O# N\IAR Systems\Embedded Workbench 8.1\arm\inc\c
R2 z: O& J. ]! u% M) i+ o' [& T8 }5 K% W! `" a
9.2.4 程序中推荐的变量命名方式
' M' p4 t* H; S# V9 E5 a看程序的时候,经常会看到各种各样的变量命名方式,例如声明一个8位无符号整数,常见的有如下几种写法:
3 D# l7 M. f8 F8 {
$ F( n$ v; T& [0 ^% `0 x0 O& Mu8 err;) _' y, m8 V5 c+ r
4 k: p. I# T, S% eU8 err;/ z5 A( u4 R: }- f) L/ B
' N9 {" ]5 }' j/ G& `1 b0 \" s4 v: eINT8U err;
+ Y' i% t( Z% L8 X8 G
* W! ^ u& e6 w' s3 o/ JUINT8 err;9 O2 n" L& V/ x# u- z$ @
4 n/ d9 _% f4 a' ^+ juint8 err;
* A' U% O+ k7 A K+ T D0 r% |8 g$ W3 t& s! q
uint8_t err;$ Q7 b! Y3 m7 e
T4 m6 b( w. U, l% P! Z( `当大家阅读别人写的程序时,往往会看到风格各异的定义方式,移植部分程序时也不知道采用哪种方式更合适。
! [9 F9 m, M& r! @* v
& y. q2 ~. {1 i0 q; T; l我们推荐大家采用最后一种定义方式,这种方法符合C99规范,ST的固件库都是采用的这种类型定义方式。像我们开发板配套的STM32例程,从2009年最初的版本开始就一直沿用C99的标准写法来定义整数。# Q. I8 p0 d4 u. ^+ [
& s/ z h# m5 `# Y' V: \4 e
9.3 局部变量和全局变量 y" `- E0 L3 ]! `
9.3.1 局部变量. T$ Z8 r K2 m0 W4 R
在一个函数内部定义的变量是内部变量,它只在本函数范围内有效,也就是说只有在本函数内才能使用它们,在此函数以外是不能使用这些变量的,这称为局部变量。' b. Q# D r! b0 f; \
, ]5 h$ H: I0 S3 ]9 [* q! F4 ?使用局部变量注意以下问题:
0 O) O7 x8 O7 j4 Y6 ~& {
1 H2 L( W* h- ? 不同函数中可以使用相同名字的变量,它们代表不同的对象,互不干扰。2 _# |. d( z' ~( q
形式参数也是局部变量。. @3 M7 G. ]: }* d: H l
局部变量的作用域在函数内部。/ c, X$ O/ |0 q. _, H3 ^* w+ z. Z) `
9.3.2 全局变量# ~4 X* y$ P* z5 K
在函数内部定义的变量是局部变量,而在函数之外定义的变量称为外部变量,也就是全局变量。使用全局变量的注意事项:
9 J6 C. v$ |5 q6 S: A8 B9 a( t! a" [
全局变量可以为本文件中其他函数所共用。它的有效范围为从定义变量的位置开始到本源文件结束。
) T% C) P9 y _' Y 设置全局变量的作用是增加了函数间数据联系的渠道。
8 n0 B2 x6 O" H% h' O2 ?3 F+ B 如果在同一个源文件中,外部变量和局部变量同名,则在局部变量的作用范围内,外部变量被“屏蔽”,即外部变量将不起作用。
3 d) e: Y' W; C& _) P4 i9.3.3 使用全局变量的缺点
- @6 I& m7 m/ b% w$ X 程序设计中,建议不要创建太多的全局变量,主要是出于以下三点考虑:
9 `6 [5 x7 o" q% r- q4 o E$ d k% q
全局变量在程序的执行过程中都占用存储单元,而不是仅在需要时才占用存储单元。! ?7 B1 j @) j9 A) m. S) R. z5 w6 Q
函数的通用性降低了,因为函数在执行时要依赖于其所在的外部变量。如果将一个函数移植到另一个文件中,还要将有关的外部变量及其值一起移植过去。
, _1 T; ]! X% U5 k- X: U; ? 使用全局变量过多,会降低程序的清晰性,特别是多个函数都调用此变量时。7 ~$ O: o$ _8 I/ s
9.3.4 变量的存储类别4 K3 w: L* a7 Y" h3 H5 p+ N! {8 C/ Z' Y
从变量的作用域来分,可以分为全局变量和局部变量,而从变量值存在的时间来看,可以分为静态存储方式和动态存储方式。
7 ?; Z1 x! G7 W1 J0 A: Q- g) X4 w! z7 z7 u/ n0 P9 m! X% ^) t W7 [
静态存储方式:指在程序运行期间由系统分配固定的存储空间方式。( u* A$ N3 ?* M6 E5 D% Q5 t
动态存储方式:在程序运行期间根据需要进行动态的分配存储空间方式。. \' P2 [7 k: |
全局变量存储在静态存储区中,动态存储区可以存放以下数据: f2 C7 f+ d, Y6 y+ A7 M6 b! V
# ]1 p; L: T$ p; R$ o, H: Y
函数形式参数,在调用函数时给形参分配存储空间。
. k- }. p. A0 ?! g. N5 x% O1 J8 c4 l- K0 L 局部变量(未加static声明的局部变量)。
0 k/ p: R+ I7 D2 b) q# ` 函数调用时的现场保护和返回地址等。
5 x4 ^! ~- i+ @9.3.5 用static声明局部或者全局变量5 T6 b5 f/ S7 S4 Y7 K6 ?. Z; T7 b
有时候希望函数中的局部变量的值在函数调用结束后不消失而保留原值,即占用的存储单元不释放,在下一次该函数调用时,该变量已有值,就是上一次函数调用结束时的值。这时可以使用关键字static进行声明。
. d3 }$ y7 O' o' O
% [# [5 B4 i, a% |: o* E用static声明一个变量的作用:
* A3 l' E* V: `) r/ z/ V
( H9 Z A+ r) s8 j 对局部变量用static声明,则使用该变量在整个程序执行期间不释放,为其分配的的空间始终存在。
8 y* r1 _! V! n5 f7 l6 W3 t 全局变量用static声明,则该变量的作用域只限于本文件模块(即被声明的文件中)。
1 Y. C' H" ^# A% g8 C9.4 堆栈
: G1 F8 W& ?9 | O+ t9.4.1 堆栈作用5 q2 c d) U: {. A5 I
栈(stack)空间,用于局部变量,函数调时现场保护和返回地址,函数的形参等。
6 f$ n Z2 V; v! }堆(heap)空间,主要用于动态内存分配,也就是说用 malloc,calloc, realloc等函数分配的变量空间是在堆上。以STM32H7为例,堆栈是在startup_stm32h743xx.s文件里面设置:1 N# v/ |) w5 K6 f. E' _3 }* x
! N3 b5 P4 c( W& r
- a2 b3 O" W7 l, d, d/ t
/ f0 Z# D g9 @7 U
- v2 t7 ?/ P, t( d1 F r! d8 \. [9.4.2 寄存器组(堆栈指针寄存器)
* ?* m' i( E1 F, @0 p+ Y/ @- aCortex – M7/M4/M3 处理器拥有R0-R15的通用寄存器组。其中R13作为堆栈指针SP。SP有两个,但在同一时刻只能有一个可以用。3 c5 \- d. B L/ m; }' F U
% s: m5 O0 `# m: u: E! W; P) B, H 主堆栈指针(MSP):这是缺省的堆栈指针,它由OS内核、异常服务例程以及所有需要特权访问的应用程序代码来使用。" X* j0 F, h7 P3 D) K
进程堆栈指针(PSP):用于常规的应用程序代码(不处于异常服务例程中时)。0 r: X1 }% j5 _# s5 w" k3 ~
另外以下两点要注意:
1 A9 i, b% P4 l
7 r: b, g8 E: {+ x$ y 大多数情况下的应用,只需使用指针MSP,而PSP多用于 RTOS 中。# @* T3 o ]8 _, o
R13 的最低两位被硬线连接到0,并且总是读出0,这意味着堆栈总是4字节对齐的。
( @( Z" `0 h8 a. z8 Q( ~3 [6 Y* X. g9 E% o! d" f5 d6 G
+ f6 z4 @- H; n1 Q9 G7 a
) U- E: B" ^8 n5 `9.4.3 Cortex-M7/M4/M3向下生长的满栈
9 b- S, I) ?% [' L0 Q, ?这个知识点在以后用H7移植RTOS时,非常有用。
# r$ j0 T+ Q3 j! Z, q
2 ?9 Q6 \. d: W' a9 h
; ^4 z" A: }8 k' s
+ p1 c: Y- l5 u9 Z9.4.4 堆栈的基本操作
0 g) N7 J7 W( X6 g这里对入栈和出栈做个简单的介绍。PUSH入栈操作:SP先自减 4,再存入新的数值:0 i" E2 j( S7 q9 |6 r! G4 q
5 E% \. {" z% i7 A4 `- e e6 r0 o: s( k/ J# T( h/ L) K6 H" C* l4 z' S
& M' x2 ?, ^9 E
POP出栈操作:先从SP指针处读出上一次被压入的值,再把SP指针自增 4:
4 ^3 f5 _ I' w" U; k6 A9 Y& J
5 U& ~( a$ A$ S& s: J5 h4 c
/ A" S) j" w/ j) Y1 B9 ?9.5 局部变量,全局变量和堆栈实例
( w! O& l3 j+ o通过下面的实例可以对局部变量,全局变量和堆栈有个感性的认识: u! `* O( |0 [1 C
# L* ] U" g, I
- uint32_t a = 0; //全局初始化区, 可以被其他c文件 extern 引用$ Y& _+ k& t, X! m# m( c: E
- static uint32_t ss = 0; //静态变量,只允许在本文件使用
5 U1 b# I) l0 U- y: t" D - uint8_t *p1; //全局未初始化区
Y6 S6 L9 V3 D& k6 G - int main(void)
, C* i- J9 L- o8 A, r - {
! ~, d! z/ _' q" a9 i - uint32_t b; //栈% z& k; O) S7 ]3 ?
- uint8_t s[] = "abc"; //栈5 b' m3 \9 ]# F
- uint8_t *p2; //栈
$ R1 }# t7 C D" ~ - uint8_t *p3 = "123456"; //123456\0在常量区,p3在栈上。% S* J' J0 d, J B+ Q8 }* ~1 s9 N- `
- static uint32_t c =0; //全局(静态)初始化区8 ?5 \4 i/ j& h, U: O4 @
- ; L) q" I. u" O' x+ }& n
- p1 = (uint8_t *)malloc(10); //在堆区申请了10个字节空间
$ M7 y+ r! @0 D. l - p2 = (uint8_t *)malloc(20); //在堆区申请了20个字节空间8 L, i. T4 `+ q' a; a$ ^6 Q
- strcpy(p1, "123456"); /* 123456字符串(结束符号是0(\0),总长度7)放在常量区,& v0 t$ {0 I c. Z4 Z. j, e: j* h+ T
- 编译器可能会将它与p3所指向的"123456"优化成一个地方 */
$ C0 F& i$ u& c, S - }
复制代码
1 z N5 }2 R, n通过查看MAP文件,可以看全局变量在RAM中的位置:
3 V0 @$ c5 F% n4 z8 X
, z8 C( ]( s2 U2 J6 H C! \6 b- Symbol Name Value Ov Type Size Object(Section)9 ?3 ]: N# a% H3 Z. U/ S \, `
- a 0x20000000 Data 4 main.o(.data)5 I* C3 z1 |, r8 a
- p1 0x2000000c Data 4 main.o(.data)" L8 O+ @6 N2 L
- ss 0x20000004 Data 4 main.o(.data)
复制代码 9 W( r& `* U# e
而局部变量要调整状态进入main函数里面查看:9 z. X, n3 u; G+ L! K
; C8 E8 c. i& |4 j6 M9 U6 l7 T0 b3 b& }& @- D
. g% P# x( G+ Z3 Z: l% G9.6 总结
- Q* ]" r8 g5 [) H5 o" IC语言的基础知识点要掌握牢靠,对于后面学习HAL库源码大有裨益。
* r [1 `( L" \/ g" ^$ v: ?1 U, {
$ J0 \7 P' |# b+ R6 [+ _
* N1 v. B2 v! W, f$ g2 F- P* S |