正点原子STM32(基于标准库)

正点原子B站视频地址:https://www.bilibili.com/video/BV1Lx411Z7Qa?p=4&spm_id_from=pageDriver

目录

STM32命名规则

在这里插入图片描述

STM32芯片解读

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

开发环境搭建(MDK - 就是ARM的keil,需破解 + 支持包 + CH340串口驱动+ JLINK驱动)

在这里插入图片描述
上图在MDK环境中点击下载即可,或者去官网下载:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

程序下载方法 (ISP串口下载 + JLINK下载更方便)

ISP串口下载

在这里插入图片描述

以前学习的是电脑USB端连接CH340芯片。
在这里插入图片描述

正点原子还设计了一键下载电路,不用来回拨动跳线帽。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
JLINK程序下载

需要先安装JLINK驱动,前面讲过了
在这里插入图片描述
在这里插入图片描述

在MDK中配置一下JLINK
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

新建工程模板——基于固件库

视频开发环境:MDK5
固件库版本:V3.5

解压正点原子文件夹内的固件库包–C:\Users\朱冠霖\Desktop\【正点原子】战舰STM32F103开发板\【正点原子】战舰STM32F103开发板资料 资料盘(A盘)\8,STM32参考资料\1,STM32F1xx固件库

新建工程模板——基于寄存器

GPIO相关配置寄存器

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

端口复用、重映射、中断

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

JLINK在线调试

JTAG/SWD调试原理

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述 在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

软件仿真

在这里插入图片描述

ST-LINK下载与调试程序(硬件)

独立看门狗IWDG

独立看门狗概述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

寄存器和库函数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
上图prer表示预分屏值
在这里插入图片描述
在这里插入图片描述

代码实现

wdg.h

#ifndef __WDG_H
#define __WDG_H
#include "sys.h"

void IWDG_Init(u8 prer,u16 rlr);
void IWDG_Feed(void);

#endif

wdg.c

#include "wdg.h"

//初始化独立看门狗
//prer:分频数:0~7(只有低3位有效!)
//分频因子=4*2^prer.但最大值只能是256!
//rlr:重装载寄存器值:低11位有效.
//时间计算(大概):Tout=((4*2^prer)*rlr)/40 (ms).
void IWDG_Init(u8 prer,u16 rlr) 
{	
 	IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); //使能对寄存器IWDG_PR和IWDG_RLR的写操作(取消写保护)
	
	IWDG_SetPrescaler(prer);  //设置IWDG预分频值:设置IWDG预分频值为64
	
	IWDG_SetReload(rlr);  //设置IWDG重装载值rlr
	
	IWDG_ReloadCounter();  //按照IWDG重装载寄存器的值rlr重装载IWDG计数器
	
	IWDG_Enable();  //使能IWDG
}
//喂独立看门狗
void IWDG_Feed(void)
{   
 	IWDG_ReloadCounter();//reload										   
}

main.c

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "usart.h"
#include "wdg.h"
 
int main(void)
{		
	delay_init();	     //延时函数初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 	 //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 //串口初始化为115200
 	LED_Init();		  	 //初始化与LED连接的硬件接口
	KEY_Init();          //按键初始化	 
	delay_ms(500);   	 //让人看得到灭
	IWDG_Init(4,625);    //预分频数为64,重载值为625,溢出时间为1s	   
	LED0=0;				 //点亮LED0
	while(1)
	{
		if(KEY_Scan(0)==WKUP_PRES)
		{	//调用函数IWDG_ReloadCounter()一样的		
			IWDG_Feed();//如果WK_UP按下,则喂狗,灯一直亮  不喂狗则程序一直复位灯一直闪烁
		}
		delay_ms(10);
	};	 
}

窗口看门狗WWDG

窗口看门狗概述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

寄存器和库函数

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码实现

wdg.h

#ifndef __WDG_H
#define __WDG_H
#include "sys.h"
 

void IWDG_Init(u8 prer,u16 rlr);
void IWDG_Feed(void);

void WWDG_Init(u8 tr,u8 wr,u32 fprer);//初始化WWDG
void WWDG_Set_Counter(u8 cnt);       //设置WWDG的计数器
void WWDG_NVIC_Init(void);
#endif

wdg.c

#include "wdg.h"
#include "led.h"


void IWDG_Init(u8 prer,u16 rlr) 
{	
 	IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);  //使能对寄存器IWDG_PR和IWDG_RLR的写操作
	
	IWDG_SetPrescaler(prer);  //设置IWDG预分频值:设置IWDG预分频值为64
	
	IWDG_SetReload(rlr);  //设置IWDG重装载值
	
	IWDG_ReloadCounter();  //按照IWDG重装载寄存器的值重装载IWDG计数器 
	
	IWDG_Enable();  //使能IWDG
}
//喂独立看门狗
void IWDG_Feed(void)
{   
 	IWDG_ReloadCounter();	//重载计数值									   
}


//保存WWDG计数器的设置值,默认为最大. 
u8 WWDG_CNT=0x7f; 
//初始化窗口看门狗 	
//tr   :T[6:0],计数器值  往下计数
//wr   :W[6:0],窗口值 
//fprer:分频系数(WDGTB),仅最低2位有效 
//Fwwdg=PCLK1/(4096*2^fprer). 
void WWDG_Init(u8 tr,u8 wr,u32 fprer)
{ 
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE);  //   WWDG时钟使能

	WWDG_CNT=tr&WWDG_CNT;   //初始化WWDG_CNT. 与运算取后7位  
	WWDG_SetPrescaler(fprer);设置IWDG预分频值

	WWDG_SetWindowValue(wr);//设置窗口值

	WWDG_Enable(WWDG_CNT);	 //使能看门狗 ,	设置 counter .                  

	WWDG_ClearFlag();//清除提前唤醒中断标志位 

	WWDG_NVIC_Init();//初始化窗口看门狗 NVIC

	WWDG_EnableIT(); //开启窗口看门狗中断
} 
//重设置WWDG计数器的值
void WWDG_Set_Counter(u8 cnt)
{
    WWDG_Enable(cnt);//使能看门狗 ,	设置 counter .	 
}

//窗口看门狗中断服务程序
void WWDG_NVIC_Init()
{
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = WWDG_IRQn;    //WWDG中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;   //抢占2,子优先级3,组2	
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;	 //抢占2,子优先级3,组2	
    NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE; 
	NVIC_Init(&NVIC_InitStructure);//NVIC初始化
}

void WWDG_IRQHandler(void)
	{

	WWDG_SetCounter(WWDG_CNT);//喂狗 当禁掉此句后,窗口看门狗将产生复位

	WWDG_ClearFlag();	  //清除提前唤醒中断标志位

	LED1=!LED1;		 //LED1状态翻转(指示喂狗成功)
	}

main.c

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "usart.h"
#include "wdg.h"
 

 int main(void)
 {		
	delay_init();	    	 //延时函数初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 //串口初始化为115200
 	LED_Init();
	KEY_Init();          //按键初始化	 
	LED0=0;
	delay_ms(300);	  					  //计数器从0x7F到0x40,触发中断喂狗,LED1灯翻转
	WWDG_Init(0X7F,0X5F,WWDG_Prescaler_8);//计数器值为7f,上窗口寄存器为5f(下窗口固定),分频数为8	   
 	while(1)
	{
		LED0=1;			  	   
	}   
}

RTC 实时时钟

前面我们介绍了两款液晶模块,这一章我们将介绍STM32F1 的内部实时时钟(RTC)。在
本章中,我们将利用ALIENTEK 2.8 寸TFTLCD 模块来显示日期和时间,实现一个简单的时钟。
另外,本章将顺带向大家介绍BKP(备份寄存器) 的使用。
在这里插入图片描述

STM32F1 RTC 时钟简介

STM32 的实时时钟(RTC)是一个独立的定时器。STM32 的RTC 模块拥有一组连续计数
的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当前的时间和日期。

RTC 模块和时钟配置系统(RCC_BDCR 寄存器)是在后备区域,即在系统复位或从待机模式
唤醒后RTC 的设置和时间维持不变。但是在系统复位后,会自动禁止访问后备寄存器和RTC,以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前,先要取消备份区域(BKP)写保护。

RTC 的简化框图,如图20.1.1 所示:
在这里插入图片描述
RTC 由两个主要部分组成(参见图20.1.1),第一部分(APB1 接口)用来和APB1 总线相连。此单元还包含一组16 位寄存器,可通过APB1 总线对其进行读写操作。APB1 接口由APB1 总线时钟驱动,用来与APB1 总线连接。通过APB1接口可以访问RTC的相关寄存器(预分频值,计数器值、闹钟值)。

另一部分(RTC 核心)由一组可编程计数器组成,分成两个主要模块。
第一个模块是RTC 的预分频模块,它可编程产生1 秒的RTC 时间基准TR_CLK。RTC 的预分频模块包含了一个20位的可编程分频器(RTC 预分频器)。如果在RTC_CR 寄存器中设置了相应的允许位,则在每个TR_CLK 周期中RTC 产生一个中断(秒中断)。
第二个模块是一个32 位的可编程计数器,可被初始化为当前的系统时间,一个32 位的时钟计数器,按秒钟计算,可以记录4294967296 秒,约合136 年左右,作为一般应用,这已经是足够了的。

RTC 还有一个闹钟寄存器RTC_ALR,用于产生闹钟。系统时间按TR_CLK 周期累加并与存储在RTC_ALR 寄存器中的可编程时间相比较,如果RTC_CR 控制寄存器中设置了相应允许
位,比较匹配时将产生一个闹钟中断。

RTC 内核完全独立于RTC APB1 接口,而软件是通过APB1 接口访问RTC 的预分频值、计
数器值和闹钟值的。但是相关可读寄存器只在RTC APB1 时钟进行重新同步的RTC 时钟的上升沿被更新,RTC 标志也是如此。这就意味着,如果APB1 接口刚刚被开启之后,在第一次的内部寄存器更新之前,从APB1 上读取的RTC 寄存器值可能被破坏了(通常读到0)。因此,若在读取RTC 寄存器曾经被禁止的RTC APB1 接口,软件首先必须等待RTC_CRL 寄存器的RSF位(寄存器同步标志位,bit3)被硬件置1。

要理解RTC 原理,我们必须先通过对寄存器的讲解,让大家有一个全面的了解。接下来,
我们介绍一下RTC 相关的几个寄存器。

这里是引用
在这里插入图片描述

首先要介绍的是RTC 的控制寄存器,RTC 总共有2 个控制寄存器RTC_CRH 和RTC_CRL,两个都是16 位的。RTC_CRH 的各位描如图20.1.2 所示:

在这里插入图片描述

这里是引用

该寄存器用来控制中断的,我们本章将要用到秒钟中断,所以在该寄存器必须设置最低位
为1,以允许秒钟中断。
我们再看看RTC_CRL 寄存器。该寄存器各位描述如图20.1.3 所示:

在这里插入图片描述

本章我们用到的是该寄存器的0、3~5 这几个位。

  • 第0位是秒钟标志位,我们在进入闹钟中断的时候,通过判断这位来决定是不是发生了秒钟中断。然后必须通过软件将该位清零(写0)。
  • 第3 位为寄存器同步标志位,我们在修改控制寄存器RTC_CRH/CRL 之前,必须先判断该位,是否已经同步了,如果没有则等待同步,在没同步的情况下修改RTC_CRH/CRL 的值是不行的。
  • 第4 位为配置标位,在软件修改RTC_CNT/RTC_ALR/RTC_PRL 的值的时候,必须先软件置位该位,以允许进入配置模式。修改完之后,设置CNF位为0退出配置模式。
  • 第5 位为RTC 操作位,该位由硬件操作,软件只读。通过该位可以判断上次对RTC 寄存器的操作是否完成,如果没有,我们必须等待上一次操作结束才能开始下一次操作。

第二个要介绍的寄存器是RTC 预分频装载寄存器,也有2个寄存器组成,RTC_PRLH 和
RTC_PRLL(分别代表高位和低位)。这两个寄存器用来配置RTC 时钟的分频数的,比如我们使用外部32.768K 的晶振作为时钟的输入频率,那么我们要设置这两个寄存器的值为32767,以得到一秒钟的计数频率。

RTC_PRLH 的各位描述如图20.1.4 所示:

在这里插入图片描述
从图20.1.4 可以看出,RTC_PRLH 只有低四位有效,用来存储PRL 的19~16 位。而PRL
的前16 位,存放在RTC_PRLL 里面,寄存器RTC_PRLL 的各位描述如图20.1.5 所示:

在这里插入图片描述

在介绍完这两个寄存器之后,我们介绍RTC 预分频器余数寄存器,该寄存器也有2 个寄存器组成RTC_DIVH 和RTC_DIVL,这两个寄存器的作用就是用来获得比秒钟更为准确的时钟,比如可以得到0.1 秒,或者0.01 秒等。该寄存器的值自减的,用于保存还需要多少时钟周期获得一个秒信号。在一次秒钟更新后,由硬件重新装载。这两个寄存器和RTC 预分频装载寄存器的各位是一样的,这里我们就不列出来了。

这里是引用

接着要介绍的是RTC 最重要的寄存器,RTC 计数器寄存器RTC_CNT。该寄存器由2 个16
位的寄存器组成RTC_CNTH 和RTC_CNTL,总共32 位,用来记录秒钟值(一般情况下)。此两个计数器也比较简单,我们也不多说了。注意一点,在修改这个寄存器的时候要先进入配置模式。

这里是引用

最后我们介绍RTC 部分的最后一个寄存器,RTC 闹钟寄存器,该寄存器也是由2 个16 为
的寄存器组成RTC_ALRH 和RTC_ALRL。总共也是32 位,用来标记闹钟产生的时间(以秒为单位),如果RTC_CNT 的值与RTC_ALR 的值相等,并使能了中断的话,会产生一个闹钟中断。该寄存器的修改也要进入配置模式才能进行。

这里是引用

因为我们使用到备份寄存器(BKP)来存储RTC 的相关信息(我们这里主要用来标记时钟是否已经经过了配置),我们这里顺便介绍一下STM32 的备份寄存器。
备份寄存器是42 个16 位的寄存器(战舰开发板就是大容量的),可用来存储84 个字节的
用户应用程序数据。他们处在备份域里,当VDD 电源被切断,他们仍然由VBAT(纽扣电池) 维持供电。
即使系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。
此外,BKP 控制寄存器用来管理侵入检测和RTC 校准功能,这里我们不作介绍。
复位后,对备份寄存器和RTC 的访问被禁止,并且备份域被保护以防止可能存在的意外的
写操作。执行以下操作可以使能对备份寄存器和RTC 的访问:

  • 1)通过设置寄存器RCC_APB1ENR 的PWREN 和BKPEN 位来打开电源和后备接口的时钟;
  • 2)电源控制寄存器(PWR_CR)的DBP 位来使能对后备寄存器和RTC 的访问。

我们一般用BKP 来存储RTC 的校验值或者记录一些重要的数据,相当于一个EEPROM,不过这个EEPROM 并不是真正的EEPROM,而是需要电池来维持它的数据。关于BKP 的详细介绍请看《STM32 参考手册》的第47 页,5.1 一节。

最后,我们还要介绍一下备份区域控制寄存器RCC_BDCR。该寄存器的个位描述如图20.1.6所示:
在这里插入图片描述

RTC 的时钟源选择及使能设置都是通过这个寄存器来实现的,所以我们在RTC 操作之前
先要通过这个寄存器选择RTC 的时钟源,然后才能开始其他的操作。
寄存器介绍就给大家介绍到这里了,我们下面来看看要经过哪几个步骤的配置才能使RTC
正常工作,这里我们将对每个步骤通过库函数的实现方式来讲解。
RTC 相关的库函数在文件stm32f10x_rtc.c 和stm32f10x_rtc.h 文件中,BKP 相关的库函数在
文件stm32f10x_bkp.c 和文件stm32f10x_bkp.h 文件中。

RTC 正常工作的一般配置步骤如下:

这里是引用
在这里插入图片描述

1)使能电源时钟和备份区域时钟。
前面已经介绍了,我们要访问RTC 和备份区域就必须先使能电源时钟和备份区域时钟。

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);

2)取消备份区写保护。
要向备份区域写入数据,就要先取消备份区域写保护(写保护在每次硬复位之后被使能),
否则是无法向备份区域写入数据的。我们需要用到向备份区域写入一个字节,来标记时钟已经配置过了,这样避免每次复位之后重新配置时钟。取消备份区域写保护的库函数实现方法是:

PWR_BackupAccessCmd(ENABLE); //使能RTC 和后备寄存器访问

3)复位备份区域,开启外部低速振荡器。
在取消备份区域写保护之后,我们可以先对这个区域复位,以清除前面的设置,当然这个
操作不要每次都执行,因为备份区域的复位将导致之前存在的数据丢失,所以要不要复位,要看情况而定。然后我们使能外部低速振荡器,注意这里一般要先判断RCC_BDCR 的LSERDY位来确定低速振荡器已经就绪了才开始下面的操作。
备份区域复位的函数是:

BKP_DeInit();//复位备份区域

开启外部低速振荡器的函数是:

RCC_LSEConfig(RCC_LSE_ON);// 开启外部低速振荡器

4)选择RTC 时钟,并使能。
这里我们将通过RCC_BDCR 的RTCSEL 来选择选择外部LSI 作为RTC 的时钟。然后通过
RTCEN 位使能RTC 时钟。
库函数中,选择RTC 时钟的函数是:

RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择LSE 作为RTC 时钟

对于RTC 时钟的选择,还有RCC_RTCCLKSource_LSI 和RCC_RTCCLKSource_HSE_Div128两个,顾名思义,前者为LSI,后者为HSE 的128 分频,这在时钟系统章节有讲解过。
使能RTC 时钟的函数是:

RCC_RTCCLKCmd(ENABLE); //使能RTC 时钟

5)设置RTC 的分频,以及配置RTC 时钟。
在开启了RTC 时钟之后,我们要做的就是设置RTC 时钟的分频数,通过RTC_PRLH 和
RTC_PRLL 来设置,然后等待RTC 寄存器操作完成,并同步之后,设置秒钟中断。然后设置RTC 的允许配置位(RTC_CRH 的CNF 位),设置时间(其实就是设置RTC_CNTH 和RTC_CNTL两个寄存器)。下面我们一一这些步骤用到的库函数:
在进行RTC 配置之前首先要打开允许配置位(CNF),库函数是:

RTC_EnterConfigMode();/// 允许配置

在配置完成之后,千万别忘记更新配置同时退出配置模式,函数是:

RTC_ExitConfigMode();//退出配置模式,更新配置

设置RTC 时钟分频数,库函数是:

void RTC_SetPrescaler(uint32_t PrescalerValue);

这个函数只有一个入口参数,就是RTC 时钟的分频数,很好理解。
然后是设置秒中断允许,RTC 使能中断的函数是:

void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState)

这个函数的第一个参数是设置秒中断类型,这些通过宏定义定义的。对于使能秒中断方法是:

RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC 秒中断

下一步便是设置时间了,设置时间实际上就是设置RTC 的计数值,时间与计数值之间是需要换算的。库函数中设置RTC 计数值的方法是:

void RTC_SetCounter(uint32_t CounterValue)//最后在配置完成之后

通过这个函数直接设置RTC 计数值。
6)更新配置,设置RTC 中断分组。
在设置完时钟之后,我们将配置更新同时退出配置模式,这里还是通过RTC_CRH 的CNF
来实现。库函数的方法是:

RTC_ExitConfigMode();//退出配置模式,更新配置

在退出配置模式更新配置之后我们在备份区域BKP_DR1 中写入0X5050 代表我们已经初始化过时钟了,下次开机(或复位)的时候,先读取BKP_DR1 的值,然后判断是否是0X5050 来决定是不是要配置。接着我们配置RTC 的秒钟中断,并进行分组。
往备份区域写用户数据的函数是:

void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data)

这个函数的第一个参数就是寄存器的标号了,这个是通过宏定义定义的。比如我们要往
BKP_DR1 写入0x5050,方法是:

BKP_WriteBackupRegister(BKP_DR1, 0X5050);

同时,有写便有读,读取备份区域指定寄存器的用户数据的函数是:

uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR)

这个函数就很好理解了,这里不做过多讲解。
设置中断分组的方法之前已经详细讲解过,调用NVIC_Init 函数即可,这里不做重复讲解。
7)编写中断服务函数。
最后,我们要编写中断服务函数,在秒钟中断产生的时候,读取当前的时间值,并显示到
TFTLCD 模块上。
通过以上几个步骤,我们就完成了对RTC 的配置,并通过秒钟中断来更新时间。接下来我
们将进行下一步的工作。

20.2 硬件设计

本实验用到的硬件资源有:

  1. 指示灯DS0
  2. 串口
  3. TFTLCD 模块
  4. RTC

前面3 个都介绍过了,而RTC 属于STM32 内部资源,其配置也是通过软件设置好就可以
了。不过RTC 不能断电,否则数据就丢失了,我们如果想让时间在断电后还可以继续走,那么必须确保开发板的电池有电(ALIENTEK 战舰STM32 开发板标配是有电池的)。

20.3 软件设计

同样,打开我们光盘的RTC 时钟实验,可以看到,我们的工程中加入了rtc.c 源文件和rtc.h
头文件,同时,引入了stm32f10x_rtc.c 和stm32f10x_bkp.c 库文件。
由于篇幅所限,rtc.c 中的代码,我们不全部贴出了,这里针对几个重要的函数,进行简要
说明,首先是RTC_Init,其代码如下:

//实时时钟配置
//初始化RTC 时钟,同时检测时钟是否工作正常
//BKP->DR1 用于保存是否第一次配置的设置
//返回0:正常
//其他:错误代码
u8 RTC_Init(void)
{
        u8 temp=0;
        //检查是不是第一次配置时钟
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR |
                        RCC_APB1Periph_BKP, ENABLE); //①使能PWR 和BKP 外设时钟
        PWR_BackupAccessCmd(ENABLE); //②使能后备寄存器访问
        if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050) //从指定的后备寄存器中
                //读出数据:读出了与写入的指定数据不相乎
        {
                BKP_DeInit(); //③复位备份区域
                RCC_LSEConfig(RCC_LSE_ON); //设置外部低速晶振(LSE)
                while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET&&temp<250)
                        //检查指定的RCC 标志位设置与否,等待低速晶振就绪
                {
                        temp++;
                        delay_ms(10);
                }
                if(temp>=250)return 1;//初始化时钟失败,晶振有问题!
                
                RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //设置RTC 时钟
                //(RTCCLK),选择LSE 外部低速时钟 作为RTC时钟,见下图所示
                RCC_RTCCLKCmd(ENABLE); //使能RTC 时钟
                RTC_WaitForLastTask(); //等待最近一次对RTC 寄存器的写操作完成
                RTC_WaitForSynchro(); //等待RTC 寄存器同步
                RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC 秒中断
                RTC_WaitForLastTask(); //等待最近一次对RTC 寄存器的写操作完成
                
                RTC_EnterConfigMode(); // 允许配置
                RTC_SetPrescaler(32767); //设置RTC 预分频的值
                RTC_WaitForLastTask(); //等待最近一次对RTC 寄存器的写操作完成
                RTC_Set(2015,1,14,17,42,55); //设置时间 下面介绍
                RTC_ExitConfigMode(); //退出配置模式
                BKP_WriteBackupRegister(BKP_DR1, 0X5050); //向指定的后备寄存器中
                //写入用户程序数据0x5050
        }
        else//系统继续计时
        {
                RTC_WaitForSynchro(); //等待最近一次对RTC 寄存器的写操作完成
                RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能RTC 秒中断
                RTC_WaitForLastTask(); //等待最近一次对RTC 寄存器的写操作完成
        }
        RTC_NVIC_Config(); //RCT 中断分组设置
        RTC_Get(); //更新时间  下面介绍
        return 0; //ok
}

该函数用来初始化RTC 时钟,但是只在第一次的时候设置时间,以后如果重新上电/复位
都不会再进行时间设置了(前提是备份电池有电),在第一次配置的时候,我们是按照上面介绍的RTC 初始化步骤来做的,这里就不在多说了,这里我们设置时间是通过时间设置函数RTC_Set函数来实现的,该函数将在后续进行介绍。这里我们默认将时间设置为2015 年1 月14 日,17点42 分55 秒。在设置好时间之后,我们通过BKP_WriteBackupRegister()函数向BKP->DR1 写入标志字0X5050(这个值自己随便定的),用于标记时间已经被设置了。这样,再次发生复位的时候,该函数通过BKP_ReadBackupRegister()读取BKP->DR1 的值,来判断决定是不是需要重新设置时间,如果不需要设置,则跳过时间设置,仅仅使能秒钟中断一下,就进行中断分组,然后返回了。这样不会重复设置时间,使得我们设置的时间不会因复位或者断电而丢失。
该函数还有返回值,返回值代表此次操作的成功与否,如果返回0,则代表初始化RTC 成
功,如果返回值非零则代表错误代码了。

这里是引用

介绍完RTC_Init,我们来介绍一下RTC_Set 函数,该函数代码如下:

//设置时钟
//把输入的时钟转换为秒钟
//以1970 年1 月1 日为基准
//1970~2099 年为合法年份
//返回值:0,成功;其他:错误代码.
//月份数据表
u8 const table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5}; //月修正数据表
//平年的月份日期表
const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};
u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
{
        u16 t;
        u32 seccount=0;
        if(syear<1970||syear>2099)return 1;
        for(t=1970;t<syear;t++) //把所有年份的秒钟相加
        { if(Is_Leap_Year(t))seccount+=31622400;//闰年的秒钟数
                else seccount+=31536000; //平年的秒钟数
        }
        smon-=1;
        for(t=0;t<smon;t++) //把前面月份的秒钟数相加
        { seccount+=(u32)mon_table[t]*86400; //月份秒钟数相加
                if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年2 月份增加一天的秒钟数
        }
        seccount+=(u32)(sday-1)*86400; //把前面日期的秒钟数相加
        seccount+=(u32)hour*3600; //小时秒钟数
        seccount+=(u32)min*60; //分钟秒钟数
        seccount+=sec; //最后的秒钟加上去
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR |
                        RCC_APB1Periph_BKP, ENABLE); //使能PWR 和BKP 外设时钟
        PWR_BackupAccessCmd(ENABLE); //使能RTC 和后备寄存器访问
        RTC_SetCounter(seccount); //设置RTC 计数器的值
        RTC_WaitForLastTask(); //等待最近一次对RTC 寄存器的写操作完成
        return 0;
}

该函数用于设置时间,把我们输入的时间,转换为以1970 年1 月1 日0 时0 分0 秒当做起
始时间的秒钟信号,后续的计算都以这个时间为基准的,由于STM32 的秒钟计数器可以保存136 年的秒钟数据,这样我们可以计时到2106 年。

接着,我们介绍RTC_Alarm_Set 函数,该函数用于设置闹钟时间,同RTC_Set 函数几乎一
模一样,主要区别,就是将调用RTC_SetCounter 函数换成了调用RTC_SetAlarm 函数,用于设置闹钟时间,具体代码请参考本例程源码。

接着,我们介绍一下RTC_Get 函数,该函数用于获取时间和日期等数据,其代码如下:

//得到当前的时间,结果保存在calendar 结构体里面
//返回值:0,成功;其他:错误代码.
u8 RTC_Get(void)
{ static u16 daycnt=0;
        u32 timecount=0;
        u32 temp=0;
        u16 temp1=0;
        timecount=RTC->CNTH; //得到计数器中的值(秒钟数)
        timecount<<=16;
        timecount+=RTC->CNTL;
        temp=timecount/86400; //得到天数(秒钟数对应的)
        if(daycnt!=temp) //超过一天了
        {
                daycnt=temp;
                temp1=1970; //从1970 年开始
                while(temp>=365)
                {
                        if(Is_Leap_Year(temp1)) //是闰年
                        {
                                if(temp>=366)temp-=366; //闰年的秒钟数
                                else break;
                        }
                        else temp-=365; //平年
                        temp1++;
                }
                calendar.w_year=temp1; //得到年份
                temp1=0;
                while(temp>=28) //超过了一个月
                {
                        if(Is_Leap_Year(calendar.w_year)&&temp1==1)//当年是不是闰年/2 月份
                        {
                                if(temp>=29)temp-=29;//闰年的秒钟数
                                else break;
                        }
                        else
                        { if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
                                else break;
                        }
                        temp1++;
                }
                calendar.w_month=temp1+1; //得到月份
                calendar.w_date=temp+1; //得到日期
        }
        temp=timecount%86400; //得到秒钟数
        calendar.hour=temp/3600; //小时
        calendar.min=(temp%3600)/60; //分钟
        calendar.sec=(temp%3600)%60; //秒钟
        calendar.week=RTC_Get_Week(calendar.w_year,calendar.w_month,calendar.w_date);
        //获取星期
        return 0;
}                          

函数其实就是将存储在秒钟寄存器RTC->CNTH 和RTC->CNTL 中的秒钟数据(通过函数
RTC_SetCounter 设置)转换为真正的时间和日期。该代码还用到了一个calendar 的结构体,calendar 是我们在rtc.h 里面将要定义的一个时间结构体,用来存放时钟的年月日时分秒等信息。
因为STM32 的RTC 只有秒钟计数器,而年月日,时分秒这些需要我们自己软件计算。我们把计算好的值保存在calendar 里面,方便其他程序调用。

最后,我们介绍一下秒钟中断服务函数,该函数代码如下:

//RTC 时钟中断
//每秒触发一次
void RTC_IRQHandler(void)
{
        if (RTC_GetITStatus(RTC_IT_SEC) != RESET) //秒钟中断
        {
                RTC_Get(); //更新时间
        }
        if(RTC_GetITStatus(RTC_IT_ALR)!= RESET) //闹钟中断
        {
                RTC_ClearITPendingBit(RTC_IT_ALR); //清闹钟中断
                RTC_Get(); //更新时间
                printf("Alarm Time:%d-%d-%d %d:%d:%d\n",calendar.w_year,calendar.w_month,
                                calendar.w_date,calendar.hour,calendar.min,calendar.sec);//输出闹铃时间
        }
        RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW); //清闹钟中断
        RTC_WaitForLastTask();
}      

此部分代码比较简单,我们通过RTC_GetITStatus 来判断发生的是何种中断,如果是秒钟
中断,则执行一次时间的计算,获得最新时间,结果保存在calendar 结构体里面,因此,我们可以在calendar 里面读到最新的时间、日期等信息。如果是闹钟中断,则更新时间后,将当前的闹铃时间通过printf 打印出来,可以在串口调试助手看到当前的闹铃情况。

rtc.c 的其他程序,这里就不再介绍了,请大家直接看光盘的源码。接下来看看rtc.h 代码,
在rtc.h 中,我们定义了一个结构体:

typedef struct
{
	vu8 hour;
	vu8 min;
	vu8 sec;
	//公历日月年周
	vu16 w_year;
	vu8 w_month;
	vu8 w_date;
	vu8 week;
}_calendar_obj;

从上面结构体定义可以看到_calendar_obj 结构体所包含的成员变量是一个完整的公历信
息,包括年、月、日、周、时、分、秒等7 个元素。我们以后要知道当前时间,只需要通过RTC_Get函数,执行时钟转换,然后就可以从calendar 里面读出当前的公历时间了。

最后看看main.c 里面的代码如下:

int main(void)
{
        u8 t=0;
        delay_init(); //延时函数初始化
        NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置NVIC 中断分组2
        uart_init(115200); //串口初始化波特率为115200
        LED_Init(); //LED 端口初始化
        LCD_Init(); //LCD 初始化
        usmart_dev.init(72); //初始化USMART
        POINT_COLOR=RED; //设置字体为红色
        LCD_ShowString(30,50,200,16,16,"WarShip STM32F103 ^_^");
        LCD_ShowString(30,70,200,16,16,"RTC TEST");
        LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
        LCD_ShowString(30,110,200,16,16,"2015/1/14");
        while(RTC_Init()) //RTC 初始化,一定要初始化成功
        { LCD_ShowString(60,130,200,16,16,"RTC ERROR! ");
                delay_ms(800);
                LCD_ShowString(60,130,200,16,16,"RTC Trying...");
        }
        //显示时间
        POINT_COLOR=BLUE; //设置字体为蓝色
        LCD_ShowString(60,130,200,16,16," - - ");
        LCD_ShowString(60,162,200,16,16," : : ");
        while(1)
        {
                if(t!=calendar.sec)
                {
                        t=calendar.sec;
                        LCD_ShowNum(60,130,calendar.w_year,4,16);
                        LCD_ShowNum(100,130,calendar.w_month,2,16);
                        LCD_ShowNum(124,130,calendar.w_date,2,16);
                        switch(calendar.week)
                        {
                                case 0:LCD_ShowString(60,148,200,16,16,"Sunday ");
                                       break;
                                case 1:LCD_ShowString(60,148,200,16,16,"Monday ");
                                       break;
                                case 2:LCD_ShowString(60,148,200,16,16,"Tuesday ");
                                       break;
                                case 3:LCD_ShowString(60,148,200,16,16,"Wednesday");
                                       break;
                                case 4:LCD_ShowString(60,148,200,16,16,"Thursday ");
                                       break;
                                case 5:LCD_ShowString(60,148,200,16,16,"Friday ");
                                       break;
                                case 6:LCD_ShowString(60,148,200,16,16,"Saturday ");
                                       break;
                        }
                        LCD_ShowNum(60,162,calendar.hour,2,16);
                        LCD_ShowNum(84,162,calendar.min,2,16);
                        LCD_ShowNum(108,162,calendar.sec,2,16);
                        LED0=!LED0;
                }
                delay_ms(10);
        };
}

这部分代码就不再需要详细解释了,在包含了rtc.h 之后,通过判断calendar.sec 是否改变
来决定要不要更新时间显示。同时我们设置LED0 每2 秒钟闪烁一次,用来提示程序已经开始跑了。

为了方便设置时间,我们在usmart_config.c 里面,修改usmart_nametab 如下:

struct _m_usmart_nametab usmart_nametab[]=
{
#if USMART_USE_WRFUNS==1 //如果使能了读写操作
	(void*)read_addr,"u32 read_addr(u32 addr)",
	(void*)write_addr,"void write_addr(u32 addr,u32 val)",
#endif
	(void*)delay_ms,"void delay_ms(u16 nms)",
	(void*)delay_us,"void delay_us(u32 nus)",
	(void*)RTC_Set,"u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)",
};

将RTC_Set 加入了usmart,同时去掉了上一章的设置(减少代码量),这样通过串口就可
以直接设置RTC 时间了。
至此,RTC 实时时钟的软件设计就完成了,接下来就让我们来检验一下,我们的程序是否
正确了。

20.4 下载验证

将程序下载到战舰STM32 后,可以看到DS0 不停的闪烁,提示程序已经在运行了。同时
可以看到TFTLCD 模块开始显示时间,实际显示效果如图20.4.1 所示:

在这里插入图片描述

如果时间不正确,大家可以用上一章介绍的方法,通过串口调用RTC_Set 来设置一下当前
时间。

待机睡眠唤醒(低功耗)实验

本章我们将向大家介绍STM32F1 的待机唤醒功能。在本章中,我们将利用WK_UP 按键
来实现唤醒和进入待机模式的功能,然后利用DS0 指示状态。

21.1 STM32 待机模式简介

很多单片机都有低功耗模式,STM32 也不例外。在系统或电源复位以后,微控制器处于运
行状态。运行状态下的HCLK 为CPU 提供时钟,内核执行程序代码。当CPU 不需继续运行时,可以利用多个低功耗模式来节省功耗,例如等待某个外部事件时。用户需要根据最低电源消耗,最快速启动时间和可用的唤醒源等条件,选定一个最佳的低功耗模式。STM32 的3 种低功耗模式我们在5.2.4 节有粗略介绍,这里我们再回顾一下。

STM32 的低功耗模式有3 种:

  • 1)睡眠模式(CM3 内核停止,外设仍然运行)
  • 2)停止模式(所有时钟都停止)
  • 3)待机模式(1.8V 内核电源关闭)

在这里插入图片描述

在运行模式下,我们也可以通过降低系统时钟关闭APB 和AHB 总线上未被使用的外设的
时钟来降低功耗。三种低功耗模式一览表见表21.1.1 所示:

在这里插入图片描述

在这三种低功耗模式中,最低功耗的是待机模式,在此模式下,最低只需要2uA左右的电
流。停机模式是次低功耗的,其典型的电流消耗在20uA左右。最后就是睡眠模式了。用户可以根据自己的需求来决定使用哪种低功耗模式。

本章,我们仅对STM32 的最低功耗模式-待机模式,来做介绍。待机模式可实现STM32
的最低功耗。该模式是在CM3 深睡眠模式时关闭电压调节器。整个1.8V 供电区域被断电。PLL、HSI 和HSE 振荡器也被断电。SRAM 和寄存器内容丢失。仅备份的寄存器和待机电路维持供电。

那么我们如何进入待机模式呢?其实很简单,只要按图21.1.1 所示的步骤执行就可以了:

在这里插入图片描述

从图21.1.1 可知,我们有4 种方式可以退出待机模式,即当一个外部复位(NRST 引脚)、IWDG 复位、WKUP 引脚上的上升沿或RTC 闹钟事件发生时,微控制器从待机模式退出。从待机唤醒后,除了电源控制/状态寄存器(PWR_CSR),所有寄存器被复位。

