My First PASM

If you're anything like me, you'll see PASM ( Propeller Assembly ) as a looming challenge, begging to be tackled and surmounted whether or not it proves to be at all useful in your Pi projects.

Programmers can certainly succumb to "challenge accepted" moments, and PASM is one of those. Don't worry, though, I've taken pains to break down every line of the Assembly in this tutorial and try to explain it in simple terms.

You'll notice terms here like "instruction" and "mnemonic", instructions are basically just numbers which tell the Propeller chip exactly what it should be doing at any one moment. Because they're long, 32-bit, numbers nobody could be expected to remember what number correlates to what action against what register or RAM address... so we use mnemonics.

A mnemonic is simply a much more memorable name for a particular assembly instruction, register or modifier. It converts what would be a completely incomprehensible number into something slightly less daunting without stripping you of the power to tell the microprocessor exactly what it should be doing.

Like all languages, the canonical way to get started is with a simple and easy blink example. Let's have a look at one in PASM. What we'll actually see is a small SPIN program loading up a new Cog with the PASM we want to run. All the code you run on a Propeller will start with a Cog running SPIN.

CON
  _CLKMODE = xtal1 + pll16x
  _XINFREQ = 6_000_000

  MY_LED_PIN = 0

PUB main
  cognew(@blink, 0) ' Start a Cog with our assembly routine, no stack
                    ' is required since PASM requires we explicitly
                    ' assign and use memory locations within the Cog/Hub
DAT
        org     0
blink   mov     dira,   Pin       ' Set our Pin to an output
        rdlong  Delay,  #0        ' Prime the Delay variable with memory location 0
                                  ' this is where the Propeller stores the CLKFREQ variable
                                  ' which is the number of clock ticks per second
        mov     Time,   cnt       ' Prime our timer with the current value of the system counter
        add     Time,   #9        ' Add a minimum delay ( more on this below )
:loop   waitcnt Time,   Delay     ' Start waiting
        xor     outa,   Pin       ' Toggle our output pin with "xor"
        jmp     #:loop            ' Jump back to the beginning of our loop

Pin     long    |< MY_LED_PIN     ' Encde MY_LED_PIN to a bit mask
Delay           res     1
Time            res     1
        fit

Whew! That's a lot to take in. While at first glance the PASM code may look like incomprehensible nonsense, it's actually a lot easier to understand than it looks. Assembly doesn't have many of the constructs we take for granted in most high level programming languages, and that means it doesn't have those complexities either.

Most of these Assembly commands are simply moving a number from one place to another, or performing an operation upon two memory locations. Once you get a simple Blink program running, it becomes easier to experiment with how other instructions affect, for example, your Pin value.

Onward to the explanation...

A slightly different 'cognew'

You'll notice, if you've read through the Multicore tutorial, that our cognew command is ever so slightly different this time around.

This is because PASM is wholly different to SPIN in how it operates, and is much more explicit about how memory is used. SPIN needs a stack to store various snippets of information as it goes about its business, and what it stores is completely hidden from the programmer, it's a high-level language.

PASM, on the other hand, only stores things where you tell it, when you tell it and how you tell it.

The second parameter in this instance becomes the read-only parameter passed into the new Cog. You can specify a single number, a complicated 32-bits long bitfield, or just give it the memory address for a handful of parameters stored in the Hub. This is all far too advanced for our first blink, though, so we'll stick with 0.

The Assembly

The PASM code in this example is quite the monster, even for a simple Blink. We'll tackle it line by line and I'll try to explain everything to the nth degree. Grab a drink and a snack, and sit tight!

DAT

All PASM is stored within the DAT section of its parent SPIN program. This is the easiest place to store PASM source which everyone can see and modify, but it's not the only way. You could use an array of longs to store compiled PASM instructions, or you could create PASM instructions on the fly ( please don't! for your own sanity! ).

org 0

This is some PASM copypasta that you'll find at the top of most, if not all, blocks of PASM code in some form or another. It simply states that the following PASM code should start at Cog RAM addr 0.

blink   mov     dira,   Pin

There's a lot going on in this line, but it's easy enough to break down.

The blink part is a label, this is a little note to the compiler telling it that we want to keep track of this point in the program so we can find it later. This label can be passed into cognew to tell it where to locate our PASM. It can also be jumped to in PASM itself, you'll see this later.

Next comes the actual instruction: mov dira, Pin. The mov part is a simple mnemonic, a text name for a numerical instruction that the Propeller can understand. If you hadn't guessed, it means "move" although it actually copies.

The rest of the instruction is made up of our destination, dira which is the address ( not the value ) of the DIRA register, the very same one we use in SPIN. And, finally, the address of our source Pin which is a 32bit integer containing the pin mask we want to toggle.

So this instruction is telling the Propeller to mov the value of memory location Pin into register dira. Well, copy, since the Pin value remains unchanged.

Bear in mind that we're blatting the whole register by moving Pin into it, so our Pin will be the only output.

A far more appropriate instruction would be or dira, Pin, which would turn the Pin to an output if its not one, and would otherwise leave it unchanged.

