[STM32]笔记

目录

0 C语言

C语言宏定义

C语言typedef

C语言结构体

C语言枚举

时钟树

APB1

APB2

1 GPIO

1.1 输入

1.1.1 模拟输入

1.1.2 浮空输入

1.1.3 下拉输入

1.1.4 上拉输入

1.2 输出

1.2.1 开漏输出

1.2.2 推挽输出

1.2.3 复用开漏输出

1.2.4 复用推挽输出

⚫GPIO库函数

2 外部中断

2.1 EXTI外部中断

2.1.1 NVIC 嵌套向量中断控制器

⚫NVIC库函数

2.1.2 EXTI 外部中断/事件控制器

⚫EXTI库函数

🔴外部中断EXTI NVIC注解

3 TIM定时中断

3.1 TIM(Timer)定时器

基本定时器

通用定时器

高级定时器

定时器基本结构

预分频器时序

计数器时序

计数器无预装时序

RCC时钟树

⚫TIM库函数

🔴TIM定时器定时中断注解

3.2 TIM输出比较

⚫TIM输出比较库函数

🔴PWM注解

3.3 TIM输入捕获

频率测量

⚫PWMI库函数

🔴PWMI注解

3.4 编码器接口

4 ADC模数转换

⚫ADC库函数

🔴AD单通道注解

🔴AD多通道注解

5 DMA直接存储器存取

⚫DMA库函数

🔴DMA数据传输注解

🔴ADC连续扫描+DMA循环转运注解

6 USART串口

⚫USART库函数

🔴USART接收模式注解

🔴USART发送和接收模式注解

6.1 HEX数据包

🔴HEX接收过程注解

6.2 文本数据包

🔴文本接收过程注解

7 I2C通信

7.1 I2C时序

7.1.1 指定地址写

7.1.2 当前地址读

7.1.3 指定地址读

🔴软件I2C注解

🔴软件读写MUP60560指定地址读写注解

7.2 I2C通信外设

主机发送

主机接收

⚫I2C库函数

🔴硬件I2C注解

🔴指定地址读写软硬件区别注解

8 SPI通信

​编辑

8.1 SPI移位

8.2 SPI时序基本单元

模式1

模式0

模式2

模式3

8.3 时序

🔴模式选择注解

🔴 软件读写W25Q64注解

8.4 SPI外设

主模式全双工连续传输

非联系传输

⚫SPI库函数

🔴硬件读写W25Q64注解

9 Unix时间戳

9.1 UTC/GMT

9.2 时间戳转换

9.3 BKP

9.4 RTC


注:笔记主要参考B站 江科大自化协 教学视频“STM32入门教程-2023持续更新中”。

注:工程及代码文件:https://pan.baidu.com/s/179uqox3SHFl6K2TzJOP0uw?pwd=1234
提取码:1234

STM32 函数

0 C语言

C语言宏定义

  • 关键字:#define

  • 用途:用一个字符串代替一个数字,便于理解,防止出错;提取程序中经常出现的参数,便于快速修改

  • 定义宏定义:

    • #define ABC 12345

  • 引用宏定义:

    • int a = ABC; //等效于int a = 12345;

C语言typedef

  • 关键字:typedef

  • 用途:将一个比较长的变量类型名换个名字,便于使用

  • 定义typedef:

    • typedef unsigned char uint8_t;

  • 引用typedef:

    • uint8_t a; //等效于unsigned char a;

C语言结构体

  • 关键字:struct

  • 用途:数据打包,不同类型变量的集合

  • 定义结构体变量:

    • struct{char x; int y; float z;} StructName;

  • 因为结构体变量类型较长,所以通常用typedef更改变量类型名

  • 引用结构体成员:

    • StructName.x = 'A';

    • StructName.y = 66;

    • StructName.z = 1.23;

    • 或 pStructName->x = 'A'; //pStructName为结构体的地址

    • pStructName->y = 66;

    • pStructName->z = 1.23;

C语言枚举

  • 关键字:enum

  • 用途:定义一个取值受限制的整型变量,用于限制变量取值范围;宏定义的集合

  • 定义枚举变量:

    • enum{FALSE = 0, TRUE = 1} EnumName;

  • 因为枚举变量类型较长,所以通常用typedef更改变量类型名

  • 引用枚举成员:

    • EnumName = FALSE;

    • EnumName = TRUE;

 

时钟树

APB1

APB2

1 GPIO

1.1 输入

读取电口处的高低电平或电压,用于读写按键输入、外接模块电平信号、ADC电压采集、模拟通信协议接收数据

1.1.1 模拟输入

  • GPIO_Mode_AIN

  • 这种输入模式比较特殊,该模式主要为片上外设ADC而配置,从外部读取模拟信号,当设置为模拟输入时,GPIO会失效,引脚直接接入内部ADC。特点:相较于其他输入模式只能读取到逻辑高/低电平(数字量),该模式能读取到细微变化的值(模拟量)。通俗来讲就是,别的模式只能读取0和1,而模拟输入可以读取到0-1的变化区间。

  • 主要应用:所有要用到ADC模拟输入的外设(例如烟雾传感器,引脚需要接收模拟信号进而计算电压值),低功耗下省电。

1.1.2 浮空输入

  • GPIO_Mode_IN_FLOATING

  • 数据通道中仅接入TTL触发器(作用是将相对缓慢变化的模拟信号变成矩形信号)整形,随后输入输入数据寄存器。浮空输入状态下,IO的电平状态是不确定的,完全由外部输入决定,如果在该引脚悬空(无信号输入)的情况下,读取该端口的电平是不确定的。该种工作模式未接入任何上拉/下拉电阻。

  • 主要应用:可用于按键KEY实验、发送接收信号RX、TX、IIC、USART等。(但按键一般更常用到上拉下拉输入,待会再重点记录)。主要还是发送和接收信号的引脚常设置为浮空输入。

1.1.3 下拉输入

  • GPIO_Mode_IPD

  • 可读取引脚电平,与浮空输入相比,它内部连接下拉电阻,悬空时默认为低电平,其余相同。

1.1.4 上拉输入

  • GPIO_Mode_IPU

  • 可读取引脚电平,与浮空输入相比,它内部连接上拉电阻,悬空时默认为高电平,其余相同

  • 主要应用:

    • ①按键的使用

    • ②器件的外部中断(IRQ)引脚触发中断条件为下降沿触发/低电平触发,这样在无信号输入时始终保持高电平,如果有事件触发中断IRQ可以输出一个低电平,进而可产生(下降沿/低电平)中断,此时就可以将该引脚设置为上拉输出,使中断条件满足。

    • ③同理,器件的外部中断(IRQ)引脚触发中断条件为上升沿触发/高电平触发时,该端口可以选择下拉输入模式。

1.2 输出

  • 控制端口输出高低电平,用以驱动LED、蜂鸣器、模拟通信协议输出时序

1.2.1 开漏输出

  • GPIO_Mode_Out_OD

  • P-MOS是无效的,只有N-MOS在工作

    • 数据寄存器为1时,下管断开,输出相当于断开,也就是高阻态模式

    • 数据寄存器为0时,下管导通,输出直接接到VSS,也就是输出低电平

    • 主要应用:通信协议的驱动方式,入I2C的通信引脚,多机通信的模式下,可以避免各个设备相互干扰,还可以用于输出5V的电平信号

    • 输出低电平+浮空输入

1.2.2 推挽输出

  • GPIO_Mode_Out_PP

  • 输出具有驱动能力,当CPU输出逻辑0时,I/O端口输出低电平,而当CPU输出逻辑1时,I/O端口输出高电平。相当于可以给其他元件供电,并且可以直接通过逻辑语言0和1控制是否供电。

  • 数据寄存器为1时,上管导通,下管断开,输出直接接到VDD,输出高电平。

  • 数据寄存器为0时,上管断开,下管导通,输出直接接到VSS,输出低电平。

1.2.3 复用开漏输出

  • GPIO_Mode_AF_OD

  • 当有多个不同的模块对应同一个引脚时,那这个GPIO就要使用复用功能,其他方面与开漏输出相同。即如果用在IC、SMBUS这些需要线与功能的复用场合,就使用复用开漏模式。

    • 主要应用:片上外设功能(TX1、MOSI、MISO.SCK.SS)

1.2.4 复用推挽输出

  • GPIO_Mode_AF_PP

  • 和复用开漏输出同理,当某一个GPIO对应多个复用模块时,要用到复用功能。

    • 主要应用:片上外设功能(I2C的SCL、SDA)

	GPIO_InitTypeDef  GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC, ENABLE);	
	//使能PC13端口时钟
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;	   //PC13 端口配置, 推挽输出
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//IO口速度为50MHz
	GPIO_Init(GPIOC, &GPIO_InitStructure);	  		 //推挽输出 ,IO口速度为50MHz
	GPIO_SetBits(GPIOC,GPIO_Pin_13); 				//PC13 输出高 

⚫GPIO库函数

void GPIO_AFIODeInit(void);
//清除AFIO外设的配置
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
//锁定GPIO配置,防止意外更改
void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
void GPIO_EventOutputCmd(FunctionalState NewState);
//配置AFIO的事件输出功能
void GPIO_PinRemapConfig(uint32_t GPIO_Remap(重映射的方式), FunctionalState NewState(新的状态));
//进行引脚重映射
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
//配置AFIO的数据选择器,来选择我们想要的中断

2 外部中断

2.1 EXTI外部中断

2.1.1 NVIC 嵌套向量中断控制器

  • NVIC :嵌套向量中断控制器,属于内核外设,管理着包括内核和片上所有外设的中断相关的功能。

  • NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级抢占优先级高的可以中断嵌套

  • 响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队

⚫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);

2.1.2 EXTI 外部中断/事件控制器

  • EXTI(Extern Interrupt)外部中断

  • EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序

  • 支持的触发方式:上升沿/下降沿/双边沿/软件触发

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

  • 通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒

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

  • 每个GPIO外设都有16个引脚,所以进来16根线,但是EXTI只有的16个通道就不够用了,所以后面会有一个AFIO中断选择的电路模块(数据选择器),他可以在前面3个GPIO外设的16个引脚里选择其中一个连接到后面的EXTI的通道里,(前面说到相同的Pin不能同时触发中断,因为对于PA0、PB0、PC0这些,通过AFIO选择后,只有其中一个能接到EXTI的通道0上,PA1、PB1、PC1同理也只能有一个连接到通道1上)然后通过AFIO选择之后的16个通道,就接到了EXTI边沿检测及控制电路上,同时,下面四个蹭网的外设也是并列接进来的,相加就得到了EXTI的20个输入信号,通过EXTI电路之后,分为了两种输出,上面的接到了NVIC,用来触发中断的(9-5、15-10会分别触发一个中断函数,需要通过根据标志位来区分到底是哪个中断进来了)

  • EXTI的右边,就是20根输入线,然后输入线首先进入边沿检测电路,在上面的上升沿寄存器和下降沿寄存器可以选择是上升沿触发还是下降沿触发,或者两个都触发,接着触发信号就进入到这个或门的输入端,在这里,硬件触发和软件中断寄存器的值接到了这个或门上,任意为1,或门就可以输出1,之后,触发信号通过这个或门之后,就兵分两路,上一路是触发中断的,下一路是触发事件的,触发中断首先会置一个挂起寄存器,这相当于是一个中断标志位,可以读取这个寄存器判断是哪个通道触发的中断,如果中断寄存器置1,它会继续向左走,和中断屏蔽寄存器共同进入一个与门,然后是至NVIC中断控制器(与门相当于开关)

⚫EXTI库函数

void EXTI_DeInit(void);
//清除EXTI的配置,恢复成上电默认的状态
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
//配置结构体
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);
//清除中断挂起位

🔴外部中断EXTI NVIC注解

uint16_t CountSensor_Count;

void CountSensor_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
	//先择某个GPIO外设作为外部中断源
	
	EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line14;
	//指定我们要配置的中断线
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
	//开启中断,指定选择的中断线的新状态
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
	//中断模式,指定外部中断线的模式
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
	//下降沿触发,指定触发信号的有效边沿
	EXTI_Init(&EXTI_InitStructure);
	//将EXTI的第14个线路配置为中断模式,下降沿触发,然后开启中断
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	//2位抢占,2位响应
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
	//指定中断通道来开启或关闭,由于EXTI为14,选择EXTI15_10_IRQn
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	//指定中断通道是使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	//指定抢占优先级 0~3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	//指定响应优先级 0~3
	NVIC_Init(&NVIC_InitStructure);

}

uint16_t CountSensor_Get(void)
{
	return CountSensor_Count;//中断触发的次数
}

void EXTI15_10_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line14) == SET)//判断中断标志位
	{
		CountSensor_Count ++;
	}
	EXTI_ClearITPendingBit(EXTI_Line14);//清除中断标志位
}

3 TIM定时中断

3.1 TIM(Timer)定时器

  • 定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断

  • 16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s(72M / 65536 / 65536 得到中断频率,然后取倒数)的定时

  • 不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能

  • 根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型

STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4

基本定时器

  • 下面三个构成了最基本的计数计时器,所以这一块电路就叫做时基单元,预分频器之前,连接的就是基准计数时钟的输入(内部时钟CK_INT,来源自RCC的TIMxCLK,频率值一般都是系统的主频72MHz),预分频器可以对72MHz进行分频(1就是2分频),计数器可以对预分频后的计数时钟进行计数(每来一个上升沿,计数器就加一,0~65535),计数器的值会在计时过程中会不断地自增运行,当自增运行到目标值时,产生中断,那就完成了定时的任务,自动重装定时器存的就是写入的计数目标,在运行的过程中,计数值不断自增,自动重装值是固定的目标,当计数值等于自动重装值时,也就是计时时间到了,就会产生一个中断信号,并清零计数器,计数器自动开始下一次的计数计时

  • 采用向上计数模式

  • UI:更新中断 U:更新事件

