这段英文主要是在介绍 C++20 的 constexpr 特性及其应用,理解如下:
介绍
探索 C++20 的 constexpr 特性如何实现以下功能:

  1. 在编译期生成数据
    利用 constexpr,程序可以在编译阶段计算结果,而不是在运行时,这样可以提升效率并减少运行时开销。
  2. 用于构造以下内容
    • 查找表(Lookup Tables)
      通过 constexpr 可以在编译期生成查找表,例如三角函数表、哈希表等,运行时直接使用,无需计算。
    • 配置熔丝(Configuration Fuses)
      可以在编译期确定一些配置选项或宏常量,从而在程序中控制功能开关或特性。
    • 压缩字符串(Compressed Strings)
      利用 constexpr 在编译期对字符串进行压缩或处理,减少运行时处理开销。
    • USB 描述符(USB Descriptors)
      可以在编译期生成 USB 描述符结构体的数据,从而减少初始化代码和运行时开销。
      总结:
      constexpr 简述(constexpr in Brief)
  3. 指定一个变量或函数可以出现在常量表达式中
    • constexpr 用来标记变量或函数,表示它们可以在编译期计算,即它们的值在编译阶段就能确定。
  4. 常量表达式可以在编译期求值
    • 被标记为 constexpr 的变量或函数,可以在编译时计算结果,而不是等到程序运行时。
  5. 用途举例(eg)
    • 数组大小(Array size):可以用 constexpr 变量定义数组长度,编译期就知道大小。
    • 非类型模板参数(non-type template parameter):模板参数必须在编译期确定,constexpr 就非常适合。
      简单理解:
      constexpr 的作用是告诉编译器“这个值/函数可以在编译期算出来”,从而提高性能并支持编译期计算。

constexpr 变量(constexpr Variable)

  1. 必须是字面量类型(Literal Type)
    也就是说,变量类型必须满足可以在编译期求值的条件,例如:
    • 标量类型(scalar):如 intcharfloat
    • 字面量数组(array of literals):数组元素都是字面量类型
    • 结构体/类/联合体(struct/class/union):但要满足一些限制(比如成员都是字面量类型)
    • 闭包类型(lambda)
  2. 必须立即初始化(Must be immediately initialised)
    • 声明时必须给出初始值,否则编译不通过
    • 例如:constexpr int x = 5; 是合法的,但 constexpr int x; 不行
  3. 隐含 const(Implies const)
    • constexpr 变量自动就是 const 的,不可修改
    • 等价于 const + 编译期可求值
      总结一句话
      constexpr 变量就是在编译期就能确定值的常量,类型和初始化都有限制,而且自动是只读的。
      好的,我来帮你把这个示例完整解释和补全:
#include <array>
#include <iostream>
constexpr int Base{0b0010'1000}; // 二进制字面量,等于 40
constexpr std::array<int, 3> Values{1, 2, 3}; // constexpr 数组,长度为3
int main() {
    // Base += 1;    // ERROR:Base 是 constexpr,自带 const,不能修改
    // 使用 constexpr 变量进行计算
    int result = Base + Values[1]; // Values[1] == 2
    std::cout << "Result: " << result << "\n"; // 输出 Result: 42
    return 0;
}

理解

  1. Base 是一个 constexpr int,值为二进制 00101000,即 40。
  2. Values 是一个 constexpr std::array<int, 3>,元素是 {1,2,3}
  3. constexpr 变量隐含 const,所以不能修改,比如 Base += 1; 会报错。
  4. Base + Values[1] 在运行时也能用,但如果用于编译期常量表达式,编译器可以在编译期计算结果。

constexpr 函数(Function)理解

  1. 返回类型要求
    • 必须返回 字面量类型(Literal Type),或者 void
    • 字面量类型包括:整数、浮点数、指针、constexpr 数组、constexpr 结构体/类(满足要求的)等。
  2. 返回值成员初始化
    • 如果返回的是结构体或类对象,所有成员必须在函数中被初始化
  3. 可以是构造函数
    • constexpr 构造函数允许在编译期创建对象。
  4. 参数要求
    • 所有参数必须是 字面量类型(如果有的话),这样才能保证编译期求值。
      简单总结:

constexpr function 就是能够在编译期求值的函数,返回类型和参数必须是“字面量”,并且必须完整初始化返回值。

#include <array>
#include <iostream>
// constexpr 函数
constexpr int make_bigger(int v) {
    return v * 100;
}
int main() {
    // 在编译期就计算结果
    constexpr int big_value = make_bigger(5); // 5 * 100 = 500
    // 用作数组大小(必须是编译期常量)
    std::array<int, make_bigger(2)> arr{}; // 数组大小 = 200
    std::cout << "big_value = " << big_value << std::endl;
    std::cout << "arr size = " << arr.size() << std::endl;
    return 0;
}

理解补充:

  1. make_biggerconstexpr,所以在编译期可以求值。
  2. big_value 被声明为 constexpr,值在编译时就已经确定为 500
  3. 数组大小也可以使用 constexpr 函数返回的值,因为它必须是编译期常量。
  4. 如果你在 main 中尝试对 big_value 修改,比如 big_value += 1;,编译器会报错,因为 constexpr 隐含了 const

consteval

  • 表示一个函数 必须 在编译期产生一个常量值(compile-time constant)。
  • constexpr 类似,但更严格:
    • constexpr 函数 可以 在编译期求值,也可以在运行期求值。
    • consteval 函数 只能在编译期求值,如果在运行期调用会报错。
  • consteval 隐含了 constexpr 的特性。
    简单例子:
#include <array>
consteval int compile_time_only(int x) { return x * 2; }
// 生成编译期数组
consteval auto generate_array(int size) {
    std::array<int, 5> arr{};
    for (int i = 0; i < size; ++i) {
        arr[i] = compile_time_only(i);
    }
    return arr;
}
int main() {
    constexpr int a = compile_time_only(5);  // OK,结果是 10
    constexpr auto arr = generate_array(5);  // OK,生成编译期数组 {0, 2, 4, 6, 8}
    // 以下都会报错:
    // int x = 5;
    // int b = compile_time_only(x); // ERROR,x 不是常量
    // auto arr2 = generate_array(x); // ERROR,x 不是常量
    return 0;
}

理解点

  • constexpr = 可编译期可运行期
  • consteval = 必须编译期

constinit 是 C++20 引入的关键字,用来标记 变量必须经过静态初始化(static initialization),也就是说:

  1. 静态存储期变量(全局、static 局部变量)在程序加载时就完成初始化,而不是等到运行时才初始化。
  2. 初始化值 必须是常量表达式,这样编译器可以在编译期计算出来并存入程序二进制中。
  3. constexpr 不同,constinit 不要求变量是 const,所以变量可以修改。
  4. 你也可以同时写 constinit const,表示静态初始化且不可修改。
    示例解读:
constinit int foo = 10;         // 静态初始化,可修改
constinit const int bar = 1;    // 静态初始化,不能修改
int main() { 
    foo += 1;       // OK,可以修改
    // bar += 1;    // ERROR,const 修饰不可修改
}

总结:

  • foo:静态初始化,存储在程序二进制中,可在运行时修改。
  • bar:静态初始化,存储在程序二进制中,不可修改(编译器会报错)。

在编译期构建各种资源 的场景,用 C++20 的 constexpr / consteval / constinit 技术可以在编译期生成数据,从而优化程序运行时性能。理解如下:

  1. Lookup Tables(查找表)
    • 在编译期生成一张表,存储预先计算好的值。
    • 运行时直接访问表中的数据,而不需要重复计算。
    • 例子:三角函数表、颜色映射表、哈希映射表。
  2. Configuration Fuses(配置熔丝 / 配置开关)
    • 编译期确定程序的配置选项或特性开关。
    • 类似硬件中的“熔丝”,一旦编译就固定。
    • 例子:启用/禁用某个功能模块的标志。
  3. Compressed String Tables(压缩字符串表)
    • 将字符串在编译期压缩存储,减少程序占用的内存。
    • 运行时解压或直接按压缩格式使用。
    • 例子:游戏中的多语言文本表、日志消息表。
  4. USB Descriptor(USB 描述符)
    • USB 设备需要一系列描述符(设备、配置、接口、端点等)来告诉主机自己有哪些功能。
    • 可以在编译期生成描述符数组,运行时直接用,无需动态构建。
      总结:这些都是把数据生成和处理提前到编译期的例子,目的是减少运行时开销,提高程序效率。
      如果你需要,我可以给你画一张 “编译期生成资源 vs 运行时生成资源”的对比图,让概念更直观。
      这段内容是在解释 为什么要在编译期生成数据或使用 constexpr / consteval 技术。理解如下:
  5. No external tools(不依赖外部工具)
    • 不需要额外的脚本或生成工具就能完成数据生成。
    • 全部在 C++ 编译器内部完成,简化了构建流程。
  6. Can enforce correctness(可以保证正确性)
    • 编译器在编译期就能检查数据的正确性。
    • 比如表格、配置或描述符在编译期就验证是否符合预期,避免运行时错误。
  7. Pushing limits is fun(挑战极限很有趣)
    • 利用编译器的编译期计算能力可以实现一些看似不可能的操作。
    • 对程序员来说,这是探索 C++ 编译期能力和极限的一种乐趣。
      这段内容是在讲 如何用 C++20 的 constexpr 构建一个编译期生成的线性插值查找表(Lookup Table)。理解如下:

核心目标

  • 构建一个 查找表(Lookup Table) 来存储函数值,例如正弦函数。
  • 通过查找表在运行时快速得到函数值,同时支持 线性插值
  • 完全在 编译期生成,保证性能和正确性。

步骤解析

  1. 辅助函数
constexpr float radians(float degrees) {
    return (std::numbers::pi / 180.0) * degrees;
}
constexpr float sine_deg(float degrees) {
    return std::sin(radians(degrees));
}
  • 将角度转换为弧度。
  • 定义 sine_deg 函数返回角度对应的正弦值。
  • 注意:constexpr std::sin 在 GCC 是扩展支持(不是标准)。
  1. 生成查找表
constexpr auto DegreeSineTable = LerpTable<float, 32>::make_table(0.0f, 90.0f, sine_deg);
  • 创建一个 32 个条目的查找表。
  • make_table(min, max, function):在 [min, max] 区间内,用 function 填充表格。
static_assert(DegreeSineTable(30.0f) >= 0.499f && DegreeSineTable(30.0f) <= 0.501f);
  • 编译期断言,确保插值正确。
  1. LerpTable 结构
template<typename T, std::size_t NUM_ENTRIES>
struct LerpTable {
    struct Entry {
        T input;
        T output;
    };
    std::array<Entry, NUM_ENTRIES> entries;
    constexpr static LerpTable make_table(T min, T max, T(&function)(T)) {
        LerpTable table;
        table.entries[0] = {min, function(min)};
        table.entries[NUM_ENTRIES-1] = {max, function(max)};
        T const step = (max - min) / NUM_ENTRIES;
        for(std::size_t i = 1; i < NUM_ENTRIES-1; ++i) {
            T const a = step * i;
            table.entries[i] = {a, function(a)};
        }
        return table;
    }
    constexpr T operator()(T in) const {
        auto const clamped_in = std::clamp(in, entries[0].input, entries[NUM_ENTRIES-1].input);
        auto entry_itr = std::lower_bound(entries.begin(), entries.end(), clamped_in,
            [](auto const &e, auto const &v){ return e.input < v; });
        auto const entry = *entry_itr;
        if(entry.input == clamped_in) return entry.output;
        auto const prev_entry = *std::prev(entry_itr);
        auto const t = (clamped_in - prev_entry.input) / (entry.input - prev_entry.input);
        return std::lerp(prev_entry.output, entry.output, t);
    }
};
  • Entry:存储单个表格条目,包含输入和输出。
  • make_table:编译期生成表格。
    • 先填充首尾条目。
    • 用线性步长填充中间条目。
  • operator():运行时查表并线性插值。
    • std::clamp 限制输入范围。
    • std::lower_bound 找到大于等于输入的条目。
    • 如果输入正好命中条目,直接返回输出。
    • 否则在相邻条目之间线性插值(std::lerp)。

总结

  • 使用 constexpr 可以 在编译期生成完整查找表
  • 运行时查表 + 插值,速度快,避免重复计算复杂函数。
  • 代码灵活,可复用不同函数或不同条目数。
#include <array>
#include <algorithm>
#include <cmath>
#include <iostream>
#include <numbers>  // C++20 提供 std::numbers::pi
#include <cassert>
// ---------- 辅助函数 ----------
// 将角度转换为弧度
constexpr float radians(float degrees) { return (std::numbers::pi / 180.0f) * degrees; }
// 计算角度的正弦值(度数制输入)
// 注意:std::sin 在标准 C++20 中不是 constexpr
// GCC/Clang 有扩展允许在 constexpr 中使用
constexpr float sine_deg(float degrees) { return std::sin(radians(degrees)); }
// ---------- 查找表模板 ----------
// 线性插值查找表模板
// T: 数据类型(float/double)
// NUM_ENTRIES: 表格条目数
template <typename T, std::size_t NUM_ENTRIES>
struct LerpTable {
    // 单个表格条目
    struct Entry {
        T input;   // 输入值
        T output;  // 对应的函数值
    };
    std::array<Entry, NUM_ENTRIES> entries;  // 存储表格条目
    // 编译期生成表格
    // 参数:
    // min/max: 范围
    // function: 计算输出值的函数
    constexpr static LerpTable make_table(T min, T max, T (&function)(T)) {
        LerpTable table{};  // 初始化空表格
        // 明确初始化首尾
        table.entries[0] = {min, function(min)};
        table.entries[NUM_ENTRIES - 1] = {max, function(max)};
        // 每个条目的步长
        T const step = (max - min) / static_cast<T>(NUM_ENTRIES - 1);
        // 填充中间条目
        for (std::size_t i = 1; i < NUM_ENTRIES - 1; ++i) {
            T const a = min + step * i;
            table.entries[i] = {a, function(a)};
        }
        return table;  // 返回完整表格
    }
    // 运行时查表 + 线性插值
    // 输入任意值,返回近似函数值
    constexpr T operator()(T in) const {
        // 限制输入范围在表格区间
        auto const clamped_in = std::clamp(in, entries[0].input, entries[NUM_ENTRIES - 1].input);
        // 找到第一个大于等于 clamped_in 的条目
        auto entry_itr = std::lower_bound(entries.begin(), entries.end(), clamped_in,
                                          [](auto const &e, auto const &v) { return e.input < v; });
        auto const entry = *entry_itr;
        // 如果恰好匹配表格值,直接返回
        if (entry.input == clamped_in) return entry.output;
        // 否则进行线性插值
        auto const prev_entry = *std::prev(entry_itr);
        auto const t = (clamped_in - prev_entry.input) / (entry.input - prev_entry.input);
        return std::lerp(prev_entry.output, entry.output, t);  // 插值返回近似值
    }
};
// ---------- 编译期生成查找表 ----------
// 生成 0~90 度的 32 条正弦表
// DegreeSineTable 是 constexpr,理论上可以在编译期生成
constexpr auto DegreeSineTable = LerpTable<float, 32>::make_table(0.0f, 90.0f, sine_deg);
// ---------- 编译期检查 ----------
// static_assert 用于在编译期验证结果是否正确
// 确保 DegreeSineTable(30.0f) 的值在 0.499~0.501 之间
static_assert(DegreeSineTable(30.0f) >= 0.499f && DegreeSineTable(30.0f) <= 0.501f);
// ---------- 测试输出 ----------
int main() {
    std::cout << "sin(0)   ~ " << DegreeSineTable(0.0f) << "\n";
    std::cout << "sin(30)  ~ " << DegreeSineTable(30.0f) << "\n";
    std::cout << "sin(45)  ~ " << DegreeSineTable(45.0f) << "\n";
    std::cout << "sin(60)  ~ " << DegreeSineTable(60.0f) << "\n";
    std::cout << "sin(90)  ~ " << DegreeSineTable(90.0f) << "\n";
    return 0;
}

好的,我帮你把这一段内容用详细解释:

配置熔丝(Config Fuses)

  1. 作用
    配置熔丝是在处理器启动前就初始化硬件的一种机制。
    • 通常存放在 Flash 固定位置
    • 以位图(bit-mapped)的方式存储“魔法值”(magic values)
    • 可以设置的内容包括:
      • 时钟(clocks)
      • 内存段(memory segments)
      • 看门狗定时器(watchdog timer)
      • JTAG 调试功能
      • 代码安全(code security)
      • 以及更多硬件功能
  2. 实现方式
    • 通常依赖 厂商特定的编译器扩展
      • 比如使用 #pragma 或者特殊宏来设置熔丝
    • 对应的位值通常用 C 风格的 #define 定义
    • 缺点:没有严格的验证机制,配置可能非常复杂,容易出错
      简单理解:Config Fuses 就像硬件的“启动开关”,在程序真正运行前就决定了处理器和外设的行为

更好的配置熔丝方法(Strongly typed Config Fuses)

目标
  1. 强类型化所有配置寄存器
    • enum class 表示不同寄存器的可能值
    • 避免直接操作魔法值,减少出错
  2. 提供“构建器”对象(Builder)
    • 用面向对象的方法设置每个寄存器的值
  3. 在编译期生成 constinit 对象
    • 数据在编译时生成,并存储在程序二进制中
  4. 使用段(segment)和链接脚本将其放入 Flash
    • 控制它在内存中的位置
代码讲解
enum class WatchDogMode { Disabled, Enabled_10ms /* ... */ };
enum class OscillatorMode { InternalRC, Crystal /* ... */ };
class ConfigBuilder 
{
public: 
    // 设置看门狗模式
    constexpr void set_watchdog(WatchDogMode wtd) { m_Wdt = wtd; } 
    // 设置振荡器模式
    constexpr void set_oscillator(OscillatorMode osc) { m_Osc = osc; } 
    // 编译期生成最终寄存器数组
    constexpr auto build() 
    { 
        std::array<std::uint32_t, 2> registers; 
        auto wdt = static_cast<std::uint32_t>(m_Wdt); 
        auto osc = static_cast<std::uint32_t>(m_Osc); 
        // 生成寄存器值(假设寄存器 32 位,2 个寄存器)
        // 并生成值的补码
        registers[0] = (wdt << 24u) | (~wdt & 0x0000'00FF); 
        registers[1] = (osc << 24u) | (~osc & 0x0000'00FF); 
        return registers; 
    }
private: 
    WatchDogMode m_Wdt {WatchDogMode::Disabled}; 
    OscillatorMode m_Osc {OscillatorMode::InternalRC}; 
};
使用方法
[[gnu::section(".config_registers"), gnu::used]] 
constinit auto const CONFIG_REGISTERS = []{ 
    ConfigBuilder cfg; 
    cfg.set_watchdog(WatchDogMode::Enabled_10ms); 
    cfg.set_oscillator(OscillatorMode::Crystal); 
    return cfg.build(); 
}();
  • captureless lambdaconstexpr,因此可以在编译期执行
  • constinit const 保证对象不可变,并在编译期生成
  • [[gnu::section(".config_registers")]] 将其放入 .config_registers 内存段(Flash)
  • [[gnu::used]] 防止编译器优化掉未被直接引用的对象
总结

这个方法相比传统 #define 或魔法值方式更安全、更可读,而且:

  • 编译期生成,无需运行期初始化
  • 强类型化,减少寄存器设置错误
  • 可放入特定 Flash 段,便于硬件读取

Config Fuses 编译期生成例子 写成一份完整可编译的 C++20 代码,并加上详细注释:

#include <array>
#include <cstdint>
// ---------- 配置寄存器类型定义 ----------
enum class WatchDogMode : uint8_t {
    Disabled = 0,
    Enabled_10ms = 1,
    Enabled_100ms = 2,
    // 其他模式可扩展
};
enum class OscillatorMode : uint8_t {
    InternalRC = 0,
    Crystal = 1,
    External = 2,
    // 其他模式可扩展
};
// ---------- 配置构建器 ----------
class ConfigBuilder {
public:
    // 设置看门狗寄存器
    constexpr void set_watchdog(WatchDogMode wtd) { m_Wdt = wtd; }
    // 设置振荡器寄存器
    constexpr void set_oscillator(OscillatorMode osc) { m_Osc = osc; }
    // 编译期生成最终寄存器值
    constexpr auto build() const {
        // 假设有两个 32 位寄存器
        std::array<std::uint32_t, 2> registers{};
        // 将枚举转成整数
        auto wdt = static_cast<std::uint32_t>(m_Wdt);
        auto osc = static_cast<std::uint32_t>(m_Osc);
        // 将寄存器值序列化
        // 假设高 8 位存储寄存器值,低 8 位存储其补码
        registers[0] = (wdt << 24u) | (~wdt & 0x0000'00FF);
        registers[1] = (osc << 24u) | (~osc & 0x0000'00FF);
        return registers;
    }
private:
    WatchDogMode m_Wdt{WatchDogMode::Disabled};        // 默认值
    OscillatorMode m_Osc{OscillatorMode::InternalRC};  // 默认值
};
// ---------- 编译期生成配置寄存器 ----------
[[gnu::section(".config_registers"), gnu::used]]
constinit auto const CONFIG_REGISTERS = [] {
    ConfigBuilder cfg;
    // 设置寄存器值
    cfg.set_watchdog(WatchDogMode::Enabled_10ms);
    cfg.set_oscillator(OscillatorMode::Crystal);
    // 返回寄存器数组(编译期生成)
    return cfg.build();
}();
// ---------- 简单的调试函数(可选) ----------
// 在嵌入式系统中,你可能会用UART或其他方式输出
void debug_print_config() {
    // 这里可以通过UART、SWO或其他调试接口输出
    // 现在只是一个占位符
    volatile auto reg0 = CONFIG_REGISTERS[0];
    volatile auto reg1 = CONFIG_REGISTERS[1];
    (void)reg0;  // 避免未使用变量警告
    (void)reg1;
}
int main() {
    debug_print_config();
    return 0;
}

注释说明

  1. enum class
    • 用强类型枚举表示硬件寄存器的可能值
    • 避免直接使用魔法数,增加可读性和安全性
  2. ConfigBuilder
    • 提供 constexpr 方法设置寄存器
    • build() 方法在编译期生成最终序列化寄存器数组
  3. constinit auto const CONFIG_REGISTERS
    • lambda 是无捕获的,因此是 constexpr
    • constinit 保证在编译期初始化
    • [[gnu::section(".config_registers")]] 将其放到 Flash 特定段
    • [[gnu::used]] 防止编译器优化掉
  4. 寄存器序列化逻辑
    • 高 8 位存储实际值
    • 低 8 位存储其补码(仅示例)

Linker Script(链接脚本)作用

  • 定义最终二进制的内存布局:告诉链接器(ld)不同的段(section)应该放到内存的哪一块区域。
  • 映射段到内存区域:比如 .text 放到程序代码区,.data 放到 RAM 数据区,.config_registers 放到硬件要求的固定 Flash 地址。
  • 一般情况不用关心,但是做底层硬件配置或裸机程序时必须要用。

目标

  • 我们的 CONFIG_REGISTERS 需要放在 0x0100 地址,这通常是 MCU 或硬件要求的固定地址。
  • 通过 linker script 可以实现把 .config_registers 段映射到 0x0100。

示例 linker script 分析

ENTRY(main)
MEMORY
{
    config    : ORIGIN = 0x0100, LENGTH = 0x8
    prog (rx) : ORIGIN = 0x1000, LENGTH = 0x100000
}
SECTIONS
{
    /* 配置寄存器段 */
    .config_registers : {
        KEEP(*(.config_registers))
    } > config
    /* 程序代码段 */
    .text : {
        *(.text)
        *(.text.*)
    } > prog
    /* 数据段 */
    .data : { *(.data) } > prog
    .bss  : { *(.bss) } > prog
    /* 丢弃其他未映射段 */
    /DISCARD/ : { *(.comment) *(.note.GNU-stack) }
}

要点:

  1. MEMORY 定义了内存区域:
    • config:从 0x0100 开始,长度 8 字节。
    • prog:从 0x1000 开始,长度 0x100000(程序代码区)。
  2. SECTIONS 定义段的映射:
    • .config_registers 段内容放入 config 区。
    • .text/.data/.bss 放入 prog 区。
    • /DISCARD/ 丢弃其他未使用的段。
  3. KEEP(*(.config_registers)) 确保即使程序未显式使用该段,也不会被编译器优化掉。

编译和链接

g++ -O3 --std=c++20 -c main.cpp -o main.o
g++ -T config_register.ld main.o -o config.out
or
ld -o config.out -T config_register.ld main.o
objdump -ds config.out
# 查看段信息
objdump -h config.out
# 查看配置寄存器的内容
objdump -s -j .config_registers config.out
# 或者查看完整的内存映射
objdump -t config.out | grep CONFIG_REGISTERS

你贴的 objdump 输出信息可以帮我们分析 CONFIG_REGISTERS 的存放和链接情况。下面逐条理解:

1⃣ .config_registers 内容

Contents of section .config_registers:
 0100 fe000001 fe000001
  • .config_registers 段从 地址 0x0100 开始。
  • 内容是 fe 00 00 01 fe 00 00 01,一共 8 字节。
  • 对应你在 C++ 里用 ConfigBuilder 构造的两个寄存器值。
  • 表明 CONFIG_REGISTERS 已经编译期生成,存放在你自定义段里。

2⃣ .text 内容

0000000000001020 <main>:
    1020: ... 
  • 代码段从 地址 0x1000 开始。
  • 说明你的 linker script 成功将 .text 放在 prog 内存区域。
  • 这里的 main() 代码和 debug_print_config() 也在 .text 段。

3⃣ 段表 objdump -h

Idx Name          Size      VMA      LMA  File off
0 .config_registers 00000008  0x100  0x100 0x1100
1 .text             0x3f    0x1000 0x1000 0x2000
2 .note.gnu.property 0x20    0x1040 0x1040 0x2040
  • .config_registers 大小为 8 字节,VMA/LMA 都是 0x0100
  • .text 段大小 0x3f,VMA/LMA 0x1000。
  • 说明 linker script 的 内存映射正确.config_registers 在 0x0100,.text 在 0x1000。

4⃣ 符号表

0000000000000100 l     O .config_registers  0x8  _ZL16CONFIG_REGISTERS
  • _ZL16CONFIG_REGISTERS 是 C++ 内部符号名(名字被编译器修饰)。
  • 本地符号(l),在 .config_registers 段偏移 0x100,大小 8。
  • 表明 你的 constinit 对象成功放进了指定段

5⃣ 总结

  1. CONFIG_REGISTERS 已经在 编译期生成,存放在 .config_registers 段。
  2. 段起始地址和 linker script 设置一致(0x0100)。
  3. 对象的大小是 8 字节,符合你设计的 2 个 32-bit 寄存器。
  4. 你现在的 ELF 文件中 .text.config_registers 成功分离,可以裸机读取。
    如果你想,我可以帮你画一张 内存布局图,把 .config_registers.text.data 等段直观显示出来,这样更容易理解整个 linker 的效果。

linker script 是一个完整的 ELF 链接脚本,它定义了内存布局和各个段的映射规则。下面我逐条解释:

1⃣ ENTRY(main)

ENTRY(main)
  • 指定 程序入口main 函数。
  • 链接器会将 ELF 文件的入口地址设置为 main 的地址。

2⃣ MEMORY 定义内存区域

MEMORY {
    config    : ORIGIN = 0x0100, LENGTH = 0x8
    prog (rx) : ORIGIN = 0x1000, LENGTH = 0x100000
}
  • config:存放配置寄存器(CONFIG_REGISTERS),起始地址 0x0100,大小 8 字节。
  • prog:程序和只读数据区,起始地址 0x1000,大小 1MB。
  • (rx) 表示 prog 区是可读(r)和可执行(x)的。

3⃣ SECTIONS 定义段的映射

3.1 配置寄存器段
.config_registers : {
    KEEP(*(.config_registers))
} > config
  • .config_registers 段中的所有内容放入 config 内存区域
  • KEEP 防止链接器优化掉未使用的符号(比如你的 constinit 对象)。
3.2 程序代码段
.text : {
    *(.text)
    *(.text.*)
} > prog
  • 所有 .text 段及扩展段都放到 prog 区。
  • 存放函数代码。
3.3 只读数据段
.rodata : {
    *(.rodata)
    *(.rodata.*)
} > prog
  • 只读数据(常量、字符串等)也放入 prog 区,而不是 config
  • 这样 .config_registers 区不会被常量数据污染。
3.4 数据段
.data : { *(.data) } > prog
.bss  : { *(.bss) } > prog
  • .data:已初始化的全局变量。
  • .bss:未初始化的全局变量。
  • 都放在 prog 内存区域。
3.5 丢弃其他段
/DISCARD/ : {
    *(.comment)
    *(.note.GNU-stack)
    *(.eh_frame)
    *(.eh_frame_hdr)
}
  • 这些段不需要放入最终 ELF(调试信息、异常处理帧、栈标记等)。
  • 链接器会直接丢弃。

总结理解

  1. config 区域只放 CONFIG_REGISTERS,大小固定 8 字节,起始地址 0x0100。
  2. prog 区域放程序代码 .text、只读数据 .rodata、初始化变量 .data、未初始化变量 .bss
  3. 丢弃段确保 ELF 文件干净,没有不必要的调试/异常信息。
  4. 使用 KEEP 保证 constinit 对象不会被优化掉。
    如果你想,我可以帮你画一个 内存布局示意图,把 configprog、各段大小、地址都标出来,一目了然。

如何在编译期根据输入动态生成不同大小或类型的数据,核心思路是利用 C++20 的 constexpr lambda。我来逐条理解:

1⃣ 问题背景

  • 之前你生成的资源(如查找表、配置寄存器)大小和类型固定
  • 现在想要:根据输入的不同,生成不同大小或类型的资源,例如:
    • USB 配置描述符:接口数量和端点数量不同 → 数据大小不同
    • 压缩字符串:长度取决于内容而非原始长度

2⃣ 编译期返回大小与类型

  • std::array 的大小必须是 编译期常量
  • 模板参数的类型或大小也必须在编译期确定。
  • 因此:
    • 要在编译期生成资源
    • 必须通过 constexpr 函数计算长度和类型
  • 但编译器不能在运行时推导类型或大小,只能使用类型信息。

3⃣ 用户数据如何传入 constexpr 函数

How can we pass user-supplied data into a constexpr function? Lambdas!

  • C++20 支持 constexpr lambda
    • Lambda 的 operator() 可以是 constexpr
    • 每个 lambda 是独一无二的类型,返回类型在编译期已知
  • 优点:
    • 可以把用户提供的数据封装成 lambda
    • 编译器在编译期调用 lambda,生成需要的资源

4⃣ 示例:every_second_item 函数

constexpr auto every_second_item(auto lambda)
{ 
    constexpr auto data = lambda();               // 调用 lambda 获取数据
    constexpr auto length = data.size() / 2;      // 计算输出数组大小
    std::array<int, length> output;              // 创建输出数组
    for(std::size_t i = 0; i < output.size(); ++i) 
        output[i] = data[i*2];                   // 每隔一个取值
    return output;                               // 返回数组
}
  • 输入:一个 lambda,返回 std::array 或类似容器
  • 输出:每隔一个元素的新数组
  • 大小:通过 data.size() / 2 计算,在编译期确定

5⃣ 调用示例

constexpr auto test = every_second_item([] { 
    return std::to_array<int>({1, 2, 3, 4, 5, 6}); 
}); 
int main() { 
    return test.size(); // == 3
}
  • Lambda 返回数组 {1,2,3,4,5,6}
  • every_second_item 取每隔一个元素 → {1,3,5}
  • 数组大小为 3,编译期已确定
  • main() 返回 3

总结理解

  1. 编译期生成动态大小资源:利用 constexpr lambda,将用户数据封装成 编译期可调用对象
  2. 返回类型和大小可推导:lambda 的返回类型在编译期已知,可以在模板或 std::array 中使用。
  3. 模板 vs lambda
    • 模板函数也可实现类似功能,但 lambda + auto 参数语法更清晰、可读性好。
  4. 适用场景:
    • USB 配置描述符、压缩字符串表、查找表等。
#include <array>
// -------------------------------------------
// 库函数:生成每隔一个元素的数组
// 参数:lambda 返回一个 std::array
// 返回:包含每隔一个元素的新数组
// -------------------------------------------
constexpr auto every_second_item(auto lambda) {
    // 调用 lambda 生成原始数据数组
    constexpr auto data = lambda();
    // 新数组长度 = 原数组长度 / 2
    constexpr auto length = data.size() / 2;
    // 创建输出数组
    std::array<int, length> output{};
    // 填充输出数组,每隔一个元素取一次
    for (std::size_t i = 0; i < output.size(); ++i)
        output[i] = data[i * 2];
    return output;
}
// -------------------------------------------
// 使用库函数生成数组
// 这里 lambda 返回 {1,2,3,4,5,6}
// 生成数组应为 {1,3,5}
// -------------------------------------------
constexpr auto test = every_second_item([] {
    return std::to_array<int>({1, 2, 3, 4, 5, 6});
});
// -------------------------------------------
// 编译期验证
// static_assert 用于在编译期检查数组长度和内容
// 如果不符合条件,编译器会报错
// -------------------------------------------
static_assert(test.size() == 3, "长度应该是3");
static_assert(test[0] == 1, "第一个元素应该是1");
static_assert(test[1] == 3, "第二个元素应该是3");
static_assert(test[2] == 5, "第三个元素应该是5");
// -------------------------------------------
// main 函数
// 仅用于保证编译通过
// -------------------------------------------
int main() {
    return 0;  // 编译期验证通过则能编译
}

这一部分内容讲的是 字符串压缩表(Compressed String Table) 的实现思路及使用方法,理解如下:

10.1 构建压缩字符串表

  1. 定义键(Key)枚举
    enum class Key { String_1, String_2, String_3 };
    
    每个字符串都有一个唯一的枚举键,用于索引。
  2. 生成映射数组(buildMapStrings)
    static constexpr auto buildMapStrings = [] {
        return std::to_array<squeeze::KeyedStringView<Key>>({
            { Key::String_3, "There is little point to using short strings in a..." },
            { Key::String_1, "We will include some long strings in the table to..." }
        });
    };
    
    • 将每个键映射到对应字符串(注意可以乱序或缺少某些键)。
    • 使用 lambda 生成数组,这样可以在编译期处理。
  3. 创建 StringMap
    constexpr auto map = squeeze::StringMap<Key>(buildMapStrings);
    
    • StringMap 内部维护一个 压缩字符串表Key -> 索引映射
    • 支持在编译期生成结构,并在运行期高效访问。
  4. 访问压缩字符串
    static constexpr auto str1 = map.get(Key::String_1);
    for (auto const &c : str1) { std::putchar(c); }
    std::putchar('\n');
    
    • 返回一个迭代器,逐字符访问压缩字符串。
    • 避免一次性解压整个字符串,节省内存和元数据。

10.2 StringMap & StringTable 设计

  1. StringTable
    • 实现对压缩字符串的简单索引访问。
    • 保存压缩数据、起始位置和长度。
  2. StringMap
    • 维护 Key -> 索引 映射。
    • 用户传入的字符串通过 lambda 提供 std::string_view
  3. 特点
    • 编译期生成,最小元数据。
    • 迭代器接口,无需一次性解压。
    • 适合长字符串和大量字符串的情况。

10.3 编译产物示例

使用 objdump 查看 .compressedmap 段内容:

$ objdump -s --section=.compressedmap example
Contents of section .compressedmap:
 20c0 00000000 00000000 01000000 00000000  ................
 20d0 02000000 00000000 00000000 00000000  ................
 20e0 00000000 00000000 4a000000 00000000  ........J.......
 ...
  • 前半部分:存储键到压缩数据的位置/长度映射。
  • 后半部分:压缩后的位流(bitstream)。
  • 每个键映射到压缩数据,压缩树/字典编码在数组中。

总结理解

  1. 目标:把字符串在编译期压缩,生成固定结构,减少运行时开销。
  2. 实现方法
    • 枚举键 Key
    • 使用 lambda 构建键-字符串数组
    • 编译期生成 StringMapStringTable
    • 支持迭代器访问压缩字符串
  3. 优点
#include <array>
#include <cstdio>
#include "squeeze-main/include/squeeze/squeeze.h"
// ------------------------ 用户代码 ------------------------
// 定义字符串键
enum class Key { String_1, String_2, String_3 };
// 编译期构建字符串映射
static constexpr auto buildMapStrings = [] {
    return std::to_array<squeeze::KeyedStringView<Key>>(
        {{Key::String_3, "There is little point to using short strings."},
         {Key::String_1, "We will include some long strings in the table."},
         {Key::String_2, "Another example string for testing."}});
};
// 编译期生成 StringMap
constexpr auto map = squeeze::StringMap<Key>(buildMapStrings);
// ------------------------ 添加自定义 section ------------------------
// 将字符串数据放入 .compressedmap section
__attribute__((section(".compressedmap"))) inline constexpr auto compressed_data =
    map;  // 注意:map 必须可用作 constexpr 对象
// 防止优化掉
volatile auto keep = &compressed_data;
// ------------------------ 测试 ------------------------
int main() {
    // 编译期访问
    static constexpr auto str1 = map.get(Key::String_1);
    static_assert(str1.size() > 0, "字符串长度应大于0");
    // 运行期打印
    for (auto c : str1) std::putchar(c);
    std::putchar('\n');
    static constexpr auto str3 = map.get(Key::String_3);
    for (auto c : str3) std::putchar(c);
    std::putchar('\n');
    return 0;
}

理解

  1. StringMap 之下是 StringTable
    • StringTable 是一个简单的表格,用于按索引访问压缩后的字符串
  2. 通过 Key -> index 映射封装
    • 在 StringTable 之上,用 StringMap 提供键到索引的映射,方便通过键快速找到对应字符串。
  3. StringMap 和 StringTable 都可以单独使用
    • 你既可以直接用索引访问 StringTable,也可以通过键访问 StringMap。
  4. 字符串(和键)通过 lambda 传递
    • 使用 std::string_view 的 lambda 来传入字符串和键,避免不必要的拷贝
  5. 提供迭代器接口
    • 允许你遍历字符串而无需一次性解压所有内容,节省内存和计算资源。
  6. 适用于长字符串,元数据开销最小
    • 对于很长的字符串,这种设计能保持索引和元数据的占用很低,效率高。

总结理解

这个系统的设计目的是高效存储和访问大量长字符串,它通过:

  • 压缩存储(StringTable)
  • 键到索引映射(StringMap)
  • 延迟访问(迭代器和 lambda)
    实现了最小内存占用和快速查找
    objdump -s --section=.compressedmap example
xiaqiu@xz:~/test/build/CppCon/day359/code$ objdump -s --section=.compressedmap example 
example:     file format elf64-x86-64
Contents of section .compressedmap:
 22a0 00000000 00000000 01000000 00000000  ................
 22b0 01000000 00000000 02000000 00000000  ................
 22c0 02000000 00000000 00000000 00000000  ................
 22d0 00000000 00000000 2d000000 00000000  ........-.......
 22e0 af000000 00000000 2f000000 00000000  ......../.......
 22f0 72010000 00000000 23000000 00000000  r.......#.......
 2300 230f45ff 9a4f263a d82f4b6b 78bfd2f3  #.E..O&:./Kkx...
 2310 a6ec517f e5e6311a f9ccfec6 3374d0b3  ..Q...1.....3t..
 2320 094d5be9 517fe5fa 5b7a946e 303107d6  .M[.Q...[z.n01..
 2330 e8a145d4 9d86448f fa2b8d99 9638feca  ..E...D..+...8..
 2340 0c000100 02000100 03000400 01000500  ................
 2350 06000100 07000800 01000900 0a000100  ................
 2360 0b000c00 01000d00 0e000100 65000000  ............e...
 2370 00007400 00000000 0f001000 01001100  ..t.............
 2380 12000100 13001400 01002000 00000000  .......... .....
 2390 15001600 01001700 18000100 67000000  ............g...
 23a0 00007200 00000000 6f000000 00001900  ..r.....o.......
 23b0 1a000100 1b001c00 01006c00 00000000  ..........l.....
 23c0 1d001e00 01006e00 00000000 73000000  ......n.....s...
 23d0 00006900 00000000 1f002000 01006800  ..i....... ...h.
 23e0 00000000 21002200 01002300 24000100  ....!."...#.$...
 23f0 25002600 01002700 28000100 64000000  %.&...'.(...d...
 2400 00006100 00000000 29002a00 01007500  ..a.....).*...u.
 2410 00000000 2b002c00 01002d00 2e000100  ....+.,...-.....
 2420 70000000 00002f00 30000100 6d000000  p...../.0...m...
 2430 00002e00 00000000 41000000 00006200  ........A.....b.
 2440 00000000 77000000 00007800 00000000  ....w.....x.....
 2450 66000000 00006300 00000000 54000000  f.....c.....T...
 2460 00005700 00000000                    ..W.....        
xiaqiu@xz:~/test/build/CppCon/day359/code$ 

你贴的 objdump -s --section=.compressedmap example 输出显示的是 ELF 文件里 .compressedmap section 的原始内容。我们来逐段理解:

1. Section 概览

Contents of section .compressedmap:
 22a0 00000000 00000000 01000000 00000000  ................
 22b0 01000000 00000000 02000000 00000000  ................
 ...
  • 左边 22a0, 22b0 … 是 section 内的偏移地址
  • 中间一大段 00000000 01000000 …16 进制的原始字节内容
  • 右边是 ASCII 尝试解释,非可打印字符显示为 .

2. 数据分析

  1. 前几行
    00000000 00000000 01000000 00000000
    01000000 00000000 02000000 00000000
    
    • 很可能是 索引或元数据
      • 每 4 或 8 字节表示一个字符串的 ID、偏移量或长度。
      • 对应你之前的 StringMap<Key> 内部的 key -> index 映射表。
  2. 中间部分
    2300 230f45ff 9a4f263a d82f4b6b 78bfd2f3 …
    
    • 这一块是 压缩后的字符串内容
    • 因为 squeeze 库使用压缩,所以你看到的不是 ASCII 可读文本,而是压缩后的二进制。
  3. 后半部分
    2400 00006100 00000000 29002a00 01007500 …
    
    • 可能是 其他索引、表格或填充
    • ELF section 通常对齐 16 或 32 字节,所以会有很多零填充。

3. 总结理解

  1. .compressedmap section 保存的是:
    • StringMap 的索引信息(key -> string offset/length)
    • 压缩后的字符串数据
    • 对齐或 padding
  2. 不是直接可读的字符串:
    • 你看到的都是二进制表示,因为 squeeze 压缩了字符串。
    • 这是为了节省内存,尤其是存储长字符串。
  3. 数据结构的意义:
    • 程序运行时可以通过索引表找到对应字符串的偏移,再解压得到原文。
    • 静态放到 .compressedmap section 可以:
      • 保证 ELF 文件自带压缩数据
      • 避免运行时分配大量内存
        理解方式
  • 前段:索引表(映射 Key -> 偏移/长度)
  • 中段:压缩字符串二进制
  • 后段:可能是 padding 或其他元数据

Huffman coding (overview)

36
16
20
8
8
8
12
'e'|4
4
'a'|4
4
4
4
5
' '|7
'n'|2
2
't'|2
'm'|2
'i'|2
2
'h'|2
's'|2
2
'f'|3
'o'|1
'u'|1
'x'|1
'p'|1
'r'|1
'T'|1
graph TD
    A[36] --> B[16]
    A --> C[20]
    B --> D[8]
    B --> E[8]
    C --> F[8] 
    C --> G[12]
    D --> H["'e'|4"]
    D --> I[4]
    E --> J["'a'|4"]
    E --> K[4]
    F --> L[4]
    F --> M[4]
    G --> N[5]
    G --> O["' '|7"]
    I --> P["'n'|2"]
    I --> Q[2]
    K --> R["'t'|2"]
    K --> S["'m'|2"]
    L --> T["'i'|2"]
    L --> U[2]
    M --> V["'h'|2"]
    M --> W["'s'|2"]
    N --> X[2]
    N --> Y["'f'|3"]
    Q --> Z["'o'|1"]
    Q --> AA["'u'|1"]
    U --> BB["'x'|1"]
    U --> CC["'p'|1"]
    X --> DD["'r'|1"]
    X --> EE["'T'|1"]

Huffman 编码(霍夫曼编码)概览以及一个示意树(Mermaid 图)。我帮你整理成理解:

1. Huffman 编码概述

  • 目标:给定符号列表及其出现频率,生成最优二进制编码
  • 符号集合:这里是所有字符串里的字符
  • 构建方式
    1. 统计每个符号的频率。
    2. 最小堆(min-heap / priority_queue)频率最小的符号开始,两两合并,逐步向上构建 Huffman 树。
    3. 从树根走向叶子节点,为每个符号分配对应的 bit 模式(0 或 1)。
  • 特点
    • 出现频率高的符号 → 编码最短
    • 出现频率低的符号 → 编码较长
    • 总体保证平均编码长度最小,提高压缩效率。

2. 你提供的 Mermaid 图解释

这个图表示 Huffman 树的 结构和符号频率

  • 数字表示节点权重(频率)
  • 叶子节点标注了字符和出现次数,例如:
    'e'|4
    'a'|4
    ' '|7
    
  • 非叶子节点是合并后的权重:
    • A[36] 是根节点,总频率 = 所有字符频率之和
    • B[16]C[20] 是根的左右子节点
    • 树从频率最小的节点向上合并,最终得到完整 Huffman 树
  • 叶子节点的路径就是每个字符的编码:
    • 从根到叶子:
      • 左分支 → 可以约定为 0
      • 右分支 → 可以约定为 1
    • 频率高的字符(如 ' ' 空格、'e''a')在树的上层 → 路径短 → 编码短
    • 频率低的字符(如 'T''o''u')在树的下层 → 路径长 → 编码长

3. 总结理解

  1. Huffman 树构建原理
    • 频率低的符号先合并,形成子树
    • 最终形成根节点,叶子是所有符号
  2. 编码规则
    • 根到叶子路径的 0/1 就是该符号的 Huffman 编码
    • 高频符号 → 短编码
    • 低频符号 → 长编码
  3. 用途
    • 典型的无损压缩方法
    • 常用在字符串压缩、文件压缩(如 ZIP)、图像压缩(如 JPEG)等
      如果你需要,我可以帮你把这个 Mermaid 图画成标注的 Huffman 树示意图,标出每个字符的二进制编码,更直观地理解。

编译期字符串压缩库(类似 squeeze)的处理流程与 C++ 实现思路理解:

1. 库的处理流程(Library Process)

  1. 统计字符频率
    • 遍历所有字符串,统计每个字符出现次数,用于 Huffman 编码。
  2. 构建 Huffman 树(Human tree)
    • 根据字符频率,构建 Huffman 树,频率高的字符走得短路径。
  3. 构建 char -> bitstream 缓存
    • 将每个字符映射到其 Huffman 编码的 bit pattern。
  4. 生成编码后的比特流
    • 对每个字符串,用 Huffman 树编码成 bit stream。
  5. 构建索引表(index -> bit position / string length)
    • 用于快速定位某个字符串在 bitstream 中的位置和长度。
  6. Huffman 树打包成数组
    • 为了静态存储,Huffman 树序列化为数组形式。
  7. 组合 Huffman 树、bitstream 和索引表
    • 最终形成可查找的结果结构。
  8. 查找(lookup)
    • 根据 key/index 快速定位对应压缩后的字符串。

2. 约束 Lambda 类型

  • KeyedStringView
    template<typename TKey>
    struct KeyedStringView { 
        TKey Key; 
        std::string_view Value; 
    };
    
    • 一个键值对结构,Key 是枚举或标识符,Value 是字符串视图。
  • CallableGivesIterableStringViews
    • 要求 Lambda 返回可迭代的 std::string_view 集合。
  • CallableGivesIterableKeyedStringViews
    • 要求 Lambda 返回可迭代的 KeyedStringView 集合。

这些概念保证模板在编译期能正确推导类型。

3. Map to Table(映射到字符串数组)

template<typename TKey> 
static constexpr auto MapToStrings(auto f) -> auto
{ 
    return [=]() { 
        constexpr auto stringmap = f(); 
        constexpr auto NumStrings = std::distance(stringmap.begin(), stringmap.end()); 
        std::array<std::string_view, NumStrings> result;
        std::size_t idx{0};
        for(auto const &v : stringmap) {
            result.at(idx++) = v.Value;
        }
        return result;
    };
}
  • 功能:把 KeyedStringView 映射成普通字符串数组。
  • 返回一个 Lambda,延迟计算字符串数组(用于编译期计算)。

4. 模块化代码(Modularizing Code)

  • 问题
    • 需要在编译期根据用户数据计算常量。
    • Lambda 必须在每个上下文重新评估,不能作为参数直接传递。
    • 可能生成长方法,但可以通过局部 Lambda 模块化。
  • 编译期构建 Huffman 树
    static constexpr auto BuildHuffmanTree(auto makeStringsLambda) { ... }
    constexpr auto st = makeStringsLambda();
    return tree;
    
  • 生成编码 bit stream
    static constexpr auto MakeEncodedBitStream(auto makeStringsLambda) { ... }
    constexpr auto st = makeStringsLambda();
    constexpr auto tree = BuildHuffmanTree(makeStringsLambda);
    ResultType<NumStrings> stream; // 填充 bit stream
    return stream;
    

5. 计算字符串长度和 bit 长度

constexpr auto charLookup = MakeCharacterLookupTable();
constexpr auto CalculateStringLength = [=](std::string_view s) -> std::size_t {
    std::size_t len{0};
    for(char const c : s) {
        len += charLookup.at(static_cast<std::size_t>(c)).BitLength;
    }
    return len;
};
constexpr auto CalculateEncodedStringBitLengths = [=]() {
    std::array<std::size_t, NumStrings> result;
    std::size_t i{0};
    for(auto const &s : st) {
        result.at(i) = CalculateStringLength(s);
        ++i;
    }
    return result;
};
constexpr auto stringLengths = CalculateEncodedStringBitLengths();
constexpr auto totalEncodedLength = std::accumulate(stringLengths.begin(), stringLengths.end(), 0);
  • charLookup:Huffman 树映射表,每个字符对应 bit 长度。
  • CalculateStringLength:计算单个字符串的编码长度(bit 数)。
  • CalculateEncodedStringBitLengths:计算所有字符串的编码长度数组。
  • totalEncodedLength:计算压缩后总比特长度。

总结

  1. 编译期处理
    • 统计字符频率 → Huffman 树 → bitstream → 索引 → 总长度
    • 全部在编译期完成,减少运行期开销。
  2. 设计思路
    • 使用 Lambda + constexpr 延迟计算。
    • 使用 概念约束 确保模板类型正确。
    • 模块化函数:BuildHuffmanTreeMakeEncodedBitStream、长度计算等。
  3. 结果
    • 静态存储压缩数据、索引表、Huffman 树。
    • 可以通过 key/index 快速访问压缩字符串。
#include <array>
#include <cstdio>
#include <numeric>
#include "squeeze-main/include/squeeze/huffmanencoder.h"
// 定义 Key 枚举,用作字符串映射的键
enum class Key { String_1, String_2, String_3 };
// ------------------------ 编译期构建字符串映射 ------------------------
// 这里用 lambda 返回一个 std::array<std::string_view>,包含需要压缩的字符串
static constexpr auto makeStringsLambda = [] {
    return std::to_array<std::string_view>({
        "We will include some long strings in the table.",  // String_1
        "Another example string for testing.",              // String_2
        "There is little point to using short strings."     // String_3
    });
};
// ------------------------ 编译期生成 Huffman 编码 ------------------------
// 调用 HuffmanEncoder::Compile,将字符串序列编码为压缩比特流
constexpr auto encoding = squeeze::HuffmanEncoder::Compile(makeStringsLambda);
// ------------------------ 放入自定义 section ------------------------
// 将压缩后的编码数据放入自定义段 ".compressedmap",方便嵌入式或静态存储
__attribute__((section(".compressedmap"))) inline constexpr auto compressed_data = encoding;
// 防止编译器优化掉未使用的变量
volatile auto keep = &compressed_data;
// ------------------------ 运行期打印 ------------------------
int main() {
    // 遍历所有字符串条目
    for (std::size_t i = 0; i < encoding.NumEntries; ++i) {
        // 获取压缩后的字符串
        auto s = encoding[i];
        // 打印字符串序号
        printf("String %zu: ", i);
        // 迭代字符串内容并打印
        for (char c : s) std::putchar(c);
        std::putchar('\n');
        // 打印压缩后的比特长度
        printf("Bit length = %zu\n", s.size());
    }
    return 0;
}
String 0: We will include some long strings in the table.
Bit length = 47
String 1: Another example string for testing.
Bit length = 35
String 2: There is little point to using short strings.
Bit length = 45

1. <algorithm> 的使用情况

  • 大多数标准算法可以在 constexpr 或普通环境中使用,例如:
    • std::sort(排序)
    • std::push_heap / std::pop_heap(堆操作)
    • std::count_if(统计满足条件的元素数量)
    • std::distance(计算迭代器间距离)
    • std::accumulate(累加)
  • 限制
    • 大部分 STL 容器不能用于 constexpr,除了 std::array
    • std::vector 需要编译器支持才能在 constexpr 中使用
    • 因此必须自己实现一些容器,例如:
      • priority_queue(优先队列)
      • list(链表)
      • bit_stream(可用 std::bitset 代替)

总结:如果有更多 constexpr 容器支持,代码会更方便。

2. 堆分配(Heap Allocations)

  • 可以在 constexpr 上下文中分配堆内存,但必须在离开上下文前释放
  • 意味着不能返回动态分配的对象,即使是返回到另一个 constexpr 上下文
  • 实际应用:
    • 使用链表实现队列来做 Huffman 树的广度优先遍历
    • 为了方便,先统计所需节点数量,再用 std::array 分配固定大小数组存储树节点
  • 需求:
    • 如果有更多容器支持(如 constexpr std::vector),构建树会更简单

3. constexpr 的执行限制

  • 编译器在 constexpr 上下文中允许的计算量是有限制的
  • 复杂处理(如字符串压缩、Huffman 编码)容易触发限制
  • 解决办法:
    • 需要更复杂的实现,比如缓存比特流,而不是每次遍历 Huffman 树获取字符
  • 注意:
    • 对于大字符串,仍可能超过编译器限制
    • 可以通过设置编译器参数,例如 -fconstexpr-ops-limit=VERY_BIG_NUMBER 来扩大限制
      总结理解
  1. 算法支持不错,但 STL 容器受限,自己实现容器是必要的
  2. 堆分配在 constexpr 下有限制,必须及时释放
  3. 编译器对 constexpr 执行量有限制,复杂压缩算法需要优化缓存策略
#ifndef USB_USBDESCRIPTOR_H
#define USB_USBDESCRIPTOR_H
//
// USB 描述符构建器库
// 
// 功能说明:
// - 这是一个编译期USB描述符构建库,用于嵌入式系统的USB设备开发
// - 使用C++20的constexpr和模板特性,在编译期生成USB描述符数据
// - 支持配置描述符、接口描述符和端点描述符的构建
//
// 当前限制:
// - 理想情况下应使用强类型来支持 USB 标准的 Interface class/subclass/protocol
// - 字符串描述符的构建,仅能指定索引
// - 不支持备用接口(Alternate Interface)
// - 只实现了 Bulk 和 Interrupt Endpoint 特化,其他类型可用 Endpoint 类创建
// - 接口号是隐式分配的,不手动编号
// - 还不支持 OTG、Class 或 Vendor 特定描述符
// - 不会创建 Device 或 Device Query 描述符,这些可参考 Endpoint 的构建方式
//
// Configuration Descriptor 会与所有附加的 Interface 和 Endpoint 描述符一起发送给 Host
// 因此它们被打包到一个缓冲区中,这也是最复杂的部分
// USB 协议参考链接:
// https://www.beyondlogic.org/usbnutshell/usb1.shtml
// https://www.usbmadesimple.co.uk/index.html
#include <cstdint>
#include <span>
#include <algorithm>
#include <numeric>
#include <tuple>
namespace usb::descriptor {
    namespace impl {
        /**
         * @brief 辅助函数:将 uint16_t 以小端序写入 buffer
         * @param buffer 长度为2的字节缓冲区
         * @param data 要写入的16位数据
         * 
         * USB协议规定多字节数据必须以小端序(低字节在前)传输
         */
        constexpr void write_le(std::span<std::uint8_t, 2> buffer, std::uint16_t data)
        {
            buffer[0] = static_cast<std::uint8_t>(data & 0xFF);        // 低字节
            buffer[1] = static_cast<std::uint8_t>((data >> 8) & 0xFF); // 高字节
        }
    }
    /**
     * @brief USB描述符类型枚举
     * 
     * 定义了USB规范中的标准描述符类型
     * 每种描述符都有特定的格式和用途
     */
    enum class DescriptorType : std::uint8_t
    {
        Device = 0x01,           // 设备描述符 - 描述整个USB设备的基本信息
        Configuration = 0x02,    // 配置描述符 - 描述设备的一种配置
        String = 0x03,          // 字符串描述符 - 提供人可读的字符串信息
        Interface = 0x04,       // 接口描述符 - 描述一个功能接口
        EndPoint = 0x05,        // 端点描述符 - 描述数据传输端点
        DeviceQualifier = 0x06  // 设备限定符描述符 - 用于高速设备
    };
    /**
     * @brief USB端点方向枚举
     * 
     * 定义数据传输方向:
     * - Out: 从主机到设备
     * - In: 从设备到主机
     */
    enum class EndpointDirection : std::uint8_t
    {
        Out = 0b0000'0000,  // Host -> Device (主机向设备发送数据)
        In  = 0b1000'0000   // Device -> Host (设备向主机发送数据)
    };
    /**
     * @brief USB端点传输类型枚举
     * 
     * USB定义了四种基本传输类型,每种适用于不同的数据特性:
     */
    enum class EndpointTransfer : std::uint8_t
    {
        Control     = 0b0000'0000,  // 控制传输 - 用于设备控制和配置
        Isochronous = 0b0000'0001,  // 等时传输 - 用于音频/视频等对时间敏感的数据
        Bulk        = 0b0000'0010,  // 批量传输 - 用于大量数据,如文件传输
        Interrupt   = 0b0000'0011   // 中断传输 - 用于小量、低延迟数据,如鼠标键盘
    };
    /**
     * @brief USB端点同步类型枚举
     * 
     * 主要用于等时传输,定义数据同步方式
     */
    enum class EndpointSynchronisation : std::uint8_t
    {
        None         = 0b0000'0000,  // 无同步要求
        Asynchronous = 0b0000'0100,  // 异步
        Adaptive     = 0b0000'1000,  // 自适应
        Synchronous  = 0b0000'1100,  // 同步
    };
    /**
     * @brief USB端点用途枚举
     * 
     * 定义端点的具体用途,主要用于等时传输
     */
    enum class EndpointUsage : std::uint8_t
    {
        Data             = 0b0000'0000,  // 数据端点
        Feedback         = 0b0001'0000,  // 反馈端点
        ImplicitFeedback = 0b0010'0000   // 隐式反馈数据端点
    };
    /**
     * @brief 通用端点类
     * 
     * 这是最基础的端点类,提供完全的自定义能力
     * 可以创建任何类型的USB端点
     */
    class Endpoint
    {
    public:
        constexpr static std::size_t DescriptorLength = 7; // USB 端点描述符固定长度为7字节
        /**
         * @brief 端点构造函数
         * @param direction 端点方向(IN/OUT)
         * @param address 端点地址(0-15)
         * @param transfer 传输类型
         * @param synchronisation 同步类型
         * @param usage 端点用途
         * @param maxPacketSize 最大包大小
         * @param interval 轮询间隔(毫秒或帧数,取决于传输类型)
         */
        constexpr Endpoint(
            EndpointDirection direction,
            std::uint8_t address,
            EndpointTransfer transfer,
            EndpointSynchronisation synchronisation,
            EndpointUsage usage,
            std::uint16_t maxPacketSize,
            std::uint8_t interval
        )
            : m_Direction(direction)
            , m_Address(address & 0x0F) // USB端点地址只有4位,最大端点号为15
            , m_Transfer(transfer)
            , m_Synchronisation(synchronisation)
            , m_Usage(usage)
            , m_MaxPacketSize(maxPacketSize)
            , m_Interval(interval)
        {
        }
        /**
         * @brief 获取描述符长度
         * @return 描述符字节长度
         */
        constexpr std::size_t length() const { return DescriptorLength; }
        /**
         * @brief 将端点描述符渲染到缓冲区
         * @param buffer 目标缓冲区,必须至少有7字节
         * 
         * 按照USB规范格式填充端点描述符:
         * 字节0: 描述符长度 (7)
         * 字节1: 描述符类型 (0x05)
         * 字节2: 端点地址 (方向位 + 地址)
         * 字节3: 端点属性 (传输类型 + 同步类型 + 用途)
         * 字节4-5: 最大包大小 (小端序)
         * 字节6: 轮询间隔
         */
        constexpr void Render(std::span<std::uint8_t> buffer) const
        {
            buffer[0] = DescriptorLength; // 描述符长度
            buffer[1] = static_cast<std::uint8_t>(DescriptorType::EndPoint);
            // 端点地址 = 方向位(bit7) + 端点号(bit0-3)
            buffer[2] = static_cast<std::uint8_t>(m_Direction) | m_Address;
            // 端点属性 = 传输类型(bit0-1) + 同步类型(bit2-3) + 用途(bit4-5)
            buffer[3] = static_cast<std::uint8_t>(m_Transfer)
                        | static_cast<std::uint8_t>(m_Synchronisation)
                        | static_cast<std::uint8_t>(m_Usage);
            // 最大包大小(小端序,16位)
            impl::write_le(buffer.subspan<4, 2>(), m_MaxPacketSize);
            buffer[6] = m_Interval; // 轮询间隔
        }
    private:
        EndpointDirection m_Direction;           // 端点方向
        std::uint8_t m_Address;                 // 端点地址(0-15)
        EndpointTransfer m_Transfer;            // 传输类型
        EndpointSynchronisation m_Synchronisation; // 同步类型
        EndpointUsage m_Usage;                  // 端点用途
        std::uint16_t m_MaxPacketSize;          // 最大包大小
        std::uint8_t m_Interval;                // 轮询间隔
    };
    /**
     * @brief Bulk端点简化类
     * 
     * 批量端点的简化版本,自动设置适合批量传输的参数:
     * - 传输类型: Bulk
     * - 同步类型: None (批量传输不需要同步)
     * - 用途: Data
     * - 轮询间隔: 0 (批量传输不使用轮询)
     */
    class BulkEndpoint : public Endpoint
    {
    public:
        /**
         * @brief Bulk端点构造函数
         * @param direction 端点方向
         * @param address 端点地址
         * @param maxPacketSize 最大包大小(通常为64、512等)
         */
        constexpr BulkEndpoint(
            EndpointDirection direction,
            uint8_t address,
            uint16_t maxPacketSize
        ) : Endpoint(
               direction,
               address,
               EndpointTransfer::Bulk,          // 批量传输
               EndpointSynchronisation::None,    // 无同步要求
               EndpointUsage::Data,             // 数据端点
               maxPacketSize,
               0                                // 批量传输不使用轮询间隔
            )
        {}
    };
    /**
     * @brief Interrupt端点简化类
     * 
     * 中断端点的简化版本,适用于键盘、鼠标等HID设备:
     * - 传输类型: Interrupt
     * - 同步类型: None
     * - 用途: Data
     */
    class InterruptEndpoint : public Endpoint
    {
    public:
        /**
         * @brief Interrupt端点构造函数
         * @param direction 端点方向
         * @param address 端点地址
         * @param maxPacketSize 最大包大小(通常较小,如8、16、64字节)
         * @param interval 轮询间隔(毫秒,对于全速设备是1-255ms)
         */
        constexpr InterruptEndpoint(
                EndpointDirection direction,
                std::uint8_t address,
                std::uint16_t maxPacketSize,
                std::uint8_t interval
        ) : Endpoint(
                direction,
                address,
                EndpointTransfer::Interrupt,     // 中断传输
                EndpointSynchronisation::None,   // 无同步要求
                EndpointUsage::Data,             // 数据端点
                maxPacketSize,
                interval                         // 轮询间隔很重要
        )
        {}
    };
    /**
     * @brief USB接口类
     * 
     * 模板参数 NumEndpoints 指定该接口包含的端点数量
     * 一个接口代表设备的一个功能单元,如HID键盘、存储设备等
     * 
     * @tparam NumEndpoints 端点数量,在编译期确定
     */
    template<std::size_t NumEndpoints>
    class Interface
    {
        // 允许辅助函数访问私有构造函数
        template<typename ... TEPs> friend
        constexpr auto define_interface(
                std::uint8_t interfaceClass,
                std::uint8_t interfaceSubClass,
                std::uint8_t interfaceProtocol,
                std::uint8_t stringIdentifier,
                TEPs ... endpoints);
    public:
        constexpr static std::size_t InterfaceDescriptorLength = 9; // USB接口描述符固定长度
        /**
         * @brief 计算接口描述符加所有端点描述符的总长度
         * @return 总字节数
         * 
         * 包括:
         * - 接口描述符本身:9字节
         * - 所有端点描述符:每个7字节
         */
        constexpr std::size_t length() const {
            const auto epLength = std::accumulate(
                    m_Endpoints.begin(), m_Endpoints.end(), 0,
                    [](auto sum, auto ep) { return sum + ep.length(); });
            return epLength + InterfaceDescriptorLength;
        }
        /**
         * @brief 将接口描述符和所有端点描述符渲染到缓冲区
         * @param buffer 目标缓冲区
         * @param interfaceNumber 接口编号(由配置描述符分配)
         * 
         * 渲染顺序:
         * 1. 接口描述符(9字节)
         * 2. 所有端点描述符(每个7字节)
         */
        constexpr void Render(std::span<std::uint8_t> buffer, std::uint8_t interfaceNumber) const {
            // 填充接口描述符(9字节)
            buffer[0] = InterfaceDescriptorLength;                          // 描述符长度
            buffer[1] = static_cast<std::uint8_t>(DescriptorType::Interface); // 描述符类型
            buffer[2] = interfaceNumber;                                    // 接口号(由外部分配)
            buffer[3] = 0;                                                 // 备用设置号(暂不支持)
            buffer[4] = m_Endpoints.size();                                // 该接口的端点数量
            buffer[5] = m_InterfaceClass;                                  // 接口类(如HID=0x03)
            buffer[6] = m_InterfaceSubClass;                               // 接口子类
            buffer[7] = m_InterfaceProtocol;                               // 接口协议
            buffer[8] = m_StringIdentifier;                                // 字符串描述符索引
            // 渲染所有端点描述符
            std::size_t location{9}; // 从第9字节开始(接口描述符后)
            for (auto &ep: m_Endpoints) {
                const auto len = ep.length();
                ep.Render(buffer.subspan(location, len));
                location += len;
            }
        }
    private:
        /**
         * @brief 私有构造函数
         * 
         * 只能通过辅助函数创建,确保参数的正确性
         */
        constexpr Interface(
                std::uint8_t interfaceClass,
                std::uint8_t interfaceSubClass,
                std::uint8_t interfaceProtocol,
                std::uint8_t stringIdentifier,
                std::array<Endpoint, NumEndpoints> endpoints
        )
                : m_InterfaceClass{interfaceClass}
                , m_InterfaceSubClass{interfaceSubClass}
                , m_InterfaceProtocol{interfaceProtocol}
                , m_StringIdentifier{stringIdentifier}
                , m_Endpoints{endpoints}
        {
        }
        std::uint8_t m_InterfaceClass;    // USB接口类代码
        std::uint8_t m_InterfaceSubClass; // USB接口子类代码
        std::uint8_t m_InterfaceProtocol; // USB接口协议代码
        std::uint8_t m_StringIdentifier;  // 字符串描述符索引
        std::array<Endpoint, NumEndpoints> m_Endpoints; // 该接口的所有端点
    };
    /**
     * @brief 接口创建辅助函数
     * 
     * 使用可变参数模板,可以传入任意数量的端点
     * 自动推导端点数量并创建对应的Interface类型
     * 
     * @tparam TEPs 端点类型(自动推导)
     * @param interfaceClass USB接口类代码
     * @param interfaceSubClass USB接口子类代码  
     * @param interfaceProtocol USB接口协议代码
     * @param stringIdentifier 字符串描述符索引
     * @param endpoints 端点对象列表
     * @return 构造好的Interface对象
     */
    template<typename ... TEPs>
    constexpr auto define_interface(
        std::uint8_t interfaceClass,
        std::uint8_t interfaceSubClass,
        std::uint8_t interfaceProtocol,
        std::uint8_t stringIdentifier,
        TEPs ... endpoints)
    {
       return Interface<sizeof...(TEPs)>{
           interfaceClass, interfaceSubClass, interfaceProtocol, stringIdentifier,
           std::array<Endpoint, sizeof...(TEPs)>{ endpoints... }
       };
    }
    /**
     * @brief 厂商特定接口创建函数
     * 
     * 创建厂商自定义接口的简化版本
     * 自动设置接口类/子类/协议为0xFF(厂商特定)
     * 
     * @tparam TEPs 端点类型(自动推导)
     * @param stringIdentifier 字符串描述符索引
     * @param endpoints 端点对象列表
     * @return 厂商特定的Interface对象
     */
    template<typename ... TEPs>
    constexpr auto define_vendor_specific_interface(
        std::uint8_t stringIdentifier,
        TEPs ... endpoints)
    {
        return define_interface(0xFF, 0xFF, 0xFF, stringIdentifier, endpoints...);
    }
    /**
     * @brief USB配置类
     * 
     * 一个USB设备可以有多个配置,但同时只能激活一个
     * 每个配置包含一个或多个接口
     * 
     * @tparam Sizes 各个接口的端点数量列表
     */
    template<std::size_t ... Sizes>
    class Configuration
    {
    public:
        constexpr static std::size_t ConfigurationDescriptorSize = 9; // 配置描述符固定长度
        constexpr static std::uint8_t NumInterfaces = sizeof...(Sizes); // 接口数量
        /**
         * @brief 配置构造函数
         * @param configurationNumber 配置号(1开始)
         * @param stringIdentifier 字符串描述符索引
         * @param selfPowered 是否自供电(true=自供电,false=总线供电)
         * @param remoteWakeup 是否支持远程唤醒
         * @param maxPower_2mAUnits 最大功率(单位:2mA,如100表示200mA)
         * @param interfaces 接口对象列表
         */
        constexpr Configuration(
            std::uint8_t  configurationNumber,
            std::uint8_t stringIdentifier,
            bool selfPowered,
            bool remoteWakeup,
            std::uint8_t maxPower_2mAUnits,
            Interface<Sizes>... interfaces
        )
            : m_ConfigurationNumber{configurationNumber}
            , m_StringIdentifier{stringIdentifier}
            , m_SelfPowered{selfPowered}
            , m_RemoteWakeup{remoteWakeup}
            , m_MaxPower_2mAUnits(maxPower_2mAUnits)
            , m_Interfaces{ interfaces... }
        {}
        /**
         * @brief 计算配置描述符加所有接口和端点描述符的总长度
         * @return 总字节数
         * 
         * 包括:
         * - 配置描述符:9字节
         * - 所有接口描述符:每个9字节
         * - 所有端点描述符:每个7字节
         */
        constexpr std::size_t length() const {
            return ConfigurationDescriptorSize + std::apply(
                [](auto && ... interfaces)
                {
                    return (0 + ... + interfaces.length());
                },
                m_Interfaces
            );
        }
        /**
         * @brief 将配置描述符和所有子描述符渲染到缓冲区
         * @param buffer 目标缓冲区
         * 
         * 渲染顺序:
         * 1. 配置描述符(9字节)
         * 2. 所有接口描述符及其端点描述符
         */
        constexpr void Render(std::span<std::uint8_t> buffer) const
        {
            // 填充配置描述符(9字节)
            buffer[0] = ConfigurationDescriptorSize;                      // 描述符长度
            buffer[1] = static_cast<std::uint8_t>(DescriptorType::Configuration); // 描述符类型
            // buffer[2-3] 总长度稍后填充(需要计算所有子描述符长度)
            buffer[4] = NumInterfaces;                                    // 接口数量
            buffer[5] = m_ConfigurationNumber;                            // 配置号
            buffer[6] = m_StringIdentifier;                               // 字符串索引
            // 配置特性字段:bit7必须为1,bit6=自供电,bit5=远程唤醒
            buffer[7] = 0b1000'0000
                        | (m_SelfPowered  ? 0b0100'0000 : 0)
                        | (m_RemoteWakeup ? 0b0010'0000 : 0);
            buffer[8] = m_MaxPower_2mAUnits;                             // 最大功率
            // 渲染所有接口描述符
            std::size_t location{ConfigurationDescriptorSize}; // 从第9字节开始
            std::apply(
                [&](auto && ... interfaces)
                {
                    std::size_t index{0}; // 接口号从0开始
                    auto render = [&](auto i) {
                        auto len = i.length();
                        i.Render(buffer.subspan(location, len), index);
                        location += len;
                        index++;
                    };
                    ((render(interfaces)), ...); // 折叠表达式,渲染每个接口
                },
                m_Interfaces
            );
            // 写入总长度到buffer[2-3](小端序)
            impl::write_le(buffer.subspan<2, 2>(), location);
        }
    private:
        std::uint8_t m_ConfigurationNumber; // 配置号
        std::uint8_t m_StringIdentifier;    // 字符串描述符索引
        bool m_SelfPowered;                 // 是否自供电
        bool m_RemoteWakeup;                // 是否支持远程唤醒
        std::uint8_t m_MaxPower_2mAUnits;   // 最大功率(2mA单位)
        std::tuple<Interface<Sizes>...> m_Interfaces; // 保存所有接口(使用tuple支持不同类型)
    };
    /**
     * @brief Lambda类型约束
     * 
     * 用于确保传入的lambda函数返回正确类型的Configuration对象
     * 这是C++20的concept特性,提供编译期类型检查
     */
    template<typename T, std::size_t ... Sizes>
    concept CallableGivesConfiguration = requires(T t)
    {
        t();            // 必须可调用(无参数)
        std::is_same_v<decltype(t()), Configuration<Sizes...>>; // 返回类型必须匹配
    };
    /**
     * @brief 配置描述符制作函数
     * 
     * 这是整个库的核心函数,它:
     * 1. 在编译期执行lambda函数构建Configuration对象
     * 2. 计算所需的缓冲区大小
     * 3. 创建适当大小的std::array
     * 4. 将Configuration渲染到array中
     * 5. 返回包含完整描述符数据的array
     * 
     * @tparam Sizes 接口端点数量参数包(自动推导)
     * @param makeConfigLambda 构建Configuration对象的lambda函数
     * @return 包含完整USB描述符数据的std::array
     * 
     * 使用示例:
     * ```cpp
     * constexpr auto myConfigDescriptor = MakeConfigurationDescriptor([]() {
     *     return Configuration{
     *         1,      // 配置号
     *         0,      // 字符串索引  
     *         false,  // 总线供电
     *         false,  // 不支持远程唤醒
     *         50,     // 100mA (50 * 2mA)
     *         define_vendor_specific_interface(0,
     *             BulkEndpoint{EndpointDirection::In, 1, 64},
     *             BulkEndpoint{EndpointDirection::Out, 2, 64}
     *         )
     *     };
     * });
     * ```
     */
    template<std::size_t ... Sizes>
    consteval static auto MakeConfigurationDescriptor(CallableGivesConfiguration<Sizes...> auto makeConfigLambda)
    {
        constexpr auto cfg = makeConfigLambda(); // 编译期执行lambda,构建配置对象
        constexpr auto len = cfg.length();       // 计算所需缓冲区大小
        std::array<std::uint8_t, len> data;      // 创建适当大小的缓冲区
        cfg.Render(data);                        // 将配置对象渲染到缓冲区
        return data;                             // 返回包含描述符数据的array
    }
} // namespace usb::descriptor
#endif // USB_USBDESCRIPTO
#include <cstdio>
#include <cstdint>
#include "usbdescriptor.h"
/**
 * @brief 创建USB配置描述符示例
 *
 * 这个例子展示了如何使用USB描述符库创建一个包含两个厂商特定接口的配置:
 * - 接口0:包含1个OUT端点和1个IN端点
 * - 接口1:包含1个OUT端点和3个IN端点
 *
 * constinit关键字确保该变量在编译期初始化,运行时不会有初始化开销
 */
[[gnu::section(".descriptor"), gnu::used]]
constinit auto const usbConfiguration1Descriptor =
    usb::descriptor::MakeConfigurationDescriptor([]() {
        using namespace usb::descriptor;
        return Configuration{
            1,      // 配置号:这是设备的第1个配置(从1开始编号)
            3,      // 字符串描述符索引:指向描述该配置的字符串(可选)
            false,  // 自供电标志:false表示设备从USB总线取电(总线供电)
            false,  // 远程唤醒标志:false表示设备不支持远程唤醒主机
            100,    // 最大功率:100 * 2mA = 200mA(设备从USB总线消耗的最大电流)
            // 第一个接口:混合端点接口(1个OUT + 1个IN)
            define_vendor_specific_interface(1,  // 字符串描述符索引:指向描述该接口的字符串
                                                 // OUT端点:主机向设备发送数据
                                             BulkEndpoint{
                                                 EndpointDirection::Out,  // 方向:OUT(主机→设备)
                                                 1,                       // 端点地址:端点1
                                                 512  // 最大包大小:512字节(高速设备典型值)
                                             },
                                             // IN端点:设备向主机发送数据(但使用中断传输)
                                             InterruptEndpoint{
                                                 EndpointDirection::In,  // 方向:IN(设备→主机)
                                                 1,  // 端点地址:端点1(注意:IN/OUT可以共用地址)
                                                 512,  // 最大包大小:512字节
                                                 1     // 轮询间隔:1ms(中断端点必须指定轮询间隔)
                                             }),
            // 第二个接口:批量传输接口(1个OUT + 3个IN)
            // 典型应用场景:高速数据采集设备或多通道数据流设备
            define_vendor_specific_interface(2,  // 字符串描述符索引:指向描述该接口的字符串
                                                 // 单个OUT端点:接收主机发送的控制命令或配置数据
                                             BulkEndpoint{
                                                 EndpointDirection::Out,  // 方向:OUT(主机→设备)
                                                 1,                       // 端点地址:端点1
                                                 512                      // 最大包大小:512字节
                                             },
                                             // 第一个IN端点:发送主数据流
                                             BulkEndpoint{
                                                 EndpointDirection::In,  // 方向:IN(设备→主机)
                                                 1,                      // 端点地址:端点1
                                                 512                     // 最大包大小:512字节
                                             },
                                             // 第二个IN端点:发送辅助数据流或状态信息
                                             BulkEndpoint{
                                                 EndpointDirection::In,  // 方向:IN(设备→主机)
                                                 2,                      // 端点地址:端点2
                                                 512                     // 最大包大小:512字节
                                             },
                                             // 第三个IN端点:发送调试信息或第三路数据
                                             BulkEndpoint{
                                                 EndpointDirection::In,  // 方向:IN(设备→主机)
                                                 3,                      // 端点地址:端点3
                                                 512                     // 最大包大小:512字节
                                             })};
    });
/**
 * @brief 十六进制数据转储函数
 *
 * 将二进制数据以十六进制格式打印到控制台,便于调试和分析
 * 格式:每行显示8个字节,字节间用空格分隔
 *
 * @param buffer 要转储的数据缓冲区
 *
 * 输出示例:
 * 09 02 39 00 02 01 03 C0 64
 * 09 04 00 00 02 FF FF FF 01
 * 07 05 01 02 00 02 00 07 05
 * ...
 */
void DumpHexBlock(std::span<std::uint8_t const> buffer) {
    constexpr static auto hex = "0123456789ABCDEF";  // 十六进制字符映射表
    for (auto i = 0; i < buffer.size(); ++i) {
        // 每8个字节换行一次,提高可读性
        if (i % 8 == 0)
            putchar('\n');
        else
            putchar(' ');  // 字节间用空格分隔
        const auto b = buffer[i];
        // 输出高4位
        putchar(hex[(b >> 4)]);
        // 输出低4位
        putchar(hex[(b & 0x0F)]);
    }
    putchar('\n');  // 最后换行
}
/**
 * @brief 主函数 - 演示USB描述符的生成和输出
 *
 * 程序执行流程:
 * 1. 编译期生成完整的USB配置描述符数据
 * 2. 运行时将描述符数据以十六进制格式输出
 * 3. 输出的数据可以直接用于USB设备固件中
 *
 * 预期输出解析:
 * - 前9字节:配置描述符
 * - 接下来每9字节:接口描述符
 * - 接下来每7字节:端点描述符
 *
 * 这种方式的优势:
 * - 编译期生成,运行时零开销
 * - 类型安全,编译期错误检查
 * - 代码清晰,易于维护和理解
 */
int main() {
    printf("USB Configuration Descriptor (总长度: %zu 字节):", usbConfiguration1Descriptor.size());
    // 转储生成的USB描述符数据
    DumpHexBlock(usbConfiguration1Descriptor);
    printf("\n描述符结构解析:");
    printf("\n- 配置描述符: 9字节");
    printf("\n- 接口0描述符 + 2个端点: 9 + 2*7 = 23字节");
    printf("\n- 接口1描述符 + 4个端点: 9 + 4*7 = 37字节");
    printf("\n- 总计: 9 + 23 + 37 = 69字节\n");
    return 0;
}
USB Configuration Descriptor (总长度: 69 字节):
09 02 45 00 02 01 03 80
64 09 04 00 00 02 FF FF
FF 01 07 05 01 02 00 02
00 07 05 81 03 00 02 01
09 04 01 00 04 FF FF FF
02 07 05 01 02 00 02 00
07 05 81 02 00 02 00 07
05 82 02 00 02 00 07 05
83 02 00 02 00
描述符结构解析:
- 配置描述符: 9字节
- 接口0描述符 + 2个端点: 9 + 2*7 = 23字节
- 接口1描述符 + 4个端点: 9 + 4*7 = 37字节
- 总计: 9 + 23 + 37 = 69字节

objdump -s --section=.descriptor example

xiaqiu@xz:~/test/build/CppCon/day359/code$ objdump -s --section=.descriptor example
example:     file format elf64-x86-64
Contents of section .descriptor:
 2140 09024500 02010380 64090400 0002ffff  ..E.....d.......
 2150 ff010705 01020002 00070581 03000201  ................
 2160 09040100 04ffffff 02070501 02000200  ................
 2170 07058102 00020007 05820200 02000705  ................
 2180 83020002 00                          .....           
xiaqiu@xz:~/test/build/CppCon/day359/code$ 
xiaqiu@xz:~/test/build/CppCon/day359/code$ objdump -s --section=.descriptor example
example:     file format elf64-x86-64
Contents of section .descriptor:
 2140 09024500 02010380 64090400 0002ffff  ..E.....d.......
 2150 ff010705 01020002 00070581 03000201  ................
 2160 09040100 04ffffff 02070501 02000200  ................
 2170 07058102 00020007 05820200 02000705  ................
 2180 83020002 00                          .....           
xiaqiu@xz:~/test/build/CppCon/day359/code$ objdump -h example | grep descriptor
 18 .descriptor   00000045  0000000000002140  0000000000002140  00002140  2**5
xiaqiu@xz:~/test/build/CppCon/day359/code$ objdump -h example
example:     file format elf64-x86-64
Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .interp       0000001c  0000000000000318  0000000000000318  00000318  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .note.gnu.property 00000030  0000000000000338  0000000000000338  00000338  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .note.gnu.build-id 00000024  0000000000000368  0000000000000368  00000368  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .note.ABI-tag 00000020  000000000000038c  000000000000038c  0000038c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .gnu.hash     00000024  00000000000003b0  00000000000003b0  000003b0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .dynsym       000000f0  00000000000003d8  00000000000003d8  000003d8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .dynstr       000000b7  00000000000004c8  00000000000004c8  000004c8  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .gnu.version  00000014  0000000000000580  0000000000000580  00000580  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 .gnu.version_r 00000040  0000000000000598  0000000000000598  00000598  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  9 .rela.dyn     000000d8  00000000000005d8  00000000000005d8  000005d8  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 10 .rela.plt     00000060  00000000000006b0  00000000000006b0  000006b0  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 11 .init         0000001b  0000000000001000  0000000000001000  00001000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 12 .plt          00000050  0000000000001020  0000000000001020  00001020  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 13 .plt.got      00000010  0000000000001070  0000000000001070  00001070  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 14 .plt.sec      00000040  0000000000001080  0000000000001080  00001080  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 15 .text         0000040c  00000000000010c0  00000000000010c0  000010c0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 16 .fini         0000000d  00000000000014cc  00000000000014cc  000014cc  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 17 .rodata       00000136  0000000000002000  0000000000002000  00002000  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 18 .descriptor   00000045  0000000000002140  0000000000002140  00002140  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 19 .eh_frame_hdr 00000084  0000000000002188  0000000000002188  00002188  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 20 .eh_frame     000001f0  0000000000002210  0000000000002210  00002210  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 21 .init_array   00000008  0000000000003d98  0000000000003d98  00002d98  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 22 .fini_array   00000008  0000000000003da0  0000000000003da0  00002da0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 23 .data.rel.ro  00000008  0000000000003da8  0000000000003da8  00002da8  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 24 .dynamic      000001f0  0000000000003db0  0000000000003db0  00002db0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 25 .got          00000060  0000000000003fa0  0000000000003fa0  00002fa0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 26 .data         00000010  0000000000004000  0000000000004000  00003000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 27 .bss          00000008  0000000000004010  0000000000004010  00003010  2**0
                  ALLOC
 28 .comment      0000002b  0000000000000000  0000000000000000  00003010  2**0
                  CONTENTS, READONLY
 29 .debug_aranges 000000c0  0000000000000000  0000000000000000  0000303b  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 30 .debug_info   00004627  0000000000000000  0000000000000000  000030fb  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 31 .debug_abbrev 0000098f  0000000000000000  0000000000000000  00007722  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 32 .debug_line   000002eb  0000000000000000  0000000000000000  000080b1  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 33 .debug_str    00005c0c  0000000000000000  0000000000000000  0000839c  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 34 .debug_line_str 000002fd  0000000000000000  0000000000000000  0000dfa8  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 35 .debug_rnglists 00000072  0000000000000000  0000000000000000  0000e2a5  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
xiaqiu@xz:~/test/build/CppCon/day359/code$ 

USB描述符数据详细解析

原始数据

09 02 45 00 02 01 03 80 64 09 04 00 00 02 FF FF
FF 01 07 05 01 02 00 02 00 07 05 81 03 00 02 01
09 04 01 00 04 FF FF FF 02 07 05 01 02 00 02 00
07 05 81 02 00 02 00 07 05 82 02 00 02 00 07 05
83 02 00 02 00

1. 配置描述符 (Configuration Descriptor) - 9字节

地址: 0x00-0x08
数据: 09 02 45 00 02 01 03 80 64
偏移 字节 字段名 解释
0 09 bLength 9 描述符长度
1 02 bDescriptorType 2 配置描述符类型
2-3 45 00 wTotalLength 0x0045 (69) 配置描述符总长度(小端序)
4 02 bNumInterfaces 2 接口数量
5 01 bConfigurationValue 1 配置值
6 03 iConfiguration 3 配置字符串索引
7 80 bmAttributes 0x80 总线供电(bit7=1必须,bit6=0自供电,bit5=0远程唤醒)
8 64 bMaxPower 100 最大功率 100×2mA=200mA

2. 接口0描述符 (Interface 0 Descriptor) - 9字节

地址: 0x09-0x11
数据: 09 04 00 00 02 FF FF FF 01
偏移 字节 字段名 解释
9 09 bLength 9 接口描述符长度
10 04 bDescriptorType 4 接口描述符类型
11 00 bInterfaceNumber 0 接口号
12 00 bAlternateSetting 0 备用设置(暂不支持)
13 02 bNumEndpoints 2 端点数量
14 FF bInterfaceClass 0xFF 厂商特定接口类
15 FF bInterfaceSubClass 0xFF 厂商特定子类
16 FF bInterfaceProtocol 0xFF 厂商特定协议
17 01 iInterface 1 接口字符串索引

3. 接口0端点1 OUT (Bulk Endpoint) - 7字节

地址: 0x12-0x18
数据: 07 05 01 02 00 02 00
偏移 字节 字段名 解释
18 07 bLength 7 端点描述符长度
19 05 bDescriptorType 5 端点描述符类型
20 01 bEndpointAddress 0x01 端点1 OUT方向(bit7=0)
21 02 bmAttributes 0x02 批量传输(bit1-0=10)
22-23 00 02 wMaxPacketSize 0x0200 (512) 最大包大小512字节(小端序)
24 00 bInterval 0 轮询间隔(批量传输不使用)

4. 接口0端点1 IN (Interrupt Endpoint) - 7字节

地址: 0x19-0x1F
数据: 07 05 81 03 00 02 01
偏移 字节 字段名 解释
25 07 bLength 7 端点描述符长度
26 05 bDescriptorType 5 端点描述符类型
27 81 bEndpointAddress 0x81 端点1 IN方向(bit7=1, 0x80+0x01)
28 03 bmAttributes 0x03 中断传输(bit1-0=11)
29-30 00 02 wMaxPacketSize 0x0200 (512) 最大包大小512字节
31 01 bInterval 1 轮询间隔1ms

5. 接口1描述符 (Interface 1 Descriptor) - 9字节

地址: 0x20-0x28
数据: 09 04 01 00 04 FF FF FF 02
偏移 字节 字段名 解释
32 09 bLength 9 接口描述符长度
33 04 bDescriptorType 4 接口描述符类型
34 01 bInterfaceNumber 1 接口号
35 00 bAlternateSetting 0 备用设置
36 04 bNumEndpoints 4 端点数量
37 FF bInterfaceClass 0xFF 厂商特定接口类
38 FF bInterfaceSubClass 0xFF 厂商特定子类
39 FF bInterfaceProtocol 0xFF 厂商特定协议
40 02 iInterface 2 接口字符串索引

6. 接口1端点1 OUT (Bulk Endpoint) - 7字节

地址: 0x29-0x2F
数据: 07 05 01 02 00 02 00
偏移 字节 字段名 解释
41 07 bLength 7 端点描述符长度
42 05 bDescriptorType 5 端点描述符类型
43 01 bEndpointAddress 0x01 端点1 OUT方向
44 02 bmAttributes 0x02 批量传输
45-46 00 02 wMaxPacketSize 512 最大包大小
47 00 bInterval 0 批量传输不使用轮询间隔

7. 接口1端点1 IN (Bulk Endpoint) - 7字节

地址: 0x30-0x36
数据: 07 05 81 02 00 02 00
偏移 字节 字段名 解释
48 07 bLength 7 端点描述符长度
49 05 bDescriptorType 5 端点描述符类型
50 81 bEndpointAddress 0x81 端点1 IN方向
51 02 bmAttributes 0x02 批量传输
52-53 00 02 wMaxPacketSize 512 最大包大小
54 00 bInterval 0 批量传输不使用轮询间隔

8. 接口1端点2 IN (Bulk Endpoint) - 7字节

地址: 0x37-0x3D
数据: 07 05 82 02 00 02 00
偏移 字节 字段名 解释
55 07 bLength 7 端点描述符长度
56 05 bDescriptorType 5 端点描述符类型
57 82 bEndpointAddress 0x82 端点2 IN方向(0x80+0x02)
58 02 bmAttributes 0x02 批量传输
59-60 00 02 wMaxPacketSize 512 最大包大小
61 00 bInterval 0 批量传输不使用轮询间隔

9. 接口1端点3 IN (Bulk Endpoint) - 7字节

地址: 0x3E-0x44
数据: 07 05 83 02 00 02 00
偏移 字节 字段名 解释
62 07 bLength 7 端点描述符长度
63 05 bDescriptorType 5 端点描述符类型
64 83 bEndpointAddress 0x83 端点3 IN方向(0x80+0x03)
65 02 bmAttributes 0x02 批量传输
66-67 00 02 wMaxPacketSize 512 最大包大小
68 00 bInterval 0 批量传输不使用轮询间隔

总结

描述符层次结构

配置1 (69字节总长度, 200mA功率)
├── 接口0 (厂商特定, 字符串索引1)
│   ├── 端点1 OUT - 批量传输, 512字节包
│   └── 端点1 IN  - 中断传输, 512字节包, 1ms轮询
└── 接口1 (厂商特定, 字符串索引2)
    ├── 端点1 OUT - 批量传输, 512字节包
    ├── 端点1 IN  - 批量传输, 512字节包
    ├── 端点2 IN  - 批量传输, 512字节包
    └── 端点3 IN  - 批量传输, 512字节包

字节统计验证

  • 配置描述符: 9字节
  • 接口0 + 2个端点: 9 + 2×7 = 23字节
  • 接口1 + 4个端点: 9 + 4×7 = 37字节
  • 总计: 9 + 23 + 37 = 69字节

设计特点

  1. 厂商特定设备: 两个接口都使用0xFF类/子类/协议
  2. 高速设备: 512字节包大小适用于USB 2.0高速模式
  3. 多数据流: 接口1有3个IN端点,适合多通道数据采集
  4. 混合传输: 接口0结合了批量传输(大数据)和中断传输(实时状态)

更多推荐