mm32-2nd-bootloader

需求

如今,几乎所有可联网的电子设备都支持远程升级(OTA)功能,OTA 一是让电子设备能够支持更多的功能,二是能够修复一些应用程序中的漏洞。

前文中,我们在 2nd Bootloader 中实现了类似 ISP 的下载的功能,上位机如果基于 Ymodem 协议,通过 2nd Bootloader 下载应用程序到 Flash 中,其实就可以实现简单的 OTA 功能,但是,这样的 OTA 却是不安全的:无法保证升级过程中,若出现意外情况,打断升级后,微控制器还能够执行原有的应用程序。健全的 OTA,需要保证微控制器即使在升级失败之后,也依然能够按照升级之前的应用程序继续工作。

设计

升级应用程序这个需求其实通过前文实现的 ISP 就能实现,而 OTA 升级失败后,依然能够执行升级前的应用程序,才是 OTA 升级的难点。

OTA 为什么会升级失败?有如下可能:

  • OTA 升级过程中,意外断开网络,固件包接收不完整。

  • OTA 升级过程中,设备突然断电,固件包接收不完整。

  • OTA 升级过程中,设备突然断电,接收到的固件包未写入到 Flash 中。

如果要保证 OTA 升级失败后,原有的应用程序还能够正常运行,就需要让接收到的固件包,不能直接覆盖到原有的固件中。因此,我们需要把 Flash 空间一分为二,分别是执行区域和备份区域,执行区域存储的是当前微控制器可执行的固件,2nd Bootloader 会引导微控制器跳转到这块区域执行应用程序,而备份区域就是存放将要下载的新固件,如图1所示:

1.png

图1 Flash 空间划分

还需要将 OTA 的过程分为两个大的步骤:

  • 固件下载过程

  • 固件更新过程

固件下载过程中,微控制器 会把接收到的固件保存到备份区域,如图2所示。

2.png

图2 固件包存储在备份区域

固件更新过程中,微控制器 会把备份区域的固件复制到执行区域中如图3所示。

3.png

图3 固件包转移到执行区域

由于固件包存储在备份区域,固件下载过程中再怎么失败,也影响不到执行区域的固件,所以应用程序能够正常运行。

然后问题来了,当备份区域的固件下载好后,该怎么转移到执行区域上呢?其流程如图4所示:

4.png

图4 备份区域内容复制到执行区域

是不是很简单,但如果在固件更新的过程中断电,固件复制到一半怎么办?

因此,我们需要在 Flash 中保存一些信息,让微控制器复位后,在执行应用程序前,都先检查下固件是否已经复制完毕。

我们在执行区域和备份区域的末尾分别上写一些内容,来验证执行区域内容的完整性,存放这些内容的区域可称为信息块,如图5所示。

5.png

图5 在各区域末尾添加信息块存储区

信息块的大小至少是 Flash 的最小擦除单位,以保证擦除信息块时不影响其它内容。大多数常见的 QSPI Flash 的最小擦除单位大小是 4KB,片内 Flash 是 1KB,因此这个信息块的大小可按照 4KB 大小设计,执行区域和备份区域各一个,共计 8KB 大小。

信息块中包含以下信息:

  • 固件版本,用于判断备份区域的固件是否为新的固件,是否执行固件更新操作

  • 固件大小,需要复制备份区域多少内容到执行区域

  • 信息块内容的校验值,建议使用是 CRC 校验,而不建议使用 BCC 校验,目的是证明这个信息块的内容可信,保证信息块的完整性

为什么说不建议使用 BCC 校验呢?一般来说,Flash 擦除信息后,读取的数据一般是 0xFF,如果使用 BCC 校验,其校验值只能是 0x00 或 0xFF,如果是 0xFF,则又与擦除后的 Flash 中的数据相同,起不到校验的效果,而 0x00 又容易碰撞到别的数据。

一般来说,上位机发送过来的文件中不包含版本号信息,那如何进行版本号管理呢?可在微控制器中自行管理:当新的固件下载好后,其版本号按照如图6方式管理:

6.png

图6 版本号管理

当 2nd Bootloader 进入到 ISP 模式后,会执行固件下载的过程,从外界接收新的固件,随后复位微控制器,进入执行应用程序的模式。

固件下载过程的流程图如图7 所示:

7.png

图7 固件下载过程

当 2nd Bootloader 进入到执行应用程序的模式后,并不会直接运行应用程序,而是执行固件更新过程,将备份区域的新固件复制到执行区域,待固件更新过程完毕后,才会执行应用程序。

事实上,每次 2nd Bootloader 进入到执行应用程序的模式后,都会执行固件更新过程,固件更新包括了检查是否下载了新固件和是否已经完成复制新固件到执行区域两部分内容,如果没有新的固件包在备份区域的话,固件更新程序就会提前结束,直接执行应用程序。

固件更新过程如图8所示:

8.png

图8 固件更新过程

过程分析

为了保证 OTA 过程的安全,我们需要保证备份区域和执行区域至少有一个固件是完整的,这样,当备份区域的固件破坏时,我们能够保证执行区域的固件还能用,而执行区域的固件损坏时,还能从备份区域恢复过来。

那怎么确定某区域的固件是完整的呢?

通过图7和图8可知,不管是下载固件到备份区域,还是复制备份区域固件到执行区域,只要有写固件的操作,都会先将对应区域的信息块进行擦除,直到写固件完成,才会把新的信息块写入到指定区域。因此,我们可以通过信息块是否完整来判断该区域的固件是否完整,信息块的校验值是验证信息块完整性的保证。

不管是人事档案,学生档案,还是党员档案,在档案袋上都会贴有密封条,并且盖有部门的红章,这个密封条就是证明档案内容完整性的保证。Flash 中的固件,就相当于档案袋里面的内容,信息块就相当于档案袋上的封条,版本号相当于封条上的日期,校验值相当于封条上的红章,擦除信息块的过程,相当于给 Flash 进行撕封条的操作,而写入信息块的过程,就相当于给 Flash进行贴封条的过程,通过这个封条,就能够证明 Flash 中的内容是完整的。

有了“封条”证明固件的完整性,还需要确定什么时候才能拆“封条”:

拆封条的原则就是:保证两个区域至少有一个固件是完整的。因此,在擦除其中一个区域的信息块时,需要查看另一个区域的信息块是否完整,只有另一个区域的信息块是完整的,才能够擦除本区域信息块的内容。

在固件下载的过程中,如果发现执行区域的信息块不完整,那就暂时还不能擦除本区域的信息块,需先执行固件更新过程,将备份区域的固件写入到执行区域,在写入执行区域的信息块后,才能继续擦除备份区域的信息块。一般来说,执行区域的信息块如果不完整,则说明该设备在上次固件更新过程中,意外掉电,固件更新失败。

在固件更新的过程中,如果备份区域的信息块不完整,则说明固件还没有下载成功,自然是不能继续进行固件更新的,就得跳过固件更新的过程。如果备份区域的固件版本小于(一般不会小于)或等于执行区域的固件版本,则说明备份区域的固件和执行区域的固件一样,没有必要固件更新,也要跳过固件更新的过程。只有备份区域的信息块完整,且固件版本大于执行区域时,或者执行区域没有完整的信息块时,才可以擦除执行区域的信息块,对执行区域的内容进行写操作。

有一种情况,执行区域和备份区域是都没有完整的固件的,那就是产品硬件生产完成后,还没有烧录程序的时候。

当产品硬件生产完成后,微控制器内部只有一个 2nd Bootloader,进入到固件下载的过程时发现,执行区域没有完整的信息块,再跳转到执行固件更新过程,但固件更新过程发现备份区域也没有完整的信息块,2nd Bootloader 傻眼了,咋哪都没有完整固件呢?

因此,在固件下载过程中,若发现执行区域和备份区域都没有完整信息块时,还是得允许向备份区域写固件。

当意外出现在任何没有操作 Flash 的时候,再次上电后,不影响应用程序的正常执行,不影响固件更新,也不影响固件下载的过程;当意外出现在对备份区域的 Flash 操作时,执行区域的固件还能够正常执行,也不影响重新执行固件下载的过程。当意外出现在对执行区域的 Flash 操作时,再次上电后,重新进行固件更新,应用程序还是能够正常运行。所以,这个完整的 OTA 过程,处处安全。

测试

根据本文讲述的过程,实现带 OTA 功能的 2nd Bootloader,进行验证。

下载新的固件前效果,如图9所示:

9.png

图9 下载新固件前的应用程序

下载固件过程中中断下载,通过观察应用程序打印的时间可知,微控制器会继续执行更新固件前的应用程序,如图10所示:

10.png

图10 Ymodem 升级失败后的应用程序

当固件下载成功后,在进入应用程序前,立刻按下复位按键,产生一次固件更新失败的事件,随后释放复位按键,观察通过应用程序打印的时间可以发现,新的固件在再次复位之后,依然进行了更新,如图11所示:

11.png

图11 固件更新失败后,再次复位程序

结语

本文主要针对 OTA 升级的流程进行了讲解,把 OTA 升级的过程拆分为固件下载和固件更新两个过程,每个过程都想办法保证设备复位后,执行区域总有可用固件,保障 OTA 过程中如果发生意外,不至于让设备变 “砖”。

OTA 升级的方法不仅仅只有串口一种,我们还可以使用 I2C 接口,SPI 接口升级固件,如果支持 USB Device,那么 微控制器微控制器可以模拟成为一个 U 盘,插在电脑上,将二进制文件放到这个 U 盘中更新固件,如果支持 USB Host,则可以将固件放到 U 盘中,微控制器读取 U 盘中的数据更新固件,或者将 U 盘换成 SD 卡…… 实现 OTA 功能的方法极多。

当 OTA 的方法越来越多时,2nd Bootloader 也随之变得臃肿起来。其实,我们也可以把 OTA 的大部分内容做到应用程序中,仅将备份区域的应用程序转移到执行区域这个核心功能保留到 2nd Bootloader 中,并且提供对备份区域 Flash 擦除和写操作的 API 即可,应用程序中的 OTA 功能,将会把下载到的新固件通过 2nd Bootloader 提供的 API 写入到备份区域,随后复位 MCU,让 2nd Bootloader 将新固件再转移到执行区域即可。

为了减少下载新固件所需的流量,其固件包可能进行了压缩处理,而解压过程可以放在上位机中,让上位机发送给微控制器的固件就是经过解压后的固件包;或者可以放在应用程序中,在应用程序中进行解压后写入到 Flash 中,或者放在 2nd Bootloader 中,从备份区域放到执行区域时再进行解压;第三种方法虽然可以减少备份区域占用的空间,但需要将压缩算法确定好,永久不会再进行改动——2nd Bootloader 虽然灵活,但就像微控制器厂商出厂微控制器前,将 1st Bootloader 固化到微控制器中那样,2nd Bootloader 只能在产品厂商将产品出厂前写入到微控制器中,产品一旦售出,就不像应用程序可以通过 OTA 升级了,除非进行产品召回,否则就没有修改的可能,因此,放在 2nd Bootloader 中的代码,一定要谨慎对待。

来源:灵动MM32MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 30

需求

前文中实现了一款简单的 2nd Bootloader,能够跳转执行存储在 QSPI Flash 中的应用程序,但 2nd Bootloader 如果仅仅只是用于跳转执行程序的话,岂不是有些太简单了?从本章开始,将会讲解几种 2nd Bootloader 进阶设计,实现类似 ISP 更新固件的功能,以及在 OTA 升级时避免变“砖”等设计,以及讲解一些 2nd Bootloader 的程序设计思路。

本文将以 Ymodem 协议获取应用程序的二进制文件为例,实现类似 ISP 更新固件的功能。

需要注意:

  • 下文中提到的 ISP 仅指由 2nd Bootloader 实现的定制 ISP,而非微控制器本身的 ISP。

  • 上位机发送的文件是二进制(.bin)文件,而不是 Intel Hex 标准的 hex (.hex)文件。

  • 目前仅考虑直接覆盖的方式烧写程序,即获取到一段二进制数据后,直接写入到 QSPI Flash 的对应位置。

Ymodem 介绍

Ymodem 协议是一个文件传输协议,通常用于在资源受限的设备中传输文件,它可以一次传输1024字节的信息块,同时还支持传输多个文件。

Ymodem 协议有较多的变种,本文使用的是常用的 Ymodem-1K 协议。

通信时序

通讯时序如图1:

1.png

图1 Ymodem 通信时序

帧格式

Ymodem 有两种帧格式:

帧头为 SOH 时,信息块长度为128字节,总长度133字节。

帧头为 STX 时,信息块长度为1024字节,总长度1029字节。

两种帧的帧格式如表1所示:

2.png

表1 SOH / STX 帧格式

包号从0x00起始,每成功传输一帧数据后包号加1,计数到0xFF后,下一次包号重新从0x00开始计数。

包号反码是包号取反的数值,如0x00的包号,包号反码为0xFF,0x01的包号,包号反码为0xFE。

信息块是要传输的具体数据块,起始帧包含了文件名和文件大小,数据帧包含了分段的数据内容。

校验采用 CRC 校验,仅校验信息块的内容。

除了两种帧格式外,还有 ACK、NAK、CAN、EOT、字符 'C' 五种命令,长度仅有1字节。

起始帧、数据帧、结束帧

起始帧采用帧头为 SOH 的帧格式传输,包号为 0x00,信息块中包含文件名字符串和文件大小字符串 (十进制表示),字符串以0x00结尾,信息块剩余部分以0x00填充。

数据帧采用帧头为 STX 的帧格式传输,包号从0x01开始计数,信息块中包含分段的文件内容。

当最后一段要发送的数据块大小超过128字节但小于1024字节时,采用帧头 STX 的帧格式传输,信息块结尾用 0x1A 填充。小于128字节,采用帧头为 SOH 的帧格式传输,信息块结尾依然用 0x1A 填充。

结束帧和起始帧一样,唯一不同的是没有文件名和文件大小,即信息块的内容全为 0x00。

通讯指令

通讯指令如表2所示。

3.png

表2 Ymodem通讯指令

协议实现

CRC 校验:

Ymodem 协议中提供了 CRC 校验的 C 代码片段,但由于该协议发布时的 C 标准不同于现在,因此不能直接使用,此处提供一份 CRC 校验的实现代码,通过调用 crc_calc() 来实现对信息块内容的校验:

    static uint16_t crc_update(uint16_t crc, uint8_t data)
{
    uint32_t crc32 = crc;
    uint32_t data32 = data;

    for (uint32_t i = 0u; i < 8u; i++)
    {
        if (0 != (crc32 & 0x8000u) )
        {
            crc32 <<= 1u;
            crc32 += ( ( (data32 <<= 1) & 0x0100u) != 0u);
            crc32 ^= 0x1021;
        }
        else
        {
            crc32 <<= 1u;
            crc32 += ( ( (data32 <<= 1) & 0x0100u) != 0u);
        }
    }

    return (uint16_t)(crc32 & 0xFFFFu);
}

static uint16_t crc_calc(uint8_t * buffer, uint32_t size)
{
    uint16_t crc = 0u;

    for ( uint32_t i = 0u; i < size; i++)
    {
        crc = crc_update(crc, buffer[i]);
    }
    crc = crc_update(crc, 0u);
    crc = crc_update(crc, 0u);

    return crc;
}

当我们需要对一段数据进行 CRC 计算时,调用 crc_calc() 函数,传入数据起始地址和数据长度即可计算出 CRC 校验值。

STX 包处理:

通过对 Ymodem 协议的介绍可知,STX 包只在接收数据的过程中使用,因此收到 STX 包时,仅需要进行如下处理:

  • CRC校验信息块。

  • 计算信息块的有效数据长度(需要注意最后一帧数据的有效长度不定)。

  • 存储数据。

  • 发送 ACK 指令或 NAK 指令。

SOH 包处理:

SOH 包的处理要比 STX 包的处理复杂,因为包含了起始帧和结束帧的处理。

起始 / 结束帧和数据帧只能通过当前状态来判断,其中,除信息块长度不同外,数据帧的处理同 STX 包的处理一致。

由于 Ymodem 可以多文件传输的特性,处于该收到结束帧的状态时也有可能收到起始帧,因此起始帧和结束帧需要进行一个判断:信息块第一个字节是否为 0x00。如果不是 0x00 则为起始帧,否则为结束帧。

起始帧要携带文件名和文件大小,信息块的第一个字节一定是一个可显示的字符,当收到起始帧时,需进行如下处理:

  • 读取文件名和文件大小。

  • 进入读数据块的状态。

  • 发送 ACK。

  • 发送 字符 'C'。

结束帧的信息块全为0x00,收到结束帧时,需进行如下处理:

  • 发送 ACK。

  • 结束 Ymodem 传输。

EOT 指令处理:

EOT 代表本次文件传输结束(但不代表所有文件都已发送完毕),因此,收到 EOT 指令时,需将当前状态调整为起始状态,准备接收新的文件,具体处理如下:

  • 进入起始状态。

  • 发送 ACK。

  • 发送字符 'C'。

