mirror of
https://github.com/mytechnotalent/Embedded-Hacking.git
synced 2026-10-08 09:01:23 +02:00
Add Week 4 Binary Ninja notebook and OpenOCD/flash host tooling
- WEEK04-BN.md/.pdf: full Binary Ninja dynamic + static lab for 0x0005 and 0x0008 - README: link the Week 4-BN notebook after Week 4a - debug-server.sh/.ps1: OpenOCD launcher (USE_CORE=0, -rtos none, BP_ADDR) - flash.sh/.ps1: UF2 flash helpers - pyproject.toml: ruff config for root host-side scripts
This commit is contained in:
1 parent
34f21bcd52
commit
3e52dd0412
11 files changed
+2264
-1095
No files matched your search
@@ -1,365 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import sys
|
|
||||||
import struct
|
|
||||||
import subprocess
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
import os.path
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
|
|
||||||
UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
|
|
||||||
UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
|
|
||||||
UF2_MAGIC_END = 0x0AB16F30 # Ditto
|
|
||||||
|
|
||||||
INFO_FILE = "/INFO_UF2.TXT"
|
|
||||||
|
|
||||||
appstartaddr = 0x2000
|
|
||||||
familyid = 0x0
|
|
||||||
|
|
||||||
|
|
||||||
def is_uf2(buf):
|
|
||||||
w = struct.unpack("<II", buf[0:8])
|
|
||||||
return w[0] == UF2_MAGIC_START0 and w[1] == UF2_MAGIC_START1
|
|
||||||
|
|
||||||
def is_hex(buf):
|
|
||||||
try:
|
|
||||||
w = buf[0:30].decode("utf-8")
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
return False
|
|
||||||
if w[0] == ':' and re.match(rb"^[:0-9a-fA-F\r\n]+$", buf):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def convert_from_uf2(buf):
|
|
||||||
global appstartaddr
|
|
||||||
global familyid
|
|
||||||
numblocks = len(buf) // 512
|
|
||||||
curraddr = None
|
|
||||||
currfamilyid = None
|
|
||||||
families_found = {}
|
|
||||||
prev_flag = None
|
|
||||||
all_flags_same = True
|
|
||||||
outp = []
|
|
||||||
for blockno in range(numblocks):
|
|
||||||
ptr = blockno * 512
|
|
||||||
block = buf[ptr:ptr + 512]
|
|
||||||
hd = struct.unpack(b"<IIIIIIII", block[0:32])
|
|
||||||
if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
|
|
||||||
print("Skipping block at " + ptr + "; bad magic")
|
|
||||||
continue
|
|
||||||
if hd[2] & 1:
|
|
||||||
# NO-flash flag set; skip block
|
|
||||||
continue
|
|
||||||
datalen = hd[4]
|
|
||||||
if datalen > 476:
|
|
||||||
assert False, "Invalid UF2 data size at " + ptr
|
|
||||||
newaddr = hd[3]
|
|
||||||
if (hd[2] & 0x2000) and (currfamilyid == None):
|
|
||||||
currfamilyid = hd[7]
|
|
||||||
if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid):
|
|
||||||
currfamilyid = hd[7]
|
|
||||||
curraddr = newaddr
|
|
||||||
if familyid == 0x0 or familyid == hd[7]:
|
|
||||||
appstartaddr = newaddr
|
|
||||||
padding = newaddr - curraddr
|
|
||||||
if padding < 0:
|
|
||||||
assert False, "Block out of order at " + ptr
|
|
||||||
if padding > 10*1024*1024:
|
|
||||||
assert False, "More than 10M of padding needed at " + ptr
|
|
||||||
if padding % 4 != 0:
|
|
||||||
assert False, "Non-word padding size at " + ptr
|
|
||||||
while padding > 0:
|
|
||||||
padding -= 4
|
|
||||||
outp.append(b"\x00\x00\x00\x00")
|
|
||||||
if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]):
|
|
||||||
outp.append(block[32 : 32 + datalen])
|
|
||||||
curraddr = newaddr + datalen
|
|
||||||
if hd[2] & 0x2000:
|
|
||||||
if hd[7] in families_found.keys():
|
|
||||||
if families_found[hd[7]] > newaddr:
|
|
||||||
families_found[hd[7]] = newaddr
|
|
||||||
else:
|
|
||||||
families_found[hd[7]] = newaddr
|
|
||||||
if prev_flag == None:
|
|
||||||
prev_flag = hd[2]
|
|
||||||
if prev_flag != hd[2]:
|
|
||||||
all_flags_same = False
|
|
||||||
if blockno == (numblocks - 1):
|
|
||||||
print("--- UF2 File Header Info ---")
|
|
||||||
families = load_families()
|
|
||||||
for family_hex in families_found.keys():
|
|
||||||
family_short_name = ""
|
|
||||||
for name, value in families.items():
|
|
||||||
if value == family_hex:
|
|
||||||
family_short_name = name
|
|
||||||
print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex))
|
|
||||||
print("Target Address is 0x{:08x}".format(families_found[family_hex]))
|
|
||||||
if all_flags_same:
|
|
||||||
print("All block flag values consistent, 0x{:04x}".format(hd[2]))
|
|
||||||
else:
|
|
||||||
print("Flags were not all the same")
|
|
||||||
print("----------------------------")
|
|
||||||
if len(families_found) > 1 and familyid == 0x0:
|
|
||||||
outp = []
|
|
||||||
appstartaddr = 0x0
|
|
||||||
return b"".join(outp)
|
|
||||||
|
|
||||||
def convert_to_carray(file_content):
|
|
||||||
outp = "const unsigned long bindata_len = %d;\n" % len(file_content)
|
|
||||||
outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {"
|
|
||||||
for i in range(len(file_content)):
|
|
||||||
if i % 16 == 0:
|
|
||||||
outp += "\n"
|
|
||||||
outp += "0x%02x, " % file_content[i]
|
|
||||||
outp += "\n};\n"
|
|
||||||
return bytes(outp, "utf-8")
|
|
||||||
|
|
||||||
def convert_to_uf2(file_content):
|
|
||||||
global familyid
|
|
||||||
datapadding = b""
|
|
||||||
while len(datapadding) < 512 - 256 - 32 - 4:
|
|
||||||
datapadding += b"\x00\x00\x00\x00"
|
|
||||||
numblocks = (len(file_content) + 255) // 256
|
|
||||||
outp = []
|
|
||||||
for blockno in range(numblocks):
|
|
||||||
ptr = 256 * blockno
|
|
||||||
chunk = file_content[ptr:ptr + 256]
|
|
||||||
flags = 0x0
|
|
||||||
if familyid:
|
|
||||||
flags |= 0x2000
|
|
||||||
hd = struct.pack(b"<IIIIIIII",
|
|
||||||
UF2_MAGIC_START0, UF2_MAGIC_START1,
|
|
||||||
flags, ptr + appstartaddr, 256, blockno, numblocks, familyid)
|
|
||||||
while len(chunk) < 256:
|
|
||||||
chunk += b"\x00"
|
|
||||||
block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END)
|
|
||||||
assert len(block) == 512
|
|
||||||
outp.append(block)
|
|
||||||
return b"".join(outp)
|
|
||||||
|
|
||||||
class Block:
|
|
||||||
def __init__(self, addr, default_data=0xFF):
|
|
||||||
self.addr = addr
|
|
||||||
self.bytes = bytearray([default_data] * 256)
|
|
||||||
|
|
||||||
def encode(self, blockno, numblocks):
|
|
||||||
global familyid
|
|
||||||
flags = 0x0
|
|
||||||
if familyid:
|
|
||||||
flags |= 0x2000
|
|
||||||
hd = struct.pack("<IIIIIIII",
|
|
||||||
UF2_MAGIC_START0, UF2_MAGIC_START1,
|
|
||||||
flags, self.addr, 256, blockno, numblocks, familyid)
|
|
||||||
hd += self.bytes[0:256]
|
|
||||||
while len(hd) < 512 - 4:
|
|
||||||
hd += b"\x00"
|
|
||||||
hd += struct.pack("<I", UF2_MAGIC_END)
|
|
||||||
return hd
|
|
||||||
|
|
||||||
def convert_from_hex_to_uf2(buf):
|
|
||||||
global appstartaddr
|
|
||||||
appstartaddr = None
|
|
||||||
upper = 0
|
|
||||||
currblock = None
|
|
||||||
blocks = []
|
|
||||||
for line in buf.split('\n'):
|
|
||||||
if line[0] != ":":
|
|
||||||
continue
|
|
||||||
i = 1
|
|
||||||
rec = []
|
|
||||||
while i < len(line) - 1:
|
|
||||||
rec.append(int(line[i:i+2], 16))
|
|
||||||
i += 2
|
|
||||||
tp = rec[3]
|
|
||||||
if tp == 4:
|
|
||||||
upper = ((rec[4] << 8) | rec[5]) << 16
|
|
||||||
elif tp == 2:
|
|
||||||
upper = ((rec[4] << 8) | rec[5]) << 4
|
|
||||||
elif tp == 1:
|
|
||||||
break
|
|
||||||
elif tp == 0:
|
|
||||||
addr = upper + ((rec[1] << 8) | rec[2])
|
|
||||||
if appstartaddr == None:
|
|
||||||
appstartaddr = addr
|
|
||||||
i = 4
|
|
||||||
while i < len(rec) - 1:
|
|
||||||
if not currblock or currblock.addr & ~0xff != addr & ~0xff:
|
|
||||||
currblock = Block(addr & ~0xff)
|
|
||||||
blocks.append(currblock)
|
|
||||||
currblock.bytes[addr & 0xff] = rec[i]
|
|
||||||
addr += 1
|
|
||||||
i += 1
|
|
||||||
numblocks = len(blocks)
|
|
||||||
resfile = b""
|
|
||||||
for i in range(0, numblocks):
|
|
||||||
resfile += blocks[i].encode(i, numblocks)
|
|
||||||
return resfile
|
|
||||||
|
|
||||||
def to_str(b):
|
|
||||||
return b.decode("utf-8")
|
|
||||||
|
|
||||||
def get_drives():
|
|
||||||
drives = []
|
|
||||||
if sys.platform == "win32":
|
|
||||||
r = subprocess.check_output([
|
|
||||||
"powershell",
|
|
||||||
"-Command",
|
|
||||||
'(Get-WmiObject Win32_LogicalDisk -Filter "VolumeName=\'RPI-RP2\'").DeviceID'
|
|
||||||
])
|
|
||||||
drive = to_str(r).strip()
|
|
||||||
if drive:
|
|
||||||
drives.append(drive)
|
|
||||||
else:
|
|
||||||
searchpaths = ["/mnt", "/media"]
|
|
||||||
if sys.platform == "darwin":
|
|
||||||
searchpaths = ["/Volumes"]
|
|
||||||
elif sys.platform == "linux":
|
|
||||||
searchpaths += ["/media/" + os.environ["USER"], "/run/media/" + os.environ["USER"]]
|
|
||||||
if "SUDO_USER" in os.environ.keys():
|
|
||||||
searchpaths += ["/media/" + os.environ["SUDO_USER"]]
|
|
||||||
searchpaths += ["/run/media/" + os.environ["SUDO_USER"]]
|
|
||||||
|
|
||||||
for rootpath in searchpaths:
|
|
||||||
if os.path.isdir(rootpath):
|
|
||||||
for d in os.listdir(rootpath):
|
|
||||||
if os.path.isdir(os.path.join(rootpath, d)):
|
|
||||||
drives.append(os.path.join(rootpath, d))
|
|
||||||
|
|
||||||
|
|
||||||
def has_info(d):
|
|
||||||
try:
|
|
||||||
return os.path.isfile(d + INFO_FILE)
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return list(filter(has_info, drives))
|
|
||||||
|
|
||||||
|
|
||||||
def board_id(path):
|
|
||||||
with open(path + INFO_FILE, mode='r') as file:
|
|
||||||
file_content = file.read()
|
|
||||||
return re.search(r"Board-ID: ([^\r\n]*)", file_content).group(1)
|
|
||||||
|
|
||||||
|
|
||||||
def list_drives():
|
|
||||||
for d in get_drives():
|
|
||||||
print(d, board_id(d))
|
|
||||||
|
|
||||||
|
|
||||||
def write_file(name, buf):
|
|
||||||
with open(name, "wb") as f:
|
|
||||||
f.write(buf)
|
|
||||||
print("Wrote %d bytes to %s" % (len(buf), name))
|
|
||||||
|
|
||||||
|
|
||||||
def load_families():
|
|
||||||
# The expectation is that the `uf2families.json` file is in the same
|
|
||||||
# directory as this script. Make a path that works using `__file__`
|
|
||||||
# which contains the full path to this script.
|
|
||||||
filename = "uf2families.json"
|
|
||||||
pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
|
|
||||||
with open(pathname) as f:
|
|
||||||
raw_families = json.load(f)
|
|
||||||
|
|
||||||
families = {}
|
|
||||||
for family in raw_families:
|
|
||||||
families[family["short_name"]] = int(family["id"], 0)
|
|
||||||
|
|
||||||
return families
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
global appstartaddr, familyid
|
|
||||||
def error(msg):
|
|
||||||
print(msg, file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.')
|
|
||||||
parser.add_argument('input', metavar='INPUT', type=str, nargs='?',
|
|
||||||
help='input file (HEX, BIN or UF2)')
|
|
||||||
parser.add_argument('-b', '--base', dest='base', type=str,
|
|
||||||
default="0x2000",
|
|
||||||
help='set base address of application for BIN format (default: 0x2000)')
|
|
||||||
parser.add_argument('-f', '--family', dest='family', type=str,
|
|
||||||
default="0x0",
|
|
||||||
help='specify familyID - number or name (default: 0x0)')
|
|
||||||
parser.add_argument('-o', '--output', metavar="FILE", dest='output', type=str,
|
|
||||||
help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible')
|
|
||||||
parser.add_argument('-d', '--device', dest="device_path",
|
|
||||||
help='select a device path to flash')
|
|
||||||
parser.add_argument('-l', '--list', action='store_true',
|
|
||||||
help='list connected devices')
|
|
||||||
parser.add_argument('-c', '--convert', action='store_true',
|
|
||||||
help='do not flash, just convert')
|
|
||||||
parser.add_argument('-D', '--deploy', action='store_true',
|
|
||||||
help='just flash, do not convert')
|
|
||||||
parser.add_argument('-w', '--wait', action='store_true',
|
|
||||||
help='wait for device to flash')
|
|
||||||
parser.add_argument('-C', '--carray', action='store_true',
|
|
||||||
help='convert binary file to a C array, not UF2')
|
|
||||||
parser.add_argument('-i', '--info', action='store_true',
|
|
||||||
help='display header information from UF2, do not convert')
|
|
||||||
args = parser.parse_args()
|
|
||||||
appstartaddr = int(args.base, 0)
|
|
||||||
|
|
||||||
families = load_families()
|
|
||||||
|
|
||||||
if args.family.upper() in families:
|
|
||||||
familyid = families[args.family.upper()]
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
familyid = int(args.family, 0)
|
|
||||||
except ValueError:
|
|
||||||
error("Family ID needs to be a number or one of: " + ", ".join(families.keys()))
|
|
||||||
|
|
||||||
if args.list:
|
|
||||||
list_drives()
|
|
||||||
else:
|
|
||||||
if not args.input:
|
|
||||||
error("Need input file")
|
|
||||||
with open(args.input, mode='rb') as f:
|
|
||||||
inpbuf = f.read()
|
|
||||||
from_uf2 = is_uf2(inpbuf)
|
|
||||||
ext = "uf2"
|
|
||||||
if args.deploy:
|
|
||||||
outbuf = inpbuf
|
|
||||||
elif from_uf2 and not args.info:
|
|
||||||
outbuf = convert_from_uf2(inpbuf)
|
|
||||||
ext = "bin"
|
|
||||||
elif from_uf2 and args.info:
|
|
||||||
outbuf = ""
|
|
||||||
convert_from_uf2(inpbuf)
|
|
||||||
elif is_hex(inpbuf):
|
|
||||||
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"))
|
|
||||||
elif args.carray:
|
|
||||||
outbuf = convert_to_carray(inpbuf)
|
|
||||||
ext = "h"
|
|
||||||
else:
|
|
||||||
outbuf = convert_to_uf2(inpbuf)
|
|
||||||
if not args.deploy and not args.info:
|
|
||||||
print("Converted to %s, output size: %d, start address: 0x%x" %
|
|
||||||
(ext, len(outbuf), appstartaddr))
|
|
||||||
if args.convert or ext != "uf2":
|
|
||||||
if args.output == None:
|
|
||||||
args.output = "flash." + ext
|
|
||||||
if args.output:
|
|
||||||
write_file(args.output, outbuf)
|
|
||||||
if ext == "uf2" and not args.convert and not args.info:
|
|
||||||
drives = get_drives()
|
|
||||||
if len(drives) == 0:
|
|
||||||
if args.wait:
|
|
||||||
print("Waiting for drive to deploy...")
|
|
||||||
while len(drives) == 0:
|
|
||||||
sleep(0.1)
|
|
||||||
drives = get_drives()
|
|
||||||
elif not args.output:
|
|
||||||
error("No drive to deploy.")
|
|
||||||
for d in drives:
|
|
||||||
print("Flashing %s (%s)" % (d, board_id(d)))
|
|
||||||
write_file(d + "/NEW.UF2", outbuf)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import sys
|
|
||||||
import struct
|
|
||||||
import subprocess
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
import os.path
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
|
|
||||||
UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
|
|
||||||
UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
|
|
||||||
UF2_MAGIC_END = 0x0AB16F30 # Ditto
|
|
||||||
|
|
||||||
INFO_FILE = "/INFO_UF2.TXT"
|
|
||||||
|
|
||||||
appstartaddr = 0x2000
|
|
||||||
familyid = 0x0
|
|
||||||
|
|
||||||
|
|
||||||
def is_uf2(buf):
|
|
||||||
w = struct.unpack("<II", buf[0:8])
|
|
||||||
return w[0] == UF2_MAGIC_START0 and w[1] == UF2_MAGIC_START1
|
|
||||||
|
|
||||||
def is_hex(buf):
|
|
||||||
try:
|
|
||||||
w = buf[0:30].decode("utf-8")
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
return False
|
|
||||||
if w[0] == ':' and re.match(rb"^[:0-9a-fA-F\r\n]+$", buf):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def convert_from_uf2(buf):
|
|
||||||
global appstartaddr
|
|
||||||
global familyid
|
|
||||||
numblocks = len(buf) // 512
|
|
||||||
curraddr = None
|
|
||||||
currfamilyid = None
|
|
||||||
families_found = {}
|
|
||||||
prev_flag = None
|
|
||||||
all_flags_same = True
|
|
||||||
outp = []
|
|
||||||
for blockno in range(numblocks):
|
|
||||||
ptr = blockno * 512
|
|
||||||
block = buf[ptr:ptr + 512]
|
|
||||||
hd = struct.unpack(b"<IIIIIIII", block[0:32])
|
|
||||||
if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
|
|
||||||
print("Skipping block at " + ptr + "; bad magic")
|
|
||||||
continue
|
|
||||||
if hd[2] & 1:
|
|
||||||
# NO-flash flag set; skip block
|
|
||||||
continue
|
|
||||||
datalen = hd[4]
|
|
||||||
if datalen > 476:
|
|
||||||
assert False, "Invalid UF2 data size at " + ptr
|
|
||||||
newaddr = hd[3]
|
|
||||||
if (hd[2] & 0x2000) and (currfamilyid == None):
|
|
||||||
currfamilyid = hd[7]
|
|
||||||
if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid):
|
|
||||||
currfamilyid = hd[7]
|
|
||||||
curraddr = newaddr
|
|
||||||
if familyid == 0x0 or familyid == hd[7]:
|
|
||||||
appstartaddr = newaddr
|
|
||||||
padding = newaddr - curraddr
|
|
||||||
if padding < 0:
|
|
||||||
assert False, "Block out of order at " + ptr
|
|
||||||
if padding > 10*1024*1024:
|
|
||||||
assert False, "More than 10M of padding needed at " + ptr
|
|
||||||
if padding % 4 != 0:
|
|
||||||
assert False, "Non-word padding size at " + ptr
|
|
||||||
while padding > 0:
|
|
||||||
padding -= 4
|
|
||||||
outp.append(b"\x00\x00\x00\x00")
|
|
||||||
if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]):
|
|
||||||
outp.append(block[32 : 32 + datalen])
|
|
||||||
curraddr = newaddr + datalen
|
|
||||||
if hd[2] & 0x2000:
|
|
||||||
if hd[7] in families_found.keys():
|
|
||||||
if families_found[hd[7]] > newaddr:
|
|
||||||
families_found[hd[7]] = newaddr
|
|
||||||
else:
|
|
||||||
families_found[hd[7]] = newaddr
|
|
||||||
if prev_flag == None:
|
|
||||||
prev_flag = hd[2]
|
|
||||||
if prev_flag != hd[2]:
|
|
||||||
all_flags_same = False
|
|
||||||
if blockno == (numblocks - 1):
|
|
||||||
print("--- UF2 File Header Info ---")
|
|
||||||
families = load_families()
|
|
||||||
for family_hex in families_found.keys():
|
|
||||||
family_short_name = ""
|
|
||||||
for name, value in families.items():
|
|
||||||
if value == family_hex:
|
|
||||||
family_short_name = name
|
|
||||||
print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex))
|
|
||||||
print("Target Address is 0x{:08x}".format(families_found[family_hex]))
|
|
||||||
if all_flags_same:
|
|
||||||
print("All block flag values consistent, 0x{:04x}".format(hd[2]))
|
|
||||||
else:
|
|
||||||
print("Flags were not all the same")
|
|
||||||
print("----------------------------")
|
|
||||||
if len(families_found) > 1 and familyid == 0x0:
|
|
||||||
outp = []
|
|
||||||
appstartaddr = 0x0
|
|
||||||
return b"".join(outp)
|
|
||||||
|
|
||||||
def convert_to_carray(file_content):
|
|
||||||
outp = "const unsigned long bindata_len = %d;\n" % len(file_content)
|
|
||||||
outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {"
|
|
||||||
for i in range(len(file_content)):
|
|
||||||
if i % 16 == 0:
|
|
||||||
outp += "\n"
|
|
||||||
outp += "0x%02x, " % file_content[i]
|
|
||||||
outp += "\n};\n"
|
|
||||||
return bytes(outp, "utf-8")
|
|
||||||
|
|
||||||
def convert_to_uf2(file_content):
|
|
||||||
global familyid
|
|
||||||
datapadding = b""
|
|
||||||
while len(datapadding) < 512 - 256 - 32 - 4:
|
|
||||||
datapadding += b"\x00\x00\x00\x00"
|
|
||||||
numblocks = (len(file_content) + 255) // 256
|
|
||||||
outp = []
|
|
||||||
for blockno in range(numblocks):
|
|
||||||
ptr = 256 * blockno
|
|
||||||
chunk = file_content[ptr:ptr + 256]
|
|
||||||
flags = 0x0
|
|
||||||
if familyid:
|
|
||||||
flags |= 0x2000
|
|
||||||
hd = struct.pack(b"<IIIIIIII",
|
|
||||||
UF2_MAGIC_START0, UF2_MAGIC_START1,
|
|
||||||
flags, ptr + appstartaddr, 256, blockno, numblocks, familyid)
|
|
||||||
while len(chunk) < 256:
|
|
||||||
chunk += b"\x00"
|
|
||||||
block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END)
|
|
||||||
assert len(block) == 512
|
|
||||||
outp.append(block)
|
|
||||||
return b"".join(outp)
|
|
||||||
|
|
||||||
class Block:
|
|
||||||
def __init__(self, addr, default_data=0xFF):
|
|
||||||
self.addr = addr
|
|
||||||
self.bytes = bytearray([default_data] * 256)
|
|
||||||
|
|
||||||
def encode(self, blockno, numblocks):
|
|
||||||
global familyid
|
|
||||||
flags = 0x0
|
|
||||||
if familyid:
|
|
||||||
flags |= 0x2000
|
|
||||||
hd = struct.pack("<IIIIIIII",
|
|
||||||
UF2_MAGIC_START0, UF2_MAGIC_START1,
|
|
||||||
flags, self.addr, 256, blockno, numblocks, familyid)
|
|
||||||
hd += self.bytes[0:256]
|
|
||||||
while len(hd) < 512 - 4:
|
|
||||||
hd += b"\x00"
|
|
||||||
hd += struct.pack("<I", UF2_MAGIC_END)
|
|
||||||
return hd
|
|
||||||
|
|
||||||
def convert_from_hex_to_uf2(buf):
|
|
||||||
global appstartaddr
|
|
||||||
appstartaddr = None
|
|
||||||
upper = 0
|
|
||||||
currblock = None
|
|
||||||
blocks = []
|
|
||||||
for line in buf.split('\n'):
|
|
||||||
if line[0] != ":":
|
|
||||||
continue
|
|
||||||
i = 1
|
|
||||||
rec = []
|
|
||||||
while i < len(line) - 1:
|
|
||||||
rec.append(int(line[i:i+2], 16))
|
|
||||||
i += 2
|
|
||||||
tp = rec[3]
|
|
||||||
if tp == 4:
|
|
||||||
upper = ((rec[4] << 8) | rec[5]) << 16
|
|
||||||
elif tp == 2:
|
|
||||||
upper = ((rec[4] << 8) | rec[5]) << 4
|
|
||||||
elif tp == 1:
|
|
||||||
break
|
|
||||||
elif tp == 0:
|
|
||||||
addr = upper + ((rec[1] << 8) | rec[2])
|
|
||||||
if appstartaddr == None:
|
|
||||||
appstartaddr = addr
|
|
||||||
i = 4
|
|
||||||
while i < len(rec) - 1:
|
|
||||||
if not currblock or currblock.addr & ~0xff != addr & ~0xff:
|
|
||||||
currblock = Block(addr & ~0xff)
|
|
||||||
blocks.append(currblock)
|
|
||||||
currblock.bytes[addr & 0xff] = rec[i]
|
|
||||||
addr += 1
|
|
||||||
i += 1
|
|
||||||
numblocks = len(blocks)
|
|
||||||
resfile = b""
|
|
||||||
for i in range(0, numblocks):
|
|
||||||
resfile += blocks[i].encode(i, numblocks)
|
|
||||||
return resfile
|
|
||||||
|
|
||||||
def to_str(b):
|
|
||||||
return b.decode("utf-8")
|
|
||||||
|
|
||||||
def get_drives():
|
|
||||||
drives = []
|
|
||||||
if sys.platform == "win32":
|
|
||||||
r = subprocess.check_output([
|
|
||||||
"powershell",
|
|
||||||
"-Command",
|
|
||||||
'(Get-WmiObject Win32_LogicalDisk -Filter "VolumeName=\'RPI-RP2\'").DeviceID'
|
|
||||||
])
|
|
||||||
drive = to_str(r).strip()
|
|
||||||
if drive:
|
|
||||||
drives.append(drive)
|
|
||||||
else:
|
|
||||||
searchpaths = ["/mnt", "/media"]
|
|
||||||
if sys.platform == "darwin":
|
|
||||||
searchpaths = ["/Volumes"]
|
|
||||||
elif sys.platform == "linux":
|
|
||||||
searchpaths += ["/media/" + os.environ["USER"], "/run/media/" + os.environ["USER"]]
|
|
||||||
if "SUDO_USER" in os.environ.keys():
|
|
||||||
searchpaths += ["/media/" + os.environ["SUDO_USER"]]
|
|
||||||
searchpaths += ["/run/media/" + os.environ["SUDO_USER"]]
|
|
||||||
|
|
||||||
for rootpath in searchpaths:
|
|
||||||
if os.path.isdir(rootpath):
|
|
||||||
for d in os.listdir(rootpath):
|
|
||||||
if os.path.isdir(os.path.join(rootpath, d)):
|
|
||||||
drives.append(os.path.join(rootpath, d))
|
|
||||||
|
|
||||||
|
|
||||||
def has_info(d):
|
|
||||||
try:
|
|
||||||
return os.path.isfile(d + INFO_FILE)
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return list(filter(has_info, drives))
|
|
||||||
|
|
||||||
|
|
||||||
def board_id(path):
|
|
||||||
with open(path + INFO_FILE, mode='r') as file:
|
|
||||||
file_content = file.read()
|
|
||||||
return re.search(r"Board-ID: ([^\r\n]*)", file_content).group(1)
|
|
||||||
|
|
||||||
|
|
||||||
def list_drives():
|
|
||||||
for d in get_drives():
|
|
||||||
print(d, board_id(d))
|
|
||||||
|
|
||||||
|
|
||||||
def write_file(name, buf):
|
|
||||||
with open(name, "wb") as f:
|
|
||||||
f.write(buf)
|
|
||||||
print("Wrote %d bytes to %s" % (len(buf), name))
|
|
||||||
|
|
||||||
|
|
||||||
def load_families():
|
|
||||||
# The expectation is that the `uf2families.json` file is in the same
|
|
||||||
# directory as this script. Make a path that works using `__file__`
|
|
||||||
# which contains the full path to this script.
|
|
||||||
filename = "uf2families.json"
|
|
||||||
pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
|
|
||||||
with open(pathname) as f:
|
|
||||||
raw_families = json.load(f)
|
|
||||||
|
|
||||||
families = {}
|
|
||||||
for family in raw_families:
|
|
||||||
families[family["short_name"]] = int(family["id"], 0)
|
|
||||||
|
|
||||||
return families
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
global appstartaddr, familyid
|
|
||||||
def error(msg):
|
|
||||||
print(msg, file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.')
|
|
||||||
parser.add_argument('input', metavar='INPUT', type=str, nargs='?',
|
|
||||||
help='input file (HEX, BIN or UF2)')
|
|
||||||
parser.add_argument('-b', '--base', dest='base', type=str,
|
|
||||||
default="0x2000",
|
|
||||||
help='set base address of application for BIN format (default: 0x2000)')
|
|
||||||
parser.add_argument('-f', '--family', dest='family', type=str,
|
|
||||||
default="0x0",
|
|
||||||
help='specify familyID - number or name (default: 0x0)')
|
|
||||||
parser.add_argument('-o', '--output', metavar="FILE", dest='output', type=str,
|
|
||||||
help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible')
|
|
||||||
parser.add_argument('-d', '--device', dest="device_path",
|
|
||||||
help='select a device path to flash')
|
|
||||||
parser.add_argument('-l', '--list', action='store_true',
|
|
||||||
help='list connected devices')
|
|
||||||
parser.add_argument('-c', '--convert', action='store_true',
|
|
||||||
help='do not flash, just convert')
|
|
||||||
parser.add_argument('-D', '--deploy', action='store_true',
|
|
||||||
help='just flash, do not convert')
|
|
||||||
parser.add_argument('-w', '--wait', action='store_true',
|
|
||||||
help='wait for device to flash')
|
|
||||||
parser.add_argument('-C', '--carray', action='store_true',
|
|
||||||
help='convert binary file to a C array, not UF2')
|
|
||||||
parser.add_argument('-i', '--info', action='store_true',
|
|
||||||
help='display header information from UF2, do not convert')
|
|
||||||
args = parser.parse_args()
|
|
||||||
appstartaddr = int(args.base, 0)
|
|
||||||
|
|
||||||
families = load_families()
|
|
||||||
|
|
||||||
if args.family.upper() in families:
|
|
||||||
familyid = families[args.family.upper()]
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
familyid = int(args.family, 0)
|
|
||||||
except ValueError:
|
|
||||||
error("Family ID needs to be a number or one of: " + ", ".join(families.keys()))
|
|
||||||
|
|
||||||
if args.list:
|
|
||||||
list_drives()
|
|
||||||
else:
|
|
||||||
if not args.input:
|
|
||||||
error("Need input file")
|
|
||||||
with open(args.input, mode='rb') as f:
|
|
||||||
inpbuf = f.read()
|
|
||||||
from_uf2 = is_uf2(inpbuf)
|
|
||||||
ext = "uf2"
|
|
||||||
if args.deploy:
|
|
||||||
outbuf = inpbuf
|
|
||||||
elif from_uf2 and not args.info:
|
|
||||||
outbuf = convert_from_uf2(inpbuf)
|
|
||||||
ext = "bin"
|
|
||||||
elif from_uf2 and args.info:
|
|
||||||
outbuf = ""
|
|
||||||
convert_from_uf2(inpbuf)
|
|
||||||
elif is_hex(inpbuf):
|
|
||||||
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"))
|
|
||||||
elif args.carray:
|
|
||||||
outbuf = convert_to_carray(inpbuf)
|
|
||||||
ext = "h"
|
|
||||||
else:
|
|
||||||
outbuf = convert_to_uf2(inpbuf)
|
|
||||||
if not args.deploy and not args.info:
|
|
||||||
print("Converted to %s, output size: %d, start address: 0x%x" %
|
|
||||||
(ext, len(outbuf), appstartaddr))
|
|
||||||
if args.convert or ext != "uf2":
|
|
||||||
if args.output == None:
|
|
||||||
args.output = "flash." + ext
|
|
||||||
if args.output:
|
|
||||||
write_file(args.output, outbuf)
|
|
||||||
if ext == "uf2" and not args.convert and not args.info:
|
|
||||||
drives = get_drives()
|
|
||||||
if len(drives) == 0:
|
|
||||||
if args.wait:
|
|
||||||
print("Waiting for drive to deploy...")
|
|
||||||
while len(drives) == 0:
|
|
||||||
sleep(0.1)
|
|
||||||
drives = get_drives()
|
|
||||||
elif not args.output:
|
|
||||||
error("No drive to deploy.")
|
|
||||||
for d in drives:
|
|
||||||
print("Flashing %s (%s)" % (d, board_id(d)))
|
|
||||||
write_file(d + "/NEW.UF2", outbuf)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import sys
|
|
||||||
import struct
|
|
||||||
import subprocess
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
import os.path
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
|
|
||||||
UF2_MAGIC_START0 = 0x0A324655 # "UF2\n"
|
|
||||||
UF2_MAGIC_START1 = 0x9E5D5157 # Randomly selected
|
|
||||||
UF2_MAGIC_END = 0x0AB16F30 # Ditto
|
|
||||||
|
|
||||||
INFO_FILE = "/INFO_UF2.TXT"
|
|
||||||
|
|
||||||
appstartaddr = 0x2000
|
|
||||||
familyid = 0x0
|
|
||||||
|
|
||||||
|
|
||||||
def is_uf2(buf):
|
|
||||||
w = struct.unpack("<II", buf[0:8])
|
|
||||||
return w[0] == UF2_MAGIC_START0 and w[1] == UF2_MAGIC_START1
|
|
||||||
|
|
||||||
def is_hex(buf):
|
|
||||||
try:
|
|
||||||
w = buf[0:30].decode("utf-8")
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
return False
|
|
||||||
if w[0] == ':' and re.match(rb"^[:0-9a-fA-F\r\n]+$", buf):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def convert_from_uf2(buf):
|
|
||||||
global appstartaddr
|
|
||||||
global familyid
|
|
||||||
numblocks = len(buf) // 512
|
|
||||||
curraddr = None
|
|
||||||
currfamilyid = None
|
|
||||||
families_found = {}
|
|
||||||
prev_flag = None
|
|
||||||
all_flags_same = True
|
|
||||||
outp = []
|
|
||||||
for blockno in range(numblocks):
|
|
||||||
ptr = blockno * 512
|
|
||||||
block = buf[ptr:ptr + 512]
|
|
||||||
hd = struct.unpack(b"<IIIIIIII", block[0:32])
|
|
||||||
if hd[0] != UF2_MAGIC_START0 or hd[1] != UF2_MAGIC_START1:
|
|
||||||
print("Skipping block at " + ptr + "; bad magic")
|
|
||||||
continue
|
|
||||||
if hd[2] & 1:
|
|
||||||
# NO-flash flag set; skip block
|
|
||||||
continue
|
|
||||||
datalen = hd[4]
|
|
||||||
if datalen > 476:
|
|
||||||
assert False, "Invalid UF2 data size at " + ptr
|
|
||||||
newaddr = hd[3]
|
|
||||||
if (hd[2] & 0x2000) and (currfamilyid == None):
|
|
||||||
currfamilyid = hd[7]
|
|
||||||
if curraddr == None or ((hd[2] & 0x2000) and hd[7] != currfamilyid):
|
|
||||||
currfamilyid = hd[7]
|
|
||||||
curraddr = newaddr
|
|
||||||
if familyid == 0x0 or familyid == hd[7]:
|
|
||||||
appstartaddr = newaddr
|
|
||||||
padding = newaddr - curraddr
|
|
||||||
if padding < 0:
|
|
||||||
assert False, "Block out of order at " + ptr
|
|
||||||
if padding > 10*1024*1024:
|
|
||||||
assert False, "More than 10M of padding needed at " + ptr
|
|
||||||
if padding % 4 != 0:
|
|
||||||
assert False, "Non-word padding size at " + ptr
|
|
||||||
while padding > 0:
|
|
||||||
padding -= 4
|
|
||||||
outp.append(b"\x00\x00\x00\x00")
|
|
||||||
if familyid == 0x0 or ((hd[2] & 0x2000) and familyid == hd[7]):
|
|
||||||
outp.append(block[32 : 32 + datalen])
|
|
||||||
curraddr = newaddr + datalen
|
|
||||||
if hd[2] & 0x2000:
|
|
||||||
if hd[7] in families_found.keys():
|
|
||||||
if families_found[hd[7]] > newaddr:
|
|
||||||
families_found[hd[7]] = newaddr
|
|
||||||
else:
|
|
||||||
families_found[hd[7]] = newaddr
|
|
||||||
if prev_flag == None:
|
|
||||||
prev_flag = hd[2]
|
|
||||||
if prev_flag != hd[2]:
|
|
||||||
all_flags_same = False
|
|
||||||
if blockno == (numblocks - 1):
|
|
||||||
print("--- UF2 File Header Info ---")
|
|
||||||
families = load_families()
|
|
||||||
for family_hex in families_found.keys():
|
|
||||||
family_short_name = ""
|
|
||||||
for name, value in families.items():
|
|
||||||
if value == family_hex:
|
|
||||||
family_short_name = name
|
|
||||||
print("Family ID is {:s}, hex value is 0x{:08x}".format(family_short_name,family_hex))
|
|
||||||
print("Target Address is 0x{:08x}".format(families_found[family_hex]))
|
|
||||||
if all_flags_same:
|
|
||||||
print("All block flag values consistent, 0x{:04x}".format(hd[2]))
|
|
||||||
else:
|
|
||||||
print("Flags were not all the same")
|
|
||||||
print("----------------------------")
|
|
||||||
if len(families_found) > 1 and familyid == 0x0:
|
|
||||||
outp = []
|
|
||||||
appstartaddr = 0x0
|
|
||||||
return b"".join(outp)
|
|
||||||
|
|
||||||
def convert_to_carray(file_content):
|
|
||||||
outp = "const unsigned long bindata_len = %d;\n" % len(file_content)
|
|
||||||
outp += "const unsigned char bindata[] __attribute__((aligned(16))) = {"
|
|
||||||
for i in range(len(file_content)):
|
|
||||||
if i % 16 == 0:
|
|
||||||
outp += "\n"
|
|
||||||
outp += "0x%02x, " % file_content[i]
|
|
||||||
outp += "\n};\n"
|
|
||||||
return bytes(outp, "utf-8")
|
|
||||||
|
|
||||||
def convert_to_uf2(file_content):
|
|
||||||
global familyid
|
|
||||||
datapadding = b""
|
|
||||||
while len(datapadding) < 512 - 256 - 32 - 4:
|
|
||||||
datapadding += b"\x00\x00\x00\x00"
|
|
||||||
numblocks = (len(file_content) + 255) // 256
|
|
||||||
outp = []
|
|
||||||
for blockno in range(numblocks):
|
|
||||||
ptr = 256 * blockno
|
|
||||||
chunk = file_content[ptr:ptr + 256]
|
|
||||||
flags = 0x0
|
|
||||||
if familyid:
|
|
||||||
flags |= 0x2000
|
|
||||||
hd = struct.pack(b"<IIIIIIII",
|
|
||||||
UF2_MAGIC_START0, UF2_MAGIC_START1,
|
|
||||||
flags, ptr + appstartaddr, 256, blockno, numblocks, familyid)
|
|
||||||
while len(chunk) < 256:
|
|
||||||
chunk += b"\x00"
|
|
||||||
block = hd + chunk + datapadding + struct.pack(b"<I", UF2_MAGIC_END)
|
|
||||||
assert len(block) == 512
|
|
||||||
outp.append(block)
|
|
||||||
return b"".join(outp)
|
|
||||||
|
|
||||||
class Block:
|
|
||||||
def __init__(self, addr, default_data=0xFF):
|
|
||||||
self.addr = addr
|
|
||||||
self.bytes = bytearray([default_data] * 256)
|
|
||||||
|
|
||||||
def encode(self, blockno, numblocks):
|
|
||||||
global familyid
|
|
||||||
flags = 0x0
|
|
||||||
if familyid:
|
|
||||||
flags |= 0x2000
|
|
||||||
hd = struct.pack("<IIIIIIII",
|
|
||||||
UF2_MAGIC_START0, UF2_MAGIC_START1,
|
|
||||||
flags, self.addr, 256, blockno, numblocks, familyid)
|
|
||||||
hd += self.bytes[0:256]
|
|
||||||
while len(hd) < 512 - 4:
|
|
||||||
hd += b"\x00"
|
|
||||||
hd += struct.pack("<I", UF2_MAGIC_END)
|
|
||||||
return hd
|
|
||||||
|
|
||||||
def convert_from_hex_to_uf2(buf):
|
|
||||||
global appstartaddr
|
|
||||||
appstartaddr = None
|
|
||||||
upper = 0
|
|
||||||
currblock = None
|
|
||||||
blocks = []
|
|
||||||
for line in buf.split('\n'):
|
|
||||||
if line[0] != ":":
|
|
||||||
continue
|
|
||||||
i = 1
|
|
||||||
rec = []
|
|
||||||
while i < len(line) - 1:
|
|
||||||
rec.append(int(line[i:i+2], 16))
|
|
||||||
i += 2
|
|
||||||
tp = rec[3]
|
|
||||||
if tp == 4:
|
|
||||||
upper = ((rec[4] << 8) | rec[5]) << 16
|
|
||||||
elif tp == 2:
|
|
||||||
upper = ((rec[4] << 8) | rec[5]) << 4
|
|
||||||
elif tp == 1:
|
|
||||||
break
|
|
||||||
elif tp == 0:
|
|
||||||
addr = upper + ((rec[1] << 8) | rec[2])
|
|
||||||
if appstartaddr == None:
|
|
||||||
appstartaddr = addr
|
|
||||||
i = 4
|
|
||||||
while i < len(rec) - 1:
|
|
||||||
if not currblock or currblock.addr & ~0xff != addr & ~0xff:
|
|
||||||
currblock = Block(addr & ~0xff)
|
|
||||||
blocks.append(currblock)
|
|
||||||
currblock.bytes[addr & 0xff] = rec[i]
|
|
||||||
addr += 1
|
|
||||||
i += 1
|
|
||||||
numblocks = len(blocks)
|
|
||||||
resfile = b""
|
|
||||||
for i in range(0, numblocks):
|
|
||||||
resfile += blocks[i].encode(i, numblocks)
|
|
||||||
return resfile
|
|
||||||
|
|
||||||
def to_str(b):
|
|
||||||
return b.decode("utf-8")
|
|
||||||
|
|
||||||
def get_drives():
|
|
||||||
drives = []
|
|
||||||
if sys.platform == "win32":
|
|
||||||
r = subprocess.check_output([
|
|
||||||
"powershell",
|
|
||||||
"-Command",
|
|
||||||
'(Get-WmiObject Win32_LogicalDisk -Filter "VolumeName=\'RPI-RP2\'").DeviceID'
|
|
||||||
])
|
|
||||||
drive = to_str(r).strip()
|
|
||||||
if drive:
|
|
||||||
drives.append(drive)
|
|
||||||
else:
|
|
||||||
searchpaths = ["/mnt", "/media"]
|
|
||||||
if sys.platform == "darwin":
|
|
||||||
searchpaths = ["/Volumes"]
|
|
||||||
elif sys.platform == "linux":
|
|
||||||
searchpaths += ["/media/" + os.environ["USER"], "/run/media/" + os.environ["USER"]]
|
|
||||||
if "SUDO_USER" in os.environ.keys():
|
|
||||||
searchpaths += ["/media/" + os.environ["SUDO_USER"]]
|
|
||||||
searchpaths += ["/run/media/" + os.environ["SUDO_USER"]]
|
|
||||||
|
|
||||||
for rootpath in searchpaths:
|
|
||||||
if os.path.isdir(rootpath):
|
|
||||||
for d in os.listdir(rootpath):
|
|
||||||
if os.path.isdir(os.path.join(rootpath, d)):
|
|
||||||
drives.append(os.path.join(rootpath, d))
|
|
||||||
|
|
||||||
|
|
||||||
def has_info(d):
|
|
||||||
try:
|
|
||||||
return os.path.isfile(d + INFO_FILE)
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return list(filter(has_info, drives))
|
|
||||||
|
|
||||||
|
|
||||||
def board_id(path):
|
|
||||||
with open(path + INFO_FILE, mode='r') as file:
|
|
||||||
file_content = file.read()
|
|
||||||
return re.search(r"Board-ID: ([^\r\n]*)", file_content).group(1)
|
|
||||||
|
|
||||||
|
|
||||||
def list_drives():
|
|
||||||
for d in get_drives():
|
|
||||||
print(d, board_id(d))
|
|
||||||
|
|
||||||
|
|
||||||
def write_file(name, buf):
|
|
||||||
with open(name, "wb") as f:
|
|
||||||
f.write(buf)
|
|
||||||
print("Wrote %d bytes to %s" % (len(buf), name))
|
|
||||||
|
|
||||||
|
|
||||||
def load_families():
|
|
||||||
# The expectation is that the `uf2families.json` file is in the same
|
|
||||||
# directory as this script. Make a path that works using `__file__`
|
|
||||||
# which contains the full path to this script.
|
|
||||||
filename = "uf2families.json"
|
|
||||||
pathname = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename)
|
|
||||||
with open(pathname) as f:
|
|
||||||
raw_families = json.load(f)
|
|
||||||
|
|
||||||
families = {}
|
|
||||||
for family in raw_families:
|
|
||||||
families[family["short_name"]] = int(family["id"], 0)
|
|
||||||
|
|
||||||
return families
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
global appstartaddr, familyid
|
|
||||||
def error(msg):
|
|
||||||
print(msg, file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
parser = argparse.ArgumentParser(description='Convert to UF2 or flash directly.')
|
|
||||||
parser.add_argument('input', metavar='INPUT', type=str, nargs='?',
|
|
||||||
help='input file (HEX, BIN or UF2)')
|
|
||||||
parser.add_argument('-b', '--base', dest='base', type=str,
|
|
||||||
default="0x2000",
|
|
||||||
help='set base address of application for BIN format (default: 0x2000)')
|
|
||||||
parser.add_argument('-f', '--family', dest='family', type=str,
|
|
||||||
default="0x0",
|
|
||||||
help='specify familyID - number or name (default: 0x0)')
|
|
||||||
parser.add_argument('-o', '--output', metavar="FILE", dest='output', type=str,
|
|
||||||
help='write output to named file; defaults to "flash.uf2" or "flash.bin" where sensible')
|
|
||||||
parser.add_argument('-d', '--device', dest="device_path",
|
|
||||||
help='select a device path to flash')
|
|
||||||
parser.add_argument('-l', '--list', action='store_true',
|
|
||||||
help='list connected devices')
|
|
||||||
parser.add_argument('-c', '--convert', action='store_true',
|
|
||||||
help='do not flash, just convert')
|
|
||||||
parser.add_argument('-D', '--deploy', action='store_true',
|
|
||||||
help='just flash, do not convert')
|
|
||||||
parser.add_argument('-w', '--wait', action='store_true',
|
|
||||||
help='wait for device to flash')
|
|
||||||
parser.add_argument('-C', '--carray', action='store_true',
|
|
||||||
help='convert binary file to a C array, not UF2')
|
|
||||||
parser.add_argument('-i', '--info', action='store_true',
|
|
||||||
help='display header information from UF2, do not convert')
|
|
||||||
args = parser.parse_args()
|
|
||||||
appstartaddr = int(args.base, 0)
|
|
||||||
|
|
||||||
families = load_families()
|
|
||||||
|
|
||||||
if args.family.upper() in families:
|
|
||||||
familyid = families[args.family.upper()]
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
familyid = int(args.family, 0)
|
|
||||||
except ValueError:
|
|
||||||
error("Family ID needs to be a number or one of: " + ", ".join(families.keys()))
|
|
||||||
|
|
||||||
if args.list:
|
|
||||||
list_drives()
|
|
||||||
else:
|
|
||||||
if not args.input:
|
|
||||||
error("Need input file")
|
|
||||||
with open(args.input, mode='rb') as f:
|
|
||||||
inpbuf = f.read()
|
|
||||||
from_uf2 = is_uf2(inpbuf)
|
|
||||||
ext = "uf2"
|
|
||||||
if args.deploy:
|
|
||||||
outbuf = inpbuf
|
|
||||||
elif from_uf2 and not args.info:
|
|
||||||
outbuf = convert_from_uf2(inpbuf)
|
|
||||||
ext = "bin"
|
|
||||||
elif from_uf2 and args.info:
|
|
||||||
outbuf = ""
|
|
||||||
convert_from_uf2(inpbuf)
|
|
||||||
elif is_hex(inpbuf):
|
|
||||||
outbuf = convert_from_hex_to_uf2(inpbuf.decode("utf-8"))
|
|
||||||
elif args.carray:
|
|
||||||
outbuf = convert_to_carray(inpbuf)
|
|
||||||
ext = "h"
|
|
||||||
else:
|
|
||||||
outbuf = convert_to_uf2(inpbuf)
|
|
||||||
if not args.deploy and not args.info:
|
|
||||||
print("Converted to %s, output size: %d, start address: 0x%x" %
|
|
||||||
(ext, len(outbuf), appstartaddr))
|
|
||||||
if args.convert or ext != "uf2":
|
|
||||||
if args.output == None:
|
|
||||||
args.output = "flash." + ext
|
|
||||||
if args.output:
|
|
||||||
write_file(args.output, outbuf)
|
|
||||||
if ext == "uf2" and not args.convert and not args.info:
|
|
||||||
drives = get_drives()
|
|
||||||
if len(drives) == 0:
|
|
||||||
if args.wait:
|
|
||||||
print("Waiting for drive to deploy...")
|
|
||||||
while len(drives) == 0:
|
|
||||||
sleep(0.1)
|
|
||||||
drives = get_drives()
|
|
||||||
elif not args.output:
|
|
||||||
error("No drive to deploy.")
|
|
||||||
for d in drives:
|
|
||||||
print("Flashing %s (%s)" % (d, board_id(d)))
|
|
||||||
write_file(d + "/NEW.UF2", outbuf)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -222,6 +222,8 @@ Variables in Embedded Systems: Debugging and Hacking Variables w/ GPIO Output Ba
|
|||||||
|
|
||||||
### Week 4a Notebook [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK04/WEEK04a.md)
|
### Week 4a Notebook [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK04/WEEK04a.md)
|
||||||
|
|
||||||
|
### Week 4-BN Notebook [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK04/WEEK04-BN.md)
|
||||||
|
|
||||||
### RP2350 SVD [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK04/rp2350.svd)
|
### RP2350 SVD [HERE](https://github.com/mytechnotalent/Embedded-Hacking/blob/main/WEEK04/rp2350.svd)
|
||||||
|
|
||||||
### Chapter 5: Intro To Variables
|
### Chapter 5: Intro To Variables
|
||||||
|
|||||||
+1393
File diff suppressed because it is too large.
Load diff
Binary file not shown.
@@ -0,0 +1,277 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Start OpenOCD as a live GDB server for the RP2350 via the Pico Debug Probe.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Starts OpenOCD in the foreground as a long-running GDB server. It exposes
|
||||||
|
rp2350.dap.core0 on 127.0.0.1:3333 and leaves the core RUNNING, so that a
|
||||||
|
debugger (Binary Ninja, or plain GDB) can attach to a target that is already
|
||||||
|
executing and therefore has sane registers.
|
||||||
|
|
||||||
|
This script is deliberately NOT flash.ps1. flash.ps1 programs flash and
|
||||||
|
exits; this one claims the probe and stays up so you can single-step, read
|
||||||
|
memory, and set breakpoints. Do not run both at once -- exactly one process
|
||||||
|
may own the debug probe.
|
||||||
|
|
||||||
|
The script ends with "reset run" rather than OpenOCD's default halt. This is
|
||||||
|
the single most important line in the file; see "Why reset run" below.
|
||||||
|
|
||||||
|
.PARAMETER OCD
|
||||||
|
Optional. Directory containing openocd.exe AND its scripts\ directory.
|
||||||
|
Overrides the PICO_OPENOCD environment variable.
|
||||||
|
|
||||||
|
.ENVIRONMENT
|
||||||
|
PICO_OPENOCD Directory containing openocd.exe AND its scripts\ directory.
|
||||||
|
Default: $env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev
|
||||||
|
The path is used for BOTH the executable and the -s scripts
|
||||||
|
argument. This must be an OpenOCD build matching your machine
|
||||||
|
architecture (x64).
|
||||||
|
ADAPTER_SPEED SWD clock in kHz. Default: 24000.
|
||||||
|
This is a read-mostly debug session, so a fast clock is fine
|
||||||
|
and makes stepping noticeably smoother. Drop it (10000 or
|
||||||
|
lower) if the link is flaky or you are using long dupont
|
||||||
|
wires instead of the probe's own connector.
|
||||||
|
USE_CORE Which cores to expose to the client. Default: 0 (core0 only).
|
||||||
|
Accepted values:
|
||||||
|
0 core0 only <- use this with Binary Ninja
|
||||||
|
1 core1 only <- rarely useful
|
||||||
|
SMP both cores as hwthreads <- see "Why USE_CORE=0"
|
||||||
|
Do not change this to SMP when driving Binary Ninja. The
|
||||||
|
explanation below is the whole reason this default is 0.
|
||||||
|
BP_ADDR OPTIONAL address to halt at during the startup run, as 0x
|
||||||
|
prefixed hex, e.g. 0x10000234 for main. Unset by default,
|
||||||
|
which is the normal "attach to a running target" behaviour.
|
||||||
|
Arms a 2-byte hardware execute breakpoint just before the
|
||||||
|
final "reset run", so the core runs from the vector table
|
||||||
|
and stops there on its own, with no client attached yet.
|
||||||
|
See "Why BP_ADDR is one-shot" below for the important
|
||||||
|
limitation. Arming a length of 2 is mandatory: the Cortex-M33
|
||||||
|
comparators are halfword-based and reject anything else.
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
Requirements:
|
||||||
|
* Pico Debug Probe (or any CMSIS-DAP / SWD adapter) connected to the target.
|
||||||
|
* OpenOCD with the rp2350 target script installed. See PICO_OPENOCD.
|
||||||
|
* Exactly one process may own the debug probe.
|
||||||
|
* A firmware image already programmed into flash (use flash.ps1 first).
|
||||||
|
* The Debug Probe must use the WinUSB driver. If OpenOCD reports
|
||||||
|
"unable to open CMSIS-DAP device", install it with Zadig
|
||||||
|
(https://zadig.akeo.ie/), selecting "Debug Probe (CMSIS-DAP)" -> WinUSB.
|
||||||
|
|
||||||
|
Why USE_CORE=0 (core1 must be hidden from the client)
|
||||||
|
The RP2350 has two Cortex-M33 cores. Core1 does not start on its own:
|
||||||
|
nothing in a Pico SDK application releases it from reset unless the program
|
||||||
|
explicitly does so. If you expose it anyway (USE_CORE=SMP), its registers
|
||||||
|
read back as meaningless reset defaults:
|
||||||
|
|
||||||
|
core1: pc 0x000000ec sp 0xf0000000 xpsr 0x09000000 lr 0x00000147
|
||||||
|
core0: pc 0x1000320c sp 0x20081f38 xpsr 0xa9000000 lr 0x100030ab
|
||||||
|
|
||||||
|
Binary Ninja's register widget renders a memory preview for every register,
|
||||||
|
which means it treats each register value as an ADDRESS and reads it. The
|
||||||
|
core1 values above are not real addresses. Each read data-aborts, and
|
||||||
|
OpenOCD responds by tearing down and re-establishing the SWD debug port,
|
||||||
|
logging a pair of lines per fault:
|
||||||
|
|
||||||
|
Error: Failed to read memory at 0xf0000000
|
||||||
|
Info : SWD DPIDR 0x4c013477
|
||||||
|
|
||||||
|
That becomes a self-sustaining loop of roughly 450 fault-and-recover cycles
|
||||||
|
every two seconds, which makes the session unusable. It looks exactly like a
|
||||||
|
broken debugger or a broken firmware. It is neither: it is a configuration
|
||||||
|
mismatch, and hiding core1 removes it completely.
|
||||||
|
|
||||||
|
Why reset run (the target must be released, not halted)
|
||||||
|
On RP2350, halting during reset stops the core at the boot ROM stub BEFORE
|
||||||
|
the stack pointer is loaded:
|
||||||
|
|
||||||
|
xPSR: 0xf9000000 pc: 0x00000088 msp: 0xf0000000 lr: 0xffffffff
|
||||||
|
|
||||||
|
Those values are garbage for every core, including core0. Attaching in that
|
||||||
|
window triggers the identical fault storm described above. Ending this
|
||||||
|
script with "reset run" means the core starts from the vector table and is
|
||||||
|
executing normally by the time you attach, so registers read back correct.
|
||||||
|
|
||||||
|
Consequence for the client: ATTACH WHILE THE TARGET IS RUNNING. Do not
|
||||||
|
press Reset/Restart in Binary Ninja -- it performs reset-halt and puts you
|
||||||
|
back in the garbage window. If you must reset, send "reset run" over the
|
||||||
|
OpenOCD telnet port (4444) instead. See WEEK04\WEEK04-BN.md.
|
||||||
|
|
||||||
|
Why BP_ADDR is one-shot (read this before relying on it)
|
||||||
|
A breakpoint armed here DOES fire during the startup "reset run" and halts
|
||||||
|
the core at your address, so the server comes up parked there and your
|
||||||
|
debugger can simply attach and look at it. That part works.
|
||||||
|
|
||||||
|
What you do NOT get is a reusable breakpoint. As soon as any GDB client
|
||||||
|
connects, OpenOCD unconditionally flushes every breakpoint it is holding:
|
||||||
|
|
||||||
|
Info : accepting 'gdb' connection on tcp/3333
|
||||||
|
Debug: breakpoints.c:328 breakpoint_remove_all_internal():
|
||||||
|
[rp2350.dap.core0] Delete all breakpoints
|
||||||
|
|
||||||
|
So by the time Binary Ninja is up, the comparator is gone -- reading
|
||||||
|
0xE0002000 shows zeros, not your address. Consequences:
|
||||||
|
|
||||||
|
* The startup stop is single-use. You cannot Resume and re-catch the
|
||||||
|
same address.
|
||||||
|
* You cannot use BP_ADDR to stop in a loop that is already running,
|
||||||
|
because the core only passes that point once per reset.
|
||||||
|
* main (0x10000234) is a good BP_ADDR value precisely because it is
|
||||||
|
reached exactly once, right after reset.
|
||||||
|
|
||||||
|
To arm anything further, or to re-arm main, do it from the OpenOCD command
|
||||||
|
port AFTER your debugger has connected -- the order is the whole trick:
|
||||||
|
|
||||||
|
nc 127.0.0.1 4444
|
||||||
|
> bp 0x1000023e 2 hw
|
||||||
|
> reset run
|
||||||
|
|
||||||
|
Arming before the client connects does not work (flushed above), and in
|
||||||
|
plain GDB the equivalent "hbreak" is cleared by "detach" -- both were
|
||||||
|
verified by reading the FPB comparator registers back.
|
||||||
|
|
||||||
|
Related Binary Ninja bug: Binary Ninja's own breakpoints cannot be used on
|
||||||
|
this target at all. It sends Z0,<addr>,1 -- a 1-byte packet -- and
|
||||||
|
OpenOCD answers "only breakpoints of two bytes length supported". Both
|
||||||
|
Toggle Breakpoint (F2) and Add Hardware Breakpoint (F3) fail, at every
|
||||||
|
address, and the dialog's Size field is disabled so there is no UI way
|
||||||
|
around it. gdb_breakpoint_override hard does not change this. Hence the
|
||||||
|
command-port workflow described above.
|
||||||
|
|
||||||
|
Why the remaining OpenOCD flags are set
|
||||||
|
gdb_breakpoint_override hard
|
||||||
|
Force every client breakpoint onto the Cortex-M33 hardware comparators
|
||||||
|
(the RP2350 has 8 breakpoints and 4 watchpoints). Without this, a
|
||||||
|
client may try to write a BKPT instruction into flash at 0x10000000,
|
||||||
|
which is read-only XIP memory, and the write fails.
|
||||||
|
gdb_memory_map disable
|
||||||
|
Stops the client probing the entire 32 MiB flash map on connect. Pure
|
||||||
|
Raspberry Pi guidance for suppressing spurious "Failed to read memory"
|
||||||
|
reports during target discovery.
|
||||||
|
cortex_m reset_config sysresetreq
|
||||||
|
The Cortex-M33 in the RP2350 has no VECTRESET. Without this, OpenOCD
|
||||||
|
warns on every reset:
|
||||||
|
VECTRESET is not supported on this Cortex-M core, using SYSRESETREQ
|
||||||
|
instead
|
||||||
|
NOTE: this MUST be the generic "cortex_m" command, not
|
||||||
|
"rp2350.dap.core1 cortex_m ...". With USE_CORE=0 the core1 target does
|
||||||
|
not exist, so a core1-scoped command aborts OpenOCD before "init" runs,
|
||||||
|
and the script exits silently having printed nothing useful.
|
||||||
|
adapter speed
|
||||||
|
See ADAPTER_SPEED above.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\debug-server.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
$env:ADAPTER_SPEED=10000; .\debug-server.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
$env:PICO_OPENOCD="C:\openocd\bin"; .\debug-server.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
$env:BP_ADDR="0x10000234"; .\debug-server.ps1
|
||||||
|
Comes up halted at main (one-shot; see "Why BP_ADDR is one-shot").
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
Exit status:
|
||||||
|
* propagated from OpenOCD. A clean shutdown via Ctrl-C exits 0; an
|
||||||
|
OpenOCD configuration error exits non-zero.
|
||||||
|
|
||||||
|
Related scripts:
|
||||||
|
flash.ps1 one-shot raw .bin programmer (exits when done)
|
||||||
|
|
||||||
|
See also:
|
||||||
|
WEEK04\WEEK04-BN.md the full walkthrough this script belongs to
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter()]
|
||||||
|
[string]$OCD
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# --- configuration ---------------------------------------------------------
|
||||||
|
|
||||||
|
if (-not $OCD) { $OCD = $env:PICO_OPENOCD }
|
||||||
|
if (-not $OCD) { $OCD = "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev" }
|
||||||
|
|
||||||
|
$SPEED = if ($env:ADAPTER_SPEED) { $env:ADAPTER_SPEED } else { "24000" }
|
||||||
|
$USE_CORE = if ($env:USE_CORE) { $env:USE_CORE } else { "0" }
|
||||||
|
$BP_ADDR = if ($env:BP_ADDR) { $env:BP_ADDR.Trim() } else { "" }
|
||||||
|
|
||||||
|
if ($BP_ADDR -and $BP_ADDR -notmatch '^0x[0-9a-fA-F]+$') {
|
||||||
|
Write-Error "BP_ADDR must be 0x-prefixed hex, e.g. 0x10000234 (got '$BP_ADDR')"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path "$OCD\openocd.exe")) {
|
||||||
|
Write-Error "OpenOCD not found at $OCD\openocd.exe (set PICO_OPENOCD or -OCD to the directory containing openocd.exe)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- announce, so the operator can verify intent before the target is touched --
|
||||||
|
|
||||||
|
Write-Host "Starting OpenOCD GDB server on 127.0.0.1:3333 using $OCD\openocd.exe"
|
||||||
|
Write-Host "SWD adapter speed: $SPEED kHz"
|
||||||
|
Write-Host "Cores exposed to GDB (USE_CORE): $USE_CORE"
|
||||||
|
Write-Host "Target will be reset and released (reset run) - attach while it is running."
|
||||||
|
|
||||||
|
if ($BP_ADDR) {
|
||||||
|
Write-Host "Startup breakpoint at $BP_ADDR (2-byte hardware execute, one-shot)."
|
||||||
|
Write-Host " It fires during this startup reset run. Any client connecting later"
|
||||||
|
Write-Host " causes OpenOCD to delete it, so arm further breakpoints on port 4444"
|
||||||
|
Write-Host " after attaching: bp <addr> 2 hw"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- start -----------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Order matters: USE_CORE must be set BEFORE -f target/rp2350.cfg is read,
|
||||||
|
# because the target script branches on it when creating the DAP targets.
|
||||||
|
#
|
||||||
|
# "reset run" is last so it happens after init and after the target is
|
||||||
|
# examined, releasing the core rather than halting it. See .NOTES for why.
|
||||||
|
#
|
||||||
|
# Any BP_ADDR breakpoint is inserted after "init" (the target must exist before
|
||||||
|
# a comparator can be programmed) but before "reset run" (so the core is already
|
||||||
|
# armed when it starts). The length must be 2: Cortex-M33 comparators reject
|
||||||
|
# other widths.
|
||||||
|
|
||||||
|
$ocdArgs = @(
|
||||||
|
"-s", "$OCD\scripts",
|
||||||
|
"-f", "interface/cmsis-dap.cfg",
|
||||||
|
"-c", "set USE_CORE $USE_CORE",
|
||||||
|
"-f", "target/rp2350.cfg",
|
||||||
|
# Drop the hwthread RTOS the RP2350 target script attaches to core0.
|
||||||
|
#
|
||||||
|
# target/rp2350.cfg creates core0 with "-rtos hwthread", which registers a
|
||||||
|
# fake RTOS whose "current thread" is coreid+1 = 1. Binary Ninja single-steps
|
||||||
|
# with the GDB packet "vCont;s" and no thread id, i.e. thread 0. OpenOCD's
|
||||||
|
# gdb_server sees rtos->current_thread (1) != thread_id (0) and takes its
|
||||||
|
# "fake step" path, replying with a stop without ever stepping the core:
|
||||||
|
#
|
||||||
|
# gdb_server.c gdb_handle_vcont_packet(): fake step thread 0
|
||||||
|
#
|
||||||
|
# The result is that Step Into / Step Over in Binary Ninja does nothing: the
|
||||||
|
# PC never moves. Clearing the RTOS removes the mismatch so the step is real.
|
||||||
|
# Harmless for single-core use, which is all this lab does (USE_CORE=0).
|
||||||
|
"-c", "rp2350.dap.core0 configure -rtos none",
|
||||||
|
"-c", "adapter speed $SPEED",
|
||||||
|
"-c", "gdb_memory_map disable",
|
||||||
|
"-c", "gdb_breakpoint_override hard",
|
||||||
|
"-c", "cortex_m reset_config sysresetreq",
|
||||||
|
"-c", "init"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($BP_ADDR) {
|
||||||
|
$ocdArgs += @("-c", "bp $BP_ADDR 2 hw")
|
||||||
|
}
|
||||||
|
|
||||||
|
$ocdArgs += @("-c", "reset run")
|
||||||
|
|
||||||
|
& "$OCD\openocd.exe" @ocdArgs
|
||||||
|
|
||||||
|
exit $LASTEXITCODE
|
||||||
Executable
+259
@@ -0,0 +1,259 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# debug-server.sh - start OpenOCD as a live GDB server for the RP2350 via the Pico Debug Probe.
|
||||||
|
#
|
||||||
|
# Synopsis:
|
||||||
|
# ./debug-server.sh
|
||||||
|
#
|
||||||
|
# Description:
|
||||||
|
# Starts OpenOCD in the foreground as a long-running GDB server. It exposes
|
||||||
|
# rp2350.dap.core0 on 127.0.0.1:3333 and leaves the core RUNNING, so that a
|
||||||
|
# debugger (Binary Ninja, or plain GDB) can attach to a target that is already
|
||||||
|
# executing and therefore has sane registers.
|
||||||
|
#
|
||||||
|
# This script is deliberately NOT flash.sh. flash.sh programs flash and exits;
|
||||||
|
# this one claims the probe and stays up so you can single-step, read memory,
|
||||||
|
# and set breakpoints. Do not run both at once -- exactly one process may own
|
||||||
|
# the debug probe.
|
||||||
|
#
|
||||||
|
# The script ends with "reset run" rather than OpenOCD's default halt. This is
|
||||||
|
# the single most important line in the file; see "Why reset run" below.
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
# - Pico Debug Probe (or any CMSIS-DAP / SWD adapter) connected to the target.
|
||||||
|
# - OpenOCD with the rp2350 target script installed. See PICO_OPENOCD.
|
||||||
|
# - Exactly one process may own the debug probe.
|
||||||
|
# - A firmware image already programmed into flash (use flash.sh first).
|
||||||
|
#
|
||||||
|
# Environment variables:
|
||||||
|
# PICO_OPENOCD Directory containing the openocd binary AND its scripts/
|
||||||
|
# directory. Default: $HOME/.pico-sdk/openocd/0.12.0+dev
|
||||||
|
# macOS note: an OpenOCD on PATH is frequently the x86_64
|
||||||
|
# Homebrew build, which will not run under Rosetta on some
|
||||||
|
# setups and cannot talk to the ARM64 firmware tooling. The
|
||||||
|
# Pico SDK ships an arm64 build; point this at it explicitly.
|
||||||
|
# ADAPTER_SPEED SWD clock in kHz. Default: 24000.
|
||||||
|
# This is a read-mostly debug session, so a fast clock is fine
|
||||||
|
# and makes stepping noticeably smoother. Drop it (10000 or
|
||||||
|
# lower) if the link is flaky or you are using long dupont
|
||||||
|
# wires instead of the probe's own connector.
|
||||||
|
# BP_ADDR OPTIONAL address to halt at during the startup run, as 0x
|
||||||
|
# prefixed hex, e.g. 0x10000234 for main. Unset by default,
|
||||||
|
# which is the normal "attach to a running target" behavior.
|
||||||
|
# Arms a 2-byte hardware execute breakpoint just before the
|
||||||
|
# final "reset run", so the core runs from the vector table
|
||||||
|
# and stops there on its own, with no client attached yet.
|
||||||
|
# See "Why BP_ADDR is one-shot" below for the important
|
||||||
|
# limitation. Arming a length of 2 is mandatory: the Cortex-M33
|
||||||
|
# comparators are halfword-based and reject anything else.
|
||||||
|
# USE_CORE Which cores to expose to the client. Default: 0 (core0 only).
|
||||||
|
# Accepted values:
|
||||||
|
# 0 core0 only <- use this with Binary Ninja
|
||||||
|
# 1 core1 only <- rarely useful
|
||||||
|
# SMP both cores as hwthreads <- see "Why USE_CORE=0"
|
||||||
|
# Do not change this to SMP when driving Binary Ninja. The
|
||||||
|
# explanation below is the whole reason this default is 0.
|
||||||
|
#
|
||||||
|
# Why USE_CORE=0 (core1 must be hidden from the client)
|
||||||
|
# The RP2350 has two Cortex-M33 cores. Core1 does not start on its own: nothing
|
||||||
|
# in a Pico SDK application releases it from reset unless the program
|
||||||
|
# explicitly does so. If you expose it anyway (USE_CORE=SMP), its registers
|
||||||
|
# read back as meaningless reset defaults:
|
||||||
|
#
|
||||||
|
# core1: pc 0x000000ec sp 0xf0000000 xpsr 0x09000000 lr 0x00000147
|
||||||
|
# core0: pc 0x1000320c sp 0x20081f38 xpsr 0xa9000000 lr 0x100030ab
|
||||||
|
#
|
||||||
|
# Binary Ninja's register widget renders a memory preview for every register,
|
||||||
|
# which means it treats each register value as an ADDRESS and reads it. The
|
||||||
|
# core1 values above are not real addresses. Each read data-aborts, and
|
||||||
|
# OpenOCD responds by tearing down and re-establishing the SWD debug port,
|
||||||
|
# logging a pair of lines per fault:
|
||||||
|
#
|
||||||
|
# Error: Failed to read memory at 0xf0000000
|
||||||
|
# Info : SWD DPIDR 0x4c013477
|
||||||
|
#
|
||||||
|
# That becomes a self-sustaining loop of roughly 450 fault-and-recover cycles
|
||||||
|
# every two seconds, which makes the session unusable. It looks exactly like a
|
||||||
|
# broken debugger or a broken firmware. It is neither: it is a configuration
|
||||||
|
# mismatch, and hiding core1 removes it completely.
|
||||||
|
#
|
||||||
|
# Why reset run (the target must be released, not halted)
|
||||||
|
# On RP2350, halting during reset stops the core at the boot ROM stub BEFORE
|
||||||
|
# the stack pointer is loaded:
|
||||||
|
#
|
||||||
|
# xPSR: 0xf9000000 pc: 0x00000088 msp: 0xf0000000 lr: 0xffffffff
|
||||||
|
#
|
||||||
|
# Those values are garbage for every core, including core0. Attaching in that
|
||||||
|
# window triggers the identical fault storm described above. Ending this
|
||||||
|
# script with "reset run" means the core starts from the vector table and is
|
||||||
|
# executing normally by the time you attach, so registers read back correct.
|
||||||
|
#
|
||||||
|
# Consequence for the client: ATTACH WHILE THE TARGET IS RUNNING. Do not
|
||||||
|
# press Reset/Restart in Binary Ninja -- it performs reset-halt and puts you
|
||||||
|
# back in the garbage window. If you must reset, send "reset run" over the
|
||||||
|
# OpenOCD telnet port (4444) instead:
|
||||||
|
#
|
||||||
|
# nc 127.0.0.1 4444 then type: reset run
|
||||||
|
#
|
||||||
|
# ...or use debug-server.sh's restart instructions in WEEK04/WEEK04-BN.md.
|
||||||
|
#
|
||||||
|
# Why BP_ADDR is one-shot (read this before relying on it)
|
||||||
|
# A breakpoint armed here DOES fire during the startup "reset run" and halts
|
||||||
|
# the core at your address, so the server comes up parked there and your
|
||||||
|
# debugger can simply attach and look at it. That part works.
|
||||||
|
#
|
||||||
|
# What you do NOT get is a reusable breakpoint. As soon as any GDB client
|
||||||
|
# connects, OpenOCD unconditionally flushes every breakpoint it is holding:
|
||||||
|
#
|
||||||
|
# Info : accepting 'gdb' connection on tcp/3333
|
||||||
|
# Debug: breakpoints.c:328 breakpoint_remove_all_internal():
|
||||||
|
# [rp2350.dap.core0] Delete all breakpoints
|
||||||
|
#
|
||||||
|
# So by the time Binary Ninja is up, the comparator is gone -- reading
|
||||||
|
# 0xE0002000 shows zeros, not your address. Consequences:
|
||||||
|
#
|
||||||
|
# * The startup stop is single-use. You cannot Resume and re-catch the
|
||||||
|
# same address.
|
||||||
|
# * You cannot use BP_ADDR to stop in a loop that is already running,
|
||||||
|
# because the core only passes that point once per reset.
|
||||||
|
# * main (0x10000234) is a good BP_ADDR value precisely because it is
|
||||||
|
# reached exactly once, right after reset.
|
||||||
|
#
|
||||||
|
# To arm anything further, or to re-arm main, do it from the OpenOCD command
|
||||||
|
# port AFTER your debugger has connected -- the order is the whole trick:
|
||||||
|
#
|
||||||
|
# nc 127.0.0.1 4444
|
||||||
|
# > bp 0x1000023e 2 hw
|
||||||
|
# > reset run
|
||||||
|
#
|
||||||
|
# Arming before the client connects does not work (flushed above), and in
|
||||||
|
# plain GDB the equivalent "hbreak" is cleared by "detach" -- both were
|
||||||
|
# verified by reading the FPB comparator registers back.
|
||||||
|
#
|
||||||
|
# Related Binary Ninja bug: Binary Ninja's own breakpoints cannot be used on
|
||||||
|
# this target at all. It sends Z0,<addr>,1 -- a 1-byte packet -- and OpenOCD
|
||||||
|
# answers "only breakpoints of two bytes length supported". Both Toggle
|
||||||
|
# Breakpoint (F2) and Add Hardware Breakpoint (F3) fail, at every address, and
|
||||||
|
# the dialog's Size field is disabled so there is no UI way around it.
|
||||||
|
# gdb_breakpoint_override hard does not change this. Hence the command-port
|
||||||
|
# workflow described above.
|
||||||
|
#
|
||||||
|
# Why the remaining OpenOCD flags are set
|
||||||
|
# gdb_breakpoint_override hard
|
||||||
|
# Force every client breakpoint onto the Cortex-M33 hardware comparators
|
||||||
|
# (the RP2350 has 8 breakpoints and 4 watchpoints). Without this, a client
|
||||||
|
# may try to write a BKPT instruction into flash at 0x10000000, which is
|
||||||
|
# read-only XIP memory, and the write fails.
|
||||||
|
# gdb_memory_map disable
|
||||||
|
# Stops the client probing the entire 32 MiB flash map on connect. Pure
|
||||||
|
# Raspberry Pi guidance for suppressing spurious "Failed to read memory"
|
||||||
|
# reports during target discovery.
|
||||||
|
# cortex_m reset_config sysresetreq
|
||||||
|
# The Cortex-M33 in the RP2350 has no VECTRESET. Without this, OpenOCD
|
||||||
|
# warns on every reset:
|
||||||
|
# VECTRESET is not supported on this Cortex-M core, using SYSRESETREQ
|
||||||
|
# instead
|
||||||
|
# NOTE: this MUST be the generic "cortex_m" command, not
|
||||||
|
# "rp2350.dap.core1 cortex_m ...". With USE_CORE=0 the core1 target does
|
||||||
|
# not exist, so a core1-scoped command aborts OpenOCD before "init" runs,
|
||||||
|
# and the script exits silently having printed nothing useful.
|
||||||
|
# adapter speed
|
||||||
|
# See ADAPTER_SPEED above.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# ./debug-server.sh
|
||||||
|
# ADAPTER_SPEED=10000 ./debug-server.sh
|
||||||
|
# PICO_OPENOCD=/opt/homebrew/bin ./debug-server.sh
|
||||||
|
# BP_ADDR=0x10000234 ./debug-server.sh
|
||||||
|
# Comes up halted at main (one-shot; see "Why BP_ADDR is one-shot").
|
||||||
|
#
|
||||||
|
# Exit status:
|
||||||
|
# * propagated from OpenOCD. A clean shutdown via Ctrl-C exits 0; an
|
||||||
|
# OpenOCD configuration error exits non-zero.
|
||||||
|
#
|
||||||
|
# Related scripts:
|
||||||
|
# flash.sh / flash.ps1 one-shot raw .bin programmer (exits when done)
|
||||||
|
#
|
||||||
|
# See also:
|
||||||
|
# WEEK04/WEEK04-BN.md the full walkthrough this script belongs to
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- configuration ---------------------------------------------------------
|
||||||
|
|
||||||
|
OCD="${PICO_OPENOCD:-$HOME/.pico-sdk/openocd/0.12.0+dev}"
|
||||||
|
SPEED="${ADAPTER_SPEED:-24000}"
|
||||||
|
USE_CORE="${USE_CORE:-0}"
|
||||||
|
BP_ADDR="${BP_ADDR:-}"
|
||||||
|
|
||||||
|
if [ -n "$BP_ADDR" ] && ! printf '%s' "$BP_ADDR" | grep -qiE '^0x[0-9a-f]+$'; then
|
||||||
|
echo "error: BP_ADDR must be 0x-prefixed hex, e.g. 0x10000234 (got '$BP_ADDR')" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$OCD/openocd" ]; then
|
||||||
|
echo "error: OpenOCD not found at $OCD/openocd" >&2
|
||||||
|
echo " set PICO_OPENOCD=/path/to/openocd (the directory containing the openocd binary)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- announce, so the operator can verify intent before the target is touched --
|
||||||
|
|
||||||
|
echo "Starting OpenOCD GDB server on 127.0.0.1:3333 using $OCD/openocd"
|
||||||
|
echo "SWD adapter speed: ${SPEED} kHz"
|
||||||
|
echo "Cores exposed to GDB (USE_CORE): ${USE_CORE}"
|
||||||
|
echo "Target will be reset and released (reset run) - attach while it is running."
|
||||||
|
|
||||||
|
if [ -n "$BP_ADDR" ]; then
|
||||||
|
echo "Startup breakpoint at ${BP_ADDR} (2-byte hardware execute, one-shot)."
|
||||||
|
echo " It fires during this startup reset run. Any client connecting later"
|
||||||
|
echo " causes OpenOCD to delete it, so arm further breakpoints on port 4444"
|
||||||
|
echo " after attaching: bp <addr> 2 hw"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- start -----------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Order matters: USE_CORE must be set BEFORE -f target/rp2350.cfg is read,
|
||||||
|
# because the target script branches on it when creating the DAP targets.
|
||||||
|
#
|
||||||
|
# "reset run" is last so it happens after init and after the target is
|
||||||
|
# examined, releasing the core rather than halting it. See the header for why.
|
||||||
|
#
|
||||||
|
# Any BP_ADDR breakpoint is inserted after "init" (the target must exist before
|
||||||
|
# a comparator can be programmed) but before "reset run" (so the core is already
|
||||||
|
# armed when it starts). The length must be 2: Cortex-M33 comparators reject
|
||||||
|
# other widths.
|
||||||
|
|
||||||
|
ocd_args=(
|
||||||
|
-s "$OCD/scripts"
|
||||||
|
-f interface/cmsis-dap.cfg
|
||||||
|
-c "set USE_CORE ${USE_CORE}"
|
||||||
|
-f target/rp2350.cfg
|
||||||
|
# Drop the hwthread RTOS the RP2350 target script attaches to core0.
|
||||||
|
#
|
||||||
|
# target/rp2350.cfg creates core0 with "-rtos hwthread", which registers a
|
||||||
|
# fake RTOS whose "current thread" is coreid+1 = 1. Binary Ninja single-steps
|
||||||
|
# with the GDB packet "vCont;s" and no thread id, i.e. thread 0. OpenOCD's
|
||||||
|
# gdb_server sees rtos->current_thread (1) != thread_id (0) and takes its
|
||||||
|
# "fake step" path, replying with a stop without ever stepping the core:
|
||||||
|
#
|
||||||
|
# gdb_server.c gdb_handle_vcont_packet(): fake step thread 0
|
||||||
|
#
|
||||||
|
# The result is that Step Into / Step Over in Binary Ninja does nothing: the
|
||||||
|
# PC never moves. Clearing the RTOS removes the mismatch so the step is real.
|
||||||
|
# Harmless for single-core use, which is all this lab does (USE_CORE=0).
|
||||||
|
-c "rp2350.dap.core0 configure -rtos none"
|
||||||
|
-c "adapter speed ${SPEED}"
|
||||||
|
-c "gdb_memory_map disable"
|
||||||
|
-c "gdb_breakpoint_override hard"
|
||||||
|
-c "cortex_m reset_config sysresetreq"
|
||||||
|
-c "init"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -n "$BP_ADDR" ]; then
|
||||||
|
ocd_args+=(-c "bp ${BP_ADDR} 2 hw")
|
||||||
|
fi
|
||||||
|
|
||||||
|
ocd_args+=(-c "reset run")
|
||||||
|
|
||||||
|
exec "$OCD/openocd" "${ocd_args[@]}"
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Program a raw .bin into RP2350 XIP flash via the Pico Debug Probe and OpenOCD.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Writes a headerless raw binary to the RP2350's external XIP flash starting at
|
||||||
|
physical address 0x10000000, verifies the written bytes by reading them back,
|
||||||
|
then releases the core so the freshly programmed firmware runs.
|
||||||
|
|
||||||
|
A raw .bin carries no address information, no entry point and no section
|
||||||
|
table, so the load address MUST be supplied out of band. On the RP2350 the
|
||||||
|
only correct value is 0x10000000: that is where the boot ROM jumps after
|
||||||
|
pulling the reset vector out of the on-chip XIP window. Flashing anywhere
|
||||||
|
else produces a board that enumerates over USB and then does nothing.
|
||||||
|
|
||||||
|
The target must already be running the stock RP2350 bootrom (the default
|
||||||
|
state after power-up, or after any BOOTSEL + UF2 reflash). OpenOCD attaches
|
||||||
|
to the bootrom over SWD, halts it, programs flash, verifies, and resets.
|
||||||
|
|
||||||
|
This is the macOS / Linux equivalent of flash.sh. The two must stay in
|
||||||
|
lockstep: same base address, same verify, same conservative SWD clock.
|
||||||
|
|
||||||
|
.PARAMETER Bin
|
||||||
|
Mandatory. Path to the raw .bin image to program.
|
||||||
|
|
||||||
|
.ENVIRONMENT
|
||||||
|
PICO_OPENOCD Directory containing openocd.exe AND its scripts\ directory.
|
||||||
|
Default: $env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev
|
||||||
|
The path is used for BOTH the executable and the -s scripts
|
||||||
|
argument.
|
||||||
|
ADAPTER_SPEED SWD clock in kHz. Default: 5000.
|
||||||
|
5000 kHz is deliberately conservative. This is a write path,
|
||||||
|
not a read-only debug session, and a marginal USB cable or
|
||||||
|
long dupont run produces spurious verify failures at higher
|
||||||
|
clocks. Raise it (24000) for read-only work; if
|
||||||
|
"Error: target not halted" or verify mismatches appear,
|
||||||
|
lower it to 1000.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\flash.ps1 -Bin 0x0005_intro-to-variables\build\0x0005_intro-to-variables.bin
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
$env:ADAPTER_SPEED=1000; .\flash.ps1 -Bin build\hacked.bin
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
$env:PICO_OPENOCD="C:\openocd\bin"; .\flash.ps1 -Bin build\hacked.bin
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
Requirements:
|
||||||
|
* Pico Debug Probe (or any CMSIS-DAP / SWD adapter) connected to the target.
|
||||||
|
* OpenOCD with the rp2350 target script installed.
|
||||||
|
* The Debug Probe must use the WinUSB driver. If OpenOCD reports
|
||||||
|
"unable to open CMSIS-DAP device", install it with Zadig
|
||||||
|
(https://zadig.akeo.ie/), selecting "Debug Probe (CMSIS-DAP)" -> WinUSB.
|
||||||
|
* Exactly one process may own the debug probe. Close any other OpenOCD,
|
||||||
|
GDB, or IDE debug session first.
|
||||||
|
|
||||||
|
Exit status:
|
||||||
|
0 flash written, verified, and core released
|
||||||
|
1 input file missing or OpenOCD not found
|
||||||
|
* any other status is propagated from OpenOCD, so a failed verify or a
|
||||||
|
write error is visible to the caller rather than being swallowed.
|
||||||
|
|
||||||
|
Related scripts:
|
||||||
|
debug-server.ps1 long-running GDB server for live debugging with
|
||||||
|
Binary Ninja. Use that instead of this script when you
|
||||||
|
need to single-step.
|
||||||
|
|
||||||
|
See also:
|
||||||
|
WEEK04\WEEK04-BN.md the full walkthrough this script belongs to
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[string]$Bin
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# --- argument validation ---------------------------------------------------
|
||||||
|
|
||||||
|
if (-not (Test-Path -PathType Leaf $Bin)) {
|
||||||
|
Write-Error "file not found: $Bin"
|
||||||
|
Write-Host "build it first: cmake -B build -G Ninja -DPICO_BOARD=pico2 -DPICO_PLATFORM=rp2350 ; cmake --build build"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- toolchain resolution --------------------------------------------------
|
||||||
|
|
||||||
|
$OCD = if ($env:PICO_OPENOCD) { $env:PICO_OPENOCD } else { "$env:USERPROFILE\.pico-sdk\openocd\0.12.0+dev" }
|
||||||
|
if (-not (Test-Path "$OCD\openocd.exe")) {
|
||||||
|
Write-Error "OpenOCD not found at $OCD\openocd.exe (set PICO_OPENOCD to the directory containing openocd.exe)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$SPEED = if ($env:ADAPTER_SPEED) { $env:ADAPTER_SPEED } else { "5000" }
|
||||||
|
|
||||||
|
# --- program ---------------------------------------------------------------
|
||||||
|
|
||||||
|
# OpenOCD flag notes:
|
||||||
|
# -f interface/cmsis-dap.cfg the Pico Debug Probe is a CMSIS-DAP v1 device
|
||||||
|
# -f target/rp2350.cfg RP2350 dual Cortex-M33 + RP2350B0-style DAP;
|
||||||
|
# sets USE_CORE=SMP by default, which is fine here
|
||||||
|
# because we never hand uninitialised core1
|
||||||
|
# registers to a debugger. For live debugging use
|
||||||
|
# debug-server.ps1, which forces USE_CORE=0.
|
||||||
|
# -c "program BIN 0x10000000 verify reset exit"
|
||||||
|
# program the write
|
||||||
|
# 0x10000000 base address (see .DESCRIPTION)
|
||||||
|
# verify read back and compare every byte; a mismatch aborts
|
||||||
|
# reset reset the core so the new image starts at its vectors
|
||||||
|
# exit release the probe and return to the shell
|
||||||
|
#
|
||||||
|
# The adapter speed is deliberately lower here than in debug-server.ps1; see
|
||||||
|
# ADAPTER_SPEED in .ENVIRONMENT.
|
||||||
|
Write-Host "Flashing $Bin -> 0x10000000 using $OCD\openocd.exe (SWD $SPEED kHz)"
|
||||||
|
|
||||||
|
& "$OCD\openocd.exe" `
|
||||||
|
-s "$OCD\scripts" `
|
||||||
|
-f interface/cmsis-dap.cfg `
|
||||||
|
-f target/rp2350.cfg `
|
||||||
|
-c "adapter speed $SPEED" `
|
||||||
|
-c "program $Bin 0x10000000 verify reset exit"
|
||||||
|
|
||||||
|
exit $LASTEXITCODE
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# flash.sh - program a raw .bin into RP2350 XIP flash via the Pico Debug Probe + OpenOCD.
|
||||||
|
#
|
||||||
|
# Synopsis:
|
||||||
|
# ./flash.sh <path-to-file.bin>
|
||||||
|
#
|
||||||
|
# Description:
|
||||||
|
# Writes a headerless raw binary to the RP2350's external XIP flash starting at
|
||||||
|
# physical address 0x10000000, then verifies the written bytes by reading them
|
||||||
|
# back, then releases the core so the freshly programmed firmware runs.
|
||||||
|
#
|
||||||
|
# A raw .bin carries no address information, no entry point and no section
|
||||||
|
# table, so the load address MUST be supplied out of band. On the RP2350 the
|
||||||
|
# only correct value is 0x10000000: that is where the boot ROM jumps after
|
||||||
|
# pulling the reset vector out of the on-chip XIP window. Flashing anywhere
|
||||||
|
# else produces a board that enumerates over USB and then does nothing.
|
||||||
|
#
|
||||||
|
# The target must already be running the stock RP2350 bootrom (the default
|
||||||
|
# state after power-up or after any BOOTSEL+UF2 reflash). OpenOCD attaches to
|
||||||
|
# the bootrom via SWD, halts it, programs flash, verifies, and resets.
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
# - Pico Debug Probe (or any CMSIS-DAP / SWD adapter) connected to the target.
|
||||||
|
# - OpenOCD with the rp2350 target script installed. The Pico SDK ships one;
|
||||||
|
# see PICO_OPENOCD below.
|
||||||
|
# - Exactly one process may own the debug probe. Close any other OpenOCD,
|
||||||
|
# GDB, or IDE debug session first.
|
||||||
|
#
|
||||||
|
# Environment variables:
|
||||||
|
# PICO_OPENOCD Directory containing the openocd binary AND its scripts/
|
||||||
|
# directory. Default: $HOME/.pico-sdk/openocd/0.12.0+dev
|
||||||
|
# The path is used for BOTH the executable and -s scripts.
|
||||||
|
# ADAPTER_SPEED SWD clock in kHz. Default: 5000.
|
||||||
|
# 5000 kHz is deliberately conservative. This is a write path,
|
||||||
|
# not a read-only debug session, and a marginal USB cable or
|
||||||
|
# long dupont run will produce spurious verify failures at
|
||||||
|
# higher clocks. Raise it (24000) for read-only work; if
|
||||||
|
# "Error: target not halted" or verify mismatches appear,
|
||||||
|
# lower it to 1000.
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# ./flash.sh 0x0005_intro-to-variables/build/0x0005_intro-to-variables.bin
|
||||||
|
# ADAPTER_SPEED=1000 ./flash.sh build/hacked.bin
|
||||||
|
# PICO_OPENOCD=/opt/homebrew/bin ./flash.sh build/hacked.bin
|
||||||
|
#
|
||||||
|
# Exit status:
|
||||||
|
# 0 flash written, verified, and core released
|
||||||
|
# 1 bad usage, missing input file, or OpenOCD not found
|
||||||
|
# * any other status is propagated from OpenOCD, so a failed verify or a
|
||||||
|
# write error is visible to the caller rather than being swallowed.
|
||||||
|
#
|
||||||
|
# Related scripts:
|
||||||
|
# debug-server.sh / debug-server.ps1 long-running GDB server for live
|
||||||
|
# debugging with Binary Ninja
|
||||||
|
#
|
||||||
|
# See also:
|
||||||
|
# WEEK04/WEEK04-BN.md the full walkthrough this script belongs to
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- argument validation ---------------------------------------------------
|
||||||
|
|
||||||
|
BIN="${1:-}"
|
||||||
|
if [ -z "$BIN" ]; then
|
||||||
|
echo "usage: $0 <path-to-file.bin>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$BIN" ]; then
|
||||||
|
echo "error: file not found: $BIN" >&2
|
||||||
|
echo " build it first: cmake -B build -G Ninja -DPICO_BOARD=pico2 -DPICO_PLATFORM=rp2350 && cmake --build build" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- toolchain resolution --------------------------------------------------
|
||||||
|
|
||||||
|
OCD="${PICO_OPENOCD:-$HOME/.pico-sdk/openocd/0.12.0+dev}"
|
||||||
|
if [ ! -x "$OCD/openocd" ]; then
|
||||||
|
echo "error: OpenOCD not found at $OCD/openocd" >&2
|
||||||
|
echo " set PICO_OPENOCD=/path/to/openocd (the directory containing the openocd binary)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SPEED="${ADAPTER_SPEED:-5000}"
|
||||||
|
|
||||||
|
# --- program ---------------------------------------------------------------
|
||||||
|
|
||||||
|
# OpenOCD flag notes:
|
||||||
|
# -f interface/cmsis-dap.cfg the Pico Debug Probe is a CMSIS-DAP v1 device
|
||||||
|
# -f target/rp2350.cfg RP2350 dual Cortex-M33 + RP2350B0-style DAP;
|
||||||
|
# sets USE_CORE=SMP by default, which is fine here
|
||||||
|
# because we never hand uninitialised core1
|
||||||
|
# registers to a debugger. For live debugging use
|
||||||
|
# debug-server.sh, which forces USE_CORE=0.
|
||||||
|
# -c "program BIN 0x10000000 verify reset exit"
|
||||||
|
# program the write
|
||||||
|
# 0x10000000 base address (see header)
|
||||||
|
# verify read back and compare every byte; a mismatch aborts
|
||||||
|
# reset reset the core so the new image starts at its vectors
|
||||||
|
# exit release the probe and return to the shell
|
||||||
|
#
|
||||||
|
# The adapter speed is deliberately lower here than in debug-server.sh; see
|
||||||
|
# ADAPTER_SPEED in the header.
|
||||||
|
echo "Flashing $BIN -> 0x10000000 using $OCD/openocd (SWD ${SPEED} kHz)"
|
||||||
|
"$OCD/openocd" \
|
||||||
|
-s "$OCD/scripts" \
|
||||||
|
-f interface/cmsis-dap.cfg \
|
||||||
|
-f target/rp2350.cfg \
|
||||||
|
-c "adapter speed ${SPEED}" \
|
||||||
|
-c "program $BIN 0x10000000 verify reset exit"
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Python standards for this repository.
|
||||||
|
#
|
||||||
|
# Scope note: only the small number of first-party helper scripts at the repo
|
||||||
|
# root are linted. Firmware projects under 0x*/ are C/C++/ASM and are not
|
||||||
|
# Python, and vendored trees (pico-sdk, build/) are excluded explicitly.
|
||||||
|
#
|
||||||
|
# Run:
|
||||||
|
# ruff check .
|
||||||
|
# ruff format .
|
||||||
|
# ruff check --fix .
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "embedded-hacking-tools"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Host-side tooling for the Embedded Hacking course"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py310"
|
||||||
|
line-length = 88
|
||||||
|
# Scope: the first-party host-side helpers at the repo root only. The lesson
|
||||||
|
# trees below are per-lesson material that predates this config and are
|
||||||
|
# intentionally not linted; migrate a file to the repo root, or run ruff on it
|
||||||
|
# directly with an explicit path, when you touch it.
|
||||||
|
extend-exclude = [
|
||||||
|
"0x*",
|
||||||
|
"WEEK*",
|
||||||
|
"**/build",
|
||||||
|
"pico-sdk",
|
||||||
|
"**/pico-sdk",
|
||||||
|
"node_modules",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
# E/W pycodestyle - formatting-independent style
|
||||||
|
# F pyflakes - unused imports, undefined names, f-string gaps
|
||||||
|
# I isort - import ordering
|
||||||
|
# UP pyupgrade - modern syntax for the declared target version
|
||||||
|
# B bugbear - real bug patterns
|
||||||
|
# SIM simplify - redundant code
|
||||||
|
# C4 comprehensions - needless comprehension / generator misuse
|
||||||
|
# RET return - inconsistent returns
|
||||||
|
# PTH pathlib - use pathlib instead of os.path
|
||||||
|
# ARG unused-arguments - dead parameters
|
||||||
|
# D pydocstyle - docstring presence and convention
|
||||||
|
# ANN annotations - type annotations
|
||||||
|
# TRY tryceratops - correct exception handling
|
||||||
|
# PIE misc - misc lints
|
||||||
|
# RUF ruff-specific - modern idioms and correctness rules
|
||||||
|
select = [
|
||||||
|
"E",
|
||||||
|
"F",
|
||||||
|
"I",
|
||||||
|
"UP",
|
||||||
|
"B",
|
||||||
|
"SIM",
|
||||||
|
"C4",
|
||||||
|
"RET",
|
||||||
|
"PTH",
|
||||||
|
"ARG",
|
||||||
|
"D",
|
||||||
|
"ANN",
|
||||||
|
"TRY",
|
||||||
|
"PIE",
|
||||||
|
"RUF",
|
||||||
|
]
|
||||||
|
|
||||||
|
ignore = [
|
||||||
|
# The UF2 block format is a byte-level binary format; several conversions
|
||||||
|
# legitimately deal in loose byte slices and magic integers.
|
||||||
|
"E501",
|
||||||
|
# C901 complexity: convert_from_uf2 is a linear decoder over a block stream
|
||||||
|
# and reads far better as one pass than split into helpers.
|
||||||
|
"C901",
|
||||||
|
# Legacy CLI scripts print progress to stdout as their primary output.
|
||||||
|
"T201",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
# Tutorial scripts are run directly and use a permissive argparse style.
|
||||||
|
"uf2conv.py" = ["D100"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.pydocstyle]
|
||||||
|
convention = "google"
|
||||||
|
|
||||||
|
[tool.ruff.lint.flake8-annotations]
|
||||||
|
# Accept `-> None` omission only on __init__; require annotations elsewhere.
|
||||||
|
suppress-dummy-args = false
|
||||||
|
mypy-init-return = true
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "space"
|
||||||
|
line-ending = "lf"
|
||||||
Reference in new issue
Block a user