mirror of
https://github.com/tillitis/tillitis-key1.git
synced 2025-04-26 18:09:16 -04:00
351 lines
7.9 KiB
C
351 lines
7.9 KiB
C
// SPDX-FileCopyrightText: 2025 Tillitis AB <tillitis.se>
|
|
// SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
#include <stdint.h>
|
|
#include <tkey/assert.h>
|
|
#include <tkey/debug.h>
|
|
#include <tkey/lib.h>
|
|
#include <tkey/proto.h>
|
|
#include <tkey/tk1_mem.h>
|
|
|
|
// Maximum payload size sent over the USB Mode Protocol.
|
|
//
|
|
// USB Mode Protocol:
|
|
// 1 byte mode
|
|
// 1 byte length
|
|
//
|
|
// Our USB Mode Protocol packets has room for 255 bytes according to
|
|
// the header but we send at most 64 bytes of payload + the 2 byte
|
|
// header. The header is removed in the USB controller and the maximum
|
|
// payload fits in a single USB frame on the other side.
|
|
#define USBMODE_PACKET_SIZE 64
|
|
|
|
static void hex(uint8_t buf[2], const uint8_t c);
|
|
static int discard(size_t nbytes);
|
|
static uint8_t readbyte(void);
|
|
static void writebyte(uint8_t b);
|
|
|
|
struct usb_mode {
|
|
enum ioend endpoint; // Current USB endpoint with data
|
|
uint8_t len; // Data available in from current USB mode.
|
|
};
|
|
|
|
static struct usb_mode cur_endpoint = {
|
|
IO_NONE,
|
|
0,
|
|
};
|
|
|
|
// clang-format off
|
|
static volatile uint32_t* const can_rx = (volatile uint32_t *)TK1_MMIO_UART_RX_STATUS;
|
|
static volatile uint32_t* const rx = (volatile uint32_t *)TK1_MMIO_UART_RX_DATA;
|
|
static volatile uint32_t* const can_tx = (volatile uint32_t *)TK1_MMIO_UART_TX_STATUS;
|
|
static volatile uint32_t* const tx = (volatile uint32_t *)TK1_MMIO_UART_TX_DATA;
|
|
static volatile uint8_t* const debugtx = (volatile uint8_t *)TK1_MMIO_QEMU_DEBUG;
|
|
// clang-format on
|
|
|
|
// writebyte blockingly writes byte b to UART
|
|
static void writebyte(uint8_t b)
|
|
{
|
|
for (;;) {
|
|
if (*can_tx) {
|
|
*tx = b;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// write_with_header writes nbytes of buf to UART with a USB Mode
|
|
// Protocol header telling the receiver about the mode and length.
|
|
static void write_with_header(enum ioend dest, const uint8_t *buf,
|
|
size_t nbytes)
|
|
{
|
|
// USB Mode Protocol header:
|
|
// 1 byte mode
|
|
// 1 byte length
|
|
|
|
writebyte(dest);
|
|
writebyte(nbytes);
|
|
|
|
for (int i = 0; i < nbytes; i++) {
|
|
writebyte(buf[i]);
|
|
}
|
|
}
|
|
|
|
// write blockingly writes nbytes bytes of data from buf to dest which
|
|
// is either:
|
|
//
|
|
// - IO_QEMU: QEMU debug port
|
|
//
|
|
// - IO_UART: Low-level UART access, no USB Mode Header added.
|
|
//
|
|
// - IO_CDC: Through the UART for the CDC endpoint, with header.
|
|
//
|
|
// - IO_FIDO: Through the UART for the FIDO endpoint, with header.
|
|
//
|
|
// - IO_DEBUG: Through the UART for the DEBUG HID endpoint, with
|
|
// header.
|
|
void write(enum ioend dest, const uint8_t *buf, size_t nbytes)
|
|
{
|
|
if (dest == IO_QEMU) {
|
|
for (int i = 0; i < nbytes; i++) {
|
|
*debugtx = buf[i];
|
|
}
|
|
|
|
return;
|
|
} else if (dest == IO_UART) {
|
|
for (int i = 0; i < nbytes; i++) {
|
|
writebyte(buf[i]);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
while (nbytes > 0) {
|
|
// We split the data into chunks that will fit in the
|
|
// USB Mode Protocol and fits neatly in the USB frames
|
|
// on the other side of the USB controller.
|
|
uint8_t len =
|
|
nbytes < USBMODE_PACKET_SIZE ? nbytes : USBMODE_PACKET_SIZE;
|
|
|
|
write_with_header(dest, (const uint8_t *)buf, len);
|
|
|
|
buf += len;
|
|
nbytes -= len;
|
|
}
|
|
}
|
|
|
|
// readbyte reads a byte from UART and returns it. Blocking.
|
|
static uint8_t readbyte(void)
|
|
{
|
|
for (;;) {
|
|
if (*can_rx) {
|
|
return *rx;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// read reads into buf of size bufsize from UART, nbytes or less, from
|
|
// the current USB endpoint. It doesn't block.
|
|
//
|
|
// Returns the number of bytes read. Empty data returns 0.
|
|
int read(enum ioend src, uint8_t *buf, size_t bufsize, size_t nbytes)
|
|
{
|
|
if (buf == NULL || nbytes > bufsize) {
|
|
return -1;
|
|
}
|
|
|
|
if (src == IO_NONE || src == IO_UART || src == IO_QEMU) {
|
|
// Destination only endpoints
|
|
return -1;
|
|
}
|
|
|
|
if (src != cur_endpoint.endpoint) {
|
|
// No data for this source available right now.
|
|
return 0;
|
|
}
|
|
|
|
int n = 0;
|
|
|
|
for (n = 0; n < nbytes; n++) {
|
|
buf[n] = readbyte();
|
|
cur_endpoint.len--;
|
|
}
|
|
|
|
return n;
|
|
}
|
|
|
|
// uart_read reads blockingly into buf o size bufsize from UART nbytes
|
|
// bytes.
|
|
//
|
|
// Returns negative on error.
|
|
int uart_read(uint8_t *buf, size_t bufsize, size_t nbytes)
|
|
{
|
|
if (nbytes > bufsize) {
|
|
return -1;
|
|
}
|
|
|
|
for (int n = 0; n < nbytes; n++) {
|
|
buf[n] = readbyte();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// discard nbytes of what's available.
|
|
//
|
|
// Returns how many bytes were discarded.
|
|
static int discard(size_t nbytes)
|
|
{
|
|
int n = 0;
|
|
uint8_t len = nbytes < cur_endpoint.len ? nbytes : cur_endpoint.len;
|
|
|
|
for (n = 0; n < len; n++) {
|
|
(void)readbyte();
|
|
cur_endpoint.len--;
|
|
}
|
|
|
|
return n;
|
|
}
|
|
|
|
// readselect blocks and returns when there is something readable from
|
|
// some mode.
|
|
//
|
|
// Use like this:
|
|
//
|
|
// readselect(IO_CDC|IO_FIDO, &endpoint, &len)
|
|
//
|
|
// to wait for some data from either the CDC or the FIDO endpoint.
|
|
//
|
|
// NOTE WELL: You need to call readselect() first, before doing any
|
|
// calls to read().
|
|
//
|
|
// Only endpoints available for read are:
|
|
//
|
|
// - IO_DEBUG
|
|
// - IO_CDC
|
|
// - IO_FIDO
|
|
//
|
|
// If you need blocking low-level UART reads, use uart_read() instead.
|
|
//
|
|
// Sets endpoint of the first endpoint in the bitmask with data
|
|
// available. Indicates how many bytes available in len.
|
|
//
|
|
// Returns non-zero on error.
|
|
int readselect(int bitmask, enum ioend *endpoint, uint8_t *len)
|
|
{
|
|
if ((bitmask & IO_UART) || (bitmask & IO_QEMU)) {
|
|
// Not possible to use readselect() on these
|
|
// endpoints.
|
|
return -1;
|
|
}
|
|
|
|
for (;;) {
|
|
// Check what is in the current UART buffer.
|
|
//
|
|
// - If nothing known, block until something comes along.
|
|
//
|
|
// - If not in bitmask, discard the data available
|
|
// from that endpoint.
|
|
//
|
|
// - If in the bitmask, return the first endpoint with
|
|
// data available and indicate how much data in len.
|
|
if (cur_endpoint.len == 0) {
|
|
// Read USB Mode Protocol header:
|
|
// 1 byte mode
|
|
// 1 byte length
|
|
cur_endpoint.endpoint = readbyte();
|
|
cur_endpoint.len = readbyte();
|
|
}
|
|
|
|
*len = cur_endpoint.len;
|
|
|
|
if (cur_endpoint.endpoint & bitmask) {
|
|
*endpoint = cur_endpoint.endpoint;
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Not the USB endpoint caller asked for. Discard the
|
|
// rest from this endpoint.
|
|
if (discard(*len) != *len) {
|
|
// We couldn't discard what the USB Mode
|
|
// Protocol itself reported was available!
|
|
// Something's fishy. Halt.
|
|
assert(1 == 2);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void putchar(enum ioend dest, const uint8_t ch)
|
|
{
|
|
write(dest, &ch, 1);
|
|
}
|
|
|
|
static void hex(uint8_t buf[2], const uint8_t c)
|
|
{
|
|
unsigned int upper = (c >> 4) & 0xf;
|
|
unsigned int lower = c & 0xf;
|
|
|
|
buf[0] = upper < 10 ? '0' + upper : 'a' - 10 + upper;
|
|
buf[1] = lower < 10 ? '0' + lower : 'a' - 10 + lower;
|
|
}
|
|
|
|
void puthex(enum ioend dest, const uint8_t c)
|
|
{
|
|
uint8_t hexbuf[2] = {0};
|
|
|
|
hex(hexbuf, c);
|
|
write(dest, hexbuf, 2);
|
|
}
|
|
|
|
// Size of of a maximum integer in hex text format
|
|
#define INTBUFSIZE 10
|
|
|
|
void putinthex(enum ioend dest, const uint32_t n)
|
|
{
|
|
uint8_t buf[INTBUFSIZE] = {0};
|
|
uint8_t hexbuf[2] = {0};
|
|
uint8_t *intbuf = (uint8_t *)&n;
|
|
int j = 0;
|
|
|
|
buf[j++] = '0';
|
|
buf[j++] = 'x';
|
|
|
|
for (int i = 3; i > -1; i--) {
|
|
hex(hexbuf, intbuf[i]);
|
|
buf[j++] = hexbuf[0];
|
|
buf[j++] = hexbuf[1];
|
|
}
|
|
|
|
write(dest, buf, INTBUFSIZE);
|
|
}
|
|
|
|
void puts(enum ioend dest, const char *s)
|
|
{
|
|
write(dest, (const uint8_t *)s, strlen(s));
|
|
}
|
|
|
|
// Size of a hex row: Contains 16 bytes where each byte is printed as
|
|
// 3 characters (hex + hex + space). Every row ends with newline or at
|
|
// most CR+LF.
|
|
#define FULLROW (16 * 3)
|
|
#define ROWBUFSIZE (FULLROW + 2)
|
|
|
|
void hexdump(enum ioend dest, void *buf, int len)
|
|
{
|
|
uint8_t rowbuf[ROWBUFSIZE] = {0};
|
|
uint8_t hexbuf[2] = {0};
|
|
uint8_t *byte_buf = (uint8_t *)buf;
|
|
|
|
int rowpos = 0;
|
|
for (int i = 0; i < len; i++) {
|
|
hex(hexbuf, byte_buf[i]);
|
|
rowbuf[rowpos++] = hexbuf[0];
|
|
rowbuf[rowpos++] = hexbuf[1];
|
|
rowbuf[rowpos++] = ' ';
|
|
|
|
// If the row is full, print it now.
|
|
if (rowpos == FULLROW) {
|
|
if (dest == IO_CDC) {
|
|
rowbuf[rowpos++] = '\r';
|
|
}
|
|
rowbuf[rowpos++] = '\n';
|
|
write(dest, rowbuf, rowpos);
|
|
rowpos = 0;
|
|
}
|
|
}
|
|
|
|
// If final row wasn't full, print it now.
|
|
if (rowpos != 0) {
|
|
if (dest == IO_CDC) {
|
|
rowbuf[rowpos++] = '\r';
|
|
}
|
|
rowbuf[rowpos++] = '\n';
|
|
write(dest, rowbuf, rowpos);
|
|
}
|
|
}
|