YAAP (Yet Another Arduino Art Project)

by stevensarns in Circuits > Art

41 Views, 1 Favorites, 0 Comments

YAAP (Yet Another Arduino Art Project)

Art in action

Wife bought a garden art thing. Waited. Nothing happened. Added a few magnets, an Arduino and viola! The result is quite mesmerizing.  

Supplies

A permanent magnet is attached to the bottom of each pendulum. An electromagnet coil is mounted beneath the hanging magnet. When the coil is energized the pendulum is repelled. A signal is induced in the coil as the magnet approaches and passes by the coil. This signal is analyzed by the microcontroller and as the magnet passes the coil, the coil is briefly energized providing a kick to sustain the swing.

The Art

AxleRetainingScrews.JPG

The Art

The stand is 28” high 15” deep and the top of the pendulum is 54” measured from the ground. If you search on Google images for “garden art pendulum” you will see several sources. This one is representative: https://www.arusticgarden.com/small-pendulum.html. It weighs about 30 pounds and I think its made in Mexico. I added 10 screws to the top rails of the base unit to keep the pendulum axles from wandering around (see photo).

The Magnets

MagnetMount.JPG

The magnets

Each magnet is 20mm long x 10mm diameter, mounted on wooden carrier glued (“Household Goop”) to bottom of each pendulum. Drill 10mm hole and press magnet into place. Glue if needed. I adjusted the position of the magnets so that a gap of about 2 to 3 mm between the coils and the magnets. Search Amazon for “10x20mm magnet”. The prices on Ali Express are no better.

The Power Supply

The power supply

I had chosen an Atari power supply from my junk box that was labeled “11.5V at 1.95A”. The unloaded output voltage was 14.5 Volts. I had expected better regulation. It was only after completing the project that I was able to determine the actual power requirements. Each pendulum requires roughly 1 amp of current for 5 mS every 2 seconds on the average when running. However, the power supply must be capable of supplying 1 amp during the time the coil is energized which can be as long as 200 mS during startup. During development I experimented with power supply voltages and found that the pendulums could be sustained with a 5 Volt supply, but I did not test startup at low voltages. The upper voltage limit is bounded by the AMS1117 which is rated at 15 Volts. The circuit board provides 3 diode drops so the input voltage could be as high as 17V (risky).


The Electromagnets

CoilMount.JPG

The electromagnets

The coils are wound on a soft iron core 3” long and 0.5” in diameter, an iron axle left over from a robot project. I drilled and tapped a hole in one end of the core for a 6-32 screw. My design criteria for the coil was simply to limit the current to 1 Ampere based on a 12 Volt supply which means 12 Ohm coils. I had a spool of 32 gauge wire so I calculated about 600 turns should do the trick. I mounted a wooden rack below the pendulums with the coils mounted to the wooden cross pieces (see photo). If you are interested in the design tradeoffs, here is an electromagnet force calculator: https://www.daycounter.com/Calculators/Magnets/Solenoid-Force-Calculator.phtml

Circuit Design

Schematic.JPG
SigLevelShiftedAndAmplifiedSig.bmp
SigAndBlank.bmp
SigWithBlankFromAnotherCoil.bmp

Circuit Design

The circuit is divided into 3 major parts; power supplies, coil control and analog signal processing. The power supplies consist of a diode isolated capacitor that supplies the current for the coils. A separate power supply consists of 3 1N4001 diodes in series (~2 Volt drop) into an AMS1117 5.0 Volt regulator and then into an MIC3565 3.3 Volt regulator that supplies the analog signal amplifiers.


Each coil is controlled by an AO3400 MOSFET. These little SOT-23 beauties are rated at 30V, 5.8A and typical 25 milliOhm resistance. The signal of interest is generated at the drain of the MOSFET when the MOSFET is off, consequently the drain is at 12 Volts. A 47 uF capacitor links the digital portion of the circuit to the analog amplifier and shifts the signal to ground level.


The analog signal is slightly over 0.5 Volts when the magnet passes the coil. However, during certain transient conditions this signal can be as much as 12V (toasted several Pills, op amps and MOSFETs in the process of debugging that one!), so it is necessary to clamp the signal with 1N4148 small signal diodes to ground limiting the signal to 0.7 Volts. Additional 1N5819 Schottky diodes clamp the negative going signal. In addition to the diode clamping, the Blue Pill also controls another MOSFET that clamps the signal to ground immediately after the controlling coil is energized and during a brief period when the other two coils are switched off. The signal is filtered, amplified by 10 with the MCP6001 rail to rail op amp and sent to the Blue Pill’s analog to digital converter. All of the components mentioned can be obtained from Ali Express.


The first scope trace shows the signal after level shifting to ground and the amplified signal. The second scope picture shows the signal (blue) and the blanking signal (yellow). Note that the blanking signal clamps the signal to ground. The third scope capture shows the activity of a different coil being turned off which would normally create a large transient on this signal, but the event is blanked by the brief clamping signal (yellow).


The PWB

The PWB

I use DipTrace for schematic capture and pwb layout. This is a free layout package for non-commercial use. The files are attached. If you do not need to modify the files, the Gerber file is also attached. I use JLCPCB.com as my pwb supplier. The analog and digital circuits are separated from each other as are the digital and analog grounds. A jumper wire connects AGND to DGND. This “single point ground” concept reduces digital noise in the analog section.

oops - "file type not supported" - cannot attach pwb layout and gerber files - contact me directly.

The Microcontroller

The Microcontroller

This project is based on an STM32F103 based “Blue Pill”. I chose the Blue Pill because, like some of my best friends, they are cheap and fast. Gone are the days when you could purchase the Blue Pill from Ali Express for less than $1 including shipping, but they are still the lowest cost controller available in the Arduino battlespace. The development environment is not as refined as the Atmel products nor is the user base as widespread. However, after working through the “gotchas”, it does perform reliably. I originally prototyped the project with an Arduino Uno which worked fine as long as the pendulums were swinging, but the Uno had problems when it came to startup – just was not able to process the signals fast enough. The Blue Pill runs at 72 MHz and the 12 bit analog to digital converter converts in just over 1 microsecond.


In addition, the timer architecture is ideally suited to the needs of this project. The STM32F103 has six 16 bit timers with six 16 bit prescalers. Each timer has 4 “channels” that can be individually set to generate an interrupt. So I can dedicate a timer to each pendulum. Channel 1 of the timer will generate an interrupt when it is time to turn off the coil power MOSFET. Channel 2 will generate an interrupt when it is time to turn off the signal blanking MOSFET. Channel 3 will detect if the pendulum has stopped swinging. A fourth timer generates a 1 millisecond timebase to control additional transient blanking.

The IDE

The IDE

