参考连接:
- 安装MinGW-64(在win10上搭建C/C++开发环境)
https://zhuanlan.zhihu.com/p/85429160
- MinGW-64;
链接:https://pan.baidu.com/s/1oE1FmjyK7aJPnDC8vASmCg?pwd=y1mz 提取码:y1mz --来自百度网盘超级会员V7的分享
- C语言菜鸟教程
https://www.runoob.com/cprogramming/c-tutorial.html
- C语言复习8-10,分别对应数组;指针;字符串;
https://blog.csdn.net/qq_43369406/article/details/125154550
;https://blog.csdn.net/qq_43369406/article/details/125163465
;https://blog.csdn.net/qq_43369406/article/details/125164029
; - C语言复习12 多文件编译
https://blog.csdn.net/qq_43369406/article/details/125168349
- KEIL MDK下载
链接:https://pan.baidu.com/s/1n7Z76RTw2eyp3tAQDjAObg?pwd=y1mz 提取码:y1mz --来自百度网盘超级会员V7的分享
- 野火F407开发板-霸天虎视频-【入门篇】
https://www.bilibili.com/video/BV1Vt411X7PK/
- 野火霸天虎教程
https://doc.embedfire.com/products/link/zh/latest/mcu/stm32/ebf_stm32f407_batianhu_v1_v2/download/stm32f407_batianhu_v1_v2.html
- 【单片机】野火STM32F103教学视频 (配套霸道/指南者/MINI)【全】(刘火良老师出品) (无字幕)
https://www.bilibili.com/video/BV1yW411Y7Gw/?vd_source=39f3289ad7c2358aaf9772ccb7ff98bf
使用硬件:
- stm32 407
使用软件:
- MinGW
- VSCode
- Keil5
使用系统(以下皆可):
- win10/11
- Linux
- Mac OS
目录:
- x.1 C语言基础知识
- x.2 单片机基础知识
- x.3 案例——使用单片机点亮LED灯案例
- x.4 Q&A::可能会遇到的问题
x.1 C语言基础知识
如果有C语言基础可以直接跳过。
x.1.1 C语言背景知识
C语言是为了Unix系统而诞生的语言。C 语言是一种通用的、面向过程式的计算机程序设计语言。1972 年,为了移植与开发 UNIX 操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言。
x.1.2 C语言的安装
可以通过访问 MinGW 的主页 mingw-w64.org
;知乎上文章- 安装MinGW-64(在win10上搭建C/C++开发环境)https://zhuanlan.zhihu.com/p/85429160
;MinGW-64; 链接:https://pan.baidu.com/s/1oE1FmjyK7aJPnDC8vASmCg?pwd=y1mz 提取码:y1mz --来自百度网盘超级会员V7的分享
来下载安装。
当安装 MinGW 时,您至少要安装 gcc-core、gcc-g++、binutils 和 MinGW runtime,但是一般情况下都会安装更多其他的项。
添加您安装的 MinGW 的 bin 子目录到您的 PATH 环境变量中,这样您就可以在命令行中通过简单的名称来指定这些工具。
x.1.3 C语言运行流程
C语言是一门针对操作系统设计的,强类型,面向过程,运行速度媲美汇编,在运行时先要将内容编译成汇编语言的一门语言。我们使用VSCODE编辑文本,使用GCC汇编成二进制文件,输入如下命令将编辑器写好的.c
文件转成.out
二进制文件,
$ gcc test1.c test2.c -o main.out
$ ./main.out
输出的.out
文件可以直接运行。当然更加细节的操作牵扯到更深的计算机知识,链接库等,但并不需要。
x.1.4 C语言的组成和运行规则
C语言由关键字,标识符,常量,字符串值,符号(分号),注释等组成。
C语言都是从main函数开始运行的,一个C语言的简单例程如下所示,
#include <stdio.h>
int main()
{
/* 我的第一个 C 程序 */
printf("Hello, World! \n");
return 0;
}
所有的 C 语言程序都需要包含 main() 函数。 代码从 main() 函数开始执行。
/* … */ 用于注释说明。
printf() 用于格式化输出到屏幕。printf() 函数在 “stdio.h” 头文件中声明。
stdio.h 是一个头文件 (标准输入输出头文件) , #include 是一个预处理命令,用来引入头文件。 当编译器遇到 printf() 函数时,如果没有找到 stdio.h 头文件,会发生编译错误。使用<>
来寻找系统环境变量中的问价,使用""
先找当前文件中的文件,找不到再去系统环境变量中查找。
return 0; 语句用于表示退出程序。
x.1.5 C语言的变量类型
C语言的基本类型包括整型(int32),布尔型(u8),浮点型(double64),字符型(u8)。我们可以使用sizeof关键字来查看对象或者变量的大小,
#include <stdio.h>
#include <limits.h>
int main()
{
printf("size of int is: %lu \n", sizeof(int));
int i = 0;
printf("%p\n", &i);//输出:000000000061FE1C 输出的是i变量的地址
return 0;
}
输出结果如下,
%lu
为 32 位无符号整数,%p
为输出指针内存地址,默认以十六位输出。常见的变量类型及其对应的大小如下,
我们使用type i的时候是对i变量声明并定义(会在内存开辟空间),使用extern type i的时候只是声明变量(并不会在内存给空间)。
extern int i; //声明,不是定义
int i; //声明,也是定义
x.1.6 C语言的常量
常见的常量有整数常量(如0xFFFFFF,7u等),浮点常量(3.14,3.14f),字符常量(如转义字符\t),还可以使用常见预处理器来定义常量,例如#define和const关键字,如下,
#define PI 3.14
const double PI_2 = 3.14;
推荐使用#define关键字,define是进行简单的文本替换,老外常用。
x.1.7 C作用域和存储类
C语言根据程序中定义的变量所存在的区域来决定变量的作用域,我们需要掌握的作用域就两种,局部变量和全局变量。在函数或块内部的变量称为局部变量,在所有函数外部的称为全局变量。
C语言变量存储类常见有auto,register,static,extern四种,局部变量的默认类型是auto,变量在函数和开始时被创建,在函数结束时被销毁。而register是放在寄存器中的变量,运行速度快,static和extern都是全局变量,在全局都可以调用,
static int count=10; /* 全局变量 - static 是默认的 */
{
auto int month; // auto是默认的
}
int main(){
printf(count);
// printf(month); // can't
return 0;
}
x.1.8 C语言运算符
C语言常见运算符有算数运算符,关系运算符(<,>),逻辑运算符(&&存在短路),位运算符等。
- 算数运算符
%是取模,即取余数。
- 关系运算符
关系运算符返回真或假的布尔常量,用于比较大小与相等与否,
- 逻辑运算
逻辑运算符与或非,也返回一个布尔值,但是与或具有短路效应,
- 位运算符(重要)
位运算符在单片机的操作中比较常见且重要,常见的运算符有对位取与,取或,取异或,取非,左移,右移(补码负数高位补1)。
x.1.8 C语言逻辑
C语言的逻辑由if-else选择判断,和循环组成。
选择判断中有if-else语句,问号语句,switch-case语句(搭配break使用,且switch中变量要为int32),例句如下,
/* ?: */
Exp1 ? Exp2 : Exp3;
/* switch - case */
#include <stdio.h>
int main()
{
int a;
printf("input integer number: ");
scanf("%d",&a);
switch(a)
{
case 1:printf("Monday\n");
break;
case 2:printf("Tuesday\n");
break;
case 3:printf("Wednesday\n");
break;
case 4:printf("Thursday\n");
break;
case 5:printf("Friday\n");
break;
case 6:printf("Saturday\n");
break;
case 7:printf("Sunday\n");
break;
default:printf("error\n");
}
}
循环中有while循环,for循环,do…while循环,搭配break,continue,goto(goto有害),例句如下,
#include <stdio.h>
int main ()
{
for( ; ; )
{
printf("该循环会永远执行下去!\n");
}
return 0;
}
在linux系统中,可以使用ctrl+c
终止无限循环的程序(程序中断,释放内存),使用ctrl+z
挂起程序(程序暂停,不释放内存)。
PS: 在VIM编辑器中ctrl+c
=esc
是停止输入。
x.1.9 C语言函数
面向对象语言三大特征:封装继承多态,C语言虽然是面向过程的,但是函数的存在体现着封装的思维。
C语言的函数声明必须要放在main()函数之前。函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的实际主体。
函数的定义由四部分构成,返回类型,函数名称,参数,函数主体。它的四大部分的作用为:
- 返回类型:一个函数可以返回一个值。return_type 是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,return_type 是关键字 void。
- 函数名称:这是函数的实际名称。函数名和参数列表一起构成了函数签名。
- 参数:参数就像是占位符。当函数被调用时,您向参数传递一个值,这个值被称为实际参数。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
- 函数主体:函数主体包含一组定义函数执行任务的语句。
return_type function_name( parameter list )
{
body of the function
}
一个简单的函数定义如下所示:
/* 函数返回两个数中较大的那个数 */
int max(int num1, int num2)
{
/* 局部变量声明 */
int result;
if (num1 > num2) {
result = num1;
} else {
result = num2;
}
return result;
}
我们使用函数名加小括号的形式调用函数。默认情况下,C 使用传值调用来传递参数。一般来说,这意味着函数内的代码不能改变用于调用函数的实际参数。
x.1.10 C语言枚举
枚举变量用于定义一组具有离散值的常量,可以实现变量名到整型的映射关系。枚举变量的定义需要使用enum关键字,案例如下:
// 1. 使用define定义常量
#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7
// 2. 使用enum实现1.中的映射关系
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
// 3. enum更改默认的映射关系
enum season {spring, summer=3, autumn, winter};
// 4. 常用的定义枚举变量的方式:定义枚举类型的同时定义枚举变量
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
enum DAY day; // 先定义枚举类型,再定义枚举变量
x.1.11 C语言指针
x.1.11.1 指针定义
C语言最有特色的地方便在于可以直接使用指针访问内存,我们在C语言中经常使用的数组和字符串便是指针类型变量。
一个简单的指针变量p如下定义,表示变量p就是一个指针变量,*p表示指向数字0,p表示它的值是内存的地址。
int *p = 0;
我们可以使用printf + %X
大写十六进制查看指针p的值(即指针指向的地址),可以使用printf + %p
+ &变量名
查看变量在内存中的实际地址。案例如下,
#include <stdio.h>
int main()
{
// 1. 使用sizeof()输出变量类型和变量大小
int i = 0;
printf("size of int is: %lu \nsize of i is: %lu\n", sizeof(int), sizeof(i));
// 2. 使用指针p指向i的地址,以十六进制输出p的指针和i的地址
int *p = &i;
printf("i's address is %p\np's address is %X\n", &i, p);
return 0;
}
输出结果如下所示,
x.1.11.2 指针的取地址运算符和解地址运算符
在指针使用中我们经常碰到取地址运算符&
和解地址运算符*
。顾名思义,取地址运算符为&
,符号后面只可跟一个变量,用于寻找该变量的地址;解地址运算符*
,用于得到指针变量指向的内存地方存储的值。*和&是相反的作用。如下所示:
x.1.11.3 使用传递指针访问内存
指针在C语言中的重要作用之一在于直接访问内存,传递局部变量的值并修改。我们可以将局部变量的值通过指针传到函数中,这样函数中修改的值便可以返回,即函数中的局部变量的值不会在函数结束后销毁。
通过如下四种等价的方式往函数中传递指针,
x.1.11.4 指针的类型
指针也是具有类型和大小的。指针类型最大的作用在于在做例如*p++
的运算的时候,一次性跳动的字节数不同。
#include <stdio.h>
int main(){
int *p = 0;
*p++;
printf("%X\n", p);
double *q = 0;
*q++;
printf("%X\n", q);
return 0;
}
运行代码,可以看到p一次性跳动四个字节(即int大小),q一次性跳动八个字节(即double大小),运行结果如下所示,
但我们需要注意的是,不管你的指针是什么类型,你定义的指针在内存中的实际大小都是一个字(例如我的电脑是x86x64,64bit,则大小就是64bit=8Byte,8字节)八字节大小,可以输入如下代码测试,
#include <stdio.h>
int main(){
void *p = NULL;
double *q = NULL;
printf("size of pointer p is %X\n", sizeof(p));
printf("size of pointer q is %X\n", sizeof(q));
return 0;
}
运行结果如下所示,在C语言中,NULL和0是等价的,
指针变量也和普通变量一样,支持强制类型转换,我们也可以声明一个新的指针变量指向原来的地址,malloc-free关键字一定会用强转,强转代码如下,
int a = 10;
int *p = &a;
// 把指向 int 类型的指针强制转换为指向 char 类型的指针
char *q = (char*)p;
// 使用 q 进行内存操作,一些平台可能会出现错误
*q = 'A';
x.1.11.5 使用指针动态申请内存
我们使用malloc-free
关键字来进行动态内存分配,malloc申请出来的指针默认是void *类型,所以我们一定会进行强制类型转换。
int *a = (int *) malloc ( n*sizeof(int) );
申请后的空间需要归还给系统,使用free(a)。记住要在同一个地方malloc和free。
动态申请内存是为了让系统自动给你开辟新的内存空间供你使用,你不会访问到内存别的地方,清占别的内存空间或者产生乱码。
x.1.11.6 指针和const搭配使用
const和指针混搭会产生两种效果,不用去刻意记忆名字,我们只需要判断const是否是直接修饰的指针,如int * const p = &i;
const便是直接修饰的指针,而const int *p = &i
const便不是直接修饰的指针。const修饰指针表示指针不能修改,而const不修饰指针则表示通过指针不能修改。
- 指针不能修改
int * const p = &i;
意思是指针不能修改。一旦该指针得到了某个变量的地址,就不能再指向其他变量了。
常用于:数组。
数组本质上便是一个被const修饰的指针,所以不能被直接赋值,int a[] <=> int * const a
。
- 通过指针不能修改
const int *p = &i;
意思是通过指针不能修改变量(并不使得那个变量成为const)。
常用于:字符串;在函数传参时希望用户只有访问权限,无修改权限的时候可以使用。
字符串本质上是一个const char *
,所以通过指针不能修改。
x.1.12 C语言数组
前面一章节已经说了,数组本质上是一个被const修饰的指针,即int * const p
,指针不能修改。
需要注意的是,数组在定义的时候,数组长度需要用常量定义,不能用变量,
int length = 10;
#define length_define 10
const int length_const = 10;
int a[10]; // ok
int a[length_define]; // ok
int a[length_const]; // ok
int a[length]; // wrong
- 数组的初始化和赋值
我们使用循环来给数组赋值,数组中每一个单元的数据都是同一个类型,数组的大小在初始化的时候就需要定义好,数组越界会报segmentation fault
;数组排序从0开始往后排;
// 1. 数组初始化
int array[10];
for ( int i = 0; i<10; i++){
array[i] = 0;
}
int array_2 = {1, 2, 3};
注意array[0]有时候指代数组array的地址。
- 数组大小计算
数组的大小计算使用sizeof关键字,
sizeof(a)/sizeof(a[0]);
x.1.13 C语言字符串和字符数组
在前面已经讲过,C语言的字符串本质上是通过指针不能修改的类型,即char *str <=> const char *str
,char *str是字符串类型。
字符串和字符数组存在区别,C语言中字符串的结尾需要加\0
,而字符数组就是char类型的const指针,不需要以\0
结尾。
- 字符数组
字符数组和字符串不同,字符数组就是以char的数组实现的,其结尾不需要加'\0'
举例如下:
- 字符串
字符串的结尾需要加'\0'
,字符数组的长度增加了一字节,所以字符串的本质就是最后一位是\0
的字符数组,如下:
在string.h的文件内有很多处理字符串的函数。在这里需要注意的是字符串和字符数组的索引都是从0开始的。
字符串变量的常见书写方法有以下几种:
char *str = "1"; // 指针形式,最常见的形式1
char str[] = {'1', '\0'};
char str[2] = "1";
// 字符数组
char str[] = "1"; // 字符数组 数组形式,最常见的形式2
C中,字符串的是以字符数组的形态存在的,但是仍然存在区别。需要注意的是char *str = "1";
是一个const char *str
类型,通过指针不能修改,所以如果创建需要修改的字符串,应该创建字符数组char s[] = "1"
;
与使用%d
来输入输出整型不同,字符串的输入输出需要用到%s
,如下,
- 字符串数组
我们有时候需要使用多个字符串,这个时候我们就可以使用字符串数组,
我们最经常使用的定义字符串数组方式如下所示:
char *a[]; // a是一个一维数组,其中每一个a[n]都是一个char *字符串,即通过指针不能更改的字符数组
这在main函数中的argv中也可以见到,
x.1.14 C语言结构体
结构体类似于LabVIEW中的簇,可以将不同类型的数据整合在一起形成一个对象,常用的定义结构的方式有以下两种,
// 1. 可以用typedef创建新类型(推荐)
typedef struct
{
int a;
char b;
double c;
} Simple2;
//现在可以用Simple2作为类型声明新的结构体变量
Simple2 u1, u2[20], *u3;
// 2. 先定义结构体类型,再定义结构体变量
//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//结构体的标签被命名为SIMPLE,没有声明变量
struct SIMPLE
{
int a;
char b;
double c;
};
//用SIMPLE标签的结构体,另外声明了变量t1、t2、t3
struct SIMPLE t1, t2[20], *t3;
typedef关键字在结构体定义的时候经常使用,用于使用它来为类型取一个新的名字。下面的实例为单字节数字定义了一个术语 BYTE:
typedef unsigned char BYTE;
在这个类型定义之后,标识符 BYTE 可作为类型 unsigned char 的缩写,例如:
BYTE b1, b2;
x.1.14 C语言编译多个文件
在c语言中导入的文件往往有两个.c文件和.h文件,.h文件是函数的声明,而.c文件才是函数的实际定义,一个.c文件实际上才是一个编译单元。
当导入多个文件的时候使用include "…h"来导入.h文件,include .h导入的同时,会自动定向到.c的函数进行执行,所以includu "…h"看似导入了.h文件,实际上将.h和.c文件一块导入了。
#include是一个编译预处理指令,和宏一样,在编译之前就需要处理。#include有#include <>和#include ""两种形式。其中#include ""
会要求编译器先在当前目录(.c文件所在目录)寻找这个文件,如果没有,则到编译器指定的目录中去找;#include <>
会要求编译器只在指定的目录中去找;注意,环境变量和编译器命令行参数可以指定寻找头文件的目录。windows的环境变量地址在环境变量中,mac的环境变量加载顺序为:
a. /etc/profile # 系统级别
b. /etc/paths # 系统级别
c. ~/.bash_profile # 用户级别,常改找个
d. ~/.bash_login
e. ~/.profile
f. ~/.bashrc
#include不是用来引入库的,他只是为了让编译器知道特定函数的原型,如你的形参和返回是什么类型的,保证调用时给出的参数值是正确的类型。一般任何.c文件都有其对应同名的.h文件,把所有函数原型和全局变量声明都放进去。
在书写.h文件时候,我们需要使用如下关键字来声明唯一头文件,
#ifndef AAA_H
#define AAA_H
#endif
案例如下,
// .h文件
#ifndef LIB_1_H
#define LIB_1_H
int sum(int a, int b);
#endif
// .c文件
int sum(int a, int b){
return (a + b);
}
// main文件
#include "lib_1.h"
#include <stdio.h>
int main(){
int a = 1;
int b = 2;
int c = sum(a, b);
printf("%d", c);
return 0;
}
需要注意的是,在使用书写多个文件后,我们使用gcc编译应该将所有的.c文件都编译进去,
gcc main.c lib_1.c
有时间可以欣赏一下红色警戒的源码,如下:
https://github.com/electronicarts/CnC_Remastered_Collection/tree/master/REDALERT
x.1.15 C语言宏定义/编程小技巧
这些宏定义可以加快编程速度,
x.1.15.1 使用#if #elif #endif
来注销部分代码
如果.c文件的整体定义如下,则只会运行block1的代码,
#if 0
/* code block 0 */
#elif 1
/* code block 1 */
#endif
x.1.15.2 使用#pragma region xx
来折叠代码
在VS Code中可以使用#pragma region xx
来折叠代码,产生效果如下,
折叠前,
折叠后,
x.2 单片机基础知识
x.2.1 单片机中的C语言
在已经有C/C++或者X.1的学习后,我们再补充一点单片机中的C语言的知识。单片机中常使用C的指针来操作寄存器,使用预编译宏来定义常量,使用移位操作来控制寄存器等。
x.2.1.1 指针中的取地址,解地址
取地址符号为&
,解地址符号为*
,使用如下,
int *p = &value; // 取地址用&
*p = 1; // 解地址用*
x.2.1.2 宏编译的条件判断
使用预编译宏来取消编译,
#if 0
...
#endif
x.2.1.3 C语言的位操作
常见的位操作有左移,右移,取反,与,或,异或,这部分参考c语言中文网,
x.2.1.4 C语言的置位,清零
第六个位置这个地方设置为1,其他地方不变,使用或操作|= (1<<6)
;
第六个位置这个地方设置为0,其他地方不变,使用与操作&= ~ (1<<6)
;
第六五位置设置为0,其他地方不变,&= ~(0x03)<<6
;因为0x03是11。
案例如下,孰能生巧,
x.2.2 KEIL MDK下载
下载下面软件直接安装便可,链接:https://pan.baidu.com/s/1n7Z76RTw2eyp3tAQDjAObg?pwd=y1mz 提取码:y1mz --来自百度网盘超级会员V7的分享
x.2.3 DAP仿真器下载程序
DAP仿真器用于将软件下载到单片机上,它遵循ARM公司的CMSIS-DAP标准,支持所有基于Cortex-M内核的点偏激,属于HID设备,无需安装驱动,支持多操作系统。
我们按连接线将DAP线连接好,用专用的typec转usbA数据线连接stm32和计算机,成功识别后打开keil5软件。接着打开Options for Target
。
先配置Device,在device中找到我们板子的型号,
接着我们切换到Utilities,勾选Use Debug Driver,
选择Debug界面,找到DAP调试,点击Settings,
在Adapter适配器中找到我们的设备DAP,使用SW端口,
接着在Flash Download窗口,勾选将所选部分清除后,重新写入,
然后我们Build,没error后将程序下载到板子上便可以,
x.2.4 STM32背景知识
ST是意法半导体,是一个公司名,他们从ARM公司买到cortex-M的内核/芯片,生产出自己的微控制器,能搭载安卓系统,Linux系统。基于Cortex-M内核的MCU(微控制器)很多,但是ST公司生产了很多的固件库,使得STM32成为了最璀璨的新星。
STM32,顾名思义,是用来对各种硬件进行通信和控制的,用于控制的端口很多,STM32中常见的有以下几种,
单片机可以做很多东西,比如扫地机器人等,做的东西可以众筹去生产,淘宝众筹拿钱等。STM32分类主要根据CPU位数和内核来进行区分,分别如下,
stm32的命名规则如下,
做产品的时候,我们要找到各个产品分配的IO,原理图引脚如下,
引脚的作用可以在数据手册上查看。当我们需要自己做PCB板的时候,我们可以联系JLC制作,
x.2.5 寄存器
STM32编程时,常用固件库编程或者寄存器编程,固件库编程更加快速,寄存器编程更加基层。
STM32芯片中最重要的就是Cortex-M4内核,我们将内核封装成STM32芯片,如下便是,芯片的顺序是以黑点开始,逆时针旋转,引脚编号依次从1到144,(所以Cortex-M4内核不是芯片,STM32芯片才是如下的芯片)
32bit是4GB,意味着存储器能访址4GB的存储空间,其中大部分地方已经被ARM公司分配好了,
我们打开前面说的数据手册,能够看到内存分块/数据线分类有AHB1,APB2等,而在AHB1上具有的引脚有GPIOI等,它对应的内存地址如0x4002 2000。而我们对寄存器进行操作的本质就是对该内存地址的内容进行对写操作的过程,
我们使用C语言的指针通过控制指定地址的内存来控制寄存器,一个简单的伪代码如下所示,
寄存器映射就是固件库实现的功能,使用别名访问寄存器;而存储器映射是给硬件如内存条/flash分配地址的过程,
x.2.6 寄存器编程
接下来我们对寄存器编程的原理进行讲解,这一过程就是书写固件库,在编程过程中我们最经常使用的就是参考手册。我们打开Reference manul
和C语言编辑器,来写对寄存器的访问和封装,我们下面先讲两种简单的思路,再进行总结,
1. 简单思路一: 我们可以使用C语言的#define关键字来依次定义常量,然后用别名来访问寄存器。我们首先打开参考手册,在第二章Memory map这一章节找到我们需要增加别名的外设的地址,如AHB1线上的GPIOF引脚,我们找到后定义它的GPIOF_BASE,
接下来我们找到第八章的GPIO registers,我们给寄存器增加别名。以GPIOx_MODER为例,我们找到其Address offset(相对偏移地址),我们在GPIOF_BASE的基础上增加相对偏移地址边便找到了GPIOF_MODER寄存器的真实地址,
我们将需要的寄存器找齐了之后,将代码整理如下,便实现了固件库中的寄存器映射:增加寄存器内存地址的别名,
2. 简单思路一: 当然上面一种方法明显效率较低,在观察数据结构后,我们发现一个GPIO的不同寄存器他们的组成形式都是类似的,有MODER,OTYPER等,我们可以使用typedef struct结构体进行封装,实现程序复用且更加易读,
于是我们再用#define进行定义别名,使用别名访问地址。使用结构体指针进行修改如GPIOF->ODR = 0XFFFF;
**3. 总结: ** 在规范流程下,我们首先找到总线地址,接着找到外设基基础,最终找到特定寄存器的地址。以上思路的实现方式分为下面两种,依次找到特定寄存器或者使用结构体,
使用结构体如下,
上面这些操作在固件库中都已经完成,所以使用固件库编程会方便很多,知其然,知其所以然。
x.2.7 KEIL MDK软件使用(可运行)
打开KEIL MDK,将先前的project关闭,new一个新的project,
找到需要存储的位置,给一个新的文件名,创建后保存,
查询我们的单片机是stm32f407zgt6, 1m FLASH,我们在新建面板选择和我们版本匹配的soc然后点击ok,
我们在project面板add new file,添加main.c
和stm32f4xx.h
,再将CMSIS中 startup.s 文件加入到工程文件中。除了.c文件之外的.h和.s文件都要手动添加,我们在Source Group中add exist,
文件类型默认只显示.c文件,我们将文件类型改为All files,添加我们需要的.h文件和.s文件,
添加完毕后,我们书写main.c
文件,为了跳过自检,我们需要创建一个SystemInit函数,然后我们点击build按钮,0 errors,
我们接下来将build好的工程下载到stm32上,按照前几节我们使用DAP下载文件,选择Port为SW,
在Flash Download中勾选Reset and Run,
然后我们将程序成功Load,
x.2.8 使用KEIL MDK软件从零开始点亮LED(可运行)
笔者使用的是启明欣欣的板子(因为需求能满足),首先打开板子的原理图,我们找到需要点亮的LED灯,以LED 0为例,他在2 PE3 LED0
这根引脚上,PE3就是GPIOE3引脚缩写,
我们再看LED灯的电路图,能够看到要点亮LED0灯,我们需要给LED0这个寄存器一个低电平,即给GPIOE3一个低电平,电子电路这块的知识可以看笔者这个blog https://blog.csdn.net/qq_43369406/article/details/135365797
。
我们查看板子的英文参考手册,找到AHB1总线上GPIOE引脚的地址是0x40021000开始,
我们再找到GPIOE_ODR的偏移地址是0x14,
结合偏移地址,于是我们找到了GPIOE的ODR寄存器的绝对地址,我们给这个地址的内存置为0,如下,
我们找到MODER寄存器的偏移地址,
结合偏移地址,我们找到了GPIOE的MODER寄存器的绝对地址,我们要修改它的PE3位置的数据,设置为寄存器写入。所以我们需要给这个地址的内存置为01,如下,
我们接着找RCC,打开GPIOE的时钟,
找到RCC的ENR寄存器,并把对应的GPIOE打开,
结合偏移地址我们进行代码书写,并将代码整理如下,
在魔术棒里的Code Generation arm compiler 要选择version 5,而且选上Use MicroLIB
然后我们Build,再load进stm32中,单片机的灯就被点亮了,
x.2.9 GPIO介绍
GPIO是通用输入输出端口的简称,我们对STM32进行单片机编程,实际上就是通过内存控制GPIO的过程,
我们查看引脚图,可以看到共有144个引脚,除了特别的几个电源引脚,其余都是GPIO引脚,而每个GPIO引脚可以通过功能复用,实现各种不同的功能,
查看英文数据手册中第三章节Pinouts and pin description可以查看引脚作用和复用说明,
例如Pin name为PE2的引脚,它的Alternate functions有TRACECLK/FSMC_A23指的是GPIOE2这个引脚可以复用的功能有TRACECLK/FSMC_A23,
除了ADC这个引脚是3.3V供电,其他都是5V供电。供电流向是电源到芯片,再从芯片到特定的引脚,
在手册的第七章我们能看到GPIO的功能框图,这一整块除了I/O引脚,其他部分都封装在芯片中,人是看不到的;输入是从引脚输入到芯片中,输出指的是从芯片输出到引脚上;
在GPIOx_MODER寄存器来决定是输入还是输出,当为输入时,经过TTL施密特触发器会将模拟信号离散化输出0,1再传入输入数据寄存器;
所以由此我们便对GPIO功能框图有了个大致的了解,而我们编程实际上操作的就是GPIO,下面这个图讲解的是GPIO初始化顺序,
x.2.10 GPIO功能框图
407有144个引脚,引脚供电大部分是5V,GPIO属于引脚,但并不是所有引脚都属于GPIO;查找每一个GPIO功能通过数据手册查找。
GPIO功能框图如下,
I/O引脚就是芯片和PCB印刷电路板的解除方式,而I/O引脚的左侧则是芯片的内部电路。
BSRR 指的是bit set reset register,其中set是指置位,是低16位,输出高电平置1。reset是指复位,是指高16位,输出低电平置1。
输入输出是相对ARM芯片而言的,如果往芯片写数据叫输入,从芯片往外写数据叫输出。
在输入中TTL使得输入模拟信号,当大于1.8V时为高电平,当低于1.8V时为低电平。
输入的输入较为简单,输出则较为复杂,输出的流程图如下,
x.2.11 书写stm32f4xx.h(可运行)
需要注意一个C语言语法,指针+1, 指向该指针的下一个“数据”。 如果是char指针,那么就前进一个字节,指针的值只加了1。 如果是int指针,那么就指向下一个整数,而一个整数是占4个字节的,所以指针的值,实际上被加了4。
下面这一段注释的代码是错误的,因为指针加0x01实际上是加32个bit,
我们在这一小节主要是要书写stm32fxx.h文件,这个文件实现将特定内存地址映射为一个常量变量,我们重新书写stm32f4xx.h如下,便于人的阅读,
#define RCC_BASE ( unsigned int )0x40023800
#define GPIOE_BASE ( unsigned int )0x40021000
#define RCC_AHB1ENR *( unsigned int *)(RCC_BASE + 0x30)
#define GPIOE_MODER *( unsigned int *)(GPIOE_BASE + 0x00)
#define GPIOE_ODR *( unsigned int *)(GPIOE_BASE + 0x14)
main.c如下,
#include "stm32f4xx.h"
/*
* 注意事项:
* 要在 Options for target 选项里面的 Use MicroLIB 这个勾选上
* 这样才能执行C文件里面的 main 函数
*/
int main(void)
{
/* Add your code */
/* 1. 开GPIO端口时钟 */
RCC_AHB1ENR |= ( 0x01 << 4 );
/* 2. 配置 GPIO 为输出 */
GPIOE_MODER &= ~( 0x03 << (2 * 3) );
GPIOE_MODER |= ( 0x01 << (2 * 3) );
/* 3. ODR 输出低电平 */
GPIOE_ODR &= ~( 0x01 << 3);
return 0;
}
void SystemInit(void)
{
/* */
}
文件的组织形式就和x.2.7一样。
x.2.12 固件库
固件库的作用,便是将寄存器编程中的寄存器地址一步一步过渡到固件库。我们在书写stm32f4xx.h实际上就是在书写固件库。通过观察GPIO的十个寄存器,我们发现它的偏移地址是连续的,结构体中成员也是连续的内存地址定义,于是我们使用结构体来增加可读性,如下,
同时我们可以新建一些.c,.h文件,将这些文件书写新的函数,如此便达到了固件库的目的。
x.3 案例——使用单片机点亮LED灯案例
x.3.1 51单片机
单片机中最基础的入门级别单片机就是51单片机,51单片机相较于stm32单片机更容易上手,因为51单片机内部已经实现了寄存器映射,所以在这里可以直接使用寄存器别名来进行访问。
电流方向永远是从正极流向负极,从高压流向低压(因为存在电势差),电子流动方向和电流流向相反。我们可以很快速地用51单片机来点亮LED灯,如果LED灯的电路图如下,则只需要控制P0,0端口将数值设置为0便可以将电路点亮,
则代码如下便可实现LED灯的开关,
x.3.2 stm32f103
寄存器映射指给寄存器地址映射一个别名,这个功能可以通过reg52.h
和stm32f10x.h
两个文件来实现。
LED灯对应的接口为PB0,则意味着是GPIOx_ODR
寄存器中的GPIOB0_ODR
。
GPIOx_ODR是指 general purpose intput output x _ Output data register,是通过ODR来控制LED灯开关的。
- SOC厂商在已经有ARM芯片基础上设计了三类地址总线,为AHB,APB2,APB1总线。我们先在参考手册中
第二章 存储器和总线架构
中找到挂载在APB2地址总线下的GPIOB的绝对地址,
- 我们需要ODR来控制LED灯,所以我们需要找到ODR0的绝对地址,我们根据地址偏移来计算,
- 我们使用ODR0来控制PB0的端口,看了电路图我们知道要实现LED中G颜色的开关,我们需要将PB0的端口电压设置为0V,这个时候我们即将PB0设置为0便可,
-
我们需要通过CRL寄存器来告诉MCU, LED中的PB0为输出值,即配置IO口为 输出。
-
打开RCC的时钟寄存器。
最终我们的代码书写如下,
x.3.3 stm32f407
- 更改GPIOx_MODER模式寄存器为输出
AHB1下
- 更改RCC时钟控制器
AHB1下
- 更改GPIOx_ODR数据寄存器
AHB1下
代码如下,stm32f4xx.h
文件内容,
main.c
文件内容,
x.4 Q&A::可能会遇到的问题
x.4.1 CMSIS-DAP-Cortex-M Error
CMSIS-DAP-Cortex-M Error会报错SWD/JTAG Communication Failure。可能是你在烧录的时候,STM32的板子没给供电,即在给电脑连上板子后,还得有一个电源给板子供电。
如果仍然报错可以试试这个方法https://blog.csdn.net/m0_53841203/article/details/122531754
peripheral - 外设