icenvcm: Program the iCE40 NVCM

icenvcm will write a bitstream into an iCE40 device with
one-time-programmable non-volatile configuration memory (NVCM)
and optionally set the security lock bits to prevent read out.

This is a dangerous operation and can result in bricked devices.
Since it is OTP, you must be sure that your design is good enough to
permanently burn into the chip.  It is still possible to use the iceprog
-S to reconfigure the SRAM, although the FPGA will no longer boot from
an attached SPI flash.

Note that the NVCM requires a 2.5V power supply, which the Lattice
devboard (and many other third party boards) do not include.
A schematic is included for a ZIF programming jig to burn the chips
before they are soldered to their final boards.

Signed-off-by: Trammell Hudson <hudson@trmm.net>
Co-authored-by: Matt Mets <matt@blinkinlabs.com>
Co-authored-by: Peter Lawrence <12226419+majbthrd@users.noreply.github.com>
This commit is contained in:
Trammell Hudson 2021-10-19 22:19:44 +02:00
parent 83b8ef947f
commit 9abf7d8468
4 changed files with 671 additions and 0 deletions

90
icenvcm/README.md Normal file
View File

@ -0,0 +1,90 @@
![ZIF socket with ftdi board](jig.jpg)
Tool for writing the one time programmable non-volatile configuration memory
in the iCE40up5k FPGA chips.
**DANGER: this will probably brick your chip**
* [Adafruit FTDI232H breakout board](https://www.adafruit.com/product/2264)
* [QFN48 ZIF socket](https://nl.aliexpress.com/item/1005003023038405.html)
* 1.2V regulator
* 2.5V regulator
* 2pc 10k resistor
| Function | FTDI pin | FPGA pin |
| -------- | -------- | -------- |
| Clk | D0 | 15 |
| DO | D1 | 14 |
| DI | D2 | 17 |
| CS | D4 | 16 |
| Reset | D7 | 8 |
| CDONE | D6 | 7 |
![Schematic of the programming connetion](schematic.jpg)
## NVCM Commands
These commands appear in the vendor tools.
| Command | Out | Wait | In | Description |
| --- | --- | --- | --- | --- |
| 7eaa997e010e | 6 | 0 | 0 | Enter NVCM mode |
| 02,addr,data | 12 | 0 | 0 | Write to register at specified address. Address is 3 bytes, data is 8 bytes |
| 03,addr | 4 | 9 | n | Read from register at specified address. Address is 3 bytes, data is n bytes |
| 04 | 1 | 0 | 0 | Program disable |
| 05 | 1 | 0 | 1 | Read status register (Note: Guess that status register is 1 byte) |
| 06 | 1 | 0 | 0 | Program enable |
| 82000020,data | 12 | 0 | 0 | Write to trim register. For read: 00000000c4000000, For write: 0015f2f0c2000000 |
| 83000025,bank | 5 | 0 | 0 | Bank select: NVCM:00, Trim:10:, Signature:20 |
| 84000020 | 4 | 9 | 8 | Read RF (trim) register |
Notes:
* Commands 82, 83, 84 are likely single byte + address, but are only used with the above addresses.
* Search for other commands has not been attempted.
## Register maps
These memory regions are accessed by the vendor tools
### NVCM bank
| Address Range | Description |
| --- | --- |
| 000000-? | Configuration bitstream. At least 104090 bytes long, possibly 104960 |
When reading it appears as a normal linear array. When writing the address
range is with 328 bytes per 4K page: `int(addr / 328) * 4096 + (addr % 328)`.
Only the non-zero blocks of 8-bytes are written.
Until the chip is locked, the NVCM data can be read back and appears to
be identical to the bitstream. It can also be written multiple times
while unlocked, with NAND semantics (default values are 0, bits can only
be set, not cleared).
### Trim bank
| Address Range | Description |
| --- | --- |
| 000020-000027 | Trim row 1. Set to: 0015f2f1c4000000. Byte 0 is read to determine if NVCM is programmed. This whole register is checked to see if trim was programmed properly. |
| 000060-000067 | Trim row 2 |
| 0000a0-0000a7 | Trim row 3 |
| 0000e0-0000e7 | Trim row 4 |
Notes:
* Programming all four trim registers with `0015f2f1c4000000` will cause the device to boot from NVCM (maybe)
* Programming all for trim registers with `3000000100000000` will set the NVCM security bit (maybe)
* The trim registers have NAND semantics, so bits can only be set, never unset
### Signature bank
| Address Range | Description |
| --- | --- |
| 000000-000007 | Signature bits (lower 8 bytes) |
| 000008-00000F | Signature bits (upper 8 bytes) |
Notes:
* The byte at address 000000 contains the chip ID, and is programmed at the factory. 0x20 identifies the device is ICE40UP5K. Other device values are listed but untested.
* The vendor tool writes configuration data (programmer revision, build, date), along with a CRC. It's unclear if the chip can access this data. This tool does not write those values.

581
icenvcm/icenvcm.py Executable file
View File

@ -0,0 +1,581 @@
#!/usr/bin/env python3
#
# Copyright (C) 2021
#
# * Trammell Hudson <hudson@trmm.net>
# * Matthew Mets https://github.com/cibomahto
# * Peter Lawrence https://github.com/majbthrd
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
from pyftdi.spi import SpiController
from binascii import unhexlify, hexlify
import sys
from time import sleep
import re
import os
debug = False
# todo: add expected bitstream sizes
nvcm_id_table = {
0x06: "ICE40LP8K / ICE40HX8K",
0x07: "ICE40LP4K / ICE40HX4K",
0x08: "ICE40LP1K / ICE40HX1K",
0x09: "ICE40LP384",
0x0E: "ICE40LP1K_SWG16",
0x0F: "ICE40LP640_SWG16",
0x10: "ICE5LP1K",
0x11: "ICE5LP2K",
0x12: "ICE5LP4K",
0x14: "ICE40UL1K",
0x15: "ICE40UL640",
0x20: "ICE40UP5K",
0x21: "ICE40UP3K",
}
def die(s):
print(s, file=sys.stderr)
exit(1)
def enable(cs,reset=1):
gpio.write(cs << cs_pin | reset << reset_pin)
def sendhex(s):
if debug and not s == "0500":
print("TX", s)
x = bytes.fromhex(s)
b = dev.exchange(x, duplex=True, readlen=len(x))
if debug and not s == "0500":
print("RX", b.hex())
return int.from_bytes(b, byteorder='big')
def delay(count):
# run the clock with no CS asserted
dev.exchange(b'\x00', duplex=True, readlen=count)
#sleep(0.1)
def tck(count,dly=0):
delay(count >> 3)
delay(count >> 3)
delay(count >> 3)
delay(count >> 3)
delay(count >> 3)
delay(count >> 3)
def init():
if debug:
print("init")
enable(1, 1)
enable(1, 0) # reset high
sleep(0.15)
enable(0, 0) # enable and reset high
sleep(0.12)
enable(0, 1) # enable low, reset high
sleep(0.12)
enable(1, 1) # enable and reset low
sleep(0.12)
return True
def status_wait(count=1000):
for i in range(0,count):
tck(5000)
enable(0)
x = sendhex("0500")
enable(1)
#print("x=%04x" %(x))
if (x & 0x00c1) == 0:
return True
print("status failed to clear", file=sys.stdout)
return False
def nvcm_command(cmd):
enable(0)
sendhex(cmd)
enable(1)
if not status_wait():
return False
tck(8)
return True
def nvcm_pgm_enable():
return nvcm_command("06")
def nvcm_pgm_disable():
return nvcm_command("04")
def nvcm_enable_access():
# ! Shift in Access-NVCM instruction;
# SMCInstruction[1] = 0x70807E99557E;
return nvcm_command("7eaa997e010e")
# Returns a big integer
def nvcm_read(address, length=8, cmd=0x03):
enable(0)
sendhex("%02x%06x" % (cmd, address))
sendhex("00" * 9) # dummy bytes
x = 0
for i in range(0,length):
x = x << 8 | sendhex("00")
enable(1)
return x
# Returns a byte array of the contents
def nvcm_read_bytes(address, length=8):
return nvcm_read(address, length).to_bytes(length, byteorder="big")
def nvcm_write(address, data, cmd=0x02):
enable(0)
sendhex("%02x%06x" % (cmd, address))
sendhex(data)
enable(1)
if not status_wait():
print("WRITE FAILED: cmd=%02x address=%06x data=%s" % (cmd, address, data.hex()), file=sys.stderr)
return False
tck(8)
return True
def nvcm_bank_select(bank):
return nvcm_write(cmd=0x83, address=0x000025, data="%02x" % (bank))
def nvcm_select_nvcm():
# ! Shift in Restore Access-NVCM instruction;
# SDR 40 TDI(0x00A40000C1);
return nvcm_bank_select(0x00)
def nvcm_select_trim():
# ! Shift in Trim setup-NVCM instruction;
# SDR 40 TDI(0x08A40000C1);
return nvcm_bank_select(0x10)
def nvcm_select_sig():
# ! Shift in Access Silicon Signature instruction;
# IDInstruction[1] = 0x04A40000C1;
# SDR 40 TDI(IDInstruction[1]);
return nvcm_bank_select(0x20)
def nvcm_read_trim():
# ! Shift in Access-NVCM instruction;
# SMCInstruction[1] = 0x70807E99557E;
if not nvcm_enable_access():
return
# ! Shift in READ_RF(0x84) instruction;
# SDR 104 TDI(0x00000000000000000004000021);
x = nvcm_read(cmd=0x84, address=0x000020, length=8)
tck(8)
#print("FSM Trim Register %x" % (x))
nvcm_select_nvcm()
return x
def nvcm_write_trim(data):
# ! Setup Programming Parameter in Trim Registers;
# ! Shift in Trim setup-NVCM instruction;
# TRIMInstruction[1] = 0x000000430F4FA80004000041;
return nvcm_write(cmd=0x82, address=0x000020, data=data)
def nvcm_enable():
if debug:
print("nvcm_enable")
# ! Shift in Access-NVCM instruction;
# SMCInstruction[1] = 0x70807E99557E;
if not nvcm_enable_access():
return
# ! Setup Reading Parameter in Trim Registers;
# ! Shift in Trim setup-NVCM instruction;
# TRIMInstruction[1] = 0x000000230000000004000041;
if debug:
print("setup_nvcm")
return nvcm_write_trim("00000000c4000000")
def nvcm_enable_trim():
# ! Setup Programming Parameter in Trim Registers;
# ! Shift in Trim setup-NVCM instruction;
# TRIMInstruction[1] = 0x000000430F4FA80004000041;
return nvcm_write_trim("0015f2f0c2000000")
def nvcm_disable():
if not nvcm_select_nvcm():
return
reset(1)
tck(8, 1)
rest(0)
tck(8, 2000)
def nvcm_trim_blank_check():
print ("NVCM Trim_Parameter_OTP blank check");
if not nvcm_select_trim():
return
x = nvcm_read(0x000020, 1)
nvcm_select_nvcm()
if x != 0:
die ("NVCM Trim_Parameter_OTP Block is not blank. (%02x)" % x);
return True
def nvcm_blank_check(total_fuse):
nvcm_select_nvcm()
status = True
print ("NVCM main memory blank check");
contents = nvcm_read_bytes(0x000000, total_fuse)
for i in range(0,total_fuse):
x = contents[i]
if debug:
print("%08x: %02x" % (i, x))
if x != 0:
print ("%08x: NVCM Main Memory Block is not blank." % (i), file=sys.stderr)
status = False
#break
nvcm_select_nvcm()
return status
def nvcm_program(rows):
nvcm_select_nvcm()
if not nvcm_enable_trim():
return False
print ("NVCM Program main memory")
if not nvcm_pgm_enable():
return false
status = True
i = 0
for row in rows:
if i % 1024 == 0:
print("%6d / %6d bytes" % (i, len(rows) * 8))
i += 8
if not nvcm_command(row):
status = False
break
nvcm_pgm_disable()
if not status:
print("PROGRAMMING FAILED", file=sys.stderr)
return status
def nvcm_write_trim_pages(lock_bits):
if not nvcm_select_nvcm():
die("select trim failed")
if not nvcm_enable_trim():
die("write trim command failed")
if not nvcm_select_trim():
die("select trim failed")
if not nvcm_pgm_enable():
die("write enable failed")
# ! Program Security Bit row 1;
# ! Shift in PAGEPGM instruction;
# SDR 96 TDI(0x000000008000000C04000040);
# ! Program Security Bit row 2;
# SDR 96 TDI(0x000000008000000C06000040);
# ! Program Security Bit row 3;
# SDR 96 TDI(0x000000008000000C05000040);
# ! Program Security Bit row 4;
# SDR 96 TDI(0x00000000800000C07000040);
if not nvcm_write(0x000020, lock_bits):
die("trim write 0x20 failed")
if not nvcm_write(0x000060, lock_bits):
die("trim write 0x60 failed")
if not nvcm_write(0x0000a0, lock_bits):
die("trim write 0xa0 failed")
if not nvcm_write(0x0000e0, lock_bits):
die("trim write 0xe0 failed")
nvcm_pgm_disable()
# verify a read back
x = nvcm_read(0x000020, 8)
nvcm_select_nvcm()
lock_bits = int(lock_bits,16)
if x & lock_bits != lock_bits:
die("Failed to write trim lock bits: %016x != expected %016x" % (x,lock_bits))
print("New state %016x" % (x))
return True
def nvcm_trim_secure():
print ("NVCM Secure")
trim = nvcm_read_trim()
if (trim >> 60) & 0x3 != 0:
print("NVCM already secure? trim=%016x" % (trim), file=sys.stderr)
return nvcm_write_trim_pages("3000000100000000")
def nvcm_trim_program():
print ("NVCM Program Trim_Parameter_OTP");
return nvcm_write_trim_pages("0015f2f1c4000000")
def nvcm_info():
nvcm_select_sig()
sig1 = nvcm_read(0x000000, 8)
nvcm_select_sig()
sig2 = nvcm_read(0x000008, 8)
# have to switch back to nvcm bank before switching to trim?
nvcm_select_nvcm()
trim = nvcm_read_trim()
nvcm_select_nvcm()
nvcm_select_trim()
trim0 = nvcm_read(0x000020, 8)
nvcm_select_trim()
trim1 = nvcm_read(0x000060, 8)
nvcm_select_trim()
trim2 = nvcm_read(0x0000a0, 8)
nvcm_select_trim()
trim3 = nvcm_read(0x0000e0, 8)
nvcm_select_nvcm()
secured = ((trim >> 60) & 0x3)
device_id = (sig1 >> 56) & 0xFF
print("Device: %s (%02x) secure=%d" % (
nvcm_id_table.get(device_id, "Unknown"),
device_id,
secured
))
print("Sig 0: %016x" % (sig1))
print("Sig 1: %016x" % (sig2))
print("TrimRF: %016x" % (trim))
print("Trim 0: %016x" % (trim0))
print("Trim 1: %016x" % (trim1))
print("Trim 2: %016x" % (trim2))
print("Trim 3: %016x" % (trim3))
return True
def nvcm_read_file(filename):
nvcm_select_nvcm()
total_fuse = 104090
contents = b''
for offset in range(0,total_fuse,8):
if offset % 1024 == 0:
print("%6d / %6d bytes" % (offset, total_fuse))
contents += nvcm_read_bytes(offset, 8)
if filename == '-':
with os.fdopen(sys.stdout.fileno(), "wb", closefd=False) as f:
f.write(contents)
f.flush()
else:
with open(filename, "wb") as f:
f.write(contents)
f.flush()
#
# bistream to NVCM command conversion is based on majbthrd's work in
# https://github.com/YosysHQ/icestorm/pull/272
#
def bitstream2nvcm(bitstream):
# ensure that the file starts with the correct bistream preamble
for origin in range(0,len(bitstream)):
if bitstream[origin:origin+4] == bytes.fromhex('7EAA997E'):
break
if origin == len(bitstream):
print("Preamble not found", file=sys.stderr)
return False
print("Found preamable at %08x" % (origin), file=sys.stderr)
# there might be stuff in the header with vendor tools,
# but not usually in icepack produced output, so ignore it for now
# todo: what is the correct size?
rows = []
for pos in range(origin, len(bitstream), 8):
row = bitstream[pos:pos+8]
# pad out to 8-bytes
row += b'\0' * (8 - len(row))
if row == bytes(8):
# skip any all-zero entries in the bistream
continue
# NVCM addressing is very weird
addr = pos - origin
nvcm_addr = int(addr / 328) * 4096 + (addr % 328)
rows += [ "02 %06x %s" % (nvcm_addr, row.hex()) ]
return rows
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument( '--port',
type=str,
default='ftdi://::/1',
help='FTDI port of the form ftdi://::/1')
parser.add_argument( '-v', '--verbose',
dest='verbose',
action='store_true',
help='Show debug information and serial read/writes')
parser.add_argument('-b', '--boot',
dest='do_boot',
action='store_true',
help='Deassert the reset line to allow the FPGA to boot')
parser.add_argument('-i', '--info',
dest='read_info',
action='store_true',
help='Read chip ID, trim and other info')
parser.add_argument('--read',
dest='read_file',
type=str,
default=None,
help='Read contents of NVCM')
parser.add_argument('--write',
dest='write_file',
type=str,
default=None,
help='bitstream file to write to NVCM (warning: not reversable!)')
parser.add_argument('--ignore-blank',
dest='ignore_blank',
action='store_true',
help='Proceed even if the chip is not blank')
parser.add_argument('--secure',
dest='set_secure',
action='store_true',
help='Set security bits to prevent modification (warning: not reversable!')
parser.add_argument('--my-design-is-good-enough',
dest='good_enough',
action='store_true',
help='Enable the dangerous commands --write and --secure')
args = parser.parse_args()
debug = args.verbose
if not args.good_enough \
and (args.write_file or args.set_secure):
print("Are you sure your design is good enough?", file=sys.stderr)
exit(1)
# Instantiate a SPI controller, with separately managed CS line
spi = SpiController()
# Configure the first interface (IF/1) of the FTDI device as a SPI controller
spi.configure(args.port)
# Get a port to a SPI device w/ /CS on A*BUS3 and SPI mode 0 @ 12MHz
# the CS line is not used in this case
dev = spi.get_port(cs=0, freq=12E6, mode=0)
reset_pin = 7
cs_pin = 4
# Get GPIO port to manage the CS and RESET pins
gpio = spi.get_gpio()
gpio.set_direction(1 << reset_pin | 1 << cs_pin, 1 << reset_pin | 1 << cs_pin)
## Request the JEDEC ID from the SPI device
#jedec_id = dev.exchange([0x9f], 3)
enable(1, 0) # enable and reset high
sleep(0.2)
enable(1, 1) # enable low, reset high
init() or exit(1)
nvcm_enable() or exit(1)
if args.read_info:
nvcm_info() or exit(1)
if args.write_file:
with open(args.write_file, "rb") as f:
bitstream = f.read()
print("read %d bytes" % (len(bitstream)))
cmds = bitstream2nvcm(bitstream)
if not cmds:
exit(1)
if not args.ignore_blank:
nvcm_trim_blank_check() or exit(1)
# how much should we check?
nvcm_blank_check(0x100) or exit(1)
# this is it!
nvcm_program(cmds) or exit(1)
# update the trim to boot from nvcm
nvcm_trim_program() or exit(1)
if args.read_file:
# read back after writing to the NVCM
nvcm_read_file(args.read_file) or exit(1)
if args.set_secure:
nvcm_trim_secure() or exit(1)
if args.do_boot:
# hold reset low for half a second
enable(1,0)
sleep(0.5)
enable(1,1)

BIN
icenvcm/jig.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

BIN
icenvcm/schematic.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB