单片机内存布局管理:sct分散加载详解

目录

[1 什么是分散加载文件?](#1 什么是分散加载文件?)

[sct 分散加载文件: 内存布局管理](#sct 分散加载文件: 内存布局管理)

[2 分散加载文件在什么时候起作用?](#2 分散加载文件在什么时候起作用?)

[3 创建自己的sct分散加载文件](#3 创建自己的sct分散加载文件)

[4 分散加载文件的格式解析(个人理解)](#4 分散加载文件的格式解析(个人理解))

个人理解总结

需要注意的点

[4 分散加载文件的格式解析(deepseek)](#4 分散加载文件的格式解析(deepseek))

[1.核心结构解析:3 个关键区块](#1.核心结构解析:3 个关键区块)

(1)加载域:LR_IROM1

[(2)执行域 1:ER_IROM1(代码区)](#(2)执行域 1:ER_IROM1(代码区))

[(3)执行域 2:RW_IRAM1(数据区)](#(3)执行域 2:RW_IRAM1(数据区))

2.关键规则与作用

3.分散加载文件解析

一、整体结构概述

二、各区域详细解析

第一个加载域:LR_IROM1(主程序区)

[RAM 执行域:RW_IRAM1(数据区)](#RAM 执行域:RW_IRAM1(数据区))

[第二个加载域:LR_IROM2(自定义 Flash 区)](#第二个加载域:LR_IROM2(自定义 Flash 区))

三、关键变化与用途

四、适用场景

示例解析:

[5 C语言__attribute__关键字](#5 C语言__attribute__关键字)

[6 一些可能存在的疑问与解答](#6 一些可能存在的疑问与解答)

[1. gcc+makefile编译时,对应.sct文件的链接文件是什么类型?](#1. gcc+makefile编译时,对应.sct文件的链接文件是什么类型?)

[2. 有时候代码在Sram中运行比在Flash中运行耗时是为什么?](#2. 有时候代码在Sram中运行比在Flash中运行耗时是为什么?)

[Flash 加速读取指令机制:](#Flash 加速读取指令机制:)

[3. 基于芯片内部指令集流水线、RTOS操作系统特征,如何进行局部调试和优化(关于指令集流水线)](#3. 基于芯片内部指令集流水线、RTOS操作系统特征,如何进行局部调试和优化(关于指令集流水线))

[4. cache命中率的影响因素](#4. cache命中率的影响因素)

[5. SCT文件会把整个调度系统(函数+全局变量)都放到RAM里吗?](#5. SCT文件会把整个调度系统(函数+全局变量)都放到RAM里吗?)

[6. SCT是如何控制代码和数据放置的?](#6. SCT是如何控制代码和数据放置的?)


1 什么是分散加载文件?

分散加载文件通常以.sct结尾,英文名是:Linker Control File,scatter loading,链接器根据这个文件的配置来分配各个节区的地址空间,并且生成分散加载代码,因此我们只要修改分散加载文件,链接器就能自动帮我们确定代码、变量等这些内容在内存中(Flash和RAM上)的地址。

sct 分散加载文件: 内存布局管理

  • 核心问题:编译好的代码和数据具体放在芯片内存的哪个地址?比如:

    • 中断向量表应该放在哪里?

    • 程序代码(Flash)和数据(RAM)如何分布?

    • 如何将某个函数或变量放到一个特定的高速内存区?

    • 如何配置堆栈的空间?

  • sct 文件:是 ARM 编译器(ARMCC、ARMClang)使用的链接脚本,全称是 Scatter-Loading Description File。它精确地指导链接器,将程序的各个段(如 .text, .data, .bss)放置到指定的物理地址上。

  • 范畴:链接阶段、内存映射、存储管理。这是嵌入式开发(尤其是资源受限、没有MMU的MCU)特有的、至关重要的知识。

2 分散加载文件在什么时候起作用?

它主要是在链接阶段产生作用,如果你之前用过gcc+makefile编译mcu项目,那可以看到在目录中会有.ld结尾

的文件,这个文件和我们这里的分散加载文件作用类似,都是在链接阶段起作用。

编译链接流程:

3 创建自己的sct分散加载文件

在原有的sct的目录下新建一个文件夹放我们自己的sct文件,点击Edit就可以开始编辑sct文件了。

可以这样找到sct文件

4 分散加载文件的格式解析(个人理解)

在默认的 sct 文件配置中仅分配了 Code、RO-data、RW-data 及 ZI-data 这些大区域的地址,链接时各个节区(函数、变量等)直接根据属性排列到具体的地址空间。

sct 文件中主要包含描述加载域及执行域的部分,一个文件中可包含有多个加载域,而一个加载域可由多个部分的执行域组成。同等级的域之间使用花括号"{}"分隔开,最外层的是加载域,第二层"{}"内的是执行域。

个人理解总结

  • 加载域会将大括号内的东西全部加载到指定地址上,但是具体要从那里读取代码读取数据执行,要看执行域的地址。

  • 如果加载域在flash,执行域在SRAM上,就会在上电时自动从flash上copy数据到SRAM上,之后要读取数据/指令就会在SRAM上进行。

  • 相当于加载域上有所有东西,如果执行域和加载域地址一样就不用copy数据,如果不一样就搬运数据到执行域地址,等真要用的时候到执行域上面拿。

需要注意的点

  • 执行地址在Flash区域的代码,其加载地址和执行地址必须对齐,不然会导致访问时出错。

  • 简单来说,假设一个加载域的地址是0x0800 0000,而这个加载域中有两个执行域,一个在flash,地址为0x0800 0000,叫执行域F;另一个在RAM上,地址为0x2000 0000,叫执行域R;此时flash的执行域应该写在前面,不然就会导致访问时出错。

  • 出错原理:如果执行域R写在加载域大括号中的第一个位置,执行域R的数据会放入加载域的地址上,放完了执行域R的数据之后才会放执行域F的数据,问题是,我们一开始给执行域F规定的地址是从0x0800 0000开始,但此时0x0800 0000的位置上已经放了执行域R的数据,假设执行域R的数据大小为0x74,则执行域F的真正数据加载地址就是0x0800 0074,此时加载地址与执行地址就形成了"错位",而程序在执行时,还是会从0x0800 0000中去取执行域F的第一条代码(此时执行域F的第一条代码真正加载位置在0x0800 0074),就会导致程序执行出错。

4 分散加载文件的格式解析(deepseek)

这个 sct 文件是 ARM Cortex-M 系列芯片的分散加载描述文件,用于定义程序在编译后,代码和数据在 Flash(ROM)与 RAM 中的存储地址和分配规则。

复制代码
加载域名 起始地址 大小{    ;加载区域大小(分号后面是注释)
    运行域名 起始地址 大小 ;执行地址
    {
        中断向量表起始地址,+First表示强制放到首地址
        
        ARM相关库,InRoot$$Sections即ARM库的链接器标号,主要作用是进行重定位COPY RW到RAM时,
        提供RW在flash上的起始地址,
        然后在RW区后面创建ZI区域。库函数__main函数中有这个段。它是 __main()的一部分。
        
        编译文件RO只读在该区域
    }
    运行内存名字 起始地址 大小
    {
        编译可读可写,静态区
    }
}

1.核心结构解析:3 个关键区块

整个文件由「加载域(Load Region)」和其包含的「执行域(Execution Region)」组成,加载域对应物理存储(如 Flash),执行域对应实际运行时的存储(如 Flash 或 RAM)。

(1)加载域:LR_IROM1

这是整个程序的总加载区域,所有代码最终会被烧录到这个区域。

  • 0x08000000:加载起始地址,是 STM32 等主流 Cortex-M 芯片的Flash 默认起始地址

  • 0x00080000:加载区域大小,即 512KB(16 进制0x80000 = 十进制 524288 字节 = 512KB),代表 Flash 的可用空间。

  • 作用:规定了程序 "烧录到哪里" 以及 "最多能烧录多大"。

(2)执行域 1:ER_IROM1(代码区)

这是只读 代码和数据的执行区域,与加载域地址相同,说明代码直接在 Flash 中运行(无需复制到 RAM)。

  • 0x08000000:执行起始地址,与加载地址一致,对应 Flash 地址。

  • 0x00080000:执行区域大小,与加载区域大小一致,限制 Flash 中可运行的代码总量。

  • 内部包含的内容(按优先级顺序):

    • *.o (RESET, +First):所有目标文件的「复位向量表(RESET)」,且强制放在该区域的最开头(芯片上电后最先读取复位向量)。

    • *(InRoot$$Sections):链接器内部使用的关键段,确保 C 库初始化等核心代码被正确包含。

    • .ANY (+RO):所有目标文件的「只读数据段(RO)」,包括代码指令、const 修饰的常量。

    • .ANY (+XO):所有目标文件的「可执行只读数据段(XO)」,通常是编译器优化后的只读代码或数据。

(3)执行域 2:RW_IRAM1(数据区)

这是可读写数据的执行区域,对应芯片的 RAM,程序运行时的变量会存放在这里。

  • 0x20000000:执行起始地址,是 STM32 等 Cortex-M 芯片的RAM 默认起始地址

  • 0x00020000:执行区域大小,即 128KB(16 进制0x20000 = 十进制 131072 字节 = 128KB),代表 RAM 的可用空间。

  • 内部包含的内容:

    • .ANY (+RW +ZI):所有目标文件的「可读写数据段(RW)」和「零初始化数据段(ZI)」。

      • RW:已初始化的全局变量 / 静态变量(如int a = 10;),上电后会从 Flash 复制到 RAM。

      • ZI:未初始化的全局变量 / 静态变量(如int b;),上电后会被自动清零。


2.关键规则与作用

  1. 地址映射规则:明确 Flash(0x08000000 开始)存代码和只读数据,RAM(0x20000000 开始)存可读写变量,避免存储冲突。

  2. 大小限制规则:Flash 最大用 512KB,RAM 最大用 128KB,超过会导致编译报错,防止程序超出芯片硬件资源。

  3. 优先级规则:复位向量表强制放 Flash 最开头,确保芯片上电后能正确找到启动入口,这是 Cortex-M 芯片启动的硬性要求。


3.分散加载文件解析

复制代码
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

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

LR_IROM2 0x08070000 0x00010000  {    ; load region size_region
  ER_IROM2 0x08070000 0x00010000  {  ; load address = execution address
   *.o (myflash)
  }
}

这个 sct 文件是在之前基础上扩展的分散加载描述文件,新增了一个独立的加载域和执行域,用于管理自定义数据段(myflashmyram)。下面详细解析其结构和作用:

一、整体结构概述

文件包含两个加载域(LR_IROM1、LR_IROM2) ,每个加载域下各包含一个执行域(ER_IROM1、ER_IROM2),同时保留了原有的 RAM 执行域(RW_IRAM1)。核心变化是新增了LR_IROM2加载域和myflashmyram自定义段,用于将特定数据分配到指定的 Flash 或 RAM 区域。

二、各区域详细解析

第一个加载域:LR_IROM1(主程序区)
  • 加载地址0x08000000(Flash 起始地址,同前)

  • 大小0x00070000(448KB,相比之前的 512KB 减少了 64KB,预留空间给新增的 LR_IROM2)

  • 作用:存储程序主体(代码、复位向量、只读数据等)。

  • 其包含的执行域ER_IROM1

    • 地址和大小与 LR_IROM1 一致(0x080000000x00070000),说明代码直接在 Flash 中运行。

    • 内容与之前基本相同:

      • 复位向量表(RESET)、链接器核心段(InRoot$$Sections)、只读代码 / 数据(+RO+XO)。
RAM 执行域:RW_IRAM1(数据区)
  • 地址0x20000000(RAM 起始地址,同前)

  • 大小0x00020000(128KB,未变)

  • 新增内容:*.o (myram)

    • 含义:所有目标文件中标记为myram的段,会被强制分配到这个 RAM 区域。

    • 用途:可通过代码中的__attribute__((section("myram")))修饰变量,将其指定存放在此 RAM 区域(例如:int buf[1024] __attribute__((section("myram")));)。

第二个加载域:LR_IROM2(自定义 Flash 区)
  • 加载地址0x08070000(Flash 偏移地址,位于 LR_IROM1 之后:0x08000000 + 0x00070000 = 0x08070000

  • 大小0x00010000(64KB,正好是 LR_IROM1 减少的空间,避免地址重叠)

  • 作用:专门用于存储标记为myflash的自定义数据(非程序主体,如校准参数、配置表等)。

  • 其包含的执行域ER_IROM2

    • 地址和大小与 LR_IROM2 一致(0x080700000x00010000),数据直接存储在该 Flash 区域,运行时无需复制到 RAM。

    • 内容:*.o (myflash)

      • 含义:所有目标文件中标记为myflash的段,会被强制分配到这个 Flash 区域。

      • 用途:通过__attribute__((section("myflash")))修饰常量,将其固定存放在此区域(例如:const char config_data[] __attribute__((section("myflash"))) = "calib_param";)。

三、关键变化与用途

  1. Flash 空间拆分:将原 512KB Flash 拆分为两部分:

    1. 主程序区(448KB,0x08000000~0x0806FFFF):存代码和默认只读数据。

    2. 自定义区(64KB,0x08070000~0x0807FFFF):存专用数据(如固件参数、日志模板等),避免与主程序混杂。

  2. 自定义段管理

    1. myflash段:强制存放在指定 Flash 区域,适合存储需要持久化、不随程序更新的只读数据(如硬件校准值)。

    2. myram段:强制存放在指定 RAM 区域,适合管理特殊用途的变量(如高频访问的缓存、DMA 缓冲区等)。

  3. 地址冲突避免 :两个加载域(LR_IROM1 和 LR_IROM2)的地址范围连续且不重叠(0x08000000~0x0806FFFF0x08070000~0x0807FFFF),确保编译和运行时无地址冲突。

四、适用场景

此配置适合需要在 Flash 中划分专用区域存储自定义数据的场景,例如:

  • 嵌入式设备的出厂参数(需固定地址,方便读取);

  • 程序中需要独立管理的大段只读数据(如字库、图片资源);

  • 对 RAM 有特殊分区要求的场景(如隔离普通变量和 DMA 缓冲区)。

如果需要进一步指定myflashmyram的地址范围(而非默认占满分配的空间),可以在段定义中细化,例如:*.o (myflash) 0x08070000 0x00008000(限制myflash仅用前 32KB)。

示例解析:
cpp 复制代码
__attribute__ ((used, section ("myflash"))) 
void flash_function_pre()
{
  int i = 0;
  i = i / 3;
  return;
}

void flash_function()
{
  int i = 0;
  i = i / 3;
  return;
}

__attribute__ ((used, section ("myram"))) 
void sram_function(void)
{
  int i = 0;
  i = i / 3;
  return;
}

__attribute__ ((used, section ("myram"))) 
void sram_function_pre(void)
{
  int i = 0;
  i = i / 3;
  return;
}

uint32_t call_flash_func_time_record(void)
{
  uint32_t start_tick = HAL_GetTick();
  uint32_t end_tick = 0;
  uint32_t i_counter = 10000000;
  void (* volatile fpointer_1)(void) = flash_function;
  void (* volatile fpointer_2)(void) = flash_function_pre;
  
  while( i_counter > 0 )
  {
    fpointer_1();
    fpointer_2();
    i_counter --;
  }
  
  end_tick = HAL_GetTick();
  
  return (end_tick - start_tick);
}

__attribute__ ((used, section ("myram"))) 
uint32_t call_sram_func_time_record(void)
{
  uint32_t start_tick = HAL_GetTick();
  uint32_t end_tick = 0;
  uint32_t i_counter = 10000000;
  void (* volatile fpointer_1)(void) = sram_function;
  void (* volatile fpointer_2)(void) = sram_function_pre;
  
  while( i_counter > 0 )
  {
    fpointer_1();
    fpointer_2();
    i_counter --;
  }
  
  end_tick = HAL_GetTick();
  
  return (end_tick - start_tick);
}

通过attribute((section("xxx")))将 4 个函数分别指定到sct文件定义的自定义段中:

  • flash_function_pre():被section("myflash")修饰,存放在LR_IROM2对应的 Flash 区域(0x08070000~0x0807FFFF)。

  • flash_function():未修饰,默认存放在主 Flash 区域(ER_IROM10x08000000~0x0806FFFF)。

  • sram_function()sram_function_pre():被section("myram")修饰,存放在RW_IRAM1对应的 RAM 区域(0x20000000~0x2001FFFF)。

  • 附加 used 属性:确保编译器不会因 "未被直接调用" 而优化删除这些函数(因为通过 函数指针 间接调用)。

简单说,used属性的核心作用是强制编译器 "保留" 某个函数 / 变量,哪怕代码里没有直接调用它,也不会把它当成 "无用代码" 删掉 ------ 这正好适配你代码里 "用函数指针间接调用" 的场景。

1.先搞懂:编译器为什么会 "删代码"?

编译器在编译时会做 "死代码消除(Dead Code Elimination)" 优化。它会扫描代码,判断哪些函数 / 变量 "没用":

  • 对函数来说,"没用" 的标准是没有任何地方直接调用它 (比如没写过flash_function_pre();这种直接调用)。

  • 编译器默认不知道 "函数指针会间接调用它",会误判这类函数是 "死代码",最终不把它编译到最终的可执行文件(.bin/.hex)里。

举个例子:如果flash_function_pre()没加used属性,编译器看到代码里只有fpointer_2 = flash_function_pre;(赋值给指针),没有flash_function_pre();(直接调用),就会认为这个函数没用,把它从编译结果里删掉。

2.再看: used 属性是怎么 "保住" 代码的?

attribute((used))是 GCC 编译器的一个特殊属性,它给编译器发了一个明确的指令:

  • "这个函数 / 变量是有用的,哪怕你没看到直接调用,也必须把它保留在最终的代码里,不能删!"

  • 这样一来,被used修饰的flash_function_pre()sram_function_pre(),即使只被赋值给函数指针,也能正常编译到指定的myflashmyram段里,后续用指针调用时才不会出现 "找不到函数" 的错误(比如硬件异常、程序跑飞)。

3.结合代码更直观

代码里,flash_function_pre()sram_function_pre()的调用逻辑是 "间接的":

  1. 先把函数地址赋值给指针:fpointer_2 = flash_function_pre;

  2. 再通过指针调用:fpointer_2();

如果没加used,编译器在第一步时,会因为没看到flash_function_pre()的直接调用,先把它删掉;到第二步用指针调用时,指针指向的地址是 "空的" 或者 "无效的",程序就会出错。而加了used之后,函数被保留,指针能正确指向函数地址,调用才会正常执行。

简单总结:used属性就是给编译器 "开特例",专门应对 "间接调用" 这种编译器默认识别不了的场景,确保关键代码不被误删。

5 C语言__attribute__关键字

参考文章:【外部】C语言__attribute__的使用_c attribute-CSDN博客

在C语言中,attribute 是GNU C编译器提供的一种特性,用于向编译器提供函数、变量和类型的额外信息。这些信息可以帮助编译器进行优化、错误检查或控制对齐方式等。attribute可以设置函数属性、变量属性和类型属性。

通过 attribute,我们可以将函数或变量放入指定的内存区域中。

6 一些可能存在的疑问与解答

1. gcc+makefile编译时,对应.sct文件的链接文件是什么类型?

在使用gcc和 makefile进行编译时,.sct文件通常指的是用于链接阶段的链接脚本 文件。在ARM编译器中,.sct 文件通常被用作链接器脚本(scatter file),而在gcc环境中,其对应的文件类型为 链接脚本,文件扩展名通常为.ld(Linker Script)

2. 有时候代码在Sram中运行比在Flash中运行耗时是为什么?

从Flash运行代码时,取 指令 由I-code总线完成,I-code在读取Flash时不仅有预取指加速,通常还有预取指令和缓存机制,延迟小,吞吐高。

所谓缓存机制,在于stm32f4中,增加了一个缓冲区,可以将flash中的指令或者数据提前取到缓冲区中,cpu访问缓冲区的速度比访问ram的速度更快。但是如果缓冲区中的指令是随机的,比如我们在FreeRTOS中有很多的判断,这些判断是随机的,那么cache就要重新从flash里面缓存指令,这时候从flash取指令就要等待,那么就比Sram要慢。这里说的cache就是下文的指令缓存存储器。

而从 SRAM 运行代码时,走的是System-Bus(S-Bus),和其他外设共享宽带,效率会低一些。

  • 在这个图中,I-Code总线走的是专用的AHB instruction bus(紫线),虽然通过Flash interface,但它是Cortex-M 内部的 专用通道,只用于Fetch指令,I-Code路径有更高优先级,对时序优化得更好。SRAM访问是通过S-Bus+AHB system bus(蓝线),S-Bus是通用型数据访问路径,与外设、DMA、外部SRAM等共用资源,总线冲突和竞争导致性能下降。

  • 另外,Flash interface 本身有预取缓存(prefetch buffer)和ART(自适应实时加速器)即使最终都走AHB,它可以将指令预先加载进内部buffer,隐藏Flash的访问延迟。

Flash 加速读取指令机制:

以下来源:《STM32F4xx参考手册》

下面是上图的指令缓存存储器的英文原文:

3. 基于芯片内部指令集流水线、RTOS操作系统特征,如何进行局部调试和优化(关于指令集流水线)

三级流水线一般指 取值 译码 执行。

  • 关于如何优化cache,比如,状态机中最常用的一种方法:Switch Cace法,适合状态机不是很复杂时使用。因为switch-case 的原理是从上到下挨个比较,越靠后,查找耗费的时间就越长,所以要注意状态和事件在各自的 switch 语句中的安排顺序,出现频率高或者实时性要求高的状态和事件的位置应该尽量靠前。

  • 这里的详细解释是,CPU的流水线会提前加载Switch Case中的前的第一个Case(没错,它就是猜这里的第一个Case执行的概率最大),如果猜错了,它就会扔掉所有的已经加入流水线的指令,造成效率降低。猜错了,它就要重新去Flash里面去取指令,但是Flash重新取指令是需要等待时间的,而SRAM是不需要的。

4. cache命中率的影响因素

空间层面:

  • 空间局部性是指如果一个存储单元被访问,那么它附近的存储单元很可能也会被访问。在Cache中,这种特性体现为当CPU读取一个数据时,Cache会把包含这个数据的一整个Cache行(包含相邻数据)读取进来。

时间层面:

  • 时间局部性是指如果一个存储单元被访问,那么它很可能在不久的将来会被再次访问。在Cache中,频繁访问的数据会一直保留在Cache中(只要Cache替换策略没有把它替换掉),从而提高命中率。顺序访问与随机访问:顺序访问数据结构(如数组)通常比随机访问数据结构(如链表)具有更高的Cache命中。

5. SCT文件会把整个调度系统(函数+全局变量)都放到RAM里吗?

不会。SCT是链接器控制内存布局的配置文件,它定义的是哪些代码和数据放在哪块内存区域。一般调度器的函数代码(比如任务切换函数)会被放在Flash中执行,而全局变量(如任务控制块、调度标志等)则会被放在RAM 中供运行时读写,这样来加快单片机的运行速度。

6. SCT是如何控制代码和数据放置的?

SCT文件通过定义Load Region(加载区)和Execution Region(执行区)来控制段的布局。比如 .text 段(代码)通常放在Flash, . data(已初始化变量)和 .bss(未初始化变量)放在RAM。链接器根据这些规则将目标文件中相应的段布局到指定地址。

相关推荐
小尧嵌入式1 小时前
基于HAL库实现ETH以太网
网络·arm开发·stm32·单片机·嵌入式硬件
飞凌嵌入式3 小时前
飞凌嵌入式RK3568开发板的TFTP烧写文件系统指南
linux·嵌入式硬件·嵌入式
lingzhilab10 小时前
零知IDE——基于STM32F103RBT6与RFID-RC522的校园餐卡系统实现
stm32·单片机·嵌入式硬件
promising-w10 小时前
【stm32入门教程】GPIO输入之按键控制LED&光敏传感器控制蜂鸣器
stm32·单片机·嵌入式硬件
必胜的思想钢印10 小时前
修改主频&睡眠模式&停机模式&待机模式
笔记·stm32·单片机·嵌入式硬件·学习
文亭湖畔程序猿13 小时前
开天斧 STC8H8K64U低功耗demo
单片机·嵌入式硬件
SundayBear14 小时前
C语言复杂类型声明完全解析:从右左原则到工程实践
c语言·开发语言·数据结构·嵌入式
ACP广源盛1392462567316 小时前
GSV5100A@ACP#2 进 2 出 HDMI2.0 中继器 / CAT 延长器,带有音频提取和插入功能
单片机·嵌入式硬件·音视频
d111111111d16 小时前
W25Q60简介--SPI通信(笔记)
笔记·stm32·单片机·嵌入式硬件·学习