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:
2020-08-16
Revision:
17:cc9b854295d6
Parent:
16:d1e4b9ad3b8b

File content as of revision 17:cc9b854295d6:

/*
    STM3_ESC    Electronic Speed Controller board, drives Two Brushless Motors, full Four Quadrant Control.
    Jon Freeman  B. Eng Hons
    2015 - 2020
*/
#include "mbed.h"
#include "STM3_ESC.h"
#include "BufferedSerial.h"
#include "FastPWM.h"
#include "Servo.h"
#include "brushless_motor.h"
#include "Radio_Control_In.h"
#include "LM75B.h"              //  New I2C temp sensor code March 2020 (to suit possible next board issue, harmless otherwise)
//#ifdef  TARGET_NUCLEO_F401RE    //    Target is TARGET_NUCLEO_F401RE for all boards produced.
//#endif
/*
Brushless_STM3_ESC board
Apr 2020    *   RC tested on 'Idle Halt' branch line - all good - also tested Inverter Gen power sorce. All good.
Dec 2019    *   Radio control inputs now fully implemented, requires fitting tiny 'RC_in_fix' board.
Jan 2019    *   WatchDog implemented. Default is disabled, 'kd' command from TS controller enables and reloads
            *   Tidied brushless_motor class, parameter passing now done properly
            *   class   RControl_In created. Inputs now routed to new pins, can now use two chans both class   RControl_In and Servo out
                (buggery board required for new inputs on June 2018 issue boards)
            *   Added version string
            *   Error handler written and included
            *   Realised Nanotec motors are 6 pole, all others are 8 pole. Modified 'mode' to include 'motor poles' in EEPROM data, now speed reading correct for all
            *   Reorganised EEPROM data - mode setting now much easier, less error prone
            *   Maximum speed now one EEPROM option, range 1.0 to 25.0 MPH in 0.1 MPH steps
                
New 29th May 2018 **
                LMT01 temperature sensor routed to T1 - and rerouted to PC_13 as InterruptIn on T1 (ports A and B I think) not workable
        March 2019 temp sensor only included with TEMP_SENSOR_ENABLE defined. Temp reading not essential, LMT01 was not a good choice due to
        significant loading of interrupts, threatening integrity of Real Time System
        *** New sensor code for LM75B temp sensor added March 2020 ***
*/


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

AnalogIn to read each motor current

____________________________________________________________________________________
        CONTROL PHILOSOPHY
This STM3_ESC Dual Brushless Motor 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_L476RG)  //  CPU in 64 pin LQFP  ** NOT PROVED ** No good, pinmap different
//#include    "F401RE.h"
//#endif
#if defined (TARGET_NUCLEO_F401RE)  //  CPU in 64 pin LQFP  -   This is what all produced boards have
#include    "F401RE.h"
#endif
#if defined (TARGET_NUCLEO_F411RE)  //  CPU in 64 pin LQFP  -   Never tried, but probably would work as is
#include    "F411RE.h"
#endif
#if defined (TARGET_NUCLEO_F446ZE)  //  CPU in 144 pin LQFP
#include    "F446ZE.h"              //  A thought for future version
#endif
/*  Global variable declarations */

//volatile    uint32_t    fast_sys_timer      = 0;    //  gets incremented by our Ticker ISR every VOLTAGE_READ_INTERVAL_US
uint32_t    WatchDog            = 0,    //  Set this up in main once pre-flight checks done. Allow extra few seconds at powerup
            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, 31250us at Sept 2019
            AtoD_Semaphore      = 0;

bool        WatchDogEnable  = false;    //  Must recieve explicit instruction from pc or controller to enable
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; //  March 2020 - Now used to indicate presence or not of LM75B i2c temp sensor

/*
    *   Not used since LMT01 proved not a good choice. See LM75B code added March 2020
    *
#ifdef  TEMP_SENSOR_ENABLE
uint32_t    temp_sensor_count = 0,  //  incremented by every rising edge from LMT01
            last_temperature_count = 0;  //  global updated approx every 100ms after each LMT01 conversion completes
Ticker  temperature_find_ticker;
Timer   temperature_timer;
#endif
*/
/*  End of Global variable declarations */

Ticker  tick_vread;     //  Device to cause periodic interrupts, used to time voltage readings etc - 50 us
Ticker  loop_timer;     //  Device to cause periodic interrupts, used to sync iterations of main programme loop

