CppCon 2021 学习: Compile-Time Compression and Resource Generation with C++20
·
这段英文主要是在介绍 C++20 的 constexpr 特性及其应用,理解如下:
介绍
探索 C++20 的 constexpr 特性如何实现以下功能:
- 在编译期生成数据
利用constexpr,程序可以在编译阶段计算结果,而不是在运行时,这样可以提升效率并减少运行时开销。 - 用于构造以下内容:
- 查找表(Lookup Tables)
通过constexpr可以在编译期生成查找表,例如三角函数表、哈希表等,运行时直接使用,无需计算。 - 配置熔丝(Configuration Fuses)
可以在编译期确定一些配置选项或宏常量,从而在程序中控制功能开关或特性。 - 压缩字符串(Compressed Strings)
利用constexpr在编译期对字符串进行压缩或处理,减少运行时处理开销。 - USB 描述符(USB Descriptors)
可以在编译期生成 USB 描述符结构体的数据,从而减少初始化代码和运行时开销。
总结:constexpr简述(constexpr in Brief)
- 查找表(Lookup Tables)
- 指定一个变量或函数可以出现在常量表达式中
constexpr用来标记变量或函数,表示它们可以在编译期计算,即它们的值在编译阶段就能确定。
- 常量表达式可以在编译期求值
- 被标记为
constexpr的变量或函数,可以在编译时计算结果,而不是等到程序运行时。
- 被标记为
- 用途举例(eg)
- 数组大小(Array size):可以用
constexpr变量定义数组长度,编译期就知道大小。 - 非类型模板参数(non-type template parameter):模板参数必须在编译期确定,
constexpr就非常适合。
简单理解:constexpr的作用是告诉编译器“这个值/函数可以在编译期算出来”,从而提高性能并支持编译期计算。
- 数组大小(Array size):可以用
constexpr 变量(constexpr Variable)
- 必须是字面量类型(Literal Type)
也就是说,变量类型必须满足可以在编译期求值的条件,例如:- 标量类型(scalar):如
int、char、float等 - 字面量数组(array of literals):数组元素都是字面量类型
- 结构体/类/联合体(struct/class/union):但要满足一些限制(比如成员都是字面量类型)
- 闭包类型(lambda)
- 标量类型(scalar):如
- 必须立即初始化(Must be immediately initialised)
- 声明时必须给出初始值,否则编译不通过
- 例如:
constexpr int x = 5;是合法的,但constexpr int x;不行
- 隐含 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;
}
理解
Base是一个constexpr int,值为二进制00101000,即 40。Values是一个constexpr std::array<int, 3>,元素是{1,2,3}。constexpr变量隐含const,所以不能修改,比如Base += 1;会报错。Base + Values[1]在运行时也能用,但如果用于编译期常量表达式,编译器可以在编译期计算结果。
constexpr 函数(Function)理解
- 返回类型要求
- 必须返回 字面量类型(Literal Type),或者
void。 - 字面量类型包括:整数、浮点数、指针、constexpr 数组、constexpr 结构体/类(满足要求的)等。
- 必须返回 字面量类型(Literal Type),或者
- 返回值成员初始化
- 如果返回的是结构体或类对象,所有成员必须在函数中被初始化。
- 可以是构造函数
constexpr构造函数允许在编译期创建对象。
- 参数要求
- 所有参数必须是 字面量类型(如果有的话),这样才能保证编译期求值。
简单总结:
- 所有参数必须是 字面量类型(如果有的话),这样才能保证编译期求值。
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;
}
理解补充:
make_bigger是constexpr,所以在编译期可以求值。big_value被声明为constexpr,值在编译时就已经确定为500。- 数组大小也可以使用
constexpr函数返回的值,因为它必须是编译期常量。 - 如果你在
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),也就是说:
- 静态存储期变量(全局、
static局部变量)在程序加载时就完成初始化,而不是等到运行时才初始化。 - 初始化值 必须是常量表达式,这样编译器可以在编译期计算出来并存入程序二进制中。
- 与
constexpr不同,constinit不要求变量是const,所以变量可以修改。 - 你也可以同时写
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 技术可以在编译期生成数据,从而优化程序运行时性能。理解如下:
- Lookup Tables(查找表)
- 在编译期生成一张表,存储预先计算好的值。
- 运行时直接访问表中的数据,而不需要重复计算。
- 例子:三角函数表、颜色映射表、哈希映射表。
- Configuration Fuses(配置熔丝 / 配置开关)
- 编译期确定程序的配置选项或特性开关。
- 类似硬件中的“熔丝”,一旦编译就固定。
- 例子:启用/禁用某个功能模块的标志。
- Compressed String Tables(压缩字符串表)
- 将字符串在编译期压缩存储,减少程序占用的内存。
- 运行时解压或直接按压缩格式使用。
- 例子:游戏中的多语言文本表、日志消息表。
- USB Descriptor(USB 描述符)
- USB 设备需要一系列描述符(设备、配置、接口、端点等)来告诉主机自己有哪些功能。
- 可以在编译期生成描述符数组,运行时直接用,无需动态构建。
总结:这些都是把数据生成和处理提前到编译期的例子,目的是减少运行时开销,提高程序效率。
如果你需要,我可以给你画一张 “编译期生成资源 vs 运行时生成资源”的对比图,让概念更直观。
这段内容是在解释 为什么要在编译期生成数据或使用constexpr/consteval技术。理解如下:
- No external tools(不依赖外部工具)
- 不需要额外的脚本或生成工具就能完成数据生成。
- 全部在 C++ 编译器内部完成,简化了构建流程。
- Can enforce correctness(可以保证正确性)
- 编译器在编译期就能检查数据的正确性。
- 比如表格、配置或描述符在编译期就验证是否符合预期,避免运行时错误。
- Pushing limits is fun(挑战极限很有趣)
- 利用编译器的编译期计算能力可以实现一些看似不可能的操作。
- 对程序员来说,这是探索 C++ 编译期能力和极限的一种乐趣。
这段内容是在讲 如何用 C++20 的constexpr构建一个编译期生成的线性插值查找表(Lookup Table)。理解如下:
核心目标
- 构建一个 查找表(Lookup Table) 来存储函数值,例如正弦函数。
- 通过查找表在运行时快速得到函数值,同时支持 线性插值。
- 完全在 编译期生成,保证性能和正确性。
步骤解析
- 辅助函数
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 是扩展支持(不是标准)。
- 生成查找表
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);
- 编译期断言,确保插值正确。
- 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)
- 作用
配置熔丝是在处理器启动前就初始化硬件的一种机制。- 通常存放在 Flash 固定位置
- 以位图(bit-mapped)的方式存储“魔法值”(magic values)
- 可以设置的内容包括:
- 时钟(clocks)
- 内存段(memory segments)
- 看门狗定时器(watchdog timer)
- JTAG 调试功能
- 代码安全(code security)
- 以及更多硬件功能
- 实现方式
- 通常依赖 厂商特定的编译器扩展
- 比如使用
#pragma或者特殊宏来设置熔丝
- 比如使用
- 对应的位值通常用 C 风格的
#define定义 - 缺点:没有严格的验证机制,配置可能非常复杂,容易出错
简单理解:Config Fuses 就像硬件的“启动开关”,在程序真正运行前就决定了处理器和外设的行为。
- 通常依赖 厂商特定的编译器扩展
更好的配置熔丝方法(Strongly typed Config Fuses)
目标
- 强类型化所有配置寄存器
- 用
enum class表示不同寄存器的可能值 - 避免直接操作魔法值,减少出错
- 用
- 提供“构建器”对象(Builder)
- 用面向对象的方法设置每个寄存器的值
- 在编译期生成
constinit对象- 数据在编译时生成,并存储在程序二进制中
- 使用段(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 lambda是constexpr,因此可以在编译期执行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;
}
注释说明
enum class- 用强类型枚举表示硬件寄存器的可能值
- 避免直接使用魔法数,增加可读性和安全性
ConfigBuilder- 提供
constexpr方法设置寄存器 build()方法在编译期生成最终序列化寄存器数组
- 提供
constinit auto const CONFIG_REGISTERS- lambda 是无捕获的,因此是
constexpr constinit保证在编译期初始化[[gnu::section(".config_registers")]]将其放到 Flash 特定段[[gnu::used]]防止编译器优化掉
- lambda 是无捕获的,因此是
- 寄存器序列化逻辑
- 高 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) }
}
要点:
MEMORY定义了内存区域:config:从 0x0100 开始,长度 8 字节。prog:从 0x1000 开始,长度 0x100000(程序代码区)。
SECTIONS定义段的映射:.config_registers段内容放入config区。.text/.data/.bss放入prog区。/DISCARD/丢弃其他未使用的段。
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⃣ 总结
CONFIG_REGISTERS已经在 编译期生成,存放在.config_registers段。- 段起始地址和 linker script 设置一致(0x0100)。
- 对象的大小是 8 字节,符合你设计的 2 个 32-bit 寄存器。
- 你现在的 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(调试信息、异常处理帧、栈标记等)。
- 链接器会直接丢弃。
总结理解
- config 区域只放
CONFIG_REGISTERS,大小固定 8 字节,起始地址 0x0100。 - prog 区域放程序代码
.text、只读数据.rodata、初始化变量.data、未初始化变量.bss。 - 丢弃段确保 ELF 文件干净,没有不必要的调试/异常信息。
- 使用
KEEP保证 constinit 对象不会被优化掉。
如果你想,我可以帮你画一个 内存布局示意图,把config、prog、各段大小、地址都标出来,一目了然。
如何在编译期根据输入动态生成不同大小或类型的数据,核心思路是利用 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 支持
constexprlambda:- Lambda 的
operator()可以是constexpr - 每个 lambda 是独一无二的类型,返回类型在编译期已知
- 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
总结理解
- 编译期生成动态大小资源:利用
constexprlambda,将用户数据封装成 编译期可调用对象。 - 返回类型和大小可推导:lambda 的返回类型在编译期已知,可以在模板或
std::array中使用。 - 模板 vs lambda:
- 模板函数也可实现类似功能,但 lambda +
auto参数语法更清晰、可读性好。
- 模板函数也可实现类似功能,但 lambda +
- 适用场景:
- 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 构建压缩字符串表
- 定义键(Key)枚举
每个字符串都有一个唯一的枚举键,用于索引。enum class Key { String_1, String_2, String_3 }; - 生成映射数组(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 生成数组,这样可以在编译期处理。
- 创建 StringMap
constexpr auto map = squeeze::StringMap<Key>(buildMapStrings);StringMap内部维护一个 压缩字符串表 和 Key -> 索引映射。- 支持在编译期生成结构,并在运行期高效访问。
- 访问压缩字符串
static constexpr auto str1 = map.get(Key::String_1); for (auto const &c : str1) { std::putchar(c); } std::putchar('\n');- 返回一个迭代器,逐字符访问压缩字符串。
- 避免一次性解压整个字符串,节省内存和元数据。
10.2 StringMap & StringTable 设计
- StringTable
- 实现对压缩字符串的简单索引访问。
- 保存压缩数据、起始位置和长度。
- StringMap
- 维护
Key -> 索引映射。 - 用户传入的字符串通过 lambda 提供
std::string_view。
- 维护
- 特点
- 编译期生成,最小元数据。
- 迭代器接口,无需一次性解压。
- 适合长字符串和大量字符串的情况。
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)。
- 每个键映射到压缩数据,压缩树/字典编码在数组中。
总结理解
- 目标:把字符串在编译期压缩,生成固定结构,减少运行时开销。
- 实现方法:
- 枚举键
Key - 使用 lambda 构建键-字符串数组
- 编译期生成
StringMap和StringTable - 支持迭代器访问压缩字符串
- 枚举键
- 优点:
- 运行期无需解压全部字符串
- 占用内存小,元数据少
- 编译期验证字符串和映射正确性
https://github.com/AshleyRoll/squeeze
#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;
}
理解
- StringMap 之下是 StringTable
- StringTable 是一个简单的表格,用于按索引访问压缩后的字符串。
- 通过 Key -> index 映射封装
- 在 StringTable 之上,用 StringMap 提供键到索引的映射,方便通过键快速找到对应字符串。
- StringMap 和 StringTable 都可以单独使用
- 你既可以直接用索引访问 StringTable,也可以通过键访问 StringMap。
- 字符串(和键)通过 lambda 传递
- 使用
std::string_view的 lambda 来传入字符串和键,避免不必要的拷贝。
- 使用
- 提供迭代器接口
- 允许你遍历字符串而无需一次性解压所有内容,节省内存和计算资源。
- 适用于长字符串,元数据开销最小
- 对于很长的字符串,这种设计能保持索引和元数据的占用很低,效率高。
总结理解
这个系统的设计目的是高效存储和访问大量长字符串,它通过:
- 压缩存储(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. 数据分析
- 前几行:
00000000 00000000 01000000 00000000 01000000 00000000 02000000 00000000- 很可能是 索引或元数据:
- 每 4 或 8 字节表示一个字符串的 ID、偏移量或长度。
- 对应你之前的
StringMap<Key>内部的 key -> index 映射表。
- 很可能是 索引或元数据:
- 中间部分:
2300 230f45ff 9a4f263a d82f4b6b 78bfd2f3 …- 这一块是 压缩后的字符串内容。
- 因为
squeeze库使用压缩,所以你看到的不是 ASCII 可读文本,而是压缩后的二进制。
- 后半部分:
2400 00006100 00000000 29002a00 01007500 …- 可能是 其他索引、表格或填充。
- ELF section 通常对齐 16 或 32 字节,所以会有很多零填充。
3. 总结理解
.compressedmapsection 保存的是:StringMap的索引信息(key -> string offset/length)- 压缩后的字符串数据
- 对齐或 padding
- 不是直接可读的字符串:
- 你看到的都是二进制表示,因为
squeeze压缩了字符串。 - 这是为了节省内存,尤其是存储长字符串。
- 你看到的都是二进制表示,因为
- 数据结构的意义:
- 程序运行时可以通过索引表找到对应字符串的偏移,再解压得到原文。
- 静态放到
.compressedmapsection 可以:- 保证 ELF 文件自带压缩数据
- 避免运行时分配大量内存
理解方式:
- 前段:索引表(映射 Key -> 偏移/长度)
- 中段:压缩字符串二进制
- 后段:可能是 padding 或其他元数据
Huffman coding (overview)
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 编码概述
- 目标:给定符号列表及其出现频率,生成最优二进制编码。
- 符号集合:这里是所有字符串里的字符。
- 构建方式:
- 统计每个符号的频率。
- 用最小堆(min-heap / priority_queue)从频率最小的符号开始,两两合并,逐步向上构建 Huffman 树。
- 从树根走向叶子节点,为每个符号分配对应的 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. 总结理解
- Huffman 树构建原理:
- 频率低的符号先合并,形成子树
- 最终形成根节点,叶子是所有符号
- 编码规则:
- 根到叶子路径的 0/1 就是该符号的 Huffman 编码
- 高频符号 → 短编码
- 低频符号 → 长编码
- 用途:
- 典型的无损压缩方法
- 常用在字符串压缩、文件压缩(如 ZIP)、图像压缩(如 JPEG)等
如果你需要,我可以帮你把这个 Mermaid 图画成标注的 Huffman 树示意图,标出每个字符的二进制编码,更直观地理解。
编译期字符串压缩库(类似 squeeze)的处理流程与 C++ 实现思路理解:
1. 库的处理流程(Library Process)
- 统计字符频率
- 遍历所有字符串,统计每个字符出现次数,用于 Huffman 编码。
- 构建 Huffman 树(Human tree)
- 根据字符频率,构建 Huffman 树,频率高的字符走得短路径。
- 构建 char -> bitstream 缓存
- 将每个字符映射到其 Huffman 编码的 bit pattern。
- 生成编码后的比特流
- 对每个字符串,用 Huffman 树编码成 bit stream。
- 构建索引表(index -> bit position / string length)
- 用于快速定位某个字符串在 bitstream 中的位置和长度。
- Huffman 树打包成数组
- 为了静态存储,Huffman 树序列化为数组形式。
- 组合 Huffman 树、bitstream 和索引表
- 最终形成可查找的结果结构。
- 查找(lookup)
- 根据 key/index 快速定位对应压缩后的字符串。
2. 约束 Lambda 类型
- KeyedStringView:
template<typename TKey> struct KeyedStringView { TKey Key; std::string_view Value; };- 一个键值对结构,Key 是枚举或标识符,Value 是字符串视图。
- CallableGivesIterableStringViews:
- 要求 Lambda 返回可迭代的
std::string_view集合。
- 要求 Lambda 返回可迭代的
- CallableGivesIterableKeyedStringViews:
- 要求 Lambda 返回可迭代的
KeyedStringView集合。
- 要求 Lambda 返回可迭代的
这些概念保证模板在编译期能正确推导类型。
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:计算压缩后总比特长度。
总结
- 编译期处理:
- 统计字符频率 → Huffman 树 → bitstream → 索引 → 总长度
- 全部在编译期完成,减少运行期开销。
- 设计思路:
- 使用 Lambda + constexpr 延迟计算。
- 使用 概念约束 确保模板类型正确。
- 模块化函数:
BuildHuffmanTree、MakeEncodedBitStream、长度计算等。
- 结果:
- 静态存储压缩数据、索引表、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代替)
- 大部分 STL 容器不能用于
总结:如果有更多
constexpr容器支持,代码会更方便。
2. 堆分配(Heap Allocations)
- 可以在
constexpr上下文中分配堆内存,但必须在离开上下文前释放 - 意味着不能返回动态分配的对象,即使是返回到另一个
constexpr上下文 - 实际应用:
- 使用链表实现队列来做 Huffman 树的广度优先遍历
- 为了方便,先统计所需节点数量,再用
std::array分配固定大小数组存储树节点
- 需求:
- 如果有更多容器支持(如
constexpr std::vector),构建树会更简单
- 如果有更多容器支持(如
3. constexpr 的执行限制
- 编译器在
constexpr上下文中允许的计算量是有限制的 - 复杂处理(如字符串压缩、Huffman 编码)容易触发限制
- 解决办法:
- 需要更复杂的实现,比如缓存比特流,而不是每次遍历 Huffman 树获取字符
- 注意:
- 对于大字符串,仍可能超过编译器限制
- 可以通过设置编译器参数,例如
-fconstexpr-ops-limit=VERY_BIG_NUMBER来扩大限制
总结理解:
- 算法支持不错,但 STL 容器受限,自己实现容器是必要的
- 堆分配在
constexpr下有限制,必须及时释放 - 编译器对
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字节 ✓
设计特点
- 厂商特定设备: 两个接口都使用0xFF类/子类/协议
- 高速设备: 512字节包大小适用于USB 2.0高速模式
- 多数据流: 接口1有3个IN端点,适合多通道数据采集
- 混合传输: 接口0结合了批量传输(大数据)和中断传输(实时状态)
更多推荐

所有评论(0)