Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit de8b6d1

Browse files
committedFeb 20, 2025
Move bootloader code to Modulino
1 parent 91fa5f8 commit de8b6d1

File tree

3 files changed

+81
-75
lines changed

3 files changed

+81
-75
lines changed
 

‎examples/change_address.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from modulino import Modulino
1313

1414
print()
15-
devices = Modulino.available_devices()
15+
bus = None # Change this to the I2C bus you are using on 3rd party host boards
16+
devices = Modulino.available_devices(bus)
1617

1718
if len(devices) == 0:
1819
print("No devices found on the bus. Try resetting the board.")

‎src/modulino/firmware_flasher.py

Lines changed: 55 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313

1414
import os
1515
import sys
16-
from machine import I2C, Pin
1716
import time
1817
from micropython import const
18+
from machine import I2C
19+
from modulino import Modulino
1920

2021
BOOTLOADER_I2C_ADDRESS = const(0x64)
2122
ACK = const(0x79)
@@ -31,55 +32,29 @@
3132

3233
CHUNK_SIZE = const(128) # Size of the memory chunk to write
3334

34-
# Define I2C pins and initialize I2C
35-
i2c = I2C(0, freq=100000)
35+
bus = None # Change this to the I2C bus you are using on 3rd party host boards
3636

37-
def send_reset(address):
38-
"""
39-
Send a reset command to the I2C device at the given address.
40-
41-
:param address: I2C address of the device.
42-
:return: 0 if the reset command was sent successfully, otherwise -1.
43-
"""
44-
buffer = b'DIE'
45-
buffer += b'\x00' * (8 - len(buffer)) # Pad buffer to 8 bytes
46-
47-
try:
48-
print(f"🔄 Resetting device at address {hex(address)}")
49-
i2c.writeto(address, buffer, True)
50-
print("📤 Reset command sent")
51-
time.sleep(0.25) # Wait for the device to reset
52-
return True
53-
except OSError as e:
54-
# ENODEV can be thrown if either the device reset while writing out the buffer or if the device
55-
# was already in bootloader mode in which case there is no device at the original address
56-
if e.errno == 19:
57-
time.sleep(0.25) # Wait for the device to reset
58-
return True
59-
else:
60-
print(f"Error sending reset command: {e}")
61-
return False
62-
63-
def wait_for_ack():
37+
def wait_for_ack(bus):
6438
"""
6539
Wait for an acknowledgment from the I2C device.
6640
6741
:return: True if an acknowledgment was received, otherwise False.
6842
"""
69-
res = i2c.readfrom(BOOTLOADER_I2C_ADDRESS, 1)[0]
43+
res = bus.readfrom(BOOTLOADER_I2C_ADDRESS, 1)[0]
7044
if res != ACK:
7145
while res == BUSY:
7246
time.sleep(0.1)
73-
res = i2c.readfrom(BOOTLOADER_I2C_ADDRESS, 1)[0]
47+
res = bus.readfrom(BOOTLOADER_I2C_ADDRESS, 1)[0]
7448
if res != ACK:
7549
print(f"❌ Error processing command. Result code: {hex(res)}")
7650
return False
7751
return True
7852

