Man Linux: Main Page and Category List

NAME

       A more sophisticated project - This project extends the basic idea of
       the simple project to control a LED with a PWM output, but adds methods
       to adjust the LED brightness. It employs a lot of the basic concepts of
       avr-libc to achieve that goal.

       Understanding this project assumes the simple project has been
       understood in full, as well as being acquainted with the basic hardware
       concepts of an AVR microcontroller.

Hardware setup

       The demo is set up in a way so it can be run on the ATmega16 that ships
       with the STK500 development kit. The only external part needed is a
       potentiometer attached to the ADC. It is connected to a 10-pin ribbon
       cable for port A, both ends of the potentiometer to pins 9 (GND) and 10
       (VCC), and the wiper to pin 1 (port A0). A bypass capacitor from pin 1
       to pin 9 (like 47 nF) is recommendable.

       Setup of the STK500Setup of the STK500

       The coloured patch cables are used to provide various interconnections.
       As there are only four of them in the STK500, there are two options to
       connect them for this demo. The second option for the yellow-green
       cable is shown in parenthesis in the table. Alternatively, the 'squid'
       cable from the JTAG ICE kit can be used if available.

       PortHeaderColorFunctionConnect to D0 1 brown RxD RXD of the RS-232
       header D1 2 grey TxD TXD of the RS-232 header D2 3 black button 'down'
       SW0 (pin 1 switches header) D3 4 red button 'up' SW1 (pin 2 switches
       header) D4 5 green button 'ADC' SW2 (pin 3 switches header) D5 6 blue
       LED LED0 (pin 1 LEDs header) D6 7 (green)clock out LED1 (pin 2 LEDs
       header) D7 8 white 1-second flashLED2 (pin 3 LEDs header) GND9 unused
       VCC10unused

       Wiring of the STK500Wiring of the STK500

       The following picture shows the alternate wiring where LED1 is
       connected but SW2 is not:

       Wiring option #2 of the STK500Wiring option #2 of the STK500

       As an alternative, this demo can also be run on the popular ATmega8
       controller, or its successor ATmega88 as well as the ATmega48 and
       ATmega168 variants of the latter. These controllers do not have a port
       named 'A', so their ADC inputs are located on port C instead, thus the
       potentiometer needs to be attached to port C. Likewise, the OC1A output
       is not on port D pin 5 but on port B pin 1 (PB1). Thus, the above
       cabling scheme needs to be changed so that PB1 connects to the LED0
       pin. (PD6 remains unconnected.) When using the STK500, use one of the
       jumper cables for this connection. All other port D pins should be
       connected the same way as described for the ATmega16 above.

       When not using an STK500 starter kit, attach the LEDs through some
       resistor to Vcc (low-active LEDs), and attach pushbuttons from the
       respective input pins to GND. The internal pull-up resistors are
       enabled for the pushbutton pins, so no external resistors are needed.

       Finally, the demo has been ported to the ATtiny2313 as well. As this
       AVR does not offer an ADC, everything related to handling the ADC is
       disabled in the code for that MCU type. Also, port D of this controller
       type only features 6 pins, so the 1-second flash LED had to be moved
       from PD6 to PD4. (PD4 is used as the ADC control button on the other
       MCU types, but that is not needed here.) OC1A is located at PB3 on this
       device.

       The MCU_TARGET macro in the Makefile needs to be adjusted appropriately
       for the alternative controller types.

       The flash ROM and RAM consumption of this demo are way below the
       resources of even an ATmega48, and still well within the capabilities
       of an ATtiny2313. The major advantage of experimenting with the
       ATmega16 (in addition that it ships together with an STK500 anyway) is
       that it can be debugged online via JTAG. Likewise, the ATmega48/88/168
       and ATtiny2313 devices can be debugged through debugWire, using the
       Atmel JTAG ICE mkII or the low-cost AVR Dragon.

       Note that in the explanation below, all port/pin names are applicable
       to the ATmega16 setup.

