十六、FLASH闪存

十四、FLASH闪存

FLASH简介

  • STM32F1系列的FLASH包含程序存储器、系统存储器和选项字节三个部分,通过闪存存储器接口(外设)可以对程序存储器和选项字节进行擦除和编程

    闪存存储器接口是一个外设,是这个闪存的管理员,把我们的指令和数据,写入到这个外设的相应寄存器,然后这个外设就会自动去操作对应的存储空间。

  • 读写FLASH的用途:

    1. 利用程序存储器的剩余空间来保存掉电不丢失的用户数据

      它的程序存储器容量是 64K,一般我们写个简单的程序,可能就只占前面的很小一部分空间,剩下的大片空余空间,我们就可以加以利用。比如存储一些我们自定义的数据

      注意:我们在选取存储区域时,一定不要覆盖了原有的程序,要不然程序自己把自己给破坏了,之后程序就运行不了了。

    2. 通过在程序中编程(IAP),实现程序的自我更新

      利用程序,来修改程序本身,实现程序的自我更新。

  • 在线编程(In-Circuit Programming – ICP)用于更新程序存储器的全部内容,它通过JTAG、SWD协议或系统加载程序(Bootloader)下载程序

  • 在程序中编程(In-Application Programming – IAP)可以使用微控制器支持的任一种通信接口下载程序

    首先需要自己写一个 BootLoader 程序,并且存放在程序更新时不会覆盖的地方,比如我们放在整个程序存储器的后面。

    需要更新程序时,我们控制程序跳转到这个自己写的 BootLoader 里来,在这里面,我们就可以接收任意一种通信接口传过来的数据,比如串口、USB、蓝牙转串口、WIFI 转串口等等,这个传过来的数据,就是待更新的程序

    控制 FLASH 读写,把收到的程序,写入到整个程序存储器的前面程序正常运行的地方,写完之后,再控制程序跳转回正常运行的地方,或者直接复位,这样程序就完成了自我升级。

注意:在编程过程中,任何读写闪存的操作都会使 CPU 暂停,直到此次闪存编程结束。这其实是读写内部闪存存储数据的一个弊端,就是闪存忙的时候,代码执行会暂停,因为执行代码需要读闪存,闪存在忙,没法读,所以 CPU 也就没法运行了,程序就会暂停,这会导致什么问题呢?假如你使用内部闪存存储数据,同时你的中断代码又是在频繁执行的,这样,读写闪存的时候,中断代码就无法执行了,这可能会导致中断无法及时响应

闪存模块组织(中容量)

这个表是中容量产品的闪存分配情况。我们 C8T6 芯片的闪存容量是 64K,属于中容量产品。对于小容量产品和大容量产品,闪存的分配方式有些区别,这个可以参考一下手册。

image

首先看一下第一列的几个块,这里分为了 3 个块,

  1. 主存储器:也就是程序存储器,用来存放程序代码的,这是最主要,也是容量最大的一块。

    对他进行了分页,分页是为了更好的管理闪存。每页的大小都是 1K,0~127,总共 128 页,总容量就是 128K,对于 C8T6 来说,它只有 64K,所以 C8T6 的页只有一半,0~63,总共 64 页,共 64K。

    擦除和写保护,都是以页为单位的,写入前必须擦除,擦除必须以最小单位进行,擦除后数据位全变为 1,数据只能 1 写 0,不能 0 写 1,擦除和写入之后都需要等待忙

    页的地址范围:

    • 第一个页的起始地址就是程序存储器的起始地址,0x0800 0000,之后就是一个字节一个地址,依次线性分配了。
    • 每页起始地址的规律,首先是 0000,然后 0400、0800、0C00,再之后,1000,后面按照规律,就是 1400、1800、1C00、2000、2400、2800、2C00 等等等等,最后一直到 1 FC00,所以地址只要以 000、400、800、C00 结尾的都一定是页的起始地址,之后如果想要给一个页的起始地址,就需要用到这个规律。
  2. 信息块

    • 启动程序代码:存放的是原厂写入的 BootLoader,用于串口下载

      起始地址是 0x1FFF F000,他的容量是 2K

    • 用户选择字节:也就是选项字节,存放一些独立的参数,这个选项字节

      起始地址是 0x1FFF F800,容量是 16 个字节,里面只有几个字节的配置参数,这个后面还会继续说的

  3. 闪存存储器接口寄存器:这一块的存储器,实际上并不属于闪存,地址都是 40 开头的,说明这个存储器接口寄存器就是一个普通的外设,他的存储介质,也都是 SRAM。这个闪存存储器接口,就是上面这些闪存的管理员,这些寄存器,就是用来控制擦除和编程这个过程的。

    KEYR 键寄存器,SR 状态寄存器,CR 控制寄存器等等常用寄存器

    外设的起始地址是 0x4002 2000,每个寄存器都是 4 个字节,也就是 32 位

