Pangram verdict · v3.3
We believe that this entire text is human-written.
AI likelihood · overall
HumanArticle text · 1,462 words · 1 segments analyzed
In my last post, I wrote about debugging the Nordic Semiconductor nRF54LM20 when a peripheral, specifically the Key Management Unit (KMU), writes directly to memory while the CPU is halted. Or I should really say that I wrote about debugging the application core (Cortex-M33), as the nRF54LM20 also has a RISC-V coprocessor (VPR) referred to as the Fast Lightweight Peripheral Processor (FLPR). Around the time that Nordic announced the VPR core, I wrote two posts; one about its architecture and the other about how communication works between it and an application core. In the latter VPR post, I attached to the application core with GDB and JLinkGDBServer in the same manner that I did in my recent KMU exploration post in order to step through the communication protocol. This strategy was appropriate for the operations described in the respective posts, but in some cases it may be desirable to connect to the VPR core for debugging. Typically, silicon vendors will provide tooling, or work with open source projects, to make it easy to designate the core you want to connect to at a higher level of abstraction. For example, Nordic products have robust suport in the Zephyr RTOS ecosystem, and Zephyr’s meta CLI, west, can be used to build (west build) and flash (west flash) firmware on all chips. west supports building and flashing multiple images in one operation using System Build (Sysbuild). Each build system managed by Sysbuild is referred to as a domain, and the --domain flag can be used to perform the specified operation for only the specified domain. While we typically want to target all domains when building and flashing, west debug is a great example of how the --domain flag can be useful. For example, if we were building the Zephyr blinky sample for the nRF54LM20, using the FLPR for GPIO control via Nordic’s High Performance Framework (HPF) (stay tuned for a future post on HPF), we would use the following command to build the blinky application for the application core and the GPIO application for the FLPR core. west build -p -b nrf54lm20dk/nrf54lm20b/cpuapp nrf/samples/zephyr/basic/blinky -- -DSB_CONFIG_HPF=y -DSB_CONFIG_HPF_GPIO=y -DSB_CONFIG_HPF_GPIO_BACKEND_ICMSG=y -DEXTRA_DTC_OVERLAY_FILE="./boards/nrf54lm20dk_nrf54lm20b_cpuapp_hpf_gpio.overlay" In the resulting build/ directory, we would see a top-level domains.yaml file with the following contents. default: blinky build_dir: <local-path>/build domains: - name: blinky build_dir: <local-application-path>/build/blinky - name: hpf_gpio build_dir: <local-application-path>/build/hpf_gpio flash_order: - blinky - hpf_gpio Issuing a west flash command would flash both of the images onto the device, but because we can only debug one core at a time, west debug with no flags would target the default domain (blinky) and connect to the application core. Specifying hpf_gpio would instead connect to the FLPR. west debug --domain hpf_gpio If you prefer a graphical UX, you could leverage this same functionality via Nordic’s VSCode tooling, as described in this post by my Nordic coworker Sebastian Viviani. Behind the scenes, Zephyr includes board support files that help west know how to setup the debugging connection. If we were to look in each specific domain’s build/<domain>/zephyr/ directory, we would find a runners.yaml file. For example, the hpf_gpio domain in the previous build includes the following contents. # Available runners configured by board.cmake. runners: - nrfutil - jlink # Default flash runner if --runner is not given. flash-runner: nrfutil # Default debug runner if --runner is not given. debug-runner: jlink # Common runner configuration values. config: board_dir: <local-application-path>/zephyr/boards/nordic/nrf54lm20dk # Build outputs: elf_file: zephyr.elf hex_file: zephyr.hex bin_file: zephyr.bin # Host tools: gdb: <local-zephyr-sdk-path>/zephyr-sdk-1.0.1/gnu/riscv64-zephyr-elf/bin/riscv64-zephyr-elf-gdb openocd: <local-zephyr-sdk-path>/zephyr-sdk-1.0.1/hosttools/usr/bin/openocd openocd_search: - <local-zephyr-sdk-path>/zephyr-sdk-1.0.1/hosttools/opt/openocd/share/openocd/scripts # Runner specific arguments args: nrfutil: [] jlink: - --dt-flash=y - --device=nRF54LM20A_RV32 - --speed=4000 If we were to look at the nRF54LM20 DK board support files in the Zephyr source tree, we would find the following CMake snippet. boards/nordic/nrf54lm20dk/board.cmake if(CONFIG_SOC_NRF54LM20A_CPUAPP OR CONFIG_SOC_NRF54LM20B_CPUAPP) board_runner_args(jlink "--device=nRF54LM20A_M33" "--speed=4000") elseif(CONFIG_SOC_NRF54LM20A_CPUFLPR OR CONFIG_SOC_NRF54LM20B_CPUFLPR) board_runner_args(jlink "--device=nRF54LM20A_RV32" "--speed=4000") endif() if(CONFIG_TFM_FLASH_MERGED_BINARY) set_property(TARGET runners_yaml_props_target PROPERTY hex_file tfm_merged.hex) endif() include(${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake) include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) The args.jlink flags at the bottom of the runners.yaml file are sourced from these board_runner_args() CMake macros, and they are passed to JLinkGDBServer when invoking west debug for the corresponding domain. Because J-Link has native multi-core support for the nRF54L series, the --device flag can be used to tell JLinkGDBServer to consult its internal database for information on how to connect to each of the respective cores. If the debugger is not already knowledgeable of the SoC architecture, it may be necessary to provide more detailed information. For example, see the nRF54H20 J-Link script for connecting to the application core. In fact, if we were to connect to the FLPR core on the nRF54LM20 using JLinkExe -device nRF54LM20A_RV32 directly, we would see the following sequence of lines in the initial output. Found SW-DP with ID 0x6BA02477 RISC-V behind DAP detected DPIDR: 0x6BA02477 CoreSight SoC-400 or earlier AP[1] (AHB-AP) specified by user as debug AP. AP map scan skipped. AP map: AP[0]: AHB-AP, APAddr = 0x00000000 AP[1]: AHB-AP, APAddr = 0x01000000 AP[2]: MEM-AP, APAddr = 0x02000000 Core base addr: 0x5004C400 (user configured) In my nRF54LM20 KMU post, I included the following diagram from the nRF54LM20 datasheet that describes the debug architecture, including the three Access Ports (APs) that are available via the Debug Access Port (DAP). The datasheet also includes a table describing the functionality of each access port. AP ID Type Description 0 AHB-AP CM33 access port 1 AHB-AP AUX access port 2 CTRL-AP Control access port As observed in the JLinkExe output, it opts to forego scanning the access port map in favor of connecting directly to the second AHB-AP (AP[1]), which it knows to be the access port for the RISC-V FLPR core. If instead invoking JLinkExe -device nRF54LM20A_M33 to debug the Cortex-M33 application core, the following output would be observed. Found SW-DP with ID 0x6BA02477 DPIDR: 0x6BA02477 CoreSight SoC-400 or earlier AP map detection skipped. Manually configured AP map found. AP[0]: AHB-AP (IDR: Not set, ADDR: 0x00000000) AP[1]: APB-AP (IDR: Not set, ADDR: 0x00000000) AP[2]: MEM-AP (IDR: Not set, ADDR: 0x00000000) Iterating through AP map to find AHB-AP to use AP[0]: Core found AP[0]: AHB-AP ROM base: 0xE00FE000 CPUID register: 0x411FD210. Implementer code: 0x41 (ARM) In this case the J-Link once again reports that the AP map is known, but instead of selecting an access port immediately, it iterates through the map looking for the Cortex-M33 core. While the underlying logic is hidden in the closed source JLinkArm.dll library that underlies most J-Link tooling, it is likely that the default behavior for Arm cores, even those on devices known to the J-Link, is to leverage the information offered by the Arm Debug Interface (ADI) to discover the core and its capabilities. The ADI specifies the architecture of a Debug Access Port (DAP), which consists of a Debug Port (DP) and one or more Access Ports (APs). There are multiple types of debug ports: JTAG (JTAG-DP), Serial Wire (SW-DP), and Serial Wire / JTAG (SWJ-DP). Likewise, there are multiple types of access ports, with the Advanced High-performance Bus Access Port (AHB-AP) itself being a type of Memory Access Port (MEM-AP), and the Control Access Port (CTRL-AP) being a custom AP implementation by Nordic. There are a minimum set of registers for DPs and APs respectively, which must be provided by any implementation. The following diagram outlines the interaction between the DP and MEM-APs (and thus AHB-APs). This diagram gives a hint to how a debug probe, such as the J-Link, may go about discovering the access ports that are available on a given DAP, even if the SoC is not already known. The first relevant register is the DP’s AP Select (SELECT) register (highlighted in the DP registers above). It can be used to select an access port (APSEL) and the register bank (APBANKSEL) within the access port. APSEL RES0 APBANKSEL DPBANKSEL XXXXXXXX XXXXXXXXXXXXXXXX XXXX XXXX [31:24] [23:8] [7:4] [3:0] If the value in the APSEL bits does not correspond to an access port on the DAP, the ADI defines the following behavior. If there is no AP with the ID APSEL, all AP transactions return zero on reads and are ignored on writes. After successfully selecting an access port, the AP’s identification register (IDR) (highlighted in the MEM-AP registers above), which is the only register that must be implemented by all APs, can be used to understand the attributes of the AP. It is always the last register in the AP register space, located at 0xFC. Revision Designer Class RES0 Variant Type XXXX XXXXXXXXXXX XXXX XXXXX XXXX XXXX [31:28] [27:17] [16:13] [12:8] [7:4] [3:0] For the most part, debuggers hide the specific reads and writes to DP and AP registers behind higher levels of abstractions, such as reading a memory address or advancing the program counter of a CPU. However, J-Link does support lower level commands to read and write directly to DPs (ReadDP / WriteDP) and APs