概述
1)OpenOCD 与调试器、MCU 的关系:
- (1)OpenOCD 运行在 PC 机上,用户可以通过 GDB 调试、Telnet 连接 Socket 以及 CMD 命令行的方式执行命令
- (2)OpenOCD 将命令发送到 MCU 端需要双方约定协议。目前常用的协议为 SWD 和 JTAG,实现这些协议的工具有 DAPLink、ST-Link、JLink 等
- (3)在 MCU 端内置了 DAP(Debug Access Port)模块,它接收到命令,通过 AHB 总线控制 CPU 内核。(因此,通过 DAP 我们可以访问挂载在 AHB 总线上所有外设,尤其是 FLASH 外设,它是实现芯片烧录程序的关键。)
2)在 《Windows 下编译 OpenOCD》一节,我们已经可以通过 JetBrains CLion 打开 OpenOCD 源码(也可以使用 VSCode 打开)。一般程序都是从 main() 函数开始,那么让我们打开 /openocd/src 目录,从 main.c 文件开始:
1 main()
- 从 main() 函数到 openocd\_main() 函数,最后再到 setup\_command\_handler() 函数,OpenOCD 进行命令注册操作。
- 那么接下来,我们重点看一下 setup_command_handler() 函数。
2 setup_command_handler()
1)setup_command_handler() 函数主要进行了以下两个操作,我们一个一个来看:
- command_init()
- (*command_registrants[i])(cmd_ctx)
2)对于 command_init() 函数,我们略去 Jim 框架的代码,可以看到以下内容:
register_commands(context, NULL, command_builtin_handlers); if (Jim_Eval_Named(interp, startup_tcl, "embedded:startup.tcl", 1) == JIM_ERR) {
- 第一行代码注册一些 OpenOCD 的内置命令,如 ocd_find、capture、sleep 等等
第二行则是解析了 startup.tcl 文件。
这里要注意,.tcl 文件里有一些调用过程,当你无法在源码中找到某些命令的实现时,可以搜索一下 .tcl 文件。如 CLion 通过 program 命令烧录,而该命令实际上是一个定义在 src/flash/startup.tcl 中的一个 proc,并在其内部转换成 flash write_image erase 组合命令来进行烧录。
3)对于 (*command_registrants[i])(cmd_ctx) 函数调用,我们结合上下文来看:
/* 1 定义名为 command_registrant_t,参数类型为 struct command_context 的函数指针 */ typedef int (*command_registrant_t)(struct command_context *cmd_ctx_value); /* 2 声明一个 command_registrant_t 类型的函数指针数组 */ static const command_registrant_t command_registrants[] = { &workaround_for_jimtcl_expr, &openocd_register_commands, &server_register_commands, &gdb_register_commands, &log_register_commands, &rtt_server_register_commands, &transport_register_commands, &adapter_register_commands, &target_register_commands, &flash_register_commands, &nand_register_commands, &pld_register_commands, &cti_register_commands, &dap_register_commands, &arm_tpiu_swo_register_commands, NULL }; /* 3 遍历并执行声明的函数指针 */ for (unsigned i = 0; command_registrants[i]; i++) { int retval = (*command_registrants[i])(cmd_ctx); if (retval != ERROR_OK) { command_done(cmd_ctx); return NULL; } }
- 也就是说,(*command_registrants[i])(cmd_ctx) 其实是调用那些注册命令的函数。下面我们来看一下命令注册的实现逻辑。
3 register_commands()
3.1 server_register_commands()
1)这里我们以 server\_register\_commands() 函数为例,如下图:
- telnet_register_commands():注册 telnet 相关的命令,如 exit、telnet_port 等,且在这里指定 telnet 端口号为 4444
- tcl_register_commands() 和 jsp_register_commands() 分别指定了 6666 和 7777 两个端口号(暂时不知道这个两个是干吗的,不过不影响使用)
- register_commands():注册 server 相关的命令,如 shutdown 等。
2)然后我们看一下命令的具体声明内容:
static const struct command_registration telnet_command_handlers[] = { { .name = "exit", .handler = handle_exit_command, .mode = COMMAND_EXEC, .usage = "", .help = "exit telnet session", }, { .name = "telnet_port", .handler = handle_telnet_port_command, .mode = COMMAND_CONFIG, .help = "Specify port on which to listen " "for incoming telnet connections. " "Read help on 'gdb_port'.", .usage = "[port_num]", }, COMMAND_REGISTRATION_DONE };
- (1)其中的 .name 是处理器名称,Jim 模块可根据该名称查找到对应的 handler
(2)mode 表示该 handler 的模式(我愿称之为生命周期):
- COMMAND_EXEC,表示该 handler 在命令行输入时才会触发
- COMMAND_CONFIG,表示该 handler 在 OpenOCD 启动时解析配置文件时触发
- COMMAND_ANY,表示该 handler 以上两种情况均会触发
3.2 register command
1)以下为注册命令的实现逻辑:
2)每个函数的作用已经在流程图中注明,需要注意有两点:
- <font color=red>将 jim_command_dispatch() 函数赋值给 cmdProc 指针的 u.native.cmdProc 属性</font>。
将 command_new() 函数构造的 command 实例,赋值给 cmdProc 指针的 u.native.privData 属性。
Jim_Cmd *cmdPtr = Jim_Alloc(sizeof(*cmdPtr)); /* Store the new details for this command */ memset(cmdPtr, 0, sizeof(*cmdPtr)); cmdPtr->inUse = 1; cmdPtr->u.native.delProc = delProc; cmdPtr->u.native.cmdProc = cmdProc; // jim_command_dispatch cmdPtr->u.native.privData = privData; // command 实例
- 3)最终所有命令会缓存到 Jim 框架的 Hash 数组结构中(查找时间复杂度 O(1))。
- 4)通过对各个 xx__register_commands() 函数的遍历,所有的命令都将注册到此。注册完命令后要执行命令,下一篇文章我们来看一下执行命令的逻辑。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。