通用定时器

  • 预分频器对时钟进行预分频,计数器自增计数,当计数值到自动重装值时,计数值清零同时产生更新终端和更新事件

  • 通用定时器和高级定时器还支持向下计数模式和中央对齐模式。向下计数模式就是从重装值开始,向下自减,减到0之后,回到重装值同时申请中断,然后继续下一轮,依次循环;中央对齐的计数模式,就是从0开始,先向上自增,计到重装值,申请中断,然后再向下自减,减到0,再申请中断,然后继续下一轮,依次循环。

  • 上面一部分就是内外时钟源选择和主从触发模式的结构

    • 内外时钟源选择:不止系统频率72MHZ,还可以选择外部时钟,

      • TIMx_ETR引脚上的外部时钟(PA0),可以接一个外部方波时钟,配置一下内部的极性选择、边检测和预分频器电路,再配置输入滤波电路,最后,滤波后的信号,兵分两路,上面一路ETRF进入触发控制器,紧跟着就可以选择作为时基单元的时钟【外部时钟模式2】;下面这里还有一路可以提供时钟,就是TRGI(Trigger In),主要是用作触发输入来使用的【外部时钟模式1】。

        • 外部时钟模式1:

          • 第一个,就是ETR引脚的信号,这里ETR引脚既可以通过上面这一路进来当做时钟,又可以通过下面这一路进来当做时钟;

          • 第二个,就是ITR信号,这一部分的时钟信号是来自其他定时器的,从右边可以看出,这个主模式的输出TRGO可以通向其他定时器,那通向其他定时器的时候,就接到了其他定时器的ITR引脚上来了,ITRO到ITR3分别来自其他4个定时器的TRGO输出

            【比如可以先初始化TIM3,然后使用主模式把它的更新事件映射到TRGO上,接着再初始化TIM2,这里选择ITR2,对应的就是TIM3的TRGO,然后后面再选择时钟为外部时钟模式1,这样TIM3的更新事件就可以驱动TM2的时基单元,也就实现了定时器的级联】;

          • 第三个,TI1F_ED,连接的是这里输入捕获单元的CH1引脚,也就是从CH1引脚获得时钟,这里后缀加一个ED(Edge)就是边沿的意思(也就是通过这一路输入的时钟,上升沿和下降沿均有效);

          • 最后,这个时钟还能通过TI1FP1和TI2FP2获得,其中TI1FP1是连接到了CH1引脚的时钟,TI1FP2是连接到了CH2引脚的时钟。

          • 总结一下就是,外部时钟模式1的输入可以是ETR引脚、其他定时器、CH1引脚的边沿、CH1引脚和CH2引脚

  • 下面这一部分主要包含了两块电路,

    • 左边这一块是输入捕获电路,总共有四个通道,分别对应CH1到CH4的引脚,可以用于测输入方波的频率

    • 右边这一块是输出比较电路,总共有四个通道,分别对应CH1到CH4的引脚,可以用于输出PWM波形,驱动电机

    • 中间这个寄存器是捕获/比较寄存器,是输入捕获和输出比较电路共用的,因为输入捕获和输出比较不能同时使用,所以这里的寄存器是共用的,引脚也是共用的

高级定时器

  • 相比于通用定时器,第一个是申请中断的地方,增加了一个重复次数计数器,有了这个计数器之后,就可以实现每隔几个计数周期,才发生一次更新事件和更新中断,

定时器基本结构

  • 最重要的PSC(Prescaler) 预分频器、CNT (Counter)计数器、ARR(AutoReloadRegister)自动重装器这三个寄存器构成的时基单元,

  • 下面这里是运行控制,就是控制寄存器的一些位(比如启动停止、向上或向下计数等等),

  • 左边是为时基单元提供时钟的部分,这里可以选择RCC提供的内部时钟,也可以选择ETR引脚提供的外部时钟模式2,当然还可以选择这里的触发输入当做外部时钟,即外部时钟模式1,对应的有ETR外部时钟、ITRx其他定时器、T输入捕获通道,这些就是定时器的所有可选的时钟源了,

  • 右边这里,就是计时时间到,产生更新中断后的信号去向(如果是高级定时器的话,还会多一个重复计数器),那这里中断信号会先在状态寄存器里置一个中断标志位,这个标志位会通过中断输出控制,到NMIC申请中断,这个中断输出控制就是一个中断输出的允许位,如果要需要某个中断,就记得允许一下

预分频器时序

  • CK_PSC,预分频器的输入时钟,选内部时钟的话一般是72MHz

  • CNT_EN,计数器使能,高电平计数器正常运行,低电平计数器停止

  • 定时器时钟CK_CNT,既是预分频器的时钟输出,也是计数器的时钟输入

    • 开始时,计数器未使能,计数器时钟不运行,然后使能后,前半段,预分频器系数为1,计数器的时钟等于预分频器前的时钟,后半段,预分频器系数变为2,计数器的时钟就也变为预分频器前时钟的一半了,

  • 在计数器时钟的驱动下,下面的计数器寄存器也跟随时钟的上升沿不断自增,在中间的这个位置FC之后,计数值变为0了(可以推断出ARR自动重装值就是FC),当计数值计到和重装值相等,并且下一个时钟来临时,数值才清零

  • 同时,下面这里产生一个更新事件,这就是一个计数周期的工作流程

  • 后三行是预分频存器的一种缓冲机制,也就是这个预分频寄存器实际上是有两个,一个是预分频控制寄存器,供我们读写用的,它并不直接决定分频系数,

  • 另外还有一个预分频缓冲器,缓冲寄存器才是真正起作用的寄存器,【比如我们在某个时刻,把预分频寄存器由0改成了1,如果在此时立刻改变时钟的分频系数,那么就会导致这里,在一个计数周期内,前半部分和后半部分的频率不一样,计数计到一半,计数频率突然就会改变了,改变了分频值,这个变化并不会立刻生效,而是会等到本次计数周期结束时,产生了更新事件,预分频奇存器的值才会被传递到缓冲寄存器里面去,才会生效】所以即使在计数中途改变了预分频值,计数频率仍然会保持为原来的频率,直到本轮计数完成,在下一轮计数时,改变后的分频值才会起作用,【不会被打断

  • 预分频计数器内部实际上也是靠计数来分频的,当预分频值为0时,计数器就一直为0,直接输出原频率,当预分频值为1时,计数器就0、1、0、1、0、1这样计数,在回到0的时候,输出一个脉冲,

  • 这样输出频率就是输入频率的2分频,预分频器的值和实际的分频系数之间有一个数的偏移

  • 计数器计数频率:CK_CNT = CK_PSC(72MHz) / (PSC + 1)

计数器时序

  • CK_INT,内部时钟72MHz

  • CNT_EN,计数器使能,高电平计数器正常运行,低电平计数器停止

  • 计数器时钟CK_CNT ,因为分频系数为2,所以这个频率是CK_INT除2,

  • 计数器寄存器在这个时钟每个上升沿自增,当增到0036的时候,发生溢出,那计到36之后,再来一个上升沿,计数器清零

  • 计数器溢出

  • 产生一个更新事件脉冲

  • 另外还会置一个更新中断标志位UIF,这个标志位只要置1了,就会去申请中断,然后中断响应后,需要在中断程序中手动清零

  • 计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1)

    = CK_PSC / (PSC + 1) / (ARR + 1)【如果想算溢出时间,只需再取个倒数】

计数器无预装时序

计数器有预装时序

RCC时钟树

⚫TIM库函数

/*时基单元*/
void TIM_DeInit(TIM_TypeDef* TIMx);
//恢复缺省配置   	
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
//时基单元初始化  

void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
//把结构体变量赋默认值

/*运行控制*/
void TIM_Cmd(TIM_TypeDef* TIMx(选择定时器), FunctionalState NewState(新的状态  使能/失能));
//使能计数器 

/*中断输出控制*/
void TIM_ITConfig(TIM_TypeDef* TIMx(选择定时器), uint16_t TIM_IT(选择配置哪个终端输出), FunctionalState NewState(新的状态  使能/失能));
//使能中断输出信号

/*时基单元的时钟选择部分*/
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
//选择RCC内部时钟
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx(选择定时器), uint16_t TIM_InputTriggerSource(选择要接入哪个其他的定时器));
//选择ITRx其他定时器
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx(选择定时器), uint16_t TIM_TIxExternalCLKSource(选择具体的某个引脚),uint16_t TIM_ICPolarity(输入的极性), uint16_t ICFilter(输入的滤波器));
//选择TIx捕获通道的时钟
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler(外部触发预分频器 对ETR的外部时钟再提前做一个分频), 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引脚的预分频器、极性、滤波器

void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler(要写入的预分频值), uint16_t TIM_PSCReloadMode(写入的模式));
//单独写预分频值
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);
//改变计数器的计数模式
void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
//自动重装器预装功能配置
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);
//给计数器写入一个值
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);
//给自动重装器写入一个值
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);
//获取当前计数器的值
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);
//获取当前预分频器的值

🔴TIM定时器定时中断注解

extern uint16_t Num;

void Timer_Init(void)
{
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
	
	TIM_InternalClockConfig(TIM2);
	
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//不分频
	//指定时钟分频,在一个固定的时钟频率进行采样,如果连续N个采样点都为相同的电平,那就代表输入信号稳定了,就把这个值输出出去,反之说明信号抖动
	//采样频率f,可以是由内部时钟直接而来,也可以是由内部时钟加一个时钟分频而来,所以分频多少就是由TIM_ClockDivision决定的
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	//向上计数
	TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
	//ARR自动重装器的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
	//PSC预分频器的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
	//重复寄存器的值
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
	
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
	//开启了更新中断到NVIC的通路
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	//2位抢占,2位响应
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
	//指定中断通道来开启或关闭,由于TIM2,选择TIM2_IRQn
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	//指定中断通道是使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	//指定抢占优先级 0~3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	//指定响应优先级 0~3
	NVIC_Init(&NVIC_InitStructure);
	
	TIM_Cmd(TIM2, ENABLE);
	
}

void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)//检查中断标志位,更新中断
	{
		Num ++;
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);//清除标志位
	}
}

3.2 TIM输出比较

  • OC(Output Compare)输出比较

    • 输出比较可以通过比较CNT与CCR寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形

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

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

  • PWM(Pulse Width Modulation)脉冲宽度调制

    • 在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域

    • PWM参数:

      • 频率 = 1 / TS 占空比 = TON / TS 分辨率 = 占空比变化步距

  • CNT计数器和CCR1第一路的捕获比较寄存器,当CNT>CCR1,或者CNT=CCR1时,就会给输出模式控制器传一个信号,然后输出模式控制器就会改变它输出OC1REF的高低电平(REF信号实际上就是指这里信号的高低电平,reference参考信号),上面的ERTF输入,是定时器的一个小功能,然后,接着这个REF信号可以前往主模式控制器,可以把这个REF映射到主模式的TRGO输出上去,通过下面一路到达极性选择,给这个寄存器写0,信号就会往上走,就是信号电平不翻转,写1就会往下走,信号通过一个非门取反,那输出的信号就是输入信号高低电平反转的信号,就是选择是不是要把高低电平反转一下,OC1引脚,就是CH1通道的引脚

一般使用向上计数,

  • PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1) 72000000 / 720 / 100 = 1000

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

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

  • 频率为1KHz,占空比为50%,分辨率为1% 的PWM波形

  • 具体步骤

    • 第一步,RCC开启时钟,把我们要用的TIM外设和GPIO外设的时钟打开

    • 第二步,配置时基单元,包括这前面的时钟源选择

    • 第三步,配置输出比较单元,包括这个CCR的值、输出比较模式、极性选择、输出使能这些参数

    • 第四步,配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置

    • 第五步,启动计数器,这样就能输出PWM

⚫TIM输出比较库函数

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);
//配置输出比较
void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);
//给输出比较结构体赋一个默认值
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
//仅使用高级定时器输出PWM时需要调用
//否则PWM将不能正常输出
//使能主输出
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_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);      //OC4无互补通道
//单独设置输出比较极性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);
//配置强制输出模式
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);      //OC4无互补通道
//单独设置输出比较极性
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);
//单独更改输出比较模式

