发新帖本帖赏金 20.00元(功能说明)我要提问
返回列表
打印
[STM32F1]

【STM32F103】神操作--如何快速读写内部flash?

[复制链接]
27773|10
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
binoo7|  楼主 | 2021-9-17 13:24 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 binoo7 于 2021-9-18 15:06 编辑

#申请原创# @21小跑堂@21小跑堂@21小跑堂
大家好,今天和大家分享一下STM32F103C8T6读写内部flash,关于103系列的单片机大家可以参考选项手册查看flash的容量


一、芯片FLASH容量分类:




可以看到我们今天介绍的这款芯片的flash大小是64K的,网上也有人说它可以支持到128K,但是官方给出的解释是前64K是有保证的,后面的无法保证,所以想要使用的小伙伴需要慎重。
现在芯片的flash大小我们知道了,下面就可以看看这个flash是怎么划分的了,通过芯片数据手册,我们能看到今天说的STM32F103C8T6是属于中等容量的设备。

既然是中等容量的设备了,那我们就来看看flash划分吧,在STM32的闪存编程手册中有这样一段话:按照不同容量,
存储器组织成:
32个1K字节/页(小容量)
128个1K字节/页(中容量)
256个2K字节/页(大容量)
这段话怎么理解呢,就是告诉我们小容量的设备(内存是6K和32K)的设备是由1K字节每页组成的,
中容量的设备(内存是64K和128K)的设备是由1K字节每页组成的,
大容量的设备(内存是256K、384K和512K)的设备是由2K字节每页组成的,
举个例子吧:
一个芯片的存储容量是64K,这64K是什么呢,就是64*1024个BYTE,一个BYTE是由8位0或1组成的,(比如0000 1111 这8个二进制数组成了一个字节,用十进制来说就是15)
小结一下:64K的flash可以存储64*1024个字节的数据。
咱们继续说,这64K的数据怎么划分,存储是按照页为单位进行存储的,一页1K的容量,也就说一页可以存储1024个字节
一共是多少页?
答案是:64页,我们看一下官方是不是这么说的

在闪存编程手册里确实是这么说的,所以我们刚才说是64页是正确的
二、 读写步骤:
上面我们知道了芯片是怎么分类的,下面我们就重点来讲解一下芯片是怎么读写的。
内部flash我们参照HAL库或者标准库,直接调用ST公司给我们封装好的库进行编程就可以了,这里我用的是标准库,有兴趣的小伙伴可以去看看HAL库
是不是有小伙伴会疑问什么是标准库,什么是HAL库?
在这里给大家解释一下,这两个库都是ST公司,直接把寄存器封装成函数,供大家直接调用某一个函数,就可以完成各种寄存器的配置,不容大家直面芯片的寄存器,方便阅读和使用,因为每个函数的名称功能都是不一样的,在调用前可以参考函数的注释,在F0和F4的标准库里甚至有每个函数的用法,不知道为什么在F1的库里把使用步骤去掉了。
咱们继续,读写的话库函数分为:
/*------------ Functions used for all STM32F10x devices -----*/
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);
FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data);
FLASH_Status FLASH_EnableWriteProtection(uint32_t FLASH_Pages);
FLASH_Status FLASH_ReadOutProtection(FunctionalState NewState);
FLASH_Status FLASH_UserOptionByteConfig(uint16_t OB_IWDG, uint16_t OB_STOP, uint16_t OB_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);
FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout);

/*------------ New function used for all STM32F10x devices -----*/
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);
在这里就不一个一个的详细说了,我们说一下常用的就行
1. 解锁
void FLASH_Unlock(void);

2. 上锁
void FLASH_Lock(void);

3. 页擦除
FLASH_Status FLASH_ErasePage(uint32_t Page_Address);

4. 半字写入
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);

上面这4个函数就是我们最常用的。
下面说一下数据写入的步骤,
第一步:解锁
第二步:判断写入的数据是否被擦除过,也就是判断写入的地址内存放的是不是0xFFFF 这里要重点说一下,为什么要判断是不是0xFFFF而不是判断是不是0xFF呢?因为我们每次写入数据都要写入半字,也就是两个字节的数据才行,而且写入的地址只能是2的整数倍,不能是奇数。这里大家注意一下。
第三步:写入数据 STM32F103C8T6只能按照半字的方式进行数据写入,写入前的数据必须是0XFFFF,因为FLASH数据写入,只能写0,不能写1,这也就是为什么我们要先确保写入前的数据是被擦除了的原因。
第四步:上锁
第五步:验证写入是否正确
其实第五步可以省略。
我们看看官方给的写入过程


好了,其实是一样的。
下面我就和大家来分享一下(百分之九十九参考的正点原子的例程)

//不检查的写入
//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);
            WriteAddr+=2;//地址增加2.
        }  
}
//从指定地址开始写入指定长度的数据
//WriteAddr:起始地址(此地址必须为2的倍数!!)
//pBuffer:数据指针
//NumToWrite:半字(16位)数(就是要写入的16位数据的个数.)

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;   //去掉0X08000000后的地址
        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;                        //扇区地址  0~127 for STM32F103RBT6
        secoff=(offaddr%STM_SECTOR_SIZE)/2;                //在扇区内的偏移(2个字节为基本单位.)
        secremain=STM_SECTOR_SIZE/2-secoff;                //扇区剩余空间大小   
        if(NumToWrite<=secremain)
        {
          secremain=NumToWrite;//不大于该扇区范围
        }
        while(1)
        {        
                STMFLASH_Read(((secpos*STM_SECTOR_SIZE)+STM32_FLASH_BASE),STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容
                for(i=0;i<secremain;i++)//校验数据
//                for(i=0;i<(STM_SECTOR_SIZE/2);i++)//校验数据
                {
                        if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除  
//        if(STMFLASH_BUF!=0XFFFF)break;//需要擦除                        
                }
                FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
                if(i<secremain)//需要擦除
//                if(i<(STM_SECTOR_SIZE/2))//需要擦除
                {
                        FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);

                        FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区
                        for(i=0;i<secremain;i++)//复制
                        {
                                STMFLASH_BUF[i+secoff]=pBuffer;         
                        }
                        STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区  
                }else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接写入扇区剩余区间.                                    
                if(NumToWrite==secremain)break;//写入结束了
                else//写入未结束
                {
                                secpos++;                                //扇区地址增1
                                secoff=0;                                //偏移位置为0         
                                  pBuffer+=secremain;          //指针偏移
                                WriteAddr+=(secremain*2);        //写地址偏移           
                                 NumToWrite-=secremain;        //字节(16位)数递减
                                if(NumToWrite>(STM_SECTOR_SIZE/2))
                                {
                                  secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完
                                }
                                else
                                {
                                  secremain=NumToWrite;//下一个扇区可以写完了
                                }
                }         
        }        
        FLASH_Lock();//上锁
}


最终我们调用STMFLASH_Write()函数进行数据的写入,是不是有没看懂的小伙伴,我给大家解释一下写入的过程吧

这个STMFLASH_Write()函数,是说给定一个写入的地址、数据和写入的个数,然后按照给定的地址开始写数据,注意红色字体。
写数据是怎么做的呢
首先是整理一下写入的页地址和需要写入多少页,每一页写入的话起始地址是什么
然后开始一页一页的写,当遇到跨页写入的时候,把第二页的地址写进去,写的个数继续写入就行
如果还有不明白的可以在下面留言,我给大家解答
还有一个地方很重要,就是我修改了库函数

/**
  * [url=home.php?mod=space&uid=247401]@brief[/url]  Programs a half word at a specified address.
  * [url=home.php?mod=space&uid=536309]@NOTE[/url]   This function can be used for all STM32F10x devices.
  * @param  Address: specifies the address to be programmed.
  * @param  Data: specifies the data to be programmed.
  * @retval FLASH Status: The returned value can be: FLASH_ERROR_PG,
  *         FLASH_ERROR_WRP, FLASH_COMPLETE or FLASH_TIMEOUT.
  */
FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)
{
        FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
  FLASH_Status status = FLASH_COMPLETE;
  /* Check the parameters */
  assert_param(IS_FLASH_ADDRESS(Address));

#ifdef STM32F10X_XL
  /* Wait for last operation to be completed */
  status = FLASH_WaitForLastOperation(ProgramTimeout);

  if(Address < FLASH_BANK1_END_ADDRESS)
  {
    if(status == FLASH_COMPLETE)
    {
      /* if the previous operation is completed, proceed to program the new data */
      FLASH->CR |= CR_PG_Set;

      *(__IO uint16_t*)Address = Data;
      /* Wait for last operation to be completed */
      status = FLASH_WaitForLastBank1Operation(ProgramTimeout);

      /* Disable the PG Bit */
      FLASH->CR &= CR_PG_Reset;
    }
  }
  else
  {
    if(status == FLASH_COMPLETE)
    {
      /* if the previous operation is completed, proceed to program the new data */
      FLASH->CR2 |= CR_PG_Set;

      *(__IO uint16_t*)Address = Data;
      /* Wait for last operation to be completed */
      status = FLASH_WaitForLastBank2Operation(ProgramTimeout);

      /* Disable the PG Bit */
      FLASH->CR2 &= CR_PG_Reset;
    }
  }
#else
  /* Wait for last operation to be completed */
  status = FLASH_WaitForLastOperation(ProgramTimeout);

  if(status == FLASH_COMPLETE)
  {
    /* if the previous operation is completed, proceed to program the new data */
    FLASH->CR |= CR_PG_Set;

    *(__IO uint16_t*)Address = Data;
    /* Wait for last operation to be completed */
    status = FLASH_WaitForLastOperation(ProgramTimeout);

    /* Disable the PG Bit */
    FLASH->CR &= CR_PG_Reset;
  }
#endif  /* STM32F10X_XL */

  /* Return the Program Status */
  return status;
}

大家能看出来吗?就是红色字体部分,增加了一个每次写入前清除所有异常状态。
为什么添加这个呢?
因为,如果你写入的数据的地址没有擦除,你就写入的话会导致异常状态的发生,而这个异常状态时要手动清除的,如果你没有清除这个异常状态,而继续写入数据的话,那么你后面写入任何数据都会报错,均写不进去,所以我在这里增加了一个异常状态清除,如果前面写入的数据报错了,不会影响我接下来的数据写入。
这里大家就清除为什么了吧。
写数据会了,那么再说一下读数据,其实这里读数据要比外部flash读取容易的多,我们直接读取地址,返回的就是地址存放的数据,是不是很简单
看下面的函数

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

//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数

void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead)           
{
        u16 i;
        for(i=0;i<NumToRead;i++)
        {
                pBuffer=STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节.
                ReadAddr+=2;//偏移2个字节.        
        }
}


有没有很开心,读写数据就是这么简单就完成了。
以后如果我们想开发BootLoader、把剩余的flash利用起来,就都很简单了。我会把用到的数据手册当成附件挂到下面,大家可以自行下载
以后我们再一起学习其他的功能,最后打个广告,ST的芯片很给力,大家应该多支持,如果你觉得学到了知识的话,那么请留意评论谢谢










586146144105917dea.png (556.87 KB )

586146144105917dea.png

STM32F10xxx闪存编程参考手册_中文.pdf

464.42 KB

使用特权

评论回复

打赏榜单

21小跑堂 打赏了 20.00 元 2021-09-18
理由:恭喜通过原创文章审核!请多多加油哦!

沙发
binoo7|  楼主 | 2021-9-17 13:28 | 只看该作者
一次传不完

cd00210843.pdf

1.97 MB

STM8和STM32产品选型手册.pdf

2.4 MB

STM32F10xxx闪存编程参考手册_英文.pdf

295.88 KB

使用特权

评论回复
板凳
呐咯密密| | 2021-9-17 17:25 | 只看该作者
flash是必须整页整页的写吗?还有写flash的速度如何,这个有测试过吗。我可能需要用这个

使用特权

评论回复
地板
呐咯密密| | 2021-9-17 17:26 | 只看该作者
还有,大佬贴代码用一下代码框,这样复制看的脑袋疼

使用特权

评论回复
5
binoo7|  楼主 | 2021-9-18 08:22 | 只看该作者
呐咯密密 发表于 2021-9-17 17:25
flash是必须整页整页的写吗?还有写flash的速度如何,这个有测试过吗。我可能需要用这个 ...

这个是可以单独写的,写入函数可以是任意长度

使用特权

评论回复
6
binoo7|  楼主 | 2021-9-18 08:22 | 只看该作者
呐咯密密 发表于 2021-9-17 17:26
还有,大佬贴代码用一下代码框,这样复制看的脑袋疼
#include "stmflash.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;   //去掉0X08000000后的地址
        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;                        //扇区地址  0~127 for STM32F103RBT6
        secoff=(offaddr%STM_SECTOR_SIZE)/2;                //在扇区内的偏移(2个字节为基本单位.)
        secremain=STM_SECTOR_SIZE/2-secoff;                //扇区剩余空间大小   
        if(NumToWrite<=secremain)
        {
          secremain=NumToWrite;//不大于该扇区范围
        }
        while(1)
        {       
                STMFLASH_Read(((secpos*STM_SECTOR_SIZE)+STM32_FLASH_BASE),STMFLASH_BUF,STM_SECTOR_SIZE/2);//读出整个扇区的内容
                for(i=0;i<secremain;i++)//校验数据
//                for(i=0;i<(STM_SECTOR_SIZE/2);i++)//校验数据
                {
                        if(STMFLASH_BUF[secoff+i]!=0XFFFF)break;//需要擦除  
//        if(STMFLASH_BUF[i]!=0XFFFF)break;//需要擦除                        
                }
                FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
                if(i<secremain)//需要擦除
//                if(i<(STM_SECTOR_SIZE/2))//需要擦除
                {
                        FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);

                        FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE);//擦除这个扇区
                        for(i=0;i<secremain;i++)//复制
                        {
                                STMFLASH_BUF[i+secoff]=pBuffer[i];          
                        }
                        STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2);//写入整个扇区  
                }else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain);//写已经擦除了的,直接写入扇区剩余区间.                                   
                if(NumToWrite==secremain)break;//写入结束了
                else//写入未结束
                {
                                secpos++;                                //扇区地址增1
                                secoff=0;                                //偏移位置为0          
                           pBuffer+=secremain;          //指针偏移
                                WriteAddr+=(secremain*2);        //写地址偏移          
                           NumToWrite-=secremain;        //字节(16位)数递减
                                if(NumToWrite>(STM_SECTOR_SIZE/2))
                                {
                                  secremain=STM_SECTOR_SIZE/2;//下一个扇区还是写不完
                                }
                                else
                                {
                                  secremain=NumToWrite;//下一个扇区可以写完了
                                }
                }         
        }       
        FLASH_Lock();//上锁
}
#endif

//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数

void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead)          
{
        u16 i;
        for(i=0;i<NumToRead;i++)
        {
                pBuffer[i]=STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节.
                ReadAddr+=2;//偏移2个字节.       
        }
}

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
















使用特权

评论回复
7
wenleileilei| | 2021-9-21 06:55 | 只看该作者
没看懂

使用特权

评论回复
8
怀揣少年梦| | 2021-9-22 15:39 | 只看该作者
过程很详细,支持大佬

使用特权

评论回复
9
duo点| | 2021-9-22 15:42 | 只看该作者
支持大佬,大佬优秀

使用特权

评论回复
10
kyzhd| | 2021-9-27 15:25 | 只看该作者
好帖,写的详细。

使用特权

评论回复
11
张岩0924| | 2023-2-3 11:33 | 只看该作者
为什么我没看见库函数修改的地方,这不是还是和原来一样吗?

使用特权

评论回复
发新帖 本帖赏金 20.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

49

主题

454

帖子

8

粉丝