@Rayman said:
Said flash loader could also attempt to load from uSD, if desired, right?
Could be useful if uSD is on different pins and/or has a power switch.
Only if you can figure out how to hot-patch the SD boot code to do that - a full SD driver is probably too heavy for the 1024 bytes of bootloader space - just use the stock flash bootloader and then make the SD loader be the payload of that.
@Rayman said:
4-pin uSD booting should be faster. Wonder if that'd make a difference...
You can make the SD boot A LOT faster as-is by making _BOOT_P2.BIX a tiny stub that sets the clock, patches some stuff in the ROM code and then pivots into a different boot file. I had one like that:
CON _CLKFREQ = 300_000_000
DAT
org 0
asmclk
'' Patch pullup check
wrlong #0,##$fc5b4 ' not sure why it fails but ok
'' Move filename into place
mov $1DC,name+0
mov $1DD,name+1
mov $1DE,name+2
drvh #38 ' Set LED
call #$fc578
drvl #38 ' Clear LED if fail
jmp #$
name byte "LOADTEST","BIX",0
(as seen, this tries to load LOADTEST.BIX - also note the Pin 38 LED, change or remove that if necessary)
Booting a large ~400K executable is a lot faster like that.
You can also compress the executable you're booting. Flexspin and SpinTools have this option built-in, I also made a standalone version
Chip,
I just bumped into something that has changed in recent releases of Pnut. I don't know if this is an explicit change you intended or not. The following compiled and ran when I was compiling with Pnut v46:
I see the problem. It's in my parsing of what comes after NEXT or QUIT. I need to back up when there is a line end, indicating there will be no constant.
NEXT and QUIT do not require integers after them, but an integer is needed if you want to NEXT/QUIT from outside your current REPEAT block. That integer would need to be a value of 2 or more.
I added a new file into the .zip called PointerFlexibility.spin2. It gives coding examples of how you can use byte/word/long pointers. The same basic rules apply to structure pointers, as well.
Here is that file, in case anyone wants to get a quick tour of pointers:
{Spin2_v52}
pub go() | ^byte p, byte buff[20] 'declare a byte pointer and a 20-byte buffer
[p] := @buff 'point the pointer to the buffer
p[++].byte := $50 'write a byte into the buffer, step pointer
p[++].long := $77777777 'write a long, step pointer
p[++].word := endianw($BEEF) 'write a big-endian word, step pointer
p[++].byte := $01 'write a byte, step pointer (.byte is for clarity, not needed)
p[++].byte := $02 'write a byte, step pointer
p[++].byte := $03 'write a byte, step pointer
p[++].long := $FFFFFFFF 'write a long, step pointer
debug(uhex_byte_array_(@buff,20)) 'show the buffer
[--]p.word := $5555 'back up and write a word
[++]p.word := $AAAA 'move ahead and write a word
debug(uhex_byte_array_(@buff,20)) 'show the buffer
p := $22 'write a byte
p[-1] := $33 'write a byte at [-1]
debug(uhex_byte_array_(@buff,20)) 'show the buffer
p++ 'inc byte
p[1]-- 'dec byte at [1]
debug(uhex_byte_array_(@buff,20)) 'show the buffer
p~~ 'set byte
[--]p.long[-1].[31..28]++ 'back up long pointer, increment top nibble of long at [-1]
debug(uhex_byte_array_(@buff,20)) 'show the buffer
'
' Pointers are longs and they can be read/modified/written by putting brackets around them:
'
' [p] 'the actual pointer value (not the data being pointed to)
' @[p] 'the address of the actual pointer value
'
'
' Once assigned, pointers can be used to read/modify/write the data they point to in
' these root syntax forms:
'
' p 'r/m/w the data being pointed to
' [++]p 'pre-inc pointer by size, then r/m/w the data being pointed to
' [--]p 'pre-dec pointer by size, then r/m/w the data being pointed to
' p[++] 'r/m/w the data being pointed to, then post-inc pointer by size
' p[--] 'r/m/w the data being pointed to, then post-dec pointer by size
'
'
' Any of these root syntax forms can be appended with an optional size override,
' an optional index, and an optional bitfield, in that order.
'
' An optional size override starts with a period followed by BYTE/WORD/LONG:
'
' .BYTE 'sets the data size to byte, regardless of the pointer data size
' .WORD 'sets the data size to word, regardless of the pointer data size
' .LONG 'sets the data size to long, regardless of the pointer data size
'
' ...followed by an optional index in brackets:
'
' [index] 'index gets scaled for byte/word/long addressing
'
' ...followed by an optional bitfield, which is a period followed by a bitfield in brackets:
'
' .[bitfield] 'selects a portion of the byte/word/long to read/modify/write
'
NEXT and QUIT do not require integers after them, but an integer is needed if you want to NEXT/QUIT from outside your current REPEAT block. That integer would need to be a value of 2 or more.
.
NEXT and QUIT can each be followed by an integer value 1..16 to alter the nesting level at which they are to occur. If no integer value is expressed, the default value of 1 is used, which means the current nesting level, The value 2 would mean the outer nesting level, while three would mean the next outer nesting level, and so on.
What is the intended behavior when the nesting level integer exceeded the number of levels?
For example, would QUIT maxLevel+1 be the same as QUIT maxLevel ? (Or should that throw a compiler error- or warning at least?...)
@cgracey said:
Rather than using integers 1..16 to specify level, might 0..15 be better? It would go like this:
0 = This REPEAT block, no change in level (the default for NEXT/QUIT)
1 = The first-outer REPEAT block
2 = The second-outer REPEAT block
Would that be better? If I change it, I should do it soon, before any serious code gets written by anyone.
Yes, I think it would be better since it represents the number of levels to go back, also may I suggest to use (or allow) negative numbers ? I think that quit -n better represents quit back n level(s).
Even better would be to have some kind of label to clearly indicate what repeat level is referred, not sure what a good syntax may be, something like repeat : label maybe.
Also, can you check if the following snippet generates the correct bytecode ?
{Spin2_v52}
PUB main() | a, b, c
repeat a from b to c
quit
next
repeat a from b to c
quit 2
next 2
a++
I had just a quick look and probably I miss something, but seems to me that the inner next 2 statement doesn't pop the inner loop stack, quit 2 correctly pops both the inner and outer stacks. Today I'm a bit busy so I may not be able to look at this with more details.
@macca said:
Even better would be to have some kind of label to clearly indicate what repeat level is referred, not sure what a good syntax may be, something like repeat : label maybe.
Yes, named breaks are good because you can't get the number wrong and can't misread it. Languages like Rust do it this way.
I looked into the next level issue and I think there is an issue.
With this example:
{Spin2_v52}
CON
_CLKFREQ = 160_000_000
PUB main() | a, b, c, d, e, f
b := 1
c := 5
debug("start")
repeat a from b to c
e := 20
f := 25
repeat d from e to f
if d == 23
next 2
debug(" ", udec(a, d))
debug("end ", udec(a, d))
repeat
PNut outputs this:
My implementation outputs this:
Cog0 INIT $0000_0000 $0000_0000 load
Cog0 INIT $0000_0F64 $0000_1888 jump
Cog0 start
Cog0 a = 1, d = 20
Cog0 a = 1, d = 21
Cog0 a = 1, d = 22
Cog0 a = 2, d = 20
Cog0 a = 2, d = 21
Cog0 a = 2, d = 22
Cog0 a = 3, d = 20
Cog0 a = 3, d = 21
Cog0 a = 3, d = 22
Cog0 a = 4, d = 20
Cog0 a = 4, d = 21
Cog0 a = 4, d = 22
Cog0 a = 5, d = 20
Cog0 a = 5, d = 21
Cog0 a = 5, d = 22
Cog0 end a = 6, d = 23
If I'm not missing something, the latter is correct, it should be equivalent to use quit in the inner loop and indeed PNut's output is correct when using quit.
Looking at the listing, PNut doesn't pop the inner loop stack with the next statement (the comments are the bytecode from PNut):
' Object "NextQuit.spin2" header (var size 4)
01844 00000 08 00 00 80 Method main @ $00008 (0 parameters, 0 returns)
01848 00004 30 00 00 00 End
' PUB main() | a, b, c, d, e, f
0184C 00008 06 (stack size)
' b := 1
0184D 00009 A2 CONSTANT (1) ' A2
0184E 0000A F1 VAR_WRITE LONG DBASE+$00001 (short) ' F1
' c := 5
0184F 0000B A6 CONSTANT (5) ' A6
01850 0000C F2 VAR_WRITE LONG DBASE+$00002 (short) ' F2
' debug("start")
' repeat a from b to c
01851 0000D 42 13 ADDRESS ($00013) ' 42 13
01853 0000F E2 VAR_READ LONG DBASE+$00002 (short) ' E2
01854 00010 E1 VAR_READ LONG DBASE+$00001 (short) ' E1
01855 00011 D0 VAR_SETUP LONG DBASE+$00000 (short) ' D0
01856 00012 7B REPEAT ' 7B
' e := 20
01857 00013 42 14 CONSTANT (20) ' 42 14
01859 00015 F4 VAR_WRITE LONG DBASE+$00004 (short) ' F4
' f := 25
0185A 00016 42 19 CONSTANT (25) ' 42 19
0185C 00018 F5 VAR_WRITE LONG DBASE+$00005 (short) ' F5
' repeat d from e to f
0185D 00019 42 1F ADDRESS ($0001F) ' 42 1F
0185F 0001B E5 VAR_READ LONG DBASE+$00005 (short) ' E5
01860 0001C E4 VAR_READ LONG DBASE+$00004 (short) ' E4
01861 0001D D3 VAR_SETUP LONG DBASE+$00003 (short) ' D3
01862 0001E 7B REPEAT ' 7B
' if d == 23
01863 0001F E3 VAR_READ LONG DBASE+$00003 (short) ' E3
01864 00020 42 17 CONSTANT (23) ' 42 17
01866 00022 70 EQUAL ' 70
01867 00023 13 05 JZ $00029 (5) ' 13 03
' next 2
01869 00025 18 0C POP 16 ' (missing)
0186B 00027 12 03 JMP $0002B (3) ' 12 03
' debug(" ", udec(a, d))
0186D 00029 D3 VAR_SETUP LONG DBASE+$00003 (short) ' D3
0186E 0002A 7D REPEAT_LOOP ' 7D
0186F 0002B D0 VAR_SETUP LONG DBASE+$00000 (short) ' D0
01870 0002C 7D REPEAT_LOOP ' 7D
' debug("end ", udec(a, d))
' repeat
01871 0002D 12 7F JMP $0002D (-1) ' 12 7F
01873 0002F 04 RETURN ' 04
Macca, I am glad you found this! I am not popping the stack correctly. I will get this fixed tomorrow, hopefully in the morning. Thank you for doing all these tests.
Comments
Said flash loader could also attempt to load from uSD, if desired, right?
Could be useful if uSD is on different pins and/or has a power switch.
Also, could maybe do something fancy like look for a backup boot file if the first one is missing...
Also, guess this could allow for long file names and directories...
4-pin uSD booting should be faster. Wonder if that'd make a difference...
Only if you can figure out how to hot-patch the SD boot code to do that - a full SD driver is probably too heavy for the 1024 bytes of bootloader space - just use the stock flash bootloader and then make the SD loader be the payload of that.
You can make the SD boot A LOT faster as-is by making _BOOT_P2.BIX a tiny stub that sets the clock, patches some stuff in the ROM code and then pivots into a different boot file. I had one like that:
(as seen, this tries to load LOADTEST.BIX - also note the Pin 38 LED, change or remove that if necessary)
Booting a large ~400K executable is a lot faster like that.
You can also compress the executable you're booting. Flexspin and SpinTools have this option built-in, I also made a standalone version
I can confirm this is a "me" problem. I am unsure why it does not boot properly, but I will figure it out. A simple program booted correctly from SD.
Well, it's working as it should now. Maybe the card was flakey, but all is well now.
The good news: No code changes for my code to work with PNUT v51a!
Chip,
I just bumped into something that has changed in recent releases of Pnut. I don't know if this is an explicit change you intended or not. The following compiled and ran when I was compiling with Pnut v46:
But now, with Pnut v51a, I need to add an extra set of brackets as per the following:
Otherwise I basically get a syntax error. Pnut v51a reports a missing comma or closing bracket without the edit.
There is a new version (v52) of PNut.exe at the top of this thread.
v52 - 2025.10.08
New MOVBYTS(), ENDIANL(), ENDIANW() methods.
NEXT/QUIT now allowed to select nesting level of operation.
DEBUG(DEBUG_END_SESSION) now ends DEBUG session and exits PNut if -rd was used on command line.
New DEBUG Display TERM color commands for added simplicity.
Blimey! That arrived almost instantly!
Seems something is broken, I got an error on a simple if/else block:
Can be reproduced also with the parser.spin2 (and other sources in the PNut package) example.
Am i missing something ?
Edit: worst than expected, seems that nothing can be added after a quit statement:
@macca Is a number now compulsory ? Does it work with "quit 0" or "quit 1" ?
(Sure, that would be silly, but just an interesting test / workaround. I'm not able to try it at the moment).
Ah, yes, with quit 1 it works (quit 0 is not valid since must be 1 to 16).
Oh goody. Might just need the default case adding back into the compiler.
@cgracey
May we have an override for "quit" and "next" without a parameter to behave like they used to, equivalent to "quit 1" and "next 1" ?
Sorry, Guys!
I see the problem. It's in my parsing of what comes after NEXT or QUIT. I need to back up when there is a line end, indicating there will be no constant.
I will have this straightened up in a few hours.
The new v52 is now posted on the OBEX and on Github.
https://obex.parallax.com/obex/pnut-spin2-latest-version/
NEXT and QUIT do not require integers after them, but an integer is needed if you want to NEXT/QUIT from outside your current REPEAT block. That integer would need to be a value of 2 or more.
I added a new file into the .zip called PointerFlexibility.spin2. It gives coding examples of how you can use byte/word/long pointers. The same basic rules apply to structure pointers, as well.
Here is that file, in case anyone wants to get a quick tour of pointers:
.
What is the intended behavior when the nesting level integer exceeded the number of levels?
For example, would QUIT maxLevel+1 be the same as QUIT maxLevel ? (Or should that throw a compiler error- or warning at least?...)
The compiler will error if you try to go too many levels out. It knows the REPEAT nesting depth.
I have a question:
Rather than using integers 1..16 to specify level, might 0..15 be better? It would go like this:
0 = This REPEAT block, no change in level (the default for NEXT/QUIT)
1 = The first-outer REPEAT block
2 = The second-outer REPEAT block
Would that be better? If I change it, I should do it soon, before any serious code gets written by anyone.
That feels a lot better to me. The way it is has been bugging me.
The languages I've used this with before, PHP and maybe Bash, both use 1-indexed. Personally I wouldn't change it. It's done; move on and conquer!
Really it's just a human language issue - how we explain the feature in docs could be worded to make either way seem like the most natural.
Yes, I think it would be better since it represents the number of levels to go back, also may I suggest to use (or allow) negative numbers ? I think that quit -n better represents quit back n level(s).
Even better would be to have some kind of label to clearly indicate what repeat level is referred, not sure what a good syntax may be, something like
repeat : label
maybe.Also, can you check if the following snippet generates the correct bytecode ?
I had just a quick look and probably I miss something, but seems to me that the inner next 2 statement doesn't pop the inner loop stack, quit 2 correctly pops both the inner and outer stacks. Today I'm a bit busy so I may not be able to look at this with more details.
Thanks.
Yes, named breaks are good because you can't get the number wrong and can't misread it. Languages like Rust do it this way.
I looked into the next level issue and I think there is an issue.
With this example:
PNut outputs this:

My implementation outputs this:
If I'm not missing something, the latter is correct, it should be equivalent to use quit in the inner loop and indeed PNut's output is correct when using quit.
Looking at the listing, PNut doesn't pop the inner loop stack with the next statement (the comments are the bytecode from PNut):
Macca, I am glad you found this! I am not popping the stack correctly. I will get this fixed tomorrow, hopefully in the morning. Thank you for doing all these tests.