Computer Laboratory

Course pages 2015–16

ECAD and Architecture Practical Classes

Exercise 3: Etch A Sketch in simulation

Functional modelling

In a typical design project, there are often several teams working in parallel. For instance, in a smartphone project the chip design team may be working on the hardware, the OS team may be working on the operating system and writing drivers, the applications team may be designing the user interface, the mechanical team working on the packaging and the manufacturing team making a production line to build and test the product cost-effectively. To reduce the time-to-market frequently these teams start their work concurrently: there is not time to perfect the hardware before writing the software.

To allow concurrent development we can simulate parts that do not yet exist. For example we can develop drivers on a simulation of the chip before silicon is available. However a full-scale simulation of the chip is very slow and unnecessarily detailed for the driver writer, and the driver writer still needs to wait for a large chunk of the chip design to be completed before proceeding.

One approach to address this is top-down refinement. We can write functional models for components that are yet to exist. These express the functionality required, before the implementation details have been determined. They are then made available for developers in other teams to start their work. Once the real implementation of the components exist they can replace the functional models, but we may retain them as they are often much quicker to simulate than the real thing. Functional models may make use of features not available in the target environment to aid debugging - for example simulation models can be written in software rather than hardware, and mechanical models might use rapidly 3D printed plastic instead of metal parts.

In Lab 2 we are going to synthesise an FPGA design containing the Yarvi processor and your SystemVerilog peripherals and download it onto an FPGA board. FPGA synthesis is a computationally hard problem and so can take up to 20 minutes to run. To aid development times you will test your software in the simulator against a functional model of the display and rotary dials on the FPGA board. The functional model will run in C and communicate over a TCP socket with your Yarvi SystemVerilog that runs in Modelsim, displaying in a window what you would see on the LCD on the FPGA board. By running in software simulation you are able to see the full behaviour of the CPU which would not be possible when it is running in hardware.

The Etch-a-Sketch

The goal of Lab 1 is to build a system to implement an Etch A Sketch in simulation. An Etch A Sketch is a 1950s mechanical toy bearing two knobs, which allows the user to draw patterns on the screen. The unit (mechanically) maintains an X,Y coordinate the pixel of which is continually painted black. When the left knob is rotated the X coordinate is increases or decreases, when the right knob is rotated the Y coordinate increases or decreases, which allows lines to be drawn. Moving both knobs at the same time enables drawing diagonal lines, and shaking the unit clears the display.

Avalon Memory-Mapped (Avalon MM) is Altera's on-FPGA memory bus. We have added a port to Yarvi so that it can be a master to peripherals on the Avalon bus. We have mapped the Avalon-MM bus into Yarvi space starting at 0x80000000. (While that will still be true on the FPGA, from the perspective of the FPGA it'll have another base – we'll come to that when we build it).

In our case we have two dials, a 'clear' button, and a 480x272 pixel display to show the picture. These are mapped into Yarvi memory space as follows: (the symbols are defined in avalon_addr.h and should all be added to AVALON_BASE to compute the address from the Yarvi perspective.).

ComponentYarvi BaseLengthAccessSymbol
64MB SDRAM0x8000000064MiBRead/writeSDRAM_BASE
— includes display framebuffer0x80000000480*272*16 bits = 261120 bytesRead/writeFRAMEBUFFER_BASE
LED output0x8400000032 bitsWrite onlyPIO_LED_BASE
Left dial counter0x8400010032 bitsRead onlyPIO_ROTARY_L
Right dial counter0x8400020032 bitsRead onlyPIO_ROTARY_R
Display buttons0x8400030016 bitsRead onlyPIO_BUTTONS

We provide a display emulator program to implement these in software that Modelsim is able to connect to. You can run the emulator as follows:

  • First, download a new copy of the Yarvi source and software, put them in a directory exercise3.
  • Open a new terminal, source the $CLTEACH/swm11/setup.bash script, and change to the directory containing your new Yarvi source.
  • Type: make
    This will build the emulator for your local machine.
  • Type: ./displayemul
    This will run the emulator, which will then listen for connections from Modelsim. It will only accept one connection at a time.
  • A black window will appear. This shows the display framebuffer, which you can write to from the Yarvi. You can also use the following keypresses:
KeyActionBit in button register
Cursor leftLeft dial anticlockwisen/a
Cursor rightLeft dial clockwisen/a
Cursor upRight dial anticlockwisen/a
Cursor downRight dial clockwisen/a
NLeft dial click14
MRight dial click13
AButton A press3
BButton B press1
XButton X press2
YButton Y press0
SNavigation stick left8
FNavigation stick right9
ENavigation stick up11
CNavigation stick down10
DNavigation stick centre click12

