07 - Reducing code with better design (struct)

Submitted by Webbot on September 24, 2008 - 3:57pm.

 

 

The compiler can only optimise the code you have given it. If you make your code 'better' then there will be less of it in the first place.

 

 

 

 

Thats obvious.

 

 

 

Yes - but we aren't very good at it !!!

 

 

 

 

 

"Most software-beginners will write code that works on data. More advanced software folk will write data and then the associated code that operates on that data."

 

 

 

Hmm - that's confusing. But think of it this way: there's no point just writing code - it has to DO something else what's the point. That 'something' is normally making a servo/motor turn, read a sensor etc.

 

 

 

 

So the software-beginner may say: I need a method that sets the speed of my left-servo and another to do the same with my right-servo. I need to do this because each servo is being driven via a different I/O pin etc. This is sort of okay unless you then have 16 servos and you now have loads of code.

 

 

 

 

The more advanced author will say: I have a generic thing called a servo which has certain properties (like which I/O pin it is connected to). Given this data - I can now write a generic routine that is passed this servo data to set the speed of the servo.

 

 

 

 

 

So we are now starting to talk about Object Orientated Programming (OOP). Where an object is a servo, a motor, a sonar sensor, a bumper switch, an LCD etc. Write the code once and then the same code can deal with any number of the devices. Oh-no you're talking about me learning an OOP language like C++ or Java!  No. You can write a lot of these sorts of things in C. Lets see how.

 

 

 

 

 

C has a useful thing called a 'struct' which is short for a 'structure'. It allows us to create a whole bunch of variables into one structure. We can also use the 'typedef' keyword to create a new datatype for the structure which is much easier to then use in the rest of our code. We can use this to define all of the variables that we need to record about an object. For example: lets assume we want to drive a servo object. A servo will need the following information to be stored against it:-

 

 

  • The I/O pin it is connected to

  • The current speed. Setting this will change the servo speed but also we could read it back to find the last required speed. Lets assume we can set the speed to any value between -127 and +127

  • Is the servo 'inverted'. ie if you have a left servo and a right servo then if speed="Full forward"  for both then one will turn clockwise whilst the other will turn anti-clockwise.

 

 

Here is the C code to create a new type called 'SERVO' that contains variables to store whatever the values of the above things might be:-

 

 

 

 

typedef struct s_servo {

 

 

    volatile uint8_t*  port;         // The I/O port register

 

 

    uint8_t    bit;           // The bit mask for this I/O pin ie 1 << pin
        int8_t speed;           // The current speed. -127=Full reverse, 0=stop, 127= Full forward
        uint8_t inverted:1;   // Is the servo inverted
    } SERVO;

 

 

 

 

 

Look at the last entry 'uint8_t inverted:1'. The ':1' means that it only requires one bit to store the value as it is either true or false (ie 1 or 0). The ability to set the number of bits for each variable within the structure allows us to minimise the amount of required space by combining several small variables into the same byte.  Looking at my makefile you will notice an entry for '-fpack-struct' which means that structures should be packed down into the smallest possible number of bytes.

 

 

 

 

 

 

So lets see how we can use that to produce a better software design:-

 

 

 


 

 

 

#include "main.h"

typedef struct s_servo {
    volatile uint8_t*  port;         // The I/O port register
    uint8_t    bit;                     // The bit mask for this I/O pin ie 1 << pin
    int8_t speed;                     // The current speed. -127=Full reverse, 0=stop, 127= Full forward
    uint8_t inverted:1;              // Is the servo inverted
    } SERVO;


// Create Servo on port D:2
SERVO left  = {&PORTD,_BV(2),0,1};

// Create Servo on port D:3
SERVO right = {&PORTD,_BV(3),0,0};

// Delay loop - note the use of volatile
void delay_cycles(volatile unsigned long int cycles){
#if F_CPU == 8000000


cycles <<= 3;    // for 8Mhz clock
#endif

 

 

    while(cycles > 0){
        cycles--;
    }
}


/*
*    Interpolate between two numbers
*    value - the current value to be used
*   minVal - the minimum that 'value' can be
*   maxVal - the maximum that 'value' can be
*   minRtn - the return value if 'value = minVal'
*   maxRtn - the return value if 'value = maxVal'
*   return a value in the range minRtn to maxRtn
*/
int16_t interpolate(int16_t value, int16_t minVal, int16_t maxVal, int16_t minRtn, int16_t maxRtn){
    int32_t lRtnRange = maxRtn - minRtn;
    int32_t lValRange = maxVal - minVal;
    int32_t lRelVal = value - minVal;
    lRtnRange =  minRtn + ( lRtnRange * lRelVal / lValRange );
    return (int16_t)lRtnRange;
}


