【STM32H750】从零编写MDK的FLM烧录算法

2023-05-16

文章目录

  • 前言
  • 一、将代码中的图片资源下载到外部flash
    • 1. 修改分散加载文件
  • 二、MDK下载算法原理
    • 1. 程序能够通过下载算法下载到芯片的原理
    • 2. 算法程序中擦除操作执行流程
    • 3. 制作FLM文件步骤
  • 三、使用STM32CubeMX新建工程
    • 1. 新建工程
      • 选择MCU型号(STM32H750XBH6)
      • 配置SPI
      • 配置UART
      • 配置时钟树
      • 设置调试接口
      • 设置工程并生成工程
    • 2. 移植SFUD串行 Flash 通用驱动库
      • SFUD 是什么?
      • 移植SFUD
      • 测试SFUD
    • 3. 生成FLM文件
      • 重新生成不带main函数的工程
      • 添加修改编程算法文件FlashPrg.c
      • 添加修改配置文件FlashDev.c
      • 地址无关代码实现
      • 修改分散加载文件
      • 将程序可执行文件axf修改为flm格式

前言

当我们要下载编译好的镜像到Flash时,首先要做的一步就是选择合适的Flash下载算法,而这个算法本身就是一个FLM文件:
在这里插入图片描述
代码既可以下载到内部flash,也可以下载到外部flash,或者一部分下载到内部,一部分下载到外部。

一、将代码中的图片资源下载到外部flash

在UI设计中往往需要大量的图片和字体,图片和字体资源在代码中以静态数组的形式存在,这些大数组在内部flash中一般存放不下,所以需要把这些占用资源比较大的数组放在外部flash中,然后通过QSPI地址映射的方式访问,或者通过SPI将flash中的资源分批读取到RAM缓存中使用。

1. 修改分散加载文件

  1. 通过MDK打开分散加载文件,配置“ExtFlashSection”段:
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x08000000 0x00020000  {    ; load region size_region
  ER_IROM1 0x08000000 0x00020000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
   .ANY (+XO)
  }
  RW_IRAM1 0x20000000 0x00020000  {  ; RW data
   .ANY (+RW +ZI)
  }
  RW_IRAM2 0x24000000 0x00080000  {
   .ANY (+RW +ZI)
  }
}

LR_EROM1 0x90000000 0x01000000  {    ; load region size_region
    ER_EROM1 0x90000000 0x01000000  {  ; load address = execution address
		*.o (ExtFlashSection)
		*.o (FontFlashSection)
		*.o (TextFlashSection)
	}
}

添加LR_EROM1 段,起始地址为0x90000000 ,大小为0x01000000 。

  1. 在代码中将图片资源分配到ExtFlashSection段
#define LOCATION_ATTRIBUTE(name) __attribute__((section(name))) __attribute__((aligned(4)))

KEEP extern const unsigned char image_watch_seconds[] LOCATION_ATTRIBUTE("ExtFlashSection") = // 4x202 ARGB8888 pixels.
{
    0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00,
    0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0xff,
    0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00,
    0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0x00,
    0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff,
    0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0xff,
    0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00,
    0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0x00,
    0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff,
    0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0x00,
    0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff,
    0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0xff,
    0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0x00,
    0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00,
    0xf8, 0xfc, 0xf8, 0x00
};

  1. 编译代码
    在这里插入图片描述
    查看map文件,image_watch_seconds这个数组已经被分配到了0X90138690这个地址了,这个地址正是LR_EROM1 所在的区间。

二、MDK下载算法原理

1. 程序能够通过下载算法下载到芯片的原理

通过MDK创建一批与地址信息无关的函数,实现的功能主要有初始化,擦除,编程,读取,校验等,然后MDK调试下载阶段,会将算法文件加载到芯片的内部RAM里面(加载地址可以通过MDK设置),然后MDK通过与这个算法文件的交互,实现程序下载,调试阶段数据读取等操作。

2. 算法程序中擦除操作执行流程

算法程序中擦除操作执行流程

  • 加载算法到芯片RAM。
  • 执行初始化函数Init。
  • 执行擦除操作,根据用户的MDK配置,这里可以选择整个芯片擦除或者扇区擦除。
  • 执行Uinit函数。
  • 操作完毕。

3. 制作FLM文件步骤

  1. 将ARM:CMSIS Pack文件夹(通常是C:\Keil\ARM\Pack\ARM\CMSIS\ version \Device_Template_Flash)中的工程复制到一个新文件夹中,取消文件夹的只读属性,重命名项目文件NewDevice.uvprojx以表示新的flash 设备名称,例如MyDevice.uvprojx。
  2. 打开工程,从工具栏中,使用下拉选择目标来选择处理器架构。
  3. 打开对话框Project - Options for Target - Output并更改Name of Executable字段的内容以表示设备,例如MyDevice。
  4. 调整文件FlashPrg中的编程算法。
  5. 调整文件FlashDev中的设备参数。
  6. 使用Project - Build Target生成新的 Flash 编程算法。

以上步骤是利用官方的工程模板修改代码,这种方式网上已有很多教程(推荐使用这种方法),不再重复介绍,接下来介绍一种不使用模板工程制作的方法,目的是为了了解其实现原理。

三、使用STM32CubeMX新建工程

1. 新建工程

硬件平台: RT-Thread官方ART-PI H750开发版
软件: STM32CubeMX,MDK

选择MCU型号(STM32H750XBH6)

在这里插入图片描述

配置SPI

在这里插入图片描述

配置UART

在这里插入图片描述

配置时钟树

在这里插入图片描述

设置调试接口

在这里插入图片描述

设置工程并生成工程

在这里插入图片描述

2. 移植SFUD串行 Flash 通用驱动库

SFUD 是什么?

