Using FullDuplexSerial to receive data via USB
Hi everyone,
I'm trying to write a program for the P1 that will allow the P1 to receive serial data via the USB port from a python program being run on the connected computer. The purpose is to broaden the functionality of automated testing that is typically done with a python program using pyserial. So I'm essentially trying to get the P1 to act like a bit whacker/banger. I'm currently using FullDuplexSerial but am having some trouble with it.
The code I'm using is below. I'll keep researching/experimenting, but if anyone has suggestions, I'd appreciate it.
Thanks,
Zach
CON
_clkmode = xtal1 + pll16x
_xinfreq = 5_000_000
RST = 15 'OLED pins
DC = 16
CS = 17
CLK_oled = 18
DIN = 19
clk_enc = 28 'Encoder pins
dt = 29
btn = 11
LED0 = 3 'pins
LED1 = 4
LED2 = 5
LED3 = 6
LED4 = 7
LED5 = 8
LED6 = 9
LED7 = 10
Rx = 31 'Serial pins
Tx = 30
LE = 2 'latch pin
CLK_FREQ = (_clkmode >> 6) * _xinfreq
MS_001 = CLK_FREQ / 1_000
'USB is on COM3
OBJ
serial : "FullDuplexSerial"
VAR
long Data
long DataX
PUB Main
serial.Start(Rx, Tx, %0000, 9600)
repeat
Data := serial.Rx
ifnot DataX == Data
outa[LED0] := (Data & %00000001)
outa[LED1] := (Data & %00000010) >> 1
outa[LED2] := (Data & %00000100) >> 2
outa[LED3] := (Data & %00001000) >> 3
outa[LED4] := (Data & %00010000) >> 4
outa[LED5] := (Data & %00100000) >> 5
outa[LED6] := (Data & %01000000) >> 6
outa[LED7] := (Data & %10000000) >> 7
DataX := Data
Comments
The problem is not with FDS. I used FDS on a Propeller board that was communicating with the PC via PySerial for a TV commercial several years ago. It worked fine.
Zach,
Grabbing raw data seems simple at first, But due to the litany of problems I have encountered over the years and different systems, I always use a simple protocol that avoids bytes that various systems and serial implementations can choke on.
It goes like this:
Command to prop:
:FF
Where the colon lets my propeller program know there is real data in the string, Then the data in hex. It can be any length, as you seem to be using single bytes, I used two hex chars for 1 byte. Then followed by 13, (Return) so you can receive it like a string (Which it is...) Then convert the hex to a raw byte.
I use a similar protocol for data in the other direction as well..
Python can easily convert the data to/from hex, and I ripped off the hex converter from Parallax Serial Terminal for the propeller end.
This has the advantage that if you use a serial terminal (Parallax Serial Terminal perhaps) to debug, it is easy to see what is happening.
@JonnyMac ,
Thanks. My suspicion was that it was more likely an issue on my end (likely oversimplifying as @R Bagget is suggesting) as opposed to FullDuplexSerial. The python program I'm using I know works using a Bit Whacker from sparkfun. So I'm trying to use that same program to send that information (in this case binary 9) to the prop instead of the Bit Whacker and have the prop light up the LEDs that would correspond to that binary 9. Then the next step would be to tie those lines going to the LEDs to lines going to an RF component to drive its state. In this case, I'm loading the Spin program to EEPROM and then running the python program in the command prompt. The python program gets about as far as checking to make sure the com port is open ( the same com port that the prop is using) which it recognizes as being open, but then the python program seems to get stuck on the the section that configures the serial pins as outputs. I'm not sure if at this point there is some conflict between my python code and spin code that is holding things up. Any thoughts?
@R Baggett,
Thank you for that tip. That's also a good idea to take a look at how Parallax Serial Terminal handles that information. Could you send me an example so I can see what you're talking about in Spin?
Thank you both,
Zach
You need to set the LED pins to output first. This id done with the DIRA[] array.
If the LEDs are contiguous like in your case, you can just write the whole byte to the pin group, no need to check the bits separatly:
PUB Main serial.Start(Rx, Tx, %0000, 9600) dira[LED7..LED0] := %11111111 repeat Data := serial.Rx ifnot DataX == Data outa[LED7..LED0] := Data DataX := Data
Andy
Good morning @Ariba ,
Thank you for pointing that out. I realized later that afternoon that there were a few small things like that I was missing. I fixed those but am still not getting the data coming over the serial port. My current code is posted below. I don't think it's an issue with my spin code. In using RxCheck from FDS, it's throwing a -1 and lighting the appropriate LED's to signal that there is no data coming through. So I'm now thinking that there may be an issue with my python code. The python code that I'm using to try and transfer the serial data works well with the JSB Bit Whacker (DEV-00762 from SparkFun), but I understand that they're using different chips that likely have different properties. I've tried a few other python code combinations to successfully transfer the data, but with no luck. I know this may not be kosher, but does anyone have a python code example that successfully transfers data over the serial port to the propeller?
CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 RST = 15 'OLED pins DC = 16 CS = 17 CLK_oled = 18 DIN = 19 clk_enc = 28 'Encoder pins dt = 29 btn = 11 LED0 = 3 'pins LED1 = 4 LED2 = 5 LED3 = 6 LED4 = 7 LED5 = 8 LED6 = 9 LED7 = 10 RxP = 31 'Serial pins TxP = 30 LE = 2 'latch pin CLK_FREQ = (_clkmode >> 6) * _xinfreq MS_001 = CLK_FREQ / 1_000 'USB is on COM3 OBJ serial : "FullDuplexSerial" OLED : "OLED_AsmFast v0.5 Driver" enk : "jm_encoder_1x" sn : "Simple_Numbers" misc : "Misc_Methods" sserial : "Simple_Serial" VAR long Data long DataX long Decimal PUB Main OLED.Init(CS,DC,DIN,CLK_oled,RST) enk.Start(clk_enc, dt, btn, true, 1, 0, 127) serial.Start(RxP, TxP, %0000, 9600) sserial.init(31, 30, 9600) dira[LED7..LED0]~~ OLED.AutoUpdateOff OLED.clearDisplay OLED.write1x6String(String("Bit"),3,15,0,OLED#Yellow,OLED#BLACK) OLED.write1x6String(String("Whack"),5,0,32,OLED#Yellow,OLED#BLACK) 'misc.wait_for_state(btn, 0, 50) OLED.clearDisplay 'misc.delay(100) repeat Data := serial.RxCheck Decimal := sn.decf(Data, 5) ifnot DataX == Data OLED.write1x6String(Decimal,5,0,0,OLED#Yellow,OLED#BLACK) outa[LED0] := (Data & %00000001) outa[LED1] := (Data & %00000010) >> 1 outa[LED2] := (Data & %00000100) >> 2 outa[LED3] := (Data & %00001000) >> 3 outa[LED4] := (Data & %00010000) >> 4 outa[LED5] := (Data & %00100000) >> 5 outa[LED6] := (Data & %01000000) >> 6 outa[LED7] := (Data & %10000000) >> 7 DataX := Data
Thanks,
Zach
Hi everyone,
I figured out what the issue was.
Thank you for all of the help!
Zach
You should probably share the solution so that others can learn from it.
Morning everyone,
Below is the code that ended up working for me to turn the P1 into a bit whacker that is sent the necessary data by a python program using the USB serial com port. The code is very much adapted from the parser code used in the program prop_serial_slave_010.spin, which is used in chapter 10 of "Programming and Customizing the Multicore Propeller Microcontroller" book from Parallax (highly recommended for anyone that wants a thorough introduction into spin and the P1 chip). It took me some time to fully understand and appreciate what the parser program was doing, but once I did, I was able to adapt it into the program so that the spin program could properly receive and parse the string being sent by the python program for the data within the string. Once parsed, each character (separated by a comma/space) is assigned a token value. Then a specific character can be taken out of the received string using its assigned token value (which is a string). To then use the program as a bit whacker, that token needs to be converted to a decimal value (done using the StrToDec(stringptr) method) and then "separated out" so that the binary representation of the decimal value in question is outputted using the eight LED's and attached wires that go to the mythical connected part to change its state.
Best,
Zach
CON _clkmode = xtal1 + pll16x _xinfreq = 5_000_000 _stack = 128 ' accomodate display memory and stack CLK_FREQ = (_clkmode >> 6) * _xinfreq MS_001 = CLK_FREQ / 1_000 CLOCKS_PER_MICROSECOND = _xinfreq / 1_000_000 ' used for delay function RST = 11 'OLED pins DC = 12 CS = 13 CLK_oled = 14 DIN = 15 {{ V = 16 VGA pins H = 17 B1 = 18 B0 = 19 G1 = 20 G0 = 21 R1 = 22 R0 = 23 }} clk_enc = 28 'Encoder pins dt = 29 btn = 25 LED0 = 3 'pins LED1 = 4 LED2 = 5 LED3 = 6 LED4 = 7 LED5 = 8 LED6 = 9 LED7 = 10 RxP = 31 'Serial pins TxP = 30 LE = 2 'latch pin ' ASCII codes for ease of parser development ASCII_A = 65 ASCII_B = 66 ASCII_C = 67 ASCII_D = 68 ASCII_E = 69 ASCII_F = 70 ASCII_G = 71 ASCII_H = 72 ASCII_O = 79 ASCII_P = 80 ASCII_Z = 90 ASCII_0 = 48 ASCII_9 = 57 ASCII_BS = 127 ' backspace ASCII_LF = $0A ' line feed ASCII_CR = $0D ' carriage return ASCII_ESC = $1B ' escape ASCII_HEX = $24 ' $ for hex ASCII_BIN = $25 ' % for binary ASCII_LB = $5B ' [ ASCII_SEMI = $3B ' ; ASCII_EQUALS = $3D ' = ASCII_PERIOD = $2E ' . ASCII_COMMA = $2C ' , ASCII_SHARP = $23 ' # ASCII_NULL = $00 ' null character ASCII_SPACE = $20 ' space ASCII_COLON = $3A ' Colon ' null pointer, null character NULL = 0 OBJ serial : "FullDuplexserial_drv_012.spin" OLED : "OLED_AsmFast v0.5 Driver" enk : "jm_encoder_1x" sn : "Simple_Numbers" misc : "Misc_Methods" VAR long Data long DataX long DataS long Decimal long cogon, cog ' ids for cogs byte input_buff[80] ' storage for input buffer long input_buff_index ' index into current position of command buffer byte tok_buff[80] ' storage for token buffer during processing byte prompt[32] ' storage for user prompt long tok_buff_index ' index into current position of tokenbuffer long token_ptr ' used to point to output token from tokenizer long tokens[ 16 ] ' array of pointers to parsed tokens ready for processing long num_tokens ' number of tokens in token array long cmd_token ' a single command token long cmd_data_ptr ' ptr to command token long cmd_parse_index ' index of command token in array long arg1, arg2, arg3, arg4 ' general parameters used during parameter extraction ' state vars for strtok_r function, basically static locals that we must define long strtok_string_ptr long strtok_string_index long strtok_string_length long Baud ' Default 9600 PUB Main | ch OLED.Init(CS,DC,DIN,CLK_oled,RST) enk.Start(clk_enc, dt, btn, true, 1, 0, 127) serial.Start(RxP, TxP, %0000, 9600) dira[LED7..LED0]~~ OLED.AutoUpdateOff OLED.clearDisplay OLED.write1x6String(String("Bit"),3,15,0,OLED#Yellow,OLED#BLACK) OLED.write1x6String(String("Whack"),5,0,32,OLED#Yellow,OLED#BLACK) input_buff_index := -1 repeat ' attempt to retrieve character from serial terminal ch := serial.rxcheck ' character ready in receive buffer? if (ch <> -1) ' process character, test for carriage return, or basic EDITing characters like back space case ch ASCII_LF, ASCII_CR: ' return ' print the buffer if (input_buff_index > -1) ' at this point we have the command buffer, so we can parse it ' copy it bytemove( @tok_buff, @input_buff, ++input_buff_index ) ' null terminate it tok_buff[ input_buff_index ] := 0 tok_buff_index := input_buff_index ' reset buffer input_buff_index := -1 ' tokenize input string num_tokens := 0 ' start the tokenization of the string (this function mimics the C strtok_r function more or less strtok_r(@tok_buff, string(","), @token_ptr) ' continue tokenization process now that first token has been found repeat while (token_ptr <> NULL) ' upcase the token before insertion StrUpper( token_ptr ) ' insert token into token array tokens[ num_tokens++ ] := token_ptr ' get next token strtok_r( NULL, string(","), @token_ptr) ' end repeat tokenization... ' // end if buffer valid ' // Now process command in large if, elseif, else chain ' each "if" attempts to process a different command, if no ' command is found then user typed it wrong or its not supported ' and an error is emmited. Each command passes parameters to the COG ' running the specific driver being requested; NTSC, VGA, etc. ' the full functionality of each driver is NOT exposed, only a subset to ' illustrate the idea of use the Prop as a slave processor controlled by a ' serial communications stream in ASCII... {{ First process "local" client commands for terminal. These include requests to the terminal such as clear the screen, help, etc. The remaining commands are "remote commands" that are processed and then issued to the "worker" cores running the respective drivers for video, vga, audio, etc. }} ' clear the terminal screen command if ( strcomp( tokens[0], string("C") ) ) serial.txstringnl( string("OK")) OLED.clearDisplay OLED.write1x6String(string("OK"),2,15,0,OLED#Yellow,OLED#BLACK) elseif ( strcomp( tokens[0], string("O") )) serial.txstringnl( string("OK")) DataS := ( tokens[2] ) Data := StrToDec(DataS) Decimal := sn.decf(Data, 5) ifnot DataX == Data OLED.write1x6String(Decimal,5,0,0,OLED#Yellow,OLED#BLACK) outa[LED0] := (Data & %00000001) outa[LED1] := (Data & %00000010) >> 1 outa[LED2] := (Data & %00000100) >> 2 outa[LED3] := (Data & %00001000) >> 3 outa[LED4] := (Data & %00010000) >> 4 outa[LED5] := (Data & %00100000) >> 5 outa[LED6] := (Data & %01000000) >> 6 outa[LED7] := (Data & %10000000) >> 7 ' print prompt serial.txstring( @prompt ) ' editing commands, for now just backspace, later you could add more advanced terminal based editing ' but the trick is to keep the buffer on the client side sync'ed with the terminal's display! ASCII_BS: ' backspace ' insert null if (input_buff_index => 0) input_buff[ input_buff_index-- ] := ASCII_NULL ' echo character serial.tx( ch ) other: ' insert character into command buffer for processing input_buff[ ++input_buff_index ] := ch ' echo character to terminal (terminal must be in non ECHO mode, otherwise you will see input 2x!) 'serial.tx( ch ) DataX := Data '////////////////////////////////////////////////////////////////////////////// 'FUNCTIONS SECTION //////////////////////////////////////////////////////////// '////////////////////////////////////////////////////////////////////////////// PUB StrToDec(stringptr) : value | char, index, multiply '' Converts a zero terminated string representation of a decimal number to a value value := index := 0 repeat until ((char := byte[stringptr][index++]) == 0) if char => "0" and char =< "9" value := value * 10 + (char - "0") if byte[stringptr] == "-" value := - value PUB HexToDec(n) ' converts hex digit to decimal if ( (n => "0") and (n =< "9") ) return (n - ASCII_0) elseif ( (n => "A") and (n =< "F") ) return (n - "A" + 10) else return ( 0 ) ' ///////////////////////////////////////////////////////////////////////////// PUB atoi2(string_ptr) | index, sum, ch ' this function tries to convert the string to a number, supports binary %, hex $, decimal (default) ' Eg. %001010110, $EF, 25 ' initialize vars index := 0 sum := 0 ' try to determine number base if (byte [string_ptr][index] == ASCII_HEX) repeat while ( (ch := byte [string_ptr][index]) <> 0) sum := (sum << 4) + HexToDec( ToUpper(ch) ) index++ return(sum) ' // end if hex number elseif (byte [string_ptr][index] == ASCII_BIN) repeat while ( (ch := byte [ string_ptr ][index]) <> 0) sum := (sum << 1) + (ch - ASCII_0) index++ return(sum) ' // end if binary number else ' must be in default base 10, assume that repeat while ( (ch := byte [ string_ptr ][index]) <> 0) sum := (sum * 10) + (ch - ASCII_0) index++ return(sum) ' else, have no idea of number format! return(0) ' ///////////////////////////////////////////////////////////////////////////// PUB strtok_r(string_ptr, delimeters_ptr, token_ptr2) {{ ' this function mimics the functionality of the C library strtok_r(...) function ' the function is first called with a pointer to string that the caller wants ' tokenized in string_ptr, then during remaining calls string_ptr is sell to NULL ' as the string is tokenized. Other than that, the 1st and remaining calls always ' require delimeters_ptr to point to a string with single character delimeters that ' the caller wants to be used to seperate token, eg. ",:" would use comma, colon and ' white space (always included), finally as tokens are located NULLs are placed at the ' terminus of each token and token_ptr2 is pointed to the head of the token string Example: 1st Call: string_ptr -> "This is a test 10,20" delimeters_ptr -> "," token_ptr2 -> "This" string_ptr -> "ThisNULLis a test 10,20" 2nd Call: string_ptr -> NULL delimeters_ptr -> "," token_ptr2 -> "is" string_ptr -> "ThisNULLisNULLa test 10,20" 3rd Call: string_ptr -> NULL delimeters_ptr -> "," token_ptr2 -> "a" string_ptr -> "ThisNULLisNULLaNULLtest 10,20" 4th Call: string_ptr -> NULL delimeters_ptr -> "," token_ptr2 -> "test" string_ptr -> "ThisNULLisNULLaNULLtestNULL10,20" 5th Call: string_ptr -> NULL delimeters_ptr -> "," token_ptr2 -> "10" string_ptr -> "ThisNULLisNULLaNULLtestNULL10NULL20" 6th Call: string_ptr -> NULL delimeters_ptr -> "," token_ptr2 -> "20" string_ptr -> "ThisNULLisNULLaNULLtestNULL10NULL20NULL" 7th Call: string_ptr -> NULL delimeters_ptr -> "," token_ptr2 -> NULL string_ptr -> "ThisNULLisNULLaNULLtestNULL10NULL20NULL" }} ' test if this is first call if (string_ptr <> NULL) ' initialize tokenizer strtok_string_ptr := string_ptr strtok_string_length := strsize ( strtok_string_ptr ) ' else nth call, continue using global string, do not re-init it ' initialize index into string to head strtok_string_index := 0 ' consume white space and delimeters repeat while ( (IsSpace(byte[strtok_string_ptr][strtok_string_index]) <> -1) or (IsInSet( byte[strtok_string_ptr][strtok_string_index], delimeters_ptr ) <> -1) ) strtok_string_index++ strtok_string_length-- ' has string been consumed? if (strtok_string_length =< 0) ' we are done, return NULL to pointers ,exit long [ token_ptr2 ] := 0 return( 0 ) ' adjust strtok_string_ptr to point to head of token strtok_string_ptr += strtok_string_index ' reset strtok_string_index to 0 strtok_string_index := 0 ' consume alpha num repeat while ( (IsInSet( byte[strtok_string_ptr][strtok_string_index], delimeters_ptr ) == -1) and ((IsAlpha(byte[strtok_string_ptr][strtok_string_index]) <> -1) or (IsDigit(byte[strtok_string_ptr][strtok_string_index]) <> -1) or (IsPunc(byte[strtok_string_ptr][strtok_string_index]) <> -1)) ) strtok_string_index++ strtok_string_length-- ' at this point we have the token, ready to return ' NULL terminate byte[strtok_string_ptr][strtok_string_index] := 0 ' write pointer out to input ptr long [ token_ptr2 ] := strtok_string_ptr ' save strtok_string_ptr string_ptr := strtok_string_ptr ' now adjust strok_string_ptr AFTER the token, so that on next call its ready to read next token (if there is one) strtok_string_ptr += (strtok_string_index+1) strtok_string_length-- ' return address of string token return( string_ptr ) ' ///////////////////////////////////////////////////////////////////////////// PUB StrUpper(string_ptr) ' upcases a string if (string_ptr <> NULL) repeat while (byte[ string_ptr ] <> NULL) byte[ string_ptr ] := ToUpper( byte[ string_ptr ] ) string_ptr++ ' return string return (string_ptr) ' ///////////////////////////////////////////////////////////////////////////// PUB ToUpper(ch) ' returns the uppercase of the sent character if (ch => $61 and ch =< $7A) return(ch-32) else return(ch) ' ///////////////////////////////////////////////////////////////////////////// PUB IsInSet(ch, set_string) ' tests if sent character is in string repeat while (byte[set_string][0] <> NULL) if (ch == byte[set_string++][0]) 'serial.txstring(string ("found!") ) return( ch ) 'serial.txstring(string ("not found!") ) ' not found return( -1 ) ' ///////////////////////////////////////////////////////////////////////////// PUB IsSpace(ch) ' tests if sent character is white space, cr, lf, space, tab if (ch == ASCII_SPACE) return (ch) else return(-1) ' ///////////////////////////////////////////////////////////////////////////// PUB IsDigit(ch) ' tests if sent character is a number 0..9, returns integer 0..9 if (ch => ASCII_0 and ch =< ASCII_9) return (ch-ASCII_0) else return(-1) ' ///////////////////////////////////////////////////////////////////////////// PUB IsAlpha(ch) ' tests if sent character is a number a...zA...Z ch := ToUpper(ch) if ( (ch => ASCII_A) and (ch =< ASCII_Z)) return (ch) else return(-1) ' ///////////////////////////////////////////////////////////////////////////// PUB IsPunc(ch) ' tests if sent character is a punctuation symbol !@#$%^&*()--+={}[]|\;:'",<.>/? ch := ToUpper(ch) if ( ((ch => 33) and (ch =< 47)) or ((ch => 58) and (ch =< 64)) or ((ch => 91) and (ch =< 96)) or ((ch =>123) and (ch =< 126)) ) return (ch) else return(-1) '////////////////////////////////////////////////////////////////////////////// PUB Delay( time_us ) ' delays time sent in microseconds waitcnt ( CNT + time_us * CLOCKS_PER_MICROSECOND ) ' return current count after delay return( CNT )
I do have a question about the sharing of the serial com port.
My steps for running the above program are: load the program into EEPROM, start the python program in Windows command prompt, and then increment the python program forward. When the python program gets to its ser.open() function though it causes a restart event for the P1. I'm pretty sure that this is related to FDS, but does anyone know of a way around that restart event?
Thanks,
Zach
well - old problem. That is the way P1 handles programming.
Opening/closing a port in windows toggles DTR and resets the prop.
Putting something into a USB port on the computer re-enumerates the USB devices and toggles DTR and resets the Prop.
Even sending serial data from the P1 to the USB chip without having anything connected to the USB chip fires up the USB chip and resets the Prop.
The only solution I know of is to physically cut the trace between the USB chip and the propeller reset and put a jumper there to enable/disable reset on the P1.
Sadly
Mike
This all sounds incredibly complicated. On the P2, from the PC, I think you can just write a forth command and the P2 Taqoz interpreter will respond to it. And Taqoz can write to the terminal, and presumably Python can read it as a string.
Of course I have not yet done this, I am sure it will be more difficult than I expect. The devil is in the details.
Chris
It has nothing to do with taqoz or python. It is just the way programming the propeller is done.
Most of Parallax p1 or p2 boards have build in USB chips hardwired to the reset of the propellers and will reset when windows opens or closes a serial port.
It is by design. The only exception currently is the EDGE board, it does not have build in USB and you can use a prop plug with a small 3 pin header in between leaving the connection to reset disconnected.
Else it will hit you.
Mike
Thank you for the comments on the USB port/chip and reset relationship. I'll keep that in mind for the future.
Zach
The easiest (but not ideal) solution is to put a pull-up resistor on the Prop's TX pin. That will keep the USB chip powered parasitically whenever the Prop is powered, thus avoiding the resets when the program raises the TX pin.
-Phil
WOW.
And I foolishly thought that if the board has a usb plug on it, just plug in and go.
Thank you guys.
Chris
From the Eval Board Rev C Document:
I think that means that I can connect with a usb cable to the upper right of the eval board, and it will not reboot every time. Is that correct?
yes.
And on Rev C you can also use the small adapter board and the WiFi module when that switch is off.
Mike