Skip to content

Commit

Permalink
Prepare for more advanced firmware upgrades
Browse files Browse the repository at this point in the history
* The "firmware_version" is now stored in a symbol in the ELF file for
  easier extraction.
* moteus_tool extracts the firmware from the ELF file, and requires
  that it be a version it knows about in order to flash.
* moteus_tool also stops telemetry and flushes before trying to flash.
  This will make flashing work even if tview was shut down uncleanly.

Currently, no advanced upgrade rules exist.  This just makes
moteus_tool properly know if it is able to handle a particular
version.
  • Loading branch information
jpieper committed Jan 13, 2021
1 parent 261b9c3 commit 13f6a2a
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 9 deletions.
6 changes: 5 additions & 1 deletion fw/moteus.cc
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
#error "Unknown target"
#endif

extern "C" {
uint32_t kMoteusFirmwareVersion = MOTEUS_FIRMWARE_VERSION;
}

auto* const MyDWT = DWT;
auto* const MyFLASH = FLASH;

Expand Down Expand Up @@ -265,7 +269,7 @@ int main(void) {

SystemInfo system_info(pool, telemetry_manager);
FirmwareInfo firmware_info(pool, telemetry_manager,
MOTEUS_FIRMWARE_VERSION, MOTEUS_MODEL_NUMBER);
kMoteusFirmwareVersion, MOTEUS_MODEL_NUMBER);
ClockManager clock(&timer, persistent_config, command_manager);

MoteusController moteus_controller(
Expand Down
12 changes: 12 additions & 0 deletions lib/python/moteus/moteus.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,18 @@ async def read(self, size, block=True):
to_return, self._read_data = self._read_data[0:size], self._read_data[size:]
return to_return

async def flush_read(self, timeout=0.01):
self._read_data = b''

try:
await asyncio.wait_for(self.read(65536), timeout)
raise RuntimeError("More data to flush than expected")
except asyncio.TimeoutError:
# This is the expected path.
pass

self._read_data = b''

async def _read_maybe_empty_line(self):
while b'\n' not in self._read_data and b'\r' not in self._read_data:
async with self.lock:
Expand Down
76 changes: 68 additions & 8 deletions lib/python/moteus/moteus_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io
import math
import os
import struct
import sys
import tempfile

Expand All @@ -36,6 +37,25 @@
MAX_FLASH_BLOCK_SIZE = 32


class FirmwareUpgrade:
'''This encodes "magic" rules about upgrading firmware, largely about
how to munge configuration options so as to not cause behavior
change upon firmware changes.
'''

def __init__(self, old, new):
self.old = old
self.new = new

if new > 0x0100:
raise RuntimeError("Firmware to be flashed has a newer version than we support")

def fix_config(self, old_config):
lines = old_config.split(b'\n')

return b'\n'.join(lines)


def _get_log_directory():
moteus_cal_dir = os.environ.get("MOTEUS_CAL_DIR", None)
if moteus_cal_dir:
Expand Down Expand Up @@ -110,6 +130,11 @@ def logical_to_physical(self, address, size):
raise RuntimeError(f"no mapping found for {address:x}")


class ElfData:
sections = []
firmware_version = None


def _read_elf(filename, sections):
fp = elftools.elf.elffile.ELFFile(open(filename, "rb"))

Expand All @@ -131,9 +156,33 @@ def _read_elf(filename, sections):
section['sh_addr'], section['sh_size'])
sections[physical_address] = data

result = []
result = ElfData()
result.sections = []

for key in sorted(sections.keys()):
result.append((key, sections[key]))
result.sections.append((key, sections[key]))

# Try to find the firmware version.
symtab = fp.get_section_by_name('.symtab')
if symtab:
maybe_version = symtab.get_symbol_by_name('kMoteusFirmwareVersion')
if maybe_version:
version = maybe_version[0]
if version['st_size'] != 4:
raise RuntimeError('Version in firmware image corrupt')

symbol_address = version['st_value']
symbol_section = version['st_shndx']

sec = fp.get_section(symbol_section)
sec_start = symbol_address - sec['sh_addr']
ver, = struct.unpack('<i', sec.data()[sec_start:sec_start+4])

result.firmware_version = ver

if result.firmware_version is None:
print("WARNING: Could not find firmware version in elf file, assuming 0x00000100")
result.firmware_version = 0x00000100

return result

Expand Down Expand Up @@ -286,11 +335,22 @@ async def write_config_stream(self, fp):
print()

async def do_flash(self, elffile):
elfs = _read_elf(elffile, [".text", ".ARM.extab", ".ARM.exidx",
".data", ".ccmram", ".isr_vector"])
count_bytes = sum([len(section) for address, section in elfs])
elf = _read_elf(elffile, [".text", ".ARM.extab", ".ARM.exidx",
".data", ".ccmram", ".isr_vector"])
count_bytes = sum([len(section) for address, section in elf.sections])

fw = '0x{:08x}'.format(elf.firmware_version)
print(f"Read ELF file version {fw}: {count_bytes} bytes")

await self.write_message("tel stop")

# Discard anything that might have been en route.
await self.stream.flush_read()

# Get the current firmware version.
old_firmware = await self.read_data("firmware")

print(f"Read ELF file: {count_bytes} bytes")
upgrade = FirmwareUpgrade(old_firmware.version, elf.firmware_version)

if not self.args.no_restore_config:
# Read our old config.
Expand All @@ -311,15 +371,15 @@ async def do_flash(self, elffile):
await self.stream.readline()

await self.command("unlock")
await self.write_flash(elfs)
await self.write_flash(elf.sections)
await self.command("lock")
# This will reset the controller, so we don't expect a response.
await self.write_message("reset")

await asyncio.sleep(1.0)

if not self.args.no_restore_config:
await self.restore_config(old_config)
await self.restore_config(upgrade.fix_config(old_config))

def _emit_flash_progress(self, ctx, type):
if self.args.verbose:
Expand Down

0 comments on commit 13f6a2a

Please sign in to comment.