NOLOAD 在自定义段的应用 - CH573 HardFault排查日记

8/24/2023 C语言BLE

# 前言

想做一个低功耗的复合宏键鼠,看上了CH571这款片子,先去弄了个板子开发着。

根据数据手册,Data区域分为16K+2K,低电压时可以选择断电。又RV32的栈是向下增长的,原始链接脚本直接将栈底放在RAM最后部,无法满足需求。

# 开始改造

# 先看一眼原始链接脚本


ENTRY( _start )

MEMORY
{
	FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 448K
	RAM (xrw) : ORIGIN = 0x20003800, LENGTH = 18K
}

SECTIONS
{

    /* 省略 */

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

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 开改

既然要将指定的数据放到后2K位置,容易想到自定义一个段,这里我叫他(.lram),将段地址指定一下。

修改后的链接脚本:


ENTRY( _start )

__lram_size = 256;

MEMORY
{
	FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 448K
	RAM (xrw) : ORIGIN = 0x20003800, LENGTH = 18K
}

SECTIONS
{

    /* 省略 */

	.stack ORIGIN(RAM)+LENGTH(RAM) - __lram_size :
	{
		. = ALIGN(4);
		PROVIDE(_eusrstack = . );
	} >RAM  
	
	.lram ORIGIN(RAM)+LENGTH(RAM) - __lram_size  :
	{
		. = ALIGN(4);
		*(.lram);
		*(.lram.*);
		. = ALIGN(4);
	} > RAM 

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

定义好以后,在源码中定义一个结构体,并按照 GCC 的规范加上 attribute,如下:


typedef struct {
    uint32_t passCode;
    uint8_t rsv[236];

} globalVar_t, *pglobalVar_t; // 256 Bytes max

__attribute__((section(".lram")))
globalVar_t globalVar;

1
2
3
4
5
6
7
8
9
10

编译通过后,用WCHISPStudio 3.40 烧录到板子上,成功获得一个不断重启且指示原因为 RPOR(真上电复位)的工程。

# 排查原因

# 东找西找

起初怀疑是破坏了栈,将栈地址前移4 Bytes,正常了,但是,还是有那么一点不对劲啊---- 原先的栈就是在RAM尾部,如果向那个地址写入,肯定会HardFault,所以不应该是那个原因。

多次尝试无果,只好向WCH的技术论坛发帖询问,得到了另一篇调试方法的博文 (opens new window),按照此文方法修改 HardFault 中断,获取异常位置。

烧录后,提示触发 HardFault 中断,mepc 是 0x7000 - 0x7e00之间的一个值(每次重新编译都不尽相同),mcause 指示为非法指令(0x2),mtval为0。

遂翻阅 Listing,查找到地址,发现是BLE库内部的函数,且出错的指令是一条普通的 li ,即加载立即数到寄存器中,不应该是非法指令。

然后怀疑是烧录问题,进行烧录校验,一切正常。(BTW:不开启代码保护还烧不进去,这种情况下居然能校验成功,想不明白)

又怀疑是hex转bin出了问题,直接将输出格式修改为bin,生成了512M大小的文件(乐),用 winhex 打开查看对应位置的数据,和Listing一致。随后打算烧录该 bin 文件,提示长度非法(必然的嘛,中间500M都是保留地址)。

# 发现踪迹

仍然怀疑 Flash 内容有问题,遂更改 HardFault 中断代码,将 mepc 前后 512 Bytes 的值都通过串口打印出来,如下:


__attribute__((interrupt("WCH-Interrupt-fast")))
__attribute__((section(".highcode")))
void HardFault_Handler(void) {

    SetSysClock(CLK_SOURCE_PLL_60MHz);
    DelayMs(1);
    uint32_t v_mepc, v_mcause, v_mtval;

    __asm volatile("csrr %0,"
            "mepc"
            : "=r"(v_mepc));

    __asm volatile("csrr %0,"
            "mcause"
            : "=r"(v_mcause));

    __asm volatile("csrr %0,"
            "mtval"
            : "=r"(v_mtval));

    uart1_send_uint32(v_mepc);
    uart1_send_newline();

    // Modify
    uint32_t val = 0;
    for(uint32_t i=v_mepc - 512;i<v_mepc+512;i+=4){
        FLASH_ROM_READ(i,&val, 4);
        uart1_send_uint32(val);
        uart1_send_newline();
    }

    uart1_send_newline();

    uart1_send_uint32(v_mcause);
    uart1_send_newline();
    uart1_send_uint32(v_mtval);
    uart1_send_newline();

    while(1);

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

编译并烧录,得到了这样的输出:

sys_rst: 1
Lib Version: CH57x_BLE_LIB_V2.10
UST
00007fd8
94018793

/* 省略 */

f7178082
47031fff
444126e7
fef714e3
fff50793
0ff7f793
00000000
00000000
00000000

/* 一共60行 00000000 */

00000000
00000000
1ffff717
16f71723

/* 省略 */

45dca819
0007d783

00000002
00000000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

注意到中间有60行0x00000000,这是妥妥的非法指令,而且数量正好是 60 * 4 = 240 字节,和我定义的结构体大小一致。

# 顺藤摸瓜

于是烧写异常实锤了。但是,到底是 hex 文件问题,还是烧录软件的问题呢?我打开了 hex 文件,找到对应地址,发现数据是对的。如下:


:107F0000011126CA4AC806CE22CCAE84328936C6B2
:107F10003AC43EC242C0EF904FF1630E0510938504
:107F2000A4FFC2050566C1819308A6C72A844945F6
:107F300063E2B808130606C8636E2607636C9906E9
:107F4000B2469305301F63E7D50622476364E60611
:107F5000138616003306960293153700635CB60449
:107F60000346E400058A39E68325440913F66500D3
:107F700031E2924702482318E404034774002316B1
:107F8000F406231494062315240723170407231744
:107F9000D4048347A401014551E389EF8347840D4D
:107FA000898B99CF93E52500232AB408C947230D6F
:107FB000F4000145F2406244D244424905618280A6
:107FC000638B2405B305990085812685EFD0CE8A81
:107FD0002316A4049305C4044A85EF30204197F783
:107FE000FE1F83D767358D46114763E5F600850789
:107FF00013F7F70FA304E4042285EF80EFBF835744

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

提示

使用 VSCode + Intel Hex 插件可获得高亮

但是文件的后面,出现了另外一段地址相似的内容,全是0,一共240 Bytes,如下:


:020000020000FC
:020000042000DA
:107F00000000000000000000000000000000000071
:107F10000000000000000000000000000000000061
:107F20000000000000000000000000000000000051
:107F30000000000000000000000000000000000041
:107F40000000000000000000000000000000000031
:107F50000000000000000000000000000000000021
:107F60000000000000000000000000000000000011
:107F70000000000000000000000000000000000001
:107F800000000000000000000000000000000000F1
:107F900000000000000000000000000000000000E1
:107FA00000000000000000000000000000000000D1
:107FB00000000000000000000000000000000000C1
:107FC00000000000000000000000000000000000B1
:107FD00000000000000000000000000000000000A1
:107FE0000000000000000000000000000000000091

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这段 Hex 的前面,还出现了两条指令(参考 Intel Hex 格式),设置了偏移基址 0x20000000。这正是512M,也解释了裸 bin 文件巨大的原因。

不难想到,如果只解析了基址的低2字节,而忽略高位的0x2000,就正好把这段数据写到上述位置,造成非法指令错误。真的是ISP软件的锅!

OS

坑死我了!足足搞了 2 天!

# 验证并修复

从 hex 文件中删去后面这段内容,重新烧录,程序运行正常。后修改链接脚本,增加 NOLOAD 属性,指定链接器不要将该段内容加载到目标,仅分配内存。如下:


    .lram ORIGIN(RAM)+LENGTH(RAM) - __lram_size (NOLOAD) :
	{
		. = ALIGN(4);
		*(.lram);
		*(.lram.*);
		. = ALIGN(4);
	} > RAM

1
2
3
4
5
6
7
8
9

至此,重新编译并烧录,程序运行正常,且 hex 文件也没有了这段 240 字节的0。

# 后续

升级 MounRiver Studio,连带的WCHISPStudio 也升级到了 3.50 版本,测试发现该版本中,未加 NOLOAD 编译的 hex 文件无法烧写,提示 hex 转 bin 失败,也算是补了漏洞吧(笑)

# Reference

WCH BBS 帖子 (opens new window)

CH57x/CH58x/CH32V wch risc-v 芯片hardfault问题追踪&程序卡死追踪 - iot-fan - 博客园 (cnblogs.com) (opens new window)

hex文件格式详解 (opens new window)