Using P2 Spin to create an array of text strings

Using P2 Spin, I have a buffer defined in a var as a long, outbuf[400] that contains a long string. I would like to store up to 6 of outbuf[400] values referenced via another array, movebuf[6]. I haven’t run across any obvious examples in my search for code. My initial test of just storing the strings into movebuf didn’t work out although my code could be at fault (it was getting late and I was tired!), thought I’d ask the coding experts for ideas before I hit it again today.
Bob
Comments
I'm sure there are better ways of doing this but this is one way I'm using in my current project.
I have a method which will let me find a specific text string in a list.
DAT negativeNumberText byte "NEGATIVE INDEX", 0 PUB FindString(firstStr, stringIndex) : result '' Finds start address of one string in a list '' of string. "firstStr" is the address of '' string #0 in the list. "stringIndex" '' indicates which of the strings in the list '' the method is to find. if stringIndex < 0 result := @negativeNumberText return result := firstStr repeat while stringIndex repeat while byte[result++] stringIndex--
I use the above method to fill an array with the start of each of the strings.
PUB FillStringPointers(indexLocation, firstString, numberOfStrings) | localIndex, maxIndex maxIndex := numberOfStrings - 1 repeat localIndex from 0 to maxIndex long[indexLocation][localIndex] := FindString(firstString, localIndex)
Here's an example call to FillStringPointers().
FillStringPointers(@zMenuIndex, @zMenuLine, Z_MENU_SIZE_9)
Here's the data used in the above.
DAT zMenuIndex long 0-0[Z_MENU_SIZE_9] zMenuLine byte "User Flow", 0 byte "Raw Flow", 0 byte "Set Units", 0 byte "Time/Date", 0 byte "Serial #", 0 byte "*Sub Flow", 0 byte "Circles", 0 byte "Rectangles", 0 byte "*Other", 0
After FillStringPointers() is called, the array zMenuIndex will contain the address of each string listed under zMenuLine.
The address of each string could have been entered into the zMenuIndex but this would add an extra element to the program I didn't want to when writing the above code.
A text string could be accessed using:
Serial Str(zMenuIndex[2])
The above command would send "Set Units" to the UART.
Edit: In case it's not obvious, here's the constant defined.
CON Z_MENU_SIZE_9 = 9
An alternative technique of storing text arrays is to use the same number of bytes for each string. This will end up having wasted space since there will be a lot of empty bytes.
Here's an example of this technique.
DAT 'Flow Units Text fUnitsT1264B0 byte "CUSTOM", 0[CHAR_PER_UNIT_12 - 6] {1270} byte "GR/", 0[CHAR_PER_UNIT_12 - 3] {1276} byte "KG/", 0[CHAR_PER_UNIT_12 - 3] {1282} byte "LB/", 0[CHAR_PER_UNIT_12 - 3] {1288} byte "MMSCF", 0[CHAR_PER_UNIT_12 - 5] {1294} byte "mSCF", 0[CHAR_PER_UNIT_12 - 4] '5 DAT nccUnits1300 byte "NCC", 0[CHAR_PER_UNIT_12 - 3] {1306} byte "NLP", 0[CHAR_PER_UNIT_12 - 3] {1312} byte "NM3/", 0[CHAR_PER_UNIT_12 - 4] {1318} byte "NMLP", 0[CHAR_PER_UNIT_12 - 4] {1324} byte "SCC", 0[CHAR_PER_UNIT_12 - 3] '10 {1330} byte "SCF", 0[CHAR_PER_UNIT_12 - 3] {1336} byte "SCI", 0[CHAR_PER_UNIT_12 - 3] {1342} byte "SCM", 0[CHAR_PER_UNIT_12 - 3] {1348} byte "SFP", 0[CHAR_PER_UNIT_12 - 3] {1354} byte "SLP", 0[CHAR_PER_UNIT_12 - 3] '15 {1360} byte "SM3/", 0[CHAR_PER_UNIT_12 - 4] {1366} byte "SMLP", 0[CHAR_PER_UNIT_12 - 4] {1372} byte "SMP", 0[CHAR_PER_UNIT_12 - 3] {1378} byte "TP", 0[CHAR_PER_UNIT_12 - 2] extraUnitText1384B0 byte "EXTRA0", 0[CHAR_PER_UNIT_12 - 6] {1390} byte "EXTRA1", 0[CHAR_PER_UNIT_12 - 6] {1396} byte "EXTRA2", 0[CHAR_PER_UNIT_12 - 10] '6 + 4 = 10 DAT reg1400 byte 0[4] 'Part of group above {1402} byte "EXTRA3", 0[CHAR_PER_UNIT_12 - 6] {1408} byte "EXTRA4", 0[CHAR_PER_UNIT_12 - 6] {1414} byte "EXTRA5", 0[CHAR_PER_UNIT_12 - 6] {1420} byte "EXTRA6", 0[CHAR_PER_UNIT_12 - 6] {1426} byte "EXTRA7", 0[CHAR_PER_UNIT_12 - 6] {1432} byte "EXTRA8", 0[CHAR_PER_UNIT_12 - 6] {1438} byte "EXTRA9", 0[CHAR_PER_UNIT_12 - 6] {1444} byte "EXTRAa", 0[CHAR_PER_UNIT_12 - 6] {1450} byte "EXTRAb", 0[CHAR_PER_UNIT_12 - 6]
Here the address of a specific string can be found by multiplying by the largest string allowed.
For example:
Serial.Str(@fUnitsT1264B0 + (2 * CHAR_PER_UNIT_12))
The above command would send the text "KG/" to the UART.
Using the fixed length technique require more RAM but has the benefit of allowing the text to be altered by the program. The altered text just as to fix in the limited area. This is 11 bytes in the example above. You need to leave a terminating zero.
Thats an interesting approach to the problem, I need to walk through your code and make sure I understand what you are doing.
My case is a bit different as it appears you are using pre-defined strings in the DAT, my output strings are constantly changing and created on the fly. This has given me a couple of ideas to try out too!
This is for my hexapod where I create a string of number’s separated by commas on the fly that is then used by the corresponding hexapod leg for movement. Right now I create one movement string and the string sequence is sent off to its leg for action. Then it starts creating the movement string for the next leg. As each string is created via a very math intensive process, and I need to create 6 separate strings, I thought about doing all the math for each leg upfront, creating an array to temporarily contain the movement strings. After all 6 strings are created I could quickly index through the array sending the strings out more quickly rather than having the lull between string creation due to the math.
Maybe I'm looking at things too simplistically, but I would start here.
con BUF_SIZE = 400 var byte move0[BUF_SIZE] byte move1[BUF_SIZE] byte move2[BUF_SIZE] byte move3[BUF_SIZE] byte move4[BUF_SIZE] byte move5[BUF_SIZE] byte bufidx
Now you can create a few help routines that simplify your coding interface.
pub buf_pntr(idx) : p_buf p_buf := @move0 + (idx * BUF_SIZE) pub clear_move(idx) bytefill(buf_pntr(idx), 0, BUF_SIZE) pub build_move(idx) | p_buf p_buf := buf_pntr(idx) ' build string starting at p_buf pub send_move(idx) : nextidx serial.str(buf_pntr(idx)) if (++idx == 6) nextidx := 0 else nextidx := idx
Am I out of whack?
Are the values being sent integers? If so, saving the values to send as strings seems like an extra (and unnecessary) complication.
I'd save the values you want to send as integers and then create the strings on the fly with "Serial.Dec()" type methods.
Jon's strategy is pretty much the same as my second strategy but Jon's is a lot easier to read. I just copied a section from a project I'm currently working on. Jon was extra nice and wrote easy to read code.
Jon's code is always easy to read! The string consists of a series of integer like this: "$2,900,0,900,1" where the "$" is the start character followed by the robot leg the string affects, the commas separate integers representing angle data to the robot leg controllers. The string is built on the fly based on the desired movement. Duane, I like the code example you sent as it gave me other ideas to try out also.
Thanks, guys, for the kind words about my coding style. To me, writing code appeals to my technical and artistic sides.
Seeing the construction of Bob's string reminded me about a project I did for the P1 several years ago called jm_hfcp.spin (human friendly control protocol). Like Bob's strings, it uses plain text to send commands and values from point A to point B (and back if needed). Over coffee I thought I'd explore porting that to the P2, and see if Bob would share how he's building his strings. Keeping it simple, I made these to methods.
pub putc(p_str, c) : p_next byte[p_str++] := c return p_str pub putdec(p_str, value) : p_next | zflag, d, c '' Put value as decimal into string at p_str '' -- returns pointer to next character in p_str '' -- working range is NEGX+1..POSX if (value == 0) byte[p_str++] := "0" return p_str ' we can exit early elseif (value < 0) byte[p_str++] := "-" ' add sign to string abs= value ' remove sign from value zflag, d := false, 1_000_000_000 ' no zeros, initial divisor repeat 10 if (value >= d) ' digit for this position? c := value / d ' extract byte[p_str++] := c + "0" ' convert to ASCII and write value //= d ' remove digit from value zflag := true ' we can print 0s now elseif (zflag || (d == 1)) ' if 0s okay or last digit byte[p_str++] := "0" ' write the zero d /= 10 ' update divisor p_next := p_str ' return next
I tested with this:
p_cmd := @cmdbuf p_cmd := putc(p_cmd, "$") p_cmd := putc(p_cmd, ",") p_cmd := putdec(p_cmd, 2) p_cmd := putc(p_cmd, ",") p_cmd := putdec(p_cmd, 900) p_cmd := putc(p_cmd, ",") p_cmd := putdec(p_cmd, 0) p_cmd := putc(p_cmd, ",") p_cmd := putdec(p_cmd, 900) p_cmd := putc(p_cmd, ",") p_cmd := putdec(p_cmd, 1)
...which produced the string Bob described in his comment. Since the P2 makes inline assembly possible, I thought I'd convert putdec() to see if there's a benefit.
pub putdec2(p_str, value) : p_next '' Put value as decimal into string at p_str '' -- returns pointer to next character in p_str '' -- working range is NEGX+1..POSX org .check0 tjnz value, #.checkneg wrbyte #"0", p_str ' value is 0 add p_str, #1 jmp #.done ' early exit .checkneg cmps value, #0 wcz if_a jmp #.dodec wrbyte #"-", p_str ' write sign to string add p_str, #1 abs value ' remove sign from value .dodec mov pr0, #0 ' zflag is false mov pr1, ##1_000_000_000 ' set divisor mov pr2, #10 ' up to 10 digits .dloop cmp value, pr1 wcz if_b jmp #.dzlast qdiv value, pr1 ' divide getqx pr3 ' get digit getqy value ' save remainder add pr3, #"0" ' convert to ASCII wrbyte pr3, p_str add p_str, #1 mov pr0, #1 ' can print 0s now jmp #.dnext .dzlast tjnz pr0, #.is0 ' is zflag set? cmp pr1, #1 wcz ' or last digit? if_ne jmp #.dnext .is0 wrbyte #"0", p_str add p_str, #1 .dnext qdiv pr1, #10 getqx pr1 djnz pr2, #.dloop .done mov p_next, p_str end
The inline assembly version will trim 30%-50% of the time required to convert a non-zero value, so it might be worth using. I did it an an exercise. Note, though, the it won't work with NEGX -- to me this is an unlikely value in a control program, so I didn't burden the conversion code with handling it.
In acting, take 1 is usually a warm-up, and things get better with take 2. After a few hours of day-job work, I took a lunch break and went back to my jm_hfcp code. The dec() method in that object does correctly deal with negx so I ported it to the P2, adding early exit for 0.
pub dec(p_buf, value) : p_next | x, d, zf '' Put [signed] decimal representation of value into p_buf if (value == 0) byte[p_buf++] := "0" return p_buf x := value == negx if (value < 0) value := abs(value+x) byte[p_buf++] := "-" d, zf := 1_000_000_000, false repeat 10 if (value >= d) byte[p_buf++] := value / d + "0" + x*(d == 1) value //= d zf := true elseif (zf || (d == 1)) byte[p_buf++] := "0" d /= 10 p_next := p_buf
It's a bit slower than the code above for having to deal with the negx fix-it flag. I ported it t inline PASM and it's a big speed boost over the Spin2 version. This is what I'll have in my P2 version of HFCP. This early exits on 0, and handles negx correctly.
pub dec2(p_buf, value) : p_next | x, n, d, zf, ch '' Put [signed] decimal representation of value into p_buf org .check0 tjnz value, #.checkneg wrbyte #"0", p_buf ' value is 0 add p_buf, #1 jmp #.done .checkneg mov x, #0 cmps value, ##negx wcz ' if negx if_e mov x, #1 ' set flag cmps value, #0 wcz ' if negative if_ae jmp #.dodec adds value, x ' adjust if negx abs value ' make positive wrbyte #"-", p_buf ' write sign add p_buf, #1 .dodec mov n, #10 ' up to 10 digits in long mov d, ##1_000_000_000 ' set divisor mov zf, #0 ' clear print 0 flag .loop cmp value, d wcz ' >0 digit at this position? if_b jmp #.dzlast qdiv value, d ' extract digit getqx ch getqy value ' save remainder add ch, #"0" ' convert to ASCII cmp d, #1 wcz ' if last digit if_e add ch, #1 ' fix negx wrbyte ch, p_buf ' write sign add p_buf, #1 mov zf, #1 jmp #.next .dzlast tjnz zf, #.is0 ' is zflag set? cmp d, #1 wcz ' or last digit? if_ne jmp #.next .is0 wrbyte #"0", p_buf ' add "0" to buffer add p_buf, #1 .next qdiv d, #10 ' update divisor getqx d djnz n, #.loop ' update digit count .done mov p_next, p_buf end
Here is the method I'm using for creating my strings. Based on the notes, you made the suggestions that went into creating this on the P1 a while back!
pub buildstring(legnum, femur1, tibia1, coxa1, LDown) ' output buffer for leg controller movement commands - based on JonnyMac code suggestions/example bytefill(@outbuf, 0, 128) strAppend(@outbuf, string("$,")) strAppend(@outbuf, dec(legnum)) strAppend(@outbuf, string(",")) strAppend(@outbuf, dec(femur1)) strAppend(@outbuf, string(",")) strAppend(@outbuf, dec(tibia1)) strAppend(@outbuf, string(",")) strAppend(@outbuf, dec(coxa1)) strAppend(@outbuf, string(",")) strAppend(@outbuf, dec(lDown)) strAppend(@outbuf, string(13)) pub strAppend(buf, s)| n1, n2 n1 := strsize(buf) n2 := strsize(s) + 1 'trailing 0 buf += n1 bytemove(buf, s, n2)
Thanks for the additional suggestions, I like your newest string builder, especially the PASM version. I can use the 2 versions to help me in learning PASM which is a task I've been putting off for a while. I especially need to convert the math heavy Spin code over to in-line PASM as it could really use the speed increase.
This gives me something to study on the plane tomorrow as my wife and I head out of this land of ice and snow to much warmer climates for a break!
Thank you Jon for all your input, I have quite a bit of code inspired or written by you in my robot!
Bob
JonnyMac, your example code for putting strings into an array got me thinking and searching the Spin commands, so I came up with this as another possible solution. The debug output shows the expected values at the correct locations.
pub testStringArray() | n ' test ability to store and address multiple strings in an array repeat n from 1 to 6 'create test string strAppend(@outbuf, string("$,")) strAppend(@outbuf, dec(n)) 'dec() comes from simple_numbers.spin by Chip Gracey as converted to Spin2 strAppend(@outbuf, string(",900 ")) 'copy test string into movebuf1 at location based on n and buffer size bytemove(@movebuf1 + (n * OUTBUF_SIZE), @outbuf, OUTBUF_SIZE) 'clear the test string from outbuf bytefill(@outbuf, 0, OUTBUF_SIZE) 'verify test strings are being saved correctly debug(zstr(@movebuf1 + (n * OUTBUF_SIZE))) ' final debug used to verify the program hasn't hung up! debug("Done!")
You indicated speed was so I was thinking that having each of the routines return pointer to the next available character in the buffer might help versus always having to use strsize() to find the length of the buffer. The code you showed might be changed to this:
bytefill(@outbuf, 0, BUF_SIZE) p_buf := @outbuf p_buf := puts(p_buf, string("$,")) p_buf := putd(p_buf, lenNum) p_buf := putc(p_buf, COMMA) p_buf := putd(p_buf, femur1) p_buf := putc(p_buf, COMMA) p_buf := putd(p_buf, tibia1) p_buf := putc(p_buf, COMMA) p_buf := putd(p_buf, coxa1) p_buf := putc(p_buf, COMMA) p_buf := putd(p_buf, lDown) p_buf := putc(p_buf, CR)
...using these methods
pub putc(p_buf, c) : p_next '' Put character c into p_buf '' -- returns pointer to next character in p_buf byte[p_buf++] := c return p_buf pub puts(p_buf, p_str) : p_next | len '' Put string p_str into p_buf '' -- returns pointer to next character in p_buf len := strsize(p_str) bytemove(p_buf, p_str, len) return p_buf + len pub putd(p_buf, value) : p_next | x, n, d, zf, ch '' Put value as decimal string into p_buf '' -- returns pointer to next character in p_buf org .check0 tjnz value, #.checkneg wrbyte #"0", p_buf ' value is 0 add p_buf, #1 jmp #.done .checkneg mov x, #0 cmps value, ##negx wcz ' if negx if_e mov x, #1 ' set flag cmps value, #0 wcz ' if negative if_ae jmp #.dodec adds value, x ' adjust if negx abs value ' make positive wrbyte #"-", p_buf ' write sign add p_buf, #1 .dodec mov n, #10 ' up to 10 digits in long mov d, ##1_000_000_000 ' set divisor mov zf, #0 ' clear print 0 flag .loop cmp value, d wcz ' >0 digit at this position? if_b jmp #.dzlast qdiv value, d ' extract digit getqx ch getqy value ' save remainder add ch, #"0" ' convert to ASCII cmp d, #1 wcz ' if last digit if_e add ch, #1 ' fix negx wrbyte ch, p_buf ' write sign add p_buf, #1 mov zf, #1 jmp #.next .dzlast tjnz zf, #.is0 ' is zflag set? cmp d, #1 wcz ' or last digit? if_ne jmp #.next .is0 wrbyte #"0", p_buf ' add "0" to buffer add p_buf, #1 .next qdiv d, #10 ' update divisor getqx d djnz n, #.loop ' update digit count .done mov p_next, p_buf end
I love having inline PASM to experiment with, and to add speed updates in many places. That said, there are elements of the Spin2 interpreter that are amazingly efficient, so it's always best to do an emperical test. I do it with this simple code.
t := getct() { test code here } t := getct() - t - 40 term.dec(t)
Sometimes this test will reveal that the manual PASM isn't worth the effort, but the only way to know is to write the code and test.
Decided to try a couple more number-to-string conversions in PASM2 while watching the Daytona 500. These produce fixed-width strings.
pub ibin(value, digits, p_dest) : p_next '' Put [indicated] binary representation of value into buffer @p_dest org cmps digits, #1 wcz ' must be >0 if_b jmp #.done fle digits, #32 ' limit to 32 wrbyte #"%", p_dest ' put indicator into buffer add p_dest, #1 ror value, digits ' move bits to output to msb end .binout rol value, #1 ' get a digit (bit) mov pr0, value ' copy and pr0, #1 ' mask off other digits add pr0, #"0" ' convert to ASCII wrbyte pr0, p_dest ' write to destination add p_dest, #1 djnz digits, #.binout ' finished? .done mov p_next, p_dest ' return next char in target end pub ihex(value, digits, p_dest) : p_next '' Put [indicated] hex representation of value into buffer @p_dest org cmps digits, #1 wcz ' must be >0 if_b jmp #.done fle digits, #8 ' limit to 8 wrbyte #"$", p_dest ' put indicator into buffer add p_dest, #1 mov pr0, digits ' copy digits shl pr0, #2 ' convert digits to bits ror value, pr0 ' align bits to output with msb .hexout rol value, #4 ' get a digit (nibble) mov pr0, value ' copy and pr0, #$F ' mask off other digits cmp pr0, #10 wcz ' check for 0..9 if_b add pr0, #"0" ' make "0".."9" if_ae add pr0, #("A"-10) ' make "A".."F" wrbyte pr0, p_dest ' write to destination add p_dest, #1 djnz digits, #.hexout ' finished? .done mov p_next, p_dest ' return next char in target end
Thanks Jon for all the input. Unfortunately I’m well away from my stuff in snowy Michigan and enjoying the warm sands of Maui for a bit before I can get back to my computer and try these out. I appreciate all you have done to help me and others out over the years.