Getting started with Litex on a Tang Nano 9K

I always liked the idea of Litex, a framework to easily build a SoC for an FPGA, but never really made time to try it out. So let’s change that and write down how it went! I am using a Sipeed Tang Nano 9K FPGA board, which is a fairly cheap bit of kit, but most of this blog should apply to any supported FPGA.

Now, there was a little bit of a learning curve. Litex is written in Python, or a little more specific, it uses Migen, a python based tool that generated Verilog. I never coded much Python, let alone Migen. I wanted to learn a couple of things to say I know the basics of Litex

  1. Understand a minimal SoC example
  2. Customize a SoC with some peripherals already in Lite
  3. Code custom firmware and run it on the created SoC
  4. Have a way of working that’s somewhat pleasant to use

Before I get to all of that, let’s first install Litex and build an example!

Building the example SoC

If running a modern Linux, this is fairly straightforward, and following their guide works on Debian 12. To be able to build some examples, the standard or full config is needed, and a RISC-V toolchain needs to be installed. Luckily the quick start guide nicely explains all that and how to do so.

Now to the Gowin toolchain. It’s not open source, but free at least, but you need to apply for a free license. It can be downloaded here after registering. An open source toolchain is being developed but at the time of writing not complete enough to use with Litex.

The gw_sh binary from Gowin has to be added to the path, so a line in the likes of this in .bashrc should do:

PATH="$PATH:/path/to/gowin/IDE/bin"

After installation, navigate to the “litex/litex-boards/litex_boards/targets” folder and run:

./sipeed_tang_nano_9k.py --build --flash

This will take a good bit of time, but it should compile, synthesize, PnR and then flash the fpga. The LEDs will blink in a nice pattern and when a serial port is opened at 115200 baud to the board, the following should greet you:

Pfff, that was a bit of work to get going, something to put in a docker perhaps…. :)

Now I can build the example, but I have no clue what it all does. Luckily a minimal example is included, let’s just take a peek at that. I took a moment to remove all that I could from the sipeed 9K example to more match the simple.py example and ended up with this:

import os
from migen import *

from litex.gen import *

from litex_boards.platforms import sipeed_tang_nano_9k

from litex.build.io import CRG
from litex.soc.integration.soc_core import *
from litex.soc.integration.soc import SoCRegion
from litex.soc.integration.builder import *

kB = 1024
mB = 1024*kB

# BaseSoC ------------------------------------------------------------------------------------------
class BaseSoC(SoCCore):
    def __init__(self, **kwargs):
        platform = sipeed_tang_nano_9k.Platform()

        sys_clk_freq = int(1e9/platform.default_clk_period)

        # CRG --------------------------------------------------------------------------------------
        self.crg = CRG(platform.request(platform.default_clk_name))

        # SoCCore ----------------------------------------------------------------------------------
        kwargs["integrated_rom_size"] = 64*kB  
        kwargs["integrated_sram_size"] = 8*kB
        SoCCore.__init__(self, platform, sys_clk_freq, ident="Tiny LiteX SoC on Tang Nano 9K", **kwargs)

# Build --------------------------------------------------------------------------------------------
def main():
    from litex.build.parser import LiteXArgumentParser
    parser = LiteXArgumentParser(platform=sipeed_tang_nano_9K_platform.Platform, description="Tiny LiteX SoC on Tang Nano 9K.")
    parser.add_target_argument("--flash",                action="store_true",      help="Flash Bitstream.")
    args = parser.parse_args()

    soc = BaseSoC( **parser.soc_argdict)

    builder = Builder(soc, **parser.builder_argdict)
    if args.build:
        builder.build(**parser.toolchain_argdict)

    if args.load:
        prog = soc.platform.create_programmer("openfpgaloader")
        prog.load_bitstream(builder.get_bitstream_filename(mode="sram"))

    if args.flash:
        prog = soc.platform.create_programmer("openfpgaloader")
        prog.flash(0, builder.get_bitstream_filename(mode="flash", ext=".fs")) 
        prog.flash(0, builder.get_bios_filename(), external=True)

if __name__ == "__main__":
    main()

Wow, that’s around 50 lines, not too bad. But it turns out, a lot of magic happens in Litex to keep this small. Let’s try to break it down a bit!

But what does it all do?

First, some imports and defines,

from litex_boards.platforms import sipeed_tang_nano_9k

imports the platform file, this file contains all the IOs and peripherals, it also includes some information about the programmer to use and the clock speed of the on board oscillator. For a custom board, a file like this has to be created from scratch.

The rest of the imports pull in migen, the HDL language used in Litex, and some basic builders to build the SoC.

import os
from migen import * 

from litex.gen import *

from litex_boards.platforms import sipeed_tang_nano_9k

from litex.build.io import CRG
from litex.soc.integration.soc_core import *
from litex.soc.integration.soc import SoCRegion
from litex.soc.integration.builder import *

kB = 1024
mB = 1024*kB

Now time to jump to the bottom of the code and take a look at the main function:

def main():
    from litex.build.parser import LiteXArgumentParser
    parser = LiteXArgumentParser(platform=sipeed_tang_nano_9K_platform.Platform, description="Tiny LiteX SoC on Tang Nano 9K.")
    parser.add_target_argument("--flash",                action="store_true",      help="Flash Bitstream.")
    args = parser.parse_args()

    soc = BaseSoC( **parser.soc_argdict)

    builder = Builder(soc, **parser.builder_argdict)
    if args.build:
        builder.build(**parser.toolchain_argdict)

    if args.load:
        prog = soc.platform.create_programmer("openfpgaloader")
        prog.load_bitstream(builder.get_bitstream_filename(mode="sram"))

    if args.flash:
        prog = soc.platform.create_programmer("openfpgaloader")
        prog.flash(0, builder.get_bitstream_filename(mode="flash", ext=".fs")) # FIXME
        prog.flash(0, builder.get_bios_filename(), external=True)

First of all, the LitexArgumentParser is imported and instantiated. This is a super handy feature in Litex that makes it easy to custimize a SoC with command line arguments. Run

 ./sipeed_tang_nano_9k.py --help 

to get all the options, here is just a few of them for example:

Yes, selecting a different CPU is just a command line argument away, awesome!

Then the BaseSoc function is called, which is used to customize our SoC. We’ll look at that in a bit. Then the Litex Builder is called with our SoC as argument to generate the final SoC.

At last, the –load and –flash argument are handled. They both call the OpenFPGALoader tool to either load a bitstream in RAM or flash it in SPI flash on the FPGA board. The OpenFPGALoader tool is installed with the Litex_setup script.

The actual SoC!

# BaseSoC ------------------------------------------------------------------------------------------
class BaseSoC(SoCCore):
    def __init__(self, **kwargs):
        platform = sipeed_tang_nano_9k.Platform()

        sys_clk_freq = int(1e9/platform.default_clk_period)

        # CRG --------------------------------------------------------------------------------------
        self.crg = CRG(platform.request(platform.default_clk_name))

        # SoCCore ----------------------------------------------------------------------------------
        kwargs["integrated_rom_size"] = 64*kB  
        kwargs["integrated_sram_size"] = 8*kB
        SoCCore.__init__(self, platform, sys_clk_freq, ident="Tiny LiteX SoC on Tang Nano 9K", **kwargs)

The BaseSoC class creates the SoC to be fed into the Litex Builder in a bit. A basic SoC in Litex contains a Vexriscv CPU, wishbone bus, some RAM, ROM, a timer and a UART peripheral. But that’s the basics, it can be customized here. For now I specify the system clock frequency and generate a CRG. CRG means Clock Reset Generator and should contain any clocks and resets. For now there is just a single clock but let’s look into that more later on.

I also specify the rom and ram size, but this could even be removed if the defaults are fine. All this information is fed into the SoCCore.__init__ function that returns our SoC.

And there we go, a minimal SoC, awesome. It can also be viewed in full in Github.

Now let’s slowly add some new functions to it!

Adding a clock reset generator

The CRG currently is very limited compared to the one in the example. There isn’t even a reset button! Let’s change that and introduce a PLL and a Reset.

class _CRG(LiteXModule):
    def __init__(self, platform, sys_clk_freq):
        self.rst    = Signal()
        self.cd_sys = ClockDomain()

        # Clk / Rst
        clk27 = platform.request("clk27")
        rst_n = platform.request("user_btn", 0)

        # PLL
        self.pll = pll = GW1NPLL(devicename=platform.devicename, device=platform.device)
        self.comb += pll.reset.eq(~rst_n)
        pll.register_clkin(clk27, 27e6)
        pll.create_clkout(self.cd_sys, sys_clk_freq)

Compared to before, the CRG now uses one of the user buttons as a reset input. A PLL is generated that for now has the same input frequency as output frequency, but this can be changed by passing a parameter with a requested system clock frequency, neat! The reset pin resets the PLL, which in term resets the CPU. A full diff compared to the previous SoC can be seen here.

Finally, peripheral time!

There are quite a few peripherals already available in Litex, timers, uart, I2C, SPI and more. Sadly the documentation can be a little lacking, but after some digging around I got most of them to work. So let’s add a few things in the sipeed_tang_nano_9k.py file!

To add some peripherals, first they need to be imported:

from litex.soc.cores.timer import *
from litex.soc.cores.gpio import *
from litex.soc.cores.bitbang import I2CMaster
from litex.soc.cores.spi import SPIMaster
from litex.soc.cores import uart

