From 4f08580358c83ca300cb6fc1165ef5cb2c493d2c Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 14 Oct 2024 23:37:54 -0500 Subject: [PATCH] Very hacky first attempt at usermod ech341 --- src/main.cpp | 4 +- src/mesh/SX126xInterface.cpp | 2 + src/platform/portduino/PortduinoGlue.cpp | 4 +- src/platform/portduino/USBHal.h | 244 +++++++++++ src/platform/portduino/libpinedio-usb.c | 536 +++++++++++++++++++++++ src/platform/portduino/libpinedio-usb.h | 71 +++ 6 files changed, 857 insertions(+), 4 deletions(-) create mode 100644 src/platform/portduino/USBHal.h create mode 100644 src/platform/portduino/libpinedio-usb.c create mode 100644 src/platform/portduino/libpinedio-usb.h diff --git a/src/main.cpp b/src/main.cpp index 45c6498ce..bd88f47aa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -92,6 +92,7 @@ NRF52Bluetooth *nrf52Bluetooth = nullptr; #include "linux/LinuxHardwareI2C.h" #include "mesh/raspihttp/PiWebServer.h" #include "platform/portduino/PortduinoGlue.h" +#include "platform/portduino/USBHal.h" #include #include #include @@ -822,8 +823,7 @@ void setup() if (settingsMap[use_sx1262]) { if (!rIf) { LOG_DEBUG("Attempting to activate sx1262 radio on SPI port %s\n", settingsStrings[spidev].c_str()); - LockingArduinoHal *RadioLibHAL = - new LockingArduinoHal(SPI, spiSettings, (settingsMap[ch341Quirk] ? settingsMap[busy] : RADIOLIB_NC)); + Ch341Hal *RadioLibHAL = new Ch341Hal(0); rIf = new SX1262Interface((LockingArduinoHal *)RadioLibHAL, settingsMap[cs], settingsMap[irq], settingsMap[reset], settingsMap[busy]); if (!rIf->init()) { diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 924cdfa9f..060e29412 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -57,6 +57,8 @@ template bool SX126xInterface::init() digitalWrite(settingsMap[sx126x_ant_sw], HIGH); pinMode(settingsMap[sx126x_ant_sw], OUTPUT); } + // lora.XTAL = true; + // lora.standbyXOSC = true; // FIXME: correct logic to default to not using TCXO if no voltage is specified for SX126X_DIO3_TCXO_VOLTAGE #elif !defined(SX126X_DIO3_TCXO_VOLTAGE) float tcxoVoltage = diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index e56016a5b..92bf7c86e 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -311,7 +311,7 @@ void portduinoSetup() // Need to bind all the configured GPIO pins so they're not simulated // TODO: Can we do this in the for loop above? // TODO: If one of these fails, we should log and terminate - if (settingsMap.count(cs) > 0 && settingsMap[cs] != RADIOLIB_NC) { + /*if (settingsMap.count(cs) > 0 && settingsMap[cs] != RADIOLIB_NC) { if (initGPIOPin(settingsMap[cs], gpioChipName) != ERRNO_OK) { settingsMap[cs] = RADIOLIB_NC; } @@ -350,7 +350,7 @@ void portduinoSetup() if (initGPIOPin(settingsMap[txen], gpioChipName) != ERRNO_OK) { settingsMap[txen] = RADIOLIB_NC; } - } + } */ if (settingsMap[displayPanel] != no_screen) { if (settingsMap[displayCS] > 0) diff --git a/src/platform/portduino/USBHal.h b/src/platform/portduino/USBHal.h new file mode 100644 index 000000000..06c9cb217 --- /dev/null +++ b/src/platform/portduino/USBHal.h @@ -0,0 +1,244 @@ +#ifndef PI_HAL_LGPIO_H +#define PI_HAL_LGPIO_H + +// include RadioLib +#include +#include +#include + +// include the library for Raspberry GPIO pins + +#define PI_RISING (PINEDIO_INT_MODE_RISING) +#define PI_FALLING (PINEDIO_INT_MODE_FALLING) +#define PI_INPUT (0) +#define PI_OUTPUT (1) +#define PI_LOW (0) +#define PI_HIGH (1) +#define PI_MAX_USER_GPIO (31) + +#define CH341_PIN_CS (101) +#define CH341_PIN_IRQ (102) + +// forward declaration of alert handler that will be used to emulate interrupts +// static void lgpioAlertHandler(int num_alerts, lgGpioAlert_p alerts, void *userdata); + +// the HAL must inherit from the base RadioLibHal class +// and implement all of its virtual methods +class Ch341Hal : public RadioLibHal +{ + public: + // default constructor - initializes the base HAL and any needed private members + Ch341Hal(uint8_t spiChannel, uint32_t spiSpeed = 2000000, uint8_t spiDevice = 0, uint8_t gpioDevice = 0) + : RadioLibHal(PI_INPUT, PI_OUTPUT, PI_LOW, PI_HIGH, PI_RISING, PI_FALLING) + { + } + + void init() override + { + // now the SPI + spiBegin(); + } + + void term() override + { + // stop the SPI + spiEnd(); + } + + // GPIO-related methods (pinMode, digitalWrite etc.) should check + // RADIOLIB_NC as an alias for non-connected pins + void pinMode(uint32_t pin, uint32_t mode) override + { + if (pin == RADIOLIB_NC) { + return; + } + if (pin == CH341_PIN_CS || pin == CH341_PIN_IRQ) { + return; + } + fprintf(stderr, "pinMode for pin %d and mode %d is not supported!\n", pin, mode); + } + + void digitalWrite(uint32_t pin, uint32_t value) override + { + if (pin == RADIOLIB_NC) { + return; + } + if (pin == CH341_PIN_CS) { + pinedio_set_cs(&pinedio, value == 0); + return; + } + fprintf(stderr, "digitalWrite for pin %d is not supported!\n", pin); + } + + uint32_t digitalRead(uint32_t pin) override + { + if (pin == RADIOLIB_NC) { + return 0; + } + if (pin == CH341_PIN_IRQ) { + + return pinedio_get_irq_state(&pinedio); + } + fprintf(stderr, "digitalRead for pin %d is not supported!\n", pin); + return 0; + } + + void attachInterrupt(uint32_t interruptNum, void (*interruptCb)(void), uint32_t mode) override + { + if ((interruptNum == RADIOLIB_NC) || (interruptNum > PI_MAX_USER_GPIO)) { + return; + } + + pinedio_attach_interrupt(&this->pinedio, (pinedio_int_pin)interruptNum, (pinedio_int_mode)mode, interruptCb); + + // set lgpio alert callback + // int result = lgGpioClaimAlert(_gpioHandle, 0, mode, interruptNum, -1); + // if(result < 0) { + // fprintf(stderr, "Could not claim pin %" PRIu32 " for alert: %s\n", interruptNum, lguErrorText(result)); + // return; + // } + + // // enable emulated interrupt + // interruptEnabled[interruptNum] = true; + // interruptModes[interruptNum] = mode; + // interruptCallbacks[interruptNum] = interruptCb; + + // lgGpioSetAlertsFunc(_gpioHandle, interruptNum, lgpioAlertHandler, (void *)this); + } + + void detachInterrupt(uint32_t interruptNum) override + { + if ((interruptNum == RADIOLIB_NC) || (interruptNum > PI_MAX_USER_GPIO)) { + return; + } + + pinedio_deattach_interrupt(&this->pinedio, (pinedio_int_pin)interruptNum); + + // // clear emulated interrupt + // interruptEnabled[interruptNum] = false; + // interruptModes[interruptNum] = 0; + // interruptCallbacks[interruptNum] = NULL; + + // disable lgpio alert callback + // lgGpioFree(_gpioHandle, interruptNum); + // lgGpioSetAlertsFunc(_gpioHandle, interruptNum, NULL, NULL); + } + + void delay(unsigned long ms) override + { + if (ms == 0) { + sched_yield(); + return; + } + + usleep(ms * 1000); + // lguSleep(ms / 1000.0); + } + + void delayMicroseconds(unsigned long us) override + { + if (us == 0) { + sched_yield(); + return; + } + usleep(us); + + // lguSleep(us / 1000000.0); + } + + void yield() override { sched_yield(); } + + unsigned long millis() override + { + // uint32_t time = lguTimestamp() / 1000000UL; + // return time; + struct timeval tv; + gettimeofday(&tv, NULL); + return (tv.tv_sec * 1000ULL) + (tv.tv_usec / 1000ULL); + } + + unsigned long micros() override + { + // uint32_t time = lguTimestamp() / 1000UL; + // return time; + struct timeval tv; + gettimeofday(&tv, NULL); + return (tv.tv_sec * 1000000ULL) + tv.tv_usec; + } + + long pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) override + { + fprintf(stderr, "pulseIn for pin %d is not supported!\n", pin); + } + + void spiBegin() + { + if (!pinedio_is_init) { + int32_t ret = pinedio_init(&pinedio, NULL); + if (ret != 0) { + fprintf(stderr, "Could not open SPI: %d\n", ret); + } else { + pinedio_is_init = true; + pinedio_set_option(&pinedio, PINEDIO_OPTION_AUTO_CS, 0); + } + } + } + + void spiBeginTransaction() {} + + void spiTransfer(uint8_t *out, size_t len, uint8_t *in) + { + int32_t result = pinedio_transceive(&this->pinedio, out, in, len); + // int result = lgSpiXfer(_spiHandle, (char *)out, (char*)in, len); + if (result < 0) { + fprintf(stderr, "Could not perform SPI transfer: %d\n", result); + } + } + + void spiEndTransaction() {} + + void spiEnd() + { + if (pinedio_is_init) { + pinedio_deinit(&pinedio); + pinedio_is_init = false; + } + } + +#if 0 + void tone(uint32_t pin, unsigned int frequency, unsigned long duration = 0) { + lgTxPwm(_gpioHandle, pin, frequency, 50, 0, duration); + } + + void noTone(uint32_t pin) { + lgTxPwm(_gpioHandle, pin, 0, 0, 0, 0); + } +#endif + + private: + // the HAL can contain any additional private members + pinedio_inst pinedio; + bool pinedio_is_init = false; +}; + +#if 0 +// this handler emulates interrupts +static void lgpioAlertHandler(int num_alerts, lgGpioAlert_p alerts, void *userdata) { + if(!userdata) + return; + + // Ch341Hal instance is passed via the user data + Ch341Hal* hal = (Ch341Hal*)userdata; + + // check the interrupt is enabled, the level matches and a callback exists + for(lgGpioAlert_t *alert = alerts; alert < (alerts + num_alerts); alert++) { + if((hal->interruptEnabled[alert->report.gpio]) && + (hal->interruptModes[alert->report.gpio] == alert->report.level) && + (hal->interruptCallbacks[alert->report.gpio])) { + hal->interruptCallbacks[alert->report.gpio](); + } + } +} +#endif + +#endif \ No newline at end of file diff --git a/src/platform/portduino/libpinedio-usb.c b/src/platform/portduino/libpinedio-usb.c new file mode 100644 index 000000000..f26432d73 --- /dev/null +++ b/src/platform/portduino/libpinedio-usb.c @@ -0,0 +1,536 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Copyright (C) 2024 Marek Kraus + * + * This code is heavily based on ch341a_spi.c from the flashrom project. + * The plan is to rework parts of code, but until that, original developers deserves to be mentioned. + * Copyright (C) 2011 asbokid + * Copyright (C) 2014 Pluto Yang + * Copyright (C) 2015-2016 Stefan Tauner + * Copyright (C) 2015 Urja Rannikko + */ + +#include "libpinedio-usb.h" +#include +#include +#include + +#if 0 +#define pinedio_mutex_lock(...) \ + { \ + printf("Locking %s\n", __func__); \ + pthread_mutex_lock(__VA_ARGS__); \ + } +#define pinedio_mutex_unlock(...) \ + { \ + printf("Unlocking %s\n", __func__); \ + pthread_mutex_unlock(__VA_ARGS__); \ + } +#else +#define pinedio_mutex_lock(...) pthread_mutex_lock(__VA_ARGS__); +#define pinedio_mutex_unlock(...) pthread_mutex_unlock(__VA_ARGS__); +#endif + +#define CH341_USB_TIMEOUT 1000 +#define CH341_WRITE_EP 0x02 +#define CH341_READ_EP 0x82 +#define CH341_PACKET_LENGTH 0x20 + +#define CH341_CMD_SPI_STREAM 0xA8 + +#define CH341_CMD_UIO_STREAM 0xAB +#define CH341_CMD_UIO_STM_OUT 0x80 +#define CH341_CMD_UIO_STM_DIR 0x40 +#define CH341_CMD_UIO_STM_END 0x20 + +#define MIN(x, y) (((x) < (y)) ? (x) : (y)) + +enum trans_state { TRANS_ACTIVE = -2, TRANS_ERR = -1, TRANS_IDLE = 0 }; + +static void platform_sleep(uint32_t msecs) +{ +#ifdef __WIN32 + Sleep(msecs); +#else + usleep(msecs * 1000); +#endif +} + +static void cb_common(const char *func, struct libusb_transfer *transfer) +{ + int *transfer_cnt = (int *)transfer->user_data; + + if (transfer->status == LIBUSB_TRANSFER_CANCELLED) { + // Silently ACK and exit. + *transfer_cnt = TRANS_IDLE; + return; + } + + if (transfer->status != LIBUSB_TRANSFER_COMPLETED) { + fprintf(stderr, "%s: error: %s\n", func, libusb_error_name(transfer->status)); + *transfer_cnt = TRANS_ERR; + } else { + *transfer_cnt = transfer->actual_length; + } +} + +// callback for bulk out async transfer +static void LIBUSB_CALL cb_out(struct libusb_transfer *transfer) +{ + cb_common(__func__, transfer); +} + +// callback for bulk in async transfer +static void LIBUSB_CALL cb_in(struct libusb_transfer *transfer) +{ + cb_common(__func__, transfer); +} + +static int32_t usb_transfer(struct pinedio_inst *inst, const char *func, unsigned int writecnt, unsigned int readcnt, + const uint8_t *writearr, uint8_t *readarr, bool lock) +{ + int state_out = TRANS_IDLE; + inst->transfer_out->buffer = (uint8_t *)writearr; + inst->transfer_out->length = writecnt; + inst->transfer_out->user_data = &state_out; + + if (lock) { + pinedio_mutex_lock(&inst->usb_access_mutex); + } + + /* Schedule write first */ + if (writecnt > 0) { + state_out = TRANS_ACTIVE; + int ret = libusb_submit_transfer(inst->transfer_out); + if (ret) { + fprintf(stderr, "%s: failed to submit OUT transfer: %s\n", func, libusb_error_name(ret)); + state_out = TRANS_ERR; + goto err; + } + } + + /* Handle all asynchronous packets as long as we have stuff to write or read. The write(s) simply need + * to complete but we need to scheduling reads as long as we are not done. */ + unsigned int free_idx = 0; /* The IN transfer we expect to be free next. */ + unsigned int in_idx = 0; /* The IN transfer we expect to be completed next. */ + unsigned int in_done = 0; + unsigned int in_active = 0; + unsigned int out_done = 0; + uint8_t *in_buf = readarr; + int state_in[USB_IN_TRANSFERS] = {0}; + do { + /* Schedule new reads as long as there are free transfers and unscheduled bytes to read. */ + while ((in_done + in_active) < readcnt && state_in[free_idx] == TRANS_IDLE) { + unsigned int cur_todo = MIN(CH341_PACKET_LENGTH - 1, readcnt - in_done - in_active); + inst->transfer_ins[free_idx]->length = cur_todo; + inst->transfer_ins[free_idx]->buffer = in_buf; + inst->transfer_ins[free_idx]->user_data = &state_in[free_idx]; + int ret = libusb_submit_transfer(inst->transfer_ins[free_idx]); + if (ret) { + state_in[free_idx] = TRANS_ERR; + fprintf(stderr, "%s: failed to submit IN transfer: %s\n", func, libusb_error_name(ret)); + goto err; + } + in_buf += cur_todo; + in_active += cur_todo; + state_in[free_idx] = TRANS_ACTIVE; + free_idx = (free_idx + 1) % USB_IN_TRANSFERS; /* Increment (and wrap around). */ + } + + /* Actually get some work done. */ + libusb_handle_events_timeout(NULL, &(struct timeval){1, 0}); + + /* Check for the write */ + if (out_done < writecnt) { + if (state_out == TRANS_ERR) { + goto err; + } else if (state_out > 0) { + out_done += state_out; + state_out = TRANS_IDLE; + } + } + /* Check for completed transfers. */ + while (state_in[in_idx] != TRANS_IDLE && state_in[in_idx] != TRANS_ACTIVE) { + if (state_in[in_idx] == TRANS_ERR) { + goto err; + } + /* If a transfer is done, record the number of bytes read and reuse it later. */ + in_done += state_in[in_idx]; + in_active -= state_in[in_idx]; + state_in[in_idx] = TRANS_IDLE; + in_idx = (in_idx + 1) % USB_IN_TRANSFERS; /* Increment (and wrap around). */ + } + } while ((out_done < writecnt) || (in_done < readcnt)); + + if (lock) { + pinedio_mutex_unlock(&inst->usb_access_mutex); + } + return 0; +err: + /* Clean up on errors. */ + fprintf(stderr, "%s: Failed to %s %d bytes\n", func, (state_out == TRANS_ERR) ? "write" : "read", + (state_out == TRANS_ERR) ? writecnt : readcnt); + /* First, we must cancel any ongoing requests and wait for them to be canceled. */ + if ((writecnt > 0) && (state_out == TRANS_ACTIVE)) { + if (libusb_cancel_transfer(inst->transfer_out) != 0) + state_out = TRANS_ERR; + } + if (readcnt > 0) { + unsigned int i; + for (i = 0; i < USB_IN_TRANSFERS; i++) { + if (state_in[i] == TRANS_ACTIVE) + if (libusb_cancel_transfer(inst->transfer_ins[i]) != 0) + state_in[i] = TRANS_ERR; + } + } + + /* Wait for cancellations to complete. */ + while (1) { + bool finished = true; + if ((writecnt > 0) && (state_out == TRANS_ACTIVE)) + finished = false; + if (readcnt > 0) { + unsigned int i; + for (i = 0; i < USB_IN_TRANSFERS; i++) { + if (state_in[i] == TRANS_ACTIVE) + finished = false; + } + } + if (finished) + break; + libusb_handle_events_timeout(NULL, &(struct timeval){1, 0}); + } + if (lock) { + pinedio_mutex_unlock(&inst->usb_access_mutex); + } + return -1; +} + +static uint8_t reverse_byte(uint8_t x) +{ + x = ((x >> 1) & 0x55) | ((x << 1) & 0xaa); + x = ((x >> 2) & 0x33) | ((x << 2) & 0xcc); + x = ((x >> 4) & 0x0f) | ((x << 4) & 0xf0); + + return x; +} + +int32_t pinedio_set_cs(struct pinedio_inst *inst, bool active) +{ + uint8_t buf[] = {CH341_CMD_UIO_STREAM, CH341_CMD_UIO_STM_DIR | 0x3f, CH341_CMD_UIO_STM_OUT | (active ? 0x36 : 0x37), + CH341_CMD_UIO_STM_END}; + + int32_t ret = usb_transfer(inst, __func__, sizeof(buf), 0, buf, NULL, true); + if (ret < 0) { + printf("Failed to set CS pin.\n"); + } + return ret; +} + +int32_t pinedio_write_read(struct pinedio_inst *inst, uint8_t *writearr, uint32_t writecnt, uint8_t *readarr, uint32_t readcnt) +{ + /* How many packets ... */ + const size_t packets = (writecnt + readcnt + CH341_PACKET_LENGTH - 2) / (CH341_PACKET_LENGTH - 1); + + /* We pluck CS/timeout handling into the first packet thus we need to allocate one extra package. */ + uint8_t wbuf[packets * CH341_PACKET_LENGTH]; + uint8_t rbuf[writecnt + readcnt]; + /* Initialize the write buffer to zero to prevent writing random stack contents to device. */ + memset(wbuf, 0, CH341_PACKET_LENGTH); + + uint8_t *ptr = wbuf; + /* CS usage is optimized by doing both transitions in one packet. + * Final transition to deselected state is in the pin disable. */ + // pluck_cs(ptr, &data->stored_delay_us); + if (inst->options[PINEDIO_OPTION_AUTO_CS]) { + pinedio_set_cs(inst, true); + } + unsigned int write_left = writecnt; + unsigned int read_left = readcnt; + unsigned int p; + for (p = 0; p < packets; p++) { + unsigned int write_now = MIN(CH341_PACKET_LENGTH - 1, write_left); + unsigned int read_now = MIN((CH341_PACKET_LENGTH - 1) - write_now, read_left); + ptr = &wbuf[p * CH341_PACKET_LENGTH]; + *ptr++ = CH341_CMD_SPI_STREAM; + unsigned int i; + for (i = 0; i < write_now; ++i) + *ptr++ = reverse_byte(*writearr++); + if (read_now) { + memset(ptr, 0xFF, read_now); + read_left -= read_now; + } + write_left -= write_now; + } + + int32_t ret = usb_transfer(inst, __func__, packets + writecnt + readcnt, writecnt + readcnt, wbuf, rbuf, true); + if (inst->options[PINEDIO_OPTION_AUTO_CS]) { + pinedio_set_cs(inst, false); + } + if (ret < 0) + return -1; + + unsigned int i; + for (i = 0; i < readcnt; i++) { + *readarr++ = reverse_byte(rbuf[writecnt + i]); + } + + return 0; +} + +int32_t pinedio_transceive(struct pinedio_inst *inst, uint8_t *write_buf, uint8_t *read_buf, uint32_t count) +{ + const size_t packets = (count + CH341_PACKET_LENGTH - 2) / (CH341_PACKET_LENGTH - 1); + + uint8_t wbuf[packets * CH341_PACKET_LENGTH]; + + uint8_t *ptr = wbuf; + if (inst->options[PINEDIO_OPTION_AUTO_CS]) { + pinedio_set_cs(inst, true); + } + unsigned int write_left = count; + unsigned int read_left = count; + unsigned int p; + for (p = 0; p < packets; p++) { + unsigned int write_now = MIN(CH341_PACKET_LENGTH - 1, write_left); + unsigned int read_now = MIN((CH341_PACKET_LENGTH - 1) - write_now, read_left); + ptr = &wbuf[p * CH341_PACKET_LENGTH]; + *ptr++ = CH341_CMD_SPI_STREAM; + unsigned int i; + for (i = 0; i < write_now; ++i) + *ptr++ = reverse_byte(*write_buf++); + if (read_now) { + memset(ptr, 0xFF, read_now); + read_left -= read_now; + } + write_left -= write_now; + } + + int32_t ret = usb_transfer(inst, __func__, packets + count, count, wbuf, read_buf, true); + if (inst->options[PINEDIO_OPTION_AUTO_CS]) { + pinedio_set_cs(inst, false); + } + if (ret < 0) + return -1; + + unsigned int i; + for (i = 0; i < count; i++) { + *read_buf++ = reverse_byte(*read_buf); + } + + return 0; +} + +int32_t pinedio_init(struct pinedio_inst *inst, void *driver) +{ + int32_t ret; + inst->int_running_cnt = 0; + inst->pin_poll_thread_exit = false; + for (int i = 0; i < PINEDIO_INT_PIN_MAX; i++) { + inst->interrupts[i].callback = NULL; + } + + inst->options[PINEDIO_OPTION_AUTO_CS] = 1; + + ret = pthread_mutex_init(&inst->usb_access_mutex, NULL); + if (ret != 0) { + fprintf(stderr, "Failed to initialize mutex, res: %d.\n", ret); + return -1; + } + + ret = libusb_init(NULL); + if (ret < 0) { + fprintf(stderr, "Couldn't initialize libusb!\n"); + return -1; + } + + libusb_set_option(NULL, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_INFO); + uint16_t vid = 0x1A86; + uint16_t pid = 0x5512; + inst->handle = libusb_open_device_with_vid_pid(NULL, vid, pid); + if (inst->handle == NULL) { + // TODO: Rework this so we can receive error and print it. + fprintf(stderr, "Couldn't open LoRa Adapator device.\n"); + return -2; + } + +#ifdef __linux__ + // On Windows, driver needs to be replaced manually by Zadig + ret = libusb_detach_kernel_driver(inst->handle, 0); + if (ret != 0 && ret != LIBUSB_ERROR_NOT_FOUND) { + fprintf(stderr, "Cannot detach the existing USB driver. Claiming the interface may fail: %s\n", libusb_error_name(ret)); + } +#endif + + ret = libusb_claim_interface(inst->handle, 0); + if (ret != 0) { + fprintf(stderr, "Failed to claim interface 0: %s\n", libusb_error_name(ret)); + goto deinit_on_error; + } + + // Allocate and pre-fill transfer structures. + inst->transfer_out = libusb_alloc_transfer(0); + if (!inst->transfer_out) { + fprintf(stderr, "Failed to alloc libusb OUT transfer.\n"); + goto deinit_on_error; + } + for (int i = 0; i < USB_IN_TRANSFERS; i++) { + inst->transfer_ins[i] = libusb_alloc_transfer(0); + if (inst->transfer_ins[i] == NULL) { + fprintf(stderr, "Failed to alloc libusb IN transfer %d.\n", i); + goto deinit_on_error; + } + } + + // We use these helpers but don't fill the actual buffer yet. + libusb_fill_bulk_transfer(inst->transfer_out, inst->handle, CH341_WRITE_EP, NULL, 0, cb_out, NULL, CH341_USB_TIMEOUT); + for (int i = 0; i < USB_IN_TRANSFERS; i++) + libusb_fill_bulk_transfer(inst->transfer_ins[i], inst->handle, CH341_READ_EP, NULL, 0, cb_in, NULL, CH341_USB_TIMEOUT); + + /** + * We don't need to initialize SPI at all, as by default it's configured properly. + * Only thing required is pinmux, what is anyway configured by CS change function. + */ + + pinedio_set_cs(inst, false); + + return 0; + +deinit_on_error: + pinedio_deinit(inst); + return ret; +} + +void pinedio_deinit(struct pinedio_inst *inst) +{ + pinedio_mutex_lock(&inst->usb_access_mutex); + if (inst->int_running_cnt != 0) { + inst->pin_poll_thread_exit = true; + pinedio_mutex_unlock(&inst->usb_access_mutex); + pthread_join(inst->pin_poll_thread, NULL); + } else { + pinedio_mutex_unlock(&inst->usb_access_mutex); + } + + for (int i = 0; i < USB_IN_TRANSFERS; i++) { + if (inst->transfer_ins[i] != NULL) { + libusb_free_transfer(inst->transfer_ins[i]); + } + } + if (inst->transfer_out != NULL) { + libusb_free_transfer(inst->transfer_out); + } + + if (inst->handle != NULL) { + // We don't know if claim of interface was successful, but libusb handles this. + libusb_release_interface(inst->handle, 0); +#ifdef __linux__ + libusb_attach_kernel_driver(inst->handle, 0); +#endif + libusb_close(inst->handle); + } +} + +static int32_t pinedio_get_input(struct pinedio_inst *inst, uint32_t *input) +{ + uint8_t buf[] = { + 0xA0, + }; + uint8_t output[6]; + + int32_t ret = usb_transfer(inst, __func__, sizeof(buf), sizeof(output), buf, output, true); + if (ret < 0) { + fprintf(stderr, "Could not get input pins.\n"); + } + *input = ((output[2] & 0x80) << 16) | ((output[1] & 0xef) << 8) | output[0]; + return ret; +} + +int32_t pinedio_get_irq_state(struct pinedio_inst *inst) +{ + uint32_t input; + int32_t ret = pinedio_get_input(inst, &input); + if (ret != 0) { + return ret; + } + return (input & (1 << 10)) != 0 ? 1 : 0; +} + +static void *pinedio_pin_poll_thread(void *arg) +{ + struct pinedio_inst *inst = arg; + int32_t ret = 0; + bool should_exit = false; + uint32_t pin_masks[PINEDIO_INT_PIN_MAX]; + pin_masks[PINEDIO_INT_PIN_IRQ] = 1 << 10; + + uint32_t input; + // pin_masks[PINEDIO_INT_PIN_BUSY] = 10 << 1; + while (!should_exit) { + ret = pinedio_get_input(inst, &input); + pinedio_mutex_lock(&inst->usb_access_mutex); + if (ret != 0) + continue; + for (uint8_t int_pin = 0; int_pin < PINEDIO_INT_PIN_MAX; int_pin++) { + struct pinedio_inst_int *inst_int = &inst->interrupts[int_pin]; + if (inst_int->callback == NULL) + continue; + uint8_t state = (input & pin_masks[int_pin]) != 0; + if (inst_int->previous_state != 255 && inst_int->previous_state != state) { + enum pinedio_int_mode mode = + inst_int->previous_state == false && state == true ? PINEDIO_INT_MODE_RISING : PINEDIO_INT_MODE_FALLING; + if (inst_int->mode & mode) { + inst_int->callback(); + } + } + inst_int->previous_state = state; + } + + should_exit = inst->pin_poll_thread_exit; + pinedio_mutex_unlock(&inst->usb_access_mutex); + platform_sleep(1000 / 30); + } +} + +int32_t pinedio_attach_interrupt(struct pinedio_inst *inst, enum pinedio_int_pin int_pin, enum pinedio_int_mode int_mode, + void (*callback)(void)) +{ + int32_t res = 0; + pinedio_mutex_lock(&inst->usb_access_mutex); + inst->interrupts[int_pin].previous_state = 255; + inst->interrupts[int_pin].mode = int_mode; + inst->interrupts[int_pin].callback = callback; + if (inst->int_running_cnt == 0) { + inst->pin_poll_thread_exit = false; + res = pthread_create(&inst->pin_poll_thread, NULL, pinedio_pin_poll_thread, inst); + if (res != 0) { + fprintf(stderr, "Failed to create thread, res: %d\n", res); + goto unlock; + } + } + inst->int_running_cnt++; + +unlock: + pinedio_mutex_unlock(&inst->usb_access_mutex); + return res; +} + +int32_t pinedio_deattach_interrupt(struct pinedio_inst *inst, enum pinedio_int_pin int_pin) +{ + pinedio_mutex_lock(&inst->usb_access_mutex); + inst->interrupts[int_pin].callback = NULL; + inst->int_running_cnt--; + if (inst->int_running_cnt == 0) { + inst->pin_poll_thread_exit = true; + pinedio_mutex_unlock(&inst->usb_access_mutex); + pthread_join(inst->pin_poll_thread, NULL); + return 0; + } + pinedio_mutex_unlock(&inst->usb_access_mutex); + return 0; +} + +int32_t pinedio_set_option(struct pinedio_inst *inst, enum pinedio_option option, uint32_t value) +{ + inst->options[option] = value; +} \ No newline at end of file diff --git a/src/platform/portduino/libpinedio-usb.h b/src/platform/portduino/libpinedio-usb.h new file mode 100644 index 000000000..4fa95ce18 --- /dev/null +++ b/src/platform/portduino/libpinedio-usb.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright 2024 Marek Kraus (@gamelaster) + +#ifndef PINEDIO_USB_LORA_ADAPTER_SDK_LIBPINEDIO_USB_H +#define PINEDIO_USB_LORA_ADAPTER_SDK_LIBPINEDIO_USB_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +#define USB_IN_TRANSFERS 32 + +enum pinedio_int_pin { + PINEDIO_INT_PIN_IRQ, + // PINEDIO_INT_PIN_BUSY, // not implemented yet + PINEDIO_INT_PIN_MAX +}; + +enum pinedio_int_mode { + PINEDIO_INT_MODE_RISING = 0x01, + PINEDIO_INT_MODE_FALLING = 0x02, +}; + +enum pinedio_option { PINEDIO_OPTION_AUTO_CS, PINEDIO_OPTION_MAX }; + +struct pinedio_inst_int { + uint8_t previous_state; + enum pinedio_int_mode mode; + void (*callback)(void); +}; + +/* Even this will work mostly on desktop (so malloc is available), I still prefer static allocation. + * That's why structure definition is in header. */ + +struct pinedio_inst { + struct libusb_device_handle *handle; + + /* We need to use many queued IN transfers for any resemblance of performance (especially on Windows) + * because USB spec says that transfers end on non-full packets and the device sends the 31 reply + * data bytes to each 32-byte packet with command + 31 bytes of data... */ + struct libusb_transfer *transfer_out; + struct libusb_transfer *transfer_ins[USB_IN_TRANSFERS]; + uint8_t int_running_cnt; + pthread_mutex_t usb_access_mutex; + pthread_t pin_poll_thread; + bool pin_poll_thread_exit; + struct pinedio_inst_int interrupts[PINEDIO_INT_PIN_MAX]; + uint32_t options[PINEDIO_OPTION_MAX]; +}; + +int32_t pinedio_init(struct pinedio_inst *inst, void *driver); +int32_t pinedio_set_option(struct pinedio_inst *inst, enum pinedio_option option, uint32_t value); +int32_t pinedio_set_cs(struct pinedio_inst *inst, bool active); +int32_t pinedio_write_read(struct pinedio_inst *inst, uint8_t *writearr, uint32_t writecnt, uint8_t *readarr, uint32_t readcnt); +int32_t pinedio_transceive(struct pinedio_inst *inst, uint8_t *write_buf, uint8_t *read_buf, uint32_t count); +int32_t pinedio_get_irq_state(struct pinedio_inst *inst); +int32_t pinedio_attach_interrupt(struct pinedio_inst *inst, enum pinedio_int_pin int_pin, enum pinedio_int_mode int_mode, + void (*callback)(void)); +int32_t pinedio_deattach_interrupt(struct pinedio_inst *inst, enum pinedio_int_pin int_pin); +void pinedio_deinit(struct pinedio_inst *inst); + +#ifdef __cplusplus +} +#endif + +#endif // PINEDIO_USB_LORA_ADAPTER_SDK_LIBPINEDIO_USB_H \ No newline at end of file