Yes a piece of dimming film is fine for prototyping but for production I will have a configuration parameter to set the display brightness and need the display to support the full range of brightness so my products will be usable in everything from low light to full sun ambient lighting conditions.
After several debug iterations Brave AI tracked the issue down to the ArduinoCore-zephyr repo ArduinoLEDMatrix set() function - the set function there discards the RGB values and was outputting only binary values to the output matrix buffer. I opened an issue there ArduinoLEDMatrix set() function discards brightness parameters · Issue #690 · arduino/ArduinoCore-zephyr · GitHub and let them know the fix, which applies to both the text() and [begin/end]Text() methods and thereby will support anti-aliased fonts as well as dimming.
I fixed this on my Uno Q by doing the following:
sudo nano ~/.arduino15/packages/arduino/hardware/zephyr/1.0.0/libraries/Arduino_LED_Matrix/src/Arduino_LED_Matrix.h
See change after // DDG: comment:
virtual void set(int x, int y, uint8_t r, uint8_t g, uint8_t b) {
if (y >= canvasHeight || x >= canvasWidth || y < 0 || x < 0) {
return;
}
// the r parameter is (mis)used to set the character to draw with
//_canvasBuffer[y][x] = (r | g | b) > 0 ? 1 : 0;
// DDG: Set grayscale value instead of binary
_canvasBuffer[y][x] = (r + g + b) / 3;
}
Then the 2nd issue was, as Brave AI summarized,
the function that actually pushes pixels to hardware is bit-packing your values, treating each byte as a single bit.
Where the data gets destroyed
Look at the call chain from endDraw() / endText():
endDraw()
→ renderBitmap(_canvasBuffer, 8, 13)
→ loadPixels(&buf[0][0], 104)
→ loadPixelsToBuffer(arr, 104, _frameHolder) ← KILLER
→ loadFrame(_frameHolder)
→ matrixWrite(frame) // 4×uint32_t, 1 bit per LED
The culprit is loadPixelsToBuffer:
static void loadPixelsToBuffer(uint8_t *arr, size_t size, uint32_t *dst) {
uint32_t partialBuffer = 0;
while (pixelIndex / 32 < size / 32) {
partialBuffer |= *frameP++; // ORs the FULL byte into the buffer
partialBuffer = partialBuffer << 1; // shifts left by 1
pixelIndex++;
}
...
}
This is designed for binary values (0 or 1). Each byte is expected to be 0 or 1, and the OR + shift packs them one bit at a time into a uint32_t. When you write 128 (0x80), it ORs bit 7 into the accumulator, then the shift smears it across adjacent bits — hence your incorrect pixels.
The fix: bypass the bit-packing path
The library already has a grayscale path that skips all of this:
void draw(uint8_t *buf) {
matrixGrayscaleWrite(buf); // sends full 8-bit values directly to hardware
}
You need to patch endDraw() and endText() to use matrixGrayscaleWrite instead of renderBitmap:
void endText(int scrollDirection = NO_SCROLL) {
ArduinoGraphics::endText(scrollDirection);
// renderBitmap(_canvasBuffer, canvasHeight, canvasWidth); // ← REMOVE
matrixGrayscaleWrite(&_canvasBuffer[0][0]); // ← USE THIS
}
void endDraw() {
ArduinoGraphics::endDraw();
if (!captureAnimation) {
// renderBitmap(_canvasBuffer, canvasHeight, canvasWidth); // ← REMOVE
matrixGrayscaleWrite(&_canvasBuffer[0][0]); // ← USE THIS
} else {
// ... animation capture path unchanged ...
}
}
--
These changes are in the same file as the 1st issue so I made these 2 changes and now all is working perfectly and the scrolling text is now as bright or as dim as I would like. Example sketch code:
#include <Arduino_RouterBridge.h>
#include <ArduinoGraphics.h>
#include <Arduino_LED_Matrix.h>
ArduinoLEDMatrix matrix;
int i = 0;
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
matrix.begin();
matrix.setGrayscaleBits(8);
matrix.stroke(63, 63, 63); // 25% brightness - much nicer!
matrix.textFont(Font_4x6);
matrix.textScrollSpeed(100);
matrix.clear();
//Serial.begin(115200);
}
void loop() {
digitalWrite(LED_BUILTIN, (i++ & 1) ? HIGH : LOW);
if(i > 1998)
i = 0;
matrix.beginDraw();
matrix.clear();
matrix.text(String(i/2), 0, 1);
matrix.endDraw();
delay(500);
}