There, that takes care of the most common ones. Instantiating is luckily pretty easy too!

        self.timer1 = Timer()
        self.timer2 = Timer()
        
        self.leds = GPIOOut(pads = platform.request_all("user_led"))
        
        # Serial stuff 
        self.i2c0 = I2CMaster(pads = platform.request("i2c0"))
        
        self.add_uart("serial0", "uart0")
        
        self.gpio = GPIOIn(platform.request("user_btn", 1))

Two more timers, some LEDs, I2C, UART and a GPIO input, just in a few lines. That is a TON easier then doing this in VHDL or Verilog. Now the platform file needs to be expanded a bit too so Litex knows what to place on what IOs:

    ("gpio", 0, Pins("25"), IOStandard("LVCMOS33")),
    ("gpio", 1, Pins("26"), IOStandard("LVCMOS33")),
    ("gpio", 2, Pins("27"), IOStandard("LVCMOS33")),
    ("gpio", 3, Pins("28"), IOStandard("LVCMOS33")),
    ("gpio", 4, Pins("29"), IOStandard("LVCMOS33")),
    ("gpio", 5, Pins("30"), IOStandard("LVCMOS33")),
    ("gpio", 6, Pins("33"), IOStandard("LVCMOS33")),
    ("gpio", 7, Pins("34"), IOStandard("LVCMOS33")),
    
    ("i2c0", 0,
        Subsignal("sda", Pins("40")),
        Subsignal("scl", Pins("35")),
        IOStandard("LVCMOS33"),
    ),
    
    ("uart0", 0,
        Subsignal("rx", Pins("41")),
        Subsignal("tx", Pins("42")),
        IOStandard("LVCMOS33")
    ),

Perfect! But there is a small tiny issue still. Well two actually. But the first one that is bothering me is that editing this all in the litex-boards repo is not really, ideal.

Time to make a separate folder for all of this, heck, a docker would be even nicer.

Containerize it all

When talking with a friend about running this all on a macbook, and the gowin IDE being not available for Mac OS he whipped up a small docker container for setting it up, just enter a license file location and good to go! I made a few small changes to it, mostly to set the workdir and add vim. So checkout that repo and give it a go!

With this, it should be possible to run Litex with the Gowin tooling reliable on any computer, regardless of OS and distro.
Ok, that is one issue taken care of, now to separate it all. I finally settled on a folder with the following structure:

platform contains the platform file and software the C source code for the program on the SoC, which can be found on my Github.

When starting the docker container, I bind the folder to docker as such:

docker run --rm \                                
    --platform linux/amd64 \
    --mac-address xx:xx:xx:xx:xx:xx \
    -v "${HOME}/gowin_E_xxxxxxxxxx.lic:/data/license.lic" \
    -v ${HOME}/Documents/Git/LitexTang9KExperiments:/data/work \
    -it gowin-docker:latest

The license file depends on mac address, so be sure to set your mac address in the docker to match the one for your license. Best would be to use a mac address generator and check that nothing in your network uses the generated mac address so there is no chance of any collisions or other network shenanigans.

After starting the container I end up in the correct folder immediately and I’m a single

./sipeed_tang_nano_9k.py --build

away from building!

Alright, the software folder is already showing up in there, but how did I get that far is another story.

Software woes

To start with, I looked at the demo application in Litex, and compiled that. It can be “uploaded” by integrating it in the internal rom of an SoC, but that means rebuilding the entire SoC on every software change. That’s quite hassle if you want to quickly reiterate on code.

Luckily, Litex also has a nice program called litex_term, which can be used to upload binaries and to have a terminal connected to the SoC. The default bios in Litex can accept a binary on boot, and run it, a little like the bootloader in an Arduino. Using it is pretty simple, run

litex_term /dev/TTYhere --kernel=yourapp.bin

and to upload the binary again after a change, just reset the board!

Now, this means there must be some RAM on your SoC that can be changed and is not used for the bios code. Can’t have your new code loaded in the RAM of the bios application of course. On some FPGA’s there is enough space to have 2 big blocks of RAM, but I opted to use the internal HyperRAM of the FPGA. This is also what the example uses and that seems to work quite well. The code to add this to the SoC is as follows:

        # HyperRAM ---------------------------------------------------------------------------------
        if not self.integrated_main_ram_size:
            # TODO: Use second 32Mbit PSRAM chip.
            dq      = platform.request("IO_psram_dq")
            rwds    = platform.request("IO_psram_rwds")
            reset_n = platform.request("O_psram_reset_n")
            cs_n    = platform.request("O_psram_cs_n")
            ck      = platform.request("O_psram_ck")
            ck_n    = platform.request("O_psram_ck_n")
            class HyperRAMPads:
                def __init__(self, n):
                    self.clk   = Signal()
                    self.rst_n = reset_n[n]
                    self.dq    = dq[8*n:8*(n+1)]
                    self.cs_n  = cs_n[n]
                    self.rwds  = rwds[n]
            # FIXME: Issue with upstream HyperRAM core, so the old one is checked in in the repo for now
            hyperram_pads = HyperRAMPads(0)
            self.comb += ck[0].eq(hyperram_pads.clk)
            self.comb += ck_n[0].eq(~hyperram_pads.clk)
            self.hyperram = HyperRAM(hyperram_pads)
            self.bus.add_slave("main_ram", slave=self.hyperram.bus, region=SoCRegion(origin=self.mem_map["main_ram"], size=4*mB))
            
        self.add_constant("CONFIG_MAIN_RAM_INIT") # This disables the memory test on the hyperram and saves some boottime