🔴PWM注解

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); 
	//开启 APB1 外设总线上的外设时钟,RCCAPB1Periph_TIM2 表示要开启 TIM2 的时钟,ENABLE 表示要开启时钟。
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	//开启 APB2 外设总线上的外设时钟,RCCAPB2Periph_GPIOA 表示要开启 GPIOA 的时钟,ENABLE 表示要开启时钟
	TIM_InternalClockConfig(TIM2);						
	//配置 TIM2 的计数器时钟源为内部时钟,即 APB1 时钟。这里没有指定具体的计数器时钟频率,所以默认使用的是 APB1 时钟频率,一般为 CPU 主频(如常见的 72MHz)。

	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;	 
	//设置计数器时钟分割(()即分频器)系数为1,即不分频。
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
	//设置 TIM2 的计数模式为向上计数模式,即计数器从 0 开始递增,当计数器达到设定的溢出周期后自动清零,并发出中断或者触发其他事件。
	TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;    
	//设置 TIM2 的溢出周期为 10000-1,即当计数器计数到 10000 时发生更新事件,同时计数器清零重新开始计数。
	TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;    
	//设置 TIM2 的分频器系数为 72-1,即计数器时钟频率为 APB1 时钟频率除以 72。这里使用了上面提到的分频系数 1 和主频为 72MHz 的情况下,所得到的计数器时钟频率为 1MHz。
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; 
	//TIMRepetitionCounter 变量没有实际用途,只是一个保留字段。明确将TIM2的 TIM_RepetitionCounter值设置为0,即使其无效化,避免误用。
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);	 
	//将配置好的 TIM_TimeBaseInitStructure 结构体应用到 TIM2 上,从而完成对 TIM2 的基本配置。

	TIM_OCInitTypeDef TIM_OCInitStructure;
	TIM_OCStructInit(&TIM_OCInitStructure);				
	//将 TIM_OCInitStructure 结构体变量中的所有成员设置为默认值,以避免未经初始化的寄存器值可能会导致不可预知的问题。
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;	
	//设置 TIM2 输出通道2 的工作模式为 PWM1 模式,即高电平持续时间小于周期的情况下输出 PWM 信号,常用于控制电机等设备。
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;    	
	//设置 PWM 信号的极性为高电平有效。
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
	//设置 TIM2 输出通道2 的使能状态为使能,即允许输出 PWM 信号.
	TIM_OCInitStructure.TIM_Pulse = 0;      
	//CCR //设置 PWM 信号的占空比初始值为0,即输出一直保持低电平。
	TIM_OC1Init(TIM2, &TIM_OCInitStructure);
	//将配置好的 TIM_OCInitStructure 结构体应用到 TIM2 输出通道2 上,使之能够输出 PWM 信号。

	TIM_ClearFlag(TIM2, TIM_FLAG_Update);		
	//清除计数器中断标志位
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);	
	//开启计数器中断

	TIM_Cmd(TIM2, ENABLE);						
	//用于启动或停止指定定时器的函数

3.3 TIM输入捕获

  • IC(Input Capture)输入捕获

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

    • 每个高级定时器和通用定时器都拥有4个输入捕获通道

    • 可配置为PWMI模式,同时测量频率和占空比

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

频率测量

  • 测频法:在闸门时间T内,对上升沿计次,得到N,则频率

    • f_x=N / T

  • 测周法:两个上升沿内,以标准频率fc计次,得到N ,则频率

    • f_x=f_c / N

  • 中界频率:测频法与测周法误差相等的频率点

    • f_m=√(f_c / T)

  • 时基单元配置好,启动定时器,CNT就会在预分频之后的这个时钟驱动下,不断自增,(CNT:测周法用来计数的东西)经过预分频之后的时钟频率,就是驱动CNT的标准频率fc(标准频率 = 72M / 预分频系数),之后,下面输入捕获通道1 的GPIO后,输入一个如图的方波信号,经过滤波器和边沿检测,选择TI1FP1设置为上升沿触发,之后选择直连的通道,分频器为不分频,当TI1FP1出现上升沿之后,CNT的当前计数值转运到CCR1里,同时触发源选择,选中TI1FP1为触发信号,从模式选择复位操作,这样TI1FP1的上升沿就会沿着上面的这一路去触发CNT清零(当然是先转运CNT的值到CCR中,在触发从模式给CNT清零,或者是非阻塞的同时转移,CNT的值转移到CCR,同时0转移到CNT里面去,总之,不会是先清零,再捕获,要不捕获的肯定是0)

  • 电路工作时,CCR1的值始终保持为最新一个周期的计数值(N),计算频率只需要fc / N

  • ARR最大为65535,CNT最大65535

  • TI1FP2,配置为下降沿触发,通过交叉通道去触发通道2 的捕获单元

  • 使用两个通道来捕获频率和占空比

    • CCR1:一整个周期的计数值

    • CCR2:高电平期间的计数值

    • 占空比:CCR2 / CCR1

  • 步骤

    • 第一步,RCC开启时钟,把GPIO和TIM的时钟打开

    • 第二步,GPIO初始化,把GPIO配置成输入模式,一般选择上拉输入或者浮空输入模式

    • 第三步,配置时基单元,让CNT计数器在内部时钟的驱动下自增运行

    • 第四步,配置输入捕获单元,包括滤波器、极性、直连通道还是交又通道、分频器这些参数

    • 第五步,选择从模式的触发源,触发源选择为TI1FP1,这里调用一个库函数,给一个参数就行了

    • 第六步,选择触发之后执行的操作,执行Reset操作,这里也是调用一个库函数


⚫PWMI库函数

void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
//用结构体配置输入捕获单元(单一配置一个通道)
void TIM_PWMIConfig(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
//把外设电路结构配置为PWMI模式
//用结构体配置输入捕获单元 (快速配置两个通道)
void TIM_ICStructInit(TIM_ICInitTypeDef* TIM_ICInitStruct);
//给输入捕获结构体赋一个初始值
void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);
//选择主模式输出的触发源
//选择输出触发源TRGO
void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
//选择从模式输出的触发源
//选择输出触发源TRGI
void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
//选择从模式
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的分配器
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);
//分别读取1、2、3、4通道的CCR

🔴PWMI注解

TIM_ICInitTypeDef TIM_ICInitStructure;
	TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;		//选择通道
	TIM_ICInitStructure.TIM_ICFilter = 0xF;					//选择输入比较的滤波器
	TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;	//边沿检测,极性选择	上升沿
	TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;		//不分频
	TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;	//触发引脚从哪个信号输入	直连通道
	TIM_ICInit(TIM3, &TIM_ICInitStructure);
	
	TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);			//触发源选择
	TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);			//配置从模式

自动把剩下的一个通道初始化为相反的配置

3.4 编码器接口

  • 第一步,RCC开启时钟,开启GPIO和定时器的时钟。

  • 第二步,配置GPIO,6-8中把PA6和PA7配置为输入模式。

  • 第三步,配置时基单元,这里的预分频器一般选择不分配,自动重装,一般给最大65535,只需要个CNT执行计数就行了。

  • 第四步,配置输入捕获单元,这里输入捕获单元只有滤波器和极性两个参数有用。

  • 第五步,配置编码器接口模式,调用库函数。

  • 最后,调用TIM_Cmd,启动定时器。

	TIM_ICInitTypeDef TIM_ICInitStructure;        //定义结构体变量
	TIM_ICStructInit(&TIM_ICInitStructure);		  //给结构体赋一个初始值
	TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
	TIM_ICInitStructure.TIM_ICFilter = 0xF;
	//TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
	TIM_ICInit(TIM3, &TIM_ICInitStructure);		  //调用ICInit配置电路,写入了硬件,之后换个值继续写入
	TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
	TIM_ICInitStructure.TIM_ICFilter = 0xF;
	//TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
	TIM_ICInit(TIM3, &TIM_ICInitStructure);

	TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);//第三 第四与上面相同

//定时器编码器接口配置
void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx, uint16_t TIM_EncoderMode,uint16_t TIM_IC1Polarity, uint16_t TIM_IC2Polarity);

4 ADC模数转换

  • ADC(Analog-Digital Converter)模拟-数字转换器

  • ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁

  • 12位逐次逼近型ADC,1us转换时间

  • 输入电压范围:0~3.3V,转换结果范围:0~4095

  • 18个输入通道,可测量16个外部和2个内部信号源

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

  • 模拟看门狗自动监测输入电压范围,例如温度和光照

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

  • 左边是输入通道,16个GPIO口,外加两个内部的通道;然后进入AD转换器,AD转换器中有两个组,一个是规则组(最多选择16个通道),一个是注入组(最多选择4个通道),然后转换的结果可以存放在AD数据寄存器中,其中规则组只有1个数据寄存器,注入组有4个;下面有触发控制,提供了开始转换这个START信号,其中可以选择软件触发和硬件触发(硬件触发主要来自于定时器,当然也可以选择外部中断的引脚),右边是来自RCC的ADC时钟CLOCK(ADC逐次比较的过程就是由这个时钟推进的);然后上面,可以布置一个模拟看门狗用于检测转换结果的范围,如果超出设定的阈值,就通过中断输出控制,向NVIC申请中断,另外,规则组和注入组转换完成后会有个EOC信号,它会置一个标志位,当然也可以通向NVIC;最后右下有一个开关控制(ADC_Cmd函数)。

  • 第一步,开启RCC时钟,包括ADC和GPIO的时钟,ADCCLK的分频器,也需要配置一下

  • 第二步,配置GPIO,把需要用的GPIO配置成模拟输入的模式

  • 第三步,配置这里的多路开关,把左边的通道接入到右边的规则组列表里

  • 第四步,就是配置ADC转换器,包括ADC是单次转换还是连转换、扫描还是非扫描、有几个通道、触发源是什么,数据对齐是左对齐还是右对齐

  • 最后,就是开关控制,调用一下ADC_Cmd函数,开启ADC

⚫ADC库函数

//配置ADCCLK分频器
void RCC_ADCCLKConfig(uint32_t RCC_PCLK2);
//可以对APB2的72MHz时钟选择2、4、6、8分频,输入到ADCCLK

//开启DMA输出信号
void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState);

//中断输出控制
void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState);

//控制校准(ADC初始化后,依次调用)
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软件开转换控制
FlagStatus ADC_GetSoftwareStartConvStatus(ADC_TypeDef* ADCx);
//ADC获取软件开转换控制

//配置间断模式
void ADC_DiscModeChannelCountConfig(ADC_TypeDef* ADCx, uint8_t Number);
//每隔几个通道间断一次
void ADC_DiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
//是不是启用间断模式

//ADC规则组通道配置
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime(指定通道的采样时间));
//给系列的每个位置填写指定通道

//ADC外部触发转换控制
void ADC_ExternalTrigConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
//是否允许外部触发转换

//ADC获取转换值
uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);
//读取转换结果

//ADC获取双模式转换值
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);
//配置看门的通道

//ADC温度传感器、内部参考电压控制
void ADC_TempSensorVrefintCmd(FunctionalState NewState);
用来开启内部的两个通道

//标志位、中断
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单通道注解

#include "stm32f10x.h"                  // Device header
  
void AD_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//开启ADC1时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//开启GPIOA的时钟
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);//进行6分频
	
	//配置GPIO
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//选择规则组的输入通道
	//在规则组菜单列表的第一个位置,写入通道0这个通道
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
	
	//用结构体初始化ADC
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;	  	//独立模式 各转换各的
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    //触发控制的选择源:不使用外部触发
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;		//连续转换模式
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;	   		//扫描模式
	ADC_InitStructure.ADC_NbrOfChannel = 1;			   		//通道数目
	ADC_Init(ADC1, &ADC_InitStructure);
	
	ADC_Cmd(ADC1, ENABLE);//开启ADC1的电源
	
	//进行校准
	ADC_ResetCalibration(ADC1);//开始复位校准 1(SET)
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
    // 如果是1,需要空循环等待,如果变成0,就说明复位校准完成,可以跳出等待
	ADC_StartCalibration(ADC1);//开始校准
	while (ADC_GetCalibrationStatus(ADC1) == SET);


}
uint16_t AD_GetValue(void)
{
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);//获取标志位状态
    //当EOC标志位=RESET时,转换未完成,while条件为真,执行空循环,转换完成后,EOC由硬件自动置1,while循环就自动跳出来了
	return ADC_GetConversionValue(ADC1);
}

🔴AD多通道注解

uint16_t AD_GetValue(uint8_t ADC_Channel)	//调用时,只需要指定一个转换的通道,返回值就是指定通道的结果
{
	ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
	return ADC_GetConversionValue(ADC1);
}

  • AD转换的步骤:采样,保持,量化,编码

  • STM32 ADC的总转换时间为:

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

    • 例如:

      • 当ADCCLK=14MHz,采样时间为1.5个ADC周期

      • TCONV = 1.5 + 12.5 = 14个ADC周期 = 1μs

5 DMA直接存储器存取

  • DMA(Direct Memory Access)直接存储器存取

  • DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源

  • 12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)

  • 每个通道都支持软件触发和特定的硬件触发

  • STM32F103C8T6 DMA资源:DMA1(7个通道)

    • DMA主要功能是传输数据,但是不需要占用CPU,即在传输数据时,CPU可以做别的事,像多线程。数据传输从外设到存储器或者从存储器到存储器。DMA控制器包含了DMA1和DMA2,其中DMA1有7个通道,DMA2有5个通道,可以理解为传输数据的一种管道。要注意的是,DMA2只存在于大容量单片机中。

  • DMA总线: 访问给个存储器

  • 通道1、2...:进行独立的数据转运

  • 仲裁器: 用于调度各个通道,防止产生冲突

  • AHB从设备: 用于配置DMA参数

  • DMA请求: 用于硬件触发DMA的数据转运

  • Flash: 主闪存,只读

  • SRAM: 运行内存,任意读写

  • DMA进行转运的条件:

    • 开关控制,DMA_Cmd必须使能

    • 传输计数器必须大于0

    • 触发源必须有触发信号。触发一次,转运一次,传输计数器自减一次,当传输计数器等于0,且没有自动重装时,这时无论是否触发,DMA都不会再进行转运,需要DM_Cmd给DSABLE,关闭DMA,再为传输计数器写入一个大于0的数,再DMA_Cmd会给ENABLE,开启DMA(写传输计数器时,必须要先关闭DMA,再进行)

  • 外设:

    • 起始地址:有外设端的起始地址,和存储器端的起始地址,这两个参数决定了数据是从哪里来,到哪里去的

    • 数据宽度:指定一次转运要按多大的数据宽度来进行,可以选择字节Byte、半字HalfWord和字Word(8位、16位、32位)

    • 地址是否自增:指定一次转运完成后,下一次转运,是不是要把地址移动到下一个位置去,

      • 比如ADC扫描模式,用DMA进行数据转运,外设地址是ADC_DR寄存器,寄存器这边,显然地址是不用自增的,如果自增,那下一次转运就跑到别的寄存器那里去了

  • 存储器:

    • 地址就需要自增,每转运一个数据后,就往后挪个坑,要不然下次再转就把上次的覆盖掉了

  • 初始化步骤

    • RCC开启DMA 的时钟

    • 直接调用DMA_Init,初始化各个参数(外设和存储器站点的起始地址、数据宽度、地址是否自增、方向、传输计数器、是否需要自动重装、选择触发源、通道优先级)

    • 开关控制,DMA_Cmd(硬件出发,调用XXX_DMA_Cmd,开启触发信号的输出;如果需要DMA的中断,就调用DMA_ITConfig,开启中断输出,再到NVIC中配置相应的终端通道,然后写中断函数就行了)

    • 在运行的过程中,如果转运完成,传输计数器清0了,这时再想给传输计数器赋值的话,->(DMA失能、写传输计数器、DMA使能)