SFUD 是一款开源的串行 SPI Flash 通用驱动库。由于现有市面的串行 Flash 种类居多,各个 Flash 的规格及命令存在差异, SFUD 就是为了解决这些 Flash 的差异现状而设计,让我们的产品能够支持不同品牌及规格的 Flash,提高了涉及到 Flash 功能的软件的可重用性及可扩展性,同时也可以规避 Flash 缺货或停产给产品所带来的风险。

  • 主要特点:支持 SPI/QSPI 接口、面向对象(同时支持多个 Flash 对象)、可灵活裁剪、扩展性强、支持 4 字节地址
  • 资源占用
    • 标准占用:RAM:0.2KB ROM:5.5KB
    • 最小占用:RAM:0.1KB ROM:3.6KB
  • 设计思路:
    • 什么是 SFDP :它是 JEDEC (固态技术协会)制定的串行 Flash 功能的参数表标准,最新版 V1.6B (点击这里查看)。该标准规定了,每个 Flash 中会存在一个参数表,该表中会存放 Flash 容量、写粒度、擦除命令、地址模式等 Flash 规格参数。目前,除了部分厂家旧款 Flash 型号会不支持该标准,其他绝大多数新出厂的 Flash 均已支持 SFDP 标准。所以该库在初始化时会优先读取 SFDP 表参数。
    • 不支持 SFDP 怎么办 :如果该 Flash 不支持 SFDP 标准,SFUD 会查询配置文件 ( /sfud/inc/sfud_flash_def.h ) 中提供的 Flash 参数信息表 中是否支持该款 Flash。如果不支持,则可以在配置文件中添加该款 Flash 的参数信息。获取到了 Flash 的规格参数后,就可以实现对 Flash 的全部操作。

移植SFUD

将下载到sfud源代码放置在工程目录中
在这里插入图片描述
将sfud添加到工程目录:
在这里插入图片描述
添加串口打印文件:
为了测试方便,添加一个串口打印的文件my_printf.c



#include <stdarg.h>
#include <stdint.h>
#include "usart.h"
#define RT_PRINTF_PRECISION
#define RT_CONSOLEBUF_SIZE 512
/* private function */
#define _ISDIGIT(c)  ((unsigned)((c) - '0') < 10)

#define ZEROPAD     (1 << 0)    /* pad with zero */
#define SIGN        (1 << 1)    /* unsigned/signed long */
#define PLUS        (1 << 2)    /* show plus */
#define SPACE       (1 << 3)    /* space if plus */
#define LEFT        (1 << 4)    /* left justified */
#define SPECIAL     (1 << 5)    /* 0x */
#define LARGE       (1 << 6)    /* use 'ABCDEF' instead of 'abcdef' */

/**
 * This function will duplicate a string.
 *
 * @param  n is the string to be duplicated.
 *
 * @param  base is support divide instructions value.
 *
 * @return the duplicated string pointer.
 */
#ifdef RT_PRINTF_LONGLONG
    inline int divide(long long *n, int base)
#else
    inline int divide(long *n, int base)
#endif /* RT_PRINTF_LONGLONG */
{
    int res;

    /* optimized for processor which does not support divide instructions. */
    if (base == 10) {
        #ifdef RT_PRINTF_LONGLONG
        res = (int)(((unsigned long long) * n) % 10U);
        *n = (long long)(((unsigned long long) * n) / 10U);
        #else
        res = (int)(((unsigned long) * n) % 10U);
        *n = (long)(((unsigned long) * n) / 10U);
        #endif
    } else {
        #ifdef RT_PRINTF_LONGLONG
        res = (int)(((unsigned long long) * n) % 16U);
        *n = (long long)(((unsigned long long) * n) / 16U);
        #else
        res = (int)(((unsigned long) * n) % 16U);
        *n = (long)(((unsigned long) * n) / 16U);
        #endif
    }

    return res;
}

static char *print_number(char *buf,
                          char *end,
#ifdef RT_PRINTF_LONGLONG
    long long  num,
#else
    long  num,
#endif /* RT_PRINTF_LONGLONG */
                          int   base,
                          int   s,
#ifdef RT_PRINTF_PRECISION
    int   precision,
#endif /* RT_PRINTF_PRECISION */
                          int   type)
{
    char c, sign;
    #ifdef RT_PRINTF_LONGLONG
    char tmp[32];
    #else
    char tmp[16];
    #endif /* RT_PRINTF_LONGLONG */
    int precision_bak = precision;
    const char *digits;
    static const char small_digits[] = "0123456789abcdef";
    static const char large_digits[] = "0123456789ABCDEF";
    register int i;
    register int size;

    size = s;

    digits = (type & LARGE) ? large_digits : small_digits;

    if (type & LEFT)
        type &= ~ZEROPAD;

    c = (type & ZEROPAD) ? '0' : ' ';

    /* get sign */
    sign = 0;

    if (type & SIGN) {
        if (num < 0) {
            sign = '-';
            num = -num;
        } else if (type & PLUS)
            sign = '+';
        else if (type & SPACE)
            sign = ' ';
    }

    #ifdef RT_PRINTF_SPECIAL

    if (type & SPECIAL) {
        if (base == 16)
            size -= 2;
        else if (base == 8)
            size--;
    }

    #endif /* RT_PRINTF_SPECIAL */

    i = 0;

    if (num == 0)
        tmp[i++] = '0';
    else {
        while (num != 0)
            tmp[i++] = digits[divide(&num, base)];
    }

    #ifdef RT_PRINTF_PRECISION

    if (i > precision)
        precision = i;

    size -= precision;
    #else
    size -= i;
    #endif /* RT_PRINTF_PRECISION */

    if (!(type & (ZEROPAD | LEFT))) {
        if ((sign) && (size > 0))
            size--;

        while (size-- > 0) {
            if (buf < end)
                *buf = ' ';

            ++ buf;
        }
    }

    if (sign) {
        if (buf < end) {
            *buf = sign;
        }

        -- size;
        ++ buf;
    }

    #ifdef RT_PRINTF_SPECIAL

    if (type & SPECIAL) {
        if (base == 8) {
            if (buf < end)
                *buf = '0';

            ++ buf;
        } else if (base == 16) {
            if (buf < end)
                *buf = '0';

            ++ buf;

            if (buf < end) {
                *buf = type & LARGE ? 'X' : 'x';
            }

            ++ buf;
        }
    }

    #endif /* RT_PRINTF_SPECIAL */

    /* no align to the left */
    if (!(type & LEFT)) {
        while (size-- > 0) {
            if (buf < end)
                *buf = c;

            ++ buf;
        }
    }

    #ifdef RT_PRINTF_PRECISION

    while (i < precision--) {
        if (buf < end)
            *buf = '0';

        ++ buf;
    }

    #endif /* RT_PRINTF_PRECISION */

    /* put number in the temporary buffer */
    while (i-- > 0 && (precision_bak != 0)) {
        if (buf < end)
            *buf = tmp[i];

        ++ buf;
    }

    while (size-- > 0) {
        if (buf < end)
            *buf = ' ';

        ++ buf;
    }

    return buf;
}

static int skip_atoi(const char **s)
{
    register int i = 0;

    while (_ISDIGIT(**s))
        i = i * 10 + *((*s)++) - '0';

    return i;
}
/**
 * This function will fill a formatted string to buffer.
 *
 * @param  buf is the buffer to save formatted string.
 *
 * @param  size is the size of buffer.
 *
 * @param  fmt is the format parameters.
 *
 * @param  args is a list of variable parameters.
 *
 * @return The number of characters actually written to buffer.
 */
int rt_vsnprintf(char *buf, int size, const char *fmt, va_list args)
{
    #ifdef RT_PRINTF_LONGLONG
    unsigned long long num;
    #else
    uint32_t num;
    #endif /* RT_PRINTF_LONGLONG */
    int i, len;
    char *str, *end, c;
    const char *s;

    uint8_t base;            /* the base of number */
    uint8_t flags;           /* flags to print number */
    uint8_t qualifier;       /* 'h', 'l', or 'L' for integer fields */
    int32_t field_width;     /* width of output field */

    #ifdef RT_PRINTF_PRECISION
    int precision;      /* min. # of digits for integers and max for a string */
    #endif /* RT_PRINTF_PRECISION */

    str = buf;
    end = buf + size;

    /* Make sure end is always >= buf */
    if (end < buf) {
        end  = ((char *) - 1);
        size = end - buf;
    }

    for (; *fmt ; ++fmt) {
        if (*fmt != '%') {
            if (str < end)
                *str = *fmt;

            ++ str;
            continue;
        }

        /* process flags */
        flags = 0;

        while (1) {
            /* skips the first '%' also */
            ++ fmt;

            if (*fmt == '-') flags |= LEFT;
            else if (*fmt == '+') flags |= PLUS;
            else if (*fmt == ' ') flags |= SPACE;
            else if (*fmt == '#') flags |= SPECIAL;
            else if (*fmt == '0') flags |= ZEROPAD;
            else break;
        }

        /* get field width */
        field_width = -1;

        if (_ISDIGIT(*fmt)) field_width = skip_atoi(&fmt);
        else if (*fmt == '*') {
            ++ fmt;
            /* it's the next argument */
            field_width = va_arg(args, int);

            if (field_width < 0) {
                field_width = -field_width;
                flags |= LEFT;
            }
        }

        #ifdef RT_PRINTF_PRECISION
        /* get the precision */
        precision = -1;

        if (*fmt == '.') {
            ++ fmt;

            if (_ISDIGIT(*fmt)) precision = skip_atoi(&fmt);
            else if (*fmt == '*') {
                ++ fmt;
                /* it's the next argument */
                precision = va_arg(args, int);
            }

            if (precision < 0) precision = 0;
        }

        #endif /* RT_PRINTF_PRECISION */
        /* get the conversion qualifier */
        qualifier = 0;
        #ifdef RT_PRINTF_LONGLONG

        if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L')
        #else
        if (*fmt == 'h' || *fmt == 'l')
        #endif /* RT_PRINTF_LONGLONG */
        {
            qualifier = *fmt;
            ++ fmt;
            #ifdef RT_PRINTF_LONGLONG

            if (qualifier == 'l' && *fmt == 'l') {
                qualifier = 'L';
                ++ fmt;
            }

            #endif /* RT_PRINTF_LONGLONG */
        }

        /* the default base */
        base = 10;

        switch (*fmt) {
            case 'c':
                if (!(flags & LEFT)) {
                    while (--field_width > 0) {
                        if (str < end) *str = ' ';

                        ++ str;
                    }
                }

                /* get character */
                c = (uint8_t)va_arg(args, int);

                if (str < end) *str = c;

                ++ str;

                /* put width */
                while (--field_width > 0) {
                    if (str < end) *str = ' ';

                    ++ str;
                }

                continue;

            case 's':
                s = va_arg(args, char *);

                if (!s) s = "(NULL)";

                for (len = 0; (len != field_width) && (s[len] != '\0'); len++);

                #ifdef RT_PRINTF_PRECISION

                if (precision > 0 && len > precision) len = precision;

                #endif /* RT_PRINTF_PRECISION */

                if (!(flags & LEFT)) {
                    while (len < field_width--) {
                        if (str < end) *str = ' ';

                        ++ str;
                    }
                }

                for (i = 0; i < len; ++i) {
                    if (str < end) *str = *s;

                    ++ str;
                    ++ s;
                }

                while (len < field_width--) {
                    if (str < end) *str = ' ';

                    ++ str;
                }

                continue;

            case 'p':
                if (field_width == -1) {
                    field_width = sizeof(void *) << 1;
                    flags |= ZEROPAD;
                }

                #ifdef RT_PRINTF_PRECISION
                str = print_number(str, end,
                                   (long)va_arg(args, void *),
                                   16, field_width, precision, flags);
                #else
                str = print_number(str, end,
                                   (long)va_arg(args, void *),
                                   16, field_width, flags);
                #endif /* RT_PRINTF_PRECISION */
                continue;

            case '%':
                if (str < end) *str = '%';

                ++ str;
                continue;

            /* integer number formats - set up the flags and "break" */
            case 'o':
                base = 8;
                break;

            case 'X':
                flags |= LARGE;

            case 'x':
                base = 16;
                break;

            case 'd':
            case 'i':
                flags |= SIGN;

            case 'u':
                break;

            default:
                if (str < end) *str = '%';

                ++ str;

                if (*fmt) {
                    if (str < end) *str = *fmt;

                    ++ str;
                } else {
                    -- fmt;
                }

                continue;
        }

        #ifdef RT_PRINTF_LONGLONG

        if (qualifier == 'L') num = va_arg(args, long long);
        else if (qualifier == 'l')
        #else
        if (qualifier == 'l')
        #endif /* RT_PRINTF_LONGLONG */
        {
            num = va_arg(args, uint32_t);

            if (flags & SIGN) num = (int32_t)num;
        } else if (qualifier == 'h') {
            num = (uint16_t)va_arg(args, int32_t);

            if (flags & SIGN) num = (int16_t)num;
        } else {
            num = va_arg(args, uint32_t);

            if (flags & SIGN) num = (int32_t)num;
        }

        #ifdef RT_PRINTF_PRECISION
        str = print_number(str, end, num, base, field_width, precision, flags);
        #else
        str = print_number(str, end, num, base, field_width, flags);
        #endif /* RT_PRINTF_PRECISION */
    }

    if (size > 0) {
        if (str < end) *str = '\0';
        else {
            end[-1] = '\0';
        }
    }

    /* the trailing null byte doesn't count towards the total
    * ++str;
    */
    return str - buf;
}

