Propeller SPIN Variable Leakage and Serial Array Transmission Fix

Daniel Price10 min read
Other ManufacturerSerial CommunicationTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Overview

The Parallax Propeller P8X32A is an eight-core microcontroller in which a Spin (or PASM) application spins up multiple cogs that share hub RAM by atomically reading and writing long variables. When one cog writes a trigger flag and another cog polls that flag to drive a FullDuplexSerial transmit routine, four common failure modes appear: variable flag "leakage" or wiggle, integer-math collapse, COG stack corruption, and serial buffer overrun at high baud. This reference isolates each root cause and provides a working pattern that yields stable CSV-style array output up to 230 400 baud on a 5 MHz crystal with the PLL16x mode (80 MHz hub).

Problem Summary

The reported symptoms in a typical encoder+ADC acquisition loop are:

  • Random CSV rows appear on the serial line even when no trigger event has occurred.
  • Increasing the polling threshold on the if statement (for example, if trigger == 2 instead of if trigger == 1) suppresses the spurious rows.
  • Spurious rows reappear when the baud rate is raised above 115 200.
  • Reading an entire array in PASM via par returns only the first element.

None of these symptoms are bugs in the FullDuplexSerial object. They are diagnostic artefacts of three underlying conditions, all of which are described in the Propeller Manual under the section Sharing Variables Between Cogs.

Root Cause 1 - SPIN Integer Math Collapses the Threshold

The constant declaration

CON
  threshold = 95/100   ' divider for difference between sequential reads

evaluates to 0 at compile time because Spin uses 32-bit signed integer arithmetic only - there is no implicit float promotion. Once the constant is zero, the expression

if oldread > DataRead * threshold
  trigger := 1
  arr[4] := oldread

is reduced to if oldread > 0, so the trigger fires on every cog loop iteration regardless of whether the encoder amplitude has actually changed. The fix is to keep the numerator and denominator as separate integer constants and perform the division in the comparison:

CON
  threshold_num = 95
  threshold_den = 100
PUB Go | diff
  repeat
    oldread := DataRead
    DataRead := 0
    outa[chipSel]~~
    outa[chipSel]~
    '... 3202 read sequence ...
    arr[4] := DataRead
    diff := (oldread * threshold_num) / threshold_den
    if DataRead > diff
      trigger := 1
      arr[4] := oldread
Note: Always place the multiplication before the division to preserve precision. DataRead * 95 / 100 keeps an effective resolution of roughly 1.05% of full scale across the 12-bit ADC range (0-4095). Order the operands so the largest value is multiplied first.

Root Cause 2 - Shared Flag Race Conditions Between Cogs

When cog A writes trigger := 1 and cog B reads it, the read is not atomic with the subsequent clearing of the flag if the clearing also happens in cog B. The Propeller's hub access window guarantees that individual long reads and long writes are atomic, but if trigger == 1 ... trigger := 0 is two hub accesses, so a second event between those accesses is lost. The visible symptom is "wiggle" - one CSV row appears for every two or three trigger events.

The robust pattern is a single-writer / single-reader counter rather than a binary flag:

VAR
  long triggerSeq   ' incremented by the source cog
PUB Go
  repeat
    if oldread > (DataRead * threshold_num) / threshold_den
      triggerSeq += 1     ' atomic long write in cog A
PUB runTest
  repeat
    if triggerSeq <> lastSeq
      lastSeq := triggerSeq
      '... transmit one row ...

For multi-source triggering use a Spin lock primitive; see the Locks and the LOCKxxx and LOCKRET Instructions section of the Propeller Manual v1.2 for the canonical pattern.

Root Cause 3 - cognew Passed an Address Instead of a Stack

The call

cognew(runTest, 2000)

does not mean "start runTest with parameter 2000." The second argument is the address of a long-aligned stack region in hub RAM that the new cog will use for its call/return linkage. Address 2000 lies in the middle of the program or variable area, so the very first comms.tx call inside runTest writes a return address on top of one of your variables - classically the trigger byte, which is why the flag "wiggles." Reserve a dedicated stack in a VAR block:

VAR
  long stack2[10]
  long stack3[10]
  long cardStack[8]
PUB start
  cognew(Go,      @stack2)
  cognew(runTest, @stack3)
  cognew(@entry,  @arr[4])
  cognew(@detecter, @detect)

