/* * Copyright (c) 2018 Intel Corporation * Copyright (c) 2019 Nordic Semiconductor ASA * * SPDX-License-Identifier: Apache-2.0 */ #define DT_DRV_COMPAT worldsemi_ws2812_gpio #include #include #define LOG_LEVEL CONFIG_LED_STRIP_LOG_LEVEL #include LOG_MODULE_REGISTER(ws2812_gpio); #include #include #include #include #include #include struct ws2812_gpio_data { struct device *gpio; struct device *clk; }; struct ws2812_gpio_cfg { u8_t pin; bool has_white; }; static struct ws2812_gpio_data *dev_data(struct device *dev) { return dev->driver_data; } static const struct ws2812_gpio_cfg *dev_cfg(struct device *dev) { return dev->config->config_info; } /* * This is hard-coded to nRF51 in two ways: * * 1. The assembly delays T1H, T0H, TxL * 2. GPIO set/clear */ /* * T1H: 1 bit high pulse delay: 12 cycles == .75 usec * T0H: 0 bit high pulse delay: 4 cycles == .25 usec * TxL: inter-bit low pulse delay: 8 cycles == .5 usec * * We can't use k_busy_wait() here: its argument is in microseconds, * and we need roughly .05 microsecond resolution. */ #define DELAY_T1H "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n" #define DELAY_T0H "nop\nnop\nnop\nnop\n" #define DELAY_TxL "nop\nnop\nnop\nnop\nnop\nnop\nnop\nnop\n" /* * GPIO set/clear (these make assumptions about assembly details * below). * * This uses OUTCLR == OUTSET+4. * * We should be able to make this portable using the results of * https://github.com/zephyrproject-rtos/zephyr/issues/11917. * * We already have the GPIO device stashed in ws2812_gpio_data, so * this driver can be used as a test case for the optimized API. * * Per Arm docs, both Rd and Rn must be r0-r7, so we use the "l" * constraint in the below assembly. */ #define SET_HIGH "str %[p], [%[r], #0]\n" /* OUTSET = BIT(LED_PIN) */ #define SET_LOW "str %[p], [%[r], #4]\n" /* OUTCLR = BIT(LED_PIN) */ /* Send out a 1 bit's pulse */ #define ONE_BIT(base, pin) do { \ __asm volatile (SET_HIGH \ DELAY_T1H \ SET_LOW \ DELAY_TxL \ :: \ [r] "l" (base), \ [p] "l" (pin)); } while (0) /* Send out a 0 bit's pulse */ #define ZERO_BIT(base, pin) do { \ __asm volatile (SET_HIGH \ DELAY_T0H \ SET_LOW \ DELAY_TxL \ :: \ [r] "l" (base), \ [p] "l" (pin)); } while (0) static int send_buf(struct device *dev, u8_t *buf, size_t len) { volatile u32_t *base = (u32_t *)&NRF_GPIO->OUTSET; const u32_t val = BIT(dev_cfg(dev)->pin); struct device *clk = dev_data(dev)->clk; unsigned int key; int rc; rc = clock_control_on(clk, CLOCK_CONTROL_NRF_SUBSYS_HF); if (rc) { return rc; } key = irq_lock(); while (len--) { u32_t b = *buf++; s32_t i; /* * Generate signal out of the bits, MSbit first. * * Accumulator maintenance and branching mean the * inter-bit time will be longer than TxL, but the * wp.josh.com blog post says we have at least 5 usec * of slack time between bits before we risk the * signal getting latched, so this will be fine as * long as the compiler does something minimally * reasonable. */ for (i = 7; i >= 0; i--) { if (b & BIT(i)) { ONE_BIT(base, val); } else { ZERO_BIT(base, val); } } } irq_unlock(key); rc = clock_control_off(clk, CLOCK_CONTROL_NRF_SUBSYS_HF); return rc; } static int ws2812_gpio_update_rgb(struct device *dev, struct led_rgb *pixels, size_t num_pixels) { const struct ws2812_gpio_cfg *config = dev->config->config_info; const bool has_white = config->has_white; u8_t *ptr = (u8_t *)pixels; size_t i; /* Convert from RGB to on-wire format (GRB or GRBW) */ for (i = 0; i < num_pixels; i++) { u8_t r = pixels[i].r; u8_t g = pixels[i].g; u8_t b = pixels[i].b; *ptr++ = g; *ptr++ = r; *ptr++ = b; if (has_white) { *ptr++ = 0; /* white channel is unused */ } } return send_buf(dev, (u8_t *)pixels, num_pixels * (has_white ? 4 : 3)); } static int ws2812_gpio_update_channels(struct device *dev, u8_t *channels, size_t num_channels) { LOG_ERR("update_channels not implemented"); return -ENOTSUP; } static const struct led_strip_driver_api ws2812_gpio_api = { .update_rgb = ws2812_gpio_update_rgb, .update_channels = ws2812_gpio_update_channels, }; #define WS2812_GPIO_LABEL(idx) \ (DT_INST_LABEL(idx)) #define WS2812_GPIO_HAS_WHITE(idx) \ (DT_INST_PROP(idx, has_white_channel) == 1) #define WS2812_GPIO_DEV(idx) \ (DT_INST_GPIO_LABEL(idx, in_gpios)) #define WS2812_GPIO_PIN(idx) \ (DT_INST_GPIO_PIN(idx, in_gpios)) #define WS2812_GPIO_FLAGS(idx) \ (DT_INST_GPIO_FLAGS(idx, in_gpios)) /* * The inline assembly above is designed to work on nRF51 devices with * the 16 MHz clock enabled. * * TODO: try to make this portable, or at least port to more devices. */ #define WS2812_GPIO_CLK(idx) DT_LABEL(DT_INST(0, nordic_nrf_clock)) #define WS2812_GPIO_DEVICE(idx) \ \ static int ws2812_gpio_##idx##_init(struct device *dev) \ { \ struct ws2812_gpio_data *data = dev_data(dev); \ \ data->gpio = device_get_binding(WS2812_GPIO_DEV(idx)); \ if (!data->gpio) { \ LOG_ERR("Unable to find GPIO controller %s", \ WS2812_GPIO_DEV(idx)); \ return -ENODEV; \ } \ \ data->clk = device_get_binding(WS2812_GPIO_CLK(idx)); \ if (!data->clk) { \ LOG_ERR("Unable to find clock %s", \ WS2812_GPIO_CLK(idx)); \ return -ENODEV; \ } \ \ return gpio_pin_configure(data->gpio, \ WS2812_GPIO_PIN(idx), \ WS2812_GPIO_FLAGS(idx) | \ GPIO_OUTPUT); \ } \ \ static struct ws2812_gpio_data ws2812_gpio_##idx##_data; \ \ static const struct ws2812_gpio_cfg ws2812_gpio_##idx##_cfg = { \ .pin = WS2812_GPIO_PIN(idx), \ .has_white = WS2812_GPIO_HAS_WHITE(idx), \ }; \ \ DEVICE_AND_API_INIT(ws2812_gpio_##idx, WS2812_GPIO_LABEL(idx), \ ws2812_gpio_##idx##_init, \ &ws2812_gpio_##idx##_data, \ &ws2812_gpio_##idx##_cfg, POST_KERNEL, \ CONFIG_LED_STRIP_INIT_PRIORITY, \ &ws2812_gpio_api) DT_INST_FOREACH(WS2812_GPIO_DEVICE)