Now run a Yarvi simulation in Modelsim. You should see:

	# Display emulator setup...
	# Successful connection to display emulator
	

to indicate a connection has been successful. When you read or write emulated devices, for example:

        lui s10, (0x80000000>>12)
        sw a0, 0(s10)
	

you should see logs in both the ModelSim window:

	#   420  0000001c                                  pre: imm=0x00080000, zero fill
	#   440  0000001c                                  x26 <- 0x80000000
	#   440  0000001c lui    x26, 0x80000
	# Display emulator write avalon[0] byteenable f <- 0xc
	#   480  00000020 sw     x10, 0(x26)
	

and on the terminal in which you are running the display emulator:

	Write address=0, byteenable=0xf, data=0xc
	---- SDRAM write address 0x00000000 byte enable 0xffffffff <= 0x0000000c
	
volatile here is an instruction to the C compiler that it should not attempt to optimise away memory accesses for this type. This is because we are accessing a hardware register that may change, rather than memory which we expect to be constant if we do not write to it. If we did not declare it volatile, the compiler could optimise away multiple reads and instead return stale data.

The following C functions will enable access to the peripherals:

	// this file contains the locations of the registers as described in the table above
	#include "avalon_addr.h"

	int avalon_read(unsigned int address)
	{
		volatile int *pointer = (volatile int *) address;
		return pointer[0];
	}

	void avalon_write(unsigned int address, int data)
	{
		volatile int *pointer = (volatile int *) address;
		pointer[0] = data;
	}
	

For example, you read the left dial position with:

C's types 'int', 'long', 'long long', short' and 'char' can vary in size depending on the compiler and CPU architecture. While we know the RISC-V GCC uses 'int' (32 bits) and 'short' (16 bits), normally it is better to #include <stdint.h> which provides types like uint16_t for a 16-bit unsigned integer. We don't do this here to make the code easier to read by not needing explicit casts, forcing conversion of one type to another.
	int left;

	left = avalon_read(AVALON_BASE + PIO_ROTARY_L);
	

When doing so, you should see both a log in the Modelsim trace and a message in the display emulator window.

You can plot a pixel with the following function:

	// our pixel format in memory is 5 bits of red, 6 bits of green, 5 bits of blue
	#define PIXEL16(r,g,b) (((b & 0x1F)<<11) | ((g & 0x3F)<<5) | ((r & 0x1F)<<0))
	// ... but for ease of programming we refer to colours in 8/8/8 format and discard the lower bits
	#define PIXEL24(r,g,b) PIXEL16((r>>3), (g>>2), (b>>3))

	#define PIXEL_WHITE PIXEL24(0xFF, 0xFF, 0xFF)
	#define PIXEL_BLACK PIXEL24(0x00, 0x00, 0x00)
	#define PIXEL_RED   PIXEL24(0xFF, 0x00, 0x00)
	#define PIXEL_GREEN PIXEL24(0x00, 0xFF, 0x00)
	#define PIXEL_BLUE  PIXEL24(0x00, 0x00, 0xFF)

	#define DISPLAY_WIDTH	480
	#define DISPLAY_HEIGHT	272

	void vid_set_pixel(int x, int y, int colour)
	{
		// derive a pointer to the framebuffer described as 16 bit integers
		volatile short *framebuffer = (volatile short *) (AVALON_BASE + FRAMEBUFFER_BASE);

		// make sure we don't go past the edge of the screen
		if ((x<0) || (x>DISPLAY_WIDTH-1))
			return;
		if ((y<0) || (y>DISPLAY_HEIGHT-1))
			return;

		framebuffer[x+y*DISPLAY_WIDTH] = colour;
	}
	

Exercise

Write some C code to implement the Etch A Sketch. Your software should read the dial inputs and set the relevant pixel to white. When the left or right dial is clicked, the screen should clear to black. You'll find clearing the screen is slow in the emulator, so don't clear it on startup.

Optional exercise for starred Tick

Take a copy of your Etch A Sketch code and modify it to implement Pong.

Pong differs from Etch A Sketch in a key way: Etch A Sketch is time invariant, while Pong becomes unplayable if gameplay is too fast or too slow. To remedy this, first write a delay function that spins for 100 microseconds, and calibrate this with Modelsim. While you are running the code in simulation you should omit delays, but you will need to insert delays in the code when it runs on FPGA. (We can also include timers on the FPGA for real-world timing but, since the simulation speed depends on workload, it is difficult to calibrate gameplay in simulation)