Skip to content

Instantly share code, notes, and snippets.

@nathansizemore
Created March 8, 2023 03:22
Show Gist options
  • Select an option

  • Save nathansizemore/43a7391949311c26f73b1f8d862452db to your computer and use it in GitHub Desktop.

Select an option

Save nathansizemore/43a7391949311c26f73b1f8d862452db to your computer and use it in GitHub Desktop.
USB Bulk Gadget Driver
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 - 2016 Samsung Electronics
* Krzysztof Opasiak <k.opasiak@samsung.com>
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy,
* modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software
* is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
* OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <malloc.h>
#include <stdlib.h>
#include <unistd.h>
#include <poll.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/eventfd.h>
#include <sys/select.h>
#include <libaio.h>
#include <linux/usb/functionfs.h>
#define EP_IN_IDX 1
#define EP_OUT_IDX 2
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) ); })
/*
* cpu_to_le16/32 are used when initializing structures, a context where a
* function call is not allowed. To solve this, we code cpu_to_le16/32 in a way
* that allows them to be used when initializing structures.
*/
#if BYTE_ORDER == __LITTLE_ENDIAN
#define cpu_to_le16(x) (x)
#define cpu_to_le32(x) (x)
#else
#define cpu_to_le16(x) ((((x) >> 8) & 0xffu) | (((x)&0xffu) << 8))
#define cpu_to_le32(x) \
((((x)&0xff000000u) >> 24) | (((x)&0x00ff0000u) >> 8) | \
(((x)&0x0000ff00u) << 8) | (((x)&0x000000ffu) << 24))
#endif
static const struct
{
struct usb_functionfs_descs_head_v2 header;
__le32 fs_count;
__le32 hs_count;
struct
{
struct usb_interface_descriptor intf;
struct usb_endpoint_descriptor_no_audio bulk_in;
struct usb_endpoint_descriptor_no_audio bulk_out;
} __attribute__((__packed__)) fs_descs, hs_descs;
} __attribute__((__packed__)) descriptors = {
.header = {
.magic = cpu_to_le32(FUNCTIONFS_DESCRIPTORS_MAGIC_V2),
.flags = cpu_to_le32(FUNCTIONFS_HAS_FS_DESC |
FUNCTIONFS_HAS_HS_DESC),
.length = cpu_to_le32(sizeof(descriptors)),
},
.fs_count = cpu_to_le32(3),
.fs_descs = {
.intf = {
.bLength = sizeof(descriptors.fs_descs.intf),
.bDescriptorType = USB_DT_INTERFACE,
.bNumEndpoints = 2,
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
.iInterface = 1,
},
.bulk_in = {
.bLength = sizeof(descriptors.fs_descs.bulk_in),
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = 1 | USB_DIR_IN,
.bmAttributes = USB_ENDPOINT_XFER_BULK,
},
.bulk_out = {
.bLength = sizeof(descriptors.fs_descs.bulk_out),
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = 2 | USB_DIR_OUT,
.bmAttributes = USB_ENDPOINT_XFER_BULK,
},
},
.hs_count = cpu_to_le32(3),
.hs_descs = {
.intf = {
.bLength = sizeof(descriptors.hs_descs.intf),
.bDescriptorType = USB_DT_INTERFACE,
.bNumEndpoints = 2,
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
.iInterface = 1,
},
.bulk_in = {
.bLength = sizeof(descriptors.hs_descs.bulk_in),
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = 1 | USB_DIR_IN,
.bmAttributes = USB_ENDPOINT_XFER_BULK,
.wMaxPacketSize = cpu_to_le16(512),
},
.bulk_out = {
.bLength = sizeof(descriptors.hs_descs.bulk_out),
.bDescriptorType = USB_DT_ENDPOINT,
.bEndpointAddress = 2 | USB_DIR_OUT,
.bmAttributes = USB_ENDPOINT_XFER_BULK,
.wMaxPacketSize = cpu_to_le16(512),
},
},
};
/*
* In previous workshops we used Loopback function
* which has exactly this string. To make this
* workshop compatible with all previous workshops
* we declare the same string as Loopback function
*/
#define STR_INTERFACE "loop input to output"
static const struct
{
struct usb_functionfs_strings_head header;
struct
{
__le16 code;
const char str1[sizeof(STR_INTERFACE)];
} __attribute__((__packed__)) lang0;
} __attribute__((__packed__)) strings = {
.header = {
.magic = cpu_to_le32(FUNCTIONFS_STRINGS_MAGIC),
.length = cpu_to_le32(sizeof(strings)),
.str_count = cpu_to_le32(1),
.lang_count = cpu_to_le32(1),
},
.lang0 = {
cpu_to_le16(0x0409), /* en-us */
STR_INTERFACE,
},
};
#define EXIT_COMMAND "\\exit"
#define MAX_LINE_LENGTH 1024 * 8 /* 8 KB should be enough for single line */
#define report_error(...) \
do \
{ \
fprintf(stderr, __VA_ARGS__); \
putchar('\n'); \
} while (0)
/* Data structure for our message */
struct message
{
uint16_t length;
char line_buf[MAX_LINE_LENGTH];
} __attribute((packed));
/*
* As there is no fully-functional equivalent of libusb for the device
* side we define some helper structures to make our life easier and
* use them as some minimal equivalent of such library
*/
struct ffs_request;
typedef void (*ffs_complete_t)(struct ffs_request *);
/* Possible states of ffs request */
enum
{
FFS_REQ_COMPLETED = 0,
FFS_REQ_IN_PROGRESS = 1,
FFS_REQ_ERROR = 2,
FFS_REQ_CANCELLED = 3,
};
/* Represents single usb request which can be transfered using ffs */
struct ffs_request
{
struct iocb iocb;
unsigned char *buf;
ssize_t length;
void *context;
int status;
int actual;
ffs_complete_t complete;
};
/* Use container_of() to get ffs_request from iocb */
static inline struct ffs_request *to_ffs_request(struct iocb *_iocb)
{
return container_of(_iocb, struct ffs_request, iocb);
}
/*
* helper structure which represents transfer
* of single message in our chat protocol
*/
struct transfer
{
struct ffs_request *length_request;
struct ffs_request *buf_request;
struct message message;
int in_progress;
io_context_t *ctx;
};
/* Indicates if device prompt is currently present */
int device_prompt;
/******************** Basic implementation of our library *********************/
/* allocates single ffs_request */
struct ffs_request *alloc_ffs_request()
{
struct ffs_request *req;
req = malloc(sizeof(*req));
if (!req)
goto out;
memset(req, 0, sizeof(*req));
out:
return req;
}
void free_ffs_request(struct ffs_request *req)
{
free(req);
}
/*
* Schedules ffs request to be asynchronously transfered.
* A little bit device side equivalent to libusb_submit_transfer()
*/
int submit_ffs_request(io_context_t *ctx, struct ffs_request *req)
{
int ret;
struct iocb *iocb = &req->iocb;
iocb->u.c.nbytes = req->length;
ret = io_submit(*ctx, 1, &iocb);
if (ret < 0)
return ret;
req->status = FFS_REQ_IN_PROGRESS;
return 0;
}
/*
* Fill given ffs request using provided data
* A little bit device side equivalent of libusb_fill_bulk_transfer()
*/
void fill_ffs_request(struct ffs_request *req, int dir, int ep, int event_fd,
unsigned char *buf, int length, ffs_complete_t complete,
void *context)
{
struct iocb *iocb = &req->iocb;
req->buf = buf;
req->length = length;
/*
* TODO: prepare read/write operation
*
* Hints:
* - Use dir param to determine type of operation
* - Remember that we are on the device side
* - USB_DIR_IN - transfer data from device to host (write())
* - USB_DIR_OUT - transfer data from host to device (read())
*
* int io_prep_pwrite()
* int io_prep_pread()
*/
if (dir == USB_DIR_IN)
io_prep_pwrite(iocb, ep, buf, length, 0);
else
io_prep_pread(iocb, ep, buf, length, 0);
io_set_eventfd(iocb, event_fd);
req->complete = complete;
req->status = 0;
req->actual = 0;
req->context = context;
}
/* Cancel choosen ffs_request */
void cancel_ffs_request(io_context_t *ctx, struct ffs_request *req)
{
struct io_event e;
if (req->status != FFS_REQ_IN_PROGRESS)
return;
io_cancel(*ctx, &req->iocb, &e);
req->status = FFS_REQ_CANCELLED;
req->actual = 0;
if (req->complete)
req->complete(req);
}
/* Handle all pending aio events */
int handle_events(io_context_t *ctx, int event_fd)
{
int ret;
int i;
uint64_t ev_cnt;
struct io_event e[2];
struct ffs_request *req;
ret = read(event_fd, &ev_cnt, sizeof(ev_cnt));
if (ret < 0)
{
report_error("unable to read eventfd");
return -errno;
}
ret = io_getevents(*ctx, 1, ev_cnt, e, NULL);
if (ret < 0)
return ret;
for (i = 0; i < ret; ++i)
{
long res = (long)e[i].res;
req = to_ffs_request(e[i].obj);
if (res >= 0)
req->status = FFS_REQ_COMPLETED;
else
req->status = FFS_REQ_ERROR;
req->actual = res;
if (req->complete)
req->complete(req);
}
return 0;
}
/* Prepare fresh ffs instance to communicate using our chat protocol */
int prepare_ffs(char *ffs_path, int *ep)
{
char *ep_path;
int i;
int ret = 0;
ep_path = malloc(strlen(ffs_path) + 4 /* "/ep#" */ + 1 /* '\0' */);
if (!ep_path)
{
report_error("malloc");
return -EINVAL;
}
/* open endpoint 0 */
sprintf(ep_path, "%s/ep0", ffs_path);
ep[0] = open(ep_path, O_RDWR);
if (ep[0] < 0)
{
report_error("unable to open ep0");
ret = -errno;
goto out;
}
/*
* TODO: Provide descriptors and strings
*
* Hints:
* - Descriptors and strings are defined on the top of this file
* - You should simply two time call write() function using ep[0] fd
*
* ssize_t write(int fd, const void *buf, size_t count)
* sizeof()
*/
if (write(ep[0], &descriptors, sizeof(descriptors)) < 0)
{
report_error("unable do write descriptors");
ret = -errno;
goto out;
}
if (write(ep[0], &strings, sizeof(strings)) < 0)
{
report_error("unable to write strings");
ret = -errno;
goto out;
}
out:
free(ep_path);
return ret;
}
/* Close all ep files */
void cleanup_ffs(int *ep)
{
int i;
for (i = 0; i < 3; ++i)
close(ep[i]);
}
/* Initialize aio context and create event fd */
int init_aio(int n_requests, io_context_t *ctx, int *event_fd)
{
int ret;
memset(ctx, 0, sizeof(*ctx));
ret = io_setup(n_requests, ctx);
if (ret < 0)
{
report_error("Unable to setup aio context");
return ret;
}
*event_fd = eventfd(0, 0);
if (*event_fd < 0)
{
report_error("Unable to open event fd");
io_destroy(*ctx);
ret = -errno;
}
return ret;
}
/* Cleanup aio context and close event fd */
void cleanup_aio(io_context_t *ctx, int event_fd)
{
close(event_fd);
io_destroy(*ctx);
}
/******************** Protocol specific implementation **********************/
/* Alloc single message transfer (2 ffs requests) */
struct transfer *alloc_transfer()
{
struct transfer *t;
t = malloc(sizeof(*t));
if (!t)
goto out;
t->length_request = alloc_ffs_request();
if (!t->length_request)
goto free_t;
t->buf_request = alloc_ffs_request();
if (!t->length_request)
goto free_length_request;
t->in_progress = 0;
return t;
free_length_request:
free_ffs_request(t->length_request);
free_t:
free(t);
out:
return NULL;
}
/* Free the memory allocated for requests */
static void free_transfer(struct transfer *t)
{
if (!t)
return;
if (t->length_request)
free_ffs_request(t->length_request);
if (t->buf_request)
free_ffs_request(t->buf_request);
free(t);
}
/* Send chat message from device to host*/
int send_message(struct transfer *out_transfer)
{
int len;
int ret;
len = strlen(out_transfer->message.line_buf) + 1;
out_transfer->message.length = cpu_to_le16(len + 2);
out_transfer->in_progress = 1;
ret = submit_ffs_request(out_transfer->ctx,
out_transfer->length_request);
if (ret)
report_error("Unable send message");
return ret;
}
/* Receive message from host to device */
int recv_message(struct transfer *in_transfer)
{
int ret;
in_transfer->in_progress = 1;
ret = submit_ffs_request(in_transfer->ctx,
in_transfer->length_request);
if (ret)
report_error("Unable to receive message");
return ret;
}
/* Called when message has been received */
void in_complete(struct ffs_request *req)
{
int ret;
struct transfer *in_transfer = req->context;
switch (req->status)
{
case FFS_REQ_COMPLETED:
break;
case FFS_REQ_CANCELLED:
/* This means that we are closing our program */
return;
default:
report_error("Failed to receive data");
/* Just die to keep things simple */
exit(-1);
}
if (in_transfer->length_request == req)
{
/*
* We have correctly received length of message
* lets wait for rest of data.
*/
int len;
len = le16toh(in_transfer->message.length) - 2;
/*
* TODO: Set the correct request length and submit it
*
* Hints:
* - In case of error, just exit(-1) to keep things simple
* - Use in_transfer->buf_request to receive message content
*
* struct ffs_request {
* (...)
* ssize_t length;
* (...)
* };
*
* int submit_ffs_request()
*/
in_transfer->buf_request->length = len;
ret = submit_ffs_request(in_transfer->ctx,
in_transfer->buf_request);
if (ret < 0)
{
report_error("Failed to submit transfer");
/* Just die to keep things simple */
exit(-1);
}
}
else
{
/*
* We have the whole message so let's print it
* and wait for another one
*/
in_transfer->in_progress = 0;
// if (device_prompt)
// printf("<skip>\n");
printf("recv data from host> %s\n", in_transfer->message.line_buf);
// if (device_prompt)
// printf("device> ");
fflush(stdout);
ret = recv_message(in_transfer);
if (ret < 0)
{
report_error("Failed to receive message");
/* Just die to keep things simple */
exit(-1);
}
}
}
/* Called when we successfully send any message to host */
void out_complete(struct ffs_request *req)
{
int ret;
struct transfer *out_transfer = req->context;
switch (req->status)
{
case FFS_REQ_COMPLETED:
break;
case FFS_REQ_CANCELLED:
/* This means that we are closing our program */
return;
default:
report_error("Failed to send data");
/* Just die to keep things simple */
exit(-1);
}
if (out_transfer->length_request == req)
{
/*
* We have correctly send the length,
* so let's send now the data.
*/
int len;
len = le16toh(out_transfer->message.length) - 2;
/*
* TODO: Set the correct request length and submit it
*
* Hints:
* - In case of error, just exit(-1) to keep things simple
* - Use out_transfer->buf_request to send the data
*
* struct ffs_request {
* (...)
* ssize_t length;
* (...)
* };
*
* int submit_ffs_request()
*/
out_transfer->buf_request->length = len;
ret = submit_ffs_request(out_transfer->ctx,
out_transfer->buf_request);
if (ret < 0)
{
report_error("Failed submit transfer");
/* Just die to keep things simple */
exit(-1);
}
}
else
{
/*
* We have the whole message so let's print
* the prompt once again.
*/
out_transfer->in_progress = 0;
device_prompt = 1;
printf("device> ");
fflush(stdout);
/* Rest of the work will be done in do_chat() */
}
}
/* prepare one in and one out chat transfers */
int prepare_transfers(int *ep, io_context_t *ctx, int event_fd,
struct transfer **in_transfer,
struct transfer **out_transfer)
{
struct transfer *it, *ot;
/* In our chat protocol we understand IN transfer
* as receiving data, but on USB level that's are OUT
* requests as data is being transfered from host to device
*/
it = alloc_transfer();
if (!it)
goto out;
ot = alloc_transfer();
if (!ot)
goto free_it;
/*
* TODO: Fill the USB OUT requests (for chat IN transfer)
*
* Hints:
* - We are on device side so we receives the data when
* USB request direction is OUT (from host to device)
* - use ep[EP_OUT_IDX] as endpoint file descriptor
* - use it->message as buffer for receiving data
* - use 2 as length for first request
* - no matter what you will use as length in second request
* as it will be overwritten in in_complete()
* - use in_complete() as completion callback
* - use it as user data as it will be needed later
*
* void fill_ffs_request()
*/
fill_ffs_request(it->length_request, USB_DIR_OUT, ep[EP_OUT_IDX],
event_fd, (unsigned char *)&it->message.length, 2,
in_complete, it);
fill_ffs_request(it->buf_request, USB_DIR_OUT, ep[EP_OUT_IDX],
/*
* Actual length will be filled after
* receiving it from host
*/
event_fd, (unsigned char *)&it->message.line_buf, 0,
in_complete, it);
/*
* TODO: Fill the USB IN requests (for chat OUT transfer)
*
* Hints:
* - We are on device side so we send the data when
* USB request direction is IN (from device to host)
* - use ep[EP_IN_IDX] as endpoint file descriptor
* - use ot->message as buffer for receiving data
* - use 2 as length for first request
* - no matter what you will use as length in second request
* as it will be overwritten in out_complete()
* - use in_complete() as completion callback
* - use ot as user data as it will be needed later
*
* void fill_ffs_request()
*/
fill_ffs_request(ot->length_request, USB_DIR_IN, ep[EP_IN_IDX],
event_fd, (unsigned char *)&ot->message.length, 2,
out_complete, ot);
fill_ffs_request(ot->buf_request, USB_DIR_IN, ep[EP_IN_IDX],
/*
* Actual length will be filled after
* reading user input
*/
event_fd, (unsigned char *)&ot->message.line_buf, 0,
out_complete, ot);
it->ctx = ctx;
ot->ctx = ctx;
*in_transfer = it;
*out_transfer = ot;
return 0;
free_it:
free_transfer(it);
out:
return -EINVAL;
}
/* Handle events generated by kernel and provided via ep0 */
int handle_ep0(int *ep, struct transfer *in_transfer, int *connected)
{
struct usb_functionfs_event event;
int ret;
ret = read(ep[0], &event, sizeof(event));
if (!ret)
{
report_error("unable to read event from ep0");
return -EIO;
}
switch (event.type)
{
case FUNCTIONFS_SETUP:
/* stall for all setuo requests */
if (event.u.setup.bRequestType & USB_DIR_IN)
(void)write(ep[0], NULL, 0);
else
(void)read(ep[0], NULL, 0);
break;
case FUNCTIONFS_ENABLE:
*connected = 1;
printf("Chat started. You may say something or type " EXIT_COMMAND
" to exit...\n");
device_prompt = 1;
printf("device> ");
fflush(stdout);
/*
* TODO: Start receiving messages from host
*
* int recv_message()
*/
ret = recv_message(in_transfer);
if (ret < 0)
{
report_error("Unable to receive message");
return ret;
}
break;
case FUNCTIONFS_DISABLE:
*connected = 0;
break;
default:
break;
}
return 0;
}
/* main chat function */
void do_chat(int *ep, io_context_t *ctx, int event_fd)
{
struct transfer *out_transfer;
struct transfer *in_transfer;
char *buf;
int buf_size;
fd_set rfds;
int max_fd;
int ret;
int len;
int wait_for_input = 1;
int connected = 0;
/* prepare our chat transfers */
ret = prepare_transfers(ep, ctx, event_fd, &in_transfer, &out_transfer);
if (ret)
{
report_error("Unable to prepare transfers");
return;
}
/*
* We are on the device side so we use IN requests for sending
* data from device to host but still it is out transfer in our
* chat protocol
*/
buf = out_transfer->message.line_buf;
buf_size = sizeof(out_transfer->message.line_buf);
printf("Waiting for connection...\n");
fflush(stdout);
/* We cannot submit any transfer here as we may be not connected */
/* our main program loop */
while (1)
{
FD_ZERO(&rfds);
/* we wait for input only if we have free out transfer */
if (wait_for_input)
FD_SET(STDIN_FILENO, &rfds);
max_fd = STDIN_FILENO;
/*
* We should wait for events only on ep0 and eventfd
* DON'T add epX (x != 0) to poll()!
*/
FD_SET(ep[0], &rfds);
max_fd = MAX(ep[0], max_fd);
FD_SET(event_fd, &rfds);
max_fd = MAX(event_fd, max_fd);
/* we block here and wait for some events */
ret = select(max_fd + 1, &rfds, NULL, NULL, NULL);
if (ret < 0)
{
if (errno == EINTR)
continue;
report_error("Unable to use select");
goto cleanup;
;
}
/* first of all we check if we have some ep0 events */
if (FD_ISSET(ep[0], &rfds))
{
ret = handle_ep0(ep, in_transfer, &connected);
if (ret)
goto cleanup;
}
/*
* TODO: handle aio events if event_fd
* is ready for reading
*
* FD_ISSET()
* int handle_events()
*/
if (FD_ISSET(event_fd, &rfds))
{
ret = handle_events(ctx, event_fd);
if (ret)
goto cleanup;
}
if (!connected)
continue;
}
cleanup:
/* we don't care if any of them is active, just cancel them */
cancel_ffs_request(ctx, in_transfer->length_request);
cancel_ffs_request(ctx, in_transfer->buf_request);
cancel_ffs_request(ctx, out_transfer->length_request);
cancel_ffs_request(ctx, out_transfer->buf_request);
free_transfer(in_transfer);
free_transfer(out_transfer);
}
int main(int argc, char **argv)
{
int ep[3];
int event_fd;
io_context_t ctx;
int ret;
/* Check if we received ffs mount point */
if (argc != 2)
{
printf("ffs directory not specified!\n");
return 1;
}
ret = prepare_ffs(argv[1], ep);
if (ret < 0)
{
report_error("Unable to prepare ffs: %d", ret);
goto out;
}
while (1)
{
sleep(1);
}
close_desc:
cleanup_ffs(ep);
out:
return ret;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment