-
Notifications
You must be signed in to change notification settings - Fork 6
Spartan7 JTAG Notes
eFuse on the Series 7 is divided into a series of banks that are unlocked; each bank can be burned once, and each bit must be burned one at a time. Burning a bit turns it from a 0
into a 1
. The banking rules causes some overlap between the function of fuses (for example, the lower 8 bits of the user fuse must be burned at the same time as the key).
Vivado HW manager starts a fuse burn by reading out DNA, FUSE, USER, and CONTROL registers. This seems to be optional. The read happens even if the readback bits are blown; all that happens is the readback returns a value of all 1's when readback is prohibited.
There is a special, undocumented fuse control instruction register, which we will call EFUSE. It has an instruction coding of 0b110000
, and it unlocks a data register with a length of 64 bits.
Data to the EFUSE machine is encoded as follows:
[32-bit key] [24-bit argument] [8-bit command]
- The 32-bit key is a fixed number used to prevent accidental activation of the EFUSE burning protocol. It has a value of 0xa08a28ac. This is a bit set equivalent to the prime numbers
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]
of the range(0, 31]
(so bits 2, 3, 5, 7...31 are set). It probably has no special meaning other than just an unlikely sequence to appear by accident. - The 24-bit argument is usually a small-ish number that looks to correspond to a specific bit within a bank.
- The 8-bit command seems to specify either a command code, or a bank, depending on the modal context of the eFuse block
When using the Vivado hardware manager, every EFUSE command is preceded by a JSTART (0b001100
) command plus two ISC_NOOP (0b010100
) commmands. There is a 2ms delay after each of the ISC_NOOP commands, thus causing about a 4ms net pause between each bank burn. Replacing the ISC_NOOP calls with a simple delay seems to also work; ISC_NOOP is undesired in our application because it causes the FPGA logic to reset, and we are trying to drive eFuse burning from the internal logic directly.
As a convention, IR commands will be spelled in all CAPS and are 6-bit numbers that go into the IR. So when we say "issue the EFUSE command" it means to put the EFUSE IR code of 0b110000
into the IR. Data belonging to the IR command is specified in parenthesis after the IR command, for example:
EFUSE(0xa08a28ac00004001/64, 0/64)
Translates to:
-
0b110000
-> IR 0xa08a28ac00004001 -> DR as 64-bit number
0x0 -> DR as 64 bit number
DRs may be specified symbolically in brackets:
{key, arg, cmd}
With the bit fields corresponding to the specification above.
The general algorithm for burning a fuse seems to be as follows:
- Select a bank:
- Issue JSTART() + ISC_NOOP() + ISC_NOOP(); alternatively, JSTART() + delay for 4ms
- EFUSE({key, 0x4, 0x1}/64, {key, 0x4, 0x1}/64). That is: EFUSE(0xa08a28ac00004001, 0xa08a28ac00004001)
- EFUSE({key, 0x0, bank_select}/64, 0x0/64)
- Burn a 1 into a specific bit and word:
- EFUSE({key, bit_offset, word_select}/64, 0x0/64)
- Repeat step 2 until the desired 1's are burned into the bank
- Close the bank:
- Issue JSTART() + ISC_NOOP() + ISC_NOOP(); alternatively, JSTART() + delay for 4ms
- EFUSE({key, 0x4, 0x1}/64, {key, 0x4, 0x1}/64).
- EFUSE({key, 0x0, bank_select}/64, 0x0/64)
- Repeat steps 1-4 for each of the desired banks to burn
- After the final bank close, push the number 0xff000000ff/64 into the DR with no intervening IR code.
In the algorithm above, we refer to bank_select
, word_select
, and bit_offset
. The exact mappings need to be worked out with further experimentation. But, for example, the two MSBs of the KEY eFuse correspond to a bank_select
of 0xa1
, a word_select
of 0xa3, and bit_offset
s 0x40 and 0x41.
See https://github.com/betrusted-io/betrusted-wiki/wiki/7series-efuse-codes-release.ods for the complete list of codes and offsets for the eFuses.
The eFuse bits are also protected with a SECDED error correcting code. It's a (30,24) extended hamming code. You can find a Rust implementation for the algorithm at https://github.com/betrusted-io/betrusted-soc/tree/main/sw/efuse-ecc
Programming a BBRAM key is accomplished using the following JTAG command sequence:
ISC_ENABLE (010000)
-> 5'h15
<- 5'h01
-> 5'h15
<- 5'h1e
XSC_PROGRAM_KEY (010010)
-> 32'hffffffff
<- 32'h1d
ISC_PROGRAM (010001)
-> 32'h557B
<- 32'h12
ISC_PROGRAM (010001)
-> 32'key[255:224]
<- 32'h12
ISC_PROGRAM (010001)
-> 32'key[223:192]
<- 32'h12
ISC_PROGRAM (010001)
-> 32'key[191:160]
<- 32'h12
ISC_PROGRAM (010001)
-> 32'key[159:128]
<- 32'h12
ISC_PROGRAM (010001)
-> 32'key[127:96]
<- 32'h12
ISC_PROGRAM (010001)
-> 32'key[95:64]
<- 32'h12
ISC_PROGRAM (010001)
-> 32'key[63:32]
<- 32'h12
ISC_PROGRAM (010001)
-> 32'key[31:0]
<- 32'h12
BBKEY_RBK (010101)
-> 37'h1f_ffff_ffff
<- 37'hAAF76
BBKEY_RBK (010101)
-> 37'h1f_ffff_ffff
<- {32'hkey[255:224], 5'b10110}
BBKEY_RBK (010101)
-> 37'h1f_ffff_ffff
<- {32'hkey[223:192], 5'b10110}
BBKEY_RBK (010101)
-> 37'h1f_ffff_ffff
<- {32'hkey[191:160], 5'b10110}
BBKEY_RBK (010101)
-> 37'h1f_ffff_ffff
<- {32'hkey[159:128], 5'b10110}
BBKEY_RBK (010101)
-> 37'h1f_ffff_ffff
<- {32'hkey[127:96], 5'b10110}
BBKEY_RBK (010101)
-> 37'h1f_ffff_ffff
<- {32'hkey[95:64], 5'b10110}
BBKEY_RBK (010101)
-> 37'h1f_ffff_ffff
<- {32'hkey[63:32], 5'b10110}
BBKEY_RBK (010101)
-> 37'h1f_ffff_ffff
<- {32'hkey[31:0], 5'b10110}
(optional?) ISC_DISABLE (010110)
Note that ->
means sending to the device, and <-
is returned from the device.
The BBKEY_RBK command is able to read back the BBRAM AES key. Presumably, this backdoor is closed once bit 3 of FUSE_CNTL
is burned, although the datasheet doesn't explicitly say this.
Note that there are subtle timing details in the JTAG command timings in order for this to work. Please see the https://github.com/betrusted-io/jtag-trace "bbramtest.jtg" file for more details.
Connect MDO4104B-6 logic analyzer to TCK, TDI, TDO, TMS.
Vivado hardware manager continuously spams the JTAG bus, so we need a trigger code.
Abuse the "SPI" decode module to do a serial decode of TDI to create a reliable-enough trigger code. JTAG isn't SPI, but if you connect TMS to the "SS" of SPI, TDI to "MOSI" of SPI, and TCK to "SCLK" of SPI, the initial high pulse to traverse to the IR instruction state creates a nice "SS" framing of all but one bit of the IR code.
So, configure the socpe as follows:
- TMS -> SPI SS
- TDI -> SPI MOSI
- Word size: 7 bits
- Bit order: LSB first
To test this works, we can trigger off of XADC while running the HW dashboard function. Here's how we translate the XADC IR code into a SPI packet:
XADC code example. XADC IR code is 110111. It is shifted LSB first.
cycle a b | c d e f g h i | j
TMS 0 1 1 | 0 0 0 0 0 0 0 | 1 X X
TDI 0 0 0 | 0 0 1 1 1 0 1 | 1 X X
cycle key:
a - select-DR-scan
b - select-IR-scan
c - capture-IR - begin "SPI" packet
d - shift-IR
e - LSB of IR
i - end "SPI" packet
j - MSB of IR, TMS goes high. MSB is "lost" because of concurrent TMS high.
The resulting XADC code then seen by the oscope is (as LSB first, cycles c-i): 101_1100 -> 0x5C trigger code
More worked examples:
eFuse FUSE_KEY code example. FUSE_KEY IR code is 110001.
TMS 0 1 1 | 0 0 0 0 0 0 0 | 1 X X
TDI 0 0 0 | 0 0 1 0 0 0 1 | 1 X X
rewrite as LSB first: 100_0100 -> 0x44 trigger code
eFuse FUSE_USER code example. FUSE_USER IR code is 110011.
TMS 0 1 1 | 0 0 0 0 0 0 0 | 1 X X
TDI 0 0 0 | 0 0 1 1 0 0 1 | 1 X X
rewrite as LSB first: 100_1100 -> 0x4C trigger code
eFuse FUSE_CNTL code example. FUSE_CNTL IR code is 110100.
TMS 0 1 1 | 0 0 0 0 0 0 0 | 1 X X
TDI 0 0 0 | 0 0 0 0 1 0 1 | 1 X X
rewrite as LSB first: 101_0000 -> 0x50 trigger code
BBRAM XSC_PROGRAM_KEY code example. XSC_PROGRAM_KEY IR code is 010010.
TMS 0 1 1 | 0 0 0 0 0 0 0 | 1 X X
TDI 0 0 0 | 0 0 0 1 0 0 1 | 0 X X
rewrite as LSB first: 100_1000 -> 0x48 trigger code
BBRAM READBACK_KEY code example. BBKEY_RBK IR code is 010101.
TMS 0 1 1 | 0 0 0 0 0 0 0 | 1 X X
TDI 0 0 0 | 0 0 1 0 1 0 1 | 0 X X
rewrite as LSB first: 101_0100 -> 0x54 trigger code
ISC_ENABLE code example. ISC_ENABLE IR code is 010000.
TMS 0 1 1 | 0 0 0 0 0 0 0 | 1 X X
TDI 0 0 0 | 0 0 0 0 0 0 1 | 0 X X
rewrite as LSB first: 100_0000 -> 0x40 trigger code
Define a "parallel bus", which has "clocked data" selected under "define inputs". Map bits [2:0] as {TDI, TMS, TDO}.
Now "event table" will only display clocked events instead of all samples. Event table can be exported as a CSV for later protocol post-processing using https://github.com/betrusted-io/jtag-trace.