从待机模式唤醒后的代码执行等同于复位后的执行(采样启动模式引脚,读取复位向量等)。
电源控制/状态寄存器(PWR_CSR)将会指示内核由待机状态退出。

在进入待机模式后,除了复位引脚以及被设置为防侵入或校准输出时的TAMPER 引脚和被
使能的唤醒引脚(WK_UP 脚),其他的IO 引脚都将处于高阻态。

图21.1.1 已经清楚的说明了进入待机模式的通用步骤,其中涉及到2 个寄存器,即电源控
制寄存器(PWR_CR)和电源控制/状态寄存器(PWR_CSR)。下面我们介绍一下这两个寄存器:

电源控制寄存器(PWR_CR),该寄存器的各位描述如图21.1.2 所示:

在这里插入图片描述

这里我们通过设置PWR_CR 的PDDS 位,使CPU 进入深度睡眠时进入待机模式,同时我
们通过CWUF 位,清除之前的唤醒位。电源控制/状态寄存器(PWR_CSR)的各位描述如图21.1.3所示:

在这里插入图片描述

这里,我们通过设置PWR_CSR 的EWUP 位,来使能WKUP 引脚用于待机模式唤醒。我
们还可以从WUF 来检查是否发生了唤醒事件。不过本章我们并没有用到。
通过以上介绍,我们了解了进入待机模式的方法,以及设置WK_UP 引脚用于把STM32

从待机模式唤醒的方法。具体步骤如下:
1)使能电源时钟。
因为要配置电源控制寄存器,所以必须先使能电源时钟。
在库函数中,使能电源时钟的方法是:

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //使能PWR 外设时钟

这个函数非常容易理解。
2) 设置WK_UP 引脚作为唤醒源。
使能时钟之后后再设置PWR_CSR 的EWUP 位,使能WK_UP 用于将CPU 从待机模式唤
醒。在库函数中,设置使能WK_UP 用于唤醒CPU 待机模式的函数是:

PWR_WakeUpPinCmd(ENABLE); //使能唤醒管脚功能

3)设置SLEEPDEEP 位,设置PDDS 位,执行WFI 指令,进入待机模式。
进入待机模式,首先要设置SLEEPDEEP 位(该位在系统控制寄存器(SCB_SCR)的第
二位,详见《CM3 权威指南》,第182 页表13.1),接着我们通过PWR_CR 设置PDDS 位,使得CPU 进入深度睡眠时进入待机模式,最后执行WFI 指令开始进入待机模式,并等待WK_UP中断的到来。在库函数中,进行上面三个功能进入待机模式是在函数PWR_EnterSTANDBYMode中实现的(一个库函数搞定):

void PWR_EnterSTANDBYMode(void)

4)最后编写WK_UP 中断函数。
因为我们通过WK_UP 中断(PA0 中断)来唤醒CPU,所以我们有必要设置一下该中断函
数,同时我们也通过该函数里面进入待机模式。

通过以上几个步骤的设置,我们就可以使用STM32 的待机模式了,并且可以通过WK_UP
来唤醒CPU,我们最终要实现这样一个功能:通过长按(3 秒)WK_UP 按键开机,并且通过DS0 的闪烁指示程序已经开始运行,再次长按该键,则进入待机模式,DS0 关闭,程序停止运行。类似于手机的开关机
在这里插入图片描述

21.2 硬件设计

本实验用到的硬件资源有:

  1. 指示灯DS0
  2. WK_UP 按键

本章,我们使用了WK_UP 按键用于唤醒和进入待机模式。然后通过DS0 来指示程序是否
在运行。这两个硬件的连接前面均有介绍。

21.3 软件设计

打开待机唤醒实验工程,我们可以发现工程中多了一个wkup.c 和wkup.h 文件,相关的用
户代码写在这两个文件中。同时,对于待机唤醒功能,我们需要引入stm32f10x_pwr.c 和
stm32f0x_pwr.h 文件。

打开wkup.c,可以看到如下关键代码:

void Sys_Standby(void)
{
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //使能PWR 外设时钟
        PWR_WakeUpPinCmd(ENABLE); //使能唤醒管脚功能
        PWR_EnterSTANDBYMode(); //进入待命(STANDBY)模式
}
//系统进入待机模式
void Sys_Enter_Standby(void)
{
        RCC_APB2PeriphResetCmd(0X01FC,DISABLE); //复位所有IO 口
        Sys_Standby();
}

//检测WKUP 脚的信号
//返回值1:连续按下3s 以上
// 0:错误的触发
u8 Check_WKUP(void)
{
        u8 t=0; //记录按下的时间
        LED0=0; //亮灯DS0
        while(1)
        {
                if(WKUP_KD)
                {
                        t++; //已经按下了
                        delay_ms(30);
                        if(t>=100) //按下超过3 秒钟
                        {
                                LED0=0; //点亮DS0
                                return 1; //按下3s 以上了
                        }
                }else
                {
                        LED0=1;
                        return 0; //按下不足3 秒
                }
        }
}

//中断,检测到PA0 脚的一个上升沿.
//中断线0 线上的中断检测
void EXTI0_IRQHandler(void)
{
        EXTI_ClearITPendingBit(EXTI_Line0); // 清除LINE10 上的中断标志位
        if(Check_WKUP()) //关机?
        {
                Sys_Enter_Standby();
        }
}

//PA0 WKUP 唤醒初始化
void WKUP_Init(void)
{ GPIO_InitTypeDef GPIO_InitStructure;
        NVIC_InitTypeDef NVIC_InitStructure;
        EXTI_InitTypeDef EXTI_InitStructure;
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |
                        RCC_APB2Periph_AFIO, ENABLE); //使能GPIOA 和复用功能时钟
        GPIO_InitStructure.GPIO_Pin =GPIO_Pin_0; //PA.0
        GPIO_InitStructure.GPIO_Mode =GPIO_Mode_IPD; //上拉输入
        GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化IO
        //使用外部中断方式
        GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
        //中断线0 连接GPIOA.0
        EXTI_InitStructure.EXTI_Line = EXTI_Line0; //设置按键所有的外部线路
        EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //外部中断模式
        EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; //上升沿触发
        EXTI_InitStructure.EXTI_LineCmd = ENABLE;
        EXTI_Init(&EXTI_InitStructure); // 初始化外部中断
        NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //使能外部中断通道
        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //先占优先级2 级
        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //从优先级2 级
        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //外部中断通道使能
        NVIC_Init(&NVIC_InitStructure); //初始化NVIC
        
        if(Check_WKUP()==0) Sys_Standby(); //不是开机,进入待机模式
}

注意,这个项目中不要同时引用exit.c 文件,因为exit.c 里面也有一个void
EXTI0_IRQHandler(void)函数,如果不删除,MDK 就会报错。该部分代码比较简单,我们在这里说明两点:

1,在void Sys_Enter_Standby(void)函数里面,我们要在进入待机模式前把所有开启的外设
全部关闭,我们这里仅仅复位了所有的IO 口,使得IO 口全部为浮空输入。其他外设(比如
ADC 等),大家根据自己所开启的情况进行一一关闭就可,这样才能达到最低功耗!
2,在void WKUP_Init(void)函数里面,我们要先判断WK_UP 是否按下了3 秒钟,来决定
要不要开机,如果没有按下3 秒钟,程序直接就进入了待机模式。所以在下载完代码的时候,是看不到任何反应的。我们必须先按WK_UP 按键3 秒钟以开机,才能看到DS0 闪烁。
wkup.h 头文件的代码非常简单,这里我们就不列出来。最后我们看看main.c 里面main 函
数代码如下:

int main(void)
{
	delay_init(); //延时函数初始化
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC 中断分组2
	uart_init(115200); //串口初始化波特率为115200
	LED_Init(); //LED 端口初始化
	WKUP_Init(); //待机唤醒初始化
	LCD_Init(); //LCD 初始化
	POINT_COLOR=RED;
	LCD_ShowString(30,50,200,16,16,"Warship STM32");
	LCD_ShowString(30,70,200,16,16,"WKUP TEST");
	LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
	LCD_ShowString(30,110,200,16,16,"2014/1/14");
	while(1)
	{ LED0=!LED0;
		delay_ms(250);
	}
}

这里我们先初始化LED 和WK_UP 按键(通过WKUP_Init()函数初始化),如果检测到
有长按WK_UP 按键3 秒以上,则开机,并执行LCD 初始化,在LCD 上面显示一些内容,如
果没有长按,则在WKUP_Init 里面,调用Sys_Enter_Standby 函数,直接进入待机模式了。

开机后,在死循环里面等待WK_UP 中断的到来,在得到中断后,在中断函数里面判断
WK_UP 按下的时间长短,来决定是否进入待机模式,如果按下时间超过3 秒,则进入待机,否则退出中断,继续执行main 函数的死循环等待,同时不停的取反LED0,让红灯闪烁。

代码部分就介绍到这里,大家记住下载代码后,一定要长按WK_UP 按键,来开机,否则
将直接进入待机模式,无任何现象。

21.4 下载与测试

在代码编译成功之后,下载代码到战舰STM32 V3 上,此时,看不到任何现象,和没下载
代码一样,其实这是正常的,在程序下载完之后,开发板检测不到WK_UP(即WK_UP 按键)的持续按下(3 秒以上),所以直接进入待机模式,看起来和没有下载代码一样。然后,我们长按WK_UP 按键3 秒钟左右(WK_UP 按下时,DS0 会长亮),可以看到DS0 开始闪烁,液晶也会显示一些内容。然后再长按WK_UP,DS0 会灭掉,液晶灭掉,程序再次进入待机模式。

2.4G无线射频通信模块

NRF24L01简介

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

寄存器介绍

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

硬件连接

在这里插入图片描述
在这里插入图片描述

源码讲解

在这里插入图片描述
24101.h

#ifndef __24L01_H
#define __24L01_H	 		  
#include "sys.h"   
//	 
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK战舰STM32开发板V3
//NRF24L01驱动代码	   
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//创建日期:2015/1/17
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2009-2019
//All rights reserved									  
//
    
//
//NRF24L01寄存器操作命令
#define NRF_READ_REG    0x00  //读配置寄存器,低5位为寄存器地址
#define NRF_WRITE_REG   0x20  //写配置寄存器,低5位为寄存器地址
#define RD_RX_PLOAD     0x61  //读RX有效数据,1~32字节
#define WR_TX_PLOAD     0xA0  //写TX有效数据,1~32字节
#define FLUSH_TX        0xE1  //清除TX FIFO寄存器.发射模式下用
#define FLUSH_RX        0xE2  //清除RX FIFO寄存器.接收模式下用
#define REUSE_TX_PL     0xE3  //重新使用上一包数据,CE为高,数据包被不断发送.
#define NOP             0xFF  //空操作,可以用来读状态寄存器	 
//SPI(NRF24L01)寄存器地址
#define CONFIG          0x00  //配置寄存器地址;bit0:1接收模式,0发射模式;bit1:电选择;bit2:CRC模式;bit3:CRC使能;
                              //bit4:中断MAX_RT(达到最大重发次数中断)使能;bit5:中断TX_DS使能;bit6:中断RX_DR使能
#define EN_AA           0x01  //使能自动应答功能  bit0~5,对应通道0~5
#define EN_RXADDR       0x02  //接收地址允许,bit0~5,对应通道0~5
#define SETUP_AW        0x03  //设置地址宽度(所有数据通道):bit1,0:00,3字节;01,4字节;02,5字节;
#define SETUP_RETR      0x04  //建立自动重发;bit3:0,自动重发计数器;bit7:4,自动重发延时 250*x+86us
#define RF_CH           0x05  //RF通道,bit6:0,工作通道频率;
#define RF_SETUP        0x06  //RF寄存器;bit3:传输速率(0:1Mbps,1:2Mbps);bit2:1,发射功率;bit0:低噪声放大器增益
#define STATUS          0x07  //状态寄存器;bit0:TX FIFO满标志;bit3:1,接收数据通道号(最大:6);bit4,达到最多次重发
                              //bit5:数据发送完成中断;bit6:接收数据中断;
#define MAX_TX  		0x10  //达到最大发送次数中断
#define TX_OK   		0x20  //TX发送完成中断
#define RX_OK   		0x40  //接收到数据中断

#define OBSERVE_TX      0x08  //发送检测寄存器,bit7:4,数据包丢失计数器;bit3:0,重发计数器
#define CD              0x09  //载波检测寄存器,bit0,载波检测;
#define RX_ADDR_P0      0x0A  //数据通道0接收地址,最大长度5个字节,低字节在前
#define RX_ADDR_P1      0x0B  //数据通道1接收地址,最大长度5个字节,低字节在前
#define RX_ADDR_P2      0x0C  //数据通道2接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等;
#define RX_ADDR_P3      0x0D  //数据通道3接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等;
#define RX_ADDR_P4      0x0E  //数据通道4接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等;
#define RX_ADDR_P5      0x0F  //数据通道5接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等;
#define TX_ADDR         0x10  //发送地址(低字节在前),ShockBurstTM模式下,RX_ADDR_P0与此地址相等
#define RX_PW_P0        0x11  //接收数据通道0有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P1        0x12  //接收数据通道1有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P2        0x13  //接收数据通道2有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P3        0x14  //接收数据通道3有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P4        0x15  //接收数据通道4有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P5        0x16  //接收数据通道5有效数据宽度(1~32字节),设置为0则非法
#define NRF_FIFO_STATUS 0x17  //FIFO状态寄存器;bit0,RX FIFO寄存器空标志;bit1,RX FIFO满标志;bit2,3,保留
                              //bit4,TX FIFO空标志;bit5,TX FIFO满标志;bit6,1,循环发送上一数据包.0,不循环;
//
//24L01操作线
#define NRF24L01_CE   PGout(8) //24L01片选信号
#define NRF24L01_CSN  PGout(7) //SPI片选信号	   
#define NRF24L01_IRQ  PGin(6)  //IRQ主机数据输入
//24L01发送接收数据宽度定义
#define TX_ADR_WIDTH    5   	//5字节的地址宽度
#define RX_ADR_WIDTH    5   	//5字节的地址宽度
#define TX_PLOAD_WIDTH  32  	//32字节的用户数据宽度
#define RX_PLOAD_WIDTH  32  	//32字节的用户数据宽度
									   	   

void NRF24L01_Init(void);						//初始化
void NRF24L01_RX_Mode(void);					//配置为接收模式
void NRF24L01_TX_Mode(void);					//配置为发送模式
u8 NRF24L01_Write_Buf(u8 reg, u8 *pBuf, u8 u8s);//写数据区
u8 NRF24L01_Read_Buf(u8 reg, u8 *pBuf, u8 u8s);	//读数据区		  
u8 NRF24L01_Read_Reg(u8 reg);					//读寄存器
u8 NRF24L01_Write_Reg(u8 reg, u8 value);		//写寄存器
u8 NRF24L01_Check(void);						//检查24L01是否存在
u8 NRF24L01_TxPacket(u8 *txbuf);				//发送一个包的数据
u8 NRF24L01_RxPacket(u8 *rxbuf);				//接收一个包的数据
#endif

24101.c

#include "24l01.h"
#include "lcd.h"
#include "delay.h"
#include "spi.h"
#include "usart.h"
//	 
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK战舰STM32开发板
//NRF24L01驱动代码	   
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//修改日期:2012/9/13
//版本:V1.0
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2009-2019
//All rights reserved									  
//
    
