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

目录

  • STM32命名规则
  • STM32芯片解读
  • 开发环境搭建(MDK - 就是ARM的keil,需破解 + 支持包 + CH340串口驱动+ JLINK驱动)
  • 程序下载方法 (ISP串口下载 + JLINK下载更方便)
  • 新建工程模板——基于固件库
  • 新建工程模板——基于寄存器
  • GPIO相关配置寄存器
  • 端口复用、重映射、中断
  • JLINK在线调试
    • JTAG/SWD调试原理
    • 软件仿真
    • ST-LINK下载与调试程序(硬件)
  • 独立看门狗IWDG
    • 独立看门狗概述
    • 寄存器和库函数
    • 代码实现
  • 窗口看门狗WWDG
    • 窗口看门狗概述
    • 寄存器和库函数
    • 代码实现
  • RTC 实时时钟
    • STM32F1 RTC 时钟简介
    • 20.2 硬件设计
    • 20.3 软件设计
    • 20.4 下载验证
  • 待机睡眠唤醒(低功耗)实验
    • 21.1 STM32 待机模式简介
    • 21.2 硬件设计
    • 21.3 软件设计
    • 21.4 下载与测试
  • 2.4G无线射频通信模块
    • NRF24L01简介
    • 寄存器介绍
    • 硬件连接
    • 源码讲解
  • SPI总线通信读写外部FLASH
    • SPI 简介
      • 配置相关引脚的复用功能,使能SPI2 时钟
      • 初始化SPI2,设置SPI2 工作模式
      • 使能SPI2
      • SPI 传输数据
      • 查看SPI 传输状态
    • W25Q128 FLASH介(数据手册)
    • 硬件设计
    • 软件设计
    • 下载验证
  • 485通信实验
    • 485接口原理
  • FLASH模拟EEPROM
    • STM32 FLASH 简介
    • FLASH操作(读、写、擦除)
    • 寄存器
    • 库函数(本质操作寄存器)
    • 硬件设计
    • 软件设计
  • 串口IAP实验(在程序中编程In Application Programming)
    • IAP配置
    • IAP 简介
    • IAP运作流程
      • APP 程序起始地址设置方法
      • 中断向量表的偏移量设置方法
      • hex转bin文件
      • 总结
    • 硬件设计
    • 软件设计
    • 下载验证
  • 蓝牙模块HC05使用
    • 模块介绍
    • 模块AT指令集
    • **与上位机串口通信**
    • **与手机蓝牙调试助手通信**
    • 与单片机连接通信

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预分频值为64IWDG_SetReload(rlr);  //设置IWDG重装载值rlrIWDG_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);  //串口初始化为115200LED_Init();           //初始化与LED连接的硬件接口KEY_Init();          //按键初始化    delay_ms(500);      //让人看得到灭IWDG_Init(4,625);    //预分频数为64,重载值为625,溢出时间为1s    LED0=0;              //点亮LED0while(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预分频值为64IWDG_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();//初始化窗口看门狗 NVICWWDG_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);  //串口初始化为115200LED_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 中断分组2uart_init(115200); //串口初始化波特率为115200LED_Init(); //LED 端口初始化LCD_Init(); //LCD 初始化usmart_dev.init(72); //初始化USMARTPOINT_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; //亮灯DS0while(1){if(WKUP_KD){t++; //已经按下了delay_ms(30);if(t>=100) //按下超过3 秒钟{LED0=0; //点亮DS0return 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.0GPIO_InitStructure.GPIO_Mode =GPIO_Mode_IPD; //上拉输入GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化IO//使用外部中断方式GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);//中断线0 连接GPIOA.0EXTI_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); //初始化NVICif(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 中断分组2uart_init(115200); //串口初始化波特率为115200LED_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); //初始化指定IOGPIO_SetBits(GPIOB,GPIO_Pin_12);//上拉               GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7|GPIO_Pin_8;   //PG8 7 推挽    GPIO_Init(GPIOG, &GPIO_InitStructure);//初始化指定IOGPIO_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;        //定义波特率预分频的值:波特率预分频值为16SPI_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;            //使能24L01NRF24L01_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通道为40NRF24L01_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);     //串口初始化为115200LED_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);//初始化GPIOBGPIO_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工作模式:设置为主SPISPI_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;     //定义波特率预分频的值:波特率预分频值为256SPI_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++;//扇区地址增1secoff=0;//偏移位置为0pBuffer+=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    0X5218extern 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);     //串口初始化为115200LED_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通信实验