/**
 * This function will print a formatted string on system console.
 *
 * @param fmt is the format parameters.
 *
 * @return The number of characters actually written to buffer.
 */
int rt_kprintf(const char *fmt, ...)
{
    va_list args;
    int length;
    static char rt_log_buf[RT_CONSOLEBUF_SIZE];

    va_start(args, fmt);
    /* the return value of vsnprintf is the number of bytes that would be
     * written to buffer had if the size of the buffer been sufficiently
     * large excluding the terminating null byte. If the output string
     * would be larger than the rt_log_buf, we have to adjust the output
     * length. */
    length = rt_vsnprintf(rt_log_buf, sizeof(rt_log_buf) - 1, fmt, args);

    if (length > RT_CONSOLEBUF_SIZE - 1)
        length = RT_CONSOLEBUF_SIZE - 1;

    HAL_UART_Transmit(&huart4, (uint8_t *)&rt_log_buf, length, 100);

    va_end(args);

    return length;
}

修改sfud_port.c文件:

#include <string.h>
#include <sfud.h>
#include <stdarg.h>
#include "gpio.h"
#include "spi.h"

typedef struct {
    SPI_HandleTypeDef *spix;
    GPIO_TypeDef *cs_gpiox;
    uint16_t cs_gpio_pin;
} spi_user_data, *spi_user_data_t;

static spi_user_data spi1;
static char log_buf[256];
void sfud_log_debug(const char *file, const long line, const char *format, ...);
extern int rt_vsnprintf(char *buf, int size, const char *fmt, va_list args);
extern int rt_kprintf(const char *fmt, ...);
static void spi_lock(const sfud_spi *spi)
{

}

static void spi_unlock(const sfud_spi *spi)
{
 
}
/* about 100 microsecond delay */
static void delay_100us(void) {
    uint32_t delay = 2000;
    while(delay--);
}
/**
 * SPI write data then read data
 */
static sfud_err spi_write_read(const sfud_spi *spi, const uint8_t *write_buf, size_t write_size, uint8_t *read_buf,
                               size_t read_size)
{
    sfud_err result = SFUD_SUCCESS;
    /**
     * add your spi write and read code
     */
    spi_user_data_t spi_dev = (spi_user_data_t) spi->user_data;

    HAL_GPIO_WritePin(spi_dev->cs_gpiox, spi_dev->cs_gpio_pin,GPIO_PIN_RESET);
    if (write_size) {
        HAL_SPI_Transmit(spi_dev->spix, (uint8_t *)write_buf,write_size,1);
    }
    if (read_size) {
        HAL_SPI_Receive(spi_dev->spix, read_buf,read_size,1);
    }
exit:
    HAL_GPIO_WritePin(spi_dev->cs_gpiox, spi_dev->cs_gpio_pin,GPIO_PIN_SET);
    return result;
}

sfud_err sfud_spi_port_init(sfud_flash *flash)
{
    sfud_err result = SFUD_SUCCESS;

    switch (flash->index) {
        case SFUD_W25Q128_DEVICE_INDEX: {
            spi1.spix = &hspi1;
            spi1.cs_gpiox = GPIOA;
            spi1.cs_gpio_pin = GPIO_PIN_4;
            /* 同步 Flash 移植所需的接口及数据 */
            flash->spi.wr = spi_write_read;
            flash->spi.lock = spi_lock;
            flash->spi.unlock = spi_unlock;
            flash->spi.user_data = &spi1;
            /* about 100 microsecond delay */
            flash->retry.delay = delay_100us;
            /* adout 60 seconds timeout */
            flash->retry.times = 60 * 10000;

            break;
        }
    }

    return result;
}

void sfud_log_debug(const char *file, const long line, const char *format, ...) {
    va_list args;

    /* args point to the first variable parameter */
    va_start(args, format);
    rt_kprintf("[SFUD](%s:%ld) ", file, line);
    /* must use vprintf to print */
    rt_vsnprintf(log_buf, sizeof(log_buf), format, args);
    rt_kprintf("%s\r\n", log_buf);
    va_end(args);
}

void sfud_log_info(const char *format, ...) {
    va_list args;

    /* args point to the first variable parameter */
    va_start(args, format);
    rt_kprintf("[SFUD]");
    /* must use vprintf to print */
    rt_vsnprintf(log_buf, sizeof(log_buf), format, args);
    rt_kprintf("%s\r\n", log_buf);
    va_end(args);
}

