1 #include <LiquidCrystal.h>
6 // --------------- ADC related stuffs.... --------------------
8 struct input_cal_t // Struct type for input calibration values
12 int center[MAX_INPUTS];
14 input_cal_t input_cal;
18 int channels; // How many channels should PPM generate for this model ...
19 float stick[8]; // The (potentially recalc'ed) value of stick/input channel.
21 int dr[8]; // The Dual-rate array uses magic numbers :P
22 /* dr[0] = Input channel #1 of 2 for D/R switch #1. 0 means off, 1-4 valid values.
23 dr[1] = Input channel #2 of 2 for D/R switch #1. 0 means off, 1-4 valid values.
24 dr[2] = Input channel #1 of 2 for D/R switch #2. 0 means off, 1-4 valid values.
25 dr[3] = Input channel #2 of 2 for D/R switch #2. 0 means off, 1-4 valid values.
26 dr[4] = D/R value for switch # 1 LOW(off). Value -100 to 100 in steps of 5.
27 dr[5] = D/R value for switch # 1 HIGH(on). Value -100 to 100 in steps of 5.
28 dr[6] = D/R value for switch # 1 LOW(off). Value -100 to 100 in steps of 5.
29 dr[7] = D/R value for switch # 1 HIGH(on). Value -100 to 100 in steps of 5.
32 volatile model_t model;
34 // ----------------- Display related stuffs --------------------
35 LiquidCrystal lcd( 12, 11, 10, 6, 7, 8, 9);
36 // Parameters are: rs, rw, enable, d4, d5, d6, d7 pin numbers.
38 // ----------------- PPM related stuffs ------------------------
39 // The PPM generation is handled by Timer0 interrupts, and needs
40 // all modifiable variables to be global and volatile...
42 //int max_channels = 6; // How many channels should PPM generate ...
43 // Moved to model_t struct...
45 volatile long sum = 0; // Frame-time spent so far
46 volatile int cchannel = 0; // Current channnel
47 volatile bool do_channel = true; // Is next operation a channel or a separator
50 // All time values in usecs
52 // The timing here (and/or in the ISR) needs to be tweaked to provide valid
53 // RC PPM signals accepted by standard RC RX'es and the Microcopter...
55 #define framelength 21500 // Max length of frame
56 #define seplength 400 // Lenght of a channel separator
57 #define chmax 1700 // Max lenght of channel pulse
58 #define chmin 600 // Min length of channel
59 #define chwidht (chmax - chmin)// Useable time of channel pulse
61 // ----------------- Menu/IU related stuffs --------------------
63 // Keys/buttons/switches for UI use, including dual-rate/expo
64 // are digital inputs connected to a 4051 multiplexer, giving
65 // 8 inputs on a single input pin.
75 // Voltage sense pin is connected to a 1/3'd voltage divider.
76 #define BATTERY_CONV (10 * 3 * (5.0f/1024.0f))
77 #define BATTERY_LOW 92
91 EXPOS, // Some radios have "drawn curves", i.e. loopup tables stored in external EEPROM ...
103 // The display/UI is handled only when more
104 // than UI_INTERVAL milliecs has passed since last...
105 #define UI_INTERVAL 250
106 unsigned long last = 0;
116 // ----------------- DEBUG-STUFF --------------------
117 unsigned long prev_loop_time;
118 unsigned long avg_loop_time;
122 // ---------- CODE! -----------------------------------
124 // ---------- Arduino SETUP code ----------------------
126 pinMode(13, OUTPUT); // led
128 pinMode(2, OUTPUT); // s0
129 pinMode(3, OUTPUT); // s1
130 pinMode(4, OUTPUT); // s2
131 pinMode(5, OUTPUT); // e
134 lcd.print("Starting....");
137 Serial.println("Starting....");
144 pinMode(A5, OUTPUT); // PPM output pin
146 set_timer( seplength );
147 Timer1.initialize(framelength);
148 Timer1.attachInterrupt(ISR_timer);
150 displaystate = VALUES;
152 // Arduino believes all pins on Port C are Analog.
153 // In reality they are tri-purpose; ADC, Digital, Digital Interrupts
154 // Unfortunately the interrupt mode is unusable in this scenario, but digital I/O works :P
157 // Debugging: how long does the main loop take on avg...
162 // Setting this here to be sure I do not forget to init' it....
163 // These initializations should be done by read_settings from eeprom,
164 // and this "default model values" should probably be moved
165 // out to a section of read_settings when handling "new model", or
166 // to a separate model_defaults function...
168 model.rev[0] = model.rev[1] = model.rev[2] = model.rev[3] =
169 model.rev[4] = model.rev[5] = model.rev[6] = model.rev[7] = false;
170 model.dr[0] = model.dr[1] = model.dr[2] = model.dr[3] = 0;
171 model.dr[4] = model.dr[5] = model.dr[6] = model.dr[7] = 100;
173 // Initializing the stopwatch timer/clock values...
174 clock_timer = (clock_timer_t){0, 0, 0, false};
177 // ---------- Arduino main loop -----------------------
180 // Determine if the UI needs to run...
182 if ( millis() - last > UI_INTERVAL ) {
190 // Wasting a full I/O pin on battery status monitoring!
191 battery_val = analogRead(1) * BATTERY_CONV;
192 if ( battery_val < BATTERY_LOW ) {
193 digitalWrite(13, 1); // Simulate alarm :P
194 displaystate = BATTERY;
201 if ( displaystate != MENU )
203 // Debugging: how long does the main loop take on avg,
204 // when not handling the UI...
206 avg_loop_time = ( t - prev_loop_time + avg_loop_time ) / 2;
210 // Whoa! Slow down partner! Let everything settle down before proceeding.
214 // ----- Simple support functions used by more complex functions ----
216 void set_ppm_output( bool state )
218 digitalWrite(A5, state); // Hard coded PPM output
221 void set_timer(long time)
223 Timer1.detachInterrupt();
224 Timer1.attachInterrupt(ISR_timer, time);
227 boolean check_key( int key)
229 return ( keys[key] && !prev_keys[key] );
232 void mplx_select(int pin)
235 delayMicroseconds(24);
237 digitalWrite(2, bitRead(pin,0)); // Arduino alias for non-modifying bitshift operation
238 digitalWrite(3, bitRead(pin,1)); // us used to extract individual bits from the int (0..7)
239 digitalWrite(4, bitRead(pin,2)); // Select the appropriate input by setting s1,s2,s3 and e
240 digitalWrite(5, 0); // on the 4051 multiplexer.
242 // May need to slow the following read down to be able to
243 // get fully reliable values from the 4051 multiplex.
244 delayMicroseconds(24);
248 // ----- "Complex" functions follow ---------------------------------
252 int i, r0, r1, r2, adc_in;
254 int num_calibrations = 200;
257 lcd.print("Move controls to");
259 lcd.print("their extremes..");
260 Serial.print("Calibration. Move all controls to their extremes.");
262 for (i=0; i< MAX_INPUTS; i++) {
263 input_cal.min[i] = 1024;
264 input_cal.max[i] = 0;
266 while ( calcount <= num_calibrations )
268 for (i=0; i<=MAX_INPUTS; i++) {
270 adc_in = analogRead(0);
272 // Naive min/max calibration
273 if ( adc_in < input_cal.min[i] ) {
274 input_cal.min[i] = adc_in;
276 if ( adc_in > input_cal.max[i] ) {
277 input_cal.max[i] = adc_in;
285 // TODO: WILL need to do center-point calibration after min-max...
288 lcd.print("Done calibrating");
290 Serial.print("Done calibrating");
294 void read_settings(void)
296 // Dummy. Will be modified to read model settings from EEPROM
297 for (int i=0; i<=7; i++) {
298 input_cal.min[i] = 0;
299 input_cal.center[i] = 512;
300 input_cal.max[i] = 1024;
304 void write_settings(void)
306 // Dummy. Not used anywhere. Will be fleshed out to save settings to EEPROM.
309 void scan_keys ( void )
314 // To get more inputs, another 4051 analog multiplexer is used,
315 // but this time it is used for digital inputs. 8 digital inputs
316 // on one input line, as long as proper debouncing and filtering
317 // is done in hardware :P
318 for (i=0; i<=7; i++) {
319 // To be able to detect that a key has changed state, preserve the previous..
320 prev_keys[i] = keys[i];
322 // Select and read input.
324 keys[i] = digitalRead(A2);
330 void process_inputs(void )
332 int current_input, r0, r1, r2, adc_in;
333 for (current_input=0; current_input<=7; current_input++) {
335 mplx_select(current_input);
336 adc_in = analogRead(0);
338 model.stick[current_input] = ((float)adc_in - (float)input_cal.min[current_input]) / (float)(input_cal.max[current_input]-input_cal.min[current_input]);
339 if ( model.rev[current_input] ) model.stick[current_input] = 1.0f - model.stick[current_input];
346 Timer1.stop(); // Make sure we do not run twice while working :P
350 set_ppm_output( LOW );
353 set_timer(seplength);
357 if ( cchannel >= model.channels )
359 set_ppm_output( HIGH );
360 long framesep = framelength - sum;
365 set_timer ( framesep );
371 set_ppm_output( HIGH );
372 long next_timer = (( chwidht * model.stick[cchannel] ) + chmin);
373 // Do sanity-check of next_timer compared to chmax ...
374 while ( chmax < next_timer ) next_timer--;
377 // Done with channel separator and value,
378 // prepare for next channel...
381 set_timer ( next_timer );
389 for (current_input=0; current_input<=7; current_input++) {
390 int v = (int)(model.stick[current_input] * 100);
392 Serial.print("Input #");
393 Serial.print(current_input);
394 Serial.print(" value: ");
395 Serial.print(model.stick[current_input]);
396 Serial.print(" pct: ");
398 Serial.print(" min: ");
399 Serial.print(input_cal.min[current_input]);
400 Serial.print(" max: ");
401 Serial.print(input_cal.max[current_input]);
404 Serial.print("Battery level is: ");
405 Serial.println(battery_val);
407 Serial.print("Average loop time:");
408 Serial.println(avg_loop_time);
412 void dr_inputselect( int no, int in )
414 if ( model.dr[menu_substate] < 0 ) model.dr[menu_substate] = 4;
415 if ( model.dr[menu_substate] > 4 ) model.dr[menu_substate] = 0;
417 lcd.setCursor(0 , 0);
418 lcd.print("D/R switch ");
421 lcd.setCursor(0 , 1);
426 if ( ! model.dr[menu_substate] ) lcd.print("Off");
427 else lcd.print(model.dr[menu_substate]);
429 if ( check_key(KEY_INC) ) {
430 model.dr[menu_substate]++;
433 else if ( check_key(KEY_DEC) ) {
434 model.dr[menu_substate]--;
447 if ( menu_substate == 4) state = keys[KEY_DR1];
448 else state = keys[KEY_DR2];
450 pos = 4 + (menu_substate - 4) * 2;
453 lcd.setCursor(0 , 0);
454 lcd.print("D/R switch ");
455 lcd.print( menu_substate - 3 );
457 lcd.setCursor(0 , 1);
458 lcd.print( state ? "HI" : "LO" );
459 lcd.print(" Value :");
461 lcd.print( model.dr[pos] );
463 if ( keys[KEY_INC] ) {
464 if ( model.dr[pos] < 100) model.dr[pos] += 5;
467 else if ( keys[KEY_DEC] ) {
468 if ( model.dr[pos] > -100) model.dr[pos] -= 5;
480 if ( displaystate != MENU )
483 if ( check_key(KEY_UP) && displaystate == VALUES ) {
484 displaystate = BATTERY;
487 else if ( check_key(KEY_UP) && displaystate == BATTERY ) {
488 displaystate = TIMER;
491 else if ( check_key(KEY_UP) && displaystate == TIMER ) {
492 displaystate = VALUES;
496 else if ( check_key(KEY_DOWN) ) {
502 digitalWrite(13, digitalRead(13) ^ 1 );
504 switch ( displaystate )
508 for (current_input=0; current_input<=7; current_input++) {
509 // In channel value display, do a simple calc
510 // of the LCD row & column location. With 8 channels
511 // we can fit eight channels as percentage values on
512 // a simple 16x2 display...
513 if ( current_input < 4 )
515 col = current_input * 4;
520 col = (current_input-4) * 4;
523 // Overwriting the needed positions with
524 // blanks cause less display-flicker than
525 // actually clearing the display...
526 lcd.setCursor(col, row);
528 lcd.setCursor(col, row);
529 // Display uses percents, while PPM uses ratio....
530 int v = (int)(model.stick[current_input] * 100);
538 lcd.print("Battery level: ");
539 lcd.setCursor(0 , 1);
540 lcd.print( (float)battery_val/10);
542 if ( battery_val < BATTERY_LOW ) lcd.print(" - WARNING");
543 else lcd.print(" - OK");
555 lcd.print("Timer: ");
556 lcd.print( clock_timer.running ? "Running" : "Stopped" );
557 lcd.setCursor(5 , 1);
558 if ( clock_timer.running )
560 clock_timer.value = millis() - (clock_timer.start + clock_timer.init);
562 hours = ( clock_timer.value / 1000 ) / 3600;
563 clock_timer.value = clock_timer.value % 3600000;
564 minutes = ( clock_timer.value / 1000 ) / 60;
565 seconds = ( clock_timer.value / 1000 ) % 60;
570 if ( minutes < 10 ) lcd.print("0");
571 lcd.print( minutes );
573 if ( seconds < 10 ) lcd.print("0");
574 lcd.print( seconds );
576 if ( check_key(KEY_INC) ) {
577 if ( !clock_timer.running && !clock_timer.start )
579 clock_timer.start = millis();
580 clock_timer.value = 0;
581 clock_timer.running = true;
582 } else if ( !clock_timer.running && clock_timer.start ) {
583 clock_timer.start = millis() - clock_timer.value;
584 clock_timer.running = true;
585 } else if ( clock_timer.running ) {
586 clock_timer.running = false;
589 } else if ( check_key(KEY_DEC) ) {
590 if ( !clock_timer.running && clock_timer.start ) {
591 clock_timer.value = 0;
592 clock_timer.start = 0;
593 clock_timer.init = 0;
601 switch ( menu_mainstate )
604 lcd.print("In MENU mode!");
605 lcd.setCursor(0 , 1);
606 lcd.print("Esc UP. Scrl DN.");
608 if ( check_key(KEY_UP) ) {
609 displaystate = VALUES;
612 else if ( check_key(KEY_DOWN) ) {
613 menu_mainstate = INVERTS;
620 if ( menu_substate >= model.channels ) menu_substate = 0;
621 if ( menu_substate < 0) menu_substate = (model.channels - 1);
622 lcd.print("Channel invert");
623 lcd.setCursor(0 , 1);
625 lcd.print(menu_substate+1);
626 lcd.print( (model.rev[menu_substate] ? ": Invert" : ": Normal"));
628 if ( check_key(KEY_UP) ) {
629 menu_mainstate = TOP;
632 else if ( check_key(KEY_DOWN) ) {
633 menu_mainstate = DUALRATES;
637 if ( check_key(KEY_RIGHT) ) {
641 else if ( check_key(KEY_LEFT) ) {
645 else if ( check_key(KEY_INC) || check_key(KEY_DEC) ) {
646 model.rev[menu_substate] ^= 1;
652 if ( menu_substate > 5 ) menu_substate = 0;
653 if ( menu_substate < 0) menu_substate = 5;
655 if ( check_key(KEY_UP) ) {
656 menu_mainstate = INVERTS;
659 if ( check_key(KEY_DOWN) ) {
660 menu_mainstate = EXPOS;
663 if ( check_key(KEY_RIGHT) ) {
667 else if ( check_key(KEY_LEFT) ) {
671 switch (menu_substate)
674 dr_inputselect(0, 0);
677 dr_inputselect(0, 1);
680 dr_inputselect(1, 0);
683 dr_inputselect(1, 1);
697 lcd.print("Input expo curve");
698 lcd.setCursor(0 , 1);
699 lcd.print("Not implemented");
700 // Possible, if input values are mapped to +/- 100 rather than 0..1 ..
701 // plot ( x*(1 - 1.0*cos (x/(20*PI)) )) 0 to 100
702 // Run in wolfram to see result, adjust the 1.0 factor to inc/red effect.
703 // Problem: -100 to 100 is terribly bad presicion, esp. considering that
704 // the values started as 0...1024, and we have 1000usec to "spend" on channels.
705 if ( check_key(KEY_UP ) ) {
706 menu_mainstate = DUALRATES;
709 if ( check_key(KEY_DOWN ) ) {
710 menu_mainstate = DEBUG;
716 lcd.setCursor(0 , 0);
717 lcd.print("Dumping debug to");
718 lcd.setCursor(0 , 1);
719 lcd.print("serial port 0");
721 if ( check_key(KEY_UP ) ) {
722 // FIXME: Remember to update the "Scroll up" state!
723 menu_mainstate = EXPOS;
725 } else if ( check_key(KEY_DOWN ) ) {
726 menu_mainstate = SAVE;
732 lcd.print("Not implemented");
733 lcd.setCursor(0 , 1);
734 lcd.print("Press DOWN...");
735 if ( check_key(KEY_DOWN ) ) menu_mainstate = TOP;