写在前面:本文是基于哔哩哔哩江协科技
的STM32入门教程-2023版 细致讲解 中文字幕
学习时写的笔记,复习查阅方便,如有侵权,联系删除。
另外本人也是初学者,有很多理解不透彻的或者错误的理解,希望大家多多批评指针,一起进步。
一、零散知识点
复位:先低电平,再高电平
断路:也叫开路,断开
短路:用导线将电源或者电子元件两端连起来
三极管:基极,发射极,集电极,PNP的三极管在上边,NPN的三极管在下边。
滤波电容:一端接在电路中,一端接地,大部分为滤波电容,保持电路稳定,分析电路可以抹掉。
MOS管:左边是控制极,高电平导通,低电平断开
老版本的是ST关键字的写法,而新版本用的都是stdint关键字,为了兼容老版本也支持ST关键字
1MHz的周期就是1us
72MHz计数72次就是1us
72MHz计数72*103 就是1ms
// 结构体
// 枚举
typedef enum {
MONDAY = 1,
TUESDAY, // 如果是顺序的,自己赋值
WEDNESDAY
} Week_t;
Week_t week;
week = MONDAY;
取出多字节的每个字节
#define Byte0(data) ( *( (char *)(&data) ) )
#define Byte1(data) ( *( (char *)(&data) + 1) )
#define Byte2(data) ( *( (char *)(&data) + 2) )
#define Byte3(data) ( *( (char *)(&data) + 3) )
二、STM32介绍
1.STM32
- STM32是ST公司基于ARM Cortex-M内核开发的32位微控制器
- STM32常应用在嵌入式领域,如智能车、无人机、机器人、无线通信、物联网、工业控制、娱乐电子产品等
- STM32功能强大、性能优异、片上资源丰富、功耗低,是一款经典的嵌入式微控制器
2.ARM
- ARM既指ARM公司,也指ARM处理器内核
- ARM公司是全球领先的半导体知识产权(IP)提供商,全世界超过95%的智能手机和平板电脑都采用ARM架构
- ARM公司设计ARM内核,半导体厂商完善内核周边电路并生产芯片
3.STM32F103C8T6
- 系列:主流系列STM32F1
- 内核:ARM Cortex-M3
- 主频:72MHz
- RAM:20K(SRAM)
- ROM:64K(Flash)
- 供电:2.0~3.6V(标准3.3V)
- 封装:LQFP48
STM32是一个系列,STM32F103C8T6是这个系列中的其中一个芯片
8Mhz经过倍频,得到72Mhz的主频
4.片上资源/外设
5.命名规则
ld: 低度产品,FLASH小于64K
md: 中等密度产品,FLASH=64 or 128
hd: 高密度产品,FLASH大于128
6.系统结构
RCC管的内核外的外设,内核外的外设要用,必须先开启时钟,EXTI除外(一直打开)
7.引脚定义
8.启动配置BOOT
寄存器的0地址
9.最小系统电路
三、软件安装以及新建工程
1.软件安装
-
安装Keil5 MDK
如果之前安装过Keil C51,直接安装在Keil C51的目录 -
安装器件支持包
有离线安装和在线安装两种方式,离线安装直接双击,路径会自己选择,在线安装点击下图所示位置
-
软件注册
-
安装STLINK驱动
插上STLINK如果在其它设备那里显示黄色警告就说明没有驱动,安装即可 -
安装USB转串口驱动
插上USB转串口如果在其它设备那里显示黄色警告就说明没有驱动,安装即可
2.新建工程
有三种方式:
- 基于寄存器:基于寄存器类似于51开发
- 基于标准库:ST公司封装好的函数库,也是本文使用的方式
- 基于HAL库:图形化快速上手
大概步骤如下,具体步骤下面有 - 建立工程文件夹,Keil中新建工程,选择型号
- 工程文件夹里建立Start、Library、User等文件夹,复制固件库里面的文件到工程文件夹
- 工程里对应建立Start、Library、User等同名称的分组,然后将文件夹内的文件添加到工程分组里
- 工程选项,C/C++,Include Paths内声明所有包含头文件的文件夹
- 工程选项,C/C++,Define内定义USE_STDPERIPH_DRIVER
- 工程选项,Debug,下拉列表选择对应调试器,Settings,Flash Download里勾选Reset and Run
2.1.固件库介绍
2.2.新建工程
- 新建工程,新建工程文件夹,在文件夹内创建project
- 芯片选择STM32F103C8
- 弹出小助手,×掉
2.3.添加启动文件
-
启动文件:STM32程序就是从启动文件开始执行的
启动文件路径:\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm
-
stm32f10x.h:外设寄存器描述文件。描述有哪些寄存器和对应的地址
-
system_stm32f10x:配置时钟,72Mhz就是这个文件配置
这两个文件路径:STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x
-
core_cm3:STM32是内核和外围电路组成的,所以内核也有寄存器描述文件
STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\CoreSupport
-
工程文件新建一个Start目录,在keil新建一个组Start,将文件全部添加到组里,这样启动文件就添加好了
注意startup中的文件只能添加一个,这里我们添加
startup_stm32f10x_md.s
),参照下表来选择
- 在工程选项中配置头文件
2.4.新建main函数
- 新建一个组User,在User下新建main文件。
- 插入头文件
#include "stm32f10x.h"
2.5.一些配置
-
编码格式,Tab缩进,也可以设置颜色和字体
-
调试器和下载配置
这样就可以进行寄存器开发,很不方便,接下来介绍库函数开发
2.6.寄存器点灯代码示例
#include "stm32f10x.h" // Device header
int main()
{
RCC->APB2ENR |= 0x00000010;
GPIOC->CRH |= 0x00300000;
GPIOC->ODR |= 0x00002000;
while(1)
{
}
}
// 注意最后一行要留一行空行,不然会报错
2.7.添加库函数,基于标准库开发
- 工程目录下新建一个文件夹
Library
来存放库函数,并在keil中添加组。
库函数源文件目录:\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver\src
,里面misc是内核库函数。
库函数头文件目录:\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver\inc
将这两个文件都复制到工程目录下的Library
中 - 配置库函数头文件的包含关系,放到
User
目录下
- 通过以下操作,才能包含标准外设库,也就是库函数
如果你定义了标准外设驱动,头文件配置函数才有效
别忘了把User和Library也添加上。
这样把库函数的工程就创建好了
2.8.代码提示功能
2.9.工程架构
四、GPIO
1.GPIO简介
- GPIO(General Purpose Input Output)通用输入输出口
- 可配置为8种输入输出模式
- 引脚电平:0V~3.3V,部分引脚可容忍5V
- 输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等
- 输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等
2.GPIO基本结构
3.GPIO位结构
这里的肖特基触发器有错误,应该是施密特触发器
一个端口只能有一个输出,但是可以有多个输入
模拟输入会关闭数字输入功能,其他的都不影响数字输入功能
开漏输出也可以输入,先输出1,再直接读取输入数据寄存器就可以了
4.GPIO模式
通过配置GPIO的端口配置寄存器,端口可以配置成以下8种模式
不进行初始化,默认的是浮空模式。
typedef enum
{ GPIO_Mode_AIN = 0x0, // 模拟输入 Analog in
GPIO_Mode_IN_FLOATING = 0x04, // 浮空输入 in floating
GPIO_Mode_IPD = 0x28, // 下拉输入 in Pull Down
GPIO_Mode_IPU = 0x48, // 上拉输入 in pull up
GPIO_Mode_Out_OD = 0x14, // 开漏输出 out open drain
GPIO_Mode_Out_PP = 0x10, // 推挽输出 out push pull
GPIO_Mode_AF_OD = 0x1C, // 复用开漏输出 atl open drain
GPIO_Mode_AF_PP = 0x18 // 复用推挽输出 atl push pull
}GPIOMode_TypeDef;
5.LED
1.使用RCC开启GPIO时钟
- 外设时钟是指微控制器中用于驱动外设(如GPIO、UART、SPI等)的时钟信号。每个外设都需要时钟信号来进行操作和通信。
- 使能(Enable)指的是通过控制寄存器或寄存器位,将特定外设的时钟信号打开或关闭。当使能外设时钟时,外设将开始工作并可以使用;当禁用外设时钟时,外设将停止工作。
2.使用GPIO_Init初始化GPIO
3.使用输出函数控制GPIO口
// LED初始化
void LED_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); // 使用RCC开启GPIO时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // GPIO模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; // GPIO引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 输出速度
GPIO_Init(GPIOA,&GPIO_InitStructure); // 读取结构体参数,执行一些判断,最后写入到GPIO配置寄存器
//GPIO_ResetBits(GPIOA,GPIO_Pin_0); // 给指定GPIO外设的指定端口低电平
//GPIO_SetBits(GPIOA,GPIO_Pin_0); // 给指定GPIO外设的指定端口高电平
//GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET); // 给指定GPIO外设的指定端口设置指定值
//GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);
}
// LED1亮
void LED1_ON(void)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_1);
}
// LED1灭
void LED1_OFF(void)
{
GPIO_SetBits(GPIOA,GPIO_Pin_1);
}
// LED1状态翻转
void LED1_Turn(void)
{
if(GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_1) == 0)
{
GPIO_SetBits(GPIOA,GPIO_Pin_1);
}
else
{
GPIO_ResetBits(GPIOA,GPIO_Pin_1);
}
}
6.蜂鸣器
// 蜂鸣器初始化
void Buzzer_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); // RCC_APB2外设时钟控制
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_1 | GPIO_Pin_2);
}
// 蜂鸣器响
void Buzzer_ON(void)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
}
// 蜂鸣器不响
void Buzzer_OFF(void)
{
GPIO_SetBits(GPIOB,GPIO_Pin_12);
}
// 蜂鸣器状态翻转
void Buzzer_Turn(void)
{
if(GPIO_ReadOutputDataBit(GPIOB,GPIO_Pin_12) == 0)
{
GPIO_SetBits(GPIOB,GPIO_Pin_12);
}
else
{
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
}
}
7.传感器模块
传感器模块:传感器元件(光敏电阻/热敏电阻/红外接收管等)的电阻会随外界模拟量的变化而变化,通过与定值电阻分压即可得到模拟电压输出,再通过电压比较器进行二值化即可得到数字电压输出。
连线方式如上图
// 光敏电阻初始化
void LightSensor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
}
// 获取光敏电阻输入电平
uint8_t LightSensor_Get(void)
{
return GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13);
}
8.按键
// 按键初始化
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
}
// 返回按键键码
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0;
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0)
{
Delay_ms(20);
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0);
Delay_ms(20);
KeyNum = 1;
}
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0)
{
Delay_ms(20);
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0);
Delay_ms(20);
KeyNum = 2;
}
return KeyNum;
}
五、OLED
- OLED(Organic Light Emitting Diode):有机发光二极管
- OLED显示屏:性能优异的新型显示屏,具有功耗低、相应速度快、宽视角、轻薄柔韧等特点
- 0.96寸OLED模块:小巧玲珑、占用接口少、简单易用,是电子设计中非常常见的显示屏模块
- 供电:3~5.5V,通信协议:I2C/SPI,分辨率:128*64
六、EXTI外部中断
1.中断系统介绍
-
中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行
-
中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源
-
中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回
2.STM32中断
- 68个可屏蔽中断通道,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设
- 使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级
3.NVIC基本结构
- NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级
- 抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队
NVIC优先级分组
NVIC对所有的中断进行排号,排号存在优先级寄存器中
分为抢占优先级和响应优先级,抢占优先级可以中断正在执行的中断,也就是可以嵌套中断
4.EXTI简介
-
EXTI(Extern Interrupt)外部中断
-
EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC- - 发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序
-
支持的触发方式:上升沿/下降沿/双边沿/软件触发
-
支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断
-
通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒
-
触发响应方式:中断响应/事件响应
-
EXTI基本结构
-
EXIT框图
5.AFIO
- AFIO主要用于引脚复用功能的选择和重定义
- 在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择
6.旋转编码器简介
-
旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向
-
类型:机械触点式/霍尔传感器式/光栅式
-
比如机械触点式旋转编码器,通过旋转时内部触电连通,产生正交波形,在其A与B引脚,产生脉冲信号。
-
正向旋转,A的脉冲下降沿,B还没有下降,B是高电平。逆向旋转,A的脉冲下降沿,B已经下降,B是低电平。
相位相差90度的波形,叫正交波形,带正交波形信号输出的编码器,可以用来测方向。
7.代码实现
// 进入中断,给变量++
#include "stm32f10x.h" // Device header
uint16_t CountSensor_Count;
void CountSensor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); // 打开GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); // 开启AFIO时钟
// EXTI(时钟一直打开)和NVIC(内核外设),不需要开启
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); // 配置AFIO中断引脚选择
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line14; // 选择要启用或者禁用的EXTI线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; // 选择EXTI线的模式,启用还是禁用
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 中断模式,还可以选择事件模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发 ,还可以是上升沿,上升沿和下降沿
EXTI_Init(&EXTI_InitStructure); // EXTI初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 配置NVIT分组,两位抢占两位响应
NVIC_InitTypeDef NVOC_InitStructure;
NVOC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; // 中断通道
NVOC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 指定中断通道是使能还是失能
NVOC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 设置所选通道的抢占优先级
NVOC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 设置所选通道的响应优先级
NVIC_Init(&NVOC_InitStructure);
}
uint16_t CountSensor_Get(void)
{
return CountSensor_Count;
}
void EXTI15_10_IRQHandler() // 中断函数的名字是固定的,在启动文件(startup_stm32f10x_md.s)查询
{
// 先进行中断标志位的判断
if(EXTI_GetITStatus(EXTI_Line14) == SET) // 判断EXTI_Line14的中断标志位是否为1
{
CountSensor_Count++;
EXTI_ClearFlag(EXTI_Line14); // 清除中断标志位,否则一直中断
}
}
- 配置RCC,打开涉及到的外设时钟
- 配置GPIO为输入模式
- 配置AFIO:引脚选择,AFIO和GPIO的库函数在一个文件里面
- 配置EXTI,边沿检测,响应方式
- 配置NVIC:选择合适的优先级
- 通过NVIC进入CPU
- 配置中断函数,函数名在
startup_stm32f10x_md.s
中断不要有delay,中断和外部不要操作可能产生冲突的硬件,设置标志位,回到主函数再操作。
七、TIM定时器 — 中断
1.TIM简介
TIM(Timer)定时器
- 定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断
- 16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时
16位计数器*预分频器(可记录的最大震荡次数)/72MHZ(每秒震荡次数) = 最大计时(震荡的时间) - 不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能
- 根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型
2.定时器类型&定时器框图
- 主模式触发DAC,让内部硬件不在程序控制下自动运行
- 主模式可以把定时器更新事件映射到触发输出TRGO,触发DAC
- 基本定时器只能向上计数
- 预分频计数器,回到0的时候,输出一个脉冲
向上计数:向上计数到自动重载计数器的值,更新中断
向下计数:自动重载计数器的值向下计数到0,更新中断
中央对齐:上下都更新中断
带黑色阴影的都具有影子/缓冲寄存器
外部时钟模式1:输入可以是ETR引脚,其他定时器,CH1引脚的边沿,CH1引脚,CH2引脚
外部时钟模式2:输入可以是内部时钟,可以是TIMX_ETR
DTG:死区生成
DTG后面是互补输出
3.定时中断基本结构
4.预分频时序
计数器计数频率:CK_CNT = CK_PSC / (PSC + 1)
5.计数器时序
-
计数器时序
计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1)
= CK_PSC / (PSC + 1) / (ARR + 1) -
计数器无预装时序
-
计数器有预装时序
SYSCLK:系统时钟
CK_PSC:预分频时钟
PSC:预分频寄存器
CNT:计数器
CK_CNT:计数器计数频率
CK_CNT_OV:计数器溢出频率
ARR:自动重装器
ITR:其他定时器
TRGI:触发输入
TRGO:触发输出
ETR:外部时钟
TI1F_ED:Timer的输入捕获的边沿选择
6.RCC时钟树
7.代码实现
(1)内部时钟中断
- 1.RCC开启时钟
- 2.选择时基单元的时钟源,选择内部时钟源
- 3.配置时基单元
- 4.配置输出中断配置,允许输出中断到NVIC
- 5.配置NVIC,在NVIC打开定时器中断的通道,分配优先级
- 6.运行控制,使能计数器
//这是使用内部时钟,中断程序可以写在这里,也可以写在main或者其他函数
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
TIM_InternalClockConfig(TIM2); // 选择定时器时钟 默认就是使用内部时钟。
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 以一个采样频率F采样N个点来滤除不稳定的波
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 计数器模式 //向上计数
TIM_TimeBaseInitStructure.TIM_Period = 10000-1; // ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200-1; // 预分频器的值
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); // 配置NVIT分组,两位抢占两位响应
NVIC_InitTypeDef NVOC_InitStructure;
NVOC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // 中断通道
NVOC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 指定中断通道是使能还是失能
NVOC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 设置所选通道的抢占优先级
NVOC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 设置所选通道的响应优先级
NVIC_Init(&NVOC_InitStructure);
TIM_Cmd(TIM2,ENABLE); // 启动定时器
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET) // 判断TIM2更新中断标志位是否为SET
{
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
(2)外部ETC中断
// 这是外部ETC中断案例
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0x00); // 通过ETR引脚的外部时钟模式2设置
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 以一个采样频率F采样N个点来滤除不稳定的波
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; // 计数器模式 //向上计数
TIM_TimeBaseInitStructure.TIM_Period = 10-1; // ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 1-1; // 预分频器的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; // 重复计数器的值
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure); // 配置时基单元
TIM_ClearFlag(TIM2,TIM_FLAG_Update); // TIM_TimeBaseInit会生成一个更新事件,来重新装载预分频器和重复计数器的值,
// 缓冲计数器的值在更新事件时才会起作用,为了让值立刻起作用,手动生成了一个更新事件。
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); // 使能更新中断
// NVIC
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 配置NVIT分组,两位抢占两位响应
NVIC_InitTypeDef NVOC_InitStructure;
NVOC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // 中断通道
NVOC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 指定中断通道是使能还是失能
NVOC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; // 设置所选通道的抢占优先级
NVOC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 设置所选通道的响应优先级
NVIC_Init(&NVOC_InitStructure);
TIM_Cmd(TIM2,ENABLE); // 启动定时器
}
八、TIM定时器 — 输出比较
1.输出比较简介
- OC(Output Compare)输出比较
- 输出比较可以通过比较CNT与CCR寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形
- 每个高级定时器和通用定时器都拥有4个输出比较通道
- 高级定时器的前3个通道额外拥有死区生成和互补输出的功能
CCR:捕获比较
2.PWM简介
- PWM(Pulse Width Modulation)脉冲宽度调制
- 在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域
- PWM参数:
频率 = 1 / TS 占空比 = TON / TS 分辨率 = 占空比变化步距
3.输出比较通道
- 输出比较通道(高级)
- 输出比较通道(通用)
4.输出比较模式
5.PWM基本结构&参数计算
- PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1) 频率就是计数器溢出频率
- PWM占空比: Duty = CCR / (ARR + 1)
- PWM分辨率: Reso = 1 / (ARR + 1)
6.舵机
- 舵机是一种根据输入PWM信号占空比来控制输出角度的装置
- 输入PWM信号要求:周期为20ms,高电平宽度为0.5ms~2.5ms
7.直流电机
- 直流电机是一种将电能转换为机械能的装置,有两个电极,当电极正接时,电机正转,当电极反接时,电机反转
- 直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作
- TB6612是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向
8.代码实现
- 开启TIM2和GPIOA时钟
- GPIO初始化
- 定时器初始化
- 初始化输出比较
// PWM初始化
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
// GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE); // TIM2的部分重映射1
// GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE); // 关闭引脚的JTAG功能,因为默认为这个功能
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推免输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_InternalClockConfig(TIM2); // 配置TIM2内部时钟
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 100 - 1; // ARR
TIM_TimeBaseStructure.TIM_Prescaler = 720 - 1; // PSC
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);
// 初始化输出比较通道
// 不同的通道对应的GPIO口不一样
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); // 给结构体赋初始值(不是所有的都用到)
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // 设置输出比较模式
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 设置输出比较的极性
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 设置输出使能
TIM_OCInitStructure.TIM_Pulse = 0; // 设置CCR 捕获比较寄存器
TIM_OC1Init(TIM2,&TIM_OCInitStructure); // 初始化输出比较
TIM_Cmd(TIM2,ENABLE); // 启动定时器
}
九、TIM定时器 — 输入捕获
1.输入捕获简介
- IC(Input Capture)输入捕获
- 输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数
- 每个高级定时器和通用定时器都拥有4个输入捕获通道
- 可配置为PWMI模式,同时测量频率和占空比
- 可配合主从触发模式,实现硬件全自动测量
2.频率测量
3.输入捕获通道
4.主从触发模式
5.输入捕获基本结构
- 用来测周法测频率
- 选择TI1FP1为上升沿触发,选择直连的通道
- 边沿检测极性选择:选择上升沿还是下降沿
- 数据选择器:选择直连通道或者交叉通道
-
- 捕获的瞬间,可以更新事件,或者产生中断,设置从机模式清理
6.PWMI基本结构
- 可以测周法测频率,测占空比
- CCR2/CCR1就是占空比
- 一个频率交叉通道
7.代码实现
- 开启TIM,GPIO时钟
- 初始化GPIO
- 初始化TIM定时器
- 初始化输入捕获
- 频率是标准频率/计数器的值
- 占空比等于第二个计数值/第一个计数值
//测量频率和占空比
void IC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef 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);
TIM_InternalClockConfig(TIM3); // 配置TIM3内部时钟
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 65536 - 1; // ARR
TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; // PSC
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);
//初始化输入捕获单元
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);
// 通道2这个方式比较麻烦
// TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
// TIM_ICInitStructure.TIM_ICFilter = 0xF; // 滤除高频噪声
// TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling; // 触发方式:下降沿触发
// TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 通道的预分频:不分频
// TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_IndirectTI; // 触发信号从哪个通道走 这个参数配置数据选择器,这里选择交叉通道
// TIM_ICInit(TIM3,&TIM_ICInitStructure);
TIM_PWMIConfig(TIM3,&TIM_ICInitStructure); // 和上面代码效果一样,自动把另一个通道设置成相反的位置 ,通道2,交叉通道,下降沿 ,注意这个只支持通道1和2
TIM_SelectInputTrigger(TIM3,TIM_TS_TI1FP1); // 从模式的输入触发源
TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset); // 选择从机模式 清零
TIM_Cmd(TIM3,ENABLE);
}
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3)+1); // CCR总是会少一个数,所以要加回来
}
uint32_t IC_GetDuty(void)
{
return (TIM_GetCapture2(TIM3) +1 )* 100 /(TIM_GetCapture1(TIM3) +1);
}
十、TIM定时器 — TIM编码器接口
1.简介
- Encoder Interface 编码器接口
- 编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度
- 每个高级定时器和通用定时器都拥有1个编码器接口
- 两个输入引脚借用了输入捕获的通道1和通道2
测量位置或者带有方向的速度
2.正交编码器
3.编码器接口基本结构
4.工作模式
5.工作模式举例
6.代码实现
// 编码器初始化
void Encoder_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Period = 65536 - 1; // ARR
TIM_TimeBaseStructure.TIM_Prescaler = 1 - 1; // PSC
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);
//初始化输入捕获单元
TIM_ICInitTypeDef TIM_ICInitStructure; // 定义结构体变量
TIM_ICStructInit(&TIM_ICInitStructure); // 初始化结构体变量,给赋默认值
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; // 通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF; // 滤除高频噪声
TIM_ICInit(TIM3,&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICFilter = 0xF; // 滤除高频噪声
TIM_ICInit(TIM3,&TIM_ICInitStructure);
//配置编码器接口
TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising); // 定时器,编码器模式,极性,
TIM_Cmd(TIM3,ENABLE);
}
// 测量位置
//int16_t Encoder_Get(void)
//{
// return TIM_GetCounter(TIM3);
//}
// 测速,返回计数器值然后清零,主函数中每隔一秒读取一下,就是每秒计数值,也就是速度
int16_t Encoder_Get(void)
{
int16_t Temp;
Temp = TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3,0);
return Temp;
}
// 主函数
int16_t Speed;
int main()
{
OLED_Init();
OLED_ShowString(1,1,"Speed:");
Timer_Init();
Encoder_Init();
while(1)
{
OLED_ShowSignedNum(1,7,Speed,5);
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update) == SET) // 判断TIM2更新中断标志位是否为SET
{
Speed = Encoder_Get();
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
十一、ADC模数转换器
1.ADC简介
- ADC(Analog-Digital Converter)模拟-数字转换器
- ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁
- 12位逐次逼近型ADC,1us转换时间
- 输入电压范围:0 ~ 3.3V,转换结果范围:0 ~ 4095
- 18个输入通道,可测量16个外部和2个内部信号源
- 规则组和注入组两个转换单元
- 模拟看门狗自动监测输入电压范围
- STM32F103C8T6 ADC资源:ADC1、ADC2,10个外部输入通道
- STM32的ADC是12位的,所以最大值为212 -1
2.逐次逼近型ADC
- 根据DAC输入的参考电压值,与ADC的电压值不断地进行比较,采用的是二分法
- 可以理解为:开始时,DAC的电压为参考电压的一半,然后采用二分法不断地变化、比较,确定模拟信号近似的数字信号。
- 逐次逼近型ADC具有较高的转换速度和较低的功耗
- 精度受限于迭代次数和采样速率
3.STM32F103C8T6单个ADC框图
-
从16个通道进去,经过多路开关,一半选择规则通道,到规则通道后选择硬件触发和软件触发,触发后逐次逼近ADC开始比较,得出对应的数字信号放在数据寄存器中,并把EOC标志位置1,然后就可以读数据寄存器。
-
数据寄存器有左对齐和右对齐两种方式,左对齐可以用于控制精度
4ADC基本结构
5.输入通道
6.转换模式
-
单次转换,非扫描模式
可以给不同的通道选择不同的次序,单次转换就是一次触发,只转换一次。非扫描模式就是一次只能保存最后一个序列。 -
连续转换,非扫描模式
一次触发,连续转换,需要的时候读取数据寄存器即可。 -
单次转换,扫描模式
触发一次,处理多个序列,但不连续 -
连续转换,扫描模式
触发一次,处理多个序列,并且连续处理并保存到数据寄存器中
7.触发控制
- 有硬件触发和软件触发两种
8.转换时间
量化和编码需要时间,所以要采样后保持
9.校准
-
ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差
-
建议在每次上电后执行一次校准
-
启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期
// ADC校准,初始化里面加上这段代码就行
ADC_ResetCalibration(ADC1); // 复位校准
while(ADC_GetResetCalibrationStatus(ADC1) == SET); // 等待复位校准完成,变为0就复位校验完成
ADC_StartCalibration(ADC1); // 开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET); // 等待校准完成
10.代码实现
- 开启GPIO和ADC时钟,配置ADCCLK分频器
- 初始化GPIO
- 初始化多路开关,通道,次序,采样时间
- 初始化ADC
- ADC工作模式:独立模式……
- 数据对齐方式:左对齐,右对齐
- 触发控制源:硬件触发,软件触发
- 连续转换模式:单词,连续
- 扫描转换模式:扫描,非扫描
- 通道数目:n
- 打开ADC开关控制
- ADC校准
单次模式循环触发并获取数据寄存器的值,连续模式直接获取就行
扫描模式可以用非扫描模式来实现,把配置通道和次序的函数提取出来,每次调用重新赋值
#include "stm32f10x.h" // Device header
void AD_Init(void)
{
// 开启ADC,GPIO的时钟,ADCCLK的分频器,也要配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 分频后12MHz
// 配置GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); // RCC_APB2外设时钟控制
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // GPIO_Mode_AIN是ADC的专属模式
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,通道,规则组序列器里的次序,通道采样时间(越小采样越快,越大越稳定)、
// 配置ADC转换器
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // ADC工作模式:独立模式
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);
// ADC校准
ADC_ResetCalibration(ADC1); // 复位校准
while(ADC_GetResetCalibrationStatus(ADC1) == SET); // 等待复位校准完成,变为0就复位校验完成
ADC_StartCalibration(ADC1); // 开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET); // 等待校准完成
}
// 这是单次非扫描模拟单次扫描
// 如果是连续非扫描,把ADC_RegularChannelConfig放回到初始化中
// 连续模式把软件触发也放到初始化中,也不用判断获取标志位状态了,直接获取寄存器的值就行
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
ADC_RegularChannelConfig(ADC1,ADC_Channel,1,ADC_SampleTime_55Cycles5); // ADC,通道,规则组序列器里的次序,通道采样时间(越小采样越快,越大越稳定)、
ADC_SoftwareStartConvCmd(ADC1,ENABLE); // 软件触发转换
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET); // 获取标志位状态 规则组转换完成标志位 EOC可以在读取数据位之后自动清除
return ADC_GetConversionValue(ADC1); // ADC获取转换值 读取ADC的DR数据寄存器
}
十二、DMA
1.DMA简介
-
DMA就相当于:从某个寄存器的地址取内容,放到另一个地址
-
DMA(Direct Memory Access)直接存储器存取
-
DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源
-
12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)
-
每个通道都支持软件触发和特定的硬件触发
-
STM32F103C8T6 DMA资源:DMA1(7个通道)
2.存储器映像
查寄存器地址:查寄存器所在外设的起始地址,外设的寄存器总表查偏移,起始地址(基地址)+偏移 = 寄存器地址。
- STM32中,使用结构体来访问寄存器的流程
定义一个结构体指针,指针地址是外设的起始地址,那这个结构体的每个成员,正好映射实际的每个寄存器
3.存储器框图
4.DMA基本结构
-
m2m:存储器到存储器member to member
-
自动重装和软件触发不能同时使用,软件触发:争取以最快的速度把传输计数器清零,而自动重装的话就永远清不掉。
-
DMA工作三个条件:
- 1.传输计数器大于0
- 2.触发源有触发信号
- 3.DMA使能
5.DMA请求
- 通道号越小,优先级越高
- 外设固定通道
6.数据宽度对齐
- 转运过程中,源端宽度与目标端宽度不一致的处理方式
7.数据转运+DMA(附代码)
- DMA在AHB总线上,开启DMA时钟
- 初始化DMA
- 分别设置外设和存储器站点的起始地址,数据宽度,是否自增
- 设置传输方向
- 设置缓冲区(传输计数器)
- 设置触发模式:硬件触发,软件触发
- 设置通道优先级
- 如果要再给传输计数器赋值,先DMA失能,然后写传输计数器,接着DMA使能。
#include "stm32f10x.h" // Device header
uint16_t MyDMA_Size;
// 从A地址转运到B地址,一次转运Size个
void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size)
{
MyDMA_Size = Size;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA; // 外设站点的起始地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 数据宽度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; // 是否自增
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB; // 存储器站点的起始地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 数据宽度
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; // 传输模式,是否自动重装
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; // 是否存储器到存储器,硬件触发还是软件触发
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; // 通道的软件优先级
DMA_Init(DMA1_Channel1,&DMA_InitStructure); // 把结构体指定的参数,配置到DMA1的通道1
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); // 清楚DMA标志位
}
8.ADC扫描模式+DMA(附代码)
- ADC配置为扫描模式
- ADC初始化
- DMA初始化
- 开启ADC到DMA的输出
- 软件触发
这样即可连续自动转运,需要数据的时候直接读取就好了。
uint16_t AD_Value[4];
void AD_Init(void)
{
// 开启ADC,GPIO的时钟,ADCCLK的分频器,也要配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 分频后12MHz
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
// 配置GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); // RCC_APB2外设时钟控制
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // GPIO_Mode_AIN是ADC的专属模式
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,通道,规则组序列器里的次序,通道采样时间(越小采样越快,越大越稳定)、
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转换器
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // ADC工作模式:独立模式
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初始化
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 外设站点的起始地址
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 数据宽度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 是否自增
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; // 存储器站点的起始地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 是否自增
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向:外设站点作为源头
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); // 把结构体指定的参数,配置到DMA1的通道1
DMA_Cmd(DMA1_Channel1,ENABLE);
// 开启ADC到DMA的输出
ADC_DMACmd(ADC1,ENABLE);
// 开关控制
ADC_Cmd(ADC1,ENABLE);
// ADC校准
ADC_ResetCalibration(ADC1); // 复位校准
while(ADC_GetResetCalibrationStatus(ADC1) == SET); // 等待复位校准完成,变为0就复位校验完成
ADC_StartCalibration(ADC1); // 开始校准
while(ADC_GetCalibrationStatus(ADC1) == SET); // 等待校准完成
// 单词转运就把这个放到下面函数中
ADC_SoftwareStartConvCmd(ADC1,ENABLE); // 软件触发ADC开始转换
}
//void AD_GetValue(void)
//{
// DMA_Cmd(DMA1_Channel1,DISABLE); // 重新写传输计数器
// DMA_SetCurrDataCounter(DMA1_Channel1,4);
// DMA_Cmd(DMA1_Channel1,ENABLE);
// ADC_SoftwareStartConvCmd(ADC1,ENABLE); // 软件触发ADC开始转换
// while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); // 传输完成标志位
// DMA_ClearFlag(DMA1_FLAG_TC1);
//}
十三、USART串口
1.通信接口
- 通信的目的:将一个设备的数据传送到另一个设备,扩展硬件系统
- 通信协议:制定通信的规则,通信双方按照协议规则进行数据收发
- 有时钟线,是同步通信,通过时钟线来接收
- 没有时钟线,就需要双方自己约定采样频率,是异步接收
- 单端:共地
- 低位先行
2.串口通信
- 简单双向串口通信有两根通信线(发送端TX和接收端RX)
- TX与RX要交叉连接
- 当只需单向的数据传输时,可以只接一根通信线
- 当电平标准不一致时,需要加电平转换芯片
3.电平标准
-
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
-
TTL电平:+3.3V或+5V表示1,0V表示0
-
RS232电平:-3-15V表示1,+3+15V表示0
-
RS485电平:两线压差+2+6V表示1,-2-6V表示0(差分信号)
4.串口参数以及时序
-
波特率:串口通信的速率
起始位:标志一个数据帧的开始,固定为低电平
数据位:数据帧的有效载荷,1为高电平,0为低电平,低位先行
校验位:用于数据验证,根据数据位计算得来
停止位:用于数据帧间隔,固定为高电平 -
波特率和比特率:波特率的单位是码元,比特率的单位是比特
二进制调制的情况下,一个码元就是一个比特
5.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
6.USART框图
- 发送数据:TXE(TX Empty)发送数据寄存器为空,就把数据放在发送数据寄存器,发送数据寄存器发送到移位寄存器中移出。
- 接收数据:RXNE(RX Not Empty)接收数据寄存器为空,就把数据移进。
- 也可以配置流控制,控制发送和接收
- 可以设置时钟线来同步通信
7.USART基本结构
8.数据帧
- 校验:无校验,奇校验,偶校验
9.起始位侦测&&数据采样
- 从上面两个图中,我们可以看出,把一个电平监测16个,采样中间三个,1多就是1,0多就是0,这样来滤除噪声
-
- 所以要传入16倍的波特率,库函数中直接输入即可,内部完成计算的。
10.波特率发生器
- 发送器和接收器的波特率由波特率寄存器BRR里的DIV确定
- 计算公式:波特率 = fPCLK2/1 / (16 * DIV)
11.发送中文设置
- 设置编码格式为
UTF-8
,并在图示位置加上:-no-multibyte-chars
。注意无空格
GB2312:汉字的编码方式
MicroLIB嵌入式精简库
12.代码实现
- 发送初始化
- 初始化GPIO,引脚采用复用推挽输出
- 初始化USART,包括波特率,硬件流控制,串口模式,校验位,停止位,字长等
- 发送字节
- 发送之后,判断数据寄存器置空标志位为1,代表发送完成
- 接收初始化
- 在发送初始化上面,添加一个GPIO端口为上拉输入,串口模式同时开启收发
- 接收字节
- 扫描接收方式:直接在主循环while里循环调用接收即可
- 中断接收方式:打开USART中断,配置NVIC,需要的时候直接调用接收即可
- 设置接收标志位,每次接收到把接收标志位置1,并在调用读取之后又置0
- 设置接收数据中间变量,中断自动赋值,需要调用即可
- 接收之后需要清除
接收寄存器不为空
标志位(为接收到数据置1,代表不为空,调用接收后手动清除)。即接收寄存器为空,数据可以继续进入
(1)发送单个数据
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>
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; // 复用推挽输出 TX
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
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_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_Cmd(USART1,ENABLE);
}
// 发送一个字节
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET); // 发送数据寄存器空标志位
}
// 发送数组
void Serial_SendArray(uint8_t *Array,uint16_t Length)
{
uint16_t i;
for(i=0;i<Length;i++)
{
Serial_SendByte(Array[i]);
}
}
// 发送字符串
void Serial_SendString(char* String)
{
uint8_t i;
for(i=0;String[i] != '\0';i++)
{
Serial_SendByte(String[i]);
}
}
// 次方计算,返回X的Y次方
uint32_t Serial_Pow(uint32_t X,uint32_t Y)
{
uint32_t Result = 1;
while(Y--)
{
Result *= X;
}
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 + 0x30);
}
}
// 把printf重定向到串口
int fputc(int ch , FILE *f) // fPutChar
{
Serial_SendByte(ch);
return ch;
}
// 封装sprintf
void Serial_Printf(char *format,...) // ... 用于接收可变参数
{
char String[100];
va_list arg; // va_list是一个类型名
va_start(arg,format); // 从format开始接收参数表,放在arg里面
vsprintf(String,format,arg); //String位置,格式化format字符串,参数表是arg
va_end(arg); // 释放参数表
Serial_SendString(String);
}
int main()
{
Serial_Init();
OLED_Init();
//OLED_ShowChar(1,1,'A');
//Serial_SendByte('A');
// uint8_t MyArray[] = {0x42,0x43,0x44,0x45};
// Serial_SendArray(MyArray,4);
// Serial_SendString("HelloWorld!\r\n");
// Serial_SendNumber(12345,5);
// printf("Num = %d\r\n",666);
// char String[100];
// sprintf(String,"Num = %d\r\n",666666); // 可以把格式化字符输出到一个字符串里
// Serial_SendString(String);
Serial_Printf("你好,世界\r\n");
while(1)
{
}
}
(2)接收单个数据
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;
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; // 复用推挽输出 TX
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);
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 = 2 ;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1,ENABLE);
}
// 接收数据寄存器标志位
uint8_t Serial_GetRxFlag(void)
{
if(Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
// 接收数据缓冲区
uint8_t Serial_GetRxData(void)
{
return Serial_RxData;
}
// 中断函数
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
{
Serial_RxData = USART_ReceiveData(USART1);
Serial_RxFlag = 1;
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}
// 主程序
uint8_t RxData;
int main()
{
Serial_Init();
OLED_Init();
OLED_ShowString(1,1,"RxData:");
while(1)
{
// 扫描模式
// if (USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
// {
// RxData = USART_ReceiveData(USART1);
// OLED_ShowHexNum(1,1,RxData,2);
// }
// 中断模式
if (Serial_GetRxFlag() == 1)
{
RxData = Serial_GetRxData();
Serial_SendByte(RxData);
OLED_ShowHexNum(1,8,RxData,2);
}
}
}
(3)收发HEX数据包
发送包分为一下两种
- 固定包长,含包头包尾
- 可变包长,含包头包尾
- 设置发送格式,包头包尾,固定包长等等
- .h文件中
extern
Serial_TxPacket[]
- 给
Serial_TxPacket[]
复制直接调用Serial_SendPacket();
// 设置发送包头包尾和数据
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);
Serial_SendArray(Serial_TxPacket,4);
Serial_SendByte(0xFE);
}
- HEX数据包接收
- 定义状态机,等待包头,接收数据,等待包尾3个状态
- 定义接收缓冲区数组
Serial_RxPacket[]
,extern或者get出去 - 接收数据,按状态机判断
Serial_RxFlag
接收标志置1,获取后清零 ,这样可能存在发送快时,出现数据错位,可以不设置自动清零,接收之后在main中手动清零。
uint8_t Serial_RxPacket[4];
uint8_t Serial_RxFlag;
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0; // 接收数据状态机
static uint8_t pRxState = 0; // 指示接受到哪一个了
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET) //若接收到数据了(数据寄存器非空标志位为1)
{
uint8_t RxData = USART_ReceiveData(USART1); // 接收数据
if(RxState == 0) // 若为0状态,则判断接受到的是否为包头
{
if(RxData == 0xFF)
{
RxState = 1;
pRxState = 0;
}
}
else if(RxState == 1) // 若为1状态,则接收数据
{
Serial_RxPacket[pRxState] = RxData;
pRxState++;
if(pRxState>=4)
{
RxState = 2; // 接收完数据之后,把置为2状态
pRxState = 0;
}
}
else if(RxState == 2) // 若为2状态,判断接受到的是否为包尾
{
if(RxData == 0xFE)
{
RxState = 0;
Serial_RxFlag = 1;
}
}
USART_ClearITPendingBit(USART1,USART_IT_RXNE); // 清除接收数据寄存器非空标志位
}
(4)收发文本数据包
-
文本数据包发送,直接发送String即可
-
文本数据包接收
-
与接收HEX数据包基本一致,记得在由于是字符串,记得在最后加上一个’\0’结束标志位。
char Serial_RxPacket[100];
uint8_t Serial_RxFlag;
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0; // 接收数据状态机
static uint8_t pRxState = 0; // 指示接受到哪一个了
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
{
uint8_t RxData = USART_ReceiveData(USART1);
if(RxState == 0)
{
if(RxData == '@' && Serial_RxFlag == 0)
{
RxState = 1;
pRxState = 0;
}
}
else if(RxState == 1)
{
if(RxData == '\r')
{
RxState = 2;
}else
{
Serial_RxPacket[pRxState] = RxData;
pRxState++;
}
}
else if(RxState == 2)
{
if(RxData == '\n')
{
RxState = 0;
Serial_RxPacket[pRxState] = '\0'; // 给字符串加个结束标志
Serial_RxFlag = 1;
}
}
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}
十四、IIC通信
1.简介
- I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线
- 两根通信线:SCL(Serial Clock)、SDA(Serial Data)
- 同步,半双工
- 带数据应答
- 支持总线挂载多设备(一主多从、多主多从)
2.硬件电路
- 所有I2C设备的SCL连在一起,SDA连在一起
- 设备的SCL和SDA均要配置成开漏输出模式
- SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
- IIC使用开漏输出的优点:1.杜绝短路。2.避免引脚模式频繁切换(开漏输出允许输入)。3.线与,共地
3.IIC的6个基本时序单元
- 起始条件:SCL高电平期间,SDA从高电平切换到低电平
- 终止条件:SCL高电平期间,SDA从低电平切换到高电平
- 发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节
- 接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)
- 发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
- 接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
4.IIC时序
- 指定设备指定地址,写指定数据
- 开始 – 发送设备地址 – 接收应答 – 发送寄存器地址 – 接收应答 – 发送数据 – 接收应答 – 停止
- 指定设备当前地址,读数据
- 开始 – 发送设备地址 – 接收应答 – 接收数据 – 不发送应答 – 停止
- 指定设备指定地址,读数据
- 开始 – 发送设备地址 – 接收应答 – 发送寄存器地址 – 接收应答 – 开始 – 发送设备地址 – 接收应答 – 接收数据 – 不发送应答 – 结束
写或者读,都会让地址指针++,所以连续接收或者发送的时候,直接发送或者接收就行。
5.MPU6050
(1)MPU6050简介
- MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景
- 3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度
- 3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度
(2)MPU6050参数
-
16位ADC采集传感器的模拟信号,量化范围:-32768~32767
-
加速度计满量程选择:±2、±4、±8、±16(g)
-
陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)
-
可配置的数字低通滤波器
-
可配置的时钟源
-
可配置的采样分频
-
I2C从机地址:1101000(AD0=0)
1101001(AD0=1)
(3)硬件电路
(4)MPU6050框图
6.代码实现
协议层(IIC),驱动层(MPU6050),应用层(main)
#include "stm32f10x.h" // Device header
#include "Delay.h"
// 把对引脚写入和读出高低电平封装成函数
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)BitValue); // BitAction枚举,非0即1
Delay_us(10);
}
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)BitValue);
Delay_us(10);
}
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
Delay_us(10);
return BitValue;
}
void MyI2C_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); // RCC_APB2外设时钟控制
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_10 | GPIO_Pin_11);
}
// 开始时序
void MyI2C_Start(void)
{
MyI2C_W_SDA(1);
MyI2C_W_SCL(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);
}
// 发送一个字节
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for(i = 0;i<8;i++)
{
MyI2C_W_SDA(Byte & (0x80)>>i);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
}
// 接收一个字节
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i,Byte = 0x00;
MyI2C_W_SDA(1);
for(i = 0;i<8;i++)
{
MyI2C_W_SCL(1);
// Byte &= (MyI2C_R_SDA()>>i);
if(MyI2C_R_SDA() == 1){Byte |= (0x80>>i);}
MyI2C_W_SCL(0);
}
return Byte;
}
// 发送应答
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit);
MyI2C_W_SCL(1);
MyI2C_W_SCL(0);
}
// 接收应答
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit = 0x00;
MyI2C_W_SDA(1);
MyI2C_W_SCL(1);
AckBit = MyI2C_R_SDA();
MyI2C_W_SCL(0);
return AckBit;
}
#include "stm32f10x.h" // Device header
#include "MyIIC.h"
#include "MPU6050_REG.h"
#define MPU6050_ADDRESS 0xD0
// 指定地址写
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{
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 Byte;
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS);
MyI2C_ReceiveAck();
MyI2C_SendByte(RegAddress);
MyI2C_ReceiveAck();
MyI2C_Start();
MyI2C_SendByte(MPU6050_ADDRESS|0x01);
MyI2C_ReceiveAck();
Byte = MyI2C_ReceiveByte();
MyI2C_SendAck(1);
MyI2C_Stop();
return Byte;
}
void MPU6050_Init(void)
{
MyI2C_Init();
MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01); // 配置电源管理寄存器1 解除睡眠,选择陀螺仪时钟
MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00); // 配置电源管理寄存器2 6个轴均不待机
MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09); // 采样率分频 10分频
MPU6050_WriteReg(MPU6050_CONFIG ,0x06); // 配置寄存器,外部同步0,数字低通滤波器 110(最平滑的滤波)
MPU6050_WriteReg(MPU6050_GYRO_CONFIG ,0x18); // 陀螺仪配置寄存器 最大量程
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG ,0x18); // 加速度计配置寄存器 最大量程
}
// 读取MPU6050加速度和陀螺仪寄存器,也可以利用IIC的特性,读或写后会指向下一个寄存器地址,这样连续读取一片连续的寄存器
void MPU6050_GetData(int16_t *AccX , int16_t *AccY , int16_t *AccZ,
int16_t *GyroX , int16_t *GyroY , int16_t *GyroZ)
{
uint8_t DataH,DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
*AccX = (DataH<<8) | DataL; // 左移不会丢失
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AccY = (DataH<<8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*AccZ = (DataH<<8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
*GyroX = (DataH<<8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
*GyroY = (DataH<<8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
*GyroZ = (DataH<<8) | DataL;
}
// 获取 MPU6050 ID
uint8_t MPU6050_GetID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}
int16_t AX,AY,AZ,GX,GY,GZ;
int main()
{
OLED_Init();
MPU6050_Init();
OLED_ShowString(1,1,"ID");
uint8_t ID = MPU6050_GetID();
OLED_ShowHexNum(1,4,ID,2);
while(1)
{
MPU6050_GetData(&AX,&AY,&AZ,&GX,&GY,&GZ);
OLED_ShowSignedNum(2,1,AX,5);
OLED_ShowSignedNum(3,1,AY,5);
OLED_ShowSignedNum(4,1,AZ,5);
OLED_ShowSignedNum(2,8,GX,5);
OLED_ShowSignedNum(3,8,GY,5);
OLED_ShowSignedNum(4,8,GZ,5);
}
}
7.硬件IIC
(1)I2C外设简介
-
STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担
-
支持多主机模型,IIC有固定多主机和可变多主机模型,STM32是可变多主机模型
-
支持7位/10位地址模式,10位地址模式,用两个寄存器为什么是10位地址?固定格式11110+位寻址
-
支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)
-
支持DMA
-
兼容SMBus协议
-
STM32F103C8T6 硬件I2C资源:I2C1、I2C2
-
同步时序的好处,允许时序不标准
(2)IIC框图
- 自身地址寄存器可自定义,支持双地址
- 引脚用复用开漏输出模式,GPIO状态由片上外设控制
- TXE:发送寄存器为空标志位
- RXNT:接收寄存器非空标志位
- BTF:字节发送结束标志位
(3)IIC基本结构
(4)主机发送
STM32默认是从机模式,CR_start会变为主模式
以7位主发送作为讲解:
- S:开始条件
- EV5:开始标志位为1,地址写入数据寄存器清除
- 地址写入数据寄存器,接收应答
- EV6:地址发送结束标志位为1,
- EV8_1:移位寄存器空,数据寄存器空,然后将数据写入数据寄存器,立马移到移位寄存器
- EV8事件:数据寄存器为空,移位寄存器有数据,下一个数据写入数据寄存器,此时EV8事件结束。
- 当移位寄存器把上一个数据处理完之后,接收应答。此时EV8事件又开启,数据寄存器把数据放入移位寄存器,又到了数据寄存器为空,移位寄存器有数据的状态。
- EV8_2:发送寄存器为空,结束标志位为1,产生停止条件时清除。
(5)主机接收
- EV7:数据寄存器非空,读数据寄存器清除
8.硬件IIC代码
只有IIC相关代码需要改
// 超时退出
void MPU6050_WaitEven(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
uint32_t Timeout;
Timeout = 10000;
while(I2C_CheckEvent(I2Cx,I2C_EVENT) != SUCCESS)
{
Timeout--;
if(Timeout == 0)
{
break;
}
}
}
// 指定地址写
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{
I2C_GenerateSTART(I2C2,ENABLE); // 开始
MPU6050_WaitEven(I2C2,I2C_EVENT_MASTER_MODE_SELECT); // EV5,等待地址写入数据寄存器
I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter); // 发送地址(移位)
MPU6050_WaitEven(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); // EV6,接收到地址,读取SR1和SR2清楚
I2C_SendData(I2C2,RegAddress); // 发送数据 寄存器地址
MPU6050_WaitEven(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING); // EV8 写入数据寄存器
I2C_SendData(I2C2,Data); // 发送数据
MPU6050_WaitEven(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED); // EV8_2 设置停止位
I2C_GenerateSTOP(I2C2,ENABLE);
}
// 指定地址读
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Byte;
I2C_GenerateSTART(I2C2,ENABLE);
MPU6050_WaitEven(I2C2,I2C_EVENT_MASTER_MODE_SELECT); // EV5
I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);
MPU6050_WaitEven(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); // EV6
I2C_SendData(I2C2,RegAddress);
MPU6050_WaitEven(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED); // EV8_2
I2C_GenerateSTART(I2C2,ENABLE);
MPU6050_WaitEven(I2C2,I2C_EVENT_MASTER_MODE_SELECT); // EV5
I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Receiver);
MPU6050_WaitEven(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); // EV6
I2C_AcknowledgeConfig(I2C2,DISABLE);// 配置应答位
I2C_GenerateSTOP(I2C2,ENABLE); // 配置停止位
MPU6050_WaitEven(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED); // wang
Byte = I2C_ReceiveData(I2C2);
I2C_AcknowledgeConfig(I2C2,ENABLE); // 默认状态下ACK == 1;
return Byte;
}
void MPU6050_Init(void)
{
// MyI2C_Init();
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructrue;
GPIO_InitStructrue.GPIO_Mode = GPIO_Mode_AF_OD; // 通用推挽输出
GPIO_InitStructrue.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructrue.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructrue);
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_ClockSpeed = 50000; // 50kHz
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 时钟占空比
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);
I2C_Cmd(I2C2,ENABLE);
……
……
……
}
十五、SPI通信
1.SPI简介
- SPI对比IIC优缺点:SPI传输更快,SPI简单,SPI硬件开销大
- SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
- 四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)
- 同步,全双工
- 支持总线挂载多设备(一主多从)
2.硬件电路
- 所有SPI设备的SCK、MOSI、MISO分别连在一起
- 主机另外引出多条SS控制线,分别接到各从机的SS引脚,ss低电平有效
- 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
3.移位示意图
- 时钟源由于主机提供
- 高位先行
- 上升沿移位输出,到通信线上,下降沿数据采样输入
- 简单来说就是主机和从机通信的过程就是交换数据,如果是写,那主机忽略接受到的,如果是读,那主机随便发送一个(一般是0xFF)
4.SPI时序基本单元
- 起始条件:SS从高电平切换到低电平
- 终止条件:SS从低电平切换到高电平
(1)交换一个字节(模式0)
- CPOL(Clock Polarity 时钟极性)=0:空闲状态时,SCK为低电平
- CPHA(Clock Phase 时钟相位)=0:SCK第一个边沿移入数据,第二个边沿移出数据
- 在SCK上升沿之前,就要移入数据,所以在SCK上升沿之前,就要移出数据
(2)交换一个字节(模式1)
- 交换一个字节(模式1)
- CPOL=0:空闲状态时,SCK为低电平
- CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
(3)交换一个字节(模式2)
- 交换一个字节(模式2)
- CPOL=1:空闲状态时,SCK为高电平
- CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
(4)交换一个字节(模式3)
- 交换一个字节(模式3)
- CPOL=1:空闲状态时,SCK为高电平
- CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
模式0和模式1的SCK与模式2和模式3的SCK相反
模式0和模式2的相位比模式1和模式3提前半个
5.W15Q64简介
- W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景
- 存储介质:Nor Flash(闪存)
- 时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)
- 存储容量(24位地址):
W25Q40: 4Mbit / 512KByte
W25Q80: 8Mbit / 1MByte
W25Q16: 16Mbit / 2MByte
W25Q32: 32Mbit / 4MByte
W25Q64: 64Mbit / 8MByte
W25Q128: 128Mbit / 16MByte
W25Q256: 256Mbit / 32MByte
224 = 16MB - 硬件电路
CS上面画线,代表低电平有效 - W25Q64框图
- 块:Block 扇:sector(最小擦除单位) 页:page(写入数据)
- 24位寻址,所以是发三个字节的地址,前两个是页的地址,最后一个是页内地址
- Counter计数器,让读写后地址+1,来实现连续读和写
- 缓冲区:写入是写入到缓冲区
- 状态寄存器:主要记录芯片是否忙,因为写之后要从缓冲区写到内存,需要一定的时间
- 写使能写完会自动关闭
6.Flash操作注意事项
写入操作时:
- 写入操作前,必须先进行写使能
- 每个数据位只能由1改写为0,不能由0改写为1
- 写入数据前必须先擦除,擦除后,所有数据位变为1
- 擦除必须按最小擦除单元进行
- 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
- 写入操作结束后,芯片进入忙状态,不响应新的读写操作
读取操作时: - 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
7.SPI软件模拟代码
#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);
}
// SPI初始化
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);
}
// 开始时序
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
// 结束时序
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
// 交换字节时序 模式0
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t i = 0;
for(i = 0;i<8;i++)
{
MySPI_W_MOSI(ByteSend & 0x80);
ByteSend <<= 1;
MySPI_W_SCK(1);
if(MySPI_R_MISO() == 1) {ByteSend |= 0x01;}
MySPI_W_SCK(0);
}
return ByteSend;
}
8.W25Q64的功能实现
void W25Q64_Init(void)
{
MySPI_Init();
}
void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)
{
MySPI_Start();
MySPI_SwapByte(W25Q64_JEDEC_ID); // 读ID号的指令
*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 生产厂商
*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE)<<8; // 高8位,内存类型
//*DID <<= 8;
*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 低8位,大小
MySPI_Stop();
}
// 写使能
void W25Q64_WriteEnable(void)
{
MySPI_Start();
MySPI_SwapByte(W25Q64_WRITE_ENABLE);
MySPI_Stop();
}
// 读状态寄存器1 判断芯片是不是忙状态 ,忙则等待,不忙无操作
void W25Q64_WaitBusy(void)
{
uint32_t Timeout;
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
Timeout = 100000; // 超时标志,避免程序死循环
while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
{
Timeout--;
if(Timeout == 0)
{
break;
}
}
MySPI_Stop();
}
// 页编程
void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count)
{
W25Q64_WriteEnable();
uint16_t i;
MySPI_Start();
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
MySPI_SwapByte(Address >> 16) ;
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
for(i = 0; i<Count; i++)
{
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();
}
// 读取数据,数组输出参数,从Address地址开始读Count个数据
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);
}
MySPI_Stop();
}
// main
uint8_t MID;
uint16_t DID;
uint8_t ArrayWrite[] = {0x55,0x66,0x77,0x88};
uint8_t ArrayRead[4];
int main()
{
OLED_Init();
W25Q64_Init();
W25Q64_ReadID(&MID,&DID);
OLED_ShowString(1,1,"MID: DID:");
OLED_ShowString(2,1,"W:");
OLED_ShowString(3,1,"R:");
OLED_ShowHexNum(1,5,MID,2);
OLED_ShowHexNum(1,12,DID,4);
W25Q64_SectorErase(0x000000); // 擦除扇区,后三位是扇区内的,可以随便写
W25Q64_PageProgram(0x000000,ArrayWrite,4);
W25Q64_ReadData(0x000000,ArrayRead,4);
OLED_ShowHexNum(2,3,ArrayWrite[0],2);
OLED_ShowHexNum(2,6,ArrayWrite[1],2);
OLED_ShowHexNum(2,9,ArrayWrite[2],2);
OLED_ShowHexNum(2,12,ArrayWrite[3],2);
OLED_ShowHexNum(3,3,ArrayRead[0],2);
OLED_ShowHexNum(3,6,ArrayRead[1],2);
OLED_ShowHexNum(3,9,ArrayRead[2],2);
OLED_ShowHexNum(3,12,ArrayRead[3],2);
while(1)
{
}
}
9.硬件SPI介绍
-
STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
-
可配置8位/16位数据帧、高位先行/低位先行
-
时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)
-
支持多主机模型、主或从操作
-
可精简为半双工/单工通信
-
支持DMA
-
兼容I2S协议(数字音频传输协议)
-
STM32F103C8T6 硬件SPI资源:SPI1、SPI2
-
SPI1在SPB2上,所以PCLK(外设时钟)为72MHz,SPI2在APB1上,PCLK为36MHz
-
SPI框图
BR012:控制分频系数
SPE:SPI使能,SPI_Cmd函数配置的位
MSTR:配置主从模式,1主0从
CPOL,CPHA,SPI的四种模式 -
SPI基本结构
10.硬件SPI传输
(1)主模式全双工连续传输
连续传输,要及时读出,对软件要求高,传输效率高
(2)主模式全双工非连续传输
11.硬件SPI代码实现
代码采用非连续传输
- 第一步:等待发送寄存器为空
- 第二步:给发送寄存器写入数据
- 第三步:判断接收寄存器非空
- 第四步:读取数据
// 把所有引脚置高低电平的函数换个名字
// 引脚的封装
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
}
// SPI初始化
void MySPI_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // SCK和MOSI为复用推免输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master ; // 主机模式
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex ; // 双线全双工
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB ; // 高位先行
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 数据帧 8位还是16位
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; // 分频系数 频率 = 72MHz/128
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low ; // 选择极性
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge ; // 时钟相位 第1个边沿开始采样(移入)
SPI_InitStructure.SPI_CRCPolynomial = 7 ; // 不用,随便填,默认7
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件NSS模式
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);
}
// 交换字节时序 模式0 非连续传输
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);
SPI_I2S_SendData(SPI1,ByteSend);
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET);
return SPI_I2S_ReceiveData(SPI1);
}
十六、Unix时间戳
-
Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒
-
时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量
-
世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间
-
GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准
-
UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致
-
C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换
-
时间戳转换
十七、BKP简介
-
BKP(Backup Registers)备份寄存器
-
BKP可用于存储用户应用程序数据。当VDD(2.03.6V)电源被切断,他们仍然由VBAT(1.83.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位
-
TAMPER引脚产生的侵入事件将所有备份寄存器内容清除
-
RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲
-
存储RTC时钟校准寄存器
-
用户数据存储容量:20字节(中容量和小容量)/ 84字节(大容量和互联型)
-
BKP基本结构
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
PWR_BackupAccessCmd(ENABLE); // 设置PWR_CR的DBP,使能对BKP的访问
BKP_WriteBackupRegister(BKP_DR1,ArrayWrite[0]); // 写备份寄存器 中容量,为1-10
BKP_WriteBackupRegister(BKP_DR2,ArrayWrite[1]);
ArrayRead[0] = BKP_ReadBackupRegister(BKP_DR1); // 读备份寄存器
ArrayRead[1] = BKP_ReadBackupRegister(BKP_DR2);
十八、RTC
1.RTC简介
- RTC(Real Time Clock)实时时钟
- RTC是一个独立的定时器,可为系统提供时钟和日历的功能
- RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.03.6V)断电后可借助VBAT(1.83.6V)供电继续走时
- 32位的可编程计数器,可对应Unix时间戳的秒计数器
- 20位的可编程预分频器,可适配不同频率的输入时钟
- 可选择三种RTC时钟源:
HSE时钟除以128(通常为8MHz/128)
LSE振荡器时钟(通常为32.768KHz)
LSI振荡器时钟(40KHz)
2.RTC框图
- RTCCLK在RCC里面配置
- RTC_DIV为自减,RTC_PRL为重装值,32767 ~ 0
- RTC——ALR放置闹钟,只响一次,之后需要重新配置
3.RTC基本结构
4.硬件电路
5.RTC操作注意事项
- 执行以下操作将使能对BKP和RTC的访问:
设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟
设置PWR_CR的DBP,使能对BKP和RTC的访问 - 必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器(库函数内部已经实现,不需要我们手动实现)
- 若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1
- 对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器
- RCC的LSERDY置1后,时钟启动完成
6.代码实现
- 开启PWR和BKP的时钟,使能BKP和RTC的访问
- 开启LSE时钟,并等待LSE时钟启动完成
- 选择RTCCLK时钟源
- 等待同步,数据同步到APB总线
- 等待上一次写入操作完成
void MyRTC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
PWR_BackupAccessCmd(ENABLE);
if(BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) // 在BKP里面随便写一个值,读到就代表VBAT没断电,初始化不需要重新配置
{
RCC_LSEConfig(RCC_LSE_ON);
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET); // 等待LSE启动完成
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); // 选择时钟
RCC_RTCCLKCmd(ENABLE); // 使能时钟
RTC_WaitForSynchro(); // 等待同步,数据同步到APB总线
RTC_WaitForLastTask(); // 等待上一次操作完成
RTC_SetPrescaler(32768 -1); // 配置预分频器
RTC_WaitForLastTask();
// RTC_SetCounter(1672588795); // 配置初始时间,写入cnt
// RTC_WaitForLastTask();
MyRTC_SetTime();
BKP_WriteBackupRegister(BKP_DR1,0xA5A5);
}
else
{
RTC_WaitForSynchro(); // 等待同步,数据同步到APB总线
RTC_WaitForLastTask(); // 等待上一次操作完成
}
}
- 把数组中的值写入tm结构体,转换为时间戳存入计数器
void MyRTC_SetTime(void)
{
time_t time_cnt;
struct tm time_data;
time_data.tm_year = MyRTC_Time[0] - 1900;
time_data.tm_mon = MyRTC_Time[1]-1;
time_data.tm_mday = MyRTC_Time[2];
time_data.tm_hour = MyRTC_Time[3];
time_data.tm_min = MyRTC_Time[4];
time_data.tm_sec = MyRTC_Time[5];
time_cnt = mktime(&time_data) -8*60*60;
RTC_SetCounter(time_cnt);
RTC_WaitForLastTask();
}
- 读取计数器中的值,转换为tm结构体,然后把值写入数组中读出
void MyRTC_ReadTime(void)
{
time_t time_cnt;
struct tm time_data;
time_cnt = RTC_GetCounter() + 8*60*60;
time_data = *localtime(&time_cnt);
MyRTC_Time[0] = time_data.tm_year + 1900;
MyRTC_Time[1] = time_data.tm_mon + 1;
MyRTC_Time[2] = time_data.tm_mday;
MyRTC_Time[3] = time_data.tm_hour;
MyRTC_Time[4] = time_data.tm_min;
MyRTC_Time[5] = time_data.tm_sec;
}
- 显示毫秒
OLED_ShowNum(4,6,(32767-RTC_GetDivider())/32767.0 * 999,10); // 显示毫秒
十九、PWR电源控制
1.PWR简介
- PWR(Power Control)电源控制
- PWR负责管理STM32内部的电源供电部分,可以实现可编程电压监测器和低功耗模式的功能
- 可编程电压监测器(PVD)可以监控VDD电源电压,当VDD下降到PVD阀值以下或上升到PVD阀值之上时,PVD会触发中断,用于执行紧急关闭任务
- 低功耗模式包括睡眠模式(Sleep)、停机模式(Stop)和待机模式(Standby),可在系统空闲时,降低STM32的功耗,延长设备使用时间
2.电源框图
3.上电和掉电复位
4.可编程电压监测器
5.低功耗模式
停机模式,存储器也不丢失
6.模式选择
(1)睡眠模式
- 执行完WFI/WFE指令后,STM32进入睡眠模式,程序暂停运行,唤醒后程序从暂停的地方继续运行
- SLEEPONEXIT位决定STM32执行完WFI或WFE后,是立刻进入睡眠,还是等STM32从最低优先级的中断处理程序中退出时进入睡眠
- 在睡眠模式下,所有的I/O引脚都保持它们在运行模式时的状态
- WFI指令进入睡眠模式,可被任意一个NVIC响应的中断唤醒
- WFE指令进入睡眠模式,可被唤醒事件唤醒
(2)停止模式
- 执行完WFI/WFE指令后,STM32进入停止模式,程序暂停运行,唤醒后程序从暂停的地方继续运行
- 1.8V供电区域的所有时钟都被停止,PLL、HSI和HSE被禁止,SRAM和寄存器内容被保留下来
- 在停止模式下,所有的I/O引脚都保持它们在运行模式时的状态
- 当一个中断或唤醒事件导致退出停止模式时,HSI被选为系统时钟
- 当电压调节器处于低功耗模式下,系统从停止模式退出时,会有一段额外的启动延时
- WFI指令进入停止模式,可被任意一个EXTI中断唤醒
- WFE指令进入停止模式,可被任意一个EXTI事件唤醒
(3)待机模式
- 执行完WFI/WFE指令后,STM32进入待机模式,唤醒后程序从头开始运行
- 整个1.8V供电区域被断电,PLL、HSI和HSE也被断电,SRAM和寄存器内容丢失,只有备份的寄存器和待机电路维持供电
- 在待机模式下,所有的I/O引脚变为高阻态(浮空输入)
- WKUP引脚的上升沿、RTC闹钟事件的上升沿、NRST引脚上外部复位、IWDG复位退出待机模式
7.代码实现
-
睡眠模式+串口收发,发送中断时执行
while末尾加上__WFI();
即可 -
停止模式+对射式红外传感器计次,红外传感器触发中断时执行
while开启RCC时钟,在while末尾进入停止模式,然后初始化系统时钟,系统使用的是HSE,停止后再开启会变成HSI,所以要手动改回去
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE); // 开始PWR时钟
PWR_EnterSTOPMode(PWR_Regulator_ON,PWR_STOPEntry_WFI); // 停止模式:低功耗还是开启,WFI还是WFE
SystemInit();
- 待机模式+实时时钟,以闹钟和WKUP引脚上拉为例
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
PWR_WakeUpPinCmd(ENABLE); // WKUP引脚上升
uint32_t Alarm = RTC_GetCounter() + 10;
RTC_SetAlarm(Alarm); // 设置闹钟为10秒后
PWR_EnterSTANDBYMode(); // 进入待机模式
二十、看门狗
1.WDG看门狗简介
- WDG(Watchdog)看门狗
- 看门狗可以监控程序的运行状态,当程序因为设计漏洞、硬件故障、电磁干扰等原因,出现卡死或跑飞现象时,看门狗能及时复位程序,避免程序陷入长时间的罢工状态,保证系统的可靠性和安全性
- 看门狗本质上是一个定时器,当指定时间范围内,程序没有执行喂狗(重置计数器)操作时,看门狗硬件电路就自动产生复位信号
- STM32内置两个看门狗
独立看门狗(IWDG):独立工作,对时间精度要求较低,独立时钟,只喂狗晚了复位
窗口看门狗(WWDG):要求看门狗在精确计时窗口起作用,使用APB1时钟,喂早了,喂晚了都复位
2.IWDG框图
3.IWDG键寄存器
- 键寄存器本质上是控制寄存器,用于控制硬件电路的工作
- 在可能存在干扰的情况下,一般通过在整个键寄存器写入特定值来代替控制寄存器写入一位的功能,以降低硬件电路受到干扰的概率
4.IWDG超时时间
- 超时时间:TIWDG = TLSI × PR预分频系数 × (RL + 1)
- 其中:TLSI = 1 / FLSI
5.WWDG框图
- 这里的T6为溢出标志位,WDGA是看门狗使能位
6.WWDG工作特性
- 递减计数器T[6:0]的值小于0x40时,WWDG产生复位
- 递减计数器T[6:0]在窗口W[6:0]外被重新装载时,WWDG产生复位
- 递减计数器T[6:0]等于0x40时可以产生早期唤醒中断(EWI),用于重装载计数器以避免WWDG复位
- 定期写入WWDG_CR寄存器(喂狗)以避免WWDG复位
7.WWDG超时时间
- 超时时间:
TWWDG = TPCLK1 × 4096 × WDGTB预分频系数 × (T[5:0] + 1) - 窗口时间:
TWIN = TPCLK1 × 4096 × WDGTB预分频系数 × (T[5:0] - W[5:0]) - 其中:TPCLK1 = 1 / FPCLK1,4096位固定分频
8.IWDG和WWDG对比
9.代码实现
(1)独立看门狗
- 开启LSI时钟(不需要手动开启)
- 写预分频器和重装寄存器(记得解除写保护)
- 启动独立看门狗
- 主循环里不断喂狗
if(RCC_GetFlagStatus(RCC_FLAG_IWDGRST) == SET)
{
OLED_ShowString(2,1,"IWDG RST");
Delay_ms(500);
OLED_ShowString(2,1," ");
Delay_ms(100);
RCC_ClearFlag();
}
else
{
OLED_ShowString(3,1,"RST");
Delay_ms(500);
OLED_ShowString(3,1," ");
Delay_ms(100);
}
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); // 解除写保护
IWDG_SetPrescaler(IWDG_Prescaler_16);
IWDG_SetReload(2500-1); // 1000毫秒
IWDG_ReloadCounter(); // 喂狗
IWDG_Enable(); // 启动独立看门狗 此时自动写保护
while(1)
{
Key_GetNum();
IWDG_ReloadCounter(); // 喂狗
OLED_ShowString(4,1,"FEED");
Delay_ms(200);
OLED_ShowString(4,1," ");
Delay_ms(600);
}
(2)窗口看门狗
窗口看门狗,启动之后,不能被关闭,除非复位。
- 开始APB1时钟
- 配置预分频器,窗口值
- 写入控制寄存器CR(看门狗使能位,溢出标志位,计数器有效位)
if(RCC_GetFlagStatus(RCC_FLAG_WWDGRST) == SET)
{
OLED_ShowString(2,1,"WWDG RST");
Delay_ms(500);
OLED_ShowString(2,1," ");
Delay_ms(100);
RCC_ClearFlag();
}
else
{
OLED_ShowString(3,1,"RST");
Delay_ms(500);
OLED_ShowString(3,1," ");
Delay_ms(100);
}
RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG,ENABLE);
WWDG_SetPrescaler(WWDG_Prescaler_8);
WWDG_SetWindowValue(21|0x40); // 30ms
WWDG_Enable(54|0x40); // |0x40是把溢出标志位置1 50ms
while(1)
{
Key_GetNum(); // 获取按键,会进入死循环
OLED_ShowString(4,1,"FEED");
Delay_ms(20);
OLED_ShowString(4,1," ");
Delay_ms(20);
WWDG_SetCounter(54|0x40); // 初始化喂狗了,如果放在前面会触发喂狗过早
}
二十一、FLASH闪存
1.FLASH简介
- STM32F1系列的FLASH包含程序存储器、系统存储器和选项字节三个部分,通过闪存存储器接口(外设)可以对程序存储器和选项字节进行擦除和编程
- 读写FLASH的用途:
利用程序存储器的剩余空间来保存掉电不丢失的用户数据
通过在程序中编程(IAP),实现程序的自我更新 - 在线编程(In-Circuit Programming – ICP)用于更新程序存储器的全部内容,它通过JTAG、SWD协议或系统加载程序(Bootloader)下载程序
- 在程序中编程(In-Application Programming – IAP)可以使用微控制器支持的任一种通信接口下载程序
闪存:非易失性存储器
FLASH:STM32内部闪存
2.闪存模块组织
3.FLASH基本结构
4.FLASH解锁
操作之前需要解锁, 使用键寄存器,防止误操作
- FPEC共有三个键值:
RDPRT键 = 0x000000A5
KEY1 = 0x45670123
KEY2 = 0xCDEF89AB - 解锁:
复位后,FPEC被保护,不能写入FLASH_CR
在FLASH_KEYR先写入KEY1,再写入KEY2,解锁
错误的操作序列会在下次复位前锁死FPEC和FLASH_CR - 加锁:
设置FLASH_CR中的LOCK位锁住FPEC和FLASH_CR
5.使用指针访问寄存器(FLASH读操作)
-
使用指针读指定地址下的存储器:
uint16_t Data = *((__IO uint16_t *)(0x08000000)); -
使用指针写指定地址下的存储器:
*((__IO uint16_t *)(0x08000000)) = 0x1234; -
其中:
#define __IO volatile
6.存储器编程和擦除
(1)程序存储器编程
(3)程序存储器页擦除
(4)程序存储器全擦除
7.选项字节
-
RDP:写入RDPRT键(0x000000A5)后解除读保护
-
USER:配置硬件看门狗和进入停机/待机模式是否产生复位
-
Data0/1:用户可自定义使用
-
WRP0/1/2/3:配置写保护,每一个位对应保护4个存储页(中容量)
-
n写入反码,用来校验
-
选项字节编程
检查FLASH_SR的BSY位,以确认没有其他正在进行的编程操作
解锁FLASH_CR的OPTWRE位
设置FLASH_CR的OPTPG位为1
写入要编程的半字到指定的地址
等待BSY位变为0
读出写入的地址并验证数据 -
选项字节擦除
检查FLASH_SR的BSY位,以确认没有其他正在进行的闪存操作
解锁FLASH_CR的OPTWRE位(解除小锁)
设置FLASH_CR的OPTER位为1(即将擦除选项字节)
设置FLASH_CR的STRT位为1(触发标志)
等待BSY位变为0
读出被擦除的选择字节并做验证
8.器件电子签名
-
电子签名存放在闪存存储器模块的系统存储区域,包含的芯片识别信息在出厂时编写,不可更改,使用指针读指定地址下的存储器可获取电子签名
-
闪存容量寄存器:
基地址:0x1FFF F7E0
大小:16位 -
产品唯一身份标识寄存器:
基地址: 0x1FFF F7E8
大小:96位
uint16_t Data = *((__IO uint16_t *)(0x1FFFF7E0)); // 闪存寄存器容量
OLED_ShowString(1,1,"F_SIZE:");
OLED_ShowHexNum(1,8,Data,4);
OLED_ShowString(2,1,"UID:");
OLED_ShowHexNum(2,6,*((__IO uint16_t *)(0x1FFFF7E8)),4);
OLED_ShowHexNum(2,11,*((__IO uint16_t *)(0x1FFFF7EA)),4);
OLED_ShowHexNum(3,1,*((__IO uint32_t *)(0x1FFFF7E8 + 0x04)),8);
OLED_ShowHexNum(4,1,*((__IO uint32_t *)(0x1FFFF7E8 + 0x08)),8);
9.修改程序代码的存储空间
程序代码的空间改小一点
10.SRAM统一的读写
- 初始化闪存:自定义一个标志位,如果在第一个半字没有标志位,则写入一个标志位,把本页其余的清0;如果有标志位,则把数据写到SRAM里
- 备份保存:修改完之后,统一的写进FLASH中
- 清除:把除标志位之外的擦除,并保存
// 操作FLASH
uint32_t MyFLASH_ReadWord(uint32_t Address)
{
return *((__IO uint32_t *)(Address));
}
uint32_t MyFLASH_ReadHalfWord(uint32_t Address)
{
return *((__IO uint16_t *)(Address));
}
uint32_t MyFLASH_ReadByte(uint32_t Address)
{
return *((__IO uint8_t *)(Address));
}
// 全擦除
void MyFLASH_EraseAllPages(void)
{
FLASH_Unlock();
FLASH_EraseAllPages();
FLASH_Lock();
}
// 指定页擦除
void MyFLASH_ErasePage(uint32_t PageAddress)
{
FLASH_Unlock();
FLASH_ErasePage(PageAddress);
FLASH_Lock();
}
// 指定地址编程一个字
void MyFLASH_ProgramWord(uint32_t Address , uint32_t Data)
{
FLASH_Unlock();
FLASH_ProgramWord(Address,Data);
FLASH_Lock();
}
// 指定地址编程半字
void MyFLASH_ProgramHalfWord(uint32_t Address , uint16_t Data)
{
FLASH_Unlock();
FLASH_ProgramHalfWord(Address,Data);
FLASH_Lock();
}
// 通过SRAM数组,来统一的读写FLASH
#define STORE_START_ADDRESS 0x0800FC00
#define STORE_COUNT 512
uint16_t Store_Data[STORE_COUNT]; // 一页1024
// 对闪存的初始化 写标志位,写0,读数据存到数组里
void Store_Init(void)
{
if(MyFLASH_ReadHalfWord(STORE_START_ADDRESS) != 0xA5A5) // 如果第一个半字不是A5A5,那就说明是第一次使用
{
MyFLASH_ErasePage(STORE_START_ADDRESS);
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS,0xA5A5);
for(uint16_t i = 1; i < STORE_COUNT; i++)
{
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2, 0x0000);
}
}
for(uint16_t i = 0; i < STORE_COUNT; i++)
{
Store_Data[i] = MyFLASH_ReadHalfWord(STORE_START_ADDRESS + i * 2);
}
}
// 备份保存
void Store_Save(void)
{
MyFLASH_ErasePage(STORE_START_ADDRESS);
for(uint16_t i = 0; i < STORE_COUNT; i++)
{
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2,Store_Data[i]);
}
}
// 清除
void Store_Clear(void)
{
for(uint16_t i = 1; i < STORE_COUNT; i++)
{
Store_Data[i] = 0x0000;
}
Store_Save();
}