CAN 指令处理:

CAN 是 cancel 的缩写,当收到 CAN 指令后,表示后续的 Ymodem 传输终止,该指令是双向的,既可以由 Host 发送, 也可以是 Device 发送,收到 CAN 指令后,具体操作如下:

  • 退出 Ymodem 传输。

接收超时处理:

Device 在接收数据前,会先向 Host 发送字符 'C',但如果此时 Host还没有将文件准备好,则会卡死在准备接收状态。

Device 在接收数据过程中,如果少接收到某个字节数据,信息不完整,则会卡死在接收数据的过程中。

Device 发送某个指令后,Host 可能没有收到指令,不会继续下一帧数据的发送,Device 还是会卡死在接收的过程中。

因此,需要引入接收超时的操作。

当接收超时后,判断状态,如果是起始状态,且没有收到任何字节,则可能是 Host 还没有准备发送文件,重新发送字符 'C'。

如果数据没有接收完整,则可能是少收到几个字节的数据,发送 NAK,让 Host 重新发送数据。

如果没有收到数据,则 Host 可能没有收到回复的指令,重新发送上次发送的指令。

软件设计

就像是计算机进入 BIOS 设置,需要用户在开机的瞬间不停按下键盘上某个按键那样,为了使 2nd Bootloader 知道自己是该跳转执行应用程序,还是进入 ISP 等模式,需要外界有一个输入:这个输入可以是某个引脚的电平变化,也可以是在有限的时间里通过某种通信接口获取到一段外界指令,当 2nd Bootloader 读取到这个来自外界的输入后,才能知道自己接下来要干什么。因此,除了实现 ISP 下载的功能外,我们还需要实现选择工作模式的功能,如图2所示:

4.png

图2 软件设计

Ymodem 只是获取二进制文件的一种方式,除了 Ymodem,我们也可以采用 Xmodem,Zmodem协议,除了串口,还可以使用 CAN,甚至通过 USB 读取 U 盘里的文件等方式。

综上所述,在设计 2nd Bootloader 时,不能绑死选择工作模式的方式,也不能绑死 ISP 的工作方式,甚至,不能绑死 2nd Bootloader 只能在两种工作模式下二选一(不要使用 if & else 的语句区分工作模式,而应使用 switch 语句区分工作模式),因此,2nd Bootloader 的顶层应用逻辑,只能是下面的设计:

int main(void)
{
    ......
    switch (get_run_mode())
    {
        case EXEC_QSPI:
            jump_to_app(QSPI_BASE);
            break;
        case ISP:
            isp();
            break;
        ......
        default:
            jump_to_app(QSPI_BASE);
            break;
    }
    ......
}

如果我们期望从某种工作模式下切换到另一种工作模式,最好的做法是先让外界输入保持为目标工作模式的状态,然后让微控制器复位,再次进入 2nd Bootloader,这样的做法是能够保持微控制器切换工作模式后,仍然保持相对 “干净” 的环境状态,例如,微控制器前一次进入到了 ISP 模式,通过串口更新了应用程序,如果直接跳转到应用程序,则发现串口依然保持打开的状态,这对应用程序而言可能不是期望的结果,那提前关闭串口呢?还有 GPIO 引脚的配置没有改动……最简单省事的做法,其实就是直接让微控制器复位,而串口和串口的 GPIO 引脚也就会在微控制器复位之后,处于默认相对比较 “干净” 的状态。这也是为什么图x所示的流程图,ISP 模式的下一步是复位微控制器。

当然,如果在 get_run_mode() 的时候就用到了串口,那还是老老实实在 get_run_mode() 执行到 return 之前,就把串口和 GPIO 处理干净。

这里提一个比较“花”的设计方法,我们可以把 ISP 也做成应用程序,下载到片内 Flash 中 一块确认好的位置(假设起始地址为 ISP_BASE),然后同样使用 jump_to_app() 跳转,只是输入参数从 QSPI_BASE 变为了 ISP_BASE,这个做法会用在 USB DFU 模式上,因为一旦进入了 USB DFU 模式,USB 就不能再作为其它设备进行工作,当 USB 设备支持 USB DFU 时,就需要使用这种办法单独进入到 DFU 模式下。

测试

选择工作模式:

在这里,我们通过读取指定引脚的电平状态来确定该进入何种工作模式。

uint32_t get_run_mode()
{
    ......
    if (GPIO_ReadInDataBit(BOARD_BOOT_GPIO_PORT, BOARD_BOOT_GPIO_PIN))
    {
        return EXEC_QSPI;
    }
    else
    {
        return ISP;
    }
    ......
}

ISP 模式:

当进入 ISP 模式后,开始使用 Ymodem 协议接收数据。

void isp()
{
    ......
    /* get new app bin & write to qspi flash. */
    ymodem_recv_start(&ym, 100000);
    while(0 == (YMODEM_STATUS_DONE &ym.status) )
    {
        ymodem_recv_byte_handler(&ym);
    }

    /* reset mcu. */
    __set_FAULTMASK(1);
    NVIC_SystemReset();
}

生成应用程序的二进制文件:

我们仍然以 MindSDK 的 hello_world 样例工程为例,修改其 Linker 文件并检查代码,使其成为一个可存储在 QSPI Flash 上的应用程序,随后在 MDK 工程中,点击魔术棒(Options for Target...),点击 User 列表,如图3所示,在指定位置(红框中的 User Command)加入下面这句话,并在前面打上对勾:

fromelf.exe --bin -o "@L.bin" "#L"

然后编译工程,就能在工程文件所在的目录下找到生成的 bin 文件。

5.png

图3 生成二进制文件

本文使用 TeraTerm 软件进行 Ymodem 传输文件,如图4所示:

6.png

图4 TeraTerm Ymodem 发送文件

但在测试时发现,当文件传输到 100% 时,TeraTerm 并没有结束传输,但对 2nd Bootloader 的代码进行分析后并没有发现存在逻辑问题,因此对 TeraTerm 的 Ymodem 协议产生了怀疑,如图5所示。

7.png

图5 TeraTerm 传输文件,总卡在 100% 处

使用两个 USB 串口模块,将其 TXD 与 RXD 相连,其中一个串口模块使用 TeraTerm 打开,另一个使用 SSCOM 打开(为了能够发送和显示一些非字符类的控制指令),使用 TeraTerm 的 Ymodem 协议发送文件, SSCOM 接收来自 TeraTerm 的数据,并按照  Ymodem 协议回复指令,模拟完整的 Ymodem 传输协议,如图6所示。

8.png

图6 模拟 Ymodem 协议传输文件过程

结果发现,TeraTerm 实现的 Ymodem 协议在发送单个文件的时候,存在以下问题:

  • 发送 EOT 指令后,需接收两次 ACK 和字符 ‘C’。

  • 没有发送 last block。

因此,我们需要针对 TeraTerm 的问题,对 Ymodem 的实现做一些改动,或者使用其它软件通过 Ymodem 传输二进制文件。修改后的时序图如下:

9.png

图7 针对 TeraTerm 的 Ymodem 实现进行的改动

删去了对 last block 的接收,并且在收到 EOT 后,主动发送两次 ACK 和 字符 ‘C’,经过修改后测试,TeraTerm 的 Ymodem 能够按照传输完成的方式正常退出。

下载程序,如图8所示:

10.png

图8 下载应用程序

运行应用程序,如图9所示:

11.png

图9 运行应用程序

结语

本文在 2nd Bootlaoder 的基础上实现了基于 Ymodem 协议的 ISP 功能,能够通过复位后指定引脚的电平状态来区分该执行应用程序还是进入 ISP 模式,进入 ISP 模式后,可以使用 TeraTerm 等软件,通过串口,使用 Ymodem 协议将二进制文件下载到与微控制器连接的 QSPI Flash 中,实现固件更新的功能。

但本文并没有对固件更新过程中可能出现的意外进行处理,所以这种 ISP 的办法不能直接用在 OTA 升级中,在下一章中,我们将会探讨 OTA 升级时可能会出现的意外情况,并且进行处理。

来源:灵动MM32MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 253

相关阅读:

mm32-2nd-bootloader技术白皮书(1)——配置软硬件环境

mm32-2nd-bootloader技术白皮书(2)——QSPI外设简介

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

mm32-2nd-bootloader技术白皮书(4)——设计实现简单的2nd bootloader

mm32-2nd-bootloader技术白皮书(5)——编译可在QSPI Flash上运行的程序

