You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

546 lines
12 KiB
C

/*
* ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):
* <joerg@FreeBSD.ORG> wrote this file. As long as you retain this notice you
* can do whatever you want with this stuff. If we meet some day, and you think
* this stuff is worth it, you can buy me a beer in return. Joerg Wunsch
* ----------------------------------------------------------------------------
*
* More advanced AVR demonstration. Controls a LED attached to OCR1A.
* The brightness of the LED is controlled with the PWM. A number of
* methods are implemented to control that PWM.
*
* $Id$
*/
#include <stdint.h>
#include <stdlib.h>
#include <avr/eeprom.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
/* Part 1: Macro definitions */
#define CONTROL_PORT PORTD
#define CONTROL_DDR DDRD
#if defined(__AVR_ATtiny2313__)
/* no PD7 and no ADC available on ATtiny2313 */
# define TRIGGER_DOWN PD2
# define TRIGGER_UP PD3
# define FLASH PD4
# define CLOCKOUT PD6
#else
# define TRIGGER_DOWN PD2
# define TRIGGER_UP PD3
# define TRIGGER_ADC PD4
# define CLOCKOUT PD6
# define FLASH PD7
#endif
#if defined(__AVR_ATmega16__)
# define PWMDDR DDRD
# define PWMOUT PD5
#elif defined(__AVR_ATmega8__) || defined(__AVR_ATmega48__) ||\
defined(__AVR_ATmega88__) || defined(__AVR_ATmega168__)
# define PWMDDR DDRB
# define PWMOUT PB1
#elif defined(__AVR_ATtiny2313__)
# define PWMDDR DDRB
# define PWMOUT PB3
# define HAVE_ADC 0
# define USART_RXC_vect USART_RX_vect
# define MCUCSR MCUSR
#else
# error "Unsupported MCU type"
#endif
#if defined(__AVR_ATmega48__) || defined(__AVR_ATmega88__) ||\
defined(__AVR_ATmega168__)
/* map ATmega8/16 names to ATmegaX8 names */
# define USART_RXC_vect USART_RX_vect
# define UDR UDR0
# define UCSRA UCSR0A
# define UCSRB UCSR0B
# define FE FE0
# define TXEN TXEN0
# define RXEN RXEN0
# define RXCIE RXCIE0
# define UDRE UDRE0
# define U2X U2X0
# define UBRRL UBRR0L
# define TIMSK TIMSK1
# define MCUCSR MCUSR
#endif
#if !defined(HAVE_ADC)
# define HAVE_ADC 1
#endif
#define F_CPU 1000000UL /* CPU clock in Hertz */
#define SOFTCLOCK_FREQ 100 /* internal software clock */
/*
* Timeout to wait after last PWM change till updating the EEPROM.
* Measured in internal clock ticks (approx. 100 Hz).
*/
#define EE_UPDATE_TIME (3 * SOFTCLOCK_FREQ) /* ca. 3 seconds */
/*
* Timer1 overflow interrupt will be called with F_CPU / 2048
* frequency. This interrupt routine further divides that value,
* resulting in an internal update interval of approx. 10 ms.
* (The complicated looking scaling by 10 / addition of 9 is
* poor man's fixed-point rounding algorithm...)
*/
#define TMR1_SCALE ((F_CPU * 10) / (2048UL * SOFTCLOCK_FREQ) + 9) / 10
/* Part 2: Variable definitions */
/*
* Bits that are set inside interrupt routines, and watched outside in
* the program's main loop.
*/
volatile struct
{
uint8_t tmr_int: 1;
uint8_t adc_int: 1;
uint8_t rx_int: 1;
}
intflags;
/*
* Last character read from the UART.
*/
volatile char rxbuff;
/*
* Last value read from ADC.
*/
volatile uint16_t adcval;
/*
* Where to store the PWM value in EEPROM. This is used in order
* to remember the value across a RESET or power cycle.
*/
uint16_t ee_pwm __attribute__((section(".eeprom"))) = 42;
/*
* Current value of the PWM.
*/
int16_t pwm;
/*
* EEPROM backup timer. Bumped by the PWM update routine. If it
* expires, the current PWM value will be written to EEPROM.
*/
int16_t pwm_backup_tmr;
/*
* Mirror of the MCUCSR register, taken early during startup.
*/
uint8_t mcucsr __attribute__((section(".noinit")));
/* Part 3: Interrupt service routines */
ISR(TIMER1_OVF_vect)
{
static uint8_t scaler = TMR1_SCALE;
if (--scaler == 0)
{
scaler = TMR1_SCALE;
intflags.tmr_int = 1;
}
}
#if HAVE_ADC
/*
* ADC conversion complete. Fetch the 10-bit value, and feed the
* PWM with it.
*/
ISR(ADC_vect)
{
adcval = ADCW;
ADCSRA &= ~_BV(ADIE); /* disable ADC interrupt */
intflags.adc_int = 1;
}
#endif /* HAVE_ADC */
/*
* UART receive interrupt. Fetch the character received and buffer
* it, unless there was a framing error. Note that the main loop
* checks the received character only once per 10 ms.
*/
ISR(USART_RXC_vect)
{
uint8_t c;
c = UDR;
if (bit_is_clear(UCSRA, FE))
{
rxbuff = c;
intflags.rx_int = 1;
}
}
/* Part 4: Auxiliary functions */
/*
* Read out and reset MCUCSR early during startup.
*/
void handle_mcucsr(void)
__attribute__((section(".init3")))
__attribute__((naked));
void handle_mcucsr(void)
{
mcucsr = MCUCSR;
MCUCSR = 0;
}
/*
* Do all the startup-time peripheral initializations.
*/
static void
ioinit(void)
{
uint16_t pwm_from_eeprom;
/*
* Set up the 16-bit timer 1.
*
* Timer 1 will be set up as a 10-bit phase-correct PWM (WGM10 and
* WGM11 bits), with OC1A used as PWM output. OC1A will be set when
* up-counting, and cleared when down-counting (COM1A1|COM1A0), this
* matches the behaviour needed by the STK500's low-active LEDs.
* The timer will runn on full MCU clock (1 MHz, CS10 in TCCR1B).
*/
TCCR1A = _BV(WGM10) | _BV(WGM11) | _BV(COM1A1) | _BV(COM1A0);
TCCR1B = _BV(CS10);
OCR1A = 0; /* set PWM value to 0 */
/* enable pull-ups for pushbuttons */
#if HAVE_ADC
CONTROL_PORT = _BV(TRIGGER_DOWN) | _BV(TRIGGER_UP) | _BV(TRIGGER_ADC);
#else
CONTROL_PORT = _BV(TRIGGER_DOWN) | _BV(TRIGGER_UP);
#endif
/*
* Enable Port D outputs: PD6 for the clock output, PD7 for the LED
* flasher. PD1 is UART TxD but not DDRD setting is provided for
* that, as enabling the UART transmitter will automatically turn
* this pin into an output.
*/
CONTROL_DDR = _BV(CLOCKOUT) | _BV(FLASH);
/*
* As the location of OC1A differs between supported MCU types, we
* enable that output separately here. Note that the DDRx register
* *might* be the same as CONTROL_DDR above, so make sure to not
* clobber it.
*/
PWMDDR |= _BV(PWMOUT);
UCSRA = _BV(U2X); /* improves baud rate error @ F_CPU = 1 MHz */
UCSRB = _BV(TXEN)|_BV(RXEN)|_BV(RXCIE); /* tx/rx enable, rx complete intr */
UBRRL = (F_CPU / (8 * 9600UL)) - 1; /* 9600 Bd */
#if HAVE_ADC
/*
* enable ADC, select ADC clock = F_CPU / 8 (i.e. 125 kHz)
*/
ADCSRA = _BV(ADEN) | _BV(ADPS1) | _BV(ADPS0);
#endif
TIMSK = _BV(TOIE1);
sei(); /* enable interrupts */
/*
* Enable the watchdog with the largest prescaler. Will cause a
* watchdog reset after approximately 2 s @ Vcc = 5 V
*/
wdt_enable(WDTO_2S);
/*
* Read the value from EEPROM. If it is not 0xffff (erased cells),
* use it as the starting value for the PWM.
*/
if ((pwm_from_eeprom = eeprom_read_word(&ee_pwm)) != 0xffff)
OCR1A = (pwm = pwm_from_eeprom);
}
/*
* Some simple UART IO functions.
*/
/*
* Send character c down the UART Tx, wait until tx holding register
* is empty.
*/
static void
putchr(char c)
{
loop_until_bit_is_set(UCSRA, UDRE);
UDR = c;
}
/*
* Send a C (NUL-terminated) string down the UART Tx.
*/
static void
printstr(const char *s)
{
while (*s)
{
if (*s == '\n')
putchr('\r');
putchr(*s++);
}
}
/*
* Same as above, but the string is located in program memory,
* so "lpm" instructions are needed to fetch it.
*/
static void
printstr_p(const char *s)
{
char c;
for (c = pgm_read_byte(s); c; ++s, c = pgm_read_byte(s))
{
if (c == '\n')
putchr('\r');
putchr(c);
}
}
/*
* Update the PWM value. If it has changed, send the new value down
* the serial line.
*/
static void
set_pwm(int16_t new)
{
char s[8];
if (new < 0)
new = 0;
else if (new > 1000)
new = 1000;
if (new != pwm)
{
OCR1A = (pwm = new);
/*
* Calculate a "percentage". We just divide by 10, as we
* limited the max value of the PWM to 1000 above.
*/
new /= 10;
itoa(new, s, 10);
printstr(s);
putchr(' ');
pwm_backup_tmr = EE_UPDATE_TIME;
}
}
/* Part 5: main() */
int
main(void)
{
/*
* Our modus of operation. MODE_UPDOWN means we watch out for
* either PD2 or PD3 being low, and increase or decrease the
* PWM value accordingly. This is the default.
* MODE_ADC means the PWM value follows the value of ADC0 (PA0).
* This is enabled by applying low level to PC1.
* MODE_SERIAL means we get commands via the UART. This is
* enabled by sending a valid V.24 character at 9600 Bd to the
* UART.
*/
enum
{
MODE_UPDOWN,
MODE_ADC,
MODE_SERIAL
} __attribute__((packed)) mode = MODE_UPDOWN;
uint8_t flash = 0;
ioinit();
if ((mcucsr & _BV(WDRF)) == _BV(WDRF))
printstr_p(PSTR("\nOoops, the watchdog bit me!"));
printstr_p(PSTR("\nHello, this is the avr-gcc/libc "
"demo running on an "
#if defined(__AVR_ATmega16__)
"ATmega16"
#elif defined(__AVR_ATmega8__)
"ATmega8"
#elif defined(__AVR_ATmega48__)
"ATmega48"
#elif defined(__AVR_ATmega88__)
"ATmega88"
#elif defined(__AVR_ATmega168__)
"ATmega168"
#elif defined(__AVR_ATtiny2313__)
"ATtiny2313"
#else
"unknown AVR"
#endif
"\n"));
for (;;)
{
wdt_reset();
if (intflags.tmr_int)
{
/*
* Our periodic 10 ms interrupt happened. See what we can
* do about it.
*/
intflags.tmr_int = 0;
/*
* toggle PD6, just to show the internal clock; should
* yield ~ 48 Hz on PD6
*/
CONTROL_PORT ^= _BV(CLOCKOUT);
/*
* flash LED on PD7, approximately once per second
*/
flash++;
if (flash == 5)
CONTROL_PORT |= _BV(FLASH);
else if (flash == 100)
{
flash = 0;
CONTROL_PORT &= ~_BV(FLASH);
}
switch (mode)
{
case MODE_SERIAL:
/*
* In serial mode, there's nothing to do anymore here.
*/
break;
case MODE_UPDOWN:
/*
* Query the pushbuttons.
*
* NB: watch out to use PINx for reading, as opposed
* to using PORTx which would be the mirror of the
* _output_ latch register (resp. pullup configuration
* bit for input pins)!
*/
if (bit_is_clear(PIND, TRIGGER_DOWN))
set_pwm(pwm - 10);
else if (bit_is_clear(PIND, TRIGGER_UP))
set_pwm(pwm + 10);
#if HAVE_ADC
else if (bit_is_clear(PIND, TRIGGER_ADC))
mode = MODE_ADC;
#endif
break;
case MODE_ADC:
#if HAVE_ADC
if (bit_is_set(PIND, TRIGGER_ADC))
mode = MODE_UPDOWN;
else
{
/*
* Start one conversion.
*/
ADCSRA |= _BV(ADIE);
ADCSRA |= _BV(ADSC);
}
#endif /* HAVE_ADC */
break;
}
if (pwm_backup_tmr && --pwm_backup_tmr == 0)
{
/*
* The EEPROM backup timer expired. Save the current
* PWM value in EEPROM. Note that this function might
* block for a few milliseconds (after writing the
* first byte).
*/
eeprom_write_word(&ee_pwm, pwm);
printstr_p(PSTR("[EEPROM updated] "));
}
}
#if HAVE_ADC
if (intflags.adc_int)
{
intflags.adc_int = 0;
set_pwm(adcval);
}
#endif /* HAVE_ADC */
if (intflags.rx_int)
{
intflags.rx_int = 0;
if (rxbuff == 'q')
{
printstr_p(PSTR("\nThank you for using serial mode."
" Good-bye!\n"));
mode = MODE_UPDOWN;
}
else
{
if (mode != MODE_SERIAL)
{
printstr_p(PSTR("\nWelcome at serial control, "
"type +/- to adjust, or 0/1 to turn on/off\n"
"the LED, q to quit serial mode, "
"r to demonstrate a watchdog reset\n"));
mode = MODE_SERIAL;
}
switch (rxbuff)
{
case '+':
set_pwm(pwm + 10);
break;
case '-':
set_pwm(pwm - 10);
break;
case '0':
set_pwm(0);
break;
case '1':
set_pwm(1000);
break;
case 'r':
printstr_p(PSTR("\nzzzz... zzz..."));
for (;;)
;
}
}
}
sleep_mode();
}
}