【普中STM32F1xx开发攻略--标准库版】-- 第 42 章 STM32 内部 FLASH 实验

(1)实验平台:

普中STM32F103 朱雀、玄武开发板https://item.taobao.com/item.htm?id=620302685024(2)资料下载 :普中科技-各型号产品资料下载链接


上一章我们介绍了 STM32F1 的 SPI 与外部 FLASH 通信, 实现了外部 FLASH数据的读写操作, 这一章我们来学习下 STM32F1 内部的 FLASH, 通过内部 FLASH实现数据读写操作, 同上一章实验效果一样, 内部 FLASH 保存的数据也具有掉电不丢失功能。 本章要实现的功能是: 使用 KEY_UP 和 KEY1 键控制内部 FLASH 的写入和读取, 并将数据显示在 TFTLCD 和串口助手上, 同时控制 DS0 指示灯不断闪烁, 提示系统正常运行。 本章分为如下几部分内容:

[42.1 STM32F1 内部 FLASH 介绍](#42.1 STM32F1 内部 FLASH 介绍)

[42.2 内部 FLASH 操作步骤](#42.2 内部 FLASH 操作步骤)

[42.3 硬件设计](#42.3 硬件设计)

[42.4.1 FLASH 读数据函数](#42.4.1 FLASH 读数据函数)

[42.4.2 FLASH 写数据函数](#42.4.2 FLASH 写数据函数)

[42.4.3 主函数](#42.4.3 主函数)

[42.5 实验现象](#42.5 实验现象)

课后作业


42.1 STM32F1 内部 FLASH 介绍

不同型号的 STM32, 其 FLASH 容量也有所不同, 最小的只有 16K 字节, 最大的则达到了 1024K 字节。 我们 STM32F1 开发板使用的芯片是 STM32F103ZET6,其 FLASH 容量为 512K 字节, 属于大容量芯片。 大容量产品的 Flash 模块组织结构如下图所示:

STM32F1 的闪存(Flash) 模块由: 主存储器、 信息块和闪存存储器接口寄存器等 3 部分组成。 下面我们就来介绍下这些组成部分:

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

②信息块: 该部分分为 2 个小部分, 其中启动程序代码, 是用来存储 ST 自带的启动程序, 用于串口下载代码, 当 BOOT0 接 V3.3, BOOT1 接 GND 的时候,运行的就是这部分代码。 用户选择字节, 则一般用于配置写保护、 读保护等功能,这里我们不作介绍, 大家可以百度了解。

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

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

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

下面我们就来看下如何对闪存进行读取、 编程和擦除。

(1) 闪存的读取

STM32F1 可通过内部的 I-Code 指令总线或 D-Code 数据总线访问内置闪存模块, 本章我们主要讲解数据读写, 即通过 D-Code 数据总线来访问内部闪存模块。 为了准确读取 Flash 数据, 必须根据 CPU 时钟 (HCLK) 频率和器件电源电压在 Flash 存取控制寄存器 (FLASH_ACR)中正确地设置等待周期数(LATENCY)。 当电源电压低于 2.1V 时, 必须关闭预取缓冲器。 Flash 等待周期与 CPU 时钟频率之间的对应关系, 如下图所示:

等待周期通过 FLASH_ACR 寄存器的 LATENCY2:0三个位设置。 系统复位后, CPU 时钟频率为内部 16M RC 振荡器, LATENCY 默认是 0, 即 1 个等待周期。 供电电压, 我们一般是 3.3V, 所以, 在我们设置 72Mhz 频率作为 CPU 时钟之前, 必须先设置 LATENCY 为 3, 否则 FLASH 读写可能出错, 导致死机。

STM23F1 的 FLASH 读取是很简单的。 例如, 我们要从地址 addr, 读取一个字(字节为 8 位, 半字为 16 位, 字为 32 位) , 可以使用如下方法来读取:

data=*(vu32*)addr;

将 addr 强制转换为 vu32 指针, 然后取该指针所指向的地址的值, 即得到了 addr 地址内的值。 类似的, 将上面的 vu32 改为 vu16, 即可读取指定地址的一个半字。 相对 FLASH 读取来说, STM32F1 FLASH 的写就复杂一点了, 下面我们介绍 STM32F1 闪存的编程和擦除。

(2) 闪存的编程和擦除

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 位将得到一个警告。

STM32 的 FLASH 编程过程如下:

从上图可以得到闪存的编程顺序如下:

① 检查 FLASH_CR 的 LOCK 是否解锁, 如果没有则先解锁

② 检查 FLASH_SR 寄存器的 BSY 位, 以确认没有其他正在进行的编程操作

③ 设置 FLASH_CR 寄存器的 PG 位为' 1'

④ 在指定的地址写入要编程的半字

⑤ 等待 BSY 位变为' 0'

⑥ 读出写入的地址并验证数据

前面提到, 我们在 STM32 的 FLASH 编程的时候, 要先判断缩写地址是否被擦除了, 所以我们有必要再介绍一下 STM32 的闪存擦除, STM32 的闪存擦除分为两种: 页擦除和整片擦除。 页擦除过程如下所示:

从上图可以看出, STM32 的页擦除顺序为:

①检查 FLASH_CR 的 LOCK 是否解锁, 如果没有则先解锁

②检查 FLASH_SR 寄存器中的 BSY 位, 确保当前未执行任何 FLASH 操作

③设置 FLASH_CR 寄存器的 PER 位为' 1'

④用 FLASH_AR 寄存器选择要擦除的页

⑤设置 FLASH_CR 寄存器的 STRT 位为' 1'

⑥等待 BSY 位变为' 0'

⑦读出被擦除的页并做验证

我们只用到了 STM32 的页擦除功能, 整片擦除功能我们在这里就不介绍了。通过以上了解, 我们基本上知道了 STM32 闪存的读写所要执行的步骤了, 由于篇幅限制, 本章并没有 STM32F1 内部 FLASH 相关寄存器进行介绍, 大家可以参考"\8--STM32 相关资料\STM32F10xxx 闪存编程参考手册" 内容, 里面有详细的讲解

42.2 内部 FLASH 操作步骤

通过上面的介绍, 我们基本上知道了 STM32F1 闪存的读写所要执行的步骤了, 接下来, 我们就来介绍如何使用库函数对它进行操作。 这个也是在编写程序中必须要了解的。 具体步骤如下:(闪存 Flash 相关库函数在 stm32f10x_flash.c和 stm32f10x_flash.h 文件中)

(1) 解锁和锁定

前面我们介绍了在对 FLASH 进行写操作前必须先解锁, 解锁操作也就是必须在 FLASH_KEYR 寄存器写入特定的序列(0X45670123 和 0XCDEF89AB) ,固件库提供了一个解锁函数, 其实就是封装了对 FLASH_KEYR 寄存器的操作。

解锁库函数是:

cpp 复制代码
void FLASH_Unlock(void);

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

cpp 复制代码
void FLASH_Lock(void);

(2) 写操作

FLASH 解锁后, 我们就可以开始写操作, 固件库内提供了 3 个 FLASH 写函数:

cpp 复制代码
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);

从函数名来看也很好理解这几个函数的功能, 分别是在对应的地址 Address内写入字, 半字, 用户选项字节。 这里需要说明, 32 位字节写入实际上是写入的两次 16 位数据, 写完第一次后地址+2, 这与我们前面讲解的 STM32 闪存的编程每次必须写入 16 位并不矛盾。 写入 8 位实际也是占用的两个地址了, 跟写入 16 位基本上没啥区别。 这些函数的内部实现过程, 实际就是按照我们介绍的编程步骤来实现的。 有兴趣的同学可以进入函数体看看, 这样会加深理解。

(3) 擦除操作

在对 FLASH 写操作的时候, 还会用到 FLASH 擦除操作, 固件库内也提供了 3个 FLASH 擦除函数:

cpp 复制代码
FLASH_Status FLASH_ErasePage(uint32_t Page_Address);
FLASH_Status FLASH_EraseAllPages(void);
FLASH_Status FLASH_EraseOptionBytes(void);

对于前面两个函数比较好理解, 第一个函数是页擦除函数, 根据页地址擦除特定的页数据, 第二个函数是擦除所有的页数据, 第三个函数是擦除用户选择字节数据。

(4) 获取 FLASH 状态

在对 FLASH 进行读写及擦除操作时, 我们可能需要获取 FLASH 当前的状态,获取 FLASH 状态主要调用的函数是:

cpp 复制代码
FLASH_Status FLASH_GetStatus(void);

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

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

从这里面我们可以看到 FLASH 操作的几个状态。

(5) 等待操作完成

在执行闪存写操作时, 任何对闪存的读操作都会锁住总线, 在写操作完成后读操作才能正确地进行; 即在进行写或擦除操作时, 不能进行代码或数据的读取操作。 所以在每次操作之前, 我们都要等待上一次操作完成这次操作才能开始。使用的函数是:

cpp 复制代码
FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout);

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

(6) 读取 FLASH 指定地址数据

读取 FLASH 指定地址的数据函数固件库并没有提供, 因此这一步操作我们需要自己编写, 其实很简单, 通过 C 语言指针即可完成, 这里我们提供一个从指定地址读取一个字的函数:

cpp 复制代码
//读取指定地址的半字(16 位数据)
//faddr:读地址(此地址必须为 2 的倍数!!)
//返回值:对应数据.
vu16 STM32_FLASH_ReadHalfWord(u32 faddr)
{
    return *(vu16*)faddr;
}

通过以上几个步骤的操作, 我们就可以对 STM32F1 内部 FLASH 进行读写数据了

42.3 硬件设计

本实验使用到硬件资源如下:

(1) DS0 指示灯

(2) KEY_UP 和 KEY1 按键

(3) 串口 1

(4) TFTLCD 模块

(5) STM32F1 内部 FLASH

DS0 指示灯、 KEY_UP 和 KEY1 按键、 串口 1、 TFTLCD 模块电路在前面章节都介绍过, 这里就不多说, 至于 STM32F1 内部 FLASH 它属于 STM32F1 芯片内部的资源, 只要通过软件配置好即可使用。 DS0 指示灯用来提示系统运行状态, KEY_UP和 KEY1 按键用来控制内部 FLASH 数据的读写, TFTLCD 模块和串口 1 用来显示读写的数据。

42.4 软件设计

本章所要实现的功能是: 使用 KEY_UP 和 KEY1 键控制内部 FLASH 的写入和读取, 并将数据显示在 TFTLCD 和串口助手上, 同时控制 DS0 指示灯不断闪烁, 提示系统正常运行。 本章我们使用的是 STM32F1 的内部 FLASH, 程序框架如下:

(1) 编写 FLASH 读数据函数

(2) 编写 FLASH 写数据函数

(3) 编写主函数

前面我们已经介绍如何操作内部 FLASH, 这里并没有像以前章节那样要初始化, 因为我们只需要对对应的 FLASH 地址操作即可。 下面我们打开"\4--实验程序\1--基础实验\34-STM32 内部 Flash 实验" 工程, 在 APP 工程组中可以看到添加了 stm32_flash.c 文件(里面包含了内部 FLASH 驱动程序) , 在StdPeriph_Driver 工程组中添加了 stm32f10x_flash.c 库文件。 FLASH 操作的库函数都放在 stm32f10x_flash.c 和 stm32f10x_flash.h 文件中, 所以使用到内部FLASH 就必须加入 stm32f10x_flash.c 文件, 同时还要包含对应的头文件路径。

这里我们分析几个重要函数, 其他部分程序大家可以打开工程查看。

42.4.1 FLASH 读数据函数

在前面介绍 FLASH 操作步骤时, 我们就讲解如何读取 FLASH 地址内数据, 其实很简单, 使用指针操作即可, 为了能够在 FLASH 任意地址读取任意个数字数据,我们也对这个读取函数进行封装, 代码如下:

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

函数参数有 3 个, 第一个为要读取数据的起始地址, 第二个用来保存读取数据(通常使用一个数组来保存) , 第三个为要读取的数据半字个数, 注意: 这里读取的是半字, 即一个半字是 2 个字节, 所以函数内每读取一个半字, 地址就增加 2。

函数内调用了 STM32_FLASH_ReadHalfWord 函数, 这个函数其实就是前面我们介绍的读取地址内数据方法, 此函数代码如下:

cpp 复制代码
//读取指定地址的半字(16 位数据)
//faddr:读地址(此地址必须为 2 的倍数!!)
//返回值:对应数据.
vu16 STM32_FLASH_ReadHalfWord(u32 faddr)
{
    return *(vu16*)faddr;
}

42.4.2 FLASH 写数据函数

FLASH 有读操作, 当然也就是写操作, FLASH 的写操作比较复杂, 我们也对FLASH 写操作使用一个函数封装, 代码如下:

cpp 复制代码
//从指定地址开始写入指定长度的数据
//WriteAddr:起始地址(此地址必须为2的倍数)
//pBuffer:数据指针
//NumToWrite:半字(16位)数(就是要写入的16位数据的个数.)
#if STM32_FLASH_SIZE<256
	#define STM32_SECTOR_SIZE 1024 //字节
#else 
	#define STM32_SECTOR_SIZE	2048
#endif		 
u16 STM32_FLASH_BUF[STM32_SECTOR_SIZE/2];//最多是2K字节
void STM32_FLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)	
{
	u32 secpos;	   //扇区地址
	u16 secoff;	   //扇区内偏移地址(16位字计算)
	u16 secremain; //扇区内剩余地址(16位字计算)	   
 	u16 i;    
	u32 offaddr;   //去掉0X08000000后的地址
	if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return;//非法地址
	FLASH_Unlock();						//解锁
	offaddr=WriteAddr-STM32_FLASH_BASE;		//实际偏移地址.
	secpos=offaddr/STM32_SECTOR_SIZE;			//扇区地址  0~127 for STM32F103RBT6
	secoff=(offaddr%STM32_SECTOR_SIZE)/2;		//在扇区内的偏移(2个字节为基本单位.)
	secremain=STM32_SECTOR_SIZE/2-secoff;		//扇区剩余空间大小   
	if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围
	while(1) 
	{	
		STM32_FLASH_Read(secpos*STM32_SECTOR_SIZE+STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE/2);//读出整个扇区的内容
		for(i=0;i<secremain;i++)//校验数据
		{
			if(STM32_FLASH_BUF[secoff+i]!=0XFFFF)
				break;//需要擦除  	  
		}
		if(i<secremain)//需要擦除
		{
			FLASH_ErasePage(secpos*STM32_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区
			for(i=0;i<secremain;i++)//复制
			{
				STM32_FLASH_BUF[i+secoff]=pBuffer[i];	  
			}
			STM32_FLASH_Write_NoCheck(secpos*STM32_SECTOR_SIZE+STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE/2);//写入整个扇区  
		}
		else 
			STM32_FLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接写入扇区剩余区间. 				   
		if(NumToWrite==secremain)
			break;//写入结束了
		else//写入未结束
		{
			secpos++;				//扇区地址增1
			secoff=0;				//偏移位置为0 	 
		   	pBuffer+=secremain;  	//指针偏移
			WriteAddr+=secremain;	//写地址偏移	   
		   	NumToWrite-=secremain;	//字节(16位)数递减
			if(NumToWrite>(STM32_SECTOR_SIZE/2))
				secremain=STM32_SECTOR_SIZE/2;//下一个扇区还是写不完
			else 
				secremain=NumToWrite;//下一个扇区可以写完了
		}	 
	}	
	FLASH_Lock();//上锁
}

此函数也有 3 参数, 意义同上一个读取数据函数一样, 用于指定地址写入指定长度的数据。 该函数的实现基本类似上一章的 EN25QXX_Flash_Write 函数,不过该函数使用的时候, 有 2 个地方需要注意:

①写入地址必须是用户代码区以外的地址。

如果你写入数据的地址在存储用户代码地址范围内, 那将导致你的代码被冲掉, 可想而知你运行的程序可能就被破坏, 从而很可能出现死机的情况。 所以建议大家使用该函数的时候, 写入地址定位到用户代码占用扇区以外的扇区, 比较保险。 通常选择后面几个扇区。

②写入地址必须是 2 的倍数, 即每次写入必须是 16 位。

42.4.3 主函数

编写好内部 FLASH 读写函数后, 接下来就可以编写主函数了, 代码如下:

cpp 复制代码
#include "system.h"
#include "SysTick.h"
#include "led.h"
#include "usart.h"
#include "tftlcd.h"
#include "key.h"
#include "stm32_flash.h"



#define STM32_FLASH_SAVE_ADDR  0X08070000		//设置FLASH 保存地址(必须为偶数,且其值要大于本代码所占用FLASH的大小+0X08000000)

const u8 text_buf[]="www.prechin.net";
#define TEXTLEN sizeof(text_buf)


int main()
{
	u8 i=0;
	u8 key;
	u8 read_buf[TEXTLEN];
	
	SysTick_Init(72);
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  //中断优先级分组 分2组
	LED_Init();
	USART1_Init(115200);
	TFTLCD_Init();			//LCD初始化
	KEY_Init();
	
	FRONT_COLOR=BLACK;
	LCD_ShowString(10,10,tftlcd_data.width,tftlcd_data.height,16,"PRECHIN STM32F1");
	LCD_ShowString(10,30,tftlcd_data.width,tftlcd_data.height,16,"www.prechin.net");
	LCD_ShowString(10,50,tftlcd_data.width,tftlcd_data.height,16,"STM32_Flash Test");
	LCD_ShowString(10,70,tftlcd_data.width,tftlcd_data.height,16,"K_UP:Write   KEY1:Read");
	
	FRONT_COLOR=RED;
	LCD_ShowString(10,130,tftlcd_data.width,tftlcd_data.height,16,"Write:");
	LCD_ShowString(10,150,tftlcd_data.width,tftlcd_data.height,16,"Read :");
	
	while(1)
	{
		key=KEY_Scan(0);
		if(key==KEY_UP_PRESS)
		{
			STM32_FLASH_Write(STM32_FLASH_SAVE_ADDR,(u16*)text_buf,TEXTLEN);
			printf("写入数据为:%s\r\n",text_buf);
			LCD_ShowString(10+6*8,130,tftlcd_data.width,tftlcd_data.height,16,(u8 *)text_buf);
		}
		if(key==KEY1_PRESS)
		{
			STM32_FLASH_Read(STM32_FLASH_SAVE_ADDR,(u16 *)read_buf,TEXTLEN);
			printf("读取数据为:%s\r\n",read_buf);
			LCD_ShowString(10+6*8,150,tftlcd_data.width,tftlcd_data.height,16,read_buf);
		}
		
		i++;
		if(i%20==0)
		{
			LED1=!LED1;
		}
		
		delay_ms(10);
			
	}
}

主函数实现的功能很简单, 首先调用之前编写好的硬件初始化函数, 包括SysTick 系统时钟, 中断分组, LED 初始化等。 然后在 TFTLCD 上显示一些提示信息。 最后进入 while 循环, 调用 KEY_Scan 函数, 不断检测 KEY_UP 和 KEY1 按键是否按下, 如果 KEY_UP 键按下, 就将 text_buf 数组内的数据从内部 FLASH 的STM32_FLASH_SAVE_ADDR 地址处开始写入, STM32_FLASH_SAVE_ADDR 是我们定义的一个宏, 地址为 0X08070000。 这里需要注意: 写入的起始地址必须为偶数,且所在扇区要大于代码所占用到的扇区, 否则写操作的时候可能会导致擦除整个扇区, 从而引起部分程序丢失引起死机。 如果 KEY1 键按下, 就从STM32_FLASH_SAVE_ADDR 地址处读取数据, 保存在 read_buf 数组内, FLASH 写入和读取的数据在 TFTLCD 上显示, 并可通过串口打印输出, 同时 DS0 指示灯会间隔 200ms 闪烁, 提示系统正常运行。

42.5 实验现象

将工程程序编译后下载到开发板内, 可以看到 DS0 指示灯不断闪烁, 表示程序正常运行。 当按下 KEY_UP 键, 写入到数据显示在 TFTLCD 上, 当按下 KEY1 键将写入的数据读取出来, 同时显示在 TFTLCD 上。 如果想在串口调试助手上看到输出信息, 必须设置好串口助手, 前面很多章节都介绍, 这里不多说。 实验现象如下图所示:

实验说明: 如果自己编写程序操作内部 FLASH 时一定要小心, 避免把芯片锁死。 如果芯片锁死的话是很麻烦的。

课后作业

(1) 使用内部 FLASH 实现上一章"课后作业" 的功能。

相关推荐
不做无法实现的梦~2 小时前
CLion+pyocd配置教程
嵌入式硬件
破晓单片机4 小时前
012、STM32项目分享:智能台灯系统
stm32·单片机·嵌入式硬件
悠哉悠哉愿意4 小时前
【单片机复习笔记】十五届国赛复盘
笔记·单片机·嵌入式硬件·学习
是温不嗜温4 小时前
芯茂微 LP7012 双重过流保护机制拆解:DESAT 单次锁存 vs OCP 连续 5 次锁存有何区别?
嵌入式硬件·开闭原则·电源管理·电源芯片·ac-dc
HPT_Lt4 小时前
ZCC5146 支持100V宽压多功能同步降压控制器,兼容LM5146
嵌入式硬件
DS小龙哥5 小时前
基于ESP32-S3设计的智能人脸识别门禁系统
stm32·单片机·嵌入式硬件
一棵树73515 小时前
信号与通信
单片机·嵌入式硬件
JNX_SEMI5 小时前
Hi6000C可与H6912直接对标,管脚完全兼容
单片机·嵌入式硬件·物联网·硬件工程
zlinear数据采集卡5 小时前
LC滤波电路深度解析:从电容与电感的“强强联合”到ZLinear采集卡的电源净化实战
单片机·嵌入式硬件