当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]传感器采集程序看似简单——读ADC、滤波、换算、输出。但用STM32标准库+HAL+printf实现后,Flash轻松突破20KB。对于成本敏感的芯片(如STM32G030,32KB Flash),剩下的空间还要跑协议栈和业务逻辑,必须把采集程序压缩到8KB以内。本文分享四个实战技巧,将一个温度采集+均值滤波+UART输出的程序从18KB砍到7.2KB。


传感器采集程序看似简单——读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空间时,这些技巧就是救命稻草。



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