RCTiny

Die RCTiny Platine ist 2009 aus der Platznot im Linde Stapler H50 meines Sohnes Florian entstanden. Um die ganzen gewünschten Sonderfunktionen mit einzelnen Fertigmodulen zu aufzubauen, war einfach nicht genügend Platz.

Aus diesem Grund habe ich mit einem möglichst kleinen ATtiny eine eigene Platine entworfen, die 'alles' erledigt:

  • 7 Kanal rundum Blinklicht
  • Blinker links/rechts
  • Licht
  • Rückfahrpieper
  • Hupe

Um nicht zu viele Steuerkanäle vom Empfänger an den Controller leiten zu müssen, wird das Summensignal des Empfängers ausgewertet. Dadurch wird nur ein Signal benötigt um trotzdem den Rückfahrpieper abhängig von der Fahrrichtung, die Blinker abhängig von der Lenkrichtung, die Schaltkänale für das Blinklicht, die Hupe und das Fahrtlicht einzeln schalten zu können.

Das Rundumlicht ist mit Cu-Lackdraht um einen Zahnstocher gelötet und dann in ein Lichtgehäuse eingebaut.

Die Schaltung habe ich in zwei Versionen mit einer Demo von Altium entwickelt. Der Schaltplan als PDF liegt hier.

Die Platine ist mit einer speziellen Bügelfolie geätzt und dann Heissluft verzinnt.

Die Firmware ist mit einer speziellen Version von GNU-C im AVR-Studio entwickelt und dann mit USBasp geflasht. Sie ist weniger als 2kByte gross, damit sie im ATtiny26 Platz hat.

Der C-Source Code ist hier als Download Die nicht kommerzielle Nutzung der Daten ist gestattet.

/*
 * TinyRC2 I/O using Sum PPM Signal
 *
 * Attiny 26
 *
 * set fuses for 4MHz internal RC Osc
 * avrdude -c USBasp -p t26 -U lfuse:w:0xE3:m -U hfuse:w:0x17:m
 *
 * program flash with
 * avrdude -c USBasp -p t26 -U flash:w:TinyRC2.hex
 *
 * Author: Heinz & Florian Bruederlin
 * 16.02.09 First implemented
 * 01.04.09 Warn implemented
 * 10.04.09 Wink reversed
 *
 * Channels of "webra nano S6"
 *	1 = Lift
 *  2 = Motor
 *  3 = Tip
 *  4 = Steering
 *  5 = Light / Blink    / Both
 *  6 = Horn  / Warnwink / Both
 */
#ifndef F_CPU
#define F_CPU 4000000     /* Clock in HZ */
#endif
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

/* PORTA defines                                        */
/* all anodes of the LEDs at PORTA are connected to VCC */
/* they are switched on with logic 0 value (inverted)   */
#define RC_NOK_OUT  PA0
#define BLINK0_OUT  PA1
#define BLINK1_OUT  PA2
#define BLINK2_OUT  PA3
#define BLINK3_OUT  PA4
#define BLINK4_OUT  PA5
#define BLINK5_OUT  PA6
#define BLINK6_OUT  PA7
#define BLINKX_OUT (_BV(BLINK0_OUT) | _BV(BLINK1_OUT) | \
			        _BV(BLINK2_OUT) | _BV(BLINK3_OUT) | \
			        _BV(BLINK4_OUT) | _BV(BLINK5_OUT) | \
			        _BV(BLINK6_OUT))


/* PORTB defines */
/* all outputs at PORTB are connected to driver transistors */
/* they are switched on with logic 1 value 					*/
#define LIGHT_OUT   PB0
#define LOUDSP_OUT  PB3
#define WINKL_OUT   PB4
#define WINKR_OUT   PB5
#define REC_IN	    PB6

/* RC range defines */
#define RC_MIN_VALUE  64
#define RC_MID_VALUE  98
#define RC_MAX_VALUE 128
#define RC_TOL_VALUE  14	// tolerance for bad value
#define RC_MIN_GAP   160	// GAP must be at least 3msec


/* RC states */
#define RC_WAIT_FOR_START 0
#define RC_TIME_CHANNEL	  1
#define RC_GOT_LONG_GAP	  2
static volatile unsigned char rc_state = RC_WAIT_FOR_START;

/* valid rc values received */
static volatile unsigned char rc_valid = 0;