FLASH基本结构

image

这里程序存储器以 C8T6 为例,它是 64K 的,所以总共只有 64 页,最后一页的起始地址是 0800 FC00。

左边是闪存存储器接口,手册里还有个名称,闪存编程和擦除控制器(FPEC),这两个名称是一个东西。然后这个控制器,就是闪存的管理员,它可以对程序存储器进行擦除和编程,也可以对选项字节进行擦除和编程。当然系统存储器是不能擦除和编程的。

选项字节,里面有很大一部分配置位,其实是配置主程序存储器的读写保护的,所以右边画的,写入选项字节可以配置程序存储器的读写保护

FLASH解锁

  • FPEC共有三个键值:

    1. RDPRT键 = 0x000000A5

      解除读保护

    2. KEY1 = 0x45670123

    3. KEY2 = 0xCDEF89AB

      KEY1和KEY2解除写保护

  • 解锁:

    1. 复位后,FPEC被保护,不能写入FLASH_CR

    2. 在FLASH_KEYR先写入KEY1,再写入KEY2,解除写保护

      要先写入 KEY1 ,再写入 KEY2 ,最终才能解除写保护。

      所以这个锁的安全性非常高,有两道锁,即使你程序跑飞了也基本不可能解锁

    3. 错误的操作序列会在下次复位前锁死FPEC和FLASH_CR

      一旦没有先写入 KEY1,再写入 KEY2,整个模块就会完全锁死,除非复位

  • 加锁:设置FLASH_CR中的LOCK位锁住FPEC和FLASH_CR

    就是控制寄存器里面有个 LOCK 位,我们在这一位写 1,就能重新锁住闪存了。

使用指针访问存储器

#define    __IO    volatile

// 使用指针读指定地址下的存储器:
uint16_t Data = *((__IO uint16_t *)(0x08000000));

// 使用指针写指定地址下的存储器:
*((__IO uint16_t *)(0x08000000)) = 0x1234;

volatile,是一个安全保障措施,在程序逻辑上,没有作用,加上这个关键字的目的,用一句话来说,就是防止编译器优化。

闪存全擦除

image

把所有页,都给擦除掉。

  1. 读取 LOCK 位,看一下芯片锁没锁。如果 LOCK 位 = 1,R锁住了,就执行解锁过程,解锁过程就是在 KEYR 寄存器,先写入 KEY1,再写入 KEY2;但是在库函数中,并没有这个判断,库函数是直接执行解锁过程,管你锁没锁,都执行解锁,这个比较简单直接,不过效果都一样。
  2. 解锁之后,首先,置控制寄存器里的 MER(Mass Erase)位为 1,然后再置 STRT(Start)位为 1,其中置 STRT 为 1 是触发条件,STRT 为 1 之后,芯片开始干活,然后芯片看到 MER 位是 1,它就知道,接下来要干的活就是全擦除,这样内部电路就会自动执行全擦除的过程。
  3. 擦除也是需要花一段时间的,所以擦除过程开始后,程序要执行等待。判断状态寄存器的 BSY(Busy)位是否为 1,BSY 位表示芯片是否处于忙状态,BSY 为 1,表示芯片忙,所以这里,如果判断 BSY = 1,就跳转回来,继续循环判断,直到 BSY = 0,跳出循环,这样全擦除过程就结束了。
  4. 最后一步,这里写的是,读出并验证所有页的数据,这个是测试程序才要做的,正常情况下,全擦除完成了,我们默认就是成功了。如果还要再全读出来验证一下,这个工作量太大了,所以这里的最后一步,我们就不管了。

闪存页擦除

image

STM32 的闪存也是写入前必须擦除。擦除之后,所有的数据位变为 1,擦除的最小单位就是一页,1K,1024 字节。

