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.3.4 Simulation of the program

Simulation goes with avr_sim as follows.

Audio1 Port init 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.

Audio1 TC0 init 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.

Audio1 ADC init 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).

Audio1 ADC running The first AD conversion has made some progress. As the conversion lasts 1.386 ms the controller remains sleeping.

Audio1 ADC result read An ADC complete interrupt has occurred and the ADC result is read to R17 using the instruction:

      in rimp,ADCH ; read MSB result

Audio1 COM Conversion to the complement of 0x22 using

      com rimp ; invert value

yields 0xFF - 0x22 = 0xDD. In that way 38 inverts to 221.

Audio1 result to OCR0A 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.



Audio1 PCINT request 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.

Audio1 PB0 toggle 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.

Audio1 After reaching the compare value in A and restarting at TCNT = 0 the respective portbit changes polarity ...

Audio1 PB0 is one The pin PB0 now is high.

Audio1 CTC timing 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.


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.

Home Top Tone generation Hardware Tone control Tables Multiplication Gamut Music


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.

Sound 2 port init 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 falling edges.

Sound 2 ADC running 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.

Sound 2 starting multiplication 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.

Sound 2 first shift to carry 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.

Sound 2 first add to the result 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.

Sound 2 first multiply by 2 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.

Sound 2 second shift 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.

Sound 2 second multiplication by 2 The next step is again multiplying R3:R2 by two, again shift and rotate.

Sound 2 third shift There are still ones in rmp, so we have to repeat shifting. With the third shift, again a one is shifted to the carry.

Sound 2 adding the third 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.

Sound 2 fourth shift 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.

Sound 2 adding the fourth The next adding of R3:R2 to R1:R0 happens here.

Sound 2 fifth multiplication by 2 And the next multiplication by 2 for R3:R2 here.

Sound 2 fifth shift Now the last 1 rolls out to carry, and we are nearly done with multiplication.

Sound 2 adding the fifth 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.

Sound 2 multiplication time 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.

Sound 2 loading gamut table to Z 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.

Sound 2 adding to gamut address 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).

Sound 2 first LPM from gamut table 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.

Sound 2 second LPM from gamut table 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.

Sound 2 TC0 to gamut tone 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.


Home Top Tone generation Hardware Tone control Tables Multiplication Gamut Music


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.

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

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

Home Top Tone generation Hardware Tone control Tables Multiplication Gamut Music


9.5.6 Simulating execution

Simulation uses avr_sim to check execution times of the first two notes of the melody played.

Sound 3 TC0 starting first note 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.

Sound 3 first note, registers 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).

Sound 3 end of first note 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).

Sound 3 start of second note 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.

Sound 3 note 2 registers The counter value R25:R24 is fine, the register Z points to the third note.

Sound 3 note 2 has been played 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.

Home Top Tone generation Hardware Tone control Tables Multiplication Gamut Music


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