Binary-coded modulation (BCM) produces LED brightness with weighted bitplanes instead of one comparator per channel. It is a natural fit for FPGAs driving many LEDs because the same scheduler can service an entire row, bank, or panel. This tutorial derives the timing, implements an 8-bit engine in SystemVerilog, and proves its most important property: an input value of N produces exactly N illuminated base-time slots per cycle.
BCM in one equation
An unsigned B-bit value is the sum of its set bits. Bit 0 is worth 1, bit 1 is worth 2, bit 2 is worth 4, and so on. BCM displays each bitplane for that same weight. The total cycle contains:
1 + 2 + 4 + … + 2B-1 = 2B − 1 base slots
For 8-bit BCM, the complete cycle is 255 base slots. A pixel value of 173 is binary 10101101, so it is on during planes worth 128, 32, 8, 4, and 1 slots. Those weights add back to 173.
The scheduler
The controller needs a plane index and a dwell counter. Plane 0 lasts one tick, plane 1 lasts two, and plane 7 lasts 128. When the dwell counter reaches the weight of the current plane, advance to the next plane.
module bcm_led #(
parameter int BITS = 8
) (
input logic clk,
input logic rst,
input logic [BITS-1:0] level,
output logic led_out,
output logic cycle_start
);
localparam int PW = $clog2(BITS);
logic [PW-1:0] plane;
logic [BITS-1:0] dwell;
logic [BITS-1:0] active_level;
logic [BITS-1:0] plane_ticks;
always_comb begin
plane_ticks = {{(BITS-1){1'b0}}, 1'b1} << plane;
led_out = active_level[plane];
cycle_start = (plane == 0) && (dwell == 0);
end
always_ff @(posedge clk) begin
if (rst) begin
plane <= '0;
dwell <= '0;
active_level <= '0;
end else if (dwell == plane_ticks - 1'b1) begin
dwell <= '0;
if (plane == BITS-1) begin
plane <= '0;
active_level <= level;
end else begin
plane <= plane + 1'b1;
end
end else begin
dwell <= dwell + 1'b1;
end
end
endmodule
The requested level is copied only at the end of a complete cycle. That prevents one cycle from containing lower planes from an old value and upper planes from a new one.
Choose the base tick
If the base-tick frequency is Fbase, the BCM cycle rate is Fbase divided by 255 for 8 bits. A 1 MHz base tick produces about 3.92 kHz. In a multiplexed display, divide again by the number of row groups and include shifting, latching, and blanking overhead.
Do not assume the FPGA clock itself can be the base tick. A 10 ns least-significant plane may be shorter than the external driver, level shifter, wiring, or LED output can settle. Generate a clock-enable pulse that establishes a practical minimum slot.
A self-checking BCM test
int on_slots;
task automatic check_level(input logic [7:0] requested);
level = requested;
// Wait for the new value to be captured, then for its cycle to begin.
@(posedge clk iff cycle_start);
@(posedge clk iff cycle_start);
on_slots = 0;
repeat (255) begin
@(posedge clk);
on_slots += led_out;
end
assert (on_slots == requested)
else $fatal("BCM %0d produced %0d on-slots",
requested, on_slots);
endtask
Run the task for boundary values 0, 1, 127, 128, 254, and 255, plus randomized values. The equality between the code and total illuminated slots is a compact specification of correct BCM behavior.
Plane order is a design choice
Least-significant-bit first is easy to understand, but it places the longest dark or light interval at the end. You can reorder planes, split the most-significant plane into several smaller visits, or interleave rows to distribute energy more evenly. The sum of dwell times must remain unchanged. Reordering helps camera behavior and peak-current distribution, but it cannot rescue a refresh budget that is fundamentally too small.
Scaling from one LED to a matrix
- Store one brightness word per color channel in a framebuffer.
- For the active plane, transmit the corresponding bit from every pixel in the row.
- Blank outputs, latch the shifted bits, select the row, and enable the plane for its weighted dwell.
- Swap framebuffers only when the full frame completes.
- Verify the refresh budget before increasing color depth.
BCM and conventional PWM deliver the same average duty cycle; they organize time differently. For the architectural tradeoff between external bitplanes and drivers that generate grayscale internally, read External PWM Bitframes vs. 16-Bit Grayscale.
Official references
- AMD Vivado Design Suite User Guide: Synthesis (UG901)
- Intel Quartus Prime Pro Edition User Guide: Design Recommendations
The complete 8-bit timeline

The diagram makes the main engineering constraint visible: the most-significant plane occupies roughly half of the cycle while the least-significant plane gets one base slot. The minimum usable slot therefore limits effective depth. If shifting, blanking, or output settling cannot fit around that slot, the framebuffer may say eight bits while the optics deliver fewer.
Download and prove the scheduler
The downloadable test drives 1, 64, 127, 128, 254, and 255. It counts exactly 255 samples for each active value and fails unless illuminated slots equal the requested unsigned code.
unzip fpga-led-controller-examples.zip
cd fpga-led-controller-examples
make bcm
Implementation checklist
- Define whether a base slot includes or excludes blanking and latch overhead.
- Capture a new level only at a full BCM-cycle boundary.
- Verify every plane duration independently as well as the total on-slot sum.
- Calculate refresh after multiplying by the row-group count.
- Measure the optical waveform when plane order matters to cameras.
FPGA LED controller tutorial series
This article is part of the FPGA LED Controller Tutorials learning path. Continue with PWM vs. BCM, the HUB75 timing calculator, or SystemVerilog verification.
Leave a Reply