const u8 TX_ADDRESS[TX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //发送地址
const u8 RX_ADDRESS[RX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01};

//初始化24L01的IO口
void NRF24L01_Init(void)
{ 	
	GPIO_InitTypeDef GPIO_InitStructure;
    SPI_InitTypeDef  SPI_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOG, ENABLE);	 //使能PB,G端口时钟
    	
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;				 //PB12上拉 防止W25X的干扰
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
 	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 	GPIO_Init(GPIOB, &GPIO_InitStructure);	//初始化指定IO
 	GPIO_SetBits(GPIOB,GPIO_Pin_12);//上拉				
 	

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7|GPIO_Pin_8;	//PG8 7 推挽 	  
 	GPIO_Init(GPIOG, &GPIO_InitStructure);//初始化指定IO
  
	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_6;   
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PG6 输入  
	GPIO_Init(GPIOG, &GPIO_InitStructure);

	GPIO_ResetBits(GPIOG,GPIO_Pin_6|GPIO_Pin_7|GPIO_Pin_8);//PG6,7,8上拉					 
		 
    SPI2_Init();    		//初始化SPI	 
 
	SPI_Cmd(SPI2, DISABLE); // SPI外设不使能

	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //SPI设置为双线双向全双工
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;		//SPI主机
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;		//发送接收8位帧结构
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;		//时钟悬空低
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;	//数据捕获于第1个时钟沿
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;		//NSS信号由软件控制
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;		//定义波特率预分频的值:波特率预分频值为16
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;	//数据传输从MSB位开始
	SPI_InitStructure.SPI_CRCPolynomial = 7;	//CRC值计算的多项式
	SPI_Init(SPI2, &SPI_InitStructure);  //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
 
	SPI_Cmd(SPI2, ENABLE); //使能SPI外设
			 
	NRF24L01_CE=0; 			//使能24L01
	NRF24L01_CSN=1;			//SPI片选取消  
	 		 	 
}
//检测24L01是否存在
//返回值:0,成功;1,失败	
u8 NRF24L01_Check(void)
{
	u8 buf[5]={0XA5,0XA5,0XA5,0XA5,0XA5};
	u8 i;
	SPI2_SetSpeed(SPI_BaudRatePrescaler_4); //spi速度为9Mhz(24L01的最大SPI时钟为10Mhz)   	 
	NRF24L01_Write_Buf(NRF_WRITE_REG+TX_ADDR,buf,5);//写入5个字节的地址.	
	NRF24L01_Read_Buf(TX_ADDR,buf,5); //读出写入的地址  
	for(i=0;i<5;i++)if(buf[i]!=0XA5)break;	 							   
	if(i!=5)return 1;//检测24L01错误	
	return 0;		 //检测到24L01
}	 	 
//SPI写寄存器
//reg:指定寄存器地址
//value:写入的值
u8 NRF24L01_Write_Reg(u8 reg,u8 value)
{
	u8 status;	
   	NRF24L01_CSN=0;                 //使能SPI传输
  	status =SPI2_ReadWriteByte(reg);//发送寄存器号 
  	SPI2_ReadWriteByte(value);      //写入寄存器的值
  	NRF24L01_CSN=1;                 //禁止SPI传输	   
  	return(status);       			//返回状态值
}
//读取SPI寄存器值
//reg:要读的寄存器
u8 NRF24L01_Read_Reg(u8 reg)
{
	u8 reg_val;	    
 	NRF24L01_CSN = 0;          //使能SPI传输		
  	SPI2_ReadWriteByte(reg);   //发送寄存器号
  	reg_val=SPI2_ReadWriteByte(0XFF);//读取寄存器内容
  	NRF24L01_CSN = 1;          //禁止SPI传输		    
  	return(reg_val);           //返回状态值
}	
//在指定位置读出指定长度的数据
//reg:寄存器(位置)
//*pBuf:数据指针
//len:数据长度
//返回值,此次读到的状态寄存器值 
u8 NRF24L01_Read_Buf(u8 reg,u8 *pBuf,u8 len)
{
	u8 status,u8_ctr;	       
  	NRF24L01_CSN = 0;           //使能SPI传输
  	status=SPI2_ReadWriteByte(reg);//发送寄存器值(位置),并读取状态值   	   
 	for(u8_ctr=0;u8_ctr<len;u8_ctr++)pBuf[u8_ctr]=SPI2_ReadWriteByte(0XFF);//读出数据
  	NRF24L01_CSN=1;       //关闭SPI传输
  	return status;        //返回读到的状态值
}
//在指定位置写指定长度的数据
//reg:寄存器(位置)
//*pBuf:数据指针
//len:数据长度
//返回值,此次读到的状态寄存器值
u8 NRF24L01_Write_Buf(u8 reg, u8 *pBuf, u8 len)
{
	u8 status,u8_ctr;	    
 	NRF24L01_CSN = 0;          //使能SPI传输
  	status = SPI2_ReadWriteByte(reg);//发送寄存器值(位置),并读取状态值
  	for(u8_ctr=0; u8_ctr<len; u8_ctr++)SPI2_ReadWriteByte(*pBuf++); //写入数据	 
  	NRF24L01_CSN = 1;       //关闭SPI传输
  	return status;          //返回读到的状态值
}				   
//启动NRF24L01发送一次数据
//txbuf:待发送数据首地址
//返回值:发送完成状况
u8 NRF24L01_TxPacket(u8 *txbuf)
{
	u8 sta;
 	SPI2_SetSpeed(SPI_BaudRatePrescaler_8);//spi速度为9Mhz(24L01的最大SPI时钟为10Mhz)   
	NRF24L01_CE=0;
  	NRF24L01_Write_Buf(WR_TX_PLOAD,txbuf,TX_PLOAD_WIDTH);//写数据到TX BUF  32个字节
 	NRF24L01_CE=1;//启动发送	   
	while(NRF24L01_IRQ!=0);//等待发送完成
	sta=NRF24L01_Read_Reg(STATUS);  //读取状态寄存器的值	   
	NRF24L01_Write_Reg(NRF_WRITE_REG+STATUS,sta); //清除TX_DS或MAX_RT中断标志
	if(sta&MAX_TX)//达到最大重发次数
	{
		NRF24L01_Write_Reg(FLUSH_TX,0xff);//清除TX FIFO寄存器 
		return MAX_TX; 
	}
	if(sta&TX_OK)//发送完成
	{
		return TX_OK;
	}
	return 0xff;//其他原因发送失败
}
//启动NRF24L01发送一次数据
//txbuf:待发送数据首地址
//返回值:0,接收完成;其他,错误代码
u8 NRF24L01_RxPacket(u8 *rxbuf)
{
	u8 sta;		    							   
	SPI2_SetSpeed(SPI_BaudRatePrescaler_8); //spi速度为9Mhz(24L01的最大SPI时钟为10Mhz)   
	sta=NRF24L01_Read_Reg(STATUS);  //读取状态寄存器的值    	 
	NRF24L01_Write_Reg(NRF_WRITE_REG+STATUS,sta); //清除TX_DS或MAX_RT中断标志
	if(sta&RX_OK)//接收到数据
	{
		NRF24L01_Read_Buf(RD_RX_PLOAD,rxbuf,RX_PLOAD_WIDTH);//读取数据
		NRF24L01_Write_Reg(FLUSH_RX,0xff);//清除RX FIFO寄存器 
		return 0; 
	}	   
	return 1;//没收到任何数据
}					    
//该函数初始化NRF24L01到RX模式
//设置RX地址,写RX数据宽度,选择RF频道,波特率和LNA HCURR
//当CE变高后,即进入RX模式,并可以接收数据了		   
void NRF24L01_RX_Mode(void)
{
	NRF24L01_CE=0;	  
  	NRF24L01_Write_Buf(NRF_WRITE_REG+RX_ADDR_P0,(u8*)RX_ADDRESS,RX_ADR_WIDTH);//写RX节点地址
	  
  	NRF24L01_Write_Reg(NRF_WRITE_REG+EN_AA,0x01);    //使能通道0的自动应答    
  	NRF24L01_Write_Reg(NRF_WRITE_REG+EN_RXADDR,0x01);//使能通道0的接收地址  	 
  	NRF24L01_Write_Reg(NRF_WRITE_REG+RF_CH,40);	     //设置RF通信频率		  
  	NRF24L01_Write_Reg(NRF_WRITE_REG+RX_PW_P0,RX_PLOAD_WIDTH);//选择通道0的有效数据宽度 	    
  	NRF24L01_Write_Reg(NRF_WRITE_REG+RF_SETUP,0x0f);//设置TX发射参数,0db增益,2Mbps,低噪声增益开启   
  	NRF24L01_Write_Reg(NRF_WRITE_REG+CONFIG, 0x0f);//配置基本工作模式的参数;PWR_UP,EN_CRC,16BIT_CRC,接收模式 
  	NRF24L01_CE = 1; //CE为高,进入接收模式 
}						 
//该函数初始化NRF24L01到TX模式
//设置TX地址,写TX数据宽度,设置RX自动应答的地址,填充TX发送数据,选择RF频道,波特率和LNA HCURR
//PWR_UP,CRC使能
//当CE变高后,即进入RX模式,并可以接收数据了		   
//CE为高大于10us,则启动发送.	 
void NRF24L01_TX_Mode(void)
{														 
	NRF24L01_CE=0;	    
  	NRF24L01_Write_Buf(NRF_WRITE_REG+TX_ADDR,(u8*)TX_ADDRESS,TX_ADR_WIDTH);//写TX节点地址 
  	NRF24L01_Write_Buf(NRF_WRITE_REG+RX_ADDR_P0,(u8*)RX_ADDRESS,RX_ADR_WIDTH); //设置TX节点地址,主要为了使能ACK	  

  	NRF24L01_Write_Reg(NRF_WRITE_REG+EN_AA,0x01);     //使能通道0的自动应答    
  	NRF24L01_Write_Reg(NRF_WRITE_REG+EN_RXADDR,0x01); //使能通道0的接收地址  
  	NRF24L01_Write_Reg(NRF_WRITE_REG+SETUP_RETR,0x1a);//设置自动重发间隔时间:500us + 86us;最大自动重发次数:10次
  	NRF24L01_Write_Reg(NRF_WRITE_REG+RF_CH,40);       //设置RF通道为40
  	NRF24L01_Write_Reg(NRF_WRITE_REG+RF_SETUP,0x0f);  //设置TX发射参数,0db增益,2Mbps,低噪声增益开启   
  	NRF24L01_Write_Reg(NRF_WRITE_REG+CONFIG,0x0e);    //配置基本工作模式的参数;PWR_UP,EN_CRC,16BIT_CRC,接收模式,开启所有中断
	NRF24L01_CE=1;//CE为高,10us后启动发送
}

spi.c
main.c

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "lcd.h"
#include "usart.h"	 
#include "24l01.h" 	 
 
 
/************************************************
 ALIENTEK战舰STM32开发板实验33
 无线通信 实验
 技术支持:www.openedv.com
 淘宝店铺:http://eboard.taobao.com 
 关注微信公众平台微信号:"正点原子",免费获取STM32资料。
 广州市星翼电子科技有限公司  
 作者:正点原子 @ALIENTEK
************************************************/


 int main(void)
 {	 
	u8 key,mode;
	u16 t=0;			 
	u8 tmp_buf[33];		    
	delay_init();	    	 //延时函数初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 	//串口初始化为115200
 	LED_Init();		  			//初始化与LED连接的硬件接口
	KEY_Init();					//初始化按键
	LCD_Init();			   		//初始化LCD  
 	NRF24L01_Init();    		//初始化NRF24L01 
 	POINT_COLOR=RED;			//设置字体为红色 
	LCD_ShowString(30,50,200,16,16,"WarShip STM32");	
	LCD_ShowString(30,70,200,16,16,"NRF24L01 TEST");	
	LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
	LCD_ShowString(30,110,200,16,16,"2015/1/17"); 
	while(NRF24L01_Check())
	{
		LCD_ShowString(30,130,200,16,16,"NRF24L01 Error");
		delay_ms(200);
		LCD_Fill(30,130,239,130+16,WHITE);
 		delay_ms(200);
	}
	LCD_ShowString(30,130,200,16,16,"NRF24L01 OK"); 	 
 	while(1)
	{	
		key=KEY_Scan(0);
		if(key==KEY0_PRES)
		{
			mode=0;   
			break;
		}else if(key==KEY1_PRES)
		{
			mode=1;
			break;
		}
		t++;
		if(t==100)LCD_ShowString(10,150,230,16,16,"KEY0:RX_Mode  KEY1:TX_Mode"); //闪烁显示提示信息
 		if(t==200)
		{	
			LCD_Fill(10,150,230,150+16,WHITE);
			t=0; 
		}
		delay_ms(5);	  
	}   
 	LCD_Fill(10,150,240,166,WHITE);//清空上面的显示		  
 	POINT_COLOR=BLUE;//设置字体为蓝色	   
	if(mode==0)//RX模式
	{
		LCD_ShowString(30,150,200,16,16,"NRF24L01 RX_Mode");	
		LCD_ShowString(30,170,200,16,16,"Received DATA:");	
		NRF24L01_RX_Mode();		  
		while(1)
		{	  		    		    				 
			if(NRF24L01_RxPacket(tmp_buf)==0)//一旦接收到信息,则显示出来.
			{
				tmp_buf[32]=0;//加入字符串结束符
				LCD_ShowString(0,190,lcddev.width-1,32,16,tmp_buf);    
			}else delay_us(100);	   
			t++;
			if(t==10000)//大约1s钟改变一次状态
			{
				t=0;
				LED0=!LED0;
			} 				    
		};	
	}else//TX模式
	{							    
		LCD_ShowString(30,150,200,16,16,"NRF24L01 TX_Mode");	
		NRF24L01_TX_Mode();
		mode=' ';//从空格键开始  
		while(1)
		{	  		   				 
			if(NRF24L01_TxPacket(tmp_buf)==TX_OK)
			{
				LCD_ShowString(30,170,239,32,16,"Sended DATA:");	
				LCD_ShowString(0,190,lcddev.width-1,32,16,tmp_buf); 
				key=mode;
				for(t=0;t<32;t++)
				{
					key++;
					if(key>('~'))key=' ';
					tmp_buf[t]=key;	
				}
				mode++; 
				if(mode>'~')mode=' ';  	  
				tmp_buf[32]=0;//加入结束符		   
			}else
			{										   	
 				LCD_Fill(0,170,lcddev.width,170+16*3,WHITE);//清空显示			   
				LCD_ShowString(30,170,lcddev.width-1,32,16,"Send Failed "); 
			};
			LED0=!LED0;
			delay_ms(1500);				    
		};
	} 
}

在这里插入图片描述

SPI总线通信读写外部FLASH

SPI 简介

SPI 是英语Serial Peripheral interface 的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX 系列处理器上定义的。SPI 接口主要应用在EEPROM,FLASH,实时时钟,AD 转换器,还有数字信号处理器和数字信号解码器之间。

SPI,是一种高速的,全双工同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB 的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议,STM32 也有SPI 接口。下面我们看看SPI 的内部简明图(图29.1.1):

在这里插入图片描述
SPI 接口一般使用4 条线通信:

  • MISO 主设备数据输入,从设备数据输出。
  • MOSI 主设备数据输出,从设备数据输入。
  • SCLK 时钟信号,由主设备产生。
  • CS 从设备片选信号,由主设备控制,方便一个SPI接口上可以挂多个SPI设备,片选引脚拉低即选中

从图中可以看出,主机和从机都有一个串行移位寄存器,主机通过向它的SPI 串行寄存器写入一个字节来发起一次传输。寄存器通过MOSI 信号线将字节传送给从机,从机也将自己的移位寄存器中的内容通过MISO 信号线返回给主机。这样,两个移位寄存器中的内容就被交换。

外设的写操作和读操作是同步完成的。如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输

SPI 主要特点有:可以同时发出和接收串行数据;可以当作主机或从机工作;提供频率可编程时钟;发送结束中断标志;写冲突保护;总线竞争保护等。

SPI 总线四种工作方式SPI 模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟极性和相位可以进行配置,时钟极性(SPI_CR寄存器的CPOL位)对传输协议没有重大的影响。如果CPOL=0,串行同步时钟的空闲状态为低电平;如果CPOL=1,串行同步时钟的空闲状态为高电平。时钟相位(SPI_CR寄存器的CPHA位)能够配置用于选择两种不同的传输协议之一进行数据传输。如果CPHA=0,在串行同步时钟的第一个跳变沿(上升或下降)数据被采样;如果CPHA=1,在串行同步时钟的第二个跳变沿(上升或下降)数据被采样。SPI 主模块和与之通信的外设备时钟相位和极性应该一致

不同时钟相位下的总线数据传输时序如图29.1.2 所示:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

STM32 的SPI 功能很强大,SPI 时钟最多可以到18Mhz,支持DMA,可以配置为SPI 协议或者I2S 协议(仅大容量型号支持,战舰STM32 开发板是支持的)。

本章,我们将利用STM32 的SPI 来读取外部SPI FLASH 芯片(W25Q128),实现类似上节的功能。这里对SPI 我们只简单介绍一下SPI 的使用,STM32 的SPI 详细介绍请参考《STM32参考手册》第457 页,23 节。然后我们再介绍下SPI FLASH 芯片。

这节,我们使用STM32 的SPI2 的主模式,下面就来看看SPI2 部分的设置步骤吧。SPI 相关的库函数和定义分布在文件stm32f10x_spi.c 以及头文件stm32f10x_spi.h 中。STM32 的主模式配置步骤如下:

配置相关引脚的复用功能,使能SPI2 时钟

我们要用SPI2,第一步就要使能SPI2 的时钟。其次要设置SPI2 的相关引脚为复用输出,这样才会连接到SPI2 上否则这些IO 口还是默认的状态,也就是标准输入输出口。这里我们使用的是PB13、14、15 这3 个(SCK.、MISO、MOSI,CS 使用软件管理方式),所以设置这三个为复用IO。

GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE );//PORTB 时钟使能
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE );//SPI2 时钟使能
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //PB13/14/15 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB

类似的时钟使能和IO 初始化我们前面多次讲解到,这里不做详细介绍。

初始化SPI2,设置SPI2 工作模式

接下来我们要初始化SPI2,设置SPI2 为主机模式,设置数据格式为8 位,然设置SCK 时钟极性及采样方式。并设置SPI2 的时钟频率(最大18Mhz),以及数据的格式(MSB 在前还是LSB 在前)。这在库函数中是通过SPI_Init 函数来实现的。

void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct)

跟其他外设初始化一样,第一个参数是SPI 标号,这里我们是使用的SPI2。下面我们来看看第二个参数结构体类型SPI_InitTypeDef 的定义:

typedef struct
{
	uint16_t SPI_Direction;
	uint16_t SPI_Mode;
	uint16_t SPI_DataSize;
	uint16_t SPI_CPOL;
	uint16_t SPI_CPHA;
	uint16_t SPI_NSS;
	uint16_t SPI_BaudRatePrescaler;
	uint16_t SPI_FirstBit;
	uint16_t SPI_CRCPolynomial;
}SPI_InitTypeDef;

结构体成员变量比较多,这里我们挑取几个重要的成员变量讲解一下:

  • 第一个参数SPI_Direction 是用来设置SPI 的通信方式,可以选择为半双工,全双工,以及串行发和串行收方式,这里我们选择全双工模式SPI_Direction_2Lines_FullDuplex。

  • 第二个参数SPI_Mode 用来设置SPI 的主从模式,这里我们设置为主机模式SPI_Mode_Master,当然有需要你也可以选择为从机模式SPI_Mode_Slave。

  • 第三个参数SPI_DataSiz 为8 位还是16 位帧格式选择项,这里我们是8 位传输,选择SPI_DataSize_8b。

  • 第四个参数SPI_CPOL 用来设置时钟极性,我们设置串行同步时钟的空闲状态为高电平所以我们选择SPI_CPOL_High。

  • 第五个参数SPI_CPHA 用来设置时钟相位,也就是选择在串行同步时钟的第几个跳变沿(上升或下降)数据被采样,可以为第一个或者第二个条边沿采集,这里我们选择第二个跳变沿,所以选择SPI_CPHA_2Edge

  • 第六个参数SPI_NSS 设置NSS 信号由硬件(NSS 管脚)还是软件控制,这里我们通过软件控制NSS 关键,而不是硬件自动控制,所以选择SPI_NSS_Soft。

  • 第七个参数SPI_BaudRatePrescaler 很关键,就是设置SPI 波特率预分频值也就是决定SPI 的时钟的参数,从不分频道256 分频8 个可选值,初始化的时候我们选择256 分频值SPI_BaudRatePrescaler_256, 传输速度为36M/256=140.625KHz。

  • 第八个参数SPI_FirstBit 设置数据传输顺序是MSB 位在前还是LSB 位在前,,这里我们选择SPI_FirstBit_MSB 高位在前。

  • 第九个参数SPI_CRCPolynomial 是用来设置CRC 校验多项式,提高通信可靠性,大于1 即可。

设置好上面9 个参数,我们就可以初始化SPI 外设了。初始化的范例格式为:

SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双线双向全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //主SPI
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // SPI 发送接收8 位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;//串行同步时钟的空闲状态为高电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;//第二个跳变沿数据被采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS 信号由软件控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //预分频256
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据传输从MSB 位开始
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC 值计算的多项式
SPI_Init(SPI2, &SPI_InitStructure); //根据指定的参数初始化外设SPIx 寄存器

使能SPI2

初始化完成之后接下来是要使能SPI2 通信了,在使能SPI2 之后,我们就可以开始SPI 通讯了。使能SPI2 的方法是:

SPI_Cmd(SPI2, ENABLE); //使能SPI 外设

SPI 传输数据

通信接口当然需要有发送数据和接受数据的函数,固件库提供的发送数据函数原型为:

void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data)

这个函数很好理解,往SPIx 数据寄存器写入数据Data,从而实现发送。
固件库提供的接受数据函数原型为:

uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx)

这个函数也不难理解,从SPIx 数据寄存器读出接受到的数据。

查看SPI 传输状态