Now that is one hurdle, but I want to be able to compile my own code, separate from the litex repo’s, but while using their pre-made drivers and such. After some experimentation, I ended up with the following makefile. The magic bits are the build and include dirs at the top:

BUILD_DIR=../../build/sipeed_tang_nano_9k
SOC_DIR=/usr/local/share/litex/litex/litex/litex/soc/

include $(BUILD_DIR)/software/include/generated/variables.mak
include $(SOC_DIR)/software/common.mak

Oddly enough, in the readme for the demo, Litex uses a small python script to compile. I decided to just stick with a makefile as that is also what the python file seems to call. My code is very much based on the demo application, first stripped and then with new code for the new peripherals added.

Peripheral drivers

After generating an SoC with some IOs, I2C and more, I want to use those peripherals! Most of them are quite simple to use, but there is not really any documentation on how to do so. The best course of action is to look at the migen code and let Litex generate a document with all the registers. The can by done by adding the “–soc-csv” option. For example:

./sipeed_tang_nano_9k.py --build --soc-csv=soc.csv

will output a file soc.csv with all the registers inside. –soc-json and –soc-svd are also possible for JSON and SVD files.

Some C include files are also generated on a build, especially csr.h in the build/sipeed_tang_nano_9k/software/include/generated/ is very useful, and for small peripherals using the functions in here is perfectly viable.

As an example, for reading the gpio pins, the “gpio_in_read” function works as expected.

For some, there are drivers available in Litex. I2C for example has a driver that works really well and can handle more then one I2C peripheral being generated, awesome!

One thing that was not clear however was using interrupts, so let’s take a dive into that real quick!

Interrupt woes

Enabling interrupts is pretty easy on the Litex/FPGA side of things. In general the irq.add function will take care of all that! For example:

        self.gpio = GPIOIn(platform.request("user_btn", 1), with_irq=True)
        self.timer1 = Timer()
        self.timer2 = Timer()
        
        # And add the interrupts!
        self.irq.add("gpio", use_loc_if_exists=True)
        self.irq.add("timer1",  use_loc_if_exists=True)
        self.irq.add("timer2",  use_loc_if_exists=True)

Easy enough, some Litex magic will take care of it :)

But how to use them in software? After a look at the existing code, I found the interrupt handler here. But there is a tiny problem:

void isr(void)
{
	__attribute__((unused)) unsigned int irqs;
	irqs = irq_pending() & irq_getmask();
	if(irqs & (1 << UART_INTERRUPT))
		uart_isr();
}

I removed some #defines for clarity, but it will only handle the uart interrupt for the default uart! So either I need to change this file in Litex, or not use the Litex libraries.
Ooooor make a small change and a PR. What I did is change the above code to the following:

// Weak function that can be overriden in own software for any IRQ that is not the uart.
// Return true (not zero) if an IRQ was handled, or 0 if not.
unsigned int __attribute__((weak)) isr_handler(int irqs);

// Override by default with return 0
unsigned int isr_handler(int irqs)
{
	return 0;
}

...

void isr(void)
{
	__attribute__((unused)) unsigned int irqs;
	irqs = irq_pending() & irq_getmask();
	if(irqs & (1 << UART_INTERRUPT))
		uart_isr();
	else
		if(!isr_handler(irqs))
			printf("Unhandled irq!\n");
}

So a simple __weak__ function is defined at the top, a weak function means that if the exact same function exists anywhere else, it will override the weak function. If it doesn’t exist it will call the weak function.

This means that if an interrupt happens that is not the uart interrupt, it will call the isr_handler(). If you implement it in your own code, awesome, it goes there and runs that. Otherwise no harm done, it will call the one in here.

So in my own main.c I can just do this:

unsigned int isr_handler(int irqs)
{	
	unsigned int irqHandled = 0;
	if(irqs & (1 << GPIO_INTERRUPT))
	{
		GpioInClearPendingInterrupt();
		irqHandled = 1;
	}
        return irqHandled;
}

