Configure the FPGA over USB Serial
This CL adds provisioning the FPGA with a bitstream sent over USB
serial via the write_fpga.py script.
The implementation uses only raw serial input and does no
verification. Followup CLs will switch to using pw_hdlc or another way
to check integrity.
Change-Id: I714dfd8b797261b600e9e4b045a7327e82cac309
Reviewed-on: https://pigweed-review.googlesource.com/c/gonk/+/183228
Reviewed-by: Umang Shah <umangshah@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed-service-accounts.iam.gserviceaccount.com>
Reviewed-by: Eric Holland <hollande@google.com>
Pigweed-Auto-Submit: Anthony DiGirolamo <tonymd@google.com>
diff --git a/README.md b/README.md
index 477707d..c9e7157 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,7 @@
The build commands are defined in: `//tools/gonk_tools/build_project.py`.
-## Verilog:
+## Gonk Verilog:
The Verilog build requires the following to be installed on Linux:
@@ -58,6 +58,39 @@
The bitstream files will be written with the `.bin` extenson under
`./out/gn/obj/fpga/*/*.bin`.
+## Gonk `fpga_config` Example
+
+Flash the stm32f7 and launch the `write_fpga.py` script on a bitstream file.
+
+### Flash with `dfu-util`
+
+1. Create a bin file from the elf. This will be automated later.
+
+ ```sh
+ arm-none-eabi-objcopy -O binary \
+ ./out/gn/arduino_size_optimized/obj/applications/fpga_config/bin/fpga_config.elf \
+ ./out/gn/arduino_size_optimized/obj/applications/fpga_config/bin/fpga_config.bin
+ ```
+
+1. Unplug gonk from USB and replug with MODE button held down.
+
+1. Run dfu-util to flash.
+
+ ```sh
+ dfu-util -d 0483:df11 -s 0x08000000:leave \
+ --serial STM32FxSTM32 -a 0 \
+ -D ./out/gn/arduino_size_optimized/obj/applications/fpga_config/bin/fpga_config.bin
+ ```
+
+### Write the FPGA bitstream
+
+1. Write an FPGA bitstream with the `write_fpga.py` script:
+
+ ```sh
+ python ./tools/gonk_tools/write_fpga.py ./applications/fpga_config/fpga_blinky.bin
+ ```
+
+
## Gonk `spi_flash_test` Example
### Flash with `dfu-util`
diff --git a/applications/fpga_config/BUILD.gn b/applications/fpga_config/BUILD.gn
index 7bc6b85..934f794 100644
--- a/applications/fpga_config/BUILD.gn
+++ b/applications/fpga_config/BUILD.gn
@@ -30,7 +30,9 @@
"$dir_pw_log",
"$dir_pw_string",
"$dir_pw_third_party/arduino:arduino_core_sources",
+ "//lib/fpga_control",
"//lib/pin_config",
+ "//lib/spi_flash",
]
ldflags = [ "-Wl,--print-memory-usage" ]
diff --git a/applications/fpga_config/fpga_blinky.bin b/applications/fpga_config/fpga_blinky.bin
new file mode 100644
index 0000000..442d775
--- /dev/null
+++ b/applications/fpga_config/fpga_blinky.bin
Binary files differ
diff --git a/applications/fpga_config/main.cc b/applications/fpga_config/main.cc
index 89adae2..d028721 100644
--- a/applications/fpga_config/main.cc
+++ b/applications/fpga_config/main.cc
@@ -12,37 +12,64 @@
// License for the specific language governing permissions and limitations under
// the License.
+#include <bitset>
#include <cstdint>
#include <Arduino.h>
#include <SPI.h>
#include "pw_log/log.h"
+#include "pw_result/result.h"
+#include "pw_string/format.h"
+#include "gonk/fpga_control.h"
#include "gonk/pin_config.h"
+#include "gonk/spi_flash.h"
+using gonk::fpga_control::FpgaControl;
+using gonk::pin_config::FlashCS;
+using gonk::pin_config::ICE40Done;
using gonk::pin_config::PinConfig;
using gonk::pin_config::StatusLed;
+using gonk::spi_flash::SpiFlash;
namespace {
PinConfig pin_config = PinConfig();
+FpgaControl fpga_control = FpgaControl(&pin_config);
+SpiFlash spi_flash = SpiFlash(FlashCS, /*baudrate=*/1000000);
+
+void ice40_done_rising_isr() { PW_LOG_DEBUG("ICE40 Done: HIGH"); }
+
+void (*current_task)();
} // namespace
-int main() {
- pin_config.Init();
- pin_config.InitFpgaPins();
- pin_config.FpgaHalt();
- pin_config.FpgaEnable();
- delay(10);
+void IdleTask();
+void FpgaConfigTask();
- uint16_t update_count = 0;
+void IdleTask() {
+ static uint32_t last_update = millis();
+ static uint32_t this_update = millis();
+ static uint16_t update_count = 0;
- while (true) {
- PW_LOG_INFO("update: %u", update_count);
- delay(1000);
+ this_update = millis();
+ // Check for serial input.
+ if (Serial.available()) {
+ int data_in = Serial.read();
+ // Press enter to restart the FPGA config.
+ if (data_in == '\n') {
+ PW_LOG_INFO("Restarting FPGA Config.");
+ current_task = &FpgaConfigTask;
+ }
+ }
+
+ // Output an idle state heartbeat message each second.
+ if (this_update > last_update + 1000) {
+ PW_LOG_INFO("Idle update: %d", update_count);
+
+ last_update = this_update;
update_count = (update_count + 1) % UINT16_MAX;
// Toggle status LED each loop.
@@ -52,6 +79,34 @@
digitalWrite(StatusLed, LOW);
}
}
+}
+void FpgaConfigTask() {
+ auto result = fpga_control.StartConfig();
+ if (result.ok()) {
+ current_task = &IdleTask;
+ } else {
+ PW_LOG_INFO("Restarting FPGA Config.");
+ current_task = &FpgaConfigTask;
+ }
+}
+
+int main() {
+ // Debug interrupt to watch when ICE40Done goes high.
+ attachInterrupt(/*pin=*/ICE40Done, /*callback=*/&ice40_done_rising_isr,
+ /*mode=*/HIGH);
+
+ pin_config.Init();
+ pin_config.InitFpgaPins();
+ delay(500);
+
+ current_task = &FpgaConfigTask;
+
+ // Start the task loop.
+ while (true) {
+ current_task();
+ }
+
+ PW_UNREACHABLE;
return 0;
}
diff --git a/lib/fpga_control/BUILD.gn b/lib/fpga_control/BUILD.gn
new file mode 100644
index 0000000..61c7987
--- /dev/null
+++ b/lib/fpga_control/BUILD.gn
@@ -0,0 +1,37 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("default_config") {
+ include_dirs = [ "public" ]
+}
+
+pw_source_set("fpga_control") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/gonk/fpga_control.h" ]
+
+ sources = [ "fpga_control.cc" ]
+
+ deps = [
+ "$dir_pw_log",
+ "$dir_pw_result",
+ "$dir_pw_span",
+ "$dir_pw_status",
+ "$dir_pw_third_party/arduino:arduino_core_sources",
+ "//lib/pin_config",
+ ]
+}
diff --git a/lib/fpga_control/fpga_control.cc b/lib/fpga_control/fpga_control.cc
new file mode 100644
index 0000000..7d4ff6a
--- /dev/null
+++ b/lib/fpga_control/fpga_control.cc
@@ -0,0 +1,184 @@
+#include "public/gonk/fpga_control.h"
+#include <Arduino.h>
+#include <bitset>
+#include <cstddef>
+#include <stdint.h>
+
+#include "gonk/fpga_control.h"
+
+#define PW_LOG_LEVEL PW_LOG_LEVEL_DEBUG
+#define PW_LOG_MODULE_NAME "FpgaControl"
+
+#include "pw_log/log.h"
+#include "pw_result/result.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+
+using gonk::pin_config::FlashCLK;
+using gonk::pin_config::FlashCS;
+using gonk::pin_config::FlashMISO;
+using gonk::pin_config::ICE40Done;
+using gonk::pin_config::PinConfig;
+
+namespace gonk::fpga_control {
+
+FpgaControl::FpgaControl(PinConfig *pin_config) : pin_config_(pin_config) {}
+
+Status FpgaControl::StartConfig() {
+ bitstream_file_position = 0;
+ memset(read_buffer, 0, 4);
+ memset(bitstream_file, 0, BitstreamFileLength);
+ bytes_written = 0;
+
+ WaitForBitstreamStart();
+ ReadBitstream();
+ auto verify_result = VerifyBitstream();
+ if (!verify_result.ok()) {
+ return verify_result;
+ }
+ return SendBitstreamToFpga();
+}
+
+Status FpgaControl::WaitForBitstreamStart() {
+ uint32_t last_update = millis();
+ uint32_t this_update = millis();
+
+ while (true) {
+ // Output a heartbeat message while waiting for data.
+ this_update = millis();
+ if (bytes_written == 0 && this_update > last_update + 1000) {
+ last_update = this_update;
+ PW_LOG_INFO("Waiting for bitstream");
+ }
+
+ // If no data waiting start over.
+ if (!Serial.available()) {
+ continue;
+ }
+
+ uint8_t data_in = Serial.read();
+ read_buffer[bytes_written % 4] = data_in;
+ bytes_written++;
+
+ PW_LOG_INFO("Discard byte: %d", bytes_written);
+ // If the last 4 bytes recieved match the start sequence, break.
+ if (read_buffer[(bytes_written + 0) % 4] == 0xff &&
+ read_buffer[(bytes_written + 1) % 4] == 0x00 &&
+ read_buffer[(bytes_written + 2) % 4] == 0x00 &&
+ read_buffer[(bytes_written + 3) % 4] == 0xff) {
+ PW_LOG_INFO("Start Sequence found.");
+ break;
+ }
+ }
+
+ return pw::OkStatus();
+}
+
+Status FpgaControl::ReadBitstream() {
+ char bitstream_buffer[4096];
+
+ while (true) {
+ size_t read_byte_count = Serial.readBytes(bitstream_buffer, 4096);
+ PW_LOG_INFO("Got bytes: %d", read_byte_count);
+ bytes_written += read_byte_count;
+
+ memcpy(&bitstream_file[bitstream_file_position], bitstream_buffer,
+ read_byte_count);
+ bitstream_file_position += read_byte_count;
+
+ if (bytes_written >= 135100) {
+ PW_LOG_INFO("All 135100 bytes recieved.");
+ break;
+ }
+ }
+
+ return pw::OkStatus();
+}
+
+Status FpgaControl::VerifyBitstream() {
+ // TODO(tonymd): Verify the bitstream with somehow, perhaps move to pw_hdlc.
+ PW_LOG_INFO("File ready: %d", bitstream_file_position);
+
+ PW_LOG_INFO("First 12 bytes");
+ for (int i = 0; i < 12; i++) {
+ PW_LOG_INFO("%x", bitstream_file[i]);
+ }
+
+ PW_LOG_INFO("Last 12 bytes");
+ for (int i = bitstream_file_position - 12; i < bitstream_file_position; i++) {
+ PW_LOG_INFO("%x", bitstream_file[i]);
+ }
+
+ return pw::OkStatus();
+}
+
+Status FpgaControl::SendBitstreamToFpga() {
+ PW_LOG_INFO("Sending bitstream file to the FPGA.");
+
+ pin_config_->SPIDisable();
+ pin_config_->FpgaSpiConfigMode();
+
+ digitalWrite(FlashCS, LOW);
+
+ // Write out the bitstream file similar to an SPI transaction but using MISO
+ // as the output pin.
+ for (int i = 0; i < bitstream_file_position; i++) {
+ uint8_t d = bitstream_file[i];
+ // Write out each 8 bits.
+ for (int b = 0; b < 8; b++) {
+ if (d & 0x80) {
+ digitalWrite(FlashCLK, LOW);
+ digitalWrite(FlashMISO, HIGH);
+ digitalWrite(FlashCLK, HIGH);
+ } else {
+ digitalWrite(FlashCLK, LOW);
+ digitalWrite(FlashMISO, LOW);
+ digitalWrite(FlashCLK, HIGH);
+ }
+ d <<= 1;
+ }
+ digitalWrite(FlashCLK, HIGH);
+ }
+
+ digitalWrite(FlashCS, HIGH);
+
+ uint32_t last_update = millis();
+ uint32_t this_update = millis();
+ bool done = false;
+ int ice40_done = 0;
+
+ // Wait for ICE40Done (CDONE) signal to go high
+ while (!done) {
+ // Write one clock pulse
+ digitalWrite(FlashCLK, LOW);
+ delayMicroseconds(100);
+ digitalWrite(FlashCLK, HIGH);
+ delayMicroseconds(100);
+
+ ice40_done = digitalRead(ICE40Done);
+ if (ice40_done == 1) {
+ PW_LOG_INFO("FPGA Config Success.");
+ break;
+ }
+
+ this_update = millis();
+ // If more than two seconds have passed something likely went wrong.
+ if (this_update > last_update + 2000) {
+ PW_LOG_ERROR("FPGA Config failed.");
+ last_update = this_update;
+ return pw::Status::Aborted();
+ }
+ }
+
+ // Send 50 more clock pulses to release release the SPI bus to user control.
+ for (int i = 0; i < 50; i++) {
+ digitalWrite(FlashCLK, LOW);
+ delayMicroseconds(200);
+ digitalWrite(FlashCLK, HIGH);
+ delayMicroseconds(200);
+ }
+
+ return pw::OkStatus();
+}
+
+} // namespace gonk::fpga_control
diff --git a/lib/fpga_control/public/gonk/fpga_control.h b/lib/fpga_control/public/gonk/fpga_control.h
new file mode 100644
index 0000000..3aa35f6
--- /dev/null
+++ b/lib/fpga_control/public/gonk/fpga_control.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <stdint.h>
+
+#include "gonk/pin_config.h"
+#include "pw_result/result.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+
+using gonk::pin_config::PinConfig;
+using pw::span;
+using pw::Status;
+
+namespace gonk::fpga_control {
+
+const int BitstreamFileLength = 135100;
+
+class FpgaControl {
+public:
+ FpgaControl(PinConfig *pin_config);
+
+ Status StartConfig();
+ Status FpgaConfigFromSerial();
+ Status WaitForBitstreamStart();
+ Status ReadBitstream();
+ Status VerifyBitstream();
+ Status SendBitstreamToFpga();
+
+private:
+ PinConfig *pin_config_;
+ int bitstream_file_position = 0;
+ char bitstream_file[BitstreamFileLength];
+ uint8_t read_buffer[4] = {0, 0, 0, 0};
+ int bytes_written = 0;
+};
+
+} // namespace gonk::fpga_control
diff --git a/lib/pin_config/pin_config.cc b/lib/pin_config/pin_config.cc
index 61719e0..5489b52 100644
--- a/lib/pin_config/pin_config.cc
+++ b/lib/pin_config/pin_config.cc
@@ -35,6 +35,7 @@
// Set FlashCS to signal the FPGA it should load the bitstream from external
// flash.
pinMode(FlashCS, INPUT_FLOATING);
+ delay(10);
// Hold in reset for a small amount of time.
digitalWrite(ICE40ResetN, LOW);
@@ -48,6 +49,41 @@
digitalWrite(ICE40ResetN, LOW);
}
+void PinConfig::FpgaSpiConfigMode() {
+ // Turn on Flash Hold so it ignores any SPI communication.
+ pinMode(FlashHold, OUTPUT);
+ pinMode(FlashWP, OUTPUT);
+ digitalWrite(FlashHold, LOW);
+ digitalWrite(FlashWP, LOW);
+ delay(1);
+
+ // Set FlashCS (ICE_SPI_SS) low and pulse ICE40ResetN. This signals the FPGA
+ // to boot into SPI config mode.
+ pinMode(ICE40ResetN, OUTPUT);
+ pinMode(FlashCS, OUTPUT);
+
+ // Pulse ICE40ResetN
+ digitalWrite(ICE40ResetN, LOW);
+ digitalWrite(FlashCS, LOW);
+ delay(10);
+ digitalWrite(ICE40ResetN, HIGH);
+
+ // Enalble GPIO control of MOSI, MISO and CLK.
+ pinMode(FlashMOSI, OUTPUT);
+ pinMode(FlashMISO, OUTPUT);
+ pinMode(FlashCLK, OUTPUT);
+ delay(1);
+
+ // Send 8 clock pulses.
+ for (int i = 0; i < 8; i++) {
+ digitalWrite(FlashCLK, LOW);
+ delayMicroseconds(100);
+ digitalWrite(FlashCLK, HIGH);
+ delayMicroseconds(100);
+ }
+ delay(10);
+}
+
void PinConfig::FpgaEnable() { digitalWrite(ICE40ResetN, HIGH); }
void PinConfig::SPIEnable() {
@@ -60,6 +96,23 @@
// Set FlashCS as output with initial state.
pinMode(FlashCS, OUTPUT);
digitalWrite(FlashCS, HIGH);
+
+ pinMode(FlashCLK, OUTPUT);
+ pinMode(FlashMOSI, OUTPUT);
+ pinMode(FlashMISO, INPUT_FLOATING);
+}
+
+void PinConfig::SPIDisable() {
+ SPI.end();
+
+ pinMode(FlashCS, OUTPUT);
+ pinMode(FlashCLK, OUTPUT);
+ pinMode(FlashMOSI, OUTPUT);
+ pinMode(FlashMISO, INPUT_FLOATING);
+
+ digitalWrite(FlashCS, HIGH);
+ digitalWrite(FlashCLK, HIGH);
+ digitalWrite(FlashMOSI, HIGH);
}
} // namespace gonk::pin_config
diff --git a/lib/pin_config/public/gonk/pin_config.h b/lib/pin_config/public/gonk/pin_config.h
index 3f6d72a..06c541f 100644
--- a/lib/pin_config/public/gonk/pin_config.h
+++ b/lib/pin_config/public/gonk/pin_config.h
@@ -27,8 +27,10 @@
// Halt the FPGA with the SPI bus released so SPI flash is accessible from the
// MCU.
void FpgaHalt();
+ void FpgaSpiConfigMode();
void FpgaEnable();
void SPIEnable();
+ void SPIDisable();
private:
};
diff --git a/tools/BUILD.gn b/tools/BUILD.gn
index 90dc3d4..41bb95a 100644
--- a/tools/BUILD.gn
+++ b/tools/BUILD.gn
@@ -26,6 +26,7 @@
"gonk_tools/build_project.py",
"gonk_tools/find_serial_port.py",
"gonk_tools/presubmit_checks.py",
+ "gonk_tools/write_fpga.py",
]
python_deps = [
"$dir_pw_arduino_build/py",
diff --git a/tools/gonk_tools/write_fpga.py b/tools/gonk_tools/write_fpga.py
new file mode 100644
index 0000000..a3852f0
--- /dev/null
+++ b/tools/gonk_tools/write_fpga.py
@@ -0,0 +1,177 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Write a binary over serial to provision the Gonk FPGA."""
+
+import argparse
+from itertools import islice
+import operator
+from pathlib import Path
+import sys
+import time
+from typing import Optional
+
+from serial import Serial
+from serial.tools.list_ports import comports
+from serial.tools.miniterm import Miniterm
+
+
+def _parse_args():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '-p',
+ '--port',
+ type=str,
+ help='Serial port path.',
+ )
+ parser.add_argument(
+ '-b',
+ '--baudrate',
+ type=int,
+ default=1000000,
+ help='Sets the baudrate for serial communication.',
+ )
+ parser.add_argument(
+ '--product',
+ default='GENERIC_F730R8TX',
+ help='Use first serial port matching this product name.',
+ )
+ parser.add_argument(
+ '--serial-number',
+ help='Use the first serial port matching this number.',
+ )
+ parser.add_argument(
+ 'bitstream_file',
+ type=Path,
+ help='FPGA Bitstream file.',
+ )
+ return parser.parse_args()
+
+
+def get_serial_port(
+ product: Optional[str] = None,
+ serial_number: Optional[str] = None,
+) -> str:
+
+ ports = sorted(comports(), key=operator.attrgetter('device'))
+
+ # Print matching devices
+ for port in ports:
+ if (product is not None and port.product is not None
+ and product in port.product):
+ return port.device
+
+ if (serial_number is not None and port.serial_number is not None
+ and serial_number in port.serial_number):
+ return port.device
+
+ return ''
+
+
+FILE_LENGTH = 135100
+FILE_START_STR = 'FF 00 00 FF'
+SYNC_START_STR = '7E AA 99 7E'
+FILE_START_BYTES = bytes.fromhex(FILE_START_STR)
+SYNC_START_BYTES = bytes.fromhex(SYNC_START_STR)
+
+
+class IncorrectBinaryFormat(Exception):
+ """Exception raised when FPGA bitstream file is in an unexpected format."""
+
+
+def batched(iterable, n):
+ """Batch data into tuples of length n. The last batch may be shorter.
+
+ Example usage:
+
+ .. code-block:: pycon
+
+ >>> list(batched('ABCDEFG', 3))
+ ['ABC', 'DEF', 'G']
+ """
+ if n < 1:
+ raise ValueError('n must be at least one')
+ it = iter(iterable)
+ while batch := tuple(islice(it, n)):
+ yield batch
+
+
+def main(
+ baudrate: int,
+ bitstream_file: Path,
+ port: Optional[str] = None,
+ product: Optional[str] = None,
+ serial_number: Optional[str] = None,
+) -> int:
+ """Write a bitstream file over serial while monitoring output."""
+
+ # Check for valid bitstream file.
+ if not bitstream_file.is_file():
+ raise FileNotFoundError(f'\nUnable to load "{bitstream_file}"')
+
+ bitstream_bytes = bitstream_file.read_bytes()
+ if (len(bitstream_bytes) != 135100
+ or bitstream_bytes[0:8] != FILE_START_BYTES + SYNC_START_BYTES):
+ raise IncorrectBinaryFormat(
+ f'the bitstream file must be:\n'
+ f' {FILE_LENGTH} bytes in length\n'
+ f' Start with "{FILE_START_STR} {SYNC_START_STR}"')
+
+ # Init serial port.
+ if port is None:
+ port = get_serial_port(product=product, serial_number=serial_number)
+
+ serial_instance = Serial(port=port, baudrate=baudrate, timeout=320)
+
+ # Use pyserial miniterm to monitor recieved data.
+ miniterm = Miniterm(
+ serial_instance,
+ echo=False,
+ eol='crlf',
+ filters=(),
+ )
+ # Use Ctrl-C as the exit character. (Miniterm default is Ctrl-])
+ miniterm.exit_character = chr(0x03)
+ miniterm.set_rx_encoding('utf-8')
+ miniterm.set_tx_encoding('utf-8')
+
+ # Start monitoring serial data.
+ miniterm.start()
+
+ # Wait a couple seconds to print early log messages from Gonk.
+ time.sleep(2)
+
+ # Write out the bitstream in batches.
+ print('Sending bitstream...')
+ written_bytes: int = 0
+ for byte_batch in batched(bitstream_bytes, 8):
+ result = serial_instance.write(byte_batch)
+ if result:
+ written_bytes += result
+ serial_instance.flush()
+ print(f'Done sending bitstream. Wrote {written_bytes}')
+
+ # Wait for ctrl-c, then shutdown miniterm.
+ try:
+ miniterm.join(True)
+ except KeyboardInterrupt:
+ pass
+ sys.stderr.write('\n--- exit ---\n')
+ miniterm.join()
+ miniterm.close()
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(**vars(_parse_args())))