DMA最常见的用途就是配合ADC的扫描模式

⚫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);
//DMA获取当前计数器
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数据传输注解

#include "stm32f10x.h"                  // Device header

uint16_t MyDMA_Size;

void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
	MyDMA_Size = Size;
	
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//开启DMA1的时钟
	
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;						//配置外设地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;	//配置外设的数据宽度,字节 uint8_t
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;			//配置外设是否自增,自增	
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;							//配置存储器地址
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;			//配置存储器的数据宽度,字节 uint8_t
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;					//配置存储器是否自增,自增
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;						//传输方向,外设站点到存储器站点的传输方向	
	DMA_InitStructure.DMA_BufferSize = Size;								//缓冲区大小,传输计数器传输的个数
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;							//传输模式,传输计数器不自动重装,自减到0后停下来
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;								//选择是否存储器到存储器,软件触发
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;					//选择优先级,中等
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);							//存储器到存储器的转运,用的是软件触发,所以通道可以任意选择
	
	DMA_Cmd(DMA1_Channel1, DISABLE);//使能
	
}

void MyDMA_Transfer(void)
{
	DMA_Cmd(DMA1_Channel1, DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
	DMA_Cmd(DMA1_Channel1, ENABLE);
	
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
    //检查是否转运完成 如果没有完成 就一直等待
	DMA_ClearFlag(DMA1_FLAG_TC1);//清除标志位
}

🔴ADC连续扫描+DMA循环转运注解

可以加一个定时器,ADC用单次扫描,再用定时器去定时触发,即定主时器触发ADC,ADC触发DMA

#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];

void AD_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//开启DMA1的时钟
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	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);	//“厨师做菜”
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);

	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;							//连续扫描
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;
	ADC_InitStructure.ADC_NbrOfChannel = 4;
	ADC_Init(ADC1, &ADC_InitStructure);
		
	DMA_InitTypeDef DMA_InitStructure;											//“服务员送菜”
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;				//配置外设地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;	//配置外设的数据宽度,半字 uint168_t
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;			//配置外设是否自增,不自增	
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;					//配置存储器地址
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;			//配置存储器的数据宽度,半字 uint168_t
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;						//配置存储器是否自增,自增
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;							//传输方向,外设站点到存储器站点的传输方向
	DMA_InitStructure.DMA_BufferSize = 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使能在DMA1_Channel1
	
	DMA_Cmd(DMA1_Channel1, ENABLE);
	ADC_DMACmd(ADC1, ENABLE);													//开启DMA触发信号
	ADC_Cmd(ADC1, ENABLE);
	
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
	
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

6 USART串口

  • USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器

  • USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里

  • 自带波特率发生器,最高达4.5Mbits/s

  • 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)

  • 可选校验位(无校验/奇校验/偶校验)

  • 支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN

  • STM32F103C8T6 USART资源: USART1、 USART2、 USART3

  • 最左边是波特率发生器,用于产生约定的通信速率,时钟来源是PCLK2或1,经过波特率发生器分频后,产生的时钟通向发送控制器和接收控制器(发送控制器和接受控制器,用力啊控制发送移位和接收移位),之后,由发送数据寄存器和发送移位寄存器这两个寄存器的配合,将数据一位一位地移出去,通过GPIO的复用输出,输出到TX引脚,产生串口协议规定的波形(发送移位寄存器左右的右移符号,是指这个移位寄存器是往右移的,是低位先行;当数据由数据寄存器移到移位寄存器时,会置一个TXE的标志位,对这个标志位进行判断,就可以知道是不是可以写到下一个数据了);同理,RX引脚的波形,通过GPIO输入,在接收控制器的控制下,一位一位地移入接收移位寄存器(右移,低位先行,从左边移进来),移完一帧数据后,数据就会统一转运到接收数据寄存器,在转移的同时,置一个RXNE标志位(检查这个标志位,就可以知道是不是收到数据了,同时,标志位可以申请中断,在收到数据时,直接进入中断函数)

    • 第一步,开启时钟,把需要用的USART和GPIO的时钟打开

    • 第二步,GPIO初始化,把TX配置成复用输出,RX配置成输入

    • 第三步,配置USART,直接使用一个结构体

    • 第四步,如果你只需要发送的功能,就直接开启USART,初始化就结束了

  • 如果你需要接收的功能,可能还需要配置中断,那就在开启USART之前,再加LLITConfig和NMIC的代码就行了

  • 那初始化完成之后,如果要发送数据,调用一个发送函数就行了

    • 如果要接收数据,就用接收的函数

    • 如果要获取发送和接收的状态,就调用获取标志位的函数

⚫USART库函数

void USART_ClockInit(USART_TypeDef* USARTx, USART_ClockInitTypeDef * USART_ClockInitStruct);
void USART_ClockStructInit(USART_ClockInitTypeDef* USART_ClockInitStruct);
//配置同步时钟输出
void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState NewState);
//开启USART到DMA的触发通道
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
//发送数据
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);
//接收数据

🔴USART接收模式注解

#include "stm32f10x.h"                  // Device header
#include <stdio.h>
#include <stdarg.h>


void Serial_Init(void)
{
	//开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);//USART1是APB2的外设 其余为1
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//开启GPIO的时钟
	
	//初始化GPIO的引脚
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    //TX引脚是USART外设控制的输出脚,要用到复用推挽输出;RX引脚是USART外设数据输入脚,所以要选择输入模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;	   
    //因为串口波形空闲状态是高电平,所以不使用下拉输入
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//初始化USART
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600;//设置波特率 Init函数内部会自动算好9600对应的分频系数,写到BRR寄存器
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//硬件流控制 不使用流控 
	USART_InitStructure.USART_Mode = USART_Mode_Tx;				//串口模式,发送模式
	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);
	//9600波特率 8位字长 无校验 1位停止位 无流控 只发送模式
	
	USART_Cmd(USART1, ENABLE);
}

void Serial_SendByte(uint8_t Byte)//发送数据
{
	USART_SendData(USART1, Byte);//Byte写入到TDR
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//如果TXE标志位==RESET,就一直循环,直到SET,结束等待
	//当TDR寄存器中的数据被硬件转移到移位寄存器的时候,该位被硬件置位。如果USART_CR1寄存器中的TXEIE为1,则产生中断。对USART_DR的写操作,将该位清零。
	//标志位置1之后,不需要手动清零,当我们下一次再SendData时,这个标志位会自动清零
}

void Serial_SendArray(uint8_t *Array, uint16_t Length)//发送数组
{
	uint16_t i;
	for (i = 0; i < Length; i ++)
    //for循环就会执行Length次,可以对Array数据进行遍历,就不断调用Serial SendByte,发送Array[i]
	{
		Serial_SendByte(Array[i]);
        //依次取出数组Array的每一项,然后通过SendByte进行发送了
	}
}

void Serial_SendString(char *String)
//发送字符串 由于字符申自带一个结束标志位,所以就不需要再传递长度参数了
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i ++)
    //'\0'=0空字符的转义字符  用结束标志位来判断,0对应空字符,是字符串结束标志位,如果不等于0,就是还没结束,进行循环,如果等于0,就是结束了,停止循环
	{
		Serial_SendByte(String[i]);
	}
}

uint32_t Serial_Pow(uint32_t X, uint32_t Y)//次方函数
{
	uint32_t Result = 1;
	while (Y --)
	{
		Result *= X;//Result累乘Y次X,就是X的Y次方
	}
	return Result;
}

void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0; i < Length; i ++)
	{
		Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
        //i这里遍历是从0开始的,10^0是个位,所以方向要反过来,Length-i-1
	}
    //假设length为2,第一次循环,2-0-1=1,10^1,就是发送十位,第二次就是个位,当然最终要以字符的形式显示,所以还要加一个偏移 '0'
	//例如函数中给234,那么一位位输出的就是02 03 04,为了输出为字符数字,就需要+0x30
}
//在函数里面,我们需要把Number的个位、十位、百位等等以十进制拆分开,然后转换成字符数字对应的数据,依次发送出去

//对printf进行重定向,将printf函数打印的东西输出到串口,因为printf函数默认是输出到屏幕,单片机没有屏幕,所以要重定向
//加上#include <stdio.h>,重写fputc函数,fputc是printf函数的底层,printf函数在打印的时候,就是不断调用fputc函数一个个打印的,把fputc函数重定向到了串口,那printf自然就输出到串口了
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}
//sprintf可以把格式化字符输出到一个字符串里
//	char String[100];
//	sprintf(String, "\r\nNum3=%d", 333);
//	Serial_SendString(String);

//封装sprintf,先添加头文件,#include <stdarg.h>
void Serial_Printf(char *format, ...)
{
	char String[100];//首先定义输出的字符申
	va_list arg;//定义一个参数列表变量
	va_start(arg, format);//从format位置开始接收参数表,放在arg里面
	vsprintf(String, format, arg);//打印位置是String,格式化字符串是format,参数表是arg
	//sprintf只能接收直接写的参数,对于这种封装格式,要用vsprintf
	va_end(arg);//释放参数表
	Serial_SendString(String);//把String发送出去
}

🔴USART发送和接收模式注解

void Serial_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_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;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600;
	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;
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	USART_Init(USART1, &USART_InitStructure);
	
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启RXNE标志位到NVIC的输出 
	
	//配置NVIC
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//中断通道
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);
	
	USART_Cmd(USART1, ENABLE);
}

6.1 HEX数据包

🔴HEX接收过程注解

uint8_t Serial_TxPacket[4];//存储发送的载荷数据
uint8_t Serial_RxPacket[4];//存储接收的载荷数据

uint8_t Serial_GetRxFlag(void)//判断是不是接收到数据包
{
	if (Serial_RxFlag == 1)
	{
		Serial_RxFlag = 0;
		return 1;
	}
	return 0;
}

//上面接收数据包的缓存去和标志位都已经定义好了,在接收中断函数里,就需要用状态机来执行接收逻辑,接收数据包,然后吧在和数据存在RxPacket数组里
void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;//定义一个静态变量来显示s = 0 1 2
	static uint8_t pRxPacket = 0;//接收到第几个数据了
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
		uint8_t RxData = USART_ReceiveData(USART1);
		
		if (RxState == 0 )//进入等待包头的程序
		{
			if (RxData == 0xFF)
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if (RxState == 1 )//进入接收数据的程序
		{
			Serial_RxPacket[pRxPacket] = RxData;
			pRxPacket ++;
			if (pRxPacket >= 4)//接收四个数据后 进入下一个状态
			{
				RxState = 2;
			}
		}
		else if (RxState == 2 )//进入等待包尾的程序
		{
			if (RxData == 0xFE)
			{
				RxState = 0;
				Serial_RxFlag = 1;//收到数据置一个标识位
			}
		}
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	}
}

6.2 文本数据包

🔴文本接收过程注解

//上面接收数据包的缓存去和标志位都已经定义好了,在接收中断函数里,就需要用状态机来执行接收逻辑,接收数据包,然后吧在和数据存在RxPacket数组里
void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;//定义一个静态变量来显示s = 0 1 2
	static uint8_t pRxPacket = 0;//接收到第几个数据了
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
		uint8_t RxData = USART_ReceiveData(USART1);
		
		if (RxState == 0 )//进入等待包头的程序
		{
			if (RxData == '@')
			{
				RxState = 1;
				pRxPacket = 0;
			}
		}
		else if (RxState == 1 )//进入接收数据的程序
		{
			if (RxData == '\r')
			{
				RxStat = 2;
			}
			else
			{
				Serial_RxPacket[pRxPacket] = RxData;
				pRxPacket ++;
			} 
		}
		else if (RxState == 2 )//进入等待包尾的程序
		{
			if (RxData == '\n')
			{
				RxState = 0;
				Serial_RxPacket[pRxPacket] = RxData;//加一个字符串结束标识位\0,方便对字符串进行处理,不然不知道字符串有多长
				Serial_RxFlag = 1;//收到数据置一个标识位
			}
		}
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	}
}

