Complete sources from github are found at /ac_inout_psu/source/system_control/system_components/ethernet_communication/ethernet/ethernet_frame_transmitter/
So far the journey to ethernet has gone though the physical cable status monitoring with MDIO, frame capturing through RGMII and In the previous blog post, a minimal protocol stack for UDP header parsing designed and it was verified against ethernet connection with a computer. The working receiver is now used for developing the transmitter.
Ethernet frame transmission
The ethernet frame consists of a 8 byte preamble, followed by the ethernet header which consists of 6 byte destination and source mac addresses and 46 to 1500 bytes of payload and ends with 4 byte frame check sequence. In contrast to the receiver which just writes data to ram as it is received, the ethernet frame transmitter needs to transmit a preamble before transmitting the frame data from ram as well as produce a frame check sequence after the ram data.
To reduce the complexity of the transmitter, the transmit functions are separated to dedicated data control structures, a transmit controller that is responsible for producing header and footer for the frame data and a ddr transmit controller responsible for transmitting the frame through the RGMII interface with a ddr transmitter. Ethernet frame transmit controller is designed first.
Ethernet frame transmit controller
The frame transmit controller takes care of the three parts of the frame creation: the preamble generation, frame data transmission and the frame check sequence generation.
After the transmit controller is created with create_transmit_controller, a request_ethernet_frame_transmission procedure can be used for triggering ethernet frame creation and a frame_transmit_is_requested can be read by the ddr transmitter control state machine for when data should be transmitted to the phy.
The transmit controller is created as an instance of a record type which includes all of the signals required by the transmit control functions. The transmit controller is declared in the transmit controller package along with a constant for initializing the frame transmitter.
package ethernet_frame_transmit_controller_pkg is
------------------------------------------------------------------------
type list_of_frame_transmitter_states is (idle, transmit_preable, transmit_data, transmit_fcs);
type frame_transmitter_record is record
frame_transmitter_state : list_of_frame_transmitter_states;
fcs_shift_register : std_logic_vector(31 downto 0);
byte_counter : natural range 0 to 2**12-1;
frame_length : natural range 0 to 2**12-1;
byte : std_logic_vector(7 downto 0);
frame_transmit_requested : boolean;
write_data_to_fifo : boolean;
ram_shift_register : std_logic_vector(31 downto 0);
ram_read_controller : ram_reader;
ram_output_port : ram_read_output_group;
end record;
constant init_transmit_controller : frame_transmitter_record :=
(
frame_transmitter_state => idle ,
fcs_shift_register => (others => '1') ,
byte_counter => 0 ,
frame_length => 0 ,
byte => x"00" ,
frame_transmit_requested => false ,
write_data_to_fifo => false ,
ram_shift_register => (others => '0') ,
ram_read_controller => ram_reader_init ,
ram_output_port => ram_read_output_init
);
------------------------------------------------------------------------
procedure create_transmit_controller (
signal transmit_controller : inout frame_transmitter_record);
------------------------------------------------------------------------
procedure transmit_ethernet_frame (
signal transmit_controller : inout frame_transmitter_record;
number_of_bytes_to_transmit : natural range 0 to 2047);
------------------------------------------------------------------------
function frame_transmit_is_requested ( transmit_controller : frame_transmitter_record)
return boolean;
------------------------------------------------------------------------
end package ethernet_frame_transmit_controller_pkg;
The create_transmit_controller creates the a state machine for the different parts of frame transmit. Once the data transmission is requested, the transmitter moves into the transmit preamble state. The preamble is seven 0xAA bytes followed by a 0xAB byte. After preamble is written, the transmit controller requests data from the ram read controller. The number of bytes requested from the ram read controller is determined by the requested frame length. The frame check sequence calculation is initiated once first byte is fetched from the embedded ram as indicated by ram_data_is_ready function. The ram reader provides an interface for the embedded ram in the fpga and was presented in previous ethernet post.
After the data transmission is ready, the frame check sequence is transmitted. The frame check sequence bits are inverted before transmission as specified by the ethernet standard.
------------------------------------------------------------------------
procedure create_transmit_controller
(
signal transmit_controller : inout frame_transmitter_record
) is
alias frame_transmitter_state is transmit_controller.frame_transmitter_state;
alias fcs_shift_register is transmit_controller.fcs_shift_register;
alias byte_counter is transmit_controller.byte_counter;
alias frame_length is transmit_controller.frame_length;
alias byte is transmit_controller.byte;
alias frame_transmit_requested is transmit_controller.frame_transmit_requested;
alias write_data_to_fifo is transmit_controller.write_data_to_fifo;
variable data_to_ethernet : std_logic_vector(7 downto 0);
begin
frame_transmit_requested <= false;
write_data_to_fifo <= false;
CASE frame_transmitter_state is
WHEN idle =>
byte_counter <= 0;
fcs_shift_register <= (others => '1');
byte <= x"00";
WHEN transmit_preable =>
write_data_to_fifo <= true;
fcs_shift_register <= (others => '1');
byte_counter <= byte_counter + 1;
if byte_counter < 7 then
byte <= x"aa";
end if;
frame_transmitter_state <= transmit_preable;
if byte_counter = 7 then
byte <= x"ab";
frame_transmitter_state <= transmit_data;
byte_counter <= 0;
load_ram_with_offset_to_shift_register(ram_controller => transmit_controller.ram_read_controller ,
start_address => 0 ,
number_of_ram_addresses_to_be_read => frame_length);
end if;
WHEN transmit_data =>
if ram_data_is_ready(transmit_controller.ram_output_port) then
write_data_to_fifo <= true;
data_to_ethernet := reverse_bit_order(transmit_controller.ram_shift_register(7 downto 0));
byte_counter <= byte_counter + 1;
if byte_counter < frame_length then
fcs_shift_register <= nextCRC32_D8(data_to_ethernet, fcs_shift_register);
byte <= data_to_ethernet;
end if;
frame_transmitter_state <= transmit_data;
if byte_counter = frame_length-1 then
frame_transmitter_state <= transmit_fcs;
byte_counter <= 0;
end if;
end if;
WHEN transmit_fcs =>
write_data_to_fifo <= true;
byte_counter <= byte_counter + 1;
fcs_shift_register <= fcs_shift_register(23 downto 0) & x"ff";
byte <= not (fcs_shift_register(31 downto 24));
frame_transmitter_state <= transmit_fcs;
if byte_counter = 3 then
frame_transmitter_state <= idle;
byte_counter <= 0;
frame_transmit_requested <= true;
end if;
end CASE;
end create_transmit_controller;
------------------------------------------------------------------------
Since the transmit controller sends out preamble and fcs in addition to the data, the total amount of bits transmitted through the RGMII interface is requested length + preamble and fcs. The frame needs to be sent out from the IO module in a single continuous stream so a fifo is used for buffering the ethernet frame. The fifo separates the frame assembling from transmission functions and allows the data transmission state to wait for the ram data to be ready. A fifo control module is explored next.
Fifo control module
The use of fifo is wrapped into a fifo input and output control records which allow for the use of interface functions init_fifo, write_data_to_fifo, load_data_from_fifo as well as checking empty and full flags as seen in the code snippet below.
--------------------------------------------------
type fifo_input_control_group is record
data : STD_LOGIC_VECTOR (7 DOWNTO 0) ;
wrreq : STD_LOGIC ;
rdreq : STD_LOGIC ;
end record;
type fifo_output_control_group is record
almost_empty : STD_LOGIC ;
empty : STD_LOGIC ;
full : STD_LOGIC ;
q : STD_LOGIC_VECTOR (7 DOWNTO 0) ;
end record;
------------------------------------------------------------------------
procedure init_fifo (
signal fifo_read_control : out fifo_input_control_group);
------------------------------------------------------------------------
procedure write_data_to_fifo (
signal fifo_in : out fifo_input_control_group;
data_to_fifo : in integer);
------------------------------------------------------------------------
procedure write_data_to_fifo (
signal fifo_in : out fifo_input_control_group;
data_to_fifo : in std_logic_vector);
------------------------------------------------------------------------
procedure load_data_from_fifo (
signal fifo_in : out fifo_input_control_group);
------------------------------------------------------------------------
function fifo_is_empty ( fifo_out : fifo_output_control_group)
return boolean;
------------------------------------------------------------------------
function fifo_is_full ( fifo_out : fifo_output_control_group)
return boolean;
------------------------------------------------------------------------
function get_data_from_fifo ( fifo_out : fifo_output_control_group)
return std_logic_vector;
------------------------------------------------------------------------
The implementations of the different fifo functions are just driving the signals of the fifo IP which is generated by the quartus software. The FIFO is instantiated by mapping the record members into the FIFO signals in the port mapping as seen in the code
-- architecture
signal fifo_data_input : fifo_input_control_group;
signal fifo_data_output : fifo_output_control_group;
begin
------------------------------------------------------------------------
u_tx_fifo : tx_fifo
PORT map
(
clock => tx_ddr_clocks.tx_ddr_clock ,
data => fifo_data_input.data ,
rdreq => fifo_data_input.rdreq ,
wrreq => fifo_data_input.wrreq ,
almost_empty => fifo_data_output.almost_empty ,
empty => fifo_data_output.empty ,
q => fifo_data_output.q
);
The records use the same names as are used in the intel quartus IP core to make the mapping easier. For portability between different vendors FPGAs an extra abstraction layer for the fifo could be made in the form of a component module to prevent the need to modify the transmitter code when a different fpga is used.
The transmit controller packs data to the transmit fifo and triggers the fifo read by the transmit control state machine. Since fifo can be read and written at the same time, the data transmission from FIFO through RGMII can be started as soon as the total amount of ram pipeline delays is passed if minimal delay between request and data being transmitted out is required.
ddr controller
The fifo is read by a ddr_control_state machine. At idle state the state machine waits for frame_transmit_request from the frame_transmit_controller. After request the state machine transfers data from the fifo until the entire fifo is transmitted as indicated by almost_empty flag. The almost empty flag rises when the last data is read from the fifo and transmitted through the ddr IO module.
-- frame transmitter process
CASE ddr_control_state is
WHEN idle =>
ddr_control_state <= idle;
if frame_transmit_is_requested(frame_transmit_controller) then
ddr_control_state <= transmit;
load_data_from_fifo(fifo_data_input);
end if;
WHEN transmit =>
ddr_control_state <= transmit;
if fifo_data_output.almost_empty /= '1' then
load_data_from_fifo(fifo_data_input);
transmit_8_bits_of_data(ethernet_tx_ddio_data_in, get_data_from_fifo(fifo_data_output));
else
transmit_8_bits_of_data(ethernet_tx_ddio_data_in, get_data_from_fifo(fifo_data_output));
ddr_control_state <= idle;
end if;
end CASE;
end if; --rising_edge
end process frame_transmitter;
------------------------------------------------------------------------
u_ethernet_tx_ddio_pkg : ethernet_tx_ddio
port map( tx_ddr_clocks ,
ethernet_frame_transmitter_FPGA_out.ethernet_tx_ddio_FPGA_out ,
ethernet_tx_ddio_data_in);
------------------------------------------------------------------------
end rtl;
The procedure call transmit_8_bits_of_data is driving the ethernet_tx_ddio IO control module, which physically transmits the data with the FPGA IO to the ethernet phy.
RGMII transmitter
The RGMII transmitter has same IO configuration as the receiver with transmit enable io and 4 data io + clock. The RGMII is implemented with ethernet_tx_ddio module which declares interface functions for the quartus ddr IP block with 5 bit IO interface.
package ethernet_tx_ddio_pkg is
------------------------------------------------------------------------
type ethernet_tx_ddio_FPGA_output_group is record
tx_ctl : std_logic;
rgmii_tx : std_logic_vector(3 downto 0);
end record;
------------------------------------------------------------------------
type ethernet_tx_ddio_data_input_group is record
tx_byte : std_logic_vector(7 downto 0);
tx_ctl : std_logic_vector(1 downto 0);
end record;
------------------------------------------------------------------------
component ethernet_tx_ddio is
port (
ethernet_tx_ddio_clocks : in ethernet_tx_ddr_clock_group;
ethernet_tx_ddio_FPGA_out : out ethernet_tx_ddio_FPGA_output_group;
ethernet_tx_ddio_data_in : in ethernet_tx_ddio_data_input_group
);
end component ethernet_tx_ddio;
------------------------------------------------------------------------
procedure init_ethernet_tx_ddio (
signal ethernet_tx_ddio_input : out ethernet_tx_ddio_data_input_group);
------------------------------------------------------------------------
procedure transmit_8_bits_of_data (
signal ethernet_tx_ddio_input : out ethernet_tx_ddio_data_input_group;
data_to_output : in integer);
------------------------------------------------------------------------
procedure transmit_8_bits_of_data (
signal ethernet_tx_ddio_input : out ethernet_tx_ddio_data_input_group;
data_to_output : in std_logic_vector(7 downto 0));
------------------------------------------------------------------------
end package ethernet_tx_ddio_pkg;
The ddr IP is instantiated in the architecture section of the module by mapping the interface signals to the IP module at the port map. This allows using the interface functions instead of driving the module signals directly thus improving readability and portability of the source code
architecture cl10_tx_ddio of ethernet_tx_ddio is
component ethddio_tx IS
PORT
(
datain_h : IN STD_LOGIC_VECTOR (4 DOWNTO 0);
datain_l : IN STD_LOGIC_VECTOR (4 DOWNTO 0);
outclock : IN STD_LOGIC ;
dataout : OUT STD_LOGIC_VECTOR (4 DOWNTO 0)
);
END component;
signal ddio_data_out_h : std_logic_vector(4 downto 0);
signal ddio_data_out_l : std_logic_vector(4 downto 0);
signal dataout : STD_LOGIC_VECTOR (4 DOWNTO 0);
------------------------------------------------------------------------
begin
ethernet_tx_ddio_FPGA_out <= ( tx_ctl => dataout(4),
rgmii_tx => dataout(3 downto 0));
ddio_data_out_l <= ethernet_tx_ddio_data_in.tx_ctl(0) & ethernet_tx_ddio_data_in.tx_byte(3 downto 0);
ddio_data_out_h <= ethernet_tx_ddio_data_in.tx_ctl(1) & ethernet_tx_ddio_data_in.tx_byte(7 downto 4);
------------------------------------------------------------------------
u_ethddio : ethddio_tx
PORT map(
datain_h => ddio_data_out_h ,
datain_l => ddio_data_out_l ,
outclock => ethernet_tx_ddio_clocks.tx_ddr_clock ,
dataout => dataout
);
------------------------------------------------------------------------
end cl10_tx_ddio;
Next the code is compiled to FPGA and tested with Cyclone 10lp FPGA.
Test with FPGA
The hardware test code is written in the frame_transmitter.vhd. The test code includes cascaded counters that produce a 1.6 second delay. After the delay, a frame transmission is requested. The control object creation and ddr driver initialization are highlighted.
The test code transmits data packets with 92 to 100 byte length with 1.6 second delay. The packet length is incremented with every transmitted packet until the maximum length is reached at which point it is wrapped to 92.
------------------------------------------------------------------------
frame_transmitter : process(tx_ddr_clocks.tx_ddr_clock)
begin
if rising_edge(tx_ddr_clocks.tx_ddr_clock) then
--------------------------------------------------
if counter_for_100kHz > 0 then
counter_for_100kHz <= counter_for_100kHz - 1;
else
counter_for_100kHz <= counter_value_at_100kHz;
if counter_for_1600ms > 0 then
counter_for_1600ms <= counter_for_1600ms - 1;
else
counter_for_1600ms <= counter_value_at_1600ms;
testicounter <= testicounter + 1;
if testicounter > 101 then
testicounter <= 92;
end if;
request_ethernet_frame_transmission(frame_transmit_controller, testicounter);
end if;
end if;
--------------------------------------------------
init_ethernet_tx_ddio(ethernet_tx_ddio_data_in);
init_fifo(fifo_data_input);
create_ram_read_controller(transmitter_ram_read_control_port ,
ethernet_frame_transmitter_ram_data_out.ram_read_port_data_out ,
frame_transmit_controller.ram_read_controller ,
frame_transmit_controller.ram_shift_register);
create_transmit_controller(frame_transmit_controller);
frame_transmit_controller.ram_output_port <= ethernet_frame_transmitter_ram_data_out.ram_read_port_data_out;
if frame_transmit_controller.write_data_to_fifo then
write_data_to_fifo(fifo_data_input, frame_transmit_controller.byte);
end if;
CASE ddr_control_state is
WHEN idle =>
ddr_control_state <= idle;
if frame_transmit_is_requested(frame_transmit_controller) then
ddr_control_state <= transmit;
load_data_from_fifo(fifo_data_input);
end if;
WHEN transmit =>
ddr_control_state <= transmit;
if fifo_data_output.almost_empty /= '1' then
load_data_from_fifo(fifo_data_input);
transmit_8_bits_of_data(ethernet_tx_ddio_data_in, get_data_from_fifo(fifo_data_output));
else
transmit_8_bits_of_data(ethernet_tx_ddio_data_in, get_data_from_fifo(fifo_data_output));
ddr_control_state <= idle;
end if;
end CASE;
end if; --rising_edge
end process frame_transmitter;
Next the ethernet frame is transmitted and correct frame capture with computer is verified using wireshark. The transmitted frame data is read from ram, that is preloaded with the test frame from file found in the project repository at ac_inout_psu/IP/ethernet_IP/memory/transmit_ram/transmit_ram_init.mif. The transmitter ram has a frame data for a UDP protocol. With the use of ram any arbitrary data can be sent out by rewriting the UDP protocols data section.
Wireshark capture of the frame is shown in in Figure 1. Although the packet is malformed, the fact that the frame can be captured means that the transmission is successful and the test data can be seen in the captured frame.
Figure 1. Wireshark capture of the test data teceived from fpga with gigabit ethernet.
The full system build with the signal processing, ad converter, uart and ethernet takes just over 1600 logic cells from the cyclone device with the current minimal gigabit ethernet communication taking less than 800 cells for MDIO, receiver, transmitter and the minimal UDP protocol header parsing included.
Figure 2. AC inout psu resource allocation with ad converter, ethernet, uart and signal processing included.
Since data can be received and transmitted with a computer as a link parner, a minimal communication should now be usable assuming static IP address, a correct UDP port number and and frame lengths are used.
Great job Sir! Keep it up.