简说stm32的startup.s文件和ld链接脚本

在STM32的开发中,除了像CMSISI库和驱动库这些源码文件外,还会用到startup.s 文件和链接脚本(通常是 .ld 文件),通常这两个件都是IDE根据相应的MCU型号自动获取放入工程中,普通开发不需要关注,但是这两个文件对程序的初始化、内存布局以及执行流程有着重要影响,做一些高级应用时需要手动配置修改。


1. startup.s 的影响

startup.s 是汇编语言编写的启动文件,通常用于初始化硬件和设置运行环境。以下是它可能影响的代码部分:

  • 堆栈初始化

    asm 复制代码
    /* 设置初始堆栈指针 */
    ldr sp, =_estack

    解释:startup.s 会初始化堆栈指针(SP),确保 C/C++ 程序能够正确使用堆栈进行函数调用和局部变量存储。

  • 全局变量初始化

    asm 复制代码
    /* 将 .data 段从 Flash 复制到 RAM */
    ldr r0, =__etext
    ldr r1, =__data_start__
    ldr r2, =__data_end__
    mov r3, #0
    copy_data:
        ldrb r4, [r0], #1
        strb r4, [r1], #1
        add r3, r3, #1
        cmp r3, r2
        bne copy_data
    
    /* 清零 .bss 段 */
    ldr r0, =__bss_start__
    ldr r1, =__bss_end__
    mov r2, #0
    zero_bss:
        strb r2, [r0], #1
        cmp r0, r1
        bne zero_bss

    解释:startup.s 负责将 .data 段从 Flash 复制到 RAM,并清零 .bss 段中的全局变量。如果这些步骤未正确完成,程序中依赖于全局变量的代码可能会出现异常行为。

  • 主函数调用

    asm 复制代码
    /* 调用 main 函数 */
    bl main

    解释:startup.s 最终调用 main 函数,启动用户代码的执行。


2. 链接文件的影响

链接文件定义了程序的内存布局,包括各个段的位置和大小。以下是它可能影响的代码部分:

  • 内存段分配

    ld 复制代码
    MEMORY {
        FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
        SRAM  (rw)  : ORIGIN = 0x20000000, LENGTH = 64K
    }
    
    SECTIONS {
        .text : {
            *(.text*)
        } > FLASH
    
        .rodata : {
            *(.rodata*)
        } > FLASH
    
        .data : {
            _sidata = LOADADDR(.data);
            *(.data*)
        } > SRAM AT> FLASH
    
        .bss : {
            *(.bss*)
        } > SRAM
    }

    解释:

    • 链接文件将代码段(.text)、只读数据段(.rodata)放置在 Flash 中。
    • 可写数据段(.data)和未初始化数据段(.bss)放置在 SRAM 中。
    • 如果链接文件配置错误,可能导致程序无法正确访问数据或执行代码。
  • 符号定义

    ld 复制代码
    PROVIDE(_estack = ORIGIN(SRAM) + LENGTH(SRAM));
    PROVIDE(__etext = .);
    PROVIDE(__data_start__ = .);
    PROVIDE(__data_end__ = .);
    PROVIDE(__bss_start__ = .);
    PROVIDE(__bss_end__ = .);

    解释:链接文件定义了一些关键符号,例如堆栈顶部地址 _estack 和数据段的起始/结束地址。这些符号被 startup.s 使用来完成初始化。


示例代码

以下是一个简单的 STM32 项目结构,展示 startup.s 和链接文件如何协作影响代码:

startup.s
asm 复制代码
/* 启动文件 */
    .syntax unified
    .cpu cortex-m4
    .fpu softvfp
    .thumb

    .global Reset_Handler
    .type Reset_Handler, %function

Reset_Handler:
    /* 初始化堆栈指针 */
    ldr sp, =_estack

    /* 复制 .data 段 */
    ldr r0, =__etext
    ldr r1, =__data_start__
    ldr r2, =__data_end__
copy_data_loop:
    cmp r0, r2
    beq clear_bss
    ldrb r3, [r0], #1
    strb r3, [r1], #1
    b copy_data_loop

clear_bss:
    /* 清零 .bss 段 */
    ldr r0, =__bss_start__
    ldr r1, =__bss_end__
