硬汉嵌入式论坛

 找回密码
 立即注册
查看: 1375|回复: 4
收起左侧

[其它] OpenOCD对RP2040的支持

[复制链接]

2

主题

12

回帖

23

积分

新手上路

积分
23
发表于 2024-5-16 02:38:00 | 显示全部楼层 |阅读模式
本帖最后由 wanower 于 2024-5-16 02:40 编辑

说明

本文的目的:看到安富莱尝试在H7-Tool中加入对RP2040的支持,心里想着OpenOCD是如何做到的。于是就有了现学现卖的这篇文章。主要是记录我的阅读OpenOCD源代码的思考过程,不得不说这次源码阅读确实加深了我对OpenOCD的理解,我也自底向上地理解了部分架构上的设计。当然也有很多不理解的地方,这些会在本文合适的地方提出来,如果有了解的朋友还希望不吝赐教。废话少说,进入正题。
OpenOCD对RP2040的支持也是才从0.12.0开始的。OpenOCD的手册中对RP2040的介绍也没有多少内容:
rp2040          [Flash Driver]
  Supports RP2040 "Raspberry Pi Pico" microcontroller. RP2040 is a dual-core device
with two CM0+ cores. Both cores share the same Flash/RAM/MMIO address space.
Non-volatile storage is achieved with an external QSPI flash; a Boot ROM provides
helper functions.
    flash bank $_FLASHNAME rp2040_flash $_FLASHBASE $_FLASHSIZE 1 32 $_TARGETNAME

大概是说,RP2040是一个有两个CM0+的核,这两个核共享了Flash/RAM/MMIO空间,这个单片机有一个BootROM,里面提供了很多有用的函数。
然后提供了一个下载命令的示例:
[XML] 纯文本查看 复制代码
flash bank $_FLASHNAME rp2040_flash $_FLASHBASE $_FLASHSIZE 1 32 $_TARGETNAME
手册上所有关于RP2040的内容就是上面的这段英文,十分精炼。虽然精炼,但其实信息量很大。

rp2040.c
查看源代码 OpenOCD-rp2040.c,通过文件最下面的结构体可以知道主要的操作:
[C] 纯文本查看 复制代码
const struct flash_driver rp2040_flash = {
        .name = "rp2040_flash",
        .flash_bank_command = rp2040_flash_bank_command,
        .erase =  rp2040_flash_erase,
        .write = rp2040_flash_write,
        .read = default_flash_read,
        .probe = rp2040_flash_probe,
        .auto_probe = rp2040_flash_auto_probe,
        .erase_check = default_flash_blank_check,
        .free_driver_priv = rp2040_flash_free_driver_priv
};
在已知对的Flash操作是先擦除后写入的情况下,一定是先阅读rp2040_flash_erase再阅读rp2040_flash_write的。然而实际上rp2040_flash_probe和rp2040_flash_auto_probe会放在最前面分析。
文件中的注释也很重要,OpenOCD的开发者写下了一些非常关键的信息。
rp2040_flash_probe
rp2040_flash_auto_probe调用的是rp2040_flash_probe。然后这个函数主要做到事情就是不断调用rp2040_lookup_symbol。
仔细阅读rp2040_lookup_symbol以后会发现,这个函数对这个BOOTROM_MAGIC_ADDR初始地址不断以4字节的偏移进行读取,直到和tag值相等,而tag值是一群宏定义。这是为什么?
[C] 纯文本查看 复制代码
/* NOTE THAT THIS CODE REQUIRES FLASH ROUTINES in BOOTROM WITH FUNCTION TABLE PTR AT 0x00000010
   Your gdbinit should load the bootrom.elf if appropriate */

/* this is 'M' 'u', 1 (version) */
#define BOOTROM_MAGIC 0x01754d
#define BOOTROM_MAGIC_ADDR 0x00000010

/* Call a ROM function via the debug trampoline
   Up to four arguments passed in r0...r3 as per ABI
   Function address is passed in r7
   the trampoline is needed because OpenOCD "algorithm" code insists on sw breakpoints. */
看到注释以后就不难理解了,Raspberrypi在RP2040的一块ROM里写入了一些函数和变量。只要我们知道起始地址,并掌握手册中的偏移规则就可以获取函数的地址以及常量和变量的信息。还有MAGIC这个数相当于一个标识数,读到了说明正确访问了ROM的内容,因此代码里不乏有对BOOTROM_MAGIC进行判断的行为(不止rp2040.c这个文件)。
注释中还提到了函数调用规则,如果函数有参数那就顺次放在r0到r3的寄存器中,然后将函数地址放入到r7寄存器中。
在RP2040的手册的2.8.3.1节的末尾、2.8.3.1.1的开始之前,还提到了编码的方式,能够帮助我们快速找到函数地址

