This is an automated email from the ASF dual-hosted git repository. xiaoxiang781216 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/nuttx.git
commit 0f7aa511b22e2b3523f7ddabf6afc7f76862731c Author: Kerogit <[email protected]> AuthorDate: Wed Jun 24 00:16:17 2026 +0200 arch/avr/src/avrdx/avrdx_ws2812: added lower-half driver for WS2812 LEDs This patch adds lower half for the WS2812 programmable LED driver to the AVR DA/DB family. USART peripheral is supported as the transmission hardware. Driver was verified by ws2812 example from nuttx-apps and by custom application that controls LEDs on two USART peripherals. [skip-cmake-parity] Signed-off-by: Kerogit <[email protected]> --- .codespellrc | 2 +- Documentation/platforms/avr/avrdx/docs/ws2812.rst | 321 +++++ arch/avr/src/avrdx/Kconfig | 18 +- arch/avr/src/avrdx/Kconfig-drivers | 126 ++ arch/avr/src/avrdx/Make.defs | 5 + arch/avr/src/avrdx/avrdx_swint.h | 12 + arch/avr/src/avrdx/avrdx_ws2812.c | 1342 +++++++++++++++++++++ arch/avr/src/avrdx/avrdx_ws2812.h | 174 +++ arch/avr/src/avrdx/avrdx_ws2812_uart_store.S | 280 +++++ arch/avr/src/avrdx/avrdx_ws2812_uart_xmit.S | 479 ++++++++ 10 files changed, 2752 insertions(+), 7 deletions(-) diff --git a/.codespellrc b/.codespellrc index 69bd7c2718c..96cd556dedf 100644 --- a/.codespellrc +++ b/.codespellrc @@ -54,7 +54,7 @@ ignore-words-list = parm, parms, pres, - RCALL, + rcall, REGONS, SAIs, sie, diff --git a/Documentation/platforms/avr/avrdx/docs/ws2812.rst b/Documentation/platforms/avr/avrdx/docs/ws2812.rst new file mode 100644 index 00000000000..0949cf9e0e4 --- /dev/null +++ b/Documentation/platforms/avr/avrdx/docs/ws2812.rst @@ -0,0 +1,321 @@ +========================================= +WS2812 RGB LED driver for AVR DA/DB chips +========================================= + +This is a description of driver for WS2812 programmable RGB LED +on AVR DA/DB chips. + +Developer notes - driver design philosophy +========================================== + +The datasheets for various WS2812 devices differ slightly but +in all of them, zero data bit starts with logical 1 pulse with 220-380ns +length. The rest varies slightly across datasheets but overall, the LED +operates at 800kHz frequency so the logical 0 state duration should be +1250ns (period) minus length of log. 1 pulse. + +To put things into perspective - the maximum clock speed of the chip +is 24MHz. Transmission of each data bit therefore takes 30 clock ticks +of the CPU. Even with most instructions only taking one clock tick, +that is not much time. + +**Using this driver with slower clock speed is therefore unsupported.** + +There are multiple options how to implement this. One of them is manual +bit flipping with timing being achieved by instruction count. This would +require the code to run with interrupts disabled (any interruption breaks +the timing.) + +Considering that an RGB LED needs 24 bits of data (or 32bit for LED +with white light) and each bit takes 1.25 microseconds, transmitting +data to single LED ties up the CPU for 30/40 microseconds. This time +increases with LED count which may cause latency problems. (Some of +which can be alleviated by enabling interrupts between data bits +where you can pause the transmission (for up to 280us, that is +considered to be a reset interval by the LEDs.) + +Another option is to use some integrated peripheral to take care +of the actual data transmission, the CPU would only be used to prepare +the data. There are multiple options for hardware that can be chosen - +the requirement is that the hardware must be able to generate +a waveform interpreted as a valid bit by the LED. + +Naming note +----------- + +This driver uses the word **lane** as a term for interconnected chain +of LEDs. + + +UART driver +----------- + +For this, we split the 1.25us period to 4 time segments, each 312.5ns +long. Logical 0 bit is then encoded as 1 segment high, 3 segments low, +vice versa for logical 1. + +This waveform presents an issue for the UART because UART transmission +starts with logical 0 pulse (the start bit) and the idle state +on the line is logical 1. This is solved in the hardware using +internal inverter. + +Since the LED does not recognize any start bit, the driver needs +to include the start bit into the waveform. Not a problem, (inverted) +start bit puts logical 1 on the line which is what the LED wants. + +Data bit waveform is then sent as a byte of data from the UART's +perspective - the waveform needs to be mapped to a byte. The simplest +way to do this would be to simply stretch the 4 time segments over +8 UART bits. Since the start bit is a part of the transmission, +this would only use 7 UART data bits - 8th bit would be left set, +simply increasing the time the line stays in logical 0 (that time +is also increased by stop bit) - since the LEDs only reset +after a 280us delay, slight delay by trailing log. 1 bits is +inconsequential. + +The problematic part of this solution is that UART bit length +needs to be 312.5/2 nanoseconds, yielding 6.4Mbaud UART transfer +rate - well beyond the capabilities of the chip. + +The solution is to only map each time segment to a single UART bit, +using only the start bit and three data bits, injecting a delay +when the remaining bits keep the line in logical 0 (inverting log. 1.) +This can be done because first time segment, which maps to start bit, +is always logical 1. This halves the required baud rate to 3.2Mbaud +which is still too much for the hardware. The maximum achievable +baud rate is 3Mbaud, making time segment 333.3 nanoseconds long +and the overall waveform 1.33 microseconds long. + +This is within the device's (LED's) tolerances. Delay between bits +is also acceptable. + +This solution only uses first 3 UART data bits (and the start bit), +leaving 5 additional UART bits unused. Each LED bit consumes 4 UART +data bits so it is even possible to pack second LED data bit +into the same UART data byte. The only UART bits left unused +after that are UART bit 7 and stop bit. Both need to be set +to logical 1 (stop bit always is.) This will cause slight delay +between LED bits but that should not be a problem - the LEDs only +reset the transmission after 280 microseconds of logical 0. + +With 8 UART data bits, +there is nothing more to be done. However, the chip has the ability +to transmit (and receive) 9 data bits. With parity and stop bit added, +that's 4 additional UART bits that can hold third LED data bit. We +only need to be able to set the required value in parity bit and +that can be done because each LED bit always differs in two UART bits +and parity value therefore only depends on last LED bit. Stop bit +is always set to one, even parity is in use. + +Complete UART frames look like this (LED bits shown in order in which +they appear on the line.) + +.. code-block:: text + + LED UART St 0 1 2 3 4 5 6 7 8 P Sp + 0 0 0 0 1 1 1 0 1 1 1 0 1 1 1 + 0 0 1 0 1 1 1 0 1 1 1 0 0 0 1 + 0 1 0 0 1 1 1 0 0 0 1 0 1 1 1 + 0 1 1 0 1 1 1 0 0 0 1 0 0 0 1 + 1 0 0 0 0 0 1 0 1 1 1 0 1 1 1 + 1 0 1 0 0 0 1 0 1 1 1 0 0 0 1 + 1 1 0 0 0 0 1 0 0 0 1 0 1 1 1 + 1 1 1 0 0 0 1 0 0 0 1 0 0 0 1 + +This is used for RGB LEDs (ie. LEDs without white component) because those +have 24 total data bits, number divisible by 3 without remainder. For LEDs +with white color component, the situation is different. Those have 32 data +bits so there is nothing to be placed into the last 4 bits of the last frame. +We need to make the line idle which is impossible: + +.. code-block:: text + + LED UART St 0 1 2 3 4 5 6 7 8 P Sp + 0 0 X 0 1 1 1 0 1 1 1 1 1 0 1 + +The line output will contain an unwanted logical 1 pulse in parity +bit. This could possibly be solved by pushing bits from the next +LED in the lane into the previous UART block. That complicates things +significantly and depending on the LED count, it may not be a solution +either. + +For this reason, only two bits per UART frame are sent. There is no need +to use 9th bit nor stop bit. + +.. code-block:: text + + LED UART St 0 1 2 3 4 5 6 7 Sp + 0 0 0 1 1 1 0 1 1 1 1 1 + 0 1 0 1 1 1 0 0 0 1 1 1 + 1 0 0 0 0 1 0 1 1 1 1 1 + 1 1 0 0 0 1 0 0 0 1 1 1 + +This driver needs to enable ``DRE`` interrupt for the USART device in use. + +Limitations +=========== + +One transmission at a time +-------------------------- + +While this CPU is able to control these LEDs, it is a very demanding task +for it. Even when using a peripheral to send the bits, simply pushing more +data to that peripheral's I/O registers takes a lot of time. (For example, +pushing bits via an UART peripheral takes 96 clock ticks of time and it takes +over 50 clock ticks worth of instructions to make that happen.) + +For this reason, even when using multiple lanes connected to different +peripheral, only one is being transmitted at a time. Lanes are serviced +in round-robin fashion. + +Even with this, expect relatively high (50-80% for UART) CPU load during +transmission. + +RGB vs BGR bit order +-------------------- + +The driver was tested with a WS2812 LED which requires data bits to be +sent in RGB order (despite datasheet provided by the retailer claiming +that the order is GBR.) It should be working correctly with the GBR-variant +though - it is the RGB variant that is going to have red and green colors +swapped. + +(RGB order on the LED was confirmed by a simple non-NuttX application +that simply bitbanged the bits and used ``nop`` instruction for timing.) + +Configuration +============= + +Driver is enabled using +:menuselection:`System Type --> AVR DA/DB Drivers --> Enable driver for WS2812 LEDs`. + +To be able to enable this option, it is first needed to enable: + - :menuselection:`System Type --> Enable software interrupt emulation` + (needs additional settings described in :doc:`software_interrupt`) + - :menuselection:`System Type --> Enable high priority interrupts` + (enable appropriate interrupt for the chosen peripheral) + - :menuselection:`Device Drivers --> LED Support --> WS2812 LED Driver`, + the driver needs to be configured as :menuselection:`New non-SPI driver` + in :menuselection:`WS2812 driver variant` + - :menuselection:`RTOS Features --> RTOS hooks --> Custom board late initialization` + +After the driver is enabled, it is also needed to choose the peripheral +which is connected to the LED chain +in :menuselection:`AVR DA/DB Drivers`. The driver currently supports UART +peripherals. For example, select +:menuselection:`Enable WS2812 lane on USART0` to use USART0 peripheral. + +Note that the peripheral must not be used by other drivers. Choosing +an option also selects corresponding high priority interrupt handling. + +If you want to use alternative pinout for the selected peripheral, +use configuration options in :menuselection:`AVR DA/DB Peripheral Selections` + +Upper-half options +------------------ + +:menuselection:`ws2812s have white LED` option in the upper-half driver +enables support for WS2812-alike LEDs that have white component. The code +supporting this option is present in the driver but disabled - author +of the driver did not have any such device to use for testing. + +Also note that using this option increases code size of the driver +to take care of necessary branching. Also increased is RAM +usage - for example, the UART peripheral converts RGB data to 8 bytes +of UART data without white component but to 12 bytes with white component +(per LED.) + +Additional options +------------------ + +:menuselection:`Limit number of LEDs in lane for faster execution` reduces +LED count limit to speed the ISR execution up. When pixel data is submitted +to the driver, the pixels are converted to a format that allows the ISR +to just load next byte from an array and send it. Loading next byte involves +an index to the array - selecting this option +reduces this index to a single byte, saving few instructions. This imposes +LED count limit for a lane - 31 LEDs without the white component and 21 LEDs +with the white component. + +Note that the driver also imposes hardcoded limit of 255 LEDs per lane. +Keep in mind that each LED's RGB data expands into more than 24(32) bits +of converted data and that the device is memory-constrained. + +:menuselection:`Disable reading the pixel buffer` reduces memory usage +of the driver by not allocating memory to store RGB data. The upper-half +driver provides both ``write`` and ``read`` syscalls, ``read`` allows +the user to read RGB data written previously. + +If this option is selected, that ability is removed and ``read()`` +returns ``ENOSYS``. + +:menuselection:`Suppress warning for white LED`. As mentioned above, code +supporting LEDs with white component is present in the driver but disabled. +If the WS2812 upper-half driver enables support for such LEDs, the compiler +will issue a warning. + +Use this option if you need to have the setting enabled in the upper-half. + +Usage +===== + +Initialization +-------------- + +Lane is initialized by a call to ``avrdx_ws2812_register()`` + +.. c:function:: int avrdx_ws2812_register (const char *path, uint8_t count, enum avrdx_ws2812_lane_e lane, bool has_white) + + ``avrdx_ws2812_register()`` initializes a Lane, that is a set of WS2812 + LEDs connected serially and driven from single hardware device + on the MCU side. + + :param path: device path for this LED lane, for example ``"/dev/led0"`` + :param count: LED count. Limited to 255 regardless of upper half setting + :param lane: enum that identifies peripheral to which are the LEDs connected + on the board + :param has_white: True if the lane has LEDs with white component. + (Ignored if CONFIG_WS2812_HAS_WHITE is unset.) + + :return: ``OK`` or negated ``errno``. In particular, ``errno`` can be set + to: + + - ``ENOMEM`` - memory allocation failures + - ``EINVAL`` - count parameter too high + - ``ENOTSUP`` - parameter ``has_white`` was set and is not ignored + +Note that the driver will reset the LEDs connected to the peripheral +automatically during boot and driver initialization. This accounts +for a possibility that the system was already +running and it was only the MCU that reset. In such case, it is not known +how much time elapsed after last transmission and if enough time elapsed +so the LEDs consider the transmission reset (see the datasheet for details.) + +For this reason, the driver will check for the uptime. If the uptime is lower +than 330 microseconds, the ``avrdx_ws2812_register()`` will sleep (for as much +time as needed for the uptime to exceed this value.) + +The possibility that the driver will sleep requires +:menuselection:`Custom board late initialization` (``BOARD_LATE_INITIALIZE``) +to be enabled. + +Example initialization: + +.. code-block:: c + + ret = avrdx_ws2812_register("/dev/led0", 24, AVRDX_WS2812_LANE_USART0, false); + +Writing and reading pixel data +------------------------------ + +This does not differ from other platforms - open the character device file +and write the data to it. + +.. code-block:: c + + uint32_t pixels[4]; + + led0fd = open("/dev/led0", O_RDWR); + write(led0fd, pixels, sizeof(uint32_t) * 4); + +Ability to read the data may be restricted by configuration - see above. diff --git a/arch/avr/src/avrdx/Kconfig b/arch/avr/src/avrdx/Kconfig index 2e3388d25ea..82783b3526c 100644 --- a/arch/avr/src/avrdx/Kconfig +++ b/arch/avr/src/avrdx/Kconfig @@ -165,7 +165,7 @@ config AVR_USART0 choice AVR_USART0_MUX prompt "USART0 pin selection" - depends on AVR_USART0 + depends on AVR_USART0 || AVR_AVRDX_WS2812_USART0 default AVR_USART0_DEFAULT ---help--- Select which I/O pins will be driven by USART0 interface. @@ -199,7 +199,7 @@ config AVR_USART1 choice AVR_USART1_MUX prompt "USART1 pin selection" - depends on AVR_USART1 + depends on AVR_USART1 || AVR_AVRDX_WS2812_USART1 default AVR_USART1_DEFAULT ---help--- Select which I/O pins will be driven by USART1 interface. @@ -234,7 +234,7 @@ config AVR_USART2 choice AVR_USART2_MUX prompt "USART2 pin selection" - depends on AVR_USART2 + depends on AVR_USART2 || AVR_AVRDX_WS2812_USART2 default AVR_USART2_DEFAULT ---help--- Select which I/O pins will be driven by USART2 interface. @@ -269,7 +269,7 @@ config AVR_USART3 choice AVR_USART3_MUX prompt "USART3 pin selection" - depends on AVR_USART3 + depends on AVR_USART3 || AVR_AVRDX_WS2812_USART3 default AVR_USART3_DEFAULT ---help--- Select which I/O pins will be driven by USART3 interface. @@ -304,7 +304,7 @@ config AVR_USART4 choice AVR_USART4_MUX prompt "USART4 pin selection" - depends on AVR_USART4 + depends on AVR_USART4 || AVR_AVRDX_WS2812_USART4 default AVR_USART4_DEFAULT ---help--- Select which I/O pins will be driven by USART4 interface. @@ -339,7 +339,7 @@ config AVR_USART5 choice AVR_USART5_MUX prompt "USART5 pin selection" - depends on AVR_USART5 + depends on AVR_USART5 || AVR_AVRDX_WS2812_USART5 default AVR_USART5_DEFAULT ---help--- Select which I/O pins will be driven by USART5 interface. @@ -542,6 +542,12 @@ endif endmenu # AVR DA/DB Peripheral Selections +menu "AVR DA/DB Drivers" + +source "arch/avr/src/avrdx/Kconfig-drivers" + +endmenu # AVR DA/DB Drivers + config AVR_AVRDX_SWINT bool "Enable software interrupt emulation" default false diff --git a/arch/avr/src/avrdx/Kconfig-drivers b/arch/avr/src/avrdx/Kconfig-drivers new file mode 100644 index 00000000000..95d0e3ef993 --- /dev/null +++ b/arch/avr/src/avrdx/Kconfig-drivers @@ -0,0 +1,126 @@ +config AVR_AVRDX_DRIVER_WS2812 + bool "Enable driver for WS2812 LEDs" + default false + depends on AVR_AVRDX_SWINT + depends on AVRDX_HIGH_PRIO_INTERRUPTS + depends on WS2812 + depends on WS2812_NON_SPI_DRIVER + depends on BOARD_LATE_INITIALIZE + ---help--- + Enable this to build lower-half driver for WS2812 LED. + + This driver uses a concept of "lanes". A lane is a set of WS2812 + LEDs connected serially and driven from single hardware device + (peripheral) on the MCU side. + + You need to select at least one peripheral to use this driver. + Selected peripheral cannot be used by its regular driver and + high priority interrupts will be enabled for it. (Or at least + for its output-related interrupt.) + +config AVR_AVRDX_WS2812_USART0 + bool "Enable WS2812 lane on USART0" + default false + depends on AVR_AVRDX_DRIVER_WS2812 + depends on AVR_HAVE_USART_2 + depends on !AVR_USART0 + select AVRDX_HIGH_PRIO_USART0_DRE + +config AVR_AVRDX_WS2812_USART1 + bool "Enable WS2812 lane on USART1" + default false + depends on AVR_AVRDX_DRIVER_WS2812 + depends on AVR_HAVE_USART_2 + depends on !AVR_USART1 + select AVRDX_HIGH_PRIO_USART1_DRE + +config AVR_AVRDX_WS2812_USART2 + bool "Enable WS2812 lane on USART2" + default false + depends on AVR_AVRDX_DRIVER_WS2812 + depends on AVR_HAVE_USART_2 + depends on !AVR_USART2 + select AVRDX_HIGH_PRIO_USART2_DRE + +config AVR_AVRDX_WS2812_USART3 + bool "Enable WS2812 lane on USART3" + default false + depends on AVR_AVRDX_DRIVER_WS2812 + depends on AVR_HAVE_USART_4 # this is correct, means 3 and 4 + depends on !AVR_USART3 + select AVRDX_HIGH_PRIO_USART3_DRE + +config AVR_AVRDX_WS2812_USART4 + bool "Enable WS2812 lane on USART4" + default false + depends on AVR_AVRDX_DRIVER_WS2812 + depends on AVR_HAVE_USART_4 + depends on !AVR_USART4 + select AVRDX_HIGH_PRIO_USART4_DRE + +config AVR_AVRDX_WS2812_USART5 + bool "Enable WS2812 lane on USART5" + default false + depends on AVR_AVRDX_DRIVER_WS2812 + depends on AVR_HAVE_USART_5 + depends on !AVR_USART5 + select AVRDX_HIGH_PRIO_USART5_DRE + +config AVR_AVRDX_WS2812_SHORT_LANE + bool "Limit number of LEDs in lane for faster execution" + default false + depends on AVR_AVRDX_DRIVER_WS2812 + ---help--- + When the LED data is submitted by the application, the bits + are converted to whatever data the hardware needs to be able + to transmit those bits. Generally, that means using more + memory - for example, the UART lanes convert each 3 bytes + of LED data into 8 + 1 bytes of UART data (more if the LEDs + have white component.) Walking through this data needs + an iterator variable (index) - data type needed for this + variable is determined by LED count. + + If you select this option, the LED count is limited to make + uint8_t large enough to hold the index. The limitation is + 31 LEDs without support for white LED (see WS2812_HAS_WHITE) + or 21 LEDs with it. + + If you do not select it, the LED count is limited to a hardcoded + limit of 255 LEDs (per lane.) + +config AVR_AVRDX_WS2812_NO_READS + bool "Disable reading the pixel buffer" + default false + depends on AVR_AVRDX_DRIVER_WS2812 + ---help--- + The upper half driver expects to be able to read data present + in the pixel buffer. That means that the lower half driver + needs to store what it receives in write call. + + The data is, however, also stored in the form suitable + for transmission by the hardware. In essence, everything + is stored twice. + + If your application never reads data from the pixel buffer, + you can enable this option. Any attempt to read will return + ENOSYS and the pixel buffer will not be allocated. + +config AVR_AVRDX_WS2812_NO_WHITE_LED + bool "Suppress warning for white LED" + depends on AVR_AVRDX_DRIVER_WS2812 + depends on WS2812_HAS_WHITE + default false + ---help--- + This driver implements support for LEDs with white component + but the author of the code did not have any such device + to test. Therefore, support for these LEDs is disabled + and the compiler will issue a warning. Any attempt to register + LED with has_white set will return an error. + + It is preferred to disable WS2812_HAS_WHITE altogether but + if you need it on (for example because the system has some other + WS2812 device using different lower-half), you can use this + option to suppress the compile-time warning and reduce + code size. + + The code will be compiled as if WS2812_HAS_WHITE was unset. diff --git a/arch/avr/src/avrdx/Make.defs b/arch/avr/src/avrdx/Make.defs index 67f734b0be3..3a3c0b9a5b0 100644 --- a/arch/avr/src/avrdx/Make.defs +++ b/arch/avr/src/avrdx/Make.defs @@ -77,3 +77,8 @@ endif ifeq ($(CONFIG_AVR_AVRDX_SWINT),y) CHIP_CSRCS += avrdx_swint.c endif + +ifeq ($(CONFIG_AVR_AVRDX_DRIVER_WS2812),y) +CHIP_CSRCS += avrdx_ws2812.c +CHIP_ASRCS += avrdx_ws2812_uart_store.S avrdx_ws2812_uart_xmit.S +endif diff --git a/arch/avr/src/avrdx/avrdx_swint.h b/arch/avr/src/avrdx/avrdx_swint.h index f4c6184a664..cb177963d3c 100644 --- a/arch/avr/src/avrdx/avrdx_swint.h +++ b/arch/avr/src/avrdx/avrdx_swint.h @@ -33,6 +33,18 @@ * Pre-processor Definitions ****************************************************************************/ +#if (CONFIG_AVRDX_SWINT_MAX_VECTORS <= 8) +# define AVRDX_SWINT_VECTORS_SIZE 1 +#elif (CONFIG_AVRDX_SWINT_MAX_VECTORS <= 16) +# define AVRDX_SWINT_VECTORS_SIZE 2 +#elif (CONFIG_AVRDX_SWINT_MAX_VECTORS <= 24) +# define AVRDX_SWINT_VECTORS_SIZE 3 +#elif (CONFIG_AVRDX_SWINT_MAX_VECTORS <= 32) +# define AVRDX_SWINT_VECTORS_SIZE 4 +#else +# error Kconfig should allow 32 vectors at most +#endif + /**************************************************************************** * Public Types ****************************************************************************/ diff --git a/arch/avr/src/avrdx/avrdx_ws2812.c b/arch/avr/src/avrdx/avrdx_ws2812.c new file mode 100644 index 00000000000..657b93e38ae --- /dev/null +++ b/arch/avr/src/avrdx/avrdx_ws2812.c @@ -0,0 +1,1342 @@ +/**************************************************************************** + * arch/avr/src/avrdx/avrdx_ws2812.c + * Driver for programmable RGB LED WS12812 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * You can read on design philosophy of this driver in the Documentation + * directory (platforms/avr/avrdx/docs/ws2812) + * + * The gist is that the implementation strives to be as fast as possible + * because with 800kHz LED bit frequency, the chip has 30 clock cycles + * to process each bit. + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include <nuttx/config.h> +#include <stdint.h> +#include <nuttx/leds/ws2812.h> +#include <nuttx/kmalloc.h> +#include <nuttx/arch.h> +#include <nuttx/clock.h> +#include <assert.h> +#include <stdlib.h> + +#include "avrdx_ws2812.h" +#include "avrdx_serial.h" +#include "avrdx_swint.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* This code assumes that WS2812_RW_PIXEL_SIZE == 4 */ + +static_assert(WS2812_RW_PIXEL_SIZE == 4, + "This code assumes that WS2812_RW_PIXEL_SIZE == 4"); + +#if (defined CONFIG_WS2812_HAS_WHITE) && \ + (defined CONFIG_AVR_AVRDX_WS2812_NO_WHITE_LED) +# undef CONFIG_WS2812_HAS_WHITE +#endif + +#ifdef CONFIG_WS2812_HAS_WHITE + +/* Author of this code does not have any such LED on hand and did not + * succeed in finding a datasheet for one. Color component ordering + * is unknown and was simply copied from driver for another chip. + * (arch/risc-v/src/rp23xx-rv/rp23xx_ws2812.c) + * + * Since the code cannot be tested, it is DISABLED. Calling register + * method avrdx_ws2812_register with has_white set will cause + * it to return ENOTSUP. To save program memory space, disable + * WS2812_HAS_WHITE in Kconfig if you can. + * + * In case someone with such a LED tests this code, please submit + * a pull request that removes this warning and re-enables has_white + * in avrdx_ws2812_register. + */ + +# warning CONFIG_WS2812_HAS_WHITE is untested and is disabled.\ + (see comment in source) +#endif + +/* Note - position of data in various arrays is static for each lane; + * for unused lanes, the position is left empty. Size is therefore + * determined solely from highest used number.) + * + * For future edits: the ordering of existing items MUST be preserved, + * code all around uses these values as offsets for various calculations. + */ + +#if defined(CONFIG_AVR_AVRDX_WS2812_USART5) +# define AVRDX_WS2812_LANE_SIZE 6 +#elif defined(CONFIG_AVR_AVRDX_WS2812_USART4) +# define AVRDX_WS2812_LANE_SIZE 5 +#elif defined(CONFIG_AVR_AVRDX_WS2812_USART3) +# define AVRDX_WS2812_LANE_SIZE 4 +#elif defined(CONFIG_AVR_AVRDX_WS2812_USART2) +# define AVRDX_WS2812_LANE_SIZE 3 +#elif defined(CONFIG_AVR_AVRDX_WS2812_USART1) +# define AVRDX_WS2812_LANE_SIZE 2 +#elif defined(CONFIG_AVR_AVRDX_WS2812_USART0) +# define AVRDX_WS2812_LANE_SIZE 1 +#else +# error There is no enabled lane +#endif + +/* This is specific for UART lanes - arrays that are not relevant + * for other lane types do not need to allocate space for them + */ + +#if defined(CONFIG_AVR_AVRDX_WS2812_USART5) +# define AVRDX_WS2812_LANE_ANY_USART +# define AVRDX_WS2812_LANE_USART_SIZE 6 +#elif defined(CONFIG_AVR_AVRDX_WS2812_USART4) +# define AVRDX_WS2812_LANE_ANY_USART +# define AVRDX_WS2812_LANE_USART_SIZE 5 +#elif defined(CONFIG_AVR_AVRDX_WS2812_USART3) +# define AVRDX_WS2812_LANE_ANY_USART +# define AVRDX_WS2812_LANE_USART_SIZE 4 +#elif defined(CONFIG_AVR_AVRDX_WS2812_USART2) +# define AVRDX_WS2812_LANE_ANY_USART +# define AVRDX_WS2812_LANE_USART_SIZE 3 +#elif defined(CONFIG_AVR_AVRDX_WS2812_USART1) +# define AVRDX_WS2812_LANE_ANY_USART +# define AVRDX_WS2812_LANE_USART_SIZE 2 +#elif defined(CONFIG_AVR_AVRDX_WS2812_USART0) +# define AVRDX_WS2812_LANE_ANY_USART +# define AVRDX_WS2812_LANE_USART_SIZE 1 +#else +# define AVRDX_WS2812_LANE_USART_SIZE 0 +#endif + +/* Avoid magic numbers in the code */ + +#ifdef CONFIG_WS2812_HAS_WHITE +# define WS2812_BYTES_PER_LED_WITH_WHITE 12 +#endif + +#define WS2812_BYTES_PER_LED_NO_WHITE 8 + +#define WS2812_MAX_LED_COUNT 255 + +/* This is 5 * 65536 - allows the compiler to simplify some comparisons. + * The value of T_reset specified in the datasheet is 280us. (Value + * given in nanoseconds to be suitable for tv_nsec in struct timespec.) + */ + +#define WS2812_T_RESET (327680) + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +/* Size of index needed to address all data for transmission. + * Use smallest data type if the user asked for it. + */ + +#ifdef CONFIG_AVR_AVRDX_WS2812_SHORT_LANE +typedef uint8_t avrdx_ws2812_idx_t; +#else +/* Needs to be able to index transmit data - which is not the same + * as pixel buffer - for hardcoded limit of 255 LEDs + */ + +typedef uint16_t avrdx_ws2812_idx_t; +#endif + +/* This is stored in inode and we get it to recognize which character + * device file we are working with at the moment. There should be more + * information in the struct but we need to store most data in flat + * arrays so they are accessible from assembler. + */ + +struct avrdx_ws2812_s +{ + enum avrdx_ws2812_lane_e lane; /* Lane - used hardware */ +#ifndef CONFIG_AVR_AVRDX_WS2812_NO_READS + /* There is a room for improvement - if CONFIG_WS2812_HAS_WHITE + * is not set, we only need 24 bits to store the pixel (uint24_t + * is supported by GCC.) + * + * Not implemented though - better memory savings can be achieved + * by not storing the data altogether. + */ + + uint32_t *pixel_buffer; +#endif +}; + +#if (AVRDX_WS2812_LANE_SIZE <= 8) +typedef uint8_t avrdx_ws2812_bitmap_t; +#elif (AVRDX_WS2812_LANE_SIZE <= 16) +typedef uint16_t avrdx_ws2812_bitmap_t; +#elif (AVRDX_WS2812_LANE_SIZE <= 32) +typedef uint32_t avrdx_ws2812_bitmap_t; +#else +# error Having more than 32 lines is not supported +#endif + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +static void trigger_transmission_hardware(void); + +/* Technically not private since we load it from different file + * but it is only supposed to be used by store_uart_bytes() + * (Ie. it would be in this file if not written in assembler.) + */ + +void avrdx_ws2812_store_uart_bytes_rgb(enum avrdx_ws2812_lane_e lane, + uint32_t *data, + uint8_t offset, + uint8_t len); + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +/* Values here correspond to the Documentation file describing this driver + * (platforms/avr/avrdx/docs/ws2812.rst) Declared in private data section + * despite not being static - used by a function written in assembler. + */ + +const uint8_t IOBJ avrdx_ws2812_uart_white[4] = +{ + 0xf7, /* 0b11110111 */ + 0xc7, /* 0b11000111 */ + 0xf4, /* 0b11110100 */ + 0xc4 /* 0b11000100 */ +}; + +static avrdx_ws2812_bitmap_t avrdx_ws2812_dirty_lanes; +static avrdx_ws2812_bitmap_t avrdx_ws2812_active_lane; + +/**************************************************************************** + * Public Data + ****************************************************************************/ + +/* Array of pointers to uint8_t. Each lane allocates appropriate amount + * of bytes and stores the pointer here. Used by assembler code, needs + * to be outside of struct. + */ + +uint8_t *g_avrdx_ws2812_xmit_bytes[AVRDX_WS2812_LANE_SIZE]; + +/* Next byte to be sent. Hardware for a lane may need additional data, + * defined below. + */ + +avrdx_ws2812_idx_t g_avrdx_ws2812_xmit_next[AVRDX_WS2812_LANE_SIZE]; + +/* Total count of bytes to be sent */ + +avrdx_ws2812_idx_t g_avrdx_ws2812_xmit_count[AVRDX_WS2812_LANE_SIZE]; + +/* Software interrupt vectors to signal completion */ + +avrdx_swint_size_t g_avrdx_ws2812_swint_vector[AVRDX_WS2812_LANE_SIZE]; + +/* Array of pointers to struct timespec which records the last time + * the lane data was transmitted. Used to ensure that the T_reset + * period elapses between retransmissions. + */ + +struct timespec *g_avrdx_ws2812_xmit_ts[AVRDX_WS2812_LANE_SIZE]; + +#ifdef AVRDX_WS2812_LANE_ANY_USART + +/* UART lanes use 9 bit transmissions. 9th bits are stored here. + * For each group of 8 bytes, one byte is allocated here and + * 8x 9th bits are stored in it. LSB holds 9th bit for byte + * with lowest index. + */ + +uint8_t *g_avrdx_ws2812_uart9_bytes[AVRDX_WS2812_LANE_USART_SIZE]; + +/* This is a working copy from g_avrdx_ws2812_uart9_bytes. Fetched + * in ISR every time xmit_next % 8 == 0, shifted to the right + * after each UART byte is transmitted. + * + * The highest bit cannot be set in this value except between + * the fetch and first shift. This means that it cannot be set + * when we enter the transmit ISR (because it does the shift + * after fetch.) We are therefore using this highest bit as a signal. + * If set, the LEDs have white component and 9 bit transmission + * is actually not in use. This check is skipped + * if CONFIG_WS2812_HAS_WHITE is unset. + */ + +uint8_t g_avrdx_ws2812_uart9_current[AVRDX_WS2812_LANE_USART_SIZE]; + +#endif + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: transmission_completed + * + * Description: + * IRQ callback called by software interrupt triggered by lane transmit + * handler (for example UART Data Register Empty ISR) when it transmits + * all bytes to LEDs + * + * Input Parameters: + * irq, context - unused + * arg - lane number (given as pointer, type cast is needed) + * + * Returned Value: OK + * + * Assumptions/Limitations: + * Running in interrupt context. + * + ****************************************************************************/ + +static int transmission_completed(int irq, void *context, FAR void *arg) +{ + enum avrdx_ws2812_lane_e lane; + + /* Record the time - lane must not retransmit its data until + * T_reset period in the LEDs elapses. + */ + + lane = (enum avrdx_ws2812_lane_e) arg; + up_timer_gettime(g_avrdx_ws2812_xmit_ts[lane]); + + /* Trigger next transmission from next lane. Or not if no lane is dirty, + * the function takes care of both cases. + */ + + trigger_transmission_hardware(); + + return OK; +} + +/**************************************************************************** + * Name: trigger_transmission_hardware + * + * Description: + * Start transmission. If currently transmitting lane is not set, start + * transmission from lowest dirty lane. (Dirty means lane that has new + * data not transmitted to the LEDs.) + * + * If it is set, transmission of that (currently transmitting) lane just + * finished, find next dirty lane to transmit. + * + * Input Parameters: none + * + * Returned Value: void + * + * Assumptions/Limitations: + * - may be running in interrupt context + * - interrupts are disabled by the caller or by the (interrupt) context + * - only called if there is no active transmission or if it + * just finished + * + ****************************************************************************/ + +static void trigger_transmission_hardware(void) +{ + uint8_t lane_idx; + avrdx_ws2812_bitmap_t lane_calc; + + if (avrdx_ws2812_active_lane == 0) + { + /* No active lane - find first dirty */ + + if (avrdx_ws2812_dirty_lanes == 0) + { + /* This function is not supposed to be called + * under these conditions. + */ + + return; + } + + /* Lowest lane in bitmap. (This expression finds lowest + * bit that is set in the value.) + */ + + avrdx_ws2812_active_lane = \ + avrdx_ws2812_dirty_lanes & ((~avrdx_ws2812_dirty_lanes) + 1); + } + else + { + /* Active lane set means that some transmission finished. + * Move on to the next one. + */ + + if (avrdx_ws2812_dirty_lanes == 0) + { + /* There is no next one, everything is up-to-date */ + + avrdx_ws2812_active_lane = 0; + return; + } + + while (1) + { + avrdx_ws2812_active_lane <<= 1; + if (avrdx_ws2812_active_lane == 0) + { + avrdx_ws2812_active_lane = 1; /* wrap */ + } + + if (avrdx_ws2812_active_lane & avrdx_ws2812_dirty_lanes) + { + /* Found dirty lane, transmit that */ + + break; + } + } + } + + /* At this point the function either returned or the lane + * to be transmitted next is set in avrdx_ws2812_active_lane. + * + * Mark the lane as not dirty now. This will allow write calls to submit + * new data. But only after T_reset elapses, which the write must check. + */ + + avrdx_ws2812_dirty_lanes &= ~avrdx_ws2812_active_lane; + + /* Convert active lane - bitmap - to index. Bitshifting to the right + * will eventually make the value 0 and that's the index we want. + * (Bitshift in the initialization compensates for the fact that + * value of 1 means lane_idx == 0 and the for loop must not run.) + */ + + lane_idx = 0; + for (lane_calc = avrdx_ws2812_active_lane >> 1; lane_calc; lane_calc >>= 1) + { + lane_idx++; + } + + /* Reset byte counter */ + + g_avrdx_ws2812_xmit_next[lane_idx] = 0; + + /* Hardware-specific action. Currently only UART */ + + if (lane_idx < AVRDX_WS2812_LANE_USART_SIZE) + { + AVRDX_USART(lane_idx).CTRLA |= USART_DREIE_bm; + } +} + +/**************************************************************************** + * Name: trigger_transmission + * + * Description: + * Trigger transmission on a lane. Intended to be called after a write + * (or other operation that changed pixel data.) Only flags the lane + * data as dirty, then calls common method (if needed.) + * + * Input Parameters: + * - lane - lane with dirty data to be written out + * + * Returned Value: void + * + * Assumptions/Limitations: + * - it is assumed that the caller holds mutex but the implementation also + * disables interrupts. + * - this function may sleep - interrupts must not be disabled + * by the caller + * + ****************************************************************************/ + +static void trigger_transmission(enum avrdx_ws2812_lane_e lane) +{ + irqstate_t irqstate; + struct timespec ts; /* Temporary value for calculations */ + avrdx_ws2812_bitmap_t lane_bit; /* Cache that avoids recomputing + * a bitshift */ + uint32_t u32; /* Computation variables */ + uint16_t u16; + + /* Pointer to a lane timestamp. The compiler will re-read the value + * fetched from the g_avrdx_ws2812_xmit_ts array even if not declared + * volatile but let's make sure it does it. + */ + + volatile struct timespec *lane_xmit_ts; + + /* Pointer to active lane value made volatile to ensure the compiler + * will re-read it on each loop pass (it did that in testing even + * without the volatile but again, let's make sure.) + */ + + volatile avrdx_ws2812_bitmap_t *active_lane; + + irqstate = up_irq_save(); + + /* If this lane is currently being transmitted, it must not be marked + * dirty again - that would cause immediate retransmission. Instead, wait + * until it is not. + */ + + lane_bit = (1 << lane); + active_lane = &avrdx_ws2812_active_lane; + for (; (*active_lane) & lane_bit; ) + { + /* Sleep for how long? 1.25us per bit means 30us per 24bits. + * For a millisecond wait, we need 34 LEDs at least. (Ignoring LEDs + * with white component.) Just use 2ms and let the loop handle the + * rest. + */ + + nxsched_usleep(2); + } + + /* Now we know the lane is currently not being transmitted. But we must + * block the trigger if the transmission happened recently and T_reset + * period is in progress + */ + + lane_xmit_ts = g_avrdx_ws2812_xmit_ts[lane]; + for (; ; ) + { + up_timer_gettime(&ts); + clock_timespec_subtract(&ts, lane_xmit_ts, &ts); + + /* Reset time is a microsecond value, if a second elapsed, + * there is no wait. + */ + + if (ts.tv_sec > 0) + { + break; + } + + /* tv_nsec should be positive, current time minus past, + * tell this to the compiler. + */ + + u32 = (ts.tv_nsec < 0) ? 0 : ts.tv_nsec; + + /* Is this value higher than WS2812_T_RESET? Value of WS2812_T_RESET + * is chosen to have 2 least significant bytes set to 0, simplify + * the comparison using this knowledge. + */ + +#if (WS2812_T_RESET & 0xffff) +# error Value of WS2812_T_RESET does not meet expectations here +#endif + + u16 = u32 >> 16; + if (u16 > (WS2812_T_RESET >> 16)) + { + break; + } + + /* Alright, we will be waiting. For how long? + * + * tv_nsec (u32) is a nanosecond value but we will call usleep + * with a microsecond parameter. We can obtain the latter + * from the former by dividing it by 1000. We do not like such + * solution though because it does a division. + * + * So, we just bitshift u16 6 bits to the left (u16 is value + * of tv_nsec bitshifted by 16 to the right.) That gives us + * tv_nsec >> 10 and if we accept that a microsecond is 1024 + * nanoseconds long, we are done. Or at least close to done, + * lower 6 bits of the resulting value are truncated and we need + * to compensate for that. Add 1 << 6 - waiting for more time + * is fine. + * + * Also - make sure that WS2812_T_RESET is set to a 24 bit value, + * all of this depends on that. (The next check is even more + * strict than that.) + */ + +#if WS2812_T_RESET > 0x7ffff +# error This code assumes WS2812_T_RESET to be 5 * 65536 +#endif + + nxsched_usleep((u16 << 6) + (1 << 6)); + } + + avrdx_ws2812_dirty_lanes |= lane_bit; + + if (avrdx_ws2812_active_lane == 0) + { + /* No outgoing transmission currently, start it for this lane. */ + + trigger_transmission_hardware(); + } + + up_irq_restore(irqstate); +} + +/**************************************************************************** + * Name: store_uart_bytes + * + * Description: + * Receives data from write syscall and encodes them into bytes + * to be transmitted through the UART. + * + * Input Parameters: + * - lane - lane which is being written to + * - data - pointer to uint32_t array of RGB data + * - offset - first LED to change with data, given in items + * - len - data length in items + * - has_white - set if the lane has LEDs with white component, + * ignored if CONFIG_WS2812_HAS_WHITE is not set + * + * Returned Value: void + * + * Assumptions/Limitations: + * - len is truncated so offset+len does not exceed the data buffer + * + ****************************************************************************/ + +static void store_uart_bytes(enum avrdx_ws2812_lane_e lane, + uint32_t *data, + uint8_t offset, + uint8_t len, + bool has_white) +{ +#ifdef CONFIG_WS2812_HAS_WHITE + + uint8_t *uart_bytes = g_avrdx_ws2812_xmit_bytes[lane]; + uint16_t temp_u16; + uint8_t temp_u8; + uint8_t i; + + /* UART lane with support for white component. */ + + if (has_white) + { + /* Data is received as WWRRGGBB, the LED wants GGRRBBWW, + * need to keep that in mind. + */ + + uart_bytes += WS2812_BYTES_PER_LED_WITH_WHITE * offset; + + for (; len; len--) + { + /* Little endian architecture, green is stored in byte +1, + * LED wants it first. The compiler should pick the byte we + * want without bitshifting. + * + * (From disassembly: no bitshifting but all four bytes + * are loaded from memory. Can probably be improved + * using some pointers and type casts.) + */ + +# ifdef CONFIG_ENDIAN_BIG +# error AVR is not big endian +# endif + temp_u8 = ((*data) & 0xff00) >> 8; + temp_u16 = temp_u8; + for (i = 4; i; i--) + { + /* Except this bitshift with temp_u16, the compiler + * recognizes that the rest can be done + * without shifting. Saves CPU time. + */ + + temp_u16 <<= 2; + *uart_bytes = \ + avrdx_ws2812_uart_white[(temp_u16 & 0x0300) >> 8]; + uart_bytes++; + } + + /* Red now. That one is at byte +2. The compiler only reads + * one byte from the memory... but gets the address from base + * value - 4 + 2 (sbiw, adiw) + */ + + temp_u8 = ((*data) & 0xff0000) >> 16; + temp_u16 = temp_u8; + for (i = 4; i; i--) + { + temp_u16 <<= 2; + *uart_bytes = \ + avrdx_ws2812_uart_white[(temp_u16 & 0x0300) >> 8]; + uart_bytes++; + } + + /* Blue - byte 0 */ + + temp_u8 = (*data) & 0xff; + temp_u16 = temp_u8; + for (i = 4; i; i--) + { + temp_u16 <<= 2; + *uart_bytes = \ + avrdx_ws2812_uart_white[(temp_u16 & 0x0300) >> 8]; + uart_bytes++; + } + + /* And white at byte + 3. Compiler still does not use bitshifts + * but it loads all bytes and then resets 3 of them + * (in registers) to zero. + */ + + temp_u8 = ((*data) & 0xff000000) >> 24; + temp_u16 = temp_u8; + for (i = 4; i; i--) + { + temp_u16 <<= 2; + *uart_bytes = \ + avrdx_ws2812_uart_white[(temp_u16 & 0x0300) >> 8]; + uart_bytes++; + } + + /* Done with this pixel, move onto the next one */ + + data++; + } + } + else +#endif /* ifdef CONFIG_WS2812_HAS_WHITE */ + { + /* No support for LEDs with white component or this lane + * does not have those. Conversion from RGB data to UART + * bytes is going to take a lot of bitshift and similar, + * we do this in assembly to save CPU time. + * + * Note - this method expects len to be non-zero. + */ + + if (len > 0) + { + avrdx_ws2812_store_uart_bytes_rgb(lane, data, offset, len); + } + } +} + +/**************************************************************************** + * Name: avrdx_ws2812_open + * + * Description: + * Character device open handler. We do not track open count so it does + * nothing. Must exist though, upper half does not check it for NULL. + * + * Input Parameters: + * filep - struct file pointer + * + * Returned Value: OK + * + ****************************************************************************/ + +static int avrdx_ws2812_open(struct file *filep) +{ + return OK; +} + +/**************************************************************************** + * Name: avrdx_ws2812_close + * + * Description: + * Character device close handler. Upper half does check for NULL + * pointer here but we just return OK so let's have it. + * + * Input Parameters: + * filep - struct file pointer + * + * Returned Value: + * OK + * + ****************************************************************************/ + +static int avrdx_ws2812_close(struct file *filep) +{ + return OK; +} + +/**************************************************************************** + * Name: avrdx_ws2812_write + * + * Description: + * Writes RGB data to pixel buffer (unless configured not to do so) + * and to the transmit buffer. + * + * Input Parameters: + * filep - struct file pointer + * data - pointer to buffer with data to be written + * len - size of the buffer + * + * Returned Value: + * Number of bytes written. + * + * Assumptions/Limitations: + * - value of len was checked by the caller to be multiple + * of WS2812_RW_PIXEL_SIZE. + * - value of data was checked by the call chain and is not NULL + * (fs/vfs/fs_read.c does that in file_readv()) + * - same for len != 0 (done by fs/vfs/fs_read.c in file_readv_compat()) + * + ****************************************************************************/ + +static ssize_t avrdx_ws2812_write(struct file *filep, + const char *data, size_t len) +{ + struct ws2812_dev_s *dev_data; + struct avrdx_ws2812_s *priv_data; + uint32_t *data_u32; + size_t pos; + + data_u32 = (uint32_t *) data; + dev_data = filep->f_inode->i_private; + priv_data = dev_data->private; + nxmutex_lock(&dev_data->lock); + + pos = filep->f_pos; + + /* Restrict len to not go beyond the buffer. */ + + if (len + pos > dev_data->nleds * WS2812_RW_PIXEL_SIZE) + { + len = dev_data->nleds * WS2812_RW_PIXEL_SIZE - pos; + } + +#ifndef CONFIG_AVR_AVRDX_WS2812_NO_READS + /* Write a copy to the pixel buffer */ + + memcpy(((uint8_t *) priv_data->pixel_buffer) + pos, data, len); + +#endif + + store_uart_bytes(priv_data->lane, data_u32, + pos >> 2, len >> 2, + dev_data->has_white); + + pos += len; + filep->f_pos = pos; + + trigger_transmission(priv_data->lane); + + nxmutex_unlock(&dev_data->lock); + + return len; +} + +/**************************************************************************** + * Name: avrdx_ws2812_read + * + * Description: + * Reads data from pixel buffer to provided pointer and returns them. + * Not compiled if CONFIG_AVR_AVRDX_WS2812_NO_READS is set. + * + * Input Parameters: + * filep - struct file pointer + * data - pointer to buffer which receives read data + * len - size of the buffer + * + * Returned Value: + * Number of bytes read. + * + * Assumptions/Limitations: + * Identical to avrdx_ws2812_write + * + ****************************************************************************/ + +#ifndef CONFIG_AVR_AVRDX_WS2812_NO_READS + +static ssize_t avrdx_ws2812_read(struct file *filep, + char *data, size_t len) +{ + struct ws2812_dev_s *dev_data; + struct avrdx_ws2812_s *priv_data; + size_t pos; + + dev_data = filep->f_inode->i_private; + priv_data = dev_data->private; + + nxmutex_lock(&dev_data->lock); + + pos = filep->f_pos; + + /* Restrict len to not go beyond the buffer. */ + + if (len + pos > dev_data->nleds * WS2812_RW_PIXEL_SIZE) + { + len = dev_data->nleds * WS2812_RW_PIXEL_SIZE - pos; + } + + memcpy(data, ((uint8_t *) priv_data->pixel_buffer) + pos, len); + pos += len; + filep->f_pos = pos; + + nxmutex_unlock(&dev_data->lock); + + return len; +} + +#endif /* ifndef CONFIG_AVR_AVRDX_WS2812_NO_READS */ + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: avrdx_ws2812_register + * + * Description: + * Initializer for one WS2812 lane. Lane is a set of WS2812 LEDs + * connected serially and driven from single hardware device + * on the MCU side. + * + * Note that this function triggers transmission of its freshly + * allocated and zeroed buffer, thus resetting the LEDs + * and turning them all of. Since the function does not know + * if it runs after a power-on or after a reset (and, therefore, + * how much time elapsed from last transmission), it will simply + * wait. + * + * Input Parameters: + * path - device path for this LED lane + * count - LED count. Limited to 255 regardless of upper half setting + * (The device does not have memory for more.) + * lane - enum avrdx_ws2812_lane_e value. Selects peripheral to which + * are the LEDs connected on the board + * has_white - bool. True if the lane has LEDs with white component. + * Ignored if CONFIG_WS2812_HAS_WHITE is unset. (Or if + * CONFIG_AVR_AVRDX_WS2812_NO_WHITE_LED is set.) + * + * Returned Value: + * OK or negated errno. + * ENOMEM - if the LED count exceeds limit (in addition to memory + * allocation failure.) + * ENOTSUP - if support for white LED is requested and not disabled + * (see #warning at the beginning of the file or description + * of AVR_AVRDX_WS2812_NO_WHITE_LED configuration option) + * EINVAL - count requests more LEDs in chain than is supported + * + * Assumptions/Limitations: + * Running in a context which allows sleeping. + * + ****************************************************************************/ + +int avrdx_ws2812_register(const char *path, uint8_t count, + enum avrdx_ws2812_lane_e lane, + bool has_white) +{ + int ret = OK; + struct ws2812_dev_s *dev_data = NULL; + struct avrdx_ws2812_s *priv_data = NULL; + avr_usart_t *usart; + uint16_t lane_bytes; + volatile uint8_t *portmux_reg; + struct timespec ts; + +#ifdef CONFIG_AVR_AVRDX_WS2812_NO_READS + uint32_t *temp_pixel_buffer = NULL; +#endif + + if (count > WS2812_MAX_LED_COUNT) + { + return -EINVAL; + } + + /* Deal with reset time (T_reset) of the LEDs. We do not know if this + * code runs during a power-up or after a reset. For the latter case, + * we have no way of knowing what was happening before the reset + * and if the T_reset period of the LEDs already expired (if any + * transmission was going out just when the MCU restarted.) + * + * This function will trigger a transmission to LEDs (to turn them + * off in case this is a reset and they have previous values) + * so it needs to make sure T_reset did expire. + * + * Datasheet says T_reset = 280us, we use 5 * 65536ns to save few + * program memory bytes. T_reset did expire if our uptime is greater. + */ + + up_timer_gettime(&ts); + + if ((ts.tv_sec == 0) && (ts.tv_nsec < WS2812_T_RESET)) + { + nxsched_usleep(WS2812_T_RESET - ts.tv_nsec); + } + + /* * * * * * * Start of hardware-specific block * * * * * * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + /* * * * * * * Start of UART-specific block * * * * * * */ + +#ifdef AVRDX_WS2812_LANE_ANY_USART + if ( +# ifdef CONFIG_AVR_AVRDX_WS2812_USART0 + (lane == AVRDX_WS2812_LANE_USART0) || +# endif +# ifdef CONFIG_AVR_AVRDX_WS2812_USART1 + (lane == AVRDX_WS2812_LANE_USART1) || +# endif +# ifdef CONFIG_AVR_AVRDX_WS2812_USART2 + (lane == AVRDX_WS2812_LANE_USART2) || +# endif +# ifdef CONFIG_AVR_AVRDX_WS2812_USART3 + (lane == AVRDX_WS2812_LANE_USART3) || +# endif +# ifdef CONFIG_AVR_AVRDX_WS2812_USART4 + (lane == AVRDX_WS2812_LANE_USART4) || +# endif +# ifdef CONFIG_AVR_AVRDX_WS2812_USART5 + (lane == AVRDX_WS2812_LANE_USART5) || +# endif + 0) + { + /* This is an UART lane. Configure the hardware. This code + * is mostly a copy of what serial driver does. (PORTMUX_USARTROUTEB + * is a macro defined in io.h if the MCU has more than 4 UARTs.) + */ + +# ifdef PORTMUX_USARTROUTEB + if (lane < 4) +# endif + { + portmux_reg = &(PORTMUX.USARTROUTEA); + } +# ifdef PORTMUX_USARTROUTEB + else + { + portmux_reg = &(PORTMUX.USARTROUTEB); + } +# endif + + /* In theory, only the kernel should manipulate this register + * and the value written is static (chosen by configuration.) + * That means there should be no need to reset it. Still doing it, + * runaway application may do accidental writes. + */ + + *(portmux_reg) = \ + (\ + *(portmux_reg) & \ + ~(avrdx_usart_portmux_masks[lane]) \ + ) | \ + avrdx_usart_portmux_bits[lane]; + + /* Difference from serial driver - we must enable the pin + * inverter (and we must do it before the pin switches to output.) + */ + + AVRDX_USART_PORT(lane).PINCONFIG = PORT_INVEN_bm; + AVRDX_USART_PORT(lane).PINCTRLUPD = \ + avrdx_usart_tx_pins[lane]; + + /* AVR128DA28/32/48/64 datasheet rev C, section 25.3.1 lists + * changing the pin direction before activating the device itself + * but that leads to short zero pulse. Change output value to serial + * line idle state to prevent that. + */ + + AVRDX_USART_PORT(lane).OUTSET = \ + avrdx_usart_tx_pins[lane]; + AVRDX_USART_PORT(lane).DIRSET = \ + avrdx_usart_tx_pins[lane]; + + usart = &(AVRDX_USART(lane)); + usart->BAUD = 64; + usart->CTRLB = (USART_RXMODE_CLK2X_GC | USART_TXEN_bm); + + /* Default configuration of CTRLC is acceptable for LEDs + * with white component; the opposite case is handled + * below - amends the configuration. + */ + +# ifdef CONFIG_WS2812_HAS_WHITE + + /* UART lane with support for white component */ + + if (has_white) + { + return -ENOTSUP; + +# ifdef CONFIG_AVR_AVRDX_WS2812_SHORT_LANE + + /* Has white component and uint8_t index is enforced. + * 255 (UINT8_MAX) / 12 (bytes per LED) = 21 LEDs + */ + + if (count > (UINT8_MAX / WS2812_BYTES_PER_LED_WITH_WHITE)) + { + ret = -EINVAL; + goto errout; + } +# endif + + lane_bytes = count * WS2812_BYTES_PER_LED_WITH_WHITE; + + /* Top bit in this variable serves as a signal for the ISR + * that the LED has white component and 9th UART data bit + * is not used. + */ + + g_avrdx_ws2812_uart9_current[lane] = 0xff; + } + else +# endif /* ifdef CONFIG_WS2812_HAS_WHITE*/ + { + /* Does not have white component or white components + * not enabled at all. + */ + +# ifdef CONFIG_AVR_AVRDX_WS2812_SHORT_LANE + + /* 255 (UINT8_MAX) / 8 (bytes per LED ) = 31 LEDs */ + + if (count > (UINT8_MAX / WS2812_BYTES_PER_LED_NO_WHITE)) + { + ret = -EINVAL; + goto errout; + } +# endif + + lane_bytes = count * WS2812_BYTES_PER_LED_NO_WHITE; + + /* Will be populated in the ISR but must not have top bit + * set when the ISR is entered. That would be interpreted + * as a signal that 9th UART data bit is not in use. + * If CONFIG_WS2812_HAS_WHITE is unset, 9th UART bit is + * always used, the check in the ISR is skipped and we + * don't need to do this. + */ + +# ifdef CONFIG_WS2812_HAS_WHITE + g_avrdx_ws2812_uart9_current[lane] = 0; +# endif + + /* Allocate array for 9th bits. Each LED needs exactly + * one byte (24 bits packed into groups of 3, each group + * needs one 9th bit.) + */ + + g_avrdx_ws2812_uart9_bytes[lane] = (uint8_t *) kmm_zalloc(count); + if (g_avrdx_ws2812_uart9_bytes[lane] == NULL) + { + ret = -ENOMEM; + goto errout; + } + + /* Initialize the hardware - only the 9th bit */ + + usart->CTRLC = USART_PMODE_EVEN_GC \ + | USART_CHSIZE_9BITH_GC; + } + } + else + + /* When support for other hardware is added, we want this to continue + * with else if. Doing it like this gives us an unterminated else though, + * needs to be taken care of at the end of the hardware-specific section. + */ + + /* * * * * * * End of UART-specific block * * * * * * */ + +#else + + /* No UART lane - since to other lane type exists, that's an error. + * Should not happen though, checked elsewhere. + */ + +# error No lane selected +#endif + { + /* Every hardware-specific block ends with else to allow + * the next block to start with if, forming else if. + * This empty block executes after the last else. + * + * The compiler is unable to recognize that the list of enums + * in the condition above is exhaustive and complaints (not always!) + * that lane_bytes may be used uninitialized. Silence it. + */ + + lane_bytes = 0; + } + + g_avrdx_ws2812_xmit_bytes[lane] = (uint8_t *) kmm_zalloc(lane_bytes); + if (g_avrdx_ws2812_xmit_bytes[lane] == NULL) + { + ret = -ENOMEM; + goto errout; + } + + g_avrdx_ws2812_xmit_count[lane] = lane_bytes; + g_avrdx_ws2812_xmit_next[lane] = 0; + + g_avrdx_ws2812_xmit_ts[lane] = \ + (struct timespec *) kmm_zalloc(sizeof(struct timespec)); + if (g_avrdx_ws2812_xmit_ts[lane] == NULL) + { + ret = -ENOMEM; + goto errout; + } + + dev_data = (struct ws2812_dev_s *) kmm_zalloc(sizeof(struct ws2812_dev_s)); + if (dev_data == NULL) + { + ret = -ENOMEM; + goto errout; + } + + priv_data = \ + (struct avrdx_ws2812_s *) kmm_zalloc(sizeof(struct avrdx_ws2812_s)); + if (priv_data == NULL) + { + ret = -ENOMEM; + goto errout; + } + +#ifndef CONFIG_AVR_AVRDX_WS2812_NO_READS + priv_data->pixel_buffer = (uint32_t *) \ + kmm_zalloc(sizeof(uint32_t) * count); + if (priv_data->pixel_buffer == NULL) + { + ret = -ENOMEM; + goto errout; + } + + /* #else - pixel_buffer member is not part of the struct, + * and it would be set to zero by kmm_zalloc above anyway + */ +#endif + + priv_data->lane = lane; + + nxmutex_init(&dev_data->lock); + + /* File operation handlers */ + + dev_data->open = avrdx_ws2812_open; + dev_data->close = avrdx_ws2812_close; + dev_data->write = avrdx_ws2812_write; + +#ifndef CONFIG_AVR_AVRDX_WS2812_NO_READS + dev_data->read = avrdx_ws2812_read; + + /* #else - initialized to NULL by kmm_zalloc */ +#endif + + /* Other data */ + + dev_data->private = priv_data; + dev_data->clock = 0; /* not explained + * in include/nuttx/leds/ws2812.h, + * not present in drivers/leds/ws2812.c, + * set to zero and see what breaks. + */ + dev_data->port = 0; /* same here */ + dev_data->nleds = count; + +#ifdef CONFIG_WS2812_HAS_WHITE + dev_data->has_white = has_white; + + /* #else - left unset. The struct is allocated with kmm_zalloc + * so FALSE is already set + */ +#endif + + g_avrdx_ws2812_swint_vector[lane] = \ + avrdx_swint_allocate(transmission_completed, (void *)lane); + if (g_avrdx_ws2812_swint_vector[lane] == 0) + { + goto errout; /* No vector available */ + } + + /* xmit_bytes are initialized to zero but we can't just put zero bytes + * into the buffer - that value is not interpreted as zero by the LED. + * Instead, we populate the buffer as if it was written by userspace. + */ + +#ifdef CONFIG_AVR_AVRDX_WS2812_NO_READS + /* No reads - no pixel buffer. We need to allocate a temporary one */ + + temp_pixel_buffer = (uint32_t *) kmm_zalloc(sizeof(uint32_t) * count); + if (temp_pixel_buffer == NULL) + { + ret = -ENOMEM; + goto errout; + } + + store_uart_bytes(lane, temp_pixel_buffer, 0, count, has_white); + kmm_free(temp_pixel_buffer); +#else + store_uart_bytes(lane, priv_data->pixel_buffer, 0, count, has_white); +#endif + + ret = ws2812_register(path, dev_data); + if (ret < 0) + { + goto errout; + } + + trigger_transmission(lane); + + return ret; + +errout: + + /* Free memory that was allocated, preferably in reverse order. */ + +#ifdef CONFIG_AVR_AVRDX_WS2812_NO_READS + if (temp_pixel_buffer) + { + kmm_free(temp_pixel_buffer); + } +#endif + + if (priv_data) + { +#ifndef CONFIG_AVR_AVRDX_WS2812_NO_READS + if (priv_data->pixel_buffer) + { + kmm_free(priv_data->pixel_buffer); + } + +#endif + kmm_free(priv_data); + } + + if (dev_data) + { + kmm_free(dev_data); + } + +#ifdef AVRDX_WS2812_LANE_ANY_USART + if (g_avrdx_ws2812_uart9_bytes[lane]) + { + kmm_free(g_avrdx_ws2812_uart9_bytes[lane]); + } +#endif + + if (g_avrdx_ws2812_xmit_ts[lane]) + { + kmm_free(g_avrdx_ws2812_xmit_ts[lane]); + } + + if (g_avrdx_ws2812_xmit_bytes[lane]) + { + kmm_free(g_avrdx_ws2812_xmit_bytes[lane]); + } + + return ret; +} diff --git a/arch/avr/src/avrdx/avrdx_ws2812.h b/arch/avr/src/avrdx/avrdx_ws2812.h new file mode 100644 index 00000000000..2aec6bb051b --- /dev/null +++ b/arch/avr/src/avrdx/avrdx_ws2812.h @@ -0,0 +1,174 @@ +/**************************************************************************** + * arch/avr/src/avrdx/avrdx_ws2812.h + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +#ifndef __ARCH_AVR_SRC_AVRDX_AVRDX_WS2812_H +#define __ARCH_AVR_SRC_AVRDX_AVRDX_WS2812_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include <nuttx/config.h> +#include <assert.h> + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +/* Lane size 1 is guaranteed, others are not. (That's why first #if + * does not check the size but others do.) Each lane has a static + * value (or is not defined if not enabled.) + * + * For future edits: lane numbers MUST NOT be redefined, the code uses + * the numeric values for various decisions, offset calculations etc. + * + * This enum is internally converted to a pointer, its size must fit + * pointer size. Asserted below the definition. + */ + +enum avrdx_ws2812_lane_e +{ +#ifdef CONFIG_AVR_AVRDX_WS2812_USART0 + AVRDX_WS2812_LANE_USART0 = 0, +#endif +#ifdef CONFIG_AVR_AVRDX_WS2812_USART1 + AVRDX_WS2812_LANE_USART1 = 1, +#endif +#ifdef CONFIG_AVR_AVRDX_WS2812_USART2 + AVRDX_WS2812_LANE_USART2 = 2, +#endif +#ifdef CONFIG_AVR_AVRDX_WS2812_USART3 + AVRDX_WS2812_LANE_USART3 = 3, +#endif +#ifdef CONFIG_AVR_AVRDX_WS2812_USART4 + AVRDX_WS2812_LANE_USART4 = 4, +#endif +#ifdef CONFIG_AVR_AVRDX_WS2812_USART5 + AVRDX_WS2812_LANE_USART5 = 5 +#endif +}; + +static_assert(sizeof(enum avrdx_ws2812_lane_e) <= sizeof(FAR void *), + "Lane enum does not fit into pointer size."); + +#ifndef __ASSEMBLY__ + +/**************************************************************************** + * Public Data + ****************************************************************************/ + +#ifdef __cplusplus +extern "C" +{ +#endif + +/**************************************************************************** + * Inline Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: <Inline function name> + * + * Description: + * Description of the operation of the inline function. + * + * Input Parameters: + * A list of input parameters, one-per-line, appears here along with a + * description of each input parameter. + * + * Returned Value: + * Description of the value returned by this function (if any), + * including an enumeration of all possible error values. + * + * Assumptions/Limitations: + * Anything else that one might need to know to use this function. + * + ****************************************************************************/ + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +/**************************************************************************** + * Name: <Global function name> + * + * Description: + * Description of the operation of the function. + * + * Input Parameters: + * A list of input parameters, one-per-line, appears here along with a + * description of each input parameter. + * + * Returned Value: + * Description of the value returned by this function (if any), + * including an enumeration of all possible error values. + * + * Assumptions/Limitations: + * Anything else that one might need to know to use this function. + * + ****************************************************************************/ + +/**************************************************************************** + * Name: avrdx_ws2812_register + * + * Initializer for one WS2812 lane. Lane is a set of WS2812 LEDs + * connected serially and driven from single hardware device + * on the MCU side. + * + * Note that this function triggers transmission of its freshly + * allocated and zeroed buffer, thus resetting the LEDs + * and turning them all of. Since the function does not know + * if it runs after a power-on or after a reset (and, therefore, + * how much time elapsed from last transmission), it will simply + * wait. + * + * Input Parameters: + * path - device path for this LED lane + * count - LED count. Limited to 255 regardless of upper half setting + * (The device does not have memory for more.) + * lane - enum avrdx_ws2812_lane_e value. Selects peripheral to which + * are the LEDs connected on the board + * has_white - bool. True if the lane has LEDs with white component. + * Ignored if CONFIG_WS2812_HAS_WHITE is unset. + * + * Returned Value: + * OK or negated errno. + * ENOMEM - if the LED count exceeds limit (in addition to memory + * allocation failure.) + * + ****************************************************************************/ + +int avrdx_ws2812_register(const char *path, uint8_t count, + enum avrdx_ws2812_lane_e lane, + bool has_white); + +#ifdef __cplusplus +} +#endif + +#endif /* __ASSEMBLY__ */ + +#endif diff --git a/arch/avr/src/avrdx/avrdx_ws2812_uart_store.S b/arch/avr/src/avrdx/avrdx_ws2812_uart_store.S new file mode 100644 index 00000000000..4fbe7bd816e --- /dev/null +++ b/arch/avr/src/avrdx/avrdx_ws2812_uart_store.S @@ -0,0 +1,280 @@ +/**************************************************************************** + * arch/avr/src/avrdx/avrdx_ws2812_uart_store.S + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include <nuttx/config.h> + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#if (defined CONFIG_WS2812_HAS_WHITE) && \ + (defined CONFIG_AVR_AVRDX_WS2812_NO_WHITE_LED) +# undef CONFIG_WS2812_HAS_WHITE +#endif + +/**************************************************************************** + * External Symbols + ****************************************************************************/ + + .file "avrdx_ws2812_uart_store.S" + +/**************************************************************************** + * Name: avrdx_ws2812_store_uart_bytes_rgb + * + * Description: + * Stores given RGB data (with offset and length limited to len + * to UART transmit buffer, encoded for direct transmission to WS2812 + * LED.) + * + * Input Parameters: + * lane - enum avrdx_ws2812_lane_e, interpreted as uint8_t + * (register r24) + * data - pointer to uint32_t array with RGB data (register r22:r23) + * offset - this is the first pixel to be written. LED count + * is limited to 255, data type is uint8_t (register r20) + * length - length of data array, same as above for type (register r18) + * + * Return value: void + * + * Assumptions/Limitations: + * - parameters are checked by the call chain (VFS) and len is not zero + * + ****************************************************************************/ + + .global avrdx_ws2812_store_uart_bytes_rgb + +avrdx_ws2812_store_uart_bytes_rgb: + + ; Register usage + ; r1 - zero register (part of ABI, no need to clear it first) + ; + ; In prologue + ; (Mostly just storing data, not doing anything with it. Treat + ; this list as free-for-use/do-not-touch.) + ; + ; Do not touch + ; r2 - copy of length + ; r22:r23 - pointer to destination data + ; r24:r25 - pointer to destination data + ; r28:r29 - pointer to source data + ; + ; Free for use (when, if not always) + ; r20 - function argument (after prologue ends) + ; r24 - function argument (after prologue ends) + ; Z - temporary pointer + ; + ; In main loop + ; r2 - copy of length, decremented after each pass, return + ; when reduced to zero + ; r18 - current pixel component + ; r19 - index register for UART byte + ; r20 - temporary byte - do not use for anything that takes + ; more than few lines of assembly + ; r21 - working copy of 9th bits (value assembled here, then + ; stored when all 8 bits are ready.) + ; r22:r23 - pointer to g_avrdx_ws2812_uart9_bytes, ie. to current + ; byte in array for 9th bits. + ; r24:r25 - pointer to current byte in UART transmit buffer. + ; The pointer is usually stored in X but if X needs + ; to address something else, copy is stored here. + ; r26:r27 - pointer to one of the transmit buffers. Code uses + ; XL:XH notation. + ; r28:r29 - pointer to source data. Code uses YL:YH notation + ; r30:r31 - pointer to UART bytes. Code uses ZL:ZH notation + ; + + ; prologue + push r2 + push YL + push YH + + ; move len to r2 to keep registers with more functionality free + mov r2, r18 + + ; Y stores pointer to source data and is not used for anything + ; else, free r22:r23 for other use (see above) + movw YL, r22 + + ; load address of current byte in 9th bits transmit buffer + ; we start with this one because this it doesn't need offset + ; multiplied. + ; + ; This is an array of pointers + ldi ZL, lo8(g_avrdx_ws2812_uart9_bytes) + ldi ZH, hi8(g_avrdx_ws2812_uart9_bytes) + + ; add lane number to select the correct pointer; + ; (pointer is 16 bit, add twice) + add ZL, r24 ; r24 lane function argument, keep, still needed + adc ZH, r1 + add ZL, r24 + adc ZH, r1 + + ; Z now points to the address of UART buffer for 9th bits. + ; Load the address to r22:r23. + ld r22, Z + ldd r23, Z+1 + + ; apply offset - 1 byte per offset in this array + add r22, r20 ; r20 offset function argument, keep, still needed + adc r23, r1 ; max 255 LEDs, high byte is zero + + ; now load address of current byte of regular transmit buffer + ldi ZL, lo8(g_avrdx_ws2812_xmit_bytes) + ldi ZH, hi8(g_avrdx_ws2812_xmit_bytes) + + ; similar as above, add lane number, load the address + add ZL, r24 + adc ZH, r1 + add ZL, r24 ; lane number no longer needed + adc ZH, r1 + ld r24, Z + ldd r25, Z+1 + + ; apply offset - this array has 8 bytes per offset + ; we can clobber the offset, do not need it after this + ; + ; note - compiler tends to do additions (bitshifts) + ; when multiplying by constant but multiplying is faster + ; here (6 instructions instead of 7) + ldi r21, 8 + mul r21, r20 + add r24, r0 + adc r25, r1 + eor r1, r1 ; ABI - r1 is zero + + ; Z points to UART bytes (ie. precalculated values that encode + ; pixel data into bytes that are transmitted by UART.) + ; + ; In case you are wondering: yes, values in this array indeed + ; have incorrect bit 7. We will deal with that on load. + ldi ZL, lo8(avrdx_ws2812_uart_white) + ldi ZH, hi8(avrdx_ws2812_uart_white) + + ; store g_avrdx_ws2812_xmit_bytes pointer here, that's what + ; is needed most of the time + movw XL, r24 + ; initialization phase done + +1: + ; green component is first in the UART data (ie. LED wants it + ; first, MSB first) but not first in xxRRGGBB pixel + ldd r18, Y+1 + ; load UART byte for G7 G6 LED bits + rcall 8f + ; G5 goes to 9th bits. note that r21 is not cleared. + ; 8 bits will be shifted to it so no reason to do that. + lsl r18 + ror r21 ; counting bits shifted in: 1 + ; same for G4 G3 G2 + rcall 8f + lsl r18 + ror r21 ; 2 bits in + ; G1 G0 can be dealt with using the function too + rcall 8f + ; need the next byte now though; the LED wants red + ldd r18, Y+2 + lsl r18 + ror r21 ; 3 bits in + ; that was R7, R6-R4 now + rcall 8f + lsl r18 + ror r21 ; 4 bits in + ; and R3-R1 + rcall 8f + lsl r18 + ror r21 ; 5 bits in + ; need R0 and B7 for the function call. Manually doing + ; what the function does otherwise. We skip that part + ; of the function but can use the rest + lsl r18 + rol r19 + ld r18, Y + lsl r18 + rol r19 + ; note - skipping the part that sets r19 + rcall 7f + ; B6 goes to the 9th bits byte + lsl r18 + ror r21 ; 6 bits in + ; B5-B3 the usual way + rcall 8f + lsl r18 + ror r21 ; 7 bits in + ; same for B2-B0 + rcall 8f + lsl r18 + ror r21 ; 8 bits in + ; we are done with the 9th bits but they are inverted + ; (0 in data is now 0 in r21 but requires short pulse, + ; ie. log.1 parity bit.) fix by inverting + com r21 + + ; store the pointer for g_avrdx_ws2812_xmit_bytes, need X + ; register usable + movw r24, XL + movw XL, r22 + ; store 9th bits byte to the new X + st X+, r21 + ; set X back to the main buffer + movw r22, XL + movw XL, r24 + + dec r2 ; decrement len + breq 9f ; next loop unless len == 0 + + adiw YL, 4 ; next item in source data + rjmp 1b + + ; epilogue +9: + pop YH + pop YL + pop r2 + ret + +8: + ; bitshift top bits from the current component + lsl r18 + rol r19 + lsl r18 + rol r19 + ; trim bits from previous call +7: andi r19, 0x03 + ; r19 is now offset in avrdx_ws2812_uart_white + add ZL, r19 + adc ZH, r1 + ; load UART byte + ld r20, Z + ; correct its 8th bit (always 0) + andi r20, 0x7f + ; store it, post-increment + st X+, r20 + ; reset Z to original position + sub ZL, r19 + sbc ZH, r1 + + ret diff --git a/arch/avr/src/avrdx/avrdx_ws2812_uart_xmit.S b/arch/avr/src/avrdx/avrdx_ws2812_uart_xmit.S new file mode 100644 index 00000000000..27c116701d7 --- /dev/null +++ b/arch/avr/src/avrdx/avrdx_ws2812_uart_xmit.S @@ -0,0 +1,479 @@ +/**************************************************************************** + * arch/avr/src/avrdx/avrdx_ws2812_uart_xmit.S + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include <nuttx/config.h> +#include <avrdx_swint.h> + +#include <avr/io.h> + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#if (defined CONFIG_WS2812_HAS_WHITE) && \ + (defined CONFIG_AVR_AVRDX_WS2812_NO_WHITE_LED) +# undef CONFIG_WS2812_HAS_WHITE +#endif + +/**************************************************************************** + * External Symbols + ****************************************************************************/ + + .global avrdx_swint_raise + .file "avrdx_ws2812_uart_xmit.S" + +/**************************************************************************** + * Name: __vector_avrdx_usart2_dre + * + * Description: + * Interrupt handler for LED data transmission. + * + * The compiler does pretty decent job if this function is compiled + * from C - execution of the handler takes 66 clock cycles (compiled + * with avr-gcc 14.2, calculated by hand.) That's with an implementation + * that takes next byte and transmits it. + * + * Such an implementation has a room for improvement though: every + * transmitted byte has 12 bits (start + 9 bits + parity + stop), that is + * 96 CPU clock cycles. If any other interrupt request comes in, it is + * almost sure to take more than 30 CPU cycles (which is 96 - 66 ie. time + * needed to transmit minus time needed to prepare the transmission.) + * That is going to leave the UART hardware idle almost for sure. + * + * Now, as far as author of this code understands it, there are two + * levels of buffering within the UART hardware. When a byte is written + * to the TX data register, it is moved to the TX Buffer if it is free. + * Then it is moved from TX Buffer to the shift register if it is free. + * This means that if the hardware is idle (for example because another + * interrupt handler was running for more than 3*96 = 288 CPU cycles), + * it can receive 3 new bytes. + * + * This is where the improvement comes from. If the interrupt handler + * waits for few CPU clock cycles after pushing a byte - and that happens + * naturally because of bookkeeping (incrementing next byte index + * for example) - it can check if the TX data register became available + * for next write (and write another byte if possible.) + * + * Making such decision is not free but on the other hand, it saves + * some 33 clock cycles worth of push/pop, initialization, jmp/reti + * and decision about white LEDs. + * + * Unfortunately, this is where the compiler stops doing good job. + * It suddenly decides to use more registers (more push/pop), variable + * that takes care of limiting loop count before the interrupt handler + * cedes the CPU to other code is forced to be 16 bits etc. Overall, + * the optimized implementation becomes slower than the simple one. + * + * That's why an assembly implementation is used. (It is mostly based + * on the compiled C code though, with some tweaks and with simplified + * implementation of the counter.) + * + * Loop count is chosen to be 4. That is 3 bytes of levels of buffering + * in the hardware plus accounting for the fact that when the 3rd byte + * if prepared, the 1st one is probably already out. (We give other + * handlers a chance to run after that.) + * + * This handler varies depending on CONFIG_AVR_AVRDX_WS2812_SHORT_LANE. + * If this option is unset, next byte index is 16 bit. + * + * Numbers (calculated by hand, for 8 bit next byte index): + * The handler takes 70 clock ticks to run (out of 96 clock ticks + * needed to transmit the byte.) Sending single byte therefore causes + * 73% CPU load. + * + * If looping, every loop only needs 37 clock ticks. Therefore, + * if the handler succeeds in pushing more than one byte, the load + * is reduced (pushed bytes - CPU load): + * 2 bytes - CPU 56% load + * 3 bytes - CPU 50% load + * 4 bytes - CPU 47% load + * + * Numbers (16 bit next byte index): + * The handler takes 77 clock ticks, a 7 tick increase, one tick of that + * in prologue, 6 ticks in loop. CPU load of single pass increases + * to 80%. Loop needs 43 clock ticks, load reduction is as follows: + * 2 bytes - CPU 63% load + * 3 bytes - CPU 57% load + * 4 bytes - CPU 54% load + * + ****************************************************************************/ + + .macro WS2812_HANDLER_UART, uartnum + .global __vector_avrdx_usart\uartnum\()_dre +__vector_avrdx_usart\uartnum\()_dre: + + ; Register usage + ; r1 - zero register (part of ABI but need to clear it first) + ; note - if CONFIG_AVR_AVRDX_WS2812_SHORT_LANE is not set, + ; then r1 is not used and NOT SET TO ZERO + ; + ; r19 - loop counter + ; r22 - temporary value + ; r23 - value of current uart9 byte until processing of 9th bits + ; is done (that is, until label 2 is reached) + ; - temporary value after that + ; r24 - value of next byte (the one processed by this run) + ; (see r25 below) + ; r30:r31 - various pointers + ; SREG(T) - flag that is set if support for LEDs with white component + ; is enabled and in use for this lane (unused if + ; CONFIG_WS2812_HAS_WHITE is not set) + ; + ; If CONFIG_AVR_AVRDX_WS2812_SHORT_LANE is not set: + ; r1 - not used and NOT SET TO ZERO + ; r25 - value of next byte (the one processed by this run), + ; high byte + + ; Prologue. + ; + ; Note that interrupt handler generated by C compiler saves RAMPZ. + ; Since there currently is no available AVR Dx device with more + ; than 64kB data space, we do not do that. +#if defined(CONFIG_AVR_AVRDX_WS2812_SHORT_LANE) + ; Zero register is not used if byte counter has 16 bits + push r1 + in r1, SREG + push r1 + eor r1, r1 +#else + ; If the zero register is not used, SREG is not saved. Use r19 + ; (Doing it like this to preserve push order.) + push r19 + in r19, SREG +#endif + push r19 + push r22 + push r23 + push r24 +#ifndef CONFIG_AVR_AVRDX_WS2812_SHORT_LANE + push r25 +#endif + push r30 + push r31 + ; total loop count + 1 + ldi r19, 5 + ; next byte to transmit - will be needed later +#if defined(CONFIG_AVR_AVRDX_WS2812_SHORT_LANE) + lds r24, g_avrdx_ws2812_xmit_next+\uartnum +#else + lds r24, g_avrdx_ws2812_xmit_next+2*\uartnum + lds r25, g_avrdx_ws2812_xmit_next+2*\uartnum\()+1 +#endif + + ; 13 ticks total (+1 with not short lane) + + ; load current 9th bit byte + lds r23, g_avrdx_ws2812_uart9_current+\uartnum + +#if defined(CONFIG_WS2812_HAS_WHITE) + ; If support for LEDs with white component is enabled and the lane + ; has such LEDs, the current uart9 byte (a group of 8 9th bits) + ; is stored with highest bit set. That is normally not possible + ; (the byte is rotated to the right after load) and is used as + ; a signal. + ; + ; If support for LEDs with white component is not enabled, this + ; check is not needed. + ; + ; C code: + ; if ((g_avrdx_ws2812_uart9_current[2] & 0x80) == 0) { + ; + ; (Note for the C code - index 2 is hardcoded for USART2 ISR.) + + ; Check for the signal that there is white component (therefore + ; 9th bits are not in use, therefore can be skipped); + ; store the information in T bit in SREG, will be used if looping + bst r23, 7 + brts 2f + ; 4 ticks unless branch is taken (2 ticks if not checked) +#endif + +3: + ; Is current byte divisible by 8? we need to load next 9th bits + ; byte in such case + ; + ; C code: + ; if ((g_avrdx_ws2812_xmit_next[2] & 0x07) == 0) { + ;g_avrdx_ws2812_uart9_current[2] = \ + ; g_avrdx_ws2812_uart9_bytes\ + ; [2] \ + ; [ \ + ; g_avrdx_ws2812_xmit_next[2] >> 3 \ + ; ]; + ; } + ; + ; Note - r24 has correct value in loops. Never overwritten and + ; incremented below. + mov r22, r24 + andi r22, 0x07 + ; Note 1 - with short lane set, compiler compares r22 with r1 + ; here - pointless (andi already set the zero flag.) + ; Note 2 - with short lane not set, compiler does eor + ; on a register and does logical or with r22 - also pointless + ; (same as above, andi already set the zero flag.) + breq 7f + ; 3 ticks if branch is not taken (7/8 cases), 4 ticks if it is + +1: + ; load 9th bit into TX register + ; + ; C code: + ; USART2.TXDATAH = g_avrdx_ws2812_uart9_current[2] & 0x01; + ; g_avrdx_ws2812_uart9_current[2] >>= 1; + mov r22, r23 + andi r22, 0x01 + ; avr/io.h provides USARTx_TXDATAH preprocessor macro + ; with address we need but we can't use it with the \uartnum + ; parameter because that is interpreted after the preprocessor + ; runs. we will work around this by applying offset to address + ; of first peripheral + sts USART0_TXDATAH+0x20*\uartnum\(), r22 + ; prepare next bit and store it back to RAM + lsr r23 + sts g_avrdx_ws2812_uart9_current+\uartnum\(), r23 + ; 7 ticks +2: + ; load current byte to TX register, trigger the transmission + ; + ; C code: + ; USART2.TXDATAL = g_avrdx_ws2812_xmit_bytes \ + ; [2] \ + ; [g_avrdx_ws2812_xmit_next[2]]; + lds r30, g_avrdx_ws2812_xmit_bytes+2*\uartnum + lds r31, g_avrdx_ws2812_xmit_bytes+2*\uartnum\()+1 + add r30, r24 +#if defined(CONFIG_AVR_AVRDX_WS2812_SHORT_LANE) + adc r31, r1 +#else + adc r31, r25 +#endif + ld r23, Z + sts USART0_TXDATAL+0x20*\uartnum\(), r23 + ; g_avrdx_ws2812_xmit_next[2]++; +#if defined(CONFIG_AVR_AVRDX_WS2812_SHORT_LANE) + subi r24, 0xff + sts g_avrdx_ws2812_xmit_next+\uartnum, r24 +#else + adiw r24, 1 + sts g_avrdx_ws2812_xmit_next+2*\uartnum, r24 + sts g_avrdx_ws2812_xmit_next+2*\uartnum\()+1, r25 +#endif + + ; Are we done? + ; + ; C code: + ; if (g_avrdx_ws2812_xmit_next[2] == g_avrdx_ws2812_xmit_count[2]) { + ; USART2.CTRLA &= ~USART_DREIE_bm; + ; } +#if defined(CONFIG_AVR_AVRDX_WS2812_SHORT_LANE) + lds r22, g_avrdx_ws2812_xmit_count+\uartnum +#else + lds r22, g_avrdx_ws2812_xmit_count+2*\uartnum + lds r23, g_avrdx_ws2812_xmit_count+2*\uartnum\()+1 +#endif + cp r24, r22 +#ifndef CONFIG_AVR_AVRDX_WS2812_SHORT_LANE + cpc r25, r23 +#endif + breq 8f + ; 16/17 ticks (branch not/taken; mostly not) + ; (+6 with not short lane) + + ; We are not done. By this time, value loaded to TXDATA should be + ; shifted to transmit buffer, if there was space available. Maybe + ; push next byte? + dec r19 + ; Do not push next byte if we looped multiple times + breq 9f + ; 2/3 ticks (expected to take the branch once out of three cases) + + ; Check for DREIF flag in status register. If set, data register + ; is empty + lds r23, USART0_STATUS+0x20*\uartnum\() + andi r23, USART_DREIF_bm + ; not set, no more loops + breq 9f + ; 4/5 ticks (not expected to take the branch) +#if defined(CONFIG_WS2812_HAS_WHITE) + ; Same as above. If T in SREG is set, LEDs with white component + ; are supported and in use here. (Skipping 9th bits load.) + ; Note - r24 has correct value of next byte, incremented above + brts 2b +#endif + ; reload uart9 back to r23 + lds r23, g_avrdx_ws2812_uart9_current+\uartnum + rjmp 3b + +9: + ; Epilogue + pop r31 + pop r30 +#ifndef CONFIG_AVR_AVRDX_WS2812_SHORT_LANE + pop r25 +#endif + pop r24 + pop r23 + pop r22 + pop r19 +#ifndef CONFIG_AVR_AVRDX_WS2812_SHORT_LANE + out SREG, r19 + pop r19 +#else + pop r1 + out SREG, r1 + pop r1 +#endif + reti + ; 9 ticks + +7: + ; Load new uart9 byte from the array + ; + ; note - this code clobbers r23 but at this point, that value + ; is used and new needs to be loaded (which is what this does.) + ; g_avrdx_ws2812_uart9_bytes is array of pointers to arrays + ; of 9th bits bytes. locate pointer for this handler + ; (Note - \uartnum is replaced by parameter, \() is ending + ; delimiter for the parameter - needed if something follows.) + lds r30, g_avrdx_ws2812_uart9_bytes+2*\uartnum + lds r31, g_avrdx_ws2812_uart9_bytes+2*\uartnum\()+1 + ; offset in this handler's array +#if defined(CONFIG_AVR_AVRDX_WS2812_SHORT_LANE) + mov r22, r24 + lsr r22 + lsr r22 + lsr r22 +#else + movw r22, r24 + lsr r23 + ror r22 + lsr r23 + ror r22 + lsr r23 + ror r22 +#endif + add r30, r22 +#if defined(CONFIG_AVR_AVRDX_WS2812_SHORT_LANE) + adc r31, r1 +#else + adc r31, r23 +#endif + ; previous byte is replaced in r23 + ld r23, Z + rjmp 1b + ; 13 ticks (+4 with not short lane) + +8: + ; Transmission finished + ; + ; clear DREIE - Data Register Empty Interrupt Enabled + lds r24, USART0_CTRLA+0x20*\uartnum\() + andi r24, ~USART_DREIE_bm + sts USART0_CTRLA+0x20*\uartnum\(), r24 + + ; We are calling C function now. We cannot assume anything about + ; its implementation and need to adhere to the ABI. Registers + ; R18–R27, R30, R31, R0 are call-clobbered and so is T flag + ; in SREG. + ; + ; All of these must be preserved from the interrupted code's + ; point of view but as long as we do nothing else after we + ; call the function, we can lose our own data. + + ; push what was not pushed in the prologue + push r0 +#ifndef CONFIG_AVR_AVRDX_WS2812_SHORT_LANE + ; Zero register was not pushed nor zeroed + push r1 + eor r1, r1 +#endif + push r18 + push r20 + push r21 +#if defined(CONFIG_AVR_AVRDX_WS2812_SHORT_LANE) + push r25 +#endif + push r26 + push r27 + + ; load software interrupt vector number +#if (AVRDX_SWINT_VECTORS_SIZE == 1) + lds r24, g_avrdx_ws2812_swint_vector+\uartnum +#elif (AVRDX_SWINT_VECTORS_SIZE == 2) + lds r24, g_avrdx_ws2812_swint_vector+2*\uartnum + lds r25, g_avrdx_ws2812_swint_vector+2*\uartnum\()+1 +#elif (AVRDX_SWINT_VECTORS_SIZE == 3) + lds r22, g_avrdx_ws2812_swint_vector+3*\uartnum + lds r23, g_avrdx_ws2812_swint_vector+3*\uartnum\()+1 + lds r24, g_avrdx_ws2812_swint_vector+3*\uartnum\()+2 +#elif (AVRDX_SWINT_VECTORS_SIZE == 4) + lds r22, g_avrdx_ws2812_swint_vector+4*\uartnum + lds r23, g_avrdx_ws2812_swint_vector+4*\uartnum\()+1 + lds r24, g_avrdx_ws2812_swint_vector+4*\uartnum\()+2 + lds r25, g_avrdx_ws2812_swint_vector+4*\uartnum\()+3 +#else +# error Unhandled AVRDX_SWINT_VECTORS_SIZE +#endif + + ; raise the interrupt + call avrdx_swint_raise + + ; now pop what we pushed here + pop r27 + pop r26 +#if defined(CONFIG_AVR_AVRDX_WS2812_SHORT_LANE) + pop r25 +#endif + pop r21 + pop r20 + pop r18 +#ifndef CONFIG_AVR_AVRDX_WS2812_SHORT_LANE + pop r1 +#endif + pop r0 + rjmp 9b + + .endm + +#if defined(CONFIG_AVR_AVRDX_WS2812_USART0) + WS2812_HANDLER_UART 0 +#endif +#if defined(CONFIG_AVR_AVRDX_WS2812_USART1) + WS2812_HANDLER_UART 1 +#endif +#if defined(CONFIG_AVR_AVRDX_WS2812_USART2) + WS2812_HANDLER_UART 2 +#endif +#if defined(CONFIG_AVR_AVRDX_WS2812_USART3) + WS2812_HANDLER_UART 3 +#endif +#if defined(CONFIG_AVR_AVRDX_WS2812_USART4) + WS2812_HANDLER_UART 4 +#endif +#if defined(CONFIG_AVR_AVRDX_WS2812_USART5) + WS2812_HANDLER_UART 5 +#endif +
