从创建模板到LED、时钟系统、中断系统、SysTick、按键轮询、按键中断、蜂鸣器
先了解一些嵌入式C语言
const
是constant(常量、常数)的缩写:可以用来修饰变量、数组、指针、函数等
-
修饰变量
使用const修饰变量,功能是对变量声明为只读特性,并保护变量值以防被修改。
例如const int i = 5;
这个例子表明整形变量i具有只读性,不能够被修改; -
修饰数组
const int array[5] = {0,1,2,3,4}; int const array[5] = {0,1,2,3,4};
与修饰变量类似,表面此数组只具有可读性,不可修改
-
修饰指针
使用const修饰指针有两种形式,一种用来限定指向空间的值不可修改;另一种是限定指针不可修改,int i = 5; int k = 10; int const *p1 = &i;//const 修饰的是*p1,即p1指向的空间的值不可改变,例如*p1=20就是错误的用法;但是p1的值是可以改变的,例如p1=&k;则没有任何问题 int * const p2 = &k;//对于指针p2,const修饰的是p2,即指针本身p2不可更改,而指向空间的值是可以改变的,例如*p2 = 15是没有问题的,而p2=&i则是错误的用法。
-
修饰函数参数
const修饰函数参数对参数起限定作用,防止其在函数内部被意外修改,所限定的参数可以是普通变量也可以是指针变量void fun(const int x) { ··· x=10;//对x的值进行了修改,是错误的 }
void fun1(const int *p) { ··· (*p)++;//对p指向的空间的值进行了修改,是错误的 }
作用域与static用法
了解static关键字的用法前,先了解C语言中的作用域、看局部变量和全局变量的概念。
一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。
块是用一对花括号{ }括起来的代码区域,定义在亏啊中的变量具有块作用域。块作用域的可见范围是从定义处到包含该定义的块末尾。以前,具有块作用域的变量都必须声明在块的开头,C99标准放宽了这一限制,允许在块中的任意位置声明变量。例如不支持C99标准的的for循环需要这样写:
void fun1(void)
{
int i = 0;
for(i=0;i<10;i++)
{
···
}
}
//在函数fun1的开头定义了局部变量i,然后在for循环中调用此变量,变量i的作用域是函数fun1内,当函数fun1执行完毕之后变量i会被释放。
而C99标准下可以这样写
void fun2(void)
{
for(int i = 0; i<10; i++)
{
...
}
}
//这样写的话,变量i的作用域则在for循环体内,当循环结束后,变量就会被释放,可见其作用域缩小了,这样的好处是增加了安全性和灵活性。
在函数fun1中,变量i被声明在函数体内,我们称这样的变量为局部变量,其有效范围是在被定义的函数内,函数执行完毕后变量即被释放;如果把这个变量定义在函数体外,如:
int k = 0;
void fun3(void)
{
for(k=0; k<10; k++)
{
...
}
}
我们则将定义在函数体外的变量称之为全局变量,其作用范围为当前源文件和工程,若其它源文件想要调用用此变量需要在文件内使用关键字extern声明,如extern int k。
-
局部变量会在每次声明的时候被重新初始化(如果在声明的时候有初始化赋值),不具有记忆能力,其作用范围仅在某个块作用域可见;
-
全局变量只会被初始化一次,之后会在程序的某个地方被修改,其作用范围可以是当前的整个源文件或者工程;
鉴于两种变量的局限性,就引入了静态变量(静态局部变量和静态全局变量),使用关键字static来修饰。其中静态局部变量满足局部变量的作用范围,但是其拥有记忆能力,不会在每次生命的时候都初始化一次,这个作用在用来实现计数功能的时候非常方便
void cnt(void)
{
static int num = 0;
num++;
}
//这个函数中,变量num就是静态局部变量,在第一次进入cnt函数的时候被声明,然后执行自加操作,num的值就等于1;当第二次进入cnt函数的时候,num不会被重新初始化变为0,而是保持1,再自己=增则变成了2,以此类推,其作用域仍然是cnt这个函数体内
静态全局变量则将全局变量的作用域缩减到了只当前源文件可见,其它文件不可见
static int k = 0;
void set_k(void)
{
k = 1;
}
void reset_k(void)
{
k = 0;
}
int get_k(void)
{
return k;
}
//静态全局变量的优势是增强了程序的安全性和健壮性,因为对于变量k而言,我们假设我们不期望它的文件有修改变量k的能力,但是其他的文件有需要变量k的值来进行逻辑运算,那我们就可以向上述例子那样做,在源文件中定义一个静态全局变量,同时使用函数对其的值进行修改和获取,对外只提供函数接口即可,其他文件通过函数结构间接使用这个变量。这样做同时也可以提高可移植性。
在C语言中static关键字除了用来修饰变量之外,还可以用来修饰函数,让函数仅在本文件可见,其它文件无法对其进行调用,例如在example1.c文件里面进行了如下定义:
static void gt_fun(void)
{
···
}
//gt_fun这个函数就只能在example1.c中被调用,在example2.c中就无法调用这个函数。而如果不使
用static来修饰这个函数,那么只需要在example2.c中使用extern关键字写下语句extern void gt_fun(void);即可调用gt_fun这个函数。
extern
在C语言中,extern关键
字用于指明函数或变量定义在其它文件中,提示编译器遇到此函数或者变量的时候到其它模块去寻找其定义,这样被extern声明的函数或变量就可以被本模块或其它模块使用。因而,extern关键字修饰的函数或者变量是一个声明而不是定义
/* example.c */
uint16_t a = 0;
uint16_t max(uint16_t i, uint16_t j)
{
return ((i>j)?i:j);
}
/* main.c */
#include <stdio.h>
extern uint16_t a;
extern uint16_t max(uint16_t i, uint16_t j);
void main(void)
{
printf("a=%d\r\n", a);
printf("Max number between 5 and 9: %d\r\n", max(5, 9));
}
extern关键字还有一个重要的作用,就是如果在C++程序中要引用C语言的文件,则需要用以下格式:
#ifdef __cplusplus
extern "C"{
#endif /* #ifdef __cplusplus */
......
#ifdef __cplusplus
}
#endif /* #ifdef __cplusplus */
//这段代码的含义是,如果当前是C++环境(_cplusplus是C++编译器中定义的宏),要编译花括号{}里面
的内容需要使用C语言的文件格式进行编译,而extern “C”就是向编译器指明这个功能的语句。
volatile
volatile原意是“易变的”,在嵌入式环境中用volatile关键字声明的变量,在每次对其值进行引用的时候都会从原始地址取值。由于该值“易变”的特性所以,针对其的任何赋值或者获取值操作都会被执行(而不会被优化)。由于这个特性,所以该关键字在嵌入式编译环境中经常用来消除编译器的优化
在C语言中当使用volatile修饰一个变量时即表示这个变量的值随时都有可能发生改变,因此编译器在编译的时候对该变量的存取操作不能进行优化,即告诉编译器每次存取该变量的时候都要从内存中去存取而不是使用其之前在寄存器中的备份
volatile int a = 1,b,c;//为a,b,c申请内存并初始化
b = a; //内存(&a) -> 寄存器
//寄存器 -> 内存(&b)
c = a; //内存(&a) -> 寄存器
//寄存器 -> 内存(&c)
//左边是c语言代码,经过编译器汇编会形成右边注释的这几部操作,b=a;用汇编则是两步操作,但是常常CPU处理速度比内存快很多,所以编译器会进行优化操作,比如上面这个4步汇编操作,第三步就是可优化的,可是在几种情况下b = a;、c = a;两段代码之间可能会发生其他操作导致寄存器的内容改变,(例如:中断、多线程、硬件寄存器),我们要保证c = a;能正确执行那就不能省略第三步汇编操作,所以volatile的作用就是告诉编译器,这个地方是易变的,不能进行优化操作。
struct
C的结构的声明格式如下
struct [结构体名] //struct关键字(它表明跟在其后的是一个结构,后面是一个可选的标记, 后面的程序中可以使用该标记引用该结构)
{ //结构体声明中用花括号括起来的是结构体成员列表。每个成员都用自己的声明来描述。成员可以是任意一种C的数据类型,甚至可以是其他结构,最后的分号是声明所必需的,表示该结构布局定义结束。
类型标识符 成员名 1;
类型标识符 成员名 2;
.
.
.
类型标识符 成员名 n;
};
//此声明描述了一个由n个数据类型的成员组成的结构,它并未创建实际的数据对象,只描述了该对象由声明组成。
struct students
{
char name[50];
char sex[50];
int age;
float score;
};
int main(void)
{
struct students student;
printf("Name: %s\t",student.name[0]);
printf("Sex: %s\t", student.sex);
printf("Age: %d\t", student.age);
printf("Score: %f\r\n", student.score);
return 0;
}
//可以把结构的声明放在所有函数的外部,也可以放在一个函数的内部。如果把一个结构声明在一个函数的内部,那么它的标记就只限于函数内部使用;如果把结构声明在所有函数的外部,那么该声明之后的所有函数都能使用它的标记。
//结构有两层含义,一层含义是“结构布局”,如上述例子的structstudent{…};告诉编译器如何表示数据,
但是它并未让编译器为数据分配空间;另一层含义是创建一个结构体变量,如上述例子的struct students
student;编译器执行这行代码便创建了一个结构体变量student,编译器使用students模板为该变量分配空间:内含50个元素的char型数组1、50个元素的char型数组2,一个int型的变量和一个float的变量,这些存储空间都与一个名称为student结合在一起
enum
enum是C语言中用来修饰枚举类型变量的关键字。在C语言中可以使用枚举类型声明符号名称来表示整型常量,使用enum关键字可以创建一个新的“类型”并指定它可具有的值(实际上,enum常量是int类型,因此只要能使用int类型的地方就可以使用枚举类型)。枚举类型的目的是提高程序的可读性,其语法与结构的语法相同
enum [枚举类型名]
{
枚举符 1,
枚举符 2
.
.
.
枚举符 n,
};
enum color
{
red,
green,
blue,
yellow
};
-
enum常量
上面的例子中,red, greeb, blue, yellow 到底是什么?从技术层面来讲,它们是 int 类型的整型常量 -
enum默认值
默认情况下,枚举列表中的常量都被赋予0,1,2等,因此下面的声明中,apple的值是2:enum fruit{banana, grape, apple};
-
enum赋值
在枚举类型中,可以为枚举常量指定整数值:enum levels{low=90, medium=80, high=100};
如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续的值
enum feline{cat, lynx=10, puma, tiger}; //那么cat=0,lynx、puma、tiger的值分别是10、11、12
typedef
typedef工具是一个高级数据特性,利用typedef可以为某一类型自定义名称。这方面与#define类似,但是两者有三处不同:
- 与#define不同,typedef创建的符号只受限于类型,不能用于值;
- tyedef由编译器解释,不是预处理器;
- 在其受限范围内,typedef比#define更灵活
该定义的作用域取决于typedef定义所在的位置。如果定义在函数中,就具有局部作用域,受限于定义所在的函数。如果定义在函数外面,就具有文件作用域
用typedef来命名一个结构体类型的时候,可以省略该结构的标签(struct):
typedef struct
{
char name[50];
unsigned int age;
float score;
}student_info;
student_info student={“Bob”, 15, 90.5};
预处理器与预处理指令
- 预处理器
#define、#include、#ifdef、#else、#endif、#ifndef、#if、#elif、#line、#error、#pragma
C语言建立在适当的的关键字、表达式、语句以及使用他们的规则上。然而C标准不仅描述C语言,还描述如何执行C预处理器。
C预处理器在执行程序之前查看程序,因而被称之为预处理器。根据程序中的预处理指令,预处理器把符号缩写替换成其表示的内容(#define)。预处理器可以包含程序所需的其它文件(#include),可以选择让编译器查看哪些代码(条件编译)。预处理器并不知道C,基本上它的工作是把一些文本转换成另外一些文本。
由于预处理表达式的长度必须是一个逻辑行(可以把逻辑行使用换行符‘\’变成多个物理行),因而为了让预处理器得到正确的逻辑行,在预处理之前还会有个编译的过程,编译器定位每个反斜杠后面跟着换行符的示例,并删除它们
printf(“Hello, Chi\
na”);
转换成一个逻辑行
printf(“Hello, China”);
#define与#undef
#define预处理器指令以#号作为一行的开始,到后面的第一个换行符为止。也就是说,指令的长度仅限于一行。然而在预处理开始前,编译器会把多行物理行处理为一行逻辑行
#define STRING “I am Chinese, \
I love China.” /* 反斜杠把该定义延续到下一行 */
//每行#define(逻辑行)都由3部分组成。第1部分是#define指令本身;第2部分是选定是缩写,也称为宏,
有些宏代表值
#undef指令用于取消已定义的#define指令
#include
当预处理器发现#include预处理指令时,会查看后面的文件名并把文件的内容包含到当前文件中,即替换文件中的#include指令。这相当于把被包含文件的全部内容输入到源文件#include指令所在的位置
#include指令有两种形式
#include <stdio.h> // 文件名在尖括号内
#include “myfile.h” // 文件名在双引号内
在UNIX中,尖括号<>告诉预处理器在标准系统目录中寻找该文件,双引号“”告诉预处理器首先在当前目录(或指定路径的目录)中寻找该文件,如果未找到再查找标准系统目录
#include <stdio.h> // 在标准系统目录中查找 stdio.h 文件
#include “myfile.h” // 在当前目录中查找 myfile.h 文件
#include “/project/header.h” // 在 project 目录查找
#include “../myheader.h” // 在当前文件的上一级目录查找
为什么我们要包含文件?因为编译器需要这些文件中的信息,例如stdio.h中通常包含EOF、NULL、getchar()和putchar()的定义。此外,该文件还包含C的其它的I/O函数。而对于我们自定义的文件,对于嵌入式开发来说,可能这些文件就有需要使用到的某些引脚宏定义、简单的功能函数宏定义等,以及某个源文件的全局变量和函数的声明等。
下面是一个子自定义一个头文件的示例
/*gpio.h*/
#ifndef __GPIO_H
#define __GPIO_H
#include <stdio.h>
typedef struct
{
uint8_t cnt;
uint16_t sum;
float result;
}MyStruct;
typedef enum
{
GPIO_RESET = 0,
GPIO_SET = 1,
}GPIO_STATE;
#define ABS(x) ((x>0) ? (x) : (-x))
#endif
/* main.c */
#include “gpio.h”
int main(void)
{
MyStruct my_struct = {0, 25, 3.14};
GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_SET);
printf(“cnt=%d, sum=%d, result=%f\n\r”, my_struct.cnt, my_struct.sum, my_struct.result);
}
#include指令也不是只包含.h文件,它同样也可以包含.c文件
条件编译
-
#ifdef、#else和#endif指令
#ifdef HI /* 如果用#define 定义了符号 HI,则执行下面的语句 */ #include <stdio.h> #define STR "Hello world" #else /* 如果没有用#define 定义符号 HI,则执行下面的语句 */ #include "mychar.h" #define STR "Hello China" #endif //#ifdef指令说明,如果预处理器已定义了后面的标识符,则执行#else或#endif指令之前的所有指令并编译所有C代码,如果未定义且有#elif指令,则执行#else和#endif指令之间的代码。 //#ifdef、#else和C和if else很像,两者的主要区别在于预处理器不识别用于标记块的花括号{},因此它使用#else(如果需要的话)和#endif(必须存在)来标记指令块。
-
#ifndef指令
#ifndef指令与#ifdef指令的用法类似,也可以和#else、#endif一起使用,但是它的逻辑和#ifdef指令相反 -
#if和#elif
#if指令很像C语言中的if。#if后面紧跟整型常量表达式,如果表达式为非零,则表达式为真,可以在指令中使用C的关系运算符和逻辑运算符#if MAX==1 printf("1"); #endif 可以按照 if else 的形式使用#if #elif: #if MAX==1 printf("1"); #elif MAX==2 printf("2"); #endif //条件编译还有一个用途是让程序更容易移植。改变文件开头部分的几个关键的定义即可根据不同的系统设置不同的值和包含不同的文件
指针
什么是指针?从根本上看,指针是一个值为内存地址的变量。正如char类型变量的值是字符,int类型变量的值是整数,指针变量的值是地址。因为计算机或者嵌入式设备的硬件指令非常依赖地址,指针在某种程度上把程序员想要表达的指令以更接近机器的方式表达,因此,使用指针的程序更有效率。尤其是指针能够有效地处理数组,而数组表示法其实是在变相的使用指针,比如:数组名是数组首元素的地址。要创建指针变量,首先要声明指针变量的类型。假如想把ptr声明为储存int类型变量地址的指针,就要使用间接运算符*来声明
ptr = &bah;
//然后使用间接运算符*找出储存在bah的值:value = *ptr;也可以成为解引用运算符。
//ptr = &bah;value = *ptr;放一起效果等同于:value = bah;
如何声明一个指针变量:
声明指针变量时必须指定指针所指向变量的类型,不同的变量类型所占据的储存空间是不同的,一些指针操作需要知道操作对象的大小。另外程序必须知道储存在指定地址的数据类型
int *pi; // pi 是指向 int 类型变量的指针
char *str; // str 是指向 char 类型变量的指针
float *pf, *pg; // pf, pg 都是只想 float 类型变量的指针
-
指针与数组
可以使用地址运算符&获取变量所在的地址,而在数组中同样可以使用取地址运算符获取数组成员中任意成员的地址int week[7] = {1, 2, 3, 4, 5, 6, 7}; int *pw; pw = &week[2]; printf("week is: %d", *pw);
-
指针与函数
-
指针与硬件地址
-
STM32F103ZET6中内部SRAM的基地址是0x20000000,我们想对这片空间的前256个字节写入数据,就可以使用指针指向这个基地址
volatile unsigned char *pData = (volatile unsigned char *)(0x20000000); int main(void) { int i = 0; for(i=0; i<256; i++) { pData[i] = i+10; } return 0; }
指针应用的基本原则:
-
首先必须要指定指针的类型;
-
如果是普通指针变量,非函数形参或者函数指针,必须要给指针变量指定地址,避免成为一个“野指针”;
回调函数
在C语言中回调函数是函数指针的高级应用。所谓回调函数,一个笼统简单的介绍就是一个被作为参数传递的函数。从字面上看,回调函数的意思是:一个回去调用的函数,如何理解这句话呢?从逻辑上分析,要“回去”,必然存在着一个已知的目的地,然后在某一个时刻去访问;那么回调函数就是存在一个已知的函数体A,将这个函数体A的地址即函数名“A”(函数名即是这个函数体的函数指针,指向这个函数的地址)告知给另外某个函数B,当那个函数B执行到某一步的时候就会去执行函数A。
位运算
位运算是指二进制位之间的运算。在嵌入式系统设计中,常常要处理二进制的问题,例如将某个寄存器中的某一个位置1或者置0,将数据左移5位等
- 按位与运算符(&)
参与运算的两个操作数,每个二进制位进行“与”运算,若两个都为1,结果为1,否者为0。例如,1011&1001,第一位都为1,结果为1;第二位都为0,结果为0;第三位一个为1,一个为0,结果为0;第四位都为1,结果为1。最后结果为1001。(全1才是1) - 按位或运算符(|)
参与运算的两个操作数,每个二进制位进行“或”运算,若两个都为0,结果为1,否者为1。例如,1011 | 1001,第一位都为1,结果为1;第二位都为0,结果为0;第三位一个为1,一个为0,结果为1;第四位都为1,结果为1。最后结果为1011(有1就是1) - 按位取反运算符(~)
按位取反运算符用于对一个二进制数按位取反。例如,~1011,第一位为1,取反为0;第二位为0,取反为1;第三位为1,取反为0,结果为1;第四位为1,取反为0。最后结果为0100。 - 左移(<<)和右移(>>)运算符
左移(<<)运算符用于将一个数左移若干位,右移(>>)运算符用于将一个数右移若干位。例如,假设val为unsigned char型数据,对应的二进制数为10111001。若val=va<<3,表示val左移3位,然后赋值给val,左移过程中,高位移出去后被丢弃,低位补0,最后val结果为11001000;val=val>>3,表示val右移3位,然后赋值给val,右移过程中,低位移出去后被丢弃,高位补0,最后val结果为00010111 - 清0或置1
例如,MCU的ODR寄存器控制引脚的输出电平高低,寄存器为32位,每位控制一个引脚的电平。假设需要控制GPIOB的1号引脚输出电平的高低,设置该寄存器第0位为1,输出高电平,设置该寄存器第0位为0,输出低电平
创建模板+LED
桌面创建一个文件夹里面放入Core、Driver、Main、Project、SDK,在特定文件中放入必要文件
配置好Keil的相关配置
点亮LED
一下午一直报错的经验就是,千万要细心别打错字母大小写USE_HAL_DRIVER,STM32F103xE