7 I2C通信

  • I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线

  • 两根通信线:SCL(Serial Clock)、SDA(Serial Data)

  • 同步,半双工

  • 带数据应答

  • 支持总线挂载多设备(一主多从、多主多从)

  • 那如何发出指令,来确定要访问的是哪个设备呢,这就需要首先把每个从设备都确定一个唯一的设备地址,从机设备地址就相当于每个设备的名字,主机在起始条件之后,要先发送一个字节叫一下从机名字,所有从机都会收到第一个字节,和自己的名字进行比较,如果不一样,则认为主机没有叫我;如果一样,就说明,主机现在在叫我,那我就响应之后主机的读写操作

  • 在同一条I2C总线里,挂载的每个设备地址必须不一样,否则,主机叫一个地址,有多个设备都响应,就乱套了

  • 第一个字节,由主机发送,最开始,SCL低电平,主机如果想发送0,就拉低SDA到低电平,如果想发送1,就放手,SDA回弹到高电平(在SCL低电平期间,允许改变SDA的电平),当这一位放好之后,主机就松手时钟线,SCL回弹到高电平

  • 高电平期间,是从机读取SDA的时候SCL高电平期间,SDA不允许变化),SCL处于高电平之后,从机需要尽快地读取SDA,一般都是在上升沿这个时刻(B7左边),从机就已经读取完成了,因为时钟是主机控制的,从机并不知道什么时候就会产生下降沿了

  • 那主机在放手SCL一段时间后,就可以继续拉低SCL,传输下一位了,主机也需要在SCL下降沿之后尽快把数据放在SDA上(但是主机有时钟的主导权,所以主机并不需要那么着急,只需要在低电平的任意时刻把数据放在SDA上就行了,数据放完之后,主机再松手SCL,SCL高电平

  • 主机拉低SCL,把数据放在SDAL上,主机松开SCL,从机读取SDA的数据,在SCL的同步下,依次进行主机发送和从机接收,循环8次,就发送了8位数据,也就是一个字节

    • 由于这里有时钟线进行同步,所以如果主机一个字节发送一半,突然进中断了,不操作SCL和SDA了,那时序就会在中断的位置不断拉长,SCL和SDA电平都暂停变化,传输也完成了暂停,等中断结束后,主机回来继续操作,传输仍然不会出问题,这就是同步时序的好处

  • 在这个单元里,SCL和SDA全程都由主机掌控,从机只能被动读取

  • 串口 低位先行

  • I2C 高位先行

  • 当主机需要发送的时候,就可以主动去拉低SDA

  • 而主机在被动接收的时候,就必须先释放SDA,不要去动它。以免影响别人发送,因为任何一个设备拉低了,总线就是低电平,如果接收的时候,还拽着SDA不放手,那无论发什么数据,总线都始终是低电平

  • 这里实线部分表示主机控制的电平,虚线部分表示从机控制的电平,SCL全程由主机控制,SDA主机在接收前要释放,交由从机控制,之后还是一样,因为SCL时钟是由主机控制的,所以从机的数据变换基本上都是贴着SCL下降沿进行的,而主机可以在SCL高电平的任意时刻读取

    • 释放SDA其实就相当于切换成输入模式


  • 区别就是SDA线,主机在接收之前要释放SDA,然后这时从机就取得了SDA的控制权,从机需要发送0,就把SDA拉低,从机需要发送1,就放手,SDA回弹高电平,然后同样的,低电平变换数据,高电平读取数据

    • 发送:低电平主机放数据,高电平从机读数据

    • 接收:低电平从机放数据,高电平主机读数据

  • 例如木头人,主机说1 2 3,都可以动,主机说木头人,都不可以动了,如果再SCL高电平期间,非要动SDA,这个信号就是起始条件和终止条件,SCL高电平时,SDA下降为起始条件,上升为终止条件

7.1 I2C时序

7.1.1 指定地址写

  • 首先,SCL高电平期间,拉低SDA,产生起始条件(Strat,S),在起始条件后,紧跟的时序必须是发送一个字节的时序,字节的内容必须是:从机地址+读写位(7+1),发送从机地址,就是确定通信对象,发送读写位,就是确认我接下来是要写入还是要读出。具体发送时,低电平期间,SDA变换数据,高电平期间,从机读取SDA。

    • SCL低电平变换数据,高电平读取数据

    • 八位的最后一位为0,说明是之后的时序主机写入操作,反之为1则是之后的时序主机读出操作


  • RA:应答位;在这个时刻,主机要释放SDA

    • 主机松手后,SDA并没有回弹高电平,这个过程,就代表从机产生了应答,最终高电平期间,主机读取SDA,发现为0,说明进行了寻址,有人进行了应答,传输没有问题;如果主机读取SDA,发现为1,说明进行寻址时,应答位期间,主机松手后,没人拽住他,没人进行应答,直接产生停止条件。

    • 应答位的上升沿,就是从机释放SDA产生的,从机交出了SDA的控制权,所以这个上升沿和SCL的下降沿,几乎是同时发生的

    • (一般第二个字节可以是寄存器地址或者是指令控制字等,比如MPU6050定义的第二个字节就是寄存器地址;比如AD转换器,第二个字节可能就是指令控制字;比如存储器,第二个字节可能就是存储器地址)


  • 如图第二个字节的波形,主机向从机发送了0x19这个数据,在MPU6050里,就表示要操作0x19地址下的寄存器,接着同样,是从机应答,主机释放SDA,从机拽佳SDA,SDA表现为低电平,主机收到应答位为0,表示收到了从机的应答,之后的这个字节就是主机想要写入到0x19地址下寄存器的内容了,发送0xAA,就表示,我要在0x19地址下,写入0xAA,最后是接收应答位,如果主机不需要继续传输了,就可以产生停止条件(Stop,P),在停止条件之前,先拉低SDA,为后续SDA的上升沿作准备,然后释放SCL,再释放SDA,这样就产生了SCL高电平期间,SDA的上升沿

  • 其数据帧的目的就是,对于指定从机地址为11010000的设备,在其内部0x19地址的寄存器中,写入0xAA这个数据

7.1.2 当前地址读

  • 最开始,还是SCL高电平期间,拉低SDA,产生起始条件,起始条件开始后,主机必须首先调用发送一个字节,来进行从机的寻址和指定读写标志位,比如图示的波形,表示本次寻址的目标是1101000的设备,同时,最后一位读写标志为1,表示主机接下来想要读取数据

  • 紧跟着,发送一个字节之后,接收一下从机应答位,从机应答0,代表从机收到了第一个字节,在从机应答之后,从这里开始,数据的传输方向就要反过来了,因为刚才主机发出了读的指令,所以这之后,主机就不能继续发送了,要把SDA的控制权交给从机,主机调用接收一个字节的时序,进行接收操作,之后,从机就得到了主机的允许,可以在SCL低电平期间写入SDA然后主机在SCL高电平期间读取SDA,那最终,主机在SCL高电平期间依次读取8位,就接收到了从机发送的一个字节数据00001111


  • 这个0x0F是从机哪个寄存器的数据呢?

    • 在读的时序中,I2C协议的规定是,主机进行寻址时,一旦读写标志位给1了,下一个字节就要立马转为读的时序,所以主机还来不及指定,想要读哪个寄存器就得开始接收了。所以这里就没有指定地址这个环节

  • 那主机并没有指定寄存器的地址,从机到底该发哪个寄存器的数据呢

    • 这就需要用到上面说的当前地址指针了(在从机中,所有的寄存器被分配到了一个线性区域中,并且,会有一个单独的指针变量,指示着其中一个寄存器,这个指针上电默认,一般指向0地址,并且,每写入一个字节和读出一个字节后,这个指针就会自动自增一次)那么在调用当前地址读的时序时,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值,例如上一个为0x19,返回的就是0x1A地址下的值,再一次就是0x1B的值

7.1.3 指定地址读

  • 在这前面一部分,就是指定地址的时序,后半部分就是当前地址读的时序

  • 指定地址后,本来我们要写入的数据,不给它发,而是直接再来个起始条件,这个Sr [(Start Repeat) 的意思就是重复起始条件,相当于另起一个时序],因为指定读写标志位只能是跟着起始条件的第一个字节,所以如果想切换读写方向,只能再来个起始条件,然后起始条件后,重新寻址并且指定读写标志位,此时读写标志位是1,代表我要开始读了,接着,主机接收一个字节,这个字节就是0x19地址下的数据

    • 可以在Sr前加入停止条件,就是两个完整的时序了

🔴软件I2C注解

程序思路

  • 首先建立12C通信层的.c和.h模块

    • 在通信层里,写好12C底层的GPIO初始化和6个时序基本单元,也就是起始、终止、发送一个字节、接收一个字节、发送一个应答、接收一个应答

  • 再建立MPU6050的c和.h模块

    • 在这一层,将基于I2C通信的模块,来实现指定地址读、指定地址写,再实现写寄存器对芯片进行配置,读奇存器得到传感器数据

  • 最终在main.c里

    • 调用MPU6050的模块,初始化,拿到数据,显示数据

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
​
void MyI2C_W_SCL(uint8_t BitValue)//W写的意思
{
    GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)BitValue);//强转为BitAction,Bitaction是一个枚举类型 表示这个位是高电平还是低电平(1高0低)
    Delay_us(10);
}
void MyI2C_W_SDA(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOB, GPIO_Pin_9, (BitAction)BitValue);
    Delay_us(10);
}
uint8_t MyI2C_R_SDA(void)
{
    uint8_t BitValue;
    BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9);
    Delay_us(10);
    return BitValue;
}
​
void MyI2C_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;//开漏输出,并不代表只能输出,输入时,先输出1,再直接读取输入数据奇存器就行了
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    
    GPIO_SetBits(GPIOB,GPIO_Pin_8 | GPIO_Pin_9);//置高电平
    
}
​
void MyI2C_Start(void)//起始条件
{
    MyI2C_W_SCL(1);//高
    MyI2C_W_SDA(1);
    MyI2C_W_SDA(0);//高->低
    MyI2C_W_SCL(0);//低
    
}
void MyI2C_Stop(void)//终止条件
{
    MyI2C_W_SDA(0);
    MyI2C_W_SCL(1);
    MyI2C_W_SDA(1);//SCL SDA都回归高电平
}
void MyI2C_SendByte(uint8_t Byte)//发送字节
{
    uint8_t i;
    for(i = 0; i < 8; i++)
    {
        MyI2C_W_SDA(Byte & (0x80 >> i));//0x80 0x40 0x20
        MyI2C_W_SCL(1);//释放
        MyI2C_W_SCL(0);//拉低 驱动时钟走一个脉冲
    }
}
uint8_t MyI2C_ReceiveByte(void)//接收字节
{
    uint8_t i,Byte = 0x00;
    MyI2C_W_SDA(1);//切换为输入模式
    for(i = 0; i < 8; i++)
    {
        MyI2C_W_SCL(1);
        if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}//把Byte最高位置1,如果度SDA为0,默认为0:
        MyI2C_W_SCL(0);//拉低 从机就会把下一位数据放到SDA上
    }
    return Byte;
}
​
void MyI2C_SendAck(uint8_t AckBit)//发送应答 
{
    MyI2C_W_SDA(AckBit);//函数进来时,SCL低电平,主机吧AckBit放到SDA上
    MyI2C_W_SCL(1);//SCL高电平,从机读取应答
    MyI2C_W_SCL(0);//SCL低电平,进入下一个时序单元
}
uint8_t MyI2C_ReceiveAck(void)//接收应答
{
    uint8_t AckBit;//函数进来时,SCL低电平,主机释放SDA,防止从机干扰
    MyI2C_W_SDA(1);//从机把应答位放在SDA上
    MyI2C_W_SCL(1);//SCL高电平,主机读取应答位
    AckBit = MyI2C_R_SDA();
    MyI2C_W_SCL(0);//SCL低电平,进入下一个时序单元
    return AckBit;
}
    //可以用for循环套起来,遍历所有的从机地址,把应答位为0的地址统计下来
    MyI2C_Start();
    MyI2C_SendByte(0xD0);// 1101 000 0  最后一位为0,写入操作
    //AD0引脚接置高电平,0xD0 -> 0xD2(1101 001 0) 只有AD0一个引脚,只能有两个名字
    uint8_t Ack = MyI2C_ReceiveAck();
    MyI2C_Stop();//点名时序 查看是不是有这个存在

🔴软件读写MUP60560指定地址读写注解

