/NeuroRISC

Primary LanguageVerilog

NeuroRISC GitHub Portfolio

This Github is separated into three main components, The RTL design which is written in verilog and details the physical hardware design that is loaded on the FPGA, The bootloading which details the assembler, programmer and memory initialisation for the processor which is all written in python and finally the assembly which is the code that is executed by NeuroRISC at runtime that simulates a spiking neural network which is written in RISC V assembly code.

HDL

This section of the code can be found in the HDL directory of the repository. It contains all of the verilog and schematic files used to compile the NeuroRISC processor. All of the work was done on Quartus Prime 18.1 and the design was compiled and run on a Terasic DE10-Nano

NeuroRISC

The primary core design can be found in the NeuroRisc.bdf and NeuroRisc.v files. This is the file that describes the functional RISC V core that is used to perform the computations. The following files are submodules of this module.

  • alu.v
  • control_fsm.v
  • FivePortMux.v
  • imm_gen.v
  • instruction_reg.v
  • MDU.v
  • mem_size.v
  • memory_interface.v
  • programCounter.v
  • registerFile.v
  • TwoPortMux.v
  • writeback.v

Image 1 Structure of NeuroRISC Core

The core is designed to conform to most of the RV32IM specifications excluding CSR instructions which means it can perform all of the base instruction set and the multiply/divide extension of RISC V. The intention of using this design is to provide a small and reprogrammable core that can efficiently emulate a spiking neural network using integer approximation. The design is simple and doesn't deviate from standard design patterns of RISC V cores. More information on the operation of RISC V can be found in the RISC V Specification.

Memory and IO Design

To interface the core with the outside world and maintain persistence across cycles the processor needs IO and memory respectively. The design for this uses Harvard architecture or some call modified Von Neumann Architecture where the processor has a separate instruction and data memory. In this design they are separated but are still accessible across the total address space. For simplicity on chip memory is used. In this design it is impossible to read data memory as instructions but it is still possible to read instructions as data. This is achieved by using a two port instruction memory where one port is wired directly to the processor and addressed by the program counter and the other being wired to a memory interface which connects all memory and IO to the processor through data memory. The memory interface takes the address generated by the processor and maps the address to the appropriate memory bank. The instruction memory has a depth of 256 words and a width of 32 bits and the data memory has a depth of 1024 words and a width of 32 bits. To give an example of how the memory interface works if the processor requests data at the address 0 it will receive the very first instruction in instruction memory, and if the processor requests data at the address 256 it will receive the very first word of data in data memory. The IO of the device is very simple for testing purposes and consists of 5 32 bit words which are only used for debugging and to identify that the processor is doing something. These 5 words are accessible via memory mapped IO and are written to and read from the next 5 addresses beyond data memory.

The following files are submodules of NeuroRISC_With_MEM.v

  • Memory_Mapper.v
  • NeuroRisc.v
  • data_memory.v
  • instr_mem.v
  • IODevice.v

Image 2 Core with memory layout

Bootloader

The supporting software for NeuroRISC consists of a custom built assembler, programmer and memory initialiser. All of these are written in python. RVassembler.py, RVprogrammer.py and neuronLoader.py can be found in the Assembly directory of the repository.

RISC V Assembler

The RISC V assembler is designed to convert RISC V assembly code to RISC V binary machine code. To do this the following functions are used in python to interpret the asm file and generate the appropriate machine code.

The assembler implements the argparse library so it can be used completely through command line using the command:

python RVassembler.py -b -m path/to/asm

The argument -b indicates that a raw binary file will be generated with the same name as the input file in the same directory.

The argument -m indicates that a memory initialisation file will be generated with the same name as the input file in the same directory.

Functions

signExtendBinary
binaryString:str, length:int
return newVal

The signExtendBinary function takes in a string that can only consists of ones and zeros and extends the string by the value of the first character in the string to the input length. For example binaryString = "1100" and length = 8 then the function would return newVal = "11111100"

hexConvert
value:str
return hexVal

The hexConvert function takes a binary string and converts it to 32 bit hexadecimal.

getRegisterBinary
stringValue:str
return registerBinary