测试SFUD

在main.c中添加测试代码:


/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "spi.h"
#include "usart.h"
#include "gpio.h"

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MPU_Config(void);
/* USER CODE BEGIN PFP */
extern int rt_kprintf(const char *fmt, ...);
#include "sfud.h"
/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
#define SFUD_DEMO_TEST_BUFFER_SIZE                     1024

static uint8_t sfud_demo_test_buf[SFUD_DEMO_TEST_BUFFER_SIZE];
/**
 * SFUD demo for the first flash device test.
 *
 * @param addr flash start address
 * @param size test flash size
 * @param size test flash data buffer
 */
static void sfud_demo(uint32_t addr, size_t size, uint8_t *data) {
    sfud_err result = SFUD_SUCCESS;
    const sfud_flash *flash = sfud_get_device_table() + 0;
    size_t i;
    /* prepare write data */
    for (i = 0; i < size; i++) {
        data[i] = i;
    }
    /* erase test */
    result = sfud_erase(flash, addr, size);
    if (result == SFUD_SUCCESS) {
        rt_kprintf("Erase the %s flash data finish. Start from 0x%08X, size is %d.\r\n", flash->name, addr,
                size);
    } else {
        rt_kprintf("Erase the %s flash data failed.\r\n", flash->name);
        return;
    }
    /* write test */
    result = sfud_write(flash, addr, size, data);
    if (result == SFUD_SUCCESS) {
        rt_kprintf("Write the %s flash data finish. Start from 0x%08X, size is %d.\r\n", flash->name, addr,
                size);
    } else {
        rt_kprintf("Write the %s flash data failed.\r\n", flash->name);
        return;
    }
    /* read test */
    result = sfud_read(flash, addr, size, data);
    if (result == SFUD_SUCCESS) {
        rt_kprintf("Read the %s flash data success. Start from 0x%08X, size is %d. The data is:\r\n", flash->name, addr,
                size);
        rt_kprintf("Offset (h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\r\n");
        for (i = 0; i < size; i++) {
            if (i % 16 == 0) {
                rt_kprintf("[%08X] ", addr + i);
            }
            rt_kprintf("%02X ", data[i]);
            if (((i + 1) % 16 == 0) || i == size - 1) {
                rt_kprintf("\r\n");
            }
        }
        rt_kprintf("\r\n");
    } else {
        rt_kprintf("Read the %s flash data failed.\r\n", flash->name);
    }
    /* data check */
    for (i = 0; i < size; i++) {
        if (data[i] != i % 256) {
            rt_kprintf("Read and check write data has an error. Write the %s flash data failed.\r\n", flash->name);
			break;
        }
    }
    if (i == size) {
        rt_kprintf("The %s flash test is success.\r\n", flash->name);
    }
}

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MPU Configuration--------------------------------------------------------*/
  MPU_Config();

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_SPI1_Init();
  MX_UART4_Init();
  /* USER CODE BEGIN 2 */
  if (sfud_init() == SFUD_SUCCESS) {
      sfud_demo(0, sizeof(sfud_demo_test_buf), sfud_demo_test_buf);
  }
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

#endif /* USE_FULL_ASSERT */

运行如下:
在这里插入图片描述

3. 生成FLM文件

重新生成不带main函数的工程

在这里插入图片描述

添加修改编程算法文件FlashPrg.c

模板工程里面提供了FlashOS.h和FlashPrg.c ,复制到此工程中,然后对FlashPrg.c 代码进行填充。

/***********************************************************************/
/*  This file is part of the ARM Toolchain package                     */
/*  Copyright (c) 2020 Keil - An ARM Company. All rights reserved.     */
/***********************************************************************/
/*                                                                     */
/*  FlashPrg.c:  Flash Programming Functions adapted for               */
/*               ST Microelectronics STM32h747I-DISCO Flash            */
/*                                                                     */
/***********************************************************************/

#include "FlashOS.H"
#include "sfud.h"
#include "gpio.h"
#include "usart.h"
#include "spi.h"

static uint32_t base_adr;

/*
 *  Initialize Flash Programming Functions
 *    Parameter:      adr:  Device Base Address
 *                    clk:  Clock Frequency (Hz)
 *                    fnc:  Function Code (1 - Erase, 2 - Program, 3 - Verify)
 *    Return Value:   0 - OK,  1 - Failed
 */

#if defined FLASH_MEM || defined FLASH_OTP
int Init (unsigned long adr, unsigned long clk, unsigned long fnc)
{
    MX_GPIO_Init();
    MX_UART4_Init();
    MX_SPI1_Init();
    base_adr = adr;
    if(sfud_init() == SFUD_SUCCESS) {
        return 0;
    } else {
        return 1;
    }
}
#endif


/*
 *  De-Initialize Flash Programming Functions
 *    Parameter:      fnc:  Function Code (1 - Erase, 2 - Program, 3 - Verify)
 *    Return Value:   0 - OK,  1 - Failed
 */

#if defined FLASH_MEM || defined FLASH_OTP
int UnInit (unsigned long fnc)
{

    return (0);
}
#endif

/*
 *  Erase complete Flash Memory
 *    Return Value:   0 - OK,  1 - Failed
 */

int EraseChip (void)
{
    int result = 0;
    const sfud_flash *flash = sfud_get_device_table();
    /* Add your Code */
    result = sfud_erase (flash, 0, flash->chip.capacity);

    if (result == SFUD_SUCCESS)
        return 0;
    else
        return result;                                  // Finished without Errors
}

/*
 *  Erase Sector in Flash Memory
 *    Parameter:      adr:  Sector Address
 *    Return Value:   0 - OK,  1 - Failed
 */

