asmdemo.c
After the usual include files, two variables are 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 actually only constitutes of a single bit, intbits.pwm_received. This bit will be set
whenever the incoming PWM decoder has updated pwm_incoming.
Both variables are marked volatile to ensure their readers will always pick up an updated value, as both
variables will be set by interrupt service routines.
The function ioinit() initializes the microcontroller peripheral devices. In particular, 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. This interrupt is used to inform the
incoming PWM decoder that the counting direction of channel 0 is just changing from up to down. Likewise,
an overflow interrupt will be generated whenever the countdown reached BOTTOM (value 0), where the
counter will again alter its counting direction to upwards. This information is needed in order to know
whether the current counter value of TCNT0 is to be evaluated from bottom or top.
Further, ioinit() activates the pin-change interrupt PCINT0 on any edge of PB4. Finally, PB1 (OC0B) will
be activated as an output pin, and global interrupts are being enabled.
In the ATtiny45 setup, the C code contains an ISR for PCINT0. At each pin-change interrupt, it will first
be analyzed whether the interrupt was caused by a rising or a falling edge. In case of the rising edge,
timer 1 will be started with a prescaler of 16 after clearing the current timer value. Then, at the
falling edge, the current timer value will be recorded (and timer 1 stopped), the pin-change interrupt
will be suspended, and the upper layer will be notified that the incoming PWM measurement data is
available.
Function main() first initializes the hardware by calling ioinit(), and then waits until some incoming
PWM value is available. If it is, the output PWM will be 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.
project.h
In order for the interrupt service routines to be as fast as possible, some of the CPU registers are set
aside completely for use by these routines, so the compiler would not use them for C code. This is
arranged for in project.h.
The file is divided into one section that will be used by the assembly source code, and another one to be
used by C code. The assembly part is distinguished by the preprocessing macro __ASSEMBLER__ (which will
be automatically set by the compiler front-end when preprocessing an assembly-language file), and it
contains just macros that give symbolic names to a number of CPU registers. The preprocessor will then
replace the symbolic names by 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 by using
declarations that bind a variable permanently to a CPU register (see Howtopermanentlybindavariabletoaregister?). Even in case the C code never has a need 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 as it is the target of a loadimmediate
(or SER) instruction that is not applicable to the entire register file.
isrs.S
This file is a preprocessed assembly source file. The C preprocessor will be run by the compiler front-
end first, resolving all #include, #define etc. directives. The resulting program text will then be
passed on to the assembler.
As the C preprocessor strips all C-style comments, preprocessed assembly source files can have both, C-
style (/* ... */, // ...) as well as assembly-style (; ...) comments.
At the top, the IO register definition file avr/io.handtheprojectdeclarationfileproject.hareincluded.TheremainderofthefileisconditionallyassembledonlyifthetargetMCUtypeisanATtiny13,soitwillbecompletelyignoredfortheATtiny45option.
Next are the two interrupt service routines for timer 0 compare A match (timer 0 hits TOP, as OCR0A is
set to 255) and timer 0 overflow (timer 0 hits BOTTOM). As discussed above, these are kept as short as
possible. They only save SREG (as 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, respectively, in order 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 will require 14 out of each 256 CPU cycles,
or about 5 % of the overall CPU time.
The pin-change interrupt PCINT0 will be handled in the final part of this file. The basic algorithm is to
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 time computed will be recorded as the start time only. Then, at the falling edge, this start time
will be subracted from the current time to compute the actual pulse width seen (left in pwm_incoming),
and the upper layers are informed of the new value by setting bit 0 in the intbits flags. At the same
time, this pin-change interrupt will be disabled so no new measurement can be performed until the upper
layer had a chance to process the current value.