Abstract USB device implementation (#124)

wch-ch32v003
David Sugar 1 year ago committed by GitHub
parent 109249913b
commit dd491cc84f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -270,8 +270,16 @@ pub fn build(b: *std.build.Builder) !void {
.optimize = optimize, .optimize = optimize,
}); });
const core_tests = b.addTest(.{
.root_source_file = .{
.path = comptime root_dir() ++ "/src/core.zig",
},
.optimize = optimize,
});
const test_step = b.step("test", "build test programs"); const test_step = b.step("test", "build test programs");
test_step.dependOn(&minimal.inner.step); test_step.dependOn(&minimal.inner.step);
test_step.dependOn(&has_hal.inner.step); test_step.dependOn(&has_hal.inner.step);
test_step.dependOn(&has_board.inner.step); test_step.dependOn(&has_board.inner.step);
test_step.dependOn(&b.addRunArtifact(core_tests).step);
} }

@ -1 +1,7 @@
pub const experimental = @import("core/experimental.zig"); pub const experimental = @import("core/experimental.zig");
/// USB data types and helper functions
pub const usb = @import("core/usb.zig");
test "core tests" {
_ = usb;
}

@ -0,0 +1,875 @@
//! Abstract USB device implementation
//!
//! This can be used to setup a USB device.
//!
//! ## Usage
//!
//! 1. Define the functions (`pub const F = struct { ... }`) required by `Usb()` (see below)
//! 2. Call `pub const device = Usb(F)`
//! 3. Define the device configuration (DeviceConfiguration)
//! 4. Initialize the device in main by calling `usb.init_clk()` and `usb.init_device(device_conf)`
//! 5. Call `usb.task()` within the main loop
const std = @import("std");
/// USB Human Interface Device (HID)
pub const hid = @import("usb/hid.zig");
/// Create a USB device
///
/// # Arguments
///
/// This is a abstract USB device implementation that requires a handful of functions
/// to work correctly:
///
/// * `usb_init_clk() void` - Initialize the USB clock
/// * `usb_init_device(*DeviceConfiguration) - Initialize the USB device controller (e.g. enable interrupts, etc.)
/// * `usb_start_tx(*EndpointConfiguration, []const u8)` - Transmit the given bytes over the specified endpoint
/// * `usb_start_rx(*usb.EndpointConfiguration, n: usize)` - Receive n bytes over the specified endpoint
/// * `get_interrupts() InterruptStatus` - Return which interrupts haven't been handled yet
/// * `get_setup_packet() SetupPacket` - Return the USB setup packet received (called if SetupReq received). Make sure to clear the status flag yourself!
/// * `bus_reset() void` - Called on a bus reset interrupt
/// * `set_address(addr: u7) void` - Set the given address
/// * `get_EPBIter(*const DeviceConfiguration) EPBIter` - Return an endpoint buffer iterator. Each call to next returns an unhandeled endpoint buffer with data. How next is implemented depends on the system.
/// The functions must be grouped under the same name space and passed to the fuction at compile time.
/// The functions will be accessible to the user through the `callbacks` field.
pub fn Usb(comptime f: anytype) type {
return struct {
/// The usb configuration set
var usb_config: ?*DeviceConfiguration = null;
/// The clock has been initialized [Y/n]
var clk_init: bool = false;
/// Index of enpoint buffer 0 out
pub const EP0_OUT_IDX = 0;
/// Index of enpoint buffer 0 in
pub const EP0_IN_IDX = 1;
/// The callbacks passed provided by the caller
pub const callbacks = f;
/// Initialize the USB clock
pub fn init_clk() void {
f.usb_init_clk();
clk_init = true;
}
/// Initialize the usb device using the given configuration
///
/// This function will return an error if the clock hasn't been initialized.
pub fn init_device(device_config: *DeviceConfiguration) !void {
if (!clk_init) return error.UninitializedClock;
f.usb_init_device(device_config);
usb_config = device_config;
}
/// Usb task function meant to be executed in regular intervals after
/// initializing the device.
///
/// This function will return an error if the device hasn't been initialized.
pub fn task(debug: bool) !void {
if (usb_config == null) return error.UninitializedDevice;
// We'll keep some state in Plain Old Static Local Variables:
const S = struct {
// When the host gives us a new address, we can't just slap it into
// registers right away, because we have to do an acknowledgement step using
// our _old_ address.
var new_address: ?u8 = null;
// Flag recording whether the host has configured us with a
// `SetConfiguration` message.
var configured = false;
// Flag recording whether we've set up buffer transfers after being
// configured.
var started = false;
// Some scratch space that we'll use for things like preparing string
// descriptors for transmission.
var tmp: [64]u8 = .{0} ** 64;
};
// Check which interrupt flags are set.
const ints = f.get_interrupts();
// Setup request received?
if (ints.SetupReq) {
if (debug) std.log.info("setup req", .{});
// Get the setup request setup packet
const setup = f.get_setup_packet();
// Reset PID to 1 for EP0 IN. Every DATA packet we send in response
// to an IN on EP0 needs to use PID DATA1, and this line will ensure
// that.
usb_config.?.endpoints[EP0_IN_IDX].next_pid_1 = true;
// Attempt to parse the request type and request into one of our
// known enum values, and then inspect them. (These will return None
// if we get an unexpected numeric value.)
const reqty = Dir.of_endpoint_addr(setup.request_type);
const req = SetupRequest.from_u8(setup.request);
if (reqty == Dir.Out and req != null and req.? == SetupRequest.SetAddress) {
// The new address is in the bottom 8 bits of the setup
// packet value field. Store it for use later.
S.new_address = @intCast(u8, setup.value & 0xff);
// The address will actually get set later, we have
// to use address 0 to send a status response.
f.usb_start_tx(
usb_config.?.endpoints[EP0_IN_IDX], //EP0_IN_CFG,
&.{}, // <- see, empty buffer
);
if (debug) std.log.info(" SetAddress: {}", .{S.new_address.?});
} else if (reqty == Dir.Out and req != null and req.? == SetupRequest.SetConfiguration) {
// We only have one configuration, and it doesn't really
// mean anything to us -- more of a formality. All we do in
// response to this is:
S.configured = true;
f.usb_start_tx(
usb_config.?.endpoints[EP0_IN_IDX], //EP0_IN_CFG,
&.{}, // <- see, empty buffer
);
if (debug) std.log.info(" SetConfiguration", .{});
} else if (reqty == Dir.Out) {
// This is sort of a hack, but: if we get any other kind of
// OUT, just acknowledge it with the same zero-length status
// phase that we use for control transfers that we _do_
// understand. This keeps the host from spinning forever
// while we NAK.
//
// This behavior copied shamelessly from the C example.
f.usb_start_tx(
usb_config.?.endpoints[EP0_IN_IDX], // EP0_IN_CFG,
&.{}, // <- see, empty buffer
);
if (debug) std.log.info(" Just OUT", .{});
} else if (reqty == Dir.In and req != null and req.? == SetupRequest.GetDescriptor) {
// Identify the requested descriptor type, which is in the
// _top_ 8 bits of value.
const descriptor_type = DescType.from_u16(setup.value >> 8);
if (debug) std.log.info(" GetDescriptor: {}", .{setup.value >> 8});
if (descriptor_type) |dt| {
switch (dt) {
.Device => {
if (debug) std.log.info(" Device", .{});
// TODO: this sure looks like a duplicate, but it's
// a duplicate that was present in the C
// implementation.
usb_config.?.endpoints[EP0_IN_IDX].next_pid_1 = true;
const dc = usb_config.?.device_descriptor.serialize();
std.mem.copy(u8, S.tmp[0..dc.len], &dc);
// Configure EP0 IN to send the device descriptor
// when it's next asked.
f.usb_start_tx(
usb_config.?.endpoints[EP0_IN_IDX],
S.tmp[0..dc.len],
);
},
.Config => {
if (debug) std.log.info(" Config", .{});
// Config descriptor requests are slightly unusual.
// We can respond with just our config descriptor,
// but we can _also_ append our interface and
// endpoint descriptors to the end, saving some
// round trips. We'll choose to do this if the
// number of bytes the host will accept (in the
// `length` field) is large enough.
var used: usize = 0;
const cd = usb_config.?.config_descriptor.serialize();
std.mem.copy(u8, S.tmp[used .. used + cd.len], &cd);
used += cd.len;
if (setup.length > used) {
// Do the rest!
//
// This is slightly incorrect because the host
// might have asked for a number of bytes in
// between the size of a config descriptor, and
// the amount we're going to send back. However,
// in practice, the host always asks for either
// (1) the exact size of a config descriptor, or
// (2) 64 bytes, and this all fits in 64 bytes.
const id = usb_config.?.interface_descriptor.serialize();
std.mem.copy(u8, S.tmp[used .. used + id.len], &id);
used += id.len;
// Seems like the host does not bother asking for the
// hid descriptor so we'll just send it with the
// other descriptors.
if (usb_config.?.hid) |hid_conf| {
const hd = hid_conf.hid_descriptor.serialize();
std.mem.copy(u8, S.tmp[used .. used + hd.len], &hd);
used += hd.len;
}
// TODO: depending on the number of endpoints
// this might not fit in 64 bytes -> split message
// into multiple packets
for (usb_config.?.endpoints[2..]) |ep| {
const ed = ep.descriptor.serialize();
std.mem.copy(u8, S.tmp[used .. used + ed.len], &ed);
used += ed.len;
}
}
// Set up EP0 IN to send the stuff we just composed.
f.usb_start_tx(
usb_config.?.endpoints[EP0_IN_IDX],
S.tmp[0..used],
);
},
.String => {
if (debug) std.log.info(" String", .{});
// String descriptor index is in bottom 8 bits of
// `value`.
const i = @intCast(usize, setup.value & 0xff);
const bytes = StringBlk: {
if (i == 0) {
// Special index 0 requests the language
// descriptor.
break :StringBlk usb_config.?.lang_descriptor;
} else {
// Otherwise, set up one of our strings.
const s = usb_config.?.descriptor_strings[i - 1];
const len = 2 + s.len;
S.tmp[0] = @intCast(u8, len);
S.tmp[1] = 0x03;
std.mem.copy(u8, S.tmp[2..len], s);
break :StringBlk S.tmp[0..len];
}
};
// Set up EP0 IN to send whichever thing we just
// decided on.
f.usb_start_tx(
usb_config.?.endpoints[EP0_IN_IDX],
bytes,
);
},
.Interface => {
if (debug) std.log.info(" Interface", .{});
// We don't expect the host to send this because we
// delivered our interface descriptor with the
// config descriptor.
//
// Should probably implement it, though, because
// otherwise the host will be unhappy. TODO.
//
// Note that the C example gets away with ignoring
// this.
},
.Endpoint => {
if (debug) std.log.info(" Endpoint", .{});
// Same deal as interface descriptors above.
},
.DeviceQualifier => {
if (debug) std.log.info(" DeviceQualifier", .{});
// We will just copy parts of the DeviceDescriptor because
// the DeviceQualifierDescriptor can be seen as a subset.
const dqd = DeviceQualifierDescriptor{
.bcd_usb = usb_config.?.device_descriptor.bcd_usb,
.device_class = usb_config.?.device_descriptor.device_class,
.device_subclass = usb_config.?.device_descriptor.device_subclass,
.device_protocol = usb_config.?.device_descriptor.device_protocol,
.max_packet_size0 = usb_config.?.device_descriptor.max_packet_size0,
.num_configurations = usb_config.?.device_descriptor.num_configurations,
};
const data = dqd.serialize();
std.mem.copy(u8, S.tmp[0..data.len], &data);
f.usb_start_tx(
usb_config.?.endpoints[EP0_IN_IDX],
S.tmp[0..data.len],
);
},
}
} else {
// Maybe the unknown request type is a hid request
if (usb_config.?.hid) |hid_conf| {
const _hid_desc_type = hid.DescType.from_u16(setup.value >> 8);
if (_hid_desc_type) |hid_desc_type| {
switch (hid_desc_type) {
.Hid => {
if (debug) std.log.info(" HID", .{});
const hd = hid_conf.hid_descriptor.serialize();
std.mem.copy(u8, S.tmp[0..hd.len], &hd);
f.usb_start_tx(
usb_config.?.endpoints[EP0_IN_IDX],
S.tmp[0..hd.len],
);
},
.Report => {
if (debug) std.log.info(" Report", .{});
// The report descriptor is already a (static)
// u8 array, i.e., we can pass it directly
f.usb_start_tx(
usb_config.?.endpoints[EP0_IN_IDX],
hid_conf.report_descriptor,
);
},
.Physical => {
if (debug) std.log.info(" Physical", .{});
// Ignore for now
},
}
} else {
// It's not a valid HID request. This can totally happen
// we'll just ignore it for now...
}
}
}
} else if (reqty == Dir.In) {
if (debug) std.log.info(" Just IN", .{});
// Other IN request. Ignore.
} else {
if (debug) std.log.info(" This is unexpected", .{});
// Unexpected request type or request bits. This can totally
// happen (yay, hardware!) but is rare in practice. Ignore
// it.
}
} // <-- END of setup request handling
// Events on one or more buffers? (In practice, always one.)
if (ints.BuffStatus) {
if (debug) std.log.info("buff status", .{});
var iter = f.get_EPBIter(usb_config.?);
while (iter.next(&iter)) |epb| {
if (debug) std.log.info(" data: {any}", .{epb.buffer});
// Perform any required action on the data. For OUT, the `data`
// will be whatever was sent by the host. For IN, it's a copy of
// whatever we sent.
switch (epb.endpoint.descriptor.endpoint_address) {
EP0_IN_ADDR => {
if (debug) std.log.info(" EP0_IN_ADDR", .{});
// We use this opportunity to finish the delayed
// SetAddress request, if there is one:
if (S.new_address) |addr| {
// Change our address:
f.set_address(@intCast(u7, addr));
} else {
// Otherwise, we've just finished sending
// something to the host. We expect an ensuing
// status phase where the host sends us (via EP0
// OUT) a zero-byte DATA packet, so, set that
// up:
f.usb_start_rx(
usb_config.?.endpoints[EP0_OUT_IDX], // EP0_OUT_CFG,
0,
);
}
},
else => {
if (debug) std.log.info(" ELSE, ep_addr: {}", .{
epb.endpoint.descriptor.endpoint_address & 0x7f,
});
// Handle user provided endpoints.
// Invoke the callback (if the user provides one).
if (epb.endpoint.callback) |callback| callback(usb_config.?, epb.buffer);
},
}
}
} // <-- END of buf status handling
// Has the host signaled a bus reset?
if (ints.BusReset) {
if (debug) std.log.info("bus reset", .{});
// Reset the device
f.bus_reset();
// Reset our state.
S.new_address = null;
S.configured = false;
S.started = false;
}
// If we have been configured but haven't reached this point yet, set up
// our custom EP OUT's to receive whatever data the host wants to send.
if (S.configured and !S.started) {
// We can skip the first two endpoints because those are EP0_OUT and EP0_IN
for (usb_config.?.endpoints[2..]) |ep| {
if (Dir.of_endpoint_addr(ep.descriptor.endpoint_address) == .Out) {
// Hey host! we expect data!
f.usb_start_rx(
ep,
64,
);
}
}
S.started = true;
}
}
};
}
// +++++++++++++++++++++++++++++++++++++++++++++++++
// Data Types
// +++++++++++++++++++++++++++++++++++++++++++++++++
// -------------------------
// | DeviceDescriptor |
// -------------------------
// |
// v
// -------------------------
// | ConfigurationDesc |
// -------------------------
// |
// v
// -------------------------
// | InterfaceDescriptor |
// -------------------------
// | |
// v ------------------------------
// ------------------------- |
// | EndpointDescriptor | v
// ------------------------- ---------------------
// | HID Descriptor |
// ---------------------
/// Types of USB descriptor
pub const DescType = enum(u8) {
Device = 0x01,
Config = 0x02,
String = 0x03,
Interface = 0x04,
Endpoint = 0x05,
DeviceQualifier = 0x06,
//-------- Class Specific Descriptors ----------
// 0x21 ...
pub fn from_u16(v: u16) ?@This() {
return switch (v) {
1 => @This().Device,
2 => @This().Config,
3 => @This().String,
4 => @This().Interface,
5 => @This().Endpoint,
6 => @This().DeviceQualifier,
else => null,
};
}
};
/// Types of transfer that can be indicated by the `attributes` field on
/// `EndpointDescriptor`.
pub const TransferType = enum(u2) {
Control = 0,
Isochronous = 1,
Bulk = 2,
Interrupt = 3,
};
/// The types of USB SETUP requests that we understand.
pub const SetupRequest = enum(u8) {
/// Asks the device to send a certain descriptor back to the host. Always
/// used on an IN request.
GetDescriptor = 0x06,
/// Notifies the device that it's being moved to a different address on the
/// bus. Always an OUT.
SetAddress = 0x05,
/// Configures a device by choosing one of the options listed in its
/// descriptors. Always an OUT.
SetConfiguration = 0x09,
pub fn from_u8(request: u8) ?@This() {
return switch (request) {
0x06 => SetupRequest.GetDescriptor,
0x05 => SetupRequest.SetAddress,
0x09 => SetupRequest.SetConfiguration,
else => null,
};
}
};
/// USB deals in two different transfer directions, called OUT (host-to-device)
/// and IN (device-to-host). In the vast majority of cases, OUT is represented
/// by a 0 byte, and IN by an `0x80` byte.
pub const Dir = enum(u8) {
Out = 0,
In = 0x80,
pub inline fn endpoint(self: @This(), num: u8) u8 {
return num | @enumToInt(self);
}
pub inline fn of_endpoint_addr(addr: u8) @This() {
return if (addr & @enumToInt(@This().In) != 0) @This().In else @This().Out;
}
};
/// Describes an endpoint within an interface
pub const EndpointDescriptor = packed struct {
/// Length of this struct, must be 7.
length: u8,
/// Type of this descriptor, must be `Endpoint`.
descriptor_type: DescType,
/// Address of this endpoint, where the bottom 4 bits give the endpoint
/// number (0..15) and the top bit distinguishes IN (1) from OUT (0).
endpoint_address: u8,
/// Endpoint attributes; the most relevant part is the bottom 2 bits, which
/// control the transfer type using the values from `TransferType`.
attributes: u8,
/// Maximum packet size this endpoint can accept/produce.
max_packet_size: u16,
/// Interval for polling interrupt/isochronous endpoints (which we don't
/// currently support) in milliseconds.
interval: u8,
pub fn serialize(self: *const @This()) [7]u8 {
var out: [7]u8 = undefined;
out[0] = 7; // length
out[1] = @enumToInt(self.descriptor_type);
out[2] = self.endpoint_address;
out[3] = self.attributes;
out[4] = @intCast(u8, self.max_packet_size & 0xff);
out[5] = @intCast(u8, (self.max_packet_size >> 8) & 0xff);
out[6] = self.interval;
return out;
}
};
/// Description of an interface within a configuration.
pub const InterfaceDescriptor = packed struct {
/// Length of this structure, must be 9.
length: u8,
/// Type of this descriptor, must be `Interface`.
descriptor_type: DescType,
/// ID of this interface.
interface_number: u8,
/// Allows a single `interface_number` to have several alternate interface
/// settings, where each alternate increments this field. Normally there's
/// only one, and `alternate_setting` is zero.
alternate_setting: u8,
/// Number of endpoint descriptors in this interface.
num_endpoints: u8,
/// Interface class code, distinguishing the type of interface.
interface_class: u8,
/// Interface subclass code, refining the class of interface.
interface_subclass: u8,
/// Protocol within the interface class/subclass.
interface_protocol: u8,
/// Index of interface name within string descriptor table.
interface_s: u8,
pub fn serialize(self: *const @This()) [9]u8 {
var out: [9]u8 = undefined;
out[0] = 9; // length
out[1] = @enumToInt(self.descriptor_type);
out[2] = self.interface_number;
out[3] = self.alternate_setting;
out[4] = self.num_endpoints;
out[5] = self.interface_class;
out[6] = self.interface_subclass;
out[7] = self.interface_protocol;
out[8] = self.interface_s;
return out;
}
};
/// Description of a single available device configuration.
pub const ConfigurationDescriptor = packed struct {
/// Length of this structure, must be 9.
length: u8,
/// Type of this descriptor, must be `Config`.
descriptor_type: DescType,
/// Total length of all descriptors in this configuration, concatenated.
/// This will include this descriptor, plus at least one interface
/// descriptor, plus each interface descriptor's endpoint descriptors.
total_length: u16,
/// Number of interface descriptors in this configuration.
num_interfaces: u8,
/// Number to use when requesting this configuration via a
/// `SetConfiguration` request.
configuration_value: u8,
/// Index of this configuration's name in the string descriptor table.
configuration_s: u8,
/// Bit set of device attributes:
///
/// - Bit 7 should be set (indicates that device can be bus powered in USB
/// 1.0).
/// - Bit 6 indicates that the device can be self-powered.
/// - Bit 5 indicates that the device can signal remote wakeup of the host
/// (like a keyboard).
/// - The rest are reserved and should be zero.
attributes: u8,
/// Maximum device power consumption in units of 2mA.
max_power: u8,
pub fn serialize(self: *const @This()) [9]u8 {
var out: [9]u8 = undefined;
out[0] = 9; // length
out[1] = @enumToInt(self.descriptor_type);
out[2] = @intCast(u8, self.total_length & 0xff);
out[3] = @intCast(u8, (self.total_length >> 8) & 0xff);
out[4] = self.num_interfaces;
out[5] = self.configuration_value;
out[6] = self.configuration_s;
out[7] = self.attributes;
out[8] = self.max_power;
return out;
}
};
/// Describes a device. This is the most broad description in USB and is
/// typically the first thing the host asks for.
pub const DeviceDescriptor = packed struct {
/// Length of this structure, must be 18.
length: u8,
/// Type of this descriptor, must be `Device`.
descriptor_type: DescType,
/// Version of the device descriptor / USB protocol, in binary-coded
/// decimal. This is typically `0x01_10` for USB 1.1.
bcd_usb: u16,
/// Class of device, giving a broad functional area.
device_class: u8,
/// Subclass of device, refining the class.
device_subclass: u8,
/// Protocol within the subclass.
device_protocol: u8,
/// Maximum unit of data this device can move.
max_packet_size0: u8,
/// ID of product vendor.
vendor: u16,
/// ID of product.
product: u16,
/// Device version number, as BCD again.
bcd_device: u16,
/// Index of manufacturer name in string descriptor table.
manufacturer_s: u8,
/// Index of product name in string descriptor table.
product_s: u8,
/// Index of serial number in string descriptor table.
serial_s: u8,
/// Number of configurations supported by this device.
num_configurations: u8,
pub fn serialize(self: *const @This()) [18]u8 {
var out: [18]u8 = undefined;
out[0] = 18; // length
out[1] = @enumToInt(self.descriptor_type);
out[2] = @intCast(u8, self.bcd_usb & 0xff);
out[3] = @intCast(u8, (self.bcd_usb >> 8) & 0xff);
out[4] = self.device_class;
out[5] = self.device_subclass;
out[6] = self.device_protocol;
out[7] = self.max_packet_size0;
out[8] = @intCast(u8, self.vendor & 0xff);
out[9] = @intCast(u8, (self.vendor >> 8) & 0xff);
out[10] = @intCast(u8, self.product & 0xff);
out[11] = @intCast(u8, (self.product >> 8) & 0xff);
out[12] = @intCast(u8, self.bcd_device & 0xff);
out[13] = @intCast(u8, (self.bcd_device >> 8) & 0xff);
out[14] = self.manufacturer_s;
out[15] = self.product_s;
out[16] = self.serial_s;
out[17] = self.num_configurations;
return out;
}
};
/// USB Device Qualifier Descriptor
/// This descriptor is mostly the same as the DeviceDescriptor
pub const DeviceQualifierDescriptor = packed struct {
/// Length of this structure, must be 18.
length: u8 = 10,
/// Type of this descriptor, must be `Device`.
descriptor_type: DescType = DescType.DeviceQualifier,
/// Version of the device descriptor / USB protocol, in binary-coded
/// decimal. This is typically `0x01_10` for USB 1.1.
bcd_usb: u16,
/// Class of device, giving a broad functional area.
device_class: u8,
/// Subclass of device, refining the class.
device_subclass: u8,
/// Protocol within the subclass.
device_protocol: u8,
/// Maximum unit of data this device can move.
max_packet_size0: u8,
/// Number of configurations supported by this device.
num_configurations: u8,
/// Reserved for future use; must be 0
reserved: u8 = 0,
pub fn serialize(self: *const @This()) [10]u8 {
var out: [10]u8 = undefined;
out[0] = 10; // length
out[1] = @enumToInt(self.descriptor_type);
out[2] = @intCast(u8, self.bcd_usb & 0xff);
out[3] = @intCast(u8, (self.bcd_usb >> 8) & 0xff);
out[4] = self.device_class;
out[5] = self.device_subclass;
out[6] = self.device_protocol;
out[7] = self.max_packet_size0;
out[8] = self.num_configurations;
out[9] = self.reserved;
return out;
}
};
/// Layout of an 8-byte USB SETUP packet.
pub const SetupPacket = packed struct {
/// Request type; in practice, this is always either OUT (host-to-device) or
/// IN (device-to-host), whose values are given in the `Dir` enum.
request_type: u8,
/// Request. Standard setup requests are in the `SetupRequest` enum.
/// Devices can extend this with additional types as long as they don't
/// conflict.
request: u8,
/// A simple argument of up to 16 bits, specific to the request.
value: u16,
/// Not used in the requests we support.
index: u16,
/// If data will be transferred after this request (in the direction given
/// by `request_type`), this gives the number of bytes (OUT) or maximum
/// number of bytes (IN).
length: u16,
};
// +++++++++++++++++++++++++++++++++++++++++++++++++
// Driver support stuctures
// +++++++++++++++++++++++++++++++++++++++++++++++++
pub const EndpointConfiguration = struct {
descriptor: *const EndpointDescriptor,
/// Index of this endpoint's control register in the `ep_control` array.
///
/// TODO: this can be derived from the endpoint address, perhaps it should
/// be.
endpoint_control_index: ?usize,
/// Index of this endpoint's buffer control register in the
/// `ep_buffer_control` array.
///
/// TODO this, too, can be derived.
buffer_control_index: usize,
/// Index of this endpoint's data buffer in the array of data buffers
/// allocated from DPRAM. This can be arbitrary, and endpoints can even
/// share buffers if you're careful.
data_buffer_index: usize,
/// Keeps track of which DATA PID (DATA0/DATA1) is expected on this endpoint
/// next. If `true`, we're expecting `DATA1`, otherwise `DATA0`.
next_pid_1: bool,
/// Optional callback for custom OUT endpoints. This function will be called
/// if the device receives data on the corresponding endpoint.
callback: ?*const fn (dc: *DeviceConfiguration, data: []const u8) void = null,
};
pub const DeviceConfiguration = struct {
device_descriptor: *const DeviceDescriptor,
interface_descriptor: *const InterfaceDescriptor,
config_descriptor: *const ConfigurationDescriptor,
lang_descriptor: []const u8,
descriptor_strings: []const []const u8,
hid: ?struct {
hid_descriptor: *const hid.HidDescriptor,
report_descriptor: []const u8,
} = null,
endpoints: [4]*EndpointConfiguration,
};
/// Buffer pointers, once they're prepared and initialized.
pub const Buffers = struct {
/// Fixed EP0 Buffer0, defined by the hardware
ep0_buffer0: [*]u8,
/// Fixed EP0 Buffer1, defined by the hardware and NOT USED in this driver
ep0_buffer1: [*]u8,
/// /// Remaining buffer pool
rest: [16][*]u8,
/// Gets a buffer corresponding to a `data_buffer_index` in a
/// `EndpointConfiguration`.
pub fn get(self: *@This(), i: usize) [*]u8 {
return switch (i) {
0 => self.ep0_buffer0,
1 => self.ep0_buffer1,
else => self.rest[i - 2],
};
}
};
// Handy constants for the endpoints we use here
pub const EP0_IN_ADDR: u8 = Dir.In.endpoint(0);
pub const EP0_OUT_ADDR: u8 = Dir.Out.endpoint(0);
const EP1_OUT_ADDR: u8 = Dir.Out.endpoint(1);
const EP1_IN_ADDR: u8 = Dir.In.endpoint(1);
const EP2_IN_ADDR: u8 = Dir.In.endpoint(2);
/// USB interrupt status
///
/// __Note__: Available interrupts may change from device to device.
pub const InterruptStatus = struct {
/// Host: raised every time the host sends a SOF (Start of Frame)
BuffStatus: bool = false,
BusReset: bool = false,
/// Set when the device connection state changes
DevConnDis: bool = false,
/// Set when the device suspend state changes
DevSuspend: bool = false,
/// Set when the device receives a resume from the host
DevResumeFromHost: bool = false,
/// Setup Request
SetupReq: bool = false,
};
pub const EPBError = error{
/// The system has received a buffer event for an unknown endpoint (this is super unlikely)
UnknownEndpoint,
/// The buffer is not available (this is super unlikely)
NotAvailable,
};
/// Element returned by the endpoint buffer iterator (EPBIter)
pub const EPB = struct {
/// The endpoint the data belongs to
endpoint: *EndpointConfiguration,
/// Data buffer
buffer: []u8,
};
/// Iterator over all input buffers that hold data
pub const EPBIter = struct {
/// Bitmask of the input buffers to handle
bufbits: u32,
/// The last input buffer handled. This can be used to flag the input buffer as handled on the
/// next call.
last_bit: ?u32 = null,
/// Point to the device configuration (to get access to the endpoint buffers defined by the user)
device_config: *const DeviceConfiguration,
/// Get the next available input buffer
next: *const fn (self: *@This()) ?EPB,
};
/// Convert an utf8 into an utf16 (little endian) string
pub fn utf8Toutf16Le(comptime s: []const u8) [s.len << 1]u8 {
const l = s.len << 1;
var ret: [l]u8 = .{0} ** l;
var i: usize = 0;
while (i < s.len) : (i += 1) {
ret[i << 1] = s[i];
}
return ret;
}
test "tests" {
_ = hid;
}
test "utf8 to utf16" {
try std.testing.expectEqualSlices(u8, "M\x00y\x00 \x00V\x00e\x00n\x00d\x00o\x00r\x00", &utf8Toutf16Le("My Vendor"));
try std.testing.expectEqualSlices(u8, "R\x00a\x00s\x00p\x00b\x00e\x00r\x00r\x00y\x00 \x00P\x00i\x00", &utf8Toutf16Le("Raspberry Pi"));
}

