STM32学习笔记
这个笔记是基于标准库的,跟随江协教程学习。
1-新建工程
新建工程时要选择启动文件,其选择依据芯片类型,具体见下表
缩写 |
释义 |
Flash容量 |
型号 |
DL_VL |
小容量产品超值系列 |
16-32K |
STM32F100 |
MD_VL |
中容量产品超值系列 |
64-128K |
STM32F100 |
HD_VL |
大容量产品超值系列 |
256-512K |
STM32F100 |
LD |
小容量产品 |
16-32K |
STM32F101/102/103 |
MD |
中容量产品 |
64-128K |
STM32F101/102/103 |
HL |
大容量产品 |
256-512K |
STM32F101/102/103 |
XL |
加大容量产品 |
大于512K |
STM32F101/102/103 |
CL |
互联型产品 |
- |
STM32F105/107 |
总结:
新建工程的步骤:
· 建立工程文件夹,Keil中新建工程,选择型号
· 工程文件夹里建立StartLibrary、User等文件夹,2复制固件库里面的文件到工程文件夹
· 工程里对应建立Start、Library、User等同名称的分组,然后将文件夹的文件添加到工程分组里
· 工程选项,C/C++,Include Paths内声明所有包含头文件的文件夹
· 工程选项,C/C++,Define内定义USE_STDPERIPH_DRIVER
· 工程选项,Debug,下拉列表选择对应调试器,Settings,FlashDownload里勾选Reset and Run
工程架构:
1 2 3 4 5 6 7 8
| startup_xx.c: system_xx.c/.h: stm32f10x.h: 复位中断: 定义Systeminit 外设寄存器描述 调用Systeminit main.c: core_cm3.c/.h: 调用main 定义main 内核寄存器描述 其他中断: stm32f10x_it.c/.h: misc.c/.h,stm32f10x_abc.c/.h…: 调用中断 定义中断 库函数 处理函数 处理函数 stm32f10x_conf.h: 其他用户文件 库函数配置
|
2-GPIO
GPIO简介
GPIO即通用输入输出口
可配置8种输入输出模式
引脚:0-3.3V,部分可容忍5V
输出模式下可以控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等
输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等
GPIO基本结构
由于stm32是32位的单片机,stm32内部的寄存器都是32位的,但是端口只有16个,所以寄存器只有低16位有对应端口
注:输入驱动器中的肖特基触发器实际为施密特触发器,此处为翻译错误
1 2
| 施密特触发器:当电压高于一定值的时候立马输出高电压,当电压低于一定值的时候立马输出低电压,处于中间保持 此处用于整形,防止信号失真
|
输出数据清除器同时控制16个端口且只能整体读写,想要控制个别位需要用”&=”以及”|=”的方法,比较麻烦,利用前面的位设置/清除寄存器可以仅控制输出数据寄存器的某一位而不影响其他位
输出控制有推挽、开漏、关闭三种输出模式
推挽:此模式下,P-MOS和N-MOS均有效。数据寄存器为1时,P导通N断开,输出接VDD,高电平;数据寄存器为0时,P断开N导通,输出接VSS,低电平。此状态下高低电平均有较强驱动能力,所以又称强推输出模式。
开漏:此模式下P-MOS无效,N-MOS有效。数据寄存器为1时,N断开,相当于输出断开,也就是高阻模式;数据寄存器为0时,N导通,输出接VSS,低电平。此模式可以作为通信协议的驱动模式,如I2C,此模式下可以避免各个设备的相互干扰;同时在此模式下,通过在IO口外接上拉电源(以5V为例),数据寄存器输出1时由外部电路上拉到5V,就可以兼容5V的设备。
关闭,此时P-MOS和N-MOS均关闭,IO口由外部电路控制。
GPIO模式
通过配置GPIO的端口寄存器,端口可配置成以下8种模式
四种输出方式:
(1)推挽输出 可输出引脚电平,高电平为高阻态,低电平接VSS
(2)开漏输出 可输出引脚电平,高电平接VDD,低电平接VSS
(3)复用推挽 由片上外设控制,高电平为高阻态,低电平接VSS
(4)复用开漏 由片上外设控制,高电平接VDD,低电平接VSS
四种输入方式:
(1)浮空输入 可读取引脚电平,若引脚悬空,则电平不确定
(2)上拉输入 可读取引脚电平,内部连接上拉电阻,悬空时默认高电平
(3)下拉输入 可读取引脚电平,内部连接下拉电阻,悬空时默认低电平
(4)模拟输入 GPIO无效,引脚直接接入内部ADC
浮空/上拉/下拉输入
在输入模式下,输出寄存器是断开的,端口只能输入而不能输出。
输入寄存器的两个的电阻可以上拉工作下拉工作或者都不工作,对应上拉输入下拉输入和浮空输入。
右侧保护二极管,VDD和VDD_FT不同,后者对5V容忍IO脚是特殊的
模拟输入
在次模式下,IO口直接模拟输入,其余部分全部无效
开漏/推挽输出
P-MOS无效时是开漏输出,P-MOS和N-MOS均有效时是推挽输出。
在输出模式下输入模式是有效的。
复用开漏/复用推挽输出
此状态下通用输出没有连接,引脚的控制权转移到片上外设。
LED和蜂鸣器
LED:反光二极管
蜂鸣器:分为有源无源,有源自带振荡源,接上直流电压即可持续发声;无源内部不带振荡源,需要控制器提供震荡脉冲才可发声,调整振荡脉冲频率可发出不同频率的声音。
面包板
结构如下图:
应用1.LED闪烁
电路板连接图:
使用GPIO点灯的步骤:
- 使用RCC开启GPIO时钟;
- 使用GPIO_Init()函数初始化
- 使用输出或者输入函数控制GPIO口
GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)和GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)是设置引脚电平的两个函数,其中第一个参数是GPIO口(如GPIOA),第二个参数是引脚号(如GPIO_PIN_0)
攥写一段代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include "stm32f10x.h"
int main(void) { 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;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_ResetBits(GPIOA,GPIO_Pin_0); while(1) { } }
|
写入后得到现象如下图:
可以看到LED成功被点亮。
在我们使用GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)时,LED熄灭。
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal)是对单个引脚写入信号的函数,在上程序中,将GPIO_ResetBits(GPIOA,GPIO_Pin_0);
替换为GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET);
也能得到点亮的效果。
下面我们要实现LED的闪烁,通过使用Delay模块即可实现(Delay模块的编写在之后的课程)。代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include "stm32f10x.h" #include "Delay.h"
int main(void) { 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;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); 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闪烁。使用SetBits或者ResetBits得到的效果同样。
在WriteBit函数中,BIT_(RE)SET可以换成0或者1,但是前面要加上”(BitAction)”切换成枚举类型
将LED长短脚位置互换之后,LED依旧闪烁,说明推挽模式下高低电平都有驱动能力;将模式改成OUT_OD(开漏输出)LED不亮了,说明开漏输出的高电平无驱动能力,重新将LED长短脚换回原来位置,LED回复闪烁,说明开漏模式下低电平有驱动能力。
应用2.LED流水灯
面包板连线:
在编写流水灯的程序,设置引脚的时候可以使用“|”来同时设置多个引脚,在流水灯应用中,我们需要使用0-7,共八个引脚,所以可以写GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5| GPIO_Pin_6 | GPIO_Pin_7;
最终代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include "stm32f10x.h" #include "Delay.h"
unsigned char i;
int main(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5| GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); GPIO_Init(GPIOA,&GPIO_InitStructure);
while(1) { for ( i = 0; i < 8; i++) { GPIO_Write(GPIOA,~(0x0001<<i)); Delay_ms(500); } } }
|
效果图如下:
应用3.蜂鸣器
面包板连线:
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include "stm32f10x.h" #include "Delay.h"
unsigned char i;
int main(void) { 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;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); GPIO_Init(GPIOB,&GPIO_InitStructure);
while(1) { GPIO_ResetBits(GPIOB,GPIO_Pin_13); Delay_ms(500); GPIO_SetBits(GPIOB,GPIO_Pin_13); Delay_ms(500); } }
|
注:我在使用的时候连接了B13,所以代码中写的是13
传感器模块
给予的工具包包含了四个传感器模块:光敏/热敏/对射式红外/反射式红外传感器,图片及电路图如下:
结构较简单,不多赘述
部分C语言知识点
数据类型
浮点数不能加unsigned
在单片机编程中,原本用于定义字符的char常被用于定义整形,所以C和ST就给其定义了新的关键字,出处于相同原因,部分其他关键字也被定义了新的名字,具体见下表:
关键字 |
位数 |
表示范围 |
stdint关键字 |
ST关键字 |
char |
8 |
-128 - 127 |
int8_t |
s8 |
unsigned char |
8 |
0-255 |
uint8_t |
u8 |
short |
16 |
-32768 - 32767 |
int16_t |
s16 |
unsigned short |
16 |
0 - 65535 |
uint16_t |
u16 |
int |
32 |
-2147483648 - 2147483647 |
int32_t |
s32 |
unsigned int |
32 |
0 - 4294967295 |
uint32_t |
u32 |
long |
32 |
-2147483648 - 2147483647 |
|
s32 |
unsigned long |
32 |
0 - 4294967295 |
|
u32 |
long long |
64 |
-(2^64)/2 - (2^64)/2-1 |
int64_t |
|
unsigned long long |
32 |
0 - (2^64)-1 |
uint64_t |
|
float |
32 |
-3.4e38 - 3.4e38 |
|
|
double |
64 |
-1.7e308 = 1.7e308 |
|
|
注:ST关键字是老版本的,推荐使用stdint关键字
宏定义
·关键字:#define
·用途:将一个字符串改成数字,便于理解防止出错;提取程序中常出现的参数,便于修改
e.p. : 定义宏定义:
#define ABC 12345
引用宏定义:
int a = ABC; //等效于int a = 12345;
typedef
·关键字:typedef
·用途:将比较长的变量类型名字改名字,便于使用
e.p. : 定义typedef:
typedef unsigned char uint8_t;
引用typedef:
unit8_t a; //等效于unsigned char a;
与宏定义的不同:
1. 新名字在后面
2. 仅能用于变量类型名
3. typedef会进行检查,更安全
结构体
·关键字:struct
·用途:数据打包,不同类型变量的集合
e.p. : 定义结构体:
struct{char x;int y;float z;} StructName;
因为结构体变量类型较长,所以常用typedef更改变量类型名
引用结构体:
StructName.x= ‘A’;
StructName.y= 66;
StructName. z= 1.14;
或:pStructName->x= ‘A’; //pSrtuctName是结构体的地址
pStructName->y= 66;
pStructName->z= 1.14;
使用typedef举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| typedef struct{ char x; int y; float z; } StructName_1;
int main() { StructName_1 c; StructName_1 d; c.x= 'A'; c.y= 66; c. z= 1.14;
prinf("c.x-%c\n",c.x);
return 0; }
|
枚举
·关键字:enum
·用途:定义一个取值受限的整型变量,用于限制变量取值范围;宏定义的集合
e.p. : 定义枚举变量:
enum{FALSE = 0, TRUE = 1} EnumName;
因为枚举变量变量类型较长,所以常用typedef更改变量类型名
引用typedef:
EnumName=FALSE;
EnumName=TRUE;
应用4.按键控制LED
面包板连接图如下:
连接完成品如下:
为了保持主函数整洁,将部分代码封装到头文件Key.c和LED.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #include "stm32f10x.h" // Device header #include "Delay.h"
/** * 函 数:按键初始化 * 参 数:无 * 返 回 值:无 */ void Key_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_1 | GPIO_Pin_11; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB1和PB11引脚初始化为上拉输入 }
/** * 函 数:按键获取键码 * 参 数:无 * 返 回 值:按下按键的键码值,范围:0~2,返回0代表没有按键按下 * 注意事项:此函数是阻塞式操作,当按键按住不放时,函数会卡住,直到按键松手 */ uint8_t Key_GetNum(void) { uint8_t KeyNum = 0; //定义变量,默认键码值为0 if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) //读PB1输入寄存器的状态,如果为0,则代表按键1按下 { Delay_ms(20); //延时消抖 while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0); //等待按键松手 Delay_ms(20); //延时消抖 KeyNum = 1; //置键码为1 } if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0) //读PB11输入寄存器的状态,如果为0,则代表按键2按下 { Delay_ms(20); //延时消抖 while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == 0); //等待按键松手 Delay_ms(20); //延时消抖 KeyNum = 2; //置键码为2 } return KeyNum; //返回键码值,如果没有按键按下,所有if都不成立,则键码为默认值0 }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| #include "stm32f10x.h" // Device header
/** * 函 数:LED初始化 * 参 数:无 * 返 回 值:无 */ void LED_Init(void) { /*开启时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟 /*GPIO初始化*/ 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); //将PA1和PA2引脚初始化为推挽输出 /*设置GPIO初始化后的默认电平*/ GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2); //设置PA1和PA2引脚为高电平 }
/** * 函 数:LED1开启 * 参 数:无 * 返 回 值:无 */ void LED1_ON(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_1); //设置PA1引脚为低电平 }
/** * 函 数:LED1关闭 * 参 数:无 * 返 回 值:无 */ void LED1_OFF(void) { GPIO_SetBits(GPIOA, GPIO_Pin_1); //设置PA1引脚为高电平 }
/** * 函 数:LED1状态翻转 * 参 数:无 * 返 回 值:无 */ void LED1_Turn(void) { if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_1) == 0) //获取输出寄存器的状态,如果当前引脚输出低电平 { GPIO_SetBits(GPIOA, GPIO_Pin_1); //则设置PA1引脚为高电平 } else //否则,即当前引脚输出高电平 { GPIO_ResetBits(GPIOA, GPIO_Pin_1); //则设置PA1引脚为低电平 } }
/** * 函 数:LED2开启 * 参 数:无 * 返 回 值:无 */ void LED2_ON(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_2); //设置PA2引脚为低电平 }
/** * 函 数:LED2关闭 * 参 数:无 * 返 回 值:无 */ void LED2_OFF(void) { GPIO_SetBits(GPIOA, GPIO_Pin_2); //设置PA2引脚为高电平 }
/** * 函 数:LED2状态翻转 * 参 数:无 * 返 回 值:无 */ void LED2_Turn(void) { if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_2) == 0) //获取输出寄存器的状态,如果当前引脚输出低电平 { GPIO_SetBits(GPIOA, GPIO_Pin_2); //则设置PA2引脚为高电平 } else //否则,即当前引脚输出高电平 { GPIO_ResetBits(GPIOA, GPIO_Pin_2); //则设置PA2引脚为低电平 } }
|
主程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include "stm32f10x.h" // Device header #include "Delay.h" #include "LED.h" #include "Key.h"
uint8_t KeyNum; //定义用于接收按键键码的变量
int main(void) { /*模块初始化*/ LED_Init(); //LED初始化 Key_Init(); //按键初始化 while (1) { KeyNum = Key_GetNum(); //获取按键键码 if (KeyNum == 1) //按键1按下 { LED1_Turn(); //LED1翻转 } if (KeyNum == 2) //按键2按下 { LED2_Turn(); //LED2翻转 } } }
|
具体功能见注释,通过这几个程序可以做到使用按键控制LED亮灭。
应用5.光敏传感器控制蜂鸣器
接线图如下:
连接完成后实物图如下:
头文件:
LightSensor.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include "stm32f10x.h"
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); }
|
LED.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| #include "stm32f10x.h"
void LED_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_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); }
void LED1_ON(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_1); }
void LED1_OFF(void) { 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) { GPIO_ResetBits(GPIOA, GPIO_Pin_2); }
void LED2_OFF(void) { 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); } }
|
主程序如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include "stm32f10x.h" #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(); } } }
|
OLED
简介
OLED(Organic Light Emitting Diode)
供电:3-5.5V,通信协议:I2C/SPI,分辨率:128*64(0.96寸)
OLED显示屏电路原理图:
调试方式
- 串口调试:通过串口通信将调试信息发送到电脑端,电脑使用串口助手显示调试信息
- 显示屏调试:将显示屏连到单片机,将调试信息直接在显示屏上显示
- Keil调试模式:借助keil软件的调试功能,可使用单步运行、设置断点、查看寄存器以及变量等功能(调试模式下不能修改代码)
驱动函数
将OLED显示屏区分为4行16列
教程给出的函数有:
函数 |
功能 |
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) |
显示16进制数字 |
OLED_ShowBinNum(4,1,0xAA55,16) |
显示2进制数字 |
实体演示
面包板接线图 ;
编写如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include "stm32f10x.h" #include "Delay.h" #include "OLED.h"
int main(void) { OLED_Init();
OLED_ShowChar(1,1,'A'); OLED_ShowString(1,3,"Homo"); OLED_ShowNum(2,1,114,3); OLED_ShowHexNum(2,4,0x514,3); OLED_ShowSignedNum(3,1,-19198,5); OLED_ShowBinNum(4,1,0x2,2); while(1) { } }
|
显示结果如下:
莫名感觉有点可爱
EXTI外部中断
简介
EXTI(Extern Interrupt)外部中断
EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO产生电平变化时,EXTI会立刻向NVIC发出中断申请,经NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序
支持的触发方式:上升沿/下降沿/双边沿/软件触发
支持全部GPIO口,但是相同的Pin不能同时触发中断
通道数:16个GPIO,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒
触发相应方式:中断响应/事件响应(中断响应是触发中断,事件响应是触发别的外设操作)
中断系统
中断:主程序运行时出现了特定的中断触发条件(中断源),使CPU暂停当前程序转去处理中断程序,处理完之后返回暂停位置继续执行原本程序
中断优先级:多个中断源同时申请中断的时候,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源
中断嵌套:一个中断正在运行的时候,有优先级更高的中断源申请中断的时候,CPU会再次暂停当前中断程序,转去处理新的中断程序,处理完之后依次返回
STM32中断包括:EXTI,TIM,ADC,USRT,SPI,I2C,PTC
中断向量表:
NVIC
NVIC基本结构
使用NVIC为了防止中断占用过多CPU引脚以及防止优先级分组对CPU性能的占用
NVIC优先级分组
NVIC的中断优先级由优先级寄存器的四位(0-15)组成,这四位可以进行切分,分成高n位的抢占优先级和低4-n位的相应优先级
抢断优先级高的可以中断嵌套,响应优先级高的可以优先排队,两者相同按中断号排队
EXTI
EXTI基本结构
结构框图:
AFIO是一个数据选择器,他会选择前面连接的一个引脚连接到EXTI,这也是为什么前面说每个引脚只能连接一个中断。
*注:原本EXTI有20路通道输出,但是其中5-9,10-15分别被集中到一个引脚中,即5-9,10-15分别会触发同一个中断函数,在这两个中断函数中,我们要通过标志位来判断具体是哪个中断
下面还有20个引脚连接外设
AFIO复用IO口
AFIO电路图:
AFIO主要用于引脚复用功能的选择和重定义
在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择
EXTI内部框图
EXTI内部框图:
图中右下角输入线就是20根输入线,通过检测器决定是进行上升沿触发/下降沿触发/两者
请求挂起寄存器可以被读取以判断是哪个通道触发的判断
· 需要使用外部中断的情况:外部驱动的突发事件
旋转编码器介绍
旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向
类型:机械触点式/霍尔传感器式/光栅式
光栅式:通过旋钮上光栅阻挡信号产生信号波,但是只能测位置和速度不能测方向
机械触电式:金属触点经过设计,正反转时。两个触电会有不同的相位差,通过相位差可以判断方向
硬件电路:
应用1.对射式红外传感器计次
面包板连接图:
头文件:
CountSensor.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| #include "stm32f10x.h"
uint16_t CounterSensor_Count;
void CounterSensor_Init() { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); 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);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
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_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_GetCount() { return CounterSensor_Count; }
void EXTI15_10_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line14)==SET) { if (GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_14)==0) { CounterSensor_Count++; } EXTI_ClearITPendingBit(EXTI_Line14); } }
|
主程序:
main.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include "stm32f10x.h" #include "Delay.h" #include "OLED.h" #include "CountSensor.h"
int main(void) { OLED_Init(); CounterSensor_Init();
OLED_ShowChar(1,1,'A');
while(1) { OLED_ShowNum(2,1,CountSensor_GetCount(),5); } }
|
现象:每次对射式红外传感器中间被隔光,OLED上显示次数+1
应用2.旋转编码器计次
面包板接线图:
![旋转编码计数器](https://pic .imgdb.cn/item/65c481509f345e8d03201b8a.jpg)
TIM定时器中断
TIM简介
定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断
16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时
不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能
根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型
类型 |
编号 |
总线 |
功能 |
高级定时器 |
TIM1、TIM8 |
APB2 |
拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能 |
通用定时器 |
TIM2、TIM3、TIM4、TIM5 |
APB1 |
拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能 |
基本定时器 |
TIM6、TIM7 |
APB1 |
拥有定时中断、主模式触发DAC的功能 |
STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4
基本定时器
PSC预分频器:输出频率=输入频率/输入的值
自动重装寄存器记录写入的计数目标,当计数==技术目标的时候产生一个中断信号并清零计数器
此类计数值等于自动重装值产生的中断叫做“更新中断”
通用定时器
通用定时器除了向上计数(从0开始累计到重装值之后清零同时申请中断),还支持向下计数(从重装值开始递减,到0之后回到重装值并申请中断)和中央对齐计数(先从0向上到重装值申请中断然后从重装值向下到0再申请中断,依次循环),高级计时器也支持这三类模式
除了使用内部72Hz的时钟,还可以通过ETR连接外部时钟(通过ETRF作为时钟使用较简单,通过TRGI会占据通道
ITR引脚来自其他定时器的TRGO输出
高级定时器
基本和通用计时器只能每个计数周期发生一次中断,但是高级计时器可以使用重复次数计时器完成多个技术周期进行一次中断
定时中断基本结构
预分频器时序图:
预分频器原理:在TIMx_PSC中写入新数据后(改变预分频值),控制,直到当前计数周期结束之后,改变的分频值才会起作用
预分频计数器内部也是靠计数器工作的,分频值为0时,计数器恒为0,分频值为1时,计数器就01计数,回到0时发出一个脉冲,定时器时钟计数。
计数器计数频率:CK_CNT=CK_PSC/(PSC+1)
计时器时序
UIF需要在中断程序中手动清零
计数器溢出频率:CK_CNT_OV=CK_CNT/(ARR+1)=CK_PSC/(PSC+1)/(ARR+1)
计数器无预装时序
计数器中有缓冲计时器,可以通过修改APRE的值控制是否使用
在无预装时许的情况下修改技术目标,若当前计数值小于技术目标,则到达修改后的计数目标然后重置,若大于,则会因为无法等于技术目标而一直到上限再重置
计数器有预装时序
影子计数器用于同步防止出错
RCC时钟树
在不修改参数的情况下,三种计时器都是72Hz
应用1.定时器定时中断
面包板接线图:
头文件Timer.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include "stm32f10x.h"
extern uint16_t Num;
void TImer_Init(void) { 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; TIM_TimeBaseInitStructure.TIM_Period=10000-1; 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_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) { Num++; TIM_ClearITPendingBit(TIM2,TIM_IT_Update); } }
|
主程序 main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include "stm32f10x.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(2,1,Num,5); } }
|
应用2.定时器外部时钟
面包板接线图:
输出比较
输出比较简介
·OC(Output Compare) 输出比较可以通过比较CNT和CRR寄存器值的关系来输出电平进行置0、置1或反转的操作,用于输出一点频率和占空比的PWM波形
·每个高级定时器和通用定时器都有4个输出比较通道
·高级定时器的前三个通道额外拥有死区生成和互补输出的功能
PWM简介
PWM(Pluse Width Modulation)脉冲宽度调制
在具有惯性的系统中,可通过对一系列脉冲的宽度进行调制来等效的获取需要的模拟参量,常用于电机控速等领域
PWM参数:
频率=1/T_s 占空比=T_ON/T_s 分辨率=占空比变化步距
输出比较通道
下图是输出比较通道框图(高级和普通)
为了控制电机,高级输出比较通道的OC1和OC1N需要输出互补的信号,在实际应用中可能会因为元件的不理想,导致出现上管未关闭下管就导通的现象,会导致功率损耗产生大量热,为防止该现象使用死区发生器,可以在一管关闭时延迟一段时间再导通另一管,避免同时导通。
通过比较CNT和CCR可以使输出模式控制器输出高低电平,通过调节寄存器可以选择不同模式,模式表如下:
模式 |
描述 |
冻结 |
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置无效电平 |
匹配时电平翻转模式可以平稳输出占空比为50%的信号,匹配时置有效电平和匹配时置有效电平相对不适合输出连续信号,比较适用于当你想要输出某一信号的情况
PWM基本结构
PWM基本结构框图:
由图可以看出PWM的基本结构
PWM参数计算
- PWM频率: Freq = CK_PSC/(PSC+1)/(ARR+1)
- PWM占空比:Duty = CRR/(ARR+1)
- PWM分辨率:Reso = 1/(ARR+1)
舵机简介
舵机是一种根据输入PWM信号占空比来控制输出角度的装置
输入PWM信号要求:周期为20ms,高电平宽度为0.5ms~2.5ms
如图:舵机不是单纯的电机,而是由多个期间组成,大概逻辑就是PWM波给电板一个指定角度,判断当前角度,大则反转小则正转
↑根据该图可以得到输入信号脉冲宽度对应的舵机输出角度
在上面图片蓝色舵机的三个引线中,棕色是GND,红色是电源,黄色是信号线,不同型号的舵机可以参考元件的说明书
给舵机供电时需要看电源是否达标
直流电机及驱动
直流电机是一种将电能转换为机械能的装置,有两个电极,当电极正接时,电机正转,当电极反接时,电机反转
直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作
TB6612是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向
VM是电机电源,需要一个能输出大电流的电源,VCC是逻辑电平输入,需要和控制器电压相同
AO和BO是电机输出,PWMA,AIN1,AIN2是AO1/2对应的控制,其中PWMA接PWM信号输出端。
STBY是待机控制,接地待机,接VCC启动,需要时可以接GPIO控制
详细见下表:
应用3.PWM驱动LED呼吸灯
面包板接线图:
头文件 PWM.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #include "stm32f10x.h"
void PWM_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin=GPIO_Pin_15; 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; TIM_TimeBaseInitStructure.TIM_Period=100-1; TIM_TimeBaseInitStructure.TIM_Prescaler=7200-1; 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_OCNPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OC1Init(TIM2,&TIM_OCInitStructure);
TIM_Cmd(TIM2,ENABLE); }
void PWM_SetCompare1(uint16_t Compare) { TIM_SetCompare1(TIM2,Compare); }
|
主程序 mian.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include "stm32f10x.h" #include "Delay.h" #include "OLED.h" #include "PWM.h"
uint16_t i;
int main(void) { OLED_Init(); PWM_Init();
while(1) { for ( i = 0; i < 100; i++) { PWM_SetCompare1(i); Delay_ms(10); } for ( i = 0; i < 100; i++) { PWM_SetCompare1(100-i); Delay_ms(10); } } }
|
总体步骤和51单片机呼吸灯相似
应用4.PWM驱动电机
面包板接线图:
所有PWM通道输出信号跳变都是同步的,所以可以四个一起用
头文件 PWM.c(有更改)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #include "stm32f10x.h"
void PWM_Init(void) { 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_1; 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; TIM_TimeBaseInitStructure.TIM_Period=2000-1; TIM_TimeBaseInitStructure.TIM_Prescaler=72-1; 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_OCNPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OC2Init(TIM2,&TIM_OCInitStructure);
TIM_Cmd(TIM2,ENABLE); }
void PWM_SetCompare2(uint16_t Compare) { TIM_SetCompare2(TIM2,Compare); }
|
Servo.c:
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include "stm32f10x.h" #include "PWM.h"
void Servo_Init(void) { PWM_Init(); }
void Servo_SetAngle(float Angle) { PWM_SetCompare2(Angle/100*2000+500); }
|
主程序 main.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include "stm32f10x.h" #include "Delay.h" #include "OLED.h" #include "Servo.h" #include "Key.h"
uint16_t KeyNum; float Angle=0;
int main(void) { OLED_Init(); Key_Init(); Servo_Init();
OLED_ShowString(1,1,"Angle:");
while(1) { OLED_ShowSignedNum(2,1,Angle,3); KeyNum=Key_GetNum(); if (KeyNum==1) { Angle+=10; if (Angle>=90) { Angle=0; } Servo_SetAngle(Angle); } } }
|
应用5.PWM驱动直流电机
面包板接线图:
头文件
Motor.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include "stm32f10x.h" #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_ResetBits(GPIOA,GPIO_Pin_4); GPIO_SetBits(GPIOA,GPIO_Pin_5); PWM_SetCompare3(-Speed); } }
|
主程序
mian.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include "stm32f10x.h" #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:"); Motor_SetSpeed(20); while(1) { KeyNum=Key_GetNum(); if (KeyNum==1) { Speed+=20; if (Speed>100) { Speed=-100; } } Motor_SetSpeed(Speed); OLED_ShowNum(2,1,Speed,3); } }
|
若电机旋转方向和自己想要的不一致可以更改Motor.c里面的Motor_SetSpeed函数
输入捕获
IC(Input Compare)输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数
每个高级定时器和通用定时器都拥有4个输入捕获通道
可配置为PWMI模式,同时测量频率和占空比
可配合主从触发模式,实现硬件全自动测量
频率测量
测频法:在闸门时间T内,对上升沿计次,得到N,则频率
测频法适合高频
测周法:两个上升沿内,以标准频率fc计次,得到N ,则频率
测周法适合低频
在上述的两种方法中,N越大,误差越大,在某个频率下测频法和测周法的N相同,此频率为中界频率
中界频率:测频法与测周法误差相等的频率点
输入捕获通道
应用6.输入捕获模式测频率
面包板接线图:
头文件
IC.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #include "stm32f10x.h" #include "PWM.h"
void IC_Init(void) { 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; TIM_TimeBaseInitStructure.TIM_Period=65536-1; TIM_TimeBaseInitStructure.TIM_Prescaler=72-1; 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) { return 1000000/(TIM_GetCapture1(TIM3)+1); }
|
主程序
mian.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include "stm32f10x.h" #include "Delay.h" #include "OLED.h" #include "PWM.h" #include "IC.h"
uint16_t i;
int main(void) { OLED_Init(); PWM_Init(); IC_Init(); OLED_ShowString(1,1,"Freq:00000Hz");
PWM_SetPrescaler(720-1); PWM_SetCompare1(50);
while(1) { OLED_ShowNum(1,6,IC_GetFreq(),5); } }
|
应用7.PWMI模式测占空比
面包板接线图:
头文件:
IC.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| #include "stm32f10x.h" #include "PWM.h"
void IC_Init(void) { 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; TIM_TimeBaseInitStructure.TIM_Period=65536-1; TIM_TimeBaseInitStructure.TIM_Prescaler=72-1; 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) { return 1000000/(TIM_GetCapture1(TIM3)+1); }
uint32_t IC_GetPuty(void) { return (TIM_GetCapture2(TIM3)+1)*100/(TIM_GetCapture1(TIM3)+1); }
|
主程序
mian.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include "stm32f10x.h" #include "Delay.h" #include "OLED.h" #include "PWM.h" #include "IC.h"
uint16_t i;
int main(void) { OLED_Init(); PWM_Init(); IC_Init(); OLED_ShowString(1,1,"Freq:00000Hz"); OLED_ShowString(2,1,"Duty:000%");
PWM_SetPrescaler(720-1); PWM_SetCompare1(50);
while(1) { OLED_ShowNum(1,6,IC_GetFreq(),5); OLED_ShowNum(2,6,IC_GetPuty(),3); } }
|
因为没有信号发生器,所以这里的信号由GPIOA_PIN0产生的PW吗波代替
编码器接口
编码器接口(Encoder Interface )可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度
每个高级定时器和通用定时器都拥有1个编码器接口
两个输入引脚借用了输入捕获的通道1和通道2
正交编码器
由图:在正转时A相提前B相90°,反转时A相滞后B相90°(具体哪个提前哪个滞后可以自己改)
使用正交信号好处:
- 更精确
- 抗噪声
编码器接口基本结构:
工作模式:
实例:
通过调节IC1PF1可以使反向
实例(反向):
应用8.编码器接口测速
面包板接线图:
头文件:Encoder.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| #include "stm32f10x.h"
void Encoder_Init(void) { 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; TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; 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) { int16_t Temp; Temp = TIM_GetCounter(TIM3); TIM_SetCounter(TIM3, 0); return Temp; }
|
主程序:main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include "stm32f10x.h" #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); } }
void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) { Speed = Encoder_Get(); TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } }
|
ADC 数模转换器
ADC(Analog-Digital Converter)模拟-数字转换器
ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁
12位逐次逼近型ADC,1us转换时间
输入电压范围:03.3V,转换结果范围:04095
18个输入通道,可测量16个外部和2个内部信号源
规则组和注入组两个转换单元
模拟看门狗自动监测输入电压范围
STM32F103C8T6 ADC资源:ADC1、ADC2,10个外部输入通道
逐次逼近型ADC
通道选择开关八个通道都可输入,地址锁存和译码可选择输入的通道
DAC输入的电压在比较器与通道进入的电压比较,逼近,直至大约相等,此时DAC电压等于输入电压,比较过程使用二分法
CLOCK是时钟,START是输入信号,EOC是输出信号
V_REF是参考电压
ADC框图
左侧ADCx_IN是输入口,经过选择器进入模数转换器(原理类似于上面逐次逼近型ADC),输出数据保存在寄存器,可以读取
在这个模型里,可以一次性选择多个通道,注入通道上限4个,规则通道上限16个
规则通道只有一个寄存器,所以直接使用会导致前面的数据丢失,只有最后的数据,常配合DMA使用,DMA可以将前面的数据移至其他地方存储。
为了减少对程序的影响,左下两个触发使用硬件触发,将TIM3设为TRGO,定时,就可以自动触发ADC转换,节省了触发资源
右上角模拟看门狗,可以设置模拟阈值,当达到阈值的时候就会申请NVIC的ADC中断,同样,(注入)转换结束也能申请中断
输入通道
ADC1和ADC2可分开使用,也可以一起使用
转换模式
单次转换非扫描模式
把想要转换的通道放在序列1,转换后数据存放在数据寄存器,通识EOC置1
连续转换非扫描模式
连续转换不需要判断结束,需要数据直接读取
单次转换扫描模式
设置通道数目,每次只转换通道数目对应数目的,全部完成之后EOC置1
连续转换扫描模式
连续扫描
触发控制
数据对齐
分为数据右对齐和数据左对齐,分别为左侧和右侧补0,右对齐比较常用
转换时间
AD转换的步骤:采样,保持,量化,编码
STM32 ADC的总转换时间为:
T_CONV = 采样时间 + 12.5个ADC周期
例如:当ADCCLK=14MHz,采样时间为1.5个ADC周期
T_CONV = 1.5 + 12.5 = 14个ADC周期 = 1μs
校准
ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差
建议在每次上电后执行一次校准
启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期
硬件电路
应用1.AD单通道
面包板接线图:
头文件 AD.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include "stm32f10x.h"
void AD_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
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_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_Init(ADC1,&ADC_InitStructure);
ADC_Cmd(ADC1,ENABLE);
ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1) == SET); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1) == SET);
ADC_SoftwareStartConvCmd(ADC1,ENABLE); }
uint16_t AD_GetValue(void) { return ADC_GetConversionValue(ADC1); }
|
上面程序配置的是连续模式,将init函数的最后一行移到getvalue函数的第一行并在后面加一行 while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET);
并将 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
改为DISABLE即可改成非连续模式。
主程序 main.c:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include "stm32f10x.h" #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:");
while(1) { ADVALUE = AD_GetValue(); Voltage = (float)ADVALUE/4095*3;
OLED_ShowNum(1,9,ADVALUE,4); OLED_ShowNum(2,9,Voltage,1); OLED_ShowNum(2,11,(uint16_t)(Voltage*100)%100,3); OLED_ShowChar(2,10,'.'); Delay_ms(100); } }
|
应用2.AD多通道
面包板接线图:
DMA
DMA(Direct Memory Access)直接存储器存取
DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源
12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)
每个通道都支持软件触发和特定的硬件触发
STM32F103C8T6 DMA资源:DMA1(7个通道)
存储器映像
ROM:
起始地址 |
存储器 |
用途 |
0x0800 0000 |
程序存储器Flash |
存储C语言编译后的程序代码 |
0x1FFF F000 |
系统存储器 |
存储BootLoader,用于串口下载 |
0x1FFF F800 |
选项字节 |
存储一些独立于程序代码的配置参数 |
DMA基本结构
DMA请求
这部分是上面DMA触发的内容
M2M是选择位,选择硬件触发还是软件触发,EN是开关控制
数据转运+DMA
ADC扫描模式+DMA
其他模式下DMA作用类似于锦上添花,但是对于ADC而言DMA非常重要,没有DMA会使ADC功能大大受限,所以ADC+DMA是非常常见经典的组合