Compare commits
69 commits
pb-matrixp
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f83bac7e42 | ||
|
|
9c45cfb0e4 | ||
|
|
3614319ecd | ||
|
|
dbd84325c7 | ||
|
|
c8ad602b45 | ||
|
|
1037d3f080 | ||
|
|
d55e3bf833 | ||
|
|
0bd9873153 | ||
|
|
2156ca0407 | ||
|
|
fb9903f97d | ||
|
|
282920f9ce | ||
|
|
267d78d6cf | ||
|
|
386371d8fd | ||
|
|
ca4f8764be | ||
| cffac25915 | |||
| 7155be1a75 | |||
| e4506e5a57 | |||
|
|
eadf2ee814 | ||
|
|
d87532b044 | ||
|
|
68943a8fcd | ||
|
|
5ad5f33a16 | ||
|
|
429d625ba7 | ||
|
|
98a2da6da4 | ||
|
|
b0e9d045f8 | ||
|
|
40d0971635 | ||
|
|
1faaec39b3 | ||
|
|
67ac7d48d4 | ||
|
|
a6dec45c38 | ||
|
|
4430d1ceb5 | ||
|
|
c9c1189e9d | ||
|
|
4a23a0bc7d | ||
|
|
9a76ee2f81 | ||
|
|
d15b59728d | ||
|
|
67cd55d033 | ||
|
|
df50a2761b | ||
|
|
93ea4fa10d | ||
|
|
ed2e701871 | ||
|
|
eca749b742 | ||
|
|
f1956d2506 | ||
|
|
2c070c8640 | ||
|
|
b4788466de | ||
|
|
7bd6440c14 | ||
|
|
5f0b40ed59 | ||
|
|
9766126f13 | ||
|
|
afce1d5149 | ||
|
|
3be437876b | ||
|
|
bab07dfedb | ||
|
|
d18ce6e085 | ||
|
|
2216ca662f | ||
| 1deae76007 | |||
| 94a18cc08a | |||
|
|
9b88c0fead | ||
| 13b6c05d5f | |||
| 10666a9c50 | |||
| ecab2fa75e | |||
| 8419c30127 | |||
| a2711624df | |||
| ce18b6d465 | |||
| 8dd189169c | |||
| fc1d948108 | |||
| 794e92179f | |||
| 7a98ebed61 | |||
| 20ca3476e5 | |||
| e920fa9483 | |||
| 40a989b06d | |||
| ac972f78cd | |||
| 40ad09d4e9 | |||
| cade4a82f2 | |||
|
|
6967bffafe |
24 changed files with 631 additions and 307 deletions
8
.github/workflows/githubci.yml
vendored
8
.github/workflows/githubci.yml
vendored
|
|
@ -7,16 +7,16 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arduino-platform: ["metro_m0", "metro_m4", "nrf52840", "esp32"]
|
||||
arduino-platform: ["metro_m0", "metro_m4", "metroesp32s2", "feather_esp32s3", "feather_rp2040", "nrf52840", "esp32"]
|
||||
|
||||
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
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -115,3 +115,18 @@ compile-time).
|
|||
Most macros and functions begin with the prefix **\_PM\_** in order to
|
||||
avoid naming collisions with other code (exception being static functions,
|
||||
which can't be seen outside their source file).
|
||||
|
||||
# Pull Requests
|
||||
|
||||
If you encounter artifacts (noise, sparkles, dropouts and other issues) and
|
||||
it seems to resolve by adjusting the NOP counts, please do not submit this
|
||||
as a PR claiming a fix. Quite often what improves stability for one matrix
|
||||
type can make things worse for other types. Instead, open an issue and
|
||||
describe the hardware (both microcontroller and RGB matrix) and what worked
|
||||
for you. A general solution working across all matrix types typically
|
||||
involves monitoring the signals on a logic analyzer and aiming for a 50%
|
||||
duty cycle on the CLK signal, 20 MHz or less, and then testing across a
|
||||
wide variety of different matrix types to confirm; trial and error on just
|
||||
a single matrix type is problematic. Maintainers: this goes for you too.
|
||||
Don't merge a "fix" unless you've checked it out on a 'scope and on tested
|
||||
across a broad range of matrices.
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ uint8_t oePin = 16;
|
|||
#define NEXT_BUTTON 3
|
||||
#elif defined(ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3)
|
||||
uint8_t rgbPins[] = {42, 41, 40, 38, 39, 37};
|
||||
uint8_t addrPins[] = {35, 36, 48, 45, 21}; // 16/32/64 pixels tall
|
||||
uint8_t addrPins[] = {45, 36, 48, 35, 21}; // 16/32/64 pixels tall
|
||||
uint8_t clockPin = 2;
|
||||
uint8_t latchPin = 47;
|
||||
uint8_t oePin = 14;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ supported boards.
|
|||
uint8_t oePin = 16;
|
||||
#elif defined(ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3) // MatrixPortal ESP32-S3
|
||||
uint8_t rgbPins[] = {42, 41, 40, 38, 39, 37};
|
||||
uint8_t addrPins[] = {35, 36, 48, 45, 21};
|
||||
uint8_t addrPins[] = {45, 36, 48, 35, 21};
|
||||
uint8_t clockPin = 2;
|
||||
uint8_t latchPin = 47;
|
||||
uint8_t oePin = 14;
|
||||
|
|
@ -121,17 +121,16 @@ Adafruit_Protomatter matrix(
|
|||
|
||||
// Sundry globals used for animation ---------------------------------------
|
||||
|
||||
int16_t textX = matrix.width(), // Current text position (X)
|
||||
textY, // Current text position (Y)
|
||||
textMin, // Text pos. (X) when scrolled off left edge
|
||||
hue = 0;
|
||||
char str[50]; // Buffer to hold scrolling message text
|
||||
int8_t ball[3][4] = {
|
||||
{ 3, 0, 1, 1 }, // Initial X,Y pos+velocity of 3 bouncy balls
|
||||
int16_t textX; // Current text position (X)
|
||||
int16_t textY; // Current text position (Y)
|
||||
int16_t textMin; // Text pos. (X) when scrolled off left edge
|
||||
char str[64]; // Buffer to hold scrolling message text
|
||||
int16_t ball[3][4] = {
|
||||
{ 3, 0, 1, 1 }, // Initial X,Y pos+velocity of 3 bouncy balls
|
||||
{ 17, 15, 1, -1 },
|
||||
{ 27, 4, -1, 1 }
|
||||
};
|
||||
uint16_t ballcolor[3]; // Colors for bouncy balls (init in setup())
|
||||
uint16_t ballcolor[3]; // Colors for bouncy balls (init in setup())
|
||||
|
||||
// SETUP - RUNS ONCE AT PROGRAM START --------------------------------------
|
||||
|
||||
|
|
@ -160,6 +159,7 @@ void setup(void) {
|
|||
uint16_t w, h;
|
||||
matrix.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); // How big is it?
|
||||
textMin = -w; // All text is off left edge when it reaches this point
|
||||
textX = matrix.width(); // Start off right edge
|
||||
textY = matrix.height() / 2 - (y1 + h / 2); // Center text vertically
|
||||
// Note: when making scrolling text like this, the setTextWrap(false)
|
||||
// call is REQUIRED (to allow text to go off the edge of the matrix),
|
||||
|
|
@ -169,7 +169,7 @@ void setup(void) {
|
|||
// Set up the colors of the bouncy balls.
|
||||
ballcolor[0] = matrix.color565(0, 20, 0); // Dark green
|
||||
ballcolor[1] = matrix.color565(0, 0, 20); // Dark blue
|
||||
ballcolor[2] = matrix.color565(20, 0, 0); // ark red
|
||||
ballcolor[2] = matrix.color565(20, 0, 0); // Dark red
|
||||
}
|
||||
|
||||
// LOOP - RUNS REPEATEDLY AFTER SETUP --------------------------------------
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ uint8_t latchPin = 15;
|
|||
uint8_t oePin = 16;
|
||||
#else // MatrixPortal ESP32-S3
|
||||
uint8_t rgbPins[] = {42, 41, 40, 38, 39, 37};
|
||||
uint8_t addrPins[] = {35, 36, 48, 45, 21};
|
||||
uint8_t addrPins[] = {45, 36, 48, 35, 21};
|
||||
uint8_t clockPin = 2;
|
||||
uint8_t latchPin = 47;
|
||||
uint8_t oePin = 14;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ supported boards. Notes have been moved to the bottom of the code.
|
|||
uint8_t oePin = 16;
|
||||
#elif defined(ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3) // MatrixPortal ESP32-S3
|
||||
uint8_t rgbPins[] = {42, 41, 40, 38, 39, 37};
|
||||
uint8_t addrPins[] = {35, 36, 48, 45, 21};
|
||||
uint8_t addrPins[] = {45, 36, 48, 35, 21};
|
||||
uint8_t clockPin = 2;
|
||||
uint8_t latchPin = 47;
|
||||
uint8_t oePin = 14;
|
||||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ supported boards.
|
|||
uint8_t oePin = 16;
|
||||
#elif defined(ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3) // MatrixPortal ESP32-S3
|
||||
uint8_t rgbPins[] = {42, 41, 40, 38, 39, 37};
|
||||
uint8_t addrPins[] = {35, 36, 48, 45, 21};
|
||||
uint8_t addrPins[] = {45, 36, 48, 35, 21};
|
||||
uint8_t clockPin = 2;
|
||||
uint8_t latchPin = 47;
|
||||
uint8_t oePin = 14;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
name=Adafruit Protomatter
|
||||
version=1.5.5
|
||||
version=1.7.0
|
||||
author=Adafruit
|
||||
maintainer=Adafruit <info@adafruit.com>
|
||||
sentence=A library for Adafruit RGB LED matrices.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
#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))
|
||||
#define _PM_portBitMask(pin) (1U << ((pin) & 31))
|
||||
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
#define _PM_byteOffset(pin) ((pin & 31) / 8)
|
||||
|
|
@ -42,6 +42,7 @@
|
|||
// 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.
|
||||
|
|
@ -49,8 +50,6 @@ IRAM_ATTR inline uint32_t _PM_timerGetCount(Protomatter_core *core) {
|
|||
return (uint32_t)timerRead((hw_timer_t *)core->timer);
|
||||
}
|
||||
|
||||
#if defined(ARDUINO) // COMPILING FOR ARDUINO ------------------------------
|
||||
|
||||
#elif defined(CIRCUITPY) // COMPILING FOR CIRCUITPYTHON --------------------
|
||||
|
||||
#endif // END CIRCUITPYTHON ------------------------------------------------
|
||||
|
|
|
|||
57
src/arch/esp32-c6.h
Normal file
57
src/arch/esp32-c6.h
Normal 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
|
||||
|
|
@ -17,7 +17,12 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#if defined(ESP32) // *All* ESP32 variants (OG, S2, S3, etc.)
|
||||
#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
|
||||
// for some ESP32 variants. Previously they were all one file, but complex
|
||||
|
|
@ -25,18 +30,23 @@
|
|||
// 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
|
||||
// is allowed, set up when calling begin()...so it's just a global here:
|
||||
void *_PM_protoPtr = NULL;
|
||||
Protomatter_core *_PM_protoPtr;
|
||||
|
||||
#define _PM_timerFreq 40000000 // 40 MHz (1:2 prescale)
|
||||
|
||||
#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
|
||||
|
|
@ -45,16 +55,7 @@ void *_PM_protoPtr = NULL;
|
|||
// 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
|
||||
|
|
@ -68,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.
|
||||
|
|
@ -84,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 --------------------
|
||||
|
|
@ -103,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)
|
||||
|
|
@ -115,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.
|
||||
IRAM_ATTR bool _PM_esp32timerCallback(void *unused) {
|
||||
#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;
|
||||
|
|
@ -125,13 +161,15 @@ IRAM_ATTR bool _PM_esp32timerCallback(void *unused) {
|
|||
// Set timer period, initialize count value to zero, enable timer.
|
||||
#if (ESP_IDF_VERSION_MAJOR == 5)
|
||||
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);
|
||||
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) {
|
||||
|
|
@ -147,30 +185,46 @@ 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);
|
||||
}
|
||||
|
||||
#if !defined(CONFIG_IDF_TARGET_ESP32S3)
|
||||
IRAM_ATTR uint32_t _PM_timerGetCount(Protomatter_core *core) {
|
||||
timer_index_t *timer = (timer_index_t *)core->timer;
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S3
|
||||
timer->hw->hw_timer[timer->idx].update.tn_update = 1;
|
||||
return timer->hw->hw_timer[timer->idx].lo.tn_lo;
|
||||
#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->hw->hw_timer[timer->idx].update.tx_update = 1;
|
||||
return timer->hw->hw_timer[timer->idx].lo.tx_lo;
|
||||
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
|
||||
|
||||
// Initialize, but do not start, timer. This function contains timer setup
|
||||
// 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) {
|
||||
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,
|
||||
|
|
@ -185,6 +239,7 @@ 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 ------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -139,13 +139,6 @@ IRAM_ATTR static void blast_byte(Protomatter_core *core, uint8_t *data) {
|
|||
IRAM_ATTR static void blast_word(Protomatter_core *core, uint16_t *data) {}
|
||||
IRAM_ATTR static void blast_long(Protomatter_core *core, uint32_t *data) {}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
#if defined(ARDUINO) // COMPILING FOR ARDUINO ------------------------------
|
||||
|
||||
void _PM_timerInit(Protomatter_core *core) {
|
||||
|
|
@ -174,6 +167,13 @@ void _PM_timerInit(Protomatter_core *core) {
|
|||
_PM_esp32commonTimerInit(core); // In esp32-common.h
|
||||
}
|
||||
|
||||
// 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 --------------------
|
||||
|
||||
void _PM_timerInit(Protomatter_core *core) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,14 @@
|
|||
|
||||
#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"
|
||||
#endif
|
||||
|
||||
// Use DMA-capable RAM (not PSRAM) for framebuffer:
|
||||
#define _PM_allocate(x) heap_caps_malloc(x, MALLOC_CAP_DMA | MALLOC_CAP_8BIT)
|
||||
#define _PM_free(x) heap_caps_free(x)
|
||||
|
|
@ -57,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 -25 here is an
|
||||
// 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.
|
||||
return dmaSetupTime + core->chainBits * 2 - 25;
|
||||
// need to dial back this number a bit or remove it.
|
||||
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
|
||||
|
|
@ -74,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 + 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>
|
||||
|
|
@ -112,6 +122,17 @@ static uint32_t _PM_directBitMask(Protomatter_core *core, int pin) {
|
|||
static dma_descriptor_t desc;
|
||||
static gdma_channel_handle_t dma_chan;
|
||||
|
||||
// If using custom "blast" function(s), all three must be declared.
|
||||
// Unused ones can be empty, that's fine, just need to exist.
|
||||
IRAM_ATTR static void blast_word(Protomatter_core *core, uint16_t *data) {}
|
||||
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);
|
||||
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[pin], PIN_FUNC_GPIO);
|
||||
gpio_set_drive_capability((gpio_num_t)pin, GPIO_DRIVE_STRENGTH);
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
@ -139,24 +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);
|
||||
#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.
|
||||
}
|
||||
|
||||
// If using custom "blast" function(s), all three must be declared.
|
||||
// Unused ones can be empty, that's fine, just need to exist.
|
||||
IRAM_ATTR static void blast_word(Protomatter_core *core, uint16_t *data) {}
|
||||
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);
|
||||
}
|
||||
|
||||
#if defined(ARDUINO) // COMPILING FOR ARDUINO ------------------------------
|
||||
|
||||
void _PM_timerInit(Protomatter_core *core) {
|
||||
static 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:
|
||||
|
|
@ -168,11 +188,17 @@ void _PM_timerInit(Protomatter_core *core) {
|
|||
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.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 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)
|
||||
|
|
@ -208,10 +234,10 @@ 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
|
||||
|
|
@ -226,123 +252,26 @@ void _PM_timerInit(Protomatter_core *core) {
|
|||
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);
|
||||
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 --------------------
|
||||
|
||||
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:
|
||||
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);
|
||||
#if defined(CIRCUITPY)
|
||||
if (dma_chan == NULL) {
|
||||
#endif
|
||||
gdma_channel_alloc_config_t dma_chan_config = {
|
||||
.sibling_chan = NULL,
|
||||
.direction = GDMA_CHANNEL_DIRECTION_TX,
|
||||
.flags = {.reserve_sibling = 0}};
|
||||
gdma_new_channel(&dma_chan_config, &dma_chan);
|
||||
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);
|
||||
#if defined(CIRCUITPY)
|
||||
}
|
||||
|
||||
// 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}};
|
||||
gdma_new_channel(&dma_chan_config, &dma_chan);
|
||||
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);
|
||||
#endif
|
||||
gdma_start(dma_chan, (intptr_t)&desc);
|
||||
|
||||
// Enable TRANS_DONE interrupt. Note that we do NOT require nor install
|
||||
|
|
@ -353,6 +282,4 @@ void _PM_timerInit(Protomatter_core *core) {
|
|||
_PM_esp32commonTimerInit(core); // In esp32-common.h
|
||||
}
|
||||
|
||||
#endif // END CIRCUITPYTHON ------------------------------------------------
|
||||
|
||||
#endif // END ESP32S3
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
#define _PM_portClearRegister(pin) \
|
||||
(volatile uint32_t *)((pin < 32) ? &GPIO.out_w1tc : &GPIO.out1_w1tc.val)
|
||||
|
||||
#define _PM_portBitMask(pin) (1U << ((pin)&31))
|
||||
#define _PM_portBitMask(pin) (1U << ((pin) & 31))
|
||||
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
#define _PM_byteOffset(pin) ((pin & 31) / 8)
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ volatile uint32_t *_PM_portClearRegister(uint32_t pin) {
|
|||
#define _PM_pinInput(pin) nrf_gpio_cfg_input(pin)
|
||||
#define _PM_pinHigh(pin) nrf_gpio_pin_set(pin)
|
||||
#define _PM_pinLow(pin) nrf_gpio_pin_clear(pin)
|
||||
#define _PM_portBitMask(pin) (1u << ((pin)&31))
|
||||
#define _PM_portBitMask(pin) (1u << ((pin) & 31))
|
||||
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
#define _PM_byteOffset(pin) ((pin & 31) / 8)
|
||||
|
|
@ -142,19 +142,19 @@ void _PM_timerInit(Protomatter_core *core) {
|
|||
IRQn_Type IRQn; // Interrupt number
|
||||
} timer[] = {
|
||||
#if defined(NRF_TIMER0)
|
||||
{NRF_TIMER0, TIMER0_IRQn},
|
||||
{NRF_TIMER0, TIMER0_IRQn},
|
||||
#endif
|
||||
#if defined(NRF_TIMER1)
|
||||
{NRF_TIMER1, TIMER1_IRQn},
|
||||
{NRF_TIMER1, TIMER1_IRQn},
|
||||
#endif
|
||||
#if defined(NRF_TIMER2)
|
||||
{NRF_TIMER2, TIMER2_IRQn},
|
||||
{NRF_TIMER2, TIMER2_IRQn},
|
||||
#endif
|
||||
#if defined(NRF_TIMER3)
|
||||
{NRF_TIMER3, TIMER3_IRQn},
|
||||
{NRF_TIMER3, TIMER3_IRQn},
|
||||
#endif
|
||||
#if defined(NRF_TIMER4)
|
||||
{NRF_TIMER4, TIMER4_IRQn},
|
||||
{NRF_TIMER4, TIMER4_IRQn},
|
||||
#endif
|
||||
};
|
||||
#define NUM_TIMERS (sizeof timer / sizeof timer[0])
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -104,9 +105,14 @@ void _PM_timerInit(Protomatter_core *core) {
|
|||
|
||||
#elif defined(CIRCUITPY) // COMPILING FOR CIRCUITPYTHON --------------------
|
||||
|
||||
#if !defined(F_CPU) // Not sure if CircuitPython build defines this
|
||||
#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;");
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ void _PM_IRQ_HANDLER(void) {
|
|||
#define _PM_pinInput(pin) gpio_set_pin_direction(pin, GPIO_DIRECTION_IN)
|
||||
#define _PM_pinHigh(pin) gpio_set_pin_level(pin, 1)
|
||||
#define _PM_pinLow(pin) gpio_set_pin_level(pin, 0)
|
||||
#define _PM_portBitMask(pin) (1u << ((pin)&31))
|
||||
#define _PM_portBitMask(pin) (1u << ((pin) & 31))
|
||||
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
#define _PM_byteOffset(pin) ((pin & 31) / 8)
|
||||
|
|
|
|||
|
|
@ -51,19 +51,19 @@ void _PM_timerInit(Protomatter_core *core) {
|
|||
uint8_t GCM_ID; // GCLK selection ID
|
||||
} timer[] = {
|
||||
#if defined(TC0)
|
||||
{TC0, TC0_IRQn, GCM_TCC0_TCC1},
|
||||
{TC0, TC0_IRQn, GCM_TCC0_TCC1},
|
||||
#endif
|
||||
#if defined(TC1)
|
||||
{TC1, TC1_IRQn, GCM_TCC0_TCC1},
|
||||
{TC1, TC1_IRQn, GCM_TCC0_TCC1},
|
||||
#endif
|
||||
#if defined(TC2)
|
||||
{TC2, TC2_IRQn, GCM_TCC2_TC3},
|
||||
{TC2, TC2_IRQn, GCM_TCC2_TC3},
|
||||
#endif
|
||||
#if defined(TC3)
|
||||
{TC3, TC3_IRQn, GCM_TCC2_TC3},
|
||||
{TC3, TC3_IRQn, GCM_TCC2_TC3},
|
||||
#endif
|
||||
#if defined(TC4)
|
||||
{TC4, TC4_IRQn, GCM_TC4_TC5},
|
||||
{TC4, TC4_IRQn, GCM_TC4_TC5},
|
||||
#endif
|
||||
};
|
||||
#define NUM_TIMERS (sizeof timer / sizeof timer[0])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -63,43 +78,43 @@ void _PM_timerInit(Protomatter_core *core) {
|
|||
uint8_t GCLK_ID; // Peripheral channel # for clock source
|
||||
} timer[] = {
|
||||
#if defined(TC0)
|
||||
{TC0, TC0_IRQn, TC0_GCLK_ID},
|
||||
{TC0, TC0_IRQn, TC0_GCLK_ID},
|
||||
#endif
|
||||
#if defined(TC1)
|
||||
{TC1, TC1_IRQn, TC1_GCLK_ID},
|
||||
{TC1, TC1_IRQn, TC1_GCLK_ID},
|
||||
#endif
|
||||
#if defined(TC2)
|
||||
{TC2, TC2_IRQn, TC2_GCLK_ID},
|
||||
{TC2, TC2_IRQn, TC2_GCLK_ID},
|
||||
#endif
|
||||
#if defined(TC3)
|
||||
{TC3, TC3_IRQn, TC3_GCLK_ID},
|
||||
{TC3, TC3_IRQn, TC3_GCLK_ID},
|
||||
#endif
|
||||
#if defined(TC4)
|
||||
{TC4, TC4_IRQn, TC4_GCLK_ID},
|
||||
{TC4, TC4_IRQn, TC4_GCLK_ID},
|
||||
#endif
|
||||
#if defined(TC5)
|
||||
{TC5, TC5_IRQn, TC5_GCLK_ID},
|
||||
{TC5, TC5_IRQn, TC5_GCLK_ID},
|
||||
#endif
|
||||
#if defined(TC6)
|
||||
{TC6, TC6_IRQn, TC6_GCLK_ID},
|
||||
{TC6, TC6_IRQn, TC6_GCLK_ID},
|
||||
#endif
|
||||
#if defined(TC7)
|
||||
{TC7, TC7_IRQn, TC7_GCLK_ID},
|
||||
{TC7, TC7_IRQn, TC7_GCLK_ID},
|
||||
#endif
|
||||
#if defined(TC8)
|
||||
{TC8, TC8_IRQn, TC8_GCLK_ID},
|
||||
{TC8, TC8_IRQn, TC8_GCLK_ID},
|
||||
#endif
|
||||
#if defined(TC9)
|
||||
{TC9, TC9_IRQn, TC9_GCLK_ID},
|
||||
{TC9, TC9_IRQn, TC9_GCLK_ID},
|
||||
#endif
|
||||
#if defined(TC10)
|
||||
{TC10, TC10_IRQn, TC10_GCLK_ID},
|
||||
{TC10, TC10_IRQn, TC10_GCLK_ID},
|
||||
#endif
|
||||
#if defined(TC11)
|
||||
{TC11, TC11_IRQn, TC11_GCLK_ID},
|
||||
{TC11, TC11_IRQn, TC11_GCLK_ID},
|
||||
#endif
|
||||
#if defined(TC12)
|
||||
{TC12, TC12_IRQn, TC12_GCLK_ID},
|
||||
{TC12, TC12_IRQn, TC12_GCLK_ID},
|
||||
#endif
|
||||
};
|
||||
#define NUM_TIMERS (sizeof timer / sizeof timer[0])
|
||||
|
|
@ -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,21 +225,204 @@ uint32_t _PM_timerStop(Protomatter_core *core) {
|
|||
return count;
|
||||
}
|
||||
|
||||
// See notes in core.c before the "blast" functions
|
||||
#if F_CPU >= 200000000
|
||||
#define _PM_clockHoldHigh asm("nop; nop; nop; nop; nop");
|
||||
#define _PM_clockHoldLow asm("nop; nop");
|
||||
#elif F_CPU >= 180000000
|
||||
#define _PM_clockHoldHigh asm("nop; nop; nop; nop");
|
||||
#define _PM_clockHoldLow asm("nop");
|
||||
#elif F_CPU >= 150000000
|
||||
#define _PM_clockHoldHigh asm("nop; nop; nop");
|
||||
#define _PM_clockHoldLow asm("nop");
|
||||
#else
|
||||
#define _PM_clockHoldHigh asm("nop; nop; nop");
|
||||
#define _PM_clockHoldLow asm("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
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
#include "timers.h"
|
||||
|
||||
#undef _PM_portBitMask
|
||||
#define _PM_portBitMask(pin) (1u << ((pin)&15))
|
||||
#define _PM_portBitMask(pin) (1u << ((pin) & 15))
|
||||
#define _PM_byteOffset(pin) ((pin & 15) / 8)
|
||||
#define _PM_wordOffset(pin) ((pin & 15) / 16)
|
||||
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
109
src/core.c
109
src/core.c
|
|
@ -95,12 +95,13 @@ 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,
|
||||
// but might be more or less elsewhere)
|
||||
// bitDepth is NOT constrained here, handle in calling function
|
||||
// (varies with implementation, e.g. GFX lib is max 6 bitplanes,
|
||||
// but might be more or less elsewhere)
|
||||
#if defined(_PM_bytesPerElement)
|
||||
#if _PM_bytesPerElement == 1
|
||||
if (rgbCount > 1)
|
||||
|
|
@ -726,7 +727,7 @@ IRAM_ATTR static void blast_byte(Protomatter_core *core, uint8_t *data) {
|
|||
_PM_PORT_TYPE rgbclock = core->rgbAndClockMask; // RGB + clock bit
|
||||
#endif
|
||||
_PM_PORT_TYPE clock = core->clockMask; // Clock bit
|
||||
uint8_t chunks = (core->chainBits + (_PM_chunkSize - 1)) / _PM_chunkSize;
|
||||
uint16_t chunks = (core->chainBits + (_PM_chunkSize - 1)) / _PM_chunkSize;
|
||||
|
||||
// PORT has already been initialized with RGB data + clock bits
|
||||
// all LOW, so we don't need to initialize that state here.
|
||||
|
|
@ -780,9 +781,9 @@ IRAM_ATTR static void blast_word(Protomatter_core *core, uint16_t *data) {
|
|||
volatile uint16_t *toggle =
|
||||
(volatile uint16_t *)core->toggleReg + core->portOffset;
|
||||
#else
|
||||
volatile uint16_t *set; // For RGB data set
|
||||
volatile _PM_PORT_TYPE *set_full; // For clock set
|
||||
volatile _PM_PORT_TYPE *clear_full; // For RGB data + clock clear
|
||||
volatile uint16_t *set; // For RGB data set
|
||||
volatile _PM_PORT_TYPE *set_full; // For clock set
|
||||
volatile _PM_PORT_TYPE *clear_full; // For RGB data + clock clear
|
||||
set = (volatile uint16_t *)core->setReg + core->portOffset;
|
||||
set_full = (volatile _PM_PORT_TYPE *)core->setReg;
|
||||
clear_full = (volatile _PM_PORT_TYPE *)core->clearReg;
|
||||
|
|
@ -829,7 +830,7 @@ IRAM_ATTR static void blast_long(Protomatter_core *core, uint32_t *data) {
|
|||
// Note in this case two copies exist of the PORT set register.
|
||||
// The optimizer will most likely simplify this; leaving as-is, not
|
||||
// wanting a special case of the PEW macro due to divergence risk.
|
||||
volatile uint32_t *set; // For RGB data set
|
||||
volatile uint32_t *set; // For RGB data set
|
||||
#if !defined(_PM_STRICT_32BIT_IO)
|
||||
volatile _PM_PORT_TYPE *set_full; // For clock set
|
||||
set_full = (volatile _PM_PORT_TYPE *)core->setReg;
|
||||
|
|
@ -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
|
||||
|
|
@ -956,7 +961,7 @@ __attribute__((noinline)) void _PM_convert_565_byte(Protomatter_core *core,
|
|||
dest += core->bufferSize * (1 - core->activeBuffer);
|
||||
}
|
||||
|
||||
//#if defined(_PM_portToggleRegister)
|
||||
// #if defined(_PM_portToggleRegister)
|
||||
#if defined(_PM_USE_TOGGLE_FORMAT)
|
||||
#if !defined(_PM_STRICT_32BIT_IO)
|
||||
// core->clockMask mask is already an 8-bit value
|
||||
|
|
@ -1016,7 +1021,7 @@ __attribute__((noinline)) void _PM_convert_565_byte(Protomatter_core *core,
|
|||
uint32_t greenBit = initialGreenBit;
|
||||
uint32_t blueBit = initialBlueBit;
|
||||
for (uint8_t plane = 0; plane < core->numPlanes; plane++) {
|
||||
//#if defined(_PM_portToggleRegister)
|
||||
// #if defined(_PM_portToggleRegister)
|
||||
#if defined(_PM_USE_TOGGLE_FORMAT)
|
||||
uint8_t prior = clockMask; // Set clock bit on 1st out
|
||||
#endif
|
||||
|
|
@ -1062,7 +1067,7 @@ __attribute__((noinline)) void _PM_convert_565_byte(Protomatter_core *core,
|
|||
if (lowerRGB & blueBit)
|
||||
result |= pinMask[5];
|
||||
// THIS is where toggle format (without toggle reg.) messes up
|
||||
//#if defined(_PM_portToggleRegister)
|
||||
// #if defined(_PM_portToggleRegister)
|
||||
#if defined(_PM_USE_TOGGLE_FORMAT)
|
||||
*d2++ = result ^ prior;
|
||||
prior = result | clockMask; // Set clock bit on next out
|
||||
|
|
@ -1070,7 +1075,7 @@ __attribute__((noinline)) void _PM_convert_565_byte(Protomatter_core *core,
|
|||
*d2++ = result;
|
||||
#endif
|
||||
} // end x
|
||||
} // end tile
|
||||
} // end tile
|
||||
|
||||
greenBit <<= 1;
|
||||
if (plane || (core->numPlanes < 6)) {
|
||||
|
|
@ -1084,7 +1089,7 @@ __attribute__((noinline)) void _PM_convert_565_byte(Protomatter_core *core,
|
|||
redBit = 0b0000100000000000;
|
||||
blueBit = 0b0000000000000001;
|
||||
}
|
||||
//#if defined(_PM_portToggleRegister)
|
||||
// #if defined(_PM_portToggleRegister)
|
||||
#if defined(_PM_USE_TOGGLE_FORMAT)
|
||||
// If using bit-toggle register, erase the toggle bit on the
|
||||
// first element of each bitplane & row pair. The matrix-driving
|
||||
|
|
@ -1096,8 +1101,8 @@ __attribute__((noinline)) void _PM_convert_565_byte(Protomatter_core *core,
|
|||
dest[-pad] &= ~clockMask; // Negative index is legal & intentional
|
||||
#endif
|
||||
dest += bitplaneSize; // Advance one scanline in dest buffer
|
||||
} // end plane
|
||||
} // end row
|
||||
} // end plane
|
||||
} // end row
|
||||
}
|
||||
|
||||
// Corresponding function for word output -- either 12 RGB bits (2 parallel
|
||||
|
|
@ -1135,7 +1140,7 @@ void _PM_convert_565_word(Protomatter_core *core, uint16_t *source,
|
|||
// register exists, "clear" really means the clock mask is set in all
|
||||
// but the first element on a scanline (per bitplane). If no toggle
|
||||
// register, can just zero everything out.
|
||||
//#if defined(_PM_portToggleRegister)
|
||||
// #if defined(_PM_portToggleRegister)
|
||||
#if defined(_PM_USE_TOGGLE_FORMAT)
|
||||
// No per-chain loop is required; one clock bit handles all chains
|
||||
uint32_t offset = 0; // Current position in the 'dest' buffer
|
||||
|
|
@ -1160,7 +1165,7 @@ void _PM_convert_565_word(Protomatter_core *core, uint16_t *source,
|
|||
uint32_t greenBit = initialGreenBit;
|
||||
uint32_t blueBit = initialBlueBit;
|
||||
for (uint8_t plane = 0; plane < core->numPlanes; plane++) {
|
||||
//#if defined(_PM_portToggleRegister)
|
||||
// #if defined(_PM_portToggleRegister)
|
||||
#if defined(_PM_USE_TOGGLE_FORMAT)
|
||||
// Since we're ORing in bits over an existing clock bit,
|
||||
// prior is 0 rather than clockMask as in the byte case.
|
||||
|
|
@ -1195,21 +1200,27 @@ 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)
|
||||
}
|
||||
// Main difference here vs byte converter is each chain
|
||||
// ORs new bits into place (vs single-pass overwrite).
|
||||
// #if defined(_PM_portToggleRegister)
|
||||
#if defined(_PM_USE_TOGGLE_FORMAT)
|
||||
*d2++ |= result ^ prior; // Bitwise OR
|
||||
prior = result;
|
||||
|
|
@ -1217,7 +1228,7 @@ void _PM_convert_565_word(Protomatter_core *core, uint16_t *source,
|
|||
*d2++ |= result; // Bitwise OR
|
||||
#endif
|
||||
} // end x
|
||||
} // end tile
|
||||
} // end tile
|
||||
greenBit <<= 1;
|
||||
if (plane || (core->numPlanes < 6)) {
|
||||
redBit <<= 1;
|
||||
|
|
@ -1227,9 +1238,9 @@ void _PM_convert_565_word(Protomatter_core *core, uint16_t *source,
|
|||
blueBit = 0b0000000000000001;
|
||||
}
|
||||
dest += bitplaneSize; // Advance one scanline in dest buffer
|
||||
} // end plane
|
||||
} // end row
|
||||
pinMask += 6; // Next chain's RGB pin masks
|
||||
} // end plane
|
||||
} // end row
|
||||
pinMask += 6; // Next chain's RGB pin masks
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1262,7 +1273,7 @@ void _PM_convert_565_long(Protomatter_core *core, uint16_t *source,
|
|||
initialBlueBit = 0b0000000000000001 << shiftLeft;
|
||||
}
|
||||
|
||||
//#if defined(_PM_portToggleRegister)
|
||||
// #if defined(_PM_portToggleRegister)
|
||||
#if defined(_PM_USE_TOGGLE_FORMAT)
|
||||
// No per-chain loop is required; one clock bit handles all chains
|
||||
uint32_t offset = 0; // Current position in the 'dest' buffer
|
||||
|
|
@ -1286,7 +1297,7 @@ void _PM_convert_565_long(Protomatter_core *core, uint16_t *source,
|
|||
uint32_t greenBit = initialGreenBit;
|
||||
uint32_t blueBit = initialBlueBit;
|
||||
for (uint8_t plane = 0; plane < core->numPlanes; plane++) {
|
||||
//#if defined(_PM_portToggleRegister)
|
||||
// #if defined(_PM_portToggleRegister)
|
||||
#if defined(_PM_USE_TOGGLE_FORMAT)
|
||||
uint32_t prior = 0;
|
||||
#endif
|
||||
|
|
@ -1319,21 +1330,27 @@ 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)
|
||||
}
|
||||
// Main difference here vs byte converter is each chain
|
||||
// ORs new bits into place (vs single-pass overwrite).
|
||||
// #if defined(_PM_portToggleRegister)
|
||||
#if defined(_PM_USE_TOGGLE_FORMAT)
|
||||
*d2++ |= result ^ prior; // Bitwise OR
|
||||
prior = result;
|
||||
|
|
@ -1341,7 +1358,7 @@ void _PM_convert_565_long(Protomatter_core *core, uint16_t *source,
|
|||
*d2++ |= result; // Bitwise OR
|
||||
#endif
|
||||
} // end x
|
||||
} // end tile
|
||||
} // end tile
|
||||
greenBit <<= 1;
|
||||
if (plane || (core->numPlanes < 6)) {
|
||||
redBit <<= 1;
|
||||
|
|
@ -1351,9 +1368,9 @@ void _PM_convert_565_long(Protomatter_core *core, uint16_t *source,
|
|||
blueBit = 0b0000000000000001;
|
||||
}
|
||||
dest += bitplaneSize; // Advance one scanline in dest buffer
|
||||
} // end plane
|
||||
} // end row
|
||||
pinMask += 6; // Next chain's RGB pin masks
|
||||
} // end plane
|
||||
} // end row
|
||||
pinMask += 6; // Next chain's RGB pin masks
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
10
src/core.h
10
src/core.h
|
|
@ -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)
|
||||
|
||||
/*!
|
||||
|
|
|
|||
Loading…
Reference in a new issue