这个也是类似的过程。

  1. 解锁的流程

  2. 置控制寄存器的 PER(Page Erase)位为 1,然后在 AR(Address Register)地址寄存器中选择要擦除的页,最后,置控制寄存器的 STRT 位为 1,置 STRT 为 1,也是触发条件

    STRT 为 1,芯片开始干活,然后芯片看到,PER = 1,他就知道接下来要执行页擦除,但是芯片要知道要具体擦哪一页,所以,它会继续看 AR 寄存器的数据,AR 寄存器我们要提前写入一个页的起始地址,这样芯片就会把我们指定的一页,给擦除掉。

  3. 然后擦除开始之后,我们也需要等待 BSY 位。

  4. 最后,读出并验证数据,这个就不用看了。

闪存写入

image

注意:STM32 的闪存再写入之前会检查指定地址有没有擦除,如果没有擦除就写入,STM32 则不执行写入操作,除非写入的全是 0,这一个数据是例外,因为不擦除就写入,可能会写入错误,但全写入 0 的话,写入肯定是没问题的。

  1. 第一步,也是解锁。

  2. 第二步,我们需要置控制寄存器的 PG(Programming)位为 1,表示我们即将写入数据。

  3. 第三步,在指定的地址写入半字,需要用到使用指针访问存储器的 *((__IO uint16_t *)(0x08000000)) = 0x1234; 这句代码在指定地址写入数据。写入数据这个代码,就是触发开始的条件,不需要像擦除一样,置 STRT 位了。

    注意:写入操作,只能以半字的形式写入。

    在 STM32 中,有几个术语,字、半字和字节,

    • 字,Word,是 32 位数据;

    • 半字,HalfWord,是 16 位数据;

    • 字节,Byte,是 8 位数据,

    那这里只能以半字写入,意思就是只能以 16 位的形式写入,一次性,写入两个字节;

    如果你要写入 32 位,就分两次完成;

    如果想单独写入一个字节,还要保留另一个字节的原始数据的话,就只能把整页数据都读到 SRAM,再随意修改 SRAM 数据,修改全部完成之后,再把整页都擦除,最后再把整页都写回去。

    所以,如果你想像 SRAM 一样随心所欲地读写,那最好的办法就是先把闪存的一页读到 SRAM 中,读写完成后,再擦除一页,整体写回去。那回到流程图这里

  4. 写入半字之后,芯片会处于忙状态,我们等待一下 BSY 清零,这样写入数据的过程就完成了。

选项字节

选项字节介绍

image

  • RDP:写入RDPRT键(0x000000A5)后解除读保护
  • USER:配置硬件看门狗和进入停机/待机模式是否产生复位
  • Data0/1:用户可自定义使用
  • WRP0/1/2/3:配置写保护,每一个位对应保护4个存储页(中容量)

这里是对应的 16 个字节,其中有一半的名称,前面都带了个 n,比如 RDP 和 nRDP,USER 和 nUSER,等等,这个意思就是你在写入 RDP 数据时,要同时在 nRDP 写入数据的反码,其他的这些都是一样,写入这个存储器时,要在带 n 的对应的存储器写入反码,这样写入操作才是有效的,如果芯片检测到这两个存储器不是反码的关系,那就代表数据无效,有错误,对应的功能就不执行,这是一个安全保障措施。

每个存储器的功能,去掉所有带 n 的,就剩下 8 个字节存储器了。

  • RDP(Read Protect),是读保护配置位,下面有解释,在 RDP 寄存器写入 RDPRT 键,就是刚才说的 A5,然后解除读保护;如果 RDP 不是 A5,那闪存就是读保护状态,无法通过调试器读取程序,避免程序被别人窃取。

  • USER,这个是一些零碎的配置位,可以配置硬件看门狗和进入停机/待机模式是否产生复位,这个了解即可。

  • 第三个和第四个字节,Data0 和 Data1,这个在芯片中没有定义功能,用户可自定义使用。

  • WRP(Write Protect)0、1、2、3,这四个字节,配置的是写保护。

    在中容量产品里,是每一个位对应保护 4 个存储页,4 个字节,总共 32 位,一位对应保护 4 页,总共保护 32*4 = 128 页,正好对应中容量的最大 128 页。

    对于小容量产品,也是每一位对应保护 4 个存储页,但是小容量产品最大只有 32K,所以只需要一个字节 WRP0 就行,4*8 = 32,其他 3 个字节没用到。

    对于大容量产品,每一个位只能保护 2 个存储页,这样的话 4 个字节就不够用了,所以这里规定 WRP3 的最高位,这一位直接把剩下的所有页一起都保护了,这是写保护的定义。