#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"
​
typedef struct
{
    int16_t AccX;
    int16_t AccY;
    int16_t AccZ;
    int16_t GyroX;
    int16_t GyroY;
    int16_t GyroZ;
}MPU6050_DataStruct;
​
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)//指定地址写 8位寄存器地址 8位数据
{
    MyI2C_Start();
    MyI2C_SendByte(MPU6050_ADDRESS);//从机地址+读写位
    MyI2C_ReceiveAck();//接收应答 有应答位可以判断从机有没有接收到数据
    MyI2C_SendByte(RegAddress);//寻址找到从机之后,可以继续发送下一个字节,指定寄存器地址
    MyI2C_ReceiveAck();//接收应答
    MyI2C_SendByte(Data);//指定要写入地址下寄存器地址下的数据
    MyI2C_ReceiveAck();//接收应答
    MyI2C_Stop();
}
​
uint8_t MPU6050_ReadReg(uint8_t RegAddress)//指定地址读
{
    uint8_t Data;
    
    MyI2C_Start();
    MyI2C_SendByte(MPU6050_ADDRESS);
    MyI2C_ReceiveAck();//接收应答
    MyI2C_SendByte(RegAddress);
    MyI2C_ReceiveAck();//接收应答
    
    MyI2C_Start();
    MyI2C_SendByte(MPU6050_ADDRESS | 0x01);//读从机的数据
    MyI2C_ReceiveAck();//接收应答
    Data = MyI2C_ReceiveByte();
    MyI2C_SendAck(1);//0 给从机应答,1不给从机应答;如果读多个字节,就要给应答
    
    return Data;
}
​
void MPU6050_Init(void)
{
    MyI2C_Init();
    MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
    //电源管理寄存器1     设备复位:0不复位 睡眠模式:0解除睡眠 循环模式:0不循环 无关位:0 温度传感器失能:0不失能 后三位选择时钟:001X轴陀螺仪时钟
    MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
    //电源管理寄存器2     循环模式唤醒频率:00不需要 后六位每个轴的待机位:000000不待机
    MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
    //采样率分频         数据输出的快慢 值越小越快 10分频
    MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
    //配置寄存器         外部同步:00000 数字低通滤波器:110
    MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
    //陀螺仪配置寄存器    自测使能:000 满量程选择:11 后三无关位:000
    MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
    //加速度计配置寄存器   自测使能:000 满量程选择:11 高通滤波器:000 
    
    //目前配置:解除睡眠模式,选择陀螺仪时钟,6个轴均不待机,采样分频为10,滤波参数给最大,陀螺仪和加速度计都选择最大量程16g 1943/32768 = x/16g
}
​
void MPU6050_GetData(MPU6050_DataStruct* MPU6050_Data)
{
    uint16_t DataH, DataL;
    
    //分别读取6个轴数据寄存器的高位和地位,拼接成16位的数据,再通过指针变量返回
    
    //加速器x轴数据
    DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);//高8位
    DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);//低8位
    MPU6050_Data->AccX = (int16_t)((DataH << 8) | DataL);//16位数据
    //用指针引言传递进来的地址,把读到的数据通过指针返回回去
    
    //加速器y轴数据
    DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
    DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
    MPU6050_Data->AccY = (int16_t)((DataH << 8) | DataL);
    
    //加速器z轴数据
    DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
    DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
    MPU6050_Data->AccZ = (int16_t)((DataH << 8) | DataL);
    
    //陀螺仪x轴数据
    DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
    DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
    MPU6050_Data->GyroX = (int16_t)((DataH << 8) | DataL);
    
    //陀螺仪y轴数据
    DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
    DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
    MPU6050_Data->GyroY = (int16_t)((DataH << 8) | DataL);
    
    //陀螺仪z轴数据
    DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
    DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
    MPU6050_Data->GyroZ = (int16_t)((DataH << 8) | DataL);
​
}

 

7.2 I2C通信外设

  • STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担

    • 支持多主机模型

    • 支持7位/10位地址模式

    • 支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)

    • 支持DMA

    • 兼容SMBus协议

  • STM32F103C8T6 硬件I2C资源:I2C1、I2C2

  • SDA

    • SMBALERT是SMBus用的,一般外设模块印出来的引脚,一般都是借用GPIO口的复用模式与外部世界相连;上方SDA数据控制部分,核心部分是数据寄存器和数据移位寄存器。当我们需要发送数据时,可以把一个字节数据写入到数据寄存器DR,当移位寄存器没有数据移位时,这个数据寄存器的值就会进一步转到移位寄存器中,在移位的过程中,我们就可以直接把下一个数据放到数据寄存器DR里等待着,一旦前一个数据移位完成,下一个数据就可以无缝衔接,继续发送,当数据由数据寄存器DR转移到移位寄存器时,就会置状态寄存器的TXE位为1,表示发送寄存器为空,这就是发送的流程。

      • 接收时,输入的数据,一位一位地从引脚移入到移位寄存器中,当一个字节的数据收齐之后,数据就整体从移位寄存器转到数据寄存器DR,同时置标志位RXNE,表示接收寄存器非空,这时就可以把数据从数据寄存器DR读出来了。

    • 与USART不同的是,USART收发是分开的,全双工,I2C是半双工

    • 比较器和地址寄存器从机模式使用的, 可以自定一个从机地址,写到寄存器中,当STM32作为从机,在被寻址时,如果收到的寻址通过比较器判断,和自身地址相同,那STM32就作为从机,响应外部主机的召唤,并且这个STM32支持同时响应两个从机地址,所以就有自身地址寄存器和双地址寄存器


  • SCL

  • 时钟控制,是用来控制SCL线的,写入控制寄存器,可以对整个电路进行控制;读取状态寄存器,可以得知电路的工作状态;中断部分,当内部由一些标志位置1后,可能事件比较紧急,就可以申请中断;DMA请求与响应,在进行很多字节的收发时,可以配合DMA来提高效率

  • 由于I2C是 高位先行,所以移位寄存器是向左移,发送的时候,最高位先移出去,一个SCL时钟,移位一次,移位8次,就能够把一个字节由高位到地位,依次放到SDA线上;在接收的时候,数据通过GPIO口从右边依次移进来,最终移8次吗,一个字节就接收完成了。使用硬件I2C时,GPIO需要配置为复用开漏输出模式,片上外设控制。

主机发送

  • 7位:起始一个字节是寻址,流程:从机地址、应答、数据1、应答、数据2、应答......p(停止)

  • 10位:起始两个字节是寻址,帧头:11110(标志位)+2位地址+1位读写位,之后的地址就是8位地址

主机接收

⚫I2C库函数

void I2C_DeInit(I2C_TypeDef* I2Cx);
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
//初始化
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);
//结构体
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
//使能I2C
void I2C_DMACmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_DMALastTransferCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
//生成起始条件
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
//生成终止条件
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
//STM32作为主机,在接收到一个字节后,是给从机应答还是非应答
void I2C_OwnAddress2Config(I2C_TypeDef* I2Cx, uint8_t Address);
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);
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);
void I2C_SoftwareResetCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_NACKPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_NACKPosition);
void I2C_SMBusAlertConfig(I2C_TypeDef* I2Cx, uint16_t I2C_SMBusAlert);
void I2C_TransmitPEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_PECPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_PECPosition);
void I2C_CalculatePEC(I2C_TypeDef* I2Cx, FunctionalState NewState);
uint8_t I2C_GetPEC(I2C_TypeDef* I2Cx);
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);

/*
1.基本状态监控
I2C_CheckEvent()    //同时判断一个或多个标志位,来确定EV5或其他是否发生
2.高级状态监控
I2C_GetLastEvent()  //直接把SR1和SR2这两个状态寄存器拼接成16位的数据
3.基于标志位的状态监控
I2C_GetFlagStatus() //判断某一个标志位是否置1
*/

🔴硬件I2C注解

//硬件I2C的初始化
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);//I2C2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//开始GPIO的时钟
	
	//设置为复用开漏模式
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	//初始化I2C外设
	I2C_InitTypeDef I2C_InitStructure;
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;//模式
	I2C_InitStructure.I2C_ClockSpeed = 50000;//时钟频率
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//时钟占空比 小于100KHz为1:1
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//应答位配置
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//STM32作为从机,可以响应几位的地址
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;//自身地址,与上面的响应几位地址相对应
	I2C_Init(I2C2, &I2C_InitStructure);
	
	//使能I2C2
	I2C_Cmd(I2C2, ENABLE);

🔴指定地址读写软硬件区别注解

void MPU6050_WatiEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
    //对while (I2C_CheckEventt(I2Cx, I2C_EVENT) != SUCCESS);直接进行封装
{
	uint32_t Timeout;
		while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
	//检测EV5事件是否发生  I2C_EVENT_MASTER_MODE_SELECT:主机模式选择EV5
	{
		Timeout--;
		if(Timeout == 0)
		{
			break;
			//实际操作可以加入打印错误日志、进行系统系统复位
		}
	}//计次计时 超时推出 防止死循环
	
}

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)//指定地址写 8位寄存器地址 8位数据
{
/**************软件**************/
//	MyI2C_Start();
//	MyI2C_SendByte(MPU6050_ADDRESS);//从机地址+读写位
//	MyI2C_ReceiveAck();//接收应答 有应答位可以判断从机有没有接收到数据
//	MyI2C_SendByte(RegAddress);//寻址找到从机之后,可以继续发送下一个字节,指定寄存器地址
//	MyI2C_ReceiveAck();//接收应答
//	MyI2C_SendByte(Data);//指定要写入地址下寄存器地址下的数据
//	MyI2C_ReceiveAck();//接收应答
//	MyI2C_Stop();

/**************硬件**************/
	I2C_GenerateSTART(I2C2, ENABLE);//生成起始条件
	//函数结束后,需要等待响应的标志位,来确保函数的操作执行到位了
	MPU6050_WatiEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
	//检测EV5事件是否发生  I2C_EVENT_MASTER_MODE_SELECT:主机模式选择EV5
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	//从机地址发送    (参数 从机地址 方向,从机地址的最低为,读写位)之后不需要应答位
	MPU6050_WatiEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
	//等待EV6事件 (主机发送事件)
	I2C_SendData(I2C2, RegAddress);
	//发送一个字节的数据RegAddress
	MPU6050_WatiEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);
	//检测EV8事件 (字节正在发送 EV8)
	I2C_SendData(I2C2, Data);
	//发送数据Data
	MPU6050_WatiEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
	//检测EV8事件 (字节已经发送完毕 EV8_2)
	I2C_GenerateSTOP(I2C2, ENABLE);
	//终止时序
	
}

uint8_t MPU6050_ReadReg(uint8_t RegAddress)//指定地址读
{
	uint8_t Data;
/**************软件**************/
//	MyI2C_Start();
//	MyI2C_SendByte(MPU6050_ADDRESS);
//	MyI2C_ReceiveAck();//接收应答
//	MyI2C_SendByte(RegAddress);
//	MyI2C_ReceiveAck();//接收应答
//	
//	MyI2C_Start();//生成重复起始条件
//	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);//读从机的数据
//	MyI2C_ReceiveAck();//接收应答
//	Data = MyI2C_ReceiveByte();
//	MyI2C_SendAck(1);//0 给从机应答,1不给从机应答;如果读多个字节,就要给应答
//	MyI2C_Stop();

//多个while会导致死循环
/**************硬件**************/
	I2C_GenerateSTART(I2C2, ENABLE);//生成起始条件
	//函数结束后,需要等待响应的标志位,来确保函数的操作执行到位了
	MPU6050_WatiEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
//	//检测EV5事件是否发生  I2C_EVENT_MASTER_MODE_SELECT:主机模式选择EV5
//	while (I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS)
//	//检测EV5事件是否发生  I2C_EVENT_MASTER_MODE_SELECT:主机模式选择EV5
//	{
//		Timeout--;
//		if(Timeout == 0)
//		{
//			break;
//		}
//	}//计次计时 超时推出 防止死循环
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	//从机地址发送    (参数 从机地址 方向,从机地址的最低为,读写位)之后不需要应答位
	MPU6050_WatiEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
	//等待EV6事件 (主机发送事件)
	I2C_SendData(I2C2, RegAddress);
	//发送一个字节的数据RegAddress
	MPU6050_WatiEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
	//检测EV8事件 (字节正在发送 EV8) 在成重复起始条件前等待,等波形全部发完,再生成重复起始条件
	
	I2C_GenerateSTART(I2C2, ENABLE);
	//生成重复起始条件
	MPU6050_WatiEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
	//检测EV5事件是否发生  I2C_EVENT_MASTER_MODE_SELECT:主机模式选择EV5
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
	//从机地址发送    (参数 从机地址 接收的方向,会自动置1 不需要写| 0x01)之后不需要应答位
	MPU6050_WatiEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
	//等待EV6事件 (主机接收事件)
	I2C_AcknowledgeConfig(I2C2, DISABLE);//不应答 ACK置0
	//目前只需要读取一个字节 在EV6后,配置ACK位
	I2C_GenerateSTOP(I2C2, ENABLE);
	//生成终止条件
	
	MPU6050_WatiEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);
	//等EV7事件产生后,一个字节的数据就已经在DR里面了,读取DR就可以拿出这个字节
	Data = I2C_ReceiveData(I2C2);
	
	I2C_AcknowledgeConfig(I2C2, ENABLE);//ACK置回1 恢复默认 方便指定地址收取多个字节
	
	return Data;
}

8 SPI通信

  • SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线

  • 四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)

  • 同步,全双工

  • 支持总线挂载多设备(一主多从)

  • 所有SPI设备的SCK、MOSI、MISO分别连在一起

  • 主机另外引出多条SS控制线,分别接到各从机的SS引脚

  • 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入


  • 所有SPI设备的SCK、MOSI、MISO分别连在一起

  • 主机另外引出多条SS控制线,分别接到各从机的SS引脚

  • 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入

8.1 SPI移位

  • 首先规定,波特率发生器时钟的上升沿,所有移位寄存器想做移动一位,移出去的位放到引脚上,波特率发生器时钟的下降沿,引脚上的位,采样输入到移位寄存器的最低位。

  • 接下来,假设主机有个数据10101010要发送到从机,同时,从机有个数据01010101要发送到主机,那我们就可以驱动时钟,先产生一个上升沿,这时,所有的位往左移动一次,从最高位移出去的数据,会放到通信线上(例如红色的1放到MOSI上,蓝色的0放到MISO上),实际上是放到了输出数据寄存器上,此时,MOSI数据是1,所以MOSI的电平就是高电平;MISO数据是0,所以MISO的电平就是低电平;这就是第一个时钟上升沿执行的结果,就是把主机和从机中,移位寄存器的最高位分别放到MOSI和MISO的通信线上,这就是数据的输出

  • 之后,时钟继续运行,上升沿之后,下一个边沿就是下降沿,在下降沿时,主机和从机内,都会进行数据采样输入,也就是,MOSI的1,会采样输入到从机(蓝色的最后)这里的最低位,MISO的0,会采样输入到主机(红色的最后)这里的最低位。这就是第一个时钟结束后的现象。

  • 之后的操作重复,只至这里原来主机(红色)里的10101010跑到了从机(蓝色)里,原来从机(蓝色)里的01010101跑到了主机(红色)里,这就实现了主机和从机一个字节的数据交换。

  • SPI的数据收发,都是基于字节交换,这个基本单元来进行的,当主机需要发送一个字节,并且同时需要接收一个字节时,就可以执行一下字节交换的时序,这样,主机要发送的数据,跑到从机,主机要从从机接收的数据,跑到主机,这就完成了发送同时接收的目的。

