STM32个人学习笔记记录 2024-9-5

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.系统结构

  1. Cortex-M3内核:

    • ICode:指令总线,加载程序指令

    • DCode:数据总线,加载数据,比如常量和调试参数等

    • System:系统总线,

  2. Flash接口:连接Flash闪存,存放程序代码

  3. SRAM:存放程序运行时的变量数据

  4. AHB:先进高性能总线,挂载基本和高性能外设,如复位、时钟控制、SDIO等,频率72MHz,性能比APB高。

  5. 桥接:由于AHB和APB的总线协议、总线速度、数据传输格式等差异,试用桥接进行数据转换与缓存。

  6. APB:(Advanced Peripheral Bus)先进外设总线,连接一般外设, 需要了解外设挂载总线。

    • APB1:频率36MHz,连接次要外设。如DAC、PWR、BKP等

    • APB2:频率72MHz(一般与AHB频率相同),性能比APB1高,连接外设中稍重要的部分。如:GPIO端口,外设UASRT1、SPI1、TIM1(高级定时器)、TIM8(高级定时器)、ADC、EXTI、AFIO等

  7. DMA:拥有CPU总线一样的控制权,用于访问外设,访问并转运数据,节省CPU开销。

4.引脚定义

  1. 阅读说明:

    1. 颜色:

      • 红色:电源相关引脚

      • 蓝色:最小系统相关引脚

      • 绿色:IO口、功能口引脚

    2. 表头:

      • 引脚号与引脚名称:与芯片引脚一一对应

      • 类型:

        • S:电源

        • I:输入

        • O:输出

        • IO:输入输出

      • I/O口电平:表示IO口能容忍的电压

        • 默认:可以容忍3.3V电压

        • FT:可以容忍5V电平,需要加装电平转换电路

      • 主功能:上电后默认的功能,一般与引脚名称相同

      • 默认复用功能:IO口同时连接的外设功能引脚,配置IO口时可与主功能切换

      • 重定义功能:两个功能同时复用在一个IO口上冲突,可以把其中一个复用功能,映射到其他端口上,需要看表才可以映射

    3. 加粗:加粗优先推荐使用,没有加粗可能需要配置或兼具其他功能

  2. 引脚功能

    1. VBAT (1):备用电池供电引脚,可以接入3V电池,当系统电源断电时,备用电池可以给内部RTC时钟和备份寄存器提供电源。

    2. PC13-TAMPER-RTC(2):IO口、侵入检测、RTC。

    3. PC14-OSC32_IN/PC14-OSC32_OUT(3,4):IO口、接入32.768KHz的RTC晶振

    4. OSC_IN/OSC_OUT(5,6):系统总晶振,一般8MHz,内部有锁相环电路,可以倍频,最终产生72MHz频率作为系统主时钟。

    5. NRST(7):复位引脚,N代表低电平

    6. VSSA/VDDA(8,9):内部模拟部分电源,如ADC、RC震荡器。

    7. PA0(10):IO口、WKUP唤醒待机模式的STM32

    8. PA1-PA7/PB0-PB1(11-19):IO口

    9. PB2(20):IO口、BOOT1引脚配置启动模式

    10. PB10/PB11(21,22):IO口

    11. VSS_1/VDD_1(23,24):系统主电源口,分区供电

    12. PB12-PB15/PA8-PA12(25-33):IO口

    13. 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

    14. VSS_2/VDD_2(35,36):系统主电源口,分区供电

    15. PB5-PB7(41-43):IO口

    16. BOOT0(44):启动配置

    17. PB8-PB9(45-46):IO口

    18. VSS_3/VDD_3(47,48):系统主电源口,分区共供电

  3. 提示:

    • IO口:根据程序输出或读取高低电平的端口

    • 侵入检测:安全保障、防拆触点,引脚变化则检测到侵入信号,清空数据保证安全

    • RTC :输出RTC校准时钟、RTC闹钟脉冲、秒脉冲

    • 正负极:VSS负极接GND,VDD正极接3.3V。

    • 分区供电:供电口比较多,都接好即可,供电很多

    • 网络标号:相同标号进行连接。

    • 启动配置:指定程序开始运行位置。一般程序从Flash程序存储器开始运行;某些情况下可以在其他地方开始执行实现特殊功能

      BOOT1BOOT0说明解释
      X(任意)0主闪存存储器正常执行flash闪存内的程序(常用)
      01(3.3V)系统存储器串口下载,接收串口数据,刷新到主闪存中
      11内置SRAM程序调试

      BOOT引脚,在上电复位的一瞬间(SYSCLK第4个上升沿)有效,之后随意(就是另一个功能)。

5.最小系统

自己画板子,可以参考最小系统。

使用面包板的话,自带最小系统。

  1. 晶振电路

    1. 主晶振:8MHz主时钟晶振,倍频后得到72MHz主频

    2. 起震电容:20pF,另一端接地。

    3. RTC:如果需要RTC功能,还需要接个32.768KHz的晶振,电路相同,接在3、4号引脚

      32.768K = 215 ,内部RTC电路经过215 分频,可以产生1s时间信号

  2. 复位电路

    1. NRST:低电平复位,7号引脚

      上电瞬间,电容充电,电平先低再高,低电平时候可以进行复位

      也就是说,单片机上电一瞬间,就复位了

    2. 手动复位:按钮按下,电容放电,变成低电平触发复位,程序从头开始运行

      设备上的小孔,用针戳一下就复位了。

  3. 启动配置

    1. 开关配置:使用两个跳线帽,充当开关,配置BOOT

      当跳线帽连接:

      • 1、3:BOOT0置为高电平

      • 3、5:BOOT0置为低电平

      • 2、4:BOOT1置为高电平

      • 4、6:BOOT1置为低电平

      也可以使用其他开关

  4. 下载端口

    1. STLINK下载:引出SWDIO和SWCLK

    2. 电源:3.3V可以使用板子供电,但是建议引出

    3. 接地:必须

  5. STM32及供电

2.Keil5 MDK

1.安装Keil5 MDK

  1. 可以切换路径,然后一路下一步

  2. 弹出命令行窗口,安装ULINK

  3. 付费软件,个人使用,可以使用注册机

2.安装软件支持包

  1. 开发哪种芯片,就安装对应的支持包

    Project -> New μVision Project... -> 创建文件 -> Device -> Software Packs -> STMicroelectronics -> STM32F1 Series -> STM32F103 -> STM32F103C8 -> OK

  2. 安装方式:

    • 离线安装:直接点击支持包,进行安装

    • 在线安装(慢):在软件中点击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

  3. STLINK驱动

    1. 插入STLINK

    2. 查看设备管理器,是否存在 STM32 STLink,会出现红色感叹号则需要安装

    3. 安装一直下一步

  4. USB串口驱动

    同上面步骤

3.新建工程

  1. 开发方式:

    1. 基于寄存器开发:类似于开发51单片机,程序直接配置寄存器,实现功能,最底层、最直接、效率高的方式,但是stm32太过复杂,所以不推荐。

    2. 基于库函数开发:使用ST官方封装好的函数,调用函数间接配置寄存器,有利于提高开放效率【课程使用】

    3. 基于HAL库开发:利用图形化界面快速配置STM32,隐藏底层逻辑,适用于快速上手,方便。

  2. 构建工程

    1. 新建一个项目文件夹,用于存放项目,方便管理

    2. 打开keil5,使用创建项目,选择刚刚存放项目的文件夹

      Project -> New μVision Project... -> 选择刚才创建的文件 ->新建一个存放本项目的文件 -> 给工程起个名字 -> Device -> Software Packs -> STMicroelectronics -> STM32F1 Series -> STM32F103 -> STM32F103C8 -> OK

      Manager Run-Time Environment 可以快速创建工程。暂时关掉,这里是手动创建工程。

    3. 在项目文件夹下,新建Start文件夹,存放启动文件。

      目录:

      .\固件库\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm

      程序执行最基本的文件,用汇编写的。

      • 定义了中断向量表、中断服务函数

      • 中断服务函数中,有个复位中断,是整个程序的入口

        • 调用SystemInit()

          设置微控制器的启动

          初始化嵌入式闪存接口、锁相环、更新系统内核的时钟变量

          复位后调用

        • 调用main()

    4. 复制系统文件(3个)到Start文件夹

      目录:上二级目录中

      stm32f10x.h #描述stm32寄存器中,有哪些寄存器和它对应的地址

      system_stm32f10x.c / system_stm32f10x.c :配置时钟,主频72MHz也是从这里配置的

    5. 复制内核寄存器描述文件(2个)到Start文件夹

      目录: .\固件库\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\CoreSupport

      core_cm3.c / core_cm3.h :内核的寄存器描述与配置

    6. 在Keil中添加到工程

      1. 选择Source Group 1 重命名为 Start

      2. 添加已经存在的文件到Group,查看Start文件夹下的所有文件(All files)

      3. 选择一个启动文件:startup_stm32f10x_md.s

        根据芯片型号选择启动文件

      4. 选择所有的.c和.h文件

      5. 添加到工程中,图标中显示小钥匙(只读,不允许修改)

      6. 点击魔术棒按钮,添加头文件路径,否则找不到文件

      7. 选择C/C++ -> Include Paths -> ... -> 新建路径 -> ... -> 添加Start路径 -> OK

    7. 打开工程文件夹,新建User文件夹,作为main函数入口

    8. 将User添加到工程中(类似操作6)

      1. 在Target 1 下面,新建Group ,改名为User

      2. 添加新文件,main.c文件,手动更改Location路径,将文件放到文件夹中

      3. 在main.c中,编辑

        #include "stm32f10x.h" //这个是右键直接添加头文件

        int main(void){

        while(1){

        }

        }

        //必须添加死循环

        //最后一行,必须是空行,否则报错

    9. 可以对编译器字体等进行调节,然后就可以进行寄存器开发了

4.寄存器开发

  1. STLINK与最小系统板连接

  2. 将STLINK插到电脑上,板子上面的电源LED灯会常亮,另一个LED灯会闪烁(测试程序)

  3. 配置调试器下载程序,点击魔术棒按钮,配置ST-Link Debugger

  4. 点击旁边的Settings按钮,Flash Download --> 勾选Reset and Run

    下载程序后会立刻复位并执行,比较方便;否则下载程序后,需要手动复位才能执行程序

  5. 重新编译程序,检查是否有错误,没有错误后,点击DownLoad按钮,程序就可以下载到stm32了

    此时没有编程,原本的测试程序被覆盖,led不再闪烁。 Q1:STM32板子插不进去怎么办?

    A1:使用“跷跷板”的方法,先使一端插入,然后另一端再压进去,如此反复,慢慢就进去了。

  6. 此时可以进行 基于寄存器开发 了,如果想要 基于库函数开发 ,则需要添加库函数

5.库函数开发

  1. 在项目文件夹内,新建Library文件夹,用于存放库函数

  2. 找到库函数位置

    目录: .\固件库\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver\src

    misc.c :是内核的库函数,

    其他的.c文件:是内核外的外设库函数

  3. 复制所有库函数.c文件,到Library

  4. 同时找到库函数头文件,也复制到Library文件夹

    目录:上一级目录的另外一个文件夹

    .\固件库\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver\inc

  5. 在编译器里面新建组,命名为Library,将所有文件(.c和.h)添加进来

  6. 找到库函数配置文件,和中断函数文件(3个),复制到User目录下(存放main.c的目录)

    目录:

    .\固件库\STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Template

    stm32f10x_conf.h:用来配置库函数包含关系,和参数检查的函数定义

    stm32f10x_it.c/stm32f10x_it.h:存放中断函数

  7. 在编译器里面,User组中添加三个文件。

  8. 编译器添加宏定义,魔术棒按钮 -> C/C++ -> Define ,粘贴字符串:USE_STDPERIPH_DRIVER

    在main.c中

    右键#include "stm32f10x.h" ,跳转到定义

    在末尾找到宏定义代码

    #ifdef USE_STDPERIPH_DRIVER #include "stm32f10x_conf.h" #endif

    在编译器中添加宏定义 USE_STDPERIPH_DRIVER

  9. 完善其他配置:配置User、Library 包含路径

    魔术棒按钮 -> C/C++ -> Include Paths

  10. 完成配置,选择三个箱子按钮,可以更改组的顺序,更美观;完成之后可以直接编译(第一次比较慢),没有错误和警告,说明配置成功,可以进行基于库函数的编程。

3.GPIO

1.GPIO 简述

  1. GPIO:通用输入输出口

    1. 命名:GPIOx ,端口(16位)连接到内部包含驱动器和寄存器(32位),连接到APB2

      寄存器低16位对应每个端口,高16位没有用到

      寄存器负责存储数据。

      驱动器增大驱动能力,也就是输出电平(点灯)。

      1. GPIOA:PA0-PA15

      2. GPIOB:PB0-PB15

      3. GPIOC:PC0-PC15

    2. 8种工作模式:配置GPIO端口配置寄存器

      端口配置寄存器:每个端口需要4位,16个端口就需要64位==》因此有两个端口配置高/低寄存器

    3. 引脚电平:0V-3.3V,部分引脚可容忍5V

    4. GPIO输出速度:限制输出引脚最大翻转速度,降低功耗,提升稳定性,一般配置为50MHz

    5. IO输入输出

      1. 输出模式:可以控制端口输出高低电平

        • 用以驱动LED

        • 控制蜂鸣器

        • 模拟通信协议

        • 输出时序等

      2. 输入模式:可以控制端口读取高低电平或电压

        • 用于读取按键输入

        • 外接模块电平信号输入

        • ADC电压采集

        • 模拟通信协议接收数据等。

    6. 结构图

      构分为两部分:上半部分是输入,下半部分是输出

      可以有多个输入,但只能有一个输出;也就是说,输入模式下,输出无效,输出模式下,可以输入。

      1. 输入部分:从右部分的I/O引脚输入电平

        1. 信号输入:从右部分的I/O引脚输入信号

        2. 保护电路:电流不会流入内部电路

          保护二极管:有钳制电压的作用,压降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压降。

        3. 输入模式:提供输入默认电平

          • 上拉输入模式:上拉电阻开关闭合,下拉电阻开关断开,开关连接到VDD,电压被强制拉升到3.3V

          • 下拉输入模式:下拉电阻开关闭合,上拉电阻开关断开,开关连接到VSS,电压被强制拉升到0V

          • 浮空输入模式:下拉电阻开关与上拉电阻开关,都断开。引脚电平容易受到外接干扰而改变

          上拉电阻和下拉电阻比较大,属于弱上拉和弱下拉,尽量不影响输入操作

          弹簧模型:假设输出端是一个水平杆子,上拉电阻就是拴在屋顶的弹簧,将杆子上拉,下拉电阻就是拴在地面的弹簧,将杆子下拉。阻值越小,弹簧拉力越大,杆子高度就是电压,杆子居中则两个无穷大力在拉扯,电路上表现是短路,应当避免。

          ps. 哪边电阻小,哪边就导通;导通后接地,就是下拉;导通后接电,就是上拉。

        4. 施密特触发器:对输入电压进行整形

          • 如果输入电压,大于某一阈值,输出就会瞬间升高为高电平

          • 如果输入电压,小于某一阈值,输出就会瞬间降低为低电平

          图中写错为 肖特基触发器

          两个比较阈值之间,有一定的变化范围,可以有效的避免,因信号波动造成的输出抖动现象,维持信号稳定。

        5. 模拟输入:连接到ADC上,接收模拟量,连接在施密特触发器前面,施密特触发器后面所有电路均无效。

        6. 复用功能输入:连接到其他需要读取端口的外设上,接收数字量,连接在施密特触发器后面

        7. 端口输入数据寄存器:

          低16位对应16个引脚,高16位没有使用

      2. 输出部分:从右部分I/O引脚输出电平

        1. 输出数据寄存器:普通IO输出,操作位控制端口,设置好的输出,存放到这里

          低16位对应16个引脚,高16位没有使用

        2. 片上外设输出:可以选择不使用“输出数据寄存器”,而采用片上外设进行输出控制

        3. 位设置/清除寄存器:单独操作某一位,不影响其他位

          高16位进行位清除

          低16位进行位设置

          置为1才有效。

          另一个寄存器“端口位清除寄存器”:使用低16位,进行位清除操作。都使用16位操作方便。

          单独操作某一位:

          1. 可以通过运算实现:&= |=,但是比较麻烦

          2. 读写stm32位带区域:类似于位寻址,在某一段地址区域,映射了RAM和外设寄存器的所有位

          3. 位设置/清除寄存器(库函数):当某一位置为0时,在清除寄存器对应位置为1即可

        4. 输出模式

          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。

  2. 上拉电阻与下拉电阻

    单片机引脚的输出电源 是 +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输出

操作流程:

  1. 使用RCC开启GPIO时钟

  2. 使用GPIO_Init()函数初始化GPIO

  3. 使用输出或输入的函数,控制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闪烁
  1. LED:发光二极管,正向通电点亮,反向通电不亮。一般长脚为正极,短脚为负极

  2. 代码

    #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.蜂鸣器
  1. 有源蜂鸣器:内部自带震荡源,将正负极接上直流电压即可持续发声,频率固定

  2. 无源蜂鸣器:内部不带震荡源,需要控制器提供震荡脉冲才可以发声,调整提供震荡脉冲频率,可以发出不同声音。

    #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 输入

输入设备:

  1. 按键:按键按下,电路导通,松手断开

    按键抖动现象:机械弹簧抖动,产生不稳定信号,通常在5ms-10ms。

    过滤抖动:加一段延时,把抖动时间耗尽

    推荐使用上面的按键连接方式,按键按下为低电平。

  2. 传感器:传感器元件电阻随外界模拟量变化而变化,通过定值电阻分压,即可得到模拟电压输出,再通过电压比较器进行二值化,可得到数字电压输出。

    常见传感器元件:光敏电阻、热敏电阻、红外接收管等。

    二值化:利用运算放大器,进行电压比较,输出较大一方的,想要输出的结果。

    同相输入端电压>反相输入端电压:输出最大值VCC

    同相输入端电压<反相输入端电压:输出最小值GND

    模拟电压输出AO:利用上拉电阻和下拉电阻分压,传感器阻值变化的时候,传递不同电压。

    数字电压输出DO:利用电压比较,将模拟量AO二值化。