@ -0,0 +1,458 @@
//! USB HID
//!
//! ## Interfaces
//!
//! HID class devices use Control and Interrupt pipes for communication
//!
//! 1. Control for USB control and data classes, reports, data from host
//! 2. Interrupt for asynchronous and low latency data to the device
//!
//! ## Settings
//!
//! ### UsbInterfaceDescriptor related settings:
//! The class type for a HID class device is defined by the Interface descriptor.
//! The subclass field is used to identify Boot Devices.
//!
//! * `interface_subclass` - 1 if boot interface, 0 else (most of the time)
//! * `interface_protocol` - 0 if no boot interface, 1 if keyboard boot interface, 2 if mouse BI
//!
//! ## Nice to know
//!
//! The host usually won't ask for the HID descriptor even if the other descriptors
//! indicate that the given device is a HID device, i. e., you should just pass the
//! HID descriptor together with the configuration descriptor.
//!
//! ## Descriptors
//!
//! ### Report
//!
//! A Report descriptor describes each piece of data that the device generates
//! and what the data is actually measuring (e.g., position or button state). The
//! descriptor consists of one or more items and answers the following questions:
//!
//! 1. Where should the input be routed to (e.g., mouse or joystick API)?
//! 2. Should the software assign a functionality to the input?
//!
//! ### Physical
//!
//! ...
//!
//! ### HID
//!
//! The HID descriptor identifies the length and type of subordinate descriptors for device.
const std = @import("std");
// +++++++++++++++++++++++++++++++++++++++++++++++++
// Common Data Types
// +++++++++++++++++++++++++++++++++++++++++++++++++
// ...
// |
// v
// -------------------------
// | InterfaceDescriptor |
// -------------------------
// | |
// | -----------------
// | |
// v v
// ... --------------------------
// | HidDescriptor |
// --------------------------
// | |
// ------ --------
// | |
// v v
// ----------------------- ---------------------
// | ReportDescriptor | | PhysicalDesc |
// ----------------------- ---------------------
pub const DescType = enum(u8) {
/// HID descriptor
Hid = 0x21,
/// Report descriptor
Report = 0x22,
/// Physical descriptor
Physical = 0x23,
pub fn from_u16(v: u16) ?@This() {
return switch (v) {
0x21 => @This().Hid,
0x22 => @This().Report,
0x23 => @This().Physical,
else => null,
};
}
};
/// USB HID descriptor
pub const HidDescriptor = packed struct {
length: u8 = 9,
descriptor_type: DescType = DescType.Hid,
/// Numeric expression identifying the HID Class Specification release
bcd_hid: u16,
/// Numeric expression identifying country code of the localized hardware
country_code: u8,
/// Numeric expression specifying the number of class descriptors
num_descriptors: u8,
/// Type of HID class report
report_type: DescType = DescType.Report,
/// The total size of the Report descriptor
report_length: u16,
pub fn serialize(self: *const @This()) [9]u8 {
var out: [9]u8 = undefined;
out[0] = 9; // length
out[1] = @enumToInt(self.descriptor_type);
out[2] = @intCast(u8, self.bcd_hid & 0xff);
out[3] = @intCast(u8, (self.bcd_hid >> 8) & 0xff);
out[4] = self.country_code;
out[5] = self.num_descriptors;
out[6] = @enumToInt(self.report_type);
out[7] = @intCast(u8, self.report_length & 0xff);
out[8] = @intCast(u8, (self.report_length >> 8) & 0xff);
return out;
}
};
/// HID interface Subclass (for UsbInterfaceDescriptor)
pub const Subclass = enum(u8) {
/// No Subclass
None = 0,
/// Boot Interface Subclass
Boot = 1,
};
/// HID interface protocol
pub const Protocol = enum(u8) {
/// No protocol
None = 0,
/// Keyboard (only if boot interface)
Keyboard = 1,
/// Mouse (only if boot interface)
Mouse = 2,
};
/// HID request report type
pub const ReportType = enum(u8) {
Invalid = 0,
Input = 1,
Output = 2,
Feature = 3,
};
/// HID class specific control request
pub const Request = enum(u8) {
GetReport = 0x01,
GetIdle = 0x02,
GetProtocol = 0x03,
SetReport = 0x09,
SetIdle = 0x0a,
SetProtocol = 0x0b,
};
/// HID country codes
pub const CountryCode = enum(u8) {
NotSupported = 0,
Arabic,
Belgian,
CanadianBilingual,
CanadianFrench,
CzechRepublic,
Danish,
Finnish,
French,
German,
Greek,
Hebrew,
Hungary,
International,
Italian,
JapanKatakana,
Korean,
LatinAmerica,
NetherlandsDutch,
Norwegian,
PersianFarsi,
Poland,
Portuguese,
Russia,
Slovakia,
Spanish,
Swedish,
SwissFrench,
SwissGerman,
Switzerland,
Taiwan,
TurkishQ,
Uk,
Us,
Yugoslavia,
TurkishF,
};
// +++++++++++++++++++++++++++++++++++++++++++++++++
// Report Descriptor Data Types
// +++++++++++++++++++++++++++++++++++++++++++++++++
pub const ReportItemTypes = enum(u2) {
Main = 0,
Global = 1,
Local = 2,
};
pub const ReportItemMainGroup = enum(u4) {
Input = 8,
Output = 9,
Collection = 10,
Feature = 11,
CollectionEnd = 12,
};
pub const CollectionItem = enum(u8) {
Physical = 0,
Application,
Logical,
Report,
NamedArray,
UsageSwitch,
UsageModifier,
};
pub const GlobalItem = enum(u4) {
UsagePage = 0,
LogicalMin = 1,
LogicalMax = 2,
PhysicalMin = 3,
PhysicalMax = 4,
UnitExponent = 5,
Unit = 6,
ReportSize = 7,
ReportId = 8,
ReportCount = 9,
Push = 10,
Pop = 11,
};
pub const LocalItem = enum(u4) {
Usage = 0,
UsageMin = 1,
UsageMax = 2,
DesignatorIndex = 3,
DesignatorMin = 4,
DesignatorMax = 5,
StringIndex = 7,
StringMin = 8,
StringMax = 9,
Delimiter = 10,
};
pub const UsageTable = struct {
const fido: [2]u8 = "\xD0\xF1".*;
};
pub const FidoAllianceUsage = struct {
const u2fhid: [1]u8 = "\x01".*;
const data_in: [1]u8 = "\x20".*;
const data_out: [1]u8 = "\x21".*;
};
const HID_DATA: u8 = 0 << 0;
const HID_CONSTANT: u8 = 1 << 0;
const HID_ARRAY = 0 << 1;
const HID_VARIABLE = 1 << 1;
const HID_ABSOLUTE = 0 << 2;
const HID_RELATIVE = 1 << 2;
const HID_WRAP_NO = 0 << 3;
const HID_WRAP = 1 << 3;
const HID_LINEAR = 0 << 4;
const HID_NONLINEAR = 1 << 4;
const HID_PREFERRED_STATE = 0 << 5;
const HID_PREFERRED_NO = 1 << 5;
const HID_NO_NULL_POSITION = 0 << 6;
const HID_NULL_STATE = 1 << 6;
const HID_NON_VOLATILE = 0 << 7;
const HID_VOLATILE = 1 << 7;
const HID_BITFIELD = 0 << 8;
const HID_BUFFERED_BYTES = 1 << 8;
// +++++++++++++++++++++++++++++++++++++++++++++++++
// Report Descriptor Functions
// +++++++++++++++++++++++++++++++++++++++++++++++++
pub fn hid_report_item(
comptime n: u2,
typ: u2,
tag: u4,
data: [n]u8,
) [n + 1]u8 {
var out: [n + 1]u8 = undefined;
out[0] = (@intCast(u8, tag) << 4) | (@intCast(u8, typ) << 2) | n;
var i: usize = 0;
while (i < n) : (i += 1) {
out[i + 1] = data[i];
}
return out;
}
// Main Items
// -------------------------------------------------
pub fn hid_collection(data: CollectionItem) [2]u8 {
return hid_report_item(
1,
@enumToInt(ReportItemTypes.Main),
@enumToInt(ReportItemMainGroup.Collection),
std.mem.toBytes(@enumToInt(data)),
);
}
pub fn hid_input(data: u8) [2]u8 {
return hid_report_item(
1,
@enumToInt(ReportItemTypes.Main),
@enumToInt(ReportItemMainGroup.Input),
std.mem.toBytes(data),
);
}
pub fn hid_output(data: u8) [2]u8 {
return hid_report_item(
1,
@enumToInt(ReportItemTypes.Main),
@enumToInt(ReportItemMainGroup.Output),
std.mem.toBytes(data),
);
}
pub fn hid_collection_end() [1]u8 {
return hid_report_item(
0,
@enumToInt(ReportItemTypes.Main),
@enumToInt(ReportItemMainGroup.CollectionEnd),
.{},
);
}
// Global Items
// -------------------------------------------------
pub fn hid_usage_page(comptime n: u2, usage: [n]u8) [n + 1]u8 {
return hid_report_item(
n,
@enumToInt(ReportItemTypes.Global),
@enumToInt(GlobalItem.UsagePage),
usage,
);
}
pub fn hid_logical_min(comptime n: u2, data: [n]u8) [n + 1]u8 {
return hid_report_item(
n,
@enumToInt(ReportItemTypes.Global),
@enumToInt(GlobalItem.LogicalMin),
data,
);
}
pub fn hid_logical_max(comptime n: u2, data: [n]u8) [n + 1]u8 {
return hid_report_item(
n,
@enumToInt(ReportItemTypes.Global),
@enumToInt(GlobalItem.LogicalMax),
data,
);
}
pub fn hid_report_size(comptime n: u2, data: [n]u8) [n + 1]u8 {
return hid_report_item(
n,
@enumToInt(ReportItemTypes.Global),
@enumToInt(GlobalItem.ReportSize),
data,
);
}
pub fn hid_report_count(comptime n: u2, data: [n]u8) [n + 1]u8 {
return hid_report_item(
n,
@enumToInt(ReportItemTypes.Global),
@enumToInt(GlobalItem.ReportCount),
data,
);
}
// Local Items
// -------------------------------------------------
pub fn hid_usage(comptime n: u2, data: [n]u8) [n + 1]u8 {
return hid_report_item(
n,
@enumToInt(ReportItemTypes.Local),
@enumToInt(LocalItem.Usage),
data,
);
}
// +++++++++++++++++++++++++++++++++++++++++++++++++
// Report Descriptors
// +++++++++++++++++++++++++++++++++++++++++++++++++
pub const ReportDescriptorFidoU2f = hid_usage_page(2, UsageTable.fido) //
++ hid_usage(1, FidoAllianceUsage.u2fhid) //
++ hid_collection(CollectionItem.Application) //
// Usage Data In
++ hid_usage(1, FidoAllianceUsage.data_in) //
++ hid_logical_min(1, "\x00".*) //
++ hid_logical_max(2, "\xff\x00".*) //
++ hid_report_size(1, "\x08".*) //
++ hid_report_count(1, "\x40".*) //
++ hid_input(HID_DATA | HID_VARIABLE | HID_ABSOLUTE) //
// Usage Data Out
++ hid_usage(1, FidoAllianceUsage.data_out) //
++ hid_logical_min(1, "\x00".*) //
++ hid_logical_max(2, "\xff\x00".*) //
++ hid_report_size(1, "\x08".*) //
++ hid_report_count(1, "\x40".*) //
++ hid_output(HID_DATA | HID_VARIABLE | HID_ABSOLUTE) //
// End
++ hid_collection_end();
test "create hid report item" {
const r = hid_report_item(
2,
0,
3,
"\x22\x11".*,
);
try std.testing.expectEqual(@intCast(usize, 3), r.len);
try std.testing.expectEqual(@intCast(u8, 50), r[0]);
try std.testing.expectEqual(@intCast(u8, 0x22), r[1]);
try std.testing.expectEqual(@intCast(u8, 0x11), r[2]);
}
test "create hid fido usage page" {
const f = hid_usage_page(2, UsageTable.fido);
try std.testing.expectEqual(@intCast(usize, 3), f.len);
try std.testing.expectEqual(@intCast(u8, 6), f[0]);
try std.testing.expectEqual(@intCast(u8, 0xd0), f[1]);
try std.testing.expectEqual(@intCast(u8, 0xf1), f[2]);
}
test "report descriptor fido" {
_ = ReportDescriptorFidoU2f;
}
Loading…
Cancel
Save