уторак, 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 cmd, uint32_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 -= 8) spiSend(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 offset, uint16_t count, uint8_t* dst) {
  uint16_t n;
  if (count == 0) return 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 token, const uint8_t* src) {
  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 blockNumber, const uint8_t* src, uint8_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.filename, fd.dir_entry.filesize, fd.curr_cluster, fd.dir_entry.first_cluster);
}

The key code is in the getDirEntry() function:
uint32_t getDirEntry(file_descriptor_t* fd, uint32_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 + 0x0c, 14);
        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.

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

Yet another one day build of a digital clock

So, what is the difference this time? I have used TM1637 four-digit, seven-segment display, instead of driverlesss display. The difference is in the number of components. Instead of having twelve resistors and four transistors to drive the 7-segment display, now I have a single display with the built-in driver.

The code is on github.

Here is the schematics:
When you compare that to my previous 4-digit 7-segment build, you will notice the difference. The code is simpler, too:

const int CLK = 17; //Set the CLK pin connection to the display
const int DIO = 16; //Set the DIO pin connection to the display

TM1637Display display(CLK, DIO); //set up the 4-Digit Display.

void setup() {

  //set the display brightness  (0x0a is max)
  display.setBrightness(0x05); 

  ...

}

void loop() {
  
  display.showNumberDec(hour_1*1000 + hour_2*100 + minute_1*10 + minute_2);

  toggle = ~toggle;
  if (toggle)
  {
    display.showNumberDecEx(hour_1*1000 + hour_2*100 + minute_1*10 + minute_2, (0x80 >> 1), true);
  } 
    delay(500);
}

This is the clock:



петак, 24. јануар 2020.

Another One Day Build - Big Digital Clock

Welcome to another digital clock build. This time I have made a big digital clock for my friend. The clock was made using programmable LED strip, cut into small pieces which would act as segments in a big 7-segment digit.

The code is on github.

Programmable LED strip

This is a very nice thing - you can set any color to the individual LED bulb on the strip:

On the picture above you can see two segments, each having four LEDs. Each LED is programmable and can be set to any color of the 16 million colors available. I have purchased two meters of W2812B LED strip and cut it in segments having four LEDs. Then I have connected all segments with three wires and created digits:

Here you can see seven segments placed temporarily on the cardboard, for the testing purposes. Then I have connected the remaining three digits and two dots between hours and minutes:

I have recycled my previous code for the LED clock so I could now set and turn on/off individual digits of my new display. Here is the main loop:

void loop() {
  setDots(counter % 2);
  setDigit(0, hour_1);
  setDigit(1, hour_2);
  setDigit(2, minute_1);
  setDigit(3, minute_2);
  delay(1000);

  counter++;
  if (counter == 2)
  {
    counter = 0;
  }
}

The code above relies on getting the current time from the DS3231 RTC, separated into hour_1, hour_2, minute_1 and minute_2 variables. Then it is just matter of setting the corresponding segments on/off:

void setDigit(int digit, int value)
{
  int i, j;
  for (i = DIGIT_IDX_START[digit]; i < (DIGIT_IDX_START[digit] + 28); i++)
  {
    j = (i - DIGIT_IDX_START[digit]) / 4;                  // j is segment index
    if (DIGIT_MATRIX[value][j])
    {
       switch (state)
      {
        case RED:
          leds[i] = CRGB(255, 0, 0);
          break;
        case GREEN:
          leds[i] = CRGB(0, 255, 0);
          break;
        case BLUE:
          leds[i] = CRGB(0, 0, 255);
          break;
        case WHITE:
          leds[i] = CRGB(255, 255, 255);
          break;
        case RAINBOW:
          leds[i] = CRGB((i % 4 + 1) * 64, (i % 4 + 2) * 64, (i % 4 + 3)* 64);
          break;
        case BLACK:
          leds[i] = CRGB(0, 0, 0);
          break;
        default:
          leds[i] = CRGB(255, 0, 0);
      }
    }
    else
    {
      leds[i] = CRGB(0, 0, 0);
    }
  }
  FastLED.setBrightness(brightness);
  FastLED.show();
}

The FastLED library works by declaring an array of LEDS in the program. Here is my array of LEDs:

#define LED_PIN     2
#define NUM_LEDS    4*28

CRGB leds[NUM_LEDS + 2];

int DIGIT_IDX_START[] = {0, 28, 56, 84};
int DIGIT_MATRIX[][7] = {
// A     B     C     D     E     F     G  
{HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, LOW},   // 0
{LOW,  HIGH, HIGH, LOW,  LOW,  LOW,  LOW},   // 1
{HIGH, HIGH, LOW,  HIGH, HIGH, LOW,  HIGH},  // 2
{HIGH, HIGH, HIGH, HIGH, LOW,  LOW,  HIGH},  // 3
{LOW,  HIGH, HIGH, LOW,  LOW,  HIGH, HIGH},  // 4
{HIGH, LOW,  HIGH, HIGH, LOW,  HIGH, HIGH},  // 5
{HIGH, LOW,  HIGH, HIGH, HIGH, HIGH, HIGH},  // 6
{HIGH, HIGH, HIGH, LOW,  LOW,  LOW,  LOW},   // 7
{HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH},  // 8
{HIGH, HIGH, HIGH, HIGH, LOW,  HIGH, HIGH},  // 9
{LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  HIGH},  // '-'  index is 10
{LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW}    // BLANK index is 11
};

I have an array of 4*28 + 2 LEDS for 4 digits and two dots. The DIGIT_IDX_START array holds the starting indices of all four digits in that array of LEDS. 

The final look is below. I have placed a white plastic board over LEDs to disperse the light and now it is quite nice to watch in all lighting conditions:


The button at the lower right corner turns on/off the display.

петак, 10. јануар 2020.

One Day Build - Digital Clock

I have recently decided to make a digital table clock. The design I chose consists of three important parts:
- ESP32,
- 7-segment four digits display with no special driver, and
- DS3231 real time clock.

The code is on github.

Since ESP32 has WiFi module, I used it for two purposes: to obtain the correct time once a day (and synchronize the DS3231 to that correct time), and to obtain the temperature information from my weather server, so I could get the outside temperature with a single button press.

I purchased the most simple 7-segment display which has 12 pins: 8 pins for the digit segment selection and 4 pins for the digit selection. Those four pins are common cathodes of the LEDs making each digit - that is why it has four common cathodes - for those four digits.

DS3231 is a good RTC with temperature compensation and optional battery (CR2032) to work without external power source.

Here is the schematics:


Driving the 7-segment four digit display is quite interesting. You cannot illuminate more than one digit in time. This means that you need to illuminate the first digit, then the second, then the third, and the fourth, and then to do it all again, fast. Thanks to our vision persistence, we get the impression that all four digits are illuminated. 

If you look at the schematics, you will notice that the digit definition is done using GPIO pins 32, 33, 25, 26, 27, 14, 12, and 2, which turn on/off segments a, b, c, d, e, f, g, and dot. That would only set the corresponding LEDs input to high or low voltage, but would not illuminate them (the circuit would not be closed until the common cathode is connected to the ground). To choose which digit will be illuminated (to have the common cathode connected to the ground), I used GPIO pins 16, 17, 18, and 19. Logical one (3.3V) on those GPIO pins would turn on the corresponding transistor and it would connect the common cathode of the connected digit to the ground.

// GPIO ports for LEDs
//           Dot A   B   C   D   E   F   G
int LEDS[] = {2, 32, 33, 25, 26, 27, 14, 12};

// GPIO ports for digits
int DIGITS[] = { 18, 19, 17, 16 };

Next I defined the matrix of LEDs for digit representation:

int DIGIT_MATRIX[][7] = {
// A     B     C     D     E     F     G  
{HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, LOW},   // 0
{LOW,  HIGH, HIGH, LOW,  LOW,  LOW,  LOW},   // 1
{HIGH, HIGH, LOW,  HIGH, HIGH, LOW,  HIGH},  // 2
{HIGH, HIGH, HIGH, HIGH, LOW,  LOW,  HIGH},  // 3
{LOW,  HIGH, HIGH, LOW,  LOW,  HIGH, HIGH},  // 4
{HIGH, LOW,  HIGH, HIGH, LOW,  HIGH, HIGH},  // 5
{HIGH, LOW,  HIGH, HIGH, HIGH, HIGH, HIGH},  // 6
{HIGH, HIGH, HIGH, LOW,  LOW,  LOW,  LOW},   // 7
{HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH},  // 8
{HIGH, HIGH, HIGH, HIGH, LOW,  HIGH, HIGH},  // 9
{LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  HIGH},  // '-'  index is 10
{LOW,  LOW,  LOW,  LOW,  LOW,  LOW,  LOW}    // BLANK index is 11
};

To display a digit, you need to call two following functions:

void displayDigit(int value)
{
  int j, k;
  for (j = 1; j < 8; j++)
  {
    digitalWrite(LEDS[j], (DIGIT_MATRIX[value][j-1]));
  }
}

The function above will set the corresponding LEDs for the given digit, but will not illuminate anything, since we haven't selected which one of the four digits should be activated. To do so, we need the following function:

void activateDigit(int digit)
{
  switch(digit)
  {
    case 0:
      digitalWrite(DIGITS[0], HIGH);
      digitalWrite(DIGITS[1], LOW);
      digitalWrite(DIGITS[2], LOW);
      digitalWrite(DIGITS[3], LOW);
      digitalWrite(LEDS[0], LOW);
    break;
    case 1:
      digitalWrite(DIGITS[0], LOW);
      digitalWrite(DIGITS[1], HIGH);
      digitalWrite(DIGITS[2], LOW);
      digitalWrite(DIGITS[3], LOW);
      digitalWrite(LEDS[0], LOW);
    break;
    case 2:
      digitalWrite(DIGITS[0], LOW);
      digitalWrite(DIGITS[1], LOW);
      digitalWrite(DIGITS[2], HIGH);
      digitalWrite(DIGITS[3], LOW);
      digitalWrite(LEDS[0], LOW);
    break;
    case 3:
      digitalWrite(DIGITS[0], LOW);
      digitalWrite(DIGITS[1], LOW);
      digitalWrite(DIGITS[2], LOW);
      digitalWrite(DIGITS[3], HIGH);
      digitalWrite(LEDS[0], LOW);
    break;
    default:
      digitalWrite(DIGITS[0], LOW);
      digitalWrite(DIGITS[1], LOW);
      digitalWrite(DIGITS[2], LOW);
      digitalWrite(DIGITS[3], LOW);
      digitalWrite(LEDS[0], LOW);
  }
}

