0%

IWDG常用函数

1
void IWDG_WriteAccessCmd(uint16_t IWDG_WriteAccess);

写使能控制

1
void IWDG_SetPrescaler(uint8_t IWDG_Prescaler);

写预分频器,即写PR寄存器

1
void IWDG_SetReload(uint16_t Reload);

写重装值,即写RLR寄存器

1
void IWDG_ReloadCounter(void);

重新装载寄存器(喂狗)

1
void IWDG_Enable(void);

启用看门狗使能

1
FlagStatus IWDG_GetFlagStatus(uint16_t IWDG_FLAG);

获取标志位状态

1
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);

查看RCC标志位,查看时钟Ready和各种Reset标志位

独立看门狗

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "KEY.h"


int main(void)
{
OLED_Init();
KEY_Init();

OLED_ShowString(1,1,"IWDG TEST");

if(RCC_GetFlagStatus(RCC_FLAG_IWDGRST) == SET)//查看RCC标志位,是不是IWDG复位
{
OLED_ShowString(2,1,"IWDGRST");
Delay_ms(500);
OLED_ShowString(2,1," ");
Delay_ms(500);

RCC_ClearFlag();//清除标志位
}
else
{
OLED_ShowString(3,1,"RST");
Delay_ms(500);
OLED_ShowString(3,1," ");
Delay_ms(500);
}

IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);//解除写保护
//根据预分频值和重装值,超时时间是1000ms
IWDG_SetPrescaler(IWDG_Prescaler_16);//配置预分频
IWDG_SetReload(2499);//配置重装值
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);

//Delay_ms(800);//延时800ms,程序不会被看门狗复位
//Delay_ms(1200);//延时1200ms,喂狗时间不满足要求,程序被看门狗复位

}
}

WWDG常用函数

1
void WWDG_DeInit(void);

WWDG恢复缺省配置

1
void WWDG_SetPrescaler(uint32_t WWDG_Prescaler);

WWDG写入预分频值

1
void WWDG_SetWindowValue(uint8_t WindowValue);

WWDG写入窗口值

1
void WWDG_EnableIT(void);

WWDG使能中断

1
void WWDG_SetCounter(uint8_t Counter);

WWDG写入计数器(喂狗)

1
void WWDG_Enable(uint8_t Counter);

WWDG使能窗口看门狗

1
FlagStatus WWDG_GetFlagStatus(void);

WWDG获取标志位

1
void WWDG_ClearFlag(void);

WWDG清除标志位

窗口看门狗

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "KEY.h"


int main(void)
{
OLED_Init();
KEY_Init();

OLED_ShowString(1,1,"WWDG TEST");

if(RCC_GetFlagStatus(RCC_FLAG_WWDGRST) == SET)//查看RCC标志位,是不是WWDG复位
{
OLED_ShowString(2,1,"WWDGRST");
Delay_ms(500);
OLED_ShowString(2,1," ");
Delay_ms(500);

RCC_ClearFlag();//清除标志位
}
else
{
OLED_ShowString(3,1,"RST");
Delay_ms(500);
OLED_ShowString(3,1," ");
Delay_ms(500);
}
RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG,ENABLE);//开启窗口看门狗时钟

WWDG_SetPrescaler(WWDG_Prescaler_8);//设置预分频值
WWDG_SetWindowValue(0x40 | 21);//设置窗口值,窗口时间30ms
WWDG_Enable(0x40 | 54);//使能,超时时间50ms



while(1)
{
Key_GetNum();//获取按键值


OLED_ShowString(4,1,"FEED");
Delay_ms(20);
OLED_ShowString(4,1," ");
Delay_ms(20);

WWDG_SetCounter(0x40 | 54);//WWDG喂狗

//Delay_ms(800);//延时800ms,程序不会被看门狗复位
//Delay_ms(1200);//延时1200ms,喂狗时间不满足要求,程序被看门狗复位

}
}


WDG简介

•WDG(Watchdog)看门狗
•看门狗可以监控程序的运行状态,当程序因为设计漏洞、硬件故障、电磁干扰等原因,出现卡死或跑飞现象时,看门狗能及时复位程序,避免程序陷入长时间的罢工状态,保证系统的可靠性和安全性
•看门狗本质上是一个定时器,当指定时间范围内,程序没有执行喂狗(重置计数器)操作时,看门狗硬件电路就自动产生复位信号
•STM32内置两个看门狗
  独立看门狗(IWDG):独立工作,对时间精度要求较低
  窗口看门狗(WWDG):要求看门狗在精确计时窗口起作用(使用APB1时钟)

IWDG框图

IWDG框图
看门狗的结构和定时器比较相似。看门狗定时器溢出,产生复位信号。

