文章目录
前言
本篇文章属于学习笔记,来源于B站教学视频,具体代码可以从视频中获取。个人纯小白,感觉Up讲解得很好,适合学习,在这里推荐给大家。个人学习笔记,只能做参考,细节方面建议观看视频。
《STM32入门教程》: https://www.bilibili.com/video/BV1th411z7sn/?spm_id_from=333.999.0.0
https://www.bilibili.com/video/BV1th411z7sn/?spm_id_from=333.337.search-card.all.click
##下载接口
JTAG——仿真,调试,下载
SWD——下载,仿真(推荐,占用接口少,可以仿真调试)
三、GPIO输入
51中,int16位
STM32中,int占32位,想表示16位的数据,需要用short
char——在单片机中通常用它来存放整数而不是字符
char函数的本质就是一个字节的int类型(弹幕)
结构体指针访问,地址传递“->”
数据太大了,用指针直接访问省内存?(弹幕,主页视频)
enum——限制取值范围,比如(week,限定取值1~7)
为什么要配置时钟
STM32每个I/O口都有一个时钟,时钟不开,就算有高低电平叶输不出去(弹幕)
参考手册6.3.7——APH外设时钟使能寄存器(RCC_AHBENR)
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
//Step1:使用RCC开启GPIO的时钟
//Step2:使用GPIO_Init函数初始化GPIO
//Step3:使用输出或输入的函数控制GPIO口
int main(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_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_ResetBits(GPIOA, GPIO_Pin_0);
while(1)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_0); //点亮
Delay_ms(500);
GPIO_SetBits(GPIOA, GPIO_Pin_0);
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET); //点亮
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)0); //点亮,1高电平,强制转换成BitAction枚举类型
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)1);
Delay_ms(500);
}
}
GPIO使用方法总结:
首先初始化时钟,然后定义结构体,赋值结构体
GPIO_Mode可以选择8种输入输出模式
GPIO_Pin可以选择引脚,可以用按位或的方式同时选中多个引脚
GPIO_Speed可以选择速度
最后使用GPIO_Init()函数,将指定的GPIO外设初始化好
8个读写函数,
★Q : 上拉、下拉以及浮空如何选择?
A : 上拉和下拉的选择原则:一般可以看一下接在这个引脚的外部模块输出的默认电平。如果外部模块空闲默认输出高电平,我们就选择上拉输入,默认输入高电平;如果外部模块默认输出低电平,我们配置下拉输入,默认输入低电平(一般来说,默认高电平,习惯)——和外部模块保持默认状态一致,防止默认电平打架
如果不确定外部模块输入的默认状态,或者外部信号输出功率非常小,这时就尽量选择浮空输入。浮空输入——没用上拉电阻和下拉电阻去影响外部信号,但是缺点就是当引脚悬空时,没用默认的电平了,输入就会受噪声干扰,来回不断地跳变。
四、OLED
正定原子“OLED.C”
“LED.C”——对应正定原子MiniSTM32板子修改过的
#include "stm32f10x.h" // Device header
void LED_Init(void)
{
//LED0——PA8,LED1——PD2
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
//LED0——PA8
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_8); //熄灭
//LED1——PD2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_SetBits(GPIOD, GPIO_Pin_2); //熄灭
}
void LED0_OFF(void)
{
GPIO_SetBits(GPIOA, GPIO_Pin_8);
}
void LED0_ON(void)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_8);
}
void LED0_Turn(void)
{
/*调用GPIO_ReadOutputDataBit, 读取当前端口状态,当前输出1,则置0,反之; */
if (GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_8) == 0)
{
GPIO_SetBits(GPIOA, GPIO_Pin_8);
}
else
{
GPIO_ResetBits(GPIOA, GPIO_Pin_8);
}
}
void LED1_OFF(void)
{
GPIO_SetBits(GPIOD, GPIO_Pin_2);
}
void LED1_ON(void)
{
GPIO_ResetBits(GPIOD, GPIO_Pin_2);
}
void LED1_Turn(void)
{
/*调用GPIO_ReadOutputDataBit, 读取当前端口状态,当前输出1,则置0,反之; */
if (GPIO_ReadOutputDataBit(GPIOD, GPIO_Pin_2) == 0)
{
GPIO_SetBits(GPIOD, GPIO_Pin_2);
}
else
{
GPIO_ResetBits(GPIOD, GPIO_Pin_2);
}
}
“LED.h”
#ifndef __LED_H__
#define __LED_H__
void LED_Init(void);
void LED0_ON(void);
void LED0_OFF(void);
void LED0_Turn(void);
void LED1_ON(void);
void LED1_OFF(void);
void LED1_Turn(void);
#endif
五、EXTI外部中断
1.代码思路:
从GPIO到NVIC一路出现的外设模块都配置好
Step1: 配置RCC,打开涉及的外设的时钟都打开(开关,不打开时钟,外设没法工作)
Step2:配置GPIO,选择端口的模式(这里选择输入模式)
Step3:配置AFIO,选择我们使用的这一路的GPIO,连接到后面的EXTI
Step4:配置EXTI,选择边沿触发方式,比如上升沿、下降沿、或者双边沿;还有选择触发响应方式,可以选择中断响应和事件响应(一般中断响应)
Step5:配置NVIC,给中断选择一个合适的优先级,最后同归NVIC,外部中断信号就能进入CPU了
RCC -> GPIOx -> AFIO -> EXTI -> NVIC -> CPU
Q:寄存器里没用EXTI时钟的控制位
A: EXTI模块是由NVIC模块直接控制的,并不需要单独的外设时钟(弹幕)
另外,NVIC也不需要开启时钟,是因为NVIC是内核的外设,内核的外设都不需要开启时钟,RCC管的都是内核外的外设
六、TIM定时器(Timer)
6.1 定时器简介
- 定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断
- 16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时
- 不仅具备基本的定时中断功能,而且还包括内外时钟源选择、输入捕获、输出捕获、编码器接口、主从触发模式等多种功能
- 三种类型:高级定时器、通用定时器、基本定时器
6.2 时基单元
可编程高级控制定时器的主要部分是一个16位计数器和与其相关的自动装载寄存器。这个计数器可以向上计数、向下计数或者向上向下双向计数。此计数器时钟由预分频器分频得到。
计数器、自动装载寄存器和预分频器寄存器可以由软件读写,即使计数器还在运行读写仍然有效。
时基单元包含:
- 计数器寄存器(TIMx_CNT)
- 预分频器寄存器(TIMx_PSC)
- 自动装载寄存器(TIMx_ARR)
- 重复次数寄存器(TIMx_RCR)
6.3 定时器定时中断 & 定时器外部时钟
6.4 TIM输出比较(OC——Output Compare)
主要用来输出PWM波形
PWM波形——Pluse Width Modulation 脉冲宽度调制
(惯性系统中)
频率 = 1 / Ts ,占空比 = Ton / Ts ,分辨率 = 占空比变化间距
(这里的频率越快 = 频率值越小, 变换越快 = 频率值越大)
(分辨率是指输出的电压梯度,频率指输出变化的次数——弹幕)
1) 8种输出比较模式
1~8
模式 | 描述 |
---|---|
①冻结 | CNT = CCR时,REF保持为原状态 |
②匹配时置有效电平 | CNT = CRR时,REF置有效电平 |
③匹配时置无效电平 | CNT = CRR时,REF置无效电平 |
④匹配时电平翻转 | CNT = CRR时,REF电平翻转 |
⑤强制为无效电平 | CNT与CRR无效,REF强制为无效电平 |
⑥强制为有效电平 | CNT与CRR无效,REF强制为有效电平 |
⑦PWM模式1 | 向上计数:CNT < CRR时,REF置有效电平;CNT ≥ CRR时,REF置无效电平;向下,反之; |
⑧PWM模式2 | 如上,反之 |
注:一般选用PWM模式1向上计数就行,取反的情况可以通过TIMx_CCER
参数计算:
PWM频率: Ferq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比:Duty = CCR / (ARR + 1)
PWM分辨率:Reso = 1 / (ARR + 1)
ARR——Automatic Reloader Register
CRR——Capture Compare Register
Step1 : RCC开启时钟,把要用的TIM外设和GPIO外设的时钟打开
Step2 : 配置时基单元
Step3 : 配置输出比较单元,模式,极性,输出使能
Step4 : GPIO,初始化为复用推挽输出
TIM_OC1234Init();
void TIM_SetCompare123(); //占空比
tim.h,1056开始
★配置输出比较模块,Output Compare**
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);
★用来单独更改CCR寄存器值的函数,在运行的时候,更改占空比,就需要用到下面四个函数**
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);
在使用高级定时器输出PWM时,需要调用这个函数,使能主输出,否则PWM将不能正常输出
void TIM_CtrlPWMOutputs(TIM_TypeDef *TIMx, FunctionalState NewState);
给输出比较配默认值
void TIM_OCStructInit(TIM_OCInitTypeDef *TIM_OCInitStruct);
//赋初值,再改
TIM_OCStructInit(&TIM_OCInitStructure);
配置强制输出模式
如果在运行中想要暂停输出波形并且强制输出高或低电平,可以用这个函数。(用得不多,因为强制输出高电平和设置100%占空比是一样的)
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_CCPreloadControl(TIM_TypeDef* TIMx, FunctionalState NewState);
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);
1000行出头,TIM_CtrlPWMOutputs;
仅高级定时器,输出PWM时,需要调用这个函数,使能
重映射——AFIO完成
★TIM_CtrlPWMOutputs(TIM1, ENABLE);
使用引脚复用功能——先打开AFIO时钟,再用AFIO将JTAG复用解除掉
时钟→用AFIO重映射外设复用的引脚,如果重映射的引脚又正好是调试端口,加上第三句
6.5 IC(Input Capture)输入捕获
捕获比较寄存器CCR——Capture Compare Register
__ 输入捕获模式下,当通道输入引脚出现指定电平跳变,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数 __
频率——1s内出现了多少个重复的周期
☆三种频率测量的方法
高频适合测频法,低频适合测周法
P254图98:
异或门,CH1和CH2可以把** 一个引脚的输入,同时映射到两个捕获单元,PWMI模式的经典结构 **
代码逻辑
注意滤波器和分频器的区别:
虽然它俩都是计次的东西,但是滤波器计次,并不会改变信号的原有频率,一般滤波器的采样频率都会远高于信号频率,所以它只会滤除高频噪声,使信号更平滑,1KHz滤波之后仍然是1KHz,信号频率不会变化;
而分频器就是对信号本身就行计次了,会改变频率,1KHz,2分频之后就是500Hz,4分频就是250Hz;
【输入捕获模式测频率】
【IC.c】
#include "stm32f10x.h" // Device header
void IC_Init(void)
{
/*Step1: Turn on the clock*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/*Step2: Configure GPIO*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_InternalClockConfig(TIM3);
/*Step3: Configure the time base unit*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
/*Step4: Initialize the input capture unit*/
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);
/*Step5: The trigger source of TRGI is configured as TI1FP1 */
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1); //Trigger source
/*Step6: The configuration slave mode is Reset*/
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
/*Step7: Invoke TIM_Cmd*/
TIM_Cmd(TIM3, ENABLE);
}
uint32_t IC_GetFreq(void)
{
return 1000000 / TIM_GetCapture1(TIM3);
}
6.7编码器接口(Encoder Interface)
编码器接口可接受增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度
正交编码器:正转 / 反转
正转的状态都向上计数,反转的状态都向下计数
补充,正交编码器抗噪声原理
代码思路:
1.配置RCC
2.配置GPIO,PA6和PA7配置成输入模式
3.配置时基单元,不分频, ARR65535,CNT计数
4.配置输入捕获单元,滤波器和极性
5.配置编码器接口模式
6.调佣TIM_Cmd,启动定时器
void TIM_EncoderInterfaceConfig();
编码器接口会托管时钟,就是一个带方向控制的外部时钟
时基单元,计数模式,计数方向也被托管
七、ADC模数转化器
(Analog-Digital Converter)
对高电平和低电平之间的任意电压进行量化,最终用一个变量来表示,读取这个变量,就可以知道引脚的具体电压是多少了,所以ADC其实就是一个电压表,把引脚的电压值测出来,放在一个变量里,这就是ADC的作用
ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁
(ADC其实就是一个电压表,把引脚的电压值测出来,放在一个变量里)
(STM32主要是数字电路,数字电路只有高低电平,没有几V电压的概念,所以如果想读取电压值,就需要借助ADC模数转化器来实现了)
7.1.2 转换时间
AD转换的步骤:采样,保持,量化,编码
STM32 ADC的总转换时间为:
TCONV = 采样时间 + 12.5个ADC周期
例如:当ADCCLK = 14MHz,采样时间为1.5个ADC周期
TCONV = 1.5 + 12.5 = 14个ADC周期 = 1us
【AD单通道】:代码思路,打通下面通路
Step1:开启RCC时钟,包括ADC和GPIO的时钟,配置ADCCLK的分频器。
Step2:配置GPIO,把需要用的GPIO配置成模拟输入的模式。
Step3:配置多路开关,把左边的通道接入到右边的规则组列表里(这个过程就是我们之前说的点菜,把各个通道的菜,列在菜单里)。
Step4:配置ADC转换器,在库函数里,用结构体来配置,可以配置中间一大块电路的参数,包括ADC是单次转换还是连续转换、扫描还是非扫描、有几个通道、触发源是什么,诗句对其是左对齐还是右对齐。
如果需要配置看门狗,有几个函数用来配置阈值和监测通道;如果想开启中断,就在中断输出控制里用ITConfig函数开启对应的中断输出,然后再在NVIC里,配置一下优先级,这样就能触发中断了。
Step5:开关控制,调用一下ADC_Cmd函数,开启ADC。( 可以对ADC进行校准,减小误差 )
void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState); //Power up the ADC
void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState); //Enable DMA output signal
在AIN模式下,GPIO口是无效的, 断开GPIO,防止你GPIO口的输入输出对我的模拟电压造成干扰,所以AIN模式就是ADC的专属模式。
八、DMA直接存储器存取
(Direct Momory Access)
DMA可以提供外设(一般是外设的数据寄存器DR,比如ADC的数据寄存器、串口的数据寄存器等等)和存储器(运行内存的SRAM和程序存储器Flash)或者存储器和存储器之间(Flash→SRAM,软件触发)的高速数据传输,无须CPU干预, 节省了CPU的资源。
DMA是一个数据转运小助手,主要用来协助CPU,完成数据转运的工作。
ROM——只读存储器,是一种非易失性、掉电不丢失的存储器
RAM——随机存储器,是一种易失性、掉电丢失的存储器
选项字节——存的主要是Flash的读保护、写保护、看门狗等等的配置
运行内存SRAM——程序中定义变量、数组、结构体的地方
外设寄存器——存储各个外设的配置参数,也就是初始化各个外设,最终所读写的东西
内核外设寄存器——存储内核各个外设的配置参数,内核外设就是NVIC和SysTick
寄存器是一种特殊的存储器。一方面,CPU可以对寄存器进行读写,就像读写运行内存一样;另一方面,寄存器的每一位背后,都连接了一根导线,这些导线可以用于控制外设电路的状态,比如置引脚的高低电平、导通和断开开关、切换数据选择器或者多位组合起来,当做计数器、数据寄存器等等等。
所以,寄存器是连接软件和硬件的桥梁。软件读写寄存器,就相当于在控制硬件的执行
DMA总线——用于访问各个寄存器
DMA内部的多个通道——可以进行独立的数据转运
仲裁器——用于调度各个通道,防止产生冲突
AHB从设备——用于配置DMA参数
DMA请求——用于硬件触发DMA的数据转运
CPU或者DMA直接访问Flash的话,是只可以读而不可以写的
SRAM是运行内存,可以任意读写,没有问题
这个软件触发并不是调用某个函数一次,触发一次,其执行逻辑为,以最快的速度,连续不断地触发DMA,争取早日把传输计数器清零,完成这一轮的转换(理解为连续触发?)。
软件触发一般适用于存储器到存储器的转运。
硬件触发——硬件触发源可以选择ADC、串口、定时器等等,使用硬件出发的转运,一般都是与外设有关的转运,这些转运需要一定的时机,比如ADC转换完成、串口收到数据、定时时间到等等,所以需要使用硬件触发,在硬件达到这些时机时,传一个信号过来,来触发DMA进行转运。
当我们的程序中出现了一大批数据,并且不需要更改时,就可以把他定义成常量,这样能节省SRAM的空间(放在Flash),比如查找表、字库数据等等。
★计算某个寄存器的地址
通过查手册进行计算,首先查一下这个寄存器所在外设的起始地址,然后再在外设的寄存器总表里,查一下偏移,起始地址+偏移,就是这个寄存器的实际地址。
DMA转运有三个条件:
(1)传输计数器大于0(DMA_BufferSize)
(2)触发源有触发信号(DMA_M2M_ENABLE)
(3)DMA使能
注:三个条件缺一不可
九、USART串口协议
通信的目的:将一个设备的数据发送到另一个设备,扩展硬件系统。
通信协议:制定通信的规则,通信双方按照协议规则进行数据收发。
(寻址就是给不同的设备编号,对应不同的学生的名字,老师课堂点名,多设备间,弹幕)
9.1串口USART
USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里。
TX和RX要交叉连接
TTL电平:3.3V或者5V表示1, 0V表示0
晶体管-晶体管逻辑电平(transistor transistor logic)
波特率:串口通信的速率,本来的意思是每秒传输码元的个数,码元/s或者直接叫Baud 。
比特率:每秒传输的比特数,bit/s,bps ,在二进制调制的情况下, 一个码元就是一个bit,此时波特率就等于比特率。
理解一下起始位(低电平0)和停止位(高电平1)
TX引脚输出定时翻转的高低电平,RX引脚定时读取引脚的高低电平
USART通用同步/异步收发器
Step1:开启时钟,把需要用的USART和GPIO的时钟打开。
Step2:GPIO初始化,把TX配置成复用输出,RX配置成输入。
Step3:配置USART,直接使用一个结构体,就可以把所有的参数都配置好。
Step4:如果只需要发送的功能,就直接开启USART,初始化就结束了;如果需要接收的功能,可能还需要配置中断,那就在开启USART之前,再加上ITConfig和NVIC的代码就行了。
fputc() 是printf()的底层
可变参数的用法
开启中断
9.4 USART串口数据包
优点:传输最直接,解析数据非常简单,比较适合一些模块发送原始的数据,比如一些使用串口通信的陀螺仪、温湿度传感器。
缺点:灵活性不足、载荷容易和包头包尾重复。
优点:数据直观易理解,非常灵活,比较适合一些输入指令进行人机交互的场合,如比蓝牙模块常用的AT指令,CNC和3D打印机常用的G代码。
缺点:解析效率低。
状态机的画和使用
9.2串口发送 + 接收
配置中断的过程:
开启中断,配置NVIC,使用查询的方式,
查询的流程:
在主函数里不断判断RXNE标志位,如果置1了,说明收到了数据,再调用ReceiveData,读取DR寄存器,这样就行了。
使用中断:
首先,在初始化的地方,加上开启中断的代码,
Step1:USART_ITConfig(USART1,)
十、I2C通信协议
★时序
I2C总线(Inter IC BUS)
两根通信线:SCL,SDA;同步,半双工;
同步和异步的区别
异步时序:
好处就是省一根时钟线,节省资源。
缺点就是对时间要求严格,对硬件电路的依赖比较严重。
同步时序:
好处就是对时间要求不严格,对硬件电路不怎么依赖,在一些低端单片机,没有硬件资源的情况下,也很容易使用软件来模拟时序。
缺点就是多一根时钟线。
缺点:
由于I2C开漏外加上拉电阻的电路结构,使得通信线高电平的驱动能力比较弱,这就会导致,通信线又低电平变到高电平的时候,这个上升沿耗时比较长,这会限制I2C的最大通信速度,所以I2C的标准模式,只有100KHz的时钟频率,I2C的快速模式,也只有400KHz
硬件电路
主机拥有SCL的绝对控制权,所以主机的SCL可以配置成推挽输出,所有丛集的SCL都配置成浮空输入或者上拉输入。
数据流向是,主机发送,所有从机接收。
SDA半双工,主机的SDA在发送的时候是输出,在接收的时候是输入,从机的SDA也会在输入和输出之间反复切换。如果两个引脚同时处于输出的状态,又正好是一个输出高电平,一个输出低电平,就会电源短路,避免总线没协调好导致电源短路。
发送 / 接收
高位先行,主机在接收之前,需要释放SDA。
10.2 MPU6050简介
6轴姿态传感器
3轴加速度计(Accelerometer):测量xyz加速度
3轴陀螺仪传感器(Gyroscope):测量xyz角速度
10.3 软件I2C读写MPU6050
首先建立I2C通信层的.c和.h模块,在通信层里,写好I2C底层的GPIO初始化和6个时序基本单元——起始、终止、发送一个字节、接受一个字节、发送应答和接收应答。
写好I2C通信层后,再建立MPU6050的.c和.h模块,在这一层,我们将基于I2C通信的模块来实现指定地址读、指定地址写,再实现写寄存器对芯片进行配置,读寄存器得到传感器数据。
最后再main.c里,调用MPU6050的模块。
初始化→拿到数据→显示数据
软件初始化
第一个任务:把SCL和SDA都初始化为开漏输出模式
第二个任务:把SCL和SDA置高电平
★按位与的方式,去除数据的某一位或者某几位
typedef enum
{
Bit_RESET = 0,
Bit_SET;
}BitAction;
BitAction是个枚举类型
多层的模块架构,
最底层I2C协议层,主要关注点是,引脚是哪两个,什么配置,时序什么时候高电平,什么时候低电平,这些协议相关的内容。
之后协议层之上,是MPU6050的驱动层,主要关注点是,如何读写寄存器,怎么配置寄存器,怎么读取数据,这些驱动相关内容。
最后就是主函数的应用层了,只需要调用GetData函数,得到数据。剩下的关注点,如何用这些数据,来完成程序的功能设计。
10.4 I2C外设简介
STM32内部继承了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担。
发送的时候,数据先写入数据寄存器,如果移位寄存器没有数据,再转到移位寄存器进行发送
GPIO复用开漏模式,复用就是GPIO的状态是交由片上外设来控制的
用硬件:
Step1:配置I2C外设,对I2C2外设进行初始化,来替换这里的MyI2C_Init()。
Step2:控制外设电路,实现指定地址写的时序,来替换这里的WriteReg()。
Step3:控制外设电路,实现指定地址读的时序,来替换这里的ReadReg()。
第一步:开启I2C外设和对应的GPIO口的时钟。
第二步:把I2C外设对应的GPIO口初始化为复用开漏模式。
第三步:使用结构体,对整个I2C进行配置。
第四步:I2C_Cmd(),使能I2C。
I2C1和I2C2都是APB1的外设,GPIO是APB2的外设
开漏,就是I2C协议的设计要求;
复用,就是GPIO的控制权要交给硬件外设。
while()防止死循环,超时退出机制
简单的计数等待,
uint32_t Timeout;
Timeout = 10000;
while(......)
{
Timeout --;
if (Timeout == 0)
{
break;
}
}
十一、SPI通信
Serial Peripheral Interface——串行外设接口
MID,M——manufacturer
四根通信线:
SCK——Serial Clock
MOSI——Master Output Slave Input
MISO——Master Input Slace Output
SS——Slave Select
特征:同步,全双工
1、SPI传输更快
2、设计简单粗暴
3、SPI的硬件开销比较大,通信线的个数比较多,通信过程中,经常会有资源浪费的现象
同步时序的好处——数据位额输出和输入,都是在SCK的上升沿或下降沿进行的,这样,数据位的收发时刻就可以明确的确定,并且,同步时序,时钟快点慢点,或者中途休息一会儿,都是没问题的.
全双工——数据发送和数据接收单独各占一条线,发送用发送的线路,接收用接收的线路,两者互不影响。简单高效,输出线就一直输出,输入线就一直输入,数据流的方向也不会改变,也不用担心发送和接收没协调好冲突。
坏处:多了一根线,会有通信资源的浪费。
需要找谁,就把谁的SS置低电平,主机只能置一个SS为低电平,只能选中一个从机。主机同时选中多个从机,就会导致数据冲突。
硬件电路
推挽输出,高低电平均有很强的驱动能力,这将使得SPI引脚信号的下降沿、上升沿非常迅速,不像I2C那样,下降沿非常迅速,但是上升沿就缓慢了。得益于推挽输出的驱动能力,SPI信号变化的快,就能达到更高的传输速度,可以轻松达到MHz的输出频率。
SPI的数据收发,都是基于字节交换,这个基本单元来进行的。当主机需要发送一个字节,并且同时需要接收一个字节时,就可以执行一个字节交换的时序,这样,主机要发送的数据,跑到从机,主机要接收的数据,跑到主机
【移位示意图】
SPI通信的基础就是交换一个字节,有了交换一个字节,就可以实现,发送一个字节、接收一个字节和发送同时接收一个字节,这三种功能。
SPI在只接收或者只发送时,会存在一些资源浪费现象。
SPI时序基本单元
SS低电平有效,选中过程中,SS要始终保持低电平
时钟极性CPOL——Clock Polarity
时钟相位CPHA——Clock Phase
模式0是最常见的模式,很多SPI设备都支持这种模式,因此兼容性较好。模式0可以实现较高的传输速率,因为数据在时钟的上升沿就被采样,不需要等待下降沿。(弹幕)
I2C的规定一般是,有效数据流第一个字节是寄存器地址,之后依次是读写的数据,使用的是读写寄存器的模型。
SPI中,通常采用的是指令码加读写数据的模型,SPI起始后,第一个交换发送给从机的数据,一般叫做指令码,在从机中,对应的会定义一个指令集,当我们需要发送什么指令时,就可以在起始后第一个字节,发送指令集里面的数据,这样就能指导从机完成相应的功能。
11.2 W25Q64简介
非易失性存储器
存储介质:Nor Flash(闪存)
存储容量(24位地址)
易失性寄存器:一般就是SRAM、DRAM等
非易失性存储器:一般就是E2PROM、Flash等,数据掉电不丢失。
Flash——STM32的程序存储器、U盘、电脑里的固态硬盘等。
Flash:分为Nor Flash和Nand Flash
软件实现主打的是方便灵活;
硬件实现主打的是高性能、节省软件资源。
最常用,8位高位先行;
串口,低位;
SPI的时钟,其实就是由PCLK分频而来,PCLK(Peripheral Clock)就是外设时钟,APB2的PCLK就是72MHz,APB1的PCLK是36MHz
注意SPI1,SPI2的位置(APB1/2)
TDR、TXE、RDR、RXNE
移位寄存器,整理转入RDR的时刻,置RXNE标志位;
TDR数据,整体转入移位寄存器的时刻,置TXE标志位;
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
//SPI的速度非常快,所以操作完引脚后,就不要延时了
}
先SS下降沿,再移出数据;再SCK上升沿,再移入数据,再SCK下降沿,再移出数据。
主机移出我的数据最高位放到MOSI上,从机移出它的数据最高位放到MISO上。
模式0~3:
①改变相位——将MySPI_W_SCK(1);
MySPI_W_SCK(0);提前
②改变极性——就是把SCK,0改1,1改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); //最高位存到ByteReceive
}
MySPI_W_SCK(0);
}
return ByteReceive;
}
非连续传输:P468 图218时序图
第1步,等待TXE位1;
第2步,写入发送的数据至TDR;
第3步,等待RXNE为1;
第4步,读取RDR接收的数据;
之后交换第二个字节,重复这四步。(将这4步封装成一个函数)
如果想在极限频率下,进一步提高数据传输速率,也就是追求最高性能, 那最好使用连续传输的操作逻辑了,或者,还要进一步采用DMA自动转运,这些方法效率都是非常高的。——在实际开发中,应该会更倾向这一种吧?
MySPI.c
#include "stm32f10x.h" // Device header
//包装4个函数
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
//SPI的速度非常快,所以操作完引脚后,就不要延时了
}
void MySPI_W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
void MySPI_W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (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); //High Level,默认不选中从机
MySPI_W_SCK(0); //使用SPI模式0,所以默认低电平
}
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
//核心部分,交换一个字节
//WriteReadByte
//传进来的参数,通过交换字节的时序发送出去
//返回值是ByteReceive,是通过交换一个字节接收到的数据
//先下降沿再数据移出,下降沿是触发移出这个动作的条件
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); //最高位存到ByteReceive
}
MySPI_W_SCK(0);
}
return ByteReceive;
}
/*第二种方法
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t i;
for (i = 0; i < 8; i ++)
{
MySPI_W_MOSI(ByteSend & 0x80); //输出最高位
ByteSend <<= 1; //移位效率更高,最低位补0
MySPI_W_SCK(1);
if (MySPI_R_MISO() == 1)
{
ByteSend |= 0x01;
}
MySPI_W_SCK(0);
}
return ByteSend;
}
*/
MySPI.h
#ifndef __MYSPI_H__
#define __MYSPI_H__
void MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);
#endif
宏定义去替换指令码——W25Q64_Ins.h
#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
十二、UNIX时间戳、BKP&RTC
12.1 UNIX时间戳
头文件time.h中的几个函数
a.struct tm *gmtime(const time_t *timer)
timer 的值被分解为 tm 结构,并用协调世界时(UTC)也被称为格林尼治标准时间(GMT)表示。
b.struct tm *localtime(const time_t *timer)
timer 的值被分解为 tm 结构,并用本地时区表示。
c.time_t mktime(struct tm *timeptr)
把 timeptr 所指向的结构转换为一个依据本地时区的 time_t 值。
12.2 BKP备份寄存器&RTC实时时钟
12.3 读写备份寄存器&实时时钟
十三、PWR电源控制