将 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)

编译选项 –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 调用会静默丢弃所有输出

printf重定向方法

嵌入式开发中串口是调试的常用方式,printf​作为调试最常用的方式是需要重定向的,不同的编译器有不同的重定向方式。


1、GCC开发环境

#if !defined(__CROSSWORKS_ARM) && defined(__GNUC__)

#include 
#include 

int _close(int file);
void _exit(int status);
int _fstat(int file, struct stat *st);
int _getpid(void);
int _isatty(int file);
int _kill(int pid, int sig);
int _lseek(int file, int ptr, int dir);
int _read(int file, char *ptr, int len);
int _write(int file, const char *ptr, int len);

/**************************************************************************//**
* Close a file.
*
* @param[in] file File you want to close.
*
* @return Returns 0 when the file is closed.
*****************************************************************************/
int _close(int file)
{
  (void) file;
  return 0;
}

/**************************************************************************//**
* Exit the program.
*
* @param[in] status The value to return to the parent process as the
* exit status (not used).
*****************************************************************************/
void _exit(int status)
{
  (void) status;
  while (1) {
  } // Hang here forever...
}

/**************************************************************************//**
* Status of an open file.
*
* @param[in] file Check status for this file.
*
* @param[in] st Status information.
*
* @return Returns 0 when st_mode is set to character special.
*****************************************************************************/
int _fstat(int file, struct stat *st)
{
  (void) file;
  st->st_mode = S_IFCHR;
  return 0;
}

/**************************************************************************//**
* Get process ID.
*
* @return Return 1 when not implemented.
*****************************************************************************/
int _getpid(void)
{
  return 1;
}

/**************************************************************************//**
* Query whether output stream is a terminal.
*
* @param[in] file Descriptor for the file.
*
* @return Returns 1 when query is done.
*****************************************************************************/
int _isatty(int file)
{
  (void) file;
  return 1;
}

/**************************************************************************//**
* Send signal to process.
*
* @param[in] pid Process id (not used).
*
* @param[in] sig Signal to send (not used).
*****************************************************************************/
int _kill(int pid, int sig)
{
  (void)pid;
  (void)sig;
  return -1;
}

/**************************************************************************//**
* Set position in a file.
*
* @param[in] file Descriptor for the file.
*
* @param[in] ptr Poiter to the argument offset.
*
* @param[in] dir Directory whence.
*
* @return Returns 0 when position is set.
*****************************************************************************/
int _lseek(int file, int ptr, int dir)
{
  (void) file;
  (void) ptr;
  (void) dir;
  return 0;
}

/**************************************************************************//**
* Read from a file.
*
* @param[in] file Descriptor for the file you want to read from.
*
* @param[in] ptr Pointer to the chacaters that are beeing read.
*
* @param[in] len Number of characters to be read.
*
* @return Number of characters that have been read.
*****************************************************************************/
int _read(int file, char *ptr, int len)
{
  (void)file;

  return readBuffer(ptr, len);
}

/**************************************************************************//**
* Write to a file.
*
* @param[in] file Descriptor for the file you want to write to.
*
* @param[in] ptr Pointer to the text you want to write
*
* @param[in] len Number of characters to be written.
*
* @return Number of characters that have been written.
*****************************************************************************/
int _write(int file, const char *ptr, int len)
{
  (void)file;

  return writeBuffer(ptr, len);
}

#endif /* !defined( __CROSSWORKS_ARM ) && defined( __GNUC__ ) */

在自己的程序中,只需实现:

int writeBuffer(char *ch, int length);
int readBuffer(char *ch, int length);

2、keil开发环境

2.1、使用MicroLib

在代码中实现fputc​函数

// 重定向printf
int fputc(int ch, FILE *f){
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000); 
  return ch;
}

// 重定向getchar
int fgetc(FILE *f)
{
  int ch;
  while (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) == RESET);
  HAL_UART_Receive(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
  return (ch);
}

2.2、使用非半主机模式

新建syscalls.c

#if defined(__CC_ARM)
/******************************************************************************/
/* RETARGET.C: 'Retarget' layer for target-dependent low-level functions */
/******************************************************************************/
/* This file is part of the uVision/ARM development tools. */
/* Copyright (c) 2005-2006 Keil Software. All rights reserved. */
/* This software may only be used under the terms of a valid, current, */
/* end user licence from KEIL for a compatible version of KEIL software */
/* development tools. Nothing else gives you the right to use this software. */
/******************************************************************************/