喂狗操作,就是重置这个计数器,看门狗计数器是一个递减计数器,运行时不断自减,程序必须在它减到0之前,及时复位这个计数器。

如果没有及时复位这个计数器,减到0之后,自动复位。

看门狗预分频器之前的输入时钟,是LSI内部低速时钟,时钟进入预分频器进行分频,IWDG_PR可以配置分频系数。为了避免程序复位,需要提前在IWDG_RLR写一个值,在IWDG_KR寄存器里写一个特定数据,控制电路进行喂狗,这时重装值会复制到当前的计数器中。

IWDG键寄存器

•键寄存器本质上是控制寄存器,用于控制硬件电路的工作
•在可能存在干扰的情况下,一般通过在整个键寄存器写入特定值来代替控制寄存器写入一位的功能,以降低硬件电路受到干扰的概率

写入键寄存器的值作用
0xCCCC启用独立看门狗
0xAAAAIWDG_RLR中的值重新加载到计数器(喂狗)
0x5555解除IWDG_PR和IWDG_RLR的写保护
0x5555之外的其他值启用IWDG_PR和IWDG_RLR的写保护

IWDG超时时间

IWDG超时时间
IWDG超时时间

例如,PR写入0时,预分频为4,最短时间RL给0,最长时间RL给0xFF。
TLSI为1/40k=0.025ms,TIWDG为0.025x4x1 = 0.1ms0.025x4x4096 = 409.6ms

WWDG框图

WWDG框图
左下角是时钟源部分PCLK1(APB1的时钟,36MHz),右侧是预分频器WDGTB,上面是6位递减计数器CNT,这个计数器位于控制寄存器的CR中,窗口看门狗没有重装寄存器,只需要直接在CNT写入数据进行喂狗。上方为窗口值,即喂狗最早时间,左侧为输出信号的操作逻辑。

递减计数器为6位,最高位T6是溢出标志位,T6为1时,表示计数器未溢出,为0时,计数器溢出。

WDGA是窗口看门狗的激活位,写入1代表启用窗口看门狗。

喂狗时间最早界限,由WWDG_CFR部分控制,最早界限计数值写入到W0-W6中。

WWDG工作特性

•递减计数器T[6:0]的值小于0x40时,WWDG产生复位
•递减计数器T[6:0]在窗口W[6:0]外被重新装载时,WWDG产生复位
•递减计数器T[6:0]等于0x40时可以产生早期唤醒中断(EWI),用于重装载计数器以避免WWDG复位
定期写入WWDG_CR寄存器(喂狗)以避免WWDG复位
WWDG工作特性

WWDG超时时间

WWDG超时时间

IWDG和WWDG对比

IWDG独立看门狗WWDG窗口看门狗
中断早期唤醒中断
时钟源LSI(40KHz)PCLK1(36MHz)
预分频系数4、8、32、64、128、2561、2、4、8
计数器12位6位(有效计数)
超时时间0.1ms~26214.4ms113us~58.25ms
喂狗方式写入键寄存器,重装固定值RLR直接写入计数器,写多少重装多少
防误操作键寄存器和写保护
用途独立工作,对时间精度要求较低要求看门狗在精确计时窗口起作用

修改主频

system_stm32f10x.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
1.  This file provides two functions and one global variable to be called from 
* user application:
* - SystemInit(): Setups the system clock (System clock source, PLL Multiplier
* factors, AHB/APBx prescalers and Flash settings).
* This function is called at startup just after reset and
* before branch to main program. This call is made inside
* the "startup_stm32f10x_xx.s" file.
*
* - SystemCoreClock variable: Contains the core clock (HCLK), it can be used
* by the user application to setup the SysTick
* timer or configure other parameters.
*
* - SystemCoreClockUpdate(): Updates the variable SystemCoreClock and must
* be called whenever the core clock is changed
* during program execution.
*
* 2. After each device reset the HSI (8 MHz) is used as system clock source.
* Then SystemInit() function is called, in "startup_stm32f10x_xx.s" file, to
* configure the system clock before to branch to main program.
*
* 3. If the system clock source selected by user fails to startup, the SystemInit()
* function will do nothing and HSI still used as system clock source. User can
* add some code to deal with this issue inside the SetSysClock() function.
*
* 4. The default value of HSE crystal is set to 8 MHz (or 25 MHz, depedning on
* the product used), refer to "HSE_VALUE" define in "stm32f10x.h" file.
* When HSE is used as system clock source, directly or through PLL, and you
* are using different crystal you have to adapt the HSE value to your own
* configuration.

SystemInit()这个函数用来配置时钟树,在main函数之前自动调用

