将 WSL2 从 C 盘迁移到其他硬盘

环境: Windows 11 + WSL2 + Ubuntu 22.04 目的: 将占用 C 盘 211 GB 的 WSL 虚拟磁盘迁移到其他硬盘,彻底释放 C 盘空间


背景

WSL2 默认将虚拟磁盘文件(ext4.vhdx)存储在 C 盘,路径为:

C:\Users\<用户名>\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu22.04LTS_<hash>\LocalState\ext4.vhdx

随着在 WSL 内安装工具、存放项目文件,这个文件会不断膨胀。本文记录了将一个占用 211 GB 的 Ubuntu 22.04 实例完整迁移到 F 盘的全过程。


前置准备

  • 目标硬盘有足够空闲空间,建议至少是当前 vhdx 大小的 2 倍(需要临时存放 tar 备份)
  • PowerShell 以普通用户权限运行即可,无需管理员
  • 迁移期间 WSL 会关闭,正在运行的任务请提前保存

步骤总览

查看发行版名称 → 关闭 WSL → 导出 tar → 注销原版 → 导入新位置 → 修复用户 → 验证清理

详细步骤

Step 1:查看当前发行版名称

wsl --list --verbose

输出示例:

  NAME                   STATE           VERSION
* Ubuntu-22.04           Running         2
  podman-machine-default Stopped         2

记下要迁移的发行版名称,本文为 Ubuntu-22.04。带 * 号的是默认发行版。


Step 2:关闭 WSL

wsl --shutdown

⚠️ 必须先关闭 WSL,否则 vhdx 文件被占用,导出会失败。


Step 3:导出为 tar 文件(备份)

在目标盘创建存放目录,然后执行导出:

# 先建好目录
mkdir F:\WSL

# 导出
wsl --export Ubuntu-22.04 F:\WSL\ubuntu22.tar

导出过程中会显示文件大小和进度,完成后提示”操作成功完成”。

💡 tar 文件大小与 vhdx 相近,确保目标盘有足够空间再执行。


Step 4:注销原发行版

wsl --unregister Ubuntu-22.04

⚠️ 执行后 C 盘的 ext4.vhdx 会被立即删除,请确认 Step 3 的 tar 文件已成功生成再执行!


Step 5:导入到新位置

wsl --import Ubuntu-22.04 F:\WSL\Ubuntu22 F:\WSL\ubuntu22.tar --version 2

参数说明:

参数 说明
Ubuntu-22.04 发行版名称,可以和原来保持一致
F:\WSL\Ubuntu22 新 vhdx 的存放目录,会自动创建
F:\WSL\ubuntu22.tar Step 3 导出的备份文件
--version 2 使用 WSL2

Step 6:恢复默认登录用户

导入后默认以 root 登录,需要改回原来的普通用户。

方法一(推荐):

ubuntu2204 config --default-user 你的用户名

方法二: 在 WSL 内编辑配置文件:

sudo tee /etc/wsl.conf <<EOF
[user]
default=你的用户名
EOF

💡 用户名替换为实际的 Linux 用户名,可在迁移前用 whoami 命令确认。


Step 7:验证并清理

进入 WSL 验证迁移结果:

wsl ~

在 WSL 内确认用户名和文件完整性:

whoami      # 确认不是 root
ls ~        # 确认个人文件都在

确认一切正常后,删除临时 tar 文件释放空间:

del F:\WSL\ubuntu22.tar

✅ 迁移完成!C 盘释放了与原 vhdx 等量的空间,本例释放了约 211 GB


常见问题

Q:迁移后 VSCode Remote WSL 还能用吗?

可以。VSCode 通过发行版名称识别 WSL,只要名称不变,重新连接后一切正常。

Q:迁移后 vhdx 没有缩小怎么办?

WSL 内删除文件后 vhdx 不会自动缩小。可以用以下命令手动压缩(需要管理员 PowerShell):

wsl --shutdown

# 进入 diskpart
diskpart

在 diskpart 中依次执行:

select vdisk file="F:\WSL\Ubuntu22\ext4.vhdx"
attach vdisk readonly
compact vdisk
detach vdisk
exit

Q:可以同时运行多个不同位置的 WSL 实例吗?

可以。用 --import 时指定不同的名称和目录即可,WSL 支持同时管理多个发行版,位置互不影响。

