A few months ago, we looked at GitHub Copilot. The tool uses AI to assist with the coding process and it felt a little like pair programming with your development machine.
Recently, OpenAI released ChatGPT which received a lot of attention on Twitter, LinkedIn, and a whole host of other locations. Obviously a lot of people were trying its ability with software languages and I noticed few people tried it with Verilog. I thought it would be a valuable exercise to see how ChatGPT writes some simple to more complex VHDL and try to actually deploy it on a simple target board like the Arty S7-50.
To get started, I asked ChatGPT to give me the code to flash a LED at 0.5 HZ when running from a 100 MHz Clock. This should be simple for it to implement however, the first code output contained an error because the output signal cannot be read back internally to toggle it. The toggle rate would also be incorrect.
-- VHDL code for a 100 MHz counter that toggles an LED twice a second
library ieee;
use ieee.std_logic_1164.all;
entity counter is
port(
clk : in std_logic;
led : out std_logic
);
end entity;
architecture behavioral of counter is
signal counter_signal : integer range 0 to 49999999 := 0;
begin
process(clk)
begin
if rising_edge(clk) then
if counter_signal = 49999999 then
counter_signal <= 0;
else
counter_signal <= counter_signal + 1;
end if;
end if;
end process;
-- toggles the LED on and off every time the counter overflows
led <= not led when counter_signal = 0 else '0';
end architecture;
Of course, I suggested that the output could not be read back and to use a signal to provide this feature.
-- VHDL code for a 100 MHz counter that toggles an LED twice a second
-- using a flip-flop for the LED signal
library ieee;
use ieee.std_logic_1164.all;
entity counter is
port(
clk : in std_logic;
led : out std_logic
);
end entity;
architecture behavioral of counter is
signal counter_signal : integer range 0 to 49999999 := 0;
signal led_signal : std_logic := '0';
begin
process(clk)
begin
if rising_edge(clk) then
if counter_signal = 49999999 then
counter_signal <= 0;
else
counter_signal <= counter_signal + 1;
end if;
end if;
end process;
-- uses the counter_signal to clock the flip-flop, toggling its output on
-- and off every time the counter overflows
led_signal <= not led_signal when counter_signal = 0 else led_signal;
-- assigns the flip-flop's output to the led signal
led <= led_signal;
end architecture;
Now the code looks pretty straight forward and, while it’s not how I would have written it, it was functional so I decided to try it in the Arty board. How it assigns LED_SIGNAL creates a latch not a flip flop as I requested but it was functional so tried it on the hardware.
I created an IP Integrator design because I wanted to add to it with further requests. When running on the hardware, this did blink the LED0 on the Arty S7-50 board as the expected.
The next question I asked was for the ChatGPT to generate the counter which could count to 87. Once the code was generated, I asked for a test bench to be created.
-- VHDL counter that resets when it reaches 87
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity counter is
port (
clk : in std_logic; -- clock input
rst : in std_logic; -- reset input
en : in std_logic; -- enable input
cnt : out std_logic_vector(7 downto 0) -- counter output
);
end counter;
architecture rtl of counter is
signal cnt_internal : unsigned(7 downto 0) := (others=>'0');
begin
-- counter process
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then
-- reset counter to 0 on rising edge of clock
cnt_internal <= (others => '0');
elsif en = '1' then
-- increment counter on rising edge of clock
if cnt_internal = 87 then
-- reset counter to 0 when it reaches 87
cnt_internal <= (others => '0');
else
cnt_internal <= cnt_internal + 1;
end if;
end if;
end if;
end process;
cnt <= std_logic_vector(cnt_internal);
end rtl;
-- VHDL test bench for counter that resets when it reaches 87
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb_counter is
end tb_counter;
architecture tb of tb_counter is
-- component declarations
component counter is
port (
clk : in std_logic;
rst : in std_logic;
en : in std_logic;
cnt : out std_logic_vector(7 downto 0)
);
end component;
-- signal declarations
signal clk : std_logic := '0';
signal rst : std_logic := '1';
signal en : std_logic := '0';
signal cnt : std_logic_vector(7 downto 0) := (others => '0');
begin
-- instantiate the counter
UUT : counter
port map (
clk => clk,
rst => rst,
en => en,
cnt => cnt
);
-- clock process
clk_process : process
begin
clk <= '0';
wait for 10 ns;
clk <= '1';
wait for 10 ns;
end process;
-- test stimulus
stimulus : process
begin
-- reset the counter
rst <= '1';
wait for 100 ns;
rst <= '0';
-- wait a few clock cycles
wait for 30 ns;
-- enable the counter
en <= '1';
-- wait until the counter reaches 87
wait until cnt = std_logic_vector(to_unsigned(87,8));
-- disable the counter
en <= '0';
-- wait a few more clock cycles
wait for 30 ns;
-- end the simulation
wait;
end process;
end tb;
The main code had issues because it expected the std_logic_vector to be able to increment. As we know though, it cannot. You need to use the numeric_std types or include the std_logic_arith library. It also tried to read back the output vector in a similar manner to the initial LED example.
The only minor issue with the test bench code was a missing type conversion on the line
wait until cnt = 87;
This needed to be
wait until cnt = std_logic_vector(to_unsigned(87,8));
Normally, I would have asked ChatGPT to correct these errors but since the server was heavily loaded, I did it manually.
Running the simulation shows that the code does exactly what is asked of it. Notice it does not reset and continues holding it count at 87 . This is exactly like I asked and a lesson in clarity of requirements when working with tools like ChatGPT.
The next question I asked the GPTChat was to see if it could create me a simple UART which would receive eight bits and latch them to the LED outputs on the RGB LEDS. We need six bits to do this so it makes for a simple application.
The first UART code generated was not going to function correctly because it did not consider the sampling of the bits. Curiously, it also forgot to include the basic IEEE Library and use of std_logic_1164 and numeric_std which it had done for the first example though these can be easily added in.
-- VHDL UART with 100 MHz clock and 115200 baud rate
-- Declare entity and ports
entity uart is
port (
clk_in : in std_logic; -- 100 MHz clock input
rx_in : in std_logic; -- RX input for receiving data
led_out : out std_logic_vector(7 downto 0) -- 8 bit LED output
);
end entity uart;
-- Declare architecture
architecture rtl of uart is
-- Declare signals
signal rx_reg : std_logic_vector(7 downto 0) := (others => '0'); -- RX register for storing received data
signal bit_cnt : integer range 0 to 11 := 0; -- Bit counter for tracking current bit number
signal start_rcv: std_logic := '0'; -- Start receive signal for beginning data reception
begin
-- Clock process
process(clk_in)
begin
if rising_edge(clk_in) then
if start_rcv = '1' then
-- Increment bit counter and store received data in register
if bit_cnt < 11 then
rx_reg <= rx_reg(6 downto 0) & rx_in;
bit_cnt <= bit_cnt + 1;
else
-- When all bits are received, reset bit counter and start receive signal
bit_cnt <= 0;
start_rcv <= '0';
end if;
else
-- Check for start bit and set start receive signal if detected
if rx_in = '0' then
start_rcv <= '1';
end if;
end if;
end if;
end process;
-- Set LED output to received data
led_out <= rx_reg;
end architecture rtl;
I pointed out this error to ChatGPT and asked it to correct it.
The code generated this time looked a little more plausible, but it is still not going to work. After a long chat with GPT I realised this would be the best I could get from it so it would need to be modified to work. The main issue was that the code kept resetting the counter because it had no understanding of when reception of the data stream had started.
library ieee;
use ieee.std_logic_1164.all;
entity uart is
port (
clk: in std_logic;
rx: in std_logic;
led: out std_logic_vector(7 downto 0)
);
end uart;
architecture behavioral of uart is
-- Baud rate divisor (100 MHz / 115200 baud)
constant BAUD_DIVISOR: integer := 858;
-- Counter to track baud rate
signal baud_counter: integer range 0 to BAUD_DIVISOR;
-- Received byte buffer
signal rx_buffer: std_logic_vector(7 downto 0) :=(others => '0');
-- Start bit detection flag
signal rx_start: std_logic := '0';
-- Received byte buffer valid flag
signal rx_valid: std_logic := '0';
begin
-- Increment and reset baud rate counter on each clock edge
process(clk, rx_start, rx_valid)
begin
if rising_edge(clk) then
if rx_start = '1' then
baud_counter <= 0;
elsif baud_counter = BAUD_DIVISOR - 1 then
baud_counter <= 0;
else
baud_counter <= baud_counter + 1;
end if;
end if;
end process;
-- Shift in received bits on each baud rate clock edge
process(clk, rx)
begin
if rising_edge(clk) then
if baud_counter = 0 then
-- Shift in received bit
rx_buffer <= rx_buffer(6 downto 0) & rx;
end if;
end if;
end process;
-- Detect start bit and valid byte reception on each clock edge
process(clk, rx)
begin
if rising_edge(clk) then
if rx_start = '0' and rx = '0' then
-- Start bit detected
rx_start <= '1';
elsif rx_start = '1' and rx_buffer(7) = '1' then
-- Valid byte received, set LED bank
led <= rx_buffer;
rx_start <= '0';
end if;
end if;
end process;
end architecture behavioral;
I then asked it to create a test bench to test its code and my eventual fixed code.
The test bench code was pretty poor and not able to correctly time the UART bit period to inject a test vector. Here I am giving GPT the benefit of the doubt as my instruction required it to be able to remember critical elements of the previous design. This is something which would be expected if working with another engineer but it might be too much for AI Chat at the moment.
I had to twiddle both the test bench and the UART code to get it to work. As a simple test I was injecting 0xAA as the data byte.
You can see in my comparisons below between the final code (left) and the original code (right) that there was an entire process which needed removing.
I had plans to increase this to a UART to AXI bridge but I decided at this point the UART was pushing it so I stopped the experiment.
Overall I was impressed with ChatGPT in that it can generate and iterate code as though you are talking to another engineer developing in the office. It does feel like your talking to a pretty new FPGA developer though, having to spell things out and make comments to refine what you want and it is pretty limited in its capabilities.
That said, it can give you a great starting point for development and a workable framework in a few minutes while eventually taking over the code and finessing it. It was also great fun (and a little frustrating) trying to explain exactly what I wanted.
This is impressive for a research preview version and I’m sure it will only get better in the future.
Workshops and Webinars
Enjoy the blog why not take a look at the free webinars, workshops and training courses we have created over the years. Highlights include
Ultra96, MiniZed & ZU1 three day course looking at HW, SW and Petalinux
Arty Z7-20 Class looking at HW, SW and Petalinux
Mastering MicroBlaze learn how to create MicroBlaze solutions
HLS Hero Workshop learn how to create High Level Synthesis based solutions
Embedded System Book
Do you want to know more about designing embedded systems from scratch? Check out our book on creating embedded systems. This book will walk you through all the stages of requirements, architecture, component selection, schematics, layout, and FPGA / software design.
We designed and manufactured the board at the heart of the book! The schematics and layout are available in Altium here
Learn more about the board (see previous blogs on Bring up, DDR validation, USB, Sensors) and view the schematics here.
Sponsored by AMD Xilinx
Comments