STM32嵌入式系统堆栈溢出问题详解及解决方案探讨

前言

在嵌入式系统开发中,堆栈溢出是常见的问题,尤其是在处理大量数据或执行复杂算法(如FFT)时更为突出。本文将深入分析STM32中堆栈溢出的原因、检测方法及解决方案,并以一个实际的FFT处理程序为例进行剖析。

问题描述

最近在STM32G431项目中实现FFT功能时,发现程序无法正常进入Filter_Mean_static函数。代码中使用了大小为1024的FFT,但当程序尝试执行滤波函数时,系统似乎"卡死"。程序默认栈大小设置为0x400(1KB),堆大小为0x200(512B)。

堆栈基础知识

STM32内存布局

STM32微控制器的内存布局通常如下:

高地址 ┌──────────────────┐
      │      栈区        │ ← 栈从高地址向低地址增长
      │        ↓         │
      │                  │
      │        ↑         │
      │      堆区        │ ← 堆从低地址向高地址增长
      │                  │
      │    全局/静态变量区 │
      │                  │
      │     只读数据区    │
      │                  │
低地址 │     程序代码区    │
      └──────────────────┘

堆与栈的区别

| 特性 | 栈(Stack) | 堆(Heap) |
|-----|-----------|----------|
| 分配方式 | 编译时确定大小,自动管理 | 运行时动态分配 |
| 增长方向 | 从高地址向低地址 | 从低地址向高地址 |
| 分配速度 | 非常快,只需移动栈指针 | 相对较慢,需内存管理算法 |
| 生命周期 | 函数调用期间,自动释放 | 手动分配与释放 |
| 存储内容 | 局部变量、函数参数、返回地址 | 动态分配的数据结构 |
| 控制方式 | 系统自动管理 | 程序员手动管理 |

STM32栈设置

STM32的栈大小在启动文件中定义,例如startup_stm32g431xx.s:

; Amount of memory (in bytes) allocated for Stack
Stack_Size      EQU     0x400

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp

问题分析

代码分析

问题代码的核心部分:

#define FFT_LENGTH 1024

// 全局变量 (现在的实现)
uint16_t adcBuff[FFT_LENGTH];
float fft_inputbuf[FFT_LENGTH * 2];  
float fft_outputbuf[FFT_LENGTH];
float adcfilter[FFT_LENGTH]; // 均值滤波后数据

void Output_FFT_Array(void)
{
    HAL_TIM_Base_Start_IT(&htim3);
    HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adcBuff, FFT_LENGTH);
    while(!AdcConvEnd1);
    
    // 数据处理
    for(int i=0; i<FFT_LENGTH; i++) {
        adcBuff[i] = adcBuff[i]*VREF/4096.0f;
        fft_inputbuf[2*i] = adcBuff[i];
        fft_inputbuf[2*i+1] = 0;
    }
    
    HAL_Delay(2000);
    Filter_Mean_static((float *)adcBuff, adcfilter, 10, FFT_LENGTH); // 滤波
    // 其他代码...
}

// 滤波函数
void Filter_Mean_static(float *input_data, float *output_data, int Filter_length, int datalength)
{
    float sum = 0;
    for(int i=0; i<(datalength-Filter_length); i++) {
        // 滤波处理
        for(int j=i; j<Filter_length+i; j++) {
            sum += input_data[j];
        }
        output_data[i] = sum/Filter_length;
        sum = 0.0f;            
    }
    // 其他代码...
}

发生栈溢出的主要原因是因为我,在函数中定义了一个1024大小的float型数组,在栈中需要4096B大小存放,导致程序编译运行时直接卡死。找了好久才发现问题。后来把局部变量改为全局变量,又增加了栈大小才解决。

问题原因分析

最初的问题是将adcfilter数组定义为局部变量:

void Output_FFT_Array(void)
{
    float adcfilter[FFT_LENGTH]; // 局部变量, 占用4KB栈空间
    // ...
}

