灵动微电子 · 2023年03月17日

mm32-2nd-bootloader技术白皮书(3)——设计实现QSPI Flash的下载算法

 简介

前文中了解了微控制器可以使用 QSPI 接口连接 QSPI Flash,来实现扩展微控制器的 Flash 空间,存储较大 code size 的应用程序。

但通过 QSPI 接口连接的 QSPI Flash,无法直接使用已有的片内 Flash 的下载算法下载程序,例如在 MDK 平台上编译的程序,在没有下载算法的情况下,是不能直接点击 MDK 的下载按钮或调试按钮下载调试程序的,这样会对使用 QSPI Flash 存储执行程序带来很大的麻烦。

因此,本文将使用 MDK 平台,以 PLUS-F5270 开发板为例,讲解如何制作适用于 MM32F5270 系列 MCU 的 QSPI Flash 下载算法,让用户在 MDK 平台中下载应用程序就如同在片内 Flash 中那样方便。

 生成下载算法文件

在 Keil-MDK 的安装目录下,包含一个下载算法的模板文件,例如:MDK 安装在 C 盘目录下,则工程模板的位置在 "C:\Keil\_v5\ARM\Flash" ,模板名称为 "_Template",如图 1 所示。

image.png

复制整个 "_Template"文件到该目录下,并重命名为“MM32F5270_QSPI_FlashLoader”,当然也可以换成其它名字;进入该文件夹,将所有名为 "NewDevice" 的文件重命名,建议同文件夹名保持一致。

重命名文件后,打开 ".uvprojx" 后缀的工程文件,进入 MDK 工程,在 Project 栏中可以看到,除了 "Abstract.txt" 文件外,只有 "FlashPrg.c" 与 "FlashDev.c" 两个文件。

修改MDK工程选项配置

打开 Options for Target,进入 Device页面,PLUS-F5270 使用的微控制器为 MM32F5277E9PV,在左下角的芯片选择中,选择 MindMotion -> MM32F5270 Series->MM32F5277E -> MM32F5277E9PV"。

由于 MM32F5270 系列芯片使用 STAR-MC1 内核处理器,不能使用 AC5 编译器编译代码,需选择 Target 页面,将 Code Generation 的 ARM Compiler版本选择为 Use default compiler version 6。

选择 Output页面,修改 Name of Executable选项中的内容以表示设备,建议该修改名称与文件夹保持一致。修改页面如图 2 所示。

image.png

由于下载算法通常是在 Flash 目录下开发,使用下载算法时要将下载算法放到 Flash 目录下,避免调试下载算法时,来回将下载算法文件复制到前一级目录。因此,在工程选项配置中,将输出下载算法的路径配置到前一级目录下。选择 User页面,修改 After Build/Rebuild 的Run #1为cmd.exe /C copy "Objects\%L" "..\@L.FLM",如图 3 所示。

image.png
Flash 编程算法使用 Read-Only Position Independent 与 Read-Write Position Independent。通过观察 "MM32F5270\_QSPI\_FlashLoader" 文件夹下的 "Target.lin" 文件可知,不管是代码还是数据,都被放入与地址无关的空间内,因此在编译时,要将代码和数据以“和地址无关”的方式编译。选择 C/C++(AC6) 页面,勾选 Read-Only Position Independent 与 Read-Write Position Independent;选择 Asm 页面,也勾选 Read-Only Position Independent 与  Read-Write Position Independent 选项,如图 4 所示。

image.png

此处需注意要将 C/C++(AC6)页面下的 Warnings: 选项选为AC5-like Warnings,否则会产生路径名不同的 warning。

FlashDev.c(25): warning: non-portable path to file '"..\FlashOS.h"'; specified path differs in case from file name on disk [-Wnonportable-include-path]#include "..\FlashOS.H"        // FlashOS Structures         ^~~~~~~~~~~~~~         "..\FlashOS.h"

查看整个 MM32F5270\_QSPI\_FlashLoader 工程文件,会发现当前工程中没有启动文件与主函数,默认只有几个功能函数,在这种情况下,编译会爆出下面的警告:

L6305W: Image does not have an entry point. (Not specified or not set due to multiple choices.)

但下载算法本身是不需要启动文件和主函数的,只在下载代码时由调试器调用下载算法,因此,需忽略该 warning。选择 Linker 界面,修改 Misc controls中的内容为 --diag_suppress L6305 ,如图 5 所示。

