/*
    Programme de contrôle de servo-moteurs.

    Les commandes sont données par un PC connecté via la liaison série (9600,8,N,1).
    Huit servos peuvent être contrôlés, mais 2 sont utilisés ici, connectés
    aux pins PD2 et PD3.
    
    Cible : ATMega8
    Quartz : 8MHz
    Compilateur : avr-gcc (WInAVR)
*/

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>

// nos fonctions de gestion de l'USART, pour la communication avec le PC
#include "usart.h"


// durées typiques (en µs)
#define PULSE_MIN_WIDTH    1000
#define PULSE_MAX_WIDTH    2000
#define PULSE_MED_WIDTH 1500

// nombre de servos gérés
#define SERVO_COUNT     8

// largeurs des pulses des servos, en fonction de la position demandée
volatile unsigned int pulse_widths[SERVO_COUNT] ;

// id du servo courant (dont on génère le créneau)
volatile unsigned char cur_servo = 0 ;

/*
    Interrupt handler pour l'Ouput Compare A du timer 1
    
    Le passage à cette valeur déclenche le rebouclage du compteur, et
    correspond au début du créneau.
*/
SIGNAL (SIG_OUTPUT_COMPARE1A) {
    // On monte donc la sortie du servo en cours de traitement
    // (en gérant la distribution des sorties sur les ports C et D)
    if (cur_servo < 2) {
        PORTC |= (1 << cur_servo) ;
    } else {
        PORTD |= (1 << cur_servo) ;
    }
    
    // On définit le comparateur B en fonction de la durée de l'impulsion à générer
    OCR1B = pulse_widths[cur_servo] ;
}


/*
    Interrupt handler pour l'Ouput Compare B du timer 1
    
    Le passage à cette valeur correspond à la fin du créneau pour
    le servo en cours.
*/
SIGNAL (SIG_OUTPUT_COMPARE1B) {
    // on descend la sortie du servo en cours de traitement
    if (cur_servo < 2) {
        PORTC &= ~(1 << cur_servo) ;
    } else {
        PORTD &= ~(1 << cur_servo) ;
    }
    
    // on passe au servo suivant, en rebouclant en fin de série
    if (++cur_servo == SERVO_COUNT) cur_servo = 0 ;
}


/*
    Initialisation des ports
*/
void port_init(void) {
    // configuration du port D
     // - PD2..PD7 en sortie
     DDRD  = 0xFC ;
    // - toutes les sorties inactives
     PORTD = 0x00 ;

    // configuration du port C
     // - PC0 et PC1 en sortie
     DDRC  = DDRC | 0x03 ;
    // - toutes les sorties inactives (en laissant /RESET à 1)
     PORTC = 0x40 ;
}

/*
    Initialisation des périphériques
*/
void init_devices(void) {
     cli();                         // inhibition des interruptions (pour avoir la paix)
     port_init();                // initialisation de prts
    usart_init(UBRR_8_9600) ;     // initialisation de l'UART

    // MCU Control Register
    // - interruptions externes sur niveau bas de la ligne
    // - sleep mode désactivé
     MCUCR = 0x00;

     // General Interrupt Control
     // - INT0 et INT1 inactives
     GICR  = 0x00;

    // Initialisation  du Timer/Counter 1
    /*
        On utilise un prescale de 8, ce qui pour un quartz à 8 MHz donne donc 1000 clocks
        par ms. Ainsi, les valeurs des compteurs seront directement les timings voulus, 
        exprimés en µs.
        
        Le créneau maximal de la commande du servo étant de 2ms, on va configurer le timer 
        pour qu'il déclenche une interruption et reboucle automatiquement en atteignant
        cette valeur (mode CTC), grâce au premier comparateur (OCR1A). Cette interruption nous 
        servira à déclencher le font montant du créneau de commande. 
        
        L'instant du front descendant sera signalé par l'interruption du deuxième comparateur 
        (OCR1B) dont la valeur sera variable et traduira la position qu'on veut donner au servo.
    */
    
    // Configuration du timer 1
    // - mode CTC avec TOP indiqué par registre OCR1A
    // => seul TCCR1B est à définir, le reste de la config donnant 0 pour TCCR1A
    // (voir pages 95 et suivantes du datasheet)
     TCCR1B = (1 << WGM12)     // Waveform Generation Mode = CTC (TOP = OCR1A)
           | (1 << CS11)     // prescale à 8
           ;
        
    // Définition du TOP à la largeur d'impulsion max + epsilon 
    /*
        (pour éviter d'éventuels pbs aux limites pour la génération d'un créneau de 2ms,
        situation dans laquelle OCR1A et OCR1B auraient les mêmes valeurs, et donc les 
        interruptions correspondantes pourraient arriver dans la mauvaise séquence)
    */
    OCR1A = PULSE_MAX_WIDTH + 10 ;
    
    // initialisation de OCR1B à la valeur de mi-course des servo
    OCR1B = PULSE_MED_WIDTH ;
    
    // initialisation du point de départ du compteur juste après cette valeur,
    // pour démarrer le processus dans la bonne phase
    TCNT1 = OCR1B + 10 ;
    
    // Activation des interuptions des comparateurs A et B
    TIMSK = (1 << OCIE1A)
          | (1 << OCIE1B)
          ;

     sei();                           // autorisation des interruptions
}

// inputs utilisateur 
unsigned char servo_id ;    // id du servo à positionner
unsigned char servo_pos ;    // position

char* msgInvalidInput = "*** Invalid input. Please re-enter ***\r\n" ;

int main(void) {
    // initialisation de tous les servos à mi-course
    for (int i = 0 ; i < SERVO_COUNT ; i++) {
        pulse_widths[i] = 1500 ;
    }
    
    init_devices();
    
    // A partir de maintenant, nous sommes en train de générer des pulses en permanence
    // sur chacune des sorties des servos (PC0, PC1, PD2,..,PD7)

    usart_puts("\fServo test application started...\r\n") ;
    
    while (1) {
        // Saisie de l'id du servo
        while (1) {
            usart_puts("Servo id [0..7] : ") ;
            servo_id = usart_getc() ;
            usart_putc(servo_id) ;
            usart_puts("\r\n") ;
            if ((servo_id >= '0') && (servo_id <= '7')) {
                servo_id -= '0' ;
                break ;
            }
            usart_puts(msgInvalidInput) ;
        }
        // arrivé ici, servo_id contient la valeur numérique de l'id du servo (de 0 à 7)
        
        // Saisie de la valeur de la position
        while (1) {
            usart_puts("Position [0..9, A] : ") ;
            servo_pos = usart_getc() ;
            usart_putc(servo_pos) ;
            usart_puts("\r\n") ;
            if ((servo_pos >= '0') && (servo_pos <= '9')) {
                servo_pos -= '0' ;
                break ;
            }
            if ((servo_pos == 'a') || (servo_pos == 'A')) {
                servo_pos = 10 ;
                break ;
            }
            usart_puts(msgInvalidInput) ;
        }
        // arrivé ici, servo_pos contient la valeur numérique de la position du servo (de 0 à 10)
        
        // Calcul de la largeur d'impulsion :
        /*
            - 0 correspond à la position mini, soit une créneau de 1ms, soit une valeur du comparateur B 
            de 1000    (cf initialisation du timer 1)
            - 10 correspond au maxi, soit un créneau de 2ms, soit un comparateur à 2000
        */
        pulse_widths[servo_id] = 1000 + 100 * servo_pos ;
    }
}