zero_loop:
    cmp r0, r1
    beq call_main
    movs r2, #0
    strb r2, [r0], #1
    b zero_loop

call_main:
    /* 调用 main 函数 */
    bl main

hang:
    b hang
链接文件 (STM32.ld)
ld 复制代码
/* 链接脚本 */
MEMORY {
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
    SRAM  (rw)  : ORIGIN = 0x20000000, LENGTH = 64K
}

SECTIONS {
    .text : {
        *(.text*)
    } > FLASH

    .rodata : {
        *(.rodata*)
    } > FLASH

    .data : {
        _sidata = LOADADDR(.data);
        *(.data*)
    } > SRAM AT> FLASH

    .bss : {
        *(.bss*)
    } > SRAM

    PROVIDE(_estack = ORIGIN(SRAM) + LENGTH(SRAM));
}
主程序 (main.c)
c 复制代码
#include <stdio.h>

int global_var = 42;

void main() {
    static int static_var = 10;
    printf("Global Var: %d\n", global_var);
    printf("Static Var: %d\n", static_var);
    while (1);
}

解释

  1. startup.s 的作用

    • 初始化堆栈指针。
    • .data 段从 Flash 复制到 SRAM。
    • 清零 .bss 段。
    • 调用 main 函数。
  2. 链接文件的作用

    • 定义内存区域(Flash 和 SRAM)。
    • 分配代码段(.text)、只读数据段(.rodata)、可写数据段(.data)和未初始化数据段(.bss)。
    • 提供符号地址(如 _estack__data_start__)供 startup.s 使用。

链接脚本(Linker Script)用于控制程序在内存中的布局,嵌入式开发中常用于指定代码段、堆栈、向量表等放置在哪些具体的内存地址。STM32 或其他 ARM Cortex-M 芯片中,链接脚本非常关键。


📚 常见用于嵌入式的链接脚本格式

你可能使用:

  • GNU 链接器脚本(.ld)--- GCC/CubeIDE/Makefile 项目用这个。
  • Keil scatter file(.sct)--- Keil MDK 项目用这个。
  • IAR linker configuration(.icf)--- IAR 项目用这个。

下面主要讲 GNU .ld 脚本语法(最常见、最可控)👇