Eight longs of stack are sufficient for Spin methods that contain only comms.* calls; reserve ten or more for nested method calls. The Propeller Cogs tutorial demonstrates the same allocation rule.

Root Cause 4 - High-Baud Overrun in FullDuplexSerial

FullDuplexSerial uses a single hub byte for the transmit buffer. At 230 400 baud on an 80 MHz Propeller, a cog can spend up to 35 system clocks per bit, but the 3202 read sequence plus the four comms.dec calls in runTest easily exceed the inter-character budget when the trigger fires faster than ~1.2 kHz. The fix is to keep runTest non-blocking and to drain the transmit FIFO at the host, or to lower the effective rate by transmitting only on edges:

con
  txBaud   = 115_200   ' conservative ceiling on xtal1+pll16x
PUB runTest | s
  comms.start(rxPin, txPin, 0, txBaud)   ' mode 0 = standard, no invert
  outa[19] := true
  waitcnt(30_000_000 + cnt)
  repeat
    if triggerSeq <> lastSeq
      lastSeq := triggerSeq
      comms.tx("$")
      comms.dec(arr[0])
      comms.tx(",")
      comms.dec(arr[1])
      comms.tx(",")
      comms.hex(cnt, 8)
      comms.tx(",")
      comms.dec(arr[2])
      comms.tx(",")
      comms.dec(arr[3])
      comms.tx(",")
      comms.dec(arr[4])
      comms.tx(13)
Baud Bits per ms Max lines/sec @ 40 bytes Notes
9 600 9.6 30 Default for terminal capture
57 600 57.6 180 Safe with FullDuplexSerial on 80 MHz
115 200 115.2 360 Stable with 12-bit ADC and 4 dec fields
230 400 230.4 720 Requires short payload, FDS buffer > 1

Step-by-Step Fix Procedure

  1. Replace the threshold = 95/100 constant with a numerator/denominator pair as shown in Root Cause 1.
  2. Convert the trigger byte into an atomic triggerSeq long written by exactly one cog and read by the transmit cog.
  3. Declare a dedicated long stackN[...] for every cognew(... , @stackN) call; do not pass a numeric literal as the second argument.
  4. Limit comms.start to a baud rate that gives at least 8 hub clock periods per bit. On 80 MHz hub (xtal1 = 5 MHz, _clkmode = xtal1 + pll16x) the practical ceiling with the published FullDuplexSerial object is 230 400 baud for short payloads and 115 200 baud for 40-byte CSV rows.
  5. End every row with comms.tx(13) (carriage return) and consider prefixing with a sentinel byte (for example $) so the host can re-sync after a buffer drop.
  6. Pre-size the transmit payload to a fixed width; that way the host parser can recover the column boundaries even if one byte is corrupted.

Sending an Array Element-by-Element Over Serial

Spin cannot pass an entire array to a cog with a single cognew. Each element must be transferred with a separate rdlong/wrlong or with a hub pointer and an index. For PASM cogs that need all elements of a five-long array, the canonical pattern is to pass the hub address of the first element and let the cog index by offset:

DAT
              org     0
detecter      rdlong  n1, par                ' par = hub address of detect[0]
              add     n1, #1
              wrlong  n1, par
              add     par, #4                ' advance to detect[1]
              rdlong  n2, par
              jmp     #detecter
dectectPin    long    1 << 7
n1            long    1
n2            long    0

On the Spin side, launch the cog with the address of the first element:

cognew(@detecter, @detect[0])   ' pass hub address of array slot 0

If only one element is needed, a plain cognew(@detecter, @detect) works because @detect is already the hub address of detect[0]. The Variable and Cog Interaction chapter of the Propeller Manual documents the addressing rules in detail.

Specifying the Sync Word for Host Parsers

When a CSV row can be corrupted, a 3-byte fixed synchronization pattern is the standard recovery marker. Choose bytes that cannot appear as ASCII in the decimal or hex fields you transmit - for example 0xAA, 0x55, 0xAA:

comms.tx($AA)
comms.tx($55)
comms.tx($AA)
' then the row

The host discards bytes until the three-byte marker is seen, then re-aligns the column parser. This pattern is the same one recommended for binary protocols over RS-232 / RS-485 links where line noise is expected.