SystemCoreClock表示主频频率的值,显示此变量,可以知道目前主频的值。

SystemCoreClockUpdate()更新SystemCoreClock,因为这个变量只要最开始的一次赋值,改变主频频率,该值不会跟着改变。

1
2
3
4
5
6
7
8
9
10
11
#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
#define SYSCLK_FREQ_24MHz 24000000
#else
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
/* #define SYSCLK_FREQ_24MHz 24000000 */
/* #define SYSCLK_FREQ_36MHz 36000000 */
/* #define SYSCLK_FREQ_48MHz 48000000 */
/* #define SYSCLK_FREQ_56MHz 56000000 */
#define SYSCLK_FREQ_72MHz 72000000
#endif

想要修改主频值,解除注释。默认的主频是72000000(72M)

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"

int main(void)
{
OLED_Init();
OLED_ShowString(1,1,"SYSCLK:");
OLED_ShowNum(1,8,SystemCoreClock,8);//显示当前的主频值,可以在system_stm32f10x.c中修改

while(1)
{
//在默认的72M主频下,Running每500ms显示一次,修改主频,显示时间会变化,因为主频改变了。比如主频改成36M,Runing每1S显示一次。如果想要修改主频之后,Running仍500ms显示一次,需要修改Delay函数。可见不能随意修改主频。
OLED_ShowString(2,1,"Running");
Delay_ms(500);
OLED_ShowString(2,1," ");
Delay_ms(500);
}
}

睡眠模式+串口发送+接收

因为主循环一直在运行,空闲时运行没有意义,还费电。
因此靠中断触发,没有中断就没作用的程序,可以加入低功耗模式。

由于程序需要使用串口USART硬件电路接收数据,只能使用睡眠模式,不能使用停止模式或待机模式,因为停止模式和待机模式下CPU和外设都不能运行,无法正常接收串口数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
uint8_t RxData;
int main(void)
{
//初始化,配置串口
OLED_Init();
OLED_ShowString(1,1,"RxData:");
Serial_Init();
while (1)
{
if(Serial_GetRxFlag() == 1)
{
RxData = Serial_GetRxData();
Serial_SendByte(RxData);
OLED_ShowHexNum(1,8,RxData,2);
}
OLED_ShowString(2,1,"Running");
Delay_ms(100);
OLED_ShowString(2,1," ");
Delay_ms(100);

__WFI();//中断唤醒

//__WFE();//事件唤醒
}
}

通过串口给单片机发送数据,每发送一次数据,进入中断,单片机会被唤醒,Running闪烁一次。

停止模式+对射式红外传感器计次

对射式红外传感器计次使用外部中断触发唤醒,可以进入更省电的停止模式。
在停止模式下,1.8V区域时钟关闭,CPU和外设都没有时钟,但是外部中断的工作不需要时钟。

常用函数

1
void PWR_DeInit(void);

恢复PWR缺省配置

1
void PWR_BackupAccessCmd(FunctionalState NewState);

使能后备区域访问

1
void PWR_PVDCmd(FunctionalState NewState);

使能PVD功能

1
void PWR_PVDLevelConfig(uint32_t PWR_PVDLevel);

配置PVD的阈值电压

1
void PWR_WakeUpPinCmd(FunctionalState NewState);

使能位于PA0位置的WKUP引脚,配合待机模式使用

1
void PWR_EnterSTOPMode(uint32_t PWR_Regulator, uint8_t PWR_STOPEntry);

进入停止模式

1
void PWR_EnterSTANDBYMode(void);

进入待机模式

1
FlagStatus PWR_GetFlagStatus(uint32_t PWR_FLAG);

获取标志位

1
void PWR_ClearFlag(uint32_t PWR_FLAG);

清除标志位

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"


int main(void)
{
OLED_Init();
CountSensor_Init();

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);//开启PWR外设的时钟

OLED_ShowString(1,1,"Count:");

while(1)
{
OLED_ShowNum(1,7,CountSensor_Get(),5);

OLED_ShowString(2,1,"Running");
Delay_ms(100);
OLED_ShowString(2,1," ");
Delay_ms(100);

PWR_EnterSTOPMode(PWR_Regulator_ON,PWR_STOPEntry_WFI);//进入停止模式
SystemInit();//配置时钟
}
}

待机模式 + 实时时钟

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"

int main(void)
{
OLED_Init();
MyRTC_Init();

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);//开启PWR时钟


OLED_ShowString(1,1,"CNT:");
OLED_ShowString(2,1,"ALR:");
OLED_ShowString(3,1,"ALRF:");

PWR_WakeUpPinCmd(ENABLE);//WKUP引脚唤醒

