提醒:由于时间关系,没有时间很好地梳理,并往更深处记录,所以中间有一些看着比较割裂。我心里清楚,我会补全这些相关的内容的,对OpenOCD有兴趣的朋友可以关注或者收藏一下。
本文的目的:看到安富莱尝试在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,里面提供了很多有用的函数。
然后提供了一个下载命令的示例:
flash bank $_FLASHNAME rp2040_flash $_FLASHBASE $_FLASHSIZE 1 32 $_TARGETNAME
手册上所有关于RP2040的内容就是上面的这段英文,十分精炼。虽然精炼,但其实信息量很大。
rp2040.c
openocd/src/flash/nor/rp2040.c at master · openocd-org/openocd
肯定是直接查看源代码的,通过关键词rp2040
找到了这个rp2040.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值是一群宏定义。这是为什么?
/* 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-datasheet.pdf
在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的实现也完全是利用宏定义来做的:
#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')
所以还有一个问题,这些字母都是从哪里来的?这个问题很显然能够从手册中找到答案:
读取到函数地址以后,就保存到对应的变量里:
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里的函数。事实上也是这样:
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源代码:
/*
The RP2040 Boot ROM provides a _flash_range_erase() API call documented in Section 2.8.3.1.3:
https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf
and the particular source code for said Boot ROM function can be found here:
https://github.com/raspberrypi/pico-bootrom/blob/master/bootrom/program_flash_generic.c
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).
*/
擦除只要给出起始地址以及擦除的长度即可:
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
看着是有四个参数要传入啊,这怎么搞?
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
以后(省略了部分代码):
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
要占用两个空间,注释里也写得很清楚了:
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进行写入:
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的结构一览
OpenOCD
做了相当多的封装(也是不得不做),也就相应的屏蔽了底层实现,只留出了对应的接口来操作单片机。回退到上面文档里提到的命令:
flash bank $_FLASHNAME rp2040_flash $_FLASHBASE $_FLASHSIZE 1 32 $_TARGETNAME
可以看到下载程序是通过rp2040_flash
锁定的单片机的识别,这其实和源代码是相对应的:
const struct flash_driver rp2040_flash = {
.name = "rp2040_flash",
};
flash bank
flash bank name driver base size chip width bus width [Config Command]
target [driver options]
Configures a flash bank which provides persistent storage for addresses from base to
base+size−1. These banks will often be visible to GDB through the target’s memory
map. In some cases, configuring a flash bank will activate extra commands; see the
driver-specific documentation.
• name ... may be used to reference the flash bank in other flash commands. A
number is also available.
• driver ... identifies the controller driver associated with the flash bank being de-
clared. This is usually cfi for external flash, or else the name of a microcontroller
with embedded flash memory. See [Flash Driver List], page 89.
• base ... Base address of the flash chip.
• size ... Size of the chip, in bytes. For some drivers, this value is detected from
the hardware.
• chip width ... Width of the flash chip, in bytes; ignored for most microcontroller
drivers.
• bus width ... Width of the data bus used to access the chip, in bytes; ignored
for most microcontroller drivers.
• target ... Names the target used to issue commands to the flash controller.
• driver options ... drivers may support, or require, additional parameters. See
the driver-specific documentation for more information.
Note: This command is not available after OpenOCD initialization has
completed. Use it in board specific configuration files, not interactively.
问题和疑惑
OpenOCD是如何和下载器通信的
因为困惑不解,所以查看开源下载器DAP,就可以看到。
DAPLink/source/daplink/interface/main_interface.c at main · ARMmbed/DAPLink
OpenOCD通过USB和下载器通信,然后将数据传送给下载器,然后就不管了。(是这样吗?)
但是我没有找到和写入相绑定的函数。(可能是我看到时间还不够长)
OpenOCD是如何调试的
启用了GDB进行调试,但是它是如何借助下载器调试存在于单片机里的代码的?不止是OpenOCD,KEIL等等都是如何做到的?
宏定义的小技巧
#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
,形成如下内容:
static int Hello(struct command_invocation *cmd) {
printf("Hello\n");
return 0;
}
int main() {
Hello(((void *)0));
return 0;
}
执行程序也能够得到Hello