In this case, if the interrupt that happens is the GPIO_INTERRUPT, I will handle it and return 1, otherwise 0 and the isr can throw a nice warning :)
This is currently on a branch as a PR and not merged, so for now make sure to use that branch!

In the style of the original demo, I created a program with that reads the serial port and can execute a few commands to test I2C, gpio, timer interupts and so on. The full code can be found here. Now there is just one small thing left I’d like to figure out. Making my own peripheral!

Creating a custom peripheral

To get a bit of a feeling for making a peripheral, I decided to make a simple pwm peripheral. Something simple that just generates a PWM signal at a given frequency and duty cycle. Internally it should have a counter and when the counter is below or above a certain value toggle an IO pin to control the PWM duty cycle.

It should have a few registers:

  • An enable register, to enable/disable the PWM peripheral
  • A divider register, to be able to make lower frequency PWM signals
  • A maximum count register, it should count to this value and then reset it’s internal counter
  • A duty cycle register, if the counter is below this value, the IO pin should be low, otherwise it should be high.

That all sounds pretty doable, and while Migen reads quite different then Verilog or VHDL, it does make for some compact code because of all the Litex goodies.

For starters, making a register and connecting it to a CPU is very easy:

from migen import *

from litex.soc.interconnect.csr import *
from litex.gen import *

class PwmModule(LiteXModule):
    def __init__(self, pad, clock_domain="sys"):
        self.divider = CSRStorage(size=16, reset=0, description="Clock divider")

Just a few lines, and a simple peripheral is made! It just is a single 16 bits register but hey, that’s pretty amazing! No need to worry about CPU busses or anything. The CSRStorage is not the fastest method but for a peripheral like PWM it’s plenty.

So let’s quickly make that complete peripheral shall we!

from migen import *

from litex.soc.interconnect.csr import *
from litex.gen import *

class PwmModule(LiteXModule):
    def __init__(self, pad, clock_domain="sys"):
        
        self.enable = CSRStorage(size=1, reset=0, description="Enable the PWM peripheral")
        self.divider = CSRStorage(size=16, reset=0, description="Clock divider")
        self.maxCount = CSRStorage(size=16, reset=0, description="Max count for the PWM counter")
        self.dutycycle = CSRStorage(size=16, reset=0, description="IO dutycycle value")
        
        divcounter = Signal(16, reset=0)
        pwmcounter = Signal(16, reset=0)
        
        sync = getattr(self.sync, clock_domain)
        
        sync += [
            If(self.enable.storage,
                divcounter.eq(divcounter + 1),
                    If(divcounter >= self.divider.storage,
                        divcounter.eq(0),
                        pwmcounter.eq(pwmcounter + 1),
                        If(pwmcounter >= self.maxCount.storage,
                            pwmcounter.eq(0),
                        ),
                    )
                )
            ]
                    
        sync += pad.eq(self.enable.storage & (pwmcounter < self.dutycycle.storage))
        

A few more registers, and some internal counters for dividing the clock signal and the PWM counter. A complete and usable peripheral in a little over 30 lines, awesome!

And to use this peripheral in the SoC, just a single line is needed:

self.pwm0 = PwmModule(platform.request("pwm0"))

On the software side, just a few registers need to be set:

        pwm0_divider_write(10);
	pwm0_maxCount_write(1000);
	pwm0_toggle_write(400);
	pwm0_enable_write(1);

And with that all settled, the complete code for the SoC can be found here.

Conclusion

That was fun! From nothing to an FPGA SoC with some custom peripherals going on, awesome. And that in quite a small number of lines of code. I’m pretty amazed by what Litex can do!

Of course, it’s not perfect, and currently the at times lacking documentation is a bit of a bummer. Hopefully this blog at least helps a little. I am considering to look at creating some more advances peripherals, that HDMI port is alluring! I also want to at some time look at mixing Verilog/VHDL and Migen, which should be very possible. For now I think that this blog is long enough already.

If you enjoyed this blog, please consider buying me a coffee!


Repairing a Tektronix 1240 logic analyzer

A long while back I picked up an old Tektronix logic analyzer. It was in decent shape but threw an error on the self test. To be more specific, the triggering board was faulty. But that means it was almost free and how can I say no to a repair job! Especially to some weird fun Tektronix box with a CRT touchscreen.

So let’s have a look inside and see what the issue might be.

Continue Reading

Let’s build a nixie watch

Ok ok ok, this is not a Nixie, it’s a Panaplex watch, but that sounds so much less fun. They work like nixies, but they are flat and often 7 segment displays, somewhat common in alarm clocks and tech gear.I got a few of them from a friend and compared to nixies, they are rather small and flat. So why not make a wristwatch with them!