uint32_t Alarm = RTC_GetCounter() + 10;
RTC_SetAlarm(RTC_GetCounter() + 10);
OLED_ShowNum(2,6,Alarm,10);

while(1)
{
OLED_ShowNum(1,6,RTC_GetCounter(),10);
OLED_ShowNum(3,6,RTC_GetFlagStatus(RTC_FLAG_ALR),1);

OLED_ShowString(4,1,"Running");
Delay_ms(100);
OLED_ShowString(4,1," ");
Delay_ms(100);

PWR_EnterSTANDBYMode();//进入待机模式
}

}

进入待机模式后,闹钟触发一次,程序从头开始执行。

PWR简介

•PWR(Power Control)电源控制
•PWR负责管理STM32内部的电源供电部分,可以实现可编程电压监测器和低功耗模式的功能
•可编程电压监测器(PVD)可以监控VDD电源电压,当VDD下降到PVD阀值以下或上升到PVD阀值之上时,PVD会触发中断,用于执行紧急关闭任务
•低功耗模式包括睡眠模式(Sleep)、停机模式(Stop)和待机模式(Standby),可在系统空闲时,降低STM32的功耗,延长设备使用时间

电源框图

电源框图
最上面是模拟部分供电(VDDA),中间是数字部分供电,包括VDD供电区域和1.8V供电区域。下面是后背供电(VBAT)。
VDDA供电部分,主要负责模拟部分供电,包括AD转换器,温度传感器,复位模块,PLL锁相环,这些电路的供电正极是VDDA,负极是VSSA。其中AD转换器,还有两根参考电压的供电脚,叫做VREF+和VREF-,这两个引脚在C8T6中已经分别接到VDDA和VSSA了,但是在引脚多的型号中是单独的引脚。
数字供电部分,有两部分组成,左侧是VDD供电区域,包括IO电路、待机电路、唤醒逻辑和独立看门狗,右侧是VDD通过电压调节器,降压到1.8V,包括CPU核心,存储器和内置数字外设。
后背供电区域,包括LSE 32K晶体振荡器、后背寄存器、RCC BDCR寄存器和RTC。低电压检测器,控制开关,VDD有电时,由VDD供电,没电时,由VBAT供电。

上电复位和掉电复位

上电复位和掉电复位
当VDD或VDDA的电压过低时,内部电路直接产生复位,STM32直接复位,复位和不复位的界限之间,设置了40mV的迟滞电压,大于上限POR时解除复位,小于下限PDR时复位。

可编程电压检测器

可编程电压检测器
PVD阈值电压使用程序指定。

低功耗模式

低功耗模式
PDDS=0,进入停机模式,PDDS=1,进入待机模式。
LPDS=0,电压调节器开启,LPDS=1,电压调节器进入低功耗。

模式选择

•执行WFI(Wait For Interrupt)或者WFE(Wait For Event)指令后,STM32进入低功耗模式
模式选择

睡眠模式

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

停止模式

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

待机模式

•执行完WFI/WFE指令后,STM32进入待机模式,唤醒后程序从头开始运行
•整个1.8V供电区域被断电,PLL、HSI和HSE也被断电,SRAM和寄存器内容丢失,只有备份的寄存器和待机电路维持供电
•在待机模式下,所有的I/O引脚变为高阻态(浮空输入)
•WKUP引脚的上升沿、RTC闹钟事件的上升沿、NRST引脚上外部复位、IWDG复位退出待机模式

常用函数

1
void BKP_DeInit(void);

BKP_DeInit恢复缺省配置,手动清空BKP所有的数据寄存器。

1
void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel);

用于配置TAMPER侵入检测功能,配置TAMPER引脚有效电平,高电平触发还是低电平触发。

1
void BKP_TamperPinCmd(FunctionalState NewState);

用于配置TAMPER侵入检测功能,是否开启侵入检测功能。

1
void BKP_ITConfig(FunctionalState NewState);

中断配置,是否开启中断。

1
void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource);

时钟输出功能,可以选择在RTC引脚上输出时钟信号,输出RTC校准时钟,RTC闹钟脉冲或秒脉冲。

1
void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue);

设置RTC校准值,写入RTC校准寄存器。

1
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);

写备份寄存器,第一个参数指定,写在哪个DR里,第二个参数指定要写入的数据。

1
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);

读备份寄存器,参数指定要读哪个DR。

1
FlagStatus BKP_GetFlagStatus(void);

获取BKP状态

1
void BKP_ClearFlag(void);

清除BKP侵入检测寄存器状态。

1
ITStatus BKP_GetITStatus(void);

获取侵入检测的中断状态。

1
void BKP_ClearITPendingBit(void);

清除侵入检测的中断挂起位。

1
void PWR_BackupAccessCmd(FunctionalState NewState);

