Skip to content

Commit ad6cffa

Browse files
committed
pybricksdev._vendored: add pynxt library
This is a partial copy of the pynxt library from [1]. [1]: https://github.com/pybricks/nxos/tree/0d4878705b23e19d341d89e940bfbf4e59d274ea
1 parent 72e3501 commit ad6cffa

File tree

11 files changed

+427
-1
lines changed

11 files changed

+427
-1
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
*.o
44
*.a
55
*.elf
6-
*.bin
76
*.map
87
*.hex
98
*.dis
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# SPDX-License-Identifier: GPL-2.0-only
2+
# Copyright 2006 David Anderson <[email protected]>
3+
4+
"""
5+
PyNXT is a Python module that enables developers to communicate with
6+
Lego Mindstorms NXT bricks at a low level. It currently facilitates
7+
scanning the USB chain for a NXT brick and implements the SAM-BA
8+
bootloader communication protocol. It comes with two utilities, fwflash
9+
and fwexec, which can be used to write a firmware to either flash memory
10+
or RAM, and execute it from there.
11+
"""
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# SPDX-License-Identifier: GPL-2.0-only
2+
# Copyright 2006 David Anderson <[email protected]>
3+
# Copyright 2023 The Pybricks Authors
4+
5+
import struct
6+
7+
8+
class Error(Exception):
9+
"""Exception base class for this program."""
10+
11+
12+
class FileTooSmall(Error):
13+
"""File is too small to be a firmware."""
14+
15+
16+
class FileTooLarge(Error):
17+
"""File is too large to fit in flash memory."""
18+
19+
20+
class InvalidHeader(Error):
21+
"""Firmware has invalid header."""
22+
23+
24+
class Firmware:
25+
FLASH_SIZE = 256 * 1024 # NXT has 256 KiB flash memory
26+
MIN_SIZE = 500 # needs at least a vector table and some init code
27+
HEADER_DEF = "<5L?"
28+
HEADER_SIZE = struct.calcsize(HEADER_DEF)
29+
30+
def __init__(self, fw_and_header: bytes):
31+
if len(fw_and_header) < Firmware.HEADER_SIZE + Firmware.MIN_SIZE:
32+
raise FileTooSmall("firmware is too small to be a valid firmware.")
33+
34+
header = fw_and_header[-self.HEADER_SIZE :]
35+
self.firmware = fw_and_header[: -self.HEADER_SIZE]
36+
37+
(
38+
magic,
39+
self.ramsize,
40+
self.romsize,
41+
self.writeaddr,
42+
self.loadaddr,
43+
self.samba,
44+
) = struct.unpack(self.HEADER_DEF, header)
45+
46+
if magic != 0xDEADBEEF:
47+
raise InvalidHeader("Bad magic on header")
48+
49+
if len(self.firmware) > Firmware.FLASH_SIZE:
50+
raise FileTooLarge(
51+
f"provided firmware is {len(self.firmware)} bytes but flash memory is only {Firmware.FLASH_SIZE} bytes"
52+
)
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# SPDX-License-Identifier: GPL-2.0-only
2+
# Copyright 2006 David Anderson <[email protected]>
3+
4+
import math
5+
6+
from importlib.resources import read_binary
7+
8+
from . import resources
9+
10+
# Mnemonics for the addresses of the various registers used by the flash
11+
# controller.
12+
PMC_MCKR = 0xFFFFFC30 # Master clock register
13+
MC_FMR = 0xFFFFFF60 # Flash mode register
14+
MC_FCR = 0xFFFFFF64 # Flash control register
15+
MC_FSR = 0xFFFFFF68 # Flash status register
16+
17+
# The addresses in ram used by the flash driver.
18+
FLASH_DRIVER_ADDR = 0x202000
19+
FLASH_TARGET_BLOCK_NUM_ADDR = 0x202300
20+
FLASH_BLOCK_DATA_ADDR = 0x202100
21+
22+
# The region lock control bits are non-volatile bits, which have special
23+
# timing requirements compared to the 'regular' flash memory. The
24+
# following constants define the timing settings for lock bits and for
25+
# normal flash memory.
26+
#
27+
# FMCN = 0x5, FWS = 0x1
28+
FLASH_REGION_LOCK_SETTING = (0x5 << 16) | (0x1 << 8)
29+
# FMCN = 0x34, FWS = 0x1
30+
FLASH_MEMORY_WRITE_SETTING = (0x34 << 16) | (0x1 << 8)
31+
32+
# The base command to unlock a flash region, and a helper to generate the
33+
# correct region number.
34+
FLASH_REGION_UNLOCK_CMD = (0x5A << 24) | (0x4)
35+
36+
37+
def _unlock_region(region_num):
38+
# The unlock command must specify the page number of any page within
39+
# the region that you want to unlock. Since each region is 64 pages
40+
# long, we just multiply the page number by 64 to get into the
41+
# correct region.
42+
return FLASH_REGION_UNLOCK_CMD | ((64 * region_num) << 8)
43+
44+
45+
class MissingFlashDriverFile(Exception):
46+
"""Could not find the flash driver firmware image."""
47+
48+
49+
class InvalidFirmwareImage(Exception):
50+
"""The given firmware image cannot be written."""
51+
52+
53+
class FlashController(object):
54+
def __init__(self, brick):
55+
self._brick = brick
56+
57+
def _wait_for_flash(self):
58+
"""Wait for the flash controller to become ready."""
59+
status = self._brick.read_word(MC_FSR)
60+
while not (status & 0x1):
61+
status = self._brick.read_word(MC_FSR)
62+
63+
def _unlock_regions(self):
64+
status = self._brick.read_word(MC_FSR)
65+
if (status & 0xFFFF0000) == 0:
66+
return
67+
68+
self._brick.write_word(MC_FMR, FLASH_REGION_LOCK_SETTING)
69+
for i in range(16):
70+
mask = 1 << (16 + i)
71+
if status & mask:
72+
self._brick.write_word(MC_FCR, _unlock_region(i))
73+
self._wait_for_flash()
74+
self._brick.write_word(MC_FMR, FLASH_MEMORY_WRITE_SETTING)
75+
76+
def _prepare_flash(self):
77+
# Switch to the PLL clock with a /2 prescaler. The timing
78+
# configurations we use are only valid for that configuration.
79+
#
80+
# TODO: The atmel tool does this as well, but according to the
81+
# specs this is horribly wrong: one should absolutely not switch
82+
# clock sources and prescalers in one go, much less switch to
83+
# the PLL without configuring it and stabilizing it first. What
84+
# the hell is this?
85+
self._brick.write_word(PMC_MCKR, 0x7)
86+
87+
# Set the correct timings for flash writes.
88+
self._brick.write_word(MC_FMR, FLASH_MEMORY_WRITE_SETTING)
89+
90+
# Check/unlock all flash regions.
91+
self._unlock_regions()
92+
93+
# Send the flash driver to the brick.
94+
driver = read_binary(resources, resources.FLASH_DRIVER)
95+
self._brick.write_buffer(FLASH_DRIVER_ADDR, driver)
96+
97+
def flash(self, firmware):
98+
self._prepare_flash()
99+
100+
num_pages = int(math.ceil(len(firmware) / 256))
101+
if num_pages > 1024:
102+
raise InvalidFirmwareImage("The firmware image must be smaller than 256kB")
103+
104+
for page_num in range(num_pages):
105+
self._brick.write_word(FLASH_TARGET_BLOCK_NUM_ADDR, page_num)
106+
self._brick.write_buffer(
107+
FLASH_BLOCK_DATA_ADDR, firmware[page_num * 256 : (page_num + 1) * 256]
108+
)
109+
self._brick.jump(FLASH_DRIVER_ADDR)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# SPDX-License-Identifier: GPL-2.0-only
2+
# Copyright 2006 David Anderson <[email protected]>
3+
4+
import usb
5+
import time
6+
7+
USB_BULK_OUT_EP = 0x1
8+
USB_BULK_IN_EP = 0x82
9+
10+
11+
def enumerate_usb():
12+
"""Return a generator yielding all attached USB devices."""
13+
busses = usb.busses()
14+
for bus in busses:
15+
devices = bus.devices
16+
for dev in devices:
17+
yield dev
18+
19+
20+
def get_device(vendor_id, product_id, version=0, timeout=None):
21+
"""Return the first device matching the given vendor/product ID."""
22+
while True:
23+
for dev in enumerate_usb():
24+
if (
25+
dev.idVendor == vendor_id
26+
and dev.idProduct == product_id
27+
and dev.iSerialNumber == version
28+
):
29+
return UsbBrick(dev)
30+
if timeout is None or timeout <= 0:
31+
return None
32+
sleep_time = min(1.0, timeout)
33+
time.sleep(sleep_time)
34+
timeout -= sleep_time
35+
36+
37+
class UsbBrick(object):
38+
def __init__(self, dev):
39+
self._dev = dev
40+
41+
def __del__(self):
42+
try:
43+
self.close()
44+
except Exception:
45+
pass
46+
47+
def open(self, interface, configuration=1, detach_kernel_driver=False):
48+
self._iface = interface
49+
self._config = configuration
50+
self._hdl = self._dev.open()
51+
if detach_kernel_driver:
52+
self._hdl.detachKernelDriver(interface)
53+
self._hdl.setConfiguration(configuration)
54+
self._hdl.claimInterface(interface)
55+
56+
def close(self):
57+
self._hdl.releaseInterface()
58+
del self._hdl
59+
60+
def read(self, size, timeout=100):
61+
"""Read the given amount of data from the device and return it."""
62+
# For some reason, bulkRead returns a tuple of longs. This is
63+
# dumb, so we convert it back to a string before returning,
64+
# kthx.
65+
try:
66+
data = self._hdl.bulkRead(USB_BULK_IN_EP, size, timeout)
67+
except usb.USBError:
68+
return None
69+
return "".join(chr(x) for x in data)
70+
71+
def write(self, data, timeout=100):
72+
"""Write the given amount of data to the device."""
73+
return self._hdl.bulkWrite(USB_BULK_OUT_EP, data, timeout)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#all:
2+
# gcc -Wall -O0 -mcpu=arm7tdmi-s -mapcs -mthumb-interwork -o flash.o flash.c -nostartfiles -nodefaultlibs -nostdlib -Wl,-e,main
3+
# objcopy -Obinary -j.text flash.o flash.bin
4+
# objdump --disassemble-all -bbinary -marm7tdmi flash.bin > flash.asm
5+
#
6+
7+
CC=arm-none-eabi-gcc
8+
AS=arm-none-eabi-as
9+
LD=arm-none-eabi-ld
10+
OBJCOPY=arm-none-eabi-objcopy
11+
12+
TARGET = flash_driver.bin
13+
14+
all:
15+
$(CC) -W -Wall -Os -msoft-float -mcpu=arm7tdmi -mapcs -c -o flash.o flash.c
16+
$(AS) --warn -mfpu=softfpa -mcpu=arm7tdmi -mapcs-32 -o crt0.o crt0.s
17+
$(LD) -Os --gc-sections crt0.o flash.o -o flash.elf
18+
$(OBJCOPY) -O binary flash.elf $(TARGET)
19+
chmod -x $(TARGET)
20+
rm -f flash.o crt0.o flash.elf
21+
22+
clean:
23+
rm -f flash.o crt0.o flash.elf $(TARGET)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# SPDX-License-Identifier: MIT
2+
# Copyright (c) 2021 The Pybricks Authors
3+
4+
"""Resource files.
5+
6+
These resources are intended to be used with the standard ``importlib.resources``
7+
module.
8+
"""
9+
10+
FLASH_DRIVER = "flash_driver.bin"
11+
"""Application running from NXT RAM to install new firmware on flash."""
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* SPDX-License-Identifier: GPL-2.0-or-later */
2+
/* Copyright 2006 David Anderson <[email protected]> */
3+
4+
/**
5+
* NXT bootstrap interface; NXT onboard flashing driver bootstrap.
6+
*/
7+
8+
.text
9+
.align 4
10+
.globl _start
11+
12+
_start:
13+
/* Initialize the stack */
14+
mov sp, #0x210000
15+
16+
/* Preserve old link register */
17+
stmfd sp!, {lr}
18+
19+
/* Call main */
20+
bl do_flash_write
21+
22+
/* Return */
23+
ldmfd sp!, {pc}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* SPDX-License-Identifier: GPL-2.0-or-later */
2+
/* Copyright 2006 David Anderson <[email protected]> */
3+
4+
/**
5+
* NXT bootstrap interface; NXT onboard flashing driver.
6+
*/
7+
8+
#define VINTPTR(addr) ((volatile unsigned int *)(addr))
9+
#define VINT(addr) (*(VINTPTR(addr)))
10+
11+
#define USER_PAGE VINTPTR(0x00202100)
12+
#define USER_PAGE_NUM VINT(0x00202300)
13+
14+
#define FLASH_BASE VINTPTR(0x00100000)
15+
#define FLASH_CMD_REG VINT(0xFFFFFF64)
16+
#define FLASH_STATUS_REG VINT(0xFFFFFF68)
17+
#define OFFSET_PAGE_NUM ((USER_PAGE_NUM & 0x000003FF) << 8)
18+
#define FLASH_CMD_WRITE (0x5A000001 + OFFSET_PAGE_NUM)
19+
20+
void do_flash_write(void)
21+
{
22+
unsigned long i;
23+
24+
while (!(FLASH_STATUS_REG & 0x1));
25+
26+
for (i = 0; i < 64; i++)
27+
FLASH_BASE[(USER_PAGE_NUM*64)+i] = USER_PAGE[i];
28+
29+
FLASH_CMD_REG = FLASH_CMD_WRITE;
30+
31+
while (!(FLASH_STATUS_REG & 0x1));
32+
}
140 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)