Doing something useful with SWD
In part 4 we got as far as reading in the CPU identification, which is of no real use; in this part we’ll actually read some of the CPU internals, but first we need to understand how SWD accesses are controlled.
DAP: DP and AP
You may think that we just need to feed a 32-bit address into the SWD port, and get a value back, but the reality is much more complicated. The SWD clock & data lines are connected to the CPU Debug Port (SW-DP), which has its own address space. Read/write accesses to the CPU memory aren’t controlled by the DP; you have to go through the Access Port (AP), which also has a separate memory space, and is bank-switched.
DP
We’ve already accessed the Debug Port ID code register, the other read-write registers are:
# Debug Port (SWD-DP) registers # See ARM DDI 0314H "Coresight Components Technical Reference Manual" DPORT_IDCODE = 0x0 # ID Code / abort DPORT_ABORT = 0x0 DPORT_CTRL = 0x4 # Control / status DPORT_STATUS = 0x4 DPORT_SELECT = 0x8 # Select DPORT_RDBUFF = 0xc # Read buffer
These registers control various aspects of the debug interface, for example the ‘abort’ register is used to reset the internal logic after an error, and the ‘control’ register is used to power up the peripherals needed for debugging.
The ‘select’ register has bitfields to control AP and DP bank-switching; it is convenient to define this using CTYPES:
from ctypes import Structure, Union, c_uint # AHB-AP Select Register class DP_SELECT_REG(Structure): _fields_ = [("DPBANKSEL", c_uint, 4), ("APBANKSEL", c_uint, 4), ("Reserved", c_uint, 16), ("APSEL", c_uint, 8)] class DP_SELECT(Union): _fields_ = [("reg", DP_SELECT_REG), ("value", c_uint)] dp_select = DP_SELECT()
The union allows us to specify the whole 32–bit register when doing SPI read & write cycles, but still access individual bitfields within it.
AP
The AP registers are:
# Access Port (AHB-AP) registers, high nybble is bank number # See ARM DDI 0337E: Cortex M3 Technical Reference Manual page 11-38 APORT_CSW = 0x0 # Control status word APORT_TAR = 0x4 # Transfer address APORT_DRW = 0xc # Data read/write APORT_BANK0 = 0x10 # Banked registers APORT_BANK1 = 0x14 APORT_BANK2 = 0x18 APORT_BANK3 = 0x1c APORT_DEBUG_ROM_ADDR= 0xf8 # Address of debug ROM APORT_IDENT = 0xfc # AP identification
The Control Status Word register controls various aspects of the CPU memory accesses, for example the data size, and auto-increment for reading blocks of memory:
# AHB-AP Control Status Word Register class AP_CSW_REG(Structure): _fields_ = [("Size", c_uint, 3), ("Res1", c_uint, 1), ("AddrInc", c_uint, 2), ("DbgStatus", c_uint, 1), ("TransInProg", c_uint, 1), ("MODE", c_uint, 4), ("Res2", c_uint, 13), ("HProt1", c_uint, 1), ("Res3", c_uint, 3), ("MasterType", c_uint, 1), ("Res4", c_uint, 2)] class AP_CSW(Union): _fields_ = [("reg", AP_CSW_REG), ("value", c_uint)] ap_csw = AP_CSW()
Because the AP can be accessing main CPU memory, it has two time-dependencies:
- the AP may return a ‘wait’ indication in the status field so the transaction has time to go though
- the data isn’t returned immediately; first you have to do a dummy read cycle, then a second read cycle that actually returns the data
So to set up a CPU memory read cycle, we need to configure the AP (including its bank-switching) then set a transfer address
# Configure AP memory accesses def ap_config(d, inc, size): dp_select.reg.APBANKSEL = 0 # Zero bank swd.swd_wr(d, swd.SWD_DP, DPORT_SELECT, dp_select.value) ap_csw.reg.MasterType = 1 ap_csw.reg.HProt1 = 1 # Enable incrementing, set access size ap_csw.reg.AddrInc = 1 if inc else 0 ap_csw.reg.Size = 0 if size==8 else 1 if size==16 else 2 return swd.swd_wr(d, swd.SWD_AP, APORT_CSW, ap_csw.value) # Set AP memory address def ap_addr(d, addr): swd.swd_wr(d, swd.SWD_AP, APORT_TAR, addr) swd.swd_idle_bytes(d, 2) # Idle to avoid 'wait' response # Do an immediate read of a 32-bit CPU memory location def cpu_mem_read32(d, addr): ap_addr(d, addr) # Address to read swd.swd_rd(d, swd.SWD_AP, APORT_DRW) # Dummy read cycle r = swd.swd_rd(d, swd.SWD_AP, APORT_DRW) # Read data return r.data.value if r.ack.value==swd.SWD_ACK_OK else None
There are 2 idle (null) bytes after the target memory address is set. These give the AP time to process the address value before it is used; if omitted, the AP gives an ack value of 2 (‘wait’) on the next transaction.
User interface
The console-based user interface is minimal; it just allows you to specify the STM32F103 GPIO port or memory address to be accessed, e.g.
python ftdi_py_part5.py gpiob DP ident: ack 1, value 1BA01477h Powerup: ack 1, value F0000040h AP ident: ack 1, value 14770011h 40010C00: GPIOB CRL=44488433 CRH=33333344 IDR=00007FDA ODR=00007C1A python ftdi_py_part5.py 0 DP ident: ack 1, value 1BA01477h Powerup: ack 1, value F0000040h AP ident: ack 1, value 14770011h 00000000: 20005000
If a gpio port is specified, four of its register values are printed out (CRL, CRH, IDR, ODR); the other diagnostic values can be useful if the access fails. Further help is available by setting the VERBOSE flag at the top of the part 4 file, which enables a printout of all the SWD cycles:
# Command line with invalid address python ftdi_py_part5.py 800000 Rd 0 IDCODE 1BA01477 Ack 1 DP ident: ack 1, value 1BA01477h Wr 0 ABORT 0000001E Ack 1 Wr 4 CTRL 50000000 Ack 1 Rd 4 STATUS F0000000 Ack 1 Powerup: ack 1, value F0000000h Wr 8 SELECT 000000F0 Ack 1 Rd C DRW/BD3 00000000 Ack 1 Rd C DRW/BD3 14770011 Ack 1 AP ident: ack 1, value 14770011h Wr 8 SELECT 00000000 Ack 1 Wr 0 CSW/BD0 22000012 Ack 1 Wr 4 TAR/BD1 00800000 Ack 1 Rd C DRW/BD3 14770011 Ack 1 Rd C DRW/BD3 00000000 Ack 4 00800000: ?
Source code
Here is the final batch of source-code. I’ve only tested the it with STM32F1 CPUs, so if you are trying to communicate with something else, there is a strong possibility you’ll need to make some changes. Points to bear in mind:
- Check the the CPU supports SWD, and the connections it uses.
- Check for any special power-up requirements, e.g. sending the reset sequence multiple times, or setting registers to enable debugging mode
- Check that the SWD clock and signal lines are toggling OK.
- Watch out for acknowledgement values 2 and 4, indicating a problem.
- Once an error occurs, it will persist over successive cycles until reset by writing to the ABORT register.
- Good luck!
# Python FTDI SWD CPU memory read from iosoft.blog # Compatible with Python 2.7 or 3.x # # v0.01 JPB 8/12/18 import sys, ftdi_py_part3 as ft, ftdi_py_part4 as swd from ctypes import Structure, Union, c_uint # STM32F1 address values for testing # Address of GPIO Ports A - E on STM32F1 ports = {"GPIOA":0x40010800, "GPIOB":0x40010C00, "GPIOC":0x40011000, "GPIOD":0x40011400, "GPIOE":0x40011800} # GPIO registers at offsets 0, 4, 8, 12 gpio_regs = ("CRL", "CRH", "IDR", "ODR") # Debug Port (SWD-DP) registers # See ARM DDI 0314H "Coresight Components Technical Reference Manual" DPORT_IDCODE = 0x0 # ID Code / abort DPORT_ABORT = 0x0 DPORT_CTRL = 0x4 # Control / status DPORT_STATUS = 0x4 DPORT_SELECT = 0x8 # Select DPORT_RDBUFF = 0xc # Read buffer # Access Port (AHB-AP) registers, high nybble is bank number # See ARM DDI 0337E: Cortex M3 Technical Reference Manual page 11-38 APORT_CSW = 0x0 # Control status word APORT_TAR = 0x4 # Transfer address APORT_DRW = 0xc # Data read/write APORT_BANK0 = 0x10 # Banked registers APORT_BANK1 = 0x14 APORT_BANK2 = 0x18 APORT_BANK3 = 0x1c APORT_DEBUG_ROM_ADDR= 0xf8 # Address of debug ROM APORT_IDENT = 0xfc # AP identification # DP Select Register class DP_SELECT_REG(Structure): _fields_ = [("DPBANKSEL", c_uint, 4), ("APBANKSEL", c_uint, 4), ("Reserved", c_uint, 16), ("APSEL", c_uint, 8)] class DP_SELECT(Union): _fields_ = [("reg", DP_SELECT_REG), ("value", c_uint)] dp_select = DP_SELECT() # AHB-AP Control Status Word Register class AP_CSW_REG(Structure): _fields_ = [("Size", c_uint, 3), ("Res1", c_uint, 1), ("AddrInc", c_uint, 2), ("DbgStatus", c_uint, 1), ("TransInProg", c_uint, 1), ("MODE", c_uint, 4), ("Res2", c_uint, 13), ("HProt1", c_uint, 1), ("Res3", c_uint, 3), ("MasterType", c_uint, 1), ("Res4", c_uint, 2)] class AP_CSW(Union): _fields_ = [("reg", AP_CSW_REG), ("value", c_uint)] ap_csw = AP_CSW() # Select AP bank, do read cycle def ap_banked_read(d, addr): dp_select.reg.APBANKSEL = addr >> 4; swd.swd_wr(d, swd.SWD_DP, DPORT_SELECT, dp_select.value) swd.swd_rd(d, swd.SWD_AP, addr&0xf) return swd.swd_rd(d, swd.SWD_AP, addr&0xf) # Configure AP memory accesses def ap_config(d, inc, size): dp_select.reg.APBANKSEL = 0 # Zero bank swd.swd_wr(d, swd.SWD_DP, DPORT_SELECT, dp_select.value) ap_csw.reg.MasterType = 1 ap_csw.reg.HProt1 = 1 # Enable incrementing, set access size ap_csw.reg.AddrInc = 1 if inc else 0 ap_csw.reg.Size = 0 if size==8 else 1 if size==16 else 2 return swd.swd_wr(d, swd.SWD_AP, APORT_CSW, ap_csw.value) # Set AP memory address def ap_addr(d, addr): swd.swd_wr(d, swd.SWD_AP, APORT_TAR, addr) swd.swd_idle_bytes(d, 2) # Idle to avoid 'wait' response # Do an immediate read of a 32-bit CPU memory location def cpu_mem_read32(d, addr): ap_addr(d, addr) # Address to read swd.swd_rd(d, swd.SWD_AP, APORT_DRW) # Dummy read cycle r = swd.swd_rd(d, swd.SWD_AP, APORT_DRW) # Read data return r.data.value if r.ack.value==swd.SWD_ACK_OK else None if __name__ == "__main__": mem = sys.argv[1].upper() if len(sys.argv) > 1 else None dev = ft.ft_open() if not dev: print("Can't open FTDI device") sys.exit(1) ft.set_bitmode(dev, 0, 2) # Enable SPI ft.set_spi_clock(dev, 1000000) # Set SPI clock ft.ft_write(dev, (0x80, 0, ft.OPS)) # Set outputs swd.swd_reset(dev) # Send SWD reset sequence resp = swd.swd_rd(dev, swd.SWD_DP, DPORT_IDCODE) # Request & response if resp is None: print("No response") else: print("DP ident: ack %u, value %08Xh" % (resp.ack.value, resp.data.value)) swd.swd_wr(dev, swd.SWD_DP, DPORT_ABORT, 0x1e) # Clear errors swd.swd_wr(dev, swd.SWD_DP, DPORT_CTRL, 0x5<<28) # Powerup request resp = swd.swd_rd(dev, swd.SWD_DP, DPORT_STATUS) # Get status print("Powerup: ack %u, value %08Xh" % (resp.ack.value, resp.data.value)) resp = ap_banked_read(dev, APORT_IDENT) # Get AP ident print("AP ident: ack %u, value %08Xh" % (resp.ack.value, resp.data.value)) ap_config(dev, 1, 32); # Configure AP RAM accesses s = "" if mem in ports: addr = ports[mem] s = "%08X: %s" % (addr, mem) for reg in gpio_regs: val = cpu_mem_read32(dev, addr) s += " %s=%08X" % (reg, val) addr += 4 else: try: addr = int(mem, 16) except: addr = None if addr is not None: s = "%08X:" % addr val = cpu_mem_read32(dev, addr) s += " %08X" % val if val is not None else " ?" print(s) dev.close() # EOF
Next steps
Whilst this code is an interesting framework for experimentation, it lacks some of the features and error-handling of a ‘real’ application, for example it’d be nice to draw an animated picture of the CPU, showing the SWD poll results graphically. That is the subject of another post.
Copyright (c) Jeremy P Bentham 2018. Please credit this blog if you are using the information or software in it.
Excellent article! Very informative. We are studying the SWD interface now for flash programming of ST and/or Cypress CPU but facing the chicken-egg paradox. In order to program the SWD based CPU, need to use yet an outside tool to upload the bootloader or initial firmware. The use of the FTDI interface is more sound as the SWD can be bit banged. Many thanks.
LikeLike