Dual Brushless Motor ESC, 10-62V, up to 50A per motor. Motors ganged or independent, multiple control input methods, cycle-by-cycle current limit, speed mode and torque mode control. Motors tiny to kW. Speed limit and other parameters easily set in firmware. As used in 'The Brushless Brutalist' locomotive - www.jons-workshop.com. See also Model Engineer magazine June-October 2019.

Dependencies:   mbed BufferedSerial Servo PCT2075 FastPWM

Update 17th August 2020 Radio control inputs completed

main.cpp

Committer:
JonFreeman
Date:
2018-06-17
Revision:
7:6deaeace9a3e
Parent:
6:f289a49c1eae
Child:
8:93203f473f6e

File content as of revision 7:6deaeace9a3e:

#include "mbed.h"
#include "DualBLS.h"
#include "BufferedSerial.h"
#include "FastPWM.h"
#include "Servo.h"

/*
New 29th May 2018 - YET TO CODE FOR - Fwd/Rev line from possible remote hand control box has signal routed to T5
                Also new LMT01 temperature sensor routed to T1 - and rerouted to PC_13 as InterruptIn on T1 (ports A and B I think) not workable
*/


/*  STM32F401RE - compile using NUCLEO-F401RE
//  PROJECT -   Dual Brushless Motor Controller -   Jon Freeman     June 2018.

AnalogIn to read each motor current

____________________________________________________________________________________
        CONTROL PHILOSOPHY
This Bogie drive board software should ensure sensible control when commands supplied are not sensible !

That is, smooth transition between Drive, Coast and Brake to be implemented here.
The remote controller must not be relied upon to deliver sensible command sequences.

So much the better if the remote controller does issue sensible commands, but ...

____________________________________________________________________________________


*/
#if defined (TARGET_NUCLEO_F401RE)  //  CPU in 64 pin LQFP
#include    "F401RE.h"
#endif
#if defined (TARGET_NUCLEO_F446ZE)  //  CPU in 144 pin LQFP
#include    "F446ZE.h"
#endif
/*  Global variable declarations */
volatile    uint32_t    fast_sys_timer      = 0;    //  gets incremented by our Ticker ISR every VOLTAGE_READ_INTERVAL_US
int         WatchDog    = WATCHDOG_RELOAD + 80; //  Allow extra few seconds at powerup
uint32_t    volt_reading        = 0,    //  Global updated by interrupt driven read of Battery Volts
            driverpot_reading   = 0,    //  Global updated by interrupt driven read of Drivers Pot
            sys_timer           = 0,    //  gets incremented by our Ticker ISR every MAIN_LOOP_REPEAT_TIME_US
            AtoD_Semaphore      = 0;
int         IAm;
bool        loop_flag   = false;    //  made true in ISR_loop_timer, picked up and made false again in main programme loop
bool        flag_8Hz    = false;    //  As loop_flag but repeats 8 times per sec
bool        temp_sensor_exists = false;
bool        eeprom_flag; //  gets set according to 24LC674 being found or not
bool        mode_good_flag  = false;
char        mode_bytes[36];

uint32_t    temp_sensor_count = 0,  //  incremented by every rising edge from LMT01
            last_temp_count = 0;  //  global updated approx every 100ms after each LMT01 conversion completes
//    struct  single_bogie_options    bogie;    
    double  rpm2mph = 0.0;  //  gets calculated from eeprom mode entries if present
/*  End of Global variable declarations */

Ticker  tick_vread;     //  Device to cause periodic interrupts, used to time voltage readings etc
Ticker  loop_timer;     //  Device to cause periodic interrupts, used to sync iterations of main programme loop
Ticker  temperature_find_ticker;
Timer   temperature_timer;

//  Interrupt Service Routines
void    ISR_temperature_find_ticker ()  {   //  every 960 us, i.e. slightly faster than once per milli sec
    static  bool    readflag = false;
    int t = temperature_timer.read_ms   ();
    if  ((t == 5) && (!readflag))    {
        last_temp_count = temp_sensor_count;
        temp_sensor_count = 0;
        readflag = true;
    }
    if  (t == 6)
        readflag = false;
}