eeprom_settings             user_settings    (SDA_PIN, SCL_PIN)  ;   //  This MUST come before Motors setup
RControl_In                 RC_chan_1   (PC_14);
RControl_In                 RC_chan_2   (PC_15);   //  Instantiate two radio control input channels and specify which InterruptIn pin
error_handling_Jan_2019     ESC_Error    ;         //  Provides array usable to store error codes.

//PCT2075 temp_sensor( i2c );    //  or LM75B temp_sensor( p?, p? );  Added March 2020
PCT2075 temp_sensor( SDA_PIN, SCL_PIN );    //  or LM75B temp_sensor( p?, p? );  Added March 2020


/*
    5   1   3   2   6   4  Brushless Motor Hall 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

Dec 2019 Support for DC motors deleted.
Hall codes 0 and 7 not used for brushless motors. Without Hall sensors connected pullup resistors give code 7.

*/
const   uint16_t    A_tabl[] = {  //  Table of motor energisation bit patterns. Rows are Handbrake, Forward, Reverse, Regen brake. Cols relate to Hall sensor outputs
//        AUHVL|AWL,  AWHUL|AVL,  AWHVL|AUH,  AVHWL|AUL,  AUHWL|AVH,  AVHUL|AWH
    0,  AUHVL|AWL,  AWHUL|AVL,  AWHVL|AUH,  AVHWL|AUL,  AUHWL|AVH,  AVHUL|AWH,    0,  //  Handbrake
//      1->3        2->6        3->2        4->5        5->1        6->4
    0,  AWHVL,      AVHUL,      AWHUL,      AUHWL,      AUHVL,      AVHWL,  0,  //  Forward 0, WV1, VU1, WU1, UW1, UV1, VW1, 0,
//      1->5        2->3        3->1        4->6        5->4        6->2
    0,  AVHWL,      AUHVL,      AUHWL,      AWHUL,      AVHUL,      AWHVL,  0,  //  Reverse 0, VW1, UV1, UW1, WU1, VU1, WV1, 0,
    0,  BRA,        BRA,        BRA,        BRA,        BRA,        BRA,    BRA,   //  Regenerative Braking
    KEEP_L_MASK_A, KEEP_H_MASK_A   //  [32 and 33]
}   ;
//        AUHVL|AWL,  AWHUL|AVL,  AWHVL|AUH,  AVHWL|AUL,  AUHWL|AVH,  AVHUL|AWH
const   uint16_t    B_tabl[] = {    //  Different tables for Motors A and B as different ports and different port bits used.
//    0,    0,      0,      0,      0,      0,      0,    0,  //  Handbrake
    0,  BUHVL|BWL,  BWHUL|BVL,  BWHVL|BUH,  BVHWL|BUL,  BUHWL|BVH,  BVHUL|BWH,    0,  //  Handbrake
    0,  BWHVL,      BVHUL,      BWHUL,      BUHWL,      BUHVL,      BVHWL,    0,  //  Forward 0, WV1, VU1, WU1, UW1, UV1, VW1, 0,
    0,  BVHWL,      BUHVL,      BUHWL,      BWHUL,      BVHUL,      BWHVL,  0,  //  Reverse 0, VW1, UV1, UW1, WU1, VU1, WV1, 0,
    0,  BRB,        BRB,        BRB,        BRB,        BRB,        BRB,    BRB,   //  Regenerative Braking
    KEEP_L_MASK_B, KEEP_H_MASK_B
}   ;

brushless_motor   MotorA  (MOT_A_I_ADC, APWMV, APWMI, A_tabl, _MAH1, _MAH2, _MAH3, PortA, PORT_A_MASK, ISHUNTA);
brushless_motor   MotorB  (MOT_B_I_ADC, BPWMV, BPWMI, B_tabl, _MBH1, _MBH2, _MBH3, PortB, PORT_B_MASK, ISHUNTB);    //  Two brushless motor instantiations

extern  cli_2019    pcc, tsc;   //  command line interpreters from pc and touch screen

static const int    LM75_I2C_ADDR = 0x90;   //  i2c temperature sensor

//  Interrupt Service Routines
/*
#ifdef  TEMP_SENSOR_ENABLE
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_temperature_count = temp_sensor_count;
        temp_sensor_count = 0;
        readflag = true;
    }
    if  (t == 6)
        readflag = false;
}

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
}
#endif
*/