/* RC channels */
#define RC_CHANNEL_CNT 6
static volatile unsigned char rc_channel = 0;
static volatile unsigned char rc_value[RC_CHANNEL_CNT];

/* RC switch levels */
#define RC_MID_TOL			10 // tolerance around RC_MID_VALUE, which is ignored
#define RC_IS_WINKL		(rc_value[3] > RC_MID_VALUE+RC_MID_TOL)
#define RC_IS_WINKR		(rc_value[3] < RC_MID_VALUE-RC_MID_TOL)

#define RC_IS_BEEP		(rc_value[1] < RC_MID_VALUE-RC_MID_TOL)

#define RC_SWITCH(v)	(((v)-RC_MIN_VALUE)/((RC_MAX_VALUE-RC_MIN_VALUE)/4)) 
												// divide channel into 4 ranges
#define RC_IS_LIGHT		(RC_SWITCH(rc_value[4])==1 || RC_SWITCH(rc_value[4])>=3)
#define RC_IS_BLINK		(RC_SWITCH(rc_value[4])==2 || RC_SWITCH(rc_value[4])>=3)



#define RC_IS_HORN		(RC_SWITCH(rc_value[5])==1 || RC_SWITCH(rc_value[5])>=3)
#define RC_IS_WARN		(RC_SWITCH(rc_value[5])==2 || RC_SWITCH(rc_value[5])>=3)



/* timing stuff */
#define TONE_HIGH_CMP 125						// 4MHz/64/125 = 500Hz
#define TONE_LOW_CMP  250						// 4Mhz/64/250 = 250Hz
#define TONE_HIGH_DIV 50-1 						// 500Hz/50 = 10Hz
#define TONE_LOW_DIV  25-1 						// 250Hz/25 = 10Hz
static volatile unsigned char tone_div = 0;		// current divider
static volatile unsigned char tone_cnt = 0;		// current divider value

#define WINK_FREQ 3-1							// 10/3 = 3.3Hz
static volatile unsigned char wink_cnt = 0;		// current wink value

#define BLINK_FREQ 1-1							// 10/1 = 10Hz
static volatile unsigned char blink_cnt   = 0;	// current blink value
static volatile unsigned char blink_state = 0;	// current blink value

static volatile unsigned char beep_cnt = 0;		// current blink value

/*
 * io_init -
 *	init io ports
 */
void io_init(void) {
	/* switch off LEDs */
	PORTA = _BV(BLINK0_OUT) | _BV(BLINK1_OUT) | _BV(BLINK2_OUT) |
	        _BV(BLINK3_OUT) | _BV(BLINK4_OUT) | _BV(BLINK5_OUT) |
		    _BV(BLINK6_OUT) | _BV(RC_NOK_OUT);
	/* Set PORT A outputs */
	DDRA  = _BV(BLINK0_OUT) | _BV(BLINK1_OUT) | _BV(BLINK2_OUT) |
	        _BV(BLINK3_OUT) | _BV(BLINK4_OUT) | _BV(BLINK5_OUT) |
		    _BV(BLINK6_OUT) | _BV(RC_NOK_OUT);
    
	/* clear outputs and set pullups */
	PORTB = _BV(REC_IN);						// enable pullup for input
												// and switch off LEDs
	/* Set PORT B to outputs */
	DDRB  = _BV(LIGHT_OUT) | _BV(LOUDSP_OUT) |
			_BV(WINKL_OUT) | _BV(WINKR_OUT);
}

/*
 * timer_init -
 *	init timer stuff
 *
 *	prescaler for timer0 is set to 64, which will result in an 
 *	interrupt every 16usec.
 *		 64 ticks are equal to approx. 1msec
 *		128 ticks are equal to approx. 2msec
 *		255 ticks are equal to approx. 4msec 
 *	The absolute maximum range with the FC-16 is 61..134
 *
 *	prescaler for timer1 is set to 64.
 *	in a frequency of 4MHz/64/OCR1C or
 *     OCR1C |   Freq
 *     ------|-------
 *      250  |  250Hz
 *      125  |  500Hz
 *      104  |  601Hz
 *       64  |  976Hz
 *       62  | 1042Hz
 *	     31  | 2016Hz
 */
void timer_init(void) {
    TCCR0 |= _BV(CS01) | _BV(CS00);     // set timer0 prescaler to 64
    TCNT0  = 0;							// clear timer0
	TIFR  |= _BV(TOV0);					// clear pending overflow 

    TCCR1B |= _BV(CS12) | _BV(CS11) | _BV(CS10) | // set timer1 prescaler to 64
			  _BV(CTC1);						  // clear timer1 on cmp match
}

