diff --git a/docs/conf_common.py b/docs/conf_common.py index 36ecf4ff7..e7ad6e2dd 100644 --- a/docs/conf_common.py +++ b/docs/conf_common.py @@ -159,6 +159,7 @@ def update_exclude_patterns(tags): # note: in toctrees, these also need to be marked with a :esp32: filter for e in ['esp32s2.rst', 'hw-reference/esp32s2/**', + 'api-guides/dfu.rst', 'api-guides/ulps2_instruction_set.rst', 'api-reference/peripherals/hmac.rst', 'api-reference/peripherals/temp_sensor.rst']: diff --git a/docs/en/api-guides/dfu.rst b/docs/en/api-guides/dfu.rst new file mode 100644 index 000000000..251b58c5c --- /dev/null +++ b/docs/en/api-guides/dfu.rst @@ -0,0 +1,99 @@ +*********************************************** +Device Firmware Upgrade through USB +*********************************************** + +.. only:: esp32 + + .. note:: + Device Firmware Upgrade through USB is not supported with ESP32 chips. + +Device Firmware Upgrade (DFU) is a mechanism for upgrading the firmware of devices through Universal Serial Bus (USB). +DFU is supported by ESP32-S2 chips. The necessary connections for the USB peripheral are shown in the following table. + ++------+-------------+ +| GPIO | USB | ++======+=============+ +| 19 | D- (green) | ++------+-------------+ +| 20 | D+ (white) | ++------+-------------+ +| GND | GND (black) | ++------+-------------+ +| | +5V (red) | ++------+-------------+ + +The software requirements of DFU are included in :ref:`get-started-get-prerequisites` of the Getting Started Guide. + +Section :ref:`api_guide_dfu_build` describes how to build firmware for DFU with ESP-IDF and +Section :ref:`api_guide_dfu_flash` deals with flashing the firmware. + +.. _api_guide_dfu_build: + +Building the DFU Image +====================== + +The DFU image can be created by running:: + + idf.py dfu + +which creates ``dfu.bin`` in the build directory. + +.. note:: + Don't forget to set the target chip by ``idf.py set-target`` before running ``idf.py dfu``. Otherwise, you might + create an image for a different chip or receive an error message something like ``unknown target 'dfu'``. + +.. _api_guide_dfu_flash: + +Flashing the Chip with the DFU Image +==================================== + +The DFU image is downloaded into the chip by running:: + + idf.py dfu-flash + +which relies on `dfu-util `_. Please see :ref:`get-started-get-prerequisites` for +installing ``dfu-util``. ``dfu-util`` needs additional setup for :ref:`api_guide_dfu_flash_win` or setting up an +:ref:`api_guide_dfu_flash_udev`. Mac OS users should be able to use ``dfu-util`` without further setup. + +See :ref:`api_guide_dfu_flash_errors` and their solutions. + +.. _api_guide_dfu_flash_udev: + +udev rule (Linux only) +---------------------- + +udev is a device manager for the Linux kernel. It allows us to run ``dfu-util`` (and ``idf.py dfu-flash``) without +``sudo`` for gaining access to the chip. + +Create file ``/etc/udev/rules.d/40-dfuse.rules`` with the following content:: + + SUBSYSTEMS=="usb", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="00??", GROUP="plugdev", MODE="0666" + +.. note:: + Please check the output of command ``groups``. The user has to be a member of the `GROUP` specified above. You may + use some other existing group for this purpose (e.g. `uucp` on some systems instead of `plugdev`) or create a new + group for this purpose. + +Restart your computer so the previous setting could take into affect or run ``sudo udevadm trigger`` to force +manually udev to trigger your new rule. + +.. _api_guide_dfu_flash_win: + +USB drivers (Windows only) +-------------------------- + +``dfu-util`` uses `libusb` to access the device. You have to register on Windows the device with the `WinUSB` driver. +Please see the `libusb wiki `_ for more +details. + +.. _api_guide_dfu_flash_errors: + +Common errors +------------- + +- ``dfu-util: command not found`` might indicate that the tool hasn't been installed or is not available from the terminal. + An easy way of checking the tool is running ``dfu-util --version``. Please see :ref:`get-started-get-prerequisites` for + installing ``dfu-util``. +- The reason for ``No DFU capable USB device available`` could be that the USB driver wasn't properly installed on + Windows (see :ref:`api_guide_dfu_flash_win`) or udev rule was not setup on Linux + (see :ref:`api_guide_dfu_flash_udev`). diff --git a/docs/en/api-guides/index.rst b/docs/en/api-guides/index.rst index 4b11122ac..946b85f6c 100644 --- a/docs/en/api-guides/index.rst +++ b/docs/en/api-guides/index.rst @@ -11,6 +11,7 @@ API Guides Build System :esp32: Build System (Legacy GNU Make) Deep Sleep Wake Stubs + :esp32s2: Device Firmware Upgrade through USB Error Handling :esp32: ESP-BLE-MESH ESP-MESH (Wi-Fi) diff --git a/docs/en/api-guides/tools/idf-tools-notes.inc b/docs/en/api-guides/tools/idf-tools-notes.inc index ffd58cd36..426b33edb 100644 --- a/docs/en/api-guides/tools/idf-tools-notes.inc +++ b/docs/en/api-guides/tools/idf-tools-notes.inc @@ -47,4 +47,9 @@ On Linux and macOS, it is recommended to install ninja using the OS-specific pac .. tool-ccache-notes +--- + +.. tool-dfu-util-notes + + --- diff --git a/docs/en/get-started/linux-setup-scratch.rst b/docs/en/get-started/linux-setup-scratch.rst index 1ba2f560d..07b236c9a 100644 --- a/docs/en/get-started/linux-setup-scratch.rst +++ b/docs/en/get-started/linux-setup-scratch.rst @@ -15,15 +15,15 @@ To compile with ESP-IDF you need to get the following packages: - CentOS 7:: - sudo yum install git wget ncurses-devel flex bison gperf python pyserial python-pyelftools cmake ninja-build ccache + sudo yum install git wget ncurses-devel flex bison gperf python pyserial python-pyelftools cmake ninja-build ccache dfu-util - Ubuntu and Debian:: - sudo apt-get install git wget libncurses-dev flex bison gperf python python-pip python-setuptools python-serial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja-build ccache libffi-dev libssl-dev + sudo apt-get install git wget libncurses-dev flex bison gperf python python-pip python-setuptools python-serial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja-build ccache libffi-dev libssl-dev dfu-util - Arch:: - sudo pacman -S --needed gcc git make ncurses flex bison gperf python-pyserial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja ccache + sudo pacman -S --needed gcc git make ncurses flex bison gperf python-pyserial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja ccache dfu-util .. note:: CMake version 3.5 or newer is required for use with ESP-IDF. Older Linux distributions may require updating, enabling of a "backports" repository, or installing of a "cmake3" package rather than "cmake". diff --git a/docs/en/get-started/linux-setup.rst b/docs/en/get-started/linux-setup.rst index bde40fe2a..80fcc75b1 100644 --- a/docs/en/get-started/linux-setup.rst +++ b/docs/en/get-started/linux-setup.rst @@ -11,15 +11,15 @@ To compile with ESP-IDF you need to get the following packages: - CentOS 7:: - sudo yum install git wget flex bison gperf python cmake ninja-build ccache + sudo yum install git wget flex bison gperf python cmake ninja-build ccache dfu-util - Ubuntu and Debian:: - sudo apt-get install git wget flex bison gperf python python-pip python-setuptools cmake ninja-build ccache libffi-dev libssl-dev + sudo apt-get install git wget flex bison gperf python python-pip python-setuptools cmake ninja-build ccache libffi-dev libssl-dev dfu-util - Arch:: - sudo pacman -S --needed gcc git make flex bison gperf python-pip cmake ninja ccache + sudo pacman -S --needed gcc git make flex bison gperf python-pip cmake ninja ccache dfu-util .. note:: CMake version 3.5 or newer is required for use with ESP-IDF. Older Linux distributions may require updating, enabling of a "backports" repository, or installing of a "cmake3" package rather than "cmake". diff --git a/docs/en/get-started/macos-setup-scratch.rst b/docs/en/get-started/macos-setup-scratch.rst index e906b9fe7..122373038 100644 --- a/docs/en/get-started/macos-setup-scratch.rst +++ b/docs/en/get-started/macos-setup-scratch.rst @@ -31,11 +31,11 @@ Install Prerequisites - If you have HomeBrew, you can run:: - brew install cmake ninja + brew install cmake ninja dfu-util - If you have MacPorts, you can run:: - sudo port install cmake ninja + sudo port install cmake ninja dfu-util Compile the Toolchain from Source ================================= diff --git a/docs/en/get-started/macos-setup.rst b/docs/en/get-started/macos-setup.rst index caede42ec..5a072c3d7 100644 --- a/docs/en/get-started/macos-setup.rst +++ b/docs/en/get-started/macos-setup.rst @@ -21,11 +21,11 @@ ESP-IDF will use the version of Python installed by default on macOS. - If you have HomeBrew_, you can run:: - brew install cmake ninja + brew install cmake ninja dfu-util - If you have MacPorts_, you can run:: - sudo port install cmake ninja + sudo port install cmake ninja dfu-util - Otherwise, consult the CMake_ and Ninja_ home pages for macOS installation downloads. diff --git a/docs/zh_CN/api-guides/dfu.rst b/docs/zh_CN/api-guides/dfu.rst new file mode 100644 index 000000000..566cdee39 --- /dev/null +++ b/docs/zh_CN/api-guides/dfu.rst @@ -0,0 +1 @@ +.. include:: ../../en/api-guides/dfu.rst diff --git a/docs/zh_CN/api-guides/index.rst b/docs/zh_CN/api-guides/index.rst index 09a32d945..9a08e2ad3 100644 --- a/docs/zh_CN/api-guides/index.rst +++ b/docs/zh_CN/api-guides/index.rst @@ -12,6 +12,7 @@ API 指南 严重错误 Event Handling Deep Sleep Wake Stubs + :esp32s2: Device Firmware Upgrade through USB ESP32 Core Dump Flash Encryption <../security/flash-encryption> FreeRTOS SMP Changes diff --git a/docs/zh_CN/api-guides/tools/idf-tools-notes.inc b/docs/zh_CN/api-guides/tools/idf-tools-notes.inc index c7c750089..946e8222c 100644 --- a/docs/zh_CN/api-guides/tools/idf-tools-notes.inc +++ b/docs/zh_CN/api-guides/tools/idf-tools-notes.inc @@ -49,4 +49,9 @@ On Linux and macOS, it is recommended to install ninja using the OS package mana .. tool-ccache-notes +--- + +.. tool-dfu-util-notes + + --- diff --git a/docs/zh_CN/get-started/linux-setup-scratch.rst b/docs/zh_CN/get-started/linux-setup-scratch.rst index 3d8bee637..5964e4c3c 100644 --- a/docs/zh_CN/get-started/linux-setup-scratch.rst +++ b/docs/zh_CN/get-started/linux-setup-scratch.rst @@ -13,15 +13,15 @@ - CentOS 7:: - sudo yum install git wget ncurses-devel flex bison gperf python pyserial python-pyelftools cmake ninja-build ccache + sudo yum install git wget ncurses-devel flex bison gperf python pyserial python-pyelftools cmake ninja-build ccache dfu-util - Ubuntu 和 Debian:: - sudo apt-get install git wget libncurses-dev flex bison gperf python python-pip python-setuptools python-serial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja-build ccache libffi-dev libssl-dev + sudo apt-get install git wget libncurses-dev flex bison gperf python python-pip python-setuptools python-serial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja-build ccache libffi-dev libssl-dev dfu-util - Arch:: - sudo pacman -S --needed gcc git make ncurses flex bison gperf python-pyserial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja ccache + sudo pacman -S --needed gcc git make ncurses flex bison gperf python-pyserial python-click python-cryptography python-future python-pyparsing python-pyelftools cmake ninja ccache dfu-util .. note:: 使用 ESP-IDF 需要 CMake 3.5 或以上版本。较早版本的 Linux 可能需要升级才能向后移植仓库,或安装 "cmake3" 软件包,而不是安装 "cmake"。 diff --git a/docs/zh_CN/get-started/linux-setup.rst b/docs/zh_CN/get-started/linux-setup.rst index 4f6019857..38b04a855 100644 --- a/docs/zh_CN/get-started/linux-setup.rst +++ b/docs/zh_CN/get-started/linux-setup.rst @@ -11,15 +11,15 @@ Linux 平台工具链的标准设置 - CentOS 7:: - sudo yum install git wget flex bison gperf python cmake ninja-build ccache + sudo yum install git wget flex bison gperf python cmake ninja-build ccache dfu-util - Ubuntu 和 Debian:: - sudo apt-get install git wget flex bison gperf python python-pip python-setuptools cmake ninja-build ccache libffi-dev libssl-dev + sudo apt-get install git wget flex bison gperf python python-pip python-setuptools cmake ninja-build ccache libffi-dev libssl-dev dfu-util - Arch:: - sudo pacman -S --needed gcc git make flex bison gperf python-pip python-pyserial cmake ninja ccache + sudo pacman -S --needed gcc git make flex bison gperf python-pip python-pyserial cmake ninja ccache dfu-util .. note:: 使用 ESP-IDF 需要 CMake 3.5 或以上版本。较早版本的 Linux 可能需要升级才能向后移植仓库,或安装 "cmake3" 软件包,而不是安装 "cmake"。 diff --git a/docs/zh_CN/get-started/macos-setup-scratch.rst b/docs/zh_CN/get-started/macos-setup-scratch.rst index f880317cd..68c0d5aba 100644 --- a/docs/zh_CN/get-started/macos-setup-scratch.rst +++ b/docs/zh_CN/get-started/macos-setup-scratch.rst @@ -31,11 +31,11 @@ MacPorts 需要完整的 XCode 软件,而 homebrew 只需要安装 XCode 命 - 若有 HomeBrew,您可以运行:: - brew install cmake ninja + brew install cmake ninja dfu-util - 若有 MacPorts,您可以运行:: - sudo port install cmake ninja + sudo port install cmake ninja dfu-util 从源代码编译工具链 ================================= diff --git a/docs/zh_CN/get-started/macos-setup.rst b/docs/zh_CN/get-started/macos-setup.rst index 4e9322550..c5d3e52bd 100644 --- a/docs/zh_CN/get-started/macos-setup.rst +++ b/docs/zh_CN/get-started/macos-setup.rst @@ -21,11 +21,11 @@ ESP-IDF 将使用 Mac OS 上默认安装的 Python 版本。 - 若有 HomeBrew_,您可以运行:: - brew install cmake ninja + brew install cmake ninja dfu-util - 若有 MacPorts_,您可以运行:: - sudo port install cmake ninja + sudo port install cmake ninja dfu-util - 若以上均不适用,请访问 CMake_ 和 Ninja_ 主页,查询有关 Mac OS 平台的下载安装问题。 diff --git a/tools/ci/config/host-test.yml b/tools/ci/config/host-test.yml index 84ec1cee8..e7018ddc1 100644 --- a/tools/ci/config/host-test.yml +++ b/tools/ci/config/host-test.yml @@ -304,3 +304,11 @@ test_sysviewtrace_proc: script: - cd ${IDF_PATH}/tools/esp_app_trace/test/sysview - ${IDF_PATH}/tools/ci/multirun_with_pyenv.sh ./test.sh + +test_mkdfu: + extends: .host_test_template + variables: + LC_ALL: C.UTF-8 + script: + - cd ${IDF_PATH}/tools/test_mkdfu + - ${IDF_PATH}/tools/ci/multirun_with_pyenv.sh ./test_mkdfu.py diff --git a/tools/ci/executable-list.txt b/tools/ci/executable-list.txt index 690ba367f..426e30851 100644 --- a/tools/ci/executable-list.txt +++ b/tools/ci/executable-list.txt @@ -85,12 +85,14 @@ tools/ldgen/ldgen.py tools/ldgen/test/test_fragments.py tools/ldgen/test/test_generation.py tools/mass_mfg/mfg_gen.py +tools/mkdfu.py tools/set-submodules-to-github.sh tools/test_check_kconfigs.py tools/test_idf_monitor/run_test_idf_monitor.py tools/test_idf_py/test_idf_py.py tools/test_idf_size/test.sh tools/test_idf_tools/test_idf_tools.py +tools/test_mkdfu/test_mkdfu.py tools/unit-test-app/tools/get_available_configs.sh tools/unit-test-app/unit_test.py tools/windows/eclipse_make.sh diff --git a/tools/ci/test_build_system_cmake.sh b/tools/ci/test_build_system_cmake.sh index 37cbc7902..48a156a2f 100755 --- a/tools/ci/test_build_system_cmake.sh +++ b/tools/ci/test_build_system_cmake.sh @@ -707,6 +707,16 @@ endmenu\n" >> ${IDF_PATH}/Kconfig bin_header_match build/bootloader/bootloader.bin "021f" rm sdkconfig + print_status "DFU build works" + rm -f -r build sdkconfig + idf.py dfu &> tmp.log + grep "command \"dfu\" is not known to idf.py and is not a Ninja target" tmp.log || (tail -n 100 tmp.log ; failure "DFU build should fail for default chip target") + idf.py set-target esp32s2 + idf.py dfu &> tmp.log + grep "build/dfu.bin\" has been written. You may proceed with DFU flashing." tmp.log || (tail -n 100 tmp.log ; failure "DFU build should succeed for esp32s2") + rm tmp.log + assert_built ${APP_BINS} ${BOOTLOADER_BINS} ${PARTITION_BIN} "dfu.bin" + print_status "All tests completed" if [ -n "${FAILURES}" ]; then echo "Some failures were detected:" diff --git a/tools/cmake/dfu.cmake b/tools/cmake/dfu.cmake new file mode 100644 index 000000000..f29de4b58 --- /dev/null +++ b/tools/cmake/dfu.cmake @@ -0,0 +1,26 @@ +# Add DFU build and flashing related targets +# + +function(__add_dfu_targets) + idf_build_get_property(target IDF_TARGET) + if(NOT "${target}" STREQUAL "esp32s2") + return() + endif() + + idf_build_get_property(python PYTHON) + idf_build_get_property(idf_path IDF_PATH) + + add_custom_target(dfu + COMMAND ${python} ${idf_path}/tools/mkdfu.py write + -o "${CMAKE_CURRENT_BINARY_DIR}/dfu.bin" + --json "${CMAKE_CURRENT_BINARY_DIR}/flasher_args.json" + DEPENDS gen_project_binary bootloader + VERBATIM + USES_TERMINAL) + + add_custom_target(dfu-flash + COMMAND dfu-util + -D "${CMAKE_CURRENT_BINARY_DIR}/dfu.bin" + VERBATIM + USES_TERMINAL) +endfunction() diff --git a/tools/cmake/idf.cmake b/tools/cmake/idf.cmake index 2d396c886..82468355e 100644 --- a/tools/cmake/idf.cmake +++ b/tools/cmake/idf.cmake @@ -43,6 +43,7 @@ if(NOT __idf_env_set) include(utilities) include(targets) include(ldgen) + include(dfu) include(version) __build_init("${idf_path}") diff --git a/tools/cmake/project.cmake b/tools/cmake/project.cmake index 11b6c7916..2ec9ea4a6 100644 --- a/tools/cmake/project.cmake +++ b/tools/cmake/project.cmake @@ -486,6 +486,9 @@ macro(project project_name) unset(idf_size) + # Add DFU build and flash targets + __add_dfu_targets() + idf_build_executable(${project_elf}) __project_info("${test_components}") diff --git a/tools/idf.py b/tools/idf.py index 39716210d..118ff5c30 100755 --- a/tools/idf.py +++ b/tools/idf.py @@ -447,7 +447,7 @@ def init_cli(verbose_output=None): def _print_closing_message(self, args, actions): # print a closing message of some kind # - if "flash" in str(actions): + if "flash" in str(actions) or "dfu" in str(actions): print("Done") return diff --git a/tools/idf_py_actions/dfu_ext.py b/tools/idf_py_actions/dfu_ext.py new file mode 100644 index 000000000..af8d2d7fa --- /dev/null +++ b/tools/idf_py_actions/dfu_ext.py @@ -0,0 +1,39 @@ +from idf_py_actions.tools import is_target_supported, ensure_build_directory, run_target +from idf_py_actions.errors import FatalError + + +def action_extensions(base_actions, project_path): + + SUPPORTED_TARGETS = ['esp32s2'] + + def dfu_target(target_name, ctx, args): + ensure_build_directory(args, ctx.info_name) + run_target(target_name, args) + + def dfu_flash_target(target_name, ctx, args): + ensure_build_directory(args, ctx.info_name) + + try: + run_target(target_name, args) + except FatalError: + # Cannot capture the error from dfu-util here so the best advise is: + print('Please have a look at the "Device Firmware Upgrade through USB" chapter in API Guides of the ' + 'ESP-IDF documentation for solving common dfu-util issues.') + raise + + dfu_actions = { + "actions": { + "dfu": { + "callback": dfu_target, + "short_help": "Build the DFU binary", + "dependencies": ["all"], + }, + "dfu-flash": { + "callback": dfu_flash_target, + "short_help": "Flash the DFU binary", + "order_dependencies": ["dfu"], + }, + } + } + + return dfu_actions if is_target_supported(project_path, SUPPORTED_TARGETS) else {} diff --git a/tools/idf_py_actions/tools.py b/tools/idf_py_actions/tools.py index 13cf71211..788bcca5e 100644 --- a/tools/idf_py_actions/tools.py +++ b/tools/idf_py_actions/tools.py @@ -270,6 +270,13 @@ def get_sdkconfig_value(sdkconfig_file, key): return value +def is_target_supported(project_path, supported_targets): + """ + Returns True if the active target is supported, or False otherwise. + """ + return get_sdkconfig_value(os.path.join(project_path, "sdkconfig"), 'CONFIG_IDF_TARGET') in supported_targets + + def _guess_or_check_idf_target(args, prog_name, cache): """ If CMakeCache.txt doesn't exist, and IDF_TARGET is not set in the environment, guess the value from diff --git a/tools/mkdfu.py b/tools/mkdfu.py new file mode 100755 index 000000000..d2e562c71 --- /dev/null +++ b/tools/mkdfu.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python +# +# Copyright 2020 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This program creates archives compatible with ESP32-S* ROM DFU implementation. +# +# The archives are in CPIO format. Each file which needs to be flashed is added to the archive +# as a separate file. In addition to that, a special index file, 'dfuinfo0.dat', is created. +# This file must be the first one in the archive. It contains binary structures describing each +# subsequent file (for example, where the file needs to be flashed/loaded). + +from collections import namedtuple +from future.utils import iteritems +import argparse +import hashlib +import json +import os +import struct +import zlib + +try: + import typing +except ImportError: + # Only used for type annotations + pass + +try: + from itertools import izip as zip +except ImportError: + # Python 3 + pass + +# CPIO ("new ASCII") format related things +CPIO_MAGIC = b"070701" +CPIO_STRUCT = b"=6s" + b"8s" * 13 +CPIOHeader = namedtuple( + "CPIOHeader", + [ + "magic", + "ino", + "mode", + "uid", + "gid", + "nlink", + "mtime", + "filesize", + "devmajor", + "devminor", + "rdevmajor", + "rdevminor", + "namesize", + "check", + ], +) +CPIO_TRAILER = "TRAILER!!!" + + +def make_cpio_header( + filename_len, file_len, is_trailer=False +): # type: (int, int, bool) -> CPIOHeader + """ Returns CPIOHeader for the given file name and file size """ + + def as_hex(val): # type: (int) -> bytes + return "{:08x}".format(val).encode("ascii") + + hex_0 = as_hex(0) + mode = hex_0 if is_trailer else as_hex(0o0100644) + nlink = as_hex(1) if is_trailer else hex_0 + return CPIOHeader( + magic=CPIO_MAGIC, + ino=hex_0, + mode=mode, + uid=hex_0, + gid=hex_0, + nlink=nlink, + mtime=hex_0, + filesize=as_hex(file_len), + devmajor=hex_0, + devminor=hex_0, + rdevmajor=hex_0, + rdevminor=hex_0, + namesize=as_hex(filename_len), + check=hex_0, + ) + + +# DFU format related things +# Structure of one entry in dfuinfo0.dat +DFUINFO_STRUCT = b" int + """ Calculate CRC32/JAMCRC of data, with an optional initial value """ + uint32_max = 0xFFFFFFFF + return uint32_max - (zlib.crc32(data, crc) & uint32_max) + + +def pad_bytes(b, multiple, padding=b"\x00"): # type: (bytes, int, bytes) -> bytes + """ Pad 'b' to a length divisible by 'multiple' """ + padded_len = (len(b) + multiple - 1) // multiple * multiple + return b + padding * (padded_len - len(b)) + + +class EspDfuWriter(object): + def __init__(self, dest_file): # type: (typing.BinaryIO) -> None + self.dest = dest_file + self.entries = [] # type: typing.List[bytes] + self.index = [] # type: typing.List[DFUInfo] + + def add_file(self, flash_addr, path): # type: (int, str) -> None + """ Add file to be written into flash at given address """ + with open(path, "rb") as f: + self._add_cpio_flash_entry(os.path.basename(path), flash_addr, f.read()) + + def finish(self): # type: () -> None + """ Write DFU file """ + # Prepare and add dfuinfo0.dat file + dfuinfo = b"".join([struct.pack(DFUINFO_STRUCT, *item) for item in self.index]) + self._add_cpio_entry(DFUINFO_FILE, dfuinfo, first=True) + + # Add CPIO archive trailer + self._add_cpio_entry(CPIO_TRAILER, b"", trailer=True) + + # Combine all the entries and pad the file + out_data = b"".join(self.entries) + cpio_block_size = 10240 + out_data = pad_bytes(out_data, cpio_block_size) + + # Add DFU suffix and CRC + out_data += struct.pack(DFUSUFFIX_STRUCT, *DFUSUFFIX_DEFAULT) + out_data += struct.pack(DFUCRC_STRUCT, dfu_crc(out_data)) + + # Finally write the entire binary + self.dest.write(out_data) + + def _add_cpio_flash_entry( + self, filename, flash_addr, data + ): # type: (str, int, bytes) -> None + md5 = hashlib.md5() + md5.update(data) + self.index.append( + DFUInfo( + address=flash_addr, + flags=0, + name=filename.encode("utf-8"), + md5=md5.digest(), + ) + ) + self._add_cpio_entry(filename, data) + + def _add_cpio_entry( + self, filename, data, first=False, trailer=False + ): # type: (str, bytes, bool, bool) -> None + filename_b = filename.encode("utf-8") + b"\x00" + cpio_header = make_cpio_header(len(filename_b), len(data), is_trailer=trailer) + entry = pad_bytes( + struct.pack(CPIO_STRUCT, *cpio_header) + filename_b, 4 + ) + pad_bytes(data, 4) + if not first: + self.entries.append(entry) + else: + self.entries.insert(0, entry) + + +def action_write(args): + writer = EspDfuWriter(args['output_file']) + for addr, f in args['files']: + print('Adding {} at {:#x}'.format(f, addr)) + writer.add_file(addr, f) + writer.finish() + print('"{}" has been written. You may proceed with DFU flashing.'.format(args['output_file'].name)) + + +def main(): + parser = argparse.ArgumentParser() + + # Provision to add "info" command + subparsers = parser.add_subparsers(dest="command") + write_parser = subparsers.add_parser("write") + write_parser.add_argument("-o", "--output-file", + help='Filename for storing the output DFU image', + required=True, + type=argparse.FileType("wb")) + write_parser.add_argument("--json", + help='Optional file for loading "flash_files" dictionary with
items') + write_parser.add_argument("files", + metavar="
", help='Add at
', + nargs="*") + + args = parser.parse_args() + + def check_file(file_name): + if not os.path.isfile(file_name): + raise RuntimeError('{} is not a regular file!'.format(file_name)) + return file_name + + files = [] + if args.files: + files += [(int(addr, 0), check_file(f_name)) for addr, f_name in zip(args.files[::2], args.files[1::2])] + + if args.json: + json_dir = os.path.dirname(os.path.abspath(args.json)) + + def process_json_file(path): + ''' + The input path is relative to json_dir. This function makes it relative to the current working + directory. + ''' + return check_file(os.path.relpath(os.path.join(json_dir, path), start=os.curdir)) + + with open(args.json) as f: + files += [(int(addr, 0), + process_json_file(f_name)) for addr, f_name in iteritems(json.load(f)['flash_files'])] + + files = sorted([(addr, f_name) for addr, f_name in iteritems(dict(files))], + key=lambda x: x[0]) # remove possible duplicates and sort based on the address + + cmd_args = {'output_file': args.output_file, + 'files': files, + } + + {'write': action_write + }[args.command](cmd_args) + + +if __name__ == "__main__": + main() diff --git a/tools/test_mkdfu/1/1.bin b/tools/test_mkdfu/1/1.bin new file mode 100644 index 000000000..60320aa0d Binary files /dev/null and b/tools/test_mkdfu/1/1.bin differ diff --git a/tools/test_mkdfu/1/2.bin b/tools/test_mkdfu/1/2.bin new file mode 100644 index 000000000..1b9950f09 Binary files /dev/null and b/tools/test_mkdfu/1/2.bin differ diff --git a/tools/test_mkdfu/1/3.bin b/tools/test_mkdfu/1/3.bin new file mode 100644 index 000000000..08369e849 Binary files /dev/null and b/tools/test_mkdfu/1/3.bin differ diff --git a/tools/test_mkdfu/1/dfu.bin b/tools/test_mkdfu/1/dfu.bin new file mode 100644 index 000000000..74f9fef52 Binary files /dev/null and b/tools/test_mkdfu/1/dfu.bin differ diff --git a/tools/test_mkdfu/1/flasher_args.json b/tools/test_mkdfu/1/flasher_args.json new file mode 100644 index 000000000..06a54d0cc --- /dev/null +++ b/tools/test_mkdfu/1/flasher_args.json @@ -0,0 +1,7 @@ +{ + "flash_files" : { + "0x8000" : "2.bin", + "0x1000" : "1.bin", + "0x10000" : "3.bin" + } +} diff --git a/tools/test_mkdfu/test_mkdfu.py b/tools/test_mkdfu/test_mkdfu.py new file mode 100755 index 000000000..1ce60c227 --- /dev/null +++ b/tools/test_mkdfu/test_mkdfu.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2020 Espressif Systems (Shanghai) CO LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import unicode_literals +import filecmp +import os +import pexpect +import shutil +import sys +import tempfile +import time +import unittest + +current_dir = os.path.dirname(os.path.realpath(__file__)) +mkdfu_path = os.path.join(current_dir, '..', 'mkdfu.py') + + +class TestHelloWorldExample(unittest.TestCase): + def common_test(self, add_args): + with tempfile.NamedTemporaryFile(delete=False) as f: + self.addCleanup(os.unlink, f.name) + cmd = ' '.join([sys.executable, mkdfu_path, 'write', + '-o', f.name, + add_args]) + p = pexpect.spawn(cmd, timeout=10) + self.addCleanup(p.terminate, force=True) + + p.expect_exact(['Adding 1/bootloader.bin at 0x1000', + 'Adding 1/partition-table.bin at 0x8000', + 'Adding 1/hello-world.bin at 0x10000', + '"{}" has been written. You may proceed with DFU flashing.'.format(f.name)]) + + # Need to wait for the process to end because the output file is closed when mkdfu exits. + # Do non-blocking wait instead of the blocking p.wait(): + for _ in range(10): + if not p.isalive(): + break + time.sleep(0.5) + else: + p.terminate() + + self.assertTrue(filecmp.cmp(f.name, os.path.join(current_dir, '1','dfu.bin')), 'Output files are different') + + def test_with_json(self): + self.common_test(' '.join(['--json', os.path.join(current_dir, '1', 'flasher_args.json')])) + + def test_without_json(self): + + self.common_test(' '.join(['0x1000', os.path.join(current_dir, '1', '1.bin'), + '0x8000', os.path.join(current_dir, '1', '2.bin'), + '0x10000', os.path.join(current_dir, '1', '3.bin') + ])) + + def test_filenames(self): + temp_dir = tempfile.mkdtemp(prefix='very_long_directory_name' * 8) + self.addCleanup(shutil.rmtree, temp_dir, ignore_errors=True) + + with tempfile.NamedTemporaryFile(dir=temp_dir, delete=False) as f: + output = f.name + + with tempfile.NamedTemporaryFile(prefix='ľščťžýáíéěř\u0420\u043e\u0441\u0441\u0438\u044f', + dir=temp_dir, + delete=False) as f: + bootloader = f.name + + shutil.copyfile(os.path.join(current_dir, '1', '1.bin'), bootloader) + + cmd = ' '.join([sys.executable, mkdfu_path, 'write', + '-o', output, + ' '.join(['0x1000', bootloader, + '0x8000', os.path.join(current_dir, '1', '2.bin'), + '0x10000', os.path.join(current_dir, '1', '3.bin') + ]) + ]) + p = pexpect.spawn(cmd, timeout=10, encoding='utf-8') + self.addCleanup(p.terminate, force=True) + + p.expect_exact(['Adding {} at 0x1000'.format(bootloader), + 'Adding 1/2.bin at 0x8000', + 'Adding 1/3.bin at 0x10000', + '"{}" has been written. You may proceed with DFU flashing.'.format(output)]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/tools.json b/tools/tools.json index f1c93c2f6..840a734e6 100644 --- a/tools/tools.json +++ b/tools/tools.json @@ -483,6 +483,43 @@ } } ] + }, + { + "description": "dfu-util (Device Firmware Upgrade Utilities)", + "export_paths": [ + [ + "dfu-util-0.9-win64" + ] + ], + "export_vars": {}, + "info_url": "http://dfu-util.sourceforge.net/", + "install": "never", + "license": "GPL-2.0-only", + "name": "dfu-util", + "platform_overrides": [ + { + "install": "always", + "platforms": [ + "win64" + ] + } + ], + "version_cmd": [ + "dfu-util", + "--version" + ], + "version_regex": "dfu-util ([0-9.]+)", + "versions": [ + { + "name": "0.9", + "status": "recommended", + "win64": { + "sha256": "5816d7ec68ef3ac07b5ac9fb9837c57d2efe45b6a80a2f2bbe6b40b1c15c470e", + "size": 735635, + "url": "https://dl.espressif.com/dl/dfu-util-0.9-win64.zip" + } + } + ] } ], "version": 1