Clean up printing in MCU code and add new options
We now have four different ways of getting debug prints off of boards,
with varying tradeoffs. Split out the printing into dedicated libraries
that are easy to switch between to avoid duplicating even more code, and
also make using the new options easy.
USB is handy for testing the code on a Teensy.
Semihosting is nice in theory, but in practice it's super slow and
messes up the code's timing.
ITM works well, as long as you have a debugger attached.
Serial also works pretty well, but it means having another cable.
Change-Id: I7af5099d421c33f0324aeca92b46732e341848d4
diff --git a/motors/print/BUILD b/motors/print/BUILD
new file mode 100644
index 0000000..26ee976
--- /dev/null
+++ b/motors/print/BUILD
@@ -0,0 +1,81 @@
+load("//tools:environments.bzl", "mcu_cpus")
+
+cc_library(
+ name = "print",
+ hdrs = [
+ "print.h",
+ ],
+ restricted_to = mcu_cpus,
+ visibility = ["//visibility:public"],
+ deps = [
+ "//motors/core",
+ "//third_party/GSL",
+ ],
+)
+
+cc_library(
+ name = "uart",
+ srcs = [
+ "uart.cc",
+ ],
+ hdrs = [
+ "uart.h",
+ ],
+ restricted_to = mcu_cpus,
+ visibility = ["//visibility:public"],
+ deps = [
+ ":print",
+ "//motors/core",
+ "//motors/peripheral:uart",
+ ],
+)
+
+cc_library(
+ name = "itm",
+ srcs = [
+ "itm.cc",
+ ],
+ hdrs = [
+ "itm.h",
+ ],
+ restricted_to = mcu_cpus,
+ visibility = ["//visibility:public"],
+ deps = [
+ ":print",
+ "//motors/core:itm",
+ ],
+)
+
+cc_library(
+ name = "semihosting",
+ srcs = [
+ "semihosting.cc",
+ ],
+ hdrs = [
+ "semihosting.h",
+ ],
+ restricted_to = mcu_cpus,
+ visibility = ["//visibility:public"],
+ deps = [
+ ":print",
+ "//motors/core:semihosting",
+ ],
+)
+
+cc_library(
+ name = "usb",
+ srcs = [
+ "usb.cc",
+ ],
+ hdrs = [
+ "usb.h",
+ ],
+ restricted_to = mcu_cpus,
+ visibility = ["//visibility:public"],
+ deps = [
+ ":print",
+ "//motors/core",
+ "//motors/usb",
+ "//motors/usb:cdc",
+ ],
+)
diff --git a/motors/print/itm.cc b/motors/print/itm.cc
new file mode 100644
index 0000000..0e3c38c
--- /dev/null
+++ b/motors/print/itm.cc
@@ -0,0 +1,95 @@
+#include "motors/print/itm.h"
+
+#include "motors/core/itm.h"
+
+namespace frc971 {
+namespace motors {
+namespace {
+
+template<int kPort> void WriteToPort(gsl::span<const char> buffer) {
+ // This ignores memory barriers etc, because it will be called by
+ // CreatePrinting which must be called before any interrupts are enabled. That
+ // means the only thing we need to worry about is actually getting it
+ // initialized with a minimal number of cycles.
+ static bool is_initialized = false;
+ if (__builtin_expect(!is_initialized, false)) {
+ is_initialized = true;
+ itm::Initialize();
+ }
+
+ const char *next_address = buffer.data();
+ int remaining_bytes = buffer.size();
+
+ // Write small chunks to make the address even.
+ if (remaining_bytes >= 1 && (reinterpret_cast<uintptr_t>(next_address) & 1)) {
+ uint8_t value;
+ memcpy(&value, next_address, 1);
+ itm::Write8(kPort, value);
+ next_address += 1;
+ remaining_bytes -= 1;
+ }
+ if (remaining_bytes >= 2 && (reinterpret_cast<uintptr_t>(next_address) & 2)) {
+ uint16_t value;
+ memcpy(&value, next_address, 2);
+ itm::Write16(kPort, value);
+ next_address += 2;
+ remaining_bytes -= 2;
+ }
+
+ // Write big chunks while we can.
+ while (remaining_bytes >= 4) {
+ uint32_t value;
+ memcpy(&value, next_address, 4);
+ itm::Write32(kPort, value);
+ next_address += 4;
+ remaining_bytes -= 4;
+ }
+
+ // Write out any remaining uneven bytes on the end.
+ if (remaining_bytes >= 2) {
+ uint16_t value;
+ memcpy(&value, next_address, 2);
+ itm::Write16(kPort, value);
+ next_address += 2;
+ remaining_bytes -= 2;
+ }
+ if (remaining_bytes >= 1) {
+ uint8_t value;
+ memcpy(&value, next_address, 1);
+ itm::Write8(kPort, value);
+ next_address += 1;
+ remaining_bytes -= 1;
+ }
+}
+
+} // namespace
+
+::std::unique_ptr<PrintingImplementation> CreatePrinting(
+ const PrintingParameters & /*parameters*/) {
+ return ::std::unique_ptr<PrintingImplementation>(new ItmPrinting());
+}
+
+extern "C" int _write(const int /*file*/, char *const ptr, const int len) {
+ WriteToPort<0>(gsl::span<const char>(ptr, len));
+ return len;
+}
+
+ItmPrinting::ItmPrinting() {
+ // Make sure we run the one-time initialization. It's important to do it here
+ // to ensure it's complete before interrupts are enabled, because it's not
+ // interrupt-safe.
+ _write(0, nullptr, 0);
+}
+
+int ItmPrinting::WriteStdout(gsl::span<const char> buffer) {
+ WriteToPort<0>(buffer);
+ return buffer.size();
+}
+
+int ItmPrinting::WriteDebug(gsl::span<const char> buffer) {
+ WriteToPort<1>(buffer);
+ return buffer.size();
+}
+
+} // namespace motors
+} // namespace frc971
diff --git a/motors/print/itm.h b/motors/print/itm.h
new file mode 100644
index 0000000..99f9970
--- /dev/null
+++ b/motors/print/itm.h
@@ -0,0 +1,32 @@
+#ifndef MOTORS_PRINT_ITM_
+#define MOTORS_PRINT_ITM_
+
+#include "motors/print/print.h"
+
+namespace frc971 {
+namespace motors {
+
+// A printing implementation via the SWO (trace output) pin. This requires an
+// attached debugger which is in SWD (Single Wire Debug) mode, has the SWO
+// (also known as JTAG_TDO) pin hooked up, and software support.
+//
+// To decode the output from this, use motors/print/itm_read.py.
+// To configure openocd to feed data to that:
+// tpiu config internal /tmp/itm.fifo uart off 120000000 64000
+class ItmPrinting final : public PrintingImplementation {
+ public:
+ ItmPrinting();
+ ~ItmPrinting() override = default;
+
+ void Initialize() override {}
+
+ // This goes to stimulus port 0.
+ int WriteStdout(gsl::span<const char> buffer) override;
+ // This goes to stimulus port 1.
+ int WriteDebug(gsl::span<const char> buffer) override;
+};
+
+} // namespace motors
+} // namespace frc971
+
+#endif // MOTORS_PRINT_ITM_
diff --git a/motors/print/itm_read.py b/motors/print/itm_read.py
new file mode 100755
index 0000000..d616da4
--- /dev/null
+++ b/motors/print/itm_read.py
@@ -0,0 +1,107 @@
+#!/usr/bin/python3
+
+# This is a program to parse output from the ITM and DWT.
+# The "Debug ITM and DWT Packet Protocol" section of the ARMv7-M Architecture
+# Reference Manual is a good reference.
+#
+# This seems like it might be a poster child for using coroutines, but those
+# look scary so we're going to stick with generators.
+
+import io
+import os
+import sys
+
+def open_file_for_bytes(path):
+ '''Returns a file-like object which reads bytes without buffering.'''
+ # Not using `open` because it's unclear from the docs how (if it's possible at
+ # all) to get something that will only do one read call and return what that
+ # gets on a fifo.
+ try:
+ return io.FileIO(path, 'r')
+ except FileNotFoundError:
+ # If it wasn't found, try (once) to create it and then open again.
+ try:
+ os.mkfifo(path)
+ except FileExistsError:
+ pass
+ return io.FileIO(path, 'r')
+
+def read_bytes(path):
+ '''Reads bytes from a file. This is appropriate both for regular files and
+ fifos.
+ Args:
+ path: A path-like object to open.
+ Yields:
+ Individual bytes from the file, until hitting EOF.
+ '''
+ with open_file_for_bytes(path) as f:
+ while True:
+ buf = f.read(1024)
+ if not buf:
+ return
+ for byte in buf:
+ yield byte
+
+def parse_packets(source):
+ '''Parses a stream of bytes into packets.
+ Args:
+ source: A generator of individual bytes.
+ Generates:
+ Packets as bytes objects.
+ '''
+ try:
+ while True:
+ header = next(source)
+ if header == 0:
+ # Synchronization packets consist of a bunch of 0 bits (not necessarily
+ # a whole number of bytes), followed by a 128 byte. This is for hardware
+ # to synchronize on, but we're not in a position to do that, so
+ # presumably those should get filtered out before getting here?
+ raise 'Not sure how to handle synchronization packets'
+ packet = bytearray()
+ packet.append(header)
+ header_size = header & 3
+ if header_size == 0:
+ while packet[-1] & 128 and len(packet) < 7:
+ packet.append(next(source))
+ else:
+ if header_size == 3:
+ header_size = 4
+ for _ in range(header_size):
+ packet.append(next(source))
+ yield bytes(packet)
+ except StopIteration:
+ return
+
+class PacketParser(object):
+ def __init__(self):
+ self.stimulus_handlers = {}
+
+ def register_stimulus_handler(self, port_number, handler):
+ '''Registers a function to call on packets to the specified port.'''
+ self.stimulus_handlers[port_number] = handler
+
+ def process(self, path):
+ for packet in parse_packets(read_bytes(path)):
+ header = packet[0]
+ header_size = header & 3
+ if header_size == 0:
+ # TODO(Brian): At least handle overflow packets here.
+ pass
+ else:
+ port_number = header >> 3
+ if port_number in self.stimulus_handlers:
+ self.stimulus_handlers[port_number](packet[1:])
+ else:
+ print('Warning: unhandled stimulus port %d' % port_number,
+ file=sys.stderr)
+ self.stimulus_handlers[port_number] = lambda _: None
+
+if __name__ == '__main__':
+ parser = PacketParser()
+ def print_byte(payload):
+ sys.stdout.write(payload.decode('ascii'))
+ parser.register_stimulus_handler(0, print_byte)
+
+ for path in sys.argv[1:]:
+ parser.process(path)
diff --git a/motors/print/print.h b/motors/print/print.h
new file mode 100644
index 0000000..d51a2c3
--- /dev/null
+++ b/motors/print/print.h
@@ -0,0 +1,93 @@
+#ifndef MOTORS_PRINT_PRINT_H_
+#define MOTORS_PRINT_PRINT_H_
+
+#include <memory>
+
+#include "motors/core/kinetis.h"
+#include "third_party/GSL/include/gsl/gsl"
+
+namespace frc971 {
+namespace teensy {
+
+class AcmTty;
+
+} // namespace teensy
+namespace motors {
+
+class PrintingImplementation {
+ public:
+ PrintingImplementation() = default;
+ virtual ~PrintingImplementation() = default;
+
+ PrintingImplementation(const PrintingImplementation &) = delete;
+ PrintingImplementation &operator=(const PrintingImplementation &) = delete;
+
+ virtual void Initialize() = 0;
+
+ // Writes something directly to stdout/stderr (they are treated as the same).
+ virtual int WriteStdout(gsl::span<const char> buffer) = 0;
+ // Writes something to a separate debug stream. Some implementations will
+ // always ignore this, and others will ignore it under some conditions.
+ virtual int WriteDebug(gsl::span<const char> buffer) { return buffer.size(); }
+};
+
+// A trivial printing "implementation" which simply does nothing. This is used
+// when a real implementation can't be created by CreatePrinting due to missing
+// parameters.
+class NopPrinting : public PrintingImplementation {
+ public:
+ NopPrinting() = default;
+ ~NopPrinting() override = default;
+
+ void Initialize() override {}
+ int WriteStdout(gsl::span<const char> buffer) override {
+ return buffer.size();
+ }
+};
+
+// Contains various parameters for controlling how the printing implementation
+// is initialized. Some of these are optional depending on which implementation
+// is selected, while others being missing will result in some implementations
+// turning into NOPs.
+struct PrintingParameters {
+ // This module must have its clock enabled and pinmuxing set up before calling
+ // CreatePrinting.
+ KINETISK_UART_t *stdout_uart_module = nullptr;
+ int stdout_uart_module_clock_frequency = 0;
+ int stdout_uart_baud_rate = 115200;
+ int stdout_uart_status_interrupt = -1;
+
+ // Setting this to true indicates the implementation should manage its own
+ // UsbDevice. If there are other USB functions around, set stdout_tty (and
+ // optionally debug_tty) instead.
+ bool dedicated_usb = false;
+
+ // If these are used, Initialize() must be called on the UsbDevice before the
+ // PrintingImplementation.
+ teensy::AcmTty *stdout_tty = nullptr;
+ teensy::AcmTty *debug_tty = nullptr;
+};
+
+// Creates an implementation of the linked-in type. Exactly one printing
+// implementation must be linked in. If all the necessary parameters aren't
+// filled out, this will return a NopPrinting instance.
+//
+// Some implementations will work even before calling this, or calling
+// Initialize() on the result. Others do require calling this before they will
+// work. This must be called before enabling any interrupts or some
+// implementations may deadlock.
+//
+// This should only be called once per program lifetime. Many implementations
+// manage global resources in the returned object. The resulting object may be
+// destroyed, but not while interrupts might be running. Destroying the object
+// may or may not stop printing.
+//
+// This will not enable any interrupts. When applicable, that is deferred until
+// Initialize() is called on the result.
+::std::unique_ptr<PrintingImplementation> CreatePrinting(
+ const PrintingParameters ¶meters);
+
+} // namespace motors
+} // namespace frc971
+
+#endif // MOTORS_PRINT_PRINT_H_
diff --git a/motors/print/semihosting.cc b/motors/print/semihosting.cc
new file mode 100644
index 0000000..799b928
--- /dev/null
+++ b/motors/print/semihosting.cc
@@ -0,0 +1,24 @@
+#include "motors/print/semihosting.h"
+
+#include "motors/core/semihosting.h"
+
+namespace frc971 {
+namespace motors {
+
+::std::unique_ptr<PrintingImplementation> CreatePrinting(
+ const PrintingParameters & /*parameters*/) {
+ return ::std::unique_ptr<PrintingImplementation>(new SemihostingPrinting());
+}
+
+extern "C" int _write(const int /*file*/, char *const ptr, const int len) {
+ semihosting::Write operation{2 /* stderr */, gsl::span<const char>(ptr, len)};
+ return len - operation.Execute();
+}
+
+int SemihostingPrinting::WriteStdout(gsl::span<const char> buffer) {
+ semihosting::Write operation{2 /* stderr */, buffer};
+ return buffer.size() - operation.Execute();
+}
+
+} // namespace motors
+} // namespace frc971
diff --git a/motors/print/semihosting.h b/motors/print/semihosting.h
new file mode 100644
index 0000000..6ea6c3b
--- /dev/null
+++ b/motors/print/semihosting.h
@@ -0,0 +1,35 @@
+#ifndef MOTORS_PRINT_SEMIHOSTING_H_
+#define MOTORS_PRINT_SEMIHOSTING_H_
+
+#include "motors/print/print.h"
+
+namespace frc971 {
+namespace motors {
+
+// A printing implementation which uses the ARM semihosting interface. This
+// requries an attached debugger with software support.
+//
+// You have to do "arm semihosting enable" in openocd to enable this.
+// It also seems to be broken with the usb-tiny-h in the openocd version we're
+// using, but works fine with the st-link-v2.
+// It may also only work if you do this immediately after starting openocd.
+//
+// Note that this implementation has strange effects on timing even of
+// interrupts-disabled code and is in general extremely slow.
+class SemihostingPrinting final : public PrintingImplementation {
+ public:
+ SemihostingPrinting() = default;
+ ~SemihostingPrinting() override = default;
+
+ void Initialize() override {}
+
+ int WriteStdout(gsl::span<const char> buffer) override;
+
+ // Could easily implement an optional WriteDebug which goes to a separate
+ // file if the name is filled out in the parameters.
+};
+
+} // namespace motors
+} // namespace frc971
+
+#endif // MOTORS_PRINT_SEMIHOSTING_H_
diff --git a/motors/print/uart.cc b/motors/print/uart.cc
new file mode 100644
index 0000000..84b33cd
--- /dev/null
+++ b/motors/print/uart.cc
@@ -0,0 +1,69 @@
+#include "motors/print/uart.h"
+
+#include "motors/core/kinetis.h"
+
+#include <atomic>
+
+namespace frc971 {
+namespace motors {
+namespace {
+
+::std::atomic<teensy::InterruptBufferedUart *> global_stdout{nullptr};
+
+} // namespace
+
+::std::unique_ptr<PrintingImplementation> CreatePrinting(
+ const PrintingParameters ¶meters) {
+ if (parameters.stdout_uart_module == nullptr) {
+ return ::std::unique_ptr<PrintingImplementation>(new NopPrinting());
+ }
+ if (parameters.stdout_uart_module_clock_frequency == 0) {
+ return ::std::unique_ptr<PrintingImplementation>(new NopPrinting());
+ }
+ if (parameters.stdout_uart_status_interrupt < 0) {
+ return ::std::unique_ptr<PrintingImplementation>(new NopPrinting());
+ }
+ return ::std::unique_ptr<PrintingImplementation>(
+ new UartPrinting(parameters));
+}
+
+extern "C" void uart0_status_isr(void) {
+ teensy::InterruptBufferedUart *const tty =
+ global_stdout.load(::std::memory_order_relaxed);
+ DisableInterrupts disable_interrupts;
+ tty->HandleInterrupt(disable_interrupts);
+}
+
+UartPrinting::UartPrinting(const PrintingParameters ¶meters)
+ : stdout_uart_{parameters.stdout_uart_module,
+ parameters.stdout_uart_module_clock_frequency},
+ stdout_status_interrupt_(parameters.stdout_uart_status_interrupt) {
+ stdout_uart_.Initialize(parameters.stdout_uart_baud_rate);
+}
+
+UartPrinting::~UartPrinting() {
+ NVIC_DISABLE_IRQ(stdout_status_interrupt_);
+ global_stdout.store(nullptr, ::std::memory_order_release);
+}
+
+void UartPrinting::Initialize() {
+ global_stdout.store(&stdout_uart_, ::std::memory_order_release);
+ NVIC_ENABLE_IRQ(stdout_status_interrupt_);
+}
+
+int UartPrinting::WriteStdout(gsl::span<const char> buffer) {
+ stdout_uart_.Write(buffer);
+ return buffer.size();
+}
+
+extern "C" int _write(const int /*file*/, char *const ptr, const int len) {
+ teensy::InterruptBufferedUart *const tty =
+ global_stdout.load(::std::memory_order_acquire);
+ if (tty != nullptr) {
+ tty->Write(gsl::make_span(ptr, len));
+ }
+ return len;
+}
+
+} // namespace motors
+} // namespace frc971
diff --git a/motors/print/uart.h b/motors/print/uart.h
new file mode 100644
index 0000000..f4a61c8
--- /dev/null
+++ b/motors/print/uart.h
@@ -0,0 +1,34 @@
+#ifndef MOTORS_PRINT_UART_H_
+#define MOTORS_PRINT_UART_H_
+
+#include "motors/peripheral/uart.h"
+#include "motors/print/print.h"
+
+namespace frc971 {
+namespace motors {
+
+// A printing implementation using a hardware UART. This has a reasonably sized
+// buffer in memory and uses interrupts to keep the hardware busy. It could
+// support DMA too in the future.
+class UartPrinting : public PrintingImplementation {
+ public:
+ // All required parameters must be filled out.
+ UartPrinting(const PrintingParameters ¶meters);
+ ~UartPrinting() override;
+
+ void Initialize() override;
+
+ int WriteStdout(gsl::span<const char> buffer) override;
+
+ private:
+ teensy::InterruptBufferedUart stdout_uart_;
+ const int stdout_status_interrupt_;
+};
+
+// Could easily create a subclass of UartPrinting that also implements
+// WriteDebug on a second UART, and conditionally instantiate that.
+
+} // namespace motors
+} // namespace frc971
+
+#endif // MOTORS_PRINT_UART_H_
diff --git a/motors/print/usb.cc b/motors/print/usb.cc
new file mode 100644
index 0000000..9284ecd
--- /dev/null
+++ b/motors/print/usb.cc
@@ -0,0 +1,66 @@
+#include "motors/print/usb.h"
+
+#include <atomic>
+
+#include "motors/core/kinetis.h"
+
+namespace frc971 {
+namespace motors {
+namespace {
+
+::std::atomic<teensy::AcmTty *> global_stdout{nullptr};
+
+} // namespace
+
+::std::unique_ptr<PrintingImplementation> CreatePrinting(
+ const PrintingParameters ¶meters) {
+ if (parameters.dedicated_usb) {
+ return ::std::unique_ptr<PrintingImplementation>(
+ new DedicatedUsbPrinting());
+ }
+ if (parameters.stdout_tty != nullptr) {
+ return ::std::unique_ptr<PrintingImplementation>(
+ new UsbPrinting(parameters.stdout_tty, parameters.debug_tty));
+ }
+ return ::std::unique_ptr<PrintingImplementation>(new NopPrinting());
+}
+
+extern "C" int _write(const int /*file*/, char *const ptr, const int len) {
+ teensy::AcmTty *const tty = global_stdout.load(::std::memory_order_acquire);
+ if (tty != nullptr) {
+ return tty->Write(ptr, len);
+ }
+ return len;
+}
+
+UsbPrinting::UsbPrinting(teensy::AcmTty *stdout_tty, teensy::AcmTty *debug_tty)
+ : stdout_tty_(stdout_tty), debug_tty_(debug_tty) {}
+
+UsbPrinting::~UsbPrinting() {
+ global_stdout.store(nullptr, ::std::memory_order_release);
+}
+
+void UsbPrinting::Initialize() {
+ global_stdout.store(stdout_tty_, ::std::memory_order_release);
+}
+
+DedicatedUsbPrinting::DedicatedUsbPrinting()
+ : usb_device_{0, 0x16c0, 0x0490},
+ stdout_tty_{&usb_device_},
+ debug_tty_{&usb_device_} {
+ usb_device_.SetManufacturer("FRC 971 Spartan Robotics");
+ usb_device_.SetProduct("FET12v2");
+ NVIC_SET_SANE_PRIORITY(IRQ_USBOTG, 0x7);
+}
+
+DedicatedUsbPrinting::~DedicatedUsbPrinting() {
+ global_stdout.store(nullptr, ::std::memory_order_release);
+}
+
+void DedicatedUsbPrinting::Initialize() {
+ usb_device_.Initialize();
+ global_stdout.store(&stdout_tty_, ::std::memory_order_release);
+}
+
+} // namespace motors
+} // namespace frc971
diff --git a/motors/print/usb.h b/motors/print/usb.h
new file mode 100644
index 0000000..7e63184
--- /dev/null
+++ b/motors/print/usb.h
@@ -0,0 +1,62 @@
+#ifndef MOTORS_PRINT_USB_H_
+#define MOTORS_PRINT_USB_H_
+
+#include "motors/print/print.h"
+#include "motors/usb/cdc.h"
+#include "motors/usb/usb.h"
+
+namespace frc971 {
+namespace motors {
+
+// A printing implementation which uses externally-created functions. The stdout
+// one is required, while the debug one is optional.
+class UsbPrinting final : public PrintingImplementation {
+ public:
+ UsbPrinting(teensy::AcmTty *stdout_tty, teensy::AcmTty *debug_tty);
+ ~UsbPrinting() override;
+
+ void Initialize() override;
+
+ int WriteStdout(gsl::span<const char> buffer) override {
+ return stdout_tty_->Write(buffer.data(), buffer.size());
+ }
+
+ int WriteDebug(gsl::span<const char> buffer) override {
+ if (debug_tty_ == nullptr) {
+ return buffer.size();
+ }
+ return debug_tty_->Write(buffer.data(), buffer.size());
+ }
+
+ private:
+ teensy::AcmTty *const stdout_tty_;
+ teensy::AcmTty *const debug_tty_;
+};
+
+// A printing implementation which creates its own UsbDevice and functions, and
+// manages their lifecycle.
+class DedicatedUsbPrinting final : public PrintingImplementation {
+ public:
+ DedicatedUsbPrinting();
+ ~DedicatedUsbPrinting() override;
+
+ void Initialize() override;
+
+ int WriteStdout(gsl::span<const char> buffer) override {
+ return stdout_tty_.Write(buffer.data(), buffer.size());
+ }
+
+ int WriteDebug(gsl::span<const char> buffer) override {
+ return debug_tty_.Write(buffer.data(), buffer.size());
+ }
+
+ private:
+ teensy::UsbDevice usb_device_;
+ teensy::AcmTty stdout_tty_;
+ teensy::AcmTty debug_tty_;
+};
+
+} // namespace motors
+} // namespace frc971
+
+#endif // MOTORS_PRINT_USB_H_