8.2 SPI时序基本单元

  • 期间需要始终保持低电平

模式1

  • 因为有多个从机输出连在了一起,如果同时开启输出,会造成冲突,解决方法:在SS未被选中的状态,从机的MISO引脚必须关断输出,即配置输出为高阻状态。

  • 在这里,SS高电平时,MISO用一条中间的线表示高阻态,SS下降沿之后,从机的MISO被允许开启输出,SS上升沿之后,从机的MISO必须置回高阻态。

移位传输

  • 因为CPHA=1,SCK第一个边沿移出数据,所以这里可以看出,SCK第一个边沿,就是上升沿,主机和从机同时移出数据(交叉),主机通过MOSI移出最高位,此时MOSI的电平就表示了主机要发送数据的B7,从机通过MISO移出最高位,此时MISO表示从机要发送数据的B7,然后时钟运行,产生下降沿,此时主机和从机同时移入数据,也就是进行数据采样,这里主机移出的B7,进入从机移位寄存器的最低位,从机移出的B7,进入主机移位奇存器的最低位,这样,一个时钟脉冲产生完毕,一个数据位传输完毕。之后步骤相同,最后就可以置SS为高电平,结束通信了,在SS的上升沿,MOSI还可以再变化一次,将MOSI置到一个默认的高电平或低电平,或者不管,因为SPI也没有硬性规定MOSI的默认电平,然后MISO,从机必须得置回高阻态,此时如果主机的MISO为上拉输入的话,那MISO引脚的电平就是默认的高电平,如果主机MISO为浮空输入,那MISO引脚的电平不确定。如果继续交换,主机就不必吧SS置回高电平。

模式0

  • 与模式1相比,模式0把这个数据变化的时机给提前了

    • 模式0和模式3的时候,是SCK上升沿采样移入

    • 模式1和模式2的时候,是SCK下降沿采样移入

    • CPHA决定是第几个边沿采样,并不能单独决定是上升沿还是下降沿

模式2

  • 与模式0的区别

    • 就是模式0的CPOL=0,模式2的CPOL=1,两者波形就是SCK的极性取反

模式3

  • 与模式1的区别

    • 模式1的CPOL=0,模式3的CPOL=1,两者波形就是SCK的极性取反

8.3 时序

  • l2C的规定一般是,有效数据流第一个字节是寄存器地址,之后依次是读写的数据,使用的是读写寄存器的模型。

  • 在SPI中,通常采用的是指令码加读写数据的模型,这个过程就是,SPI起始后,第一个交换发送给从机的数据,一般叫做指令码,在从机中,对应的会定义一个指令集,当我们需要发送什么指令时,就可以在起始后第一个字节发送指令集里面的数据,这样就能指导从机完成相应的功能了。不同的指令,可以有不同的数据个数,有的指定,只需要一个字节的指令码就可以完成,比如W25Q64的写能、写失能等指令;而有的指令,后面就需要再跟要读写的数据,比如W25Q64的写数据、读数据等。

🔴模式选择注解

#include "stm32f10x.h"                  // Device header

//函数封装
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
void MySPI_W_SCK(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}
void MySPI_W_MOSI(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}
uint8_t MySPI_R_MISO(void)
{
	return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}

void MySPI_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	MySPI_W_SS(1);//默认不选中从机
	MySPI_W_SCK(0);//使用SPI模式0,所以默认是低电平	
}

void MySPI_Start(void)
{
	MySPI_W_SS(0);//置低电平 开始
}
void MySPI_Stop(void)
{
	MySPI_W_SS(1);//置高电平 结束
}

uint8_t MySPI_SwapByte(uint8_t ByteSend)//模式0
{
	uint8_t i;
	
	for(i = 0; i < 8; i++)//移位模型
	{
		MySPI_W_MOSI(ByteSend & 0x80);//发送位
		ByteSend <<= 1;//效果就是把ByteSend的最高位移出到MOSI,ByteSend左移了一位,它的最低位会自动补0
		MySPI_W_SCK(1);//SCK上升沿,从机会自动把MOSI的数据读走,主机的任务,就是在这个上升沿后,把从机刚才放到MISO的数据位读进来
		if(MySPI_R_MISO() == 1){ByteSend |= 0x01;}//读MISO,把收到的数据放在ByteSend的最低位
		MySPI_W_SCK(0);//SCK产生下降沿
	}
	
	return ByteSend;
}

/***************模式0**************
void MySPI_Start(void)
{
	MySPI_W_SS(0);//置低电平 开始
}
void MySPI_Stop(void)
{
	MySPI_W_SS(1);//置高电平 结束
}

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i, ByteReceive = 0x00;
	
	for (i = 0; i < 8; i ++)
	{
		MySPI_W_MOSI(ByteSend & (0x80 >> i));
		MySPI_W_SCK(1);
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
		MySPI_W_SCK(0);
	}
	
	return ByteReceive;
}
***********************************/

/***************模式1**************
void MySPI_Start(void)
{
	MySPI_W_SS(0);//置低电平 开始
}
void MySPI_Stop(void)
{
	MySPI_W_SS(1);//置高电平 结束
}

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i, ByteReceive = 0x00;
	
	for (i = 0; i < 8; i ++)
	{
		MySPI_W_SCK(1);
		MySPI_W_MOSI(ByteSend & (0x80 >> i));
		MySPI_W_SCK(0);
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
	}
	
	return ByteReceive;
}
***********************************/

/***************模式2**************
void MySPI_Start(void)
{
	MySPI_W_SS(1);//置低电平 开始
}
void MySPI_Stop(void)
{
	MySPI_W_SS(0);//置高电平 结束
}

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i, ByteReceive = 0x00;
	
	for (i = 0; i < 8; i ++)
	{
		MySPI_W_MOSI(ByteSend & (0x80 >> i));
		MySPI_W_SCK(1);
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
		MySPI_W_SCK(0);
	}
	
	return ByteReceive;
}
***********************************/

/***************模式3**************
void MySPI_Start(void)
{
	MySPI_W_SS(1);//置低电平 开始
}
void MySPI_Stop(void)
{
	MySPI_W_SS(0);//置高电平 结束
}

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i, ByteReceive = 0x00;
	
	for (i = 0; i < 8; i ++)
	{
		MySPI_W_SCK(0);
		MySPI_W_MOSI(ByteSend & (0x80 >> i));
		MySPI_W_SCK(1);
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
	}
	
	return ByteReceive;
}
***********************************/

🔴 软件读写W25Q64注解

#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"

//驱动层
void W25Q64_Init(void)
{
	MySPI_Init();
}

//拼接完整是时序
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)//指针实现多返回值
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_JEDEC_ID);//从头文件中读取0x9F 读ID的指令
	*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//厂商ID。要给从机一个东西,此时目的是接收,所以给它抛的东西就没有意义,置换有意义的数据
	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//设备ID高8位
	*DID <<= 8;//把高8位左移8位
	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);//设备ID低8位
	MySPI_Stop();
}

void W25Q64_WriteEnable(void)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_WRITE_ENABLE);//写使能
	MySPI_Stop();
}

void W25Q64_WaitBusy(void)//等待忙,不忙就退出,忙就在函数里等待
{
	uint32_t Timeout;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);//读状态寄存器1
	Timeout = 10000;
	while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)//接收数据 用掩码取出最低为 Busy为1,就会进入循环,再次读出一次状态奇存器,继续判断,直到Busy为0,跳出循环
	{
		Timeout--;
		if(Timeout == 0)
		{
			break;
			//实际操作可以加入打印错误日志、进行系统系统复位
		}
	}//计次计时 超时推出 防止死循环
	MySPI_Stop();
}

//页范围XXXX00 ~ XXXXFF
//页内地址为后两位,若为FF,写入数据不能进行跨页  在FF出写入55 66 77 88,55写入至FF,66 77 88写入至00,不会另起一页
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)//数据用来输入
{
	uint16_t i;
	
	W25Q64_WriteEnable();//写使能仅对之后跟随的一条时序有效,一条时序结束后,会顺手关门
	
	MySPI_Start();
	MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
	MySPI_SwapByte(Address >> 16);//如果地址是0x123456,有移16位,就是0x12了,最高位的字节
	MySPI_SwapByte(Address >> 8);//如果地址是0x123456,有移8位,就是0x1234,但是交换字节函数只能接收8位数据,所以高位舍弃,实际发送0x34,就是中间的字节
	MySPI_SwapByte(Address);//如果地址是0x123456,舍弃高位,实际发送0x56,就是最低位的字节
	//连续发送24位字节就完成了
	for(i = 0; i < Count; i++)//写入Count次的DataArray数据
	{
		MySPI_SwapByte(DataArray[i]);//写入字节
	}
	MySPI_Stop();
	
	W25Q64_WaitBusy();//事后等待
}

//实际上如果不执行擦除,读出的数据 = 原始数据 & 写入的数据
//写入数据前必须擦除
void W25Q64_SectorErase(uint32_t Address)//指定地址所在扇区会被擦除
{
	W25Q64_WriteEnable();
	
	MySPI_Start();
	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	MySPI_Stop();
	
	W25Q64_WaitBusy();//W25Q64_ReadData不用调用 写入进行了,读取一定不忙
}
 
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)//数据用来输出
{
	uint32_t i;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_DATA);
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	for(i = 0; i < Count; i++)
	{
		DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//读取Count次的DataArray数据
	}
	MySPI_Stop();
}

8.4 SPI外设

  • STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担

  • 可配置8位/16位数据帧、高位先行/低位先行

  • 时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256) 外设时钟 比I2C快了90倍

  • 支持多主机模型、主或从操作

  • 可精简为半双工/单工通信

  • 支持DMA

  • 兼容I2S协议

  • STM32F103C8T6 硬件SPI资源:SPI1(APB2 72M)、SPI2(APB1 36M)

  • 高位移出去,通过GPIO,到MOSI,从MOSI输出,显然这是SPI的主机,之后移入的数据,从MISO进来,通过GPIO,到移位寄存器的低位,这样循环8次,就能实现主机和从机交换一个字节,然后TDR和RDR的配合,可以实现连续的数据流。

  • TDR数据,整体转入移位寄存器的时刻,置TXE标志位1,移位奇存器数据,整体转入RDR的时刻,置RXNE标志位(此标志为‘1’时表明在接收缓冲器中包含有效的接收数据,读SPI数据寄存器可以清除此标志),波特率发生器,产生时钟,输出到SCK引脚,数据控制器呢,就看成是一个管理员,它控制着所有电路的运行。

主模式全双工连续传输

  • 第一行是SCK时钟线,这里,CPOL=1,CPHA=1,即SPI模式3,所以SCK默认是高电平,然后在第一个下降沿,MOSI和MISO移出数据,之后,上升沿移入数据。

  • 第二行是MOSI和MISO输出的波形,跟随SCK时钟变化,数据位依次出现,这里从前到后,依次出现的是b0、b1,一直到b7,低位先行(实际高位先行多一些)。

  • 第三行是TXE标志位,发送奇存器空标志位

  • 第四行是发送缓冲器(写入SPI_DR,即TDR)

  • 第五行是BSY标志位,BUSY,是由硬件自动设置和清除的,当有数据传输时,BUSY置1

  • 首先,SS置低电平,开始时序,在刚开始时,TXE为1,表示TDR空,可以写入数据开始传输,第一步为软件写入0xF1至SPI_DR,0xF1,就是要发送的第一个数据,之后可以看到,写入之后,TDR变为0xF1,同时,TXE变为0,表示TDR已经有数据了,TDR是等候区,移位寄存器才是真正的发送区,移位寄存器刚开始肯定没有数据,所以等候区的F1就会立刻转入移位寄存器,开始发送,转入瞬间,置TXE标志为1,表示发送寄存器空,移位寄存器有数据,波形就会产生,在移位产生F1波形的同时,等候区TDR是空的,为了移位完成时,下一个数据能不间断地跟随,我们就要提早把下一个数据写入到TDR里等着了。第二步写入F1之后,软件等待TXE=1,一旦TDR空了,就写入F2至SPI_DR,写入之后,TDR的内容,就变成F2了,也就是把下一个数据放到TDR里候着,之后同理。F1数据波形产生完毕后,F2转入移位奇存器开始发送,这时,TXE=1,我们尽快把下一个数据F3放到TDR里等着(即软件等待TXE=1,然后写入F3至DR),写入后TDR变成F3,最后如果我们只想发送3个数据,F3转入移位寄存器之后,TXE=1,就不要继续写入了,之后TXE一直为1,但需要等待一段时间,F3的波形才能完整发送。发送完后,BUSY标志由硬件清除,此时,波形才发送完毕。

  • 以上为输出的流程

  • 第六行是MISO/MOSI的输入数据

  • 第七行是RXNE,接收数据寄存器非空标志位

  • 第八行是接收缓冲器(读出SPI_DR,即RDR)

  • SPI是全双工,发送的同时,还有接收,第一个字节发送完成后,第一个字节也接受成功,接收到的数据1,是A1,这时,移位寄存器的数据,整体转入RDR,RDR随后存储的就是A1,转入的同时,RXNE标志位也置1,表示收到数据了(软件等待RXNE=1,=1表示收到数据了),然后从SPI_DR,也就是RDR,读出数据A1,接收之后,软件清除RXNE标志位,然后,当下一个数据2收到之后,RXNE重新置1,我们监测到RXNE=1时,就继续读出RDR,为A2,最后,在最后一个字节时序完全产生之后数据3才能收到,一个字节的波形收到后,移位寄存器的数据自动转入RDR,会覆盖原有的数据,所以,我们读取RDR要及时,如果不及时,A2会覆盖A1,就不能实现连续数据流的接收了。

  • 以上为输入的流程

  • 发送数据1、发送数据2、之后接收数据1,然后再,发送数据3、接收数据2、发送数据4、接收数据3,这个交换的流程是交错的。

