Path:
Home ==>
AVR-EN ==>
Micro beginner ==> 9. Audio generator
Diese Seite in Deutsch (extern):
Lecture 9: An audio generator with ADC, tone table, multiplication
The OC0A output is used here as a variable audio generator with adjusting the
tone's frequency with a potentiometer. Further more 8 bit numbers are multiplied
and music is played here.
9.0 Overview
- Introduction to audio generation
- Hardware, components and mounting
- Tone control
- Introduction to tables
- Introduction to multiplication
- Gamut output
- Playing pieces of music
Generating tones is essentially the same as blinking a LED, with frequencies
between 30 cs/s and 20 kcs/s (for bats: up to 40 kcs/s). So
we learn here not much on timers, only how to attach a speaker to a port pin
instead of a LED.
9.2.1 The hardware scheme
To sound tones a speaker is needed. This is attached to OC0A. A capacitor of
47 µF decouples the DC from the port pin and transfers only the AC
component.
A key and the potentiometer are attached like in the previous experiments.
9.2.2 The components
The speaker
This is
the speaker. He has an impedance of 45 Ω to yield a strong audio signal.
The two pins are soldered to a short cable and short pins that fit into the
breadboard. The polarity of the speaker is irrelevant for our application,
it has only accustic consequences if two more of those are operated.
The electrolytical capacitor
This is an electrolytical capacitor. This is a component for which correct
polarity is essential. The minus pole is marked, the longer of the two wires is
plus.
9.2.3 Mounting
The speaker is tied to pin 5, via the electrolyt.
With that we can start sound-generation.
9.3.1 Simple task 1
Task 1 is to output audio tones via the speaker and to regulate their frequency
with the potentiometer. The tones shall range between 300 cs/s and 75 kcs/s.
The tone shall only be audible if the key is pressed.
9.3.2 Solution
Frequency ranges
It is already clear that the CTC mode of the timer has to be used here. The OC0A
output pin has to toggle (from low to high and back). As each swing of the rectangle
requires two CTC periods, the resulting frequency is half that of the CTC frequency.
The frequency depends from the prescaler and the compare value. Ranges cover the
following frequencies:
Clock | Pre- scaler | OCR0A =0 | OCR0A =255 |
9.6 Mcs/s | 1 | 4.8 Mcs/s | 18.75 kcs/s |
8 | 600 kcs/s | 2.34 kcs/s |
64 | 75 kcs/s | 292.5 Hz |
256 | 18.75 kcs/s | 73.1 cs/s |
1024 | 4.69 kcs/s | 18.3 cs/s |
1.2 Mcs/s | 1 | 600 kcs/s | 2.34 kcs/s |
8 | 75 kcs/s | 292.5 cs/s |
64 | 9.38 kcs/s | 36.6 cs/s |
256 | 2.35 kcs/s | 9.15 cs/s |
1024 | 586 cs/s | 2.29 cs/s |
At 1.2 Mcs/s clock, the audible range is well covered with a prescaler of 8.
AD values and OCR0A values
The higher the measured voltage from the potentiometer the higher the tone frequency should
be. The OCR0A value behaves opposite: the larger the lower is the frequency. So either the
potentiometer has to be reverted or a reversion of the measured value has to take place.
A software solution for this would be to subtract the measured 8 bit value from hex 0xFF.
That would go like this:
ldi Register1,0xFF
sub Register1,Register2 ; Register2 = measured value
mov Register2,Register1
Inverting all bits in a register is a task that the controller's Central Processing Unit
can do with a special instruction, see the source code, so that we do not need to take
this path.
9.3.3 Program
This is the program, the source code here.
;
; ***********************************************
; * Audio generator with key and tone regulator *
; * (C)2017 by http://www.avr-asm-tutorial.net *
; ***********************************************
;
.NOLIST
.INCLUDE "tn13def.inc"
.LIST
;
; --------- Registers ------------------------
; free: R0 .. R14
.def rSreg = R15 ; Save/restore status register
.def rmp = R16 ; Multi purpose register
.def rimp = R17 ; Multi purpose inside interrupts
; free: R18 .. R31
;
; --------- Ports ----------------------------
.equ pOut = PORTB ; Output port
.equ pDir = DDRB ; Direction port
.equ pInp = PINB ; Input port
.equ bLspD = DDB0 ; Speaker output direction pin
.equ bTasO = PORTB3 ; Pull up key output pin
.equ bTasI = PINB3 ; Key input pin
.equ bAdID = ADC2D ; ADC input disable
;
; --------- Timing ---------------------------
; Clock = 1200000 cs/s
; Prescaler = 8
; CTC TOP range = 0 .. 255
; CTC divider range = 1 .. 256
; Toggle divider = 2
; Frequency range = 75 kcs/s .. 293 cs/s
;
; --------- Reset- und Interrupt vectors -----
.CSEG ; Assemble to Code-Segment
.ORG 0 ; Start at address zero
rjmp Start ; Reset Vector, jump to init
reti ; INT0-Int, inactive
rjmp PcIntIsr ; PCINT-Int, active
reti ; TIM0_OVF, inactive
reti ; EE_RDY-Int, inactive
reti ; ANA_COMP-Int, inactive
reti ; TIM0_COMPA-Int, inactive
reti ; TIM0_COMPB-Int, inactive
reti ; WDT-Int, inactive
rjmp AdcIsr ; ADC-Int, active
;
; ---------- Interrupt Service Routines -----
PcIntIsr: ; PCINT-Interrupt key
sbic pInp,bTasI ; Skip if key input = 0
rjmp PcIntIsrOff ; Key is not pressed
ldi rimp,(1<<COM0A0)|(1<<WGM01) ; Toggle output, CTC-A
out TCCR0A,rimp ; to timer control port A
rjmp PcIntIsrRet ; return
PcIntIsrOff:
ldi rimp,(1<<COM0A1)|(1<<WGM01) ; Clear output, CTC-A
out TCCR0A,rimp ; to timer control port A
PcIntIsrRet:
reti ; return from interrupt, set I flag
;
AdcIsr: ; ADC interrupt
in rSreg,SREG ; save SREG
in rimp,ADCH ; read MSB result
com rimp ; invert value
out OCR0A,rimp ; to CTC TOP port
; Restart ADC, int enable
ldi rimp,(1<<ADEN)|(1<<ADSC)|(1<<ADIE)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0)
out ADCSRA,rimp ; to ADC control port A
out SREG,rSreg ; restore SREG
reti ; return from interrupt, set I flag
;
; ---------- Program start and init ----------
Start:
; Stack init
ldi rmp,LOW(RAMEND) ; Set to SRAM end
out SPL,rmp ; to stack pointer
; In- and output ports
ldi rmp,1<<bLspD ; Speaker direction output
out pDir,rmp ; to direction port
ldi rmp,1<<bTasO ; Pull up on key port pin
out pOut,rmp ; to output port
; Configure timer as CTC
ldi rmp,(1<<COM0A1)|(1<<WGM01) ; Clear output, CTC-A
out TCCR0A,rmp ; to control port A
ldi rmp,1<<CS01 ; Prescaler = 8, start timer
out TCCR0B,rmp ; to timer control port B
; Configure and start AD conversion
ldi rmp,(1<<ADLAR)|(1<<MUX1) ; Left adjust, ADC2
out ADMUX,rmp ; to ADC MUX port
ldi rmp,(1<<ADEN)|(1<<ADSC)|(1<<ADIE)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0)
out ADCSRA,rmp ; to ADC control port A, start
; Configure PCINT for key input
ldi rmp,1<<PCINT3 ; Enable PB3 interrupt
out PCMSK,rmp ; in PCINT mask port
ldi rmp,1<<PCIE ; Enable PCINT
out GIMSK,rmp ; in interrupt mask port
; Enable sleep mode
ldi rmp,1<<SE ; Sleep mode idle
out MCUCR,rmp ; to MCU control port
; Enable interrupts
sei
; ------------ Main program loop --------
Loop:
sleep ; go to sleep
nop ; Wake up dummy
rjmp Loop ; go to sleep again
;
; End of source code
;
The following instruction is new:
- COM Register: inverts all bits in the register. From
0x00 to 0xFF and from 0xFF to 0x00. Subtract the register content from
0xFF, so called one's complement.
With the machine we can generate morse code with a regulated tone.
9.3.4 Simulation of the program
Simulation goes with avr_sim
as follows.
The I/O port PB0 is set to be output, the output pin is low.
The key input portbit PB3 is set to switch on the pull-up resistor.
Closing the key pushes the input to low.
On PB3 the PCINT enable mask is active and the PCINT enable bit is
set.
The timer TC0 is set to CTC mode, with clearing the counter after the
compare match in A has been reached. The output PB0 is cleared on
compare match, leaving PB0 at low voltage. The prescaler is at eight,
dividing 1,200,000 Hz to 150,000 Hz counting frequency.
The ADC works with the clock divided by 128: 1,200,000 Hz means
9,375 Hz or, for 13 ADC cycles, requires 1.386 ms per
conversion. The reference voltage is 5 V. The voltage value
of 0.675 V shall lead to an ADC value of 138. The left adjust
should yield a MSB of 34 (0x22). The first conversion has been
started (the progress status bar is visible).
The first AD conversion has made some progress. As the conversion
lasts 1.386 ms the controller remains sleeping.
An ADC complete interrupt has occurred and the ADC result is read
to R17 using the instruction:
in rimp,ADCH ; read MSB result
Conversion to the complement of 0x22 using
com rimp ; invert value
yields 0xFF - 0x22 = 0xDD. In that way 38 inverts to 221.
With "out OCR0A,rimp ; to CTC TOP port" this is written
to the timer's compare portregister. This does not lead to an
audible change as far as the "PB0 clear" condition remains
unchanged.
This changes only if a PCINT is executed. Initiated either by changing
the input signal in PINB3 manually or by clicking on PCINT3 such a
PCINT is initiated and, after four wait cycles and if no other
interrupt request is pending, executed.
Within the interrupt service routine, the two instructions
ldi rimp,(1<<COM0A0)|(1<<WGM01) ; Toggle output, CTC-A
out TCCR0A,rimp ; to timer control port A
switch toggling of the PB0 portpin by the timer on.
After reaching the compare value in A and restarting at TCNT = 0
the respective portbit changes polarity ...
The pin PB0 now is high.
Since the last CTC 1.4775 ms have elapsed. The time for two such
CTC cycles corresponds to an audio frequency of 338 Hz, something between
e1 and f1.
9.4.1 Task
With this part of the lecture the gamut shall be played, the potentiometer shall select
the tone to be played.
9.4.2 Introduction to tables
The gamut table
Unfortunately german/european/american taste allows only certain tones. Softly changing
tone heights are out and not allowed here. We need a table of those allowed tones. Those
are collected in the following table.
To teach those tones to the controller's timer, the CTC values and prescalers are associated
to those tones. As the timer does not exactly fit the desired frequency, the resulting
frequencies and the deviation from the gamut tone are also given.
Tone | Cs/s | Presc | CTC | Real(cs/s) | Delta(%) | # |
a | 440 | 8 | 170 | 441.18 | 0.27% | 0 |
h | 495 | 8 | 152 | 493.42 | -0.32% | 1 |
cis | 550 | 8 | 136 | 551.47 | 0.27% | 2 |
d | 586.66 | 8 | 128 | 585.94 | -0.12% | 3 |
e | 660 | 8 | 114 | 657.89 | -0.32% | 4 |
fis | 733.33 | 8 | 102 | 735.29 | 0.27% | 5 |
gis | 825 | 8 | 91 | 824.18 | -0.10% | 6 |
a' | 880 | 8 | 85 | 882.35 | 0.27% | 7 |
h' | 990 | 8 | 76 | 986.84 | -0.32% | 8 |
cis' | 1100 | 8 | 68 | 1102.94 | 0.27% | 9 |
d' | 1173.32 | 8 | 64 | 1171.88 | -0.12% | 10 |
e' | 1320 | 8 | 57 | 1315.79 | -0.32% | 11 |
fis' | 1466.66 | 8 | 51 | 1470.59 | 0.27% | 12 |
gis' | 1650 | 8 | 45 | 1666.67 | 1.01% | 13 |
A | 1760 | 8 | 43 | 1744.19 | -0.90% | 14 |
H | 1980 | 8 | 38 | 1973.68 | -0.32% | 15 |
CIS | 2200 | 8 | 34 | 2205.88 | 0.27% | 16 |
D | 2346.64 | 8 | 32 | 2343.75 | -0.12% | 17 |
E | 2640 | 1 | 227 | 2643.17 | 0.12% | 18 |
FIS | 2933.32 | 1 | 205 | 2926.82 | -0.22% | 19 |
GIS | 3300 | 1 | 182 | 3296.70 | -0.10% | 20 |
A' | 3520 | 1 | 170 | 3529.41 | 0.27% | 21 |
H' | 3960 | 1 | 152 | 3947.36 | -0.32% | 22 |
CIS' | 4400 | 1 | 136 | 4411.76 | 0.27% | 23 |
D' | 4693.28 | 1 | 128 | 4687.5 | -0.12% | 24 |
E' | 5280 | 1 | 114 | 5263.15 | -0.32% | 25 |
FIS' | 5866.64 | 1 | 102 | 5882.35 | 0.27% | 26 |
GIS' | 6600 | 1 | 91 | 6593.40 | -0.10% | 27 |
A'' | 7040 | 1 | 85 | 7058.82 | 0.27% | 28 |
The deviations are, all in all, relatively small. I am not able to realize a difference between 440
and 441 cs/s. So we can accept that, without having an extra xtal oscillator of 1.1968 Mcs/s
(which is not available anyway).
To enable the timer to play those frequency we need a gamut table from which the controller can
read the CTC and prescaler values. As the values from a to a', from a' to A, from A to A' and
from A' to A'' always differ by a constant value of 2, we could calculate those from a base table.
But that would be complicated because of the changes in the optimal prescaler value (at high
frequencies above D = 1, at smaller ones = 8). So the table should as well hold the prescaler
value together with the CTC value. The table length covers four octaves.
Tables and their placement
The table needs 2*29 = 58 bytes length. There are principally three locations in the controller
where this table can be placed:
- the SRAM storage. In an ATtiny13 64 bytes of SRAM are available. The SRAM would be rather full
and conflicts with the stack are to be expected., which also uses SRAM.
- the EEPROM storage. This provides also 64 bytes. That would fit, but would be rather full.
- the Flash storage. This has 512 words or 1,024 bytes and would provide enough space,
even for additional octaves.
Table in SRAM
In this first case there is no other opportunity than writing each value of the table, e.g. with
the instruction STS Address,Register, to its place. The following would have to be
programmed:
ldi R16,8 ; Prescaler value
sts 0x60,R16 ; store at address 0x0060 in SRAM
ldi R16,170 ; CTC value
sts 0x60+1,R16 ; store at address 0x0061 in SRAM
[...]
ldi R16,1 ; Prescaler value
sts 0x60+56,R16 ; store at address 0x0098 in SRAM
ldi R16,85 ; CTC value
sts 0x61,R16 ; store at address 0x0099 in SRAM
Each value pait would require four instructions, of which two (STS) are double-word instructions (six
instruction words per pair). Even if we simplify those instructions a little bit by using the
instruction ST Z+,Register, this would be a lengthy affair. ST Z+ goes as follows:
ldi ZH,HIGH(0x0060) ; Pointer Z to SRAM start address, MSB
ldi ZL,LOW(0x0060) ; dto., LSB
ldi R16,8 ; Prescaler value
st Z+,R16 ; store R16 in SRAM und increase address in Z
ldi R16,170 ; CTC value
st Z+,R16 ; to next address
[...]
ldi R16,1 ; Prescaler value
st Z+,R16 ; store in SRAM and increase address in Z
ldi R16,85 ; CTC value
st Z,R16 ; store R16 to last address in SRAM
With the last value to be written the storing with automatic address increment is changed to
ST Z,Register, without increasing Z.
The opposite instruction, to decrease the address in Z, is also available, but works a bit
different. ST -Z,Register the address is first decreased and then the
register content is written to SRAM to the already decreased address. Norwegian language
constructors know how to confuse beginners.
Besides the lengthy procedure to code the assembler source, the SRAM is not a conventient place
to locate a lengthy table.
Table in EEPROM
The second location to place the table is the EEPROM. Which offers slightly better conditions for
that. Here the construction would be:
.ESEG
.ORG 0
.db 8,170
[...]
.db 1,85
.CSEG
The assembler directive ".ESEG" places the resulting code not into the storage flash
memory but into an extra hex file named ".EEP", which can be written to the EEPROM
directly, byte by byte. At the end of the EEPROM content the directive ".CSEG"
switches back into the code segment, so that further instructions can be programmed to the
flash memory.
The ".ORG 0" directice defines the address place to which the table is written in the
EEOROM. The 0 places the EEPROM table to the beginning.
The numerous ".DB" directives with the comma-separated bytes are placed to subsequent
addresses in the EEPROM. .DB accepts bytes, words or text (ASCII characters in '', character by
character).
How we access to the EEPROM's content we learn in a later lecture.
This is a more comfortable manner, but not as elegant like the third ooportunity.
Table in program flash memory
Now it is a little bit more complicated, because the program storage memory is organized wordwise,
in 16 bit words. With
Table:
.db 8,170
[...]
.db 1,85
the following will happen:
- The "8" will be converted to the LSB, the "170" as MSB and placed as a
16 bit word into the program memory. The current address at which this word is stored is given
by the label "Table:".
- All subsequent ".DB" directives place similar words to the following addresses.
It is clear that per ".DB" ALWAYS whole words are written to program memory. If
".DB 1" is written to the source code, effectively 0x0001 is written there. The automatic
addition of 0x00 as MSB, if the number of bytes placed with a single .DB directive is odd, will
be signalled by a warning during the assembly process. In the .DB directive the first byte written
is always the LSB.
The same warning of the assembler results, if we place text with a .DB directive into flash memory,
e.g. with "ABC", and if the text has an odd number of characters. In that case a 0x00
character is added, if we do not formulate this different, e.g. as .DB "ABC ".
A second opportunity to construct the table in the flash is by definitely words to the table,
e.g.
Table:
.dw 8+170*256
[...]
.dw 1+85*256
This creates directly the words to be placed to the table, and we can select which part is the LSB
and which part is the MSB.
Now we have to access this table to read values from it. To read the first byte, we
can use the LPM instruction. Without parameters this instruction
reads the byte on address Z in the flash memory to the register R0. Note that the
address is in the bits 1 to 15 of Z while bit 0 specifies if the lower (0) or upper
(1) byte on that address shall be read. We can formulate as follows:
ldi ZH,HIGH(2*Table) ; MSB pointer to Z
ldi ZL,LOW(2*Table) ; dto., LSB
lpm ; Load from Program Memory
Note that the address "Table:" is multiplied by two, because each address location holds
two bytes. "2*Table" accesses the LSB. If access to the the MSB is desired write
"2*Table+1".
The LSB read now is stored to register R0. To read it to somewhere else we formulate
ldi ZH,HIGH(2*Table) ; MSB pointer to Z
ldi ZL,LOW(2*Table) ; dto., LSB
lpm R16,Z ; Load from Program Memory to R16
To read both bytes one after the other, the LPM r,Z+ instruction has to be
executed: the Z+ increments the content of Z automatically after reading the content at Z. Like
this:
ldi ZH,HIGH(2*Table) ; MSB pointer to table in Z
ldi ZL,LOW(2*Table) ; dto., LSB
lpm XL,Z+ ; Load LSB from Program Memory to register XL
lpm XH,Z+ ; Load MSB from Program Memory to register XH
This increases the address automatically. Backwards the norwegian programming style is again to
decrease the address first and then to read the content, with LPM r,-Z.
But beware: this instruction is not implemented in the ATtiny13. By the way, the registers XL
and XH as well as YH and YL and ZH and ZL as well as the double register pairs X, Y and Z are
defined in the "def.inc" file. If you do not include this in the assembler source
header, you will get error messages from the assembler, unless you defined those e.g. with
".def ZH = R31". This has historic reasons because the first AVR devices (e.g. the
ancient AT90S1200) had no pointer register pairs.
With that, the gamut table is constructable, like this:
GamutTable:
.db 1<<CS01, 169 ; a #0
.db 1<<CS01, 151 ; h #1
.db 1<<CS01, 135 ; cis #2
.db 1<<CS01, 127 ; d #3
.db 1<<CS01, 113 ; e #4
.db 1<<CS01, 101 ; fis #5
.db 1<<CS01, 90 ; gis #6
.db 1<<CS01, 84 ; a' #7
.db 1<<CS01, 75 ; h' #8
.db 1<<CS01, 67 ; cis' #9
.db 1<<CS01, 63 ; d' #10
.db 1<<CS01, 56 ; e' #11
.db 1<<CS01, 50 ; fis' #12
.db 1<<CS01, 44 ; gis' #13
.db 1<<CS01, 42 ; A #14
.db 1<<CS01, 37 ; H #15
.db 1<<CS01, 33 ; CIS #16
.db 1<<CS01, 31 ; D #17
.db 1<<CS00, 226 ; E #18
.db 1<<CS00, 204 ; FIS #19
.db 1<<CS00, 181 ; GIS #20
.db 1<<CS00, 169 ; A' #21
.db 1<<CS00, 151 ; H' #22
.db 1<<CS00, 135 ; CIS' #23
.db 1<<CS00, 127 ; D' #24
.db 1<<CS00, 113 ; E' #25
.db 1<<CS00, 101 ; FIS' #26
.db 1<<CS00, 90 ; GIS' #27
.db 1<<CS00, 84 ; A'' #28
This table requires 29 words in the flash storage, which are 5.7% of the memory of the
ATtiny13 and is an acceptable size.
If we want to read the tenth note from the table and write it to the timer, the code for
that goes like this:
ldi R16,10 ; tenth note
lsl R16 ; Note multiplied by 2 (2 bytes per note)
ldi ZH,HIGH(2*GamuTable)
ldi ZL,LOW(2*GamutTable)
add ZL,R16 ; add note to pointer
ldi R16,0 ; CLR would also clear the carry flag!
adc ZH,R16 ; add eventual carry
lpm R0,Z+ ; read prescaler value
out TCCR0B,R0 ; write to timer control port B
lpm R0,Z ; read CTC value
out OCR0A,R0 ; write to CTC compare value
With that, the storage, the reading and the use of the gamut table is resolved for our
case here.
One problem solved, but another problem comes up immediately. The ADC provides measurement
data between 0 and 1,023 (without ADLAR) or 0 and 255 (with ADLAR). But our gamut table has
29 entries only. We could resolve that by just limiting the values down to 28, and the problem
is already solved. That would not be a really nice and intelligent solution, because those
28 different notes would be available within 2.8% of the potentiometer range (10 bit), while
note #29 takes all the rest (97.2%). With ADLAR 11% cover 28 tones while 89% covers only
one single note, not very much more convenient. Not a real linear coverage, A'' will be
seriously overrated.
Somehow the incoming 1,023 or 255 should be linearly downsized to 28. The C programmer has
no problem with that, he divides by 36.5 resp. by 9.1 and rounds the result. Unfortunately
dividing with those numbers the C compiler envokes the floating number library. And this
library alone fills the available space in an ATtiny13 completely, and even exceeds that.
The C programmer now moves to a 96 pin ATxmega to have enough flash memory for his monster
lib. The assembler programmer instead invests a little bit of intelligence and comes up with
a much more clever solution, well fitting to the available memory of an ATtiny13.
The solution is to multiply the ADC result, with ADLAR set, with 29 and to divide it by
256. As a math formula: "Result = 29 * ADC / 256".
Dividing by 256 in binary math is as simple as can be: just skip the last eight bits of
the result of the multiplication (or: just ignore the LSB).
The problem therefore is reduced to the question on how to multiply the ADC result with
29. Several methods are possible to do this.
Simplest multiplication
The simplest type of multiplication is to add the ADC result 29 times to a 16 bit result.
E.g. like this (with number of necessary clock cycles to get execution times):
in R0,ADCH ; Read ADC result, +1 = 1
clr R2 ; Clear R2:R1 at start adding, +1 = 2
clr R1 ; +1 = 3
ldi R16,29 ; Multiplicator 29, +1 = 4
AddLoop:
add R1,R0 ; add ADC to result, LSB, +29*1 = 33
brcc PostCarry ; No overflow to carry, +29*1 = 62
inc R2 ; Increase MSB when carry is set, +29*1 = 91
PostCarry:
dec R16 ; count downwards, + 29*1 = 130
brne AddLoop ; add once again, +28*2 + 1 = 187
The 187 clock cycles that are needed are not very long. At 1.2 Mcs/s those are
156 µs, less than a single audio sine wave.
The method to add can be used in all cases where the multiplicator is not too large
and where the extended execution time is not too large. E.g. in case of 10 it is a
preferred method: add the base number once, multiply the result twice by 2 (LSL, ROL),
then add the base number again and multiply the result by 2 (again LSL and ROL).
In our case the 187 clock cycles are not too much, but there are other methods to multiply
that are very much faster.
Faster multiplication
Multiplying by 2 is, in the binary world, the simplest and fastest task (LSL and ROL).
We can multply by 2 on and on until we are near our 29. Then we add or subtract a little
bit and we have the 29 fold. The next 2 potence is 32, from which we can subtract the
ADC result three times. Like in this example:
in R0,ADCH ; Read result from ADC to R0, +1 = 1
clr R2 ; R2:R1 is the result, +1 = 2
mov R1,R0 ; Copy ADC result once, +1 = 3
lsl R1 ; LSB * 2, +1 = 4
rol R2 ; MSB * 2 plus carry, +1 = 5
lsl R1 ; LSB * 4, +1 = 6
rol R2 ; MSB * 4 plus carry, +1 = 7
lsl R1 ; LSB * 8, +1 = 8
rol R2 ; MSB * 8 plus carry, +1 = 9
lsl R1 ; LSB * 16, +1 = 10
rol R2 ; MSB * 16 plus carry, +1 = 11
lsl R1 ; LSB * 32, +1 = 12
rol R2 ; MSB * 32 plus carry, +1 = 13
sub R1,R0 ; Subtract once, +1 = 14
brcc NoCarry1 ; Carry clear, +1/2 = 15/16
dec R2 ; Decrease MSB, +1 = 16
NoCarry1:
sub R1,R0 ; Subtract twice, +1 = 17
brcc KeinCarry2 ; Carry clear, +1/2 = 18/19
dec R2 ; Decrease MSB, +1 = 19
NoCarry2:
sub R1,R0 ; Subtract three times, +1 = 20
brcc NoCarry3 ; Carry clear, +1/2 = 21/22
dec R2 ; Decrease MSB, +1 = 22
NoCarry3:
New is the instruction ROL register. This is the left-rolling version,
compared to the right-rolling ROR. ROL rolls
- the bits 0 to 6 to the next left bit (1 to 7),
- the carry bit to bit 0 of the register, and
- bit 7 of the register to the carry flag.
The 22 clock cycles of this mode of multiplication are by a factor of eight faster than
the primitive multiplication. But we can do it even faster.
Even faster multiplication
The previous methods are tailored closely to the task. The real multiplication, applicable
to any combination of two 8 bit binaries, is not so complicated that even C programmer
can learn that method (to avoid monster libraries and the associated monster controllers).
The binary multiplication is even simpler than decimal multiplication. But let us start
with decimal to understand the mechanism.
Decimal multiplication goes like this. The first step is to multiply the number with the
least significant digit and to add this to the result. Then the number is shifted once left
(multiplied by 10), multiplied by the second significant digit and again added to the
result. This is repeated until all digits of the multiplicator have been multiplied and
added.
The binary multiplication is even simpler, because only a 0 or a 1 can roll out. Which means
no adding to the sum (0) or adding to the sum (1). The mechanism is the same (to roll out
one bit to the right, to add the left-shifted number (or not) and to left-shift the number.
The multiplication of 255 by 29 is shown in the picture.
The second number (in R16) is shifted to the right, by which the next 0 or 1 is shifted to
the carry flag. If that is 0, the first number (in R1:R0) is not added. If it is a 1 it
is added 16 bit wise to the result (in R3:R2). Then the first number (in R1:R0) is shifted
left (16 bit left shift). Again a right-shift of the second number, to add or not to add,
etc. If all ones are shifted out of the second number, the multiplication is ended.
That is how the code looks like:
in R0,ADCH ; Read MSB from the ADC as LSB, +1 = 1
clr R1 ; Clear MSB, +1 = 2
clr R2 ; Clear LSB result, +1 = 3
clr R3 ; dto., MSB, +1 = 4
ldi R16,29 ; Set multiplicator, +1 = 5
MultLoop:
lsr R16 ; Shift lowest bit to carry, +5*1 = 10
brcc NoAdding ; C = 0, do not add, +1*2+4*1 = 16
add R2,R0 ; add LSB, +5*1 = 21
adc R3,R1 ; add MSB with carry, +5*1 = 21
NoAdding:
lsl R0 ; Left shift first number, LSB, +5*1 = 26
rol R1 ; Left shift MSB and roll carry to MSB, +5*1 = 31
tst R16 ; End reached?, +1*5 = 36
brne MultLoop ; still ones to be processed, +4*2+1*1 = 45
The result (28) is now in register R3 (we ignore the LSB R2 = division by 256).
Ok, these are 45 clock cycles, and more than method 2. But the routine is processing any
8-by-8 bit multiplication and is not tailored to the 29 in our case. The routine is so
simple that even C programmer can learn that, without having to import massive libraries.
So our problem is solved on how to make 28 out of an input of 255 (or whatever value the
ADC returns). Without dividing (division by 256 does not really count as a division in the
binary world). Many mathmatical problems, that require a division, can be solved in that
way by avoiding divisions.
With that we have the basis to implement the gamut. The program follows, the
source code can be downloaded here.
;
; ***************************************************
; * Gamut tones with a potentiometer on an ATtiny13 *
; * (C)2017 by www.avr-asm-tutorial.net *
; ***************************************************
;
.NOLIST
.INCLUDE "tn13def.inc"
.LIST
;
; --------- Registers -----------------
; Used: R0 for LPM and calculations
; Used: R1 for calculations
.def rMultL = R2 ; Multiplicator, LSB
.def rMultH = R3 ; dto., MSB
; free: R4 .. R14
.def rSreg = R15 ; Save/restore SREG
.def rmp = R16 ; Multi purpose outside ints
.def rimp = R17 ; Multi purpose inside ints
.def rFlag = R18 ; Flag register
.equ bAdcR = 0 ; Read in ADC value
; free: R18 .. R29
; Used: R31:R30, Z = ZH:ZL for LPM
;
; --------- Ports ---------------------
.equ pOut = PORTB ; Output port
.equ pDir = DDRB ; Direction port
.equ pInp = PINB ; Input port
.equ bSpkD = DDB0 ; Speaker output pin
.equ bKeyO = PORTB3 ; Pull up Tasteneingang
.equ bKeyI = PINB3 ; Tasteninputpin
.equ bAdID = ADC2D ; ADC Input Disable pin
;
; --------- Timing --------------------
; Clock = 1200000 cs/s
; Prescaler = 1 or 8
; CTC TOP range = 0 .. 255
; CTC divider range = 1 .. 256
; Toggle divider = 2
; Frequency range = 600 kcs/s .. 293 cs/s
;
; ---- Reset- and Interrupt vectors ---
.CSEG ; Assemble to code segment
.ORG 0 ; At start address
rjmp Start ; Reset vector, Init
reti ; INT0-Int, inactive
rjmp PcIntIsr ; PCINT-Int, active
reti ; TIM0_OVF, inactive
reti ; EE_RDY-Int, inactive
reti ; ANA_COMP-Int, inactive
reti ; TIM0_COMPA-Int, inactive
reti ; TIM0_COMPB-Int, inactive
reti ; WDT-Int, inactive
rjmp AdcIsr ; ADC-Int, active
;
; ----- Interrupt Service Routines ----
PcIntIsr: ; PCINT-Interrupt key
sbic pInp,bKeyI ; Skip next instruction if key = 0
rjmp PcIntIsrOff ; Key is not pressed
; Tone output on
ldi rimp,(1<<COM0A0)|(1<<WGM01) ; Toggle OC0A, CTC-A
out TCCR0A,rimp ; to timer control port A
rjmp PcIntIsrRet ; return
PcIntIsrOff:
; Tone output off
ldi rimp,(1<<COM0A1)|(1<<WGM01) ; Clear OC0A, CTC-A
out TCCR0A,rimp ; to timer control port A
PcIntIsrRet:
reti
;
AdcIsr: ; ADC-Interrupt
in rSreg,SREG ; Save SREG
in rMultL,ADCH ; Read MSB of ADC (ADLAR)
sbr rFlag,1<<bAdcR ; Set flag new ADC value
; Restart ADC
ldi rimp,(1<<ADEN)|(1<<ADSC)|(1<<ADIE)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0)
out ADCSRA,rimp ; to ADC control port A
out SREG,rSreg ; Restore SREG
reti
;
; ---------- Program start and Init ----------
Start:
; Init stack
ldi rmp,LOW(RAMEND) ; to SRAM end
out SPL,rmp ; to stack pointer
; Init In- and Output ports
ldi rmp,1<<bSpkD ; Speaker output direction
out pDir,rmp ; to direction port
ldi rmp,1<<bKeyO ; Pull up on key pin
out pOut,rmp ; to output port
; Configure timer as CTC
ldi rmp,(1<<COM0A1)|(1<<WGM01) ; Clear OC0A, CTC-A
out TCCR0A,rmp ; to timer control port A
; Prescaler and timer start by ADC int
; Configure ADC and start
ldi rmp,(1<<ADLAR)|(1<<MUX1) ; Left adjust, ADC2
out ADMUX,rmp ; to ADC MUX port
ldi rmp,(1<<ADEN)|(1<<ADSC)|(1<<ADIE)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0)
out ADCSRA,rmp ; to ADC control port A, start ADC
; PCINT for key input
ldi rmp,1<<PCINT3 ; Enable PB3-Int
out PCMSK,rmp ; to PCINT mask port
ldi rmp,1<<PCIE ; Enable PCINT
out GIMSK,rmp ; to Interrupt Mask port
; Enable sleep
ldi rmp,1<<SE ; Sleep mode idle
out MCUCR,rmp ; to MCU control port
; Enable interrupts
sei
; ------------ Main program loop --------
Loop:
sleep ; go to sleep
nop ; After wake up by int
sbrc rFlag,bAdcR ; Skip next if Adc flag zero
rcall AdcCalc ; Convert ADC value to tone
rjmp Loop ; go to sleep again
;
; ------------ Convert AD value ----------------
; ADC value to tone height and timer start
; AD value is in rMultL
AdcCalc:
cbr rFlag,1<<bAdcR ; Clear flag
; Multiply ADC value by 29
clr rMultH ; Clear MSB
clr R0 ; Clear result LSB
clr R1 ; dto., MSB
ldi rmp,29 ; Number of tones plus one
AdcCalcShift:
lsr rmp ; Shift lowest bit to carry
brcc AdcCalcNoAdd
add R0,rMultL ; add LSB to result
adc R1,rMultH ; add MSB with carry
AdcCalcNoAdd:
lsl rMultL ; Multiply by two
rol rMultH ; Roll carry to MSB und multiply by two
tst rmp ; rmp already empty?
brne AdcCalcShift ; no, go on multiplying
; Tone from gamut table
lsl R1 ; Tone number multiply by two
ldi ZH,HIGH(2*GamutTable) ; Z to tone table
ldi ZL,LOW(2*GamutTable)
add ZL,R1 ; Add tone number
ldi rmp,0 ; Add carry
adc ZH,rmp ; add 0 with carry
lpm R0,Z+; Read table value LSB to R0
out TCCR0B,R0 ; to timer control port B
lpm ; Read next table value MSB to R0
out OCR0A,R0 ; to compare match A port
ret ; done
;
; ------- Gamut table -----------
GamutTable:
.db 1<<CS01, 169 ; a #0
.db 1<<CS01, 151 ; h #1
.db 1<<CS01, 135 ; cis #2
.db 1<<CS01, 127 ; d #3
.db 1<<CS01, 113 ; e #4
.db 1<<CS01, 101 ; fis #5
.db 1<<CS01, 90 ; gis #6
.db 1<<CS01, 84 ; a' #7
.db 1<<CS01, 75 ; h' #8
.db 1<<CS01, 67 ; cis' #9
.db 1<<CS01, 63 ; d' #10
.db 1<<CS01, 56 ; e' #11
.db 1<<CS01, 50 ; fis' #12
.db 1<<CS01, 44 ; gis' #13
.db 1<<CS01, 42 ; A #14
.db 1<<CS01, 37 ; H #15
.db 1<<CS01, 33 ; CIS #16
.db 1<<CS01, 31 ; D #17
.db 1<<CS00, 226 ; E #18
.db 1<<CS00, 204 ; FIS #19
.db 1<<CS00, 181 ; GIS #20
.db 1<<CS00, 169 ; A' #21
.db 1<<CS00, 151 ; H' #22
.db 1<<CS00, 135 ; CIS' #23
.db 1<<CS00, 127 ; D' #24
.db 1<<CS00, 113 ; E' #25
.db 1<<CS00, 101 ; FIS' #26
.db 1<<CS00, 90 ; GIS' #27
.db 1<<CS00, 84 ; A'' #28
;
; End of source code
;
Besides the already applied LPM instructions in all of its sub variants no new instructions
are used here.
A serious hint for programming this into the chip: on pin 5 a large electrolytic capacitor
and a relatively low resistance of the speaker are attached. Those components conflict with
the relatively high serial programming pulses, and error messages from the programmer will
result. So please remove the electrolyt first, before starting to program, and plug it in
again after this is finished.
9.4.5 Debugging with avr_sim
The simulation with avr_sim
shows the following results. Simulated are the multiplication routine and
the gamut table access with LPM.
To initiate the hardware we step through the first instructions until the interrupts
were enabled by SEI. Then we click on the PCINT in the ports display and initiate
starting to play the melody.
Like in the first case the audio output on pin PB0 is set as output and is cleared.
Pin PB3 has its pullup resistor on and the PCINT interrupt mask enables interrupts on
both edges.
The AD converter is constantly running to measure the input voltage of ADC2,
which is on portpin PB4. The input voltage simulated is 3.75 V, which
should yield 3.75 / 5.00 * 1023 = 767 or, in the left
adjusted operating mode, 191 decimal or 0xBF.
After reaching the ADC conversion complete interrupt, the interrupt service
routine has written the upper eight bits of the result to register R2 and
has set the bAdcR flag. This triggers the routine AdcCalc: after the interrupt
woke up the controller from SLEEP. Now we have to calculate which gamut note
is selected with the potentiometer.
The instructions
124: 000032 2433 clr rMultH ; Clear MSB
125: 000033 2400 clr R0 ; Clear result LSB
126: 000034 2411 clr R1 ; dto., MSB
127: 000035 E10D ldi rmp,29 ; Number of tones plus one
clear rMultH (R3), clear the multiplication result in R0 and R1 and load the
number of available gamut tones, decimal 29, to register rmp (R16). Now
multiplication can start.
The instruction
129: 000036 9506 lsr rmp ; Shift lowest bit to carry
shifts R16 to the right and shifts bit 0 to the carry flag in the SREG. In
this case the bit is 1.
The instructions
130: 000037 F410 brcc AdcCalcNoAdd
131: 000038 0C02 add R0,rMultL ; add LSB to result
132: 000039 1C13 adc R1,rMultH ; add MSB with carry
first check if the carry flag is cleared, and adding will be skipped. Here
this is not the case and the 16 bit number in R3:R2 is added
to the result in R1:R0. In the third instrcution ADC handles the carry from
any overflows to the MSB that might have occurred during the first ADD.
The two insructions
134: 00003A 0C22 lsl rMultL ; Multiply by two
135: 00003B 1C33 rol rMultH ; Roll carry to MSB und multiply by two
shift the multiplicator in R3:R2 one position to the left and multiply the
content by two.
As R16 is still not zero, the multiplication is continued by shifting the
next bit in R16 to the carry flag. This time a zero is shifted, so adding
R3:R2 to R1:R0 is skipped.
The next step is again multiplying R3:R2 by two, again shift and rotate.
There are still ones in rmp, so we have to repeat shifting. With the
third shift, again a one is shifted to the carry.
Again R3:R2 are added to R1:R0 in 16-bit manner, with ADD/ADC. We do not
display the boring multiplication of R3:R2 by 2 that follows next.
It is getting boring, but we have to shift rmp again. This time a one
is shifted to the carry. We do not display addition of R3:R2 to R1:R0,
but this has to be done next.
The next adding of R3:R2 to R1:R0 happens here.
And the next multiplication by 2 for R3:R2 here.
Now the last 1 rolls out to carry, and we are nearly done with multiplication.
The last addition of R3:R2 to R1:R0. The following next multiplication by 2
is not necessary any more and makes no sense because R16 is already empty
and the multiplication loop will not repeat any more.
The stop watch was used to measure the time for multiplication. It shows
55 µs, which is rather fast considering the many steps of
multiplication. No need to upgrade to a ATmega with built-in hardware
multiplication though, to avoid the necessary time (that we have plenty
of) and replacing the 12 instructions of the multiplication routine by
just one single MUL instruction.
Now Z is set to the gamut table. The instructions
138: ; Tone from gamut table
139: 00003E 0C11 lsl R1 ; Tone number multiply by two
140: 00003F E0F0 ldi ZH,HIGH(2*GamutTable) ; Z to tone table
141: 000040 E9E2 ldi ZL,LOW(2*GamutTable)
multiply the result of the multiplication by 2 and load the register
pair Z with the doubled address of the gamut table.
The gamut table is here:
151: ; ------- Gamut table -----------
152: GamutTable:
153: .db 1<<CS01, 169 ; a #0
000049 A902
154: .db 1<<CS01, 151 ; h #1
00004A 9702
...
It's address is 0x000049, which is doubled to 0x0092 in Z to access the
content of the table bytewise.
Now the doubled value in R1, 0x2A, is added to the table address 0x0092.
142: 000041 0DE1 add ZL,R1 ; Add tone number
143: 000042 E000 ldi rmp,0 ; Add carry
144: 000043 1FF0 adc ZH,rmp ; add 0 with carry
Here, ZL and R1 are added, the result goes to ZL. Then rmp is set to
zero and is added with the carry flag to ZH. This ensures that the
MSB is correct. Please note that CLR rmp would have also cleared the
carry flag, so LDI is a better choice. Now Z points to 0x00BC.
The note to play at 0x00BC, which corresponds to table address 0x005E,
is
174: .db 1<<CS00, 169 ; A' #21
00005E A901
So the prescaler will be set to CS00 (prescaling by 1) and the compare
match A value to 169. That means a frequency of
f = 1,200,000 / 1 / (169 + 1) / 2 = 3,529.4 Hz
,
which is roughly the gamut note A' (should be 3,520 Hz).
Now the instruction
145: 000044 9005 lpm R0,Z+; Read table value LSB to R0
reads the LSB of the table value to R0 and increases the address in Z
by one. The result in R0 is written to the timer control register
TCCR0B and sets the prescaler to 1.
Now the instruction
147: 000046 95C8 lpm ; Read next table value MSB to R0
reads in the second byte in the gamut table, 0xA9 or decimal 169, to
R0. LPM always loads to R0 and Z is not increased here. This value
is written to the compare match A portregister.
Prescaler and compare match A value of timer TC0 have been set and
are ready to play gamut tone A' now, but the portpin PB0 is still
cleared on compare match and the sound is off.
9.4.6 Debugging with the Studio
If we write such routines such as the multiplication or the reading from the gamut table,
we would like to know if that part really works fine. At the latest if no tone emerges from
the speaker when the key is pressed, we know that we have a bug in our source code software.
There are opportunities to follow the controller's work step by step and so to search for
such bugs and to identify and correct those. The step-by-step analysis is integrated into
the Studio and its name is simulator.
To start the simulator, we just select "Start Debugging" from the menue. Before we
do that with our source code, we add the following lines:
; ---- To debug the multiplication routine
.equ debug = 1 ; Switch debugging on
.if debug == 1 ; if switch is on do the following:
ldi rmp,0xFF ; To preselect a simulated ADC value
mov rMultL,rmp ; copy to the register used for that
rjmp AdcCalc ; Jump directly to the multiplication routine
.else
; Skip all the following when the debug switch is on:
;
; ---- Reset- and Interrupt vectors ---
.CSEG ; Assemble to code segment
.ORG 0 ; To the beginning
rjmp Start ; Reset vector, Init
reti ; INT0-Int, inactive
rjmp PcIntIsr ; PCINT-Int, active
reti ; TIM0_OVF, inactive
reti ; EE_RDY-Int, inactive
reti ; ANA_COMP-Int, inactive
reti ; TIM0_COMPA-Int, inactive
reti ; TIM0_COMPB-Int, inactive
reti ; WDT-Int, inactive
rjmp AdcIsr ; ADC-Int, active
.endif
;
Those lines mask the reset and vector table if the debug switch is on. With that the
simulator loads a test number to rMultL and jumps directly to the routine, without all
the other init procedures. The reason is that the int and vector table does not allow
to add code lines prior to address 0. We could also change the init source code section
to load our test value and to jump to the routine.
After the simulator has been started, he offers manyfold information on the interior of
the simulated controller. We can look at the content of the registers (below, left), you
can start a stop watch counting cycles and execution times (left, middle). The yellow
cursor (above, right) points to the first executable instruction.
With the menue entry "Step into" (F11) the execution of this instruction is
simulated. Executed was the source code "ldi rmp,0xFF", with rmp = R16. In the
list of registers the changed value of R16 is marked in red, the program counter is now
at 0001 and the cursor points to the next instruction.
If we do not want to single step through lengthy code, we can add breakpoints. Those
are points in the code where the simulation stops and register values or ports can be
examined. Only executable instructions can have a breakpoint, other lines in the source
code cannot. To add a breakpoint we set the cursor to that code line and select
"Toggle breakpoint" from the context menue. With "Run" in the debug
menue the simulator runs until he comes to such a breakpoint. He then stops.
In our case of debugging the multiplication routine is of interest and the end of writing
the table values to the timer ports. In the first case the register R1 holds 0x1C, which
is the expected decimal 28.
This is the state of the timer when the second breakpoint is reached. The two ports that
we wrote the gamt table values to, TCCR0B and OCR0A show the correct values from the
gamut table for tone #28. Because we skipped the timer init procedure the other timer
ports are incorrect, but our multiplication and timer write is correct
With those tools we can search and identify errors in our source code.
9.5.1 Task
The controller shall play the Internationale when the key is pressed.
9.5.2 The melody to be played
This has to be played.
Those are the notes to translate the melody to the gamut table. In order to translate it
would be more convenient to not handle notes numbers but to have names associated with
those numbers. To do this we add ".equ name=number"e; for each note. Those
names start with an n, then the tones a to g, followed by the octave 0 to 4. E.g. like
that:
; Notes symbols
.equ na0 = 0
.equ nh0 = 1
.equ nc0 = 2
.equ nd0 = 3
.equ ne0 = 4
.equ nf0 = 5
.equ ng0 = 6
.equ na1 = 7
.equ nh1 = 8
.equ nc1 = 9
.equ nd1 = 10
.equ ne1 = 11
.equ nf1 = 12
.equ ng1 = 13
.equ nA2 = 14
.equ nH2 = 15
.equ nC2 = 16
.equ nD2 = 17
.equ nE2 = 18
.equ nF2 = 19
.equ nG2 = 20
.equ nA3 = 21
.equ nH3 = 22
.equ nC3 = 23
.equ nD3 = 24
.equ nE3 = 25
.equ nF3 = 26
.equ nG3 = 27
.equ nA4 = 28
The original notation of notes, that works with small and large letters cannot be used
in assembler because assembler does not know the difference between small and capital
letters.
With these definitions the table for our melody goes like this:
Melody:
.db ne1,nd1,nc1,ng0,ne0,na1,nf0,0xFF
This can be conveniently handled.
The trailing 0xFF signals that the melody ends here (if that would not be here the
controller would interprete his whole flash storage as melody and would play that on and
on).
9.5.3 Tone duration
The symbols used in music writing encode the duration over which a tone has to be played.
We therefore need an additional information on tone duration for our melody. To encode
this to our melody table we could add two bits to our notes table, one means 1/8, two means
1/4, three means 3/8 and four means 1/2. Our melody would look like this:
.equ bLow = 1<<5 ; Duration encoding, low bit
.equ bHigh = 1<<6 ; Duration encoding, high bit
.equ d12 = bLow + bHigh ; Duration 1/2, both duration bits high
.equ d14 = bHigh ; Duration 1/4, upper duration bit high
.equ d38 = bLow ; Duration 3/8, lower duration bit high
.equ d18 = 0 ; Duration 1/8, neither upper nor lower bit high
Melody:
.db ne1+d38,nd1+d14,nc1+d12,ng0+d38,ne0+d18,na1+d12,nf0+d14,0xFF
In order to decode those notes we would program the following:
; Z points to melody table
ldi ZH,HIGH(2*Musik) ; MSB pointer for LPM
ldi ZL,LOW(2*Musik) ; dto., LSB
; Read note byte
lpm R17,Z ; Read first note from table to R17
; Check end of melody
cpi R17,0xFF ; End of melody signature?
breq MelodyOff ; Switch music off
; Decode duration of note
andi R17,0b01100000 ; Isolate bits 5 and 6
ldi R16,1 ; Number of octas
sbrc R17,5 ; Check bit 5
subi R16,-1 ; Increase by one (addi is not available)
sbrc R17,6 ; Check bit 6
subi R17,-2 ; Add two
; Convert note to timer prescaler and timer CTC
lpm R17,Z+ ; Again read note, inc pointer
andi R17,0b10011111 ; Clear duration bits
[Convert and play note for duration in R16]
MelodyOff:
[Switch off music output]
We can also encode the duration in an extra byte. This would ease note processing,
but double the length of the melody table. With our simple and short melody we can
affort this without risking flash memory shortages:
Melody: ; LSB: Note or FF, MSB: Duration in octa
.db ne1,3,nd1,2,nc1,4,ng0,3,ne0,1,na1,4,nf0,2,0xFF,0xFF
Now an additional 0xFF has to be added to the table to reach an even number of
bytes in the table.
9.5.4 Tone pauses
To play notes means not only tones but also pauses in between. Not between the first and the second
note of that melody, but in between any other notes. So we need not only an end signature but also
a pause signal. We select 0xFE for that. Our table now looks like that:
Musik: ; LSB: Note or FF, MSB: Duration in octa
.db ne1,3,nd1,2,0xFE,1,nc1,4,0xFE,1,ng0,3,0xFE,1,ne0,1,0xFE,1,na1,4,0xFE,1,nf0,2,0xFF,0xFF
9.5.5 Duration to play different notes
From the controller point of view another problem arises. If we play different tones with
frequencies between 440 and 7,040 cs/s the duration of each half wave is very different.
A different number of half waves have to be absolved to come to the same tone duration. At
440 cs/s 880 CTC events have to be absolved to yield one second, but at 7,040 cs/s
those have to be 14,080 CTC events long. The number of CTC events to be performed is
reversely proportional to the frequency. We can resolve this with two opportunities:
- We divide 600.000 by the prescaler and the CTC value and get the number of CTC cycles, or
- we add this number to our gamut table and read that out with LPM.
As we are lazy and are not willing to divide 24 bit integers by 8 bit numbers in assembler and
as we do not want the controller to do that lengthy and boring procedure each time he reads a
note, we choose the second option. The C programmer sees no problem here and imports its
floating point arithmetic library here, but changes to an ATxmega.
The formulation of the gamut table, now with the duration of the tone in one octa of a second,
looks like this:
GamutTable_Duration:
.DB 1<<CS01, 169, 110, 0 ; a #0
.DB 1<<CS01, 151, 123, 0 ; h #1
.DB 1<<CS01, 135, 138, 0 ; cis #2
.DB 1<<CS01, 127, 146, 0 ; d #3
.DB 1<<CS01, 113, 164, 0 ; e #4
.DB 1<<CS01, 101, 184, 0 ; fis #5
.DB 1<<CS01, 90, 206, 0 ; gis #6
.DB 1<<CS01, 84, 221, 0 ; a' #7
.DB 1<<CS01, 75, 247, 0 ; h' #8
.DB 1<<CS01, 67, 20, 1 ; cis' #9
.DB 1<<CS01, 63, 37, 1 ; d' #10
.DB 1<<CS01, 56, 73, 1 ; e' #11
.DB 1<<CS01, 50, 112, 1 ; fis' #12
.DB 1<<CS01, 44, 161, 1 ; gis' #13
.DB 1<<CS01, 42, 180, 1 ; A #14
.DB 1<<CS01, 37, 237, 1 ; H #15
.DB 1<<CS01, 33, 39, 2 ; CIS #16
.DB 1<<CS01, 31, 74, 2 ; D #17
.DB 1<<CS00, 226, 149, 2 ; E #18
.DB 1<<CS00, 204, 220, 2 ; FIS #19
.DB 1<<CS00, 181, 56, 3 ; GIS #20
.DB 1<<CS00, 169, 114, 3 ; A' #21
.DB 1<<CS00, 151, 219, 3 ; H' #22
.DB 1<<CS00, 135, 79, 4 ; CIS' #23
.DB 1<<CS00, 127, 148, 4 ; D' #24
.DB 1<<CS00, 113, 36, 5 ; E' #25
.DB 1<<CS00, 101, 191, 5 ; FIS' #26
.DB 1<<CS00, 90, 112, 6 ; GIS' #27
.DB 1<<CS00, 84, 229, 6 ; A'' #28
; Pause for one octa of a second
; .DB (1<<CS01)|(1<<CS00), 255, 18, 0 ; Pause #254
Our table now has four bytes per tone and we can start programming the melody.
9.5.4 Processing structure
To program this we need to
- decide on a key press event, if the melody is still processed. If not, the process
has to be started by setting a start flag,
- read the next note from the melody table,
- to read the tone's prescaler and CTC values from the gamut table and to write
these to the timer,
- to read the duration over which the note will be played from the gamut table, to read
the duration factors (full notes, half notes, etc.) from the melody table and to
write this to a 16 bit counter accordingly,
- switch on (in tones) or off (in pauses) the speaker pulse output mode,
- detect the end of the melody and if detected to switch off the speaker output, clear
the timer and to re-enable key events.
Within interrupt service routines the following has to done:
- PCINT: To detect the polarity of the key input, if high then set a start flag.
- TC0-Compare-A-Int: Decrease the 16 bit counter, if zero then set an end flag.
Within the main program loop both flags are ckecked and the associated actions performed
(start melody from scratch, output next note, etc,).
9.5.5 Program
This is the final program, the source code is here.
;
; ***************************************
; * To play a melody with an ATtiny13 *
; * (C)2017 by www.avr-asm-tutorial.net *
; ***************************************
;
.NOLIST
.INCLUDE "tn13def.inc"
.LIST
;
; ------- Register ---------------------
; free: R0 .. R14
.def rSreg = R15 ; Save SREG
.def rmp = R16 ; Multi purpose register
.def rFlag = R17 ; Flag register
.equ bStart = 0 ; Start melody play
.equ bNote = 1 ; Play next note
; free: R18 .. R23
.def rCtrL = R24 ; 16 bit counter CTC events, LSB
.def rCtrH = R25 ; dto., MSB
; used: X, XH:XL for duration calculation and to save Z
; used: Y, YH:YL for octa duration
; used: Z, ZH:ZL for reading from program memory, as melody pointer
;
; ------ Ports -------------------------
.equ pOut = PORTB ; Output port
.equ pDir = DDRB ; Direction port
.equ pInp = PINB ; Input port
.equ bSpkD = DDB0 ; Speaker output pin
.equ bKeyO = PORTB3 ; Pull up key input pin, write
.equ bTasI = PINB3 ; Key input pin, read
;
; --------- Timing --------------------
; Clock = 1200000 cs/s
; Prescaler = 1, 8, 64
; CTC TOP range = 0 .. 255
; CTC divider range = 1 .. 256
; Toggle divider = 2
; Frequency range = 600 kcs/s to 36 cs/s
;
; ---- Reset- and Interrupt vectors ---
.CSEG ; Assemble to the code segment
.ORG 0 ; To the beginning
rjmp Start ; Reset-Vector, Init
reti ; INT0-Int, inactive
rjmp PcIntIsr ; PCINT-Int, active
reti ; TIM0_OVF, inactive
reti ; EE_RDY-Int, inactive
reti ; ANA_COMP-Int, inactive
rjmp TC0CAIsr ; TIM0_COMPA-Int, active
reti ; TIM0_COMPB-Int, inactive
reti ; WDT-Int, inactive
reti ; ADC-Int, inactive
;
; ---- Interrupt service routines ---
PcIntIsr: ; PCINT on key events
in rSreg,SREG ; Save SREG
sbic pInp,bTasI ; Skip next if input is low
rjmp PcIntIsrRet ; Ready
brts PcIntIsrRet ; If T flag is set, skip
sbr rFlag,1<<bStart ; Set start flag
PcIntIsrRet:
out SREG,rSreg ; Restore SREG
reti
;
TC0CAIsr: ; Timer CTC A Int
in rSreg,SREG ; Save SREG
sbiw rCtrL,1 ; Downcount 16 bit counter
brne TC0CAIsrRet ; not yet zero
sbr rFlag,1<<bNote ; Set flag for next note
TC0CAIsrRet:
out SREG,rSreg ; Restore SREG
reti
;
; ---- Program start, Init -------------
Start:
; Init stack
ldi rmp,LOW(RAMEND) ; Stack pointer to SRAM end
out SPL,rmp
; Clear T flag
clt ; Set flag inactive
; Init and configure portpins
ldi rmp,1<<bSpkD ; Speaker output pin output
out pDir,rmp ; to direction port
ldi rmp,1<<bKeyO ; Pull up on key port pin
out pOut,rmp ; to output port
; Start timer as CTC
ldi rmp,(1<<COM0A1)|(1<<WGM01) ; Clear OC0A, CTC-A
out TCCR0A,rmp ; to timer control port A
; Prescaler, timer start and Int in start routine
; PCINT for key input events
ldi rmp,1<<PCINT3 ; Anable PB3 Int
out PCMSK,rmp ; in PCINT mask port
ldi rmp,1<<PCIE ; Enable PCINT
out GIMSK,rmp ; in Interrupt mask port
; Enable sleep
ldi rmp,1<<SE ; Sleep mode idle
out MCUCR,rmp ; in MCU control port
; Enable interrupts
sei
; ---- Main program loop -----------
Loop:
sleep ; Go to sleep
nop ; Wake up
sbrc rFlag,bStart ; Skip next if start flag zero
rcall MelodyStart ; Start melody output
sbrc rFlag,bNote ; Skip next if no note to be played
rcall PlayNote ; Play next note
rjmp Loop ; Go back to sleep again
;
; ----- Flag handling routines -------------
MelodyStart: ; Start melody output
cbr rFlag,1<<bStart ; Clear flag
set ; Set T flag
ldi ZH,HIGH(2*Melody) ; Pointer to melody
ldi ZL,LOW(2*Melody)
rcall PlayNote ; Output next note
ldi rmp,1<<OCIE0A ; Enable CTC ints
out TIMSK0,rmp ; to timer interrupt mask
ret
;
PlayNote: ; Output next note
cbr rFlag,1<<bNote ; Clear flag
rcall PlayNext ; Output next note
ret
;
PlayNext: ; Play the note to which Z points
lpm rmp,Z+ ; Read note from melody table
cpi rmp,0xFF ; Check melody end
brne PlayNext1 ; Not at end
; Melody is over
ldi rmp,(1<<COM0A1)|(1<<WGM01) ; Clear OC0A, CTC-A
out TCCR0A,rmp ; to timer control port A
clr rmp ; Clear timer interrupts
out TIMSK0,rmp ; in TC0 Interrupt mask port
pop rmp ; Remove call address from stack
pop rmp
clt ; Clear T flag
ret
PlayNext1: ; Not at end
cpi rmp,0xFE ; Pause?
brne PlayNext2 ; No
; Pause, output off
ldi rmp,(1<<COM0A1)|(1<<WGM01) ; Clear OC0A, CTC-A
out TCCR0A,rmp ; to timer control port A
ldi rmp,(1<<CS01)|(1<<CS00) ; Prescal = 64
out TCCR0B,rmp ; to timer control port B
ldi rmp,255 ; CTC to largest value
ldi rCtrL,18 ; Counter to one eights
ldi rCtrH,0
lpm R16,Z+ ; Overread duration byte
ret
PlayNext2: ; Normal note
mov XH,ZH ; Save melody pointer
mov XL,ZL
ldi ZH,HIGH(2*GamutTable_Duration) ; Pointer to gamut table
ldi ZL,LOW(2*GamutTable_Duration)
lsl rmp ; Note number * 2
lsl rmp ; * 4
add ZL,rmp ; add to pointer
ldi rmp,0 ; Carry adder
adc ZH,rmp ; Add with carry
lpm rmp,Z+ ; Read prescaler
out TCCR0B,rmp ; to timer control port B
lpm rmp,Z+ ; Read CTC value
out OCR0A,rmp ; to compare match A port
lpm YL,Z+ ; Read octa duration to Y
lpm YH,Z+
mov ZH,XH ; Restore pointer to melody
mov ZL,XL
lpm rmp,Z+ ; Read duration byte
mov XH,YH ; Copy single duration
mov XL,YL
PlayNext3:
dec rmp ; Decrease duration byte
breq PlayNext4 ; final
add XL,YL ; Add octa duration, LSB
adc XH,YH ; dto. MSB plus carry
rjmp PlayNext3
PlayNext4:
mov rCtrL,XL ; copy to counter, LSB
mov rCtrH,XH ; dto., MSB
ldi rmp,(1<<COM0A0)|(1<<WGM01) ; Toggle OC0A, CTC-A
out TCCR0A,rmp ; to timer control port A
ret
;
; Gamut table with duration
GamutTable_Duration:
.DB 1<<CS01, 169, 110, 0 ; a #0
.DB 1<<CS01, 151, 123, 0 ; h #1
.DB 1<<CS01, 135, 138, 0 ; cis #2
.DB 1<<CS01, 127, 146, 0 ; d #3
.DB 1<<CS01, 113, 164, 0 ; e #4
.DB 1<<CS01, 101, 184, 0 ; fis #5
.DB 1<<CS01, 90, 206, 0 ; gis #6
.DB 1<<CS01, 84, 221, 0 ; a' #7
.DB 1<<CS01, 75, 247, 0 ; h' #8
.DB 1<<CS01, 67, 20, 1 ; cis' #9
.DB 1<<CS01, 63, 37, 1 ; d' #10
.DB 1<<CS01, 56, 73, 1 ; e' #11
.DB 1<<CS01, 50, 112, 1 ; fis' #12
.DB 1<<CS01, 44, 161, 1 ; gis' #13
.DB 1<<CS01, 42, 180, 1 ; A #14
.DB 1<<CS01, 37, 237, 1 ; H #15
.DB 1<<CS01, 33, 39, 2 ; CIS #16
.DB 1<<CS01, 31, 74, 2 ; D #17
.DB 1<<CS00, 226, 149, 2 ; E #18
.DB 1<<CS00, 204, 220, 2 ; FIS #19
.DB 1<<CS00, 181, 56, 3 ; GIS #20
.DB 1<<CS00, 169, 114, 3 ; A' #21
.DB 1<<CS00, 151, 219, 3 ; H' #22
.DB 1<<CS00, 135, 79, 4 ; CIS' #23
.DB 1<<CS00, 127, 148, 4 ; D' #24
.DB 1<<CS00, 113, 36, 5 ; E' #25
.DB 1<<CS00, 101, 191, 5 ; FIS' #26
.DB 1<<CS00, 90, 112, 6 ; GIS' #27
.DB 1<<CS00, 84, 229, 6 ; A'' #28
;
; ---- Notes symbols ----------
;
.equ na0 = 0
.equ nh0 = 1
.equ nc0 = 2
.equ nd0 = 3
.equ ne0 = 4
.equ nf0 = 5
.equ ng0 = 6
.equ na1 = 7
.equ nh1 = 8
.equ nc1 = 9
.equ nd1 = 10
.equ ne1 = 11
.equ nf1 = 12
.equ ng1 = 13
.equ nA2 = 14
.equ nH2 = 15
.equ nC2 = 16
.equ nD2 = 17
.equ nE2 = 18
.equ nF2 = 19
.equ nG2 = 20
.equ nA3 = 21
.equ nH3 = 22
.equ nC3 = 23
.equ nD3 = 24
.equ nE3 = 25
.equ nF3 = 26
.equ nG3 = 27
.equ nA4 = 28
;
; ---- Melody -----------------
Melody: ; LSB: Note or FF, MSB: Duration in octa
; Voel- ker hoert die
.db ne1,3,nd1,2,0xFE,1,nc1,4,0xFE,1,ng0,3,0xFE,1
; Sig- na- le!
.db ne0,1,0xFE,1,na1,4,0xFE,1,nf0,2,0xFF,0xFF
;
;
; End of source code
;
The source code uses one new instruction in a somehow strange way. It is the
POP instruction. This copies the upmost byte that was pushed onto the stack,
e.g. by a call to a subroutine, and increases the stack pointer by one. If
one does that two times and ignores the byte content, the calling address
is removed from the stack. A return now jumps to the previous calling address
and ignores the last call. Make sure in this case that this is in fact the
calling address of the previous call and not a pushed data byte or something
else.
9.5.6 Simulating execution
Simulation uses avr_sim
to check execution times of the first two notes of the melody played.
The first note of the melody has been loaded. Timer/counter 0 is configured
as CTC with Compare A as TOP value, which is at 56. The prescaler is 8 and
the execution time of each CTC cycle is
tCTC = 8 * (56 + 1) / 1.2 = 380 µs
The compare match interrupt is enabled and portpin PB0 is toggled each time
CTC occurs. This generates a tone with
f1 = 1,000,000 / 380 / 2 = 1,316 Hz
The first note in the melody table is ne1:
252: Melody: ; LSB: Note or FF, MSB: Duration in octa
253: ; Voel- ker hoert die
254: .db ne1,3,nd1,2,0xFE,1,nc1,4,0xFE,1,ng0,3,0xFE,1
0000A7 030B 020A 01FE 0409
ne1 is defined as
232: .equ ne1 = 11
being note 11, which is corresponding to the following entry in the gamut table:
200: .DB 1<<CS01, 56, 73, 1 ; e' #11
000083 3802 0149
With CS01 in TCCR0B the prescaler is 8, the 56 defines the compare match A
value. 73 and 1 are the durations of that note, which is 1*256 + 73 = 329. The
3 following the ne1 in the melody table says "increase duration by a
factor of 3, which yields 987 CTC cycles (or 0x03DB) and a duration of
987 * 380 µs = 375 ms.
The duration 0x03DB is in the double register R25:R24 and will be decreased
in the interrupt service routine of the compare match A int.
Z is pointing to the next note in the flash (0x0150), which points to address
0x00A8 and is multiplied by 2. This is correct (see the above listing of the
melody).
This is the time when the registers R25:R24 reach zero and the next note will
have to be played. The 379 ms are nearly exact (should be 375).
The second note of the melody has been loaded to TC0. The prescaler is again
8, but the compare match A value is now 63. That corresponds to
f2 = 1,200,000 / 8 / (63 + 1) / 2 = 1,171 Hz
The note to be played is nd1:
253: ; Voel- ker hoert die
254: .db ne1,3,nd1,2,0xFE,1,nc1,4,0xFE,1,ng0,3,0xFE,1
which is note #10:
231: .equ nd1 = 10
And note #10 is d':
199: .DB 1<<CS01, 63, 37, 1 ; d' #10
000081 3F02 0125
So the prescaler and the compare match A are fine.
The duration of the note is now (1*256 + 37) * 2 =
586 or 0x024A CTC cycles or 586 * 380 µs = 223 ms.
The counter value R25:R24 is fine, the register Z points to the third note.
250 ms have elapsed, just slightly above the 223 ms that were desired and
calculated.
The third note is a pause (0xFE). The only difference between a note and
a pause is that OCR0A is not toggled but cleared, switching PB0 output to
be always low.
Simulation is a powerful tool to debug even complex procedures step-by-step
and to measure execution times exactly. Use it to test your own designs
and to verify that the controller really does what you thought that he
should do and in a timely manner.
©2017 by http://www.avr-asm-tutorial.net