mm32-2nd-bootloader技术白皮书(6)——总结及注意事项

引言

上文描述了如何将一个样例工程下载到 QSPI Flash 并运行,那么就会有一个新的问题:将应用程序存储在 QSPI Flash、片内 Flash 以及片内 SRAM 上,执行的效果又如何?

本章将通过在不同 Flash 中执行相同的测试程序,记录其执行程序所花费的时间,验证不同 Flash 对微控制器执行程序性能的影响。

在进行 Flash 速度验证前,我们需要知道如何获取各 Flash 的速度:

  • QSPI Flash 访问速度可通过 QSPI 的 SCK 波特率直接求出。

  • 片内 Flash 的访问速度可通过 DataSheet 手册或寄存器配置得到。

  • 通常,片内 SRAM (系统时钟速度访问)的访问速度与系统时钟速度保持一致。

若要验证在不同 Flash 中程序的执行速度,还需考虑以下几点:

  • 验证程序中要包含适量的小循环,从而展现 ICACHE 和 DCACHE 对执行程序带来的性能提升。

  • 验证程序中要包含适量的长跳转,使 CPU 需要重新访问 Flash 获取指令和数据,从而展现不同 Flash 对执行程序速度带来的影响。

  • 验证程序要足够复杂,且 code size 要足够大,尽量消除偶然性因素,从而能够模拟真实使用场景,使验证结果更加可信。

  • 验证程序应尽量使用通用的算法应用,可以在多平台中进行适配,且能够进行横向比较。

验证环境

MCU F5270/F5280

  • 测试所用开发板

    POKT-F5270 (MM32F5277E9PV),外扩 QSPI Flash

    POKT-F5280 (MM32F5287L9PV),合封 QSPI Flash

  • 开发工具

    MDK5.37(ARM Compiler: v6.18)

  • MCU F5270/F5280 配置

    System Clock:120MHz

    AHB Clock:120MHz

    APB1 Clock:60MHz

    APB2 Clock:60MHz

  • ICACHE: 开启

  • DCACHE: 开启

  • FPU: 开启 (单精度)

  • 片内 SRAM

    Base:0x30000000

    Speed:120MHz(1 访问周期 + 0 等待周期)

  • 片内 Flash

    Base:0x08000000

    Speed:24MHz(1 访问周期 + 4 等待周期)

  • QSPI Flash

    Base:0x90000000

  • 存放数据的 RAM base:0x20000000 (with DTCM),在测试用例中使用另外的 RAM 存放程序

QSPI Flash

  • 测试所用开发板:

    型号:W25Q128JVSIQ,FM25Q16A

  • SCK波特率:30MHz (MM32F5270 only,120MHz AHB 时钟 4 分频) 与 60MHz (MM32F5280 only,120MHz AHB 时钟 2 分频)

    受外界环境(线路不等长,阻抗不匹配等因素)和 GPIO 电平翻转速度(电平上升下降沿所需时间)影响,片外 QSPI Flash难以在 SCK 波特率 60MHz 的环境下读取到正确的数据,因此 60MHz 验证只在 MM32F5280 上进行。

    其余验证程序均在 POKT-F5270 开发板上进行运行。

  • SPI 模式:SPI 模式 3 (CPOH = 1, CPHA = 0)

    POKT-F5270 (MM32F5277E9PV),外扩 QSPI Flash

    POKT-F5280 (MM32F5287L9PV),合封 QSPI Flash

  • 工作模式:QPI 模式(各通信阶段线宽皆为四线)下进行 Fast Read

  • 指令线宽:4-line

  • 指令位宽:8-bit

  • 地址线宽:4-line

  • 地址位宽:24-bit

  • 空指令周期数:2-sck_cycles

  • 数据线宽:4-line

  • 交互方式:

1.png

由交互方式可知,无论向 QSPI Flash 读取多少字节数据,读取数据前都会有 10 个 SCK 时钟准备数据。

Arm CMSIS-DSP FFT 验证

FFT (快速傅里叶变换),是一种能够将一段离散的波形数据转换为频谱数据的算法。

CMSIS-DSP 中 FFT 计算的 API,具有良好的可移植性,可在 ARM 内核的芯片中进行横向对比,具有可信性,且它的 FFT 计算涉及到大量的循环和跳转等操作,用例的 code size 足够大,计算方法足够复杂,可充分展现 CPU 和 Flash 之间配合,适合用于当前各 Flash 对微控制器性能影响测试。

验证方法

使用 Arm CMSIS-DSP FFT 验证方法时,可指定一段波形数据,通过 FFT 进行一次正向运算,得出频谱数据,再将频谱数据进行一次 FFT 反向运算,得出原始的波形数据。

使用 SysTick 定时器记录时间,由于进行一次正向运算和一次反向运算所需的时间较短,因此循环多次(1000次),统计计算所需的时间。计算所需的时间越短,表明在该 Flash 上执行程序的性能越好,记录完一次时间后,调整程序的优化等级,再次进行验证。

验证用例简介

用例中主要使用函数 fft_test_init_f32() 与 fft_test_run_f32() 执行 FFT 转换。

  • fft_test_init_f32()

    对一块 float32_t 类型的buffer进行初始化,生成一段波峰为1,波数为100,采样点共1024个的波形数据。

  • fft_test_run_f32()

    初始化 FFT,对 buffer 中的内容进行正向 FFT 转换,将波形数据转换为频谱数据,替换 buffer 中原有的数据;

    再对 buffer 中的内容进行逆向 FFT 转换,将频谱数据转换为波形数据,替换 buffer 中原有的数据;

    循环正向 FFT 转换和逆向 FFT 转换,循环次数为 test_times 次。

  • Arm CMSIS-DSP FFT 用例的验证方法

    调用 fft_test_init_f32() 函数对一块 float32_t 类型的 buffer 进行初始化。

    开启 SysTick 定时器,每毫秒产生一次中断,实现计时功能。

    记录开始验证的时间,调用 fft_test_run_f32() 函数,test_times 为 1000。

    记录验证结束的时间,打印验证花费的时间。

验证结果

在 QSPI Flash(60MHz与30MHz),片内 SRAM 和片内 Flash 上运算 Arm CMSIS-DSP FFT 的性能数据及代码变量如表1所示。

此处需注意,由于浮点数记录数据时会有精度问题,经过 1000 轮 FFT 转换后的波形数据与 FFT 转换前的波形数据相比,会有轻微变化,属正常现象。

2.png

表1 各 Flash 运行 FFT 的性能数据对比

以 -ofast 优化为例,片内 SRAM 的运算时长为基准单位,各 Flash 进行 FFT 运算所需时长对比如图1所示。

3.png

图1 各 Flash 的 FFT 运算速度比较 (-ofast)

验证程序

Arm CMSIS-DSP FFT 测试程序包含三种优化等级程序:

  • o0 文件夹

    编译优化选项为 -o0 的验证程序,包含片内 Flash,片内 SRAM 和 QSPI Flash 三种环境的工程。

  • ofast 文件夹

    编译优化选项为 -ofast 的验证程序,包含片内 Flash,片内 SRAM 和 QSPI Flash 三种环境的工程。

  • oz 文件夹

    编译优化选项为 -oz image size 的验证程序,包含片内 Flash,片内 SRAM 和 QSPI Flash 三种环境的工程。

当程序运行在片内 SRAM 或 QSPI Flash 时,需要 bootlader 进行引导,bootloader文件夹中提供三种环境下的 bootloader:

  • pokt-f5270_bootloader_qspi_sckdiv2_mdk

    验证程序执行在 QSPI Flash 上,且需要 SCK 时钟为 AHB 时钟的二分频(AHB 时钟为 120MHz 时 SCK 波特率为 60MHz)。

  • pokt-f5270_bootloader_qspi_sckdiv4_mdk

    验证程序执行在 QSPI Flash 上,且需要 SCK 时钟为 AHB 时钟的四分频(AHB 时钟为 120MHz 时 SCK 波特率为 30MHz)。

  • pokt-f5270_bootloader_sram_mdk

  • 测试程序执行在片内 SRAM 上,该 bootloader 仅实现引导 CPU 执行片内 SRAM 上的程序,不实现加载程序到片内 SRAM 的功能。下载本 bootloader 后,可使用如 JLink 等工具,将验证程序加载到片内 SRAM 中。即使微控制器发生复位,片内 SRAM 中的程序也不会被擦除。

Mbed-TLS RSA1024 验证

RSA1024 是一种非对称加密算法,常用于网络通信时的数据加密。Mbed-TLS RSA1024 计算量大,包含大量循环,跳转等操作,且其为纯应用代码,便于移植,计算方法复杂,可用于当前各 Flash 对微控制器性能影响测试。