/*
 * tone_set -
 *	set high/low frequency for sound (and timing).
 *	we avoid glitches, by checking if we have already the right settings
 */
void tone_set(unsigned char high) {
	if (high) {
		if (tone_div != TONE_HIGH_DIV) {
			OCR1C    = TONE_HIGH_CMP;
			tone_div = TONE_HIGH_DIV;
		}
	} else {
		if (tone_div != TONE_LOW_DIV) {
			OCR1C    = TONE_LOW_CMP;
			tone_div = TONE_LOW_DIV;
		}
	}
}

/*
 * sound_set -
 *	enable/disable sound output
 *	we check wether the TCCR1A has already the right settings
 *	to avoid gliches, when set/reset multiple times.
 */
void sound_set(unsigned char on) {
	if (on) {
		if (!(TCCR1A & _BV(COM1B0))) {
			TCCR1A |= _BV(COM1B0);
		}
	} else {
		if (TCCR1A & _BV(COM1B0)) {
			TCCR1A &= ~_BV(COM1B0);
			PORTB  &= ~_BV(LOUDSP_OUT); // switch off loudspeaker output
		}
	}
}
   
/*
 * intr_init -
 *	init interrupts
 *	timer1 overflow is used to recognize the end of a PPM frame after 3+n*4msec
 *	the external interrupt INT0 will be used to recognize the
 *	falling edge of the PPM channels:
 *		    _     _      _             _
 *         | |   | |    | |           | |
 *   ______| |___| |____| |___________| |_
 *           <-----><-----><------------><--..
 *	         CH1    CH2     Gap          CH1
 *           1..2ms 1..2ms  ~4..12ms
 */
void intr_init(void) {
    
    TIMSK |= _BV(TOIE0) | // enable timer0 overflow interrupt
			 _BV(OCIE1B); // enable timer1 output cmp interrupt

    //falling edge generates an interrupt
    MCUCR |= _BV(ISC01);

    // enable external interrupt on PB6 (REC_IN)
    GIMSK |= _BV(INT0);

	// enable interupts
	sei();
}

/*
 * TIMER0_OVF0_vect -
 *	timer 0 overflow interrupt handler
 *	if the timer overflows while measuring a channel we wait for a new start.
 *	if we are waiting for start, the gap is longer than 4msec.
 */
ISR(TIMER0_OVF0_vect) {
	switch (rc_state) {
		case RC_WAIT_FOR_START:
			rc_state = RC_GOT_LONG_GAP;
			break;
		case RC_GOT_LONG_GAP:
			/* long gap gets even longer */;
			break;
		case RC_TIME_CHANNEL:
			if (rc_channel < RC_CHANNEL_CNT) {
				rc_valid = 0;
				rc_state = RC_WAIT_FOR_START;
			} else {
				rc_state = RC_GOT_LONG_GAP;
			}
			break;
	}
}

/*
 * INT0_vect -
 *	external interrupt 0
 *	called on each falling edge of PB6 (REC_IN)
 *	because we have only a 8bit timer, we recognize
 *  the start of a frame, if there was an overflow, or 
 *	the start gap is at least RC_MIN_GAP long.
 *  A valid channel value must be between RC_MIN_VALUE and RC_MAX_VALUE.
 *  In all other cases (too short start, or invalid channel value)
 *	we go to the RC_WAIT_FOR_START state.
 *  A valid frame is signaled by setting rc_value to 1, if we have
 *	received RC_CHANNEL_CNT valid values and got a long enough start gap.
 */
ISR(INT0_vect) {
	unsigned char value = TCNT0; // store width of this channel

    TCNT0 = 0;		   // start measurement of next channel
	TIFR |= _BV(TOV0); // clear Overflow which occured while we are in 
					   // in this interrupt with higher prio.
	switch (rc_state) {
		case RC_WAIT_FOR_START:
		case RC_GOT_LONG_GAP:
			if (value > RC_MIN_GAP || rc_state==RC_GOT_LONG_GAP) {
				if (rc_channel>=RC_CHANNEL_CNT) rc_valid = 1;
				rc_channel = 0;
				rc_state   = RC_TIME_CHANNEL;
			}
			break;
		case RC_TIME_CHANNEL:
			if (value < RC_MIN_VALUE-RC_TOL_VALUE ||
				value > RC_MAX_VALUE+RC_TOL_VALUE) {
				rc_valid = 0;
				rc_state = RC_WAIT_FOR_START;
			} else if (rc_channel < RC_CHANNEL_CNT) {
				rc_value[rc_channel] = value;
			}
			rc_channel++;
			break;
	}
}

/*
 * TIMER1_CMPB_vect -
 *	timer1 controls the loudspeaker
 *	with one of two OCR settings: TONE_HIGH_CMP and TONE_LOW_CMP
 *	is runs with 4MHz/64/TONE_HIGH_CMP or 4Mhz/64/TONE_LOW_CMP.
 *	This interrupt divides this by TONE_HIGH_DIV or
 *	TONE_LOW_DIV depending on variable tone_high,
 * 	to get a fixed 10Hz clock.
 *
 *	This 10Hz is used for the timing of Blinking and other stuff.
 */
ISR(TIMER1_CMPB_vect) {
	if (tone_cnt) {
		tone_cnt--;
		return;
	}
	tone_cnt = tone_div;
	// the following code will run with 10Hz

	if (!rc_valid) return;

	// Winker Code
	if (wink_cnt) {
		wink_cnt--;
	} else {
		wink_cnt = WINK_FREQ;
		if (RC_IS_WINKR || RC_IS_WARN) {
			PORTB ^= _BV(WINKR_OUT);
		} else {
			PORTB &= ~_BV(WINKR_OUT);
		}
		if (RC_IS_WINKL || RC_IS_WARN) {
			PORTB ^= _BV(WINKL_OUT);
		} else {
			PORTB &= ~_BV(WINKL_OUT);
		}
	}

	// Blinker code
	if (blink_cnt) {
		blink_cnt--;
	} else {
		blink_cnt = BLINK_FREQ;
		if (RC_IS_BLINK) {
			unsigned char b;
			blink_state++;
			if (blink_state>6) blink_state=0;
			PORTA |= BLINKX_OUT; // all off
			switch (blink_state) {
				case 0:  b = (unsigned char)(~_BV(BLINK0_OUT)); break;
				case 1:  b = (unsigned char)(~_BV(BLINK1_OUT)); break;
				case 2:  b = (unsigned char)(~_BV(BLINK2_OUT)); break;
				case 3:  b = (unsigned char)(~_BV(BLINK3_OUT)); break;
				case 4:  b = (unsigned char)(~_BV(BLINK4_OUT)); break;
				case 5:  b = (unsigned char)(~_BV(BLINK5_OUT)); break;
				default: b = (unsigned char)(~_BV(BLINK6_OUT)); break;
			}
			PORTA = (PORTA | BLINKX_OUT) & b;
		} else {
			PORTA |= BLINKX_OUT; // all off
		}
	}

	// beeper code
	if (RC_IS_BEEP && !RC_IS_HORN) {
		switch (beep_cnt) {
			case 0: sound_set(1);
					beep_cnt++;
					break;
			case 1: 
			case 2: beep_cnt++;
					break;
			case 3: sound_set(0);
					beep_cnt++;
					break;
			case 4: beep_cnt=0;
					break;
		}
	} else {
		sound_set(0);
	}
}

/*
 * main -
 *	init everthing and go to endlees loop
 */
int main(void) {
	io_init();
	timer_init();
	intr_init();

	for(;;) {
		if (rc_valid) {
			PORTA |= _BV(RC_NOK_OUT);	// rc not ok LED off
			if (RC_IS_HORN) {
				tone_set(0);	// low frequence sound
				sound_set(1);	// enable loudspeaker
			} else {
				tone_set(1);	// high frequence sound for beep and timing
				if (!RC_IS_BEEP) {
					sound_set(0); // disable loudspeaker if not backward beeping
				}
			}
			if (RC_IS_LIGHT) {
				PORTB |= _BV(LIGHT_OUT);	// light LEDs on
			} else {
				PORTB &= ~_BV(LIGHT_OUT);	// light LEDs off
			}
		} else {
			PORTA &= ~_BV(RC_NOK_OUT);	// rc not ok LED on
			PORTA |= BLINKX_OUT;		// blink LEDs off
			PORTB &= ~_BV(LIGHT_OUT) &	// light LED off
					 ~_BV(WINKR_OUT) &  // right winker off
					 ~_BV(WINKL_OUT);   // left winker off
			sound_set(0);				// disable loudspeaker
		}
	}
}