Add a working CDC TTY USB device
I'm pretty sure it reliably moves data in both directions, but it has a
lot of TODOs left wrt error handling.
Change-Id: I1d96a63eea7bdc08f6ec78c868c4a1ccbfbda600
diff --git a/motors/pistol_grip/BUILD b/motors/pistol_grip/BUILD
index d83b284..364801b 100644
--- a/motors/pistol_grip/BUILD
+++ b/motors/pistol_grip/BUILD
@@ -9,6 +9,7 @@
deps = [
'//motors:util',
'//motors/usb',
+ '//motors/usb:cdc',
'//motors/core',
],
restricted_to = mcu_cpus,
diff --git a/motors/pistol_grip/drivers_station.cc b/motors/pistol_grip/drivers_station.cc
index 370483e..8632a8d 100644
--- a/motors/pistol_grip/drivers_station.cc
+++ b/motors/pistol_grip/drivers_station.cc
@@ -6,10 +6,94 @@
#include "motors/core/time.h"
#include "motors/core/kinetis.h"
#include "motors/usb/usb.h"
+#include "motors/usb/cdc.h"
#include "motors/util.h"
namespace frc971 {
namespace motors {
+namespace {
+
+void EchoChunks(teensy::AcmTty *tty1) {
+ while (true) {
+ char buffer[512];
+ size_t buffered = 0;
+ while (buffered < sizeof(buffer)) {
+ const size_t chunk =
+ tty1->Read(&buffer[buffered], sizeof(buffer) - buffered);
+ buffered += chunk;
+ }
+ size_t written = 0;
+ while (written < buffered) {
+ const size_t chunk = tty1->Write(&buffer[written], buffered - written);
+ written += chunk;
+ }
+
+ GPIOC_PTOR = 1 << 5;
+ for (int i = 0; i < 100000000; ++i) {
+ GPIOC_PSOR = 0;
+ }
+ GPIOC_PTOR = 1 << 5;
+ }
+}
+
+void EchoImmediately(teensy::AcmTty *tty1) {
+ while (true) {
+ if (false) {
+ // Delay for a while.
+ for (int i = 0; i < 100000000; ++i) {
+ GPIOC_PSOR = 0;
+ }
+ }
+
+ char buffer[64];
+ const size_t chunk = tty1->Read(buffer, sizeof(buffer));
+ size_t written = 0;
+ while (written < chunk) {
+ written += tty1->Write(&buffer[written], chunk - written);
+ }
+ }
+}
+
+void WriteData(teensy::AcmTty *tty1) {
+ GPIOC_PTOR = 1 << 5;
+ for (int i = 0; i < 100000000; ++i) {
+ GPIOC_PSOR = 0;
+ }
+ GPIOC_PTOR = 1 << 5;
+ for (int i = 0; i < 100000000; ++i) {
+ GPIOC_PSOR = 0;
+ }
+
+ const char data[] =
+ "Running command lineRunning command lineRunning command lineRunning "
+ "command lineRunning command lineRunning command lineRunning command "
+ "lineRunning command lineRunning command lineRunning command lineRunning "
+ "command lineRunning command lineRunning command lineRunning command "
+ "lineRunning command lineRunning command lineRunning command lineRunning "
+ "command lineRunning command lineRunning command lineRunning command "
+ "lineRunning command lineRunning command lineRunning command lineRunning "
+ "command lineRunning command lineRunning command lineRunning command "
+ "lineRunning command lineRunning command lineRunning command lineRunning "
+ "command lineRunning command lineRunning command lineRunning command "
+ "lineRunning command lineRunning command lineRunning command lineRunning "
+ "command lineRunning command lineRunning command lineRunning command "
+ "lineRunning command lineRunning command lineRunning command lineRunning "
+ "command lineRunning command lineRunning command lineRunning command "
+ "lineRunning command lineRunning command lineRunning command lineRunning "
+ "command lineRunning command lineRunning command lineRunning command "
+ "lineRunning command lineRunning command lineRunning command lineRunning "
+ "command lineRunning command lineRunning command lineRunning command "
+ "lineRunning command line\n";
+ size_t written = 0;
+ while (written < sizeof(data)) {
+ written += tty1->Write(&data[written], sizeof(data) - written);
+ }
+ GPIOC_PSOR = 1 << 5;
+ while (true) {
+ }
+}
+
+} // namespace
extern "C" {
void *__stack_chk_guard = (void *)0x67111971;
@@ -44,10 +128,12 @@
delay(100);
teensy::UsbDevice usb_device(0, 0x16c0, 0x0490);
+ usb_device.SetManufacturer("FRC 971 Spartan Robotics");
+ usb_device.SetProduct("Pistol Grip Controller interface");
+ teensy::AcmTty tty1(&usb_device);
usb_device.Initialize();
- //GPIOC_PSOR = 1 << 5;
- while (true) {}
+ WriteData(&tty1);
return 0;
}
diff --git a/motors/usb/BUILD b/motors/usb/BUILD
index 55b1790..0696354 100644
--- a/motors/usb/BUILD
+++ b/motors/usb/BUILD
@@ -45,6 +45,45 @@
)
cc_library(
+ name = 'cdc',
+ visibility = ['//visibility:public'],
+ hdrs = [
+ 'cdc.h',
+ ],
+ srcs = [
+ 'cdc.cc',
+ ],
+ deps = [
+ ':usb',
+ ':queue',
+ '//motors:util',
+ ],
+ restricted_to = mcu_cpus,
+)
+
+cc_library(
+ name = 'queue',
+ hdrs = [
+ 'queue.h',
+ ],
+ srcs = [
+ 'queue.cc',
+ ],
+ compatible_with = mcu_cpus,
+)
+
+cc_test(
+ name = 'queue_test',
+ srcs = [
+ 'queue_test.cc',
+ ],
+ deps = [
+ ':queue',
+ '//aos/testing:googletest',
+ ],
+)
+
+cc_library(
name = 'constants',
hdrs = [
'constants.h',
diff --git a/motors/usb/cdc.cc b/motors/usb/cdc.cc
new file mode 100644
index 0000000..b082750
--- /dev/null
+++ b/motors/usb/cdc.cc
@@ -0,0 +1,426 @@
+#include "motors/usb/cdc.h"
+
+#include <string.h>
+
+#define CHECK(c) \
+ do { \
+ if (!(c)) { \
+ while (true) { \
+ for (int i = 0; i < 10000000; ++i) { \
+ GPIOC_PSOR = 1 << 5; \
+ } \
+ for (int i = 0; i < 10000000; ++i) { \
+ GPIOC_PCOR = 1 << 5; \
+ } \
+ } \
+ } \
+ } while (false)
+
+namespace frc971 {
+namespace teensy {
+namespace {
+
+// Aka the Communications Device Class code, the Communications Class code,
+// and the Communications Interface Class code.
+constexpr uint8_t communications_class() { return 0x02; }
+constexpr uint8_t data_interface_class() { return 0x0A; }
+
+namespace cdc_descriptor_subtype {
+constexpr uint8_t header() { return 0x00; }
+constexpr uint8_t header_length() { return 5; }
+constexpr uint8_t call_management() { return 0x01; }
+constexpr uint8_t call_management_length() { return 5; }
+constexpr uint8_t abstract_control_management() { return 0x02; }
+constexpr uint8_t abstract_control_management_length() { return 4; }
+constexpr uint8_t direct_line_management() { return 0x03; }
+constexpr uint8_t telephone_ringer() { return 0x04; }
+constexpr uint8_t telephone_call_etc() { return 0x05; }
+// Can't just call this "union" because that's a keyword...
+constexpr uint8_t union_function() { return 0x06; }
+constexpr uint8_t union_length(int number_subordinates) {
+ return 4 + number_subordinates;
+}
+constexpr uint8_t country_selection() { return 0x07; }
+constexpr uint8_t telephone_operational_modes() { return 0x08; }
+constexpr uint8_t usb_terminal() { return 0x09; }
+constexpr uint8_t network_channel() { return 0x0A; }
+constexpr uint8_t protocol_unit() { return 0x0B; }
+constexpr uint8_t extension_unit() { return 0x0C; }
+constexpr uint8_t multichannel_management() { return 0x0D; }
+constexpr uint8_t capi_control() { return 0x0E; }
+constexpr uint8_t ethernet_networking() { return 0x0F; }
+constexpr uint8_t atm_networking() { return 0x10; }
+constexpr uint8_t wireless_handset_control() { return 0x11; }
+constexpr uint8_t mobile_direct_line() { return 0x12; }
+constexpr uint8_t mdlm_detail() { return 0x13; }
+constexpr uint8_t device_management() { return 0x14; }
+constexpr uint8_t obex() { return 0x15; }
+constexpr uint8_t command_set() { return 0x16; }
+constexpr uint8_t command_set_detail() { return 0x17; }
+constexpr uint8_t telephone_control() { return 0x18; }
+constexpr uint8_t obex_service() { return 0x19; }
+constexpr uint8_t ncm() { return 0x1A; }
+} // namespace cdc_descriptor_subtype
+
+namespace communications_subclass {
+constexpr uint8_t direct_line_control_model() { return 0x01; }
+constexpr uint8_t abstract_control_model() { return 0x02; }
+constexpr uint8_t telephone_control_model() { return 0x03; }
+constexpr uint8_t multichannel_control_model() { return 0x04; }
+constexpr uint8_t capi_control_model() { return 0x05; }
+constexpr uint8_t ethernet_networking_control_model() { return 0x06; }
+constexpr uint8_t atm_networking_control_model() { return 0x07; }
+constexpr uint8_t wireless_handset_control_model() { return 0x08; }
+constexpr uint8_t device_management() { return 0x09; }
+constexpr uint8_t mobile_direct_line_model() { return 0x0A; }
+constexpr uint8_t obex() { return 0x0B; }
+constexpr uint8_t ethernet_emulation_model() { return 0x0C; }
+constexpr uint8_t network_control_model() { return 0x0D; }
+} // namespace communications_subclass
+
+namespace cdc_class_requests {
+constexpr uint8_t send_encapsulated_command() { return 0x00; }
+constexpr uint8_t get_encapsulated_response() { return 0x01; }
+constexpr uint8_t set_comm_feature() { return 0x02; }
+constexpr uint8_t get_comm_feature() { return 0x03; }
+constexpr uint8_t clear_comm_feature() { return 0x04; }
+constexpr uint8_t set_aux_line_state() { return 0x10; }
+constexpr uint8_t set_hook_state() { return 0x11; }
+constexpr uint8_t pulse_setup() { return 0x12; }
+constexpr uint8_t send_pulse() { return 0x13; }
+constexpr uint8_t set_pulse_time() { return 0x14; }
+constexpr uint8_t ring_aux_jack() { return 0x15; }
+constexpr uint8_t set_line_coding() { return 0x20; }
+constexpr uint8_t get_line_coding() { return 0x21; }
+constexpr uint8_t set_control_line_state() { return 0x22; }
+constexpr uint8_t send_break() { return 0x23; }
+constexpr uint8_t set_ringer_parms() { return 0x30; }
+constexpr uint8_t get_ringer_parms() { return 0x31; }
+constexpr uint8_t set_operation_parms() { return 0x32; }
+constexpr uint8_t get_operation_parms() { return 0x33; }
+constexpr uint8_t set_line_parms() { return 0x34; }
+constexpr uint8_t get_line_parms() { return 0x35; }
+constexpr uint8_t dial_digits() { return 0x36; }
+constexpr uint8_t set_unit_parameter() { return 0x37; }
+constexpr uint8_t get_unit_parameter() { return 0x38; }
+constexpr uint8_t clear_unit_parameter() { return 0x39; }
+constexpr uint8_t get_profile() { return 0x3A; }
+} // namespace cdc_class_requests
+
+} // namespace
+
+void AcmTty::Initialize() {
+ status_interface_ = AddInterface();
+ data_interface_ = AddInterface();
+ status_endpoint_ = AddEndpoint();
+ data_tx_endpoint_ = AddEndpoint();
+ data_rx_endpoint_ = AddEndpoint();
+
+ {
+ const auto iad_descriptor = CreateDescriptor(
+ iad_descriptor_length(), UsbDescriptorType::kInterfaceAssociation);
+ iad_descriptor->AddByte(status_interface_); // bFirstInterface
+ iad_descriptor->AddByte(2); // bInterfaceCount
+ iad_descriptor->AddByte(communications_class()); // bFunctionClass
+ iad_descriptor->AddByte(communications_subclass::
+ abstract_control_model()); // bFunctionSubClass
+ iad_descriptor->AddByte(0); // bFunctionProtocol
+ iad_descriptor->AddByte(device()->AddString("UsbTty")); // iFunction
+ }
+
+ {
+ const auto interface_descriptor = CreateDescriptor(
+ interface_descriptor_length(), UsbDescriptorType::kInterface);
+ interface_descriptor->AddByte(status_interface_); // bInterfaceNumber
+ interface_descriptor->AddByte(0); // bAlternateSetting
+ interface_descriptor->AddByte(1); // bNumEndpoints
+ interface_descriptor->AddByte(communications_class()); // bInterfaceClass
+ interface_descriptor->AddByte(
+ communications_subclass::
+ abstract_control_model()); // bInterfaceSubClass
+ interface_descriptor->AddByte(0); // bInterfaceProtocol
+ interface_descriptor->AddByte(
+ device()->AddString("UsbTty.status")); // iInterface
+ }
+
+ {
+ const auto cdc_header =
+ CreateDescriptor(cdc_descriptor_subtype::header_length(),
+ UsbClassDescriptorType::kInterface);
+ cdc_header->AddByte(
+ cdc_descriptor_subtype::header()); // bDescriptorSubtype
+ cdc_header->AddUint16(0x0110); // bcdCDC
+ }
+
+ {
+ const auto call_management =
+ CreateDescriptor(cdc_descriptor_subtype::call_management_length(),
+ UsbClassDescriptorType::kInterface);
+ call_management->AddByte(
+ cdc_descriptor_subtype::call_management()); // bDescriptorSubtype
+ // We don't do call management.
+ call_management->AddByte(0); // bmCapabilities
+ call_management->AddByte(data_interface_); // bDataInterface
+ }
+
+ {
+ const auto abstract_control_management =
+ CreateDescriptor(cdc_descriptor_subtype::abstract_control_management_length(),
+ UsbClassDescriptorType::kInterface);
+ abstract_control_management->AddByte(
+ cdc_descriptor_subtype::abstract_control_management()); // bDescriptorSubtype
+ // We support:
+ // line_coding and serial_state
+ // send_break
+ // We don't support:
+ // comm_feature
+ // network_notification
+ abstract_control_management->AddByte(6); // bmCapabilities
+ }
+
+ {
+ const auto cdc_union_descriptor =
+ CreateDescriptor(cdc_descriptor_subtype::union_length(1),
+ UsbClassDescriptorType::kInterface);
+ cdc_union_descriptor->AddByte(
+ cdc_descriptor_subtype::union_function()); // bDescriptorSubtype
+ cdc_union_descriptor->AddByte(status_interface_); // bMasterInterface
+ cdc_union_descriptor->AddByte(data_interface_); // bSlaveInterface
+ }
+
+ {
+ const auto endpoint_descriptor = CreateDescriptor(
+ endpoint_descriptor_length(), UsbDescriptorType::kEndpoint);
+ endpoint_descriptor->AddByte(status_endpoint_ |
+ m_endpoint_address_in()); // bEndpointAddress
+ endpoint_descriptor->AddByte(
+ m_endpoint_attributes_interrupt()); // bmAttributes
+ endpoint_descriptor->AddUint16(kStatusMaxPacketSize); // wMaxEndpointSize
+ // Set it to the max because we have nothing to send, so no point using bus
+ // bandwidth asking.
+ endpoint_descriptor->AddByte(255); // bInterval
+ }
+
+ {
+ const auto interface_descriptor = CreateDescriptor(
+ interface_descriptor_length(), UsbDescriptorType::kInterface);
+ interface_descriptor->AddByte(data_interface_); // bInterfaceNumber
+ interface_descriptor->AddByte(0); // bAlternateSetting
+ interface_descriptor->AddByte(2); // bNumEndpoints
+ interface_descriptor->AddByte(data_interface_class()); // bInterfaceClass
+ interface_descriptor->AddByte(0); // bInterfaceSubClass
+ interface_descriptor->AddByte(0); // bInterfaceProtocol
+ interface_descriptor->AddByte(
+ device()->AddString("UsbTty.data")); // iInterface
+ }
+
+ // Kernel seems to think tx and rx belong in the other order, but it deals
+ // with either one. Everybody's examples seem to use this order, and the
+ // kernel deals with it, so going to go with this.
+ {
+ const auto endpoint_descriptor = CreateDescriptor(
+ endpoint_descriptor_length(), UsbDescriptorType::kEndpoint);
+ endpoint_descriptor->AddByte(data_rx_endpoint_); // bEndpointAddress
+ endpoint_descriptor->AddByte(m_endpoint_attributes_bulk()); // bmAttributes
+ endpoint_descriptor->AddUint16(kDataMaxPacketSize); // wMaxEndpointSize
+ endpoint_descriptor->AddByte(1); // bInterval
+ }
+
+ {
+ const auto endpoint_descriptor = CreateDescriptor(
+ endpoint_descriptor_length(), UsbDescriptorType::kEndpoint);
+ endpoint_descriptor->AddByte(data_tx_endpoint_ |
+ m_endpoint_address_in()); // bEndpointAddress
+ endpoint_descriptor->AddByte(m_endpoint_attributes_bulk()); // bmAttributes
+ endpoint_descriptor->AddUint16(kDataMaxPacketSize); // wMaxEndpointSize
+ endpoint_descriptor->AddByte(1); // bInterval
+ }
+}
+
+// We deliberately don't implement SendEncapsulatedCommand and
+// GetEncapsulatedResponse because there doesn't seem to be any reason for
+// anybody to send those to us, despite them being "required".
+UsbFunction::SetupResponse AcmTty::HandleEndpoint0SetupPacket(
+ const UsbDevice::SetupPacket &setup_packet) {
+ if (G_SETUP_REQUEST_TYPE_TYPE(setup_packet.request_type) !=
+ SetupRequestType::kClass) {
+ return SetupResponse::kIgnored;
+ }
+ if (G_SETUP_REQUEST_TYPE_RECIPIENT(setup_packet.request_type) !=
+ standard_setup_recipients::kInterface) {
+ return SetupResponse::kIgnored;
+ }
+ if (setup_packet.index != status_interface_) {
+ return SetupResponse::kIgnored;
+ }
+ const bool in = setup_packet.request_type & M_SETUP_REQUEST_TYPE_IN;
+ switch (setup_packet.request) {
+ case cdc_class_requests::set_line_coding():
+ if (in || setup_packet.value != 0 ||
+ setup_packet.length != sizeof(line_coding_)) {
+ return SetupResponse::kStall;
+ }
+ next_endpoint0_out_ = NextEndpoint0Out::kLineCoding;
+ return SetupResponse::kHandled;
+
+ case cdc_class_requests::get_line_coding():
+ if (!in || setup_packet.value != 0 ||
+ setup_packet.length != sizeof(line_coding_)) {
+ return SetupResponse::kStall;
+ }
+ line_coding_to_send_ = line_coding_;
+ device()->QueueEndpoint0Data(
+ reinterpret_cast<const char *>(&line_coding_to_send_),
+ setup_packet.length);
+ return SetupResponse::kHandled;
+
+ case cdc_class_requests::set_control_line_state():
+ if (in || setup_packet.length != 0) {
+ return SetupResponse::kStall;
+ }
+ control_line_state_ = setup_packet.value;
+ device()->SendEmptyEndpoint0Packet();
+ return SetupResponse::kHandled;
+
+ case cdc_class_requests::send_break():
+ if (in || setup_packet.length != 0) {
+ return SetupResponse::kStall;
+ }
+ // TODO(Brian): setup_packet.value is the length of the break in ms.
+ // 0xFFFF means keep sending break until receiving another one.
+ device()->SendEmptyEndpoint0Packet();
+ return SetupResponse::kHandled;
+ }
+ return SetupResponse::kStall;
+}
+
+UsbFunction::SetupResponse AcmTty::HandleEndpoint0OutPacket(void *data,
+ int data_length) {
+ switch (next_endpoint0_out_) {
+ case NextEndpoint0Out::kNone:
+ return SetupResponse::kIgnored;
+
+ case NextEndpoint0Out::kLineCoding:
+ if (data_length != sizeof(line_coding_)) {
+ return SetupResponse::kStall;
+ }
+ memcpy(&line_coding_, data, data_length);
+ device()->SendEmptyEndpoint0Packet();
+ return SetupResponse::kHandled;
+
+ default:
+ return SetupResponse::kIgnored;
+ }
+}
+
+void AcmTty::HandleOutFinished(int endpoint, BdtEntry *bdt_entry) {
+ if (endpoint == data_rx_endpoint_) {
+ const size_t data_size = G_USB_BD_BC(bdt_entry->buffer_descriptor);
+ CHECK(rx_queue_.Write(static_cast<char *>(bdt_entry->address), data_size) ==
+ data_size);
+ dma_memory_barrier();
+
+ DisableInterrupts disable_interrupts;
+ // If we don't have space to handle it, don't return the entry so we'll
+ // ask the host to wait to transmit more data.
+ if (rx_queue_.space_available() >= kDataMaxPacketSize * 2) {
+ bdt_entry->buffer_descriptor = M_USB_BD_OWN | M_USB_BD_DTS |
+ V_USB_BD_BC(kDataMaxPacketSize) |
+ static_cast<uint32_t>(next_rx_toggle_);
+ next_rx_toggle_ = Data01Inverse(next_rx_toggle_);
+ } else {
+ if (first_rx_held_ == nullptr) {
+ first_rx_held_ = bdt_entry;
+ } else {
+ CHECK(second_rx_held_ == nullptr);
+ second_rx_held_ = bdt_entry;
+ }
+ }
+ }
+}
+
+void AcmTty::HandleInFinished(int endpoint, BdtEntry * /*bdt_entry*/,
+ EvenOdd odd) {
+ if (endpoint == data_tx_endpoint_) {
+ DisableInterrupts disable_interrupts;
+ CHECK(odd == BufferStateToEmpty(tx_state_));
+ tx_state_ = BufferStateAfterEmpty(tx_state_);
+ EnqueueTxData(disable_interrupts);
+ }
+}
+
+void AcmTty::HandleConfigured(int endpoint) {
+ // TODO(Brian): Handle data already in the buffers correctly.
+ if (endpoint == status_endpoint_) {
+ device()->ConfigureEndpointFor(status_endpoint_, false, true, true);
+ } else if (endpoint == data_tx_endpoint_) {
+ device()->ConfigureEndpointFor(data_tx_endpoint_, false, true, true);
+ next_tx_toggle_ = Data01::kData0;
+ DisableInterrupts disable_interrupts;
+ EnqueueTxData(disable_interrupts);
+ } else if (endpoint == data_rx_endpoint_) {
+ device()->ConfigureEndpointFor(data_rx_endpoint_, true, false, true);
+ next_rx_toggle_ = Data01::kData0;
+ device()->SetBdtEntry(
+ data_rx_endpoint_, Direction::kRx, EvenOdd::kEven,
+ {M_USB_BD_OWN | M_USB_BD_DTS | V_USB_BD_BC(tx_buffers_[0].size()),
+ tx_buffers_[0].data()});
+ device()->SetBdtEntry(
+ data_rx_endpoint_, Direction::kRx, EvenOdd::kOdd,
+ {M_USB_BD_OWN | M_USB_BD_DTS | V_USB_BD_BC(tx_buffers_[1].size()) |
+ M_USB_BD_DATA1,
+ tx_buffers_[1].data()});
+ }
+}
+
+size_t AcmTty::Read(void *buffer, size_t buffer_size) {
+ const size_t r = rx_queue_.Read(static_cast<char *>(buffer), buffer_size);
+
+ DisableInterrupts disable_interrupts;
+ if (rx_queue_.space_available() >= kDataMaxPacketSize * 2) {
+ if (first_rx_held_ != nullptr) {
+ first_rx_held_->buffer_descriptor =
+ M_USB_BD_OWN | M_USB_BD_DTS | V_USB_BD_BC(kDataMaxPacketSize) |
+ static_cast<uint32_t>(next_rx_toggle_);
+ next_rx_toggle_ = Data01Inverse(next_rx_toggle_);
+ }
+ if (second_rx_held_ != nullptr) {
+ second_rx_held_->buffer_descriptor =
+ M_USB_BD_OWN | M_USB_BD_DTS | V_USB_BD_BC(kDataMaxPacketSize) |
+ static_cast<uint32_t>(next_rx_toggle_);
+ next_rx_toggle_ = Data01Inverse(next_rx_toggle_);
+ }
+ first_rx_held_ = second_rx_held_ = nullptr;
+ }
+
+ return r;
+}
+
+size_t AcmTty::Write(const void *buffer, size_t buffer_size) {
+ const size_t r =
+ tx_queue_.Write(static_cast<const char *>(buffer), buffer_size);
+ DisableInterrupts disable_interrupts;
+ EnqueueTxData(disable_interrupts);
+ return r;
+}
+
+// TODO(Brian): Could this critical section be broken up per buffer we fill?
+void AcmTty::EnqueueTxData(const DisableInterrupts &) {
+ while (BufferStateHasEmpty(tx_state_) && !tx_queue_.empty()) {
+ const EvenOdd next_tx_odd = BufferStateToFill(tx_state_);
+ const size_t buffer_size =
+ tx_queue_.Read(tx_buffer_for(next_tx_odd), kDataMaxPacketSize);
+ CHECK(buffer_size > 0);
+ dma_memory_barrier();
+ device()->SetBdtEntry(
+ data_tx_endpoint_, Direction::kTx, next_tx_odd,
+ {M_USB_BD_OWN | M_USB_BD_DTS | V_USB_BD_BC(buffer_size) |
+ static_cast<uint32_t>(next_tx_toggle_),
+ tx_buffer_for(next_tx_odd)});
+ tx_state_ = BufferStateAfterFill(tx_state_);
+ next_tx_toggle_ = Data01Inverse(next_tx_toggle_);
+ }
+}
+
+} // namespace teensy
+} // namespace frc971
diff --git a/motors/usb/cdc.h b/motors/usb/cdc.h
new file mode 100644
index 0000000..f7c7437
--- /dev/null
+++ b/motors/usb/cdc.h
@@ -0,0 +1,133 @@
+#ifndef MOTORS_USB_CDC_H_
+#define MOTORS_USB_CDC_H_
+
+#include <array>
+
+#include "motors/usb/usb.h"
+#include "motors/usb/queue.h"
+#include "motors/util.h"
+
+// CDC (Communications Device Class) is a "class standard" which takes 30 pages
+// to explain that a communications device has communications and data, and they
+// can come via separate interfaces or the same ones, and the data can be in a
+// lot of formats. The only commonality across the "class" seems to be some
+// constants and a few descriptors.
+
+namespace frc971 {
+namespace teensy {
+
+struct CdcLineCoding {
+ static constexpr uint8_t stop_bits_1() { return 0; }
+ static constexpr uint8_t stop_bits_1_5() { return 1; }
+ static constexpr uint8_t stop_bits_2() { return 2; }
+
+ static constexpr uint8_t parity_none() { return 0; }
+ static constexpr uint8_t parity_odd() { return 1; }
+ static constexpr uint8_t parity_even() { return 2; }
+ static constexpr uint8_t parity_mark() { return 3; }
+ static constexpr uint8_t parity_space() { return 4; }
+
+ // The baud rate in bits/second.
+ uint32_t rate; // dwDTERate
+
+ uint8_t stop_bits; // bCharFormat
+
+ uint8_t parity; // bParityType
+
+ // 5, 6, 7, 8, or 16 according to the standard.
+ uint8_t data_bits; // bDataBits
+} __attribute__((packed));
+static_assert(sizeof(CdcLineCoding) == 7, "wrong size");
+
+// Implements a pretty dumb serial port via CDC's ACM (Abstract Control
+// Management) and Call Management functions.
+class AcmTty final : public UsbFunction {
+ public:
+ AcmTty(UsbDevice *device) : UsbFunction(device) {}
+ ~AcmTty() override = default;
+
+ size_t Read(void *buffer, size_t buffer_size);
+ size_t Write(const void *buffer, size_t buffer_size);
+
+ private:
+ enum class NextEndpoint0Out {
+ kNone,
+ kLineCoding,
+ };
+
+ // We're going with the largest allowable sizes for full speed devices for
+ // the data endpoints to maximize throughput.
+ static constexpr uint16_t kDataMaxPacketSize = 64;
+
+ // We have no information to send here, so no point allocating bus bandwidth
+ // for it.
+ static constexpr uint16_t kStatusMaxPacketSize = 1;
+
+ ::std::array<::std::array<char, kDataMaxPacketSize>, 2> tx_buffers_,
+ rx_buffers_;
+
+ void Initialize() override;
+
+ SetupResponse HandleEndpoint0SetupPacket(
+ const UsbDevice::SetupPacket &setup_packet) override;
+ SetupResponse HandleEndpoint0OutPacket(void *data, int data_length) override;
+ void HandleOutFinished(int endpoint, BdtEntry *bdt_entry) override;
+ void HandleInFinished(int endpoint, BdtEntry *bdt_entry,
+ EvenOdd odd) override;
+ void HandleConfigured(int endpoint) override;
+ void HandleReset() override {
+ // TODO(Brian): Handle data already in the buffers correctly.
+ DisableInterrupts disable_interrupts;
+ tx_state_ = EndpointBufferState::kBothEmptyEvenFirst;
+ device()->SetBdtEntry(status_endpoint_, Direction::kTx, EvenOdd::kEven,
+ {0, nullptr});
+ device()->SetBdtEntry(status_endpoint_, Direction::kTx, EvenOdd::kOdd,
+ {0, nullptr});
+ device()->SetBdtEntry(data_tx_endpoint_, Direction::kTx, EvenOdd::kEven,
+ {0, nullptr});
+ device()->SetBdtEntry(data_tx_endpoint_, Direction::kTx, EvenOdd::kOdd,
+ {0, nullptr});
+ device()->SetBdtEntry(data_rx_endpoint_, Direction::kRx, EvenOdd::kEven,
+ {0, nullptr});
+ device()->SetBdtEntry(data_rx_endpoint_, Direction::kRx, EvenOdd::kOdd,
+ {0, nullptr});
+ }
+
+ char *tx_buffer_for(EvenOdd odd) {
+ return tx_buffers_[EvenOddIndex(odd)].data();
+ }
+
+ void EnqueueTxData(const DisableInterrupts &);
+
+ // In theory, we could sent notifications over this about things like the line
+ // state, but we don't have anything to report so we pretty much just ignore
+ // this.
+ int status_interface_;
+ int data_interface_;
+ int status_endpoint_, data_tx_endpoint_, data_rx_endpoint_;
+
+ Queue tx_queue_{1024}, rx_queue_{1024};
+
+ NextEndpoint0Out next_endpoint0_out_ = NextEndpoint0Out::kNone;
+
+ // This is only manipulated with interrupts disabled.
+ EndpointBufferState tx_state_;
+ Data01 next_tx_toggle_;
+
+ // These are BdtEntries which we're holding into without releasing back to the
+ // hardware so that we won't receive any more data until we have space for it.
+ // They are only manipulated with interrupts disabled.
+ BdtEntry *first_rx_held_ = nullptr, *second_rx_held_ = nullptr;
+ Data01 next_rx_toggle_;
+
+ CdcLineCoding line_coding_{0, CdcLineCoding::stop_bits_1(),
+ CdcLineCoding::parity_none(), 8};
+ CdcLineCoding line_coding_to_send_;
+
+ uint16_t control_line_state_ = 0;
+};
+
+} // namespace teensy
+} // namespace frc971
+
+#endif // MOTORS_USB_CDC_H_
diff --git a/motors/usb/queue.cc b/motors/usb/queue.cc
new file mode 100644
index 0000000..82fa32f
--- /dev/null
+++ b/motors/usb/queue.cc
@@ -0,0 +1,32 @@
+#include "motors/usb/queue.h"
+
+#include <string.h>
+
+namespace frc971 {
+namespace teensy {
+
+size_t Queue::Read(char *out_data, size_t out_size) {
+ const size_t read_cursor = read_cursor_.load(::std::memory_order_relaxed);
+ const size_t write_cursor = write_cursor_.load(::std::memory_order_acquire);
+ const size_t r = ::std::min(out_size, space_used(read_cursor, write_cursor));
+ const size_t first_chunk = ::std::min(r, size_ - read_cursor);
+ memcpy(out_data, &data_[read_cursor], first_chunk);
+ memcpy(out_data + first_chunk, &data_[0], r - first_chunk);
+ read_cursor_.store(wrap(read_cursor + r), ::std::memory_order_release);
+ return r;
+}
+
+size_t Queue::Write(const char *in_data, size_t in_size) {
+ const size_t write_cursor = write_cursor_.load(::std::memory_order_relaxed);
+ const size_t read_cursor = read_cursor_.load(::std::memory_order_acquire);
+ const size_t r =
+ ::std::min(in_size, size_ - space_used(read_cursor, write_cursor) - 1);
+ const size_t first_chunk = ::std::min(r, size_ - write_cursor);
+ memcpy(&data_[write_cursor], in_data, first_chunk);
+ memcpy(&data_[0], in_data + first_chunk, r - first_chunk);
+ write_cursor_.store(wrap(write_cursor + r), ::std::memory_order_release);
+ return r;
+}
+
+} // namespace teensy
+} // namespace frc971
diff --git a/motors/usb/queue.h b/motors/usb/queue.h
new file mode 100644
index 0000000..669abe4
--- /dev/null
+++ b/motors/usb/queue.h
@@ -0,0 +1,69 @@
+#ifndef MOTORS_USB_QUEUE_H_
+#define MOTORS_USB_QUEUE_H_
+
+#include <memory>
+#include <atomic>
+
+namespace frc971 {
+namespace teensy {
+
+// A FIFO queue which reads/writes variable-sized chunks.
+//
+// Reading data happens-after it is written. However, external synchronization
+// between multiple readers/writers is required.
+class Queue {
+ public:
+ Queue(size_t size) : size_(size), data_(new char[size]) {}
+
+ // Writes as much of in_data as will fit to the queue. Returns the number of
+ // bytes written.
+ size_t Write(const char *in_data, size_t in_size);
+
+ // Reads up to out_size from the queue into out_data. Returns the number of
+ // bytes read.
+ size_t Read(char *out_data, size_t out_size);
+
+ size_t data_queued() const {
+ return space_used(read_cursor_.load(::std::memory_order_relaxed),
+ write_cursor_.load(::std::memory_order_relaxed));
+ }
+
+ size_t space_available() const {
+ return size_ - data_queued() - 1;
+ }
+
+ bool empty() const {
+ return read_cursor_.load(::std::memory_order_relaxed) ==
+ write_cursor_.load(::std::memory_order_relaxed);
+ }
+
+ private:
+ size_t space_used(size_t read_cursor, size_t write_cursor) const {
+ size_t r = write_cursor - read_cursor;
+ if (r > size_) {
+ r = size_ + r;
+ }
+ return r;
+ }
+
+ size_t wrap(size_t cursor) const {
+ if (cursor >= size_) {
+ return cursor - size_;
+ } else {
+ return cursor;
+ }
+ }
+
+ const size_t size_;
+ const ::std::unique_ptr<char[]> data_;
+
+ // The next index we're going to read from.
+ ::std::atomic<size_t> read_cursor_{0};
+ // The next index we're going to write to.
+ ::std::atomic<size_t> write_cursor_{0};
+};
+
+} // namespace teensy
+} // namespace frc971
+
+#endif // MOTORS_USB_QUEUE_H_
diff --git a/motors/usb/queue_test.cc b/motors/usb/queue_test.cc
new file mode 100644
index 0000000..fed54d6
--- /dev/null
+++ b/motors/usb/queue_test.cc
@@ -0,0 +1,45 @@
+#include "motors/usb/queue.h"
+
+#include "gtest/gtest.h"
+
+namespace frc971 {
+namespace teensy {
+namespace testing {
+
+TEST(QueueTest, Basic) {
+ Queue queue(64);
+ ASSERT_EQ(0u, queue.data_queued());
+ ASSERT_TRUE(queue.empty());
+ ASSERT_EQ(5u, queue.Write("abcde", 5));
+ ASSERT_EQ(5u, queue.data_queued());
+ ASSERT_FALSE(queue.empty());
+ char buffer[5];
+ ASSERT_EQ(5u, queue.Read(buffer, 5));
+ ASSERT_EQ("abcde", ::std::string(buffer, 5));
+ ASSERT_TRUE(queue.empty());
+}
+
+TEST(QueueTest, Fill) {
+ Queue queue(8);
+ ASSERT_EQ(0u, queue.data_queued());
+ ASSERT_EQ(7u, queue.Write("abcdefgh", 8));
+ ASSERT_EQ(7u, queue.data_queued());
+ char buffer[7];
+ ASSERT_EQ(7u, queue.Read(buffer, 100));
+ ASSERT_EQ("abcdefg", ::std::string(buffer, 7));
+ ASSERT_TRUE(queue.empty());
+
+ ASSERT_EQ(3u, queue.Write("xyz", 3));
+ ASSERT_EQ(3u, queue.Read(buffer, 100));
+ ASSERT_EQ(0u, queue.data_queued());
+
+ ASSERT_EQ(7u, queue.Write("abcdefgh", 8));
+ ASSERT_EQ(7u, queue.data_queued());
+ ASSERT_EQ(7u, queue.Read(buffer, 100));
+ ASSERT_EQ("abcdefg", ::std::string(buffer, 7));
+ ASSERT_TRUE(queue.empty());
+}
+
+} // namespace testing
+} // namespace teensy
+} // namespace frc971
diff --git a/motors/util.h b/motors/util.h
index b215996..e4a0271 100644
--- a/motors/util.h
+++ b/motors/util.h
@@ -150,6 +150,18 @@
#define USB0_ENDPTn(n) (*(volatile uint8_t *)(0x400720C0 + ((n)*4)))
#ifdef __cplusplus
+// RAII class to disable interrupts temporarily.
+class DisableInterrupts {
+ public:
+ DisableInterrupts() { __disable_irq(); }
+ ~DisableInterrupts() { __enable_irq(); }
+
+ DisableInterrupts(const DisableInterrupts &) = delete;
+ DisableInterrupts &operator=(const DisableInterrupts &) = delete;
+};
+#endif // __cplusplus
+
+#ifdef __cplusplus
}
#endif