brain of mat kelcey...
running paula on tiliqua
June 13, 2026 at 10:10 PM | categories: eurorack, tiliqua, fpgaintro
protracker on the amiga was my introduction to making electronic music and its paula chip was the first time i really worked with assembly.
was super excited then to see a c64 sid wrapper done as an example for the tiliqua eurorack fpga module
if tiliqua can run sid, surely it'll be easy enough to run paula too? right? right?!
TL;DR
just want to hear results? ( turn on subtitles for commentary! )
code structure
minimig aga mister is a port of minimig ( a cycle accurate implementation of the amiga ) to the aga_mister board.
at first i was wondering if running this entire thing would be easiest to get paula going but at the same time i wanted to isolate a minimal piece to have more control. i like this code since it has a really clear separation of the paula audio pieces
side note; it wasn't until i had made a decent start on things that i realised it wasn't just paula i needed to deal with but the agnus chip too. though paula is the synth voice it's agnus that manages everything around memory access ( and not just for audio, it handles video and more ). my initial thought was to bring all of it across, but then decided just to implement a minimal fake_agnus to hook up instead.
paula registers
the crux of this project is to
- init a wrapper of the
paula_audioverilog - hook up a sampler to write data that
fake_agnuscan read from - read a bunch of midi values to set various registers on paula ( in the mother of all state machines )
- pipe paula audio out via tiliqua
simple. ish.
HUGE SHOUTOUT to the internet archive for hosting the amiga hardware reference manual. i couldn't have done this project without it as a reference and i hugely regret putting my copy in the recycling 20+ years ago when my amiga500 died :( any pNNN references below refer to this
paula has 4 channels; each channel is controlled by a set of registers; of interest for us are ( for x in [0, 1, 2, 3] )
AUDxLEN - sample length. we set an init value of a max sample size ( see sampling below ) and
we can vary this down to 1 ( not quite granular in my implementation, i think i have a bug )
AUDxPER - sample period; the number of ticks between each sample fed to the DAC.
like the inverse of the pitch. see p140. a lot of the interesting character of the amiga sound comes from this one IMHO.
AUDxVOL - simple 6 bit volume. of note is that i had to add some gain on output based on my sampling.
ADKCON ( & "creative difference" )
there's also a special register that controls cross modulation between channels. ( one of the main reason i wanted to even do this project! )
there are bits for ATPER0, 1, 2 & ATVOL0, 1, 2 that dictate whether chN will modulate chN+1
for either PER ( FM mod ) or VOL ( AM mod )
and i have to say that it was super satisfying after all the messing around with fake_agnus dma crap
to implement this piece and have it "just work" #angelsSinging
however... as i was first messing around toggling this bits i thought i had a bug. modulation was all over the place & after reading the reference manual ( p149-150 ) i realised it was working perfectly. it's just that the behaviour is not what i expected.
so how does the modulation work? when you set ch_x to modulate the period, or volume, of ch_y
the wiring is just, quite literally, AUDxDAT -> AUDyPER or AUDxDAT -> AUDyVOL.
what interesting is ...
AUDxVOLis just the values 0 to 64 ( the lower 6 bits )AUDxPERis an unsigned valueAUDxDATis signed audio
so if you were hand crafting in assembly a channel to be a modulator for, say, VOL, you'd never set bits 7-15. but if you use audio samples as modulators all manner of weird hell breaks loose! :D
this is fun, in it's own right, but i wanted something a bit different. so i changed this behaviour to make things more what i'd expect
- FM modulation ( PER ) is more classic audio FM; modulator=0 => original freq, +ve => faster, -ve => slower.
- AM modulation ( VOL ) is ring modulation.
these changes had to be done in a vendored version of the paula_XXX.v source...
sacrilege! i know! i expect to be struck down by the paula gods some time soon...
the change is pretty small to be honest, and if i ever want more realistic playback ( e.g. to playback IFF samples ) i can just put in a toggle.
sampling
i certainly didn't want to write my own wavetable in pseudo assembly so instead i added a simple sampler to
fake_agnus.
you can start/stop a recording and then toggle playback per channel. toggling playback makes the whole thing wavetable focussed but would be trivial to switch to one shots instead.
midi
all of this is controlled via midi! setup for my beat step pro but ccs and notes are configurable via a json config ( at compile time )
i use encoders to set range based registers ( AUDxVOL etc )
and pads to toggle various register bits in ADKCON as well as to start/stop recording/playback.

