第1章.STM32单片机入门知识介绍
1.1什么是嵌入式系统?
嵌入式系统是以应用为中心、以计算机技术为基础、软硬件可裁剪、对功能、可靠性、成本、体积和功耗有严格要求的专用计算机系统。
嵌入式系统和通用计算机系统都属于计算机系统。如图。从系统组成上它们都是软件和硬件组成,工作原理也相同。从硬件上看,两者都是由CPU、存储器、I/O接口和中断系统等部件组成;从软件上看,两者都可以换分为系统软件和应用软件。

嵌入式系统和通用计算机系统也是有很多不同点

1.2嵌入式系统的应用领域
广泛应用于工业控制,交通管理,信息家电,环境工程等

1.3 单片机基本概念
单片机,全称单片微型计算机(Single-Chip Microcomputer),是指集成在一个芯片上的微型计算机。这种计算机系统的体积较小,内部包含中央处理器CPU、随机存储器RAM、只读存储器ROM、定时/计数器和多种I/O接口电路,并在一块集成电路芯片上集成了微型计算机的各种功能部件,具有高性能、低价格的优点。国内经常使用“单片机”这个称呼,国外通常称为“微控制器”,英文缩写MCU(Microcontroller Unit)。
1.4 ARM简介
1.4.1 ARM公司介绍
ARM,全称为Advanced RISC Machines,既指ARM公司,也指ARM处理器内核。ARM公司是全球领先的半导体知识产权(IP)提供商,是一家在微处理器设计领域具有重要影响力和广泛应用的公司,ARM的总部位于英国的剑桥。
ARM设计了大量高性能、廉价、低功耗的RISC(Reduced Instruction Set Computer,精简指令集计算机)处理器及芯片。ARM作为知识产权供应商,本身不直接从事芯片的生产,其将知识产权授权给世界各大半导体厂商后,各大生产商再根据不同的应用领域,加入适当的外围电路,形成自己的ARM微处理器芯片。ARM架构其高效、节能的特性,使得它非常适合在移动设备和嵌入式系统中使用,它的技术广泛应用于各种嵌入式系统设计,从无线通讯、网络和消费娱乐产品到成像、汽车、安全和存储解决方案等领域。ARM架构已经成为当今使用最广泛的32位嵌入式RISC指令集架构,ARM公司已经成为全球RISC标准的缔造者。目前全球超过95%的智能手机和平板电脑都采用了ARM架构。
1.4.2 ARM处理器简介
ARM公司开发了很多系列的处理器内核,以前都是以ARMx(x是数字)进行命名,比如经典的ARM7,ARM9,ARM11等。在早期,还在数字后面加字母后缀,用来进一步明确该处理器支持的特性。比如,ARM7TDMI,T代表Thumb指令集,D代表支持JTAG调试(Debugging),M代表快速乘法器,I代表一个嵌入式ICE模块。
到了ARMv7架构时代,ARM改革了一度使用的冗长命名方法,转为另一种看起来比较整齐的命名法:按照应用等级分成3个类别,并以Cortex作为前缀,而且每一个大的系列又换分若干小的系列。ARM体系结构与ARM内核的对应关系如图
ARM架构进化史

ARM的Cortex系列处理器分为三种:Cortex-A、Cortex-R和Cortex-M,每种系列都有其特定的使用场景。
Cortex-A系列处理器:主要用于高性能计算设备,如智能手机、平板电脑、个人电脑和服务器等。它们通常具有较高的时钟频率和更大的存储容量,面向尖端的基于虚拟内存的操作系统和用户应用,因此也被称为应用程序处理器。
Cortex-R系列处理器:专为实时应用程序设计,如实时嵌入式系统中的自动驾驶、工控系统和医疗设备等。这些处理器针对实时系统,面向深层的嵌入式实时应用,能够处理需要快速响应和高可靠性的任务。
Cortex-M系列处理器:专为嵌入式系统设计,用于低功耗、实时控制和物联网设备。它们被广泛应用于各种应用,包括智能家居、汽车电子、医疗设备、工业自动化等领域。Cortex-M系列处理器具有低功耗、高性能、实时性、易于开发等特点,能够满足微控制器领域对于快速且具有高确定性的中断管理,以及低功耗和低成本的需求。
目前市场上比较流行的几大系列微处理器,按性能、功能和处理能力划分如图