#include 

#pragma import(__use_no_semihosting_swi)

struct __FILE{
  int handle;
};

//Standard output stream
FILE __stdout;

/**************************************************************************//**
* Writes character to file
*
* @param[in] f File
*
* @param[in] ch Character
*
* @return Written character
*****************************************************************************/
int fputc(int ch, FILE *f)
{
  return putChar(ch);
}

/**************************************************************************//**
* Reads character from file
*
* @param[in] f File
*
* @return Character
*****************************************************************************/
int fgetc(FILE *f)
{
return getChar();
}

/**************************************************************************//**
* Tests the error indicator for the stream pointed to by file
*
* @param[in] f File
*
* @return Returns non-zero if it is set
*****************************************************************************/
int ferror(FILE *f)
{
  // Your implementation of ferror
  return EOF;
}

/**************************************************************************//**
* Writes a character to the console
*
* @param[in] ch Input character
*****************************************************************************/
void _ttywrch(int ch)
{
  putChar(ch);
}

/**************************************************************************//**
* Library exit function. This function is called if stack overflow occurs.
*
* @param[in] return_code Return code
*****************************************************************************/
void _sys_exit(int return_code)
{
  label: goto label; // endless loop
}

#endif /* defined( __CC_ARM ) */

收藏-希腊字母表


收藏以备之后使用

序号 大写 小写 英文注音 国际音标注音 中文注音
1 Α α alpha a:lf 阿尔法
2 Β β beta bet 贝塔
3 Γ γ gamma ga:m 伽马
4 Δ δ delta delt 德尔塔
5 Ε ε epsilon ep`silon 伊普西龙
6 Ζ ζ zeta zat 截塔
7 Η η eta eit 艾塔
8 Θ θ thet θit 西塔
9 Ι ι iot aiot 约塔
10 Κ κ kappa kap 卡帕
11 λ lambda lambd 兰布达
12 Μ μ mu mju
13 Ν ν nu nju
14 Ξ ξ xi ksi 克西
15 Ο ο omicron omik`ron 奥密克戎
16 π pi pai
17 Ρ ρ rho rou
18 σ sigma `sigma 西格马
19 Τ τ tau tau
20 Υ υ upsilon jup`silon 宇普西龙
21 Φ φ phi fai 佛爱
22 Χ χ chi phai 西
23 Ψ ψ psi psai 普西
24 Ω ω omega o`miga 欧米伽

U-boot无法使用NFS从Ubuntu中下载文件

最近在调试一块Linux板卡的时候需要在U-boot中挂载Linux镜像,但是发现总是超时挂载失败。

失败时的log输出

因为板卡是可以ping通Ubuntu主机的,所以可以基本可以排除是本地网络的问题。

既然考虑是NFS的问题,那么就需要先确定NFS的配置是否正确

先查看/etc/exports文件中的配置是否正确,确认无误后确定NFS根目录的权限是否正常,这些都确认没有问题后就可以进行下一步的检查了。

首先查看nfs的版本:

sudo cat /proc/fs/nfsd/versions
输出:
-2 +3 +4 +4.1 +4.2

可以看到当前Ubuntu主机没有支持V2,但是U-boot需要使用V2,所以这里需要打开NFS V2的支持。

这里有两种方式打开,如果是Ubuntu18.04的话,需要编辑配置文件的内容。

sudo vim /etc/default/nfs-kernel-server 
# Number of servers to start up
#RPCNFSDCOUNT=8
RPCNFSDCOUNT="-V 2 8"

# Runtime priority of server (see nice(1))
RPCNFSDPRIORITY=0

# Options for rpc.mountd.
# If you have a port-based firewall, you might want to set up
# a fixed port here using the --port option. For more information, 
# see rpc.mountd(8) or http://wiki.debian.org/SecuringNFS
# To disable NFSv4 on the server, specify '--no-nfs-version 4' here
#RPCMOUNTDOPTS="--manage-gids"
RPCMOUNTDOPTS="-V 2 --manage-gids"

# Do you want to start the svcgssd daemon? It is only required for Kerberos
# exports. Valid alternatives are "yes" and "no"; the default is "no".
NEED_SVCGSSD=""

# Options for rpc.svcgssd.
#RPCSVCGSSDOPTS=""
RPCSVCGSSDOPTS="--nfs-version 2,3,4 --debug --syslog"

修改后保存,退出,然后重启NFS服务就可以了。

sudo /etc/init.d/nfs-kernel-server restart

如果是Ubuntu20.04需要修改NFS的配置文件

sudo vim /etc/nfs.conf
#
# This is a general configuration for the
# NFS daemons and tools
#
[general]
pipefs-directory=/run/rpc_pipefs
#
[exports]
# rootdir=/export
#
[exportfs]
# debug=0
#
[gssd]
# verbosity=0
# rpc-verbosity=0
# use-memcache=0
# use-machine-creds=1
# use-gss-proxy=0
# avoid-dns=1
# limit-to-legacy-enctypes=0
# context-timeout=0
# rpc-timeout=5
# keytab-file=/etc/krb5.keytab
# cred-cache-directory=
# preferred-realm=
#
[lockd]
# port=0
# udp-port=0
#
[mountd]
# debug=0
manage-gids=y
# descriptors=0
# port=0
# threads=1
# reverse-lookup=n
# state-directory-path=/var/lib/nfs
# ha-callout=
#
[nfsdcld]
# debug=0
# storagedir=/var/lib/nfs/nfsdcld
#
[nfsdcltrack]
# debug=0
# storagedir=/var/lib/nfs/nfsdcltrack
#
[nfsd]
# debug=0
# threads=8
# host=
# port=0
# grace-time=90
# lease-time=90
udp=y
# tcp=y
vers2=y
# vers3=y
# vers4=y
# vers4.0=y
# vers4.1=y
# vers4.2=y
# rdma=n
# rdma-port=20049
#
[statd]
# debug=0
# port=0
# outgoing-port=0
# name=
# state-directory-path=/var/lib/nfs/statd
# ha-callout=
# no-notify=0
#
[sm-notify]
# debug=0
# force=0
# retry-time=900
# outgoing-port=
# outgoing-addr=
# lift-grace=y
#
[svcgssd]
# principal=

这个文件需要修改两个地方,一个是开启Version2的支持,另一个如果当前NFS不支持UDP也需要开启UDP的支持。是否已经支持UDP可以使用:

$netstat -a | grep "nfs"
#没有支持
tcp        0      0 0.0.0.0:nfs             0.0.0.0:*               LISTEN     
tcp6       0      0 [::]:nfs                [::]:*                  LISTEN  

#支持
tcp        0      0 0.0.0.0:nfs             0.0.0.0:*               LISTEN     
tcp6       0      0 [::]:nfs                [::]:*                  LISTEN     
udp        0      0 0.0.0.0:nfs             0.0.0.0:*                          
udp6       0      0 [::]:nfs                [::]:*    

如果至此依旧不能解决问题,请确认系统防火墙以及杀毒软件是否误杀,可以先都关闭后尝试。

[Zephyr] 01-开发环境搭建

前言:因为需要做一个蓝牙项目,所以接触到了Zephyr操作系统。本文的内容主要来自官方的文档,有部分是自己的理解和笔记。我的开发过程都是在Ubuntu环境下进行的,Zephyr也支持在Windows和macOS环境下开发,但是该系列文章不会涉及Windows和macOS环境下的开发环境搭建以及开发。

免责声明:本文引用的文章、资料版权及所有权均归原作者所有。受限于自己的能力,本文不保证完全没有错误。如果有与官方文档不一致的地方请以官方文档为准!

1、Zephyr简介

Zephyr是专门面向微控制器(MCU)的实时操作系统。经过很多年的发展,现在的Zephyr已经能够很好的支持多种架构的CPU以及很多厂家的芯片,包括大家熟悉的STM32、GD32、NRF、ESP32等系列的芯片,大有成为MCU界的Linux之势。而事实上现在的Zephyr也是由Linux基金会和Wind River Systems Inc(Intel旗下的子公司)共同管理。在Zephyr身上确实也能看到Linux的影子,比如设备树,没错,Zephyr上也有设备树,不过Zephyr的设备树和Linux的设备树不能说有所不同,只能说完全不同。受限于MCU平台的资源,Zephyr的设备树不像Linux中会有单独的文件和设备树系统。而是在编译阶段发挥作用,在预编译阶段脚本会把.dts中的内容以宏定义的形式生成.h文件。

不过我觉得与其说Zephyr是一个嵌入式操作系统,不如说它是一套开发工具,因为除了操作系统内核Zephyr还包含了文件系统、驱动模型、电源管理、固件更新、密码学等等,Zephyr甚至还包含了蓝牙协议栈、USB协议栈、LVGL等等。

除了上面说的这些优势,最重要的是Zephyr是完全开源的,并且Zephyr 使用Apache 2.0 许可证,也就是说Zephyr是支持商用的。

更详细的介绍大家可以去看Zephyr官方文档中的介绍

2、安装

Zephyr的开发环境搭建相比Keil、MDK、IAR等大家熟悉的IDE那种无脑下一步的安装方式还是要麻烦不少,不管是什么平台都没法逃过命令行,不过大家也不用一看到命令行就发懵,其实常用的命令就那么几个,很快就可以熟悉的。

下面就介绍在Linux下的安装过程,这里我用Ubuntu系统,Zephyr还支持其它的Linux发行版,其它发行版请参照Zephyr官方文档。注意:Zephyr需要Ubuntu 18.04 LTS 及更高的版本!整个安装过程会涉及到很从外网服务器下载源码以及安装包的过程,需要多次尝试或者想别的办法解决。推荐使用Ubuntu20.04或以上版本,会容易一些。

更新软件

Linux在搭建一个环境之前我们一般都会先同步系统上的软件包列表为最新的状态并且更新本地的软件为最新的状态。

sudo apt update
sudo apt upgrade

安装依赖

工具 版本
CMake 3.20.0
Python 3.8
Devicetree compiler 1.4.6
Zephyr需要的主要软件依赖以及版本

当Ubuntu版本小于22.04时(实测20.04也不需要,官方文档说大于22.04不需要),需要添加额外的存储库来满足上面的主要依赖的最低版本需求,这里需要使用 Kitware 存档脚本将 Kitware APT 存储库添加到源列表中。

<code>wget https://apt.kitware.com/kitware-archive.sh sudo bash kitware-archive.sh

如果是22.04版本就不需要上面这步。其余版本执行完脚本后就可以使用apt安装需要的依赖

sudo apt install --no-install-recommends git cmake ninja-build gperf \
  ccache dfu-util device-tree-compiler wget \
  python3-dev python3-pip python3-setuptools python3-tk python3-wheel xz-utils file \
  make gcc gcc-multilib g++-multilib libsdl2-dev libmagic1

安装完成后检查一下主要依赖的版本和上面表格中的版本,如果一致就进行下一步,如果不一致就需要手动调整版本。具体的解决方式参考官方安装Linux主机依赖项

cmake --version
python3 --version
dtc --version

获取Zephyr并且安装Python依赖项

Zephyr的安装、使用都依赖于west工具,west工具依赖Python环境,用过Python的朋友一定知道我们平时做Python环境的时候一般都不会全局配置,因为如果全局配置各个软件的兼容性会让你怀疑人生。所以安装Zephyr的时候我们同样使用Python的虚拟环境安装,之后使用的时候再激活环境就可以了,虽然听着好像麻烦了一点,但是如果全局安装遇到兼容性的问题的时候就知道虚拟环境有多美好了。

首先安装Python的venv包,然后创建虚拟环境并且激活环境:

sudo apt install python3-venv
# 在Home下创建了一个名叫zephyrproject的文件夹,后续Zephyr相关的内容都会放在这个文件夹下。这个文件夹的名字和路径可以自定义,但是后面涉及路径的就需要改为你自己的实际路径。
cd ~
mkdir zephyrproject
python3 -m venv ~/zephyrproject/.venv
source ~/zephyrproject/.venv/bin/activate
# 想要停用环境可以用
# source ~/zephyrproject/.venv/bin/deactivate

通过上面的步骤就安装并且激活了Python的虚拟环境,下面我们就在刚才激活的环境中安装west,并且获取Zephyr源代码:

pip install west
west init ~/zephyrproject
cd ~/zephyrproject
west update

成功获取Zephyr的源代码后就可以导出Zephyr Cmake包(让Cmake能够自动加载编译Zephyr应用程序所需的样板代码)并且安装Zephyr额外的依赖:

west zephyr-export
pip install -r ~/zephyrproject/zephyr/scripts/requirements.txt

安装Zephyr SDK

经过上面的步骤,我们已经安装了Zephyr的开发工具west,并且获取Zephyr的源码到我们的电脑中,想要将代码编译为各个实际的可执行文件还需要对应架构的工具链。Zephyr将各个平台的工具链都包含在了Zephyr SDK中,我们只需要安装Zephyr SDK就可以支持各个平台的编译了。

#下载并校验SDK包
cd ~
wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.15.0/zephyr-sdk-0.15.0_linux-x86_64.tar.gz
wget -O - https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.15.0/sha256.sum | shasum --check --ignore-missing

#解压SDK包,Zephyr推荐了几个解压路径,这里我直接解压到home目录下
tar xvf zephyr-sdk-0.15.0_linux-x86_64.tar.gz

#进入解压后的文件夹,安装Zephyr SDK
cd zephyr-sdk-0.15.0
./setup.sh

#安装udev规则这样就可以以普通用户的身份刷写大多数 Zephyr 板
sudo cp ~/zephyr-sdk-0.15.0/sysroots/x86_64-pokysdk-linux/usr/share/openocd/contrib/60-openocd.rules /etc/udev/rules.d
sudo udevadm control --reload

编译测试

至此Zephyr的开发环境就搭建完成了,现在可以尝试编译一下例程验证一下我们的环境搭建是否成功。

这里我使用NRF52840DK开发板,尝试运行LED闪烁的代码。

cd ~/zephyrproject/zephyr
west build -p always -b nrf52840dk_nrf52840 samples/basic/blinky
#下载
west flash

如果环境搭建成功,那么就会看到与我类似的编译输出。

编译完成后输出的内存占用情况

简单解释一下上面的west指令的含义,build代表这是编译命令,-p always表示编译前始终清除之前的编译内容,强制开始完整的编译过程-p 还可以跟auto ,表示自动判断是否需要清除之前的编译结果,如果不加-p指令,如果需要将当前的代码编译为别的板子上运行的代码就会报错。后面的-b是指当前要编译的板子类型,官方支持的板子都在zephyr->board目录下,我们常用的比如STM32,GD32,NRF52840这些都是arm32位的,都在arm目录下。ESP32在xtensa目录下,安装完成之后最好先自己翻着看看各个目录下都有些什么东西。

west flash 可以直接将编译后的代码烧写到板子中,但是在实际情况中经出会遇到比如编译是在统一的云服务器中,或者主机没办法连接调试器,这就需要我们将文件从服务器copy到本地用对应的烧写工具烧写。这里推荐一个叫做NetDrive2的软件,可以很方便的将服务器的文件夹映射成为本地磁盘,当然还有很多别的方式,这里用自己最熟悉的就可以。

至此,环境的安装,验证都完成了,看似整个安装过程很复杂其实归根结底只有三个步骤:

  • 准备环境,依赖、python
  • 安装west,获取源码
  • 安装SDK

安装过程就写到这里,下一篇文章将会讲解如何创建自定义的板级描述文件,并且创建自己的工程。

树莓派镜像打包

众所周知,树莓派是用来折腾的,但是因为系统跑在SD上以及折腾时手残总是无法避免,这就导致系统很容易崩了,系统蹦了如果从头烧镜像,装环境那简直是生不如死,所以这就体现了备份镜像的必要性。

备份镜像最傻瓜的方式就是在Windows平台下使用[Win32 Disk Image]的Read功能直接把SD卡中的镜像读取到文件中,但是直接读取的文件是与SD卡大小一致的,这就导致不管是备份存储还是重新写入都会有很多麻烦,为了解决这个麻烦就需要调整SD卡的分区大小然后再读出备份。而调整分区的操作在Windows下又会遇到一些问题。而且Linux下有十分便利的脚本可以实现镜像压缩,所以推荐的方式是在Linux平台下操作,比如ubuntu。

具体操作:

先将SD连接到Linux主机,并查看设备名以及分区信息。

xingyong@xingyong-dell-7050:~$ sudo fdisk -l

Disk /dev/sda: 931.5 GiB, 1000204886016 bytes, 1953525168 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes

Disk /dev/nvme0n1: 238.5 GiB, 256060514304 bytes, 500118192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x6a4965ff

Device         Boot Start       End   Sectors   Size Id Type
/dev/nvme0n1p1 *     2048 500117503 500115456 238.5G 83 Linux

Disk /dev/sdb: 59.5 GiB, 63864569856 bytes, 124735488 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xe4b7df54

Device     Boot  Start       End   Sectors  Size Id Type
/dev/sdb1         8192    532479    524288  256M  c W95 FAT32 (LBA)
/dev/sdb2       532480 124735487 124203008 59.2G 83 Linux

此时可以看到SD卡的设备名称,我的是sdb,有两个分区,然后使用dd指令将SD中的镜像拷贝到Linux主机内,注意,此时拷贝的依旧是完整的镜像,大小与SD卡大小一致,这就需要你的主机得有比SD卡容量大的空闲空间,如果不够,就需要先调整SD卡分区大小再操作!

xingyong@xingyong-dell-7050:~$ sudo dd if=/dev/sdb of=./raspberrypi.img bs=512
124735488+0 records in
124735488+0 records out
63864569856 bytes (64 GB, 59 GiB) copied, 779.038 s, 82.0 MB/s

经过上面的操作,我们就将镜像从SD卡打包到img文件中了,但是此时的文件大小还是很大,需要压缩一下,这里推荐一个超级好用的脚本[GitHub链接],关于脚本的详细信息大家可以去仓库查看,这里就不展开了。

xingyong@xingyong-dell-7050:~$ sudo pishrink.sh raspberrypi.img 
pishrink.sh v0.1.2
pishrink.sh: Gathering data ...
Creating new /etc/rc.local
pishrink.sh: Checking filesystem ...
rootfs: 120206/3784416 files (0.4% non-contiguous), 1326501/15525376 blocks
resize2fs 1.44.1 (24-Mar-2018)
pishrink.sh: Shrinking filesystem ...
resize2fs 1.44.1 (24-Mar-2018)
Resizing the filesystem on /dev/loop0 to 1660441 (4k) blocks.
Begin pass 2 (max = 14804)
Relocating blocks             XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Begin pass 3 (max = 474)
Scanning inode table          XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
The filesystem on /dev/loop0 is now 1660441 (4k) blocks long.

pishrink.sh: Shrinking image ...
pishrink.sh: Shrunk imgname.img from 60G to 6.6G ...

可以看到执行完脚本之后镜像从60G变为了6.6G,这样你就可以愉快的使用这个打包并压缩的镜像了。

如果要在Linux下烧录镜像依旧可以使用dd指令,只需要调换上面if/of的内容就好。

xingyong@xingyong-dell-7050:~$ sudo dd if=./raspberrypi.img of=/dev/sdb bs=512
13816009+0 records in
13816009+0 records out
7073796608 bytes (7.1 GB, 6.6 GiB) copied, 873.813 s, 8.1 MB/s

把做好镜像的TF卡插回树莓派上启动,进入系统后使用raspi-config对文件系统扩展使用全部TF卡空间就可以愉快的使用了。

如果不知道如何扩展可以查看树莓派手册Expand Filesystem章节

Arduino与树莓派字节对齐问题记录

我在做一个小东西的时候用到了Arduino和树莓派,因为需要将数据采集端放的比较远所以没有直接将传感器接在树莓派上,而是选择了使用Arduino采集传感器数据,然后使用串口与树莓派通信。就在我很快写完两端的代码之后遇到了一个奇怪的问题,那就是明明数据接收解析过程没有问题,但是解析的数据怎么也不对。经过分析发现是两个平台数据长度和默认的对齐方式不同导致的,特此记录一下。

struct sensor_data{
    uint16_t hand;
    double ambient_temp;
    double object_temp;
    uint16_t tail;
}sensor_data;

我在两个平台定义了相同的结构体用来传输数据,在Arduino上将传感器数据获取到之后写入结构体,然后使用按字节从串口发送,因为两个平台都是小端模式,所以理论上在树莓派上我只需要按字节把接收的数据拷贝到相同结构体就可以获取到数据,但是,实际发现Arduino只发送了14字节,也就是说在Arduino平台下double只占用了4字节,与float是一致的。而且是按2字节对齐。

A5 5A D0 A3 E2 41 30 5C E1 41 0F F0

实际打印的数据也印证了我的猜想,后来发现Arduino的文档中关于double的描述明确指出了这个问题。

文档中关于double的说明

树莓派自然是不存在double与float长度一致的问题,而且树莓派是4字节对齐,所以就会出现我遇到的数据错误的问题。知道问题所在,修改代码就愉快的解决问题了。

关于C语言中的字节对齐一般有两种方式:

一、强制按字节对齐:

#pragma pack (n)    //C编译器将按照n个字节对齐。
#pragma pack ()     //取消自定义字节对齐方式。

二、对齐到n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐:

__attribute((aligned (n)))  //C编译器将按照n个字节对齐。
__attribute ((packed))      //取消自定义字节对齐方式。

最后附上此次测试的代码:

树莓派:

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <wiringPi.h>
#include <wiringSerial.h>

#pragma pack (2)
struct sensor_data{
    uint16_t hand;
    uint16_t serial_number;
    float ambient_temp;
    float object_temp;
    uint16_t tail;
}sensor_data;
#pragma pack ()

struct sensor_data rx_data;

int main(void)
{
    int hs1;
    char uart_rx_buffer[64];
    uint16_t uart_flag = 0;
    int16_t index = 0;
    int16_t data_flag = 0;
    wiringPiSetup();                            // 使用wiring编码去初始化GPIO序号
    // hs1 = serialOpen("/dev/ttyS0", 115200);     // 打开 /dev/ttyS0 串口设备,波特率115200
    hs1 = serialOpen("/dev/ttyUSB0", 115200); // 打开 /dev/ttyUSB0 串口设备,波特率115200
    if(hs1 < 0){
        printf("UART open error %d!\r\n",hs1);
        return -1;
    }
    else{
        printf("UART open success %d!\r\n",hs1);
    }

    char keyboard_input[10] = {};
    while(TRUE){

        int buffer_size = serialDataAvail(hs1);
        while(buffer_size){
            char c = serialGetchar(hs1);    // 从接收缓存区读取一个字节
            if((uart_flag == 0)&&(c == 0xA5)){
                uart_flag = 1;
            }
            else if((uart_flag == 1)&&(c == 0x5A)){
                uart_flag = 2;
            }
            else if((uart_flag == 1)&&(c != 0x5A)){
                uart_flag = 0;
                index = 0;
                memset(uart_rx_buffer,0,sizeof(uart_rx_buffer));
            }

            if(uart_flag > 0){
                uart_rx_buffer[index++] = c;
                if(index > sizeof(rx_data)+1){
                    uart_flag = 0;
                    index = 0;
                    memset(uart_rx_buffer,0,sizeof(uart_rx_buffer));
                }
            }

            if((uart_flag == 2)&&(c == 0x0F)){
                uart_flag = 3;
            }
            else if((uart_flag == 3)&&(c == 0xF0)){
                data_flag = 1;
                memcpy((&rx_data),uart_rx_buffer,sizeof(rx_data));
                uart_flag = 0;
                index = 0;
                memset(uart_rx_buffer,0,sizeof(uart_rx_buffer));
            }
            else if((uart_flag == 3)&&(c != 0xF0)){
                uart_flag = 0;
                index = 0;
                memset(uart_rx_buffer,0,sizeof(uart_rx_buffer));
            }
            buffer_size --;
        }

        if(data_flag){
            printf("%5d Ambient = %5.2f   Object = %5.2f\r\n",\
                  rx_data.serial_number,\
                  rx_data.ambient_temp,\
                  rx_data.object_temp);
            data_flag = 0;
        }
    }

    serialClose(hs1);                           // 关闭串口
    return 0;
}

因为使用到了wiringPi的库,所以编译的时候记得加-lwiringPi链接,执行的时候也需要使用管理员权限才可执行。

Arduino:

#include <Wire.h>
#include <Adafruit_MLX90614.h>

Adafruit_MLX90614 mlx = Adafruit_MLX90614();

struct sensor_data{
  uint16_t hand;
  uint16_t serial_number;
  float ambient_temp;
  float object_temp;
  uint16_t tail;
}sensor_data;

struct sensor_data tx_data;
uint16_t num =0;

void setup() {
  tx_data.hand = 0x5aa5;
  tx_data.tail = 0xf00f;
  Serial.begin(115200);
  mlx.begin();  
}

void loop() {
  tx_data.serial_number = num;
  tx_data.ambient_temp = mlx.readAmbientTempC();
  tx_data.object_temp = mlx.readObjectTempC();
  Serial.write((char*)(&tx_data),sizeof(tx_data));
  num++;
  delay(500);
}