type
slug
status
summary
icon
category
date
tags
password

0 Overview

  • Why Embedded Software System (ESS) ?
  • Running process of ESS
notion image
 
 

1 Boot与Loader

Bootloader: BSP (Board Support Package)

1.1 Boot(启动)

  • 指的是系统从上电或复位开始一步步“引导”到能够运行操作系统或主程序的过程。
  • 启动过程的基本步骤
    • 上电或复位,处理器跳转到固定启动地址(比如 0x00000000)
    • 执行 Bootloader 中的初始化代码
    • 加载主程序(或操作系统)
    • 跳转到主程序入口继续运行

1.2 Loader(加载器)

  • Loader 是更广义的术语,指“负责把程序加载到内存中并开始执行”的代码。
  • 在嵌入式系统中,“Loader”的功能基本包含在 Bootloader 中。比如:
    • 从 Flash 读取程序到 RAM(将代码从加载域拷贝到运行域)
    • 设置堆栈、初始化中断向量表
    • 跳转到主程序

1.3 Bootloader(引导加载程序)

  • Bootloader 是运行在嵌入式系统上电后最早执行的程序,用于初始化硬件、准备运行环境,并将主程序(或操作系统)加载并启动。
  • Bootloader 主要职责
    • 初始化最小硬件(如时钟、内存控制器)
    • 拷贝主程序代码到运行地址
    • 设置堆栈、向量表等
    • 跳转到主程序(main)
    • 有些支持更新固件(比如通过串口/U盘/网络)
 

1.4 BSP(Board Support Package)

  • BSP 是 Bootloader 和操作系统之间的“桥梁”,是“板级支持包”,为特定的硬件平台(开发板或芯片)提供适配代码。
  • 包括:
    • 启动代码(startup.s)
    • 链接脚本(linker script)
    • 驱动代码(如时钟、串口、中断)
    • 中断向量表定义
    • 外设初始化
  • BSP 让操作系统(比如 uC/OS-II、RT-Thread、Linux)能正确运行在你的板子上。
 

2 ARM 启动流程

notion image
  • 重点代码
    • Load the image into the RAM
    • Copy Vectors
    • Initialize the BSS data
    • notion image
我认为的过程是,在初始化了内存之后应该建立映射,也就是将Flash的地址映射到0x00000000,然后在代码拷贝结束的时候再取消映射(所以我感觉上面的load和前面的去取消映射的顺序反了) 从上面的顺序中也能看出来,拷贝代码是在拷贝中断向量表之前执行的

2.1 关闭中断

notion image

2.2 关闭内存管理单元以及缓存

notion image
💡
协处理器指令,p15协处理器是用于管理内存的,所以关闭cache等操作需要使用p15协处理器。 可以总结出:协处理器p15中的c7寄存器是用于存储缓存相关信息的;c1寄存器是用于使能内存相关设备的;c5指令码表示的是关闭指令缓存;c6指令码表示的是关闭数据缓存
 

2.3 初始化协处理器

notion image
  • 这个代码是在非主核上面跑的话,就会跳转到sec子函数中执行。
 

