Compare commits

...

36 commits

Author SHA1 Message Date
Dan Halbert
f83bac7e42
Merge pull request #86 from adafruit/idf5.5
Update for ESP-IDF 5.5 compatibility
2025-08-25 19:35:14 -04:00
Scott Shawcroft
9c45cfb0e4
Remove extra stuff from refactor 2025-08-25 12:07:12 -07:00
Dan Halbert
3614319ecd
Merge pull request #77 from chockenberry/master
Updated simple sketch to use correct values for pin mappings.
2025-08-20 13:54:44 -04:00
Scott Shawcroft
dbd84325c7
More formatting 2025-08-15 10:23:46 -07:00
Scott Shawcroft
c8ad602b45
clang format 2025-08-15 10:08:12 -07:00
Scott Shawcroft
1037d3f080
Tweak IOMUX setting 2025-08-15 09:59:10 -07:00
Scott Shawcroft
d55e3bf833
Mark version 1.7.0 2024-09-23 11:02:26 -07:00
Dan Halbert
0bd9873153
Merge pull request #82 from adafruit/STATIC-fix
STATIC -> static in stm32.h
2024-09-09 19:52:35 -04:00
Dan Halbert
2156ca0407 STATIC -> static 2024-09-09 19:44:17 -04:00
Scott Shawcroft
fb9903f97d
Merge pull request #80 from tannewt/rp2350
Enable RP2350 with same code as RP2040
2024-08-12 13:45:30 -07:00
Scott Shawcroft
282920f9ce
clang format 2024-08-12 13:26:21 -07:00
Scott Shawcroft
267d78d6cf
Enable RP2350 with same code as RP2040 2024-08-09 15:34:40 -07:00
Craig Hockenberry
386371d8fd Updated simple sketch to use correct values for pin mappings.
In reference to: https://forums.adafruit.com/viewtopic.php?p=1013458#p1013458
2024-04-28 13:15:14 -07:00
ladyada
ca4f8764be add c6 2024-04-24 11:31:51 -04:00
cffac25915
Merge pull request #75 from adafruit/updates-for-arduino-churn
Update for arduino esp32 3.0.0-rc1
2024-04-16 08:32:13 -05:00
7155be1a75 Update for arduino esp32 3.0.0-rc.1
.. tested on matrixportal s3 only. Other esp-family mcus may need
further changes.
2024-04-12 09:36:30 -05:00
e4506e5a57 bump actions versions to fix node deprecation warning 2024-04-12 09:32:29 -05:00
Scott Shawcroft
eadf2ee814
Merge pull request #73 from tannewt/cp_id5_5.1
Update CircuitPython implementation to ESP-IDF 5.1 APIs
2024-02-09 10:10:21 -08:00
Scott Shawcroft
d87532b044
Add comment and remove memory checks
I tried to check all relevant memory but still couldn't prevent it
from crashing. It wasn't clear how non-internal memory was being
referenced, so I couldn't fix it.

This is essentially what the old code was doing because of the
missed negation.
2024-02-08 16:00:59 -08:00
Scott Shawcroft
68943a8fcd
clang-format 2024-02-07 15:39:34 -08:00
Scott Shawcroft
5ad5f33a16
Switch to IDF 5.1 APIs
This also guards the ISR from running with inaccessible memory.
The timer will be called when the SPI flash/PSRAM cache is disabled.
When it is, we skip it because the data we need may not be available
and will crash if we access it.
2024-02-07 15:27:40 -08:00
ladyada
429d625ba7 try adding more nop's as per
https://forums.adafruit.com/viewtopic.php?t=204580&start=15
2023-10-06 20:41:39 -04:00
Paint Your Dragon
98a2da6da4
Merge pull request #68 from tannewt/fix_nons3_idf5_compile
Fix non-s3 idf5 compile
2023-10-05 09:51:15 -07:00
Scott Shawcroft
b0e9d045f8
clang-format 2023-10-03 15:38:18 -07:00
Scott Shawcroft
40d0971635
Fix non-S3 builds for IDF 5 and add C6 support 2023-10-03 10:55:27 -07:00
Scott Shawcroft
1faaec39b3
Merge pull request #67 from tannewt/fix_idf5_compile
Fix IDF 5 compilation error
2023-09-29 15:14:34 -07:00
Scott Shawcroft
67ac7d48d4
Fix IDF 5 compilation error 2023-09-29 14:26:55 -07:00
Phillip Burgess
a6dec45c38 SAMD51 CircuitPython: enable high drive strength (maybe, need to confirm pin indexing) 2023-09-10 16:31:06 -07:00
Phillip Burgess
4430d1ceb5 SAMD51: explicitly declare some vars volatile for cycle-counting reasons
This should have no effect on Arduino projects, where the library is already producing expected timing both before and after. This is really for CircuitPython, a last-ditch effort to avoid going to inline assembly or a complex DMA setup.
2023-09-08 10:07:39 -07:00
Paint Your Dragon
c9c1189e9d
Merge pull request #65 from adafruit/pb-s3-drive-strength
ESP32-S3: fix GPIO strength, dial down bit clock to ~17.8 MHz
2023-08-28 17:06:22 -07:00
Phillip Burgess
4a23a0bc7d ESP32-S3: fix GPIO strength, dial down bit clock to ~17.8 MHz 2023-08-28 16:47:12 -07:00
Paint Your Dragon
9a76ee2f81
Merge pull request #64 from adafruit/pb-m4-experiment
SAMD51: adjustable pixel clock duty cycle
2023-08-25 15:10:01 -07:00
Phillip Burgess
d15b59728d clang-format, bump version 2023-08-25 14:46:50 -07:00
Phillip Burgess
67cd55d033 clang-format 2023-08-25 14:41:39 -07:00
Phillip Burgess
df50a2761b SAMD51 loose ends; all widths and speeds implemented 2023-08-25 14:38:15 -07:00
Phillip Burgess
93ea4fa10d Ever-so-slightly adjustable duty on SAMD51 2023-08-24 17:07:04 -07:00
18 changed files with 546 additions and 287 deletions

View file

@ -12,11 +12,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v1
- uses: actions/setup-python@v5
with:
python-version: '3.8'
- uses: actions/checkout@v2
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: adafruit/ci-arduino
path: ci

View file

@ -38,6 +38,13 @@ supported boards. Notes have been moved to the bottom of the code.
uint8_t clockPin = 13;
uint8_t latchPin = 0;
uint8_t oePin = 1;
#elif defined(ARDUINO_ADAFRUIT_FEATHER_ESP32C6) // Feather ESP32-C6
// not featherwing compatible, but can 'hand wire' if desired
uint8_t rgbPins[] = {6, A3, A1, A0, A2, 0};
uint8_t addrPins[] = {8, 5, 15, 7};
uint8_t clockPin = 14;
uint8_t latchPin = RX;
uint8_t oePin = TX;
#elif defined(ARDUINO_ADAFRUIT_FEATHER_ESP32S2) // Feather ESP32-S2
// M0/M4/RP2040 Matrix FeatherWing compatible:
uint8_t rgbPins[] = {6, 5, 9, 11, 10, 12};
@ -59,11 +66,11 @@ supported boards. Notes have been moved to the bottom of the code.
uint8_t latchPin = 0;
uint8_t oePin = 1;
#elif defined(_SAMD21_) // Feather M0 variants
uint8_t rgbPins[] = {6, 7, 10, 11, 12, 13};
uint8_t addrPins[] = {0, 1, 2, 3};
uint8_t clockPin = SDA;
uint8_t latchPin = 4;
uint8_t oePin = 5;
uint8_t rgbPins[] = {6, 5, 9, 11, 10, 12};
uint8_t addrPins[] = {A5, A4, A3, A2};
uint8_t clockPin = 13;
uint8_t latchPin = 0;
uint8_t oePin = 1;
#elif defined(NRF52_SERIES) // Special nRF52840 FeatherWing pinout
uint8_t rgbPins[] = {6, A5, A1, A0, A4, 11};
uint8_t addrPins[] = {10, 5, 13, 9};

View file

@ -1,5 +1,5 @@
name=Adafruit Protomatter
version=1.5.10
version=1.7.0
author=Adafruit
maintainer=Adafruit <info@adafruit.com>
sentence=A library for Adafruit RGB LED matrices.

View file

@ -144,6 +144,16 @@ public:
*/
uint16_t colorHSV(uint16_t hue, uint8_t sat = 255, uint8_t val = 255);
/*!
@brief Adjust HUB clock signal duty cycle on architectures that support
this (currently SAMD51 only) (else ignored).
@param Duty setting, 0 minimum. Increasing values generate higher clock
duty cycles at the same frequency. Arbitrary granular units, max
varies by architecture and CPU speed, if supported at all.
e.g. SAMD51 @ 120 MHz supports 0 (~50% duty) through 2 (~75%).
*/
void setDuty(uint8_t d) { _PM_setDuty(d); };
private:
Protomatter_core core; // Underlying C struct
void convert_byte(uint8_t *dest); // GFXcanvas16-to-matrix

View file

@ -192,6 +192,7 @@ _PM_CUSTOM_BLAST If defined, instructs core code to not compile
#include "esp32-s2.h"
#include "esp32-s3.h"
#include "esp32-c3.h"
#include "esp32-c6.h"
#include "nrf52.h"
#include "rp2040.h"
#include "samd-common.h"
@ -242,3 +243,11 @@ _PM_CUSTOM_BLAST If defined, instructs core code to not compile
#if !defined(_PM_PORT_TYPE)
#define _PM_PORT_TYPE uint32_t ///< PORT register size/type
#endif
#if !defined(_PM_maxDuty)
#define _PM_maxDuty 0 ///< Max duty cycle setting (where supported)
#endif
#if !defined(_PM_defaultDuty)
#define _PM_defaultDuty 0 ///< Default duty cycle setting (where supported)
#endif

57
src/arch/esp32-c6.h Normal file
View file

@ -0,0 +1,57 @@
/*!
* @file esp32-c3.h
*
* Part of Adafruit's Protomatter library for HUB75-style RGB LED matrices.
* This file contains ESP32-C3-SPECIFIC CODE.
*
* Adafruit invests time and resources providing this open source code,
* please support Adafruit and open-source hardware by purchasing
* products from Adafruit!
*
* Written by Phil "Paint Your Dragon" Burgess and Jeff Epler for
* Adafruit Industries, with contributions from the open source community.
*
* BSD license, all text here must be included in any redistribution.
*
*/
#pragma once
// NOTE: there is some intentional repetition in the macros and functions
// for some ESP32 variants. Previously they were all one file, but complex
// preprocessor directives were turning into spaghetti. THEREFORE, if making
// a change or bugfix in one variant-specific header, check the others to
// see if the same should be applied!
#if defined(CONFIG_IDF_TARGET_ESP32C6)
#define _PM_portOutRegister(pin) (volatile uint32_t *)&GPIO.out
#define _PM_portSetRegister(pin) (volatile uint32_t *)&GPIO.out_w1ts
#define _PM_portClearRegister(pin) (volatile uint32_t *)&GPIO.out_w1tc
#define _PM_portBitMask(pin) (1U << ((pin) & 31))
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
#define _PM_byteOffset(pin) ((pin & 31) / 8)
#define _PM_wordOffset(pin) ((pin & 31) / 16)
#else
#define _PM_byteOffset(pin) (3 - ((pin & 31) / 8))
#define _PM_wordOffset(pin) (1 - ((pin & 31) / 16))
#endif
// No special peripheral setup on ESP32C3, just use common timer init...
#define _PM_timerInit(core) _PM_esp32commonTimerInit(core);
#if defined(ARDUINO) // COMPILING FOR ARDUINO ------------------------------
// Return current count value (timer enabled or not).
// Timer must be previously initialized.
// This function is the same on all ESP32 parts EXCEPT S3.
IRAM_ATTR inline uint32_t _PM_timerGetCount(Protomatter_core *core) {
return (uint32_t)timerRead((hw_timer_t *)core->timer);
}
#elif defined(CIRCUITPY) // COMPILING FOR CIRCUITPYTHON --------------------
#endif // END CIRCUITPYTHON ------------------------------------------------
#endif // END ESP32C3

View file

