0x00 前言
笔者这几天在尝试 DIY 一个 DAPLink,所以来阅读一遍 raspberrypi/debugprobe 的源码。这个项目在 RP2040 上实现了 CMSIS-DAP 调试器,并提供虚拟串口。
在观察代码之前,我们有必要先了解一下 CMSIS-DAP 项目。我们的电脑上有 usb 接口,而单片机上有 swd 或 jtag 调试接口;CMSIS-DAP 即是将它们连接起来的桥梁。
CMSIS-DAP 本身是一种协议,不过 ARM-software/CMSIS-DAP 项目中也提供了一份参考实现。简而言之,一个完全实现了最新版 CMSIS-DAP 协议的调试器,可以提供如下功能:
- 通过 swd 或 jtag 连接到目标芯片,访问 Cortex-A/R/M 的 CoreSight 寄存器。支持多核调试。
- 支持 SWO。我们可以将其理解为 MCU 发往主机的单向串口,从而能通过 SWO 实现
printf
。它的速度比 semihosting 更快(但是没有 SEGGER RTT 快)。 - 免驱。CMSIS-DAP v1 使用 usb HID,消息传输的最小间隔是 1ms,拖慢了速度。CMSIS-DAP v2 使用 winusb。见 armbbs 讨论。
CMSIS-DAP 有很多种软件实现,除了它自带的参考实现(CMSIS-DAP firmware,支持恩智浦单片机)之外,最著名的是 ARMmbed/DAPLink 项目,该项目是 CMSIS-DAP firmware 的后继项目,所以可以认为 DAPLink 目前是 CMSIS-DAP 的标准实现。它除了实现 CMSIS-DAP v1(usb HID)、CMSIS-DAP v2(winusb)之外,还提供了虚拟串口、拖拽烧录、WebUSB 功能。
我们的开发板一般只引出了 SWCLK 和 SWDIO 这两条调试线。那么,一个极简板的调试器,只需要维护 SWCLK 和 SWDIO 波形。事实上市售的绝大部分调试器都没有完整支持 CMSIS-DAP,而是只提供 swd 接口和虚拟串口。debugprobe 就是 CMSIS-DAP 的简版实现,不支持 jtag,也不支持 SWO。
0x01 背景:usb 和 swd 简述
debugprobe 的一端通过 usb 协议连接电脑,另一端通过 swd 协议连接单片机。我们先粗略了解这两个协议。
usb 协议参考资料:维基百科、yooooooo 博客文章。
usb 2.0 协议分为低速(Low-Speed,1.5 Mbps,亦称为 usb 1.0)、全速(Full-Speed,12 Mbps,亦称为 usb 1.1)、高速(High-Speed,480 Mbps)模式,通过差分线 D+/D- 传递信息,本文无需关心物理层细节。我们常用的单片机,很多都支持 FS 模式,例如 RP2040 和 STM32F103。
usb 2.0 协议是请求-响应式协议,只能由主机发起请求。每个设备可以有多个端点(endpoint),由逻辑管道(pipe)连接到主机。一个端点对应一条逻辑管道。单个设备至多拥有 16 个输入端点和 16 个输出端点。
pipe 分为 message pipe 和 stream pipe 两种。
- message pipe 是双向管道,用于控制。每个设备的 0 号端点都是控制端点。
- stream pipe 是单向管道,连接到单向端点。这类管道用于传输数据,有几种传输方式:
- interrupt transfer(中断传输)。保证延迟上限。一般用于低延迟应用,例如鼠标、键盘,由主机轮询设备。
- bulk transfer(批量传输)。尽可能快地传输大量数据,保证数据准确性,但不保证速率。
- isochronous transfer(等时传输)。保证恒定速率,但可能会丢包。
usb 设备插入主机后的初始化过程称为枚举(enumeration)。在枚举过程中,主机会给设备分配 7 bit 的地址。
我们首先观察 RP2040 的处理器结构图:
可以看到,DAP(Debug Access Port)外挂在 Cortex-M0+ 之外,每个核各有一个 DAP 连接到核内的调试接口。DAP 则通过 swd 协议与调试器相连。
ADIv5 手册中的 DAP 结构图:
一个 DAP 中拥有 DP 和 AP。其中 DP(Debug Port)连接到调试器,走 jtag 或 swd 协议(对应的 DP 实现分别称为 JTAG-DP 和 SW-DP):
DAP 中可以有多个 AP(Access Port)。ADIv5 手册中给出了 MEM-AP 和 JTAG-AP 两种选择。前者可以直连调试寄存器,也可以连接到系统总线。
RP2040 的 DAP 可以访问整个地址空间。详情参考 ARMv6-M 手册中的 debug 部分。
接下来我们分析 swd 协议,即调试器与 SW-DP 之间的通讯。每一次 swd 操作包含的步骤是:
- packet request,由调试器向 DP 发出请求
- acknowledge response,由 DP 返回 ACK 信号
- data transfer phase,传输数据。可能是 RDATA(从 DP 发往调试器)或 WDATA(从调试器发往 DP)。
下图展示了一次成功的 write 操作。它包含 8 bit 的 packet request、3 bit 的 ACK,以及 33 bit 的 data write(最后一个 bit 是奇偶校验位)。
对于成功的 read 操作,由主机发送 8 bit 的 packet request,由 DP 返回 ACK 和数据。
对于一次请求,DP 可以返回 WAIT。此时只发回 010
ACK 信号。
类似地,DP 可以返回 FAULT:
以上就是所有的数据传输报文类型。在初始化过程中,调试器会发送一个 line reset 信号,即连续 50 个高电平。协议规定,无论 DP 处于何种状态,只要收到 50 个连续的 1
,它就应该重置。调试器在发送 line reset 之后,必须立即读取 IDCODE 寄存器。
寄存器清单:
0x02 通讯实例分析
我们已经了解调试器与 SW-DP 之间的通讯协议,不妨现在来监听一次通讯。调试器选用树莓派 debugprobe,目标 MCU 是 STM32H743VIT6。指令:
openocd -f interface/cmsis-dap.cfg -f target/stm32h7x.cfg
波形:
调试器首先发送了 line reset 信号,然后选择 swd 协议,然后再次发送 line reset 信号,接下来进入通讯流程。读取 IDCODE 寄存器,返回 0x6ba02477
:
现在,我们试试用 swd 协议读一个特定地址。打开 pyocd cmd,读取芯片的 unique ID(即从 0x1FF1E800
开始的 12 个字节):
$ pyocd cmd -t stm32h743vitx
Connected to STM32H743VITx [Running]: E6633861A352632C
pyocd> read8 0x1FF1E800 12
1ff1e800: 1f 00 2f 00 05 51 33 31 30 38 32 32 |../..Q310822|
波形如下:
详细观察通讯过程。首先,调试器向 AP4 寄存器写入了我们要读取的地址 0x1ff1e800
:
接下来读取 APc 寄存器。有趣的是,读出来的值是一个旧值(笔者在 pyocd 中执行的上一条指令是 reg
,得到 xpsr 的值是 0x21000000
),而不是我们请求的值:
继续读取 APc 寄存器,读出 0x002f001f
,这是我们请求的位置的前 4 字节:
继续读取 APc 寄存器,读出 0x31335105
,是目标位置的中间 4 字节:
最后一次读取,读的不是 APc,而是 RDBUFF。返回了最后 4 字节:
详细的 swd 通讯过程解释可以参考这篇文章。
可以用 wireshark + usbpcap 抓取 usb 通讯。结果如下:
可见,debugprobe 使用 bulk 模式传输数据。报文如下:
PC -> debugprobe: 05 00 04 05 00e8f11f 0f 0f 0f
debugprobe -> PC: 05 04 01 1f002f000551333130383232
按照 CMSIS-DAP 文档,可以解读出内容。这次通讯是 DAP_Transfer
指令,用于读写寄存器。PC 发送的报文中,05
表示指令类型,00
在 swd 模式下被忽略,04
是 Transfer Count,05
是第一次 transfer 的 Transfer Request 信息,指定了 APnDP=1
,即要访问的是 AP 寄存器;RnW=0
,即写入寄存器,接下来发送了要写入的内容。后三次 transfer 的 Transfer Request 均为 0x0f
,读取 AP 寄存器。
0x03 debugprobe 项目结构
在开始阅读 debugprobe 源码之前,我们先观察其结构:
debugprobe
├── CMakeLists.txt
├── CMSIS_5
├── freertos
├── FreeRTOS_Kernel_import.cmake
├── include
│ ├── board_debug_probe_config.h
│ ├── board_example_config.h
│ ├── board_pico_config.h
│ └── DAP_config.h
├── pico_sdk_import.cmake
├── README.md
└── src
├── cdc_uart.c
├── cdc_uart.h
├── FreeRTOSConfig.h
├── get_serial.c
├── get_serial.h
├── led.c
├── led.h
├── main.c
├── probe.c
├── probe_config.c
├── probe_config.h
├── probe.h
├── probe_oen.pio
├── probe.pio
├── sw_dp_pio.c
├── tusb_config.h
├── tusb_edpt_handler.c
├── tusb_edpt_handler.h
└── usb_descriptors.c
统计一下代码行数:
find . -type f -name '*.c' | xargs cat | wc -l
# 1568
find . -type f -name '*.h' | xargs cat | wc -l
# 1295
可见这是一个小型项目,预计几个小时就能读完代码。编译方式:
git submodule update --init
mkdir build
cd build
PICO_SDK_PATH=../../../../embedded/rp2040/pico-sdk/ cmake -DDEBUG_ON_PICO=ON ..
make
我们先来看 CMakeList。
cmake_minimum_required(VERSION 3.12)
# 引入 pico sdk
include(pico_sdk_import.cmake)
# 引入 FreeRTOS
set(FREERTOS_KERNEL_PATH ${CMAKE_CURRENT_LIST_DIR}/freertos)
include(FreeRTOS_Kernel_import.cmake)
project(debugprobe)
pico_sdk_init()
# 本项目代码
add_executable(debugprobe
src/probe_config.c
src/led.c
src/main.c
src/usb_descriptors.c
src/probe.c
src/cdc_uart.c
src/get_serial.c
src/sw_dp_pio.c
src/tusb_edpt_handler.c
)
# CMSIS-DAP 源码
target_sources(debugprobe PRIVATE
CMSIS_5/CMSIS/DAP/Firmware/Source/DAP.c
CMSIS_5/CMSIS/DAP/Firmware/Source/JTAG_DP.c
CMSIS_5/CMSIS/DAP/Firmware/Source/DAP_vendor.c
CMSIS_5/CMSIS/DAP/Firmware/Source/SWO.c
#CMSIS_5/CMSIS/DAP/Firmware/Source/SW_DP.c
)
target_include_directories(debugprobe PRIVATE
CMSIS_5/CMSIS/DAP/Firmware/Include/
CMSIS_5/CMSIS/Core/Include/
include/
)
target_compile_options(debugprobe PRIVATE -Wall)
# .pio 转 .h
pico_generate_pio_header(debugprobe ${CMAKE_CURRENT_LIST_DIR}/src/probe.pio)
pico_generate_pio_header(debugprobe ${CMAKE_CURRENT_LIST_DIR}/src/probe_oen.pio)
target_include_directories(debugprobe PRIVATE src)
target_compile_definitions (debugprobe PRIVATE
PICO_RP2040_USB_DEVICE_ENUMERATION_FIX=1
)
# 为 pico 开发板构建
option (DEBUG_ON_PICO "Compile firmware for the Pico instead of Debug Probe" OFF)
if (DEBUG_ON_PICO)
target_compile_definitions (debugprobe PRIVATE
DEBUG_ON_PICO=1
)
set_target_properties(debugprobe PROPERTIES
OUTPUT_NAME "debugprobe_on_pico"
)
endif ()
# 引入库
target_link_libraries(debugprobe PRIVATE
pico_multicore
pico_stdlib
pico_unique_id
tinyusb_device
tinyusb_board
hardware_pio
FreeRTOS-Kernel
FreeRTOS-Kernel-Heap1
)
# 在启动时将所有代码搬到 RAM 中执行
pico_set_binary_type(debugprobe copy_to_ram)
pico_add_extra_outputs(debugprobe)
可以看到,debugprobe 使用了三个第三方库:CMSIS_5、FreeRTOS 和 tinyusb。尤其值得注意的是,debugprobe 使用了 CMSIS-DAP firmware 中的 DAP.c
、JTAG_DP.c
、DAP_vendor.c
、SWO.c
,但没有使用 SW_DP.c
。观察 SW_DP.c
,发现其用途是对 SWCLK 和 SWDIO 进行 bit-banging。在 RP2040 上,这份工作可以改用更优雅的 PIO 来实现。
既然「调试器到 DAP」的上层逻辑沿用了 CMSIS-DAP 的代码,那么剩余的工作就是写一份基于 PIO 的底层传输代码,以及实现调试器与主机之间的 usb 通讯。后者建立在 tinyusb 库上。
0x04 include 目录
debugprobe 项目只有四个 .h
文件,所以我们在进入 main
函数之前,先快速浏览一遍这些 .h
文件。
include/board_example_config.h
是 board config 模板。目前 debugprobe 项目适配了官方调试器产品(原理图开源)和 Pico 开发板,它们拥有各自的 board config。来看模板内容,首先是引脚连接方式:
// SWCLK/SWDIO 的物理连接方式。Pico 开发板是 PROBE_IO_RAW,调试器产品是 PROBE_IO_SWDI
/* Select one of these. */
/* Direct connection - SWCLK/SWDIO on two GPIOs */
#define PROBE_IO_RAW
/* SWCLK connected to a GPIO, SWDO driven from a GPIO, SWDI sampled via a level shifter */
#define PROBE_IO_SWDI
/* Level-shifted SWCLK, SWDIO with separate SWDO, SWDI and OE_N pin */
#define PROBE_IO_OEN
接下来是 uart 相关设置:
/* Include CDC interface to bridge to target UART. Omit if not used. */
#define PROBE_CDC_UART
/* Board implements hardware flow control for UART RTS/CTS instead of ACM control */
#define PROBE_UART_HWFC
接下来是复位信号:
/* Target reset GPIO (active-low). Omit if not used.*/
#define PROBE_PIN_RESET 1
接着是 PIO 状态机相关的设置:
#define PROBE_SM 0
#define PROBE_PIN_OFFSET 12
/* PIO config for PROBE_IO_RAW */
#if defined(PROBE_IO_RAW)
#define PROBE_PIN_SWCLK (PROBE_PIN_OFFSET + 0)
#define PROBE_PIN_SWDIO (PROBE_PIN_OFFSET + 1)
#endif
/* PIO config for PROBE_IO_SWDI */
#if defined(PROBE_IO_SWDI)
#define PROBE_PIN_SWCLK (PROBE_PIN_OFFSET + 0)
#define PROBE_PIN_SWDIO (PROBE_PIN_OFFSET + 1)
#define PROBE_PIN_SWDI (PROBE_PIN_OFFSET + 2)
#endif
/* PIO config for PROBE_IO_OEN - note that SWDIOEN and SWCLK are both side_set signals, so must be consecutive. */
#if defined(PROBE_IO_OEN)
#define PROBE_PIN_SWDIOEN (PROBE_PIN_OFFSET + 0)
#define PROBE_PIN_SWCLK (PROBE_PIN_OFFSET + 1)
#define PROBE_PIN_SWDIO (PROBE_PIN_OFFSET + 2)
#define PROBE_PIN_SWDI (PROBE_PIN_OFFSET + 3)
#endif
又是几个 uart 设置:
#if defined(PROBE_CDC_UART)
#define PROBE_UART_TX 4
#define PROBE_UART_RX 5
#define PROBE_UART_INTERFACE uart1
#define PROBE_UART_BAUDRATE 115200
#if defined(PROBE_UART_HWFC)
/* Hardware flow control - see 1.4.3 in the RP2040 datasheet for valid pin settings */
#define PROBE_UART_CTS 6
#define PROBE_UART_RTS 7
#else
/* Software flow control - RTS and DTR can be omitted if not used */
#define PROBE_UART_RTS 9
#endif
#define PROBE_UART_DTR 10
#endif
最后是 LED 设置:
/* LED config - some or all of these can be omitted if not used */
#define PROBE_USB_CONNECTED_LED 2
#define PROBE_DAP_CONNECTED_LED 15
#define PROBE_DAP_RUNNING_LED 16
#define PROBE_UART_RX_LED 7
#define PROBE_UART_TX_LED 8
#define PROBE_PRODUCT_STRING "Example Debug Probe"
综上所述,假如我们想要自己做一块调试器,那么我们可以写一份 board config 文件,自定义 swd 物理连接类型、是否使用 uart、是否使用硬件流控、使用哪些引脚控制 LED。
下面来看 include/board_pico_config.h
,我们将 Pico 开发板刷成调试器的时候,使用的就是这份配置。
// SWCLK 和 SWDIO 直连 GPIO
#define PROBE_IO_RAW
// 使用虚拟串口
#define PROBE_CDC_UART
// PIO 相关
#define PROBE_SM 0
#define PROBE_PIN_OFFSET 2
#define PROBE_PIN_SWCLK (PROBE_PIN_OFFSET + 0) // 2
#define PROBE_PIN_SWDIO (PROBE_PIN_OFFSET + 1) // 3
// 不引出 RESET 信号
#if false
#define PROBE_PIN_RESET 1
#endif
// uart 相关
#define PROBE_UART_TX 4
#define PROBE_UART_RX 5
#define PROBE_UART_INTERFACE uart1
#define PROBE_UART_BAUDRATE 115200
// 把板载 LED 作为 connected 信号灯
#define PROBE_USB_CONNECTED_LED 25
// 调试器名称,会显示在 pyocd list 中
#define PROBE_PRODUCT_STRING "Debugprobe on Pico (CMSIS-DAP)"
而调试器产品拥有电平转换电路和五个 LED,它的配置如下:
// SWCLK、SWDO 直连;SWDI 经过电平转换 IC 之后再连入 GPIO
#define PROBE_IO_SWDI
#define PROBE_CDC_UART
// No reset pin
// PIO config
#define PROBE_SM 0
#define PROBE_PIN_OFFSET 12
#define PROBE_PIN_SWCLK (PROBE_PIN_OFFSET + 0)
// For level-shifted input.
#define PROBE_PIN_SWDI (PROBE_PIN_OFFSET + 1)
#define PROBE_PIN_SWDIO (PROBE_PIN_OFFSET + 2)
// UART config
#define PROBE_UART_TX 4
#define PROBE_UART_RX 5
#define PROBE_UART_INTERFACE uart1
#define PROBE_UART_BAUDRATE 115200
// 五个 LED
#define PROBE_USB_CONNECTED_LED 2
#define PROBE_DAP_CONNECTED_LED 15
#define PROBE_DAP_RUNNING_LED 16
#define PROBE_UART_RX_LED 7
#define PROBE_UART_TX_LED 8
// 调试器名称
#define PROBE_PRODUCT_STRING "Debug Probe (CMSIS-DAP)"
最后,我们还剩 include/DAP_config.h
这个文件要分析。事实上它是从 CMSIS_5/CMSIS/DAP/Firmware/Config/DAP_config.h
修改来的。文件有点长,我们挑重点看:
// 给出 MCU 频率
/// Processor Clock of the Cortex-M MCU used in the Debug Unit.
/// This value is used to calculate the SWD/JTAG clock speed.
/* Debugprobe actually uses kHz rather than Hz, so just lie about it here */
#define CPU_CLOCK 125000000U ///< Specifies the CPU Clock in Hz.
// IO 速度。RP2040 是 Cortex-M0+,有 single cycle IO
/// Number of processor cycles for I/O Port write operations.
/// This value is used to calculate the SWD/JTAG clock speed that is generated with I/O
/// Port write operations in the Debug Unit by a Cortex-M MCU. Most Cortex-M processors
/// require 2 processor cycles for a I/O Port Write operation. If the Debug Unit uses
/// a Cortex-M0+ processor with high-speed peripheral I/O only 1 processor cycle might be
/// required.
#define IO_PORT_WRITE_CYCLES 1U ///< I/O Cycles: 2=default, 1=Cortex-M0+ fast I/0.
#define DELAY_SLOW_CYCLES 1U // We don't differentiate between fast/slow, we've got a 16-bit divisor for that
// 声明自己提供 swd 协议支持
/// Indicate that Serial Wire Debug (SWD) communication mode is available at the Debug Access Port.
/// This information is returned by the command \ref DAP_Info as part of <b>Capabilities</b>.
#define DAP_SWD 1 ///< SWD Mode: 1 = available, 0 = not available.
// 声明自己不支持 jtag
/// Indicate that JTAG communication mode is available at the Debug Port.
/// This information is returned by the command \ref DAP_Info as part of <b>Capabilities</b>.
#define DAP_JTAG 0 ///< JTAG Mode: 1 = available, 0 = not available.
// 最大 DAP 包长度,采用 64
/// Maximum Package Size for Command and Response data.
/// This configuration settings is used to optimize the communication performance with the
/// debugger and depends on the USB peripheral. Typical vales are 64 for Full-speed USB HID or WinUSB,
/// 1024 for High-speed USB HID and 512 for High-speed USB WinUSB.
#define DAP_PACKET_SIZE 64U ///< Specifies Packet Size in bytes.
// usb buffer 大小
/// Maximum Package Buffers for Command and Response data.
/// This configuration settings is used to optimize the communication performance with the
/// debugger and depends on the USB peripheral. For devices with limited RAM or USB buffer the
/// setting can be reduced (valid range is 1 .. 255).
#define DAP_PACKET_COUNT 2U ///< Specifies number of packets buffered.
// 声明自己不支持 SWO
/// Indicate that UART Serial Wire Output (SWO) trace is available.
/// This information is returned by the command \ref DAP_Info as part of <b>Capabilities</b>.
#define SWO_UART 0 ///< SWO UART: 1 = available, 0 = not available.
// 声明自己不支持 DAP uart。此项目自行实现了虚拟串口。
/// Indicate that UART Communication Port is available.
/// This information is returned by the command \ref DAP_Info as part of <b>Capabilities</b>.
#define DAP_UART 0 ///< DAP UART: 1 = available, 0 = not available.
// 不指定 target。所以我们运行 pyocd list 时,debugprobe 没有显示特定的 target
/// Debug Unit is connected to fixed Target Device.
/// The Debug Unit may be part of an evaluation board and always connected to a fixed
/// known device. In this case a Device Vendor, Device Name, Board Vendor and Board Name strings
/// are stored and may be used by the debugger or IDE to configure device parameters.
#define TARGET_FIXED 0 ///< Target: 1 = known, 0 = unknown;
以上就是配置,接下来是一些函数的实现。例如,在 CMSIS 的 DAP_config.h
中,有这样一行:
__STATIC_INLINE void LED_CONNECTED_OUT (uint32_t bit) {}
而在 debugprobe 的 include/DAP_config.h
中,给出了具体实现:
__STATIC_INLINE void LED_RUNNING_OUT (uint32_t bit) {
#ifdef PROBE_DAP_RUNNING_LED
gpio_put(PROBE_DAP_RUNNING_LED, bit);
#endif
}
debugprobe 不支持复位引脚,所以对于 RESET_TARGET
的实现是:
__STATIC_INLINE uint8_t RESET_TARGET (void) {
return (0U); // change to '1' when a device reset sequence is implemented
}
至此,我们看完了 include
目录下的头文件。接下来该关注 src
目录了。
0x05 三个 RTOS 任务
来看 main
函数:
int main(void) {
// 写一些(二进制层面的)注释,pico examples 中常用。可以通过 picotool 读出来
bi_decl_config();
// 初始化调试板,例如点亮 Pico 开发板的 LED
board_init();
// 用 RP2040 的 unique id 计算出 usb iSerialNumber
usb_serial_init();
// 初始化 uart 外设
cdc_uart_init();
// 初始化 tinyusb
tusb_init();
// 将 printf 输出到 RP2040 自身的串口。应该是调试用途
stdio_uart_init();
// 初始化 DAP。这个函数是 DAP.c 提供的
DAP_Setup();
// 初始化五个 LED
led_init();
probe_info("Welcome to debugprobe!\n");
// 这份文件的开头已经定义 THREADED = 1,所以一定会执行下面的代码
if (THREADED) {
// 调用 FreeRTOS API,创建三个任务
// uart 任务,优先级为 +3
/* UART needs to preempt USB as if we don't, characters get lost */
xTaskCreate(cdc_thread, "UART", configMINIMAL_STACK_SIZE, NULL, UART_TASK_PRIO, &uart_taskhandle);
// tinyusb 任务,优先级为 +2
xTaskCreate(usb_thread, "TUD", configMINIMAL_STACK_SIZE, NULL, TUD_TASK_PRIO, &tud_taskhandle);
/* Lowest priority thread is debug - need to shuffle buffers before we can toggle swd... */
// DAP 任务,优先级为 +1
xTaskCreate(dap_thread, "DAP", configMINIMAL_STACK_SIZE, NULL, DAP_TASK_PRIO, &dap_taskhandle);
// 启动 FreeRTOS 调度器
vTaskStartScheduler();
}
// 下面的逻辑不会执行。应该是调试用途
while (!THREADED) {
tud_task();
cdc_task();
#if (PROBE_DEBUG_PROTOCOL == PROTO_DAP_V2)
if (tud_vendor_available()) {
uint32_t resp_len;
tud_vendor_read(RxDataBuffer, sizeof(RxDataBuffer));
resp_len = DAP_ProcessCommand(RxDataBuffer, TxDataBuffer);
tud_vendor_write(TxDataBuffer, resp_len);
}
#endif
}
return 0;
}
这份代码的逻辑非常清晰,我们接下来只需要跟进 cdc_thread
、usb_thread
、dap_thread
这三个任务。我们就按照优先级从高到低的顺序来分析这些任务,首先是 cdc:
// 简而言之:连续调用 cdc_task()
void cdc_thread(void *ptr)
{
BaseType_t delayed;
last_wake = xTaskGetTickCount();
bool keep_alive;
/* Threaded with a polling interval that scales according to linerate */
while (1) {
keep_alive = cdc_task();
if (!keep_alive) {
delayed = xTaskDelayUntil(&last_wake, interval);
if (delayed == pdFALSE)
last_wake = xTaskGetTickCount();
}
}
}
bool cdc_task(void)
{
static int was_connected = 0;
static uint cdc_tx_oe = 0;
uint rx_len = 0;
bool keep_alive = false;
// 把串口收到的数据装进 rx_buf
// Consume uart fifo regardless even if not connected
while(uart_is_readable(PROBE_UART_INTERFACE) && (rx_len < sizeof(rx_buf))) {
rx_buf[rx_len++] = uart_getc(PROBE_UART_INTERFACE);
}
// 检查 cdc 是否连接。这是 lib/tinyusb/src/class/cdc/cdc_device.h 提供的 API
if (tud_cdc_connected()) {
was_connected = 1;
int written = 0;
/* Implicit overflow if we don't write all the bytes to the host.
* Also throw away bytes if we can't write... */
// 若现在 buf 里面有数据,则应该发给电脑
if (rx_len) {
#ifdef PROBE_UART_RX_LED
// 点亮 LED
gpio_put(PROBE_UART_RX_LED, 1);
rx_led_debounce = debounce_ticks;
#endif
// tud_cdc_write_available() 这个 API 返回 cdc 现在能接受多少个字节的写入
written = MIN(tud_cdc_write_available(), rx_len);
if (rx_len > written)
cdc_tx_oe++;
// 把数据写入 cdc,发给电脑
if (written > 0) {
tud_cdc_write(rx_buf, written);
tud_cdc_write_flush();
}
} else {
// 考虑关闭 RX LED
#ifdef PROBE_UART_RX_LED
if (rx_led_debounce)
rx_led_debounce--;
else
gpio_put(PROBE_UART_RX_LED, 0);
#endif
}
/* Reading from a firehose and writing to a FIFO. */
size_t watermark = MIN(tud_cdc_available(), sizeof(tx_buf));
if (watermark > 0) {
size_t tx_len;
#ifdef PROBE_UART_TX_LED
// 有数据要发送给 uart,点亮 LED
gpio_put(PROBE_UART_TX_LED, 1);
tx_led_debounce = debounce_ticks;
#endif
/* Batch up to half a FIFO of data - don't clog up on RX */
watermark = MIN(watermark, 16);
tx_len = tud_cdc_read(tx_buf, watermark);
// 发出数据
uart_write_blocking(PROBE_UART_INTERFACE, tx_buf, tx_len);
} else {
// 没数据要发给 uart,考虑关闭 TX LED
#ifdef PROBE_UART_TX_LED
if (tx_led_debounce)
tx_led_debounce--;
else
gpio_put(PROBE_UART_TX_LED, 0);
#endif
}
/* Pending break handling */
if (timed_break) {
if (((int)break_expiry - (int)xTaskGetTickCount()) < 0) {
timed_break = false;
uart_set_break(PROBE_UART_INTERFACE, false);
#ifdef PROBE_UART_TX_LED
tx_led_debounce = 0;
#endif
} else {
keep_alive = true;
}
}
} else if (was_connected) {
// 以下逻辑在 cdc 连接断开时执行
tud_cdc_write_clear();
uart_set_break(PROBE_UART_INTERFACE, false);
timed_break = false;
was_connected = 0;
#ifdef PROBE_UART_TX_LED
tx_led_debounce = 0;
#endif
cdc_tx_oe = 0;
}
return keep_alive;
}
上述代码逻辑还算清晰,就是在 tinyusb 的 cdc 与 RP2040 的 uart 外设之间交换数据。tx_buf
和 rx_buf
的长度都是 32 字节。
接下来分析 usb_thread
。
void usb_thread(void *ptr)
{
TickType_t wake;
wake = xTaskGetTickCount();
do {
tud_task();
#ifdef PROBE_USB_CONNECTED_LED
if (!gpio_get(PROBE_USB_CONNECTED_LED) && tud_ready())
gpio_put(PROBE_USB_CONNECTED_LED, 1);
else
gpio_put(PROBE_USB_CONNECTED_LED, 0);
#endif
// Go to sleep for up to a tick if nothing to do
if (!tud_task_event_ready())
xTaskDelayUntil(&wake, 1);
} while (1);
}
这个函数的功能就是连续调用 tud_task()
,而 tud_task
是 tinyusb 提供的 API。根据 tinyusb 文档,用户应该定期调用这个函数。我们没有跟进的必要。
接下来是最后一个任务,dap_thread
。代码位于 src/tusb_edpt_handler.c
,内容如下:
void dap_thread(void *ptr)
{
uint32_t n;
do
{
while(USBRequestBuffer.rptr != USBRequestBuffer.wptr)
{
/*
* Atomic command support - buffer QueueCommands, but don't process them
* until a non-QueueCommands packet is seen.
*/
n = USBRequestBuffer.rptr;
// 这是循环队列
// 队列中可以容纳 DAP_PACKET_COUNT 个元素
// 有 wptr 和 rptr 这两个指针
// 向队列中加入元素则 wptr++
// 从队列中消耗元素则 rptr++
// 因此,rptr 指向队首,wptr 指向队尾,队列元素是 [rptr, wptr)
// 在这个循环中,n 从队首开始向后走,直到遇到非 ID_DAP_QueueCommands 的元素
while (USBRequestBuffer.data[n % DAP_PACKET_COUNT][0] == ID_DAP_QueueCommands) {
probe_info("%u %u DAP queued cmd %s len %02x\n",
USBRequestBuffer.wptr, USBRequestBuffer.rptr,
dap_cmd_string[USBRequestBuffer.data[n % DAP_PACKET_COUNT][0]], USBRequestBuffer.data[n % DAP_PACKET_COUNT][1]);
// 将队首从 ID_DAP_QueueCommands 改为 ID_DAP_ExecuteCommands
USBRequestBuffer.data[n % DAP_PACKET_COUNT][0] = ID_DAP_ExecuteCommands;
n++;
while (n == USBRequestBuffer.wptr) {
// 若已经走到队列末尾,则等待新元素入队
/* Need yield in a loop here, as IN callbacks will also wake the thread */
probe_info("DAP wait\n");
vTaskSuspend(dap_taskhandle);
}
}
// 现在,把 USBRequestBuffer 的队首输出到 DAPRequestBuffer
// Read a single packet from the USB buffer into the DAP Request buffer
memcpy(DAPRequestBuffer, RD_SLOT_PTR(USBRequestBuffer), DAP_PACKET_SIZE);
probe_info("%u %u DAP cmd %s len %02x\n",
USBRequestBuffer.wptr, USBRequestBuffer.rptr,
dap_cmd_string[DAPRequestBuffer[0]], DAPRequestBuffer[1]);
USBRequestBuffer.rptr++;
// If the buffer was full in the out callback, we
// need to queue up another buffer for the endpoint
// to consume, now that we know there is space in the buffer.
if(USBRequestBuffer.wasFull)
{
vTaskSuspendAll(); // Suspend the scheduler to safely update the write index
USBRequestBuffer.wptr++;
// 把主机传来的数据存进 USBRequestBuffer
usbd_edpt_xfer(_rhport, _out_ep_addr, WR_SLOT_PTR(USBRequestBuffer), DAP_PACKET_SIZE);
USBRequestBuffer.wasFull = false;
xTaskResumeAll();
}
// 这个 API 由 DAP.c 提供,执行 DAP 请求并获得响应
_resp_len = DAP_ExecuteCommand(DAPRequestBuffer, DAPResponseBuffer);
probe_info("%u %u DAP resp %s\n",
USBResponseBuffer.wptr, USBResponseBuffer.rptr,
dap_cmd_string[DAPResponseBuffer[0]]);
// Suspend the scheduler to avoid stale values/race conditions between threads
vTaskSuspendAll();
// 若现在 USBResponseBuffer 为空
if(buffer_empty(&USBResponseBuffer))
{
// 把响应结果拷贝到 USBResponseBuffer
memcpy(WR_SLOT_PTR(USBResponseBuffer), DAPResponseBuffer, (uint16_t) _resp_len);
USBResponseBuffer.wptr++;
// 把响应结果发给主机
usbd_edpt_xfer(_rhport, _in_ep_addr, RD_SLOT_PTR(USBResponseBuffer), (uint16_t) _resp_len);
} else {
// 把响应结果拷贝到 USBResponseBuffer
memcpy(WR_SLOT_PTR(USBResponseBuffer), DAPResponseBuffer, (uint16_t) _resp_len);
USBResponseBuffer.wptr++;
// The In callback needs to check this flag to know when to queue up the next buffer.
USBResponseBuffer.wasEmpty = false;
}
xTaskResumeAll();
}
// Suspend DAP thread until it is awoken by a USB thread callback
vTaskSuspend(dap_taskhandle);
} while (1);
}
这段代码比较复杂,循环队列也缺乏封装,略微难读。理清逻辑之后,总结如下:
- 向 DAP 发送 request。其中,
QueueCommands
不会被立即发送,而是等到出现非QueueCommands
的指令,再将它们依次发出。发出时,这些QueueCommands
的 ID 已经改为ExecuteCommands
。 - 从 DAP 获取 response。
- 把 response 发回给电脑。
代码中出现了「等待新元素进入 USBRequestBuffer
」的过程,我们应该分析一下这会在何种时候发生。直观上看,由于 usb 通讯是由 tinyusb 处理的,则从主机发来消息之后,tinyusb 会调用 debugprobe 指定的回调函数,从而把新的 request 加入 USBRequestBuffer
。找到对应函数:
// Manage USBResponseBuffer (request) write and USBRequestBuffer (response) read indices
bool dap_edpt_xfer_cb(uint8_t __unused rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes)
{
const uint8_t ep_dir = tu_edpt_dir(ep_addr);
if(ep_dir == TUSB_DIR_IN)
{
// 主机发起了一次 IN 传输,即 MCU 现在要向主机回复消息
if(xferred_bytes >= 0u && xferred_bytes <= DAP_PACKET_SIZE)
{
// 发出 USBResponseBuffer 的队首
USBResponseBuffer.rptr++;
// This checks that the buffer was not empty in DAP thread,
// which means the next buffer was not queued up for the in endpoint callback
// So, queue up the buffer at the new read index, since we
// expect read to catch up to write at this point.
// It is possible for the read index to be multiple spaces
// behind the write index (if the USB callbacks are lagging behind dap thread),
// so we account for this by only setting wasEmpty to true
// if the next callback will empty the buffer
if(!USBResponseBuffer.wasEmpty)
{
usbd_edpt_xfer(rhport, ep_addr, RD_SLOT_PTR(USBResponseBuffer), (uint16_t) _resp_len);
USBResponseBuffer.wasEmpty = (USBResponseBuffer.rptr + 1) == USBResponseBuffer.wptr;
}
// Wake up DAP thread after processing the callback
vTaskResume(dap_taskhandle);
return true;
}
return false;
} else if(ep_dir == TUSB_DIR_OUT) {
// 主机发起了一次 OUT 传输,即主机现在要通知 MCU 一个 request
if(xferred_bytes >= 0u && xferred_bytes <= DAP_PACKET_SIZE)
{
// Only queue the next buffer in the out callback if the buffer is not full
// If full, we set the wasFull flag, which will be checked by dap thread
if(!buffer_full(&USBRequestBuffer))
{
// 把新的 request 加入 USBRequestBuffer 队列
USBRequestBuffer.wptr++;
usbd_edpt_xfer(rhport, ep_addr, WR_SLOT_PTR(USBRequestBuffer), DAP_PACKET_SIZE);
USBRequestBuffer.wasFull = false;
}
else {
USBRequestBuffer.wasFull = true;
}
// Wake up DAP thread after processing the callback
vTaskResume(dap_taskhandle);
return true;
}
return false;
}
else return false;
}
至此,我们分析完了高层的任务管理逻辑。拼图还剩下最后的一块——debugprobe 使用了 DAP.c
但没有使用 SW_DP.c
,现在需要用 PIO 提供一个替代实现。
0x06 利用 PIO 管理 swd 通讯
在观察 debugprobe 的 swd 通讯相关代码之前,我们先看一看 CMSIS-DAP 中的实现包含了哪些内容。以下是 CMSIS_5/CMSIS/DAP/Firmware/Source/SW_DP.c
代码:
// Generate SWJ Sequence
// count: sequence bit count
// data: pointer to sequence bit data
// return: none
#if ((DAP_SWD != 0) || (DAP_JTAG != 0))
void SWJ_Sequence (uint32_t count, const uint8_t *data) {
uint32_t val;
uint32_t n;
// 用 bit-banging 发送 data
val = 0U;
n = 0U;
while (count--) {
if (n == 0U) {
val = *data++;
n = 8U;
}
if (val & 1U) {
PIN_SWDIO_TMS_SET();
} else {
PIN_SWDIO_TMS_CLR();
}
SW_CLOCK_CYCLE();
val >>= 1;
n--;
}
}
// 要么发出 swdo 内容,要么把信息读进 swdi
#if (DAP_SWD != 0)
void SWD_Sequence (uint32_t info, const uint8_t *swdo, uint8_t *swdi) {
// bit-banging,略
}
// 执行一次完整的 DAP transfer,即发出 Packet Request、读 ACK、处理数据
#if (DAP_SWD != 0)
#define SWD_TransferFunction(speed) /**/ \
static uint8_t SWD_Transfer##speed (uint32_t request, uint32_t *data) { \
// bit-banging,略
}
// 利用上面的宏 SWD_TransferFunction 展开,获得 fast 和 slow 两个传输函数
#undef PIN_DELAY
#define PIN_DELAY() PIN_DELAY_FAST()
SWD_TransferFunction(Fast)
#undef PIN_DELAY
#define PIN_DELAY() PIN_DELAY_SLOW(DAP_Data.clock_delay)
SWD_TransferFunction(Slow)
// swd 传输接口
// SWD Transfer I/O
// request: A[3:2] RnW APnDP
// data: DATA[31:0]
// return: ACK[2:0]
uint8_t SWD_Transfer(uint32_t request, uint32_t *data) {
if (DAP_Data.fast_clock) {
return SWD_TransferFast(request, data);
} else {
return SWD_TransferSlow(request, data);
}
}
参考 CMSIS_5/CMSIS/DAP/Firmware/Include/DAP.h
,这份代码向外暴露的函数是:
void SWJ_Sequence (uint32_t count, const uint8_t *data);
void SWD_Sequence (uint32_t info, const uint8_t *swdo, uint8_t *swdi);
uint8_t SWD_Transfer (uint32_t request, uint32_t *data);
现在,我们来看 src/sw_dp_pio.c
的替代实现。前两个函数如下:
#if ((DAP_SWD != 0) || (DAP_JTAG != 0))
void SWJ_Sequence (uint32_t count, const uint8_t *data) {
uint32_t bits;
uint32_t n;
if (DAP_Data.clock_delay != cached_delay) {
probe_set_swclk_freq(MAKE_KHZ(DAP_Data.clock_delay));
cached_delay = DAP_Data.clock_delay;
}
probe_debug("SWJ sequence count = %d FDB=0x%2x\n", count, data[0]);
n = count;
while (n > 0) {
if (n > 8)
bits = 8;
else
bits = n;
// 调用 probe_write_bits() 发出数据,每次最多 8 bit
probe_write_bits(bits, *data++);
n -= bits;
}
}
#endif
#if (DAP_SWD != 0)
void SWD_Sequence (uint32_t info, const uint8_t *swdo, uint8_t *swdi) {
uint32_t bits;
uint32_t n;
if (DAP_Data.clock_delay != cached_delay) {
probe_set_swclk_freq(MAKE_KHZ(DAP_Data.clock_delay));
cached_delay = DAP_Data.clock_delay;
}
probe_debug("SWD sequence\n");
n = info & SWD_SEQUENCE_CLK;
if (n == 0U) {
n = 64U;
}
bits = n;
if (info & SWD_SEQUENCE_DIN) {
while (n > 0) {
if (n > 8)
bits = 8;
else
bits = n;
// 一次最多读取 8 bit
*swdi++ = probe_read_bits(bits);
n -= bits;
}
} else {
while (n > 0) {
if (n > 8)
bits = 8;
else
bits = n;
// 一次最多发出 8 bit
probe_write_bits(bits, *swdo++);
n -= bits;
}
}
}
#endif
上述代码就是把 bit-banging 改成了调用 probe_read_bits()
和 probe_write_bits()
,从而一次性操作 8 个 bit 而不是 1 bit。接下来是比较复杂的 SWD_Transfer
函数:
#if (DAP_SWD != 0)
// SWD Transfer I/O
// request: A[3:2] RnW APnDP
// data: DATA[31:0]
// return: ACK[2:0]
uint8_t SWD_Transfer (uint32_t request, uint32_t *data) {
uint8_t prq = 0;
uint8_t ack;
uint8_t bit;
uint32_t val = 0;
uint32_t parity = 0;
uint32_t n;
if (DAP_Data.clock_delay != cached_delay) {
probe_set_swclk_freq(MAKE_KHZ(DAP_Data.clock_delay));
cached_delay = DAP_Data.clock_delay;
}
probe_debug("SWD_transfer\n");
// 组装 packet request 字节
/* Generate the request packet */
prq |= (1 << 0); /* Start Bit */
for (n = 1; n < 5; n++) {
bit = (request >> (n - 1)) & 0x1;
prq |= bit << n;
parity += bit;
}
prq |= (parity & 0x1) << 5; /* Parity Bit */
prq |= (0 << 6); /* Stop Bit */
prq |= (1 << 7); /* Park bit */
// 发出 packet request 字节
probe_write_bits(8, prq);
// 制造几个时钟周期,即 ADIv5 所要求的 trn,并读取 ACK 信息
/* Turnaround (ignore read bits) */
ack = probe_read_bits(DAP_Data.swd_conf.turnaround + 3);
ack >>= DAP_Data.swd_conf.turnaround;
// 若 ACK 正常
if (ack == DAP_TRANSFER_OK) {
/* Data transfer phase */
if (request & DAP_TRANSFER_RnW) {
// 本次操作的 RnW flag 是 1,即 read
/* Read RDATA[0:31] - note probe_read shifts to LSBs */
// 读出 32 bit 数据
val = probe_read_bits(32);
// 读出 1 bit 奇偶校验位,并验证
bit = probe_read_bits(1);
parity = __builtin_popcount(val);
if ((parity ^ bit) & 1U) {
/* Parity error */
ack = DAP_TRANSFER_ERROR;
}
if (data)
*data = val;
probe_debug("Read %02x ack %02x 0x%08x parity %01x\n",
prq, ack, val, bit);
// 制造 trn
/* Turnaround for line idle */
probe_hiz_clocks(DAP_Data.swd_conf.turnaround);
} else {
// 本次操作的 RnW flag 是 0,即 write
// 制造 trn
/* Turnaround for write */
probe_hiz_clocks(DAP_Data.swd_conf.turnaround);
/* Write WDATA[0:31] */
val = *data;
// 写入数据
probe_write_bits(32, val);
// 写入奇偶校验位
parity = __builtin_popcount(val);
/* Write Parity Bit */
probe_write_bits(1, parity & 0x1);
probe_debug("write %02x ack %02x 0x%08x parity %01x\n",
prq, ack, val, parity);
}
// 记录时间戳
/* Capture Timestamp */
if (request & DAP_TRANSFER_TIMESTAMP) {
DAP_Data.timestamp = time_us_32();
}
// 连续发送若干个 0
/* Idle cycles - drive 0 for N clocks */
if (DAP_Data.transfer.idle_cycles) {
for (n = DAP_Data.transfer.idle_cycles; n; ) {
if (n > 256) {
probe_write_bits(256, 0);
n -= 256;
} else {
probe_write_bits(n, 0);
n -= n;
}
}
}
return ((uint8_t)ack);
}
// 以下是 ACK 不正常情况下的错误处理流程
// 若 ACK 为 WAIT 或 FAULT
if ((ack == DAP_TRANSFER_WAIT) || (ack == DAP_TRANSFER_FAULT)) {
if (DAP_Data.swd_conf.data_phase && ((request & DAP_TRANSFER_RnW) != 0U)) {
// 若操作为 read,则读 33 个垃圾 bit
/* Dummy Read RDATA[0:31] + Parity */
probe_read_bits(33);
}
// 制造 trn
probe_hiz_clocks(DAP_Data.swd_conf.turnaround);
if (DAP_Data.swd_conf.data_phase && ((request & DAP_TRANSFER_RnW) == 0U)) {
// 若操作为 read,则发 33 个垃圾 bit
/* Dummy Write WDATA[0:31] + Parity */
probe_write_bits(32, 0);
probe_write_bits(1, 0);
}
return ((uint8_t)ack);
}
// ACK 信号非预期
/* Protocol error */
n = DAP_Data.swd_conf.turnaround + 32U + 1U;
/* Back off data phase */
probe_read_bits(n);
return ((uint8_t)ack);
}
#endif /* (DAP_SWD != 0) */
这段代码逻辑也非常清晰。总结一下 PIO 模块要提供的接口:
probe_read_bits
:读取若干个 bitprobe_write_bits
:写入若干个 bitprobe_hiz_clocks
:制造若干个时钟周期probe_set_swclk_freq
:改变时钟频率
现在,我们开始阅读 src/probe.pio
。
.program probe
.side_set 1 opt ; 这里的 opt 表示 side-set 是可选功能,无需在每条指令之后都指定 side-set
public write_cmd:
public turnaround_cmd: ; Alias of write, used for probe_oen.pio
; 拉取待发送的数据
pull
write_bitloop:
; 设置 SWDIO,拉低 SWCLK,延时一个周期
out pins, 1 [1] side 0x0 ; Data is output by host on negedge
; 拉高 SWCLK
jmp x-- write_bitloop [1] side 0x1 ; ...and captured by target on posedge
; Fall through to next command
.wrap_target
public get_next_cmd:
; 拉取信息
pull side 0x0 ; SWCLK is initially low
; 把 8 个 bit 放进 x,作为下一次传输的 bit count
out x, 8 ; Get bit count
; 1 个 bit 表示 SWDIO 方向
out pindirs, 1 ; Set SWDIO direction
; 直接设置 pc 寄存器,强制跳转
out pc, 5 ; Go to command routine
read_bitloop:
nop ; Additional delay on taken loop branch
public read_cmd:
; 从 SWDIO 读入一个 bit,拉高 SWCLK
in pins, 1 [1] side 0x1 ; Data is captured by host on posedge
; 拉低 SWCLK
jmp x-- read_bitloop side 0x0
push
.wrap ; Wrap to next command
这份 PIO 代码写得相当 hack(当然了,大部分 PIO 代码都很 hack,毕竟指令集有限,指令数量也被限制为 32 条)。PIO 的主要入口是 get_next_cmd
,MCU 向 tx fifo 中写入控制信息(bit count、方向、要跳转到的位置),然后 PIO 会去执行读取或写入逻辑;接下来 MCU 把具体要发送的数据写入 tx fifo,或者从 rx fifo 读取信息。综上,一次读取或写入操作,要访问两次 PIO fifo,第一次访问是发送控制信息,第二次是真正的数据传输。
值得注意,out pc, 5
是绝对地址跳转,因此 MCU 需要计算出跳转位置,组装进控制信息中。我们来看看是如何实现的:
void probe_write_bits(uint bit_count, uint32_t data_byte) {
DEBUG_PINS_SET(probe_timing, DBG_PIN_WRITE);
pio_sm_put_blocking(pio0, PROBE_SM, fmt_probe_command(bit_count, true, CMD_WRITE));
pio_sm_put_blocking(pio0, PROBE_SM, data_byte);
probe_dump("Write %d bits 0x%x\n", bit_count, data_byte);
// Return immediately so we can cue up the next command whilst this one runs
DEBUG_PINS_CLR(probe_timing, DBG_PIN_WRITE);
}
显然 fmt_probe_command
即为组装控制信息的过程。跟进:
static inline uint32_t fmt_probe_command(uint bit_count, bool out_en, probe_pio_command_t cmd) {
uint cmd_addr =
cmd == CMD_WRITE ? probe.offset + probe_offset_write_cmd :
cmd == CMD_SKIP ? probe.offset + probe_offset_get_next_cmd :
cmd == CMD_TURNAROUND ? probe.offset + probe_offset_turnaround_cmd :
probe.offset + probe_offset_read_cmd;
return ((bit_count - 1) & 0xff) | ((uint)out_en << 8) | (cmd_addr << 9);
}
其中的 probe_offset_*
都是在 .pio
编译成 .h
文件之后产生的。因此可以引用这些 offset,而无需在代码中硬编码 magic number,那样非常不便于维护。
至此,我们已经看完了 debugprobe 的所有 swd 相关代码。由于大量复用了 CMSIS-DAP 和 tinyusb 库,debugprobe 要做的工作事实上并不多。这也为自行移植 CMSIS-DAP 提供了参考:
- 直接使用 CMSIS-DAP 的
DAP.c
、DAP_vendor.c
,它们似乎是平台无关的。若打算用 bit-banging 实现 swd 协议控制,则使用SW_DP.c
,否则自行实现。 - CMSIS-DAP 中没有现成的 usb 代码可抄,需要自行实现 usb 相关功能。我们可以考虑复用 debugprobe 的相关代码,这部分代码看起来不太依赖于 RP2040,而且几乎所有常用平台都有 tinyusb 支持。
- FreeRTOS 应该是可选项。手动轮询或者使用轻量级的任务管理器,大概也能支撑 swd 和 cdc 功能。若想要移植到资源极为受限的环境中,则可以考虑去掉 FreeRTOS。