Picaxe Remote Procedure Calls over I2C

MSP1

New Member
Having experimented with RPCs as a means of putting some structure on I2C communication, I thought the basic ideas might be of interest to others. I was not able to find any information after searching for Picaxe Remote Procedure Call online. The PDF attached to this post provides a detailed account of the RPC implementation I have been using. It also presents a simple demonstration complete with logic analyser traces showing how a system contain a Picaxe RPC server and a Picaxe RPC client synchronise their activities. The sources for the client and the server are also attached to this post as copying source text from a PDF often results in loss of layout details.

--- The attached PDF and the following programs were updated after the discussion below ---

Server Source
Code:
'Picaxe I2C Remote Procedure Call - Slave

#Picaxe 28X1

Symbol i2c_slave_address = $C6

Symbol rpc_procedure_id  =                    0
Symbol rpc_result        = rpc_procedure_id + 1
Symbol rpc_parameter_1   = rpc_result       + 1
Symbol rpc_parameter_2   = rpc_parameter_1  + 1
Symbol rpc_parameter_3   = rpc_parameter_2  + 1

Symbol no_operation      =                    0
Symbol procedure_1_id    =                    1
Symbol procedure_2_id    =                    2
Symbol procedure_3_id    =                    3

main:
  Call initialise_i2c_as_slave
  Do
    Call handle_call
  Loop
Return

initialise_i2c_as_slave:
  hi2csetup i2cslave, i2c_slave_address
  Put rpc_procedure_id, no_operation
Return

handle_call:
  Symbol procedure_id = b0
  ptr = rpc_procedure_id
  Do Loop Until @ptr <> no_operation
  Get rpc_procedure_id,  procedure_id
  Select Case procedure_id
    Case procedure_1_id: Call procedure_1
    Case procedure_2_id: Call procedure_2
    Case procedure_3_id: Call procedure_3
    Else                 'Do nothing
  EndSelect
  Put rpc_procedure_id, no_operation
Return

procedure_1:
  Symbol x1   = b1
  Symbol y1   = b2
  Symbol sum1 = b3
  Get rpc_parameter_1, x1
  Get rpc_parameter_2, y1
  sum1 = x1 + y1
  Put rpc_result, sum1
Return

procedure_2:
  Call wait_for_button_press
Return

wait_for_button_press:
  Symbol press_button        = pinc.0
  Symbol contact_bounce_time = 200
  Do Loop While press_button = 0
  Pause contact_bounce_time
  Do Loop While press_button = 1
Return

procedure_3:
  Symbol pin   = b1
  Symbol value = b2
  Get rpc_parameter_1, pin
  Get rpc_parameter_2, value
  ptr = rpc_parameter_3
  Do While @ptr <> 0
    serout pin, N4800, (@ptrinc)
  Loop
  serout pin, N4800, (": ", #value, Cr, Lf)
Return
Client Source
Code:
'Picaxe I2C RPC - Master

#Picaxe 08M2

Symbol i2c_slave_address = $C6

Symbol rpc_procedure_id  =                    0
Symbol rpc_result        = rpc_procedure_id + 1
Symbol rpc_parameter_1   = rpc_result       + 1
Symbol rpc_parameter_2   = rpc_parameter_1  + 1
Symbol rpc_parameter_3   = rpc_parameter_2  + 1

Symbol no_operation      =                    0
Symbol procedure_1_id    =                    1
Symbol procedure_2_id    =                    2
Symbol procedure_3_id    =                    3
  
Symbol sum1              =                   b0

main:
  Call initialise_i2c
  Do
    Call wait_for_button_press
    sertxd ("Start...", Cr, Lf)
  
    Call procedure_1
    sertxd ("procedure_1: done ", #sum1, Cr, Lf)
  
    Call procedure_2
    sertxd ("procedure_2: done", Cr, Lf)
  
    Call send_min_value_on_remote_pin1
    sertxd ("send_min_value_on_remote_pin1: done", Cr, Lf)
    
    Call send_max_value_on_remote_pin1
    sertxd ("send_max_value_on_remote_pin1: done", Cr, Lf)
  
    Call send_count_on_remote_pin2
    sertxd ("send_count_on_remote_pin2: done", Cr, Lf)
  Loop
Return

initialise_i2c:
  hi2csetup i2cmaster, i2c_slave_address, i2cslow, i2cbyte
Return

wait_for_button_press:
  Symbol press_button = pinc.3
  Do Loop While press_button = 0
  Pause 200
  Do Loop While press_button = 1
Return

procedure_1:
  Symbol x1 = b1
  Symbol y1 = b2
  x1 = 12
  y1 = 15
  hi2cout rpc_parameter_1,  (x1, y1)
  hi2cout rpc_procedure_id, (procedure_1_id)
  Call wait_for_rpc_completion
  hi2cin rpc_result, (sum1)
Return

procedure_2:
  hi2cout rpc_procedure_id, (procedure_2_id)
  Call wait_for_rpc_completion
Return

send_min_value_on_remote_pin1:
  Symbol min_pin   = b1
  Symbol min_value = b2
  min_pin   = 1
  min_value = 5
  hi2cout rpc_parameter_1,  (min_pin,  min_value, "Min", 0)
  hi2cout rpc_procedure_id, (procedure_3_id)
  Call wait_for_rpc_completion
Return

send_max_value_on_remote_pin1:
  Symbol max_pin   = b1
  Symbol max_value = b2
  max_pin   =  1
  max_value = 15
  hi2cout rpc_parameter_1,  (max_pin,  max_value, "Max", 0)
  hi2cout rpc_procedure_id, (procedure_3_id)
  Call wait_for_rpc_completion
Return

send_count_on_remote_pin2:
  Symbol count_pin   = b1
  Symbol count_value = b2
  count_pin   =   2
  count_value = 100
  hi2cout rpc_parameter_1,  (count_pin,  count_value, "Count", 0)
  hi2cout rpc_procedure_id, (procedure_3_id)
  Call wait_for_rpc_completion
Return

wait_for_rpc_completion:
  Symbol procedure_id = b0
  Do
    hi2cin rpc_procedure_id, (procedure_id)
  Loop Until procedure_id = no_operation
Return
 

Attachments

Last edited:

hippy

Technical Support
Staff member
An excellent write-up and clearly explained.

There is a potential pitfall in using 'hi2cflag' in the server to indicate an RPC has been placed. This flag is ( I believe ) set on the first byte written over I2C so could potentially be set ( and RPC handler invoked ) before all RPC parameters have received.

That will be mitigated by the time it takes the server to get from seeing 'hi2cflag' as set into the RPC handler and using those parameters, and speed of transfer is aided by placing the master RPC invocation in a single HI2COUT command as you are doing. If you had a large number of parameters or were passing them with multiple HI2COUT commands it could lead to some timing issues.

One option might be to also pass the number of parameters in the RPC invocation and then check that with respect to 'hi2clast' in the server to ensure all parameters have been received.

I don't think that detracts in any way from what you have and it may be a more theoretical problem that a practical one.
 

MSP1

New Member
Hi Hippy.

I knew this would benefit from the attention of a Picaxe expert! I was assuming that the hi2cflag did not get set until the entire write from the master had completed. Given what you point out, I'll have a look at something along the lines you suggest and update my documents. Thanks for your comments.
 

MSP1

New Member
In my experience, theoretical problems tend to turn into practical problems sooner rather than later! Therefore having thought about this a bit more and managed to get the demonstration system to fail sending 25 character strings to procedure_3, I have come up with an alternative way of having the server determine when it is safe to start executing a remote call.

After looking at a number of alternatives, they either had the characteristic of making the stubs significantly more complicated, or they involved things that it is too easy for developers to get wrong on a regular basis.

Hippy's suggestion falls into the later category (sorry Hippy!). While sending the number of parameter bytes as an additional parameter and using hi2clast to wait until all of them have arrived would work, it places an additional responsibility on the client developer to get the counts right all the time. This may not seem to be a big issue (we can all count right?), but it implies that every time a small change is made to any parameters, e.g. inserting an extra character in a string, the counts need to be updated. I can easily imagine myself forgetting to do this on a regular basis and not finding out about it until something goes wrong in testing. Worse still, such a fault might be masked by something else and not show up until the system has been in service for a bit.

Therefore, given the behaviour of the hi2cflag, I propose not using it and not using hi2clast either. As an alternative, I propose to change all client stubs so that the parameters are sent first and then the procedure identifier is send last of all. The following example shows how the stub for procedure_1 would be rewritten. Notice that the first hi2cout used to write the parameters specifies the starting location in the server's scratch-pad memory to be written to is the location already allocated for the first parameter. The second write then fills in the procedure identifier in the correct place.

Code:
procedure_1:
  Symbol x1 = b1
  Symbol y1 = b2
  x1 = 12
  y1 = 15
  hi2cout rpc_parameter_1,  (x1, y1)
  hi2cout rpc_procedure_id, (procedure_1_id)
  Call wait_for_rpc_completion
  hi2cin rpc_result, (sum1)
Return
This format has the advantages that no counting is involved, and we no longer need to send the place holder for the return result. In addition, it is fairly easy for the developer to remember that remote calls are sent in post-fix order with the procedure identifier last.

The server now needs to be modified to initialise the procedure identifier location to the no_operation value. All it then needs to do is to check the procedure identifier location to see if there is an outstanding request, i.e. the procedure identifier location is not equal to the no_operation value. This can only happen after all of the parameters have been sent. The modified portions of the server are shown below.

Code:
initialise_i2c_as_slave:
  hi2csetup i2cslave, i2c_slave_address
  Put rpc_procedure_id, no_operation
Return

handle_call:
  Symbol procedure_id = b0
  ptr = rpc_procedure_id
  Do Loop Until @ptr <> no_operation
  Get rpc_procedure_id,  procedure_id
  Select Case procedure_id
    Case procedure_1_id: Call procedure_1
    Case procedure_2_id: Call procedure_2
    Case procedure_3_id: Call procedure_3
    Else                 'Do nothing
  EndSelect
  Put rpc_procedure_id, no_operation
Return
Having modified the demonstration programs, they now work even with the long parameter strings that broke the previous version.

Unless anyone can see a problem with this, I will update my original documentation and the embedded sources for the demonstration system over the next day or so. Thanks again to Hippy for his input.
 

sages

Member
In you amended protocol, what's to stop the procedure id occurring as one of the passed parameters and aborting the call early?
I've worked with communication protocols for a couple of years and have found that adding a 'data length' parameter ( as mentioned by Hippy ) works very well and is a pretty simple technique.
The old xon/xoff type protocols do use unique characters as delimiters but they only work if the character ( or additional timing ) is unique.
 

MSP1

New Member
Hi sages.

Even though the procedure identifier is sent last in the revised scheme, it is still sent to the first location in the scratch-pad memory. Parameter information is always stored after the first location so there is no confusion. The master can address scratch-pad memory in any order it likes as the first arguments for both hi2cout and hi2cin specify the starting location for data to be written to or read from. I agree that data length is often used in communication protocols, but here it would make the server more complex, and as I pointed out requires the developer to manually work out the counts and keep them up-to-date every time changes are made. I routinely waste time due to forgetting about this sort of thing!

RPCs are designed to abstract away from low level communication details, so forcing the developer to keep track of values that are really part of the RPC implementation, rather than part of the high level design, detracts from that. Unfortunately there are no mechanisms in the Picaxe language to automate the counting:(
 

sages

Member
ok, I can now see how you are sending an address as well as data with the final write being at the address of the procedure identifier. I'm struggling with the difficulty in counting the number of parameters. You have to know how many parameters to send for a function so by definition you know how many are being sent.

Something else to consider with your protocol is 'stack overflow'. ie there is no limit check on the receiving scratch pad routine to ensure that the sender doesn't transmit more parameters than has been allowed for in the receiving routine.
 

MSP1

New Member
As I said, counting the parameters is not difficult, but it is an extra step that can be eliminated. Why have an extra step that is not required?

Worse still, it is an extra step that could be a source of errors. If a count were required before the first parameter, one of the stubs from the demonstration client would look as shown below. The seven is intended to be the count, but whoops, it should be six. Two for min_pin and min_value plus three characters in the string plus one more for the zero terminator. Even if I got it right first time, it is still very easy to insert some extra characters in the string and then forget to update the count, and as I pointed out, there is no language mechanism to automated setting the count, so it always has to be a manual process.

Code:
send_min_value_on_remote_pin1:
  Symbol min_pin   = b1
  Symbol min_value = b2
  min_pin   = 1
  min_value = 5
  hi2cout rpc_parameter_1,  (7, min_pin,  min_value, "Min", 0)
  hi2cout rpc_procedure_id, (procedure_3_id)
  Call wait_for_rpc_completion
Return
I've spent too many years teaching people to develop software systems and seeing how common, time consuming and costly silly mistakes can be to want to include an unnecessary extra step that would be a source of errors. I used to have a tutor who was fond of saying "Always try and avoid doing anything clever as it just increases the odds of making mistakes. Simplify!"

With respect to error handling, this is always going to be application specific. Very little on the Picaxe has any error checking because embedded systems are not like that. Even if you detect an error, what are you going to do? You can't just throw up an error dialogue on a screen, because there isn't one. Things like making sure you don't exceed the bounds of the scratch-pad memory, the size of which is different on different chips, or making sure that you don't try and store 677 in a byte are unfortunately manual processes that cannot be eliminated in this environment.
 

sages

Member
fair enough.
min_pin and min_value are defined as byte variables not word variables in the your last example thus proving your point regarding errors. Unless it was an edit by Mr Freud ;)
 

MSP1

New Member
Actually "Two for min_pin and min_value" was supposed to mean two bytes between them not two bytes each:)
 

rossko57

Senior Member
Even if you detect an error, what are you going to do?
Old fashioned protocols like Modbus implement fairly robust processes;
"Do nothing" with a frame in error - it's always better than doing the wrong thing with rubbish data.
Note that doing nothing also allows the sender to recover the situation, presuming a response is expected. Optionally, the sender can implement a timeout on an expected response - the timeout event is then able to initiate a retry.

I do see what you are saying about adding complexity, but surely a valid approach is to perfect "library routines" once only. These take care of counting bytes, calculating checksums, checking received data etc according to the protocol. The developer passes his data to and from those library routines without worrying their pretty head about such nuts and bolts?
 
Top