summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhathach <thach@tinyusb.org>2018-09-04 17:02:45 +0700
committerhathach <thach@tinyusb.org>2018-09-04 18:33:08 +0700
commit3ae24d9f9a36cfba6aeb616abe5aa576ce27d69a (patch)
tree129bbf329a3b517f08bd3c4bd573b2f84f3a9520
parente17253071e0597dbfda950dbf6bb5c8c7a0e89a2 (diff)
add neopixel_write implementation, work ok
-rw-r--r--ports/nrf/common-hal/neopixel_write/__init__.c234
1 files changed, 233 insertions, 1 deletions
diff --git a/ports/nrf/common-hal/neopixel_write/__init__.c b/ports/nrf/common-hal/neopixel_write/__init__.c
index c5f671360..ff2a2fa77 100644
--- a/ports/nrf/common-hal/neopixel_write/__init__.c
+++ b/ports/nrf/common-hal/neopixel_write/__init__.c
@@ -27,6 +27,238 @@
#include "py/mphal.h"
#include "shared-bindings/neopixel_write/__init__.h"
-void common_hal_neopixel_write(const digitalio_digitalinout_obj_t* digitalinout, uint8_t *pixels, uint32_t numBytes) {
+// https://github.com/adafruit/Adafruit_NeoPixel/blob/master/Adafruit_NeoPixel.cpp
+// [[[Begin of the Neopixel NRF52 EasyDMA implementation
+// by the Hackerspace San Salvador]]]
+// This technique uses the PWM peripheral on the NRF52. The PWM uses the
+// EasyDMA feature included on the chip. This technique loads the duty
+// cycle configuration for each cycle when the PWM is enabled. For this
+// to work we need to store a 16 bit configuration for each bit of the
+// RGB(W) values in the pixel buffer.
+// Comparator values for the PWM were hand picked and are guaranteed to
+// be 100% organic to preserve freshness and high accuracy. Current
+// parameters are:
+// * PWM Clock: 16Mhz
+// * Minimum step time: 62.5ns
+// * Time for zero in high (T0H): 0.31ms
+// * Time for one in high (T1H): 0.75ms
+// * Cycle time: 1.25us
+// * Frequency: 800Khz
+// For 400Khz we just double the calculated times.
+// ---------- BEGIN Constants for the EasyDMA implementation -----------
+// The PWM starts the duty cycle in LOW. To start with HIGH we
+// need to set the 15th bit on each register.
+// WS2812 (rev A) timing is 0.35 and 0.7us
+//#define MAGIC_T0H 5UL | (0x8000) // 0.3125us
+//#define MAGIC_T1H 12UL | (0x8000) // 0.75us
+
+// WS2812B (rev B) timing is 0.4 and 0.8 us
+#define MAGIC_T0H 6UL | (0x8000) // 0.375us
+#define MAGIC_T1H 13UL | (0x8000) // 0.8125us
+#define CTOPVAL 20UL // 1.25us
+
+// ---------- END Constants for the EasyDMA implementation -------------
+//
+// If there is no device available an alternative cycle-counter
+// implementation is tried.
+// The nRF52832 runs with a fixed clock of 64Mhz. The alternative
+// implementation is the same as the one used for the Teensy 3.0/1/2 but
+// with the Nordic SDK HAL & registers syntax.
+// The number of cycles was hand picked and is guaranteed to be 100%
+// organic to preserve freshness and high accuracy.
+// ---------- BEGIN Constants for cycle counter implementation ---------
+#define CYCLES_800_T0H 18 // ~0.36 uS
+#define CYCLES_800_T1H 41 // ~0.76 uS
+#define CYCLES_800 71 // ~1.25 uS
+
+// ---------- END of Constants for cycle counter implementation --------
+
+// find a free PWM device, which is not enabled and has no connected pins
+static NRF_PWM_Type* find_free_pwm (void) {
+ NRF_PWM_Type* PWM[3] = { NRF_PWM0, NRF_PWM1, NRF_PWM2 };
+
+ for ( int device = 0; device < 3; device++ ) {
+ if ( (PWM[device]->ENABLE == 0) && (PWM[device]->PSEL.OUT[0] & PWM_PSEL_OUT_CONNECT_Msk)
+ && (PWM[device]->PSEL.OUT[1] & PWM_PSEL_OUT_CONNECT_Msk)
+ && (PWM[device]->PSEL.OUT[2] & PWM_PSEL_OUT_CONNECT_Msk)
+ && (PWM[device]->PSEL.OUT[3] & PWM_PSEL_OUT_CONNECT_Msk) ) {
+ return PWM[device];
+ }
+ }
+
+ return NULL;
+}
+
+void common_hal_neopixel_write (const digitalio_digitalinout_obj_t* digitalinout, uint8_t *pixels, uint32_t numBytes) {
+ // To support both the SoftDevice + Neopixels we use the EasyDMA
+ // feature from the NRF25. However this technique implies to
+ // generate a pattern and store it on the memory. The actual
+ // memory used in bytes corresponds to the following formula:
+ // totalMem = numBytes*8*2+(2*2)
+ // The two additional bytes at the end are needed to reset the
+ // sequence.
+ //
+ // If there is not enough memory, we will fall back to cycle counter
+ // using DWT
+ uint32_t pattern_size = numBytes * 8 * sizeof(uint16_t) + 2 * sizeof(uint16_t);
+ uint16_t* pixels_pattern = NULL;
+
+ NRF_PWM_Type* pwm = find_free_pwm();
+
+ // only malloc if there is PWM device available
+ if ( pwm != NULL ) {
+ pixels_pattern = (uint16_t *) m_malloc(pattern_size, false);
+ }
+
+ // Use the identified device to choose the implementation
+ // If a PWM device is available use DMA
+ if ( (pixels_pattern != NULL) && (pwm != NULL) ) {
+ uint16_t pos = 0; // bit position
+
+ for ( uint16_t n = 0; n < numBytes; n++ ) {
+ uint8_t pix = pixels[n];
+
+ for ( uint8_t mask = 0x80, i = 0; mask > 0; mask >>= 1, i++ ) {
+ pixels_pattern[pos] = (pix & mask) ? MAGIC_T1H : MAGIC_T0H;
+ pos++;
+ }
+ }
+
+ // Zero padding to indicate the end of sequence
+ pixels_pattern[++pos] = 0 | (0x8000); // Seq end
+ pixels_pattern[++pos] = 0 | (0x8000); // Seq end
+
+ // Set the wave mode to count UP
+ pwm->MODE = (PWM_MODE_UPDOWN_Up << PWM_MODE_UPDOWN_Pos);
+
+ // Set the PWM to use the 16MHz clock
+ pwm->PRESCALER = (PWM_PRESCALER_PRESCALER_DIV_1 << PWM_PRESCALER_PRESCALER_Pos);
+
+ // Setting of the maximum count
+ // but keeping it on 16Mhz allows for more granularity just
+ // in case someone wants to do more fine-tuning of the timing.
+ pwm->COUNTERTOP = (CTOPVAL << PWM_COUNTERTOP_COUNTERTOP_Pos);
+
+ // Disable loops, we want the sequence to repeat only once
+ pwm->LOOP = (PWM_LOOP_CNT_Disabled << PWM_LOOP_CNT_Pos);
+
+ // On the "Common" setting the PWM uses the same pattern for the
+ // for supported sequences. The pattern is stored on half-word
+ // of 16bits
+ pwm->DECODER = (PWM_DECODER_LOAD_Common << PWM_DECODER_LOAD_Pos)
+ | (PWM_DECODER_MODE_RefreshCount << PWM_DECODER_MODE_Pos);
+
+ // Pointer to the memory storing the patter
+ pwm->SEQ[0].PTR = (uint32_t) (pixels_pattern) << PWM_SEQ_PTR_PTR_Pos;
+
+ // Calculation of the number of steps loaded from memory.
+ pwm->SEQ[0].CNT = (pattern_size / sizeof(uint16_t)) << PWM_SEQ_CNT_CNT_Pos;
+
+ // The following settings are ignored with the current config.
+ pwm->SEQ[0].REFRESH = 0;
+ pwm->SEQ[0].ENDDELAY = 0;
+
+ // The Neopixel implementation is a blocking algorithm. DMA
+ // allows for non-blocking operation. To "simulate" a blocking
+ // operation we enable the interruption for the end of sequence
+ // and block the execution thread until the event flag is set by
+ // the peripheral.
+ // pwm->INTEN |= (PWM_INTEN_SEQEND0_Enabled<<PWM_INTEN_SEQEND0_Pos);
+
+ // PSEL must be configured before enabling PWM
+ pwm->PSEL.OUT[0] = ( digitalinout->pin->port*32 + digitalinout->pin->pin );
+
+ // Enable the PWM
+ pwm->ENABLE = 1;
+
+ // After all of this and many hours of reading the documentation
+ // we are ready to start the sequence...
+ pwm->EVENTS_SEQEND[0] = 0;
+ pwm->TASKS_SEQSTART[0] = 1;
+
+ // But we have to wait for the flag to be set.
+ while ( !pwm->EVENTS_SEQEND[0] ) {
+//#ifdef MICROPY_VM_HOOK_LOOP
+// MICROPY_VM_HOOK_LOOP
+//#endif
+ }
+
+ // Before leave we clear the flag for the event.
+ pwm->EVENTS_SEQEND[0] = 0;
+
+ // We need to disable the device and disconnect
+ // all the outputs before leave or the device will not
+ // be selected on the next call.
+ // TODO: Check if disabling the device causes performance issues.
+ pwm->ENABLE = 0;
+ pwm->PSEL.OUT[0] = 0xFFFFFFFFUL;
+
+ m_free(pixels_pattern);
+ } // End of DMA implementation
+ // ---------------------------------------------------------------------
+ else {
+ // Fall back to DWT
+ // If you are using the Bluetooth SoftDevice we advise you to not disable
+ // the interrupts. Disabling the interrupts even for short periods of time
+ // causes the SoftDevice to stop working.
+ // Disable the interrupts only in cases where you need high performance for
+ // the LEDs and if you are not using the EasyDMA feature.
+ __disable_irq();
+
+ NRF_GPIO_Type* port = ( digitalinout->pin->port ? NRF_P1 : NRF_P0 );
+ uint32_t pinMask = ( 1UL << digitalinout->pin->pin );
+
+ uint32_t CYCLES_X00 = CYCLES_800;
+ uint32_t CYCLES_X00_T1H = CYCLES_800_T1H;
+ uint32_t CYCLES_X00_T0H = CYCLES_800_T0H;
+
+ // Enable DWT in debug core
+ CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
+ DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
+
+ // Tries to re-send the frame if is interrupted by the SoftDevice.
+ while ( 1 ) {
+ uint8_t *p = pixels;
+
+ uint32_t cycStart = DWT->CYCCNT;
+ uint32_t cyc = 0;
+
+ for ( uint16_t n = 0; n < numBytes; n++ ) {
+ uint8_t pix = *p++;
+
+ for ( uint8_t mask = 0x80; mask; mask >>= 1 ) {
+ while ( DWT->CYCCNT - cyc < CYCLES_X00 )
+ ;
+ cyc = DWT->CYCCNT;
+
+ port->OUTSET |= pinMask;
+
+ if ( pix & mask ) {
+ while ( DWT->CYCCNT - cyc < CYCLES_X00_T1H )
+ ;
+ } else {
+ while ( DWT->CYCCNT - cyc < CYCLES_X00_T0H )
+ ;
+ }
+
+ port->OUTCLR |= pinMask;
+ }
+ }
+ while ( DWT->CYCCNT - cyc < CYCLES_X00 )
+ ;
+
+ // If total time longer than 25%, resend the whole data.
+ // Since we are likely to be interrupted by SoftDevice
+ if ( (DWT->CYCCNT - cycStart) < (8 * numBytes * ((CYCLES_X00 * 5) / 4)) ) {
+ break;
+ }
+
+ // re-send need 300us delay
+ mp_hal_delay_us(300);
+ }
+
+ // Enable interrupts again
+ __enable_irq();
+ }
}