#ifdef FLASH_MEM
int EraseSector (unsigned long adr)
{
    int result = 0;
    uint32_t block_start;
    const sfud_flash *flash;
    flash  = sfud_get_device_table();
    block_start  = adr - base_adr;
    result = sfud_erase (flash, block_start, 4096);

    if (result == SFUD_SUCCESS)
        return 0;
    else
        return result;
}
#endif


/*
 *  Program Page in Flash Memory
 *    Parameter:      adr:  Page Start Address
 *                    sz:   Page Size
 *                    buf:  Page Data
 *    Return Value:   0 - OK,  1 - Failed
 */

#if defined FLASH_MEM || defined FLASH_OTP
int ProgramPage (unsigned long block_start, unsigned long size, unsigned char *buffer)
{
    const sfud_flash *flash = sfud_get_device_table() + 0;
    uint32_t start_addr = block_start - base_adr;

    if(sfud_write(flash, start_addr, size, buffer) == SFUD_SUCCESS)
        return 0;
    else
        return 1;
}


#define PAGE_SIZE            4096
uint8_t aux_buf[PAGE_SIZE];
unsigned long Verify (unsigned long adr, unsigned long sz, unsigned char *buf)
{
    int i;
    const sfud_flash *flash = sfud_get_device_table();
    sfud_read(flash, adr - base_adr, sz, aux_buf);

    for (i = 0; i < PAGE_SIZE; i++) {
        if (aux_buf[i] != buf[i])
            return (adr + i);                 // Verification Failed (return address)
    }

    return (adr + sz);                    // Done successfully
}

#endif

在工程中定义FLASH_MEM宏
在这里插入图片描述

添加修改配置文件FlashDev.c

模板工程里面提供了FlashDev.c ,复制到此工程中,然后对代码进行修改。

/***********************************************************************/
/*  This file is part of the ARM Toolchain package                     */
/*  Copyright (c) 2020 Keil - An ARM Company. All rights reserved.     */
/***********************************************************************/
/*                                                                     */
/*  FlashDev.c:  Device Description for ST STM32H747I-DISCO Flash      */
/*                                                                     */
/***********************************************************************/

#include "FlashOS.H"        

#ifdef FLASH_MEM
struct FlashDevice const FlashDevice  =  {
   FLASH_DRV_VERS,             // Driver Version, do not modify!
   "STM32H750-ARTPI",          // Device Name 
   EXTSPI,                     // Device Type
   0x90000000,                 // Device Start Address
   0x08000000,                 // Device Size in Bytes (128MB)
   0x00001000,                 // Programming Page Size 4096 Bytes
   0x00,                       // Reserved, must be 0
   0xFF,                       // Initial Content of Erased Memory
   10000,                      // Program Page Timeout 100 mSec
   6000,                       // Erase Sector Timeout 6000 mSec

// Specify Size and Address of Sectors
   0x1000, 0x000000,            // Sector Size  4kB
   SECTOR_END
};
#endif // FLASH_MEM

特别注意:"STM32H750-ARTPI"就是MDK的Option选项里面会识别出这个名字。0x90000000是MDK分散加载文件中定义的外部flash起始地址。

地址无关代码实现

C和汇编的配置都勾选上:
在这里插入图片描述
在这里插入图片描述

ROPI地址无关实现

如果程序的所有只读段都与位置无关,则该程序为只读位置无关(ROPI, Read-only position independence)。ROPI段通常是位置无关代码(PIC,position-independent code),但可以是只读数据,也可以是PIC和只读数据的组合。选择“ ROPI”选项,可以避免用户不得不将代码加载到内存中的特定位置。这对于以下例程特别有用:

(1)加载以响应运行事件。

(2)在不同情况下使用其他例程的不同组合加载到内存中。

(3)在执行期间映射到不同的地址。

RWPI数据无关实现
使用Read-Write position independence同理,表示的可读可写数据段。
使用RWPI编译代码,解决RW段即全局变量的加载。首先编译的时候会为每一个全局变量生成一个相对于r9寄存器的偏移量,这个偏移量会在.text段中。

在加载elf阶段,将RW段加载到RAM当中之后,需要将r9寄存器指向此片内存的基地址,然后接下来就可以跳转到加载的elf的代码中去执行,就可以实现全局变量的加载了。这也就是利用MDK的FLM文件生成通用flash驱动中提到的需要在编译选项中添加-ffixed-r9的原因。

综上所述,勾选ROPI和RWPI选项,可以实现elf文件的动态加载,还遗留的一个小问题是elf模块如何调用系统函数,与此文无关,留在以后再讲。

特别注意:

  • 由于模块中不含中断向量表,所以程序中不要开启任何中断。
  • startup_stm32h750xx.s不再需要参与编译
    在这里插入图片描述

修改分散加载文件

复制一份新的分散加载文件到工程目录中,然后修改成如下代码
在这里插入图片描述
–diag_suppress L6305用于屏蔽没有入口地址的警告信息。

分散加载文件中的内容如下:

; Linker Control File (scatter-loading)
;

PRG 0 PI               ; Programming Functions
{
  PrgCode +0           ; Code
  {
    * (+RO)
  }
  PrgData +0           ; Data
  {
    * (+RW,+ZI)
  }
}

DSCR +0                ; Device Description
{
  DevDscr +0
  {
    FlashDev.o
  }
}

将程序可执行文件axf修改为flm格式

通过这个cmd.exe /C copy "!L" "..\@L.FLM"命令就可以将生成的axf可执行文件修改为flm。
在这里插入图片描述
在这里插入图片描述
将生成的flm文件拷贝到…\Keil_v5\ARM\Flash目录,即可被MDK识别到。

