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
Sponsored by AMD Xilinx