备份寄存器访问使能。设置PWR_CR的DBP,使能对BKP和RTC的访问。

1
void RCC_LSEConfig(uint8_t RCC_LSE);

配置LSE外部低速时钟

1
void RCC_LSICmd(FunctionalState NewState);

配置LSI内部低速时钟

1
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);

RTCCLK配置,选择RTCCLK时钟源

1
void RCC_RTCCLKCmd(FunctionalState NewState);

RTCCLK使能,调用上面函数选择时钟之后,使能。

1
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);

获取RCC标志位,调用启动时钟函数之后,要等待标志位,LSERDY置1后,才算启动完成。

1
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);

配置RTC中断输出

1
void RTC_EnterConfigMode(void);

进入配置模式,CNF位置1。

1
void RTC_ExitConfigMode(void);

RTC退出配置模式,把CNF位置0。

1
uint32_t  RTC_GetCounter(void);

RTC获取CNT计数器的值。用于读取时钟。

1
void RTC_SetCounter(uint32_t CounterValue);

写入CNT计数器的值。用于设置时间。

1
void RTC_SetPrescaler(uint32_t PrescalerValue);

写入预分频器,写入到预分频的PRL重装寄存器中,配置预分频器的分频系数。

1
void RTC_SetAlarm(uint32_t AlarmValue);

写入闹钟值,用于配置闹钟。

1
uint32_t  RTC_GetDivider(void);

获取预分频器中的DIV余数寄存器。余数寄存器是一个自减计数器,获取这个值,一般是为了获取更细致的时间。

1
void RTC_WaitForLastTask(void);

等待上次操作完成。

1
void RTC_WaitForSynchro(void);

等待同步。清除RSF标志位,然后循环,直到RSF为1。

1
2
3
4
FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);
void RTC_ClearFlag(uint16_t RTC_FLAG);
ITStatus RTC_GetITStatus(uint16_t RTC_IT);
void RTC_ClearITPendingBit(uint16_t RTC_IT);

RTC标志位相关函数。

读写备份寄存器

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "key.h"

uint16_t ArrayWrite[] = {0x1234,0x5678};//存放写入数据的数组
uint16_t ArrayRead[2];//存放读取数据的数组
uint8_t KeyNum;

int main(void)
{
OLED_Init();
KEY_Init();

OLED_ShowString(1,1,"W:");
OLED_ShowString(2,1,"R:");

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);//开启APB1的PWR外设时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);//开启APB1的BKP外设时钟

PWR_BackupAccessCmd(ENABLE);//使能PWR


//BKP_WriteBackupRegister(BKP_DR1,0x1234);//BKP写入,在BKPDR1中写入了0x1234
//OLED_ShowHexNum(1,1,BKP_ReadBackupRegister(BKP_DR1),4);//OLED显示写入的值

while(1)
{
KeyNum = Key_GetNum();//获取按钮状态

if(KeyNum == 1)
{
ArrayWrite[0]++;//写入的数据自增1
ArrayWrite[1]++;

BKP_WriteBackupRegister(BKP_DR1,ArrayWrite[0]);//把ArrayWrite[0]的数据写入到DR1中
BKP_WriteBackupRegister(BKP_DR2,ArrayWrite[1]);//把ArrayWrite[1]的数据写入到DR2中

OLED_ShowHexNum(1,3,ArrayWrite[0],4);
OLED_ShowHexNum(1,8,ArrayWrite[1],4);
}
ArrayRead[0] = BKP_ReadBackupRegister(BKP_DR1);//读取BKPDR1的值,放到ArrayRead[0]中
ArrayRead[1] = BKP_ReadBackupRegister(BKP_DR2);//读取BKPDR2的值,放到ArrayRead[1]中

OLED_ShowHexNum(2,3,ArrayRead[0],4);
OLED_ShowHexNum(2,8,ArrayRead[1],4);
}
}

实时时钟

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"

int main(void)
{
OLED_Init();
MyRTC_Init();

OLED_ShowString(1,1,"Date:XXXX-XX-XX");
OLED_ShowString(2,1,"Time:XX:XX:XX");
OLED_ShowString(3,1,"CNT:");
OLED_ShowString(4,1,"DIV:");
while(1)
{
MyRTC_ReadTime();
OLED_ShowNum(1,6,MyRTC_Time[0],4);
OLED_ShowNum(1,11,MyRTC_Time[1],2);
OLED_ShowNum(1,14,MyRTC_Time[2],2);
OLED_ShowNum(2,6,MyRTC_Time[3],2);
OLED_ShowNum(2,9,MyRTC_Time[4],2);
OLED_ShowNum(2,12,MyRTC_Time[5],2);
OLED_ShowNum(3,6,RTC_GetCounter(),10);
OLED_ShowNum(4,6,RTC_GetDivider(),10);
}
}