// Send pulse out to servo
// Assume 24=min, 34=center, 44=max
void servo_drive(SERVO* servo){

 

 

 

// Get the speed we want for the motor
    int8_t speed = servo->speed;

 

 

 

// If the motor is inverted then invert the speed
    if(servo->inverted){
      speed = 0 - speed;
    }   


    // Do linear interpolation
    // input  = -127 to +127
    // output = 29 to 49 - this may need to be tweaked for your servos
    uint8_t delay = interpolate(speed, -127, 127, 29, 49);
       


    // Turn output port on
    *servo->port |= servo->bit;

 

 

 

// Pause
    delay_cycles(delay);


    // Turn output port off
    *servo->port &= ~servo->bit;

 

 

 


    delay_cycles(200);

}


int main(void){
    int8_t speed = 0; // The speed to set the servos to
    int8_t diff = 1;    // Are we going up (1), or down (-1)


    while(1){
    // If we've hit the end stops then move the other way
    if(speed==127){
        diff = -1;
    }else if (speed==-127){
            diff = 1;
    }

 

 

 

// Increment or decrement the speed

 

   speed += diff;

 

 

 

// Set the speed we want the motors to go at
        left.speed = speed;
        right.speed = speed;
   
    // Send commands to the servos to set their current speed
    servo_drive(&left);
    servo_drive(&right);

    // loop pause
    delay_cycles(200);

    }
}

 

 

 


 

 

 

Notice that instead of having seperate methods to set the speed of each servo we now have one generic routine called 'servo_drive'. We pass the data about a specific servo to that method so that it can use that data to drive that servo. So if we added another 30 servos then we wouldn't need any more code. We would just need to declare them at the top of the file to say what port they were connected to.

 

 

 

Of course the big beneift is that if you to choose to replace your 30 servo with 30 DC motors then you've only got one routine to re-write not 30.

 

 

 

 

 

Also notice that structures can contain other structures. So we could have a generic structure for an IOPin and this can then be embedded in other structures such as our SERVO. Here's an example that does with the previous code:-

 


 

 

 

#include "global.h"
#include "uart.h"

// Define an IO pin
typedef struct s_iopin {
    volatile uint8_t*   port;        // The I/O port register
    uint8_t    bit;         // The bit mask for this I/O pin ie 1 << pin
   } IOPin;

 

 

 


// Define a SERVO including the IO pin that it uses

 

typedef struct s_servo {
    IOPin io;                // The I/O pin I'm connected to       
    int8_t speed;           // The current speed. -127=Full reverse, 0=stop, 127= Full forward
    uint8_t inverted:1;     // Is the servo inverted
    } SERVO;


// Create Servo on port D:2
SERVO left  = { {&PORTD,_BV(2)},0,1};   // Note how we have to put curly brackets around the first 2 parameters as they are now in a new structure

// Create Servo on port D:3
SERVO right = { {&PORTD,_BV(3)},0,0};

// Delay loop
void delay_cycles(volatile unsigned long int cycles){

#if F_CPU == 8000000

 

cycles <<= 3;    // for 8Mhz clock


#endif

 

 


    while(cycles > 0){
        cycles--;
    }
}

 

// NEW ROUTINE TO SET AN IOPIN HIGH
void io_set_high(IOPin* io){
    *io->port |= io->bit;
}

 

// NEW ROUTINE TO SET AN IOPIN LOW
void io_set_low(IOPin* io){
    *io->port &= ~io->bit;
}


/*
*    Interpolate between two numbers
*    value - the current value to be used
*   minVal - the minimum that 'value' can be
*   maxVal - the maximum that 'value' can be
*   minRtn - the return value if 'value = minVal'
*   maxRtn - the return value if 'value = maxVal'
*   return a value in the range minRtn to maxRtn
*/
int16_t interpolate(int16_t value, int16_t minVal, int16_t maxVal, int16_t minRtn, int16_t maxRtn){
    int32_t lRtnRange = maxRtn - minRtn;
    int32_t lValRange = maxVal - minVal;
    int32_t lRelVal = value - minVal;
    lRtnRange =  minRtn + ( lRtnRange * lRelVal / lValRange );
    return (int16_t)lRtnRange;
}


// Send pulse out to servo
// Assume 24=min, 34=center, 44=max
void servo_drive(SERVO* servo){
    int8_t speed = servo->speed;
    if(servo->inverted){
      speed = 0 - speed;
    }   


    // Do linear interpolation
    // input  = -127 to +127
    // output = 39-10 to 39+10
    uint8_t delay = interpolate(speed, -127, 127, 29, 49);
       


    // Turn output port on ----- CHANGED TO USE NEW ROUTINE
    io_set_high(&servo->io);

    delay_cycles(delay);


    // Turn output port off ---- CHANGED TO USE NEW ROUTINE
    io_set_low(&servo->io);


    delay_cycles(200);

}


int main(void){
    int8_t speed = 0;
    int8_t diff = 1;   


    while(1){
    // Calculate required speed for each servo
    if(speed==127){
        diff = -1;
    }else if (speed==-127){
            diff = 1;
    }
        speed += diff;

        left.speed = speed;
        right.speed = speed;
   
    // Notify the physical servos
    servo_drive(&left);
    servo_drive(&right);

    // loop pause
    delay_cycles(200);

    }
}