解决printf时导致死机的问题

我第一次使用CH32V307芯片,以下内容内容难免瑕疵,所以仅供参考,希望帮助有类似困惑的开发者。


系统启动后在运行第一行printf时导致死机并进入HardFault,原因是heap内存不足,C库分配printf缓存失败并导致后续故障。


我在sbrk函数中添加了一段日志输出:

__attribute__((used)) void *_sbrk(ptrdiff_t incr)
{
    extern char _end[];
    extern char _heap_end[];
    static char *curbrk = _end;

    if ((curbrk + incr < _end) || (curbrk + incr > _heap_end)){
        const char msg[] = "sbrk overflow\r\n";
        _write(0, (char*)msg, sizeof(msg));
        return NULL - 1;
    }

    curbrk += incr;
    return curbrk - incr;
}

通过这段消息我才定位到是堆内存溢出导致的故障。


进一步分析堆内存为什么会溢出,通过检查_end指针和_heap_end指针,发现有效的堆空间已经不足1000字节。进一步分析链接脚本,发现官方提供的链接脚本没有保证heap空间的最小值,当用户占用的RAM要接近最大值时,heap空间的长度没有保证。

    .bss :
    {
        . = ALIGN(4);
        PROVIDE( _sbss = .);
        *(.sbss*)
        *(.gnu.linkonce.sb.*)
        *(.bss*)
        *(.gnu.linkonce.b.*)        
        *(COMMON*)
        . = ALIGN(4);
        PROVIDE( _ebss = .);
    } >RAM AT>FLASH

    PROVIDE( _end = _ebss);     /* heap的起始地址夹在bss段和stack段之间 */
    PROVIDE( end = . );         /* 这中间的剩余空间大小不好控制 */

    .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size :
    {
        PROVIDE( _heap_end = . );    
        . = ALIGN(4);
        PROVIDE(_susrstack = . );
        . = . + __stack_size;
        PROVIDE( _eusrstack = .);
        __freertos_irq_stack_top = .;
    } >RAM


调整一下链接脚本:

ENTRY( _start )

__stack_size = 2048;
__heap_size  = 2048;        /* 显式声明heap空间大小 */

SECTIONS
{
    /* other section */

    .bss :
    {
        . = ALIGN(4);
        PROVIDE( _sbss = .);
        *(.sbss*)
        *(.gnu.linkonce.sb.*)
        *(.bss*)
        *(.gnu.linkonce.b.*)
        *(COMMON*)
        . = ALIGN(4);
        PROVIDE( _ebss = .);   /* 这句可以调整到下面 */
    } >RAM

    .heap :
    {
        . = ALIGN(4);
        PROVIDE ( end = . );
        PROVIDE ( _end = . );
        . = . + __heap_size;
        . = ALIGN(4);
        PROVIDE( _heap_end = . );
    } >RAM

    .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size :
    {
        . = ALIGN(4);
        PROVIDE(_susrstack = . );
        . = . + __stack_size;
        PROVIDE( _eusrstack = .);
        __freertos_irq_stack_top = .;
    } >RAM
}


以上这个链接脚本将固定heap空间为__heap_size设置的大小,heap段和stack段之间剩余的空间将可被stack使用,可避免栈溢出。如果想将剩余空间分配到heap,可将PROVIDE( _heap_end = . );移动到stack段的起始位置。


在我的开发环境中,默认情况下使用printf会使用1500字节左右的heap空间,如果你希望printf减小heap的使用,可将标准输出的行缓冲模式改为0缓冲模式,在第一次使用printf函数前执行setvbuf函数。

int main(void)
{
    setvbuf(stdout, NULL, 0, _IONBF);       /* 零缓冲模式 */

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    SystemCoreClockUpdate();
    Delay_Init();
    USART_Printf_Init(115200);

    printf("SystemClk:%d\r\n",SystemCoreClock);
    printf("ChipID:%08x\r\n", DBGMCU_GetCHIPID() );

    while(1);
}


这样可大幅减少heap空间的使用。


当出现故障时我们可以在HardFault函数中输出必要的信息来协助分析问题:

#include 

void HardFault_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));

extern int _write(int fd, char *buf, int size);
static int fault_printf(const char *format, ...)
{
    #define FAULT_MSG_LEN (32)
    static char buf[FAULT_MSG_LEN];
    int len;
    va_list args;
    va_start(args, format);
    len = vsnprintf(buf, FAULT_MSG_LEN, format, args);
    va_end(args);
    if(len > FAULT_MSG_LEN)
        len = FAULT_MSG_LEN;
    _write(0, buf, len);
    return len;
}

void HardFault_Handler(void)
{
    fault_printf("HardFault:\r\n");
    fault_printf("MSTATUS: %#x\r\n", __get_MSTATUS());
    fault_printf("MCAUSE : %#x\r\n", __get_MCAUSE());
    fault_printf("MEPC   : %#x\r\n", __get_MEPC());

    if((__get_MCAUSE() & 0x80000000) == 0){
        uint16_t exc_id = __get_MCAUSE() & 7;
        const char* cause[8] = {
            "Code unaligned",
            "Code can't access",
            "Code error",
            "Break point",
            "Load MEM unaligned",
            "Load MEM error",
            "Store MEM unaligned",
            "Store MEM error",
        };
        if(exc_id < 8){
            fault_printf("%s\r\n", cause[exc_id]);
        }
    }
    while (1)
    {
    }
}


在HardFault使用了一个自定义的print,这是为了避免使用C库的printf时出现内存分配失败后导致的一系列连续故障。在HardFault中出现新的HardFault故障会把事情搞得更加复杂。


最后,我们来调整一下MounRiver软件的设置,这样可以让我们能够清楚直观的看到Flash和RAM分别用了多少。


在工程设置中定位到C/C++build - Setting中,在链接选项中添加一个额外的链接选项 -Wl,--print-memory-usage


1710471903383150.jpg

这样比直接显示各个字段的大小要更加直观。

1710471903200868.jpg

上图显示RAM用了93%,当RAM占用接近最大时,需要及时调整代码中各个模块的内存分配。

您好,可以按照下图勾选使用我们的打印库试一下,应该不会有堆内存溢出的问题。使用我们打印库时,main函数中setvbuf那个函数就不需要加了。后续若有问题,可邮箱(lzs@wch.cn)和我沟通。

image.png


只有登录才能回复,可以选择微信账号登录