非联系传输

  • 首先,这个配置还是SPI模式3,SCK默认高电平,想发送数据时,如果检测到TXE=1了,TDR为空,就软件写入0xF1至SPI_DR,这时,TDR的值变为F1,TXE变为0,目前移位寄存器也是空,所以这个F1会立刻转入移位寄存器开始发送,波形产生,并且TXE置回1,表示你可以把下一个数据放在TDR里候着了(在连续传输的这里,一旦TXE=1了,我们就会把下一个数据写到TDR里候着,这样是为了连续传输,数据衔接更紧密,但流程混乱在非连续传输中,TXE=1了,我们不着急把下一个数据写进去,而是一直等待,等第一个字节时序结束,这时接收的RXNE会置1,先把第一个接收到的数据读出来,之后,再写入下一个字节数据,也就是软件等待TXE=1,但是较晚写入0xF2至SPI_DR,之后,数据2开始发送,但还是不着急写数据3,先把接收的数据2收着,再继续写入数据3,数据3时序结束后,最后,再接收数据3置换回来的数据

  • 第1步,等待TXE为1

  • 第2步,写入发送的数据至TDR

  • 第3步,等待RXNE为1

  • 第4步,读取RDR接收的数据

  • 之后交换第二个字节,重复这4步,这样,我们就可以把这4步封装到一个函数,调用一次,交换一个字节

⚫SPI库函数

void SPI_I2S_DeInit(SPI_TypeDef* SPIx);
//恢复缺省配置
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
void I2S_Init(SPI_TypeDef* SPIx, I2S_InitTypeDef* I2S_InitStruct);
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);
void I2S_StructInit(I2S_InitTypeDef* I2S_InitStruct);
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
void I2S_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);
//中断使能
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);
//DMA使能
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
//写DR数据寄存器
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
//读DR数据寄存器
void SPI_NSSInternalSoftwareConfig(SPI_TypeDef* SPIx, uint16_t SPI_NSSInternalSoft);
//NSS引脚配置
void SPI_SSOutputCmd(SPI_TypeDef* SPIx, FunctionalState NewState);
void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);
//8位或16位数据帧的配置
void SPI_TransmitCRC(SPI_TypeDef* SPIx);
void SPI_CalculateCRC(SPI_TypeDef* SPIx, FunctionalState NewState);
uint16_t SPI_GetCRC(SPI_TypeDef* SPIx, uint8_t SPI_CRC);
uint16_t SPI_GetCRCPolynomial(SPI_TypeDef* SPIx);
void SPI_BiDirectionalLineConfig(SPI_TypeDef* SPIx, uint16_t SPI_Direction);
//半双工,双向线的配置
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
//获取TXE和RXNE标志位的状态,配合写DR和读DR的函数,就可以控制时序的产生
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);
//获取标志位和清除标志位

🔴硬件读写W25Q64注解

#include "stm32f10x.h"                  // Device header

//函数封装
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}


void MySPI_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);//开启SPI时钟
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;//SS 软件
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;//SCK MOSI
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;//MISO
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//初始化SPI
	SPI_InitTypeDef SPI_InitStructure;
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;//主机 这个参数决定当前设备是SPI的主机还是从机
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//双线全双工 配置SPI的裁剪引脚
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;//8 配置8位还是16位数据帧
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//高位 配置高位先行还是低位先行
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;//72MHz/128 500KHz (若为APB1则为36MHz/128)配置SCK时钟的频率,分频系数越大,SCK时钟频率越小,传输越慢
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;//低电平 CPL=0时钟极性
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;//CPHA=0 时钟相位 边沿采样 
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//软件NSS
	SPI_InitStructure.SPI_CRCPolynomial = 7;//CRC校验的多项式
	SPI_Init(SPI1, &SPI_InitStructure);
	
	SPI_Cmd(SPI1, ENABLE);
	
	MySPI_W_SS(1);//默认高电平
}

void MySPI_Start(void)
{
	MySPI_W_SS(0);//置低电平 开始
}
void MySPI_Stop(void)
{
	MySPI_W_SS(1);//置高电平 结束
}

uint8_t MySPI_SwapByte(uint8_t ByteSend)//模式0
{
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);//等待TXE 不等于SET,while条件为真,进入循环等待
	SPI_I2S_SendData(SPI1, ByteSend);//ByteSend写入TDR是要发送的数据,ByteSend自动转入移位奇存器,一旦移位奇存器有数据了,时序波形就会自动产生
	//之后ByteSend就会通过MOSI一位一位地移出去,在MOSI线上,就会自动产生这个发送的时序波形,由于是非连续传输,就不必提前把下一个数据放到TDR里了,在发送的同时,MISO还会移位进行接收,发送和接收是同步的,接收移位完成时,会收到一个字节数据
	//写入DR时,会顺便执行清除TXE的操作,SPI_I2S_FLAG_TXE这个就不用手动清除,SPI_I2S_FLAG_RXNE同理
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);//等RXNE为1了,表示收到一个字节,同时也表示发送时序产生完成了
	return SPI_I2S_ReceiveData(SPI1);//读取RDR接收的数据
	/*	第1步,等待TXE为1
		第2步,写入发送的数据至TDR
		第3步,等待RXNE为1
		第4步,读取RDR接收的数据*/
}

9 Unix时间戳

  • Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒

  • 时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量

  • 世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间

9.1 UTC/GMT

  • GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准

  • UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致

9.2 时间戳转换

9.3 BKP

  • BKP(Backup Registers)备份寄存器

  • BKP可用于存储用户应用程序数据。当VDD(2.0~3.6V)电源被切断,他们仍然由VBAT(1.8~3.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位

  • TAMPER引脚产生的侵入事件将所有备份寄存器内容清除

  • RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲

  • 存储RTC时钟校准寄存器

  • 用户数据存储容量:

  • 20字节(中容量和小容量)/ 84字节(大容量和互联型)

  • 橙色部分:后备区域(BKP,RTC的相关电路)

    • 当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电,当VDD主电源上电时,后备区域供电会由VBAT切换到VDD,也就是主电源有电时,VBAT不会用到,这样可以节省电池电量

    • BKP里主要有数据存器、控制寄存器、状态寄存器和RTC时钟校准寄存器

      • 数据存器是主要部分,用来存储数据的,每个数据奇存器都是16位的,也就是,一个数据寄存器可以存2个字节,

        • 那对于中容量和小容量的设备,里面有DR1、DR2、一直到、DR10,一共10个数据寄存器,一个寄存器存两个字节,所以容量是20个

        • 对于大容量和互联型设备,里面除了DR1到DR10,还有DR11、DR12、一直到、DR42,总共42个数据寄存器,容量是84个字节

      • 侵入检测:可以从PC13位置的TAMPER引脚引入一个检测信号,当TAMPER产生上升沿或者下降沿时,清除BKP所有的内容,以保证安全

      • 时钟输出:可以把RTC的相关时钟,从PC13位置的RTC引脚输出出去,供外部使用,其中,输出校准时钟时,再配合校准存器,可以对RTC的误差进行校准

9.4 RTC

  • RTC(Real Time Clock)实时时钟

  • RTC是一个独立的定时器,可为系统提供时钟和日历的功能

  • RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.0~3.6V)断电后可借助VBAT(1.8~3.6V)供电继续走时

  • 32位的可编程计数器,可对应Unix时间戳的秒计数器

  • 20位的可编程预分频器,可适配不同频率的输入时钟

  • 可选择三种RTC时钟源:

    • HSE时钟除以128(通常为8MHz/128)

    • LSE振荡器时钟(通常为32.768KHz)

    • LSI振荡器时钟(40KHz)

  • 左边这一块是核心的、分频和计数计时部分,右边这一块是中断输出使能和NVIC部分,上面这一块是APB1总线读写部分,下面这一块是和PWR关联的部分,意思就是RTC的闹钟可以唤醒设备,退出待机模式。灰色部分为后备区域,这些电路在主申源电后,可以使用备用电池维持工作,并且这些模块在待机时都会继续维持供电

  • 首先,看分频和计数计时部分,这一块的输入时钟是RTCCLK,【RTCCLK的来源需要在RCC里进行配置,可选HSE、LSE(主选)、LSI。因为这3个时钟,频率各不相同,而目都远大于我们所需要的1Hz的秒计数频率】所以RTCCLG共安需要首先经过RTC预分频器进行分频,这个分频器由两个寄存器组成,上面这个是重装载寄存器RTC_PRL,下面这个,RTC_DIV,手册里叫作余数奇存器,【但实际上,这一块跟我们之前定时器时基单元里的计数器CNT和重装值ARR,是一样的作用】,每来一个输入时钟,DIV的值自减一次,自减到0时。再来一个输入时钟,DIV输出一个脉冲,产生溢出信号,同时DIV从PRL莱取重装值,回到重装值继续自减。

    • 比如RTCCLK输入时钟是32.768KHz,即32768Hz,为了分频之后得到1Hz,PRL就要给32767,这个数值是始终不变的,DIV可以保持初始值为0,那在第一个输入时钟到来时,DIV就立刻溢出,产生溢出信号给后续电路,同时,DIV变为重装值32767,然后第二个输入时钟,DIV自减变为32766,第三个时钟,DIV变为32765,一直这样,来一个输入时钟自减一次,直到变为0,然后再来一个输入时钟,就会产生一个溢出信号,同时DIV回到32767,以此往复循环,这样的话,也就是每来32768个输入脉冲,计数器溢出一次,产生一个输出脉冲,这就是32768分频了,分频输出后的时钟频率是1Hz,提供给后续的秒计数器

  • 计数计时部分:32位可编程计数器RTC CNT,就是计时最核心的部分,RTC还设计的有一个闹钟存器RTC_ALR,也是一个32位的奇存器,可以在ALR写一个秒数,设定闹钟,当CNT的值跟ALR设定的闹钟值一样时,这时就会产生RTC Alarm闹钟信号,通往右边的中断系统,在中断函数里,你可以执行相应的操作,同时,闹钟信号可以让STM32退出待机模式,【另外,这个闹钟值,是一个定值,只能响一次,所以如果想实现周期性的闹钟,那在每次闹钟响之后,都需要再重新设置一下下一个闹钟时间】

  • 中断部分:在左边这里,有3个信号可以触发中断,

    • RTC_Second,秒中断。来源就是CNT的输入时钟,如果开启这个中断,那么程序就会每秒进一次RTC中断

    • RTC_Overflow,溢出中断。来源就是CNT的右边,就是CNT的32位计数器计满溢出了,会触发一次中断,一般不会触发

    • RTC_Alarm,闹钟中断。当计数器和闹钟值相等时,触发中断,同时闹钟信号可以让STM32退出待机模式

  • 最左边是RTCCLK时钟来源,这一块需要在RCC里配置,3个时钟,选择一个,当作RTCCLK,之后,RTCCLK先通过预分频器,对时钟进行分频。【余数奇存器是一个自减计数器,存储当前的计数值】【重装寄存器是计数目标,决定分频值】分频之后,得到1Hz的秒计数信号,通向32位计数器,一秒自增一次,下面的32位闹钟值,可以设定闹钟,然后右边有3个信号可以触发中断,分别是秒信号、计数器溢出信号和闹钟信号,三个信号先通过中断输出控制,进行中断使能,使能的中断才能通向NVIC,然后向CPU申请中断

    • 配置这个数据选择,可以选择时钟来源

    • 配置重装寄存器,可以选择分频系数

    • 配置32位计数器,可以进行日期时间的读写

    • 需要闹钟的话,配置32位闹钟值即可

    • 需要中断的话,先允许中断,再配置NVIC,最后写对应的中断函数即可

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
江科大STM32笔记是关于STM32单片机的学习笔记,其中涵盖了一些关于按键初始化和按键读取的代码示例。在这些代码中,通过引用中的Key_Init函数来对按键进行初始化,然后通过引用中的Key_GetNum函数来获取按键按下的键码值。代码中使用了STM32的GPIO模块来配置引脚的工作模式和读取引脚的电平状态。此外,引用中提到STM32内部集成了硬件收发电路,可以通过写入控制寄存器CR和数据寄存器DR来实现与外设的通信,并通过读取状态寄存器SR来了解外设电路的当前状态。这些寄存器的使用可以实现对外设的控制和监测,减轻CPU的负担。因此,江科大STM32笔记主要是介绍了STM32单片机的相关知识和编程技巧。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [STM32学习笔记 -- I2C(江科大)](https://blog.csdn.net/weixin_61244109/article/details/131002266)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [STM32江科大学习笔记](https://blog.csdn.net/weixin_38647099/article/details/128337708)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值