验证方法

指定公钥和私钥,对一段指定的数据进行 RSA1024 加密和解密。

使用 SysTick 定时器记录时间,由于进行一次加密和解密的时间较短,需将一组加密与解密循环多次 (10次),记录所需时间。

计算所需的时间越短,表明在该 Flash 上执行程序的性能越好,记录完一次时间后,调整优化等级,再次进行验证,不得使用专用的加解密硬件外设协助计算。

验证用例简介

用例中主要使用函数 rsa_test_init() 与 rsa_test_run() 执行数据加密。

  • rsa_test_init()

    对存放解密数据的 buffer1 进行初始化,存放原始数据。

    对存放加密数据的 buffer2 进行初始化,填充 0x00。

    加载 RSA1024 公钥和私钥。

  • rsa_test_run()

    将存放解密数据的 buffer1 中的内容进行加密,并将加密后的数据存放到 buffer2 中。

    将存放加密数据的 buffer2 中的内容进行解密,并将解密后的数据存放到 buffer1 中。

    循环以上操作 test_times 次。

  • 验证方法

    调用 rsa_test_init() 函数对一块 float32_t 类型的buffer进行初始化。

    开启 SysTick 定时器,每毫秒产生一次中断,实现计时功能。

    记录开始验证的时间,调用 rsa_test_run() 函数,test_times 为 10。

    记录验证结束的时间,打印验证花费的时间。

验证结果

在 QSPI Flash(60MHz与30MHz),片内 SRAM 和片内 Flash 运行 Mbed-TLS RSA1024 程序的性能数据,如表2所示。

4.png

表2 各 Flash 运行 Mbed-TLS RSA1024 的性能数据

以 -ofast 优化为例,片内 SRAM 的运算时长为基准单位,各 Flash 进行 RSA1024 运算所需时长对比如图2所示。

5.png

图2 RSA1024 运算速度对比 (-ofast)

验证程序

Mbed-TLS RSA1024 测试程序包含三种优化等级程序:

  • o0 文件夹

    编译优化选项为 -o0 的验证程序,包含片内 Flash,片内 SRAM 和 QSPI Flash 三种环境的工程。

  • ofast 文件夹

    编译优化选项为 -ofast 的验证程序,包含片内 Flash,片内 SRAM 和 QSPI Flash 三种环境的工程。

  • oz 文件夹

    编译优化选项为 -oz image size 的验证程序,包含片内 Flash,片内 SRAM 和 QSPI Flash 三种环境的工程。

当程序运行在片内 SRAM 或 QSPI Flash 时,需要 bootlader 进行引导,三种环境下的 bootloader 结构与 Arm CMSIS-DSP FFT 验证程序中对 bootloader 的介绍相同。

需要注意,MbedTLS RSA1024 用例未使用 MicroLIB库

  • 验证时发现,使用 MicroLIB 后,在片内 SRAM 中执行 calloc 函数无法申请内存空间,影响 RSA1024 运算,因此在工程配置选项中选择Options for Target ... ->  Target  取消勾选  Use MicroLIB 。

  • 为使用 printf() 函数打印验证结果,在工程配置选项中选择 Manage Run-Time Environment ->  Compiler -> I/O 勾选 STDERR , STDIN , STDOUT 。程序配置如图3所示。

6.png

图3 Manage Run-Time Environment 配置

Helix MP3 Decoder 验证

Helix MP3 解码库作为一款开源的 MP3 解码组件,常在多媒体应用中使用。

Helix MP3 Decoder为纯应用代码,便于移植,且其在进行 MP3 解码的过程中,需要进行大量的循环和长跳转操作,适合用于当前各 Flash 对微控制器性能影响测试。

验证方法

使用 Helix MP3 Decoder 验证方法,指定一段 MP3 文件,将其文件中的原始数据存放在待测试的 Flash 中。

使用 Helix MP3 解码库将该 MP3 文件的原始数据解码为 PCM 格式。

使用 SysTick 定时器记录时间,由于指定的 MP3 文件较小,且 Helix MP3 解码速度较快,因此循环多次(10次),统计计算所需的时间。

计算所需的时间越短,表明在该 Flash 上执行程序的性能越好。完成一次时间统计后,通过调整程序优化等级,再次进行验证。

验证用例简介

用例中主要使用函数 mp3_dec_test_run() 执行 MP3 解码。

  • mp3_dec_test_run()

    初始化 Helix MP3 Decoder。

    循环寻找下一个 MP3 的同步帧的起始位置,并开始解码这一帧 MP3 原始数据,直至 MP3 文件全部解码。

    释放 Helix MP3 Decoder。

    循环上述的 MP3 解码过程,循环次数为 test_times 次。

  • 验证方法

    开启 SysTick 定时器,每毫秒产生一次中断,实现计时功能。

    记录开始验证的时间,调用 mp3_dec_test_run() 函数,test_times 为 10。

    记录验证结束的时间,打印验证花费的时间。

验证结果

在 QSPI Flash(60MHz,30MHz),片内 SRAM 和片内 Flash 中运行 Helix MP3 Decoder 所获取的性能数据,如表3所示。

7.png

表3 各 Flash 运行 Helix MP3 Decoder 的性能数据

以 -ofast 优化为例,片内 SRAM 的运算时长为基准单位,各 Flash 进行 Helix MP3 Decoder 运算所需时长对比,如图4所示。

8.png

图4 mp3 dec test 运算速度对比 (-ofast)

验证程序

Helix MP3 Decoder 测试程序包含三种优化等级程序:

  • o0 文件夹

    编译优化选项为 -o0 的验证程序,包含片内 Flash,片内 SRAM 和 QSPI Flash 三种环境的工程。

  • ofast 文件夹

    编译优化选项为 -ofast 的验证程序,包含片内 Flash,片内 SRAM 和 QSPI Flash 三种环境的工程。

  • oz 文件夹

    编译优化选项为 -oz image size 的验证程序,包含片内 Flash,片内 SRAM 和 QSPI Flash 三种环境的工程。

当程序运行在片内 SRAM 或 QSPI Flash 时,需要 bootlader 进行引导,三种环境下的 bootloader 结构与 Arm CMSIS-DSP FFT 验证程序部分介绍的相同。

总结

本文通过在 QSPI Flash,片内 Flash 与片内 SRAM 中分别运行测试工程 Arm CMSIS-DSP FFT、Mbed-TLS RSA1024 与 Helix MP3 Decoder,获取微控制器性能数据,从而对比在不同 Flash 位置的执行速度的差异。

通过对比上述验证数据可知:

  • 不同型号 QSPI Flash 的访问速度受 SCK 波特率影响,当访问 QSPI Flash 的方法一致时,不同型号的 QSPI Flash 访问速度一样。

  • 同一优化选项下,使用不同验证程序,不同 Flash 位置的执行速度存在一定差异。

MM32F5270 系列芯片具备 ICACHE 和 DCACHE,验证执行速度比较小的程序,说明具备良好的时间局部性和空间局部性,具有较高的 CACHE 命中率,减少了访问 Flash 所花费的时间;验证执行速度比较大的程序,说明执行程序时,进行了较多较大范围的跳转操作,需不断访问 Flash,刷新 CACHE,造成执行速度变慢。

当然,即使关闭 ICACHE 和 DCACHE,执行速度比也是会存在一定差异的,这是由于 QSPI Flash 的访问方式中规定了不论读取多少字节的数据,都会包含一个指令阶段,一个地址阶段和一个空指令阶段,需要花费 10 个 SCK 时钟周期,因此造成读取 2n 字节数据花费的时间和 n 字节数据花费的时间,不是简单的二倍关系,造成执行速度比存在一定差异。

来源:灵动MM32MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 21

相关阅读:

mm32-2nd-bootloader技术白皮书(1)——配置软硬件环境

mm32-2nd-bootloader技术白皮书(2)——QSPI外设简介

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

mm32-2nd-bootloader技术白皮书(4)——设计实现简单的2nd bootloader

mm32-2nd-bootloader技术白皮书(5)——编译可在QSPI Flash上运行的程序

总结

为了扩展微控制器可用的 Flash 空间,用户可以使用 QSPI 接口连接 QSPI Flash;为了让用户能尽量像使用片内 Flash 那样下载和运行程序,减少额外操作,需要有对应的下载算法与相应的 2nd Bootloader。