/** void    ISR_loop_timer  ()
*   This ISR responds to Ticker interrupts at a rate of (probably) 32 times per second (check from constant declarations above)
*   This ISR sets global flag 'loop_flag' used to synchronise passes around main programme control loop.
*   Increments global 'sys_timer', usable anywhere as general measure of elapsed time
*/
void    ISR_loop_timer  ()      //  This is Ticker Interrupt Service Routine - loop timer - MAIN_LOOP_REPEAT_TIME_US
{
    loop_flag = true;   //  set flag to allow main programme loop to proceed
    sys_timer++;        //  Just a handy measure of elapsed time for anything to use
    if  ((sys_timer & 0x03) == 0)
        flag_8Hz    = true;
}

/** void    ISR_voltage_reader  ()
*   This ISR responds to Ticker interrupts every 'VOLTAGE_READ_INTERVAL_US' micro seconds
*
*   AtoD_reader() called from convenient point in code to take readings outside of ISRs
*/

void    ISR_voltage_reader  ()      //  This is Ticker Interrupt Service Routine ; few us between readings ; VOLTAGE_READ_INTERVAL_US    = 50
{
    AtoD_Semaphore++;
    fast_sys_timer++;        //  Just a handy measure of elapsed time for anything to use
}


class   RControl_In
{  //  Read servo style pwm input
    Timer   t;
    int32_t clock_old, clock_new;
    int32_t pulse_width_us, period_us;
public:
    RControl_In   ()   {
        pulse_width_us = period_us = 0;
        com2.printf ("Setting up Radio_Congtrol_In\r\n");
    }   ;
    bool    validate_rx ()  ;
    void    rise    ()  ;
    void    fall    ()  ;
    uint32_t    pulsewidth    ()  ;
    uint32_t    period    ()  ;
    bool    rx_active;
}   ;

uint32_t    RControl_In::pulsewidth   ()  {
    return  pulse_width_us;
}

uint32_t    RControl_In::period   ()  {
    return  period_us;
}

bool    RControl_In::validate_rx ()
{
    if  ((clock() - clock_new) > 4)
        rx_active = false;
    else
        rx_active = true;
    return  rx_active;
}

void    RControl_In::rise    ()     //  These may not work as use of PortB as port may bugger InterruptIn use
{
    t.stop   ();
    period_us = t.read_us    ();
    t.reset ();
    t.start ();
}
void    RControl_In::fall    ()
{
    pulse_width_us = t.read_us   ();
    clock_old = clock_new;
    clock_new = clock();
    if  ((clock_new - clock_old) < 4)
        rx_active = true;
}


Servo * Servos[2];

//uint32_t    HAtot = 0, HBtot = 0, A_Offset = 0, B_Offset = 0;
/*
    5   1   3   2   6   4  SENSOR SEQUENCE

1   1   1   1   0   0   0  ---___---___ Hall1
2   0   0   1   1   1   0  __---___---_ Hall2
4   1   0   0   0   1   1  -___---___-- Hall3

    UV  WV  WU  VU  VW  UW  OUTPUT SEQUENCE
*/
const   uint16_t    A_tabl[] = {  //  Origial table
    0,  0,  0,  0,  0,  0,  0,  0,  //  Handbrake
    0,  AWV,AVU,AWU,AUW,AUV,AVW,  0,  //  Forward 0, WV1, VU1, WU1, UW1, UV1, VW1, 0,    //  JP, FR, SG, PWM = 1 0 1 1   Forward1
    0,  AVW,AUV,AUW,AWU,AVU,AWV,  0,  //  Reverse 0, VW1, UV1, UW1, WU1, VU1, WV1, 0,    //  JP, FR, SG, PWM = 1 1 0 1   Reverse1
    0,  BRA,BRA,BRA,BRA,BRA,BRA,0,   //  Regenerative Braking
    KEEP_L_MASK_A, KEEP_H_MASK_A   //  [32 and 33]
}   ;
InterruptIn * AHarr[] = {
    &MAH1,
    &MAH2,
    &MAH3
}   ;
const   uint16_t    B_tabl[] = {
    0,  0,  0,  0,  0,  0,  0,  0,  //  Handbrake
    0,  BWV,BVU,BWU,BUW,BUV,BVW,  0,  //  Forward 0, WV1, VU1, WU1, UW1, UV1, VW1, 0,    //  JP, FR, SG, PWM = 1 0 1 1   Forward1
    0,  BVW,BUV,BUW,BWU,BVU,BWV,  0,  //  Reverse 0, VW1, UV1, UW1, WU1, VU1, WV1, 0,    //  JP, FR, SG, PWM = 1 1 0 1   Reverse1
    0,  BRB,BRB,BRB,BRB,BRB,BRB,0,   //  Regenerative Braking
    KEEP_L_MASK_B, KEEP_H_MASK_B
}   ;
InterruptIn * BHarr[] = {
    &MBH1,
    &MBH2,
    &MBH3
}   ;