1.C语言定义简介
  1. 数据类型

    C51的int是16位,STM32的int是32位

    stdint关键字:利用typedef重新命名的关键字,数字代表位数。

    ST关键字是老版本的命名,新版本仍然支持,只是为了兼容老版本,不建议使用。

    【typedef 变量类型名 新名称】 只能替换类型,更安全。

  2. 宏定义:【#define 新名称 替换名称】,将任何字符替换到对应位置。

  3. 数组:多个同一类型变量。

  4. 结构体:多个不同类型变量。数据打包、伪面向对象。

    结构体引用成员:struct {char name;} a; a.name / (&a)->name

    利用typedef重新命名,更方便。

  5. 枚举enum:受限制的整形变量,不能使用未定义的变量。

    比如星期、月份,都是固定的,不能写出非法数值。

  6. 指针:C语言值传递,所以传递一个“位置编号”,才可以做到操作同一个东西。

2.按键控制LED
  1. 使用模块化编程,管理方便、代码整洁。

  2. 新建Hardware文件夹,存放硬件驱动,配置好组、工程路径文件夹等。

  3. 在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));
    }

  4. 在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;
    }

  5. 编写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.光敏传感器控制蜂鸣器
  1. 在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();
    	}
    }

  2. 在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);
    }

  3. 编写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.调试方式

缩小范围、控制变量、对比测试等

  1. 串口调试:通过串口通信,将调试信息发送到电脑端,电脑使用串口助手现实调试信息

    可以显示各种数据流、图像等,但是只能一行行显示,而且需要电脑。

  2. 显示屏调试:显示屏直接连接到单片机,将调试信息打印在显示屏上

    屏幕太小,显示内容有限,功能有限

  3. keil调试模式:keil软件的调试模式,可以使用单步运行、设置断点、查看寄存器及变量等功能。

  4. 点灯调试:在代码中穿插点灯代码,进行调试。

  5. 注释调试:合理利用注释,注释程序寻找问题所在,缩小范围。

  6. 对照调试:找到没有问题的程序,对照自己的程序,替换它的代码,缩小范围。

2.OLED简介
  1. OLED:有机发光二极管

  2. OLED显示屏:性能优异、功耗低(每个像素都是单独的发光二极管)、响应速度快(高刷新率、总线时序快、避免阻塞)、宽视角(自发光、任何角度都清晰)、轻薄柔韧等

  3. 0.96寸OLED模块:小巧、占用接口少、简单易用、常见的显示屏模块

    • 供电:3~5.5V,最好使用电源供电,而不是端口供电

    • 通信协议:I2C(一般4针脚使用)/SPI(一般7针脚使用)

    • 分辨率:128*64

  4. OLED驱动函数(第三方)

  1. 将 .\程序源码\STM32Project\1-4 OLED驱动函数模块\4针脚I2C版本 目录下的三个文件复制到Hardware文件夹中

  2. 在keil中,添加这三个文件到工程里

  3. 了解函数功能

    #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调试
  1. 选择调试方式:硬件调试【√】/模拟仿真

    • 模拟仿真

  2. 准备工作:连接好电路,确保代码编译没有错误

  3. 进入调试模式

    • 窗口

      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 -> 所有外设寄存器,可以实时显示数据变化。

  4. 修改代码:需要退出调试模式才可以修改,然后重新编译后,再进入调试模式

  5. Help查看官方文档

4.中断系统

1.中断简介
  1. 中断:在主程序运行过程中,出现了特定的触发条件(中断源),使得CPU暂停当前正常执行的程序,转而去处理中断程序,处理完成后返回到被暂停的位置,继续运行。

  2. 中断优先级:多个中断源同时申请中断的时候,CPU根据优先级高的先响应处理。

    中断嵌套:处理中断的时候,有优先级更高的中断源申请中断,那么CPU会暂停当前中断程序,去处理新的中断,处理完成后返回

  3. STM32中断:

    1. 68个可屏蔽中断通道(中断源),包含EXTI外部中断、TIM定时器、ADC模数转换器、USART串口、SPI通信、I2C通信、RTC实时时钟等多个外设

    2. 使用NVIC统一管理中断,每个中断通道都可以拥有16个可编程的优先等级,可以对等级进行分组,进一步设置抢占优先级和响应优先级。

2.STM32中断
  1. 中断表

    1. 深颜色的是内核中断,一般用不到

    2. 其他部分,是STM32外设中断

    3. 中断地址列表,就是中断向量表,是中断跳转的一个跳板。

  2. NVIC基本结构

    1. 一个外设,可能会同时占据n个通道,所以有n条线

    2. NVIC只有一个输出口,根据中断优先级分配中断先后顺序

    3. CPU处理中断,而不需要知道中断顺序

    4. NVIC优先级:每个中断有16个优先级(中断优先级寄存器:4位)

      5种分组方式

      • 抢占优先级:高n位

        中断嵌套,中断当前中断,优先执行紧急中断

      • 响应优先级:低4-n位

        紧急中断,插队到前面,等待前面的任务完成后,优先执行紧急中断

  3. EXTI:外部中断

    1. 可以监测指定GPIO口的电平信号,引脚电平变化,申请中断

    2. 支持触发方式

      • 上升沿:低电平变到高电平,触发中断

      • 下降沿:高电平变到低电平,触发中断

      • 双边沿:上升沿和下降沿都可以触发中断

      • 软件触发:程序执行代码触发

    3. 支持GPIO口:所有GPIO口,但相同的Pin不能同时触发中断

      也就是说,PA1和PB1不能同时使用,因为都是 Pin1的端口。

    4. 占用通道:16个GPIO_Pin,以及PVD输出、RTC闹钟、USB唤醒、以太网唤醒

      共有20个中断线路

      后面四个,是来外部中断蹭网的:由于外部中断可以从低功耗模式的停止模式下,唤醒STM32。

      • 对于PVD电源电压监测:电源从电压过低恢复时,需要PVD借助外部中断退出停止模式

      • 对于RTC闹钟:为了省电,RTC定了闹钟之后,STM32会进入停止模式,等到闹钟响了会再唤醒,需要借助外部中断。

      • USB唤醒、以太网唤醒,都是类似的作用。

    5. 触发响应方式:中断响应/事件响应

      • 中断响应:申请中断,让CPU执行中断函数

      • 事件响应:不触发外部中断,通向其他外设,触发其他外设操作。属于外设之间的联合工作。

    6. EXTI基本结构

      1. 每个GPIO的外设,都有16个引脚

      2. 由于EXTI模块,只有16个GPIO的通道,因此使用AFIO中断引脚选择模块(数据选择器)

      3. 不同GPIO外设,相同Pin端口不能同时触发EXTI,只能三选一。

      4. 对于4个蹭网外设,直接连接到EXTI

      5. 经过EXTI中断后,分为两种输出

        • 输出到NVIC触发中断:

          • EXTI9~EXTI5,分给一个通道

          • EXTI15~EXTI0,分给一个通道

        • 输出到其他外设响应事件:

          • 20条线路分给其他外设

2.对射式红外传感器计次
  1. 打开RCC所有外设的时钟:APB2的GPIOB、AFIO,外部中断EXTI不需要开启时钟,NVIC是内核外设无需手动开启

  2. 设置端口为输入模式

  3. 选择GPIO端口,通过AFIO引脚选择,连接到EXTI

  4. 配置EXTI,选择触发方式【上升沿、下降沿……】、触发响应方式【中断响应、事件响应】

  5. 配置NVIC,给中断设置合适的优先级,通过NVIC,外部中断信号就可以进入CPU

  1. 在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.旋转编码器计次

旋转编码器:用来测量位置、速度、旋转方向的装置。

  1. 读取:

    • 旋转轴旋转时,输出 与旋转速度和方向对应的方波信号

    • 读取方波信号频率和相位信息,可得知旋转轴速度和方向

  2. 类型:

    • 机械触点式:利用金属触点进行通断,左右两部分触点,中间有个按键,编码盘经过设计,旋转会产生90°相位差(正交波形),可以检测方向。

      触点式,适合调节音量等功能,接触式不适合电机测速等高速旋转的地方。

    • 霍尔传感式:中心有磁铁,边上有霍尔传感器,磁铁旋转输出正交方波信号

    • 光栅式:遮挡透过,捕获变化边沿,无法测方向

    • 独立编码器元件:输入轴转动,输出波形

  3. 模块引脚

    • VCC:3.3V

    • GND:接地

    • A:A向输出,接到一个引脚

    • C:GND,暂时不用

    • B:B向输出,接到另一个引脚

      A 和 B 的Pin编号不要相同

  1. 在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);
    	}
    }
    1. 中断中,不要执行耗时过长的代码,比如Delay,主程序会严重阻塞

    2. 中断函数和主函数中,不可以操作同一个硬件、不可以调用同一个函数。可以使用变量传递数据,减少代码耦合。

5.TIM定时器

【默认情况下,所有定时器,内部基准时钟都是72MHz】

1.定时器基础介绍
  1. 定时器:可以对输入的时钟进行计数,在计数达到设定值时,触发中断

    STM32中,定时器基准时钟一般是主频72MHz

    也就是说,对72MHz计72个数(72M/72=1M,1/1M=1us),就是1MHz=1us

    => 72 / 72M = 1us

  2. 【16位计数器、与分频器、自动重装寄存器】的时基单元,在72MHz计数时钟下,最大可以实现59.65s的定时

    (65536 / 72M) * 65536 ≈ 59.65s

    65536个数,在72MHz频率下,最大每 (65536 / 72M) 秒触发一次中断,而最多可以触发65536个数,因此最大定时时间是(65536 / 72M) * 65536 ≈ 59.65s

    定时器级联:添加一个定时器,把前一个定时器的输出作为输入,最大定时时间扩大655362 倍。

    时基单元:由 16位计数器、与分频器、自动重装寄存器 三部分组成,都是16位。

  3. 定时器功能:

    • 定时中断

    • 内外时钟源选择

    • 输入捕获

    • 输出比较

    • 编码器接口

    • 主从触发模式等

  4. 定时器分类:根据复杂度和应用场景

    • 高级定时器(APB2):TIM1、TIM8

    • 通用定时器(APB1):TIM2、TIM3、TIM4、TIM5

    • 基本定时器(APB1):TIM6、TIM7

    课程学习:通用计算器

    不同单片机,拥有定时器资源不同,不要操作不存在的外设

  5. 定时器结构

    1. 基本定时器

      • 时基单元:由 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引脚

    2. 通用定时器

      • 时基单元:

        计数方式:【通用定时器、高级定时器】

        • 向上计数:从0 加到 目标值 后,产生中断

        • 向下计数:从目标值 减到 0 后,产生中断

        • 中央对齐:从0 加到 目标值 后,产生中断;然后从目标值 减到 0 后,产生中断;

        时钟选择:

        • 内部时钟:RCC,一般为72MHz

        • 外部时钟:

          • TIMx_ETR引脚的外部时钟

            1. 指定引脚接入外部方波时钟

            2. 然后配置内部极性选择、边缘检测、预分频器电路

            3. 输入滤波电路,对输入的波形进行滤波整形

              滤波使用:固定频率下采样,采样值都相同,代表信号稳定,就可以输出采样值,进行信号消除抖动。

            4. 滤波后的信号兵分两路:对于时钟输入是等价的

              • 进入ETRF触发控制器,可以选择作为时基单元的时钟

                外部时钟模式2:

                可以对ETR时钟计数,把定时器当做计数器来使用

              • 占用TRGI触发输入通道:当做外部时钟输入

                外部时钟模式1:

                占用TRGI触发输入通道

          • ITR信号:信号来自其他定时器,外部时钟模式1

            1. 接收其他定时器的TRGO输出

              TIM2的ITR0,接在TIM1的TRGO上

              TIM2的ITR1,接在TIM8的TRGO上

              以此类推

              可以实现定时器级联功能:

              比如:

              1. 初始化TIM3,使用主模式把更新事件映射到TRGO上

              2. 再初始化TIM2,选择ITR2,对应TIM3的TRGO

              3. 选择时钟为外部时钟模式1,TIM3的更新事件可以驱动TIM2的时基单元,实现定时器级联

          • TI1F_ED:连接输入捕获单元CH1引脚,从CH1引脚的边沿获取时钟,外部时钟模式1

            ED:边沿触发,上升沿和下降沿均有效

          • TI1FP1和TI1FP2获取时钟:外部时钟模式1和编码器模式

            TI1FP1:连接CH1引脚的时钟

            TI1FP2:连接CH2引脚的时钟

    3. 高级定时器

      上半部分和通用定时器相同,下半部分有差异

      1. 重复次数计数器:实现每隔几个计数周期,发生一次更新事件和中断

        对输出信号又进行一次分频,定时时间扩大65536倍

      2. DTG死区生成电路:生成一定时长的死区,防止在开关切换瞬间出现短暂的直通现象。

        延时一段时间,让桥臂的上下管全部关断,防止直通现象

      3. 互补输出:

        1. 可以输出一对互补的PWM波,可以驱动三相无刷电机(前三路)

        2. 第四路仍是一个输出,无变化,因为驱动三相无刷电机,只需要三路即可

      4. 刹车输入:给电机驱动提供安全保证

        当外部引脚BKIN产生刹车信号,或者内部时钟产生故障,控制电路会自动切断电机的输出,防止意外

  1. 时基单元:定时器核心功能

    • 预分频器PSC

    • 计数器CNT

    • 自动重装器ARR

  2. 运行控制:控制寄存器,操作时基单元的运行

    启动停止、向上向下计数等

  3. 时钟源:为时基单元提供时钟

    1. 内部时钟RCC:默认时钟,默认频率为72MHz

    2. ETR外部时钟(外部时钟模式2)

    3. 触发输入(外部时钟模式1):ETR外部时钟、ITRx其他定时器、TIx输入捕获通道

  4. 编码器模式:编码器独用的模式,普通时钟用不到

  5. 重复计数器:高级定时器专用,在时基单元与中断输出控制之间

  6. 状态寄存器:产生中断信号,会在状态寄存器里面设置一个中断标志位

    比如:

    • 更新中断UI

    • 触发信号TGI

    • 输入捕获与输出比较 CCxI

  7. 中断输出控制:状态寄存器的中断标志,经过中断输出控制,到NVIC申请中断

    中断输出控制:是中断输出的允许位

2.时序问题
  1. 预分频器时序

    【预分频器参数,从1变成2的过程】

    1. CK_PSC:预分频器的输入时钟,内部时钟一般为72MHz

    2. CNT_EN:计数器使能,高电平时,计数器正常运行,低电平时,计时器停止

    3. CK_CNT:计数器时钟,预分频器的时钟输出,也是计数器的1时钟输入

      • CNT_EN 计数器未使能,计数器时钟不运行

      • CNT_EN 计数器使能,计数器时钟运行

        • 预分配系数 = 1(前半段):计数器的时钟 = 预分配器前的时钟

        • 预分配系数 = 2(后半段):计数器的时钟 = 预分配器后时钟的一半

    4. 计数器寄存器:跟随时钟的上升沿,不断自增,计数器与重装值相等,并且在下一个时钟来临时,计数器清零,同时产生一个更新事件

    5. 更新事件UEV:计数器与重装值相等,并且在下一个时钟来临时,计数器清零,同时产生一个更新事件

    6. 预分频控制寄存器:用户读写使用的寄存器,并不直接决定分频系数

      用户更改分频系数不会立刻生效,等到本次计数周期结束时,产生了更新事件,预分频控制寄存器的值,会被传递到缓冲寄存器里,才会生效。

    7. 缓冲寄存器(影子寄存器):决定分频系数,图中带阴影的方块,都有影子寄存器

      预分频器、自动重装寄存器、捕获比较寄存器等

      可以设置是否使用:不使用的情况下,立刻更新;使用的情况下,需要等到本次周期结束后更新。

    8. 预分频计数器:预分频器内部靠计数来分频

      当预分频值 = 0,预分频计数器 = 0,直接输出原频率

      当预分频值 = 1,预分频计数器 = 0和1,每次回到0的时候,输出一个脉冲,就是二分频

    9. 公式:计数器计数频率 CK_CNT = CK_PSC ÷ (PSC + 1)

      计数器计数频率,取决于预分频器的分频,预分频器频率经过(PSC+1)分频,就是计数器计数频率

      PSC:预分频器的值,当PSC = 1 的时候,就是二分频

  2. 计数器分频

    【计数器内部时钟,分频系数 = 2 ( = 分频值 + 1),也就是二分频】

    1. CK_INT:内部时钟,默认72MHz

    2. CNT_EN:计数器使能,高电平起点

    3. CK_CNT:计数器时钟,分频系数 = 2(二分频),频率就是CK_INT内部时钟 / 2,

    4. 计数器寄存器:时钟在上升沿自增,图中ARR(重装值)=0036,再经过一个上升沿后,寄存器清零,发生计数器溢出,产生一个更新事件脉冲,设置一个更新中断标志位UIF

    5. 计数器溢出:计数器寄存器到达重装值,再经过一个上升沿后,寄存器清零,发生计数器溢出,产生一个更新事件脉冲,设置一个更新中断标志位UIF

    6. 更新事件脉冲UEV:计数器溢出,产生一个更新事件脉冲,设置一个更新中断标志位UIF

    7. 更新中断标志位UIF:标志位置为1,就可以去申请中断。

      响应后的中断,需要手动清零

    8. 计数器溢出频率:倒数就是【计时时间】。

      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) 的情况下,可以有多少次记录

  3. RCC时钟树

    以AHB划分,AHB左部分,都是时钟产生电路,右边是时钟分配电路

    1. 震荡源:产生时钟

      • 内部8MHz,高速RC震荡器

      • 外部4-16MHz,高速石英晶体震荡器

        也就是晶振,一般8MHz,比内部的稳定

      • 外部32.768KHz,低速晶振

        一般给RTC提供时钟

      • 内部40KHz,低速RC振荡器

        可以给看门狗提供时钟

      两个高速晶振,都可以给系统提供时钟

      可以给AHB、APB2、APB1等提供时钟,一般外部晶振更稳定,但是简单的系统要求不高,可以使用内部RC振荡器,省略外部晶振电路。

    2. ST配置时钟:从SystemInit()函数开始

      1. 首先启动内部时钟,选择内部8MHz为系统时钟,暂时以内部时钟运行

        8MHz HSI RC 经过HSI,进入AHB

      2. 然后启动外部时钟,把系统时钟由8MHz切换为72MHz

        OSC_OUT与OSC_IN 的4-16MHz HSE OSC 经过PLLXTPRE-->PLLSRC --> 进入PLLMUL锁相环进行倍频(8MHz倍频9倍 = 72MHz),锁相环输出稳定后,经过PLLCLK进入AHB

        【外部时钟出问题,可能会导致系统慢近10倍】

    3. CSS:时钟安全系统,负责切换时钟,监测外部时钟运行状态,一旦外部时钟失效,自动把外部时钟切换为内部时钟。

      高级定时器,刹车输入部分,有CSS检测外部时钟

    4. 系统时钟,进入AHB后,就是进入了时钟分配电路

    5. AHB总线:系统时钟进入AHB总线的预分频器,在SystemInit()中配置分频系数

      72MHz的时钟,进入AHB总线,在AHB总线的预分频器中,分频系数 = 1,那么AHB的时钟就是72MHz

    6. APB1总线:AHB的时钟,进入APB1的预分频器,频率变为36MHz

      AHB的时钟就是72MHz,进入APB1总线,配置分频系数=2,APB1总线时钟 = 72MHz / 2 = 36MHz

    7. TIM2-TIM7时钟:所有定时器时钟,都是72MHz

      APB1总线时钟频率是36MHz,单独开一条支路,进行倍频,频率回到72MHz

      规则:

      • APB1的预分频系数 = 1,则频率不变

      • APB1的预分频系数 ≠ 1,频率 * 2

      不改变SystemInit()默认配置的前提下,【所有定时器,内部基准时钟都是72MHz】

      支路中,有与门:是外部时钟使能控制RCC_APB1PeriphClockCmd

    8. APB2总线:频率72MHz,分频系数 = 1

    9. TIM和TIM8时钟:所有定时器时钟,都是72MHz

      APB2总线给定时器,单独开一条支路,进行倍频,APB2分频系数 = 1,频率不变

      规则:

      • APB2的预分频系数 = 1,则频率不变

      • APB2的预分频系数 ≠ 1,频率 * 2

      不改变SystemInit()默认配置的前提下,【所有定时器,内部基准时钟都是72MHz】

      支路中,有与门:是外部时钟使能控制RCC_APB2PeriphClockCmd