本文以 PLUS-F5270 开发板为例(该开发板搭载的 MM32F5277E9PV 微控制器具备 QSPI 接口,且板载 W25Q64JVISQ QSPI Flash 芯片),介绍了 MM32F5 系列芯片的 QSPI 引脚,以及与 QSPI Flash 通信的过程,在 MDK 平台上实现客制化的下载算法,简单的 2nd Bootloader,并以 MindSDK 的 hello_world 样例工程为例,进行少量的修改,使其能够存储在 QSPI Flash 上,最终验证了微控制器能够运行存储在QSPI Flash 上的应用程序。

注意事项

  • 外置 QSPI Flash选型

在进行 QSPI Flash 的选型过程中,尤其要关注其工作电压的范围,部分 QSPI Flash 的额定工作电压为 1.8V ,若在3.3V 电压下,是无法正常工作的,反之亦然。

除了电压范围外,部分 QSPI Flash 芯片在默认情况下,处于非四线模式,原本 QSPI 的 D2 & D3 引脚会被用于写保护使能和 HOLD 使能,使能四线模式的方法,需参照使用的 QSPI Flash 芯片手册进行操作。

QSPI Flash 与微控制器连接时,为了满足阻抗匹配的要求,其信号线上需要串联22Ω或33Ω的电阻,否则,在与 QSPI Flash 通信时,会发现读到的数据与期望的数据可能不一致。

  • 生成 FlashLoader 时出现显示路径不同的warning

在配置 FlashLoader 的 MDK 工程文件时,在 Options for Target ->  C/C++(AC6)  -> warnings 选项下,若选择 All warnings 选项,在build时会出现 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"

将该配置选项修改为 AC5-like Warnings 后,样例build无问题。

Program Size: Code=4088 RO-data=4256 RW-data=4 ZI-data=0  
After Build - User command #1: cmd.exe /C copy "Objects\MM32F5270_QSPI_FlashLoader.axf" "..\MM32F5270_QSPI_FlashLoader.FLM"
已复制         1 个文件。
".\Objects\MM32F5270_QSPI_FlashLoader.axf" - 0 Error(s), 0 Warning(s).
  • 生成下载算法时出现 L6305 warning

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

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

但下载算法本身是不需要启动文件和主函数的,只在下载代码时由调试器调用下载算法中的功能,因此,需忽略该 warning。选择 Options for Target ->  Linker` -> Misc controls选项,修改该选项内容为 --diag_suppress L6305 。

  • 修改源文件后下载该文件出现无法加载闪存编程算法的问题

对比所生成的下载算法大小与工程文件的`Options for Target -> Debug -> settings -> Flash Download -> RAM for Algorithm -> Size 选项下配置的算法大小,发现当前下载算法大于 Size 所配置的大小,因此,需适当调大 Size 的大小,例如由0x1000增加到0x2000。

在实现下载算法时,为了保证下载算法足够小,其代码量不要过多,需删除未使用的函数。

  • 应用程序不能出现影响 QSPI 使用的代码

需要检查应用程序的代码,对可能影响到 QSPI 的代码进行调整,例如 GPIO 的配置,GPIO 时钟的复位,以及直接对 QSPI 的操作。

来源:灵动MM32MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 18

相关阅读:

mm32-2nd-bootloader技术白皮书(1)——配置软硬件环境

mm32-2nd-bootloader技术白皮书(2)——QSPI外设简介

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

mm32-2nd-bootloader技术白皮书(4)——设计实现简单的2nd bootloader

引言

在前文中已经实现了一个能够在 MDK 平台进行下载代码到 QSPI Flash 中的下载算法,以及一个能跳转执行应用程序的 2nd Bootloader,但若想将代码下载到 QSPI Flash上并运行,仍需对所需执行的代码文件进行少量的修改,使其能够在 QSPI Flash 上运行。

修改Linker文件