class   motor
{
    uint32_t    Hall_total, visible_mode, inner_mode, edge_count_table[MAIN_LOOP_ITERATION_Hz]; //  to contain one seconds worth
    uint32_t    latest_pulses_per_sec, Hall_tab_ptr, direction, ppstmp;
    bool    moving_flag;
    const   uint16_t * lut;
    FastPWM * maxV, * maxI;
    PortOut * Motor_Port;
    InterruptIn * Hall1, * Hall2, * Hall3;
public:
    struct  currents    {
        uint32_t    max, min, ave;
    }  I;
    int32_t     angle_cnt;
    uint32_t    current_samples[CURRENT_SAMPLES_AVERAGED];  //  Circular buffer where latest current readings get stored
    uint32_t    Hindex[2], tickleon, encoder_error_cnt;
    uint32_t    RPM, PPS;
    double      last_V, last_I;
    motor   ()  {}  ;   //  Default constructor
    motor   (PortOut * , FastPWM * , FastPWM * , const uint16_t *, InterruptIn **)  ;
    void    set_V_limit (double)    ;  //  Sets max motor voltage
    void    set_I_limit (double)    ;  //  Sets max motor current
    void    Hall_change ()  ;           //  Called in response to edge on Hall input pin
    void    motor_set   ()  ;           //  Energise Port with data determined by Hall sensors
    void    direction_set   (int)  ;    //  sets 'direction' with bit pattern to eor with FORWARD or REVERSE in set_mode
    bool    set_mode    (int);          //  sets mode to HANDBRAKE, FORWARD, REVERSE or REGENBRAKE
    bool    is_moving   ()  ;           //  Returns true if one or more Hall transitions within last 31.25 milli secs
    void    current_calc    ()  ;       //  Updates 3 uint32_t I.min, I.ave, I.max
    uint32_t    pulses_per_sec   ()  ;  //  call this once per main loop pass to keep count = edges per sec
    int     read_Halls  ()  ;           //  Returns 3 bits of latest Hall sensor outputs
    void    high_side_off   ()  ;
}   ;   //MotorA, MotorB, or even Motor[2];

motor   MotorA  (&MotA, &A_MAX_V_PWM, &A_MAX_I_PWM, A_tabl, AHarr);
motor   MotorB  (&MotB, &B_MAX_V_PWM, &B_MAX_I_PWM, B_tabl, BHarr);

//motor * MotPtr[8];  //  Array of pointers to some number of motor objects

motor::motor    (PortOut * P , FastPWM * _maxV_ , FastPWM * _maxI_ , const uint16_t * lutptr, InterruptIn ** Hall)        //  Constructor
{   //  Constructor
    maxV = _maxV_;
    maxI = _maxI_;
    Hall_total = 0;  //  mode can be only 0, 8, 16 or 24, lut row select for Handbrake, Forward, Reverse, or Regen Braking
    latest_pulses_per_sec = 0;
    for (int i = 0; i < MAIN_LOOP_ITERATION_Hz; i++)
        edge_count_table[i] = 0;
    if  (lutptr != A_tabl && lutptr != B_tabl)
        pc.printf   ("Fatal in 'motor' constructor, Invalid lut address\r\n");
    Hall_tab_ptr = 0;
    Motor_Port = P;
    pc.printf   ("In motor constructor, Motor port = %lx\r\n", P);
    maxV->period_ticks      (MAX_PWM_TICKS + 1);  //  around 18 kHz
    maxI->period_ticks      (MAX_PWM_TICKS + 1);
    maxV->pulsewidth_ticks  (MAX_PWM_TICKS / 20);
    maxI->pulsewidth_ticks  (MAX_PWM_TICKS / 30);
    visible_mode    = REGENBRAKE;
    inner_mode      = REGENBRAKE;
    lut = lutptr;
    Hindex[0] = Hindex[1]  = read_Halls    ();
    ppstmp  = 0;
    tickleon    = 0;
    direction   = 0;
    angle_cnt   = 0;        //  Incremented or decremented on each Hall event according to actual measured direction of travel
    encoder_error_cnt = 0;  //  Incremented when Hall transition not recognised as either direction
    Hall1 = Hall[0];
    Hall2 = Hall[1];
    Hall3 = Hall[2];
    PPS     = 0;
    RPM     = 0;
    last_V = last_I = 0.0;
}