3.定时器定时中断与定时器外部时钟

1.定时器定时中断
  1. RCC开启时钟,定时器基准时钟和整个外设的工作时钟都会同时打开

  2. 选择时基单元的时钟源,对于定时中断,选择内部时钟源

    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引脚的预分频器、极性、滤波器参数
  3. 配置时基单元,一个结构体就可以配置好

    void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);	//配置时基单元(要配置的定时器,初始化结构体)

  1. 配置输出中断控制,允许更新中断输出到NVIC

    void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);	//配置输出中断控制(要配置的定时器,配置的中断输出,使能)

  1. 配置NVIC,在NVIC中打开指定定时器中断通道,分配好优先级

  2. 运行控制,配置好模块后,需要使能一下计数器,不然计数器不会运行,计数器更新时,会触发中断

    void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);	//中断控制,开启定时器。

  1. 写定时器中断函数

  1. 新建文件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);
    }

  2. 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.定时器外部时钟
  1. 复制上面的工程,更改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:脉冲宽度调制

  1. 输出比较OC:可以通过比较CNT与CCR寄存器值的关系

    CNT:时基单元内的计数器

    CCR:捕获/比较寄存器,输入捕获与输出比较共用。

    • 来对电平进行操作,用于输出一定频率和占空比的PWM波形

    • 每个高级定时器和通用定时器,都有4个输出比较通道

    • 高级定时器的前3个通道,额外拥有死区生成和互补输出的功能

  2. PWM波形:在具备惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效低获得所需要的模拟参量,常用于电机控速领域

    频率:1 / Ts

    占空比:Ton / Ts

    线性等效:高电平5V,低电平0V,占空比等效于电压

    占空比50%:电压等效于0.5 * 5V = 2.5V

    占空比20%:电压等效于0.2 * 5V = 1V

    分辨率:占空比变化步距

    占空比变化的细腻程度

    要求不高,一般设置为分辨率 = 1%

  3. PWM基本结构:

    1. 黄色线:计数器重装值ARR

    2. 蓝色线:计数器值CNT

    3. 红色线:捕获/比较CCR

    CCR设置的高低,决定占空比的大小。

    • PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)

      计数器更新频率

    • PWM占空比:Duty = CCR / (ARR + 1)

      CCR设置的高低,决定占空比的大小。

    • PWM分辨率:Reso = 1 / (ARR + 1)

      步距范围在 0 ~ ARR

  4. 输出比较通道:

    1. 通用计时器比较通道

      1. CNT与CCR1进行比较,当CNT>CCR1或CNT=CCR1时,输出控制器就会传一个信号

      2. 输出控制器改变oc1ref电平

        ref:reference参考信号

        电平翻转:可以生成PWM波,电平翻转两次,PWM生成一个周期,占空比50%

        PWM模式:输出频率和占空比都可以调节的PWM波形

      1. ETRF输入:定时器小功能,无需了解

      2. 主模式控制器:把ref映射到主模式的TRGO输出上。

      3. CC1P:极性选择,是否要反转信号

      4. CC1E:选择输出使能电路是否要输出

      5. OC1:输出通道 CH1引脚

    2. 高级定时器比较通道

      互补输出:OC1与OC1N连接到推挽电路,控制输出模式

      死区生成:OC1与OC1N同时导通,器件容易发热损坏,所以OC1开关与OC1N开关之间延迟一小段时间。

  5. 舵机:

    1. 舵机是一种根据输入PWM信号占空比来控制输出角度的装置

    2. PWM信号要求:周期20ms,高低电平宽度为0.5ms~2.5ms

      旋转角度,线性分配:-90°~90°

    3. 拆解图

      内部由直流电机驱动,把PWM作为协议使用

      输入一个PWM波形,输出固定在一个角度

      • GND(棕色线):接地

      • +5V(红色线):驱动大功率设备需要+5V,可以单独供电

      • PWM信号线(橙色线):信号线,接PWM信号

  6. 直流电机:

    1. 直流电机:将电能转化为机械能的装置

    2. 旋转方向:有两个电极,正接正转,反接反转

    3. 电机驱动:大功率设备,GPIO无法驱动,使用电机驱动电路

      电机驱动芯片:TB6612、DRV8833、L9110、L298N、mos管等

  7. TB6612驱动芯片

    1. 双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向,由两路推挽电路组成

    2. 硬件电路

      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呼吸灯
  1. RCC开启时钟,把TIM外设和GPIO外设时钟打开

  2. 配置时基单元、时钟源选择

  3. 配置输出比较单元:CCR的值、输出比较模式、极性选择、输出使能等参数

  4. 配置GPIO,初始化为复用推挽输出的配置

  5. 运行控制,启动计数器

  1. 新建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);
    }

  2. 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驱动舵机
  1. 修改文件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);
    }

  2. 新建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);
    }

  3. 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驱动直流电机
  1. 修改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

    加大频率,修改预分频器的值,不会影响占空比,可以消除声音

  2. 新建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);
    	}
    }

  3. 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 只读

  1. 输入捕获IC:当输入通道出现指定电平跳变时,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数

    类似于外部中断,都是检测电平跳变然后执行动作。

    外部中断的动作是,向CPU申请中断;这里的动作是,控制后续电路,锁存CNT的值到CCR中

    • 通用定时器和高级定时器,有相同的输入捕获功能;基本定时器没有输入捕获

    • 可配制为PWMI模式(PWM输入模式),同时测量频率和占空比

    • 可配合主从触发模式,实现硬件全自动测量

  2. 测量频率方法:

    • 测频法:fx = {N \over T} (高频适合)

      在闸门时间T内,对上升沿(或下降沿)计次,得到N,可以计算频率 fx

    • 测周法:fx = {f_c \over N} (低频适合)

      在两个上升沿内,以标准频率fc 计次,得到N,可以计算频率

    • 中界频率:fm = \sqrt[]{f_c \over T}

      测频法和测周法误差相等的频率点,在N相等的前提下。

  3. 输入捕获通道:

    1. 三输入异或门:通道1,2,3端口,当任何一个引脚有电平翻转时,输出引脚就产生一次电平翻转,通过数据选择器,到达输入捕获通道1

      服务于三相无刷电机,根据转子位置换相

      TRC也是为了无刷电机的驱动

    2. 输入信号:输入信号进入输入滤波器和边沿检测器,进行信号滤波、边沿检测触发,当出现指定电平时,边沿检测电路会触发后续电路执行动作

      • 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的后续电路

      通道交叉连接:

      • 灵活切换后续捕获电路的输入

      • 把一个引脚的输入,同时映射到两个捕获单元

    3. 预分频器:分频触发信号,可以触发捕获电路的工作

      每有一个触发信号,CNT的值,就会向CCR转运一次,同时会发生一个捕获事件,事件会在状态寄存器置一个标志位,同时也可以产生中断。

    4. 硬件电路:

      1. TI1:CH1引脚,进来信号TI1

      2. 滤波器向下计数器:信号TI1,经过滤波器,输出滤波后的信号TI1F

      3. fDTS :滤波器的采样时钟来源

      4. CCMR1:CCMR1寄存器中的ICF位,可以控制滤波器的参数

      5. 边沿检测器:滤波后的信号TI1F,通过边沿检测器,捕获上升沿或下降沿

      6. CCER寄存器:CCER寄存器中的CC1P位,可以进行极性选择

      7. TI1FP1触发信号:通过数据选择器,进入通道1后续的捕获电路

      8. CCMR1:CC1S位,可以对数据选择器进行选择,ICPS位可以配置分频器

      9. CCER:CC1E位,可以控制使能或失能

      10. IC1PS:信号经过电路,到达IC1PS,就可以让CNT中的值,转运到CCR中

        每一次捕获CNT,都要把CNT清零(自动清零),以便于下一次捕获

      11. 从模式控制器:可以完成自动清零

    5. 主从触发模式

      主从触发模式:是主模式、从模式、触发源选择的简称

      1. 主模式:可以将定时器内部的信号,映射到TRGO引脚,用于触发其他外设

      2. 从模式:被别的信号控制,可以接收其他外设,或者自身外设的信号用于控制自身定时器运行,从模式可以从从模式列表中选择一项任务自动执行

        自动清零:触发源选择TI1FP1,通过TRGI,触发Reset,实现自动清零

      3. 触发源选择:选择从模式的信号源,是从模式的一部分;选择指定的一个信号,得到TRGI,使用TRGI触发从模式

    6. 输入捕获基本结构

      【测周法】:图中未使用交叉通道,只使用一个通道,目前只能测量频率:CCR1中的值就是N,fx = {f_c \over N}

      1. 配置好时基单元,启动定时器,计数器CNT,不断自增

        CNT计数:测周法用来的计数计时,fc 就是预分频器分频后,CK_CNT的时钟频率,标准频率fc = {72MHz \over (预分频系数)}

      2. GPIO输入:输入捕获通道1的GPIO口,经过滤波器、边沿检测、极性选择后,输出信号为TI1FP1。

      3. 分频器:设置TI1FP1上升沿触发,输入选择直连的通道,分频器选择不分频

      4. CCR1捕获/比较器:当TI1FP1出现上升沿后,CNT的当前计数值转运到CCR1里

      5. 触发源选择:CNT值转运到CCR1后,触发源选择选中TI1FP1为触发信号。

      6. 从模式复位操作:TI1FP1上升沿就会触发CNT清零

      • 如果频率太低,信号间隔时间变长,可能会导致CNT没有记录完成就会溢出,CNT:0~65535

      • 自动清零:如果想要使用自动清零,实现硬件自动化,就只能选择通道1和通道2的从模式;通道3和通道4只能开启中断手动清零,比较消耗资源

    7. PWMI基本结构

      【测周法】:图中使用交叉通道,使用两个个通道,可以同时测量频率和占空比。

      1. 通道1,进行频率测量(测周法,一个周期的时间,计算频率)

      2. 通道2,进行高电平测量(测周法,一个周期,高电平时间)

      3. 可以计算占空比。

1. 输入捕获模式测频率
  1. RCC开启时钟,打开GPIO、TIM时钟

  2. 初始化GPIO,配置成输入模式(上拉/浮空)

  3. 配置时基单元,计数器自增运行

  4. 配置输入捕获单元,滤波器、极性、通道模式(直连/交叉)、分频器等参数,一个结构体即可

  5. 选择从模式触发源TI1FP1

  6. 选择从模式,触发后的操作:自动清零

  7. 开启定时器TIM_Cmd

  1. 修改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);
    }

  2. 新建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);
    }

  3. 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模式测频率占空比
  1. 修改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);	
    }
    

  2. 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.编码器接口

可以通过定时器编码器接口,进行自动计次,节约软件资源;

之前使用外部中断,进行手动计次,程序频繁进入中断,消耗资源。

对于需要频繁执行,而且操作比较简单的任务,可以设计硬件电路自动完成

  1. 编码器接口(Encoder Interface):自动给编码器进行计次的电路

    每隔一段时间取一下计次值,就可以得到编码器旋转速度

    编码器测速:可用于电机控制

  2. 功能:可以接受增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲、自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度

    正交编码器:输出两个方波信号,相位相差90°,超期90°或滞后90°,分别代表正转和反转

  3. 位置:

    • 每个高级定时器和通用定时器,都拥有1个编码器接口

      如果定时器配置为编码器接口模式,基本上干不了其他活,资源比较紧张(4个定时器,全配置编码器的话,就没有定时器可以用,可以使用外部中断弥补)

    • 编码器的两个输入引脚,借用了输入捕获的通道1和通道2

  4. 编码器接口基本结构

    1. CH1通道1的TI1FP1,连接到编码器接口引脚TI1FP1

    2. CH2通道2的TI1FP2,连接到编码器接口引脚TI1FP2

    3. CH1和CH2的输入滤波器和边沿检测器,编码器使用;后面的是否交叉、预分频器、CCR寄存器等,与编码器接口无关

    4. 编码器接口输出:类似于从模式控制器,控制CNT计数时钟和计数方向,其中CK_PSC和时基单元初始化时设置的计数方向,并不会使用,都受编码器控制

    1. 两个GPIO接口,通过滤波器和边沿检测极性选择,产生TI1FP1和TI2FP2,通向编码器接口

    2. 编码器接口,通过预分频器,控制CNT计数器的时钟,同时根据编码器的旋转方向,控制CNT的计数方向。(正转自增和反转自减)

    3. ARR一般设置为 65535,最大量程,可以利用补码的特性,得到负数

  5. 工作模式:

    一般使用,第三种模式,精度最高

  6. 实例图:

    1. 正交编码器,抗噪声原理:如果上下波形,一个不变,一个跳变,那么一个噪声波形输入进来后,计数会先上升再下降,或先下降再上升,保持不变,实现抗噪声。

    2. 正向与反向:在编码器接口模式下,上升沿和下降沿都可能进行计次,那么极性选择就不再是边沿的极性选择,而是高低电平的极性选择,也就是高低电平是否反转。

      • 均不反相:IT1和IT2均不反转

      • TI1反相:TI1电平反转。分析的时候,把TI1的波形反相后,对照表格才能正确分析

        如果旋转方向反了,可以把任一个引脚反相,也可以把A和B引脚交换。

1.编码器接口测速
  1. RCC开启时钟,开启GPIO和定时器

  2. 配置GPIO,把PA6和PA7设置为输入模式

  3. 配置时基单元,选择不分频,自动重装给65535最大,只需要个CNT执行计数

  4. 配置输入捕获单元,只需要配置滤波器、极性两个参数

  5. 配置编码器接口模式

  6. 启动定时器TIM_Cmd

  7. 测量编码器位置:读取CNT的值

  8. 测量编码器速度方向:使用闸门,每隔一定时间取出CNT的值,然后清零

  1. 新建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;
    }
    

  2. 调用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);
    }

  3. 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模数转换器

