記事ディレクトリ
1. フラッシュの概要
フラッシュ メモリ (フラッシュ メモリ) は、電子的に消去可能でプログラム可能な読み取り専用メモリの一種で、動作中に複数回消去または書き込みが可能です。不揮発性メモリですので、電源を切ってもデータは消えません。
2. STM32F1のフラッシュ
STM32F103ZET6のFlashサイズは512KBと大容量の製品です。中国語リファレンスマニュアルには大容量製品のフラッシュモジュール構成図が記載されています
- メイン メモリ
メイン メモリは、コードと定義されたいくつかの定数データを保存するために使用されます。Boot0 と Boot1 の両方が GND に接続されている場合、チップはメイン メモリの開始アドレス 0x0800 0000 からコードの実行を開始します。 - 情報ブロック
システム メモリにはブート プログラム コードが保存されます。スタートアップ プログラムはシリアル ポートによってダウンロードされたコードです。Boot0 が VCC に接続され、Boot1 が GND に接続されている場合、システム メモリ内のコードが実行されます。システム メモリに保存されているスタートアップ コードは、チップの工場出荷時に ST 社によってダウンロードされており、ユーザーが変更することはできません。オプションバイトは、ライトプロテクトおよびデュプロテクト機能を設定するために使用されます。 - フラッシュメモリインタフェースレジスタ
フラッシュメモリインタフェースレジスタは、フラッシュメモリ全体の制御機構であり、フラッシュメモリの多くの制御レジスタとステータスレジスタが含まれています。
在执行闪存写操作时,任何对闪存的读操作都会被锁住。只有对闪存的写操作结束后,读操作才能够正常执行。也就是说,在对闪存进行写操作或者擦除操作时,无法对闪存进行读操作。
3. フラッシュ操作手順
- ロックを解除してロックする
- 書き込み/消去動作
- フラッシュステータスの取得
- 操作が完了するまで待ちます
- Flashの指定アドレスデータを読み出す
4. プログラミング
操作内部Flash时,最小单位是半字(16位)。
4.1 データの読み取り
データの読み出し方法はポインタですが、以前のブロガーさんの記事で、ポインタを使って指定したアドレスのデータを読み書きする操作を紹介しました。
/*
*==============================================================================
*函数名称:Med_Flash_ReadHalfWord
*函数功能:读取指定地址的半字(16位数据)
*输入参数:faddr:读取地址
*返回值:对应读取地址数据
*备 注:对内部Flash的操作是以半字为单位,所以读写地址必须是2的倍数
*==============================================================================
*/
vu16 Med_Flash_ReadHalfWord (u32 faddr)
{
return *(vu16*)faddr;
}
/*
*==============================================================================
*函数名称:Med_Flash_Read
*函数功能:从指定地址开始读出指定长度的数据
*输入参数:ReadAddr:读取起始地址;pBuffer:数据指针;
NumToRead:读取(半字)数
*返回值:无
*备 注:对内部Flash的操作是以半字为单位,所以读写地址必须是2的倍数
*==============================================================================
*/
void Med_Flash_Read (u32 ReadAddr,u16 *pBuffer,u16 NumToRead)
{
u16 i;
for(i = 0;i < NumToRead;i ++)
{
pBuffer[i] = Med_Flash_ReadHalfWord(ReadAddr); // 读取2个字节.
ReadAddr += 2; // 偏移2个字节.
}
}
4.2 データの書き込み(チェックしない)
ここでの非チェックとは、書き込み前に書き込みアドレスが書き込み可能かどうかのチェックを行わないことを意味する。
/*
*==============================================================================
*函数名称:Med_Flash_Write_NoCheck
*函数功能:不检查的写入
*输入参数:WriteAddr:写入起始地址;pBuffer:数据指针;
NumToWrite:写入(半字)数
*返回值:无
*备 注:对内部Flash的操作是以半字为单位,所以读写地址必须是2的倍数
*==============================================================================
*/
void Med_Flash_Write_NoCheck (u32 WriteAddr,u16 *pBuffer,u16 NumToWrite)
{
u16 i;
for(i = 0;i < NumToWrite;i ++)
{
FLASH_ProgramHalfWord(WriteAddr,pBuffer[i]);
WriteAddr += 2; // 地址增加2.
}
}
4.3 データ書き込み(チェック)
/*
*==============================================================================
*函数名称:Med_Flash_Read
*函数功能:从指定地址开始写入指定长度的数据
*输入参数:WriteAddr:写入起始地址;pBuffer:数据指针;
NumToRead:写入(半字)数
*返回值:无
*备 注:对内部Flash的操作是以半字为单位,所以读写地址必须是2的倍数
*==============================================================================
*/
// 根据中文参考手册,大容量产品的每一页是2K字节
#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];
void Med_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; // 扇区地址
secoff = (offaddr % STM32_SECTOR_SIZE) / 2; // 在扇区内的偏移(2个字节为基本单位)
secremain = STM32_SECTOR_SIZE / 2 - secoff; // 扇区剩余空间大小
if (NumToWrite <= secremain)
{
secremain = NumToWrite; // 不大于该扇区范围
}
while (1)
{
// 读出整个扇区的内容
Med_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];
}
// 写入整个扇区
Med_Flash_Write_NoCheck(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE / 2);
}
else
{
// 写已经擦除了的,直接写入扇区剩余区间
Med_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(); // 上锁
}
マクロは次のように定義されています
// STM32的Flash容量,单位为KB
#define STM32_FLASH_SIZE 512
// FLASH主存储块起始地址
#define STM32_FLASH_BASE 0x08000000
上記のデータの読み取りとチェックなしの書き込みは比較的単純であるため、これ以上の分析は行いません。ここでは、チェックを使用して記述するためのプログラミングのアイデアを分析します。
- まず、条件付きコンパイルの短いセクションを使用して、大容量の製品を他の製品と区別します。大容量品の1ページ(1セクタ)は2Kバイト、中小容量品の1ページは1Kバイトのためです。配列が指定されており、配列のサイズはセクターのサイズです。
// 根据中文参考手册,大容量产品的每一页是2K字节
#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];
大容量品の場合、内蔵Flash動作時の最小単位がハーフワードとなるため、セクタは2Kバイトを2で割った2Kバイトとなります。
- 次に、書き込まれるアドレスが正当であるかどうか、つまり主記憶ブロックのアドレス範囲内であるかどうかを判断します。
// 判断写入地址是否在合法范围内
if (WriteAddr < STM32_FLASH_BASE || (WriteAddr >= (STM32_FLASH_BASE + 1024 * STM32_FLASH_SIZE)))
{
return; // 非法地址
}
- 書き込まれるアドレスが正当な場合は、ロックを解除した後にいくつかのパラメータ値を計算します。
offaddr = WriteAddr - STM32_FLASH_BASE; // 实际偏移地址
実際のオフセット アドレスは、書き込まれるアドレスと主記憶ブロックのベース アドレス (0x0800 0000) の差を指します。
secpos = offaddr / STM32_SECTOR_SIZE; // 扇区地址
セクタアドレスとは、書き込むアドレスが位置するセクタの前のセクタ番号を指します。すべてのパラメータは浮動小数点ではないため、除算を行う場合、小数点以下の桁は 0 になります。最終的な除算の結果は、現在のセクターの前にあるセクターの数になります。
secoff = (offaddr % STM32_SECTOR_SIZE) / 2; // 在扇区内的偏移(2个字节为基本单位)
セクタ内のオフセットとは、書き込まれるアドレスと、それが配置されているセクタの最初のアドレスとの差を指します。書き込まれるアドレスを使用して各セクターのバイト数を減算し、その余りがオフセット アドレスになります。ただし、内蔵Flashの動作最小単位はハーフワードのため、2で割る必要があります。
secremain = STM32_SECTOR_SIZE / 2 - secoff; // 扇区剩余空间大小
セクタ内の残りのスペースのサイズは、セクタの合計スペース サイズからオフセット アドレスを減算することで取得できます。ただし、単位はすべてハーフワードであることに注意してください。这里的剩余空间大小,并不是真正的剩余空间大小。而是指写入地址后面的扇区大小。这里不太好理解,画一个图表示一下
。
それは、ここでのセクターの残りのスペースのサイズが、実際の残りのスペースのサイズを指すものではないからです。残りの領域には既にデータが書き込まれているアドレスも存在する可能性があります。したがって、後に消去が必要かどうかの判断が必要となる。
- 書き込みアドレスが存在するセクタに書き込み内容が全て書き込めるかどうかを判定
if (NumToWrite <= secremain)
{
secremain = NumToWrite; // 不大于该扇区范围
}
可能であれば、現在のセクタの残りのスペースに書き込まれるハーフワードの数を直接割り当てます。現在のセクタの残りのスペースが書き込むハーフワード数に対応できるサイズであれば、一度書き込むだけでよく、書き込みが完了したかどうかの判断時に直接渡され、while が渡されます。ループは 1 回だけ実行されます。
- セクターの内容全体を読み取って、消去する必要があるかどうかを判断します。
// 读出整个扇区的内容
Med_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;
}
}
要对内部Flash某个地址写入数据时,需要确保该地址数值为0xFFFF。
判定方法は、セクタ内のオフセットから開始し、読み込んだセクタの残りの領域に書き込まれているアドレスがあるかどうかをforループで判定します。for ループは i の値を見つけます。i にセクター内のオフセット後のスペースを加えたものに 1 を加えたものが、セクター内の残りのスペースの実際のサイズになります。
forループ終了後、消去が必要かを判断します
// 需要擦除
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];
}
// 写入整个扇区
Med_Flash_Write_NoCheck(secpos * STM32_SECTOR_SIZE + STM32_FLASH_BASE,STM32_FLASH_BUF,STM32_SECTOR_SIZE / 2);
}
else
{
// 写已经擦除了的,直接写入扇区剩余区间
Med_Flash_Write_NoCheck(WriteAddr,pBuffer,secremain);
}
擦除时,最小单元为一个扇区。在大容量产品中,也就是2048字节。
- 最後に、書き込むデータを対応する場所に書き込みます。消去が必要な場合は、書き込み時に元の内容を抽出し、後で書き込む内容を埋め込み、セクタ全体を消去した後にまとめて書き込みます。消す必要がない場合は直接書き込んでください。
5. 注意すべき事項
Flash を操作する際は、コード領域の内容を消去したり書き込まないように注意してください。消去したアドレスがコード領域にある場合、プログラムが異常動作する原因となります。では、操作するアドレスがコード領域にないことを確認するにはどうすればよいでしょうか? そのためには、コードがどれだけのメモリを占有しているかを知る必要があります。Keil5 のコンパイルが完了すると、次の内容が表示されます
- コード
プログラムが占有するメモリ サイズ (フラッシュに保存) - RO データ
プログラムで定義された定数が占めるメモリ サイズ (フラッシュに保存) - RW データ
によって初期化されたグローバル変数が占有するメモリ サイズ(プログラムの初期化時に、RW データが FLASH から RAM にコピーされます)
ZI データ
によって初期化されていないグローバル変数によって占有されるメモリ サイズ( RAM に保存されます))
最後に、プログラム コードが占めるフラッシュ領域を計算します。flash = Code + RO-data + RW-data
。