Diving into JTAG protocol. Part 2 — Debugging.
As noted in my previous article Diving into JTAG protocol. Part 1 — Overview, JTAG was initially developed for testing integrated circuits and printed circuit boards. However, its potential for debugging was realized over time, and now JTAG has become the standard protocol for microcontroller debugging. Many Firmware and Embedded engineers first encountered it in this particular context.
However, there’s a catch: while the use of JTAG in testing is fairly standardized when it comes to debugging, each processor architecture has its unique nuances. With that in mind, this article will focus on debugging using JTAG on the ARM Cortex-M architecture, specifically with the STM32F407VG microcontroller.
For successful debugging, it’s not enough to simply know the basics of JTAG. It’s also essential to understand the workings of the ARM Debug Access Port, a key component that allows access to the controller’s internal registers and memory. I’ve detailed this module in the article A Deep Dive into the ARM Debug Access Port. With that groundwork laid, let’s dive into the world of debugging through JTAG.
JTAG Access to the STM32F407VG Controller
Before connecting to the STM32F407VG controller via the JTAG protocol, we need to determine the following: the length of the IR
register and the number of TAP controllers connected to the JTAG chain. The official documentation, specifically the RM0090: Reference manual, can assist us in this.
According to this document, the STM32F407VG has two TAP controllers: the STM32 Boundary Scan and the CoreSight JTAG-DP. The first in the chain is the Boundary Scan TAP with a IR
register size of 5 bits. Following it is the CoreSight JTAG-DP TAP with a IR
register size of 4 bits.
This knowledge is crucial for us because it determines how many bits to send in the JTAG chain and in what sequence to do so when accessing different TAP controllers. According to this information, we can connect to the CoreSight JTAG-DP TAP controller and attempt to read its IDCODE
.
Read of ID CODE
The sequence of bits for reading the IDCODE
of the CoreSight JTAG-DP TAP controller will look as follows:
Let’s analyze this sequence in detail:
- So the first step is to reset the TAP State Machine to the initial state
Test-Logic-Reset
in order to be sure that we are in a known state. To achieve this, simply hold theTMS
in logic 1 for 5 clock cycles ofTCK
. The pink bit sequence is responsible for this step:
- Next, we need to load the
IDCODE
command code into theIR
register, and to do this we put the TAP State Machine into theShift-IR
state. To do this, feed theTMS
line with the sequence [0, 1, 1, 0, 0]. A sequence of blue bits is responsible for this step:
As you can see on the TDO
line in the last measure there is a one. Let's find out where it came from. The thing is that going through the Capture-IR
state, the TAP controller loads into the shift register a value of 0b0001
for CoreSight JTAG-DP TAP and 0b00001
for Boundary Scan TAP which starts to move along the TDO
line from this measure and LSB bits forward. So, over the next 9 clock cycles (including this one) we will get a TDO
sequence of 0b100010000
.
- The next step is to directly load the
IDCODE
command code into the CoreSight JTAG-DP TAPIR
register and theBYPASS
command code into the Boundary Scan TAPIR
register. I think the situation for the CoreSight JTAG-DP TAP is clear, but why we load theBYPASS
command into the Boundary Scan TAP should be explained. You remember that there are two TAP controllers in our JTAG chain connected in series and we can not access the CoreSight JTAG-DP TAP registers without having to write something to the Boundary Scan TAP, and the size of theIR
register for Boundary Scan is 5 bits, and theDR
register is 32 bits, so to write a new data to theDR
register of the CoreSight JTAG-DP TAP we need to write a 32 + 32 = 64 bits sequence of bits to the JTAG circuit. This is not very convenient. TheBYPASS
command will help us here. When this command is in the registerIR
the data register of the selected TAP becomes 1 bit long. So now we need to write a sequence of 33 bits Instead of 64. TheIDCODE
code is0b1110
, and theBYPASS
command code is0b11111
total we need to push 9 bits:0b011111111
. TheTMS
input must be held at logical 0 during the 8 LSB input in order for the machine to remain inShift-IR
state. The MSB bit of the instruction is pushed in when there is an exit from this state by settingTMS
to logical 1. The red bit sequence is responsible for this step:
The TDO
wire continues to output the sequence 0b100010000
which was loaded in the Capture-IR
state.
After this step, we are in the Exit1-IR
state.
- Great, the necessary commands are set in the
IR
registers and now we need to return to theRun Test/Idle
state for further work. At this point, thanks to the last 1 on theTMS
line we are in theExit-1-IR
state and hence according to the state diagram we need to set the following bit sequence on theTMS
line toRun Test/Idle
: [1, 0]. The orange bit sequence is responsible for this step:
- After setting the
IDCODE
command in theIR
register we need to readIDCODE
from theDR
register. To do this the first step is to go to theShift-DR
state. To do this we set the following bit sequence [1, 0, 0] on theTMS
pin considering that we are in theRun Test/Idle
state at the moment. The yellow bit sequence is responsible for this step:
On the TDO
line we have the sequence: [0, 0, 1]. The last one is already the LSB of our chip ID. The fact is that passing through the Capture-DR
state our chip ID is loaded into the shift register and begins to move outward in the next clock cycles.
- In the
Shift-DR
state the remaining bits of the chip ID are unloaded. To remain in theShift-DR
state theTMS
input must be held at logic 0 during the input of all bits except the MSB bit. The MSB data is pulled out when this state is exited by settingTMS
to a logical 1. The green bit sequence is responsible for this step:
So we got a chip ID of the following kind:
11101110001000000000010111010010
Let’s check this code according to the documentation:c
Something is wrong here. What is wrong is that the LSB is in the usual position in the figure, but we took it in the inverted order. Let’s correct this situation:
01001011101000000000010001110111
- And the final step is to go back to the
Run Test/Idle
state:
Let’s demonstrate the whole process in the following gif file, there are no gifs in this part of the article, we need to fix that :).
Interaction with memory
Great, the JTAG connection works and we’ve read the IDCODE
. Let's now proceed and try interacting with the microcontroller's memory by writing a value to it and reading it back. After all, reading/writing memory is one of the primary operations when debugging a microcontroller.
To interact with the microcontroller’s memory, we need to understand the mechanism behind it. The JTAG protocol itself doesn’t provide any specifics about this. Access to the internal resources of ARM architecture controllers (such as memory) is facilitated by a special module called DAP (Debug Access Port). I’ve detailed this module extensively in this article A Deep Dive into the ARM Debug Access Port. Unfortunately, without this knowledge, advancing further might be challenging. Therefore, I highly recommend reading that article or the official documentation IHI0031 — ARM Debug Interface.
The first step for any access operation to the internal resources of an ARM architecture microcontroller with a DAP module is to request clocking and power for the debug module. This is done by setting the CSYSPWRUPREQ
and CDBGPWRUPREQ
bits in the DP CTRL/STAT
register.
After this initial setup, we can begin our memory operations.
In the following two sections, I will be heavily referencing the “Practical Part” from the article A Deep Dive into the ARM Debug Access Port. This section describes the algorithm for reading/writing memory through interaction with the DAP module. Additionally, I will supplement this algorithm with a sequence of bits, illustrating how this will appear from the perspective of the JTAG protocol.
Writing a variable to memory
Alright, let’s write the value 0xAA55AA55
to the address 0x20000000
:
- Write
0b1010
(theDPACC
register code) into theIR
register. - Some settings in the
AP
registerCSW
need to be configured. To access this register, we need to select the correspondingAP
and register bank in theDP SELECT
register. Write to theDR
register:
- DATA[31:0] =
0x00
- APSEL[31:24] =
0x00
- APBANKSEL[7:4] =
0x00
- A[3:2] =
0b10
(address ofSELECT
register) - RnW =
0b0
3. Next, we need to write the CSW
register. Since this is an AP
register, we need to use the APACC
register to access it:
- Write
0b1011
(theAPACC
register code) into theIR
register.
4. Create the data for the configuration: set the data size for writing to 32 bits (Size[0:2] = 0b010
), disable the auto-increment function of the address (AddrInc[5:4] = 0b00
). Write to DR
:
- DATA[31:0] =
0x00000002
- A[3:2] =
0b00
(address ofCSW
register) - RnW =
0b0
5. Then we need to set the memory cell address to which we want to write the data. This is done through the AP
register TAR
. And since this register belongs to the same AP
as CSW
and is in the same bank, we can omit the reference to the DP SELECT
register and immediately write the address value. Write to DR
:
- DATA[31:0] =
0x20000000
- A[3:2] =
0b01
(address ofTAR
register) - RnW =
0b0
6. The last step is to actually write the data. To do this, we need to write them to the DRW
register. Write to DR
:
- DATA[31:0] =
0xAA55AA55
- A[3:2] =
0b11
(address ofDRW
register) - RnW =
0b0
Reading a variable from memory
Let’s try to read the value from the memory cell at the address 0x20000000
- Write
0b1010
(theDPACC
register code) into theIR
register. - We need to configure some settings in the
CSW
register of theAP
. To access this register, we need to select the correspondingAP
and register bank in theDP SELECT
register. Using the data from the table, we form 35 bits of data that we write to theDR
register:
- DATA[31:0] =
0x00
- APSEL[31:24] =
0x00
- APBANKSEL[7:4] =
0x00
- A[3:2] =
0b10
(address ofSELECT
register) - RnW =
0b0
3. Next, we need to write directly to the CSW
register. Since this is an AP
register, we need to use the APACC
register to access it:
- Write
0b1011
(theAPACC
register code) into theIR
register.
4. We create the data for configuration: we set the data size for writing to 32 bits (Size[0:2] = 0b010
) and disable the auto-increment address function (AddrInc[5:4] = 0b00
). Write to DR
:
- DATA[31:0] =
0x00000002
- A[3:2] =
0b00
(address ofCSW
register) - RnW =
0b0
5. Next, we need to set the memory cell address from which we will read the data. This is done through the TAR
register of the AP
. And since this register belongs to the same AP
as CSW
and is in the same bank, we can skip the DP SELECT
register access and immediately write the address value:
- DATA[31:0] =
0x20000000
- A[3:2] =
0b01
(address ofTAR
register) - RnW =
0b0
6. We form a read request. To do this, we write to the DRW
register:
- DATA[31:0] =
0x00
- A[3:2] =
0b11
(address ofDRW
register) - RnW =
0b1
7. Read the DRW
register by reading 35 bits from the DR
register.
Interacting with the Processor Core
Well, we’ve explored how the debugger interacts with the microcontroller’s memory. Now, let’s delve into how the debugger can halt program execution, step through code, read the processor’s registers, and so on. In the case of the STM32F407VG microcontroller, this is achieved through special registers:
As detailed in section C1.6 Debug system registers of the ARMv7-M Architecture Reference Manual, these registers play a pivotal role in debugging.
What’s crucial for us now is understanding that by setting the C_DEBUGEN
and C_HALT
bits of the DHCSR
register, we can halt the execution of the program. Similarly, by setting the C_DEBUGEN
and C_STEP
bits, we can execute the program step by step.
Access to these registers via the DAP module is essentially the same as accessing memory. Instead of providing a memory address, you provide the address of the desired register. For instance, in the case of the DHCSR
register, the address is 0xE000EDF0
. If you let me, I won't delve into an example with the bit sequence since it would be identical to the example from the chapter on memory interaction.
Closing
Well, we’ve briefly explored how JTAG is used in microcontroller debugging. This topic is big, and it’s challenging to cover it fully within a single article. In this piece, we focused on invasive debugging, while non-invasive debugging wasn’t even mentioned. However, that wasn’t the goal when writing this article. For those who want to delve deeper into this topic, I recommend studying the links list listed below. As for us, we’ll move on, and in the next article, we’ll tackle the subject of testing printed circuit boards using the JTAG protocol.
Thank you for your time! :)
Next part: Diving into JTAG. Part 3 — Boundary Scan
Links
- RM0090: Reference manual
- ARMv7-M Architecture Reference Manual
- Cortex-M4.Technical Reference Manual
- ARM CoreSight Architecture Specification
- DEBUGGING WITH JTAG
- Step-through debugging with no debugger on Cortex-M
- How do breakpoints even work?
- A Deep Dive into ARM Cortex-M Debug Interfaces
- Profiling Firmware on Cortex-M
- How do breakpoints even work?
- Step-through debugging with no debugger on Cortex-M