电位器:滑动变阻器的作用,调节电阻,产生连续变化的模拟电压信号。

  1. 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,那么测量外部引脚电压可能不对,这时可以读取这个基准电压进行校准,可以得到正确电压值。

    • 规则组和注入组两个转换单元

      • 规则组:常规使用

      • 注入组:用于突发事件

      STM32的ADC增强功能,

      普通流程:启动一次转换->读一次值->再启动->再读值……

      STM32的ADC:列一个组,一次启动一个组,连续转换多个值

    • 模拟看门狗自动检测输入电压范围

      检测比较某个阈值,可以使用模拟看门狗自动执行

      模拟看门狗:可以监测指定的某些通道,当AD值高于它设定的上阈值或低于下阈值时,就会申请中断。

    • STM32F103C8T6的ADC资源:ADC1,ADC2,10个外部输入通道

      这个芯片,最多测量10个外部引脚的模拟信号

  2. 逐次逼近型ADC

    1. ADC0809是经典的ADC芯片,现在单片机性能和集成度提升,很多单片机内部已经集成了ADC外设,就不需要挂载外设芯片了。

    2. IN0~IN7:8路输入通道,通过通道选择开关,进入比较器【待测电压】。

      STM32,内部是18路输入的多路开关

    3. ADDA~ADDC:地址锁存和译码,选中哪个通道,就把通道号放到这三个引脚上

      输入IN0~IN7的地址

    4. ALE:锁存信号,对应通路的开关就可以自动拨好。

      拨好对应地址的开关

    5. 比较器:两个电压进行大小判断,如果DAC电压过大,就调小DAC数据;如果DAC电压过小,就调大DAC数据。直到DAC输出电压与外部通道输入电压近似相等。

      • 待测电压:通道选择开关传输过来的电压值

      • DAC:数模转换,近似到待测电压,输出DAC编码数据 。

    6. 逐次逼近寄存器SAR:利用二分法,逐次逼近找到近似电压,得到DAC数据。

      判断某一位是1还是0的过程,对于8位ADC,需要判断8次;12位判断12次。

    7. DAC:数模转换,利用加权电阻网络,不同数据对应不同大小的电压值。输出数据就是未知电压的编码

    8. 8位三态锁存缓冲区:输出DAC的编码数据,多少位就有多少根线输出。

    9. EOC:End Of Convert 转换结束信号

    10. START:开始转换信号,给一个输入脉冲,就开始转换。

    11. CLOCK:ADC时钟,因为ADC内部是一步一步进行判断的,利用时钟推动过程。

    12. VREF(+) 和 VREF(-) :参考电压。

      比如数据 255,对应的电压是多少V?由参考电压决定

      参考电压,也决定了ADC的输入范围,一般接在Vcc 和 GND,范围就是Vcc 和 GND

    13. Vcc 和 GND:芯片的供电与接地,通常会和参考电压接在一起。

  3. STM32的ADC

    1. ADC输入通道:

      • ADCx_IN1~ADCx_IN15:16个GPIO口

      • 温度传感器、VREFINT :两个内部通道,内部温度传感器和内部参考电压。

    2. 模拟多路开关:指定我们想要选择的通道,输出进入模数转换器

      • 普通ADC,多路开关一般只选择一个

      • 这里可以选择多个,而且分成两个组(菜单模型)

        菜单模型:普通ADC只能一个一个点菜,这个分组之后,可以直接点一个菜单的菜。如果菜单中只有一个菜,就退化成了普通ADC。

        • 注入通道组:最多可以选择4个通道

        • 规则通道组:最多可以选择16个通道

    3. 模拟至数字转换器:模数转换器,执行逐次比较的过程,转换的结果放到上面的数据寄存器中

    4. 数据寄存器:存放转换结果(餐桌模型)

      餐桌模型:餐桌上,最多能放1个菜,要上新菜的时候,就要把原来的菜撤走。

      其中,注入通道数据寄存器,可以放四个菜。

      1. 注入通道数据寄存器(4*16位):可以存放四个数据,最多4个数据不用担心数据覆盖。(涉及不多)

      2. 规则通道数据寄存器(16位):只能存放一个数据,最多16个数据的前15个会被挤掉,因此最好配合DMA实现(数据转运助手)

    5. 触发转换部分

      • START信号:开始转换。STM32有两种信号,可以触发ADC开始转换

        • 软件触发:程序中手动调用代码,可以启动转换

        • 硬件触发:主要来自于定时器、TRGO主模式输出

          防止频繁进入中断,消耗资源,硬件可以自动触发。

          • 注入组:转换完成,产生EOC和JEOC转换完成信号

          • 规则组:转换完成,产生EOC转换完成信号

          JEOC和EOC会在状态寄存器里面置标志位。 这两个标志位也可以进入NVIC申请中断

        • 外部中断引脚触发:在程序中配置

    6. 参考电压:芯片内部已经接好了。

      VDDA 接3.3V;VSSA 接GND,所以ADC输入电压范围就是:0~3.3V

      • VREF+ :参考电压,决定了ADC输入电压范围

      • VREF- :参考电压,决定了ADC输入电压范围

      • VSSA : 供电引脚,内部模拟部分电源,一般VREF+ 接入VSSA

      • VDDA :供电引脚,内部模拟部分电源,一般VREF- 接入VDDA

    7. ADCCLK:ADC时钟,驱动内部逐次比较的时钟,来自于ADC预分频器,最大是14MHz。

    8. 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分频

    9. DMA请求:用于触发DMA进行数据转运

    10. 模拟看门狗:存放一个阈值高限(12位)和阈值低限(12位)

      如果启动了模拟看门狗,并且制定了看门的通道,那么看门狗就会关注这个通道,一旦超过阈值,就会申请一个模拟看门狗中断,通向NVIC

  4. ADC基本结构

    1. 输入通道:16个GPIO口,和两个内部通道

    2. AD转换器:

      • 规则组:最多可以选中16个通道

      • 注入组:最多可以选中4个通道

    3. AD数据寄存器:存放转换的结果

      • 规则组:只有1个数据寄存器

      • 注入组:有4个数据寄存器

    4. 触发控制:提供开始转换的START信号

      • 软件触发:代码

      • 硬件触发:主要来自于定时器,也可以使用外部中断引脚

    5. RCC:来自RCC的ADC时钟CLOCK,ADC逐次比较过程就是由这个时钟推动

    6. 模拟看门狗:用于监测转换结果范围,超出设定阈值,就会通过中断输出控制,向NVIC申请中断

    7. 转换完成:

      • 规则组:完成转换后,会产生EOC信号,设置标志位,也可以通向NVIC

      • 注入组:完成转换后,会产生JEOC信号,设置标志位,也可以通向NVIC

    8. 开关控制:ADC_Cmd给ADC上电。

  5. ADC输入通道

    1. STM32F103C8T6:只有10个外部输入通道

      PA0~PA7

      PB0~PB1

      只有ADC1和ADC2

      其中,ADC1和ADC2的通道共用同一个,可以使用双ADC模式,也可以单独使用

      双ADC模式:ADC1和ADC2一起工作,可以配合组成同步模式、交叉模式等模式,提高采样率

    2. 整个系列最多有18个通道

      通道0~通道17

      ADC1:只有ADC1有通道16、17

      ADC2:GPIO的引脚,与ADC1引脚相同

      ADC3:有些变化

  6. 规则组的转换模式

    1. 单次转换,非扫描模式

      1. 规则组中的菜单,有16个空位,可以写入要转换的通道

      2. 非扫描模式:只有序列1有效,菜单同时选中一组的方式,退化为简单的选中一个

      3. 序列1位置,指定想要转换的通道

      4. 可以触发转换,ADC对通道2进行模数转换

      5. 过一小段时间后,转换完成,转换结果放在数据寄存器中,同时给EOC标志位,置1

      6. 看到EOC标志位,说明转换完成,可以在数据寄存器读取结果

      7. 再启动一次转换,就要再触发一次。

    2. 连续转换,非扫描模式

      1. 非扫描模式:只有序列1有效

      2. 连续转换:第一次转换后,不会停止,立刻开始下一次转换,一直持续下去。

      3. 只需要最开始触发一次,就可以一直转换。

      4. 无需判断是否结束,直接读取寄存器值

    3. 单次转换,扫描模式

      1. 单次转换:转换之后,就会停下来,下次转换需要再此触发才能开始

      2. 扫描模式:使用菜单列表,在序列中任意指定通道,并且通道可以重复,需要在结构体中配置通道数目,转换结果放到数据寄存器中,防止数据覆盖,要及时使用DMA挪走数据

      3. 需要触发下一次,才可以开始新一轮转换

    4. 连续转换,扫描模式

      类似的套路,不再说明。

    5. 间断模式

      在扫描模式下,扫描过程中,每隔几个转换,就暂停一次,需要再次触发才能继续。

  7. 触发控制

    信号源:

    • 定时器:定时器控制

    • 外部引脚/定时器:需要AFIO重映射确定

    • 软件控制:软件触发

  8. 数据对齐:数据寄存器16位,ADC有12位

    1. 数据右对齐:12位数据向右靠,高位补零,直接就是数据。(一般使用)

    2. 数据左对齐:12位数据向左靠,低位补零,数据比实际值大16倍,把数据左移了4次。如果不想要这么高的分辨率(0~4095),可以取出数据高8位,舍弃精度,12位ADC退化为8位ADC

  9. 转换时间

    1. 采用、保持:设置采样开关,存储外部电压,之后断开采样开关,进行AD转换,可以在量化、编码期间,电压始终保持不变,才可以精确定位未知电压位置。

    2. 量化、编码:ADC逐次比较的过程

    3. TCONV = 采样时间 + 12.5个ADC周期

      • 采用时间:采样、保持花费的时间,可以在程序中配置,采用时间越大,越能避免毛刺信号干扰。

      • 12.5个ADC周期:量化、编码花费的时间,12位ADC需要花费12个周期,半个周期做一些其他事情。

      • ADC周期:从RCC分频过来的ADCCLK,最大是14MHz

    4. 最快转换时间:当ADCCLK = 14MHz,采样时间为1.5个ADC周期

      TCONV = 1.5 + 12.5 = 14个ADC周期 = 1{\mu}s

      采样时间更长的话,就达不到1{\mu}s 时间,

      若把ADCCLK时钟设置超过14MHz,ADC超频,转换时间比1{\mu}s 更短,但是不稳定。

  10. 校准:过程是固定的,只需要在ADC初始化最后,加上固定几条代码

    1. ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差

    2. 建议在每次上电后执行一次校准

    3. 启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期

  11. 硬件电路

    1. 电位器:产生一个可调电压,可以输出0~3.3V电压,PA0可接入ADC输入通道

    当向上滑动时,电压增大;当向下滑动时,电压减小。

    电阻直接跨接在正负极,阻值太小比较费电,更小就可能发热冒烟。(KΩ级别)

    1. 传感器:传感器输出电压,可变电阻阻值没办法直接测量。

    光敏电阻、热敏电阻、红外接收管、麦克风等可以等效一个可变电阻,阻值没办法直接测量。

    测量可变电阻:串联一个固定电阻,阻值相近,进行分压,可以得到一个反应电阻值电压的电路

    杆子模型:可变电阻阻值变小,下拉作用变强,输出端电压下降;可变电阻阻值变大,下拉作用变弱,输出端电压生高

    当固定电阻与可变电阻,交换位置,那么输出极性就反过来。

    1. 电压转换电路:测量0~5V的电压,(对于5V、10V适用,更高电压不建议)

    ADC只能接受0~3.3V电压,那么可以使用电阻进行分压。

    PA2分得电压,和R2电压相同,得到电压范围0~3.3V,可以进行ADC转换。 分压公式: UPA2 = {R2 \over R1+R2}

    高电压采集,最好使用一些专用的采集芯片,如隔离放大器等

1.AD单通道
  1. 开启RCC时钟,ADC和GPIO,同时配置ADCCLK的分频器

  2. 配置GPIO,配置成模拟输入的形式

  3. 配置GPOI直连的,多路开关,接入到规则组列表中

  4. 配置ADC转换器,结构体配置,可以直接配置AD转换器、AD数据寄存器

单次转换/连续转换、扫描/非扫描、通道数量、触发源、对齐方式、

  1. 如果需要模拟看门狗,就配置阈值和检测通道,想要开启中断,就在中断输出控制中,配置ITConfig函数开启,之后NVIC中配置优先级,就可以触发中断了

  2. 开关控制ADC_Cmd,开启ADC。

  3. 校准ADC,减小误差

  1. 新建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);
    }
    

    可以设置为单通道、连续转换、非扫描,可以不需要手动触发开始,无需等待转换完成,直接读取值。【耗电】

  2. 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多通道

思路:

  1. 使用扫描模式,填入多个通道,实现多通道;但是数据会覆盖,需要转运数据

    • DMA转运数据

    • 手动转运数据:很困难,但也可行。

      • 扫描模式下,每个通达完成后不会产生标志位、也不会触发中断,只有当整个列表全部完成后才会产生一次EOC标志位,才能触发中断。

      • 转换一次数据只有几us,手动转运数据要求比较高

      • 可以使用间断模式,每扫描一个通道就暂停一次,手动转运数据后,继续触发进行下一次转换

        • 由于启动转换之后,没有标志位,只能通过Delay,延时足够长的时间,才能保证转运完成

        • 费力,不推荐使用

    • 单次转换,非扫描模式实现多通道

      • 每次触发转换之前,手动更改列表中,第一个位置的通道

  1. 修改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);
    }

  2. 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直接存储器存取
  1. DMA直接存储器存取:

    • DMA可以提供高速数据传输,无须CPU干预,节省了CPU的资源

      • 外设和存储器(一般使用硬件触发)

      • 存储器与存储器之间(一般使用软件触发)

    • 12个独立可配置的通道,每个通道都支持软件触发和特定的硬件触发

      • DMA1(7个通道)

      • DMA2(5个通道)

  2. STM32F103C8T6 的DMA资源

    • DMA1(7个通道)

    • 没有DMA2

  3. 存储器映像

    1. ROM:只读存储器,是一种非易失性、掉电不丢失的存储器

      • 程序存储器Flash:主闪存,存储C语言编译后的程序代码,是下载程序的位置,也是程序运行的开始

      • 系统存储器:存储BootLoader,是程序芯片出厂自动写入的,一般不允许修改。也可以用于串口下载。

      • 选项字节:存储一些独立于程序代码的配置参数,在ROM最后面,下载程序的时候,可以选择不刷新选项字节的内容,可以保持选项字节配置不变

        主要存储:Flash读保护、Flash写保护、看门狗等

    2. RAM:随机存储器,是一种易失性、掉电丢失的存储器

      • SRAM:存储运行过程中的临时变量,也就是程序中定义变量、数组、结构体的地方

        电脑的内存条

      • 外设寄存器:存储各个外设的配置参数,是初始化外设最终读写的东西

      • 内核外设寄存器:存储内核各个外设的配置参数,比如NVIC、SysTick

        内核外设和其他外设,不是一个厂家设计,因此地址分开

    3. 存储器地址范围:0x0000 0000 ~ 0xFFFF FFFF

      32位寻址范围,最大支持4GB的存储器,但是STM32内部存储器是KB级别,地址使用率不足1%

  4. DMA框图

    1. 可以把Cortex-M3看做CPU,剩下都是存储器(Flash、SRAM、各个外设寄存器)

      寄存器是连接软件和硬件的桥梁。软件读写寄存器,就是相当于控制硬件执行

    2. DMA数据转运:都可以看作是,从某个地址取内容,放到另一个地址。

    3. 总线矩阵:高效有条理的访问存储器

      • 主动单元:是总线矩阵的左端,拥有存储器的访问权

        • 内核: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发出硬件触发信号

  5. DAM基本结构

    1. 外设寄存器站点:外设寄存器

    2. 存储器站点:Flash、SRAM、

      手册中的存储器,一般特指Flash、SRAM,不包含外设寄存器

    3. DMA数据转运:可以配置转运方向

      • 外设到存储器

      • 存储器到外设

      • 存储器到存储器

        不允许SRAM到Flash,也不允许Flash到Flash,因为Flash只读

        如果进行数据转运:需要把其中一个存储器地址,放到外设这个站点。

        • Flash到SRAM

        • SRAM到SRAM

    4. 站点参数:起始地址、数据宽度、地址是否自增

      • 起始地址:决定数据从哪里来到哪里去

      • 数据宽度:指定一次转运,要按多大数据宽度进行

        字节Byte半字HalfWord字Word
        8位16位32位
      • 地址是否自增:下一次转运,是否要把地址移动到下一个位置

    5. 传输计数器:自减计数器,可以指定转运几次

      写传输计数器时,必须【先关闭】DMA,再进行写入

    6. 自动重装器:传输计数器减到0之后,是否要自动恢复最初的值

      • 单次模式:不重装

        复制数组,一轮结束

      • 循环模式:重装,执行一轮工作后,立即开启下一轮工作

        ADC扫描模式+连续转换,为了配合ADC,DMA需要使用循环模式

    7. DMA触发控制:决定DMA需要在什么时机进行转运

      触发模式由M2M参数决定

      1. 硬件触发:M2M设置为0,触发源可以选择多种,一般与外设有关的转运,需要一定的时机,达到时机时,传入信号触发DMA转运

        触发源:ADC、串口、定时器等

        一定时机:ADC转换完成、串口接收到数据、定时时间到等

      2. 软件触发:M2M设置为1,以最快的速度,连续不断的触发DMA,争取早日把计数器清零,完成这一轮循环;

        不能和循环模式同时使用。否则DMA会停不下来。

        用于存储器到存储器的转运,软件启动不需要时机,而且尽快完成任务

    8. 开关控制:DMA使能后,DMA就准备就绪,可以进行运转。

      DMA_Cmd函数

    9. DMA运行条件:

      1. DMA使能

      2. 计数器>0

      3. 触发源有触发信号

  6. DMA请求

    1. 这是DMA触发控制部分的结构图

    2. 图中有7个通道,每个通道都有一个数据选择器,可以选择软件或硬件触发

    3. EN不是数据选择器的控制位,而是当前通道的使能位,是控制开关

    4. M2M才是数据选择控制位,选择硬件触发或软件触发

    5. 外设请求信号:每个通道的硬件触发源都是不同的,需要特定的硬件触发,软件相同,只有开启DMA输出才可以硬件触发

      比如:

      • 用ADC1触发,就必须选择通道1

        ADC_DMACmd()

      • 用TM2通道3,可以触发通道1

        TIM_DMACmd()

      • 用TIM2更新事件触发,必须选择通道2

    6. 7个触发源进入仲裁器,进行优先级判断,最终产生内部DMA1的请求

      默认优先级:通道号越小,优先级越高

      也可以手动配置优先级

  7. 数据宽度与对齐

    1. 数据转移过程中,需要配置数据宽度参数

      • 数据宽度相同:正常一个个转运

      • 数据宽度不同:查表

    2. 数据宽度与对齐:

      • 数据宽度相同:正常移动

      • 源数据<目标数据 (高位补零)

        源端宽度目标宽度写入
        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数据转运
  1. 数据转运与DMA

    1. 外设地址:DataA数组首地址

    2. 存储器地址:DataB数组首地址

    3. 数据宽度:都是uint8_t,都是8位字节传输

    4. 地址自增:DataA[i] --> DataB[i] ,因此两个地址都需要自增

    5. 方向参数:外设站点转运到存储器站点。

    6. 传输计数器:一共7个数据,因此设置为7

    7. 自动重装:暂时不需要

    8. 触发选择部分:软件触发,是存储器到存储器的转运,不需要等待硬件时机,可以尽快完成

    9. DMA使能:调用DMA_Cmd

    10. 数据转运结束:传输计数器自减到0,DMA停止,转运完成

    11. 复制转运:转运后的DataA数据不会消失

  2. 小测试:对不同的变量,进行取地址操作,配合地址映射表,查看对应存储位置

    1. const常量:会存放在Flash中,节省SRAM空间,随机分配

      当程序中,出现大量无需更改的数据,可以使用常量

      如:OLED字库

    2. 一般变量:存放在SRAM中,随机分配

    3. 寄存器地址:外设寄存器区域,地址固定,手册可以查到

  3. 步骤:

    1. RCC开启DMA时钟

    2. 调用DMA_Init 初始化各个参数,一个结构体即可

      外设站点与存储器站点:起始地址、数据宽度、地址是否自增

      方向

      传输计数器

      是否需要重装

      选择触发源

      通道优先级

    3. 开关控制DMA_Cmd,给指定通道使能

    4. 如果使用硬件触发,要调用XXX_DMACmd,开启触发信号输出

    5. 如果需要DMA的中断,就要调用DMA_ITConfig,开启中断输出,配置NVIC

    6. 给传输计数器赋值时,要使DMA失能后,再写入,然后使能

  1. 新建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);
    }

  2. 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多通道
  1. ADC扫描模式与DMA

    1. 触发一次开始:7个通道依次进行AD转换,结果都放到ADC_DR数据寄存器

    2. DMA数据转运:在每个单独的通道转换完成后,进行一个DMA数据转运,并且目的地址(存放结果的位置)进行自增,防止数据覆盖

    3. 地址自增:外设地址不自增,存储器地址自增

    4. 传输方向:外设站点到存储器站点

    5. 传输计数器:一共7个通道,因此计数7次

    6. 自动重装

      • ADC单次扫描:DMA的传输计数器可以不自动重装,一轮停止

      • ADC连续扫描:DMA可以使用自动重装,ADC启动下一轮转换时,DMA也启动下一轮转运。二者同步工作

    7. 触发选择:需要与ADC单个通道转换完成同步,选择ADC硬件触发,虽然不产生任何标志位和中断,但是会产生DMA请求去触发DMA转运。

  1. 修改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);
    
    }

  2. 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);
    	}
    }