✅ 链接脚本基本结构(.ld

ld 复制代码
ENTRY(Reset_Handler)

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 64K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS
{
  /* 向量表和启动代码 */
  .isr_vector :
  {
    KEEP(*(.isr_vector))
  } >FLASH

  /* 程序代码段 */
  .text :
  {
    *(.text)           /* 所有代码 */
    *(.text*)          /* 所有函数 */
    *(.rodata)         /* 常量 */
    *(.rodata*)        /* 字符串常量等 */
    KEEP(*(.init))
    KEEP(*(.fini))
  } >FLASH

  /* 初始化数据,运行时从 FLASH 复制到 RAM */
  .data : AT(__etext)
  {
    __data_start__ = .;
    *(.data)
    *(.data*)
    __data_end__ = .;
  } >RAM

  /* 未初始化变量 */
  .bss :
  {
    __bss_start__ = .;
    *(.bss)
    *(.bss*)
    *(COMMON)
    __bss_end__ = .;
  } >RAM

  /* 堆区 */
  .heap (COPY):
  {
    __heap_start__ = .;
    . = . + 4K;
    __heap_end__ = .;
  } >RAM

  /* 栈区 */
  .stack (COPY):
  {
    __stack_start__ = .;
    . = . + 2K;
    __stack_end__ = .;
  } >RAM

  /* 结束地址 */
  _end = .;
}

🔍 语法要点速查

语法 含义
MEMORY 定义物理内存区域和大小
SECTIONS 定义逻辑段,决定哪些代码/变量放哪
> 表示把段放入哪个 MEMORY 区域
AT(x) 指定链接地址和加载地址不一致(常用于 .data
KEEP() 强制保留被 GCC 可能优化掉的段(如 .isr_vector
. 当前地址指针,可控制偏移和大小
__symbol__ = .; 保存当前地址为符号,用于初始化、拷贝等
*(.text) 收集所有 .text 段的内容(函数体)

💡 例子:配置一个堆栈在 SRAM 顶部

假设 20KB RAM 从 0x20000000 开始,我们将 .stack 放到最顶:

ld 复制代码
MEMORY
{
  RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}

SECTIONS
{
  ...
  .stack (COPY):
  {
    . = ORIGIN(RAM) + LENGTH(RAM) - 1K; /* 栈在最后 1KB */
    __stack_top = .;
    . = . + 1K;
    __stack_bottom = .;
  } >RAM
}

🧩 用途示例

你可以通过链接脚本来:

  • 测试 SRAM 某段是否可用(限制变量位置)
  • 把栈搬到 SRAM 最尾部(避免和测试冲突)
  • 把某段变量放入专用 RAM 区(比如 .dma 用于 DMA)
  • 定义 bootloader + app 的 FLASH 分区

🛠 工具辅助

如果你使用的是 STM32CubeIDEMakefile + GCC,链接脚本通常位于:

复制代码
project/STM32xxxx_FLASH.ld

你可以直接编辑此文件来自定义内存布局。


📌 小结

内容 示例
定义内存区域 MEMORY { FLASH(rx): ORIGIN = 0x08000000, LENGTH = 64K }
放入段 .text : { *(.text) } >FLASH
指定偏移 . = . + 4K;
定义符号 __bss_start__ = .;
强制保留 KEEP(*(.isr_vector))

链接脚本里同名段(比如 .text)理论上可以定义多个,只要它们分别位于不同的地址位置,链接器会把它们当作同一个逻辑段的多个"分片",在内存中连续或不连续放置。


举例说明

ld 复制代码
.text : { /* 第一部分 */
  *(.text_start)
}

. = ALIGN(512);

.isr_vector : { /* 中断向量 */
  KEEP(*(.isr_vector*))
}

. = ALIGN(1024);

.text ALIGN(4) : { /* 第二部分 */
  *(.text_main)
}

. = ALIGN(2048);

.text ALIGN(4) : { /* 第三部分 */
  *(.text_extra)
}

结果

  • 链接器会把所有 .text 段内容合并成逻辑上的一个 .text 段;
  • 物理上这 .text 段被拆成了 3 个片段,分别位于不同地址(由你写的对齐和地址移动决定);
  • 这中间可以插入别的段(比如 .isr_vector),造成不连续内存布局。

使用场景

  • 放启动代码、向量表、主程序代码、额外代码段等,需要特定地址对齐或空间;
  • 对应硬件启动要求或内存映射设计。

注意点

  • 多段 .text 分片可能使调试器显示段不连续,调试体验可能稍差;
  • 需要确认链接器脚本和启动文件匹配,避免地址冲突;
  • 链接器输出的 map 文件能帮你查看段实际地址布局。

.text : {} 第一个空段

-Ttext=<addr> 是链接器(ld)的一个命令行参数,用来 指定程序的 .text 段在内存中的起始加载地址


具体作用:

  • 链接器默认把 .text 段放在链接脚本里定义的默认地址(通常是 Flash 起始地址,比如 0x08000000);
  • 使用 -Ttext=<addr> 可以覆盖链接脚本里 .text 段的默认起始地址 ,让代码从你指定的 <addr> 地址开始放置;
  • 这样可以灵活安排程序代码在内存中的位置,比如给 Bootloader 留空间,把主程序放在偏移地址,或者放到 RAM 运行等。

举例

bash 复制代码
arm-none-eabi-ld ... -Ttext=0x08004000 ...
  • 让程序代码从 Flash 地址 0x08004000 开始加载,而不是从 0x08000000
  • 常见于需要把程序放在 Flash 某个偏移位置的场景。

什么时候用?

  • 有 Bootloader,需要主程序放在 Bootloader后面固定偏移位置;
  • 需要把程序放在某个特殊内存区域;
  • 多程序分区加载和升级设计。

注意

  • -Ttext 只影响 .text 段的起始地址,不影响其他段(如 .data, .bss);
  • 链接脚本可能还需要配合修改,确保其他段地址合理。

简单总结:

作用 说明
指定 .text 段起始地址 灵活调整代码在内存中的加载位置
适应 Bootloader 或分区设计 保证程序代码地址满足硬件或升级需求

能用,但可能会有一些限制和风险,具体看你的链接脚本和硬件需求。


1. 没有第一段空的 .text : {},链接器会怎样?

  • 链接器会把第一个定义的 .text 段内容放在链接脚本里定义的默认起始地址;
  • 如果没有空的占位段,后面实际代码段会紧贴起始地址放置,不会留出预留空间
  • 这可能导致无法插入其他必须放在起始地址的段(比如中断向量表),或者启动代码和程序代码无法分区。

2. 可能的影响

  • 如果中断向量表 .isr_vector 也放在 .text,没空段也没关系,代码会连续放置,程序照常运行;
  • 如果你需要预留空间给 Bootloader 或特殊段,没空段就没法"留地方",升级和分区就麻烦;
  • 如果启动代码和程序代码有地址冲突,程序可能启动失败或异常。

3. 总结

情况 结论
简单项目,无特殊预留需求 可以不用空 .text : {}
有 Bootloader 或特殊布局需求 建议用空 .text 占位,方便布局

简单说:

如果你的链接脚本和启动流程设计不依赖那个空段,没它程序也能跑;但如果你需要分区、升级、或对齐,空段是很有用的"预留地"。


简单示例:

c 复制代码
RAM_START   = 0x20000000;
RAM_END     = 0x20005000;

_estack     = RAM_END;

/* ENTRY(main) */


SECTIONS
{
    /* This is for ability to change link address with `-Ttext=<addr>` ld option */
    .text : {}

    /* Align interrupts vectors table to 512-byte boundary */
    . = ALIGN(512);

    /* C generated vectors sections of name `.isr_vector.__vec_*` */
    INCLUDE vectors.ld

    /* ASM/C generated vectors */
    .isr_vector : { KEEP(*(.isr_vector*)) KEEP(*(.iv))  KEEP(*(.vt)) }

    /* Code and read-only data; can be aligned to (2) */
    .text ALIGN(4) : { *(.text*) *(.rodata*) }

    /* Data alignment is not stricly required */

    /* Save .text end address; .data init values retain here */
    _sidata = ALIGN(4);

    /* Move .data and .bss to ram if . isn't already there */
    . = . < RAM_START ? RAM_START : . ;

    /* Link .data always to RAM */
    .data ALIGN(4) : AT (_sidata) { _sdata = . ; *(.data*) _edata = . ; }

    /* Link .bss always to RAM after the .data */
    .bss ALIGN(4) : { _sbss = . ; *(.bss*) *(COMMON) _ebss = . ; }

    /* Remove sections that are not required */
    /DISCARD/ : { *(.ARM.attributes) *(.comment*) *(.note*) }
}

启动代码

负责把.data.bss 段的数据搬运到 RAM


1. .data.bss 段的区别

段名 含义 初始存储位置 运行时存储位置
.data 初始化的全局/静态变量 FLASH(非易失存储) RAM(可读写)
.bss 未初始化的全局/静态变量 不占FLASH空间(全0) RAM,且初始化为0

2. 为什么 .data 要放 FLASH?

  • .data 变量需要初始化为非零的初始值。
  • 这些初始值必须存储在非易失存储器(通常是 FLASH)中。
  • 程序启动时,把 FLASH 中的 .data 初始值"搬运"到 RAM。

3. .bss 的初始化

  • .bss 是未初始化的变量,标准要求它们启动时被清零
  • .bss 不占 FLASH 空间,直接在 RAM 中分配空间,启动时由启动代码清零。

4. 搬运时机:启动代码(启动汇编/初始化C库)

  • 搬运 .data 和清零 .bss 是由启动代码完成的,通常写在启动汇编文件或 C 库初始化函数中。
  • 这个代码在 Reset_Handler_start 入口处运行,最先执行。

典型伪代码流程:

c 复制代码
// 搬运 .data 段初始化值:从FLASH复制到RAM
for (p = &_ldata; p < &_edata; p++) {
  *p = *p_flash++;
}

// 清零 .bss 段
for (p = &_sbss; p < &_ebss; p++) {
  *p = 0;
}

这里:

  • _ldata 是 FLASH 中 .data 初始值起始地址
  • _edata.data 末尾地址(RAM 中)
  • _sbss_ebss.bss 的 RAM 区间地址

5. 链接脚本的作用

链接脚本会:

  • .data加载地址(LOADADDR)放在 FLASH(含初始值)
  • .data运行地址(VMA,Virtual Memory Address)放在 RAM(变量实际存放处)
  • 定义 _ldata_sdata_edata 等符号,供启动代码使用

6. .bss 在链接脚本中只分配 RAM 空间

它不存初始值,启动时清零即可。


7. 具体示例链接脚本片段(简化版)

ld 复制代码
.data : AT (LOADADDR(.data)) {
  _sdata = .;           /* 运行时起始地址,RAM */
  *(.data)
  _edata = .;
} >RAM AT >FLASH

.bss : {
  _sbss = .;
  *(.bss)
  *(COMMON)
  _ebss = .;
} >RAM

总结

内容 说明
.data 初始值 存储在 FLASH,程序启动时复制到 RAM
.data 运行区 RAM,CPU 读写变量空间
.bss RAM,启动时清零,无FLASH初始值
搬运时机 复位启动代码(Reset_Handler)执行时,C 库初始化部分

bss段

程序代码(比如 .text 段里的启动代码)在编译和链接阶段就已经知道 .bss 段的起始和结束地址了,这是通过链接器脚本定义的符号来实现的。


原理

  • 链接器脚本定义了 .bss 段,并在其中定义符号,如:

    ld 复制代码
    _sbss = .;          /* .bss段起始地址 */
    *(.bss*)
    _ebss = .;          /* .bss段结束地址 */
  • 这些符号 _sbss_ebss 在链接后成为固定地址常量,写入可执行文件的符号表和重定位表里。

  • 编译器/汇编器在编译 .text 里的代码时,会引用这些符号作为外部变量(extern),链接后程序代码中使用的就是具体地址。


结果

启动代码中类似:

c 复制代码
extern uint32_t _sbss;
extern uint32_t _ebss;

void Reset_Handler(void) {
    uint32_t *p = &_sbss;
    while (p < &_ebss) {
        *p++ = 0;
    }
    // 继续执行其他初始化
}
  • _sbss_ebss 不是"运行时查符号表",而是编译链接后就固定成具体的地址值;
  • CPU 执行时用的就是这些实际的 RAM 地址,完成 .bss 的清零。

总结

阶段 符号作用
链接时 符号被解析为具体内存地址
编译时 代码引用符号作为变量或地址
运行时 直接使用地址,清零 .bss

我们可以分两步来看清楚这个过程:


✅ 第一步:链接器脚本定义 .bss 段和它的边界符号

链接器脚本(.ld 文件)中有类似如下定义:

ld 复制代码
.bss (NOLOAD) : {
    _sbss = .;         /* 记录 .bss 起始地址 */
    *(.bss*)
    *(COMMON)
    _ebss = .;         /* 记录 .bss 结束地址 */
} > RAM

链接器的作用:

  • 把所有未初始化的全局/静态变量放进 .bss 段;
  • 设置 _sbss_ebss 这两个符号,代表 .bss 段的起止地址;
  • 在 ELF 文件的符号表中记录这两个符号的位置;
  • 最终,.text 段中引用这两个符号的位置,会被替换成它们的实际地址值

✅ 第二步:启动代码引用这些符号,用于清零 .bss

在启动代码中(通常是 C 或汇编),你会看到类似如下代码:

c 复制代码
extern uint32_t _sbss;
extern uint32_t _ebss;

void Reset_Handler(void) {
    uint32_t *dst = &_sbss;
    while (dst < &_ebss) {
        *dst++ = 0;
    }
}
  • 这段代码使用 _sbss_ebss,就是在清空 .bss 区域;
  • 编译时,这些符号被当成外部变量;
  • 链接时,它们被替换成 .bss 段实际的起止地址。

✅ 本质

你说的"启动代码清 .bss 的代码,和 .bss 段的符号是链接时对应上的"------本质上是:

链接器把 .bss 段确定了地址范围,并生成对应的符号 _sbss_ebss,这些符号的地址被内嵌到 .text 段的启动代码中,所以两者天然就对得上。


✅ 可视化理解(Flash / RAM 布局)

复制代码
FLASH:
0x08000000  ───────────────────
             [ .isr_vector ]
             [ .text ]       ← 启动代码在这里,包含 bss 清零指令
             [ .rodata ]
             [ .data (initial values) ]

RAM:
0x20000000  ───────────────────
             [ .data ]        ← 从 Flash 拷贝来的初始化数据
             [ .bss ]         ← 启动时清零的区域(由 _sbss/_ebss 指定)
             [ heap/stack ]

在链接脚本中,AT 关键字用于显式指定数据段或代码段的 ‌加载内存地址(Load Memory Address, LMA)‌,与其运行时地址(Virtual Memory Address, VMA)分离。具体解析如下:

一、AT 的作用与语法‌

核心功能‌

AT 指令定义数据在 ‌存储介质(如 Flash)中的物理存放位置‌:

LMA‌:程序烧录时数据存储的位置地址(如 Flash)。

VMA‌:程序运行时数据被加载到内存(如 RAM)的地址。

语法格式‌

在 .ld 文件中的典型用法:

ld

Copy Code

.section_name : {

/* 段内容 */

} > VMA_REGION AT> LMA_REGION // 方式1:指定存储器区域

或:

ld

Copy Code

.section_name : AT(LMA_ADDRESS) {

/* 段内容 */

} > VMA_REGION // 方式2:直接指定地址

二、关键应用场景‌

初始化全局变量(.data 段)‌

需将初始值存储在 Flash(LMA),运行时复制到 RAM(VMA):

ld

Copy Code

.data : ALIGN(4) {

_sdata = .; // VMA 起始地址(RAM)
(.data .data. )

_edata = .; // VMA 结束地址(RAM)

} > RAM AT> FLASH // VMA 在 RAM,LMA 在 FLASH

启动代码通过 _sidata = LOADADDR(.data) 获取初始值位置并复制到 RAM。

自定义数据段存储‌

将特定数据(如配置表)固定存储到 Flash 的独立区域:

ld

Copy Code

MEMORY {

FLASH_CONFIG (rx) : ORIGIN = 0x0800C000, LENGTH = 16K

}

.config_data : {

*(.config_section)

} AT> FLASH_CONFIG // LMA 指定到 FLASH_CONFIG

三、AT Vs. 默认行为‌

特性‌ ‌默认行为(无 AT)‌ ‌显式 AT 指令‌

LMA 地址‌ 等于 VMA(可能导致数据未初始化) 独立于 VMA(通常为 Flash)

初始化需求‌ 需手动初始化数据 启动代码自动从 LMA 复制到 VMA

典型用例‌ 纯 RAM 运行 嵌入式系统初始化变量/常量

四、技术原理‌

分离 LMA 与 VMA‌:AT 确保数据在编译阶段被写入正确的存储位置(如 Flash),运行时再加载到目标地址(如 RAM)。

符号关联‌:通过 LOADADDR(.section) 获取 LMA,ADDR(.section) 获取 VMA ,供启动代码使用。

对齐处理‌:常结合 ALIGN(n) 确保地址对齐(如 ALIGN(4) 满足 32 位访问)。

综上,AT 是链接脚本中管理存储与运行地址分离的核心指令,对嵌入式系统的数据初始化和存储器布局至关重要。

相关推荐
星宇CY15 小时前
STM32 定时器应用:从精准延时到智能控制的实战指南
stm32·单片机·嵌入式硬件
学习噢学个屁15 小时前
基于STM32音频频谱分析设计
c语言·stm32·单片机·嵌入式硬件·音视频
is081518 小时前
调试`build.sh` 和用 `CMake` 编译出来的 `.elf` / `.bin` / `.hex` 文件大小或行为不同?
stm32
Peter_Deng.20 小时前
单片机 - STM32 非阻塞式编程详解:以 LED 和按键为例(附超详细寄存器级代码)
stm32·单片机·嵌入式硬件
平凡灵感码头20 小时前
基于 STM32 的四路 PWM 控制智能小车运动的模块化控制程序
stm32·单片机·嵌入式硬件
Camellia03111 天前
嵌入式学习--江协stm32day7
stm32·嵌入式硬件·学习
小灰灰搞电子1 天前
STM32+rt-thread判断是否联网
服务器·网络·stm32
三三十二1 天前
STM32实战: CAN总线数据记录仪设计方案
stm32·单片机·嵌入式硬件
c7_ln1 天前
嵌入式里的时间魔法:RTC 与 BKP 深度拆解
stm32·嵌入式硬件·实时音视频·江协科技