DEMO下载地址:https://gitee.com/Aladdin-Wang/STM32H7_W25QXXX

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【STM32H750】从零编写MDK的FLM烧录算法 的相关文章

  • C++程序员必看书单

    转载 xff1a https blog csdn net ljy1988123 article details 7748913 comments C 43 43 xff1a Prata C 43 43 Primer Plus xff1a 基
  • git submodule 升级commit并push

    git submodule 升级commit并push 关于这个问题 xff0c 可以参照以下文章 xff1a https blog csdn net wwj 748 article details 73991862 流程写的很清楚 xff
  • 欧拉角pitch、yaw,roll的理解

    关于旋转永远是做游戏的难点和混乱点 我们知道表示一个旋转有多种方式 xff0c 简单的欧拉角 xff0c 复杂点的四元数 xff0c 再复杂点的矩阵 之前接触unity可以用四元数和欧拉角两种方式表示旋转 xff0c 最近一直研究虚幻引擎
  • Mac执行ruby命令提示 dyld: Library not loaded等类似问题解决方案

    说一下为啥会遇见这么个问题 xff0c 我在给一个xcode项目添加podfile的时候 xff0c 在终端执行了pod init命令 xff0c 随即给了我一个如下图的提示 xff08 报错信息一样的 xff0c 执行pod的命令早就被解
  • 编程题:多线程交替打印ABC

    要求创建3个线程 xff0c 分别打印ABC xff0c 共交替打印10次 span class token keyword public span span class token keyword class span span clas
  • Android App Bundle 自动打包原理

    Google推出Android App Bundle 已经有一段时间了 根据Google的政策说明 xff0c 预计2021年8月之后 xff0c 新发布的应用都必须使用Android App Bundle aab 来上架Google Pl
  • 【玩转Linux】Linux虚拟机设置固定IP

    Linux虚拟机Centos系统的ip总是变化 xff0c 如何固定下来 xff1f 尝试了好多方式 xff0c 终于找到一种最为简单的方法 文章目录 1 查看Centos的IP信息2 修改文件3 刷新网络配置 1 查看Centos的IP信
  • Docker安装MySQL数据库

    文章目录 一 简单方式二 挂载方式1 创建挂载目录2 启动容器 三 修改配置文件1 新建my cnf2 编辑my cnf3 查看是否生效 一 简单方式 span class token function docker span run sp
  • 解决Docker镜像拉取失败问题

    一 问题 Docker拉取mysql镜像 xff0c 发生报错 span class token function docker span pull mysql 8 0 22 报错信息 xff1a Error response from d
  • Docker安装RabbitMQ消息队列

    文章目录 1 启动容器2 连接访问 1 启动容器 span class token function docker span run name rabbitmq span class token punctuation span resta
  • 【玩转Linux】Linux安装宝塔面板

    文章目录 一 简介二 安装1 centos脚本安装2 浏览器访问 三 总结 一 简介 宝塔面板 xff0c 是安全高效的服务器运维面板 xff0c 一个提升运维效率的服务器管理软件 xff0c 支持一键LAMP LNMP 集群 监控 网站
  • 使用JDK的keytool工具生成JKS证书

    使用JDK的keytool工具生成JKS证书 文章目录 1 生成JKS证书2 查看JKS证书详细信息3 导出证书 1 生成JKS证书 keytool genkey alias jwt keyalg RSA keystore jwt jks
  • 【算法】二分查找

    算法 二分查找 题目 xff1a 请实现无重复数字的升序数组的二分查找 难度 xff1a 简单 代码 xff1a 二分查找 xff0c 又叫折半查找 xff0c 要求待查找的序列有序 每次取中间位置的值与待查关键字比较 xff0c 如果中间
  • 【算法】反转链表

    算法 反转链表 题目 xff1a 给定一个单链表的头结点pHead xff0c 长度为n xff0c 反转该链表后 xff0c 返回新链表的表头 难度 xff1a 简单 代码 xff1a span class token keyword c
  • 【算法】合并两个排序的链表

    算法 合并两个排序的链表 题目 xff1a 输入两个递增的链表 xff0c 单个链表的长度为n xff0c 合并这两个链表并使新链表中的节点仍然是递增排序的 难度 xff1a 简单 代码 xff1a span class token key
  • 【算法】判断链表中是否有环

    算法 判断链表中是否有环 题目 xff1a 判断给定的链表中是否有环 如果有环则返回true xff0c 否则返回false 难度 xff1a 简单 代码 xff1a span class token keyword public span
  • 教你快速高效接入SDK——手游聚合SDK的总体思路和架构

    U8SDK技术博客 xff1a http www uustory com xff0c 欢迎来坐坐 百度传课已经停运 xff0c 最新U8SDK视频教程已经转移至B站 xff1a U8SDK最新视频教程 题记 xff1a 很多做游戏开发的人
  • 【算法】删除链表的倒数第N个结点

    算法 删除链表的倒数第N个结点 题目 xff1a 给你一个链表 xff0c 删除链表的倒数第 n 个结点 xff0c 并且返回链表的头结点 难度 xff1a 中等 代码 xff1a span class token keyword publ
  • 阿里云智能编码插件Cosy,提升开发效率杠杠滴!

    文章目录 一 简介二 核心功能1 代码智能补全2 代码示例搜索 三 产品特性1 提升编码效率2 沉浸式开发3 低资源消耗4 隐私保护 四 快速开始1 安装IntelliJ IDEA插件2 测试代码智能补全3 测试代码示例搜索 五 总结 一
  • Alibaba Cloud Toolkit轻量部署插件,一键发布服务器

    文章目录 一 简介二 部署方式1 传统部署方式2 Cloud Toolkit部署方式 四 产品功能五 部署步骤1 安装插件2 添加主机3 Deploy to Host4 控制台5 服务器 六 服务器代码热部署七 Arthas诊断 一 简介

