Today's release of Bitlash 1.1 corrects something that has always bugged me about Bitlash: it was too hard to add new functions to the interpreter. Now, it's easy.
You can visit Bitlash Online to read the detailed documentation of how user functions work in the new release. And there are examples that ship with Bitlash that you can load from the File → Examples → bitlash menu.
My intention here is to walk through the servo example to show how to integrate a relatively complex chunk of C as a Bitlash function, in this case a function named servo() that can drive up to 8 servos from the command line, or from macros. Load this in the Arduino IDE (File → Examples → bitlash → servo) and follow along as we step through the pieces.
My notional requirements for the servo() function were these:
So you'd be able to say things like servo(4,90) to set a servo on pin 4 to 90 degrees, to servos on 8 different pins.
Now let's consider the code from the example sketch file servo.pde to see one way to do this. Skipping past the copyright and license, which of course you already know by heart, we come to the definition of the servohandler(), which is the C function which will handle the servo() function. Here is the code:
#include "Servo.h"
#define NUMSERVOS 8 // bump this for more per http://www.arduino.cc/en/Reference/Servo
byte servo_install_count; // number of servos that have been initialized
byte servo_pin[NUMSERVOS]; // the pins of those servos
Servo myservos[NUMSERVOS]; // the Servo object for those servos
//////////
// servohandler: function handler for Bitlash servo() function
//
void servohandler(numvar servopin, numvar setting) {
byte slot = 0;
// is this pin already allocated a servo slot?
while (slot < servo_install_count) {
if (servo_pin[slot] == servopin) break;
slot++;
}
if (slot >= servo_install_count) { // not found
// do we have a free slot to allocate?
if (servo_install_count < NUMSERVOS) {
slot = servo_install_count++;
servo_pin[slot] = servopin;
myservos[slot].attach(servopin);
}
else {
Serial.println("servohandler: oops!"); // please increase NUMSERVOS above
return;
}
}
myservos[slot].write(setting);
}
The code declares arrays with NUMSERVOS slots to hold the pin the servo is bound to and a Servo object for each slot. The number of used slots is tracked by servo_install_count so when we recognize the first reference to a servo pin we properly initialize the servo with Servo.attach().
The handler routine first checks to see if we've encountered a servo command for this pin before. If not, it checks to see if we can allocate a slot for this new pin. If not, we print “oops” and do nothing.
If we can get a new slot, we initialize the Servo object by calling Servo.attach(). Then, for all good cases, we fall through and call Servo.write() for the designated pin to set the angle to the setting that is passed in.
This is a reasonably gnarly chunk of C, but note that the only Bitlash-related parts so far are in the function declaration, where the servopin and setting variables are declared as type numvar, which is a Bitlash-defined type used for all numeric calculations. For a void function this is all you need change to make your function Bitlash-ready. If your function returns a value, you need to specify numvar as the return value type. (unumvar is similarly available for unsigned quantities.)
The rest of the Bitlash integration is pretty simple, too: we need to call addBitlashFunction() to register the servohandler function with under the name “servo”, and tell Bitlash to expect 2 arguments passed when servo() is called:
void setup(void) {
initBitlash(57600); // must be first to initialize serial port
// Register the extension function with Bitlash:
// "servo" is the name Bitlash will match for the function
// -2 is the argument signature: 2 args, no return value
// (bitlash_function) servohandler is the C function handler declared above
//
addBitlashFunction("servo", -2, (bitlash_function) servohandler);
}
void loop(void) {
runBitlash();
}
The cast ”(bitlash_function)” may be unfamiliar to new C coders, and it is the only other bit of tricky business. It is required that those exact characters ”(bitlash_function)” be in front of your function name, in parentheses, without the quotes, just as it shows in the example. If you get complaints from the compiler that look like this:
In function 'void setup()': error: invalid conversion from 'void (*)(numvar, numvar)' to 'void (*)()'
…you probably forgot the (bitlash_function) cast.
Let's review the two little tweaks it takes to make your C function Bitlash-ready:
If you've read this far, you're probably curious to see it how it works in action. Let's build a small application for two servos on pin 4 and 5.
Suppose we are trying to accomplish these motion objectives:
Almost like I planned it, we can model this as two state machines, one with three states and one with four, running at different rates requiring millisecond timing. Easy, right?
Let's start with servo 4. It wants a pattern that goes 0, 90, 180, 0, 90, 180,… with a 350 ms interval. Let's use the Bitlash variable 's' to represent the state of this servo. If you examine the formula s++%3 * 90 in the second argument here, you will see that each time it is called the expression produces the next value in the required 0, 90, 180, 0, 90, 180,… sequence:
> sm4:="servo(4, s++%3 * 90)" > run4:="run sm4,350" > run4 (servo cycles happily, ^C to stop)
The sm4 macro sets the servo to the appropriate angle for state s, and increments s for next time. The run4 macro arranges for Bitlash to call sm4 about every 350 milliseconds, as best it can on a round robin basis.
So we have servo 4 doing what we want with two lines of Bitlash code. We ducked a bullet by finding a simple formula to generate the required output pattern.
(Or did we? There is a subtle bug here, one that would not be quickly evident, and the first to write with a correct description of the problem will be immortalized in print here with credit for finding it when I reveal the bug, and the simple fix…)
For servo 5 the pattern of angles 25, 75, 135, 180 is not so easy, so let's use a more direct, brute force approach: we will use the variable 'a' as a global servo 5 angle variable. A separate macro for each state of servo 5 selects the appropriate angle; these are the z-macros below. The variable 'z' holds the state of servo 5:
z0:="a=25" z1:="a=75" z2:="a=135" z3:="a=180" sm5:="switch z++%4:z0,z1,z2,z3; servo(5,a)" run5:="run sm5,227" (servo cycles happily, ^C to stop)
NOTE: Mind your commas and semicolons around switch statements or you are guaranteed to have an interesting debugging experience.
The macros z0 through z3 are responsible for setting the conditions of the state. Other conditions could of course be set, including variable timing per state step (using snooze()) if it were a requirement, though in this case it is not.
The sm5 macro is the state machine switch. The formula z++%4 generates 0, 1, 2, 3, 0, 1, 2, 3 so you might call it a state-stepper for the switch: it calls the right z-macro for the selected state. The z-macro sets up the angle variable, and then sm5 finishes by calling servo(5,a) to set the servo to that angle.
Then run5 runs the whole thing every 227 milliseconds or so.
Perhaps there is prettier code that can accomplish the same result, but let me point out one benefit from this slightly brute-force method: you can change the definition of any of the z-macros while the machine is running and it will cycle happily to the new angle the next time that state comes around.
Let's wrap up by arranging a startup macro to kick off this servonic mayhem:
startup:="run4; run5"
Giving this for our complete application:
> ls sm4:="servo(4, s++%3 * 90)" run4:="run sm4,350" z0:="a=25" z1:="a=75" z2:="a=135" z3:="a=180" sm5:="switch z++%4:z0,z1,z2,z3; servo(5,a)" run5:="run sm5,227" startup:="run4; run5"
So from the top, we've walked through all the stages of integrating a C function with your code into Bitlash, and building a Bitlash application on top.
I hope you will find the User Functions enhancement to Bitlash as useful as I do, and I look forward to your comments and feedback.
Discussion