MyRTC.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include "stm32f10x.h"                  // Device header
#include <time.h>

uint16_t MyRTC_Time[] = {2023,1,1,23,59,55};

void MyRTC_SetTime(void)
{
time_t time_cnt;
struct tm time_date;
//把数组指定的时间填充到结构体中
time_date.tm_year = MyRTC_Time[0] - 1900;
time_date.tm_mon = MyRTC_Time[1] - 1;
time_date.tm_mday = MyRTC_Time[2];
time_date.tm_hour = MyRTC_Time[3];
time_date.tm_min = MyRTC_Time[4];
time_date.tm_sec = MyRTC_Time[5];

time_cnt = mktime(&time_date) - 8*60*60;//得到秒数,偏移8小时

RTC_SetCounter(time_cnt);//秒数写入到RTC的CNT中
RTC_WaitForLastTask();//等待写入时间完成
}

void MyRTC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);//开启PWR时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);//开启BKP时钟
PWR_BackupAccessCmd(ENABLE);

if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)//判断DR1中的值是否为0xA5A5,用于判断是否完全断电
{
//开启LSE时钟,并等待LSE时钟启动完成
RCC_LSEConfig(RCC_LSE_ON);//LSE振荡器打开
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);//获取标志位,LSE时钟启动完成

//选择RTCCLK时钟源
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);//RTCCLK时钟源选择LSE时钟
RCC_RTCCLKCmd(ENABLE);//使能RTCCLK时钟

//等待同步和等待上一次写入操作完成
RTC_WaitForSynchro();//等待同步
RTC_WaitForLastTask();//等待上一次操作完成

//配置预分频器
RTC_SetPrescaler(32768-1);
RTC_WaitForLastTask();//等待写入预分频值完成

//RTC_SetCounter(1672588795);//设置初始时间
//RTC_WaitForLastTask();//等待写入时间完成

MyRTC_SetTime();

BKP_WriteBackupRegister(BKP_DR1,0xA5A5);
}
else
{
//等待同步和等待上一次写入操作完成
RTC_WaitForSynchro();//等待同步
RTC_WaitForLastTask();//等待上一次操作完成
}
}



void MyRTC_ReadTime(void)
{
time_t time_cnt;
struct tm time_date;
//得到秒数
time_cnt = RTC_GetCounter() + 8*60*60;//偏移8小时
time_date = *localtime(&time_cnt);

MyRTC_Time[0] = time_date.tm_year + 1900;
MyRTC_Time[1] = time_date.tm_mon + 1;
MyRTC_Time[2] = time_date.tm_mday;
MyRTC_Time[3] = time_date.tm_hour;
MyRTC_Time[4] = time_date.tm_min;
MyRTC_Time[5] = time_date.tm_sec;

}


BKP简介

•BKP(Backup Registers)备份寄存器
•BKP可用于存储用户应用程序数据。当VDD(2.03.6V)电源被切断,他们仍然由VBAT(1.83.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位
•TAMPER引脚产生的侵入事件将所有备份寄存器内容清除
•RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲(这3个功能,同时只能使用1个)
•存储RTC时钟校准寄存器
•用户数据存储容量:
  20字节(中容量和小容量)/ 84字节(大容量和互联型)

BKP基本结构

BKP简介

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)

RTC框图

RTC框图
上图的32位可编程计数器,对应着时间戳里的秒计数器。

RTC基本结构

RTC基本结构

硬件电路

硬件电路

RTC操作注意事项

•执行以下操作将使能对BKP和RTC的访问:
  设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟
  设置PWR_CR的DBP,使能对BKP和RTC的访问
•若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1
•必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器
•对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器

Unix时间戳

•Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒
•时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量
•世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间
Unix时间戳

UTC/GMT

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

时间戳转换

•C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换

函数作用
time_t time(time_t*);获取系统时钟
struct tm* gmtime(const time_t*);秒计数器转换为日期时间(格林尼治时间)
struct tm* localtime(const time_t*);秒计数器转换为日期时间(当地时间)
time_t mktime(struct tm*);日期时间转换为秒计数器(当地时间)
char* ctime(const time_t*);秒计数器转换为字符串(默认格式)
char* asctime(const struct tm*);日期时间转换为字符串(默认格式)
size_t strftime(char*, size_t, const char*, const struct tm*);日期时间转换为字符串(自定义格式)
时间戳转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <time.h>

time_t time_cnt;
struct tm time_date;
char *time_str;


