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
546 lines
12 KiB
C
4 years ago
|
/*
|
||
|
* ----------------------------------------------------------------------------
|
||
|
* "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();
|
||
|
}
|
||
|
}
|