/**
void    motor::direction_set   (int dir)  {
Used to set direction according to mode data from eeprom
*/
void    motor::direction_set   (int dir)  {
    if  (dir != 0)
        dir = FORWARD | REVERSE;  //  bits used in eor
    direction = dir;
}

int     motor::read_Halls  ()  {
    int x = 0;
    if  (*Hall1 != 0)    x |= 1;
    if  (*Hall2 != 0)    x |= 2;
    if  (*Hall3 != 0)    x |= 4;
    return  x;
}

void    motor::high_side_off   ()  {
    uint16_t    p = *Motor_Port;
    p &= lut[32];   //  KEEP_L_MASK_A or B
    *Motor_Port = p;
}

void    motor::current_calc ()
{
    I.min = 0x0fffffff; //  samples are 16 bit
    I.max = 0;
    I.ave = 0;
    uint16_t    sample;
    for (int i = 0; i < CURRENT_SAMPLES_AVERAGED; i++)  {
        sample  = current_samples[i];
        I.ave += sample;
        if  (I.min > sample)
            I.min   = sample;
        if  (I.max < sample)
            I.max   = sample;
    }
    I.ave /= CURRENT_SAMPLES_AVERAGED;
}

void    motor::set_V_limit (double p)  //  Sets max motor voltage
{
    if  (p < 0.0)
        p = 0.0;
    if  (p > 1.0)
        p = 1.0;
    last_V = p;     //  for read by diagnostics
    p *= 0.95;   //  need limit, ffi see MCP1630 data
    p   = 1.0 - p;  //  because pwm is wrong way up
    maxV->pulsewidth_ticks  ((int)(p * MAX_PWM_TICKS));  //  PWM output to MCP1630 inverted motor pwm as MCP1630 inverts
}

void    motor::set_I_limit (double p)     //  Sets max motor current. pwm integrated to dc ref voltage level
{
    int a;
    if  (p < 0.0)
        p = 0.0;
    if  (p > 1.0)
        p = 1.0;
    last_I = p;
    a = (int)(p * MAX_PWM_TICKS);
    if  (a > MAX_PWM_TICKS)
        a = MAX_PWM_TICKS;
    if  (a < 0)
        a = 0;
    maxI->pulsewidth_ticks  (a);  //  PWM
}

uint32_t    motor::pulses_per_sec   ()       //  call this once per 'MAIN_LOOP_REPEAT_TIME_US= 31250' main loop pass to keep count = edges per sec
{           //  Can also test for motor running or not here
    if  (ppstmp == Hall_total)  {
        moving_flag  = false;       //  Zero Hall transitions since previous call - motor not moving
        tickleon    = TICKLE_TIMES;
    }
    else    {
        moving_flag  = true;
        ppstmp = Hall_total;
    }
    latest_pulses_per_sec = ppstmp - edge_count_table[Hall_tab_ptr];
    edge_count_table[Hall_tab_ptr] = ppstmp;
    Hall_tab_ptr++;
    if  (Hall_tab_ptr >= MAIN_LOOP_ITERATION_Hz)
        Hall_tab_ptr = 0;
    PPS = latest_pulses_per_sec;
    RPM = (latest_pulses_per_sec * 60) / 24;
    return  latest_pulses_per_sec;
}

bool    motor::is_moving ()
{
    return  moving_flag;
}

/**
bool    motor::set_mode (int m)
Use to set motor to one mode of HANDBRAKE, FORWARD, REVERSE, REGENBRAKE.
If this causes change of mode, also sets V and I to zero.
*/
bool    motor::set_mode (int m)
{
    if  ((m != HANDBRAKE) && (m != FORWARD) && (m != REVERSE) && (m !=REGENBRAKE))  {
        pc.printf   ("Error in set_mode, invalid mode %d\r\n", m);
        return  false;
    }
    if  (visible_mode != m) {   //  Mode change, kill volts and amps to be safe
        set_V_limit (0.0);
        set_I_limit (0.0);
        visible_mode = m;
    }
    if  (m == FORWARD || m == REVERSE)
        m ^= direction;
    inner_mode = m;     //  idea is to use inner_mode only in lut addressing, keep 'visible_mode' true regardless of setup data in eeprom
    return  true;
}

