Compare commits

...

13 Commits

4 changed files with 229 additions and 110 deletions

View File

@ -1,6 +1,48 @@
This is a [USB DFU](https://www.usb.org/document-library/device-firmware-upgrade-11-new-version-31-aug-2004) (DFU mode) implementation for [ESP32-S2](https://www.espressif.com/en/products/socs/esp32-s2) based on [tinyUSB](https://docs.tinyusb.org/en/latest/index.html).
It allows flashing the firmware using [dfu-util](http://dfu-util.sourceforge.net/).
background
==========
ESP-ROM
-------
The ESP-S2 comes with a ROM bootloader that already allows you to flash over USB using the serial CDC ACM profile.
But this method does not let you restart into the main firmware.
The ROM bootloader USB stack even offers DFU capability, and you can flash using dfu-util.
But this one also does not let you restart into the main firmware.
To flash using USB serial (can't restart the device):
~~~
idf.py -p /dev/ttyACM0 flash
...
WARNING: ESP32-S2 chip was placed into download mode using GPIO0.
esptool.py can not exit the download mode over USB. To run the app, reset the chip manually.
To suppress this note, set --after option to 'no_reset'.
~~~
And to [flash using DFU](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-guides/dfu.html):
~~~
# generate the DFU binary (not the same as the usual flash binary)
idf.py dfu
dfu-util --device 303a:0002 --download build/dfu.bin
# or
idf.py dfu-flash
~~~
Note: after detaching, the bootloader claims for be in runtime mode, but this is still the bootloader, and not the flashed firmware.
mode switch
-----------
To switch from the ESP32-S2 ROM bootloader back to the firmware flashed over USB, you need to press the reset button.
This is cumbersome when developing firmware, and sometimes impossible if the board is encased or installed remotely.
This USB DFU implementation allows to switch back to runtime mode without pressing a button.
The runtime firmware can also reboot into the DFU mode without pressing a button.
install
=======
@ -24,7 +66,7 @@ idf.py -B_build/wemos_s2_mini -DFAMILY=esp32s2 -DBOARD=wemos_s2_mini -DIDF_TARG
flash
-----
To flash the compiled fimware:
To flash the compiled firmware:
~~~
idf.py -B_build/wemos_s2_mini -DFAMILY=esp32s2 -DBOARD=wemos_s2_mini -DIDF_TARGET=esp32s2 -p /dev/ttyACM0 flash
@ -37,8 +79,8 @@ esptool.py -p /dev/ttyACM0 --before no_reset --after no_reset --chip esp32s2 wr
~~~
This will flash the DFU firmware to the `factory` partition.
The DFU firmware will in turn flash the downladed image onto the [OTA0](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-reference/system/ota.html) partition.
For that a custom [partition table](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-guides/partition-tables.html) is used.
The DFU firmware will in turn flash the downloaded image onto the [OTA0](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-reference/system/ota.html) partition.
For that, a custom [partition table](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-guides/partition-tables.html) is used.
config
------
@ -53,10 +95,27 @@ runtime
-------
This firmware is just for the DFU mode.
The main application needs to implement the runtime mode seperately.
The main application needs to implement the runtime mode separately.
To switch from runtime to DFU mode during detach, set the boot partition in `otadata` to `factory`, and restart the ESP.
force DFU
---------
The USB DFU firmware does not act as bootloader.
Meaning, the ESP bootloader does not start the USB DFU firmware, which in turn starts the main firmware.
The main firmware in the OTA0 partition is directly loaded by the ESP bootloader.
Thus is it up to the main firmware to start the DFU mode, as described in `runtime`.
In case the main firmware is defective, and does not allow to switch back to DFU mode, you can still force booting the USB DFU firmware.
For that, boot the ESP ROM bootloader (i.e. download mode), and invalidate the OTA data partition:
~~~
otatool.py --esptool-args after=no_reset_stub --port /dev/ttyACM0 erase_otadata
~~~
When restating the ESP by pressing the reset button, the ESP bootloader will start the firmware which is in the factory partition, which should be the DFU firmware previously flashed.
design choice
=============
@ -94,36 +153,6 @@ Feel free to reuse the 'partitions.csv' file as example.
alternatives
============
ESP-ROM
-------
The ESP-S2 comes with a ROM bootloader that already allows you to flash over USB using the serial CDC ACM profile.
But this method does not let you restart into the main firmware.
The ROM bootloader USB stack even offers DFU capability, and you can flash using dfu-util.
But this one also does not let you restart into the main firmware.
To flash using USB serial (can't restart the device):
~~~
idf.py -p /dev/ttyACM0 flash
...
WARNING: ESP32-S2 chip was placed into download mode using GPIO0.
esptool.py can not exit the download mode over USB. To run the app, reset the chip manually.
To suppress this note, set --after option to 'no_reset'.
~~~
And to [flash using DFU](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-guides/dfu.html):
~~~
# generate the DFU binary (not the same as the usual flash binary)
idf.py dfu
dfu-util --device 303a:0002 --download build/dfu.bin
# or
idf.py dfu-flash
~~~
Note: after detaching, the bootloader claims for be in runtime mode, but this is still the bootloader, and not the flashed firmware.
TinyUF2
-------

View File

@ -14,3 +14,15 @@ CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
#
# UART console
#
CONFIG_ESP_CONSOLE_UART_NONE=y
CONFIG_CONSOLE_UART_NUM=-1
#
# CPU frequency
#
CONFIG_ESP32S2_DEFAULT_CPU_FREQ_240=y
CONFIG_ESP32S2_DEFAULT_CPU_FREQ_MHZ=240

View File

@ -87,6 +87,9 @@ void usb_device_task(void* param);
// used to download image onto OTA partition
esp_ota_handle_t ota_handle = 0;
// how to program the downloaded data (0 = using OTA tools, 1 = using partition tools)
static uint8_t dl_method = 0;
// download blocks to program
static uint8_t dl_block[CFG_TUD_DFU_XFER_BUFSIZE] = {0};
static uint16_t dl_block_len = 0;
@ -99,42 +102,139 @@ static void dl_task(void* arg)
esp_err_t rc;
while (true) {
if (dl_block_len) { // we received a new block
// get handle for OTA update
if (0 == ota_handle) {
const esp_partition_t *ota_part = esp_ota_get_next_update_partition(NULL);
if (NULL == ota_part) {
ESP_LOGE(TAG, "OTA not found");
tud_dfu_finish_flashing(DFU_STATUS_ERR_PROG);
return;
const esp_partition_t *ota_part = esp_ota_get_next_update_partition(NULL);
if (NULL == ota_part) {
ESP_LOGE(TAG, "OTA not found");
tud_dfu_finish_flashing(DFU_STATUS_ERR_PROG);
continue;
}
if (0 == dl_method) { // using OTA tools
// get handle for OTA update
if (0 == ota_handle) {
ESP_LOGD(TAG, "init OTA flash");
rc = esp_ota_begin(ota_part, OTA_SIZE_UNKNOWN, &ota_handle);
if (ESP_OK != rc) {
ESP_LOGE(TAG, "init OTA failed");
esp_ota_abort(ota_handle);
ota_handle = 0;
tud_dfu_finish_flashing(DFU_STATUS_ERR_ERASE);
continue;
}
}
rc = esp_ota_begin(ota_part, OTA_SIZE_UNKNOWN, &ota_handle);
// write data to partition
rc = esp_ota_write_with_offset(ota_handle, dl_block, dl_block_len, dl_block_num * CFG_TUD_DFU_XFER_BUFSIZE);
if (ESP_OK != rc) {
ESP_LOGE(TAG, "init OTA failed");
ESP_LOGE(TAG, "writing OTA failed");
esp_ota_abort(ota_handle);
ota_handle = 0;
tud_dfu_finish_flashing(DFU_STATUS_ERR_ERASE);
return;
tud_dfu_finish_flashing(DFU_STATUS_ERR_WRITE);
continue;
}
}
// write data to partition
rc = esp_ota_write_with_offset(ota_handle, dl_block, dl_block_len, dl_block_num * CFG_TUD_DFU_XFER_BUFSIZE);
if (ESP_OK != rc) {
ESP_LOGE(TAG, "writing OTA failed");
esp_ota_abort(ota_handle);
ota_handle = 0;
tud_dfu_finish_flashing(DFU_STATUS_ERR_WRITE);
return;
tud_dfu_finish_flashing(DFU_STATUS_OK); // flashing op for download complete without error
} else if (1 == dl_method) { // using partition tools
// read current data
uint8_t flash_block[CFG_TUD_DFU_XFER_BUFSIZE];
rc = esp_partition_read(ota_part, dl_block_num * CFG_TUD_DFU_XFER_BUFSIZE, flash_block, dl_block_len);
if (ESP_OK != rc) {
ESP_LOGE(TAG, "reading range failed");
tud_dfu_finish_flashing(DFU_STATUS_ERR_CHECK_ERASED);
continue;
}
// check if the data is different and we need to write it
if (0 == memcmp(dl_block, flash_block, dl_block_len)) { // data is the same
ESP_LOGD(TAG, "data already flashed");
dl_block_len = 0; // ready for next block
tud_dfu_finish_flashing(DFU_STATUS_OK); // flashing op for download complete without error
continue;
}
// check if the area to be written is already erased
bool erased = true;
for (uint16_t i = 0; i < dl_block_len && erased; i++) {
if (0xff != flash_block[i]) {
erased = false;
}
}
// get range when some of it is not erased
if (!erased) {
#define RANGE_SIZE 4096U // range/page size for the flash
// get range data
const uint32_t range_start = (dl_block_num * CFG_TUD_DFU_XFER_BUFSIZE) - ((dl_block_num * CFG_TUD_DFU_XFER_BUFSIZE) % RANGE_SIZE);
uint8_t range_data[RANGE_SIZE];
rc = esp_partition_read(ota_part, range_start, range_data, RANGE_SIZE);
if (ESP_OK != rc) {
ESP_LOGE(TAG, "reading range failed");
tud_dfu_finish_flashing(DFU_STATUS_ERR_CHECK_ERASED);
continue;
}
// erase range
rc = esp_partition_erase_range(ota_part, range_start, RANGE_SIZE); // erase page (block is smaller than page)
if (ESP_OK != rc) {
ESP_LOGE(TAG, "erase range failed");
tud_dfu_finish_flashing(DFU_STATUS_ERR_ERASE);
continue;
}
// write range data before downloaded block
rc = esp_partition_write(ota_part, range_start, range_data, dl_block_num * CFG_TUD_DFU_XFER_BUFSIZE - range_start); // write old
if (ESP_OK != rc) {
ESP_LOGE(TAG, "writing existing range failed");
tud_dfu_finish_flashing(DFU_STATUS_ERR_WRITE);
continue;
}
}
rc = esp_partition_write(ota_part, dl_block_num * CFG_TUD_DFU_XFER_BUFSIZE, dl_block, dl_block_len); // write new data
if (ESP_OK != rc) {
ESP_LOGE(TAG, "writing range failed");
tud_dfu_finish_flashing(DFU_STATUS_ERR_WRITE);
continue;
}
ESP_LOGD(TAG, "writing range OK");
dl_block_len = 0; // ready for next block
tud_dfu_finish_flashing(DFU_STATUS_OK); // flashing op for download complete without error
} else { // unknown method
tud_dfu_finish_flashing(DFU_STATUS_ERR_TARGET);
}
dl_block_len = 0; // ready for next block
tud_dfu_finish_flashing(DFU_STATUS_OK); // flashing op for download complete without error
} else { // no new block
vTaskDelay(1); // allow other tasks to run (and watchdog reset)
}
}
}
// task to complete flashing, used when entering manifestation
static void complete_task(void* arg)
{
esp_err_t rc;
// finish flashing
if (ota_handle) {
rc = esp_ota_end(ota_handle);
ota_handle = 0;
if (ESP_OK != rc) {
ESP_LOGE(TAG, "close OTA failed");
tud_dfu_finish_flashing(DFU_STATUS_ERR_PROG);
vTaskDelete(NULL); // close task
}
}
// switch to OTA app
const esp_partition_t *ota = esp_ota_get_next_update_partition(NULL);
esp_app_desc_t ota_desc;
esp_ota_get_partition_description(ota, &ota_desc);
if (ESP_APP_DESC_MAGIC_WORD == ota_desc.magic_word) {
ESP_LOGI(TAG, "set boot to valid app");
esp_ota_set_boot_partition(ota);
xTimerChangePeriod(blinky_tm, pdMS_TO_TICKS(BLINK_IDLE), 0);
tud_dfu_finish_flashing(DFU_STATUS_OK);
} else {
ESP_LOGI(TAG, "no valid app");
tud_dfu_finish_flashing(DFU_STATUS_ERR_VERIFY);
}
vTaskDelete(NULL); // close task
}
//--------------------------------------------------------------------+
// Main
//--------------------------------------------------------------------+
@ -142,12 +242,13 @@ static void dl_task(void* arg)
int main(void)
{
board_init();
const esp_partition_t *next = esp_ota_get_next_update_partition(NULL);
esp_app_desc_t next_desc;
esp_ota_get_partition_description(next, &next_desc);
if (ESP_APP_DESC_MAGIC_WORD == next_desc.magic_word) {
ESP_LOGI(TAG, "boot set to valid app");
ESP_LOGI(TAG, "set app for next boot");
esp_ota_set_boot_partition(next);
} else {
ESP_LOGI(TAG, "no valid app");
@ -166,7 +267,7 @@ int main(void)
#endif
// create task to handle download progress
xTaskCreate(dl_task, "dl_task", 2 * 1024, NULL, 8, NULL);
xTaskCreate(dl_task, "dl_task", 7 * 1024, NULL, 8, NULL);
ESP_LOGI(TAG, "DFU mode");
@ -250,13 +351,13 @@ void tud_dfu_download_cb(uint8_t alt, uint16_t block_num, uint8_t const* data, u
ESP_LOGD(TAG, "download, alt=%u block=%u", alt, block_num);
if (alt > 0) {
if (alt > 1) {
ESP_LOGW(TAG, "download to invalid alt %u", alt);
tud_dfu_finish_flashing(DFU_STATUS_ERR_ADDRESS);
return;
}
if (0 == length) { // there is nothing to programm
if (0 == length) { // there is nothing to program
// finish flashing
if (ota_handle) {
rc = esp_ota_end(ota_handle);
@ -271,16 +372,17 @@ void tud_dfu_download_cb(uint8_t alt, uint16_t block_num, uint8_t const* data, u
tud_dfu_finish_flashing(DFU_STATUS_OK); // flashing op for download complete without error
} else if (length > CFG_TUD_DFU_XFER_BUFSIZE) { // more data than we can handle
tud_dfu_finish_flashing(DFU_STATUS_ERR_PROG);
} else if (dl_block_len) { // there is already a block to programm
} else if (dl_block_len) { // there is already a block to program
tud_dfu_finish_flashing(DFU_STATUS_ERR_STALLEDPKT);
} else { // all is fine to programm
} else { // all is fine to program
if (!dl_started) {
xTimerChangePeriod(blinky_tm, pdMS_TO_TICKS(BLINK_DOWNLOAD), 0);
dl_started = true;
}
memcpy(dl_block, data, length); // copy data for dl_task
dl_method = alt; // remember how to flash
dl_block_num = block_num; // remember offset
dl_block_len = length; // notitfy dl_task block is ready
dl_block_len = length; // notify dl_task block is ready
}
}
@ -289,38 +391,9 @@ void tud_dfu_download_cb(uint8_t alt, uint16_t block_num, uint8_t const* data, u
// Once finished flashing, application must call tud_dfu_finish_flashing()
void tud_dfu_manifest_cb(uint8_t alt)
{
(void) alt;
esp_err_t rc;
(void)alt; // not used
ESP_LOGI(TAG, "download completed, enter manifestation");
blinky_tm = xTimerCreateStatic(NULL, pdMS_TO_TICKS(BLINK_IDLE), true, NULL, led_blinky_cb, &blinky_tmdef);
// finish flashing
if (ota_handle) {
rc = esp_ota_end(ota_handle);
ota_handle = 0;
if (ESP_OK != rc) {
ESP_LOGE(TAG, "close OTA failed");
tud_dfu_finish_flashing(DFU_STATUS_ERR_PROG);
return;
}
}
// switch to OTA app
const esp_partition_t *ota = esp_ota_get_next_update_partition(NULL);
esp_app_desc_t ota_desc;
esp_ota_get_partition_description(ota, &ota_desc);
if (ESP_APP_DESC_MAGIC_WORD == ota_desc.magic_word) {
ESP_LOGI(TAG, "boot set to valid app");
esp_ota_set_boot_partition(ota);
} else {
ESP_LOGI(TAG, "no valid app");
tud_dfu_finish_flashing(DFU_STATUS_ERR_VERIFY);
return;
}
// flashing op for manifest is complete without error
// Application can perform checksum, should it fail, use appropriate status such as errVERIFY.
tud_dfu_finish_flashing(DFU_STATUS_OK);
xTaskCreate(complete_task, "complete_task", 3 * 1024, NULL, 8, NULL);
}
// Invoked when received DFU_UPLOAD request

View File

@ -82,7 +82,7 @@ uint8_t const * tud_descriptor_device_cb(void)
//--------------------------------------------------------------------+
// Number of Alternate Interface (each for 1 flash partition)
#define ALT_COUNT 1
#define ALT_COUNT 2
enum
{
@ -123,7 +123,8 @@ char const* string_desc_arr [] =
"CuVoodoo", // 1: Manufacturer
"ESP32-S2 DFU", // 2: Product
"", // 3: Serials, should use chip ID
"ota0", // 4: DFU Partition 1
"app (erase all)", // 4: DFU Partition (erase the complete partition, slow)
"app (overwrite)", // 5: DFU Partition (just overwrite new data, fast)
};
static uint16_t _desc_str[32];
@ -136,35 +137,39 @@ uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid)
size_t chr_count;
if ( index == 0)
{
if (index == 0) { // get language (not a string)
memcpy(&_desc_str[1], string_desc_arr[0], 2);
chr_count = 1;
}
else
{
} else { // get other description (strings)
// Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors.
// https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors
if ( !(index < sizeof(string_desc_arr)/sizeof(string_desc_arr[0])) ) return NULL;
if ( !(index < sizeof(string_desc_arr)/sizeof(string_desc_arr[0])) ) return NULL; // check if requested descriptor exists
const char* str = string_desc_arr[index]; // load requested string
if (3 == index) { // serial
// set MAC as serial
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_ETH);
char usb_serial[13] = {0};
snprintf(usb_serial, sizeof(usb_serial), "%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
str = usb_serial;
}
const char* str = string_desc_arr[index];
// Cap at max char
// check string length
chr_count = strlen(str);
if ( chr_count > 31 ) {
if ( chr_count > 31 ) { // cap at max char
chr_count = 31;
}
// Convert ASCII string into UTF-16
for(uint8_t i=0; i<chr_count; i++)
// convert ASCII string into UTF-16
for(uint8_t i = 0; i < chr_count; i++)
{
_desc_str[1+i] = str[i];
_desc_str[1 + i] = str[i];
}
}
// first byte is length (including header), second byte is string type
_desc_str[0] = (uint16_t)((((uint16_t)TUSB_DESC_STRING) << 8 ) | (2u*chr_count + 2u));
_desc_str[0] = (uint16_t)((((uint16_t)TUSB_DESC_STRING) << 8 ) | (2u * chr_count + 2u));
return _desc_str;
}