当前位置:首页 > EDA > 电子设计自动化
[导读]资源受限MCU跑C++,体积膨胀往往不是class本身,而是异常栈展开表、RTTI的typeinfo、动态分配和未用模板实例被链进固件。实际工程里,把异常、RTTI、线程静态guard、未用段一起关掉,再配合LTO和gc-sections,带多态与STL小对象的工程常能把Flash缩减约40%;纯GPIO/状态机类较轻,降幅会小一些。


资源受限MCU跑C++,体积膨胀往往不是class本身,而是异常栈展开表、RTTI的typeinfo、动态分配和未用模板实例被链进固件。实际工程里,把异常、RTTI、线程静态guard、未用段一起关掉,再配合LTO和gc-sections,带多态与STL小对象的工程常能把Flash缩减约40%;纯GPIO/状态机类较轻,降幅会小一些。

一、基础编译选项

ARM GCC裸机建议:

CXXFLAGS += -mcpu=cortex-m4 -mthumb -std=c++17 -Os

CXXFLAGS += -fno-exceptions -fno-rtti -fno-threadsafe-statics

CXXFLAGS += -ffunction-sections -fdata-sections

CXXFLAGS += -fno-unwind-tables -fno-asynchronous-unwind-tables

CXXFLAGS += -flto -Wall -Wextra

LDFLAGS  += -Wl,--gc-sections

-fno-exceptions去掉try/catch、栈展开和__cxa_throw等运行时;-fno-rtti去掉dynamic_cast/typeid及每类typeinfo,多态类只保留虚表、不再附加类型名字符串。 -fno-threadsafe-statics避免局部static构造插入guard锁,裸机单线程/未起RTOS前更安全。 配合-ffunction-sections/-fdata-sections与--gc-sections,未调用函数、未用模板实例可被链接器删除。

二、用错误码替代throw

禁用异常后,错误走返回值。轻量Result可自己写:

enum class Err : uint8_t { Ok, Param, Busy, Crc, Timeout };


template<typename T>

struct Result {

   T value; Err err;

   constexpr bool ok() const { return err == Err::Ok; }

};


Result<uint16_t> read_reg(uint8_t id, uint16_t addr){

   if(id >= 8) return {0, Err::Param};

   uint16_t v = bus_read(id, addr);

   if(crc_check(id, addr, v) == false) return {0, Err::Crc};

   return {v, Err::Ok};

}

调用处必须判ok(),不会因throw未捕获进terminate。C++23可用std::expected,老工具链用上面这个零分配版本更稳。

三、用CRTP替代dynamic_cast

运行时类型查询在MCU里大多可编译期确定。基类模板转发到派生类,无虚表、无RTTI:

template<typename D>

struct SensorBase {

   int read(uint8_t* buf, uint16_t len){

       return static_cast<D*>(this)->read_impl(buf, len);

   }

};


struct TempSensor : SensorBase<TempSensor> {

   int read_impl(uint8_t* buf, uint16_t len){

       buf[0] = raw_temp >> 8; buf[1] = raw_temp & 0xFF;

       return (len >= 2) ? 2 : 0;

   }

   uint16_t raw_temp = 0;

};

多传感器同接口、不同实现,编译期生成各自代码,调用可被内联;相比继承+虚函数省vtable指针,相比dynamic_cast彻底不依赖typeinfo。 若必须运行时分发,用函数指针表或枚举state,比虚继承更可控。

四、禁动态分配,用静态容器

std::vector/std::string会引入堆分配与异常路径,小MCU尽量不用。固定容量用std::array,字符串用char[]+snprintf,复杂表用ETL的etl::vector/etl::string或自写池:

alignas(uint32_t) static uint8_t pool[512];

inline void* operator new(size_t n, void* p) noexcept { (void)n; return p; }

inline void operator delete(void*, void*) noexcept {}


struct LogBuf {

   char data[128]; uint16_t len = 0;

   void append(const char* s){

       while(*s && len < sizeof(data)-1) data[len++] = *s++;

       data[len]=0;

   }

};

全局重载operator new为delete,可让意外写new直接链接受阻;只保留placement new构造静态对象,避免碎片和非确定时延。

五、验收与边界

构建后用size看.text/.data/.bss,用nm --size-sort筛大头:

arm-none-eabi-size firmware.elf

arm-none-eabi-nm --size-sort firmware.elf | tail -20

对比基准:开异常+RTTI、关异常RTTI、再加LTO+gc-sections三档。异常重度工程(多try、深调用、STL异常处理)关掉后.eh_frame/.gcc_except_table可省不少;RTTI仅在有大量多态类时省得多,纯模板工程本来就几乎不靠它。 注意:关异常后第三方C++库若内部throw,要换无异常移植版或自己包错误码;构造函数不做阻塞硬件操作,全局对象构造顺序用函数内static局部替代,降低启动依赖。

按“禁异常RTTI+禁隐式堆+CRTP/错误码+sections/LTO/gc”这套收口,8~64KB级Flash的Cortex-M工程能把C++写得接近等价C的体积,同时保留类型安全、模板复用和RAII式资源管理。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