79-
def execute_command(opcode, command_params, response_length = 0, verbose=False):
53+
def execute_command(bus, opcode, command_params, response_length = 0, verbose=False):
8054
"""
8155
Execute an I2C command on the device.
8256
57+
:param bus: The I2C bus to use.
8358
:param opcode: The command opcode.
8459
:param command_params: The buffer containing the command parameters.
8560
:param response_length: The expected length of the response data frame.
@@ -90,44 +65,45 @@ def execute_command(opcode, command_params, response_length = 0, verbose=False):
9065
print(f"🕵️ Executing command {hex(opcode)}")
9166

9267
cmd = bytes([opcode, 0xFF ^ opcode]) # Send command code and complement (XOR = 0x00)
93-
i2c.writeto(BOOTLOADER_I2C_ADDRESS, cmd, True)
94-
if not wait_for_ack():
68+
bus.writeto(BOOTLOADER_I2C_ADDRESS, cmd, True)
69+
if not wait_for_ack(bus):
9570
print(f"❌ Command not acknowledged: {hex(opcode)}")
9671
return None
9772

9873
if command_params is not None:
99-
i2c.writeto(BOOTLOADER_I2C_ADDRESS, command_params, True)
100-
if not wait_for_ack():
74+
bus.writeto(BOOTLOADER_I2C_ADDRESS, command_params, True)
75+
if not wait_for_ack(bus):
10176
print("❌ Command failed")
10277
return None
10378

10479
if response_length == 0:
10580
return None
10681

107-
data = i2c.readfrom(BOOTLOADER_I2C_ADDRESS, response_length)
82+
data = bus.readfrom(BOOTLOADER_I2C_ADDRESS, response_length)
10883

109-
if not wait_for_ack():
84+
if not wait_for_ack(bus):
11085
print("❌ Failed completing command")
11186
return None
11287

11388
return data
11489

115-
def flash_firmware(firmware_path, verbose=False):
90+
def flash_firmware(device : Modulino, firmware_path, verbose=False):
11691
"""
11792
Flash the firmware to the I2C device.
11893
119-
:param firmware: The binary firmware data.
120-
:param length: The length of the firmware data.
94+
:param device: The Modulino device to flash.
95+
:param firmware_path: The binary firmware path.
12196
:param verbose: Whether to print debug information.
12297
:return: True if the flashing was successful, otherwise False.
12398
"""
124-
data = execute_command(CMD_GET_VERSION, None, 1, verbose)
99+
bus = device.i2c_bus
100+
data = execute_command(bus, CMD_GET_VERSION, None, 1, verbose)
125101
if data is None:
126102
print("❌ Failed to get protocol version")
127103
return False
128104
print(f"ℹ️ Protocol version: {data[0] & 0xF}.{data[0] >> 4}")
129105

130-
data = execute_command(CMD_GET, None, CMD_GET_LENGTH_V12, verbose)
106+
data = execute_command(bus, CMD_GET, None, CMD_GET_LENGTH_V12, verbose)
131107
if data is None:
132108
print("❌ Failed to get command list")
133109
return False
@@ -136,7 +112,7 @@ def flash_firmware(firmware_path, verbose=False):
136112
print("👀 Supported commands:")
137113
print(", ".join([hex(byte) for byte in data[2:]]))
138114

139-
data = execute_command(CMD_GET_ID, None, 3, verbose)
115+
data = execute_command(bus, CMD_GET_ID, None, 3, verbose)
140116
if data is None:
141117
print("❌ Failed to get device ID")
142118
return False
@@ -146,7 +122,7 @@ def flash_firmware(firmware_path, verbose=False):
146122

147123
print("🗑️ Erasing memory...")
148124
erase_params = bytearray([0xFF, 0xFF, 0x0]) # Mass erase flash
149-
execute_command(CMD_ERASE_NO_STRETCH, erase_params, 0, verbose)
125+
execute_command(bus, CMD_ERASE_NO_STRETCH, erase_params, 0, verbose)
150126

151127
with open(firmware_path, 'rb') as file:
152128
firmware_data = file.read()
@@ -161,7 +137,7 @@ def flash_firmware(firmware_path, verbose=False):
161137
checksum ^= b
162138
start_address.append(checksum)
163139
data_slice = firmware_data[i:i + CHUNK_SIZE]
164-
if not write_firmware_page(start_address, data_slice):
140+
if not write_firmware_page(bus, start_address, data_slice):
165141
print(f"❌ Failed to write page {hex(i)}")
166142
return False
167143
time.sleep(0.01) # Give the device some time to process the data
@@ -170,39 +146,40 @@ def flash_firmware(firmware_path, verbose=False):
170146

171147
print("🏃 Starting firmware")
172148
go_params = bytearray([0x8, 0x00, 0x00, 0x00, 0x8])
173-
execute_command(CMD_GO, go_params, 0, verbose) # Jump to the application
149+
execute_command(bus, CMD_GO, go_params, 0, verbose) # Jump to the application
174150

175151
return True
176152

177-
def write_firmware_page(command_params, firmware_data):
153+
def write_firmware_page(bus, command_params, firmware_data):
178154
"""
179155
Write a page of the firmware to the I2C device.
180156
157+
:param bus: The I2C bus to use.
181158
:param command_params: The buffer containing the command parameters.
182159
:param firmware_data: The buffer containing the firmware data.
183160
:return: True if the page was written successfully, otherwise False.
184161
"""
185162
cmd = bytes([CMD_WRITE_NO_STRETCH, 0xFF ^ CMD_WRITE_NO_STRETCH])
186-
i2c.writeto(BOOTLOADER_I2C_ADDRESS, cmd)
187-
if not wait_for_ack():
163+
bus.writeto(BOOTLOADER_I2C_ADDRESS, cmd)
164+
if not wait_for_ack(bus):
188165
print("❌ Write command not acknowledged")
189166
return False
190167

191-
i2c.writeto(BOOTLOADER_I2C_ADDRESS, command_params)
192-
if not wait_for_ack():
168+
bus.writeto(BOOTLOADER_I2C_ADDRESS, command_params)
169+
if not wait_for_ack(bus):
193170
print("❌ Failed to write command parameters")
194171
return False
195172

196173
data_size = len(firmware_data)
197174
tmp_buffer = bytearray(data_size + 2) # Data plus size and checksum
198-
tmp_buffer[0] = data_size - 1 # Size of the data # TODO Arduino code uses data_size - 1
175+
tmp_buffer[0] = data_size - 1 # Size of the data
199176
tmp_buffer[1:data_size + 1] = firmware_data
200177
tmp_buffer[-1] = 0 # Checksum placeholder
201178
for i in range(data_size + 1): # Calculate checksum over size byte + data bytes
202179
tmp_buffer[-1] ^= tmp_buffer[i]
203180

204-
i2c.writeto(BOOTLOADER_I2C_ADDRESS, tmp_buffer)
205-
if not wait_for_ack():
181+
bus.writeto(BOOTLOADER_I2C_ADDRESS, tmp_buffer)
182+
if not wait_for_ack(bus):
206183
print("❌ Failed to write firmware")
207184
return False
208185

@@ -243,7 +220,7 @@ def select_file(bin_files):
243220
return None
244221

245222
if len(bin_files) == 1:
246-
confirm = input(f"📄 Found one biary file: {bin_files[0]}. Do you want to flash it? (yes/no) ")
223+
confirm = input(f"📄 Found one binary file: {bin_files[0]}. Do you want to flash it? (yes/no) ")
247224
if confirm.lower() == 'yes':
248225
return bin_files[0]
249226
else:
@@ -257,37 +234,41 @@ def select_file(bin_files):
257234
return None
258235
return bin_files[choice - 1]
259236

260-
def select_i2c_device():
237+
def select_device(bus : I2C) -> Modulino:
261238
"""
262239
Scan the I2C bus for devices and prompt the user to select one.
263240
264-
:return: The selected I2C device address.
241+
:param bus: The I2C bus to scan.
242+
:return: The selected Modulino device.
265243
"""
266-
devices = i2c.scan()
244+
devices = Modulino.available_devices(bus)
267245

268246
if len(devices) == 0:
269-
print("❌ No I2C devices found")
247+
print("❌ No devices found")
270248
return None
271249

272250
if len(devices) == 1:
273-
confirm = input(f"🔌 Found one I2C device at address {hex(devices[0])}. Do you want to flash it? (yes/no) ")
251+
device = devices[0]
252+
confirm = input(f"🔌 Found {device.device_type} at address {hex(device.address)}. Do you want to update this device? (yes/no) ")
274253
if confirm.lower() == 'yes':
275254
return devices[0]
276255
else:
277256
return None
278257

279-
print("🔌 I2C devices found:")
258+
print("🔌 Devices found:")
280259
for index, device in enumerate(devices):
281-
print(f"{index + 1}. Address: {hex(device)}")
282-
choice = int(input("Select the I2C device to flash (number): "))
260+
print(f"{index + 1}) {device.device_type} at {hex(device.address)}")
261+
choice = int(input("Select the device to flash (number): "))
283262
if choice < 1 or choice > len(devices):
284263
return None
285264
return devices[choice - 1]
286265

287-
def run():
266+
def run(bus: I2C):
288267
"""
289268
Initialize the flashing process.
290269
Finds .bin files, scans for I2C devices, and flashes the selected firmware.
270+
271+
:param bus: The I2C bus to use. If None, the default I2C bus will be used.
291272
"""
292273

293274
bin_files = find_bin_files()
@@ -300,23 +281,25 @@ def run():
300281
print("❌ No file selected")
301282
return
302283

303-
device_address = select_i2c_device()
304-
if device_address is None:
284+
device = select_device(bus)
285+
if device is None:
305286
print("❌ No device selected")
306287
return
307-
308-
if send_reset(device_address):
288+
289+
print(f"🔄 Resetting device at address {hex(device.address)}")
290+
if device.enter_bootloader():
309291
print("✅ Device reset successfully")
310292
else:
311293
print("❌ Failed to reset device")
312294
return
313295

314296
print(f"🕵️ Flashing {bin_file} to device at address {hex(BOOTLOADER_I2C_ADDRESS)}")
315297

316-
if flash_firmware(bin_file):
298+
if flash_firmware(device, bin_file):
317299
print("✅ Firmware flashed successfully")
318300
else:
319301
print("❌ Failed to flash firmware")
320302

321303
if __name__ == "__main__":
322-
run()
304+
print()
305+
run(bus)

‎src/modulino/modulino.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,24 @@ def change_address(self, new_address: int):
250250

251251
self.address = new_address
252252

253+
def enter_bootloader(self):
254+
"""
255+
Enters the I2C bootloader of the device.
256+
This is only supported on Modulinos that have a microcontroller.
257+
258+
Returns:
259+
bool: True if the device entered bootloader mode, False otherwise.
260+
"""
261+
buffer = b'DIE'
262+
buffer += b'\x00' * (8 - len(buffer)) # Pad buffer to 8 bytes
263+
try:
264+
self.i2c_bus.writeto(self.address, buffer, True)
265+
sleep(0.25) # Wait for the device to reset
266+
return True
267+
except OSError as e:
268+
# ENODEV (e.errno == 19) can be thrown if either the device reset while writing out the buffer
269+
return False
270+
253271
def read(self, amount_of_bytes: int) -> bytes | None:
254272
"""
255273
Reads the given amount of bytes from the i2c device and returns the data.
@@ -293,14 +311,18 @@ def has_default_address(self) -> bool:
293311
return self.address in self.default_addresses
294312

295313
@staticmethod
296-
def available_devices() -> list[Modulino]:
314+
def available_devices(bus: I2C = None) -> list[Modulino]:
297315
"""
298316
Finds all devices on the i2c bus and returns them as a list of Modulino objects.
299317
318+
Parameters:
319+
bus (I2C): The I2C bus to use. If not provided, the default I2C bus will be used.
320+
300321
Returns:
301322
list: A list of Modulino objects.
302323
"""
303-
bus = _I2CHelper.get_interface()
324+
if bus is None:
325+
bus = _I2CHelper.get_interface()
304326
device_addresses = bus.scan()
305327
devices = []
306328
for address in device_addresses:

0 commit comments

Comments
 (0)
Please sign in to comment.