--- /dev/null
+#include <LiquidCrystal.h>
+#include <TimerOne.h>
+
+// --------------- ADC related stuffs.... --------------------
+
+int maxval[8]; // Stores maximum values read during calibration, setup() sets 1024
+int minval[8]; // Stores minimum values read during calibration, setup() sets 0
+int rev[8];
+int dr[8]; // The Dual-rate array uses magic numbers :P
+/* dr[0] = Input channel #1 of 2 for D/R switch #1. 0 means off, 1-4 valid values.
+ dr[1] = Input channel #2 of 2 for D/R switch #1. 0 means off, 1-4 valid values.
+ dr[2] = Input channel #1 of 2 for D/R switch #2. 0 means off, 1-4 valid values.
+ dr[3] = Input channel #2 of 2 for D/R switch #2. 0 means off, 1-4 valid values.
+ dr[4] = D/R value for switch # 1 LOW(off). Value -100 to 100 in steps of 5.
+ dr[5] = D/R value for switch # 1 HIGH(on). Value -100 to 100 in steps of 5.
+ dr[6] = D/R value for switch # 1 LOW(off). Value -100 to 100 in steps of 5.
+ dr[7] = D/R value for switch # 1 HIGH(on). Value -100 to 100 in steps of 5.
+*/
+
+volatile float channel[8];
+
+// ----------------- Display related stuffs --------------------
+LiquidCrystal lcd( 12, 11, 10, 6, 7, 8, 9);
+// Parameters are: rs, rw, enable, d4, d5, d6, d7 pin numbers.
+
+// ----------------- PPM related stuffs ------------------------
+// The PPM generation is handled by Timer0 interrupts, and needs
+// all modifiable variables to be global and volatile...
+int max_channels = 6; // How many channels should PPM generate ...
+volatile long sum = 0; // Frame-time spent so far
+volatile int cchannel = 0; // Current channnel
+volatile bool do_channel = true; // Is next operation a channel or a separator
+
+// All time values in usecs
+// TODO:
+// The timing here (and/or in the ISR) needs to be tweaked to provide valid
+// RC PPM signals accepted by standard RC RX'es and the Microcopter...
+long framelength = 21500; // Max length of frame
+long seplength = 400; // Lenght of a channel separator
+long chmax = 1700; // Max lenght of channel pulse
+long chmin = 600; // Min length of channel
+long chwidht = (chmax - chmin); // Useable time of channel pulse (1000)
+
+// ----------------- Menu/IU related stuffs --------------------
+
+// Keys/buttons/switches for UI use, including dual-rate/expo
+// are digital inputs connected to a 4051 multiplexer, giving
+// 8 inputs on a single input pin.
+#define KEY_UP 0
+#define KEY_DOWN 1
+#define KEY_RIGHT 2
+#define KEY_LEFT 3
+#define KEY_INC 4
+#define KEY_DEC 5
+#define KEY_DR1 6
+#define KEY_DR2 7
+
+// Voltage sense pin is connected to a 1/3'd voltage divider.
+#define BATTERY_CONV (10 * 3 * (5.0f/1024.0f))
+#define BATTERY_LOW 92
+
+enum {
+ VALUES,
+ BATTERY,
+ TIMER,
+ MENU
+}
+displaystate;
+
+enum {
+ TOP,
+ INVERTS,
+ DUALRATES,
+ EXPOS, // Some radios have "drawn curves", i.e. loopup tables stored in external EEPROM ...
+ DEBUG,
+ SAVE
+}
+menu_mainstate;
+int menu_substate;
+
+boolean keys[8];
+boolean prev_keys[8];
+
+int battery_val;
+
+// The display/UI is handled only when more
+// than UI_INTERVAL milliecs has passed since last...
+#define UI_INTERVAL 250
+unsigned long last = 0;
+
+unsigned long timer_start = 0;
+unsigned long timer_init = 0;
+unsigned long timer_value = 0;
+boolean timer_running = false;
+
+// ----------------- DEBUG-STUFF --------------------
+unsigned long prev_loop_time;
+unsigned long avg_loop_time;
+unsigned long t;
+
+
+// ---------- CODE! -----------------------------------
+
+// ---------- Arduino SETUP code ----------------------
+void setup(){
+ pinMode(13, OUTPUT); // led
+
+ pinMode(2, OUTPUT); // s0
+ pinMode(3, OUTPUT); // s1
+ pinMode(4, OUTPUT); // s2
+ pinMode(5, OUTPUT); // e
+
+ lcd.begin(16,2);
+ lcd.print("Starting....");
+
+ Serial.begin(9600);
+ Serial.println("Starting....");
+ delay(500);
+ read_settings();
+ scan_keys();
+ if ( keys[KEY_UP])
+ calibrate();
+
+ pinMode(A5, OUTPUT); // PPM output pin
+ do_channel = false;
+ set_timer( seplength );
+ Timer1.initialize(framelength);
+ Timer1.attachInterrupt(ISR_timer);
+
+ displaystate = VALUES;
+
+ // Arduino believes all pins on Port C are Analog.
+ // In reality they are tri-purpose; ADC, Digital, Digital Interrupts
+ // Unfortunately the interrupt mode is unusable in this scenario, but digital I/O works :P
+ pinMode(A2, INPUT);
+
+ // Debugging: how long does the main loop take on avg...
+ t = micros();
+ avg_loop_time = t;
+ prev_loop_time = t;
+
+ dr[0] = dr[1] = dr[2] = dr[3] = 0;
+ dr[4] = dr[5] = dr[6] = dr[7] = 100;
+
+}
+
+// ---------- Arduino main loop -----------------------
+void loop () {
+
+ // Determine if the UI needs to run...
+ boolean disp;
+ if ( millis() - last > UI_INTERVAL ) {
+ last = millis();
+ disp = true;
+ }
+ else disp = false;
+
+ process_inputs();
+
+ // Wasting a full I/O pin on battery status monitoring!
+ battery_val = analogRead(1) * BATTERY_CONV;
+ if ( battery_val < BATTERY_LOW ) {
+ digitalWrite(13, 1); // Simulate alarm :P
+ displaystate = BATTERY;
+ }
+
+ if ( disp )
+ {
+ ui_handler();
+ }
+ if ( displaystate != MENU )
+ {
+ // Debugging: how long does the main loop take on avg,
+ // when not handling the UI...
+ t = micros();
+ avg_loop_time = ( t - prev_loop_time + avg_loop_time ) / 2;
+ prev_loop_time = t;
+ }
+
+ // Whoa! Slow down partner! Let everything settle down before proceeding.
+ delay(5);
+}
+
+// ----- Simple support functions used by more complex functions ----
+
+void set_ppm_output( bool state )
+{
+ digitalWrite(A5, state); // Hard coded PPM output
+}
+
+void set_timer(long time)
+{
+ Timer1.detachInterrupt();
+ Timer1.attachInterrupt(ISR_timer, time);
+}
+
+boolean check_key( int key)
+{
+ return ( keys[key] && !prev_keys[key] );
+}
+
+void mplx_select(int pin)
+{
+ digitalWrite(5, 1);
+ delayMicroseconds(24);
+
+ digitalWrite(2, bitRead(pin,0)); // Arduino alias for non-modifying bitshift operation
+ digitalWrite(3, bitRead(pin,1)); // us used to extract individual bits from the int (0..7)
+ digitalWrite(4, bitRead(pin,2)); // Select the appropriate input by setting s1,s2,s3 and e
+ digitalWrite(5, 0); // on the 4051 multiplexer.
+
+ // May need to slow the following read down to be able to
+ // get fully reliable values from the 4051 multiplex.
+ delayMicroseconds(24);
+
+}
+
+// ----- "Complex" functions follow ---------------------------------
+
+void calibrate()
+{
+ int i, r0, r1, r2, adc_in;
+ int calcount = 0;
+ int num_calibrations = 200;
+
+ lcd.clear();
+ lcd.print("Move controls to");
+ lcd.setCursor(0,1);
+ lcd.print("their extremes..");
+ Serial.print("Calibration. Move all controls to their extremes.");
+
+ for (i=0; i<=7; i++) {
+ minval[i] = 1024;
+ maxval[i] = 0;
+ }
+ while ( calcount <= num_calibrations )
+ {
+ for (i=0; i<=7; i++) {
+ mplx_select(i);
+ adc_in = analogRead(0);
+
+ // Naive min/max calibration
+ if ( adc_in < minval[i] ) {
+ minval[i] = adc_in;
+ }
+ if ( adc_in > maxval[i] ) {
+ maxval[i] = adc_in;
+ }
+ delay(10);
+ }
+
+ calcount++;
+ }
+
+ // TODO: WILL need to do center-point calibration after min-max...
+
+ lcd.clear();
+ lcd.print("Done calibrating");
+
+ Serial.print("Done calibrating");
+ delay(2000);
+}
+
+void read_settings(void)
+{
+ // Dummy. Will be modified to read model settings from EEPROM
+ for (int i=0; i<=7; i++) {
+ minval[i] = 0;
+ maxval[i] = 1024;
+ }
+}
+
+void write_settings(void)
+{
+ // Dummy. Not used anywhere. Will be fleshed out to save settings to EEPROM.
+}
+
+void scan_keys ( void )
+{
+ int i, r0, r1, r2;
+ boolean key_in;
+
+ // To get more inputs, another 4051 analog multiplexer is used,
+ // but this time it is used for digital inputs. 8 digital inputs
+ // on one input line, as long as proper debouncing and filtering
+ // is done in hardware :P
+ for (i=0; i<=7; i++) {
+ // To be able to detect that a key has changed state, preserve the previous..
+ prev_keys[i] = keys[i];
+
+ // Select and read input.
+ mplx_select(i);
+ keys[i] = digitalRead(A2);
+ delay(2);
+ }
+}
+
+
+void process_inputs(void )
+{
+ int current_input, r0, r1, r2, adc_in;
+ for (current_input=0; current_input<=7; current_input++) {
+
+ mplx_select(current_input);
+ adc_in = analogRead(0);
+
+ channel[current_input] = ((float)adc_in - (float)minval[current_input]) / (float)(maxval[current_input]-minval[current_input]);
+ if ( rev[current_input] ) channel[current_input] = 1.0f - channel[current_input];
+ }
+}
+
+
+void ISR_timer(void)
+{
+ Timer1.stop(); // Make sure we do not run twice while working :P
+
+ if ( !do_channel )
+ {
+ set_ppm_output( LOW );
+ sum += seplength;
+ do_channel = true;
+ set_timer(seplength);
+ return;
+ }
+
+ if ( cchannel >= max_channels )
+ {
+ set_ppm_output( HIGH );
+ long framesep = framelength - sum;
+
+ sum = 0;
+ do_channel = false;
+ cchannel = 0;
+ set_timer ( framesep );
+ return;
+ }
+
+ if ( do_channel )
+ {
+ set_ppm_output( HIGH );
+ long next_timer = (( chwidht * channel[cchannel] ) + chmin);
+ // Do sanity-check of next_timer compared to chmax ...
+ while ( chmax < next_timer ) next_timer--;
+ sum += next_timer;
+
+ // Done with channel separator and value,
+ // prepare for next channel...
+ cchannel++;
+ do_channel = false;
+ set_timer ( next_timer );
+ return;
+ }
+}
+
+void serial_debug()
+{
+ int current_input;
+ for (current_input=0; current_input<=7; current_input++) {
+ int v = (int)(channel[current_input] * 100);
+
+ Serial.print("Input #");
+ Serial.print(current_input);
+ Serial.print(" value: ");
+ Serial.print(channel[current_input]);
+ Serial.print(" pct: ");
+ Serial.print(v);
+ Serial.print(" min: ");
+ Serial.print(minval[current_input]);
+ Serial.print(" max: ");
+ Serial.print(maxval[current_input]);
+ Serial.println();
+ }
+ Serial.print("Battery level is: ");
+ Serial.println(battery_val);
+
+ Serial.print("Average loop time:");
+ Serial.println(avg_loop_time);
+
+ Serial.println();
+}
+void dr_inputselect( int no, int in )
+{
+ if ( dr[menu_substate] < 0 ) dr[menu_substate] = 4;
+ if ( dr[menu_substate] > 4 ) dr[menu_substate] = 0;
+
+ lcd.setCursor(0 , 0);
+ lcd.print("D/R switch ");
+ lcd.print( no + 1 );
+ lcd.print(" ");
+ lcd.setCursor(0 , 1);
+ lcd.print("Input ");
+ lcd.print(in+1);
+
+ lcd.print(": ");
+ if ( ! dr[menu_substate] ) lcd.print("Off");
+ else lcd.print(dr[menu_substate]);
+
+ if ( check_key(KEY_INC) ) {
+ dr[menu_substate]++;
+ return;
+ }
+ else if ( check_key(KEY_DEC) ) {
+ dr[menu_substate]--;
+ return;
+ }
+ // Wrap around.
+ return;
+
+}
+
+void dr_value()
+{
+ int pos;
+ int state;
+
+ if ( menu_substate == 4) state = keys[KEY_DR1];
+ else state = keys[KEY_DR2];
+
+ pos = 4 + (menu_substate - 4) * 2;
+ if (state) pos++;
+
+ lcd.setCursor(0 , 0);
+ lcd.print("D/R switch ");
+ lcd.print( menu_substate - 3 );
+ lcd.print(" ");
+ lcd.setCursor(0 , 1);
+ lcd.print( state ? "HI" : "LO" );
+ lcd.print(" Value :");
+
+ lcd.print( dr[pos] );
+
+ if ( keys[KEY_INC] ) {
+ if ( dr[pos] < 100) dr[pos] += 5;
+ return;
+ }
+ else if ( keys[KEY_DEC] ) {
+ if ( dr[pos] > -100) dr[pos] -= 5;
+ return;
+ }
+
+ return;
+}
+void ui_handler()
+{
+ int row;
+ int col;
+ scan_keys();
+
+ if ( displaystate != MENU )
+ {
+ menu_substate = 0;
+ if ( check_key(KEY_UP) && displaystate == VALUES ) {
+ displaystate = BATTERY;
+ return;
+ }
+ else if ( check_key(KEY_UP) && displaystate == BATTERY ) {
+ displaystate = TIMER;
+ return;
+ }
+ else if ( check_key(KEY_UP) && displaystate == TIMER ) {
+ displaystate = VALUES;
+ return;
+ }
+
+ else if ( check_key(KEY_DOWN) ) {
+ displaystate = MENU;
+ return;
+ }
+ }
+
+ digitalWrite(13, digitalRead(13) ^ 1 );
+
+ switch ( displaystate )
+ {
+ case VALUES:
+ int current_input;
+ for (current_input=0; current_input<=7; current_input++) {
+ // In channel value display, do a simple calc
+ // of the LCD row & column location. With 8 channels
+ // we can fit eight channels as percentage values on
+ // a simple 16x2 display...
+ if ( current_input < 4 )
+ {
+ col = current_input * 4;
+ row = 0;
+ }
+ else
+ {
+ col = (current_input-4) * 4;
+ row = 1;
+ }
+ // Overwriting the needed positions with
+ // blanks cause less display-flicker than
+ // actually clearing the display...
+ lcd.setCursor(col, row);
+ lcd.print(" ");
+ lcd.setCursor(col, row);
+ // Display uses percents, while PPM uses ratio....
+ int v = (int)(channel[current_input] * 100);
+ lcd.print(v);
+ }
+ break;
+
+
+ case BATTERY:
+ lcd.clear();
+ lcd.print("Battery level: ");
+ lcd.setCursor(0 , 1);
+ lcd.print( (float)battery_val/10);
+ lcd.print("V");
+ if ( battery_val < BATTERY_LOW ) lcd.print(" - WARNING");
+ else lcd.print(" - OK");
+ break;
+
+
+
+ case TIMER:
+ unsigned long delta;
+ int hours;
+ int minutes;
+ int seconds;
+
+ lcd.clear();
+ lcd.print("Timer: ");
+ lcd.print( timer_running ? "Running" : "Stopped" );
+ lcd.setCursor(5 , 1);
+ if ( timer_running )
+ {
+ timer_value = millis() - (timer_start + timer_init);
+ }
+ hours = ( timer_value / 1000 ) / 3600;
+ timer_value = timer_value % 3600000;
+ minutes = ( timer_value / 1000 ) / 60;
+ seconds = ( timer_value / 1000 ) % 60;
+ if ( hours ) {
+ lcd.print(hours);
+ lcd.print(":");
+ }
+ if ( minutes < 10 ) lcd.print("0");
+ lcd.print( minutes );
+ lcd.print(":");
+ if ( seconds < 10 ) lcd.print("0");
+ lcd.print( seconds );
+
+ if ( check_key(KEY_INC) ) {
+ if ( !timer_running && !timer_start )
+ {
+ timer_start = millis();
+ timer_value = 0;
+ timer_running = true;
+ } else if ( !timer_running && timer_start ) {
+ timer_start = millis() - timer_value;
+ timer_running = true;
+ } else if ( timer_running ) {
+ timer_running = false;
+ }
+ return;
+ } else if ( check_key(KEY_DEC) ) {
+ if ( !timer_running && timer_start ) {
+ timer_value = 0;
+ timer_start = 0;
+ timer_init = 0;
+ }
+ return;
+ }
+ break;
+
+ case MENU:
+ lcd.clear();
+ switch ( menu_mainstate )
+ {
+ case TOP:
+ lcd.print("In MENU mode!");
+ lcd.setCursor(0 , 1);
+ lcd.print("Esc UP. Scrl DN.");
+ menu_substate = 0;
+ if ( check_key(KEY_UP) ) {
+ displaystate = VALUES;
+ return;
+ }
+ else if ( check_key(KEY_DOWN) ) {
+ menu_mainstate = INVERTS;
+ return;
+ }
+ break;
+
+
+ case INVERTS:
+ if ( menu_substate >= max_channels ) menu_substate = 0;
+ if ( menu_substate < 0) menu_substate = (max_channels - 1);
+ lcd.print("Channel invert");
+ lcd.setCursor(0 , 1);
+ lcd.print("Ch ");
+ lcd.print(menu_substate+1);
+ lcd.print( (rev[menu_substate] ? ": Invert" : ": Normal"));
+
+ if ( check_key(KEY_UP) ) {
+ menu_mainstate = TOP;
+ return;
+ }
+ else if ( check_key(KEY_DOWN) ) {
+ menu_mainstate = DUALRATES;
+ return;
+ }
+
+ if ( check_key(KEY_RIGHT) ) {
+ menu_substate++;
+ return;
+ }
+ else if ( check_key(KEY_LEFT) ) {
+ menu_substate--;
+ return;
+ }
+ else if ( check_key(KEY_INC) || check_key(KEY_DEC) ) {
+ rev[menu_substate] ^= 1;
+ return;
+ }
+ break;
+
+ case DUALRATES:
+ if ( menu_substate > 5 ) menu_substate = 0;
+ if ( menu_substate < 0) menu_substate = 5;
+
+ if ( check_key(KEY_UP) ) {
+ menu_mainstate = INVERTS;
+ return;
+ }
+ if ( check_key(KEY_DOWN) ) {
+ menu_mainstate = EXPOS;
+ return;
+ }
+ if ( check_key(KEY_RIGHT) ) {
+ menu_substate++;
+ return;
+ }
+ else if ( check_key(KEY_LEFT) ) {
+ menu_substate--;
+ return;
+ }
+ switch (menu_substate)
+ {
+ case 0:
+ dr_inputselect(0, 0);
+ return;
+ case 1:
+ dr_inputselect(0, 1);
+ return;
+ case 2:
+ dr_inputselect(1, 0);
+ return;
+ case 3:
+ dr_inputselect(1, 1);
+ return;
+ case 4:
+ case 5:
+ dr_value();
+ return;
+ default:
+ menu_substate = 0;
+ break;
+ }
+ break;
+
+ case EXPOS:
+ //________________
+ lcd.print("Input expo curve");
+ lcd.setCursor(0 , 1);
+ lcd.print("Not implemented");
+ // Possible, if input values are mapped to +/- 100 rather than 0..1 ..
+ // plot ( x*(1 - 1.0*cos (x/(20*PI)) )) 0 to 100
+ // Run in wolfram to see result, adjust the 1.0 factor to inc/red effect.
+ // Problem: -100 to 100 is terribly bad presicion, esp. considering that
+ // the values started as 0...1024, and we have 1000usec to "spend" on channels.
+ if ( check_key(KEY_UP ) ) {
+ menu_mainstate = DUALRATES;
+ return;
+ }
+ if ( check_key(KEY_DOWN ) ) {
+ menu_mainstate = DEBUG;
+ return;
+ }
+ break;
+
+ case DEBUG:
+ lcd.setCursor(0 , 0);
+ lcd.print("Dumping debug to");
+ lcd.setCursor(0 , 1);
+ lcd.print("serial port 0");
+ serial_debug();
+ if ( check_key(KEY_UP ) ) {
+ // FIXME: Remember to update the "Scroll up" state!
+ menu_mainstate = EXPOS;
+ return;
+ } else if ( check_key(KEY_DOWN ) ) {
+ menu_mainstate = SAVE;
+ return;
+ }
+ break;
+
+ default:
+ lcd.print("Not implemented");
+ lcd.setCursor(0 , 1);
+ lcd.print("Press DOWN...");
+ if ( check_key(KEY_DOWN ) ) menu_mainstate = TOP;
+ }
+ break;
+
+
+ default:
+ // Invalid
+ return;
+ }
+
+ return;
+}
+
+
+
+
+\r