image.png

修改FlashDev.c文件

在 MM32F5270_QSPI_FlashLoader工程文件中,FlashDev.c 文件的内容由实际所使用的 QSPI Flash 芯片决定,该文件中包含一个名为 FlashDevice的结构体,本文以 W25Q128 芯片为例,作为与 MM32F5270 MCU 相连接的 QSPI Flash 芯片,该芯片容量为 16MB,扇区大小为 4KB,页大小为 256Byte。则结构体FlashDevice 的配置如下:

struct FlashDevice const FlashDevice  =  {   FLASH_DRV_VERS,              // Driver Version, do not modify!   "MM32F5270 QSPI FlashLoader",// Device Name    EXTSPI,                      // Device Type   0x90000000,                  // Device Start Address   0x01000000,                  // Device Size in Bytes (16MB)   256,                         // Programming Page Size   0,                           // Reserved, must be 0   0xFF,                        // Initial Content of Erased Memory   100,                         // Program Page Timeout 100 mSec   3000,                        // Erase Sector Timeout 3000 mSec// Specify Size and Address of Sectors   0x001000, 0x000000,          // Sector Size  4kB (1 Sectors)   SECTOR_END};

若使用 W25Q64 芯片,其芯片容量 8MB,Device Size in Bytes 行的值需修改为 0x00800000。

修改好后,先进行一次编译,然后在 MM32F5270 的任意工程中查看列表,以MM32F5270中的 hello_world 样例工程为例。

打开 hello_world 样例工程的 Options for Target 选项,到 Debug  页面下,打开调试器的 Settings, 到 Flash Download 页面下,进入 Add 选项,可看见列表中新增了一个名为 MM32F5270 QSPI FlashLoader 的下载算法,列表界面如图 6 所示。

image.png

当然,这个下载算法还尚且不能使用,仍需要进行功能填充。若要让下载算法可用,首先,需要添加必要的文件,使用 MindSDK 生成任意一个 MM32F5270 的工程,提取其中的 device 目录到下载算法的目录下。

下载算法需要使用到的文件如图 7 所示:

image.png

添加完所需文件后,需添加头文件地址才能使用对应文件,如图 8 所示。

image.png

为什么没有把所有的驱动文件都包含进去呢?这是由于 FlashLoader 需尽可能的小,如果把所有的驱动都添加进去的话,下载算法就会变得很大很大,要知道,下载算法没有启动文件和主函数,编译器不知道哪些函数没有用到,因此不会进行优化,将没有用到的函数删除。因此仅添加 GPIO,QSPI 和 RCC 三个必须使用到的驱动,如有必要,甚至可以写好下载算法后,将这三个驱动文件中没有用到的函数也进行删除。

QSPI Flash芯片的驱动开发

要想将代码下载到 QSPI Flash 中,就要根据 QSPI Flash 手册中的对应要求来操作 QSPI Flash,为此,需要为 QSPI Flash 芯片设计驱动,当然,为了减少代码量,仅实现必要的功能。

QSPI Flash 支持单线,双线和四线操作,本文为方便编程,仅描述实现单线模式的操作,且认为 QSPI Flash 没有对任何块实施写保护等操作,同时,QSPI Flash 没有进入所谓的 QPI 模式。当然,上述特殊需求也能够在 Flashloader 中实现,但本文不再详细赘述如何实现。

在 FlashPrg.c 文件中设计并添加 QspiFlash\_Init()、QspiFlash\_IsBusy()、QspiFlash\_EnableWrite()、QspiFlash\_EraseSector()、QspiFlash\_WritePage() 函数作为 QSPI Flash 芯片的驱动函数。

QspiFlash\_Init() 函数

设计函数QspiFlash\_Init()用于初始化,该函数包括 GPIO 的初始化,QSPI 模块的初始化,QSPI 直接模式读的初始化,由于仅使用单线模式,故无需额外打开 QSPI Flash 的四线模式。以上初始化的需求如下:

GPIO 的初始化

配置 GPIO 使其作为 QSPI 的信号线,供 QSPI 外设与 QSPI Flash 进行通信。

QSPI 模块的初始化

使其能够与 QSPI Flash 进行通信,实现读数据和写数据的功能。

QSPI 直接模式读的初始化

为了使系统能够通过访问地址的形式直接读取到 QSPI Flash 中的数据,调试器下载程序后,会通过访问地址的形式读取 QSPI Flash 中的数据,与下载的数据进行对比,实现校验功能。

需要注意的是,GPIO 要根据 QSPI 实际使用到的引脚进行配置,QSPI Flash 的初始化要根据 QSPI Flash 的数据手册进行调整,可能部分 QSPI Flash 在读写数据之前,会有一些针对 QSPI Flash 的初始化操作。

void QspiFlash_Init(void)
{
    /* enable periph clock. */
    RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOA
                        | RCC_AHB1_PERIPH_GPIOB
                        | RCC_AHB1_PERIPH_GPIOG
                        | RCC_AHB1_PERIPH_QSPI
                        , true);