在SPI 传输过程中,我们经常要判断数据是否传输完成,发送区是否为空等等状态,这是通过函数SPI_I2S_GetFlagStatus 实现的,这个函数很简单就不详细讲解,判断发送是否完成的方法是:

SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE)

W25Q128 FLASH介(数据手册)

SPI2 的使用就介绍到这里,接下来介绍一下W25Q128。W25Q128 是华邦公司推出的大容量SPI FLASH 产品,W25Q128 的容量为128Mb,该系列还有W25Q80/16/32/64 等。ALIENTEK所选择的W25Q128 容量为128Mb,也就是16M 字节。

看数据手册:W25Q128 将16M 的容量分为256 个块(Block),每个块大小为64K 字节,每个块又分为16 个扇区(Sector),每个扇区4K 个字节。W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除4K 个字节。这样我们需要给W25Q128 开辟一个至少4K 的缓存区,这样对SRAM 要求比较高,要求芯片必须有4K 以上SRAM 才能很好的操作。

W25Q128 的擦写周期多达10W 次,具有20 年的数据保存期限,支持电压为2.7~3.6V,W25Q128 支持标准的SPI,还支持双输出/四输出的SPI,最大SPI 时钟可以到80Mhz(双输出时相当于160Mhz,四输出时相当于320M),更多的W25Q128 的介绍,请参考W25Q128 的DATASHEET

硬件设计

本章实验功能简介:开机的时候先检测W25Q128 是否存在,然后在主循环里面检测两个按键,其中1 个按键(KEY1)用来执行写入W25Q128 的操作,另外一个按键(KEY0)用来执行读出操作,在TFTLCD 模块上显示相关信息。同时用DS0 提示程序正在运行。所要用到的硬件资源如下:
1)指示灯DS0
2)KEY0 和KEY1 按键
3)TFTLCD 模块
4)SPI
5)W25Q128
这里只介绍W25Q128 与STM32 的连接,板上的W25Q128 是直接连在STM32 的SPI2 上的,连接关系如图29.2.1 所示:
在这里插入图片描述

这里,我们的F_CS 是连接在PB12 上面的,另外要特别注意:W25Q128 和NRF24L01 共用SPI2,所以这两个器件在使用的时候,必须分时复用(通过片选控制)才行。

软件设计

打开我们光盘的SPI 实验工程,可以看到我们加入了spi.c,w25qxx.c 文件以及头文件spi.h和w25qxx.h,同时引入了库函数文件stm32f10x_spi.c 文件以及头文件stm32f10x_spi.h。

打开spi.c 文件,看到如下代码:

#include "spi.h"
 
//以下是SPI模块的初始化代码,配置成主机模式,访问SD Card/W25Q64/NRF24L01						  
//SPI口初始化
//这里针是对SPI2的初始化

void SPI2_Init(void)
{
 	GPIO_InitTypeDef GPIO_InitStructure;
    SPI_InitTypeDef  SPI_InitStructure;

	RCC_APB2PeriphClockCmd(	RCC_APB2Periph_GPIOB, ENABLE );//PORTB时钟使能 
	RCC_APB1PeriphClockCmd(	RCC_APB1Periph_SPI2,  ENABLE );//SPI2时钟使能 	
 
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //PB13/14/15复用推挽输出 
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);//初始化GPIOB

 	GPIO_SetBits(GPIOB,GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15);  //PB13/14/15上拉

	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;		//设置SPI工作模式:设置为主SPI
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;		//设置SPI的数据大小:SPI发送接收8位帧结构
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;		//串行同步时钟的空闲状态为高电平
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;	//串行同步时钟的第二个跳变沿(上升或下降)数据被采样
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;		//NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;		//定义波特率预分频的值:波特率预分频值为256
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;	//指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
	SPI_InitStructure.SPI_CRCPolynomial = 7;	//CRC值计算的多项式
	SPI_Init(SPI2, &SPI_InitStructure);  //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
 
	SPI_Cmd(SPI2, ENABLE); //使能SPI外设
	
	SPI2_ReadWriteByte(0xff);//启动传输	SPI通信规则:主机通过向它的SPI串行寄存器写入一个字节来发起一次传输
 

}   
//SPI 速度设置函数
//SpeedSet:
//SPI_BaudRatePrescaler_2   2分频   
//SPI_BaudRatePrescaler_8   8分频   
//SPI_BaudRatePrescaler_16  16分频  
//SPI_BaudRatePrescaler_256 256分频 
  
void SPI2_SetSpeed(u8 SPI_BaudRatePrescaler)
{
  assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));
	SPI2->CR1&=0XFFC7;
	SPI2->CR1|=SPI_BaudRatePrescaler;	//设置SPI2速度 
	SPI_Cmd(SPI2,ENABLE); 

} 

//SPIx 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
u8 SPI2_ReadWriteByte(u8 TxData)
{		
	u8 retry=0;				 	
	while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) //检查指定的SPI标志位设置与否:发送缓存空标志位
	{
		retry++;
		if(retry>200)return 0;
	}			  
	SPI_I2S_SendData(SPI2, TxData); //通过外设SPIx发送一个数据
	retry=0;

	while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET) //检查指定的SPI标志位设置与否:接受缓存非空标志位
	{
		retry++;
		if(retry>200)return 0;
	}	  						    
	return SPI_I2S_ReceiveData(SPI2); //返回通过SPIx最近接收的数据					    
}

此部分代码主要初始化SPI,这里我们选择的是SPI2,所以在SPI2_Init 函数里面,其相关的操作都是针对SPI2 的,其初始化步骤和我们上面介绍步骤1-5 一样,我们在代码中也使用了①~⑤标注。在初始化之后,我们就可以开始使用SPI2 了,在SPI2_Init 函数里面,把SPI2 的波特率设置成了最低(36Mhz,256 分频为140.625KHz)。在外部函数里面,我们通过
SPI2_SetSpeed 来设置SPI2 的速度,而我们的数据发送和接收则是通过SPI2_ReadWriteByte 函数来实现的。SPI2_SetSpeed 函数我们是通过寄存器设置方式来实现的,因为固件库并没有提供单独的设置分频系数的函数,当然,我们也可以勉强的调用SPI_Init 初始化函数来实现分频系数修改。要读懂这段代码,可以直接查找中文参考手册中SPI 章节的寄存器CR1 的描述即可。

这里特别注意,SPI 初始化函数的最后有一个启动传输,这句话最大的作用就是维持MOSI为高电平,而且这句话也不是必须的,可以去掉。

下面我们打开w25qxx.c,里面编写的是与W25Q128 操作相关的代码,由于篇幅所限,详细代码,这里就不贴出了。我们仅介绍几个重要的函数,首先是W25QXX_Read 函数,该函数用于从W25Q128 的指定地址读出指定长度的数据。其代码如下:

//读取SPI FLASH
//在指定地址开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddr:开始读取的地址(24bit)
//NumByteToRead:要读取的字节数(最大65535)
void W25QXX_Read (u8* pBuffer,u32 ReadAddr,u16 NumByteToRead)
{
	u16 i;
	SPI_FLASH_CS=0; //使能器件
	SPI2_ReadWriteByte(W25X_ReadData); //发送读取命令
	SPI2_ReadWriteByte((u8)((ReadAddr)>>16)); //发送24bit 地址
	SPI2_ReadWriteByte((u8)((ReadAddr)>>8));
	SPI2_ReadWriteByte((u8)ReadAddr);
	for(i=0;i<NumByteToRead;i++)
	{
		pBuffer[i]=SPI2_ReadWriteByte(0XFF); //循环读数
	}
	SPI_FLASH_CS=1;
}

由于W25Q128 支持以任意地址(但是不能超过W25Q128 的地址范围)开始读取数据,所以,这个代码相对来说就比较简单了,在发送24 位地址之后,程序就可以开始循环读数据了,其地址会自动增加的,不过要注意,不能读的数据超过了W25Q128 的地址范围哦!否则读出来的数据,就不是你想要的数据了。

有读的函数,当然就有写的函数了,接下来,我们介绍W25QXX_Write 这个函数,该函数的作用与W25QXX_Flash_Read 的作用类似,不过是用来写数据到W25Q128 里面的,其代码如下:

u8 W25QXX_BUFFER[4096];
void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
	u32 secpos;
	u16 secoff;
	u16 secremain;
	u16 i;
	u8 * W25QXX_BUF;
	W25QXX_BUF=W25QXX_BUFFER;
	secpos=WriteAddr/4096;//扇区地址
	secoff=WriteAddr%4096;//在扇区内的偏移
	secremain=4096-secoff;//扇区剩余空间大小
	//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用
	if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大于4096 个字节
	while(1)
	{
		W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容
		for(i=0;i<secremain;i++)//校验数据
		{
			if(W25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除
		}
		if(i<secremain)//需要擦除
		{
			W25QXX_Erase_Sector(secpos); //擦除这个扇区
			for(i=0;i<secremain;i++) //复制
			{
				W25QXX_BUF[i+secoff]=pBuffer[i];
			}
			W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区
		}else W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);
		//写已经擦除了的,直接写入扇区剩余区间.
		if(NumByteToWrite==secremain)break;//写入结束了
		else//写入未结束
		{
			secpos++;//扇区地址增1
			secoff=0;//偏移位置为0
			pBuffer+=secremain; //指针偏移
			WriteAddr+=secremain; //写地址偏移
			NumByteToWrite-=secremain; //字节数递减
			if(NumByteToWrite>4096)secremain=4096;//下一个扇区还是写不完
			else secremain=NumByteToWrite; //下一个扇区可以写完了
		}
	};
}

该函数可以在W25Q128 的任意地址开始写入任意长度(必须不超过W25Q128 的容量)的数据。我们这里简单介绍一下思路:先获得首地址(WriteAddr)所在的扇区,并计算在扇区内的偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定长度的数据,然后擦除这个扇区,再一次性写入。当所需要写入的数据长度超过一个扇区的长度的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循环,直到写入结束。

接着打开w25qxx.h 文件可以看到,这里面就定义了一些与W25Q128 操作相关的命令(部分省略了),这些命令在W25Q128 的数据手册时序上都有详细的介绍,感兴趣的读者可以参考该数据手册。

#ifndef __FLASH_H
#define __FLASH_H			    
#include "sys.h" 
	  
//W25X系列/Q系列芯片列表	   
//W25Q80  ID  0XEF13
//W25Q16  ID  0XEF14
//W25Q32  ID  0XEF15
//W25Q64  ID  0XEF16	
//W25Q128 ID  0XEF17	
#define W25Q80 	0XEF13 	
#define W25Q16 	0XEF14
#define W25Q32 	0XEF15
#define W25Q64 	0XEF16
#define W25Q128	0XEF17

#define NM25Q80 	0X5213
#define NM25Q16 	0X5214
#define NM25Q32 	0X5215
#define NM25Q64 	0X5216
#define NM25Q128	0X5217
#define NM25Q256 	0X5218

extern u16 W25QXX_TYPE;					//定义W25QXX芯片型号		   

#define	W25QXX_CS 		PBout(12)  		//W25QXX的片选信号
				 

 
//指令表
#define W25X_WriteEnable		0x06 
#define W25X_WriteDisable		0x04 
#define W25X_ReadStatusReg		0x05 
#define W25X_WriteStatusReg		0x01 
#define W25X_ReadData			0x03 
#define W25X_FastReadData		0x0B 
#define W25X_FastReadDual		0x3B 
#define W25X_PageProgram		0x02 
#define W25X_BlockErase			0xD8 
#define W25X_SectorErase		0x20 
#define W25X_ChipErase			0xC7 
#define W25X_PowerDown			0xB9 
#define W25X_ReleasePowerDown	0xAB 
#define W25X_DeviceID			0xAB 
#define W25X_ManufactDeviceID	0x90 
#define W25X_JedecDeviceID		0x9F 

void W25QXX_Init(void);
u16  W25QXX_ReadID(void);  	    		//读取FLASH ID
u8	 W25QXX_ReadSR(void);        		//读取状态寄存器 
void W25QXX_Write_SR(u8 sr);  			//写状态寄存器
void W25QXX_Write_Enable(void);  		//写使能 
void W25QXX_Write_Disable(void);		//写保护
void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);
void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead);   //读取flash
void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);//写入flash
void W25QXX_Erase_Chip(void);    	  	//整片擦除
void W25QXX_Erase_Sector(u32 Dst_Addr);	//扇区擦除
void W25QXX_Wait_Busy(void);           	//等待空闲
void W25QXX_PowerDown(void);        	//进入掉电模式
void W25QXX_WAKEUP(void);				//唤醒
#endif

最后,我们看看main.c 里面代码如下:

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "lcd.h"
#include "usart.h"	 
#include "w25qxx.h"	 
			 	
//要写入到W25Q64的字符串数组
const u8 TEXT_Buffer[]={"WarShipSTM32 SPI TEST"};
#define SIZE sizeof(TEXT_Buffer)
 int main(void)
 {	 
	u8 key;
	u16 i=0;
	u8 datatemp[SIZE];
	u32 FLASH_SIZE; 
    u16 id = 0;
     
	delay_init();	    	 //延时函数初始化	  
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 	//串口初始化为115200
	LED_Init();		  		//初始化与LED连接的硬件接口
	LCD_Init();			   	//初始化LCD 	
	KEY_Init();				//按键初始化		 	 	
	W25QXX_Init();			//W25QXX初始化

 	POINT_COLOR=RED;//设置字体为红色 
	LCD_ShowString(30,50,200,16,16,"WarShip STM32");	
	LCD_ShowString(30,70,200,16,16,"SPI TEST");	
	LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
	LCD_ShowString(30,110,200,16,16,"2015/1/15");	
	LCD_ShowString(30,130,200,16,16,"KEY1:Write  KEY0:Read");	//显示提示信息		
	while(1)
	{
		id = W25QXX_ReadID();
		if (id == W25Q128 || id == NM25Q128)
			break;
		LCD_ShowString(30,150,200,16,16,"W25Q128 Check Failed!");
		delay_ms(500);
		LCD_ShowString(30,150,200,16,16,"Please Check!        ");
		delay_ms(500);
		LED0=!LED0;//DS0闪烁
	}
	LCD_ShowString(30,150,200,16,16,"W25Q128 Ready!");    
	FLASH_SIZE=128*1024*1024;	//FLASH 大小为16M字节
 	POINT_COLOR=BLUE;//设置字体为蓝色	  
	while(1)
	{
		key=KEY_Scan(0);
		if(key==KEY1_PRES)	//KEY1按下,写入W25QXX
		{
			LCD_Fill(0,170,239,319,WHITE);//清除半屏    
 			LCD_ShowString(30,170,200,16,16,"Start Write W25Q128...."); 
			W25QXX_Write((u8*)TEXT_Buffer,FLASH_SIZE-100,SIZE);			//从倒数第100个地址处开始,写入SIZE长度的数据
			LCD_ShowString(30,170,200,16,16,"W25Q128 Write Finished!");	//提示传送完成
		}
		if(key==KEY0_PRES)	//KEY0按下,读取字符串并显示
		{
 			LCD_ShowString(30,170,200,16,16,"Start Read W25Q128.... ");
			W25QXX_Read(datatemp,FLASH_SIZE-100,SIZE);					//从倒数第100个地址处开始,读出SIZE个字节
			LCD_ShowString(30,170,200,16,16,"The Data Readed Is:  ");	//提示传送完成
			LCD_ShowString(30,190,200,16,16,datatemp);//显示读到的字符串
		}
		i++;
		delay_ms(10);
		if(i==20)
		{
			LED0=!LED0;//提示系统正在运行	
			i=0;
		}		   
	}
}

这部分代码和IIC 实验那部分代码大同小异,我们就不多说了,实现的功能就和IIC 差不多,不过此次写入和读出的是SPI FLASH,而不是EEPROM。

下载验证

在代码编译成功之后,我们通过下载代码到ALIENTEK 战舰STM32 开发板上,通过先按KEY1 按键写入数据,然后按KEY0 读取数据,得到如图29.4.1 所示:
在这里插入图片描述
伴随DS0 的不停闪烁,提示程序在运行。程序在开机的时候会检测W25Q128 是否存在,如果不存在则会在TFTLCD 模块上显示错误信息,同时DS0 慢闪。大家可以把PB14 和GND短接就可以看到报错了。

485通信实验

本章我们将向大家介绍如何利用STM32F1 的串口实现485 通信(半双工)。在本章中,我们将利用STM32F1 的 串口2 来实现两块开发板之间的 485 通信,并将结果显示在TFTLCD 模块上。

485 简介

485(一般称作RS485/EIA-485)是隶属于OSI 模型物理层的电气特性规定为 2 线,半双工,多点通信的标准。它的电气特性和RS-232 大不一样。用缆线两端的电压差值来表示传递信号。RS485 仅仅规定了接受端和发送端的电气特性。它没有规定或推荐任何数据协议。

RS485 的特点包括:

  • 1)接口电平低,不易损坏芯片。RS485 的电气特性:逻辑“1”以两线间的电压差为+(2~ 6)V表示;逻辑“0”以两线间的电压差为-(2~6)V 表示。接口信号电平比RS232 降低了,不易损坏接口电路的芯片,且该电平与TTL 电平兼容,可方便与TTL 电路连接。
  • 2)传输速率高。10 米时,RS485 的数据最高传输速率可达35Mbps,在1200m 时,传输速度可达100Kbps。
  • 3)抗干扰能力强。RS485 接口是采用平衡驱动器和差分接收器的组合,抗共模干扰能力增强,即抗噪声干扰性好。
  • 4)传输距离远,支持节点多。RS485 总线最长可以传输1200m 以上(速率≤100Kbps)一般最大支持32 个节点,如果使用特制的485 芯片,可以达到128 个或者256 个节点,最大的可以支持到400 个节点。

RS485 推荐使用在点对点网络中,线型,总线型,不能是星型,环型网络。理想情况下RS485需要2 个匹配电阻,其阻值要求等于传输电缆的特性阻抗(一般为120Ω)。没有特性阻抗的话,当所有的设备都静止或者没有能量的时候就会产生噪声,而且线移需要双端的电压差。没有终接电阻的话,会使得较快速的发送端产生多个数据信号的边缘,导致数据传输出错。485 推荐的连接方式如图30.1.1 所示:

在这里插入图片描述
图30.1.1 RS485 连接

在上面的连接中,如果需要添加匹配电阻,我们一般在总线的起止端加入,也就是主机和设备4 上面各加一个120Ω的匹配电阻。
由于RS485 具有传输距离远、传输速度快、支持节点多和抗干扰能力更强等特点,所以RS485 有很广泛的应用。
战舰STM32 开发板采用 SP3485芯片 作为收发器,该芯片支持3.3V 供电,最大传输速度可达10Mbps,支持多达32 个节点,并且有输出短路保护。该芯片的框图如图30.1.2 所示:

在这里插入图片描述

  • A、B 总线接口,用于连接485 总线
  • RO 是接收输出端
  • DI 是发送数据收入端
  • RE是接收使能信号(低电平有效)
  • DE 是发送使能信号(高电平有效)

本章,我们通过该芯片连接STM32 的串口2,实现两个开发板之间的485 通信。本章将实现这样的功能:通过连接两个战舰STM32 开发板的RS485 接口,然后由KEY0 控制发送,当按下一个开发板的KEY0 的时候,就发送5 个数据给另外一个开发板,并在两个开发板上分别显示发送的值和接收到的值。

本章,我们只需要配置好串口2,就可以实现正常的485 通信了,串口2 的配置和串口1基本类似,只是串口的时钟来自APB1,最大频率为36Mhz。

硬件设计

本章要用到的硬件资源如下:
1)指示灯DS0
2)KEY0 按键
3)TFTLCD 模块
4)串口2
5)RS485 收发芯片
前面3 个都有详细介绍,这里我们介绍RS485 和串口2 的连接关系,如图30.2.1 所示:

在这里插入图片描述

图30.2.1 STM32 与SP3485 连接电路图

从上图可以看出:STM32F1 的串口2 通过P7 端口设置,连接到SP3485,通过STM32F1的PD7 控制SP3485 的收发,当PD7=0 (拉低)的时候,为接收模式;当PD7=1 (拉高)的时候,为发送模式

这里需要注意,RS485_RE 信号和DM9000_RST 共用PD7,所以他们也不可以同时使用,只能分时复用。

另外,图中的R19 和R22 是两个偏置电阻,用来保证总线空闲时,A、B 之间的电压差都会大200mV(逻辑1)。从而避免因总线空闲时,A、B 压差不定,引起逻辑错乱,可能出现的乱码。

然后,我们要设置好开发板上P7 排针的连接,通过跳线帽将PA2 和PA3 分别连接到485_RX和485_TX 上面,如图30.2.2 所示:

在这里插入图片描述

最后,我们用2 根导线将两个开发板RS485 端子的A 和A,B 和B 连接起来。这里注意不要接反了(A 接B),接反了会导致通讯异常!!

软件设计

打开我们的485 实验例程,可以发现项目中加入了一个rs485.c 文件以及其头文件rs485 文件,同时485 通信调用的库函数和定义分布在stm32f10x_usart.c 文件和头文件stm32f10x_usart.h文件中。

打开rs485.c 文件,代码如下:

#include "sys.h"
#include "rs485.h"
#include "delay.h"
#ifdef EN_USART2_RX // 如果使能了接收
// 接收缓存区
u8 RS485_RX_BUF[64]; // 接收缓冲,最大64 个字节.
// 接收到的数据长度
u8 RS485_RX_CNT = 0;
void USART2_IRQHandler(void)
{
    u8 res;
    if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) // 接收到数据
    {
        res = USART_ReceiveData(USART2); // 读取接收到的数据
        if (RS485_RX_CNT < 64)
        {
            RS485_RX_BUF[RS485_RX_CNT] = res; // 记录接收到的值
            RS485_RX_CNT++;                   // 接收数据增加1
        }
    }
}
#endif
// 初始化IO 串口2
// bound:波特率
void RS485_Init(u32 bound)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |
                               RCC_APB2Periph_GPIOG,
                           ENABLE);                        // 使能GPIOA,G 时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // 使能串口2 时钟
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;              	// PG9 端口配置
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;       	// 推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOG, &GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;       		// PA2
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; 		// 复用推挽
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;             	// PA3
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; 	// 浮空输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART2, ENABLE);      // 复位串口2
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_USART2, DISABLE);     // 停止复位
#ifdef EN_USART2_RX                                             // 如果使能了接收
    USART_InitStructure.USART_BaudRate = bound;                 // 波特率设置;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 8 位数据长度
    USART_InitStructure.USART_StopBits = USART_StopBits_1;      // 一个停止位
    USART_InitStructure.USART_Parity = USART_Parity_No;         // 奇偶校验位
    USART_InitStructure.USART_HardwareFlowControl =
        USART_HardwareFlowControl_None;                             // 无硬件数据流控制
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发
    USART_Init(USART2, &USART_InitStructure);                       // 初始化串口
    NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;               // 使能串口2 中断
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;       // 先占优先级2 级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;              // 从优先级2 级
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                 // 使能外部中断通道
    NVIC_Init(&NVIC_InitStructure);                                 // 初始化NVIC 寄存器
    USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);                  // 开启中断
    USART_Cmd(USART2, ENABLE);                                      // 使能串口
#endif
    RS485_TX_EN = 0; // 默认为接收模式
}

// RS485 发送len 个字节.
// buf:发送区首地址
// len:发送的字节数(为了和本代码的接收匹配,这里建议不要超过64 个字节)
void RS485_Send_Data(u8 *buf, u8 len)
{
    u8 t;
    RS485_TX_EN = 1;          // 设置为发送模式(拉高)
    for (t = 0; t < len; t++) // 循环发送数据
    {
        while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET)
            ;
        USART_SendData(USART2, buf[t]);
    }
    while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET)
        ;
    RS485_RX_CNT = 0;
    RS485_TX_EN = 0; 			// 设置为接收模式(拉低)
}

// RS485 查询接收到的数据
// buf:接收缓存首地址
// len:读到的数据长度
void RS485_Receive_Data(u8 *buf, u8 *len)
{
    u8 rxlen = RS485_RX_CNT;
    u8 i = 0;
    *len = 0;                           // 默认为0
    delay_ms(10);                       // 等待10ms,连续超过10ms 没有接收到一个数据,则认为接收结束
    if (rxlen == RS485_RX_CNT && rxlen) // 接收到了数据,且接收完成了
    {
        for (i = 0; i < rxlen; i++)
        {
            buf[i] = RS485_RX_BUF[i];
        }
        *len = RS485_RX_CNT; // 记录本次数据长度
        RS485_RX_CNT = 0;    // 清零
    }
}

此部分代码总共4 个函数,其中RS485_Init 函数为485 通信初始化函数,其实基本上就是在配置串口2,只是把PD7 也顺带配置了,用于控制SP3485 的收发。同时如果使能中断接收的话,会执行串口2 的中断接收配置。USART2_IRQHandler 函数用于中断接收来自485 总线的数据,将其存放在RS485_RX_BUF 里面。最后RS485_Send_Data 和RS485_Receive_Data 这两个函数用来发送数据到485 总线和读取从485 总线收到的数据,都比较简单。

头文件rs485.h 中代码比较简单,在其中我们开启了串口2 的中断接收。最后,我们看看主函数main 的内容如下:

int main(void)
{
    u8 key;
    u8 i = 0, t = 0;
    u8 cnt = 0;
    u8 rs485buf[5];
    delay_init();                                   // 延时函数初始化
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 设置中断优先级分组为组2
    uart_init(115200);                              // 串口初始化为115200
    LED_Init();                                     // 初始化与LED 连接的硬件接口
    LCD_Init();                                     // 初始化LCD
    KEY_Init();                                     // 按键初始化
    RS485_Init(9600);                               // 初始化RS485
    POINT_COLOR = RED;                              // 设置字体为红色
    LCD_ShowString(30, 50, 200, 16, 16, "WarShip STM32");
    LCD_ShowString(30, 70, 200, 16, 16, "RS485 TEST");
    LCD_ShowString(30, 90, 200, 16, 16, "ATOM@ALIENTEK");
    LCD_ShowString(30, 110, 200, 16, 16, "2015/1/15");
    LCD_ShowString(30, 130, 200, 16, 16, "KEY0:Send");     // 显示提示信息
    POINT_COLOR = BLUE;                                    // 设置字体为蓝色
    LCD_ShowString(30, 150, 200, 16, 16, "Count:");        // 显示当前计数值
    LCD_ShowString(30, 170, 200, 16, 16, "Send Data:");    // 提示发送的数据
    LCD_ShowString(30, 210, 200, 16, 16, "Receive Data:"); // 提示接收到的数据
    while (1)
    {
        key = KEY_Scan(0);
        if (key == KEY0_PRES) // KEY0 按下,发送一次数据
        {
            for (i = 0; i < 5; i++)
            {
                rs485buf[i] = cnt + i;                                    // 填充发送缓冲区
                LCD_ShowxNum(30 + i * 32, 190, rs485buf[i], 3, 16, 0X80); // 显示数据
            }
            RS485_Send_Data(rs485buf, 5); // 发送5 个字节
        }
        RS485_Receive_Data(rs485buf, &key);
        if (key) // 接收到有数据
        {
            if (key > 5)
                key = 5; // 最大是5 个数据.
            for (i = 0; i < key; i++)
                LCD_ShowxNum(30 + i * 32, 230, rs485buf[i], 3, 16, 0X80); // 显示数据
        }
        t++;
        delay_ms(10);
        if (t == 20)
        {
            LED0 = !LED0; // 提示系统正在运行
            t = 0;
            cnt++;
            LCD_ShowxNum(30 + 48, 150, cnt, 3, 16, 0X80); // 显示数据
        }
    }
}

此部分代码,cnt 是一个累加数,一旦KEY0 按下按下,就以这个数位基准连续发送5 个数据。当485 总线收到数据的时候,就将收到的数据直接显示在LCD 屏幕上。

下载验证

在代码编译成功之后,我们通过下载代码到ALIENTEK 战舰STM32 开发板上(注意要2个开发板都下载这个代码哦),得到如图30.4.1 所示:

在这里插入图片描述
图30.4.1 程序运行效果图

伴随DS0 的不停闪烁,提示程序在运行。此时,我们按下KEY0 就可以在另外一个开发板上面收到这个开发板发送的数据了。如图30.4.2 和图30.4.3 所示:

在这里插入图片描述
图30.4.2 RS485 发送数据

在这里插入图片描述
图30.4.3 RS485 接收数据

图30.4.2 来自开发板A,发送了5 个数据,图30.4.3 来自开发板B,接收到了来自开发板A 的5 个数据。

本章介绍的485 总线时通过串口控制收发的,我们只需要将P7 的跳线帽稍作改变(将PA2/PA3 连接COM2_RX/COM2_TX),该实验就变成了一个RS232 串口通信实验了,通过对接两个开发板的RS232 接口,即可得到同样的实验现象,有兴趣的读者可以实验一下。

另外,利用USMART 测试的部分,我们这里就不做介绍了,大家可自行验证下。

内部FLASH 操作

块(bank/block两种叫法?) > 扇区(sector) > 页(page)

摘自:flash 内部 扇区 页 块 区别

一般 一个块 (bank)有多个扇区 (sector),一个扇区(sector)有多个页(page)

块(bank) > 扇区(sector) > 页(page)

在这里插入图片描述
在这里插入图片描述

摘自:FLASH ERASE:CHIP、BLOCK、SECTOR

SPI FLASH 的ERASE、READ、WRITE:ERASE包括CHIP、BLOCK、SECTOR三方面的内容;READ可对指定地址的单个及连续读取数据;WRITE也有单个及连续写入数据。

ERASE的内容:

  • CHIP(芯片):整片FLASH ERASE,视容量大小占耗时间,是最长的ERASE时间。
  • BLOCK:大部分的FLASH都以64K为单位空间ERASE,具体可有针对地参考资料查证,特别是FLASH的TOP或BOTTOM容易有差别。每个BLOCK的ERASE时间较快。
  • SECTOR:比BLOCK更小的单位,ERASE时间最快。

这三方面的ERASE都带两个参数HIGH ADDRESS及LOW ADDRESS,或可通过寄存器R0-R7来传递。使用时,可根据利用到的FLASH空间及ERASE时间长短对程序的影响来作出适当的选择。

Flash相对于普通设备的特殊性

摘自:https://www.cnblogs.com/sankye/articles/1638852.html

Flash最小操作单位,有些特殊。

一般设备,比如硬盘/内存,读取和写入都是以bit位为单位,读取一个bit的值,将某个值写入对应的地址的位,都是可以按位操作的。

但是Flash由于物理特性,使得内部存储的数据,只能从1变成0,这点,可以从前面的内部实现机制了解到,只是方便统一充电,不方便单独的存储单元去放电,所以才说,只能从1变成0,也就是释放电荷。

所以,总结一下Flash的特殊性如下:

项目普通设备(硬盘/内存等)Flash
读取/写入的叫法读取/写入读取/编程(Program)①
读取/写入的最小单位Bit/位Page/页
擦除(Erase)操作的最小单位Bit/位Block/块②
擦除操作的含义将数据删除/全部写入0将整个块都擦除成全是1,也就是里面的数据都是0xFF③
对于写操作直接写即可在写数据之前,要先擦除,然后再写

注:

① 之所以将写操作叫做编程,是因为,flash 和之前的EPROM,EEPROM继承发展而来,而之前的EEPROM(Electrically Erasable Programmable Read-Only Memory),往里面写入数据,就叫做编程Program,之所以这么称呼,是因为其对数据的写入,是需要用电去擦除/写入的,就叫做编程。

② 对于目前常见的页大小是2K/4K的Nand Flash,其块的大小有128KB/256KB/512KB等。而对于Nor Flash,常见的块大小有64K/32K等。

③在写数据之前,要先擦除,内部就都变成0xFF了,然后才能写入数据,也就是将对应位由1变成0。

FLASH 模拟 EEPROM

STM32 本身没有自带EEPROM,但是STM32 具有IAP(在应用编程)功能,所以我们可以把它的FLASH 当成EEPROM 来使用。本章,我们将利用STM32 内部的FLASH 来实现第二十八章类似的效果,不过这次我们是将数据直接存放在STM32 内部,而不是存放在W25Q128。

STM32 FLASH 简介

不同型号的STM32,其FLASH 容量也有所不同,最小的只有16K 字节,最大的达到了1024K 字节。战舰STM32 开发板选择的STM32F103ZET6 的FLASH 容量为512K 字节,属于大容量产品(另外还有中容量和小容量产品),大容量产品的闪存模块组织如图39.1.1 所示:

在这里插入图片描述

STM32 的闪存模块由:主存储器、信息块和闪存存储器接口寄存器等3 部分组成。

主存储器,该部分用来存放代码和数据常数(如const型常数)。对于大容量产品,其被划分为256 页,每页2K 字节。注意,小容量和中容量产品则每页只有1K 字节。从上图可以看出主存储器的起始地址就是0X08000000,B0、B1 都接GND 的时候,就是从0X08000000开始运行代码的。

信息块,该部分分为2 个小部分,其中启动程序代码,是用来存储ST自带的启动程序,用于串口下载代码,当B0 接V3.3,B1 接GND 的时候,运行的就是这部分代码。用户选择字节,则一般用于配置写保护、读保护等功能,本章不作介绍。

闪存存储器接口寄存器,该部分用于控制闪存读写等,是整个闪存模块的控制机构。

对主存储器和信息块的写入由内嵌的闪存编程/擦除控制器(FPEC)管理;编程与擦除的高电压由内部产生。

在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确地进行;既在进行写或擦除操作时,不能进行代码或数据的读取操作。

在这里插入图片描述

在这里插入图片描述

FLASH操作(读、写、擦除)

闪存的读取
内置闪存模块可以在通用地址空间直接寻址,任何32 位数据的读操作都能访问闪存模块的内容并得到相应的数据。读接口在闪存端包含一个读控制器,还包含一个AHB 接口与CPU 衔接,这个接口的主要工作是产生读闪存的控制信号并预取CPU 要求的指令块,预取指令块仅用于在I-Code总线上的取指操作数据常量是通过D-Code总线访问的。这两条总线的访问目标是相同的闪存模块,访问D-Code 将比预取指令优先级高。

这里要特别留意一个闪存等待时间,因为CPU 运行速度比FLASH 快得多,STM32F103的FLASH 最快访问速度≤24Mhz,如果CPU 频率超过这个速度,那么必须加入等待时间,比如我们一般使用72Mhz的主频,那么FLASH等待周期就必须设置为2,该设置通过FLASH_ACR寄存器设置。

例如,我们要从地址addr,读取一个半字(半字为16 为,字为32 位),可以通过如下的语句读取:

data=*(vu16*)addr;

将addr 强制转换为vu16 指针,然后取该指针所指向的地址的值,即得到了addr 地址的值。

类似的,将上面的vu16 改为vu8,即可读取指定地址的一个字节。

闪存的写入
STM32 的闪存编程是由FPEC(闪存编程和擦除控制器)模块处理的,这个模块包含7 个32 位寄存器,他们分别是:

⚫ FPEC 键寄存器(FLASH_KEYR)
⚫ 选择字节键寄存器(FLASH_OPTKEYR)
⚫ 闪存控制寄存器(FLASH_CR)
⚫ 闪存状态寄存器(FLASH_SR)
⚫ 闪存地址寄存器(FLASH_AR)
⚫ 选择字节寄存器(FLASH_OBR)
⚫ 写保护寄存器(FLASH_WRPR)

其中FPEC 键寄存器总共有3 个键值:
RDPRT 键=0X000000A5
KEY1=0X45670123
KEY2=0XCDEF89AB

  • STM32 复位后,FPEC 模块是被保护的,不能写入FLASH_CR 寄存器;通过写入特定的序列到FLASH_KEYR 寄存器可以打开FPEC 模块(即写入KEY1 和KEY2),只有解除写保护后,我们才能操作相关寄存器

  • STM32 闪存的编程每次必须写入16 位(不能单纯的写入8 位数据),当FLASH_CR 寄存器的PG位为’1’时,在一个闪存地址写入一个半字将启动一次编程;写入任何非半字的数据,FPEC 都会产生总线错误。

  • 在编程过程中BSY位为’1’,任何读写闪存的操作都会使CPU暂停,直到此次闪存编程结束。

  • STM32 的FLASH 在编程的时候,必须要求其写入地址的FLASH 是被擦除了的(也就是其值必须是0XFFFF),否则无法写入,在FLASH_SR 寄存器的PGERR 位将得到一个警告。

STM23 的FLASH 编程过程如图39.1.2 所示:
在这里插入图片描述
从上图可以得到闪存的编程顺序如下:
⚫ 检查FLASH_CR 的LOCK 是否解锁,如果没有则先解锁
⚫ 检查FLASH_SR 寄存器的BSY 位,以确认没有其他正在进行的编程操作
⚫ 设置FLASH_CR 寄存器的PG 位为’1’
⚫ 在指定的地址写入要编程的半字
⚫ 等待BSY 位变为’0’
⚫ 读出写入的地址并验证数据

