Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 42 additions & 13 deletions core/src/core/usb.zig
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@ pub const StructFieldAttributes = struct {
default_value_ptr: ?*const anyopaque = null,
};

/// Meant to make transition to zig 0.16 easier
/// Helper to create a struct, wrapping around @Type, meant to make transition to zig 0.16 easier
pub fn Struct(
comptime layout: std.builtin.Type.ContainerLayout,
comptime BackingInt: ?type,
comptime field_names: []const [:0]const u8,
comptime field_types: *const [field_names.len]type,
comptime field_attrs: *const [field_names.len]StructFieldAttributes,
layout: std.builtin.Type.ContainerLayout,
BackingInt: ?type,
field_names: []const [:0]const u8,
field_types: *const [field_names.len]type,
field_attrs: *const [field_names.len]StructFieldAttributes,
) type {
comptime var fields: []const std.builtin.Type.StructField = &.{};
var fields: []const std.builtin.Type.StructField = &.{};
// Iterate over the names, field types, and attributes, creating a new struct field entry
for (field_names, field_types, field_attrs) |n, T, a| {
fields = fields ++ &[1]std.builtin.Type.StructField{.{
.name = n,
Expand All @@ -47,6 +48,7 @@ pub fn Struct(
} });
}

// What does this do? It lets you iterate through interfaces and endpoints?
pub const DescriptorAllocator = struct {
next_ep_num: [2]u8,
next_itf_num: u8,
Expand Down Expand Up @@ -96,6 +98,7 @@ pub const DescriptorAllocator = struct {
}
};

/// Wraps a Descriptor type. Returned by the `create` method of the Descriptor.
pub fn DescriptorCreateResult(Descriptor: type) type {
return struct {
descriptor: Descriptor,
Expand All @@ -108,7 +111,7 @@ pub fn DescriptorCreateResult(Descriptor: type) type {
}

/// USB Device interface
/// Any device implementation used with DeviceController must implement those functions
/// Any device implementation used with DeviceController must implement these functions
pub const DeviceInterface = struct {
pub const VTable = struct {
ep_writev: *const fn (*DeviceInterface, types.Endpoint.Num, []const []const u8) types.Len,
Expand Down Expand Up @@ -191,16 +194,24 @@ pub const Config = struct {
max_current_ma: u9,
Drivers: type,

/// Generate A struct with a field for each field in Drivers, where the type is the third
/// arg of the Drivers' Descriptor's 'create' method.
pub fn Args(self: @This()) type {
const fields = @typeInfo(self.Drivers).@"struct".fields;
var field_names: [fields.len][:0]const u8 = undefined;
var field_types: [fields.len]type = undefined;
for (fields, 0..) |fld, i| {
// Collect field names
field_names[i] = fld.name;
// Collect the type info for the Descriptor.create function parameter
const params = @typeInfo(@TypeOf(fld.type.Descriptor.create)).@"fn".params;
// Ensure it takes 3 parameters
assert(params.len == 3);
// The first must be a DescriptorAllocator
assert(params[0].type == *DescriptorAllocator);
// The second is usb.types.Len
assert(params[1].type == types.Len);
// And save the type of the third
field_types[i] = params[2].type.?;
}
return Struct(.auto, null, &field_names, &field_types, &@splat(.{}));
Expand Down Expand Up @@ -251,11 +262,14 @@ pub fn validate_controller(T: type) void {

/// USB device controller
///
/// This code handles usb enumeration and configuration and routes packets to drivers.
/// Responds to host requests and dispatches to the appropriate drivers.
/// When this type is build (at comptime), it builds descriptor and handler tables based on the
/// provided config.
pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type {
std.debug.assert(config.configurations.len == 1);

return struct {
// We only support one configuration
const config0 = config.configurations[0];
const driver_fields = @typeInfo(config0.Drivers).@"struct".fields;
const DriverEnum = std.meta.FieldEnum(config0.Drivers);
Expand Down Expand Up @@ -298,24 +312,33 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type {
var driver_alloc_attrs: []const StructFieldAttributes = &.{};

for (driver_fields, 0..) |drv, drv_id| {
// Get descriptor type for the current driver
const Descriptors = drv.type.Descriptor;
// Call the create method on the descriptor, which wraps it with the allow stuff.
const result = Descriptors.create(&alloc, max_psize, @field(driver_args[0], drv.name));
// Get the descriptor instance from the result.
const descriptors = result.descriptor;

// If the driver requests memory, then collect the names, types, and attrs
if (result.alloc_bytes) |len| {
driver_alloc_names = driver_alloc_names ++ &[1][:0]const u8{drv.name};
driver_alloc_types = driver_alloc_types ++ &[1]type{[len]u8};
driver_alloc_attrs = driver_alloc_attrs ++ &[1]StructFieldAttributes{.{ .@"align" = result.alloc_align }};
driver_alloc_names = driver_alloc_names ++ &[_][:0]const u8{drv.name};
driver_alloc_types = driver_alloc_types ++ &[_]type{[len]u8};
driver_alloc_attrs = driver_alloc_attrs ++ &[_]StructFieldAttributes{.{ .@"align" = result.alloc_align }};
} else {
assert(result.alloc_align == null);
}

assert(@alignOf(Descriptors) == 1);
size += @sizeOf(Descriptors);

// Collect handler types, names, and drivers, to later be bound to the appropriate
// endpoints
for (@typeInfo(Descriptors).@"struct".fields, 0..) |fld, desc_num| {
const desc = @field(descriptors, fld.name);

// Determine which interface numbers this driver owns. If it is an
// InterfaceAssociation, then use the interface count. If it is an Interface,
// then the driver owns just that one interface.
if (desc_num == 0) {
const itf_start, const itf_count = switch (fld.type) {
descriptor.InterfaceAssociation => .{ desc.first_interface, desc.interface_count },
Expand All @@ -329,10 +352,11 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type {
};
if (itf_start != itf_handlers.len)
@compileError("interface numbering mismatch");
itf_handlers = itf_handlers ++ &[1]DriverEnum{@field(DriverEnum, drv.name)} ** itf_count;
itf_handlers = itf_handlers ++ &[_]DriverEnum{@field(DriverEnum, drv.name)} ** itf_count;
}

switch (fld.type) {
// Register handler for endpoints
descriptor.Endpoint => {
const ep_dir = @intFromEnum(desc.endpoint.dir);
const ep_num = @intFromEnum(desc.endpoint.num);
Expand All @@ -347,6 +371,7 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type {
ep_handler_drivers[ep_dir][ep_num] = drv_id;
ep_handler_names[ep_dir][ep_num] = fld.name;
},
// Interface association must be first
descriptor.InterfaceAssociation => assert(desc_num == 0),
else => {},
}
Expand All @@ -356,9 +381,12 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type {
field_attrs[drv_id] = .{ .default_value_ptr = &descriptors };
}

// Finally, bind the handler functions based on the data collected above.
const Tuple = std.meta.Tuple;
// Create a tuple with the appropriate types
const ep_handlers_types: [2]type = .{ Tuple(&ep_handler_types[0]), Tuple(&ep_handler_types[1]) };
var ep_handlers: Tuple(&ep_handlers_types) = undefined;
// Iterate over all IN and OUT endpoints and bind the handler for any that are set.
for (&ep_handler_types, &ep_handler_names, &ep_handler_drivers, 0..) |htypes, hnames, hdrivers, dir| {
for (&htypes, &hnames, &hdrivers, 0..) |T, name, drv_id, ep| {
if (T != void)
Expand Down Expand Up @@ -558,6 +586,7 @@ pub fn DeviceController(config: Config, driver_args: config.DriverArgs()) type {
}
}

// Return the appropriate descriptor type as determined by the top 8 bits of the value.
fn get_descriptor(value: u16) ?[]const u8 {
const asBytes = std.mem.asBytes;
const desc_type: descriptor.Type = @enumFromInt(value >> 8);
Expand Down
27 changes: 18 additions & 9 deletions core/src/core/usb/drivers/CDC.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const usb = @import("../../usb.zig");
const assert = std.debug.assert;
const log = std.log.scoped(.usb_cdc);

/// CDC PSTN Subclass Management Element Requests
pub const ManagementRequestType = enum(u8) {
SetCommFeature = 0x02,
GetCommFeature = 0x03,
Expand All @@ -27,6 +28,7 @@ pub const ManagementRequestType = enum(u8) {
_,
};

/// Line coding structure for SetLineCoding/GetLineCoding requests
pub const LineCoding = extern struct {
pub const StopBits = enum(u8) { @"1" = 0, @"1.5" = 1, @"2" = 2, _ };
pub const Parity = enum(u8) {
Expand All @@ -44,13 +46,14 @@ pub const LineCoding = extern struct {
data_bits: u8,

pub const init: @This() = .{
.bit_rate = 115200,
.stop_bits = 0,
.parity = 0,
.bit_rate = .from(115200),
.stop_bits = .@"1",
.parity = .none,
.data_bits = 8,
};
};

// This struct bundles all the descriptors CDC needs into one Configuration.
pub const Descriptor = extern struct {
const desc = usb.descriptor;

Expand All @@ -70,6 +73,11 @@ pub const Descriptor = extern struct {
itf_data: []const u8 = "",
};

/// This function is used during descriptor creation. Endpoint and interface numbers are
/// allocated through the `alloc` parameter. Third argument can be of any type, it's passed
/// by the user when creating the device controller type. If multiple instances of a driver
/// are used, this function is called for each, with different arguments. Passing arguments
/// through this function is preferred to making the whole driver generic.
pub fn create(
alloc: *usb.DescriptorAllocator,
max_supported_packet_size: usb.types.Len,
Expand Down Expand Up @@ -130,6 +138,8 @@ pub const Descriptor = extern struct {
}
};

// These field names are matched (at comptime) to the field names in the descriptor returned from
// `create` when binding the endpoints.
pub const handlers: usb.DriverHandlers(@This()) = .{
.ep_notifi = on_notifi_ready,
.ep_out = on_rx,
Expand Down Expand Up @@ -202,21 +212,18 @@ pub fn flush(self: *@This()) bool {
return true;
}

// Called when the host selects a configuration.
pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface, data: []u8) void {
const len_half = @divExact(data.len, 2);
assert(len_half == desc.ep_in.max_packet_size.into());
assert(len_half == desc.ep_out.max_packet_size.into());
self.* = .{
.device = device,
.descriptor = desc,
.line_coding = .{
.bit_rate = .from(115200),
.stop_bits = .@"1",
.parity = .none,
.data_bits = 8,
},
.line_coding = .init,
.notifi_ready = .init(true),

// `init` provides a data buffer, which we split in half for rx and tx data.
.rx_data = data[0..len_half],
.rx_seek = 0,
.rx_end = 0,
Expand All @@ -229,6 +236,8 @@ pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterfac
device.ep_listen(desc.ep_out.endpoint.num, @intCast(self.rx_data.len));
}

/// Handle class-specific SETUP requests where recipient=Interface
/// Called by DeviceController when the interface number matches this driver.
pub fn class_request(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 {
const mgmt_request: ManagementRequestType = @enumFromInt(setup.request);
log.debug("cdc setup: {any} {} {}", .{ mgmt_request, setup.length.into(), setup.value.into() });
Expand Down
31 changes: 15 additions & 16 deletions core/src/core/usb/drivers/EchoExample.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@ pub const Descriptor = extern struct {
ep_out: desc.Endpoint,
ep_in: desc.Endpoint,

/// This function is used during descriptor creation. Endpoint and
/// interface numbers are allocated through the `alloc` parameter.
/// Third argument can be of any type, it's passed by the user when
/// creating the device controller type. If multiple instances of
/// a driver are used, this function is called for each, with different
/// arguments. Passing arguments through this function is preffered to
/// making the whole driver generic.
/// This function is used during descriptor creation. Endpoint and interface numbers are
/// allocated through the `alloc` parameter. Third argument can be of any type, it's passed
/// by the user when creating the device controller type. If multiple instances of a driver
/// are used, this function is called for each, with different arguments. Passing arguments
/// through this function is preffered to making the whole driver generic.
pub fn create(
alloc: *usb.DescriptorAllocator,
max_supported_packet_size: usb.types.Len,
Expand All @@ -43,9 +41,9 @@ pub const Descriptor = extern struct {
}
};

/// This is a mapping from endpoint descriptor field names to handler
/// function names. Counterintuitively, usb devices send data on 'in'
/// endpoints and receive on 'out' endpoints.
/// This is a mapping from endpoint descriptor field names to handler function names.
/// 'in' and 'out' are from the perspective of the host, so, usb devices send data on 'in' endpoints
/// and receive on 'out' endpoints.
pub const handlers: usb.DriverHandlers(@This()) = .{
.ep_in = on_tx_ready,
.ep_out = on_rx,
Expand All @@ -56,9 +54,8 @@ descriptor: *const Descriptor,
packet_buffer: []u8,
tx_ready: std.atomic.Value(bool),

/// This function is called when the host chooses a configuration
/// that contains this driver. `self` points to undefined memory.
/// `data` is of the length specified in `Descriptor.create()`.
/// This function is called when the host chooses a configuration that contains this driver. `self`
/// points to undefined memory. `data` is of the length specified in `Descriptor.create()`.
pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterface, data: []u8) void {
assert(data.len == desc.ep_in.max_packet_size.into());
self.* = .{
Expand All @@ -77,19 +74,21 @@ pub fn init(self: *@This(), desc: *const Descriptor, device: *usb.DeviceInterfac
/// Data returned by this function is sent on endpoint 0.
pub fn class_request(self: *@This(), setup: *const usb.types.SetupPacket) ?[]const u8 {
_ = self;
_ = setup;
log.debug("setup: {x}, {}, {}", .{ setup.request, setup.length.into(), setup.value.into() });
return usb.ack;
}

/// Transmit handler callback.
/// Each endpoint (as defined in the descriptor) has its own handler.
/// Endpoint number is passed as an argument so that it does not need
/// to be stored in the driver.
/// Endpoint number is passed as an argument so that it does not need to be stored in the driver.
pub fn on_tx_ready(self: *@This(), ep_tx: usb.types.Endpoint.Num) void {
log.info("tx ready ({t})", .{ep_tx});
// Mark transmission as available
self.tx_ready.store(true, .seq_cst);
}

/// Receive handler callback.
/// Endpoint number is passed as an argument so that it does not need to be stored in the driver.
pub fn on_rx(self: *@This(), ep_rx: usb.types.Endpoint.Num) void {
var buf: [64]u8 = undefined;
// Read incoming packet into a local buffer
Expand Down
20 changes: 13 additions & 7 deletions examples/raspberrypi/rp2xxx/src/usb_cdc.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ const rp2xxx = microzig.hal;
const time = rp2xxx.time;
const gpio = rp2xxx.gpio;
const usb = microzig.core.usb;
// Port-specific type which implements the DeviceInterface interface, used by the USB core to
// read from/write to the peripheral.
const USB_Device = rp2xxx.usb.Polled(.{});
// USB class driver
const USB_Serial = usb.drivers.CDC;

var usb_device: USB_Device = undefined;

// Generate a device controller with descriptor and handlers setup for CDC (USB_Serial) and
// picotool-controlled reset (ResetDriver).
var usb_controller: usb.DeviceController(.{
.bcd_usb = USB_Device.max_supported_bcd_usb,
.device_triple = .unspecified,
Expand Down Expand Up @@ -71,6 +76,7 @@ pub fn main() !void {
// You can now poll for USB events
usb_device.poll(&usb_controller);

// Ensure that the host as finished enumerating our USB device
if (usb_controller.drivers()) |drivers| {
new = time.get_time_since_boot().to_us();
if (new - old > 500000) {
Expand All @@ -93,8 +99,9 @@ pub fn main() !void {

var usb_tx_buff: [1024]u8 = undefined;

// Transfer data to host
// NOTE: After each USB chunk transfer, we have to call the USB task so that bus TX events can be handled
/// Transfer data to host
/// NOTE: After each USB chunk transfer, we have to call the USB task so that bus TX events can be
/// handled
pub fn usb_cdc_write(serial: *USB_Serial, comptime fmt: []const u8, args: anytype) void {
var tx = std.fmt.bufPrint(&usb_tx_buff, fmt, args) catch &.{};

Expand All @@ -109,11 +116,10 @@ pub fn usb_cdc_write(serial: *USB_Serial, comptime fmt: []const u8, args: anytyp

var usb_rx_buff: [1024]u8 = undefined;

// Receive data from host
// NOTE: Read code was not tested extensively. In case of issues, try to call USB task before every read operation
pub fn usb_cdc_read(
serial: *USB_Serial,
) []const u8 {
/// Receive data from host
/// NOTE: Read code was not tested extensively. In case of issues, try to call USB task before every
/// read operation
pub fn usb_cdc_read(serial: *USB_Serial) []const u8 {
var rx_len: usize = 0;

while (true) {
Expand Down
Loading
Loading