Tutorial: Quartus Prime UART Core Lite#


Intel/Altera IP Cores for Serial Communication#

Quartus Prime software provides a set of free IP cores for FPGA designs targeting Intel / Altera FPGA devices. For serial data communication, some UART IP cores are available such as RS232-UART Core (Intel FPGA IP), Intel FPGA 16550 Compatible UART Core, UART Core, Lightweight UART Core and JTAG-UART Core.

This tutorial explains how to use the Lightweight UART Core. The figure below shows a simplified block diagram of the Lightweight UART Core. This core uses the Avalon bus memory-mapped interface, and there is a small set of 16-bit readable and writable registers associated with the operation of the UART core.

Figure: Block diagram of the Lightweight UART Core (Source: Intel / Altera)

Figure: Registers of the Lightweight UART Core (Source: Intel / Altera)

For more details, please consult the "Embedded Peripherals IP User Guide".

 


Lightweight UART Core Instantiation#

This section provides a guideline for using the Lightweight UART Core:

  1. Open Quartus Prime, create a new FPGA project using the "New Project Wizard".
  2. Go to the menu "Tools > Platform Designer" to start the Platform Designer tool.
  3. Add the Lightweight UART Core to the System Contents. Select the IP core from "Library > Interface Protocols > Serial > Lightweight UART (RS232 Serial Port)".
  4. Configure the IP core:
    • Basic Settings:
      • Use 8N1 (8-bit data, no parity, 1 stop bit)
      • Include CTS/RTS (flow control): No
      • Include end-of-packet: No
    • FIFO Settings:
      • Implement TXFIFO in register: Yes
      • Implement RXFIFO in register: Yes
      • Select the FIFO depth for TX and RX FIFOs
    • Baud rate: 115200 (use fixed baud rate)
  5. Export and rename the selected UART signals, then connect the clock and reset signals for the components as shown in the wiring diagram below.
  6. Click the "Generate HDL" button to generate code for the IP core. Choose VHDL as the output type and specify the output directory for the IP core files.
  7. Save the Platform Designer project (.qsys). After generating the IP core, add the .qsys to the project.
  8. Instantiate the Lightweight UART core in your top-level design.

Figure: UART IP core settings

Figure: IP core configuration

Figure: Generating the IP core and saving the IP core settings to a .qsys file

 


Serial-Loopback Test#

Below is a top-level design written in VHDL, provided as an example showing how to implement an FSM to interface with the UART core according to the Avalon memory-mapped interface.

The FSM starts with a polling read operation to check whether the RXFIFO is not empty. It reads the STATUS register of the IP core and checks the "Receive character ready" (RRDY) bit. If it is set, the FSM reads the RXFIFO register and extracts the data byte from the 16-bit value.

Next, the FSM performs a polling read operation on the STATUS register to check whether the "TX data empty" bit (TMT) is set. If the transmit data register is empty, the data byte is written as a 16-bit value to the TXFIFO register, enabling a serial-loopback test behavior.

LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.numeric_std.ALL;

ENTITY serial_tester IS
    PORT (
        CLK       : IN STD_LOGIC;  -- system clock 
        RESET_N   : IN STD_LOGIC;  -- active-low reset
        UART_RXD  : IN STD_LOGIC;  -- UART RXD
        UART_TXD  : OUT STD_LOGIC; -- UART TXD
        LEDS      : OUT STD_LOGIC_VECTOR(7 DOWNTO 0) -- LED indicators
    );
END ENTITY;

ARCHITECTURE rtl OF serial_tester IS

    -- Component Declaration of uart_core
    COMPONENT uart_core IS
        PORT (
            clk_clk              : IN STD_LOGIC;
            reset_reset_n        : IN STD_LOGIC;
            uart_slave_address   : IN STD_LOGIC_VECTOR(2 DOWNTO 0);
            uart_slave_read      : IN STD_LOGIC;
            uart_slave_write     : IN STD_LOGIC;
            uart_slave_writedata : IN STD_LOGIC_VECTOR(15 DOWNTO 0);
            uart_slave_readdata  : OUT STD_LOGIC_VECTOR(15 DOWNTO 0);
            uart_rxd             : IN  STD_LOGIC;
            uart_txd             : OUT STD_LOGIC
        );
    END COMPONENT;

    -- UART slave signals
    SIGNAL uart_addr  : STD_LOGIC_VECTOR(2 DOWNTO 0) := (OTHERS => '0');
    SIGNAL uart_read  : STD_LOGIC := '0';
    SIGNAL uart_write : STD_LOGIC := '0';
    SIGNAL uart_wdata : STD_LOGIC_VECTOR(15 DOWNTO 0) := (OTHERS => '0');
    SIGNAL uart_rdata : STD_LOGIC_VECTOR(15 DOWNTO 0) := (OTHERS => '0');
    SIGNAL data_byte  : STD_LOGIC_VECTOR( 7 DOWNTO 0) := (OTHERS => '0');

    CONSTANT RXFIFO_REG : STD_LOGIC_VECTOR(2 DOWNTO 0) := "000";
    CONSTANT TXFIFO_REG : STD_LOGIC_VECTOR(2 DOWNTO 0) := "001";
    CONSTANT STATUS_REG : STD_LOGIC_VECTOR(2 DOWNTO 0) := "010";

    TYPE state_type IS (S0, S1, S2, S3, S4);
    SIGNAL state : state_type := S0;

    SIGNAL wait_cnt : INTEGER RANGE 0 TO 1 := 0;

