Home ==> Micro beginner ==> 9. Audio generator
ATtiny13

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

  1. Introduction to audio generation
  2. Hardware, components and mounting
  3. Tone control
  4. Introduction to tables
  5. Introduction to multiplication
  6. Gamut output
  7. Playing pieces of music

9.1 Introduction to audio generation

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 Hardware, components and mounting

9.2.1 The hardware scheme

Scheme audio gen 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

SpeakerThis 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

Elko 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

Mounting The speaker is tied to pin 5, via the electrolyt.

With that we can start sound-generation.


Home Top Tone generation Hardware Tone control Tables Multiplication Gamut Music


9.3 Regulating the tone frequency

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:
ClockPre-
scaler
OCR0A
=0
OCR0A
=255
9.6 Mcs/s14.8 Mcs/s18.75 kcs/s
8600 kcs/s2.34 kcs/s
6475 kcs/s292.5 Hz
25618.75 kcs/s73.1 cs/s
10244.69 kcs/s18.3 cs/s
1.2 Mcs/s1600 kcs/s2.34 kcs/s
875 kcs/s292.5 cs/s
649.38 kcs/s36.6 cs/s
2562.35 kcs/s9.15 cs/s
1024586 cs/s2.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: With the machine we can generate morse code with a regulated tone.

Home Top Tone generation Hardware Tone control Tables Multiplication Gamut Music


9.4 Task 2: To play the tones of the gamut

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.
ToneCs/sPrescCTCReal(cs/s)Delta(%)#
a4408170441.180.27%0
h4958152493.42-0.32%1
cis5508136551.470.27%2
d586.668128585.94-0.12%3
e6608114657.89-0.32%4
fis733.338102735.290.27%5
gis825891824.18-0.10%6
a'880885882.350.27%7
h'990876986.84-0.32%8
cis'11008681102.940.27%9
d'1173.328641171.88-0.12%10
e'13208571315.79-0.32%11
fis'1466.668511470.590.27%12
gis'16508451666.671.01%13
A17608431744.19-0.90%14
H19808381973.68-0.32%15
CIS22008342205.880.27%16
D2346.648322343.75-0.12%17
E264012272643.170.12%18
FIS2933.3212052926.82-0.22%19
GIS330011823296.70-0.10%20
A'352011703529.410.27%21
H'396011523947.36-0.32%22
CIS'440011364411.760.27%23
D'4693.2811284687.5-0.12%24
E'528011145263.15-0.32%25
FIS'5866.6411025882.350.27%26
GIS'66001916593.40-0.10%27
A''70401857058.820.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:
  1. 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.
  2. the EEPROM storage. This provides also 64 bytes. That would fit, but would be rather full.
  3. 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: 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.

Home Top Tone generation Hardware Tone control Tables Multiplication Gamut Music


9.4.3 Introduction to multiplication

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 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).

Decimal multiplication 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.

Binary multiplication

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.

Home Top Tone generation Hardware Tone control Tables Multiplication Gamut Music


9.4.4 Programming the gamut

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 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.

Debug menue 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- und Interruptvektoren ---
.CSEG ; Assemblieren in den Code-Bereich
.ORG 0 ; An den Anfang
	rjmp Start ; Rest-Vektor, Init
	reti ; INT0-Int, nicht aktiv
	rjmp PcIntIsr ; PCINT-Int, aktiv
	reti ; TIM0_OVF, nicht aktiv
	reti ; EE_RDY-Int, nicht aktiv
	reti ; ANA_COMP-Int, nicht aktiv
	reti ; TIM0_COMPA-Int, nicht aktiv
	reti ; TIM0_COMPB-Int, nicht aktiv
	reti ; WDT-Int, nicht aktiv
	rjmp AdcIsr ; ADC-Int, aktiv

.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.

Debug start 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.

Single step 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.

Breakpoints 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.

End of the multiplication 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.

Timer ports 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.

Home Top Tone generation Hardware Tone control Tables Multiplication Gamut Music


9.5 Playing music

9.5.1 Task

The controller shall play the Internationale when the key is pressed.

9.5.2 The melody to be played

Melody This has to be played.

Notes 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

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

Pauses between notes 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: 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 Within interrupt service routines the following has to done: 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
	reti
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
;   Völ-  ker          hört         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.

Home Top Tone generation Hardware Tone control Tables Multiplication Gamut Music


©2017 by http://www.avr-asm-tutorial.net