Skip to content

Commit

Permalink
Add support for mapping current firmware into memory (#1158)
Browse files Browse the repository at this point in the history
* Add abstraction for firmware content

* More work

* More work

* Add test

* Really add test

* Simplify bounds check

* Minor tweaks

* Extend test

* Remove commented code

* Address review feedback
  • Loading branch information
kasperl authored Oct 28, 2022
1 parent 039c678 commit b1c31ed
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 37 deletions.
8 changes: 7 additions & 1 deletion lib/system/api/firmware.toit
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import system.services show ServiceClient
interface FirmwareService:
static UUID /string ::= "777096e8-05bc-4af7-919e-5ba696549bd5"
static MAJOR /int ::= 0
static MINOR /int ::= 4
static MINOR /int ::= 5

is_validation_pending -> bool
static IS_VALIDATION_PENDING_INDEX /int ::= 0
Expand Down Expand Up @@ -42,6 +42,9 @@ interface FirmwareService:
firmware_writer_pad handle/int size/int value/int -> none
static FIRMWARE_WRITER_PAD_INDEX /int ::= 10

firmware_writer_flush handle/int -> none
static FIRMWARE_WRITER_FLUSH_INDEX /int ::= 12

firmware_writer_commit handle/int checksum/ByteArray? -> none
static FIRMWARE_WRITER_COMMIT_INDEX /int ::= 7

Expand Down Expand Up @@ -85,5 +88,8 @@ class FirmwareServiceClient extends ServiceClient implements FirmwareService:
firmware_writer_pad handle/int size/int value/int -> none:
invoke_ FirmwareService.FIRMWARE_WRITER_PAD_INDEX [handle, size, value]

firmware_writer_flush handle/int -> none:
invoke_ FirmwareService.FIRMWARE_WRITER_FLUSH_INDEX handle

firmware_writer_commit handle/int checksum/ByteArray? -> none:
invoke_ FirmwareService.FIRMWARE_WRITER_COMMIT_INDEX [handle, checksum]
7 changes: 7 additions & 0 deletions lib/system/base/firmware.toit
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ abstract class FirmwareServiceDefinitionBase extends ServiceDefinition implement
if index == FirmwareService.FIRMWARE_WRITER_PAD_INDEX:
writer ::= (resource client arguments[0]) as FirmwareWriter
return firmware_writer_pad writer arguments[1] arguments[2]
if index == FirmwareService.FIRMWARE_WRITER_FLUSH_INDEX:
writer ::= (resource client arguments) as FirmwareWriter
return firmware_writer_flush writer
if index == FirmwareService.FIRMWARE_WRITER_COMMIT_INDEX:
writer ::= (resource client arguments[0]) as FirmwareWriter
return firmware_writer_commit writer arguments[1]
Expand Down Expand Up @@ -74,10 +77,14 @@ abstract class FirmwareServiceDefinitionBase extends ServiceDefinition implement
firmware_writer_pad writer/FirmwareWriter size/int value/int -> none:
writer.pad size value

firmware_writer_flush writer/FirmwareWriter -> none:
writer.flush

firmware_writer_commit writer/FirmwareWriter checksum/ByteArray? -> none:
writer.commit checksum

interface FirmwareWriter:
write bytes/ByteArray -> int
pad size/int value/int -> int
flush -> none
commit checksum/ByteArray? -> none
103 changes: 95 additions & 8 deletions lib/system/firmware.toit
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,27 @@ _client_ /FirmwareServiceClient? ::= (FirmwareServiceClient --no-open).open
/**
The configuration of the current firmware.
*/
config ::= FirmwareConfig_
config /FirmwareConfig ::= FirmwareConfig_

/**
The content bytes of the current firmware.
Map the current firmware into memory, so the content
bytes of it can be accessed.
The mapping is only valid while executing the given
$block.
*/
content -> ByteArray?:
if not _client_: return null
return _client_.content
map --from/int=0 --to/int?=null [block] -> none:
mapping/FirmwareMapping_? := null
if _client_:
data := firmware_map_ _client_.content
if data:
if not to: to = data.size
if 0 <= from <= to <= data.size:
mapping = FirmwareMapping_ data from (to - from)
try:
block.call mapping
finally:
if mapping: firmware_unmap_ mapping.data_

/**
Returns whether the currently executing firmware is
Expand Down Expand Up @@ -101,19 +114,93 @@ class FirmwareWriter extends ServiceResourceProxy:
pad size/int --value/int=0 -> none:
_client_.firmware_writer_pad handle_ size value

flush -> none:
_client_.firmware_writer_flush handle_

commit --checksum/ByteArray?=null -> none:
_client_.firmware_writer_commit handle_ checksum

class FirmwareConfig_:
interface FirmwareConfig:
/**
Returns the configuration entry for the given $key, or
null if the $key isn't present in the configuration.
*/
operator [] key/string -> any:
return _client_.config_entry key
operator [] key/string -> any

/**
Returns the UBJSON encoded configuration.
*/
ubjson -> ByteArray

interface FirmwareMapping:
/**
Returns the size of the mapped firmware in bytes.
*/
size -> int

/**
Returns the byte at the given $index.
*/
operator [] index/int -> int

/**
Returns a slice of the firmware mapping.
*/
operator [..] --from/int=0 --to/int=size -> FirmwareMapping

/**
Copies a section of the mapped firmware into the $into byte
array.
*/
copy from/int to/int --into/ByteArray -> none

// -------------------------------------------------------------------------
class FirmwareConfig_ implements FirmwareConfig:
operator [] key/string -> any:
return _client_.config_entry key

ubjson -> ByteArray:
return _client_.config_ubjson

class FirmwareMapping_ implements FirmwareMapping:
data_/ByteArray
offset_/int
size/int

constructor .data_ .offset_=0 .size=data_.size:

operator [] index/int -> int:
#primitive.core.firmware_mapping_at

operator [..] --from/int=0 --to/int=size -> FirmwareMapping:
if not 0 <= from <= to <= size: throw "OUT_OF_BOUNDS"
return FirmwareMapping_ data_ (offset_ + from) (to - from)

copy from/int to/int --into/ByteArray -> none:
if not 0 <= from <= to <= size: throw "OUT_OF_BOUNDS"
// Determine if we can do an aligned block copy taking
// the offset into account.
offset := offset_
block_from := min to ((round_up (from + offset) 4) - offset)
block_to := (round_down (to + offset) 4) - offset
// Copy the bytes in up to three chunks.
cursor := copy_range_ from block_from into 0
if block_from < block_to:
cursor = copy_block_ block_from block_to into cursor
else:
block_to = block_from
copy_range_ block_to to into cursor

copy_range_ from/int to/int into/ByteArray index/int -> int:
while from < to: into[index++] = this[from++]
return index

copy_block_ from/int to/int into/ByteArray index/int -> int:
#primitive.core.firmware_mapping_copy

firmware_map_ data/ByteArray? -> ByteArray?:
#primitive.core.firmware_map

firmware_unmap_ data/ByteArray -> none:
#primitive.core.firmware_unmap
4 changes: 4 additions & 0 deletions src/primitive.h
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ namespace toit {
PRIMITIVE(get_env, 1) \
PRIMITIVE(literal_index, 1) \
PRIMITIVE(word_size, 0) \
PRIMITIVE(firmware_map, 1) \
PRIMITIVE(firmware_unmap, 1) \
PRIMITIVE(firmware_mapping_at, 2) \
PRIMITIVE(firmware_mapping_copy, 5) \

#define MODULE_TIMER(PRIMITIVE) \
PRIMITIVE(init, 0) \
Expand Down
130 changes: 106 additions & 24 deletions src/primitive_core.cc
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
#ifdef TOIT_FREERTOS
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "esp_spi_flash.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
Expand Down Expand Up @@ -1696,36 +1698,15 @@ PRIMITIVE(byte_array_new_external) {
return result;
}

static bool memory_overlaps(const uint8* address_a, int length_a, const uint8* address_b, int length_b) {
if (address_a <= address_b && address_b < address_a + length_a) return true;
if (address_b <= address_a && address_a < address_b + length_b) return true;
return false;
}

PRIMITIVE(byte_array_replace) {
ARGS(MutableBlob, receiver, int, index, Blob, source_object, int, from, int, to);

if (index < 0) OUT_OF_BOUNDS;

if (from < 0) OUT_OF_BOUNDS;
if (to < 0) OUT_OF_BOUNDS;
if (to > source_object.length()) OUT_OF_BOUNDS;

if (index < 0 || from < 0 || to < 0 || to > source_object.length()) OUT_OF_BOUNDS;
int length = to - from;
if (length < 0) OUT_OF_BOUNDS;

if (index + length > receiver.length()) OUT_OF_BOUNDS;
if (length < 0 || index + length > receiver.length()) OUT_OF_BOUNDS;

uint8* dest = receiver.address() + index;
const uint8* source = source_object.address() + from;

if (((reinterpret_cast<uintptr_t>(source) | length) & 3) == 0 &&
!memory_overlaps(dest, length, source, length)) {
iram_safe_memcpy(dest, source, length);
} else {
memmove(dest, source, length);
}

memmove(dest, source, length);
return process->program()->null_object();
}

Expand Down Expand Up @@ -2332,4 +2313,105 @@ PRIMITIVE(word_size) {
return Smi::from(WORD_SIZE);
}

#ifdef TOIT_FREERTOS
static spi_flash_mmap_handle_t firmware_mmap_handle;
static bool firmware_is_mapped = false;
#endif

PRIMITIVE(firmware_map) {
ARGS(Object, bytes);
#ifndef TOIT_FREERTOS
return bytes;
#else
if (bytes != process->program()->null_object()) {
// If we're passed non-null bytes, we use that as the
// firmware bits.
return bytes;
}

ByteArray* proxy = process->object_heap()->allocate_proxy();
if (proxy == null) ALLOCATION_FAILED;

if (firmware_is_mapped) {
// We unmap to allow the next attempt to get the current
// system image to succeed.
spi_flash_munmap(firmware_mmap_handle);
firmware_is_mapped = false;
QUOTA_EXCEEDED; // Quota is 1.
}

const esp_partition_t* current_partition = esp_ota_get_running_partition();
if (current_partition == null) OTHER_ERROR;

const void* mapped_to = null;
esp_err_t err = esp_partition_mmap(
current_partition,
0, // Offset from start of partition.
current_partition->size,
SPI_FLASH_MMAP_INST,
&mapped_to,
&firmware_mmap_handle);
if (err != ESP_OK) OTHER_ERROR;

firmware_is_mapped = true;
proxy->set_external_address(
current_partition->size,
const_cast<uint8*>(reinterpret_cast<const uint8*>(mapped_to)));
return proxy;
#endif
}

PRIMITIVE(firmware_unmap) {
#ifdef TOIT_FREERTOS
ARGS(ByteArray, proxy);
if (!firmware_is_mapped) process->program()->null_object();
spi_flash_munmap(firmware_mmap_handle);
firmware_is_mapped = false;
proxy->clear_external_address();
#endif
return process->program()->null_object();
}

PRIMITIVE(firmware_mapping_at) {
ARGS(Instance, receiver, int, index);
int offset = Smi::cast(receiver->at(1))->value();
int size = Smi::cast(receiver->at(2))->value();
if (index < 0 || index >= size) OUT_OF_BOUNDS;

Blob input;
if (!receiver->at(0)->byte_content(process->program(), &input, STRINGS_OR_BYTE_ARRAYS)) {
WRONG_TYPE;
}

// Firmware is potentially mapped into memory that only allow word
// access. We read the full word before masking and shifting. This
// asssumes that we're running on a little endian platform.
index += offset;
const uint32* words = reinterpret_cast<const uint32*>(input.address());
uint32 shifted = words[index >> 2] >> ((index & 3) << 3);
return Smi::from(shifted & 0xff);
}

PRIMITIVE(firmware_mapping_copy) {
ARGS(Instance, receiver, int, from, int, to, ByteArray, into, int, index);
int offset = Smi::cast(receiver->at(1))->value();
int size = Smi::cast(receiver->at(2))->value();
if (!Utils::is_aligned(from + offset, sizeof(uint32)) ||
!Utils::is_aligned(to + offset, sizeof(uint32))) INVALID_ARGUMENT;
if (from > to || from < 0 || to > size) OUT_OF_BOUNDS;

Blob input;
if (!receiver->at(0)->byte_content(process->program(), &input, STRINGS_OR_BYTE_ARRAYS)) {
WRONG_TYPE;
}

// Firmware is potentially mapped into memory that only allow word
// access. We use an IRAM safe memcpy alternative that guarantees
// always reading whole words to avoid issues with this.
ByteArray::Bytes output(into);
int bytes = to - from;
iram_safe_memcpy(output.address() + index, input.address() + from + offset, bytes);
return Smi::from(index + bytes);
}

} // namespace toit
15 changes: 11 additions & 4 deletions system/extensions/esp32/firmware.toit
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ class FirmwareServiceDefinition extends FirmwareServiceDefinitionBase:
return config_.get key

content -> ByteArray?:
// TODO(kasper): Implement this.
// We deliberately return null here to let the caller know that
// it should try to use the firmware content provided by the
// underlying system (if any). On the ESP32, the system will
// use this to give access to the content of the currently
// running OTA partition.
return null

firmware_writer_open client/int from/int to/int -> FirmwareWriter:
Expand Down Expand Up @@ -93,11 +97,14 @@ class FirmwareWriter_ extends ServiceResource implements FirmwareWriter:
written_ = ota_write_ buffer_
fullness_ = 0

flush -> none:
if fullness_ == 0: return
written_ = ota_write_ buffer_[..fullness_]
fullness_ = 0

commit checksum/ByteArray? -> none:
if fullness_ != 0:
written_ = ota_write_ buffer_[..fullness_]
fullness_ = 0
// Always commit. Always.
flush
ota_end_ written_ checksum
buffer_ = null

Expand Down
Loading

0 comments on commit b1c31ed

Please sign in to comment.