将Zephyr 中的 SMF(状态机) 移植到FreeRTOS

Zephyr 提供了一个内置的状态机框架(SMF – State Machine Framework),位于 smf.h 和 smf.c。


核心数据结构

状态定义:struct smf_state

struct smf_state {
    const state_execution entry;  // 进入状态时调用
    const state_execution run;    // 状态运行时反复调用
    const state_execution exit;   // 退出状态时调用
    const struct smf_state *parent; // 父状态(仅层次状态机)
};

状态机上下文:struct smf_ctx

struct smf_ctx {
    const struct smf_state *current;   // 当前状态
    const struct smf_state *previous;  // 上一个状态
    int32_t terminate_val;             // 终止值
    uint32_t internal;                 // 内部标志
};

核心 API

API 说明
SMF_CREATE_STATE(entry, run, exit) 创建扁平状态
SMF_CREATE_STATE(entry, run, exit, parent) 创建层次状态
smf_set_initial(ctx, state) 初始化并设置初始状态
smf_set_state(ctx, new_state) 触发状态转换
smf_run_state(ctx) 执行一次状态迭代
smf_set_terminate(ctx, val) 终止状态机

两种模式

1. 扁平状态机(默认)

每个状态相互独立,无父子关系:

#include <zephyr/smf.h>

/* 定义状态枚举 */
enum my_state { STATE_A, STATE_B, STATE_C };

/* 用户对象(第一个成员必须是 smf_ctx)*/
static struct my_obj {
    struct smf_ctx ctx;    // 必须是第一个成员
    int event;
} obj;

/* 状态处理函数 */
static void state_a_entry(void *o) { /* 进入 A */ }
static void state_a_run(void *o) {
    struct my_obj *self = (struct my_obj *)o;
    if (self->event == 1)
        smf_set_state(SMF_CTX(o), &states[STATE_B]); // 转换到 B
}
static void state_a_exit(void *o) { /* 退出 A */ }

/* 状态表 */
static const struct smf_state states[] = {
    [STATE_A] = SMF_CREATE_STATE(state_a_entry, state_a_run, state_a_exit),
    [STATE_B] = SMF_CREATE_STATE(NULL, state_b_run, NULL),
    [STATE_C] = SMF_CREATE_STATE(NULL, state_c_run, NULL),
};

int main(void) {
    smf_set_initial(SMF_CTX(&obj), &states[STATE_A]); // 初始化

    while (1) {
        // 处理事件,更新 obj.event ...
        int ret = smf_run_state(SMF_CTX(&obj)); // 运行一次迭代
        if (ret) break; // 非零表示终止
    }
}