    /* enable gpio. */
    GPIO_Init_Type gpio_init;
    gpio_init.PinMode = GPIO_PinMode_AF_PushPull;
    gpio_init.Speed   = GPIO_Speed_50MHz;

    gpio_init.Pins    = GPIO_PIN_3; /* QSPI_DA1. */
    GPIO_Init(GPIOA, &gpio_init);
    GPIO_PinAFConf(GPIOA,gpio_init.Pins, GPIO_AF_10);

    gpio_init.Pins    = GPIO_PIN_3 | GPIO_PIN_10; /* QSPI_DA2 & QSPI_CS. */
    GPIO_Init(GPIOB, &gpio_init);
    GPIO_PinAFConf(GPIOB,gpio_init.Pins, GPIO_AF_10);

    gpio_init.Pins    = GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8; /* QSPI_DA0, QSPI_SCK & QSPI_DA3. */
    GPIO_Init(GPIOG, &gpio_init);
    GPIO_PinAFConf(GPIOG,gpio_init.Pins, GPIO_AF_10);

    /* enable qspi. */
    QSPI_Init_Type qspi_init;
    qspi_init.SpiMode           = QSPI_SpiMode_3;
    qspi_init.SckDiv            = 8u;
    qspi_init.CsHighLevelCycles = 9u;
    qspi_init.RxSampleDelay     = 1u;

    QSPI_Init(QSPI, &qspi_init);

    /* enable qspi direct xfer mode. */
    QSPI_DirectXferConf_Type qspi_direct_conf;
    qspi_direct_conf.CmdBusWidth  = QSPI_BusWidth_1b;
    qspi_direct_conf.CmdValue     = 0x0B; /* fast read. */
    qspi_direct_conf.AddrBusWidth = QSPI_BusWidth_1b;
    qspi_direct_conf.AddrWordWidth    = QSPI_WordWidth_24b;
    qspi_direct_conf.AltBusWidth  = QSPI_BusWidth_None;
    qspi_direct_conf.DummyCycles  = 8u;
    qspi_direct_conf.DataBusWidth = QSPI_BusWidth_1b;

    QSPI_EnableDirectRead(QSPI, &qspi_direct_conf);
}

QspiFlash_IsBusy() 函数

QspiFlash_IsBusy() 函数通过读取 QSPI Flash 的寄存器值,确定 QSPI Flash 是否处于忙碌状态。在向 QSPI Flash 中写入数据之后,需要使用该函数等待 QSPI Flash 将写进去的数据完全烧写到 Flash 单元中。

判断本文使用的 QSPI Flash 是否忙碌的方法是:读取这个 QSPI Flash 寄存器 2 中的第一个比特位是否为1,若为1 则是忙碌状态。

具体的判断方法,还是需要根据使用的 QSPI Flash 手册来确定判断方法。

bool QspiFlash_IsBusy(void)
{
    QSPI_ClearStatus(QSPI, QSPI_STATUS_XFER_DONE);

    QSPI_IndirectXferConf_Type xfer_conf;
    xfer_conf.CmdBusWidth   = QSPI_BusWidth_1b;
    xfer_conf.CmdValue      = 0x05; /* get reg1 value. */
    xfer_conf.AddrBusWidth  = QSPI_BusWidth_None;
    xfer_conf.AltBusWidth   = QSPI_BusWidth_None;
    xfer_conf.DummyCycles   = 0u;
    xfer_conf.DataBusWidth  = QSPI_BusWidth_1b;
    xfer_conf.DataWordWidth = QSPI_WordWidth_8b;
    xfer_conf.DataLen       = 1;

    QSPI_SetIndirectReadConf(QSPI, &xfer_conf);

    while(0 == (QSPI_GetStatus(QSPI) & QSPI_STATUS_XFER_DONE) )
    {}

    uint32_t reg1 = QSPI_GetIndirectData(QSPI);

    if (0 == (reg1 & 0x01) )
    {
        return false;
    }
    else
    {
        return true;
    }
}

QspiFlash_EnableWrite() 函数

QspiFlash_EnableWrite() 函数主要是为了进行写使能操作,保证数据可写入。

每次进行任何写操作时,都需要执行一次写使能操作。需要注意,每一次写操作完成后,写使能就会关闭(当然,还需视具体芯片而定)。

void QspiFlash_EnableWrite(bool enable)
{
    QSPI_ClearStatus(QSPI, QSPI_STATUS_XFER_DONE);

    QSPI_IndirectXferConf_Type xfer_conf;
    xfer_conf.CmdBusWidth   = QSPI_BusWidth_1b;
    if (true == enable)
    {
        xfer_conf.CmdValue      = 0x06; /* enbale write. */
    }
    else
    {
        xfer_conf.CmdValue      = 0x04; /* disable write. */
    }
    xfer_conf.AddrBusWidth  = QSPI_BusWidth_None;
    xfer_conf.AltBusWidth   = QSPI_BusWidth_None;
    xfer_conf.DummyCycles   = 0u;
    xfer_conf.DataBusWidth  = QSPI_BusWidth_None;

    QSPI_SetIndirectWriteConf(QSPI, &xfer_conf);

    while(0 == (QSPI_GetStatus(QSPI) & QSPI_STATUS_XFER_DONE) )
    {}
}

QspiFlash\_EraseSector() 函数

QspiFlash\_EraseSector() 函数执行擦除扇区操作。

void QspiFlash_EraseSector(uint32_t addr)
{
    QspiFlash_EnableWrite(true);
    QSPI_ClearStatus(QSPI, QSPI_STATUS_XFER_DONE);

    QSPI_IndirectXferConf_Type xfer_conf;
    xfer_conf.CmdBusWidth   = QSPI_BusWidth_1b;
    xfer_conf.CmdValue      = 0x20; /* sector erase. */
    xfer_conf.AddrBusWidth  = QSPI_BusWidth_1b;
    xfer_conf.AddrWordWidth = QSPI_WordWidth_24b;
    xfer_conf.AddrValue     = addr & 0x00FFFFFF;
    xfer_conf.AltBusWidth   = QSPI_BusWidth_None;
    xfer_conf.DummyCycles   = 0u;
    xfer_conf.DataBusWidth  = QSPI_BusWidth_None;

    QSPI_SetIndirectWriteConf(QSPI, &xfer_conf);

    while(0 == (QSPI_GetStatus(QSPI) & QSPI_STATUS_XFER_DONE) )
    {}

    while(QspiFlash_IsBusy() ) /* wait for erase done. */
    {}

    QspiFlash_EnableWrite(false);
}

QspiFlash\_WritePage() 函数

QspiFlash\_WritePage() 函数用于执行写数据操作。

注意,擦数据是按块(或者叫做扇区)擦除,写操作是按页进行写,页的大小是小于块的大小的。块大小和页大小已经在前面的 FlashDev.c 中定义好了,调试下载器会按照上面定义的大小进行块擦除和页写入。

void QspiFlash_WritePage(uint32_t addr, uint32_t size, uint8_t * data)
{
    QspiFlash_EnableWrite(true);
    QSPI_ClearStatus(QSPI, QSPI_STATUS_XFER_DONE);

    QSPI_IndirectXferConf_Type xfer_conf;
    xfer_conf.CmdBusWidth   = QSPI_BusWidth_1b;
    xfer_conf.CmdValue      = 0x02; /* sector erase. */
    xfer_conf.AddrBusWidth  = QSPI_BusWidth_1b;
    xfer_conf.AddrWordWidth = QSPI_WordWidth_24b;
    xfer_conf.AddrValue     = addr & 0x00FFFFFF;
    xfer_conf.AltBusWidth   = QSPI_BusWidth_None;
    xfer_conf.DummyCycles   = 0u;
    xfer_conf.DataBusWidth  = QSPI_BusWidth_1b;
    xfer_conf.DataWordWidth = QSPI_WordWidth_8b;
    xfer_conf.DataLen       = size;

    QSPI_SetIndirectWriteConf(QSPI, &xfer_conf);

    while(0 == (QSPI_GetStatus(QSPI) & QSPI_STATUS_XFER_DONE) )
    {
        if (size > 0 && 0 != (QSPI_GetStatus(QSPI) & QSPI_STATUS_FIFO_EMPTY) )
        {
            QSPI_PutIndirectData(QSPI, *data++);
            size--;
        }
    }

    while(QspiFlash_IsBusy() ) /* wait for erase done. */
    {}

    QspiFlash_EnableWrite(false);
}

修改FlashPrg.c文件

在 MM32F5270_QSPI_FlashLoader 工程的 FlashPrg.c 文件中,根据用户手册对于 Flash 描述和 Flash 控制器的特性,适配编程算法。

使用上述QSPI Flash芯片驱动开发函数对FlashPrg.c文件中的模板函数进行填充。

Init()函数

初始化函数除了初始化 QSPI Flash 本身,还需要初始化 MCU 的时钟,使其工作在片内 RC 振荡器提供的 8M 时钟环境下。

int Init (unsigned long adr, unsigned long clk, unsigned long fnc) 
{
    /* Switch to HSI. */
    RCC->CR |= RCC_CR_HSION_MASK; /* Make sure the HSI is enabled. */
    while ( RCC_CR_HSIRDY_MASK != (RCC->CR & RCC_CR_HSIRDY_MASK) )
    {
    }
    RCC->CFGR = RCC_CFGR_SW(0u); /* Reset other clock sources and switch to HSI. */
    while ( RCC_CFGR_SWS(0u) != (RCC->CFGR & RCC_CFGR_SWS_MASK) ) /* Wait while the SYSCLK is switch to the HSI. */
    {
    }

    /* Reset all other clock sources. */
    RCC->CR = RCC_CR_HSION_MASK;

    /* Disable all interrupts and clear pending bits. */
    RCC->CIR = RCC->CIR; /* clear flags. */
    RCC->CIR = 0u; /* disable interrupts. */

    QspiFlash_Init();
    return (0);                                  /* Finished without Errors. */
}

UnInit()函数

无需任何操作。

UnInit() 函数在下载程序的操作完成后被调用,如果 Flashloader 操作的是片内 Flash,可能会在该函数内实现一个对片内 Flash 上锁的操作,保证 Flash 中的数据不会被意外修改,而本文使用的 QSPI Flash 不需要类似的操作,因此这个函数不用做任何改动,当然,也得视具体的 QSPI Flash 而定。

EraseChip()函数

QSPI Flash 本身具有擦除整颗芯片数据的指令,但为了减少代码量,可以使用循环调用擦除扇区的方法来擦除整颗芯片的数据。

int EraseChip (void) {    for (uint32_t i = 0; i < 0x01000000; i+= 0x1000)    {        QspiFlash_EraseSector(i);    }    return (0);                                  /* Finished without Errors. */}

EraseSector() 函数

此函数的目的是擦除扇区。

需要注意的是,Flash_EraseSector() 需要传入的参数是 QSPI Flash 芯片内的地址,而不是在 MCU 上映射的地址,因此需要使用 0x00FFFFFF 掩码取有效地址。

int EraseSector (unsigned long adr) 
{
    QspiFlash_EraseSector(adr & 0x00FFFFFF);
    return (0);                                  /* Finished without Errors. */
}

ProgramPage()函数

此函数的目的是将程序写入 Flash 中。

int ProgramPage (unsigned long adr, unsigned long sz, unsigned char *buf) 
{
    QspiFlash_WritePage(adr,sz,buf);
    return (0);                                /* Finished without Errors. */
}

 总结

完成上述下载算法所需的函数后,下载算法也就制作完成了。使用这个下载算法,就可以实现下载程序到 QSPI Flash 的操作,但本文还没有讲该怎么实现这个可以存储在 QSPI Flash 中的应用程序,也没有讲解该怎么执行这个程序,所以暂时还没有办法验证这个下载算法是否可用,不过后续也将会讲解如何跳转执行存储在 QSPI Flash 中的应用程序,和如何让应用程序能够存储在 QSPI Flash 中。

作者:灵动MM32
文章来源:灵动MM32MCU

推荐阅读

更多MM32F5系列资料请关注灵动MM32 MCU专栏。如想进行MM32相关芯片技术交流,请添加极术小姐姐微信(id:aijishu20)加入微信群。
推荐阅读
关注数
6100
内容数
272
灵动MM32 MCU相关技术知识,欢迎关注~
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息