Verification

  1. Run the program and observe the terminal at 115 200 baud. Trigger events from the 3202 trailing-edge detector should produce exactly one CSV row per event.
  2. Hold the encoder stationary for 10 seconds. No row should appear - if any appears, Root Cause 1 or 2 is still present.
  3. Set the trigger threshold to 95/100 in code and confirm that the equivalent (DataRead * 95) / 100 arithmetic produces the same result on a 12-bit full-scale sweep (0-4095).
  4. Monitor the triggerSeq value with a debug print at 9 600 baud and verify that it increments monotonically with no skipped values up to 10 000 events.
  5. Disconnect the serial cable mid-stream and reconnect. The host should re-sync on the 3-byte marker within 200 ms.

Parameter and Pin Reference

Symbol Pin / Value Purpose
chipDin P1 3202 serial data in (MSB first, single-ended, channel 0)
chipClk P3 3202 serial clock, idle low, latches on rising edge
chipSel P0 3202 chip select, active low
chipDout P2 3202 serial data out
rxPin P5 FullDuplexSerial RX (USB-Serial adapter TX)
txPin P6 FullDuplexSerial TX (USB-Serial adapter RX)
BitsRead 12 3202 output word length, MSB first
_clkmode xtal1 + pll16x 5 MHz crystal x 16 = 80 MHz hub
_xinfreq 5_000_000 Crystal frequency in Hz
threshold_num 95 Numerator for 5% hysteresis
threshold_den 100 Denominator for 5% hysteresis

Troubleshooting Matrix

Symptom Likely Cause Fix
Trigger fires when nothing changes Integer collapse: threshold = 95/100 = 0 Use (DataRead * 95) / 100
CSV rows appear in pairs Cog stack passed a numeric literal Allocate long stackN[...] per cog
Trigger flag toggles without source event Hub RAM overwritten by stack frame Reserve a dedicated VAR long stackN[...]
Data dropped above 115 200 baud FullDuplexSerial single-byte TX buffer overrun Lower baud or shorten payload
PASM cog sees only detect[0] Array address not indexed in cog Advance par with add par, #4
Lost row after cable reconnect Host cannot re-sync Send 3-byte marker $AA $55 $AA

Field-Commissioning Checklist

  • Confirm the 3202 sees a clean 5 V supply and a decoupling capacitor of at least 100 nF within 25 mm of VDD - the chip is sensitive to digital noise on VDD which presents as the "wiggle" symptom.
  • Verify that the USB-Serial adapter is set to 3.3 V logic, not 5 V, when interfacing to the Propeller I/O directly. The P8X32A is not 5 V tolerant.
  • Add a 100 ohm series resistor on the TX line if the cable is longer than 0.5 m to reduce ringing at 230 400 baud.
  • Set the host terminal to raw mode so that any local line-ending translation does not double the apparent row count.

Why does threshold = 95/100 evaluate to zero in Spin?

Spin uses 32-bit signed integer arithmetic only - there is no implicit float promotion - so the constant expression 95/100 is calculated at compile time and rounded to zero. Always split the numerator and denominator into separate constants and perform the division in the comparison: if DataRead > (oldread * 95) / 100.

What is the correct way to allocate a stack for a new cog?

Declare a dedicated long stackN[M] array in the VAR block, then call cognew(method, @stackN). Eight longs are sufficient for one level of method nesting; ten or more for nested calls. Never pass a numeric literal as the second argument of cognew.

How do I pass an entire array to a PASM cog?

You cannot pass an array as a single value. Pass the hub address of element zero (@arrayName) and use add par, #4 inside the cog to walk the array. Each rdlong/wrlong accesses one long, and hub access is guaranteed atomic for that long.

What is the maximum reliable baud rate for FullDuplexSerial on an 80 MHz Propeller?

With the standard FullDuplexSerial object the practical ceiling is 230 400 baud for short payloads of a few bytes, and 115 200 baud for 40-byte CSV rows, because the object has a one-byte transmit buffer and a transmit call can stall the cog while it spins on the buffer.

Why do spurious rows appear on the serial line when the encoder is stationary?

Either the threshold expression collapsed to zero (integer math), or the cog that drives FullDuplexSerial is using an address that overlaps the trigger variable because a numeric literal was used in place of a proper VAR long stackN[] allocation. The fix is to keep the threshold arithmetic in integer form and to give every cog its own stack array.

Back to blog