擦除选项字节

  1. 解锁闪存

  2. 检查FLASH_SR的BSY位,以确认没有其他正在进行的闪存操作

  3. 解锁FLASH_CR的OPTWRE位

    解锁 CR 的 OPTWRE(Option Write Enable)位,这一步是选项字节的解锁,选项字节里面还有一个单独的锁,在解锁闪存后,还需要再解锁选项字节的锁,之后才能操作选项字节。

    解锁选项字节的话,看一下闪存模块组织的寄存器,整个闪存的锁是 KEYR,里面选项字节的小锁,是下面的 OPTKEYR(Option Key Register),解锁这个小锁,也是类似的流程,我们需要在 OPTKEYR 里,先写入 KEY1,再写入 KEY2,这样就能解锁选项字节的小锁了。

  4. 设置FLASH_CR的OPTER位为1

  5. 设置FLASH_CR的STRT位为1

  6. 等待BSY位变为0

  7. 读出被擦除的选择字节并做验证

写入选项字节

  1. 解锁闪存
  2. 检查FLASH_SR的BSY位,以确认没有其他正在进行的编程操作
  3. 解锁FLASH_CR的OPTWRE位
  4. 设置FLASH_CR的OPTPG位为1
  5. 写入要编程的半字到指定的地址
  6. 等待BSY位变为0
  7. 读出写入的地址并验证数据

和普通的闪存写入也差不多,先检测 BSY;然后解除小锁;之后设置 CR 的 OPTPG(Option Programming)位为 1,表示即将写入选项字节;再之后,写入要编程的半字到指定的地址,这个是指针写入操作;最后,等待忙。这样写入选项字节就完成了。

器件电子签名

电子签名存放在闪存存储器模块的系统存储区域,包含的芯片识别信息在出厂时编写,不可更改,使用指针读指定地址下的存储器可获取电子签名

电子签名,其实就是 STM32 的 ID 号,它的存放区域是系统存储器,就是 FLASH 基本结构图中的系统存储器。它不仅有 BootLoader 程序,还有几个字节的 ID 号,系统存储器,起始地址是 1FFF F000。

  • 闪存容量寄存器:
    地址:0x1FFF F7E0
    大小:16位

  • 产品唯一身份标识寄存器:
    地址: 0x1FFF F7E8
    大小:96位

    也就是每个芯片的身份证号。每一个芯片的这 96 位数据,都是不一样的,使用这个唯一 ID 号,可以做一些加密的操作。

    比如你想写入一段程序,只能在指定设备运行,那就可以在程序的多处加入 ID 号判断,如果不是指定设备的 ID 号,就不执行程序功能,这样即使你的程序被盗,在别的设备上也难以运行。

FLSHA库函数

/*------------ 功能适用于所有STM32F10x设备 -----*/
/* 与内核运行代码有关 */
void FLASH_SetLatency(uint32_t FLASH_Latency);
void FLASH_HalfCycleAccessCmd(uint32_t FLASH_HalfCycleAccess);
void FLASH_PrefetchBufferCmd(uint32_t FLASH_PrefetchBuffer);

// 解锁。解除写保护
void FLASH_Unlock(void);

// 解锁。解除写保护
void FLASH_Lock(void);

// 擦除指定页
FLASH_Status FLASH_ErasePage(uint32_t Page_Address);

// 擦除全部
FLASH_Status FLASH_EraseAllPages(void);

// 擦除选项字节
FLASH_Status FLASH_EraseOptionBytes(void);

// 在指定地址写入字
FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);

// 在指定地址写入半字
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);

// 写入选项字节的Data0和Data1
FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data);

// 设置写保护
FLASH_Status FLASH_EnableWriteProtection(uint32_t FLASH_Pages);

// 设置读保护
FLASH_Status FLASH_ReadOutProtection(FunctionalState NewState);

// 设置选项字节中USER的三个位:IWDG_SW / RST_STOP / RST_STDBY。
FLASH_Status FLASH_UserOptionByteConfig(uint16_t OB_IWDG, uint16_t OB_STOP, uint16_t OB_STDBY);