STM32F103型号属于cortex-M3系列。
1.5 STM32简介
1.5.1基于Cortex内核的MCU
ARM-Cortex处理器内核是微控制器的中央处理单元(CPU)。完整的基于Cortex的MCU(MicroController Unit)还需要很多其他组件。在芯片制造商得到Cortex处理器内核的使用授权后,它们就可以把Cortex内核用在自己的硅片设计中,添加存储器、外设、I/O及其他功能块,即为基于Cortex的微控制器。不同厂家设计出的MCU会有不同的配置,包括存储器容量、类型、外设等都各具特色。以STM32的Cortex-M3内核为例,Cortex-M3内核和基于Cortex-M3的MCU关系如图。

1.5.2 什么是stm32
STM32是意法半导体(STMicroelectronics)公司推出的32位ARM Cortex-M内核微控制器系列。从字面上来理解,ST代表意法半导体,M是Microelectronics的缩写,而32则表示32位。
1.5.3 stm32产品线介绍

STM32还可以用于无人机和机器人的控制系统开发,提供高性能的实时控制和传感器处理能力。同时,它也可以用于开发各种嵌入式设备,如测试仪器、智能卡等。
但stm32也存在局限,如在程序代码较大,基于linux或android,需要极高计算能力的场景下,使用起来不适合。

根据上表,可见STM32 主要分两大类,MCU 和 MPU,MCU 就是我们常见的STM32微控器,不能跑 Linux,而 MPU 则是 ST 在 19 年才推出的微处理器,可以跑 Linux。
由于 STM32 系列有很好的兼容性,我们只要能够熟练掌握其中一任何一款 MCU,就可以很方便的学会并使用其他系列的 MCU。比如学好了 STM32F103,再去学 F4/F7/H7 就比较容易学会,由于 STM32F103 系列最早推向市场,资料和教程都是最多的,在市场上的使用也是最为广泛,所以对于没有接触过 STM32 的初学者来说,建议先学习 STM32F103,再去学习其他的 STM32 系列。
STM32 的产品名字里面包含了:家族、类别、特定功能、引脚数、闪存容量、封装、温度范围等重要信息,这些信息可以帮助我们识别和区分 STM32 不同芯片。

第2章.STM32开发C语言常用知识点
2.1. STM32嵌入式开发C语言编程的不同

