STM32个人学习笔记分享:学习记录与心得分享(日期:XXXX年XX月XX日)
1.STM32学习记录
2.全文14w2669字
标签
#本人初学者#,#仅供学习交流,其他用途与本人无关#,#侵权必删#,#转载标明出处#,#STM32#,#个人笔记# ,#江协科技#
声明:
1.欢迎各位朋友转载,但是请注明文章出处,附上文章链接。
2.所有文章均为个人学习心得,部分学习视频过程中的截图如有侵犯到作者隐私,可联系我删除。
3.欢迎各位朋友多交流学习,本人对自己的文章进行不定期的修正与更新,因此请到我的博客首页查看某篇章的最新版本。
STM32学习记录
1.STM32简介
1.简介
STM : ST公司基于ARM Cortex-M 内核开发的微控制器
32 :32位
F :通用类型
系列:
101:基本型
102:usb基本型,usb 2.0全速设备
103:增强型
105/107:互联型
引脚数目:
T:36脚
C:48脚
R:64脚
V:100脚
Z:144脚
闪存存储器容量(字节)
4:16K
6:32K
8:64K
B:128K
C:256K
D:384K
E:512K
封装
H:BGA
T:LQFP
U:VFQFPN
Y:WLCSP64
温度范围:
6:-40°C ~ 85°C
7:-40°C ~ 105°C
2.外设
外设不一定有,具体看对应 封装手册
3.系统结构
-
Cortex-M3内核:
-
ICode:指令总线,加载程序指令
-
DCode:数据总线,加载数据,比如常量和调试参数等
-
System:系统总线,
-
Flash接口:连接Flash闪存,存放程序代码
-
SRAM:存放程序运行时的变量数据
-
AHB:先进高性能总线,挂载基本和高性能外设,如复位、时钟控制、SDIO等,频率72MHz,性能比APB高。
-
桥接:由于AHB和APB的总线协议、总线速度、数据传输格式等差异,试用桥接进行数据转换与缓存。
-
APB:(Advanced Peripheral Bus)先进外设总线,连接一般外设, 需要了解外设挂载总线。
-
APB1:频率36MHz,连接次要外设。如DAC、PWR、BKP等
-
APB2:频率72MHz(一般与AHB频率相同),性能比APB1高,连接外设中稍重要的部分。如:GPIO端口,外设UASRT1、SPI1、TIM1(高级定时器)、TIM8(高级定时器)、ADC、EXTI、AFIO等
-
DMA:拥有CPU总线一样的控制权,用于访问外设,访问并转运数据,节省CPU开销。
4.引脚定义
-
阅读说明:
-
颜色:
-
红色:电源相关引脚
-
蓝色:最小系统相关引脚
-
绿色:IO口、功能口引脚
-
-
表头:
-
引脚号与引脚名称:与芯片引脚一一对应
-
类型:
-
S:电源
-
I:输入
-
O:输出
-
IO:输入输出
-
I/O口电平:表示IO口能容忍的电压
-
默认:可以容忍3.3V电压
-
FT:可以容忍5V电平,需要加装电平转换电路
-
主功能:上电后默认的功能,一般与引脚名称相同
-
默认复用功能:IO口同时连接的外设功能引脚,配置IO口时可与主功能切换
-
重定义功能:两个功能同时复用在一个IO口上冲突,可以把其中一个复用功能,映射到其他端口上,需要看表才可以映射
-
加粗:加粗优先推荐使用,没有加粗可能需要配置或兼具其他功能
引脚功能
-
VBAT (1):备用电池供电引脚,可以接入3V电池,当系统电源断电时,备用电池可以给内部RTC时钟和备份寄存器提供电源。
-
PC13-TAMPER-RTC(2):IO口、侵入检测、RTC。
-
PC14-OSC32_IN/PC14-OSC32_OUT(3,4):IO口、接入32.768KHz的RTC晶振
-
OSC_IN/OSC_OUT(5,6):系统总晶振,一般8MHz,内部有锁相环电路,可以倍频,最终产生72MHz频率作为系统主时钟。
-
NRST(7):复位引脚,N代表低电平
-
VSSA/VDDA(8,9):内部模拟部分电源,如ADC、RC震荡器。
-
PA0(10):IO口、WKUP唤醒待机模式的STM32
-
PA1-PA7/PB0-PB1(11-19):IO口
-
PB2(20):IO口、BOOT1引脚配置启动模式
-
PB10/PB11(21,22):IO口
-
VSS_1/VDD_1(23,24):系统主电源口,分区供电
-
PB12-PB15/PA8-PA12(25-33):IO口
-
PA13-PA15/PB3-PB4(34,37-40):调试端口,调试程序、下载程序,支持两种调试方式
-
SWD(STLINK方式):需要两根线,SWDIO(PA13-34)和SWCLK(PA14-37)
使用SWD方式后,PA15、PB3、PB4(38-40)可以作为普通IO口使用,但需要配置
-
JTAG:需要五根线,JTMS、JTCK、JTDI、JTDO、NJTRST
VSS_2/VDD_2(35,36):系统主电源口,分区供电
PB5-PB7(41-43):IO口
BOOT0(44):启动配置
PB8-PB9(45-46):IO口
VSS_3/VDD_3(47,48):系统主电源口,分区共供电
提示:
IO口:根据程序输出或读取高低电平的端口
侵入检测:安全保障、防拆触点,引脚变化则检测到侵入信号,清空数据保证安全
RTC :输出RTC校准时钟、RTC闹钟脉冲、秒脉冲
正负极:VSS负极接GND,VDD正极接3.3V。
分区供电:供电口比较多,都接好即可,供电很多
网络标号:相同标号进行连接。
启动配置:指定程序开始运行位置。一般程序从Flash程序存储器开始运行;某些情况下可以在其他地方开始执行实现特殊功能
BOOT1 | BOOT0 | 说明 | 解释 |
---|---|---|---|
X(任意) | 0 | 主闪存存储器 | 正常执行flash闪存内的程序(常用) |
0 | 1(3.3V) | 系统存储器 | 串口下载,接收串口数据,刷新到主闪存中 |
1 | 1 | 内置SRAM | 程序调试 |
BOOT引脚,在上电复位的一瞬间(SYSCLK第4个上升沿)有效,之后随意(就是另一个功能)。
5.最小系统
自己画板子,可以参考最小系统。
使用面包板的话,自带最小系统。
-
晶振电路
-
主晶振:8MHz主时钟晶振,倍频后得到72MHz主频
-
起震电容:20pF,另一端接地。
-
RTC:如果需要RTC功能,还需要接个32.768KHz的晶振,电路相同,接在3、4号引脚
32.768K = 215 ,内部RTC电路经过215 分频,可以产生1s时间信号
-
-
复位电路
-
NRST:低电平复位,7号引脚
上电瞬间,电容充电,电平先低再高,低电平时候可以进行复位
也就是说,单片机上电一瞬间,就复位了
-
手动复位:按钮按下,电容放电,变成低电平触发复位,程序从头开始运行
设备上的小孔,用针戳一下就复位了。
-
-
启动配置
-
开关配置:使用两个跳线帽,充当开关,配置BOOT
当跳线帽连接:
1、3:BOOT0置为高电平
3、5:BOOT0置为低电平
2、4:BOOT1置为高电平
4、6:BOOT1置为低电平
也可以使用其他开关
-
-
下载端口
-
STLINK下载:引出SWDIO和SWCLK
-
电源:3.3V可以使用板子供电,但是建议引出
-
接地:必须
-
-
STM32及供电
2.Keil5 MDK
1.安装Keil5 MDK
-
可以切换路径,然后一路下一步
-
弹出命令行窗口,安装ULINK
-
付费软件,个人使用,可以使用注册机
2.安装软件支持包
-
开发哪种芯片,就安装对应的支持包
Project -> New μVision Project… -> 创建文件 -> Device -> Software Packs -> STMicroelectronics -> STM32F1 Series -> STM32F103 -> STM32F103C8 -> OK
-
安装方式:
-
离线安装:直接点击支持包,进行安装
-
在线安装(慢):在软件中点击Pack Installer (绿色按钮),等待更新获取(右下角、左下角)后, 有不同公司的支持包
Devices -> Device -> GigaDevice (公司) -> GD32F10x Series -> GD32F103 -> GD32F103C8
Devices -> Device -> MindMotion (公司) ->MM32F10x Series -> MM32F103x8 -> MM32F103C8T
(DFP文件)
Devices -> Device -> STMicroelectronics(公司) ->STM32F2 Series
Packs ->Pack -> Device Specific -> Keil:STM32F2xx_DFP -> install
-
STLINK驱动
-
插入STLINK
-
查看设备管理器,是否存在 STM32 STLink,会出现红色感叹号则需要安装
-
安装一直下一步
-
-
USB串口驱动
同上面步骤
3.新建工程
-
开发方式:
-
基于寄存器开发:类似于开发51单片机,程序直接配置寄存器,实现功能,最底层、最直接、效率高的方式,但是stm32太过复杂,所以不推荐。
-
基于库函数开发:使用ST官方封装好的函数,调用函数间接配置寄存器,有利于提高开放效率【课程使用】
-
基于HAL库开发:利用图形化界面快速配置STM32,隐藏底层逻辑,适用于快速上手,方便。
-
-
构建工程
-
新建一个项目文件夹,用于存放项目,方便管理
-
打开keil5,使用创建项目,选择刚刚存放项目的文件夹
Project -> New μVision Project… -> 选择刚才创建的文件 ->新建一个存放本项目的文件 -> 给工程起个名字 -> Device -> Software Packs -> STMicroelectronics -> STM32F1 Series -> STM32F103 -> STM32F103C8 -> OK
Manager Run-Time Environment 可以快速创建工程。暂时关掉,这里是手动创建工程。
-
在项目文件夹下,新建Start文件夹,存放启动文件。
目录:
.\固件库\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm
程序执行最基本的文件,用汇编写的。
定义了中断向量表、中断服务函数
中断服务函数中,有个复位中断,是整个程序的入口
调用SystemInit()
设置微控制器的启动
初始化嵌入式闪存接口、锁相环、更新系统内核的时钟变量
复位后调用
调用main()
-
-
复制系统文件(3个)到Start文件夹
目录:上二级目录中
stm32f10x.h #描述stm32寄存器中,有哪些寄存器和它对应的地址
system_stm32f10x.c / system_stm32f10x.c :配置时钟,主频72MHz也是从这里配置的
-
复制内核寄存器描述文件(2个)到Start文件夹
目录: .\固件库\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\CoreSupport
core_cm3.c / core_cm3.h :内核的寄存器描述与配置
-
在Keil中添加到工程
-
选择Source Group 1 重命名为 Start
-
添加已经存在的文件到Group,查看Start文件夹下的所有文件(All files)
-
选择一个启动文件:startup_stm32f10x_md.s
根据芯片型号选择启动文件
-
选择所有的.c和.h文件
-
添加到工程中,图标中显示小钥匙(只读,不允许修改)
-
点击魔术棒按钮,添加头文件路径,否则找不到文件
-
选择C/C++ -> Include Paths -> … -> 新建路径 -> … -> 添加Start路径 -> OK
-
-
打开工程文件夹,新建User文件夹,作为main函数入口
-
将User添加到工程中(类似操作6)
-
在Target 1 下面,新建Group ,改名为User
-
添加新文件,main.c文件,手动更改Location路径,将文件放到文件夹中
-
在main.c中,编辑
#include "stm32f10x.h" //这个是右键直接添加头文件
int main(void){
while(1){
}
}
//必须添加死循环
//最后一行,必须是空行,否则报错
-
-
可以对编译器字体等进行调节,然后就可以进行寄存器开发了
4.寄存器开发
-
STLINK与最小系统板连接
-
将STLINK插到电脑上,板子上面的电源LED灯会常亮,另一个LED灯会闪烁(测试程序)
-
配置调试器下载程序,点击魔术棒按钮,配置ST-Link Debugger
-
点击旁边的Settings按钮,Flash Download –> 勾选Reset and Run
下载程序后会立刻复位并执行,比较方便;否则下载程序后,需要手动复位才能执行程序
-
重新编译程序,检查是否有错误,没有错误后,点击DownLoad按钮,程序就可以下载到stm32了
此时没有编程,原本的测试程序被覆盖,led不再闪烁。 Q1:STM32板子插不进去怎么办?
A1:使用“跷跷板”的方法,先使一端插入,然后另一端再压进去,如此反复,慢慢就进去了。
-
此时可以进行 基于寄存器开发 了,如果想要 基于库函数开发 ,则需要添加库函数
5.库函数开发
-
在项目文件夹内,新建Library文件夹,用于存放库函数
-
找到库函数位置
目录: .\固件库\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver\src
misc.c :是内核的库函数,
其他的.c文件:是内核外的外设库函数
-
复制所有库函数.c文件,到Library
-
同时找到库函数头文件,也复制到Library文件夹
目录:上一级目录的另外一个文件夹
.\固件库\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver\inc
-
在编译器里面新建组,命名为Library,将所有文件(.c和.h)添加进来
-
找到库函数配置文件,和中断函数文件(3个),复制到User目录下(存放main.c的目录)
目录:
.\固件库\STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Template
stm32f10x_conf.h:用来配置库函数包含关系,和参数检查的函数定义
stm32f10x_it.c/stm32f10x_it.h:存放中断函数
-
在编译器里面,User组中添加三个文件。
-
编译器添加宏定义,魔术棒按钮 -> C/C++ -> Define ,粘贴字符串:USE_STDPERIPH_DRIVER
在main.c中
右键#include "stm32f10x.h" ,跳转到定义
在末尾找到宏定义代码
#ifdef USE_STDPERIPH_DRIVER #include "stm32f10x_conf.h" #endif
在编译器中添加宏定义 USE_STDPERIPH_DRIVER
-
完善其他配置:配置User、Library 包含路径
魔术棒按钮 -> C/C++ -> Include Paths
-
完成配置,选择三个箱子按钮,可以更改组的顺序,更美观;完成之后可以直接编译(第一次比较慢),没有错误和警告,说明配置成功,可以进行基于库函数的编程。
3.GPIO
1.GPIO 简述
-
GPIO:通用输入输出口
-
命名:GPIOx ,端口(16位)连接到内部包含驱动器和寄存器(32位),连接到APB2
寄存器低16位对应每个端口,高16位没有用到
寄存器负责存储数据。
驱动器增大驱动能力,也就是输出电平(点灯)。
-
GPIOA:PA0-PA15
-
GPIOB:PB0-PB15
-
GPIOC:PC0-PC15
-
-
8种工作模式:配置GPIO端口配置寄存器
端口配置寄存器:每个端口需要4位,16个端口就需要64位==》因此有两个端口配置高/低寄存器
-
引脚电平:0V-3.3V,部分引脚可容忍5V
-
GPIO输出速度:限制输出引脚最大翻转速度,降低功耗,提升稳定性,一般配置为50MHz
-
IO输入输出
-
输出模式:可以控制端口输出高低电平
-
用以驱动LED
-
控制蜂鸣器
-
模拟通信协议
-
输出时序等
-
-
输入模式:可以控制端口读取高低电平或电压
-
用于读取按键输入
-
外接模块电平信号输入
-
ADC电压采集
-
模拟通信协议接收数据等。
-
结构图
构分为两部分:上半部分是输入,下半部分是输出
可以有多个输入,但只能有一个输出;也就是说,输入模式下,输出无效,输出模式下,可以输入。
-
输入部分:从右部分的I/O引脚输入电平
-
信号输入:从右部分的I/O引脚输入信号
-
保护电路:电流不会流入内部电路
保护二极管:有钳制电压的作用,压降0.7V
-
当输入电压>3.3V:触发保护电路,VDD 会接通,电流从I/O引脚流入到VDD
+5V电压,超过3.3V(VDD) + 0.7V = 4V。因此只能通过+4V电压,剩余1V由输入源内阻消耗
-
当输入电压<0V:触发保护电路,VSS 会接通,电流从VSS流出到I/O引脚
-5V电压,低于0V(Vss) – 0.7V = -0.7V。因此只能通过-0.7V电压,输入源内阻承担4.3V压降。
-
-
输入模式:提供输入默认电平
-
上拉输入模式:上拉电阻开关闭合,下拉电阻开关断开,开关连接到VDD,电压被强制拉升到3.3V
-
下拉输入模式:下拉电阻开关闭合,上拉电阻开关断开,开关连接到VSS,电压被强制拉升到0V
-
浮空输入模式:下拉电阻开关与上拉电阻开关,都断开。引脚电平容易受到外接干扰而改变
上拉电阻和下拉电阻比较大,属于弱上拉和弱下拉,尽量不影响输入操作
弹簧模型:假设输出端是一个水平杆子,上拉电阻就是拴在屋顶的弹簧,将杆子上拉,下拉电阻就是拴在地面的弹簧,将杆子下拉。阻值越小,弹簧拉力越大,杆子高度就是电压,杆子居中则两个无穷大力在拉扯,电路上表现是短路,应当避免。
ps. 哪边电阻小,哪边就导通;导通后接地,就是下拉;导通后接电,就是上拉。
施密特触发器:对输入电压进行整形
如果输入电压,大于某一阈值,输出就会瞬间升高为高电平
如果输入电压,小于某一阈值,输出就会瞬间降低为低电平
图中写错为 肖特基触发器
两个比较阈值之间,有一定的变化范围,可以有效的避免,因信号波动造成的输出抖动现象,维持信号稳定。
模拟输入:连接到ADC上,接收模拟量,连接在施密特触发器前面,施密特触发器后面所有电路均无效。
复用功能输入:连接到其他需要读取端口的外设上,接收数字量,连接在施密特触发器后面
端口输入数据寄存器:
低16位对应16个引脚,高16位没有使用
输出部分:从右部分I/O引脚输出电平
-
输出数据寄存器:普通IO输出,操作位控制端口,设置好的输出,存放到这里
低16位对应16个引脚,高16位没有使用
-
片上外设输出:可以选择不使用“输出数据寄存器”,而采用片上外设进行输出控制
-
位设置/清除寄存器:单独操作某一位,不影响其他位
高16位进行位清除
低16位进行位设置
置为1才有效。
另一个寄存器“端口位清除寄存器”:使用低16位,进行位清除操作。都使用16位操作方便。
单独操作某一位:
-
可以通过运算实现:&= |=,但是比较麻烦
-
读写stm32位带区域:类似于位寻址,在某一段地址区域,映射了RAM和外设寄存器的所有位
-
位设置/清除寄存器(库函数):当某一位置为0时,在清除寄存器对应位置为1即可
-
-
输出模式
MOS管,是一种电子开关,使用信号控制开关的导通与关闭 输出模式:由输出数据寄存器控制
-
推挽输出模式(强推输出模式):P-MOS与N-MOS均有效,STM32对IO口有绝对控制权。
-
关闭输出模式:P-MOS与N-MOS均无效,输出关闭,端口电平由外部信号控制
-
开漏输出模式:数据寄存器为0,P-MOS断开,N-MOS导通,输出直接接到VSS ,也就是输出低电平。
开漏模式,可以作为
通信协议的驱动方式(如I2C通信)
多机通信情况下,可以避免各个设备的相互干扰
可以用于输出5V电平信号
N-MOS输出:
低电平:接到VSS 输出低电平
高电平:高阻态不导通,输出接一个5V上拉,输出5V
不需要看图中,高低电平导通MOS。
上拉电阻与下拉电阻
单片机引脚的输出电源 是 +5V
由于单片机内部电源有内阻
下拉电阻,串联分压。
上拉电阻,并联降低阻抗、
2.硬件电路
低电平驱动电路
LED正极接入3.3V,负极通过一个限流电阻,接到PA0
当PA0输出低电平时,LED两端产生电压差,形成正向导通电流,点亮LED
当PA0输出高电平时,LED两端都是3.3V,没有电压差,不会形成电流,所以LED熄灭
限流电阻:
防止LED因为电流过大而烧毁
可以调整LED亮度,电阻越大,LED越暗
【推荐接法:高电平弱驱动,低电平强驱动,一定程度可以避免高低电平打架】
高电平驱动电路
高电流点亮LED,低电平熄灭
蜂鸣器电路PNP
三极管开关:左边是基极,带箭头的是发射极,剩下的集电极
PNP三极管:基极给低电平,三极管导通
【设备接在集电极方向】
PNP接在设备之前,由于三极管内部通断,需要在发射极和基极直接产生一定的开启电压,接错了可能导致三极管无法开启
蜂鸣器电路NPN
NPN三极管:基极给高电平,三极管导通
3.GPIO输出
操作流程:
使用RCC开启GPIO时钟
使用GPIO_Init()函数初始化GPIO
使用输出或输入的函数,控制GPIO口
#include "stm32f10x.h" // Device header int main(void){ //使用RCC开启GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //初始化GPIOA_1端口,设置结构体 GPIO_InitTypeDef GPIO_InitStruct; //设置端口 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1; //设置推挽输出 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //设置跳变速度,一般为50MHz GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //使用GPIO_Init()函数初始化GPIOA GPIO_Init(GPIOA,&GPIO_InitStruct); //设置GPIOA_1端口,置为高电平 GPIO_SetBits(GPIOA,GPIO_Pin_1); //必要死循环 while(1){ } } //必要空行
RCC外设:常用AHB、APB1、APB2 外设控制函数
GPIO:读写操作
1.LED闪烁
-
LED:发光二极管,正向通电点亮,反向通电不亮。一般长脚为正极,短脚为负极
-
代码
#include "stm32f10x.h" // Device header #include "Delay.h" // 延时代码库,引入头文件(第三方) int main(void){ // RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); while(1){ //设置GPIOA的GPIO_Pin_1端口,为高电平 GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_SET); //延时0.1s Delay_ms(100); //设置GPIOA的GPIO_Pin_1端口,为低电平 GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_RESET); //延时0.1s Delay_ms(100); } }
操作GPIO端口方法很多:
GPIO_SetBits(GPIOA,GPIO_Pin_1); //设置GPIOA_1端口为高电平 GPIO_ResetBits(GPIOA,GPIO_Pin_1); //设置GPIOA_1端口为低电平 GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_SET); //设置GPIOA_1端口为高电平 (BitAction)1 GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_RESET); //设置GPIOA_1端口为低电平 (BitAction)0 GPIO_Write(GPIOA,0x0000); //同时设置16个位的高低电平 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD; //开漏输出->高电平没有驱动能力
高阻态:三极管没有导通的状态,类似于断路。
2.LED流水灯
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
//开启GPIO的16个端口,设置为推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_All;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
while(1){
//从GPIOA_Pin_1 开始亮,count代表当前亮灯的位。
uint16_t count = 1; //0000 0000 0000 0001 <<=1 0000 0000 0000 0010
//依次循环亮起4个流水灯
for(int i = 0;i<4;i++){
//设置16个端口值
GPIO_Write(GPIOA,count);
//左移运算,开启下一个灯
count <<= 1;
//延时0.2s
Delay_ms(200);
}
}
}
Q1:为什么有的灯不亮
A1:没插好,长脚插入正极,短脚插入负极
A2:和代码设置不同,此代码为推挽输出
A3:端口选择错误,代码中端口选择为PB1–>PB4。
3.蜂鸣器
-
有源蜂鸣器:内部自带震荡源,将正负极接上直流电压即可持续发声,频率固定
-
无源蜂鸣器:内部不带震荡源,需要控制器提供震荡脉冲才可以发声,调整提供震荡脉冲频率,可以发出不同声音。
#include "stm32f10x.h" // Device header #include "Delay.h" int main(void){ //使用GPIOB_PIN_0 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; //开漏输出,低电平有效 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStruct); while(1){ //低电平,发出声音 GPIO_ResetBits(GPIOB,GPIO_Pin_0); Delay_ms(500); //高电平,停止声音 GPIO_SetBits(GPIOB,GPIO_Pin_0); Delay_ms(100); } }
Q1:蜂鸣器声音小怎么办?
A1:声音小是无源蜂鸣器,使用有源蜂鸣器声音大。
A2:蜂鸣器上面有贴纸粘住,把贴纸撕下后声音变大。
4.GPIO 输入
输入设备:
按键:按键按下,电路导通,松手断开
按键抖动现象:机械弹簧抖动,产生不稳定信号,通常在5ms-10ms。
过滤抖动:加一段延时,把抖动时间耗尽
推荐使用上面的按键连接方式,按键按下为低电平。
传感器:传感器元件电阻随外界模拟量变化而变化,通过定值电阻分压,即可得到模拟电压输出,再通过电压比较器进行二值化,可得到数字电压输出。
常见传感器元件:光敏电阻、热敏电阻、红外接收管等。
二值化:利用运算放大器,进行电压比较,输出较大一方的,想要输出的结果。
同相输入端电压>反相输入端电压:输出最大值VCC
同相输入端电压<反相输入端电压:输出最小值GND
模拟电压输出AO:利用上拉电阻和下拉电阻分压,传感器阻值变化的时候,传递不同电压。
数字电压输出DO:利用电压比较,将模拟量AO二值化。
1.C语言定义简介
-
数据类型
C51的int是16位,STM32的int是32位
stdint关键字:利用typedef重新命名的关键字,数字代表位数。
ST关键字是老版本的命名,新版本仍然支持,只是为了兼容老版本,不建议使用。
【typedef 变量类型名 新名称】 只能替换类型,更安全。
-
宏定义:【#define 新名称 替换名称】,将任何字符替换到对应位置。
-
数组:多个同一类型变量。
-
结构体:多个不同类型变量。数据打包、伪面向对象。
结构体引用成员:struct {char name;} a; a.name / (&a)->name
利用typedef重新命名,更方便。
-
枚举enum:受限制的整形变量,不能使用未定义的变量。
比如星期、月份,都是固定的,不能写出非法数值。
-
指针:C语言值传递,所以传递一个“位置编号”,才可以做到操作同一个东西。
2.按键控制LED
-
使用模块化编程,管理方便、代码整洁。
-
新建Hardware文件夹,存放硬件驱动,配置好组、工程路径文件夹等。
-
在Hardware文件夹下,新建LED.c和LED.h,编写LED驱动
//LED.h文件 #ifndef __LED_H //防止重定义,如果没有定义这个宏,则进行定义 #define __LED_H //声明宏定义,保证唯一性,随便写,一般为下划线与大写字母结合 void LED_Init(void); //声明所有LED.c中的函数。 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 //结束重定义 //LED.c文件 #include "stm32f10x.h" // Device header void LED_Init(void){ //配置GPIOA_pin1和pin2端口,放置两个低电平有效的LED灯 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); //初始化led熄灭 GPIO_SetBits(GPIOA,GPIO_Pin_1|GPIO_Pin_2); } //置为高电平,LED1熄灭 void LED1_OFF(void){ GPIO_SetBits(GPIOA,GPIO_Pin_1); } //置为低电平,LED1点亮 void LED1_ON(void){ GPIO_ResetBits(GPIOA,GPIO_Pin_1); } //读取LED1当前端口电平,设置取反 void LED1_Turn(void){ //设置GPIO_Pin1端口, 读取GPIO_Pin1的电平,然后设置为相反的值 GPIO_WriteBit(GPIOA,GPIO_Pin_1,!GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_1)); } void LED2_OFF(void){ GPIO_SetBits(GPIOA,GPIO_Pin_2); } void LED2_ON(void){ GPIO_ResetBits(GPIOA,GPIO_Pin_2); } void LED2_Turn(void){ GPIO_WriteBit(GPIOA,GPIO_Pin_2,!GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_2)); }
-
在Hardware文件夹下,新建Key.c和Key.h,编写Key驱动
#ifndef __KEY_H #define __KEY_H void Key_Init(void); uint8_t Key_GetNum(void); #endif #include "stm32f10x.h" // Device header #include "Delay.h" //按键消除抖动,需要延迟函数 void Key_Init(void){ //开启GPIOB_Pin1和pin11端口,用来检测按键输入 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); GPIO_InitTypeDef GPIO_InitStruct; //读取按键,上拉输入,按下低电平,松手高电平 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_11; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStruct); } //返回按下的按键键码 uint8_t Key_GetNum(void){ uint8_t KeyNum = 0; //读取GPIOB的Pin1端口,如果是低电平,进入if 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函数,实现按钮按下,点灯操作
#include "stm32f10x.h" // Device header #include "Delay.h" #include "LED.h" //引入头文件 #include "Key.h" //引入头文件 //全局变量,获取键码,和Key.h的KeyNum毫无关系。 uint8_t KeyNum; int main(void){ //初始化led LED_Init(); //初始化key Key_Init(); while(1){ //不断获取按键键码 KeyNum = Key_GetNum(); //键码为1,则LED1取反【亮变灭,灭变亮】 if(KeyNum == 1){ LED1_Turn(); } if(KeyNum == 2){ LED2_Turn(); } } }
Q1:按钮没反应?
A1_1:确保代码和端口配置正确。
A1_2:是不是按得太快了?慢一点按。
Q2:为什么按键用上拉输入模式?
A2_1:如果模块始终接在端口上,也可以选择浮空输入,要保证引脚不会悬空。
A2_2:确保按键在未按下的时候,IO引脚可以安全的读到高电平
A2_3:上拉下拉都可以,参考上面按键电路。
3.光敏传感器控制蜂鸣器
-
在Hardware文件夹下,新建Buzzer.c和Buzzer.h,编写Buzzer驱动,配置存放好路径
//Buzzer.h文件 #ifndef __BUZZER_H #define __BUZZER_H void Buzzer_Init(void); void Buzzer_OFF(void); void Buzzer_ON(void); void Buzzer_Turn(void); #endif //Buzzer.c文件 #include "stm32f10x.h" // Device header void Buzzer_Init(void){ //初始化GPIOB_12端口 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStruct); GPIO_SetBits(GPIOB,GPIO_Pin_12); } void Buzzer_OFF(void){ GPIO_SetBits(GPIOB,GPIO_Pin_12); } void Buzzer_ON(void){ GPIO_ResetBits(GPIOB,GPIO_Pin_12); } //输出为低电平,蜂鸣器报警 void Buzzer_Turn(void){ if(GPIO_ReadOutputDataBit(GPIOB,GPIO_Pin_12) == 0){ Buzzer_ON(); }else{ Buzzer_OFF(); } }
-
在Hardware文件夹下,新建LightSensor.c和LightSensor.h,编写LightSensor驱动,配置存放好路径
//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); //配置GPIOB_0端口为输入端口,读取光敏传感器数字信号 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStruct); } //读取GPIOB_0端口,返回数字信号==》是否被遮挡 uint8_t LightSensor_Get(void){ return GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0); }
-
编写main函数
#include "stm32f10x.h" // Device header #include "Delay.h" #include "LED.h" #include "Buzzer.h" #include "LightSensor.h" uint8_t KeyNum; int main(void){ //使用led进行测试,在GPIOB_13端口上测试失败,因此转到GPIOB_0端口。 Buzzer_Init(); //初始化蜂鸣器 LightSensor_Init(); //初始化光敏传感器 //LED_Init(); while(1){ //获取光敏传感器是否被遮挡? if(LightSensor_Get()==1){ //被遮挡则蜂鸣器报警 Buzzer_ON(); //LED1_ON(); }else{ Buzzer_OFF(); //LED1_OFF(); } } }
5.OLED显示屏
作为调试工具使用
1.调试方式
缩小范围、控制变量、对比测试等
-
串口调试:通过串口通信,将调试信息发送到电脑端,电脑使用串口助手现实调试信息
可以显示各种数据流、图像等,但是只能一行行显示,而且需要电脑。
-
显示屏调试:显示屏直接连接到单片机,将调试信息打印在显示屏上
屏幕太小,显示内容有限,功能有限
-
keil调试模式:keil软件的调试模式,可以使用单步运行、设置断点、查看寄存器及变量等功能。
-
点灯调试:在代码中穿插点灯代码,进行调试。
-
注释调试:合理利用注释,注释程序寻找问题所在,缩小范围。
-
对照调试:找到没有问题的程序,对照自己的程序,替换它的代码,缩小范围。
2.OLED简介
-
OLED:有机发光二极管
-
OLED显示屏:性能优异、功耗低(每个像素都是单独的发光二极管)、响应速度快(高刷新率、总线时序快、避免阻塞)、宽视角(自发光、任何角度都清晰)、轻薄柔韧等
-
0.96寸OLED模块:小巧、占用接口少、简单易用、常见的显示屏模块
-
供电:3~5.5V,最好使用电源供电,而不是端口供电
-
通信协议:I2C(一般4针脚使用)/SPI(一般7针脚使用)
-
分辨率:128*64
-
OLED驱动函数(第三方)
将 .\程序源码\STM32Project\1-4 OLED驱动函数模块\4针脚I2C版本 目录下的三个文件复制到Hardware文件夹中
在keil中,添加这三个文件到工程里
了解函数功能
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" int main(void){ OLED_Init(); OLED_ShowChar(1,1,'A'); //在1行1列,显示字符'A' OLED_ShowString(1,3,"HelloWorld!"); //在1行3列,显示字符串 OLED_ShowNum(2,1,1234,5); //在2行1列,显示数字1234,数字长度为5,不足前面补0,超过则删除高位 OLED_ShowSignedNum(2,7,-66,2); //在2行7列。显示有符号数字-66,数字长度为2 OLED_ShowHexNum(3,1,0xAA55,4); //在3行1列,显示16进制数0xAA55,数字长度为4 OLED_ShowBinNum(4,1,0xAA55,16); //在4行1列,显示2进制数0xAA55,数字长度为16 //OLED_Clear(); //清除屏幕显示 while(1){ } }
3.keil调试
-
选择调试方式:硬件调试【√】/模拟仿真
-
模拟仿真
-
准备工作:连接好电路,确保代码编译没有错误
-
进入调试模式
-
窗口
Registers:寄存器窗口
Disassembly:代码的汇编显示,与代码区一一对应
-
按钮
黄色箭头:下一行将要执行的代码,程序从main函数开始
设置断点:代码区,左部分深灰色的区域,单机出现红点,即是设置断点。
Reset:复位,程序回到最开始位置
Run:全速运行,遇到断点停下
Stop:停止全速运行
Step:单步运行,一行行执行代码,遇到函数跳入
Step Over:跳过当前行单步运行
Setp Out:跳出当前函数单步运行,运行当前函数结束后
Run to Cursor Line :跳到光标指定行单步运行
Command Window:命令窗口
Disassembly Window:反汇编窗口
Symbols Window:符号窗口,实时查看程序中所有变量的值,选择变量放到Watch窗口,可以实时查看值变化。
-
查看外设寄存器:菜单栏 -> Peripherals -> System Viewer -> 所有外设寄存器,可以实时显示数据变化。
-
修改代码:需要退出调试模式才可以修改,然后重新编译后,再进入调试模式
-
Help查看官方文档
4.中断系统
1.中断简介
-
中断:在主程序运行过程中,出现了特定的触发条件(中断源),使得CPU暂停当前正常执行的程序,转而去处理中断程序,处理完成后返回到被暂停的位置,继续运行。
-
中断优先级:多个中断源同时申请中断的时候,CPU根据优先级高的先响应处理。
中断嵌套:处理中断的时候,有优先级更高的中断源申请中断,那么CPU会暂停当前中断程序,去处理新的中断,处理完成后返回
-
STM32中断:
-
68个可屏蔽中断通道(中断源),包含EXTI外部中断、TIM定时器、ADC模数转换器、USART串口、SPI通信、I2C通信、RTC实时时钟等多个外设
-
使用NVIC统一管理中断,每个中断通道都可以拥有16个可编程的优先等级,可以对等级进行分组,进一步设置抢占优先级和响应优先级。
-
2.STM32中断
-
中断表
-
深颜色的是内核中断,一般用不到
-
其他部分,是STM32外设中断
-
中断地址列表,就是中断向量表,是中断跳转的一个跳板。
-
-
NVIC基本结构
-
一个外设,可能会同时占据n个通道,所以有n条线
-
NVIC只有一个输出口,根据中断优先级分配中断先后顺序
-
CPU处理中断,而不需要知道中断顺序
-
NVIC优先级:每个中断有16个优先级(中断优先级寄存器:4位)
5种分组方式
-
抢占优先级:高n位
中断嵌套,中断当前中断,优先执行紧急中断
-
响应优先级:低4-n位
紧急中断,插队到前面,等待前面的任务完成后,优先执行紧急中断
-
EXTI:外部中断
-
可以监测指定GPIO口的电平信号,引脚电平变化,申请中断
-
支持触发方式
-
上升沿:低电平变到高电平,触发中断
-
下降沿:高电平变到低电平,触发中断
-
双边沿:上升沿和下降沿都可以触发中断
-
软件触发:程序执行代码触发
支持GPIO口:所有GPIO口,但相同的Pin不能同时触发中断
也就是说,PA1和PB1不能同时使用,因为都是 Pin1的端口。
占用通道:16个GPIO_Pin,以及PVD输出、RTC闹钟、USB唤醒、以太网唤醒
共有20个中断线路
后面四个,是来外部中断蹭网的:由于外部中断可以从低功耗模式的停止模式下,唤醒STM32。
对于PVD电源电压监测:电源从电压过低恢复时,需要PVD借助外部中断退出停止模式
对于RTC闹钟:为了省电,RTC定了闹钟之后,STM32会进入停止模式,等到闹钟响了会再唤醒,需要借助外部中断。
USB唤醒、以太网唤醒,都是类似的作用。
触发响应方式:中断响应/事件响应
中断响应:申请中断,让CPU执行中断函数
事件响应:不触发外部中断,通向其他外设,触发其他外设操作。属于外设之间的联合工作。
EXTI基本结构
每个GPIO的外设,都有16个引脚
由于EXTI模块,只有16个GPIO的通道,因此使用AFIO中断引脚选择模块(数据选择器)
不同GPIO外设,相同Pin端口不能同时触发EXTI,只能三选一。
对于4个蹭网外设,直接连接到EXTI
经过EXTI中断后,分为两种输出
输出到NVIC触发中断:
EXTI9~EXTI5,分给一个通道
EXTI15~EXTI0,分给一个通道
输出到其他外设响应事件:
20条线路分给其他外设
2.对射式红外传感器计次
打开RCC所有外设的时钟:APB2的GPIOB、AFIO,外部中断EXTI不需要开启时钟,NVIC是内核外设无需手动开启
设置端口为输入模式
选择GPIO端口,通过AFIO引脚选择,连接到EXTI
配置EXTI,选择触发方式【上升沿、下降沿……】、触发响应方式【中断响应、事件响应】
配置NVIC,给中断设置合适的优先级,通过NVIC,外部中断信号就可以进入CPU
-
在Hardware文件夹下,建立CountSensor.c和CountSensor.h,存放传感器触发代码,注意文件存放位置
#ifndef __COUNTSENSOR_H #define __COUNTSENSOR_H void CountSensor_Init(void); uint16_t CountSensor_Get(void); #endif
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line); //获取中断标志位【主程序中用】 void EXTI_ClearFlag(uint32_t EXTI_Line); //清除中断标志位【主程序中用】 ITStatus EXTI_GetITStatus(uint32_t EXTI_Line); //获取中断标志位【中断程序中用】 void EXTI_ClearITPendingBit(uint32_t EXTI_Line); //清除中断标志位【中断程序中用】
针对不同场景,区分函数
#include "stm32f10x.h" // Device header //记录中断触发次数 uint16_t CountSensor_count; void CountSensor_Init(void){ //开启时钟:GPIOB RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启时钟:AFIO RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //初始化GPIOB_0号引脚,设置输入模式,上拉输入 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(GPIOB,&GPIO_InitStructure); //配置AFIO,虽然配置的是GPIO的函数,实际上是AFIO,选择外部中断线 //GPIOB端口的,0号中断源。 GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0); //配置EXTI,初始化 EXTI_InitTypeDef EXTI_InitStructure; //配置EXTI中断线 EXTI_InitStructure.EXTI_Line = EXTI_Line0; //允许中断线触发 EXTI_InitStructure.EXTI_LineCmd = ENABLE; //配置中断触发/事件触发 EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //下降沿触发 EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //初始化EXTI EXTI_Init(&EXTI_InitStructure); //设置中断分组,组号2(共4位:2位抢占,2位响应),整个工程执行一次就行,可以放到main函数 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //初始化NVIC NVIC_InitTypeDef NVIC_InitStruct; //指定中断通道,这个通道是从EXTI连接到NVIC的线,选择【对应芯片型号】的中断通道。 NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn; //指定通道使能 NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; //制定抢占优先级:每个分组不同,取值范围也不同,分组2,取值【0-3】 NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; //指定响应优先级:每个分组不同,取值范围也不同,分组2,取值【0-3】 NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; //初始化NVIC NVIC_Init(&NVIC_InitStruct); } //返回触发计次 uint16_t CountSensor_Get(void){ return CountSensor_count; } //中断函数,EXTI0触发,名字不能写错,固定写法,不需要声明,自动触发 void EXTI0_IRQHandler(){ //判断中断标志位,确保是我们想要的中断源触发的中断,如果是多通道,则必写 if(EXTI_GetITStatus(EXTI_Line0)==SET){ //执行中断程序:计数 CountSensor_count++; //清除中断标志位,否则一直响应中断。 EXTI_ClearITPendingBit(EXTI_Line0); } }
#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); } }
Q1:为什么触发一次,变化值很大?
A1:可以使用按键消除抖动的思路。
3.旋转编码器计次
旋转编码器:用来测量位置、速度、旋转方向的装置。
读取:
旋转轴旋转时,输出 与旋转速度和方向对应的方波信号
读取方波信号频率和相位信息,可得知旋转轴速度和方向
类型:
机械触点式:利用金属触点进行通断,左右两部分触点,中间有个按键,编码盘经过设计,旋转会产生90°相位差(正交波形),可以检测方向。
触点式,适合调节音量等功能,接触式不适合电机测速等高速旋转的地方。
霍尔传感式:中心有磁铁,边上有霍尔传感器,磁铁旋转输出正交方波信号
光栅式:遮挡透过,捕获变化边沿,无法测方向
独立编码器元件:输入轴转动,输出波形
模块引脚
VCC:3.3V
GND:接地
A:A向输出,接到一个引脚
C:GND,暂时不用
B:B向输出,接到另一个引脚
A 和 B 的Pin编号不要相同
-
在Hardware中,新建Encoder.c和Encoder.h文件,编写旋转编码器代码,注意文件存放位置
#ifndef __ENCODER_H #define __ENCODER_H void Encoder_Init(void); int16_t Encoder_Get(void); #endif #include "stm32f10x.h" // Device header int16_t Encoder_Count; void Encoder_Init(void){ //开启时钟:GPIOB,AFIO RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //初始化两个端口:GPIOB_0和GPIOB_1 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); //配置AFIO的两条选择外部中断线,Pin不能重复(AFIO引脚选择) GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1); //初始化外部中断,配置两条EXTI中断线:EXTI_Line0|EXTI_Line1(AFIO连接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_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC中断通道(EXTI连接NVIC) NVIC_InitTypeDef NVIC_InitStruct0; NVIC_InitStruct0.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStruct0.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStruct0.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStruct0.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStruct0); NVIC_InitTypeDef NVIC_InitStruct1; NVIC_InitStruct1.NVIC_IRQChannel = EXTI1_IRQn; NVIC_InitStruct1.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStruct1.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStruct1.NVIC_IRQChannelSubPriority = 2; NVIC_Init(&NVIC_InitStruct1); } //每次获取变化量 int16_t Encoder_Get(void){ int16_t tmp; tmp = Encoder_Count; Encoder_Count = 0; return tmp; } //外部中断EXTI0 void EXTI0_IRQHandler(void){ //获取中断线标志,如果选择线使能,就进入中断 if(EXTI_GetITStatus(EXTI_Line0)==SET){ //判断另一个引脚,没有触发的时候 if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0){ Encoder_Count++; } //清除标志位 EXTI_ClearITPendingBit(EXTI_Line0); } } //外部中断EXTI1 void EXTI1_IRQHandler(){ if(EXTI_GetITStatus(EXTI_Line1)==SET){ if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0)==0){ Encoder_Count--; } EXTI_ClearITPendingBit(EXTI_Line1); } }
-
中断中,不要执行耗时过长的代码,比如Delay,主程序会严重阻塞
-
中断函数和主函数中,不可以操作同一个硬件、不可以调用同一个函数。可以使用变量传递数据,减少代码耦合。
-
5.TIM定时器
【默认情况下,所有定时器,内部基准时钟都是72MHz】
1.定时器基础介绍
-
定时器:可以对输入的时钟进行计数,在计数达到设定值时,触发中断
STM32中,定时器基准时钟一般是主频72MHz
也就是说,对72MHz计72个数(72M/72=1M,1/1M=1us),就是1MHz=1us
=> 72 / 72M = 1us
-
【16位计数器、与分频器、自动重装寄存器】的时基单元,在72MHz计数时钟下,最大可以实现59.65s的定时
(65536 / 72M) * 65536 ≈ 59.65s
65536个数,在72MHz频率下,最大每 (65536 / 72M) 秒触发一次中断,而最多可以触发65536个数,因此最大定时时间是(65536 / 72M) * 65536 ≈ 59.65s
定时器级联:添加一个定时器,把前一个定时器的输出作为输入,最大定时时间扩大655362 倍。
时基单元:由 16位计数器、与分频器、自动重装寄存器 三部分组成,都是16位。
-
定时器功能:
-
定时中断
-
内外时钟源选择
-
输入捕获
-
输出比较
-
编码器接口
-
主从触发模式等
-
定时器分类:根据复杂度和应用场景
-
高级定时器(APB2):TIM1、TIM8
-
通用定时器(APB1):TIM2、TIM3、TIM4、TIM5
-
基本定时器(APB1):TIM6、TIM7
-
定时器结构
-
基本定时器
-
时基单元:由 16位计数器、与分频器、自动重装寄存器 三部分组成,都是16位
-
基本定时器只能选择内部时钟,因此可以理解为:CK_PSC这根线,直接与内部时钟CK_INT连接。频率一般是主频72MHz。
-
PSC预分频器:实际分配系数 = 预分配器的值 + 1
预分频器寄存器:16位,包含65536个数,因此可以写 0 — 65535,最大65536分频
值为0 –> 不分频/1分配 输出频率 = 输入频率 = 72MHz
值为1–> 2分频 输出频率 = 输入频率 / 2 = 36MHz
值为2–> 3分频 以此类推
-
CNT计数器:检测预分配器分频后的时钟,检测到上升沿,计数器+1
16位,取值范围:0–65535,超过范围就回到 0
当计数器自增到目标值的时候,触发中断
向上计数:从0加到目标值后产生中断
-
自动重装寄存器:写入的计数目标
当计数器值 == 自动重装值,就是计时时间到,触发中断
-
UI向上箭头:产生中断信号
当计数器值 == 自动重装值,触发的中断,叫做“更新中断”
触发更新中断之后,就会通往NVIC,配置好NVIC的定时器通道后,CPU就可以响应更新中断了。
-
U向下箭头:产生一个事件
当计数器值 == 自动重装值,触发的事件,叫做“更新事件”
更新事件不会触发中断,但是可以触发内部其他电路工作。
-
主从触发模式:内部硬件不受程序的控制下实现自动运行。
可以极大减轻CPU负担。
主模式:把事件映射到TRGO引脚
将更新事件,映射到TRGO触发输出的位置,TRGO直接接到DAC触发转换引脚上。
定时器更新就可以不需要中断触发DAC转换,过程中不需要软件参与,实现硬件自动化。
TRGO可以通向其他定时器,接入其他定时器ITR引脚
-
-
通用定时器
-
时基单元:
计数方式:【通用定时器、高级定时器】
向上计数:从0 加到 目标值 后,产生中断
向下计数:从目标值 减到 0 后,产生中断
中央对齐:从0 加到 目标值 后,产生中断;然后从目标值 减到 0 后,产生中断;
时钟选择:
内部时钟:RCC,一般为72MHz
外部时钟:
TIMx_ETR引脚的外部时钟
-
指定引脚接入外部方波时钟
-
然后配置内部极性选择、边缘检测、预分频器电路
-
输入滤波电路,对输入的波形进行滤波整形
滤波使用:固定频率下采样,采样值都相同,代表信号稳定,就可以输出采样值,进行信号消除抖动。
-
滤波后的信号兵分两路:对于时钟输入是等价的
-
进入ETRF触发控制器,可以选择作为时基单元的时钟
外部时钟模式2:
可以对ETR时钟计数,把定时器当做计数器来使用
-
占用TRGI触发输入通道:当做外部时钟输入
外部时钟模式1:
占用TRGI触发输入通道
课程学习:通用计算器
不同单片机,拥有定时器资源不同,不要操作不存在的外设
ITR信号:信号来自其他定时器,外部时钟模式1
接收其他定时器的TRGO输出
TIM2的ITR0,接在TIM1的TRGO上
TIM2的ITR1,接在TIM8的TRGO上
以此类推
可以实现定时器级联功能:
比如:
初始化TIM3,使用主模式把更新事件映射到TRGO上
再初始化TIM2,选择ITR2,对应TIM3的TRGO
选择时钟为外部时钟模式1,TIM3的更新事件可以驱动TIM2的时基单元,实现定时器级联
TI1F_ED:连接输入捕获单元CH1引脚,从CH1引脚的边沿获取时钟,外部时钟模式1
ED:边沿触发,上升沿和下降沿均有效
TI1FP1和TI1FP2获取时钟:外部时钟模式1和编码器模式
TI1FP1:连接CH1引脚的时钟
TI1FP2:连接CH2引脚的时钟
高级定时器
上半部分和通用定时器相同,下半部分有差异
重复次数计数器:实现每隔几个计数周期,发生一次更新事件和中断
对输出信号又进行一次分频,定时时间扩大65536倍
DTG死区生成电路:生成一定时长的死区,防止在开关切换瞬间出现短暂的直通现象。
延时一段时间,让桥臂的上下管全部关断,防止直通现象
互补输出:
可以输出一对互补的PWM波,可以驱动三相无刷电机(前三路)
第四路仍是一个输出,无变化,因为驱动三相无刷电机,只需要三路即可
刹车输入:给电机驱动提供安全保证
当外部引脚BKIN产生刹车信号,或者内部时钟产生故障,控制电路会自动切断电机的输出,防止意外
时基单元:定时器核心功能
预分频器PSC
计数器CNT
自动重装器ARR
运行控制:控制寄存器,操作时基单元的运行
启动停止、向上向下计数等
时钟源:为时基单元提供时钟
内部时钟RCC:默认时钟,默认频率为72MHz
ETR外部时钟(外部时钟模式2)
触发输入(外部时钟模式1):ETR外部时钟、ITRx其他定时器、TIx输入捕获通道
编码器模式:编码器独用的模式,普通时钟用不到
重复计数器:高级定时器专用,在时基单元与中断输出控制之间
状态寄存器:产生中断信号,会在状态寄存器里面设置一个中断标志位
比如:
更新中断UI
触发信号TGI
输入捕获与输出比较 CCxI
中断输出控制:状态寄存器的中断标志,经过中断输出控制,到NVIC申请中断
中断输出控制:是中断输出的允许位
2.时序问题
-
预分频器时序
【预分频器参数,从1变成2的过程】
-
CK_PSC:预分频器的输入时钟,内部时钟一般为72MHz
-
CNT_EN:计数器使能,高电平时,计数器正常运行,低电平时,计时器停止
-
CK_CNT:计数器时钟,预分频器的时钟输出,也是计数器的1时钟输入
-
CNT_EN 计数器未使能,计数器时钟不运行
-
CNT_EN 计数器使能,计数器时钟运行
-
预分配系数 = 1(前半段):计数器的时钟 = 预分配器前的时钟
-
预分配系数 = 2(后半段):计数器的时钟 = 预分配器后时钟的一半
-
-
计数器寄存器:跟随时钟的上升沿,不断自增,计数器与重装值相等,并且在下一个时钟来临时,计数器清零,同时产生一个更新事件
-
更新事件UEV:计数器与重装值相等,并且在下一个时钟来临时,计数器清零,同时产生一个更新事件
-
预分频控制寄存器:用户读写使用的寄存器,并不直接决定分频系数
用户更改分频系数不会立刻生效,等到本次计数周期结束时,产生了更新事件,预分频控制寄存器的值,会被传递到缓冲寄存器里,才会生效。
-
缓冲寄存器(影子寄存器):决定分频系数,图中带阴影的方块,都有影子寄存器
预分频器、自动重装寄存器、捕获比较寄存器等
可以设置是否使用:不使用的情况下,立刻更新;使用的情况下,需要等到本次周期结束后更新。
-
预分频计数器:预分频器内部靠计数来分频
当预分频值 = 0,预分频计数器 = 0,直接输出原频率
当预分频值 = 1,预分频计数器 = 0和1,每次回到0的时候,输出一个脉冲,就是二分频
-
公式:计数器计数频率 CK_CNT = CK_PSC ÷ (PSC + 1)
计数器计数频率,取决于预分频器的分频,预分频器频率经过(PSC+1)分频,就是计数器计数频率
PSC:预分频器的值,当PSC = 1 的时候,就是二分频
计数器分频
【计数器内部时钟,分频系数 = 2 ( = 分频值 + 1),也就是二分频】
CK_INT:内部时钟,默认72MHz
CNT_EN:计数器使能,高电平起点
CK_CNT:计数器时钟,分频系数 = 2(二分频),频率就是CK_INT内部时钟 / 2,
计数器寄存器:时钟在上升沿自增,图中ARR(重装值)=0036,再经过一个上升沿后,寄存器清零,发生计数器溢出,产生一个更新事件脉冲,设置一个更新中断标志位UIF
计数器溢出:计数器寄存器到达重装值,再经过一个上升沿后,寄存器清零,发生计数器溢出,产生一个更新事件脉冲,设置一个更新中断标志位UIF
更新事件脉冲UEV:计数器溢出,产生一个更新事件脉冲,设置一个更新中断标志位UIF
更新中断标志位UIF:标志位置为1,就可以去申请中断。
响应后的中断,需要手动清零
计数器溢出频率:倒数就是【计时时间】。
CK_CNT_OV = CK_CNT÷ (ARR + 1)
= CK_PSC ÷ (PSC + 1) ÷ (ARR + 1)
CK_CNT_OV = CK_CNT÷ (ARR + 1)
CK_CNT:计数器频率,计数器最多可以记录多少个数
(ARR + 1):计数器分频系数,想要记录多少个数。
想要记录30个数,就是0 – 29 ,设置的时候,告诉它记录到ARR=29,就是进行了30次记录。
CK_CNT_OV :计数器溢出频率,多少次记录
想要记录30个数,运算后得出的值,就是在1s中,有多少次记录,其中每次记录有30个数。
结果倒数:达成一次记录,耗费的时间,也就是定时时间。
CK_CNT_OV = CK_PSC ÷ (PSC + 1) ÷ (ARR + 1)
CK_PSC:预分频器时钟频率,一共可以记录的数
(PSC + 1) :预分频器分频系数,进行(PSC + 1) 分频,每次记录(PSC + 1)个数。
设置PSC = 2,计数器每次记录0、1、2,就是3分频,当计数器=PSC=2的时候,就说明已经记录了3个数,进行溢出清零。
CK_PSC ÷ (PSC + 1) :每次记录(PSC + 1) 的情况下,可以有多少次记录
RCC时钟树
以AHB划分,AHB左部分,都是时钟产生电路,右边是时钟分配电路
震荡源:产生时钟
内部8MHz,高速RC震荡器
外部4-16MHz,高速石英晶体震荡器
也就是晶振,一般8MHz,比内部的稳定
外部32.768KHz,低速晶振
一般给RTC提供时钟
内部40KHz,低速RC振荡器
可以给看门狗提供时钟
两个高速晶振,都可以给系统提供时钟
可以给AHB、APB2、APB1等提供时钟,一般外部晶振更稳定,但是简单的系统要求不高,可以使用内部RC振荡器,省略外部晶振电路。
ST配置时钟:从SystemInit()函数开始
首先启动内部时钟,选择内部8MHz为系统时钟,暂时以内部时钟运行
8MHz HSI RC 经过HSI,进入AHB
然后启动外部时钟,把系统时钟由8MHz切换为72MHz
OSC_OUT与OSC_IN 的4-16MHz HSE OSC 经过PLLXTPRE–>PLLSRC –> 进入PLLMUL锁相环进行倍频(8MHz倍频9倍 = 72MHz),锁相环输出稳定后,经过PLLCLK进入AHB
【外部时钟出问题,可能会导致系统慢近10倍】
CSS:时钟安全系统,负责切换时钟,监测外部时钟运行状态,一旦外部时钟失效,自动把外部时钟切换为内部时钟。
高级定时器,刹车输入部分,有CSS检测外部时钟
系统时钟,进入AHB后,就是进入了时钟分配电路
AHB总线:系统时钟进入AHB总线的预分频器,在SystemInit()中配置分频系数
72MHz的时钟,进入AHB总线,在AHB总线的预分频器中,分频系数 = 1,那么AHB的时钟就是72MHz
APB1总线:AHB的时钟,进入APB1的预分频器,频率变为36MHz
AHB的时钟就是72MHz,进入APB1总线,配置分频系数=2,APB1总线时钟 = 72MHz / 2 = 36MHz
TIM2-TIM7时钟:所有定时器时钟,都是72MHz
APB1总线时钟频率是36MHz,单独开一条支路,进行倍频,频率回到72MHz
规则:
APB1的预分频系数 = 1,则频率不变
APB1的预分频系数 ≠ 1,频率 * 2
不改变SystemInit()默认配置的前提下,【所有定时器,内部基准时钟都是72MHz】
支路中,有与门:是外部时钟使能控制RCC_APB1PeriphClockCmd
APB2总线:频率72MHz,分频系数 = 1
TIM和TIM8时钟:所有定时器时钟,都是72MHz
APB2总线给定时器,单独开一条支路,进行倍频,APB2分频系数 = 1,频率不变
规则:
APB2的预分频系数 = 1,则频率不变
APB2的预分频系数 ≠ 1,频率 * 2
不改变SystemInit()默认配置的前提下,【所有定时器,内部基准时钟都是72MHz】
支路中,有与门:是外部时钟使能控制RCC_APB2PeriphClockCmd
3.定时器定时中断与定时器外部时钟
1.定时器定时中断
RCC开启时钟,定时器基准时钟和整个外设的工作时钟都会同时打开
选择时基单元的时钟源,对于定时中断,选择内部时钟源
void TIM_InternalClockConfig(TIM_TypeDef* TIMx); //选择RCC内部时钟源(要配置的定时器) void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource); //选择ITRx其他定时器的时钟(要配置的定时器,选择要接入的定时器) //定时器级联 void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,uint16_t TIM_ICPolarity, uint16_t ICFilter); //TIx捕获通道的时钟(要配置的定时器,选择TIx具体某个引脚,时钟的极性,时钟的滤波器) void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,uint16_t ExtTRGFilter); //ETR通过外部时钟模式1输入的时钟(要配置的定时器,外部时钟分频,时钟的极性,时钟的滤波器) void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter); //ETR通过外部时钟模式2输入的时钟,参数相同。对于ETR输入的外部时钟,如果不需要触发输入的功能,则两个函数等效 void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,uint16_t ExtTRGFilter); //单独配置ETR引脚的预分频器、极性、滤波器参数配置时基单元,一个结构体就可以配置好
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct); //配置时基单元(要配置的定时器,初始化结构体)
配置输出中断控制,允许更新中断输出到NVIC
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState); //配置输出中断控制(要配置的定时器,配置的中断输出,使能)
配置NVIC,在NVIC中打开指定定时器中断通道,分配好优先级
运行控制,配置好模块后,需要使能一下计数器,不然计数器不会运行,计数器更新时,会触发中断
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState); //中断控制,开启定时器。
写定时器中断函数
-
新建文件Timer.c和Timer.h,放到System文件夹
#ifndef __TIMER_H #define __TIMER_H void Timer_Init(void); uint16_t Timer_GetCount(void); #endif #include "stm32f10x.h" // Device header void Timer_Init(){ //开启APB1的外设TIM2 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //设置内部时钟源,默认也是内部,可以省略 TIM_InternalClockConfig(TIM2); //时基单元初始化 TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; //预分频器,这里是输入滤波电路,设置频率大小进行采样,采样点越多越准确 TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; //计数器模式:向上计数 TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; /** 使用化简后公式:【定时时间 = (ARR+1) * (PSC + 1) / 72MHz】,定时1s 也就是说,只要(ARR+1) * (PSC + 1) ÷ 72M = 1就可以 然后可以反向推算出,(ARR+1) * (PSC + 1) = 72M 我们只需要找到设置的数值满足这个条件即可,其中ARR和PSC范围是0~65535 ps.ARR数值越大,中断次数越少 因为ARR是重装值,只有计数器达到重装值,才会溢出,这样才可以触发中断。 */ //设置重装值ARR = 9999 TIM_TimeBaseInitStruct.TIM_Period = 10000 - 1; //设置预分频器分频值 PSC= 7199 TIM_TimeBaseInitStruct.TIM_Prescaler = 7200 - 1; //高级定时器的重复次数计数器,TIM2是通用定时器,没有,写0 TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; //初始化时基单元 TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct); //清除更新中断标志位,优化从 1 开始计数的问题 TIM_ClearITPendingBit(TIM2,TIM_IT_Update); //配置输出中断控制,允许更新中断触发中断 TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); //NVIC设置分组 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStruct; //设置TIM2通道 NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStruct); //开启TIM2计数器 TIM_Cmd(TIM2, ENABLE); } //返回计数器值。ARR=9999,因此计数器变化从0~9999 uint16_t Timer_GetCount(void){ return TIM_GetCounter(TIM2); }
-
main函数
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Timer.h" int16_t num; int main(void){ OLED_Init(); Timer_Init(); OLED_ShowString(1,1,"s:"); //记录秒 OLED_ShowString(2,1,"ms:"); //记录毫秒 while(1){ OLED_ShowNum(1,4,num,5); OLED_ShowNum(2,4,Timer_GetCount(),5); } } //设置TIM2中断 void TIM2_IRQHandler(void){ //获取是否是TIM_IT_Update触发的中断 if(TIM_GetFlagStatus(TIM2,TIM_IT_Update)==SET){ //执行时间+1操作 num++; //清空中断标志位 TIM_ClearITPendingBit(TIM2,TIM_IT_Update); } }
Q1:72MHz 是多少?
A1:72MHz = 72 × 106 Hz
2.定时器外部时钟
-
复制上面的工程,更改Timer.c的内容
#include "stm32f10x.h" // Device header void Timer_Init(){ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //开启GPIOA_PIN_0端口:默认复用TIM2_CH1_ETR RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //初始化GPIOA_PIN_0,输入模式,高电平有效/可以使用浮空输入 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); //ETR通过外部时钟模式2输入的时钟(配置定时器,预分频器,高电平有效,过滤采样0x0F最大消除抖动) TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0x0F); TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; //手动模拟时钟输入,因此调小:重装值9,累计10个数触发中断 TIM_TimeBaseInitStruct.TIM_Period = 10 - 1; //0分频,按照原本时钟频率输出 TIM_TimeBaseInitStruct.TIM_Prescaler = 1 - 1; TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct); TIM_ClearITPendingBit(TIM2,TIM_IT_Update); TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2; NVIC_Init(&NVIC_InitStruct); TIM_Cmd(TIM2, ENABLE); } uint16_t Timer_GetCount(void){ return TIM_GetCounter(TIM2); }
4.定时器输出比较
输出比较:CCR写
OC:输出比较
IC:输入捕获
CC:输入捕获与输出比较单元
PWM:脉冲宽度调制
-
输出比较OC:可以通过比较CNT与CCR寄存器值的关系
CNT:时基单元内的计数器
CCR:捕获/比较寄存器,输入捕获与输出比较共用。
-
来对电平进行操作,用于输出一定频率和占空比的PWM波形
-
每个高级定时器和通用定时器,都有4个输出比较通道
-
高级定时器的前3个通道,额外拥有死区生成和互补输出的功能
-
PWM波形:在具备惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效低获得所需要的模拟参量,常用于电机控速领域
频率:1 / Ts
占空比:Ton / Ts
线性等效:高电平5V,低电平0V,占空比等效于电压
占空比50%:电压等效于0.5 * 5V = 2.5V
占空比20%:电压等效于0.2 * 5V = 1V
分辨率:占空比变化步距
占空比变化的细腻程度
要求不高,一般设置为分辨率 = 1%
-
PWM基本结构:
-
黄色线:计数器重装值ARR
-
蓝色线:计数器值CNT
-
红色线:捕获/比较CCR
CCR设置的高低,决定占空比的大小。
-
-
PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
计数器更新频率
-
PWM占空比:Duty = CCR / (ARR + 1)
CCR设置的高低,决定占空比的大小。
-
PWM分辨率:Reso = 1 / (ARR + 1)
步距范围在 0 ~ ARR
-
输出比较通道:
-
通用计时器比较通道
-
CNT与CCR1进行比较,当CNT>CCR1或CNT=CCR1时,输出控制器就会传一个信号
-
输出控制器改变oc1ref电平
ref:reference参考信号
电平翻转:可以生成PWM波,电平翻转两次,PWM生成一个周期,占空比50%
PWM模式:输出频率和占空比都可以调节的PWM波形
-
ETRF输入:定时器小功能,无需了解
-
主模式控制器:把ref映射到主模式的TRGO输出上。
-
CC1P:极性选择,是否要反转信号
-
CC1E:选择输出使能电路是否要输出
-
OC1:输出通道 CH1引脚
-
-
高级定时器比较通道
互补输出:OC1与OC1N连接到推挽电路,控制输出模式
死区生成:OC1与OC1N同时导通,器件容易发热损坏,所以OC1开关与OC1N开关之间延迟一小段时间。
-
-
舵机:
-
舵机是一种根据输入PWM信号占空比来控制输出角度的装置
-
PWM信号要求:周期20ms,高低电平宽度为0.5ms~2.5ms
旋转角度,线性分配:-90°~90°
-
拆解图
内部由直流电机驱动,把PWM作为协议使用
输入一个PWM波形,输出固定在一个角度
GND(棕色线):接地
+5V(红色线):驱动大功率设备需要+5V,可以单独供电
PWM信号线(橙色线):信号线,接PWM信号
-
-
直流电机:
-
直流电机:将电能转化为机械能的装置
-
旋转方向:有两个电极,正接正转,反接反转
-
电机驱动:大功率设备,GPIO无法驱动,使用电机驱动电路
电机驱动芯片:TB6612、DRV8833、L9110、L298N、mos管等
-
-
TB6612驱动芯片
-
双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向,由两路推挽电路组成
-
硬件电路
VM电源(4.5V~10V):驱动电机的电源,5V电机就接5V,7.2V电机就接7.2V
VCC(2.7V~5.5V):逻辑电平输入端,与控制器电源保持一致,STM32接3.3V;51单片机接5V
GND:选择一个使用即可,接地
STBY:待机控制脚,接入GND芯片不工作处于待机状态;接入VCC,芯片正常工作,可以直接接入3.3V / GPIO控制
驱动两个电机
A路电机
输入控制端
PWMA
AIN1
AIN2
-
电机输出:
-
AO1
-
AO2
-
-
B路电机
-
输入控制端
-
PWMB
-
BIN1
-
BIN2
-
电机输出:
-
BO1
-
BO2
1.PWM驱动LED呼吸灯
RCC开启时钟,把TIM外设和GPIO外设时钟打开
配置时基单元、时钟源选择
配置输出比较单元:CCR的值、输出比较模式、极性选择、输出使能等参数
配置GPIO,初始化为复用推挽输出的配置
运行控制,启动计数器
-
新建PWM.c和PWM.h 文件,放到Hardware中,注意文件夹
#ifndef __PWM_H #define __PWM_H void PWM_Init(void); void PWM_SetCompare1(uint16_t Compare); #endif
#include "stm32f10x.h" // Device header void PWM_Init(){ //开启时钟:GPIOA,TIM2 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); /* //端口复用,将使用AFIO设备,将端口重映射 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //选择重映射的端口 GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE); //如果重映射的端口是调试口,那么就禁用调试口 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE); */ //初始化GPIOA_0 GPIO_InitTypeDef GPIO_InitStruct; //复用推挽输出,可以用片上外设进行操作 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; //GPIO_Pin_0 重映射到 GPIO_Pin_15,就需要配置这个端口 //GPIO_InitStruct.GPIO_Pin = GPIO_Pin_15; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); //选择内部时钟 TIM_InternalClockConfig(TIM2); //设置时基单元,利用公式计算占空比 /* PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1) PWM占空比:Duty = CCR / (ARR + 1) PWM分辨率:Reso = 1 / (ARR + 1) 1.频率设置为1%,那么 ARR = 99 2.占空比可任意调节,CCR范围是0~ARR,也就是0~99 3.PWM频率Freq = 72MHz / (PSC + 1) / 100 = 1000 (1s钟产生1000个方波信号) 倒数是定时时间:100 * (PSC + 1) / 72MHz 想要定时多长时间,手动配置PSC, PSC = 720 - 1 => 1ms */ TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStruct.TIM_Period = 100 - 1; //ARR TIM_TimeBaseInitStruct.TIM_Prescaler = 720 - 1; //PSC TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct); TIM_OCInitTypeDef TIM_OCInitStruct; //初始化结构体,使用默认值,无需配置不需要的参数 TIM_OCStructInit(&TIM_OCInitStruct); //PWM通用定时器,只需要设置这些参数 //输出比较模式:PWM1 TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; //设置有效电平:高电平 TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; //允许输出标志 TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; //CCR,设置CCR寄存器的值,占空比50%的波形 TIM_OCInitStruct.TIM_Pulse = 50 ; //CCR TIM_OC1Init(TIM2,&TIM_OCInitStruct); //开启计数器 TIM_Cmd(TIM2,ENABLE); } //动态更改CCR的值,设置不同占空比 //占空比由CCR和ARR共同决定: Duty = CCR / (ARR + 1) void PWM_SetCompare1(uint16_t Compare){ TIM_SetCompare1(TIM2,Compare); }
-
main函数
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "PWM.h" uint16_t i; int main(void){ PWM_Init(); while(1){ //占空比增大,呼吸灯不断变亮 for(i = 0;i<100;i++){ PWM_SetCompare1(i); Delay_ms(20); } //占空比减小,呼吸灯不断变暗 for(i = 0;i<100;i++){ PWM_SetCompare1(100 - i); Delay_ms(20); } } }
2.PWM驱动舵机
-
修改文件PWM.c和PWM.h,换成通道二输出PWM波形
#ifndef __PWM_H #define __PWM_H void PWM_Init(void); void PWM_SetCompare2(uint16_t Compare); #endif
#include "stm32f10x.h" // Device header void PWM_Init(){ //开启RCC时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //初始化GPIOA_PIN_1端口,复用推挽输出,通道2 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); TIM_InternalClockConfig(TIM2); TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; /* 舵机驱动周期:20ms = 50Hz,变动范围0.5ms~2.5ms(-90°~90°) 50Hz = 72MHz / (PSC + 1) / (ARR + 1) 算的(PSC + 1)*(ARR + 1) = 72 * 20K 那么PSC和ARR随意分配,结果不唯一,这里 ARR = 20000 - 1 PSC = 72 - 1 CCR = 500 ~ 2500 */ TIM_TimeBaseInitStruct.TIM_Period = 20000 - 1; TIM_TimeBaseInitStruct.TIM_Prescaler = 72 - 1; TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct); TIM_OCInitTypeDef TIM_OCInitStruct; TIM_OCStructInit(&TIM_OCInitStruct); TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStruct.TIM_Pulse = 0 ; //CCR //通道2 TIM_OC2Init(TIM2,&TIM_OCInitStruct); TIM_Cmd(TIM2,ENABLE); } //设置通道2的CCR void PWM_SetCompare2(uint16_t Compare){ TIM_SetCompare2(TIM2,Compare); }
-
新建Servo.c和Servo.h,放到Hardware中,用于舵机驱动
#ifndef __SERVO_H #define __SERVO_H void Servo_Init(void); void Servo_SetAngle(float Angle); #endif
#include "stm32f10x.h" // Device header #include "PWM.h" void Servo_Init(){ PWM_Init(); } /* CCR变化范围:500~2500 我们规定: 当CCR=500时,舵机0° 当CCR=2500时,舵机180° 因此,输入度数,线性转换为CCR的值。 思路:180°变化了 2500-500 = 2000 ,那么1°变化了 2000/180°,所以Angle度,对应变化Angle* (2000/180),就是0~2000的度数变化函数,加上偏移500, */ void Servo_SetAngle(float Angle){ PWM_SetCompare2(Angle/180 * 2000 + 500); }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "PWM.h" #include "Servo.h" #include "Key.h" uint16_t num; uint16_t keyNum; int main(void){ OLED_Init(); Servo_Init(); Key_Init(); OLED_ShowString(1,1,"Angle:"); while(1){ keyNum = Key_GetNum(); if(keyNum == 1){ num+=30; if(num > 180){ num = 0; } } Servo_SetAngle(num); OLED_ShowNum(1,7,num,3); } }
Q1:为什么舵机不转?
A1:TIM2的CH2通道配置错误
A2:CCR的值,不在舵机变化范围内(500~2500)
A3:电机5V驱动电压,接入3.3V,可以听到转动声音,但是不转。
Q2:为什么舵机乱转?
A1:没有初始化按键。
3.PWM驱动直流电机
-
修改PWM.c和PWM.h文件,使用通道3
#ifndef __PWM_H #define __PWM_H void PWM_Init(void); void PWM_SetCompare3(uint16_t Compare); #endif
#include "stm32f10x.h" // Device header void PWM_Init(){ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //GPIOA_PIN_2,作为PWM输出,通道3 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_2; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); TIM_InternalClockConfig(TIM2); TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStruct.TIM_Period = 100 - 1; TIM_TimeBaseInitStruct.TIM_Prescaler = 720 - 1; TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct); TIM_OCInitTypeDef TIM_OCInitStruct; TIM_OCStructInit(&TIM_OCInitStruct); TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStruct.TIM_Pulse = 50 ; //CCR //通道3 TIM_OC3Init(TIM2,&TIM_OCInitStruct); TIM_Cmd(TIM2,ENABLE); } //使用通道3, void PWM_SetCompare3(uint16_t Compare){ TIM_SetCompare3(TIM2,Compare); }
人耳听到声音的频率范围是:20Hz~20KHz
加大频率,修改预分频器的值,不会影响占空比,可以消除声音
-
新建Motor.c和Motor.h作为直流电机驱动函数
#ifndef __MOTOR_H #define __MOTOR_H void Motor_Init(void); void Motor_SetSpeed(int8_t Speed); #endif
#include "stm32f10x.h" // Device header #include "PWM.h" void Motor_Init(){ //开启RCC时钟GPIOA RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //使用GPIOA_PIN_4和GPIOA_PIN_5,作为电机输入方向端口 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_5; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); //初始化PWM PWM_Init(); } //设置点击旋转速度,输入的值带方向。 void Motor_SetSpeed(int8_t Speed){ //电机正转 if(Speed >= 0){ //GPIO_Pin_4 高电平,GPIO_Pin_5低电平 =>电机正转 GPIO_SetBits(GPIOA,GPIO_Pin_4); GPIO_ResetBits(GPIOA,GPIO_Pin_5); //PWM驱动电机速度 PWM_SetCompare3(Speed); }else{ //GPIO_Pin_4 低电平,GPIO_Pin_5高电平 =>电机反转 GPIO_SetBits(GPIOA,GPIO_Pin_5); GPIO_ResetBits(GPIOA,GPIO_Pin_4); //PWM驱动电机速度 PWM_SetCompare3(-Speed); } }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Motor.h" #include "Key.h" int8_t Speed; int main(void){ OLED_Init(); Motor_Init(); Key_Init(); OLED_ShowString(1,1,"Speed:"); while(1){ //按钮按下,调节点击旋转方向和速度。 if(Key_GetNum()==1){ Speed += 20; if(Speed >100){ Speed = -100; } } OLED_ShowSignedNum(1,7,Speed,3); Motor_SetSpeed(Speed); } }
Q1:为什么电机不转?
A1:面包板电压传递后下降,到电机驱动模块的时候,电压不够,可以把STM32附近的正负极,用线连接到模块附近。
5.定时器输入捕获
输入捕获:CCR 只读
-
输入捕获IC:当输入通道出现指定电平跳变时,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数
类似于外部中断,都是检测电平跳变然后执行动作。
外部中断的动作是,向CPU申请中断;这里的动作是,控制后续电路,锁存CNT的值到CCR中
-
通用定时器和高级定时器,有相同的输入捕获功能;基本定时器没有输入捕获
-
可配制为PWMI模式(PWM输入模式),同时测量频率和占空比
-
可配合主从触发模式,实现硬件全自动测量
-
测量频率方法:
-
测频法:fx = {N \over T} (高频适合)
在闸门时间T内,对上升沿(或下降沿)计次,得到N,可以计算频率 fx
-
测周法:fx = {f_c \over N} (低频适合)
在两个上升沿内,以标准频率fc 计次,得到N,可以计算频率
-
中界频率:fm = \sqrt[]{f_c \over T}
测频法和测周法误差相等的频率点,在N相等的前提下。
-
输入捕获通道:
-
三输入异或门:通道1,2,3端口,当任何一个引脚有电平翻转时,输出引脚就产生一次电平翻转,通过数据选择器,到达输入捕获通道1
服务于三相无刷电机,根据转子位置换相
TRC也是为了无刷电机的驱动
-
输入信号:输入信号进入输入滤波器和边沿检测器,进行信号滤波、边沿检测触发,当出现指定电平时,边沿检测电路会触发后续电路执行动作
-
TI1FP1(TI1 Filter Polarity 1):经过滤波和极性选择,得到TI1FP1,输入给通道1的后续电路
-
TI1FP2(TI1 Filter Polarity 2):经过另一个滤波和极性选择,得到TI1FP2,输入给通道2的后续电路
-
TI2FP1(TI2 Filter Polarity 1):经过滤波和极性选择,得到TI2FP1,输入给通道1的后续电路
-
TI2FP2(TI2 Filter Polarity 2):经过另一个滤波和极性选择,得到TI2FP2,输入给通道2的后续电路
-
灵活切换后续捕获电路的输入
-
把一个引脚的输入,同时映射到两个捕获单元
通道交叉连接:
-
-
预分频器:分频触发信号,可以触发捕获电路的工作
每有一个触发信号,CNT的值,就会向CCR转运一次,同时会发生一个捕获事件,事件会在状态寄存器置一个标志位,同时也可以产生中断。
-
硬件电路:
-
TI1:CH1引脚,进来信号TI1
-
滤波器向下计数器:信号TI1,经过滤波器,输出滤波后的信号TI1F
-
fDTS :滤波器的采样时钟来源
-
CCMR1:CCMR1寄存器中的ICF位,可以控制滤波器的参数
-
边沿检测器:滤波后的信号TI1F,通过边沿检测器,捕获上升沿或下降沿
-
CCER寄存器:CCER寄存器中的CC1P位,可以进行极性选择
-
TI1FP1触发信号:通过数据选择器,进入通道1后续的捕获电路
-
CCMR1:CC1S位,可以对数据选择器进行选择,ICPS位可以配置分频器
-
CCER:CC1E位,可以控制使能或失能
-
IC1PS:信号经过电路,到达IC1PS,就可以让CNT中的值,转运到CCR中
每一次捕获CNT,都要把CNT清零(自动清零),以便于下一次捕获
-
从模式控制器:可以完成自动清零
-
-
主从触发模式
主从触发模式:是主模式、从模式、触发源选择的简称
-
主模式:可以将定时器内部的信号,映射到TRGO引脚,用于触发其他外设
-
从模式:被别的信号控制,可以接收其他外设,或者自身外设的信号用于控制自身定时器运行,从模式可以从从模式列表中选择一项任务自动执行
自动清零:触发源选择TI1FP1,通过TRGI,触发Reset,实现自动清零
-
触发源选择:选择从模式的信号源,是从模式的一部分;选择指定的一个信号,得到TRGI,使用TRGI触发从模式
-
-
输入捕获基本结构
【测周法】:图中未使用交叉通道,只使用一个通道,目前只能测量频率:CCR1中的值就是N,fx = {f_c \over N}
-
配置好时基单元,启动定时器,计数器CNT,不断自增
CNT计数:测周法用来的计数计时,fc 就是预分频器分频后,CK_CNT的时钟频率,标准频率fc = {72MHz \over (预分频系数)}
-
GPIO输入:输入捕获通道1的GPIO口,经过滤波器、边沿检测、极性选择后,输出信号为TI1FP1。
-
分频器:设置TI1FP1上升沿触发,输入选择直连的通道,分频器选择不分频
-
CCR1捕获/比较器:当TI1FP1出现上升沿后,CNT的当前计数值转运到CCR1里
-
触发源选择:CNT值转运到CCR1后,触发源选择选中TI1FP1为触发信号。
-
从模式复位操作:TI1FP1上升沿就会触发CNT清零
如果频率太低,信号间隔时间变长,可能会导致CNT没有记录完成就会溢出,CNT:0~65535
自动清零:如果想要使用自动清零,实现硬件自动化,就只能选择通道1和通道2的从模式;通道3和通道4只能开启中断手动清零,比较消耗资源
-
-
PWMI基本结构
【测周法】:图中使用交叉通道,使用两个个通道,可以同时测量频率和占空比。
-
通道1,进行频率测量(测周法,一个周期的时间,计算频率)
-
通道2,进行高电平测量(测周法,一个周期,高电平时间)
-
可以计算占空比。
-
1. 输入捕获模式测频率
RCC开启时钟,打开GPIO、TIM时钟
初始化GPIO,配置成输入模式(上拉/浮空)
配置时基单元,计数器自增运行
配置输入捕获单元,滤波器、极性、通道模式(直连/交叉)、分频器等参数,一个结构体即可
选择从模式触发源TI1FP1
选择从模式,触发后的操作:自动清零
开启定时器TIM_Cmd
-
修改PWM.c和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
#include "stm32f10x.h" // Device header void PWM_Init(){ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //设置通道1,GPIOA_Pin_0 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); TIM_InternalClockConfig(TIM2); TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStruct.TIM_Period = 100 - 1; //ARR,影响分辨率和占空比,因此不作为修改频率的方法。 TIM_TimeBaseInitStruct.TIM_Prescaler = 720 - 1; //PSC,影响频率,并不影响分辨率和占空比,可以作为修改频率的方法,下面有函数。 TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct); TIM_OCInitTypeDef TIM_OCInitStruct; TIM_OCStructInit(&TIM_OCInitStruct); TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStruct.TIM_Pulse = 50 ; //CCR默认值 //设置通道1 TIM_OC1Init(TIM2,&TIM_OCInitStruct); TIM_Cmd(TIM2,ENABLE); } //设置CCR的值,占空比,设置通道1 void PWM_SetCompare1(uint16_t Compare){ TIM_SetCompare1(TIM2,Compare); } //添加设置频率的函数:可以重新设置定时器预分频器的值 void PWM_SetPrescaler(uint16_t Prescaler){ TIM_PrescalerConfig(TIM2,Prescaler,TIM_PSCReloadMode_Immediate); }
-
新建IC.c和IC.h,放到Hardware文件夹
#ifndef __IC_H #define __IC_H void IC_Init(void); uint32_t IC_GetCapture1(void); #endif
#include "stm32f10x.h" // Device header void IC_Init(){ //开启时钟 GPIOA,TIM3 因为TIM2被占用输出PWM波 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); //初始化GPIOA_Pin_6,按照接口定义,可作为TIM3_CH1,通道1 GPIO_InitTypeDef GPIO_InitStruct; //输入模式,高电平触发 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); //设置时钟源:内部时钟 TIM_InternalClockConfig(TIM3); //初始化时基单元 TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; // 允许计数的最大值,设置大一点,防止溢出 TIM_TimeBaseInitStruct.TIM_Period = 65536 - 1; /* 因为 fx = fc / N N:是计数值,当读取到一个周期的时候,CNT的值 fc:是计数器输入的时钟频率,是在时基单元中,经过预分频器之后输出的频率。 可以计算出fc = 72M / 72 = 1M = 1,000,000 */ TIM_TimeBaseInitStruct.TIM_Prescaler = 72 - 1; TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct); //初始化IC输入捕获 TIM_ICInitTypeDef TIM_ICInitStructure; //选择四个通道:选择TIM3的通道1 TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //滤波器:过滤强度:0x0~0xF TIM_ICInitStructure.TIM_ICFilter = 0xF; //极性选择,上升沿触发 TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //1分频,原样输出,每次触发都有效,//TIM_SetIC1Prescaler(TIM3,TIM_ICPSC_DIV1); TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //直连通道 / 交叉通道 ,选择直连通道 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInit(TIM3,&TIM_ICInitStructure); //TRGI选择触发源:TI1FP1 TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1); //配置从模式:执行Reset操作 TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset); //开启定时器 TIM_Cmd(TIM3,ENABLE); } uint32_t IC_GetCapture1(){ //fc = 1M //fx = fc / N //在计数的过程中,会出现正负1的误差,是符合要求的;最后的+1 是为了看起来好看。 //TIM_GetCapture1(),获取通道1的CCR寄存器值,也就是N return 1000000 / (TIM_GetCapture1(TIM3)+ 1); }
-
main
#include "stm32f10x.h" // Device header #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"); // ARR = 100 - 1 PWM_SetPrescaler(720 - 1); //Freq = 72M / (PSC + 1) / 100 PWM_SetCompare1(50); //Duty = CCR / 100 while(1){ OLED_ShowNum(1,6,IC_GetCapture1() ,5); } }
2.PWMI模式测频率占空比
-
修改IC.c和IC.h代码
#ifndef __IC_H #define __IC_H void IC_Init(void); uint32_t IC_GetCapture1(void); uint32_t IC_GetDuty(void); #endif
#include "stm32f10x.h" // Device header void IC_Init(){ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); TIM_InternalClockConfig(TIM3); TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; // 1ms TIM_TimeBaseInitStruct.TIM_Period = 65536 - 1; TIM_TimeBaseInitStruct.TIM_Prescaler = 72 - 1; TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct); 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; //只需要修改IC初始化部分,读取一个频率,同时使用通道1获取频率,使用通道2获取占空比。 //TIM_ICInit(TIM3,&TIM_ICInitStructure); //也可以写两个通道,分别初始化。 //PWMI会自动的,配置好与结构体相反的,另一个通道,只限于通道1和通道2之间 /* 上面结构体,配置了通道1,上升沿触发、直连通道 那么TIM_PWMIConfig,会自动初始化,通道2,下降沿触发、交叉通道 */ TIM_PWMIConfig(TIM3,&TIM_ICInitStructure); TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1); TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset); TIM_Cmd(TIM3,ENABLE); } uint32_t IC_GetCapture1(){ return 1000000 / (TIM_GetCapture1(TIM3)+ 1); } //计算占空比,通道2记录一个周期内,高电平数量,占空比最后 * 100% ,否则是小数显示。 uint32_t IC_GetDuty(){ return (TIM_GetCapture2(TIM3)+1) * 100 / (TIM_GetCapture1(TIM3)+1); }
-
main
#include "stm32f10x.h" // Device header #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:00%"); // ARR = 100 - 1 PWM_SetPrescaler(720 - 1); //Freq = 72M / (PSC + 1) / 100 PWM_SetCompare1(80); //Duty = CCR / 100 while(1){ OLED_ShowNum(1,6,IC_GetCapture1() ,5); OLED_ShowNum(2,6,IC_GetDuty(),2); } }
上述值设定:
ARR = 65536 – 1
PSC = 72 -1
测量下限:可以测量的最低频率是: 1M / 65535 ≈ 15Hz ,信号频率再低,计数器就会溢出(因为等不到一个周期的完成)。
可以增加 PSC 的值,可以测量更低频率的信号
测量上限:信号频率越大,误差越大,最高上限1MHz,但没有实际意义,根据使用者对误差的要求(1 / 计数值 = 误差值)
可以减少PSC的值,提高上限;但频率更高,可以更改为测频法。
误差 = {1 \over 计数值}
要求误差到{1\over100} :频率上限 {1M \over 100 }=10KHz
要求误差到{1\over1000} :频率上限 {1M \over 1000 }=1KHz
晶振误差:晶振微小误差慢慢累积
测量误差:接收的信号需要进行滤波处理
6.编码器接口
可以通过定时器编码器接口,进行自动计次,节约软件资源;
之前使用外部中断,进行手动计次,程序频繁进入中断,消耗资源。
对于需要频繁执行,而且操作比较简单的任务,可以设计硬件电路自动完成
-
编码器接口(Encoder Interface):自动给编码器进行计次的电路
每隔一段时间取一下计次值,就可以得到编码器旋转速度
编码器测速:可用于电机控制
-
功能:可以接受增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲、自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度
正交编码器:输出两个方波信号,相位相差90°,超期90°或滞后90°,分别代表正转和反转
-
位置:
-
每个高级定时器和通用定时器,都拥有1个编码器接口
如果定时器配置为编码器接口模式,基本上干不了其他活,资源比较紧张(4个定时器,全配置编码器的话,就没有定时器可以用,可以使用外部中断弥补)
-
编码器的两个输入引脚,借用了输入捕获的通道1和通道2
-
编码器接口基本结构
-
CH1通道1的TI1FP1,连接到编码器接口引脚TI1FP1
-
CH2通道2的TI1FP2,连接到编码器接口引脚TI1FP2
-
CH1和CH2的输入滤波器和边沿检测器,编码器使用;后面的是否交叉、预分频器、CCR寄存器等,与编码器接口无关
-
编码器接口输出:类似于从模式控制器,控制CNT计数时钟和计数方向,其中CK_PSC和时基单元初始化时设置的计数方向,并不会使用,都受编码器控制
-
两个GPIO接口,通过滤波器和边沿检测极性选择,产生TI1FP1和TI2FP2,通向编码器接口
-
编码器接口,通过预分频器,控制CNT计数器的时钟,同时根据编码器的旋转方向,控制CNT的计数方向。(正转自增和反转自减)
-
ARR一般设置为 65535,最大量程,可以利用补码的特性,得到负数
-
-
工作模式:
一般使用,第三种模式,精度最高
-
实例图:
-
正交编码器,抗噪声原理:如果上下波形,一个不变,一个跳变,那么一个噪声波形输入进来后,计数会先上升再下降,或先下降再上升,保持不变,实现抗噪声。
-
正向与反向:在编码器接口模式下,上升沿和下降沿都可能进行计次,那么极性选择就不再是边沿的极性选择,而是高低电平的极性选择,也就是高低电平是否反转。
-
均不反相:IT1和IT2均不反转
-
TI1反相:TI1电平反转。分析的时候,把TI1的波形反相后,对照表格才能正确分析
如果旋转方向反了,可以把任一个引脚反相,也可以把A和B引脚交换。
-
1.编码器接口测速
RCC开启时钟,开启GPIO和定时器
配置GPIO,把PA6和PA7设置为输入模式
配置时基单元,选择不分频,自动重装给65535最大,只需要个CNT执行计数
配置输入捕获单元,只需要配置滤波器、极性两个参数
配置编码器接口模式
启动定时器TIM_Cmd
测量编码器位置:读取CNT的值
测量编码器速度方向:使用闸门,每隔一定时间取出CNT的值,然后清零
-
新建Encoder.c和Encoder.h文件,放到Hardware文件夹
#ifndef __ENCODER_H #define __ENCODER_H void Encoder_Init(void); int16_t Encoder_GetCounter(void); #endif
#include "stm32f10x.h" // Device header void Encoder_Init(){ //开启RCC时钟,TIM3和GPIOA RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //初始化GPIOA_Pin_6和GPIOA_Pin_7,输入模式 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); //初始化时基单元,作为编码器接口,ARR设置最大值65535,不分频 TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStruct.TIM_Period = 65536 - 1; TIM_TimeBaseInitStruct.TIM_Prescaler = 1 - 1; TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct); //输入捕获配置,选择通道1,配置极性不翻转。 TIM_ICInitTypeDef TIM_ICInitStruct1; TIM_ICStructInit(&TIM_ICInitStruct1); TIM_ICInitStruct1.TIM_Channel = TIM_Channel_1; TIM_ICInitStruct1.TIM_ICFilter = 0xF; //TIM_ICInitStruct1.TIM_ICPolarity = TIM_ICPolarity_Rising; //TIM_EncoderInterfaceConfig()中可以设置极性,两者配置相同,因此可以省略在下面配置 TIM_ICInit(TIM3,&TIM_ICInitStruct1); 输入捕获配置,选择通道2,配置极性不翻转。 TIM_ICInitTypeDef TIM_ICInitStruct2; TIM_ICStructInit(&TIM_ICInitStruct2); TIM_ICInitStruct2.TIM_Channel = TIM_Channel_2; TIM_ICInitStruct2.TIM_ICFilter = 0xF; //TIM_ICInitStruct2.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInit(TIM3,&TIM_ICInitStruct2); //配置编码器接口,选择IT1和IT2都触发,极性不翻转 TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising); //开启定时器 TIM_Cmd(TIM3,ENABLE); } //获取CNT,uint16_t 强制返回 int16_t ,可以做到正负号显示。 int16_t Encoder_GetCounter(){ int16_t tmp; tmp = TIM_GetCounter(TIM3); //CNT每次清零, TIM_SetCounter(TIM3,0); //返回每次计数的值【距离】,如果每1s【时间】显示一次,就是旋转【速度 = 距离 / 时间】。 return tmp; }
-
调用time.c函数,触发中断,每秒读取一次【之前写过,直接调用即可】
#include "stm32f10x.h" // Device header void Timer_Init(){ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; TIM_InternalClockConfig(TIM2); TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; // 1s TIM_TimeBaseInitStruct.TIM_Period = 10000 - 1; TIM_TimeBaseInitStruct.TIM_Prescaler = 7200 - 1; TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct); TIM_ClearITPendingBit(TIM2,TIM_IT_Update); TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStruct); TIM_Cmd(TIM2,ENABLE); }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Encoder.h" #include "Timer.h" int16_t num; int main(void){ OLED_Init(); Timer_Init(); Encoder_Init(); OLED_ShowString(1,1,"CNT:"); while(1){ OLED_ShowSignedNum(1,5,num,5); } } //每1s触发一次中断,读取显示到显示屏。 void TIM2_IRQHandler(void){ if(TIM_GetFlagStatus(TIM2,TIM_IT_Update)==SET){ num = Encoder_GetCounter(); TIM_ClearITPendingBit(TIM2,TIM_IT_Update); } }
GPIO输入模式选择:和外部默认电平保持一致,如果不确定/电压小,可以选择浮空输入。
6. ADC与DMA
1.ADC模数转换器
电位器:滑动变阻器的作用,调节电阻,产生连续变化的模拟电压信号。
-
ADC:模拟-数字转换器
-
ADC可以把,引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁。
-
STM32的ADC是,12为逐次逼近型ADC,1us的转换时间。
逐次逼近型: ADC的一种工作模式
ADC的参数
分辨率:12位AD值,范围是0~212 -1 (0~4095)
转换时间(转换频率):从AD转换开始,到产生结果,需要1us时间(1MHz)
这是STM32最快的转换频率1MHz,如果要转换频率非常高的信号,可能不够用
输入电压范围:0~3.3V
转换结果范围:0~4095
-
18个输入通道,可测量16个外部信号源(这个系列最多16个),和2个内部信号源
外部信号源:GPIO口,在引脚上直接接入模拟信号即可,不需要额外的电路
内部信号源:
内部温度传感器:可以测量CPU温度
内部参考电压:是一个1.2V左右的基准电压,不随外部供电电压变化而变化
如果芯片供电不是标准的3.3V,那么测量外部引脚电压可能不对,这时可以读取这个基准电压进行校准,可以得到正确电压值。
-
规则组和注入组两个转换单元
-
规则组:常规使用
-
注入组:用于突发事件
-
模拟看门狗自动检测输入电压范围
检测比较某个阈值,可以使用模拟看门狗自动执行
模拟看门狗:可以监测指定的某些通道,当AD值高于它设定的上阈值或低于下阈值时,就会申请中断。
-
STM32F103C8T6的ADC资源:ADC1,ADC2,10个外部输入通道
这个芯片,最多测量10个外部引脚的模拟信号
-
逐次逼近型ADC
-
ADC0809是经典的ADC芯片,现在单片机性能和集成度提升,很多单片机内部已经集成了ADC外设,就不需要挂载外设芯片了。
-
IN0~IN7:8路输入通道,通过通道选择开关,进入比较器【待测电压】。
STM32,内部是18路输入的多路开关
-
ADDA~ADDC:地址锁存和译码,选中哪个通道,就把通道号放到这三个引脚上
输入IN0~IN7的地址
-
ALE:锁存信号,对应通路的开关就可以自动拨好。
拨好对应地址的开关
-
比较器:两个电压进行大小判断,如果DAC电压过大,就调小DAC数据;如果DAC电压过小,就调大DAC数据。直到DAC输出电压与外部通道输入电压近似相等。
-
待测电压:通道选择开关传输过来的电压值
-
DAC:数模转换,近似到待测电压,输出DAC编码数据 。
-
-
逐次逼近寄存器SAR:利用二分法,逐次逼近找到近似电压,得到DAC数据。
判断某一位是1还是0的过程,对于8位ADC,需要判断8次;12位判断12次。
-
DAC:数模转换,利用加权电阻网络,不同数据对应不同大小的电压值。输出数据就是未知电压的编码
-
8位三态锁存缓冲区:输出DAC的编码数据,多少位就有多少根线输出。
-
EOC:End Of Convert 转换结束信号
-
START:开始转换信号,给一个输入脉冲,就开始转换。
-
CLOCK:ADC时钟,因为ADC内部是一步一步进行判断的,利用时钟推动过程。
-
VREF(+) 和 VREF(-) :参考电压。
比如数据 255,对应的电压是多少V?由参考电压决定
参考电压,也决定了ADC的输入范围,一般接在Vcc 和 GND,范围就是Vcc 和 GND
-
Vcc 和 GND:芯片的供电与接地,通常会和参考电压接在一起。
STM32的ADC增强功能,
普通流程:启动一次转换->读一次值->再启动->再读值……
STM32的ADC:列一个组,一次启动一个组,连续转换多个值
STM32的ADC
-
ADC输入通道:
-
ADCx_IN1~ADCx_IN15:16个GPIO口
-
温度传感器、VREFINT :两个内部通道,内部温度传感器和内部参考电压。
模拟多路开关:指定我们想要选择的通道,输出进入模数转换器
普通ADC,多路开关一般只选择一个
这里可以选择多个,而且分成两个组(菜单模型)
菜单模型:普通ADC只能一个一个点菜,这个分组之后,可以直接点一个菜单的菜。如果菜单中只有一个菜,就退化成了普通ADC。
注入通道组:最多可以选择4个通道
规则通道组:最多可以选择16个通道
模拟至数字转换器:模数转换器,执行逐次比较的过程,转换的结果放到上面的数据寄存器中
数据寄存器:存放转换结果(餐桌模型)
餐桌模型:餐桌上,最多能放1个菜,要上新菜的时候,就要把原来的菜撤走。
其中,注入通道数据寄存器,可以放四个菜。
-
注入通道数据寄存器(4*16位):可以存放四个数据,最多4个数据不用担心数据覆盖。(涉及不多)
-
规则通道数据寄存器(16位):只能存放一个数据,最多16个数据的前15个会被挤掉,因此最好配合DMA实现(数据转运助手)
触发转换部分
START信号:开始转换。STM32有两种信号,可以触发ADC开始转换
软件触发:程序中手动调用代码,可以启动转换
硬件触发:主要来自于定时器、TRGO主模式输出
防止频繁进入中断,消耗资源,硬件可以自动触发。
注入组:转换完成,产生EOC和JEOC转换完成信号
规则组:转换完成,产生EOC转换完成信号
JEOC和EOC会在状态寄存器里面置标志位。 这两个标志位也可以进入NVIC申请中断
外部中断引脚触发:在程序中配置
参考电压:芯片内部已经接好了。
VDDA 接3.3V;VSSA 接GND,所以ADC输入电压范围就是:0~3.3V
VREF+ :参考电压,决定了ADC输入电压范围
VREF- :参考电压,决定了ADC输入电压范围
VSSA : 供电引脚,内部模拟部分电源,一般VREF+ 接入VSSA
VDDA :供电引脚,内部模拟部分电源,一般VREF- 接入VDDA
ADCCLK:ADC时钟,驱动内部逐次比较的时钟,来自于ADC预分频器,最大是14MHz。
ADC预分频器:来源于RCC,输出驱动内部逐次比较的时钟信号。可以选择2,4,6,8分频
由于传入的时钟是72MHz,而且ADCCLK最大频率是14MHz
分频:
2分频: 72M / 2 = 36M > 14M ,超出允许范围
4分频: 72M / 4 = 18M > 14M ,超出允许范围
6分频: 72M / 6 = 12M < 14M ,允许范围
8分频: 72M / 8 = 9M < 14M ,允许范围
因此,只能选择6分频或8分频
DMA请求:用于触发DMA进行数据转运
模拟看门狗:存放一个阈值高限(12位)和阈值低限(12位)
如果启动了模拟看门狗,并且制定了看门的通道,那么看门狗就会关注这个通道,一旦超过阈值,就会申请一个模拟看门狗中断,通向NVIC
ADC基本结构
-
输入通道:16个GPIO口,和两个内部通道
-
AD转换器:
-
规则组:最多可以选中16个通道
-
注入组:最多可以选中4个通道
AD数据寄存器:存放转换的结果
规则组:只有1个数据寄存器
注入组:有4个数据寄存器
触发控制:提供开始转换的START信号
软件触发:代码
硬件触发:主要来自于定时器,也可以使用外部中断引脚
RCC:来自RCC的ADC时钟CLOCK,ADC逐次比较过程就是由这个时钟推动
模拟看门狗:用于监测转换结果范围,超出设定阈值,就会通过中断输出控制,向NVIC申请中断
转换完成:
规则组:完成转换后,会产生EOC信号,设置标志位,也可以通向NVIC
注入组:完成转换后,会产生JEOC信号,设置标志位,也可以通向NVIC
开关控制:ADC_Cmd给ADC上电。
ADC输入通道
-
STM32F103C8T6:只有10个外部输入通道
PA0~PA7
PB0~PB1
只有ADC1和ADC2
其中,ADC1和ADC2的通道共用同一个,可以使用双ADC模式,也可以单独使用
双ADC模式:ADC1和ADC2一起工作,可以配合组成同步模式、交叉模式等模式,提高采样率
-
整个系列最多有18个通道
通道0~通道17
ADC1:只有ADC1有通道16、17
ADC2:GPIO的引脚,与ADC1引脚相同
ADC3:有些变化
规则组的转换模式
-
单次转换,非扫描模式
-
规则组中的菜单,有16个空位,可以写入要转换的通道
-
非扫描模式:只有序列1有效,菜单同时选中一组的方式,退化为简单的选中一个
-
序列1位置,指定想要转换的通道
-
可以触发转换,ADC对通道2进行模数转换
-
过一小段时间后,转换完成,转换结果放在数据寄存器中,同时给EOC标志位,置1
-
看到EOC标志位,说明转换完成,可以在数据寄存器读取结果
-
再启动一次转换,就要再触发一次。
-
-
连续转换,非扫描模式
-
非扫描模式:只有序列1有效
-
连续转换:第一次转换后,不会停止,立刻开始下一次转换,一直持续下去。
-
只需要最开始触发一次,就可以一直转换。
-
无需判断是否结束,直接读取寄存器值
-
-
单次转换,扫描模式
-
单次转换:转换之后,就会停下来,下次转换需要再此触发才能开始
-
扫描模式:使用菜单列表,在序列中任意指定通道,并且通道可以重复,需要在结构体中配置通道数目,转换结果放到数据寄存器中,防止数据覆盖,要及时使用DMA挪走数据
-
需要触发下一次,才可以开始新一轮转换
-
-
连续转换,扫描模式
类似的套路,不再说明。
-
间断模式
在扫描模式下,扫描过程中,每隔几个转换,就暂停一次,需要再次触发才能继续。
触发控制
信号源:
定时器:定时器控制
外部引脚/定时器:需要AFIO重映射确定
软件控制:软件触发
数据对齐:数据寄存器16位,ADC有12位
数据右对齐:12位数据向右靠,高位补零,直接就是数据。(一般使用)
数据左对齐:12位数据向左靠,低位补零,数据比实际值大16倍,把数据左移了4次。如果不想要这么高的分辨率(0~4095),可以取出数据高8位,舍弃精度,12位ADC退化为8位ADC
转换时间
采用、保持:设置采样开关,存储外部电压,之后断开采样开关,进行AD转换,可以在量化、编码期间,电压始终保持不变,才可以精确定位未知电压位置。
量化、编码:ADC逐次比较的过程
TCONV = 采样时间 + 12.5个ADC周期
采用时间:采样、保持花费的时间,可以在程序中配置,采用时间越大,越能避免毛刺信号干扰。
12.5个ADC周期:量化、编码花费的时间,12位ADC需要花费12个周期,半个周期做一些其他事情。
ADC周期:从RCC分频过来的ADCCLK,最大是14MHz
最快转换时间:当ADCCLK = 14MHz,采样时间为1.5个ADC周期
TCONV = 1.5 + 12.5 = 14个ADC周期 = 1{\mu}s
采样时间更长的话,就达不到1{\mu}s 时间,
若把ADCCLK时钟设置超过14MHz,ADC超频,转换时间比1{\mu}s 更短,但是不稳定。
校准:过程是固定的,只需要在ADC初始化最后,加上固定几条代码
-
ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差
-
建议在每次上电后执行一次校准
-
启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期
硬件电路
电位器:产生一个可调电压,可以输出0~3.3V电压,PA0可接入ADC输入通道
当向上滑动时,电压增大;当向下滑动时,电压减小。
电阻直接跨接在正负极,阻值太小比较费电,更小就可能发热冒烟。(KΩ级别)
传感器:传感器输出电压,可变电阻阻值没办法直接测量。
光敏电阻、热敏电阻、红外接收管、麦克风等可以等效一个可变电阻,阻值没办法直接测量。
测量可变电阻:串联一个固定电阻,阻值相近,进行分压,可以得到一个反应电阻值电压的电路
杆子模型:可变电阻阻值变小,下拉作用变强,输出端电压下降;可变电阻阻值变大,下拉作用变弱,输出端电压生高
当固定电阻与可变电阻,交换位置,那么输出极性就反过来。
电压转换电路:测量0~5V的电压,(对于5V、10V适用,更高电压不建议)
ADC只能接受0~3.3V电压,那么可以使用电阻进行分压。
PA2分得电压,和R2电压相同,得到电压范围0~3.3V,可以进行ADC转换。 分压公式: UPA2 = {R2 \over R1+R2}
高电压采集,最好使用一些专用的采集芯片,如隔离放大器等
1.AD单通道
开启RCC时钟,ADC和GPIO,同时配置ADCCLK的分频器
配置GPIO,配置成模拟输入的形式
配置GPOI直连的,多路开关,接入到规则组列表中
配置ADC转换器,结构体配置,可以直接配置AD转换器、AD数据寄存器
单次转换/连续转换、扫描/非扫描、通道数量、触发源、对齐方式、
如果需要模拟看门狗,就配置阈值和检测通道,想要开启中断,就在中断输出控制中,配置ITConfig函数开启,之后NVIC中配置优先级,就可以触发中断了
开关控制ADC_Cmd,开启ADC。
校准ADC,减小误差
-
新建AD.c和AD.h文件,放到Hardware文件夹
#ifndef __AD_H #define __AH_H void AD_Init(void); uint16_t AD_GetValue(void); #endif
#include "stm32f10x.h" // Device header void AD_Init(void){ //开启RCC时钟,ACD1和GPIOA RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //配置ADCCLK,选择分频数:6分频 => 12M RCC_ADCCLKConfig(RCC_PCLK2_Div6); //配置GPIOA_PIN_0,作为模拟量输入端口,配置AIN模拟量输入模式 //AIN模式下,GPIO口无效,防止输入输出影响模拟电压 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); //选择规则组的输入通道:ADC_Channel_0对应GPIOA_PIN_0口 //RANK:1,对应规则组序列器中的次序,也就是规则组(16个序列)中的编号 //采样时间:需要更快转换,就选更小(1.5),需要更稳定转换,就选更大(239.5),没有需求,随便选:55.5,代表采样时间需要55.5个ADCCLK周期 ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5); //初始化ADC ADC_InitTypeDef ADC_InitStruct; //独立模式/双ADC模式,这里选择独立模式 ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; //数据对齐:左对齐/右对齐 ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; //外部触发转换选择:触发控制的触发源,对应框图左下角外部触发源、None不使用外部触发,也就是使用软件触发 ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //连续转换模式:不启用 ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; //扫描模式:不启用 ADC_InitStruct.ADC_ScanConvMode = DISABLE; //通道数目:在扫描模式下,使用多少个通道 ADC_InitStruct.ADC_NbrOfChannel = 1; ADC_Init(ADC1,&ADC_InitStruct); //开启ADC ADC_Cmd(ADC1,ENABLE); //校验:固定写法 //复位校准,获取CR2寄存器中的RSTCAL标志位,软件设置硬件自动清除,置1表示开始初始化 ADC_ResetCalibration(ADC1); //当初始化完成后,硬件自动请除,标志位置0 while(ADC_GetResetCalibrationStatus(ADC1)==SET); //开始校准 ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1) == SET); } //获取ADC转换值 uint16_t AD_GetValue(){ //软件手动触发转换, ADC_SoftwareStartConvCmd(ADC1,ENABLE); //等待标志位EOC,转换完成,完成后自动设置为1 // 55.5(采集时间) + 12.5(转换时间) = 68个周期,时钟频率12M //也就是需要等待时间,大概是 5.6us = 68 / 12M while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET);//5.6us //读取转换完成的值 return ADC_GetConversionValue(ADC1); }
可以设置为单通道、连续转换、非扫描,可以不需要手动触发开始,无需等待转换完成,直接读取值。【耗电】
-
main函数
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "AD.h" int16_t AD_Value; float V_value; int main(void){ OLED_Init(); AD_Init(); OLED_ShowString(1,1,"AD:"); OLED_ShowString(2,1,"V:0.00v"); while(1){ //获取模拟值 AD_Value = AD_GetValue(); //手动计算电压值 V_value = (float)AD_GetValue()/4095 * 3.3 ; OLED_ShowNum(1,4,AD_Value,4); //无法显示小数,自己手动拼接。 OLED_ShowNum(2,3,V_value,1); OLED_ShowChar(2,4,'.'); OLED_ShowNum(2,5,(uint16_t)(V_value * 100) % 100,2); } }
2.AD多通道
思路:
使用扫描模式,填入多个通道,实现多通道;但是数据会覆盖,需要转运数据
DMA转运数据
手动转运数据:很困难,但也可行。
扫描模式下,每个通达完成后不会产生标志位、也不会触发中断,只有当整个列表全部完成后才会产生一次EOC标志位,才能触发中断。
转换一次数据只有几us,手动转运数据要求比较高
可以使用间断模式,每扫描一个通道就暂停一次,手动转运数据后,继续触发进行下一次转换
由于启动转换之后,没有标志位,只能通过Delay,延时足够长的时间,才能保证转运完成
费力,不推荐使用
单次转换,非扫描模式实现多通道
每次触发转换之前,手动更改列表中,第一个位置的通道
-
修改AD.c和AD.h
#ifndef __AD_H #define __AH_H void AD_Init(void); uint16_t AD_GetValue(uint8_t ADC_Channel); #endif
#include "stm32f10x.h" // Device header void AD_Init(void){ RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN; //开启四个通道,分别挂载不同的传感器 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); ADC_InitTypeDef ADC_InitStruct; ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; ADC_InitStruct.ADC_ScanConvMode = DISABLE; ADC_InitStruct.ADC_NbrOfChannel = 1; ADC_Init(ADC1,&ADC_InitStruct); ADC_Cmd(ADC1,ENABLE); 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,ADC_SampleTime_55Cycles5); ADC_SoftwareStartConvCmd(ADC1,ENABLE); while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET);//5.6us return ADC_GetConversionValue(ADC1); }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "AD.h" int16_t AD0,AD1,AD2,AD3; float V_value; 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){ 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_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); } }
2.DMA直接存储器存取
-
DMA直接存储器存取:
-
DMA可以提供高速数据传输,无须CPU干预,节省了CPU的资源
-
外设和存储器(一般使用硬件触发)
-
存储器与存储器之间(一般使用软件触发)
-
12个独立可配置的通道,每个通道都支持软件触发和特定的硬件触发
-
DMA1(7个通道)
-
DMA2(5个通道)
-
STM32F103C8T6 的DMA资源
-
DMA1(7个通道)
-
没有DMA2
-
存储器映像
-
ROM:只读存储器,是一种非易失性、掉电不丢失的存储器
-
程序存储器Flash:主闪存,存储C语言编译后的程序代码,是下载程序的位置,也是程序运行的开始
-
系统存储器:存储BootLoader,是程序芯片出厂自动写入的,一般不允许修改。也可以用于串口下载。
-
选项字节:存储一些独立于程序代码的配置参数,在ROM最后面,下载程序的时候,可以选择不刷新选项字节的内容,可以保持选项字节配置不变
主要存储:Flash读保护、Flash写保护、看门狗等
-
-
RAM:随机存储器,是一种易失性、掉电丢失的存储器
-
SRAM:存储运行过程中的临时变量,也就是程序中定义变量、数组、结构体的地方
电脑的内存条
-
外设寄存器:存储各个外设的配置参数,是初始化外设最终读写的东西
-
内核外设寄存器:存储内核各个外设的配置参数,比如NVIC、SysTick
内核外设和其他外设,不是一个厂家设计,因此地址分开
-
存储器地址范围:0x0000 0000 ~ 0xFFFF FFFF
32位寻址范围,最大支持4GB的存储器,但是STM32内部存储器是KB级别,地址使用率不足1%
DMA框图
-
可以把Cortex-M3看做CPU,剩下都是存储器(Flash、SRAM、各个外设寄存器)
寄存器是连接软件和硬件的桥梁。软件读写寄存器,就是相当于控制硬件执行
-
DMA数据转运:都可以看作是,从某个地址取内容,放到另一个地址。
-
总线矩阵:高效有条理的访问存储器
-
主动单元:是总线矩阵的左端,拥有存储器的访问权
-
内核:CPU
-
DCode(数据总线):专门访问Flash
总线直接访问Flash,无论是CPU还是DMA都是只读的,不能写入;
如果DMA的目的地址填写Flash区域,转运时就会出错。
可以配置Flash接口控制器,先对Flash进行按页擦除,然后进行写入。
-
系统总线:访问外设
-
DMA总线:DMA1、DMA2、以太网MAC,都各自有一条DMA总线
-
DMA1:有7个通道
-
多个通道:每个通道都可以设置,转运数据的源地址和目的地址,就可以独立工作
-
仲裁器:只有一个DMA总线,使用仲裁器控制访问优先级
-
AHB从设备:DMA外设接在AHB总线上,CPU可以通过这条线路进行配置
DMA是总线矩阵的主动单元,可以读写各种存储器;也是AHB总线上的被动单元,CPU可以通过这条线路进行配置
DMA2:有5个通道
以太网MAC:不用管
被动单元:是总线矩阵的右端,它们的存储器只能被左边的主动单元读写
DMA请求:各个外设作为DMA硬件触发源,可以向DMA发出硬件触发信号
DAM基本结构
-
外设寄存器站点:外设寄存器
-
存储器站点:Flash、SRAM、
手册中的存储器,一般特指Flash、SRAM,不包含外设寄存器
-
DMA数据转运:可以配置转运方向
-
外设到存储器
-
存储器到外设
-
存储器到存储器
不允许SRAM到Flash,也不允许Flash到Flash,因为Flash只读
如果进行数据转运:需要把其中一个存储器地址,放到外设这个站点。
-
Flash到SRAM
-
SRAM到SRAM
站点参数:起始地址、数据宽度、地址是否自增
起始地址:决定数据从哪里来到哪里去
数据宽度:指定一次转运,要按多大数据宽度进行
字节Byte 半字HalfWord 字Word 8位 16位 32位
地址是否自增:下一次转运,是否要把地址移动到下一个位置
传输计数器:自减计数器,可以指定转运几次
写传输计数器时,必须【先关闭】DMA,再进行写入
自动重装器:传输计数器减到0之后,是否要自动恢复最初的值
单次模式:不重装
复制数组,一轮结束
循环模式:重装,执行一轮工作后,立即开启下一轮工作
ADC扫描模式+连续转换,为了配合ADC,DMA需要使用循环模式
DMA触发控制:决定DMA需要在什么时机进行转运
触发模式由M2M参数决定
-
硬件触发:M2M设置为0,触发源可以选择多种,一般与外设有关的转运,需要一定的时机,达到时机时,传入信号触发DMA转运
触发源:ADC、串口、定时器等
一定时机:ADC转换完成、串口接收到数据、定时时间到等
-
软件触发:M2M设置为1,以最快的速度,连续不断的触发DMA,争取早日把计数器清零,完成这一轮循环;
不能和循环模式同时使用。否则DMA会停不下来。
用于存储器到存储器的转运,软件启动不需要时机,而且尽快完成任务
开关控制:DMA使能后,DMA就准备就绪,可以进行运转。
DMA_Cmd函数
DMA运行条件:
-
DMA使能
-
计数器>0
-
触发源有触发信号
DMA请求
-
这是DMA触发控制部分的结构图
-
图中有7个通道,每个通道都有一个数据选择器,可以选择软件或硬件触发
-
EN不是数据选择器的控制位,而是当前通道的使能位,是控制开关
-
M2M才是数据选择控制位,选择硬件触发或软件触发
-
外设请求信号:每个通道的硬件触发源都是不同的,需要特定的硬件触发,软件相同,只有开启DMA输出才可以硬件触发
比如:
用ADC1触发,就必须选择通道1
ADC_DMACmd()
用TM2通道3,可以触发通道1
TIM_DMACmd()
用TIM2更新事件触发,必须选择通道2
-
7个触发源进入仲裁器,进行优先级判断,最终产生内部DMA1的请求
默认优先级:通道号越小,优先级越高
也可以手动配置优先级
数据宽度与对齐
-
数据转移过程中,需要配置数据宽度参数
-
数据宽度相同:正常一个个转运
-
数据宽度不同:查表
数据宽度与对齐:
数据宽度相同:正常移动
源数据<目标数据 (高位补零)
源端宽度 | 目标宽度 | 写入 |
---|---|---|
8位 | 8位 | 12345678 -> 12345678 |
8位 | 16位 | 1 2 3 4 5 6 7 8-> 01 02 03 04 05 06 07 08 |
8位 | 32位 | 12345678 -> 0001 0002 0003 0004 0005 0006 0007 0008 |
源数据>目标数据 (舍弃高位)
源端宽度 | 目标宽度 | 写入 |
---|---|---|
16位 | 8位 | A1 A2 A3 A4 A5 A6 A7 A8 -> 12345678 |
32位 | 8位 | ABC1 ABC2 ABC3 ABC4 ABC5 ABC6 ABC7 ABC8->12345678 |
1.DMA数据转运
数据转运与DMA
![]()
外设地址:DataA数组首地址
存储器地址:DataB数组首地址
数据宽度:都是uint8_t,都是8位字节传输
地址自增:DataA[i] –> DataB[i] ,因此两个地址都需要自增
方向参数:外设站点转运到存储器站点。
传输计数器:一共7个数据,因此设置为7
自动重装:暂时不需要
触发选择部分:软件触发,是存储器到存储器的转运,不需要等待硬件时机,可以尽快完成
DMA使能:调用DMA_Cmd
数据转运结束:传输计数器自减到0,DMA停止,转运完成
复制转运:转运后的DataA数据不会消失
小测试:对不同的变量,进行取地址操作,配合地址映射表,查看对应存储位置
![]()
const常量:会存放在Flash中,节省SRAM空间,随机分配
当程序中,出现大量无需更改的数据,可以使用常量
如:OLED字库
一般变量:存放在SRAM中,随机分配
寄存器地址:外设寄存器区域,地址固定,手册可以查到
步骤:
RCC开启DMA时钟
调用DMA_Init 初始化各个参数,一个结构体即可
外设站点与存储器站点:起始地址、数据宽度、地址是否自增
方向
传输计数器
是否需要重装
选择触发源
通道优先级
开关控制DMA_Cmd,给指定通道使能
如果使用硬件触发,要调用XXX_DMACmd,开启触发信号输出
如果需要DMA的中断,就要调用DMA_ITConfig,开启中断输出,配置NVIC
给传输计数器赋值时,要使DMA失能后,再写入,然后使能
-
新建MyDMA.h和MyDMA.c,由于与硬件无关,所以放到System文件夹
#ifndef __MYDMA_H #define __MYDMA_H void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size); void MyDMA_Transform(void); #endif
#include "stm32f10x.h" // Device header //存储,需要转运的,数据个数。 uint16_t MyDMA_Size; void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size){ MyDMA_Size = Size; //RCC开启DMA设备1 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE); //初始化DMA DMA_InitTypeDef DMA_InitStruct; //外设地址:地址A DMA_InitStruct.DMA_PeripheralBaseAddr = AddrA; //数据宽度:Byte (8位) DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //地址自增:开启自增 DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //存储器地址:地址B DMA_InitStruct.DMA_MemoryBaseAddr = AddrB; //数据宽度:Byte (8位) DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //地址自增:开启自增 DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; //方向:外设作为源地址(外设->存储器) DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; //传输计数器,填写转运的数据个数 DMA_InitStruct.DMA_BufferSize = Size; //自动重装:不重装,普通模式; //注意:自动重装的循环模式,不能在软件模式触发下使用,否则DMA会停不下来。 DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; //触发选择:Enable==1,是软件触发 DMA_InitStruct.DMA_M2M = DMA_M2M_Enable; //优先级选择,因为只有一个,不重要,随便设置 DMA_InitStruct.DMA_Priority = DMA_Priority_Medium; //初始化DMA1的1号通道 DMA_Init(DMA1_Channel1,&DMA_InitStruct); //DMA运行条件:1. DMA使能;2. 计数器>0;3. 触发源有触发信号 //暂时失能,不开启运转,用下面函数手动开启运转 DMA_Cmd(DMA1_Channel1,DISABLE); } //手动开启运转 void MyDMA_Transform(){ //为了写入传输计数器的值,需要关闭使能才可以写入。 DMA_Cmd(DMA1_Channel1,DISABLE); //写入传输计数器,从初始化得到的,转运的数据个数 DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_Size); //开启使能,运行DMA1转运 DMA_Cmd(DMA1_Channel1,ENABLE); //等待转运完成,获取DMA1转运完成标志位 while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); //手动清除标志位。 DMA_ClearFlag(DMA1_FLAG_TC1); }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "MyDMA.h" //需要转运的数据 uint8_t DataA[] = {0x01,0x02,0x03,0x04}; //转运数据的目的地 uint8_t DataB[] = {0,0,0,0}; int main(void){ OLED_Init(); //初始化DMA MyDMA_Init((uint32_t)DataA,(uint32_t)DataB,4); //屏幕显示DataA的地址 OLED_ShowString(1,1,"DataA:"); OLED_ShowHexNum(1,7,(uint32_t)DataA,8); //屏幕显示DataB的地址 OLED_ShowString(3,1,"DataB:"); OLED_ShowHexNum(3,7,(uint32_t)DataB,8); while(1){ //更新DataA和DataB的显示,同时更改DataA的值,使其每次都有变化 for(int i = 0;i < 4;i++){ DataA[i]++; OLED_ShowHexNum(2,1 + 3*i,DataA[i],2); OLED_ShowHexNum(4,1 + 3*i,DataB[i],2); } //延时1s后开始转运 Delay_ms(1000); //开启转运 MyDMA_Transform(); //输出转运后的结果:DataB的变化对比 for(int i = 0;i < 4;i++){ OLED_ShowHexNum(2,1 + 3*i,DataA[i],2); OLED_ShowHexNum(4,1 + 3*i,DataB[i],2); } Delay_ms(1000); } }
2.DMA+AD多通道
ADC扫描模式与DMA
![]()
触发一次开始:7个通道依次进行AD转换,结果都放到ADC_DR数据寄存器
DMA数据转运:在每个单独的通道转换完成后,进行一个DMA数据转运,并且目的地址(存放结果的位置)进行自增,防止数据覆盖
地址自增:外设地址不自增,存储器地址自增
传输方向:外设站点到存储器站点
传输计数器:一共7个通道,因此计数7次
自动重装
ADC单次扫描:DMA的传输计数器可以不自动重装,一轮停止
ADC连续扫描:DMA可以使用自动重装,ADC启动下一轮转换时,DMA也启动下一轮转运。二者同步工作
触发选择:需要与ADC单个通道转换完成同步,选择ADC硬件触发,虽然不产生任何标志位和中断,但是会产生DMA请求去触发DMA转运。
-
修改AD.c和AD.h
#ifndef __AD_H #define __AH_H extern uint16_t AD_Value[4]; void AD_Init(void); void AD_GetValue(void); #endif
#include "stm32f10x.h" // Device header uint16_t AD_Value[4]; void AD_Init(void){ RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); //开启ADC1的四个端口,分别放到菜单的1,2,3,4里 ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_55Cycles5); ADC_InitTypeDef ADC_InitStruct; ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStruct.ADC_ContinuousConvMode = DISABLE; //启用扫描模式,可以扫描菜单 ADC_InitStruct.ADC_ScanConvMode = ENABLE; //扫描菜单的长度1,2,3,4,共4个 ADC_InitStruct.ADC_NbrOfChannel = 4; ADC_Init(ADC1,&ADC_InitStruct); DMA_InitTypeDef DMA_InitStruct; //ADC数据寄存器,只能存储一个数据,因此每次都需要转运出来 //外设地址:ADC1的DR寄存器 DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //数据长度 16位 = 半字 必须配置成半字,不然读取不到 DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //不自增 DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //存储器地址:数据转运的地方:数组 DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)AD_Value; //半字 DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //自增,依次存储到数组的每个空间 DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; //方向:从外设转运到存储器,外设为源地址 DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; //转运计数器:4次 DMA_InitStruct.DMA_BufferSize = 4; //正常模式,不自动重装 DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; //硬件触发 DMA_InitStruct.DMA_M2M = DMA_M2M_Disable; //优先级 DMA_InitStruct.DMA_Priority = DMA_Priority_Medium; DMA_Init(DMA1_Channel1,&DMA_InitStruct); //ADC使能 ADC_Cmd(ADC1,ENABLE); //ADC1启用DMA,只能使用通道1 ADC_DMACmd(ADC1,ENABLE); //DMA的通道1使能【可以每次转换再使能,这里可以随意填写】 DMA_Cmd(DMA1_Channel1,DISABLE); //校验 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)==SET); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1) == SET); } //转运,重装 void AD_GetValue(){ //重装计数器 DMA_Cmd(DMA1_Channel1,DISABLE); DMA_SetCurrDataCounter(DMA1_Channel1,4); DMA_Cmd(DMA1_Channel1,ENABLE); //软件触发ADC ADC_SoftwareStartConvCmd(ADC1,ENABLE); //等待转运完成,标志位 while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); DMA_ClearFlag(DMA1_FLAG_TC1); }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "AD.h" int main(void){ OLED_Init(); AD_Init(); for(int i = 1;i<=4;i++){ OLED_ShowString(i,1,"ADx:"); OLED_ShowChar(i,3,'0' + i); } while(1){ AD_GetValue(); for(int i = 1;i<= 4;i++){ OLED_ShowNum(i,5,AD_Value[i - 1],4); } Delay_ms(100); } }
使用连续扫描
-
修改AD.c和AD.h
#ifndef __AD_H #define __AH_H extern uint16_t AD_Value[4]; void AD_Init(void); #endif
#include "stm32f10x.h" // Device header uint16_t AD_Value[4]; void AD_Init(void){ RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_55Cycles5); ADC_InitTypeDef ADC_InitStruct; ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //连续模式 ADC_InitStruct.ADC_ContinuousConvMode = ENABLE; //扫描模式 ADC_InitStruct.ADC_ScanConvMode = ENABLE; ADC_InitStruct.ADC_NbrOfChannel = 4; ADC_Init(ADC1,&ADC_InitStruct); DMA_InitTypeDef DMA_InitStruct; DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)AD_Value; DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStruct.DMA_BufferSize = 4; //DMA循环模式 DMA_InitStruct.DMA_Mode = DMA_Mode_Circular; //硬件触发 DMA_InitStruct.DMA_M2M = DMA_M2M_Disable; DMA_InitStruct.DMA_Priority = DMA_Priority_Medium; DMA_Init(DMA1_Channel1,&DMA_InitStruct); ADC_Cmd(ADC1,ENABLE); ADC_DMACmd(ADC1,ENABLE); DMA_Cmd(DMA1_Channel1,ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)==SET); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1) == SET); //ADC的触发 ADC_SoftwareStartConvCmd(ADC1,ENABLE); }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "AD.h" int main(void){ OLED_Init(); AD_Init(); for(int i = 1;i<=4;i++){ OLED_ShowString(i,1,"ADx:"); OLED_ShowChar(i,3,'0' + i); } while(1){ for(int i = 1;i<= 4;i++){ OLED_ShowNum(i,5,AD_Value[i - 1],4); } Delay_ms(100); } }
7.STM32通信接口
-
通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
-
通信协议:制定通信的规则,通信双方按照协议规则进行数据收发
STM32F103C8T6全部支持
双工模式:
全双工:一般通信有两根通信线,发送线路和接受线路互不影响 (电话)
半双工:通信有一根数据线,不能同时发送和接收。(对讲机)
单工:只能从一个设备到另一个设备,不能反向 (广播)
时钟特性:
同步:用单独的时钟线,接收方可以在时钟的指引下进行采样
异步:没有时钟线,双方需要约定好一个采样频率,同时需要帧头帧尾进行采样位置对齐
电平特性:
单端信号:高低电平是对于GND的电压差,单端通信的双方需要共地,把GND接在一起
差分信号:靠两个引脚的电压差传输信号,不需要GND,但是协议里面有些地方需要单端信号,所以需要共地,差分信号抗干扰能力强,速度快、距离远。
设备:
点对点:点对点通信 (单独谈话,一对一)
多设备:可以在总线上挂在多个设备 (讲话,一对多,需要寻址确定对象)
1.串口通信
异步通信
-
特点:应用广泛、成本低、使用容易、通信简单,可以实现两个设备互相通信
通信:
单片机与单片机
单片机与电脑
单片机与各个模块
-
硬件电路
-
如果两设备都有独立供电模块,VCC可以独立供电不接在一起;如果其中一个没有供电,那么可以接在一起,有电的向没电的供电,注意供电电压要求。
-
GND必须接在一起,共地
-
TX与RX:交叉连接,发送与接收相连;只接一根,就是单工通信
-
注意电平标准,可以使用电平转换芯片,相同电平才能通信
直接从控制器里输出的信号,一般是TTL电平
-
-
电平标准:是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系。
-
TTL电平:+3.3V或+5V表示1,0V表示0
最远几十米,课程基于TTL电平讲解,需要使用其他电平,可以加入电平转换芯片
-
RS232电平: -3V~-15V表示1, +3V~+15V表示0
一般在大型机器上使用,由于环境恶劣,静电干扰较大,因此电压和波动范围较大
最远几十米
-
RS485电平:两线压差+2V~+6V表示1, -2V~-6V表示0
差分信号,抗干扰能力强,通信距离可以达到上千米
-
-
串口参数及时序
-
波特率:串口通信的速率
异步通信,双方要约定好通信速率
单位是,码元/s (或波特),表示每秒传输码元的个数
比特率:每秒传输的比特数,单位bit/s (或bps)
在二进制调制的前提下,一个码元就是一个bit,此时波特率 = 比特率
在单片机串口通信中,基本都是二进制调制,所以经常混用
如果是多进制表示,就不想等
-
起始位:标志一个数据帧的开始,固定为低电平
空闲状态,没有数据传输的时候,是高电平
起始位:发送数据的时候,先发送一个起始位,必须是低电平,也就是产生一个下降沿
-
数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
低位先发送
-
校验位:用于数据验证,根据数据位计算得来
根据数据位计算出来
奇偶校验位:可以数据是否传输错误,若出错了可以选择丢弃或重传,只能保证一定程度上的数据校验,要求更高可以选择CRC校验
奇偶校验方式:如果有两位同时出错,那么校验不出来
无校验:不需要校验位,波形图是左边的
奇校验:包括数据位于校验位,会出现奇数个1
偶校验:包括数据位于校验位,会出现偶数个1
-
停止位:用于数据帧间隔,固定为高电平
停止位:为下一个起始位做准备,把引脚恢复为高电平
停止位可以配置长度,可以把数据分割更宽
-
时序图
-
串口发送数据的格式,是串口协议规定的格式
-
串口中,每一个字节都装载在数据帧中。
-
每个数据帧,都有:起始位、数据位、停止位,共8位组成,可以在数据位后面加上奇偶校验位,就是9位数据
-
USART(Universal Synchronous/Asynchronous Receiver/Transmitter):通用同步/异步收发器
-
引脚
-
TX (Transmit Exchange):数据发送脚,也叫TXD
-
RX (Receive Exchange):数据接收脚,也叫RXD
-
GND:共地
特性:全双工、异步、单端、点对点
USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里
自带波特率发生器,最高达4.5Mbits/s
可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)
可选校验位(无校验/奇校验/偶校验)
支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN
同步模式:多了个时钟CLK输出
硬件流控制:防止发送太快,导致无法处理的现象,可以反馈接收者的状态,是否可以接收
DMA转运数据
智能卡
IrDA:红外发光管与红外接收管,依靠闪烁红外光通信
LIN:局域网通信协议
STM32F103C8T6 USART资源: USART1、 USART2、 USART3
USART1:是APB2设备
USART2、 USART3:是APB1设备
USART框图
-
左上角引脚部分
-
TX:发送引脚,连接到发送移位寄存器
-
RX:接收引脚,连接到接收移位寄存器
-
SW_RX、IRDA_OUT、IRDA_IN:是智能卡和IrDA的通信引脚,我们不使用这些协议,不需要管
数据寄存器:
DR寄存器:程序上表现为一个寄存器DR,实际上是两个寄存器
-
发送数据寄存器TDR:用于发送,只写,写入后硬件自动检查,当前移位寄存器是否有数据正在移位,如果没有,写入的数据就立刻,全部移动到发送移位寄存器,准备发送
-
接收数据寄存器RDR:用于接收,只读;当标志位RXNE(接收数据寄存器非空)置1,我们就可以把数据读走了
移位寄存器:正好对应串口协议的波形数据位
-
发送移位寄存器:把一个字节数据,一位一位的移出去;当数据从发送寄存器移动到移位寄存器时,会置一个TXE标志位(发送寄存器空),如果标志位置1,就可以在TDR写下一个数据
-
接收移位寄存器:把一个字节数据,一位一位的读进来;当8位数据接收完成,就会把完整8位数据,整体转移到RDR中,同时会设置一个标志位RXNE(接收数据寄存器非空),当标志位置1,我们就可以把数据读走了。
控制器
-
发送器控制:发送器控制发送移位寄存器,向右移位,一位一位的把数据输出到TX引脚
-
接收器控制:接收器控制接收移位寄存器,一位一位的读取RX高电平,先放在最高位,然后右移
-
硬件数据流控(流控):发送太快,接收端来不及处理,只能丢弃或覆盖的时候,流控可以避免这个问题
【n是低电平有效】,一般不使用流控功能。
nRTS与nCTS需要交叉连接
-
nRTS:请求发送,是输出脚,告诉发送端,当前能不能接受数据
-
nCTS:清除发送,是输入端,用于接收他人的nRTS信号。
SCLK控制:用于产生同步的时钟信号,配合发送移位寄存器输出的,这个寄存器每移位一次,同步时钟电平就跳变一个周期;
时钟支持输出,不支持输入;一般不使用时钟功能。
时钟可以兼容其他协议:串口+时钟,和SPI协议很像
时钟可以做自适应波特率:不确定发送设备的波特率时,可以测量这个时钟周期,计算得到波特率
唤醒单元:实现串口挂载多设备
串口一般是点对点通信,只支持两个设备互相通信;一般不使用唤醒单元功能
多设备:在一条总线上,可以接多个从设备,每个设备分配个地址,想要跟某个设备通信,就线进行寻址,确定通信对象,再进行数据收发。
USART中断控制:中断申请位,就是状态寄存器这里的各种标志位,配置中断是否可以通向NVIC
TXE:发送寄存器空标志位
RXNE:接收存储器非空标志位
波特率发生器:对APB时钟分频,得到发送移位和接受移位的时钟
时钟输入:fPCLKx (x = 1,2)
USART1挂载在APB2,所以就是时钟fPCLK2的时钟,一般是72MHz
其他USART挂载在APB1,所以就是时钟fPCLK1的时钟,一般是36MHz
USARTDIV时钟分频:除以一个USARTDIV的分频系数,分为整数部分与小数部分,因为有些波特率除不尽,因此小数支持小数点后4位
发送器时钟与接收器时钟:分频后频率再除以16,就可以得到发送器时钟与接收器时钟,通向控制部分
TE:TE = 1,发送器波特率控制使能,发送部分的波特率有效
RE:RE = 1,接收器波特率控制使能,接收部分的波特率有效
USART基本结构
-
波特率发生器:分频,产生约定的通信速率
-
时钟:时钟来源是PCLK2 或 PCLK1,经过波特率发生器分频后,产生的时钟通向控制器。
-
发送接收控制器:
-
发送控制器:控制发送移位
-
发送移位寄存器:使用发送数据寄存器TDR和发送移位寄存器,将数据一位一位移出去
移位寄存器右移,低位先行
-
GPIO:一位一位的数据,通过GPIO复用输出,输出到TX引脚,产生串口协议规定的波形
-
发送数据寄存器TDR:我们在软件层面,进行写入DR,发送数据
接收控制器:控制接收移位
接收移位寄存器:使用接收数据寄存器RDR和接收移位寄存器,将数据一位一位移动到,接收移位寄存器
移位寄存器右移,低位先行
GPIO:RX引脚波形,通过GPIO输入,在接收移位控制器的控制下,将数据一位一位移动到,接收移位寄存器。
接收数据寄存器RDR:当移完一帧数据后,数据会统一转运到接收数据寄存器,转移的同时,会置一个RXNE标志位,判断是否接收到数据,这个标志位也可以申请中断
开关控制:配置完成后,用Cmd启动外设
数据帧
-
字长选择:可以组成四种发送方式
-
9位字长:
-
有校验:一般使用
-
无校验
8位字长
有校验
无校验:一般使用
波形:
空闲:高电平
接收数据:
起始位:低电平,数据帧开始
当输入电路,侦测到数据起始位以后,就会以波特率的频率,连续采样一帧数据
同时,从起始位开始,采样位置就要对齐到位的正中间,只要第一位对齐了,那么后面就都对齐了
数据位:数据0~7位
可能的奇偶校验位:数据第8位,可以配置无校验、奇校验、偶校验
停止位:高电平,数据帧结束
停止位时长:一般使用1个停止位
1个停止位
1.5个停止位
2个停止位
0.5个停止位
时钟:同步时钟输出功能
每个数据位中间,都有时钟上升沿。
时钟频率和数据速率相同
接收端可以在时钟上升沿进行采样,就可以精准定位每一位数据
LBCL:时钟最后一位,可以桶LBCL位,控制是否输出
时钟极性、相位可以通过配置寄存器配置
采样时钟细分:以波特率16倍的频率进行采样,也就是1位数据,允许采样16次
起始位采样:
对1位数据的进行16次采样:在下降沿后的
第3、5、7次进行采样
第8、9、10次进行采样【正中间】
要求每3位中,至少有2位至少都是0;
如果全是0:没有噪声
如果2个0,一个1:有噪声,在状态寄存器里置一个NE噪声标志位
如果超过2个1:不算检测到起始位,忽略前面的检测,重新开始捕获下降沿
后续接收数据位时,都在【8,9,10】次进行采样
数据采样:
-
采样分析【8,9,10】次采样
-
其中全是0或全是1:说明收到了数据0或1
-
有0有1:按照2:1的规则来,谁多认为收到了谁,同时噪声标志位NE置1
空闲帧:从头到尾都是1,局域网协议使用,串口不使用
断开帧:从头到尾都是0,局域网协议使用,串口不使用
波特率发生器:本质就是分频器
-
波特率由BRR里的分频系数DIV确定
-
分频系数DIV:分为整数部分和小数部分,可以实现更细腻的分频
-
波特率计算公式: 波特率 = { f_{PCLK2/1} \over (16 * DIV)}
因为内部还有一个16被波特率的采样时钟,直接除会得到16倍的波特率
波特率9600 = 72M / (16 * DIV) ,可以求出DIV = 468.75 = 1 1101 0100.11 写入分频器需要转化为二进制。
CH340G模块原理图:USB转串口
-
USB:
-
GND:接地
-
UD+、UD-:USB通信协议的数据线
-
VCC+5V:标准5V供电,为整个芯片供电;通过稳压管电路进行降压,得到VCC+3.3V。
-
CH340G芯片:
-
CH340G芯片,将USB协议,转化为串口协议
-
TXD、RXD:串口协议
CON6:
使用排针印出来TXD和RXD
VCC+5V与VCC+3.3V:由USB的VCC+5V和VCC+3.3V接入,输出进行供电
CH340G_VCC:接入CH340G芯片的CH340G_VCC,是CH340G芯片的电源输入脚,可以使用跳线帽
选择【5,6】:CH340G芯片供电5V,TTL电平5V
选择【4,5】:CH340G芯片供电3.3V,TTL电平3.3V
STM32供电3.3V,因此用跳线帽插上4,5脚
优先确保供电正确,通电电平无法一致,小问题
指示灯与滤波:
PWR:电源指示灯
TXD:传输数据时闪烁
RXD:接收数据时闪烁
数据包:
-
数据包分类:
-
HEX数据包:含有包头包尾,发送传输原始数据,解析简单,适合模块发送原始数据。
-
固定包长:可以避免数据与包头包尾发生冲突
包头包尾不一定全需要,但是冲突问题更严重
-
可变包长:与包头和包尾不会冲突的情况,可以选用
-
-
文本数据包:含有包头包尾,数据经过编码译码,适合输入指令人机交互场景,解析效率低
-
固定包长:
-
可变包长:字符选择更多,基本上不担心包头包尾冲突问题
-
-
-
数据包发送:数组、字符串等,调用函数直接发送
-
数据包接收:状态机,处理包头、数据、包尾等不同状态,执行不同操作,且进行状态的合理转移
串口下载
FlyMcu串口下载:绿色版,直接打开
-
连接和电路,保证串口电路可以和【USART1】连接,进行串口通信
芯片的串口下载,只适配了USART1,也就是GPIO_PIN_9和GPIO_PIN_10
-
在keil中,点击魔术棒按钮->OutPut->Create HEX file ->编译代码,可以在Objects目录下找到.hex文件
-
单片机配置BOOT引脚,让STM32执行BootLoader程序,将配置BOOT0 = 0引脚的跳线帽,配置为BOOT0 = 1,然后按下复位键。
-
BootLoader程序:也叫自举程序,在【系统存储器】中,可以程序自我更新、串口下载,接收到USART1的数据刷新到程序存储器。
-
BOOT引脚启动配置
-
跳线帽替代:
-
STM32一键下载电路,使用CH340G的RTS和DTR两个输出引脚(流控引脚当做普通引脚使用),来控制BOOT0和RST。
软件下方的复位选项,根据复位电路来自行选择;如果没有下载电路,就随意选择无所谓
-
使用“编程后执行”选项,取消勾选“选项字节区”,更改跳线帽后,刷入程序可以直接运行。不过复位后失效,可以把跳线帽接回来。
-
打开FlyMcu串口下载软件,进入串口和波特率选择,载入.hex文件,点击开始编程就可以下载程序
单片机的BOOT0引脚换回来,点击复位键,即可正常运行程序
读Flash:点击读Flash按钮,选择存放路径后,可以将芯片中的程序读出来
程序是.bin格式,记录了STM32从0800开始存储的程序数据
清除芯片:点击清除芯片按钮,可以把程序擦除,所有数据变成FF,读取信息回把序列号信息读出来
点击“设定选项字节等”按钮,选择“STM32F1选项配置”:设置好后勾选选项字节区的写入框,只能在下载程序过程中,顺便写入
读保护:开启读保护,防止程序被偷走
阻止读出后,下载程序会失败。
取消读保护,会清空芯片程序。
硬件选项字节:看门狗、停机模式和待机模式不产生复位、
用户数据字节:不论程序如何变化,选项字节内容可以不变,可以存储一些参数;也可以使用上位机方便修改,作为可供用户配置的参数
写保护:可以对Flash的每几个页,单独进行写保护,不想在下载的时候被擦除,可以设置写保护锁起来;如果写入保护区,会出错;而且不支持单独写某块字节,会死循环。
ST-LINK Utility:需要安装。
-
只需要连接好ST-LINK,可以不接串口,跳线帽恢复、复位
-
点击连接按钮
-
Target ->Option Bytes -> 可以配置 选项字节配置,可以单独更新选项自己内容。
-
ST-LINK固件更新:ST-LINK -> Firmware updata ->Connect ->提示重启,重插即可 ->yes更新
1.串口通信
RCC开启时钟:GPIOA,USART
GPIO初始化,TX配置为复用输出,RX配置为输入
配置USART,直接使用一个结构体,就可以配置所有参数
如果只需要发送功能,就可以直接开启USART,初始化结束
如果需要接收功能,需要配置中断,在开启USART之前,加上ITConfig,NVIC配置
初始化完成后,要发送数据或接收数据,直接调用函数;获取发送或接收状态,调用获取标志位函数
-
创建Serial.c和Serial.h文件,放在Hardware文件夹下
#ifndef __SERIAL_H #define __SERIAL_H #include <stdio.h> void Serial_Init(void); void Serial_Send(uint8_t msg); void Serial_SendArray(uint8_t* arr,uint16_t len); void Serial_SendString(char* str); void Serial_SendNumber(uint32_t number,uint16_t len); void Serial_Printf(char* format, ...); #endif
#include "stm32f10x.h" // Device header #include <stdio.h> #include <stdarg.h> void Serial_Init(){ //开启RCC时钟,USART1和GPIOA RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //初始化GPIO_PIN_9作为TX串口输出引脚,复用推挽输出模式 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); //初始化USART USART_InitTypeDef USART_InitStruct; //波特率 USART_InitStruct.USART_BaudRate = 9600; //不使用流模式 USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //作为Tx输出串口输出端口,如果同时使用发送可以用或| USART_InitStruct.USART_Mode = USART_Mode_Tx; //奇偶校验位 USART_InitStruct.USART_Parity = USART_Parity_No; //停止位长度 USART_InitStruct.USART_StopBits = USART_StopBits_1; //数据帧长度:8 / 9 字节 USART_InitStruct.USART_WordLength = USART_WordLength_8b; USART_Init(USART1,&USART_InitStruct); //开启USART串口通信 USART_Cmd(USART1,ENABLE); } //发送字节,USART_SendData中msg是16bit,但是只能发送8bit,高位被清空,为了兼容 void Serial_Send(uint8_t msg){ USART_SendData(USART1,msg); //判断发送标志位,是否发送成功,自动清空标志位 while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET); } //发送数组,需要传递数组长度 void Serial_SendArray(uint8_t* arr,uint16_t len){ for(uint16_t i = 0;i<len;i++){ Serial_Send(arr[i]); } } //发送字符串,由于字符串\0结尾,因此不需要传递长度 void Serial_SendString(char* str){ for(int i =0;str[i] != '\0' ; i++){ Serial_Send(str[i]); } } //幂次运算,返回 x^y,用于计算移动位置,10^y uint32_t Serial_Pow(uint32_t x,uint32_t y){ uint32_t res = 1; while(y--){ res*=x; } return res; } //发送数字,需要传递数字长度 void Serial_SendNumber(uint32_t number,uint16_t len){ for(int i = len - 1; i >= 0;i--){ //倒叙循环,先发送高位: //发送原理:清除低位,然后这个高位就是个位,%10取个位发送 Serial_Send('0' + (number / Serial_Pow(10,i))%10); } } //重定向printf函数,调用printf,可以使用串口发送 //需要使用魔法棒,配置为Use MicroLIB,只对某个USARTx有效 int fputc(int ch,FILE *f){ Serial_Send(ch); return ch; } //重写Printf,对于所有USARTx串口均有效,因为使用字符串发送的方法。 //可变参数的使用 void Serial_Printf(char* format, ...){ char str[100]; va_list arg; va_start(arg,format); vsprintf(str,format,arg); va_end(arg); Serial_SendString(str); }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Serial.h" int main(void){ OLED_Init(); Serial_Init(); // \r\n 是换行符。 uint8_t arr[]= {'\r','\n','A','B','C',0x45,'\r','\n'}; char* str = "nihaoa\r\n"; Serial_Send('A'); Serial_SendArray(arr,8); Serial_SendString(str); Serial_SendNumber(123456,6); Serial_Printf("\r\nHelloWorld = %d\r\n",10); while(1){ } }
UTF-8:魔法棒按钮,在Editor中的Encoding中选择UTF-8,然后在C/C++的Misc Controls 写入参数,–no-multibyte-chars
GB2312:魔法棒按钮,在Editor中的Encoding中选择GB2312,然后回到编译器,先删掉中文、关掉文件,再打开,看到文字变成宋体,编码才改变成功。
解决编码问题,保证输入端和接收端编码格式相同,修改编码的时候,注意备份,防止丢失。
2.串口发送与接收
-
修改Serial.c和Serial.h,加入接收功能
#ifndef __SERIAL_H #define __SERIAL_H #include <stdio.h> void Serial_Init(void); void Serial_Send(uint8_t msg); void Serial_SendArray(uint8_t* arr,uint16_t len); void Serial_SendString(char* str); void Serial_SendNumber(uint32_t number,uint16_t len); void Serial_Printf(char* format, ...); uint8_t Serial_GetRxData(void); uint8_t Serial_GetRxFlag(void); //这个是使用判断标志位的方法接收 uint8_t Serial_Receive1(void); #endif
#include "stm32f10x.h" // Device header #include <stdio.h> #include <stdarg.h> //存放接收数据 uint8_t Serial_RxData; //接收数据标志位,表示Serial_RxData有数据,未被读取 uint8_t Serial_RxFlag; void Serial_Init(){ RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); //使用发送端口GPIO_PIN_10,设置为输入模式,高电平有效 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); USART_InitTypeDef USART_InitStruct; USART_InitStruct.USART_BaudRate = 9600; USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //配置发送与接收同时使用 USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_InitStruct.USART_Parity = USART_Parity_No; USART_InitStruct.USART_StopBits = USART_StopBits_1; USART_InitStruct.USART_WordLength = USART_WordLength_8b; USART_Init(USART1,&USART_InitStruct); //配置接收中断,接收到消息就可以触发 USART_ITConfig(USART1,USART_IT_RXNE,ENABLE); //配置NVIC优先级 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStruct.NVIC_IRQChannelCmd =ENABLE ; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStruct); USART_Cmd(USART1,ENABLE); } void Serial_Send(uint8_t msg){ USART_SendData(USART1,msg); while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET); } void Serial_SendArray(uint8_t* arr,uint16_t len){ for(uint16_t i = 0;i<len;i++){ Serial_Send(arr[i]); } } void Serial_SendString(char* str){ for(int i =0;str[i] != '\0' ; i++){ Serial_Send(str[i]); } } uint32_t Serial_Pow(uint32_t x,uint32_t y){ uint32_t res = 1; while(y--){ res*=x; } return res; } void Serial_SendNumber(uint32_t number,uint16_t len){ for(int i = len - 1; i >= 0;i--){ Serial_Send('0' + (number / Serial_Pow(10,i))%10); } } int fputc(int ch,FILE *f){ Serial_Send(ch); return ch; } void Serial_Printf(char* format, ...){ char str[100]; va_list arg; va_start(arg,format); vsprintf(str,format,arg); va_end(arg); Serial_SendString(str); } //使用判断标志位的方法接收数据,放在while循环中,一直判断 uint8_t Serial_Receive1(){ while(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == RESET); return USART_ReceiveData(USART1); } //接收到数据就置一个标志位,读取标志位后,清空标志位 uint8_t Serial_GetRxFlag(){ if(Serial_RxFlag == 1){ Serial_RxFlag = 0; return 1; } return 0; } //返回接收的数据,首先判断标志位 uint8_t Serial_GetRxData(){ return Serial_RxData; } //接收数据触发中断 void USART1_IRQHandler(){ if(USART_GetITStatus(USART1,USART_IT_RXNE)== SET){ //数据存到变量 Serial_RxData = USART_ReceiveData(USART1); //标志位置1,表示接收到数据 Serial_RxFlag = 1; //清空标志位,可以省略,成功USART_ReceiveData读取到数据后,会自动清空 USART_ClearITPendingBit(USART1,USART_IT_RXNE); } }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Serial.h" uint8_t RxData; int main(void){ OLED_Init(); Serial_Init(); OLED_ShowString(1,1,"RxData:"); while(1){ //判断标志 if(Serial_GetRxFlag() == 1){ //接收数据 RxData = Serial_GetRxData(); OLED_ShowHexNum(1,8,RxData,2); //数据回显 Serial_Send(RxData); } } }
3.串口收发HEX数据包
-
修改Serial.c和Serial.h,加入接收和发送HEX数据包功能
#ifndef __SERIAL_H #define __SERIAL_H #include <stdio.h> extern uint8_t Serial_TxPacket[4]; extern uint8_t Serial_RxPacket[4]; void Serial_Init(void); void Serial_Send(uint8_t msg); void Serial_SendArray(uint8_t* arr,uint16_t len); void Serial_SendString(char* str); void Serial_SendNumber(uint32_t number,uint16_t len); void Serial_Printf(char* format, ...); void Serial_SendPacket(void); uint8_t Serial_GetRxFlag(void); #endif
#include "stm32f10x.h" // Device header #include <stdio.h> #include <stdarg.h> //接收数据包 uint8_t Serial_RxPacket[4]; //发送数据包 uint8_t Serial_TxPacket[4]; uint8_t Serial_RxFlag; void Serial_Init(){ RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); USART_InitTypeDef USART_InitStruct; USART_InitStruct.USART_BaudRate = 9600; USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_InitStruct.USART_Parity = USART_Parity_No; USART_InitStruct.USART_StopBits = USART_StopBits_1; USART_InitStruct.USART_WordLength = USART_WordLength_8b; USART_Init(USART1,&USART_InitStruct); USART_ITConfig(USART1,USART_IT_RXNE,ENABLE); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStruct.NVIC_IRQChannelCmd =ENABLE ; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStruct); USART_Cmd(USART1,ENABLE); } void Serial_Send(uint8_t msg){ USART_SendData(USART1,msg); while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET); } void Serial_SendArray(uint8_t* arr,uint16_t len){ for(uint16_t i = 0;i<len;i++){ Serial_Send(arr[i]); } } void Serial_SendString(char* str){ for(int i =0;str[i] != '\0' ; i++){ Serial_Send(str[i]); } } uint32_t Serial_Pow(uint32_t x,uint32_t y){ uint32_t res = 1; while(y--){ res*=x; } return res; } void Serial_SendNumber(uint32_t number,uint16_t len){ for(int i = len - 1; i >= 0;i--){ Serial_Send('0' + (number / Serial_Pow(10,i))%10); } } int fputc(int ch,FILE *f){ Serial_Send(ch); return ch; } void Serial_Printf(char* format, ...){ char str[100]; va_list arg; va_start(arg,format); vsprintf(str,format,arg); va_end(arg); Serial_SendString(str); } //发送数据包,固定长度,拼接协议即可 void Serial_SendPacket(void){ Serial_Send(0xFF); Serial_SendArray(Serial_TxPacket,4); Serial_Send(0xFE); } //接收标志位 uint8_t Serial_GetRxFlag(){ if(Serial_RxFlag == 1){ Serial_RxFlag = 0; return 1; } return 0; } //接收数据中断 void USART1_IRQHandler(){ //静态变量:只有第一次执行的时候进行初始化操作。 //记录接收状态机的状态 static uint8_t RxState = 0; //记录接收数据包的数据位置 static uint8_t pRxPacket = 0; //获取终端标志,接收到数据 if(USART_GetITStatus(USART1,USART_IT_RXNE)== SET){ //读取当前数据 uint8_t RxData = USART_ReceiveData(USART1); //状态机RxState = 0,接收数据包头0xFF if(RxState == 0){ //接收到数据包头 if(RxData == 0xFF){ //切换下一个状态:接受数据 RxState = 1; //接收数据的指针清空,数据从0开始 pRxPacket = 0; } //状态机:RxState = 1,接收数据,固定4位 }else if(RxState == 1){ //数据存放到,接受数据包中,每次接受一个,一共接受4个 Serial_RxPacket[pRxPacket++] = RxData; //当接收到4个数据。就转换下一个状态:接收包尾 if(pRxPacket >= 4){ RxState = 2; } //状态机:RxState = 2,接收包尾:0xFE }else if(RxState == 2){ //接收到包尾 if(RxData == 0xFE){ //状态设置为接受包头,可以接受下一个数据 RxState = 0; //接收数据状态置1,表示可以读出了 Serial_RxFlag = 1; } } //清空中断标志位,可以省略,读取数据自动清除。 USART_ClearITPendingBit(USART1,USART_IT_RXNE); } }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Serial.h" #include "Key.h" uint8_t KeyNum; int main(void){ OLED_Init(); Serial_Init(); Key_Init(); OLED_ShowString(1,1,"TxPacket:"); OLED_ShowString(3,1,"RxPacket:"); Serial_TxPacket[0] = 0x01; Serial_TxPacket[1] = 0x02; Serial_TxPacket[2] = 0x03; Serial_TxPacket[3] = 0x04; while(1){ //按下按键,就发送数据。 KeyNum = Key_GetNum(); if(KeyNum == 1){ Serial_SendPacket(); OLED_ShowHexNum(2,1,Serial_TxPacket[0],2); OLED_ShowHexNum(2,4,Serial_TxPacket[1],2); OLED_ShowHexNum(2,7,Serial_TxPacket[2],2); OLED_ShowHexNum(2,10,Serial_TxPacket[3],2); for(int i = 0;i < 4;i++){ Serial_TxPacket[i]++; } } //检测接收标志,显示数据。 if(Serial_GetRxFlag() == 1){ OLED_ShowHexNum(4,1,Serial_RxPacket[0],2); OLED_ShowHexNum(4,4,Serial_RxPacket[1],2); OLED_ShowHexNum(4,7,Serial_RxPacket[2],2); OLED_ShowHexNum(4,10,Serial_RxPacket[3],2); } } }
程序可能会出现数据交叉的现象,由于读取一半的时候,来了新的数据。
-
可以在读取数据包时加入判断,只有读取完成后,才能接收下一个数据包
-
对于连续性的数据,可以不处理。
-
大多数情况下不处理。
-
4.串口收发文本数据包
-
修改Serial.c和Serial.h,加入接收和发送文本数据包功能
#ifndef __SERIAL_H #define __SERIAL_H #include <stdio.h> extern uint8_t Serial_TxPacket[]; extern char Serial_RxPacket[]; extern uint8_t Serial_RxFlag; void Serial_Init(void); void Serial_Send(uint8_t msg); void Serial_SendArray(uint8_t* arr,uint16_t len); void Serial_SendString(char* str); void Serial_SendNumber(uint32_t number,uint16_t len); void Serial_Printf(char* format, ...); void Serial_SendPacket(void); uint8_t Serial_GetRxFlag(void); #endif
#include "stm32f10x.h" // Device header #include <stdio.h> #include <stdarg.h> //可以存放的字符串长度 char Serial_RxPacket[100]; uint8_t Serial_TxPacket[4]; uint8_t Serial_RxFlag; void Serial_Init(){ RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); USART_InitTypeDef USART_InitStruct; USART_InitStruct.USART_BaudRate = 9600; USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_InitStruct.USART_Parity = USART_Parity_No; USART_InitStruct.USART_StopBits = USART_StopBits_1; USART_InitStruct.USART_WordLength = USART_WordLength_8b; USART_Init(USART1,&USART_InitStruct); USART_ITConfig(USART1,USART_IT_RXNE,ENABLE); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStruct.NVIC_IRQChannelCmd =ENABLE ; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStruct); USART_Cmd(USART1,ENABLE); } void Serial_Send(uint8_t msg){ USART_SendData(USART1,msg); while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET); } void Serial_SendArray(uint8_t* arr,uint16_t len){ for(uint16_t i = 0;i<len;i++){ Serial_Send(arr[i]); } } void Serial_SendString(char* str){ for(int i =0;str[i] != '\0' ; i++){ Serial_Send(str[i]); } } uint32_t Serial_Pow(uint32_t x,uint32_t y){ uint32_t res = 1; while(y--){ res*=x; } return res; } void Serial_SendNumber(uint32_t number,uint16_t len){ for(int i = len - 1; i >= 0;i--){ Serial_Send('0' + (number / Serial_Pow(10,i))%10); } } int fputc(int ch,FILE *f){ Serial_Send(ch); return ch; } void Serial_Printf(char* format, ...){ char str[100]; va_list arg; va_start(arg,format); vsprintf(str,format,arg); va_end(arg); Serial_SendString(str); } void Serial_SendPacket(void){ Serial_Send(0xFF); Serial_SendArray(Serial_TxPacket,4); Serial_Send(0xFE); } uint8_t Serial_GetRxFlag(){ if(Serial_RxFlag == 1){ Serial_RxFlag = 0; return 1; } return 0; } void USART1_IRQHandler(){ static uint8_t RxState = 0; static uint8_t pRxPacket = 0; if(USART_GetITStatus(USART1,USART_IT_RXNE)== SET){ char RxData = USART_ReceiveData(USART1); if(RxState == 0){ //读取包头:@ //检测Serial_RxFlag标志,把读写数据严格分开,防止数据交叉 //只有读完数据,才可以写;因此写太快可能会丢失数据包,可使用缓冲区。 if(RxData == '@' && Serial_RxFlag == 0){ RxState = 1; pRxPacket = 0; } }else if(RxState == 1){ //数据读取到\r\n(也就是回车)的时候,切出读取数据的状态,转化为读取包尾状态 if(RxData == '\r'){ RxState = 2; }else{ //不是包尾标志,就算是正常读数据 Serial_RxPacket[pRxPacket++] = RxData; } }else if(RxState == 2){ //读取到\r\n,就是数据包结束 if(RxData == '\n'){ RxState = 0; //字符串后面跟上\0,表示字符串结束 Serial_RxPacket[pRxPacket] = '\0'; //标志位表示,Rx中有数据,可以读取。 Serial_RxFlag = 1; } } USART_ClearITPendingBit(USART1,USART_IT_RXNE); } }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Serial.h" #include "LED.h" #include <string.h> uint8_t KeyNum; int main(void){ OLED_Init(); Serial_Init(); LED_Init(); OLED_ShowString(1,1,"TxPacket:"); OLED_ShowString(3,1,"RxPacket:"); while(1){ //获取标志位,当读取完成的时候,才可以清空标志位。 //读写严格分开,因此不能发送数据太快。否则会丢包。 if(Serial_RxFlag == 1){ OLED_ShowString(4,1," "); OLED_ShowString(4,1,Serial_RxPacket); //字符串匹配,接收到LED_ON进行点灯。 if(strcmp(Serial_RxPacket,"LED_ON") == 0){ LED1_ON(); Serial_SendString("LED_ON_OK\r\n"); //清空当前行,防止脏数据 OLED_ShowString(2,1," "); OLED_ShowString(2,1,"LED_ON_OK"); }else if(strcmp(Serial_RxPacket,"LED_OFF") == 0){ //关灯操作 LED1_OFF(); Serial_SendString("LED_OFF_OK\r\n"); OLED_ShowString(2,1," "); OLED_ShowString(2,1,"LED_OFF"); }else{ //读取到其他数据包,显示错误命令,不执行操作 Serial_SendString("ERROR_CMD\r\n"); OLED_ShowString(2,1," "); OLED_ShowString(2,1,"ERROR_CMD"); } Serial_RxFlag = 0; } } }
2.I2C通信
同步通信
-
I2C(Inter IC Bus):是由Philips公司开发的一种通用数据总线,也叫I2 C (I方C)
MPU6050模块:姿态测量
OLED模块:显示字符、图片等信息
AT24C02:存储器模块
DS3231:实时时钟模块
-
两根通信线:
-
SCL(Serial Clock):串行时钟线,同步时序
-
SDA(Serial Data):串行数据线
-
GND:共地
-
特点:
-
同步:使用同步时序、降低对硬件的依赖,稳定性比异步高
-
半双工:半双工模式,一根线兼具发送和接收,最大化利用资源
-
带数据应答
-
支持总线挂载多设备
-
一主多从:单片机作为主机,主导I2C通信,挂载在I2C总线上的所有外部模块都是从机,
从机在被主机点名后,才能控制I2C总线,不可以在未经允许的情况下去碰I2C总线,防止冲突
课堂模型1:老师讲课,只有点名的同学才可以讲话
-
多主多从:多个主机,在总线上任何一个模块都可以主动跳出来,但还需要进行时钟同步,协议比较复杂
课堂模型2:老师讲课,同学突然站起来打断,说所有同学听我指挥;
但是总线同一个时间只能有一个人主导,否则发生冲突,这时需要I2C协议仲裁,优先级更高的获得总线控制权,另一个变成从机
-
硬件电路(一主多从)
-
CPU:我们的单片机,作为主线的主机,权利很大
-
可以对SCL线完全控制
-
空闲状态下,主机可以主动发起对SDA的控制;
-
只有在从机发送数据和从机应答时,主机才会转交SDA的控制权给从机
-
-
被控ICx :挂载在I2C总线上的从机,从机权利比较小
可以是姿态传感器、OLED、存储器、时钟模块等
-
任何时刻,从机在都只能被动读取SCL时钟线信号
-
从机不允许主动发起对SDA的控制,只有在主机发送读取从机的命令后,或者从机应答时,从机才可以短暂的获取SDA控制权
-
接线要求:
-
所有I2C设备的SCL连在一起,SDA连在一起
-
SCL接线:
-
一主多从模式,主机对SCL有绝对控制权,所以主机可以配置为推挽输出
但是仍然采用开漏加上拉输出模式;因为多主机下有“线与”特性。
-
从机SCL可以配置为,浮空输入或上拉输入
-
SDA接线:主机和从机都会在输入和输出之间切换,协调不好容易短路
-
I2C总线,禁止所有设备输出强上拉的高电平
-
采用外置弱上拉电阻,加上开漏输出的电路结构
-
IC设备中,有强下拉电路,可以变成低电平和浮空状态。
-
为了避免高电平造成的引脚浮空,可以在总线外SCL和SDA上,格接入弱上拉电阻
-
设备的SCL和SDA均要配置成开漏输出模式
开漏输出模式:输出低电平、或浮空,没有高电平。
-
根据杆子模型分析,在这个电路中,杆子不会处于一个被同时强拉或强推的状态,
-
避免引脚模式的频繁切换,开漏加弱上拉模式,同时兼具了输入和输出的功能
输出:进行拉杆子或放手,操控杆子变化
输入:直接放手,观察杆子高低
开漏模式下,输出高电平,就相当于断开引脚;因此在输入之前,可以直接输出高电平,不需要再切换输入模式
-
”线与“ 现象:有设备输出低电平,那么总线就处于低电平;只有所有设备输出高电平,总线才处于高电平。
可以实现多主机模式下,时钟同步和时钟仲裁
I2C时序基本单元
-
起始条件:SCL从高电平期间,SDA从高电平切换到低电平
-
终止条件:SCL从高电平期间,SDA从低电平切换到高电平
-
只有主机,才可以产生起始和终止,不允许从机产生;因此总线空闲状态时,从机不允许触碰总线,否则是多机模型。
-
期间触发中断,会保持时钟信号不变,保护现场
-
发送一个字节:低电平主机发数据,高电平从机读数据
-
SCL低电平期间,主机将数据位依次放到SDA线上(高位先行)
SCL低电平,SDA准备数据。
主机在接收之前,需要先释放SDA
-
然后释放SCL,从机将在SCL高电平期间读取数据位
SCL转换为高电平,开始发送SDA数据
一般上升沿触发
-
所以SCL高电平期间SDA不允许有数据变化
发送数据期间,不允许数据变化
-
依次循环上述过程8次,即可发送一个字节
SCL转换为低电平,发送完成。
-
-
接收一个字节:低电平从机发数据,高电平主机读数据
-
SCL低电平期间,从机将数据位依次放到SDA线上(高位先行)
-
然后释放SCL,主机将在SCL高电平期间读取数据位
一般下降沿触发
-
所以SCL高电平期间SDA不允许有数据变化
-
依次循环上述过程8次,即可接收一个字节
(主机在接收之前,需要释放SDA)
-
-
应答机制:发送一位,接收一位,这一位作为应答
-
发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
-
接受应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
I2C时序:每个设备地址(7位)必须不同,一般设备地址的最后几位可以在电路中改变
释放:变为高电平
拉低:变为低电平
连续读写:停止位之前,重复多次发送字节的时序;注意指针每次+1。
主机应答:读操作的时候,如果将SDA应答设置为非应答,应答高电平,就表示即将结束。只在最后一次应答时设置。
-
指定地址写:对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)
指定设备(Slave Address):从机地址确定
指定地址(Reg Address):某个设备内部的寄存器地址
指定数据(Data):在这个寄存器写入的数据
时序图:
-
起始条件:SCL高电平期间,拉低SDA
-
发送字节:从机地址 + 读写位,8位
-
设备地址7位,不能重复
-
第8位,也就是最低位,表示读写位
0写、1读。
-
-
应答位RA:1位数据的应答位;
-
紧跟设备地址、读写位其后,接收从机的应答位RA;
-
主机先要释放SDA,电平应该回弹到高电平
-
但是协议规定,应答的时候,从机立刻拉低SDA,应答结束后放开
-
因此,波形为低电平信号,表示从机产生了应答
-
应答结束后,从机释放SDA产生高电平信号,交出SDA控制权
发送字节:从机设备可以自己定义第二个字节和后续字节的用途
可以是寄存器地址、指令控制字等
这里发送0x19地址,表示要操作这个地址下的寄存器
应答:从机应答,SDA表现为低电平,主机收到应答位0。
发送字节:这里表示,写入0x19地址下寄存器的数据,写入0xAA。
应答:接收应答位,主机非应答(SA),不把SDA拉低,即将结束
停止条件:如果主机不需要传输,产生停止条件,
先拉低SDA,为后续SDA上升沿做准备
然后释放SCL
再释放SDA
当前地址读:对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
当前地址指针:从机中的所有寄存器,被分配到了一个线性区域中,有一个指针变量,指示着其中一个寄存器,上电默认为0地址。
每次读/写一个字节后,指针就会移动到下一个位置。
主角没有指定读哪个寄存器的地址,从机就会返回指针指向的寄存器的值
【无法指定读的地址,因此使用不多】
时序图:
起始条件:SCL高电平期间,拉低SDA,产生起始条件
发送一个字节:进行从机的寻址和指定读写标志位,1读。
从机应答位:位0,表示从机接收到第一个字节
读命令:
主机不能继续发送,SDA控制权交给从机
主机调用接收一个字节的时序,进行接收操作
主机在SCL高电平期间,依次读取了8位,就接收到了从机发送的一个字节数据0x0F
指向从机指针指向的寄存器地址,也就是上一个寄存器的,下一个寄存器
指定地址读:对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)
实现原理:复合格式
把指定地址写,写入数据操作,去掉
指定地址写,把前面的指定地址的时序,追加到当前地址读时序前面
指定地址写,只指定了地址,没来的及写,后面使用当前地址读 =>指定地址读
时序图:
起始条件:SCL高电平期间,拉低SDA,产生起始条件
发送一个字节:进行寻址,指定从机地址和读写标志。【1101000 0写】
从机应答:
发送一个字节,用来指定地址,写入到从机的地址指针里
从机应答
停止条件:可以加上也可以省略,加上的话就凑成了一个完整时序。【图中省略】
重复起始条件(Sr):另起一个时序
发送一个字节:重新寻址,指定从机地址和读写标志。【1101000 1读】
从机应答
主机接收一个字节数据,位置已经指定
MPU6050芯片
PS:产品说明书,芯片的功能描述、电气参数、引脚定义、硬件电路等
RM:寄存器映像,芯片内部寄存器描述、寄存器每一位的详细解读,代码实现细节等
-
MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数
姿态角:也叫欧拉角,进准稳定的欧拉角,需要这几种传感器相互配合,数据融合,取长补短。
数据融合算法:互补滤波、卡尔曼滤波等【惯性导航领域、姿态解算】
应用场景:平衡车、飞行器等需要检测自身姿态的场景
飞机模型:欧拉角,就是飞机机身,相对于初始3个角的夹角,飞机的姿态
俯仰Pitch:飞机的机头,下倾或上仰,这个轴的夹角叫俯仰
滚转Roll:飞机的机身,左翻滚或右翻滚,这个轴的夹角叫滚转
偏航Yaw:飞机的机身保持水平,机头向左或向右转向,这个轴的夹角叫偏航
-
通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景
-
3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度
加速度计,可以测量加速度,具有静态稳定性,不具有动态稳定性
-
3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度
一般可以测量角度。
但是这个芯片内部的陀螺仪,只能测量角速度,不能测量角度
飞椅模型:旋转飞椅,转的越快,椅子飞的越远,测量对向两个椅子飞起来的距离/夹角,就可以得出中间轴的角速度,对角速度进行积分,可以得到角度。
陀螺仪具有动态稳定性,不具有静态稳定性:
物体静止时,角速度因为噪声无法完全归零,经过积分的不断累积,计算的角度产生缓慢的漂移,时间越长误差越大,但是不会受到物体运动的影响。
-
3轴的磁场传感器:测量X、Y、Z轴的磁场强度
如果芯片中,再集成一个3轴的磁场传感器,测量XYZ轴磁场强度,就是9轴姿态传感器;
-
1轴的气压传感器:测量高度信息,海拔越高气压越低。
再集成一个气压传感器(高度信息、海拔越高气压越低),就是10轴。
MPU6050参数
16位ADC采集传感器的模拟信号,量化范围:-32768~32767
分为两个字节存储
加速度计满量程选择:±2、±4、±8、±16(g)
满量程范围:就是变化范围
物体运动剧烈,满量程可以选择大一些,防止加速度或角速度超出量程;
物体运动平缓,满量程可以选择小一些,分辨率更大,测量更细腻;
陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)
可配置的数字低通滤波器
可以配置寄存器选择对输出数据进行低通滤波,针对抖动剧烈的信号
可配置的时钟源
提供时钟
可配置的采样分频
控制时钟分频系数,控制AD转换的快慢
I2C从机地址:
0xD0是I2C从机地址:最低位加上读写控制位。
0x68是I2C从机地址:使用的时候左移1位,变成0xD0。
1101000x(AD0=0)
1101001x(AD0=1)
MPU6050硬件电路
-
MPU6050芯片:
-
SCL、SDA:模块内置两个上拉电阻,接线时候直接接入GPIO口即可
-
MDP单元:进行数据融合和姿态解算
-
VDD供电:2.375~3.46V,不能直接接入5V。
CON1:
XCL、XDA:主机I2C通信引脚,扩展芯片功能,MPU6050主机接口可以直接访问这些扩展芯片的数据
六轴姿态传感器不够用的时候,可以扩展
通常外接磁力计、气压计等
如果不需要姿态解算功能,可以直接接到SCL和SDA总线上
AD0引脚:从机地址的最低位,可以修改0 / 1
INT引脚:中断输出引脚,可以配置芯片内部事件,触发中断
数据准备好、I2C主机错误等
实用小功能:自由落体检测、运动检测、零运动检测等
不需要可以不配置
LDO:供电逻辑,稳压器,扩大芯片供电范围
输入端VCC_5V:可以在3.3V~5V之间
LDO:经过3.3V稳压器,输出稳定的3.3V,给芯片供电
LED:电源指示灯,有电就亮
如果有稳定的3.3V电源,可以不需要
MPU6050框图
-
时钟CLKIN和CLKOUT:时钟输入输出脚,一般使用内部时钟,因此不需要使用
外部时钟需要额外电路
-
传感器:本质上是可变电阻,分压后输出模拟电压
-
XYZ加速度计
-
XYZ陀螺仪
-
稳定传感器
ADC:传感器的模拟电压,经过ADC模数转换。
数据寄存器:模数转换的数据,存放到16位数据寄存器中
数据自动转换,我们直接读取寄存器即可。
自测单元:启用后,芯片内部会模拟一个外力施加在传感器上,外力会导致传感器数据比平时大一些
自测响应:使能自测和失能自测,获取两个数据,相减后得到自测响应
自测响应在一定范围内(手册上有范围),说明芯片正常
电荷泵:也叫充电泵,是一种升压电路,CPOUT需要外接电容
串联正接,电容充电;充满电后,串联反接,电压提升到2倍。
自动进行、
寄存器:
中断状态寄存器:可以控制内部的事件到引脚输出
FIFO:先进先出寄存器,可以对数据流进行缓存。
配置寄存器:可以对内部各个电路进行配置
传感寄存器:也就是数据寄存器,存储了各个传感器的数据
工厂校准:内部传感器都进行了校准
DMP数字运动处理器:芯片内部自带的姿态解算的硬件算法
可以配合官方DMP库,可以进行姿态解算
FSYNC:帧同步
通信接口部分:
I2C和SPI通信接口,用于和STM32进行通信
主机I2C通信接口,用于和MPU6050扩展设备通信
接口旁路选择器:一个开关。
拨到上面:STM32可以控制所有设备,包括MPU6050和扩展设备
拨到下面:STM可以控制MPU6050,MPU6050控制扩展设备
供电:所有寄存器上电默认值都是0x00;
除了107号寄存器(电源管理器1)默认0x40,新品上电默认睡眠模式,操作前先解除。
117号寄存器默认(ID号)0x68
I2C外设【硬件I2C】:节省软件资源、效率高
-
STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担
硬件电路自动翻转电平,只需要写入控制寄存器CR、数据寄存器DR就可以实现协议,也需要读取状态寄存器SR,获取电路状态
-
功能指标
-
支持多主机模型
-
支持7位/10位地址模式
-
使用两个字节:11110开头,说明使用10位地址
支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)
支持DMA
兼容SMBus协议
STM32F103C8T6 硬件I2C资源:I2C1、I2C2
软件I2C没有资源限制
I2C框图
SDA:数据控制部分,半双工,数据收发使用同一组寄存器
-
DATA REGISTER:数据寄存器,我们需要发送的一个字节数据,写入DR寄存器。
-
数据移位寄存器:当寄存器没有移位时,就可以把DR寄存器的数据,送到数据移位寄存器
数据从数据寄存器移到数据移位寄存器时,状态寄存器TXE位(发送寄存器空)置1
数据从数据移位寄存器移到数据寄存器时,状态寄存器RXNE位(接收寄存器非空)置1
-
数据控制:数据移位寄存器,把数据移位到数据控制寄存器,一旦移位完成,就可以无缝衔接,继续发送
-
控制寄存器:控制收发
-
比较器、地址寄存器:多主机模式下,STM32作为从机模式使用;如果使用一主多从模式,STM32就不会作为从机,因此不需要使用
-
自身地址寄存器:STM32不进行通信时,就是从机,从机可以被召唤,因此有个地址。
-
双地址寄存器:STM32支持同时响应两个从机地址
-
比较器:STM32作为从机,在被寻址时,通过比较器判断地址,响应外部主机的召唤。
-
-
帧错误校验PEC计算:STM32的数据校验模块,发送多字节的数据帧时,硬件可以自动执行CRC校验计算,校验位附加在数据帧后面;同时接收到数据帧后,STM32硬件也可以自动进行校验判断。校验错误,硬件自动置校验错误标志位。
CRC是常见的数据校验算法,经过前面数据进行各种运算,然后得到一个字节的校验位。
SCL时钟控制:写入对应的位,电路就会执行对应功能
-
控制逻辑电路:黑盒子
-
中断:可以开启中断
-
DMA请求与响应:多数据时,可以配合DMA提高效率
时钟控制寄存器:操作对应的位,可以执行对应的功能
控制寄存器:写入控制寄存器,可以对整个电路进行控制
状态寄存器:可以得知电路工作状态
I2C基本结构
-
移位寄存器:I2C高位先行,向左移位
-
GPIO:使用复用输入和复用输出
-
数据控制器:黑盒
-
开关控制:I2C_Cmd
操作流程:4个流程,但是从机模式暂时不考虑
主机发送
-
主发送:
-
7位地址:起始条件后的1个字节是寻址,和1位读写位
-
10位地址:起始条件后的2个字节是寻址;第一个字节的前5位是标志位11110,以及2位地址,和1位读写位;第二个字节是8位地址
7位地址流程:
初始化之后,总线默认空闲状态,STM32默认从模式,写入控制寄存器,产生起始条件
控制寄存器CR1中,START标志位置1,可以产生起始条件,硬件自动清除。
起始、
STM32由从模式,转变为主模式
产生EV5事件:包含多个标志位的改变,可以理解为一个大标志位
EV5事件:SB=1 (状态寄存器SR1中的标志位,置1表示起始条件已经发送;软件读取后,写数据寄存器的操作,可以自动清除标志位);
从机地址、
发送从机地址,数据写入数据寄存器DR中,硬件电路会自动把这一个字节转到移位寄存器中,发送到I2C总线上;
应答、
硬件会自动接收应答位,并判断;没有应答,硬件会置应答失败的标志位。这个标志位可以申请中断。
寻址完成后会发生EV6事件
EV6事件:ADDR标志位置1(主模式下代表地址发送结束)
然后发送EV8_1事件
EV8_1事件:TxE = 1,移位寄存器空,数据寄存器空。这时需要我们写入数据寄存器DR进行数据发送。
由于移位寄存器空,所以DR会立刻转到移位寄存器进行发送,产生EV8事件
数据1、
发送时,数据线写入数据寄存器,当移位寄存器空的时候,数据转到移位寄存器发送
EV8事件:TxE = 1,移位寄存器非空,数据寄存器空,写入DR寄存器将会清除该事件
在数据1发送的时候,数据2已经在数据寄存器DR中等待。
应答、
对方是否接收到;产生EV8事件
数据2、
在数据1发送的时候,已经在数据寄存器中等待
产生EV8事件后,移位寄存器非空,开始移动数据2到移位寄存器,发送数据2;数据寄存器空,数据3进入数据寄存器,准备发送。
应答、
……、
数据N、
最后一个数据发送,没有下一个数据
应答、
发送完成,产生EV8_2事件
EV8_2事件:BTF=1(字节发送结束标志位),移位寄存器空,数据寄存器空(TxE= 1),请求设置停止位。
停止。
数据可由各大厂商自己规定
MPU6050规定:数据1指定寄存器地址、数据2指定寄存器地址的数据……后续就是从指定寄存器开始,依次往后写数据
主机接收
-
流程:
-
起始条件、
-
EV5事件(起始条件已发送)、
-
寻址、
-
应答、
-
EV6事件(寻址已完成)、
-
数据1(数据通过移位寄存器输入)、
-
EV6_1事件(没有事件标志、只适用接收1个字节的情况)、
-
应答,产生EV7
-
EV7事件:移位寄存器已经完成一个字节移入,转移到数据寄存器,RxNE标志位置1表示数据寄存器非空,清除读DR事件
等待EV7事件,读取DR就可以收到数据
-
……
产生EV7_1
-
最后数据N
在接收最后一个字节之前,也就是EV7_1事件,要提前把ACK置0,STOP置1;设置晚了,时序上面就会多一个字节
-
应答:产生EV7
-
EV7_1:控制寄存器ACK = 0,设置终止条件请求。
-
终止
软件I2C与硬件I2C波形对比
软件I2C波形可能不标准,但由于是同步时序,因此不影响通信,可以容忍不标准波形
-
电平变化
-
波形相同、对应数据相同
时钟
硬件I2C波形更加规整,时钟周期占空比比较一致
软件I2C,由于加入了延时,占空比可能不规整,但影响不大
读写:SCL下降沿写,上升沿读
读:时钟上升沿,可以读SDA数据。
写:时钟下降沿,写入SDA数据,便于下次读
硬件I2C,读写数据都是紧贴SCL变化上升沿/下降沿,
软件I2C,读写数据会有延时,SCL变化后,数据不会紧贴SCL上升沿/下降沿变化,而是等了一小会才进行读写操作
1.软件I2C读写MPU6050
建立I2C.c和I2C.h通信层模块,初始化GPIO、6个时序基本单元(起始、终止、发送一个字节、接收一个字节、发送应答、接收应答)
把SCL和SDA初始化为开漏输出模式
把SCL和SDA置为高电平
建立MPU6050的MPU6050.c和MPU6050.h模块,基于I2C通信模块,实现指定地址读、指定地址写、写寄存器对芯片进行配置、读寄存器得到传感器数据
在main.c中,调用MPU6050模块,初始化、拿到数据、显示数据
-
创建文件MyI2C.c和MyI2C.h,放到Hardware文件夹下
#ifndef __MYI2C_H #define __MYI2C_H void MyI2C_Init(void); void MyI2C_Start(void); void MyI2C_Stop(void); void MyI2C_SendByte(uint8_t Byte); uint8_t MyI2C_ReceiveByte(void); void MyI2C_SendAck(uint8_t AckBit); uint8_t MyI2C_ReceiveAck(void); #endif
#include "stm32f10x.h" // Device header #include "Delay.h" //写SCL:将GPIO_Pin_10作为SCL的输出【1为浮空,0为低电平】 void MyI2C_W_SCL(uint8_t BitValue){ GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue); //延时10微秒,产生一个信号。 Delay_us(10); } //写SDA:将数据从GPIO_Pin_11,模拟电平信号输出 void MyI2C_W_SDA(uint8_t BitValue){ GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue); Delay_us(10); } //读SDA:读数据,接受从GPIO_Pin_11外部,传输过来的数据,读取电平信号。 uint8_t MyI2C_R_SDA(void){ uint8_t BitValue; BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11); Delay_us(10); return BitValue; } //初始化 void MyI2C_Init(){ //开启GPIOB_PIN_10和GPIOB_PIN_11,设置为开漏输出模式 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStructure); //都设置为高电平,初始化完成。 GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11); } //协议开始 void MyI2C_Start(void){ /* I2C开始: 1.时钟线和数据线,都处于高电平状态 2.当数据线,先拉低到低电平,然后时钟线也拉低到低电平,说明协议开始。 */ MyI2C_W_SDA(1); MyI2C_W_SCL(1); MyI2C_W_SDA(0); MyI2C_W_SCL(0); } //协议停止 void MyI2C_Stop(void){ /* I2C停止: 0.接收应答标志为:1,表示无应答 1.时钟线和数据线,都处于拉低的低电平状态 > 时钟线不需要手动拉低,因为读取数据后会自动拉低 2.时钟线,先释放为高电平,然后数据线也释放到高电平,说明协议结束。 */ MyI2C_W_SDA(0); MyI2C_W_SCL(1); MyI2C_W_SDA(1); } //发送协议 void MyI2C_SendByte(uint8_t Byte){ /* I2C发送协议: 1.时钟线拉低到低电平,然后等待数据传输 > 因为发送结束,会自动拉低时钟线,所以不需要手动拉低。 > 数据高位优先传输,一共8位数据,先发送最高位。 2.数据传输的时间,时钟线释放发送数据后,再拉低时钟线,表示发送结束 3.读取结束后,数据线可以准备下一个数据的传输。 */ for(uint8_t i = 0;i < 8;i++){ //高位有优先 MyI2C_W_SDA(Byte & (0x80 >> i)); MyI2C_W_SCL(1); MyI2C_W_SCL(0); } } //接收协议 uint8_t MyI2C_ReceiveByte(void){ uint8_t Byte = 0x00; /* I2C接收 1. 主机释放SDA,允许从机把数据放到SDA 2. 主机释放SCL,允许主机读取数据 3. 读取数据,高位先行 4. 读取数据后,把SCL拉低,允许从机放入数据 */ MyI2C_W_SDA(1); for(uint8_t i = 0;i < 8;i++){ MyI2C_W_SCL(1); if(MyI2C_R_SDA() == 1){ Byte |= (0x80 >> i); } MyI2C_W_SCL(0); } return Byte; } //发送应答:发送字节简化版,发送一位 void MyI2C_SendAck(uint8_t AckBit){ MyI2C_W_SDA(AckBit); MyI2C_W_SCL(1); MyI2C_W_SCL(0); } //接收应答:接收字节简化版,接收一位 uint8_t MyI2C_ReceiveAck(void){ uint8_t AckBit; MyI2C_W_SDA(1); MyI2C_W_SCL(1); AckBit |= MyI2C_R_SDA(); MyI2C_W_SCL(0); return AckBit; }
-
新建MPU6050_Reg.h文件,放到Hardware文件夹下,作为MPU6050寄存器定义重命名。
#ifndef __MPU6050_REG_H #define __MPU6050_REG_H // 重定义的MPU6050寄存器名 寄存器实际地址 //采样率分频寄存器:决定数据输出的快慢,值越小,越快。【0x09也就是10分频】 #define MPU6050_SMPLRT_DIV 0x19 //配置寄存器:---、外部同步【0不需要】、数字低通滤波器【110最平滑的滤波、看需求】 #define MPU6050_CONFIG 0x1A //陀螺仪配置寄存器:自测使能【000不自测】、满量程选择【11最大量程、看需求】、--- #define MPU6050_GYRO_CONFIG 0x1B //加速度配置寄存器:自测使能【000不自测】、满量程选择【11最大量程、看需求】、高通滤波器【000不需要】 #define MPU6050_ACCEL_CONFIG 0x1C //加速度X的高8位寄存器 #define MPU6050_ACCEL_XOUT_H 0x3B //加速度X的低8位寄存器 #define MPU6050_ACCEL_XOUT_L 0x3C //加速度Y的高8位寄存器 #define MPU6050_ACCEL_YOUT_H 0x3D //加速度Y的低8位寄存器 #define MPU6050_ACCEL_YOUT_L 0x3E //加速度Z的高8位寄存器 #define MPU6050_ACCEL_ZOUT_H 0x3F //加速度Z的低8位寄存器 #define MPU6050_ACCEL_ZOUT_L 0x40 //温度的高8位寄存器 #define MPU6050_TEMP_OUT_H 0x41 //温度的低8位寄存器 #define MPU6050_TEMP_OUT_L 0x42 //陀螺仪X的高8位寄存器 #define MPU6050_GYRO_XOUT_H 0x43 //陀螺仪X的低8位寄存器 #define MPU6050_GYRO_XOUT_L 0x44 //陀螺仪Y的高8位寄存器 #define MPU6050_GYRO_YOUT_H 0x45 //陀螺仪Y的低8位寄存器 #define MPU6050_GYRO_YOUT_L 0x46 //陀螺仪Z的高8位寄存器 #define MPU6050_GYRO_ZOUT_H 0x47 //陀螺仪Z的低8位寄存器 #define MPU6050_GYRO_ZOUT_L 0x48 //电源管理寄存器1:设备复位【0不复位】、睡眠模式【0解除睡眠】、循环模式【0不需要循环】、无关位【0即可】、温度传感器失能【0不失能】、最后三位时钟【000内部时钟、XYZ陀螺仪时钟】 #define MPU6050_PWR_MGMT_1 0x6B //电源管理寄存器2:循环模式唤醒频率【00不需要】、后六位【每一个轴的待机位,0不需要待机】 #define MPU6050_PWR_MGMT_2 0x6C //获取设备的ID号 #define MPU6050_WHO_AM_I 0x75 #endif
-
新建文件MPU6050.c和MPU6050.h文件,放到Hardware文件夹
#ifndef __MPU6050_H #define __MPU6050_H void MPU6050_Init(void); void MPU6050_WriteReg(uint8_t RegAddr,uint8_t Data); uint8_t MPU6050_ReadReg(uint8_t RegAddr); void MPU6050_GetData(int16_t* AccX,int16_t* AccY,int16_t* AccZ, int16_t* GyroX,int16_t* GyroY,int16_t* GyroZ); uint16_t MPU6050_GetID(void); #endif
#include "stm32f10x.h" // Device header #include "MyI2C.h" #include "MPU6050_Reg.h" //定义MPI=U6050的地址,0xD0,设备唯一标识,不能重复,AD0引脚可以修改地址最低位 //默认是写的方式:最后一位0代表写,1代表读;前7位才是设备地址 #define MPU6050_ADDRESS 0xD0 //声明函数,因为Init里面需要使用。 void MPU6050_WriteReg(uint8_t RegAddr,uint8_t Data); //初始化函数 void MPU6050_Init(void){ MyI2C_Init(); //电源管理寄存器1:x陀螺仪时钟 MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01); //电源管理寄存器2:不唤醒、不待机 MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00); //10分频,值越小,速度越快 MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09); //滤波参数最大,最平滑的滤波。 MPU6050_WriteReg(MPU6050_CONFIG,0x06); //陀螺仪配置:最大量程 MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18); //加速度配置:最大量程 MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18); } //指定地址写寄存器:设备地址->写寄存器地址->写入数据 void MPU6050_WriteReg(uint8_t RegAddr,uint8_t Data){ //开启协议 MyI2C_Start(); //发送设备地址,找到MPU6050设备 MyI2C_SendByte(MPU6050_ADDRESS); //应答位,需要进行判断,这里省略了:1代表未响应;0代表响应。 MyI2C_ReceiveAck(); //找到设备对应的寄存器 MyI2C_SendByte(RegAddr); //接收应答位,判断省略 MyI2C_ReceiveAck(); //在MPU6050的寄存器中,写入数据 MyI2C_SendByte(Data); //应答位 MyI2C_ReceiveAck(); //结束操作 MyI2C_Stop(); } //指定地址读寄存器:设备地址->写寄存器->【去掉写寄存器】 -> 读寄存器 -> 读出数据 uint8_t MPU6050_ReadReg(uint8_t RegAddr){ uint8_t Data; //开启协议:指定地址写的前半部分 MyI2C_Start(); // 发送设备地址,找到MPU6050设备 MyI2C_SendByte(MPU6050_ADDRESS); //应答位,处理省略 MyI2C_ReceiveAck(); //找到寄存器,当前指针指向了这个寄存器 MyI2C_SendByte(RegAddr); //接收应答 MyI2C_ReceiveAck(); //重新开启协议:当前地址读的部分 MyI2C_Start(); //找到发送设备地址,找到MPU6050设备,并且使用“读”的方式 MyI2C_SendByte(MPU6050_ADDRESS | 0x01); //接收应答 MyI2C_ReceiveAck(); //读取数据:由于指针已经指向的这个寄存器,所以直接读取即可 Data = MyI2C_ReceiveByte(); //发送应答位:1,表示未响应,就结束;0表示,响应,可以继续发送。 MyI2C_SendAck(1); //结束协议 MyI2C_Stop(); //返回读出的数据 return Data; } //读寄存器的值:加速度X,Y,Z 陀螺仪X、Y、Z //值传递方法:1. 全局变量 2.指针传递【√】 3.结构体打包 void MPU6050_GetData(int16_t* AccX,int16_t* AccY,int16_t* AccZ, int16_t* GyroX,int16_t* GyroY,int16_t* GyroZ){ //8位寄存器的数据,由于需要移位,所以设置16位。 uint16_t DataH,DataL; //获取高8位,加速度X寄存器的值 DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); //高8位移位,拼接上低8位,就是加速度X的值。 *AccX = (DataH<< 8)|DataL; DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L); *AccY = (DataH<< 8)|DataL; DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H); DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L); *AccZ = (DataH<< 8)|DataL ; DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H); DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L); *GyroX = (DataH<< 8)|DataL ; DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H); DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L); *GyroY = (DataH<< 8)|DataL ; DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H); DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L); *GyroZ = (DataH<< 8)|DataL ; } //获取MPU6050的ID。 uint16_t MPU6050_GetID(){ //读取who am i 寄存器,获取MPU6050的ID return MPU6050_ReadReg(MPU6050_WHO_AM_I); }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "MPU6050.h" //存放寄存器的值 int16_t AX,AY,AZ,GX,GY,GZ; int main(void){ OLED_Init(); //初始化 MPU6050_Init(); //获取ID号 OLED_ShowString(1,1,"ID:"); OLED_ShowHexNum(1,4,MPU6050_GetID(),2); while(1){ //获取寄存器的值 MPU6050_GetData(&AX,&AY,&AZ,&GX,&GY,&GZ); //显示 OLED_ShowSignedNum(2,1,AX,5); OLED_ShowSignedNum(3,1,AY,5); OLED_ShowSignedNum(4,1,AZ,5); OLED_ShowSignedNum(2,8,GX,5); OLED_ShowSignedNum(3,8,GY,5); OLED_ShowSignedNum(4,8,GZ,5); } }
2.硬件I2C读写MPU6050
RCC开启时钟,I2C外设、GPIO
I2C外设的GPIO口,初始化为复用开漏模式
使用结构体,对I2C配置
I2C_Cmd使能I2C
-
使用I2C2,所以MPU6050接入GPIO对应引脚GPIOB_PIN_10和GPIOB_PIN_11
这里和上面软件模拟方法,接入的端口虽然相同,但是意义完全不同。软件模拟可以介入任意端口,硬件只能接入对应端口
-
删除Hardware文件夹下的MyI2C.c和MyI2C.h,因为要使用硬件I2C
-
关掉MyI2C.c和MyI2C.h文件选项卡
-
在Hardware文件夹下,右键MyI2C.c和MyI2C.h文件,选择Remove File xxx ,移除文件
-
在项目文件夹的Hardware文件夹下,删除MyI2C.c和MyI2C.h文件,保持目录与工程结构一致
-
-
修改MPU6050.c和MPU6050.h
#ifndef __MPU6050_H #define __MPU6050_H void MPU6050_Init(void); void MPU6050_WriteReg(uint8_t RegAddr,uint8_t Data); uint8_t MPU6050_ReadReg(uint8_t RegAddr); void MPU6050_GetData(int16_t* AccX,int16_t* AccY,int16_t* AccZ, int16_t* GyroX,int16_t* GyroY,int16_t* GyroZ); uint16_t MPU6050_GetID(void); #endif
#include "stm32f10x.h" // Device header #include "MPU6050_Reg.h" #define MPU6050_ADDRESS 0xD0 void MPU6050_WriteReg(uint8_t RegAddr,uint8_t Data); void MPU6050_WaitEvent(I2C_TypeDef* I2Cx,uint32_t I2C_EVENT); //初始化 void MPU6050_Init(void){ //RCC开启时钟:I2C2和GPIOB RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //初始化GPIO,设置为复用模式,找到I2C对应的引脚 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStructure); //初始化I2C I2C_InitTypeDef I2C_InitStructure; //使用I2C模式,不使用BUS总线什么模式 I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; //最大不超过400KHz,时钟频率>100kHz,是快速状态 //时钟频率<= 100KHz,是标准速度,占空比固定1:1 I2C_InitStructure.I2C_ClockSpeed = 50000; //选择占空比:2:1 或 16:9,低电平应该分配更多时间【低:高】。 //标准模式下,时钟频率<=100KHz,占空比无效 I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; //默认给应答 I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //STM32作为从机,可以响应几位地址。 I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //STM32作为从机的地址。暂时不需要,随便给,不重复即可 I2C_InitStructure.I2C_OwnAddress1 = 0x00; I2C_Init(I2C2,&I2C_InitStructure); //使能I2C I2C_Cmd(I2C2,ENABLE); MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01); MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00); MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09); MPU6050_WriteReg(MPU6050_CONFIG,0x06); MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18); MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18); } void MPU6050_WriteReg(uint8_t RegAddr,uint8_t Data){ /* MyI2C_Start(); MyI2C_SendByte(MPU6050_ADDRESS); MyI2C_ReceiveAck(); MyI2C_SendByte(RegAddr); MyI2C_ReceiveAck(); MyI2C_SendByte(Data); MyI2C_ReceiveAck(); MyI2C_Stop(); */ //写寄存器 //生成起始条件。 I2C_GenerateSTART(I2C2,ENABLE); //封装的阻塞式程序,等待标志位,以及超时处理 //检测EV5事件是否发生:主机模式已选择事件。STM32默认为从机,发送起始条件后变成主机 MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); //发送从机地址,接收应答 //SendData和Send7bitAddress都可以完成工作,使用专用函数符合规范,而且都自带接收应答功能 //从机地址 + 读写位 I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter); //发生应答后,产生EV6事件:发送模式已选择 //EV6事件后,还有EV8_1事件(可以写入DR),但是我们不需要等待EV8_1事件 MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //写入DR,发送数据 I2C_SendData(I2C2,RegAddr); //等待EV8事件:这个事件很快,表示字节正在发送 MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING); //发送数据 I2C_SendData(I2C2,Data); //如果继续发送,就等待EV8事件:字节正在发送 //最后一个字节,发送完就终止,需要等待EV8_2事件:字节已经发送完毕 MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED); //终止时序 I2C_GenerateSTOP(I2C2,ENABLE); } uint8_t MPU6050_ReadReg(uint8_t RegAddr){ uint8_t Data; /* MyI2C_Start(); MyI2C_SendByte(MPU6050_ADDRESS); MyI2C_ReceiveAck(); MyI2C_SendByte(RegAddr); MyI2C_ReceiveAck(); MyI2C_Start(); MyI2C_SendByte(MPU6050_ADDRESS | 0x01); MyI2C_ReceiveAck(); Data = MyI2C_ReceiveByte(); MyI2C_SendAck(1); MyI2C_Stop(); */ //起始时序 I2C_GenerateSTART(I2C2,ENABLE); MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); //寻址设备 I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter); MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //寻址寄存器 I2C_SendData(I2C2,RegAddr); //保险起见:等待 字节发送完成 标志 MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED); //重新开始时序,读取数据 I2C_GenerateSTART(I2C2,ENABLE); MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT); //寻址设备:读标志 I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Receiver); //等待EV6事件:主机接收 的EV6事件 MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //接收字节,需要等待EV7事件。 //在接收最后一个字节之前,要对EV7_1事件:提前 清除响应、停止条件产生 //ACK置0 STOP置1 I2C_AcknowledgeConfig(I2C2,DISABLE); I2C_GenerateSTOP(I2C2,ENABLE); //等待EV7事件:接收1个字节后产生 MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED); //EV7事件后,就可以读出DR的字节 Data = I2C_ReceiveData(I2C2); //ACK重新置1,恢复默认的ACK = 1,方便指定地址接收多个字节,可以进一步改变代码 I2C_AcknowledgeConfig(I2C2,ENABLE); return Data; } void MPU6050_GetData(int16_t* AccX,int16_t* AccY,int16_t* AccZ, int16_t* GyroX,int16_t* GyroY,int16_t* GyroZ){ uint16_t DataH,DataL; DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); *AccX = (DataH<< 8)|DataL ; DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L); *AccY = (DataH<< 8)|DataL; DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H); DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L); *AccZ = (DataH<< 8)|DataL ; DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H); DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L); *GyroX = (DataH<< 8)|DataL ; DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H); DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L); *GyroY = (DataH<< 8)|DataL ; DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H); DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L); *GyroZ = (DataH<< 8)|DataL ; } uint16_t MPU6050_GetID(){ return MPU6050_ReadReg(MPU6050_WHO_AM_I); } //封装的阻塞式程序,等待标志位,以及超时处理 void MPU6050_WaitEvent(I2C_TypeDef* I2Cx,uint32_t I2C_EVENT){ uint32_t Timeout = 10000; while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS) { Timeout --; if (Timeout == 0) { //超时处理:可以打印日志、系统复位、紧急停机等。 break; } } }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "MPU6050.h" int16_t AX,AY,AZ,GX,GY,GZ; int main(void){ OLED_Init(); MPU6050_Init(); OLED_ShowString(1,1,"ID:"); OLED_ShowHexNum(1,4,MPU6050_GetID(),2); while(1){ MPU6050_GetData(&AX,&AY,&AZ,&GX,&GY,&GZ); OLED_ShowSignedNum(2,1,AX,5); OLED_ShowSignedNum(3,1,AY,5); OLED_ShowSignedNum(4,1,AZ,5); OLED_ShowSignedNum(2,8,GX,5); OLED_ShowSignedNum(3,8,GY,5); OLED_ShowSignedNum(4,8,GZ,5); } }
3.SPI通信
速度更快、简单、硬件开销大、通信线个数多、经常有资源浪费现象
-
SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
-
四根通信线:
-
SCK(Serial Clock):串行时钟线
SCLK、CLK、CK
-
MOSI(Master Output Slave Input):主机输出、从机输入
DO
-
MISO(Master Input Slave Output):主机输入、从机输出
DI
-
SS(Slave Select):从机选择,一个从机连接一条
NSS
CS (Chip Select) :片选,用于指定通信对象
-
GND:共地
-
基本特征:
-
同步:需要时钟线
-
全双工:数据发送和数据接收单独各占一根线
-
支持总线挂载多设备(一主多从):SS指定从机通信,不需要地址、应答机制
-
硬件电路
-
SPI主机,主导整个SPI主线,一般使用控制器作为主机
比如STM32
-
SPI从机,是挂载在主机上的从设备
存储器、显示屏、通信模块、传感器等
-
SS从机选择线,一根SS线连接一个从机,从主机输入,低电平选择,同时只能选择一个从机通信
当主机未选择的从机,SS置为高电平的同时,从机的MISO,要切换为高阻态,防止一条线有多条输出,防止冲突。
-
SPI的6根通信线,都是单端信号,高低电平都是相对于GND的电压差,所有设备需要共地。【主机和从机】
-
如果从机没有供电,可以从主机额外引出电源正极VCC给从机供电
-
时钟线SCK:对于主机是输入线,对于从机是接收线,保证同步时钟
-
MOSI:主机输出、从机输入;主机通过MOSI输出,从机通过MOSI输入
-
MISO:主机输入、从机输出;从机通过MISO输出,主机通过MISO输入
-
输出引脚配置为推挽输出,输入引脚配置为浮空输入或上拉输入
下降沿和上升沿都迅速,通讯速度可以达到MHz速度,不支持多主机、实现全双工、不会;冲突所以可以使用推挽输出;
对比I2C上升沿缓慢、下降沿迅速,速度受制于上升沿信号,为了实现半双工、多主机时钟同步、总线仲裁,不允许使用推挽输出,防止冲突短路;
-
通信:主机需要与哪个从机通信,就把哪个从机的SS线,置为低电平
-
-
移位示意图:高位先行
-
SPI主机内,有个8位的移位寄存器,高位先行,根据时钟向左移位
-
SPI从机内,也有个8位的移位寄存器,高位先行,根据时钟向左移位
-
波特率发生器:移位寄存器的时钟源,控制主机和从机,驱动移位寄存器移位
-
MOSI:主机移位寄存器移出去的数据,通过MOSI引脚输入到从机移位寄存器的右边
-
MISO:从机移位寄存器移出去的数据,通过MISO引脚输入到主机移位寄存器的右边
-
工作流程
-
波特率发生器时钟的上升沿触发
-
所有移位寄存器向左移动移位,移出去的位,放到引脚上
主机移位放到MOSI引脚
从机移位放到MISO引脚
-
波特率发生器时钟的下降沿触发
-
引脚采样的位,输入到移位寄存器的最低位
-
8个时钟之后,实现主机和从机的一个字节数据交换
主机只想发送,不想接收:那么交换过来的数据,不看即可,没有意义的数据
主机只想接收,不想发送:随意发送一串数据(一般为0x00或0xFF),把从机的数据交换过来即可。
-
-
-
SPI时序
-
SPI时序基本单元
-
SS从高变到低,表示选中某个从机,是通信的开始
-
SS从低变到高,表示结束了从机的选择,是通信的结束
-
在从机的选中过程中,SS时钟保持为低电平
-
-
交换一个字节的模式:模式1更符合定义,模式0使用更多
-
模式0:CPOL = 0,CPHA = 0
CPOL = 0:空闲状态时,SCK为低电平 CPHA = 0:SCK第一个边沿移入数据,第二个边沿移出数据
-
模式1:CPOL = 0,CPHA = 1
CPOL=0:空闲状态时,SCK为低电平 CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
-
模式2:CPOL = 1,CPHA = 0
CPOL=1:空闲状态时,SCK为高电平 CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
-
模式3:CPOL = 1,CPHA = 1
CPOL=1:空闲状态时,SCK为高电平 CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
-
-
SPI时序:向SS指定的设备,发送指令(0x06)
-
发送开始,将SS置为低电平
-
发送0x06,交换得到0xFF
-
发送结束,将SS置为高电平
-
-
SPI时序:指定地址写
-
向SS指定的设备,发送写指令(0x02),
-
随后在指定地址(Address[23:0])下,写入指定数据(Data)
向地址:0x123456,写入数据0x55
-
-
W25Q64
-
W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器
数据掉电不丢失
常应用于:数据存储、字库存储、固件程序存储等场景
-
属性
-
存储介质:Nor Flash(闪存)
-
时钟频率:
-
80MHz:时钟线最大频率
-
160MHz (Dual SPI) : 双重SPI模式等效的频率
单独发送或接收时,会有浪费。
因此可以把MOSI和MISO同时兼具发送和接收功能,每次可以传输2位,因此等效为时钟频率2倍
实际上时钟频率还是80MHz,只是等效而已
-
320MHz (Quad SPI):四重SPI模式等效的频率
同时传输4位,等效4倍,4位并行
-
-
存储容量(24位地址):
-
24位地址,3个字节
-
40和80,应该是04,08;
-
数字表示容量,M为单位;字节表示是前面数字除以8。
-
硬件电路
-
引脚功能定义
-
VCC、GND:3V3
-
CS:低电平有效,片选引脚,对应SS
-
CLK:时钟线
-
DI:MOSI,主机输出,从机输入
-
DO:MISO,主角输入,从机输出
-
WP:写保护,实现硬件写保护,低电平有效,不让写。
-
HOLD:数据保持,低电平有效,中断保护现场,保持时序有效
-
双重SPI:DI当做IO0 ,DO当做IO1 ,数据同时收发两个数据位
-
四重SPI:WP当做IO2 ,HOLD 当做IO3 ,四个引脚都可以收发数据
-
-
模块原理图:
-
U1是W25QXX芯片
-
VCC:接入J1的6号引脚
-
GND:接入J1的3号引脚
-
通信4个脚:CS、CLK、DI、DO,直接接到J1
-
HOLD和WP:低电平有效,暂时不使用,直接接入VCC。
-
C1:滤波电容,接入到VCC和GND
-
R1和D1:电源指示灯,通电就亮
-
-
J1是6脚排针
-
-
-
W25Q64框图
-
芯片内部划分,容量是8MB,划分后容易管理,所有操作按照基本单元进行
常见划分:
-
一整块存储空间,划分为若干块Block
-
每一块,划分为若干扇区Sector
-
每个扇区,分为若干页Page
W25Q64划分:毫秒级别ms写入、擦除
-
芯片24位地址,3个字节表示;容量8MB,24位最大寻址范围是16MB,因此地址为00 00 00H~ 7F FF FFH,因此只用了一半
-
以64KB为一个基本单元,划分为若干块,从0地址开始,依次是块0、块1、块2、……、块127
每个块地址变化规律:7FH = 127
xx 00 00 ~ xx FF FF
-
在一块中,以4KB为一个单元,进行切分,每一块可以划分16个扇区:扇区0、扇区1、……、扇区15
每个扇区地址变化规律:FH = 15
xx x0 00 ~ xx xF FF
-
在每个扇区,以256B划分一页,一个扇区可以分为16页:页0、页1、……、页15
页的地址规律:每一行就是一页
xx xx 00 ~ xx xx FF
-
-
SPI控制逻辑:左下角,芯片内部的操作(地址锁存、数据读写等),控制逻辑都可以自动完成。
-
SPI通信引脚:与主控芯片相连接,主控芯片通过SPI协议,把指令和数据发给控制逻辑
-
状态寄存器:控制及状态寄存器,芯片的状态
是否忙状态、是否写使能、是否写保护等
状态寄存器1:其他位暂时不用
BUSY:当设备正在执行页编程(写入数据)、扇区擦除、块擦除、整片擦除、写状态寄存器时,BUSY置1,处于忙状态设备会忽略进一步的指示,指令结束后,BUSY清零。
WEL:写使能锁存位,执行完写使能指令后,WEL置1,代表芯片可以进行写入操作;芯片写失能(上电后默认失能、发送写失能指令、页编程、扇区擦除等)时,WEL置0。
状态寄存器2:暂时不使用
-
写控制逻辑:Write Control Logic,配合WP引脚实现硬件写保护
-
高电压生成器:配合Flash进行编程,为掉电不丢失存储提供高电压。
-
页地址锁存/计数器:指定24位地址,3个字节,读入前两个字节。通过写保护和行解码,选择操作哪一页。
内部存在计数器,读写后自动+1;
-
字节地址锁存/计数器:指定24位地址,3个字节,读入后一个字节。通过列解码和256字节页缓存,对指定字节进行读写操作。
内部存在计数器,读写后自动+1;
256字节页缓冲区:是一个256字节的RAM存储器,数据读写从这里实现,跟随SPI速度;因此写入的一个时序,连续写入的数据量不能超过256字节
写完成后,数据会从缓冲区,存入到Flash里面,速度较慢,需要时间。因此写入时序后,芯片会进入一段忙状态。
-
Flash操作的注意事项:比RAM要求更多
写入操作时:
写入操作前,必须先进行写使能
防止误操作,发送一个写使能的指令
每个数据位只能由1改写为0,不能由0改写为1
Flash不能覆盖改写,可能是技术原因
写入数据前必须先擦除,擦除后,所有数据位变为1
弥补只能由1改0的缺陷。 FF表示空白区域。
擦除必须按最小擦除单元进行
可以选择整个芯片擦除、按块擦除、按扇区擦除,最小单元是一个扇区(4KB=4096B)
为了不丢失数据,只能先把数据读出来,再写回去。
擦除指令,也会让芯片进入忙状态。
连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
一个写入时序,最多写入256字节
写入结尾后,会跳到页首写,造成地址错乱
写入操作结束后,芯片进入忙状态,不响应新的读写操作
忙状态(BUSY = 1忙)不会响应写入操作。 读状态寄存器,读取忙状态后,判断是否写入。
读取操作时:要求少
直接调用读取时序,无需使能,无需额外操作,没有页的限制
读取操作结束后不会进入忙状态,但不能在忙状态时读取
SPI指令集
-
写使能:发送指令码0x06,后续不需要跟随数据
只对一条时序有效,完成后自动清除
-
读状态寄存器1:发送指令码0x05,继续发送,交换读取一个字节,就是寄存器状态。
可以查看忙状态
-
页编程:有256字节限制,先发送指令码0x02,跟随写入3位地址(高->低),跟随数据,多位数据依次写入
-
扇区擦除:擦除4KB数据,最小的擦除。发送指令码0x20,发送擦除地址3位(一般对齐到扇区首地址)
-
读取ID号:发送指令码0x9F,连续读取3个字节(厂商id 1位,设备id 2位)
-
读取数据:发送指令码0x03,交换发送3个字节地址,读取数据,跟随数据,多位数据依次读取
SPI外设:高性能、节省软件资源
STM32F103C8T6 硬件SPI资源:SPI1、SPI2
-
STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
-
属性
-
可配置8位/16位数据帧、高位先行/低位先行
可以把写入uint16_t的数据,一次性发送两个字节,波形与发两次8位一样,用的很少。
SPI和I2C一般高位先行(读数据从左往右),串口是低位先行(读数据从右往左)
-
时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)
一个SCK时钟,交换一个bit数据,一般体现为传输速度,单位是Hz或bit/s
PCLK是外设时钟:SPI1是APB2外设,PCLK=72MHz、SPI2是APB1外设,PCLK = 36MHz
分频系数可以配置为2, 4, 8, 16, 32, 64, 128, 256,不能任意指定
-
支持多主机模型、主或从操作
-
可精简为半双工/单工通信
两根线MOSI和MISO:属于全双工
一根线分时发送或接收:属于半双工
-
支持DMA
可以自动搬运数据,大量数据时使用,更高效
-
兼容I2S协议
I2S ,数字音频信号传输的专用协议
SPI框图
-
移位寄存器,数据从MOSI移出去,MISO的数据移进来
左边进来,移动到高位,说明低位先行配置
-
LSBFIRST:移位先行控制器,LSBFIRST= 1,低位先行。
如果LSBFIRST = 0,高位先行,那么图中的移位寄存器的进入与输出应该改变
-
主从模式引脚变换:MOSI和MISO的交叉部分,SPI外设可以作为主机也可以作从机
做主机时,不使用交叉:MOSI作为输出,MISO作输入
做从机时,使用交叉:MOSI从机输入,MISO从机输出
因此图中箭头,从MISO指向MOSI的箭头,画错方向了。
-
TDR和RDR占用同一个地址,但是是两个寄存器,统一叫做DR。数据寄存器,发送和接收时分离的;移位寄存器,发送和接收共用
连续传输:没有数据移位时,TDR的数据转入移位寄存器【发送缓冲区】,开始移位,同时状态寄存器标志位TXE=1(发送寄存器空),此时下一个数据可以提前写入到TDR等待。
连续接收:数据移出完成,那么数据移入也完成。数据从移位寄存器,转入到接收缓冲区RDR,同时状态寄存器标志位RXNE=1(接收寄存器非空),检查RXNE后,尽快把数据从RDR读出来,在下个数据到来前读出RDR就可以实现连续接收,否则会覆盖
-
波特率发生器:产生SCK时钟,输入时钟PCLK(72M或36M)进行分频后输出到SCK引脚,控制移位寄存器
-
CR1寄存器:BR0、BR1、BR2控制分频系数,实现2~256的分频
-
常用寄存器标志
-
SPE:SPI使能,就是SPI_Cmd的配置位
-
BR:配置波特率,就是SCK的时钟频率
-
MSTR:配置主从模式,1是主模式(常用);0是从模式
-
CPOL和CPHA:控制SPI的4种模式
-
SR状态寄存器
-
TXE:发送寄存器空
-
RXNE:接收寄存器非空
CR2寄存器:一些使能位
NSS从机选择,低电平有效。
偏向于实现多主机模型,因此暂时不会使用。
需要把设备的NSS引脚连接到一起,可以把NSS配置为输出或输入模式,
输出模式(SSOE = 1):可以输出电平,将NSS置为低电平,告诉其他设备,我现在要变成主机,其他设备变成从机。
输入模式(SSOE = 0):可以接收其他设备信号,当有设备是主机,拉低NSS后,就无法成为主机。输入的信号进入数据选择器,分为0硬件模式和1软件模式。
【使用GPIO口进行模拟SS引脚】:直接置高低电平即可
SPI基本结构
-
移位寄存器左移,高位优先,通过GPIO口输出到MOSI,从MOSI输出,因此是SPI主机
-
移入的数据,从MISO进入,通过GPIO到移位寄存器低位,循环8次,可以实现主机和从机交换一个字节
-
TDR和RDR的配合,可以实现连续的数据流
-
TDR数据整体转入,置TXE标志位
-
移位寄存器数据,转入RDR,置RXNE标志位
波特率发生器:产生时钟,输出到SCK引脚
数据控制器:控制所有电路运行
开关控制SPI_Cmd,使能整个外设
从机选择引脚SS,图中没有画,可以使用普通的GPIO口模拟
一主多从模式下,GPIO模拟SS是最佳选择
SPI时序
如果对性能要求不高,可以使用非连续传输,更加简单
-
CPOL = 1,CPHA = 1,图中使用的模式3,SCK默认高电平,在第一个下降沿,MOSI和MISO移出数据;在上升沿移入数据。
-
SCK时钟线:模式3规则,SCK默认高电平。在第一个下降沿,MOSI和MISO移出数据;在上升沿移入数据。
-
MISO/MOSI(输出):是MOSI和MISO的输出波形,图中演示为低位先行。
-
TXE标志:发送寄存器空,
-
发送缓冲器(写入SPI_DR):就是TDR
-
BSY标志:BUSY标志,由硬件自动设置和清除,有数据传输时标志置1
-
MISO/MOSI(输入):
-
RXNE:接收数据寄存器非空标志位
-
接收缓冲器(读出SPI_DR):就是RDR
-
发送流程:
-
当SS置低电平,开始时序
-
开始时,TXE = 1,表示TDR为空,可以写入数据开始传输
-
软件写入0xF1到SPI_DR,表示要发送一个数据
-
写入后,TDR = 0xF1,同时TXE = 0,表示TDR已经有数据了。
TDR是等候区,移位寄存器是真正的发送器
-
移位寄存器开始没有数据,那么TDR数据立刻转入到移位寄存器,开始发送,同时置TXE = 1,表示发送寄存器空
-
移位寄存器有数据,波形开始自动生成
数据波形图中有些早,应该延后一些,在TXE标志位置1的上升沿时刻。
-
为了连续发送数据,在移位寄存器发送时,把下一个数据移入到TDR中;因此在TDR空时,立刻把下一个数据0xF2写入。后续同理
-
如果不想继续发送,那么TXE = 1保持不变,直到移位寄存器空全部发送完成,BUSY由硬件清除,才表示波形发送完成
-
-
接收流程:全双工,发送同时接收
-
第一个字节发送完成后,接收也完成了。
-
接收到数据A1,移位寄存器数据整体转入到RDR,同时RXNE标志位置1,表示接收到数据
-
从SPI_DR也就是RDR中读出数据A1,软件清除RXNE标志位,等待下一个数据接收
移位寄存器自动转入RDR,会覆盖原有数据,因此要及时读取RDR数据
-
在最后一个字节传输,时序完全产生后,才可以接收到数据;
-
-
CPOL = 1,CPHA = 1,配置为SPI模式3,SCK默认高电平
-
发送数据时,检测到TXE= 1,TDR为空,就可以软件写入0xF1到SPI_DR
-
此时,TDR = 0xF1,TXE = 0,同时移位寄存器为空,那么TDR数据立刻转入移位寄存器开始发送
-
等待这个字节传输完成,接收的RXNE = 1,可以把数据读出
连续传输:此刻TXE = 1,可以把下一个数据放入到TDR中等待
-
读出字节后,可以写入下一个字节数据。较晚写入TDR,然后可以继续发送
1.软件SPI读写W25Q64
-
新建MySPI.c和MySPI.h,放到Hardware文件夹
#ifndef __MYSPI_H #define __MYSPI_H void MySPI_Init(void); void MySPI_Start(void); void MySPI_Stop(void); uint8_t MySPI_SwapByte(uint8_t ByteSend); #endif
#include "stm32f10x.h" // Device header //写SS,选择从机 void MySPI_W_SS(uint8_t BitValue){ GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue); } //输出时钟信号 void MySPI_W_SCK(uint8_t BitValue){ GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue); } //主机输出,从机输入:主机写数据 void MySPI_W_MOSI(uint8_t BitValue){ GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue); } //主机输入,从机输出:主机读数据 uint8_t MySPI_R_MISO(void){ return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6); } //初始化 void MySPI_Init(void){ //开启时钟GPIOA RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //初始化GPIOA_PIN_4、GPIOA_PIN_5、GPIOA_PIN_7,推挽输出模式 //写SS,选择从机 GPIO_Pin_4 //输出时钟信号 GPIO_Pin_5 //主机输出,从机输入:主机写数据 GPIO_Pin_7 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化GPIOA_PIN_6,上拉输入模式(浮空输入也可以) //主机输入,从机输出:主机读数据 GPIO_Pin_6 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); //从机选择:低电平有效。 MySPI_W_SS(1); //时钟线:低电平开始通信。 MySPI_W_SCK(0); } //从机选择,准备开启通信 void MySPI_Start(void){ MySPI_W_SS(0); } //切断从机,结束通信 void MySPI_Stop(void){ MySPI_W_SS(1); } //交换数据:模式0 uint8_t MySPI_SwapByte(uint8_t ByteSend){ uint8_t ByteReceive = 0x00; //移位寄存器操作 for(uint8_t i = 0;i < 8 ;i++){ //主机高位先行 MySPI_W_MOSI(ByteSend & (0x80 >> i)); //时钟上升沿,发送数据 MySPI_W_SCK(1); //同时接收交换的数据。 if(MySPI_R_MISO() == 1){ ByteReceive |= (0x80 >> i); } //准备下一个数据移位 MySPI_W_SCK(0); } //返回接收值 return ByteReceive; }
-
新建W25Q64_Ins,给SPI命令集重新命名
#ifndef __W25Q64_INS_H #define __W25Q64_INS_H //写使能 #define W25Q64_WRITE_ENABLE 0x06 //写失能 #define W25Q64_WRITE_DISABLE 0x04 //读状态寄存器1 #define W25Q64_READ_STATUS_REGISTER_1 0x05 //读状态寄存器2 #define W25Q64_READ_STATUS_REGISTER_2 0x35 //写状态寄存器 #define W25Q64_WRITE_STATUS_REGISTER 0x01 //页编程,最大256B #define W25Q64_PAGE_PROGRAM 0x02 #define W25Q64_QUAD_PAGE_PROGRAM 0x32 //块擦除:64KB #define W25Q64_BLOCK_ERASE_64KB 0xD8 //块擦除:32KB #define W25Q64_BLOCK_ERASE_32KB 0x52 //扇区擦除:4KB,最小单位 #define W25Q64_SECTOR_ERASE_4KB 0x20 //片擦除:整个芯片擦除,变成FF #define W25Q64_CHIP_ERASE 0xC7 #define W25Q64_ERASE_SUSPEND 0x75 #define W25Q64_ERASE_RESUME 0x7A #define W25Q64_POWER_DOWN 0xB9 #define W25Q64_HIGH_PERFORMANCE_MODE 0xA3 #define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF #define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB #define W25Q64_MANUFACTURER_DEVICE_ID 0x90 #define W25Q64_READ_UNIQUE_ID 0x4B //获取芯片ID:厂商ID(1位)、设备ID(2位) #define W25Q64_JEDEC_ID 0x9F //读数据 #define W25Q64_READ_DATA 0x03 #define W25Q64_FAST_READ 0x0B #define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B #define W25Q64_FAST_READ_DUAL_IO 0xBB #define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B #define W25Q64_FAST_READ_QUAD_IO 0xEB #define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3 //无效字节 #define W25Q64_DUMMY_BYTE 0xFF #endif
-
新建W25Q64.c和W25Q64.h,放到Hardware文件夹
#ifndef __W25Q64_H #define __W25Q64_H void W25Q64_Init(void); void W25Q64_ReadID(uint8_t *MID,uint16_t *DID); void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count); void W25Q64_SectorErase(uint32_t Address); void W25Q64_ReadData(uint32_t Address,uint8_t* DataArray,uint32_t Count); #endif
#include "stm32f10x.h" // Device header #include "MySPI.h" #include "W25Q64_Ins.h" //初始化 void W25Q64_Init(void){ MySPI_Init(); } //读取设备ID void W25Q64_ReadID(uint8_t *MID,uint16_t *DID){ //开启SPI协议:打开从设备 MySPI_Start(); //交换字节:发送获取ID命令 MySPI_SwapByte(W25Q64_JEDEC_ID); //一位厂商ID *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //两位设备ID,高位先行 //接收高位 *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //左移到高位 *DID <<= 8; //接收低位 *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //结束SPI通信 MySPI_Stop(); } //写使能:保证后面一个时序写使能开启。 void W25Q64_WriteEnable(void){ //开启SPI通信 MySPI_Start(); //交换字节:发送写使能命令 MySPI_SwapByte(W25Q64_WRITE_ENABLE); //结束SPI通信 MySPI_Stop(); } //读取设备状态:是否忙状态 void W25Q64_WaitBusy(void){ //超时时间 uint32_t TimeOut = 100000; //开启SPI MySPI_Start(); //交换数据:发送 读取状态寄存器1 命令 MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //不断读取状态寄存器1的数据,判断BUSY位是否处于忙状态。 while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01)==0x01){ TimeOut --; //超时处理 if(TimeOut == 0){ break; } } //结束SPI通信 MySPI_Stop(); } //写数据,页编程 void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count){ //写使能开启 W25Q64_WriteEnable(); MySPI_Start(); //页编程命令 MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //发送编辑地址:24位地址,高位先行 MySPI_SwapByte(Address>>16); MySPI_SwapByte(Address>>8); MySPI_SwapByte(Address); //写数据 for(uint8_t i = 0;i<Count;i++){ MySPI_SwapByte(DataArray[i]); } //结束通信 MySPI_Stop(); //忙等 W25Q64_WaitBusy(); } //扇区擦除:4KB,传入地址最好 xx x0 00 结尾,保证扇区对齐 void W25Q64_SectorErase(uint32_t Address){ //写使能 W25Q64_WriteEnable(); MySPI_Start(); //发送擦除命令 MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //指定擦除地址 MySPI_SwapByte(Address>>16); MySPI_SwapByte(Address>>8); MySPI_SwapByte(Address); MySPI_Stop(); //忙等 W25Q64_WaitBusy(); } //读数据 void W25Q64_ReadData(uint32_t Address,uint8_t* DataArray,uint32_t Count){ MySPI_Start(); //发送读命令 MySPI_SwapByte(W25Q64_READ_DATA); //发送读地址 MySPI_SwapByte(Address>>16); MySPI_SwapByte(Address>>8); MySPI_SwapByte(Address); //读取数据:传递无效字节即可。 for(uint8_t i = 0;i<Count;i++){ DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); } MySPI_Stop(); }
2.硬件SPI读写W25Q64
RCC开启时钟,SPI和GPIO外设
初始化GPIO,其中SCK和MOSI是由硬件外设控制的输出信号,所以使用复用推挽输出
MISO是硬件外设的输入信号,配置为上拉输入,输入可以有多个,所以不存在复用输入,GPIO口可以输入、外设也可以输入
SS是软件控制的输出信号,配置为推挽输出
配置SPI外设,使用一个结构体即可配置,使用SPI_Init初始化
开关控制SPI_Cmd,使能
-
修改MySPI.c,使用硬件SPI实现
#include "stm32f10x.h" // Device header //软件选择从机 void MySPI_W_SS(uint8_t BitValue){ GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue); } //初始化 void MySPI_Init(void){ //初始化GPIOA和SPI1外设 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE); //初始化GPIO口,使用硬件SPI1的外设。 /* GPIO_Pin_4:作为从机选择,软件控制。任意GPIO口,使用推挽输出 GPIO_Pin_5:是CLK时钟线,使用SPI1外设时钟,复用推挽输出 GPIO_Pin_7:是DO/MOSI,主机输出从机输入,使用SPI1外设资源,复用推挽输出 GPIO_Pin_6:是DI/MISO,主机输入从机输出,使用SPI1外设资源,上拉输入。 注意:主机STM32的DI和DO,与从机W25Q64的DI和DO反接。 */ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5|GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&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); //初始化SPI1 SPI_InitTypeDef SPI_InitStruct; //主机模式 SPI_InitStruct.SPI_Mode = SPI_Mode_Master; //2根线,全双工模式 SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //一次传输8bit,一个字节 SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b; //高位先行 SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; //APB2的外设72MHz,进行128分频(适中),看需求,数值越大,时钟越慢 SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; //SPI模式0(选择模式3也可以) //空闲时,默认低电平 SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; //第一个边沿开始采样(移入) / 第二个边沿开始采样 SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; //用不到NSS,使用软件的SS选择 SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; //CRC校验多项式,默认值7,不重要 SPI_InitStruct.SPI_CRCPolynomial = 7; SPI_Init(SPI1,&SPI_InitStruct); //SPI1使能 SPI_Cmd(SPI1,ENABLE); //默认给SS输出高电平,默认不选择从机 MySPI_W_SS(1); } void MySPI_Start(void){ MySPI_W_SS(0); } void MySPI_Stop(void){ MySPI_W_SS(1); } //大多数标志位都需要手动清除。 uint8_t MySPI_SwapByte(uint8_t ByteSend){ //判断标志TXE,发送寄存器不为空,就等待 while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET); //发送寄存器为空,发送数据;当写入SPI_DR时,TXE标志被自动清除 SPI_I2S_SendData(SPI1,ByteSend); //判断标志RXNE,接收寄存器为空,就等待 while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET); //接收寄存器不为空,接收到数据,返回;当读SPI_DR时,RXNE标志被自动清除 return SPI_I2S_ReceiveData(SPI1); }
4.CAN通信
-
引脚:差分数据脚,两个引脚表示一个差分数据
-
CAN_H
-
CAN_L
5.USB通信
-
引脚:差分数据脚,两个引脚表示一个差分数据
-
DP
-
DM
8.RTC实时时钟
-
Unix时间戳:定义为从UTC/GMT(伦敦本初子午线)的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒
时间标准:
GMT:格林尼治标准时间,地球自转一周的时间,不准
UTC:协调世界时,原子钟,稳定
中国东八区:时间 + 8小时
千年虫:2038年,int32_t的时间计数溢出。
-
C语言时间模块
头文件time.h,可以使用时间戳,直接转换各种类型的时间格式
time_t 是 int64_t类型,64位有符号整型
struct tm 结构体类型:tm_year最小值是70;tm_isdst 夏令时。
-
BKP备份寄存器:本质是RAM存储器,掉电丢失数据。
-
BKP备份寄存器:可用于存储用户应用程序数据。当VDD(2.0 ~ 3.6V)电源被切断,他们仍然由VBAT(1.8 ~3.6V)维持供电。
VBAT备用电池:
正极接入备用电池
负极和主电源负极共地即可。
-
功能特点:
-
当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位;
当备用电池和VDD都断电,BKP数据会清空。
-
TAMPER引脚产生的侵入事件将所有备份寄存器内容清除
TAMPER引脚是一个安全保障设计,可以做防拆功能,清空BKP中的敏感数据。
TAMPER引脚加一个默认的上拉/下拉电阻,使用一根线连接到设备外壳的防拆开关或触点,当设备拆开时,触发开关,在TAMPER引脚产生上升沿或下降沿,STM32就检测到侵入事件,BKP数据清空,然后申请中断,可以在中断中,继续保护设备(清除其他存储器数据、设备锁死等)
主电源断电后,备用电池供电,侵入设备仍然有效,即使设备关机也可以防拆。
-
RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲
外部用设备测量RTC校准时钟,可以对内部RTC微小误差进行校准
闹钟脉冲和秒脉冲,可以输出为其他设备提供信号
PC13、TAMPER、RTC三个引脚共用一个端口,同一时间只能使用一个。
-
存储RTC时钟校准寄存器
-
用户数据存储容量:
-
20字节(中容量和小容量)【√】
-
84字节(大容量和互联型)
-
-
BKP基本结构
-
BKP属于后备区域,但是后备区域不只有BKP
后备区域:当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电;当VDD主电源上电时,后备区域供电由VBAT切换到VDD
-
数据寄存器:存储数据,每个数据寄存器都是16位(2字节)
20字节(中容量和小容量)一般有:DR1 ~ DR10
84字节(大容量和互联型)一般有:DR1 ~ DR42
-
侵入检测:从TAMPER引脚(PC13)引入检测信号,当产生上升沿或下降沿时,清除BKP所有内容,保证安全
-
时钟输出:可以把RTC相关时钟,从RTC位置(PC13)输出出去,供外部使用。
输出校准时钟时,配合校准寄存器,可以对RTC的误差进行校验
-
RTC时钟校准寄存器:输出RTC时钟时,对于RTC的误差进行校准
-
RTC外设:实时时钟。RTC是一个独立的定时器,可为系统提供时钟和日历的功能
C51可以挂在DS1302外置RTC芯片
STM32内置RTC外设,需要必要元件:RTC晶振、备用电池
-
RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.0 ~ 3.6V)断电后可借助VBAT(1.8 ~ 3.6V)供电继续走时
当VDD主电源掉电时,可以由VBAT的备用电池供电;
当VDD主电源上电时,后备区域供电由VBAT切换到VDD;
-
功能特点:
-
32位的可编程计数器,可对应Unix时间戳的秒计数器
-
20位的可编程预分频器,可适配不同频率的输入时钟
-
可选择三种RTC时钟源:选择一个,接入到RTCCLK
H是高速;L是低速
I是内部;E是外部
-
HSE时钟除以128(通常为8MHz/128)
HSE 高速外部时钟信号
主要作为系统主时钟
-
LSE振荡器时钟(通常为32.768KHz)
低速外部时钟信号
RTC专用时钟【常用】
可以通过VBAT备用电池供电,其余两个时钟不可以
-
LSI振荡器时钟(40KHz)
低速内部时钟信号
主要作为看门狗时钟。
-
-
-
RTC框图
-
灰色区域:属于备用区域,主电源掉电后,使用备用电池工作
-
计数计时
-
RTC_CNT:32位可编程计数器,Unix时间戳秒寄存器,1s自增一次,需要1Hz信号。
方便的借助time.h函数计算时间
-
RTC_ALR:闹钟寄存器,32位寄存器,设置闹钟
当CNT的值和ALR设定的值相同时,产生RTC_Alarm信号,通往中断系统或退出待机模式
-
RTC_Alarm:可以通往中断系统,也可以让STM32退出待机模式
待机模式省电
-
-
RTCCLK:提供RTC时钟,经过分频器得到1Hz信号
一般选择LSE振荡器时钟(通常为32.768KHz)
-
RTC预分频器:20位的分频器,实现1 ~ 220 (1M)范围内的分频
与时基单元类似
-
RTC_PRL:重装载寄存器,配置n分频(n = x + 1)
-
RTC_DIV:余数寄存器,实际上是自减计数器,自动重装
-
TR_CLK:计数器溢出信号
-
-
中断部分:3个信号可以触发中断
-
RTC_Second:秒中断,开启后每秒进入一次RTC中断
-
RTC_Overflow:溢出中断,CNT的32位计数器溢出,触发一次中断,一般不会触发
CNT是32位无符号数,2106年才会溢出
-
RTC_Alarm:闹钟中断,当闹钟和计时器值相等时触发。闹钟信号也可以把设备从待机模式唤醒。
-
-
读写寄存器:通过APB1接口
-
WKUP引脚:可以唤醒设备,和闹钟信号相同
-
-
RTC基本结构
-
硬件电路
-
最小系统基础上,外部电路需要额外加上两部分
-
备用电池
-
外部低速晶振
-
RTC操作注意事项:RTCCLK和PCLK1的时钟频率不同,需要等待、同步
-
执行以下操作将使能对BKP和RTC的访问:
-
设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟
-
设置PWR_CR的DBP,使能对BKP和RTC的访问
若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1
直接调用等待同步的函数即可
必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器
将CNF位置1,才可以设置时间;库函数自动加入了这个操作
对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器
调用等待函数即可
1.读写备份寄存器
-
程序简单,直接修改main.c即可
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Key.h" uint16_t ArrayWrite[] = {0x1234,0x5678}; uint16_t ArrayRead[2]; int main(void){ Key_Init(); OLED_Init(); OLED_ShowString(1,1,"W:"); OLED_ShowString(2,1,"R:"); //开启RCC时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE); //PWR备份访问控制 PWR_BackupAccessCmd(ENABLE); while(1){ if(Key_GetNum()==1){ ArrayWrite[0]++; ArrayWrite[1]++; //写入备份寄存器,写入DR1单元(2字节) BKP_WriteBackupRegister(BKP_DR1,ArrayWrite[0]); 写入备份寄存器,写入DR2单元(2字节) BKP_WriteBackupRegister(BKP_DR2,ArrayWrite[1]); OLED_ShowHexNum(1,3,ArrayWrite[0],4); OLED_ShowHexNum(1,8,ArrayWrite[1],4); } //读入备份寄存器,读入DR1单元数据(2字节) ArrayRead[0] = BKP_ReadBackupRegister(BKP_DR1); //读入备份寄存器,读入DR2单元数据(2字节) ArrayRead[1] = BKP_ReadBackupRegister(BKP_DR2); OLED_ShowHexNum(2,3,ArrayRead[0],4); OLED_ShowHexNum(2,8,ArrayRead[1],4); } }
2.实时时钟
开启PWR和BKP时钟,使能BKP和RTC访问
启动RTC时钟,计划使用LSE作为系统时钟,默认关闭,需要手动开启
配置RTCCLK前面的数据选择器,指定LSE为RTCCLK
期间调用寄存器同步等待,以及上一步操作完成等待。
配置预分频器,设置PRL一个合适的分频值,确保时钟频率1Hz
配置计数器,设置CNT的值,是一个初始的时间。
需要闹钟可以配置闹钟值;需要中断可以配置中断部分
RTC比较简单,没有结构体来配置,也没有RTC_Cmd使能函数;开启时钟就能自动运行。
-
新建MyRTC.c和MyRTC.h,放到System文件夹
#ifndef __MYRTC_H_ #define __MYRTC_H_ extern uint16_t MyRTC_Time[]; void MyRTC_Init(void); void MyRTC_SetTime(void); void MyRTC_ReadTime(void); #endif
#include "stm32f10x.h" // Device header #include <time.h> //存放时间的数组 uint16_t MyRTC_Time[] = {2024,7,27,16,3,58}; void MyRTC_SetTime(void); //初始化RTC void MyRTC_Init(void){ //开启RCC时钟,BKP和PWR RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE); //PWR备份访问控制 PWR_BackupAccessCmd(ENABLE); //判断BKP_DR1中的标志,如果是设置好的标志,就说明电池没掉电。 if(BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5){ //掉电了,就初始化 //初始化LSE时钟,开启LSE时钟 RCC_LSEConfig(RCC_LSE_ON); //等待标志,LSE准备,开启完成标志 while(RCC_GetFlagStatus(RCC_FLAG_LSERDY)==RESET); //配置RTC时钟,时钟源选择LSE RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //TRC时钟使能。 RCC_RTCCLKCmd(ENABLE); //等待同步 RTC_WaitForSynchro(); //等待任务完成 RTC_WaitForLastTask(); //32768分频,由于LSE是32.768KHz,分频为1Hz RTC_SetPrescaler(32768 - 1); //等待操作完成 RTC_WaitForLastTask(); //设置时间,下面函数,设置经过处理的时间。 MyRTC_SetTime(); //完成初始化后,在BKP_DR1写入标志,判断是否掉电。掉电则值清空。 BKP_WriteBackupRegister(BKP_DR1,0xA5A5); }else{ //没掉电,就不需要初始化。 //等待时钟同步 RTC_WaitForSynchro(); //等待操作完成 RTC_WaitForLastTask(); } } //设置RTC时钟时间 void MyRTC_SetTime(void){ //存放时间戳 time_t time_cnt; //存放转换的日期 struct tm time_date; //读取数组中的日期,转换为时间戳。 //年:偏移1900 time_date.tm_year = MyRTC_Time[0] - 1900; //月:偏移 1 time_date.tm_mon = MyRTC_Time[1] - 1; //日 time_date.tm_mday = MyRTC_Time[2]; //时 time_date.tm_hour = MyRTC_Time[3]; //分 time_date.tm_min = MyRTC_Time[4]; //秒 time_date.tm_sec = MyRTC_Time[5]; //数组转换为时间戳,同时减去时间偏移8小时,数组中表示东8区的北京时间,时间戳存放格林尼治时间。 time_cnt = mktime(&time_date) - 8 * 60 * 60; //设计计数器,设置为当前时间 RTC_SetCounter(time_cnt); //等待操作完成 RTC_WaitForLastTask(); } //读取时间 void MyRTC_ReadTime(void){ //存放时间戳 time_t time_cnt; //存放日期 struct tm time_date; //读取计数器值,是当前时间戳,加上8h是北京时间。 time_cnt = RTC_GetCounter() + 8 * 60 * 60; //将时间戳转换为日期 time_date = *localtime(&time_cnt); //日期存放到数组中。 MyRTC_Time[0] = time_date.tm_year + 1900; MyRTC_Time[1] = time_date.tm_mon + 1; MyRTC_Time[2] = time_date.tm_mday; MyRTC_Time[3] = time_date.tm_hour; MyRTC_Time[4] = time_date.tm_min; MyRTC_Time[5] = time_date.tm_sec; }
-
main
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "MyRTC.h" int main(void){ OLED_Init(); MyRTC_Init(); OLED_ShowString(1,1,"Date:XXXX-XX-XX"); OLED_ShowString(2,1,"Time:XX:XX:XX"); OLED_ShowString(3,1,"CNT :"); OLED_ShowString(4,1,"DIV :"); while(1){ MyRTC_ReadTime(); OLED_ShowNum(1,6,MyRTC_Time[0],4); OLED_ShowNum(1,11,MyRTC_Time[1],2); OLED_ShowNum(1,14,MyRTC_Time[2],2); OLED_ShowNum(2,6,MyRTC_Time[3],2); OLED_ShowNum(2,9,MyRTC_Time[4],2); OLED_ShowNum(2,12,MyRTC_Time[5],2); OLED_ShowNum(3,6,RTC_GetCounter(),10); OLED_ShowNum(4,6,RTC_GetDivider(),10); } }
9.PWR电源控制
-
PWR(Power Control)电源控制:PWR负责管理STM32内部的电源供电部分,可以实现可编程电压监测器和低功耗模式的功能
-
PVD:可编程电压监测器(PVD)可以监控VDD电源电压
【了解即可】
当VDD下降到PVD阀值以下或上升到PVD阀值之上时,PVD会触发中断,用于执行紧急关闭任务。
-
低功耗模式:可在系统空闲时,降低STM32的功耗 ,延长设备使用时间
三种模式,从上到下,关闭电路越来越多,越来越省电,越来越难唤醒
降低主频也可以省点。
-
睡眠模式(Sleep)
执行完WFI/WFE指令后,STM32进入睡眠模式,程序暂停运行,唤醒后程序从暂停的地方继续运行
SLEEPONEXIT位决定STM32执行完WFI或WFE后,是立刻进入睡眠,还是等STM32从最低优先级的中断处理程序中退出时进入睡眠
在睡眠模式下,所有的I/O引脚都保持它们在运行模式时的状态
WFI指令进入睡眠模式,可被任意一个NVIC响应的中断唤醒
WFE指令进入睡眠模式,可被唤醒事件唤醒
-
直接调用WFI或WFE,即可进入睡眠模式
两个内核指令,可以使用对应函数调用
-
WFI:Wait For Interrupt 等待中断,任意中断可以唤醒,醒来后进入中断处理
-
WFE:Wait For Event 等待事件,唤醒事件可以唤醒,醒来后不需要进入中断,而是从睡的地方继续运行。
外部中断配置为事件模式
使能中断,但没有配置NVIC
-
睡眠模式影响:
只把CPU时钟关了,对其他电路无操作
-
对1.8V区域时钟:CPU时钟关,对其他时钟和ADC时钟无影响
-
对VDD 区域时钟:无
-
对电压调节器操作:开
-
停机模式(Standby)
执行完WFI/WFE指令后,STM32进入停止模式,程序暂停运行,唤醒后程序从暂停的地方继续运行
1.8V供电区域的所有时钟都被停止,PLL、HSI和HSE被禁止,SRAM和寄存器内容被保留下来 在停止模式下,所有的I/O引脚都保持它们在运行模式时的状态
当一个中断或唤醒事件导致退出停止模式时,HSI被选为系统时钟
当电压调节器处于低功耗模式下,系统从停止模式退出时,会有一段额外的启动延时
WFI指令进入停止模式,可被任意一个EXTI中断唤醒
WFE指令进入停止模式,可被任意一个EXTI事件唤醒
-
进入停机模式:
-
设置SLEEPDEEP = 1,告诉CPU可以进入深度睡眠模式
-
PDSS位用来区分进入停机模式还是待机模式
PDDS = 0 停机模式
PDDS = 1 待机模式
-
LPDS位设置电压调节器
LPSD = 0 电压调节器开启
LPDS = 1 电压调节器进入低功耗
-
调用WFI或WFE进入停机模式
-
-
停机模式唤醒:
-
WFI:任意【外部】中断,以及PVD、RTC闹钟、USB唤醒、ETH唤醒借用了外部中断通道,因此也可以。
-
WFE:外部中断的事件模式唤醒
停机模式影响
对1.8V区域时钟:关闭所有1.8V区域时钟
CPU停止运行、外设停止运行、定时器暂停、串口停止收发
CPU和外设寄存器数据维持原状
对VDD 区域时钟:HSI和HSE的振荡器关闭
LSI和LSE不会主动关闭,开启过这两个时钟,还会继续运行
对电压调节器操作:LPDS控制,开启或低功耗模式
可以维持1.8V区域寄存器和存储器内容
低功耗模式更省电,但唤醒时需要更多时间
待机模式(Stop)
执行完WFI/WFE指令后,STM32进入待机模式,唤醒后程序从头开始运行
整个1.8V供电区域被断电,PLL、HSI和HSE也被断电,SRAM和寄存器内容丢失,只有备份的寄存器和待机电路维持供电
在待机模式下,所有的I/O引脚变为高阻态(浮空输入)
WKUP引脚的上升沿、RTC闹钟事件的上升沿、
NRST引脚上外部复位、IWDG复位退出待机模式
-
进入待机模式:
-
设置SLEEPDEEP = 1,告诉CPU可以进入深度睡眠模式
-
PDSS = 1位用来区分进入待机模式
PDDS = 0 停机模式
PDDS = 1 待机模式
-
调用WFI或WFE进入停机模式
-
-
待机模式唤醒:
-
普通外设的中断和外部中断,都无法唤醒待机模式
-
只有指定信号可以唤醒待机模式
-
WKUP引脚(PA0)上升沿
PA0引脚上升沿
-
RTC闹钟事件
闹钟定时器
-
NRST引脚的外部复位
复位键
-
IWDG复位
独立看门狗复位
待机模式影响
对1.8V区域时钟:关闭所有1.8V区域时钟
对VDD 区域时钟:HSI和HSE的振荡器关闭
对电压调节器操作:关闭
1.8V区域的电源关闭,内部寄存器和存储器的值全部丢失
不会主动关闭LSI和LSE两个低速时钟
电源框图
-
VDDA 供电区域:主要负责模拟部分的供电
电路正极是VDDA ;电路负极是 VSSA
-
A/D转换器
VREF+ 和VREF- 是AD转换器参考电压供电引脚,引脚少的型号会在内部接到VDDA 和VSSA
-
温度传感器
-
复位模块
-
PLL锁相环
VDD 供电区域:由VDD 供电区域和 1.8V供电区
VDD 供电区域:
IO电路
待机电路
唤醒逻辑
独立看门狗
1.8V供电区:大部分关键电路,以1.8V低电压运行
VDD 通过【电压调节器】,降压到1.8V
低电压可以降低功耗
CPU核心
存储器
内置数字外设
后备供电区域:
由低电压检测器控制开关
VDD 有电时,由VDD供电
VDD 没电时,由VBAT供电
LSE 32K晶体振动器
后备寄存器
RCC BDCR寄存器
叫备份域控制器,是RCC的寄存器。
RTC实时时钟
上电复位和掉电复位
-
当VDD 或 VDDA 电压过低时,内部电路直接产生复位
-
在复位和不复位的界限之间,有一段40mV迟滞电压,超过上限POR时解除复位,小于下限PDR时复位
-
复位信号Reset低电平有效,电压过低时复位;中间电压正常时不复位。
-
数据手册
-
上电/掉电复位阈值:
迟滞电压阈值40mV = 1.92V – 1.88V
-
下降沿:PDR掉电复位的阈值下限;
典型值1.88V
-
上升沿:POR上电复位的阈值上限:
典型值1.92V
复位持续时间:
典型值2.5ms
可编程电压检测器
-
测VDD 和 VDDA 的供电电压。
-
PVD的阈值电压可以使用程序指定,自定义调节。
配置PLS寄存器的3个位,使用迟滞比较,因此有两个阈值,范围是2.2V ~ 2.9V 左右,迟滞电压100mV
-
正常供电3.3V
-
当电压降低,在2.2V ~ 2.9V之间,属于PVD监测范围,可以通过PVD设置警告线
-
当电压继续降低,在1.9V,就是复位电路监测范围,低于1.9V直接复位。不让动
电压过低,PVD输出1;电压正常时为0;
PVD信号可以去申请EXTI外部中断,在上升沿或下降沿触发中断。
1. 修改主频
后续暂时没学
10. 看门狗
-
看门狗WDG:当程序卡死或跑飞时,看门狗可以自动复位程序
本质上是一个定时器,在规定时间内没有重置计数器(喂狗),看门狗硬件电路就会自动产生复位信号。
-
独立看门狗:独立工作,对时间要求精度低。
喂狗不能过晚
内部低速时钟LSI(40kHz)
-
窗口看门狗:要求看门狗在精确计时窗口起作用。
喂狗不能过早、也不能过晚
使用APB1的时钟
-
独立看门狗框图
-
低速时钟LSI进入预分频器,最大进行256分频
-
IWDG_PR预分配寄存器,配置分频系数(PSC)
-
递减计数器,最大4095,自减到0时产生IWDG复位
-
设置重装载数值IWDG_RLR(ARR),重置递减计数器
-
防止复位,设置键寄存器,控制电路进行喂狗。重装值会复制到递减计数器中,防止自减到0复位。
-
状态寄存器SR:有两个更新同步位,基本不用看
-
上面寄存器,位于1.8V供电区。
-
下面工作电路,位于VDD供电区。即在停机和待机模式时仍能正常工作。
-
-
键寄存器
-
本质是控制寄存器,控制硬件电路的工作
-
由于程序跑飞、收到干扰等,为了降低干扰,因此不使用一位标志位,使用写入特定值操作。
-
-
IWDG超时时间
{T_{IWDG} = T_{LSI} \times {PR预分频系数} \times {(RL + 1)}}
{T_{LSI} = {1 \over F_{LIS}} }
-
{F_{LIS} = 40KHz} 输入时钟频率
-
{T_{LSI} = 0.025ms} 输入时钟周期
-
{PR预分频系数} 固定
ps.输入2,就是16分频
-
RL 计数目标,12位计数器
-
T_{IWDG} 超时时间
-
窗口看门狗框图
作者:keyidao666