void    motor::Hall_change  ()
{
    const   int32_t delta_theta_lut[] =     //  Looks up -1 for forward move detected, +1 for reverse move detected, 0 for error or unknown
            {
                0, 0, 0, 0, 0, 0, 0, 0,     //  Previous Hindex was 0
                0, 0, 0,-1, 0, 1, 0, 0,     //  Previous Hindex was 1
                0, 0, 0, 1, 0, 0,-1, 0,     //  Previous Hindex was 2
                0, 1,-1, 0, 0, 0, 0, 0,     //  Previous Hindex was 3
                0, 0, 0, 0, 0,-1, 1, 0,     //  Previous Hindex was 4
                0,-1, 0, 0, 1, 0, 0, 0,     //  Previous Hindex was 5
                0, 0, 1, 0,-1, 0, 0, 0,     //  Previous Hindex was 6
                0, 0, 0, 0, 0, 0, 0, 0,     //  Previous Hindex was 7
            }  ;
    int32_t delta_theta = delta_theta_lut[(Hindex[1] << 3) | Hindex[0]];
    if  (delta_theta == 0)
        encoder_error_cnt++;
    else
        angle_cnt += delta_theta;
    *Motor_Port = lut[inner_mode | Hindex[0]];  //  changed mode to inner_mode 27/04/18
    Hall_total++;
    Hindex[1] = Hindex[0];
}


void    temp_sensor_isr ()  {   //  got rising edge from LMT01. ALMOST CERTAIN this misses some
    int t = temperature_timer.read_us   (); //  Must be being overrun by something, most likely culprit A-D reading ?
    temperature_timer.reset ();
    temp_sensor_count++;
    if  (t > 18)            //  Yes proved some interrupts get missed, this fixes temperature reading
        temp_sensor_count++;
//    T2 = !T2;   //  scope hanger
}

void    MAH_isr    ()
{
    uint32_t x = 0;
    MotorA.high_side_off    ();
    T3 = !T3;
    if  (MAH1 != 0) x |= 1;
    if  (MAH2 != 0) x |= 2;
    if  (MAH3 != 0) x |= 4;
    MotorA.Hindex[0] = x;       //  New in [0], old in [1]
    MotorA.Hall_change  ();
}

void    MBH_isr    ()
{
    uint32_t x = 0;
    MotorB.high_side_off    ();
    if  (MBH1 != 0) x |= 1;
    if  (MBH2 != 0) x |= 2;
    if  (MBH3 != 0) x |= 4;
    MotorB.Hindex[0] = x;
    MotorB.Hall_change  ();
}


//  End of Interrupt Service Routines

void    motor::motor_set  ()
{
    Hindex[0]  = read_Halls    ();
    *Motor_Port = lut[inner_mode | Hindex[0]];
}

void    setVI   (double v, double i)  {
    MotorA.set_V_limit  (v);  //  Sets max motor voltage
    MotorA.set_I_limit  (i);  //  Sets max motor current
    MotorB.set_V_limit  (v);
    MotorB.set_I_limit  (i);
}
void    setV    (double v)  {
    MotorA.set_V_limit  (v);
    MotorB.set_V_limit  (v);
}
void    setI    (double i)  {
    MotorA.set_I_limit  (i);
    MotorB.set_I_limit  (i);
}

void    read_RPM    (uint32_t * dest)  {
    dest[0] = MotorA.RPM;
    dest[1] = MotorB.RPM;
}

void    read_PPS    (uint32_t * dest)  {
    dest[0] = MotorA.PPS;
    dest[1] = MotorB.PPS;
}

void    read_last_VI    (double * d)  {   //  only for test from cli
    d[0]    = MotorA.last_V;
    d[1]    = MotorA.last_I;
    d[2]    = MotorB.last_V;
    d[3]    = MotorB.last_I;
}