- the blue section represents the range control for
AUDxLEN,AUDxPERandAUDxVOL. one set per channel - the red section represents the pads to start/stop recording or playback for each channel
- the green section toggles the 6 interesting bits of
ADKCON; 3 for the PER modulation ( FM ) , 3 for the VOL modulation ( AM )
additionally there are two reset pads ( R ). one to set ADKCON bits to 0 and the other to set all AUDxxxx registers
to their defaults.
TODOS
known bugs
- channels can get locked up if you mash the record/play pads :(
- pretty sure i have a shifting bug re:
AUDxLEN, the shortest length is still pretty long.
cleanup
midi -> register -> signals
the code started to get really squirrely with a mix of general lists of N channels vs assuming 4 will generalise further if i ever want to run 2 paulas over 8 channels
i did notice when i started this project that the sid code uses a soft cpu for the register setting. perhaps the hellish state machine i've got would be more cleanly represented in rust?
( i might actually do this just for the purpose of learning more around with vexriscv. )
fake agnii
the first version i wrote ( that i subsequently completely rewrote ) used a single fake_agnus.
for the second version i ended up splitting things to have 4 fake angus chips instead ( one for each channel ).
the way i rewrote it just made dev and debugging easier. now that things work i could refactor this back to one fake chip, but am not sure i can be bothered.
there is also a stack of weird and wonderful derived constants in this class trying to accurately represent the various strobe timing and dma handling. 80+% of my time was spent here, i'll sure there is a lot of simplification i could do here...
extensions
clearly there's a truck load of things i could add
wavetable vs one shot everything so far is about building wavetables from inputs. i build short loops ( with clickless zero crossing checks ) but it could instead just do one shot playback by 1) increasing buffer size and 2) instead of toggle just do one shot. trivial change.
v/oct vs AUDxPER currently you tune frequency directly via the AUDxPER register. from an encoder!
very playable :/ but it could easily support v/oct. there's a whole set of tables around v/oct -> AUDxPER
on p158 onwards i could wire in.
granular aside from the ( potential ) AUDxLEN bug mentioned i think i could "invented" a new
register AUDxSTART to do more interesting granular stuff. would slot into fake_agnus easily.
panning standard output from paula is ch0 & 3 -> left & ch 1 & 2 -> right. could expose in paula.v the raw 4 so the routing could just be tiliqua.inN -> paula.chN -> tiliqua.outN
xN paulas why init one paula when you can init two? basically you could have one per eurorack-pmod.
cv mod rather than control AUDx registers with midi they could handled by audio rate cv in.
would just have to wire things differently.
modulation ring real paula only allowed chN to modulate chN+1 ( for N in 0,1,2 ) but why not have ch3 modulate ch0 ? works for the lyra8 well enough!
coding agents as over enthusiastic interns
how good are agents?! they definitely helped me in a number of places. i probably could have vibe coded this from scratch but that would have robbed me of the pleasure of finding things out
i used an agent to refactor code to do all sorts of things; separate code into modules, generalise cut and paste code developed for handling ch0 and ch1 into loops to support N=4 channels, etc etc
but the biggest thing was around the DMA between paula and fake_agnus. my original code used the "direct audio output" approach ( p157 ) and i had that working pretty quickly. but when i switched to the DMA approach i definitely hit a wall and had a bunch of small lockup timing bugs. without an agent helping me write loads of debugging code ( e.g. highjacking audio outputs to see various pulse and strobe timing ) i probably would have given up in frustration :/





