嵌入式C语言内存优化实战:把传感器采集程序Flash占用压缩到8KB以内
传感器采集程序看似简单——读ADC、滤波、换算、输出。但用STM32标准库+HAL+printf实现后,Flash轻松突破20KB。对于成本敏感的芯片(如STM32G030,32KB Flash),剩下的空间还要跑协议栈和业务逻辑,必须把采集程序压缩到8KB以内。本文分享四个实战技巧,将一个温度采集+均值滤波+UART输出的程序从18KB砍到7.2KB。
一、抛弃标准库和HAL,直接操作寄存器
HAL库和标准库方便但臃肿。一个HAL_UART_Transmit背后调用多层函数,占用数KB。改用寄存器操作,UART发送函数只需几行:
// 寄存器版UART发送(以STM32G0为例)
void uart_send(uint8_t *data, uint16_t len) {
for(uint16_t i=0; i<len; i++) {
while(!(USART1->ISR & USART_ISR_TXE_TXFNF));
USART1->TDR = data[i];
}
while(!(USART1->ISR & USART_ISR_TC));
}
相比HAL版本,此函数占用约60字节,而HAL版至少600字节。ADC初始化同样直接操作寄存器,省去MX_ADC1_Init的庞大配置结构。
二、用定点数代替浮点运算
温度传感器(如NTC)的Steinhart-Hart方程涉及对数运算,标准做法用math.h的logf,但浮点库会拉进数KB的数学函数。改用查表+线性插值,或者用定点数近似:
// 定点数线性化:假设已知10个温度-电阻对应点
#define TABLE_SIZE 10
const int16_t temp_table[TABLE_SIZE] = { -20, 0, 20, 40, 60, 80, 100, 120, 140, 160 }; // 温度*10
const uint16_t adc_table[TABLE_SIZE] = { 3800, 2900, 2100, 1450, 950, 620, 420, 280, 190, 130 };
int16_t calc_temp(uint16_t adc_val) {
// 二分查找区间
uint8_t i;
for(i=0; i<TABLE_SIZE-1; i++) {
if(adc_val >= adc_table[i+1]) break;
}
// 线性插值,结果单位为0.1℃
int32_t t0 = temp_table[i];
int32_t t1 = temp_table[i+1];
int32_t a0 = adc_table[i];
int32_t a1 = adc_table[i+1];
return t0 + (t1 - t0) * (adc_val - a0) / (a1 - a0);
}
此函数仅使用整数运算,不链接任何浮点库,节省约4KB。
三、精简printf,改用自定义格式化输出
printf及其依赖的vsnprintf是Flash大户,通常占用5KB以上。如果只是输出"Temp: 25.3C\r\n",可以手写一个整数转字符串函数:
void put_int(int16_t val, uint8_t decimals) {
char buf[8];
uint8_t pos = 0;
if(val < 0) { uart_putc('-'); val = -val; }
uint16_t int_part = val / decimals;
uint16_t frac_part = val % decimals;
// 整数部分倒序
do { buf[pos++] = '0' + int_part % 10; int_part /= 10; } while(int_part);
while(pos--) uart_putc(buf[pos]);
if(decimals > 1) {
uart_putc('.');
uart_putc('0' + frac_part / (decimals/10));
uart_putc('0' + frac_part % (decimals/10));
}
}
配合uart_putc,输出温度只需put_int(temp, 10)。去掉printf及相关库,节省5KB以上。
四、编译选项与链接优化
在Makefile或IDE中开启以下选项,进一步压缩:
CFLAGS += -Os -flto -fdata-sections -ffunction-sections
LDFLAGS += -Wl,--gc-sections -Wl,--print-gc-sections
-Os:尺寸优化
-flto:链接时优化,跨文件裁剪未用函数
-fdata-sections -ffunction-sections:将每个函数/数据放入独立段
--gc-sections:链接时剔除未引用的段
此外,将中断向量表放在Flash开头,使用__attribute__((section(".text")))将非常用的初始化代码移到尾部,让链接器自动回收。
五、实测对比
优化措施 Flash占用 说明
原始HAL+printf+浮点 18.2 KB 基线
寄存器+HAL移除 12.5 KB 省5.7KB
定点查表替代浮点 8.8 KB 省3.7KB
自定义输出替代printf 7.2 KB 省1.6KB
编译选项+gc-sections 6.8 KB 省0.4KB
最终程序仅6.8KB,剩余空间足够放下Modbus RTU协议栈。
六、三个翻车高发点
忘记关闭浮点printf支持:即使没用printf,链接器可能因某些库函数(如snprintf)拉入浮点转换代码。检查map文件中是否有_printf_float,若有则加-u _printf_float或改用--specs=nano.specs --specs=nosys.specs。
查表区间边界处理:ADC值超出表格范围时,插值可能溢出。在函数开头加饱和处理,返回边界值。
链接器脚本未配合:--gc-sections需要-ffunction-sections配合,否则无效。同时确认链接脚本中没有KEEP多余的段。
将传感器采集程序压缩到8KB以内,本质是对“默认便利”的剥离——放弃HAL、浮点库、printf,用寄存器、定点数和手写输出替代。这不仅节省Flash,还提升了实时性。当你的MCU只剩8KB空间时,这些技巧就是救命稻草。