The getRegisterBinary function takes a string input of "x0" to "x31" (which is the common naming convention of the 32 register files in RISC V assembly) and converts the value to a binary value between 0 and 32. The value is zero extended to 5 bits to prepare it to be loaded into a 32 bit binary string of machine code. For example stringValue = "x13" then the function would return registerBinary = "01101" which is 13 in binary.

generateMachineCode
parsedList:list[str]
return mCodeList

The generateMachineCode function is the backbone of the assembler. It takes a list of valid assembly lines that have been scrubbed of comments and then loops through each line to identify the type of instruction and then feeds the string into the appropriate instruction conversion function. The function returns a list of binary strings that represent the machine code equivalent of the assembly instruction.

genRTypeInst
parsedList:list[str]
return out

The genRTypeInst function is used to generate the appropriate machine code out of an R type RISC V assembly instruction. For the format of an R type instruction refer to image 3. The function takes in an assembly instruction that has been separated into a list of its tokens and then generates the binary data based on the data in each token. For example with an input of parsedList = ['ADD', 'x1', 'x2', 'x0'] The function would return out = '00000000000000010000000010110011'

Image 3 RISC V Specification

genITypeInst
parsedList:list[str]
return out

The genITypeInst function is used to generate the appropriate machine code out of an I type RISC V assembly instruction. For the format of an I type instruction refer to image 3. The function takes in an assembly instruction that has been separated into a list of its tokens and then generates the binary data based on the data in each token. For example with an input of parsedList = ['ADDI', 'x5', 'x0', '1'] The function would return out = '00000000000100000000001010010011'

genSTypeInst
parsedList:list[str]
return out

The genSTypeInst function is used to generate the appropriate machine code out of an S type RISC V assembly instruction. For the format of an S type instruction refer to image 3. The function takes in an assembly instruction that has been separated into a list of its tokens and then generates the binary data based on the data in each token. For example with an input of parsedList = ['LW', 'x1', '0', 'x0'] The function would return out = '00000000000000000010000010000011'

genBTypeInst
parsedList:list[str]
return out

The genBTypeInst function is used to generate the appropriate machine code out of an B type RISC V assembly instruction. For the format of an B type instruction refer to image 3. The function takes in an assembly instruction that has been separated into a list of its tokens and then generates the binary data based on the data in each token. For example with an input of parsedList = ['BLT', 'x10', 'x21', '40'] The function would return out = '00000001010101010101010001100011'

genUTypeInst
parsedList:list[str]
return out

The genUTypeInst function is used to generate the appropriate machine code out of an U type RISC V assembly instruction. For the format of an U type instruction refer to image 3. The function takes in an assembly instruction that has been separated into a list of its tokens and then generates the binary data based on the data in each token. For example with an input of parsedList = ['LUI', 'x1', '200000'] The function would return out = '00110000110101000000000010110111'

genJTypeInst
parsedList:list[str]
return out

The genJTypeInst function is used to generate the appropriate machine code out of an J type RISC V assembly instruction. For the format of an J type instruction refer to image 3. The function takes in an assembly instruction that has been separated into a list of its tokens and then generates the binary data based on the data in each token. For example with an input of parsedList = ['JAL', 'x1', '20'] The function would return out = '00000000110000000000000011101111'

genPseudo
parsedList:list[str]
return mCodeList

The function genPseudo is used to account for edge cases in the RISC V assembly language. Pseudo instructions are generally multiple instructions bundled into a single instruction to make typing the code easier. This function parses the list for matching pseudo instructions and generates the appropriate instructions in order and returns them as a list of 32 bit binary strings.

generateMif
machineCode:list[str], filename:pathlib.Path

The function generateMif takes a list of binary strings and a file name and generates a memory initialisation file of the binary data that was input. The function used a template mif file and appends the binary data to it then saves it as a new file. This is primarily useful for FPGA development as MIF files can preload a program into memory on FPGA.

generateHex
machineCode:list[str], filename:pathlib.Path

The function generateHex is used to generate a file of the raw binary data which can be executed directly by a RISC V processor. The file generated from this can be loaded onto the RISC V processor at runtime.

NeuroRISC Programmer

The programmer for NeuroRISC is a relatively rudimentary implementation of UART communication. It is a python script that can be called with the command:

python RVProgrammer.py -p PORT path/to/bin

Where -p is an identifier for the port to send packets through. The script reads the binary file and adds a 32 bit address to each instruction and a header byte to each address and instruction. The address is added as the instruction is loaded into the processor asynchronously. The header is added to identify the start of a correct message and to differentiate between address and instruction. On the processor side the data is received a single bit at a time which is reconstructed into a byte then temporarily stored in a register until the full packet has been sent. Once a full instruction and address is sent the processor loads the instruction into memory at the correct address.

NeuroRISC Memory Loader

The memory loader much like the programmer is a auxiliary program that is used to generate the memory initialisation file with the neural network that is going to execute on the processor. Each neuron of the network takes up 16 words and the program generates an abstract model for the neural network then it generates the appropriate memory data to be loaded onto the processor. To generate the model a class called neuron is used to generate individual neurons with parameters and a class called neuronPopulation is used to generate a group of interconnected neuron objects. for the purpose of testing the connections generated in a neuron population is randomised based on a seed of each neurons index and all neurons exhibit the regular spiking parameters of the izhikevich model.

Assembly

This section comprises most of the novel research on implementing spiking neural networks as close to physical hardware as possible using standard instructions found in the RV32IM instruction set. The file izhikevichTM.asm is a RISC V assembly program that is capable of simulating a small network of 64 neurons. Each section of the assembly code is broken into routines and commented thoroughly. This section of the documentation will cover what each routine is doing to detail how the network operates on chip.

Memory address load Routine

The file starts with loading the memory starts into registers for easy access. The memory structure of the processor is as follows:

  • 0-255 is instruction memory
  • 256-1279 is data memory
  • 1280-1285 is IO

In the code the data memory start and IO start are loaded.

Izhikevich parameter Load Routine

Like with any other code we start by loading constants that need to be used throughout the program. The very large values are actually 32 bit fixed point representations of decimals which is what makes integer representation of spiking neural networks possible. By multiplying the with these numbers and taking the top 32 bits of the multiplication you can perform division and multiplication in a single instruction and save cycles.

Memory Pointer initialization Routine

This section is used to initialise pointers which will move and collect data throughout memory. All three pointers are initialised at the start of data memory.

Spiking neuron load Routine

The start of spiking operations begins by fetching the neuron data for the first neuron in memory. Neurons are represented by 16 words in memory. Only 11 of those words are used and the other 5 are reserved to make the neuron unit evenly divisible in memory. The program starts by loading the Izhikevich parameters of the neuron from memory.

IO Read Routine

Next the neuron reads values from IO if it meets the criteria of the bne instruction. This essentially acts as a way to inject current into a neuron to cause excitation.

Calculation Routine

After the input data is summed the Izhikevich neuron can be calculated. All of the calculations are performed here and saved to the registers where the neuron information was loaded to initially.

Spike Detection Routine

After calculation the voltage is checked to determine if a spike has occurred in the neuron. If it has all the following routines are performed. If it hasn't, the program skips to the Neuron Store Routine.

Reset Neuron Routine

The Reset Neuron Routine is the last component of the Izhikevich model where the voltage is reset to resting threshold and the U value is incremented according to the Izhikevich model.

Spike Emission Routine

This is the most complicated part of the model as it determines which neurons receive spikes from the current action potential of the neuron that is firing. This is accomplished by loading the excitatory and inhibitory connection registers from memory. Each neuron has 64 bits for excitatory connections and 64 bits for inhibitory connections. Each bit represents if the neuron has an excitatory or inhibitory connection to the corresponding neuron. As the network gets larger so does this data. The Emission routine starts by performing an and operation on the connection register and a register loaded with the value of 1. If the operation returns a 1 than the neuron at the current location of the emission pointer is connected. A right shift is performed on the connection register and the emission pointer is incremented to the next neuron in memory. The process is repeated until the end of memory is reached. If a neuron is found to be connected it is then checked if it is an inhibitory connection or excitatory connection. Finally the current data for the active neuron is written to the input of the next neuron.

Neuron Store Routine

The final stages of the neuron calculation is storing the information back in memory. zero is written back to the current storage as it is assumed that the current has already been accounted for and that location in memory is only used for input current.

Change Neuron Routine

Finally the neuron pointer is incremented the spike output is cleared and the emission pointer is reset. In this stage if the neuron pointer exceeds the data memory it is reset to its initial location. The program then jumps back to Spiking Neuron Load routine and the process repeats forever.