/** 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 (32Hz)
{
    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
}

//  End of Interrupt Service Routines

const char *    get_version    ()  {
    return  "1.0.y2020.m05.d21\0";  //  Version string, readable using 'ver' serial command
}

/*bool    read_temperature    (float & t) {
//    pc.printf   ("test param temp = %7.3f\r\n", t);
    if  (!temp_sensor_exists)
        return  false;
    t = temp_sensor;
    return  true;
}*/

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

void    setVI_B   (double v, double i)
{
    MotorB.set_V_limit  (v);
    MotorB.set_I_limit  (i);
}

void    setVI_both   (double v, double i)
{
    setVI_A (v, i);
    setVI_B (v, 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;
    if  (MotorA.tickleon)
        MotorA.high_side_off    ();
    if  (MotorB.tickleon)
        MotorB.high_side_off    ();
    if  (AtoD_Semaphore > 10)   {
        pc.printf   ("WARNING - sema cnt %d\r\n", AtoD_Semaphore);
        AtoD_Semaphore = 10;
    }
    while   (AtoD_Semaphore > 0)    {   //  semaphore gets incremented in timer interrupt handler, t=50us
        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.sniff_current    (); //  Initiate ADC current reading
                break;
            case    3:
                MotorB.sniff_current    ();
                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 by returning average of most recent two readings
*   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 !
}


double  trim    (const double min, const double max, double input)  {
    if  (input < min)   input = min;
    if  (input > max)   input = max;
    return  input;
}

void    brake_motors_both    (double brake_effort)  {
    MotorA.brake    (brake_effort);
    MotorB.brake    (brake_effort);
}


void    mode_set_motors_both   (int mode)      //  called from cli to set fw, re, rb, hb
{
    MotorA.set_mode (mode);
    MotorB.set_mode (mode);
}


bool    is_it_worth_the_bother    (double a, double b)    {   //  Tests size of change. No point updating tiny demand changes
    double c = a - b;
    if  (c < 0.0)
        c = 0.0 - c;
    if  (c > 0.02)
        return  true;
    return  false;
}

void    hand_control_state_machine  (int source)  {     //  if hand control user_settings '3', get here @ approx 30Hz to read drivers control pot
                                                        //  if servo1 user_settings '4', reads input from servo1 instead
enum    {   //  states used in RC_or_hand_control_state_machine()
        HAND_CONT_IDLE,
        HAND_CONT_BRAKE_WAIT,
        HAND_CONT_BRAKE_POT,
        HAND_CONT_STATE_INTO_BRAKING,
        HAND_CONT_STATE_BRAKING,
        HAND_CONT_STATE_INTO_DRIVING,
        HAND_CONT_STATE_DRIVING
        }  ;

    static  int hand_controller_state = HAND_CONT_IDLE;
//    static  int old_hand_controller_direction = T5;              //  Nov 2018 reworked thanks to feedback from Rob and Quentin
    static  double  brake_effort, drive_effort, pot_position, old_pot_position = 0.0;
    static  int dirbuf[5] = {100,100,100,100,100};      //  Initialised with invalid direction such that 'change' is read from switch
    static  int dirbufptr = 0, direction_old = -1, direction_new = -1;
    const   int buflen = sizeof(dirbuf) / sizeof(int);
    double      User_Brake_Range;  //

   direction_old = direction_new;

//      Test for change in direction switch setting.
//      If whole buffer NEWLY filled with all Fwd or all Rev, state = brake_wait
    int diracc = 0;    
    if  (dirbufptr >= buflen)
        dirbufptr = 0;
    dirbuf[dirbufptr++] = T5;   //  Read direction switch into circular debounce buffer
    for (int i = 0; i < buflen; i++)
        diracc += dirbuf[i];    //  will = 0 or buflen if direction switch stable
    if  (diracc == buflen || diracc == 0)   //  if was all 0 or all 1
        direction_new = diracc / buflen;
    if  (direction_new != direction_old)
        hand_controller_state = HAND_CONT_BRAKE_WAIT;   //  validated change of direction switch

    switch  (source)    {
        case    HAND:  //  driver pot is control input
            pot_position = Read_DriverPot   ();     //  Only place read in the loop, rteads normalised 0.0 to 1.0
            User_Brake_Range = user_settings.user_brake_range();
            break;
        default:
            pot_position = 0.0;
            break;
    }

    switch  (hand_controller_state) {
        case    HAND_CONT_IDLE:         //  Here for first few passes until validated direction switch reading
            break;

        case    HAND_CONT_BRAKE_WAIT:   //  Only get here after direction input changed or newly validated at power on
            pc.printf   ("At HAND_CONT_BRAKE_WAIT\r\n");
            brake_effort = 0.05;    //  Apply braking not too fiercely
            brake_motors_both   (brake_effort);
            hand_controller_state = HAND_CONT_BRAKE_POT;
            break;

        case    HAND_CONT_BRAKE_POT:        //  Only get here after one pass through HAND_CONT_BRAKE_WAIT but
            if  (brake_effort < 0.95)    {   //   remain in this state until driver has turned pott fully anticlockwise
                brake_effort += 0.05;   //  ramp braking up to max over about one sec after direction change or validation
                brake_motors_both    (brake_effort);  //  Direction change or testing at power on
                pc.printf   ("Brake effort %.2f\r\n", brake_effort);
            }
            else    {   //  once braking up to quite hard
                if  (pot_position < 0.02)   {   //  Driver has turned pot fully anticlock
                    hand_controller_state = HAND_CONT_STATE_BRAKING;
                }
            }
            break;

        case    HAND_CONT_STATE_INTO_BRAKING:
            brake_effort = (User_Brake_Range - pot_position) / User_Brake_Range;
            if  (brake_effort < 0.0)
                brake_effort = 0.5;
            brake_motors_both    (brake_effort);
            old_pot_position = pot_position;
            hand_controller_state = HAND_CONT_STATE_BRAKING;
            pc.printf   ("Brake\r\n");
            break;

        case    HAND_CONT_STATE_BRAKING:
            if  (pot_position > User_Brake_Range)    //  escape braking into drive
                hand_controller_state = HAND_CONT_STATE_INTO_DRIVING;
            else    {
                if  (is_it_worth_the_bother(pot_position, old_pot_position))  {
                    old_pot_position = pot_position;
                    brake_effort = (User_Brake_Range - pot_position) / User_Brake_Range;
                    brake_motors_both    (brake_effort);
                }
            }
            break;

        case    HAND_CONT_STATE_INTO_DRIVING:   //  Only get here after HAND_CONT_STATE_BRAKING
            pc.printf   ("Drive\r\n");
            if  (direction_new == 1)
                mode_set_motors_both   (MOTOR_FORWARD);  //  sets both motors
            else
                mode_set_motors_both   (MOTOR_REVERSE);
            hand_controller_state = HAND_CONT_STATE_DRIVING;
            break;

        case    HAND_CONT_STATE_DRIVING:
            if  (pot_position < User_Brake_Range)    //  escape braking into drive
                hand_controller_state = HAND_CONT_STATE_INTO_BRAKING;
            else    {
                if  (is_it_worth_the_bother(pot_position, old_pot_position))  {
                    old_pot_position = pot_position;
                    drive_effort = (pot_position - User_Brake_Range) / (1.0 - User_Brake_Range);
                    setVI_both  (drive_effort, drive_effort);   //  Wind volts and amps up and down together ???
                    pc.printf   ("Drive %.2f\r\n", drive_effort);
                }
            }
            break;

        default:
            pc.printf   ("Unhandled Hand Controller state %d\r\n", hand_controller_state);
            break;
    }       //  endof switch  (hand_controller_state) {
}

void    set_RCIN_offsets    ()  {
    RC_chan_1.set_offset    (user_settings.rd(RCI1_TRIM), user_settings.rd(RCIN_REGBRAKE_RANGE), user_settings.rd(RCIN_STICK_ATTACK));
    RC_chan_2.set_offset    (user_settings.rd(RCI2_TRIM), user_settings.rd(RCIN_REGBRAKE_RANGE), user_settings.rd(RCIN_STICK_ATTACK));
    RC_chan_1.set_chanmode  (user_settings.rd(RCIN1), user_settings.rd(RCIN1REVERSE))  ;
    RC_chan_2.set_chanmode  (user_settings.rd(RCIN2), user_settings.rd(RCIN2REVERSE))  ;
}

void    rcins_report    ()  {
    pc.printf   ("RC1 pulsewidth %d, period %d, pulsecount %d\r\n", RC_chan_1.pulsewidth(), RC_chan_1.period(), RC_chan_1.pulsecount());
    pc.printf   ("RC2 pulsewidth %d, period %d, pulsecount %d\r\n", RC_chan_2.pulsewidth(), RC_chan_2.period(), RC_chan_2.pulsecount());
    pc.printf   ("\r\n");
}

int main()  //  Programme entry point
{
    uint32_t    eighth_sec_count = 0;
    //  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 - 50 us
    loop_timer.attach_us    (&ISR_loop_timer, MAIN_LOOP_REPEAT_TIME_US);    //  Start periodic interrupt generator

    //  Done setting up system interrupt timers

    com3.baud   (1200);     //  Once had an idea to use this for IR comms, never tried
    com2.baud   (19200);    //  Opto isolated serial bus connecting 'n' STM3_ESC boards to 1 Brute Touch Screen controller
//    pc.baud     (9600);     //  COM port to pc
    pc.baud     (user_settings.baud());     //  COM port to pc

//    pc.printf   ("Baud rate %d\r\n", user_settings.baud());

    MotorA.set_direction    (user_settings.rd(MOTADIR));     //  user_settingss all setup in class   eeprom_settings {}  user_settings    ; constructor
    MotorB.set_direction    (user_settings.rd(MOTBDIR));
    MotorA.poles            (user_settings.rd(MOTAPOLES));   //  Returns true if poles 4, 6 or 8. Returns false otherwise
    MotorB.poles            (user_settings.rd(MOTBPOLES));   //  Only two calls are here
//    MotorA.set_mode         (MOTOR_REGENBRAKE);   //  Done in motor constructor
//    MotorB.set_mode         (MOTOR_REGENBRAKE);
//    setVI_both  (0.9, 0.5);              //  Power up with moderate regen braking applied
    set_RCIN_offsets    ();

    class  RC_stick_info   RCstick1, RCstick2;

//    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;
    pc.printf   ("\r\n\nSTM3_ESC Starting Ver %s, Command Source setting = %d\r\n", get_version(), user_settings.rd(COMM_SRC));
    pc.printf   ("Designed by Jon Freeman  B. Eng. Hons - 2017-2020. e jon@jons-workshop.com\r\n");
    if  (user_settings.do_we_have_i2c  (0xa0))
        pc.printf   ("Got EEPROM, i2c error count = %d\r\n", user_settings.errs());
    else
        pc.printf   ("No eeprom found\r\n");
    pc.printf   ("ESC_Error.all_good() ret'd %s\r\n", ESC_Error.all_good() ? "true - Ready to Roll !" : "false");

//bool    eeprom_settings::do_we_have_i2c  (uint32_t x)
    pc.printf   ("LM75 temp sensor ");
    if  (user_settings.do_we_have_i2c  (LM75_I2C_ADDR))    {
        pc.printf   ("reports temperature %7.3f\r\n", (float)temp_sensor );
        temp_sensor_exists  = true;
    }
    else
        pc.printf   ("Not Fitted\r\n");

    uint32_t    old_hand_controller_direction = T5;
    if  (user_settings.rd(COMM_SRC) == 3)  {      //  Read fwd/rev switch 'T5', PA15 on 401
        pc.printf   ("Pot control\r\n");
        if  (T5)
            mode_set_motors_both   (MOTOR_FORWARD);  //  sets both motors
        else
            mode_set_motors_both   (MOTOR_REVERSE);
    }
//    pc.printf   ("SystemCoreClock=%d, MAX_PWM_TICKS=%d\r\n", SystemCoreClock, MAX_PWM_TICKS);

    WatchDog    = WATCHDOG_RELOAD + 80; //  Allow extra few seconds at powerup
    pcc.flush   (); //  Flush serial rx buffers
    tsc.flush   ();
//    pc.printf   ("sizeof int is %d\r\n", sizeof(int));  //  sizeof int is 4
    double      Current_Scaler;  //  New idea - Sept 2019. Plan is to scale down motor current limit when voltage lower than nom.
                                            //  See schematic for full details, but cycle-by-cycle current limit has the effect of allowing larger average I
                                            //  at lower voltages. This is simply because current takes longer to build in motor inductance when voltage
                                            //  is low. Conversely, at high supply voltages, motor current reaches limit quickly, cutting drive, meaning
                                            //  similar current flows for shorter times at higher voltages.

    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
            pcc.core    ()  ;   //  Proceed beyond here once loop_timer ticker ISR has set loop_flag true
            tsc.core    ()  ;   //  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
        }                       //  32Hz original setting for loop repeat rate
        loop_flag = false;              //  Clear flag set by ticker interrupt handler. WHEN LAST CHECKED this was about every 32ms

            //  double  trim    (const double min, const double max, double input)  {
        Current_Scaler = trim  (0.1, 1.0, Read_BatteryVolts() / user_settings.Vnom());
        MotorA.I_scale  (Current_Scaler);   //  Reduces motor current limits when voltage below nominal.
        MotorB.I_scale  (Current_Scaler);   //  This goes some way towards preventing engine stalls perhaps

        MotorA.speed_monitor_and_control   ();   //  Needed to keep table updated to give reading in Hall transitions per second
        MotorB.speed_monitor_and_control   ();   //  Read MotorX.PPS to read pulses per sec or MotorX.RPM to read motor RPM

        RC_chan_1.read(RCstick1);   //  Get latest info from Radio Control inputs
        RC_chan_2.read(RCstick2);

//#define SERVO_OUT_TEST
#ifdef  SERVO_OUT_TEST
        if  (RCstick1.active)   Servo1 = RCstick1.deflection;
        if  (RCstick2.active)   Servo2 = RCstick2.deflection;
#endif

        switch  (user_settings.rd(COMM_SRC))  {   //  Look to selected source of driving command, act on commands from wherever
//            case    0:  //  Invalid
//                break;
//            case    COM1:  //  COM1    -   Primarily for testing, bypassed by command line interpreter
//                break;
            case    COM2:  //  COM2    -   Nothing done here, all serial instructions handled in command line interpreter
                break;
            case    HAND:  //  Put all hand controller input stuff here
                hand_control_state_machine  (HAND);
                break;  //  endof hand controller stuff
            case    RC_IN1:  //  RC_chan_1
                RC_chan_1.energise  (RCstick1, MotorA)  ;   //  RC chan 1 drives both motors
                RC_chan_1.energise  (RCstick1, MotorB)  ;
                break;
            case    RC_IN2:  //  RC_chan_2
                RC_chan_2.energise  (RCstick2, MotorA)  ;   //  RC chan 2 drives both motors
                RC_chan_2.energise  (RCstick2, MotorB)  ;
                break;
            case    RC_IN_BOTH:  //  Robot Mode
                RC_chan_1.energise  (RCstick1, MotorA)  ;   //  Two RC chans drive two motors independently
                RC_chan_2.energise  (RCstick2, MotorB)  ;
                break;
            default:
                if  (ESC_Error.read(FAULT_UNRECOGNISED_STATE))  {
                    pc.printf   ("Unrecognised state %d\r\n", user_settings.rd(COMM_SRC));    //  set error flag instead here
                    ESC_Error.set   (FAULT_UNRECOGNISED_STATE, 1);
                }
                break;
        }   //  endof   switch  (user_settings_bytes[COMM_SRC])  {

//#define SERVO_OUT_TEST
//#ifdef  SERVO_OUT_TEST
//    static double  servo_angle = 0.0;  //  For testing servo outs
        //  servo out test here     December 2018
/*
        servo_angle += 0.01;
        if  (servo_angle > TWOPI)
            servo_angle -= TWOPI;
        Servo1 = ((sin(servo_angle)+1.0) / 2.0);
        Servo2 = ((cos(servo_angle)+1.0) / 2.0);
*/
        //  Yep!    Both servo outs work lovely December 2018
//#endif

        if  (flag_8Hz)  {   //  do slower stuff
            flag_8Hz    = false;
            LED = !LED;                   // Toggle green LED on board, should be seen to fast flash
            if  (WatchDogEnable)    {
                WatchDog--;
                if  (WatchDog < 1) {   //  Deal with WatchDog timer timeout here
                    WatchDog = 0;
                    setVI_both  (0.0, 0.0);  //  set motor volts and amps to zero
                    pc.printf ("TIMEOUT %c\r\n", user_settings.rd(BOARD_ID));   //  Brute touch screen controller can do nothing with this
                }                       //  End of dealing with WatchDog timer timeout
            }
            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;
                ESC_Error.report_any (false);   //  retain = false - reset error after reporting once
            }
        }   //  End of if(flag_8Hz)
    }       //  End of main programme loop
}