Arduino based Hardware Controller

Having a dedicated hardware for IO control can be conveniently modular and surprisingly time-effective. Additionally, it provides isolation from the main resource intensive computation module which is desirable especially during hardware prototyping stage.

I have used several home-brewed PICs as hardware controllers with Intel Core 2 Duo and lately with Raspberry PI and it has served its purpose. But, it has also demanded lot of maintainability and keep up from me as well. So naturally, when Arduino started gaining open source community support, it was time to transition over there.

Sparkfun's Red board was my first choice for Arduino because Friday lunch hour is best spent driving to Sparkfun and glimpsing over their newest kits in display under the pretext of picking up the order. And also, who wants to let their weekend go in vain when there is an option to get it locally.

For my drive-and-steer robot , the hardware I wanted to control with this was:

  • 2 Servos for gripper unit
  • 2 inputs from limit switches
  • 2 PWM outputs for DC motor control, and

Both Raspberry Pi and Arduino support high speed Serial. So I went with it.

A key-value parser ran in an eternal loop on the Arduino to grab serial data and route it to proper IOs. Whereas a python script on Raspberry Pi made sure that the serial commands were put out as quickly as possible to keep the overall latency low.

Initializations (setup();)

Setup serial: The hardware controller gets data from Raspberry Pi over Serial. The highest baud rate I could go with was 500k

Pin initializations: This sets the initial power-on pin states for all the IOs. Clearly, depends on what hardware is connected to what IO.

// PIN assignments
// Left Motor (lm)
#define LM_EN   6   //ENA (~)
#define LM_DIR1 7   //IN1
#define LM_DIR2 8   //IN2

// Right Motor (rm)
#define RM_EN   11  //ENB (~)
#define RM_DIR1 12  //IN3
#define RM_DIR2 13  //IN4

// Vertical lift Motor (vm)
#define VM_EN   3   //EN (~)
#define VM_DIR1 2   //IN
#define VM_DIR2 4   //IN

// Gripper Motor (gm)
#define GS_EN   5

Service Thread (loop();)

The main loop() function cyclically and sequentially calls the following pseudo-threads:

  • getSerialDataAndParse();
  • inertialEngine();
  • motionTimeoutEngine();

Pseudo-threads

Pseudo-threads are function calls which do a discrete task before giving up the processor for other tasks. The general principles are:

  1. Small Latency: There should be no delay() in a pseudo-thread, or any blocking call like while(1)
  2. OOism: The pseudo-thread should maintain its own private data to clock through its internal state machine.
  3. Non-overlapping Scope: There should be minimal overlapping in responsibilities that psuedo-threads share avoiding complex data sharing and redundant functionality.
  4. Scalability: The pseudo-threads should be able to accommodate addition of more pseudo-threads without their performance or functionality deterioration.

An overview of each of the 3 pseudo-threads is as follows:

Pseudo-thread 1: getSerialDataAndParse();

This is the entry point on power-up. It receives the data and parses it. The key state-machine steps are:

  • Gets the data over Serial
  • Breaks received data into string chunks separated by delimiter character
  • Parses key (sub string before =)
  • Uses setDeviceParameter() call to set the value of the corresponding key element.
    • boolean setDeviceParameter(Devices_t device, DeviceParm_t deviceParm, int value)

Pseudo-thread 2: inertialEngine();

All motors have their armature inertia. This pseudo-thread makes sure that the software commands to control the motor follow the motor inertial characteristics to prevent any over current / under voltage / reverse voltage damages.

A static device matrix with initial values for all motors is used as a single-source of truth to track the current value, target value and other configurations:

DeviceRow_t deviceMatrix[] = {
/*  device    currentValue, lastChangeTimeStamp, targetValue, minLimitVal, maxLimitVal, jumpOffsetValue, timeDelay */
 {  leftMotor,           1,                   0,           0,        -255,         255,               1,       5 },
 {  rightMotor,          1,                   0,           0,        -255,         255,               1,       5 },
 {  verticalMotor,       1,                   0,           0,        -255,         255,               1,       5 },
 {  panServo,            0,                   0,           0,           0,         180,             255,       5 },
 {  tiltServo,           0,                   0,           0,           0,         180,             255,       5 },
 {  gripperServo,        0,                   0,          40,          35,          72,             255,       0 },
 {  dispatchServo,       0,                   0,           0,           0,         180,             255,       5 },
};

The inertialEngine pseudo-thread also uses this matrix to track the states of each device and maintains the internal state of it's state machine.

The setDeviceParameter(...) is the API to make changes to this matrix. The getSerialDataAndParse() pseudo-thread parses the incoming data and uses this call to set, for example, targetValue for the specified device.

The inertialEngine() cyclically goes through each device and changes the currentValue to be closer to targetValue by the offset configured by jumpOffsetValue. It does so if the difference in lastChangeTimeStamp and currentTimeStamp is more or equal to the one set by timeDelay.

The currentValue stays in bounds defined by minLimitVal and maxLimitVal.

Pseudo-thread 3: motionTimeoutEngine();

This pseudo-thread's primary role is to ensure safety by ensuring auto-power off on the configured motors.

A static timeout matrix with initial values for all motors is used as a single-source of truth to track the trigger value, time out and target value:

TimeoutMatrixRow_t timeoutMatrix[] = {
/*     device     triggerValue, startTime, timeout, targetValue*/ 
  {verticalMotor,          100,         0,    3050,         75, },
//{leftMotor,                1,         0,    5000,          0, },
//{rightMotor,               1,         0,    5000,          0, },
};

The motionTimeoutEngine pseudo-thread also uses this matrix to track the states of each device and maintains the internal state of it's state machine.

The motionTimeoutEngine() cyclically goes through each device and sets the currentValue of the device (in deviceMatrix matrix) to targetValue configured in timeoutMatrix if it's currentValue is greater than triggerValue and it had been timeout milliseconds since then.

Related but unrelated:

  • This officially should be a design consideration for RTOS's pseudo-threads: