This repository contains VHDL project for a 2-wheels drone, which task is to use color detection sensors to navigate itself- following the black line.
This project targets Basys3 FPGA. This device is the driver for a 2-wheel drone, which is equipped with color detection sensors. These sensors are used to determine where is the black line that the drone has to follow. The black line defines the path.
The drone is initially in the IDLE state and is awaiting for a button press which sets it to RUNNING state. Since the
button trigger drone begins to follow the black line which is underneath it- in case of no black line beneath the robot it pauses (awaiting in the RUNNING mode). As long as robot can detect the black line it follows it effectively performing loops according to the track geometry. The second button press returns drone to the IDLE state. Moreover the robot is equipped with a simple screen (check Basys3 documentation), which displays robot's current speed.
The source code for the drone is entirely done in VHDL. The diagram below presents relations between modules.
System's comoponents are separated in three principal packages:
control_pkq: contains two FSMs which control movement of the dronedrone_utils_pkg: contains generic IPs (pwm,edge_detector,btn_debouncer) used to directly interact with the hardware and electric motorsscreen_utils_pkq: contains 7-segment display driver module and a function for controlling the display
The application logic is controller by two state machines:
start_stop_FSM: decides when the robot starts and stops according to the push of the buttonmovement_FSM: while the robot is allowed to move it responds to black line sensors to determine th epath which follows the line
Start/Stop FSM responds to btn_pressed trigger signal by switching between two states:
IDLERUNNING
Signal i_btn_pressed is the output of edge_detector. State RUNNING activates movement_FSM to leave its IDLE state.
The idea behind following the path of the black line is to keep that line centrally underneath the drone. As already mentioned the drone has two sensors, which indicate when the black color is detected- this indicates a turn. When both sensors indicate 0 this means the robot is exactly underneath the line. This FSM controls the movement of the drone with four states:
IDLE: robot remains in place awaiting fori_is_runningsignal fromstart_stop_FSMFORWARD: robot is moving forward when both sensors indicate 0; 50% of power to the enginesT_LEFT: robot is turning to the left; right engine receives 15%, left engine receives 90% of powerT_RIGHT: robot is turning to the right; right engine receives 90%, left engine receives 15% of power
This FSM doesn't have any knowledge regarding the pwm parameters. It outputs a custom t_pwm_duty_cycle (defined inside control_pkg), which gets interpretted into an actual duty cycle values inside drone_top. Thanks to that movement_FSM and pwm are decoupled.
User can customize, if provided sensors indicate the black line as either logical 0 or 1.
GENERIC (
G_BLACK_LINE : STD_LOGIC := '1' -- specify the value which color sensors
-- provide when the black color is detected.
-- logic 1 by default
);btn_debouncer is the very first element in the chain of modules responsible for running the drone. Its task is to perform two activities:
- Apply double flip-flop approach to mitigate the potential metastability when user presses the start button
- Debounce the button- make sure that the signal triggered by the pressed button is long enough that it can be passed to the edge detection without casuing false triggers to the system
When the button is pressed it secures with the double flip-flop approach the stable signal. When the signal indicates that the button has been pressed it proceeds the count down for the specified duration and then checks again for the value of it, if the button indicates logical 1 when the counter is 0, then the triggering signal is sent to the edge_detector.
This module allows for customization in terms of the necessary time for a stable button press and the main clock frequency.
GENERIC (
G_DEBOUNCE_TIMEOUT_MS : POSITIVE := 20; -- button debounce time delay (by default 20ms)
G_CLK_FREQ_HZ : POSITIVE := 100_000_000 -- for this project it is assumed 100MHz
);edge_detector is responsible for determining, if the button was pressed by evaluating the rising/falling edge of the input signal i_signal. This module assumes that the i_signal is stable, and this is guaranteed by btn_debouncer. It determines rising/falling edge by initially assigning i_signal to s_prev_i_signal and then comparing two signals.
This module allows for customization in terms of rising/falling edge detection.
GENERIC (
G_RISING_EDGE : BOOLEAN := true -- detect rising edge by default
);pwm generates actual steering signal to an electric motor. In the drone_top module one pwm module is instantiated per motor.
This implementation is based on the two counters approach. The first counter, defined as an internal signal- clk_cnt, is used to derive lower frequency control signal for the pwm_process. Given we wish to have PWM module operating in 5kHz frequency and the main clock is 100MHz we need to perform the following calculations:
100MHz/5kHz=20000 -> full PWM counting defined by resolution of G_PWM_BITS
must take 20000 cycles of 100MHz clock
20000/lenght(G_PWM_BITS = 256)=78 -> signle PWM counter step (o_pwm_cnt) happens
every 78 100MHz clock cycles
The logic which decides when o_pwm_cnt increments and when PWM issues an impulse happens (as for the example above) every 78 100MHz clock cycles.
Customization is done via the generic map.
GENERIC (
G_PWM_BITS : INTEGER; -- specifies the resolution
-- if equal to 8 bits the PWM counter will
-- count from 0 to 255
G_CLK_DIV : POSITIVE := 78 -- clock divider, it specifies how many
-- "fast clk ticks" equal one "slow clk tick"
-- set by default to the value allowing to
-- create 5kHz signal from 100MHz clock
);This utility package is responsible for providing driver module for the 7-segment display in order to monitor the velocity of each of robot's wheels. It declares drone_controller module which is responsible for setting two robot's output ports:
an: selects which digit to updateseq: selects what value to display per selected digit
This module is meant to operate with 1ms refresh rate per digit, which means that an enitre screen updates within 4ms.
The key element of display_controller is:
p_screen_timer : PROCESS (i_clk, i_rst_n)
BEGIN
IF (i_rst_n = '0') THEN
r_counter <= (OTHERS => '0');
ELSIF rising_edge(i_clk) THEN
r_counter <= r_counter + 1;
END IF;
END PROCESS p_screen_timer;
s_digit_select <= STD_LOGIC_VECTOR(r_counter(r_counter'high DOWNTO r_counter'high - 1));Where process p_screen_timer counts until around 500000 FPGA clock cycles (equivalent of 4ms). In a meantime, s_digit_select selects two MSBs and can choose between: 00, 01, 10, 11. Each number has to be updated in a respective 1ms interval. During the first interval: 0ms-1ms MSBs equals 00 so the rightmost digit is updated, 1ms-2ms MSBs equals 01 etc.. until r_counter overflows at the end with MSBs being 11.
drone_top unifies all the packages into the final design. Its important element is the p_pwm_decoder, which interprets movement_FMS instructions (provided via signals: s_fsm_cmd_left, s_fsm_cmd_right) into an actual duty cycle, which is then provided to pwm modules.
-- Decoding FSM commands and generating the final PWM signal
p_pwm_decoder : PROCESS (s_fsm_cmd_left, s_fsm_cmd_right) IS
BEGIN
-- left motor command
CASE s_fsm_cmd_left IS
WHEN DUTY_CYCLE_0 => s_pwm_duty_left <= C_DUTY_0_PCNT;
WHEN DUTY_CYCLE_15 => s_pwm_duty_left <= C_DUTY_15_PCNT;
WHEN DUTY_CYCLE_50 => s_pwm_duty_left <= C_DUTY_50_PCNT;
WHEN DUTY_CYCLE_90 => s_pwm_duty_left <= C_DUTY_90_PCNT;
END CASE;
-- right motor command
CASE s_fsm_cmd_right IS
WHEN DUTY_CYCLE_0 => s_pwm_duty_right <= C_DUTY_0_PCNT;
WHEN DUTY_CYCLE_15 => s_pwm_duty_right <= C_DUTY_15_PCNT;
WHEN DUTY_CYCLE_50 => s_pwm_duty_right <= C_DUTY_50_PCNT;
WHEN DUTY_CYCLE_90 => s_pwm_duty_right <= C_DUTY_90_PCNT;
END CASE;
END PROCESS p_pwm_decoder;The project consists of the following directories:
build/: stores ghdl relevant build files when targets executelog/: stores logs produced by each targetscripts/: stores TCL scripts called from Makefile (TODO for Vivado batch mode integrations)simulation/: stores simulation results per run testbenchsrc/: stores HDL and tesbenches source code as well as constraint file
There is one main Makefile specyfying all the targets. The simulation assumes using GHDL. User can run a single testbench or all of them at once.
- Single testbench:
make <tb_name> - All testbenches:
make all, this implementation supports parallel execution so it is possible to do for example:make -j4 all
Due to the implementation of ghdl imports via ghdl -i... by default ghdl will try to import all the files listed inside src/hdl/ this includes the files which might be buggy or incomplete. User can exclude them by editing the Makefile:
IGNORE_SRCS := \
$(SRC_DIR)/buggy_file.vhd \
$(SRC_DIR)/incomplete_file.vhdAfter succesfull execution of a testbench its results are stored inside: simulation/tb_name as *.vcd files. Inside log/tb_name there are simulation logs per testbench and in build/tb_name there are some build artefacts.
- enable Vivado based compilation using TCL scripts
- enable Vivad batch mode build and device flash
- push such changes and modify my other project which tries that for Verilog- the project