diff --git a/ports/nrf/Makefile b/ports/nrf/Makefile index f3cc0e35ba..82b1f51708 100755 --- a/ports/nrf/Makefile +++ b/ports/nrf/Makefile @@ -141,6 +141,7 @@ SRC_COMMON_HAL += \ microcontroller/__init__.c \ microcontroller/Pin.c \ microcontroller/Processor.c \ + neopixel_write/__init__.c \ os/__init__.c \ time/__init__.c \ analogio/__init__.c \ diff --git a/ports/nrf/common-hal/neopixel_write/__init__.c b/ports/nrf/common-hal/neopixel_write/__init__.c new file mode 100644 index 0000000000..c84bc4381a --- /dev/null +++ b/ports/nrf/common-hal/neopixel_write/__init__.c @@ -0,0 +1,267 @@ +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2018 hathach for Adafruit Industries + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "py/mphal.h" +#include "shared-bindings/neopixel_write/__init__.h" +#include "nrf_pwm.h" + +// 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[] = { + NRF_PWM0, NRF_PWM1, NRF_PWM2 +#ifdef NRF_PWM3 + , NRF_PWM3 +#endif + }; + + for ( int device = 0; device < ARRAY_SIZE(PWM); 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 + // Set the PWM to use the 16MHz clock + // 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. + nrf_pwm_configure(pwm, NRF_PWM_CLK_16MHz, NRF_PWM_MODE_UP, CTOPVAL); + + // Disable loops, we want the sequence to repeat only once + nrf_pwm_loop_set(pwm, 0); + + // On the "Common" setting the PWM uses the same pattern for the + // for supported sequences. The pattern is stored on half-word of 16bits + nrf_pwm_decoder_set(pwm, PWM_DECODER_LOAD_Common, PWM_DECODER_MODE_RefreshCount); + + // Pointer to the memory storing the pattern + nrf_pwm_seq_ptr_set(pwm, 0, pixels_pattern); + + // Calculation of the number of steps loaded from memory. + nrf_pwm_seq_cnt_set(pwm, 0, pattern_size / sizeof(uint16_t)); + + // The following settings are ignored with the current config. + nrf_pwm_seq_refresh_set(pwm, 0, 0); + nrf_pwm_seq_end_delay_set(pwm, 0, 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<pin->port*32 + digitalinout->pin->pin, 0xFFFFFFFFUL, 0xFFFFFFFFUL, 0xFFFFFFFFUL} ); + + // Enable the PWM + nrf_pwm_enable(pwm); + + // After all of this and many hours of reading the documentation + // we are ready to start the sequence... + nrf_pwm_event_clear(pwm, NRF_PWM_EVENT_SEQEND0); + nrf_pwm_task_trigger(pwm, NRF_PWM_TASK_SEQSTART0); + + // But we have to wait for the flag to be set. + while ( !nrf_pwm_event_check(pwm, NRF_PWM_EVENT_SEQEND0) ) { +#ifdef MICROPY_VM_HOOK_LOOP + MICROPY_VM_HOOK_LOOP +#endif + } + + // Before leave we clear the flag for the event. + nrf_pwm_event_clear(pwm, NRF_PWM_EVENT_SEQEND0); + + // 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. + nrf_pwm_disable(pwm); + nrf_pwm_pins_set(pwm, (uint32_t[]) {0xFFFFFFFFUL, 0xFFFFFFFFUL, 0xFFFFFFFFUL, 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(); + +#ifdef NRF_P1 + NRF_GPIO_Type* port = ( digitalinout->pin->port ? NRF_P1 : NRF_P0 ); +#else + NRF_GPIO_Type* port = NRF_P0; +#endif + 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(); + } +} diff --git a/ports/nrf/mpconfigport.h b/ports/nrf/mpconfigport.h index 281f31917c..b710808b91 100644 --- a/ports/nrf/mpconfigport.h +++ b/ports/nrf/mpconfigport.h @@ -180,6 +180,7 @@ extern const struct _mp_obj_module_t struct_module; extern const struct _mp_obj_module_t time_module; extern const struct _mp_obj_module_t supervisor_module; extern const struct _mp_obj_module_t gamepad_module; +extern const struct _mp_obj_module_t neopixel_write_module; extern const struct _mp_obj_module_t usb_hid_module; extern const struct _mp_obj_module_t bleio_module; @@ -210,6 +211,7 @@ extern const struct _mp_obj_module_t mp_module_ubluepy; { MP_OBJ_NEW_QSTR (MP_QSTR_digitalio ), (mp_obj_t)&digitalio_module }, \ { MP_OBJ_NEW_QSTR (MP_QSTR_pulseio ), (mp_obj_t)&pulseio_module }, \ { MP_OBJ_NEW_QSTR (MP_QSTR_microcontroller ), (mp_obj_t)µcontroller_module }, \ + { MP_OBJ_NEW_QSTR (MP_QSTR_neopixel_write ), (mp_obj_t)&neopixel_write_module }, \ { MP_OBJ_NEW_QSTR (MP_QSTR_bitbangio ), (mp_obj_t)&bitbangio_module }, \ { MP_OBJ_NEW_QSTR (MP_QSTR_os ), (mp_obj_t)&os_module }, \ { MP_OBJ_NEW_QSTR (MP_QSTR_random ), (mp_obj_t)&random_module }, \ diff --git a/ports/nrf/nrfx_config.h b/ports/nrf/nrfx_config.h index 88fb973e8a..fb603a896d 100644 --- a/ports/nrf/nrfx_config.h +++ b/ports/nrf/nrfx_config.h @@ -34,4 +34,15 @@ #define NRFX_UART_DEFAULT_CONFIG_PARITY NRF_UART_PARITY_EXCLUDED #define NRFX_UART_DEFAULT_CONFIG_BAUDRATE NRF_UART_BAUDRATE_115200 +// PWM +#define NRFX_PWM0_ENABLED 1 +#define NRFX_PWM1_ENABLED 1 +#define NRFX_PWM2_ENABLED 1 + +#ifdef NRF_PWM3 +#define NRFX_PWM3_ENABLED 1 +#else +#define NRFX_PWM3_ENABLED 0 +#endif + #endif diff --git a/ports/nrf/usb/usb.c b/ports/nrf/usb/usb.c index f8b5fd6632..248d31e213 100644 --- a/ports/nrf/usb/usb.c +++ b/ports/nrf/usb/usb.c @@ -130,8 +130,8 @@ uint32_t tusb_hal_millis(void) { void tud_cdc_line_state_cb(uint8_t itf, bool dtr, bool rts) { (void) itf; // interface ID, not used - // disconnected event - if ( !dtr && !rts ) + // DTR = false is counted as disconnected + if ( !dtr ) { cdc_line_coding_t coding; tud_cdc_get_line_coding(&coding);