这导致了栈溢出:

  • FFT_LENGTH为1024,float类型占4字节,因此adcfilter数组需要4KB内存
  • STM32G431默认栈大小为1KB(0x400)
  • 当函数尝试在栈上分配4KB的数组时,超过了可用栈空间,导致栈溢出
  • 函数调用栈分析

    当函数嵌套调用时,栈的使用是累加的:

    Output_FFT_Array栈帧:
    - 局部变量
    - 返回地址等信息
    
    ↓ 调用Filter_Mean_static
    
    Output_FFT_Array栈帧 (保持不变)
    +
    Filter_Mean_static栈帧:
    - 局部变量
    - 返回地址等信息

    当嵌套调用链上有大型局部数组时,很容易导致栈溢出。

    解决方案

    1. 使用全局变量

    将大型数组从栈移到全局/静态存储区:

    // 在文件顶部定义全局变量
    float adcfilter[FFT_LENGTH];
    
    void Output_FFT_Array(void)
    {
        // 使用全局变量,不在栈上分配空间
        Filter_Mean_static((float *)adcBuff, adcfilter, 10, FFT_LENGTH);
    }

    2. 增加栈大小

    在启动文件中增加栈大小:

    Stack_Size      EQU     0x2000  ; 从0x400(1KB)增加到0x2000(8KB)

    3. 减小数组大小

    如果不需要这么高的精度,可以减小FFT_LENGTH:

    #define FFT_LENGTH 512  // 从1024减为512

    4. 使用动态内存分配

    void Output_FFT_Array(void)
    {
        float *adcfilter = (float *)malloc(FFT_LENGTH * sizeof(float));
        if(adcfilter) {
            Filter_Mean_static((float *)adcBuff, adcfilter, 10, FFT_LENGTH);
            free(adcfilter);
        }
    }

    5. 分段处理数据

    #define SEGMENT_SIZE 256
    
    void Output_FFT_Array(void)
    {
        float segment_filter[SEGMENT_SIZE];
        
        for(int seg=0; seg<FFT_LENGTH/SEGMENT_SIZE; seg++) {
            // 处理每个段
            Filter_Mean_static((float *)&adcBuff[seg*SEGMENT_SIZE], 
                             segment_filter, 
                             10, 
                             SEGMENT_SIZE);
                             
            // 复制结果到目标数组
            memcpy(&adcfilter[seg*SEGMENT_SIZE], segment_filter, SEGMENT_SIZE*sizeof(float));
        }
    }

    检测堆栈溢出的方法

    硬件方法:使用STM32的MPU或Stack指针监控

    软件方法:栈填充模式,在栈区填充特定模式后检测变化

    编译器选项:开启栈溢出检测 -fstack-protector

    调试输出:在关键函数前后添加输出,确定程序执行位置

    递增测试:逐步减小数组大小,直到程序正常运行

    栈使用分析

    在本例中,Output_FFT_Array函数的栈使用情况:

  • 局部变量(int i): 4字节
  • 函数调用开销(HAL_TIM_Base_Start_IT等): ~20-30字节/调用
  • 子函数本地变量(Filter_Mean_static中的sum等): ~20字节
  • printf开销: ~100字节
  • 总计约200-300字节,远小于设置的1KB栈空间。但如果添加局部大数组(例如4KB的float数组),则会远超栈大小。

    总结

    栈的自动内存管理特性

  • 栈变量在函数调用时自动分配,函数返回时自动释放
  • 分配和释放仅通过移动栈指针完成,速度快但空间严格受限
  • 函数执行期间,其栈空间始终占用直到函数返回
  • 栈溢出问题本质

  • STM32典型栈空间仅1KB,而单个大型数组就可能需要4KB
  • 函数调用链上的栈空间累加使用,嵌套调用时风险倍增
  • 溢出不产生编译警告,但会导致程序崩溃或不可预测行为
  • 解决方案核心

  • 大型数组(>200字节)改用全局变量或静态变量
  • 需要灵活大小的数组使用堆分配(malloc)
  • 降低单次处理数据量,采用分段处理策略
  • 栈变量简单易用,但在资源受限的MCU上,与其便利性相比,更应注重内存分配的合理性。

    作者:taptaptap.jic

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32嵌入式系统堆栈溢出问题详解及解决方案探讨

    发表回复