闪存的擦除
前面提到,我们在STM32 的FLASH 编程的时候,要先判断缩写地址是否被擦除了,STM32 的闪存擦除分为两种:页擦除和整片擦除。页擦除过程如图39.1.3 所示
在这里插入图片描述
从上图可以看出,STM32 的页擦除顺序为:
⚫ 检查FLASH_CR 的LOCK 是否解锁,如果没有则先解锁
⚫ 检查FLASH_SR 寄存器的BSY 位,以确认没有其他正在进行的闪存操作
⚫ 设置FLASH_CR 寄存器的PER 位为’1’
⚫ 用FLASH_AR 寄存器选择要擦除的页
⚫ 设置FLASH_CR 寄存器的STRT 位为’1’
⚫ 等待BSY 位变为’0’(擦除完毕)
⚫ 读出被擦除的页并做验证

本章,我们只用到了STM32 的页擦除功能。

在这里插入图片描述

寄存器

  • FPEC 键寄存器:FLASH_KEYR。该寄存器各位描述如图39.1.4 所示:
    在这里插入图片描述
    该寄存器主要用来解锁FPEC,必须在该寄存器写入特定的序列(KEY1 和KEY2)解锁后,才能对FLASH_CR 寄存器进行写操作。

  • 闪存控制寄存器:FLASH_CR。该寄存器的各位描述如图39.1.5 所示:
    在这里插入图片描述
    该寄存器我们本章只用到了它的LOCK、STRT、PER 和PG 等4 个位。

LOCK 位,该位用于指示FLASH_CR 寄存器是否被锁住,该位在检测到正确的解锁序列后,硬件将其清零。在一次不成功的解锁操作后,在下次系统复位之前,该位将不再改变。

STRT 位,该位用于开始一次擦除操作。在该位写入1 ,将执行一次擦除操作。
PER 位,该位用于选择页擦除操作,在页擦除的时候,需要将该位置1。
PG 位,该位用于选择编程操作,在往FLASH 写数据的时候,该位需要置1。
FLASH_CR 的其他位,我们就不在这里介绍了,请大家参考《STM32F10xxx 闪存编程参考手册》第18 页。

  • 闪存状态寄存器:FLASH_SR。该寄存器各位描述如图39.1.6 所示:
    在这里插入图片描述

该寄存器主要用来指示当前FPEC 的操作编程状态。

  • 闪存地址寄存器:FLASH_AR。该寄存器各位描述如图39.1.7 所示:
    在这里插入图片描述

该寄存器在本章,我们主要用来设置要擦除的页。

关于STM32 FLASH 的基础知识介绍,我们就介绍到这。更详细的介绍,请参考《STM32F10xxx 闪存编程参考手册》。

库函数(本质操作寄存器)

在stm32f10x_flash.c 以及stm32f10x_flash.h 文件中。

1、锁定 / 解锁函数
上面讲解到在对FLASH 进行写操作前必须先解锁,解锁操作也就是必须在FLASH_KEYR 寄存器写入特定的序列(KEY1 和KEY2),固件库函数实现很简单:

void FLASH_Unlock(void)

同样的道理,在对FLASH 写操作完成之后,我们要锁定FLASH,使用的库函数是:

void FLASH_Lock(void)

2、写操作函数
固件库提供了三个FLASH 写函数:

FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);
FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data);

FLASH_ProgramWord 为32 位字写入函数,其他分别为16 位半字写入和用户选择字节写入函数。
这里需要说明,32 位字节写入实际上是写入的两次16 位数据,写完第一次后地址+2,这与我们前面讲解的STM32 闪存的编程每次必须写入16 位并不矛盾。写入8位实际也是占用的两个地址了,跟写入16 位基本上没啥区别。

3、擦除函数
固件库提供三个FLASH 擦除函数:

FLASH_Status FLASH_ErasePage(uint32_t Page_Address);
FLASH_Status FLASH_EraseAllPages(void);
FLASH_Status FLASH_EraseOptionBytes(void);

第一个函数是页擦除函数,根据页地址擦除特定的页数据;
第二个函数是擦除所有的页数据;
第三个函数是擦除用户选择字节数据。这三个函数使用非常简单。

4、获取FLASH 状态
主要是用的函数是:

FLASH_Status FLASH_GetStatus(void)

返回值是通过枚举类型定义的:

typedef enum
{
	FLASH_BUSY = 1,//忙
	FLASH_ERROR_PG,//编程错误
	FLASH_ERROR_WRP,//写保护错误
	FLASH_COMPLETE,//操作完成
	FLASH_TIMEOUT//操作超时
}FLASH_Status;

从这里面我们可以看到FLASH 操作的5 个状态,每个代表的意思我们在后面注释了。

5、等待操作完成函数
在执行闪存写操作时,任何对闪存的读操作都会锁住总线。
所以在每次操作之前,我们都要等待上一次操作完成这次操作才能开始。使用的函数是:

FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout)

入口参数为等待时间,返回值是FLASH 的状态,这个很容易理解,这个函数本身我们在固件库中使用得不多,但是在固件库函数体中间可以多次看到。

6、读FLASH 特定地址数据函数
读取FLASH 指定地址的半字的函数固件库并没有给出来,这里我们自己写的一个函数:

u16 STMFLASH_ReadHalfWord(u32 faddr)
{
	return *(vu16*)faddr;
}

在这里插入图片描述

在这里插入图片描述

硬件设计

本章实验功能简介:开机的时候先显示一些提示信息,然后在主循环里面检测两个按键:按键WK_UP执行FLASH写操作,按键KEY1执行FLASH读操作,在TFTLCD 模块上显示相关信息。同时用DS0 提示程序正在运行。
所要用到的硬件资源如下:

  1. 指示灯DS0
  2. WK_UP 和KEY1 按键
  3. TFTLCD 模块
  4. STM32 内部FLASH

本章需要用到的资源和电路连接,在之前已经全部有介绍过了。

软件设计

打开FLASH 模拟EEPROM 实验工程,我们添加了两个文件stmflash.c 和stm32flash.h 。
同时引入了固件库flash 操作文件stm32f10x_flash.c 和头文件stm32f10x_flash.h。

这个是前面EEPROM写入思路:
在这里插入图片描述 这个是FLASH写入思路:
在这里插入图片描述

打开stmflash.c 文件,代码如下:

stmflash.h

#ifndef __STMFLASH_H__
#define __STMFLASH_H__
#include "sys.h"  

//用户根据自己的需要设置
#define STM32_FLASH_SIZE 512 	 		//所选STM32的FLASH容量大小(单位为K)
#define STM32_FLASH_WREN 1              //使能FLASH写入 0-不使能 1-使能

//FLASH起始地址
#define STM32_FLASH_BASE 0x08000000 	//STM32 FLASH的起始地址
//FLASH解锁键值
 
u16 STMFLASH_ReadHalfWord(u32 faddr);   							//读出半字  
void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite);		//指定地址开始写入指定长度的数据
void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead);   		//指定地址开始读出指定长度的数据

//测试写入
void Test_Write(u32 WriteAddr,u16 WriteData);								   
#endif

stmflash.c

#include "stmflash.h"
#include "delay.h"
#include "usart.h"
 
//读取指定地址的半字(16位数据)
//faddr:读地址(此地址必须为2的倍数!!)
//返回值:对应数据.
u16 STMFLASH_ReadHalfWord(u32 faddr)
{
	return *(vu16*)faddr; 
}
#if STM32_FLASH_WREN	//如果使能了写   

//不检查的写入(写之前要擦除,检查是否擦除了,这里是已经检查过了直接写入)
//WriteAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数   
void STMFLASH_Write_NoCheck(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)   
{ 			 		 
	u16 i;
	for(i=0;i<NumToWrite;i++)
	{
		FLASH_ProgramHalfWord(WriteAddr,pBuffer[i]);
	    WriteAddr+=2;//地址增加2.
	}  
} 
//从指定地址开始写入指定长度的数据
//WriteAddr:起始地址(此地址必须为2的倍数!!)
//pBuffer:数据指针
//NumToWrite:半字(16位)数(就是要写入的16位(半字)数据的个数.)
#if STM32_FLASH_SIZE<256
#define STM_SECTOR_SIZE 1024 //页大小
#else 
#define STM_SECTOR_SIZE	2048
#endif		 
u16 STMFLASH_BUF[STM_SECTOR_SIZE/2];//最多是2K字节
void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)	
{
	u32 secpos;	   //扇区地址
	u16 secoff;	   //扇区内偏移地址(16位字计算)
	u16 secremain; //扇区内剩余地址(16位字计算)	   
 	u16 i;    
	u32 offaddr;   

	if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址,地址越界
	FLASH_Unlock();						    //解锁
	offaddr=WriteAddr-STM32_FLASH_BASE;		//实际偏移地址.
	secpos=offaddr/STM_SECTOR_SIZE;			//落在第几页sector  0~127 for STM32F103RBT6
	secoff=(offaddr%STM_SECTOR_SIZE)/2;		//在扇区内的偏移(2个字节为基本单位.)
	secremain=STM_SECTOR_SIZE/2-secoff;		//扇区剩余空间大小   
	if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围
	while(1) 
	{	                                                      //读到buf数组里面	
	    STMFLASH_Read(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容
		for(i=0;i<secremain;i++)//是否擦除
		{
			if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除
		}
		if(i<secremain)//擦除操作
		{
			FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区
			for(i=0;i<secremain;i++)//复制到flash里面
			{
				STMFLASH_BUF[i+secoff]=pBuffer[i];	  
			}
			STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区  
		}else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//不用擦除,直接写入到扇区剩余区间 				   
		
		if(NumToWrite==secremain)break;//写入结束了 就是没跨越页
		else//写入未结束  跨页了
		{
			secpos++;				 //扇区地址增1
			secoff=0;				 //偏移位置为0 	 
		   	pBuffer+=secremain;  	 //指针偏移
			WriteAddr+=(secremain*2);//写地址偏移	   
		   	NumToWrite-=secremain;	 //字节(16位)数递减
			if(NumToWrite>(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完
			else secremain=NumToWrite;//下一个扇区可以写完了
		}	 
	};	
	FLASH_Lock();//上锁
}
#endif

//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数
void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead)   	
{
	u16 i;
	for(i=0;i<NumToRead;i++)
	{
		pBuffer[i]=STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节(半字)
		ReadAddr+=2;//偏移2个字节.	
	}
}

//WriteAddr:起始地址
//WriteData:要写入的数据
void Test_Write(u32 WriteAddr,u16 WriteData)   	
{
	STMFLASH_Write(WriteAddr,&WriteData,1);//写入一个字 
}

STMFLASH_ReadHalfWord()函数的实现原理我们在前面已经详细讲解了。

STMFLASH_Write()函数用于在STM32 的指定地址写入指定长度的数据,该函数的实现基本类似第29 章的W25QXX_Write 函数,不过对写入地址是有要求的,必须保证以下两点:
1:该地址必须是用户代码区以外的地址。
2:该地址必须是2 的倍数,STM32 FLASH 的要求,每次必须写入16 位。

该函数的STMFLASH_BUF 数组,也是根据所用STM32 的FLASH 容量来确定的,战舰STM32 开发板的FLASH 是512K 字节,所以STM_SECTOR_SIZE 的值为512,故该数组大小为2K 字节。

main.c

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "lcd.h"
#include "usart.h"	 
#include "stmflash.h"


//要写入到STM32 FLASH的字符串数组
const u8 TEXT_Buffer[]={"STM32F103 FLASH TEST"};
#define SIZE sizeof(TEXT_Buffer)		//数组长度
#define FLASH_SAVE_ADDR  0X08070000		//设置FLASH 保存地址(必须为偶数,且其值要大于本代码所占用FLASH的大小+0X08000000)

 int main(void)
 {	 
	u8 key;
	u16 i=0;
	u8 datatemp[SIZE];

	delay_init();	    	 //延时函数初始化	  
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 	//串口初始化为115200
 	LED_Init();		  			//初始化与LED连接的硬件接口
	KEY_Init();					//初始化按键
	LCD_Init();			   		//初始化LCD  
 	POINT_COLOR=RED;			//设置字体为红色 
	LCD_ShowString(30,50,200,16,16,"WarShip STM32");	
	LCD_ShowString(30,70,200,16,16,"FLASH EEPROM TEST");	
	LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
	LCD_ShowString(30,110,200,16,16,"2015/1/18"); 
	LCD_ShowString(30,130,200,16,16,"KEY1:Write  KEY0:Read");
	
	while(1)
	{
		key=KEY_Scan(0);
		if(key==KEY1_PRES)	//KEY1按下,写入STM32 FLASH
		{
			LCD_Fill(0,170,239,319,WHITE);//清除半屏    
 			LCD_ShowString(30,170,200,16,16,"Start Write FLASH....");
			STMFLASH_Write(FLASH_SAVE_ADDR,(u16*)TEXT_Buffer,SIZE);
			LCD_ShowString(30,170,200,16,16,"FLASH Write Finished!");//提示传送完成
		}
		if(key==KEY0_PRES)	//KEY0按下,读取字符串并显示
		{
 			LCD_ShowString(30,170,200,16,16,"Start Read FLASH.... ");
			STMFLASH_Read(FLASH_SAVE_ADDR,(u16*)datatemp,SIZE);
			LCD_ShowString(30,170,200,16,16,"The Data Readed Is:  ");//提示传送完成
			LCD_ShowString(30,190,200,16,16,datatemp);//显示读到的字符串
		}
		i++;
		delay_ms(10);  
		if(i==20)
		{
			LED0=!LED0;//提示系统正在运行	
			i=0;
		}		   
	} 
}

在代码编译成功之后,我们通过下载代码到ALIENTEK 战舰STM32 开发板上,通过先按WK_UP 按键写入数据,然后按KEY1 读取数据,DS0不停闪烁,提示程序在运行。如图39.4.1 所示:

在这里插入图片描述

串口IAP实验(在程序中编程In Application Programming)

IAP配置

bootloader的三个作用(程序要实现的功能):

1、接收APP的bin文件
2、将这个文件写到flash的某个区域
3、实现跳转

IAP,即在应用编程。很多单片机都支持这个功能,STM32 也不例外。在之前的FLASH模拟EEPROM 实验里面,我们学习了STM32 的FLASH 自编程,本章我们将结合FLASH 自编程的知识,通过STM32 的串口实现一个简单的IAP 功能

IAP 简介

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

IAP(In Application Programming)即在应用编程,IAP 是用户自己的程序在运行过程中对User Flash 的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。

通常实现IAP 功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序通过某种通信方式(如USB、USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码是真正的功能代码。这两部分项目代码都同时烧录在User Flash 中,当芯片上电后,首先是第一个项目代码开始运行,它作如下操作:

1)检查是否需要对第二部分代码进行更新
2)如果不需要更新则转到4)
3)执行更新操作
4)跳转到第二部分代码执行

第一部分代码必须通过其它手段,如JTAG或ISP烧入;第二部分代码可以使用第一部分代码IAP功能烧入,也可以和第一部分代码一起烧入,以后需要程序更新时再通过第一部分IAP代码更新。

我们将第一个项目代码称之为Bootloader程序,第二个项目代码称之为APP程序,他们存放在STM32 FLASH 的不同地址范围,一般从最低地址区开始存放Bootloader,紧跟其后的就是APP 程序(注意,如果FLASH 容量足够,是可以设计很多APP 程序的,本章我们只讨论一个APP 程序的情况)。这样我们就是要实现2 个程序:Bootloader 和APP。

STM32 的APP 程序不仅可以放到FLASH 里面运行,也可以放到SRAM 里面运行,本章,我们将制作两个APP,一个用于FLASH 运行,一个用于SRAM 运行。

IAP运作流程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

我们先来看看STM32 正常的程序运行流程,如图52.1.1 所示:

在这里插入图片描述
STM32 的内部闪存(FLASH)地址起始于0x08000000,一般情况下,程序文件就从此地址开始写入。此外STM32 是基于Cortex-M3 内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而这张“中断向量表”的起始地址是0x08000004,当中断来临,STM32 的内部硬件机制亦会自动将PC 指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。

在图52.1.1 中,STM32 在复位后,先从0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的main 函数,如图标号②所示;而我们的main 函数一般都是一个死循环,在main 函数执行过程中,如果收到中断请求(发生重中断),此时STM32 强制将PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回main 函数执行,如图标号⑤所示。

加入IAP 程序之后,程序运行流程如图52.1.2 所示:

在这里插入图片描述

在图52.1.2 所示流程中,STM32 复位后,还是从0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到IAP 的main 函数,如图标号①所示,此部分同图52.1.1 一样;在执行完IAP 以后(即将新的APP 代码写入STM32的FLASH,灰底部分。新程序的复位中断向量起始地址为0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main 函数,如图标号②和③所示,同样main 函数为一个死循环,并且注意到此时STM32 的FLASH,在不同位置上,共有两个中断向量表

在main 函数执行过程中,如果CPU 得到一个中断请求,PC 指针仍强制跳转到地址0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回main 函数继续运行,如图标号⑥所示。

通过以上两个过程的分析,我们知道IAP 程序必须满足两个要求:
1)新程序必须在IAP 程序之后的某个偏移量为x 的地址开始;
2)必须设置新程序的中断向量表偏移,移动的偏移量为x;

本章,我们有2 个APP 程序,一个为FLASH 的APP,程序在FLASH 中运行,另外一个位SRAM 的APP,程序运行在SRAM 中,图52.1.2 虽然是针对FLASH APP 来说的,但是在SRAM 里面运行的过程和FLASH 基本一致,只是需要设置向量表的地址为SRAM 的地址。

APP 程序起始地址设置方法

随便打开一个之前的实例工程,点击Options for Target→Target 选项卡,如图52.1.3 所示:
在这里插入图片描述
默认的条件下,图中IROM1 的起始地址(Start)一般为0X08000000,大小(Size)为0X80000,即从0X08000000 开始的512K 空间为我们的程序存储,因为我们的STM32F103ZET6 的FLASH大小是512K。

而图中,我们设置起始地址(Start)为0X08010000,即偏移量为0X10000(64K字节),因而,留给APP 用的FLASH 空间(Size)只有0X80000-0X10000=0X70000(448K 字节)大小了。设置好Start 和Szie,就完成APP 程序的起始地址设置。