So, displaying time from the RTC module looks like this. First we read the time from the RTC (once in two seconds):

void read_time() 
{
  DateTime now = rtc.now();
  int hour = now.hour();
  int minute = now.minute();

  hour_1 = hour / 10;
  hour_2 = hour % 10;
  minute_1 = minute / 10;
  minute_2 = minute % 10;
}

We break up the time into four digits: hour_1, hour_2, minute_1, and minute_2. Then, in the main loop, we display that time by showing each digit quickly (each digit is illuminated one millisecond):

void loop()
{
  switch(counter)
  {
    case 0:
      displayDigit(hour_1);
      break;
    case 1:
      displayDigit(hour_2);
      break;
    case 2:
      displayDigit(minute_1);
      break;
    case 3:
      displayDigit(minute_2);
      break;
  }
  activateDigit(counter);
  counter++;
  if (counter == 4)
  {
    counter = 0;
  }

  // duty cycle adjustment for the LED brightness
  delayMicroseconds(500);
  activateDigit(-1);
  delayMicroseconds(500);
}

As you can see, the time is fetched from the RTC outside of the main loop. I have done that because obtaining the time from the RTC inside the loop might screw up the LED illumination, since the main loop would be blocked while reading the time from the RTC. To avoid that, I have used the ESP32 task to do the RTC reading. In the setup function, I have created a task for obtaining time:

// create a task that will be executed in the Task1code() function,
// with priority 1 and executed on core 0
// this task will get time from RTC every two seconds
  xTaskCreatePinnedToCore(
              Task1code,   /* Task function. */
              "Task1",     /* name of task. */
              10000,       /* Stack size of task */
              NULL,        /* parameter of the task */
              1,           /* priority of the task */
              &Task1,      /* Task handle */
              1);          /* pin task to core 1 */


Task1code() is a task function which is executed in parallel with the loop() function. Task1 is a task handler variable, which holds a handler for this task:

TaskHandle_t Task1;

void Task1code( void * pvParameters ){
  for(;;){
     read_time();
     delay(2000);
  } 
}

This task is executed as an infinite loop. It obtains the time from the RTC and then sleeps for two seconds. Then it does all of it again.

This is the time displayed by the clock:



Getting the outside temperature from my weather server was an interesting task. When I press the button, the clock would have to connect to the weather server, send the HTTP request for the temperature info, receive the HTTP response, parse it and display the outside temperature on the clock, instead of current time.

const char root_ca[]= {
// here comes the certificate in bytes...
};

http.begin("https://myserver/status", root_ca);

int httpCode = http.GET();

// httpCode will be negative on error
if(httpCode > 0) {
    // got the response from the server
    if(httpCode == HTTP_CODE_OK) 
    {
        String payload = http.getString();
        String t = parse_response(payload);
        int temp = t.toInt();
        if (t.startsWith("-"))
        {
          hour_2 = 10;  // "-" sign
          temp  = -temp;
        } else 
        {
          hour_2 = 11;  // BLANK character
        }
        hour_1 = 11;
        minute_1 = temp / 10;
        minute_2 = temp % 10;
    }
} else {
    Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
}

http.end();

The clock would display the temperature outside:



When I press the button again, the clock will display the internal temperature, obtained from the RTC. RTC has the temperature sensor built in so that it can compensate the time drift caused by the temperature change. That sensor info is exposed to the programmer:

float int_temp = rtc.getTemperature();
int temp = (int)int_temp;
temp = temp -2;
hour_1 = 11;
hour_2 = 11;
minute_1 = temp / 10;
minute_2 = temp % 10;

Here is the temperature inside:


How is this clock initialized when powered up? How is this clock synchronized to the precise time? I used the WiFi connection to connect the clock to the time server:

http.begin("https://myserver/time", root_ca);

int httpCode = http.GET();

// httpCode will be negative on error
if(httpCode > 0) {
    // got the response from the server
    if(httpCode == HTTP_CODE_OK) 
    {
        String payload = http.getString();
        parse_time(payload);
        rtc.adjust(DateTime(year, month, day, hour, minute, second));
    }
} else {
    Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
}

http.end();

The code above does not synchronize the clock to the exactly precise time (because of the delay), but it is good enough for me.

Conclusion

This table clock build was very interesting. The main challenge was to make all the digits show properly on the 7-segment display. When I mastered that, the rest was a breeze: getting the temperature from the weather server and showing the internal room temperature.