CppCon 2022 学习: MODERN C++ TO IMPRESS YOUR EMBEDDED DEV FRIENDS
GPIODEF用于描述每个引脚功能,完全声明式,便于初始化和 functor 使用。延迟初始化:避免全局静态对象中使用空指针,全局对象改为在main()内创建。:functor 模式,使用模板AssertType控制高/低有效逻辑。:绑定总线、协议和 chip select functor,实现简单 SPI 事务模拟。打印调试信息:可以清楚看到每次 chip select 的 assert/dea
目标(OBJECTIVE)
Making embedded code easier (and more pleasant) to write and maintain
理解:
- 目标是让 嵌入式代码更容易编写和维护
- 同时让开发体验 更愉快(更直观、更可读、更高效)
议程(AGENDA)
- GPIO 配置(GPIO configuration)
- 嵌入式硬件的 通用输入输出(GPIO)接口 配置
- 可能包括现代 C++ idioms 让配置更清晰、类型安全
- 编译时查找表生成(Compile-Time Lookup Table Generation)
- 利用 constexpr / 模板 在编译时生成查找表
- 减少运行时开销,提高效率
- 开发者友好的数值构造(Developer-Friendly Numeric Constructs)
- 提供更安全、可读、可维护的数值类型和操作
- 避免魔法数字、易出错的类型转换
- 精简的基于流的输入输出(Lean stream-based IO)
- 小型、轻量的 IO 流处理
- 类似标准库
iostream
,但更适合嵌入式环境
- 堆内存管理:一次性分配(Heap memory management for allocate-once)
- 管理有限堆内存
- 提供一次性分配的策略,避免频繁 malloc/free
- 像高手一样使用
std::chrono
(Use std::chrono like a boss)- 利用现代 C++ 的 时间库 高效处理时间和延时
- 提高可读性和准确性
- 一个随机话题(A std::random topic)
- 可能涉及 随机数生成 或其他小技巧
- 用于展示现代 C++ 在嵌入式中的应用
总结
- 目标:提高嵌入式代码可读性、可维护性和愉快性
- 议程:涵盖 GPIO 配置、编译时优化、类型安全、轻量 IO、内存管理、时间处理、随机数等现代 C++ 技巧
- 核心思想:用 现代 C++ idioms 改善嵌入式开发体验,同时保持效率和资源友好
#include <cstdint>
#include <span>
#include <concepts>
#include <iostream>
// ===== 硬件寄存器定义 =====
// 模拟 STM32 风格的外设寄存器结构
struct GPIO_TypeDef {
uint32_t ODR = 0; // Output Data Register,初始化为 0
};
struct SPI_TypeDef {}; // SPI 外设占位符(仅用于示例)
// ===== GPIO 配置类型 =====
// 用于描述单个 GPIO 引脚的全部功能
using GPIODEF = struct IODefStuct {
GPIO_TypeDef* GPIO; // 指向对应 GPIO 外设寄存器
uint32_t PinNumber; // 引脚号
enum IOFUNCTION : uint32_t { INPUT = 0, OUTPUT = 1, ALT = 2, ANALOG = 3 } Function;
uint32_t AltFunc; // 如果是复用模式,指定复用功能编号
enum IOTYPE : uint32_t { NORMAL = 0, OPENDRAIN = 1 } Type; // 输出类型
enum IOSPEED : uint32_t { LOW = 0, MEDIUM = 1, HIGH = 2, VERYHIGH = 3 } Speed; // 输出速度
enum IOPULL : uint32_t { NONE = 0, PULLUP = 1, PULLDOWN = 2 } Bias; // 上下拉电阻
enum IOSTATE : uint32_t { LOGIC_LOW, LOGIC_HIGH, DONT_CARE } InitialState; // 初始逻辑状态
};
// ===== 全局外设指针(在 main 中初始化)=====
// 注意:在嵌入式系统中,这些通常由硬件提供
GPIO_TypeDef* GPIOB = nullptr;
SPI_TypeDef* SPI1 = nullptr;
// ===== 延迟初始化 GPIO 配置函数 =====
// 避免在全局静态对象中使用尚未初始化的 GPIO 指针
static GPIODEF make_gpiodef(GPIO_TypeDef* gpio, uint32_t pin) {
return {gpio,
pin,
GPIODEF::IOFUNCTION::OUTPUT,
1,
GPIODEF::IOTYPE::NORMAL,
GPIODEF::IOSPEED::LOW,
GPIODEF::IOPULL::NONE,
GPIODEF::IOSTATE::LOGIC_LOW};
}
// ===== SPI 相关类 =====
struct SPIBus {
SPI_TypeDef* peripheral; // 绑定 SPI 外设实例
SPIBus(SPI_TypeDef* p) : peripheral(p) {}
};
struct SPIProtocol {
enum class SPIMode { Mode1, Mode2, Mode3, Mode4 }; // SPI 模式
SPIMode mode;
uint32_t speed; // SPI 时钟频率
SPIProtocol(SPIMode m, uint32_t s) : mode(m), speed(s) {}
};
// ===== Assert Type 系统 =====
// 用于描述芯片选择逻辑电平
class AssertType {}; // 基类,用于约束 assert 类型
struct AssertTypeLogicHigh : public AssertType {
static constexpr bool ValueWhenAsserted = true; // 高电平有效
};
struct AssertTypeLogicLow : public AssertType {
static constexpr bool ValueWhenAsserted = false; // 低电平有效
};
// ===== GPIO Functor =====
// 使用模板创建 functor,根据 AssertType 控制 GPIO 输出
template <typename Assert>
requires std::derived_from<Assert, AssertType>
class GPIOAssertFunctor2 {
const GPIODEF& mGPIO; // 保存 GPIO 配置引用
public:
constexpr GPIOAssertFunctor2(const GPIODEF& io) : mGPIO(io) {}
void operator()(bool enable) const {
if (!mGPIO.GPIO) {
std::cout << " WARNING: GPIO pointer is null!\n";
return;
}
// 根据 AssertType 决定置位还是清零
if (enable == Assert::ValueWhenAsserted) {
mGPIO.GPIO->ODR |= (0b1 << mGPIO.PinNumber);
std::cout << "✓ GPIO Pin " << mGPIO.PinNumber << " asserted (ODR = 0x" << std::hex
<< mGPIO.GPIO->ODR << ")\n";
} else {
mGPIO.GPIO->ODR &= ~(0b1 << mGPIO.PinNumber);
std::cout << "✓ GPIO Pin " << mGPIO.PinNumber << " deasserted (ODR = 0x" << std::hex
<< mGPIO.GPIO->ODR << ")\n";
}
}
};
// ===== SPI Connection =====
// 使用模板 functor,绑定总线、协议和芯片选择逻辑
template <typename T>
requires std::derived_from<T, AssertType>
struct SPIConnection3 {
const SPIBus& mBus;
const SPIProtocol& mProtocol;
const GPIOAssertFunctor2<T>& mEnableFunction;
SPIConnection3(const SPIBus& bus, const SPIProtocol& proto, const GPIOAssertFunctor2<T>& enable)
: mBus(bus), mProtocol(proto), mEnableFunction(enable) {}
bool ReadWrite(std::span<char> outdata, std::span<char> indata, size_t length) {
std::cout << "\n SPI Transaction Start\n";
mEnableFunction(true); // Assert chip select
// 模拟数据传输(打印发送数据)
std::cout << "Sending: ";
for (size_t i = 0; i < length; ++i) {
std::cout << "0x" << std::hex << (int)(unsigned char)outdata[i] << " ";
}
std::cout << "\n";
mEnableFunction(false); // Deassert chip select
std::cout << " SPI Transaction End\n";
return true;
}
};
// ===== Main =====
int main() {
std::cout << "=== Embedded SPI Demo ===\n\n";
// 步骤1:初始化硬件寄存器实例
GPIO_TypeDef gpioBInstance{};
SPI_TypeDef spi1Instance{};
GPIOB = &gpioBInstance; // GPIOB 映射
SPI1 = &spi1Instance; // SPI1 映射
std::cout << "✓ Hardware initialized: GPIOB = " << GPIOB << ", SPI1 = " << SPI1 << "\n\n";
// 步骤2:创建 GPIO 配置(在 main 中,确保指针有效)
GPIODEF gpioCS = make_gpiodef(GPIOB, 0); // Pin 0 作为 Chip Select
// 步骤3:创建 GPIO functor
GPIOAssertFunctor2<AssertTypeLogicLow> memCS{gpioCS};
// 步骤4:构造 SPI 总线和协议
SPIBus bus(SPI1);
SPIProtocol protocol(SPIProtocol::SPIMode::Mode1, 4'000'000); // 4 MHz SPI
SPIConnection3 connection(bus, protocol, memCS);
// 步骤5:执行 SPI 数据传输
char outdata[4] = {char(0xDE), char(0xAD), char(0xBE), char(0xEF)};
char indata[4] = {};
connection.ReadWrite(std::span<char>(outdata, 4), std::span<char>(indata, 4), 4);
// 步骤6:打印最终 GPIO 状态
std::cout << "\n✓ Final GPIO ODR = 0x" << std::hex << GPIOB->ODR << std::dec << "\n";
return 0;
}
注释总结
- GPIODEF 用于描述每个引脚功能,完全声明式,便于初始化和 functor 使用。
- 延迟初始化:避免全局静态对象中使用空指针,全局对象改为在
main()
内创建。 - GPIOAssertFunctor2:functor 模式,使用模板
AssertType
控制高/低有效逻辑。 - SPIConnection3:绑定总线、协议和 chip select functor,实现简单 SPI 事务模拟。
- 打印调试信息:可以清楚看到每次 chip select 的 assert/deassert 状态,以及最终 ODR 值。
温度传感器(热敏电阻)代码添加了详细的注释,逐行解释每个函数和设计意图:
下面是你这段代码的详细注释版,我会逐步解释每个函数的作用、原理和注意事项。
#include <array>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
// ===== 改进的 constexpr log 实现 =====
// 作用:提供一个可以在编译期求值的自然对数函数
// 原理:使用对数的归一化性质 + 泰勒级数展开
constexpr float ConstexprLog(float x) {
// 边界检查:对数只对正数有定义
if (x <= 0.0f) {
return -std::numeric_limits<float>::infinity(); // log(0) 或负数返回负无穷
}
if (x == 1.0f) {
return 0.0f; // log(1) = 0
}
// 对 x 进行归一化,使其落在 [1,2) 区间,加速泰勒级数收敛
int exponent = 0;
while (x >= 2.0f) {
x *= 0.5f; // 除 2
exponent++;
}
while (x < 1.0f) {
x *= 2.0f; // 乘 2
exponent--;
}
// 泰勒级数展开:ln(x) = 2 * [y + y³/3 + y⁵/5 + ...]
float y = (x - 1.0f) / (x + 1.0f);
float y2 = y * y;
float result = 0.0f;
float term = y;
for (int n = 1; n < 50; n += 2) { // 迭代计算泰勒展开
result += term / n;
term *= y2;
if (term < 1e-7f) break; // 当项足够小时提前终止
}
result *= 2.0f;
// 加上归一化指数部分:ln(2) ≈ 0.693147180559945
constexpr float LN2 = 0.693147180559945f;
result += exponent * LN2;
return result;
}
// ===== 更精确的 constexpr pow 实现 =====
// 作用:在编译期求浮点数 base 的整数次幂 exp
constexpr float ConstexprPow(float base, int exp) {
if (exp == 0) return 1.0f; // 任何数的 0 次幂为 1
if (exp < 0) return 1.0f / ConstexprPow(base, -exp); // 负指数取倒数
float result = 1.0f;
for (int i = 0; i < exp; ++i) {
result *= base; // 逐次累乘
}
return result;
}
// ===== 计算热敏电阻温度 =====
// coefficients:Steinhart-Hart 系数
// R:热敏电阻的电阻值(欧姆)
// 返回值:温度(开尔文)
template <size_t N>
constexpr float ThermistorValue(const std::array<float, N>& coefficients, const float R) {
if (R <= 0.0f) {
return std::numeric_limits<float>::quiet_NaN(); // 非法电阻返回 NaN
}
float denom = 0.0f; // 用于计算 1/T 的分母
float logR = ConstexprLog(R); // log(R)
float logR_power = 1.0f; // log(R)^0 = 1
// 逐项累加 Steinhart-Hart 多项式
// 1/T = A + B*ln(R) + C*(ln(R))^2 + D*(ln(R))^3 ...
for (size_t i = 0; i < N; ++i) {
denom += coefficients[i] * logR_power;
logR_power *= logR; // 计算下一个幂次
}
if (denom == 0.0f) {
return std::numeric_limits<float>::quiet_NaN(); // 避免除 0
}
return 1.0f / denom; // 返回温度(开尔文)
}
// ===== 由分压计算电阻 =====
// V0:分压电路总电压
// V:测得电压
// R0:已知电阻
// 返回值:热敏电阻电阻值(欧姆)
constexpr float ResistanceFromDivider(const float V0, const float V, const float R0) {
if (V0 <= V || V <= 0.0f) {
return std::numeric_limits<float>::quiet_NaN(); // 电压非法返回 NaN
}
return R0 * V / (V0 - V); // 分压公式求热敏电阻 R
}
// ===== 由 ADC 码计算电压 =====
// Vref:参考电压
// n:ADC 位宽
// code:ADC 码值
// 返回值:对应电压
constexpr float VoltageFromCode(const float Vref, const size_t n, const uint16_t code) {
return Vref * code / ConstexprPow(2.0f, n); // ADC 输出码线性映射到电压
}
// ===== 构建热敏电阻查找表 =====
// coeff:Steinhart-Hart 系数
// R0:已知电阻
// V0:分压电源电压
// Vref:ADC 参考电压
// 返回值:长度为 256 的温度查找表
template <size_t N>
constexpr std::array<float, 256> ThermistorTable(const std::array<float, N>& coeff, const float R0,
const float V0, const float Vref) {
std::array<float, 256> table{}; // 初始化查找表
for (size_t code = 0; code < 256; ++code) {
float V = VoltageFromCode(Vref, 8, code); // 由 ADC 码计算电压
float R = ResistanceFromDivider(V0, V, R0); // 由电压计算电阻
table[code] = ThermistorValue(coeff, R); // 由电阻计算温度
}
return table; // 返回 256 元素温度查找表
}
// ===== Steinhart-Hart 系数示例 =====
static constexpr std::array<float, 4> ThermistorCoefficients{
1.0e-3f, // A
1.0e-4f, // B
1.0e-6f, // C
1.0e-8f // D
};
// ===== 编译期生成查找表 =====
constexpr auto ThermistorLookup = ThermistorTable(ThermistorCoefficients,
10.0e3f, // R0 = 10kΩ
3.3f, // V0 = 3.3V
3.3f // Vref = 3.3V
);
// ===== 测试函数 =====
void test_constexpr_log() {
std::cout << "=== ConstexprLog vs std::log ===\n";
const float test_values[] = {0.1f, 0.5f, 1.0f, 2.0f, 10.0f, 100.0f, 1000.0f, 10000.0f};
for (float x : test_values) {
float custom = ConstexprLog(x);
float standard = std::log(x);
float error = std::abs(custom - standard) / standard * 100.0f; // 百分比误差
std::cout << "x = " << x << " | Custom: " << custom << " | Std: " << standard
<< " | Error: " << error << "%\n";
}
}
int main() {
// 测试自定义 log 函数精度
test_constexpr_log();
std::cout << "\n=== Thermistor Lookup Table (first 10 entries) ===\n";
std::cout << "ADC Code | Temperature (K) | Temperature (°C)\n";
std::cout << "---------|-----------------|------------------\n";
for (size_t i = 0; i < 10; ++i) {
float T_kelvin = ThermistorLookup[i]; // 查表得到开尔文温度
float T_celsius = T_kelvin - 273.15f; // 转换为摄氏度
std::cout << i << " | " << T_kelvin << " | " << T_celsius << "\n";
}
return 0;
}
主要改进点:
ConstexprLog
支持在编译期计算 log,避免了原先std::log
的编译期限制。ConstexprPow
用移位/循环实现整数次幂,可用于VoltageFromCode
的编译期计算。- 在
ThermistorValue
、ResistanceFromDivider
等函数中增加非法值检查(如 R<=0、V>=V0),避免出现 NaN 或无效计算。 - 查找表
ThermistorLookup
可以完全在编译期生成,提高运行时效率。 - 添加了对 log 归一化、泰勒展开和指数部分的详细注释,方便理解原理。
- 主函数演示了 log 精度测试及热敏电阻查找表前 10 个值的输出。
详细解释你这个 Bash 脚本里用到的编译选项和工具的含义,特别针对 ARM Cortex-M4 交叉编译场景。
1⃣ 选择交叉编译器
CXX=$(find /usr/bin /opt/arm -name arm-none-eabi-g++ -print -quit)
[[ -z $CXX ]] && { echo "Compiler not found!"; exit -1; }
arm-none-eabi-g++
是针对 ARM Cortex-M 系列(无操作系统、裸机,EABI)的 C++ 编译器。find ... -print -quit
找到第一个匹配路径。- 如果没找到就退出。
OBJCOPY=$(find /usr/bin /opt/arm -name arm-none-eabi-objcopy)
[[ -z $OBJCOPY ]] && { echo "Object copy not found!"; exit -1; }
arm-none-eabi-objcopy
用于将 ELF 可执行文件转成其他格式,例如.bin
或.hex
。- 这里用于生成裸机二进制文件。
2⃣ 编译器版本
$CXX --version
- 输出编译器版本,确认使用的工具链正确。
3⃣ 编译选项解释
CXXFLAGS="-g0 -Os -std=c++20 \
-march=armv7e-m+fp -mfpu=fpv4-sp-d16 -mfloat-abi=hard -mtune=cortex-m4 \
-Wall -Wextra -Wpedantic -Wno-psabi \
-fno-rtti -fno-exceptions -ffunction-sections \
--specs=nosys.specs -Wl,--gc-sections"
优化和调试
-g0
:不生成调试信息(减少代码体积)。-Os
:优化生成最小代码体积。-std=c++20
:使用 C++20 标准。
ARM 架构和浮点设置
-march=armv7e-m+fp
:指定目标架构为 ARMv7E-M(Cortex-M4)并启用浮点扩展。-mfpu=fpv4-sp-d16
:使用单精度浮点单元(16 个寄存器)。-mfloat-abi=hard
:硬件浮点 ABI,浮点参数通过 FPU 寄存器传递。-mtune=cortex-m4
:针对 Cortex-M4 CPU 优化指令调度。
编译警告
-Wall
:开启常用警告。-Wextra
:开启额外警告。-Wpedantic
:严格标准兼容警告。-Wno-psabi
:关闭与浮点 ABI 相关的警告(ABI 不一致警告)。
C++ 特性控制
-fno-rtti
:禁用运行时类型信息,减少代码大小。-fno-exceptions
:禁用异常,适合裸机环境。
链接优化
-ffunction-sections
:每个函数放入单独的节(section)。--specs=nosys.specs
:裸机编译,不依赖操作系统的标准库。-Wl,--gc-sections
:链接器丢弃未使用的节,进一步减少程序体积。
4⃣ 编译和生成二进制
$CXX $CXXFLAGS main.cpp -o main.elf
$OBJCOPY --output-format binary main.elf main.bin
main.elf
:生成的 ELF 可执行文件(包含符号信息、节信息)。main.bin
:裸机二进制文件,直接可烧录到 MCU。
5⃣ 查看 .rodata 节和查找表位置
readelf --sections --wide main.elf | grep --color rodata
- 查看
.rodata
节(只读数据节)。 - 编译期生成的查找表
ThermistorLookup
通常会被放在这里。
readelf --symbols --wide --demangle main.elf | grep --color ThermistorLookup
- 查看符号表,确认
ThermistorLookup
是否在.rodata
节。 --demangle
可以显示 C++ 符号的原名。
总结
- 编译器和 objcopy 都是 ARM Cortex-M4 裸机交叉工具链。
- 编译选项:
- 优化体积(
-Os
、-ffunction-sections
、--gc-sections
) - 关闭异常和 RTTI(
-fno-exceptions
、-fno-rtti
) - 浮点硬件支持(
-mfpu
、-mfloat-abi=hard
) - 严格警告(
-Wall -Wextra -Wpedantic
)
- 优化体积(
- 生成 ELF,然后用 objcopy 转为二进制以烧录 MCU。
- 查找表(如 ThermistorLookup)会放在
.rodata
节。
“设备描述字符串”示例改写为更现代、类型安全的方式,并添加 main
函数,同时加上详细注释。
#include <cstdint>
#include <iostream>
#include <array>
#include <iomanip>
#include <cstring>
// ===== 传统方法示例 =====
// 方法 #1:使用宏将单字符转换为 UTF-16 小端表示
#define CHAR_TO_UNICODE(x) x, 0x00
static const uint8_t DeviceDescription_old1[] = {
CHAR_TO_UNICODE('M'), CHAR_TO_UNICODE('y'), CHAR_TO_UNICODE(' '),
CHAR_TO_UNICODE('D'), CHAR_TO_UNICODE('e'), CHAR_TO_UNICODE('v'),
CHAR_TO_UNICODE('i'), CHAR_TO_UNICODE('c'), CHAR_TO_UNICODE('e')};
// 方法 #2:直接使用 uint16_t 数组(错误示例)
// 这个方法有问题:每个元素占 16 位,但这里混合了字符和 0x00
static const uint16_t DeviceDescription_old2_wrong[] = {'M', 0x00, 'y', 0x00, ' ', 0x00,
'D', 0x00, 'e', 0x00, 'v', 0x00,
'i', 0x00, 'c', 0x00, 'e', 0x00};
// 正确的 uint16_t 方法:每个元素就是一个 UTF-16 字符
static const uint16_t DeviceDescription_old2_correct[] = {'M', 'y', ' ', 'D', 'e',
'v', 'i', 'c', 'e'};
// ===== 现代方法(使用 std::array 和 UTF-16 字面量) =====
static constexpr std::array<char16_t, 9> DeviceDescription_modern = {u'M', u'y', u' ', u'D', u'e',
u'v', u'i', u'c', u'e'};
// ===== 更现代的方法:直接使用 UTF-16 字符串字面量 =====
static constexpr auto DeviceDescription_string = u"My Device";
// ===== USB 描述符格式(典型用例) =====
struct USBStringDescriptor {
uint8_t bLength; // 描述符长度(字节)
uint8_t bDescriptorType; // 描述符类型(0x03 表示字符串)
char16_t bString[]; // UTF-16LE 字符串(不包含结束符)
} __attribute__((packed));
// ===== 编译期生成 USB 字符串描述符 =====
template <size_t N>
constexpr auto MakeUSBStringDescriptor(const char16_t (&str)[N]) {
struct Descriptor {
uint8_t bLength;
uint8_t bDescriptorType;
std::array<char16_t, N - 1> bString; // 去掉 null 终止符
};
Descriptor desc{};
desc.bLength = 2 + (N - 1) * sizeof(char16_t); // 2 字节头 + 字符串
desc.bDescriptorType = 0x03; // USB String Descriptor
for (size_t i = 0; i < N - 1; ++i) {
desc.bString[i] = str[i];
}
return desc;
}
// 编译期生成描述符
constexpr auto USB_DeviceDescriptor = MakeUSBStringDescriptor(u"My Device");
// ===== 辅助函数:打印字节表示 =====
void print_bytes(const void* data, size_t length, const char* label) {
std::cout << label << " (" << length << " bytes):\n ";
const uint8_t* bytes = static_cast<const uint8_t*>(data);
for (size_t i = 0; i < length; ++i) {
std::cout << "0x" << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(bytes[i]) << " ";
if ((i + 1) % 16 == 0) std::cout << "\n ";
}
std::cout << std::dec << "\n";
}
// ===== 辅助函数:打印 UTF-16 字符串 =====
void print_utf16(const char16_t* str, size_t length, const char* label) {
std::cout << label << ": \"";
for (size_t i = 0; i < length; ++i) {
// 简单处理 ASCII 范围字符
if (str[i] < 128) {
std::cout << static_cast<char>(str[i]);
} else {
std::cout << "\\u" << std::hex << std::setw(4) << std::setfill('0')
<< static_cast<int>(str[i]) << std::dec;
}
}
std::cout << "\"\n";
}
// ===== Main 函数 =====
int main() {
std::cout << "=== USB 设备描述符编码对比 ===\n\n";
// ===== 方法 #1:宏展开的 uint8_t 数组 =====
std::cout << "【方法 #1】宏展开 (CHAR_TO_UNICODE)\n";
print_bytes(DeviceDescription_old1, sizeof(DeviceDescription_old1), "内存布局");
std::cout << "文本内容: \"";
for (size_t i = 0; i < sizeof(DeviceDescription_old1); i += 2) {
std::cout << static_cast<char>(DeviceDescription_old1[i]);
}
std::cout << "\"\n";
std::cout << "✓ 优点: 明确的小端格式,适合直接发送给 USB 主机\n";
std::cout << "✗ 缺点: 需要宏,代码冗长\n\n";
// ===== 方法 #2:uint16_t 数组(错误示例) =====
std::cout << "【方法 #2 错误】uint16_t 混合数组\n";
print_bytes(DeviceDescription_old2_wrong, sizeof(DeviceDescription_old2_wrong), "内存布局");
std::cout << "✗ 问题: 数组长度是字符数的 2 倍(18 个元素而非 9 个)\n";
std::cout << "✗ 内存浪费: " << sizeof(DeviceDescription_old2_wrong) << " 字节\n\n";
// ===== 方法 #2:uint16_t 数组(正确示例) =====
std::cout << "【方法 #2 正确】uint16_t 数组\n";
print_bytes(DeviceDescription_old2_correct, sizeof(DeviceDescription_old2_correct), "内存布局");
print_utf16(reinterpret_cast<const char16_t*>(DeviceDescription_old2_correct),
sizeof(DeviceDescription_old2_correct) / sizeof(uint16_t), "文本内容");
std::cout << "✓ 优点: 紧凑,每个元素是一个字符\n";
std::cout << " 注意: 字节序取决于平台(大端/小端)\n\n";
// ===== 方法 #3:现代 C++ (std::array<char16_t>) =====
std::cout << "【方法 #3】std::array<char16_t>\n";
print_bytes(DeviceDescription_modern.data(), DeviceDescription_modern.size() * sizeof(char16_t),
"内存布局");
print_utf16(DeviceDescription_modern.data(), DeviceDescription_modern.size(), "文本内容");
std::cout << "✓ 优点: 类型安全,constexpr,现代 C++\n";
std::cout << "✓ 编译期大小检查: " << DeviceDescription_modern.size() << " 个字符\n\n";
// ===== 方法 #4:UTF-16 字符串字面量 =====
std::cout << "【方法 #4】UTF-16 字符串字面量\n";
print_bytes(
DeviceDescription_string,
(std::char_traits<char16_t>::length(DeviceDescription_string) + 1) * sizeof(char16_t),
"内存布局");
print_utf16(DeviceDescription_string,
std::char_traits<char16_t>::length(DeviceDescription_string), "文本内容");
std::cout << "✓ 优点: 最简洁,支持完整 Unicode\n";
std::cout << "✓ 自动添加 null 终止符\n\n";
// ===== USB 描述符示例 =====
std::cout << "【USB 字符串描述符】\n";
print_bytes(&USB_DeviceDescriptor, sizeof(USB_DeviceDescriptor), "完整 USB 描述符");
std::cout << "bLength: " << static_cast<int>(USB_DeviceDescriptor.bLength) << " 字节\n";
std::cout << "bDescriptorType: 0x" << std::hex
<< static_cast<int>(USB_DeviceDescriptor.bDescriptorType) << std::dec << "\n";
print_utf16(USB_DeviceDescriptor.bString.data(), USB_DeviceDescriptor.bString.size(),
"bString");
// ===== 大小对比 =====
std::cout << "\n=== 内存占用对比 ===\n";
std::cout << "方法 #1 (uint8_t[]): " << sizeof(DeviceDescription_old1) << " 字节\n";
std::cout << "方法 #2 错误 (uint16_t[]): " << sizeof(DeviceDescription_old2_wrong)
<< " 字节 \n";
std::cout << "方法 #2 正确 (uint16_t[]): " << sizeof(DeviceDescription_old2_correct)
<< " 字节\n";
std::cout << "方法 #3 (std::array): " << sizeof(DeviceDescription_modern) << " 字节\n";
std::cout << "方法 #4 (u\"string\"): "
<< (std::char_traits<char16_t>::length(DeviceDescription_string) + 1) *
sizeof(char16_t)
<< " 字节 (含 null)\n";
std::cout << "USB 描述符: " << sizeof(USB_DeviceDescriptor) << " 字节\n";
return 0;
}
=== USB 设备描述符编码对比 ===
【方法 #1】宏展开 (CHAR_TO_UNICODE)
内存布局 (18 bytes):
0x4d 0x00 0x79 0x00 0x20 0x00 0x44 0x00 0x65 0x00 0x76 0x00 0x69 0x00 0x63 0x00
0x65 0x00
文本内容: "My Device"
✓ 优点: 明确的小端格式,适合直接发送给 USB 主机
✗ 缺点: 需要宏,代码冗长
【方法 #2 错误】uint16_t 混合数组
内存布局 (36 bytes):
0x4d 0x00 0x00 0x00 0x79 0x00 0x00 0x00 0x20 0x00 0x00 0x00 0x44 0x00 0x00 0x00
0x65 0x00 0x00 0x00 0x76 0x00 0x00 0x00 0x69 0x00 0x00 0x00 0x63 0x00 0x00 0x00
0x65 0x00 0x00 0x00
✗ 问题: 数组长度是字符数的 2 倍(18 个元素而非 9 个)
✗ 内存浪费: 36 字节
【方法 #2 正确】uint16_t 数组
内存布局 (18 bytes):
0x4d 0x00 0x79 0x00 0x20 0x00 0x44 0x00 0x65 0x00 0x76 0x00 0x69 0x00 0x63 0x00
0x65 0x00
文本内容: "My Device"
✓ 优点: 紧凑,每个元素是一个字符
注意: 字节序取决于平台(大端/小端)
【方法 #3】std::array<char16_t>
内存布局 (18 bytes):
0x4d 0x00 0x79 0x00 0x20 0x00 0x44 0x00 0x65 0x00 0x76 0x00 0x69 0x00 0x63 0x00
0x65 0x00
文本内容: "My Device"
✓ 优点: 类型安全,constexpr,现代 C++
✓ 编译期大小检查: 9 个字符
【方法 #4】UTF-16 字符串字面量
内存布局 (20 bytes):
0x4d 0x00 0x79 0x00 0x20 0x00 0x44 0x00 0x65 0x00 0x76 0x00 0x69 0x00 0x63 0x00
0x65 0x00 0x00 0x00
文本内容: "My Device"
✓ 优点: 最简洁,支持完整 Unicode
✓ 自动添加 null 终止符
【USB 字符串描述符】
完整 USB 描述符 (20 bytes):
0x14 0x03 0x4d 0x00 0x79 0x00 0x20 0x00 0x44 0x00 0x65 0x00 0x76 0x00 0x69 0x00
0x63 0x00 0x65 0x00
bLength: 20 字节
bDescriptorType: 0x3
bString: "My Device"
=== 内存占用对比 ===
方法 #1 (uint8_t[]): 18 字节
方法 #2 错误 (uint16_t[]): 36 字节
方法 #2 正确 (uint16_t[]): 18 字节
方法 #3 (std::array): 18 字节
方法 #4 (u"string"): 20 字节 (含 null)
USB 描述符: 20 字节
#include <cstdint>
#include <array>
#include <stdexcept>
#include <iostream>
// ============================ 旧方法示例 ============================
// UUID(通用唯一标识符)示例
// {01234567-89AB-CDEF-0123-456789ABCDEF}
const uint8_t uuid1[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF};
// MAC 地址示例
// 12:34:56:78:90:AB
const uint8_t mac1[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB};
// IPv4 地址示例
// 192.168.0.1/24
struct IP4Addr {
uint8_t Address[4];
uint8_t MaskBits;
} address = {{192, 168, 0, 1}, 24};
// Unicode 字符串示例(UTF-16 小端表示)
char devicedesc[] = {'W', 0, 'i', 0, 'd', 0, 'g', 0, 'e', 0, 't', 0};
// ============================ 更好的方法 ============================
// UUID 类,封装 16 字节数据
class UUID {
private:
std::array<uint8_t, 16> mData{0}; // 默认初始化为 0
public:
UUID() = default;
// 友元函数,用于自定义字符串字面量
friend constexpr UUID operator""_uuid(const char* text, size_t length);
// 提供访问函数(便于打印)
void print() const {
for (size_t i = 0; i < mData.size(); ++i) {
printf("%02X", mData[i]);
if (i == 3 || i == 5 || i == 7 || i == 9) printf("-");
}
printf("\n");
}
};
// 自定义字面量,将字符串形式的 UUID 转换为 UUID 对象
constexpr UUID operator""_uuid(const char* text, size_t length) {
UUID result;
size_t count = 0; // 已填充的十六进制数字数量
for (size_t index = 0; index < length && count < 32; ++index) {
char c = text[index];
uint8_t value = 0;
if (c >= '0' && c <= '9') {
value = c - '0';
} else if (c >= 'a' && c <= 'f') {
value = 10 + (c - 'a');
} else if (c >= 'A' && c <= 'F') {
value = 10 + (c - 'A');
} else {
// 非十六进制字符(如 '-')直接跳过
continue;
}
// 每两个十六进制数字合并为一个字节
if (count % 2 == 0) {
result.mData[count / 2] = value << 4;
} else {
result.mData[count / 2] |= value;
}
++count;
}
if (count != 32) throw std::invalid_argument("Invalid UUID string");
return result;
}
// 使用自定义字面量生成 UUID 对象
static UUID uuid2 = "01234567-89AB-CDEF-0123-456789ABCDEF"_uuid;
// ============================ main 函数示例 ============================
int main() {
std::cout << "=== 旧方法 UUID 示例 ===\n";
for (size_t i = 0; i < sizeof(uuid1); ++i) {
printf("%02X", uuid1[i]);
if (i == 3 || i == 5 || i == 7 || i == 9) printf("-");
}
printf("\n\n");
std::cout << "=== 新方法 UUID 示例 ===\n";
uuid2.print();
std::cout << "\n=== MAC 地址示例 ===\n";
for (size_t i = 0; i < sizeof(mac1); ++i) {
printf("%02X", mac1[i]);
if (i != sizeof(mac1) - 1) printf(":");
}
printf("\n");
std::cout << "\n=== IPv4 地址示例 ===\n";
for (size_t i = 0; i < 4; ++i) {
printf("%d", address.Address[i]);
if (i != 3) printf(".");
}
printf("/%d\n", address.MaskBits);
return 0;
}
你这段代码主要是展示不同硬件/通信相关数据类型的定义和处理方式,以及如何在 C++ 中用更现代的方法处理它们。具体用处可以总结如下:
1. 旧方法示例
- UUID、MAC、IPv4、Unicode 字符串
const uint8_t uuid1[] = { ... };
const uint8_t mac1[] = { ... };
struct IP4Addr { ... } address = { ... };
char devicedesc[] = { ... };
- 用途:
- UUID:唯一标识设备或对象(如 IoT 设备、通信协议标识)。
- MAC 地址:网络接口硬件地址。
- IPv4 地址:网络配置。
- Unicode 字符串:设备描述信息(兼容 UTF-16,常用于 USB、BLE 或 Windows API)。
- 缺点:
- 初始化方式繁琐,不安全(手动写每个字节)。
- 易出错,尤其是 Unicode 或 UUID。
2. 更好的方法(UUID 类 + 字面量)
class UUID { ... };
constexpr UUID operator""_uuid(const char* text, size_t length);
static UUID uuid2 = "01234567-89AB-CDEF-0123-456789ABCDEF"_uuid;
- 用途:
- 现代 C++ 风格:用类封装 UUID 数据,保证类型安全。
- 自定义字面量
_uuid
:- 可以直接用字符串初始化 UUID。
- 自动解析十六进制字符。
- 忽略中间的
-
符号。
- 打印函数:
- 方便调试和日志输出。
- 优势:
- 安全、简洁、易于维护。
- 编译期生成 UUID 对象(
constexpr
支持)。
3. MAC 地址和 IPv4 地址示例
- 打印和使用示例:
- 直接用数组表示硬件地址或 IP。
- 可用于网络协议栈、设备驱动、嵌入式系统。
4. Unicode 字符串
char devicedesc[] = {'W',0,'i',0,'d',0,...};
- 用途:
- 表示 UTF-16 小端字符串,通常用于设备描述信息。
- 在旧的 API(如 Windows USB、BLE 服务)中常见。
5. main 函数用途
- 演示了如何打印:
- UUID(旧方法和新方法)
- MAC 地址
- IPv4 地址
- 可以直接用于调试和验证硬件/网络配置数据是否正确。
总结:
这个程序的主要用途是演示和封装硬件相关数据类型:
- 如何用数组表示二进制数据(UUID、MAC、IP、Unicode)。
- 如何用现代 C++(类 + constexpr + 字面量)安全、简洁地初始化和操作这些数据。
- 提供打印和验证手段,适合嵌入式、物联网或设备驱动场景。
“声明式 GPIO 配置(Declarative GPIO Configuration)” 的方法,重点是从传统手动配置 GPIO 的方式,过渡到使用 强类型(strongly typed)、可迭代的容器和函数对象(functor) 来管理和操作 GPIO,引入了一种现代、可维护、高可读性的硬件抽象方式。下面我帮你总结和理解:
1⃣ 旧方法:手动配置 GPIO
传统方式大概是这样:
GPIOInit_t config;
config.PinNumber = 1;
config.Mode = GPIO_MODE_PUSH_PULL;
config.Pull = GPIO_PULLUPDOWN_NONE;
config.Speed = GPIO_SPEED_MEDIUM;
config.AlternateFunc = GPIO_AF_6;
GPIO_Configure(GPIOA, &config);
GPIO_WritePin(GPIOA, 1, GPIO_LOGIC_HIGH);
config.PinNumber = 4;
config.AlternateFunc = GPIO_AF_2;
GPIO_Configure(GPIOA, &config);
特点:
- 每个引脚都要手动设置每个寄存器字段。
- 如果项目中 GPIO 很多,这种方式很难维护。
- 易出错:重复填写、忘记某个字段或顺序出错。
2⃣ 新方法:声明式 GPIO(Declarative GPIO)
(1) 用强类型结构定义 GPIO
struct GPIODEF {
GPIO_TypeDef* GPIO;
uint32_t PinNumber;
enum class IOFUNCTION { INPUT, OUTPUT, ALT, ANALOG } Function;
uint32_t AltFunc;
enum class IOTYPE { NORMAL, OPENDRAIN } Type;
enum class IOSPEED { LOW, MEDIUM, HIGH, VERYHIGH } Speed;
enum class IOPULL { NONE, PULLUP, PULLDOWN } Pull;
enum class IOSTATE { LOGIC_LOW, LOGIC_HIGH, DONT_CARE } InitialState;
};
- 使用 enum class 约束类型,避免错误。
- 每个引脚的定义集中在一个结构体里。
- 可以定义一个数组,把所有项目 GPIO 集中管理:
static const std::array<GPIODEF, 2> gpiodefs = {{
{GPIOB, 0, GPIODEF::IOFUNCTION::OUTPUT, 1, GPIODEF::IOTYPE::NORMAL, GPIODEF::IOSPEED::LOW, GPIODEF::IOPULL::NONE, GPIODEF::IOSTATE::LOGIC_LOW},
{GPIOB, 2, GPIODEF::IOFUNCTION::OUTPUT, 1, GPIODEF::IOTYPE::NORMAL, GPIODEF::IOSPEED::LOW, GPIODEF::IOPULL::NONE, GPIODEF::IOSTATE::LOGIC_LOW}
}};
优势:
- 集中管理整个项目的 IO 配置。
- 便于批量初始化、修改和阅读。
- 避免魔法数字,易于维护。
(2) 配置函数的迭代/范围写法
可以写一个通用函数,把数组中的 GPIO 一次性配置:
template<typename Iter>
bool Configure(Iter begin, Iter end) {
while (begin != end) Configure(*begin++);
}
template<typename T> requires std::ranges::range<T>
bool Configure(const T& definitions) {
for (auto& def : definitions) Configure(def);
}
- 支持 迭代器 或 ranges。
- 可以一次性初始化所有 GPIO,而不是手动一个个调用。
(3) 使用 Functor 操作 GPIO
传统写法:
GPIO_WritePin(GPIOA, 1, GPIO_LOGIC_HIGH);
现代写法,用 GPIOAssertFunctor:
class GPIOAssertFunctor {
const GPIODEF& mGPIO;
public:
constexpr GPIOAssertFunctor(const GPIODEF& io) : mGPIO(io) {}
void operator()(bool enable) const {
if (enable) {
mGPIO.GPIO->ODR |= (1 << mGPIO.PinNumber);
} else {
mGPIO.GPIO->ODR &= ~(1 << mGPIO.PinNumber);
}
}
};
- Functor 可以传给 SPI、I²C 或其他外设对象。
- 通过
enable(true/false)
控制引脚。 - 可以结合模板和类型系统,进一步约束逻辑:
class AssertTypeLogicHigh : public AssertType { static constexpr bool ValueWhenAsserted = true; };
class AssertTypeLogicLow : public AssertType { static constexpr bool ValueWhenAsserted = false; };
template<typename Assert>
class GPIOAssertFunctor { ... }
- 静态检查 GPIO 高低电平逻辑是否正确。
- 提高安全性和可读性。
(4) 在 SPI 总线等外设中使用 GPIO Functor
auto bus = SPIBus(SPI1);
auto protocol = SPIProtocol(SPIProtocol::SPIMode::Mode1, 1'000'000);
auto chipselect = GPIOAssertFunctor<AssertTypeLogicLow>(IOPins[4]);
auto connection = SPIConnection(bus, protocol, chipselect);
connection.ReadWrite(out, in, len);
- Chip Select 控制被封装成 Functor,避免手动设置寄存器。
- SPI、I²C 等外设都可以复用 Functor。
- 高可维护性,减少硬件相关的分散代码。
总结
- 集中定义 IO:所有 GPIO 在一个数组/结构中定义,便于管理。
- 强类型安全:使用
enum class
、模板约束、静态检查减少出错。 - 批量初始化:通过迭代器或 ranges 一次性配置 GPIO。
- 函数对象封装 GPIO 操作:用 Functor 传递给外设对象,更安全、更直观。
- 声明式、可维护:整个项目 GPIO 配置在一处集中管理,易于阅读和修改。
核心思想:
从“手动逐个配置寄存器” → “声明式定义数据结构+函数对象+迭代配置”,提高可读性、安全性、可维护性,同时保留高性能。
如何在编译期生成查找表(lookup table),尤其是嵌入式系统里对温度传感器(热敏电阻 Thermistor)的处理。下面我帮你用整理理解一下重点内容,并结合你的笔记顺序:
1. 传统做法 vs 编译器驱动生成
传统做法:
- 在 Excel、脚本或者其他工具中生成数值表格,然后手动或自动复制到代码中。
- 生成的表可能存在维护成本,硬件/固件迭代时需要重新生成表格。
编译器驱动做法: - 把热敏电阻的数学模型直接写入代码,利用
constexpr
在编译期生成表。 - 优点:
- 常量直接放入
.rodata
,不会占用 RAM。 - 表格随固件更新,避免外部生成的复杂性。
- 保证一致性和可维护性。
- 常量直接放入
2. 热敏电阻数学模型
- Steinhart-Hart 模型(通用形式):
1T=∑n=0∞an(lnR)n \frac{1}{T} = \sum_{n=0}^{\infty} a_n (\ln R)^n T1=n=0∑∞an(lnR)n
这里 ® 是热敏电阻阻值,(a_n) 是系数。 - 分压电路:
R=V0⋅R0V0−Vx R = \frac{V_0 \cdot R_0}{V_0 - V_x} R=V0−VxV0⋅R0
这里 (V_x) 是 ADC 读取的电压。 - ADC 电压计算:
V=Vref⋅code2n V = V_\text{ref} \cdot \frac{\text{code}}{2^n} V=Vref⋅2ncode
3. 编译期函数设计(C++ constexpr
)
- 用
constexpr
函数把上述公式计算出来:
template<size_t N>
constexpr float ThermistorValue(const std::array<float,N> &coeff, float R) {
float denom = 0.0;
for (size_t i = 0; i < N; ++i) {
denom += coeff[i] * std::pow(std::log(R), i); // log 和 pow
}
return 1.0f / denom;
}
constexpr float ResistanceFromDivider(float V0, float V, float R0) {
return R0 * V0 / (V0 - V);
}
constexpr float VoltageFromCode(float Vref, size_t n, uint16_t code) {
return Vref * code / std::pow(2, n);
}
- 然后生成表:
template<size_t N>
constexpr std::array<float, 256> ThermistorTable(
const std::array<float,N> &coeff, float R0, float V0, float Vref)
{
std::array<float, 256> table{};
for (size_t code = 0; code < 256; ++code) {
float V = VoltageFromCode(Vref, 8, code);
float R = ResistanceFromDivider(V0, V, R0);
table[code] = ThermistorValue(coeff, R);
}
return table;
}
// 使用例:
static constexpr std::array<float,4> ThermistorCoefficients {1e-3, 1e-4, 1e-7, 0};
constexpr auto ThermistorLookup = ThermistorTable(ThermistorCoefficients, 10e3, 3.3, 3.3);
4. 编译器生成的结果
- 生成的表会放在
.rodata
段,示例:
Section .rodata: ThermistorLookup 存储在这里
- 这样常量表直接在 Flash(非易失性存储)中,而不会占用 RAM。
- 可以通过
readelf
查看符号和段信息。
5. 注意事项
- 标准库的
std::log
和std::pow
不保证在constexpr
下可用。 - 解决办法:
- 使用专门的编译期数学库,例如 gcem:
denom += coeff[i] * gcem::pow(gcem::log(R), i);
- 适合嵌入式、实时系统,速度快且可靠。
6. 总结
- 编译期生成查找表的优点:
- 速度快,实时系统友好。
- 常量放在 Flash,不占 RAM。
- 不依赖外部生成工具,避免人为错误。
- 可直接维护在源代码里,硬件/固件更新更方便。
**嵌入式系统中各种“地址型”数据结构(address-like structures)**的定义与现代 C++ 表示方式,尤其强调可读性和易编辑性。我来帮你用梳理和理解一下核心点:
1⃣ 什么是 Address-Like Structures
嵌入式应用中经常会用到一些长的数字型结构,这些结构在程序中表示各种地址或标识符,例如:
- Bluetooth:128-bit UUID
- MAC 地址:48-bit
- 网络地址:
- IPv4:32-bit
- IPv6:128-bit
- USB 描述符:Unicode 编码的字符串
这些结构通常在编译时就已知,可以直接存储在非易失性存储(flash)中。
2⃣ 传统写法的问题
传统上,像 UUID、MAC、IP 地址、USB 字符串这样的数据,需要用十六进制或字符数组硬编码。例如:
// UUID
static constexpr uint8_t uuid1[] = { 0x01, 0x23, 0x45, 0x67, ... };
// MAC
static constexpr uint8_t mac1[] = { 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB };
// IPv4
static constexpr struct IP4Addr { uint8_t Address[4]; uint8_t MaskBits; } addr = {{192,168,0,1}, 24};
// USB Unicode 字符串
#define CHAR_TO_UNICODE(x) x, 0x00
static constexpr uint8_t DeviceDescription[] = { CHAR_TO_UNICODE('M'), CHAR_TO_UNICODE('y'), ... };
问题:
- 数字长而难读、难编辑
- 容易出错(顺序、大小写、分隔符)
3⃣ 现代 C++ 写法(可读、可编辑)
使用**用户自定义字面量(user-defined literals)**将字符串直接转换为地址结构:
UUID
class UUID {
private:
std::array<uint8_t,16> mData {0};
public:
UUID() = default;
friend constexpr UUID operator""_uuid(const char* text, size_t length);
};
constexpr UUID operator""_uuid(const char* text, size_t length) {
UUID result;
size_t count = 0;
for (size_t i=0; i<length; ++i) {
char c = text[i];
if (c >= '0' && c <= '9') result.mData[count++] = c - '0';
else if (c >= 'a' && c <= 'f') result.mData[count++] = c - 'a' + 10;
else if (c >= 'A' && c <= 'F') result.mData[count++] = c - 'A' + 10;
// 跳过非法字符(如 '-' 或空格)
}
if (count != 16) throw std::invalid_argument("Invalid UUID string");
return result;
}
// 使用:
static const UUID uuid = "{01234567-89AB-CDEF-0123-456789ABCDEF}"_uuid;
MAC 地址、IPv4 地址、USB 字符串
类似地,也可以定义 _macaddr
、_ip4addr
、_utf16
等字面量,把字符串直接转换成对应的数字数组或结构体。
优点:
- 可读性高:直接用常见的字符串形式
"12:34:56:78:90:AB"
- 易于编辑:不用记十六进制顺序
- 编译期生成:
constexpr
,无运行时开销
4⃣ 总结
- 传统方式:直接写十六进制数组,难读、易错
- 现代方式:用用户自定义字面量和
constexpr
类,字符串 → 数字数组 - 适用场景:UUID、MAC、IP 地址、USB 字符串描述符等嵌入式固定地址型数据
在嵌入式系统中使用轻量级、流式(stream-based)IO,而尽量跳过标准库的方式,并且展示了如何实现一个可扩展的轻量级 FileStream
类。下面我帮你用整理和理解关键点:
1. 为什么要“轻量级流式 IO”?
- 标准 C++ IO(
iostream
)在嵌入式平台上非常重,代码大小很大。 - 示例:
#include <iostream>
int main() {
std::cout << "Hello, world!" << "\r\n";
}
编译后:
- 使用标准库:
main-conventional.bin
约 157 KB - 轻量实现:
main-bare.bin
约 1.7 KB - 差异巨大,嵌入式系统通常没有那么多存储空间。
2. 创建轻量级 FileStream
定义一个类 FileStream
,用于不同的输出设备,例如 UART、SWO、UDP:
namespace mcu {
class FileStream {
public:
// 支持不同进制输出
using Radix_t = enum class RadixEnum { Binary=2, Octal=8, Decimal=10, Hexadecimal=16 };
protected:
size_t mFileDescriptor;
Radix_t mRadixSetting;
public:
FileStream(size_t fd) : mFileDescriptor(fd), mRadixSetting(RadixEnum::Hexadecimal) {}
// 输出字符串
FileStream& operator<<(const char* string) {
const size_t length = std::strlen(string);
_write(mFileDescriptor, string, length);
return *this;
}
// 输出整型、浮点数等可以通过模板扩展
template<typename U> requires std::integral<U>
FileStream& operator<<(U value) { ... }
template<typename U> requires std::floating_point<U>
FileStream& operator<<(U value) { ... }
};
} // namespace mcu
- 核心思想:跳过
iostream
,直接调用底层_write(fd, data, length)
,节省空间。
3. 输出设备实例
namespace mcu {
FileStream debug(1); // UART
FileStream swo(2); // SWO
}
底层 _write
根据 fd
调用不同的驱动:
extern "C" int _write(int fd, const void* data, size_t length) {
switch (fd) {
case 1: UARTWrite((const char*)data, length); break;
case 2: SWOWrite((const char*)data, length); break;
case 3: UDPWrite(connection, (const char*)data, length); break;
}
return length;
}
4. 使用示例
#include "filestream.hpp"
int main() {
mcu::swo << "Hello, Serial Wire Debug!" << "\r\n";
mcu::debug << "Hello, UART Debug!" << "\r\n";
}
- 编译后的二进制大小约 2.3 KB,比使用标准库小很多。
5. 扩展输出类型
- 可以通过重载
operator<<
支持更多类型,比如矩阵/张量:
template<typename T>
class Tensor { ... };
FileStream& operator<<(FileStream& stream, const Tensor<float>& tensor) {
for (size_t i = 0; i < tensor.Dimension(1); i++)
for (size_t j = 0; j < tensor.Dimension(0); j++)
stream << tensor.Element(i, j);
return stream;
}
- 这样就可以直接:
Tensor<float> result({10,10});
mcu::debug << result;
输出整个张量内容。
6. 支持进制输出
FileStream
内部维护mRadixSetting
,可以轻松输出二进制、十六进制等:
mcu::debug << FileStream::RadixEnum::Hexadecimal << 32 << "\r\n"; // 输出 0x20
总结
- 节省空间:跳过
<iostream>
,直接操作_write
。 - 流式接口:保留
operator<<
,代码可读性和传统流式写法一致。 - 易扩展:可以输出整型、浮点、字符串,也可以扩展到自定义类型(如张量)。
- 多设备支持:通过文件描述符
fd
区分不同输出设备(UART、SWO、UDP)。
在嵌入式系统中使用堆(heap)分配的利与弊,以及一些适用场景。我帮你整理成理解:
1. 嵌入式系统中使用堆的风险
使用堆分配在嵌入式系统中需要谨慎,主要有以下几个问题:
- 运行时可能耗尽堆
- 如果程序不断动态分配内存,可能会在运行时耗尽可用堆空间。
- 长期运行导致碎片化
- 嵌入式程序通常是长期运行的。
- 堆分配/释放会产生内存碎片,长时间运行可能出现无法解决的碎片化问题。
- 堆错误没有优雅的处理方式
- 当分配失败或内存损坏时,系统通常无法优雅地恢复,容易导致程序崩溃。
2. 常见做法
- 大多数嵌入式应用倾向于使用静态分配
- 静态分配在编译期就确定内存大小。
- 优点:可靠、确定性高,无碎片化风险。
- 禁止使用标准容器:
std::vector
、std::map
、std::list
、std::deque
因为这些容器内部会动态分配堆,风险同上。
3. 堆的安全使用场景
- “一次分配,重复使用”场景
- 可以在初始化阶段分配堆,然后长期使用,不做频繁的分配/释放。
示例:
- 可以在初始化阶段分配堆,然后长期使用,不做频繁的分配/释放。
class CalculationNode {
public:
using NodeHandle = std::unique_ptr<CalculationNode>;
private:
size_t mNodeNumber;
std::vector<NodeHandle> mNodeCollection; // 这里分配一次
public:
virtual bool Calculate() {
for (auto& node : mNodeCollection)
node->Calculate(); // 只使用已分配好的节点
}
};
- 特点:
- 节点在创建时分配内存 (
unique_ptr
)。 - 后续操作不再频繁分配/释放。
- 避免碎片化问题。
- 节点在创建时分配内存 (
总结
- 堆分配在嵌入式中风险高,要慎用。
- 安全用法:
- 一次性分配并长期使用。
- 避免频繁动态分配。
- 避免使用标准容器直接操作堆。
- 其他情况下推荐静态分配,保证系统稳定性。
1. 什么是 Arena Allocator
- Arena Allocator(区块分配器)是一种单调分配(monotonic)、一次性分配的内存管理策略。
- 内存从一个大区块(Arena)中顺序分配,不做释放操作。
- 特点:
- 快速:分配操作只需移动指针,无复杂堆管理。
- 开销低:没有额外的堆元数据或锁。
- 内存使用可预测:因为整个 Arena 是固定大小。
- 适合场景:
- 初始化阶段分配对象后长期使用。
- 嵌入式系统中避免堆碎片化。
参考资料:
- Lakos (2017)
- Steagall (2017)
2. 如何使用 Arena Allocator
(1)覆盖全局 new
操作符
void* operator new(size_t size)
{
static constexpr size_t ArenaSize = 1'000;
static char Arena[ArenaSize]; // 分配固定大小的内存池
auto ptr = Arena + AllocatedBytes; // 分配当前位置
AllocatedBytes += size; // 更新已分配字节数
return ptr;
}
- 优点:全局对象分配都走 Arena,快速且无堆碎片。
- 缺点:固定大小的 Arena,需要预估内存需求。
(2)按类覆盖 new
操作符
template<typename T>
class ArenaAllocator
{
static constexpr size_t ArenaSize = 1'000;
static char Arena[ArenaSize];
public:
T* allocate(size_t size) { /* ... */ }
};
class CalculationNode
{
public:
using NodeHandle = std::unique_ptr<CalculationNode>;
private:
std::vector<NodeHandle, ArenaAllocator<NodeHandle>> mCollectionOfNodes;
void* operator new(size_t size)
{
ArenaAllocator<char> alloc;
return alloc.allocate(size);
}
};
- 为特定类使用 Arena 分配器,避免全局影响。
- 可以用在像
CalculationNode
这样的复杂数据结构中。 - 保证运行时分配,但避免堆碎片和 heap 开销。
3. 优势总结
- 可在运行时动态分配对象,同时避免:
- 堆碎片化
- 堆管理开销(如 malloc/free 的时间和空间成本)
- 安全可靠:
- 一旦 Arena 分配好,分配操作简单且确定性高。
- 非安全关键嵌入式应用非常适合。
- 适合单调、allocate-once 的场景:
- 初始化阶段分配完毕后长期使用,几乎没有动态内存管理风险。
核心思想:
- 初始化阶段分配完毕后长期使用,几乎没有动态内存管理风险。
“在嵌入式系统中,如果必须用堆,就用 Arena 分配器,避免碎片化和不可预测的堆开销,同时保证分配快速和可预测。”
在嵌入式系统中解锁时间和随机数功能,以及如何自己实现类似 std::chrono
和 std::random_device
的功能。我帮你整理成理解如下:
1. 解锁时间功能(std::chrono
)
嵌入式时间源
- 相对时间:硬件定时器(Hardware Timer),用于测量时间间隔。
- 绝对时间:实时时钟(RTC),用于获取日历时间和时间戳。
- 应用:
- 性能统计(Performance instrumentation)
- 日历时间(Time-of-day)
- 高分辨率计时(High-resolution timing)
配置微秒级硬件定时器
static TIM_TypeDef* ChronoHWTimer = nullptr;
bool MicrosecondClockConfigure(TIM_TypeDef* timer, volatile uint32_t& rccreg)
{
bool success = true;
const uint32_t timer_freq = 1'000'000; // 1 MHz = 1 µs
ChronoHWTimer = timer;
rccreg |= rccvalue; // 打开定时器时钟
timer->PSC = (ClockTree.pclk2 / timer_freq) - 1; // 预分频
timer->ARR = 0xFFFFFFFF; // 自动重装载寄存器最大值
timer->CNT = 0ul; // 计数器清零
return success;
}
ChronoHWTimer
用作全局定时器,提供微秒级计数。- 配置定时器的预分频和计数器范围,使得
std::chrono::high_resolution_clock
可以使用。
自定义 std::chrono::high_resolution_clock::now()
namespace std::chrono
{
time_point<high_resolution_clock> high_resolution_clock::now()
{
return time_point(
nanoseconds(1000ull * static_cast<nanoseconds::rep>(ChronoHWTimer->CNT))
);
}
}
- 使用硬件计数器
CNT
返回高分辨率时间点。 - 乘以
1000
将微秒转换为纳秒,符合std::chrono
时间单位。
可选择的时钟类型
system_clock
:墙钟时间(RTC),提供实际日期时间。steady_clock
:单调递增,不受系统时间调整影响。high_resolution_clock
:最高分辨率计时。utc_clock
:UTC 标准时间。
2. 随机数生成器(std::random_device
)
设计目标
- 嵌入式系统中实现硬件随机数生成器(RNG)。
- 支持类似
std::random_device
接口。 - 可用于生成各种概率分布的随机数,例如 双峰分布(Bimodal Distribution)。
MCU 随机数设备类
namespace mcu
{
class random_device
{
public:
using result_type = unsigned int;
static constexpr result_type min() { return std::numeric_limits<result_type>::min(); }
static constexpr result_type max() { return std::numeric_limits<result_type>::max(); }
constexpr double entropy() const noexcept { return 32.0; }
random_device()
{
RCC->AHB2ENR |= RCC_AHB2ENR_RNGEN; // 使能 RNG 时钟
RNG->CR |= RNG_CR_RNGEN; // 使能硬件 RNG
}
random_device& operator=(const random_device& other) = delete;
result_type operator()()
{
while (!(RNG->SR & RNG_SR_DRDY)) continue; // 等待随机数就绪
return RNG->DR; // 返回随机数
}
};
}
RCC->AHB2ENR
:使能 RNG 时钟。RNG->CR
:控制寄存器,启动硬件随机数。RNG->SR & RNG_SR_DRDY
:检查随机数是否就绪。RNG->DR
:读取生成的随机数。
多模态分布示例
class BimodalDistribution
{
std::normal_distribution<float> mFirstMode;
std::normal_distribution<float> mSecondMode;
std::uniform_real_distribution<float> mWeight;
float mMode1Weight;
template<typename Generator>
float operator()(Generator& device)
{
auto x = mWeight(device);
if (std::abs(x) < mMode1Weight)
return mFirstMode(device);
else
return std::copysign(1.0f, x) * mSecondMode(device);
}
};
- 使用硬件 RNG 作为
device
。 - 结合不同概率分布实现复杂随机行为(例如“随机游走”带大跳变)。
总结 - 时间功能:通过硬件定时器配置,实现微秒级计时,并可用在
std::chrono
接口。 - 随机数功能:通过 MCU 硬件 RNG 实现
std::random_device
接口,可与标准概率分布组合使用。 - 在嵌入式系统中,你可以解锁时间与随机数功能而无需依赖全功能标准库。
如果你愿意,我可以帮你画一张嵌入式std::chrono
+random_device
总览图,清楚显示: - 硬件定时器如何驱动高分辨率时钟
- 硬件 RNG 如何生成随机数
- 软件接口(
std::chrono
和random_device
)如何与硬件对接
更多推荐
所有评论(0)