I use the Dan Drown (http://dan.drown.org/stm32duino/package_STM32duino_index.json) board definition library. The timer documentation is best described at http://static.leaflabs.com/pub/leaflabs/maple-docs/0.0.12/lang/api/hardwaretimer.html. Set the following variables in the TOOLS tab;

"Generic STM32F103C series", upload method=serial, optimise=smallest (uses 30% flash). I tried “fastest” which uses 41% flash, but could not detect enough speedup to justify deviating from the default setting.


Attach a serial to USB converter to the TX, RX and ground terminals. Note – connecting +5V on the board to the +5 volt USB serial converter will connect the +5 from your PC to the +5 from this board’s power supply. Maybe ok, but not recommended. Put jumper “BOOT0” in the “1” position, press the reset button and you are ready to download your code. Invoke the IDE monitor and Serial.print() statements appear in the monitor. When you are finished, place the BOOT0 jumper in the “0” position and the system will start running the code in flash upon application of power.

The Software

The Software

The software is divided into several main components; a state machine, signal processing, coil control, blanking (clamping) control, timers and timer interrupt service routines.


A state machine controls the actions of each individual pendulum in one of three states; STARTUP, BOOST and STABLE. At startup, the magnet at the bottom of the pendulum has been attracted to the soft iron core of the coil. In this “captured” condition, the pendulum vibrates at a much higher frequency than normal, typically ten times faster than the normal “swinging” frequency. At startup, the timer channel 3 interrupt service routine generates a coil control signal that is near the captured pendulum’s fundamental frequency plus a random “dither”, which, at some point, will kick the pendulum free of the magnet. When the pendulum is freed from the magnet below a large signal is developed indicating that the pendulum has escaped capture. When this signal is detected, the state is changed to the BOOST state and the coil is energized with a large, constant impulse as the magnet recedes from the detection coil. Finally, after boosting for a fixed number of kicks, the pendulum assumes the STABLE state where the coil power is a function of the error of the desired period minus the actual period using a PID closed loop control algorithm based on only the “P” term.


In the STABLE state, the signal is monitored, when a sharp rise followed by a sharp fall is detected, the coil is energized for an amount of time that is calculated to keep the pendulum swinging at the prescribed period (closed loop control). Each time the signal is detected, the coil control routine reprograms the timer corresponding to the pendulum to generate 3 interrupts; timer channel 1 will turn the coil control MOSFET off and briefly blank the other 2 signals during the coil turn-off transient, timer channel 2 will turn the signal blanking off just before the next magnet encounter is expected and timer channel 3 will detect that the pendulum has stopped swinging.


A simple terminal based information screen is presented. The user can control which pendulum is reported and set the desired period for that pendulum (which controls the displacement of the pendulum). The report automatically switches from pendulum to pendulum during startup depending on which pendulum is being started. Also, note that the LED flashes only when the particular pendulum being reported on signal is detected.

// Triple Pendulum controller, STM32F103C6T8 "BluePill"
// IDE Tools setup: "Generic STM32F103C series", upload method=serial, optimise=fastest (uses 43% flash)
// Additional Boards Managers URLs: http://dan.drown.org/stm32duino/package_STM32duino_index.json


// 0819 change state names
// 0820 led, restructure main loop
// 0821 big test pwb, change pin numbers
// 0822 working, but analog signals messed up - feedthrough
// 0823 add aux blank
// 0824 revert - timer1 ch2 either kills print or does not get evoked
// 0825 abandon timer1 ch2, use timer4 instead
// 0828 all working
// 0830 P3 startup
// 0831 rethink boost calculation = constant in STABLE state
// 0832 STABLE state: min constrain = sustain value
// 0833 P1 startup, two cases; captured by magnet or not, difficult if not, eval STARTUP exit criteria
// 0834 work on dead state
// 0840 new state names, new dead state algorithm
// 0842 working
// 0843 reset boost counter BOOST->STABLE
// 0844 P1 start conditions, PxperiodCmd is being reset to zero


//                      +-----------+
// scope        T1BKIN  |PB12   GND |
//              T1C1N   |PB13   GND |
//              T1C2N   |PB14   V3V |
//              T1C3N   |PB15   RST |                   
//              T1C1    |PA8    PB11| RX3   SDA2        
// IDE      TX  T1C2    |PA9    PB10| TX3   SCL2         
// IDE      RX  T1C3    |PA10   PB1 | ADC9        T3C4  
//         USB- T1C4    |PA11   PB0 | ADC8        T3C3  scope
//         USB+ T1ETR   |PA12   PA7 | ADC7  MOSI1 T3C2  led
//              T2C1E   |PA15   PA6 | ADC6  MISO1 T3C1    
//              T2C2    |PB3    PA5 | ADC5  SCK1         
// P1 blank     T3C1    |PB4    PA4 | ADC4  NSS1        P3 signal
// P2 blank     T2C2    |PB5    PA3 | ADC3  RX2   T2C4  
// P3 blank     T4C1    |PB6    PA2 | ADC2  TX2   T2C3  
// P3 gate      T4C2    |PB7    PA1 | ADC1        T2C2  P2 signal 
// P2 gate SCL1 T4C3    |PB8    PA0 | ADC0  WKUP  T2C1E P1 signal
// P1 gate SDA1 T4C4    |PB9   PC15 | OSC32             
//              +5      |5V    PC14 | OSC32             
//              GND     |GND   PC13 | LED                
//              3V3     |3V3   VBAT |
//                      +-----------+


// ------------------------- STM32F103C6 ----------------------
// ints are 4 bytes, +/-2,147,483,648
// STM32F pullup/down R ~40k Ohms
// Fault Tolerant; PA8-15, PB2-4, 6-15, PC6-9, 10-12
// Not Fault Tolerant; PA0-7, PB0-1, 5, PC0-5, 13-15


// ------------------------ Coil data --------------------------
// coil is ~20 mH, saturates in 7 mS @ boost level=15
// ~12 Ohms (32ga wire), 6 Ohms (28ga wire)
// .5" soft iron core, 3" long, ~600 turns


HardwareTimer pirTimer(1);  // timer1, name=pirTimer, use channel 1
HardwareTimer P2timer(2);   // timer2, name=P2timer, using channels 1,2,3
HardwareTimer P3timer(3);   // timer3, name=P3timer, using channels 1,2,3
HardwareTimer P4timer(4);   // timer4, name=P4timer, using channels 1,2,3


// ------------------------- Pins ------------------------------------
#define P1signalPin PA0     // P1 signal, analog
#define P2signalPin PA1     // P2 signal, analog
#define P3signalPin PA4     // P3 signal, analog
#define P1blankPin  PB4     // P1 MOSFET blanking clamp
#define P2blankPin  PB5     // P2 MOSFET blanking clamp
#define P3blankPin  PB6     // P3 MOSFET blanking clamp
#define P1gatePin   PB9     // P1 MOSFET coil power
#define P2gatePin   PB8     // P2 MOSFET coil power
#define P3gatePin   PB7     // P3 MOSFET coil power
#define ledPin      PA7     // led, active high
#define scopePin    PB0     // debug


// ------------------------------- Variables ---------------------------------------------
byte        report;         // which P to report on
byte        lineCounter;    // renew heading title line counter
uint32_t    loops;          // show main loops/second execution rate
uint16_t    seconds;        // seconds counter, very long time
char const  TAB     = 9;    // tab
byte const  STARTUP = 0;    // all pendulums start in STARTUP state
byte const  BOOST   = 1;    // BOOSTing, either being kicked by STARTUP isr or BOOST case
byte const  STABLE  = 2;    // STABLE, kicked by STABLE case
byte const  timex   = 50;   // supplimental blank transcient suppression, 1mS ticks


byte        P1boostCount;   // count after successfully exiting STARTUP state
byte        P1state;        // state of P1
byte        P1startCount;   // not used
uint16_t    P1periodCmd;    // desired period
uint16_t    P1periodAct;    // actual period
uint16_t    P1signal;       // signal from magnet approaching coil
uint16_t    P1sigMax;       // used to detect approaching magnet
uint16_t    P1countA;       // half period
uint16_t    P1countB;       // other half period
uint16_t    P1boost;        // amount of energization to deliver to coil
uint16_t    P1blank;        // time to clamp P1 signal to ground, greater than boost
int16_t     P1periodErr;    // signed, command - actual
boolean     P1flip;         // flipflop for period measurement
boolean     P1blankFinished;// signal clamp released
boolean     P1blankx;       // brief blank active when another channel releases
boolean     P1osc;          // flag set by isr channel 3 to excite P1 at fundamental freq
float       P1Kp=1;         // proportional term of PID control loop


byte        P2boostCount;   // count after successfully exiting STARTUP state
byte        P2state;        // state of P2
byte        P2startCount;   // not used
uint16_t    P2periodCmd;    // desired period
uint16_t    P2periodAct;    // actual period
uint16_t    P2signal;       // signal from magnet approaching coil
uint16_t    P2sigMax;       // used to detect approaching magnet
uint16_t    P2countA;       // half period
uint16_t    P2countB;       // other half period
uint16_t    P2boost;        // amount of mag energization to deliver to coil
uint16_t    P2blank;        // time to clamp P1 signal to ground, greater than boost
int16_t     P2periodErr;    // signed, command - actual
uint16_t    P2sigAveSave;   // save for report
boolean     P2flip;         // flipflop for period measurement
boolean     P2magDetect;    // mag signal detected flag
boolean     P2blankFinished;// signal clamp released
boolean     P2blankx;       // brief blank active when another channel releases
boolean     P2osc;          //
float       P2Kp=.5;        // proportional term of PID control loop


byte        P3boostCount;   // count after successfully exiting STARTUP state
byte        P3state;        // state of P3
byte        P3startCount;   //
uint16_t    P3periodCmd;    // desired period
uint16_t    P3periodAct;    // actual period
uint16_t    P3signal;       // signal from magnet approaching coil
uint16_t    P3sigMax;       //
uint16_t    P3countA;       // half period
uint16_t    P3countB;       // other half period
uint16_t    P3boost;        // amount of mag energization to deliver to coil
uint16_t    P3blank;        // time to clamp P1 signal to ground, greater than boost
int16_t     P3periodErr;    // signed, command - actual
uint16_t    P3sigAveSave;   // save for report
boolean     P3flip;         // flipflop for period measurement
boolean     P3magDetect;    // mag signal detected flag
boolean     P3blankFinished;// signal clamp released
boolean     P3blankx;       // brief blank active when another channel releases
boolean     P3osc;
float       P3Kp=1;         // proportional term of PID control loop


uint16_t volatile P1timex;  // 3 timers control blanking of signal artifact generated
uint16_t volatile P2timex;  // when a coil is switched off.  Timers generate blanking on
uint16_t volatile P3timex;  // other coil signals for brief period (~50ms).
// -------------------------- Timer1 ISR --------------------------------
void pirIsr(void) {     // interrupt is called 1000x/sec
  if(P1timex) {         // decrement timer until=0
    P1timex--;          // if signal normal blanking is not active release clamp
    if(!P1timex && P1blankFinished) digitalWrite(P1blankPin, LOW);// release signal clamp
  }
  if(P2timex) {
    P2timex--;
    if(!P2timex && P2blankFinished) digitalWrite(P2blankPin, LOW);
  }
  if(P3timex) {
    P3timex--;
    if(!P3timex && P3blankFinished) digitalWrite(P3blankPin, LOW);
  }
}


// ------------------------- Timer4 ISRs --------------------------------
// Timer 1 channel 2 interacts with print, so must use timer 4
// Timer 4 is associated with Pendulum 1, Timer2->P2, Timer3->P3
// Channel 1 turns off coil MOSFET gate
// Channel 2 turns off signal blanking clamp MOSFET
// Channel 3 detects no activity -> issues kick to start moving
void P4chan1isr() {                 // coil off->induces spike on other analog channels
  digitalWrite(P1gatePin,LOW);      // turn off coil
  digitalWrite(ledPin,  LOW);       // turn off led
  digitalWrite(P2blankPin, HIGH);   // inactive channels clamped
  digitalWrite(P3blankPin, HIGH);   // inactive channels clamped
  P1timex = 0;                      // not this channel
  P2timex = timex;                  // start blanking, short time
  P3timex = timex;                  // start blanking
}


void P4chan2isr() {                 // channel 2 turns off signal blanking clamp P1
  digitalWrite(P1blankPin,LOW);     // turn off blanking clamp
  P1blankFinished = true;           // set flag -> blank finished
}


void P4chan3isr() {                 // channel 3 generates startup waveform then detects dead P1  
  P1startCount = 0;                 // not used
  P1state = STARTUP;                // P1 must be dead if this isr executes
  report = 1;
  lineCounter = 0;
  P1osc = true;
}


// ----------------------- Timer2 ISRs -----------------------------------
void P2chan1isr() {
  digitalWrite(P2gatePin,LOW);      // kick off
  digitalWrite(ledPin,  LOW);       // turn off led
  digitalWrite(P1blankPin, HIGH);
  digitalWrite(P3blankPin, HIGH);
  P2timex = 0;
  P1timex = timex;
  P3timex = timex;
}


void P2chan2isr() {
  digitalWrite(P2blankPin,LOW);     // blank off
  P2blankFinished = true;
}


void P2chan3isr() {                 // 
  P2startCount = 0;
  P2state = STARTUP;                // must clamp for 1.5sec after pulse finish
  report = 2;
  lineCounter = 0;
  P2osc = true;
}


// -------------------------- Timer3 ISRs --------------------------------
void P3chan1isr() {
  digitalWrite(P3gatePin,LOW);  // kick off
  digitalWrite(ledPin,  LOW);  // turn off led
  digitalWrite(P2blankPin, HIGH);
  digitalWrite(P1blankPin, HIGH);
  P3timex = 0;
  P2timex = timex;
  P1timex = timex;
}


void P3chan2isr() {
  digitalWrite(P3blankPin,LOW); // blank off
  P3blankFinished = true;
}


void P3chan3isr() {
  P3startCount = 0;
  P3state = STARTUP;
  report = 3;
  lineCounter = 0;
  P3osc = true;
}


//------------------------------- Setup ---------------------------------
void setup() {
  disableDebugPorts();              // required to access PB3, PB4(!), PA13, PA14 & PA15
  pinMode(P1signalPin,INPUT_ANALOG);// signal detection circuit 
  pinMode(P2signalPin,INPUT_ANALOG);// 
  pinMode(P3signalPin,INPUT_ANALOG);// 
  pinMode(P1gatePin,  OUTPUT);      // coil MOSFET gate pin
  pinMode(P2gatePin,  OUTPUT);
  pinMode(P3gatePin,  OUTPUT);
  pinMode(P1blankPin, OUTPUT);      // signal blanking MOSFET gate pin
  pinMode(P2blankPin, OUTPUT);
  pinMode(P3blankPin, OUTPUT);
  pinMode(ledPin,     OUTPUT);
  pinMode(scopePin,   OUTPUT);      // debug
  digitalWrite(ledPin,   HIGH);     // led on
  digitalWrite(P1gatePin, LOW);     // turn off P1 coil
  digitalWrite(P2gatePin, LOW);
  digitalWrite(P3gatePin, LOW);
  digitalWrite(P1blankPin,LOW);     // P1 blanking off, allow P1 signal
  digitalWrite(P2blankPin,LOW);
  digitalWrite(P3blankPin,LOW);
  Serial.begin(9600);   // 9600 Baud to Arduino IDE monitor


  // -------------------- Timer1, 1 mS timebase -----------------------
  pirTimer.pause();                           // pause timer4
  pirTimer.setPrescaleFactor(72);             // set timer 4 prescaler
  pirTimer.setOverflow(1000);                 // period
  pirTimer.setMode(1,TIMER_OUTPUT_COMPARE);   // channel 1, generate interrupt on compare
  pirTimer.setCompare(1,1);                   // interrupt at start
  pirTimer.attachInterrupt(1,pirIsr);         // attach interrupt
  pirTimer.refresh();                         // scope confirm 1mS timebase
  pirTimer.resume();                          // continue
  
// -------------------- Timer4, P1 timer -----------------------
  P4timer.pause();                            // pause timer4
  P4timer.setPrescaleFactor(36000);           // 72MHz/36000=2kHz, .5mS/tick
  P4timer.setOverflow(65535);                 // period, 32.768 sec rollover
  P4timer.refresh();                          // refresh
  P4timer.resume();                           // continue


// -------------------- timer2, P2 timer -----------------------
  P2timer.pause();                            // pause timer2
  P2timer.setPrescaleFactor(36000);           // set timer 2 prescaler, .5mS/tick
  P2timer.setOverflow(65535);                  // period, 32 sec
  P2timer.refresh();                          // refresh
  P2timer.resume();                           // continue
  
// -------------------- Timer3, P3 timer -------------------------
  P3timer.pause();                            // pause timer3
  P3timer.setPrescaleFactor(36000);           // set timer 3 prescaler
  P3timer.setOverflow(65535);                  // period, 32 sec
  P3timer.refresh();                          // refresh
  P3timer.resume();                           // continue
  
  P1periodCmd = 8100;                         // nominal periods for pendulums
  P2periodCmd = 12500;
  P3periodCmd = 6600;


  P1blankFinished = true;
  P2blankFinished = true;
  P3blankFinished = true;
  digitalWrite(P1blankPin, LOW);
  digitalWrite(P2blankPin, LOW);
  digitalWrite(P3blankPin, LOW);


  P1osc = true;
  P2osc = true;
  P3osc = true;
  
  report = 2;
}


// -------------------------------- Main ---------------------------------
// Each pendulum has 3 conditions; STARTUP, BOOSTing and STABLE
// Pendulum starts STARTUP. Channel 3 kicks after timeout.  Random kicks will get it moving.
// Dead kick recurring timeout should be greater than natural period.
// After STARTUP kick, pendulm enters BOOST state where signal determines kick timing.
// After several swings->STABLE state.  Amount of kick in STABLE state is closed loop control.
//  P1    P2    P3
//  --    --    --
//  240   420   200 millisec  period during startup (magnetic capture) phase
//  7800  11800 6200 timer ticks period during stable running state
//  8300  12500 6750 max period of pendulums
//  7650  11500 6100 min period


//  digitalWrite(scopePin, !digitalRead(scopePin));
//  digitalWrite(ledPin, !digitalRead(ledPin));


void loop() {
  loops++;                        // execution rate 66k (P3) to 120k (P2)loops/kick
  if(P2state==STABLE)  P1();      // start P2 after P1 is STABLE
  P2();                           // P2 is hardest to start, goes longest, start first
  if(P1state==STABLE) P3();       // P3 is easiest to start, sustains least, start last


//  P1();
//  P2();
//  P3();
  if(Serial.available()) opCom(); // get operator command
}


// --------------------------- P1 state machine --------------------------------
void P1() {
  P1signal = analogRead(P1signalPin);         // read signal for this pendulum
  P1sigMax = max(P1signal,P1sigMax);          // looking for max signal level
  switch(P1state) {                           // STABLE, BOOST, STARTUP
    case STABLE:                              // steady state - works perfectly
      if((P1sigMax>3000)&&(P1signal<10)&&P1blankFinished) {
        P1periodErr = P1periodCmd - P1periodAct;// error
        P1boost = 10 + (P1Kp*P1periodErr);    // calc P term
        P1boost = constrain(P1boost,9,80);    // "9" is sustain
        P1blank = 2600;                       // period is ~3900, blank 2600 good
        P1kick(P1boost, P1blank,10000);       // blank for 1 second, STARTUP after 30 sec
      }    
      break;
    
    case BOOST:                               // tricky->low signal level, ADC noise
      if((P1sigMax>2000)&&(P1signal<10)&&P1blankFinished) {
        P1boostCount++;                       // count successful kicks
        if(P1boostCount>10) {
          P1boostCount = 0;
          P1state = STABLE;                   // change state
        }                                                                                                                                
        P1boost = 100;                        // must synch with rapid freq period
        P1blank = 300;                        // during BOOST phase (captive of magnet), else 2700
        P1kick(P1boost,P1blank,5000);         // 
      }
      break;
      
    case STARTUP:                             // signal goes to zero only when
      if(P1sigMax>3500) P1state = BOOST;      // magnet swings past coil
      if(P1osc) {                             // BOOST state does not trigger
        P1osc = false;                        // if magnet recaptures pendulum
        P1kick(200, 250, random(1050,1150));   // after exiting STARTUP state
      }                                       // 1100 works
      break;
  }
}


// ----------------------------- P2 state machine -----------------------------------
void P2() {  
  P2signal = analogRead(P2signalPin);         // read signal for this pendulum
  P2sigMax = max(P2signal,P2sigMax);          // recored max sig for this cycle
  switch(P2state) {                           // STARTUP or STABLE
    case STABLE:                              // steady state
      if((P2sigMax>3000)&&(P2signal<10)&&P2blankFinished) {
        P2periodErr = P2periodCmd - P2periodAct;// error
        P2boost = 14 + (P2Kp*P2periodErr);    // calc P term
        P2boost = constrain(P2boost,9,80);    // "9" is sustain
        P2blank = 5000;                       // period ~5900, 5000 good for period>10800
        P2kick(P2boost,P2blank,20000);        // blank for 1 second, STARTUP after 5 sec
      }    
      break;
    
    case BOOST:                               // tricky->low signal level, ADC noise
      if((P2sigMax>2000)&&(P2signal<10) && P2blankFinished) {
        P2boostCount++;                       // count successsful kicks
        if(P2boostCount>10) {
          P2boostCount = 0;
          P2state = STABLE;                   // change state
        }
        P2boost = 100;
        P2blank = 1000;                       // high freq, captive of magnet
        P2kick(P2boost,P2blank,20000);        // big kick, blank 1.5s, STARTUP after 10s
      }
      break;
      
    case STARTUP:                             // 
      if(P2sigMax>3500) P2state = BOOST;      // magnet swings past coil
      if(P2osc) {                             // BOOST state does not trigger
        P2osc = false;                        // if magnet recaptures pendulum
        P2kick(400, 450, random(2700,2800));  // after exiting STARTUP state
      }                                       // 2750 works
      break;
    }
  }


// ----------------------------- P3 state machine -----------------------------------
void P3() {
  P3signal = analogRead(P3signalPin);         // read signal for this pendulum
  P3sigMax = max(P3signal,P3sigMax);          // record max signal
  switch(P3state) {                           // STARTUP, BOOST, STABLE
    case STABLE:                              // steady state - works perfectly
      if((P3sigMax>3000)&&(P3signal<10)&&P3blankFinished) {
        P3periodErr = P3periodCmd - P3periodAct;// error
        P3boost = 10 + (P3Kp*P3periodErr);    // calc P term
        P3boost = constrain(P3boost,7,80);    // "7" is sustain
        P3blank = 2400;                       // period ~3100 (2400 good)
        P3kick(P3boost,P3blank,10000);        // blank for 1 second, STARTUP after 10 sec
      }    
      break;
    
    case BOOST:                               // getting started, constant kick
      if((P3sigMax>2000)&&(P3signal<10)&&P3blankFinished) {
        P3boostCount++;     // count successful kicks
        if(P3boostCount>10) {
          P3boostCount = 0;
          P3state = STABLE; // change state
        }
        P3boost = 100;                        // constant, large kick in boost phase
        P3blank = 500;                        // blank should be brief in case wrong trigger
        P3kick(P3boost,P3blank,4000);         // big kick
      }
      break;
      
    case STARTUP:                             // tricky->low signal level, ADC noise
      if(P3sigMax>3500) P3state = BOOST;      // magnet swings past coil
      if(P3osc) {                             // BOOST state does not trigger
        P3osc = false;                        // if magnet recaptures pendulum
        P3kick(400, 600, random(1500,2600));  // after exiting STARTUP state
      }                                       // random(1500,2600) works
      break;
  }
}


// -------------------------- P1 coil control ----------------------------
void P1kick(uint16_t bst, uint16_t blnk, uint16_t ded) {
  if(report==1) digitalWrite(ledPin,HIGH);// turn on led if report to this P is active
  P1flip = !P1flip;                       // flip flop
  P4timer.pause();                        // stop timer
  if(P1flip) P1countA = P4timer.getCount();// calc period of P4
  else       P1countB = P4timer.getCount();// get half count
  P1periodAct = P1countA + P1countB;      // add two halves
  P4timer.setCount(0);                    // restart timer at zero count
  P4timer.attachInterrupt(1,P4chan1isr);  // isr will turn off boost
  P4timer.attachInterrupt(2,P4chan2isr);  // isr will turn off blanking
  P4timer.attachInterrupt(3,P4chan3isr);  // isr will detect STARTUP condition
  P4timer.setMode(1,TIMER_OUTPUT_COMPARE);// channel 1, generate interrupt on compare
  P4timer.setMode(2,TIMER_OUTPUT_COMPARE);// channel 4, generate interrupt on compare
  P4timer.setMode(3,TIMER_OUTPUT_COMPARE);// channel 3, generate interrupt on compare
  P4timer.setCompare(TIMER_CH1,bst);      // boost
  P4timer.setCompare(TIMER_CH2,blnk);     // blank
  P4timer.setCompare(TIMER_CH3,ded);      // STARTUP
  P4timer.refresh();                      // update
  P4timer.resume();                       // start timer
  digitalWrite(P1gatePin, HIGH);          // turn on magnet
  digitalWrite(P1blankPin,HIGH);          // turn on signal blanking  
  P1blankFinished = false;                // reset blank finished flag
  if(report==1) report1();                // report to IDE monitor
  P1sigMax = 0;
}


// -------------------------- P2 coil control ----------------------------
void P2kick(uint16_t bst, uint16_t blnk, uint16_t ded) {
  if(report==2) digitalWrite(ledPin,HIGH); // turn on led if report to this P is active
  P2flip = !P2flip;                       // calculate period of P2
  P2timer.pause();                        // stop timer
  if(P2flip) P2countA = P2timer.getCount();
  else       P2countB = P2timer.getCount();// get half count
  P2periodAct = P2countA + P2countB;      // add two halves
  P2timer.setCount(0);                    // reset
  P2timer.attachInterrupt(1,P2chan1isr);// isr will turn off boost
  P2timer.attachInterrupt(2,P2chan2isr);// isr will turn off blanking
  P2timer.attachInterrupt(3,P2chan3isr);// isr will detect STARTUP condition
  P2timer.setMode(1,TIMER_OUTPUT_COMPARE);   // channel 1, generate interrupt on compare
  P2timer.setMode(2,TIMER_OUTPUT_COMPARE);   // channel 2, generate interrupt on compare
  P2timer.setMode(3,TIMER_OUTPUT_COMPARE);   // channel 3, generate interrupt on compare
  P2timer.setCompare(TIMER_CH1,bst);      // boost
  P2timer.setCompare(TIMER_CH2,blnk);     // blank
  P2timer.setCompare(TIMER_CH3,ded);      // STARTUP
  P2timer.refresh();                      // update
  P2timer.resume();                       // start timer
  digitalWrite(P2gatePin, HIGH);          // turn on magnet
  digitalWrite(P2blankPin,HIGH);          // turn on signal blanking  
  P2blankFinished = false;                // reset blank finished flag
  if(report==2) report2();                // report to IDE monitor
  P2sigMax = 0;
}


// -------------------------- P3 coil control ----------------------------
void P3kick(uint16_t bst, uint16_t blnk, uint16_t ded) {
  if(report==3) digitalWrite(ledPin,HIGH); // turn on led if report to this P is active
  P3flip = !P3flip;                       // calculate period of P3
  P3timer.pause();                        // stop timer
  if(P3flip) P3countA = P3timer.getCount();
  else       P3countB = P3timer.getCount();// get half count
  P3periodAct = P3countA + P3countB;      // add two halves
  P3timer.setCount(0);                    // reset
  P3timer.attachInterrupt(1,P3chan1isr);  // isr will turn off boost
  P3timer.attachInterrupt(2,P3chan2isr);  // isr will turn off blanking
  P3timer.attachInterrupt(3,P3chan3isr);  // isr will detect STARTUP condition
  P3timer.setMode(1,TIMER_OUTPUT_COMPARE);// channel 1, generate interrupt on compare
  P3timer.setMode(2,TIMER_OUTPUT_COMPARE);// channel 2, generate interrupt on compare
  P3timer.setMode(3,TIMER_OUTPUT_COMPARE);// channel 3, generate interrupt on compare
  P3timer.setCompare(TIMER_CH1,bst);      // boost
  P3timer.setCompare(TIMER_CH2,blnk);     // blank
  P3timer.setCompare(TIMER_CH3,ded);      // STARTUP
  P3timer.refresh();                      // update
  P3timer.resume();                       // start timer
  digitalWrite(P3gatePin, HIGH);          // turn on magnet
  digitalWrite(P3blankPin,HIGH);          // turn on signal blanking  
  P3blankFinished = false;                // reset blank finished flag
  if(report==3) report3();                // report to IDE monitor
  P3sigMax = 0;
}  


// -------------------------------  operator interface -------------------------------------
void opCom() {
  int inNum;  
  char inChar;
  inChar = Serial.read();                       // get char from incoming serial stream
  if((inChar==13)||(inChar==10)) return;        // ignore trailing <CR> <LF> last command
  inChar = toupper(inChar);
  switch(inChar)    {
    case 'R':                     // (R)eport, select which pendulum
      report = Serial.parseInt(); // activate selected report
      lineCounter = 0;
      break;       
  
    case 'P':                     // Set period of selected pendulum
      inNum = Serial.parseInt();  // get number
      Serial.println();
      Serial.print("P");          // (P)eriod
      Serial.print(report);       // 
      Serial.print(" Period ");   // 
      Serial.println(inNum);      // <LF><CR>
      if(report==1) P1periodCmd = inNum;
      if(report==2) P2periodCmd = inNum;
      if(report==3) P3periodCmd = inNum;
      break;


     default:
       Serial.print(" ?");
       inNum = inChar;
       Serial.println(char(inNum));
       break;
  }
}


void plot() {
  Serial.println(P2sigAveSave);           // change baud to 115200
  delay(10);
}


// ---------------------------- reports ---------------------------------------------
void  report1() {
  if(!(lineCounter%20)){
    Serial.println();
    Serial.println("P1State Psigmax Pact    Pcmd    Err     Boost   Loops");
  }
  lineCounter++;
  if(P1state==STARTUP)  Serial.print("STARTUP");
  if(P1state==BOOST)    Serial.print("BOOST");
  if(P1state==STABLE)   Serial.print("STABLE");
  Serial.print(TAB);
  Serial.print(P1sigMax);
  Serial.print(TAB);
  Serial.print(P1periodAct);
  Serial.print(TAB);
  Serial.print(P1periodCmd);
  Serial.print(TAB);
  Serial.print(P1periodErr);
  Serial.print(TAB);
  Serial.print(P1boost);
  Serial.print(TAB);
  Serial.print(loops);
  Serial.println();
  loops = 0;
}


void  report2() {
  if(!(lineCounter%20)){
    Serial.println();
    Serial.println("P2State Psigmax Pact    Pcmd    Err     Boost   Loops");
  }
  lineCounter++;
  if(P2state==STARTUP)  Serial.print("STARTUP");
  if(P2state==BOOST)    Serial.print("BOOST");
  if(P2state==STABLE)   Serial.print("STABLE");
  Serial.print(TAB);
  Serial.print(P2sigMax);
  Serial.print(TAB);
  Serial.print(P2periodAct);
  Serial.print(TAB);
  Serial.print(P2periodCmd);
  Serial.print(TAB);
  Serial.print(P2periodErr);
  Serial.print(TAB);
  Serial.print(P2boost);
  Serial.print(TAB);
  Serial.print(loops);
  Serial.println();
  loops = 0;
}


void  report3() {
  if(!(lineCounter%20)){
    Serial.println();
    Serial.println("P3State Psigmax Pact    Pcmd    Err     Boost   Loops");
  }
  lineCounter++;
  if(P3state==STARTUP)  Serial.print("STARTUP");
  if(P3state==BOOST)    Serial.print("BOOST");
  if(P3state==STABLE)   Serial.print("STABLE");
  Serial.print(TAB);
  Serial.print(P3sigMax);
  Serial.print(TAB);
  Serial.print(P3periodAct);
  Serial.print(TAB);
  Serial.print(P3periodCmd);
  Serial.print(TAB);
  Serial.print(P3periodErr);
  Serial.print(TAB);
  Serial.print(P3boost);
  Serial.print(TAB);
  Serial.print(loops);
  Serial.println();
  loops = 0;
}