rdlong  Delay,  #0

Next up is a Hub access instruction. Propeller Assembly includes special instructions for accessing Hub memory. This particular one, rdlong, reads a long ( a 32bit integer ) into its target from a source memory location.

In this instance Delay is our target, a long we've reserved for our use, and the source is #0 which is a literal memory address. So, we're loading Delay from Hub memory location 0. This just so happens to be where the Propeller keeps the CLKFREQ value, which is the number of clock ticks in a second. Using this, we can create a 1sec delay later.

mov     Time,   cnt

Just like our earlier mov instruction, we're copying the value of one memory address or register into another.

In this case we're priming Time with the value of the system counter, for which there's a handy shortcut cnt so you don't have to remember its numeric memory location.

add     Time,   #9

Now for an add instruction. This one should be easy to understand. We're adding the literal value 9 ( we indicate that it's not an address by prefixing with # ) to our Time variable.

But, why on earth are we doing this? The reason is perplexing, but simple once you understand it.

The waitcnt instruction in PASM will wait until the cnt register equals the value we give it. We're passing it the value of Time for its first wait, if this value simply equalled the system counter then the very act of calling waitcnt, which itself takes time, will cause us to miss the value we're looking for.

Imagine I gave you a piece of paper with the number 30 on it, and told you at exactly 30 seconds past to wait until the clock next shows 30 seconds. By the time I've finished telling you what to do it's going to be at least 32 seconds past- so you'll wait nearly a whole minute instead of the 30 seconds I wanted.

When waitcnt misses a particular value of cnt, this is more or less what's happening!

A more succinct way to write this line would be:

add     Time,   Delay

Which causes our first waitcnt delay to be about 1 second, instead of the minimum possible delay. However I deliberately left this minimum wait to illustrate a common tripping point in PASM.

:loop   waitcnt Time,   Delay

Now we start waiting. This line does two things.

  • First waitcnt will wait until the system counter matches the value we've specified in Time.
  • Second, the value of Delay will be added to Time to create the next value to wait for.

Because Delay is added to Time for us, there's no need to do it manually within the loop, and the next time we call waitcnt in the same way, we'll handily get another 1 second delay.

This is, of course, unless all the things we've decided to do within the loop take more than the delay time!

The :loop prefix on this line is another label. Using ':' to prefix a label is a handy way of making it obvious that it's a label, and not a variable, register address or instruction mnemonic. We've labelled this line because, in order to loop, we want to jump back to it later.

The word "loop" is not required, you can make this label ":monkeys" if you want!

xor     outa,   Pin

This line is where the magic happens. We're finally setting a value to our output register outa. Using xor is a really lazy way of toggling our output pin, since it literally means "exclusive or".

So, if our pin is on, we get:

1 xor 1 = 0

And if our pin is off, we get:

0 xor 1 = 1

And so on!

The thing to remember here is that xor is operating upon the whole 32bit register, so xor is a really handy way to toggle a pin using a bit mask, without affecting the others.

jmp     #:loop

And now we're finally jumping back to the label we set earlier, completing the loop and resulting in the xor on our output register being run about once a second.

You can jmp to any memory address you like, but using a label makes it much easier to keep track of where the jump is going.

Pin     long    |< MY_LED_PIN

This line creates a new, long type variable called Pin which we're assiging with a bitmask decoded from our MY_LED_PIN constant. The decode operator "|<" will turn 1 into 0001, 2 into 0010, 3 into 0100 and so on.

This is the bitmask we use when calling xor against the output register. Remember:

  • 0001 xor 0001 = 0000 ie: turns pin On
  • 0000 xor 0001 = 0001 ie: turns pin Off
Delay           res     1
Time            res     1

These two lines reserve longs of memory using the res directive. We can reserve 1 or more longs, but since Time and Delay both fit into a single 32-bit integer we'll only use 1.

res is a directive, not an instruction, this means it's evaluated by the compiler and never run as PASM. Upon evaluation it just leaves gaps in the Cog memory and gives them the friendly names, symbols, which we can refer to them by in our code.

The symbols ( or labels ) Delay and Time don't exist as far as PASM is confirmed, they're just friendly names for us to re-use that represent memory addresses.

In plain English, these commands mean "leave the next memory address empty and call it Delay so we can use it elsewhere".

fit

This final directive isn't essential, specially when we're dealing with such small amounts of PASM code, but it's useful to remember. fit is another instruction to the compiler, it simply asks "does all the PASM we've written actually fit into Cog RAM?"

fit will generate a compiler error if we've written too much code to fit into a single Cog. At this point we'd need to either cut down the code by optimising it, or split its jobs over muliple Cogs.

Whew!

Well, that was an explanation marathon. Go and cool off your brain for a minute, and then get ready to compile and run your code. For this example you can use the same layout we used in the SPIN blink example:

Propeller Blink Layout

That's all folks!

Search above to find more great tutorials and guides.

Plasma 2040

Swathe everything in rainbows with this all-in-one, USB-C powered controller for WS2812/Neopixel and APA102/Dotstar addressable LED strip.