STM32小白视角笔记(标准库)(乱序版)
STM32
一、点灯操作
1.用寄存器的方式
更改起来比较麻烦:
除了PC13之外的位都配置成0,通过更改GPIOC->ODR来实现点灯熄灯操作
2.用库函数的方式

在工程目录下新建Library文件夹📂用来存放库函数
函数不会写怎么办?

把光标放在RCC_APB2PeriphClockCmd上面,就可以看到需要传递的参数了
然后你右键此函数名称:
直接跳转到函数定义,里面定义了参数值可以填入哪些
OK,that is
可以看到,此函数都是封装好的,或等于和与等于,so:库函数的配置不会影响到寄存器的其他位(库函数相比较寄存器有很大的好处)
我们只需要调用库函数,配置好参数就可以了✅
GPIO_Init这个函数使用结构体来配置参数
此函数的简介
结构体的其中一个参数如图:


GPIO_Mode_Out_PP是通用推挽输出,GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_13;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
&:取地址符
STM32的配置方式都是固定的,熟能生巧
最后设置高低电平:
这就是全部代码了:
手指缩放调节字体大小,或点击工具栏的🔧图标配置字体和颜色
分别通过注释高低电平,实现点灯关灯操作:
先点击左上角编译按钮,再点LOAD下载按钮,然后观察💡的亮与灭:
F103最小系统板PC13是低电平点亮:
3.3V → 限流电阻 → LED正极 → LED负极 → STM32 IO引脚
- IO输出低电平(0):电流流通 → 灯亮

IO输出高电平(1):两端电压相等,无电流 → 灯灭
二、GPIO输出
简介
GPIO(General Purpose Input Output)–通用输入/出口
应用:驱动LED、控制蜂鸣器、模拟通讯协议输出时序
三种输出模式 :
-
推挽输出模式:(又称:强推输出模式)
P-MOS和N-MOS均有效
数据寄存器为1时,上管导通,下管断开,输出直接接到VDD-输出高电压;
数据寄存器为0时,上管断开,下管导通,输出直接接到VSS-输出低电压.
-
开漏输出模式:(可以作为通讯协议的驱动方式)(在多机通信的情况下,可以避免各个设备的相互干扰)
P-MOS无效,N-MOS有效
数据寄存器为1时,下管断开,输出相当于断开-高阻模式;
数据寄存器为0时,下管导通,输出直接接到VSS-输出低电压.
-
关闭模式
三、GPIO应用
1.LED闪烁

用跳线:GND引到负极-,3.3V引到正极+

操作STM32的GPIO的3个步骤:
Step1:使用RCC开启GPIO的时钟
Step2:使用GPIO_Init函数初始化GPIO
Step3:使用输出/入函数控制GPIO口
看一下,RCC最常用的三个函数:(外设时钟控制)
两个参数:第一个参数选择外设,第二个参数选择使/失能(ENABLE/DISABLE)
GPIO常用的库函数:

调用GPIO_DeInit函数,指定的GPIO外设会被复位;
GPIO_AFIODeInit函数,可以复位AFIO外设;
GPIO_Init函数重要📌,用结构体的参数初始化GPIO口;
GPIO_StructInit函数
读取函数:GPIO_ReadInputDataBit/GPIO_ReadInputData/GPIO_ReadOutputDataBit/GPIO_ReadOutputData
写入函数:GPIO_SetBits/GPIO_ResetBits/GPIO_WriteBit/GPIO_Write
光标放在变量上,右键跳转定义:
GPIO的八种工作模式:
我们点灯用的是推挽输出,所以用GPIO_Mode_Out_PP
可以看下图,LED灯亮了:

然后:
灯不亮了:
然后:
又亮了:
然后:
又不亮了:
2.LED流水灯
需要用到循环语句,并且在工程里加上延时函数:


if你就想要给它1和0,1是高电平,0是低电平怎么办?
需要强制类型转换
if我们(前提:先断开电,避免损坏设备,把ST-LINK从电脑拔下来即可)把LED的正极插到A0口,负极插到电源负极,则LED变为高电平点亮方式,LED也是可以正常工作的
说明:推挽模式下,高低电平都有驱动能力

if变为GPIO_Mode_Out_OD开漏输出模式,则发现LED不亮了:
因为:开漏输出模式下,高电平相当于高阻态,没有驱动能力
代码示例如下:
LED排列:
3.蜂鸣器
引脚:
需要蜂鸣器和3根公对母杜邦线
VCC接正极,GND接负极,I/O接一个端口就行,但是A15/B3/B4要接的话需要额外配置
查看库函数:
打开后缀为.h的文件,翻到最后,看都有哪些函数,右键跳转到函数定义,查看函数及其参数的用法
四、GPIO输入
1.传感器介绍
传感器模块:传感器元件(光敏电阻/热敏电阻/红外接收管等)的电阻会随外界模拟量的变化而变化,通过与定值电阻分压即可得到模拟电压输出,再通过电压比较器进行二值化即可得到数字电压输出
什么是二值化?
例如:
LM393电压比较器完成【二值化】规则:
-
当前模拟电压 > 阈值 → 输出 高电平(数字1)
-
当前模拟电压 < 阈值 → 输出 低电平(数字0)
结果:无数种连续电压,最终只剩下 0 / 1 两种电平,这就叫电压信号的二值化!
这部分会用到一些电子电路的和C语言知识
2.基础知识
C语言宏定义:
关键字:#define
就是用一个字符串代替一个数字;提取程序中经常出现的参数,便于快速修改
结构体也是非常重要的📌:
声明一个结构休类型的一般形式如下:
struct 结构体名
{成员列表};
在结构体里可以出现不同类型的变量,即:结构体可以组合不同的数据类型:
例如:
struct student
{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
}student1, student2;
C语言枚举:
关键字:enum
枚举类型是用于声明一组命名的常数的基本数据类型(值类型),限制变量取值范围
3.LED按键控制
接两个按键和两个LED
在工程下新建文件夹📂Hardware用来封装函数代码:
新建LED.c文件,存放驱动程序的主体代码;
新建LED.h文件,存放驱动程序可以对外提供的函数/变量的声明.

这个样初始化之后,默认是低电平,所以LED是亮的:
if是这样:
则初始化之后,LED是不亮的:
在函数里,优先使用局部变量,没有的话,才会使用外部的全局变量
实现两个按键控制一个LED灯的亮与不亮:
以下是三部分代码:
Key.h文件:
#ifndef __KEY_H
#define __KEY_H
void Key_Init(void);
uint8_t Key_GetNum(void);
#endif
Key.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
//按键初始化函数
void Key_Init(void)
{
//初始化函数的端口
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
//定义结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
//用点引出成员
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_1 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
//调用函数
//第一个参数是外设,第二个参数是结构体变量名:
GPIO_Init(GPIOB,&GPIO_InitStructure);
}
//读取按键值的函数
uint8_t Key_GetNum(void)
{
//定义局部变量
uint8_t KeyNum=0;
if ( GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0)
{Delay_ms(20);
while ( GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0);
Delay_ms(20);
KeyNum=1;
}
if ( GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0)
{
Delay_ms(20);
while ( GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0);
Delay_ms(20);
KeyNum=2;
}
return KeyNum;
}
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
//定义全局变量
uint8_t KeyNum;
//主函数
int main(void)
{
LED_Init();
Key_Init();
while (1)
{
KeyNum = Key_GetNum();
if (KeyNum==1)
{
LED1_ON();
}
if (KeyNum==2)
{
LED1_OFF();
}
}
}
4.光敏传感器控制蜂鸣器
引脚:
需要蜂鸣器和3根公对母杜邦线:
VCC接正极,GND接负极,I/O接一个端口(这里我选的PB12)就行,但是A15/B3/B4要接的话需要额外配置
需要光敏传感器和3根公对母杜邦线:
VCC接正极,GND接负极,I/O接一个端口(这里我选的PB13)
这个是光敏传感器:
以下是三部分代码:
LightSensor.h文件:
#ifndef __LIGHTSENSOR_H
#define __LIGHTSENSOR_H
void LightSensor_Init(void);
uint8_t LightSensor_Get(void);
#endif
LightSensor.c文件:
#include "stm32f10x.h" // Device header
void LightSensor_Init(void)
{
//初始化函数的端口
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
//定义结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
//用点引出成员
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_13;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
//调用函数
//第一个参数是外设,第二个参数是结构体变量名:
GPIO_Init(GPIOB,&GPIO_InitStructure);
}
uint8_t LightSensor_Get(void)
{
return GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13);
}
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Buzzer.h"
#include "LightSensor.h"
//主函数
int main(void)
{
//初始化
Buzzer_Init();
LightSensor_Init();
while (1)
{
if(LightSensor_Get()==1)
{
Buzzer_ON();
}
else{
Buzzer_OFF();
}
}
}
当硬件比较多时,我们一般情况下就会选择把每个硬件的驱动函数单独拿出来,分别封装在.h和.c文件中,简化main函数
五、OLED
1.调试方式
串口调试:通过串口通信,将调试信息发送到电脑端,电脑使用串口助手显示调试信息
显示屏调试:直接将显示屏连接到单片机,将调试信息打印在显示屏上
Keil调试模式:借助Keil软件的调试模式,可使用单步运行、设置断点、查看寄存器及变量等功能
2.OLED简介
OLED(Organic Light Emitting Diode):有机发光二极管
2.1OLED显示屏
性能优异的新型显示屏,具有功耗低、相应速度快、宽视角、轻薄柔韧等特点0.96寸OLED模块:小巧玲珑、占用接口少、简单易用,是电子设计中非常常见的显示屏模块供电:3~5.5V,通信协议:I2C/SPI,分辨率:128*64