随机推荐

  • 新人一看就懂:Dubbo3 + Nacos的RPC远程调用框架demo

    文章目录 一 前言Feign和Dubbo到底有啥区别 xff0c 为啥大厂都爱用RPC框架 xff1f 二 简介三 dubbo api xff08 对外暴漏的接口 xff09 1 TestService接口 四 dubbo provider
  • Docker安装Kafka消息队列

    文章目录 1 安装zookeeper2 安装kafka3 安装kafka map xff08 可选 xff09 1 安装zookeeper span class token function docker span run span cla
  • 【Spring Boot实战与进阶】集成Kafka消息队列

    汇总目录链接 xff1a Spring Boot实战与进阶 学习目录 文章目录 一 简介二 集成Kafka消息队列1 引入依赖2 配置文件3 测试生产消息4 测试消费消息 一 简介 Kafka是由Apache软件基金会开发的一个开源流处理平
  • Hutool工具类之excel导入导出

    文章目录 1 导入excel2 导出excel 1 导入excel span class token class name ExcelReader span reader span class token operator 61 span
  • Docker安装RockerMQ消息队列

    文章目录 1 安装namesrv2 安装broker3 安装console xff08 可选 xff09 1 安装namesrv namesrv就类似于消息队列的注册中心 span class token function docker s
  • 【Spring Boot实战与进阶】集成RockerMQ消息队列

    汇总目录链接 xff1a Spring Boot实战与进阶 学习目录 文章目录 一 简介二 集成RockerMQ消息队列1 引入依赖2 配置文件3 测试生产消息4 测试消费消息 一 简介 RocketMQ 是阿里巴巴在2012年开源的分布式
  • linux下elasticsearch 安装、配置及示例

    简介 开始学es xff0c 我习惯边学边记 xff0c 总结出现的问题和解决方法 本文是在两台linux虚拟机下 xff0c 安装了三个节点 本次搭建es同时实践了两种模式 单机模式和分布式模式 条件允许的话 xff0c 可以在多台机器上
  • Java服务器热部署的实现原理

    今天发现早年在大象笔记中写的一篇笔记 xff0c 之前放在ijavaboy上的 xff0c 现在它已经访问不了了 前几天又有同事在讨论这个问题 这里拿来分享一下 在web应用开发或者游戏服务器开发的过程中 xff0c 我们时时刻刻都在使用热
  • win10打开热点提示:我们无法设置移动热点

    解决方案 xff1a 1 右键我的电脑 xff0c 打开管理 2 双击带有下载标记wi fi 适配器 xff0c 点击启用设备 xff0c 确认即可
  • Docker容器打包迁移

    有时在一个docker内部署的容器不想在其它服务器容器上重新部署 xff0c 再配置配置文件 这时直接将当前的容器打包然后直接部署到新的服务器上即可 xff0c 免了再配置一遍 1 将容器保存为镜像 先把容器停止运行不然会有问题 容器保存为
  • 四旋翼飞行器的原理研究和建模

    四旋翼飞行器的原理研究和建模 对四旋翼飞行器的工作原理进行了简单介绍 xff0c 对其飞行姿态角进行描述 xff0c 并在此基础上建立数学模型 四旋翼飞行器的原理 根据四旋翼飞行器的运动方式的特点将其飞行控制划分为四种基本的飞行控制方式 1
  • 数组名和函数名是什么东西

    数组名和函数名的本质都是一个 指向数组首地址或函数体的指针常量 的名字 规则1 xff1a 数组 61 指向数组首地址的指针常量 43 数组元素 简单说就是 xff0c 数组 61 指针常量 43 数组内容 xff0c 数组名就是这个指针常
  • RT-Thread进阶之低功耗PM组件应用笔记

    电源管理组件 嵌入式系统低功耗管理的目的在于满足用户对性能需求的前提下 xff0c 尽可能降低系统能耗以延长设备待机时间 高性能与有限的电池能量在嵌入式系统中矛盾最为突出 xff0c 硬件低功耗设计与软件低功耗管理的联合应用成为解决矛盾的有
  • 【STM32H750】玩转ART-Pi(一)——使用STM32CUBMX生成TouchGFX工程

    目录 STM32H750 玩转ART Pi xff08 一 xff09 使用STM32CUBMX生成TouchGFX工程 STM32H750 玩转ART Pi xff08 二 xff09 制作MDK的外部QSPI FLASH烧录算法 STM
  • 【STM32H750】玩转ART-Pi(二)——制作MDK的外部QSPI-FLASH烧录算法

    目录 STM32H750 玩转ART Pi xff08 一 xff09 使用STM32CUBMX生成TouchGFX工程 STM32H750 玩转ART Pi xff08 二 xff09 制作MDK的外部QSPI FLASH烧录算法 STM
  • linux驱动开发篇(四)—— platform平台设备驱动

    linux系列目录 xff1a linux基础篇 xff08 一 xff09 GCC和Makefile编译过程 linux基础篇 xff08 二 xff09 静态和动态链接 ARM裸机篇 xff08 一 xff09 i MX6ULL介绍 A
  • C语言变参函数和可变参数宏

    文章目录 一 变参函数的设计与实现1 变参函数初体验2 变参函数改进版3 变参函数 V3 0 版本3 变参函数 V4 0 版本4 变参函数 V5 0 版本 二 可变参数宏的设计与实现1 什么是可变参数宏2 宏连接符 的作用3 可变参数宏的另
  • 利用MDK的FLM文件生成通用flash驱动

    文章目录 前言一 FLM文件是什么 xff1f 二 FLM文件结构1 FlashPrg c2 FlashPrg c 三 解析FLM文件1 解析flm文件 四 设计flash驱动抽象层五 快速使用 前言 在进行Flash操作时 xff0c 一
  • 游标的概念和作用

    游标实际上是一种能从包括多条数据记录的结果集中每次提取一条记录的机制 游标充当指针的作用 尽管游标能遍历结果中的所有行 xff0c 但他一次只指向一行 概括来讲 xff0c SQL的游标是一种临时的数据库对象 xff0c 即可以用来存放在数
  • 【STM32H750】从零编写MDK的FLM烧录算法

    文章目录 前言一 将代码中的图片资源下载到外部flash1 修改分散加载文件 二 MDK下载算法原理1 程序能够通过下载算法下载到芯片的原理2 算法程序中擦除操作执行流程3 制作FLM文件步骤 三 使用STM32CubeMX新建工程1 新建