int main()
{
//time_cnt = time(NULL);
time(&time_cnt);
printf("%d\n",time_cnt);

time_date = *gmtime(&time_cnt);
printf("%d\n",time_date.tm_year + 1900);
printf("%d\n",time_date.tm_mon + 1);
printf("%d\n",time_date.tm_mday);
printf("%d\n",time_date.tm_hour);
printf("%d\n",time_date.tm_min);
printf("%d\n",time_date.tm_sec);
printf("%d\n",time_date.tm_wday);
return 0;
}

SPI外设简介

•STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
•可配置8位/16位数据帧、高位先行/低位先行
•时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256);不支持任意指定,只能进行指定的8种分频
•支持多主机模型、主或从操作
•可精简为半双工/单工通信
•支持DMA
•兼容I2S协议(音频传输协议)
•STM32F103C8T6 硬件SPI资源:SPI1(APB2,72M)、SPI2(APB1,36M)

SPI框图

SPI框图
左上角,核心部分是移位寄存器,右边的数据低位,一位位地从MOSI移出去,然后MISO的数据,一位一位地移入到左侧地数据高位。LSBFIRST可以控制是低位先行(1,LSB)还是高位先行(0,MSB)
右下角,波特率发生器,是用来产生SCK时钟的。CR1寄存器的三个系数BR0,BR1,BR2用来控制分频系数。
NSS引脚,SS是从机选择,低电平有效。通常多从机使用。

SPI基本结构

SPI基本结构
核心部分位为数据寄存器和移位寄存器。TDR寄存器整体移入移位寄存器,置TXE标志位。移位寄存器整体移入RDR时,置RXNE标志位。

主模式全双工连续传输

主模式全双工连续传输
性能强,使用复杂。
CPOL=1,CPHA=1,示例使用的是SPI模式3。
第一行,SCK时钟线。在时钟线第一个下降沿,MOSI和MISO移出数据,之后上升沿移入数据,依次进行。
TXE是发送寄存器空标志位。
BSY标志,当有数据传输时,BSY置1。

非连续传输

非连续传输
容易使用,但是损失性能。

硬件SPI读写W25Q64

常用函数
1
void SPI_I2S_DeInit(SPI_TypeDef* SPIx);

SPI恢复缺省配置

1
void I2S_Init(SPI_TypeDef* SPIx, I2S_InitTypeDef* I2S_InitStruct);

SPI初始化

1
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);

SPI结构体初始化

1
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);

SPI外设使能

1
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);

SPI中断使能

1
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);

DMA使能

1
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);

写DR数据寄存器

1
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

读DR数据寄存器

1
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);

SPI获取标志位状态

W25Q64简介

•W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器(数据掉电不丢失),常应用于数据存储、字库存储、固件程序存储等场景
•存储介质:Nor Flash(闪存)
•时钟频率:80MHz / 160MHz (Dual SPI双重SPI) / 320MHz (Quad SPI四重SPI)
•存储容量(24位地址):
  W25Q40:    4Mbit / 512KByte
  W25Q80:    8Mbit / 1MByte
  W25Q16:    16Mbit / 2MByte
  W25Q32:    32Mbit / 4MByte
  W25Q64:    64Mbit / 8MByte
  W25Q128:  128Mbit / 16MByte
  W25Q256:  256Mbit / 32MByte

硬件电路

硬件电路

硬件电路

引脚功能
VCC、GND电源(2.7~3.6V)
CS(SS)SPI片选
CLK(SCK)SPI时钟
DI(MOSI)SPI主机输出从机输入
DO(MISO)SPI主机输入从机输出
WP写保护
HOLD数据保持

W25Q64框图

W25Q64框图
右上角为存储器的规划示意图,以64KB为基本单元,分为若干个块。

Flash操作注意事项

写入操作时:

•写入操作前,必须先进行写使能(使用SPI发送写使能的指令)
•每个数据位只能由1改写为0,不能由0改写为1
•写入数据前必须先擦除,擦除后,所有数据位变为1
•擦除必须按最小擦除单元进行
•连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
•写入操作结束后,芯片进入忙状态,不响应新的读写操作

读取操作时:

•直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取

软件SPI读写W25Q64

MySPI.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include "stm32f10x.h"                  // Device header

void MySPI_W_CS(uint8_t BitValue)//写CS引脚
{
GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
}

void MySPI_W_SCK(uint8_t BitValue)//写SCK引脚
{
GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue);
}

void MySPI_W_MOSI(uint8_t BitValue)//写MOSI引脚
{
GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue);
}

uint8_t MySPI_R_MISO(void)//读MISO引脚
{
return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}

