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 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!
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! ).
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.
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
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
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
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.
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
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.
waitcntwill wait until the system counter matches the value we've specified in
- Second, the value of
Delaywill be added to
Timeto create the next value to wait for.
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!
: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
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.
And now we're finally jumping back to the label we set earlier, completing the loop and resulting in the
on our output register being run about once a second.
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 = 0000ie: turns pin On
0000 xor 0001 = 0001ie: 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".
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.
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:
Search above to find more great tutorials and guides.