Saturday 1 February 2020

Rendering Linux Console to VGA screen: Part 2

Foreword

In the previous post we managed to compile the Simple Frambuffer driver and got it load on Linux startup on a Zybo board.

We also found that once this framebuffer  driver has loaded, Linux automatically utilises this framebuffer as part of a new console. We confirmed this by inspecting the contents of the framebuffer in memory, finding that Linux rendered the contents of the startup console into it.

In this post we will be developing the FPGA side of the solution that will render the console to a VGA screen.

FPGA code tweaks

If we have a look at our current C64 FPGA module, we see that there already exist code that will render the contents of a framebuffer in memory to a VGA screen.

Throughout the course of this Blog-series I haven't really provided full source code listings for the C64 module as I went along. One can, however, get a big chunk of the code on the following github repo: https://github.com/ovalcode/c64fpga.

The code in previous mentioned Github Repo, doesn't contain the code explained in recent posts, but is enough to render contents from a framebuffer in memory and rendering it to a VGA screen.

In addition, this code will only render on a small portion of a screen big enough to fit the contents of a C64 screen. Preferably we would want to render on the full screen when rendering a Linux console, so we need to make some minor tweaks to the code on this Github Repo.

Let us start by having a look at the files in this Github Repo that are relevant to VGA rendering. All of these files are located in ip/vga_block_c64 and are as follows:

  • GrayCounter.v
  • aFifo.v
  • burst_read_block.v
  • fifo.v
  • sync_dual_port_ram.v
  • vga.v

From this list of files it is only the file vga.v that we need to make changes to. Let us have a look at the extend of the changes we need to make to this file.

First change that we should do is to make the image to display fill the whole screen, which ,in my case, is a screen with resolution 1360x768:

...
aFifo
  #(.DATA_WIDTH(16))
  my_fifo
     //Reading port
    (.Data_out(out_pixel_buffer), 
           .ReadEn_in((vert_pos_next > 0)  & (vert_pos_next < 766) &
                           (horiz_pos_next > 0) & (horiz_pos_next < 1361)),

     .RClk(clk),        
     //Writing port.  
     .Data_in(/*out_pixel*/shift_reg_16_bit[31:16]),  
     .Full_out(buffer_full),
     .WriteEn_in((state_shift_reg == STATE_16_SHIFT_STORED | state_shift_reg == STATE_16_SHIFT_SHIFTED) & !buffer_full),
     .WClk(clk_axi),
  
     .Clear_in(trigger_restart_state == RESTART_STATE_RESTART)
     
     );
...
 assign out_pixel_buffer_final = (vert_pos > 0)  & (vert_pos < 766) &
                                (horiz_pos > 0) & (horiz_pos < 1361)
                                ? out_pixel_buffer : 0;
...

These changes will read and display image data when we are in the full visible area of the screen and will render images correctly with resolution similar to that of my screen.

One problem I had when displaying a picture full screen, was that the first pixel always showd a garbage pixel value, causing the whole image to be displayed incorrectly on the screen.

This garbled pixel was caused by reading from the AXI buffer during the period when this buffer transitions from been empty to been filled with one item.

We can remedy the situation by waiting for a couple of clock cycles when the AXI buffer have been filled with the first item:

...
always @(posedge clk_axi)
begin
  axi_buffer_empty3 <= axi_buffer_empty_temp;
  axi_buffer_empty2 <= axi_buffer_empty3;
  axi_buffer_empty1 <= axi_buffer_empty2;
  axi_buffer_empty <= axi_buffer_empty1;
end
...
burst_read_block my_read_block(
          .clk(clk_axi),
          .reset(reset),
          .restart(trigger_restart_state == RESTART_STATE_RESTART),
          //.write_data(ram[current_address]), 
          .count_in_buf(),
          //output src ready
          //-----------------------------------------
          .ip2bus_mst_addr(ip2bus_mst_addr),
          .ip2bus_mst_length(ip2bus_mst_length),
          .ip2bus_mstrd_d(ip2bus_mstrd_d),
          .ip2bus_inputs(ip2bus_inputs),
          .ip2bus_otputs(ip2bus_otputs),
          .axi_d_out(axi_read_data),
          .empty(axi_buffer_empty_temp),
          .read(read_from_axi),
            );