2.2OLED驱动函数
| 函数 | 作用 |
|---|---|
| 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); | 显示二进制数字 |
3.OLED显示屏实践
新建项目工程:
把OLED模块添加到工程里:
示例1:
则OLED显示屏:
示例2:
if给的数字长度比后面数字小,在最前面补0:
示例3:
if给的数字长度比后面数字大,从最高位依次清除:
示例4:

示例5:

无符号函数:
OLED_ShowNum全是正数
@brief OLED显示数字(十进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~4294967295
* @param Length 要显示数字的长度,范围:1~10
* @retval 无
有符号函数:带±号
OLED_ShowSignedNum
@brief OLED显示数字(十进制,带符号数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-2147483648~2147483647
* @param Length 要显示数字的长度,范围:1~10
* @retval 无
4.补充
调试模式的使用:
点击👆工具栏里这个放大镜🔎(里面写了一个红色的d):进入Keil自带的调试模式
界面如图所示:
主窗口是C语言程序:
下面这个是C语言翻译成的汇编程序:
界面左边是寄存器组和状态标志位信息:
这个是复位:
这个是全速运行:
下面这几个是跳步运行:
黄色箭头指向的是下一句将要执行的代码:
在这里可以查看帮助文档:

六、中断系统
1.什么是中断?
中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行
中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源
中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回
STM32中断
8个可屏蔽中断通道,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设
2.NVIC
NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级
抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队
| 分组方式 | 抢占优先级 | 响应优先级 |
|---|---|---|
| 分组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不能同时触发中断通道数:16个GPIO_Pin
触发响应方式:中断响应/事件响应
与门(相当于一个开关):
1&任意数=任意数
0&任意数=0
/20:表示20根线,20个通道
AFIO复用IO口
AFIO主要用于引脚复用功能的选择和重定义
在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择
旋转编码器
旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向
类型:机械触点式/霍尔传感器式/光栅式
上图是:旋转编码器(属于:机械触点式)
内部是通过金属触点进行判断的
输出的是正交波形,可以用来测方向(因为正转和反转相位相差90°)
4.对射式红外传感器计次
如图:
硬件连接好是这个样子的:
然后,起初是这样的:
当拿东西挡住这个传感器中间的卡槽时:
输出指示灯不亮了
所以高低电平输出没问题👌
我们还是要把传感器的功能封装在一个模块里面:

void GPIO_AFIODeInit(void);
上面👆这个函数是用来复位AFIO外设的
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
上面👆这个函数是用来锁定🔒GPIO配置的,调用此函数,参数指定某个引脚,这个引脚的配置就会被锁定,防止意外更改
这两个函数很重要📌:
EXTI的库函数:
EXTI_DeInit:复位
EXTI_Init:传参,配置EXTI外设(初始化EXTI)
EXTI_StructInit:把参数传递的结构体变量赋给一个默认值
倒数4个函数,用来看标志位
NVIC:
优先级分组函数:NVIC_PriorityGroupConfig
优先级是在多个中断源同时申请产生拥挤时才有作用
中断函数在Start启动文件📂里找:
每次中断程序结束以后,需要清除一下中断标志位,不然程序将陷入死循环
中断函数不需要调用,本身自动执行
进入调试模式:
我们在中断函数这里打个断点:
然后点击左上角全速运行:
硬件是这样的:
然后拿东西遮挡传感器,就会发现:
程序成功✅️跳转到中断函数,停在断点处:
初始显示的Count值为00000:
拿东西遮挡传感器一次,Count的值就+1:
我们前面配置的参数是下降沿触发,所以移开的时候触发中断,+1
if是上升沿触发,那么就是遮挡的时候触发中断,+1
f改成上升沿和下降沿都触发,那么就是遮挡和移开的时候都会触发中断
核心文件配置:
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"
//主函数
int main(void)
{
OLED_Init();
CountSensor_Init();
OLED_ShowString(1,1,"Count:");
while (1)
{
OLED_ShowNum(1,7,CountSensor_Get(),5);
}
}
CountSensor.c文件:
#include "stm32f10x.h"
#include "stm32f10x_exti.h" // Device header
uint16_t CountSensor_Count;
//初始化
void CountSensor_Init(void)
{
//RCC开启时钟,函数和参数要对应
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
//开启AFIO时钟,AFIO也是APB2的外设
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
//定义结构体
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
//配置外部中断引脚选择,我们硬件连接的是PB14
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
//EXTI
//定义结构体
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line=EXTI_Line14;
EXTI_InitStructure.EXTI_LineCmd=ENABLE;
EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;
//触发方式
EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
//NVIC
//优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//初始化
//定义结构体
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
}
uint16_t CountSensor_Get(void){
return CountSensor_Count;
}
//定义中断函数,中断函数无参也无返回值
void EXTI15_10_IRQHandler(void)
{
//先进行中断标志位的判断
if (EXTI_GetITStatus(EXTI_Line14)==SET)
{
CountSensor_Count++;
//清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
CountSensor.h文件:
#ifndef __COUNT_SENSOR_H
#define __COUNT_SENSOR_H
//把函数放在头文件里声明一下
void CountSensor_Init(void);
uint16_t CountSensor_Get(void);
#endif
5.旋转编码器计次
旋转编码器:
最后硬件连接:

新建项目工程:

新建旋转编码器模块:

找到EXTI的中断函数:
初始位置:
向右转数字增加:
向左转数字减小:

短快速,在本质上,中断函数是处理突发情况的函数,不要让中断函数阻碍主函数,而且不要让中断函数和主函数去调用相同的函数,或者是同一个硬件,会导致错误❎
核心文件配置:
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Encoder.h"
int16_t Num;
//主函数
int main(void)
{
OLED_Init();
Encoder_Init();
OLED_ShowString(1,1,"Num:");
while (1)
{
Num+=Encoder_Get();
OLED_ShowSignedNum(1,5,Num,5);
}
}
Encoder.c文件:
#include "stm32f10x.h" // Device header
int16_t Encoder_Count;
void Encoder_Init(void)
{
//初始化外部中断⏸️
//RCC开启时钟,函数和参数要对应
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
//开启AFIO时钟,AFIO也是APB2的外设
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
//定义结构体
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
//配置外部中断引脚选择
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);
//EXTI
//定义结构体
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line=EXTI_Line0 | EXTI_Line1;
EXTI_InitStructure.EXTI_LineCmd=ENABLE;
EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;
//触发方式
EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
//NVIC
//优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//初始化
//定义结构体
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel=EXTI1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=2;
NVIC_Init(&NVIC_InitStructure);
}
//返回变量
int16_t Encoder_Get(void)
{
int16_t Temp;
Temp=Encoder_Count;
Encoder_Count=0;
return Temp;
}
void EXTI0_IRQHandler(void)
{
//先进行中断标志位的判断
if (EXTI_GetITStatus(EXTI_Line0)==SET)
{
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0)
{
//反转
Encoder_Count--;
}
//清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
void EXTI1_IRQHandler(void)
{
//先进行中断标志位的判断
if (EXTI_GetITStatus(EXTI_Line1)==SET)
{
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0)==0)
{
//正转
Encoder_Count++;
}
//清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line1);
}
}
Encoder.h文件:
#ifndef __ENCODER_H
#define __ENCODER_H
//声明一下函数
int16_t Encoder_Get(void);
void Encoder_Init(void);
#endif
七、TIM定时中断
1.TIM简介
TIM(Timer)定时器
定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断
不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能
16位计数器(用来执行计数定时的一个寄存器,每来一个时钟,计数器就会+1)、预分频器(对计数器的时钟进行分频,让计数更加灵活)、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时
分类:高级/通用/基本定时器⏲️
| 类型 | 编号 | 总线 | 功能 |
|---|---|---|---|
| 高级定时器 | TIM1、TIM8 | APB2 | 拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能 |
| 通用定时器 | TIM2、TIM3、TIM4、TIM5 | APB1 | 拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能 |
| 基本定时器 | TIM6、TIM7 | APB1 | 拥有定时中断、主模式触发DAC的功能 |
STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4
主从触发模式是stm 32定时器的一大特色
基本定时器只支持向上计数
而通用定时器和高级定时器支持向上/向下计数还有中央对齐三种模式,but:最常用的还是向上计数
时钟是最先需要配置的东西,也是所有外设运行的基础
2.定时器⏲️定时中断
硬件部分:

新建项目工程:
给定时器⏲️新建模块,因为定时器不涉及硬件,所以不用放到Hardware文件夹📂下:

现在Library文件夹📂里找一下tim的库函数:

看一下常用的库函数:
void TIM_DeInit(TIM_TypeDef* TIMx);
恢复缺省配置
void TIM_TimeBaseInit(TIM_TypeDef* TIMx,TIM_TimeBaseInitTypeDef*,TIM_TimeBaseInitStruct);⚠️📌
用来配置时基单元,第一个参数是选择定时器;第二个是结构体,里面包含了配置实际单元的一些参数
把光标放在某一行,然后点击这个蓝色的小旗子,可以插入一个书签,方便代码的查找:

void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
把结构体变量赋一个默认值
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
第一个参数选择定时器,第二个是选择状态
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
使能中断输出信号,第一个参数选择定时器,第二个参数选择要配置哪个中断输出,第三个是选择状态(此函数很常见,会经常用到)(中断输出控制)
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
这个函数是选择内部时钟
时钟源选择用下面这6个函数:
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,
uint16_t TIM_ICPolarity, uint16_t ICFilter);
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);
时基单元,用TimeBaseInit函数:
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef*TIM_TimeBaseInitStruct);
每经过1秒,Num自增+1:

按下复位按钮后,从0开始计数:
核心文件配置:
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
//定义全局变量
uint16_t Num;
//主函数
int main(void)
{
OLED_Init();
//初始化定时器
Timer_Init();
OLED_ShowString(1,1,"Num:");
while (1)
{
//调用函数
OLED_ShowNum(1, 5,Num,5);
//CNT计数器的变化
//OLED_ShowNum(2,5,TIM_GetCounter(TIM2),5);
}
}
//中断函数
void TIM2_IRQHandler(void)
{
//检查中断标志位
//更新中断
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET)
{
Num++;
//清除标志位
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
Timer.c文件:
#include "stm32f10x.h" // Device header
//跨函数的变量,要使用extern声明变量
//extern uint16_t Num;
//初始化
void Timer_Init(void)
{
//初始化定时器⏲️
//RCC开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
//选择时基单元的时钟
//内部时钟(默认就是内部时钟,所以不写也可)
TIM_InternalClockConfig(TIM2);
//配置时基单元
//定义结构体
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
//分频
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
//计数器模式 向上计数
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
//ARR自动重装器(减去偏差)
TIM_TimeBaseInitStructure.TIM_Period=10000 - 1;
//PSC预分频器(减去偏差)
TIM_TimeBaseInitStructure.TIM_Prescaler=7200 - 1;
//重复计数器(高级定时器才有的,现在我们这个定时器用不了)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
//手动清除更新中断位,避免刚初始化完就进中断的问题
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
//使能中断
//更新中断
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);
//NVIC
//NVIC优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//定义结构体
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
//启动定时器
TIM_Cmd(TIM2,ENABLE);
}
//中断函数
/*void TIM2_IRQHandler(void)
{
//检查中断标志位
//更新中断
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET)
{
//清除标志位
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}*/
Timer.h文件:
#ifndef __TIMER_H
#define __TIMER_H
//在头文件里声明函数
void Timer_Init(void);
#endif
3.定时器⏲️外部时钟
硬件部分:

新建项目工程:



遮挡传感器一次,CNT数值+1,一直加到9之后,自动清零,同时申请中断,Num++
核心文件配置:
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
//定义全局变量
uint16_t Num;
//主函数
int main(void)
{
OLED_Init();
//初始化定时器
Timer_Init();
OLED_ShowString(1,1,"Num:");
OLED_ShowString(2,1,"CNT:");
while (1)
{
//调用函数
OLED_ShowNum(1, 5,Num,5);
OLED_ShowNum(2, 5,Timer_GetCounter(),5);
//CNT计数器的变化
//OLED_ShowNum(2,5,TIM_GetCounter(TIM2),5);
}
}
//中断函数
void TIM2_IRQHandler(void)
{
//检查中断标志位
//更新中断
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET)
{
Num++;
//清除标志位
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
Timer.c文件:
#include "stm32f10x.h" // Device header
//跨函数的变量,要使用extern声明变量
//extern uint16_t Num;
//初始化
void Timer_Init(void)
{
//初始化定时器⏲️
//RCC开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
//GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//GPIOA开启时钟
//定义结构体
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
//初始化
GPIO_Init(GPIOA,&GPIO_InitStructure);
//外部时钟
TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0x00);
//配置时基单元
//定义结构体
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
//分频
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
//计数器模式 向上计数
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
//ARR自动重装器(减去偏差)
TIM_TimeBaseInitStructure.TIM_Period=10 - 1;
//PSC预分频器(减去偏差)
TIM_TimeBaseInitStructure.TIM_Prescaler=1 - 1;
//重复计数器(高级定时器才有的,现在我们这个定时器用不了)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
//手动清除更新中断位,避免刚初始化完就进中断的问题
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
//使能中断
//更新中断
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);
//NVIC
//NVIC优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//定义结构体
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
//启动定时器
TIM_Cmd(TIM2,ENABLE);
}
//封装函数
//CNT计数器的值
uint16_t Timer_GetCounter(void)
{
return TIM_GetCounter(TIM2);
}
//中断函数
/*void TIM2_IRQHandler(void)
{
//检查中断标志位
//更新中断
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET)
{
//清除标志位
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}*/
Timer.h文件:
#ifndef __TIMER_H
#define __TIMER_H
//在头文件里声明函数
void Timer_Init(void);
uint16_t Timer_GetCounter(void);
#endif
八、TIM输出比较📌📌📌
1.输出比较介绍
OC(Output Compare)输出比较
IC(Input Compare)输入捕获
CC(Capture Compare)捕获比较
输出比较可以通过比较CNT与CCR(Capture Compare Register)捕获比较寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形
每个高级定时器和通用定时器都拥有4个输出比较通道高级定时器的前3个通道额外拥有死区生成和互补输出的功能
2.PWM介绍
PWM(Pulse Width Modulation)脉冲宽度调制
在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域
PWM参数: 频率 = 1 / TS 占空比 = TON / TS 分辨率 = 占空比变化步距
占空比决定了PWM等效出来的模拟电压的大小,占空比越大,等效的模拟电压就越趋近于高电平,否则反之
REF=reference:参考信号📶
输出比较模式:
| 模式 | 描述 |
|---|---|
| 冻结 | CNT=CCR时,REF保持为原状态 |
| 匹配时置有效电平 | 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置无效电平 |
匹配时置有/无效电平:这两个模式是一次性的,不适合输出连续变化的波形,适合用于定时输出一个一次性的信号
参数计算:
1.计数器的更新频率公式:
PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM的频率等于计数器的更新频率
2.PWM占空比: Duty = CCR / (ARR + 1)
3.PWM分辨率: Reso = 1 / (ARR + 1)
分辨率也就是占空比变化的步距,占空比变化的越细腻越好
3.舵机介绍
舵机是一种根据输入PWM信号占空比来控制输出角度的装置
输入PWM信号要求:周期为20ms,高电平宽度为0.5ms~2.5ms
硬件:

拆解图:

引脚定义图:
4.直流电机介绍
直流电机是一种将电能转换为机械能的装置,有两个电极,当电极正接时,电机正转,当电极反接时,电机反转
直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作
5.PWM驱动LED呼吸灯💡
此LED高电平点亮,低电平熄灭
占空比越大,LED越亮,否则反之
硬件连接:
新建工程项目:
新建PWM模块:

这几个库函数很重要📌⚠️:
4个初始化函数,对应4个输出比较单元/通道
void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC3Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC4Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
用结构体来初始化输出比较单元
下面这个函数只有高级定时器能使用,在使用高级定时器输出PWM时,需要调用这个函数使能主输出,否则PWM将不能正常输出:
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
这几个库函数也很重要📌⚠️:
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);
void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);
void TIM_SetCompare3(TIM_TypeDef* TIMx, uint16_t Compare3);
void TIM_SetCompare4(TIM_TypeDef* TIMx, uint16_t Compare4);
几种输出比较模式:
#define TIM_OCMode_Timing 冻结模式 ((uint16_t)0x0000)
#define TIM_OCMode_Active 相等时置有效电平 ((uint16_t)0x0010)
#define TIM_OCMode_Inactive 相等时置有效电平 ((uint16_t)0x0020)
#define TIM_OCMode_Toggle 相等时电平反转 ((uint16_t)0x0030)
#define TIM_OCMode_PWM1 PWM模式1 ((uint16_t)0x0060)
#define TIM_OCMode_PWM2 PWM模式2 ((uint16_t)0x0070)
#define TIM_OCPolarity_High ((uint16_t)0x0000) high,高极性, 极性不反转,REF波形直接输出,(有效电平是高电平,REF有效时,输出高电平)
#define TIM_OCPolarity_Low ((uint16_t)0x0002) low,低极性,REF电平取反,有效电平为低电平

如果想让定时器来控制引脚,需要使用复用开漏/推挽输出的模式
核心文件配置:
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
uint8_t i;
//主函数
int main(void)
{
OLED_Init();
PWM_Init();
while (1)
{
for (i=0;i<=100;i++)
{//一直调用函数
PWM_SetCompare1(i);//设置CCR寄存器的值
Delay_ms(10);
}
for (i=0;i<=100;i++)
{//一直调用函数
PWM_SetCompare1(100 - i);
Delay_ms(10);
}
}
}
PWM.c文件:
#include "stm32f10x.h" // Device header
//初始化
void PWM_Init(void)
{
//初始化定时器⏲️
//RCC开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//定义结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
//用点运算符引出成员
//推挽输出
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
//调用函数
//第一个参数是外设,第二个参数是结构体变量名
GPIO_Init(GPIOA,&GPIO_InitStructure);
//选择时基单元的时钟
//内部时钟(默认就是内部时钟,所以不写也可)
TIM_InternalClockConfig(TIM2);
//配置时基单元
//定义结构体
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
//分频
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
//计数器模式 向上计数
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
//ARR自动重装器(减去偏差)
TIM_TimeBaseInitStructure.TIM_Period=100 - 1;//ARR
//PSC预分频器(减去偏差)
TIM_TimeBaseInitStructure.TIM_Prescaler=720 - 1;//PSC
//重复计数器(高级定时器才有的,现在我们这个定时器用不了)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
//初始化输出比较单元
//定义结构体
TIM_OCInitTypeDef TIM_OCInitStructure;
//给结构体赋初始值
TIM_OCStructInit(&TIM_OCInitStructure);
//模式
TIM_OCInitStructure.TIM_OCMode=TIM_OCMode_PWM1;
//极性
TIM_OCInitStructure.TIM_OCPolarity=TIM_OCPolarity_High;
//使能(输出状态)
TIM_OCInitStructure.TIM_OutputState=TIM_OutputState_Enable;
//CCR
TIM_OCInitStructure.TIM_Pulse=0;//CRR
//取地址
TIM_OC1Init(TIM2,&TIM_OCInitStructure);
//启动定时器
TIM_Cmd(TIM2,ENABLE);
}
void PWM_SetCompare1(uint16_t Compare)
{
//调用函数
TIM_SetCompare1(TIM2,Compare);
}
PWM.h文件:
#ifndef __PWM_H
#define __PWM_H
//声明函数
void PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);
#endif
6.PWM驱动舵机
舵机硬件:

硬件连接:

舵机的GND接负极,5V引脚接ST-LINK,PWM接引脚PA1
新建工程项目:

舵机输入PWM信号要求:周期为20ms,高电平宽度为0.5ms~2.5ms

代码写好之后,你往左⬅️掰一下,会感到有一股力你掰不动
我们改一下参数,原来的500->2500:
原本:
现在:

对应180度
接着改为1500,对应90度:
新建模块:

每按一下按键舵机的角度,就会增加30度,直到180度,它会复位

核心文件配置:
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Servo.h"
#include "Key.h"
uint8_t KeyNum;
float Angle;
//主函数
int main(void)
{
OLED_Init();
Servo_Init();
Key_Init();
OLED_ShowString(1,1,"Angle:");
while (1)
{
KeyNum=Key_GetNum();
if(KeyNum == 1)
{
Angle+=30;
if (Angle>180)
{
Angle=0;
}
}
Servo_SetAngle(Angle);
OLED_ShowNum(1,7,Angle, 3);
{
}
}
}
Servo.c文件:
#include "stm32f10x.h" // Device header
#include "PWM.h"
void Servo_Init(void)
{
//初始化
PWM_Init();
}
//0° 对应 500
//180° 2500
void Servo_SetAngle(float Angle)
{
//完成角度映射(计算一下)
PWM_SetCompare2(Angle / 180 * 2000 + 500);
}
Servo.h文件:
#ifndef __SERVO_H
#define __SERVO_H
//声明函数
void Servo_Init(void);
void Servo_SetAngle(float Angle);
#endif
7.PWM驱动直流电机
电机驱动模块:
千万要仔细一点,不要接反了,以免烧坏驱动
VM引脚,电机电源,接在ST-LINK5V引脚
VCC逻辑电源,接在面包板3.3V正极
GND电源负极,接在面包板的负极
AO1,AO2,是电机的输出端,接在电机的两根线上
直流电机:
硬件连接部分:


新建工程项目:


现在按按键可以控制方向和速度的大小
核心文件配置:
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Motor.h"
#include "Key.h"
uint8_t KeyNum;
int8_t Speed;
//主函数
int main(void)
{
OLED_Init();
Motor_Init();
Key_Init();
OLED_ShowString(1,1,"Speed:");
while (1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)
{
Speed +=20;
if (Speed >100)
{
Speed = -100;
}
}
//正转50
Motor_SetSpeed(Speed);
OLED_ShowSignedNum(1,7,Speed,3);
}
}
Motor.c文件:
#include "stm32f10x.h" // Device header
//继承PWM模块
#include "PWM.h"
void Motor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//定义结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
//用点运算符引出成员
//推挽输出
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
//初始化方向控制的两个引脚
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4 | GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
//调用函数
//第一个参数是外设,第二个参数是结构体变量名
GPIO_Init(GPIOA,&GPIO_InitStructure);
//初始化
PWM_Init();
}
//速度函数🏎️💨
void Motor_SetSpeed(int8_t Speed)
{
//正转
if (Speed >= 0)
{
GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_ResetBits(GPIOA, GPIO_Pin_5);
PWM_SetCompare3(Speed);
}
//反转
else{
GPIO_SetBits(GPIOA, GPIO_Pin_5);
GPIO_ResetBits(GPIOA, GPIO_Pin_4);
PWM_SetCompare3(-Speed);
}
}
Motor.h文件:
#ifndef __MOTOR_H
#define __MOTOR_H
void Motor_Init(void);
void Motor_SetSpeed(int8_t Speed);
#endif
九、TIM输入捕获
1.输入捕获介绍
IC(Input Capture)输入捕获
输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率/占空比/脉冲间隔/电平持续时间等
每个高级定时器和通用定时器都拥有4个输入捕获通道
基本定时器没有输入捕获通道
可配置为PWMI模式(它的输入模式),同时测量频率和占空比
可配合主从触发模式,实现硬件全自动测量
2.频率测量
stm32测频率只能测量数字信号
如果你要测量正弦波,需要搭建一个信号预处理电路
把正弦波转换为数字信号,再输入给stm32
经过处理,最终输入给STM 32的信号,是高低电平信号,高电平3.3v,低电平0v
1.测频法:在闸门时间T内,对上升沿计次,得到N,则频率(测频法适合测量高频信号)
(频率就是1秒内出现了多少个重复的周期,那么频率就是多少hz)
2.测周法:两个上升沿内,以标准频率fc计次,得到N ,则频率fx=fc/ N(测周法适合测量低频信号)
(周期的倒数就是频率)
上升沿用于触发输入捕获,CNT用于计数计时,每经过一个上升沿,取一下CNT的值,自动存在CCR里,CCR捕获到的值,就是计数值N,CT的驱动时钟就是fc,每次捕获之后,我们都要把CNT清零一下,这样下次上升沿再捕获的时候,取出的CNT才是两个上升沿的时间间隔
3.中界频率:测频法与测周法误差相等的频率点fm=√fc / T
滤波器的工作原理:数字滤波器由一个事件计数器组成,它记录到N个事件后,会产生一个输出的跳变,以采样频率对输入信号进行采样,当连续N个值都为高电平,输出才为高电平,否则反之,当连续N个值都为低电平,输出才为低电平。当信号出现高频抖动时,导致连续采样N个值不是全都一样的,那么输出就不会变化,这样就可以达到滤波的效果。采样的频率越低,采样个数N越大,滤波效果就越好。
主模式:可以将定时器内部的信号映射到TRGO引脚,用于触发别的外设
从模式:接收其他外设/自身外设的一些信号,用于控制自身定时器的运行,被别的信号控制
触发源选择:选择从模式的触发信号源,选择指定的一个信号,得到TRGI,TRGI去触发从模式
3.输入捕获模式测频率
硬件连接部分:
新建工程项目:
新建模块:


稍微有一点点🤏🏻误差:
把代码改一下:
这下好了:
核心文件配置:
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
//主函数
int main(void)
{
OLED_Init();
PWM_Init();
IC_Init();
OLED_ShowString(1,1,"Freq:00000Hz");
//调用
PWM_SetPrescaler(720 - 1); //Freq=72M / (PSC+1) /100 计算频率
PWM_SetCompare1(50); //Duty=CCR/ 100 计算占空比
while (1)
{
OLED_ShowNum(1,6,IC_GetFreq(),5);
}
}
IC.c文件:
#include "stm32f10x.h" // Device header
//初始化
void IC_Init(void)
{
//输入捕获的定时器⏲️用TIM3
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//定义结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
//用点运算符引出成员
//上拉输入
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
//调用函数
//第一个参数是外设,第二个参数是结构体变量名
GPIO_Init(GPIOA,&GPIO_InitStructure);
//选择时基单元的时钟
//内部时钟(默认就是内部时钟,所以不写也可)
TIM_InternalClockConfig(TIM3);
//配置时基单元
//定义结构体
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
//分频
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
//计数器模式 向上计数
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
//ARR自动重装器(减去偏差)(值给大一点,防止溢出)
TIM_TimeBaseInitStructure.TIM_Period=65536 - 1;//ARR
//PSC预分频器(减去偏差)
TIM_TimeBaseInitStructure.TIM_Prescaler=72 - 1;//PSC
//重复计数器(高级定时器才有的,现在我们这个定时器用不了)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
//定义结构体
TIM_ICInitTypeDef TIM_ICInitStructure;
//结构体成员
//指定要配置的通道
TIM_ICInitStructure.TIM_Channel=TIM_Channel_1;
//选择输入捕获的滤波器
TIM_ICInitStructure.TIM_ICFilter=0xF;
//极性 选择上升沿触发
TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising;
//分频器 我们需要每次触发都有效,所以选择不分频
TIM_ICInitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1;
//选择触发信号📶从哪个引脚输入 直连通道输入
TIM_ICInitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI;
//取地址
TIM_ICInit(TIM3,&TIM_ICInitStructure);
//触发源选择
TIM_SelectInputTrigger(TIM3,TIM_TS_TI1FP1);
//从模式
TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);
//调用函数
TIM_Cmd(TIM3,ENABLE);
}
uint32_t IC_GetFreq(void)
{ //N就是读取CCR的值
return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}
滤波器计次不会改变信号的原有频率,滤波器的采样频率会远高于信号频率,它会滤除高频噪声,使信号更加平滑,信号频率不会发生变化,该是多少还是多少。
分频器对信号本身进行计次会改变频率,例如2000Hz二分频之后就是1000Hz,四分频之后就是500Hz。
IC.h文件:
#ifndef __IC_H
#define __IC_H
//声明函数
void IC_Init(void);
uint32_t IC_GetFreq(void);
#endif
PWM.c文件:
#include "stm32f10x.h" // Device header
//初始化
void PWM_Init(void)
{
//初始化定时器⏲️
//RCC开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//定义结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
//用点运算符引出成员
//推挽输出
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
//调用函数
//第一个参数是外设,第二个参数是结构体变量名
GPIO_Init(GPIOA,&GPIO_InitStructure);
//选择时基单元的时钟
//内部时钟(默认就是内部时钟,所以不写也可)
TIM_InternalClockConfig(TIM2);
//配置时基单元
//定义结构体
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
//分频
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
//计数器模式 向上计数
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
//ARR自动重装器(减去偏差)
TIM_TimeBaseInitStructure.TIM_Period=100 - 1;//ARR
//PSC预分频器(减去偏差)
TIM_TimeBaseInitStructure.TIM_Prescaler=720 - 1;//PSC
//重复计数器(高级定时器才有的,现在我们这个定时器用不了)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
//初始化输出比较单元
//定义结构体
TIM_OCInitTypeDef TIM_OCInitStructure;
//给结构体赋初始值
TIM_OCStructInit(&TIM_OCInitStructure);
//模式
TIM_OCInitStructure.TIM_OCMode=TIM_OCMode_PWM1;
//极性
TIM_OCInitStructure.TIM_OCPolarity=TIM_OCPolarity_High;
//使能(输出状态)
TIM_OCInitStructure.TIM_OutputState=TIM_OutputState_Enable;
//CCR
TIM_OCInitStructure.TIM_Pulse=0;//CCR
//取地址
TIM_OC1Init(TIM2,&TIM_OCInitStructure);
//启动定时器
TIM_Cmd(TIM2,ENABLE);
}
void PWM_SetCompare1(uint16_t Compare)
{
//调用函数
TIM_SetCompare1(TIM2,Compare);
}
//初始化之后单独修改PSC
void PWM_SetPrescaler(uint16_t Prescaler)
{
//调用库函数单独写入PSC
TIM_PrescalerConfig(TIM2,Prescaler,TIM_PSCReloadMode_Immediate);
}
PWM.h文件:
#ifndef __PWM_H
#define __PWM_H
//声明函数
void PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);
void PWM_SetPrescaler(uint16_t Prescaler);
#endif
4.PWMI模式测频率占空比
新建项目工程:

跳转TIM_PWMIConfig函数定义:
void TIM_PWMIConfig(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct)
{
uint16_t icoppositepolarity = TIM_ICPolarity_Rising;
uint16_t icoppositeselection = TIM_ICSelection_DirectTI;
/* Check the parameters */
assert_param(IS_TIM_LIST6_PERIPH(TIMx));
/* Select the Opposite Input Polarity */
if (TIM_ICInitStruct->TIM_ICPolarity == TIM_ICPolarity_Rising)
{
icoppositepolarity = TIM_ICPolarity_Falling;
}
else
{
icoppositepolarity = TIM_ICPolarity_Rising;
}
/* Select the Opposite Input */
if (TIM_ICInitStruct->TIM_ICSelection == TIM_ICSelection_DirectTI)
{
icoppositeselection = TIM_ICSelection_IndirectTI;
}
else
{
icoppositeselection = TIM_ICSelection_DirectTI;
}
if (TIM_ICInitStruct->TIM_Channel == TIM_Channel_1)
{
/* TI1 Configuration */
TI1_Config(TIMx, TIM_ICInitStruct->TIM_ICPolarity, TIM_ICInitStruct->TIM_ICSelection,
TIM_ICInitStruct->TIM_ICFilter);
/* Set the Input Capture Prescaler value */
TIM_SetIC1Prescaler(TIMx, TIM_ICInitStruct->TIM_ICPrescaler);
/* TI2 Configuration */
TI2_Config(TIMx, icoppositepolarity, icoppositeselection, TIM_ICInitStruct->TIM_ICFilter);
/* Set the Input Capture Prescaler value */
TIM_SetIC2Prescaler(TIMx, TIM_ICInitStruct->TIM_ICPrescaler);
}
else
{
/* TI2 Configuration */
TI2_Config(TIMx, TIM_ICInitStruct->TIM_ICPolarity, TIM_ICInitStruct->TIM_ICSelection,
TIM_ICInitStruct->TIM_ICFilter);
/* Set the Input Capture Prescaler value */
TIM_SetIC2Prescaler(TIMx, TIM_ICInitStruct->TIM_ICPrescaler);
/* TI1 Configuration */
TI1_Config(TIMx, icoppositepolarity, icoppositeselection, TIM_ICInitStruct->TIM_ICFilter);
/* Set the Input Capture Prescaler value */
TIM_SetIC1Prescaler(TIMx, TIM_ICInitStruct->TIM_ICPrescaler);
}
}
根据上面的if,else条件判断,如果传入的是rising,则对向就是falling,否则我传入的是falling,对向就是rising。如果我选择的是直连,对向就是交叉;否则,反之。如果我选择配置通道1,那么结构体参数配置到TI1,对向参数配置到TI2;否则,反之。简而言之,就是,我们传入任意一个通道的参数,都会把另一个通道配置成相反的参数。
这个函数只支持通道1和通道2的配置
看现象:
改一下参数再试一下:

核心文件配置:
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
//主函数
int main(void)
{
OLED_Init();
PWM_Init();
IC_Init();
//显示频率
OLED_ShowString(1,1,"Freq:00000Hz");
//显示占空比
OLED_ShowString(2,1,"Duty:00%");
//调用
PWM_SetPrescaler(7200 - 1); //Freq=72M / (PSC+1) /100 计算频率
PWM_SetCompare1(80); //Duty=CCR/ 100 计算占空比
while (1)
{
OLED_ShowNum(1,6,IC_GetFreq(),5);
OLED_ShowNum(2,6,IC_GetDuty(),2);
}
}
IC.c文件:
#include "stm32f10x.h" // Device header
//初始化
void IC_Init(void)
{
//输入捕获的定时器⏲️用TIM3
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//定义结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
//用点运算符引出成员
//上拉输入
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
//调用函数
//第一个参数是外设,第二个参数是结构体变量名
GPIO_Init(GPIOA,&GPIO_InitStructure);
//选择时基单元的时钟
//内部时钟(默认就是内部时钟,所以不写也可)
TIM_InternalClockConfig(TIM3);
//配置时基单元
//定义结构体
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
//分频
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
//计数器模式 向上计数
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
//ARR自动重装器(减去偏差)(值给大一点,防止溢出)
TIM_TimeBaseInitStructure.TIM_Period=65536 - 1;//ARR
//PSC预分频器(减去偏差)
TIM_TimeBaseInitStructure.TIM_Prescaler=72 - 1;//PSC
//重复计数器(高级定时器才有的,现在我们这个定时器用不了)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
//定义结构体
TIM_ICInitTypeDef TIM_ICInitStructure;
//结构体成员
//指定要配置的通道
TIM_ICInitStructure.TIM_Channel=TIM_Channel_1;
//选择输入捕获的滤波器
TIM_ICInitStructure.TIM_ICFilter=0xF;
//极性 选择上升沿触发
TIM_ICInitStructure.TIM_ICPolarity=TIM_ICPolarity_Rising;
//分频器 我们需要每次触发都有效,所以选择不分频
TIM_ICInitStructure.TIM_ICPrescaler=TIM_ICPSC_DIV1;
//选择触发信号📶从哪个引脚输入 直连通道输入
TIM_ICInitStructure.TIM_ICSelection=TIM_ICSelection_DirectTI;
//调用封装好的函数
TIM_PWMIConfig(TIM3,&TIM_ICInitStructure);
//触发源选择
TIM_SelectInputTrigger(TIM3,TIM_TS_TI1FP1);
//从模式
TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);
//调用函数
TIM_Cmd(TIM3,ENABLE);
}
uint32_t IC_GetFreq(void)
{ //N就是读取CCR的值
//获取频率
return 1000000 / (TIM_GetCapture1(TIM3) + 1);
}
//获取占空比的函数
uint32_t IC_GetDuty(void)
{
//高电平的计数值存在CCR2里,整个周期的计数值存在CCR1里
return (TIM_GetCapture2(TIM3)+1) * 100 / (TIM_GetCapture1(TIM3)+1);
}
IC.h文件:
#ifndef __IC_H
#define __IC_H
//声明函数
void IC_Init(void);
uint32_t IC_GetFreq(void);
uint32_t IC_GetDuty(void);
#endif
十、TIM编码器接口
1.编码器接口介绍
Encoder Interface 编码器接口
工作流程:
编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度
每个高级定时器和通用定时器都拥有1个编码器接口
两个输入引脚借用了输入捕获的通道1和通道2
每个定时器的CH1和CH2引脚,CH3和CH4不能接编码器
正交编码器可以测量位置/带有方向的速度值,一般有两个信号输出引脚,一个是A相,一个是B相
正交信号精度更高,因为A相和B相都可以计次,相当于计次频率提高了一倍;而且正交信号可以抗噪声,正交信号的两个信号是交替跳变的,可以用来设计一个抗噪声电路,如果一个信号不变,另一个信号连续跳变,也就是产生了噪声,那么计次值是不会变化的。
工作模式
拿上边的两个表来这里一一对应一下,会发现:正转时都向上计数,反转时都向下计数
一般情况下,我们都使用最下面这个模式:两个引脚的边沿都计数,因为它的计数精度最高
编码器接口模式基本上相当于使用了一个带有方向选择的外部时钟。

具体可以参考STM32F10xxx参考手册
2.编码器接口测速
新建项目工程:
硬件连接:
新建模块:
TIM_EncoderInterfaceConfig:定时器编码器接口配置函数
是选择上拉输入,还是选择下拉输入:看一下接在这个引脚的外部模块输出的默认电平是高电平还是低电平,如果外部模块默认输出高电平,选择上拉输入,默认输入高电平;否则,反之。和外部模块保持默认状态一致
初始CNT值为0:
向右旋转编码器,CNT值变为6:

把函数改成int16_t Encoder_Get(void),借用补码的特性,完成负数转换:
向左转,变为负数:
实现极性反转两种办法:
1.硬件层面:把A/B相两根线调换
2.软件层面:改变两个输入通道的极性,把任意一个极性反转一下,方向就会反过来
,如果两个极性都反转,极性依旧保持不变
静止时速度显示为0:

向右转速度为正值:
核心文件配置:
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
#include "Encoder.h"
//定义全局变量
int16_t Speed;
//主函数
int main(void)
{
OLED_Init();
//初始化定时器
Timer_Init();
Encoder_Init();
OLED_ShowString(1,1,"Speed:");
while (1)
{
//调用函数
OLED_ShowSignedNum(1,7,Speed,5);
// //延迟
// //提高速度刷新🔄频率,并防止计数器溢出
// Delay_ms(1000); //1000ms=1s
}
}
//中断函数
void TIM2_IRQHandler(void)
{
//检查中断标志位
//更新中断
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET)
{
Speed=Encoder_Get();
//清除标志位
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
Encoder.c文件:
#include "stm32f10x.h" // Device header
//初始化
void Encoder_Init(void)
{
//输入捕获的定时器⏲️用TIM3
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//定义结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
//用点运算符引出成员
//上拉输入
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6 | GPIO_Pin_7 ;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
//调用函数
//第一个参数是外设,第二个参数是结构体变量名
GPIO_Init(GPIOA,&GPIO_InitStructure);
//选择时基单元的时钟
//配置时基单元
//定义结构体
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
//分频
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
//计数器模式 向上计数
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;
//ARR自动重装器(减去偏差)(值给大一点,防止溢出)满量程计数
TIM_TimeBaseInitStructure.TIM_Period=65536 - 1;//ARR
//PSC预分频器(减去偏差)
TIM_TimeBaseInitStructure.TIM_Prescaler=1 - 1;//PSC
//重复计数器(高级定时器才有的,现在我们这个定时器用不了)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
//定义结构体
TIM_ICInitTypeDef TIM_ICInitStructure;
//给结构体赋初始值
TIM_ICStructInit(&TIM_ICInitStructure);
//结构体成员
//指定要配置的通道
TIM_ICInitStructure.TIM_Channel=TIM_Channel_1;
//选择输入捕获的滤波器
TIM_ICInitStructure.TIM_ICFilter=0xF;
//取地址
TIM_ICInit(TIM3,&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel=TIM_Channel_2;
//选择输入捕获的滤波器
TIM_ICInitStructure.TIM_ICFilter=0xF;
//取地址
TIM_ICInit(TIM3,&TIM_ICInitStructure);
//配置编码器接口
//调用函数
//两个通道都计数,选择不反相
TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
TIM_Cmd(TIM3,ENABLE);
}
int16_t Encoder_Get(void)
{
//用Temp缓存
int16_t Temp;
Temp=TIM_GetCounter(TIM3);
//先读取CNT,再清零操作
TIM_SetCounter(TIM3,0);
return Temp;
}
Encoder.h文件:
#ifndef __ENCODER_H
#define __ENCODER_H
//声明函数
void Encoder_Init(void);
int16_t Encoder_Get(void);
#endif
十一、ADC模数转换器
1.ADC介绍
ADC(Analog-Digital Converter)模拟-数字转换器
ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁
12位逐次逼近型(工作模式)ADC,1us转换时间
输入电压范围:0~ 3.3V,转换结果范围:0~4095
18个输入通道,可测量16个外部(最多)和2个内部信号源
规则组(常规)和注入组(突发事件)两个转换单元
模拟看门狗自动监测输入电压范围(阈值)
STM32F103C8T6 ADC资源:ADC1、ADC2,10个外部输入通道
逐次逼近型ADC:

IN0~IN7是8个通道
ADC转换是一个很快的过程
如果想转换多路信号,一个AD转换器,然后加一个多路选择开关,想转换哪一路,先拨一下开关,选中对应通道,再开始转换就行
电压对应的编码数据,用逐次逼近的方法来比较,电压比较器可以判断两个输入信号电压的大小关系,输出一个高低电平,看谁大谁小,输入端:一个是待测电压,一个是DAC的电压输出端,DAC是数模转换器,给其一个数据,它就可以输出数据对应的电压,DAC内部是使用加权电阻网络来实现转化,用二分法
EOC(End Of Convert)是转换结束信号📶
STM32ADC框图📌⚠️:

2.触发控制

需要用AFIO重映射来确定是引脚还是定时器
ADC是12位的,它的转换结果是一个12位的数据
3.数据对齐
3.1数据右对齐
12位的数据向右靠,高位多出来的几位补0,一般使用数据右对齐,这样读取这个16位的寄存器,直接就是转换结果
3.2数据左对齐

4.时序图

5.DMA请求
因为规则通道转换的值储存在一个仅有的数据寄存器中,所以当转换多个规则通道时需要使用
DMA,这可以避免丢失已经存储在ADC_DR寄存器中的数据。
只有在规则通道的转换结束时才产生DMA请求,并将转换的数据从ADC_DR寄存器传输到用户
指定的目的地址。
注: 只有ADC1和ADC3拥有DMA功能。由ADC2转化的数据可以通过双ADC模式,利用ADC1的DMA功能传输。
6.AD单通道
硬件部分:
蓝色这个是电位器
电位器左右两个引脚接的是电阻的两个固定端,中间这个引脚接的是滑动抽头
中间这个十字形的卡槽可以拧动,往左拧抽头就往左靠,往右拧抽头就往右靠,所以左边的固定端接在负极,右边的固定端接在正极,中间就可以输出从负极到正极可调的电压了,右边把可调的电压输出接在PA0口,从B1到A0引脚,都是ADC的通道,都可以接模拟电压
新建项目工程:
新建AD模块:

中断输出控制函数:
void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState);
获取标志位状态函数:
FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);
配置间断模式的函数:
1.每隔几个通道间断一次:
void ADC_DiscModeChannelCountConfig(ADC_TypeDef* ADCx, uint8_t Number);
2.是否启用间断模式:
void ADC_DiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
3.ADC规则组通道配置函数⚠️📌:
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);
第一个参数是使用的ADC硬件外设(编号),第二个参数是要指定的通道,第三个参数是序列几的位置,第四个参数是指定通道的采样时间
4.ADC获取转换值函数⚠️📌:
uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);
获取AD转换的数据寄存器,读取转换结果要使用这个函数
GPIO_Mode_AIN模式:模拟输入模式,GPIO口是无效的,断开GPIO防止GPIO口的输入/出对模拟电压造成干扰,是ADC下使用的模式
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;
在规则组列表的第一个位置,写入通道0:
//规则组的输入通道
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
核心文件配置:
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
uint16_t ADValue;
float Voltage;
//主函数
int main(void)
{
OLED_Init();
AD_Init();
OLED_ShowString(1,1,"ADValue:");
OLED_ShowString(2,1,"Voltage:0.00V");
while (1)
{
ADValue=AD_GetValue();
//强制类型转换
Voltage=(float)ADValue/4095*3.3;
OLED_ShowNum(1,9,ADValue,4);
OLED_ShowNum(2,9,Voltage,1);
OLED_ShowNum(2,11,(uint16_t)(Voltage*100)%100,2);
Delay_ms(100);
}
}
AD.c文件:
#include "stm32f10x.h" // Device header
//初始化
void AD_Init(void)
{
//开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
//开启GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//配置ADCCLK 分频
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//配置GPIO
//定义结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
//用点运算符引出成员
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
//调用函数
//第一个参数是外设,第二个参数是结构体变量名
GPIO_Init(GPIOA,&GPIO_InitStructure);
//规则组的输入通道
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
//用结构体初始化ADC
//定义结构体变量
ADC_InitTypeDef ADC_InitStructure;
//用点运算符引出成员
//连续转换模式 单次转换
ADC_InitStructure.ADC_ContinuousConvMode=DISABLE;
//数据对齐
ADC_InitStructure.ADC_DataAlign=ADC_DataAlign_Right;
//外部触发转换选择,触发控制的触发源,...None就是不使用外部触发,使用内部软件触发
ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
//ADC工作模式
ADC_InitStructure.ADC_Mode=ADC_Mode_Independent;
//通道数目
ADC_InitStructure.ADC_NbrOfChannel=1;
//扫描转换模式 非扫描
ADC_InitStructure.ADC_ScanConvMode=DISABLE;
//取地址
ADC_Init(ADC1,&ADC_InitStructure);
//开启ADC电源🔌
ADC_Cmd(ADC1,ENABLE);
//校准ADC
//复位校准
ADC_ResetCalibration(ADC1);
//返回复位校准的状态
while ( ADC_GetResetCalibrationStatus(ADC1) == SET);
//开始校准
ADC_StartCalibration(ADC1);
//获取校准状态
while (ADC_GetCalibrationStatus(ADC1) == SET);
}
uint16_t AD_GetValue(void)
{
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
while ( ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET);
return ADC_GetConversionValue(ADC1);
}
AD.h文件:
#ifndef __AD_H
#define __AD_H
//声明函数
void AD_Init(void);
uint16_t AD_GetValue(void);
#endif
7.AD多通道
使用四个AD通道,第一个通道是电位器接在PA0口,其他三个通道接3个传感器模块,光敏/热敏/反射式红外传感器,VCC都接在正极,GND都接在负极,三个传感器的AO模拟量输出引脚,分别接在PA1/PA2/PA3口
反射式红外传感器:


硬件部分:
新建项目工程:

核心文件配置:
main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
//定义变量
uint16_t AD0,AD1,AD2,AD3;
//主函数
int main(void)
{
OLED_Init();
AD_Init();
OLED_ShowString(1,1,"AD0:");
OLED_ShowString(2,1,"AD1:");
OLED_ShowString(3,1,"AD2:");
OLED_ShowString(4,1,"AD3:");
while (1)
{
//单次转换非扫描模式,实现AD多通道
//启动四次转换 指定了通道
AD0=AD_GetValue(ADC_Channel_0);
AD1=AD_GetValue(ADC_Channel_1);
AD2=AD_GetValue(ADC_Channel_2);
AD3=AD_GetValue(ADC_Channel_3);
//OLED显示
OLED_ShowNum(1,5,AD0,4);
OLED_ShowNum(2,5,AD1,4);
OLED_ShowNum(3,5,AD2,4);
OLED_ShowNum(4,5,AD3,4);
Delay_ms(100);
}
}
AD.c文件:
#include "stm32f10x.h" // Device header
//初始化
void AD_Init(void)
{
//开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
//开启GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//配置ADCCLK 分频
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//配置GPIO
//定义结构体变量
GPIO_InitTypeDef GPIO_InitStructure;
//用点运算符引出成员
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 ;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
//调用函数
//第一个参数是外设,第二个参数是结构体变量名
GPIO_Init(GPIOA,&GPIO_InitStructure);
//用结构体初始化ADC
//定义结构体变量
ADC_InitTypeDef ADC_InitStructure;
//用点运算符引出成员
//连续转换模式 单次转换
ADC_InitStructure.ADC_ContinuousConvMode=ENABLE;
//数据对齐
ADC_InitStructure.ADC_DataAlign=ADC_DataAlign_Right;
//外部触发转换选择,触发控制的触发源,...None就是不使用外部触发,使用内部软件触发
ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
//ADC工作模式
ADC_InitStructure.ADC_Mode=ADC_Mode_Independent;
//通道数目
ADC_InitStructure.ADC_NbrOfChannel=1;
//扫描转换模式 非扫描
ADC_InitStructure.ADC_ScanConvMode=DISABLE;
//取地址
ADC_Init(ADC1,&ADC_InitStructure);
//开启ADC电源🔌
ADC_Cmd(ADC1,ENABLE);
//校准ADC
//复位校准
ADC_ResetCalibration(ADC1);
//返回复位校准的状态
while ( ADC_GetResetCalibrationStatus(ADC1) == SET);
//开始校准
ADC_StartCalibration(ADC1);
//获取校准状态
while (ADC_GetCalibrationStatus(ADC1) == SET);
}
//指定转换的通道
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
//规则组的输入通道
ADC_RegularChannelConfig(ADC1,ADC_Channel_1,1,ADC_SampleTime_55Cycles5);
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
while ( ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET);
return ADC_GetConversionValue(ADC1);
}
AD.h文件:
#ifndef __AD_H
#define __AD_H
//声明函数
void AD_Init(void);
uint16_t AD_GetValue(uint8_t ADC_Channel);
#endif
十二、DMA直接存储器存取
1.DMA介绍
DMA(Direct Memory Access)直接存储器存取
DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源
12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)
每个通道都支持软件触发和特定的硬件触发
存储器到存储器的数据转运使用软件触发;外设到存储器的数据转运使用硬件触发
每个DMA的通道,它的硬件触发源是不一样的
STM32F103C8T6 DMA资源:DMA1(7个通道)
2.存储器映像
| 类型 | 起始地址 | 存储器 | 用途 |
|---|---|---|---|
| ROM | 0x0800 0000 | 程序存储器Flash(主闪存) | 存储C语言编译后的程序代码 |
| ROM | 0x1FFF F000 | 系统存储器 | 存储BootLoader,用于串口下载 |
| ROM | 0x1FFF F800 | 选项字节 | 存储一些独立于程序代码的配置参数 |
| RAM | 0x2000 0000 | 运行内存SRAM | 存储运行过程中的临时变量 |
| RAM | 0x4000 0000 | 外设寄存器 | 存储各个外设的配置参数 |
| RAM | 0xE000 0000 | 内核外设寄存器 | 存储内核各个外设的配置参数 |
ROM是只读存储器,非易失性,断电不丢失;RAM是随机存储器,易失性,断电丢失
reserved区域是保留区域,没有被使用
3.DMA框图

Flash是主闪存(CPU/DMA直接访问的话,只读不可写),SRAM是运行内存(可以任意读取)
寄存器是连接软件和硬件的桥梁
总线矩阵的左端是主动单元,拥有存储器的访问权;右端是被动端元,它们的存储器只能被左边的主动单元读写
DCode总线是专门访问Flash的,系统总线是访问其他东西的
DMA请求是DMA的硬件触发源,用于硬件触发DMA的数据转运
仲裁器用于调度各个通道,防止产生冲突
用于硬件触发DMA的数据转运
外设的3个参数:
起始地址:外设端/存储器端的起始地址;
数据宽度:指定一次转运要按多大的数据宽度来进行,例如bite(8位,uint8_t)/ half word(16位,uint16_t) /word(32位,uint32_t);
地址是否自增:指定一次转运完成后,下一次转运是不是要把地址移到下一个位置去,相当于指针
传输计数器: 指定总共需要转运几次,是一个自减计数器,转运过程中,每转运一次,计数器的数就会减1,减到0之后DMA就不会再进行数据转运了,而且之前自增的地址也会恢复到起始地址的位置
自动重装器:当传输计数器减到0之后,是否要自动恢复到最初的值,如果不使用自动重装器,DMA就结束;否则就会立即重装到初始值。它决定了转运的模式,如果不重装,就是正常的单次模式;如果重装,就是循环模式
M2M:Memory to memory,存储器到存储器
DMA进行转运,有几个条件:一,开关控制DMA_Cmd必须使能(ENABLE);二,传输计数器必须大于0;三,触发源,必须有触发信号,触发一次,转运一次,传输计数器自减一次,当传输计数器等于零,且没有自动重装时,无论是否触发,DMA都不会再进行转运,这时需要DMA_Cmd参数为DISABLE,关闭DMA,再为传输计数器写入一个大于零的数,再DMA_Cmd使能(ENABLE),开启DMA,DMA才能继续工作
写传输计数器时,必须要先关闭DMA再进行,不能在DMA开启时写传输计数器
4.DMA请求

5.数据宽度与对齐

如果目标宽度比源端宽度要大,就在目标数据前面多出来的空位补0即可
如果目标宽度比源端宽度要小,把多出来的高位舍去不要
更多推荐


所有评论(0)