这里的64K字节,需要大家根据Bootloader 程序大小进行选择,比如我们本章的Bootloader程序为22K 左右,理论上我们只需要确保APP 起始地址在Bootloader 之后,并且偏移量为0X200的倍数即可(相关知识,请参考:http://www.openedv.com/posts/list/392.htm)。这里我们选择64K(0X10000)字节,留了一些余量,方便Bootloader 以后的升级修改。

这是针对FLASH APP 的起始地址设置,如果是SRAM APP,那么起始地址设置如图52.1.4所示:

在这里插入图片描述
(上图中IROM1其实就是RAM)从地址0X20000000偏移0X1000(0X1000是4K 给Bootloader 程序使用)开始,存放APP 代码,大小为0XC000(48K 字节)。因为整个STM32F103ZET6 的SRAM大小为64K 字节,所以IRAM1 (SRAM)的起始地址变为0X2000D000(0x20001000+0xC000=0X2000D000 ),大小只有0X3000 (12K 字节)。

这样,整个STM32F103ZET6 的SRAM 分配情况为:最开始的4K 给Bootloader程序使用,随后的48K存放APP程序,最后12K,用作APP 程序的内存。这个分配关系大家可以根据自己的实际情况修改,需要保证偏移量为0X200 的倍数(我们这里为0X1000)。

中断向量表的偏移量设置方法

之前我们讲解过,在系统启动的时候,会首先调用systemInit 函数初始化时钟系统,同时systemInit 还完成了中断向量表的设置,我们可以打开systemInit 函数,看看函数体的结尾处有这样几行代码:

#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;
/* Vector Table Relocation in Internal SRAM. */
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
/* Vector Table Relocation in Internal FLASH. */
#endif

VTOR 寄存器存放的是中断向量表的起始地址。默认的情况VECT_TAB_SRAM 没有定义,所以执行SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET。
对于FLASH APP,设置为FLASH_BASE+偏移量0x10000,所以在FLASH APP 的main 函数最开头处添加如下代码实现中断向量表的起始地址的重设:

SCB->VTOR = FLASH_BASE | 0x10000;

当使用SRAM APP 的时候,我们设置起始地址为:SRAM_bASE+0x1000,同样的方法,在SRAM APP 的main 函数最开始处,添加下面代码:

SCB->VTOR = SRAM_BASE | 0x1000;

这样,我们就完成了中断向量表偏移量的设置。

hex转bin文件

MDK 默认生成的文件是.hex 文件,我们希望生成的文件是.bin 文件(BIN文件和HEX文件区别)。这里我们通过MDK 自带的格式转换工具fromelf.exe,来实现.axf 文件到.bin 文件的转换。该工具在MDK 的安装目录\ARM\BIN40 文件夹里面。

fromelf.exe 转换工具的语法格式为:fromelf [options] input_file。其中options 有很多选项可以设置,详细使用请参考光盘《mdk 如何生成bin 文件.doc》.

本章,我们通过在MDK 点击Options for Target→User 选项卡,在After Build/Rebuild 栏,勾选Run #1,并写入:D:\tools\mdk5.14\ARM\ARMCC\bin\fromelf.exe --bin -o …\OBJ\RTC.bin
…\OBJ\RTC.axf。如图52.1.6 所示:
在这里插入图片描述

通过这一步设置,我们就可以在MDK 编译成功之后,调用fromelf.exe(注意,我的MDK 是安装在D:\tools\mdk5.14 文件夹下,如果你是安装在其他目录,请根据你自己的目录修改fromelf.exe 的路径),根据当前工程的RTC.axf(如果是其他的名字,请记住修改,这个文件存放在OBJ 目录下面,格式为xxx.axf),生成一个RTC.bin 的文件。并存放在axf 文件相同的目
录下,即工程的OBJ 文件夹里面。在得到.bin 文件之后,我们只需要将这个bin 文件传送给单片机,即可执行IAP 升级。

总结

APP 程序的生成步骤:
1)设置APP程序的起始地址和存储空间大小
对于在FLASH里面运行的APP程序,我们可以按照图52.1.3的设置。对于SRAM里面运行的APP程序,我们可以参考图52.1.4的设置。
2)设置中断向量表偏移量
这一步按照上面讲解,重新设置SCB->VTOR 的值即可。
3)设置编译后运行fromelf.exe,生成.bin文件.
通过在User 选项卡,设置编译后调用fromelf.exe,根据.axf文件生成.bin文件,用于IAP更新。
以上3 个步骤,我们就可以得到一个.bin的APP程序,通过Bootlader程序即可实现更新。

硬件设计

本章实验(Bootloader 部分)功能简介:开机的时候先显示提示信息,然后等待串口输入接收APP 程序(无校验,一次性接收),在串口接收到APP 程序之后,即可执行IAP,指示灯DS0用于指示程序运行状态。

  • 如果是SRAM APP,通过按下KEY0 即可执行这个收到的SRAM APP 程序。
  • 如果是FLASH APP,则需要先按下WK_UP按键,将串口接收到的APP程序存放到STM32 的FLASH,之后再按KEY1执行FLASH APP程序
  • KEY2按键,可以手动清除串口接收到的APP程序(实际代码只是显示清除)。

本实验用到的资源如下:
1)指示灯DS0
2)四个按键(KEY0/KEY1/KEY2/WK_UP)
3)串口
4)TFTLCD 模块

软件设计

本章,我们总共需要3 个程序:1、Bootloader;2、FLASH APP;3、SRAM APP;其中,我们选择之前做过的RTC 实验(在第二十章介绍)来做为FLASH APP 程序(起始地址为0X08010000),选择触摸屏实验(在第三十二章介绍)来做SRAM APP 程序(起始地址为0X20001000)。Bootloader 则是通过TFTLCD 显示实验(在第十八章介绍)修改得来。

iap.c

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "stmflash.h"//要对flash进行操作,引入头文件
#include "iap.h"


iapfun jump2app; 
u16 iapbuf[1024];   
//appxaddr:应用程序的起始地址
//appbuf:应用程序CODE.
//appsize:应用程序大小(字节).
void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 appsize)
{
	u16 t;
	u16 i=0;
	u16 temp;
	u32 fwaddr=appxaddr;//当前写入的地址
	u8 *dfu=appbuf;
	for(t=0;t<appsize;t+=2)
	{						    
		temp=(u16)dfu[1]<<8;
		temp+=(u16)dfu[0];	  
		dfu+=2;//偏移2个字节
		iapbuf[i++]=temp;	    
		if(i==1024)
		{
			i=0;
			STMFLASH_Write(fwaddr,iapbuf,1024);	
			fwaddr+=2048;//偏移2048  16=2*8.所以要乘以2.
		}
	}
	if(i)STMFLASH_Write(fwaddr,iapbuf,i);//将最后的一些内容字节写进去.  
}

//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
	if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)	//检查栈顶地址是否合法.
	{ 
		jump2app=(iapfun)*(vu32*)(appxaddr+4);		//用户代码区第二个字为程序开始地址(复位地址)		
		MSR_MSP(*(vu32*)appxaddr);					//初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
		jump2app();									//跳转到APP.
	}
}		 

iap_write_appbin 函数将存放在串口接收buf 里面的APP 程序写入到FLASH。iap_load_app 函数跳转到APP 程序运行,其参数appxaddr为APP 程序的起始地址。
程序先判断栈顶地址是否合法,在得到合法的栈顶地址后,通过MSR_MSP 函数(该函数在sys.c 文件)设置栈顶地址,最后通过一个虚拟的函数(jump2app)跳转到APP 程序执行代码,实现IAP→APP 的跳转。

打开iap.h 代码如下:

iap.h

#ifndef __IAP_H__
#define __IAP_H__
#include "sys.h"  


typedef  void (*iapfun)(void);				//定义一个函数类型的参数.

#define FLASH_APP1_ADDR		0x08010000  	//第一个应用程序起始地址(存放在FLASH)
											//保留0X08000000~0X0800FFFF的空间(64K 根据实际大小来)为IAP使用

void iap_load_app(u32 appxaddr);			//执行flash里面的app程序,起始地址为appxaddr
void iap_load_appsram(u32 appxaddr);		//执行SRAM里面的app程序
void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 applen);	//在flash指定地址appxaddr开始,写入bin
#endif

我们是通过串口接收APP 程序的,我们将usart.c 和usart.h做了稍微修改,在usart.h 中,我们定义USART_REC_LEN 为55K 字节,也就是串口最大一次可以接收55K 字节的数据,这也是本Bootloader 程序所能接收的最大APP 程序大小。然后新增一个USART_RX_CNT 的变量,用于记录接收到的文件大小,而USART_RX_STA 不再使用。

usart.h

#ifndef __USART_H
#define __USART_H
#include "stdio.h"	
#include "sys.h" 


#define USART_REC_LEN  			55*1024 //定义最大接收字节数 55K字节,bin文件不能大于55k 实际应用中根据工程需要修改
#define EN_USART1_RX 			1		//使能(1)/禁止(0)串口1接收
	  	  	
extern u8  USART_RX_BUF[USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.末字节为换行符 
extern u16 USART_RX_STA;         		//接收状态标记	
extern u16 USART_RX_CNT;				//接收的字节数	  
//如果想串口中断接收,请不要注释以下宏定义
void uart_init(u32 bound);
#endif

打开usart.c,可以看到我们修改USART1_IRQHandler 部分代码如下:

usart.c

#include "sys.h"
#include "usart.h"	  
// 	 
//如果使用ucos,则包括下面的头文件即可.
#if SYSTEM_SUPPORT_OS
#include "includes.h"					//ucos 使用	  
#endif

//
//加入以下代码,支持printf函数,而不需要选择use MicroLIB	  
#if 1
#pragma import(__use_no_semihosting)             
//标准库需要的支持函数                 
struct __FILE 
{ 
	int handle; 

}; 

FILE __stdout;       
//定义_sys_exit()以避免使用半主机模式    
void _sys_exit(int x) 
{ 
	x = x; 
} 
//重定义fputc函数 
int fputc(int ch, FILE *f)
{      
	while((USART1->SR&0X40)==0);//循环发送,直到发送完毕   
    USART1->DR = (u8) ch;      
	return ch;
}
#endif 

/*使用microLib的方法*/
 /* 
int fputc(int ch, FILE *f)
{
	USART_SendData(USART1, (uint8_t) ch);

	while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET) {}	
   
    return ch;
}
int GetKey (void)  { 

    while (!(USART1->SR & USART_FLAG_RXNE));

    return ((int)(USART1->DR & 0x1FF));
}
*/
 
#if EN_USART1_RX   //如果使能了接收
//串口1中断服务程序
//注意,读取USARTx->SR能避免莫名其妙的错误   	
u8 USART_RX_BUF[USART_REC_LEN] __attribute__ ((at(0X20001000)));//接收缓冲,最大USART_REC_LEN个字节,并限定数组存放在SRAM区域的起始地址为0X20001000(前面留出一定空间给bootloader使用)
//接收状态
//bit15,	接收完成标志
//bit14,	接收到0x0d
//bit13~0,	接收到的有效字节数目
u16 USART_RX_STA=0;       	//接收状态标记	  
u16 USART_RX_CNT=0;			//接收的字节数	   
  
void uart_init(u32 bound)
{
	//GPIO端口设置
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	//使能USART1,GPIOA时钟

	//USART1_TX   GPIOA.9
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
	GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9

	//USART1_RX	  GPIOA.10初始化
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
	GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10  

	//Usart1 NVIC 配置
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
	NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器

	//USART 初始化设置

	USART_InitStructure.USART_BaudRate = bound;//串口波特率
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式

	USART_Init(USART1, &USART_InitStructure); //初始化串口1
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
	USART_Cmd(USART1, ENABLE);                    //使能串口1 

}

void USART1_IRQHandler(void)//每接收一个字节都要执行中断服务函数
{
	u8 res;	
#ifdef OS_CRITICAL_METHOD 	//如果OS_CRITICAL_METHOD定义了,说明使用ucosII了.
	OSIntEnter();    
#endif
	if(USART1->SR&(1<<5))//接收到数据
	{	 
		res=USART1->DR; 
		if(USART_RX_CNT<USART_REC_LEN)
		{
			USART_RX_BUF[USART_RX_CNT]=res;//接收到的数据存放在buf里,后续写入指定flash区域
			USART_RX_CNT++;			 									     
		}
	}
#ifdef OS_CRITICAL_METHOD 	//如果OS_CRITICAL_METHOD定义了,说明使用ucosII了.
	OSIntExit();  											 
#endif
} 
#endif	

这里,我们指定USART_RX_BUF 的地址是从0X20001000 开始,该地址也就是SRAM APP程序的起始地址。然后在USART1_IRQHandler 函数里面,将串口发送过来的数据,全部接收到USART_RX_BUF,并通过USART_RX_CNT 计数。

main.c

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "lcd.h"
#include "usart.h"
#include "stmflash.h"
#include "iap.h"
 

int main(void)
{		
	u8 t;
	u8 key;
	u16 oldcount=0;				//老的串口接收数据值
	u16 applenth=0;				//接收到的app代码长度
	u8 clearflag=0;  

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	//串口初始化为115200
	delay_init();	   	 	//延时初始化 
 	LED_Init();		  			//初始化与LED连接的硬件接口
	KEY_Init();					//初始化按键
	LCD_Init();			   		//初始化LCD  
	POINT_COLOR=RED;//设置字体为红色 
	LCD_ShowString(30,50,200,16,16,"Warship STM32");	
	LCD_ShowString(30,70,200,16,16,"IAP TEST");	
	LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
	LCD_ShowString(30,110,200,16,16,"2015/1/27");  
	LCD_ShowString(30,130,200,16,16,"KEY_UP:Copy APP2FLASH");
	LCD_ShowString(30,150,200,16,16,"KEY2:Erase SRAM APP");
	LCD_ShowString(30,170,200,16,16,"KEY1:Run FLASH APP");
	LCD_ShowString(30,190,200,16,16,"KEY0:Run SRAM APP");
	POINT_COLOR=BLUE;
	//显示提示信息
	POINT_COLOR=BLUE;//设置字体为蓝色	  
	
	while(1)
	{
	 	if(USART_RX_CNT)
		{
			if(oldcount==USART_RX_CNT)//新周期内,没有收到任何数据,认为本次数据接收完成.
			{
				applenth=USART_RX_CNT;
				oldcount=0;
				USART_RX_CNT=0;
				printf("用户程序接收完成!\r\n");//程序接收完成
				printf("代码长度:%dBytes\r\n",applenth);
			}else oldcount=USART_RX_CNT;			
		}
		t++;
		delay_ms(10);
		if(t==30)
		{
			LED0=!LED0;
			t=0;
			if(clearflag)
			{
				clearflag--;
				if(clearflag==0)LCD_Fill(30,210,240,210+16,WHITE);//清除显示
			}
		}	  	 
		
		key=KEY_Scan(0);  //按键扫描
		if(key==WKUP_PRES)//按键按下  APP程序存放到FLASH
		{
			if(applenth)
			{
				printf("开始更新固件...\r\n");	
				LCD_ShowString(30,210,200,16,16,"Copying APP2FLASH...");
 				if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
				{	 
					iap_write_appbin(FLASH_APP1_ADDR,USART_RX_BUF,applenth);//将bin程序写入到FLASH   
					LCD_ShowString(30,210,200,16,16,"Copy APP Successed!!");
					printf("固件更新完成!\r\n");	
				}else 
				{
					LCD_ShowString(30,210,200,16,16,"Illegal FLASH APP!  ");	   
					printf("非FLASH应用程序!\r\n");
				}
 			}else 
			{
				printf("没有可以更新的固件!\r\n");
				LCD_ShowString(30,210,200,16,16,"No APP!");
			}
			clearflag=7;//标志更新了显示,并且设置7*300ms后清除显示									 
		}
		
		if(key==KEY2_PRES)//按键2按下
		{
			if(applenth)
			{																	 
				printf("固件清除完成!\r\n");    
				LCD_ShowString(30,210,200,16,16,"APP Erase Successed!");
				applenth=0;//置0清除固件
			}else 
			{
				printf("没有可以清除的固件!\r\n");
				LCD_ShowString(30,210,200,16,16,"No APP!");
			}
			clearflag=7;//标志更新了显示,并且设置7*300ms后清除显示									 
		}
		
		if(key==KEY1_PRES)//按键1按下  跳转执行FLASH APP代码
		{
			printf("开始执行FLASH用户代码!!\r\n");
			if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
			{	 
				iap_load_app(FLASH_APP1_ADDR);//跳转执行FLASH APP代码
			}else 
			{
				printf("非FLASH应用程序,无法执行!\r\n");
				LCD_ShowString(30,210,200,16,16,"Illegal FLASH APP!");	   
			}									 
			clearflag=7;//标志更新了显示,并且设置7*300ms后清除显示	  
		}
		
		if(key==KEY0_PRES)//按键0按下  跳转执行SRAM地址程序
		{
			printf("开始执行SRAM用户代码!!\r\n");
			if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x20000000)//判断是否为0X20XXXXXX.
			{	 
				iap_load_app(0X20001000);//执行SRAM地址程序
			}else 
			{
				printf("非SRAM应用程序,无法执行!\r\n");
				LCD_ShowString(30,210,200,16,16,"Illegal SRAM APP!");	   
			}									 
			clearflag=7;//标志更新了显示,并且设置7*300ms后清除显示	 
		}				   
	}   	   
}

该段代码,实现了串口数据处理,以及IAP 更新和跳转等各项操作。

Bootloader 程序就设计完成了,一般要求bootloader 程序越小越好(给APP 省空间),我们把一些不需要用到的.c 文件全部去掉,最后得到工程截图如图52.3.1 所示:
在这里插入图片描述

从上图结果可以看出,虽然去掉了一些不用的.c 文件,但是Bootloader 大小还是有36K 左右,比较大,主要原因是液晶驱动和printf 占用了比较多的flash,如果大家想进一步删减,可以去掉LCD 显示和printf 等,不过我们在本章为了演示效果,所以保留了这些代码。

FLASH APP 和SRAM APP 两部分代码,我们在实验目录下提供了两个实验供大家参考,不过要提醒大家,根据我们的设置,FLASH APP 的起始地址必须是0X08010000,而SRAM APP的起始地址必须是0X20001000。

下载验证

在代码编译成功之后,我们下载代码到ALIENTEK 战舰STM32 开发板上,得到,如图52.4.1所示:

在这里插入图片描述

此时,我们可以通过串口,发送FLASH APP 或者SRAM APP 到战舰STM32 开发板,如图52.4.2 所示:

在这里插入图片描述
首先找到开发板USB 转串口的串口号,打开串口(我电脑是COM3),然后设置波特率为115200(图中标号1 所示),然后,点击打开文件按钮(如图标号2 所示),找到APP 程序生成的.bin 文件(注意:文件类型得选择所有文件,默认是只打开txt 文件的),最后点击发送文件(图中标号3 所示),将.bin 文件发送给战舰STM32F103,发送完成后,XCOM 会提示文件发
送完毕。

开发板在收到APP 程序之后,我们就可以通过KEY0/KEY1 运行这个APP 程序了(如果是FLASH APP,则先需要通过WK_UP 将其存入对应FLASH 区域)。

蓝牙模块HC05使用

模块介绍

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

模块AT指令集

在这里插入图片描述
在这里插入图片描述

与上位机串口通信

在这里插入图片描述

与手机蓝牙调试助手通信

在这里插入图片描述
蓝牙模块接收到手机端发来的数据,通过串口发送出去,在上位机使用串口调试助手可以查看到。
在这里插入图片描述

与单片机连接通信

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 2
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行稳方能走远

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值