Lazy loaded image
📘STM32-标准库学习笔记
字数 108020阅读时长 271 分钟
2024-8-1
2026-9-15
😀
看江协的视频,板子是STM32F103C8T6最小系统板。
看江协科技的视频为主,这是我在学完c语言之后首次接触嵌入式。
以下是我的笔记~

0. 资料在哪准备

1. 创建标准库的工程模板

这一部分只是创建了一个基础的标准库模板。并没有硬件等文件

1.解压库函数的压缩包并打开

库函数为:STM32F10x_StdPeriph_Lib_V3.5.0,这个可以去官网下载。
解压后文件夹内容如下
  • Libraries是库函数的文件
  • Project是官方提供的工程示例和模板
  • Utilities是官方做的一个小电路板,存放的用来测评stm32的程序
  • Release_Notes.html是发布文档
  • stm32f10x_stdperiph_lib_um.chm为库函数使用手册

2.建立工程模板,尝试使用寄存器/标准库来点灯

本章建立了工程模板并尝试用寄存器和库函数来进行点灯操作
建立工程模板是为了配置好STM32的环境,并且方便下次去建立工程。不用每次都新建一次。
  1. 建立工程文件是为了方便管理。可以起名为STM32Project
  1. 在Keil上新建工程,起名后 选择芯片型号(这里为STM32F103C8)

一、添加Start启动文件夹

  1. 新建Start文件,用来存放STM32的启动文件,STM32运行时会先运行它们
    1. 启动文件的存放位置位于库函数的压缩包中的Libraries文件中。下面的位置可以参考用
      \STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm
      我们把这里面的所有文件都粘贴到Start文件中。
      往回走,可以在这个目录下找到一些头文件
      \STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x
      • stm32f10x.h - STM32寄存器外设描述文件,是用来描述stm32有哪些寄存器,和他对应的地址
      • system_stm32f10x.c - 这两个system_stm32f10x文件主要是用来配置时钟的(STM32的72MHZ主频就是在这里配置的)
      • system_stm32f10x.h 。
      上面三个同上,也是全部放到Start文件中
      因为STM32是内核和内核外围的设备组成的,并且内核寄存器描述和外围寄存器描述是不在一起的,所以要添加内核寄存器的描述文件
      这次就不是打开DeviceSupport 文件了,而是打开内核的CoreSupport文件
      • core_cm3.c - 这两个就是内核的寄存器描述文件.h以及配置文件.c
      • core_cm3.h
  1. Keil添加启动文件Start。.s的启动文件只需要添加一个。但他有分类
    1.  
      所以我们选择_md.s后缀的文件
      添加后再把其他的.c .h后缀的文件都添加进来(要选择筛选为ALL,否则看不全)
  1. 在工程选项中添加文件的路径,否则找不到。 并且这样也有利于移动文件,因为添加后工程是以目录所在的文件位置去找文件,而不是使用绝对路径去找。可以很方便的打包工程发给别人
    1. 打开魔术棒,在C/C++选项中找到Include Paths的框框,点三个点找到Start的文件夹。然后确定。

二、添加User用户文件夹

  1. 在工程模板的文件夹下添加User文件夹,用来存放用户写的代码
  1. 在Keil软件的Start的上一个目录Target 1上右键,点击添加组。然后重命名为Start。 并在魔术棒→ C/C++中,添加好头文件路径 Include Path
  1. Keil软件上右键Start添加新文件,选择.c输入ain.c文件然后选择存放路径到User文件中,否则会默认放到文件夹外的工程目录中。
    1. (这里我已经在魔术棒上设置好Tab=4个空格和显示空格等配置了。) (并且魔术棒按钮的Target块中选择的是ARMCode编译器为v5.06) (在扳手工具处已经设置字号为14,编码格式为:GB2312)
  1. 在里边输入
    1. 此时编译(编译分为全局编译和单文件编译,全局编译是对所有文件编译,单文件则是对当下的文件进行编译。这里先选择单文件就够了)就可以看到0错误0警告。
  1. 把STINK和系统板插好,然后连接电脑。
  • 3.3V对应3.3V
  • GND对应GND
  • SWDIO对应SWDIO
  • SWCLK对应SWCLK
  1. 点击魔术棒选择Debug,设置Use为STink调试器(ST-Link Debugger),然后点击右边的设置按钮。在flash下载的选项中,把Reset and Run 勾选上,这样每次下载程序后会自动复位并执行,不需要按复位按键了
    1. (这里我已经下载好虚拟串口的驱动CH340了,不下是发现不了设备的)
  1. 编译一下,可以看到0警告0错误

三、尝试用寄存器点亮LED灯

当弄到现在,创建好User文件夹对于寄存器开发者来说,已经建立好工程模板了。
可以先尝试一下寄存器点灯
这里我点的灯位于GPIOC,
寄存器点灯只需要配置三个寄存器。
  1. 首先是配置RCC寄存器,使能GPIOC的时钟, 要使能GPIOC的时钟。而GPIO是挂载到APB2的总线上的。
    1. 打开STM32F10xxx参考手册(中文)手册后可以看到IOPC是在RCC_APB2ENR总线上(偏移地址:0x18)的第4位控制。并且下面有说明这一项为1就开启。
      所以把这一项写1就可以开启他的时钟了。
      整个寄存器的2进制数据换成16进制就是0 0 0 0 0 0 1 0
      notion image
      所以写上代码RCC->APB2ENR = 0x00000010;就可以打开GPIOC的时钟了。
  1. 然后第二个寄存器,要配置PC13的端口的模式
    1. 在手册中的通用和复用I/O中可以找到GOIO寄存器的一小节。在里面找到端口配置高寄存器(GPIOx_CRH)的一小节。 (0-7的前八位是在低寄存器里,8-15是在高寄存器里配置) 其中的CNF13 和MODE13就是用来配置端口PC13的
      这里我们需要将端口配置为 通用推挽输出模式也就是CNF13的两位要为00, MODE则是设置为输出模式,最大速度为50MHZ,也就是MODE两位要为11
      最后按照16进制,把32位二进制位转换为16进制的数字 也就是把0 0 3 0 0 0 0 0
      notion image
      所以写上代码GPIOC->CRH = 0x00300000;就配置PC13口为推挽输出模式,速度为50MHz了
  1. 下一步就是配置端口输出数据寄存器
    1. 在手册中的通用和复用I/O中可以找到GOIO寄存器的一小节。 在里面找到端口输出数据寄存器(GPIOx_ODR)的一小节。
      notion image
      这一位写一就是把PC13的输出设置为高电平了。
      把这32位2进制换算为16进制,就是0 0 0 0 2 0 0 0
      所以写上代码GPIOC->ODR = 0x00002000;就配置PC13口为高电平
      因为这个板子的灯是低电平点亮,所以配置为全0就能点亮灯了GPIOC->ODR = 0x00000000; 下边那个灯就是我点亮的。
      notion image
      可以看到寄存器点灯十分的麻烦,得去手册查寄存器。而寄存器那么多,根本不可能记得完。并且我刚才点灯是把除了PC13之外的位都设置成了0。,这样会影响其他端口的正常配置,如果要不影响的话还得用&=或者|=。就会更加麻烦。所以寄存器的操作方式,虽然代码简洁,但是操作起来很不方便 下面我就要去学习库函数的使用,来用库函数点灯!!

***、尝试用库函数来点亮LED灯

 

四、添加Library文件

本章添加了库函数到Library文件,并在Keil软件中定义了库函数头文件。
Library是用来存放库函数的,让我们可以使用。
  1. 在工程目录新建Library的文件夹。
  1. 添加库函数到新建的Library文件
    1. 库函数位于库函数压缩包的目录为:\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver\src 中了。其中STM32F10x_StdPeriph_Lib_V3.5.0 为STM公司推出的标准外设库。
      其中的文件就是库函数的源文件了。
      misc.c 为内核的库函数。其余的都是内核外的外设库函数。 这里就Ctrl+A全选,全部复制到Library文件夹中
      然后再打开固件库的inc文件夹,目录就在上一级。 \STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver\inc
      这个文件夹中放的是库函数的头文件,也把他复制到Library文件夹中。
  1. 接着回到keil软件,在target 1 处右键,添加组,重命名,然后添加文件到组
    1. 但是到这里,对于库函数来说还是不能直接使用的,需要再添加三个文件 它位于固件库目录的:\STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Template
      可以看到几个文件
      • stm32f10x_conf - 用来配置库函数头文件的包含关系,还有用来参数检查的函数定义,这是所有库函数都需要的
      • 剩下的两个it后缀的是用来存放中断函数的
      把这三个文件放到User的目录中。接着在keil软件的User组中添加这三个文件。并在魔术棒→ C/C++中,添加好头文件路径 Include Path
  1. 完成后,就需要添加库函数的头文件了。
    1. 先对#include "stm32f10x.h" stm32f10x.h 右键,选择打开stm32f10x.h 滑到最下边可以看到这样一段代码:
      这是C语言中的条件编译语句,意思是如果我定义了USE_STDPERIPH_DRIVER。下面的这个#include "stm32f10x_conf.h"才有效
      这需要我们点击魔术棒按钮,在C/C++的选项中,在Define框框中输入USE_STDPERIPH_DRIVER 这样才能包含标准库函数。
      (记得把每个组(文件)的路径添加好头文件路径 Include Path,在魔术棒的C/C++ 中)
      可以在KEil软件看到。 User中的文件,我们是可以修改的,而Start和Library文件中的东西都带有小钥匙 是改不了的。我们可以点击魔术棒旁边的小箱子来拖动挑中左边的组的排序位置。
      比如把User放到最下边,其他放上边,这样的话平时不用就可以收起来。
       
      下面再编译一下就可以看到成功了。 (单文件编译,如果新添加了文件会默认使用全局编译一次。所以比较慢)
       

五、使用库函数进行点灯操作

本小节使用库函数进行点灯。
库函数本质还是配置寄存器,不过是间接的了。
  1. 第一步还是使能GOIOC的时钟 它位于APB2的外设上。
    1. 库函数中用来开启时钟的函数是RCC_APB2PeriphClockCmd 大概翻译一下就是RCC_APB2 外设 时钟 命令(控制)
      输入完后会提示出来这个函数要输入两个参数。
      可以在编译后按F12或者右键跳到函数定义来看他的参数需要填什么,都会有介绍。
      notion image
      简介中会说,这个函数是让APB2的时钟使能或者失能的。他的参数可以是下面这些。
      我们这里用的是RCC_APB2Periph_GPIOC 这一项,然后填写到函数的第一个参数就OK
      第二个参数是ENABLE 使能。
      这样就可以使能GPIOC的时钟了。
      通过F12下这个函数,来查看这个函数的内部代码。其实他只是包装了一下。实际上还是配置寄存器的。
      notion image
  1. 第二步还是配置端口的模式。
    1. 这里用的库函数是GPIO_Init,F12查看定义,可以知道他的两个参数
      notion image
      第一个参数是选择GPIO。x可以是A-G中的数字 所以第一个参数就是GPIOC。
      第二个参数是一个GPIO_InitTypeDef 的结构体的指针(是指针!)。我们需要先定义一个结构体。结构体的名字根据官方的推荐,最好叫GPIO_InitStructure。
      代码:GPIO_InitTypeDef GPIO_InitStructure;
      然后复制结构体的名字,用 . 操作符来配置结构体内部的变量。
      这里可以现在Mode的一行按F12 ,跳转到Mode参数的定义。
      他说:Mode的值在GPIOMode_TypeDef(这是一个枚举变量)里可以找到。那么就可以在当前文件Ctrl+F搜一下。找到GPIOMode_TypeDef这个枚举变量的位置。 这里我们用到的是GPIO_Mode_Out_PP :通用推挽输出。 然后把这个参数放到Mode = 后就可以了。
      notion image
      下面就是配置GPIO_Pin的参数了,
      在使用F12跳转的时候,会显示一个框,这是因为他的定义后很多个。 这里需要的是member这一项,双击就可跳转过去了。
      notion image
      (其实还是刚才GPIO_Mode跳转过去的那个位置)
      他说Pin的值在GPIO_pins_define(这是一个枚举变量)里可以找到
      仍然是Ctrl+F搜一下。
      可以看到这里是#define的宏定义列表
      notion image
      这里我们需要用到的是GPIO_Pin_13
      仍然是复制粘贴过去
      Speed的参数也是同理
      notion image
      这里选择GPIO_Speed_50MHz
      致此,我们定义的结构体变量GPIO_InitTypeDef GPIO_InitStructure;的参数就配置完全了。 下面就可以把结构体变量的地址(是传地址!!)放到配置GPIO模式的函数GPIO_Init 的第二个参数中了。
  1. 第三步:配置GPIO口为高/低电平
    1. 这里用到的函数是 GPIO_SetBits - 设置GPIO为高电平 GPIO_ResetBits - 设置GPIO为低电平
      参数则都是GPIOx,GPIO_Pin_x 要看定义也是同上F12
  1. 编译!!
    1. 代码如下,如果编译的时候报错:
      他的意思这一行的声明位置。C 语言要求变量声明通常应在可执行语句之前。这违反了 C 语言的语法规则,想让我把GPIO_InitTypeDef GPIO_InitStructure; 放到第一行声明。我能听他的?
      解决办法就是:在魔术棒→ C/C++处勾选C99Mode(C语言的C99标准),这样就不会报错了。
      点灯如下:
      notion image

2. GPIO的工作原理

GPIO如果只是单独的进行输入输出就太浪费了。所以芯片的GPIO口还可以复用为外设功能引脚(比如串口)
GPIO的4种输入模式
  • 输入浮空 GPIO_Mode_IN_FLOATING
  • 输入上拉 GPIO_Mode_IPU
  • 输入下拉 GPIO_Mode_IPD
  • 模拟输入 GPIO_Mode_AIN
GPIO的4种输出模式
  • 开漏输出(带上拉或者下拉) GPIO_Mode_Out_OD
  • 开漏复用功能(带上拉或者下拉)GPIO_Mode_AF_OD
  • 推挽式输出(带上拉或者下拉) GPIO_Mode_Out_PP
  • 推挽式复用功能(带上拉或者下拉)GPIO_Mode_AF_PP
4种最大输出速度(这里与F1不同)
  • 2M
  • 25M
  • 50M
  • 100M
F4的芯片手册中,GPIo表格只要有FT标识,就代表他可以容忍5V的输入
F4与F1的不同就是这里的上下拉电阻被移动到了保护二极管那边,而不是在里边。
notion image
  1. 浮空输入模式:
    1. 既不上拉又不下拉,输入信号直接经过施密特触发器,然后存入寄存器,让芯片读取
  1. 输入上/下拉模式
    1. 区别就是输入电平会经过上拉或者下拉电阻拉高或拉低
  1. 模拟输入
    1. 此时不经过施密特触发器来转换为高低电平。是跳过触发器直接到AD(A是模拟,D是数字)这里
  1. 开漏输出
    1. 输出强低电平。
      输出时,首先是操作位设置寄存器(间接去操作输出数据寄存器)或者输出数据寄存器。然后通过输出控制电路来控制Nmos管是否接地,当输出控制电路输出1时,Nmos为断开,此时的GPIO为高阻态,如果想输出高电平。,就需要配置电阻为上拉。简单来说,开漏输出只可以输出强低电平,高电平得靠外部电阻拉高。显然,这种输出方式就有一个优点,由于高电平完全由外部电阻控制,那此模式下的输出电平是可以通过改变电阻而改变的
  1. 开漏复用功能
    1. 通过复用功能外设来控制输入输出,不是那俩寄存器。其他的都是一样的
  1. 推挽输出
    1. 输出强高低电平。
      输出时,首先是操作位设置寄存器(间接去操作输出数据寄存器)或者输出数据寄存器。然后通过输出控制电路来控制两个NMOS管的通断,来控制输出的高低电平。他也可以去配置上下拉电阻,
  1. 推挽式复用功能
    1. 通过复用功能外设来控制输入输出,不是那俩寄存器。其他的都是一样的
      每组(16个IO口)GPIO端口的寄存器包括:
      • 4个32位配置寄存器
        • 1个端口模式寄存器(GPIOx MODER)
        • 1个端口输出类型寄存器(GPIOx OTYPER)
        • 1个端口输出速度寄存器(GPIOx OSPEEDR)
        • 1个端口上拉下拉寄存器(GPIOx PUPDR)
      • 2个32位数据寄存器
        • 1个端口输入数据寄存器(GPIOx IDR)
        • 1个端口输出数据寄存器(GPIOx ODR)
      • 1个端口置位/复位寄存器(GPIOx BSRR)
      • 1个端口配置锁存寄存器(GPIOx LCKR)
      • 两个复位功能寄存器(低位GPIOx AFRL & GPIOx AFRH) (这个比较重要)
       
      所有的IO口都可以用作中断的输入
       
 

3. 标准库的LED跑马灯

这里的工程文件,我是用的是江协的工程文件,在其基础上添加了Hardware文件夹和Delay文件夹
并在Keil中添加和保存文件相对路径。
代码如下:
main.c:
LED.c
LED.h
Delay.c就不写上去了。
结果如图
notion image
 

4. 标准库的按键点灯

按键检测使用的是gpio的输入功能。函数为
KEY1 GPIO_ReadInputDataBit(GPIO_B,GPIO_PIN_11)//读取 GPIOB 11引脚的电平

按键支持连续按的一般思路

支持连续按,每次检测只要是按下的状态都会返回有效值 思路如下:

按键不支持连续按的一般思路

不支持连续按,按下按键后只要不松开就不会返回有效值 思路如下: 使用了static静态修饰变量
  1. 存储持续性:static 修饰的局部变量具有静态存储持续性,它在程序的整个运行期间都存在,而不是在函数调用结束时被销毁。
      • 例如,每次函数调用结束后,static 局部变量的值会被保留,下次函数调用时会继续使用上次修改后的值。
  1. 初始化:只在第一次函数调用时进行初始化。
  • 假设一个函数被多次调用,但static 局部变量只会在第一次调用时被初始化为指定的值,后续调用不会再次初始化。
  1. 作用域:作用域仍然局限在声明它的函数内部,在函数外部无法直接访问。
  1. 存储持续性:static 修饰的局部变量具有静态存储持续性,它在程序的整个运行期间都存在,而不是在函数调用结束时被销毁。
      • 例如,每次函数调用结束后,static 局部变量的值会被保留,下次函数调用时会继续使用上次修改后的值。
  1. 初始化:只在第一次函数调用时进行初始化。
      • 假设一个函数被多次调用,但static 局部变量只会在第一次调用时被初始化为指定的值,后续调用不会再次初始化。
  1. 作用域:作用域仍然局限在声明它的函数内部,在函数外部无法直接访问。

两种模式合二为一的思路

根据传入的mode值来强制使Key up为1

实现按键点亮对应LED灯

这里已经在头文件内定义了宏,
#define KEY1 GPIO_ReadInputDataBit(GPIO_B,GPIO_PIN_11)//读取 GPIOB 11引脚的宏
点灯图如下:
notion image
key.文件如下
Key.h文件如下
LED.c文件如下
LED.h文件如下

5. MDK中寄存器地址名称映射的再理解

举个栗子就比较明白了。
比如(这里的外设是不准确的,具体要看手册里的。)
外设基地址为 0x01000000 APB1的外设基地址为0x00100000
  • GPIOA外设是挂载在AHB1上的。 GPIOA地址为 0x00110000
APB2的外设基地址为0x00200000
  • I2C外设是挂载在AHB2上的。 I2c的地址为 0x00210000
看图大概就是这样子
notion image
每一级往下都会有一个偏移量。是相对于他的母目录的地址的偏移量

6. 时钟树系统的了解

STM32(ARM)的时钟为什么要设置的那么复杂

为什么ARM的芯片不能像51单片机就一样,全部都弄成一个时钟呢?
  1. 有利于省电 51单片机是很早之前比较广泛应用,当时对芯片的功耗高低的要求不高 而STM32作为新型的芯片,他的内核是Cortex - M3的内核。他对于功耗的要求就比较高。这里的高不是功耗要变高,而是对功耗控制的精度变高。不同的外设用不同的频率。因为频率越高。功耗也就越高
  1. 不同外设的频率不相同 使用不同外设,如果用相同的频率。那么外设的抗干扰能力就会变弱。功耗也会变大。

分析时钟树的方法(这里是对F4系列芯片)

时钟分析方法

要分析之前,首先要知道梯形符号是什么意思. 梯形符号叫做“选择器” 他可以选择多个时钟的其中一条来输出
  1. LSI 低速内部时钟 L - low低速 S - speed 速度 I - interior内部
    1. 频率为32KHZ,是内部的LC振荡器,不太稳定。一般是给独立看门狗做时钟的。(看门狗也需要使能看门狗的使能位。)
      notion image
  1. LSE 低速外部时钟
    1. E - external 外部 这里是我们外接的(低速)晶振时钟。它的稳定性就很高了。
  1. HSI 高速内部时钟 16Mhz 。 内部的,不稳定
  1. PLLCLK 锁相环时钟输出 有俩,第一个是主用的,第二个是专用的。 其中的xN是倍频器。 /P是分频器。 /Q是USB模块用的。/R主用的没用到。
    1. 为什么要有一个专用的呢?因为对于I2S的时钟对频率要求非常高,所以是专用的。
      notion image
      可以看到,主PLL锁相环输出的系统时钟。可以经过选择器。 AHB与分频器给很多的外设 AHB的信号,又能经过APBX的与分频器,又能产生很多时钟。 APBX就是给相关外设用的。
      notion image
  1. HSE 高速外部时钟(可以接4-26M的外部时钟)
  1. MCO1 MCO2 这两个是可以经过分频器和选择器来输出到芯片引脚上的

总结:

  1. STM32 有5个时钟源:HSI、HSE、LSI、LSE、PLL.
    1. HSI是高速内部时钟,RC振荡器,频率为16MHZ,精度不高。可以直接作为系统时钟或者用作PLL时钟输入。
    2. HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~26MHZ。
    3. LSI是低速内部时钟,RC振荡器,频率为32KH2,提供低功耗时钟。主要供独立看门狗和自动唤醒单元使用。
    4. LSE是低速外部时钟,接频率为32.768kHz的石英晶体。RTCPLL为锁相环倍频输出。STM32F4有两个PLL
      1. 主PLL(PLL)由HSE或者HSI提供时钟信号,并具有两个不同的输出时钟
        1. 第一个输出PLLP用于生成高速的系统时钟(最高168MHz)
        2. 第二个输出PLLQ用于生成USBOTGFS的时钟(48MHZ),随机数发生器的时钟和SDIO时钟。
      2. 专用PLL(PLLI2S)用于生成精确时钟,从而在12S接口实现高品质音频性能
  1. .系统时钟SYSCLK可来源于三个时钟源:
    1. HSI振荡器时钟
    2. HSE振荡器时钟
    3. PLL时钟

常用时钟配置寄存器

  • RCC时钟控制寄存器 RCC_CR
    • 主要用来配置、使能时钟源。就绪时钟源的
  • RCC PLL配置寄存器 RCC_PLLCFGR
    • 是用来配置PLL锁相环输出中的P、Q、R等值。
  • RCC 时钟配置寄存器 RCC_CFGR
    • 用来配置分频系数以及时钟源。(梯形以及方块里的/M的配置)
  • RCC AHBX外设时钟使能寄存器 RCC_ AHBXENR
    • 使能一些外设的时钟
  • RCC APB1、APB2 外设时钟使能寄存器 RCC_APBXENR
    • 使能一些外设的时钟

7. SystemInit.c时钟系统初始化文件了解

在系统初始化之后,是先调用Systemlnit函数,然后才调用main函数的。(这点在.s的启动文件哪里,可大概看到汇编指令是先执行sys再执行main的)
这个文件会打开时钟以及复位一些东西,具体可以去手册的RCC和SystemInit.c文件中对应的去看。把16进制翻译为2进制后,找到寄存器的对应位就OK。这样就知道这些是什么作用了。
这个建议新手不要改

8. Systick定时器

Systick定时器基础知识了解

  • Systick定时器,是一个简单的定时器,对于CM3.CM4内核芯片,都有Systick定时器
  • Systick定时器常用来做延时,或者实时系统的心跳时钟这样可以节省MCU资源,不用浪费一个定时器。比如UCOS中,分时复用,需要一个最小的时间戳,一般在STM32+UCOS系统中,都采用Systick做UCOS心跳时钟:
  • Svstick定时器就是系统滴答定器一个24 位的倒计数定时器, 计到0时,将从RELOAD寄存器中自动重装载定时初值
  • 只要不把它在SvsTick控制及状态寄存器中的使能位清除,就永不停息,即使在睡眠模式下也能工作:
  • SysTick定时器被捆绑在NVIC中,用于产生SYSTICK异常(异常号:15) 意思就是它能够产生中断。 Systick中断的优先级也可以设置
 

Systick定时器的四个寄存器

  • SysTick控制和状态寄存器 - CRTR
    • notion image
      可以配置使能、是否产生中断(异常请求)、配置内外部时钟源 以及在一次循环之后。可以读取的位段16的值。(读取后复位)
      配置的函数:SysTick_CLKSourceConfig();
  • SysTick 自动重装载初值寄存器 - LOAD 放重装时放的值
    • notion image
  • SysTick 当前值寄存器 - VAL 把重装值复制过来,然后每个时钟周期-1 减到零就再次重装
    • notion image
  • SysTick 校准值寄存器
    • 不太重要。

Systick相关函数

固件库中在 misc.c文件中
SysTick_CLKSourceConfig(); 时钟源选择
SysTick_Config(uint32_t ticks); 初始化Systick,时钟为HCLK。并开启中断
SYstick中断服务函数
void SysTick_Handler(void);

尝试使用Systick写一个延时函数

  1. 把定义好Systick的重装值。
    1. 这里使用到了中断与Systick的 LOAD 寄存器
  1. volatile 告知编译器,每次读取 TimingDelay 的值时都要从内存中获取,而不能依赖于可能过时的缓存值,从而保证了在多线程或硬件中断等环境下,对该变量的操作能够反映其最新的状态。 如用volatile: volatile u8int_8 test; test = 1; test = 2; test = 3; 则所有语句都会被编译。test先后被设置成1、2、3
SysTick_Config是SysTick的时钟源选择函数。 起内部的代码是
在判断是否超过最大值后,这个函数会把你传入的值放到SYStick的LOAD寄存器(重装寄存器)中。 然后设置优先级为最高 并且将VAL寄存器(当前值寄存器)置零。 噔噔蹬蹬….
SystemCoreClock 是 STM32 标准库中定义的一个全局变量,用于表示系统的核心时钟频率。使用了条件编译的命令。这里默认为F1的72M
那么SysTick_Config(SystemCoreClock / 1000000) 的意思就是设置系统滴答定时器的重装值为为7200(72 000 000000/1000000)。
因为晶振的速度为72M,也就是一秒钟有72M个高电平。
所以晶振震荡7200次花费的时间就是1us。
所以可以这样去判断
其中的中断函数void SysTick_Handler(void),是库函数固定的,必须要用这个名字。他是SysTick的固定中断服务函数。
void Delay_us(uint32_t us) 是定义的us函数
举例子说,想Delay_us 函数中传参:50.
那么全局变量TimingDelay == 50
然后就会进入到while (TimingDelay!= 0); 命令中一直等待。此时就在等待中断处理函数把全局变量TimingDelay 减为0.然后跳出函数。此时程序就能继续运行了。
在中断处理函数中,每当重装值7200 减 到 0 之后。就会触发一次中断,(也就是执行一次 SysTick_Handler()函数。)全局变量TimingDelay就会减一。当他捡到0之后就不会再减了。
要实现1ms延时。也很简单。只需要把传入的值x1000就OK啦
这次用这个Delay函数来实现一下闪烁灯试试.完全Ok。
notion image
(如果在编译的时候报错,根据报错信息。大概率要去stm32f103x_it.h找到void SysTick_Handler(void)函数然后注释掉。不然会出现多次定义。头文件。)

9. IO引脚的复用和映射

看这个之前可以先去把NVIC中断优先级了解一下。在第10大标题

什么是端口复用?

STM32有很多的内置外设,这些外设的外部引脚都是与GPIO复用的。 也就是说,一个GPIO如果可以复用为内置外设的功能引脚,那么当这个GPIO作为内置外设使用的时候,就叫做复用。
举个栗子: 比如串口1的俩引脚是PA9和PA10。那么如果PA9和PA10端口不用做串口(USART),二用作复用功能的串口1的发送接收引脚的时候,就叫做端口复用
查端口的表在手册可以看到。
每个引脚都会有一个复用器(其实是时钟树那种的梯形选择器),选择器可以选择连接对应的复用功能引脚。
端口复用映射是通过AFRL或者AFRH来配置的
notion image
notion image
0-7八个寄存器是寄存器AFRL控制的。每四位控制一个GPIO口。 同理AFRH也是一样
首先参考复用端口示意图,然后去配置对应的端口寄存器。

芯片的复用功能映射配置

引脚有不同的作用,
  1. 系统功能: 将 I/O 连接到 AF0,然后根据所用功能进行配置(比如):
      • JTAG/SWD:在各器件复位后,会将这些引脚指定为专用引脚,可供片上调试模块立即使用(不受 GPIO 控制器控制)。
      • RTC REFIN:此引脚应配置为输入浮空模式。
      • MCO1 和 MCO2:这些引脚必须配置为复用功能模式
  1. GPIO 在 GPIOX_MODER 寄存器中将所需 I/0 配置为输出或输入。
  1. 外设复用功能
    1. 对于ADC和DAC,需要再GPIOx_MODER寄存器把IO配置为模拟通道,
      对于其他外设
      • 在 GPIOx_MODER 寄存器中将所需 I/0 配置为复用块能
      • 通过 GPIOX OTYPER、GPIOX PUPDR 和 GPIOx OSPEEDER 寄存器,分别选择类型、上拉/下拉以及输出速度
      • 在 GPIOX AFRL 或 GPIOX AFRH 寄存器中,将 I/0 连接到所需 AFx

[江协]什么是通信

通信的目的:将一个设备的数据传到另一个设备,扩展硬件系统
通信协议:定制通信的规则,通信双方按照协议规则进行数据收发

[江协]常用通信方式以及不同

名称
引脚
双工
时钟
电平
设备
USART
TX、RX
全双工
异步
单端
点对点
I2C
SCL、SDA
半双工
同步
单端
多设备
SPI
SCLK、MOSI、MISO、CS
全双工
同步
单端
多设备
CAN
CAN_N、CAN_L
半双工
异步
差分
多设备
USB
DP、DM
半双工
异步
差分
点对点

[江协]什么是串口通信USART

串口是什么

  • 串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信
  • 单片机的串口可以使单片机与单片机、单片机与电脑、单片机与名式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力
  • 串口一般使用异步通信,所以一般要双方约定一个通信速率

串口的连接注意事项及分类

  • 简单双向串口通信有两根通信线(发送端TX和接收端RX)
  • TX与RX要交叉连接
  • 当只需单向的数据传输时,可以只接一根通信线
  • 当电平标准不一致时,需要加电平转换芯片
    • 电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,1串口常用的电平标准有如下三种:
      • TTL电平:+3.3V或+5V表示1,0V表示0
      • RS232电平:-3~-15V表示1,+3~+15V表示0(一般在大型机器上使用)
      • RS485电平:两线压差+2~+6V表示1,-2~-6V表示0(差分信号) (可以传输上千米。上面俩就几十米)
 

串口的参数

  1. 波特率: 串口通信的速率 串口一般使用异步通信,所以一般要双方约定一个通信速率
  1. 起始位: 标志一个数据帧的开始,固定为低电平 高速接收设备我要发送数据了。
  1. 数据位: 数据帧的有效载荷,1为高电平,0为低电平,低位先行
  1. 校验位: 用于数据验证,根据数据位计算得来 无校验、奇校验和偶校验 奇校验就是通过校验位保证数据位和校验位为奇数个1. 偶校验就是通过校验位保证数据位和校验位为偶数个1. (更复杂的话可以去了解CRC校验)
  1. 停止位: 用于数据帧间隔,固定为高电平

USART简介及框图介绍

USART通用同步/异步收发器 Universal Synchronous/Asynchronous Receiver/Transmitter
UART是异步收发器。一般很少用。S只是多了一个时钟输出而已。
它只支持时钟输出,不支持时钟输入。(或许是为了别的通信协议设计)
我们学习的主要是异步通信。
 
USART是STM32内部集成的硬件外设。
STM32F103C8T6的USART资源有 USART1、USART2、USART3
其中USART1为APB1总线上、USART2、USART3在APB2总线上挂载
USART框图
notion image
左边的IRDA之类的是别的通信协议要用的,这里不做了解
只了解一下串口通信。
TX数据发送,是由发送移位寄存器发送的
同理,RX数据接收,是通过接受移位寄存器接受的。
最上边的发送(TDR)\接收(RDR)数据寄存器。发送或者接受的字节数据都存在这里。 这是俩寄存器,共用同一个地址,在程序上就用一个数据寄存器DR表示。
TDR是只写的,RDR是只读的。
他们下边的发送移位寄存器,和接收移位寄存器,是把一个字节的数据,一位一位的移出去。
如果此时进行了写操作,写入的字节会存放在发送寄存器TDR中,在等待发送移位寄存器的数据移位完毕后,会立刻把这个数据赋值(送)到移位寄存器中。然后置一个标志位 1 ,我们只需要检查这个发送数据寄存器TDR置的TEX标志位,TEX是否为1.只要为1 就可以在TDR写入下一个数据了。 移位寄存器会收到发送端控制的控制,向右一位一位的把数据输出到TX引脚。向右移位。也就是低位先行
接收端也是类似,也是向右移位。当接收移位寄存器满了之后,就会一下子把数据转移到接受数据寄存器RDR。转移的过程中,也会 置一个RXNE标志位,。当RXNE为1时,就可以把数据读走了 。
剔除帧头帧尾是电路帮我们自动删除了。
唤醒单元是用多设备串口通信用的。 硬件流控也是交叉连接,用来控制双方数据停止继续的,
右边的SCLK是会每发送一个字节就输出一下高电平。可以在别的硬件上做自适应的波特率检测。
notion image
再往下就是波特率发生装置,其实就是个分频器。他们的频率不同。
简化后其实就是这样子
notion image

STM32在接受端的天才设计

STM32在接收时,为了应对噪声。有一个很好的处理方法。对高电平时一次采样,但如果突然检测到下降沿(也就是起始位)
就会立马开始进行16次采样。3次一组。两组一次。这两组如果全都是0,就代表起始位确实开始了。不是噪声。就可以开始采样了。 但是如果发现某组有一个1,俩0.就代表他有些许的噪声,但是不会做处理,这时如果有俩组通过,也算他检测到起始位了。不过会有一个噪声标志位,NE。会提醒你一下,数据我收到了。但是你悠着点用。
再其次就是有某组为俩0,那么这组就废了。继续开始下一组,直到连着两组通过。就算成功!
如果通过,他就会在第7 8 9 次采样的时候为有效采样。也是遵循2:1的原则。 7 8 9次检测也是刚刚好在最中间,非常好!

[正点(但是F103板)]端口的复用功能配置过程(串口举例)

以PA9、PA10配置为串口为例
  1. 使能端口GPIOA的时钟
    1. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
  1. 使能复用外设时钟
    1. RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
  1. 把端口模式配置为复用功能(除了ADC和DAC都得这么配置) F1这里与F4的不太一样 F4的是GPIO_PinAFConfig(GPIOA,GPIO_PinSource9)。并且每个端口都需要配置复用。 但是在F103中,只需要把输入引脚RX配置成上拉输入或者浮空输入。 把输出引脚TX配置成推挽输出就可以了。
  1. 配置GPIOX_AFRL寄存器或者GPIO_AFRH寄存器,把IO连接到所需要的外设 这里也与F4不同,这里只需要直接配置USART就可以了。然后使能USART的串口
    1. 参考代码如下:

10. NVIC中断优先级管理

NVIC中断优先级分组

不同的系列芯片,他的中断个数和中断可编程优先级的是不一样的。 中断分为内核 中断和可屏蔽中断。
在手册中可以在“中断和事件”中查找到中断的列表
分组配置是在寄存器SCB - >AIRCR中配置的。
这个寄存器是位于8 9 10位的。可以有以下分类
AIRCR[10:8]
IP Bit[7:4]分配情况
分配结果
0
111
0:4
0位抢占优先级,4位相应优先级
1
110
1:3
1位抢占优先级,3位相应优先级
2
101
2:2
2位抢占优先级,2位相应优先级
3
100
3:1
3位抢占优先级,1位相应优先级
4
001
4:0
4位抢占优先级,0位相应优先级
每个中断有一个寄存器, ip bit 他的定位是4 5 6 7四个位
他决定了有几个位是用来设置抢占优先级、有几个是用来响应优先级。
  • 抢占优先级:数字越低级别越高。高级别的抢占优先级的中断可以打断正在进行中断的低级别的抢占优先级的中断
  • 相应优先级:数字越低级别越高,!在抢占优先级相同的情况才会生效。此时若两个中断同时发生,那么高响应优先级的会优先执行。但若已经有相同抢占优先级的中断在运行了。是不会去打搅的。(也就是说在相同抢占优先级,并且同时请求中断的时候。响应优先级才会生效。)

抢占优先级和响应优先级 的区别

  • 高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的。
  • 抢占优先级粕同的中断,高响应优先级不可以打断低响应优先级的中断。
  • 当两个中断同时发生的情况抢占优先级相同的中断,下,哪个响应优先级高,哪个先执行。
  • 如果两个中断的抢占优先级和响应优先级都是一样的话,则看哪个中断先发生就先执行;
一般系统只会设置一次分组。 不然会发生混乱
 

NVIC中断优先级设置

中断优先级分组函数:void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup) 在misc.c的文件中 他实际操作的就是SCB->AIRCR 寄存器
可以在misc.h的头文件看到这个
2 bits for subpriority */
#define NVIC_PriorityGroup_3 ((uint32_t)0x400) /*!< 3 bits for pre-emption priority
1 bits for subpriority */
#define NVIC_PriorityGroup_4 ((uint32_t)0x300) /*!< 4 bits for pre-emption priority
0 bits for subpriority */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级
NVIC_InitTypeDef NCIC_InitStructure;
NCIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//设置中断通道
NCIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NCIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//设置抢占优先级
NCIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//设置响应优先级
NVIC_Init(&NCIC_InitStructure);//使能中断通道
notion image
  • 挂起寄存器(EXTI_PR)
    • notion image

4.6 EXTI相关函数

notion image
这两个函数在gpio.c文件中,Callback函数是回调函数,IRQHandler函数是中断处理函数。

5 AFIO(复用功能IO)

5.1 AFIO简介

  • AFIO 是 Alternate Function Input/Output 的缩写,表示复用功能 IO,主要用于实现 I/O 端口的复用功能以及外部中断的控制。
  • STM32上有很多 I/ O口以及内置外设(如I2C、ADC、ISP、USART等)。 为了节省引出管脚的数量,这些内置外设通常与 I/O 口共用管脚,即 I/O 管脚具有复用功能。 例如,一个 GPIO 管脚除了可以作为普通的 I/O端口外,还可以被复用为某个内置外设的功能引脚。
  • 然而,为了优化64脚或100脚封装的外设数量,有时需要将一些复用功能重新映射到其他引脚上。这时,就可以使用AFIO的复用重映射功能。通过设置复用重映射和调试I/O配置寄存器(AFIO_MAPR),可以实现引脚的重新映射,使得复用功能不再映射到它们的原始分配上。
  • 此外,AFIO 还用于控制外部中断,用来配置 EXTI 中断线 0~15 对应哪个具体 IO 口。
  • 当需要使能外部中断线或进行外部中断线的映射时,通常需要开启AFIO的时钟。

5.2 AFIO与IO对应关系

notion image

5.3 AFIO寄存器

外部中断配置寄存器(AFIO_EXTICR)
notion image

5.4 EXTI配置流程

notion image
 

12. STM32单片机串口

通信接口概述

通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
通信协议:制定通信的规则,通信双方按照协议规则进行数据收发
名称
引脚
双工
时钟
电平
设备
USART
TX, RX
全双工
异步
单端
点对点
I2C
SCL, SDA
半双工
同步
单端
多设备
SPI
SCLK, MOSI, MISO,CS
全双工
同步
单端
多设备
CAN
CAN_H, CAN_L
半双工
异步
差分
多设备
USB
DP, DM
半双工
异步
差分
点对点

通信方式分类

设备之间的通信方式可以分为串行通信并行通信
  • 串行通信是按照将数据按位顺序传输,这样做的优势是占用的引脚资源少,对于引脚资源紧张的MCU来大有益处,但是由于每次只能传输一个数据,传输速度较慢。
  • 并行通信就是数据的各个位同时传输,优点是数据传输快,缺点是占用引脚资源较多。

串行通信传输方向分类

串行通信分为单工、半双工和全双工。
  • 单工 是只能向着一个方向传输数据
  • 半双工 是可以双向传输,但每次只能有一个传输方向
  • 全双工 是既可以双向传输,它又可以同时有两个传输方向。

同步通信和异步通信

  • 同步通信需要时钟线(用作时钟信号的同步)的参与,例如SPI和IIC通信接口,
  • 而异步通信就是不带时钟线的,例如UART和one-wire。
同步是:按照任务的顺序执行任务,前一个任务没有执行结束,下一个任务不会执行,要等待上一个任务执行结束。 在同步通信时,有一根时钟线。 这约定了两个设备何时开始工作, 决定了传输速率。以及工作时间(比如单片机突然进中断,就可以先暂时时让时钟失能,两边的设备都先暂停到这里,不能动)
同步可以理解为打电话。 你没说完之前 我是不能说话的。
异步是:这两件事在同时进行而不是一方等待另一方因此这就是为什么一般来说异步比同步高效的本质所在。 在异步通信时,两遍是没有约定的。如果我突然有事,但是你还在哇哇哇说个不停,这些东西就不被我接收到了。
异步可以理解为发邮件。你发了邮件我就看。你不发我可以继续忙我的事情。
单总线(one-wire)是美国 DALLAS 公司推出的外围串行扩展总线技术,与 SPI、I2C 等串行数据通信方式不同。 它采用单根信号线,既传输时钟又传输数据,而且数据传输是双向的。 它具有节省 I/O口线资源、结构简单、成本低廉、便于总线扩展和维护等诸多优点。)

串口通信

STM32提供了UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器) USART(Universal Synchronous/Asynchronous Receiver/Transmitter,通用同步异步收发器) 这两种串口通信接口。于实现与其他设备之间的数据交换。
  • UART是一种异步通信协议,它使用起始位、数据位、校验位和停止位来定义一个字符的传输格式。
  • USART则是一种同步/异步通信协议,它支持全双工通信,并具备更高的数据传输速率和更好的抗干扰能力。他也可以作为UART使用。
notion image

数据帧的组成

在串口通信中,每一个字节的数据都装载在一个数据帧中。
每个数据帧由以下几个部分组成: 起始位:标志数据帧的开始。 数据位:实际传输的数据,通常为8位。 校验位(可选):用于错误检测。 停止位:标志数据帧的结束。
  1. 起始位:固定位低电平
  1. 数据位长度:可配置为8位或9位数据长度。
  1. 停止位长度:可选0.5、1、1.5或2个停止位。
  1. 校验位:可选无校验、奇校验或偶校验。
  1. 停止位:恢复高电平持续几个周期、
notion image
起始位 作用:标志一个数据帧的开始。 电平:固定为低电平(0)。 机制:串口的空闲状态是高电平(1),没有数据传输时,信号线处于高电平。发送数据时,起始位从高电平跳变到低电平,产生一个下降沿,通知接收设备数据传输的开始。
数据位 组成:数据帧的有效载荷。 长度:通常为8位,代表一个字节的8位二进制数据。 传输顺序:低位先行(在STM32中发送移位寄存器就是这样工作的)。 电平表示:1为高电平,0为低电平。
校验位 作用:用于数据验证,检测数据传输中的错误。 类型:奇校验和偶校验。 奇校验:确保数据帧中的1的数量是奇数。 偶校验:确保数据帧中的1的数量是偶数。 计算:根据数据位计算得来,并添加到数据位的最后。 例如,数据为00001111,有4个1(偶数) 那么偶校验之后校验位就补0,成为000011110。让整个串串的1保持偶数,否则补1 奇校验时,校验位为1,使1的数量变为奇数(000011111)。让整个串串的1保持奇数。
停止位 作用:标志一个数据帧的结束,同时为下一个数据帧的起始位做准备。 电平:固定为高电平(1)。 机制:在一个字节数据发送完成后,必须有一个停止位,用于数据帧间隔。如果没有停止位,当数据的最后一位是0时,下一个起始位的低电平无法形成下降沿,接收设备无法识别数据帧的开始。
波特率 定义:规定串口通信的速率,即每秒传输的码元数单位是波特(Baud)。 比特率:每秒传输的比特数,单位是比特每秒(bps)。 在二进制调制情况下,1个码元等于1个比特所以波特率等于比特率。 设定:发送和接收设备必须约定相同的波特率,以确保数据正确接收。 例如,波特率为1000bps,表示1秒传输1000位,每位的时间间隔为1毫秒(ms)。发送设备每隔1ms发送一位,接收设备也每隔1ms接收一位。

采样策略等

起始位侦测 检测起始位:输入电路检测到一个数据帧的起始位后,会以波特率的频率连续采样一帧数据。采样位置必须从起始位开始对齐到位的正中间只要第一位对齐,后面的位也会对齐。 16倍采样率:为了实现精确采样,输入电路以波特率的16倍频率进行采样,即在一位的时间内可以进行16次采样。
采样策略 初始采样:在空闲状态下,采样结果一直为高电平(1)。 一旦在某个位置采集到低电平(0),说明出现了下降沿,可能是起始位。 连续采样:在起始位期间进行16次连续采样。如果没有噪声,这16次采样结果应全为0。 如果有噪声 噪声处理:实际电路中可能存在噪声。因此,在检测到下降沿后,额外进行几次采样: 第3、5、7次采样。 第8、9、10次采样。 噪声判断:在两批采样中,每批3次采样中至少有2个0,才能确认是起始位。如果采样结果满足条件但有噪声,状态寄存器会设置NE(Noise Error)标志位,提醒数据有噪声。如果3次采样中只有一个0,则认为不是起始位,电路忽略前面的数据,重新捕捉下降沿。
中间采样 如果通过了起始位检测,接收状态从空闲变为接收状态,并且第8、9、10次采样的位置正好是起始位的正中间之后的数据位也在第8、9、10次进行采样,确保采样位置在每个位的正中间。

STM32串口通信

在进行STM32串口通信之前,需要对串口通信参数进行配置。这些参数包括波特率、数据位、停止位、校验位等。 其中, 波特率表示每秒钟传输的bit,在STM32F103系列中,最高可达4.5Mbps; 数据位表示每个字符的数据长度(8位或者9位)停止位用于表示字符的结束(1bit或者2bit)校验位用于检查数据传输的正确性(无校验、奇校验或者偶校验)
在STM32中,这些参数可以通过配置相应的寄存器来实现。
 

13. 串口的学习

[江协]F1芯片编写串口收发数据

这里使用的是STM32F103C8T6的最小系统板
他的USART的串口1是PA9和PA10。
PA9为TXD、PA10为RXD。

准备工作

1.接线
使用到的是USB转串口模块。用到的引脚有RXD、TXD、GND
RXD是发送数据到STM32的TXD(PA9)
TXD是接受数据从STM32的RXD(PA10)
GND是为了有电平的参考。要共地。
2.新建工程
这里不详细说了。我添加了一个新的文件给USART用。
这里的编码格式(在右上角的扳手哪里),选择的个GB2312.这个格式在串口收发的时候可以直接看到汉字。而UTF-8需要配置一些东西(在魔术棒里写一行字)
并且开启了魔术邦德Use MCicroLIB。 这是Keil为嵌入式优化的一个精简库。是用来移植Printf用的。
notion image

编写程序

由端口的复用功能配置大概可以了解。
其实对于F103端口,他的开启串口1的方式和GPIO的差不太多。
用到的函数在rcc.h和usart.h里。都可以找到。、
 
这里是先说明了如何接收,然后再说了如何发送。 发送部分分为两种:查询法,中断法 查询则是通过读之前说过的输入寄存器的RXNE寄存器的值来说是够了。 但是对于中断法,需要去配置NVIC代码前期不全。到最后我会补全针对中断的代码。
 
 
主模块serial.c
  1. 使能串口所在的GPIO时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
  1. 使能想开启的外设时钟:USART1。(他挂载在APB2总线)
    1. RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
  1. 分别配置PA9(TXD,发送)、PA10(RXD,接收)为推挽输出和浮空输入(或上拉输入)。
    1. GPIO_InitTypeDef GPIO_InitStructure;
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽模式
      GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //9
      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_10; //9
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_Init(GPIOA,&GPIO_InitStructure);//初始化
      USART_InitTypeDef USART_InitStructure;
      USART_InitStructure.USART_BaudRate = 9600;//波特率。会自动算好填入BRR寄存器
      USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//硬件流控制、不使用.
      USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//用| 使用两个功能
      USART_InitStructure.USART_Parity = USART_Parity_No; //校验位。
      USART_InitStructure.USART_StopBits = USART_StopBits_1;//停止位1位。
      USART_InitStructure.USART_WordLength = USART_WordLength_8b; //不需要校验,所以字长选择8位字长。
      USART_Init(USART1,&USART_InitStructure);
      USART_Cmd(USART1,ENABLE);
void Serial_SendByte(uint16_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);//如果写标志位没有置1(没有发送完),那么就等
}
void Serial_SendArray(uint8_t* Array,uint16_t len)
{
uint16_t i = 0;
for(i = 0; i < len; i++)
{
Serial_SendByte(Array[i]);
}
}
//发送字符串
void Serial_SendString(char* String)//有结束的0 所以不用再传长度了,
{
uint16_t i = 0;
for(i = 0; (String[i] != '0'); i++)
{
Serial_SendByte(String[i]);
}
}
//求次方函数
uint32_t Serial_Pow(uint32_t X,uint8_t Y)
{
int Num = 1;
while(Y--)
{
Num *= X;
}
return Num;
}
//发送字符形式的数字
void Serial_SendNumber(uint32_t Number,uint8_t len)
{
//从高位像低位取数字然后输出
uint8_t i = 0;
for(i = 0; i < len; i++)
{
Serial_SendByte(Number / Serial_Pow(10,len - i - 1) %10 + '0');
}
}
//下边没学过,直接搬过来的。
/
  • 函 数:使用printf需要重定向的底层函数
  • 参 数:保持原始格式即可,无需变动
  • 返 回 值:保持原始格式即可,无需变动
*/
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数
return ch;
}
/
  • 函 数:自己封装的prinf函数
  • 参 数:format 格式化字符串
  • 参 数:... 可变的参数列表
  • 返 回 值:无
