我第一次使用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
这样比直接显示各个字段的大小要更加直观。
上图显示RAM用了93%,当RAM占用接近最大时,需要及时调整代码中各个模块的内存分配。