While reverse engineering the Behringer P16-I I’ve found a nice thing: this devices has 32 individual LEDs at the front to display the audio-level and the clipping of a channel. But from the internal FPGA only three lines are connected to the front-leds. So I hooked up an oscilloscope and found out, that the signal looked very like an I2S connection. The brightness of each LED corresponded with an individual bit of the 16-bit stereo I2S signal.

As a single bit cannot transport information for a pulsewidth-modulation, it turned out, that the Behringer guys uses a PDM, a pulse-density-modulation, to control the brightness of the LEDs. So I’ve implemented my own LED-controller that multiplexes all 32 LED signals into an I2S stream with a 1.536 MHz bitclock, a 48kHz frame-sync and 16 bits for “left” and “right” resulting in 32 individual bits.

Each Sigma-Delta-Modulator expects a 8-bit signal as reference. The signals look like this, when feeding the block with a sine-wave between 0 and 255. The input-signal is the upper signal, the internal accumulator the signal in the middle and the signal at the bottom is the digital output of the modulator:

In plain C-code the modulator would look like this

C
int16_t input;
int16_t feedback = 0;
int16_t accumulator = 0;
bool output = false;

accumulator = accumulator + input - feedback;

if (accumulator > 255) {
	output = true;
	feedback = 255;
}else{
	output = false;
	feedback = 0;
}

Next to the clock signals, the following VHDL-block expects 16 individual 8-bit brightness-values that will be fed into the first-order sigma-delta modulator. The outputs of all SDMs are then stored as individual bits into the output-shift register to be sent as regular I2S-data:

VHDL
-- 32-channel LED-PDM-Controller
-- Christian Noeding, christian@noeding-online.de
-- https://chrisdevblog.com | https://github.com/xn--nding-jua
--
-- Released under GNU General Public License v3

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity led_pdm_controller is
	generic (
		bit_width : integer := 8	-- depth of the PWM/PDM
	);
	port (
		bclk			: in std_logic;
		fsync			: in std_logic;
		led1_in		: in std_logic_vector(bit_width - 1 downto 0);
		led2_in		: in std_logic_vector(bit_width - 1 downto 0);
		led3_in		: in std_logic_vector(bit_width - 1 downto 0);
		led4_in		: in std_logic_vector(bit_width - 1 downto 0);
		led5_in		: in std_logic_vector(bit_width - 1 downto 0);
		led6_in		: in std_logic_vector(bit_width - 1 downto 0);
		led7_in		: in std_logic_vector(bit_width - 1 downto 0);
		led8_in		: in std_logic_vector(bit_width - 1 downto 0);
		led9_in		: in std_logic_vector(bit_width - 1 downto 0);
		led10_in		: in std_logic_vector(bit_width - 1 downto 0);
		led11_in		: in std_logic_vector(bit_width - 1 downto 0);
		led12_in		: in std_logic_vector(bit_width - 1 downto 0);
		led13_in		: in std_logic_vector(bit_width - 1 downto 0);
		led14_in		: in std_logic_vector(bit_width - 1 downto 0);
		led15_in		: in std_logic_vector(bit_width - 1 downto 0);
		led16_in		: in std_logic_vector(bit_width - 1 downto 0);
		reset			: in std_logic;
		data_out		: out std_logic	-- I2S-like signal containing 16 bits with each a PDM
	);
end led_pdm_controller;

architecture rtl of led_pdm_controller is
	-- signals for input
	type led_array_t is array (0 to 15) of std_logic_vector(bit_width - 1 downto 0);
	signal led_input_array : led_array_t;

	-- signals for I2S output
	signal zfsync			: std_logic;
	signal channel_count	: integer range 0 to 15 := 0;
	signal led_green		: std_logic := '1';

	-- signals for sigma-delta-modulator (PDM)
	type accum_array_t is array (0 to 15) of signed(bit_width + 1 downto 0);
	signal accumulator : accum_array_t := (others => (others => '0'));
	signal feedback	 : accum_array_t := (others => (others => '0'));
	signal pdm_out     : std_logic_vector(15 downto 0);
begin
	led_input_array <= (led1_in, led2_in, led3_in, led4_in, led5_in, led6_in, led7_in, led8_in, led9_in, led10_in, led11_in, led12_in, led13_in, led14_in, led15_in, led16_in);

	process(bclk)
	begin
		if rising_edge(bclk) then
			if reset = '1' then
				-- reset internal signals
				accumulator <= (others => (others => '0'));
				feedback <= (others => (others => '0'));
				pdm_out <= (others => '0');
				led_green <= '1';
			else
				if (fsync = '1' and zfsync = '0') then
					-- first edge of bit-clock after rising LR-clock

					-- reset bitcounter
					channel_count <= 1; -- we start with one channel offset
					led_green <= '1';
					
					-- Calculate 16-channel First-Order Sigma-Delta Modulator (PDM) every 10.42us (96kHz) resulting in a 1kHz resolution for the LEDs
					-- =============================================
					-- C-Code for First-Order Sigma-Delta Modulator
					-- ---------------------------------------------
					--	accumulator = accumulator + input - feedback;
					--	
					--	if (accumulator >= 255) {
					--		output = 1;
					--		feedback = 255;
					--	}else{
					--		output = 0;
					--		feedback = 0;
					--	}
					-- =============================================
					
					for i in 0 to 15 loop
						accumulator(i) <= accumulator(i) + signed(resize(unsigned(led_input_array(i)), bit_width + 2)) - feedback(i);
					
						-- here accumulator(i) will be used from previous step, but should be fine for an LED
						if (accumulator(i) > to_signed(2**(bit_width) - 1, bit_width + 1)) then
							pdm_out(i) <= '1';
							feedback(i) <= to_signed(2**(bit_width) - 1, bit_width + 2);
						else
							pdm_out(i) <= '0';
							feedback(i) <= to_signed(0, bit_width + 2);
						end if;
					end loop;
				else
					-- regular rising edge of bit-clock
					
					if (led_green = '1') then
						-- next LED is red of same channel
						led_green <= '0';
					else
						-- increase to next channel
						if (channel_count < 15) then
							channel_count <= channel_count + 1;
						else
							channel_count <= 0;
						end if;
						
						-- next LED is green
						led_green <= '1';
					end if;
				end if;
				
				zfsync <= fsync;
			end if;
		end if;
	end process;

	-- output data on every falling edge
	process(bclk)
	begin
		if rising_edge(bclk) then
			if (led_green = '1') then
				-- green LED on channel "channel_count"
				data_out <= pdm_out(pdm_out'left - channel_count);
			else
				-- red LED on channel "channel_count + 1"
				
				-- if LED-brightness-value is above 248 (0b11111000) this corresponds to
				-- 0b011111000000000000000000 audio-data, which means only less than 4.5dBfs
				-- headroom. So we enable the CLIP-LED
				if (unsigned(led_input_array(15 - channel_count - 1)) > 248) then
					data_out <= '1';
				else
					data_out <= '0';
				end if;
			end if;
		end if;
	end process;
end rtl;

Leave a comment

Your email address will not be published. Required fields are marked *