编码定位是一件很重要的事情,2.8.3.1第二段:
These functions are normally made available to the user by the SDK, however a lower level method is provided to locate them (their locations may change with each Bootrom release) and call them directly.

就是说如果没有定位的手段,那么不同版本的的BOOTROM里同样的函数地址很可能不是一样的。虽然有SDK可以直接使用,但是总的来说还是要有通过地址调用函数的方式。
编码定位到方式也很简单粗暴,OpenOCD的实现也完全是利用宏定义来做的:

[C] 纯文本查看 复制代码
#define MAKE_TAG(a, b) (((b)<<8) | a)
#define FUNC_DEBUG_TRAMPOLINE       MAKE_TAG('D', 'T')
#define FUNC_DEBUG_TRAMPOLINE_END   MAKE_TAG('D', 'E')
#define FUNC_FLASH_EXIT_XIP         MAKE_TAG('E', 'X')
#define FUNC_CONNECT_INTERNAL_FLASH MAKE_TAG('I', 'F')
#define FUNC_FLASH_RANGE_ERASE      MAKE_TAG('R', 'E')
#define FUNC_FLASH_RANGE_PROGRAM    MAKE_TAG('R', 'P')
#define FUNC_FLASH_FLUSH_CACHE      MAKE_TAG('F', 'C')
#define FUNC_FLASH_ENTER_CMD_XIP    MAKE_TAG('C', 'X')

