Skip to content

Commit

Permalink
firmware: Check bootloader write protection fuses during initialization
Browse files Browse the repository at this point in the history
Previously, we assumed that the SAMD UF2 bootloader would *always* set
the bootloader write protection fuses, however, it turns out that it
only sets it in two cases:

1. The fuses have completely bogus values (`0xFFFFFFFF`)
2. The bootloader self-updates via UF2

Since our boards have factory fuses set and we install the bootloader
via flashing a `.bin` file, neither of these cases ever happened. This
left the bootloader write protection disabled which could lead to the
bootloader getting corrupted by spurious writes. We actually observed
this behavior by stress testing power cycles.

This change checks fuses on startup and sets the `BOOTPROT` fuse if
necessary.
  • Loading branch information
theacodes committed Apr 21, 2021
1 parent 1d4237b commit 0d6cd8d
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 1 deletion.
1 change: 1 addition & 0 deletions firmware/src/gem.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "gem_clocks.h"
#include "gem_config.h"
#include "gem_dotstar.h"
#include "gem_fuses.h"
#include "gem_i2c.h"
#include "gem_led_animation.h"
#include "gem_lookup_tables.h"
Expand Down
87 changes: 87 additions & 0 deletions firmware/src/hw/gem_fuses.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
Copyright (c) 2021 Alethea Katherine Flowers.
Published under the standard MIT License.
Full text available at: https://opensource.org/licenses/MIT
*/

#include "gem_fuses.h"
#include "printf.h"

/* 0x02 = 8k, see datasheet section 22.6.5 */
#define BOOTLOADER_BOOTPROT_SIZE 0x02

/* Private forward declarations */
static void print_fuses();

/* Public functions */

void gem_fuses_check() {
/* Check bootprot and ensure the bootloader is write-protected. */
if (NVM_USER->USER_ROW.bit.BOOTPROT == BOOTLOADER_BOOTPROT_SIZE) {
/* All good, bootprot is set correctly. */
printf("Fuses OK\n");
return;
}

/* bootprot needs to be set. */
printf("Setting BOOTPROT fuses. Current fuses:\n");
print_fuses();

NVM_USER_Type fuses = *NVM_USER;
fuses.USER_ROW.bit.BOOTPROT = BOOTLOADER_BOOTPROT_SIZE;
gem_fuses_write(fuses);

printf("BOOTPROT set, updated fuses:\n");
print_fuses();

/* Reboot. */
NVIC_SystemReset();
}

void gem_fuses_write(NVM_USER_Type fuses) {
__disable_irq();

/* Setup NVM for writing and disable cache. */
uint32_t ctrlb = NVMCTRL->CTRLB.reg;
NVMCTRL->STATUS.reg |= NVMCTRL_STATUS_MASK;
NVMCTRL->CTRLB.reg |= NVMCTRL_CTRLB_CACHEDIS | NVMCTRL_CTRLB_MANW;
NVMCTRL->ADDR.reg = NVMCTRL_FUSES_BOOTPROT_ADDR / 2;

/* Erase the row, flush the page cache. */
NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | NVMCTRL_CTRLA_CMD_EAR;
while (NVMCTRL->INTFLAG.bit.READY == 0) {}
NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | NVMCTRL_CTRLA_CMD_PBC;
while (NVMCTRL->INTFLAG.bit.READY == 0) {}

/* Write the data - must be done in 16 or 32 bit chunks. */
((uint32_t*)NVMCTRL_USER)[0] = ((uint32_t*)&fuses)[0];
((uint32_t*)NVMCTRL_USER)[1] = ((uint32_t*)&fuses)[1];

/* Write the page. */
NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | NVMCTRL_CTRLA_CMD_WAP;
while (NVMCTRL->INTFLAG.bit.READY == 0) {}

/* Restore saved CTRLB value. */
NVMCTRL->CTRLB.reg = ctrlb;

__enable_irq();
}

/* Private functions */

