diff --git a/bootloaders/riotboot_serial/doc.txt b/bootloaders/riotboot_serial/doc.txt new file mode 100644 index 0000000000..63b621a1ca --- /dev/null +++ b/bootloaders/riotboot_serial/doc.txt @@ -0,0 +1,24 @@ +/** +@defgroup bootloader_riotboot_serial riotboot serial bootloader +@ingroup bootloaders + +# Overview +`riotboot_dfu` is a variation on @ref bootloader_riotboot that adds the capability to flash +a new firmware using a serial (UART) connection. + +After reset, riotboot will wait for `RIOTBOOT_DELAY_MS` ms, if within that time no command is +received it will automatically start the application. + +If it reads the `B` character, it will enter bootloader mode where flash sectors can be erased +and written. + +The application in `dist/tools/riotboot_serial` is used to talk to the bootloader and to flash +`.hex` files using the bootloader's serial protocol. + +# Flashing a device +To flash a device that is equipped with the riotboot serial bootloader, set the `PROGRAMMER` variable +to `riotboot_serial` and chose the `PORT` accordingly. + +e.g. + + make BOARD=same54-xpro PORT=/dev/ttyACM0 PROGRAMMER=riotboot_serial flash diff --git a/sys/Makefile.dep b/sys/Makefile.dep index a3e96ab080..9f38ee91d2 100644 --- a/sys/Makefile.dep +++ b/sys/Makefile.dep @@ -622,6 +622,13 @@ ifneq (,$(filter riotboot_slot, $(USEMODULE))) USEMODULE += riotboot_hdr endif +ifneq (,$(filter riotboot_serial, $(USEMODULE))) + FEATURES_REQUIRED += periph_flashpage + FEATURES_REQUIRED += periph_uart + USEMODULE += riotboot + USEMODULE += checksum +endif + ifneq (,$(filter riotboot_hdr, $(USEMODULE))) USEMODULE += checksum USEMODULE += riotboot diff --git a/sys/include/riotboot/serial.h b/sys/include/riotboot/serial.h new file mode 100644 index 0000000000..fc2ccd77f6 --- /dev/null +++ b/sys/include/riotboot/serial.h @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2020 Benjamin Valentin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup sys_riotboot_serial Serial Bootloader Protocol + * @ingroup sys + * @{ + * + * @file + * @brief riotboot as a serial bootloader + * + * @author Benjamin Valentin + * + * @} + */ + +#ifndef RIOTBOOT_SERIAL_H +#define RIOTBOOT_SERIAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief riotboot commands + * Commands typically have the format [type|length|value] + * where type and length are one byte and value is $length bytes. + * + * Commands are signed with a CRC-8 checksum that is calculated + * over the entire record. The Checksum is not part of length. + * @{ + */ +/** + * start application. + * parameter: + * - '\n': launch default application + * - '0' : launch slot 0 + * - '1' : launch slot 1 + */ +#define RIOTBOOT_CMD_BOOT 'b' + +/** + * erase page + * parameter: + * - length of page number (8 bit - should be 4) + * - page number (32 bit little endian) + */ +#define RIOTBOOT_CMD_ERASE 'e' + +/** + * write data + * parameter: + * - length of data + address (8 bit) + * - destination address (32 bit little endian) + * - data (up to (RX_BUF_LEN-6) bytes) + */ +#define RIOTBOOT_CMD_WRITE 'w' + +/** + * get page of address + * parameter: + * - length of address (8 bit - should be 4) + * - address (32 bit little endian) + * + * returns status code followed by 32 bit little endian + * value that represents the page in which the address lies. + */ +#define RIOTBOOT_CMD_GET_PAGE 'P' +/** @} */ + +/** + * @brief riotboot response codes to commands + * @{ + */ +/** operation successful */ +#define RIOTBOOT_STAT_OK '.' +/** CRC error - try again */ +#define RIOTBOOT_STAT_BAD_CRC '?' +/** illegal parameter */ +#define RIOTBOOT_STAT_ILLEGAL '!' +/** @} */ + +/** + * @brief riotboot serial loader synchronisation + * @{ + */ +/* sent to stop auto-boot */ +#define RIOTBOOT_ENTER_LOADER 'B' +/* sent to probe if auto-boot is paused and riotboot is active */ +#define RIOTBOOT_PROBE '?' + +/* continuously sent by riotboot before booting */ +#define RIOTBOOT_STAT_WAITING 'b' +/* indicates riotboot is ready to accept commands */ +#define RIOTBOOT_STAT_READY '>' +/** @} */ + +/** + * @brief CRC-8 Polynom used for riotboot chunks + */ +#ifndef RIOTBOOT_CRC8_POLY +#define RIOTBOOT_CRC8_POLY (0x31) +#endif + +/** + * @brief Start interactive serial bootloader + * @return slot to boot, -1 if default slot should be started + */ +int riotboot_serial_loader(void); + +#ifdef __cplusplus +} +#endif + +#endif /* RIOTBOOT_SERIAL_H */ diff --git a/sys/riotboot/serial.c b/sys/riotboot/serial.c new file mode 100644 index 0000000000..292516d2b2 --- /dev/null +++ b/sys/riotboot/serial.c @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2020 Benjamin Valentin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup sys_riotboot_serial + * @{ + * + * @file + * @brief Serial Bootloader + * + * @author Benjamin Valentin + * + * @} + */ + +#include +#include + +#include "stdio_uart.h" +#include "periph/uart.h" +#include "periph/flashpage.h" +#include "unaligned.h" +#include "checksum/crc8.h" +#include "riotboot/serial.h" + +#include "board.h" + +#ifndef RIOTBOOT_UART_DEV +#define RIOTBOOT_UART_DEV STDIO_UART_DEV +#endif + +#ifndef RIOTBOOT_UART_BAUDRATE +#define RIOTBOOT_UART_BAUDRATE STDIO_UART_BAUDRATE +#endif + +#ifndef RIOTBOOT_DELAY_MS +#define RIOTBOOT_DELAY_MS (1000) +#endif + +#define RX_BUF_LEN (40) + +/* we send a characters each iteration -> 8 bit + start & stop bit */ +#define RIOTBOOT_DELAY (RIOTBOOT_DELAY_MS * RIOTBOOT_UART_BAUDRATE / 10000) + +static inline void uart_write_byte(uart_t uart, uint8_t data) +{ + uart_write(uart, &data, 1); +} + +/* send 'hello' byte until we get enter bootloader byte or timeout */ +static bool _bootdelay(unsigned tries, volatile bool *boot_default) +{ + if (tries == 0) { + return true; + } + + while (--tries && *boot_default) { + uart_write_byte(RIOTBOOT_UART_DEV, 0); + } + + return *boot_default; +} + +__attribute__ ((aligned(4))) +static struct { + uint8_t pos; /* current pos in rx buffer */ + uint8_t remaining; /* remaining bytes to read */ + union { + uint8_t u8[RX_BUF_LEN]; /* rx buffer */ + struct { + uint8_t type; /* command type */ + uint8_t len; /* length of data (without checksum) */ + uint8_t data[]; /* data is aligned at word boundary */ + } val; + } rx; +} ctx; + +/** + * Format: + * [ Type (1 byte) | Length (1 byte) | value (n bytes) | checksum (1 byte) ] + */ +static void _uart_rx_cmd(void *arg, uint8_t data) +{ + bool *reading = arg; + uint8_t crc; + + /* ignore RX while processing buffer */ + if (!reading) { + return; + } + + ctx.rx.u8[ctx.pos] = data; + + switch (ctx.pos) { + case 0: + switch (data) { + case RIOTBOOT_CMD_BOOT: + case RIOTBOOT_CMD_ERASE: + case RIOTBOOT_CMD_WRITE: + case RIOTBOOT_CMD_GET_PAGE: + ctx.remaining = 1; + break; + /* re-create initial sync handshake if already in bootloader */ + case RIOTBOOT_PROBE: + uart_write_byte(RIOTBOOT_UART_DEV, RIOTBOOT_STAT_WAITING); + return; + case RIOTBOOT_ENTER_LOADER: + uart_write_byte(RIOTBOOT_UART_DEV, RIOTBOOT_STAT_READY); + *reading = false; + return; + default: + return; + } + + break; + case 1: + /* data length + checksum byte + dummy byte for fall-through */ + ctx.remaining = data + 2; + + /* boot command needs no checksum */ + if (ctx.rx.val.type == RIOTBOOT_CMD_BOOT) { + *reading = false; + return; + } + + /* bail out early if the buffer would not fit */ + /* data len + sizeof(type, len) */ + if (ctx.remaining + 2 >= RX_BUF_LEN) { + crc = 0; + goto error; + } + + /* fall-through */ + default: + + /* end of data block not reached */ + if (--ctx.remaining) { + break; + } + + /* fall-through */ + case (RX_BUF_LEN - 1): + + /* calculate checksum */ + crc = crc8(ctx.rx.u8, ctx.pos, RIOTBOOT_CRC8_POLY, 0xFF); + +error: + ctx.remaining = 0; + ctx.pos = 0; + + /* checksum error */ + if (crc != data) { + uart_write_byte(RIOTBOOT_UART_DEV, RIOTBOOT_STAT_BAD_CRC); + break; + } + + *reading = false; + break; + } + + if (ctx.remaining) { + ++ctx.pos; + } + + return; +} + +static void _get_page(uintptr_t addr) +{ + uart_write_byte(RIOTBOOT_UART_DEV, RIOTBOOT_STAT_OK); + uint32_t page = flashpage_page((void *)addr); + uart_write(RIOTBOOT_UART_DEV, (void *)&page, sizeof(page)); +} + +static void _erase(uint16_t sector) +{ + /* don't erase bootloader */ + if ((uintptr_t)flashpage_addr(sector) < SLOT0_OFFSET) { + uart_write_byte(RIOTBOOT_UART_DEV, RIOTBOOT_STAT_ILLEGAL); + return; + } + + flashpage_erase(sector); + + uart_write_byte(RIOTBOOT_UART_DEV, RIOTBOOT_STAT_OK); +} + +static void _write(uint32_t addr, uint8_t len, const uint8_t *data) +{ + /* don't overwrite bootloader */ + if (addr < SLOT0_OFFSET) { + uart_write_byte(RIOTBOOT_UART_DEV, RIOTBOOT_STAT_ILLEGAL); + return; + } + + flashpage_write((void *)addr, data, len); + + uart_write_byte(RIOTBOOT_UART_DEV, RIOTBOOT_STAT_OK); +} + +int riotboot_serial_loader(void) +{ + volatile bool reading = true; + + uart_init(RIOTBOOT_UART_DEV, RIOTBOOT_UART_BAUDRATE, + _uart_rx_cmd, (void *)&reading); + + /* give the user some time to interrupt auto boot */ + if (_bootdelay(RIOTBOOT_DELAY, &reading)) { + return -1; + } + + while (1) { + + /* we can't use mutex in riotboot */ + while (reading) {} + + switch (ctx.rx.val.type) { + case RIOTBOOT_CMD_BOOT: + switch (ctx.rx.val.len) { + case '\n': + uart_write_byte(RIOTBOOT_UART_DEV, RIOTBOOT_STAT_OK); + return -1; +#ifdef MODULE_RIOTBOOT_SLOT + case '0': + uart_write_byte(RIOTBOOT_UART_DEV, RIOTBOOT_STAT_OK); + return 0; + case '1': + uart_write_byte(RIOTBOOT_UART_DEV, RIOTBOOT_STAT_OK); + return 1; +#endif + default: + uart_write_byte(RIOTBOOT_UART_DEV, RIOTBOOT_STAT_ILLEGAL); + break; + } + break; + case RIOTBOOT_CMD_GET_PAGE: + _get_page(unaligned_get_u32(ctx.rx.val.data)); + break; + case RIOTBOOT_CMD_ERASE: + _erase(unaligned_get_u16(ctx.rx.val.data)); + break; + case RIOTBOOT_CMD_WRITE: + _write(unaligned_get_u32(ctx.rx.val.data), /* address */ + ctx.rx.val.len - sizeof(uint32_t), /* sizeof(data) - sizeof(address) */ + &ctx.rx.val.data[4]); /* data */ + break; + } + + reading = true; + } + + return -1; +}