# STM32 入门教程
# keil5 MDK
是用来给 ARM 系统的单片机编程的
# STM32 的介绍
- STM32(也是一个单片机)是 ST 公司基于 ARM Cortex-M 内核开发的 32 位微控制器
- STM32 常应用在嵌入式领域,如智能车、无人机、机器人、无限通信、物联网、工业控制、娱乐电子产品等
- STM32 功能强大、性能优异、片上资源丰富、功耗低、是一款经典的嵌入式微控制器
STM32 家族
片上资源 / 外设(Peripheral)
英文缩写 | 名称 | 英文缩写 | 名称 |
---|---|---|---|
NVIC | 嵌套向量中断控制器 | CAN | CAN 通信 |
SysTick | 系统滴答定时器 | USB | USB 通信 |
RCC | 复位和时钟控制 | RTC | 实时时钟 |
GPIO | 通用 IO 口 | CRC | CRC 校验 |
AFIO | 复用 IO 口 | PWR | 电源控制 |
EXTI | 外部中断 | BKP | 备份寄存器 |
TIM | 定时器 | IWDG | 独立看门狗 |
ADC | 模数转换器 | WWDG | 窗口看门狗 |
DMA | 直接内存访问 | DAC | 数模转换器 |
USART | 同步 / 异步串口通信 | SDIO | SD 卡接口 |
I2C | I2C 通信 | FSMC | 可变静态存储控制器 |
SPI | SPI 通信 | USN OTG | USN 主机接口 |
# 软件安装
安装:
- 安装 Keil5 MDK
- 安装器件支持包
- 软件注册
- 安装 STLINK 驱动
- 安装 USB 转串口驱动
# 新建工程
STM32 的开发方式(主要有):
-
基于寄存器的方式
就是用程序直接配置寄存器,是最底层、最直接、效率会更高一些
但是由于 STM32 的结构复杂、寄存器太多,所以基于寄存器的方法目前是不推荐的
-
基于标准库的方式(即库函数的方式)
是使用 ST 官方提供的封装好的函数,通过调用这些函数来间接配置这些寄存器
由于 ST 对寄存器封装的比较好,所以这种方式既能满足对寄存器的配置,对开发人员也比较友好,有利于提高开发效率
-
基于 HAL 库的方式
可以用图形话界面快速配置 STM32,比较适合快速上手 STM32
但是这种方法隐藏了底层逻辑
# 过程
-
先建立一个存放工程的文件夹(比如在 D 盘、E 盘)
以后的工程都存放在这个文件夹,方便管理
-
点击进入软件
再点击 Project, 并选择 New uVision Project...
-
选择我们刚才建立的文件夹
-
给你的项目命名,再点保存
-
选择你所要项目的型号,再点 OK
-
弹出的这个界面我们暂时还用不上,先点击右上角的❌
现在项目就建好了,但是这里的工程文件是空空如也,还不能直接用,需要给它添加一些必要的文件
C:\Users\22731\Desktop\STM32\ 固件库 \STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm
根据这些路径找到这个界面
这些文件就是 STM32 的执行文件,把文件全部复印下来,然后回到工程模板文件夹里,可以看到这些就是我们刚才新建工程自动生成的文件
如果直接粘贴到模板文件夹里,就会太乱,可以新建一个文件夹,然后将启动文件粘贴到这个新的文件夹里面
并将 STM32F10x 文件夹中的这三个文件也粘贴到模板文件夹中
因为 STM32 是内核和内核外围的设备组成的,而且这个内核的寄存器描述和外围设备的描述文件不是再一起的,所以我们还需要添加一个外围设备的描述文件
这两个文件就是内核寄存器的描述,将这两个文件也粘贴到模板文件夹中
必要文件复制完成
回到 Keil5 软件,把刚才复制的文件添加到工程里,将文件改名为 Start,
接着点右键,选择 Add Files to Group 'Start', 点击进入 Start
将文件类型改为 All files (* . *), 再选择一个启动文件添加(注:只能选一个启动文件),这里我们选择以.md.s 结尾的文件,点击 Add, 然后将剩下所有的.h 和.c 文件添加进来,最后点击 Close
这样,Start 文件夹里面的文件就添加好了(这些带钥匙的文件是只读文件,是不允许我们修改的)
最后,我们还需要在工程选项里添加上这个文件夹的头文件路径,要不然软件是找不到.h 文件的
点击魔术棒按钮,打开工程选项,在 C/C++ 里,点 Include Paths 右边的三个小点,
点击这个小方框,在点击这三个小点
选择 Start 文件夹,再点击 OK
接下来我们再新建一个 main 函数,看看这个工程是不是可行
打开工程文件夹,再新建一个文件夹
在 User 上点击右键,再选择 Add New Item to Group 'User'
选择 C 文件,名字为 main,路径选择到 User, 再点击 Add
这样我们就拥有 main.c 文件了
再 main.c 文件中,右键,选择 Inster '#include file', 再选择 stm32f10x.h (插入头文件)
STM32F1 系列中的型号分类及缩写
缩写 | 释义 | Flash 容量 | 型号 |
---|---|---|---|
LD_VL | 小容量产品超值系列 | 16~32K | STM32F100 |
MD_VL | 中容量产品超值系列 | 64~128K | STM32F100 |
HD_VL | 大容量产品超值系列 | 256~512K | STM32F100 |
LD | 小容量产品 | 16~32K | STM32F101/102/103 |
MD | 中容量产品 | 64~128K | STM32F101/102/103 |
HD | 大容量产品 | 256~512K | STM32F101/102/103 |
XL | 加大容量产品 | 大于 512K | STM32F101/102/103 |
CL | 互联型产品 | - | STM32F105/107 |
# GPIO 通用输入输出口
# GPIO 简介
- GPIO(General Purpose Input Output)通用输入输出口
- 可配置为 8 种输入输出模式
- 引脚电平:0V~3.3V,部分引脚可容忍 5V
- 输出模式下可控制端口输出高低电平,用以驱动 LED、控制蜂鸣器、模拟通信协议输出时序等
- 输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC 电压采集、模拟通信协议接收数据等
在每个 GPIO 模块内,主要包含了寄存器和驱动器这些东西
寄存器:就是一段特殊的存储器,内核可以通过 APB2 总线对寄存器进行读写
# GPIO 模式
通过配置 GPIO 的端口配置寄存器,端口可以配置成以下 8 种模式
模式名称 | 性质 | 特征 |
---|---|---|
浮空输入 | 数字输入 | 可读取引脚电平,若引脚悬空,则电平不确定 |
上拉输入 | 数字输入 | 可读取引脚电平,内部连接上拉电阻,悬空时默认高电平 |
下拉输入 | 数字输入 | 可读取引脚电平,内部连接下拉电阻,悬空时默认低电平 |
模拟输入 | 模拟输入 | GPIO 无效,引脚直接接入内部 ADC |
开漏输出 | 数字输出 | 可输出引脚电平,高电平为高阻态,低电平接 VSS |
推挽输出 | 数字输出 | 可输出引脚电平,高电平接 VDD,低电平接 VSS |
复用开漏输出 | 数字输出 | 由片上外设控制,高电平为高阻态,低电平接 VSS |
复用推挽输出 | 数字输出 | 由片上外设控制,高电平接 VDD,低电平接 VSS |
注:
- 模拟输入可以说是 ADC 模数转化器的专属配置
- 在这 8 种模式中,除了模拟输入这个模式会关闭数字的输入功能,在其他 7 种模式中,所有的输入都是有效的
# STM32 外部的设备和电路
# LED 和蜂鸣器简介
**LED:** 发光二极管,正向通电点亮,反向通电不亮
注:
如果是引脚没有被剪过的 LED,那其中长脚是正极,短脚是负极
通过 LED 内部也可以看正负极,这里较小的一半是正极,较大的一半是负极
** 有源蜂鸣器:** 内部自带振荡源,将正负极接上直流电压即可持续发声,频率固定
** 无源蜂鸣器:** 内部不带振荡源,需要控制器提供振荡脉冲才可发声,调整提供振荡脉冲的频率,可发出不同频率的声音
# LED 闪烁 & LED 流水灯 & 蜂鸣器
首先需要搭建一个面包板,按照图示将相关的硬件安装好,再在 keil5 MDK 中创建一个项目 (具体操作看新建工程)
将默认的组(Groups:Source Group 1)删掉,再点击 New (Insert) 新建三个组,分别为 Start,Library,User
然后对这三个组添加对应的文件
Start:
Library () 全选:
User:
全部添加好后,点击 OK 键
接着点击魔术棒按钮,打开工程选项,选择 C/C++,在 Include Paths 中添加好路径,并在 Define 中填好 USE_STDPERIPH_DRIVER
[](https://postimg.cc/FYKjHbmn)
然后点击 Debug,调试器选择 ST-Link,再点 seettings
点击 Flash Download,勾选 Reset and Run, 然后确定,OK
这样工程选项就配置好了
接着打开 main.c, 将里面原来的代码全部删掉
# LED 闪烁
操作 STM32 的 GPIO 总共需要 3 个步骤:
- 使用 RCC 开启 GPIO 的时钟
- 使用 GPIO_Init 函数初始化 GPIO
- 使用输入或者输出的函数控制 GPIO 口
** 涉及到了两个外设:**RCC 和 GPIO
灯亮:
灯灭:
LED 闪烁:
为了实现 LED 闪烁,我们需要在主循环里,写上 **‘’点亮 LED,延时一段时间,熄灭 LED,延时一段时间‘’** 的逻辑
准备工作:
点击三个盒子的图标,点击方框,添加新组,再将文件里的文件全部添加进来,最后点击 OK
最后点击魔术棒,选择 C/C++,将 System 的源路径添加进来,就 OK 了
代码:
# 研究推挽输出和开漏输出的驱动问题
首先,将 LED 拔掉,然后把长脚插到 PA0 口,短脚插到负极,这样 LED 就是高电平点亮方式(也能正常闪烁,代码看上图)
说明在推挽模式下,高低电平都是有驱动能力的
改成开漏模式:
这时,我们就可以看到 LED 不亮了,现在 LED 还是高电平点亮的方式,LED 不亮,说明开漏输出的模式高电平是没有驱动能力的
我们再把 LED 该为低电平的方式,可以看到 LED 又亮起来了,说明开漏输出的模式低电平是有驱动能力的
# LED 流水灯
接线图:
需要拿出 8 个 LED 灯,正极都插在正极的供电口,负极依次插到 PA0 到 PA7 的端口
在代码中引用引脚,有两种方式:
为了同时控制 16 个端口,我们可以使用 GPIO_Write 这个函数
这样就是第一个灯点亮,其他灯都熄灭了
代码:
执行之后,可以看到 LED 依次点亮
# 蜂鸣器
接线图:
VCC 正极接到正极供电口,GND 负极接到负极供电口,I/O 控制极随便选择一个 IO 口接上及可(示列选择 PB12)
注意:A15、B3、B4 这三个口不能选,这三个口是 JTAG 的调试端口,如果要用作普通端口的话,还需要进行一些配置
给 PB12 输出低电平,蜂鸣器就会响,输出高电平,蜂鸣器就不响
代码:
还可以换个响的方式
# GPIO 输入
# 按键简介
-
按键:常见的输入设备,按下导通,松手断开
-
按键抖动(在单片机中存在):由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动
这个我们在程序中需要过滤一下(最简单的过滤方法就是加一段延时,把这个抖动时间耗过去)
# 传感器模块简介
传感器模块:传感器元件(光敏电阻 / 热敏电阻 / 红外接收管等)的电阻会随外界模拟量的变化而变化,通过与定值电阻分压即可得到模拟电压输出,再通过电压比较器进行二值化即可得到数字电压输出
滤波电容:它是为了给中间的电压输出进行滤波的,用来滤除一些干扰,保证输出电压波形的平滑
注意:一般我们在电路中遇到这种一段接在电路中,另一端接地的电容,都可以考虑一下这个是不是滤波电容的作用(如果是则可以在分析电路的时候把这个电容抹掉)
LM393:是一个电压比较器芯片,里面有两个独立的电压比较器电路,有时可以当作是一个运算放大器
# 按键的四种接法:
一般我们的按键都是用下接的方式
下接按键按下时引脚是低电平,松手是高电平,上接则相反
(1)这两种接法要求引脚必须是上拉或下拉输入的模式
(2)这两种接法可以允许引脚是浮空输入的模式,因为已经外设了上拉电阻和下拉电阻
# C 语言数据类型
关键字 | 位数 | 表示范围 | stdint 关键字 | ST 关键字 |
---|---|---|---|---|
char | 8 | -128 ~ 127 | int8_t | s8 |
unsigned char | 8 | 0 ~ 255 | uint8_t | u8 |
short | 16 | -32768 ~ 32767 | int16_t | s16 |
unsigned short | 16 | 0 ~ 65535 | uint16_t | u16 |
int | 32 | -2147483648 ~ 2147483647 | int32_t | s32 |
unsigned int | 32 | 0 ~ 4294967295 | uint32_t | u32 |
long | 32 | -2147483648 ~ 2147483647 | ||
unsigned long | 32 | 0 ~ 4294967295 | ||
long long | 64 | -(2^64)/2 ~ (2^64)/2-1 | int64_t | |
unsigned long long | 64 | 0 ~ (2^64)-1 | uint64_t | |
float | 32 | -3.4e38 ~ 3.4e38 | ||
double | 64 | -1.7e308 ~ 1.7e308 |
# C 语言宏定义
-
关键字:#define
-
** 用途:** 用一个字符串代替一个数字,便于理解,防止出错;提取程序中经常出现的参数,便于快速修改
-
定义宏定义:
#define ABC 12345
-
引用宏定义:
int a = ABC; // 等效于 int a = 12345;
# C 语言 typedef
-
** 关键字:**typedef
-
** 用途:** 将一个比较长的变量类型名换个名字,便于使用
-
定义 typedef:
typedef unsigned char uint8_t;
-
引用 typedef:
uint8_t a; // 等效于 unsigned char a;
typedef 和宏定义的区别:
- 宏定义的新名字在左边,typedef 的新名字在右边
- 宏定义不需要分号,typedef 后面必须加分号
- 宏定义任何名字都可以换,而 typedef 只能专门给变量类型换名字,所以宏定义的改名范围要更宽一些,只不过对变量类型重命名而言,所以 typedef 更加安全
# C 语言结构体
-
** 关键字:**struct
-
** 用途:** 数据打包,不同类型变量的集合
-
定义结构体变量:
struct{char x; int y; float z;} StructName;
因为结构体变量类型较长,所以通常用 typedef 更改变量类型名
-
引用结构体成员:
StructName.x = 'A';
StructName.y = 66;
StructName.z = 1.23;
或 pStructName->x = 'A'; //pStructName 为结构体的地址 pStructName->y = 66;
pStructName->z = 1.23;
# C 语言枚举
-
** 关键字:**enum
-
** 用途:** 定义一个取值受限制的整型变量,用于限制变量取值范围;宏定义的集合
-
定义枚举变量:
enum{FALSE = 0, TRUE = 1} EnumName;
因为枚举变量类型较长,所以通常用 typedef 更改变量类型名
-
引用枚举成员:
EnumName = FALSE;
EnumName = TRUE;
# 按键控制 LED & 光敏传感器控制蜂鸣器
# 按键控制 LED
接线图:
新建一个文件夹,命名为 Hardware (用来存放硬件驱动)
点击三个箱子的按钮,打开工程管理,新建一个组,也叫 Hardware,点击 OK
在点开魔术棒按钮,打开工程选项,选择 C/C++,在源路径中将我们刚刚创建的 Hardware 添加进来,点击 OK, 这样 Hardware 就成功的添加了进来
在 Hardware 点击右键,选择 Add New Item to Group 'Hardware', 选择.c 文件,Name 命名为 LED,路径选择 Hardware,点击 Add
同理,创建一个.h 文件
LED.c 和 LED.h 用来封装 LED 的驱动程序,LED.c 用来存放驱动程序的主体代码,LED.h 用来存放这个驱动程序用来对外提供的函数或变量的说明
在 LED.c 和 LED.h 中写好这样一段
接下来我们开始封装一下 LED 的代码
测试:
运行之后,可以看到两个 LED 都亮起来了,说明我们端口配置和模块化编程是没有问题的
因为 GPIO 配置好了之后默认就是低电平,所以我们还没操作 LED,LED 就亮起来了
我们可以初始化,在其后面加上一段代码,这样初始化后,如果不操作 LED,LED 就是熄灭了的
接下来,我们就需要完善一下 LED 驱动程序模块
除了初始化,还需要点亮和熄灭 LED 的函数
将这四个函数放在 LED.h 中说明一下
这样,LED 的驱动函数模块就封装好了,再写好代码
这时我们可以看到 LED 交替闪烁
接下来我们来写按键的代码(同样也要封装再驱动函数模块里,步骤同上,添加.c 和.h 文件)
最后,我们可以看到,按一下右边的按键 1,LED1 点亮,按一下左边的按键 2,LED2 熄灭
# 实现按键按下,LED 去反
在 LED.cZ 中给 LED1 和 LED2 分别加上翻转功能
最后将这两个函数的第一行放在头文件里申明一下
运用
# 光敏传感器控制蜂鸣器
接线图:
VCC、GND 同样是接电源,DO 数字输出端,接 PB13 号口
和上一小节一样,添加命名为 Buzzer 的.c 和.h 文件 (在 Hardware 中),封装好
编写好函数,蜂鸣器不断鸣响,说明驱动函数没有问题
在封装好光敏传感器的模块(命名为 LightSensor,同样在 Hardware 中)
运行,遮住光敏电阻,蜂鸣器响,拿开,蜂鸣器不响
# 总结
# GPIO 的使用方法:
首先初始化时钟,然后定义结构体,赋值结构体
GPIO_Mode 可以选择那 8 种输入输出模式
GPIO_Pin 选择引脚,可以用按位或的方式同时选中多个引脚
GPIO_Speed 选择输出速度
# 模块编程的方法:
一般我们自己做一个产品的话,外围硬件比较多,所以需要我们把每个硬件的驱动函数单独提取出来,封装在.c 和.h 文件里,这样有利于简化主函数的逻辑,也有利于我们移植程序,还有利于我们进行分工合作
# OLED 调试工具
# OLED 简介
- **OLED(Organic Light Emitting Diode):** 有机发光二极管
- **OLED 显示屏:** 性能优异的新型显示屏,具有功耗低、相应速度快、宽视角、轻薄柔韧等特点
- **0.96 寸 OLED 模块:** 小巧玲珑、占用接口少、简单易用,是电子设计中非常常见的显示屏模块
- ** 供电:**3~5.5V ** 通信协议:**I2C/SPI ** 分辨率:**128*64
# 调试方式
-
** 串口调试:** 通过串口通信,将调试信息发送到电脑端,电脑使用串口助手显示调试信息
优势:可以借助强大的电脑来调试,电脑端的软件,不仅可以显示单独的参数,而且可以显示曲线、图形、图像等,还可以自己做一个软件,来实现一个强大的用户交互界面
弊端: 调试的时候需要拖这电脑,而且通常的串口助手只能以信息流的方式呈现数据(即只能一行一行的打印)
-
** 显示屏调试:** 直接将显示屏连接到单片机,将调试信息打印在显示屏上
优势:对于不断变化的数据,可以覆盖刷新显示,而且显示屏可以始终接在单片机上,显示方式很直接
弊端:屏幕太小,显示内容有限,没有电脑软件那么强大的功能
-
**Keil 调试模式:** 借助 Keil 软件的调试模式,可使用单步运行、设置断点、查看寄存器及变量等功能
测试程序的基本思想:缩小范围、控制变量、对比测试等
# OLED 驱动函数
函数 | 作用 |
---|---|
OLED_Init(); | 初始化 |
OLED_Clear(); | 清屏 |
OLED_ShowChar(1, 1, 'A'); | 显示一个字符 |
OLED_ShowString(1, 3, "HelloWorld!"); | 显示字符串 |
OLED_ShowNum(2, 1, 12345, 5); | 显示十进制数字 |
OLED_ShowSignedNum(2, 7, -66, 2); | 显示有符号十进制数字 |
OLED_ShowHexNum(3, 1, 0xAA55, 4); | 显示十六进制数字 |
OLED_ShowBinNum(4, 1, 0xAA55, 16); | 显示二进制数字 |
# OLED 显示屏
接线图:
将 4 针脚 I2C 版本中的文件复制,粘贴到 Hardware 文件中
接着进入 kile,在 Hardware 点击右键,选择 Add Files to Group 'Hardware', 选中这三个文件,点击 Add 添加
注意:如果长度参数比数字长度长,那它就会在前面补 0;如果长度参数比数字长度小,那它就会给高位的数字删掉
# EXTI 外部中断
中断系统是管理和执行中断的逻辑结构,外部中断是众多能产生中断的外设之一
# 中断系统
-
** 中断:** 在主程序运行过程中,出现了特定的中断触发条件(中断源),使得 CPU 暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行
使用中断系统,能够极大的提高程序的效率
-
** 中断优先级:** 当有多个中断源同时申请中断时,CPU 会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源
-
** 中断嵌套:** 当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU 再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回
** 断点:** 被中断的地方
# STM32 中断
-
68 个可屏蔽中断通道,包含 EXTI、TIM、ADC、USART、SPI、I2C、RTC 等多个外设
-
使用 NVIC 统一管理中断,每个中断通道都拥有 16 个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级
NVIC(嵌套中断向量控制器)就是 STM32 用来管理中断、分配优先级的
# NVIC 优先级分组
-
NVIC 的中断优先级由优先级寄存器的 4 位(0~15)决定,这 4 位可以进行切分,分为高 n 位的抢占优先级和低 4-n 位的响应优先级
优先级的数是指值越小,优先级越高,0 就是最高优先级
-
抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队
分组方式 | 抢占优先级 | 响应优先级 |
---|---|---|
分组 0 | 0 位,取值为 0 | 4 位,取值为 0~15 |
分组 1 | 1 位,取值为 0~1 | 3 位,取值为 0~7 |
分组 2 | 2 位,取值为 0~3 | 2 位,取值为 0~3 |
分组 3 | 3 位,取值为 0~7 | 1 位,取值为 0~1 |
分组 4 | 4 位,取值为 0~15 | 0 位,取值为 0 |
# EXTI 简介
-
EXTI(Extern Interrupt)外部中断
-
EXTI 可以监测指定 GPIO 口的电平信号,当其指定的 GPIO 口产生电平变化时,EXTI 将立即向 NVIC 发出中断申请,经过 NVIC 裁决后即可中断 CPU 主程序,使 CPU 执行 EXTI 对应的中断程序
简单来说,就是引脚电平变化,申请中断
-
** 支持的触发方式:** 上升沿 / 下降沿 / 双边沿 / 软件触发
软件触发:即引脚什么事也没有,程序里执行一段代码,就能触发中断
-
** 支持的 GPIO 口:** 所有 GPIO 口,但相同的 Pin 不能同时触发中断(PA0 和 PB0 不能同时用,或者 PA1、PB1、PC1 这样的,端口 GPIO_Pin 一样的,只能选一个作为中断引脚)
-
** 通道数:**16 个 GPIO_Pin,外加 PVD 输出、RTC 闹钟、USB 唤醒、以太网唤醒
-
** 触发响应方式:** 中断响应 / 事件响应
注意:外部中断的 95 会触发同一个中断函数,1510 也会触发同一个中断函数,在编程的时候,我们在这两个中断函数里,需要再根据标志位来区分到底是那个中断进来的
# AFIO 复用 IO 口
- AFIO 主要用于引脚复用功能的选择和重定义
- 在 STM32 中,AFIO 主要完成 ** 两个任务:** 复用功能引脚重映射、中断引脚选择
AFIO 选择中断引脚的结构图:
图中梯形的符号,是数据选择器,有多个输入,一个输出,在侧面有选择控制端,根据控制端的数据,从输入选择一个接到输出
# EXTI 框图
图片里面像月亮的符号是或门,它可以有多个输入,但只能有一个输出,执行的是或的逻辑,在输入端,只要有一个是高电平 1,输出就是高电平 1,只有全部输入低电平 0,输出才为 0
图中带有直边的符号是于门,它可以有多个输入,但只能有一个输出,执行的是与的逻辑,在输入端,只要有一个是低电平 0,输出就是低电平 0,只有全部输入高电平 1,输出才为 1
# 旋转编码器简介
- ** 旋转编码器:** 用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向
- ** 类型:** 机械触点式 / 霍尔传感器式 / 光栅式
# 硬件电路
模块的电路图:
# 对射式红外传感器计次 & 旋转编码器计次
# 对射式红外传感器计次
接线图:
我们还是把传感器的功能封装到一个模块里(具体操作看以前的示例,命名为 CountSensor, 也是放在 Hardware 文件里)
用来复位 AFIO 外设,调用一下这个函数,AFIO 外设的外置就会全部清除(350)
用来锁定 GPIO 配置,调用这个函数,参数指定某个引脚,那这个引脚的配置就会被锁定,防止意外更改,这个也是 GPIO 外设的函数(361)
这两个函数是用来配置 AFIO 的事件输出功能的(362、363)
可以用来进行引脚重映射,第一个参数可以选择你要重映射的方式,第二个参数是新的状态(364)
调用这个函数,就可以配置 AFIO 的数据选择器,来选择我们想要的中断引脚(365)
和以太网有关(366)
调用它,就可以把 EXTI 的配置都清除,恢复成上电默认的状态(158)
调用这个函数,就可以这个结构体里的参数配置 EXIT 外设,初始化 EXTI 主要用的就是这个函数(159)
调用这个函数,可以把参数传递的结构体变量赋一个默认值(160)
用来软件触发外部中断,调用这个函数,参数给一个指定的中断线,就能软件触发一次这个外部中断(161)
库函数的模板函数,因为在外设运行的过程中,会产生一些状态标志位(162、163、164、165)
中断分组,参数是中断分组的方式(196)
根据结构体里面指定的参数初始化 NVIC(197)
设置中断向量表(198)
系统低功耗配置(199)
在中断函数里,一般都是先进行一个中断标志位的判断,确保是我们想要的中断源触发的这个函数,因为这个函数 EXTI10 到 EXTI15 都能进来,所以要先判断一下是不是我们想要的
最后,中断程序结束后,一定要再调用一下清除中断标志位的函数,因为只有中断标志 1 了,程序就会跳转到中断函数,如果不清除中断标志位,那它就会一直申请中断
运行后,遮挡的时候数字加 1
# 旋转编码器计次
接线图:
再 Hardware 中同上面几个步骤一样,添加名为 Encoder 的.c 和.h 文件
运行,向右转,数字增加,向左转,数字减小
中断编程的建议:
- 在这个中断函数里,最好不要执行耗时过长的代码,中断函数要简短快速,因为中断是处理突发的事情
- 最好不要在中断函数和主函数调用相同的函数或者操作同一个硬件,尤其是硬件相关的函数
# TIM 定时中断
** 计时器:**STM32 中功能最强大、结构最复杂的一个外设
# TIM 简介
- TIM(Timer)定时器
- 定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断(定时器的基本功能)
- 16 位计数器、预分频器、自动重装寄存器的时基单元,在 72MHz 计数时钟下可以实现最大 59.65s 的定时
- 不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能
- 根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型
# 定时器类型
类型 | 编号 | 总线 | 功能 |
---|---|---|---|
高级定时器 | TIM1、TIM8 | APB2 | 拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能 |
通用定时器 | TIM2、TIM3、TIM4、TIM5 | APB1 | 拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能 |
基本定时器 | TIM6、TIM7 | APB1 | 拥有定时中断、主模式触发 DAC 的功能 |
这三种定时器是由高级到低级向下兼容的
STM32F103C8T6 定时器资源:TIM1、TIM2、TIM3、TIM4
基本定时器:
自动重装载寄存器、PSC 预分频器和 CNT 计数器这一块电路叫做时基单元
图中向上的折线箭头,就代表这里会产生中断信号,像这种计数值等于自动重装值产生的中断,我们一般把它叫做 “更新中断”
图中向下的折线箭头,代表的是会产生一个事件,这里对应的事件就叫做 “更新事件”,更新事件不会触发中断,但可以触发电路内部其他电路的工作
** 主模式触发 DAC 的功能 :** 它能让内部的硬件在不受程序的控制下实现自动运行
通用定时器:
高级定时器:
# 定时中断基本结构
# 预分频器时序
**CK_PSC:** 预分频器的输入时钟,选内部时钟的话一般是 72MHz
**CNT_EN:** 计数器使能,高电平计数器正常运行,低电平计算器停止
**CK_CNT:** 计数器时钟
** 计数器计数频率:**CK_CNT = CK_PSC / (PSC + 1)
# 计数器时序
** 计数器溢出频率:**CK_CNT_OV = CK_CNT / (ARR + 1)
= CK_PSC / (PSC + 1) / (ARR + 1)
# 定时器定时中断 & 定时器外部时钟
# 定时器定时中断
接线图:
我们任然需要建立.c 和.h 文件(在 System 文件中,命名为 Timer)
恢复缺省配置 (1054)
时基单元初始化,第一个参数,选择某个定时器;第二个参数,结构体,里面包含了配置时基单元的一些参数 (1055)
这个函数可以把结构体变量赋一个默认值
用来使能计数器,它有两个参数,第一个 TIMx 选择定时器,第二个 NewState 新的状态,也就是使能还是失能,使能,计数器就可以运行,失能,计数器就不运行
这个是用来使能中断输出信号的,第一个 TIMx 选择定时器,第二个 TIM_IT,选择要配置那个中断输出,第三个 NewState 新的状态,使能还是失能
时钟源选择:
选择内部时钟 (1073)
选择 ITRx 其他定时器的时钟,参数 InputTriggerSource,选择要接入那个其他的定时器 (1074)
选择 TIx 捕获通道的时钟 (1075),参数 TIxExternalCLKSource,选择 TIx 具体的某个引脚,ICPolarity 和 ICFilter,输入的极性和滤波器
选择 ETR 通过外部时钟模式 1 输入的时钟,参数 ExtTRGPrescaler,外部触发预分频器,这里可以对 ETR 的外部时钟再提前做一个分频 (1077)
选择 ETR 通过外部时钟模式 2 输入的时钟 (1079)
单独用来配置 ETR 引脚的预分频器、极性、滤波器这些参数的 (1081)
用来单独写预分频值,参数,Prescaler,就是要写入的预分频值,PSCReloadMode,写入的模式(预分频器有一个缓冲器,写入的值是在更新事件发生后才有效的),可以选择是听从安排,在更新事件生效,或者是,在写入后,手动产生一个更新事件,让这个值立即生效 (1083)
用来改变计数器的计数模式,参数,CounterMode,选择新的计数模式 (1084)
自动重装器预装功能配置
给计数器写入一个值 (1125)
给自动重装器写入一个值 (1126)
获取当前计数器的值,返回值就是当前计数器的值 (1140)
获取当前的预分频器的值 (1141)
获取标志位和清除标志位 (1142~1145)
跨越不同文件的变量的解决方法:
-
如果你想跨文件使用变量,可以在使用变量那个文件的上面,用 extern 申明一下要用的变量
extern 申明变量,就是告诉编译器,我现在有 Num 这个变量,它在别的文件里定义了,编译器就会自己找到其所在的位置,找到之后,它就会把这个 extern 申明的变量,当作被找到的文件的一个引用
注意:这个过程并没有定义新的变量
-
直接把这个中断函数复制一下,放在主函数后面
运行之后,Num 的值就正在执行每秒 + 1 的操作
# 定时器外部时钟
接线图:
运行之后,当当光片挡一下,CNT 加 1,加到 9 后,自动清 0,同时申请中断,Num++
# TIM 输出比较
** 输出比较功能:** 主要是用来输出 PWM 波形的,PWM 波形又是驱动电机的必要条件
# 输出比较简介
- OC(Output Compare)输出比较
- 输出比较可以通过比较 CNT 与 CCR 寄存器值的关系,来对输出电平进行置 1、置 0 或翻转的操作,用于输出一定频率和占空比的 PWM 波形
- 每个高级定时器和通用定时器都拥有 4 个输出比较通道
- 高级定时器的前 3 个通道额外拥有死区生成和互补输出的功能
# PWM 简介
-
PWM(Pulse Width Modulation)脉冲宽度调制
PWM 波形是一个数字输出信号,意思由高低电平组成的
-
在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域
-
PWM 参数:
频率 = 1 / Ts 占空比 = Ton / Ts 分辨率 = 占空比变化步距
Ton 在这里是高电平的时间 Ts 是一个周期的时间
占空比决定了 PWM 等效出来的模拟电压的大小,占空比越大,等效的模拟电压就越趋近于高电平,占空比越小,等效的模拟电压就越趋近于低电平,这个等效关系一般来说是线型的
使用 PWM 波形,就可以在数字系统等效输出模拟量,就能实现 LED 控制亮度、电机控速等功能
输出比较通道 (通用)
输出比较通道 (高级)
图中 REF 信号(参考信号)实际上就是指这里信号的高低电平
到图中三角形部分,就是极性选择,就是选择是不是要把高低电平反转一下
# 输出比较模式
模式 | 描述 |
---|---|
冻结 | CNT=CCR 时,REF 保持为原状态 (即 CNT 和 CCR 无效) |
匹配时置有效电平 | CNT=CCR 时,REF 置有效电平 |
匹配时置无效电平 | CNT=CCR 时,REF 置无效电平 |
匹配时电平翻转 | CNT=CCR 时,REF 电平翻转 |
强制为无效电平 | CNT 与 CCR 无效,REF 强制为无效电平 |
强制为有效电平 | CNT 与 CCR 无效,REF 强制为有效电平 |
PWM 模式 1 | 向上计数:CNT<CCR 时,REF 置有效电平,CNT≥CCR 时,REF 置无效电平 向下计数:CNT>CCR 时,REF 置无效电平,CNT≤CCR 时,REF 置有效电平 |
PWM 模式 2 | 向上计数:CNT<CCR 时,REF 置无效电平,CNT≥CCR 时,REF 置有效电平 向下计数:CNT>CCR 时,REF 置有效电平,CNT≤CCR 时,REF 置无效电平 |
# 参数计算
- PWM 频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
- PWM 占空比: Duty = CCR / (ARR + 1)
- PWM 分辨率: Reso = 1 / (ARR + 1)
# 舵机简介
- 舵机是一种根据输入 PWM 信号占空比来控制输出角度的装置
- ** 输入 PWM 信号要求:** 周期为 20ms(即 50Hz),高电平宽度为 0.5ms~2.5ms
# 直流电机及驱动简介
-
直流电机是一种将电能转换为机械能的装置,有两个电极,当电极正接时,电机正转,当电极反接时,电机反转
-
直流电机属于大功率器件,GPIO 口无法直接驱动,需要配合电机驱动电路来操作
电机这类器件基本都属于大功率设备,必须要加驱动电路才能控制
-
TB6612 是一款双路 H 桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向
# PWM 驱动 LED 呼吸灯 & PWM 驱动舵机 & PWM 驱动直流电机
# PWM 驱动 LED 呼吸灯
接线图:
同上几个步骤一样,建立.c 和.h 文件(在 Hardware 文件中,命名为 PWM)
接下来,打通模块,就可以输出 PWM
具体步骤为:
- PCC 开启时钟,把我们要用的 TIM 外设和 GPIO 外设的时钟打开
- 配置时基单元
- 配置输出比较单元
- 配置 GPIO,把 PWM 对应的 GPIO 口,初始化为复用推挽输出的配置
- 运行控制
配置输出比较模块
给输出比较结构体赋一个默认值
配置强制输出模式(如果你在运行中想要暂停波形并且强制输出高或低电平,可以用)
用来配置 CCR 寄存器的预装功能(即影子寄存器)(1096~1099)
用来快速配置使能(1100~1103)
外部事件时清除 REF 信号(1104~1107)
用来单独设置输出比较的极性,这里带 N 的就是高级定时器里互补通道的配置,OC4 没有互补通道(1108~1114)
一般来说,结构体里的参数,都会有一个单独的函数可以进行更改
用来单独修改使能参数(1115、1116)
选择输出比较模式,用来单独更改输出比较模式的函数
用来单独更改 CCR 寄存器值的函数
仅高级定时器使用,在使用高级定时器输出 PWM 时,需要调用这个函数,使能主输出,否则 PWM 将不能正常输出
运行之后,LED 点亮,并以 1kHz 的频率闪烁
实现呼吸灯效果
运行之后,LED 不断变亮变暗,呈现了呼吸灯的效果
重映射:更改复用的引脚
# PWM 驱动舵机
接线图:
编写代码
运行之后,舵机就移动到 0 度的位置了(如果我们稍微扳一下输出轴,可以感觉有一股力在维持它现在的位置)
在 Hardware 中添加.c 和.h 文件,命名为 Servo
运行之后,可以看到舵机的角度,每按一下,加 30 度
# PWM 驱动直流电机
接线图:
在 Hardware 中添加.c 和.h 文件,命名为 Motor
运行,我们可以看到电机已经转起来了
同时,现在可以发现一个问题,就是这个电机会发出蜂鸣器的响声(是正常现象),如果想避免,可以加大 PWM 频率
运行,按下按键,OLED 上速度变量变化,同时电机也转起来了
# TIM 输入捕获
# 输入捕获简介
- IC(Input Capture)输入捕获
- 输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前 CNT 的值将被锁存到 CCR 中,可用于测量 PWM 波形的频率、占空比、脉冲间隔、电平持续时间等参数
- 每个高级定时器和通用定时器都拥有 4 个输入捕获通道
- 可配置为 PWMI 模式,同时测量频率和占空比
- 可配合主从触发模式,实现硬件全自动测量
# 频率测量
** 测频法(测高频):** 在闸门时间 T 内,对上升沿计次,得到 N,则频率
f_x=N / T
T 通常设置为一秒
** 测周法(测低频):** 两个上升沿内,以标准频率 fc 计次,得到 N ,则频率
f_x=f_c / N
** 中界频率:** 测频法与测周法误差相等的频率点
f_m=√(f_c / T)
# 输入捕获通道
# 主从触发模式
就是主模式、从模式和触发源选择这三个功能的选择
** 主模式:** 可以将定时器内部的信号,映射到 TRGO 引脚,用于触发别的外设
** 从模式:** 就是接收其他外设或者自身外设的一些信号,用于控制自身定时器的运行,也接收被别的信号控制
复位模式(是从模式的一部分):选中的触发输入(TRGI)的上升沿重新初始化计数器,并且产生一个更新寄存器的信号
** 触发源选择:** 接收选择从模式的触发信号源的,可以认为它是从模式的一部分,选择一个指定的信号,得到 TRGI,TRGI 去触发从模式
# 输入捕获模式测频率 & PWMI 模式测频率占空比
# 输入捕获模式测频率
接线图:
在 Hardware 中添加.c 和.h 文件,命名为 IC
步骤:
- RCC 开启时钟,把 GPIO 和 TIM 的时钟打开
- GPIO 初始化,把 GPIO 配置成输入模式,一般选择上拉输入或者浮空输入模式
- 配置时基单元,让 CNT 计数器在内部时钟的驱动下自增运行
- 配置输入捕获单元,包括滤波器、极性、直连通道还是交叉通道、分频器这些参数,用一个结构体就可以统一进行配置了
- 选择从模式的触发源
- 选择触发之后执行的操作,执行 Reset 操作
- 最后,调用 TIM_Cmd 函数,开启定时器
** 注意:** 滤波器和分频器的区别,滤波器记次,并不会改变信号的原有频率,一般滤波器的采样频率都会高于信号频率,所以它只会滤除高频噪声,使信号更平滑;分频器就是对信号本身进行记次,会改变频率
运行,可以测量到频率
# PWMI 模式测频率占空比
接线图:
目前测量结果,频率 1000Hz, 占空比 50%
# TIM 编码器接口
# 编码器接口简介
- Encoder Interface 编码器接口
- 编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制 CNT 自增或自减,从而指示编码器的位置、旋转方向和旋转速度
- 每个高级定时器和通用定时器都拥有 1 个编码器接口
- 两个输入引脚借用了输入捕获的通道 1 和通道 2
# 正交编码器
正转:
边沿 | 另一相状态 |
---|---|
A 相↑ | B 相低电平 |
A 相↓ | B 相高电平 |
B 相↑ | A 相高电平 |
B 相↓ | A 相低电平 |
反转:
边沿 | 另一相状态 |
---|---|
A 相↑ | B 相高电平 |
A 相↓ | B 相低电平 |
B 相↑ | A 相低电平 |
B 相↓ | A 相高电平 |
# 编码器接口基本结构
# 编码器接口测速
接线图:
在 Hardware 中添加.c 和.h 文件,命名为 Encoder
运行,目前显示 CNT 是 0,向右转一下编码器,可以看到值变化(增加),往左转,CNT 自减
在.c 文件后面加上
运行,目前 CNT 的值就代表速度,单位是脉冲个数 /s, 向右慢速转,速度是正数,比较小,快速转,正数比较大,向左慢速转,速度负数。比较小,快速转,负数比较大
如果主程序中没有其他东西的话,可以使用 Delay 函数,但是如果有其他东西的话,最好就不要在主函数加入过长的 Delay, 这样会阻塞主循环的执行,可以用定时中断
运行,向右旋转,正的速度;向左旋转,负的速度;停止速度为 0
# ADC 模数转换器
# ADC 简介
-
ADC(Analog-Digital Converter)模拟 - 数字转换器
-
ADC 可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁
** 注:** 数字到模拟的桥梁 ----DAC,数字模拟转换器,使用 DAC 就可以将数字变量转换为模拟电压,同时,PWM 也可以实现数字到模电的转变,但是 PWM 只有完全导通和完全断开两种状态,在这两种状态下都没有功率损耗,所以在直流电机调速这种大功率的应用场景,使用 PWM 来等效模拟量,是比 DAC 更好的选择,并且 PWM 电路更加简单,更加常用
-
12 位逐次逼近型 ADC,1us 转换时间
-
** 输入电压范围:**03.3V,** 转换结果范围:**04095
-
18 个输入通道,可测量 16 个外部和 2 个内部信号源
-
规则组和注入组两个转换单元
-
模拟看门狗自动监测输入电压范围
-
STM32F103C8T6 **ADC 资源:**ADC1、ADC2,10 个外部输入通道
# 逐次逼近型 ADC
# STM32ADC 框图
# ADC 基本结构
# 输入通道
通道 | ADC1 | ADC2 | ADC3 |
---|---|---|---|
通道 0 | PA0 | PA0 | PA0 |
通道 1 | PA1 | PA1 | PA1 |
通道 2 | PA2 | PA2 | PA2 |
通道 3 | PA3 | PA3 | PA3 |
通道 4 | PA4 | PA4 | PF6 |
通道 5 | PA5 | PA5 | PF7 |
通道 6 | PA6 | PA6 | PF8 |
通道 7 | PA7 | PA7 | PF9 |
通道 8 | PB0 | PB0 | PF10 |
通道 9 | PB1 | PB1 | |
通道 10 | PC0 | PC0 | PC0 |
通道 11 | PC1 | PC1 | PC1 |
通道 12 | PC2 | PC2 | PC2 |
通道 13 | PC3 | PC3 | PC3 |
通道 14 | PC4 | PC4 | |
通道 15 | PC5 | PC5 | |
通道 16 | 温度传感器 | ||
通道 17 | 内部参考电压 |