/**
void    AtoD_reader ()  //  Call to here every VOLTAGE_READ_INTERVAL_US    = 50 once loop responds to flag set in isr
Not part of ISR
*/
void    AtoD_reader ()  //  Call to here every VOLTAGE_READ_INTERVAL_US    = 50 once loop responds to flag set in isr
{
    static uint32_t i = 0, tab_ptr = 0;

    if  (MotorA.tickleon)
        MotorA.high_side_off    ();
    if  (MotorB.tickleon)
        MotorB.high_side_off    ();
    if  (AtoD_Semaphore > 20)   {
        pc.printf   ("WARNING - silly semaphore count %d, limiting to sensible\r\n", AtoD_Semaphore);
        AtoD_Semaphore = 20;
    }
    while   (AtoD_Semaphore > 0)    {
        AtoD_Semaphore--;
        //  Code here to sequence through reading voltmeter, demand pot, ammeters
        switch  (i) {   //
            case    0:
                volt_reading += Ain_SystemVolts.read_u16    ();     //  Result = Result + New Reading
                volt_reading >>= 1;                                 //  Result = Result / 2
                break;                                              //  i.e. Very simple digital low pass filter
            case    1:
                driverpot_reading += Ain_DriverPot.read_u16    ();
                driverpot_reading >>= 1;
                break;
            case    2:
                MotorA.current_samples[tab_ptr] = Motor_A_Current.read_u16    (); //
                break;
            case    3:
                MotorB.current_samples[tab_ptr++] = Motor_B_Current.read_u16    (); //
                if  (tab_ptr >= CURRENT_SAMPLES_AVERAGED)   //  Current reading will be lumpy and spikey, so put through moving average filter
                    tab_ptr = 0;
                break;
        }
        i++;    //  prepare to read the next input in response to the next interrupt
        if  (i > 3)
            i = 0;
    }   //  end of while (AtoD_Semaphore > 0)    {
    if  (MotorA.tickleon)   {
        MotorA.tickleon--;
        MotorA.motor_set    (); //  Reactivate any high side switches turned off above
    }
    if  (MotorB.tickleon)   {
        MotorB.tickleon--;
        MotorB.motor_set    ();
    }
}

/** double  Read_DriverPot  ()
*   driverpot_reading is a global 16 bit unsigned int updated in interrupt service routine
*   ISR also filters signal
*   This function returns normalised double (range 0.0 to 1.0) representation of driver pot position
*/
double  Read_DriverPot  ()
{
    return ((double) driverpot_reading) / 65536.0;     //  Normalise 0.0 <= control pot <= 1.0
}

double  Read_BatteryVolts   ()
{
    return  ((double) volt_reading) / 951.0;    //  divisor fiddled to make voltage reading correct !
}

void    read_supply_vi   (double * val)  {  //  called from cli
    val[0] = MotorA.I.ave;
    val[1] = MotorB.I.ave;
    val[2] = Read_BatteryVolts  ();
}

void    mode_set   (int mode, double val)  {   //  called from cli to set fw, re, rb, hb
    MotorA.set_mode (mode);
    MotorB.set_mode (mode);
    if  (mode == REGENBRAKE)    {
        if  (val > 1.0)
            val = 1.0;
        if  (val < 0.0)
            val = 0.0;
        val *= 0.9;    //  set upper limit, this is essential
        val = sqrt  (val);  //  to linearise effect
        setVI  (val, 1.0);
    }
}

extern  void    setup_comms ()  ;
extern  void    command_line_interpreter_pc    ()  ;
extern  void    command_line_interpreter_loco    ()  ;
extern  int     check_24LC64   ()  ;   //  Call from near top of main() to init i2c bus
extern  bool    wr_24LC64  (int mem_start_addr, char * source, int length)   ;
extern  bool    rd_24LC64  (int mem_start_addr, char * dest, int length)   ;

/*struct  motorpairoptions    {   //  This to be user settable in eeprom, 32 bytes
    uint8_t MotA_dir,   //  0 or 1
            MotB_dir,   //  0 or 1
            gang,       //  0 for separate control (robot mode), 1 for ganged loco bogie mode
            serv1,      //  0, 1, 2 = Not used, Input, Output
            serv2,      //  0, 1, 2 = Not used, Input, Output
            cmd_source, //  0 Invalid, 1 COM1, 2 COM2, 3 Pot, 4 Servo1, 5 Servo2
            last;
}   ;
*/
int I_Am    ()  {   //  Returns boards id number as ASCII char
    return  IAm;
}

double  mph (int    rpm)    {
    if  (mode_good_flag)    {
        return  rpm2mph * (double) rpm;
    }
    return  -1.0;
}

