summaryrefslogtreecommitdiff
path: root/ports/nrf/common-hal/neopixel_write/__init__.c
blob: 8062937f4b70b86f2756985f9b8db08220dff38e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
/*
 * 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 "py/mpstate.h"
#include "shared-bindings/neopixel_write/__init__.h"
#include "supervisor/port.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 nRF52840 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 ( size_t 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;
}

static size_t pixels_pattern_heap_size = 0;
// Called during reset_port() to free the pattern buffer
void neopixel_write_reset(void) {
    MP_STATE_VM(pixels_pattern_heap) = NULL;
    pixels_pattern_heap_size = 0;
}

uint64_t next_start_raw_ticks = 0;

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 NRF52. 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

#define PATTERN_SIZE(numBytes) (numBytes * 8 * sizeof(uint16_t) + 2 * sizeof(uint16_t))
// Allocate PWM space for up to STACK_PIXELS on the stack, to avoid malloc'ing.
// We may need to write to the status neopixel or to Circuit Playground NeoPixels
// when we cannot malloc, between VM instantiations.
// We need space for at least 10 pixels for Circuit Playground, but let's choose 24
// to handle larger NeoPixel rings without malloc'ing.
#define STACK_PIXELS 24
    uint32_t pattern_size = PATTERN_SIZE(numBytes);
    uint16_t* pixels_pattern = NULL;

    // Use the stack to store STACK_PIXEL's worth of PWM data. uint32_t to ensure alignment.
    // It is 3*STACK_PIXELS to handle RGB.
    // PATTERN_SIZE is a multiple of 4, so we don't need round up to make sure one_pixel is large enough.
    uint32_t stack_pixels[PATTERN_SIZE(3 * STACK_PIXELS) / sizeof(uint32_t)];

    NRF_PWM_Type* pwm = find_free_pwm();

    // only malloc if there is PWM device available
    if ( pwm != NULL ) {
        if (pattern_size <= sizeof(stack_pixels)) {
            pixels_pattern = (uint16_t *) stack_pixels;
        } else {
            uint8_t sd_en = 0;
            (void) sd_softdevice_is_enabled(&sd_en);

            if (pixels_pattern_heap_size < pattern_size) {
                // Current heap buffer is too small.
                if (MP_STATE_VM(pixels_pattern_heap)) {
                    // Old pixels_pattern_heap will be gc'd; don't free it.
                    pixels_pattern = NULL;
                    pixels_pattern_heap_size = 0;
                }

                // realloc routines fall back to a plain malloc if the incoming ptr is NULL.
                if (sd_en) {
                    // If the soft device is enabled then we must use PWM to
                    // transmit. This takes a bunch of memory to do so raise an
                    // exception if we can't.
                    MP_STATE_VM(pixels_pattern_heap) =
                        (uint16_t *) m_realloc(MP_STATE_VM(pixels_pattern_heap), pattern_size);
                } else {
                    // Might return NULL.
                    MP_STATE_VM(pixels_pattern_heap) =
                        // true means move if necessary.
                        (uint16_t *) m_realloc_maybe(MP_STATE_VM(pixels_pattern_heap), pattern_size, true);
                }
                if (MP_STATE_VM(pixels_pattern_heap)) {
                    pixels_pattern_heap_size = pattern_size;
                }
            }
            // Might be NULL, which means we failed to allocate.
            pixels_pattern = MP_STATE_VM(pixels_pattern_heap);
        }
    }

    // Wait to make sure we don't append onto the last transmission. This should only be a tick or
    // two.
    while (port_get_raw_ticks(NULL) < next_start_raw_ticks) {}

    // Use the identified device to choose the implementation
    // If a PWM device is available and we have a buffer, 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; mask > 0; mask >>= 1 ) {
                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<<PWM_INTEN_SEQEND0_Pos);

        // PSEL must be configured before enabling PWM
        nrf_pwm_pins_set(pwm, (uint32_t[]) {digitalinout->pin->number, 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) ) {
            RUN_BACKGROUND_TASKS;
        }

        // 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} );

    } // 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();

        uint32_t decoded_pin = digitalinout->pin->number;
        NRF_GPIO_Type* port = nrf_gpio_pin_port_decode(&decoded_pin);

        uint32_t pinMask = ( 1UL << decoded_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();
    }

    // Update the next start.
    next_start_raw_ticks = port_get_raw_ticks(NULL) + 4;
}