Table of Contents
Implement CMSIS-DAP on STM32F103C8
- This project has been uploaded to Github and
- You can get the source package from my downloads page as well.
If you want to develop some embedded software on the modern MCUs, you do need a JTAG probe to connect the chip and your debugger. For ARM mcu, J-Link is a wellknown device which can be connected to the USB port and provide JTAG protocol for you debugger. CMSIS-DAP is a similar device which is published by ARM campany. The major diference between DAP and J-Link is that DAP is open source and J-Link is commercial product. Moreover, CMSIS-DAP is an USB HID device. You don't have to install a custom driver for it.
The source code of CMSIS-DAP 2.0.0 consists in the CMSIS library version 5. With the firmware source there are two examples developed for LPC-Link-II. The examples use RTE components and USB middleware from Keil MDK, even the CMSIS-RTOS API. There is a library file named USB_CM3.lib in the examples without source code. All of these makes the examples non-portable1). As a open source fan, I decide to implement this useful hardware with totally free development tools.
STM32F103 is a very popular and cheap mcu with a ARM Cortex-M3 core. It's designed by STMicroelectronics and I have some very simple developing board which is named Bluepill. This is a tiny core board with a STM32F103C8T6 on it. There isn't any unnecessary components on the board. The only problem is that the pullup resistor attached to USB D+ pin is 10K. It should be 1.5K, but it still works with the wrong value.
Creating a complete embedded software developing environment for STM32 mcu is quite easy. On STMicro's website, we can download some free software. The most important one is STM32CubeMX. This is a graphical configuration tools to help users choose the function of chip pins and automatically generate driver code. I configured the USB port, the SPI1 port and the USART1 and choose the USB Custom HID middleware. GPIOB10 to GPIOB15 are configured as JTAG master port. GPIOC13 is configured to drive the LED on the board. However, I don't know how to configure the CoreDebug and DWT module with STM32CubeMX. Is it possible?
STMicro developed their own JTAG probe which is named STLink. It's not necessary if you like to use J-Link or other device. The drivers and utilities of STLink can be downloaded on STMicro's website. There is an IDE software2) based on Eclipse developing system published on STMicro's web. I choose an IDE software named Embitz which is based on CodeBlocks. The arm_none_eabi_gcc version 5.4.1 is integrated into the IDE. Before I finished my project, I had to use STLink to debug my code. Now I'm using OpenOCD with my new CMSIS-DAP.
Getting started with source code of CMSIS-DAP
The source code of CMSIS-DAP is included in both CMSIS software package version 4 and 5. I downloaded CMSIS_5 from Github. The firmware of CMSIS-DAP core consists of five files but the examples are more important than the core files. I will start from the header files of the example V1.
- DAP_config.h
I choosed the LPC-Link-II V1 as my reference because it implemented an USB HID device. The first file I analyzed is “DAP_config.h”. The first critical section is listed below:
#ifdef _RTE_ #include "RTE_Components.h" #include CMSIS_device_header #else #include "device.h" // Debug Unit Cortex-M Processor Header File #endif
I want to discard the RTE directory, so the macro “_RTE_” won't be defined and I must create the header file “device.h”.
I redefined the macro “CPU_CLOCK” to 72000000 (72MHz). the macro “DAP_PACKET_SIZE” must be redefined to 64U according to the comments in this file. I redefined the “SWO_UART” to zero. That makes my work easier. The macro “TIMESTAMP_CLOCK” was defined to 72000000 (72MHz). LPC-Link-II uses DWT module of Cortex-M3 to implement TIMESTAMP. That's why I was trying to configure the DWT of STM32F103 with CubeMX. At last I wrote a tiny piece of code (in the file device.c) to do that:
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;
The pins defination are totally different from NXP LPC43xx. I rewrite all port operating code for STM32F103. There are some strange comments in this file, for example:
// 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); }
I was confused why the DAP core needs to read SWCLK/TCK pin? This pin needs to be configured as a “Output Push/Pull” pin to generate a CLOCK signal to JTAG slave. You can see the source code listed above returns the PIN value to caller. My implementation is a little bit different:
__STATIC_FORCEINLINE uint32_t PIN_SWCLK_TCK_IN (void) { return (uint32_t)(JTAG_TCK_GPIO_Port->ODR & JTAG_TCK_Pin ? 1:0); }
I return the CURRENT OUTPUT VALUE of the spicific pin. I'm not sure if it is right or not. This routine is invoked only two times by the function named “DAP_SWJ_Pins” in the file “DAP.c”. I guess “DAP_SWJ_Pins” is used to test if the PORT IO works well or not.
Another strange routine is “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); } }
I think the “release” means reconfigure the nRESET pin as a pure input (output disabled) pin. My code for STM32F103 is:
__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); } }
A special hint to you:
- You can see in some routines I wrote “if(…)” statment as below:
if ((bit & 1U) == 1) { ... } else { ... }
I use the lsb of parameter “bit” only because sometimes the parameter “bit” isn't 0 or 1. It may be 2 or other strange value, so don't write your code like this:
if (bit) { ... } else { ... }
IT DOES NOT WORK !!!
- 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__ */
This is a simple header file. It includes another header file which is named “cmsis_os2.h”. It is a part of the CMSIS library but I didn't copy it from that lib because not all contents of this file are necessary. I wrote a fake one instead of the original header. There is another header file named “DAP.h”. It belongs the DAP core module and includes the header “cmsis_compiler.h” which is part of the CMSIS library too. Certainly I wrote another fake one. So far I need to create three header file (device.h & cmsis_os2.h & cmsis_compiler.h) to support the DAP application.
Next I will make a brief introduction about the C source file: main.c and USBD_User_HID_0.c.
- main.c
I was trying to do a minimal modification to all the source files I downloaded. That's why I didn't take the header file “rl_usb.h” out of the project. Definitely I need a header file to define some macros about USB communation as well. There are some functions from CMSIS RTOS library. The most important one is “osThreadNew”. In my project it is implemented as below:
osThreadId_t osThreadNew(void (*func)(void *), void * n, void * ctx) { (void)n; (*func)(ctx); return 0; }
I just “JUMP into” the thread which is needed to be created. That means the functions “osKernelGetState & osKernelStart & osDelay” in “main.c” will never be invoked. The next important function is “USBD_Configured”. I will explain it when I generate driver code with STM32CubeMX.
- USBD_User_HID_0.c
I removed the file inclusion of “RTE\USB\USBD_Config_HID_0.h” and redefined the two macros “USBD_HID0_OUT_REPORT_MAX_SZ & USBD_HID0_IN_REPORT_MAX_SZ” in my own “rl_usb.h”.
The kernel of USB HID communacation is two circular queues which are managed by two interface routines:
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; }
USBD_HID0_SetReport will be invoked when the host send a “OUTPUT REPORT” to DAP. The parameter “rtype” of this function must be “HID_REPORT_OUTPUT”. USBD_HID0_GetReport will be invoked when an “INPUT REPORT” is sent to the host successfully. The parameter “rtype” of this function must be “HID_REPORT_INPUT” and the parameter “req” must be “USBD_HID_REQ_EP_INT”. That implies us all REPORTs must be transmited via USB INTERRUPT ENDPOINT with 64B packet size.
The routine “DAP_Thread” is just a command dispatcher. In this function there is an important function call:
USBD_HID_GetReportTrigger(0U, 0U, USB_Response[n], DAP_PACKET_SIZE);
We must implement the routine named “USBD_HID_GetReportTrigger” to send the INPUT REPORT to the host.
Generate driver codes with STM32CubeMX
There is a 8MHz crystal resonater on Bluepill board so I choose the HSE as the major clock signal. The “PLLMul” is “x9” and the 72MHz “PLLCLK” is selected for CPU and AHB/APB2. The “PCLK1” for APB1 is set to 36MHz. The USB prescaler is set to “1.5” to generate 48MHz clock to the USB peripheral. I choose “SPI1” as “Full-Duplex Master” without “NSS” signal and “USART1” as an “Asynchronous” port. The USB Device is enabled and the USB Middleware is configured as “Custom HID Class”. The “USBD_CUSTOMHID_OUTREPORT_BUF_SIZE” is set to 64 Bytes.
An important hint
- I didn't change the VID and PID in the DEVICE DESCRIPTOR. I guess some software running on the host will detect this two IDs.
If you find your software can not operate this CMSIS-DAP, maybe you need to use proper VID and PID. Just try the VID/PID in the source file of LPC-Link-II. The file name is “USBD_Config_0.c”. I removed it from my project.
The source codes generated by STM32CubeMX needs to be hacked. In the file “usbd_customhid.h”, the “CUSTOM_HID_EPIN_SIZE” and “CUSTOM_HID_EPOUT_SIZE” must be set to “0x40U”. I changed “CUSTOM_HID_FS_BINTERVAL” to 0x01 to try to speed up the HID communation.
In the structure “_USBD_CUSTOM_HID_Itf”, I inserted a new interface as below:
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;
When an “INPUT REPORT” is sent to host successfully, the function “InEvent” should be Invoked. Of course the function “USBD_CUSTOM_HID_DataIn” in “usbd_customhid.c” needs to be modified as below:
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; }
The “CUSTOM_HID_ReportDesc_FS” is defined in source file “usbd_custom_hid_if.c”. I defined a minimal descriptor as below:
/** 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 */ };
Maybe the “Feature Report” is not necessary in CMSIS-DAP. Just keep it.
I implemented my new interface function “CUSTOM_HID_InEvent_FS” in this source file as below:
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 */ }
The function “CUSTOM_HID_Init_FS” and “CUSTOM_HID_DeInit_FS” are modified to hook the Init/DeInit functions of DAP core:
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 */ }
Write a bridge module to connect all source codes together
In order to take the CMSIS RTOS out of my project, I wrote some stub routine to emulate a 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; }
The function “USBD_Configured” and “USBD_HID_GetReportTrigger” are implemented as below:
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; }
The function “USBD_CUSTOM_HID_SendReport” is generated by STM32CubeMX and stored in source file “usbd_customhid.c”. My own EventHandlers are listed below:
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); } }
- An important hint:
In the source file “DAP.h” of CMSIS-DAP core, is there a function named “PIN_DELAY_SLOW”. the original implementation of this function is:
__STATIC_FORCEINLINE void PIN_DELAY_SLOW (uint32_t delay) { uint32_t count; count = delay; while (--count); }
This empty loop “while (–count);” will be optimized by GCC. I found a smart idea at StackOverflow. It works well but isn't portable. Do you have new ideas?
__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(""); } }
How do we use the new CMSIS-DAP?
The hardware connection is really simple. I use one Bluepill board as the CMSIS-DAP and the other board as the target board. This picture shows the connection with only four wires.
If you use some commercial toolchain, in theory the new CMSIS-DAP will be supported automatically unless they try to detect the VID and PID. I'm using gnu toolchain so I decided to choose the OpenOCD as my gdbserver. The Windows version of OpenOCD can be downloaded from gnutoolchains.com. However, you must have a Telnet Client to operate the OpenOCD. It's easy to enable the Telnet Client of windows 7 or download Inetutils if you use Cygwin.
Before we launch the OpenOCD, we must create a config file for it:
source [find interface/cmsis-dap.cfg] cmsis_dap_vid_pid 0x0483 0x5750 transport select swd source [find target/stm32f1x.cfg]
I named this file as openocd.cfg and placed it under the current installed directory3) of OpenOCD. You can see that I specified the VID and PID of my DAP. I don't know the default VID and PID supported by OpenOCD.
Now we start the cmd.exe and switch the current directory to the OpenOCD installed directory where the config file was placed. Then we can launch the openocd.exe with a command .\bin\openocd and the config file will be loaded automatically. If all things are OK we will get some infomation as below:
You can see the OpenOCD is waiting for telnet connection at port 4444. We can operate the target board drived by CMSIS-DAP with a Telnet Client and a bunch of commands defined by OpenOCD:
I just tried a reset halt command. If you have a HEX file placed under the current directry, you can try to download it into the target board with the command listed below:
flash write_image erase ./filename.hex
Now we will try to use the new CMSIS-DAP with the IDE software EmBitz. EmBitz has a builtin debug module which supports OpenOCD, but it doesn't support CMSIS-DAP. If you want to use the builtin debug module, you have to hack some script files which are stored under ${EMBITZ}\share\EmBitz\debuggers\Interfaces\openOCD. It is a little bit complicated so I use the OpenOCD as a generic gdbserver:
You can see I moved the OpenOCD installed directory to ${EMBITZ}\share\contrib\. We need to click the Settings »
button to tell EmBitz how to reset the CPU on target board. It is a simple command sent to OpenOCD directly. The prefix monitor tells the GDB Client passing the command reset halt to gdbserver.
We need to send some commands to OpenOCD when EmBitz connects gdbserver successfully. These commands are used to download machine codes into the flash memory of target board. Please notice that the load command doesn't have the prefix monitor because it is a command for GDB Client. EmBitz will download the executable file after OpenOCD erase the whole flash of target board.
There are two hints at the end of the article:
- The setting option “Run to main()” is located on Target settings property page. You can also select “Try to stop at valid source info”. The debugger will stop at the first instruction of your cstartup file.
- Copy the openocd.cfg into your project directory where the .cbp file is placed.