本文将 MindSDK 的 PLUS-F5270 hello_world 样例工程作为所需执行的文件,可通过 MindSDK 官网 (https://mindsdk.mindmotion.com.cn/) 获取该样例,如图 1 所示。

1.png

图1 通过官网获取 MindSDK PLUS-F5270 hello_world样例工程

打开 hello_world 样例工程,在 Options for Target 选项的Linker页面下,找到 Scatter File 选项,该选项中内容为 Linker 所使用的 Scatter File 文件路径,点击该选项右侧的 Edit ,此时样例工程中会弹出对应的 Scatter File 文件界面,如图 2 所示。

2.png

图2 打开scatter file文件界面

在弹出的 mm32f5277e_flash.scf 文件中,需要根据  QSPI 的存储器映像配置 __ROM_BASE 的数值,QSPI 外设的基础地址为 0x90000000,这个地址值来自于微控制器的用户手册的 “地址映射” 章节,如图 3 所示。因此,__ROM_BASE的数值应该为 0x90000000。

3.png

图3 QSPI 外设的存储器映像编址范围

QSPI Flash 的大小为8MB,其中 8MB = 8 * 1024 *1024 = 8388608,换算为 16 进制是 0x00800000,因此,__ROM_SIZE 的数值应该为 0x00800000。

Scatter File文件的内容修改如下:

...    
/*--------------------- Flash Configuration ----------------------------------
; <h> Flash Configuration
;   <o0> Flash Base Address <0x0-0xFFFFFFFF:8>
;   <o1> Flash Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
 *----------------------------------------------------------------------------*/
#define __ROM_BASE      0x90000000
#define __ROM_SIZE      0x00800000
...

修改源代码

在配置好 Linker 所需的 Scatter File 文件后,需要对将要执行的文件进行检查,去除可能会影响到 QSPI Flash 运行的代码,需要检查的点如下:

  • 检查时钟初始化部分:

    是否存在复位 QSPI 模块的时钟或复位 QSPI 所使用的 GPIO 引脚时钟的情况

    若存在复位 QSPI 模块,则处理器将无法从 QSPI Flash 中读取下一个要执行的指令。

    若存在复位 QSPI 使用引脚的时钟,则运行到引脚时钟复位后,GPIO 复位,引脚不再作为 QSPI 的接口,因此将会影响到 QSPI 与 QSPI Flash 之间的通信。

    在 “hello_world” 工程中,以上代码可能会出现在 board 目录下的 “clock_init.c” 中。

  • 检查引脚初始化部分:

    是否占用 QSPI 所使用的引脚

    若引脚初始化时,占用 QSPI 所需引脚,将会影响与 QSPI Flash 的通信

    在 “hello_world” 工程中,以上代码可能会出现在 board 目录下的 “pin_init.c” 中。

  • 不能出现以间接模式操作 QSPI Flash 的代码

    若执行间接模式操作 QSPI Flash 的代码,会中断直接读模式,而直接读模式是执行存储在 QSPI Flash 的应用程序的基础,因此,执行间接模式操作 QSPI Flash 的代码会使应用程序跑飞。

    在 “hello_world” 工程中,没有执行间接模式操作 QSPI Flash 的代码。

添加下载算法到可执行文件

该可执行样例最终将在 QSPI Flash 中存储并运行,因此,需要添加根据实际所使用的 QSPI Flash芯片而配置的下载算法到样例工程中,以帮助调试器将指定位置的数据写入 Flash。

以配置完成的 PLUS-F5270 hello_world 样例工程为例,打开 Options for Target 的 Debug 页面,选择 Settings 选项进入 Cortex Jlink/JTrace Target Driver Setup 页面的 Flash Download ,选择 Add 选项,进入下载算法选择列表,选择前文中设置好的 MM32F5270 QSPI FlashLoader 下载算法,点击 Add 进行添加,如图 4 所示。

4.png

图4 添加下载算法到可执行文件中

此处需注意,若下载算法的大小大于 Flash Download 中 RAM for Algorithm 的 Size 选项中的大小,会导致在下载时出现 "Cannot Load Flash Programming Algorithm" 的问题,此时需适当将 Size 的大小调大一些,本文中设置 Size 为 0x2000。

验证

配置完成在 QSPI Flash 上运行的可执行文件后,可尝试使用下载算法将整个工程下载到 QSPI Flash 中并使用 2nd Bootloader 执行这个工程。

先将前文中实现运行在片内 Flash 的 2nd Bootloader 下载到 PLUS-F5270 开发板中,再将已经配置好的添加了下载算法的 hello_world 样例工程下载到开发板中的 QSPI Flash 中。下载成功后复位微控制器,通过串口调试器可看到输出字符 "hello_world"。

5.png

图5 使用QSPI Flash的hello_world样例工程运行结果

在调试模式下,可以从 Disassemby 窗口观察到进入main函数后地址处于 0x90000950,属于 QSPI Flash 的范围内,由此可见,整个 hello_world 下载到 QSPI Flash中并且正确运行。

6.png

图6 通过调试查看样例起始地址

至此,已经验证了下载算法可用, 2nd Bootloader 可用,修改后的样例工程也可用。

但仍需验证下中断是否可用,本文修改 hello_world 工程,验证 SysTick_Handler() 能否正确执行。

volatile uint32_t systime = 0u;

int main(void)
{
    uint8_t ch;

    BOARD_Init();

    printf("hello, world\r\n");

    SysTick_Config(CLOCK_SYSTICK_FREQ / 1000u);

    while (1)
    {
        if (systime > 1000)
        {
            systime = 0;
            putchar('*');
        }
    }
}

void SysTick_Handler()
{
    systime++;
}

将修改后的样例工程下载并运行在 QSPI Flash 上,其结果如图 7 所示。

7.png

图7 验证SysTick中断可用的样例结果

至此,也验证了中断也可正确执行。

总结

为了编译可在QSPI Flash上运行的可执行文件,需要作出以下两件事:

修改 Linker 文件,将 ROM 的位置和大小修改为 QSPI Flash 的映射地址和大小

修改源代码中所有可能会影响访问QSPI Flash的代码,例如 GPIO 的配置,时钟的配置等

可以发现编译可在 QSPI Flash上运行的可执行文件并不难,因此可以轻松将应用程序迁移到 QSPI Flash 里。

来源:灵动MM32MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 26

相关阅读:

mm32-2nd-bootloader技术白皮书(1)——配置软硬件环境

mm32-2nd-bootloader技术白皮书(2)——QSPI外设简介

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

什么是 2nd Bootloader?

引导加载程序(Bootloader)是为了能够正确执行应用程序,在执行应用程序之前对系统进行的一系列初始化操作,并且在完成一系列初始化后,能够引导 CPU 去执行应用程序的程序。

为什么 Bootloader 要在前面加上一个 2nd?因为在执行2nd Bootloader 之前,MCU 中就已经在执行了一个 Bootloader,可以称为 1st Bootloader,这个 Bootloader 的目的是等待片内时钟稳定,确定 MCU 的 BOOT 引脚电平,来了解自己应该执行哪块区域的代码——是用户代码,还是 ISP 代码,如图 1 所示。

1.png

图1 1st Bootloader

1st Bootloader 已经在 MCU 出厂的时候,固化在芯片内部,用户无法改动这块区域的内容。为了满足客制化需求,可以在在 1st Bootloader 的基础上,再进入一次 Bootloader,做一些客制化的事情,这就是 2nd Bootloader,如图2所示。

2.png

图2 简单的 2nd Bootloader

甚至,可以通过 2nd Bootloader,在 MCU 中存储两套应用程序,选择其一执行,就像电脑的双系统那样,也可以实现定制化的 ISP 功能,OTA 功能,如图3所示:

3.png

图3多功能的 2nd Bootloader

2nd Bootloader 的基本需求

当程序下载到 QSPI Flash 之后,为了能够执行 QSPI Flash 中的程序,就需要在 MCU 复位之后,把 QSPI 外设和 GPIO 引脚配置好,QSPI 外设和 GPIO 引脚看上去像是 BSP 相关的事情(确实是与 BSP 相关),能否放在应用程序的 “board_init()” 中呢?当然不行,把 QSPI 外设和 GPIO 引脚配置好这些事情要在进入应用程序之前完成,这样才能正常执行应用程序。

因此, 2nd Bootloader 第一个基本需求就是:初始化 QSPI 外设和使用到的 GPIO 引脚。

初始化好 QSPI 外设和 GPIO 引脚后,第二个问题随之而来,怎么跳转到应用程序中呢?

第二个基本需求就出来了:跳转并执行应用程序。

快速体验

MindSDK 提供了简单的 2nd Bootloader 的样例,方便用户开发:

在 demo_apps/basic 中选择 bootloader_qspi 工程,如图 4 所示。

4.png

图4 选择 demo_apps/basic/bootloader_qspi 工程

编译并下载到 PLUS-F5270 中即可使用,十分方便。

但本文将继续讲解 2nd Bootloader 的实现方式,以了解 2nd Bootloader 的工作原理,方便继续开发 2nd Bootloader。

软件实现

初始化 QSPI 外设和 GPIO 引脚

PLUS-F5270 使用到的 QSPI Flash 引脚如表 1 所示。

5.png

表1 PLUS-F5270使用的QSPI引脚

初始化系统时钟, QSPI & GPIO 外设时钟: 

void BOARD_InitBootClocks(void)
{
    CLOCK_ResetToDefault();

    /* QSPI. */
    RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_QSPI, true);
    RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_QSPI);

    /* GPIOA. */
    RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOA, true);
    RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOA);

    /* GPIOB. */
    RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOB, true);
    RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOB);

    /* GPIOG. */
    RCC_EnableAHB1Periphs(RCC_AHB1_PERIPH_GPIOG, true);
    RCC_ResetAHB1Periphs(RCC_AHB1_PERIPH_GPIOG);
}

初始化 GPIO 引脚:

void BOARD_InitPins(void)
{
    GPIO_Init_Type gpio_init;

    /* PB10 - QSPI_CS. */
    gpio_init.Pins  = GPIO_PIN_10;
    gpio_init.PinMode  = GPIO_PinMode_AF_PushPull;
    gpio_init.Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &gpio_init);
    GPIO_PinAFConf(GPIOB, gpio_init.Pins, GPIO_AF_10);

    /* PG7 - QSPI_SCK. */
    gpio_init.Pins  = GPIO_PIN_7;
    gpio_init.PinMode  = GPIO_PinMode_AF_PushPull;
    gpio_init.Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOG, &gpio_init);
    GPIO_PinAFConf(GPIOG, gpio_init.Pins, GPIO_AF_10);

    /* PG6 - QSPI_D0. */
    gpio_init.Pins  = GPIO_PIN_6;
    gpio_init.PinMode  = GPIO_PinMode_AF_PushPull;
    gpio_init.Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOG, &gpio_init);
    GPIO_PinAFConf(GPIOG, gpio_init.Pins, GPIO_AF_10);

    /* PA3 - QSPI_D1. */
    gpio_init.Pins  = GPIO_PIN_3;
    gpio_init.PinMode  = GPIO_PinMode_AF_PushPull;
    gpio_init.Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_init);
    GPIO_PinAFConf(GPIOA, gpio_init.Pins, GPIO_AF_10);

    /* PB3 - QSPI_D2. */
    gpio_init.Pins  = GPIO_PIN_3;
    gpio_init.PinMode  = GPIO_PinMode_AF_PushPull;
    gpio_init.Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &gpio_init);
    GPIO_PinAFConf(GPIOB, gpio_init.Pins, GPIO_AF_10);

    /* PG8 - QSPI_D3. */
    gpio_init.Pins  = GPIO_PIN_8;
    gpio_init.PinMode  = GPIO_PinMode_AF_PushPull;
    gpio_init.Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOG, &gpio_init);
    GPIO_PinAFConf(GPIOG, gpio_init.Pins, GPIO_AF_10);
}

初始化 QSPI 直接读模式:这里我们采用四线快读的方式访问 QSPI Flash。

void BOARD_InitExternalFlash(void)
{
    /* init qspi hardware. */
    QSPI_Init_Type qspi_init;
    qspi_init.SckDiv            = BOARD_QSPI_SCK_DIV;
    qspi_init.CsHighLevelCycles = BOARD_QSPI_CS_HIGH_LEVEL_TIME;
    qspi_init.RxSampleDelay     = BOARD_QSPI_RX_DELAY_CYCLES;
    qspi_init.SpiMode           = QSPI_SpiMode_3; /* suggest using SPI Mode 3. */
    /* init qspi. */
    QSPI_Init(QSPI, &qspi_init);

    /* enable qspi direct read mode. */
    QSPI_DirectXferConf_Type direct_conf;
    direct_conf.CmdBusWidth   = BOARD_EXT_FLASH_CMD_BUS_WIDTH;
    direct_conf.CmdValue      = BOARD_EXT_FLASH_CMD_VALUE;
    direct_conf.AddrBusWidth  = BOARD_EXT_FLASH_ADDR_BUS_WIDTH;
    direct_conf.AddrWordWidth = BOARD_EXT_FLASH_ADDR_WORD_WIDTH;
    direct_conf.AltBusWidth   = QSPI_BusWidth_None;
    direct_conf.DummyCycles   = BOARD_EXT_FLASH_DUMMY_CYCLES;
    direct_conf.DataBusWidth  = BOARD_EXT_FLASH_DATA_BUS_WIDTH;

    QSPI_EnableDirectRead(QSPI, &direct_conf);
}

