今天在逛CSDN的时候发现一篇嵌入式开发基础篇,看完感觉写的非常好,特拿过来跟大家分享一下。原篇地址:https://blog.csdn.net/qq_61672347/article/details/126760788。
文章目录
前言
嵌入式开发中既有底层硬件的开发又涉及上层应用的开发,即涉及系统的硬件和软件,C语言既具有汇编语言操作底层的优势,又具有高级语言功能性强的特点,当之无愧地成为嵌入式开发的主流语言。在 STM32开发过程中,不论是基于寄存器开发还是基于库开发,深入理解和掌握嵌入式C语言的函数、指针、结构体是学习STM32的关键。
嵌入式C语言的结构特点如下。
(1)程序总是从main函数开始执行,语句以分号“;”结束,采用/
…
/或//做注释。
(2)函数是C语言的基本结构,每个C语言程序均由一个或多个功能函数组成。
(3) 函数由两部分组成:说明部分和函数体。
函数名(参数)
{
[说明部分];
函数体;
}
(4)一个C语言程序包含若干个源程序文件(.c文件)和头文件(.h文件),其中.h头文件主要由预处理命令(包括文件、宏定义、条件编译等)和数据声明(全局变量、函数等声明)组成;c源文件主要是功能函数的实现文件。
(5)采用外设功能模块化设计方法,一个外设功能模块包括一个源文件(.c文件)和一个头文件(.h文件),.c文件用于具体外设功能模块函数的实现,.h头文件用于对该外设功能模块参数及功能函数的声明。
嵌入式系统开发多采用模块化、层次化的设计思想,系统层次架构清晰,便于协同开发。图1为嵌入式系统的软件基本结构框图。
图1 嵌入式系统的软件基本结构框架图
一、STM32的数据类型
数据是嵌入式C语言的基本操作对象,数据类型是指数据在计算机内存中的存储方式,如基本数据类型中的整型(存放整数)、浮点型(存放实数)、字符型(存放字符)、指针(存放地址)以及派生出的复合数据类型(如数组、结构体、共用体、枚举类型)。嵌入式C语言的数据类型如图2所示。
图二 嵌入式C语言的数据类型
由于不同CPU定义的数据类型的长度不同,因此ARM公司联合其他半导体厂商制定了统一的CMSIS 软件标准,这个标准中预先定义了相关的数据类型,ST公司也为开发人员提供了基于C语言的标准外设库,其定义的数据类型如表1所示,相关源代码请参考STM32标准外设库v3.5.0的stdint.h头文件。
stm32f10x.h头文件还对标准外设库之前版本所使用的数据类型进行了说明,v3.5.0版本已不再使用这些旧的数据类型,为了兼容以前的版本,新版本对其进行了兼容说明,如图3所示。
表1 STM32定义的数据类型
图3 STM32标准外设库数据类型兼容说明
图3中的_I、_O以及_IO为IO类型限定词,内核头文件 core_cm3.h定义了标准外设库所使用的IO类型限定词,如表2所示。注意,IO类型限定词加下画线是为了避免命名冲突。
表1的数据类型与表2中的IO类型限定词相结合,在标准外设库中常用来定义寄存器和结构体变量,图4为stm32f10x.h头文件中相关外设的寄存器定义。
表2 STM32的IO类型限定词
图4 stm32f10x.h头文件中相关外设的寄存器定义
结合表2和图3,可以看出同一数据类型有多种表示方式,如无符号8位整型数据有unsigned char、uint8_t、u8三种表示方式,在不同的ST标准外设库版本中这三种表示方式都可以表示无符号8位整型数据,初学者应了解这三种表达方式,最新的v3.5.0版本采用 CMSIS软件标准的C99标准,即 uint8_t方式。
二、C语言中常用的关键字
1、const关键字
const关键字用于定义只读的变量,其值在编译时不能被改变,注意,const关键字定义的是变量而不是常量。
使用 const关键字是为了在编译时防止变量的值被误修改,同时提高程序的安全性和可靠性,一般放在头文件中或者文件的开始部分。
在C99标准中,const关键字定义的变量是全局变量。const 关键字与#definc关键字存在区别,#define关键字只是简单的文本替换,而const关键字定义的变量是存储在静态存储器中的。使用#define关键字定义常量的形式为
#define PI3.14159
使用该方式定义后,无论在何处使用PI,都会被预处理器以3.14159替代,编译器不对PI进行类型检查,若使用不慎,则很可能由预处理引入错误,且这类错误很难发现。用const声明变量的方式虽然增加了分配空间,但可以很好地消除预处理引入的错误,并提供了良好的类型检查形式,保证安全性。
利用 const关键字进行编程时需要注意以下三点。
(1)使用const关键字声明的变量,只能读取,不能被赋值。如:
const uint8t sum = 3.14;
uint8_t abs=0;
...
sum= abs;//非法,将导致编译错误,因为sum 只能被读取,不能赋值
abs- sum: //合法
(2) const关键词修饰的变量在声明时必须初始化,上述语句表示 sum值是3.14,且sum值在编译时不能修改,若在编译过程中直接修改sum值,则编译器会提示出错。
(3)函数的形参声明为const,则意味着所传递的指针指向的内容只能读,不能被修改。如C语言的标准函数库中用于统计字符串长度的函数 int strlen(const char*str)。
2、static 关键字
在嵌入式C语言中,static关键字可以用来修饰变量,使用static关键字修饰的变量,称为静态变量。
静态变量的存储方式与全局变量一样,都是静态存储方式。全局变量的作用范围是整个源程序,当一个源程序由多个源文件组成时,全局变量在各个源文件中都是有效的,即一个全局变量定义在某个源文件中,若想在另一个源文件中使用该全局变量,则只需要在该源文件中通过 extern关键字声明该全局变量就可以使用了。若在该全局变量前加上关键字static,则该全局变量被定义成一个静态全局变量,其作用范围只在定义该变量的源文件内有效,其他源文件不能引用该全局变量,这样就避免了在其他源文件中因引用相同名字的变量而引发的错误,有利于模块化程序设计。
利用static关键字进行编程时需要注意以下要点。
(1)static关键字不仅可以用来修饰变量,而且可以用来修饰函数。模块化程序设计中,若用static声明一个函数,则该函数只能被该模块内的其他函数调用,例如:
#include "stm32f1xx_hal .h”
static void DMA_SetConfig (DMA_HandleTypeDef *hdma,uint32_t SrcAddress,uint32_t DstAddress, uint32_t DataLength);
...
HAL_statusTypeDef HAL_DMA_start_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength)
{
HAL_StatusTypeDef status- HAL_OK;”
.... ...
if(HAL_DMA_STATE_REA.DY m- hdma->state)
{
DMA_Setconfig(hdma, SrcAddress, DstAddress, DataLength);
... ...
}
... ...
}
上述代码为DMA模块的源文件stm32f1xx_hal_dma.c,若利用static将DMA_SetConfig()函数声明为一个静态函数,则 DMA_SetConfig)函数只能被stm32flxx_hal_dma.c中的其他函数调用,而不能被其他模块的文件使用,即定义了一个本地函数,有效避免了因其他模块的文件定义了同名函数而引发的错误,充分体现了程序的模块化设计思想。
(2) static除了用于定义静态全局变量,还用于定义静态局部变量,保证静态局部变量在调用过程中不被重新初始化。典型应用案例有实现计数统计功能。
void fun_count()
{
static count_num=0;
//声明一个静态局部变量,count_num用作计数器,初值为0
count_num++;
printf("%d\n",count_num) :
}
int main(void)
(
int i=0;
for( i=0;i<=5;i++)
{
fun_count();
}
return 0;
}
在main函数中每调用一次 fun_count()函数,静态局部变量count_num加1,而不是每次都被初始化为初值0。
3、volatile关键字
嵌入式开发中,常用到volatile关键字,它是一个类型修饰符,含义为“易变的”。使用方式如下:
volatile char i;
这里使用volatile关键字定义了一个字符型的变量i,指出i是随时可能发生变化的,每次使用该变量时都必须从i的地址中读取。
由于内存的读/写速度远不及CPU中寄存器的读/写速度,为了提高数据信息的存取速度,一方面在硬件上引入高速缓存Cache,另一方面在软件上使用编译器对程序进行优化,将变量的值提前从内存读取到CPU的寄存器中,以后用到该变量时,直接从速度较快的寄存器中读取,这样有利于提高运算速度,但同时也可能存在风险,如该变量在内存中的值有可能被程序的其他部分(如其他线程)修改或覆盖,而寄存器中存放的仍是之前的值,这就导致应用程序读取的值和实际变量值不一致;也有可能是寄存器中的值发生了改变,而内存中该变量的值没有被修改,同样也会导致不一致的情况发生。因此,为防止由于编译器对程序进行优化导致读取错误数据,使用 volatile关键词进行定义。
简单地说,使用volatile关键字就是不让编译器进行优化,即每次读取或者修改值时,都必须重新从内存中读取或者修改,而不是使用保存在寄存器的备份。
举个简单的例子:大学里的奖/助学金的发放一般都是直接转给学校,学校再发给每名学生,学校财务处都登记了每名学生的银行卡号,但不可避免地会有一些学生因各种原因丢失银行卡或不再使用这张银行卡,而没来得及去财务处重新登记,从而影响奖/助学金的发放,这里,学生就是变量的原始地址,而财务处的银行卡号就是变量在寄存器中的备份,使用 volatile关键字来定义学生这个变量,这样每次发放奖/助学金时都去找学生这个变量的原始地址,而不是直接转到财务处保存的银行卡上,进而避免错误的发生。
const关键字的含义为“只读”,volatile关键字的含义为“易变的”,但volatile关键字解释为“直接存取原始内存地址”更为合适,使用 volatile关键字定义变量后,该变量就不会因外因而发生变化了。一般来说,volatile 关键字常用在以下场合。
(1)中断服务程序中修改的、供其他程序检测的变量需要使用volatile关键字。
(2)多任务环境下各任务间共享的标志应添加 volatile关键字。
(3)外设寄存器地址映射的硬件寄存器通常要用volatile关键字进行声明。
4、extern关键字
extern关键字用于指明此函数或变量定义在其他文件中,提示编译器遇到此函数或变量时到其他模块中寻找其定义。这样,extern关键字声明的函数或变量就可以在本模块或其他模块中使用,因此,使用extern关键字是一个声明而不是重新定义。使用方法如下:
extern int a;
extern int funA( ):
解析:第一条语句仅仅是变量a的声明,而不是定义变量a,并未为a分配内存空间,变量a作为全局变量只能被定义一次。第二条语句声明函数funA(),此函数已在其他文件中定义。
STM32中,extern关键字还有一个重要作用,即与”C一起连用,即 extern “c”,进行链接指定。例如,stm32f10x.h头文件中有如下代码。
#ifndef _STM32F10× H
#define _STM32F10x_H
#ifdef .epluspius
extern "C"{
#endif
...
#ifdef _eplusplus
}
#endif
这段代码的含义是,若没有定义_STM32F10x_H,则定义_STM32F10x H,若已经定义_cplusplus,则执行 extern “C”中语句,extern “C”是告诉C++编译器括号中的程序代码是按照C语言的文件格式进行编译的,_cplusplus是C++编译器中自定义的宏,plus是“+”的意思。
C+H+支持函数重载,即在编译时会将函数名与参数联合起来生成一个新的中间函数名称,而C语言不支持函数重载,这就导致在C++环境下使用C函数会出现链接时找不到对应函数的情况,这时就需要使用extern “C”进行链接指定,告知编译器此时采用的是C语言定义的函数,需要使用C语言 的命名规则来处理函数,不要生成用于链接的中间函数名。
一般将函数声明存放在头文件中,当函数有可能被C语言或C+使用时,将函数声明存放在 extern “C”中以免出现编译错误,完整的使用方法如下:
#ifdef__cplusplus
extern "C"{
#endif
//函数声明
#ifdef_Cplusplus
}
#endif
STM32中很多头文件都采用这样的用法,如标准外设库中的 stm32f1 0x_adc.h ,stm32f10x can.h、 stm32f1Ox_gpio.h 等。
利用extern 关键字进行编程时需要注意以下要点。
嵌入式开发一般采用模块化设计思想,因此,为保证全局变量和功能函数的使用,extern关键字一般用在.h头文件中对某个模块提供给其他模块调用的外部函数及变量进行声明,实际编程中只需要将该.h头文件包含进该模块对应的.c文件中,即在该模块的.c文件中加入代码#include “xxx.h”。实例如下:
5、struct结构体
struct用于定义结构体类型,其作用是将不同数据类型的数据组合在一起,构造出一个新的数据类型。struct一般用法如下:
struct 结构体名
{
数据类型 成员名1;
数据类型 成员名2;
数据类型 成员名n;
};
struct Student{ //声明结构体
char name[20]; //姓名
int num; //学号
float score; //成绩
};
6、enum
有时一个变量会有几种可能的取值,如一个星期有7天、每学期开设的课程、12种不同的颜色(红、橙、黄、绿、青、蓝、紫、灰、粉、黑、白、棕)等,C语言提供了一种enum枚举类型,用来将变量或对象的所有可能的值一一列出,变量取值只限于列举出来的值。enum枚举类型的用法如下:
enum枚举名
{
枚举成员1,
枚举成员2,
...
枚举成员n;
}枚举变量;
enum枚举类型是一个集合,将所有可能的取值用花括号括住,花括号中的各枚举成员之间用逗号隔开,最后一个枚举成员后省略逗号。enum枚举类型以分号结束,这里的枚举变量可以省略,在后面需要时再根据枚举名进行定义。
例如,利用enum枚举类型列举几种常见的颜色。
enum Color
{
RED,
GREEN,
BLACK,
YELLOw
};
上述名为 Color的枚举类型只有4个成员:RED、GREEN、BLACK、YELLOW,即意味着Color类型变量的取值只能取这4种颜色中的某一种颜色。
例如,利用enum定义一个 Weekdays枚举类型名,包括7个枚举成员:从星期一到星期日,并定义枚举变量 Mydays 与 Olddays.
enumweekdays
{
Monday=1,
Tuesday,
wednesday,
Thursday,
Friday,
Saturday,
sunday
}Mydays,olddays;
注意:enum枚举类型具有自动编号功能,第一个枚举成员的默认值为整型的0,后续枚举成员的值在前一个成员值上自动加1,也可以自定义枚举成员的值,若把第一个枚举成员的值定义为1,则第二枚举成员的值就为2,依此类推,如上述例子中 Friday 的值为5。因此,enum枚举类型中的枚举成员的值是常量而不是变量,不能在程序中用赋值语句再对它赋值,但可以将枚举值赋给枚举变量。
例如,以下两条语句是正确的。
Mydays=Thursday;
olddays=Friday;
但以下两条语句是错误的。
Tuesday=o;
Mydays=1;
7、typedef
typedef用于为复杂的声明定义一个简单的别名,它不是一个真正意义上的新类型。在编程中使用 typedef的目的一般有两个:①为变量起一个容易记忆且意义明确的新名称;②简化一些比较复杂的类型声明。其基本格式如下:
typedef类型名自定义的别名;
例如:
typedef signed char int8_t;//为数据类型signed char起别名int8_t
typedef signed int int32_t;//为数据类型signed int起别名int32_t
STM32开发中,typedef主要有以下三种用法。
-
typedef的基本应用
为已知的数据类型起一个简单的别名,如上例。 -
typedef 与结构体struct结合使用
该用法用于自定义数据类型。如 stm32f10x_gpio.h头文件中的GPIO初始化结构体GPIO_InitTypeDef。
typedef struct
{
uint16_t GPIO_ Pin;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOMode TypeDef GPIO_Mode;
}GPIO_InitTypeDef;
上述语句利用 struct创建了一个新的结构体,这个新结构体有三个成员 GPIO_Pin、GPIO_Speed和 GPIO_Mode,同时又使用 typedef为这个新建的结构体定义一个新的名称GPIO_InitTypeDef,在应用时就可以直接使用GPIO_InitTypeDef 来定义变量。例如:
GPIO_InitTypeDef GPIO_ InitStrueture;
上述语句利用 GPIO_InitTypeDef结构体定义了一个变量GPIO_InitStructure,引用三个成员的方法如下:
GPIO InitStructure.GPIO_Pin;
GPIO_InitStructure.GPIO_Speed;
GPIO InitStructure.GPIO Mode;
-
typedef 与 enum结合使用
利用typedef关键字将枚举类型定义成别名,并利用该别名进行变量声明,STM32标准外设库v3.5.0版本中有很多enum和 typedef结合使用的应用。stm32f10x_gpio.h头文件中的代码如下。
Typedef enum
{
GPIO Speed_1OMHz=1,
GPIo_Speed_2MHz,
GPIOSpeed_50MHz;
}GPIOSpeed_TypeDef;
该例中enum枚举类型共有三个成员:GPIO Speed_10MHz、GPIO Speed_2MHz和GPIO_Speed_50MHz,并将第一个枚举成员GPIO_Speed_10MHz赋值为1,enum枚举类型会将枚举成员的赋值在第一个枚举成员赋值的基础上加1,因此GPIO_Speed_2MHz 默认值为2,GPIO_Speed_50MHz默认值为3。同时,利用typedef关键字将此枚举类型定义一个别名GPIOSpeed TypeDef,这里省略了枚举类型的枚举名,只用 typedef为枚举类型定义一个别名。
8、#define
#define是C语言的预处理命令,它用于宏定义,用来将一个标识符定义为一个字符串,该标识符称为宏名,被定义的字符串称为替换文本,采用宏定义的目的主要是方便程序编写,一般放在源文件的前面,称为预处理部分。
所谓预处理是指在编译前所做的工作。预处理是C语言的一个重要功能,由预处理程序负责完成,程序编译时,系统将自动引用预处理程序对源程序中的预处理部分进行处理,处理完毕后自动进入对源程序的编译。
STM32标准外设库中,#define的使用方式主要有以下两种。
1.无参数宏定义
无参数宏定义的一般形式如下:
#define<宏名>字符串>
其中,字符串可以是常数、字符串和表达式等。
例如:
#define UINT8_MAX 255
该语句表示定义了宏名UINT8_MAX,它代表255,例如:
#define_IO volatile;
该语句表示定义宏名_IO,代表 volatile,若以后程序中再需要用到 volatile,则可以使用IO。
例如:
#define RCC AHBPeriph_DMA1 ((uint32_t)0x00000001)
该语句表示定义RCC_AHBPeriph_DMA1宏名,代表32位的无符号数据0x00000001.
STM32中有很多此类用法,如标准外设库 v3.5.0的 stm32f1 0x_rcc.h文件中APB2_peripheral外设基地址的定义,如图5所示。
图5 APB2_peripheral各外设基地址的定义
2.带参数的宏定义
宏定义格式如下:
#define<宏名>(参数1,参数2,…,参数n)<替换列表>
例如:
#define SUM(x,y) (x+y)
…
a=SUM(2,2);
其中,a的结果是4,将 SUM(X,y)定义为x+y,预编译时会将SUM(x,y)替换为xty。
例如:
#define IsGPIO_SPEED(SPEED)(((SPEED) = GP1o_Speed_10MHz)||((SPEED)==GPIO_Speed_ 2MHz)||((SPEED)==GP10_Speed_50MHz))
使用宏定义#define 将 IS_GPIO_SPEED(SPEED)替换为 GPIO_Speed_10MHz、GPIO_Speed_2MHz或者GPIO_Speed_50MHz。
注意
:带参数的宏定义同样也只是进行简单的字符替换,替换是在编译前进行的,展开并不分配内存单元,不进行值的传递处理,因此替换不会占用运行时间,只占用编译时间,因此该方式可以提高运行效率。
#define与 typedef的区别为:typedef是在编译阶段处理的,具有类型检查的功能,而#define是在预处理阶段处理的,即在编译前,只进行简单的字符串替换,而不进行任何检查。
三、回调函数
回调函数是一个通过函数指针调用的函数。操作系统中的某些函数常需要调用用户定义的函数来实现其功能,由于与常用的用户程序调用系统函数的调用方向相反,因此将这种调用称为回调(Callback),而被系统函数调用的函数就称为回调函数。
STM32的HAL库在stm32flxx_hal_xxx.c文件中定义了相应的回调函数,并由中断触发,其实质是中断处理程序。如 stm32flxx_hal_gpio.c代码中通过GPIO中断处理函数voidHAL _GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)调用相应的回调函数HAL_GPIO_EXTICallback(GPIO_Pin),开发人员只需要在回调函数中编写应用程序就能实现中断服务功能。
四、#ifdef 、#ifndef、#else 、#if
#define 定义一个预处理宏
#undef 取消宏的义
#if 编译预处理中的条件命令,相当于C语法中的if语句
#ifdef 判断某个宏是否被定义,若已定义,执行随后的语句
#ifndef 与#ifdef相反,判断某个宏是否未被定义
#elif 若#if, #ifdef, #ifndef或前面的#elif条件不满足,则执行#elif之后的语句,相当于C语法中的else-if
#else 与#if, #ifdef, #ifndef对应, 若这些条件不满足,则执行#else之后的语句,相当于C语法中的else
#endif #if, #ifdef, #ifndef这些条件命令的结束标志.
defined 与#if, #elif配合使用,判断某个宏是否被定义
指针相关内容我这里就不在赘述了网上又很丰富的资料