2.4 内存初始化以及取消重映射

  • 内存初始化是硬件和boot完成的。RAM存储器需要在上电时设置刷新频率等,所以需要先初始化RAM。取消重映射我认为是在加载镜像之后,所以这里我也不介绍了
  • 地址mapping的效果(在这里),是让一部分RAM的地址等价于对应某个Flash的地址,从而使得我们最开始pc指向的0x00000000能执行到存储在flash中的boot代码,这个代码内容在这张图:
    • notion image
  • 你可以看到norflash的地址也0x00000000,因为此时地址被remap到norflash的地方(实际地址是这张图:
notion image
实际上,在执行这里拷贝flash部分的代码到ram(也就是流程图的Load the image into RAM)之前,我们就已经取消了重映射了。所以我认为,最开始boot还做了一个操作时将pc指针修改为实际运行的norflash地址(因为我们是norflash启动的,如果是直接烧录到ram里面,那就可以ram内启动,也就不需要搬运代码,所以你看到下面这张图左上方的红色字体代码做了一个判定,是否装载的地址(ADR R1,dummy)(因为boot是从装载处的地址执行的)就是运行时地址(LDR r4,=dummy)(因为这里是绝对地址,我们编译产生代码的地址是按照它运行的时候的地址来得出的,所以也是运行时地址)),然后取消掉重映射,然后就是执行:这张图这部分的boot代码,进行load过程,前面注释说了,左上方红色字体是用于判定是否需要地址搬运。
流程梳理:
  • 处理器复位后,PC通常指向Flash的物理基地址,判定是否需要代码搬运,ADR(反映装载地址),LDR(反映运行时的绝对地址),相等,代码已经在正确位置不需要搬运,不等,则需要将代码从Flash搬运到RAM。取消重映射并执行复制。注意取消重映射前需调整PC,取消重映射后,需显式设置PC指向Flash的实际物理地址,确保后续指令正确执行。
notion image
最后,至于给中断向量表腾出空间这个操作,实际上不是这里做的,是我们设计内存布局的时候,我们设计了Text_start的位置以及中断向量表的位置,所以你的搬运操作只要不覆盖它就行了。
总结描述一下过程(来自彭同学,略加了一点修改): 也就是完整的过程是这样,首先会mapping,将0x40000000的地址映射到0x00000000的位置,从而使的可以从零开始执行启动代码(的一部分memory init这些,以及将pc跳转到norflash对应代码处),执行完后就取消重映射。第二步就是利用那个左上方红色代码检测是否需要搬运,最后在复制向量表之前,将原来的0x40000000位置的代码拷贝到0x06000000(也就是实现搬运到我们程序真正任务应该运行在的text段位置)
 

2.5 加载镜像

notion image
  • dummy: ; 假设 dummy 的位置在段 .text 结尾处,编译时会被放在一个确定的位置 ; 这个标签的存在是为了产生一个“标记地址”,让我们对比运行地址 vs 加载地址
  • 上方红色代码部分(判断运行位置)
指令
含义
LDR r4, =dummy
通过常量池或链接器,取出 dummy 的运行地址(Run Address),这个是你编译时给 dummy: 标签分配的目标地址,比如 0x06000078(放在 RAM)
ADR r1, dummy
获取当前 dummy: 在程序中的实际执行地址(Load Address),这个地址依赖于代码当前是从 Flash 运行,还是从 RAM 运行,比如 0x40000078(NOR Flash 起始 + 偏移)
CMP r1, r4
比较运行地址和加载地址
BXEQ r4
如果二者一致,说明我们已经在 RAM 中运行了,无需复制,直接跳过去
如果运行时 dummy 地址是 0x06000078,说明代码已经被搬运到 RAM;如果是 0x40000078,说明仍然在 Flash 中运行,需要搬运。
为什么不一开始就运行在 RAM?
ARM Cortex-M MCU 上电默认从 Flash 启动。
已经搬运完成,这里有个错误:第二个r4应该是0x06000078(代表代码已经被搬运到 RAM)
notion image
 

2.6 拷贝中断向量表

代码拷贝结束之后代码就完全位于运行域中了,所以这个时候就可以直接使用LDR指令获取地址,不需要使用ADR指令获取地址了
notion image
所以这里获取标号就可以直接使用LDR指令了 同样需要注意的是这里需要判断一下中断向量表是不是已经被存放在0x0的位置了(就相当与是前面dummy的作用) 接下来就是使用后变址去拷贝中断向量表(后变址的指令格式稍微记一下,这里是没有!号的,并且中括号没有包含立即数)
 

2.7 初始化栈空间

notion image
这里实际上就是通过切换模式将当前的sp指针切换为对应模式的sp寄存器,然后再向sp寄存器中写值(预先分配的栈地址)即可
 

2.7 清零bss段

清零bss段主要是因为语言要求所有未初始化的静态变量以及全局变量都应该为0,所以这里需要将bss段清零。
notion image
 
 
 
 
 
 
 
 
 

3 X86 启动流程

notion image
notion image

3.1 RomInit.s GDT实模式和保护模式的切换(不懂但考……)

notion image
LABEL_GDT 定义了GDT中第一个条目的描述符,该条目的Base和Limit都是0,Type是0,表示这个条目并不对应实际的段。 LABEL_DESC_CODE32定义了一个代码段的描述符,该段的Base是0,Limit是SegCode32Len-1,表示段的长度,Type的值是DA_C+DA_32表示这是个可执行的、32位的代码段。 LABEL_DESC_VIDEO定义了一个显存段的描述符,该段的Base是0xB8000,Limit是0xFFFF,表示该段的长度为64KB。Type的值DA_DRW表示该段是可读写的。这个段通常被用来在终端上显示文字和图形。
初始化GDT的描述符部分代码:
notion image
首先,将eax清零(通过xor eax,eax)。然后将cs寄存器的值移入ax内,因为实模式下x86需要段地址左移4位和偏移量相加,所以需要*16得到代码段基地址,然后将LABEL_SEG_CODE32的地址加上ax中的值,即可得到代码段的起始地址,将其存入eax里面。 接着就是根据描述符的结构存入我们的segbase
然后就是设置全局描述符表(GDT)
notion image
同样先清零,然后,和之前cs ax的操作一样,先将ds寄存器的值移入ax中,左移4位加上LABEL_GDT得到GDT的起始地址,然后将它存入GdtPtr字段中的2-6字节(两个字的大小,dword),接着,用lgdt指令将GdtPtr的地址作为操作数,将GDT的信息加载到GDTR寄存器中,完成GDT的初始化。
之所以要保留GdtPtr字段的低16位(2字节)是因为:
lgdt 指令需要一个 48 位的值,其中低 16 位是 GDT 的长度,高 32 位是 GDT 的基地址。例如,lgdt [GdtPtr] 会加载 GDT 指针。
低16位已经通过:
GdtPtr  dw  GdtLen - 1
dd  0
的GdtLen-1赋值了长度
先是有一个Description Tabel 然后执行一段16位格式的代码 随后进行“实模式-》保护模式”的切换,开始执行32位格式的代码
notion image
X86架构GDT组件
notion image
notion image
X86启动流程
 
S3C2440 BootLoader
 
 
 

24年期末试题分析

notion image
1.差分时间链
notion image
notion image
notion image
优势:
  • 采用差分时间等待链后,每当时钟中断产生一个tick,只需对队列头部结点进行“减1”操作,当减到O时,就将其从等待链中取出,后续结点将成为新的头部结点并且被激活。在该过程中,等待链中其他结点的值保持不变,无须遍历对每一个结点进行“减1”操作,这样可减小计算开销。
 
2.任务有什么特征,任务与进程的区别,任务与函数的区别
任务的特征:
2.1.1 什么是任务
任务是一个可执行的软件实体,它负责执行一些特定的操作。它包括一个指令序列、相关的数据以及计算机资源
任务是一个无限循环的函数。它就像一个普通函数一样需要接收参数,但是它永远不会返回。任务的返回值类型永远是void。
  • 函数VS任务:
    • 函数:是静态代码块,由任务调用,不能独立运行,依赖于任务上下文。不参与 OS 的调度。
    • 任务:是动态运行的实体,通过 OSTaskCreate() 等 API 创建,由 OS 管理,具备独立上下文和栈空间。
 
2.1.2 任务的特点
Concurrence(Simultaneousness)(并发性)
  • 由于系统中多个任务并发执行,这些任务宏观上是同时运行的,微观上仍然是串行的。
Independence & Asynchrony (dependence & Synchrony)(异步独立性)
  • 任务间相互独立,不存在前驱和后继的关系(独立)。按照各自的运行速度运行(异步)。
  • 反之,任务间相互依赖,则任务具有同步性。
Dynamic(动态性)
  • 就跟操作系统课上讲的是一样的,就是进程是一个动态实体,而程序是一个静态实体。
  • 程序(Program)是静态实体:它是存储在磁盘或内存中的一组指令或代码,没有执行过程,仅仅是一段静态的数据。进程(Process)或任务(Task)是动态实体:它是程序在执行时的实例,有动态的运行状态,包含了执行中的指令、数据、栈、寄存器、资源等。
    • notion image
任务具有动态性、异步独立性、并发性
 
 
2.1.3 任务、线程、进程
进程(Process)资源分配的基本单位。独立运行的程序实例,具有独立的地址空间、独立资源、独立生命周期,包含多个线程。
线程(Thread)操作系统调度的基本单位。运行在进程的地址空间内,是进程中的可独立调度执行单元,共享进程资源。
任务(Task)RTOS 中的可独立调度的执行单元。在嵌入式 RTOS 中,任务 ≈ 线程,表示 OS 中可独立调度的运行实体。(每个任务/线程具有独立堆栈、上下文和运行状态。任务/线程的切换由内核进行调度,任务/线程的调度策略由操作系统负责管理。任务/线程都支持并发执行,并通过调度机制进行管理。)
 
进程和线程的区别:
只需要把任务当成一个线程就好了。而线程是没有内存隔离的,而进程是有内存隔离的。RTOS只实现了多线程。线程之间是共享地址的,能够访问相同的全局变量。当前线程的地址对于其他线程是可见的,如果修改了当前线程的数据,其他线程是可以知道并且能够访问的。进程之间不能相互访问对方的变量,数据变化不会影响其他进程,一般通过地址保护和虚拟地址来实现。
 
 
3.CPSR寄存器的N、Z、C、V分别的作用是什么,请根据图示汇编代码说说是如何依次提高代码运行效率的。
1.2.2 CPSR各位详解
notion image
Bit 范围
含义
31
N(负号标志)
结果为负
30
Z(零标志)
结果为0
29
C(进位标志)
有进位/借位
28
V(溢出标志)
有溢出
27-8
保留位
7
I(IRQ中断禁止位)
IRQ(普通中断)屏蔽位,1=禁止IRQ中断
6
F(FIQ中断禁止位)
FIQ(快速中断)屏蔽位,1=禁止FIQ中断
5
T(Thumb状态标志)
T=1:Thumb 模式(使用16位指令) T=0:ARM 模式(使用32位指令)
4-0
模式位(M4~M0,表示当前CPU模式)
模式名
值(M4~M0)
描述
User
10000 (0x10)
普通用户模式
FIQ
10001 (0x11)
快速中断模式
IRQ
10010 (0x12)
普通中断模式
Supervisor
10011 (0x13)
管理模式(启动后进入)
Abort
10111 (0x17)
异常模式(数据访问错误)
Undefined
11011 (0x1B)
未定义指令异常
System
11111 (0x1F)
特权用户模式
 
 
 
 
优化汇编代码提高代码运行效率

✅ 二、答题时你需要讲的关键点是:
优化手段
原理
效率提高点
减少内存访问
内存访问比寄存器访问慢
把变量存在寄存器
避免重复计算
循环外提前计算
避免冗余指令
精简指令
用更少指令实现相同功能
减少指令周期
循环展开
减少跳转和条件判断次数
提高流水线效率
使用条件执行
减少跳转(如 CMP+BEQMOVNE
减少分支预测失败
使用乘法移位优化
LSL 替代乘以 2、4、8 的乘法
乘法慢、移位快

✅ 三、举个具体例子(ARM 汇编 形式)
下面是一个经典从“低效到高效”的 ARM 汇编优化例子:

🌱 版本1:低效代码
 
🧨 问题分析:
  • 每次都重复计算 array 的地址 LDR R2, =array
  • 每次都 i * 4 计算地址(效率较低)
  • 循环有分支跳转,可能造成流水线失效

🌿 版本2:优化代码
✅ 优化点:
  1. LDR R2, =array 移出循环 → 避免重复加载地址
  1. [R2, R1, LSL #2]地址计算融合进 LDR 中
  1. 指令数减少,提高流水线效率

🌳 版本3:进一步优化 - 循环展开
✅ 优化点:
  1. 取消循环 → 没有跳转,提升流水线效率
  1. 减少比较和分支,CPU 不需要猜测跳转方向
  1. 非常适合数组长度固定的情况

✅ 四、考试答题模板(建议背下来):
本题通过多次优化汇编代码以提高效率。每一步优化都针对不同的性能瓶颈进行改进,具体如下:
  1. 消除冗余指令:将常量地址或变量移出循环体,避免每轮重复计算,提高执行效率。
  1. 使用寄存器偏移寻址:使用如 [R2, R1, LSL #2] 的形式合并地址计算和访存操作,减少指令数。
  1. 减少跳转指令:通过循环展开等方式,降低分支跳转频率,提高流水线利用率。
  1. 更快的指令替代如乘法用移位(LSL)替代,CMP/BNE 改为条件执行(ADDEQ)。
这些方法综合作用下,显著提升了代码的执行效率,减少了指令周期和访存次数。

寄存器偏移寻址的核心概念:
💡
明白了!你是希望从零基础学清楚「使用寄存器偏移寻址」「内存访问」这些ARM 汇编的基本概念,那我们就一点点来讲清楚,用图解 + 例子 + 中文解释,务必让你明白这个知识点。直接使用寄存器号,即使用这个寄存器的内容。

🧱 一、你必须先理解的基础:寄存器与内存的区别
概念
举例
访问速度
说明
寄存器
R0, R1, R2...
🚀 超快
CPU 内部的小仓库,读写快
内存(RAM)
array, 变量x 地址
🐢 慢很多
数据存在内存中,需要地址访问
👉 所以:我们常用寄存器存临时变量,提高速度,只在必要时访问内存!

🧠 二、LDR / STR:最基本的内存访问指令
  • LDR R1, [R2]:把内存地址 R2 指向的内容读进寄存器 R1
  • STR R1, [R2]:把寄存器 R1 的内容存入内存地址 R2 指向的位置
举例图解(假设 array 是数组):
✅ 理解:
  • [R2] 是访问 R2 指向的内存内容
  • [R2, #4] 是访问 R2 + 4 的内存内容(偏移量)

🧠 三、寄存器偏移寻址:核心概念
我们经常访问一个数组的第 i 项:array[i],等于 array + i * 4(假设是 4 字节整型)。
🚀 用汇编表达这个过程:
✅ 中文解释:
  • R2 是基地址
  • R1 是索引 i
  • LSL #2 表示左移两位,相当于乘以 4
  • [R2, R1, LSL #2] 表示:R2 + R1*4 —— 这是访问数组中第 i 项!

🧠 四、为什么使用这种写法更高效?
写法
解释
效率
ADD R3, R2, R1, LSL #2LDR R0, [R3]
先算地址,再访问
2 条指令 ❌
LDR R0, [R2, R1, LSL #2]
一条指令直接访问地址
1 条指令 ✅
所以:寄存器偏移寻址可以少一条指令,提升运行速度!

📌 总结一句话背诵:
在 ARM 汇编中,使用 [基址寄存器, 索引寄存器, 移位] 的方式访问内存叫做“寄存器偏移寻址”。它可以在一条指令内完成地址计算和访问,比拆成两条指令效率高,是常用的优化手段!

🧪 小练习题(带答案)
Q:若数组 array 从地址 0x20000000 开始,如何用汇编访问第 i 项?

🧩 如果考试问这个知识点,你可以这样答:
ARM 汇编中通过使用 [R2, R1, LSL #2] 等“寄存器偏移寻址”方式,可以在一条指令中完成地址计算和内存访问,从而减少指令数量、提高效率。同时避免了使用额外 ADD 等指令来计算数组偏移,提高流水线执行速度,因此是一种常用的代码优化方式。

 
 
 
7.给了一个点灯一个灭灯的Task(就是上课PPT的那两个Task),让你画OSTimeDly(10)函数的流程图
  • 一个简单的点灯任务(基于MSP430 P90)
notion image
💡
这里把延时设计为OSTimeDly()与之前裸机程序使用的延时函数有什么区别?
  • OSTimeDly()在执行时,CPU资源从一个任务转交给其它任务执行
  • 裸机的延时函数在执行时,CPU资源全部分配给延时函数,无法执行其它函数。
notion image
  • 引入两个任务之后,操作系统的调度逻辑
notion image
💡
  • IDLE任务在系统空闲时运行,执行节能等操作。
  • ISR_WDT作为系统的时钟节拍来源,每个Tick到来时都执行,以确保系统的定时功能和任务调度的准确性。在 ISR_WDT 中,会触发 uC/OS-II 的时间管理机制,包括任务延时、就绪判断等。使得调度器正常运行。
  • CPU使用情况:
    • 当没有其他任务处于就绪状态时,CPU执行的是 IDLE 任务。这个任务是 uC/OS-II 系统启动时自动创建的最低优先级任务,用于“打发”CPU时间,防止系统停滞。IDLE 任务一般是个死循环,会执行一些低功耗操作,比如 __WFI()(等待中断)进入低功耗模式。
    • CPU 正在执行 IDLE 任务,此时 WDT 中断触发。CPU 暂停 IDLE 任务,进入中断服务程序 ISR_WDT()在中断服务程序中会调用 OSTimeTick(),这是 uC/OS-II 的时间管理核心函数,用于:
      • 更新任务延时计数器(比如 OSTimeDly() 中的任务)。
      • 检查是否有任务从挂起变为就绪(如延时结束、超时等待等)。
    • ISR_WDT 执行完毕后,中断退出前会进行一次调度 OSIntExit(),判断是否需要切换任务。若仍然没有其他更高优先级任务就绪,则继续返回执行 IDLE 任务。 若有任务变为就绪(例如某个任务延时结束),则调度器将中断退出后切换到该就绪任务
  • 任务就绪队列使用情况
    • 当系统空闲时:就绪表中仅有 IDLE 任务(最低优先级的任务)
    • Tick 中断期间:
      • OSTimeTick()遍历任务控制块(TCB),更新计时器
      • 有任务满足条件时,会被加入到就绪队列;
      • 否则,仍然只有 IDLE 任务保持就绪状态。
      notion image
  • 从图中看,IDLE 任务似乎是连续的,但这并不意味着 ISR_WDT 不占用时间,而是因为ISR_WDT 占据时间很短,但确实会打断 IDLE 执行。(相较于 IDLE 运行时间可以忽略不计,所以图上显示成“几乎连续”。
    • 举个具体例子:系统时钟 72MHz、OSTimeTick() 在 ISR 中执行完大概需要3微秒、Tick周期为10毫秒。所以约3μs执行ISR_WDT,剩下的9.997ms都在执行IDLE。
下面是OSTimeDly()的具体代码实现:
2.6.2.1 OSTickISR的核心代码
ARM Mini2440的0号中断对应的是时钟中断,如果产生了时钟中断,就需要相应的中断服务程序来进行处理。μC/OSI在ARMMini2440上的中断服务程序需要完成的工作由OSTickISR定义。
notion image
 
 
ERTOS 采用了什么措施来保证它的实时性与确定性?
  • 实时性(Real-Time):系统对外部事件能在规定时间内作出响应(强调“及时”)。
  • 确定性(Determinism):系统对相同事件总是产生相同响应时间(强调“可预测”)。
1. 抢占式调度机制
  • 高优先级任务可以立即打断低优先级任务执行。
  • 确保紧急任务在最短时间内获得 CPU。
✅ 作用:保证响应事件的 及时性(实时性)。

2. 优先级确定性(固定优先级)
  • 每个任务有一个确定的优先级。
  • 调度器总是选择就绪任务中 优先级最高的执行。
✅ 作用:调度行为可预测,提高确定性。

3. 时间管理机制(如 OSTimeDly)
  • 提供任务延时机制,支持 周期性任务
  • 结合系统 Tick 定时器,可实现精确定时。
✅ 作用:保证时间事件处理的一致性和精度

4. 中断快速处理与服务划分
  • 中断处理程序尽量快速完成,将复杂操作留给任务完成。
  • 通常采用“两段式”中断处理:
    • 上半部(ISR):快速响应硬件中断
    • 下半部(任务):由 RTOS 管理,处理具体逻辑
✅ 作用:中断响应快,系统恢复快,提高整体响应速度。

5. 内核关键资源保护机制
  • 如使用临界区管理信号量、互斥量等。
  • 防止资源冲突,提高并发的确定性。
✅ 作用:避免并发访问出错,保证多任务并行的可控性

6. 可预测的任务切换时间
  • RTOS 内核设计精简,任务切换时间(上下文切换)固定可控。
  • 调度器耗时小,可估算。
✅ 作用:系统行为可计算、可分析,适合实时应用。

✅ 可选一句总结(考试写在最后加分):
总的来说,RTOS 通过抢占式调度、优先级管理、中断快速处理、时间精确控制等机制,确保了系统在处理外部事件时的响应是及时的(实时性),而处理流程是可预测的(确定性)。
 
Chapter3:Hardware System第一章:数据库系统概论
Loading...
🐟🐟
🐟🐟
在坚冰还盖着北海的时候,我看到了怒放的梅花
最新发布
Chapter4:Software System
2025-5-21
Chapter3:Hardware System
2025-5-21
Chapter2:Kernel of ERTOS
2025-5-21
Chapter1:Introduction
2025-5-21
Hi3861 & 服创 & 计设
2025-5-17
1-1-1 用Keil点亮一盏小灯
2025-5-16
公告
🎉NotionNext 3.15已上线🎉
-- 感谢您的支持 ---
👏欢迎更新体验👏