int main()
{
    int eighth_sec_count = 0;

    MotA   = 0;     //  Output all 0s to Motor drive ports A and B
    MotB   = 0;
//    MotPtr[0] = &MotorA;    //  Pointers to motor class objects
//    MotPtr[1] = &MotorB;
    
    Temperature_pin.fall (&temp_sensor_isr);
    Temperature_pin.mode (PullUp);

    MAH1.rise   (& MAH_isr);    //  Set up interrupt vectors
    MAH1.fall   (& MAH_isr);
    MAH2.rise   (& MAH_isr);
    MAH2.fall   (& MAH_isr);
    MAH3.rise   (& MAH_isr);
    MAH3.fall   (& MAH_isr);

    MBH1.rise   (& MBH_isr);
    MBH1.fall   (& MBH_isr);
    MBH2.rise   (& MBH_isr);
    MBH2.fall   (& MBH_isr);
    MBH3.rise   (& MBH_isr);
    MBH3.fall   (& MBH_isr);

    MAH1.mode   (PullUp);
    MAH2.mode   (PullUp);
    MAH3.mode   (PullUp);
    MBH1.mode   (PullUp);
    MBH2.mode   (PullUp);
    MBH3.mode   (PullUp);
    Servo1_i.mode   (PullUp);
    Servo2_i.mode   (PullUp);

    //  Setup system timers to cause periodic interrupts to synchronise and automate volt and current readings, loop repeat rate etc
    tick_vread.attach_us    (&ISR_voltage_reader, VOLTAGE_READ_INTERVAL_US);    //  Start periodic interrupt generator
    loop_timer.attach_us    (&ISR_loop_timer, MAIN_LOOP_REPEAT_TIME_US);    //  Start periodic interrupt generator
    temperature_find_ticker.attach_us   (&ISR_temperature_find_ticker, 960);
    //  Done setting up system interrupt timers
    temperature_timer.start ();

//    const   int TXTBUFSIZ = 36;
//    char    buff[TXTBUFSIZ];
    pc.baud     (9600);
    com3.baud   (1200);
    com2.baud   (19200);
    setup_comms ();

    IAm = '0';
    if  (check_24LC64() != 0xa0)  { //  searches for i2c devices, returns address of highest found
        pc.printf   ("Check for 24LC64 eeprom FAILED\r\n");
        com2.printf   ("Check for 24LC64 eeprom FAILED\r\n");
        eeprom_flag = false;
    }
    else   {        //  Found 24LC64 memory on I2C
        eeprom_flag = true;
        bool k;
        k = rd_24LC64   (0, mode_bytes, 32);
        if  (!k)
            com2.printf ("Error reading from eeprom\r\n");

        int err = 0;
        for (int i = 0; i < numof_eeprom_options; i++) {
            if  ((mode_bytes[i] < option_list[i].min) || (mode_bytes[i] > option_list[i].max))  {
                com2.printf ("EEROM error with %s\r\n", option_list[i].t);
                err++;
            }
//            else
//                com2.printf ("%2x Good %s\r\n", buff[i], option_list[i].t);
        }
        rpm2mph = 0.0;
        if  (err == 0)  {
            mode_good_flag = true;
            MotorA.direction_set    (mode_bytes[MOTADIR]);
            MotorB.direction_set    (mode_bytes[MOTBDIR]);
            IAm = mode_bytes[ID];
            rpm2mph = 60.0                                                          //  to Motor Revs per hour;
                    * ((double)mode_bytes[MOTPIN] / (double)mode_bytes[WHEELGEAR])  //  Wheel revs per hour
                    * PI * ((double)mode_bytes[WHEELDIA] / 1000.0)                  //  metres per hour
                    * 39.37 / (1760.0 * 36.0);                                      //  miles per hour
        }
           //  Alternative ID 1 to 9
//        com2.printf ("Alternative ID = 0x%2x\r\n", buff[6]);
    }
//    T1 = 0;   Now WRONGLY hoped to be InterruptIn counting pulses from LMT01 temperature sensor
    T2 = 0; //  T2, T3, T4 As yet unused pins
    T3 = 0;
    T4 = 0;
//    T5 = 0; now input from fw/re on remote control box
    T6 = 0;
//    MotPtr[0]->set_mode (REGENBRAKE);
    MotorA.set_mode (REGENBRAKE);
    MotorB.set_mode (REGENBRAKE);
    setVI  (0.9, 0.5);

    Servos[0] = Servos[1] = NULL;
    //  NOTE The ONLY way to get both servos working properly is to NOT use any if (bla) Servo ervo1(PB_8);
    //  Only works with unconditional inline code
    //  However, setup code for Input by default,
    //  This can be used to monitor Servo output also !
    Servo   Servo1  (PB_8)  ;
    Servos[0] = & Servo1;
    Servo   Servo2  (PB_9)  ;
    Servos[1] = & Servo2;
    
//    pc.printf   ("last_temp_count = %d\r\n", last_temp_count);  //  Has had time to do at least 1 conversion
    if  ((last_temp_count > 160) && (last_temp_count < 2400))   //  in range -40 to +100 degree C
        temp_sensor_exists  = true;
/*
    //  Setup Complete ! Can now start main control forever loop.
    //  March 16th 2018 thoughts !!!
    //  Control from one of several sources and types as selected in options bytes in eeprom.
    //  Control could be from e.g. Pot, Com2, Servos etc.
    //  Suggest e.g.
*/    /*
    switch  (mode_byte) {   //  executed once only upon startup
        case    POT:
            while   (1) {   //  forever loop
                call    common_stuff    ();
                ...
            }
            break;
        case    COM2:
            while   (1) {   //  forever loop
                call    common_stuff    ();
                ...
            }
            break;
    }
    */
//    pc.printf   ("Ready to go!, wheel gear in position %d\r\n", WHEELGEAR);
    pc.printf   ("About to start!\r\n");
    while   (1) {      //  Loop forever, repeats synchroised by waiting for ticker Interrupt Service Routine to set 'loop_flag' true
        while   (!loop_flag)  {         //  Most of the time is spent in this loop, repeatedly re-checking for commands from pc port
            command_line_interpreter_pc    ()  ;   //  Proceed beyond here once loop_timer ticker ISR has set loop_flag true
            command_line_interpreter_loco    ()  ;   //  Proceed beyond here once loop_timer ticker ISR has set loop_flag true
            AtoD_reader ();                     //  Performs A to D conversions at rate set by ticker interrupts
        }
        loop_flag = false;              //  Clear flag set by ticker interrupt handler
        MotorA.pulses_per_sec   ();   //  Needed to keep table updated to give reading in Hall transitions per second
        MotorB.pulses_per_sec   ();   //  Read MotorX.PPS to read pulses per sec or MotorX.RPM to read motor RPM
        T4 = !T4;   //  toggle to hang scope on to verify loop execution
        //  do stuff
        if  (flag_8Hz)  {   //  do slower stuff
            flag_8Hz    = false;
            LED = !LED;                   // Toggle LED on board, should be seen to fast flash
            WatchDog--;
            if  (WatchDog == 0) {   //  Deal with WatchDog timer timeout here
                setVI  (0.0, 0.0);  //  set motor volts and amps to zero
                com2.printf ("TIMEOUT %2x\r\n", (I_Am() & 0x0f));   //  Potential problem of multiple units reporting at same time overcome by adding board number to WATCHDOG_RELOAD
            }                       //  End of dealing with WatchDog timer timeout
            if  (WatchDog < 0)
                WatchDog = 0;
            eighth_sec_count++;
            if  (eighth_sec_count > 6)    {   //  Send some status info out of serial port every second and a bit or thereabouts
                eighth_sec_count = 0;
                MotorA.current_calc ();     //  Updates readings in MotorA.I.min, MotorA.I.ave and MotorA.I.max
                MotorB.current_calc ();
/*                if  (temp_sensor_exists)    {
                    double  tmprt = (double) last_temp_count;
                    tmprt /= 16.0;
                    tmprt -= 50.0;
                    pc.printf   ("Temp %.2f\r\n", tmprt);
                }*/
//                com2.printf   ("V=%+.2f, Pot=%+.2f, HA %d, HB %d, IAmin %d, IAave %d, IAmax %d, IB %d, Arpm %d, Brpm %d\r\n", Read_BatteryVolts(), Read_DriverPot(), MotorA.read_Halls  (), MotorB.read_Halls  (), MotorA.I.min, MotorA.I.ave, MotorA.I.max, MotorB.I.ave, (Apps * 60) / 24, (Bpps * 60) / 24);
            }
        }   //  End of if(flag_8Hz)
    }       //  End of main programme loop
}