// 获取选项字节中USER的三个位:IWDG_SW / RST_STOP / RST_STDBY。
uint32_t FLASH_GetUserOptionByte(void);

// 获取写保状态
uint32_t FLASH_GetWriteProtectionOptionByte(void);

// 获取读保护状态
FlagStatus FLASH_GetReadOutProtectionStatus(void);

// 获取预期缓冲区状态
FlagStatus FLASH_GetPrefetchBufferStatus(void);

// 中断使能
void FLASH_ITConfig(uint32_t FLASH_IT, FunctionalState NewState);

// 获取指定标志位
FlagStatus FLASH_GetFlagStatus(uint32_t FLASH_FLAG);

// 清除指定标志位
void FLASH_ClearFlag(uint32_t FLASH_FLAG);

// 获取状态
FLASH_Status FLASH_GetStatus(void);

// 等待忙。也就是等待BSY=0。调用擦除、写入等等库函数时,内部已经帮我们调用过该函数了,不需要我们再调用
FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout);

/*------------ 新功能用于所有STM32F10x器件 -----*/
void FLASH_UnlockBank1(void);
void FLASH_LockBank1(void);
FLASH_Status FLASH_EraseAllBank1Pages(void);
FLASH_Status FLASH_GetBank1Status(void);
FLASH_Status FLASH_WaitForLastBank1Operation(uint32_t Timeout);

#ifdef STM32F10X_XL
/*---- 新功能仅适用于STM32F10x_XL大容量器件 -----*/
void FLASH_UnlockBank2(void);
void FLASH_LockBank2(void);
FLASH_Status FLASH_EraseAllBank2Pages(void);
FLASH_Status FLASH_GetBank2Status(void);
FLASH_Status FLASH_WaitForLastBank2Operation(uint32_t Timeout);
FLASH_Status FLASH_BootConfig(uint16_t FLASH_BOOT);
#endif

案例:读写FLASH闪存

接线图
image

使用到的函数

// 解锁。解除写保护
void FLASH_Unlock(void);

// 解锁。解除写保护
void FLASH_Lock(void);

// 擦除指定页
FLASH_Status FLASH_ErasePage(uint32_t Page_Address);

// 擦除全部
FLASH_Status FLASH_EraseAllPages(void);

// 在指定地址写入字
FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);

// 在指定地址写入半字
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);

示例代码

main.c

#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "Store.h"
#include "Key.h"

// 键码
uint8_t Num;

int main()
{
	OLED_Init();
	Store_Init();
	Key_Init();
	OLED_ShowHexNum(1,1,Store_Data[1],4);
	OLED_ShowHexNum(2,1,Store_Data[2],4);
	OLED_ShowHexNum(3,1,Store_Data[3],4);
	OLED_ShowHexNum(4,1,Store_Data[4],4);
	while(1)
	{
		Num = Key_Num();
		if(Num == 1)
		{
			Store_Data[1]++;
			Store_Data[2]+=2;
			Store_Data[3]+=3;
			Store_Data[4]+=4;
			Store_Save();
		}
		
		if(Num == 2)
		{
			Store_Clear();
		}
		OLED_ShowHexNum(1,1,Store_Data[1],4);
		OLED_ShowHexNum(2,1,Store_Data[2],4);
		OLED_ShowHexNum(3,1,Store_Data[3],4);
		OLED_ShowHexNum(4,1,Store_Data[4],4);
	}
}

Flash.c

#include "stm32f10x.h"                  // Device header

// 读指定地址下的一个字节
uint8_t Flash_ReadByte(uint32_t Addr)
{
	return *((__IO uint8_t *)(Addr));
}

// 读指定地址下的一个半字
uint16_t Flash_ReadHalfWord(uint32_t Addr)
{
	return *((__IO uint16_t *)(Addr));
}

// 读指定地址下的一个字
uint32_t Flash_ReadWord(uint32_t Addr)
{
	return *((__IO uint32_t *)(Addr));
}

// 擦除指定页
void Flash_ErasurePage(uint32_t Addr)
{
	// 解锁
	FLASH_Unlock();
	// 擦除页
	FLASH_ErasePage(Addr);
	// 上锁
	FLASH_Lock();
}

// 擦除全部
void Flash_ErasureAll()
{
	// 解锁
	FLASH_Unlock();
	// 擦除全部
	FLASH_EraseAllPages();
	// 上锁
	FLASH_Lock();
}

// 在指定地址写入半字
void Flash_WriteHalfWord(uint32_t Addr, uint16_t Data)
{
	// 解锁
	FLASH_Unlock();
	// 在指定地址写入半字
	FLASH_ProgramHalfWord(Addr, Data);
	// 上锁
	FLASH_Lock();
}

// 在指定地址写入半字
void Flash_WriteWord(uint32_t Addr, uint32_t Data)
{
	// 解锁
	FLASH_Unlock();
	// 在指定地址写入字
	FLASH_ProgramWord(Addr, Data);
	// 上锁
	FLASH_Lock();
}

Flash.h

#ifndef __Flash_H
#define __Flash_H

uint8_t Flash_ReadByte(uint32_t Addr);
uint16_t Flash_ReadHalfWord(uint32_t Addr);
uint32_t Flash_ReadWord(uint32_t Addr);

void Flash_ErasurePage(uint32_t Addr);
void Flash_ErasureAll(void);

void Flash_WriteHalfWord(uint32_t Addr, uint16_t Data);
void Flash_WriteWord(uint32_t Addr, uint32_t Data);
#endif

Store.c

#include "stm32f10x.h"                  // Device header
#include "Flash.h"

#define STORE_START_ADDRESS		0x0800FC00	// 存储的起始地址
#define STORE_COUNT				512			// 存储数据的个数

// 将闪存中的数据以半字为单位读取到RAM中
uint16_t Store_Data[STORE_COUNT];

// 初始化
void Store_Init()
{
	/*判断是不是第一次使用*/
	// 读取第一个半字的标志位,if不成立,则执行第一次使用的初始化
	if(Flash_ReadHalfWord(STORE_START_ADDRESS) == 0x7777)
	{
		// 将存储区域的所有数据都读到Store_Data数组中
		for(uint16_t i = 0;i < STORE_COUNT; i++)
		{
			Store_Data[i] = Flash_ReadHalfWord(STORE_START_ADDRESS + (i * 2));
		}
	}
	else
	{
		// 擦除指定页
		Flash_ErasurePage(STORE_START_ADDRESS);
		// 在存储的起始位置写入自定义的标志位。下次检测到该标志位说明不是第一次使用
		Flash_WriteHalfWord(STORE_START_ADDRESS, 0x7777);
		
		// 将存储区域的数据全部置0,除了标志位,因此i从1开始
		for(uint16_t i = 1;i < STORE_COUNT; i++)
		{
			// 每个地址对应一个字节,而这里是两个字节进行存储,所以i*2。如果四个字节进行存储则乘以四
			Flash_WriteHalfWord(STORE_START_ADDRESS + (i * 2), 0x0000);
		}
	}
}

// 保存数据
void Store_Save()
{
	// 擦除页
	Flash_ErasurePage(STORE_START_ADDRESS);
	// 将数组Store_Data的数据全部写入闪存中
	for(uint16_t i = 0;i < STORE_COUNT; i++)
	{
		Flash_WriteHalfWord(STORE_START_ADDRESS +(i * 2), Store_Data[i]);
	}
}

// 数据清零
void Store_Clear()
{
	// 将除了标志位以外的数据全部清零
	for(uint16_t i = 1;i < STORE_COUNT;i++)
	{
		Store_Data[i] = 0;
	}
	Store_Save();
}

Store.h

#ifndef __Store_H
#define __Store_H

extern uint16_t Store_Data[];

void Store_Init(void);
void Store_Save(void);
void Store_Clear(void);

#endif

Key.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

// 初始化按键
void Key_Init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;	// 上拉输入
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStruct);
}

// 获取按键键码
uint8_t Key_Num()
{
	uint8_t Num = 0;
	// PB11=0说明按键1按下
	if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == Bit_RESET)
	{
		Delay_ms(20);
		while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) == Bit_RESET);
		Num = 1;
	}
	
	// PB1=0说明按键2按下
	if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == Bit_RESET)
	{
		Delay_ms(20);
		while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == Bit_RESET);
		Num = 2;
	}
	return Num;
}

Key.h

#ifndef __Key_H
#define __Key_H

void Key_Init(void);
uint8_t Key_Num(void);

#endif

posted @ 2024-03-08 04:04  7七柒  阅读(21)  评论(0编辑  收藏  举报