@ -20,6 +20,8 @@
#if defined(ESP32) || \
defined(ESP_PLATFORM) // *All* ESP32 variants (OG, S2, S3, etc.)
#include <inttypes.h>
#include "esp_idf_version.h"
// NOTE: there is some intentional repetition in the macros and functions
@ -28,7 +30,6 @@
// a change or bugfix in one variant-specific header, check the others to
// see if the same should be applied!
#include "driver/timer.h"
#include "soc/gpio_periph.h"
// As currently written, only one instance of the Protomatter_core struct
@ -39,7 +40,13 @@ Protomatter_core *_PM_protoPtr;
#if defined(ARDUINO) // COMPILING FOR ARDUINO ------------------------------
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
#define _PM_timerNum 0 // Timer #0 (can be 0-3)
static hw_timer_t *_PM_esp32timer = NULL;
#define _PM_TIMER_DEFAULT &_PM_esp32timer
#else
#define _PM_TIMER_DEFAULT ((void *)-1) // some non-NULL but non valid pointer
#endif
// The following defines and functions are common to all ESP32 variants in
// the Arduino platform. Anything unique to one variant (or a subset of
@ -48,16 +55,7 @@ Protomatter_core *_PM_protoPtr;
// started down that path, it's okay, but move the code out of here and
// into the variant-specific headers.
// This is the default aforementioned singular timer. IN THEORY, other
// timers could be used, IF an Arduino sketch passes the address of its
// own hw_timer_t* to the Protomatter constructor and initializes that
// timer using ESP32's timerBegin(). All of the timer-related functions
// below pass around a handle rather than accessing _PM_esp32timer directly,
// in case that's ever actually used in the future.
static hw_timer_t *_PM_esp32timer = NULL;
#define _PM_TIMER_DEFAULT &_PM_esp32timer
extern IRAM_ATTR void _PM_row_handler(Protomatter_core *core); // In core.c
extern void _PM_row_handler(Protomatter_core *core); // In core.c
// Timer interrupt handler. This, _PM_row_handler() and any functions
// called by _PM_row_handler() should all have the IRAM_ATTR attribute
@ -71,9 +69,15 @@ IRAM_ATTR static void _PM_esp32timerCallback(void) {
// Set timer period, initialize count value to zero, enable timer.
IRAM_ATTR inline void _PM_timerStart(Protomatter_core *core, uint32_t period) {
hw_timer_t *timer = (hw_timer_t *)core->timer;
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
timerAlarmWrite(timer, period, true);
timerAlarmEnable(timer);
timerStart(timer);
#else
timerWrite(timer, 0);
timerAlarm(timer, period ? period : 1, true, 0);
timerStart(timer);
#endif
}
// Disable timer and return current count value.
@ -87,11 +91,19 @@ IRAM_ATTR uint32_t _PM_timerStop(Protomatter_core *core) {
// that's common to all ESP32 variants; code in variant-specific files might
// set up its own special peripherals, then call this.
void _PM_esp32commonTimerInit(Protomatter_core *core) {
hw_timer_t *timer = (hw_timer_t *)core->timer; // pointer-to-pointer
if (timer == _PM_TIMER_DEFAULT) {
hw_timer_t *timer_in = (hw_timer_t *)core->timer;
if (!timer_in || timer_in == _PM_TIMER_DEFAULT) {
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
core->timer = timerBegin(_PM_timerNum, 2, true); // 1:2 prescale, count up
#else
core->timer = timerBegin(_PM_timerFreq);
#endif
}
timerAttachInterrupt(timer, &_PM_esp32timerCallback, true);
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
timerAttachInterrupt(core->timer, &_PM_esp32timerCallback, true);
#else
timerAttachInterrupt(core->timer, _PM_esp32timerCallback);
#endif
}
#elif defined(CIRCUITPY) // COMPILING FOR CIRCUITPYTHON --------------------
@ -106,7 +118,12 @@ void _PM_esp32commonTimerInit(Protomatter_core *core) {
#include "driver/gpio.h"
#include "esp_idf_version.h"
#include "hal/timer_ll.h"
#include "peripherals/timer.h"
#if ESP_IDF_VERSION_MAJOR == 5
#include "driver/gptimer.h"
#include "esp_memory_utils.h"
#else
#include "driver/timer.h"
#endif
#define _PM_TIMER_DEFAULT NULL
#define _PM_pinOutput(pin) gpio_set_direction((pin), GPIO_MODE_OUTPUT)
@ -118,8 +135,24 @@ void _PM_esp32commonTimerInit(Protomatter_core *core) {
// (RAM-resident functions). This isn't really the ISR itself, but a
// callback invoked by the real ISR (in arduino-esp32's esp32-hal-timer.c)
// which takes care of interrupt status bits & such.
#if ESP_IDF_VERSION_MAJOR == 5
// This is "private" for now. We link to it anyway because there isn't a more
// public method yet.
extern bool spi_flash_cache_enabled(void);
static IRAM_ATTR bool
_PM_esp32timerCallback(gptimer_handle_t timer,
const gptimer_alarm_event_data_t *event, void *unused) {
#else
static IRAM_ATTR bool _PM_esp32timerCallback(void *unused) {
#endif
#if ESP_IDF_VERSION_MAJOR == 5
// Some functions and data used by _PM_row_handler may exist in external flash
// or PSRAM so we can't run them when their access is disabled (through the
// flash cache.)
if (_PM_protoPtr && spi_flash_cache_enabled()) {
#else
if (_PM_protoPtr) {
#endif
_PM_row_handler(_PM_protoPtr); // In core.c
}
return false;
@ -127,14 +160,16 @@ static IRAM_ATTR bool _PM_esp32timerCallback(void *unused) {
// Set timer period, initialize count value to zero, enable timer.
#if (ESP_IDF_VERSION_MAJOR == 5)
static IRAM_ATTR void _PM_timerStart(Protomatter_core *core, uint32_t period) {
timer_index_t *timer = (timer_index_t *)core->timer;
timer_ll_enable_counter(timer->hw, timer->idx, false);
timer_ll_set_reload_value(timer->hw, timer->idx, 0);
timer_ll_trigger_soft_reload(timer->hw, timer->idx);
timer_ll_set_alarm_value(timer->hw, timer->idx, period);
timer_ll_enable_alarm(timer->hw, timer->idx, true);
timer_ll_enable_counter(timer->hw, timer->idx, true);
IRAM_ATTR void _PM_timerStart(Protomatter_core *core, uint32_t period) {
gptimer_handle_t timer = (gptimer_handle_t)core->timer;
gptimer_alarm_config_t alarm_config = {
.reload_count = 0, // counter will reload with 0 on alarm event
.alarm_count = period, // period in ms
.flags.auto_reload_on_alarm = true, // enable auto-reload
};
gptimer_set_alarm_action(timer, &alarm_config);
gptimer_start(timer);
}
#else
IRAM_ATTR void _PM_timerStart(Protomatter_core *core, uint32_t period) {
@ -150,10 +185,11 @@ IRAM_ATTR void _PM_timerStart(Protomatter_core *core, uint32_t period) {
// Disable timer and return current count value.
// Timer must be previously initialized.
IRAM_ATTR uint32_t _PM_timerStop(Protomatter_core *core) {
timer_index_t *timer = (timer_index_t *)core->timer;
#if (ESP_IDF_VERSION_MAJOR == 5)
timer_ll_enable_counter(timer->hw, timer->idx, false);
gptimer_handle_t timer = (gptimer_handle_t)core->timer;
gptimer_stop(timer);
#else
timer_index_t *timer = (timer_index_t *)core->timer;
timer_ll_set_counter_enable(timer->hw, timer->idx, false);
#endif
return _PM_timerGetCount(core);
@ -161,10 +197,17 @@ IRAM_ATTR uint32_t _PM_timerStop(Protomatter_core *core) {
#if !defined(CONFIG_IDF_TARGET_ESP32S3)
IRAM_ATTR uint32_t _PM_timerGetCount(Protomatter_core *core) {
#if (ESP_IDF_VERSION_MAJOR == 5)
gptimer_handle_t timer = (gptimer_handle_t)core->timer;
uint64_t raw_count;
gptimer_get_raw_count(timer, &raw_count);
return (uint32_t)raw_count;
#else
timer_index_t *timer = (timer_index_t *)core->timer;
uint64_t result;
timer_ll_get_counter_value(timer->hw, timer->idx, &result);
return (uint32_t)result;
#endif
}
#endif
@ -172,6 +215,16 @@ IRAM_ATTR uint32_t _PM_timerGetCount(Protomatter_core *core) {
// that's common to all ESP32 variants; code in variant-specific files might
// set up its own special peripherals, then call this.
static void _PM_esp32commonTimerInit(Protomatter_core *core) {
#if (ESP_IDF_VERSION_MAJOR == 5)
gptimer_handle_t timer = (gptimer_handle_t)core->timer;
gptimer_event_callbacks_t cbs = {
.on_alarm = _PM_esp32timerCallback, // register user callback
};
gptimer_register_event_callbacks(timer, &cbs, NULL);
gptimer_enable(timer);
#else
timer_index_t *timer = (timer_index_t *)core->timer;
const timer_config_t config = {
.alarm_en = false,
@ -186,6 +239,7 @@ static void _PM_esp32commonTimerInit(Protomatter_core *core) {
timer_isr_callback_add(timer->group, timer->idx, _PM_esp32timerCallback, NULL,
0);
timer_enable_intr(timer->group, timer->idx);
#endif
}
#endif // END CIRCUITPYTHON ------------------------------------------------

View file

@ -25,6 +25,9 @@
#if defined(CONFIG_IDF_TARGET_ESP32S3)
#define GPIO_DRIVE_STRENGTH GPIO_DRIVE_CAP_3
#define LCD_CLK_PRESCALE 9 // 8, 9, 10 allowed. Bit clock = 160 MHz / this.
#if defined(CIRCUITPY) // COMPILING FOR CIRCUITPYTHON --------------------
#include "components/esp_rom/include/esp_rom_sys.h"
#include "components/heap/include/esp_heap_caps.h"
@ -62,16 +65,16 @@
// dmaSetupTime (measured in blast_byte()) measures the number of timer
// cycles to set up and trigger the DMA transfer...
static uint32_t dmaSetupTime = 100;
// ...then, the version of _PM_timerGetCount() here uses that figure as a
// starting point, plus the known constant DMA transfer speed (20 MHz) and
// timer frequency (40 MHz), i.e. 2 cycles/column, to return a fair estimate
// of the one-scanline transfer time, from which everything is extrapolated:
// ...then, the version of _PM_timerGetCount() here uses that as a starting
// point, plus the known constant DMA xfer speed (160/LCD_CLK_PRESCALE MHz)
// and timer frequency (40 MHz), to return an estimate of the one-scanline
// transfer time, from which everything is extrapolated:
IRAM_ATTR inline uint32_t _PM_timerGetCount(Protomatter_core *core) {
// Time estimate seems to come in a little high, so the -10 here is an
// empirically-derived fudge factor that may yield ever-so-slightly better
// refresh in some edge cases. If visual glitches are encountered, might
// need to dial back this number a bit or remove it.
return dmaSetupTime + core->chainBits * 2 - 10;
return dmaSetupTime + core->chainBits * 40 * LCD_CLK_PRESCALE / 160 - 10;
}
// Note that dmaSetupTime can vary from line to line, potentially influenced
// by interrupts, nondeterministic DMA channel clearing times, etc., which is
@ -79,13 +82,15 @@ IRAM_ATTR inline uint32_t _PM_timerGetCount(Protomatter_core *core) {
// slightly different length of time, but duty cycle scales with this so it's
// perceptually consistent; don't see bright or dark rows.
#define _PM_minMinPeriod (200 + (uint32_t)core->chainBits * 2)
#define _PM_minMinPeriod \
(200 + (uint32_t)core->chainBits * 40 * LCD_CLK_PRESCALE / 160)
#if (ESP_IDF_VERSION_MAJOR == 5)
#include <esp_private/periph_ctrl.h>
#else
#include <driver/periph_ctrl.h>
#endif
#include <driver/gpio.h>
#include <esp_private/gdma.h>
#include <esp_rom_gpio.h>
#include <hal/dma_types.h>
@ -124,11 +129,10 @@ IRAM_ATTR static void blast_long(Protomatter_core *core, uint32_t *data) {}
static void pinmux(int8_t pin, uint8_t signal) {
esp_rom_gpio_connect_out_signal(pin, signal, false, false);
gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO);
gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_CAP_MAX);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO);
gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_STRENGTH);
}
#if defined(ARDUINO) // COMPILING FOR ARDUINO ------------------------------
// LCD_CAM requires a complete replacement of the "blast" functions in order
// to use the DMA-based peripheral.
#define _PM_CUSTOM_BLAST // Disable blast_*() functions in core.c
@ -156,148 +160,23 @@ IRAM_ATTR static void blast_byte(Protomatter_core *core, uint8_t *data) {
// Timer was cleared to 0 before calling blast_byte(), so this
// is the state of the timer immediately after DMA started:
#if defined(ARDUINO)
dmaSetupTime = (uint32_t)timerRead((hw_timer_t *)core->timer);
// See notes near top of this file for what's done with this info.
}
void _PM_timerInit(Protomatter_core *core) {
// On S3, initialize the LCD_CAM peripheral and DMA.
// LCD_CAM isn't enabled by default -- MUST begin with this:
periph_module_enable(PERIPH_LCD_CAM_MODULE);
periph_module_reset(PERIPH_LCD_CAM_MODULE);
// Reset LCD bus
LCD_CAM.lcd_user.lcd_reset = 1;
esp_rom_delay_us(100);
// Configure LCD clock
LCD_CAM.lcd_clock.clk_en = 1; // Enable clock
LCD_CAM.lcd_clock.lcd_clk_sel = 3; // PLL160M source
LCD_CAM.lcd_clock.lcd_clkm_div_a = 1; // 1/1 fractional divide,
LCD_CAM.lcd_clock.lcd_clkm_div_b = 1; // plus '7' below yields...
LCD_CAM.lcd_clock.lcd_clkm_div_num = 7; // 1:8 prescale (20 MHz CLK)
LCD_CAM.lcd_clock.lcd_ck_out_edge = 0; // PCLK low in first half of cycle
LCD_CAM.lcd_clock.lcd_ck_idle_edge = 0; // PCLK low idle
LCD_CAM.lcd_clock.lcd_clk_equ_sysclk = 1; // PCLK = CLK (ignore CLKCNT_N)
// Configure frame format. Some of these could probably be skipped and
// use defaults, but being verbose for posterity...
LCD_CAM.lcd_ctrl.lcd_rgb_mode_en = 0; // i8080 mode (not RGB)
LCD_CAM.lcd_rgb_yuv.lcd_conv_bypass = 0; // Disable RGB/YUV converter
LCD_CAM.lcd_misc.lcd_next_frame_en = 0; // Do NOT auto-frame
LCD_CAM.lcd_data_dout_mode.val = 0; // No data delays
LCD_CAM.lcd_user.lcd_always_out_en = 0; // Only when requested
LCD_CAM.lcd_user.lcd_8bits_order = 0; // Do not swap bytes
LCD_CAM.lcd_user.lcd_bit_order = 0; // Do not reverse bit order
LCD_CAM.lcd_user.lcd_2byte_en = 0; // 8-bit data mode
// MUST enable at least one dummy phase at start of output, else clock and
// data are randomly misaligned by 1-2 cycles following required TX FIFO
// reset in blast_byte(). One phase MOSTLY works but sparkles a tiny bit
// (as in still very occasionally misaligned by 1 cycle). Two seems ideal;
// no sparkle. Since HUB75 is just a shift register, the extra clock ticks
// are harmless and the zero-data shifts off end of the chain.
LCD_CAM.lcd_user.lcd_dummy = 1; // Enable dummy phase(s) @ LCD start
LCD_CAM.lcd_user.lcd_dummy_cyclelen = 1; // 2 dummy phases
LCD_CAM.lcd_user.lcd_cmd = 0; // No command at LCD start
LCD_CAM.lcd_user.lcd_cmd_2_cycle_en = 0;
LCD_CAM.lcd_user.lcd_update = 1;
// Configure signal pins. IN THEORY this could be expanded to support
// 2 parallel chains, but the rest of the LCD & DMA setup is not currently
// written for that, so it's limited to a single chain for now.
const uint8_t signal[] = {LCD_DATA_OUT0_IDX, LCD_DATA_OUT1_IDX,
LCD_DATA_OUT2_IDX, LCD_DATA_OUT3_IDX,
LCD_DATA_OUT4_IDX, LCD_DATA_OUT5_IDX};
for (int i = 0; i < 6; i++)
pinmux(core->rgbPins[i], signal[i]);
pinmux(core->clockPin, LCD_PCLK_IDX);
gpio_set_drive_capability(core->latch.pin, GPIO_DRIVE_CAP_MAX);
gpio_set_drive_capability(core->oe.pin, GPIO_DRIVE_CAP_MAX);
for (uint8_t i = 0; i < core->numAddressLines; i++) {
gpio_set_drive_capability(core->addr[i].pin, GPIO_DRIVE_CAP_MAX);
}
// Disable LCD_CAM interrupts, clear any pending interrupt
LCD_CAM.lc_dma_int_ena.val &= ~LCD_LL_EVENT_TRANS_DONE;
LCD_CAM.lc_dma_int_clr.val = 0x03;
// Set up DMA TX descriptor
desc.dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA;
desc.dw0.suc_eof = 1;
desc.dw0.size = desc.dw0.length = core->chainBits;
desc.buffer = core->screenData;
desc.next = NULL;
// Alloc DMA channel & connect it to LCD periph
gdma_channel_alloc_config_t dma_chan_config = {
.sibling_chan = NULL,
.direction = GDMA_CHANNEL_DIRECTION_TX,
.flags = {.reserve_sibling = 0}};
esp_err_t ret = gdma_new_channel(&dma_chan_config, &dma_chan);
(void)ret;
gdma_connect(dma_chan, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0));
gdma_strategy_config_t strategy_config = {.owner_check = false,
.auto_update_desc = false};
gdma_apply_strategy(dma_chan, &strategy_config);
gdma_transfer_ability_t ability = {
.sram_trans_align = 0,
.psram_trans_align = 0,
};
gdma_set_transfer_ability(dma_chan, &ability);
gdma_start(dma_chan, (intptr_t)&desc);
// Enable TRANS_DONE interrupt. Note that we do NOT require nor install
// an interrupt service routine, but DO need to enable the TRANS_DONE
// flag to make the LCD DMA transfer work.
LCD_CAM.lc_dma_int_ena.val |= LCD_LL_EVENT_TRANS_DONE & 0x03;
_PM_esp32commonTimerInit(core); // In esp32-common.h
}
#elif defined(CIRCUITPY) // COMPILING FOR CIRCUITPYTHON --------------------
// LCD_CAM requires a complete replacement of the "blast" functions in order
// to use the DMA-based peripheral.
#define _PM_CUSTOM_BLAST // Disable blast_*() functions in core.c
IRAM_ATTR static void blast_byte(Protomatter_core *core, uint8_t *data) {
// Reset LCD DOUT parameters each time (required).
// IN PRINCIPLE, cyclelen should be chainBits-1 (resulting in chainBits
// cycles). But due to the required dummy phases at start of transfer,
// extend by 1; set to chainBits, issue chainBits+1 cycles.
LCD_CAM.lcd_user.lcd_dout_cyclelen = core->chainBits;
LCD_CAM.lcd_user.lcd_dout = 1;
LCD_CAM.lcd_user.lcd_update = 1;
// Reset LCD TX FIFO each time, else we see old data. When doing this,
// it's REQUIRED in the setup code to enable at least one dummy pulse,
// else the PCLK & data are randomly misaligned by 1-2 clocks!
LCD_CAM.lcd_misc.lcd_afifo_reset = 1;
// Partially re-init descriptor each time (required)
desc.dw0.size = desc.dw0.length = core->chainBits;
desc.buffer = data;
gdma_start(dma_chan, (intptr_t)&desc);
esp_rom_delay_us(1); // Necessary before starting xfer
LCD_CAM.lcd_user.lcd_start = 1; // Begin LCD DMA xfer
// Timer was cleared to 0 before calling blast_byte(), so this
// is the state of the timer immediately after DMA started:
#elif defined(CIRCUITPY)
uint64_t value;
#if (ESP_IDF_VERSION_MAJOR == 5)
gptimer_handle_t timer = (gptimer_handle_t)core->timer;
gptimer_get_raw_count(timer, &value);
#else
timer_index_t *timer = (timer_index_t *)core->timer;
timer_get_counter_value(timer->group, timer->idx, &value);
#endif
dmaSetupTime = (uint32_t)value;
#endif
// See notes near top of this file for what's done with this info.
}
static void _PM_timerInit(Protomatter_core *core) {
// TO DO: adapt this function for any CircuitPython-specific changes.
// If none are required, this function can be deleted and the version
// above can be moved before the ARDUIO/CIRCUITPY checks. If minimal
// changes, consider a single _PM_timerInit() implementation with
// ARDUINO/CIRCUITPY checks inside. It's all good.
// On S3, initialize the LCD_CAM peripheral and DMA.
// LCD_CAM isn't enabled by default -- MUST begin with this:
@ -312,8 +191,14 @@ static void _PM_timerInit(Protomatter_core *core) {
LCD_CAM.lcd_clock.clk_en = 1; // Enable clock
LCD_CAM.lcd_clock.lcd_clk_sel = 3; // PLL160M source
LCD_CAM.lcd_clock.lcd_clkm_div_a = 1; // 1/1 fractional divide,
LCD_CAM.lcd_clock.lcd_clkm_div_b = 1; // plus '7' below yields...
LCD_CAM.lcd_clock.lcd_clkm_div_b = 1; // plus prescale below yields...
#if LCD_CLK_PRESCALE == 8
LCD_CAM.lcd_clock.lcd_clkm_div_num = 7; // 1:8 prescale (20 MHz CLK)
#elif LCD_CLK_PRESCALE == 9
LCD_CAM.lcd_clock.lcd_clkm_div_num = 8; // 1:9 prescale (17.8 MHz CLK)
#else
LCD_CAM.lcd_clock.lcd_clkm_div_num = 9; // 1:10 prescale (16 MHz CLK)
#endif
LCD_CAM.lcd_clock.lcd_ck_out_edge = 0; // PCLK low in first half of cycle
LCD_CAM.lcd_clock.lcd_ck_idle_edge = 0; // PCLK low idle
LCD_CAM.lcd_clock.lcd_clk_equ_sysclk = 1; // PCLK = CLK (ignore CLKCNT_N)
@ -349,10 +234,10 @@ static void _PM_timerInit(Protomatter_core *core) {
for (int i = 0; i < 6; i++)
pinmux(core->rgbPins[i], signal[i]);
pinmux(core->clockPin, LCD_PCLK_IDX);
gpio_set_drive_capability(core->latch.pin, GPIO_DRIVE_CAP_MAX);
gpio_set_drive_capability(core->oe.pin, GPIO_DRIVE_CAP_MAX);
gpio_set_drive_capability(core->latch.pin, GPIO_DRIVE_STRENGTH);
gpio_set_drive_capability(core->oe.pin, GPIO_DRIVE_STRENGTH);
for (uint8_t i = 0; i < core->numAddressLines; i++) {
gpio_set_drive_capability(core->addr[i].pin, GPIO_DRIVE_CAP_MAX);
gpio_set_drive_capability(core->addr[i].pin, GPIO_DRIVE_STRENGTH);
}
// Disable LCD_CAM interrupts, clear any pending interrupt
@ -367,7 +252,9 @@ static void _PM_timerInit(Protomatter_core *core) {
desc.next = NULL;
// Alloc DMA channel & connect it to LCD periph
#if defined(CIRCUITPY)
if (dma_chan == NULL) {
#endif
gdma_channel_alloc_config_t dma_chan_config = {
.sibling_chan = NULL,
.direction = GDMA_CHANNEL_DIRECTION_TX,
@ -382,7 +269,9 @@ static void _PM_timerInit(Protomatter_core *core) {
.psram_trans_align = 0,
};
gdma_set_transfer_ability(dma_chan, &ability);
#if defined(CIRCUITPY)
}
#endif
gdma_start(dma_chan, (intptr_t)&desc);
// Enable TRANS_DONE interrupt. Note that we do NOT require nor install
@ -393,6 +282,4 @@ static void _PM_timerInit(Protomatter_core *core) {
_PM_esp32commonTimerInit(core); // In esp32-common.h
}
#endif // END CIRCUITPYTHON ------------------------------------------------
#endif // END ESP32S3

View file

@ -26,7 +26,8 @@
#pragma once
#if defined(ARDUINO_ARCH_RP2040) || defined(PICO_BOARD) || defined(__RP2040__)
#if defined(ARDUINO_ARCH_RP2040) || defined(PICO_BOARD) || \
defined(__RP2040__) || defined(__RP2350__)
#include "../../hardware_pwm/include/hardware/pwm.h"
#include "hardware/irq.h"
@ -105,8 +106,13 @@ void _PM_timerInit(Protomatter_core *core) {
#elif defined(CIRCUITPY) // COMPILING FOR CIRCUITPYTHON --------------------
#if !defined(F_CPU) // Not sure if CircuitPython build defines this
#ifdef __RP2040__
#define F_CPU 125000000 // Standard RP2040 clock speed
#endif
#ifdef __RP2350__
#define F_CPU 150000000 // Standard RP2350 clock speed
#endif
#endif
// 'pin' here is GPXX #
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
@ -240,10 +246,10 @@ uint32_t _PM_timerStop(Protomatter_core *core) {
#define _PM_clockHoldLow asm("nop; nop; nop;");
#define _PM_clockHoldHigh asm("nop; nop; nop;");
#elif (F_CPU >= 175000000)
#define _PM_clockHoldLow asm("nop; nop;");
#define _PM_clockHoldLow asm("nop; nop; nop;");
#define _PM_clockHoldHigh asm("nop;");
#elif (F_CPU >= 125000000)
#define _PM_clockHoldLow asm("nop;");
#define _PM_clockHoldLow asm("nop; nop; nop;");
#define _PM_clockHoldHigh asm("nop;");
#elif (F_CPU >= 100000000)
#define _PM_clockHoldLow asm("nop;");

View file

@ -47,6 +47,21 @@
#define F_CPU (120000000)
// Enable high output driver strength on one pin. Arduino does this by
// default on pinMode(OUTPUT), but CircuitPython requires the motions...
static void _hi_drive(uint8_t pin) {
// For Arduino testing only:
// pin = g_APinDescription[pin].ulPort * 32 + g_APinDescription[pin].ulPin;
// Input, pull-up and peripheral MUX are disabled as we're only using
// vanilla PORT writes on Protomatter GPIO.
PORT->Group[pin / 32].WRCONFIG.reg =
(pin & 16)
? PORT_WRCONFIG_WRPINCFG | PORT_WRCONFIG_DRVSTR |
PORT_WRCONFIG_HWSEL | (1 << (pin & 15))
: PORT_WRCONFIG_WRPINCFG | PORT_WRCONFIG_DRVSTR | (1 << (pin & 15));
}
#else
// Other port register lookups go here
@ -157,6 +172,20 @@ void _PM_timerInit(Protomatter_core *core) {
NVIC_EnableIRQ(timer[timerNum].IRQn);
// Timer is configured but NOT enabled by default
#if defined(CIRCUITPY) // See notes earlier; Arduino doesn't need this.
// Enable high drive strength on all Protomatter pins. TBH this is kind
// of a jerky place to do this (it's not actually related to the timer
// peripheral) but Protomatter doesn't really have a spot for it.
uint8_t i;
for (i = 0; i < core->parallel * 6; i++)
_hi_drive(core->rgbPins[i]);
for (i = 0; i < core->numAddressLines; i++)
_hi_drive(core->addr[i].pin);
_hi_drive(core->clockPin);
_hi_drive(core->latch.pin);
_hi_drive(core->oe.pin);
#endif
}
// Set timer period, initialize count value to zero, enable timer.
@ -196,31 +225,204 @@ uint32_t _PM_timerStop(Protomatter_core *core) {
return count;
}
// See notes in core.c before the "blast" functions.
// The NOP counts here were derived by monitoring on a fast logic analyzer,
// aiming for 20 MHz clock at 50% duty cycle (for the unrolled parts of the
// 'blast' loop...every Nth bit is somewhat longer), and tested for each
// F_CPU setting. This seems to have the broadest compatibility across many
// matrix varieties. Only one, a 64x32 flex matrix, showed some artifacts at
// the end of a 4-matrix chain at 120-150 MHz F_CPU -- either use in shorter
// chains, or can kludge it by running at 180-200 MHz or by moving one NOP
// from the Low to High section (but which then causes issues with other
// matrix types, so it's not done here), unfortunately no means of run-time
// configuration for this.
#if F_CPU >= 200000000
#define _PM_clockHoldHigh asm("nop; nop");
#define _PM_clockHoldLow asm("nop; nop; nop; nop; nop");
#elif F_CPU >= 180000000
#define _PM_clockHoldHigh asm("nop; nop");
#define _PM_clockHoldLow asm("nop; nop; nop; nop");
#elif F_CPU >= 150000000
#define _PM_clockHoldHigh asm("nop");
#define _PM_clockHoldLow asm("nop; nop; nop");
#else
#define _PM_clockHoldHigh asm("nop");
#define _PM_clockHoldLow asm("nop; nop");
// SAMD51 takes a WEIRD TURN here, in an attempt to make the HUB75 clock
// waveform slightly adjustable. Old vs new matrices seem to have different
// preferences, and this tries to address that. If this works well then the
// approach might be applied to other architectures (which are all fixed
// duty cycle right now). THE CHALLENGE is that Protomatter works in a bit-
// bangingly way (this is on purpose and by design, avoiding peripherals
// that might work only on certain pins, for better compatibility with
// existing shields and wings from the AVR era), we're aiming for nearly a
// 20 MHz signal, and the SAMD51 cycle clock is ostensibly 120 MHz. With
// just a few cycles to toggle the data and clock lines, that doesn't even
// leave enough time for a counter loop.
#define _PM_CUSTOM_BLAST ///< Disable blast_*() functions in core.c
#define _PM_chunkSize 8 ///< Data-stuffing loop is unrolled to this size
extern uint8_t _PM_duty; // In core.c
// The approach is to use a small list of pointers, with a clock-toggling
// value written to each one in succession. Most of the pointers are aimed
// on a nonsense "bit bucket" variable, effectively becoming NOPs, and just
// one is set to the PORT toggle register, raising the clock. A couple of
// actual traditional NOPs are also present because concurrent PORT writes
// on SAMD51 incur a 1-cycle delay, so the NOPs keep the clock frequency
// constant (tradeoff is that the clock is now 7 rather than 6 cycles --
// ~17.1 MHz rather than 20 with F_CPU at 120 MHz). The NOPs could be
// removed and duty range increased by one, but the tradeoff then is
// inconsistent timing at different duty settings. That 1-cycle delay is
// also why this uses a list of pointers with a common value, rather than
// a common pointer (the PORT reg) with a list of values -- because those
// writes would all take 2 cycles, way too slow. A counter loop would also
// take 2 cycles/count, because of the branch.
#if F_CPU >= 200000000 // 200 MHz; 10 cycles/bit; 20 MHz, 6 duty settings
#define _PM_maxDuty 5 ///< Allow duty settings 0-5
#define _PM_defaultDuty 2 ///< ~60%
#define PEW \
asm("nop"); \
*toggle = *data++; \
asm("nop"); \
*ptr0 = clock; \
*ptr1 = clock; \
*ptr2 = clock; \
*ptr3 = clock; \
*ptr4 = clock; \
*ptr5 = clock;
#elif F_CPU >= 180000000 // 180 MHz; 9 cycles/bit; 20 MHz, 5 duty settings
#define _PM_maxDuty 4 ///< Allow duty settings 0-4
#define _PM_defaultDuty 1 ///< ~50%
#define PEW \
asm("nop"); \
*toggle = *data++; \
asm("nop"); \
*ptr0 = clock; \
*ptr1 = clock; \
*ptr2 = clock; \
*ptr3 = clock; \
*ptr4 = clock;
#elif F_CPU >= 150000000 // 150 MHz; 8 cycles/bit; 18.75 MHz, 4 duty settings
#define _PM_maxDuty 3 ///< Allow duty settings 0-3
#define _PM_defaultDuty 1 ///< ~55%
#define PEW \
asm("nop"); \
*toggle = *data++; \
asm("nop"); \
*ptr0 = clock; \
*ptr1 = clock; \
*ptr2 = clock; \
*ptr3 = clock;
#else // 120 MHz; 7 cycles/bit; 17.1 MHz, 3 duty settings
#define _PM_maxDuty 2 ///< Allow duty settings 0-2
#define _PM_defaultDuty 0 ///< ~50%
#define PEW \
asm("nop"); \
*toggle = *data++; \
asm("nop"); \
*ptr0 = clock; \
*ptr1 = clock; \
*ptr2 = clock;
#endif
static void blast_byte(Protomatter_core *core, uint8_t *data) {
// If here, it was established in begin() that the RGB data bits and
// clock are all within the same byte of a PORT register, else we'd be
// in the word- or long-blasting functions now. So we just need an
// 8-bit pointer to the PORT:
volatile uint8_t *toggle =
(volatile uint8_t *)core->toggleReg + core->portOffset;
uint8_t bucket, clock = core->clockMask;
// Pointer list must be distinct vars, not an array, else slow.
volatile uint8_t *ptr0 =
(_PM_duty == _PM_maxDuty) ? toggle : (volatile uint8_t *)&bucket;
volatile uint8_t *ptr1 =
(_PM_duty == (_PM_maxDuty - 1)) ? toggle : (volatile uint8_t *)&bucket;
volatile uint8_t *ptr2 =
(_PM_duty == (_PM_maxDuty - 2)) ? toggle : (volatile uint8_t *)&bucket;
#if _PM_maxDuty >= 3
volatile uint8_t *ptr3 =
(_PM_duty == (_PM_maxDuty - 3)) ? toggle : (volatile uint8_t *)&bucket;
#endif
#if _PM_maxDuty >= 4
volatile uint8_t *ptr4 =
(_PM_duty == (_PM_maxDuty - 4)) ? toggle : (volatile uint8_t *)&bucket;
#endif
#if _PM_maxDuty >= 5
volatile uint8_t *ptr5 =
(_PM_duty == (_PM_maxDuty - 5)) ? toggle : (volatile uint8_t *)&bucket;
#endif
uint16_t chunks = core->chainBits / 8;
// PORT has already been initialized with RGB data + clock bits
// all LOW, so we don't need to initialize that state here.
do {
PEW PEW PEW PEW PEW PEW PEW PEW
} while (--chunks);
// Want the PORT left with RGB data and clock LOW on function exit
// (so it's easier to see on 'scope, and to prime it for the next call).
// This is implicit in the no-toggle case (due to how the PEW macro
// works), but toggle case requires explicitly clearing those bits.
// rgbAndClockMask is an 8-bit value when toggling, hence offset here.
*((volatile uint8_t *)core->clearReg + core->portOffset) =
core->rgbAndClockMask;
}
// This is a copypasta of blast_byte() with types changed to uint16_t.
static void blast_word(Protomatter_core *core, uint16_t *data) {
volatile uint16_t *toggle = (uint16_t *)core->toggleReg + core->portOffset;
uint16_t bucket, clock = core->clockMask;
volatile uint16_t *ptr0 =
(_PM_duty == _PM_maxDuty) ? toggle : (volatile uint16_t *)&bucket;
volatile uint16_t *ptr1 =
(_PM_duty == (_PM_maxDuty - 1)) ? toggle : (volatile uint16_t *)&bucket;
volatile uint16_t *ptr2 =
(_PM_duty == (_PM_maxDuty - 2)) ? toggle : (volatile uint16_t *)&bucket;
#if _PM_maxDuty >= 3
volatile uint16_t *ptr3 =
(_PM_duty == (_PM_maxDuty - 3)) ? toggle : (volatile uint16_t *)&bucket;
#endif
#if _PM_maxDuty >= 4
volatile uint16_t *ptr4 =
(_PM_duty == (_PM_maxDuty - 4)) ? toggle : (volatile uint16_t *)&bucket;
#endif
#if _PM_maxDuty >= 5
volatile uint16_t *ptr5 =
(_PM_duty == (_PM_maxDuty - 5)) ? toggle : (volatile uint16_t *)&bucket;
#endif
uint16_t chunks = core->chainBits / 8;
do {
PEW PEW PEW PEW PEW PEW PEW PEW
} while (--chunks);
*((volatile uint16_t *)core->clearReg + core->portOffset) =
core->rgbAndClockMask;
}
// This is a copypasta of blast_byte() with types changed to uint32_t.
static void blast_long(Protomatter_core *core, uint32_t *data) {
volatile uint32_t *toggle = (uint32_t *)core->toggleReg;
uint32_t bucket, clock = core->clockMask;
volatile uint32_t *ptr0 =
(_PM_duty == _PM_maxDuty) ? toggle : (volatile uint32_t *)&bucket;
volatile uint32_t *ptr1 =
(_PM_duty == (_PM_maxDuty - 1)) ? toggle : (volatile uint32_t *)&bucket;
volatile uint32_t *ptr2 =
(_PM_duty == (_PM_maxDuty - 2)) ? toggle : (volatile uint32_t *)&bucket;
#if _PM_maxDuty >= 3
volatile uint32_t *ptr3 =
(_PM_duty == (_PM_maxDuty - 3)) ? toggle : (volatile uint32_t *)&bucket;
#endif
#if _PM_maxDuty >= 4
volatile uint32_t *ptr4 =
(_PM_duty == (_PM_maxDuty - 4)) ? toggle : (volatile uint32_t *)&bucket;
#endif
#if _PM_maxDuty >= 5
volatile uint32_t *ptr5 =
(_PM_duty == (_PM_maxDuty - 5)) ? toggle : (volatile uint32_t *)&bucket;
#endif
uint16_t chunks = core->chainBits / 8;
do {
PEW PEW PEW PEW PEW PEW PEW PEW
} while (--chunks);
*((volatile uint32_t *)core->clearReg + core->portOffset) =
core->rgbAndClockMask;
}
#define _PM_minMinPeriod 160
#endif // END __SAMD51__ || SAM_D5X_E5X

View file

@ -83,7 +83,7 @@ volatile uint16_t *_PM_portClearRegister(uint32_t pin) {
// TODO: this is no longer true, should it change?
void *_PM_protoPtr = NULL;
STATIC TIM_HandleTypeDef tim_handle;
static TIM_HandleTypeDef tim_handle;
// Timer interrupt service routine
void _PM_IRQ_HANDLER(void) {

View file

@ -95,8 +95,9 @@ ProtomatterStatus _PM_init(Protomatter_core *core, uint16_t bitWidth,
uint8_t addrCount, uint8_t *addrList,
uint8_t clockPin, uint8_t latchPin, uint8_t oePin,
bool doubleBuffer, int8_t tile, void *timer) {
if (!core)
if (!core) {
return PROTOMATTER_ERR_ARG;
}
// bitDepth is NOT constrained here, handle in calling function
// (varies with implementation, e.g. GFX lib is max 6 bitplanes,
@ -922,6 +923,10 @@ static void _PM_resetFM6126A(Protomatter_core *core) {
_PM_rgbState(core, 0); // Set all RGB low so port toggle can work
}
uint8_t _PM_duty = _PM_defaultDuty;
void _PM_setDuty(uint8_t d) { _PM_duty = (d > _PM_maxDuty) ? _PM_maxDuty : d; }
#if defined(ARDUINO) || defined(CIRCUITPY)
// Arduino and CircuitPython happen to use the same internal canvas
@ -1195,18 +1200,24 @@ void _PM_convert_565_word(Protomatter_core *core, uint16_t *source,
uint16_t upperRGB = upperSrc[srcIdx]; // Pixel in upper half
uint16_t lowerRGB = lowerSrc[srcIdx]; // Pixel in lower half
uint16_t result = 0;
if (upperRGB & redBit)
if (upperRGB & redBit) {
result |= pinMask[0];
if (upperRGB & greenBit)
}
if (upperRGB & greenBit) {
result |= pinMask[1];
if (upperRGB & blueBit)
}
if (upperRGB & blueBit) {
result |= pinMask[2];
if (lowerRGB & redBit)
}
if (lowerRGB & redBit) {
result |= pinMask[3];
if (lowerRGB & greenBit)
}
if (lowerRGB & greenBit) {
result |= pinMask[4];
if (lowerRGB & blueBit)
}
if (lowerRGB & blueBit) {
result |= pinMask[5];
}
// Main difference here vs byte converter is each chain
// ORs new bits into place (vs single-pass overwrite).
// #if defined(_PM_portToggleRegister)
@ -1319,18 +1330,24 @@ void _PM_convert_565_long(Protomatter_core *core, uint16_t *source,
uint16_t upperRGB = upperSrc[srcIdx]; // Pixel in upper half
uint16_t lowerRGB = lowerSrc[srcIdx]; // Pixel in lower half
uint32_t result = 0;
if (upperRGB & redBit)
if (upperRGB & redBit) {
result |= pinMask[0];
if (upperRGB & greenBit)
}
if (upperRGB & greenBit) {
result |= pinMask[1];
if (upperRGB & blueBit)
}
if (upperRGB & blueBit) {
result |= pinMask[2];
if (lowerRGB & redBit)
}
if (lowerRGB & redBit) {
result |= pinMask[3];
if (lowerRGB & greenBit)
}
if (lowerRGB & greenBit) {
result |= pinMask[4];
if (lowerRGB & blueBit)
}
if (lowerRGB & blueBit) {
result |= pinMask[5];
}
// Main difference here vs byte converter is each chain
// ORs new bits into place (vs single-pass overwrite).
// #if defined(_PM_portToggleRegister)

View file

@ -255,6 +255,16 @@ extern uint32_t _PM_timerGetCount(Protomatter_core *core);
*/
extern void _PM_swapbuffer_maybe(Protomatter_core *core);
/*!
@brief Adjust duty cycle of HUB75 clock signal. This is not supported on
all architectures.
@param d Duty setting, 0 minimum. Increasing values generate higher clock
duty cycles at the same frequency. Arbitrary granular units, max
varies by architecture and CPU speed, if supported at all.
e.g. SAMD51 @ 120 MHz supports 0 (~50% duty) through 2 (~75%).
*/
extern void _PM_setDuty(uint8_t d);
#if defined(ARDUINO) || defined(CIRCUITPY)
/*!