2. 层次状态机(开启 CONFIG_SMF_ANCESTOR_SUPPORT

子状态共享父状态的行为,父状态的 entry/exit 在进入/退出子状态时自动执行(同级切换时不重复执行):

// 父状态
static const struct smf_state states[] = {
    [PARENT_AB] = SMF_CREATE_STATE(parent_entry, parent_run, parent_exit, NULL),
    [STATE_A]   = SMF_CREATE_STATE(a_entry, a_run, a_exit, &states[PARENT_AB]),
    [STATE_B]   = SMF_CREATE_STATE(b_entry, b_run, b_exit, &states[PARENT_AB]),
    [STATE_C]   = SMF_CREATE_STATE(c_entry, c_run, c_exit, NULL), // 无父状态
};

层次状态机中执行顺序规则:

  • entry:父状态先于子状态执行
  • run:子状态先执行,若无状态转换则父状态再执行
  • exit:子状态先于父状态执行
  • 同级切换(A→B,共享 PARENT_AB):父状态的 exit/entry 不重新执行

状态转换流程

smf_run_state() 调用 current->run()
        │
        └─ 内部调用 smf_set_state(ctx, new_state)
                │
                ├─ 执行 current->exit()
                ├─(层次模式)执行祖先 exit()
                ├─ 更新 ctx->current = new_state
                ├─(层次模式)执行祖先 entry()
                └─ 执行 new_state->entry()

Kconfig 选项

prj.conf 中启用:

CONFIG_SMF=y
CONFIG_SMF_ANCESTOR_SUPPORT=y  # 启用层次状态机(可选)

测试用例在 test_lib_flat_smf.c 和 test_lib_hierarchical_smf.c 中有完整示例,可参考学习。

SMF 对 Zephyr 的依赖非常少,移植到 FreeRTOS 主要需要处理以下几个方面:


依赖分析

smf.h 中的 Zephyr 依赖

依赖项 说明 FreeRTOS 替换方案
#include <zephyr/kernel.h> 提供 int32_tuint32_t 等类型 替换为 #include <stdint.h> + #include <stdbool.h>
CONFIG_SMF_ANCESTOR_SUPPORT Kconfig 编译开关 改用普通 #define SMF_ANCESTOR_SUPPORT
头文件保护宏 ZEPHYR_INCLUDE_SMF_H_ 命名规范 改为 SMF_H_

smf.c 中的 Zephyr 依赖

依赖项 说明 FreeRTOS 替换方案
#include smf.h> 头文件路径 改为 #include "smf.h"
#include <zephyr/logging/log.h> Zephyr 日志系统 删除,或自定义宏
LOG_MODULE_REGISTER(smf) 注册日志模块 删除
LOG_WRN("...") 日志警告输出 替换为 printf 或自定义宏
IS_ENABLED(CONFIG_SMF_ANCESTOR_SUPPORT) Kconfig 运行时检查宏 替换为 #ifdef SMF_ANCESTOR_SUPPORT 判断
__unused Zephyr 编译器属性宏 替换为 __attribute__((unused)) 或删除

具体修改步骤

第一步:新建 smf.h

#ifndef SMF_H_
#define SMF_H_

#include <stdint.h>
#include <stdbool.h>

/* 开启层次状态机支持,不需要则注释掉 */
#define SMF_ANCESTOR_SUPPORT

#ifdef SMF_ANCESTOR_SUPPORT
#define SMF_CREATE_STATE(_entry, _run, _exit, _parent) \
{ \
    .entry  = _entry, \
    .run    = _run,   \
    .exit   = _exit,  \
    .parent = _parent \
}
#else
#define SMF_CREATE_STATE(_entry, _run, _exit) \
{ \
    .entry = _entry, \
    .run   = _run,   \
    .exit  = _exit   \
}
#endif

#define SMF_CTX(o) ((struct smf_ctx *)o)

typedef void (*state_execution)(void *obj);

struct smf_state {
    const state_execution entry;
    const state_execution run;
    const state_execution exit;
#ifdef SMF_ANCESTOR_SUPPORT
    const struct smf_state *parent;
#endif
};

struct smf_ctx {
    const struct smf_state *current;
    const struct smf_state *previous;
    int32_t terminate_val;
    uint32_t internal;
};

void    smf_set_initial(struct smf_ctx *ctx, const struct smf_state *init_state);
void    smf_set_state(struct smf_ctx *ctx, const struct smf_state *new_state);
void    smf_set_terminate(struct smf_ctx *ctx, int32_t val);
int32_t smf_run_state(struct smf_ctx *ctx);

#endif /* SMF_H_ */

第二步:修改 smf.c

// 原来
#include <zephyr/smf.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(smf);

// 替换为
#include "smf.h"
#include <stdio.h>  // 用于 printf(可选)
// 原来
LOG_WRN("Calling %s from exit action", __func__);

// 替换为(或直接删掉这行)
printf("[SMF WRN] Calling %s from exit action\n", __func__);
// 原来
__unused static bool smf_execute_ancestor_entry_actions(...)
__unused static bool smf_execute_ancestor_run_actions(...)
__unused static bool smf_execute_ancestor_exit_actions(...)

// 替换为(GCC 通用属性)
__attribute__((unused)) static bool smf_execute_ancestor_entry_actions(...)
// 原来
if (IS_ENABLED(CONFIG_SMF_ANCESTOR_SUPPORT)) { ... }

// 替换为
#ifdef SMF_ANCESTOR_SUPPORT
    // ... 相关代码
#endif

总结

SMF 的核心逻辑(smf.c 中约 286 行)完全不依赖 FreeRTOS 任何 API,它只是一个纯 C 的状态机调度框架,没有涉及任务、队列、信号量等 RTOS 原语。

移植工作量非常小,归纳为 4 件事

  1. #include <zephyr/kernel.h> 改为 <stdint.h> + <stdbool.h>
  2. CONFIG_SMF_ANCESTOR_SUPPORT 改为普通 #define,并将 IS_ENABLED(...) 改为 #ifdef
  3. 删除 Zephyr 日志相关代码(LOG_MODULE_REGISTERLOG_WRN
  4. __unused 改为 __attribute__((unused)) 或直接删除

晶振负载电容选型

  • image-20260314102913-7t78sfv

负载电容是指电路中的这两个电容,但不完全等于“这两个电容的标称值”

图里的 C5 和 C11就是用来构成晶振“负载电容(Load Capacitance, CL)”的那两个电容。 但:

晶振规格里写的“负载电容 12 pF”,不是指 C5\=12 pF、C11\=12 pF,而是指“等效负载电容 CL”


正确的负载电容定义

晶振看到的负载电容是:

$$ CL = \frac{C5 \times C11}{C5 + C11} + C_{stray} $$

其中:

  • C5、C11:你画的这两个对地电容
  • C_stray(寄生电容)

    • MCU 引脚电容(2\~5 pF)
    • PCB 走线电容(1\~3 pF)
    • 焊盘、封装等

👉 晶振参数里标的 12 pF,指的是这个 CL


结合你现在的电路看

现在是:

  • C5 \= 18 pF
  • C11 \= 18 pF

先算理想值(不含寄生):

$$ CL_{ideal} = \frac{18 \times 18}{18 + 18} = 9\text{ pF} $$

再加上常见寄生电容:

  • C_stray ≈ 3\~5 pF(非常常见)

$$ CL \approx 9 + (3\sim5) = 12\sim14\text{ pF} $$

👉 这正好匹配晶振标称的“负载电容 12 pF”


经验公式 👇

常用经验选值(C5 \= C11)

晶振标称 CL 常用电容
6 pF 10–12 pF
8 pF 12–15 pF
12 pF 18–22 pF
16 pF 22–27 pF

👉 12 pF 的晶振,用 18 pF 是最常见方案之一


❌ 错误理解

“晶振负载电容是 12 pF,所以我 C5\=12 pF,C11\=12 pF”

这样算出来:

$$ CL \approx 6\text{ pF} + C_{stray} $$

👉 频率会偏高(High Frequency Shift)

EIDE [Builder Options]下[Global Options]说明


ARM/Thumb Mode — Thumb Mode

  • 使用 Thumb 指令集(16位压缩指令)

  • Cortex-M 系列(如你的 N32G430)只支持 Thumb/Thumb-2,必须选这个

  • 选 ARM Mode 会编译失败


Thumb Interwork

  • 允许 ARM 指令和 Thumb 指令**混合调用**

  • Cortex-M 不需要


Hardware floating-point ABI

  • <span data-type="code">soft</span>:纯软件模拟浮点,最慢

  • <span data-type="code">softfp</span>:用硬件FPU计算,但函数调用用软件ABI传参,兼容性最好

  • <span data-type="code">hard</span>:最快,但库必须全部用 hard 编译,否则链接报错

  • N32G430 /STM32F4这种有 FPU的,<span data-type="code">softfp</span> 是稳妥选择,但如果不调用别人编译好的lib,可以使用hard


Output debug information

  • 生成调试符号(.elf 里含地址/变量名映射)

  • 调试阶段必须开,量产时可关掉缩小固件体积


Other Global Options

--specs=nosys.specs --specs=nano.specs
选项 作用
​<span data-type="code">--specs=nosys.specs</span> 用**空桩函数**替代系统调用(_write/_read等),裸机必须加,否则链接报”undefined reference to _sbrk
​<span data-type="code">--specs=nano.specs</span> 使用 **newlib-nano**,精简版 C 库,printf 体积大幅减小,嵌入式推荐

⚠️ –specs=nano.specs带来的问题

​<span data-type="code">--specs=nano.specs</span> 下的 printf **默认不支持 %f 浮点格式化**,如果你的日志里有打印浮点数,需要加:

-u _printf_float

否则浮点日志会输出空或乱码

 

编译选项 –specs=nano.specs /newlib-nano的影响

--specs=nano.specs (newlib-nano) 的影响


1. printf / sprintf 格式化限制

格式符 默认nano -u _printf_float
%d %x %s ✅ 正常 ✅ 正常
%f %e %g ❌ 输出空/乱码 ✅ 正常
%lld (64位整数) ❌ 不支持 ❌ 需另加 -u _printf_long_long

2. 堆内存相关

  • malloc / free 使用的是精简版分配器
  • 没有线程安全保护(裸机无所谓,FreeRTOS下需注意)
  • _sbrk 需要你自己实现,否则 malloc 会 HardFault

3. 本地化 / 宽字符

  • 不支持 wprintf / wchar_t 相关函数
  • setlocale() 无效,中文字符处理受限

4. 文件 I/O

  • fopen / fread 等是空桩(配合 nosys.specs
  • 需要自己实现 _write / _read 才能让 printf 输出到串口

5. 数学库

  • libm 体积同样缩减,部分精度受影响
  • 建议显式链接:-lm

⚠️ 最容易踩的坑,嵌入式中一定要自己重定向输出函数

/* [ARMCC] retarget the C library printf function to the USART */
int fputc(int ch, FILE *f)
{
    USART_Data_Send(USART2, (uint8_t)ch);
    while (USART_Flag_Status_Get(USART2, USART_FLAG_TXDE) == RESET)
        ;
    return (ch);
}

/* [GCC] retarget the C library printf function to the USART */
int _write(int file, char *data, int len)
{
    // if ((file != STDOUT_FILENO) && (file != STDERR_FILENO))
    // {
    //     errno = EBADF;
    //     return -1;
    // }
    for (int i = 0; i < len; i++) {
        USART_Data_Send(USART2, data[i]);
        while (USART_Flag_Status_Get(USART2, USART_FLAG_TXDE) == RESET)
            ;
    }
    return 0;
}

如果 _write 没有实现或者实现有误,printf 调用会静默丢弃所有输出

MCU中如何将字符串指定到确定的链接地址

 

 

MCU中如何将字符串指定到确定的链接地址

要把某个字符串(常量)放到固定地址,推荐用“自定义段 + 链接脚本/散装文件固定该段地址”。不要只写一个固定地址指针(那样地址处的数据不会被链接器自动放进去,除非你另外烧录)。

下面分别给 GCC(ld)ARMCC(AC5 scatter) 的做法。


方案 A(推荐):放到固定 Flash 地址(由链接器保证)

1) C 里把字符串放到自定义 section

// 放在任意 .c 文件里
__attribute__((section(".fixed_str"), used))
const char g_fixed_str[] = "POWER_BOARD_APP:1.2.3";

used/KEEP 是为了防止链接优化把它丢掉。

2) GCC:在 .ld 里把该 section 固定到地址

在你的链接脚本里加一段(地址换成你要的,比如 Flash 尾部):

/* 例:固定到 0x0800FC00(请确认没和别的段冲突) */
.fixed_str 0x0800FC00 :
{
  KEEP(*(.fixed_str))
} > FLASH

方案 B:ARMCC(AC5) 用 scatter 文件固定地址

1) C 里同样放到指定 section 名

