diff --git a/gpio/ws2812b_tester/README.md b/gpio/ws2812b_tester/README.md new file mode 100644 index 0000000..845888b --- /dev/null +++ b/gpio/ws2812b_tester/README.md @@ -0,0 +1,23 @@ +# WS2812B LED Tester +This application is used to test WS2812B LEDs. You can connect the WS2812B LEDs to any available GPIO pin. If you are powering the LEDs using the Flipper Zero, be sure to consider the power requirements of the LEDs. The 3V3 pin has a 1200mA max current (~4 watts). 5V pin has a 1000mA max current (5 watts). + +Please let me know any feedback! +- Discord - https://discord.com/invite/NsjCvqwPAd (@CodeAllNight) +- YouTube - https://youtube.com/@MrDerekJamison/playlists +- GitHub - https://github.com/jamisonderek/flipper-zero-tutorials +- Wiki - https://github.com/jamisonderek/flipper-zero-tutorials/wiki + +# Overview +This application has three submenu items: +* Test WS2812B +* About + +# Test WS2812B +- LED Pin : Select the GPIO pin that the WS2812B LED data wire is connected to. +- LED Count : select the total number of LEDs in the chain. +- LED Pattern : select the pattern to display on the LEDs. +- LED Brightness : Select the brightness of the LEDs. The brighter the LEDs, the more power they will consume. +- Enable +5V pin : Select "Yes" if you are using pin 1 to power the LEDs (note, there is a 1000mA max current on this pin). + +# About +The "About" menu item contains information about the application. diff --git a/gpio/ws2812b_tester/app.c b/gpio/ws2812b_tester/app.c new file mode 100644 index 0000000..20ba258 --- /dev/null +++ b/gpio/ws2812b_tester/app.c @@ -0,0 +1,528 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "led_driver.h" + +#include "ws2812b_tester_app_icons.h" + +#define TAG "WS2812B-Tester" + +// Our application menu has 2 items. +typedef enum { + LedTesterSubmenuIndexLeds, + LedTesterSubmenuIndexAbout, +} LedTesterSubmenuIndex; + +// Custom events that describe why the view dispatcher is calling our custom event callback. +typedef enum { + LedTesterEventInitialized, + LedTesterEventDeinit, + LedTesterEventTimer, + LedTesterEventConfigChange, + LedTesterEventClicked, +} LedTesterEventId; + +// Each view is a screen we show the user. +typedef enum { + LedTesterViewSubmenu, // The menu when the app starts + LedTesterViewLeds, // The LED screen + LedTesterViewAbout, // The about screen with directions, link to social channel, etc. +} LedTesterView; + +// Our model is the data we use to control the LEDs. +typedef struct { + uint8_t led_pin_index; // The index of the pin we are using to control the LEDs + uint32_t led_count; // The number of LEDs + uint8_t led_pattern_index; // The pattern index + uint8_t led_max_brightness; // The maximum brightness (0-100) + uint8_t timer_counter; // The current timer counter (used for auto-scrolling) + bool enable_5v; // Enable 5V output +} LedTesterModel; + +// The application object. +typedef struct { + ViewDispatcher* view_dispatcher; // Switches between our views + Submenu* submenu; // The application menu + VariableItemList* variable_item_list; // The WS2812B settings + Widget* widget_about; // The about screen + FuriTimer* timer; // Timer for automatic updating the LEDs + LedTesterModel* model; // The model +} LedTesterApp; + +// Hack so that we can access the application object from a variable_item_list on_enter/exit callback. +static LedTesterApp* global_app = NULL; + +/** + * @brief Callback for exiting the application. + * @details This function is called when user press back button. We return VIEW_NONE to + * indicate that we want to exit the application. + * @param _context The context - unused + * @return next view id +*/ +static uint32_t led_tester_navigation_exit_callback(void* _context) { + UNUSED(_context); + return VIEW_NONE; +} + +/** + * @brief Callback for the application's menu. + * @details This function is called when user press back button. We return LedTesterViewSubmenu to + * indicate that we want to return to the application menu. + * @param _context The context - unused + * @return next view id +*/ +static uint32_t led_tester_navigation_submenu_callback(void* _context) { + UNUSED(_context); + return LedTesterViewSubmenu; +} + +/** + * @brief Handle submenu item selection. + * @details This function is called when user selects an item from the submenu. + * @param context The context - LedTesterApp object. + * @param index The LedTesterSubmenuIndex item that was clicked. +*/ +static void led_tester_submenu_callback(void* context, uint32_t index) { + LedTesterApp* app = (LedTesterApp*)context; + switch(index) { + case LedTesterSubmenuIndexLeds: + view_dispatcher_switch_to_view(app->view_dispatcher, LedTesterViewLeds); + break; + case LedTesterSubmenuIndexAbout: + view_dispatcher_switch_to_view(app->view_dispatcher, LedTesterViewAbout); + break; + default: + break; + } +} + +/** + * @brief Callback for timer elapsed. + * @details This function is called when the timer is elapsed. + * @param context The context - LedTesterApp object. +*/ +static void led_tester_timer_callback(void* context) { + LedTesterApp* app = (LedTesterApp*)context; + view_dispatcher_send_custom_event(app->view_dispatcher, LedTesterEventTimer); +} + +/** + * @brief Callback for when the LED configuration settings are updated. + * @details This function is called when the LED configuration settings are changed by the user. + * @param context The context - LedTesterApp object. +*/ +static void led_tester_settings_updated(LedTesterApp* app) { + view_dispatcher_send_custom_event(app->view_dispatcher, LedTesterEventConfigChange); +} + +/** + * @brief Callback for when the LED configuration settings are clicked. + * @details This function is called when the user presses OK button while in the LED configuration settings. + * @param context The context - LedTesterApp object. +*/ +static void led_tester_item_clicked(void* context, uint32_t index) { + UNUSED(index); + LedTesterApp* app = (LedTesterApp*)context; + view_dispatcher_send_custom_event(app->view_dispatcher, LedTesterEventClicked); +} + +/** + * @brief Enable 5V power. + * @details This function enables the 5V power output on pin 1. +*/ +static void led_tester_5v_power_on() { + uint8_t attempts = 5; + while(--attempts > 0) { + if(furi_hal_power_enable_otg()) break; + } +} + +/** + * @brief Disable 5V power. + * @details This function disables the 5V power output on pin 1. +*/ +static void led_tester_5v_power_off() { + if(furi_hal_power_is_otg_enabled()) { + furi_hal_power_disable_otg(); + } +} + +// Settings for configuring which GPIO pin to use for controlling the WS2812B LEDs. +static const char* setting_led_pin_config_label = "LED Pin"; +static const GpioPin* setting_led_pin_values[] = + {&gpio_ext_pa7, &gpio_ext_pa6, &gpio_ext_pa4, &gpio_ext_pb3, &gpio_ext_pb2, &gpio_ext_pc3}; +static char* setting_led_pin_names[] = {"A7", "A6", "A4", "B3", "B2", "C3"}; +static void led_tester_setting_led_pin_change(VariableItem* item) { + LedTesterApp* app = variable_item_get_context(item); + LedTesterModel* model = app->model; + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, setting_led_pin_names[index]); + model->led_pin_index = index; +} + +// Settings for configuring how many LEDs to enable. +static const char* setting_led_count_config_label = "LED Count"; +static uint16_t setting_led_count_values[] = + {1, 2, 3, 4, 5, 6, 7, 8, 16, 32, 64, 128, 256, 512, 1024}; +static char* setting_led_count_names[] = + {"1", "2", "3", "4", "5", "6", "7", "8", "16", "32", "64", "128", "256"}; +static uint8_t setting_led_count_default_index = 3; // 4 LEDs +static void led_tester_setting_led_count_change(VariableItem* item) { + LedTesterApp* app = variable_item_get_context(item); + LedTesterModel* model = app->model; + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, setting_led_count_names[index]); + model->led_count = setting_led_count_values[index]; + led_tester_settings_updated(app); +} + +// Settings for configuring which LED pattern to use. +static const char* setting_led_pattern_config_label = "LED Pattern"; +static char* setting_led_pattern_names[] = + {"Red", "Green", "Blue", "White", "RGBW", "GBWR", "BWRG", "WRGB", "Rotate"}; +static void led_tester_setting_led_pattern_change(VariableItem* item) { + LedTesterApp* app = variable_item_get_context(item); + LedTesterModel* model = app->model; + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, setting_led_pattern_names[index]); + model->led_pattern_index = index; + led_tester_settings_updated(app); +} + +// Settings for configuring the LED brightness. +static const char* setting_led_brightness_config_label = "LED Brightness"; +static uint16_t setting_led_brightness_values[] = {1, 2, 3, 5, 10, 20, 25, 50, 75, 100}; +static char* setting_led_brightness_names[] = + {"1%", "2%", "3%", "5%", "10%", "20%", "25%", "50%", "75%", "100%"}; +static void led_tester_setting_led_brightness_change(VariableItem* item) { + LedTesterApp* app = variable_item_get_context(item); + LedTesterModel* model = app->model; + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, setting_led_brightness_names[index]); + model->led_max_brightness = setting_led_brightness_values[index]; + led_tester_settings_updated(app); +} + +// Settings for configuring the 5V output. +static const char* setting_5v_config_label = "Enable +5V pin"; +static bool setting_5v_values[] = {true, false}; +static char* setting_5v_names[] = {"Yes", "No"}; +static void led_tester_setting_5v_change(VariableItem* item) { + LedTesterApp* app = variable_item_get_context(item); + LedTesterModel* model = app->model; + uint8_t index = variable_item_get_current_value_index(item); + variable_item_set_current_value_text(item, setting_5v_names[index]); + model->enable_5v = setting_5v_values[index]; + if(app->model->enable_5v) { + led_tester_5v_power_on(); + } else { + led_tester_5v_power_off(); + } +} + +/** + * @brief Callback for custom events. + * @details This function is called when a custom event is sent to the view dispatcher. + * @param context The context - LedTesterApp object. + * @param event The event id - LedTesterEventId value. +*/ +static bool led_tester_custom_event_callback(void* context, uint32_t event) { + LedTesterApp* app = (LedTesterApp*)context; + const GpioPin* pin = setting_led_pin_values[app->model->led_pin_index]; + + if(event == LedTesterEventTimer) { + app->model->timer_counter++; + } + + uint32_t rgb[4] = {0}; + switch(app->model->led_pattern_index) { + case 0: // RED + rgb[0] = 0xFF0000; + rgb[1] = rgb[0]; + rgb[2] = rgb[0]; + rgb[3] = rgb[0]; + break; + case 1: // GREEN + rgb[0] = 0x00FF00; + rgb[1] = rgb[0]; + rgb[2] = rgb[0]; + rgb[3] = rgb[0]; + break; + case 2: // BLUE + rgb[0] = 0x0000FF; + rgb[1] = rgb[0]; + rgb[2] = rgb[0]; + rgb[3] = rgb[0]; + break; + case 3: // WHITE + rgb[0] = 0xFFFFFF; + rgb[1] = rgb[0]; + rgb[2] = rgb[0]; + rgb[3] = rgb[0]; + break; + case 4: // RGBW + rgb[0] = 0xFF0000; + rgb[1] = 0x00FF00; + rgb[2] = 0x0000FF; + rgb[3] = 0xFFFFFF; + break; + case 5: // GBWR + rgb[3] = 0xFF0000; + rgb[0] = 0x00FF00; + rgb[1] = 0x0000FF; + rgb[2] = 0xFFFFFF; + break; + case 6: // BWRG + rgb[2] = 0xFF0000; + rgb[3] = 0x00FF00; + rgb[0] = 0x0000FF; + rgb[1] = 0xFFFFFF; + break; + case 7: // WRGB + rgb[0] = 0xFFFFFF; + rgb[1] = 0xFF0000; + rgb[2] = 0x00FF00; + rgb[3] = 0x0000FF; + break; + case 8: // Rotate + rgb[(app->model->timer_counter + 0) & 0x3] = 0xFFFFFF; + rgb[(app->model->timer_counter + 1) & 0x3] = 0xFF0000; + rgb[(app->model->timer_counter + 2) & 0x3] = 0x00FF00; + rgb[(app->model->timer_counter + 3) & 0x3] = 0x0000FF; + break; + default: + break; + } + + if(event == LedTesterEventDeinit) { + rgb[0] = 0; + rgb[1] = 0; + rgb[2] = 0; + rgb[3] = 0; + } + + // Scale the brightness + for(size_t i = 0; i < 4; i++) { + rgb[i] = (((rgb[i] >> 16) & 0xFF) * app->model->led_max_brightness / 100) << 16 | + (((rgb[i] >> 8) & 0xFF) * app->model->led_max_brightness / 100) << 8 | + ((rgb[i] & 0xFF) * app->model->led_max_brightness / 100); + } + + LedDriver* led_driver = led_driver_alloc(256, pin); + + // Set all LEDs to off + for(size_t i = 0; i < 256; i++) { + led_driver_set_led(led_driver, i, 0); + } + + // Set the LEDs to the pattern + for(size_t i = 0; i < app->model->led_count; i++) { + led_driver_set_led(led_driver, i, rgb[i % 4]); + } + + led_driver_transmit(led_driver); + led_driver_free(led_driver); + + return true; +} + +/** + * @brief Callback for entering the LED configuration settings. + * @details This function is called when the user enters the LED configuration settings. We + * start the periodic timer to update the LEDs & we signal initialized so the LEDs + * turn on to their default state. + * @param _context The context - unused +*/ +void led_tester_enter_leds_callback(void* _context) { + // _context is variable_item_list, but it doesn't expose the item, so we can't get the context. + UNUSED(_context); + // Hack, we use a global to access the app object. + LedTesterApp* app = (LedTesterApp*)global_app; + app->timer = furi_timer_alloc(led_tester_timer_callback, FuriTimerTypePeriodic, app); + furi_timer_start(app->timer, 500); + view_dispatcher_send_custom_event(app->view_dispatcher, LedTesterEventInitialized); +} + +/** + * @brief Callback for exiting the LED configuration settings. + * @details This function is called when the user exits the LED configuration settings. We + * stop the periodic timer to update the LEDs. + * @param _context The context - unused +*/ +void led_tester_exit_leds_callback(void* _context) { + // _context is variable_item_list, but it doesn't expose the item, so we can't get the context. + UNUSED(_context); + // Hack, we use a global to access the app object. + LedTesterApp* app = (LedTesterApp*)global_app; + furi_timer_stop(app->timer); + furi_timer_free(app->timer); + app->timer = NULL; + view_dispatcher_send_custom_event(app->view_dispatcher, LedTesterEventDeinit); +} + +/** + * @brief Allocate the led tester application. + * @details This function allocates the led tester application resources. + * @return LedTesterApp object. +*/ +static LedTesterApp* led_tester_app_alloc() { + LedTesterApp* app = (LedTesterApp*)malloc(sizeof(LedTesterApp)); + global_app = app; + + app->model = (LedTesterModel*)malloc(sizeof(LedTesterModel)); + + Gui* gui = furi_record_open(RECORD_GUI); + + app->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_enable_queue(app->view_dispatcher); + view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + + app->submenu = submenu_alloc(); + submenu_add_item( + app->submenu, "Test WS2812B", LedTesterSubmenuIndexLeds, led_tester_submenu_callback, app); + submenu_add_item( + app->submenu, "About", LedTesterSubmenuIndexAbout, led_tester_submenu_callback, app); + view_set_previous_callback( + submenu_get_view(app->submenu), led_tester_navigation_exit_callback); + view_dispatcher_add_view( + app->view_dispatcher, LedTesterViewSubmenu, submenu_get_view(app->submenu)); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, led_tester_custom_event_callback); + view_dispatcher_switch_to_view(app->view_dispatcher, LedTesterViewSubmenu); + + app->variable_item_list = variable_item_list_alloc(); + variable_item_list_set_enter_callback(app->variable_item_list, led_tester_item_clicked, app); + view_set_enter_callback( + variable_item_list_get_view(app->variable_item_list), led_tester_enter_leds_callback); + view_set_exit_callback( + variable_item_list_get_view(app->variable_item_list), led_tester_exit_leds_callback); + variable_item_list_reset(app->variable_item_list); + + // Pin + VariableItem* item = variable_item_list_add( + app->variable_item_list, + setting_led_pin_config_label, + COUNT_OF(setting_led_pin_names), + led_tester_setting_led_pin_change, + app); + uint8_t setting_led_pin_index = 0; + variable_item_set_current_value_index(item, setting_led_pin_index); + variable_item_set_current_value_text(item, setting_led_pin_names[setting_led_pin_index]); + app->model->led_pin_index = setting_led_pin_index; + + // Count + item = variable_item_list_add( + app->variable_item_list, + setting_led_count_config_label, + COUNT_OF(setting_led_count_names), + led_tester_setting_led_count_change, + app); + uint8_t setting_led_count_index = setting_led_count_default_index; + variable_item_set_current_value_index(item, setting_led_count_index); + variable_item_set_current_value_text(item, setting_led_count_names[setting_led_count_index]); + app->model->led_count = setting_led_count_values[setting_led_count_index]; + + // Pattern + item = variable_item_list_add( + app->variable_item_list, + setting_led_pattern_config_label, + COUNT_OF(setting_led_pattern_names), + led_tester_setting_led_pattern_change, + app); + uint8_t setting_led_pattern_index = 0; + variable_item_set_current_value_index(item, setting_led_pattern_index); + variable_item_set_current_value_text( + item, setting_led_pattern_names[setting_led_pattern_index]); + app->model->led_pattern_index = setting_led_pattern_index; + + // Brightness + item = variable_item_list_add( + app->variable_item_list, + setting_led_brightness_config_label, + COUNT_OF(setting_led_brightness_names), + led_tester_setting_led_brightness_change, + app); + uint8_t setting_led_brightness_index = 3; + variable_item_set_current_value_index(item, setting_led_brightness_index); + variable_item_set_current_value_text( + item, setting_led_brightness_names[setting_led_brightness_index]); + app->model->led_max_brightness = setting_led_brightness_values[setting_led_brightness_index]; + + // 5-volt pin + item = variable_item_list_add( + app->variable_item_list, + setting_5v_config_label, + COUNT_OF(setting_5v_names), + led_tester_setting_5v_change, + app); + uint8_t setting_5v_index = 0; + variable_item_set_current_value_index(item, setting_5v_index); + variable_item_set_current_value_text(item, setting_5v_names[setting_5v_index]); + app->model->enable_5v = setting_5v_values[setting_5v_index]; + if(app->model->enable_5v) { + led_tester_5v_power_on(); + } + + view_set_previous_callback( + variable_item_list_get_view(app->variable_item_list), + led_tester_navigation_submenu_callback); + view_dispatcher_add_view( + app->view_dispatcher, + LedTesterViewLeds, + variable_item_list_get_view(app->variable_item_list)); + + app->widget_about = widget_alloc(); + widget_add_text_scroll_element( + app->widget_about, + 0, + 0, + 128, + 64, + "This is a WS2812B LED tester\n\nConnect WS2812B LED data\nwire to GPIO pin on Flipper.\n\nThe 3V3 pin has a 1200mA\nmax current (~4 watts). The\n5V pin has a 1000mA max\ncurrent (5 watts).\n\nauthors: @codeallnight and\nZ3BRO!\n\nhttps://discord.com/invite/NsjCvqwPAd\nhttps://youtube.com/@MrDerekJamison"); + view_set_previous_callback( + widget_get_view(app->widget_about), led_tester_navigation_submenu_callback); + view_dispatcher_add_view( + app->view_dispatcher, LedTesterViewAbout, widget_get_view(app->widget_about)); + + return app; +} + +/** + * @brief Free the led tester application. + * @details This function frees the led tester application resources. + * @param app The led tester application object. +*/ +static void led_tester_app_free(LedTesterApp* app) { + view_dispatcher_remove_view(app->view_dispatcher, LedTesterViewAbout); + widget_free(app->widget_about); + view_dispatcher_remove_view(app->view_dispatcher, LedTesterViewLeds); + variable_item_list_free(app->variable_item_list); + view_dispatcher_remove_view(app->view_dispatcher, LedTesterViewSubmenu); + submenu_free(app->submenu); + view_dispatcher_free(app->view_dispatcher); + furi_record_close(RECORD_GUI); + free(app); +} + +/** + * @brief Main function for led tester application. + * @details This function is the entry point for the led tester application. It should be defined in + * application.fam as the entry_point setting. + * @param _p Input parameter - unused + * @return 0 - Success +*/ +int32_t ws2812b_led_tester_app(void* _p) { + UNUSED(_p); + LedTesterApp* app = led_tester_app_alloc(); + view_dispatcher_run(app->view_dispatcher); + led_tester_app_free(app); + return 0; +} \ No newline at end of file diff --git a/gpio/ws2812b_tester/app.png b/gpio/ws2812b_tester/app.png new file mode 100644 index 0000000..8048c79 Binary files /dev/null and b/gpio/ws2812b_tester/app.png differ diff --git a/gpio/ws2812b_tester/application.fam b/gpio/ws2812b_tester/application.fam new file mode 100644 index 0000000..a370503 --- /dev/null +++ b/gpio/ws2812b_tester/application.fam @@ -0,0 +1,14 @@ +App( + appid="ws2812b_tester_app", + name="WS2812B LED Tester App", + apptype=FlipperAppType.EXTERNAL, + entry_point="ws2812b_led_tester_app", + stack_size=4 * 1024, + requires=[ + "gui", + ], + order=10, + fap_icon="app.png", + fap_category="GPIO", + fap_description="WS2812B LED Tester App. This is intended to test that WS2812B LEDs are functioning correctly.", +) diff --git a/gpio/ws2812b_tester/led_driver.c b/gpio/ws2812b_tester/led_driver.c new file mode 100644 index 0000000..7c600d2 --- /dev/null +++ b/gpio/ws2812b_tester/led_driver.c @@ -0,0 +1,316 @@ +/** + * @file led_driver.c + * @brief WS2812B LED Driver + * @details This driver uses DMA and TIM2 AAR to drive WS2812B LEDs. There is no circular buffer, + * so all of the data is loaded into memory at the beginning. We are able to driver 256 LEDs. + * +*/ + +#include + +#include "led_driver.h" + +#define MAX_LED_COUNT 256 +// We store the HIGH/LOW durations (2 values) for each color bit (24 bits per LED) +#define LED_DRIVER_BUFFER_SIZE (MAX_LED_COUNT * 2 * 24) +// We use a setinel value to figure out when the timer is complete. +#define LED_DRIVER_TIMER_SETINEL 0xFFU + +/** 64 transitions per us @ 64MHz. Our timing is in NANO_SECONDS */ +#define LED_DRIVER_TIMER_NANOSECOND (1000U / (SystemCoreClock / 1000000U)) +// Timings for WS2812B +#define LED_DRIVER_T0H 400U +#define LED_DRIVER_T1H 800U +#define LED_DRIVER_T0L 850U +#define LED_DRIVER_T1L 450U + +// Wait for 35ms for the DMA to complete. NOTE: 1000 leds*(850ns+450ns)*24 = 32ms +#define LED_DRIVER_SETINEL_WAIT_MS 35 + +struct LedDriver { + LL_DMA_InitTypeDef dma_gpio_update; + LL_DMA_InitTypeDef dma_led_transition_timer; + + const GpioPin* gpio; + uint32_t gpio_buf[2]; // On/Off for GPIO + + uint8_t timer_buffer[LED_DRIVER_BUFFER_SIZE + 2]; + uint32_t write_pos; + uint32_t read_pos; + + uint32_t count_leds; + uint32_t* led_data; +}; + +static void led_driver_init_dma_gpio_update(LedDriver* led_driver, const GpioPin* gpio) { + led_driver->gpio = gpio; + + // Memory to Peripheral + led_driver->dma_gpio_update.Direction = LL_DMA_DIRECTION_MEMORY_TO_PERIPH; + // Peripheral (GPIO - We populate GPIO port's BSRR register) + led_driver->dma_gpio_update.PeriphOrM2MSrcAddress = (uint32_t)&gpio->port->BSRR; + led_driver->dma_gpio_update.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT; + led_driver->dma_gpio_update.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_WORD; + // Memory (State to set GPIO) + led_driver->dma_gpio_update.MemoryOrM2MDstAddress = (uint32_t)led_driver->gpio_buf; + led_driver->dma_gpio_update.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT; + led_driver->dma_gpio_update.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_WORD; + // Data + led_driver->dma_gpio_update.Mode = LL_DMA_MODE_CIRCULAR; + led_driver->dma_gpio_update.NbData = 2; // We cycle between two (HIGH/LOW)values + // When to perform data exchange + led_driver->dma_gpio_update.PeriphRequest = LL_DMAMUX_REQ_TIM2_UP; + led_driver->dma_gpio_update.Priority = LL_DMA_PRIORITY_VERYHIGH; +} + +static void led_driver_init_dma_led_transition_timer(LedDriver* led_driver) { + // Timer that triggers based on user data. + led_driver->dma_led_transition_timer.Direction = LL_DMA_DIRECTION_MEMORY_TO_PERIPH; + // Peripheral (Timer - We populate TIM2's ARR register) + led_driver->dma_led_transition_timer.PeriphOrM2MSrcAddress = (uint32_t)&TIM2->ARR; + led_driver->dma_led_transition_timer.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT; + led_driver->dma_led_transition_timer.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_WORD; + // Memory (Timings) + led_driver->dma_led_transition_timer.MemoryOrM2MDstAddress = + (uint32_t)led_driver->timer_buffer; + led_driver->dma_led_transition_timer.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT; + led_driver->dma_led_transition_timer.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_BYTE; + // Data + led_driver->dma_led_transition_timer.Mode = LL_DMA_MODE_NORMAL; + led_driver->dma_led_transition_timer.NbData = LED_DRIVER_BUFFER_SIZE; + // When to perform data exchange + led_driver->dma_led_transition_timer.PeriphRequest = LL_DMAMUX_REQ_TIM2_UP; + led_driver->dma_led_transition_timer.Priority = LL_DMA_PRIORITY_HIGH; +} + +LedDriver* led_driver_alloc(int count_leds, const GpioPin* gpio) { + furi_assert(gpio); + furi_assert(count_leds && count_leds <= MAX_LED_COUNT); + + LedDriver* led_driver = malloc(sizeof(LedDriver)); + led_driver_init_dma_gpio_update(led_driver, gpio); + led_driver_init_dma_led_transition_timer(led_driver); + led_driver->led_data = malloc(MAX_LED_COUNT * sizeof(uint32_t)); + + led_driver->count_leds = count_leds; + + return led_driver; +} + +void led_driver_free(LedDriver* led_driver) { + furi_assert(led_driver); + + free(led_driver->led_data); + free(led_driver); +} + +uint32_t led_driver_set_led(LedDriver* led_driver, uint32_t index, uint32_t rrggbb) { + furi_assert(led_driver); + if(index >= led_driver->count_leds) { + return 0xFFFFFFFF; + } + + uint32_t previous = led_driver->led_data[index]; + led_driver->led_data[index] = rrggbb; + return previous; +} + +uint32_t led_driver_get_led(LedDriver* led_driver, uint32_t index) { + furi_assert(led_driver); + if(index >= led_driver->count_leds) { + return 0xFFFFFFFF; + } + + return led_driver->led_data[index]; +} + +static void led_driver_start_dma(LedDriver* led_driver) { + furi_assert(led_driver); + + LL_DMA_Init(DMA1, LL_DMA_CHANNEL_1, &led_driver->dma_gpio_update); + LL_DMA_Init(DMA1, LL_DMA_CHANNEL_2, &led_driver->dma_led_transition_timer); + + LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1); + LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_2); +} + +static void led_driver_start_timer() { + furi_hal_bus_enable(FuriHalBusTIM2); + + LL_TIM_SetCounterMode(TIM2, LL_TIM_COUNTERMODE_UP); + LL_TIM_SetClockDivision(TIM2, LL_TIM_CLOCKDIVISION_DIV1); + LL_TIM_SetPrescaler(TIM2, 0); + // Updated by led_driver->dma_led_transition_timer.PeriphOrM2MSrcAddress + LL_TIM_SetAutoReload(TIM2, LED_DRIVER_TIMER_SETINEL); + LL_TIM_SetCounter(TIM2, 0); + + LL_TIM_EnableCounter(TIM2); + LL_TIM_EnableUpdateEvent(TIM2); + LL_TIM_EnableDMAReq_UPDATE(TIM2); + LL_TIM_GenerateEvent_UPDATE(TIM2); +} + +static void led_driver_stop_timer() { + LL_TIM_DisableCounter(TIM2); + LL_TIM_DisableUpdateEvent(TIM2); + LL_TIM_DisableDMAReq_UPDATE(TIM2); + furi_hal_bus_disable(FuriHalBusTIM2); +} + +static void led_driver_stop_dma() { + LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_1); + LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_2); + LL_DMA_ClearFlag_TC1(DMA1); + LL_DMA_ClearFlag_TC2(DMA1); +} + +static void led_driver_spin_lock(LedDriver* led_driver) { + const uint32_t prev_timer = DWT->CYCCNT; + const uint32_t wait_time = LED_DRIVER_SETINEL_WAIT_MS * SystemCoreClock / 1000; + + do { + /* Make sure it's started (allow 100 ticks), but then check for sentinel value. */ + if(TIM2->ARR == LED_DRIVER_TIMER_SETINEL && DWT->CYCCNT - prev_timer > 100) { + break; + } + + // 0xFF is fairly quick, make sure we didn't miss it. + if((DWT->CYCCNT - prev_timer > wait_time)) { + FURI_LOG_D( + "Demo", "0xFF not found (ARR 0x%08lx, read %lu)", TIM2->ARR, led_driver->read_pos); + led_driver->read_pos = led_driver->write_pos - 1; + break; + } + } while(true); +} + +static void led_driver_add_period_length(LedDriver* led_driver, uint32_t length) { + led_driver->timer_buffer[led_driver->write_pos++] = length; + led_driver->timer_buffer[led_driver->write_pos] = LED_DRIVER_TIMER_SETINEL; +} + +static void led_driver_add_period(LedDriver* led_driver, uint16_t duration_ns) { + furi_assert(led_driver); + + uint32_t reload_value = duration_ns / LED_DRIVER_TIMER_NANOSECOND; + + if(reload_value > 255) { + FURI_LOG_E("Demo", "reload_value: %ld", reload_value); + } + furi_check(reload_value > 0); + furi_check(reload_value < 256); + + led_driver_add_period_length(led_driver, reload_value - 1); +} + +static void led_driver_add_color(LedDriver* led_driver, uint32_t rrggbb) { + UNUSED(rrggbb); + + uint32_t ggrrbb = (rrggbb & 0xFF) | ((rrggbb & 0xFF00) << 8) | ((rrggbb & 0xFF0000) >> 8); + + for(int i = 23; i >= 0; i--) { + if(ggrrbb & (1 << i)) { + led_driver_add_period(led_driver, LED_DRIVER_T0L); + led_driver_add_period(led_driver, LED_DRIVER_T1L); + } else { + led_driver_add_period(led_driver, LED_DRIVER_T0H); + led_driver_add_period(led_driver, LED_DRIVER_T1H); + } + } +} + +void led_driver_transmit(LedDriver* led_driver) { + furi_assert(led_driver); + + furi_assert(!led_driver->read_pos); + furi_assert(!led_driver->write_pos); + + furi_hal_gpio_init(led_driver->gpio, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh); + furi_hal_gpio_write(led_driver->gpio, false); + + const uint32_t bit_set = led_driver->gpio->pin << GPIO_BSRR_BS0_Pos; + const uint32_t bit_reset = led_driver->gpio->pin << GPIO_BSRR_BR0_Pos; + + // Always start with HIGH + led_driver->gpio_buf[0] = bit_set; + led_driver->gpio_buf[1] = bit_reset; + + for(size_t i = 0; i < LED_DRIVER_BUFFER_SIZE; i++) { + led_driver->timer_buffer[i] = LED_DRIVER_TIMER_SETINEL; + } + for(size_t i = 0; i < led_driver->count_leds; i++) { + led_driver_add_color(led_driver, led_driver->led_data[i]); + } + led_driver->dma_led_transition_timer.NbData = led_driver->write_pos + 1; + + FURI_CRITICAL_ENTER(); + + led_driver_start_dma(led_driver); + led_driver_start_timer(); + + led_driver_spin_lock(led_driver); + + led_driver_stop_timer(); + led_driver_stop_dma(); + + FURI_CRITICAL_EXIT(); + + memset(led_driver->timer_buffer, LED_DRIVER_TIMER_SETINEL, LED_DRIVER_BUFFER_SIZE); + led_driver->read_pos = 0; + led_driver->write_pos = 0; +} + +/* +int32_t main_led_test(void* _p) { + UNUSED(_p); + + uint16_t num_leds = MAX_LED_COUNT; + LedDriver* led_driver = led_driver_alloc(num_leds, &gpio_ext_pc3); + + uint32_t* data[80]; + for(int i = 0; i < 80; i++) { + data[i] = malloc(16 * 16 * sizeof(uint32_t)); + } + + for(int j = 0; j < num_leds; j++) { + uint8_t red = rand() % 2; + uint8_t green = rand() % 4; + uint8_t blue = rand() % 4; + data[0][j] = red << 16 | green << 8 | blue; + } + data[0][0] = 0x000F00; + + for(int i = 1; i < 80; i++) { + for(int j = 1; j < num_leds; j++) { + uint8_t red = rand() % 2; + uint8_t green = rand() % 4; + uint8_t blue = rand() % 4; + data[i][j] = red << 16 | green << 8 | blue; + data[i][j] = data[i - 1][j - 1]; + } + data[i][0] = data[i - 1][num_leds - 1]; + // for(int j = 0; j < num_leds; j++) { + // if(data[i - 1][j] == 0x000F00) { + // data[i][j] = 0x000F00; + // } + // } + data[i][rand() % num_leds] = 0x000F00; + } + + int counter = 0; + while(true) { + uint32_t i = counter++ % 80; + for(int j = 0; j < num_leds; j++) { + led_driver_set_led(led_driver, j, data[i][j]); + } + led_driver_transmit(led_driver); + furi_delay_ms(20); + } + + for(int i = 0; i < 80; i++) { + free(data[i]); + } + + return 0; +} +*/ \ No newline at end of file diff --git a/gpio/ws2812b_tester/led_driver.h b/gpio/ws2812b_tester/led_driver.h new file mode 100644 index 0000000..a2c1a76 --- /dev/null +++ b/gpio/ws2812b_tester/led_driver.h @@ -0,0 +1,10 @@ +#include +#include + +typedef struct LedDriver LedDriver; + +LedDriver* led_driver_alloc(int count_leds, const GpioPin* gpio); +void led_driver_free(LedDriver* led_driver); +uint32_t led_driver_set_led(LedDriver* led_driver, uint32_t index, uint32_t rrggbb); +uint32_t led_driver_get_led(LedDriver* led_driver, uint32_t index); +void led_driver_transmit(LedDriver* led_driver);