十六、FLASH闪存
十四、FLASH闪存
FLASH简介
-
STM32F1系列的FLASH包含程序存储器、系统存储器和选项字节三个部分,通过闪存存储器接口(外设)可以对程序存储器和选项字节进行擦除和编程
闪存存储器接口是一个外设,是这个闪存的管理员,把我们的指令和数据,写入到这个外设的相应寄存器,然后这个外设就会自动去操作对应的存储空间。
-
读写FLASH的用途:
-
利用程序存储器的剩余空间来保存掉电不丢失的用户数据
它的程序存储器容量是 64K,一般我们写个简单的程序,可能就只占前面的很小一部分空间,剩下的大片空余空间,我们就可以加以利用。比如存储一些我们自定义的数据
注意:我们在选取存储区域时,一定不要覆盖了原有的程序,要不然程序自己把自己给破坏了,之后程序就运行不了了。
-
通过在程序中编程(IAP),实现程序的自我更新
利用程序,来修改程序本身,实现程序的自我更新。
-
-
在线编程(In-Circuit Programming – ICP)用于更新程序存储器的全部内容,它通过JTAG、SWD协议或系统加载程序(Bootloader)下载程序
-
在程序中编程(In-Application Programming – IAP)可以使用微控制器支持的任一种通信接口下载程序
首先需要自己写一个 BootLoader 程序,并且存放在程序更新时不会覆盖的地方,比如我们放在整个程序存储器的后面。
需要更新程序时,我们控制程序跳转到这个自己写的 BootLoader 里来,在这里面,我们就可以接收任意一种通信接口传过来的数据,比如串口、USB、蓝牙转串口、WIFI 转串口等等,这个传过来的数据,就是待更新的程序
控制 FLASH 读写,把收到的程序,写入到整个程序存储器的前面程序正常运行的地方,写完之后,再控制程序跳转回正常运行的地方,或者直接复位,这样程序就完成了自我升级。
注意:在编程过程中,任何读写闪存的操作都会使 CPU 暂停,直到此次闪存编程结束。这其实是读写内部闪存存储数据的一个弊端,就是闪存忙的时候,代码执行会暂停,因为执行代码需要读闪存,闪存在忙,没法读,所以 CPU 也就没法运行了,程序就会暂停,这会导致什么问题呢?假如你使用内部闪存存储数据,同时你的中断代码又是在频繁执行的,这样,读写闪存的时候,中断代码就无法执行了,这可能会导致中断无法及时响应
闪存模块组织(中容量)
这个表是中容量产品的闪存分配情况。我们 C8T6 芯片的闪存容量是 64K,属于中容量产品。对于小容量产品和大容量产品,闪存的分配方式有些区别,这个可以参考一下手册。
首先看一下第一列的几个块,这里分为了 3 个块,
-
主存储器:也就是程序存储器,用来存放程序代码的,这是最主要,也是容量最大的一块。
对他进行了分页,分页是为了更好的管理闪存。每页的大小都是 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 结尾的都一定是页的起始地址,之后如果想要给一个页的起始地址,就需要用到这个规律。
-
信息块
-
启动程序代码:存放的是原厂写入的 BootLoader,用于串口下载
起始地址是 0x1FFF F000,他的容量是 2K
-
用户选择字节:也就是选项字节,存放一些独立的参数,这个选项字节
起始地址是 0x1FFF F800,容量是 16 个字节,里面只有几个字节的配置参数,这个后面还会继续说的
-
-
闪存存储器接口寄存器:这一块的存储器,实际上并不属于闪存,地址都是 40 开头的,说明这个存储器接口寄存器就是一个普通的外设,他的存储介质,也都是 SRAM。这个闪存存储器接口,就是上面这些闪存的管理员,这些寄存器,就是用来控制擦除和编程这个过程的。
KEYR 键寄存器,SR 状态寄存器,CR 控制寄存器等等常用寄存器
外设的起始地址是 0x4002 2000,每个寄存器都是 4 个字节,也就是 32 位
FLASH基本结构
这里程序存储器以 C8T6 为例,它是 64K 的,所以总共只有 64 页,最后一页的起始地址是 0800 FC00。
左边是闪存存储器接口,手册里还有个名称,闪存编程和擦除控制器(FPEC),这两个名称是一个东西。然后这个控制器,就是闪存的管理员,它可以对程序存储器进行擦除和编程,也可以对选项字节进行擦除和编程。当然系统存储器是不能擦除和编程的。
选项字节,里面有很大一部分配置位,其实是配置主程序存储器的读写保护的,所以右边画的,写入选项字节可以配置程序存储器的读写保护
FLASH解锁
-
FPEC共有三个键值:
-
RDPRT键 = 0x000000A5
解除读保护
-
KEY1 = 0x45670123
-
KEY2 = 0xCDEF89AB
KEY1和KEY2解除写保护
-
-
解锁:
-
复位后,FPEC被保护,不能写入FLASH_CR
-
在FLASH_KEYR先写入KEY1,再写入KEY2,解除写保护
要先写入 KEY1 ,再写入 KEY2 ,最终才能解除写保护。
所以这个锁的安全性非常高,有两道锁,即使你程序跑飞了也基本不可能解锁
-
错误的操作序列会在下次复位前锁死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,是一个安全保障措施,在程序逻辑上,没有作用,加上这个关键字的目的,用一句话来说,就是防止编译器优化。
闪存全擦除
把所有页,都给擦除掉。
- 读取 LOCK 位,看一下芯片锁没锁。如果 LOCK 位 = 1,R锁住了,就执行解锁过程,解锁过程就是在 KEYR 寄存器,先写入 KEY1,再写入 KEY2;但是在库函数中,并没有这个判断,库函数是直接执行解锁过程,管你锁没锁,都执行解锁,这个比较简单直接,不过效果都一样。
- 解锁之后,首先,置控制寄存器里的 MER(Mass Erase)位为 1,然后再置 STRT(Start)位为 1,其中置 STRT 为 1 是触发条件,STRT 为 1 之后,芯片开始干活,然后芯片看到 MER 位是 1,它就知道,接下来要干的活就是全擦除,这样内部电路就会自动执行全擦除的过程。
- 擦除也是需要花一段时间的,所以擦除过程开始后,程序要执行等待。判断状态寄存器的 BSY(Busy)位是否为 1,BSY 位表示芯片是否处于忙状态,BSY 为 1,表示芯片忙,所以这里,如果判断 BSY = 1,就跳转回来,继续循环判断,直到 BSY = 0,跳出循环,这样全擦除过程就结束了。
- 最后一步,这里写的是,读出并验证所有页的数据,这个是测试程序才要做的,正常情况下,全擦除完成了,我们默认就是成功了。如果还要再全读出来验证一下,这个工作量太大了,所以这里的最后一步,我们就不管了。
闪存页擦除
STM32 的闪存也是写入前必须擦除。擦除之后,所有的数据位变为 1,擦除的最小单位就是一页,1K,1024 字节。
这个也是类似的过程。
-
解锁的流程
-
置控制寄存器的 PER(Page Erase)位为 1,然后在 AR(Address Register)地址寄存器中选择要擦除的页,最后,置控制寄存器的 STRT 位为 1,置 STRT 为 1,也是触发条件
STRT 为 1,芯片开始干活,然后芯片看到,PER = 1,他就知道接下来要执行页擦除,但是芯片要知道要具体擦哪一页,所以,它会继续看 AR 寄存器的数据,AR 寄存器我们要提前写入一个页的起始地址,这样芯片就会把我们指定的一页,给擦除掉。
-
然后擦除开始之后,我们也需要等待 BSY 位。
-
最后,读出并验证数据,这个就不用看了。
闪存写入
注意:STM32 的闪存再写入之前会检查指定地址有没有擦除,如果没有擦除就写入,STM32 则不执行写入操作,除非写入的全是 0,这一个数据是例外,因为不擦除就写入,可能会写入错误,但全写入 0 的话,写入肯定是没问题的。
-
第一步,也是解锁。
-
第二步,我们需要置控制寄存器的 PG(Programming)位为 1,表示我们即将写入数据。
-
第三步,在指定的地址写入半字,需要用到使用指针访问存储器的
*((__IO uint16_t *)(0x08000000)) = 0x1234;
这句代码在指定地址写入数据。写入数据这个代码,就是触发开始的条件,不需要像擦除一样,置 STRT 位了。注意:写入操作,只能以半字的形式写入。
在 STM32 中,有几个术语,字、半字和字节,
-
字,Word,是 32 位数据;
-
半字,HalfWord,是 16 位数据;
-
字节,Byte,是 8 位数据,
那这里只能以半字写入,意思就是只能以 16 位的形式写入,一次性,写入两个字节;
如果你要写入 32 位,就分两次完成;
如果想单独写入一个字节,还要保留另一个字节的原始数据的话,就只能把整页数据都读到 SRAM,再随意修改 SRAM 数据,修改全部完成之后,再把整页都擦除,最后再把整页都写回去。
所以,如果你想像 SRAM 一样随心所欲地读写,那最好的办法就是先把闪存的一页读到 SRAM 中,读写完成后,再擦除一页,整体写回去。那回到流程图这里
-
-
写入半字之后,芯片会处于忙状态,我们等待一下 BSY 清零,这样写入数据的过程就完成了。
选项字节
选项字节介绍
- 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 的最高位,这一位直接把剩下的所有页一起都保护了,这是写保护的定义。
擦除选项字节
-
解锁闪存
-
检查FLASH_SR的BSY位,以确认没有其他正在进行的闪存操作
-
解锁FLASH_CR的OPTWRE位
解锁 CR 的 OPTWRE(Option Write Enable)位,这一步是选项字节的解锁,选项字节里面还有一个单独的锁,在解锁闪存后,还需要再解锁选项字节的锁,之后才能操作选项字节。
解锁选项字节的话,看一下闪存模块组织的寄存器,整个闪存的锁是 KEYR,里面选项字节的小锁,是下面的 OPTKEYR(Option Key Register),解锁这个小锁,也是类似的流程,我们需要在 OPTKEYR 里,先写入 KEY1,再写入 KEY2,这样就能解锁选项字节的小锁了。
-
设置FLASH_CR的OPTER位为1
-
设置FLASH_CR的STRT位为1
-
等待BSY位变为0
-
读出被擦除的选择字节并做验证
写入选项字节
- 解锁闪存
- 检查FLASH_SR的BSY位,以确认没有其他正在进行的编程操作
- 解锁FLASH_CR的OPTWRE位
- 设置FLASH_CR的OPTPG位为1
- 写入要编程的半字到指定的地址
- 等待BSY位变为0
- 读出写入的地址并验证数据
和普通的闪存写入也差不多,先检测 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闪存
接线图
使用到的函数
// 解锁。解除写保护
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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具