Like nixies, panaplex displays require a high voltage to operate, around 200V. Which, on a wristwatch is a little challenging. I also wanted this to be a somewhat usable watch, unlike some of the nixie watches that, while cool, look quite cumbersome to wear due to their size.

This display is the ZM1570, which according to the datasheet, is similar to the ZM1550 but a little radioactive, fun!

So let’s look at what it takes to make a watch with these and how small all the circuitry can be.

Continue Reading

Computing like it’s 1985!

Yes I bought more 1980s computers. This time an HP9000/310. This is a computer launched in 1985 meant to control measuring equipment via HPIB, the interface later renamed to GBIP and still in use today. Now, a computer from the 80s meant to control EE equipment is not something I could not buy right?

The computer was stored working, but a little dirty. I first checked if the power supply was operating in spec and if there was any visible damage from things like leaking capacitors or batteries. That all seemed OK, so let’s turn it on!

And indeed, it still works! Now let’s take a good look at it, give it a clean and see if I can make it do something fun.

Continue Reading

Restoring a Siemens luggable computer, now in video!

A while back I got a Siemens luggable computer from a friend. It’s a computer from the mid 1980s and it was not working anymore. The latest state was “smoke came out when I turned it on” so it needed some repairs. This is not a personal computer, but instead a computer made to program and debug Siemens Simatic S5 PLCs.

This was kind of perfect as I wanted to try out something new. I wanted to try making a video thingy/blog and see how that goes. So I guess this would be Just Another Electronics Video Blog? Don’t worry, it won’t replace the written blog, but I think that some topics are a little nicer on video and some are nicer written.

Continue Reading

Designing a dock for the Lichee RV

It took a few years, but it’s finally possible to order an affordable Linux capable RISCV CPU. The Lichee RV is a small System On Module (SOM) with the Allwinner D1 System on Chip (SoC). This SoC contains a 1Ghz RISCV64GCV single core CPU. The SOM comes with 512MB of RAM, although some 1GB versions have popped up, and costs less then 30 euro’s delivered. The CPU on the Allwinner D1 is also open source, which is pretty neat. Sipeed makes a ton of interesting boards, like the FPGA boards I recently covered. They didn’t sponsor this post, I just think they make cool stuff!

There is also a small dock that it can be mounted on that has HDMI, USB, Wifi and more broken out. But I wanted to have a few different interfaces. I like having good old Ethernet and I also wanted a headphone jack. So how hard can it be to design a dock with those interfaces and get Linux to co-operate? Let’s find out!

Continue Reading

A single board computer from before the Raspberry Pi

When the Raspberry pi launched in 2012, it sold out immediately and became an instant hit overnight. The combination of a powerful enough Linux board, capable of Full HD video and an unbelievably low price of $25 was unheard of back in the day. And after 10 years, well over 40 million of them have been sold.

Of course, this was not the first Single Board Computer (SBC) that could run Linux. When I saw an add pop up on the local version of Ebay with some Atmel Linux boards from 2007 I was curious. When I saw it was an AVR32 CPU, something that Atmel discontinued quite a while ago, I gotten even more curious and picked them up. It even came with a CD with the original board support package (BSP)

So let’s have a look at this SBC, the AVR32 CPU, how to use them in 2022 and what they can still do.

The Atmel NGW100 SBC

The NGW100 was a SBC from Atmel, which was $100 or $50 with student discount from what I could find. The AVR32 CPU ran at 140Mhz and was paired to 32MB of SDRAM. A big difference from the 700Mhz CPU and 256/512MB RAM of the first Raspberry pi! For storage, it has 8MB parallel flash and 8MB SPI flash. The latter can only be used for data storage. Of course there is an SD-card slot but it cannot boot from that directly. A bootloader like u-boot has to be located on the 8MB parallel flash which could then boot from SD-card.

Now of course, this board has not 1 but 2 network ports! That makes up for the lack of video out right? And yes, there used to be OpenWRT support for this board, though I doubt it makes for a good router nowadays.

Continue Reading

A storage scope from the early 90s

I gotten yet another odd piece of electronic test equipment. This time something that seems to be an early storage oscilloscope. It looked very interesting as it has more buttons then I have seen on a scope yet, so I had to pick it up (I swear I don’t have a problem :) )

I couldn’t find much information online about it. No user manuals, let alone a service manual. I did find a little bit of information calling it a universal waveform analyzer. So let’s see what it is, how to use it and what’s inside. Warning, this blog post is a bit picture heavy!

Continue Reading

Sipeed Tang 4K ramblings

Sipeed is a China based company that makes all kinds of interesting dev boards for CPU’s and FPGA’s. I have looked at one of their boards before and recently saw that they released something new that looked like fun. It’s called the Sipeed Tang 4K. The name refers to a smaller FPGA board, the Tang Nano. The Tang 4K however has an FPGA with ~4000 LUTs instead of the ~1100 of the Tang Nano. At around $20 it’s also very nicely priced and comes with some interesting features.

The most obvious one is the HDMI port. Sadly 4K video is not going to be possible with this, but their example does a very respectable 720P. Having HDMI is really nice in my opinion as driving displays is a great showcase of something an FPGA can do compared to a microcontroller.

Looking a little further, more and more fun stuff started popping up. The Gowin FPGA on this board has an ARM M3 core with some peripherals build in, as well as 8MByte of HyperRAM, 8MB of PSRAM and 4MB of NOR flash. Even without the FPGA, an ARM M3 with that amount of RAM and flash is pretty impressive.

So let’s look at using it and adjust a few existing FPGA projects to run on this FPGA.

The programming environment

The Gowin FPGA on the Tang 4K can be programmed using the proprietary Gowin IDE, though an open source toolchain is being worked on. The Gowin IDE is free, but it needs an account and a license to function. Luckily the latter is provided within a few hours in after requesting it. The IDE itself is a fairly small at around 1GB installed, at least compared to Vivado/Quartus program. It even worked on Ubuntu 20.04 without any issues:

Compared to other vendor IDEs, this one is actually pretty simple to use. When loading an example project, 3 tabs can be found. Design, Process and Hierarchy. Design contains all the files and info on the selected FPGA. Hierarchy contains the HDL files in hierarchical view, when there is a design error, it will state so. After synthesizing it also contains the resource usage per file.

Finally Process, which contains the Synthesize, place and route and program device stuff. Double-click on each to start this task and a minute later it’s done, at least for a small example. All in all I find it a fairly simple and usable IDE. Of course, like all FPGA IDE’s, the build in text editor is pretty bad. But at least there is a dark theme :)

Resources are nicely visible per file

The example projects

There is a github page with exactly 2 examples. The ubiquitous blink a led example, and an example that generates an image inside the HyperRAM and uses the HDMI to output it. That sounds a lot more interesting, so let’s run it!

That looks fun. Let’s have a little look at how it works!

Oh, encrypted blobs of verilog for the HyperRAM, HDMI and framebuffer. That’s not fun :(
I think we can do a little better then that. Since I started tinkering with the FPGA, someone else made a cool project with this board, a gameboy to HDMI adapter, though it also uses some encrypted verilog for the HDMI part.

Using the HDMI

HDMI has been implemented on FPGAs quite a few times before, though a reasonably fast FPGA is needed for directly outputting HDMI. I found this project online, which supports several FPGAs. So let’s port that over to the Gowin FPGA. Now, just HDMI is a little boring, much nicer if there is something cool to show on the screen.

I always liked the idea of a simple BASIC computer, like the 6502. There is a nice FOSS project with the 6502 on an FPGA from Grant that could work. So the idea is, run a 6502 computer that boots BASIC with HDMI out on a small FPGA board. Even better, I just need to hookup a few already existing projects, easy enough right? Right?

I started with just getting the HDMI to work. Luckily there is a porting guide for the projf display controller. A PLL is needed for the HDMI clock, as well as serialization and differential signalling. The porting guide mentions 10:1 serialization for HDMI, and this is something the FPGA can luckily do. Gowin has a nice document describing all the IO modes and is full of sample Verilog and VHDL snippets, great!

The FPGA should be capable of 720P, so let’s go for that. The clock speed needed is 74.25Mhz, but as the data is clocked out serially with 10 bits per clock, the clock speed on the IO needs to be 742.5Mhz. That’s quite something. Luckily this is done via the 10:1 serializer, which needs 5 times the base clock. Just a comfy 371Mhz needed. I used the PLL and clock divider wizards from the IP wizard tool in the IDE, which happily generated this.

After adding the code for the OSER10 serializers and the TLVDS_OBUF DDR outputs, this all worked without too much issues.

I optimistically tried 1080P, which gave a lot of timing errors and well, there was an attempt let’s say :)

Perhaps 900P is possible, but I haven’t tried. But with HDMI up and running, let’s look at that 6502!

VGA and HDMI timing issues

The 6502 page describes a few example projects with VGA output, even color VGA. So all I have to do is hook it up HDMI and BASIC like it’s 1977 right> Sadly it is a little more complicated then that. To output VGA, almost all projects generate their own VGA timings, And for HDMI, the display controller generates it’s own timings. Converting that to HDMI is a little complicated

After fighting around a bit, I decided to approach it a little differently. I opted for the 6502 computer with serial out so I can test that with a USB to UART converter to my PC first, and then add a terminal emulator and botch HDMI to that. I messed around with this great VT52 FPGA project before and was familiar with it’s code. Eventually I ended up matching the VGA timings in the VT52 with the HDMI timings until it worked. Not a great solution, but workable for now.

