Table of Contents
用STM32F103C8芯片做一个CMSIS-DAP调试器
如果你在一些比较现代的单片机上开发软件,你一定需要一个JTAG调试器来连接你的单片机和主机端的调试软件。对于ARM芯片来说,J-Link是一个非常知名的调试器,它可以连接到主机侧的USB接口上为你的调试软件提供JTAG协议。CMSIS-DAP是一个类似的设备,但它是由ARM公司自行设计发布的。与J-Link的主要区别在于DAP是完全开源的而J-Link是商用产品。再者,CMSIS-DAP是一个USB HID设备,在主机侧不用安装任何驱动程序。
CMSIS-DAP 2.0.0的源代码包含在CMSIS代码包的第5版中。和固件源代码一起的还有两个针对LPC-Link-II的实现示例。这两个实现示例使用了源自Keil MDK的RTE组件和CMSIS-RTOS应用程序接口,其中包含一个名为USB_CM3.lib的闭源库文件,这使得这两个示例难以移植1)。作为一个开源爱好者,我决定完全基于开源免费的软件开发工具重新实现这个有用的调试器。
STM32F103是一个基于ARM Coretex-M3核的单片机,价格便宜因而十分流行。它是由STMicroelectronics公司开发,我手里有一些名为蓝药丸的非常简单的开发板。这是一种很小的核心板,板载一颗STM32F103C8T6芯片,板上只有一个最小系统的电路,没有什么不必要的元件。唯一的问题是接在USB D+线上的上拉电阻是10K的,正确的阻值应该是1.5K,但这个错误的电阻并不影响正常工作。
建立一个完整的STM32单片机软件开发环境是很容易的,在ST公司的网站上,我们可以下载一些免费的软件,最重要的一个就是STM32CubeMX。这是一个图形化的配置工具可以帮助用户选择芯片管脚的功能并自动生成驱动程序源码。我配置了USB接口,SPI1和USART1两个串行接口,并选择了USB Custom HID中间件。GPIO10到GPIO15配置成JTAG主接口,GPIOC13配置成板载LED驱动引脚,不过,我不知道怎么配置片内CoreDebug调试模块和DWT模块,确实可以使用STM32CubeMX配置这两个部分吗?
ST公司开发了它们自己的JTAG调试器,名为STLink。如果你乐于使用J-Link或其它设备的话那这个STLink调试器就不是必须的。STLink的驱动及工具软件可以从ST公司的网站上下载。ST公司还有自己的IDE集成开发环境2),这个软件是基于Eclipse软件开发系统构建的。我选择的集成开发环境名为Embitz,这是基于CodeBlocks软件开发系统构建的,内置了arm_none_eabi_gcc version 5.4.1。在我的DAP实现之前,我不得不使用STLink调试代码,不过现在我已经可以用OpenOCD加我新做的CMSIS-DAP调试了。
从CMSIS-DAP的源代码开始
CMSIS-DAP的源代码在CMSIS软件包的第4版和第5版内都有,我从Github上下载了CMSIS_5这个源码包。CMSIS-DAP的核心源码由5个文件构成,但是那两个实现示例其实更重要。所以我首先从第一个示例的这个头文件开始:
- DAP_config.h
我之所以选择LPC-Link-II的V1示例是因为它实现了USB HID设备。DAP_config.h是我分析的第一个文件,其中第一个关键的代码块如下:
#ifdef _RTE_ #include "RTE_Components.h" #include CMSIS_device_header #else #include "device.h" // Debug Unit Cortex-M Processor Header File #endif
我打算弃掉示例工程中的RTE子目录,所以这个宏“_RTE_”并不在我的工程中定义,这样我就必须编写这个“devicd.h”头文件。
我把宏“CPU_CLOCK”重新定义为72000000 (72MHz),根据DAP_config.h文件中的注释,宏“DAP_PACKET_SIZE”必须定义为64U。我把宏“SWO_UART”重定义为0,这可以让我的移植工作容易一点。宏“TIMESTAMP_CLOCK”也定义为72000000 (72MHz),LPC-Link-II使用Cortex-M3内核中的DWT模块实现时间戳功能,这就是为何我试图使用STM32CubeMX配置DWT模块的原因。最后我还是自己写了点代码 (在device.c文件中) 配置DWT:
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; /** * On Cortex-M7 core there is a LAR register in DWT domain. * Any time we need to setup DWT registers, we MUST write * 0xC5ACCE55 into LAR first. LAR means Lock Access Register. */ DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
芯片引脚的定义与NXP的LPC43xx芯片完全不同,所以我为STM32F103重写的所有端口操作代码。我看到在DAP_config.h文件中有一些奇怪的注释,比如说:
// SWCLK/TCK I/O pin ------------------------------------- /** SWCLK/TCK I/O pin: Get Input. \return Current status of the SWCLK/TCK DAP hardware I/O pin. */ __STATIC_FORCEINLINE uint32_t PIN_SWCLK_TCK_IN (void) { return ((LPC_GPIO_PORT->PIN[PIN_SWCLK_TCK_PORT]>> PIN_SWCLK_TCK_BIT) & 1U); }
我有点疑惑DAP内核为什么要读取SWCLK/TCK引脚?这个引脚需要配置成“推挽输出”模式用于向被调试的芯片提供时钟信号。你可以看到上面列出的代码确实读取了这个引脚的电平,而我的代码实现与之相比有小小的不同:
__STATIC_FORCEINLINE uint32_t PIN_SWCLK_TCK_IN (void) { return (uint32_t)(JTAG_TCK_GPIO_Port->ODR & JTAG_TCK_Pin ? 1:0); }
我返回了这个引脚当前正在向外输出的电平,我不太确定这是对是错。这个函数只被DAP.c文件中的“DAP_SWJ_Pins”函数调用了两次,我推测“DAP_SWJ_Pins”函数是用来检查芯片的端口操作是否正常的。
另一个有点奇怪的函数是“PIN_nRESET_OUT”:
/** nRESET I/O pin: Set Output. \param bit target device hardware reset pin status: - 0: issue a device hardware reset. - 1: release device hardware reset. */ __STATIC_FORCEINLINE void PIN_nRESET_OUT (uint32_t bit) { if (bit) { LPC_GPIO_PORT->DIR[PIN_nRESET_PORT] &= ~(1U <<PIN_nRESET_BIT); LPC_GPIO_PORT->CLR[PIN_nRESET_OE_PORT] = (1U <<PIN_nRESET_OE_BIT); } else { LPC_GPIO_PORT->SET[PIN_nRESET_OE_PORT] = (1U <<PIN_nRESET_OE_BIT); LPC_GPIO_PORT->DIR[PIN_nRESET_PORT] |= (1U <<PIN_nRESET_BIT); } }
我认为代码注释中的“release”意思是重新配置这个引脚为单纯输入(输出禁止)模式,所以我的代码写成了这样:
__STATIC_FORCEINLINE void PIN_nRESET_OUT (uint32_t bit) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if ((bit & 1U) == 1) { JTAG_nRESET_GPIO_Port->BSRR = JTAG_nRESET_Pin; GPIO_InitStruct.Pin = JTAG_nRESET_Pin; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(JTAG_nRESET_GPIO_Port, &GPIO_InitStruct); } else { JTAG_nRESET_GPIO_Port->BRR = JTAG_nRESET_Pin; GPIO_InitStruct.Pin = JTAG_nRESET_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(JTAG_nRESET_GPIO_Port, &GPIO_InitStruct); } }
特别提醒
- 你可以看到我的代码中很多if(…)语句写成了这样:
if ((bit & 1U) == 1) { ... } else { ... }
我只取了参数bit的最低一位进行判断,这是因为我发现传来的参数bit取值并不只有0和1,还可能是2或其它什么值,所以代码不能写成这样:
if (bit) { ... } else { ... }
这完全不好使!!!
- osObjects.h
#ifndef __osObjects_h__ #define __osObjects_h__ #include "cmsis_os2.h" #ifdef osObjectsExternal extern osThreadId_t DAP_ThreadId; #else extern osThreadId_t DAP_ThreadId; osThreadId_t DAP_ThreadId; #endif extern void DAP_Thread (void *argument); extern void app_main (void *argument); #endif /* __osObjects_h__ */
这是一个很简单的头文件,它包含了另一个名为“cmsis_os2.h”的头文件,这个“cmsis_os2.h”是CMSIS软件包的一部分但我并没有把它拷贝过来直接用,因为不是文件中的所有内容都有用。我写了一个假的“cmsis_os2.h”代替原来的那个文件。 DAP的内核中还有一个名为“DAP.h”的文件,它包含了一个名为“cmsis_compiler.h”的头文件,这也是CMSIS软件包的一个组成部分,我当然也写了一个假的。所以到目前为止我需要编写3个头文件 (device.h & cmsis_os2.h & cmsis_compiler.h) 来支持CMSIS-DAP核心。
接下来我需要对两个C源文件,main.c和USBD_User_HID_0.c做一个简单的说明。
- main.c
我一直试图得所有源码做尽可能小的改动,这就是为什么我没有把“rl_usb.h”这个文件从我的项目中剃除出去,当然我也需要有一个头文件来定义一个与USB通信有关的宏。还有一些来自于CMSIS RTOS库的函数,其中最重要的一个是“osThreadNew”,在我的代码中它被写成了这样:
osThreadId_t osThreadNew(void (*func)(void *), void * n, void * ctx) { (void)n; (*func)(ctx); return 0; }
我只是直接“跳入”了需要被创建的线程,这意味着main.c文件后面的一些函数调用“osKernelGetState & osKernelStart & osDelay”是永远不会执行的。接下来一个重要的函数是“USBD_Configured”,在用STM32CubeMX生成完驱动程序后我再解释它。
- USBD_User_HID_0.c
我删除了对文件“RTE\USB\USBD_Config_HID_0.h”的包含,在我自己的“rl_usb.h”文件中我重定义了两个宏“USBD_HID0_OUT_REPORT_MAX_SZ & USBD_HID0_IN_REPORT_MAX_SZ”。USB HID通信的核心是两个环形的队列,它们由两个接口函数负责管理:
int32_t USBD_HID0_GetReport (uint8_t rtype, uint8_t req, uint8_t rid, uint8_t *buf) { (void)rid; switch (rtype) { case HID_REPORT_INPUT: ... break; } return (0); }
bool USBD_HID0_SetReport (uint8_t rtype, uint8_t req, uint8_t rid, const uint8_t *buf, int32_t len) { (void)req; (void)rid; switch (rtype) { case HID_REPORT_OUTPUT: ... break; } return true; }
在主机向DAP发送了一个“OUTPUT REPORT”之后,函数USBD_HID0_SetReport会被调用,参数“rtype”一定是“HID_REPORT_OUTPUT”。当DAP向主机成功返回了一个“INPUT REPORT”之后,函数USBD_HID0_GetReport会被调用,参数“rtype”一定是“HID_REPORT_INPUT”并且参数“req”一定是“USBD_HID_REQ_EP_INT”。这提示了我们所有的REPORT一定是通过USB中断端点以64字节为包长进行双向传输的。 函数“DAP_Thread”只是一个指令分发器,其中有一个重要的函数调用:
USBD_HID_GetReportTrigger(0U, 0U, USB_Response[n], DAP_PACKET_SIZE);
我们必须实现“USBD_HID_GetReportTrigger”这个函数,这个函数用来把一个“INPUT REPORT”发送给主机。
使用STM32CubeMX生成驱动程序
蓝药丸板上有一个8MHz的石英晶体,所以我进择HSE做为主时钟信号。“PLLMul”设置为“x9”,生成72MHz的“PLLCLK”做为CPU和AHB/APB2的主时钟。APB1总线的“PCLK1”设为36MHz。USB的预分频系数设为“1.5”从而得到48MHz的时钟信号。“SPI1”接口选择了“全双工主模式”且没有硬件的“NSS”信号。“USART1”则配置为异步串口。USB只能选Device模式并且选择了“USB Custom HID”模式的中间件,“USBD_CUSTOMHID_OUTREPORT_BUF_SIZE”设置为64字节。
重要提示
- 我没有改变USB设备描述符中的VID和PID,我推测有些运行于主机端的软件可能会检查这两个ID,如果你发现你所用的软件不正常操作这个DAP,可能你需要使用一对合适的VID/PID。不妨试一下LPC-Link-II源码中的VID/PID,在源文件“USBD_Config_0.c”之中,这个源文件从我的项目中删除了。
STM32CubeMX生成的源代码需要做一些修改,在文件“usbd_customhid.h”中,“CUSTOM_HID_EPIN_SIZE”和“CUSTOM_HID_EPOUT_SIZE”这两个宏必须定义成“0x40U”,我还把“CUSTOM_HID_FS_BINTERVAL”修改为“0x01”以获得最快的HID通信速度。
在结构体“_USBD_CUSTOM_HID_Itf”之中,我插入了一个新的接口“InEvent”:
typedef struct _USBD_CUSTOM_HID_Itf { uint8_t *pReport; int8_t (* Init)(void); int8_t (* DeInit)(void); int8_t (* OutEvent)(uint8_t event_idx, uint8_t state); /* I add an extra interface func below. Zach Lee */ int8_t (* InEvent)(uint8_t event_idx, uint8_t state); } USBD_CUSTOM_HID_ItfTypeDef;
每当一个“INPUT REPORT”成功发回主机之后,这个“InEvent”函数就会被调用,为此在源码文件“usbd_customhid.c”之中的函数“USBD_CUSTOM_HID_DataIn”就要做如下修改:
static uint8_t USBD_CUSTOM_HID_DataIn(USBD_HandleTypeDef *pdev, uint8_t epnum) { /* Ensure that the FIFO is empty before a new transfer, this condition could be caused by a new transfer before the end of the previous transfer */ USBD_CUSTOM_HID_HandleTypeDef *hhid = (USBD_CUSTOM_HID_HandleTypeDef *)pdev->pClassData; hhid->state = CUSTOM_HID_IDLE; /* I add a new interface func in the structure USBD_CUSTOM_HID_ItfTypeDef. Zach Lee */ ((USBD_CUSTOM_HID_ItfTypeDef *)pdev->pUserData)->InEvent(hhid->Report_buf[0], hhid->Report_buf[1]); return USBD_OK; }
HID报告描述符“CUSTOM_HID_ReportDesc_FS”在源码文件“usbd_custom_hid_if.c”之中定义,我定义了一个最小化的描述符:
/** Usb HID report descriptor. */ __ALIGN_BEGIN static uint8_t CUSTOM_HID_ReportDesc_FS[USBD_CUSTOM_HID_REPORT_DESC_SIZE] __ALIGN_END = { /* USER CODE BEGIN 0 */ /* A minimal Report Desc with INPUT/OUTPUT/FEATURE report. Zach Lee */ 0x06,0x00,0xFF, /* Usage Page (vendor defined) ($FF00) global */ 0x09,0x01, /* Usage (vendor defined) ($01) local */ 0xA1,0x01, /* Collection (Application) */ 0x15,0x00, /* LOGICAL_MINIMUM (0) */ 0x25,0xFF, /* LOGICAL_MAXIMUM (255) */ 0x75,0x08, /* REPORT_SIZE (8bit) */ // Input Report 0x95,64, /* Report Length (64 REPORT_SIZE) */ 0x09,0x01, /* USAGE (Vendor Usage 1) */ 0x81,0x02, /* Input(data,var,absolute) */ // Output Report 0x95,64, /* Report Length (64 REPORT_SIZE) */ 0x09,0x01, /* USAGE (Vendor Usage 1) */ 0x91,0x02, /* Output(data,var,absolute) */ // Feature Report 0x95,64, /* Report Length (64 REPORT_SIZE) */ 0x09,0x01, /* USAGE (Vendor Usage 1) */ 0xB1,0x02, /* Feature(data,var,absolute) */ /* USER CODE END 0 */ 0xC0 /* END_COLLECTION */ };
可能这个还不够小,“Feature Report”有可能并不需要,就这样吧。 在这个源码文件中我插入了自己定义的接口函数“CUSTOM_HID_InEvent_FS”:
static int8_t CUSTOM_HID_InEvent_FS(uint8_t event_idx, uint8_t state); /* An extra interface func. */ USBD_CUSTOM_HID_ItfTypeDef USBD_CustomHID_fops_FS = { CUSTOM_HID_ReportDesc_FS, CUSTOM_HID_Init_FS, CUSTOM_HID_DeInit_FS, CUSTOM_HID_OutEvent_FS, /* I add an extra interface func below. Zach Lee */ CUSTOM_HID_InEvent_FS };
extern void USBD_OutEvent(void); /* Implemented in file "device.h" */ static int8_t CUSTOM_HID_OutEvent_FS(uint8_t event_idx, uint8_t state) { /* USER CODE BEGIN 6 */ USBD_OutEvent(); /* OUTPUT REPORT was received. Zach Lee */ return (USBD_OK); /* USER CODE END 6 */ } extern void USBD_InEvent(void); /* Implemented in file "device.h" */ static int8_t CUSTOM_HID_InEvent_FS(uint8_t event_idx, uint8_t state) { /* USER CODE BEGIN extra */ USBD_InEvent(); /* INPUT REPORT has been sent. Zach Lee */ return (USBD_OK); /* USER CODE END extra */ }
函数“CUSTOM_HID_Init_FS”和“CUSTOM_HID_DeInit_FS”也做了修改,钩挂了DAP应用中的Init/DeInit函数:
extern void USBD_HID0_Initialize (void); static int8_t CUSTOM_HID_Init_FS(void) { /* USER CODE BEGIN 4 */ USBD_HID0_Initialize(); /* Initialize USB communication of DAP. Zach Lee */ return (USBD_OK); /* USER CODE END 4 */ } extern void USBD_HID0_Uninitialize (void); static int8_t CUSTOM_HID_DeInit_FS(void) { /* USER CODE BEGIN 5 */ USBD_HID0_Uninitialize(); /* Uninitialize. Zach Lee */ return (USBD_OK); /* USER CODE END 5 */ }
编写一个桥接模块连接所有源码文件
为了把CMSIS RTOS应用程序接口从我的项目中剃除出去,我编写了一些支撑函数模拟一个RTOS:
/** * Replace CMSIS RTOS api */ static volatile int osFlags; /* Use "volatile" to prevent GCC optimizing the code. */ void osKernelInitialize(void) { osFlags = 0; return; } int osThreadFlagsWait(int mask, int b, int c) { (void)b; (void)c; int ret; while((osFlags&mask) == 0) { ; } ret = osFlags; osFlags &= ~mask; return ret; } void osThreadFlagsSet(int tid, int f) { (void)tid; osFlags |= f; return; }
函数“USBD_Configured”和“USBD_HID_GetReportTrigger”按以下方式实现:
int USBD_Configured(int n) { (void)n; return (hUsbDeviceFS.dev_state == USBD_STATE_CONFIGURED ? 1:0); }
void USBD_HID_GetReportTrigger(int a, int b, void * report, int len) { (void)a; (void)b; if (USBD_OK != USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, report, len)) { ; } return; }
这里的函数“USBD_CUSTOM_HID_SendReport”是由STM32CubeMX自动生成的,保存于源码文件“usbd_customhid.c”。我自己的EVENT处理函数是这个样子的:
bool USBD_HID0_SetReport (uint8_t rtype, uint8_t req, uint8_t rid, const uint8_t *buf, int32_t len); void USBD_OutEvent(void) { USBD_CUSTOM_HID_HandleTypeDef *hhid = (USBD_CUSTOM_HID_HandleTypeDef *)hUsbDeviceFS.pClassData; USBD_HID0_SetReport(HID_REPORT_OUTPUT, 0, 0, hhid->Report_buf, USBD_CUSTOMHID_OUTREPORT_BUF_SIZE); } int32_t USBD_HID0_GetReport (uint8_t rtype, uint8_t req, uint8_t rid, uint8_t *buf); void USBD_InEvent(void) { int32_t len; USBD_CUSTOM_HID_HandleTypeDef *hhid = (USBD_CUSTOM_HID_HandleTypeDef *)hUsbDeviceFS.pClassData; if ((len=USBD_HID0_GetReport(HID_REPORT_INPUT, USBD_HID_REQ_EP_INT, 0, hhid->Report_buf)) > 0) { USBD_HID_GetReportTrigger(0, 0, hhid->Report_buf, len); } }
- 重要提示:
在CMSIS-DAP内核源码文件“DAP.h”中,有一个函数名为“PIN_DELAY_SLOW”,它原本是这个样子的:
__STATIC_FORCEINLINE void PIN_DELAY_SLOW (uint32_t delay) { uint32_t count; count = delay; while (--count); }
这里的空循环“while (–count);”会被GCC优化掉,我在StackOverflow网站上找到了一个很聪明的解决方案,它效果很好但是不可移植,你有什么更好的方案吗?
__STATIC_FORCEINLINE void PIN_DELAY_SLOW (uint32_t delay) { uint32_t count; count = delay; while (--count) { /** * Empty loop will be totally omitted by GCC. * Search "How to prevent GCC from optimizing out a busy wait loop?" @ StackOverflow. * This solution isn't portable. Zach Lee */ __ASM(""); } }
如何使用这个CMSIS-DAP进行代码调试?
硬件的连接是很简单的,我使用一块蓝药丸板做了CMSIS-DAP,用另一块蓝药丸板做被调试的目标板,下面这个图显示了如何把两块板连接在一起:
如果你使用一些商用的开发工具链,理论上CMSIS-DAP会自动地被这些工具链支持,除非它们试图检查设备的VID和PID。我现在使用gnu工具链,所以我决定选择OpenOCD做为gdbserver。WINDOWS版的OpenOCD可以从gnutoolchains.com网站上下载。但是,你必须要有一个Telnet客户端软件用来操作OpenOCD。启用WINDOWS 7内置的Telnet客户端很容易,如果你使用Cygwin,Telnet软件在Inetutils包中。
在启动OpenOCD之前,我们需要为其准备一个配置文件:
source [find interface/cmsis-dap.cfg] cmsis_dap_vid_pid 0x0483 0x5750 transport select swd source [find target/stm32f1x.cfg]
把这个文件命名为“openocd.cfg”,并且把它放到OpenOCD的安装目录之下3),你可以看到我指定了我自己的VID和PID,我不知道OpenOCD默认支持的VID/PID是什么。 现在我们运行“cmd.exe”打开一个DOS窗口,然后切换到OpenOCD安装目录之下,输入命令“.\bin\openocd”即可运行“openocd.exe”,并且配置文件“openocd.cfg”会被自动加载。如果一切正常的话我们会看到以下信息显示:
你可以看到OpenOCD正在监听4444端口等待Telnet连接,我们可以运行一个Telnet客户端使用一组OpenOCD定义的命令操作DAP后面连接的目标板。
我只试了一个reset halt命令,如果你在当前目录下放了个HEX文件,你可以试试用以下命令把它写入目标板:
flash write_image erase ./filename.hex
现在我们要试试在EmBitz这个IDE中使用我们新做的CMSIS-DAP。EmBitz有一个内建的调试模块可以支持OpenOCD,但是不支持CMSIS-DAP。如果你想用这个内建的调试模块,你必须修改一些脚本文件,它们都存放在${EMBITZ}\share\EmBitz\debuggers\Interfaces\openOCD目录下。这稍微有点复杂,所以我使用generic gdbserver:
你可以看到我把OpenOCD的安装目录挪到了${EMBITZ}\share\contrib\。我们需要点击Settings »
这个按钮告诉EmBitz如何复位目标板上的CPU,这很容易,只是简单地把一条命令直接交给OpenOCD即可。命令前缀monitor是告诉GDB客户端后接的命令reset halt是直接转发给OpenOCD的。
当EmBitz成功连接了OpenOCD之后我们还要发送一组命令,这些命令用于把编译好的机器码下载到目标板上的CPU内部的FLASH程序存储器中。需注意一下其中的load命令没有前缀monitor,这个命令是由GDB客户端执行的,EmBitz在OpenOCD擦除了目标板上的CPU存储器后把可执行代码下载到目标板。
还有两个提示最后说明一下:
- 有一个设置项“Run to main()”在Target settings属性页之中,你也可以在这页中选择“Try to stop at valid source info”设置项,这样选调试软件就会在启动代码cstartup的第一条指令处停下来。
- 把“openocd.cfg”文件放到工程目录下,也就是.cbp文件所在的那个目录。