diff --git a/firmware/boards/TOAD_H7/ldscript.ld b/firmware/boards/TOAD_H7/ldscript.ld index 94020e6..4b7b5ab 100644 --- a/firmware/boards/TOAD_H7/ldscript.ld +++ b/firmware/boards/TOAD_H7/ldscript.ld @@ -45,7 +45,8 @@ _Min_Stack_Size = 0x400; /* required amount of stack */ MEMORY { RAM_D1 (xrw) : ORIGIN = 0x24000000, LENGTH = 512K - FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K /* Memory is divided. Actual start is 0x08000000 and actual length is 2048K */ + FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K /* Bank 1. Actual full flash is 2048K across two 1024K banks. */ + BPT_FLASH (rw) : ORIGIN = 0x081E0000, LENGTH = 128K DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K RAM_D2 (xrw) : ORIGIN = 0x30000000, LENGTH = 288K RAM_D3 (xrw) : ORIGIN = 0x38000000, LENGTH = 64K @@ -133,6 +134,13 @@ SECTIONS . = ALIGN(4); } >FLASH + /* Reserved for the NAND flash Bad Page Table */ + .bpt_table (NOLOAD) : + { + . = ALIGN(4); + _bpt_start = .; + } >BPT_FLASH + /* Used by the startup to initialize data */ _sidata = LOADADDR(.data); diff --git a/firmware/lib/logging/bad_page_table.cpp b/firmware/lib/logging/bad_page_table.cpp new file mode 100644 index 0000000..6869d0b --- /dev/null +++ b/firmware/lib/logging/bad_page_table.cpp @@ -0,0 +1,257 @@ +/** + * @file bad_page_table.cpp + * @brief Bad Page Table Implementation for STM32H7 internal flash + * @note There are effectively two bad page tables, one is the initial + * table filled only at init and the other is the entries table, filled + * dynamically over time. + * + * @author Daniel Proano (dproano@purdue.edu) + */ + +#include + +#include "bad_page_table.h" +#include "flash.h" +#include "stm32h7xx_hal_flash.h" +#include "stm32h7xx_hal_flash_ex.h" + +#include "CommsSerial.h" +#include +#include + +extern uint32_t _bpt_start; + +#define STM32_FLASH_WORD_SIZE 32u +#define BPT_EMPTY_ENTRY 0xFFFFFFFFu + +namespace Bad_Page_Table { + +namespace { + +constexpr uint32_t BPT_MAGIC_NUMBER = 0x0123ABCDu; +constexpr uint32_t BPT_MAX_ENTRIES = + (FLASH_SECTOR_SIZE - STM32_FLASH_WORD_SIZE - NUM_BYTES_TRACKING_BLOCKS) / STM32_FLASH_WORD_SIZE; + +uint32_t *bpt_magic = &_bpt_start; +uint8_t *bpt_table = reinterpret_cast(&_bpt_start) + STM32_FLASH_WORD_SIZE; +uint8_t *bpt_entries = bpt_table + NUM_BYTES_TRACKING_BLOCKS; + +bool program_flash_word(uint32_t flash_addr, uint32_t word_addr) { + HAL_StatusTypeDef status; + + HAL_FLASH_Unlock(); + // Flash Typeprogram flashword means program 1 flash word, 32 bytes on the H7 + status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_FLASHWORD, flash_addr, word_addr); + HAL_FLASH_Lock(); + + return status == HAL_OK; +} + +uint32_t get_entry_addr(uint32_t index) { + return reinterpret_cast(bpt_entries + index * STM32_FLASH_WORD_SIZE); +} + +uint32_t get_entry(uint32_t index) { + return *reinterpret_cast(get_entry_addr(index)); +} + +uint32_t get_bpt_bad_byte(uint32_t block_num) { + return block_num / 8; +} + +uint8_t get_bpt_bad_bit(uint32_t block_num) { + return (1 << (block_num % 8)); +} + +int generate_key() { + const int key_min = 1000; + const int key_max = 10000; + srand(millis()); + return (rand() % (key_max - key_min) + key_min); +} + +// bad block from factory +bool is_initial_bad_block(uint32_t block_num) { + return !(bpt_table[get_bpt_bad_byte(block_num)] & get_bpt_bad_bit(block_num)); +} + +bool get_runtime_bad_page(uint32_t block_num, uint32_t *out_page) { + *out_page = NAND_NUM_PAGES; + + // find the bad page in the block + for (uint32_t i = 0; i < BPT_MAX_ENTRIES; i++) { + uint32_t entry = get_entry(i); + + // last entry already seen, no more entries + if (entry == BPT_EMPTY_ENTRY) { + break; + } + + if (entry / NAND_PAGES_PER_BLOCK == block_num && entry < *out_page) { + *out_page = entry; + } + } + + return *out_page != NAND_NUM_PAGES; +} + +bool add_entry(uint32_t page_address) { + uint32_t block_num = page_address / NAND_PAGES_PER_BLOCK; + + for (uint32_t i = 0; i < BPT_MAX_ENTRIES; i++) { + if (get_entry(i) != BPT_EMPTY_ENTRY) { + continue; + } + + uint8_t word_buf[STM32_FLASH_WORD_SIZE]; + memset(word_buf, PAGE_MARKED_GOOD, sizeof(word_buf)); + memcpy(word_buf, &page_address, sizeof(page_address)); + + uint32_t word_buf_addr = reinterpret_cast(word_buf); + uint32_t entry_addr = get_entry_addr(i); + + if (!program_flash_word(entry_addr, word_buf_addr)) { + CommsSerial.printf("WARNING: failed to mark page %lu (block %lu) bad.\n", page_address, block_num); + return false; + } + + return true; + } + + CommsSerial.printf("WARNING: Bad Page Table full, block %lu not marked bad.\n", block_num); + return false; +} + +bool scan_factory_bad_blocks() { + uint8_t table_buf[NUM_BYTES_TRACKING_BLOCKS]; + memset(table_buf, PAGE_MARKED_GOOD, sizeof(table_buf)); + + for (uint32_t i = 0; i < NAND_NUM_BLOCKS; i++) { + uint32_t cur_page = i * NAND_PAGES_PER_BLOCK; + + uint8_t bad_block_mark; + if (Flash::read_spare(cur_page, 0, &bad_block_mark, 1, false) != FLASH_SUCCESS) { + return false; + } + + if (bad_block_mark != PAGE_MARKED_GOOD) { + table_buf[get_bpt_bad_byte(i)] &= ~get_bpt_bad_bit(i); + } + } + + // flash can only be programmed once per erase, in 32 byte aligned chunks + for (uint32_t offset = 0; offset < NUM_BYTES_TRACKING_BLOCKS; offset += STM32_FLASH_WORD_SIZE) { + uint32_t flash_addr = reinterpret_cast(bpt_table + offset); + uint32_t table_addr = reinterpret_cast(table_buf + offset); + if (!program_flash_word(flash_addr, table_addr)) { + return false; + } + } + + return true; +} + +} // namespace + +bool begin(void) { + if (*bpt_magic == BPT_MAGIC_NUMBER) { + return true; + } + + int key = generate_key(); + + CommsSerial.println("WARNING: Bad Page Table not initialized - a factory bad-block scan is required."); + CommsSerial.printf("Enter %d to proceed: \n", key); + + char *res = CommsSerial.readline(); + if (atoi(res) != key) { + CommsSerial.println("Incorrect key entered."); + return false; + } + + // erase bad page table + FLASH_EraseInitTypeDef erase_init = { + .TypeErase = FLASH_TYPEERASE_SECTORS, + .Banks = FLASH_BANK_2, + .Sector = FLASH_SECTOR_7, + .NbSectors = 1, + .VoltageRange = FLASH_VOLTAGE_RANGE_3 + }; + uint32_t sector_error; + + HAL_FLASH_Unlock(); + bool erase_ok = HAL_FLASHEx_Erase(&erase_init, §or_error) == HAL_OK; + HAL_FLASH_Lock(); + + if (!erase_ok) { + CommsSerial.println("Bad Page Table erase failed."); + return false; + } + + CommsSerial.println("Scanning for factory bad blocks. This may take a while."); + + if (!scan_factory_bad_blocks()) { + CommsSerial.println("Factory bad-block scan failed."); + return false; + } + + uint8_t word_buf[STM32_FLASH_WORD_SIZE]; + memset(word_buf, 0, sizeof(word_buf)); + memcpy(word_buf, &BPT_MAGIC_NUMBER, sizeof(BPT_MAGIC_NUMBER)); + + uint32_t flash_magic_addr = reinterpret_cast(bpt_magic); + uint32_t word_buf_addr = reinterpret_cast(word_buf); + + if (!program_flash_word(flash_magic_addr, word_buf_addr)) { + CommsSerial.println("Flash program failed"); + return false; + } + + CommsSerial.println("Bad Page Table initialized."); + return true; +}; + +page_status_t get_page_status(uint32_t page_address) { + if (page_address >= NAND_NUM_PAGES) { + return PAGE_BAD; + } + + uint32_t block_num = page_address / NAND_PAGES_PER_BLOCK; + if (is_initial_bad_block(block_num)) { + return PAGE_BAD; + } + + uint32_t failed_page; + if (!get_runtime_bad_page(block_num, &failed_page)) { + return PAGE_GOOD; + } + + return page_address < failed_page ? PAGE_READ_ONLY : PAGE_BAD; +} + +bool mark_page_bad(uint32_t page_address) { + if (page_address >= NAND_NUM_PAGES) { + return false; + } + + if (get_page_status(page_address) != PAGE_GOOD) { + return true; + } + + return add_entry(page_address); +}; + +bool retire_block(uint32_t page_address) { + if (page_address >= NAND_NUM_PAGES) { + return false; + } + + uint32_t first_page = page_address / NAND_PAGES_PER_BLOCK * NAND_PAGES_PER_BLOCK; + if (get_page_status(first_page) == PAGE_BAD) { + return true; + } + + return add_entry(first_page); +} + +} // namespace Bad_Page_Table \ No newline at end of file diff --git a/firmware/lib/logging/bad_page_table.h b/firmware/lib/logging/bad_page_table.h new file mode 100644 index 0000000..df7a64a --- /dev/null +++ b/firmware/lib/logging/bad_page_table.h @@ -0,0 +1,33 @@ +/** + * @file bad_page_table.h + * @brief Bad Page Table Header + * + * @author Daniel Proano (dproano@purdue.edu) + */ + +#pragma once + +#include "flash_defs.h" +#include + +#define NUM_BITS (NAND_NUM_BLOCKS) +#define NUM_BYTES_TRACKING_BLOCKS (NUM_BITS / 8) +#define PAGE_MARKED_GOOD (0xFF) + +typedef enum { + PAGE_GOOD, + PAGE_READ_ONLY, + PAGE_BAD +} page_status_t; + +namespace Bad_Page_Table { + +bool begin(); + +page_status_t get_page_status(uint32_t address); + +bool mark_page_bad(uint32_t address); + +bool retire_block(uint32_t address); + +} diff --git a/firmware/lib/logging/flash.cpp b/firmware/lib/logging/flash.cpp new file mode 100644 index 0000000..79e383f --- /dev/null +++ b/firmware/lib/logging/flash.cpp @@ -0,0 +1,493 @@ +/** + * @file flash.cpp + * @brief NAND flash implementation for GD5F1GQ5UEYIGR chip + * + * @author Daniel Proano (dproano@purdue.edu) + */ + +#include "flash.h" +#include "CommsSerial.h" +#include "ec_pins.h" +#include "flash_defs.h" +#include "stm32h7xx_hal_qspi.h" + +#include +#include +#include +#include + +void HAL_QSPI_MspInit(QSPI_HandleTypeDef *) { + __HAL_RCC_QSPI_CONFIG(RCC_QSPICLKSOURCE_D1HCLK); + __HAL_RCC_QSPI_CLK_ENABLE(); + + __HAL_RCC_QSPI_FORCE_RESET(); + __HAL_RCC_QSPI_RELEASE_RESET(); + + pinmap_pinout(digitalPinToPinName(PIN_QSPI_CLK), PinMap_QUADSPI_SCLK); + pinmap_pinout(digitalPinToPinName(PIN_QSPI_IO0), PinMap_QUADSPI_DATA0); + pinmap_pinout(digitalPinToPinName(PIN_QSPI_IO1), PinMap_QUADSPI_DATA1); + pinmap_pinout(digitalPinToPinName(PIN_QSPI_IO2), PinMap_QUADSPI_DATA2); + pinmap_pinout(digitalPinToPinName(PIN_QSPI_IO3), PinMap_QUADSPI_DATA3); + pinmap_pinout(digitalPinToPinName(PIN_QSPI_CS), PinMap_QUADSPI_SSEL); +} + +namespace Flash { + +namespace { + +QSPI_HandleTypeDef hqspi; + +// Header phase command +#define QSPI_CMD_OR_RETURN(cmd_ptr, fail_ret) \ + do { \ + if (HAL_QSPI_Command(&hqspi, (cmd_ptr), QSPI_TIMEOUT_MS) != HAL_OK) \ + return (fail_ret); \ + } while (0) + +// Data phase command +#define QSPI_TRANSMIT_OR_RETURN(data_ptr, fail_ret) \ + do { \ + if (HAL_QSPI_Transmit(&hqspi, (data_ptr), QSPI_TIMEOUT_MS) != HAL_OK) \ + return (fail_ret); \ + } while (0) + +// Receives a QSPI data phase +#define QSPI_RECV_OR_RETURN(data_ptr, fail_ret) \ + do { \ + if (HAL_QSPI_Receive(&hqspi, (data_ptr), QSPI_TIMEOUT_MS) != HAL_OK) \ + return (fail_ret); \ + } while (0) + +typedef struct { + uint32_t opcode; + uint32_t addr = UINT32_MAX; // no address if max address + uint32_t addr_size = QSPI_ADDRESS_8_BITS; + uint32_t data_size = 0; // 0 = no data phase + uint32_t dummy_cycles = 0; + uint32_t data_lines = QSPI_DATA_1_LINE; +} cmd_spec; + +QSPI_CommandTypeDef build_cmd(cmd_spec spec) { + QSPI_CommandTypeDef out_cmd; + memset(&out_cmd, 0, sizeof(out_cmd)); + + out_cmd.Instruction = spec.opcode; + // The NAND chip only recognizes instructions + // on one line, even though the QSPI supports + // using more for instructions + out_cmd.InstructionMode = QSPI_INSTRUCTION_1_LINE; + out_cmd.DummyCycles = spec.dummy_cycles; + + if (spec.addr != UINT32_MAX) { + out_cmd.Address = spec.addr; + out_cmd.AddressSize = spec.addr_size; + out_cmd.AddressMode = QSPI_ADDRESS_1_LINE; + } + + if (spec.data_size != 0) { + out_cmd.DataMode = spec.data_lines; + out_cmd.NbData = spec.data_size; + } + + return out_cmd; +} + +// load directly from register, only need 1 Instr. CMD + 1 Receive +static bool get_feature(uint8_t feature_addr, uint8_t *data_out) { + cmd_spec new_cmd_spec = { + .opcode = CMD_NAND_GET_FEATURES, + .addr = feature_addr, + .data_size = 1, + }; + QSPI_CommandTypeDef cmd = build_cmd(new_cmd_spec); + QSPI_CMD_OR_RETURN(&cmd, false); + QSPI_RECV_OR_RETURN(data_out, false); + + return true; +} + +bool get_status_a(flash_status_a_t *data_out) { + uint8_t data; + if (!get_feature(FEATURE_ADDR_STATUS_A, &data)) { + return false; + } + + memcpy(data_out, &data, sizeof(data)); + return true; +}; + +bool get_status_b(flash_status_b_t *data_out) { + uint8_t data; + if (!get_feature(FEATURE_ADDR_STATUS_B, &data)) { + return false; + } + + memcpy(data_out, &data, sizeof(data)); + return true; +}; + +flash_error_t wait_until_ready(uint32_t timeout) { + uint32_t start_time = micros(); + + while (true) { + flash_status_a_t data; + if (!get_status_a(&data)) { + return FLASH_FAIL; + } + + // if operation no longer in progress + if (!data.OIP) { + return FLASH_READY; + } + + if (micros() - start_time > timeout) { + return FLASH_TIMED_OUT; + } + } +} + +bool disable_block_protection() { + /* page 49 of GD5F1 datasheet */ + + cmd_spec disable_spec = { + .opcode = CMD_NAND_SET_FEATURES, + .addr = FEATURE_ADDR_BLOCK_LOCK, + .data_size = 1 + }; + + QSPI_CommandTypeDef cmd = build_cmd(disable_spec); + QSPI_CMD_OR_RETURN(&cmd, false); + + uint8_t unlock = 0x00; + if (HAL_QSPI_Transmit(&hqspi, &unlock, QSPI_TIMEOUT_MS) != HAL_OK) { + return false; + } + + // ensure write unlock succeedded + uint8_t protection_reg; + if (!get_feature(FEATURE_ADDR_BLOCK_LOCK, &protection_reg)) { + return false; + } + + if (protection_reg != unlock) { + return false; + } + + return true; +} + +bool init_qspi_peripheral() { + // See page 1261 of STM32H7 dm00392525 user manual + uint32_t f_hclk = HAL_RCC_GetHCLKFreq(); + + hqspi.Instance = QUADSPI; + + // Clock Prescaler here is 32 bits but + // must be a value between 0 and 255 per + // stm32h7xx_hal_qspi.h:53 + hqspi.Init.ClockPrescaler = (f_hclk + FREQ_MAX - 1) / FREQ_MAX - 1; + if (hqspi.Init.ClockPrescaler > 255) + hqspi.Init.ClockPrescaler = 255; + + hqspi.Init.FifoThreshold = 1; + hqspi.Init.ClockMode = QSPI_CLOCK_MODE_0; + hqspi.Init.SampleShifting = QSPI_SAMPLE_SHIFTING_NONE; + hqspi.Init.ChipSelectHighTime = QSPI_CS_HIGH_TIME_8_CYCLE; + + // 128 MB = 2^27 bytes + // Flash size + 1 = # of bits to address whole array + // Flash size = 27 - 1 = 26 + hqspi.Init.FlashSize = 26; + hqspi.Init.FlashID = QSPI_FLASH_ID_1; + + // Dual flash defaults to disabled + + // Internally calls MspInit + return HAL_QSPI_Init(&hqspi) == HAL_OK; +} + +bool reset_and_check_id() { + cmd_spec reset_cmd = { + .opcode = CMD_NAND_RESET, + }; + QSPI_CommandTypeDef cmd = build_cmd(reset_cmd); + QSPI_CMD_OR_RETURN(&cmd, false); + + // wait for reset to happen + delayMicroseconds(RESET_MAX_DELAY_US); + + // read ID bytes + // read page 14 of DS-00889 + cmd_spec read_cmd = { + .opcode = CMD_NAND_READ_ID, + .data_size = 2, + .dummy_cycles = 8, + }; + cmd = build_cmd(read_cmd); + QSPI_CMD_OR_RETURN(&cmd, false); + + uint8_t id[2]; + QSPI_RECV_OR_RETURN(id, false); + + if (id[0] != MANUFACTURER_ID || id[1] != DEVICE_ID) { + CommsSerial.print("Bad Chip ID Values"); + return false; + } + + return true; +} + +bool enable_quad_mode() { + // tell registers to send config + cmd_spec get_config_cmd = { + .opcode = CMD_NAND_GET_FEATURES, + .addr = FEATURE_ADDR_CONFIG, + .data_size = 1, + }; + QSPI_CommandTypeDef cmd = build_cmd(get_config_cmd); + QSPI_CMD_OR_RETURN(&cmd, false); + + // receive config + uint8_t config; + QSPI_RECV_OR_RETURN(&config, false); + + config |= CONFIG_QE_BIT; + + // write updated config back + cmd_spec set_config_cmd = { + .opcode = CMD_NAND_SET_FEATURES, + .addr = FEATURE_ADDR_CONFIG, + .data_size = 1, + }; + cmd = build_cmd(set_config_cmd); + QSPI_CMD_OR_RETURN(&cmd, false); + + if (HAL_QSPI_Transmit(&hqspi, &config, QSPI_TIMEOUT_MS) != HAL_OK) + return false; + + // confirm QE bit was set + cmd_spec read_status_cmd = { + .opcode = CMD_NAND_GET_FEATURES, + .addr = FEATURE_ADDR_CONFIG, + .data_size = 1, + }; + cmd = build_cmd(read_status_cmd); + QSPI_CMD_OR_RETURN(&cmd, false); + + uint8_t config_check; + QSPI_RECV_OR_RETURN(&config_check, false); + + if (!(config_check & CONFIG_QE_BIT)) + return false; + + return true; +} + +flash_error_t read_at_col(uint32_t addr, uint32_t col, uint8_t *data_out, size_t len, bool check_ecc) { + // Don't read past the chip's main page and spare page bounds + if (col + len > NAND_PAGE_SIZE + NAND_SPARE_PAGE_SIZE) { + return FLASH_FAIL; + } + + // load page into cache + cmd_spec page_load_cmd = { + .opcode = CMD_NAND_PAGE_READ, + .addr = addr, + .addr_size = QSPI_ADDRESS_24_BITS, + }; + QSPI_CommandTypeDef cmd = build_cmd(page_load_cmd); + QSPI_CMD_OR_RETURN(&cmd, FLASH_FAIL); + + // wait for page to arrive in cache + flash_error_t err = wait_until_ready(READ_ARRAY_WITH_ECC_MAX_DELAY_US); + if (err != FLASH_READY) { + return err; + } + + // tell cache to send data, starting at the given column offset + cmd_spec cache_send_cmd = { + .opcode = CMD_NAND_READ_FROM_CACHE_QUAD, + .addr = col, + .addr_size = QSPI_ADDRESS_16_BITS, + .data_size = static_cast(len), + .dummy_cycles = 8, + .data_lines = QSPI_DATA_4_LINES + }; + cmd = build_cmd(cache_send_cmd); + QSPI_CMD_OR_RETURN(&cmd, FLASH_FAIL); + + // receive data from cache across spi + QSPI_RECV_OR_RETURN(data_out, FLASH_FAIL); + + // if we dont have to check ecc, end early + if (!check_ecc) { + return FLASH_SUCCESS; + } + + // load status register + flash_status_a_t status_a; + memset(&status_a, 0, sizeof(status_a)); + if (!get_status_a(&status_a)) { + return FLASH_FAIL; + } + + // check whether read was ok + if (status_a.ECCS == UNCORRECTED_ERROR) { + return FLASH_BAD_BLOCK; + } + + return FLASH_SUCCESS; +} + +} // namespace + +flash_error_t read_main(uint32_t addr, uint32_t offset, uint8_t *out, size_t len, bool check_ecc) { + if (offset + len > NAND_PAGE_SIZE) { + return FLASH_FAIL; + } + + return read_at_col(addr, offset, out, len, check_ecc); +} + +flash_error_t read_spare(uint32_t addr, uint32_t offset, uint8_t *out, size_t len, bool check_ecc) { + if (offset + len > NAND_SPARE_PAGE_SIZE) { + return FLASH_FAIL; + } + + return read_at_col(addr, NAND_PAGE_SIZE + offset, out, len, check_ecc); +} + +flash_error_t write_to_cache(uint32_t col, uint8_t *data, size_t len) { + // does cache page have enough space? + if (col + len > NAND_PAGE_SIZE) { + return FLASH_FAIL; + } + + // Program Load at col 0 clears the cache, Load Random keeps existing content + uint32_t nand_cmd = col == 0 ? CMD_NAND_PROGRAM_LOAD : CMD_NAND_PROGRAM_LOAD_RANDOM; + + // load data into cache + cmd_spec load_cmd = { + .opcode = nand_cmd, + .addr = col, + .addr_size = QSPI_ADDRESS_16_BITS, + .data_size = len + }; + QSPI_CommandTypeDef cmd = build_cmd(load_cmd); + QSPI_CMD_OR_RETURN(&cmd, FLASH_FAIL); + + // send data across spi into cache + QSPI_TRANSMIT_OR_RETURN(data, FLASH_FAIL); + + return FLASH_SUCCESS; +} + +flash_error_t program(uint32_t addr) { + // chip can lose commands if it is busy when instruction occurs + // ensure worst case timeout is waited until chip free + flash_error_t err = wait_until_ready(WORST_CASE_PROGRAM_TIMEOUT); + if (err != FLASH_READY) + return err; + + cmd_spec write_enable_cmd = { + .opcode = CMD_NAND_WRITE_ENABLE + }; + QSPI_CommandTypeDef cmd = build_cmd(write_enable_cmd); + QSPI_CMD_OR_RETURN(&cmd, FLASH_FAIL); + + cmd_spec commit_cache_cmd = { + .opcode = CMD_NAND_PROGRAM_EXECUTE, + .addr = addr, + .addr_size = QSPI_ADDRESS_24_BITS + }; + cmd = build_cmd(commit_cache_cmd); + QSPI_CMD_OR_RETURN(&cmd, FLASH_FAIL); + + // wait for cache to be written + err = wait_until_ready(WORST_CASE_MEMORY_TIMEOUT); + + if (err != FLASH_READY) { + return err; + } + + // ensure successful write + flash_status_a_t status_a; + memset(&status_a, 0, sizeof(status_a)); + if (!get_status_a(&status_a)) { + return FLASH_FAIL; + } + + if (status_a.P_FAIL) { + return FLASH_BAD_BLOCK; + } + + return FLASH_SUCCESS; +} + +flash_error_t erase_block(uint32_t addr) { + // ensure chip is done with any previous erases + flash_error_t err = wait_until_ready(WORST_CASE_MEMORY_TIMEOUT); + if (err != FLASH_READY) + return err; + + cmd_spec enable_write_cmd = { + .opcode = CMD_NAND_WRITE_ENABLE, + }; + QSPI_CommandTypeDef cmd = build_cmd(enable_write_cmd); + QSPI_CMD_OR_RETURN(&cmd, FLASH_FAIL); + + cmd_spec erase_cmd = { + .opcode = CMD_NAND_BLOCK_ERASE, + .addr = addr, + .addr_size = QSPI_ADDRESS_24_BITS + }; + cmd = build_cmd(erase_cmd); + QSPI_CMD_OR_RETURN(&cmd, FLASH_FAIL); + + // wait for erase to finish + err = wait_until_ready(WORST_CASE_MEMORY_TIMEOUT); + if (err != FLASH_READY) { + return err; + } + + // ensure erase was successful + flash_status_a_t status_a; + memset(&status_a, 0, sizeof(status_a)); + if (!get_status_a(&status_a)) { + return FLASH_FAIL; + } + + if (status_a.E_FAIL) { + return FLASH_BAD_BLOCK; + } + + return FLASH_SUCCESS; +} + +bool begin() { + if (!init_qspi_peripheral()) { + CommsSerial.println("QSPI Init Failed"); + return false; + } + + if (!reset_and_check_id()) { + CommsSerial.println("NAND reset/ID check failed"); + return false; + } + + if (!disable_block_protection()) { + CommsSerial.println("NAND disable block protection failed"); + return false; + } + + if (!enable_quad_mode()) { + CommsSerial.println("NAND enable quad mode failed"); + return false; + } + + CommsSerial.println("NAND flash driver ready"); + + return true; +} + +}; // namespace Flash diff --git a/firmware/lib/logging/flash.h b/firmware/lib/logging/flash.h new file mode 100644 index 0000000..04086e4 --- /dev/null +++ b/firmware/lib/logging/flash.h @@ -0,0 +1,50 @@ +/** + * @file flash.h + * @brief NAND flash header for GD5F1GQ5UEYIGR chip + * + * @author Daniel Proano (dproano@purdue.edu) + */ +#pragma once + +#include +#include + +typedef enum { + FLASH_SUCCESS, + FLASH_READY, + FLASH_FAIL, + FLASH_TIMED_OUT, + FLASH_BAD_BLOCK +} flash_error_t; + +typedef struct __attribute__((packed)) { + uint8_t OIP : 1; + uint8_t WEL : 1; + uint8_t E_FAIL : 1; + uint8_t P_FAIL : 1; + uint8_t ECCS : 2; + uint8_t RESERVED1 : 2; +} flash_status_a_t; + +typedef struct __attribute__((packed)) { + uint8_t RESERVED1 : 3; + uint8_t BPS : 1; + uint8_t ECCSE : 2; + uint8_t RESERVED2 : 2; +} flash_status_b_t; + +namespace Flash { + +bool begin(); + +flash_error_t read_main(uint32_t addr, uint32_t offset, uint8_t *out, size_t len, bool check_ecc = true); + +flash_error_t read_spare(uint32_t addr, uint32_t offset, uint8_t *out, size_t len, bool check_ecc = true); + +flash_error_t write_to_cache(uint32_t col, uint8_t *data, size_t len); + +flash_error_t program(uint32_t addr); + +flash_error_t erase_block(uint32_t addr); + +}; // namespace Flash diff --git a/firmware/lib/logging/flash_defs.h b/firmware/lib/logging/flash_defs.h new file mode 100644 index 0000000..86d63ea --- /dev/null +++ b/firmware/lib/logging/flash_defs.h @@ -0,0 +1,57 @@ +/** + * @file flash_defs.h + * @brief NAND flash definitions for the GD5F1GQ5xExxG chip + * + * @author Daniel Proano (dproano@purdue.edu) + */ + +#pragma once + +#define NAND_NUM_BLOCKS 1024u +#define NAND_PAGES_PER_BLOCK 64u +#define NAND_BYTES_PER_PAGE 2176u +#define NAND_PAGE_SIZE 2048u +#define NAND_SPARE_PAGE_SIZE (NAND_BYTES_PER_PAGE - NAND_PAGE_SIZE) +#define NAND_NUM_PAGES (NAND_PAGES_PER_BLOCK * NAND_NUM_BLOCKS) +#define NAND_NUM_BYTES_PER_BLOCK (NAND_PAGES_PER_BLOCK * NAND_BYTES_PER_PAGE) +#define NAND_NUM_BYTES (NAND_NUM_BYTES_PER_BLOCK * NAND_NUM_BLOCKS) + +#define CMD_NAND_WRITE_ENABLE 0x06u +#define CMD_NAND_WRITE_DISABLE 0x04u +#define CMD_NAND_GET_FEATURES 0x0Fu +#define CMD_NAND_SET_FEATURES 0x1Fu +#define CMD_NAND_PAGE_READ 0x13u +#define CMD_NAND_READ_FROM_CACHE 0x03u +#define CMD_NAND_READ_FROM_CACHE_QUAD 0x6Bu +#define CMD_NAND_READ_FROM_CACHE_QUAD_IO 0xEBu +#define CMD_NAND_READ_ID 0x9Fu +#define CMD_NAND_PROGRAM_LOAD 0x02u +#define CMD_NAND_PROGRAM_LOAD_QUAD 0x32u +#define CMD_NAND_PROGRAM_EXECUTE 0x10u +#define CMD_NAND_PROGRAM_LOAD_RANDOM 0x84u +#define CMD_NAND_PROGRAM_LOAD_RANDOM_QUAD 0xC4u +#define CMD_NAND_BLOCK_ERASE 0xD8u +#define CMD_NAND_RESET 0xFFu + +#define FEATURE_ADDR_BLOCK_LOCK 0xA0u +#define FEATURE_ADDR_CONFIG 0xB0u +#define FEATURE_ADDR_STATUS_A 0xC0u +#define FEATURE_ADDR_STATUS_B 0xF0u + +#define CONFIG_QE_BIT (1u << 0) + +// chips limit is 104 MHz for 1.8V or 133 MHz for 3.3 V +#define FREQ_MAX 50'000'000 +#define QSPI_TIMEOUT_MS 10000 +#define WORST_CASE_PROGRAM_TIMEOUT 600 +#define WORST_CASE_MEMORY_TIMEOUT 10'000 + +// MID & DID ID values for GD5F1GQ5UExxG +#define MANUFACTURER_ID 0xC8u +#define DEVICE_ID 0x51u + +// Cmd worst case timing +#define RESET_MAX_DELAY_US 500 +#define READ_ARRAY_WITH_ECC_MAX_DELAY_US 60 + +#define UNCORRECTED_ERROR 2 \ No newline at end of file diff --git a/firmware/lib/logging/logging.cpp b/firmware/lib/logging/logging.cpp new file mode 100644 index 0000000..cdf7094 --- /dev/null +++ b/firmware/lib/logging/logging.cpp @@ -0,0 +1,302 @@ +/** + * @file logging.cpp + * @brief Packed struct logging to NAND flash for GD5F1GQ5UEYIGR chip + * + * @author Daniel Proano (dproano@purdue.edu) + */ + +#include + +#include "logging.h" +#include "CommsSerial.h" + +#include + +#define DUMP_CONTINUE_CHAR 'c' +#define DUMP_TIMEOUT_MS 5000u +#define DumpSerial SerialUSB + +namespace Logging { + +namespace { + +uint8_t page_buf[NAND_PAGE_SIZE]; +uint8_t read_buf[NAND_PAGE_SIZE]; +uint32_t page_offset = 0; +uint32_t cur_page = NAND_NUM_PAGES; // init to one past the last valid page + +uint32_t skip_to_next_block(uint32_t page) { + return (page / NAND_PAGES_PER_BLOCK + 1) * NAND_PAGES_PER_BLOCK; +} + +uint32_t first_good_page_from(uint32_t page) { + while (page < NAND_NUM_PAGES && Bad_Page_Table::get_page_status(page) != PAGE_GOOD) { + // if one page in a block is bad, then all 64 pages are marked bad + // and we can just skip to the next block + page = skip_to_next_block(page); + } + + return page; +} + +void reset_page_buf() { + memset(page_buf, LOG_EMPTY_BYTE, sizeof(page_buf)); + page_offset = 0; +} + +bool page_is_empty(uint32_t page) { + uint8_t first_byte; + if (Flash::read_main(page, 0, &first_byte, 1) != FLASH_SUCCESS) { + return false; + } + + return first_byte == LOG_EMPTY_BYTE; +} + +// Find the first empty good page to resume writing +uint32_t find_write_page() { + uint32_t last_used_block = UINT32_MAX; + + for (uint32_t block = 0; block < NAND_NUM_BLOCKS; block++) { + uint32_t first_page = block * NAND_PAGES_PER_BLOCK; + if (Bad_Page_Table::get_page_status(first_page) != PAGE_GOOD) { + continue; + } + + if (page_is_empty(first_page)) { + break; + } + + last_used_block = block; + } + + if (last_used_block == UINT32_MAX) { + return first_good_page_from(0); + } + + uint32_t page = last_used_block * NAND_PAGES_PER_BLOCK; + uint32_t block_end = page + NAND_PAGES_PER_BLOCK; + while (page < block_end && !page_is_empty(page)) { + page++; + } + + return first_good_page_from(page); +} + +bool commit_page() { + if (Flash::write_to_cache(0, page_buf, NAND_PAGE_SIZE) != FLASH_SUCCESS) { + CommsSerial.println("Write to cache failed"); + return false; + } + + flash_error_t err = Flash::program(cur_page); + + if (err == FLASH_BAD_BLOCK) { + if (!Bad_Page_Table::mark_page_bad(cur_page)) { + CommsSerial.printf("While committing page %lu, it was found to be bad but marking it as such failed\n", cur_page); + + // skip to the next block manually + cur_page = skip_to_next_block(cur_page); + } + cur_page = first_good_page_from(cur_page); + } + + if (cur_page >= NAND_NUM_PAGES) { + return false; + } + + if (err != FLASH_SUCCESS) { + return false; + } + + reset_page_buf(); + cur_page = first_good_page_from(cur_page + 1); + + return true; +} + +bool wait_for_continue() { + uint32_t start = millis(); + + while (!DumpSerial.available()) { + if (millis() - start > DUMP_TIMEOUT_MS) { + return false; + } + } + + return DumpSerial.read() == DUMP_CONTINUE_CHAR; +} + +flash_error_t read_page(uint32_t page_addr) { + return Flash::read_main(page_addr, 0, read_buf, NAND_PAGE_SIZE); +} + +bool send_page() { + if (!wait_for_continue()) { + return false; + } + + uint8_t checksum_a = 0; + uint8_t checksum_b = 0; + for (uint32_t i = 0; i < NAND_PAGE_SIZE; i++) { + checksum_a += read_buf[i]; + checksum_b += checksum_a; + } + + DumpSerial.write(read_buf, NAND_PAGE_SIZE); + DumpSerial.write(checksum_a); + DumpSerial.write(checksum_b); + return true; +} + +bool send_end_page() { + memset(read_buf, LOG_EMPTY_BYTE, sizeof(read_buf)); + return send_page(); +} + +bool send_log() { + uint32_t page = 0; + while (page < NAND_NUM_PAGES) { + page_status_t status = Bad_Page_Table::get_page_status(page); + + if (status == PAGE_BAD) { + page = skip_to_next_block(page); + continue; + } + + if (read_page(page) != FLASH_SUCCESS) { + page++; + continue; + } + + if (read_buf[0] == LOG_EMPTY_BYTE) { + // end of logs found + if (status == PAGE_GOOD) { + break; + } + + // otherwise, page is bad, go to next block + page = skip_to_next_block(page); + continue; + } + + if (!send_page()) { + return false; + } + + page++; + } + + // send an empty page to indicate no more data + return send_end_page(); +} + +} // namespace + +bool begin() { + if (!Flash::begin()) { + CommsSerial.println("Flash init failed"); + return false; + } + + if (!Bad_Page_Table::begin()) { + CommsSerial.println("Bad Page Table Init failed"); + return false; + } + + reset_page_buf(); + + cur_page = find_write_page(); + + if (cur_page >= NAND_NUM_PAGES) { + CommsSerial.println("WARNING: NAND log is full, erase it before logging."); + } + + return true; +} + +bool erase() { + bool ok = true; + + CommsSerial.println("Erasing NAND log. This may take a while."); + + // loop through all blocks and erase filled blocks + for (uint32_t block = 0; block < NAND_NUM_BLOCKS; block++) { + uint32_t first_page = block * NAND_PAGES_PER_BLOCK; + page_status_t status = Bad_Page_Table::get_page_status(first_page); + if (status == PAGE_BAD) { + continue; + } + + if (status == PAGE_READ_ONLY) { + Flash::erase_block(first_page); + ok &= Bad_Page_Table::retire_block(first_page); + continue; + } + + flash_error_t err = Flash::erase_block(first_page); + + if (err == FLASH_BAD_BLOCK) { + // only takes one bad page marking to fail + ok &= Bad_Page_Table::mark_page_bad(first_page); + } + + if (err != FLASH_SUCCESS) { + ok = false; + } + } + + reset_page_buf(); + cur_page = first_good_page_from(0); + + if (cur_page >= NAND_NUM_PAGES) { + CommsSerial.println("Something is very wrong, first good page is outside flash"); + return false; + } + + CommsSerial.println("NAND log erased."); + return ok; +} + +bool write(uint8_t struct_id, uint8_t *data, size_t len) { + size_t entry_len = sizeof(struct_id) + len; + if (struct_id == LOG_EMPTY_BYTE || entry_len > NAND_PAGE_SIZE) { + return false; + } + + if (page_offset + entry_len > NAND_PAGE_SIZE) { + if (!commit_page()) { + return false; + } + } + + if (cur_page >= NAND_NUM_PAGES) { + return false; + } + + page_buf[page_offset] = struct_id; + memcpy(page_buf + page_offset + sizeof(struct_id), data, len); + page_offset += entry_len; + + return true; +} + +bool flush() { + // Either buffer is empty and nothing to flush or + // flash is full and cannot commit another page + if (page_offset == 0 || cur_page >= NAND_NUM_PAGES) { + return true; + } + + return commit_page(); +} + +bool dump() { + if (!flush()) { + return false; + } + + return send_log(); +} + +} // namespace Logging diff --git a/firmware/lib/logging/logging.h b/firmware/lib/logging/logging.h new file mode 100644 index 0000000..1d08fd3 --- /dev/null +++ b/firmware/lib/logging/logging.h @@ -0,0 +1,30 @@ +/** + * @file logging.h + * @brief Packed struct logging to NAND flash for GD5F1GQ5UEYIGR chip + * + * @author Daniel Proano (dproano@purdue.edu) + */ + +#pragma once + +#include "bad_page_table.h" +#include "flash.h" + +#include +#include + +#define LOG_EMPTY_BYTE 0xFFu + +namespace Logging { + +bool begin(); + +bool erase(); + +bool write(uint8_t struct_id, uint8_t *data, size_t len); + +bool flush(); + +bool dump(); + +} // namespace Logging diff --git a/firmware/platformio.ini b/firmware/platformio.ini index 9f8332f..caafc8c 100644 --- a/firmware/platformio.ini +++ b/firmware/platformio.ini @@ -56,6 +56,8 @@ board = TOAD_H7 build_flags = ${env.build_flags} -D TOAD_ENGINE_CONTROLLER_ONLY + -D USBCON + -D USBD_USE_CDC build_src_filter = -<**/*.cpp> diff --git a/firmware/src/ec_main.cpp b/firmware/src/ec_main.cpp index 5011fc4..2b2672e 100644 --- a/firmware/src/ec_main.cpp +++ b/firmware/src/ec_main.cpp @@ -2,9 +2,9 @@ #include "CommandRouter.h" #include "CommsSerial.h" -#include "ErrorCounters.h" #include "PressureSensors.h" #include "RCS.h" +#include "logging.h" #include "RS485.h" #include "SolenoidValves.h" #include "TVC_Actuators.h" @@ -12,6 +12,7 @@ #include "ThrottleValves.h" #include "ValveController.h" #include "fdcan_toad.h" +#include "ErrorCounters.h" // shared interfaces CommsSerial_t USB_CommsSerial; @@ -54,6 +55,7 @@ void setup() { all_modules_ok &= SolenoidValves::begin(); all_modules_ok &= TVC_Actuators::begin(); all_modules_ok &= ValveController::begin(); + all_modules_ok &= Logging::begin(); if (!all_modules_ok) { while (true) { diff --git a/firmware/src/prog_main.cpp b/firmware/src/prog_main.cpp index a3e479e..ea57832 100644 --- a/firmware/src/prog_main.cpp +++ b/firmware/src/prog_main.cpp @@ -8,7 +8,7 @@ // https://www.st.com/resource/en/application_note/an4286-how-to-use-spi-protocol-in-bootloader-on-stm32-mcus-stmicroelectronics.pdf // Citations commented as [pg#] -CommsSerial_t USB_CommsSerial; +// CommsSerial_t USB_CommsSerial; CommsSerial_t HW_CommsSerial(PIN_HW_COMM_SERIAL_RX, PIN_HW_COMM_SERIAL_TX); CommsSerial_t HW_FallbackSerial(PIN_HW_FALLBACK_SERIAL_RX, PIN_HW_FALLBACK_SERIAL_TX); @@ -51,7 +51,7 @@ void setup() { // All shared interfaces are begun here. // Use same baud rate on all Comm Serials for consistency. - USB_CommsSerial.begin(RADIO_BAUD); + // USB_CommsSerial.begin(RADIO_BAUD); HW_CommsSerial.begin(RADIO_BAUD); HW_FallbackSerial.begin(RADIO_BAUD);