使用连续扫描

  1. 修改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);
    }

  2. 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通信接口

  1. 通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统

  2. 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发

STM32F103C8T6全部支持

双工模式:

  • 全双工:一般通信有两根通信线,发送线路和接受线路互不影响 (电话)

  • 半双工:通信有一根数据线,不能同时发送和接收。(对讲机)

  • 单工:只能从一个设备到另一个设备,不能反向 (广播)

时钟特性:

  • 同步:用单独的时钟线,接收方可以在时钟的指引下进行采样

  • 异步:没有时钟线,双方需要约定好一个采样频率,同时需要帧头帧尾进行采样位置对齐

电平特性:

  • 单端信号:高低电平是对于GND的电压差,单端通信的双方需要共地,把GND接在一起

  • 差分信号:靠两个引脚的电压差传输信号,不需要GND,但是协议里面有些地方需要单端信号,所以需要共地,差分信号抗干扰能力强,速度快、距离远。

设备:

  • 点对点:点对点通信 (单独谈话,一对一)

  • 多设备:可以在总线上挂在多个设备 (讲话,一对多,需要寻址确定对象)

1.串口通信

异步通信

  1. 特点:应用广泛、成本低、使用容易、通信简单,可以实现两个设备互相通信

    通信:

    • 单片机与单片机

    • 单片机与电脑

    • 单片机与各个模块

  2. 硬件电路

    1. 如果两设备都有独立供电模块,VCC可以独立供电不接在一起;如果其中一个没有供电,那么可以接在一起,有电的向没电的供电,注意供电电压要求。

    2. GND必须接在一起,共地

    3. TX与RX:交叉连接,发送与接收相连;只接一根,就是单工通信

    4. 注意电平标准,可以使用电平转换芯片,相同电平才能通信

      直接从控制器里输出的信号,一般是TTL电平

  3. 电平标准:是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系。

    1. TTL电平:+3.3V或+5V表示1,0V表示0

      最远几十米,课程基于TTL电平讲解,需要使用其他电平,可以加入电平转换芯片

    2. RS232电平: -3V~-15V表示1, +3V~+15V表示0

      一般在大型机器上使用,由于环境恶劣,静电干扰较大,因此电压和波动范围较大

      最远几十米

    3. RS485电平:两线压差+2V~+6V表示1, -2V~-6V表示0

      差分信号,抗干扰能力强,通信距离可以达到上千米

  4. 串口参数及时序

    1. 波特率:串口通信的速率

      异步通信,双方要约定好通信速率

      单位是,码元/s (或波特),表示每秒传输码元的个数

      比特率:每秒传输的比特数,单位bit/s (或bps)

      • 在二进制调制的前提下,一个码元就是一个bit,此时波特率 = 比特率

      • 在单片机串口通信中,基本都是二进制调制,所以经常混用

      • 如果是多进制表示,就不想等

    2. 起始位:标志一个数据帧的开始,固定为低电平

      空闲状态,没有数据传输的时候,是高电平

      起始位:发送数据的时候,先发送一个起始位,必须是低电平,也就是产生一个下降沿

    3. 数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行

      低位先发送

    4. 校验位:用于数据验证,根据数据位计算得来

      根据数据位计算出来

      奇偶校验位:可以数据是否传输错误,若出错了可以选择丢弃或重传,只能保证一定程度上的数据校验,要求更高可以选择CRC校验

      奇偶校验方式:如果有两位同时出错,那么校验不出来

      • 无校验:不需要校验位,波形图是左边的

      • 奇校验:包括数据位于校验位,会出现奇数个1

      • 偶校验:包括数据位于校验位,会出现偶数个1

    5. 停止位:用于数据帧间隔,固定为高电平

      停止位:为下一个起始位做准备,把引脚恢复为高电平

      停止位可以配置长度,可以把数据分割更宽

    6. 时序图

      • 串口发送数据的格式,是串口协议规定的格式

      • 串口中,每一个字节都装载在数据帧中。

      • 每个数据帧,都有:起始位、数据位、停止位,共8位组成,可以在数据位后面加上奇偶校验位,就是9位数据

  5. USART(Universal Synchronous/Asynchronous Receiver/Transmitter):通用同步/异步收发器

    1. 引脚

      • TX (Transmit Exchange):数据发送脚,也叫TXD

      • RX (Receive Exchange):数据接收脚,也叫RXD

      • GND:共地

    2. 特性:全双工、异步、单端、点对点

    3. USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里

    4. 自带波特率发生器,最高达4.5Mbits/s

    5. 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)

    6. 可选校验位(无校验/奇校验/偶校验)

    7. 支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN

      同步模式:多了个时钟CLK输出

      硬件流控制:防止发送太快,导致无法处理的现象,可以反馈接收者的状态,是否可以接收

      DMA转运数据

      智能卡

      IrDA:红外发光管与红外接收管,依靠闪烁红外光通信

      LIN:局域网通信协议

    8. STM32F103C8T6 USART资源: USART1、 USART2、 USART3

      USART1:是APB2设备

      USART2、 USART3:是APB1设备

  6. USART框图

    1. 左上角引脚部分

      • TX:发送引脚,连接到发送移位寄存器

      • RX:接收引脚,连接到接收移位寄存器

      • SW_RX、IRDA_OUT、IRDA_IN:是智能卡和IrDA的通信引脚,我们不使用这些协议,不需要管

    2. 数据寄存器:

      • DR寄存器:程序上表现为一个寄存器DR,实际上是两个寄存器

        1. 发送数据寄存器TDR:用于发送,只写,写入后硬件自动检查,当前移位寄存器是否有数据正在移位,如果没有,写入的数据就立刻,全部移动到发送移位寄存器,准备发送

        2. 接收数据寄存器RDR:用于接收,只读;当标志位RXNE(接收数据寄存器非空)置1,我们就可以把数据读走了

      • 移位寄存器:正好对应串口协议的波形数据位

        1. 发送移位寄存器:把一个字节数据,一位一位的移出去;当数据从发送寄存器移动到移位寄存器时,会置一个TXE标志位(发送寄存器空),如果标志位置1,就可以在TDR写下一个数据

        2. 接收移位寄存器:把一个字节数据,一位一位的读进来;当8位数据接收完成,就会把完整8位数据,整体转移到RDR中,同时会设置一个标志位RXNE(接收数据寄存器非空),当标志位置1,我们就可以把数据读走了。

      • 控制器

        1. 发送器控制:发送器控制发送移位寄存器,向右移位,一位一位的把数据输出到TX引脚

        2. 接收器控制:接收器控制接收移位寄存器,一位一位的读取RX高电平,先放在最高位,然后右移

        3. 硬件数据流控(流控):发送太快,接收端来不及处理,只能丢弃或覆盖的时候,流控可以避免这个问题

          【n是低电平有效】,一般不使用流控功能。

          nRTS与nCTS需要交叉连接

          • nRTS:请求发送,是输出脚,告诉发送端,当前能不能接受数据

          • nCTS:清除发送,是输入端,用于接收他人的nRTS信号。

        4. SCLK控制:用于产生同步的时钟信号,配合发送移位寄存器输出的,这个寄存器每移位一次,同步时钟电平就跳变一个周期;

          时钟支持输出,不支持输入;一般不使用时钟功能。

          时钟可以兼容其他协议:串口+时钟,和SPI协议很像

          时钟可以做自适应波特率:不确定发送设备的波特率时,可以测量这个时钟周期,计算得到波特率

        5. 唤醒单元:实现串口挂载多设备

          串口一般是点对点通信,只支持两个设备互相通信;一般不使用唤醒单元功能

          多设备:在一条总线上,可以接多个从设备,每个设备分配个地址,想要跟某个设备通信,就线进行寻址,确定通信对象,再进行数据收发。

        6. 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,接收器波特率控制使能,接收部分的波特率有效

  7. USART基本结构

    1. 波特率发生器:分频,产生约定的通信速率

    2. 时钟:时钟来源是PCLK2 或 PCLK1,经过波特率发生器分频后,产生的时钟通向控制器。

    3. 发送接收控制器:

      • 发送控制器:控制发送移位

        • 发送移位寄存器:使用发送数据寄存器TDR和发送移位寄存器,将数据一位一位移出去

          移位寄存器右移,低位先行

        • GPIO:一位一位的数据,通过GPIO复用输出,输出到TX引脚,产生串口协议规定的波形

        • 发送数据寄存器TDR:我们在软件层面,进行写入DR,发送数据

      • 接收控制器:控制接收移位

        • 接收移位寄存器:使用接收数据寄存器RDR和接收移位寄存器,将数据一位一位移动到,接收移位寄存器

          移位寄存器右移,低位先行

        • GPIO:RX引脚波形,通过GPIO输入,在接收移位控制器的控制下,将数据一位一位移动到,接收移位寄存器。

        • 接收数据寄存器RDR:当移完一帧数据后,数据会统一转运到接收数据寄存器,转移的同时,会置一个RXNE标志位,判断是否接收到数据,这个标志位也可以申请中断

    4. 开关控制:配置完成后,用Cmd启动外设

  8. 数据帧

    1. 字长选择:可以组成四种发送方式

      • 9位字长:

        • 有校验:一般使用

        • 无校验

      • 8位字长

        • 有校验

        • 无校验:一般使用

    2. 波形:

      • 空闲:高电平

      • 接收数据:

        • 起始位:低电平,数据帧开始

          当输入电路,侦测到数据起始位以后,就会以波特率的频率,连续采样一帧数据

          同时,从起始位开始,采样位置就要对齐到位的正中间,只要第一位对齐了,那么后面就都对齐了

        • 数据位:数据0~7位

        • 可能的奇偶校验位:数据第8位,可以配置无校验、奇校验、偶校验

        • 停止位:高电平,数据帧结束

          停止位时长:一般使用1个停止位

          1. 1个停止位

          2. 1.5个停止位

          3. 2个停止位

          4. 0.5个停止位

    3. 时钟:同步时钟输出功能

      • 每个数据位中间,都有时钟上升沿。

      • 时钟频率和数据速率相同

      • 接收端可以在时钟上升沿进行采样,就可以精准定位每一位数据

      • LBCL:时钟最后一位,可以桶LBCL位,控制是否输出

      • 时钟极性、相位可以通过配置寄存器配置

      • 采样时钟细分:以波特率16倍的频率进行采样,也就是1位数据,允许采样16次

        起始位采样:

        1. 对1位数据的进行16次采样:在下降沿后的

          • 第3、5、7次进行采样

          • 第8、9、10次进行采样【正中间】

        2. 要求每3位中,至少有2位至少都是0;

          • 如果全是0:没有噪声

          • 如果2个0,一个1:有噪声,在状态寄存器里置一个NE噪声标志位

          • 如果超过2个1:不算检测到起始位,忽略前面的检测,重新开始捕获下降沿

        3. 后续接收数据位时,都在【8,9,10】次进行采样

        数据采样:

        1. 采样分析【8,9,10】次采样

          • 其中全是0或全是1:说明收到了数据0或1

          • 有0有1:按照2:1的规则来,谁多认为收到了谁,同时噪声标志位NE置1

    4. 空闲帧:从头到尾都是1,局域网协议使用,串口不使用

    5. 断开帧:从头到尾都是0,局域网协议使用,串口不使用

  9. 波特率发生器:本质就是分频器

    1. 波特率由BRR里的分频系数DIV确定

    2. 分频系数DIV:分为整数部分和小数部分,可以实现更细腻的分频

    3. 波特率计算公式: 波特率 = { f_{PCLK2/1} \over (16 * DIV)}

      因为内部还有一个16被波特率的采样时钟,直接除会得到16倍的波特率

      波特率9600 = 72M / (16 * DIV) ,可以求出DIV = 468.75 = 1 1101 0100.11 写入分频器需要转化为二进制。

  10. CH340G模块原理图:USB转串口

    1. USB:

      • GND:接地

      • UD+、UD-:USB通信协议的数据线

      • VCC+5V:标准5V供电,为整个芯片供电;通过稳压管电路进行降压,得到VCC+3.3V。

      1. CH340G芯片:

        • CH340G芯片,将USB协议,转化为串口协议

        • TXD、RXD:串口协议

      2. 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脚

            优先确保供电正确,通电电平无法一致,小问题

      3. 指示灯与滤波:

        • PWR:电源指示灯

        • TXD:传输数据时闪烁

        • RXD:接收数据时闪烁

  11. 数据包:

    1. 数据包分类:

      1. HEX数据包:含有包头包尾,发送传输原始数据,解析简单,适合模块发送原始数据。

        1. 固定包长:可以避免数据与包头包尾发生冲突

          包头包尾不一定全需要,但是冲突问题更严重

        2. 可变包长:与包头和包尾不会冲突的情况,可以选用

      2. 文本数据包:含有包头包尾,数据经过编码译码,适合输入指令人机交互场景,解析效率低

        1. 固定包长:

        2. 可变包长:字符选择更多,基本上不担心包头包尾冲突问题

    2. 数据包发送:数组、字符串等,调用函数直接发送

    3. 数据包接收:状态机,处理包头、数据、包尾等不同状态,执行不同操作,且进行状态的合理转移

  12. 串口下载

    • FlyMcu串口下载:绿色版,直接打开

      1. 连接和电路,保证串口电路可以和【USART1】连接,进行串口通信

        芯片的串口下载,只适配了USART1,也就是GPIO_PIN_9和GPIO_PIN_10

      2. 在keil中,点击魔术棒按钮->OutPut->Create HEX file ->编译代码,可以在Objects目录下找到.hex文件

      3. 单片机配置BOOT引脚,让STM32执行BootLoader程序,将配置BOOT0 = 0引脚的跳线帽,配置为BOOT0 = 1,然后按下复位键。

        1. BootLoader程序:也叫自举程序,在【系统存储器】中,可以程序自我更新、串口下载,接收到USART1的数据刷新到程序存储器。

        2. BOOT引脚启动配置

        3. 跳线帽替代:

          • STM32一键下载电路,使用CH340G的RTS和DTR两个输出引脚(流控引脚当做普通引脚使用),来控制BOOT0和RST。

            软件下方的复位选项,根据复位电路来自行选择;如果没有下载电路,就随意选择无所谓

          • 使用“编程后执行”选项,取消勾选“选项字节区”,更改跳线帽后,刷入程序可以直接运行。不过复位后失效,可以把跳线帽接回来。

      4. 打开FlyMcu串口下载软件,进入串口和波特率选择,载入.hex文件,点击开始编程就可以下载程序

      5. 单片机的BOOT0引脚换回来,点击复位键,即可正常运行程序

      读Flash:点击读Flash按钮,选择存放路径后,可以将芯片中的程序读出来

      程序是.bin格式,记录了STM32从0800开始存储的程序数据

      清除芯片:点击清除芯片按钮,可以把程序擦除,所有数据变成FF,读取信息回把序列号信息读出来

      点击“设定选项字节等”按钮,选择“STM32F1选项配置”:设置好后勾选选项字节区的写入框,只能在下载程序过程中,顺便写入

      1. 读保护:开启读保护,防止程序被偷走

        • 阻止读出后,下载程序会失败。

        • 取消读保护,会清空芯片程序。

      2. 硬件选项字节:看门狗、停机模式和待机模式不产生复位、

      3. 用户数据字节:不论程序如何变化,选项字节内容可以不变,可以存储一些参数;也可以使用上位机方便修改,作为可供用户配置的参数

      4. 写保护:可以对Flash的每几个页,单独进行写保护,不想在下载的时候被擦除,可以设置写保护锁起来;如果写入保护区,会出错;而且不支持单独写某块字节,会死循环。

    • ST-LINK Utility:需要安装。

      1. 只需要连接好ST-LINK,可以不接串口,跳线帽恢复、复位

      2. 点击连接按钮

      3. Target ->Option Bytes -> 可以配置 选项字节配置,可以单独更新选项自己内容。

      4. ST-LINK固件更新:ST-LINK -> Firmware updata ->Connect ->提示重启,重插即可 ->yes更新

