Objective and use case
What you’ll build: Implement a simple UART echo at 9600 bps on the Basys 3 FPGA board. The FPGA will receive bytes from a serial terminal and immediately transmit them back.
Why it matters / Use cases
- Facilitates real-time communication between embedded systems and PCs, enhancing debugging capabilities.
- Enables the development of interactive applications where user input is processed and echoed back, such as command-line interfaces.
- Serves as a foundational project for understanding serial communication protocols, applicable in IoT devices using LoRa or MQTT.
- Demonstrates the use of FPGA for handling asynchronous data streams, crucial for applications in robotics and automation.
Expected outcome
- Successful transmission and reception of data at 9600 bps with minimal latency (under 10 ms).
- Measured packet loss should be 0%, ensuring reliable communication.
- Data integrity confirmed through checksum validation, with 100% accuracy in echoed bytes.
- Demonstrated ability to handle 8 data bits, no parity, and 1 stop bit configuration consistently.
Audience: Beginners in FPGA programming; Level: Basic
Architecture/flow: Data flows from the serial terminal through the USB-to-TTL UART adapter to the Basys 3 FPGA, which processes and echoes the data back.
Hands‑on Practical: UART Echo at 9600 bps on Basys 3 (Xilinx Artix‑7)
Objective: Implement a simple UART echo (9600 bps, 8 data bits, no parity, 1 stop bit) on the Basys 3 FPGA board. The FPGA will receive bytes from a serial terminal and immediately transmit them back.
Level: Basic
Preferred language: Verilog (concise subset)
Toolchain: Xilinx Vivado WebPACK CLI (no GUI), precise versions and commands provided
Note: This tutorial uses an external 3.3 V USB‑to‑TTL UART adapter connected to the Basys 3 Pmod JA header. This makes the IO mapping explicit and avoids ambiguity, and it works on any host OS with a serial terminal. If you prefer the on‑board USB‑UART bridge, you can switch to the Digilent Master XDC mapping later (covered in Improvements).
Prerequisites
- Basic familiarity with:
- Writing and organizing small Verilog modules
- Running shell commands (Linux/macOS Terminal or Windows PowerShell)
- Serial terminals (PuTTY, Tera Term, screen, minicom)
- Installed software:
- Xilinx Vivado WebPACK 2023.2 (or 2024.x; commands shown for 2023.2)
- USB‑to‑serial drivers for your adapter (CP210x/FTDI, if needed)
- Host OS:
- Linux (Ubuntu 20.04/22.04), macOS, or Windows 10/11
Check Vivado is available:
vivado -version
Expected (example):
Vivado v2023.2 (64-bit)
SW Build 4026211
Materials
- Basys 3 (Xilinx Artix‑7) board:
- Exact model: Digilent Basys 3, FPGA part: XC7A35T-1CPG236C
- 100 MHz on‑board oscillator
- Micro‑USB cable (for JTAG programming/power)
- USB‑to‑TTL UART adapter (3.3 V logic level)
- Example: FTDI‑based FT232RL or Silicon Labs CP2102/CP2102N module
- Must operate at 3.3 V logic; do not use 5 V logic directly
- 3–4 female‑to‑female jumper wires
Optional (helpful for validation):
– A logic analyzer or oscilloscope (to check bit timing)
Setup/Connection
We will map the UART signals to the Pmod JA header to keep constraints explicit and portable. The UART RX on the FPGA connects to the USB‑UART TX, and FPGA TX connects to USB‑UART RX. Also connect GND and 3.3 V.
Important:
– The USB‑UART adapter must be set for 3.3 V logic level.
– Never drive FPGA IOs with 5 V.
Physical connections
- Basys 3 Pmod JA top row has signal pins conventionally referenced as JA1, JA2, JA3, JA4 (plus power pins). We will use:
- JA1 = FPGA UART TX (out)
- JA2 = FPGA UART RX (in)
- JA GND = Adapter GND
- JA 3V3 = Adapter VCC (if your adapter needs a 3.3 V reference; do not power the adapter from the FPGA unless the adapter requires minimal current—most adapters get power from the computer’s USB; in that case, leave 3V3 unconnected and only tie GNDs)
We map JA1/JA2 to specific FPGA package pins via the XDC file. The following table summarizes the wiring and FPGA mapping used below.
Connection table
| Signal role | Basys 3 header | FPGA package pin (XDC) | USB‑UART adapter |
|---|---|---|---|
| UART TX (FPGA → PC) | JA1 | J1 | RXD |
| UART RX (PC → FPGA) | JA2 | L2 | TXD |
| Ground | JA GND | — | GND |
| 3.3 V reference (if used) | JA 3V3 | — | VCC (3.3 V) |
Also connect the Basys 3 micro‑USB for programming and power.
Note: We also use the on‑board 100 MHz clock (FPGA pin W5). One LED (LD0) is used as a heartbeat activity indicator (mapped to U16).
Full Code
We will implement three Verilog modules:
– uart_rx: receives bytes with 1x mid‑bit sampling at 9600 bps
– uart_tx: transmits bytes at 9600 bps
– top: ties RX to TX (echo), includes a heartbeat LED and simple “byte received” strobe
The UART implementation:
– 8 data bits, LSB first
– No parity
– 1 stop bit
– 9600 bps derived from 100 MHz clock using integer rounding
The bit period in clock ticks is rounded:
BIT_TICKS = (CLK_FREQ + BAUD/2) / BAUD
For 100 MHz and 9600 bps, BIT_TICKS = (100_000_000 + 4800)/9600 = 10417 tick clocks per bit.
This yields ~0.0064% timing error, within typical UART tolerance.
src/uart_rx.v
// uart_rx.v - Simple 8N1 UART receiver, mid-bit sampling at 1x
// Public domain / educational sample
module uart_rx #(
parameter integer CLK_FREQ = 100_000_000,
parameter integer BAUD = 9600
)(
input wire clk,
input wire rst,
input wire rx, // UART RX line from host (idle high)
output reg valid, // Pulses high for 1 clk when data_out is valid
output reg [7:0] data_out
);
localparam integer BIT_TICKS = (CLK_FREQ + (BAUD/2)) / BAUD;
localparam [1:0] IDLE = 2'd0, START = 2'd1, DATA = 2'd2, STOP = 2'd3;
reg [1:0] state = IDLE;
reg [13:0] tick_cnt = 0; // enough bits for ~10417
reg [2:0] bit_idx = 0;
reg [7:0] shreg = 0;
reg rx_sync1 = 1'b1, rx_sync2 = 1'b1;
// Synchronize RX to clk domain
always @(posedge clk) begin
rx_sync1 <= rx;
rx_sync2 <= rx_sync1;
end
always @(posedge clk) begin
if (rst) begin
state <= IDLE;
tick_cnt <= 0;
bit_idx <= 0;
shreg <= 0;
valid <= 1'b0;
data_out <= 8'h00;
end else begin
valid <= 1'b0; // default
case (state)
IDLE: begin
if (rx_sync2 == 1'b0) begin
// Start bit detected (falling edge). Wait half bit to sample center.
state <= START;
tick_cnt <= (BIT_TICKS >> 1); // half bit
end
end
START: begin
if (tick_cnt == 0) begin
// Sample start bit; if still low, proceed
if (rx_sync2 == 1'b0) begin
state <= DATA;
bit_idx <= 3'd0;
tick_cnt <= BIT_TICKS - 1;
end else begin
// False start; go back to IDLE
state <= IDLE;
end
end else begin
tick_cnt <= tick_cnt - 1;
end
end
DATA: begin
if (tick_cnt == 0) begin
// Sample data bit in the middle of bit time
shreg <= {rx_sync2, shreg[7:1]}; // LSB first
tick_cnt <= BIT_TICKS - 1;
if (bit_idx == 3'd7) begin
state <= STOP;
end
bit_idx <= bit_idx + 1;
end else begin
tick_cnt <= tick_cnt - 1;
end
end
STOP: begin
if (tick_cnt == 0) begin
// Sample stop bit; should be high
data_out <= shreg;
valid <= 1'b1;
state <= IDLE;
end else begin
tick_cnt <= tick_cnt - 1;
end
end
default: state <= IDLE;
endcase
end
end
endmodule
src/uart_tx.v
// uart_tx.v - Simple 8N1 UART transmitter
// Public domain / educational sample
module uart_tx #(
parameter integer CLK_FREQ = 100_000_000,
parameter integer BAUD = 9600
)(
input wire clk,
input wire rst,
input wire [7:0] data_in,
input wire start, // pulse high for 1 clk to start transmit (if not busy)
output reg tx, // UART TX line to host (idle high)
output wire busy // high while transmitting
);
localparam integer BIT_TICKS = (CLK_FREQ + (BAUD/2)) / BAUD;
reg [3:0] bit_idx = 0;
reg [13:0] tick_cnt = 0;
reg [9:0] frame = 10'b1111111111; // default (idle)
reg active = 1'b0;
assign busy = active;
// Frame format: start (0), 8 data bits LSB first, stop (1)
always @(posedge clk) begin
if (rst) begin
active <= 1'b0;
frame <= 10'b1111111111;
bit_idx <= 4'd0;
tick_cnt <= 14'd0;
tx <= 1'b1; // idle high
end else begin
if (!active) begin
// Idle; accept start if requested
if (start) begin
frame <= {1'b1, data_in, 1'b0}; // [stop|data|start]
bit_idx <= 4'd0;
tx <= 1'b0; // start bit
active <= 1'b1;
tick_cnt <= BIT_TICKS - 1;
end else begin
tx <= 1'b1;
end
end else begin
if (tick_cnt == 0) begin
bit_idx <= bit_idx + 1;
if (bit_idx < 9) begin
tx <= frame[bit_idx + 1]; // shift next bit
tick_cnt <= BIT_TICKS - 1;
end else begin
// Done after stop bit
active <= 1'b0;
tx <= 1'b1;
end
end else begin
tick_cnt <= tick_cnt - 1;
end
end
end
end
endmodule
src/top_uart_echo.v
// top_uart_echo.v - Basys 3 UART echo @9600 bps using Pmod JA
// Connect JA1 (J1) -> USB-UART RX, JA2 (L2) <- USB-UART TX
module top_uart_echo (
input wire clk, // 100 MHz clock (W5)
input wire rst_btn, // reset input (active high) mapped to a user button
input wire uart_rx, // from USB-UART TX
output wire uart_tx, // to USB-UART RX
output reg led0 // heartbeat / activity LED
);
// Synchronize and stretch reset
reg [3:0] rst_shreg = 4'hF;
always @(posedge clk) begin
rst_shreg <= {rst_shreg[2:0], rst_btn};
end
wire rst = rst_shreg[3];
// Heartbeat at ~1 Hz
reg [26:0] hb_cnt = 0;
always @(posedge clk) begin
if (rst) begin
hb_cnt <= 0;
led0 <= 1'b0;
end else begin
hb_cnt <= hb_cnt + 1;
if (hb_cnt == 100_000_000/2) begin // ~0.5s
led0 <= ~led0;
hb_cnt <= 0;
end
end
end
wire rx_valid;
wire [7:0] rx_data;
uart_rx #(
.CLK_FREQ(100_000_000),
.BAUD(9600)
) U_RX (
.clk (clk),
.rst (rst),
.rx (uart_rx),
.valid (rx_valid),
.data_out (rx_data)
);
reg tx_start = 1'b0;
wire tx_busy;
uart_tx #(
.CLK_FREQ(100_000_000),
.BAUD(9600)
) U_TX (
.clk (clk),
.rst (rst),
.data_in (rx_data),
.start (tx_start),
.tx (uart_tx),
.busy (tx_busy)
);
// Echo logic: when a byte arrives and TX is idle, fire tx_start for 1 clk
reg rx_valid_d = 1'b0;
always @(posedge clk) begin
if (rst) begin
tx_start <= 1'b0;
rx_valid_d <= 1'b0;
end else begin
rx_valid_d <= rx_valid;
tx_start <= 1'b0;
if (rx_valid && !rx_valid_d && !tx_busy) begin
tx_start <= 1'b1;
end
end
end
endmodule
Constraints: constraints/basys3_uart_ja.xdc
- Maps the 100 MHz clock on W5 to the top‑level port clk
- Maps JA1 (J1) to uart_tx
- Maps JA2 (L2) to uart_rx
- Maps BTN_CENTER (or any user button) to rst_btn
- Maps LED0 (U16) to led0
Note: On Basys 3, the standard 100 MHz clock is on W5; LED0 is U16; the user buttons include center BTN(C) at U18 (commonly used). The assignments below are consistent with Basys 3 reference designs.
## Clock: 100 MHz
set_property PACKAGE_PIN W5 [get_ports {clk}]
set_property IOSTANDARD LVCMOS33 [get_ports {clk}]
create_clock -add -name sys_clk -period 10.000 -waveform {0 5} [get_ports {clk}]
## UART via Pmod JA
## JA1 (top) -> FPGA TX to USB-UART RX
set_property PACKAGE_PIN J1 [get_ports {uart_tx}]
set_property IOSTANDARD LVCMOS33 [get_ports {uart_tx}]
## JA2 (top) <- FPGA RX from USB-UART TX
set_property PACKAGE_PIN L2 [get_ports {uart_rx}]
set_property IOSTANDARD LVCMOS33 [get_ports {uart_rx}]
## LED0 (LD0)
set_property PACKAGE_PIN U16 [get_ports {led0}]
set_property IOSTANDARD LVCMOS33 [get_ports {led0}]
## Reset button (BTN_CENTER)
set_property PACKAGE_PIN U18 [get_ports {rst_btn}]
set_property IOSTANDARD LVCMOS33 [get_ports {rst_btn}]
set_property PULLUP true [get_ports {rst_btn}]
If your center button pin differs, consult the Basys 3 Master XDC and update the mapping accordingly.
Build/Flash/Run Commands (Vivado WebPACK CLI)
This section uses a clean project directory with a scripted, repeatable flow.
1) Create directory structure
Linux/macOS:
mkdir -p basys3-uart-echo/{src,constraints,scripts,build}
Windows PowerShell:
mkdir basys3-uart-echo
mkdir basys3-uart-echo\src
mkdir basys3-uart-echo\constraints
mkdir basys3-uart-echo\scripts
mkdir basys3-uart-echo\build
Place the three Verilog files into basys3-uart-echo/src and the XDC into basys3-uart-echo/constraints.
- src/top_uart_echo.v
- src/uart_rx.v
- src/uart_tx.v
- constraints/basys3_uart_ja.xdc
2) Create a Vivado TCL build script scripts/build.tcl
set_param general.maxThreads 4
# Paths
set proj_name "basys3_uart_echo"
set proj_dir [file normalize "../build"]
set src_dir [file normalize "../src"]
set xdc_dir [file normalize "../constraints"]
file mkdir $proj_dir
# Create project
create_project $proj_name $proj_dir -part xc7a35tcpg236-1 -force
# (Optional but recommended) Set board part to Basys 3
# Requires Vivado board files; if available, uncomment:
# set_property board_part digilentinc.com:basys3:part0:1.2 [current_project]
# Add sources
add_files -fileset sources_1 [list \
"$src_dir/top_uart_echo.v" \
"$src_dir/uart_rx.v" \
"$src_dir/uart_tx.v" \
]
set_property top top_uart_echo [current_fileset]
# Add constraints
add_files -fileset constrs_1 "$xdc_dir/basys3_uart_ja.xdc"
# Synthesis
launch_runs synth_1
wait_on_run synth_1
report_timing_summary -file "$proj_dir/post_synth_timing.rpt" -warn_on_violation
# Implementation
launch_runs impl_1 -to_step write_bitstream
wait_on_run impl_1
report_utilization -file "$proj_dir/post_impl_util.rpt"
report_timing_summary -file "$proj_dir/post_impl_timing.rpt" -warn_on_violation
# Bitstream path
set bitfile [glob -nocomplain "$proj_dir/${proj_name}.runs/impl_1/*.bit"]
puts "INFO: Bitstream: $bitfile"
# Program device (requires Basys 3 connected over USB)
open_hw
connect_hw_server
open_hw_target
current_hw_device [lindex [get_hw_devices xc7a35t*] 0]
refresh_hw_device [current_hw_device]
set_property PROGRAM.FILE $bitfile [current_hw_device]
program_hw_devices [current_hw_device]
puts "INFO: Programming complete."
close_hw
exit
3) Run Vivado in batch mode
From basys3-uart-echo/scripts directory:
Linux/macOS:
vivado -mode batch -source build.tcl
Windows PowerShell:
vivado.bat -mode batch -source build.tcl
Expected outputs:
– build/post_synth_timing.rpt, post_impl_util.rpt, post_impl_timing.rpt
– A .bit file in build/basys3_uart_echo.runs/impl_1/
– Programming automatically occurs if the Basys 3 is connected
If programming does not occur (e.g., permissions), you can program manually later with a separate TCL script or via the Hardware Manager GUI.
Step‑by‑step Validation
1) Verify physical connections
– Basys 3 powered via micro‑USB (PROG/UART port).
– USB‑UART adapter connected to host computer.
– Jumper wires:
– Basys3 JA1 (Pmod) → Adapter RXD
– Basys3 JA2 (Pmod) → Adapter TXD
– Basys3 GND (Pmod) ↔ Adapter GND
– Optional 3.3 V reference: Basys3 3V3 ↔ Adapter VCC (only if adapter requires external 3.3 V reference; most don’t)
2) Open a serial terminal at 9600 8N1
– Linux (screen example):
ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null
screen /dev/ttyUSB0 9600
– Linux (minicom example):
sudo apt-get install -y minicom
minicom -b 9600 -D /dev/ttyUSB0
– macOS (screen example):
ls /dev/tty.usb* /dev/tty.SLAB_USBtoUART* 2>/dev/null
screen /dev/tty.usbserial-XXXX 9600
– Windows (PuTTY/TeraTerm):
– Set Serial line: COMx
– Speed: 9600
– Data bits: 8
– Parity: None
– Stop bits: 1
– Flow control: None
3) Press and release the center button (mapped as rst_btn) on Basys 3
– LED0 should blink about once per second (heartbeat). This confirms the design is clocking.
4) Type characters in the terminal
– Each character you type should be echoed back immediately.
– Try sending:
– ASCII letters (A–Z, a–z)
– Numbers (0–9)
– Punctuation
– The echo should match exactly for each byte sent.
5) Validate bit timing (optional)
– If you have a logic analyzer or scope, probe JA1 (TX) relative to GND.
– Measure the width of a data bit on the TX line:
– Expected ~104.17 µs per bit (1 / 9600)
– Start bit is low, then 8 data bits (LSB first), then stop bit high.
– A small deviation (<<1%) is expected and acceptable.
6) Negative tests (optional)
– Change terminal speed to 115200 and observe garbled echo; switch back to 9600 to restore proper echo.
7) Reset behavior
– Pressing reset (center button) should pause operation and restart heartbeat after release. The echo resumes immediately.
Troubleshooting
- No echo appears
- Check that TX and RX are crossed:
- FPGA TX (JA1) must go to adapter RXD.
- FPGA RX (JA2) must come from adapter TXD.
- Verify GND is common between Basys 3 and the adapter.
- Confirm terminal is connected to the correct port at 9600 8N1, flow control none.
-
Ensure the bitstream programmed successfully (the LED heartbeat indicates the logic is running).
-
Garbled characters or intermittent errors
- Confirm serial settings: 9600, 8 bits, no parity, 1 stop bit, no flow control.
- Ensure the adapter uses 3.3 V logic levels (5 V can damage or cause undefined behavior).
- Cable length and noise: keep jumpers short; avoid running near noisy sources.
-
Try pressing reset to re‑synchronize, then type single characters slowly to validate stability.
-
Build issues
- Vivado not found: ensure Vivado 2023.2 WebPACK is installed and on PATH.
- License error: WebPACK covers Artix‑7 xc7a35t; select part xc7a35tcpg236‑1 in the script (already set).
-
Constraint errors: double‑check that XDC port names match the Verilog top ports.
-
Cannot program the board via script
- On Linux, you may need udev rules or to run Vivado as a user in the dialout/plugdev groups.
- Try the GUI Hardware Manager to confirm connectivity.
-
Close any other application using the cable (e.g., Digilent Adept or another Vivado instance).
-
Reset button mapping
-
If center button isn’t U18 on your XDC, open the Basys 3 Master XDC from Digilent and confirm the BTN_CENTER pin. Update the XDC mapping accordingly.
-
Timing failures (unlikely for this simple design)
- Verify the create_clock constraint exists and matches 10.000 ns (100 MHz).
- The reports in build/post_impl_timing.rpt should show no setup/hold violations.
Improvements
- Use on‑board USB‑UART instead of Pmod
- The Basys 3 includes an on‑board USB‑UART bridge connected to specific FPGA pins.
- Download the “Basys 3 Master XDC” from Digilent and uncomment the UART section. Replace the JA1/JA2 pin assignments in your XDC with the on‑board USB‑UART pins (named uart_rxd/uart_txd in the XDC). Keep IOSTANDARD LVCMOS33.
-
Rebuild and program. Then use the same USB cable for both JTAG and UART (the board enumerates two interfaces).
-
Add robust RX with oversampling
-
Implement 16x oversampling and majority vote for improved noise immunity and better tolerance for clock mismatches.
-
Add TX/RX FIFOs
-
Useful for buffering bursts from the host and for later integration into larger designs.
-
Parameterize baud at runtime
-
Add a simple register interface to program the baud divider at runtime; integrate with switches/LEDs or AXI4‑Lite.
-
Add error detection
-
Detect framing errors (stop bit low), overrun, or break conditions and visualize via LEDs or a status register.
-
Extend to a mini shell
-
Implement simple commands parsed from the serial input (e.g., ‘h’ for help, ‘v’ for version, ‘l’ to toggle LEDs).
-
Instrumentation
- Add an Integrated Logic Analyzer (ILA) core to capture RX/TX waveforms internally for learning and debug.
Final Checklist
- Prerequisites
- Vivado WebPACK 2023.2 installed and on PATH
- USB‑UART adapter drivers installed
-
Serial terminal available (PuTTY, TeraTerm, screen, or minicom)
-
Materials
- Basys 3 (Xilinx Artix‑7 XC7A35T‑1CPG236C)
- Micro‑USB cable
- 3.3 V USB‑to‑TTL UART (FT232RL/CP2102/CP2102N)
-
Jumper wires
-
Setup/Connection
- JA1 → Adapter RXD
- JA2 → Adapter TXD
- JA GND ↔ Adapter GND
- Optional JA 3V3 ↔ Adapter VCC (only if needed)
-
Basys 3 powered and connected via micro‑USB
-
Files
- src/uart_rx.v
- src/uart_tx.v
- src/top_uart_echo.v
- constraints/basys3_uart_ja.xdc
-
scripts/build.tcl
-
Build/Flash
- Run: vivado -mode batch -source scripts/build.tcl
- Confirm bitstream generation and successful programming
-
LED0 heartbeat visible
-
Validation
- Open terminal at 9600 8N1
- Type characters and observe correct echo
-
Optional: scope/logic analyzer confirms ~104.17 µs bit width
-
Troubleshooting
- TX/RX cross‑check and GND common
- 3.3 V logic only
- Correct COM port/device and 9600 8N1
- XDC port names/pins correct
If each item is satisfied, your Basys 3 is successfully running a UART echo at 9600 bps using Pmod JA, built and programmed entirely from the Vivado WebPACK command line.
Find this product and/or books on this topic on Amazon
As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.



