新建工程
1.创建一个存放工程的文件夹,自定义重命名STM32Project,工程都存在这个文件夹下,方便管理。
2.打开keil5软件,点击Project→New uVision Project,然后选择我们刚才新建的文件夹,在这里面要再新建一个文件夹,用来存放本次的工程,起名2-1 STM32工程模板,然后点进去,接下来给工程文件起名,在此我们可以取一个通用的名字Project,然后点击保存(这个工程是干什么的我们可以用文件夹名称说明,文件夹的名称方便修改)
3.选择芯片型号STM32F103C8,点击OK,然后会弹出Keil软件的一个新建工程小助手,这个可以帮助我们快速新建工程,我们暂时不使用,可以叉掉。
4.打开固件库的文件夹,Libraries→CMSIS→DeviceSupport→ST→STM32F10x→startup→arm
这些就是STM32的启动文件,STM32的程序就是从启动文件开始执行的,我们将这些文件全部都复制下来,然后回到工程模板文件夹里,新建一个文件夹,取名Start,把启动文件粘贴到这里面。接着我们回到STM32F10x文件夹,可以看到stm32f10x.h和两个system开头的文件
这个stm32f10x.h,就是STM32的外设寄存器描述文件,作用与51单片机的头文件REGX52.H一样,是用来描述STM32有哪些寄存器和它对应的地址的。这两个system开头的文件是用来配置时钟的,STM32主频72MHz,就是system文件里的函数配置的。我们将这三个文件复制下来,也粘贴到Start文件夹下。
接下来,因为STM32是内核和内核外围的设备组成的,而且这个内核的寄存器描述文件和外围设备的描述文件不是在一起的,所以我们还需添加一个内核寄存器的描述文件。
打开CM3→CoreSupport
这两个cm3(Cortex-M3)文件就是内核的寄存器描述,还自带了一些内核的配置函数,所以多了给.c文件,我们把它两一并复制下来,也粘贴到Start文件夹下。
5.回到keil5软件,把我们刚才复制的那些文件添加到工程里来,我们可以点击Souce Group 1,然后再单击一下,把这个组改一下名字,也叫Start,接着右键,选择添加已经存在的文件到组里来,打开Start文件夹,文件类型选择All files,我们首先添加一下启动文件,这个启动文件有很多分类,我们只能添加其中一个,视频所用型号需要选择后缀为md.s的启动文件,其他的.c.h文件都添加进去,如下图
最后我们还需要在工程选项里添加上这个文件夹的头文件路径(否则软件找不到.h文件),我们点击魔术棒按钮,打开工程选项,在C/C++找到这个Include Paths栏,然后点击右边三个点的按钮,
在这里新建路径,然后再点三个点的按钮,把Start的路径添加进来,点击OK,这样就把这个头文件的路径添加进来了,接下来我们再新建一个main函数,看看这个工程是不是可行。
6.打开工程文件夹,然后新建一个文件夹叫做User,我们的main函数就放在这个文件夹里然后keil里,在Target这里右键,点击添加组,改名为User,然后在User上右键,点击添加新文件,选择c文件,名字叫main,下面的路径要选择User文件夹,要不然默认是放在文件夹外面的
然后点击Add,这样我们就有了main.c文件了。在工程文件夹的User目录下,也可以看到我们新建的main.c文件
在这个main.c里我们先右键,插入头文件,选择stm32f10x,h
然后写一个main函数,文件的最后一行必须是空行,要不然会报警告如果你想用寄存器开发STM32,那么工程建到这里就可以了
接下来我们配置一下寄存器,来点亮这个灯,我们只需要配置3个寄存器,就可以点灯了,我们打开STM32的参考手册,首先是RCC的一个寄存器,来使能GPIOC的时钟,GPIO都是APB2的外设,所以在这个APB2外设时钟使能寄存器RCC_APB2ENR里面配置可以看到这里有个IOPCEN,这一位就是使能GPIOC的时钟的,下面的解释是,这一位写1,就是打开GPIOC的时钟,那这一位写1,其他的无关项我们都先给0,那整个寄存器的2进制数据换成16进制,就是四个一分组,也就是00000010,然后我们回到keil,在while死循环之前,写上RCC的APB2ENR寄存器=0x00000010;这样就可以打开GPIOC的时钟了。
然后第二个寄存器,我们需要配置一下PC13口的模式,我们可以找到端口配置高寄存器GPIOx_CRH,这个x可以是A到E的任意一个字母然后右边的CNF13和MODE13就是用来配置13号口的
这个CNF我们需要配置为通用推挽输出模式,也就是这两位为00
MODE要配置为输出模式,最大速度可以给50Mhz,也就是这两位为11
最后我们对照上面的寄存器,这四位为0011,其他的我们也都给他配置为0,这样整个寄存器的值换算成16进制就是00300000,然后我们回到keil,在这里写上GPIOC的CRH=0x00300000;接下来我们就可以给PC13口输出数据了
我们可以看到这个端口输出寄存器GPIOx_ODR中间有一位ODR13,这一位写1,13号口就是高电平,写0就是低电平也就是0x00002000,因为这个灯是低电平点亮的所以我们给ODR置0就是亮置1就是灭
#include "stm32f10x.h"
int main(void)
{
RCC->APB2ENR = 0x00000010;//使能GPIOC时钟
GPIOC->CRH = 0x00300000;//配置推挽输出速度50Mhz模式
GPIOC->ODR = 0x00000000;//灯亮
while(1)
{
}
}
寄存器的操作方式,虽然代码简洁,但操作繁琐,那接下来我们为这个工程添加库函数
我们打开工程文件夹,在这里新建一个文件夹,叫Library,用来存放库函数。接着打开固件库的文件夹,打开Libraries,STM32标准外设驱动,src这些就是库函数的源文件,这个misc是内核的库函数,其他的就是内核外的外设库函数了,Ctral+A全选然后复制,在Library文件夹下粘贴,然后再打开固件库的inc文件夹
这些是库函数的头文件,我们继续Ctrl+A全选复制,粘贴至Library文件夹
接着回到kel软件,同样,在Target处右键,然后添加组,改名为Library,在右键添加已经存在的文件,打开Library全选添加,这样就把所有的库函数文件都添加进来了,但还不能直接使用,我们还需再添加一个文件。
我们打开固件库文件夹,打开Project,STM32Template这个conf(configuration)文件是用来配置库函数头文件的包含关系的,另外里面还有个用来参数检查的函数定义,这是所有库函数都需要的,两个it文件是用来存放中断函数的,我们把这三个文件都复制下来,然后粘贴到工程的User目录下,回到keil软件,在User组里添加刚才那三个文件
最后还需要一个宏定义,我们可以在这个头文件右键,打开文件,然后滑到最下面意思是你定义了use_stdperiph_driver,下面这个include conf.h才有效
所以我们还需要复制一下这个字符串,然后打开工程选项然后在Include Paths把User和Library的路径也都添加上,这样我们基于库函数的工程就建好了
我们用库函数来实现点灯的操作
#include "stm32f10x.h"
int main();
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);//使能时钟GPIOC
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//配置推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC,&GPIO_InitStructure);
GPIO_SetBits(GPIOC,GPIO_Pin_13);//将pc13置为高电平
GPIO_ResetBits(GPIOC,GPIO_Pin_13);//将pc13置为低电平
while(1)
{
}
}
新建工程的启动文件选择
我们新建工程时第一个加的就是启动文件,这个启动文件有很多类型,至于选择哪一个,我们要根据芯片型号根据此表缩写选择文件后缀
GPIO输出
这里带FT(Five Tolerate)的,就是可以容忍5v的,不带FT的,就只能接入3.3V的电压
在STM32中,所有的GPIO都是挂载在APB2外设总线上的,其中GPIO外设的名称是按照GPIOA、GPIOB、GPIOC……这样来命名的,每个GPIO外设,总共有16个引脚,编号是从0到15,GPIOA的第0号引脚,我们一般把它称作PA0,以此类推。在每个GPIO模块内,主要包含了寄存器和驱动器这些东西,寄存器就是一段特殊的存储器,内核可以通过APB2总线对寄存器进行读写,这样就可以完成输出电平和读取电平的功能了,这个寄存器每一位对应一个引脚,其中输出寄存器写1,对应的引脚就会输出高电平,写0,就输出低电平,输入寄存器读取为1,就证明对应的端口目前是高电平,读取为0,就是低电平。因为STM32是32位的单片机,所以STM32内部的寄存器都是32位的,但这个端口只有16位,所以这个寄存器只有低16位对应的有端口,高16位是没有用到的。这个驱动器是用来增加信号的驱动能力的,寄存器只负责存储数据,如果要进行点灯这样的操作的话,还是需要驱动器来负责增大驱动能力。
输入部分:首先是这个IO引脚,这里接了两个保护二极管,这个是输入电压进行限幅的,上面这个二极管接VDD,3.3V,下面接VSS,0V,如果输入电压比3.3V还要高,那上方这个二极管就会导通,输入电压产生的电流就会直接流入VDD而不会流入内部电路,这样就可以避免过高的电压对内部这些电路产生伤害;如果输入电压比0V还要低,这个电压是相对与VSS的电压,所以是可以有负电压的,那这时下方这个二极管就会导通,电流会从VSS直接流出去
如果输入电压在0~3.3V之间,那两个二极管均不会导通
上拉电阻至VDD,下拉电阻至VSS,这个开关是可以通过程序进行配置的。
上面导通,下面断开,就是上拉输入模式,又可以称作是默认为高电平的输入模式
下面导通,上面断开,就是下拉输入模式,又可以称作是默认为低电平的输入模式
两个都断开,就是浮空输入模式
上拉和下拉的作用:为了给输入提供一个默认的输入电平(如果输入什么都不接,这时输入就会处于一种浮空的状态,引脚的输入电平极易受外界干扰而改变,为了避免引脚悬空导致的输入数据不确定,我们就需要在这里加上上拉或者下拉电阻了)
这个上拉电阻和下拉电阻的阻值都是比较大的,是一种弱上拉和弱下拉,目的是尽量不影响正常的输入操作(不是很理解)翻译错误,应叫施密特触发器 它的执行逻辑是
如果输入电压大于某一阈值,输出就会瞬间升为高电平
如果输入电压小于某一阈值,输出就会瞬间降为低电平
如果输入电压处于上下阈值之间,输出就会保持之前的电平情况进行输出,直到下一次超出阈值才可能发生改变
作用:这样可以有效地避免因信号波动造成的输出抖动现象
接下来经过施密特触发器整形的波形就可以直接写入输入数据寄存器了,我们再用程序读取输入数据寄存器对应某一位的数据,就可以知道端口的输入电平
最后上面还有模拟输入和复用功能输入,这就是连接到片上外设的一些端口
其中模拟输入,这个是连接到ADC上的,因为ADC需要接收模拟量,所以这个线是接到施密特触发器前面的
另一个是复用功能输入,这个是连接到其他需要读取端口的外设上的。比如串口的输入引脚等,这根线接收的是数字量,所以在施密特触发器后面
输出部分:数字部分可以由输出数据寄存器或片上外设控制,两种控制方式通过这个数据选择器接到了输出控制部分
选择输出数据寄存器进行控制,就是普通的IO口输出,写这个数据寄存器的某一位就可以操作对应的某个端口了
位设置/清除寄存器:可以用来单独操作输出数据寄存器的某一位,而不影响其他位
(因为这个输出数据寄存器同时控制16个端口,并且这个寄存器只能整体读写,所以想单独控制其中某一个端口而不影响其他端口的话,就需要一些特殊的方式)
方式一:先读出这个寄存器,然后用按位与和按位或的方式更改某一位,最后再将更改后的数据写回去,在c语言中就是&=和|=的操作
方式二:设置位设置和位清除寄存器,如果我们对某一位进行置1的操作,在位设置寄存器的对应位写1即可,不需要操作的位写0,这样内部会有电路自动将输出数据寄存器中对应位置置1
方式三:位带的操作方式
输出控制之后就接到了两个MOS管
MOS管就是一种电子开关,我们的信号控制开关的导通和关闭,开关负责将IO口接到VDD或VSS
在推挽输出模式下,P-MOS和N-MOS均有效,数据寄存器为1时,上管导通,下管断开,输出直接接到VDD,就是输出高电平;数据寄存器为0时,上管断开,下管导通,输出直接接到VSS,就是输出低电平;
这种模式,高低电平均有较强的驱动能力,所以推挽输出模式也可以叫强推挽输出模式
在开漏输出模式下,这个P-MOS是无效的,只有N-MOS在工作。数据寄存器为1时,下管断开,这时输出相当于断开,也就是高阻模式,数据寄存器为0时,下管导通,输入直接接到VSS,也就是输出低电平,这种模式下,只有低电平有驱动能力,高电平是没有驱动能力的
开漏模式可以作为通信协议的驱动方式,比如I2C通信的引脚,就是使用开漏模式;还可以用于输出5V的电平信号输出关闭时,两个MOS管都无效
一个IO口可以有多个输入,但只有一个输出
LED灯闪烁
#include "stm32f10x.h"
#include "Delay.h"
int main();
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能时钟GPIOC
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//配置推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
while(1)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET);
Delay_ms(500);
GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);
Delay_ms(500);
}
}
LED流水灯
#include "stm32f10x.h"
#include "Delay.h"
int main();
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能时钟GPIOC
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//配置推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
while(1)
{
GPIO_Write(GPIOA,~0x0001);//0000 0000 0000 0001
Delay_ms(100);
GPIO_Write(GPIOA,~0x0002);//0000 0000 0000 0010
Delay_ms(100);
GPIO_Write(GPIOA,~0x0004);//0000 0000 0000 0100
Delay_ms(100);
GPIO_Write(GPIOA,~0x0008);//0000 0000 0000 1000
Delay_ms(100);
GPIO_Write(GPIOA,~0x0010);//0000 0000 0001 0000
Delay_ms(100);
GPIO_Write(GPIOA,~0x0020);//0000 0000 0010 0000
Delay_ms(100);
GPIO_Write(GPIOA,~0x0040);//0000 0000 0100 0000
Delay_ms(100);
GPIO_Write(GPIOA,~0x0080);//0000 0000 1000 0000
Delay_ms(100);
}
蜂鸣器
#include "stm32f10x.h"
#include "Delay.h"
int main();
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//使能时钟GPIOB
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//配置推挽输出模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
while(1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
Delay_ms(500);
GPIO_SetBots(GPIOB,GPIO_Pin_12);
Delay_ms(500);
}
GPIO输入
电阻的变化不容易直接被观察 ,所以我们通常将传感器元件与定值电阻进行串联分压,这样就可以得到模拟电压的输出,对于电路来说,检测电压就非常容易;另外这个模块还可以通过电压比较器,来对这个模拟电压进行二值化,这样就可以得到数字电压输出了
这个N1就是传感器元件所代表的可变电阻,它的阻值可以根据环境的光线、温度等模拟量进行变化,上面这个R1,是和N1进行分压的定制电阻,R1和N1串联,一端接在VCC正极,一端接在GND负极,这构成了基本的分压电路,左边这个C2是一个滤波电容,它是为了给中间的电压输出滤波的,用来滤除一些干扰,保证输出电压波形的平滑。(一般我们在电路中遇到这种一端接在电路中,另一端接地的电容,都可以考虑一下这个是不是滤波电容的作用)
上拉电阻和下拉电阻,在单片机电路中会经常出现比如弱上拉、弱下拉、强上拉、强下拉等,这里的强和弱就指电阻阻值的大小,上拉和下拉就是指接到VCC还是GND
我们回到电路,AO就是我们想要的模拟电压输出了,这就是AO电压的由来,仅需两个电阻分压即可得到,数字输出就是对AO进行二值化的输出,这里二值化是通过LM393来完成的
LM393是一个电压比较器芯片,里面有两个独立的电压比较器电路,然后剩下的是VCC和GND供电,那我们VCC就接到了电路的VCC,GND也接到了电路的GND,C1是一个电源供电的滤波电容
这个电压比较器其实就是一个运算放大器(详见51单片机AD/DA)
当同向输入端的电压大于反向输入端的电压时,输出会瞬间升高为最大值也就是输出接VCC
当同向输入端的电压小于反向输入端的电压时,输出会瞬间降低为最小值也就是输出接GND
这样对模拟电压进行二值化拧动电位器,IN-就会生成一个可调的阈值电压
DO最终就接到了引脚的输出端
C语言补充
区别:宏定义的新名字在左边,typedef的新名字在右边;宏定义不需要分号,typedef后必须加分号;宏定义任何名字都可以换,而typedef只能专门给变量类型换名字,所以宏定义的改名范围要更宽一些。对于变量类型重命名而言,使用typedef更加安全,因为typedef会对命名进行检查,如果不是变量类型的名字,那是不行的
按键控制LED
封装硬件驱动
在工程文件夹创建新文件夹命名为Hardware,回到keil软件里,点击三个箱子的按钮,打开工程管理,新建一个组,也叫Hardware
然后再点击魔术棒按钮,打开工程选项,再include path里把刚才新建的Hardware文件夹添加到头文件路径列表中添加LED.c和LED.h文件并修改文件位置到Hardware
LED.c用来1存放驱动程序的主体代码
#include "stm32f10x.h"
void LED_Init(void)//LEDIO口配置初始化
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypedef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_SetBits(GPIOA,GPIO_Pin_1|GPIO_Pin_2);//这样初始化之后,LED默认熄灭
}
void LED1_ON(void)//点亮LED1
{
GPIO_ResetBits(GPIOA,GPIO_Pin_1);
}
void LED1_OFF(void)//熄灭LED1
{
GPIO_SetBits(GPIOA,GPIO_Pin_1);
}
void LED1_turn(void)//实现端口的电平翻转
{
if(GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_1) == 0)
{
GPIO_SetBits(GPIOA,GPIO_Pin_1);
}
else
{
GPIO_ResetBits(GPIOA,GPIO_Pin_1);
}
}
void LED2_ON(void)//点亮LED2
{
GPIO_ResetBits(GPIOA,GPIO_Pin_2);
}
void LED2_OFF(void)//熄灭LED2
{
GPIO_SetBits(GPIOA,GPIO_Pin_2);
}
void LED2_turn(void)//实现端口的电平翻转
{
if(GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_2) == 0)
{
GPIO_SetBits(GPIOA,GPIO_Pin_2);
}
else
{
GPIO_ResetBits(GPIOA,GPIO_Pin_2);
}
}
LED.h用来存放这个驱动程序可以对外提供的函数和变量的声明
#ifndef __LED_H
#define __LED_H
void LED_Init(void);//对模块外部声明,这个函数是可以被外部调用的函数
void LED1_ON(void);
void LED1_OFF(void);
void LED1_turn(void);
void LED2_ON(void);
void LED2_OFF(void);
void LED2_turn(void);
#endif
同样,我们也把按键封装在驱动函数模块里,我们在Hardware同样的方式添加Key.c和Key.h文件
Key.c
#include "stm32f10x.h"
#include "Delay.h"
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypedef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_IPU;//配置上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//这里的速度是GPIO的输出速度,在输入模式下这个参数无效
GPIO_Init(GPIOB,&GPIO_InitStructure);
}
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0;
if(GPIO_ReadInputDateBit(GPIOB,GPIO_Pin_1)==0)//读取PB1端口的值,0代表低电平,1代表高电平
{
Delay_ms(20);//消抖
while(GPIO_ReadInputDateBit(GPIOB,GPIO_Pin_1)==0);//检测是否松手
Delay_ms(20);//消抖
KeyNum = 1;
}
if(GPIO_ReadInputDateBit(GPIOB,GPIO_Pin_11)==0)//读取PB11端口的值,0代表低电平,1代表高电平
{
Delay_ms(20);//消抖
while(GPIO_ReadInputDateBit(GPIOB,GPIO_Pin_11)==0);//检测是否松手
Delay_ms(20);//消抖
KeyNum = 2;
}
return KeyNum;
}
Key.h
#ifndef __KEY_H
#define __KEY_H
void Key_Init(void);
uint8_t Key_GetNum(void);
#endif
main.c
#include "stm32f10x.h"
#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();
}
}
}
光敏传感器控制蜂鸣器
Buzzer.c
#include "stm32f10x.h" // Device header
/**
* 函 数:蜂鸣器初始化
* 参 数:无
* 返 回 值:无
*/
void Buzzer_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB12引脚初始化为推挽输出
/*设置GPIO初始化后的默认电平*/
GPIO_SetBits(GPIOB, GPIO_Pin_12); //设置PB12引脚为高电平
}
/**
* 函 数:蜂鸣器开启
* 参 数:无
* 返 回 值:无
*/
void Buzzer_ON(void)
{
GPIO_ResetBits(GPIOB, GPIO_Pin_12); //设置PB12引脚为低电平
}
/**
* 函 数:蜂鸣器关闭
* 参 数:无
* 返 回 值:无
*/
void Buzzer_OFF(void)
{
GPIO_SetBits(GPIOB, GPIO_Pin_12); //设置PB12引脚为高电平
}
/**
* 函 数:蜂鸣器状态翻转
* 参 数:无
* 返 回 值:无
*/
void Buzzer_Turn(void)
{
if (GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_12) == 0) //获取输出寄存器的状态,如果当前引脚输出低电平
{
GPIO_SetBits(GPIOB, GPIO_Pin_12); //则设置PB12引脚为高电平
}
else //否则,即当前引脚输出高电平
{
GPIO_ResetBits(GPIOB, GPIO_Pin_12); //则设置PB12引脚为低电平
}
}
Buzzer.h
#ifndef __BUZZER_H
#define __BUZZER_H
void Buzzer_Init(void);
void Buzzer_ON(void);
void Buzzer_OFF(void);
void Buzzer_Turn(void);
#endif
LighetSensor.c
#include "stm32f10x.h" // Device header
/**
* 函 数:光敏传感器初始化
* 参 数:无
* 返 回 值:无
*/
void LightSensor_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
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); //将PB13引脚初始化为上拉输入
}
/**
* 函 数:获取当前光敏传感器输出的高低电平
* 参 数:无
* 返 回 值:光敏传感器输出的高低电平,范围:0/1
*/
uint8_t LightSensor_Get(void)
{
return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13); //返回PB13输入寄存器的状态
}
LighetSensor.h
#ifndef __LIGHT_SENSOR_H
#define __LIGHT_SENSOR_H
void LightSensor_Init(void);
uint8_t LightSensor_Get(void);
#endif
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) //如果当前光敏输出1
{
Buzzer_ON(); //蜂鸣器开启
}
else //否则
{
Buzzer_OFF(); //蜂鸣器关闭
}
}
}
OLED调试工具
OLED显示屏
EXTI外部中断
任意的GPIO口都可以当作外部中断的引脚,但相同的Pin不能同时触发中断,比如PA0和PB0不能同时用。所以如果有多个中断引脚,要选择不同的Pin的引脚
NVIC分配中断先后顺序
AFIO是一个数据选择器,它可以在这3给GPIO外设的16个引脚里选择其中一个连接到后面EXTI的通道里。外部中断的9~5会触发同一个中断函数,15~10也会触发同一个中断函数。下面这里,有20条输出线到了其他外设,这是用来触发其他外设操作的,也就是事件响应
当我们想把默认复用功能的引脚换到重定义的这个位置来,就是用AFIO来完成的
第一个图:为了测速,配置了这样一个光栅编码盘,当编码盘转动时,红外传感器的红外光就会出现遮挡、透过、遮挡、透过的现象,对应模块输出的电平就是高低电平交替的方波,方波的个数表示了转过的角度,方波的频率表示转速。我们可以用外部中断来捕获这个方波的边沿,以此判断位置和速度。
它的内部是用金属触点来进行通断的,所以它是一种机械触点式编码器。这里左右是两部分开关触点,其中内侧的这两根细的触点,都是和中间c上面这个引脚连接的,外侧的触点,左边的接在A引脚,右边接在b引脚。然后中间这个圆的金属片是一个按键,我们这个旋转编码器的轴是可以按下去的这个按键的两根线就在上面引出来了
在编码盘旋转时,依次接通和断开两边的触点,它能使两侧触点的通断产生一个90°的相位差,配合外部电路,这个编码器的两个输出就会输出这样的波形
反向旋转时B相就会提前90°
第三个图:霍尔传感器形式编码器,中间是一个圆形磁铁,边上有两个位置错开的霍尔传感器,当磁铁选择时,通过霍尔传感器,就可以输出正交的方波信号
上面按键的两根线,这个模块并没有使用,是悬空的;下面的就是编码器内部的两个触点,旋转轴旋转时,这两个触点以相位相差90°的方式交替导通,并配合外围电路输出高低电平,看左边这里接了一个10k的上拉电阻,默认没旋转的情况下,这个点被上拉为高电平,当旋转时,内部触点导通这个点就直接被拉低到GND了
对射式红外传感器计次
CountSensor.c
#include "stm32f10x.h" // Device header
uint16_t CountSensor_Count; //全局变量,用于计数
/**
* 函 数:计数传感器初始化
* 参 数:无
* 返 回 值:无
*/
void CountSensor_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟,外部中断必须开启AFIO的时钟
/*GPIO初始化*/
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引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);//将外部中断的14号线映射到GPIOB,即选择PB14为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure; //定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line14; //选择配置外部中断的14号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //指定外部中断线为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure); //将结构体变量交给EXTI_Init,配置EXTI外设
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //选择配置NVIC的EXTI15_10线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
}
/**
* 函 数:获取计数传感器的计数值
* 参 数:无
* 返 回 值:计数值,范围:0~65535
*/
uint16_t CountSensor_Get(void)
{
return CountSensor_Count;
}
/**
* 函 数:EXTI15_10外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI15_10_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line14) == SET) //判断是否是外部中断14号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
{
CountSensor_Count ++; //计数值自增一次
}
EXTI_ClearITPendingBit(EXTI_Line14); //清除外部中断14号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
CountSensor_Init(); //计数传感器初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Count:"); //1行1列显示字符串Count:
while (1)
{
OLED_ShowNum(1, 7, CountSensor_Get(), 5); //OLED不断刷新显示CountSensor_Get的返回值
}
}
旋转编码器计次
Enconsor.c
#include "stm32f10x.h" // Device header
int16_t Encoder_Count; //全局变量,用于计数旋转编码器的增量值
/**
* 函 数:旋转编码器初始化
* 参 数:无
* 返 回 值:无
*/
void Encoder_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟,外部中断必须开启AFIO的时钟
/*GPIO初始化*/
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); //将PB0和PB1引脚初始化为上拉输入
/*AFIO选择中断引脚*/
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);//将外部中断的0号线映射到GPIOB,即选择PB0为外部中断引脚
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);//将外部中断的1号线映射到GPIOB,即选择PB1为外部中断引脚
/*EXTI初始化*/
EXTI_InitTypeDef EXTI_InitStructure; //定义结构体变量
EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1; //选择配置外部中断的0号线和1号线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //指定外部中断线使能
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //指定外部中断线为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //指定外部中断线为下降沿触发
EXTI_Init(&EXTI_InitStructure); //将结构体变量交给EXTI_Init,配置EXTI外设
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //选择配置NVIC的EXTI0线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; //选择配置NVIC的EXTI1线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //指定NVIC线路的响应优先级为2
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
}
/**
* 函 数:旋转编码器获取增量值
* 参 数:无
* 返 回 值:自上此调用此函数后,旋转编码器的增量值
*/
int16_t Encoder_Get(void)
{
/*使用Temp变量作为中继,目的是返回Encoder_Count后将其清零*/
/*在这里,也可以直接返回Encoder_Count
但这样就不是获取增量值的操作方法了
也可以实现功能,只是思路不一样*/
int16_t Temp;
Temp = Encoder_Count;
Encoder_Count = 0;
return Temp;
}
/**
* 函 数:EXTI0外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) == SET) //判断是否是外部中断0号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) //PB0的下降沿触发中断,此时检测另一相PB1的电平,目的是判断旋转方向
{
Encoder_Count --; //此方向定义为反转,计数变量自减
}
}
EXTI_ClearITPendingBit(EXTI_Line0); //清除外部中断0号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
/**
* 函 数:EXTI1外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI1_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line1) == SET) //判断是否是外部中断1号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0) //PB1的下降沿触发中断,此时检测另一相PB0的电平,目的是判断旋转方向
{
Encoder_Count ++; //此方向定义为正转,计数变量自增
}
}
EXTI_ClearITPendingBit(EXTI_Line1); //清除外部中断1号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Encoder.h"
int16_t Num; //定义待被旋转编码器调节的变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Encoder_Init(); //旋转编码器初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Num:"); //1行1列显示字符串Num:
while (1)
{
Num += Encoder_Get(); //获取自上此调用此函数后,旋转编码器的增量值,并将增量值加到Num上
OLED_ShowSignedNum(1, 5, Num, 5); //显示Num
}
}
中断注意事项:不要在主程序和中断程序里,操作可能产生冲突的硬件
在实现功能时,可以在中断操作变量或者标注位,当中断返回时,再对这个变量进行显示和操作
TIM定时器中断
基本定时器
预分频器、计数器、自动重装寄存器构成了最基本的计数计时电路,所以这块电路叫做时基单元。预分频器之前,连接的就是基准计数时钟的输入,由于基本定时器只能选择内部时钟,所以你可以直接认为,这根线直接连到了内部时钟CK_INT,内部时钟的来源是RCC_TIMxCLK,这里的频率值一般都是系统的主频72MHz,所以通向时基单元的计数基准频率就是72MHz
预分频器可以对72MHz的计数时钟进行预分频,比如预分频器的寄存器写0,那就是不分频,或者说1分频,这时输出频率=输入频率=72MHz;如果预分频器写1,那就是2分频,输出频率=输入频率/2=36MHz,如果写2,那就是3分频,输出=输入/3,以此类推。所以预分频器的值与实际的分频系数相差了1,即实际分频系数=预分频器的值+1,这个预分频器是16位的,所以最大值可以写2^16-1=65535,也就是65536分频.预分频器的作用就是对输入的基准频率提前进行一个分频的操作。
然后是计数器,计数器可以对预分频后的计数时钟进行计数,计数时钟每来一个上升沿,计数值就加1,计数器也是16位的,所以里面的值可以从0一直加到65535,如果再加,计数器就会回到0重新开始,所以计数器的值再计时过程中会不断的自增运行,当自增运行到目标值时,产生中断,那就完成了定时的任务,所以现在还需要一个存储目标值的寄存器,那就是自动重装寄存器了
自动重装寄存器也是16位的,它存的就是我们写入的计数目标,在运行过程中,计数值不断自增,自动重装值是固定的目标,当计数值等于自动重装值时,也就是计时时间到了,那它就会产生一个中断信号,并且清零计数器,计数器自动开始下一次的计数计时,在图上画的一个向上的折线箭头,就代表这里会产生中断信号,像这种计数值等于自动重装值产生的中断叫做更新中断,更新中断之后会通往NVIC,我们再配置好NVIC的定时器通道,那定时器的更新中断就能够得到CPU的响应了。这里向下的折线箭头,代表的是产生一个事件,这里对应的事件叫做更新事件,更新事件不会触发中断,但可以触发内部其他电路的工作
主模式触发DAC
我们使用DAC的时候,可能会用DAC输出一段波形,那就需要每隔一段时间来触发一次DAC,让它输出下一个电压点。如果用正常的思路来实现的话,就是先设置一个定时器产生中断,每隔一段时间在中断程序中调用代码手动触发一次DAC转换,然后DAC输出,这样没问题,但会使主程序处于频繁被中断的状态,这会影响主程序的运行和其他中断的响应。所以定时器就设计了一个主模式,这个主模式可以把这个定时器的更新事件,映射到这个触发输出TRGO(Trigger Out)的位置,然后TRGO直接接到DAC触发转换引脚上,这样定时器的更新就不需要再通过中断来触发DAC转换了,只需要把更新事件通过主模式映射到TRGO,然后TRGO就会直接去触发DAC,整个过程不需要软件的参与,实现了硬件的自动化。
通用定时器
通用定时器和高级定时器还支持向下计数模式和中央对齐模式
向下计数模式就是从重装值开始,向下自减,减到0之后,回到重装值同时申请中断,然后进行下一轮,依次循环
中央对齐计数模式就是从0开始,先向上自增,计到重装值,申请中断,然后再向下自减,减到0,再申请中断,然后进行下一轮,依次循环
我们先看下内外时钟源选择,基本定时器只能选择内部时钟,也就是系统频率72MHz;通用定时器的时钟源不仅可以选择内部的72MHz时钟,还可以选择外部时钟
第一个外部时钟就是来自TIMx_ETR引脚上的外部时钟,ETR(External)引脚的位置,可以参考一下引脚定义表可以看到这里有TIM2_CH1_ETR,意思是这个TIM2的CH1和ETR都复用在这个位置,也就是PA0引脚,其他定时器的引脚也都可以在这里找到。
我们可以在TIM2的ETR引脚,也就是PA0上接一个外部方波时钟,然后配置一下内部的极性选择、边沿检测、和预分频器电路,再配置一下输入滤波电路(这两块电路可以对外部时钟进行一定的整形),最后,滤波后的信号,兵分两路,上面一路ETRF进入触发控制器,紧跟着就可以选择作为时基单元的时钟。如果你想在ETR外部引脚提供时钟,或者想对ETR时钟进行计数,把这个定时器当作计数器来用的话下面还有一路也就是TRGI(Trigger In)也可以提供时钟,主要用作触发输入来使用的,触发输入可以触发定时器的从模式。暂且可以把这个TRGI当作外部时钟的输入来看,当这个TRGI被当作外部时钟来使用的时候,这一路就叫做外部时钟模式1,那通过这个TRGI的时钟都有哪些你?往左看,第一个,就是ETR引脚的信号,这里ETR引脚即可以通过上面这一路来当作时钟,又可以通过下面这一路来当作时钟
只不过下面这一路输入会占用触发输入的通道而已,然后第二个,就是ITR信号,这一部分的时钟信号是来自其他定时器的,从右边可以看出,这个主模式的输出TRGO可以通向其他定时器,那通向其他定时器的时候,就接到了其他定时器的ITR引脚上来了,这个ITR0到ITR3分别来自其他4给定时器的TRGO输出,至于具体的连接方式,手册的这个位置有一张表
这里可以看到,TIM2的ITR0是接在了TIM1的TRGO上的,ITR1接在了TIM8,以此类推
通过这一路我们就可以实现定时器级联的功能。比如我们可以初始化TIM3,然后使用主模式把它的更新事件映射到TRGO上,接着再初始化TIM2,这里选择ITR2,对应的就是TIM3的TRGO,然后面再选择时钟为外部时钟模式1,这样TIM3的更新事件就可以驱动TIM2的时基单元,也就实现了定时器的级联
我们继续看,这里还可以选择TI1F_ED
这里连接的是输入捕获单元的CH1引脚,也就是从CH1引脚获得时钟,这里后缀加一个ED(Edge)就是边沿的意思,也就是通过这一路输入的时钟,上升沿和下降沿均有效
最后,这个时钟还能通过TI1FP1和TI2FP2获得,其中TI1FP1是总结:外部时钟模式1的输入可以是ETR引脚、其他定时器、CH1引脚的边沿、CH1引脚和CH2引脚。对于时钟输入而言,最常用的还是内部的72MHz的时钟,如果要使用外部时钟,首选ETR引脚外部时钟模式2的输入(编码器接口可以读取正交编码器的输出波形,后续详讲)
我们可以把定时器的内部事件映射到TRGO,用于触发其他定时器、DAC或ADC,范围比基本定时器更广
分别对应CH1到CH4的引脚,可以用于输出PWM波形,驱动电机
对应的也是CH1到CH4的引脚,可以用于测输入方波的频率等
因为输入捕获和输出比较不能同时使用,所以这里的寄存器是共用的,引脚也是共用的
高级定时器
主要改变的是右边这几个部分
这个计数器可以实现每隔几个计数周期,才发生一次更新事件和更新中断,这就相当于对输出的更新信号又做了一次分频右边这里的输出引脚,由原来的一个变为了两个互补的输出,可以输出一对互补的PWM波,这些电路是为了驱动三项无刷电机的,因为三相无刷电机的驱动电路一般需要3个桥臂,每个桥臂2个大功率开关管来控制,总共需要6个大功率开关管来控制,所以这里输出PWM引脚的前三路就1变为了互补的输出,而第四路却没什么变化,因为三相电机只需要三路,为了防止互补输出的PWM驱动桥臂时,在开关切换的瞬间,由于期间的不理想,造成短暂的直通现象,所以这里前面就加上了死区生成电路,在开关切换的瞬间,产生一定时长的死区,让桥臂的上下管全都关断,防止直通现象
刹车输入部分,如果外部引脚BKIN(Break IN)产生了刹车信号,或者内部时钟失效,产生了故障,那么控制电路就会自动切断电机的输出,防止意外的发生
定时中断和内外时钟源选择这些所涉及的结构,还是PSC(Prescaler)预分频器、CNT(Count)计数器、ARR(AutoReloadRegister)自动重装器这三个寄存器构成的时基单元,下面这里是运行控制,就是控制寄存器的一些位,比如启动停止、向上或向下计数等等,我们操作这些寄存器就能控制时基单元的运行,左边是为时基单元提供时钟的部分,这里可以选择RCC提供的内部时钟,也可以选择ETR引脚提供的外部时钟模式2,也可以选择外部时钟模式1,对应的有ETR外部时钟、ITRx其他定时器、TIx输入捕获通道;最后这里还有个编码器模式,这一般是编码器独用的模式;接下来右边这里就是定时时间到,产生更新中断的信号去向,在这里如果是高级定时器的话,还会多一个重复计数器,这个标志位会通过中断输出控制,到NVIC申请中断(为什么会有中断输出控制,因为这个定时器模块有很多地方都要申请中断,比如前面的框图里不仅更新要申请中断,TRGI触发信号也会申请中断,还有下面的输入捕获和输出比较匹配也会申请,所以这些中断都要经过中断输出控制,如果需要这个中断,那就允许,如果不需要,那就禁止),简单来说,这个中断输出控制就是一个中断输出的允许位,如果需要某个中断,就记得允许一下。
接下来我们看几个时序图,研究一下时基单元运行的一些细节问题CK_PSC就是图中的这个位置
CNT_EN 计数器使能,高电平计数器正常运行,低电平计数器停止
CK_CNT 计数器时钟 上图中有这个位置,它既是预分频器的时钟输出,也是计数器的时钟输入(开始时,计数器未使能,计数器时钟不运行,然后使能后,前半段,预分频系数为1,计数器时钟等于预分频器前的时钟,后半段吗,预分频器系数变为了2,计数器的时钟也变为预分频前时钟的一半)
在计数器时钟的驱动下,下面的计数器寄存器也跟随时钟的上升沿不断自增,在中间这个位置FC之后,计数值变为0了,ARR自动重装值就是FC,当计数值计到和重装值相等,并且下一个时钟来临时,计数值才清零,同时下面产生一个更新事件,这就是一个计数周期的工作流程
然后下面三行时序描述的是这个预分频器的一种缓存机制,也就是预分频寄存器实际上有两个,一个是预分频控制寄存器,它并不直接决定分频系数,另外还有一个缓冲寄存器(影子寄存器)才是真正起作用的寄存器,当我们在计数计到一半的时候改变了分频值,这个变化并不会立刻生效,而是会等到本次计数周期结束时,产生了更新事件,预分频器的值才会被传递到缓冲寄存器里面去,才会生效(即这一次计数完毕分频系数才改变)
RCC时钟树
时钟树,就是STM32中·用来产生和配置时钟,并且把配置好的时钟发送到各个外设的系统,时钟是所有外设运行的基础,所有时钟也是最先需要配置的东西;程序的主函数还会执行一个Systemlnit函数,这个函数就是用来配置这个时钟树的中间的这个SYSCLK就是系统时钟72MHz
在时钟产生电路,有四个震荡源,分别是内部的8MHz高速RC振荡器、外部的4-16MHz高速石英晶体振荡器,也就是晶振,一般都是接8MHz、外部的32.768KHz低速晶振(一般给RTC提供时钟)、内部的40KHz低速RC振荡器(可以给看门狗提供时钟)
上面这两个高速晶振,是用来提供系统时钟的,我们AHB、APB2、APB1的时钟都是来源于这两个高速晶振,这里内部和外部都有一个8MHz的晶振,都是可以用的,只不过是外部的石英振荡器比内部的RC振荡器更加稳定,所以我们一般用外部晶振,但你系统很简单不需要那么精确的时钟,那也是可以使用内部RC振荡器的,这样就可以省下外部晶振的电路了
在SystemInit函数里,ST是这样来配置时钟的,首先它会启动内部时钟,选择内部8MHz为系统时钟,暂时以内部8MHz的时钟运行,然后再启动外部时钟,进入PLL锁相环进行倍频,8MHz倍频9倍,得到72MHz,等到锁相环输出稳定后,选择锁相环输出为系统时钟,这样就把系统时钟由8MHz切换为了72MHz,
另外这里还有个CSS(Clock Security System),时钟安全系统,它也是负责切换时钟的,它可以检测外部时钟的运行状态,一旦外部时钟失效,它就会自动把外部时钟切换为内部时钟,保证系统时钟的运行,防止程序卡死造成事故,另外在高级定时器
这里通过或门,就会立刻反应到输出比较这里,让这个输出控制的电机立刻停止,防止意外。
再看右边的时钟分频电路,首先系统时钟72MHz进入AHB总线,AHB总线有个预分频器,在SystemInit里面配置的分配系数为1,那AHB的时钟就是72MHz,然后进入APB1总线,这里配置的分配系数是2,所以APB1总线的时钟为72MHz/2=36MHz,下面还有一条支路如果APB1预分频系数=1,则频率不变,否则频率×2,然后再看右边,发现这一路是单独为定时器2~7开通的,所以1通向定时器2~7的时钟就又回到了72MHz。无论是高级定时器还是通用定时器,还是基本定时器,它们的内部基准时钟都是72MHz
然后再看下面APB2的时钟,这里给的预分频系数为1,所以APB2的时钟和AHB一样都是72MHz,这里接在APB2的高级定时器也单开了一路再时钟输出这里,都有一个与门进行输出控制,控制位写的外部时钟使能,这就是我们在程序中写RCC_APB2/1PeriphClockCmd作用的地方,打开时钟就是在这个位置写1让左边的时钟能够通过与门输出给外设
定时器定时中断
第一步 RCC开启时钟 打开时钟后,定时器的基准时钟和整个外设的工作时钟就都会同时打开
第二步 选择时基单元的时钟源,对于定时中断,我们就选择内部时钟
第三步 配置时基单元 (预分频器、自动重装器、计数模式等等,使用结构体配置)
第四步 配置中断控制 允许更新中断输出到NVIC
第五步 配置NVIC 在NVIC中打开定时器中断的通道,并分配一个优先级
第六步 运行控制
整个模块配置完成后,我们还需要使能一下计数器,否则计数器是不会运行的,当定时器使能后,计数器就会开始计数了,当计数器更新时,触发中断,最后我们再写一个定时器的中断函数,这样这个中断函数每隔一段时间就能自动执行一次了
Timer.c
#include "stm32f10x.h" // Device header
/**
* 函 数:定时中断初始化
* 参 数:无
* 返 回 值:无
*/
void Timer_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
/*配置时钟源*/
TIM_InternalClockConfig(TIM2); //选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
/*中断输出配置*/
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除定时器更新标志位
//TIM_TimeBaseInit函数末尾,手动产生了更新事件
//若不清除此标志位,则开启中断后,会立刻进入一次中断
//如果不介意此问题,则不清除此标志位也可
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //开启TIM2的更新中断
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //选择配置NVIC的TIM2线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //指定NVIC线路的抢占优先级为2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
/* 定时器中断函数,可以复制到使用它的地方
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
*/
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
uint16_t Num; //定义在定时器中断里自增的变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Timer_Init(); //定时中断初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Num:"); //1行1列显示字符串Num:
while (1)
{
OLED_ShowNum(1, 5, Num, 5); //不断刷新显示Num变量
}
}
/**
* 函 数:TIM2中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断是否是TIM2的更新事件触发的中断
{
Num ++; //Num变量自增,用于测试定时中断
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除TIM2更新事件的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
定时器外部时钟
Timer.c
#include "stm32f10x.h" // Device header
/**
* 函 数:定时中断初始化
* 参 数:无
* 返 回 值:无
* 注意事项:此函数配置为外部时钟,定时器相当于计数器
*/
void Timer_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0引脚初始化为上拉输入
/*外部时钟配置*/
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F);
//选择外部时钟模式2,时钟从TIM_ETR引脚输入
//注意TIM2的ETR引脚固定为PA0,无法随意更改
//最后一个滤波器参数加到最大0x0F,可滤除时钟信号抖动
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 10 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
/*中断输出配置*/
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除定时器更新标志位
//TIM_TimeBaseInit函数末尾,手动产生了更新事件
//若不清除此标志位,则开启中断后,会立刻进入一次中断
//如果不介意此问题,则不清除此标志位也可
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); //开启TIM2的更新中断
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//即抢占优先级范围:0~3,响应优先级范围:0~3
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //选择配置NVIC的TIM2线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //指定NVIC线路的抢占优先级为2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
/**
* 函 数:返回定时器CNT的值
* 参 数:无
* 返 回 值:定时器CNT的值,范围:0~65535
*/
uint16_t Timer_GetCounter(void)
{
return TIM_GetCounter(TIM2); //返回定时器TIM2的CNT
}
/* 定时器中断函数,可以复制到使用它的地方
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
*/
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
uint16_t Num; //定义在定时器中断里自增的变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Timer_Init(); //定时中断初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Num:"); //1行1列显示字符串Num:
OLED_ShowString(2, 1, "CNT:"); //2行1列显示字符串CNT:
while (1)
{
OLED_ShowNum(1, 5, Num, 5); //不断刷新显示Num变量
OLED_ShowNum(2, 5, Timer_GetCounter(), 5); //不断刷新显示CNT的值
}
}
/**
* 函 数:TIM2中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断是否是TIM2的更新事件触发的中断
{
Num ++; //Num变量自增,用于测试定时中断
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除TIM2更新事件的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
TIM输出比较
OC(Output Compare) 输出比较
IC(Input Capture)输入捕获
CC(Capture/Compare)输入捕获和输出比较的单元
输出比较可以通过比较CNT(计数器)与CCR(捕获/比较)寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形
每个高级定时器和通用定时器都拥有4个输出比较通道
高级定时器的前3个通道额外拥有死区生成和互补输出的功能
PWM波形实现呼吸灯原理 让LED不断点亮、熄灭、点亮、熄灭,当这个点亮、熄灭的频率足够大时,由于视觉暂留现象(LED不会立马熄灭,而是有一定的惯性,过一小段时间才会熄灭)让我们感觉是呈现出一个中等亮度,当我们调控这个点亮和熄灭的时间比例时,就能让LED呈现出不同的亮度级别。对于电机调速也同理(电机的转动不会立马停止,而是有一定的惯性,过一小段时间才会停止)。这样具有惯性的系统才能使用PWM。
在如上左图中可以看出,这种高低电平跳变的数字信号,它是可以等效为中间这个紫色的虚线所表示的模拟量的,当上面电平时间长一点,下面电平时间短一点的时候,那等效的模拟量就偏向于上面,当下面电平时间长一点,上面电平时间短一点的时候,等效的模拟量就偏向于下面。
PWM的频率越大,Ts越小划分越多,那它等效模拟的信号就越平稳
占空比就是高电平时间相对于整个周期时间的比例,决定了PWM等效出来的模拟电压的大小,占空比越大,那等效的模拟电压就越趋近于高电平,占空比越小,那等效的模拟电压就越趋近于低电平。比如高电平是5V,低电平是0V,占空比为50%,那么等效出来的电压就是2.5V,占空比为20%就等效于1/5的电压,就是1V。
分辨率等于占空比变化步距,比如有的占空比只能是1%、2%、3%等等这样以1%的步距跳变,那它的分辨率就是1%,如果可以1.1%、1.2%、1.3%等等3这样以0.1%的步距跳变,那它的分辨率就是0.1%,所以这个分辨率就是占空比变化的精细程度。
我们先看一下通用定时器的输出比较通道
在这个图里,左边就是CNT计数器和CCR1第一路的捕获/比较寄存器,它俩进行比较,当CNT>CCR1,或者CNT=CCR1时,就会给这个输出模式控制器传一个信号,然后输出模式控制器就会改变它输出oc1ref的高低电平,ref是reference的缩写,意为参考信号,左上角还有个ETRF输入,这个是定时器的一个小功能,接着这个REF信号可以前往主模式控制器,你可以把这个REF映射到主模式的TRGO输出上去,不过REF的主要去向还是下面这一路,这是一个极性选择,给这个寄存器写0,信号就会往上走,就是信号电平不翻转,进来啥样,出去还是啥样,写1的话,信号就会往下走,就是信号通过一个非门取反,那输出的信号就是输入信号高低电平翻转的信号,这就是极性选择,就是选择是不是要把高低电平反转一下,最后就是输出使能电路,选择要不要输出,最后就是OC1引脚,这个引脚就是CH1通道的引脚,在引脚定义表里就可以知道具体是哪个gpio口了
我们看这个输出模式控制器具体是怎么工作的
这就是输出比较的8种模式,也就是这个输出模式控制器里面的执行逻辑,这个模式控制器的输入是CNT和CCR的大小关系,输出的是REF的高低电平,里面可以选择多种模式来更加灵活的控制REF输出,这个模式可以通过寄存器来配置,你需要哪个模式就可以选哪个模式
这个图对应的就是
左边是CNT和CCR比较的结果,右边就是输出比较电路,最后通过TIM_CH1输出到GPIO引脚上
然后下面还有三个同样的单元,分别输出到CH2、CH3、CH4
可以看出PWM的周期始终对应着计数器的一个溢出更新周期,所以PWM的频率就等于计数器的更新频率
接着是占空比参数 CNT从0一直加到ARR,比如ARR是99,那CNT总共就是计了100个数,再看高电平的时间,CNT是从0一直加到CCR,比如CCR是30,注意这里在等于30的瞬间,就已经跳变为低电平了,所以CNT从0到29是高电平,总共是30个数的时间,所以占空比是30/99+1=30%
最后还有分辨率,也就是占空比变化的步距。从上面这个图可以看出,CCR的值应该设置在0到ARR+1这个范围里,CCR=ARR+1时,占空比就正好是100%,如果CCR再大一些,那占空比就始终是100%,没有意义了,所以CCR的变化范围取决于ARR的值,ARR越大,CCR的范围就越大,对应的分辨率就越大,所以总结一下公式就是,分辨率=1/(ARR+1),也可以把CCR的值定义成分辨率,那就是值越大越好。
最后我们再回过来,简单的看一下高级定时器的输出比较电路
在它外面通常要接一个这样的电路,上面是正极,接着是一个大功率开关管,一般都是MOS管,就是一种大功率电子开关,然后再来一个MOS管,最后到GND,MOS管左边是控制极,比如说给高电平右边两根线就导通,低电平就断开,下面也是一样,有一个控制极,高电平导通,低电平断开,这就是一个最基本的推挽电路,中间是输出,如果上管导通下管断开,那输出就是高电平,如果下管导通,上管断开,那输出就是低电平,如果上下管都导通,那就是电源短路,这样是不允许的,如果上下管都断开,那输出就是高阻态,这就是推挽电路的工作流程,如果有两个这样的推挽电路,那就构成了H桥电路,就可以控制直流电机正反转了,如果有三个这样的推挽电路,那就可以用于驱动三相无刷电机了
那对于这个电路来说,如果直接用单片机来控制的话,那就需要两个控制极,并且这两个控制极电平是相反的,也就是互补,因为上管导通下管就必须断开,下管导通上管就必须断开;首先这个OC1和OC1N就是两个互补的输出端口,分别控制上管和下管的导通和关闭,然后是在切换上下管导通状态时,如果在上管关断的瞬间,下管立刻就打开,那可能会因为器件的不理想,上管还没完全关断,下管就已经导通了,出现了短暂的上下管同时导通的现象,这会导致功率损耗,引起器件发热,所以在这里为了避免这个问题,就有了死区生成电路,它会在上管关闭的时候,延迟一小段时间,再导通下管,下管关闭的时候,延迟一段时间,再导通上管,这样就可以避免上下管同时导通的现象了。这就是死区生成和互补输出的用途。
先看一下舵机,舵机是一种根据输入PWM信号占空比来控制输出角度的装置
下面这个图就是我们套件里的舵机,型号是SG90,它有三根输入线,两根是电源线,一根是信号线,我们的PWM就是输入到这个信号线,来控制舵机的,在这边有一个白色的输出轴,它的轴会固定在一个指定的角度不动,至于固定在哪个位置,是由信号线的PWM信号来决定的,这就是舵机的工作方式;然后中间这个是舵机的一个拆解图,在这里可以看到里面的结构有舵机上盖、输出轴、减速齿轮组、下面这里是直流电机、舵机控制板、电位器,可以看出,舵机其实并不是一种单独的电机,它的内部是由直流电机驱动的,它里面还有一个控制电路板,是一个电机的控制系统,大概的执行逻辑是:PWM信号输入到控制板,给控制板一个指定的目标角度,然后这个电位器检测输出轴的当前角度,如果大于目标角度,电机就会反转;如果小于目标角度,电机就会正转,最终使输出轴固定在指定角度,这就是舵机的内部工作流程;但不管怎样,我们要知道,输入一个PWM波形,输出轴固定在一个角度就行了。
接着我们来看输入PWM信号的要求:周期为20ms,高电平宽度为0.5ms~2.5ms。这个范围对应的输出角度,我们看一下右边这个图,我们这是一个180度的舵机,输出轴的角度是-90度到+90度或者你规定是0到180度,都没问题,然后输入信号脉冲宽度,周期为20ms,也就是这个上升沿到下一个上升沿,这之间的时间是20ms.然后高电平时间如果是0.5ms,对应的角度就是-90度,如果是1ms,对应的角度就是-45度,以此类推,中间的对应关系都是线性分配的,按比例来,你给个PWM,输出轴就会固定在一个角度。这里PWM波形,它其实是当做一个通信协议来使用的,与之前说的用PWM等效一个模拟输出,关系不大,把PWM当作一个通信协议,也是一个比较常用的应用,因为很多控制器都有PWM输出的功能,而且PWM只需要一根信号线就行了,这也是一种应用形式。
然后我们来看一下舵机的硬件电路
我们先看上面这个引脚定义图,在这个舵机上有三根线,分别是黑红黄,对应的,黑色是电源负极GND,红色是电源正极,这里5V舵机就接+5V,黄色是信号线,接PWM信号,然后继续看下面这个电路,实际应用的话GND就接GND,电源正的5V,这个是电机的驱动电源,一般电机都是大功率设备,它的驱动电源也必须是一个大功率的输出设备,如果能像这样单独提供供电,那就再好不过了,如果不能那也要注意电源的功率是不是能达标,如果单独供电的话,供电的负极要和STM32共地。最后就是信号线PWM,就直接接到STM32引脚上就行了,比如PA0,因为舵机内部是有驱动电路的,这个PWM只是一个通信线,是不需要大功率的。
然后我们再看一下直流电机
因为这个直流电机是一个单独的电机,里面没有驱动电路,所以我们就要外挂一个驱动电路来控制了。
现在市面上也有很多驱动电路可以选择,比如TB6612、DRV8833、L9110、L298N等等
右图就是H桥电路的基本结构,它是由两路推挽电路组成的,比如左边这个,上管导通、下管断开,那左边输出就是接在VM的电机电源正极,下管导通,上管断开,那就是接在PGND的电源负极,如果有两路推挽电路,中间这里接一个电机,左上和右下导通,电流从左向右,右上和左下导通,那电流方向就反过来了,从右边流向左边,H桥可以控制电流流过的方向,所以它能控制电机正反转。
接下来我们看看怎么用这个驱动芯片
我们看一下硬件电路,左边就是这个电机驱动模块的硬件电路了,我们来看一下,这里左下角VM,就是电机电源的正极了,要接一个可以输出大电流的电源,右边引脚定义看一下,这个VM是驱动电压输入端,范围是4.5到10V,这个电压一般和你的电机额定电压保持一致,比如如果你是5V的电机,这里就接5V,如果你是7.2V的电机,这里就接7.2V;然后下一个引脚是VCC,这个是逻辑电平输入端,范围是2.7到5.5V,这个要和我们控制器的电源保持一致,比如你使用STM32,是3.3V的器件,那就接3.3V,如果是51单片机,是5V的器件那就接5V。然后第三个引脚是GND这个就接系统的负极就行了,然后板子最右边还有两个GND,这个都是一样的引脚,在板子内部都是连通的,随便选一个GND用就可以,然后继续看,这里AO1、AO2、BO1、BO2就是两路电机的输出了,可以像这样分别接两个电机,AO1和AO2就是A路的两个输出,它的控制端就是上面的这三个PWMA、AIN2、AIN1这三个引脚控制下面A路的一个电机(这里对应关系已用灰色填充),这三个引脚直接接到单片机的GPIO口就行了,其中PWMA引脚要接PWM信号输出端,其他两个引脚可以任意接两个普通的GPIO口,那这三个引脚给一个低功率的控制信号,驱动电路就会从VM汲取电流,来输出到电机,这样就能够完成低功率的控制信号控制大功率设备的目的了。最后还剩个STBY引脚,这个是待机控制脚,如果接GND,芯片就不工作,处于待机状态,如果接逻辑电源VCC,芯片就正常工作,这个引脚如果不需要待机模式的话,可以直接接VCC,3.3V,如果需要的话,可以任意接一个GPIO,给高低电平就可以控制了。
PWM驱动LED呼吸灯
第一步 RCC开启时钟,把我们要用的TIM外设和GPIO外设的时钟打开
第二步 配置时基单元,包括这前面的时钟源选择和时基单元都配置好
第三步 配置输出比较单元,里面包括这个CCR的值、输出比较模式、极性选择、输出使能这些参数,在库函数里也是用结构体统一来配置的
第四步 配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置,这个PWM和GPIO的对应关系可以参考引脚定义表
第五步 运行控制 启动计数器,这样就能输出PWM了
PWM.c
#include "stm32f10x.h" // Device header
/**
* 函 数:PWM初始化
* 参 数:无
* 返 回 值:无
*/
void PWM_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO重映射*/
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //开启AFIO的时钟,重映射必须先开启AFIO的时钟
// GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE); //将TIM2的引脚部分重映射,具体的映射方案需查看参考手册
// GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); //将JTAG引脚失能,作为普通GPIO引脚使用
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0引脚初始化为复用推挽输出
//受外设控制的引脚,均需要配置为复用模式
/*配置时钟源*/
TIM_InternalClockConfig(TIM2); //选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
/*输出比较初始化*/
TIM_OCInitTypeDef TIM_OCInitStructure; //定义结构体变量
TIM_OCStructInit(&TIM_OCInitStructure); //结构体初始化,若结构体没有完整赋值
//则最好执行此函数,给结构体所有成员都赋一个默认值
//避免结构体初值不确定的问题
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //输出比较模式,选择PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性,选择为高,若选择极性为低,则输出高低电平取反
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //输出使能
TIM_OCInitStructure.TIM_Pulse = 0; //初始的CCR值
TIM_OC1Init(TIM2, &TIM_OCInitStructure); //将结构体变量交给TIM_OC1Init,配置TIM2的输出比较通道1
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
/**
* 函 数:PWM设置CCR
* 参 数:Compare 要写入的CCR的值,范围:0~100
* 返 回 值:无
* 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
* 占空比Duty = CCR / (ARR + 1)
*/
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare); //设置CCR1的值
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
uint8_t i; //定义for循环的变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
PWM_Init(); //PWM初始化
while (1)
{
for (i = 0; i <= 100; i++)
{
PWM_SetCompare1(i); //依次将定时器的CCR寄存器设置为0~100,PWM占空比逐渐增大,LED逐渐变亮
Delay_ms(10); //延时10ms
}
for (i = 0; i <= 100; i++)
{
PWM_SetCompare1(100 - i); //依次将定时器的CCR寄存器设置为100~0,PWM占空比逐渐减小,LED逐渐变暗
Delay_ms(10); //延时10ms
}
}
}
PWM驱动舵机
Servo.c
#include "stm32f10x.h" // Device header
#include "PWM.h"
/**
* 函 数:舵机初始化
* 参 数:无
* 返 回 值:无
*/
void Servo_Init(void)
{
PWM_Init(); //初始化舵机的底层PWM
}
/**
* 函 数:舵机设置角度
* 参 数:Angle 要设置的舵机角度,范围:0~180
* 返 回 值:无
*/
void Servo_SetAngle(float Angle)
{
PWM_SetCompare2(Angle / 180 * 2000 + 500); //设置占空比
//将角度线性变换,对应到舵机要求的占空比范围上
}
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(); //OLED初始化
Servo_Init(); //舵机初始化
Key_Init(); //按键初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Angle:"); //1行1列显示字符串Angle:
while (1)
{
KeyNum = Key_GetNum(); //获取按键键码
if (KeyNum == 1) //按键1按下
{
Angle += 30; //角度变量自增30
if (Angle > 180) //角度变量超过180后
{
Angle = 0; //角度变量归零
}
}
Servo_SetAngle(Angle); //设置舵机的角度为角度变量
OLED_ShowNum(1, 7, Angle, 3); //OLED显示角度变量
}
}
PWM驱动直流电机
Motor.c
#include "stm32f10x.h" // Device header
#include "PWM.h"
/**
* 函 数:直流电机初始化
* 参 数:无
* 返 回 值:无
*/
void Motor_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
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); //将PA4和PA5引脚初始化为推挽输出
PWM_Init(); //初始化直流电机的底层PWM
}
/**
* 函 数:直流电机设置速度
* 参 数:Speed 要设置的速度,范围:-100~100
* 返 回 值:无
*/
void Motor_SetSpeed(int8_t Speed)
{
if (Speed >= 0) //如果设置正转的速度值
{
GPIO_SetBits(GPIOA, GPIO_Pin_4); //PA4置高电平
GPIO_ResetBits(GPIOA, GPIO_Pin_5); //PA5置低电平,设置方向为正转
PWM_SetCompare3(Speed); //PWM设置为速度值
}
else //否则,即设置反转的速度值
{
GPIO_ResetBits(GPIOA, GPIO_Pin_4); //PA4置低电平
GPIO_SetBits(GPIOA, GPIO_Pin_5); //PA5置高电平,设置方向为反转
PWM_SetCompare3(-Speed); //PWM设置为负的速度值,因为此时速度值为负数,而PWM只能给正数
}
}
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(); //OLED初始化
Motor_Init(); //直流电机初始化
Key_Init(); //按键初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Speed:"); //1行1列显示字符串Speed:
while (1)
{
KeyNum = Key_GetNum(); //获取按键键码
if (KeyNum == 1) //按键1按下
{
Speed += 20; //速度变量自增20
if (Speed > 100) //速度变量超过100后
{
Speed = -100; //速度变量变为-100
//此操作会让电机旋转方向突然改变,可能会因供电不足而导致单片机复位
//若出现了此现象,则应避免使用这样的操作
}
}
Motor_SetSpeed(Speed); //设置直流电机的速度为速度变量
OLED_ShowSignedNum(1, 7, Speed, 3); //OLED显示速度变量
}
}
TIM输入捕获
IC(Input Capture)输入捕获
4个输入捕获和输出比较通道,共用4个CCR寄存器,另外它们的CH1到CH4,4个通道的引脚,也是共用的,所以对于同一个定时器,输入捕获和输出比较只能使用其中一个,不能同时使用。
输入捕获模式下,当通道输入引脚出现指定电平跳变(就是上升沿或者下降沿,可以通过程序配置),当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数。
每个高级定时器和通用定时器都拥有4个输入捕获通道,基本定时器没有输入捕获功能
可配置为PWMI模式,同时测量频率和占空比
可配合主从触发模式,实现硬件全自动测量
频率测量的相关知识在这里是一个频率逐渐降低的方波波形,越往左,频率越高,越往右,频率越低,这里信号都是只有高低电平的数字信号,对于STM32测频率而言,它也是只能测量数字信号的,如果你需要测量一个正弦波,那还需要搭建一个信号预处理电路,最简单的就是用运放搭一个比较器,把正弦波转换为数字信号,再输入给STM32就行了,如果你测量的信号电压非常高,那还要考虑一下隔离的问题,比如用一些隔离放大器,电压互感器等元件,隔离高压端和低压端,保证电路的安全,总之,经过处理最终输入给STM32的信号,要是这样的高低电平信号,高电平3.3V低电平0V
然后我们来研究一下测量这样一个信号的方法,首先为了测量频率,我们有两种方法可以选择
测频法:在闸门时间T内,对上升沿计次得到N,则频率f=N/T
测频法的图示说明,就是这个图的左边,比如我们要测量这一 块信号的频率,那就可以自定一个闸门时间T,通常设置为1s,在1s时间内,对信号上升沿计次,从0开始计,每来一个上升沿,计次+1,每来一个上升沿,其实就是来了一个周期的信号,所以在1s时间内,来了多少个周期,那它的频率就是多少Hz,频率的定义就是1s内出现了多少个重复的周期,那频率就是多少Hz
测周法:两个上升沿内,以标准频率fc计次,得到N,则频率fx=fc/N
测周法的基本原理就是,周期的倒数就是频率,我们如果能测出一个周期的时间,再取个倒数,不就是频率了,所以,我们看一下这右边的测周法图示说明,我们捕获信号的两个上升沿,然后测量一下这之间持续的时间就行了,但实际上,我们并没有一个精度无穷大的秒表来测量时间,测量时间的方法,实际上也是定时器计次,我们使用一个已知的标准频率fc的计次时钟,来驱动计数器,从一个上升沿开始计,计数器从0开始,一直计到下一个上升沿,停止。N/fc就是周期,再取个倒数,就得到了公式,fx=fc/N
测频法适合测量高频信号,测周法适合测量低频信号
测频法,在闸门时间内,最好要多出现一些上升沿,计次数量多一些,这样有助于减小误差。假如你定了1s的闸门时间,结果信号频率非常低,1s的时间才只有寥寥无几的几个上升沿,甚至一个上升沿都没有,那你总不能认为频率是0吧,在计次N很少时,误差会非常大,所以测频法要求,信号频率要稍微高一些;那对于测周法,就要求信号频率低一些,低频信号,周期比较长,计次就会比较多,有助于减小误差。否则的话,比如标准频率fc为1MHz,待测信号频率太高,比如待测信号500KHz,那在一个周期内只能计一两个数,如果待测信号再高一些,甚至一个数也计不到,那总不能认为频率无穷大吧,所以测周法需要待测信号频率低一些,然后是测频法测量结果更新的慢一些,数值相对稳定,测周法更新的快,数据跳变的也非常快
测频法测量的是在闸门时间内的多个周期,所以它自带一个均值滤波,如果在闸门时间内波形频率有变化,那得到的其实是这一段时间的平均频率,如果闸门时间选为1s,那么每隔1s才能得到一次结果,所以测频法结果更新慢,测量结果是一段时间的平均值,值比较平滑,反观测周法,它只测量一个周期,就能出一次结果,所以出结果的速度取决于待测信号的频率,一般而言,待测信号都是几百几千Hz,所以一般情况下,测周法结果更新更快,但是由于它只测量一个周期,所以结果值会受噪声的影响,波动比较大
那么多高的频率算高,多低的频率算低呢?这就涉及到中界频率的概念了,中界频率是,测频法与测周法误差相等的频率点,这个计次数量N,尽量要大一些,N越大,相对误差越小,因为在这些方法中,计次可能会存在正负1误差,比如测频法,在闸门时间内,并不是每个周期信号都是完整的,比如在最后的时间里,可能有个周期刚出现一半,只能舍弃掉或者当作一整个周期来看,因为计次只有整数,不可能计次0.5个数,那在这个过程,就会出现多计一个,或者少计一个的情况,这就叫做正负1误差,另外在测周法这里也有,标准频率fc计次,在最后时刻,有可能像这样,一个数刚数到一半,计时就结束了,那这个半个数也只能舍弃或者按一整个数来算了,这里也会出现正负1误差,要想减小正负1误差的影响,那就只能多计一些数。
当有一个频率,测频法和测周法计次的N相同,就说明误差相同,这不就是中界频率了吗。所以这个公式fm=根号fc/T怎么来的,我们把测频法的N提出来,测周法,N也提出来;令这两个方法,N相等,把fx解出来,就得到中界频率,fm=根号fc/T,对应图上,当待测信号频率小于中界频率时,测周法误差更小,选用测周法更合适,当待测信号大于中界频率时,测频法误差更小,选用测频法更合适
接下来我们用STM32来实现这两种方法,首先,测频法,这个我们用之前学过的外设就可以实现,我们之前写过,对射式红外传感器计次、定时器外部时钟,这些代码稍加改进,就是测频法,比如对射式红外传感器计次,每来一个上升沿计次+1,那我们再用一个定时器,定一个1s的定时中断,在中断里,每隔1s取一下计次值,同时清零计次为下一次做准备,这样每次读取的计次值就直接是频率,对应定时器外部时钟的代码,也是如此,每隔1秒取一下计次,就能实现测频法测量功率的功能了
本节输入捕获测频率,使用的方法是测周法,就是测量两个上升沿之间的时间,来进行频率测量的,我们先回到这个定时器框图从左到右来看,最左边,是四个通道的引脚,参考引脚定义表,就能知道这个引脚是复用在了哪个位置,然后引脚进来,这里有一个三输入的异或门,这个异或门的输入接在了通道1、2、3端口,当三个输入引脚的任何一个有电平翻转时,输出引脚就产生一次电平翻转,之后输出通过数据选择器,到达输入捕获通道1,数据选择器如果选择上面一个,那输入捕获通道1的输入,就是3个引脚的异或值,如果选择下面一个,那异或门就没有用,4个通道各用各的引脚,设计这个异或门,其实还是为三相无刷电机服务的,无刷电机有3个霍尔传感器检测转子的位置,可以根据转子的位置进行换相,有了这个异或门,就可以在前3个通道接上无刷电机的霍尔传感器,然后这个定时器就作为无刷电机的接口定时器去驱动换相电路工作。
然后继续看,来到了输入滤波器和边沿检测器,输入滤波器可以对信号进行滤波,避免一些高频的毛刺信号误触发,然后边沿检测器,这就和外部中断那里是一样的,可以选择高电平触发,或者低电平触发,当出现指定的电平时,边沿检测电路就会触发后续电路执行动作,另外这里,它其实是设计了两套滤波和边沿检测电路,第一套电路,经过滤波和极性选择,得到TI1FP1(TI1 Filter Polarity 1),输入给通道1的后续电路,第二套电路,经过另一个滤波和极性选择,得到TI1FP2(TI1 Filter Polarity 2),输入给下面通道2的后续电路,同理,下面TI2信号进来,也经过两套滤波和极性选择,得到TI2FP1,和TI2FP2,提供TI2PF1输入给上面,TI2PF2输入给下面,在这里,两个信号进来,可以选择各走各的,也可以选择进行一个交叉,让CH2引脚输入给通道1,或者CH1引脚输入给通道2,那这里为什么要进行一个交叉连接呢,这样做的目的主要有两个,第一,可以灵活切换后续捕获电路的输入,比如你一会想以CH1作为输入,一会想以CH2作为输入,这样就可以通过这个数据选择器,灵活的进行选择;第二个目的,也是它交叉的主要目的,就是可以把一个引脚的输入,同时映射到两个捕获单元,这也是PWMI模式的经典结构,第一个捕获通道,使用上升沿触发,用来捕获周期,第二个通道使用下降沿触发,用来捕获占空比,两个通道同时对一个引脚进行捕获,就可以同时测量频率和占空比,这就是PWMI模式
一个通道灵活切换两个引脚,和两个通道同时捕获一个引脚,这就是这里交叉一下的作用和目的,同样,下面通道3和通道4也是同样的结构,另外,这里还有个TRC信号,也可以选择作为捕获部分的输入,这个TRC信号是来源于这里的然后继续往后看,输入信号进行滤波和极性选择后,就来到了预分频器,预分频器,每个通道各有一个,可以选择对前面的信号进行分频,分频之后的触发信号,就可以触发捕获电路正常进行工作了,每来一个触发信号,CNT的值,就会向CCR转运一次,转运的同时,会发生一个捕获事件,这个事件会在状态寄存器标志位,同时也可以产生中断,如果需要在捕获的瞬间,处理一些事情的话,就可以开启这个捕获中断,这就是整个电路的工作流程,比如我们可以控制上升沿触发捕获,每来一个上升沿,CNT转运到CCR一次,又因为整个CNT计数器是由内部的标准时钟驱动的,所以CNT的数值,其实就可以用来记录两个上升沿之间的时间间隔,这个时间间隔就是周期,再取个倒数,就是测周法的频率了
上升沿用于触发输入捕获,CNT用于计数计时,每来一个上升沿,取一下CNT的值,自动存在CCR里,CCR捕获到的值,就是计数值N,CNT的驱动时钟,就是fc,fc/N就得到了待测信号的频率。另外这里还有给个细节问题,就是每次捕获之后,我们都要把CNT清零一下,这样下次上升沿再捕获的时候,取出的CNT才是两个上升沿的时间间隔,这个在一次捕获后自动将CNT清零的步骤,我们可以用主从触发模式,自动来完成
这是输入捕获通道1的一个更详细的框图
这个结构,我们只使用了一个通道,所以它目前只能测量频率,在右上角这里是时基单元,我们把时基单元配置好,启动定时器,那这个CNT就会在预分频之后的这个时钟驱动下,不断自增,这个CNT,就是我们测周法用来计数计时的东西,经过预分频之后这个位置的时钟频率,就是驱动CNT的标准频率fc,这里不难看出,标准频率=72MHz/预分频系数,然后下面输入捕获通道1的一个GPIO口,输入一个这样的方波信号,经过滤波器和边沿检测,选择TI1FP1为上升沿触发,之后输入选择直连的通道,分频器选择不分频,当TI1FP1出现上升沿之后,CNT的当前计数值转运到CCR1里,同时触发源选择,选中TI1FP1为触发信号,从模式选择复位操作,这样TI1FP1的上升沿,也会通过上面这一路去触发CNT清零,当然这里会有个先后顺序,肯定是得先转运CNT的值到CCR里去,再触发从模式给CNT清零或者是非阻塞的同时转移,CNT的值转移到CCR,同时0转移到CNT里去
然后看下左上角的图,在这里,信号出现一个上升沿,CCR1=CNT,就是把CNT的值转运到CCR1里面去,这里输入捕获是1自动执行的,然后CNT=0,清零计数器,这是从模式自动执行的,然后在一个周期之内,CNT在标准时钟的驱动下,不断自增,并且由于之前清零过了,所以CNT就是从上升沿开始,从0开始计数,一直++,直到,下一次上升沿来临,然后执行相同的操作,CCR1的值不断刷新,始终保持为最新一个周期的计数值,这个计数值就是N,然后fc/N就是信号的频率,所以当我们想要读取信号的频率时,只需要读取CCR1得到N,再计算fc/N,就行了,当我们不需要读取的时候,整个电路全自动测量,不需要占用任何软件资源
注意事项:ARR一般设置为最大65535,CNT最大也只能计65535个数,如果信号频率太低,CNT'计数值可能会溢出;从模式的触发源选择,在这里看的话,只有TI1FP2和TI2FP2,没有TI3和TI4的信号,所以如果想使用从陌模式自动清零CNT,只能使用通道1和通道2,对于通道3和通道4,就只能开启捕获中断,在中断里手动清零
这里展示的是PWMI基本结构,这个PWMI,使用了两个通道同时捕获一个引脚,可以同时测量周期和占空比,上面这部分结构,和刚才演示的一样,下面这里多了一个通道,首先TI1FP1配置上升沿触发,触发捕获和清零CNT,正常的捕获周期,这时我们再来一个TI1FP2,配置为下降沿触发,通过交叉通道,去触发通道2的捕获单元
这时会发生什么,我们看左上图,最开始上升沿,CCR1捕获,同时清零CNT,之后CNT一直++,然后,在下降沿这个时刻,触发CCR2捕获,所以这时CCR2的值,就是高电平期间的计数值,CCR2捕获,并不触发CNT清零,所以CNT继续++,直到下一次上升沿,CCR1捕获周期,CNT清零,这样捕获之后,CCR1就是一整个周期的计数值,CCR2就是高电平期间的计数值,我们用CCR2/CCR1,就是占空比,这就是PWMI模式,使用两个通道来捕获频率和占空比的思路
另外这里,可以两根个通道捕获第一个引脚的输入,这样通道前面这一部分就没有用到,当然也可以配置两个通道同时捕获第二个引脚的输入
输入捕获模式测频率
配置步骤
第一步,RCC开启时钟,把GPIO和TIM的时钟打开
第二步,GPIO初始化,把GPIO配置成输入模式,一般选择上拉输入或者浮空输入模式
第三步,配置时基单元,让CNT计数器在内部时钟的驱动下自增运行
第四步,配置输入捕获单元,包括滤波器、极性、直连通道还是交叉通道、分频器这些参数,用一个结构体就可以统一配置了
第五步,选择从模式的触发源,触发源选择为TI1FP1,这里调用一个库函数,给一个参数就行了
第六步,选择触发之后执行的操作,执行Reset操作,这里也算是调用一个库函数就行了,最后当这些电路都配置好之后,调用TIM_Cmd函数,开启定时器。
IC.c
#include "stm32f10x.h" // Device header
/**
* 函 数:输入捕获初始化
* 参 数:无
* 返 回 值:无
*/
void IC_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //开启TIM3的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
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); //将PA6引脚初始化为上拉输入
/*配置时钟源*/
TIM_InternalClockConfig(TIM3); //选择TIM3为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM3的时基单元
/*输入捕获初始化*/
TIM_ICInitTypeDef TIM_ICInitStructure; //定义结构体变量
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择配置定时器通道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_ICInit,配置TIM3的输入捕获通道
/*选择触发源及从模式*/
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1); //触发源选择TI1FP1
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset); //从模式选择复位
//即TI1产生上升沿时,会触发CNT归零
/*TIM使能*/
TIM_Cmd(TIM3, ENABLE); //使能TIM3,定时器开始运行
}
/**
* 函 数:获取输入捕获的频率
* 参 数:无
* 返 回 值:捕获得到的频率
*/
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1); //测周法得到频率fx = fc / N,这里不执行+1的操作也可
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
PWM_Init(); //PWM初始化
IC_Init(); //输入捕获初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Freq:00000Hz"); //1行1列显示字符串Freq:00000Hz
/*使用PWM模块提供输入捕获的测试信号*/
PWM_SetPrescaler(720 - 1); //PWM频率Freq = 72M / (PSC + 1) / 100
PWM_SetCompare1(50); //PWM占空比Duty = CCR / 100
while (1)
{
OLED_ShowNum(1, 6, IC_GetFreq(), 5); //不断刷新显示输入捕获测得的频率
}
}
PWMI模式测频率占空比
IC.c
#include "stm32f10x.h" // Device header
/**
* 函 数:输入捕获初始化
* 参 数:无
* 返 回 值:无
*/
void IC_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //开启TIM3的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
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); //将PA6引脚初始化为上拉输入
/*配置时钟源*/
TIM_InternalClockConfig(TIM3); //选择TIM3为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM3的时基单元
/*PWMI模式初始化*/
TIM_ICInitTypeDef TIM_ICInitStructure; //定义结构体变量
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择配置定时器通道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_PWMIConfig,配置TIM3的输入捕获通道
//此函数同时会把另一个通道配置为相反的配置,实现PWMI模式
/*选择触发源及从模式*/
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1); //触发源选择TI1FP1
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset); //从模式选择复位
//即TI1产生上升沿时,会触发CNT归零
/*TIM使能*/
TIM_Cmd(TIM3, ENABLE); //使能TIM3,定时器开始运行
}
/**
* 函 数:获取输入捕获的频率
* 参 数:无
* 返 回 值:捕获得到的频率
*/
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1); //测周法得到频率fx = fc / N,这里不执行+1的操作也可
}
/**
* 函 数:获取输入捕获的占空比
* 参 数:无
* 返 回 值:捕获得到的占空比
*/
uint32_t IC_GetDuty(void)
{
return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1); //占空比Duty = CCR2 / CCR1 * 100,这里不执行+1的操作也可
}
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
PWM_Init(); //PWM初始化
IC_Init(); //输入捕获初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "Freq:00000Hz"); //1行1列显示字符串Freq:00000Hz
OLED_ShowString(2, 1, "Duty:00%"); //2行1列显示字符串Duty:00%
/*使用PWM模块提供输入捕获的测试信号*/
PWM_SetPrescaler(720 - 1); //PWM频率Freq = 72M / (PSC + 1) / 100
PWM_SetCompare1(50); //PWM占空比Duty = CCR / 100
while (1)
{
OLED_ShowNum(1, 6, IC_GetFreq(), 5); //不断刷新显示输入捕获测得的频率
OLED_ShowNum(2, 6, IC_GetDuty(), 2); //不断刷新显示输入捕获测得的占空比
}
}
TIM编码器接口
Encoder Interface 编码器接口
编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置,旋转方向和旋转速度
每个高级定时器和通用定时器都拥有1个编码器接口
两个输入引脚借用了输入捕获的通道1和通道2
这里编码器测速一般应用在电机控制的项目上,使用PWM驱动电机,再用编码器测量电机的速度,然后再用PID算法进行闭环控制,这是一个比较常见的使用场景