A better approach would be to completely replace the VGA parts, but perhaps for another time. For now, I am really happy being able to BASIC:

I exported the project to Github, it should be possible to directly open it in the Gowin IDE. The resulting binary is also included. To use this, a PS/2 keyboard needs to be connected to the FPGA. The PS/2 clock to pin 14 and the PS/2 data to pin 33. A level shifter is needed but for me it worked fine without for a while.

A RISCV CPU with HyperRAM

That’s HDMI up and running, let’s have a little look at the build in HyperRAM. First, what is HyperRAM?

HyperRAM is pseudo-static RAM, meaning that internally it’s DRAM, but with a controller that handles all the annoying DRAM stuff. The databus is 8 bit wide, so the number of pins needed is quite small. It’s popular for hobby FPGA projects because of those reasons. Generally it’s available in 8 MByte chips, but bigger capacities exist.

Gowin decided to just add some into the FPGA, most likely by having multiple die’s in the IC package. I wanted to work with HyperRAM for a while now, so that seems like a great excuse. There is also some PSRAM inside the FPGA as well, but let’s focus on HyperRAM for now. They sure have filled this chip to the brim with goodies.

RISC-V time

So how to test that memory, having a CPU to read/write data and print out the results to UART seems like a good choice. But what CPU to pick? I have done a whole blog about it, but I’ll pick one I haven’t covered yet, the NEORV32. It’s based on the NEO430, but with a RISC-V core. I really liked the NEO430 and so far the NEORV32 is even nicer. It has wishbone support and the base SoC is very complete and easy to use. The bootloader they offer is also a real life saver, and NEORV32 also supports optional hardware debugging.

There is a small downside, it doesn’t fit. The FPGA has 10 memory blocks, but to implement a 32 bit RAM or ROM that is also accessible per 8 bit word, 4 of those blocks are needed. With RAM, ROM, the bootloader and registers in RAM blocks, 12 would be needed. I tried to implement the registers in logic cells instead of RAM blocks, while costing a lot of logic at least the CPU would fit like this. In the end I opted to use the RISC-V e extension, which cuts the general purpose registers down from 32 to 16. With this implemented I only need 10 RAM blocks and around 70% of the FPGA’s logic cells, perfect.

Adding the RAM

Gowin recommends to use their own library, but in their documentation they list that the HyperRAM used is a Winbond model, most likely this one. They also go into some detail on how to use the HyperRAM.

In order to connect to the RAM, the top level should have the ports for the RAM with the correct names. The synthesizer will then deal with it and magically connect it to the RAM, neat.

  port (
    O_hpram_ck      : out std_logic_vector(0 downto 0);
    O_hpram_ck_n    : out std_logic_vector(0 downto 0);
    O_hpram_cs_n    : out std_logic_vector(0 downto 0);
    O_hpram_reset_n : out std_logic_vector(0 downto 0);
    IO_hpram_dq     : inout std_logic_vector(7 downto 0);
    IO_hpram_rwds   : inout std_logic_vector(0 downto 0)
  );

Now to add a HyperRAM controller, A popular open source HyperRAM controller is this one from Blackmesa labs, which should work nicely. And even better, Greg Davill made a simple wishbone wrapper for it for the bosonFrameGrabber project. So let’s hook that all up to the CPU and give it a go. I ran into a few issues, like the reset for the CPU being active low and HyperRAM being active high. But it was all communicating rather quickly. Good to note is that the CPU is written in VHDL and the HyperRAM controller in Verilog, the tooling has no problems mixing these two.

I made some simple code to read and write to the HyperRAM, and also added the RAM to the linker file so the CPU can use it as RAM. With a 54Mhz CPU and RAM, the read and write speed is around 12.5Mbyte/sec. Considering the HyperRAM controller is made for simplicity and not speed, this seems very reasonable.

This project can also be found on Github and requires a serial to USB converter to be connected to pin 22 (RX) and pin 23 (TX)

Conclusion

There are a lot more interesting things in this FPGA, another 8MB of RAM, NOR flash and an entire CPU. But for now, having 2 projects working with a lot less encrypted verilog seems like a good start. A lot of work being done on the open source toolchain, I am curious to see where that leads to in the future.

I hope these projects can help others get going using this interesting FPGA board. If you enjoyed reading this, consider donating me a coffee!


Let’s finish that lab power supply

A good while ago I started working on a lab power supply based on the HP E3610A design. After some issues getting all the needed ICs, and a few creative approaches later, I finally had all the parts to finish it. along the way I ran across a good number of fun issues, from I2C cables being too long, to enclosures not really fitting and much more. But at least it works and the front panel is looking pretty nice.

So let’s look at how it all came together, what worked well and what didn’t Of course, a few things went wrong:

oops
Continue Reading

Pages:12345