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())))