A two-knob DMX512 controller for bi-colour LED panels, built from an Arduino Uno or Nano and a MAX485 transceiver.
One knob sets brightness. The other sets colour temperature. That is the whole interface — the controls the panel would have had, if it had shipped with any.
Deutsche Version: README.de.md
Plenty of cheap LED panels are sold as DMX-only fixtures with no local controls at all. They are meant to hang in a rig and take orders from a lighting desk. If you have one on a stand in a room, and you just want it a bit warmer and a bit dimmer, you are stuck: either you buy a DMX desk to turn two knobs, or you use the panel at whatever setting it powers up in.
This is the two knobs. About €15 of parts, and the panel behaves like a normal light again.
- Two-knob control — brightness and colour temperature, no menus.
- Perceptual dimming curve — a gamma table maps knob travel to how bright the light actually looks, not to raw LED current. Without this, the bottom third of the knob does almost nothing and the top third barely changes.
- Filtered inputs — oversampling, an exponential moving average, and a deadband, so the output does not shimmer from ADC noise. This matters most at low intensity, where one DMX step is a large relative change.
- Non-blocking loop — the knobs are sampled at 100 Hz and respond immediately.
- Two fixture layouts — intensity + CCT, or direct warm/cool emitter
control. One constant in
config.h. - Everything configurable in one file — pins, DMX start address, filter
strength, knob direction.
config.his the only file you should need to touch.
Arduino Uno or Nano, a MAX485 breakout, two 10 kΩ linear pots, a male XLR-3 connector, and a 120 Ω termination resistor.
Full bill of materials, the wiring diagram, and the XLR pinout are in docs/wiring.md.
- Wire it up per docs/wiring.md.
- Install the DmxSimple library — in the Arduino IDE, Tools → Manage Libraries → "DmxSimple".
- Open
firmware/DmxBicolorController/DmxBicolorController.ino. - In
config.h, setDMX_START_ADDRESSto match your fixture's address. - Flash, and turn the knobs.
With arduino-cli instead:
arduino-cli lib install DmxSimple
arduino-cli compile --fqbn arduino:avr:uno firmware/DmxBicolorController
arduino-cli upload --fqbn arduino:avr:uno -p /dev/ttyUSB0 \
firmware/DmxBicolorControllerEverything lives in
firmware/DmxBicolorController/config.h.
The settings you are most likely to change:
| Setting | Default | What it does |
|---|---|---|
DMX_START_ADDRESS |
1 |
The address dialled in on the fixture. Two consecutive channels are used. |
OUTPUT_MODE |
MODE_INTENSITY_CCT |
How the fixture reads its two channels. See below. |
RS485_USE_DIRECTION_PIN |
true |
Set false if your transceiver has DE/RE jumpered to VCC. |
INVERT_INTENSITY / INVERT_CCT |
false |
Flip a knob that runs backwards, without resoldering. |
USE_GAMMA_CURVE |
true |
Turn off only if the fixture applies its own dimming curve. |
EMA_SHIFT |
2 |
Filter strength. Raise for dirty pots, at the cost of responsiveness. |
MODE_INTENSITY_CCT — channel N is master intensity, N+1 is colour
temperature. The fixture does its own mixing. This is the common layout on
modern panels; try it first.
MODE_WARM_COOL — channel N drives the warm emitters, N+1 the cool
ones, and the firmware crossfades between them so their sum always equals
the requested intensity.
If you pick the wrong one the symptom is unmistakable: the colour knob changes brightness instead of colour, or vice versa. Change the constant and reflash.
gamma_table.h is generated, not hand-written. To change the curve, edit
GAMMA in the generator and run it from the repository root:
python3 tools/generate_gamma_table.py >firmware/DmxBicolorController/gamma_table.h2.2 is the default and matches the sRGB/video convention. Lower (1.8) feels snappier at the bottom of the range; higher (2.6) gives finer control down low, which is useful for close-up work.
Nothing happens at all. Swap XLR pins 2 and 3. Reversed data polarity is
the most common DMX wiring fault and it fails silently — no damage, just a
dead line. After that, check that the fixture's DMX address matches
DMX_START_ADDRESS.
Still nothing, and your MAX485 board has a solder jumper near DE/RE. The
jumper may already tie them to VCC, in which case D2 does nothing. Set
RS485_USE_DIRECTION_PIN to false.
The colour knob changes brightness. Wrong OUTPUT_MODE. See above.
A knob works backwards. Set the matching INVERT_* constant to true.
Visible flicker. In order of likelihood: missing 120 Ω termination on a
long cable; a dirty or worn pot (raise EMA_SHIFT to 3 or 4); DEBUG_SERIAL
left enabled, which competes with the DMX timer interrupt for CPU time.
analogWrite() on pin 3 or 11 stopped working. Expected. DmxSimple
bit-bangs the DMX signal from Timer2, which those pins share. tone() is
unavailable for the same reason.
This started as a working but rough sketch. The substantive changes, for anyone building something similar:
The RS-485 direction pin was being toggled around each write. This is
the right instinct for a request/response protocol like Modbus and exactly
wrong for DMX. DmxSimple transmits continuously from a timer interrupt, so
dropping DE low chops holes in the middle of frames the interrupt is still
sending. DE now goes high once in setup() and stays there. This was the
one bug with real teeth. There is a longer explanation in
docs/wiring.md.
delay() in the main loop. It capped knob updates at roughly 16 per
second, for no benefit — DMX output is interrupt-driven and needs no help
from loop(). Replaced with a millis() check at 100 Hz.
No input filtering. Raw analogRead() jitters by a few LSB, which shows
up as visible shimmer at low intensity. Now oversampled, smoothed, and
deadbanded. The filter constants were chosen by simulating the noise
response rather than by guessing.
Linear dimming. Replaced with a gamma curve, which is the single largest improvement in how the thing feels to use.
A 16-bit integer overflow in warm * brightness / 255, where the
product reaches 65025 and int on AVR tops out at 32767. Worth fixing as
undefined behaviour, but in the interest of not overselling it: on avr-gcc
the wraparound and the subsequent truncation to uint8_t nearly cancel, so
the actual error is at most one DMX step, on 407 of 65536 input
combinations. Invisible on a light. Fixed on principle, not because it was
breaking anything.
Built for a specific panel and a specific problem. It is small, it is finished, and it does one thing. Issues and pull requests are welcome, particularly reports from different fixtures — the two output modes cover what I could test, and there are certainly panels that want something else.
MIT — see LICENSE.