BEGIN

    -- Instantiate UART Core
    u0 : uart_core
    PORT MAP(
        clk_clk => CLK,
        reset_reset_n => RESET_N,
        uart_slave_address => uart_addr,
        uart_slave_read => uart_read,
        uart_slave_write => uart_write,
        uart_slave_writedata => uart_wdata,
        uart_slave_readdata => uart_rdata,
        uart_rxd => UART_RXD,
        uart_txd => UART_TXD
    );

    PROCESS (CLK, RESET_N)
    BEGIN
        IF RESET_N = '0' THEN
            state <= S0;
            uart_read  <= '0';
            uart_write <= '0';
            uart_addr  <= (OTHERS => '0');
            uart_wdata <= (OTHERS => '0');
            data_byte  <= (OTHERS => '0');
            wait_cnt   <= 0;

        ELSIF rising_edge(CLK) THEN

            CASE state IS

                WHEN S0 =>
                    uart_read <= '1';
                    uart_addr <= STATUS_REG;
                    wait_cnt  <= 1;
                    state     <= S1;

                WHEN S1 =>
                    uart_read <= '0';
                    IF wait_cnt = 0 THEN
                        IF uart_rdata(7) = '1' THEN -- RX FIFO not empty
                            uart_read <= '1';
                            uart_addr <= RXFIFO_REG;
                            wait_cnt  <= 1;
                            state     <= S2;
                        ELSE
                            uart_read <= '1';
                            uart_addr <= STATUS_REG;
                            wait_cnt  <= 1;
                            state     <= S1;
                        END IF;
                    ELSE
                        wait_cnt <= wait_cnt - 1;
                    END IF;

                WHEN S2 =>
                    IF wait_cnt = 0 THEN
                        data_byte <= uart_rdata(7 DOWNTO 0);
                        uart_read <= '1';
                        uart_addr <= STATUS_REG;
                        wait_cnt  <= 1;
                        state     <= S3;
                    ELSE
                        wait_cnt <= wait_cnt - 1;
                    END IF;

                WHEN S3 =>
                    uart_read <= '0';
                    IF wait_cnt = 0 THEN
                        IF uart_rdata(5) = '1' THEN -- Transmit data empty
                            uart_addr  <= TXFIFO_REG;
                            uart_wdata <= x"00" & data_byte;
                            uart_write <= '1';
                            state      <= S4;
                        ELSE
                            uart_addr <= STATUS_REG;
                            uart_read <= '1';
                            state     <= S3;
                            wait_cnt  <= 1;
                        END IF;
                    ELSE
                        wait_cnt <= wait_cnt - 1;
                    END IF;

                WHEN S4 =>
                    uart_write <= '0';
                    uart_write <= '0';
                    state <= S0;

                WHEN OTHERS =>
                    state <= S0;
            END CASE;
        END IF;
    END PROCESS;

    LEDS(7 DOWNTO 0) <= data_byte;

END ARCHITECTURE;

The Tcl script for FPGA pin assignment (for the DE10-Lite FPGA board) is given as an example below.

#set_global_assignment -name DEVICE 10M50DAF484C7G
#set_global_assignment -name FAMILY "MAX 10"

set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to CLK
set_instance_assignment -name IO_STANDARD "3.3 V Schmitt Trigger" -to RESET_N
set_location_assignment PIN_P11 -to CLK
set_location_assignment PIN_B8  -to RESET_N

set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to UART_RXD
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to UART_TXD
set_location_assignment PIN_V5  -to UART_RXD
set_location_assignment PIN_W7  -to UART_TXD

set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to LEDS[0]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to LEDS[1]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to LEDS[2]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to LEDS[3]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to LEDS[4]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to LEDS[5]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to LEDS[6]
set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to LEDS[7]

set_location_assignment PIN_A8  -to LEDS[0]
set_location_assignment PIN_A9  -to LEDS[1]
set_location_assignment PIN_A10 -to LEDS[2]
set_location_assignment PIN_B10 -to LEDS[3]
set_location_assignment PIN_D13 -to LEDS[4]
set_location_assignment PIN_C13 -to LEDS[5]
set_location_assignment PIN_E14 -to LEDS[6]
set_location_assignment PIN_D14 -to LEDS[7]

The following Python is provided for serial-loopback test.

import serial
import time

# Configure and open the serial port
ser = serial.Serial(
    port="COM13",     # specify the serial port (e.g. 'COM13' on Windows)
    baudrate=115200,  # set the baud rate
    timeout=0.1,      # set serial timeout (in seconds) for reading data
)

print(f"Opened {ser.portstr}")

try:
    cnt = 0
    recv_str = ""
    while True:
        msg = f"Hello World! ({cnt})\r\n"
        ser.write( msg.encode('utf-8')) # write a string to serial
        incoming = ser.readline()       # read a string line from serial
        if incoming:
            recv_str = incoming.decode("utf-8", errors="replace")
            print(f"Received message: {recv_str}", end="")
            cnt = cnt + 1  # increment the counter
            time.sleep(0.1) 

except KeyboardInterrupt:
    print("\nTerminated...")

finally:
    if ser:
        ser.close()
    print("Serial port closed.")

To test the FPGA design with the DE10 Lite FPGA board, a USB-to-serial adapter (3.3V logic level) is required.

In addition, it is recommended to use the Signal Tap Logic Analyzer to capture and analyze selected internal signals of the design to gain a better understanding of the circuit’s operation.

Figure: Using Signal Tap Logic Analyzer to analyze the design.

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2025-09-22 | Last Updated: 2025-09-23