__attribute__((section("FIXED_STR"), used))
const char g_fixed_str[] = "POWER_BOARD_APP:1.2.3";

2) 在 .sct(scatter)里创建一个固定地址的执行区放它

示例结构(你需要把它合并到现有 scatter 中,地址/大小按需调整):

; 在 scatter 文件里新增一个执行区,把 FIXED_STR 放进去
LR_IROM1 0x08000000 0x00010000  {
  ER_IROM1 0x08000000 0x0000FC00  {  ; 先放常规 RO
    * (+RO)
  }
  ER_FIXED 0x0800FC00 0x00000400  {  ; 固定区域
    *(FIXED_STR)
  }
}

方案 C(仅“读取固定地址”):固定地址指针(不负责放置数据)

如果你只是想读取某固定地址已有的字符串(比如出厂信息区),可以:

#define FIXED_STR_ADDR (0x0800FC00u)
const char * const g_fixed_str_ptr = (const char *)FIXED_STR_ADDR;

注意:这不会把 “…” 放进该地址;该地址内容必须已被你单独烧录或由其他镜像生成。


关键注意点

  • 固定地址必须在有效的 Flash/RAM 范围内,且不与 .text/.rodata/.data/.bss 等重叠。

  • 常量字符串通常放 Flash(RO);放 RAM 需要另外做段和初始化策略。