485接口原理

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 STM32F103RBT6secoff=(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++;               //扇区地址增1secoff=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);     //串口初始化为115200LED_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.9GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9GPIO_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;//PA10GPIO_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 ;//抢占优先级3NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;       //子优先级3NVIC_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); //初始化串口1USART_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();
#endifif(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);   //串口初始化为115200delay_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指令集


与上位机串口通信

与手机蓝牙调试助手通信


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

与单片机连接通信



正点原子STM32(基于标准库)相关推荐

  1. 正点原子STM32(基于HAL库)3

    目录 高级定时器实验 高级定时器简介 高级定时器输出指定个数PWM 实验 高级定时器输出指定个数PWM原理 TIM1/TIM8 寄存器 硬件设计 课堂源码(输出指定个数PWM灯就闪几次) 程序设计 下 ...

  2. 正点原子STM32(基于HAL库)0

    目录 开发环境搭建与使用 常用开发工具简介 MDK 安装 仿真器驱动安装 CH340 USB 虚拟串口驱动安装 使用MDK5 编译例程 使用串口下载程序 使用DAP 下载与调试程序 使用DAP 下载程 ...

  3. 正点原子STM32(基于HAL库)5

    目录 SRAM 实验 存储器简介 SRAM 方案简介 硬件设计 程序设计 程序流程图 程序解析 下载验证 内存管理实验 内存管理简介 硬件设计 程序设计 程序流程图 程序解析 下载验证 SD 卡实验 ...

  4. 正点原子STM32(基于HAL库)4

    目录 ADC 实验 ADC 简介 单通道ADC 采集实验 ADC 寄存器 硬件设计 程序设计 下载验证 单通道ADC 采集(DMA 读取)实验 ADC & DMA 寄存器 硬件设计 程序设计 ...

  5. 正点原子stm32中hal库iic模拟`#define SDA_IN() {GPIOB->MODER=~(3<<(9*2));GPIOB->MODER|=0<<9*2;}` //PB9 输入模式

    正点原子hal库模拟iic中涉及到 #define SDA_IN() {GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=0<< ...

  6. THWATCH-01 陀螺仪 MPU6050 HAL库 正点原子 STM32驱动 计步

    THWATCH-01 陀螺仪 MPU6050 HAL库 正点原子 STM32驱动 计步 一级目录 二级目录 三级目录 一.cubemx配置 1.使用cubemx配置串口 2.配置IIC1 3.配置时钟 ...

  7. 【正点原子STM32连载】第四十五章 SD卡实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1

    1)实验平台:正点原子MiniPro H750开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560 3)全套实验源码+手册+视频 ...

  8. 【正点原子STM32连载】第五十七章 USB读卡器(Slave)实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1

    1)实验平台:正点原子MiniPro H750开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560 3)全套实验源码+手册+视频 ...

  9. 【正点原子STM32连载】第四章 STM32初体验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1

    1)实验平台:正点原子MiniPro H750开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560 3)全套实验源码+手册+视频 ...

  10. 【正点原子STM32连载】第四十六章 FATFS实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1

    1)实验平台:正点原子MiniPro H750开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560 3)全套实验源码+手册+视频 ...

最新文章

  1. 2019年 ICPC亚洲区预赛(上海赛区)总结
  2. Java面向对象基础接口和抽象的理解
  3. SpringBoot 动态创建多定时任务
  4. C#LeetCode刷题之#448-找到所有数组中消失的数字(Find All Numbers Disappeared in an Array)
  5. Java基础学习总结(151)——Java 8时间处理API再复习
  6. 农场管理软件行业调研报告 - 市场现状分析与发展前景预测
  7. 【转】腾讯移动品质中心TMQ [腾讯 TMQ] 测试管理平台大比拼
  8. python canvas画弧度_超清字符画——Python代码
  9. 系统学习机器学习之神经网络(九) --Hopfield网络
  10. Android心得1.5--第一次搭建Android环境的心得和第一个应用程序部分代码解析
  11. 远程控制办公室电脑在家远程连接办公室电脑进行办公
  12. ps 条件动作添加 图层锁定和解锁
  13. Action大全和用法
  14. cαr怎么发音_最全英语口语发音规则与技巧
  15. AES链路弹性和故障转移
  16. 记一次重装系统后恢复EFS加密文件过程
  17. 系统信息 打包压缩 软件安装 其他命令
  18. 查看 Windows 10 快捷键 占用 VSCode 多行编辑 ctrl alt uparrow
  19. IDEA配置SpringBoot项目热部署
  20. 如何防止网站被黑客入侵,避免网站被黑的防御方法

热门文章

  1. 计算机音乐东京不太热,洛天依 - 东京不太热[FLAC格式]
  2. 刷单之骗人先交钱QQ,如何从qq,找到他的ip
  3. 哈理工oj1003 Tiles of Tetris, NOT!
  4. 扫雷游戏网页版_佛性扫雷 炸不炸随缘
  5. 【系统分析师之路】第五章 复盘软件工程(敏捷开发)
  6. 2022年最新google drive 谷歌云盘下载文件限额解决方法
  7. C语言-----职工信息管理系统
  8. GitHub客户端上传本地代码
  9. SIFT特征提取原理
  10. qxdm无法安装问题闪一下_关于QXDM的安装,解决Win7下QIK报错的问题