• 从零编写STM32H7的MDK SPI FLASH下载算法


    从零编写STM32H7的MDK SPI FLASH下载算法 - 知乎

    Part1前言

    当我们要下载编译好的镜像到Flash时,首先要做的一步就是选择合适的Flash下载算法,而这个算法本身就是一个FLM文件:

    代码既可以下载到内部flash,也可以下载到外部flash,或者一部分下载到内部,一部分下载到外部。

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

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

    1. 通过MDK打开分散加载文件,配置“ExtFlashSection”段:
    1. ; *************************************************************
    2. ; *** Scatter-Loading Description File generated by uVision ***
    3. ; *************************************************************
    4. LR_IROM1 0x08000000 0x00020000 { ; load region size_region
    5. ER_IROM1 0x08000000 0x00020000 { ; load address = execution address
    6. *.o (RESET, +First)
    7. *(InRoot$$Sections)
    8. .ANY (+RO)
    9. .ANY (+XO)
    10. }
    11. RW_IRAM1 0x20000000 0x00020000 { ; RW data
    12. .ANY (+RW +ZI)
    13. }
    14. RW_IRAM2 0x24000000 0x00080000 {
    15. .ANY (+RW +ZI)
    16. }
    17. }
    18. LR_EROM1 0x90000000 0x01000000 { ; load region size_region
    19. ER_EROM1 0x90000000 0x01000000 { ; load address = execution address
    20. *.o (ExtFlashSection)
    21. *.o (FontFlashSection)
    22. *.o (TextFlashSection)
    23. }
    24. }

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

    1. 在代码中将图片资源分配到ExtFlashSection段
    1. #define LOCATION_ATTRIBUTE(name) __attribute__((section(name))) __attribute__((aligned(4)))
    2. KEEP extern const unsigned char image_watch_seconds[] LOCATION_ATTRIBUTE("ExtFlashSection") = // 4x202 ARGB8888 pixels.
    3. {
    4. 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00,
    5. 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0xff,
    6. 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00,
    7. 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0x00,
    8. 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff,
    9. 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0xff,
    10. 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00,
    11. 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0x00,
    12. 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff,
    13. 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0x00,
    14. 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff,
    15. 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0xff,
    16. 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0xff, 0xf8, 0xfc, 0xf8, 0x00,
    17. 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00, 0xf8, 0xfc, 0xf8, 0x00,
    18. 0xf8, 0xfc, 0xf8, 0x00
    19. };
    1. 编译代码

    查看map文件,image_watch_seconds这个数组已经被分配到了0X90138690这个地址了,这个地址正是LR_EROM1 所在的区间。

    Part3二、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 编程算法。

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

    Part4三、使用STM32CubeMX新建工程

    4新建工程

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

    软件: STM32CubeMX,MDK

    选择MCU型号(STM32H750XBH6)

    选择MCU型号

    配置SPI

    配置SPI

    配置UART

    配置UART

    配置时钟树

    配置时钟树

    设置调试接口

    设置调试接口

    设置工程并生成工程

    生成工程

    广告

    嵌入式/物联网开发学习资料

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

    SFUD 是什么

    SFUD(https://github.com/armink/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添加到工程目录:

    修改sfud_port.c文件:

    1. #include <string.h>
    2. #include <sfud.h>
    3. #include <stdarg.h>
    4. #include "gpio.h"
    5. #include "spi.h"
    6. typedef struct {
    7. SPI_HandleTypeDef *spix;
    8. GPIO_TypeDef *cs_gpiox;
    9. uint16_t cs_gpio_pin;
    10. } spi_user_data, *spi_user_data_t;
    11. static spi_user_data spi1;
    12. static char log_buf[256];
    13. void sfud_log_debug(const char *file, const long line, const char *format, ...);
    14. extern int rt_vsnprintf(char *buf, int size, const char *fmt, va_list args);
    15. extern int rt_kprintf(const char *fmt, ...);
    16. static void spi_lock(const sfud_spi *spi)
    17. {
    18. }
    19. static void spi_unlock(const sfud_spi *spi)
    20. {
    21. }
    22. /* about 100 microsecond delay */
    23. static void delay_100us(void) {
    24. uint32_t delay = 2000;
    25. while(delay--);
    26. }
    27. /**
    28. * SPI write data then read data
    29. */
    30. static sfud_err spi_write_read(const sfud_spi *spi, const uint8_t *write_buf, size_t write_size, uint8_t *read_buf,
    31. size_t read_size)
    32. {
    33. sfud_err result = SFUD_SUCCESS;
    34. /**
    35. * add your spi write and read code
    36. */
    37. spi_user_data_t spi_dev = (spi_user_data_t) spi->user_data;
    38. HAL_GPIO_WritePin(spi_dev->cs_gpiox, spi_dev->cs_gpio_pin,GPIO_PIN_RESET);
    39. if (write_size) {
    40. HAL_SPI_Transmit(spi_dev->spix, (uint8_t *)write_buf,write_size,1);
    41. }
    42. if (read_size) {
    43. HAL_SPI_Receive(spi_dev->spix, read_buf,read_size,1);
    44. }
    45. exit:
    46. HAL_GPIO_WritePin(spi_dev->cs_gpiox, spi_dev->cs_gpio_pin,GPIO_PIN_SET);
    47. return result;
    48. }
    49. sfud_err sfud_spi_port_init(sfud_flash *flash)
    50. {
    51. sfud_err result = SFUD_SUCCESS;
    52. switch (flash->index) {
    53. case SFUD_W25Q128_DEVICE_INDEX: {
    54. spi1.spix = &hspi1;
    55. spi1.cs_gpiox = GPIOA;
    56. spi1.cs_gpio_pin = GPIO_PIN_4;
    57. /* 同步 Flash 移植所需的接口及数据 */
    58. flash->spi.wr = spi_write_read;
    59. flash->spi.lock = spi_lock;
    60. flash->spi.unlock = spi_unlock;
    61. flash->spi.user_data = &spi1;
    62. /* about 100 microsecond delay */
    63. flash->retry.delay = delay_100us;
    64. /* adout 60 seconds timeout */
    65. flash->retry.times = 60 * 10000;
    66. break;
    67. }
    68. }
    69. return result;
    70. }
    71. void sfud_log_debug(const char *file, const long line, const char *format, ...) {
    72. va_list args;
    73. /* args point to the first variable parameter */
    74. va_start(args, format);
    75. rt_kprintf("[SFUD](%s:%ld) ", file, line);
    76. /* must use vprintf to print */
    77. rt_vsnprintf(log_buf, sizeof(log_buf), format, args);
    78. rt_kprintf("%s\r\n", log_buf);
    79. va_end(args);
    80. }
    81. void sfud_log_info(const char *format, ...) {
    82. va_list args;
    83. /* args point to the first variable parameter */
    84. va_start(args, format);
    85. rt_kprintf("[SFUD]");
    86. /* must use vprintf to print */
    87. rt_vsnprintf(log_buf, sizeof(log_buf), format, args);
    88. rt_kprintf("%s\r\n", log_buf);
    89. va_end(args);
    90. }

    测试SFUD

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

    1. /* USER CODE END Header */
    2. /* Includes ------------------------------------------------------------------*/
    3. #include "main.h"
    4. #include "spi.h"
    5. #include "usart.h"
    6. #include "gpio.h"
    7. /* Private function prototypes -----------------------------------------------*/
    8. void SystemClock_Config(void);
    9. static void MPU_Config(void);
    10. /* USER CODE BEGIN PFP */
    11. extern int rt_kprintf(const char *fmt, ...);
    12. #include "sfud.h"
    13. /* USER CODE END PFP */
    14. /* Private user code ---------------------------------------------------------*/
    15. /* USER CODE BEGIN 0 */
    16. #define SFUD_DEMO_TEST_BUFFER_SIZE 1024
    17. static uint8_t sfud_demo_test_buf[SFUD_DEMO_TEST_BUFFER_SIZE];
    18. /**
    19. * SFUD demo for the first flash device test.
    20. *
    21. * @param addr flash start address
    22. * @param size test flash size
    23. * @param size test flash data buffer
    24. */
    25. static void sfud_demo(uint32_t addr, size_t size, uint8_t *data) {
    26. sfud_err result = SFUD_SUCCESS;
    27. const sfud_flash *flash = sfud_get_device_table() + 0;
    28. size_t i;
    29. /* prepare write data */
    30. for (i = 0; i < size; i++) {
    31. data[i] = i;
    32. }
    33. /* erase test */
    34. result = sfud_erase(flash, addr, size);
    35. if (result == SFUD_SUCCESS) {
    36. rt_kprintf("Erase the %s flash data finish. Start from 0x%08X, size is %d.\r\n", flash->name, addr,
    37. size);
    38. } else {
    39. rt_kprintf("Erase the %s flash data failed.\r\n", flash->name);
    40. return;
    41. }
    42. /* write test */
    43. result = sfud_write(flash, addr, size, data);
    44. if (result == SFUD_SUCCESS) {
    45. rt_kprintf("Write the %s flash data finish. Start from 0x%08X, size is %d.\r\n", flash->name, addr,
    46. size);
    47. } else {
    48. rt_kprintf("Write the %s flash data failed.\r\n", flash->name);
    49. return;
    50. }
    51. /* read test */
    52. result = sfud_read(flash, addr, size, data);
    53. if (result == SFUD_SUCCESS) {
    54. rt_kprintf("Read the %s flash data success. Start from 0x%08X, size is %d. The data is:\r\n", flash->name, addr,
    55. size);
    56. rt_kprintf("Offset (h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\r\n");
    57. for (i = 0; i < size; i++) {
    58. if (i % 16 == 0) {
    59. rt_kprintf("[%08X] ", addr + i);
    60. }
    61. rt_kprintf("%02X ", data[i]);
    62. if (((i + 1) % 16 == 0) || i == size - 1) {
    63. rt_kprintf("\r\n");
    64. }
    65. }
    66. rt_kprintf("\r\n");
    67. } else {
    68. rt_kprintf("Read the %s flash data failed.\r\n", flash->name);
    69. }
    70. /* data check */
    71. for (i = 0; i < size; i++) {
    72. if (data[i] != i % 256) {
    73. rt_kprintf("Read and check write data has an error. Write the %s flash data failed.\r\n", flash->name);
    74. break;
    75. }
    76. }
    77. if (i == size) {
    78. rt_kprintf("The %s flash test is success.\r\n", flash->name);
    79. }
    80. }
    81. /* USER CODE END 0 */
    82. int main(void)
    83. {
    84. /* MPU Configuration--------------------------------------------------------*/
    85. MPU_Config();
    86. /* MCU Configuration--------------------------------------------------------*/
    87. /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    88. HAL_Init();
    89. SystemClock_Config();
    90. MX_GPIO_Init();
    91. MX_SPI1_Init();
    92. MX_UART4_Init();
    93. /* USER CODE BEGIN 2 */
    94. if (sfud_init() == SFUD_SUCCESS) {
    95. sfud_demo(0, sizeof(sfud_demo_test_buf), sfud_demo_test_buf);
    96. }
    97. while (1)
    98. {
    99. }
    100. }
    101. #endif /* USE_FULL_ASSERT */

    运行如下:

    63. 制作下载算法

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

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

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

    1. #include "FlashOS.H"
    2. #include "sfud.h"
    3. #include "gpio.h"
    4. #include "usart.h"
    5. #include "spi.h"
    6. static uint32_t base_adr;
    7. /*
    8. * Initialize Flash Programming Functions
    9. * Parameter: adr: Device Base Address
    10. * clk: Clock Frequency (Hz)
    11. * fnc: Function Code (1 - Erase, 2 - Program, 3 - Verify)
    12. * Return Value: 0 - OK, 1 - Failed
    13. */
    14. #if defined FLASH_MEM || defined FLASH_OTP
    15. int Init (unsigned long adr, unsigned long clk, unsigned long fnc)
    16. {
    17. MX_GPIO_Init();
    18. MX_UART4_Init();
    19. MX_SPI1_Init();
    20. base_adr = adr;
    21. if(sfud_init() == SFUD_SUCCESS) {
    22. return 0;
    23. } else {
    24. return 1;
    25. }
    26. }
    27. #endif
    28. /*
    29. * De-Initialize Flash Programming Functions
    30. * Parameter: fnc: Function Code (1 - Erase, 2 - Program, 3 - Verify)
    31. * Return Value: 0 - OK, 1 - Failed
    32. */
    33. #if defined FLASH_MEM || defined FLASH_OTP
    34. int UnInit (unsigned long fnc)
    35. {
    36. return (0);
    37. }
    38. #endif
    39. /*
    40. * Erase complete Flash Memory
    41. * Return Value: 0 - OK, 1 - Failed
    42. */
    43. int EraseChip (void)
    44. {
    45. int result = 0;
    46. const sfud_flash *flash = sfud_get_device_table();
    47. /* Add your Code */
    48. result = sfud_erase (flash, 0, flash->chip.capacity);
    49. if (result == SFUD_SUCCESS)
    50. return 0;
    51. else
    52. return result; // Finished without Errors
    53. }
    54. /*
    55. * Erase Sector in Flash Memory
    56. * Parameter: adr: Sector Address
    57. * Return Value: 0 - OK, 1 - Failed
    58. */
    59. #ifdef FLASH_MEM
    60. int EraseSector (unsigned long adr)
    61. {
    62. int result = 0;
    63. uint32_t block_start;
    64. const sfud_flash *flash;
    65. flash = sfud_get_device_table();
    66. block_start = adr - base_adr;
    67. result = sfud_erase (flash, block_start, 4096);
    68. if (result == SFUD_SUCCESS)
    69. return 0;
    70. else
    71. return result;
    72. }
    73. #endif
    74. /*
    75. * Program Page in Flash Memory
    76. * Parameter: adr: Page Start Address
    77. * sz: Page Size
    78. * buf: Page Data
    79. * Return Value: 0 - OK, 1 - Failed
    80. */
    81. #if defined FLASH_MEM || defined FLASH_OTP
    82. int ProgramPage (unsigned long block_start, unsigned long size, unsigned char *buffer)
    83. {
    84. const sfud_flash *flash = sfud_get_device_table() + 0;
    85. uint32_t start_addr = block_start - base_adr;
    86. if(sfud_write(flash, start_addr, size, buffer) == SFUD_SUCCESS)
    87. return 0;
    88. else
    89. return 1;
    90. }
    91. #define PAGE_SIZE 4096
    92. uint8_t aux_buf[PAGE_SIZE];
    93. unsigned long Verify (unsigned long adr, unsigned long sz, unsigned char *buf)
    94. {
    95. int i;
    96. const sfud_flash *flash = sfud_get_device_table();
    97. sfud_read(flash, adr - base_adr, sz, aux_buf);
    98. for (i = 0; i < PAGE_SIZE; i++) {
    99. if (aux_buf[i] != buf[i])
    100. return (adr + i); // Verification Failed (return address)
    101. }
    102. return (adr + sz); // Done successfully
    103. }
    104. #endif

    在工程中定义FLASH_MEM宏

    添加修改配置文件FlashDev.c

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

    1. #include "FlashOS.H"
    2. #ifdef FLASH_MEM
    3. struct FlashDevice const FlashDevice = {
    4. FLASH_DRV_VERS, // Driver Version, do not modify!
    5. "STM32H750-ARTPI", // Device Name
    6. EXTSPI, // Device Type
    7. 0x90000000, // Device Start Address
    8. 0x08000000, // Device Size in Bytes (128MB)
    9. 0x00001000, // Programming Page Size 4096 Bytes
    10. 0x00, // Reserved, must be 0
    11. 0xFF, // Initial Content of Erased Memory
    12. 10000, // Program Page Timeout 100 mSec
    13. 6000, // Erase Sector Timeout 6000 mSec
    14. // Specify Size and Address of Sectors
    15. 0x1000, 0x000000, // Sector Size 4kB
    16. SECTOR_END
    17. };
    18. #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用于屏蔽没有入口地址的警告信息。

    1. ; Linker Control File (scatter-loading)
    2. ;
    3. PRG 0 PI ; Programming Functions
    4. {
    5. PrgCode +0 ; Code
    6. {
    7. * (+RO)
    8. }
    9. PrgData +0 ; Data
    10. {
    11. * (+RW,+ZI)
    12. }
    13. }
    14. DSCR +0 ; Device Description
    15. {
    16. DevDscr +0
    17. {
    18. FlashDev.o
    19. }
    20. }

    将程序可执行文件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

  • 相关阅读:
    JVM面试题-JVM对象的创建过程、内存分配、内存布局、访问定位等问题详解
    Spring 常用注解及作用
    【全开源】Java同城预约月嫂服务上门服务本地服务源码APP+小程序+公众号+H 5
    79 C++ STL pair(对组)
    Git认识与运用
    刚参加工作的表弟问我枚举跟常量的使用场景
    非零基础自学Java (老师:韩顺平) 第8章 面向对象编程(中级部分) 8.4 包
    数据结构——常见简答题汇总
    设计模式java版本阅读笔记 一 创建型
    就业喜报:产品岗位提升的底层能力是产品思维
  • 原文地址:https://blog.csdn.net/xiaohaolaoda/article/details/132692004