
STM32 FLASH 使用详解 及 LL 库 FLASH 驱动实现 & i3 p$ J6 r" O& H5 [, ]- H/ E4 u: i/ x' j4 } 最新项目中需要使用 STM32L476 的片子。在选择片子时,资源的多少成为了一个比较重要的考量。在斟酌一番之后,我决定采用 LL 库来实现本次的功能。但是在使用 LL 库的时候发现其中并没有处理 FLASH 的驱动 stm32l4xx_ll_flash.h 和 stm32l4xx_ll_flash.c。同样去其他系列芯片中,也没有发现 LL 库的 FLASH 驱动。于是决定自己来实现一下!6 ]1 \& x2 d% X! U 引导模式 搞过 Linux 的应该都了解 Boot Loader 这个东西。但是通常在单片机中,我们基本就接触不到这个东西了。以这里的 ST MCU 为例,我们的代码通常情况下就直接从 FLASH 开始执行了。其实,MCU 是支持不同的启动模式的,下图是 STM32L476 的 Boot 配置: 在这里插入图片描述 + Q! r b# Y" F+ R. d ![]() 复位后,BOOT0 引脚和 nBOOT1 位的值均被锁存。 用户自行设置 nBOOT1 和 BOOT0 来选择所需的引导模式。 从 Main memory 引导: Main memory 被映射到为引导存储空间(0x00000000),但仍可以从其原始存储空间(0x08000000)中访问。 换句话说,可以从地址 0x00000000 或 0x08000000 开始访问 Main memory。1 i# W+ N. z& L& P& K8 `3 a) s b 从 System memory 引导: System memory 被映射到为引导存储空间(0x00000000),但仍可以从其原始存储空间(0x1FFF0000)中访问。这部分存储空间中,存放了 ST 的 Boot 代码!不同的芯片里面的代码是有区别的,这个部分在手册 AN2606.pdf 中有详细的介绍。 从 SRAM1 引导: SRAM1 被映射到为引导存储空间(0x00000000),但仍可以从其原始存储空间(0x20000000)中访问. F1 {2 V+ i* w7 E7 t; t- q% ? / n( J8 o% j: p 注意: ST 众多 MCU 中,FLASH 并不是一样的,而且差别很大!我们在使用 FLASH 的时候,比较关心的往往就是一下几点: 1.总的大小及组织情况% h; }* Z9 @$ F0 v) p$ w( F4 }0 P. m 2.读、写、擦 时的大小限制0 y9 O3 P0 _! P" \' q 下面是我常用的几个片子的 FLASH 特性介绍:& j4 X$ E7 `+ h& ?# O! d% T3 ]" w ![]() 特性 下面是我常用的几个片子的 FLASH 组织情况比较图:. r1 x1 W5 G; \- _9 l# q4 L ![]() 组织情况6 T- R( s: q; |2 s ·Main memory: 包含应用程序和用户数据。本文我们主要就是介绍这一部分。 ·Information block: 它由三部分组成:2 o# Z3 X5 W2 f: i ·Option bytes: 用于硬件和内存保护用户配置& Q, N( Z- J" V8 s ·System memory: 包含ST专有代码。如果需要 BootLoader 模式。则需要配置从此处启动。3 n* z& y! Q% s ·OTP (one-time programmable) area: 一次性编程区域 下面就以 STM32L476 为例,来简单介绍一下一些在使用时需要注意的事项。; {/ X& [. @! j# y( R5 a6 V Read access latency 要从 FLASH 中正确读取数据,必须在 FLASH 访问控制寄存器(FLASH_ACR)中设置正确的等待状态数(LATENCY)。CPU 时钟(HCLK)的频率和设备 VCORE 的内部电压范围决定了等待状态数(LATENCY)的具体值。下表为等待状态和CPU时钟频率之间的对应关系: ![]() 等待状态和CPU时钟频率之间的对应关系 STM32L476 在复位后,CPU时钟频率为 4 MHz,并且在 FLASH_ACR 寄存器中配置了 0 个等待状态(WS)。更改 CPU 频率时,必须根据手册推荐的流程来更改等待状态数(WS)。 ![]() 经常使用标准外设库的应该知道,在配置时钟的接口 static void SetSysClock(void); (system_stm32xxxx.c 中)有如下语句: ![]() Z. o" y/ Q+ q! ~ 同样,如果使用 HAL 库,在时钟配置函数 void SystemClock_Config(void) (默认在 main.c 中)也会有对于 FLASH 的配置:/ F5 K) W* x2 S6 r. P + q% e2 R- y5 s; q4 x ![]() 上面的代码都是使用 标准外设库 MCU 时钟配置工具 和 STM32Cube 生成的。当我们配置好主频之后,两个工具就会自动设置正确的 FLASH 等待周期数。如果我们手动移植或者是要改变主频的时候(例如进出低功耗时),则必须注意这点! ART Accelerator™1 w( d+ p9 L3 v" n4 [ 为了释放处理器的全部性能,加速器实现了指令预取队列和分支缓存,从而提高了 64 位闪存的程序执行速度。 这个东西仅针对具有 FPU 的 MCU。这个东西主要提供了三个功能:指令预取、指令缓冲、数据缓冲。; m. {5 F7 X( ?5 |% Q& \ 在这里插入图片描述( V6 s! D& H$ f/ j; d+ X 关于这部分,默认情况下是不开启的。我暂时有没有用到,这里不多介绍! ![]() FLASH 在上电之后,默认都是写保护的。貌似 STM32F0 没有写保护。写保护通常也分为两部分:FLASH_CR 保护、FLASH 地址空间保护。前者主要是通过寄存器 FLASH_KEYR 来控制,后者则通过指定的一些寄存器控制。 不同的系列对于写保护的处理也是有区别的。详细的需要看参考手册即可。 读保护 通过设置 FLASH_OPTR 寄存器的 RDP 位,然后通过应用系统复位来重新加载新的RDP选项字节,可以激活读保护。 读保护保护闪存主存储器,选项字节,备份寄存器(RTC中的RTC_BKPxR)和 SRAM2。从无保护(0级)到最大保护或无调试(2级),共有三种读取保护级别。/ ?* s8 |9 _, s* _9 Y9 Z$ ^- S8 r 在这里插入图片描述 ![]() Level 1: 这是擦除 RDP 选项字节时的默认保护级别。 当 RDP 值为不同于 0xAA 和 0xCC 的任何值时,或者即使补码不正确,也将定义该值。1 d, t' l/ J7 n4 o- t( V7 ^ User mode: 在用户模式(即 Boot Flash)下,可以以任意操作访问 Flash Main memory 区域,option bytes,备份寄存器(RTC 中的 RTC_BKPxR)和 SRAM2。 Debug, boot RAM and boot loader modes: 在调试模式下或着从 Boot RAM 或 boot loader 运行代码时,Flash Main memory 区域,备份寄存器(RTC 中的 RTC_BKPxR)和 SRAM2 完全不可访问。 在这些模式下,对 FLASH 的读或写访问会产生总线错误和硬故障中断。 Level 2: 这个级别是在 Level 1 的基础之上的。此外,Cortex-M4调试端口、从RAM引导(Boot RAM 模式)和从系统内存引导(boot loader 模式)都不再可用。在用户执行模式(boot FLASH mode)下,所有操作都允许在闪存主存上进行。相反,只能对选项字节执行读操作。, G1 Q4 E% g8 ^& b ~) b+ E/ T" q9 H$ f Option bytes 既不能编程也不能擦除。 因此,Level 2 根本无法删除:这是不可逆的操作。 尝试修改Option bytes 时,FLASH_SR 寄存器中的保护错误标志 WRPERR将被置位,并且可以生成中断。 读写擦 读最简单,直接访问要读取的地址即可。写和擦则需要专门的执行序列(操作多个寄存器)。写和擦都是有固定大小限制的!!例如这里的STM32L476 写入的数据必须是 64 位 8 字节对齐的!擦除则可以按页擦除、BANK 擦除、MASS 擦除。6 @9 l; J4 Q! } 写和擦的编程序列具体见手册即可,没啥需要特殊注意的! LL 库 FLASH 驱动 不知道为啥,LL 库中没有实现 FLASH 驱动。下面我们参照已有的其他外设的 LL 库来实现一下 FLASH 部分。为开源项目提交过贡献的人应该知道,想要自己的贡献被接受,除了实现没有错误之外,代码风格也必须与开源项目的代码风格一致!. K7 C3 M$ ]5 w. \$ }9 i6 e 在实现的过程中,主要有以下这些实现部分以及需要注意的事项:2 |# [9 u+ Q7 t b7 x 1.在 stm32l4xx_ll_system.h 中,有部分对于 FLASH 的操作(主要是针对 FLASH_ACR 寄存器),这就导致如果我们完全按照 LL 库的编码风格就会有部分宏和函数重名。1 X- t6 H/ L* }- M0 G! H5 ^ 2.每个寄存器位,LL 库会在每个外设自己的实现文件中重新定义一遍,并且均以 LL_ 开头。例如,在串口驱动中,状态寄存器的每个位会重新定义 #define LL_USART_ISR_PE USART_ISR_PE /*!< Parity error flag */. C) t1 E9 M# A/ Q0 b6 U& c 3.定义寄存器读写宏函数:
, T* [& ?4 F, g( B' K0 n" R9 E 5.静态内联接口(寄存器操作)。这部分接口可以说是 LL 库外设驱动的的核心代码,任何功能都可以使用这部分函数组合成一定的寄存器操作序列来实现。这部分通常不包含任何复杂的寄存器操作序列。 6.常用处理过程的封装(例如,写、擦等寄存器操作序列)。这部分接口的实现放在对应的 .c 文件中。 7.LL 库中,不同外设的代码实现并不是很统一。当然,下面不牵扯代码风格,其实也没啥问题。- G; T' W7 B2 ] ![]() ![]() 未实现部分4 {9 S, v1 j# p+ n: y I 由于有些功能没有用到,因此该版本驱动库中部分功能没有实现,具体如下: 1)FLASH option bytes 部分只实现了其中的一部分接口 2)读写保护寄存器目前没有实现+ g9 J: H! b. C/ F& ? 3)Fast programming 未实现 0 W: i r# c& Y 6 F( _% I# C, p3 C0 \ c $ P. W( B+ [. I% |+ p4 m |
非常有用的LL操作 |