From 44c1caf2b9cf05efbd7f4bce042193ab2e181ca9 Mon Sep 17 00:00:00 2001 From: Arksine Date: Wed, 3 Feb 2021 06:54:00 -0500 Subject: [PATCH] spi_flash: support for firmware upgrades via SD Card This module connects directly to MCU's previously flashed with Klipper, uploads Klipper firmware to an attached SD Card, and performs a device reset to intiate the bootloader's update process. Signed-off-by: Eric Callahan --- scripts/spi_flash/board_defs.py | 93 +++ scripts/spi_flash/fatfs_api.c | 232 +++++++ scripts/spi_flash/fatfs_api.h | 50 ++ scripts/spi_flash/fatfs_lib.py | 87 +++ scripts/spi_flash/spi_flash.py | 1112 +++++++++++++++++++++++++++++++ 5 files changed, 1574 insertions(+) create mode 100644 scripts/spi_flash/board_defs.py create mode 100644 scripts/spi_flash/fatfs_api.c create mode 100644 scripts/spi_flash/fatfs_api.h create mode 100644 scripts/spi_flash/fatfs_lib.py create mode 100644 scripts/spi_flash/spi_flash.py diff --git a/scripts/spi_flash/board_defs.py b/scripts/spi_flash/board_defs.py new file mode 100644 index 00000000..15b6315b --- /dev/null +++ b/scripts/spi_flash/board_defs.py @@ -0,0 +1,93 @@ +# SPI Flash board definitions +# +# Copyright (C) 2021 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license. + +########################################################### +# +# Board Definitions +# +########################################################### + +BOARD_DEFS = { + 'generic-lpc1768': { + 'mcu': "lpc1768", + 'spi_bus': "ssp1", + "cs_pin": "P0.6" + }, + 'generic-lpc1769': { + 'mcu': "lpc1769", + 'spi_bus': "ssp1", + "cs_pin": "P0.6" + }, + 'btt-skr-mini': { + 'mcu': "stm32f103xe", + 'spi_bus': "spi1", + "cs_pin": "PA4" + }, + 'flyboard-mini': { + 'mcu': "stm32f103xe", + 'spi_bus': "spi2", + "cs_pin": "PB12", + "current_firmware_path": "FLY.CUR" + }, + 'mks-robin-e3': { + 'mcu': "stm32f103xe", + 'spi_bus': "spi2", + "cs_pin": "PA15", + "firmware_path": "Robin_e3.bin", + "current_firmware_path": "Robin_e3.cur" + }, + 'btt-skr-pro': { + 'mcu': "stm32f407xx", + 'spi_bus': "swspi", + 'spi_pins': "PA6,PB5,PA5", + "cs_pin": "PA4" + }, + 'btt-gtr': { + 'mcu': "stm32f407xx", + 'spi_bus': "spi1", + "cs_pin": "PA4" + } +} + +########################################################### +# +# Board Definition Aliases +# +########################################################### + +BOARD_ALIASES = { + 'btt-skr-v1.1': BOARD_DEFS['generic-lpc1768'], + 'btt-skr-v1.3': BOARD_DEFS['generic-lpc1768'], + 'btt-skr-v1.4': BOARD_DEFS['generic-lpc1768'], + 'mks-sgenl-v1': BOARD_DEFS['generic-lpc1768'], + 'mks-sbase': BOARD_DEFS['generic-lpc1768'], + 'smoothieboard-v1': BOARD_DEFS['generic-lpc1769'], + 'btt-skr-turbo-v1.4': BOARD_DEFS['generic-lpc1769'], + 'btt-skr-e3-turbo': BOARD_DEFS['generic-lpc1769'], + 'mks-sgenl-v2': BOARD_DEFS['generic-lpc1769'], + 'btt-skr-mini-v1.1': BOARD_DEFS['btt-skr-mini'], + 'btt-skr-mini-e3-v1': BOARD_DEFS['btt-skr-mini'], + 'btt-skr-mini-e3-v1.2': BOARD_DEFS['btt-skr-mini'], + 'btt-skr-mini-e3-v2': BOARD_DEFS['btt-skr-mini'], + 'btt-skr-mini-mz': BOARD_DEFS['btt-skr-mini'], + 'btt-skr-e3-dip': BOARD_DEFS['btt-skr-mini'], + 'btt002-v1': BOARD_DEFS['btt-skr-mini'], + 'creality-v4.2.7': BOARD_DEFS['btt-skr-mini'], + 'btt-skr-pro-v1.1': BOARD_DEFS['btt-skr-pro'], + 'btt-skr-pro-v1.2': BOARD_DEFS['btt-skr-pro'], + 'btt-gtr-v1': BOARD_DEFS['btt-gtr'], + 'mks-robin-e3d': BOARD_DEFS['mks-robin-e3'], +} + +def list_boards(): + return sorted(list(BOARD_DEFS.keys()) + list(BOARD_ALIASES.keys())) + +def lookup_board(name): + name = name.lower() + bdef = BOARD_ALIASES.get(name, BOARD_DEFS.get(name, None)) + if bdef is not None: + return dict(bdef) + return None diff --git a/scripts/spi_flash/fatfs_api.c b/scripts/spi_flash/fatfs_api.c new file mode 100644 index 00000000..c3d5db1c --- /dev/null +++ b/scripts/spi_flash/fatfs_api.c @@ -0,0 +1,232 @@ +// Python CFFI bindings for fatfs +// +// Copyright (C) 2021 Eric Callahan +// +// This file may be distributed under the terms of the GNU GPLv3 license. + +#include // memset +#include // malloc +#include "fatfs_api.h" // python-fatfs prototypes +#include "../../lib/fatfs/ff.h" // fatfs APIs +#include "../../lib/fatfs/diskio.h" // fatfs media APIs (callbacks) + +/* Callbacks */ +static uint8_t (*python_disk_status)(void) = 0; +static uint8_t (*python_disk_initialize)(void) = 0; +static uint8_t (*python_disk_read)(uint8_t* buff, uint32_t sector, + unsigned int count) = 0; +static uint8_t (*python_disk_write)(const uint8_t* buff, uint32_t sector, + unsigned int count) = 0; +static uint8_t (*python_disk_ioctl)(uint8_t cmd, void* buff) = 0; +static uint32_t (*python_fattime)(void) = 0; + +void __visible +fatfs_set_callbacks( + uint8_t (*status_callback)(void), + uint8_t (*init_callback)(void), + uint8_t (*read_callback)(uint8_t*, uint32_t, unsigned int), + uint8_t (*write_callback)(const uint8_t*, uint32_t, unsigned int), + uint8_t (*ioctl_callback)(uint8_t, void*), + uint32_t (*fattime_callback)(void) +) +{ + python_disk_status = status_callback; + python_disk_initialize = init_callback; + python_disk_read = read_callback; + python_disk_write = write_callback; + python_disk_ioctl = ioctl_callback; + python_fattime = fattime_callback; +} + +void __visible +fatfs_clear_callbacks(void) +{ + python_disk_status = 0; + python_disk_initialize = 0; + python_disk_read = 0; + python_disk_write = 0; + python_disk_ioctl = 0; + python_fattime = 0; +} + +/* Callbacks from fatfs to python. These methods are used to */ +/* Access access the SD Card APIs */ + +/* Get FAT Time */ +DWORD +get_fattime(void) +{ + if (python_fattime == 0) + { + // Return a default FATTIME of 1/1/2021 + return 41 << 25 | 1 << 21 | 1 << 16; + } + return python_fattime(); +} + +DSTATUS +disk_status(BYTE pdrv) +{ + if (python_disk_status != 0) + return python_disk_status(); + + return STA_NOINIT; +} + +DSTATUS +disk_initialize(BYTE pdrv) +{ + if (python_disk_initialize != 0) + return python_disk_initialize(); + return STA_NOINIT; +} + + +DRESULT +disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) +{ + if (python_disk_read != 0) + return python_disk_read(buff, sector, count); + return RES_NOTRDY; +} + + +DRESULT +disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) +{ + if (python_disk_write != 0) + return python_disk_write(buff, sector, count); + return RES_NOTRDY; +} + +DRESULT +disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) +{ + if (python_disk_ioctl != 0) + return python_disk_ioctl(cmd, buff); + return RES_NOTRDY; +} + +/* Python-FatFS Interface Functions */ + +// Wrapper around the FatFS FIL struct. FIL is type defined +// anonymously, thus can't be forward declared and poses +// challenges with CFFI. +struct ff_file { + FIL fobj; +}; +static FATFS fs; + +uint8_t __visible +fatfs_mount(void) +{ + FRESULT res; + memset(&fs, 0, sizeof(fs)); + res = f_mount(&fs, "", 1); + return res; +} + +uint8_t __visible +fatfs_unmount(void) +{ + FRESULT res; + res = f_unmount(""); + memset(&fs, 0, sizeof(fs)); + return res; +} + +struct ff_file* __visible +fatfs_open(const char* path, uint8_t mode) +{ + FRESULT res; + struct ff_file* fhdl = malloc(sizeof(*fhdl)); + memset(fhdl, 0, sizeof(*fhdl)); + res = f_open(&(fhdl->fobj), path, mode); + if (res != FR_OK) { + free(fhdl); + fhdl = NULL; + } + return fhdl; +} + +uint8_t __visible +fatfs_close(struct ff_file* fhdl) +{ + FRESULT res; + res = f_close(&(fhdl->fobj)); + free(fhdl); + return res; +} + +int __visible +fatfs_read(struct ff_file* fhdl, void* rbuf, uint16_t btr) +{ + FRESULT res; + UINT bytes_read; + res = f_read(&(fhdl->fobj), rbuf, btr, &bytes_read); + if (res != FR_OK) { + return -res; + } + return bytes_read; +} + +int __visible +fatfs_write(struct ff_file* fhdl, const void* wbuf, uint16_t btw) +{ + FRESULT res; + UINT bytes_written; + res = f_write(&(fhdl->fobj), wbuf, btw, &bytes_written); + if (bytes_written < btw) { + return -res; + } + return bytes_written; +} + +// Remove a file or director +uint8_t __visible +fatfs_remove(const char* path) +{ + FRESULT res; + res = f_unlink(path); + return res; +} + +uint8_t __visible +fatfs_get_fstats(struct ff_file_info* finfo, const char* path) +{ + FRESULT res; + FILINFO nfo; + res = f_stat(path, &nfo); + if (res == FR_OK) + memcpy(finfo, &nfo, sizeof(nfo)); + return res; +} + +uint8_t __visible +fatfs_get_disk_info(struct ff_disk_info* dinfo) +{ + FRESULT res; + res = f_getlabel("", dinfo->label, &(dinfo->serial_number)); + if (res != FR_OK) + return res; + dinfo->fs_type = fs.fs_type; + return res; +} + +uint8_t __visible +fatfs_list_dir(struct ff_file_info* flist, uint8_t max_size, char* path) +{ + FRESULT res; + DIR dir; + FILINFO nfo; + res = f_opendir(&dir, path); + if (res == FR_OK) { + for (uint8_t i=0; i < max_size; i++) { + res = f_readdir(&dir, &nfo); + if (res != FR_OK ||nfo.fname[0] == 0) + break; + memcpy(&(flist[i]), &nfo, sizeof(nfo)); + } + } + return FR_OK; +} diff --git a/scripts/spi_flash/fatfs_api.h b/scripts/spi_flash/fatfs_api.h new file mode 100644 index 00000000..2400bc86 --- /dev/null +++ b/scripts/spi_flash/fatfs_api.h @@ -0,0 +1,50 @@ +// Python CFFI definitions for fatfs +// +// Copyright (C) 2021 Eric Callahan +// +// This file may be distributed under the terms of the GNU GPLv3 license. +#ifndef FATFS_API_H +#define FATFS_API_H + +#include // uint8_t + +#define __visible __attribute__((externally_visible)) + +void fatfs_set_callbacks( + uint8_t (*status_callback)(void), + uint8_t (*init_callback)(void), + uint8_t (*read_callback)(uint8_t*, uint32_t, unsigned int), + uint8_t (*write_callback)(const uint8_t*, uint32_t, unsigned int), + uint8_t (*ioctl_callback)(uint8_t, void*), + uint32_t (*fattime_callback)(void)); +void fatfs_clear_callbacks(void); + +struct ff_file; + +struct ff_file_info { + uint32_t size; + uint16_t modified_date; + uint16_t modified_time; + uint8_t attrs; + char name[13]; +}; + +struct ff_disk_info { + char label[12]; + uint32_t serial_number; + uint8_t fs_type; +}; + +uint8_t fatfs_mount(void); +uint8_t fatfs_unmount(void); +struct ff_file* fatfs_open(const char* path, uint8_t mode); +uint8_t fatfs_close(struct ff_file* fhdl); +int fatfs_read(struct ff_file* fhdl, void* rbuf, uint16_t btr); +int fatfs_write(struct ff_file* fhdl, const void* wbuf, uint16_t btw); +uint8_t fatfs_remove(const char* path); +uint8_t fatfs_get_fstats(struct ff_file_info* finfo, const char* path); +uint8_t fatfs_get_disk_info(struct ff_disk_info* dinfo); +uint8_t fatfs_list_dir(struct ff_file_info* flist, uint8_t max_size, + char* path); + +#endif diff --git a/scripts/spi_flash/fatfs_lib.py b/scripts/spi_flash/fatfs_lib.py new file mode 100644 index 00000000..5894bb52 --- /dev/null +++ b/scripts/spi_flash/fatfs_lib.py @@ -0,0 +1,87 @@ +# FatFS CFFI support +# +# Copyright (C) 2021 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import os +import sys +KLIPPER_DIR = os.path.abspath(os.path.join( + os.path.dirname(__file__), "../../")) +sys.path.append(os.path.join(KLIPPER_DIR, "klippy")) +import chelper + +DEST_LIB = "fatfs.so" +FATFS_DIR = os.path.join(KLIPPER_DIR, "lib/fatfs") +FATFS_SRC = ["ff.c", "ffsystem.c", "ffunicode.c"] +SPI_FLASH_SRC = ["fatfs_api.c"] +FATFS_HEADERS = ["diskio.h", "ff.h", "ffconf.h"] +SPI_FLASH_HEADERS = ["fatfs_api.h"] + +FATFS_CDEFS = """ + void fatfs_set_callbacks( + uint8_t (*status_callback)(void), + uint8_t (*init_callback)(void), + uint8_t (*read_callback)(uint8_t*, uint32_t, unsigned int), + uint8_t (*write_callback)(const uint8_t*, uint32_t, unsigned int), + uint8_t (*ioctl_callback)(uint8_t, void*), + uint32_t (*fattime_callback)(void)); + void fatfs_clear_callbacks(void); + + struct ff_file_info { + uint32_t size; + uint16_t modified_date; + uint16_t modified_time; + uint8_t attrs; + char name[13]; + }; + + struct ff_disk_info { + char label[12]; + uint32_t serial_number; + uint8_t fs_type; + }; + + uint8_t fatfs_mount(void); + uint8_t fatfs_unmount(void); + struct ff_file* fatfs_open(const char* path, uint8_t mode); + uint8_t fatfs_close(struct ff_file* fhdl); + int fatfs_read(struct ff_file* fhdl, void* rbuf, uint16_t btr); + int fatfs_write(struct ff_file* fhdl, const void* wbuf, uint16_t btw); + uint8_t fatfs_remove(const char* path); + uint8_t fatfs_get_fstats(struct ff_file_info* finfo, const char* path); + uint8_t fatfs_get_disk_info(struct ff_disk_info* dinfo); + uint8_t fatfs_list_dir(struct ff_file_info* flist, uint8_t max_size, + char* path); +""" + +fatfs_ffi_main = None +fatfs_ffi_lib = None + +def check_fatfs_build(printfunc=lambda o: o): + printfunc("Checking FatFS CFFI Build...\n") + ffi_main, ffi_lib = chelper.get_ffi() + srcdir = os.path.dirname(os.path.realpath(__file__)) + srcfiles = chelper.get_abs_files(FATFS_DIR, FATFS_SRC) + srcfiles.extend(chelper.get_abs_files(srcdir, SPI_FLASH_SRC)) + ofiles = chelper.get_abs_files(FATFS_DIR, FATFS_HEADERS) + ofiles.extend(chelper.get_abs_files(srcdir, SPI_FLASH_HEADERS)) + destlib = os.path.join(srcdir, DEST_LIB) + if chelper.check_build_code(srcfiles+ofiles+[__file__], destlib): + if chelper.check_gcc_option(chelper.SSE_FLAGS): + cmd = "%s %s %s" % (chelper.GCC_CMD, chelper.SSE_FLAGS, + chelper.COMPILE_ARGS) + else: + cmd = "%s %s" % (chelper.GCC_CMD, chelper.COMPILE_ARGS) + printfunc("Building FatFS shared library...") + os.system(cmd % (destlib, ' '.join(srcfiles))) + printfunc("Done\n") + global fatfs_ffi_main, fatfs_ffi_lib + ffi_main.cdef(FATFS_CDEFS) + fatfs_ffi_lib = ffi_main.dlopen(destlib) + fatfs_ffi_main = ffi_main + +def get_fatfs_ffi(): + global fatfs_ffi_main, fatfs_ffi_lib + if fatfs_ffi_main is None: + check_fatfs_build() + return fatfs_ffi_main, fatfs_ffi_lib diff --git a/scripts/spi_flash/spi_flash.py b/scripts/spi_flash/spi_flash.py new file mode 100644 index 00000000..5376dc2d --- /dev/null +++ b/scripts/spi_flash/spi_flash.py @@ -0,0 +1,1112 @@ +#!/usr/bin/env python2 +# Module supporting uploads Klipper firmware to an SD Card via SPI +# +# Copyright (C) 2021 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license. +import sys +import argparse +import os +import zlib +import hashlib +import logging +import collections +import time +import traceback +import board_defs +import fatfs_lib +import reactor +import serialhdl +import clocksync +import mcu + +########################################################### +# +# Helper methods +# +########################################################### + +def output_line(msg): + sys.stdout.write("%s\n" % (msg,)) + sys.stdout.flush() + +def output(msg): + sys.stdout.write("%s" % (msg,)) + sys.stdout.flush() + +def calc_crc7(data): + # G(x) = x^7 + x^3 + 1 + # Shift left as we are only calculating a 7 bit CRC + poly = 0b10001001 << 1 + crc = 0 + for b in data: + crc ^= b & 0xFF + for i in range(8): + crc = (crc << 1) ^ poly if crc & 0x80 else crc << 1 + # The sdcard protocol likes the crc left justfied with a + # padded bit + return crc | 1 + +def calc_crc16(data): + poly = 0b10001000000100001 + crc = 0 + for b in data: + crc ^= (b & 0xFF) << 8 + for i in range(8): + crc = (crc << 1) ^ poly if crc & 0x8000 else crc << 1 + return crc & 0xFFFF + +# Translate a serial device name to a stable serial name in +# /dev/serial/by-path/ +# Borrowed from klipper/scripts/flash_usb.py +def translate_serial_to_tty(device): + ttyname = os.path.realpath(device) + if os.path.exists('/dev/serial/by-path/'): + for fname in os.listdir('/dev/serial/by-path/'): + fname = '/dev/serial/by-path/' + fname + if os.path.realpath(fname) == ttyname: + return ttyname, fname + return ttyname, ttyname + +def check_need_convert(board_name, config): + if board_name.lower().startswith('mks-robin-e3'): + # we need to convert this file + robin_util = os.path.join( + fatfs_lib.KLIPPER_DIR, "scripts/update_mks_robin.py") + klipper_bin = config['klipper_bin_path'] + robin_bin = os.path.join( + os.path.dirname(klipper_bin), + os.path.basename(config['firmware_path'])) + cmd = "%s %s %s %s" % (sys.executable, robin_util, klipper_bin, + robin_bin) + output("Converting Klipper binary to MKS Robin format...") + os.system(cmd) + output_line("Done") + config['klipper_bin_path'] = robin_bin + + +########################################################### +# +# SPI FLash Implementation +# +########################################################### + +SPI_OID = 0 +SPI_MODE = 0 +SD_SPI_SPEED = 4000000 +# MCU Command Constants +RESET_CMD = "reset" +GET_CFG_CMD = "get_config" +GET_CFG_RESPONSE = "config is_config=%c crc=%u move_count=%hu is_shutdown=%c" +ALLOC_OIDS_CMD = "allocate_oids count=%d" +SPI_CFG_CMD = "config_spi oid=%d pin=%s" +SPI_BUS_CMD = "spi_set_bus oid=%d spi_bus=%s mode=%d rate=%d" +SW_SPI_BUS_CMD = "spi_set_software_bus oid=%d " \ + "miso_pin=%s mosi_pin=%s sclk_pin=%s mode=%d rate=%d" +SPI_SEND_CMD = "spi_send oid=%c data=%*s" +SPI_XFER_CMD = "spi_transfer oid=%c data=%*s" +SPI_XFER_RESPONSE = "spi_transfer_response oid=%c response=%*s" +FINALIZE_CFG_CMD = "finalize_config crc=%d" + +class SPIFlashError(Exception): + pass + +class SPIDirect: + def __init__(self, ser): + self.oid = SPI_OID + self._spi_send_cmd = mcu.CommandWrapper(ser, SPI_SEND_CMD) + self._spi_transfer_cmd = mcu.CommandQueryWrapper( + ser, SPI_XFER_CMD, SPI_XFER_RESPONSE, self.oid) + + def spi_send(self, data): + self._spi_send_cmd.send([self.oid, data]) + + def spi_transfer(self, data): + return self._spi_transfer_cmd.send([self.oid, data]) + + +# FatFs Constants. Enums are implemented as lists. The item's index is its value +DRESULT = ['RES_OK', 'RES_ERROR', 'RES_WRPRT', 'RES_NOTRDY', 'RES_PARERR'] +FRESULT = [ + 'FR_OK', 'FR_DISK_ERR', 'FR_INT_ERR', 'FR_NOT_READY', 'FR_NO_FILE', + 'FR_NO_PATH', 'FR_INVALID_NAME', 'FR_DENIED', 'FR_EXIST', + 'FR_INVALID_OBJECT', 'FR_WRITE_PROTECTED', 'FR_INVALID_DRIVE', + 'FR_NOT_ENABLED', 'FR_NO_FILESYSTEM', 'FR_MKFS_ABORTED', 'FR_TIMEOUT', + 'FR_LOCKED', 'FR_NOT_ENOUGH_CORE', 'FR_TOO_MANY_OPEN_FILES', + 'FR_INVALID_PARAMETER' +] +FS_TYPES = {1: "FAT12", 2: "FAT16", 3: "FAT32", 4: "EXFAT"} +STA_NO_INIT = 1 << 0 +STA_NO_DISK = 1 << 1 +STA_WRITE_PROTECT = 1 << 2 +SECTOR_SIZE = 512 + +# FAT16/32 File System Support +class FatFS: + def __init__(self, ser): + self.sdcard = SDCardSPI(ser) + self.disk_status = STA_NO_INIT | STA_NO_DISK + self.ffi_callbacks = [] + self.ffi_main, self.ffi_lib = fatfs_lib.get_fatfs_ffi() + self._register_callbacks() + + def _register_callbacks(self): + status_cb = self.ffi_main.callback( + "uint8_t(void)", self._fatfs_cb_status) + init_cb = self.ffi_main.callback( + "uint8_t(void)", self._fatfs_cb_initialize) + read_cb = self.ffi_main.callback( + "uint8_t(uint8_t*, uint32_t, unsigned int)", + self._fatfs_cb_disk_read) + write_cb = self.ffi_main.callback( + "uint8_t(const uint8_t*, uint32_t, unsigned int)", + self._fatfs_cb_disk_write) + ioctl_cb = self.ffi_main.callback( + "uint8_t(uint8_t, void*)", self._fatfs_cb_disk_ioctl) + ftime_cb = self.ffi_main.callback( + "uint32_t(void)", self._fatfs_cb_get_fattime) + # Keep a reference to the callbacks so they don't get gc'ed + self.ffi_callbacks = [status_cb, init_cb, read_cb, write_cb, + ioctl_cb, ftime_cb] + self.ffi_lib.fatfs_set_callbacks( + status_cb, init_cb, read_cb, write_cb, ioctl_cb, ftime_cb) + + def clear_callbacks(self): + self.ffi_lib.fatfs_clear_callbacks() + self.ffi_callbacks = [] + + def _fatfs_cb_status(self): + return self.disk_status + + def _fatfs_cb_initialize(self): + try: + self.sdcard.init_sd() + except Exception: + logging.exception("flash_sdcard: error initializing sdcard") + self.disk_status = STA_NO_INIT + else: + self.disk_status = 0 + if self.sdcard.write_protected: + self.disk_status |= STA_WRITE_PROTECT + return self.disk_status + + def _fatfs_cb_disk_read(self, readbuf, sector, count): + start = 0 + end = SECTOR_SIZE + for sec in range(sector, sector + count, 1): + tries = 3 + buf = None + while True: + try: + buf = self.sdcard.read_sector(sec) + except Exception: + tries -= 1 + if not tries: + logging.exception("SD Card Read Error") + break + else: + break + if buf is None: + return DRESULT.index("RES_ERROR") + readbuf[start:end] = list(buf) + start = end + end += SECTOR_SIZE + return 0 + + def _fatfs_cb_disk_write(self, writebuf, sector, count): + start = 0 + end = SECTOR_SIZE + for sec in range(sector, sector + count, 1): + tries = 3 + while True: + try: + self.sdcard.write_sector(sec, writebuf[start:end]) + except Exception: + tries -= 1 + if not tries: + logging.exception("SD Card Write Error") + return DRESULT.index("RES_ERROR") + else: + break + start = end + end += SECTOR_SIZE + return 0 + + def _fatfs_cb_disk_ioctl(self, cmd, buff): + # The Current FatFS configuration does not require + # this module to take any actions for incoming IOCTL + # commands. + ioctl_cmds = [ + 'CTRL_SYNC', 'GET_SECTOR_COUNT', 'GET_SECTOR_SIZE', + 'GET_BLOCK_SIZE', 'CTRL_TRIM'] + logging.debug("flash_sdcard: Received IOCTL command %s" + % (ioctl_cmds[cmd])) + return 0 + + def _fatfs_cb_get_fattime(self): + tobj = time.localtime() + year = tobj[0] - 1980 + sec = min(tobj[5], 59) // 2 + return (year << 25) | (tobj[1] << 21) | (tobj[2] << 16) \ + | (tobj[3] << 11) | (tobj[4] << 5) | sec + + def mount(self, print_func=logging.info): + ret = self.ffi_lib.fatfs_mount() + if ret == 0: + self.sdcard.print_card_info(print_func) + dinfo = self.get_disk_info() + for key, val in sorted(dinfo.items(), key=lambda x: x[0]): + print_func("%s: %s" % (key, val)) + else: + raise OSError("flash_sdcard: failed to mount SD Card, returned %s" + % (FRESULT[ret])) + + def unmount(self): + self.ffi_lib.fatfs_unmount() + self.sdcard.deinit() + self.disk_status = STA_NO_INIT | STA_NO_DISK + + def open_file(self, sd_path, mode="r"): + sdf = SDCardFile(self.ffi_main, self.ffi_lib, sd_path, mode) + sdf.open() + return sdf + + def remove_item(self, sd_path): + # Can be path to directory or file + ret = self.ffi_lib.fatfs_remove(sd_path) + if ret != 0: + raise OSError("flash_sdcard: Error deleting item at path '%s'," + " result: %s" + % (sd_path, FRESULT[ret])) + + def get_file_info(self, sd_file_path): + finfo = self.ffi_main.new("struct ff_file_info *") + ret = self.ffi_lib.fatfs_get_fstats(finfo, sd_file_path) + if ret != 0: + raise OSError( + "flash_sdcard: Failed to retreive file info for path '%s'," + " result: %s" + % (sd_file_path, FRESULT[ret],)) + return self._parse_ff_info(finfo) + + def list_sd_directory(self, sd_dir_path): + flist = self.ffi_main.new("struct ff_file_info[128]") + ret = self.ffi_lib.fatfs_list_dir(flist, 128, sd_dir_path) + if ret != 0: + raise OSError("flash_sdcard: Failed to retreive file list at path" + " '%s', result: %s" + % (sd_dir_path, FRESULT[ret],)) + convlist = [] + for f in flist: + if f.size == 0 and f.modified_date == 0 and f.modified_time == 0: + # Empty record indicates end of list + break + convlist.append(self._parse_ff_info(f)) + return convlist + + def _parse_ff_info(self, finfo): + fdate = finfo.modified_date + ftime = finfo.modified_time + dstr = "%d-%d-%d %d:%d:%d" % ( + (fdate >> 5) & 0xF, fdate & 0x1F, ((fdate >> 9) & 0x7F) + 1980, + (ftime >> 11) & 0x1F, (ftime >> 5) & 0x3F, ftime & 0x1F) + return { + 'name': self.ffi_main.string(finfo.name, 13), + 'size': finfo.size, + 'modified': dstr, + 'is_dir': bool(finfo.attrs & 0x10), + 'is_read_only': bool(finfo.attrs & 0x01), + 'is_hidden': bool(finfo.attrs & 0x02), + 'is_system': bool(finfo.attrs & 0x04) + } + + def get_disk_info(self): + disk_info = self.ffi_main.new("struct ff_disk_info *") + ret = self.ffi_lib.fatfs_get_disk_info(disk_info) + if ret != 0: + logging.info("flash_sdcard: Failed to retreive disk info: %s" + % (FRESULT[ret],)) + return {} + return { + 'volume_label': self.ffi_main.string(disk_info.label, 12), + 'volume_serial': disk_info.serial_number, + 'fs_type': FS_TYPES.get(disk_info.fs_type, "UNKNOWN") + } + + +SD_FILE_MODES = { + 'w+x': 0x01 | 0x02 | 0x04, 'wx': 0x02 | 0x04, + 'r+': 0x01 | 0x02, 'w+': 0x01 | 0x02 | 0x08, + 'a+': 0x01 | 0x02 | 0x30, 'r': 0x01, + 'w': 0x02 | 0x08, 'a': 0x02 | 0x30} + +class SDCardFile: + def __init__(self, ffi_main, ffi_lib, sd_path, mode="r"): + self.ffi_main = ffi_main + self.ffi_lib = ffi_lib + self.path = sd_path + mode = mode.lower() + if mode[-1] == 'b': + mode = mode[:-1] + self.mode = SD_FILE_MODES.get(mode, 0) + self.fhdl = None + self.eof = False + + def open(self): + if self.fhdl is not None: + # already open + return + self.fhdl = self.ffi_lib.fatfs_open(self.path, self.mode) + self.eof = False + if self.fhdl == self.ffi_main.NULL: + self.fhdl = None + raise OSError("flash_sdcard: Could not open file '%s':" + % (self.path)) + + def read(self, length=None): + if self.fhdl is None: + raise OSError("flash_sdcard: File '%s' not open" % (self.path)) + if self.eof: + return b"" + ret_buf = b"" + full_read = False + if length is None: + # read until eof + full_read = True + length = 4096 + cdata_buf = self.ffi_main.new("uint8_t[]", length) + byte_buf = self.ffi_main.buffer(cdata_buf) + while True: + bytes_read = self.ffi_lib.fatfs_read(self.fhdl, cdata_buf, length) + if bytes_read < 0: + raise OSError("flash_sdcard: Error Reading file '%s'" + % (self.path)) + self.eof = (bytes_read < length) + ret_buf += byte_buf[0:bytes_read] + if self.eof or not full_read: + break + return ret_buf + + def write(self, buf): + if self.fhdl is None: + raise OSError("flash_sdcard: File '%s' not open" % (self.path)) + if not buf: + return 0 + cbuf = self.ffi_main.from_buffer(buf) + bytes_written = self.ffi_lib.fatfs_write(self.fhdl, cbuf, len(cbuf)) + if bytes_written < 0: + # Disk Full or some other error + raise OSError("flash_sdcard: Error writing file '%s'" % (self.path)) + return bytes_written + + def close(self): + if self.fhdl is not None: + ret = self.ffi_lib.fatfs_close(self.fhdl) + self.fhdl = None + if ret != 0: + logging.info("flash_sdcard: Error closing sd file '%s', " + "returned %d" + % (self.path, FRESULT[ret])) + + def __enter__(self): + self.open() + return self + + def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): + self.close() + + +SD_COMMANDS = { + 'GO_IDLE_STATE': 0, + 'SEND_IF_COND': 8, + 'SEND_CSD': 9, + 'SEND_CID': 10, + 'SD_SEND_OP_COND': 41, + 'SEND_STATUS': 13, + 'SET_BLOCKLEN': 16, + 'READ_SINGLE_BLOCK': 17, + 'WRITE_BLOCK': 24, + 'APP_CMD': 55, + 'READ_OCR': 58, + 'CRC_ON_OFF': 59, +} + +class SDCardSPI: + def __init__(self, ser): + self.spi = SPIDirect(ser) + self.reactor = ser.get_reactor() + self.enable_crc = True + self.mutex = self.reactor.mutex() + self.initialized = False + self.sd_version = 0 + self.high_capacity = False + self.write_protected = False + self.total_sectors = 0 + self.card_info = collections.OrderedDict() + + def init_sd(self): + with self.mutex: + if self.initialized: + return + # Send reset command (CMD0) + if not self._check_command(1, 'GO_IDLE_STATE', 0): + raise OSError( + "flash_sdcard: failed to reset SD Card\n" + "Note that older (Version 1.0) SD cards can not be\n" + "hot swapped. Execute FIRMWARE_RESTART with the card\n" + "inserted for successful initialization.") + # Check Voltage Range (CMD8). Only Cards meeting the v2.0 spec + # support this. V1.0 cards (and MMC) will return illegal command. + check_pattern = 0b1010 + resp = self._send_command_with_response( + 'SEND_IF_COND', (1 << 8) | check_pattern) + resp = resp.strip(b'\xFF') + if resp and resp[0] & (1 << 2): + # CMD8 is illegal, this is a version 1.0 card + self.sd_version = 1 + elif len(resp) == 5: + if resp[0] == 1: + self.sd_version = 2 + if not (resp[-2] == 1 and resp[-1] == check_pattern): + raise OSError("flash_sdcard: SD Card not running in a " + "compatible voltage range") + else: + raise OSError("flash_sdcard: CMD8 Error 0x%X" % (resp[0],)) + else: + raise OSError("flash_sdcard: Invalid CMD8 response: %s" + % (repr(resp))) + if self.enable_crc: + # Enable SD crc checks (CMD59) + if not self._check_command(1, 'CRC_ON_OFF', 1): + logging.info("flash_sdcard: failed to enable CRC checks") + if self.sd_version == 2: + # Init card and come out of idle (ACMD41) + # Version 2 Cards may init before checking the OCR + if not self._check_command(0, 'SD_SEND_OP_COND', 1 << 30, + is_app_cmd=True): + raise OSError("flash_sdcard: SD Card did not come" + " out of IDLE after reset") + # Read OCR Register (CMD58) + resp = self._send_command_with_response('READ_OCR', 0) + resp = resp.strip(b'\xFF') + # If 'READ_OCR' is illegal then this is likely MMC. + # At this time MMC is not supported + if len(resp) == 5: + if self.sd_version == 1 and resp[0] == 1: + # Check acceptable volatage range for V1 cards + if resp[2] != 0xFF: + raise OSError("flash_sdcard: card does not support" + " 3.3v range") + elif self.sd_version == 2 and resp[0] == 0: + # Determine if this is a high capacity sdcard + if resp[1] & 0x40: + self.high_capacity = True + else: + raise OSError("flash_sdcard: READ_OCR Error 0x%X" + % (resp[0],)) + else: + raise OSError("flash_sdcard: Invalid OCR Response") + if self.sd_version == 1: + # Init card and come out of idle (ACMD41) + # Version 1 Cards do this after checking the OCR + if not self._check_command(0, 'SD_SEND_OP_COND', 0, + is_app_cmd=True): + raise OSError("flash_sdcard: SD Card did not come" + " out of IDLE after reset") + + # Set block size to 512 (CMD16) + if self._check_command(0, 'SET_BLOCKLEN', SECTOR_SIZE, tries=5): + self.initialized = True + else: + raise OSError("flash_sdcard: failed to set block size") + # Read out CSD and CID information registers + self._process_cid_reg() + self._process_csd_reg() + + def deinit(self): + with self.mutex: + if self.initialized: + # Reset the SD Card + try: + if not self._check_command(1, 'GO_IDLE_STATE', 0): + logging.info("flash_sdcard: failed to reset SD Card") + # Disable CRC Checks + if not self._check_command(1, 'CRC_ON_OFF', 0): + logging.info("flash_sdcard: failed to disable CRC") + except Exception: + logging.exception("Error resetting SD Card") + self.initialized = False + self.sd_version = 0 + self.high_capacity = False + self.total_sectors = 0 + self.card_info.clear() + + def _check_command(self, expected, cmd, args, is_app_cmd=False, tries=15): + func = self._send_app_cmd_with_response if is_app_cmd else \ + self._send_command_with_response + while True: + resp, rt = func(cmd, args, get_rt=True) + # logging.info("flash_sdcard: Check cmd %s, response: %s" + # % (cmd, repr(resp))) + resp = resp.strip(b'\xFF') + if resp and expected == resp[0]: + return True + tries -= 1 + if tries < 1: + return False + self.reactor.pause(rt + .1) + + def _send_command(self, cmd, args): + command = SD_COMMANDS[cmd] | 0x40 + request = [command] + if isinstance(args, int): + for i in range(3, -1, -1): + request.append((args >> (8*i)) & 0xFF) + elif isinstance(args, list) and len(args) == 4: + request += args + else: + raise OSError("flash_sdcard: Invalid SD Card Command argument") + crc = calc_crc7(request) + request.append(crc) + self.spi.spi_send(request) + + def _send_command_with_response(self, cmd, args, get_rt=False): + self._send_command(cmd, args) + params = self.spi.spi_transfer([0xFF]*8) + if get_rt: + return bytearray(params['response']), params['#receive_time'] + else: + return bytearray(params['response']) + + def _send_app_cmd_with_response(self, cmd, args, get_rt=False): + # CMD55 tells the SD Card that the next command is an + # Application Specific Command. + self._send_command_with_response('APP_CMD', 0) + return self._send_command_with_response(cmd, args, get_rt) + + def _find_sd_token(self, token, tries=10): + while tries: + params = self.spi.spi_transfer([0xFF]) + resp = bytearray(params['response']) + if resp[0] == token: + return True + tries -= 1 + return False + + def _find_sd_response(self, tries=10): + while tries: + params = self.spi.spi_transfer([0xFF]) + resp = bytearray(params['response']) + if resp[0] != 0xFF: + return resp[0] + tries -= 1 + return 0xFF + + def _process_cid_reg(self): + self._send_command('SEND_CID', 0) + reg = self._do_block_read(size=16) + if reg is None: + raise OSError("flash_sdcard: Error reading CID register") + cid = collections.OrderedDict() + cid['manufacturer_id'] = reg[0] + cid['oem_id'] = reg[1:3].decode(encoding='ascii', errors='ignore') + cid['product_name'] = reg[3:8].decode( + encoding='ascii', errors='ignore') + cid['product_revision'] = str(reg[8] >> 4 & 0xFF) + "." \ + + str(reg[8] & 0xFF) + cid['serial_number'] = "".join(["%02X" % (c,) for c in reg[9:13]]) + mfg_year = (((reg[13] & 0xF) << 4) | ((reg[14] >> 4) & 0xF)) + 2000 + mfg_month = reg[14] & 0xF + cid['manufacturing_date'] = "%d/%d" % (mfg_month, mfg_year) + crc = calc_crc7(reg[:15]) + if crc != reg[15]: + raise OSError("flash_sdcard: CID crc mismatch: 0x%02X, recd: 0x%02X" + % (crc, reg[15])) + self.card_info.update(cid) + + def _process_csd_reg(self): + self._send_command('SEND_CSD', 0) + reg = self._do_block_read(size=16) + if reg is None: + raise OSError("flash_sdcard: Error reading CSD register") + str_capacity = "Invalid" + max_capacity = 0 + csd_type = (reg[0] >> 6) & 0x3 + if csd_type == 0: + # Standard Capacity (CSD Version 1.0) + max_block_len = 2**(reg[5] & 0xF) + c_size = ((reg[6] & 0x3) << 10) | (reg[7] << 2) | \ + ((reg[8] >> 6) & 0x3) + c_mult = 2**((((reg[9] & 0x3) << 1) | (reg[10] >> 7)) + 2) + max_capacity = (c_size + 1) * c_mult * max_block_len + str_capacity = "%.1f MiB" % (max_capacity / (1024.0**2)) + elif csd_type == 1: + # High Capacity (CSD Version 2.0) + c_size = ((reg[7] & 0x3F) << 16) | (reg[8] << 8) | reg[9] + max_capacity = (c_size + 1) * 512 * 1024 + str_capacity = "%.1f GiB" % (max_capacity / (1024.0**3)) + else: + logging.info("sdcard: Unsupported csd type: %d" % (csd_type)) + self.write_protected = (reg[14] & 0x30) != 0 + crc = calc_crc7(reg[:15]) + if crc != reg[15]: + raise OSError("flash_sdcard: CSD crc mismatch: 0x%02X, recd: 0x%02X" + % (crc, reg[15])) + self.card_info['capacity'] = str_capacity + self.total_sectors = max_capacity // SECTOR_SIZE + + def print_card_info(self, print_func=logging.info): + print_func("\nSD Card Information:") + print_func("Version: %.1f" % (self.sd_version)) + print_func("SDHC/SDXC: %s" % (self.high_capacity)) + print_func("Write Protected: %s" % (self.write_protected)) + print_func("Sectors: %d" % (self.total_sectors,)) + for name, val in self.card_info.items(): + print_func("%s: %s" % (name, val)) + + def read_sector(self, sector): + buf = None + err_msg = "flash_sdcard: read error, sector %d" % (sector,) + with self.mutex: + if not 0 <= sector < self.total_sectors: + err_msg += " out of range" + elif not self.initialized: + err_msg += ", SD Card not initialized" + else: + offset = sector + if not self.high_capacity: + offset = sector * SECTOR_SIZE + self._send_command('READ_SINGLE_BLOCK', offset) + buf = self._do_block_read() + if buf is None: + raise OSError(err_msg) + return buf + + def _do_block_read(self, size=SECTOR_SIZE): + valid_response = True + sd_resp = self._find_sd_response() + if sd_resp != 0: + logging.info("flash_sdcard: invalid read block response: 0x%02X" + % (sd_resp)) + valid_response = False + if not self._find_sd_token(0xFE): + logging.info("flash_sdcard: read error, unable to find " + "start token") + valid_response = False + if not valid_response: + # In the event of an invalid response we will still + # send 514 bytes to be sure that the sdcard's output + # buffer is clear + bcount = size + 2 + while bcount: + sent = min(32, bcount) + self.spi.spi_send([0xFF]*sent) + bcount -= sent + self._find_sd_token(0xFF) + return None + buf = bytearray() + while len(buf) < size: + count = min(32, size - len(buf)) + params = self.spi.spi_transfer([0xFF]*count) + buf += bytearray(params['response']) + # Get the CRC + params = self.spi.spi_transfer([0xFF, 0xFF]) + # Make sure we leave the busy state + self._find_sd_token(0xFF) + crc = bytearray(params['response']) + crc_int = (crc[0] << 8) | crc[1] + calculated_crc = calc_crc16(buf) + if calculated_crc != crc_int: + logging.info( + "flash_sdcard: CRC Mismatch, Received: %04X, Calculated: %04X" + % (crc_int, calculated_crc)) + return None + return buf + + def write_sector(self, sector, data): + with self.mutex: + if not 0 <= sector < self.total_sectors: + raise OSError( + "flash_sdcard: write error, sector number %d invalid" + % (sector)) + if not self.initialized: + raise OSError("flash_sdcard: write error, SD Card not" + " initialized") + outbuf = bytearray(data) + if len(outbuf) > SECTOR_SIZE: + raise OSError("sd_card: Cannot write sector larger" + " than %d bytes" + % (SECTOR_SIZE)) + elif len(outbuf) < SECTOR_SIZE: + outbuf += bytearray([0] * (SECTOR_SIZE - len(outbuf))) + offset = sector + if not self.high_capacity: + offset = sector * SECTOR_SIZE + if not self._check_command(0, 'WRITE_BLOCK', offset, tries=2): + raise OSError("flash_sdcard: Error writing to sector %d" + % (sector,)) + crc = calc_crc16(outbuf) + outbuf.insert(0, 0xFE) + outbuf.append((crc >> 8) & 0xFF) + outbuf.append(crc & 0xFF) + while outbuf: + self.spi.spi_send(outbuf[:32]) + outbuf = outbuf[32:] + resp = self._find_sd_response() + err_msgs = [] + if (resp & 0x1f) != 5: + err_msgs.append("flash_sdcard: write error 0x%02X" % (resp,)) + # wait until the card leaves the busy state + if not self._find_sd_token(0xFF, tries=128): + err_msgs.append("flash_sdcard: could not leave busy" + " state after write") + else: + status = self._send_command_with_response('SEND_STATUS', 0) + status = status.strip(b'\xFF') + if len(status) == 2: + if status[1] != 0: + err_msgs.append( + "flash_sdcard: write error 0x%02X" + % (status[1],)) + else: + err_msgs.append("flash_sdcard: Invalid status response" + " after write: %s" % (repr(status),)) + if err_msgs: + raise OSError("\n".join(err_msgs)) + +class MCUConnection: + def __init__(self, k_reactor, device, baud, board_cfg): + self.reactor = k_reactor + # TODO: a change in baudrate will cause an issue, come up + # with a method for handling it gracefully + self._serial = serialhdl.SerialReader(self.reactor, device, baud) + self.clocksync = clocksync.ClockSync(self.reactor) + self.board_config = board_cfg + self.fatfs = None + self.connect_completion = None + self.connected = False + self.enumerations = {} + + def connect(self): + output("Connecting to MCU..") + self.connect_completion = self.reactor.completion() + self.connected = False + self.reactor.register_callback(self._do_serial_connect) + curtime = self.reactor.monotonic() + while True: + curtime = self.reactor.pause(curtime + 1.) + output(".") + if self.connect_completion.test(): + self.connected = self.connect_completion.wait() + break + self.connect_completion = None + if not self.connected: + output("\n") + raise SPIFlashError("Unable to connect to MCU") + output_line("Connected") + msgparser = self._serial.get_msgparser() + mcu_type = msgparser.get_constant('MCU') + build_mcu_type = self.board_config['mcu'] + if mcu_type != build_mcu_type: + raise SPIFlashError( + "MCU Type mismatch: Build MCU = %s, Connected MCU = %s" + % (build_mcu_type, mcu_type)) + self.enumerations = msgparser.get_enumerations() + + def _do_serial_connect(self, eventtime): + endtime = eventtime + 60. + while True: + try: + self._serial.connect() + self.clocksync.connect(self._serial) + except Exception: + curtime = self.reactor.monotonic() + if curtime > endtime: + self.connect_completion.complete(False) + return + output("Connection Error, retrying..") + self._serial.disconnect() + self.reactor.pause(curtime + 2.) + else: + break + self.connect_completion.complete(True) + + def reset(self): + output("Attempting MCU Reset...") + if self.fatfs is not None: + self.fatfs.unmount() + self.fatfs.clear_callbacks() + # XXX: do we need to support other reset methods? + self._serial.send(RESET_CMD) + self.reactor.pause(self.reactor.monotonic() + 0.015) + self.reactor.end() + output_line("Done") + + def disconnect(self): + if not self.connected: + return + self._serial.disconnect() + self.connected = False + + def check_need_restart(self): + output("Checking Current MCU Configuration...") + get_cfg_cmd = mcu.CommandQueryWrapper( + self._serial, GET_CFG_CMD, GET_CFG_RESPONSE) + params = get_cfg_cmd.send() + output_line("Done") + if params['is_config'] or params['is_shutdown']: + output_line("MCU needs restart: is_config=%d, is_shutdown=%d" + % (params['is_config'], params['is_shutdown'])) + return True + return False + + def configure_mcu(self, printfunc=logging.info): + # TODO: add commands for buttons? Or perhaps an endstop? We + # just need to be able to query the status of the detect pin + cs_pin = self.board_config['cs_pin'].upper() + bus = self.board_config['spi_bus'] + bus_enums = self.enumerations.get( + 'spi_bus', self.enumerations.get('bus')) + pin_enums = self.enumerations.get('pin') + if bus == "swspi": + cfgpins = self.board_config['spi_pins'] + pins = [p.strip().upper() for p in cfgpins.split(',') if p.strip()] + pin_err_msg = "Invalid Software SPI Pins: %s" % (cfgpins,) + if len(pins) != 3: + raise SPIFlashError(pin_err_msg) + for p in pins: + if p not in pin_enums: + raise SPIFlashError(pin_err_msg) + bus_cmd = SW_SPI_BUS_CMD % (SPI_OID, pins[0], pins[1], pins[2], + SPI_MODE, SD_SPI_SPEED) + else: + if bus not in bus_enums: + raise SPIFlashError("Invalid SPI Bus: %s" % (bus,)) + bus_cmd = SPI_BUS_CMD % (SPI_OID, bus, SPI_MODE, SD_SPI_SPEED) + if cs_pin not in pin_enums: + raise SPIFlashError("Invalid CS Pin: %s" % (cs_pin,)) + cfg_cmds = [ + ALLOC_OIDS_CMD % (1), + SPI_CFG_CMD % (SPI_OID, cs_pin), + bus_cmd, + ] + config_crc = zlib.crc32('\n'.join(cfg_cmds)) & 0xffffffff + cfg_cmds.append(FINALIZE_CFG_CMD % (config_crc,)) + for cmd in cfg_cmds: + self._serial.send(cmd) + self.fatfs = FatFS(self._serial) + self.reactor.pause(self.reactor.monotonic() + .5) + printfunc("Initializing SD Card and Mounting file system...") + try: + self.fatfs.mount(printfunc) + except OSError: + logging.exception("SD Card Mount Failure") + raise SPIFlashError( + "Failed to Initialize SD Card. Is it inserted?") + + def sdcard_upload(self): + output("Uploading Klipper Firmware to SD Card...") + input_sha = hashlib.sha1() + sd_sha = hashlib.sha1() + klipper_bin_path = self.board_config['klipper_bin_path'] + fw_path = self.board_config.get('firmware_path', "firmware.bin") + try: + with open(klipper_bin_path, 'rb') as local_f: + with self.fatfs.open_file(fw_path, "wb") as sd_f: + while True: + buf = local_f.read(4096) + if not buf: + break + input_sha.update(buf) + sd_f.write(buf) + except Exception: + logging.exception("SD Card Upload Error") + raise SPIFlashError("Error Uploading Firmware") + output_line("Done") + output("Validating Upload...") + try: + finfo = self.fatfs.get_file_info(fw_path) + with self.fatfs.open_file(fw_path, 'r') as sd_f: + while True: + buf = sd_f.read(4096) + if not buf: + break + sd_sha.update(buf) + except Exception: + logging.exception("SD Card Download Error") + raise SPIFlashError("Error reading %s from SD" % (fw_path)) + sd_size = finfo.get('size', -1) + input_chksm = input_sha.hexdigest().upper() + sd_chksm = sd_sha.hexdigest().upper() + if input_chksm != sd_chksm: + raise SPIFlashError("Checksum Mismatch: Got '%s', expected '%s'" + % (sd_chksm, input_chksm)) + output_line("Done") + output_line( + "Firmware Upload Complete: %s, Size: %d, Checksum (SHA1): %s" + % (fw_path, sd_size, sd_chksm)) + return sd_chksm + + def verify_flash(self, req_chksm): + output("Verifying Flash...") + cur_fw_sha = hashlib.sha1() + cur_fw_path = self.board_config.get('current_firmware_path', + "FIRMWARE.CUR") + try: + with self.fatfs.open_file(cur_fw_path, 'r') as sd_f: + while True: + buf = sd_f.read(4096) + if not buf: + break + cur_fw_sha.update(buf) + except Exception: + msg = "Failed to read file %s" % (cur_fw_path,) + logging.exception(msg) + raise SPIFlashError(msg) + cur_fw_chksm = cur_fw_sha.hexdigest().upper() + if req_chksm == cur_fw_chksm: + output_line("Done") + output_line("Firmware Flash Successful") + else: + raise SPIFlashError("Checksum Mismatch: Got '%s', expected '%s'" + % (cur_fw_chksm, req_chksm)) + +class SPIFlash: + def __init__(self, args): + self.board_config = args + if not os.path.exists(args['device']): + raise SPIFlashError("No device found at '%s'" % (args['device'],)) + if not os.path.isfile(args['klipper_bin_path']): + raise SPIFlashError("Unable to locate klipper binary at '%s'" + % (args['klipper_bin_path'],)) + tty_name, dev_by_path = translate_serial_to_tty(args['device']) + self.device_path = dev_by_path + self.baud_rate = args['baud'] + self.mcu_conn = None + self.firmware_checksum = None + self.task_complete = False + self.need_upload = True + + def _wait_for_reconnect(self): + output("Waiting for device to reconnect...") + time.sleep(1.) + if os.path.exists(self.device_path): + # device is already available, this could be a UART + time.sleep(2.) + else: + wait_left = 30 + while wait_left: + time.sleep(1.) + output(".") + if os.path.exists(self.device_path): + break + wait_left -= 1 + else: + output_line("Error") + raise SPIFlashError("Unable to reconnect") + output_line("Done") + + def run_reset(self, eventtime): + # Reset MCU to default state if necessary + self.mcu_conn.connect() + if self.mcu_conn.check_need_restart(): + self.mcu_conn.reset() + self.task_complete = True + else: + self.need_upload = False + self.run_sdcard_upload(eventtime) + + def run_sdcard_upload(self, eventtime): + # Reconnect and upload + if not self.mcu_conn.connected: + self.mcu_conn.connect() + self.mcu_conn.configure_mcu(printfunc=output_line) + self.firmware_checksum = self.mcu_conn.sdcard_upload() + self.mcu_conn.reset() + self.task_complete = True + + def run_verify(self, eventtime): + # Reconnect and verify + self.mcu_conn.connect() + self.mcu_conn.configure_mcu() + self.mcu_conn.verify_flash(self.firmware_checksum) + self.mcu_conn.reset() + self.task_complete = True + + def run_reactor_task(self, run_cb): + self.task_complete = False + k_reactor = reactor.Reactor() + self.mcu_conn = MCUConnection(k_reactor, self.device_path, + self.baud_rate, self.board_config) + k_reactor.register_callback(run_cb) + try: + k_reactor.run() + except SPIFlashError: + raise + except Exception: + # ignore exceptions that occur after a task is complete + if not self.task_complete: + raise + finally: + self.mcu_conn.disconnect() + k_reactor.finalize() + self.mcu_conn = k_reactor = None + + def run(self): + self.run_reactor_task(self.run_reset) + self._wait_for_reconnect() + if self.need_upload: + self.run_reactor_task(self.run_sdcard_upload) + self._wait_for_reconnect() + self.run_reactor_task(self.run_verify) + +def main(): + parser = argparse.ArgumentParser( + description="SD Card Firmware Upload Utility") + parser.add_argument( + "-l", "--list", action="store_true", + help="List Supported Boards") + args = parser.parse_known_args() + if args[0].list: + blist = board_defs.list_boards() + output_line("Available Boards:") + for board in blist: + output_line(board) + return + parser.add_argument( + "-b", "--baud", metavar="", type=int, + default=250000, help="Serial Baud Rate") + parser.add_argument( + "-v", "--verbose", action="store_true", + help="Enable verbose output") + parser.add_argument( + "device", metavar="", help="Device Serial Port") + parser.add_argument( + "board", metavar="", help="Board Type") + parser.add_argument( + "klipper_bin_path", metavar="", + help="Klipper firmware binary") + args = parser.parse_args() + log_level = logging.DEBUG if args.verbose else logging.CRITICAL + logging.basicConfig(level=log_level) + flash_args = board_defs.lookup_board(args.board) + if flash_args is None: + output_line("Unable to find defintion for board: %s" % (args.board,)) + sys.exit(-1) + flash_args['device'] = args.device + flash_args['baud'] = args.baud + flash_args['klipper_bin_path'] = args.klipper_bin_path + check_need_convert(args.board, flash_args) + fatfs_lib.check_fatfs_build(output) + try: + spiflash = SPIFlash(flash_args) + spiflash.run() + except Exception as e: + output_line("\nSD Card Flash Error: %s" % (str(e),)) + traceback.print_exc(file=sys.stdout) + sys.exit(-1) + output_line("SD Card Flash Complete") + + +if __name__ == "__main__": + main()