代码中涉及到的宏定义:

/* APP BASE. */
#define BOARD_APP_BASE                  0x90000000

/* QSPI. */
#define BOARD_QSPI_SCK_DIV              4u
#define BOARD_QSPI_RX_DELAY_CYCLES      1u
#define BOARD_QSPI_CS_HIGH_LEVEL_TIME   2u

/* cmd. */
#define BOARD_EXT_FLASH_CMD_BUS_WIDTH   QSPI_BusWidth_1b
#define BOARD_EXT_FLASH_CMD_VALUE       0x6B /* fast read quad. */
#define BOARD_EXT_FLASH_ADDR_BUS_WIDTH  QSPI_BusWidth_1b
#define BOARD_EXT_FLASH_ADDR_WORD_WIDTH QSPI_WordWidth_24b
#define BOARD_EXT_FLASH_DUMMY_CYCLES    8u
#define BOARD_EXT_FLASH_DATA_BUS_WIDTH  QSPI_BusWidth_4b

QSPI Flash 的地址映射空间为 0x90000000 ~ 0x9FFFFFFF,共 256MB 大小,这段信息来自微控制器的 UM 地址映射章节,如图 5 所示。

6.png

图5 QSPI Flash 在 MCU中的地址映射

跳转执行应用程序

在跳转执行应用程序之前,先看下应用程序的二进制代码长什么样。

以 MindSDK 的 hello_world 工程为例,生成一份程序的二进制文件,打开 MDK 工程,点击魔术棒(Options for Target...),点击 User 列表,如图x所示,在指定位置(红框中的 User Command)加入下面这句话,并在前面打上对勾:

fromelf.exe --bin -o "@L.bin" "#L"

然后编译工程,就能在工程文件所在的目录下找到生成的 bin 文件。

7.png

图6 生成二进制文件

可以使用如 VS Code 等软件查看这个二进制文件的内容,如图5所示:

8.png

图7 应用程序的二进制片段

看下应用程序的开头内容:开头是 “00 C0 01 30” 的数据,它是 0x3001C000 的小端模式表示,而 0x3001C000 是应用程序的 SP (堆栈指针)初始化值。后面紧接着的的 “09 17 00 08” 则代表 0x08001709,Reset_Handler() 的指针。前面一段内容,就是中断向量表,通过中断向量表可以知道这段应用程序的 SP 起始地址,Reset_Handler() 函数的指针,而这两个数据,再加上存放这些数据的起始地址,是能够正确执行应用程序的关键。

获取新的 SP 指针和 Reset_Handler() 函数指针:uint32_t * vtor;

void jump_to_app(uint32_t app_base)
{
    vtor = (uint32_t *)app_base;
    ......
}

vtor[0] 就是 SP 指针,vtor[1] 就是 Reset_Handler() 函数指针。

可以发现,vtor 变量是以全局变量的方式进行存储的,而并不是在函数中定义变量(局部变量)保存,其原因是修改 MSP 和 PSP 指针后,局部变量会发生意外的变化,导致后面无法正确跳转程序。

修改中断向量表:

void jump_to_app(uint32_t app_base)
{
    ......
    __disable_irq();
    SCB->VTOR = app_base;

    __ISB();
    __DSB();
    ......
}

修改中断向量表是为了保证应用程序的中断程序能够正确执行,可以做一个实验,将修改中断向量表的代码注释掉,编译下载这个 2nd Bootloader,发现应用程序开始是能正常执行的,但是呢,一旦触发了中断,程序就不知道跑到一个莫名的位置上了,这个莫名的位置就是 2nd Bootloader 的中断程序。

为了保证修改中断向量表,以及后面修改 MSP 和 PSP 指针时不会有中断干扰,需手动关闭所有中断;使用 __disable_irq() 关闭所有中断。

Cortex-M0 的中断向量表修改方法和 Cortex-M3,M4,Star-MC1 等内核不同,它没有 VTOR 寄存器,而是将应用程序的中断向量表复制到 SRAM 中,通过修改某一个寄存器的字段值(非 Cortex-M0 内核寄存器,需参考具体芯片的手册进行修改),将绝对地址 0x00000000 起始的映射区间(不同 MCU 的映射区间大小可能不同)由片内 Flash 起始地址变为 SRAM 的起始地址,而绝对地址 0x00000000 就是 Cortex-M0 内核芯片的中断向量表起始地址。

修改 MSP & PSP:void jump_to_app(uint32_t app_base)
{
    ......
    __set_MSP(vtor[0]);
    __set_PSP(vtor[0]);
    ......
}

当这两个指针改变后,再读取函数中的局部变量,会发现其值发生了变化,这是由于局部变量是通过 SP 指针加相对地址进行访问的,当SP 指针发生变化后,局部变量的实际存储位置也会发生变化,修改后,再访问局部变量的时候,由于还是通过堆栈指针加相对地址进行访问,则局部变量的绝对地址发生了变化,访问到的值就不再是实际值。

跳转执行应用程序:

void jump_to_app(uint32_t app_base)
{
    ......
    __enable_irq();
    ((void(*)(void))vtor[1])();
    ......
}

在跳转执行应用程序前,我们需要将前面已经关闭的中断再次打开,使其在跳转应用程序后,中断函数能够正确执行;使用 __enable_irq() 打开所有中断。

跳转执行应用程序的方法就是执行应用程序的 Reset_Handler() ,Reset_handler() 的指针就存储在中断向量表的第二个四字节地址中,因此我们读取vtor[1]的值,并将其强制转换成指向函数的指针,并执行这个函数即可实现应用程序的跳转。

跳转程序完整代码:

uint32_t * vtor;
void jump_to_app(uint32_t app_base)
{
    vtor = (uint32_t *)app_base;

    __disable_irq();
    SCB->VTOR = app_base;

    __ISB();
    __DSB();

    __set_MSP(vtor[0]);
    __set_PSP(vtor[0]);

    __enable_irq();
    ((void(*)(void))vtor[1])();
}

初始化板子相关的事情后,就可以使用这个函数进行跳转程序:

int main(void)
{
    BOARD_Init();

    /* jump to app. */
    jump_to_app(BOARD_APP_BASE);

    while (1)
    {
    }
}

至此,简单的 2nd Bootloader 就已经完成了,现在,有一个能下载程序到 QSPI Flash 的下载算法,有一个能跳转执行应用程序的 2nd Bootloader,至于验证,还需要等下一章编译出一个存储在 QSPI Flash 上的应用程序后,才能验证。

结语

2nd Bootloader 本质上其实就是一个应用程序,是一段由 1st Bootloader 引导执行的应用程序。它的好处是可以根据客户需求进行定制,自由修改。

本文只是先实现一个简单的 2nd Bootloader,其作用只是帮助 MCU 去执行存储在 QSPI Flash 中的应用程序。

本文使用的访问 QSPI Flash 方法为四线快读模式,其实某些 QSPI Flash 还支持 QPI 模式,发送 0x38(不同 QSPI Flash 的进入方法可能不同) 指令后,即可进入,发送 0xFF 后即可退出;进入 QPI 模式后,访问 QSPI Flash 的所有阶段均为 4 线模式,加快了访问 QSPI Flash 的速度,这一步操作我们也可以在 2nd Bootloader 中实现。不过这会出现一个问题:需要在下载算法中判断 QSPI Flash 是否进入了 QPI 模式,该用何种方式下载程序,这会增加下载算法的代码量。

来源:灵动MM32MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 47

相关阅读:

mm32-2nd-bootloader技术白皮书(1)——配置软硬件环境

mm32-2nd-bootloader技术白皮书(2)——QSPI外设简介

简介

前文中了解了微控制器可以使用 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 所示。

1.png

图1 下载算法模板的目录与包含文件

复制整个 "_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 所示。

2.png

图2 MDK的工程选项配置界面

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

3.png

图3 Keil-MDK的输出下载算法路径配置界面

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 所示。

4.png

图4 Keil-MDK的工程算法编译方式选项配置界面

此处需注意要将 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 所示。

5.png

图5 配置 MDK 工程禁用错误 L6305

修改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 所示。

6.png

图6 MDK 的下载算法添加列表

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

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

7.png

图7 在工程中添加下载算法所需文件

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

8.png

图8 在工程中添加头文件地址

为什么没有把所有的驱动文件都包含进去呢?这是由于 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 中。

来源:灵动MM32MCU

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 77
订阅 RSS - mm32-2nd-bootloader