1.串口通信
  1. RCC开启时钟:GPIOA,USART

  2. GPIO初始化,TX配置为复用输出,RX配置为输入

  3. 配置USART,直接使用一个结构体,就可以配置所有参数

  4. 如果只需要发送功能,就可以直接开启USART,初始化结束

  5. 如果需要接收功能,需要配置中断,在开启USART之前,加上ITConfig,NVIC配置

  6. 初始化完成后,要发送数据或接收数据,直接调用函数;获取发送或接收状态,调用获取标志位函数

  1. 创建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);
    }	

  2. 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.串口发送与接收
  1. 修改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);
    	}
    }

  2. 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数据包
  1. 修改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);
    	}
    }

  2. 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);
    		}
    	}
    }

    程序可能会出现数据交叉的现象,由于读取一半的时候,来了新的数据。

    1. 可以在读取数据包时加入判断,只有读取完成后,才能接收下一个数据包

    2. 对于连续性的数据,可以不处理。

    3. 大多数情况下不处理。

4.串口收发文本数据包
  1. 修改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);
    	}
    }

  2. 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通信

同步通信

  1. I2C(Inter IC Bus):是由Philips公司开发的一种通用数据总线,也叫I2 C (I方C)

    MPU6050模块:姿态测量

    OLED模块:显示字符、图片等信息

    AT24C02:存储器模块

    DS3231:实时时钟模块

  2. 两根通信线:

    • SCL(Serial Clock):串行时钟线,同步时序

    • SDA(Serial Data):串行数据线

    • GND:共地

  3. 特点:

    • 同步:使用同步时序、降低对硬件的依赖,稳定性比异步高

    • 半双工:半双工模式,一根线兼具发送和接收,最大化利用资源

    • 带数据应答

    • 支持总线挂载多设备

      • 一主多从:单片机作为主机,主导I2C通信,挂载在I2C总线上的所有外部模块都是从机,

        从机在被主机点名后,才能控制I2C总线,不可以在未经允许的情况下去碰I2C总线,防止冲突

        课堂模型1:老师讲课,只有点名的同学才可以讲话

      • 多主多从:多个主机,在总线上任何一个模块都可以主动跳出来,但还需要进行时钟同步,协议比较复杂

        课堂模型2:老师讲课,同学突然站起来打断,说所有同学听我指挥;

        但是总线同一个时间只能有一个人主导,否则发生冲突,这时需要I2C协议仲裁,优先级更高的获得总线控制权,另一个变成从机

  4. 硬件电路(一主多从)

    1. CPU:我们的单片机,作为主线的主机,权利很大

      • 可以对SCL线完全控制

      • 空闲状态下,主机可以主动发起对SDA的控制;

      • 只有在从机发送数据和从机应答时,主机才会转交SDA的控制权给从机

    2. 被控ICx :挂载在I2C总线上的从机,从机权利比较小

      可以是姿态传感器、OLED、存储器、时钟模块等

      • 任何时刻,从机在都只能被动读取SCL时钟线信号

      • 从机不允许主动发起对SDA的控制,只有在主机发送读取从机的命令后,或者从机应答时,从机才可以短暂的获取SDA控制权

    3. 接线要求:

      • 所有I2C设备的SCL连在一起,SDA连在一起

        • SCL接线:

          • 一主多从模式,主机对SCL有绝对控制权,所以主机可以配置为推挽输出

            但是仍然采用开漏加上拉输出模式;因为多主机下有“线与”特性。

          • 从机SCL可以配置为,浮空输入或上拉输入

        • SDA接线:主机和从机都会在输入和输出之间切换,协调不好容易短路

          • I2C总线,禁止所有设备输出强上拉的高电平

          • 采用外置弱上拉电阻,加上开漏输出的电路结构

          • IC设备中,有强下拉电路,可以变成低电平和浮空状态。

          • 为了避免高电平造成的引脚浮空,可以在总线外SCL和SDA上,格接入弱上拉电阻

    4. 设备的SCL和SDA均要配置成开漏输出模式

      开漏输出模式:输出低电平、或浮空,没有高电平。

      • 根据杆子模型分析,在这个电路中,杆子不会处于一个被同时强拉或强推的状态,

      • 避免引脚模式的频繁切换,开漏加弱上拉模式,同时兼具了输入和输出的功能

        输出:进行拉杆子或放手,操控杆子变化

        输入:直接放手,观察杆子高低

        开漏模式下,输出高电平,就相当于断开引脚;因此在输入之前,可以直接输出高电平,不需要再切换输入模式

      • ”线与“ 现象:有设备输出低电平,那么总线就处于低电平;只有所有设备输出高电平,总线才处于高电平。

        可以实现多主机模式下,时钟同步和时钟仲裁

  5. I2C时序基本单元

    1. 起始条件:SCL从高电平期间,SDA从高电平切换到低电平

    2. 终止条件:SCL从高电平期间,SDA从低电平切换到高电平

    3. 只有主机,才可以产生起始和终止,不允许从机产生;因此总线空闲状态时,从机不允许触碰总线,否则是多机模型。

    4. 期间触发中断,会保持时钟信号不变,保护现场

    5. 发送一个字节:低电平主机发数据,高电平从机读数据

      1. SCL低电平期间,主机将数据位依次放到SDA线上(高位先行)

        SCL低电平,SDA准备数据。

        主机在接收之前,需要先释放SDA

      2. 然后释放SCL,从机将在SCL高电平期间读取数据位

        SCL转换为高电平,开始发送SDA数据

        一般上升沿触发

      3. 所以SCL高电平期间SDA不允许有数据变化

        发送数据期间,不允许数据变化

      4. 依次循环上述过程8次,即可发送一个字节

        SCL转换为低电平,发送完成。

    6. 接收一个字节:低电平从机发数据,高电平主机读数据

      1. SCL低电平期间,从机将数据位依次放到SDA线上(高位先行)

      2. 然后释放SCL,主机将在SCL高电平期间读取数据位

        一般下降沿触发

      3. 所以SCL高电平期间SDA不允许有数据变化

      4. 依次循环上述过程8次,即可接收一个字节

        (主机在接收之前,需要释放SDA)

    7. 应答机制:发送一位,接收一位,这一位作为应答

      • 发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答

      • 接受应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)

  6. I2C时序:每个设备地址(7位)必须不同,一般设备地址的最后几位可以在电路中改变

    释放:变为高电平

    拉低:变为低电平

    连续读写:停止位之前,重复多次发送字节的时序;注意指针每次+1。

    主机应答:读操作的时候,如果将SDA应答设置为非应答,应答高电平,就表示即将结束。只在最后一次应答时设置。

    1. 指定地址写:对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)

      指定设备(Slave Address):从机地址确定

      指定地址(Reg Address):某个设备内部的寄存器地址

      指定数据(Data):在这个寄存器写入的数据

      时序图:

      1. 起始条件:SCL高电平期间,拉低SDA

      2. 发送字节:从机地址 + 读写位,8位

        • 设备地址7位,不能重复

        • 第8位,也就是最低位,表示读写位

          0写、1读。

      3. 应答位RA:1位数据的应答位;

        • 紧跟设备地址、读写位其后,接收从机的应答位RA;

        • 主机先要释放SDA,电平应该回弹到高电平

        • 但是协议规定,应答的时候,从机立刻拉低SDA,应答结束后放开

        • 因此,波形为低电平信号,表示从机产生了应答

        • 应答结束后,从机释放SDA产生高电平信号,交出SDA控制权

      4. 发送字节:从机设备可以自己定义第二个字节和后续字节的用途

        可以是寄存器地址、指令控制字等

        • 这里发送0x19地址,表示要操作这个地址下的寄存器

      5. 应答:从机应答,SDA表现为低电平,主机收到应答位0。

      6. 发送字节:这里表示,写入0x19地址下寄存器的数据,写入0xAA。

      7. 应答:接收应答位,主机非应答(SA),不把SDA拉低,即将结束

      8. 停止条件:如果主机不需要传输,产生停止条件,

        • 先拉低SDA,为后续SDA上升沿做准备

        • 然后释放SCL

        • 再释放SDA

    2. 当前地址读:对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)

      1. 当前地址指针:从机中的所有寄存器,被分配到了一个线性区域中,有一个指针变量,指示着其中一个寄存器,上电默认为0地址。

        • 每次读/写一个字节后,指针就会移动到下一个位置。

        • 主角没有指定读哪个寄存器的地址,从机就会返回指针指向的寄存器的值

      2. 【无法指定读的地址,因此使用不多】

      时序图:

      1. 起始条件:SCL高电平期间,拉低SDA,产生起始条件

      2. 发送一个字节:进行从机的寻址和指定读写标志位,1读。

      3. 从机应答位:位0,表示从机接收到第一个字节

      4. 读命令:

        • 主机不能继续发送,SDA控制权交给从机

        • 主机调用接收一个字节的时序,进行接收操作

        • 主机在SCL高电平期间,依次读取了8位,就接收到了从机发送的一个字节数据0x0F

        • 指向从机指针指向的寄存器地址,也就是上一个寄存器的,下一个寄存器

    3. 指定地址读:对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)

      实现原理:复合格式

      1. 把指定地址写,写入数据操作,去掉

      2. 指定地址写,把前面的指定地址的时序,追加到当前地址读时序前面

      指定地址写,只指定了地址,没来的及写,后面使用当前地址读 =>指定地址读

      时序图:

      1. 起始条件:SCL高电平期间,拉低SDA,产生起始条件

      2. 发送一个字节:进行寻址,指定从机地址和读写标志。【1101000 0写】

      3. 从机应答:

      4. 发送一个字节,用来指定地址,写入到从机的地址指针里

      5. 从机应答

      6. 停止条件:可以加上也可以省略,加上的话就凑成了一个完整时序。【图中省略】

      7. 重复起始条件(Sr):另起一个时序

      8. 发送一个字节:重新寻址,指定从机地址和读写标志。【1101000 1读】

      9. 从机应答

      10. 主机接收一个字节数据,位置已经指定

  7. MPU6050芯片

    PS:产品说明书,芯片的功能描述、电气参数、引脚定义、硬件电路等

    RM:寄存器映像,芯片内部寄存器描述、寄存器每一位的详细解读,代码实现细节等

    1. 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轴。

    2. 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)

    3. MPU6050硬件电路

      1. MPU6050芯片:

        • SCL、SDA:模块内置两个上拉电阻,接线时候直接接入GPIO口即可

        • MDP单元:进行数据融合和姿态解算

        • VDD供电:2.375~3.46V,不能直接接入5V。

      2. CON1:

        • XCL、XDA:主机I2C通信引脚,扩展芯片功能,MPU6050主机接口可以直接访问这些扩展芯片的数据

          六轴姿态传感器不够用的时候,可以扩展

          通常外接磁力计、气压计等

          如果不需要姿态解算功能,可以直接接到SCL和SDA总线上

        • AD0引脚:从机地址的最低位,可以修改0 / 1

        • INT引脚:中断输出引脚,可以配置芯片内部事件,触发中断

          数据准备好、I2C主机错误等

          实用小功能:自由落体检测、运动检测、零运动检测等

          不需要可以不配置

      3. LDO:供电逻辑,稳压器,扩大芯片供电范围

        • 输入端VCC_5V:可以在3.3V~5V之间

        • LDO:经过3.3V稳压器,输出稳定的3.3V,给芯片供电

        • LED:电源指示灯,有电就亮

        • 如果有稳定的3.3V电源,可以不需要

    4. MPU6050框图

      1. 时钟CLKIN和CLKOUT:时钟输入输出脚,一般使用内部时钟,因此不需要使用

        外部时钟需要额外电路

      2. 传感器:本质上是可变电阻,分压后输出模拟电压

        • XYZ加速度计

        • XYZ陀螺仪

        • 稳定传感器

      3. ADC:传感器的模拟电压,经过ADC模数转换。

      4. 数据寄存器:模数转换的数据,存放到16位数据寄存器中

      5. 数据自动转换,我们直接读取寄存器即可。

      6. 自测单元:启用后,芯片内部会模拟一个外力施加在传感器上,外力会导致传感器数据比平时大一些

        自测响应:使能自测和失能自测,获取两个数据,相减后得到自测响应

        自测响应在一定范围内(手册上有范围),说明芯片正常

      7. 电荷泵:也叫充电泵,是一种升压电路,CPOUT需要外接电容

        串联正接,电容充电;充满电后,串联反接,电压提升到2倍。

        自动进行、

      8. 寄存器:

        • 中断状态寄存器:可以控制内部的事件到引脚输出

        • FIFO:先进先出寄存器,可以对数据流进行缓存。

        • 配置寄存器:可以对内部各个电路进行配置

        • 传感寄存器:也就是数据寄存器,存储了各个传感器的数据

        • 工厂校准:内部传感器都进行了校准

      9. DMP数字运动处理器:芯片内部自带的姿态解算的硬件算法

        可以配合官方DMP库,可以进行姿态解算

      10. FSYNC:帧同步

      11. 通信接口部分:

        • I2C和SPI通信接口,用于和STM32进行通信

        • 主机I2C通信接口,用于和MPU6050扩展设备通信

        • 接口旁路选择器:一个开关。

          • 拨到上面:STM32可以控制所有设备,包括MPU6050和扩展设备

          • 拨到下面:STM可以控制MPU6050,MPU6050控制扩展设备

      12. 供电:所有寄存器上电默认值都是0x00;

        • 除了107号寄存器(电源管理器1)默认0x40,新品上电默认睡眠模式,操作前先解除。

        • 117号寄存器默认(ID号)0x68

  8. I2C外设【硬件I2C】:节省软件资源、效率高

    1. STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担

      硬件电路自动翻转电平,只需要写入控制寄存器CR、数据寄存器DR就可以实现协议,也需要读取状态寄存器SR,获取电路状态

    2. 功能指标

      • 支持多主机模型

      • 支持7位/10位地址模式

        • 使用两个字节:11110开头,说明使用10位地址

      • 支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)

      • 支持DMA

      • 兼容SMBus协议

    3. STM32F103C8T6 硬件I2C资源:I2C1、I2C2

      软件I2C没有资源限制

    4. I2C框图

      • SDA:数据控制部分,半双工,数据收发使用同一组寄存器

        1. DATA REGISTER:数据寄存器,我们需要发送的一个字节数据,写入DR寄存器。

        2. 数据移位寄存器:当寄存器没有移位时,就可以把DR寄存器的数据,送到数据移位寄存器

          数据从数据寄存器移到数据移位寄存器时,状态寄存器TXE位(发送寄存器空)置1

          数据从数据移位寄存器移到数据寄存器时,状态寄存器RXNE位(接收寄存器非空)置1

        3. 数据控制:数据移位寄存器,把数据移位到数据控制寄存器,一旦移位完成,就可以无缝衔接,继续发送

        4. 控制寄存器:控制收发

        5. 比较器、地址寄存器:多主机模式下,STM32作为从机模式使用;如果使用一主多从模式,STM32就不会作为从机,因此不需要使用

          1. 自身地址寄存器:STM32不进行通信时,就是从机,从机可以被召唤,因此有个地址。

          2. 双地址寄存器:STM32支持同时响应两个从机地址

          3. 比较器:STM32作为从机,在被寻址时,通过比较器判断地址,响应外部主机的召唤。

        6. 帧错误校验PEC计算:STM32的数据校验模块,发送多字节的数据帧时,硬件可以自动执行CRC校验计算,校验位附加在数据帧后面;同时接收到数据帧后,STM32硬件也可以自动进行校验判断。校验错误,硬件自动置校验错误标志位。

          CRC是常见的数据校验算法,经过前面数据进行各种运算,然后得到一个字节的校验位。

      • SCL时钟控制:写入对应的位,电路就会执行对应功能

        1. 控制逻辑电路:黑盒子

          • 中断:可以开启中断

          • DMA请求与响应:多数据时,可以配合DMA提高效率

        2. 时钟控制寄存器:操作对应的位,可以执行对应的功能

        3. 控制寄存器:写入控制寄存器,可以对整个电路进行控制

        4. 状态寄存器:可以得知电路工作状态

    5. I2C基本结构

      1. 移位寄存器:I2C高位先行,向左移位

      2. GPIO:使用复用输入和复用输出

      3. 数据控制器:黑盒

      4. 开关控制:I2C_Cmd

    6. 操作流程:4个流程,但是从机模式暂时不考虑

      • 主机发送

        1. 主发送:

          • 7位地址:起始条件后的1个字节是寻址,和1位读写位

          • 10位地址:起始条件后的2个字节是寻址;第一个字节的前5位是标志位11110,以及2位地址,和1位读写位;第二个字节是8位地址

        2. 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指定寄存器地址的数据……后续就是从指定寄存器开始,依次往后写数据

      • 主机接收

        1. 流程:

          • 起始条件、

          • 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,设置终止条件请求。

          • 终止

  9. 软件I2C与硬件I2C波形对比

    软件I2C波形可能不标准,但由于是同步时序,因此不影响通信,可以容忍不标准波形

    1. 电平变化

      • 波形相同、对应数据相同

    2. 时钟

      • 硬件I2C波形更加规整,时钟周期占空比比较一致

      • 软件I2C,由于加入了延时,占空比可能不规整,但影响不大

    3. 读写:SCL下降沿写,上升沿读

      读:时钟上升沿,可以读SDA数据。

      写:时钟下降沿,写入SDA数据,便于下次读

      • 硬件I2C,读写数据都是紧贴SCL变化上升沿/下降沿,

      • 软件I2C,读写数据会有延时,SCL变化后,数据不会紧贴SCL上升沿/下降沿变化,而是等了一小会才进行读写操作

