Tying into SDRAM Nov 9, 2016
Synchronous Dynamic RAM is perhaps the most common form of random access memory - it’s large (MBs, vs SRAM’s KBs), it’s fast (supporting 100 MHz and higher), and it’s low cost due to the widespread use in PCs and laptops for the past few decades.
But SDRAM chips are notoriously hard to use, compared to SRAM: they’re dynamic and need special periodic “refresh” cycles to maintain their contents. They also use a multiplexed row/column address to keep the chip’s pin count low.
The “dynamic” tag is because all data is represented by (gradually leaking) electric charges, and you can’t go half way: once you store anything in the chip, you’ll need to manage its periodic refresh mechanism.
Microcontrollers are too slow for this, but FPGAs are well up to the task. I’ve adapted a very basic implementation to work with a few of my boards. It’s not very smart and doesn’t take advantage of the inherent row/col/bank structure of SDRAMs. As a result, this implementation only reaches a fraction of the performance attainable with even low-end SDRAM chips: some 60..100 ns per access with a 50 MHz clock.
But… that’s still plenty for a lot of use cases.
Implementing an SDRAM controller is not trivial. You need to keep track of the state the chip is in, and also sneak in refresh cycle requests every so often, to force each row inside the chip to keep its data intact.
The code I currently use is on GitHub.
But the controller is really only half the battle – how do we know if it works?
I’ve used the SPI peek device described a few weeks ago on this weblog to allow testing SDRAM interactively. It’s now 64 bits wide to accommodate the full address (24 bits) and data (16 bits in and out) - the SDRAM I’m using has its memory organised as 16M x 16 bits.
It’s all crude and not terribly performant, but hey - it works:
spi-init %0000000001011100 SPI1-CR1 ! \ clk/16, i.e. 4.5 MHz, master \ %0000000001001100 SPI1-CR1 ! \ clk/4, i.e. 18 MHz, master (max supported) : >f32> ( u -- u ) dup 24 rshift >spi> 24 lshift swap dup 16 rshift >spi> 16 lshift swap dup 8 rshift >spi> 8 lshift swap >spi> or or or ; : >fpga> ( u2 -- u2 ) \ exchange 64 bits with attached FPGA swap +spi >f32> swap >f32> -spi ; \ verify that data comes back when loopback is set depth . $01234567 $89ABCDEF >fpga> swap hex. hex. depth . depth . $00FF0000 $12345678 >fpga> swap hex. hex. depth . 31 bit constant SD.REQ 30 bit constant SD.WRn : sd-cycle ( data addr -- u ) \ 2over >fpga> 2drop over SD.REQ or over >fpga> 2drop >fpga> drop ; : >sd ( data addr -- ) sd-cycle drop ; : sd> ( addr -- data ) SD.WRn swap sd-cycle ;
With these Forth definitions loaded into the µC, I can peek and poke into SDRAM as if it were attached directly to the µC:
$1234 $543210 >sd $543210 sd> hex. $0123 $054321 >sd $054321 sd> hex. 100 ms $543210 sd> hex. 100 ms $054321 sd> hex.
The output will be
$0123, respectively: we’ve
stored two different values, and retrieved them again!
This SPI peek device implementation is not super-speedy: the above code needs about 46 µs per read or write, since it has to cross the SPI link to the FPGA (twice, in fact).
So now there’s 32 MB of memory the FPGA can use, in addition to its (faster) internal 30 KB of (static) Block RAM. Onwards!