This week I'm taking a break from my series on Android System Verification to talk about something completely different.
One of the interesting things about working on Incisive Software Extensions (ISX) is the wide variety of embedded software and processors I learn about. Recently, I worked on a project with a very small Verilog CPU. As a tool provider we do our best to make all of our tools as generic as possible so they can be used regardless of the CPU type, C compiler, CPU model type, memory model type, etc. Working with this small CPU I noticed some interesting behavior right away related to the C code. Most of the time I'm working with 32-bit CPUs. I would say the majority of projects have 32-bit registers and C data types that are usually the same: char is 1 byte, short is 2 bytes, int and long are 4 bytes, and long long is 8 bytes. Sometimes the actual hardware bus is 64-bits or there are multiple busses, but this is isolated from the programming model. On this particular occasion I saw that an int was only 2 bytes. After a bit of digging I saw that a void * (or any pointer) is only 2 bytes since the CPU has 16 address bits. Adjusting to this new world of 2 byte integers and 2 byte pointers can take some time to get used to. If you are interested in the details I'm sure there are C standards you can study. Here is a short summary I was able to come up with:
A short int must not be larger than an int
An int must not be larger than a long int
A short int must be at least 16 bits
An int must be at least 16 bits
A long int must be at least 32 bits
A long long int must be at least 64 bits
The "common" sizes I gave above are actually called ILP32, which stands for Integer, Long, and Pointer 32.
Another thing about small CPUs is they generally don't have caches. This means that if the data bus is only 8-bits, every instruction needs to be fetched from memory over and over again. The results can be quite surprising if you are used to using integers for variables and you want to write a value to memory. There will be lots of instruction fetching and the actual integer write will take 2 writes on the bus. All of this means that every instruction is important and every data access is important to get the maximum performance from a system.
Today, I'm going to provide a quick start for anybody interested in working with a small Verilog CPU to learn how to run it with Incisive. This article will cover the initial setup of how to create a simulation and then compile C code and run it on the CPU. Future articles will cover the use of additional tools to better understand software execution and how to monitor and verify code running on the CPU. This blog area is called "System Design and Verification", so is this small Verilog simulation really about system verification? I'm not sure, but whenever there is a mix of hardware and software it's interesting for me, and I'm sure many others also.
To get started download the Wishbone High Performance z80 project from opencores.org. There are many 8-bit CPUs that have been around for years, but I found this z80 to be a good one for demonstration purposes. You may need to sign up for a login to OpenCores to get the download. I downloaded the file wb_z80_latest.tar.gz
After extracting the tar file go to the wb_z80/trunk/ directory and make some modifications to get things going.
First, we need a script to compile and run the design, this is pretty easy. I created a run script and a run.f file with the Verilog files to compile.
The 2 files look like this:
jasona-lin:86 % more run
irun -gui -f run.f +access+r -linedebug
jasona-lin:87 % more run.f
The next problem is the Verilog code is expecting to load a memory file that represents the software program. You can see this in the file z80_bist_logic.v.
$readmemh( "readmem.txt", z80_testbed.i_z80_core_top.i_z80_sram.mem );
Here is where we have to do some hacking. The wb_z80 comes with a Perl script called runit.pl that creates the software memory file and runs Icarus Verilog. Clearly this was done on Windows as there is an assembler called AS80.EXE that will not be much use on Linux. Maybe it would run using a Windows emulator like Wine, but never mind. I'll save you the trouble of producing the needed readmem.txt for the readmemh command. I also posted 2 files that are created by the assembler, the listing file named BJS80TST.LST and the hex file named BJS80TST.HEX. If you comment out the call to as80.exe you can still generate the readmem.txt yourself using the runit.pl script if you fix the DOS paths by removing the \\ and replacing with a normal / for Linux. You can also comment out the Icarus Verilog part at the end. For the quickest path, just put the readmem.txt in the trunk/ directory and you can run the initial test. We are interested in C anyway so this assembly code is not useful. It is some kind of test suite for the CPU itself.
Now we can run the test with the run script and readmem.txt file. Take a look at the waveforms and logfile to see how the simulation is structured. You should see something like this on the console or in the irun.log file.
BL mem  = c3
BL mem  = c6
BL mem  = 2b
BL mem  = xx
BL mem  = xx
BL mem  = xx
BL mem  = xx
BL mem  = xx
BL mem  = 3e
BL mem  = 02
0123456 12345 12345 54321 54321
0123456 12345 12345 54321 54321
If above lines match instruction test passed
TB bist ok
Simulation complete via $finish(1) at time 144905 NS + 0
So far so good. The next thing to do is get a C compiler for the z80 and create and run a C program. For this I recommend Small Device C Compiler (SDCC). It works very well on Linux and supports the z80. I downloaded version 2.9.0 for Linux. Installation is nothing more than untaring the file and putting the bin/ directory in your path.
To get started I created a very simple C program simple.c and header file simple.h. Use the links to view or download them. I created a new directory wb_z80/trunk/sw to hold the simple C program. Next I created a small script to compile the C program called comp.
jasona-lin:100 % more comp
as-z80 -o crt0.o crt0.s
sdcc -mz80 --no-std-crt0 --data-loc 0x6000 simple.c crt0.o
There are 2 things to highlight. First, I'm not using the provided crt0.s that comes with the compiler. Instead I copied and modified it to add a way to cause the Verilog simulation to stop. It's common when running code on a CPU in an HDL simulation to use the software to trigger the end of the simulation. If the software completes correctly it writes to some address and some Verilog code is added to watch for this write and then stop or finish the simulation.
Second, I moved the data area of the C program to a new location of 0x6000. This is to accommodate for the memory available in the hardware design. The default location happens to be above (or beyond) the memory in the hardware design so things don't run very well if there is no memory there. This simple concept demonstrates the relationship between hardware and software and the need to always be mindful of the hardware when creating software.
Once the software is compiled, we need to create a new readmem.txt file for the readmemh to load. I modified the original runit.pl to just change the file names to load simple.ihex and write out readmem.txt. The new version is runsimple.pl. This also demonstrates another common thing to learn. C compilers don't output files in the correct format for readmemh in Verilog. They create formats like ELF, S-record, and Intel Hex. Scripts like this Perl script are commonly used to process compiler output into something readmemh can use.
There are only a couple of hardware modifications to make. First, there was a timeout in the Verilog code that causes it to finish before the software is done so this is removed. Then I put in the monitor to watch for the secret write from the software that signals the end of the program. These changes are done in the file z80_testbed.v.
In the crt0.s you see a write of data 0xee to address 0x999
In the Verilog code it's monitored as:
if (wb_we_o && (wb_dat_o == 8'hee) && (wb_adr_o == 16'h0999))
$display("Software stopped the simulation");
That's it! We have created a working simulation of a z80 Verilog CPU and we have demonstrated how to compile C code using the Small Device C Compiler and run the C code with the Incisive simulator. In future articles we will look at some additional topics related to debugging and verifying embedded software with Incisive.
I know many of you have figured out clever ways to extend SimVision to help figure out what is happening with a C program running in an HDL simulation like this. Please feel free to share them.