largedemo(3)

πŸ“¦ version 2.2.1, in fedora 43, fedora 44, opensuse-leap 16.0

largedemo(3) πŸ’–

okay so this is the largedemo page from the avr-libc manual, bestie. It's part 1, and more parts are coming, so don't go anywhere. πŸŽ€βœ¨


🌸 NAME 🌸

largedemo β€” that's the name, periodt. πŸ’…


✨ SYNOPSIS ✨

Detailed Description

okay so this project takes the basic idea from the simple project β€” controlling a LED with a PWM output β€” and then levels it all the way up with ways to adjust the LED brightness. It uses a whole bunch of the basic concepts from avr-libc to pull that off. no cap. πŸ’…

To really get this project, you're gonna need the simple project fully understood first, plus you should be familiar with the basic hardware concepts of an AVR microcontroller. iykyk.


πŸ¦‹ Hardware setup πŸ¦‹

This demo is set up so it can run on the ATmega16 that comes with the STK500 development kit. The only external part you need is a potentiometer hooked up to the ADC. It connects to a 10-pin ribbon cable for port A: both ends of the potentiometer go to pins 9 (GND) and 10 (VCC), and the wiper goes 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 the ones doing all the interconnecting. Since the STK500 only has four of them, there are two ways to connect them for this demo. The second option for the yellow-green cable is shown in parenthesis in the table. Or, if you have it, you can use the 'squid' cable from the JTAG ICE kit instead.

  • D0 β€” header 1, color brown, function RxD, connect to RXD of the RS-232 header.
  • D1 β€” header 2, color grey, function TxD, connect to TXD of the RS-232 header.
  • D2 β€” header 3, color black, function button 'down', connect to SW0 (pin 1 switches header).
  • D3 β€” header 4, color red, function button 'up', connect to SW1 (pin 2 switches header).
  • D4 β€” header 5, color green, function button 'ADC', connect to SW2 (pin 3 switches header).
  • D5 β€” header 6, color blue, function LED, connect to LED0 (pin 1 LEDs header).
  • D6 β€” header 7, color yellow-green ((green) as the second option shown in parenthesis), function clock out, connect to LED1 (pin 2 LEDs header).
  • D7 β€” header 8, color white, function 1-second flash, connect to LED2 (pin 3 LEDs header).
  • GND β€” header 9, unused.
  • VCC β€” header 10, unused.

Wiring of the STK500Wiring of the STK500

The picture below 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 run on the popular ATmega8 controller, or its successor ATmega88, plus the ATmega48 and ATmega168 variants of the latter. These controllers don't have a port named 'A', so their ADC inputs live on port C instead β€” meaning the potentiometer needs to be attached to port C. Same energy with OC1A: it's not on port D pin 5, it's on port B pin 1 (PB1). So the cabling scheme above needs a glow-up: PB1 connects to the LED0 pin. (PD6 stays unconnected.) When you're using the STK500, grab one of the jumper cables for this connection. All the other port D pins should be connected exactly like described for the ATmega16 above.

If you're 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 already enabled for the pushbutton pins, so no external resistors are needed. slay.

Finally, the demo has been ported to the ATtiny2313 too. This AVR doesn't offer an ADC, so everything related to handling the ADC is disabled in the code for that MCU type. Also, port D on this controller type only has 6 pins, so the 1-second flash LED had to move from PD6 to PD4. (PD4 is used as the ADC control button on the other MCU types, but that's 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. don't forget, bestie.

The flash ROM and RAM consumption of this demo are way below the resources of even an ATmega48, and still well within what an ATtiny2313 can handle. The major advantage of experimenting with the ATmega16 (besides the fact 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. iconic.

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 they can 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 too.

PD2 through PD4 are configured as inputs, and they control the application unless the serial port has taken over. Shorting PD2 to GND will decrease the current PWM value, and 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 you're running in serial control mode, you can show off the watchdog timer by typing an r. This makes the demo application run in a tight loop without retriggering the watchdog, so after some seconds, the watchdog will reset the MCU. You can figure this situation out on startup by reading the MCUCSR register.

The current PWM value gets backed up into an EEPROM cell after about 3 seconds of idle time following the last change. If that EEPROM cell holds a reasonable (i. e. non-erased) value at startup, it becomes the initial value for the PWM. This basically keeps the last value alive across power cycles. And because the EEPROM isn't updated immmediately β€” only after a timeout β€” EEPROM wear is reduced considerably compared to immediately writing the value at each change. ✨

✨ A code walkthrough ✨

okay bestie, this is the fun part β€” it's literally the tour of the code πŸ’– The source has been divided into numbered parts, and each little subsection below explains one of them. no cap.


🌸 Part 1: Macro definitions 🌸

A whole bunch of preprocessor macros get defined to make the application more readable and/or portable. πŸ’…

The first macros describe the IO pins our LEDs and pushbuttons are connected to. It's giving mini-HAL (hardware abstraction layer) energy, so if some of the connections get changed, they don't need to be changed inside the code β€” only on top. πŸŽ€ Note that the location of the PWM output itself is mandated by the hardware, so it cannot be easily changed. And since 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 get mapped back to their ATmega8/16 equivalents to keep the actual program code portable. periodt.

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 ngl, 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. iconic. πŸ‘‘

πŸ¦‹ 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. 😭

bestie listen β€” that watchdog is lowkey savage fr. block too long and it WILL reset you. plan your code paths accordingly. ⚠️

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 πŸ“œ

(that's the code itself, bestie β€” go read it, it's all right there πŸ’–)

πŸ’– Author πŸ’–

Generated automatically by Doxygen for avr-libc from the source code. πŸ€–


okay that's a wrap bestie πŸŽ€ this whole page is from Version 2.1.0, dated Sat Jan 29 2022, and it's the largedemo(3) page β€” slay, you made it to the end, so proud of you 🫢✨

πŸ“¦ where this page comes from