diff --git a/applications/debug/spi_test/application.fam b/applications/debug/spi_test/application.fam new file mode 100644 index 00000000000..30c0c268fc8 --- /dev/null +++ b/applications/debug/spi_test/application.fam @@ -0,0 +1,10 @@ +App( + appid="spi_test", + name="SPI Test", + apptype=FlipperAppType.DEBUG, + entry_point="spi_test_app", + requires=["gui"], + stack_size=1 * 1024, + fap_category="Debug", + fap_libs=["softio"], +) diff --git a/applications/debug/spi_test/spi_test.c b/applications/debug/spi_test/spi_test.c new file mode 100644 index 00000000000..b047f049d9c --- /dev/null +++ b/applications/debug/spi_test/spi_test.c @@ -0,0 +1,140 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include + +#define TAG "SpiTest" + +typedef struct { + Gui* gui; + ViewDispatcher* view_dispatcher; + Submenu* submenu; +} SpiTest; + +typedef enum { + SpiTestViewSubmenu, +} SpiTestView; + +typedef enum { + SpiTestSubmenuHardwareTx, + SpiTestSubmenuSoftwareTx, +} SpiTestSubmenu; + +static void spi_test_submenu_callback(void* context, uint32_t index) { + SpiTest* instance = (SpiTest*)context; + UNUSED(instance); + + uint8_t tx_buffer[] = {0x55, 0xAA}; + uint8_t rx_buffer[sizeof(tx_buffer)] = {0}; + + if(index == SpiTestSubmenuHardwareTx) { + FuriHalSpiBusHandle* handle = &furi_hal_spi_bus_handle_external; + furi_hal_spi_bus_handle_init(handle); + furi_hal_spi_acquire(handle); + furi_hal_spi_bus_trx(handle, tx_buffer, rx_buffer, sizeof(tx_buffer), FuriWaitForever); + furi_hal_spi_release(handle); + furi_hal_spi_bus_handle_deinit(handle); + } else { + // initialize + SoftIoSpiBusConfig bus_cfg = { + .miso = &gpio_ext_pa6, + .mosi = &gpio_ext_pa7, + .sck = &gpio_ext_pb3, + .clk_polarity = 0, + .clk_fq_khz = 200, + .clk_phase = 0, + }; + SoftIoSpiBus* bus = softio_spi_alloc(&bus_cfg); + softio_spi_init(bus); + furi_hal_gpio_write(&gpio_ext_pa4, true); + furi_hal_gpio_init(&gpio_ext_pa4, GpioModeOutputOpenDrain, GpioPullUp, GpioSpeedVeryHigh); + + // transmit + furi_hal_gpio_write(&gpio_ext_pa4, false); + softio_spi_trx(bus, tx_buffer, rx_buffer, sizeof(tx_buffer), FuriWaitForever); + furi_hal_gpio_write(&gpio_ext_pa4, true); + + // deinitialize + furi_hal_gpio_init(&gpio_ext_pa4, GpioModeAnalog, GpioPullNo, GpioSpeedLow); + softio_spi_deinit(bus); + softio_spi_free(bus); + } + + FURI_LOG_I( + TAG, + "sent %02hhx %02hhX, received %02hhx %02hhX", + tx_buffer[0], + tx_buffer[1], + rx_buffer[0], + rx_buffer[1]); +} + +static uint32_t spi_test_exit_callback(void* context) { + UNUSED(context); + return VIEW_NONE; +} + +SpiTest* spi_test_alloc(void) { + SpiTest* instance = malloc(sizeof(SpiTest)); + + View* view = NULL; + + instance->gui = furi_record_open(RECORD_GUI); + instance->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_attach_to_gui( + instance->view_dispatcher, instance->gui, ViewDispatcherTypeFullscreen); + + // Menu + instance->submenu = submenu_alloc(); + view = submenu_get_view(instance->submenu); + view_set_previous_callback(view, spi_test_exit_callback); + view_dispatcher_add_view(instance->view_dispatcher, SpiTestViewSubmenu, view); + submenu_add_item( + instance->submenu, + "Hardware TRX", + SpiTestSubmenuHardwareTx, + spi_test_submenu_callback, + instance); + submenu_add_item( + instance->submenu, + "Software TRX", + SpiTestSubmenuSoftwareTx, + spi_test_submenu_callback, + instance); + + return instance; +} + +void spi_test_free(SpiTest* instance) { + view_dispatcher_remove_view(instance->view_dispatcher, SpiTestViewSubmenu); + submenu_free(instance->submenu); + + view_dispatcher_free(instance->view_dispatcher); + furi_record_close(RECORD_GUI); + + free(instance); +} + +int32_t spi_test_run(SpiTest* instance) { + view_dispatcher_switch_to_view(instance->view_dispatcher, SpiTestViewSubmenu); + view_dispatcher_run(instance->view_dispatcher); + return 0; +} + +int32_t spi_test_app(void* p) { + UNUSED(p); + + SpiTest* instance = spi_test_alloc(); + + int32_t ret = spi_test_run(instance); + + spi_test_free(instance); + + return ret; +} diff --git a/lib/SConscript b/lib/SConscript index fb0473f8d48..738c780ce86 100644 --- a/lib/SConscript +++ b/lib/SConscript @@ -43,6 +43,7 @@ libs = env.BuildModules( "ble_profile", "bit_lib", "datetime", + "softio", ], ) diff --git a/lib/softio/SConscript b/lib/softio/SConscript new file mode 100644 index 00000000000..409b90edeb3 --- /dev/null +++ b/lib/softio/SConscript @@ -0,0 +1,20 @@ +Import("env") + +env.Append( + LINT_SOURCES=[ + Dir("."), + ], + CPPPATH=[ + "#/lib/softio", + ], + SDK_HEADERS=[], +) + +libenv = env.Clone(FW_LIB_NAME="softio") +libenv.ApplyLibFlags() + +sources = libenv.GlobRecursive("*.c*") + +lib = libenv.StaticLibrary("${FW_LIB_NAME}", sources) +libenv.Install("${LIB_DIST_DIR}", lib) +Return("lib") diff --git a/lib/softio/softio_spi.c b/lib/softio/softio_spi.c new file mode 100644 index 00000000000..e68e579c1d9 --- /dev/null +++ b/lib/softio/softio_spi.c @@ -0,0 +1,166 @@ +#include "softio_spi.h" +#include +#include +#include +#include +#include + +#define SOFTSPI_TIM TIM17 +#define SOFTSPI_TIM_BUS FuriHalBusTIM17 +#define SOFTSPI_TIM_IRQ FuriHalInterruptIdTim1TrgComTim17 +#define SOFTSPI_TIM_FQ_KHZ 64000UL + +struct SoftIoSpiBus { + SoftIoSpiBusConfig config; + uint8_t is_currently_initd; +}; + +typedef struct { + SoftIoSpiBusConfig config; + const uint8_t* tx_buffer; + uint8_t* rx_buffer; + size_t size; + FuriSemaphore* done_semaphore; + uint8_t bit : 3; + uint8_t sck_level : 1; + uint8_t done : 1; + uint8_t + out_level : 1; // miso); + furi_check(config->mosi); + furi_check(config->sck); + + SoftIoSpiBus* bus = malloc(sizeof(SoftIoSpiBus)); + bus->config = *config; + + return bus; +} + +void softio_spi_free(SoftIoSpiBus* bus) { + furi_check(bus); + furi_check(!bus->is_currently_initd); + free(bus); +} + +void softio_spi_init(SoftIoSpiBus* bus) { + furi_check(bus); + furi_check(!bus->is_currently_initd); + + furi_hal_gpio_write(bus->config.mosi, false); + furi_hal_gpio_write(bus->config.sck, bus->config.clk_polarity); + furi_hal_gpio_init(bus->config.mosi, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh); + furi_hal_gpio_init(bus->config.sck, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh); + furi_hal_gpio_init(bus->config.miso, GpioModeInput, GpioPullNo, GpioSpeedVeryHigh); + + bus->is_currently_initd = true; +} + +void softio_spi_deinit(SoftIoSpiBus* bus) { + furi_check(bus); + furi_check(bus->is_currently_initd); + + furi_hal_gpio_init(bus->config.mosi, GpioModeAnalog, GpioPullNo, GpioSpeedLow); + furi_hal_gpio_init(bus->config.sck, GpioModeAnalog, GpioPullNo, GpioSpeedLow); + furi_hal_gpio_init(bus->config.miso, GpioModeAnalog, GpioPullNo, GpioSpeedLow); + + bus->is_currently_initd = false; +} + +static void softio_spi_timer_isr(void* param) { + furi_check(LL_TIM_IsActiveFlag_UPDATE(SOFTSPI_TIM)); + LL_TIM_ClearFlag_UPDATE(SOFTSPI_TIM); + + SoftspiTimerIsrContext* context = param; + + if(context->done) { + furi_hal_gpio_write(context->config.sck, context->config.clk_polarity); + LL_TIM_DisableCounter(SOFTSPI_TIM); + return; + } else { + furi_hal_gpio_write(context->config.sck, context->sck_level); + } + + if(context->sck_level == context->out_level) { + // TX edge + if(context->tx_buffer) + furi_hal_gpio_write(context->config.mosi, (*context->tx_buffer >> context->bit) & 1); + } else { + // RX edge + if(context->rx_buffer) + *context->rx_buffer |= furi_hal_gpio_read(context->config.miso) << context->bit; + + if(context->bit == 0) { + // entire byte transmitted + if(context->tx_buffer) context->tx_buffer++; + if(context->rx_buffer) context->rx_buffer++; + if(!--context->size) { + furi_semaphore_release(context->done_semaphore); + context->done = 1; + } else { + context->bit = 7; + } + } else { + context->bit--; + } + } + + context->sck_level = !context->sck_level; +} + +void softio_spi_trx( + SoftIoSpiBus* bus, + const uint8_t* tx_buffer, + uint8_t* rx_buffer, + size_t size, + uint32_t timeout) { + furi_check(bus); + furi_check(bus->is_currently_initd); + furi_assert(size); + + SoftspiTimerIsrContext context = { + .config = bus->config, + .tx_buffer = tx_buffer, + .rx_buffer = rx_buffer, + .size = size, + .bit = 7, + .done_semaphore = furi_semaphore_alloc(1, 0), + .sck_level = bus->config.clk_polarity, + .done = 0, + .out_level = bus->config.clk_polarity ^ bus->config.clk_phase, + }; + + furi_hal_bus_enable(SOFTSPI_TIM_BUS); + furi_hal_interrupt_set_isr_ex( + SOFTSPI_TIM_IRQ, FuriHalInterruptPriorityHighest, softio_spi_timer_isr, &context); + LL_TIM_SetPrescaler(SOFTSPI_TIM, 0); + LL_TIM_SetCounterMode(SOFTSPI_TIM, LL_TIM_COUNTERMODE_UP); + LL_TIM_SetAutoReload( + SOFTSPI_TIM, SOFTSPI_TIM_FQ_KHZ / bus->config.clk_fq_khz / 2); // f_ISR = 2 f_CLK + LL_TIM_DisableARRPreload(SOFTSPI_TIM); + LL_TIM_SetRepetitionCounter(SOFTSPI_TIM, 0); + LL_TIM_SetClockDivision(SOFTSPI_TIM, LL_TIM_CLOCKDIVISION_DIV1); + LL_TIM_SetClockSource(SOFTSPI_TIM, LL_TIM_CLOCKSOURCE_INTERNAL); + LL_TIM_GenerateEvent_UPDATE(SOFTSPI_TIM); + LL_TIM_EnableIT_UPDATE(SOFTSPI_TIM); + LL_TIM_EnableCounter(SOFTSPI_TIM); + + furi_semaphore_acquire(context.done_semaphore, timeout); + + furi_hal_interrupt_set_isr(SOFTSPI_TIM_IRQ, NULL, NULL); + furi_hal_bus_disable(SOFTSPI_TIM_BUS); + furi_semaphore_free(context.done_semaphore); +} + +void softio_spi_tx(SoftIoSpiBus* bus, const uint8_t* buffer, size_t size, uint32_t timeout) { + furi_check(buffer); + softio_spi_trx(bus, buffer, NULL, size, timeout); +} + +void softio_spi_rx(SoftIoSpiBus* bus, uint8_t* buffer, size_t size, uint32_t timeout) { + furi_check(buffer); + softio_spi_trx(bus, NULL, buffer, size, timeout); +} diff --git a/lib/softio/softio_spi.h b/lib/softio/softio_spi.h new file mode 100644 index 00000000000..587f7a36eeb --- /dev/null +++ b/lib/softio/softio_spi.h @@ -0,0 +1,122 @@ +#pragma once + +/** + * @file softio_spi.h + * Software (bit-banged) SPI implementation. Master-only. Supports all 4 modes + * with clock rates of up to 200kHz. + * + * @example + * ```c + * // initialize + * SoftIoSpiBusConfig bus_cfg = { + * .miso = &gpio_ext_pa6, + * .mosi = &gpio_ext_pa7, + * .sck = &gpio_ext_pb3, + * .clk_polarity = 0, + * }; + * SoftIoSpiSlaveConfig dev_cfg = { + * .cs = &gpio_ext_pa4, + * .clk_fq_khz = 200, + * .clk_phase = 0, + * }; + * SoftIoSpiBus* bus = softio_spi_alloc(&bus_cfg); + * SoftIoSpiSlave* device = softio_spi_attach_slave(bus, &dev_cfg); + * softio_spi_init(bus); + * + * // transmit + * uint8_t buffer[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x55, 0xAA}; + * softio_spi_acquire(device); + * softio_spi_tx(device, buffer, sizeof(buffer), FuriWaitForever); + * softio_spi_release(device); + * + * // deinitialize + * softio_spi_deinit(bus); + * softio_spi_detach_slave(bus, device); + * softio_spi_free(bus); + * ``` + */ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * @brief Opaque software SPI bus handle + */ +typedef struct SoftIoSpiBus SoftIoSpiBus; + +/** + * @brief Software SPI bus configuration + */ +typedef struct { + const GpioPin* miso; + const GpioPin* mosi; + const GpioPin* sck; + uint32_t clk_fq_khz; + uint8_t clk_phase : 1; + uint8_t clk_polarity : 1; +} SoftIoSpiBusConfig; + +/** + * @brief Allocates a software SPI bus with the specified configuration + * @param [in] config Pointer to the configuration structure. Does not have to remain valid after the function exits. + */ +SoftIoSpiBus* softio_spi_alloc(SoftIoSpiBusConfig* config); + +/** + * @brief Deallocates a software SPI bus + * @param [in] bus Software SPI bus + */ +void softio_spi_free(SoftIoSpiBus* bus); + +/** + * @brief Initializes bus pins: MOSI, MISO and SCK and all CS pins + * @param [in] bus Software SPI bus + */ +void softio_spi_init(SoftIoSpiBus* bus); + +/** + * @brief Deinitializes bus pins: MOSI, MISO and SCK and all CS pins + * @param [in] bus Software SPI bus + */ +void softio_spi_deinit(SoftIoSpiBus* bus); + +/** + * @brief Simultaneously transmits and receives a buffer on the software SPI bus + * @param [in] bus Software SPI bus + * @param [in] tx_buffer Buffer to transmit. May be NULL if transmission is not required. + * @param [in] rx_buffer Buffer to receive data into. May be NULL if reception is not required. + * @param size Buffer length (both buffers must be of the same size) + * @param timeout Timeout in ticks. Transaction will be interrupted abruptly if this timeout is reached. + */ +void softio_spi_trx( + SoftIoSpiBus* bus, + const uint8_t* tx_buffer, + uint8_t* rx_buffer, + size_t size, + uint32_t timeout); + +/** + * @brief Transmits a buffer on the software SPI bus + * @param [in] bus Software SPI bus + * @param [in] buffer Buffer to transmit + * @param size Buffer length + * @param timeout Timeout in ticks. Transmission will be interrupted abruptly if this timeout is reached. + */ +void softio_spi_tx(SoftIoSpiBus* bus, const uint8_t* buffer, size_t size, uint32_t timeout); + +/** + * @brief Receives a buffer from the software SPI bus + * @param [in] bus Software SPI bus + * @param [in] buffer Buffer to receive into + * @param size Buffer length + * @param timeout Timeout in ticks. Reception will be interrupted abruptly if this timeout is reached. + */ +void softio_spi_rx(SoftIoSpiBus* bus, uint8_t* buffer, size_t size, uint32_t timeout); + +#ifdef __cplusplus +} +#endif diff --git a/targets/furi_hal_include/furi_hal_spi.h b/targets/furi_hal_include/furi_hal_spi.h index d497dff5c35..c55b05e86bf 100644 --- a/targets/furi_hal_include/furi_hal_spi.h +++ b/targets/furi_hal_include/furi_hal_spi.h @@ -66,7 +66,7 @@ void furi_hal_spi_release(FuriHalSpiBusHandle* handle); * @param size transaction size (buffer size) * @param timeout operation timeout in ms * - * @return true on sucess + * @return true on success */ bool furi_hal_spi_bus_rx( FuriHalSpiBusHandle* handle,