1.软件I2C读写MPU6050
  1. 建立I2C.c和I2C.h通信层模块,初始化GPIO、6个时序基本单元(起始、终止、发送一个字节、接收一个字节、发送应答、接收应答)

    1. 把SCL和SDA初始化为开漏输出模式

    2. 把SCL和SDA置为高电平

  2. 建立MPU6050的MPU6050.c和MPU6050.h模块,基于I2C通信模块,实现指定地址读、指定地址写、写寄存器对芯片进行配置、读寄存器得到传感器数据

  3. 在main.c中,调用MPU6050模块,初始化、拿到数据、显示数据

  1. 创建文件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;
    }

  2. 新建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

  3. 新建文件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);
    }

  4. 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
  1. RCC开启时钟,I2C外设、GPIO

  2. I2C外设的GPIO口,初始化为复用开漏模式

  3. 使用结构体,对I2C配置

  4. I2C_Cmd使能I2C

  1. 使用I2C2,所以MPU6050接入GPIO对应引脚GPIOB_PIN_10和GPIOB_PIN_11

    这里和上面软件模拟方法,接入的端口虽然相同,但是意义完全不同。软件模拟可以介入任意端口,硬件只能接入对应端口

  2. 删除Hardware文件夹下的MyI2C.c和MyI2C.h,因为要使用硬件I2C

    1. 关掉MyI2C.c和MyI2C.h文件选项卡

    2. 在Hardware文件夹下,右键MyI2C.c和MyI2C.h文件,选择Remove File xxx ,移除文件

    3. 在项目文件夹的Hardware文件夹下,删除MyI2C.c和MyI2C.h文件,保持目录与工程结构一致

  3. 修改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;
    		}
    	}
    }

  4. 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通信

速度更快、简单、硬件开销大、通信线个数多、经常有资源浪费现象

  1. 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指定从机通信,不需要地址、应答机制

  2. 硬件电路

    1. SPI主机,主导整个SPI主线,一般使用控制器作为主机

      比如STM32

    2. SPI从机,是挂载在主机上的从设备

      存储器、显示屏、通信模块、传感器等

    3. SS从机选择线,一根SS线连接一个从机,从主机输入,低电平选择,同时只能选择一个从机通信

      当主机未选择的从机,SS置为高电平的同时,从机的MISO,要切换为高阻态,防止一条线有多条输出,防止冲突。

    4. SPI的6根通信线,都是单端信号,高低电平都是相对于GND的电压差,所有设备需要共地。【主机和从机】

    5. 如果从机没有供电,可以从主机额外引出电源正极VCC给从机供电

    6. 时钟线SCK:对于主机是输入线,对于从机是接收线,保证同步时钟

    7. MOSI:主机输出、从机输入;主机通过MOSI输出,从机通过MOSI输入

    8. MISO:主机输入、从机输出;从机通过MISO输出,主机通过MISO输入

    9. 输出引脚配置为推挽输出,输入引脚配置为浮空输入或上拉输入

      下降沿和上升沿都迅速,通讯速度可以达到MHz速度,不支持多主机、实现全双工、不会;冲突所以可以使用推挽输出;

      对比I2C上升沿缓慢、下降沿迅速,速度受制于上升沿信号,为了实现半双工、多主机时钟同步、总线仲裁,不允许使用推挽输出,防止冲突短路;

    10. 通信:主机需要与哪个从机通信,就把哪个从机的SS线,置为低电平

  3. 移位示意图:高位先行

    1. SPI主机内,有个8位的移位寄存器,高位先行,根据时钟向左移位

    2. SPI从机内,也有个8位的移位寄存器,高位先行,根据时钟向左移位

    3. 波特率发生器:移位寄存器的时钟源,控制主机和从机,驱动移位寄存器移位

    4. MOSI:主机移位寄存器移出去的数据,通过MOSI引脚输入到从机移位寄存器的右边

    5. MISO:从机移位寄存器移出去的数据,通过MISO引脚输入到主机移位寄存器的右边

    6. 工作流程

      1. 波特率发生器时钟的上升沿触发

      2. 所有移位寄存器向左移动移位,移出去的位,放到引脚上

        主机移位放到MOSI引脚

        从机移位放到MISO引脚

      3. 波特率发生器时钟的下降沿触发

      4. 引脚采样的位,输入到移位寄存器的最低位

      5. 8个时钟之后,实现主机和从机的一个字节数据交换

      主机只想发送,不想接收:那么交换过来的数据,不看即可,没有意义的数据

      主机只想接收,不想发送:随意发送一串数据(一般为0x00或0xFF),把从机的数据交换过来即可。

  4. SPI时序

    1. SPI时序基本单元

      1. SS从高变到低,表示选中某个从机,是通信的开始

      2. SS从低变到高,表示结束了从机的选择,是通信的结束

      3. 在从机的选中过程中,SS时钟保持为低电平

    2. 交换一个字节的模式:模式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第一个边沿移出数据,第二个边沿移入数据

    3. SPI时序:向SS指定的设备,发送指令(0x06)

      1. 发送开始,将SS置为低电平

      2. 发送0x06,交换得到0xFF

      3. 发送结束,将SS置为高电平

    4. SPI时序:指定地址写

      1. 向SS指定的设备,发送写指令(0x02),

      2. 随后在指定地址(Address[23:0])下,写入指定数据(Data)

        向地址:0x123456,写入数据0x55

    5. W25Q64

      1. W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器

        数据掉电不丢失

        常应用于:数据存储、字库存储、固件程序存储等场景

      2. 属性

        • 存储介质: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。

      3. 硬件电路

        1. 引脚功能定义

          1. VCC、GND:3V3

          2. CS:低电平有效,片选引脚,对应SS

          3. CLK:时钟线

          4. DI:MOSI,主机输出,从机输入

          5. DO:MISO,主角输入,从机输出

          6. WP:写保护,实现硬件写保护,低电平有效,不让写。

          7. HOLD:数据保持,低电平有效,中断保护现场,保持时序有效

          8. 双重SPI:DI当做IO0 ,DO当做IO1 ,数据同时收发两个数据位

          9. 四重SPI:WP当做IO2 ,HOLD 当做IO3 ,四个引脚都可以收发数据

        2. 模块原理图:

          1. U1是W25QXX芯片

            1. VCC:接入J1的6号引脚

            2. GND:接入J1的3号引脚

            3. 通信4个脚:CS、CLK、DI、DO,直接接到J1

            4. HOLD和WP:低电平有效,暂时不使用,直接接入VCC。

            5. C1:滤波电容,接入到VCC和GND

            6. R1和D1:电源指示灯,通电就亮

          2. J1是6脚排针

      4. W25Q64框图

        1. 芯片内部划分,容量是8MB,划分后容易管理,所有操作按照基本单元进行

          常见划分:

          1. 一整块存储空间,划分为若干块Block

          2. 每一块,划分为若干扇区Sector

          3. 每个扇区,分为若干页Page

          W25Q64划分:毫秒级别ms写入、擦除

          1. 芯片24位地址,3个字节表示;容量8MB,24位最大寻址范围是16MB,因此地址为00 00 00H~ 7F FF FFH,因此只用了一半

          2. 以64KB为一个基本单元,划分为若干块,从0地址开始,依次是块0、块1、块2、……、块127

            每个块地址变化规律:7FH = 127

            xx 00 00 ~ xx FF FF

          3. 在一块中,以4KB为一个单元,进行切分,每一块可以划分16个扇区:扇区0、扇区1、……、扇区15

            每个扇区地址变化规律:FH = 15

            xx x0 00 ~ xx xF FF

          4. 在每个扇区,以256B划分一页,一个扇区可以分为16页:页0、页1、……、页15

            页的地址规律:每一行就是一页

            xx xx 00 ~ xx xx FF

        2. SPI控制逻辑:左下角,芯片内部的操作(地址锁存、数据读写等),控制逻辑都可以自动完成。

        3. SPI通信引脚:与主控芯片相连接,主控芯片通过SPI协议,把指令和数据发给控制逻辑

        4. 状态寄存器:控制及状态寄存器,芯片的状态

          是否忙状态、是否写使能、是否写保护等

          状态寄存器1:其他位暂时不用

          • BUSY:当设备正在执行页编程(写入数据)、扇区擦除、块擦除、整片擦除、写状态寄存器时,BUSY置1,处于忙状态设备会忽略进一步的指示,指令结束后,BUSY清零。

          • WEL:写使能锁存位,执行完写使能指令后,WEL置1,代表芯片可以进行写入操作;芯片写失能(上电后默认失能、发送写失能指令、页编程、扇区擦除等)时,WEL置0。

          状态寄存器2:暂时不使用

        5. 写控制逻辑:Write Control Logic,配合WP引脚实现硬件写保护

        6. 高电压生成器:配合Flash进行编程,为掉电不丢失存储提供高电压。

        7. 页地址锁存/计数器:指定24位地址,3个字节,读入前两个字节。通过写保护和行解码,选择操作哪一页。

          内部存在计数器,读写后自动+1;

        8. 字节地址锁存/计数器:指定24位地址,3个字节,读入后一个字节。通过列解码和256字节页缓存,对指定字节进行读写操作。

          内部存在计数器,读写后自动+1;

          256字节页缓冲区:是一个256字节的RAM存储器,数据读写从这里实现,跟随SPI速度;因此写入的一个时序,连续写入的数据量不能超过256字节

          写完成后,数据会从缓冲区,存入到Flash里面,速度较慢,需要时间。因此写入时序后,芯片会进入一段忙状态。

    6. Flash操作的注意事项:比RAM要求更多

      • 写入操作时:

        • 写入操作前,必须先进行写使能

          防止误操作,发送一个写使能的指令

        • 每个数据位只能由1改写为0,不能由0改写为1

          Flash不能覆盖改写,可能是技术原因

        • 写入数据前必须先擦除,擦除后,所有数据位变为1

          弥补只能由1改0的缺陷。 FF表示空白区域。

        • 擦除必须按最小擦除单元进行

          可以选择整个芯片擦除、按块擦除、按扇区擦除,最小单元是一个扇区(4KB=4096B)

          为了不丢失数据,只能先把数据读出来,再写回去。

          擦除指令,也会让芯片进入忙状态。

        • 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入

          一个写入时序,最多写入256字节

          写入结尾后,会跳到页首写,造成地址错乱

        • 写入操作结束后,芯片进入忙状态,不响应新的读写操作

          忙状态(BUSY = 1忙)不会响应写入操作。 读状态寄存器,读取忙状态后,判断是否写入。

      • 读取操作时:要求少

        • 直接调用读取时序,无需使能,无需额外操作,没有页的限制

        • 读取操作结束后不会进入忙状态,但不能在忙状态时读取

  5. SPI指令集

    1. 写使能:发送指令码0x06,后续不需要跟随数据

      只对一条时序有效,完成后自动清除

    2. 读状态寄存器1:发送指令码0x05,继续发送,交换读取一个字节,就是寄存器状态。

      可以查看忙状态

    3. 页编程:有256字节限制,先发送指令码0x02,跟随写入3位地址(高->低),跟随数据,多位数据依次写入

    4. 扇区擦除:擦除4KB数据,最小的擦除。发送指令码0x20,发送擦除地址3位(一般对齐到扇区首地址)

    5. 读取ID号:发送指令码0x9F,连续读取3个字节(厂商id 1位,设备id 2位)

    6. 读取数据:发送指令码0x03,交换发送3个字节地址,读取数据,跟随数据,多位数据依次读取

  6. SPI外设:高性能、节省软件资源

    STM32F103C8T6 硬件SPI资源:SPI1、SPI2

    1. STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担

    2. 属性

      • 可配置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 ,数字音频信号传输的专用协议

    3. SPI框图

      1. 移位寄存器,数据从MOSI移出去,MISO的数据移进来

        左边进来,移动到高位,说明低位先行配置

      2. LSBFIRST:移位先行控制器,LSBFIRST= 1,低位先行。

        如果LSBFIRST = 0,高位先行,那么图中的移位寄存器的进入与输出应该改变

      3. 主从模式引脚变换:MOSI和MISO的交叉部分,SPI外设可以作为主机也可以作从机

        做主机时,不使用交叉:MOSI作为输出,MISO作输入

        做从机时,使用交叉:MOSI从机输入,MISO从机输出

        因此图中箭头,从MISO指向MOSI的箭头,画错方向了。

      4. TDR和RDR占用同一个地址,但是是两个寄存器,统一叫做DR。数据寄存器,发送和接收时分离的;移位寄存器,发送和接收共用

        连续传输:没有数据移位时,TDR的数据转入移位寄存器【发送缓冲区】,开始移位,同时状态寄存器标志位TXE=1(发送寄存器空),此时下一个数据可以提前写入到TDR等待。

        连续接收:数据移出完成,那么数据移入也完成。数据从移位寄存器,转入到接收缓冲区RDR,同时状态寄存器标志位RXNE=1(接收寄存器非空),检查RXNE后,尽快把数据从RDR读出来,在下个数据到来前读出RDR就可以实现连续接收,否则会覆盖

      5. 波特率发生器:产生SCK时钟,输入时钟PCLK(72M或36M)进行分频后输出到SCK引脚,控制移位寄存器

      6. CR1寄存器:BR0、BR1、BR2控制分频系数,实现2~256的分频

      7. 常用寄存器标志

        • SPE:SPI使能,就是SPI_Cmd的配置位

        • BR:配置波特率,就是SCK的时钟频率

        • MSTR:配置主从模式,1是主模式(常用);0是从模式

        • CPOL和CPHA:控制SPI的4种模式

        • SR状态寄存器

          • TXE:发送寄存器空

          • RXNE:接收寄存器非空

        • CR2寄存器:一些使能位

      8. NSS从机选择,低电平有效。

        偏向于实现多主机模型,因此暂时不会使用。

        需要把设备的NSS引脚连接到一起,可以把NSS配置为输出或输入模式,

        • 输出模式(SSOE = 1):可以输出电平,将NSS置为低电平,告诉其他设备,我现在要变成主机,其他设备变成从机。

        • 输入模式(SSOE = 0):可以接收其他设备信号,当有设备是主机,拉低NSS后,就无法成为主机。输入的信号进入数据选择器,分为0硬件模式和1软件模式。

        【使用GPIO口进行模拟SS引脚】:直接置高低电平即可

    4. SPI基本结构

      1. 移位寄存器左移,高位优先,通过GPIO口输出到MOSI,从MOSI输出,因此是SPI主机

      2. 移入的数据,从MISO进入,通过GPIO到移位寄存器低位,循环8次,可以实现主机和从机交换一个字节

      3. TDR和RDR的配合,可以实现连续的数据流

        • TDR数据整体转入,置TXE标志位

        • 移位寄存器数据,转入RDR,置RXNE标志位

      4. 波特率发生器:产生时钟,输出到SCK引脚

      5. 数据控制器:控制所有电路运行

      6. 开关控制SPI_Cmd,使能整个外设

      7. 从机选择引脚SS,图中没有画,可以使用普通的GPIO口模拟

        一主多从模式下,GPIO模拟SS是最佳选择

    5. SPI时序

      如果对性能要求不高,可以使用非连续传输,更加简单

      1. CPOL = 1,CPHA = 1,图中使用的模式3,SCK默认高电平,在第一个下降沿,MOSI和MISO移出数据;在上升沿移入数据。

      2. SCK时钟线:模式3规则,SCK默认高电平。在第一个下降沿,MOSI和MISO移出数据;在上升沿移入数据。

      3. MISO/MOSI(输出):是MOSI和MISO的输出波形,图中演示为低位先行。

      4. TXE标志:发送寄存器空,

      5. 发送缓冲器(写入SPI_DR):就是TDR

      6. BSY标志:BUSY标志,由硬件自动设置和清除,有数据传输时标志置1

      7. MISO/MOSI(输入):

      8. RXNE:接收数据寄存器非空标志位

      9. 接收缓冲器(读出SPI_DR):就是RDR

      10. 发送流程:

        1. 当SS置低电平,开始时序

        2. 开始时,TXE = 1,表示TDR为空,可以写入数据开始传输

        3. 软件写入0xF1到SPI_DR,表示要发送一个数据

        4. 写入后,TDR = 0xF1,同时TXE = 0,表示TDR已经有数据了。

          TDR是等候区,移位寄存器是真正的发送器

        5. 移位寄存器开始没有数据,那么TDR数据立刻转入到移位寄存器,开始发送,同时置TXE = 1,表示发送寄存器空

        6. 移位寄存器有数据,波形开始自动生成

          数据波形图中有些早,应该延后一些,在TXE标志位置1的上升沿时刻。

        7. 为了连续发送数据,在移位寄存器发送时,把下一个数据移入到TDR中;因此在TDR空时,立刻把下一个数据0xF2写入。后续同理

        8. 如果不想继续发送,那么TXE = 1保持不变,直到移位寄存器空全部发送完成,BUSY由硬件清除,才表示波形发送完成

      11. 接收流程:全双工,发送同时接收

        1. 第一个字节发送完成后,接收也完成了。

        2. 接收到数据A1,移位寄存器数据整体转入到RDR,同时RXNE标志位置1,表示接收到数据

        3. 从SPI_DR也就是RDR中读出数据A1,软件清除RXNE标志位,等待下一个数据接收

          移位寄存器自动转入RDR,会覆盖原有数据,因此要及时读取RDR数据

        4. 在最后一个字节传输,时序完全产生后,才可以接收到数据;

      1. CPOL = 1,CPHA = 1,配置为SPI模式3,SCK默认高电平

      2. 发送数据时,检测到TXE= 1,TDR为空,就可以软件写入0xF1到SPI_DR

      3. 此时,TDR = 0xF1,TXE = 0,同时移位寄存器为空,那么TDR数据立刻转入移位寄存器开始发送

      4. 等待这个字节传输完成,接收的RXNE = 1,可以把数据读出

        连续传输:此刻TXE = 1,可以把下一个数据放入到TDR中等待

      5. 读出字节后,可以写入下一个字节数据。较晚写入TDR,然后可以继续发送

1.软件SPI读写W25Q64
  1. 新建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;
    }

  2. 新建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

  3. 新建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
  1. RCC开启时钟,SPI和GPIO外设

  2. 初始化GPIO,其中SCK和MOSI是由硬件外设控制的输出信号,所以使用复用推挽输出

  3. MISO是硬件外设的输入信号,配置为上拉输入,输入可以有多个,所以不存在复用输入,GPIO口可以输入、外设也可以输入

  4. SS是软件控制的输出信号,配置为推挽输出

  5. 配置SPI外设,使用一个结构体即可配置,使用SPI_Init初始化

  6. 开关控制SPI_Cmd,使能

  1. 修改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通信
  1. 引脚:差分数据脚,两个引脚表示一个差分数据

    • CAN_H

    • CAN_L

5.USB通信
  1. 引脚:差分数据脚,两个引脚表示一个差分数据

    • DP

    • DM

8.RTC实时时钟

  1. Unix时间戳:定义为从UTC/GMT(伦敦本初子午线)的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒

    时间标准:

    • GMT:格林尼治标准时间,地球自转一周的时间,不准

    • UTC:协调世界时,原子钟,稳定

    中国东八区:时间 + 8小时

    千年虫:2038年,int32_t的时间计数溢出。

  2. C语言时间模块

    头文件time.h,可以使用时间戳,直接转换各种类型的时间格式

    time_t 是 int64_t类型,64位有符号整型

    struct tm 结构体类型:tm_year最小值是70;tm_isdst 夏令时。

  3. BKP备份寄存器:本质是RAM存储器,掉电丢失数据。

    1. BKP备份寄存器:可用于存储用户应用程序数据。当VDD(2.0 ~ 3.6V)电源被切断,他们仍然由VBAT(1.8 ~3.6V)维持供电。

      VBAT备用电池:

      • 正极接入备用电池

      • 负极和主电源负极共地即可。

    2. 功能特点:

      • 当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位;

        当备用电池和VDD都断电,BKP数据会清空。

      • TAMPER引脚产生的侵入事件将所有备份寄存器内容清除

        TAMPER引脚是一个安全保障设计,可以做防拆功能,清空BKP中的敏感数据。

        TAMPER引脚加一个默认的上拉/下拉电阻,使用一根线连接到设备外壳的防拆开关或触点,当设备拆开时,触发开关,在TAMPER引脚产生上升沿或下降沿,STM32就检测到侵入事件,BKP数据清空,然后申请中断,可以在中断中,继续保护设备(清除其他存储器数据、设备锁死等)

        主电源断电后,备用电池供电,侵入设备仍然有效,即使设备关机也可以防拆。

      • RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲

        外部用设备测量RTC校准时钟,可以对内部RTC微小误差进行校准

        闹钟脉冲和秒脉冲,可以输出为其他设备提供信号

        PC13、TAMPER、RTC三个引脚共用一个端口,同一时间只能使用一个。

      • 存储RTC时钟校准寄存器

      • 用户数据存储容量:

        • 20字节(中容量和小容量)【√】

        • 84字节(大容量和互联型)

    3. BKP基本结构

      1. BKP属于后备区域,但是后备区域不只有BKP

        后备区域:当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电;当VDD主电源上电时,后备区域供电由VBAT切换到VDD

      2. 数据寄存器:存储数据,每个数据寄存器都是16位(2字节)

        20字节(中容量和小容量)一般有:DR1 ~ DR10

        84字节(大容量和互联型)一般有:DR1 ~ DR42

      3. 侵入检测:从TAMPER引脚(PC13)引入检测信号,当产生上升沿或下降沿时,清除BKP所有内容,保证安全

      4. 时钟输出:可以把RTC相关时钟,从RTC位置(PC13)输出出去,供外部使用。

        输出校准时钟时,配合校准寄存器,可以对RTC的误差进行校验

      5. RTC时钟校准寄存器:输出RTC时钟时,对于RTC的误差进行校准

  4. RTC外设:实时时钟。RTC是一个独立的定时器,可为系统提供时钟和日历的功能

    C51可以挂在DS1302外置RTC芯片

    STM32内置RTC外设,需要必要元件:RTC晶振、备用电池

    1. RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.0 ~ 3.6V)断电后可借助VBAT(1.8 ~ 3.6V)供电继续走时

      当VDD主电源掉电时,可以由VBAT的备用电池供电;

      当VDD主电源上电时,后备区域供电由VBAT切换到VDD;

    2. 功能特点:

      1. 32位的可编程计数器,可对应Unix时间戳的秒计数器

      2. 20位的可编程预分频器,可适配不同频率的输入时钟

      3. 可选择三种RTC时钟源:选择一个,接入到RTCCLK

        H是高速;L是低速

        I是内部;E是外部

        1. HSE时钟除以128(通常为8MHz/128)

          HSE 高速外部时钟信号

          主要作为系统主时钟

        2. LSE振荡器时钟(通常为32.768KHz)

          低速外部时钟信号

          RTC专用时钟【常用】

          可以通过VBAT备用电池供电,其余两个时钟不可以

        3. LSI振荡器时钟(40KHz)

          低速内部时钟信号

          主要作为看门狗时钟。

    3. RTC框图

      1. 灰色区域:属于备用区域,主电源掉电后,使用备用电池工作

      2. 计数计时

        1. RTC_CNT:32位可编程计数器,Unix时间戳秒寄存器,1s自增一次,需要1Hz信号。

          方便的借助time.h函数计算时间

        2. RTC_ALR:闹钟寄存器,32位寄存器,设置闹钟

          当CNT的值和ALR设定的值相同时,产生RTC_Alarm信号,通往中断系统或退出待机模式

        3. RTC_Alarm:可以通往中断系统,也可以让STM32退出待机模式

          待机模式省电

      3. RTCCLK:提供RTC时钟,经过分频器得到1Hz信号

        一般选择LSE振荡器时钟(通常为32.768KHz)

      4. RTC预分频器:20位的分频器,实现1 ~ 220 (1M)范围内的分频

        与时基单元类似

        1. RTC_PRL:重装载寄存器,配置n分频(n = x + 1)

        2. RTC_DIV:余数寄存器,实际上是自减计数器,自动重装

        3. TR_CLK:计数器溢出信号

      5. 中断部分:3个信号可以触发中断

        1. RTC_Second:秒中断,开启后每秒进入一次RTC中断

        2. RTC_Overflow:溢出中断,CNT的32位计数器溢出,触发一次中断,一般不会触发

          CNT是32位无符号数,2106年才会溢出

        3. RTC_Alarm:闹钟中断,当闹钟和计时器值相等时触发。闹钟信号也可以把设备从待机模式唤醒。

      6. 读写寄存器:通过APB1接口

      7. WKUP引脚:可以唤醒设备,和闹钟信号相同

    4. RTC基本结构

    5. 硬件电路

      1. 最小系统基础上,外部电路需要额外加上两部分

        • 备用电池

        • 外部低速晶振

    6. RTC操作注意事项:RTCCLK和PCLK1的时钟频率不同,需要等待、同步

      1. 执行以下操作将使能对BKP和RTC的访问:

        • 设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟

        • 设置PWR_CR的DBP,使能对BKP和RTC的访问

      2. 若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1

        直接调用等待同步的函数即可

      3. 必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器

        将CNF位置1,才可以设置时间;库函数自动加入了这个操作

      4. 对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器

        调用等待函数即可

1.读写备份寄存器
  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.实时时钟
  1. 开启PWR和BKP时钟,使能BKP和RTC访问

  2. 启动RTC时钟,计划使用LSE作为系统时钟,默认关闭,需要手动开启

  3. 配置RTCCLK前面的数据选择器,指定LSE为RTCCLK

  4. 期间调用寄存器同步等待,以及上一步操作完成等待。

  5. 配置预分频器,设置PRL一个合适的分频值,确保时钟频率1Hz

  6. 配置计数器,设置CNT的值,是一个初始的时间。

  7. 需要闹钟可以配置闹钟值;需要中断可以配置中断部分

  8. RTC比较简单,没有结构体来配置,也没有RTC_Cmd使能函数;开启时钟就能自动运行。

  1. 新建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;
    
    }

  2. 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电源控制

  1. PWR(Power Control)电源控制:PWR负责管理STM32内部的电源供电部分,可以实现可编程电压监测器和低功耗模式的功能

  2. PVD:可编程电压监测器(PVD)可以监控VDD电源电压

    【了解即可】

    当VDD下降到PVD阀值以下或上升到PVD阀值之上时,PVD会触发中断,用于执行紧急关闭任务。

  3. 低功耗模式:可在系统空闲时,降低STM32的功耗 ,延长设备使用时间

    三种模式,从上到下,关闭电路越来越多,越来越省电,越来越难唤醒

    降低主频也可以省点。

    1. 睡眠模式(Sleep)

      • 执行完WFI/WFE指令后,STM32进入睡眠模式,程序暂停运行,唤醒后程序从暂停的地方继续运行

      • SLEEPONEXIT位决定STM32执行完WFI或WFE后,是立刻进入睡眠,还是等STM32从最低优先级的中断处理程序中退出时进入睡眠

      • 在睡眠模式下,所有的I/O引脚都保持它们在运行模式时的状态

      • WFI指令进入睡眠模式,可被任意一个NVIC响应的中断唤醒

      • WFE指令进入睡眠模式,可被唤醒事件唤醒

      1. 直接调用WFI或WFE,即可进入睡眠模式

        两个内核指令,可以使用对应函数调用

        • WFI:Wait For Interrupt 等待中断,任意中断可以唤醒,醒来后进入中断处理

        • WFE:Wait For Event 等待事件,唤醒事件可以唤醒,醒来后不需要进入中断,而是从睡的地方继续运行。

          外部中断配置为事件模式

          使能中断,但没有配置NVIC

      2. 睡眠模式影响:

        只把CPU时钟关了,对其他电路无操作

        • 对1.8V区域时钟:CPU时钟关,对其他时钟和ADC时钟无影响

        • 对VDD 区域时钟:无

        • 对电压调节器操作:开

    2. 停机模式(Standby)

      • 执行完WFI/WFE指令后,STM32进入停止模式,程序暂停运行,唤醒后程序从暂停的地方继续运行

      • 1.8V供电区域的所有时钟都被停止,PLL、HSI和HSE被禁止,SRAM和寄存器内容被保留下来 在停止模式下,所有的I/O引脚都保持它们在运行模式时的状态

      • 当一个中断或唤醒事件导致退出停止模式时,HSI被选为系统时钟

      • 当电压调节器处于低功耗模式下,系统从停止模式退出时,会有一段额外的启动延时

      • WFI指令进入停止模式,可被任意一个EXTI中断唤醒

      • WFE指令进入停止模式,可被任意一个EXTI事件唤醒

      1. 进入停机模式:

        1. 设置SLEEPDEEP = 1,告诉CPU可以进入深度睡眠模式

        2. PDSS位用来区分进入停机模式还是待机模式

          PDDS = 0 停机模式

          PDDS = 1 待机模式

        3. LPDS位设置电压调节器

          LPSD = 0 电压调节器开启

          LPDS = 1 电压调节器进入低功耗

        4. 调用WFI或WFE进入停机模式

      2. 停机模式唤醒:

        • WFI:任意【外部】中断,以及PVD、RTC闹钟、USB唤醒、ETH唤醒借用了外部中断通道,因此也可以。

        • WFE:外部中断的事件模式唤醒

      3. 停机模式影响

        • 对1.8V区域时钟:关闭所有1.8V区域时钟

          CPU停止运行、外设停止运行、定时器暂停、串口停止收发

          CPU和外设寄存器数据维持原状

        • 对VDD 区域时钟:HSI和HSE的振荡器关闭

          LSI和LSE不会主动关闭,开启过这两个时钟,还会继续运行

        • 对电压调节器操作:LPDS控制,开启或低功耗模式

          可以维持1.8V区域寄存器和存储器内容

          低功耗模式更省电,但唤醒时需要更多时间

    3. 待机模式(Stop)

      • 执行完WFI/WFE指令后,STM32进入待机模式,唤醒后程序从头开始运行

      • 整个1.8V供电区域被断电,PLL、HSI和HSE也被断电,SRAM和寄存器内容丢失,只有备份的寄存器和待机电路维持供电

      • 在待机模式下,所有的I/O引脚变为高阻态(浮空输入)

      • WKUP引脚的上升沿、RTC闹钟事件的上升沿、

      • NRST引脚上外部复位、IWDG复位退出待机模式

      1. 进入待机模式:

        1. 设置SLEEPDEEP = 1,告诉CPU可以进入深度睡眠模式

        2. PDSS = 1位用来区分进入待机模式

          PDDS = 0 停机模式

          PDDS = 1 待机模式

        3. 调用WFI或WFE进入停机模式

      2. 待机模式唤醒:

        • 普通外设的中断和外部中断,都无法唤醒待机模式

        • 只有指定信号可以唤醒待机模式

          • WKUP引脚(PA0)上升沿

            PA0引脚上升沿

          • RTC闹钟事件

            闹钟定时器

          • NRST引脚的外部复位

            复位键

          • IWDG复位

            独立看门狗复位

      3. 待机模式影响

        • 对1.8V区域时钟:关闭所有1.8V区域时钟

        • 对VDD 区域时钟:HSI和HSE的振荡器关闭

        • 对电压调节器操作:关闭

          1.8V区域的电源关闭,内部寄存器和存储器的值全部丢失

          不会主动关闭LSI和LSE两个低速时钟

  4. 电源框图

    1. VDDA 供电区域:主要负责模拟部分的供电

      电路正极是VDDA ;电路负极是 VSSA

      • A/D转换器

        VREF+ 和VREF- 是AD转换器参考电压供电引脚,引脚少的型号会在内部接到VDDA 和VSSA

      • 温度传感器

      • 复位模块

      • PLL锁相环

    2. VDD 供电区域:由VDD 供电区域和 1.8V供电区

      • VDD 供电区域:

        • IO电路

        • 待机电路

        • 唤醒逻辑

        • 独立看门狗

      • 1.8V供电区:大部分关键电路,以1.8V低电压运行

        VDD 通过【电压调节器】,降压到1.8V

        低电压可以降低功耗

        • CPU核心

        • 存储器

        • 内置数字外设

    3. 后备供电区域:

      由低电压检测器控制开关

      VDD 有电时,由VDD供电

      VDD 没电时,由VBAT供电

      • LSE 32K晶体振动器

      • 后备寄存器

      • RCC BDCR寄存器

        叫备份域控制器,是RCC的寄存器。

      • RTC实时时钟

  5. 上电复位和掉电复位

    1. 当VDD 或 VDDA 电压过低时,内部电路直接产生复位

    2. 在复位和不复位的界限之间,有一段40mV迟滞电压,超过上限POR时解除复位,小于下限PDR时复位

    3. 复位信号Reset低电平有效,电压过低时复位;中间电压正常时不复位。

    4. 数据手册

      • 上电/掉电复位阈值:

        迟滞电压阈值40mV = 1.92V - 1.88V

        • 下降沿:PDR掉电复位的阈值下限;

          典型值1.88V

        • 上升沿:POR上电复位的阈值上限:

          典型值1.92V

      • 复位持续时间:

        典型值2.5ms

  6. 可编程电压检测器

    1. 测VDD 和 VDDA 的供电电压。

    2. PVD的阈值电压可以使用程序指定,自定义调节。

      配置PLS寄存器的3个位,使用迟滞比较,因此有两个阈值,范围是2.2V ~ 2.9V 左右,迟滞电压100mV

    3. 正常供电3.3V

      • 当电压降低,在2.2V ~ 2.9V之间,属于PVD监测范围,可以通过PVD设置警告线

      • 当电压继续降低,在1.9V,就是复位电路监测范围,低于1.9V直接复位。不让动

    4. 电压过低,PVD输出1;电压正常时为0;

    5. PVD信号可以去申请EXTI外部中断,在上升沿或下降沿触发中断。

1. 修改主频

后续暂时没学

10. 看门狗

  1. 看门狗WDG:当程序卡死或跑飞时,看门狗可以自动复位程序

    本质上是一个定时器,在规定时间内没有重置计数器(喂狗),看门狗硬件电路就会自动产生复位信号。

    • 独立看门狗:独立工作,对时间要求精度低。

      喂狗不能过晚

      内部低速时钟LSI(40kHz)

    • 窗口看门狗:要求看门狗在精确计时窗口起作用。

      喂狗不能过早、也不能过晚

      使用APB1的时钟

  2. 独立看门狗框图

    1. 低速时钟LSI进入预分频器,最大进行256分频

    2. IWDG_PR预分配寄存器,配置分频系数(PSC)

    3. 递减计数器,最大4095,自减到0时产生IWDG复位

    4. 设置重装载数值IWDG_RLR(ARR),重置递减计数器

    5. 防止复位,设置键寄存器,控制电路进行喂狗。重装值会复制到递减计数器中,防止自减到0复位。

    6. 状态寄存器SR:有两个更新同步位,基本不用看

    7. 上面寄存器,位于1.8V供电区。

    8. 下面工作电路,位于VDD供电区。即在停机和待机模式时仍能正常工作。

  3. 键寄存器

    1. 本质是控制寄存器,控制硬件电路的工作

    2. 由于程序跑飞、收到干扰等,为了降低干扰,因此不使用一位标志位,使用写入特定值操作。

  4. 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} 超时时间

  5. 窗口看门狗框图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值