static void print_fuses() {
printf("Fuses: 0x%8x%8x\n", ((uint32_t*)NVMCTRL_USER)[1], ((uint32_t*)NVMCTRL_USER)[0]);
printf("- BOOTPROT: 0x%x\n", NVM_USER->USER_ROW.bit.BOOTPROT);
printf("- EEPROM: 0x%x\n", NVM_USER->USER_ROW.bit.EEPROM);
printf("- BOD33_LEVEL: 0x%x\n", NVM_USER->USER_ROW.bit.BOD33_LEVEL);
printf("- BOD33_ENABLE: 0x%x\n", NVM_USER->USER_ROW.bit.BOD33_ENABLE);
printf("- BOD33_ACTION: 0x%x\n", NVM_USER->USER_ROW.bit.BOD33_ACTION);
printf("- WDT_ENABLE: 0x%x\n", NVM_USER->USER_ROW.bit.WDT_ENABLE);
printf("- WDT_ALWAYSON: 0x%x\n", NVM_USER->USER_ROW.bit.WDT_ALWAYSON);
printf("- WDT_PERIOD: 0x%x\n", NVM_USER->USER_ROW.bit.WDT_PERIOD);
printf("- WDT_WINDOW: 0x%x\n", NVM_USER->USER_ROW.bit.WDT_WINDOW);
printf("- WDT_EWOFFSET: 0x%x\n", NVM_USER->USER_ROW.bit.WDT_EWOFFSET);
printf("- WDT_WEN: 0x%x\n", NVM_USER->USER_ROW.bit.WDT_WEN);
printf("- BOD33_HYST: 0x%x\n", NVM_USER->USER_ROW.bit.BOD33_HYST);
printf("- LOCK: 0x%x\n", NVM_USER->USER_ROW.bit.LOCK);
}
51 changes: 51 additions & 0 deletions firmware/src/hw/gem_fuses.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
Copyright (c) 2021 Alethea Katherine Flowers.
Published under the standard MIT License.
Full text available at: https://opensource.org/licenses/MIT
*/

#pragma once

/* Checks/sets SAMD21 fuses (NVM USER ROW) */

#include "sam.h"
#include <stdint.h>

/*
Struct mapping for the User Row, since it's not included in the normal CMSIS headers for the SAMD21.
See datasheet section 10.3.1 for full details.
*/
// clang-format off
typedef union {
struct {
uint64_t BOOTPROT:3; /*!< bit: 0.. 2 Used to select one of eight different bootloader sizes. */
uint64_t :1; /*!< bit: 3 Reserved */
uint64_t EEPROM:3; /*!< bit: 4.. 6 Used to select one of eight different EEPROM sizes. */
uint64_t :1; /*!< bit: 7 Reserved */
uint64_t BOD33_LEVEL:6; /*!< bit: 8..13 BOD33 threshold Level at power on. */
uint64_t BOD33_ENABLE:1; /*!< bit: 14 BOD33 enable at power on. */
uint64_t BOD33_ACTION:2; /*!< bit: 15..16 BOD33 action at power on. */
uint64_t :8; /*!< bit: 17..24 Reserved: Voltage Regulator Internal BOD (BOD12) configuration. These bits are written in production and must not be changed. */
uint64_t WDT_ENABLE:1; /*!< bit: 25 WDT enable at power on. */
uint64_t WDT_ALWAYSON:1; /*!< bit: 26 WDT always on at power on. */
uint64_t WDT_PERIOD:4; /*!< bit: 27..30 WDT period at power on. */
uint64_t WDT_WINDOW:4; /*!< bit: 31..34 WDT window at power on. */
uint64_t WDT_EWOFFSET:4; /*!< bit: 35..38 WDT early warning interrupt time at power on. */
uint64_t WDT_WEN:1; /*!< bit: 39 WDT timer window mode enable at power on. */
uint64_t BOD33_HYST:1; /*!< bit: 40 BOD33 hysteresis configuration at power on. */
uint64_t :1; /*!< bit: 41 Reserved: Voltage Regulator Internal BOD(BOD12) configuration. This bit is written in production and must not be changed. */
uint64_t :6; /*!< bit: 42..47 Reserved */
uint64_t LOCK:16; /*!< bit: 48..63 NVM Region Lock Bits. */
} bit;
uint64_t reg;
} USER_ROW_Type;
// clang-format on

typedef struct {
__IO USER_ROW_Type USER_ROW;
} NVM_USER_Type;

#define NVM_USER ((NVM_USER_Type*)NVMCTRL_USER)

void gem_fuses_check();
void gem_fuses_write(NVM_USER_Type fuses);
5 changes: 4 additions & 1 deletion firmware/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ static void init_() {
Core system initialization.
*/

/* Before doing *anything*, check the fuses. */
gem_fuses_check();

/* Enable the Micro Trace Buffer for better debug stacktraces. */
wntr_mtb_init();

Expand All @@ -52,7 +55,7 @@ static void init_() {
gem_nvm_init();

/* Tell the world who we are and how we got here. :) */
printf(wntr_build_info_string());
printf("%s\n", wntr_build_info_string());

/*
Peripheral setup
Expand Down

0 comments on commit 0d6cd8d

Please sign in to comment.