*/
void Serial_Printf(char *format, ...)
{
char String[100]; //定义字符数组
va_list arg; //定义可变参数列表数据类型的变量arg
va_start(arg, format); //从format开始,接收参数列表到arg变量
vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中
va_end(arg); //结束变量arg
Serial_SendString(String); //串口发送字符数组(字符串)
}
#ifndef __SERIAL_H
#include "stm32f10x.h"
#include "stdio.h"
#include "stdarg.h"
#define __SERIAL_H
//初始化串口
void Serial_Init(void);
//发送字节
void Serial_SendByte(uint16_t Byte);
//发送数组
void Serial_SendArray(uint8_t* Array,uint16_t len);
//发送字符串
void Serial_SendString(char* String);
//发送字符形式的数字
void Serial_SendNumber(uint32_t Number,uint8_t len);
//移植printf
void Serial_Printf(char *format, ...);
#endif
uint8_t RxData;
while(1)
{
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
{
RxData = USART_ReceiveData(USART1);
Serial_SendByte(RxData);
}
}
}
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NCIC_InitStructure;
NCIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NCIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NCIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NCIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//优先级
NVIC_Init(&NCIC_InitStructure);
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;
uint8_t Serial_GetRxfalg(void)
{
if(Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
uint8_t Serial_GetRxData(void)
{
return Serial_RxData;
}
void USART1_IRQHandler(void)//中断
{
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
{
Serial_RxData = USART_ReceiveData(USART1);//读取
Serial_RxFlag = 1;
USART_ClearITPendingBit(USART1,USART_FLAG_RXNE);//手动清除标志位
}
}
uint8_t RxData;
while(1)
{
if(Serial_GetRxfalg() == 1)
{
RxData = Serial_GetRxData();
Serial_SendByte(RxData);
}
}
如果此时进行了写操作,写入的字节会存放在发送寄存器TDR中,在等待发送移位寄存器的数据移位完毕后,会立刻把这个数据赋值(送)到移位寄存器中。然后置一个标志位 1 ,我们只需要检查这个发送数据寄存器TDR置的TEX标志位,TEX是否为1.只要为1 就可以在TDR写入下一个数据了。 移位寄存器会收到发送端控制的控制,向右一位一位的把数据输出到TX引脚。向右移位。也就是低位先行
接收端也是类似,也是向右移位。当接收移位寄存器满了之后,就会一下子把数据转移到接受数据寄存器RDR。转移的过程中,也会 置一个RXNE标志位,。当RXNE为1时,就可以把数据读走了 。
剔除帧头帧尾是电路帮我们自动删除了。
唤醒单元是用多设备串口通信用的。 硬件流控也是交叉连接,用来控制双方数据停止继续的,
右边的SCLK是会每发送一个字节就输出一下高电平。可以在别的硬件上做自适应的波特率检测。
notion image
再往下就是波特率发生装置,其实就是个分频器。他们的频率不同。
简化后其实就是这样子
notion image

STM32在接受端的天才设计

STM32在接收时,为了应对噪声。有一个很好的处理方法。对高电平时一次采样,但如果突然检测到下降沿(也就是起始位)
就会立马开始进行16次采样。3次一组。两组一次。这两组如果全都是0,就代表起始位确实开始了。不是噪声。就可以开始采样了。 但是如果发现某组有一个1,俩0.就代表他有些许的噪声,但是不会做处理,这时如果有俩组通过,也算他检测到起始位了。不过会有一个噪声标志位,NE。会提醒你一下,数据我收到了。但是你悠着点用。
再其次就是有某组为俩0,那么这组就废了。继续开始下一组,直到连着两组通过。就算成功!
如果通过,他就会在第7 8 9 次采样的时候为有效采样。也是遵循2:1的原则。 7 8 9次检测也是刚刚好在最中间,非常好!

[正点]F4芯片串口收发步骤

我没F4芯片,所以就记一下步骤 F4多了一个引脚复用映射的函数。把引脚映射为串口 并且在GPIO初始化时,使用的模式不是F1的推挽输出和输入了。而是启用了复用功能 其他的都一样哒.
  1. 串口时钟使能: RCC APBxPerphClockCmd(); GPIO时钟使能: RCCAHB1PerphClockcmd();
  1. 引脚复用映射: GPIO PinAFConfig();
  1. GPIO端口模式设置: GPI0 Init(); 模式设置为GPIO Mode AF4
  1. 串口参数初始化:USART Init();
  1. 开启中断并且初始化NVIC (如果需要开启中断才需要这个步骤) NvIc Init(), USART ITConfig0;
  1. 使能串曰:USART Cmd();
  1. 编写中断处理函数:USARTx IRQHandler();
  1. 串口数据收发:void USART SendData();//发送数据到串口,
    1. DRuint16 tUSART ReceiveData();/接受数据从DR读取接受到的数据
  1. 串口传输状态获取: FlagStatus USART GetFlagStatus(), void USART CleanTPendingBit();
 

[江协]F1芯片串口收发数据包理解与思路

数据包的使用理解

数据包的作用:把一个个字节打包起来,方便我们进行多字节的数据通信
比如传输位置的XYZ,如果不封装起来。接收方就不知道那个是X,Y ,Z
  • 对于Hex数据包来说,如果载荷会出现和包头包尾重复的情况。应选择固定包长。这样可以避免接受错误,不然很容易乱套。 如果确定不会出现包头和尾与载荷的重复,那么可以使用可变的包长。因为包头包尾是唯一的
  • Hex的优点是,传输最直接,解析数据简单。适合用类似于串口通信的陀螺仪,温湿度传感器等等。 缺点则是灵活性不好。载荷容易和包头包尾重复
  • 对于文本数据包(其实每个文本字符的背后,都是一个字节的Hex数据包。不过是把他们编码和译码了) 由于译码后,产生的字符。所以我们可以用很多的字符来作为包头和包尾。所以文本是比较灵活的
  • 文本数据包的优点是,数据直观易理解。非常灵活。适合输入指令进行人机交互的场合。比如蓝牙模块常用的AT指令、CNC和 3D打印机常用的G代码。 缺点则是解析效率低,比如发送个数据包100.看着是100但实际上是文本数据1 0 0 还要把文本转换成数据才能得到100.。
    • 所以,我们要根据实际场景来选择和设计数据包的格式

数据包发送思路

对于数据包发送,我们是自主可控的。是很简单的,这里大概写个代码

数据包接受思路

数据包接收这里相较于发送比较难
这里尝试的是使用的是固定包长HEX数据包的接收方法。
以及尝试使用可变包长的文本数据包的接受方法。
HEX固定包长数据包接收
在接受数据的时候 ,数据是一个字节一个字节传输进来的。
每接受一次字符,程序就会进一次中断,在中断函数中可以拿到这个字节。
所以每拿到一个数据都是一个独立的过程。
但对于数据包来说,数据是有前后关联性的。
所以。要制作一个数据包接受,就要有 状态机 的思维
设计数据包,画一个状态转移图是必要的
notion image
对于上图,固定包长的HEX数据包。
我们定义了三个状态,等待包头、收到数据、等待包尾
用标志位S来表示当前在什么状态
这里的包头定义的是0xFF 。包尾定义的是0xFE。
在不同的状态,执行不同的程序。
文本的可变包长数据包接收
notion image
如上图,这里定义的包尾是\r和\n。如果不用俩包尾也行,这样更保险。 @是包头
 

[江协]F1芯片编写串口收发数据包

HEX数据包的发送与接收

在串口收发数据的中断函数部分上进行更改
定义包头和包尾为EE和EF
  1. 删除了读取单字节的函数,因为中断部分要用来读取数据包的每个字节
  1. 添加了两个数组: uint8_t Serial_TxPacket[4] = {0}; 定义发送数据包数组 uint8_t Serial_RxPacket[4] = {0}; 定义接收数据包数组 uint8_t Serial_RxFlag; 仍是接收到数据(包)的标志位
  1. 发送数据包部分很简单,只需要这段代码 先发送包头,再调用发送数组的函数发送数据包。再发送包尾
    1. 接收部分则用到了 状态机 机的思想。
      1. static uint8_t RxState = 0; 是状态机的状态位置。他的值决定了下次进入中断(收到字节)后对数据的处理
        static uint8_t pRxPacket = 0; 是定义下次接收的数据放到缓存数组的那个下标的位置。(其实跟指针差不多,但他不是指针。他就是个记录下标的)
        大概流程为
      2. 若接收到包头:0xFF。则进入第二状态(置标志位为1):存放4位数据 然后重置数据缓存存放位置的下标变量pRxPacket 准备放东西
      3. 开始放,直到放了4个之后,进入下一状态(置标志位为2)。
      4. 检查是否接收到0xFE,然后重新把标志位置0,然后把接收到数据包标志位置1 告诉大家,我的数据包填满了。可以来取了。
      5. 这里我在main函数的测试程序是:
        使用按键,每次按下数组位每位+1.然后显示在OLED屏幕上,
        每次发送,也会被接受,然后显示在OLED上。
        程序结果如下:
        俩是可以同时显示的
        notion image
         
        notion image

    文本数据包的接收

    文本数据包的接收是在HEX数据包的基础上改进的。
    因为发送文本数据包使用printf或者自己写的SendString就可以了 不然挺麻烦的。所以这里就写一个文本数据包的接受就可以了。
    这里的数据头为@,数据结尾是\r \n
    为什么是\r和\n呢。我个人觉得是因为在你发送一段文本的时候,他后面默认带的就是\r\n。
    要制作文本接受。要先处理一下HEX的代码
    首先把不用的函数和定义的常量都删掉。
    比如uint8_t Serial_TxPacket[4] = {0}; 定义发送数据包数组 这行话(记得把头文件里的声明也去除掉。), 还有这段函数
    然后就要开始改造HEX数据包了!
    首先,我们接收的是字符串了。不是数组,并且字符串的长度是可变的。所以可以把原本的接收数组缓存区的uint8_t Serial_RxPacket[4] = {0}; 改为char Serial_RxPacket[100]= {0}; 这样我们就有足够的空间去存放未知的字符串了。
    剩下的就是去修改中断函数里的状态机部分了
    在接受字符串包的时候。首先要判断@。然后需要先判断是否遇到了结束标志。再进行存放字符,因为我们不知道现在这个字符是不是包尾标志。在遇到\r之后,需要再判断一下。\n 如果\n有效,。那么就可以把接收到包的位置1。
    代码如下,几乎是没怎么变的。
    验证仍然是同上,就不演示了
    现在既然接收到了字符串数据,我们就可以利用C语言自带库中的String.h这个头文件中的strcmp这个字符串对比的指令(相同为0)
    就可以实现点灯操作了!
    代码参考:
    结果如图:
    notion image

    14. 外部中断

    [正点&江协]什么是外部中断,他的特点是什么

    在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行
    中断分为内核的中断和外设的中断
    中断是由NVIC根据优先级。去判断然后根据优先级告诉CPU
    stm32的GPIO口可以当做外部中断的输入
    STM32F4的中断控制器支持22个外部中断/事件请求
    那么外部中断就是EXTI了
    EXTI可以检测电平信号。(上升,下降。双边沿、软件触发)
    EXTI线0-15:对应外部IQ口的输入中断 EXTI线16:连接到PVD输出。 EXTI线17:连接到RTC闹钟事件。 EXTI线18:连接到USB OTG FS唤醒事件。 EEXTI线19:连接到以太网唤醒事件 XTI线20:连接到USB OTG HS(在FS中配置)唤醒事件:XTI
    线21:连接到RTC入侵和时间戳事件。 EEXTI线22:连接到RTC唤醒事件。
    由此我们大概可以了解到,STM32的供GPIo使用的中断线是有限的。
    需要有终端线才能产生中断请求。
    GPIO与中断线的映射关系就是这次的重点
    同一时间只能有一个pin口映射到中断线
    因为GPIOA 的1和GPIOB的1….他们的引脚是在一个映射器上的。
    对于每个中断线,可以设置相应的触发方式,比如上升沿下降沿边沿触发之类的。还包是否要开启等等。
    那么,16个中断线就要有16个中断服务函数呢? 不 在F4系列中,只有7个可使用的中断服务函数。 F4中,外部中断线5-9分配分配一个、10-15分配一个中断向量。他们都是共用一个中断函数,
    所以我们只能是用EXTI 0 1 2 3 4 9_5 15_10这七个。
    也就是说,gpio0只能映射到EXTI0 GPIO3 只能映射到EXTI3 而 GPIO6需要映射到EXTI9_5
    对于F103c8t6.同一个pin他的连接是下边这样(图中都是pin16)所以也由此可知,对于同一个pin、是不能同时发生中断请求的。
    AFIO是一个数据选择器。它可以把前边16个GPIO口的其中一个连接到EXTI外部中断中
    通道数量可以看到。一共有16个GPIO_Pin口+PVD+RTC+USB+ETH。其余四个其实是来蹭网的。PVD是外加PVD输出,RTC为闹钟、USB唤醒USB、以太网唤醒ETH。
    所以EXTI有20个输入信号。
    如图,再EXTI外部中断连接到NVIC时,把9-5 15-10 goio放到了一个通道里。 所以在用的时候需要判断一下到底是谁弄的
    其他相应,则是之前说的事件响应。
    AFIO的作用不止于此,我们之前用的 端口重映射(把普通的gpio映射为他复用的功能)。也是他干的。
    notion image

    [江协]外部中断的再理解

    notion image
    如图,是EXTI的框图,在触发终端的二时候,请求挂起寄存器就会置一。就相当于中断的标志位
    再往后走,可以看到 请求挂起寄存器和中断屏蔽寄存器,共同进入一个与门。 所以当中断屏蔽寄存器为0时,中断就失能了。如果他们都为1,则进入NVIC进行下一步
    事件的也是一样。(二十根线都是这样子的。)

    [江协]外部中断的适用场景

    外部中断的应用场合 是 “单片机事先不知道我们要去操作,但如果一操作就需要处理的突发事件,并且信号的来源是外部的,stm32不知道什么时候来,并且信号速度很快,要迅速执行”
     

    [江协]外部中断EXTI函数作用介绍

    • void EXTI_DeInit(void); 把EXEI配置清除,恢复成上电默认状态
    • void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct); 根据结构体中的参数配置EXTIi外设
    • void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct); 把参数传递的结构体变量赋一个默认值
    • void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line); 软件触发外部中断
    • 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);
      • 对中断标志位是清除(只能在中断中使用)

    [江协]NVIC函数作用介绍

    • void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
      • 中断分组用,参数为中断分组的方式
    • void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
      • 根据结构体里的配置来初始化NVIC
    • void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset);
      • 设置中断向量表,
    • void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState);
      • 系统低功耗配置
    • void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource);
      • 之前讲滴答定时器用过了。

    [江协]编写:红外传感器计次

    代码如下,已经解释的很全了
    //配置AFIO (F1中在GPIO.h文件中)(目的是为了把GPIOA的PIN2引脚映射到AFIO中。)
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource2);//这里需要根据PIn和GPIOx来选择
    //配置EXTI
    EXTI_InitTypeDef EXTI_InitStructure;
    EXTI_InitStructure.EXTI_Line = EXTI_Line2; //选择中断线路,这里是PIN2 所以为2
    EXTI_InitStructure.EXTI_LineCmd = ENABLE; //是否使能指定的中断线路
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //中断或响应模式
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//上升或下降或边沿触发
    EXTI_Init(&EXTI_InitStructure);
    //配置NVIC(因为NVIC属于内核,所以被分配到内核的杂项中去了,在misc.c)
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置抢占和响应优先级
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; //在stm32f10x.h文件里。让你找IRQn_Type里的一个中断通道。这里使用的是md的芯片(如果引脚是15-10或者9-5则需要去找对应的那个)这里我是PIN2.所以找2
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //是否使能指定的中断通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//抢占优先级(这里可以看表。看范围,)
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定响应优先级
    NVIC_Init(&NVIC_InitStructure);
    }
    //获取当前的计数
    uint16_t CountSenSor_Get(void)
    {
    return CountSensor_Count;
    }
    //在STM32中,中断的函数都是固定的。他们在启动文件中存放xxx.s
    //以IRQHandler结尾的就是中断函数的名字。
    //在这里需要找到对应的中断函数,我这里是2
    void EXTI2_IRQHandler(void)//中断函数是无参无返回值的。中断函数必须写对,写错就进不去
    {
    //在进入中断后,一般要判断一下这个是不是我们想要的那个中断源触发的中断。
    //但是在这里。我是GPIOA的PIN2引脚,所以不用写。
    //如果是5-9 10-15的引脚。他们EXTI到NVIC是几个共用的。
    //所以需要根据EXTI输入时的16根引脚。来判断是16根引脚的那一根发送的中断请求。
    //这里规范写的话需要加上去
    //查找标志位函数在exit.h中。
    if(EXTI_GetITStatus(EXTI_Line2) == SET)//第一个参数是行数.判断这个线的标志位是不是== SET。是则是我们想要的
    {
    CountSensor_Count++;
    EXTI_ClearITPendingBit(EXTI_Line2);//中断结束后,要调用清除标志位的函数。如果你不清除,程序会一直进入中断
    }
    }
    #ifndef __COUNTSENSOR_H
    #define __COUNTSENSOR_H
    //初始化旋转编码计次
    void CountSensor_Init(void);
    //获取当前计数值
    uint16_t CountSenSor_Get(void);
    #endif
    有预装计数器时序和无预装计时器时序
    • 无预装时序计数器
    notion image
    可以看到,在更新自动加载寄存器为36周。立马就改变了。如果此时计数器寄存器的值为37.那么他并不会开始下个周期。而是一直++++++++。+到FFFF之后才会重新清零。
    • 有预装时序计数器
    notion image
    这里的和之前说过的与分频器是一样的。
    如果我们突然改变了自动重装寄存器ARR的值。他不会立刻改。而是等待这个周期结束才改变。
     
    这其实也是同步和异步的区别使用影子寄存器就是同步

    tim.h库定时器中断和外部计数的常用函数介绍

    notion image
    时基单元部分
    TIM_DeInit 回复缺省配置(恢复初始化前的状态。理解成停止吧)
    TIM_TimeBaseInit 时基单元初始化
    TIM_TimeBaseStructInit 把结构体变量赋一个默认值
    运行控制部分
    TIM_Cmd 选择寄存器使能还是失能
    中断控制部分
    TIM_ITConfig 使能终端输出信号(中断输出控制) 选择定时器、选择配置那个中断输出,使能还是失能
    时基单元的始终选择部分
    void TIM_InternalClockConfig(TIM_TypeDef* TIMx); 选择内部时钟
    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输入的时钟(预分频器,极性和滤波器)
    void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter); 用来配置ETR引脚的预分频器、极性和滤波器….
    NVIC见外部中断哪一章
    单独改写部分参数部分
    TIM_PrescalerConfig 单独写预分频值(TIMx、要分频的值、写入的模式 (两种)
    TIM_CounterModeConfig 改变计数器模式
    TIM_ARRPreloadConfig 是否开启计数器的预装模式(是否使用影子寄存器)
    TIM_SetCounter 手动给计数器写个值
    TIM_SetAutoreload 手动给重装寄存器写个值
    TIM_GetCounter 获取当前计数器的值
    TIM_GetFlagStatus 获取当前预分频器的值
    获取和清除中断标志位
    FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
    void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
    ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
    void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
     

    编写:定时器定时中断计数

    定时器中断 (这里是向上计数)就是是 计数器CNT的数字到达重装寄存器ARR之后。发生的中断(除此之外还有一个时间响应)。
    notion image
    1. 要使用定时器TIM2,首先要把通往TIM2的时钟打通。TIM2在时钟树上可以看到,是挂载在APB1总线上的。RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
    1. 开启时钟之后,就可以去选择时钟源了,时钟源有很多的选择。一般用的是内部时钟或者是外部时钟。外部时钟可以走外部时钟模式1或者外部时钟模式2. 这里选择内部时钟模.这点需要在tim.h找到相应的函数:TIM_InternalClockConfig(TIM2);
    1. 配置通时钟输入之后,就可以开始配置中间的时基单元了,配置时基单元需要用到一个结构体,然后使用初始化函数,这点跟GPIO的一样。作用可以 看下边的。
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1 这个结构体参数去看注释。
    大概的意思是他是给滤波器的分频器。填写的值域F N 有关 。 F是采样频率。越高越好。N是采样次数。 意思是只要我采样到N次 才算他有效。填写啥东西 对应的F和N在手册中可查
     
    程序在运行TIM_TimeBaseInit 函数时,会在最后一步手动更新一下更新事件。因为 计时器和ARR重装寄存器。他们都有一个 影子寄存器 因为影子寄存器的存在。我们写入的值(重装寄存器ARR和计数器的值CNT)不会立刻生效,而是等待此次周期结束也就是在更新事件的时候才会生效。所以这个函数在最后的时候,手动生成了一下更新事件。 但副作用是,更新事件和更新中断是同时发生作用的。所以此时的中断标志位已经置一了。(中断标志位 不手动清除会一直为1)。所以当程序起来之后,会立马进入中断一次。 所以在上电复位之后,在查看计数器时已经是1了) 所以在这里我们需要手动的先清除一下此次的更新中断标志位 TIM_ClearFlag(TIM2,TIM_IT_Update);
    1. 再往下走。需要配置TIm的中断输出控制 要使能我们的 CNT计数到ARR时产生的中断,才能被NVIC接受到。 TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);//使能TIM2的更新中断到NVIC
    1. 下一步就比较熟悉了。 使能NVIC
      1. 最后一步!
        1. 使能寄存器!!TIM_Cmd(TIM2,ENABLE);
      1. 中断函数的配置。是进入中断是 num++
        1. 这里我们声明了 main.c里的函数里的Num (声明的时候不能同时赋值,否则会出错T.T)
          总代码如下!!

      编写:定时器外部时钟计数

      抄一遍上边的。
      只需要改一下输入的时钟,(以及对定时器的配置。因为我是手动输入..得把他改小点)
      选择的时钟源改为了外部时钟模式2
      其他的没啥了。不细说了

      TIM输出比较是什么

      OC(输出比较)与PWM介绍

      • OC(Output Compare)输出比较
      • 输出比较可以通过比较CNT与CCR寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形
      • 每个高级定时器和通用定时器都拥有4个输出比较通道
      • 高级定时器的前3个通道额外拥有死区生成和互补输出的功能(用于驱动三相无刷电机)
      notion image
      看下半部分。其中的捕获/比较寄存器是输入和输出共用的。
      当输入时,他就是输入捕获寄存器
      当输出是,他就是输出比较寄存器
      在输出比较时。 这个电路会比较CNT和我们设置的CCR的值。对应的输出1 或 0
      他非常适合用来输出PWM波
      PWM不细讲。是调节占空比的。
      脉冲宽度调制PWM(Pulse Width Modulation)
      在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域
      PWM参数:
      频率 分辨率 分辨率(占空比变化的细腻程度)
       

      输出比较框图介绍

      notion image
      左边是CNT和CCR的输入。比较之后根据输出模式输出信号到右边,
      右边有一个控制电路,可以把这个信号取反或者原信号不变。
      再往右就是使能,然后输出到OC1
      OC1其实就是 在引脚表上的 复用了

      输出比较模式介绍

      notion image
      输出比较模式有这几种
      冻结意思就是 当前状态维持不变
      其他俩的有效 其实可以理解为高电平。 或低电平
      最后俩是最重要的。

      输出比较 根据计数器输出 PWM的基本结构(通用定时器)

      notion image
      当我们配置好时基单元之后,CNT就开始了自增运行(自己设置,一般都是自增)
      这里的输出比较单元 模式是 PWM1模式, CNT<CCR时,为有效电平。≥则输出无效电平。输出的信号用REF表示
      可以看右上角的图, CNT<CCR时,为有效电平。≥则输出无效电平。并且占空比收到CCR的控制
      再经过极性选择。就输出到GPIO了
      可以看图。
      PWM的频率 就是 计数器的频率
      占空比的大小 是 CCR / ARR+1 (CNT和CCR在相等的瞬间就条变为低电平了)
      分辨率大小 是 1/(ARR+1) 也就是自动重装值+1 这个值是越小越好的。意思就是PWM从0到100中间的值就更多。

      高级定时器的比较通道大概介绍

      可以看到,大概是一样的。不过多了一个死区生成电路和另一对输出使能电路
      OC1和OC1N是两个互补的端口,为了方便理解,可以假设为他们现在接了一个这样的电路(红线),这是一个基础的控制mos管的电路。
      如果上下都导通,是不允许的,因为电源直接接地
      如果上导通下不导通,则输出高电平
      如果下导通上不导通,则输出低电平
      如果上下都不导通,则为高阻态。
      notion image
      我们可以这么理解。在驱动Mos管的时候。
      上管关断,下管立刻就打开的时候,由于器件的不理想。 可能上管没完全关断。下管就打开了。出现了短暂的上下管导通的现象。这就会导致功率损耗,并且引起发热。
      为了避免这个问题,就引入了死区生成电路。他会在上管关闭 的时候延迟一小段时间再打开下管。

      tim.h库定时器OC比较电路常用的函数介绍

      void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1); void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2); void TIM_SetCompare3(TIM_TypeDef* TIMx, uint16_t Compare3); void TIM_SetCompare4(TIM_TypeDef* TIMx, uint16_t Compare4);
      单独修改CCR寄存器值的函数
       
      void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); void TIM_OC3Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); void TIM_OC4Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
      这些就是了。因为输出比较单元有四个,所以一个OC就是配置一个比较单元啦。
       
      void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);
      这个是用来给输出比较结构体赋一个默认值的
       
      一些小功能和运行时更改参数的函数
      void TIM_ForcedOC1Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction); void TIM_ForcedOC2Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction); void TIM_ForcedOC3Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction); void TIM_ForcedOC4Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
      上面这些是强制让某个输出比较单元输出高或低电平。 (仅了解就好,因为他们和对0%占空比100%占空比一样的)
       
      void TIM_OC1PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload); void TIM_OC2PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload); void TIM_OC3PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload); void TIM_OC4PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
      这四个寄存器是用来配置预装功能的(之前的影子寄存器) 让你新写入的CCR不会立刻生效,而是在时间更新后生效(一般不用。知道就好)
       
       
      void TIM_OC1FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast); void TIM_OC2FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast); void TIM_OC3FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast); void TIM_OC4FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);
      配置快速使能的,用得不多,不用掌握
       
      void TIM_ClearOC1Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear); void TIM_ClearOC2Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear); void TIM_ClearOC3Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear); void TIM_ClearOC4Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
      清除外部事件的REF信号的。不用掌握
       
      void TIM_OC1PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity); void TIM_OC1NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity); void TIM_OC2PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity); void TIM_OC2NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity); void TIM_OC3PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity); void TIM_OC3NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity); void TIM_OC4PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
      这些是单独设置输出比较的极性的。其中带N的是高级定时器的互补通道的配置(因为OC4没有互补通道,所有没有OC4N)
       
       
      void TIM_CCxCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCx); void TIM_CCxNCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCxN);
      单独修改输出使能参数
       
      void TIM_SelectOCxM(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_OCMode);
      单独更改输出比较模式的函数
       
       

      小知识:GPIO与GPIO端口映射

      在引脚图上,部分的GPIO是可以映射到别的GPIO口的。不过是固定的。不可以随意的去配置(在AFIO映射部分的手册上有)。使用前要先打开AFIO的时钟。然后使用 GPIO_PinRemapConfig(xxxx,ENABLE) 去重映射。
      不过要注意别把固定复用的JTAG SWD给清除掉。不然就只能用串口去下载程序恢复了。

      编写:输出PWM 驱动LED呼吸灯

      结果:
      notion image
      配置定时器输出PWM波。
      1. 使能输出PWM的PA0端口,配置PA0为复用推挽输出模式 因为在代码中,我们用的是TIM2的OC1输出比较器。在框图中:TIMx的OC1的输出,是固定输出在PA0引脚上。所以需要对PA0引脚复用为输出。
      1. 使能TIM2的时钟,TIM2是挂载在APB1的通道上。
      1. 选择输入时钟为内部时钟。
      1. 配置时基单元 在配置时基单元时,我们要根据预期输出的PWM的频率、占空比以及分辨率去设置重装寄存器ARR的值和与分频器PSC的值公式和举例如下:
          • 目的:产生频率为1KHZ 占空比50% 分辨率为1%
          • 公式
            • 分辨率 = 1 / (重装寄存器ARR+1)
            • 占空比 = 比较控制值CCR / (重装寄存器ARR+1)
            • 频率 = 时钟频率CK_PSC(72MHZ) / (预分频值PSC+1)(重装寄存器ARR+1)
          • 结果
            • ARR = 100 - 1 自动重装载寄存器
            • CCR = 50 输出比较寄存器
            • PSC = 720 预分频寄存器
      1. 配置OC输出比较电路
      1. 使能TIM2 OC和时基单元啥的都在TIm2里。所以最后打开
      1. 使用TIM_SetCompare1 更改输出比较寄存器的CCR值。来更改PWM波 在这里如果要更改PWM只需要修改CCR就OK。这是因为我们的ARR自动重装载寄存器的值为100-1 所以 输入的CCR就相当于百分比了。实际情况需要考虑一下CCR与ARR的值
      1. 最后是在main.c函数中使用for 让CCR的值从 0 - 100 100 - 0循环。实现PWM驱动LED的呼吸灯效果
      程序如下: PWM.c
      TIM_TimeBaseInit(TIM2,&TIM_TImeBaseInitStructure);
      /*
      产生频率为1KHZ 占空比50% 分辨率为1%
      分辨率 = 1 / (重装寄存器ARR+1) ARR = 100 - 1
      占空比 = 比较控制值CCR / (重装寄存器ARR+1) CCR = 50
      频率 = 时钟频率CK_PSC(72MHZ) / (预分频值PSC+1)(重装寄存器ARR+1)
      PSC = 720
      */
      //使能OC比较通道 1
      TIM_OCInitTypeDef TIM_OCInitStructure; //不用引出高级定时器的成员。
      TIM_OCStructInit(&TIM_OCInitStructure); //但是要先把每个成员一个初始值,避免不必要的问题
      TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //设置输出比较模式 - PWM1模式
      TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //是否反转 - 否
      TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //输出使能 - 使能
      TIM_OCInitStructure.TIM_Pulse = 0; //配置CCR寄存器的值。16位
      TIM_OC1Init(TIM2,&TIM_OCInitStructure);
      //使能定时器
      TIM_Cmd(TIM2,ENABLE);
      }
      void PWM_SetCompare1(uint16_t Comparel)
      {
      TIM_SetCompare1(TIM2,Comparel);
      }
      #include "stm32f10x.h" // Device header
      #include "oled.h"
      #include "Delay.h"
      #include "pwm.h"
      int main()
      {
      OLED_Init();//初始化OLED;
      Delay_Init();//初始化延时
      PWM_Init();//初始化PWM
      uint16_t i = 0;
      while(1)
      {
      for(i = 0; i <= 100; i++)
      {
      PWM_SetCompare1(i);
      Delay_ms(10);//延时,否则占空比变的太快10*100也就是1秒变亮
      }
      for(i = 0; i <= 100; i++)
      {
      PWM_SetCompare1(100 - i);//从100慢慢下降
      Delay_ms(10);
      }
      }
      }
      #include "stm32f10x.h" // Device header
      /*
      ARR = 20000
      占空比0.5时
      RCC = 20000 * 0.5/20 = 500
      */
      void PWM_Init(void)
      {
      //使能PWM的输出引脚PA1
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
      GPIO_InitTypeDef GPIO_InitStructuer;
      GPIO_InitStructuer.GPIO_Mode = GPIO_Mode_AF_PP;//选择复用推挽输出,把GPIO引脚的控制权交给偏上外设
      GPIO_InitStructuer.GPIO_Pin = GPIO_Pin_1;//OC通道输出
      GPIO_InitStructuer.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_Init(GPIOA,&GPIO_InitStructuer);
      //使能TIM2时钟
      RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
      //选择时钟源为内部时钟
      TIM_InternalClockConfig(TIM2);
      //配置时基单元
      TIM_TimeBaseInitTypeDef TIM_TImeBaseInitStructure;
      TIM_TImeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //指定时钟分频
      TIM_TImeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //选择计数模式
      TIM_TImeBaseInitStructure.TIM_Period = 20000 - 1; //配置自动重装寄存器ARR
      TIM_TImeBaseInitStructure.TIM_Prescaler = 72 - 1; //配置到计数器信号的分频系数PSC
      TIM_TImeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数,高级定时器才用到
      TIM_TimeBaseInit(TIM2,&TIM_TImeBaseInitStructure);
      /*
      产生频率为50HZ 占空比0% 分辨率为0.05
      分辨率 = 1 / (重装寄存器ARR+1)
      占空比 = 比较控制值CCR / (重装寄存器ARR+1)
      频率 = 时钟频率CK_PSC(72MHZ) / (预分频值PSC+1)(重装寄存器ARR+1)
      0.05 = 1 / ARR + 1 ---- ARR = 20000 - 1
      50 = 72M / (psc+1)(20000)
      PSC+1 = 72 --- PSC = 72 -1
      */
      //使能OC比较通道 1
      TIM_OCInitTypeDef TIM_OCInitStructure; //不用引出高级定时器的成员。
      TIM_OCStructInit(&TIM_OCInitStructure); //但是要先把每个成员一个初始值,避免不必要的问题
      TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //设置输出比较模式 - PWM1模式
      TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //是否反转 - 否
      TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //输出使能 - 使能
      TIM_OCInitStructure.TIM_Pulse = 0; //配置CCR寄存器的值。16位
      TIM_OC2Init(TIM2,&TIM_OCInitStructure);//配置在OC2通道。输出在PA1引脚上
      //使能定时器
      TIM_Cmd(TIM2,ENABLE);
      }
      void PWM_SetCompare2(uint16_t Compare2)//更改为通道2
      {
      TIM_SetCompare2(TIM2,Compare2);
      }
      /*
      PA1引脚输出PWM波控制舵机
      按键调整PWM占空比
      显示在OLED屏幕上
      舵机要求输入PWM要求:周期20ms,高电平宽度要求0.5ms~2.5ms
      ←- -90° 0.5ms
      ↖ -45° 1ms
      ↑ 0° 1.5ms
      ↗ 45° 2ms
      → 90° 2.5ms
      */
      int main()
      {
      OLED_Init();//初始化OLED;
      Delay_Init();//初始化延时
      PWM_Init();//初始化PWM
      KEY_Init();//初始化KEY
      OLED_ShowString(1,1,"CCR:");
      OLED_ShowString(2,1,"ARR:20000");
      OLED_ShowNum(1,5,0,4);
      uint16_t Num = 0;
      while(1)
      {
      if(!KEY_Get())//低电平有效
      {
      Delay_ms(10);
      if(!KEY_Get())//确定
      {
      while(!KEY_Get());//等待松开
      Num += 500;
      if(Num > 2500)
      {
      Num = 500;
      }
      OLED_ShowNum(1,5,Num,4);
      PWM_SetCompare2(Num);
      }
      }
      }
      }
      触发源选择就是选择用那个源来连接到TRGI去 触发 从模式 从模式 则可以在右边的列表中,选择一项来自动执行
      比如我们要清除CNT计数,就可以选择TI1FP1上升沿连接到TRGI 从模式选择Reset、
      这些意义都可以在控制寄存器里手册查看

      测周法测量频率思路

      有了上面的框图介绍。 大概就可以设计出测周法测频率的方法了。
      每来一个上升沿用于触发输入捕获,CNT来计数得到N(得到N后要清零,可以使用主从触发模式自动完成),fc则是时钟频率。
      fc/N就是待测信号的频率

      输入捕获测频率的框图

      notion image
      GPIO的信号经过滤波器滤波、
      边沿检测检测信号之后输出到分频器分频。
      然后分频器触发信号给捕获器。
      捕获器读取CNT的值到CCR中。
      另一路,在把UT1FP1的信号映射到从模式触发器上。从模式控制时基单元的CNT清除操作。

      输入捕获测频率和占空比(PWMI)的框图

      notion image
      和上边一样,不过多了一路
      这里可以自己去选择。 这里只是用到了通道2 的分频器部分。 因为每个边沿检测和极性选择都有两个输出。 TxFP1和TxFP2。
      下边一路选择为下降沿触发,这样 下降沿触发所记录的值 比上 下一个上升沿所记录的值就是占空比了!
       
      在TIMx功能描述有 手册里

      测频和测周法的应用场景

      1. 测频法:在T秒内对上升沿次数计次得到N频率就是N/T (T为1则直接就是频率) 测周法是计算的闸门时间内的平均频率、T秒跳变一次. 在测周法的时候,可能最后或者第一个信号才过了一半就到了时间,所以测周法有一个+-1误差(取决于我们算他为有效还是无效) 测频法适合测量高频率的信号。这样精度更好
      1. 测周法:测出一个周期的时间,然后取倒数 实际上是用一个比已知频率还要高的频率fc。在已知频率的一个周期内,计算fc的高电平的次数N。记一个数的时间是1/fc 那么N个数的时间就是N/fc 那么取到数就是测量的信号的的频率啦fc/N 测频法跳变比较快、已知频率来一个上升沿他就跳一次。波动比较大 测周法也是一样,有可能计次记到一半,来上升沿了。也会出现- +1个误差。(取决于我们算他为有效还是无效) 测周法适合测量低频率的信号,精度更好
        1. 那多高算高频率。多低算低频呢? 这就引出了一个新的概念:中界频率 中介频率是测频法和测周法误差相等的频率点
          notion image
          可以看到这个图。分别是测频法和测周法。
          他们都有计次N。并且计次N的值是越多越好。也就是越准确。
          我们把测周法和测频法的公式列出来
          测频法:fx = N/T
          测周法:fx = fc/N
          当N = N时,解出来
          所以可以得到下面的公式
          fm = ( fc/T ) ^ (1/2) (fc/T后开方)
          所以,当测量频率大于fm 用测频法。
          当测量频率小于fm 用测周法

      测周法测频率和占空比的思路

      测周法测频率 是
      由我们已知的频率fc 来测量待测信号的两个上升沿之间 计数的值。
      在每个上升沿,由输入捕获记录CNT的计数值。 由从模式自动清除CNT的计数值 N(待测信号一个周期内,已知信号的上升沿个数)
      然后使用公式fc / N 得到频率。单位为秒
      测周法测频率和占空比
      notion image
      刚才使用测频法,只使用了一个IC通道。但若要测量占空比,则需要使用两个IC通道。
      由输入捕获框图可以了解到。IC1和IC2 的通道,实际上是有两个输入滤波和边沿检测器。他们俩一路输出TI1FP1,一路输出TI1FP2. 一个是直接连接到后续电路。另一个则是交叉连接到了IC2通道。(1 和 2 通道交叉连接 3 和4 通道交叉连接)。
      我们可以通过配置Ic2的通道,使得一个TI1FP2的滤波极性检测器输出与TI1FP1完全相反的检测(1检测上升沿,那么2检测下降沿),在这里也就是让2检测下降沿时CNT所记的数,存到CCR2。 此时 通道2的 CCR2 与通道1 的 CCR1 所记录的是 高电平的时间与整个周期的时间, 所以他们的比值就是占空比。(可以*100更直观)
       

      tim.h库定时器IC比较电路常用的函数介绍

      1. void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct); 用结构体配置输入捕获单元的函数 因为有交叉通道,所以不想OC一样每个都单独拎出来。
      1. void TIM_PWMIConfig(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct); 和上个函数类似,但是快速配置两通道的。配置成PWMI模式
      1. void TIM_ICStructInit(TIM_ICInitTypeDef* TIM_ICInitStruct); 给输入捕获结构体赋一个初始值
      1. void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource); 选择从模式的触发源
      1. void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource); 选择主模式输出触发源。
      1. TIM_SelectOnePulseMode 选择从模式(要从模式干啥)
      1. void TIM_SetIC1Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC); void TIM_SetIC2Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC); void TIM_SetIC3Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC); void TIM_SetIC4Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC); 配置通道1 、 2、 3、 4、的分频器
      1. uint16_t TIM_GetCapture1(TIM_TypeDef* TIMx); uint16_t TIM_GetCapture2(TIM_TypeDef* TIMx); uint16_t TIM_GetCapture3(TIM_TypeDef* TIMx); uint16_t TIM_GetCapture4(TIM_TypeDef* TIMx); uint16_t TIM_GetCounter(TIM_TypeDef* TIMx); 读取通道1、 2、 3、 4、的CCR

      编写:测周法测量频率和占空比

      写一个PA0输出可调占空比和频率的的PWM

      PA0是TIM2的复用输出引脚。 首先要对他进行初始化,
      初始化的步骤是 1,、使能GPIOA的时钟, 配置PA0的引脚为复用推挽输出
      1. 使能定时器外设,并选择输入时钟为内部时钟,然后配置时基单元 这里的72 - 1 是随便设置的。他到时候是要在主函数进行修改的。
      1. 使用OC比较输出 输出可调的PWM波
      1. 使能定时器TIM_Cmd(TIM2,ENABLE);
      1. 封装修改占空比和频率的函数 需要注意的是,修改频率有两种方式,一种是修改ARR重装寄存器的值。 另一种是修改PSC。 这里选择修改PSC。因为ARR不变可以更好的配置占空比。 使用的函数是:TIM_SetCompare1TIM_PrescalerConfig 修改PSC。为什么这里的修改预分频器不是set_Prescaler 这是因为 在修改psc的时候需要配置参数。不太方便,所以就一并参进去了。这里的第三个参数是 选择立刻生效还是在更新后生效(影子寄存器预装载)
      到这里,PA0就可以输出可调的PAM波了。
       

      写一个PA6输入PWM波然后检测占空比和频率

      为什么是PA6 因为我们在这里用的是TIM3 也就是第二个通用定时器去测量。
      而TIM3的 的输入是复用在PA6的引脚上的。
      所以我们在写程序的时候第一步也是使能GPIOA 和 配置PA6为输入 上拉。
      配置好GPIO之后就要开始配置时钟了。
      首先要使能TIM3的外设。以及配置TIM3的输入信号
      下一步就是配置时基单元了。 时基单元的配置决定了我们的fc的信号. 这里要注意的是 我们设置的ARR为65536.因为CNT的最大计数值决定了在计数的时候所能识别的最低频率。过小的话计数器就溢出了。导致频率不准。 PSC 是72 。也就是fc 的频率为72M/72 = 1MHZ。也就是说我们测量的最低频率为1M/65536 大约等于15HZ。如果频率低于15HZ,计数器就会发生溢出 如果想要下限再低一些。那么可以使PSC再加一点。这样标准频率1M就变小了。所支持的频率就也变小了。 下一步就是测量频率的上限了。这个最大频率,并没有一个明显的界限。 因为随着待测频率的增大, 测量频率的误差也会增大,如果非要找一个频率上线,那应该就是标准频率的1MHZ,如果超过标准频率了。信号频率比标准频率还高 那么肯定是测不了的。但是这个上限是没有意义的,因为当信号到1MHZ的时候误差已经非常大了。 所以对于最大误差的要求就是我们对 误差的要求: 也就是说,如果你要求误差为千分之一,那么频率上限就是1M/1000 = 1000HZ 如果你要求误差为百分之一,那么频率上限就是1M/100 = 10KHZ
      想要提高频率的上限就要把PSC给降低一些。提高了标准频率 上限就会提高。 (要是频率还想高 就考虑测频法把。 测周法适合低频)
      TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1;//决定了CNT计数的最大值,测频率最好要设置大一些,防止计数溢出
      TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;//决定了测周法的标准频率fc,暂时先定义为1MHZ
      TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//高级定时器的复用定时器
      TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
      //清除时基单元配置好后产生的标志位
      TIM_ClearFlag(TIM3,TIM_IT_Update);
      //配置输出捕获单元IC
      TIM_ICInitTypeDef TIM_ICInitStructure;
      TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//选择通道几?
      TIM_ICInitStructure.TIM_ICFilter = 0xF;//配置滤波器参数(对应参数在参考手册有0x0~0xF)
      TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//触发方式(上升下降或者都)--这里是上升沿读取
      TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;//触发信号分频器
      TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;//选择触发信号从哪个引脚输入(配置数据选择器)
      TIM_ICInit(TIM3,&TIM_ICInitStructure);
      TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;//选择通道2
      TIM_ICInitStructure.TIM_ICFilter = 0xF;
      TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling;//下降沿有效
      TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;//触发信号分频器
      TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_IndirectTI;//交叉输入
      TIM_ICInit(TIM3,&TIM_ICInitStructure);
      // 会进行判断然后帮你弄成相反的(可以看F2)
      //(只支持通道1 和通道2 的配置,不能传入通道3 4)
      TIM_PWMIConfig(TIM3,&TIM_ICInitStructure);
      //配置主从模式的TRGI的触发源为TI1FP1
      TIM_SelectInputTrigger(TIM3,TIM_TS_TI1FP1);
      //配置从模式为reset
      TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);//读取后自动清除
      //获取频率
      uint32_t IC_GetFreq(void)
      {
      //执行公式,fx = fc / N fc = 1M
      return 1000000 / (TIM_GetCapture1(TIM3)+1);//读取TIM3的通道1的CCR的值
      // +1 为面向对象编程...因为1000HZ显示的是1001
      //+1 的原因是因为每次都会少记录一个。+1补成999+1 这样就能整除了
      }
      //获取占空比
      uint32_t IC_GetButy(void)
      {
      //下降沿的计数 比上 上升沿计数就是占空比
      //下降沿记得数存在通道二的CCR2中,上升沿记得数存在CCR1中。
      return (TIM_GetCapture2(TIM3) + 1 ) 100 / (TIM_GetCapture1(TIM3) + 1);//100是现实整数 1%~100%
      //CCR总会少一个数,所以面向对象编程
      }
      #include "stm32f10x.h" // Device header
      #include "oled.h"
      #include "Delay.h"
      #include "key.h"
      #include "pwm.h"
      #include "ic.h"
      /*
      PA0输出频率和占空比可调的PWM波
      PA6作为输入 来检测PA0的频率和占空比
      采用测周法
      */
      int main()
      {
      OLED_Init();//初始化OLED;
      PWM_Init();//初始化PWM
      IC_Init();//初始化IC
      KEY_Init();//初始化KEY
      //设置频率和占空比 //(ARR+1)
      PWM_SetPrescaler(720 - 1); //1000HZ = 72M / 100 / (PSC+1)
      PWM_SetCompare1(50); //占空比 = CCR/ARR = CCR/100
      OLED_ShowString(1,1,"Freq:00000HZ");
      OLED_ShowString(2,1,"Buty:00%");
      while(1)
      {
      //循环显示当前周期
      OLED_ShowNum(1,6,IC_GetFreq(),5);//显示频率
      OLED_ShowNum(2,6,IC_GetButy(),2);//显示占空比
      }
      }
      void Encoder_Init(void)
      {
      //开启GPIOA和定时器外设的时钟
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
      RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
      //配置PA6、PA7
      GPIO_InitTypeDef GPIO_InitStructure;
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
      GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_Init(GPIOA,&GPIO_InitStructure);
      //编码器会托管时钟,这里不需要配置输入时钟
      //配置时基单元
      TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
      TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//滤波器分频设置F N
      TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//计数模式没用,会被编码器托管
      TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //一般选择满量程计数,这样在从0-1就会变成65535.后续强制把65536转换为有符号数,就可以把他转换为负数。
      TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;//不分频
      TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
      TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
      //配置输入捕获单元的滤波器和边缘极性选择
      //其余的交给初始化函数
      TIM_ICInitTypeDef TIM_ICInitStructure;
      TIM_ICStructInit(&TIM_ICInitStructure);//初始化结构体
      TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//通道为1
      TIM_ICInitStructure.TIM_ICFilter = 0XF;//滤波器为0xF
      TIM_ICInit(TIM3,&TIM_ICInitStructure);
      TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;//通道为2
      TIM_ICInitStructure.TIM_ICFilter = 0XF;//滤波器为0xF
      TIM_ICInit(TIM3,&TIM_ICInitStructure);
      //配置编码器接口 选择TI12 都计数 是否反向?
      TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
      TIM_Cmd(TIM3,ENABLE);
      //读取编码器的值
      //读完后清零CNT
      int16_t Encoder_Get(void)
      {
      int16_t Temp = TIM_GetCounter(TIM3);
      TIM_SetCounter(TIM3,0);//清零
      return Temp;
      }
      int main(){
      OLED_Init();//初始化OLED;
      Encoder_Init();//初始化Encoder
      Delay_Init();
      OLED_ShowString(1,1,"Num:");
      while(1)
      {
      OLED_ShowSignedNum(1,5,Encoder_Get(),5);
      Delay_ms(1000);
      }
      }
      NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//选择分组
      NVIC_InitTypeDef NVIC_InitStructuer;//定义结构体
      NVIC_InitStructuer.NVIC_IRQChannel = TIM2_IRQn;//选择定时器2在NVIC里的通道
      NVIC_InitStructuer.NVIC_IRQChannelCmd = ENABLE;//是否使能
      NVIC_InitStructuer.NVIC_IRQChannelPreemptionPriority = 2;//抢占优先级
      NVIC_InitStructuer.NVIC_IRQChannelSubPriority = 2;//响应优先级。
      NVIC_Init(&NVIC_InitStructuer);
      //配置中断函数
      void TIM2_IRQHandler(void)
      {
      if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)//检查中断标志位
      {
      Speed = Encoder_Get();
      TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
      }
      }
      #include "stm32f10x.h" // Device header
      #include "Encoder.h"
      extern int16_t Speed;//记录时间的变量、这里是引用了main.c里的Num来控制。
      void Timer_Init(void)
      {
      //开启RCC时钟
      RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//TIM2是挂载在APB1总线上的
      //选择时基单元钟源为内部时钟。在tim.h头文件
      TIM_InternalClockConfig(TIM2);
      //配置时基单元
      TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;//初始化时基单元要定义的结构体
      TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//指定时钟分频、1分频
      TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//选择计数模式,分别是向上、向下、三种中央对其模式
      TIM_TimeBaseStructure.TIM_Period = 10000 - 1;//ARR重装值 = 值 +1 所以我们再用的时候要-1.才能正确匹配。这里是ARR和PSC定时1s
      TIM_TimeBaseStructure.TIM_Prescaler = 7200 - 1;//PSC分频器分频系数= 值+1
      TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;//重复计数器,这个是高级计数器才有的,我们不需要。所以给0
      TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);//这个函数是初始化时基单元的。
      TIM_ClearFlag(TIM2,TIM_IT_Update); //清除更新标志位
      //配置输出中断控制
      TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);//使能TIM2的更新中断到NVIC
      //配置NVIC
      NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//选择分组
      NVIC_InitTypeDef NVIC_InitStructuer;//定义结构体
      NVIC_InitStructuer.NVIC_IRQChannel = TIM2_IRQn;//选择定时器2在NVIC里的通道
      NVIC_InitStructuer.NVIC_IRQChannelCmd = ENABLE;//是否使能
      NVIC_InitStructuer.NVIC_IRQChannelPreemptionPriority = 2;//抢占优先级
      NVIC_InitStructuer.NVIC_IRQChannelSubPriority = 2;//响应优先级。
      NVIC_Init(&NVIC_InitStructuer);
      //运行控制,使能计数器
      TIM_Cmd(TIM2,ENABLE);
      }
      //配置中断函数
      void TIM2_IRQHandler(void)
      {
      if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)//检查中断标志位
      {
      Speed = Encoder_Get();
      TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
      }
      }
      #include "stm32f10x.h" // Device header
      #include "oled.h"
      #include "Delay.h"
      #include "key.h"
      #include "Encoder.h"
      #include "Timer.h"
      /*
      使用PA6 和 PA7 引脚的复用模式
      也就是TIM3 的CH1通道和CH2通道
      使用编码器接口
      */
      int16_t Speed = 0;//对Timer.c中声明过的变量
      int main(){
      OLED_Init();//初始化OLED;
      Encoder_Init();//初始化Encoder
      // Delay_Init();//初始化延时函数
      Timer_Init();//初始化定时1s中断函数
      OLED_ShowString(1,1,"Num:");
      while(1)
      {
      OLED_ShowSignedNum(1,5,Speed,5);
      // Delay_ms(1000);
      }
      }
      #include "stm32f10x.h" // Device header
      #include "oled.h"
      #include "Delay.h"
      #include "key.h"
      /*
      独立看门狗测试程序
      喂狗时间为1s内
      */
      int main()
      {
      OLED_Init();//初始化OLED;
      Delay_Init();
      KEY_Init();
      //这是一个IWDG测试程序
      OLED_ShowString(1,1,"Test IWDG");
      //复位后判断复位原因
      if(RCC_GetFlagStatus(RCC_FLAG_IWDGRST) == SET )//是独立看门狗复位
      {
      OLED_ShowString(2,1,"IWDG RST");
      //清除独立看门狗复位标志位,这个他复位也不会自动清除,需要手动清
      RCC_ClearFlag();
      }
      else
      {
      OLED_ShowString(2,1," RST");
      }
      //配置独立看门狗
      IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);//写入0x5555关闭保护
      IWDG_SetPrescaler(IWDG_Prescaler_16);//设置16分频(这样最大时间才为0.025164096 = 1638ms)
      IWDG_SetReload(2500 - 1);//设置重装值为2500 这样喂狗时间就是1s内了。
      IWDG_ReloadCounter();//启动前喂狗一次,重置下计数器
      IWDG_Enable(); //启动小狗
      while(1)
      {
      KEY_Get();
      //0.9s喂狗一次
      Delay_ms(200);
      IWDG_ReloadCounter();
      OLED_ShowString(3,1,"WangWang");//显示喂狗了
      Delay_ms(700);
      OLED_ShowString(3,1," ");//闪烁
      }
      }
      //按键按下会进入while阻塞程序,不让喂狗
      //uint8_t KEY_Get(void)
      //{
      // if(KEY1)
      // {
      // Delay_ms(10);
      // while(KEY1);//等待松开
      // return 1;
      // }
      // return 0;
      //}
      #include "stm32f10x.h" // Device header
      #include "oled.h"
      #include "Delay.h"
      #include "key.h"
      /*
      窗口看门狗测试程序
      超时时间50ms
      窗口时间30ms
      */
      int main()
      {
      OLED_Init();//初始化OLED;
      KEY_Init();
      //这是一个WWDG测试程序
      OLED_ShowString(1,1,"Test WWDG");
      //复位后判断复位原因
      if(RCC_GetFlagStatus(RCC_FLAG_WWDGRST) == SET )//是独立看门狗复位
      {
      OLED_ShowString(2,1,"WWDG RST");
      //清除窗口看门狗复位标志位,这个他复位也不会自动清除,需要手动清
      RCC_ClearFlag();
      }
      else
      {
      OLED_ShowString(2,1," RST");
      }
      /*
      首先得到时钟时间。32M * 1/4096 = 7812.5HZ
      那么他一个周期的时间就是0.000128s
      那么选择分频为3,也就是2^3 = 8分频。那么现在一个周期的时间就是 0.000128 * 8= 0.001024s
      这时候如果重装载寄存器选为最大数字(6位)63 + 1
      那么他的最大超时时间就是0.58s
      也就是最大值为58ms
      我们要设置的50ms在范围内
      那么我们要设置的分频系数就是 3 所求得的重装值应是(约等于)55。实际给54 (因为 - 1)
      计算窗口时间,设置为30ms 那么设置窗口的6位的值为
      30 = 1/32m 4096 8 *(54 - W[5:0])
      求得W的值为 21
      */
      //配置窗口看门狗
      //开启窗口开门狗时钟
      RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG,ENABLE);
      WWDG_SetPrescaler(WWDG_Prescaler_8);
      WWDG_SetWindowValue(0x40 | 21);//30ms
      WWDG_Enable(0x40 | 54);
      // 50ms 这里的0x40是为了把计数器的第七位置1
      // 因为当他减为0是生成复位信号。(这也是重装载需要 -1 的原因)
      uint32_t i = 0;
      while(1)
      {
      KEY_Get();
      OLED_ShowString(3,1,"Wang");//很奇怪,如果是显示WangWang就不行了....多显示几个字符难道这么浪费时间吗
      Delay_ms(20);
      OLED_ShowString(3,1," ");
      Delay_ms(20);
      OLED_ShowNum(4,1,i,4);
      i++;
      WWDG_SetCounter(0x40 | 54);//喂狗,这里注意不要离使能时喂狗太近。
      }
      }
      notion image

      连续转换,扫描模式

      一次转换结束后,立刻开始下一次转换
      notion image

      一些知识补充

      规则组的触发信号

      notion image

      数据对齐

      一个是数据往右方,高位补零(一般使用这个)
      一个是数据往左放,低位补零(他的作用是舍弃一些精度。)

      转换时间

      AD转换的步骤 : 采样,保持,量化,编码
      采样 :因为电平不断跳变,所以要暂时用电容把他存起来。 开关的打开和关闭需要时间 所以采样需要时间 STM32 ADC的总转换时间为:T_CONV =采样时间+12.5个ADC周期 例如:当ADCCLK=14MHz,采样时间为1.5个ADC周期TcoNv=1.5+12.5=14个ADC周期= 1us 这就是ADC 最快为1us的时间来源

      ADC校准

      ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差
      建议在每次上电后执行一次校准
      启动校准前,ADC必须处于关电状态超过至少两个ADC时钟周期

      ADC的库函数介绍

      void RCC_ADCCLKConfig(uint32_t RCC_PCLK2); RCC.h中 用来配置RCC的时钟
      void ADC_DeInit(ADC_TypeDef* ADCx); void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct); void ADC_StructInit(ADC_InitTypeDef* ADC_InitStruct); void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState); 恢复缺省配置、 配置初始化 初始化、使能ADC
      void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState); 开启DMA搬运
      void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState); 控制某个中断能不能通往NVIC
      void ADC_ResetCalibration(ADC_TypeDef* ADCx); FlagStatus ADC_GetResetCalibrationStatus(ADC_TypeDef* ADCx); void ADC_StartCalibration(ADC_TypeDef* ADCx); FlagStatus ADC_GetCalibrationStatus(ADC_TypeDef* ADCx); 复位校准、获取复位校准状态、开始校准、获取开始校准状态 控制校准状态用的
      void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState); ADC软件转换控制、 软件触发转换
      void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState); 给SWStart位置1,以开始转换
      void ADC_ClearFlag(ADC_TypeDef* ADCx, uint8_t ADC_FLAG); 获取标志位状态。比如EOC 转换结束标志位
      void ADC_DiscModeChannelCountConfig(ADC_TypeDef* ADCx, uint8_t Number); void ADC_DiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState); 间断模式配置和启用间断模式
      void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime); 作用是给序列的每个位置填写指定的位置 第一个参数ADCx、 指定的通道、 序列几的位置 、 通道的采样时间
      void ADC_ExternalTrigConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState); 是否允许外部触发转换
      uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx); 获取AD转换的数据寄存器,读取转换需要用到
      uint32_t ADC_GetDualModeConversionValue(void); 双ADC读取转换数据寄存器
       
      void ADC_AnalogWatchdogCmd(ADC_TypeDef* ADCx, uint32_t ADC_AnalogWatchdog); void ADC_AnalogWatchdogThresholdsConfig(ADC_TypeDef* ADCx, uint16_t HighThreshold, uint16_t LowThreshold); void ADC_AnalogWatchdogSingleChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel); 模拟看门狗配置: 配置是否启动看门狗、 配置高低阈值、配置看门通道
       
      void ADC_TempSensorVrefintCmd(FunctionalState NewState); ADC温度传感器,内部参考电压设置 这个是内部的俩通道的
       
      FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG); void ADC_ClearFlag(ADC_TypeDef* ADCx, uint8_t ADC_FLAG); ITStatus ADC_GetITStatus(ADC_TypeDef* ADCx, uint16_t ADC_IT); void ADC_ClearITPendingBit(ADC_TypeDef* ADCx, uint16_t ADC_IT); 获取标志位状态、清除标志位、获取中断状态、清除中断挂起位
       
      注入组的一些配置

      编写:单通道 单次转换 非扫描模式测电压

      AD.c

      main.c

      编写:单通道 单次转换 实现检测四个电压

      AD.c

      ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发:无
      ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式:独立ADC模式
      ADC_InitStructure.ADC_NbrOfChannel = 1; //指定扫描模式下用几个通道
      ADC_InitStructure.ADC_ScanConvMode = DISABLE; //是否扫描模式:否
      ADC_Init(ADC1,&ADC_InitStructure);
      //开启ADC1
      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);//规则组序列1的位置,配置为通道ADC_Channel
      //软件触发转换
      ADC_SoftwareStartConvCmd(ADC1,ENABLE);
      while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET);//等待转换完成
      //获取转换值
      return ADC_GetConversionValue(ADC1);
      }
      #include "stm32f10x.h" // Device header
      #include "oled.h"
      #include "Delay.h"
      #include "key.h"
      #include "AD.h"
      /*
      使用ADC1
      检测ADC通道1 PA0引脚的模拟电压
      在OLED显示
      */
      uint8_t mode = 2;
      uint16_t ADValue1 = 0;
      uint16_t ADValue2 = 0;
      uint16_t ADValue3 = 0;
      uint16_t ADValue4 = 0;
      float Voltage1 = 0;
      float Voltage2 = 0;
      float Voltage3 = 0;
      float Voltage4 = 0;
      int main()
      {
      OLED_Init();//初始化OLED;
      Delay_Init();
      AD_Init();//初始化AD
      if(mode == 1)
      {
      OLED_ShowString(1,1,"Value1:0000");
      OLED_ShowString(2,1,"Value2:0000");
      OLED_ShowString(3,1,"Value3:0000");
      OLED_ShowString(4,1,"Value4:0000");
      }
      else if(mode == 2)
      {
      OLED_ShowString(1,1,"Voltage1:0.00V");
      OLED_ShowString(2,1,"Voltage2:0.00V");
      OLED_ShowString(3,1,"Voltage3:0.00V");
      OLED_ShowString(4,1,"Voltage4:0.00V");
      }
      while(1)
      {
      if(mode == 1)
      {
      //显示数值
      ADValue1 = AD_Getvalue(ADC_Channel_0);
      ADValue2 = AD_Getvalue(ADC_Channel_1);
      ADValue3 = AD_Getvalue(ADC_Channel_2);
      ADValue4 = AD_Getvalue(ADC_Channel_3);
      OLED_ShowNum(1,8,ADValue1,4);
      OLED_ShowNum(2,8,ADValue2,4);
      OLED_ShowNum(3,8,ADValue3,4);
      OLED_ShowNum(4 ,8,ADValue4,4);
      }
      else if(mode == 2)
      {
      ADValue1 = AD_Getvalue(ADC_Channel_0);
      ADValue2 = AD_Getvalue(ADC_Channel_1);
      ADValue3 = AD_Getvalue(ADC_Channel_2);
      ADValue4 = AD_Getvalue(ADC_Channel_3);
      //显示转换的电压
      Voltage1 = (float)ADValue1 / 4095 * 3.3;
      Voltage2 = (float)ADValue2 / 4095 * 3.3;
      Voltage3 = (float)ADValue3 / 4095 * 3.3;
      Voltage4 = (float)ADValue4 / 4095 * 3.3;
      OLED_ShowNum(1,10,Voltage1,1);
      OLED_ShowNum(1,12,(uint16_t)(Voltage1 * 100 ) % 100, 2);
      OLED_ShowNum(2,10,Voltage2,1);
      OLED_ShowNum(2,12,(uint16_t)(Voltage2 * 100 ) % 100, 2);
      OLED_ShowNum(3,10,Voltage3,1);
      OLED_ShowNum(3,12,(uint16_t)(Voltage3 * 100 ) % 100, 2);
      OLED_ShowNum(4,10,Voltage4,1);
      OLED_ShowNum(4,12,(uint16_t)(Voltage4 * 100 ) % 100, 2);
      }
      Delay_ms(10);
      }
      }

      数据转运 + DMA 讲解

      以这个例子来说。应该填
      • 起始位置为DataA 目标位置为 Data B
      • 数据宽度是8bit, 也就是1字节
      • 并且两个站点的地址都应该自增。
      • 传输次数为 7 不需要自动重装
      • 触发选择为 软件触发。 这是存储器到存储器的转运
      notion image

      ADC扫描模式 + DMA 讲解

      左边为ADC的7 个通道,每次触发 7个通道会 依次进行转换。 但是在存入的时候只能存入一个到ADC_DR这一个寄存器里。
      所以在每个单独的通道转换之后,就要用DMA移动到SRAM数组。并且自增存储器的 地址
      所以选择外设触发,外设地址选择有ADC_DR的通道
      因为ADC_DR和SRAM都是16bit的宽度, 所以在转运的过程中 选择的是16 bit 的半字输出
      通道有7条,所以 计数为7次。
      对于重装寄存器是否要工作,则要根据ADC的配置来选择 如果ADC是单次扫描,那么DMA的计数器可以不自动扫描 如果ADC是连续扫描,那么DMA的计数器可以使用自动重装。DMA和ADC同步工作,
      notion image

      DMA相关函数介绍

      • void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx); 恢复缺省配置
      • void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct); 初始化
      • void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct); 结构体初始化
      • void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState); 使能
      • void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState); 中断输出使能
      • void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); DMA设置当前数据寄存器 给 传输计数器写数据用的
      • uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx); 返回传输计数器的值
      • FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG); 获取标志位状态
      • void DMA_ClearFlag(uint32_t DMAy_FLAG); 清楚标志位
      • ITStatus DMA_GetITStatus(uint32_t DMAy_IT); 获取中断状态
      • void DMA_ClearITPendingBit(uint32_t DMAy_IT); 清除中断挂起位
       
       

      编写:DMA存储器与存储器之间的搬运

      notion image
      DMA存储器与存储器搬运。
      const 修饰的内容会被保存在Flash区域,这是只读的,并且上电不丢失
      其他的变量会被保存在地址为0x2000 0000 的静态随机存储器(SRAM)上
      数组与数组之间的搬运在DMA初始化时函数时,要接受两个数组的 源地址、目标地址、以及两个数组的 宽度 。
      DMA是AHB总线上的 外设。所以在启动时钟时使用的是AHB总线。
      存储器与存储器之间的搬运,一般用软件触发。 DMA在被软件出发后,会尽可能快的清零传输计数器,来完成此次搬运。 所以在使用软件触发的时候,不能开启自动重装器,否则DMA就会一直搬运。无法停下
      DMA在启动后会立刻开始工作。在干完这一轮之后 ,计数器清零。也就停下了。
      若想要再次触发,则要再次填充传输计数器里的值。 要先失能 DMA。否则无法填充。填充的函数是 DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_size);
      并且在填充完成后,要在函数内判断是否传输完成。完成后才能开启下一轮的转换。 这是通过判断DMA1_FLAG_TC1标志位来实现的(标志位置1为实现)。这个完成标志位不会自动清除,所以要手动在函数内清除,这是一个很好的习惯。 DMA_ClearFlag(DMA1_FLAG_TC1); 其他的还有这些标志位 “GL” 是 “Global”(全局)的缩写。 “TC” 是 “Transfer Complete”(转运完成)的缩写。 “HT” 是 “Half Transfer”(转运过半)的缩写。 “TE” 是 “Transfer Error”(转运错误)的缩写。

      MyDMA.c代码如下(添加My是为了不与自带的库的函数重复名字)

      main.c代码如下:

       

      编写:ADC单次连续扫描+DMA单次连续搬运

      notion image
      ADC单次连续扫描,是 没有开启连续转换的 要软件触发一次,ADC才能启动连续扫描一哦。 ADC在扫描指定的前几个通道后,就自动停下。
      但是对于规则组。所有(16个,对于C8T6只有7个)的通道是共用一个ADC_DR寄存器的。 也就是说,同时只能有一个通道在寄存器中等待读取。
      那么!我们就要在每个通道转换完成,然后输出到ADC_DR寄存器的同时,抓紧把已经上好的菜端走,否则就要被下一个菜挤走了。
      初始ADC,需要注意的是,配置通道为4条,并把它放入对应的规则组序列位置。 并且要把扫描通道的个数改为 4 。 并且要把扫描模式打开
      在初始化ADC后,就准备DMA的初始化了。 首先要设置源地址为ADC的DR寄存器。记得把他转为uint32_t的格式,否则无法被正确识别。并且宽度为半字。 16位。 并且是不自增的! 其次要设置目标地址为我们定义的四个元素大小的uint16_t的数组AD_Value[4]中。 。这事宽度为半字,这里要自增。每次搬运到一个元素后自动前往下一个位置。 DMA同样的, 要和ADC协同,ADC是4个通道。所以DMA要搬运四次。 并且,我们这里是ADC单次连续扫描,DMA单次连续搬运。所以DMA的重装寄存器是关闭的状态,不要让他重装。 并且,为了让DMA知道自己该搬运了。就要启动DMA的硬件触发。也就是M2M位是否选择软件触发,填DISABLE。 到这里,DMA的配置就基本完成了
      但是。DMA要如何知道自己该干活了呢? 这里就要开启DMA和AD之间的通道了。 也就是开启ADC到DMA的输出。ADC_DMACmd(ADC1, ENABLE); 这样,ADC1在每次某个通道完成之后,就会告诉DMA,你快来搬!!!!如下是ADC的框图,有一个DMA请求
      notion image
      在经过四次循环之后,DMA和ADC就同时结束了。
      如果我们要再次进行搬运,就要先关闭DMA.然后填入新的数字。再使能DMA 然后软件触发一次ADC1 。让他再次扫描四个通道。并且完成转换,同时DMA也能在ADC的告知下完成搬运。

      AD.c代码如下

      uint16_t AD_Value[4];//定义用于存放AD转换结果的全局数组
      void AD_Init(void)
      {
      //开启GPIOA时钟、ADC1外设时钟、DMA外设的时钟
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
      RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
      //配置ADC预分频器
      RCC_ADCCLKConfig(RCC_PCLK2_Div6);//12M
      //配置PA0
      GPIO_InitTypeDef GPIO_InitStructure;
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入模式
      GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 ;
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_Init(GPIOA,&GPIO_InitStructure);
      //配置规则通道
      ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);//规则组序列1的位置,配置为通道0
      ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5);//规则组序列2的位置,配置为通道1
      ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_55Cycles5);//规则组序列3的位置,配置为通道2
      ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_55Cycles5);//规则组序列4的位置,配置为通道3
      //初始化ADC
      ADC_InitTypeDef ADC_InitStructure;
      ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //是否连续转换:否
      ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //右对齐
      ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发:无
      ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式:独立ADC模式
      ADC_InitStructure.ADC_NbrOfChannel = 4; //用4个通道
      ADC_InitStructure.ADC_ScanConvMode = ENABLE; //是否扫描模式:是
      ADC_Init(ADC1,&ADC_InitStructure);
      /ADC使能之前 开始配置DMA/
      //初始化DMA参数
      DMA_InitTypeDef DMA_InitStructure;
      DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//源基地址
      DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//半字,16位
      DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//源地址是否自增:否
      DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;//目标基地址
      DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//目标宽度 字节
      DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//目的地否自增:是
      DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//传输方向 1-存储器到外设。2-外设到 存储器
      DMA_InitStructure.DMA_BufferSize = 4;//传输计数器 :4次
      DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//指定重装器:单次
      DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //是否使用软件触发 否:
      DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//选择优先级
      DMA_Init(DMA1_Channel1,&DMA_InitStructure);//硬件触发,ADC1 的硬件触发在通道1 所以不能变
      //使能DMA
      DMA_Cmd(DMA1_Channel1, DISABLE);
      //开启ADC到DMA的输出
      ADC_DMACmd(ADC1, ENABLE);
      //开启ADC1
      ADC_Cmd(ADC1, ENABLE);
      //校准
      ADC_ResetCalibration(ADC1); //复位校准
      while(ADC_GetResetCalibrationStatus(ADC1) == SET) //等待复位校准完成
      ADC_StartCalibration(ADC1); //开始校准
      while(ADC_GetCalibrationStatus(ADC1) == SET); //等待校准完成
      }
      //单次DMA,单次ADC模式
      void AD_Getvalue(void)
      {
      //因为是ADC是单次,所以DMA没有开启重装循环模式
      DMA_Cmd(DMA1_Channel1,DISABLE);//失能
      DMA_SetCurrDataCounter(DMA1_Channel1,4);//装填四次
      DMA_Cmd(DMA1_Channel1,ENABLE);//使能
      //软件触发ADC转换
      ADC_SoftwareStartConvCmd(ADC1,ENABLE);
      //其他的等待转换完成,不需要了
      //等待DMA搬运完成标志位,手动清除标志位
      while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//T.T 注意加分号
      DMA_ClearFlag(DMA1_FLAG_TC1);
      }
      #ifndef __AD_H
      #define __AD_H
      //对外声明数组
      extern uint16_t AD_Value[4];
      //初始化
      void AD_Init(void);
      //开启ADC一次,然后搬运出去一次
      void AD_Getvalue(void);
      #endif
      #include "stm32f10x.h" // Device header
      #include "oled.h"
      #include "Delay.h"
      #include "key.h"
      #include "AD.h"
      /*
      DMA单次 连续扫描,
      DMA单次 连续搬运
      显示4个通道的 转换值
      */
      int main()
      {
      OLED_Init();//初始化OLED;
      Delay_Init();//初始化延时
      AD_Init();
      while(1)
      {
      //显示通道
      OLED_ShowString(1,1,"AD1:");
      OLED_ShowString(2,1,"AD2:");
      OLED_ShowString(3,1,"AD3:");
      OLED_ShowString(4,1,"AD4:");
      //开启ADC 转换四条通道 1 次
      //然后DMA搬运到AD_Value数组中
      AD_Getvalue();
      //显示搬运后的数据
      OLED_ShowNum(1, 5, AD_Value[0], 4);
      OLED_ShowNum(2, 5, AD_Value[1], 4);
      OLED_ShowNum(3, 5, AD_Value[2], 4);
      OLED_ShowNum(4, 5, AD_Value[3], 4);
      Delay_ms(100);
      }
      }
      #include "stm32f10x.h" // Device header
      uint16_t AD_Value[4];//定义用于存放AD转换结果的全局数组
      void AD_Init(void)
      {
      //开启GPIOA时钟、ADC1外设时钟、DMA外设的时钟
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
      RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
      //配置ADC预分频器
      RCC_ADCCLKConfig(RCC_PCLK2_Div6);//12M
      //配置PA0
      GPIO_InitTypeDef GPIO_InitStructure;
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入模式
      GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 ;
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_Init(GPIOA,&GPIO_InitStructure);
      //配置规则通道
      ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);//规则组序列1的位置,配置为通道0
      ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5);//规则组序列2的位置,配置为通道1
      ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_55Cycles5);//规则组序列3的位置,配置为通道2
      ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_55Cycles5);//规则组序列4的位置,配置为通道3
      //初始化ADC
      ADC_InitTypeDef ADC_InitStructure;
      ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //是否连续转换:是
      ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //右对齐
      ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发:无
      ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式:独立ADC模式
      ADC_InitStructure.ADC_NbrOfChannel = 4; //用4个通道
      ADC_InitStructure.ADC_ScanConvMode = ENABLE; //是否扫描模式:是
      ADC_Init(ADC1,&ADC_InitStructure);
      /ADC使能之前 开始配置DMA/
      //初始化DMA参数
      DMA_InitTypeDef DMA_InitStructure;
      DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//源基地址
      DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//半字,16位
      DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//源地址是否自增:否
      DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;//目标基地址
      DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//目标宽度 字节
      DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//目的地否自增:是
      DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//传输方向 1-存储器到外设。2-外设到 存储器
      DMA_InitStructure.DMA_BufferSize = 4;//传输计数器 :4次
      DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//指定重装器:循环
      DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //是否使用软件触发 否:
      DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//选择优先级
      DMA_Init(DMA1_Channel1,&DMA_InitStructure);//硬件触发,ADC1 的硬件触发在通道1 所以不能变
      //使能DMA
      DMA_Cmd(DMA1_Channel1, ENABLE);
      //开启ADC到DMA的输出
      ADC_DMACmd(ADC1, ENABLE);
      //开启ADC1
      ADC_Cmd(ADC1, ENABLE);
      //校准
      ADC_ResetCalibration(ADC1); //复位校准
      while(ADC_GetResetCalibrationStatus(ADC1) == SET) //等待复位校准完成
      ADC_StartCalibration(ADC1); //开始校准
      while(ADC_GetCalibrationStatus(ADC1) == SET); //等待校准完成
      //软件触发ADC
      ADC_SoftwareStartConvCmd(ADC1,ENABLE);
      }
      #ifndef __AD_H
      #define __AD_H
      extern uint16_t AD_Value[4];
      //ʼ
      void AD_Init(void);
      #endif
      #include "stm32f10x.h" // Device header
      #include "oled.h"
      #include "Delay.h"
      #include "key.h"
      #include "AD.h"
      /*
      重复ADC扫描, DMA连续搬运
      显示ADC转换值
      显示ADC转换值转换的电压
      */
      int main()
      {
      OLED_Init();//初始化OLED;
      Delay_Init();//初始化延时
      AD_Init();//开启AD连续转换,DMA连续搬运
      //显示通道
      OLED_ShowString(1,1,"AD1:");
      OLED_ShowString(2,1,"AD2:");
      OLED_ShowString(3,1,"AD3:");
      OLED_ShowString(4,1,"AD4:");
      OLED_ShowString(1,12,"0.00V");
      OLED_ShowString(2,12,"0.00V");
      OLED_ShowString(3,12,"0.00V");
      OLED_ShowString(4,12,"0.00V");
      //求电压
      float AD_Voltage1 = 0;
      float AD_Voltage2 = 0;
      float AD_Voltage3 = 0;
      float AD_Voltage4 = 0;
      while(1)
      {
      //显示搬运后的数据
      OLED_ShowNum(1, 5, AD_Value[0], 4);
      OLED_ShowNum(2, 5, AD_Value[1], 4);
      OLED_ShowNum(3, 5, AD_Value[2], 4);
      OLED_ShowNum(4, 5, AD_Value[3], 4);
      //显示转换的电压
      AD_Voltage1 = (float) AD_Value[0] / 4095 * 3.3;
      AD_Voltage2 = (float) AD_Value[1] / 4095 * 3.3;
      AD_Voltage3 = (float) AD_Value[2] / 4095 * 3.3;
      AD_Voltage4 = (float) AD_Value[3] / 4095 * 3.3;
      OLED_ShowNum(1,12,AD_Voltage1,1);
      OLED_ShowNum(1,14,(uint16_t)(AD_Voltage1 * 100 ) % 100, 2);
      OLED_ShowNum(2,12,AD_Voltage2,1);
      OLED_ShowNum(2,14,(uint16_t)(AD_Voltage2 * 100 ) % 100, 2);
      OLED_ShowNum(3,12,AD_Voltage3,1);
      OLED_ShowNum(3,14,(uint16_t)(AD_Voltage3 * 100 ) % 100, 2);
      OLED_ShowNum(4,12,AD_Voltage4,1);
      OLED_ShowNum(4,14,(uint16_t)(AD_Voltage4 * 100 ) % 100, 2);
      Delay_ms(20);
      }
      }
      1. 发送读写位:确认接下来是写入还是读出(0表示写入,1表示读出)。
      1. 接收应答位
      1. 主机发送从机地址和读写位后,释放SDA。
      1. 从机响应后会拉低SDA,产生应答位。
      1. 应答位产生后,从机释放SDA的控制权。
      发送数据
      1. 主机再次发送第二个字节,通常为寄存器地址或控制字。
      1. 接着发送第三个字节,表示要写入寄存器地址中的数据值。
      产生停止条件
      1. 主机拉低SDA,为后续的上升沿做准备。
      1. 然后依次释放SCL和SDA,产生SCL高电平期间的SDA上升沿,表示停止条件。
      示例
      对于从机地址为1101000的设备,在其内部0x19地址的寄存器中写入0xAA这个数据,数据帧的过程如下:
      1. 起始条件:SCL高电平期间,SDA从高电平拉低。
      1. 发送从机地址和读写位(1101000+0)。
      1. 接收从机应答位。
      1. 发送寄存器地址(0x19)。
      1. 接收从机应答位。
      1. 发送数据值(0xAA)。
      1. 接收从机应答位。
      1. 停止条件:SCL高电平期间,SDA从低电平释放至高电平。
      通过这些步骤,主机可以可靠地在从机的指定寄存器地址中写入数据

      当前地址读

      对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
      notion image
      产生起始条件
      • 主机在SCL高电平期间拉低SDA,产生起始条件。
      寻址和读写操作
      1. 主机首先发送一个字节,用于从机的寻址和设置读写标志位。
      1. 在本次通信中,主机发送的目标地址是1101000,读写标志位为1,表示主机接下来要读取数据。
      接收从机应答位
      1. 主机发送完寻址字节后,接收从机的应答位。
      1. 从机收到第一个字节后,拉低SDA,表示应答,并将SDA的控制权交回主机。
      接收数据
      1. 主机调用接收数据的时序,准备接收从机发送的数据。
      1. 从机得到允许后,在SCL低电平期间将数据写入SDA。
      1. 主机在SCL高电平期间读取SDA上的数据。
      1. 主机在SCL高电平期间依次读取8位数据,从而接收到从机发送的一个字节数据(例如:0000 1111,即0x0F)。
      指针自动递增
      1. I2C通信中没有指定寄存器地址的环节,默认情况下,从机中的所有寄存器被分配到一个线性区域中。
      1. 从机内部有一个指针变量,指向某个寄存器。上电时,这个指针通常默认指向0地址。
      1. 每次 写入 或 读出 一个字节后,这个指针会自动递增,指向下一个寄存器位置。
      1. 主机读取到的数据是当前指针指向的寄存器的值。
      所以一般这个时序用的不是很多

      指定地址读

      对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)
      notion image
      对弈指定地址读,其实是先告诉设备我要写,当寄存器指向这个地址之后,我又不写了。我要读。
      写入操作
      1. 指定从机地址和读写标志位: 从机地址为1101000,读写标志位为0,表示要进行写操作。
      1. 从机应答: 从机应答后,主机发送第二个字节(0001 1001),用于指定寄存器地址。 这个数据写入到从机的地址指针中,从机的寄存器指针指向0x19位置。
      重复起始 开始读操作
      1. 产生一个新的起始条件: 主机再次产生一个起始条件。
      1. 重新寻址并指定读写标志位: 主机重新发送从机地址(1101000)和读写标志位(此时读写标志位为1,表示读操作)。
      1. 主机接收数据: 从机应答后,主机开始接收一个字节的数据。 这个字节的数据是从机0x19地址下的内容。
      读写操作也不一定只能一次一个字节。 在主机读或写的时候,只要主机应答,从机就不会停。直到主机给非应答,从机才会停止,然后把SDA主导权交给主机。

      21. STM32通过I2C软件读写MPU6050

      MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景
      3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度
      3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度

      1.运动学概念

      • 欧拉角:
        • 欧拉角是用来描述三维空间中刚体旋转的三个角度:俯仰角(Pitch)、滚转角(Roll)和偏航角(Yaw)。
        • 俯仰角(Pitch):飞机机头上下倾斜的角度。
        • 滚转角(Roll):飞机左右倾斜的角度。
        • 偏航角(Yaw):飞机左右转向的角度。
        • 任何一种传感器,都不能获得精确且稳定的欧拉角。要想获得,就需要进行数据融合,综合几种传感器的数据,取长补短。通常有互补滤波、卡尔曼滤波等

      2.MPU6050工作原理

      1. 加速度计: 加速度计的内部可以想象为这个样子。是一个滑变电阻+两个弹簧
        1. notion image
        2. 测量在X、Y、Z轴方向上测量加速度。通过检测重力加速度,可以推断出设备的倾斜角度。例如,在一个静止状态下,水平放置。 X和Y就是0。Z轴会受到一个向下重力所引的加速度;再例如,现在在自由落体。则X,Y,Z都为0。
        3. 在使用加速度计求角度的时候,只能是静态时才行。否则不行, 例如一个水平的小车,水平加速。如图,小人(芯片)收到的是向后和向下的力。 这时用三角函数求小车姿态,得到的就是小车停在一个斜坡上受到的力。
          1. 总结就是:加速度具有静态稳定性,不具有动态稳定性
          notion image
          notion image
      1. 陀螺仪:
        1. notion image
        2. 测量设备在X、Y、Z轴上的角速度。陀螺仪可以用于检测设备的旋转运动,比如快速转动或者缓慢旋转等。
        3. 想要通过角速度的到角度,只需要对角速度进行积分即可。但是陀螺仪测角速度也有局限性。 当物体静止时,角速度的值会因为噪声无法完全归零。经过积分的不断累积,角速度的值就会产生缓慢的漂移。
        4. 总结就是:陀螺仪具有动态稳定性,但不具备静态稳定性。
      所以,在使用时,进行互补滤波,就能得到静态和动态都稳定的都稳定的姿态角了

      3.MPU6050参数

      ADC和数据存储
      • 16位ADC:MPU6050集成了16位的ADC,用于将模拟信号转换为数字信号,量化范围为-32768到32767。
      • 量化过程:ADC将模拟信号转换为数字信号,并以两个字节进行存储。
      加速度计满量程选择:±2、±4、±8、±16(g)
      如果测量的物体的运动非常剧烈,就可以选择量程大一些。分辨率会更加粗糙
      如果测量的物体的运动比较平缓,就可以选择量程小一些。分辨率会更加细腻
      仪满量程选择: ±250、±500、±1000、±2000(°/sec)
      陀螺仪的选择也是同上,满量程越大,测量范围越广、满量程越小,测量分辨率就越高
      可配置的数字低通滤波器
      • 允许用户根据应用需求通报配置寄存器,来配置滤波器,滤除噪声和干扰。
      可配置的时钟源、采样分频
      • 支持多种时钟源,包括内部振荡器、外部参考时钟。经过采样分频之后为AD和其他内部电路提供时钟。控制了分频系数,就控制了AD转换的快慢了。
      I2C从机地址:
      当AD0引脚接低电平(AD0=0):地址为1101 000 (0x68)。 当AD0引脚接高电平(AD0=1): 地址为1101 001 (0x69)。 具体地址配置决定了在I2C总线上的唯一性,避免地址冲突。
       

      4.MPU6050量程选择

      满量程选择
      • 剧烈运动:选择较大的满量程,确保测量范围足够大。
      • 平缓运动:选择较小的满量程,提升测量分辨率。
      加速度计满量程示例:
      • ±16g:
        • 读取的ADC值为最大值32768时,对应实际加速度为16g。
        • ADC值为32768的一半(16384)时,对应加速度为8g。
      • ±2g:
        • 读取的ADC值为最大值32768时,对应实际加速度为2g。
        • ADC值为32768的一半(16384)时,对应加速度为1g。
      测量分辨率:
      • 满量程越小,测量分辨率越高,测量越精细。
      • 满量程越大,测量范围越广。
      • ADC值与加速度值呈线性关系,可以通过乘一个系数从ADC值计算出实际加速度。

      5.MPU6050从机地址配置

      二进制地址转换为十六进制:
      • 以从机地址110 1000为例。
        • 把7位二进制数1101000转换为十六进制,即分割低4位和高3位:0110 1000,转换后为0x68。
      I2C时序中的地址格式:
      • 在I2C通信时,需要发送7位从机地址110 1000加上1位读写位。共八位
      • 使用0x68作为从机地址,需要将0x68左移1位,再加上读写位(0或1)。
      • 转换步骤:
        • 将0x68左移1位:1101 0000(即0xD0)。
        • 再与读写位(0或1)进行或操作:(0x68 << 1) | 0 或 (0x68 << 1) | 1
        • 再转换成二进制其实就是0xD0或0xD1
        • 这样其实就是融入了读写位的从机地址
      实际应用:
      写操作时 直接发送一个字节。从机地址为0xD0。 读操作时 直接发送一个字节。从机地址为0xD1。

      6.硬件电路

      notion image
      引脚
      功能
      VCC、GND
      电源3.3v
      SCL、SDA
      I2C通信引脚
      XCL、XDA
      主机I2C通信引脚
      AD0
      从机地址最低位
      INT
      中断信号输出
       

      7.MPU6050框图

      notion image
      左边一列是自测响应。用来验证芯片是否正常
      左下角的Charge Pump 是电荷泵,也叫做充电泵。是外接电容的。是用来升压的
      有各种传感器。甚至还有个温度传感器。
      这些东西测出来的值经过ADC进行模数转换。
      这些数据统一放到数据寄存器中。
      我们读取就OK了。其他的都是全自动执行的
      右边部分
      • Interrupt Status Register 中断控制寄存器,可以控制内部的事件到中断引脚的输出
      • FIFO 先入先出寄存器,可以对数据流进行缓存
      • Config Registors 配置寄存器,对内部的各个电路进行配置
      • Sensor Registers 传感器寄存器(数据寄存器),存储了各个传感器的数据
      • Factory Calibratilor 工厂校准,内部的传感器都进行了校准。
      • Digital Motion Processor(DMP) 数字运动处理器,芯片内部自带的姿态解算的硬件算法。需要配合官方的DMP库,进行姿态解算。
      • FSYNC 帧同步,
      • 剩下的这些这些(上边仨)就是通信接口的部分了。 上边是MPU6050与STM32进行通信的接口。 下边是MPU6050作为主机的扩展接口
        • 其中SerialInterface Bypass Mux 是用来进行旁路控制的, 他可以控制 下边的接口是直接接到STM32的总线上(黄线所连接),作为STM32的直接大哥 或者是MPU6050自己去控制这些外设(蓝色线所连接)。使STM32是MPU6050的大哥。MPU6050是这些外设的大哥
      notion image

      8.MPU60X0寄存器映射(常用)

      寄存器映射框图各部分作用介绍

      每个寄存器都是八位的,他们有唯一的地址。
      notion image

      常用寄存器简介

      按照标黄部分从上往下介绍
      第一部分
      notion image
      SMPLRT_DIV 采样频率分频器
      • 陀螺仪刷新率(采样频率) = 时钟频率(内部晶振、陀螺仪晶振输出和或者外部时钟引脚方波) / (1+分频值)
      • 使用了滤波器,陀螺仪时钟就是1KHZ。 不使用滤波器,陀螺仪时钟是8KHZ
      CONFIG 配置寄存器
      • (外部同步这里不用)
      • 低通滤波器可以让输出数据更加平滑,参数越大,输出都懂就越小 配置的参数可以是如下的0-7 (最后一位是保留位没用到)
        • notion image
          notion image
      GYRO_CONFIG 陀螺仪配置寄存器
      notion image
      • 高三位是XYZ轴的自测使能位 自测响应 = 自测使能时数据 - 自测失能时数据 得到的值在范围内就算通过自测。
        • notion image
      • 中间两位是满量程选择位 量程选择,则需要根据实际情况来。量程越大,量程越广。量程越小,分辨率越高
      notion image
      ACCEL_CONFIG 加速度计配置寄存器
      notion image
      • 同上,高三位为自测使能位
      • 中间两位是满量程选择位
        • notion image
      • 不过后面还多了三位,这三位是配置 高通滤波器的 在这里暂时用不到。(是内置小功能需要用到的。比如自由落体检测,运动检测、零运动检测等)对我们的输入输出没有影响。
      第二部分
      notion image
      这一大块,是数据寄存器
      其中_H 表示高八位 _L表示第八位
      加速度计XYZ轴
      ACCEL_XOUT_H
      ACCEL_XOUT_L
      ACCEL_YOUT_H
      ACCEL_YOUT_L
      ACCEL_ZOUT_H
      ACCEL_ZOUT_L
      • 这是加速度计的数据寄存器 在读取数据时直接读取数据寄存器就OK了。 是一个16位的有符号数 以二进制补码的方式存储
      notion image

      温度传感器
      TEMP_OUT_H
      TEMP_OUT_L
      • 同上
      notion image

      陀螺仪XYZ轴
      GYRO_XOUT_H
      GYRO_XOUT_L
      GYRO_YOUT_H
      GYRO_YOUT_L
      GYRO_ZOUT_H
      GYRO_ZOUT_L
      • 同上
      notion image


      第三部分
      notion image
      PWR_MGMT_1 电源管理寄存器 1
      notion image
      • 第一位,设备复位 :写1 时 所有设备恢复默认值
      • 第二位,睡眠模式:写1 时 芯片睡眠,进入低功耗。芯片不工作。
      • 第三位,循环模式:写1时 设备进入低功耗,过一段时间启动一次。 并且唤醒的频率由电源管理寄存器2决定。
      • 第四位,温度传感器失能:写1时 禁用内部温度传感器
      • 最后三位,用来选择系统时钟来源
      notion image

      PWR_MGMT_2 电源管理寄存器 2
      • 这两位决定了唤醒的频率
      • 后面6位可以分别控制6个轴进入待机模式 可以省电用。如果只需要部分数据的话
      notion image

      WHO_AM_I 我是谁(器件ID号)
      • 这个寄存器是只读的。
      • 中间六位固定为 110100 所以读出这个寄存器的值就为0x68
      • 实际上ID号就是这个芯片的I2C地址。
      • 并且通过外部引脚AD0 修改设备号最低位的时候 是不反映在寄存器中的。寄存器并不会根据AD0的引脚的电平来变化。
      notion image

      第四部分
      芯片上电之后,默认的是所有寄存器为0x00。
      除了两个:
      • Register 107: 0x40 也就是电源管理寄存器1。0x40即0100 0000 对应寄存器也就是睡眠模式为1 也就是上电默认随眠
      • Register 117: 0x68 这个就是ID号的寄存器了。

      9.软件I2C和硬件I2C的区别

      • 硬件电路件I2C的波形更加规整,时钟周期和占空比非常一致。
        • 每个时钟周期后都有严格的延时,保证每个周期的时间相同。
          硬件电路需要专门的某些GPIO口才能实现
      • 软件I2C的波形较为不规整,每个时钟周期和空闲时间都不一致。
        • 软件I2C时的引脚操作会有一定的延时,因此各个时钟周期的间隔和占空比都不均匀。
          但是软件实现I2C更加的方便,随便那个GPIO口都可以实现。
      这里我们使用的是软件I2C

      10.软件I2C读写MPU6050编写步骤

      10.1程序整体构架

      首先建立I2C通信层的.c和.h模块,再建立MPU6050.c, 最后是main.c
      • Main.c
          1. 调用MPU6050初始化函数。
          1. 循环读取数据并进行显示。
      • MPU6050.c
          1. 基于I2C通信协议,实现指定地址读,指定地址写
          1. 实现写寄存器来配置芯片,读寄存器得到传感器数据。
      • I2C.c
          1. 初始化GPIO。
          1. 编写基本的I2C操作函数,包括起始条件、终止条件、发送/接收一个字节、发送/接收应答等。

      10.2需要注意的点

      1. 开漏输出模式
          • 开漏输出模式下GPIO有强下拉,弱上拉的特性,所以需要借助外部上拉电阻来输出高电平。
          • 当Out输出为1时,晶体管截止。此时可以直接读取输入数据寄存器,就可以得到开漏模式下输出的值。
          notion image
      1. 写起始条件时
        1. 在SCL高电平期间,SDA由高电平变为低电平,产生起始条件(Start Condition)。这表示一次I2C通信的开始。 (先拉低SDA再拉低SDA)
          notion image
          • 起始条件不仅要能在SDA和SCL都为高电平时按照先SDA再SCL顺序拉低。他们谁先拉高都是无所谓的。
          • 也要兼容在重复起始条件。在重复起始条件时。 刚刚完成了应答的操作, SCL将要到拉低为低电平,而SDA的电平不敢确定。所以为了保险起见,需要趁SCL为低电平时 抓紧上拉SDA,再 上拉SCL才能保证不触发终止。举个栗子如下:
            • notion image
          所以综上来说,在写起始条件时。顺序应该为SDA 1、SCL 1、SDA 0、 SCL 0
      1. 写终止条件时
        1. notion image
          • 同上, 在上次应答完成之后。SCL下一个周期一定为下降沿。而SDA的状态不确定。 所以为了确保在终止条件时,SDA一定能在SCL为高时产生上升沿, 需要先趁着SCL为低电平把SDA拉低,再把SCL拉高。 再拉高SDA。 这样就能产生在SCl为高电平时的上升沿了
      1. 写发送字节时
          • 发送字节是高位先行
          • 起始条件之后,SCL和SDA一定为低电平
          • 除了起始条件和终止条件,其他时候都是SDA再SCL低电平时变化
          • 取出一个字节(8bit)的任一位。可以使用 & 操作,比如 1011 0000 & 0x40 = 1011 0000 & 0100 0000 = 0000 0000(0) 1111 0000 & 0x40 = 1011 0000 & 0100 0000 = 0100 0000(非0即1) 所以发送的电平一定是0 或 1
          • 循环8次 发送一个字节
      1. 写接收字节时
          • 首先主机要释放SDA的控制权。也就是输出1,使mos管截止
          • 从机在SCL低电平期间 控制SDA 拉高或拉低
          • 主机控制SCL回到高电平。主机读取SDA
          • 拉低SCL,从机控制SDA。
          • 循环八次..接收一个字节

      10.3软件I2C编写步骤

      1. 将打算用作SCL和SDA的GPIO引脚都初始化为开漏输出模式
      1. 将SCL和SDA都置高电平
      1. 写 写SCL函数(可选择加延时);
      1. 写 写SDA函数(可选择加延时);
      1. 写 读SDA函数(可选择加延时);
      1. 写起始条件函数: 拉高SDA、拉高SCL、拉低SDA、拉低SCL
      1. 写终止条件函数: 拉低SDA、拉高SCL、拉高SDA、
      1. 发送的一个字节函数 发送一个字节的最高位; 拉高SCL(从机读取) 拉低SCL(准备下次写入) 发送一个字节的次高位; 拉高SCL(从机读取) 拉低SCL(准备下次写入)
        1. …循环八次。可使用for循环
      1. 接收一个字节的函数 主机释放SDA(SDA输出1,SDA对 地 相当于断开)(这时候SCL为低,从机已经在SDA存放了数据) 主机拉高SCL(准备读取) 主机输入并存入数据(可以用或来存入) 主机拉低SCL(从机SDA存放数据) 主机拉高SCL(准备读取) 主机输入并存入数据(可以用或来存入) …..循环 返回循环后接收到的一个字节
      1. 发送应答的函数 (发送一个字节的简化版) 发送应答 SCL高电平(从机读取应答) SCL低电平(进入下一个时序单元)
      1. 接收应答的函数 主机释放SDA(SDA输出1,SDA对 地 相当于断开)(这时候SCL为低,从机已经在SDA存放了数据) 主机拉高SCL(准备读取) 主机输入并存入数据(可以用或来存入) 主机拉低SCL(从机SDA存放数据) 返回读出的值

      10.4软件I2C读写MPU6050编写步骤

      有了软件I2C的初始化,写MPU就很方便了。
      只需要调用已经写好的起始、结束、发送字节、接受字节的时序就可以。
      1. 初始化MPU6050 包括调用I2C初始化函数。解除睡眠模式、设置采样频率、配置陀螺仪和加速度计等寄存器
      1. 编写“指定地址一个字节“的函数
          • 起始
          • 发送字节(设备地址+0)(我要写入)
          • 接收从机应答
          • 发送字节(指定要写入的地址)
          • 接收从机应答
          • 发送字节(发送要写入的数据)
          • 接收从机应答
          • 停止
          其中接受从机应答可以去判断,并输出对应的报错信息。这里没有添加。
      1. 编写“指定地址一个字节“的函数
          • 起始
          • 发送字节(设备地址+0)(我要写入)
          • 接收从机应答
          • 发送字节(指定要写入的地址)
          • 接收应答位
          • 重复起始
          • 发送字节(设备地址+1)我要读取
          • 接收应答位
          • 接收并保存从机发送的字节
          • 主机不应答(如果应答,从机会继续发送)
          • 停止
          • 返回读取到的值
      1. 编写“读取六轴传感器各个寄存器的值”
        1. 可以直接传地址,也可以用结构体或者数组。方法很多。

      11.代码编写:

      程序文件简要说明:

      • MyI2C.c:初始化I2C所需要的引脚、编写时序的基本组成(起始、终止、发送/接收一个字节、应答、发送应答)
      • MyI2C.h:函数声明
      • MPU6050.c:初始化MPU6050的寄存器。在MyI2C的基础上,编写对指定地址发送指定值、读取指定地址的函数
      • MPU6050.h:函数声明、数据结构体声明
      • MPU6050_Reg.h:寄存器的地址与名称声明
      • main.c:测试I2C通信结果。

      MyI2C.c

      MyI2C.h

      MPU6050.c

      //MPU6050_ACCEL_ZOUT_H 0x3F
      //MPU6050_ACCEL_ZOUT_L 0x40
      //MPU6050_TEMP_OUT_H 0x41
      //MPU6050_TEMP_OUT_L 0x42
      //MPU6050_GYRO_XOUT_H 0x43
      //MPU6050_GYRO_XOUT_L 0x44
      //MPU6050_GYRO_YOUT_H 0x45
      //MPU6050_GYRO_YOUT_L 0x46
      //MPU6050_GYRO_ZOUT_H 0x47
      //MPU6050_GYRO_ZOUT_L 0x48
      #ifndef __MPU6050_H
      #define __MPU6050_H
      /
      • 函 数:初始化MPU6050
      • 参 数:无
      • 返 回 值:无
      */
      void MPU6050_Init(void);
      /
      • 函 数:指定地址写一个字节
      • 参 数:RegAddress 指定的寄存器地址
        • Data 指定写入的数据
      • 返 回 值:无
      */
      void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
      /
      • 函 数:指定地址读一个字节
      • 参 数:RegAddress 指定要读的寄存器地址
      • 返 回 值:无
      */
      uint8_t MPU6050_ReadeReg(uint8_t RegAddress);
      //传感器数据
      typedef struct Data
      {
      int16_t AccX;
      int16_t AccY;
      int16_t AccZ;
      int16_t Temp;
      int16_t GyroX;
      int16_t GyroY;
      int16_t GyroZ;
      }SensorData;
      extern SensorData MPU6050_Data;
      /
      • 函 数:得到六轴传感器中的数据
      • 参 数:Str MPU6050_Data的地址
      • 返 回 值:无
      */
      void MPU6050_GetData(SensorData *Str);
      #endif
      #ifndef __MPU6050_REG_H
      #define __MPU6050_REG_H
      #define MPU6050_SMPLRT_DIV 0x19 //采样率分频
      #define MPU6050_CONFIG 0x1A //配置寄存器
      #define MPU6050_GYRO_CONFIG 0x1B //陀螺仪配置寄存器
      #define MPU6050_ACCEL_CONFIG 0x1C //加速度计配置寄存器
      #define MPU6050_ACCEL_XOUT_H 0x3B
      #define MPU6050_ACCEL_XOUT_L 0x3C
      #define MPU6050_ACCEL_YOUT_H 0x3D
      #define MPU6050_ACCEL_YOUT_L 0x3E
      #define MPU6050_ACCEL_ZOUT_H 0x3F
      #define MPU6050_ACCEL_ZOUT_L 0x40
      #define MPU6050_TEMP_OUT_H 0x41
      #define MPU6050_TEMP_OUT_L 0x42
      #define MPU6050_GYRO_XOUT_H 0x43
      #define MPU6050_GYRO_XOUT_L 0x44
      #define MPU6050_GYRO_YOUT_H 0x45
      #define MPU6050_GYRO_YOUT_L 0x46
      #define MPU6050_GYRO_ZOUT_H 0x47
      #define MPU6050_GYRO_ZOUT_L 0x48
      #define MPU6050_PWR_MGMT_1 0x6B //电源管理1
      #define MPU6050_PWR_MGMT_2 0x6C //电源管理2
      #define MPU6050_WHO_AM_I 0x75 //设备号
      #endif
      #include "stm32f10x.h" // Device header
      #include "oled.h"
      #include "Delay.h"
      #include "key.h"
      #include "MPU6050.h"
      /*
      指定6050设备,读取117位寄存器(设备号)
      在OLED显示出来
      */
      int main()
      {
      OLED_Init();//初始化OLED;
      MPU6050_Init();//初始化MPU6050
      while(1)
      {
      MPU6050_GetData(&MPU6050_Data);
      //显示加速度
      OLED_ShowSignedNum(1,1,(int16_t)MPU6050_Data.AccX,5);
      OLED_ShowSignedNum(2,1,(int16_t)MPU6050_Data.AccY,5);
      OLED_ShowSignedNum(3,1,(int16_t)MPU6050_Data.AccZ,5);
      //显示陀螺仪
      OLED_ShowSignedNum(1,8,MPU6050_Data.GyroX,5);
      OLED_ShowSignedNum(2,8,MPU6050_Data.GyroY,5);
      OLED_ShowSignedNum(3,8,MPU6050_Data.GyroZ,5);
      //显示温度
      OLED_ShowSignedNum(4,4,MPU6050_Data.Temp,5);
      }
      }
      /测试读写寄存器/
      //#include "stm32f10x.h" // Device header
      //#include "oled.h"
      //#include "Delay.h"
      //#include "key.h"
      //#include "MPU6050.h"
      ///*
      // 指定6050设备,读取117位寄存器(设备号)
      // 写入寄存器,并读取出来。检查是否正确写入
      // 在OLED显示出来
      //*/
      //int main()
      //{
      // OLED_Init();//初始化OLED;
      // MPU6050_Init();//初始化MPU6050
      // /读取0x75寄存器设备号并显示/
      // uint8_t ID = MPU6050_ReadeReg(0x75);
      // OLED_ShowHexNum(1, 1, ID, 2);
      // /解除芯片睡眠模式,并测试写入/
      // MPU6050_WriteReg(0x6B,0x00);//解除
      // MPU6050_WriteReg(0x19,0xAA);//写入
      // uint8_t Data = MPU6050_ReadeReg(0x19);//读出
      // OLED_ShowHexNum(2, 1, Data, 2);//显示
      // MPU6050_WriteReg(0x19,0x66);//写入
      // Data = MPU6050_ReadeReg(0x19);//读出
      // OLED_ShowHexNum(3, 1, Data, 2);//显示
      //}
      /测试I2C时序/
      //#include "stm32f10x.h" // Device header
      //#include "oled.h"
      //#include "Delay.h"
      //#include "key.h"
      //#include "MyI2C.h"
      ///*
      // 查询所有7位I2C地址
      // 找到总线上挂载的所有设备
      // 在OLED显示出来
      //*/
      //int main()
      //{
      // OLED_Init();//初始化OLED;
      // MyI2C_Init();//初始化I2C
      //
      // /*
      // 从机地址为 1101 000
      // 左移后补0 = 1101 0001 = 0xD0(表示指定这个设备写)
      // */
      // uint8_t Row = 1;
      // uint8_t i = 0x00;
      // for (i = 0x00; i < 0x7F; i++)
      // {
      // MyI2C_Start();//起始时序
      // MyI2C_SendByte((i << 1) | 0);//发送字节。写模式
      // uint8_t Ack = MyI2C_ReceiveAck();//接收应答
      // MyI2C_Stop();//结束时序
      // if(Ack == 0)
      // {
      // OLED_ShowHexNum(Row,1,i,2);
      // OLED_ShowBinNum(Row,4,i,7);
      // Row++;
      // }
      // }
      //}
      notion image
      7位地址主机接收流程:
      • 起始、从机地址+读、接收、接收数据、发送应答、接收数据、发送应答…接收数据、非应答、停止
      10位地址主机接收流程
      • 起始、发送帧头(11110 + 两位地址 + 0(写))、发送第二个字节的8位地址、重复起始条件、发送枕头(11110 + 两位地址 + 1(写))、直接接收数据、发送应答、接收…发送…接收、非应答、停止(没有发送第二次的字节)
      7位详解:
      1. 写入控制位的Start位、产生起始条件
      1. 等待EV5事件 表示已发送
      1. 发送地址(地址+读)
      1. 接收应答(ACK,写1 在接受一个字节后就返回一个应答)
      1. 产生EV6事件, 代表寻址已完成
      1. 产生EV6_1时间, 没有对应的标志事件。
      1. 接收数据1 代表数据正在通过移位寄存器输入
      1. 发送应答 硬件自动执行,需要配置是否应答。CR1寄存器中,ACK写1 应答, 否则不应答
      1. 产生EV7事件 因为此时就代表已经接收到数据了。 数据会通过移位寄存器转移到数据寄存器。 产生RxNE标志位,表示数据寄存器非空 (读DR寄存器来清除这个Ev7事件)
      1. 接收数据2 在我们正在读DR寄存器是 数据2就已经移位到移位寄存器中。等待移位到DR数据寄存器中。
      1. 发送应答
      1. 这事数据又被传入DR数据寄存器。EV7事件又来了。需要我们读取、
      1. 如此循环……直到停止 也就是需要设置ACK = 0 不应答, 和STOP请求。
       
      其实同上,这里是在发送一个字节之后,在对面没有应答前,就填充好了下一次要发送的数据。产生的标志位也是一样的。

      4. STM32硬件与软件的波形对比

      notion image

      5. STM32配置硬件I2C外设流程

      1. 开启外设和对应GPIO口的时钟
      1. 把I2C外设对应的GPIO口配置为复用开漏模式
      1. 使用结构体,对I2C进行配置
      1. 使能I2C

      6. STM32的I2C.h标准库函数介绍

      1.I2C配置和使用函数

      void I2C_DeInit(I2C_TypeDef* I2Cx);
      • 恢复缺省配置
      void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
      • 初始化I2C
      void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);
      • 初始化结构体
      void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
      • 使能I2C
      void I2C_DMACmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
      • I2C MDA使能
      • void I2C_DMALastTransferCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
        • 用于控制 I2C 传输中的最后一次传输的相关操作。
      void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
      • 调用,生成起始条件
      void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
      • 调用,生成终止条件
      void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
      • 配置 I2C 的应答机制。1为应答
      • void I2C_OwnAddress2Config(I2C_TypeDef* I2Cx, uint8_t Address);
        • 设置 I2C 设备自身的地址。
      • void I2C_DualAddressCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
        • 启用或禁用双地址模式,以适应特定的通信需求。
      • void I2C_GeneralCallCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
        • 控制是否响应通用呼叫。
      • void I2C_ITConfig(I2C_TypeDef* I2Cx, uint16_t I2C_IT, FunctionalState NewState);
        • 配置 I2C 的中断功能,根据不同的中断类型进行使能或禁用。
      void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
      • 发送一个字节的数据。
      uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);
      • 接收一个字节的数据。
      void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);
      • 发送 7 位地址并指定通信方向(读或写)。
      • uint16_t I2C_ReadRegister(I2C_TypeDef* I2Cx, uint8_t I2C_Register);
        • 读取 I2C 设备的特定寄存器的值。
      • void I2C_SoftwareResetCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
        • 通过软件方式对 I2C 进行复位操作。
      • void I2C_NACKPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_NACKPosition);
        • 配置非应答信号(NACK)的位置。
      • void I2C_SMBusAlertConfig(I2C_TypeDef* I2Cx, uint16_t I2C_SMBusAlert);
        • 针对 SMBus 警报进行相关配置。
      • void I2C_TransmitPEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
        • 控制是否传输 PEC(Packet Error Checking,数据包错误检查)信息。
      • void I2C_PECPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_PECPosition);
        • 配置 PEC 的位置。
      • void I2C_CalculatePEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
        • 决定是否计算 PEC 值。
      • uint8_t I2C_GetPEC(I2C_TypeDef* I2Cx);
        • 获取计算得到的 PEC 值。
      • void I2C_ARPCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
        • 控制自动重传功能的启用或禁用。
      • void I2C_StretchClockCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
        • 配置时钟拉伸功能。
      • void I2C_FastModeDutyCycleConfig(I2C_TypeDef* I2Cx, uint16_t I2C_DutyCycle);
        • 设置快速模式下的占空比,以优化通信性能。
      • FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
        • 读取标志位
      • void I2C_ClearFlag(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
        • 清除标志位
      • ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT);
        • 读取中断标志位
      • void I2C_ClearITPendingBit(I2C_TypeDef* I2Cx, uint32_t I2C_IT);
        • 清除中断标志位

      2.I2C状态监控功能

      I2C的标志位,往往是由多个标志位组合起来的一个大标志位。
      一个个去判断往往比较麻烦。
      所以I2C的库在.h头文件的最后,
      为我们提供了两种检测方案
      1. 基础状态监测
          • 使用 I2C_CheckEvent()函数 可以同时检测多个标志位来进行比较。 一般使用这个。
      notion image
      1. 高级状态监测
          • 使用I2C_GetLastEvent()函数 (实际上是把SR1和SR2两个状态寄存器拼接成一个16位的数据扔给你,随便你处理。所以一般不用)
      notion image
      1. 基于标志位的状态监控
          • 使用I2C_GetFlagStatus()函数 可以判断某一个标志位是否置一
      notion image

      7. STM32编写代码实现I2C硬件读写MPU6050

      7.1编写时需要注意的点

      • 发送字节时
        • 对于发送一个字节,发送地址和读写的一个字节之后,在移位寄存器还未发送完成时,就要填充下一次要发送的字节到DR数据寄存器中。否则将发生EV8_2标志(移位寄存器空,数据寄存器空)。导致读写停止。 正常应为EV8事件发生(字节正在发送,移位寄存器非空,数据寄存器空)
        • 对于发送多个字节,只需要重复发送、等待EV8事件即可。不发送时给予不应答、停止即可。
        • notion image
      • 接收字节时,
        • 对于接收一个字节,在指定完地址,重复起始之后,接收第第一个字节中时,就要提前配置Ack非应答和停止条件。之后等数据全部移位到数据寄存器中,才能保证只接收了一个字节。读取DR寄存器即可。
        • 对于接收多个字节,可以重复应答、然后等待EV7事件到来,读取寄存器。直到不想接收时,在数据进行移位时(还未到达DR寄存器时)提前设置非应答与停止操作。
        • notion image

      7.2程序文件简要说明:

      • MPU6050.c:初始化MPU6050的寄存器。使用STM32 自带的I2C外设,编写对指定地址发送指定值、读取指定地址的函数。以及等待标志位且超时退出的函数
      • MPU6050.h:函数声明、数据结构体声明
      • MPU6050_Reg.h:寄存器的地址与名称声明
      • main.c:测试I2C通信结果。

      MPU650.c

      MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);//等待EV5事件发生
      I2C_Send7bitAddress(I2C2,MPU6050ADDRESS,I2C_Direction_Transmitter);//发送地址和读写操作(也能用发送一个字节来完成)(自动应答)
      MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);//等待发送EV6事件发生
      I2C_SendData(I2C2,RegAddress);
      MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);//等待发送EVEV8_2事件发生(字节发送完毕)
      I2C_GenerateSTART(I2C2,ENABLE);//重复起始
      MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);//等待EV5事件发生
      I2C_Send7bitAddress(I2C2,MPU6050ADDRESS,I2C_Direction_Receiver);//发送地址和读写操作(也能用发送一个字节来完成)(自动应答)
      MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);//等待发送EV6事件发生
      I2C_AcknowledgeConfig(I2C2,DISABLE);//在字节来之前,设置非应答。
      I2C_GenerateSTOP(I2C2,ENABLE);//直接停止。但是会接收字节完毕之后才停
      MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED);//等待EV7事件到达。代表一个数据的字节已经在DR里了
      Data = I2C_ReceiveData(I2C2);
      I2C_AcknowledgeConfig(I2C2,ENABLE);//恢复默认状态,给从机应答
      return Data;//返回读取到的值
      }
      /
      • 函 数:初始化MPU6050
      • 参 数:无
      • 返 回 值:无
      */
      void MPU6050_Init(void)
      {
      /初始化I2C/
      // MyI2C_Init();
      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); //初始化PB10、11为复用开漏
      /初始化I2C外设/
      I2C_InitTypeDef I2C_InitStructure;
      I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//确定是否应答
      I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//指定STM32作为从机。可以响应几位的地址
      I2C_InitStructure.I2C_ClockSpeed = 50000;//最大400KHZ(快速)、标准为(100KHZ)(MPU6050最快也是400KHZ)
      I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//配置占空比。进入快速模式后才有用(>100KHZ后) (默认为1:1)
      I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;//I2C模式
      I2C_InitStructure.I2C_OwnAddress1 = 0x00;//指定STM32作为从机。STM本身的地址
      I2C_Init(I2C2,&I2C_InitStructure);
      /使能I2C外设/
      I2C_Cmd(I2C2,ENABLE);
      /初始化MPU6050/
      MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01); //电源管理1:不复位、解除睡眠、不循环、温度传感器不失能、选择X轴陀螺仪时钟
      MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00); //电源管理2:不需要循环模式唤醒频率、每个轴都不需要待机
      MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09); //采样率分频:数据输出的快慢,越小输出越快.这里给10分频
      MPU6050_WriteReg(MPU6050_CONFIG,0x06); //配置寄存器:外部同步不需要、数字低通滤波器设置为110
      MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18); //陀螺仪配置寄存器:不自测、满量程选择:11最大量程
      MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18); //加速度计配置寄存器:不自测、满量程选择:11最大量程、不用高通滤波器
      }
      /
      • 函 数:得到六轴传感器中的数据
      • 参 数:Str MPU6050_Data的地址
      • 返 回 值:无
      */
      SensorData MPU6050_Data;
      void MPU6050_GetData(SensorData *Str)//存放这种结构体类型的地址
      {
      Str->AccX = ( MPU6050_ReadeReg(MPU6050_ACCEL_XOUT_H) <<8|
      MPU6050_ReadeReg(MPU6050_ACCEL_XOUT_L));
      Str->AccY = ( MPU6050_ReadeReg(MPU6050_ACCEL_YOUT_H) <<8|
      MPU6050_ReadeReg(MPU6050_ACCEL_YOUT_L));
      Str->AccZ = ( MPU6050_ReadeReg(MPU6050_ACCEL_ZOUT_H) <<8|
      MPU6050_ReadeReg(MPU6050_ACCEL_ZOUT_L));
      Str->Temp = ( MPU6050_ReadeReg(MPU6050_TEMP_OUT_H) <<8|
      MPU6050_ReadeReg(MPU6050_TEMP_OUT_L));
      Str->GyroX = ( MPU6050_ReadeReg(MPU6050_GYRO_XOUT_H) <<8 |
      MPU6050_ReadeReg(MPU6050_GYRO_XOUT_L));
      Str->GyroY = ( MPU6050_ReadeReg(MPU6050_GYRO_YOUT_H) <<8 |
      MPU6050_ReadeReg(MPU6050_GYRO_YOUT_L));
      Str->GyroZ = ( MPU6050_ReadeReg(MPU6050_GYRO_ZOUT_H) <<8 |
      MPU6050_ReadeReg(MPU6050_GYRO_ZOUT_L));
      }
      //MPU6050_ACCEL_XOUT_H 0x3B
      //MPU6050_ACCEL_XOUT_L 0x3C
      //MPU6050_ACCEL_YOUT_H 0x3D
      //MPU6050_ACCEL_YOUT_L 0x3E
      //MPU6050_ACCEL_ZOUT_H 0x3F
      //MPU6050_ACCEL_ZOUT_L 0x40
      //MPU6050_TEMP_OUT_H 0x41
      //MPU6050_TEMP_OUT_L 0x42
      //MPU6050_GYRO_XOUT_H 0x43
      //MPU6050_GYRO_XOUT_L 0x44
      //MPU6050_GYRO_YOUT_H 0x45
      //MPU6050_GYRO_YOUT_L 0x46
      //MPU6050_GYRO_ZOUT_H 0x47
      //MPU6050_GYRO_ZOUT_L 0x48
      #ifndef __MPU6050_H
      #define __MPU6050_H
      /
      • 函 数:初始化MPU6050
      • 参 数:无
      • 返 回 值:无
      */
      void MPU6050_Init(void);
      /
      • 函 数:指定地址写一个字节
      • 参 数:RegAddress 指定的寄存器地址
        • Data 指定写入的数据
      • 返 回 值:无
      */
      void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
      /
      • 函 数:指定地址读一个字节
      • 参 数:RegAddress 指定要读的寄存器地址
      • 返 回 值:无
      */
      uint8_t MPU6050_ReadeReg(uint8_t RegAddress);
      //传感器数据
      typedef struct Data
      {
      int16_t AccX;
      int16_t AccY;
      int16_t AccZ;
      int16_t Temp;
      int16_t GyroX;
      int16_t GyroY;
      int16_t GyroZ;
      }SensorData;
      extern SensorData MPU6050_Data;
      /
      • 函 数:得到六轴传感器中的数据
      • 参 数:Str MPU6050_Data的地址
      • 返 回 值:无
      */
      void MPU6050_GetData(SensorData *Str);
      #endif
      #ifndef __MPU6050_REG_H
      #define __MPU6050_REG_H
      #define MPU6050_SMPLRT_DIV 0x19 //采样率分频
      #define MPU6050_CONFIG 0x1A //配置寄存器
      #define MPU6050_GYRO_CONFIG 0x1B //陀螺仪配置寄存器
      #define MPU6050_ACCEL_CONFIG 0x1C //加速度计配置寄存器
      #define MPU6050_ACCEL_XOUT_H 0x3B
      #define MPU6050_ACCEL_XOUT_L 0x3C
      #define MPU6050_ACCEL_YOUT_H 0x3D
      #define MPU6050_ACCEL_YOUT_L 0x3E
      #define MPU6050_ACCEL_ZOUT_H 0x3F
      #define MPU6050_ACCEL_ZOUT_L 0x40
      #define MPU6050_TEMP_OUT_H 0x41
      #define MPU6050_TEMP_OUT_L 0x42
      #define MPU6050_GYRO_XOUT_H 0x43
      #define MPU6050_GYRO_XOUT_L 0x44
      #define MPU6050_GYRO_YOUT_H 0x45
      #define MPU6050_GYRO_YOUT_L 0x46
      #define MPU6050_GYRO_ZOUT_H 0x47
      #define MPU6050_GYRO_ZOUT_L 0x48
      #define MPU6050_PWR_MGMT_1 0x6B //电源管理1
      #define MPU6050_PWR_MGMT_2 0x6C //电源管理2
      #define MPU6050_WHO_AM_I 0x75 //设备号
      #endif
      #include "stm32f10x.h" // Device header
      #include "oled.h"
      #include "Delay.h"
      #include "key.h"
      #include "MPU6050.h"
      /*
      硬件读写I2C
      */
      int main()
      {
      OLED_Init();//初始化OLED;
      MPU6050_Init();//初始化MPU6050
      while(1)
      {
      MPU6050_GetData(&MPU6050_Data);
      //显示加速度
      OLED_ShowSignedNum(1,1,(int16_t)MPU6050_Data.AccX,5);
      OLED_ShowSignedNum(2,1,(int16_t)MPU6050_Data.AccY,5);
      OLED_ShowSignedNum(3,1,(int16_t)MPU6050_Data.AccZ,5);
      //显示陀螺仪
      OLED_ShowSignedNum(1,8,MPU6050_Data.GyroX,5);
      OLED_ShowSignedNum(2,8,MPU6050_Data.GyroY,5);
      OLED_ShowSignedNum(3,8,MPU6050_Data.GyroZ,5);
      //显示温度
      OLED_ShowSignedNum(4,4,MPU6050_Data.Temp,5);
      }
      }
      • SS(Slave Select): 片选信号,用于选择从设备。每个从设备都有一个独立的SS线,通过拉低SS信号来选择相应的从设备进行通信。
      SPI通信线别称
      • SCK: SCLK, CLK, CK
      • MOSI: MI, SIO, DO(Data Output)
      • MISO: DI(Data Input)
      • SS: NSS(Not Slave Select), CS(Chip Select)
      时序和数据传输
      • 无应答机制: SPI没有应答机制,这意味着主机发送的数据没有确认信号,数据传输的完整性和正确性需要其他手段保障。
      • 时钟信号同步: SPI通信依赖时钟信号(SCK)进行数据同步,确保数据位在SCK的时钟沿(上升沿或下降沿)传输。

      2. SPI和I2C的区别

      I2C
      • I2C的硬件电路和软件时序更加复杂
      • I2C通信的性价比高:在消耗最低硬件资源的情况下,实现最多的功能
      • I2C的弱上拉,会限制最大通信速度(标准100KHZ以下,快速400KHZ以下)
      SPI
      • SPI传输更快。这一般取决于芯片厂商设计时的需求(比如W25Q64存储器芯片最大速度为80MHZ)
      • SPI设计的功能少,学习简单
      • SPI的硬件开销大,通信线的个数比较多。并且通信时经常会出现资源浪费的情况
      • SPI不会有I2C的应答机制

      3. SPI的硬件电路规定

      • 所有SPI设备的SCK、MOSI、MISO分别连在一起
      • 主机另外引出多条SS控制线,分别接到各从机的SS引脚
      • 所有设备的输出引脚配置为推挽输出 !但是,SPI协议规定,为了防止从机全都是推挽输出导致冲突。 !规定从机在未被点名时,应该将输出设置为高阻态(引脚断开)
      • 所有设备的输入引脚配置为浮空或上拉输入
       
      主机通过MOSI输出、从机通过MOSI输入
      主机已通过MISO输入,从机通过MISO输出
      主机(输出)想和谁通信,就把他的控制线输出为低电平。然后被对应的从机接收(输入)(同一时间只能选一个从机)
      notion image

      4. SPI的移位示意图

      SPI在通信时有三种情况
      • 边发送边接收
      • 只接收,不发送
      • 只发送,不接收

      1.边接收边发送

      SPI的主机和从机可以理解为这个框图。
      • 波特率发生器
        • 波特率发生器就是主机的时钟。波特率发生器会控制主机和从机的移位寄存器的移位操作(左移)
      • 在进行边接收边发送时,比如我们写入了1 0 1 0 1 0 1 0这个数据希望发送到从机、而从机西方发送0 1 0 1 0 1 0 1这个数据,希望发送到主机。
        • 主机和从机的移位寄存器就会根据时钟的上升沿和下降沿(上升沿发送,下降沿接收)一位一位的发送和接收数据。
          在下降沿没来到之前,暂存在发送寄存器中。
          在下降沿来到之后,接收器就会接收主机/从机发送来的数据,并保存在移位寄存器的最低位。
          如此循环八次,就可以得到一个字节的数据。
          如下图所示
          notion image
          notion image
       

      2.只接收,不发送

      在只接收时,接收方还是会发送的,只是发送由我们指定发送0x00或者0xFF。 并且接收方发来的数据,发送方是不会去读取的。

      3.只发送,不接收

      在发送时,接收方还是会发送的,只是接收由我们指定发送0x00或者0xFF。 并且接收方发来的数据,发送方是不会去读取的。

      5. SPI软件设计(时序基本单元)

      5.1 起始条件与终止条件的时序

      • 起始条件:SS从高电平到低电平
      • 终止条件:SS从低电平到高电平
      notion image

      5.2 主机与从机交换一个字节的时序

      SPI有两个配置位:CPOLCPHA
      他们的不同值,对应了SPI交换一个字节的不同方式
      • CPOL = 0时:空闲状态时,SCK为低电平
      • CPOL = 1时:空闲状态时,SCK为高电平
      • CPHA =0时:SCK第一个边沿移入数据,第二个边沿移出数据
      • CPHA =1时:SCK第一个边沿移出数据,第二个边沿移入数据
      • 其中CPHA表示的是时钟相位,决定第一个时钟采样移入还是第二个时钟采样移入
      一、交换一个字节(模式1)CPOL = 0 CPHA = 1
      notion image
      1. 点名:SS信号线被置为低电平,表示从机已被选中并准备接收或发送数据。
      1. SCK空闲状态:SCK(Serial Clock)信号线保持为低电平,这是CPOL=0模式下的空闲状态。
      1. 坐下:当数据交换完成后,SS信号线被置回高电平,从机不再被选中。
      1. 第一个边沿来临:SCK的第一个上升沿到来,主机和从机开始发送高位数据。
      1. 第二个边沿来临:SCK的第二个下降沿到来,主机和从机接收到数据到最低位。
      1. 不断循环:这个过程不断重复,直到完成一个字节的数据交换。 如果想接收多个数据,就让SS一直为0就OK
      1. 主机输出后的电平没有硬性要求:在数据交换结束后,SCK可以继续保持低电平也可以恢复到空闲状态。
      1. 但是从机恢复推挽输出:从机在未被点名前应是高阻态。
      二、交换一个字节(模式0)CPOL = 0 CPHA = 0
      在实际应用中。模式0的应用是最多的。
      notion image
      1. 点名:SS信号线被置为低电平,表示从机已被选中并准备接收或发送数据。
      1. 在第一个边沿来临的时候就要读取数据了,所以在此之前要提前发出最高位。
      1. 主机和从机在第一个边沿到达时开始接收信号了, 此时开始捕获数据到自己的移位寄存器的最低位。
      1. 主机和从机在第二个边沿开始发送信号, 送出自己的次高位
      1. 如此循环……,在接收到一个字节之后。 还会有一个低电平回到SCK静态。此时如果主机不打算交流了(SS返回高电平) 此时MOSI,主机输出端 可以上拉也可以保持,然后停止点名。 而MISO,从机的输出端 要回到高阻态
      1. 如果想要继续发送字节,就保持SS为0,主机从机的端口也不变。再来一遍这个波形。
      三、交换一个字节(模式2)CPOL = 1 CPHA = 0
      只不过是把CLK的波形取反一下就可以了。其他的和模式0一样
      notion image
      四、交换一个字节(模式0)CPOL = 0 CPHA = 0
      只不过是把CLK的波形取反一下就可以了。其他的和模式1一样
      notion image

      6. SPI通信举例(软硬件波形、控制W25Q64方法)

      6.1. 举例:SPI通信控制W25Q64使能

      SPI不像I2C那样,有效数据流第一个字节是寄存器地址、之后依次是读写的数据。 使用的是读写寄存器的模型
      SPI使用的是指令码+读写数据的模型:在SPI起始之后,第一个交换给从机的数据就是指令码。在从机中,会有一个对应的指令集。指令集会对应从机的不同功能。不同的功能对应不同的字节个数,比如控制W25Q64 存储器 使能,就需要一个字节。而控制写数据时,就需要多个字节。
      W25Q64存储器的使能的指令为 0x06
      在这里可以参考W25Q64使能的波形图
      这实际上就是主机和从机交换了0x06和0xFF的一个字节。
      在从机经过对比之后。发现0x06为自己的指令集中的使能,那么他就会开启。
      • 并且SPI是没有I2C的应答机制的
      notion image

      6.2. 举例:SPI通信控制W25Q64指定地址写

      指定地址写同样的也需要写指令集。不过这次的就不同于使能只有一位字节就可以了。
      • 需要先发送写指令(0x02)、
      • 随后在指定地址(W25Q64有8M的存储空间,所以他的地址长度为24位 [23::0],也就是我们要发送三个字节长的地址)
      • 最后再发送指定数据。(Data)
      波形如下:
      这也是一次次的交换数据
      这段波形的意思是:我要在地址为0x12 34 56的寄存器下写入0x55
      从机的输出一直为1 也就是主机一直跟从机交换0xFF
      notion image
      这里的和I2C的一样,每次读写都会使地址指针++、如果在一次发送之后不终止,后续发送或者读取的字节就会一次指到后边的字节中
       

      6.3. 举例:SPI通信控制W25Q64指定地址读

      指定地址读
      • 向SS指定的设备
      • 发送读指令(0x03)
      • 随后在指定地址(Address[23:0])下,读取从机数据(Data)
      这个波形的意思是:
      从前4个字节一直都是0xFF.第五个字节开始,从机开始发送字节。此时主机的输出为0xFF。从机开始发送这个寄存器中的值,从机的输出就不再是0xFF。而主机交换给从机的值是0XFF
      notion image
      这里的和I2C的一样,每次读写都会使地址指针++、如果在一次发送之后不终止,后续发送或者读取的字节就会一次指到后边的字节中
       
      • 并且还可以看出,由于从机回复 是硬件执行。所以从机回复的高低电平是紧贴着下降沿的。
      • 而软件,在下降沿后 主机的输出并没有紧贴着,稍有延迟。 这就是软件模拟的一些延迟。不过因为有同步时钟,这些都问题不大
      • 并且。在W25Q64发送数据时,他的数据是在上一个波形的下降沿提前发送(移出)的。 因为这是SPI的 模式0
        • notion image

      7.SPI基本时序编写

      7.1编写时需要注意的点

      SPI的基本时序有三个
      • 起始
      • 结束
      • 交换字节
      起始和结束只需要控制SS片选信号的高低电平即可
      交换字节分为两种方法
      • 掩码依次提取法
        • 掩码提取其实就是通过 & 操作来提取出一个字节中的某一位
      • 移位模型法
        • 移位模型法正是SPI通信时用到的方法。这个方法更加高效

      7.2程序文件简要说明:

      • MySPI:为了防止与stm32函数重名,所以添加了My前缀。 主要是为了完成SPI的三个基本时序
      • MySPI.h:函数声明

      MySPI.c

      • 返 回 值:无
      • 注意事项:无
      */
      void MySPI_W_MOSI (uint8_t BitValue)
      {
      GPIO_WriteBit(MOSI_Port,MOSI_Pin,(BitAction)BitValue);
      }
      /
      • 函 数:读 主机输入,从机输出信号MISO
      • 参 数:无
      • 返 回 值:BitValue
      • 注意事项:无
      */
      uint8_t MySPI_R_MISO (void)
      {
      return GPIO_ReadInputDataBit(MISO_Port,MISO_Pin);
      }
      /
      • 函 数:MySPI初始化
      • 参 数:无
      • 返 回 值:无
      • 注意事项:无
      */
      void MySPI_Init(void)
      {
      /配置时钟与引脚/
      RCC_APB2PeriphClockCmd(RCCPeriph,ENABLE);
      GPIO_InitTypeDef GPIO_InitStructure;
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
      GPIO_InitStructure.GPIO_Pin = SCK_Pin | SS_Pin | MOSI_Pin;
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_Init(MOSI_Port,&GPIO_InitStructure); //时钟、片选、MOSI都是推挽输出
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
      GPIO_InitStructure.GPIO_Pin = MISO_Pin;
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_Init(MISO_Port,&GPIO_InitStructure); //MISO 为上拉输入
      MySPI_W_SS(1); //片选默认为高
      MySPI_W_SCK(0); //时钟默认为低
      }
      /*/
      /SPI的三个时序单元/
      /*/
      /
      • 函 数:起始信号
      • 参 数:无
      • 返 回 值:无
      • 注意事项:无
      */
      void MySPI_Start(void)
      {
      MySPI_W_SS(0);
      }
      /
      • 函 数:终止条件
      • 参 数:无
      • 返 回 值:无
      • 注意事项:无
      */
      void MySPI_Stop(void)
      {
      MySPI_W_SS(1);
      }
      /
      • 函 数:交换一个字节(模式0)(方法1)
      • 参 数:SendByte 待发送的字节
      • 返 回 值:ReceiveByte 接收到的字节
      • 注意事项:这是使用掩码依次提取数据中的每一位保存或发送
        • 好处是不会改变传入参数本身,但是效率不高
          如果要改为模式1,则先上升沿再发送。先下降沿再接收(2、3则直接改时钟极性就ok了)
      */
      //uint8_t MySPI_WarpByte(uint8_t SendByte)
      //{
      // uint8_t ReceiveByte = 0x00;
      //
      // uint8_t i = 0;
      // for(i = 0; i < 8; i++)
      // {
      // MySPI_W_MOSI(SendByte & (0x80 >> i)); //发送第一个bit
      // MySPI_W_SCK(1);//第上升沿来临
      // if (MySPI_R_MISO() == 1)
      // {
      // ReceiveByte |= (0x80 >> i); //按照从高往低接收数据
      // }
      // MySPI_W_SCK(0); //下降沿来临
      // }
      // return ReceiveByte;
      //}
      /
      • 函 数:交换一个字节(模式0)(方法2)
      • 参 数:SendByte 待发送的字节
      • 返 回 值:ReceiveByte 接收到的字节
      • 注意事项:这是使用了移位模型的方式。效率更快
        • 如果要改为模式1,则先上升沿再发送。先下降沿再接收(2、3则直接改时钟极性就ok了)
      */
      uint8_t MySPI_WarpByte(uint8_t SendByte)
      {
      uint8_t i = 0;
      for(i = 0; i < 8; i++)
      {
      MySPI_W_MOSI(SendByte & 0x80); //发送第一个bit
      SendByte <<= 1; //发送数据左移一位
      MySPI_W_SCK(1); //第上升沿来临
      if (MySPI_R_MISO() == 1)
      {
      SendByte |= 0x01; //保存收到的数据到发送寄存器的最低位
      }
      MySPI_W_SCK(0); //下降沿来临
      }
      return SendByte;
      }
      #ifndef __MYSPI_H
      #define __MYSPI_H
      //初始化
      void MySPI_Init(void);
      //起始
      void MySPI_Start(void);
      //终止
      void MySPI_Stop(void);
      //交换
      uint8_t MySPI_WarpByte(uint8_t SendByte);
      #endif
      notion image

      6. W25Q64读写存储器编写

      7.1编写时需要注意的点

      一定要先看懂时序图再去编写存储器 的 读写时序
      需要注意Flash的操作注意事项
      写入操作时:
      • 写入操作前,必须先进行写使能
      • 每个数据位只能由1改写为0,不能由0改写为1
        • 所以写入数据前必须先擦除,擦除后,所有数据位变为1
      • 擦除必须按最小擦除单元进行(整个芯片、按块、按扇区)
      • 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入 写入操作结束后,芯片进入忙状态,不响应新的读写操作
      读取操作时:
      • 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取

      7.2程序文件简要说明:

      • MySPI:为了防止与stm32函数重名,所以添加了My前缀。 主要是为了完成SPI的三个基本时序
      • MySPI.h:函数声明
      • W25Q64.c:初始化W25Q64存储寄存器。完成读写、擦除存储器的时序
      • W25Q64.h:函数声明、数据结构体声明
      • W25Q64_Ins.h:控制存储器时需要用到的指令集
      • main.c:测试SPI读取存储器结果。

      MySPI.c

      MySPI.h

      W25Q64.c

      MySPI_Stop();//停止
      }
      #ifndef __W25Q64_H
      #define __W25Q64_H
      //初始化W25Q64
      void W25Q64_Init(void);
      /厂商和设备ID号/
      typedef struct ID
      {
      uint8_t MID;//8位厂商ID
      uint16_t DID;//16位设备ID
      }ID;
      extern ID W25Q64_ID;
      //获取厂商和设备号ID
      void W25Q64_ReadID(ID* Str);
      //页编程
      void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArray,uint16_t Count);
      //页擦除
      void W25Q64_PageErase(uint32_t Address);
      //读取
      void W25Q64_ReadData(uint32_t Address, uint8_t* DataArray,uint32_t Count);
      #endif
      #ifndef __W25Q64_INS_H
      #define __W25Q64_INS_H
      #define W25Q64_WRITE_ENABLE 0x06
      #define W25Q64_WRITE_DISABLE 0x04
      #define W25Q64_READ_STATUS_REGISTER_1 0x05
      #define W25Q64_READ_STATUS_REGISTER_2 0x35
      #define W25Q64_WRITE_STATUS_REGISTER 0x01
      #define W25Q64_PAGE_PROGRAM 0x02
      #define W25Q64_QUAD_PAGE_PROGRAM 0x32
      #define W25Q64_BLOCK_ERASE_64KB 0xD8
      #define W25Q64_BLOCK_ERASE_32KB 0x52
      #define W25Q64_SECTOR_ERASE_4KB 0x20
      #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
      #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
      #include "stm32f10x.h" // Device header
      #include "oled.h"
      #include "Delay.h"
      #include "key.h"
      #include "W25Q64.h"
      /
      • 函 数:验证SPI控制W25Q64存储器
      • 参 数:无
      • 返 回 值:无
      • 注意事项:无
      */
      uint8_t ArrWrite[] = {0x01,0x02,0x03,0x04};
      uint8_t ArrRead[4];
      int main()
      {
      Delay_Init();//初始化演示
      OLED_Init();//初始化OLED;
      W25Q64_Init();//初始化W25Q64存储器
      OLED_ShowString(1,1,"MID: ,DID: ");
      W25Q64_ReadID(&W25Q64_ID);//读ID放到这个结构体中
      OLED_ShowHexNum(1,5,W25Q64_ID.MID,2);
      OLED_ShowHexNum(1,12,W25Q64_ID.DID,4);//显示MID DID
      OLED_ShowString(2,1,"W:");
      OLED_ShowString(3,1,"R:");
      W25Q64_PageErase(0x000000); //擦除地址。写入前需要(最好定位到扇区的起始地址(后三位为0))
      W25Q64_PageProgram(0x000000,ArrWrite,4); //写入数组中数据到存储器
      W25Q64_ReadData(0x000000,ArrRead,4); //读取存储器中数据到数组
      OLED_ShowHexNum(2, 3, ArrWrite[0], 2);
      OLED_ShowHexNum(2 ,6, ArrWrite[1], 2);
      OLED_ShowHexNum(2, 9, ArrWrite[2], 2);
      OLED_ShowHexNum(2, 12, ArrWrite[3], 2);
      OLED_ShowHexNum(3, 3, ArrRead[0], 2);
      OLED_ShowHexNum(3 ,6, ArrRead[1], 2);
      OLED_ShowHexNum(3, 9, ArrRead[2], 2);
      OLED_ShowHexNum(3, 12, ArrRead[3], 2);
      while(1)
      {
      }
      }
      W25Q64_PageErase(0x000000); //擦除地址。写入前需要(最好定位到扇区的起始地址(后三位为0))
      // W25Q64_PageProgram(0x000000,ArrWrite,4); //写入数组中数据到存储器
      W25Q64_ReadData(0x000000,ArrRead,4); //读取存储器中数据到数组
      //W25Q64_PageErase(0x000000); //擦除地址。写入前需要(最好定位到扇区的起始地址(后三位为0))
      W25Q64_PageProgram(0x000000,ArrWrite,4); //写入数组中数据到存储器
      W25Q64_ReadData(0x000000,ArrRead,4); //读取存储器中数据到数组
      • LSBFIRST — 决定高位先行还是低位先行
      • SPE —(SPI ENABLE)决定SPI使能
      • BR —(Baud Rate)配置波特率,也就是SCK
      • MSTR —(Master)配置主从模式
      • CPOL和CPHA — 选择SPI的四种模式
      CR1寄存器
      • TXE — 发送寄存器空
      • RXNE — 接收寄存器非空
      CR2寄存器
      • 中断使能..DMA使能等
      1. 最后电路的左下角还有一个NSS位也就是SS位(不过我们一般直接用GPIO模拟,这个位置更偏向用于使用到多主机时采用的)
        1. 简要了解一下
          • 多主机时,所有的主机连接到一条NSS线上。当别的NSS置0时。代表现在别人已经是主机了。会沿着线到一个数据选择器。告诉这个机器,已经有设备是主机了。自己不能跟他抢。

      3.STM32的SPI基本框图

      notion image

      4. STM32的SPI主模式全双工连续传输 时序图

      notion image

      5. STM32的SPI主模式全双工非连续传输 时序图

      这里可以理解为,一个一个发,发完一个收一个。收完之后再发下一个
      并不会像连续一样 提前写入一个字节候着。会造成一定的资源浪费,拖慢传输速度
      • 适用于对传输速率要求不高的场合。
      notion image

      6. STM32非连续传输时,不同时钟速率的区别

      因为在非连续传输时,数据不是连续的流。所以在软件读取后才发送会浪费很长时间。
      对于CLK时钟频率越快的情况下,造成资源浪费的情况就显得越严重

      256分频

      notion image

      128分频

      notion image

      64分频

      notion image

      2分频(32MHZ)

      这里是因为他的频率已经超过我的示波器的采样频率了。但是也是可以看个大概的
      可以看到 ,时间浪费就很严重。
      notion image

      7. 编写SPI硬件读写W25Q64注意事项

      1. 硬件SPI外设不能随意指定,只能看复用到的引脚列表。
        1. 其中NSS片选引脚可以自己指定。用他指定的也可以
          notion image
           

      8. STM32的SPI外设相关库函数(只看SPI的)

      void SPI_I2S_DeInit(SPI_TypeDef* SPIx);
      • I2S 恢复缺省配置
      void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
      • SPI 初始化
      void I2S_Init(SPI_TypeDef* SPIx, I2S_InitTypeDef* I2S_InitStruct);
      • I2S 初始化
      void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);
      • SPI 初始化
      void I2S_StructInit(I2S_InitTypeDef* I2S_InitStruct);
      void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
      • SPI 外设使能
      void I2S_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
      • I2S 使能
      void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);
      • I2S 中断配置
      void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);
      • I2S DMA配置
      void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
      • I2S 写数据到TDR寄存器(发送)
      uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
      • I2S 读DR数据寄存器(接收)
      void SPI_NSSInternalSoftwareConfig(SPI_TypeDef* SPIx, uint16_t SPI_NSSInternalSoft);
      • NSS引脚配置
      void SPI_SSOutputCmd(SPI_TypeDef* SPIx, FunctionalState NewState);
      • NSS引脚配置
      void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);
      • 8位或16位数据帧配置
      void SPI_TransmitCRC(SPI_TypeDef* SPIx);
      • CRC校验配置
      void SPI_CalculateCRC(SPI_TypeDef* SPIx, FunctionalState NewState);
      • CRC校验配置
      uint16_t SPI_GetCRC(SPI_TypeDef* SPIx, uint8_t SPI_CRC);
      • CRC校验配置
      uint16_t SPI_GetCRCPolynomial(SPI_TypeDef* SPIx);
      • CRC校验配置
      void SPI_BiDirectionalLineConfig(SPI_TypeDef* SPIx, uint16_t SPI_Direction);
      • 半双时,双向线方向配置
      FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
      • 获取标志位
      void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
      • 清除标志位
      ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
      • 获取中断标志位
      void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
      • 清除中断标志位

      7.编写SPI硬件读写W25Q64步骤

      1. 开启GPIO和SPI外设时钟
      1. 初始化GPIO口
          • MISO为上拉输入,SCK、MOSI复用推挽输出
          • SS 通用推挽输出
      1. 配置SPI外设
          • 用结构体即可
      1. 开关控制
          • 使能
      1. 参考时序图编写代码

      8. 编写STM32SPI主模式全双工非连续传输

      notion image

      8.1 步骤

      1. 等待发送寄存器为空标志位TXE = 1
      1. 软件写入数据到发送寄存器
      1. 等待接收完成(这时发送也一定完成)
      1. 读取、返回RDR
      • !!注意其中的软件清除 是不需要我们手动清除的。 因为在我们写入TDR时会顺便清除TXE标志位、读取RDR时会清除RXNE标志位
      所以这个只需要把底层的MySPI.c代码改一下就可以了

      8.2 程序文件简要说明:

      • MySPI:完成SPI的三个基本时序
      • MySPI.h:函数声明
      • W25Q64.c:初始化W25Q64存储寄存器。完成读写、擦除存储器的时序
      • W25Q64.h:函数声明、数据结构体声明
      • W25Q64_Ins.h:控制存储器时需要用到的指令集
      • main.c:测试硬件SPI读取存储器结果。

      MySPI.c(相对软件,硬件仅需修改此文件)

      void MySPI_Start(void)
      {
      MySPI_W_SS(0);
      }
      /
      • 函 数:终止条件
      • 参 数:无
      • 返 回 值:无
      • 注意事项:无
      */
      void MySPI_Stop(void)
      {
      MySPI_W_SS(1);
      }
      /
      • 函 数:交换一个字节(模式0)(非连续)
      • 参 数:SendByte 待发送的字节
      • 返 回 值:ReceiveByte 接收到的字节
      • 注意事项:这是使用了移位模型的方式。效率更快
        • 如果要改为模式1,则先上升沿再发送。先下降沿再接收(2、3则直接改时钟极性就ok了)
      */
      uint8_t MySPI_WarpByte(uint8_t SendByte)
      {
      //等待TXE标志位为1 ,发送寄存器空
      while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);
      //软件写入数据到发送寄存器
      SPI_I2S_SendData(SPI1,SendByte);
      //等待接收完成(这时发送也一定完成)
      while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET);
      //读取RDR
      return SPI_I2S_ReceiveData(SPI1);
      }
      #ifndef __MYSPI_H
      #define __MYSPI_H
      //初始化
      void MySPI_Init(void);
      //起始
      void MySPI_Start(void);
      //终止
      void MySPI_Stop(void);
      //交换
      uint8_t MySPI_WarpByte(uint8_t SendByte);
      #endif
      #include "stm32f10x.h" // Device header
      #include "W25Q64.h"
      #include "W25Q64_Ins.h"
      #include "MySPI.h"
      /
      • 函 数:初始化W25Q64
      • 参 数:无
      • 返 回 值:无
      • 注意事项:无
      */
      void W25Q64_Init(void)
      {
      MySPI_Init();
      }
      //
      /拼接完整的通信时序/
      //
      /
      • 函 数:查看W25Q64的厂商号和设备号
      • 参 数:ID* Str 存放了ID结构体的指针
      • 返 回 值:无
      • 注意事项:接收第八位时是|=
      */
      ID W25Q64_ID;//存放设备ID号的结构体
      void W25Q64_ReadID(ID* Str)
      {
      MySPI_Start();//起始
      MySPI_WarpByte(W25Q64_JEDEC_ID);//发送读取设备号指令。返回值不要
      Str->MID = MySPI_WarpByte(W25Q64_DUMMY_BYTE);//接收厂商ID 从机发来的设备号。发送值随便
      Str->DID = MySPI_WarpByte(W25Q64_DUMMY_BYTE);//接收设备ID高八位
      Str->DID <<= 8;//把接收到的数据放到高八位
      Str->DID |= MySPI_WarpByte(W25Q64_DUMMY_BYTE);//接收设备ID低八位
      MySPI_Stop();//停止
      }
      /
      • 函 数:写使能
      • 参 数:无
      • 返 回 值:无
      • 注意事项:无
      */
      void W25Q64_WriteEnable(void)
      {
      MySPI_Start();//起始
      MySPI_WarpByte(W25Q64_WRITE_ENABLE);//发送
      MySPI_Stop();//停止
      }
      /
      • 函 数:写失能
      • 参 数:无
      • 返 回 值:无
      • 注意事项:无
      */
      void W25Q64_WriteDisable(void)
      {
      MySPI_Start();//起始
      MySPI_WarpByte(W25Q64_WRITE_DISABLE);//发送
      MySPI_Stop();//停止
      }
      /
      • 函 数:等待忙函数
      • 参 数:无
      • 返 回 值:无
      • 注意事项:无
      */
      void W25Q64_WaitBusy(void)
      {
      MySPI_Start();//起始
      MySPI_WarpByte(W25Q64_READ_STATUS_REGISTER_1); //发送
      uint32_t TimeOut = 100000;
      while((MySPI_WarpByte(W25Q64_DUMMY_BYTE)&0x01) == 0x01) //读取状态寄存器1的Busy位是否为1,为1则等待
      {
      TimeOut--;
      if(TimeOut == 0)
      {
      break; //超时退出
      }
      }
      MySPI_Stop(); //停止
      }
      /
      • 函 数:页编程
      • 参 数:Address 要写入那个页地址
        • *DataArray 存储字节所用的数组
          Count 一次写入多少字节
      • 返 回 值:无
      • 注意事项:一次只能写入最多0-256个字节
      */
      void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArray,uint16_t Count)//(0-256,所以要16位)
      {
      W25Q64_WaitBusy();
      //事前等待。(事后等待是先等待再退出,比较保险。 事前等待可以先做别的事,再进去。效率高)
      W25Q64_WriteEnable();//写使能(每次写时都要先写使能)
      MySPI_Start();//起始
      MySPI_WarpByte(W25Q64_PAGE_PROGRAM);//发送页编程指令
      MySPI_WarpByte(Address >> 16 );
      MySPI_WarpByte(Address >> 8 );//(接收只能接收8位。会自动舍弃)
      MySPI_WarpByte(Address >> 0);//发送页地址
      uint16_t i = 0;
      for(i = 0; i < Count; i++)
      {
      MySPI_WarpByte(DataArray[i]);//发送Count个数组的第i位
      }
      MySPI_Stop();//停止
      }
      /
      • 函 数:页擦除
      • 参 数:Address 要擦除那一页
      • 返 回 值:无
      • 注意事项:最小的擦除单位。4kb 1扇区
      */
      void W25Q64_PageErase(uint32_t Address)
      {
      W25Q64_WaitBusy();
      //事前等待。(事后等待是先等待再退出,比较保险。 事前等待可以先做别的事,再进去。效率高)
      W25Q64_WriteEnable();//写使能(每次写时都要先写使能)
      MySPI_Start();//起始
      MySPI_WarpByte(W25Q64_SECTOR_ERASE_4KB);//发送页编程指令
      MySPI_WarpByte(Address >> 16 );
      MySPI_WarpByte(Address >> 8 );//发送地址
      MySPI_WarpByte(Address >> 0);
      MySPI_Stop();//停止
      }
      /
      • 函 数:读取数据
      • 参 数:Address 要读取个地址
        • *DataArray 存储字节所用的数组
          Count 一次读取多少字节
      • 返 回 值:无
      • 注意事项:读取可以无限制读取
      */
      void W25Q64_ReadData(uint32_t Address, uint8_t* DataArray,uint32_t Count)//读取没有限制
      {
      W25Q64_WaitBusy();
      //事前等待。(事后等待是先等待再退出,比较保险。 事前等待可以先做别的事,再进去。效率高)
      MySPI_Start();//起始
      MySPI_WarpByte(W25Q64_READ_DATA);//发送页编程指令
      MySPI_WarpByte(Address >> 16 );
      MySPI_WarpByte(Address >> 8 );//(接收只能接收8位。会自动舍弃)
      MySPI_WarpByte(Address >> 0);//发送页地址
      uint32_t i = 0;
      for(i = 0; i < Count; i++)
      {
      DataArray[i] = MySPI_WarpByte(W25Q64_DUMMY_BYTE);//接收Count个字节,放到数组的第i位
      }
      MySPI_Stop();//停止
      }
      #ifndef __W25Q64_H
      #define __W25Q64_H
      //初始化W25Q64
      void W25Q64_Init(void);
      /厂商和设备ID号/
      typedef struct ID
      {
      uint8_t MID;//8位厂商ID
      uint16_t DID;//16位设备ID
      }ID;
      extern ID W25Q64_ID;
      //获取厂商和设备号ID
      void W25Q64_ReadID(ID* Str);
      //页编程
      void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArray,uint16_t Count);
      //页擦除
      void W25Q64_PageErase(uint32_t Address);
      //读取
      void W25Q64_ReadData(uint32_t Address, uint8_t* DataArray,uint32_t Count);
      #endif
      #ifndef __W25Q64_INS_H
      #define __W25Q64_INS_H
      #define W25Q64_WRITE_ENABLE 0x06
      #define W25Q64_WRITE_DISABLE 0x04
      #define W25Q64_READ_STATUS_REGISTER_1 0x05
      #define W25Q64_READ_STATUS_REGISTER_2 0x35
      #define W25Q64_WRITE_STATUS_REGISTER 0x01
      #define W25Q64_PAGE_PROGRAM 0x02
      #define W25Q64_QUAD_PAGE_PROGRAM 0x32
      #define W25Q64_BLOCK_ERASE_64KB 0xD8
      #define W25Q64_BLOCK_ERASE_32KB 0x52
      #define W25Q64_SECTOR_ERASE_4KB 0x20
      #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
      #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
      #include "stm32f10x.h" // Device header
      #include "oled.h"
      #include "Delay.h"
      #include "key.h"
      #include "W25Q64.h"
      /
      • 函 数:验证SPI控制W25Q64存储器
      • 参 数:无
      • 返 回 值:无
      • 注意事项:无
      */
      uint8_t ArrWrite[] = {0x01,0x02,0x03,0x04};
      uint8_t ArrRead[4];
      int main()
      {
      Delay_Init();//初始化演示
      OLED_Init();//初始化OLED;
      W25Q64_Init();//初始化W25Q64存储器
      OLED_ShowString(1,1,"MID: ,DID: ");
      W25Q64_ReadID(&W25Q64_ID);//读ID放到这个结构体中
      OLED_ShowHexNum(1,5,W25Q64_ID.MID,2);
      OLED_ShowHexNum(1,12,W25Q64_ID.DID,4);//显示MID DID
      OLED_ShowString(2,1,"W:");
      OLED_ShowString(3,1,"R:");
      W25Q64_PageErase(0x000000); //擦除地址。写入前需要(最好定位到扇区的起始地址(后三位为0))
      W25Q64_PageProgram(0x000000,ArrWrite,4); //写入数组中数据到存储器
      W25Q64_ReadData(0x000000,ArrRead,4); //读取存储器中数据到数组
      OLED_ShowHexNum(2, 3, ArrWrite[0], 2);
      OLED_ShowHexNum(2 ,6, ArrWrite[1], 2);
      OLED_ShowHexNum(2, 9, ArrWrite[2], 2);
      OLED_ShowHexNum(2, 12, ArrWrite[3], 2);
      OLED_ShowHexNum(3, 3, ArrRead[0], 2);
      OLED_ShowHexNum(3 ,6, ArrRead[1], 2);
      OLED_ShowHexNum(3, 9, ArrRead[2], 2);
      OLED_ShowHexNum(3, 12, ArrRead[3], 2);
      while(1)
      {
      }
      }

      26. STM32后备区域:读写BKP备份寄存器与使用RTC实时时钟详解

      1 什么是STM32的后备区域

      STM32 的后备区域是芯片内部的一个特殊区域。
      特点和作用
      • 后备区域可以在主电源VCC断电之后由备用电源VBA提供供电。
      • 后备区域存储的数据不会因为复位而重置
      • 但是如果包括主电源和VBAT都断电了。那么备用区域也会清除,因为他们的存储器本质是RAM存储器,掉电丢失。
      后备区域有什么:
      • BKP备份寄存器
      • RTC实时时钟

      *********分割线**********

      下面开始BKP部分

      2.1 BKP备份寄存器简介

      BKP(Backup Registers)备份寄存器(后备寄存器)
      它位于后备区域。
      • BKP可用于存储数据。
      • 存储特性:
          1. 当VDD(2.0~3.6V)电源(主电源)被切断,后备区域仍然由VBAT备用电源(1.8~3.6V)维持供电。
          1. 并且系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位
          1. 但是如果备用电源VBAT和主电源VCC都断电了,就会清除数据,因为BKP本质是RAM存储器。掉电丢失数据
      • STM32的TAMPER引脚
        • 他可以产生的侵入事件可以将所有备份寄存器内容清除
          1. TAMPER是用于引入检测信号(可以是或上升沿/下降沿)的,当发生入侵时,将清除BKP所有内容,并申请中断。
          1. 并且他是由备用电源供电,主电源断电后侵入检测仍然有效,以保证数据安全
      • BKP的RTC引脚可以输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲
      • BKP存储RTC时钟校准寄存器
      • STM32后备区域的供电特性:
          1. 当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电。
          1. 当VDD主电源上电时,后备区域供电会自动从VBAT切换到VDD。
      • BKP数据存储容量:
          1. 20字节(中容量和小容量)/ 84字节(大容量和互联型)
      • 手册建议
          1. 如果没有外部电池,建议VBAT引脚接到VDD,就是VBAT和主电源接到一起,并且再连接一个100nf的滤波电容

      2.2 BKP备份寄存器基本结构

      notion image
      其中橙色部分就是后备区域,BKP处于后备区域。但后备区域不止有BKP。
      STM32后备区域的特性:
      • 当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电。
      • 当VDD主电源上电时,后备区域供电会自动从VBAT切换到VDD。
      数据寄存器:
      • 每个寄存器有16位(可存储2个字节)。
      • 中小容量设备有DR1~DR10,总共10个数据寄存器,每个寄存器存储2个字节,总容量为20字节。
      • 大容量和互联型设备有42个数据寄存器(DR)。
      TAMPER引脚:
      • 用于引入检测信号(上升沿/下降沿),清除BKP所有内容以保证数据安全。
      时钟输出:
      • 可以将RTC相关时钟从PC13位置的RTC引脚输出出去,供外部使用。
      • 输出校准时钟时,可以配合校准寄存器对RTC的误差进行校准。

      2.3 BKP外设头文件 bkp.h介绍

      void BKP_DeInit(void);
      • 恢复缺省配置(用于清空BKP所有BKP寄存器)
      void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel);
      • 配置TAMPER引脚的有效电平
      void BKP_TamperPinCmd(FunctionalState NewState);
      • 是否开启侵入检测功能
      void BKP_ITConfig(FunctionalState NewState);
      • 是否开启中断
      void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource);
      • 时钟输出功能配置(在RTC引脚上输出时钟信号、RTC校准时钟、RTC闹钟脉冲、秒脉冲)
      void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue);
      • 设置RTC校准值
      void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);
      • 写BKP备份寄存器
      uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);
      • 读BKP寄存器
      FlagStatus BKP_GetFlagStatus(void);
      查看标志位
      void BKP_ClearFlag(void);
      清除标志位
      ITStatus BKP_GetITStatus(void);
      查看中断标志位
      void BKP_ClearITPendingBit(void);
      清除中断标志位
      pwr.h中也有一个函数是需要用到的 void PWR_BackupAccessCmd(FunctionalState NewState);
      • 备份寄存器访问使能(其实是设置BDP位)

      2.4 读写 BKP备份寄存器 操作步骤

      1. 启PWR和BKP的时钟
          • PWR的开启函数为RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); 它的目的是开启后备电源的时钟(可以理解为开启后备电源VBAT)。
          • BKP的开启函数RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); 他的目的是开启BKP外设的时钟,可以看到他们都是APB1总线下的。
      1. 使能备份区域的访问
          • 因为RTC实时时钟和BKP备份寄存器,都处于备份区域中。在STM32中,想访问RTC和BKP,就要先开启备份区域的访问权限。它在PwR.h中 函数:PWR_BackupAccessCmd(ENABLE);
      1. 读写操作
          • 写入:BKP_WriteBackupRegister(BKP_DR1,Data);
          • 读出:Data = BKP_ReadBackupRegister(BKP_DR1);

      2.5 编写 读写备份寄存器

      5.1 文件介绍

      • main.c : 读写测试BKP备份寄存器是否在单主电源掉电时丢失数据。是否受到复位影响…

      main.c

      *********分割线**********

      下面开始RTC部分

      3.1 什么是Unix时间戳

      • Unix时间戳,最早是Unix系统使用的。所以叫Unix时间戳。
      • 目前Windows、linux、安卓等系统都是实用的Unix时间戳。
      • Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒。
        • GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准 (不过他不精准,因为地球自转是越来越慢的。所以又有了如下的规定)
          • (格林尼治是伦敦的一个区)
        • UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致
        • 我们时间戳所说的1970年1月1日0时0分0秒。指的是伦敦伦敦时间的0时0秒。 其他的位置可以分为24个时区。每偏差一个时区,相应的时间就要加或减一个小时
      • 时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量
      • 世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间 比如Unix时间戳为0,代表伦敦的0时0分,那么北京就是+8得到8时0分
      • C语言官方为我们提供了函数,可以直接把Unix时间戳转换为时间。
      • 可以在百度直接搜索unix在线时间戳。

      3.2 C语言中时间戳转换函数

      2.1 C语言提供的time.h函数介绍

      • C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换
      • 一些常用的函数如下:(粗细为更重要)
        • time_t time(time_t*);
          获取系统时钟
          struct tm* gmtime(const time_t*);
          秒计数器转换为日期时间(格林尼治时间)
          struct tm* localtime(const time_t*);
          秒计数器转换为日期时间(当地时间)
          time_t mktime(struct tm*);
          日期时间转换为秒计数器(当地时间)
          char* ctime(const time_t*);
          秒计数器转换为字符串(默认格式)
          char* asctime(const struct tm*);
          日期时间转换为字符串(默认格式)
          size_t strftime(char*, size_t, const char*, const struct tm*);
          日期时间转换为字符串(自定义格式)

      2.2 时间戳转换图

      这张图就清晰了显示了各个函数的作用:其实就是在各种数据类型之间进行转换。
      notion image
      • 首先先了解一下各个数据类型是什么
          1. 秒计数器数据类型:time_t,其实就是一个32或64位的有符号的整形数据。也就是64位的秒计数器(如果不特别声明,默认为64。)
          1. 日期时间数据类型:struct tm,这时一个结构体类型。成员如下:
            1. notion image
              • 秒、
              • 分、
              • 时、
              • 月的几日、
              • 月份、(需要+1偏移量)
              • 年份(需要+1900偏移量)、
              • 周某开始的星期几、从1月1日开始的第几天、
              • 是否使用夏令时 (是为了鼓励夏天时早睡早起节约用电设计的,目前个别国家在使用)
          1. 字符串型数据类型:char*,用来指向一个表示时间的字符串
      所以,在转换时,要根据函数的返回值,来进行相应赋值。比如struct tm* gmtime(const time_t*);的返回值为Struct tm的指针类型。那么在赋值给自己定义的Struct tm xxx 结构体时,就要先用*取值,才能正确赋值
      等等以此类推,需要根据不同的类型进行转换, (time_t 整形、Struct tm 结构体 Char* 字符指针..)

      3.3 RTC简介

      RTC (Real Time Clock):实时时钟。
      • RTC也位于STM32的后备区域中。可以由VBAT备用电源供电
      • RTC是一个独立的定时器,可为系统提供时钟和日历的功能。
      • RTC和时钟配置系统处于后备区域,系统复位时数据不清零,因为在VDD(2.0 - 3.6V)断电后可借助VBAT(1.8 - 3.6V)供电继续走时。
      • RTCCLK可选择三种RTC时钟源:
          1. HSE时钟除以128(通常为8MHz/128)
          1. LSE振荡器时钟(通常为32.768KHz)
          1. LSI振荡器时钟(40KHz)

      3.4 RTC时钟选择

      时钟信号解释
      • HSE = 高速外部时钟信号
      • HSI = 高速内部时钟信号
      • LSI = 低速内部时钟信号
      • LSE = 低速外部时钟信号
      • H (High):高速,L (Low):低速,E (External):外部,I (Internal):内部
        • notion image
      时钟选择
      • 外部高速时钟:一般作为系统主时钟。供内部程序运行和主要外设使用。
      • 内部低速时钟:主要用做看门狗等的使用。
      • 外部低速时钟:这个时钟一般都是为了给RTC提供时钟的。
          1. 因为LSE外部低速时钟,才可以通过VBAT备用电池供电
          1. 所以在主电源断电情况下LSE可以继续震荡,实现RTC主电源掉电继续走时的功能。
          1. 并且他的频率为32.768KHZ。经过2^15分频之后每次自然溢出时刚好为1s,也就是1HZ。

      3.5 RTC框图

      notion image
      灰色填充区域均是后备区域。
      可编程预分频器:
      • RTC_CNT每秒自增,因此驱动计数器的时钟 TR_CLK 需要是1Hz的信号。 实际提供RTC模块的时钟(RTCCLK)频率较高,因此RTCCLK经过20位RTC预分频器(1~2^20分频),保证输出给计数器的频率为1Hz。
      分频和计数:
      • 输入时钟RTCCLK,经过RTC预分频器(由重装载寄存器RTC_PRL和余数寄存器RTC_DIV控制),计数器重装值ARR和CNT进行分频。
      RTC_CNT:
      • 可以作为Unix时间戳的秒计数器,再借用time.h的函数可以方便地得到年月日时分秒。
      闹钟寄存器RTC_ALR:
      • 32位寄存器,用来设置闹钟。设置闹钟时,将ALR写入一个秒数,当CNT的值等于ALR设定的闹钟值时,就会产生RTC_Alarm闹钟信号。 通过中断系统,在闹钟中断里执行相应操作。
      • 同时,闹钟信号可以让STM32退出待机模式。 此外,这个闹钟值是一个定值,只能响一次。若想实现周期闹钟,在每次闹钟响过后,都需要重新设置下一次闹钟时间。
      中断信号:
      • RTC_Second(秒中断):来自于CNT的输入时钟。开启此中断后,程序会每秒进入一次RTC中断。
      • RTC_Overflow(溢出中断):来自CNT右边,表示CNT的32位计数器计满溢出时触发一次中断。
      • RTC_Alarm(闹钟中断):当计数器的值和闹钟值相等时触发中断,同时可以唤醒设备退出待机模式。
      中断标志位和中断输出控制
      • F(Flag) 结尾的是对应的中断标志位。
      • IE(Interrupt Enable) 结尾的是中断使能。
      • 最后三个信号通过一个或汇聚到NVIC中断控制器。
      APB1总线和APB1接口:
      • 程序读写寄存器的地方可以通过APB1总线完成,RTC位于APB1总线上的设备。 退出待机模式:唤醒机制
      • 闹钟信号和WKUP引脚都可以唤醒设备,退出待机模式。

      3.6 RTC基本框图

      notion image
      时钟来源配置:
      • 最左边的RTCCLK时钟来源在RCC中配置,可以从三个时钟中选择一个作为RTCCLK。
      时钟预分频:
      • 选择的RTCCLK经过预分频器对时钟进行分频。
      • 余数计数器是一个自减计数器,存储当前的计数值。
      • 重装寄存器决定计数目标和分频值。
      • 分频后得到1Hz的秒计数信号,传递给32位计数器,每秒自增一次。
      闹钟设定:
      • 32位计数器下有一个32位的闹钟值,可以设定闹钟时间。
      中断信号触发:
      • 右侧有三个信号可以触发中断:秒信号、计数器溢出信号和闹钟信号。
      • 这三个信号通过中断输出控制,进行中断使能。
      • 启用的中断信号才能传递到NVIC,然后向CPU申请中断。
      程序配置步骤:
      配置数据选择器:选择RTCCLK时钟来源。
      配置重装寄存器:选择分频系数。
      配置32位计数器:
      • 进行日期时间的读写。
      • 如果需要闹钟,配置32位闹钟值。
      配置中断:
      • 启用中断,再配置NVIC。
      • 最后,编写对应的中断函数。

      3.7 RTC硬件电路

      notion image
      1. 备用电池供电
          • 简单连接(左侧):使用一个3V的电池B1直接连接到VBAT和GND。这样设计简单,但是电源冗余不高。
          • 推荐连接(中间):使用两个3V的电池B2和B3通过两个二极管D1和D2连接到VBAT和GND。这样设计增加了电源的可靠性,因为如果一个电池失效,另一个电池还能提供电源。电容C3(0.1uF)用于滤波,稳定电压。
      1. 外部低速晶振
          • 晶振部分(中间):使用一个32.768kHz的晶振(X1)连接到两个10pF的电容(C1和C2),并接地。这部分电路提供了一个稳定的时钟信号,通常用于RTC(实时时钟)功能。
          • 连接到STM32单片机(右侧):OSC32_IN和OSC32_OUT分别连接到STM32单片机的PC14和PC15引脚。
      1. STM32单片机连接
          • 供电和地(右侧):VDD和VSS分别是电源和地,VDD连接到电源正极,VSS连接到地。VBAT连接到备用电池供电部分的输出。
          • 时钟信号(右侧):PC14和PC15分别连接到外部低速晶振的OSC32_IN和OSC32_OUT。
          • 其他引脚(右侧):图中列出了STM32F103C8T6单片机的引脚配置,包括PA0到PA15,PB0到PB15等。这些引脚可以根据具体应用进行配置。

      3.8 RTC头文件介绍

      void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);
      • RTC 中断使能:通过传入指定的中断类型 RTC_IT 和状态 NewState,实现对 RTC 中断的使能或失能控制。
      void RTC_EnterConfigMode(void);
      • 进入 RTC 配置模式:用于进入 RTC 的配置状态,以便进行相关参数的修改。
      void RTC_ExitConfigMode(void);
      • 退出 RTC 配置模式:在完成 RTC 配置操作后,使用此函数退出配置模式。
      uint32_t RTC_GetCounter(void);
      • 获取 RTC 计数器的值:返回 RTC 计数器的当前数值。
      void RTC_SetCounter(uint32_t CounterValue);
      • 设置 RTC 计数器的值:将 RTC 计数器设置为指定的数值 CounterValue
      void RTC_SetPrescaler(uint32_t PrescalerValue);
      • 设置 RTC 预分频的值:为 RTC 预分频设置特定的值 PrescalerValue
      void RTC_SetAlarm(uint32_t AlarmValue);
      • 设置 RTC 闹钟的值:设定 RTC 闹钟的触发值为 AlarmValue
      uint32_t RTC_GetDivider(void);
      • 获取 RTC 预分频分频因子的值:获取当前 RTC 预分频分频因子的数值。
      void RTC_WaitForLastTask(void);
      • 等待最近一次对 RTC 寄存器的写操作完成:确保之前对 RTC 寄存器的写入操作已经完成。
      void RTC_WaitForSynchro(void);
      • 等待 RTC 寄存器与 RTC 的 APB 时钟同步:等待 RTC 相关寄存器(如 RTC_CNTRTC_ALR 和 RTC_PRL)与 APB 时钟完成同步。
      FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);
      • 检查指定的 RTC 标志位设置与否:通过传入标志位 RTC_FLAG,返回其状态。
      void RTC_ClearFlag(uint16_t RTC_FLAG);
      • 清除 RTC 的待处理标志位:清除指定的 RTC 标志位。
      ITStatus RTC_GetITStatus(uint16_t RTC_IT);
      • 检查指定的 RTC 中断发生与否:根据传入的中断类型 RTC_IT,判断中断是否发生。
      void RTC_ClearITPendingBit(uint16_t RTC_IT);
      • 清除 RTC 的中断待处理位:清除指定 RTC 中断的待处理位。

      3.9 使用 RTC实时时钟 操作步骤

      RTC使用时,一般都是和BKP一起使用的:
      因为RTC在初始化时,需要配置其32位的时间戳计数器。
      但每次复位或者主电源上电后,在执行主程序时,会重新初始化RTC的时间戳计数器。这就导致了RTC实时时钟本来能在备份电源的供给下计时,但是上电后又给我重新初始化覆盖了。导致不实时了!
      所以需要在初始化程序中判断是否需要设置32位的时间戳计数器。
      一般的方法是,在第一次初始化RTC时,顺便在BKP备份寄存器中写入特定值。在下一次初始化RTC时,对BKP备份寄存器的值进行判断。如果之前没有写入,那么就初始化。否则不初始化。
      所以使用RTC实时时钟操作步骤如下:
      • 执行以下操作将使能对BKP和RTC的访问:
          1. 启PWR和BKP的时钟
              • PWR的开启函数为RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); 开启后备电源的时钟
              • BKP的开启函数RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); 开启BKP外设的时钟
          1. 使能备份区域的访问
              • 在STM32中,想访问RTC和BKP,就要先开启备份区域的访问权限。
              函数:PWR_BackupAccessCmd(ENABLE);
      • 判断是否需要初始化RTC见代码部分
      • 开启LSE时钟
          1. 开启LSE时钟 RCC_LSEConfig(RCC_LSE_ON);
          1. 等待LSE时钟开启完毕 while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);
          1. 选择RTCCLK时钟为LSERCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
          1. 使能RTCCLK时钟 RCC_RTCCLKCmd(ENABLE);
      • 等待时钟同步
          1. 因为可能在恢复主电源之后,APB1总线刚刚恢复震荡频率,但是RTCCLK需要经过外部震荡源分频后才能有一次输出。如果直接读取,会导致读取不准确。 所以需要等待RTCCLK产生上升沿来激活更新一下时间戳计数器,这时APB1直接读取。才是准确的。 所以软件读取时必须等待RTCCLK来一个上升沿,使RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1。这时再读取RTC_CRL寄存器。才能正确读取。
            1. 函数:RTC_WaitForSynchro(); (等待时钟同步)
      • 等待写入完成
          1. 对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。
          1. 可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。 当RTOFF状态位是1时,才可以写入RTC寄存器
            1. 函数:RTC_WaitForLastTask();
      • 设置RTC预分频器
          1. 设置预分频器RTC_SetPrescaler(32768 - 1);
          1. 等待写入完成RTC_WaitForLastTask();
      • 写入前其实是需要设置RTC_CRL寄存器中的CNF位 (函数会帮我们自动完成,所以不需要)
          1. 在写入时,必须设置RTC_CRL寄存器中的CNF位, 使RTC进入配置模式后,才能写入RTC_PRL(预分频器)、RTC_CNT(时间戳计数器)、RTC_ALR(闹钟寄存器)寄存器
      • 设置CNT时间戳计数器时间
        • (利用C语言中time.h来转换时间戳并写入,详见代码)
      • 在BKP备份寄存器中写入特定数据。为下次复位或仅主电源断电后上电时判断是否初始化打下基础

      3.10 编写RTC实时时钟显示年月日时分秒

      3.10.1 文件介绍

      • MyRTC.c 初始化RTC、通过C语言time.h函数来编写 时间转换时间戳、时间戳转换时间的函数
      • MyRTC.h 函数声明
      • main.c 测试MyRTC显示

      MyRTC.c

      BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); //在备份寄存器写入自己规定的标志位,用于判断RTC是不是第一次执行配置
      }
      else //RTC不是第一次配置
      {
      RTC_WaitForSynchro(); //等待同步
      RTC_WaitForLastTask(); //等待上一次操作完成
      }
      }
      /
      • 函 数:RTC设置时间
      • 参 数:无
      • 返 回 值:无
      • 说 明:调用此函数后,全局数组里时间值将刷新到RTC硬件电路
      */
      void MyRTC_SetTime(void)
      {
      time_t time_cnt = 0; //定义秒计数器数据类型
      struct tm time_date; //定义日期时间数据类型
      time_date.tm_year = MyRTC_Time[0] - 1900; //将数组的时间赋值给日期时间结构体
      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];
      time_cnt = mktime(&time_date) - 8 60 60; //调用mktime函数,将日期时间转换为秒计数器格式
      //- 8 60 60为东八区的时区调整
      RTC_SetCounter(time_cnt); //将秒计数器写入到RTC的CNT中
      RTC_WaitForLastTask(); //等待上一次操作完成
      }
      /
      • 函 数:RTC读取时间
      • 参 数:无
      • 返 回 值:无
      • 说 明:调用此函数后,RTC硬件电路里时间值将刷新到全局数组
      */
      void MyRTC_ReadTime(void)
      {
      time_t time_cnt; //定义秒计数器数据类型
      struct tm time_date; //定义日期时间数据类型
      time_cnt = RTC_GetCounter() + 8 60 60; //读取RTC的CNT,获取当前的秒计数器
      //+ 8 60 60为东八区的时区调整
      time_date = *localtime(&time_cnt); //使用localtime函数,将秒计数器转换为日期时间格式
      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;
      }
      #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 "Delay.h"
      #include "OLED.h"
      #include "MyRTC.h"
      int main(void)
      {
      /模块初始化/
      OLED_Init(); //OLED初始化
      MyRTC_Init(); //RTC初始化
      /显示静态字符串/
      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(); //RTC读取时间,最新的时间存储到MyRTC_Time数组中
      OLED_ShowNum(1, 6, MyRTC_Time[0], 4); //显示MyRTC_Time数组中的时间值,年
      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); //显示32位的秒计数器
      OLED_ShowNum(4, 6, RTC_GetDivider(), 10); //显示余数寄存器
      }
      }
      1. 强制关闭电压调节器,1.8V电源直接关断,意味着内部的寄存器数据全部丢失
      • 如何唤醒
          1. WKUP的引脚产生上升沿、
          1. RTC闹钟事件
          1. NRST引脚上的外部复位
          1. IWDG复位

      5. 低功耗模式选择框图

      执行WFI(Wait For Interrupt)或者 WFE(Wait For Event)指令后,STM32进入低功耗模式。(这两条指令为最终开启低功耗模式的触发条件)
      notion image
      可以看到。
      一旦WFI 或者 WFE 执行了。 芯片是通过判断这些标志位来进入低功耗的三种模式的。
      • SLEEPDEEP 位 决定是否进入深度睡眠。0为浅睡眠模式(睡眠模式)、1为深度睡眠模式(停机或待机)
        • 在睡眠模式时
          • SLEEPONEXIT位的0、1标志可以决定立刻睡眠,还是等待中断事件处理结束后才睡眠。一般不用,只有在中断中调用睡眠模式,才会考虑这个标志位。
        • 在深度睡眠模式时
          • 判断PDDS位,为0则进入停机。为1则进入待机模式
            • 停机模式下,会继续判断LPDS位,为0则开启电压调节器。为1则电压调节器开启低功耗(更省电,但唤醒时间更长)
      使用睡眠模式时,一般是直接使用使用__WFI来配置为睡眠模式,对于刚才所说的SLEEPDEEP = 0和 SLEEPONEXIT 的位 使用默认的就可以了。所以是不需要配置的。直接输入__WFI 而停止模式和待机模式,都有对应的函数可以直接一键配置
      要是真想去配置的话,输入SCB->SCR = 0x….; 就可以配置这些位了
      notion image

      6. 低功耗三种模式总结

      6.1 睡眠模式

      • 执行完WFI/WFE指令后,STM32进入睡眠模式,程序暂停运行,唤醒后程序从暂停的地方继续运行
      • SLEEPONEXIT位决定STM32执行完WFI或WFE后,是立刻进入睡眠,还是等STM32从最低优先级的中断处理程序中退出时进入睡眠
      • 在睡眠模式下,所有的I/O引脚都保持它们在运行模式时的状态
      • WFI指令进入睡眠模式,可被任意一个NVIC响应的中断唤醒
      • WFE指令进入睡眠模式,可被唤醒事件唤醒

      6.2 停机模式

      • 执行完WFI/WFE指令后,STM32进入停止模式,程序暂停运行,唤醒后程序从暂停的地方继续运行
      • 1.8V供电区域的所有时钟都被停止,PLL、HSI和HSE被禁止,SRAM和寄存器内容被保留下来
      • 在停止模式下,所有的I/O引脚都保持它们在运行模式时的状态
      • 当一个中断或唤醒事件导致退出停止模式时,HSI被选为系统时钟 所以在停止模式唤醒之后,第一时间就是重启HSE,配置主频为72MHZ
      • 当电压调节器处于低功耗模式下,系统从停止模式退出时,会有一段额外的启动延时
      • WFI指令进入停止模式,可被任意一个EXTI中断唤醒
      • WFE指令进入停止模式,可被任意一个EXTI事件唤醒

      6.3 待机模式

      • 执行完WFI/WFE指令后,STM32进入待机模式,唤醒后程序从头开始运行
      • 整个1.8V供电区域被断电,PLL、HSI和HSE也被断电,SRAM和寄存器内容丢失,只有备份的寄存器和待机电路维持供电
      • 在待机模式下,所有的I/O引脚变为高阻态(浮空输入)
      • WKUP引脚的上升沿、RTC闹钟事件的上升沿、NRST引脚上外部复位、IWDG复位退出待机模式
      • 在进入待机模式之前,一般都需要把所有的外设 都关闭,比如屏幕、电机等等。这样才能达到 最最最省电的效果

      7. STM32各个状态下的电量消耗

      7.1 睡眠模式

      睡眠模式下,各个频率的电流 是mA级别的电流。
      notion image

      7.2 停机模式和待机模式

      电流变为了μA级别。这是非常省电的。
      对于一个300mAh的电池:睡眠(10h),停机(1w h),待机(10w h), 的使用时间能相差很大!
      备份区域的供电 消耗电量也很小,仅仅1μA
      notion image

      8. 修改系统主频的方法

      修改主频可以有效的降低功耗。并且在停机模式下唤醒,也需要修改主频到72MHZ
      这个需要去system_stm32f10x.csystem_stm32f10x.h 中查看学习

      7.1 system_stm32f10x.c中可调用的函数

      • system_stm32f10x.c 文件中有两个外部可调用的函数和一个外部可调用的变量.(可以在头文件中看到。)
        • 两个函数是SystemInit()SystemCoreClockUpdate()
          一个变量是SystemCoreClock
      • SystemInit() 是用来配置时钟树的,他是在执行main函数之前,在启动文件中调用的。
      • SystemCoreClock 表示主频频率的值。如果我们要显示当前频率就可以直接调用显示
      • SystemCoreClockUpdate() 更新上边的SystemCoreClock 的值。

      7.2 system_stm32f10x.c中的宏定义

      解除对应的注释,来选择想要的系统主频(带有钥匙的文件无法修改,需要取消文件的只读属性)
      这个if和 lese的意思是:只要你不是VL(超值系列)。就可以配置那么多的主频(lese下边的)
      notion image

      [扩展]STM32配置系统时钟时都做了什么

      系统在进入main函数之前,会由system_stm32f10x.c函数来配置系统时钟。
      首先system会启动HSI内部高速时钟
      然后会进行各种恢复缺省配置(比如)
      最后调用SetSysClock函数(这是一个分配函数,根据我们之前解除的宏定义,来选择执行不同的配置函数。比如SetSysClockTo72、To56、To48…..)
      在执行的配置函数中。才会真正的对stm32的寄存器开始配置。
      比如To72的配置为:选择HSE外部告诉始终作为锁相环输入,锁相环进行9倍频 再选择锁相环输出作为主频。

      9. 编写:睡眠模式+串口收发数据

      9.1 为什么一定是睡眠模式呢?

      • 因为在停机模式和待机模式下。会关闭1.8V区域的时钟。这就导致了USART外设不能接收数据而产生中断。 并且停机模式和待机模式只能是由外部中断唤醒的。
      所以只能使用睡眠模式,因为睡眠模式仅仅是关闭了CPU的时钟。使程序不再往下运行。1.8V供电区域和电压调节器都是开着的。可以使用USART的中断进行唤醒。

      9.2 代码框架介绍

      • main.c 仅仅添加了一行代码:__WFI 中断睡眠。 其余的不用配置,使用默认为0的就行(睡眠模式0 + 立刻睡眠0) 另外还加入了Running来验证程序是否进入睡眠模式
      • 执行流程:__WFI()后,睡眠,串口发送数据,产生中断,唤醒cpu,执行中断,执行主函数,遇到__WFI() ,睡眠,
      • Serial.c 串口收发数据
      • Serial.h 串口收发数据头文件
      • 注意事项:如果Delay.c中的延时实现是靠SysTick滴答定时器中断实现的。这个中断也会触发唤醒操作。需要关闭SysTick的中断

      main.c

      Serial.c

      int fputc(int ch, FILE *f)
      {
      Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数
      return ch;
      }
      /
      • 函 数:自己封装的prinf函数
      • 参 数:format 格式化字符串
      • 参 数:... 可变的参数列表
      • 返 回 值:无
      */
      void Serial_Printf(char *format, ...)
      {
      char String[100]; //定义字符数组
      va_list arg; //定义可变参数列表数据类型的变量arg
      va_start(arg, format); //从format开始,接收参数列表到arg变量
      vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中
      va_end(arg); //结束变量arg
      Serial_SendString(String); //串口发送字符数组(字符串)
      }
      uint8_t Serial_RxData;
      uint8_t Serial_RxFlag;
      uint8_t Serial_GetRxfalg(void)
      {
      if(Serial_RxFlag == 1)
      {
      Serial_RxFlag = 0;
      return 1;
      }
      return 0;
      }
      uint8_t Serial_GetRxData(void)
      {
      return Serial_RxData;
      }
      void USART1_IRQHandler(void)//中断
      {
      if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
      {
      Serial_RxData = USART_ReceiveData(USART1);//读取
      Serial_RxFlag = 1;
      USART_ClearITPendingBit(USART1,USART_FLAG_RXNE);//手动清除标志位
      }
      }
      #ifndef __SERIAL_H
      #include "stm32f10x.h"
      #include "stdio.h"
      #include "stdarg.h"
      #define __SERIAL_H
      //初始化串口
      void Serial_Init(void);
      //发送字节
      void Serial_SendByte(uint16_t Byte);
      //发送数组
      void Serial_SendArray(uint8_t* Array,uint16_t len);
      //发送字符串
      void Serial_SendString(char* String);
      //发送字符形式的数字
      void Serial_SendNumber(uint32_t Number,uint8_t len);
      //移植printf
      void Serial_Printf(char *format, ...);
      //获取输入的RXNE标志位
      uint8_t Serial_GetRxfalg(void);
      //获取输入的字符
      uint8_t Serial_GetRxData(void);
      #endif
      #include "stm32f10x.h" // Device header
      #include "Delay.h"
      #include "oled.h"
      #include "Delay.h"
      #include "CountSensor.h"
      int main()
      {
      OLED_Init();//初始化OLED;
      CountSensor_Init();//初始化
      RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);//开启PWR外设
      OLED_ShowString(1,1,"Count:");
      while(1)
      {
      OLED_ShowSignedNum(1, 7,CountSenSor_Get(),5);
      OLED_ShowString(2,1,"Running");
      Delay_ms(100);
      OLED_ShowString(2,1," ");
      Delay_ms(100);
      PWR_EnterSTOPMode(PWR_Regulator_ON,PWR_STOPEntry_WFI);//开启电压调节器, WFI模式进入
      SystemInit();//重新启动主频
      }
      }
      #include "stm32f10x.h" // Device header
      //引脚为A2
      //计次变量
      uint16_t CountSensor_Count = 0;
      //初始化
      void CountSensor_Init(void)
      {
      //开启A2的时钟
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
      //开启挂载在APB2外设的AFIO外设时钟
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
      //EXTI时钟和NVIC的时钟不用手动打开。
      //配置GPIO
      GPIO_InitTypeDef GPIO_InitStructrue;
      GPIO_InitStructrue.GPIO_Mode = GPIO_Mode_IPU; //上拉输入模式
      GPIO_InitStructrue.GPIO_Pin = GPIO_Pin_2;
      GPIO_InitStructrue.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_Init(GPIOA,&GPIO_InitStructrue);
      //配置AFIO (F1中在GPIO.h文件中)(目的是为了把GPIOA的PIN2引脚映射到AFIO中。)
      GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource2);//这里需要根据PIn和GPIOx来选择
      //配置EXTI
      EXTI_InitTypeDef EXTI_InitStructure;
      EXTI_InitStructure.EXTI_Line = EXTI_Line2; //选择中断线路,这里是PIN2 所以为2
      EXTI_InitStructure.EXTI_LineCmd = ENABLE; //是否使能指定的中断线路
      EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //中断或响应模式
      EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//上升或下降或边沿触发
      EXTI_Init(&EXTI_InitStructure);
      //配置NVIC(因为NVIC属于内核,所以被分配到内核的杂项中去了,在misc.c)
      NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置抢占和响应优先级
      NVIC_InitTypeDef NVIC_InitStructure;
      NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; //在stm32f10x.h文件里。让你找IRQn_Type里的一个中断通道。这里使用的是md的芯片(如果引脚是15-10或者9-5则需要去找对应的那个)这里我是PIN2.所以找2
      NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //是否使能指定的中断通道
      NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//抢占优先级(这里可以看表。看范围,)
      NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定响应优先级
      NVIC_Init(&NVIC_InitStructure);
      }
      //获取当前的计数
      uint16_t CountSenSor_Get(void)
      {
      return CountSensor_Count;
      }
      //在STM32中,中断的函数都是固定的。他们在启动文件中存放xxx.s
      //以IRQHandler结尾的就是中断函数的名字。
      //在这里需要找到对应的中断函数,我这里是2
      void EXTI2_IRQHandler(void)//中断函数是无参无返回值的。中断函数必须写对,写错就进不去
      {
      //在进入中断后,一般要判断一下这个是不是我们想要的那个中断源触发的中断。
      //但是在这里。我是GPIOA的PIN2引脚,所以不用写。
      //如果是5-9 10-15的引脚。他们EXTI到NVIC是几个共用的。
      //所以需要根据EXTI输入时的16根引脚。来判断是16根引脚的那一根发送的中断请求。
      //这里规范写的话需要加上去
      //查找标志位函数在exit.h中。
      if(EXTI_GetITStatus(EXTI_Line2) == SET)//第一个参数是行数.判断这个线的标志位是不是== SET。是则是我们想要的
      {
      CountSensor_Count++;
      EXTI_ClearITPendingBit(EXTI_Line2);//中断结束后,要调用清除标志位的函数。如果你不清除,程序会一直进入中断
      }
      }
      #ifndef __COUNTSENSOR_H
      #define __COUNTSENSOR_H
      //初始化旋转编码计次
      void CountSensor_Init(void);
      //获取当前计数值
      uint16_t CountSenSor_Get(void);
      #endif
      #include "stm32f10x.h" // Device header
      #include "Delay.h"
      #include "OLED.h"
      #include "MyRTC.h"
      int main(void)
      {
      /模块初始化/
      OLED_Init(); //OLED初始化
      MyRTC_Init(); //RTC初始化
      /显示静态字符串/
      OLED_ShowString(1, 1, "CNT:"); //时间戳计数器
      OLED_ShowString(2, 1, "ALR:"); //闹钟值
      OLED_ShowString(3, 1, "ALRF:"); //闹钟标志位
      PWR_WakeUpPinCmd(ENABLE);
      //存下要显示的闹钟值(闹钟寄存器只可写不可读,所以要存下,方便显示)
      uint32_t Alarm = RTC_GetCounter() + 10;
      //设定闹钟为十秒后
      RTC_SetAlarm(Alarm);
      //显示闹钟
      OLED_ShowNum(2, 5, Alarm, 10);
      //开启PWR时钟
      RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
      while (1)
      {
      OLED_ShowNum(1, 5, RTC_GetCounter(), 10); //显示32位的秒计数器
      OLED_ShowNum(3, 6, RTC_GetFlagStatus(RTC_FLAG_ALR), 1); //显示32位的秒计数器
      OLED_ShowString(4,1,"Running");
      Delay_ms(100);
      OLED_ShowString(4,1," ");
      Delay_ms(100);
      //在进入待机模式之前,一般都需要把所有的外设 都关闭,比如屏幕、电机等等。
      //这里清个屏表示一下
      OLED_Clear();
      PWR_EnterSTANDBYMode();//进入待机模式
      }
      }
      #include "stm32f10x.h" // Device header
      #include <time.h>
      uint16_t MyRTC_Time[] = {2024, 8, 18, 23, 46, 0}; //定义全局的时间数组,数组内容分别为年、月、日、时、分、秒
      void MyRTC_SetTime(void); //函数声明
      /
      • 函 数:RTC初始化
      • 参 数:无
      • 返 回 值:无
      */
      void MyRTC_Init(void)
      {
      /开启时钟/
      RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟
      RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); //开启BKP的时钟
      /备份寄存器、RTC访问使能/
      PWR_BackupAccessCmd(ENABLE); //使用PWR开启对备份寄存器和RTC的访问
      if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) //通过写入备份寄存器的标志位,判断RTC是否是第一次配置
      //if成立则执行第一次的RTC初始化
      {
      RCC_LSEConfig(RCC_LSE_ON); //开启LSE时钟
      while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET); //等待LSE准备就绪
      RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择RTCCLK来源为LSE
      RCC_RTCCLKCmd(ENABLE); //RTCCLK使能
      RTC_WaitForSynchro(); //等待同步
      RTC_WaitForLastTask(); //等待上一次操作完成
      RTC_SetPrescaler(32768 - 1); //设置RTC预分频器,预分频后的计数频率为1Hz
      RTC_WaitForLastTask(); //等待上一次操作完成
      MyRTC_SetTime(); //设置时间,调用此函数,全局数组里时间值刷新到RTC硬件电路
      BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); //在备份寄存器写入自己规定的标志位,用于判断RTC是不是第一次执行配置
      }
      else //RTC不是第一次配置
      {
      RTC_WaitForSynchro(); //等待同步
      RTC_WaitForLastTask(); //等待上一次操作完成
      }
      }
      /
      • 函 数:RTC设置时间
      • 参 数:无
      • 返 回 值:无
      • 说 明:调用此函数后,全局数组里时间值将刷新到RTC硬件电路
      */
      void MyRTC_SetTime(void)
      {
      time_t time_cnt = 0; //定义秒计数器数据类型
      struct tm time_date; //定义日期时间数据类型
      time_date.tm_year = MyRTC_Time[0] - 1900; //将数组的时间赋值给日期时间结构体
      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];
      time_cnt = mktime(&time_date) - 8 60 60; //调用mktime函数,将日期时间转换为秒计数器格式
      //- 8 60 60为东八区的时区调整
      RTC_SetCounter(time_cnt); //将秒计数器写入到RTC的CNT中
      RTC_WaitForLastTask(); //等待上一次操作完成
      }
      /
      • 函 数:RTC读取时间
      • 参 数:无
      • 返 回 值:无
      • 说 明:调用此函数后,RTC硬件电路里时间值将刷新到全局数组
      */
      void MyRTC_ReadTime(void)
      {
      time_t time_cnt; //定义秒计数器数据类型
      struct tm time_date; //定义日期时间数据类型
      time_cnt = RTC_GetCounter() + 8 60 60; //读取RTC的CNT,获取当前的秒计数器
      //+ 8 60 60为东八区的时区调整
      time_date = *localtime(&time_cnt); //使用localtime函数,将秒计数器转换为日期时间格式
      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;
      }
      #ifndef __MYRTC_H
      #define __MYRTC_H
      extern uint16_t MyRTC_Time[];
      void MyRTC_Init(void);
      void MyRTC_SetTime(void);
      void MyRTC_ReadTime(void);
      #endif
      • 如果解锁,就置控制寄存器的MER(Mass Erase 大规模擦除)再置STRT(Start 开始)为1
      1. 判断状态控制器BSY(Busy 忙)是否为1 , 如果为1 就继续判断。等待他忙完(BSY = 0)跳出循环
      1. 最后一步验证一般不管。

      6.4 FLASH 闪存 页 擦除 时 过程

      notion image
      1. 读取LOCK位,看看芯片是否被锁,
          • 如果锁住,就执行解锁过程(在KEYR寄存器先写入KEY1,再写入KEY2)
          • 如果解锁,就置控制寄存器的PERPage Erase 大规模擦除)、 然后在AR(Address Register 地址寄存器)选择要擦除的页。 最后置STRT(Start 开始)为1
      1. 判断状态控制器BSY(Busy 忙)是否为1 , 如果为1 就继续判断。等待他忙完(BSY = 0)跳出循环
      1. 最后一步验证一般不管。

      6.5 FLASH 闪存 写入 时 过程

      notion image
      STM32的闪存在写入之前会检查指定地址有没有擦除。如果没有擦除就写入, STM32则不执行写入操作(除非写入的数据全是0)
      1. 读取LOCK位,看看芯片是否被锁,
          • 如果锁住,就执行解锁过程(在KEYR寄存器先写入KEY1,再写入KEY2)
          • 如果解锁,就置控制寄存器的PG(Programming 程序编制)表示我们即将写入数据、
      1. 在指定的地址写入半字(16位):*((__IO uint16_t *)(0x08000000)) = 0x1234; (不需要置STRT了)
      1. 判断状态控制器BSY(Busy 忙)是否为1 , 如果为1 就继续判断。等待他忙完(BSY = 0)跳出循环
      1. 最后一步验证一般不管。

      7. STM32 选项字节 的组织和用途

      notion image
      选项字节存储的区域只有16个字节。起始地址为0x1FFF F800
      其中有一半的字节前边都带了个n(比如USER和nUSER……)
      这个的意思是,在写入USER时,要同时写入nUSER的反码..其他都是一样
      只有芯片检测到这两个芯片是反码的关系才会执行相对应的功能。
      这是一个安全保障措施(硬件会自动计算反码并填入)
      每个存储器的功能
      • RDP:写入RDPRT键(0x000000A5)后解除读保护
      • USER:配置硬件看门狗和进入停机/待机模式是否产生复位
      • Data0/1:用户可自定义使用
      • WRP0/1/2/3:配置写保护,每一个位对应保护4个存储页(中容量)
        • WRP0位都是反逻辑位:0为实施写保护,1为取消写保护(因为闪存擦除之后都是1,1 是默认的)
        • ( (小容量产品32K)也是每位保护4页,所以只需要WRP0一个字节就够了)
          • ( (大容量产品512K)每位保护2页,但是WRP3的位7直接剩下把所有页(62~255页)全部保护)

      8. STM32 选项字节 的擦除和编程

      选项字节本身也是闪存,所以在写入前也要擦除。流程和程序存储器类似,但细节有些出入

      擦除 选项字节时 过程

      1. 解锁FLASH闪存 (手册中没写,但一定要打开 这个相当于 门口大锁)
      1. 检查FLASH_SR的BSY位,以确认没有其他正在进行的闪存操作 (事前等待)
      1. 解锁FLASH_CR的OPTWRE (Option Write Enable 选项写入使能)位 (相当于卧室小锁)
      1. 设置FLASH_CR的OPTER(Option Erase 选项清除)位为1 (即将擦除选项字节)
      1. 设置FLASH_CR的STRT(Start 开始)位为1 (触发芯片开始干活)
      1. 等待BSY位变为0 (等待忙完)
      1. 读出被擦除的选择字节并做验证
       

      编程 选项字节时 过程

      1. 解锁FLASH闪存 (手册中没写,但一定要打开 这个相当于 门口大锁)
      1. 检查FLASH_SR的BSY位,以确认没有其他正在进行的闪存操作 (事前等待)
      1. 解锁FLASH_CR的OPTWRE (Option Write Enable 选项写入使能)位 (相当于卧室小锁)
      1. 设置FLASH_CR的OPTPG(Option Programming 选项编程)位为1 (即将开始编程写入)
      1. 写入要编程的半字(16位)到指定的地址
      1. 等待BSY位变为0 (等待忙完)
      1. 读出被擦除的选择字节并做验证

      9 . STM32 FLASH 器件的电子签名

      电子签名存放在闪存存储器模块的系统存储区域(就是BootLoader哪里),包含的芯片识别信息在出厂时编写,不可更改,使用指针读指定地址下的存储器可获取电子签名
      闪存容量寄存器:(显示闪存容量) 基地址:0x1FFF F7E0 大小:16位
      产品唯一身份标识寄存器:( 可以用来作为序列号、防被盗、激活秘钥) 基地址: 0x1FFF F7E8 大小:96位 可以以字节、半字、字的方式读取

      10. STM32 ST-LINK Utility调试工具的使用

      1. 连接
          • 插入好ST link之后点击连接(不能在stm32休眠模式下连接)
      1. 退出
          • 在使用完ST-LINK Utility调试工具后要及时断开,不然KEIl下载不了程序
      1. 查看程序数据
          • 连接后,下面窗口中就是闪存中的数据了
          • 可以指定地址 查看数据、指定查看范围、指定以什么类型去看(字节、半字、字、)
      notion image
      1. 查看选项字节配置
          • 进入
          notion image
          • 功能介绍
          notion image

      11. KEIL FLASH 下载程序 需要注意的设置

      11.1 下载程序起始位置。划定范围

      在下载程序时,我们可以选定FLASH和RAM的起始位置。这样下载程序就可以指定位置下载。并且可以限制最大到那个位置,
      比如要下载一个自己写的BootLoader。就可以指定到页尾去下载。
      或者程序的最后几页,打算存储数据用,那么就可以通过划定程序的下载范围,不让下载时覆盖掉自己想保存的数据。 当然,如果程序过大,你划的范围幼小,那么会下载失败。
      notion image

      11.2 下载程序 时 对FLASH擦除选项

      一般使用用多少擦除多少。下载速度快且不会动后面的程序
      notion image

      11.3 查看程序占用大小

      在程序编译之后,会有一段:Program Size :……
      前三个数为占用FLASH程序存储的大小, 后俩数 是占用SRAM的大小
      notion image
       
      也可以在Target处直接双击,在.map文件拖到最后边。也有大小显示。
      notion image

      12. STM32 flash.h介绍

      flash.h中三个部分的介绍

      如下是flash.h中的函数声明
      notion image
      可以看到在flash.h中可以看到有三块区域。分别对应了
      • 这些是所有F10x设备机都可以使用的函数
      • 所有设备机都可以使用的、新的函数
      • 只有XL加大容量的设备才可以使用的、新的函数(有个预编译,定义了宏才有效)
      他们的产生原因是:
      • 期初在stm32中,最初 只有小容量LD、中容量MD、大容量HD
      • 之后,加大容量XL才推出。XL是添加了一块新的、独立的闪存。即有两块
        • 所以设计者命名新加的一块交Bank2 。与之对应,原来的小中大容量的那一块叫做Bank1
      • 在加大容量系列出来之后,对flash.c .h中的函数 进行了适配和更新(具体更改可以在.c中查看)
       

      适用于所有stm32F10x设备的函数(第一部分)函数介绍

      和内核运行代码有关,不需要过多了解
      void FLASH_SetLatency(uint32_t FLASH_Latency);
      void FLASH_HalfCycleAccessCmd(uint32_t FLASH_HalfCycleAccess);
      void FLASH_PrefetchBufferCmd(uint32_t FLASH_PrefetchBuffer);
      void FLASH_Unlock(void);
      • 解锁
      void FLASH_Lock(void);
      • 加锁
      FLASH_Status FLASH_ErasePage(uint32_t Page_Address);
      • 闪存擦除某一页(返回值为完成状态)
      FLASH_Status FLASH_EraseAllPages(void);
      • 闪存全擦除(返回值为完成状态)
      FLASH_Status FLASH_EraseOptionBytes(void);
      • 擦除选项字节(返回值为完成状态)
      FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);
      • 在指定地址写入字
      FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);
      • 在指定地址写入半字
      FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data);
      • 选项字节:自定义Data 0 、 1
      FLASH_Status FLASH_EnableWriteProtection(uint32_t FLASH_Pages);
      • 写保护
      FLASH_Status FLASH_ReadOutProtection(FunctionalState NewState);
      • 读保护
      FLASH_Status FLASH_UserOptionByteConfig(uint16_t OB_IWDG, uint16_t OB_STOP, uint16_t OB_STDBY);
      • 用户选项的三个配置位
      uint32_t FLASH_GetUserOptionByte(void);
      • 获取用户选项的三个配置位
      uint32_t FLASH_GetWriteProtectionOptionByte(void);
      • 获取写保护状态
      FlagStatus FLASH_GetReadOutProtectionStatus(void);
      • 获取读保护状态
      FlagStatus FLASH_GetPrefetchBufferStatus(void);
      • 获取预取缓冲区状态
      void FLASH_ITConfig(uint32_t FLASH_IT, FunctionalState NewState);FlagStatus
      • 中断使能
      FLASH_GetFlagStatus(uint32_t FLASH_FLAG);
      • 获取标志位
      void FLASH_ClearFlag(uint32_t FLASH_FLAG);
      • 清除标志位
      FLASH_Status FLASH_GetStatus(void);
      • 获取状态
      FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout);
      • 等待上一次操作(等待BSY) 函数内部会自动调用,并不需要我们单独调用

      13. 编写:读写内部FLASH

      11.1 工程目标

      在闪存最后一页进行读写。
      • 为了方便读写,提高效率。在SRAM中定义数组来对数组操作,通过函数间接控制FLASH。
      • SRAM在每次更改时,都把自己以整体备份到闪存中
      • 在每次上电时,把闪存中的 数据初始化加载到SRAM数组中
      • 另外为了判断此闪存是否之前保存过数据,使用页的第一个半字来存放标志位。如果标志位有,那么上电就直接加载闪存数据到SRAM中就可以了。如果不是就把标志位放进去,然后初始化闪存,再把闪存的数据搬运到SRAM中

      10.1 工程结构

      • MyFLASH.c :实现闪存最基本的三个功能:读取、擦除、编程
      • Store.c :实现对数据的读写和存储管理:定义SRAM数组,把SRAM数组自动备份到FLASH里。复位、上电后,闪存数据会自动读回道SRAM。
      • main.c : 测试读写内部FLASH

      MyFLASH.c

      MyFLASH.h

      Store.c

      Store.h

      main.c

      14. 编写:读写内部ID

      在13的基础上 只修改了main.c

      main.c

       
       
      上一篇
      STM32-HAL库学习笔记
      下一篇
      龙芯2k0300初探(从点灯到图像识别)

      评论
      Loading...
      目录