From 666f8212635d12be2c22bd4380eba8d154d53cfe Mon Sep 17 00:00:00 2001 From: sheinz Date: Tue, 16 Aug 2016 10:53:56 +0300 Subject: [PATCH] ws2812_i2s: WS2812 leds driver implementation using i2s_dma library --- examples/ws2812_i2s/Makefile | 6 + examples/ws2812_i2s/ws2812_i2s_colour_loop.c | 83 +++++++++ extras/ws2812_i2s/README.md | 15 ++ extras/ws2812_i2s/component.mk | 9 + extras/ws2812_i2s/ws2812_i2s.c | 181 +++++++++++++++++++ extras/ws2812_i2s/ws2812_i2s.h | 53 ++++++ 6 files changed, 347 insertions(+) create mode 100644 examples/ws2812_i2s/Makefile create mode 100644 examples/ws2812_i2s/ws2812_i2s_colour_loop.c create mode 100644 extras/ws2812_i2s/README.md create mode 100644 extras/ws2812_i2s/component.mk create mode 100644 extras/ws2812_i2s/ws2812_i2s.c create mode 100644 extras/ws2812_i2s/ws2812_i2s.h diff --git a/examples/ws2812_i2s/Makefile b/examples/ws2812_i2s/Makefile new file mode 100644 index 0000000..e639c57 --- /dev/null +++ b/examples/ws2812_i2s/Makefile @@ -0,0 +1,6 @@ +# Makefile for the ws2812_i2s example + +PROGRAM=ws2812_i2s_example +EXTRA_COMPONENTS = extras/i2s_dma extras/ws2812_i2s + +include ../../common.mk diff --git a/examples/ws2812_i2s/ws2812_i2s_colour_loop.c b/examples/ws2812_i2s/ws2812_i2s_colour_loop.c new file mode 100644 index 0000000..022f010 --- /dev/null +++ b/examples/ws2812_i2s/ws2812_i2s_colour_loop.c @@ -0,0 +1,83 @@ +/** + * Example of ws2812_i2s library usage. + * + * This example shows light that travels in circle with fading tail. + * As ws2812_i2s library using hardware I2S the output pin is GPIO3 and + * can not be changed. + * + * This sample code is in the public domain., + */ +#include "espressif/esp_common.h" +#include "FreeRTOS.h" +#include "task.h" +#include "esp/uart.h" +#include +#include +#include + +#include "ws2812_i2s/ws2812_i2s.h" + +const uint32_t led_number = 60; +const uint32_t tail_fade_factor = 2; +const uint32_t tail_length = 8; + +static void fade_pixel(ws2812_pixel_t *pixel, uint32_t factor) +{ + pixel->red = pixel->red / factor; + pixel->green = pixel->green / factor; + pixel->blue = pixel->blue / factor; +} + +static int fix_index(int index) +{ + if (index < 0) { + return (int)led_number + index; + } else if (index >= led_number) { + return index - led_number; + } else { + return index; + } +} + +static ws2812_pixel_t next_colour() +{ + ws2812_pixel_t colour = {0, 0, 0}; + colour.red = rand() % 256; + colour.green = rand() % 256; + colour.blue = rand() % 256; + + return colour; +} + +static void demo(void *pvParameters) +{ + ws2812_pixel_t pixels[led_number]; + int head_index = 0; + + ws2812_i2s_init(led_number); + + memset(pixels, 0, sizeof(ws2812_pixel_t) * led_number); + + while (1) { + pixels[head_index] = next_colour(); + for (int i = 0; i < led_number; i++) { + head_index = fix_index(head_index + 1); + pixels[head_index] = pixels[fix_index(head_index-1)]; + for (int ii = 1; ii < tail_length; ii++) { + fade_pixel(&pixels[fix_index(head_index - ii)], tail_fade_factor); + } + memset(&pixels[fix_index(head_index - tail_length)], 0, + sizeof(ws2812_pixel_t)); + + ws2812_i2s_update(pixels); + vTaskDelay(20 / portTICK_RATE_MS); + } + } +} + +void user_init(void) +{ + uart_set_baud(0, 115200); + + xTaskCreate(&demo, (signed char *)"ws2812_i2s", 256, NULL, 10, NULL); +} diff --git a/extras/ws2812_i2s/README.md b/extras/ws2812_i2s/README.md new file mode 100644 index 0000000..1cd6953 --- /dev/null +++ b/extras/ws2812_i2s/README.md @@ -0,0 +1,15 @@ +# WS2812 led driver + +This driver uses I2S and DMA subsystems to drive WS2812 leds. +The idea to use I2S to control WS2812 leds belongs to [CNLohr](https://github.com/CNLohr). + +## Pros + + * Not using CPU to generate pulses. + * Interrupt neutral. Reliable operation even with high network load. + +## Cons + + * Using RAM for DMA buffer. 12 bytes per pixel. + * Can not change output PIN. Use I2S DATA output pin which is GPIO3. + diff --git a/extras/ws2812_i2s/component.mk b/extras/ws2812_i2s/component.mk new file mode 100644 index 0000000..08b4449 --- /dev/null +++ b/extras/ws2812_i2s/component.mk @@ -0,0 +1,9 @@ +# Component makefile for extras/ws2812_i2s + +# expected anyone using ws2812_i2s driver includes it as 'ws2812_i2s/ws2812_i2s.h' +INC_DIRS += $(ws2812_i2s_ROOT).. + +# args for passing into compile rule generation +ws2812_i2s_SRC_DIR = $(ws2812_i2s_ROOT) + +$(eval $(call component_compile_rules,ws2812_i2s)) diff --git a/extras/ws2812_i2s/ws2812_i2s.c b/extras/ws2812_i2s/ws2812_i2s.c new file mode 100644 index 0000000..310869b --- /dev/null +++ b/extras/ws2812_i2s/ws2812_i2s.c @@ -0,0 +1,181 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2016 sheinz (https://github.com/sheinz) + * + * 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 "ws2812_i2s.h" +#include "i2s_dma/i2s_dma.h" + +#include +#include + +// #define WS2812_I2S_DEBUG + +#ifdef WS2812_I2S_DEBUG +#include +#define debug(fmt, ...) printf("%s" fmt "\n", "ws2812_i2s: ", ## __VA_ARGS__); +#else +#define debug(fmt, ...) +#endif + +#define MAX_DMA_BLOCK_SIZE 4095 +#define DMA_PIXEL_SIZE 12 // each colour takes 4 bytes + +/** + * Amount of zero data to produce WS2812 reset condition. + * DMA data must be multiple of 4 + * 16 bytes of 0 gives ~50 microseconds of low pulse + */ +#define WS2812_ZEROES_LENGTH 16 + +static uint8_t i2s_dma_zero_buf[WS2812_ZEROES_LENGTH] = {0}; + +static dma_descriptor_t *dma_block_list; +static uint32_t dma_block_list_size; + +static void *dma_buffer; +static uint32_t dma_buffer_size; + +#ifdef WS2812_I2S_DEBUG +volatile uint32_t dma_isr_counter = 0; +#endif + +static volatile bool i2s_dma_processing = false; + +static void dma_isr_handler(void) +{ + if (i2s_dma_is_eof_interrupt()) { +#ifdef WS2812_I2S_DEBUG + dma_isr_counter++; +#endif + i2s_dma_processing = false; + } + i2s_dma_clear_interrupt(); +} + +/** + * Form a linked list of descriptors (dma blocks). + * The last two blocks are zero block and stop block. + * The last block is a stop terminal block. It has no data and no next block. + */ +static inline void init_descriptors_list(uint8_t *buf, uint32_t total_dma_data_size) +{ + for (int i = 0; i < dma_block_list_size; i++) { + dma_block_list[i].owner = 1; + dma_block_list[i].eof = 0; + dma_block_list[i].sub_sof = 0; + dma_block_list[i].unused = 0; + dma_block_list[i].buf_ptr = buf; + + if (total_dma_data_size >= MAX_DMA_BLOCK_SIZE) { + dma_block_list[i].datalen = MAX_DMA_BLOCK_SIZE; + dma_block_list[i].blocksize = MAX_DMA_BLOCK_SIZE; + total_dma_data_size -= MAX_DMA_BLOCK_SIZE; + buf += MAX_DMA_BLOCK_SIZE; + } else { + dma_block_list[i].datalen = total_dma_data_size; + dma_block_list[i].blocksize = total_dma_data_size; + total_dma_data_size = 0; + } + + if (i == (dma_block_list_size - 2)) { // zero block + dma_block_list[i].buf_ptr = i2s_dma_zero_buf; + dma_block_list[i].datalen = WS2812_ZEROES_LENGTH; + dma_block_list[i].blocksize = WS2812_ZEROES_LENGTH; + } + + if (i == (dma_block_list_size - 1)) { // stop block + // it needs a valid buffer even if no data to output + dma_block_list[i].buf_ptr = i2s_dma_zero_buf; + dma_block_list[i].datalen = 0; + dma_block_list[i].blocksize = WS2812_ZEROES_LENGTH; + dma_block_list[i].next_link_ptr = 0; + + // the last stop block should trigger interrupt + dma_block_list[i].eof = 1; + } else { + dma_block_list[i].next_link_ptr = &dma_block_list[i + 1]; + } + } +} + +void ws2812_i2s_init(uint32_t pixels_number) +{ + dma_buffer_size = pixels_number * DMA_PIXEL_SIZE; + dma_block_list_size = dma_buffer_size / MAX_DMA_BLOCK_SIZE; + + if (dma_buffer_size % MAX_DMA_BLOCK_SIZE) { + dma_block_list_size += 1; + } + + dma_block_list_size += 2; // zero block and stop block + + debug("allocating %d dma blocks\n", dma_block_list_size); + + dma_block_list = (dma_descriptor_t*)malloc( + dma_block_list_size * sizeof(dma_descriptor_t)); + + debug("allocating %d bytes for DMA buffer\n", dma_buffer_size); + dma_buffer = malloc(dma_buffer_size); + memset(dma_buffer, 0xFA, dma_buffer_size); + + init_descriptors_list(dma_buffer, dma_buffer_size); + + i2s_clock_div_t clock_div = i2s_get_clock_div(3333333); + i2s_pins_t i2s_pins = {.data = true, .clock = false, .ws = false}; + + debug("i2s clock dividers, bclk=%d, clkm=%d\n", + clock_div.bclk_div, clock_div.clkm_div); + + i2s_dma_init(dma_isr_handler, clock_div, i2s_pins); +} + +const IRAM_DATA int16_t bitpatterns[16] = +{ + 0b1000100010001000, 0b1000100010001110, 0b1000100011101000, 0b1000100011101110, + 0b1000111010001000, 0b1000111010001110, 0b1000111011101000, 0b1000111011101110, + 0b1110100010001000, 0b1110100010001110, 0b1110100011101000, 0b1110100011101110, + 0b1110111010001000, 0b1110111010001110, 0b1110111011101000, 0b1110111011101110, +}; + +void ws2812_i2s_update(ws2812_pixel_t *pixels) +{ + while (i2s_dma_processing) {}; + + uint16_t *p_dma_buf = dma_buffer; + + for (uint32_t i = 0; i < (dma_buffer_size / DMA_PIXEL_SIZE); i++) { + // green + *p_dma_buf++ = bitpatterns[pixels[i].green & 0x0F]; + *p_dma_buf++ = bitpatterns[pixels[i].green >> 4]; + + // red + *p_dma_buf++ = bitpatterns[pixels[i].red & 0x0F]; + *p_dma_buf++ = bitpatterns[pixels[i].red >> 4]; + + // blue + *p_dma_buf++ = bitpatterns[pixels[i].blue & 0x0F]; + *p_dma_buf++ = bitpatterns[pixels[i].blue >> 4]; + } + + i2s_dma_processing = true; + i2s_dma_start(dma_block_list); +} diff --git a/extras/ws2812_i2s/ws2812_i2s.h b/extras/ws2812_i2s/ws2812_i2s.h new file mode 100644 index 0000000..fc12e26 --- /dev/null +++ b/extras/ws2812_i2s/ws2812_i2s.h @@ -0,0 +1,53 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2016 sheinz (https://github.com/sheinz) + * + * 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. + */ +#ifndef __WS2812_I2S_H__ +#define __WS2812_I2S_H__ + +#include +#include + +typedef struct { + uint8_t red; + uint8_t green; + uint8_t blue; +} ws2812_pixel_t; + +/** + * Initialize i2s and dma subsystems to work with ws2812 led strip. + * + * Please note that each pixel will take 12 bytes of RAM. + * + * @param pixels_number Number of pixels in the strip. + */ +void ws2812_i2s_init(uint32_t pixels_number); + +/** + * Update ws2812 pixels. + * + * @param pixels Array of 'pixels_number' pixels. The array must contain all + * the pixels. + */ +void ws2812_i2s_update(ws2812_pixel_t *pixels); + +#endif // __WS2812_I2S_H__