void MySPI_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//初始化DI、CLK、CS为推挽输出
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_SetBits(GPIOA,GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7);
//初始化DO为上拉输入
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);
GPIO_SetBits(GPIOA,GPIO_Pin_6);
MySPI_W_CS(1);
MySPI_W_SCK(0);
}

void MySPI_Start(void)//起始信号,只需要把CS置低电平即可
{
MySPI_W_CS(0);
}

void MySPI_Stop(void)//终止信号,只需要把CS置高电平即可
{
MySPI_W_CS(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);
// }
uint8_t i;
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;
}

W25Q64.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"

void W25Q64_Init(void)//初始化W25Q64
{
MySPI_Init();
}

void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)//读取ID
{
MySPI_Start();//先发送起始信号
MySPI_SwapByte(W25Q64_JEDEC_ID);//发送0x9F,交换到厂商号
*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//接收厂商号,发送的0xFF是为了交换到返回的厂商号
*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//接收返回值的高8位
*DID <<= 8;//左移8位
*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);//接收返回值的低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 = 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();//事后等待
}

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);//发送0xFF,换回有用的数据
}
MySPI_Stop();
}

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"

uint8_t MID;
uint16_t DID;

uint8_t ArrayWrite[] = {0x55,0x66,0x77,0x88};
uint8_t ArrayRead[4];

int main(void)
{
OLED_Init();
W25Q64_Init();

OLED_ShowString(1,1,"MID: DID:");
OLED_ShowString(2,1,"W:");
OLED_ShowString(3,1,"R:");
W25Q64_ReadID(&MID,&DID);
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);
}


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引脚(SS线低电平有效
•输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
硬件电路
左侧的SPI主机,主导整个SPI总线,一般是控制器来做,比如STM32。

由于有3个从机,因此需要3根SS(从机选择)线。

上图中没画出GND线,但是需要所有设备共地。

时钟线SCK完全由主机掌控,因此对于主机来说SCK为输出,对于所有从机来说,SCK为输入,由此把主机的同步时钟送到各个从机。
MOSI,主机输出从机输入,主机通过MOSI输出,所有从机通过MOSI输入。
MISO,主机输入从机输出,所有从机通过MISO输出,主机通过MISO输入。

移位示意图

移位示意图
每来一个时钟,主机中的移位寄存器向左移位,从机也是一样。时钟上升沿,主机和从机进行输出,把数据放到MOSI和MISO线上,时钟下降沿,主机和从机进行读取,依次交换字节。

时钟源(波特率发生器)由主机提供,驱动主机的移位寄存器移位,同时通过SCK引脚输出,到从机的移位寄存器。

主机移位寄存器左侧移出的数据,通过MOSI引脚,输入到从机移位寄存器的右边。从机移位寄存器左边移出的数据,通过MISO引脚输入到主机移位寄存器的左侧。

如果SPI主机只想发送数据,不想接收怎么办?

按照交换字节的时序,发送同时接收,但是不读取接收到的内容即可。

SPI时序基本单元

•起始条件:SS从高电平切换到低电平,左侧,代表选中某个从机
•终止条件:SS从低电平切换到高电平,右侧,结束从机选中状态
SPI时序基本单元

!!! note “”
CPHA表示时钟相位,决定第一个时钟采样移入还是第二个时钟采样移入。

•交换一个字节(模式0)
•CPOL=0:空闲状态时,SCK为低电平
•CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
交换一个字节(模式0)

•交换一个字节(模式1)
•CPOL=0:空闲状态时,SCK为低电平
•CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
交换一个字节(模式1)

•交换一个字节(模式2)
•CPOL=1:空闲状态时,SCK为高电平
•CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
交换一个字节(模式2)

•交换一个字节(模式3)
•CPOL=1:空闲状态时,SCK为高电平
•CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
交换一个字节(模式3)

SPI时序

•发送指令
•向SS指定的设备,发送指令(0x06)
下图,主机用0x06换来了从机的0xFF,但是实际上从机没有输出,0xFF是默认的高电平,没有意义。
SPI时序

•指定地址写
•向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data)
下图,发送的第一个数据0x02,为字节指令,第2(0x12)、3(0x34)、4(0x56)个字节为地址0x123456(W25Q64芯片的地址长度为3个字节),第5个字节为写入的数据,表示在0x123456地址下,写入0x55这个数据。
指定地址写

•指定地址读
•向SS指定的设备,发送读指令(0x03),随后在指定地址(Address[23:0])下,读取从机数据(Data)
下图,发送的第一个数据0x03,为字节指令,第2(0x12)、3(0x34)、4(0x56)个字节为地址0x123456(W25Q64芯片的地址长度为3个字节),第5个字节为0xFF,是为了和从机交换给的数据,此时从机传回0x123456地址下的数据0x55。
指定地址读