所以还有一个问题,这些字母都是从哪里来的?这个问题很显然能够从手册中找到答案:
[img=2689,560]![image.png](#averageHue=%23f7f7f6&clientId=ud19420da-7e16-4&from=paste&height=560&id=ue115c5e1&originHeight=1119&originWidth=2689&originalType=binary&ratio=2&rotation=0&showTitle=false&size=217233&status=done&style=none&taskId=ufde62ec7-3911-4e27-847f-c74dce01208&title=&width=1344.5)[/img]
读取到函数地址以后,就保存到对应的变量里:
[C] 纯文本查看 复制代码
struct rp2040_flash_bank {
        /* flag indicating successful flash probe */
        bool probed;
        /* stack used by Boot ROM calls */
        struct working_area *stack;
        /* function jump table populated by rp2040_flash_probe() */
        uint16_t jump_debug_trampoline;
        uint16_t jump_debug_trampoline_end;
        uint16_t jump_flash_exit_xip;
        uint16_t jump_connect_internal_flash;
        uint16_t jump_flash_range_erase;
        uint16_t jump_flash_range_program;
        uint16_t jump_flush_cache;
        uint16_t jump_enter_cmd_xip;
        /* detected model of SPI flash */
        const struct flash_device *dev;
};
看上去这只是一群16位的变量,实际上存放的是函数的地址,当然函数是地址32位的,但由于函数被放在了以0x00000010为开始的地址之后,加之BOOTROM空间有限(16kB),所以高16位其实为0,因此这里并不声明为uint32_t的变量,以节省空间。
不管怎样,等到rp2040_flash_probe执行结束以后,我们知道了我们想要的函数的地址,只需要适时使用合适的API进行调用就好了。
rp2040_flash_erase
调用了一个rp2040_stack_grab_and_prep,进入到这个函数以后可以看到主要调用了rp2040_call_rom_func,根据函数名也不难才出来这是在调用rom里的函数。事实上也是这样:
[C] 纯文本查看 复制代码
err = rp2040_call_rom_func(target, priv, priv->jump_connect_internal_flash, NULL, 0, 1000);
err = rp2040_call_rom_func(target, priv, priv->jump_flash_exit_xip, NULL, 0, 1000);



调用了通过rp2040_flash_probe获得的函数。
那么jump_connect_internal_flash和jump_flash_exit_xip对应的函数主要是干什么的呢?

手册中提到了:
A typical call sequence for erasing a flash sector from user code would be:
● _connect_internal_flash
● _flash_exit_xip
● _flash_range_erase(addr, 1 << 12, 1 << 16, 0xd8)
● _flash_flush_cache
● Either a call to _flash_enter_cmd_xip or call into a flash second stage that was previously copied out into SRAM
Note that, in between the first and last calls in this sequence, the SSI is not in a state where it can handle XIP accesses, so the code that calls the intervening functions must be located in SRAM. The SDK hardware_flash library hides these details.

上述步骤在OpenOCD的编程中有体现,在代码的注释中也提到了手册和Raspberrypi的BOOTROM源代码:
[C] 纯文本查看 复制代码
/*
The RP2040 Boot ROM provides a _flash_range_erase() API call documented in Section 2.8.3.1.3:
[url=https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf]https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf[/url]
and the particular source code for said Boot ROM function can be found here:
[url=https://github.com/raspberrypi/pico-bootrom/blob/master/bootrom/program_flash_generic.c]https://github.com/raspberrypi/p ... ram_flash_generic.c[/url]

In theory, the function algorithm provides for erasing both a smaller "sector" (4096 bytes) and
an optional larger "block" (size and command provided in args).
*/

擦除只要给出起始地址以及擦除的长度即可:
[C] 纯文本查看 复制代码
uint32_t start_addr = bank->sectors[first].offset;
uint32_t length = bank->sectors[last].offset + bank->sectors[last].size - start_addr;

然后调用ROM里的_flash_range_erase函数进行擦除。

这时候会感觉到不对劲了,之前的函数好理解,都没有参数要传入,直接调用就好了,这个_flash_range_erase看着是有四个参数要传入啊,这怎么搞?
[C] 纯文本查看 复制代码
uint32_t args[4] = {
    bank->sectors[first].offset, /* addr */
    bank->sectors[last].offset + bank->sectors[last].size - bank->sectors[first].offset, /* count */
    priv->dev->sectorsize, /* block_size */
    priv->dev->erase_cmd /* block_cmd */
};

unsigned int timeout_ms = 2000 * (last - first) + 1000;

err = rp2040_call_rom_func(target, priv, priv->jump_flash_range_erase,
                        args, ARRAY_SIZE(args), timeout_ms);


OpenOCD通过一个args数组传入要传入的参数,进入到rp2040_call_rom_func以后(省略了部分代码):
[C] 纯文本查看 复制代码
static int rp2040_call_rom_func(struct target *target, struct rp2040_flash_bank *priv,
                    uint16_t func_offset, uint32_t argdata[], unsigned int n_args, unsigned int timeout_ms)
{
        char *regnames[4] = { "r0", "r1", "r2", "r3" };
    
    target_addr_t stacktop = priv->stack->address + priv->stack->size;
    
    struct reg_param args[ARRAY_SIZE(regnames) + 2];
        struct armv7m_algorithm alg_info;
    
        for (unsigned int i = 0; i < n_args; ++i) {
                init_reg_param(&args[i], regnames[i], 32, PARAM_OUT);
                buf_set_u32(args[i].value, 0, 32, argdata[i]);
        }
        /* Pass function pointer in r7 */
        init_reg_param(&args[n_args], "r7", 32, PARAM_OUT);
        buf_set_u32(args[n_args].value, 0, 32, func_offset);
        /* Setup stack */
        init_reg_param(&args[n_args + 1], "sp", 32, PARAM_OUT);
        buf_set_u32(args[n_args + 1].value, 0, 32, stacktop);
        unsigned int n_reg_params = n_args + 2;        /* User arguments + r7 + sp */

        for (unsigned int i = 0; i < n_reg_params; ++i)
                LOG_DEBUG("Set %s = 0x%" PRIx32, args[i].reg_name, buf_get_u32(args[i].value, 0, 32));

        /* Actually call the function */
        alg_info.common_magic = ARMV7M_COMMON_MAGIC;
        alg_info.core_mode = ARM_MODE_THREAD;
        int err = target_run_algorithm(
                target,
                0, NULL,          /* No memory arguments */
                n_reg_params, args, /* User arguments + r7 + sp */
                priv->jump_debug_trampoline, priv->jump_debug_trampoline_end,
                timeout_ms,
                &alg_info
        );
}

当耐着性子仔细看完会发现其实也没什么,这里假设有四个参数,那么会依此给args这个结构体变量数组的每一个结构体元素赋值,但是也要注意在声明args的时候,struct reg_param args[ARRAY_SIZE(regnames) + 2];,多声明了两个元素,原因是,还有r7和sp要占用两个空间,注释里也写得很清楚了:
[C] 纯文本查看 复制代码
n_reg_params, args, /* User arguments + r7 + sp */

这还只是赋值,真正的执行动作全在target_run_algorithm里面。大概可以猜测一下,这个函数执行以后会将这些“虚拟”地操作转变为RP2040真实的内容,也就是说,r0到r3寄存器,以及r7和sp会有我们写入的真实值,加上对PC的控制,就可以实现RP2040真正执行这个函数,以及我们传入的参数,进而对我们所要求的区域进行擦除。
rp2040_flash_write
和rp2040_flash_erase一样执行了rp2040_stack_grab_and_prep,然后进入到了while循环中,主要是按页对Flash进行写入:
[C] 纯文本查看 复制代码
err = target_write_buffer(target, bounce->address, write_size, buffer);
uint32_t args[3] = {
    offset, /* addr */
    bounce->address, /* data */
    write_size /* count */
};
err = rp2040_call_rom_func(target, priv, priv->jump_flash_range_program,
                                                                 args, ARRAY_SIZE(args), 3000);




要写入的数据在bounce->address里面,每写完一页如果还要继续写,就会更新bounce->address里的内容。
小总结
从流程的理解上来说并不是很困难,但是代码中存在众多的细节,比如要擦除的页的计算,边界对齐,内存空间的分配和释放……
问题和疑惑
OpenOCD是如何和下载器通信的
因为困惑不解,所以查看开源下载器DAP,就可以看到。
https://github.com/ARMmbed/DAPLink/blob/main/source/daplink/interface/main_interface.c
OpenOCD通过USB和下载器通信,然后将数据传送给下载器,然后就不管了。(是这样吗?)
但是我没有找到和写入相绑定的函数。(可能是我看到时间还不够长)
OpenOCD是如何调试的
启用了GDB进行调试,但是它是如何借助下载器调试存在于单片机里的代码的?不止是OpenOCD,KEIL等等都是如何做到的?

宏定义的小技巧

[C] 纯文本查看 复制代码
#include "stdio.h"
#define __COMMAND_HANDLER(name, extra ...) \
		int name(struct command_invocation *cmd, ## extra)

#define COMMAND_HANDLER(name) \
		static __COMMAND_HANDLER(name)

COMMAND_HANDLER(Hello) {
    printf("Hello\n");
    return 0;
}

int main() {
    Hello(NULL);
    return 0;
}

这个宏能够忽略int name(struct command_invocation *cmd, ## extra)后边的, ## extra,形成如下内容:
[C] 纯文本查看 复制代码
static int Hello(struct command_invocation *cmd) {
    printf("Hello\n");
    return 0;
}

int main() {
    Hello(((void *)0));
    return 0;
}


执行程序也能够得到Hello

rp2040-datasheet.pdf

5.09 MB, 下载次数: 0

RP2040的手册

评分

参与人数 1金币 +100 收起 理由
eric2013 + 100 很给力!

查看全部评分

回复

使用道具 举报

1万

主题

7万

回帖

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
117512
QQ
发表于 2024-5-16 08:26:41 | 显示全部楼层
谢谢楼主分享,好帖。
回复

使用道具 举报

2

主题

12

回帖

23

积分

新手上路

积分
23
 楼主| 发表于 2024-5-16 08:41:52 | 显示全部楼层
eric2013 发表于 2024-5-16 08:26
谢谢楼主分享,好帖。

上传图片说是被服务器限制了,所以我放了图片链接但是现在不显示,实际上这些链接是可以在其他文本编辑器里显示的。
然后由于时间仓促,还没来得及打磨,没有往我看到的更深处写,着实抱歉。
回复

使用道具 举报

81

主题

1362

回帖

1605

积分

至尊会员

积分
1605
发表于 2024-5-16 09:03:03 | 显示全部楼层
太复杂了,还是pyocd好用
回复

使用道具 举报

1万

主题

7万

回帖

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
117512
QQ
发表于 2024-5-16 09:27:21 | 显示全部楼层
wanower 发表于 2024-5-16 08:41
上传图片说是被服务器限制了,所以我放了图片链接但是现在不显示,实际上这些链接是可以在其他文本编辑器 ...

这个是我们论坛的bug,得手动上传才行,不支持复制粘贴上传图片了,这个没关系,晚些时候让我们论坛管理员把图片单独上传下。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|小黑屋|Archiver|手机版|硬汉嵌入式论坛

GMT+8, 2025-8-12 03:09 , Processed in 0.048617 second(s), 32 queries .

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2023, Tencent Cloud.

快速回复 返回顶部 返回列表