asmdemo(3avr)
π asmdemo(3avr) π
okay so bestie, welcome to the asmdemo(3avr) manual page. It's literally about Combining C and assembly source files and honestly? iconic. π«Άβ¨
β¨ NAME β¨
asmdemo β Combining C and assembly source files. no cap. π
πΈ SYNOPSIS πΈ
Detailed Description
okay so real talk: when your app is time-critical or space-critical, you might wanna mix C code (because it's easy to maintain, slay) with assembly code (because it's max speed / min code size, periodt). This demo is literally the blueprint for how to do that. π
The goal? Decode radio-controlled model PWM signals, then control an output PWM based on whatever the current input signal's value is. The incoming PWM pulses use a standard encoding scheme: a pulse width of 920 microseconds means one end of the scale β that's 0 % pulse width on the output β and 2120 microseconds means the other end, aka 100 % output PWM. Normally you'd have multiple channels encoded like that in subsequent pulses, followed by a bigger gap, so the whole frame repeats every 14 through 20 ms. But for this demo? We're ignoring that, so only a single input PWM channel is assumed. lowkey simpler. π
So the basic challenge is to use the cheapest controller available for this job: an ATtiny13 with only a single timer channel. But that timer channel is already required to run the outgoing PWM signal generation, so the incoming PWM decoding had to be adjusted to the constraints set by the outgoing PWM. it's giving resource management. π
Here's the tea: PWM generation toggles the counting direction of timer 0 between up and down after every 256 timer cycles. So you can't just read TCNT0 and deduce the current time β you also gotta know which direction the timer is counting. That means servicing interrupts whenever the timer hits TOP (255) and BOTTOM (0) so you learn about each counting-direction change. For PWM generation, you usually want it running at the highest possible speed so filtering the PWM frequency from the modulated output signal is easy. So the PWM timer runs at full CPU speed. That makes the overflow and compare match interrupts trigger every 256 CPU clocks, which means they have to run with the absolute minimal number of processor cycles possible β otherwise those interrupt service routines would impose a too-high CPU load. And that's the main reason the entire interrupt handling is implemented in fine-tuned assembly code instead of C. periodt. π₯
To verify parts of the algorithm and the underlying hardware, the demo is set up so the pin-compatible but more expensive ATtiny45 (or its siblings ATtiny25 and ATtiny85) could be used as well. In that case, no separate assembly code is required, because there are two timer channels available. we love flexibility. π¦
π Hardware setup π
The incoming PWM pulse train gets fed into PB4. That generates a pin change interrupt there on each edge of the incoming signal.
The outgoing PWM is generated through OC0B of timer channel 0 (PB1). For demonstration purposes, you should connect a LED to that pin β like, one of the LEDs on an STK500. π‘
The controllers run on their internal calibrated RC oscillators: 1.2 MHz on the ATtiny13, and 1.0 MHz on the ATtiny45.
π§ A code walkthrough π§
asmdemo.c
After the usual include files, two variables get defined. The first one, pwm_incoming, is used to communicate the most recent pulse width detected by the incoming PWM decoder up to the main loop.
The second variable is actually just a single bit: intbits.pwm_received. This bit gets set whenever the incoming PWM decoder has updated pwm_incoming.
Both variables are marked volatile so their readers always pick up an updated value β because both variables get set by interrupt service routines. we love a consistency queen. π
The function ioinit() initializes the microcontroller peripheral devices. Specifically, it starts timer 0 to generate the outgoing PWM signal on OC0B. Setting OCR0A to 255 (which is the TOP value of timer 0) is used to generate a timer 0 overflow A interrupt on the ATtiny13. That interrupt informs the incoming PWM decoder that the counting direction of channel 0 is just changing from up to down. Likewise, an overflow interrupt gets generated whenever the countdown reaches BOTTOM (value 0), where the counter will again alter its counting direction to upwards. You need this info to know whether the current counter value of TCNT0 should be evaluated from bottom or top.
Also, ioinit() activates the pin-change interrupt PCINT0 on any edge of PB4. Finally, PB1 (OC0B) gets activated as an output pin, and global interrupts are being enabled.
In the ATtiny45 setup, the C code contains an ISR for PCINT0. On each pin-change interrupt, it first analyzes whether the interrupt was caused by a rising or a falling edge. If it's the rising edge, timer 1 gets started with a prescaler of 16 after clearing the current timer value. Then, on the falling edge, the current timer value gets recorded (and timer 1 stopped), the pin-change interrupt gets suspended, and the upper layer gets notified that the incoming PWM measurement data is available.
Function main() first initializes the hardware by calling ioinit(), then waits until some incoming PWM value is available. If it is, the output PWM gets adjusted by computing the relative value of the incoming PWM. Finally, the pin-change interrupt is re-enabled, and the CPU is put to sleep. goodnight bestie π΄
project.h
To make the interrupt service routines as fast as possible, some CPU registers are set aside completely for use by these routines, so the compiler won't use them for C code. That's arranged in project.h.
The file is split into one section used by the assembly source code, and another used by C code. The assembly part is distinguished by the preprocessing macro __ASSEMBLER__ (which the compiler front-end automatically sets when preprocessing an assembly-language file). That part contains just macros that give symbolic names to a number of CPU registers. The preprocessor then replaces the symbolic names with their right-hand side definitions before calling the assembler.
In C code, the compiler needs to see variable declarations for these objects. This is done using declarations that bind a variable permanently to a CPU register (see How to permanently bind a variable to a register?). Even if the C code never needs to access these variables, declaring the register binding that way causes the compiler to not use these registers in C code at all.
The flags variable needs to be in the range of r16 through r31, because it's the target of a load immediate (or SER) instruction that isn't applicable to the entire register file.
isrs.S
This file is a preprocessed assembly source file. The C preprocessor gets run by the compiler front-end first, resolving all #include, #define etc. directives. The resulting program text then gets passed on to the assembler.
Since the C preprocessor strips all C-style comments, preprocessed assembly source files can have both C-style (/* ... */, // ...) and assembly-style (; ...) comments. variety is the spice of life. πΆοΈ
At the top, the IO register definition file avr/io.h and the project declaration file project.h are included. The rest of the file is conditionally assembled only if the target MCU type is an ATtiny13, so it gets completely ignored for the ATtiny45 option.
Next up are the two interrupt service routines for timer 0 compare A match (timer 0 hits TOP, since OCR0A is set to 255) and timer 0 overflow (timer 0 hits BOTTOM). Like we said, these are kept as short as possible. They only save SREG (because the flags will be modified by the INC instruction), increment the counter_hi variable which forms the high part of the current time counter (the low part is formed by querying TCNT0 directly), and clear or set the variable flags to note the current counting direction. The RETI instruction terminates these interrupt service routines. Total cycle count is 8 CPU cycles, so together with the 4 CPU cycles needed for interrupt setup, and the 2 cycles for the RJMP from the interrupt vector to the handler, these routines require 14 out of each 256 CPU cycles, or about 5 % of the overall CPU time. efficiency ate. π
The pin-change interrupt PCINT0 is handled in the final part of this file. The basic algorithm: quickly evaluate the current system time by fetching the current timer value of TCNT0, and combining it with the overflow part in counter_hi. If the counter is currently counting down rather than up, the value fetched from TCNT0 must be negated. Finally, if this pin-change interrupt was triggered by a rising edge, the computed time gets recorded as the start time only. Then, on the falling edge, this start time gets subtracted from the current time to compute the actual pulse width seen (left in pwm_incoming), and the upper layers get informed of the new value by setting bit 0 in the intbits flags. At the same time, this pin-change interrupt gets disabled so no new measurement can be performed until the upper layer had a chance to process the current value.
π The source code π
okay so this section is literally just a title in the originalβ¦ no extra deets. the vibes are immaculate though. π
π Author π
Generated automatically by Doxygen for AVR-LibC from the source code.
this page is from AVR-LibC, version 2.2.1, and it's giving asmdemo(3avr) energy. slay, bestie. πβ¨π
π¦ where this page comes from
- ubuntu resolute: avr-libc 1:2.2.1-1build1 Β· BSD-3-Clause
- ubuntu stonking: avr-libc 1:2.2.1-1build1 Β· BSD-3-Clause
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms