Приказивање постова са ознаком fpga. Прикажи све постове
Приказивање постова са ознаком fpga. Прикажи све постове

уторак, 30. јун 2020.

SPI interface on my FPGA computer

This is a follow-up of my original FPGA computer post.

SPI interface is a kind of a standard when it comes to connecting various peripherals to a computer (or, at least to a microcontroller). There is also I2C interface, but I will focus on the SPI in this post.

SPI stands for Serial Peripheral Interface. It is organized as a master-slave communication. If we presume that our FPGA computer is master, then the peripheral will be slave.

It usually has four important pins:
1. MISO (Master In Slave Out) - a wire which is used to transport data from slave to the master device,
2. MOSI (Master Out Slave In) - a wire which is used to transport data from master to the slave device,
3. SCL - clock (all the data transport is synchronized using this clock line), and
4. SS (Slave Select) - when active, the slave is selected (sometimes it is called CS - chip select). With this wire, it is possible to connect several peripherals to the same three mentioned wires (MISO, MOSI and SCK) and to have separate SS wires to each peripheral.

Why did I choose to use the SPI on my computer? First of all, SD cards have SPI built-in. This means that every SD card is actually a SPI slave device. Next, I use the ENC28J60 Ethernet module for my Arduino/ESP32/RaspberryPi Zero devices for the Ethernet connectivity. That module has SPI interface, too.


How did I integrate SPI into my FPGA computer. I have found a very nice implementation in Verilog here:
https://github.com/nandland/spi-master

BTW, that guy has excellent YouTube channel here: https://www.youtube.com/channel/UCsdA-aNqtMA1_2T15aXePWw

Next I had to integrate that module into my FPGA computer. I have decided to allocate an interrupt for the incoming data from the SPI and to ignore the module-controlled SS pin (I will manually activate SS signal from code, instead of letting that job to the SPI module):
// ####################################
// SPI Master instance
// ####################################
wire spi_start;
wire [7:0] spi_in;
reg [7:0] spi_out;
wire spi_ready;
wire spi_received;
reg [7:0] spi_in_r;
reg fake_CS;

SPI_Master_With_Single_CS spi0 (
.i_Clk(clk100),
.i_Rst_L(KEY[0]),
.i_TX_Count(1),
.i_TX_DV(spi_start),
.o_RX_Byte(spi_in),
.i_TX_Byte(spi_out),
.o_RX_DV(spi_received),
.o_TX_Ready(spi_ready),

.o_SPI_MOSI(gpio0[32]),
.i_SPI_MISO(gpio0[30]),
.o_SPI_Clk(gpio0[28]),
.o_SPI_CS_n(fake_CS)
);


The code above creates a SPI module named spi0 and connects it to a set of wires and registers. Next, in the main interrupt part, when the spi_received wire goes high (a byte has arrived on SPI), the IRQ_SPI interrupt is triggered:
// ##################### IRQ3 - SPI Master #####################
if (spi_received) begin
spi_in_r <= spi_in;
// if we have received a byte from the MISO,
  // we will trigger the IRQ#3
irq[IRQ_SPI] <= 1'b1;
end
else
begin
irq[IRQ_SPI] <= 1'b0;
end


In the CPU module, the IRQ_SPI interrupt causes processor to go to the predefined interrupt handler routine at the address of 56:
else if (irq_r[IRQ_SPI]) begin
// SPI byte received
pc <= 16'd56;
addr <= 16'd28;
irq_r[IRQ_SPI] <= 0;
end


All you have to do is to put some code at the address of 56 and to return from the interrupt handler routine using the IRET assembly instruction:

spi_irq_triggered:     push r0     ld.w    r0, [PORT_SPI_IN]   # PORT_SPI_IN.5_1, PORT_SPI_IN     ld.s    r0, [r0]    # _2, *PORT_SPI_IN.5_1     zex.s   r0, r0  # _3, _2     st.w    [received_byte], r0 # received_byte, _3    mov.w   r0, 1   # tmp29,     st.w    [received_from_slave], r0   # received_from_slave, tmp29     pop r0     iret

Now that I have the C compiler, the SPI interrupt handler routine can be written in C:
void init_spi()
{
    *SPI_HANDLER_INSTR  = 1;
    *SPI_HANDLER_ADDR   = (int)&spi_irq_triggered;
}

void spi_irq_triggered()
{
    received_byte = *PORT_SPI_IN;
    received_from_slave = 1;
    asm 
    (
        "mov.w sp,r13\npop r13\niret"
    );
}

In order to read the received byte, and to send some byte to the SPI, we need to implement some IO operations. As usual, I have done that in both direct and memory-mapped way. Here is the direct way using the IN and OUT assembly instructions:
// OUT [xx], reg
4'b0100: begin
`ifdef DEBUG
$display("%2x: OUT [%4d], r%-d",ir[3:0], data_r, (ir[15:12]));
`endif
case (mc_count) 
0: begin
// get the xx
addr <= (pc + 2) >> 1;
pc <= pc + 2;
mc_count <= 1;
next_state <= EXECUTE;
state <= READ_DATA;
end
1: begin
mbr <= data_r;
mc_count <= 2;
end
2: begin
case (mbr)
...
PORT_SPI_OUT: begin
spi_out <= regs[ir[15:12]];
spi_start <= 1'b1;
end
...
default: begin
end
endcase  // end of case (data)
mc_count <= 3;
end
3: begin
tx_send <= 1'b0;
spi_start <= 1'b0;
spi_start1 <= 1'b0;
state <= CHECK_IRQ;
pc <= pc + 2;
end
default: begin
end
endcase
end // end of OUT [xx], reg

What happens above? The OUT instruction is written in memory using four bytes. First two bytes are OPCODE of the instruction, and the second two bytes hold the port number (limiting the total number of available ports to 65536, but I think it is enough). 

In the first cycle (step 0) of the OUT instruction, the CPU sets the address to be read to be next two bytes after those two OPCODE bytes. Then the CPU waits for those two bytes to arrive (step 1). 

Then the CPU checks which IO port has been read from the memory, and of the port number is PORT_SPI_OUT, it means that we are trying to send some byte to the SPI, and the CPU sends the data to that port (step 2). In step 3 the CPU finishes sending and sets the next CPU state to be the IRQ check.

