Moving average on Red Pitaya
On the Red Pitaya development board, we will build a circuit for signal scaling and filtering with a small averaging sieve that calculates the average of four consecutive samples.
Generation of an example from the repository
To launch the example from the repository, you need to execute the following command:
cd C:/Users/RedPitaya-FPGA/prj/Examples/Simple_moving_average/
vivado -source make_project.tcl
Creation of a new project
- Make a folder called “Simple Moving Average” in /RedPitaya-FPGA/prj/Examples.
- Copy all files from /RedPitaya-FPGA/prj/v0.94 into the newly created folder.
- Create a new file named red_pitaya_proc.vhd in the “Simple Moving Average/rtl” directory.
- Copy the framework of filter development into the file.
library IEEE;
use IEEE.STD_LOGIC_1164.all;
use IEEE.NUMERIC_STD.all;
entity red_pitaya_proc is
port (
clk_i : in std_logic; -- bus clock
rstn_i : in std_logic; -- bus reset - active low
addr_i : in std_logic_vector(31 downto 0); -- bus address
wdata_i : in std_logic_vector(31 downto 0); -- bus write data
wen_i : in std_logic; -- bus write enable
ren_i : in std_logic; -- bus read enable
rdata_o : out std_logic_vector(31 downto 0); -- bus read data
err_o : out std_logic; -- bus error indicator
ack_o : out std_logic; -- bus acknowledge signal
adc_i : in std_logic_vector(13 downto 0);
adc_o : out std_logic_vector(13 downto 0)
);
end red_pitaya_proc;
architecture Behavioral of red_pitaya_proc is
component moving_average
port (
data_i : in std_logic_vector (13 downto 0);
clk_i : in std_logic;
rstn_i : in std_logic;
tag_i : in unsigned (1 downto 0);
data_o : out std_logic_vector (13 downto 0));
end component;
begin
pbusr: process(clk_i)
begin
if(rising_edge(clk_i)) then
if (wen_i or ren_i)='1' then
ack_o <= '1';
end if;
if (rstn_i = '0') then
else
case addr_i(19 downto 0) is
when X"00000" => rdata_o <= X"00000001";
when others => rdata_o <= X"00000000";
end case;
end if;
end if;
end process;
end Behavioral;
Also, copy red_pitaya_scope.v from /RedPitaya-FPGA/rtl/classic to Simple Moving Average/rtl and rename it to loop_scope.v. Also, change the name of the module inside the file from red_pitaya_scope to loop_scope.
Now create red_pitaya_proc_tb.vhd in /Simple Moving Average/tbn and copy the code there:
library IEEE;
use IEEE.STD_LOGIC_1164.all;
use IEEE.numeric_std.all;
entity red_pitaya_proc_tb is
end red_pitaya_proc_tb;
architecture Behavioral of red_pitaya_proc_tb is
component red_pitaya_proc
port (
clk_i : in std_logic;
rstn_i : in std_logic;
addr_i : in std_logic_vector(31 downto 0);
wdata_i : in std_logic_vector(31 downto 0);
wen_i : in std_logic;
ren_i : in std_logic;
rdata_o : out std_logic_vector(31 downto 0);
err_o : out std_logic;
ack_o : out std_logic;
adc_i : in std_logic_vector(13 downto 0);
adc_o : out std_logic_vector(13 downto 0)
);
end component;
signal clk_i : std_logic := '0';
signal rstn_i : std_logic;
signal addr_i : std_logic_vector(31 downto 0);
signal wdata_i : std_logic_vector(31 downto 0);
signal wen_i : std_logic;
signal ren_i : std_logic;
signal rdata_o : std_logic_vector(31 downto 0);
signal err_o : std_logic;
signal ack_o : std_logic;
signal adc_i : std_logic_vector(13 downto 0);
signal adc_o : std_logic_vector(13 downto 0);
signal i : integer range 0 to 30 := 0;
type memory_type is array (0 to 29) of integer range -128 to 127;
signal sine : memory_type := (0, 16, 31, 45, 58, 67, 74, 77, 77, 74, 67, 58, 45, 31, 16, 0,
-16, -31, -45, -58, -67, -74, -77, -77, -74, -67, -58, -45, -31, -16);
-- Simulation control
signal sim : std_logic := '0';
constant T : time := 50 ns;
begin
uut : red_pitaya_proc port map (
clk_i => clk_i,
rstn_i => rstn_i,
addr_i => addr_i,
wdata_i => wdata_i,
wen_i => wen_i,
ren_i => ren_i,
rdata_o => rdata_o,
err_o => err_o,
ack_o => ack_o,
adc_i => adc_i,
adc_o => adc_o
);
-- Define the clock
clk_process : process
begin
if sim = '0' then
clk_i <= '0';
wait for T/2;
clk_i <= '1';
wait for T/2;
else
wait;
end if;
end process;
-- Generate a sine signal from the table
singen : process(clk_i)
begin
if (rising_edge(clk_i)) then
-- adc_i <= std_logic_vector(to_signed(20*sine(i), 14));
if (sine(i) > 0) then
adc_i <= std_logic_vector(to_signed(2000, 14));
else
adc_i <= std_logic_vector(to_signed(-2000, 14));
end if;
i <= i + 1;
if (i = 29) then
i <= 0;
end if;
end if;
end process;
-- Sets the simplified AXI bus signals
stim_proc : process
begin
rstn_i <= '0'; -- active reset
addr_i <= X"00000008";
wdata_i <= X"00000000";
wen_i <= '0'; ren_i <= '0';
wait for T;
rstn_i <= '1'; -- deactivate reset, write to register
addr_i <= X"00000008";
wdata_i <= X"00000002";
wen_i <= '1';
wait for T;
wen_i <= '0';
wait for 100*T; -- entry of a new value in the register
wdata_i <= x"00000003";
wen_i <= '1';
wait for T;
addr_i <= X"00000000";
wen_i <= '0';
wait for 100*T;
sim <= '1'; -- stop the simulation
wait;
end process;
end;
Now we need to create a project generation script. Make a copy of the red_pitaya_vivado_project_Z10.tcl and name it Average_project.tcl, for example.
We need to change some strings in the file:
cd prj/$prj_name → cd prj/Examples/$prj_name
set path_brd ./../brd → set path_brd ./../../brd
set path_sdc ../../sdc → set path_sdc ../../../sdc
add_files ../../$path_rtl → add_files ../../../$path_rtl
Add a variable:
set path_tbn tbn
Also, we need to add the following strings after the string in the second code-block below:
add_files -fileset sim_1 -norecurse $path_tbn/red_pitaya_proc_tb.vhd
add_files $path_bd
Now we can generate a project (the -tclargs parameter should be the same as the main project folder name):
vivado -source Average_project.tcl -tclargs "Simple Moving Average"
If everything is done correctly, in the generated project we can generate a bitstream without any errors.
Edit the file red_pitaya_top.sv. Declare two new signals (adc_i and adc_o), connect them to the oscilloscope module, and replace red_pitaya_scope with our new loop_scope:
////////////////////////////////////////////////////////////////////////////////
// oscilloscope
////////////////////////////////////////////////////////////////////////////////
logic trig_asg_out;
logic [14-1: 0] adc_i;
logic [14-1: 0] adc_o;
loop_scope i_scope (
// Simple Moving Average
.adc_in (adc_o ),
.adc_out (adc_i ),
// ADC
.adc_a_i (adc_dat[0] ), // CH 1
.adc_b_i (adc_dat[1] ), // CH 2
...
Add two ADC data ports to the loop_scope.v file (found under Design Sources by expanding the red_pitaya_top module and double clicking i_scope: loop_scope).
module loop_scope #(parameter RSZ = 14 // RAM size 2^RSZ
)(
// Simple Moving Average
input [ 14-1: 0] adc_in ,
output [ 14-1: 0] adc_out ,
// ADC
input adc_clk_i , // ADC clock
input adc_rstn_i , // ADC reset - active low
Furthermore, replace this process:
always @(posedge adc_clk_i) begin
if (adc_we && adc_dv) begin
adc_a_buf[adc_wp] <= adc_a_dat ;
adc_b_buf[adc_wp] <= adc_b_dat ;
end
end
With this one:
// Simple Moving Average
always @(posedge adc_clk_i) begin
if (adc_we && adc_dv) begin
adc_a_buf[adc_wp] <= adc_in ;
adc_b_buf[adc_wp] <= adc_b_dat ;
end
end
assign adc_out = adc_b_dat;
Then, in the file red_pitaya_top.sv, we must connect the signals to red_pitaya_proc. Add the following code somewhere in the oscilloscope section, after the logic declarations:
// Simple Moving Average
red_pitaya_proc i_proc (
.clk_i ( adc_clk ), // clock
.rstn_i ( adc_rstn ), // reset - active low
.addr_i ( sys[6].addr ), // address
.wdata_i ( sys[6].wdata), // write data
.wen_i ( sys[6].wen ), // write enable
.ren_i ( sys[6].ren ), // read enable
.rdata_o ( sys[6].rdata), // read data
.err_o ( sys[6].err ), // error indicator
.ack_o ( sys[6].ack ), // acknowledge signal
.adc_i ( adc_i ),
.adc_o ( adc_o )
);
We need to remove the stub for the current bus (near line 290 - change the i=6 to i=7):
generate
for (genvar i=7; i<8; i++) begin: for_sys
sys_bus_stub sys_bus_stub_5_7 (sys[i]);
end: for_sys
endgenerate
After these manipulations, we have redirected the data from the red_pitaya_proc.vhd module to the first ADC channel. And the data from the second channel was connected to the red_pitaya_proc.vhd input. Within this module, you can already start processing data.
Development of the moving average
Create a scheme that calculates the current average of the last three inputs. Basic outline of the moving average:
Connections:
clk, reset (active at logical 0)data_i, 8-bit inputtag_i, 2-bit control inputdata_o, 8-bit outputtag_o, 2-bit control output
The data comes into the circuit one after the other, and the control input indicates the cycles in which the data is valid. At tag_i = 01, the first data row is at the input; at 10, the second data row is at the output; and at 11, the last data row is at the output:
cycle | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
tag_i | 00 | 01 | 10 | 10 | 10 | 10 | 11 | 00 | 00 |
data_i | xx | 100 | 50 | 200 | 200 | 200 | 120 | xx | xx |
The task of the circuit is to calculate the current average of the last three values. For the first valid data, assume that the previous two values are equal to 0.
The circuit contains three series-connected registers and a combinational circuit for calculating the average value after equations: p = (a + b + c) * 1/3 ≈ ((a + b + c) * 85) >> 8
Instead of dividing by 3, we will use an approximation: ⅓ ≈ 85/256. Use a 7-bit constant of 85 to multiply. Division by 256 represents the value shifted by 8 places to the right. The shift is made by selection subvector, where the lower 8 bits of the product are removed.
In order to implement this, we will create a new component with VHDL:
Create a new file moving_average.vhd in Simple Moving Average/rtl (Add Sources => Add or create design sources => Create File (VHDL)).
Code Explanation
Define inputs and outputs:
entity moving_average is
Port ( data_i : in std_logic_vector (13 downto 0); -- adc input data
clk_i : in std_logic; -- bus clock
rstn_i : in std_logic; -- bus reset - active low
tag_i : in unsigned (1 downto 0); -- filter window size
data_o : out std_logic_vector (13 downto 0)); -- filtered data
end moving_average;
We will need some memory to store previous values. Describe the memory type and create it. Also, we will need some kind of register to store the sum:
architecture Behavioral of moving_average is
type mem_t is array (0 to 2) of signed (13 downto 0);
signal regs: mem_t; -- buffer for moving average algorithm
signal sum: signed(13 downto 0); -- register for storing the sum of register values
begin
The data is updated for each clk, thus, the process runs at each clock change:
process (clk_i)
begin
if(rising_edge(clk_i)) then
We need to reset the registers:
if (rstn_i = '0') then
sum <= "00000000000000";
Connect the first register with the ADC directly.
regs(0) <= signed(data_i);
The summer will always constructively add 3 registers:
sum <= regs(0) + regs(1) + regs(2);
Then we should describe connections among registers. We should keep in mind that the summer constructively adds 3 registers. Thus, we need to reset register values to 0 so that the moving average is calculated correctly each time.
if (tag_i(1) = '1') then
regs(1) <= regs(0);
else
regs(1) <= "00000000000000";
end if;
if (tag_i(0) = '1') then
regs(2) <= regs(1);
else
regs(2) <= "00000000000000";
end if;
The last thing we need is the multiplexer to calculate an average value for a buffer with different lengths. Since division is a pretty complex procedure, we need to simplify it. One of the approaches is a real number with a fixed point. We can represent a division as 1/3 ≈ 85/256. Division by 256 is executed by a simple operation of right logical shift.
case tag_i is
-- regs
when "01" => data_o <= std_logic_vector(sum);
-- regs / 2
when "10" => data_o <= std_logic_vector(shift_right(sum, 1));
-- (regs * 85) / 256
when "11" => data_o <= std_logic_vector(resize(shift_right(sum * 85, 8), 14));
-- (regs * 85) / 256
when others => data_o <= std_logic_vector(resize(shift_right(sum * 85, 8), 14));
end case;
The complete code
library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.all;
entity moving_average is
Port ( data_i : in std_logic_vector (13 downto 0); --
clk_i : in std_logic; -- bus clock
rstn_i : in std_logic; -- bus reset - active low
tag_i : in unsigned (1 downto 0); --
data_o : out std_logic_vector (13 downto 0)); --
end moving_average;
architecture Behavioral of moving_average is
type mem_t is array (0 to 2) of signed (13 downto 0);
signal regs: mem_t; -- buffer for moving average algorithm
signal sum: signed(13 downto 0);
begin
regs(0) <= signed(data_i);
process (clk_i)
begin
if(rising_edge(clk_i)) then
if (rstn_i = '0') then
sum <= "00000000000000";
else
case tag_i is
-- regs
when "01" => data_o <= std_logic_vector(sum);
-- regs / 2
when "10" => data_o <= std_logic_vector(shift_right(sum, 1));
-- (regs * 85) / 256
when "11" => data_o <= std_logic_vector(resize(shift_right(sum * 85, 8), 14));
-- (regs * 85) / 256
when others => data_o <= std_logic_vector(resize(shift_right(sum * 85, 8), 14));
end case;
if (tag_i(1) = '1') then
regs(1) <= regs(0);
else
regs(1) <= "00000000000000";
end if;
if (tag_i(0) = '1') then
regs(2) <= regs(1);
else
regs(2) <= "00000000000000";
end if;
sum <= regs(0) + regs(1) + regs(2);
end if;
end if;
end process;
end Behavioral;
By clicking the + sign under sources, you can add the red_pitaya_proc.vhd file to the project. The previously created module/component must be added to red_pitaya_proc. The component moving_average is already added to the file (component … end component), so we just add the component connection to the architecture (anywhere between the begin and end architecture lines):
rp_average:
moving_average
port map (
data_i => adc_i,
clk_i => clk_i,
rstn_i => rstn_i,
tag_i => tag_i,
data_o => adc_o
);
Create a register/signal in the architecture to store the moving average of a chosen length- Between the end component and the begin lines, insert the following code:
signal tag_i: unsigned(1 downto 0) := "01";
Define the value after the reset in the process:
if (rstn_i = '0') then
tag_i <= "01";
else
...
Work with registers
In order to change the buffer dimension, we need to have the “writing” rights for this register at the address. The module red_pitaya_proc is already connected to the system bus and has the following address: 0x406xxxxx. Upon receiving data by address, we must write in the tag_i register (further modification to the process in the red_pitaya_proc.vhd file):
case addr_i(19 downto 0) is
when X"00000" => rdata_o <= X"00000001";
when X"00008" => tag_i <= unsigned(wdata_i(1 downto 0));
when others => rdata_o <= X"00000000";
end case;
You can find more details about the Red Pitaya register map here.
Device enquiry and their configuration are made by 0x40600000, thus, we’re using 0x40600008.
Simulation
red_pitaya_proc_tb.vhd should be defined as the upper module in the Simulation Sources-> sim_1:
Launch the simulation and configure the signals adc_i and adc_o as analog:
Setup the data type of signal:
Setup the display of these signals:
Set the simulation time to 10 us and restart the simulation:
After the simulation is done, you should see the following oscillogram:
We can notice that the signal gets corrupted when we change the size of tag_i (about 5us on the oscillogram). This is caused by the fact that when we increase the size of tag_i, one or two registers become empty and the signal amplitude falls down.
- Generate a sine signal from the table
singen : process(clk_i)
begin if(rising_edge(clk_i)) then adc_i <= std_logic_vector(to_signed(20*sine(i), 14));
-- if (sine(i) > 0) then
-- adc_i <= std_logic_vector(to_signed(2000, 14));
-- else
-- adc_i <= std_logic_vector(to_signed(-2000, 14));
-- end if; i <= i+ 1; if(i = 29) then i <= 0; end if; end if;
end process;
Upload bitstream to Red Pitaya
Insert an SD card with the uploaded ecosystem.
OS version 1.04 or older
OS version 2.00
Testing
Connect to the Red Pitaya and start the oscilloscope and connect OUT1 to IN2. Start the generator on the first channel at a frequency of 1 MHz or more. You should see a signal on IN1 even though nothing is connected to it. This is just the filtered moving average data. In order to setup the filter, we need to connect via SSH and enter the following command:
monitor 0x40600008 3
where 0x40600008 is the address of our register and 3 is the value that should be written in the register.
The result of our filter’s work when the register value equals 3:
The result of our filter’s work when the register value equals 2:
The result of our filter’s work when the register value equals 1:
Author & Source
Orignal author: Laboratory for Integrated Circuit Design
Original lesson: link
Please note that the original site is in Slovene.