...
assign read_from_axi = !axi_buffer_empty & !buffer_full & (state_shift_reg == STATE_16_SHIFT_IDLE | state_shift_reg == STATE_16_SHIFT_SHIFTED) ? 1 : 0;
...

Here we see that axi_buffer_empty forms part of the decision when to trigger a read action on the AXI_BUFFER. By delaying this signal with the help of a couple of cascaded flip-flops, we effectively delaying reading from this buffer when it transitions to non-empty.

One final thing we should change is the start address of the frame we read from SDRAM for display on screen.

The read addreses to SDRAM is also generated within burst_read_block.v. If you have a look within this module you will see that it is currently been hardcoded to 0x200000.

From the previous post, you will remember that the start address of the framebuffer we use is at address 0x1fb00000.

So, we can either change this hardcoded value in burst_read_block.v, or you can add a port to this module in which you you provide this address:

burst_read_block my_read_block(
          .clk(clk_axi),
          .reset(reset),          
          .restart(trigger_restart_state == RESTART_STATE_RESTART),           
          .count_in_buf(),
          //output src ready
          //-----------------------------------------
          .ip2bus_mst_addr(ip2bus_mst_addr),
          .ip2bus_mst_length(ip2bus_mst_length),
          .ip2bus_mstrd_d(ip2bus_mstrd_d),
          .ip2bus_inputs(ip2bus_inputs),
          .ip2bus_otputs(ip2bus_otputs),
          .axi_d_out(axi_read_data),
          .empty(axi_buffer_empty_temp),
          .read(read_from_axi),
          .start_address(32'h1fb00000)
            );

One could go one step further where you service this port out of the C64 module, and then add the necessary logic where we could set the address from Linux. We will however not do it in this post.

Loading the FPGA design on Zybo board at startup

With the FPGA design created in the previous section, let us see if we can get it loaded when we boot the Zybo board into Linux.

To start, a bit bitstream will need to be created for the FPGA design and exported to an SDK project.

Next we need to create an FSBL as I explain in a previous post.

With the FSBL created, we need to encapsulate it together with our FPGA bistream, as well the ubootloader into a single file.

We have done something similar in a previous post, as explained here. The resulting boot.bin file, however, didn't contain an FPGA bistream file.

To create a boot.bin file containing an FPGA bitstream, we need to use a boot.bif file that looks as follow:

image : {
        [bootloader]fsbl_vga.elf
        design_1_wrapper.bit
        u-boot.elf
}

Of course, design_1_wrapper.bit is your FPGA bitstream.

As previously, we create the boot.bin file with the following command:

bootgen -image boot.bif -o i boot.bin

Copy the resulting boot.bin back to the SDCard from which your Zybo boards boots from.

The End Result

Let us have a look at the end result.

I have inserted the SDCard back into the Zybo board, hooked it up to a VGA screen and attached a USB keyboard.

I got hold of a 5V power adaptor which attached to the Zybo board via its barrel connector. I have also set the appropriate jumper setting so that the Zybo board draws power from this device.

The following video shows the end result:


In this video I start off by switching on the Zybo board and a couple of seconds later some boot messages appear, after which we are presented with the Linux command prompt.

At the command prompt I clear the screen with the clear command, after which I show directory contents of the /etc directory.

In Summary

In this post we managed to boot the Zybo board completely standalone and watch the console output on an attached VGA monitor.

In the next post we will investigate how to capture key-up and key-down events in Linux from a keyboard.

We will use this information in coming posts so we can interface the Linux running on the Zybo board with our C64 FPGA module, so that Linux can delegate keystrokes to it.

Till next time!

2 comments:

  1. Funny, so much work to get a console running natively on Linux; something that seems to be so easy on a PC.
    But it provides support for many features, as we will see in the next episodes.

    Very interesting Johan, thanks for sharing!

    ReplyDelete
  2. It is indeed a pain, all the hoops one has to jump through, just to get Linux running on a Zybo board!

    This pain is also partly due to the fact there is no prebuilt Linux Images that you can download for the Zybo board. PetaLinux is probably getting very close to a prebuilt image for the Zybo board.

    PetaLinux, however, will only give you a Linux console via SSH or a USB-Serial connection. It will not provide you with an implementation at all for outputting the Linux console via the VGA port.

    ReplyDelete