Functional overview

       PD6 will be toggled with each internal clock tick (approx. 10 ms). PD7
       will flash once per second.

       PD0 and PD1 are configured as UART IO, and can be used to connect the
       demo kit to a PC (9600 Bd, 8N1 frame format). The demo application
       talks to the serial port, and it can be controlled from the serial
       port.

       PD2 through PD4 are configured as inputs, and control the application
       unless control has been taken over by the serial port. Shorting PD2 to
       GND will decrease the current PWM value, shorting PD3 to GND will
       increase it.

       While PD4 is shorted to GND, one ADC conversion for channel 0 (ADC
       input is on PA0) will be triggered each internal clock tick, and the
       resulting value will be used as the PWM value. So the brightness of the
       LED follows the analog input value on PC0. VAREF on the STK500 should
       be set to the same value as VCC.

       When running in serial control mode, the function of the watchdog timer
       can be demonstrated by typing an `r'. This will make the demo
       application run in a tight loop without retriggering the watchdog so
       after some seconds, the watchdog will reset the MCU. This situation can
       be figured out on startup by reading the MCUCSR register.

       The current value of the PWM is backed up in an EEPROM cell after about
       3 seconds of idle time after the last change. If that EEPROM cell
       contains a reasonable (i. e. non-erased) value at startup, it is taken
       as the initial value for the PWM. This virtually preserves the last
       value across power cycles. By not updating the EEPROM immmediately but
       only after a timeout, EEPROM wear is reduced considerably compared to
       immediately writing the value at each change.

A code walkthrough

       This section explains the ideas behind individual parts of the code.
       The source code has been divided into numbered parts, and the following
       subsections explain each of these parts.

   Part 1: Macro definitions
       A number of preprocessor macros are defined to improve readability
       and/or portability of the application.

       The first macros describe the IO pins our LEDs and pushbuttons are
       connected to. This provides some kind of mini-HAL (hardware abstraction
       layer) so should some of the connections be changed, they don't need to
       be changed inside the code but only on top. Note that the location of
       the PWM output itself is mandated by the hardware, so it cannot be
       easily changed. As the ATmega48/88/168 controllers belong to a more
       recent generation of AVRs, a number of register and bit names have been
       changed there, so they are mapped back to their ATmega8/16 equivalents
       to keep the actual program code portable.

       The name F_CPU is the conventional name to describe the CPU clock
       frequency of the controller. This demo project just uses the internal
       calibrated 1 MHz RC oscillator that is enabled by default. Note that
       when using the <util/delay.h> functions, F_CPU needs to be defined
       before including that file.

       The remaining macros have their own comments in the source code. The
       macro TMR1_SCALE shows how to use the preprocessor and the compiler's
       constant expression computation to calculate the value of timer 1's
       post-scaler in a way so it only depends on F_CPU and the desired
       software clock frequency. While the formula looks a bit complicated,
       using a macro offers the advantage that the application will
       automatically scale to new target softclock or master CPU frequencies
       without having to manually re-calculate hardcoded constants.

   Part 2: Variable definitions
       The intflags structure demonstrates a way to allocate bit variables in
       memory. Each of the interrupt service routines just sets one bit within
       that structure, and the application's main loop then monitors the bits
       in order to act appropriately.

       Like all variables that are used to communicate values between an
       interrupt service routine and the main application, it is declared
       volatile.

       The variable ee_pwm is not a variable in the classical C sense that
       could be used as an lvalue or within an expression to obtain its value.
       Instead, the

        __attribute__((section('.eeprom')))

       marks it as belonging to the EEPROM section. This section is merely
       used as a placeholder so the compiler can arrange for each individual
       variable's location in EEPROM. The compiler will also keep track of
       initial values assigned, and usually the Makefile is arranged to
       extract these initial values into a separate load file
       (largedemo_eeprom.* in this case) that can be used to initialize the
       EEPROM.

       The actual EEPROM IO must be performed manually.

       Similarly, the variable mcucsr is kept in the .noinit section in order
       to prevent it from being cleared upon application startup.

   Part 3: Interrupt service routines
       The ISR to handle timer 1's overflow interrupt arranges for the
       software clock. While timer 1 runs the PWM, it calls its overflow
       handler rather frequently, so the TMR1_SCALE value is used as a
       postscaler to reduce the internal software clock frequency further. If
       the software clock triggers, it sets the tmr_int bitfield, and defers
       all further tasks to the main loop.

       The ADC ISR just fetches the value from the ADC conversion, disables
       the ADC interrupt again, and announces the presence of the new value in
       the adc_int bitfield. The interrupt is kept disabled while not needed,
       because the ADC will also be triggered by executing the SLEEP
       instruction in idle mode (which is the default sleep mode). Another
       option would be to turn off the ADC completely here, but that increases
       the ADC's startup time (not that it would matter much for this
       application).

   Part 4: Auxiliary functions
       The function handle_mcucsr() uses two __attribute__ declarators to
       achieve specific goals. First, it will instruct the compiler to place
       the generated code into the .init3 section of the output. Thus, it will
       become part of the application initialization sequence. This is done in
       order to fetch (and clear) the reason of the last hardware reset from
       MCUCSR as early as possible. There is a short period of time where the
       next reset could already trigger before the current reason has been
       evaluated. This also explains why the variable mcucsr that mirrors the
       register's value needs to be placed into the .noinit section, because
       otherwise the default initialization (which happens after .init3) would
       blank the value again.

       As the initialization code is not called using CALL/RET instructions
       but rather concatenated together, the compiler needs to be instructed
       to omit the entire function prologue and epilogue. This is performed by
       the naked attribute. So while syntactically, handle_mcucsr() is a
       function to the compiler, the compiler will just emit the instructions
       for it without setting up any stack frame, and not even a RET
       instruction at the end.

       Function ioinit() centralizes all hardware setup. The very last part of
       that function demonstrates the use of the EEPROM variable ee_pwm to
       obtain an EEPROM address that can in turn be applied as an argument to
       eeprom_read_word().

       The following functions handle UART character and string output. (UART
       input is handled by an ISR.) There are two string output functions,
       printstr() and printstr_p(). The latter function fetches the string
       from program memory. Both functions translate a newline character into
       a carriage return/newline sequence, so a simple \n can be used in the
       source code.

       The function set_pwm() propagates the new PWM value to the PWM,
       performing range checking. When the value has been changed, the new
       percentage will be announced on the serial link. The current value is
       mirrored in the variable pwm so others can use it in calculations. In
       order to allow for a simple calculation of a percentage value without
       requiring floating-point mathematics, the maximal value of the PWM is
       restricted to 1000 rather than 1023, so a simple division by 10 can be
       used. Due to the nature of the human eye, the difference in LED
       brightness between 1000 and 1023 is not noticable anyway.

   Part 5: main()
       At the start of main(), a variable mode is declared to keep the current
       mode of operation. An enumeration is used to improve the readability.
       By default, the compiler would allocate a variable of type int for an
       enumeration. The packed attribute declarator instructs the compiler to
       use the smallest possible integer type (which would be an 8-bit type
       here).

       After some initialization actions, the application's main loop follows.
       In an embedded application, this is normally an infinite loop as there
       is nothing an application could 'exit' into anyway.

       At the beginning of the loop, the watchdog timer will be retriggered.
       If that timer is not triggered for about 2 seconds, it will issue a
       hardware reset. Care needs to be taken that no code path blocks longer
       than this, or it needs to frequently perform watchdog resets of its
       own. An example of such a code path would be the string IO functions:
       for an overly large string to print (about 2000 characters at 9600 Bd),
       they might block for too long.

       The loop itself then acts on the interrupt indication bitfields as
       appropriate, and will eventually put the CPU on sleep at its end to
       conserve power.

       The first interrupt bit that is handled is the (software) timer, at a
       frequency of approximately 100 Hz. The CLOCKOUT pin will be toggled
       here, so e. g. an oscilloscope can be used on that pin to measure the
       accuracy of our software clock. Then, the LED flasher for LED2 ('We are
       alive'-LED) is built. It will flash that LED for about 50 ms, and pause
       it for another 950 ms. Various actions depending on the operation mode
       follow. Finally, the 3-second backup timer is implemented that will
       write the PWM value back to EEPROM once it is not changing anymore.

       The ADC interrupt will just adjust the PWM value only.

       Finally, the UART Rx interrupt will dispatch on the last character
       received from the UART.

       All the string literals that are used as informational messages within
       main() are placed in program memory so no SRAM needs to be allocated
       for them. This is done by using the PSTR macro, and passing the string
       to printstr_p().

The source code

Author

       Generated automatically by Doxygen for avr-libc from the source code.