根据嵌入式的需要,stm32应用于各种场景,需要自己设计一套c语言来适配。
2.2. C语言常用知识点
常见的位操作运算符有&(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)、>>(右移)。建议学习stm32前补习一下c的知识。
2.2.1.如何改变某些特定位,而不改变其他位?
GPIOA->CRL &= 0XFFFFFF0F; /* 将高 4~7位清 0 */
/*然后再与需要设置的值进行|或运算:*/
GPIOA->CRL |= 0X00000040; /* 设置相应位的值(4),不改变其他位的值 */
2.2.2.移位操作还提高代码的可读性
SysTick->CTRL |= 1 << 1; /*这是delay库函数里面的一段代码*/
SysTick->CTRL |= 0X0002; /*上面与下面对比可以看见,上面的代码逻辑是设置最低位为1,向左高位移动一位,让第二位置1。我们还可以修改左移值,改变想要置1的位置*/
2.2.3.~按位取反操作使用技巧
SysTick->CTRL &= ~(1 << 0) ; /* 关闭 SYSTICK 注意这里寄存器的初始值为1*/
SysTick->CTRL &= 0XFFFFFFFE; /* 关闭 SYSTICK 上面的代码,可以对想要清零的操作位进行设置*/
2.2.4.^按位异或操作使用技巧
GPIOB->ODR ^= 1 << 5;/*让操作位状态翻转,常用于LED闪烁*/
2.2.5.define 宏定义
define 是 C 语言中的预处理命令,它用于宏定义(定义的是常量),后续如果想修改π的值,可以直接在宏定义的地方修改,
#define PIE 3.14159f //"标识符"为所定义的宏名;"字符串"可以是常数、表达式、格式串等
2.2.6.条件编译
2.2.6.1.#ifdef
当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句,这种语句常常在嵌入式中使用。其中#else 部分也可以没有。
#ifdef 标识符
程序段 1
#else
程序段 2
#endif
2.2.6.2.#ifndef
#ifndef SOME_MACRO
// 如果 SOME_MACRO 没有被定义,则编译以下代码
#endif
2.2.6.2.#if !defined
#if !defined(SOME_MACRO)
// 如果 SOME_MACRO 没有被定义,则编译以下代码
#endif
这也是检查是否没有定义某个宏的方法,但它使用了!defined操作符.
2.2.7 extern 变量声明
C 语言中 extern 可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。但注意,对于extern声明变量可以多次,但定义只有一次。
extern uint16_t speed_x;
字节=XX
半字=XXXX
字=XXXXXXXX
单片机内部的flash可擦除,常用于代码泄露保护
2.2.8 typedef 类型别名
typedef 用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义,例如C99标准中引入的头文件,定义了一组具有固定宽度的整数类型,包括有符号和无符号的8位、16位、32位和64位整数。这些类型分别命名为int8_t、int16_t、int32_t、int64_t(以及对应的无符号类型uint8_t、uint16_t、uint32_t、uint64_t)。在STM32F10x的标准库函数stm32f10x.h中又对这些数据类型进行了重新定义,代码如下:
typedef int32_t s32;
typedef int16_t s16;
typedef int8_t s8;
typedef uint32_t u32;
typedef uint16_t u16;
typedef uint8_t u8;
typedef在 MDK 用得最多的就是定义结构体的类型别名和枚举类型了。
struct _GPIO
{
__IO uint32_t CRL;
__IO uint32_t CRH;
…
};
定义了一个结构体 GPIO,这样我们定义结构体变量的方式为:
struct _GPIO gpiox; /* 定义结构体变量 gpiox */
但是这样很繁琐,MDK中有很多这样的结构体变量需要定义。这里我们可以为结体定义一个别名GPIO_TypeDef,这样我们就可以在其他地方通过别名GPIO_TypeDef来定义结构体变量了,方法如下:
typedef struct
{
__IO uint32_t CRL;
__IO uint32_t CRH;
…
} GPIO_TypeDef;
Typedef为结构体定义一个别名GPIO_TypeDef,这样我们可以通过GPIO_TypeDef来定义结构体变量:
GPIO_TypeDef gpiox;
这里的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了,但是 GPIO_TypeDef 使用起来方便很多。
2.2.9结构体
在C语言中,结构体(struct)是一种用户自定义的数据类型,它允许你将不同类型的数据项组合成一个单独的数据结构。结构体可以用来表示一个具有复杂属性的实体,比如一个人(具有姓名、年龄、性别等属性)或者一本书(具有书名、作者、出版日期等属性)。
2.2.9.1 结构体的声明和定义
/*声明结构体类型: */
struct 结构体名
{
成员列表;
}变量名列表;
你可以在声明结构体的时候直接创建结构体变量,也可以先定义结构体类型,然后再创建变量,如下面几种方式都是可以的:
// 直接定义并创建结构体变量
struct {
int age;
char name[50];
} person1;
// 直接定义并创建结构体变量
struct Person{
int age;
char name[50];
} person2;
// 先定义结构体类型,再创建变量
struct Person {
int age;
char name[50];
};
struct Person person3;
2.2.9.2 结构体的声明和定义
要访问结构体变量的成员,你需要使用.运算符(对于结构体变量)或->运算符(对于指向结构体的指针)。
/*接前面章节2.2.9.1的示例代码*/
// 访问结构体变量的成员
person1.age = 25;
// 如果有一个指向结构体的指针
struct Person *ptr = &person2;
ptr->age = 30;
2.2.9.3 结构体的作用
在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如串口,它的初始化状态是由几个属性来决定的,比如串口号,波特率,极性,以及模式。对于这种情况,在我们没有学习结构体的时候,我们一般的方法是:
void usart_init(uint8_t usartx, uiut32_t BaudRate, uint32_t Parity,
uint32_t Mode);
这种方式是有效的同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函数里面再传入一个/几个参数,那么势必我们需要修改这个函数的定义,重新加入新的入口参数,随着开发不断的增多,那么是不是我们就要不断的修改函数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢?
我们使用结构体参数,就可以在不改变入口参数的情况下,只需要改变结构体的成员变量就可以达到改变入口参数的目的。
结构体就是将多个变量组合为一个有机的整体,上面的函数usartx,BaudRate,Parity,Mode等这些参数,他们对于串口而言,是一个有机整体,都是来设置串口参数的,所以我们可以将他们通过定义一个结构体来组合在一个。MDK中是这样定义的:
typedef struct
{
uint32_t BaudRate;
uint32_t WordLength;
uint32_t StopBits;
uint32_t Parity;
uint32_t Mode;
uint32_t HwFlowCtl;
uint32_t OverSampling;
} UART_InitTypeDef;
这样,我们在初始化串口的时候入口参数就可以是 USART_InitTypeDef 类型的变量或者指针变量了,于是我们可以改为:
void usart_init(UART_InitTypeDef *huart);
这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义就可以达到增加变量的目的。
在以后的开发过程中,如果你的变量定义过多,如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可以提高你的代码的可读性。使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。
2.2.9.4 结构体成员的内存分布与对齐
- 声明一个结构体类型的时候是没有为它分配任何存储空间的,只有在定义结构体变量的时候,才会为变量分配存储空间。
- 结构体中可以有不同的数据类型成员,成员在定义时依次存储在内存连续的空间中,结构体变量的首地址就是第一个成员的地址,内存偏移量就是各个成员相对于第一个成员地址的差(即,把低位内存分配给最先定义的变量)。
- 理论上,结构体所占用的存储空间是各个成员变量所占的存储空间之和,但是为了提高CPU的访问效率,采用了内存对齐方式:
- ①结构体的每一个成员起始地址必须是自身类型大小的整数倍,若不足,则不足部分用数据填充至所占内存的整数倍。
- ②结构体大小必须是结构体占用最大字节数成员的整数倍,这样在处理数组时可以保证每一项都边界对齐根据上面的说明,我们举例子分析如下:
struct test
{
char a;
int b;
float c;
double d;
}mytest;
这个结构体所占用的内存怎么算呢?理论结果为17,实际上并不是17,而是24。为什么会这样呢?这个就是前面我们说的内存对齐。
char型变量占1个字节(并不是4的倍数),所以它的起始地址是0。int类型占用4个字节,它的起始地址要求是4的整数倍数,那么内存地址1、2、3就需要被填充(被填充的内存不适于变量),b从4开始。float类型也是占用4个字节,起始地址要求是4的倍数,所以c的起始地址就是8。double类型变量占用8个字节,起始地址为16,12~15被填充。这里,第一个成员a的地址首地止,第二个成员b的偏移量为4,第三个成员c的偏移量是8,以此类推,是如下图所示:

2.2.10 关键字
在STM32的一些库函数头文件中,经常会看到如下代码, 表示将 volatile 或者 volatile const 来代替某一个符号。
#define __I volatile
#define __O volatile
#define __IO volatile
#define __IM volatile const
#define __OM volatile
#define __IOM volatile
2.2.10.1 volatile
volatile 表示强制编译器减少优化,告诉编译器必须每次去内存中取变量值。如果程序改变了内存里面的值,寄存器里的值却未改变,加了volatile可以告诉编译器去内存中取改变值,这样确保了数据的准确性,却降低了效率。
2.2.10.2 const
const称为常量限定符,限制变量只读。const修饰的变量存储在只读数据段,在程序结束时释放,而const局部变量存储在栈中,代码块结束时释放。用const定义变量时就要初始化该变量:
const int a = 1;
2.2.10.3 static
这个叫做静态变量,存储在全局区(也叫静态区),如果声明变量时使用未赋值,那么编译器就会自动赋初值为0。
在函数内被static声明的变量,仅能在本函数中使用,也叫静态局部变量。
在文件内(函数体外)被static声明的变量,仅能被本文件内的函数访问,不能被其他文件中的函数访问,也叫静态全局变量。
静态全局变量和普通的全局变量不同,静态全局变量仅限于本文件中使用,在其它文件中可以定义一个与静态全局变量名字相同的变量。普通的全局变量可以通过extern外部声明后被其他文件使用,也就是整个工程可见,而且其他文件不能再定义一个与普通全局变量名字相同的变量了。
局部静态变量
void func() {
static int count = 0; // 只在整个程序开始时初始化一次
count++;
printf("%d\n", count);
}
全局静态变量
// file1.c
static int file_scope_var = 42; // 只能在file1.c中访问
// file2.c
extern int file_scope_var; // 错误:无法在其他文件中访问file_scope_var
静态函数
当在文件级别使用static关键字声明一个函数时,该函数将具有内部链接,即它只能在其定义的文件内被调用。这提供了另一种封装机制,允许你隐藏函数的实现细节,只暴露需要被其他文件使用的函数。
// file1.c
static void internal_function() {
// ...
}
// file2.c
extern void internal_function(); // 错误:无法在其他文件中调用internal_function
静态初始化:
指针
在STM32这样的嵌入式系统开发中,指针的使用与底层硬件的联系尤为密切。STM32库开发中,我们对寄存器进行了封装,将寄存器放入到结构体(如GPIOX)当中。通过指针,我们可以指向这些结构体的地址,从而访问和操作寄存器,完成对寄存器的配置。
STM32F1x系统架构及资源介绍
补充
什么是轮询?
https://blog.csdn.net/superSmart_Dong/article/details/134065816
微机原理入门
https://blog.csdn.net/2401_84247058/article/details/141968925
https://www.bilibili.com/read/cv18440288
byte
https://blog.csdn.net/ChineseSoftware/article/details/122533596