And, here is the memory-mapped IO way:
// Memory mapped IO
case (addr & 32'h3FFFFFFF)
...
PORT_SPI_OUT/2: begin
spi_out <= data_to_write;
spi_start <= 1'b1;
end
...
endcase

Memory-mapped is a bit simpler, but does the same job of sending a byte to the SPI.

OK, now that we have the working SPI interface, how can we use it to work with the SD card? I have made a Frankenstein-like code merging the original Arduino SD card code (written in C++) with some other pieces of code from the github in a way that now I have some elementary support for the SD cards. For example:

uint8_t sdcard_init(){
  writeCRC_ = errorCode_ = inBlock_ = partialBlockRead_ = type_ = 0;
  // 16-bit init start time allows over a minute
  uint32_t t0 = (uint32_t)get_millis();
  uint32_t arg;
   // must supply min of 74 clock cycles with CS high.
  for (uint8_t i = 0; i < 10; i++) spiSend(0XFF);

  chipSelectLow();

  // command to go idle in SPI mode
  while ((status_ = cardCommand(CMD0, 0)) != R1_IDLE_STATE) {
    if (((uint32_t)get_millis() - t0) > SD_INIT_TIMEOUT) {
      error(SD_CARD_ERROR_CMD0);
      goto fail;
    }
  }
 
  // check SD version
  if ((cardCommand(CMD8, 0x1AA) & R1_ILLEGAL_COMMAND)) {
    type(SD_CARD_TYPE_SD1);
  } else {
    // only need last byte of r7 response
    for (uint8_t i = 0; i < 4; i++) status_ = spiRec();
    if (status_ != 0XAA) {
      error(SD_CARD_ERROR_CMD8);
      goto fail;
    }
    type(SD_CARD_TYPE_SD2);
  }
  ... }

In the code above, we see that there are some spi-related functions, like spiSend() or spiRec(). Here are those:

void spiSend(int b)
{
    received_from_slave = 0;
    unsigned short int busy;
    do 
    { 
        busy = *PORT_SPI_OUT_BUSY;
    } while (busy);
    *PORT_SPI_OUT = b; //send the byte to the SPI
    
    do 
    { 
        busy = *PORT_SPI_OUT_BUSY;
    } while (busy);
}

uint8_t spiRec(void) {
    send_spi(0xFF);
    return read_spi();
}
int read_spi()
{
    while (!received_from_slave || *PORT_SPI_OUT_BUSY) 
    {
    }
    return received_byte;
}

Now, when we look at the spi_irq_triggered() function, we see that whenever that interrupt routine is triggered by the incoming byte from the SPI, that byte is stored in the received_byte variable. That byte is returned from the read_spi() function to the spiRec() function, and from that to the caller function.

OK, what next? How is this used? All of the interaction with the SD card is done by sending card commands and reading and writing 512 bytes of data, in so-called blocks:
uint8_t cardCommand(uint8_t cmduint32_t arg) {
  // end read if in partialBlockRead mode
  readEnd();

  // select card
  chipSelectLow();

  // wait up to 300 ms if busy
  waitNotBusy(300);

  // send command
  spiSend(cmd | 0x40);

  // send argument
  for (int8_t s = 24; s >= 0; s -= 8spiSend(arg >> s);

  // send CRC
  uint8_t crc = 0XFF;
  if (cmd == CMD0) crc = 0X95;  // correct crc for CMD0 with arg 0
  if (cmd == CMD8) crc = 0X87;  // correct crc for CMD8 with arg 0X1AA
  spiSend(crc);

  // wait for response
  for (uint8_t i = 0; ((status_ = spiRec()) & 0X80) && i != 0XFF; i++);
  return status_;
}

uint8_t readData(uint32_t block,
        uint16_t offsetuint16_t countuint8_tdst) {
  uint16_t n;
  if (count == 0return true;
  if ((count + offset) > 512) {
    goto fail;
  }

  #ifdef FAT_DEBUG
  printf("block: %d, offset: %d, count: %d\n", block, offset, count);
  #endif

  if (!inBlock_ || block != block_ || offset < offset_) {
    block_ = block;
    // use address if not SDHC card
    if (get_type()!= SD_CARD_TYPE_SDHC) block <<= 9;
    if (cardCommand(CMD17, block)) {
      error(SD_CARD_ERROR_CMD17);
      goto fail;
    }
    if (!waitStartBlock()) {
      goto fail;
    }
    offset_ = 0;
    inBlock_ = 1;
  }

  // skip data before offset
  for (;offset_ < offset; offset_++) {
    spiRec();
  }
  // transfer data
  for (uint16_t i = 0; i < count; i++) {
    dst[i] = spiRec();
//    printf("%x ", dst[i]);
  }

  offset_ += count;
  if (!partialBlockRead_ || offset_ >= 512) {
    // read rest of data, checksum and set chip select high
    readEnd();
  }
  return true;

 fail:
  chipSelectHigh();
  #if FAT_DEBUG
  printf("read data error code: %d\n", errorCode_);
  #endif
  return false;
}

uint8_t writeData(uint8_t tokenconst uint8_tsrc) {
  spiSend(token);
  for (uint16_t i = 0; i < 512; i++) {
    spiSend(src[i]);
  }
  spiSend(0xff);  // dummy crc
  spiSend(0xff);  // dummy crc

  status_ = spiRec();
  if ((status_ & DATA_RES_MASK) != DATA_RES_ACCEPTED) {
    error(SD_CARD_ERROR_WRITE);
    chipSelectHigh();
    return false;
  }
  return true;
}

uint8_t writeBlock(uint32_t blockNumberconst uint8_tsrcuint8_t blocking) {
  #if FAT_DEBUG
  printf("Write block number: %d\n", blockNumber);
  #endif
//  return true;
  // don't allow write to first block
  if (blockNumber == 0) {
    error(SD_CARD_ERROR_WRITE_BLOCK_ZERO);
    goto fail;
  }

  // use address if not SDHC card
  if (get_type() != SD_CARD_TYPE_SDHC) {
    blockNumber <<= 9;
  }
  if (cardCommand(CMD24, blockNumber)) {
    error(SD_CARD_ERROR_CMD24);
    goto fail;
  }
  if (!writeData(DATA_START_BLOCK, src)) {
    goto fail;
  }
  if (blocking) {
    // wait for flash programming to complete
    if (!waitNotBusy(SD_WRITE_TIMEOUT)) {
      error(SD_CARD_ERROR_WRITE_TIMEOUT);
      goto fail;
    }
    // response is r2 so get and check two bytes for nonzero
    if (cardCommand(CMD13, 0) || spiRec()) {
      error(SD_CARD_ERROR_WRITE_PROGRAMMING);
      goto fail;
    }
  }
  chipSelectHigh();
  return true;

fail:
  chipSelectHigh();
  return false;
}

Now that we are able to read and write 512-sized blocks, we need to figure out how the data is organized on SD cards. Well, the format is FAT32. That is an ancient format from Microsoft, but it is quite simple and is used everywhere.

The format can be found on Wikipedia and on this excellend blog post: https://codeandlife.com/2012/04/02/simple-fat-and-sd-tutorial-part-1/

So, if we want, for example, to list all files in the root folder, here is the code:
file_descriptor_t fd;
int next = 0;
while ((next = getDirEntry(&fd, next)) != 0)
{
    printf("%s %d bytes, cluster: %d (%d)\n"fd.dir_entry.filenamefd.dir_entry.filesizefd.curr_clusterfd.dir_entry.first_cluster);
}

The key code is in the getDirEntry() function:
uint32_t getDirEntry(file_descriptor_tfduint32_t index)
{
  int i,j;
  uint16_t cluster;
  uint32_t file_size;
  uint8_t b;
  uint8_t *buf = g_block_buf;
  char filename_upper[12];
  uint32_t counter = 0;

  for (i = 0; i < (dataStartBlock_ - rootDirStart_); i++)
  {
    b = readBlock(rootDirStart_ + i, g_block_buf);
    for(j = 0; j < 16; j++)
    {
      if (*(buf + j*32)==0 || *(buf + j*32)==0x2e || *(buf + j*32)==0xe5 || *(buf + j*32 + 0x0b) == 0xf)
      { 
        continue// free, or deleted file/folder, or phantom entry for long names?
        if (counter > index)
          return 0;
      }
      
      if(counter == index)
      {
        file_size = *(buf + j*32 + 0x1c);
        file_size += *(buf + j*32 + 0x1c + 1)<<8;
        file_size += *(buf + j*32 + 0x1c + 2)<<16;
        file_size += *(buf + j*32 + 0x1c + 3)<<24;
        cluster = *(buf + j*32 + 0x1a);
        cluster += *(buf + j*32 + 0x1a + 1) << 8;
        cluster += *(buf + j*32 + 0x14 + 0) << 16;
        cluster += *(buf + j*32 + 0x14 + 1) << 24;

        strncpy(filename_upper, (char*)(buf+j*32), 11);
        filename_upper[11] = '\0';

        // fill in dir_entry
        memmove(fd->dir_entry.filename, filename_upper, 12);
        fd->dir_entry.attributes = *(buf + j*32 + 0x0b);
        memmove(fd->dir_entry.unused_attr, buf + j*32 + 0x0c14);
        fd->dir_entry.filesize = file_size;
        fd->dir_entry.block = rootDirStart_ + i;
        fd->dir_entry.slot = j;
        fd->dir_entry.first_cluster = cluster;
        fd->curr_cluster = cluster;
        return counter + 1;
      } else if (counter > index) {
        return 0;
      }
      counter++;
    }
  }
  return 0;
}

The code above loads chunks of 512 bytes from the root directory start block, and then tries to iterate through the directory structure until it finds the right entry, given by its index. The directory structure is this:
typedef struct
{
  char filename[12];  /** The file's name and extension, total 11 chars padded with spaces. */
  uint8_t attributes;  /** The file's attributes. Mask of the FAT_ATTRIB_* constants. */
  uint8_t unused_attr[14]; /** Attributes in directory which are unused or unsupported */
  uint16_t first_cluster;     /** The cluster in which the file's first byte resides. */
  uint32_t filesize;   /** The file's size. */
  uint32_t block; /** The number of a block from the rootDirStart_ where this entry resides. */
  uint32_t slot; /** The number of the slot in the block where this entry resids. Each slot is 32 bytes large. */
dir_entry_t;


Since my FPGA computer is big endian, I couldn't just read bytes for file size and cluster address. Instead, I had to compute those numbers byte-by-byte.

Conclusion

Initial implementation of the SPI was simple enough. It is what you can do with it what matters. I was able to use the SPI to integrate SD card into my FPGA computer. That way, I don't need the Arduino/ESPP32 anymore to do the role of SD card reader, as I used to have.

уторак, 17. март 2020.

TinyBasic made for my FPGA platform

This is a follow-up of my original FPGA computer post.

In my previous post, I have described how I have modified GCC cross compiler made originally for the moxie platform to generate assembly code for my FPGA platform. I have used my new cross compiler to make a port of TinyBasic for my platform. I have downloaded TinyBasic C code and modified it to be a bit more programmer-friendly. That port can be found here:

https://github.com/milanvidakovic/FPGABasic

Besides standard BASIC commands, I had a freedom to invent my own commands and to play with them. First of all, I have created a MODE command which is used to set the video card mode:
0 - text mode
1 - graphics mode of 640x480x2 colors, and
2 - graphics mode of 320x240x8 colors.

Besides MODE command, I now have the following graphics commands:
- PLOT x, y, color
- LINE x1, y1, x2, y2, color
- CIRCLE x, y, r
- DRAW x, y, "TEXT"

I have also added two key-related functions: KEY() and ISKEY(). Both functions return virtual key that has been pressed, but the first is a blocking one - it waits until some key is pressed, while the other one just immediately returns the virtual code of a last key being pressed.

I have also played with the file system on my "hard disk". I have created following commands:
- DIR - lists the content of the "hard disk" root folder,
- LOAD PROGRAM.BAS - loads a BASIC program into the computer memory,
- SAVE PROGRAM.BAS - saves a BASIC program on the "hard disk"
- EXEC PROGRAM.BIN - loads and executes a binary executable
- SYS ADDRESS - executes a machine program loaded at the given address.

The BASIC now boots from the SD card and can be used immediately. Here is the video of the computer booting from the SD card into the BASIC:


субота, 14. март 2020.

Modifying GCC to work with my FPGA computer

This is a follow-up of my original FPGA computer post.

In this text I will talk about the modification of the GCC compiler in order to work with my FPGA platform. I wanted to make a cross-compiler that would be able to compile C programs for my FPGA platform. This post will describe how far I have reached. Currently, a modified GCC compiler produces assembly code for my platform as a result of C program compilation.

When I say a cross-compiler, I think of a compiler that would compile C code on my PC, but the executable would be for my FPGA computer. GCC already supports a lot of cross-compilers and all you have to do is to choose the appropriate target when building GCC. That way, you will build a cross-compiler that would produce an executable for the target platform. However, if you have created a new platform, then you need to add a cross-compiler code to the GCC, and then build a cross-compiler for that new platform. That step is very complicated. Trying to add your own platform into the GCC is almost impossible job if you have not done something like that already (I haven't).

I have found a very useful blog post that describes how GCC generates code:

https://kristerw.blogspot.com/2017/08/writing-gcc-backend_4.html

Besides that blog, there is a simple GCC cross-compiler that is present in the list of supported cross compilers - moxie. The author is Anthony Green and he has initially made a blog about modifying GCC for a fictional platform - moxie:

http://atgreen.github.io/ggx/

The project managed to become an official part of GCC and is now incorporated in the GCC source tree.

There is a sandboxed environment made for this platform - moxiebox. The code is on the github repository: https://github.com/jgarzik/moxiebox

I have forked that repo and added my own modification of a moxie cross compiler that is adjusted for my own FPGA platform:

https://github.com/milanvidakovic/moxiebox

So, how can you use moxie to make your own gcc cross-compiler? We need to know at least fundamentals of GCC compiler to do so. First of all, there is a frontend and there is a backend. Frontend deals with the actual compiling and produces a target-independent representation code, which is a passed to the backend, which in turn generates target platform code. Between frontend and backend is an optimizer, whose job is obvious.

Backend starts target platform code generation by processing insns. An insn is a kind of a virtual assembly instruction created by the frontend during compilation. Your cross-compiler now need to generate a real machine instruction(s) out of an insn. That is where I have started to investigate moxie.

First of all, I have downloaded moxiebox from the github and unpacked that on the disk. Then I have installed necessary packages to be able to build moxiebox on my Ubuntu:

sudo apt install device-tree-compiler texinfo flex  build-essential libgmp-dev libmpfr-dev libmpc-dev
sudo apt install git subversion cvs

Then I have executed the
/moxiebox/contrib/download-tools-sources.sh script. After that, the moxiebox is ready to start building. You do so by executing the
/moxiebox/contrib/build-moxiebox-tools.sh script. It takes about an hour to build everything.

To make moxie-based gcc tools present in the path, you need to add them to PATH and LD_LIBRARY_PATH in your .bashrc file (at the end):

export PATH=/moxiebox/contrib/root/usr/bin:$PATH
export LD_LIBRARY_PATH=/moxiebox/contrib/root/usr/lib:$LD_LIBRARY_PATH

Now it is a good moment to start changing original moxie cross-compiler in order to work with my platform. Fortunately, moxie is quite similar to my FPGA CPU, so it was not a huge job.

First of all, I have changed /moxiebox/contrib/gcc/gcc/config/moxie/moxie.md file to generate my FPGA instructions instead of moxie ones.  Here is one example:

(define_insn "*movsi"
  [(set (match_operand:SI 0 "nonimmediate_operand" "=r,r,r,W,A,r,r,B,r")
(match_operand:SI 1 "moxie_general_movsrc_operand" "O,r,i,r,r,W,A,r,B"))]
  "register_operand (operands[0], SImode)
   || register_operand (operands[1], SImode)"
  "@
   xor.w\\t%0, %0
   mov.w\\t%0, %1
   mov.w\\t%0, %1
   st.w\\t%0, %1
   st.w\\t[%0], %1
   ld.w\\t%0, %1
   ld.w\\t%0, [%1]
   st.w\\t%0, %1
   ld.w\\t%0, %1"
  [(set_attr "length" "2,2,6,2,6,2,6,6,6")])

Then I have changed moxie.c and moxie.h in order to use my own register names and to generate stack frame epilogue and prologue the way I am used to. Prologue:

emit_insn (gen_movsi_push (hard_frame_pointer_rtx));
emit_move_insn (hard_frame_pointer_rtx, stack_pointer_rtx);
moxie_compute_frame ();

And epilogue:

emit_move_insn (stack_pointer_rtx, hard_frame_pointer_rtx);
emit_insn (gen_movsi_pop (hard_frame_pointer_rtx, hard_frame_pointer_rtx));
emit_jump_insn (gen_returner ());

I have tried to make a standard stack frame and to pass all the arguments over the stack frame, instead of registers (it is slower, but I understand it better). I have placed the patch which substitutes original moxie cross-compiler files here:

https://github.com/milanvidakovic/moxiebox/blob/master/contrib/contrib.zip

When I try to build the whole package I still get a lot of errors, but I now have the moxiebox-gcc compiler which (unfortunately) cannot build the whole executable, but can generate assembly file by using the -S argument:

moxiebox-gcc -S test.c -o test.s

After that, it was easy to use the customasm assembler to generate the executable for my platform.

Conclusion

I have done just a half of the job. Currently, only the assembly files are being generated out of the C files. Next step is to change GCC assembler (and linker) to produce a proper executable.



петак, 6. март 2020.

Cache implemented on my FPGA computer

Introduction

This is a follow-up of my original FPGA computer post.

My FPGA computer uses SDRAM as operating memory. It has static RAM too, but most of it is used as dual-port RAM for the VGA video subsystem. The SDRAM inside is 32MB, 16-bit data bus memory and it usually takes about six clock cycles for read and the same amount of cycles for write. The clock is 100MHz. Knowing all of this, it was about time to do some performance measurement:

I have made a simple program that counts from 1 to 10 000 000. If that program is loaded in SDRAM, it takes about 15 seconds to finish. However, if I load it in static RAM, it takes about 6 seconds to finish. So, there was an obvious motivation to try to implement the cache controller. You can look at the Verilog code here:
https://github.com/milanvidakovic/FPGAComputer32/blob/master/cpu.v

Implementation

I haven't used all of the static RAM in my FPGA computer, so I was able to make about 8KB of L1 cache. Here are the details:
- I have 4096 cache lines, each having two bytes. That is 8KB of cache.
- for each cache line, I have added 12-bit TAG, used for the direct mapping of the cache line. That consumes additional 5632 bytes of static RAM.
- I have implemented write-through policy, since I didn't have enough resources to make a write-back removal policy. I will try to make write-back, but it requires a complete rework of the cache controller, so, perhaps later...

Ho this thing works in practice? First of all, here is the Verilog code:

// cache TAG
reg [11:0] tag[4095:0];
// cache line
reg [15:0] cl[4095:0];

Each cache line (a row in the cl variable) holds two bytes of data. Whenever a CPU wants to do a read, the address from the address bus goes into the cache controller:

if (tag[addr[11:0]] == addr[23:12]) begin
// cache hit (required data is in cache)
data_r <= cl[addr[11:0]];
state <= next_state;
end
else begin
// cache miss -> we need to read from SDRAM
rd_enable_o <= 1'b1;
if (busy_i) begin
state <= READ_WAIT;
end
end

12 lower bits of the address (addr[11:0]) are used to address the cache line. To check if the wanted data is in cache, the tag is used. The same 12 lower bits address the tag which is assigned to a cache line. If the upper 12 bits of the address (addr[23:12]) match those in the tag, then we have a cache hit and the data can be returned directly from the cache. 

If that is not the case, then we need to perform a read from the SDRAM, and then:

rd_enable_o <= 1'b0;
if (rd_ready_i) begin
data_r <= rd_data_i;
// we store the fetched data into the cache
cl[addr[11:0]] <= rd_data_i;
// write tag
tag[addr[11:0]] <= addr[23:12];
state <= next_state;
end

When we finally obtain the data from the SDRAM, we return that data to the CPU, but we also write down that same data in the cache line, and we update the tag associated to that cache line with the upper 12 bits of the address.

That was the read cycle. Let's see how write works. When CPU wants to write data, it is saved into the SDRAM and into the cache as well:

// Write through, meaning that we save data in both SDRAM and cache
wr_data_o <= data_to_write;
// now we need to store the data that had to be saved into cache
cl[addr[11:0]] <= data_to_write;
// write tag
tag[addr[11:0]] <= addr[23:12];
wr_enable_o <= 1'b1;
if (busy_i) begin
state <= WRITE_WAIT;
end

As we can see, data is saved in both SDRAM and cache, and then we just return back:

wr_enable_o <= 1'b0;
if (~busy_i) begin
state <= next_state;
end

Performance

The cache controller works like a charm! The same counting example works now (almost) as fast as when it was executed in the static RAM (about 6 seconds to count from 1 to 10 million). 

Conclusion

Write-through implementation is simpler than write-back and maintains SDRAM in synchronization with the cache. However, it is slower, because CPU needs to wait for the data to be saved in SDRAM, instead of doing fast save just into the cache. Write-back is faster, since we don't have to wait for the slow SDRAM save, but the cache goes out-of-sync with the SDRAM (since we saved data in cache only). When we have a full cache, in case of write-back, we need to empty the corresponding cache line, by writing the content into the SDRAM, and then to write the new content in the cache.

The write cycle could be implemented as write-back, but with this setup, I cannot do that (not enough resources on FPGA chip). I will investigate that in future.

субота, 5. октобар 2019.

Flashing DE0-NANO FPGA board and using DEV_CLRn reset functionality

In this post I am going to talk about programming DE0-NANO FPGA board two ways:
1. temporary programming, meaning that the design will not survive powering off, and
2. permanently storing (flashing) the design, so it will survive power off.

I will also address the idea of a mega-reset using the DEV_CLRn feature at the bottom of this post.

Disclamer: You are doing all of this at your own risk. I am not responsible for any problem caused by these examples. To prevent problems, check the documentation of your DE0-NANO to see if you have the same type of Programmer and EEPROM chips.

1. Temporary programming

Whenever you compile your design at the Quartus II IDE, you can send the design to the FPGA board via Programmer:

1. double click on the Program Device in the table:

2. When the Programmer opens, look for the USB blaster right to the Hardware Setup... button:


3. If you see the "No Hardware" text, click on the Hardware Setup... button. That would open the Hardware Setup dialog:

4. Double click the USB Blaster and Close. The Programmer should look like this:


5. Now the USB Blaster is present right to the Hardware Setup... button.

6. Click on the Start button to send the design to the FPGA board.

All this is temporary, meaning that the design will be erased when you power off the board.

Alternative way of temporary programming

An alternative way of doing this is by executing the following program:

C:\altera\13.0\quartus\bin\quartus_pgm.exe

You need to supply that program with the following command line parameters:

-c usb-blaster -m jtag -o P;<path_to_the_SOF_file>

That could be typed like this:


C:\altera\13.0\quartus\bin\quartus_pgm.exe -c usb-blaster -m jtag -o P;<path_to_the_SOF_file>

I have done that in my FPGARaspbootin program (the Run FPGA manually button, and Auto Run FPGA check box):


2. Permanent programming (flashing)

To flash the DE0-NANO device, you need to:

1. convert the SOF file into the JTAG Indirect Configuration File (*.jic file), by choosing File -> Convert Programming Files... menu option. That would open the Convert Programming File Dialog.

2. Choose the Programming file type to:
JTAG Indirect Configuration File (*.jic file)

3. Open the Configuration device combo box and choose: EPCS64

4. Click on the Flash Loader in the bottom table and click on the Add Device... button. That would open the Select Devices dialog. Choose Cyclone IV E and EP4CE22:


5. Click on the SOF Data in the table and click on the Add File... button. Choose your SOF file:

6. Click on the chosen SOF file in the table (in my example, the computer.sof file) and click on the Properties. Turn on the compression:

7. After that start the Programmer, delete the SOF file (if existed), and add the JIC file (click on the Add File... button). Make sure that both Program/Configure checkboxes are turned on:

8. Click on the Start button and wait for the 100% at the progress bar (upper right corner).

9. You can power off and then power on the FPGA and it will still have the design in it.

Reset all registers with the Reset button (DEV_CLRn option)

I have experienced some strange behavior regarding resetting my design. I have made my KEY[0] clear all of my registers using the Verilog code. That design, however, failed somehow to completely reset my board. Simply, after the reset, the design would behave unreliable. I have managed to solve this problem by introducing the mega-reset feature. Here is the explanation:

In DE0-NANO, the KEY[0] is connected to the PIN_J15, which is in turn connected to the DEV_CLRn pin. This can be used to reset all registers in the FPGA when you press the KEY[0] key on the DE0-NANO board. That is a kind of mega-reset. However, this feature is turned off by default. You need to enable it. Here is the procedure:

1. right click on the Cyclone IV... in the Project Navigator and choose the Device... option:

2. click on the Device and Pin Options... button:

3. Find and enable the Enable device-wide reset (DEV_CLRn) check box:

4. Open the Pin Planner dialog (Assignments -> Pin Planner menu option), and change the KEY[0] from PIN_J15 to something unassigned, like PIN_G5 (it is unused in my project):

After that, whenever you press the KEY[0] key, you will reset your entire board.

среда, 18. септембар 2019.

Added new VGA graphics mode

This is a follow-up of my original FPGA computer post.

FPGA computer has got a new VGA mode: 640x480 in two colors. One byte of the video memory holds 8 pixels, each being 1 or 0 (white or black):

Pixel 7
Pixel 6
Pixel 5
Pixel 4
Pixel 3
Pixel 2
Pixel 1
Pixel 0

If you want to put four white and four black pixels at the top left corner of the screen (from the (0,0) to the (7,0) coordinates), you need to type:

mov.w r0, 0xF0
st.b [1024], r0

This mode is made out of existing VGA text mode, since it does almost all the job. The text mode shows characters made of 8x8 pixels on the 640x480 VGA screen. I have inserted additional Verilog code inside the text mode module, in a way that when the 640x480x2 mode is set, it shows the pixels, not the characters.

First of all, the programmer needs to set the display mode to 640x480x2:

mov.w r0, 2
out [128], r0

The VGA module detects this and changes the signal generation on the VGA connector:

if (valid) begin
  if (vga_mode == 0)  begin
    r <= inverse ^ (pixels[7 - (x & 7)] ? !curr_char[6+8] : curr_char[2+8]);
    g <= inverse ^ (pixels[7 - (x & 7)] ? !curr_char[5+8] : curr_char[1+8]);
    b <= inverse ^ (pixels[7 - (x & 7)] ? !curr_char[4+8] : curr_char[0+8]);
  end
  else if (vga_mode == 2) begin
    r <= inverse ^ (curr_char[15 - (x & 15)]);
    g <= inverse ^ (curr_char[15 - (x & 15)]);
    b <= inverse ^ (curr_char[15 - (x & 15)]);
  end
end 

What we see above is the Verilog code that sets the R, G and B wires of the VGA connector to the corresponding values, depending of the display mode. If the mode is text (vga_mode == 0), it outputs the font pixels of the character that was found in the video memory (pixels module returns actual pixels of the current_char register). However, if the mode is graphics (vga_mode == 2), then the actual byte found in the video memory is outputted to the wires (actual bits of the byte are pixels).

All this means is that current_char register holds two bytes from the video memory, and it is periodically loaded from the video memory. At the beginning, it is loaded from the very first word of the video memory, and after that VGA module loads two bytes of the video memory periodically:
- at the end of each character in text mode, it fetches the next character,
- at the end of each 16 pixels of the graphics mode, it fetches next 16 bits (pixels),
- at the end of each scan line it fetches the content of the beginning of the next scan line,
- at the lower right corner scanline end, it fetches the content of the top left corner.

This is the photo of the actual monitor:

And, this is the screenshot of the emulator:


Conclusion

Before this feature was introduced, the FPGA computer had two video modes:
- text 80x60 mode characters text mode (made of 640x480 pixels and each character is 8x8 pixels in size).
- graphics 320x240 mode, each pixel being in one of 8 colors.

This new mode is added to the existing VGA text module (80x60 characters) since that module already works with 640x480 pixels. The only additional thing was to show pixels, not characters. So, the computer now has one more mode: 640x480 in two colors (black and white). You can look at the Verilog code:

https://github.com/milanvidakovic/FPGAComputer32/blob/master/vga_module.v

And, you can look at the assembly code which draws everything here:

https://github.com/milanvidakovic/Assembler32/blob/master/raspbootin/graphics640.asm