{"id":4,"date":"2018-06-11T01:44:26","date_gmt":"2018-06-11T01:44:26","guid":{"rendered":"http:\/\/apwizardry.com\/?p=4"},"modified":"2019-04-04T17:25:21","modified_gmt":"2019-04-04T17:25:21","slug":"avr-top-octave-tone-generator","status":"publish","type":"post","link":"http:\/\/apwizardry.com\/?p=4","title":{"rendered":"AVR Top Octave Tone Generator"},"content":{"rendered":"<p>After reading the hackaday entry &#8220;ASK HACKADAY: HOW DO YOU DIY A TOP-OCTAVE GENERATOR?&#8221; (<a href=\"https:\/\/hackaday.com\/2018\/05\/24\/ask-hackaday-diy-top-octave-generator\/\">https:\/\/hackaday.com\/2018\/05\/24\/ask-hackaday-diy-top-octave-generator\/<\/a>), I decided to take up the challenge.<\/p>\n<p>On a standard 16 MHz Arduino Uno, I was able to get 10 outputs running until I ran out of CPU clock cycles.<\/p>\n<p>Switching to a 20 MHz clock, all 12 outputs are operational.<\/p>\n<p>The basic idea of the code is to generate, in real time, a table entry of bits to flip (2 bytes) and the delay until the next flip (1 byte), and an ISR that consumes the table entries while setting up an interrupt after the next delay is complete.<\/p>\n<p>The main loop generates the entries for the table. Its job is to calculate the table entries faster than the ISR can consume them. With a 16 MHz clock, the best we can do is to calculate 10 outputs while staying ahead of the ISR. With a 20 MHz clock, all 12 outputs can be calculated faster than the ISR can read them.<\/p>\n<p>Because the AVR clock is 20 MHz and the delays are in increments of 20 clock cycles, the shortest delay is 1 us. To match the Top Octave Generator values with a 2 MHz clock, we only generate 50% duty cycle outputs for the even values at 2 MHz. For the odd values, we generate a low time that is one period longer than the high time.<\/p>\n<table>\n<tbody>\n<tr>\n<th>Note<\/th>\n<th>2MHz Delay Value<\/th>\n<th>1MHz\u00a0high Delay Value<\/th>\n<th>1MHz Low Delay Value<\/th>\n<\/tr>\n<tr>\n<th>C8<\/th>\n<th>239<\/th>\n<th>119<\/th>\n<th>120<\/th>\n<\/tr>\n<tr>\n<th>B7<\/th>\n<th>253<\/th>\n<th>126<\/th>\n<th>127<\/th>\n<\/tr>\n<tr>\n<th>A#7<\/th>\n<th>268<\/th>\n<th>134<\/th>\n<th>134<\/th>\n<\/tr>\n<tr>\n<th>A7<\/th>\n<th>284<\/th>\n<th>142<\/th>\n<th>142<\/th>\n<\/tr>\n<tr>\n<th>G#7<\/th>\n<th>301<\/th>\n<th>150<\/th>\n<th>151<\/th>\n<\/tr>\n<tr>\n<th>G7<\/th>\n<th>319<\/th>\n<th>159<\/th>\n<th>160<\/th>\n<\/tr>\n<tr>\n<th>F#7<\/th>\n<th>338<\/th>\n<th>169<\/th>\n<th>169<\/th>\n<\/tr>\n<tr>\n<th>F7<\/th>\n<th>358<\/th>\n<th>179<\/th>\n<th>179<\/th>\n<\/tr>\n<tr>\n<th>E7<\/th>\n<th>379<\/th>\n<th>189<\/th>\n<th>190<\/th>\n<\/tr>\n<tr>\n<th>D#7<\/th>\n<th>402<\/th>\n<th>201<\/th>\n<th>201<\/th>\n<\/tr>\n<tr>\n<th>D7<\/th>\n<th>426<\/th>\n<th>213<\/th>\n<th>213<\/th>\n<\/tr>\n<tr>\n<th>C#7<\/th>\n<th>451<\/th>\n<th>225<\/th>\n<th>226<\/th>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Every time the Output Compare matches the Timer Count, the main loop is interrupted and the ISR runs. This will consume at least one entry in the table before it returns to the main loop.<\/p>\n<p>I added one extra 0 byte at the end of the entry to keep everything on a modulo 4 boundary, and to automatically do the pointer wrap at the end of the table by incrementing only XL. There are 256 bytes for this table, or 64 entries.<\/p>\n<p>The main loop is written in assembly for speed. The C code version of this loop is as follows:<\/p>\n<pre>#define NUM_TONES 12\r\n\r\nuint8_t tc[12];\r\nuint8_t cnt[12];\r\nint8_t  phase[12];\r\n\r\nuint8_t buf[256];\r\n\r\n\/\/ Load the terminal counts, and\r\n\/\/ initialize the counters\r\ntc[0]  = cnt[0]  = 119;  \/\/ 119 and 120\r\ntc[1]  = cnt[1]  = 126;  \/\/ 126 and 127\r\ntc[2]  = cnt[2]  = 134;\r\ntc[3]  = cnt[3]  = 142;\r\ntc[4]  = cnt[4]  = 150;  \/\/ 150 and 151\r\ntc[5]  = cnt[5]  = 159;  \/\/ 159 and 160\r\ntc[6]  = cnt[6]  = 169;\r\ntc[7]  = cnt[7]  = 179;\r\ntc[8]  = cnt[8]  = 189;  \/\/ 189 and 190\r\ntc[9]  = cnt[9]  = 201;\r\ntc[10] = cnt[10] = 213;\r\ntc[11] = cnt[11] = 225;  \/\/ 225 and 226\r\n\r\n\/\/ If phase == 0, then use the same terminal count\r\n\/\/ for the high and low time.\r\n\/\/ If phase == 1, then the low time will be\r\n\/\/ one cycle longer than the high time.\r\nphase[0] = 1;\r\nphase[1] = 1;\r\nphase[2] = 0;\r\nphase[3] = 0;\r\nphase[4] = 1;\r\nphase[5] = 1;\r\nphase[6] = 0;\r\nphase[7] = 0;\r\nphase[8] = 1;\r\nphase[9] = 0;\r\nphase[10] = 0;\r\nphase[11] = 1;\r\n\r\nvolatile uint8_t rptr = 0;\r\nuint8_t wptr = 0;\r\nuint8_t min = 119;  \/\/ Start with tc of the smallest entry\r\nuint16_t prev_tog = 0;  \/\/ First table entry has no outputs toggle\r\n\r\nwhile (1) {\r\n  uint8_t  next_min = 0xff;\r\n  uint16_t tog = 0;\r\n\r\n  for (int ii = NUM_TONES-1; ii &gt;= 0; ii--) {\r\n    cnt[ii] -= min_val;\r\n\r\n    if (cnt[ii] == 0) {\r\n      \/\/ This counter has expired\r\n      \/\/ Reload the counter\r\n      cnt[ii] = tc[ii];\r\n\r\n      \/\/ Adjust the next terminal count (if necessary)\r\n      tc[ii] += phase[ii];\r\n\r\n      \/\/ If the phase is 0, then this doesn't have any effect.\r\n      \/\/ Otherwise, this will cause the terminal count to\r\n      \/\/ increment or decrement each time the counter expires.\r\n      phase[ii] = -phase[ii];\r\n\r\n      \/* Toggle this output *\/\r\n      tog |= (1&lt;&lt;ii);\r\n    }\r\n\r\n    \/\/ Find the smallest value before counter expiration.\r\n    \/\/ The smallest value will be the delay until\r\n    \/\/ the next counter expires next pass through the loop.\r\n    if (cnt[ii] &lt; next_min) {\r\n      next_min = cnt[ii];\r\n    }\r\n  }\r\n\r\n  \/\/ Add entry to buffer\r\n  buf[wptr++] = prev_tog &amp; 0xff;\r\n  buf[wptr++] = prev_tog &gt;&gt; 8;\r\n  buf[wptr++] = min_val-1;  \/\/ 0 == smallest delay (20 clocks)\r\n  wptr = (wptr + 1) &amp; 0xff; \/\/ Make entry mod4, keep wptr on table\r\n\r\n  \/\/ The calculated delay must complete before the bits toggle.\r\n  \/\/ This delays the toggle by one pass through the loop.\r\n  min_val = next_min;\r\n  prev_tog = tog;\r\n\r\n  \/\/ Loop here until the buffer has room\r\n  while (rptr == wptr)\r\n    ;\r\n}\r\n<\/pre>\n<p>An interrupt service routine reads the table and changes the 8 bits of PORTD and 4 bits of PORTB.<\/p>\n<p>The 20-cycle loop (when the delay == 0) is:<\/p>\n<pre>dly0:   LD      r0,X+\r\n        OUT     PIND,r0\r\n        LD      r0,X+\r\n        OUT     PINB,r0\r\n        LD      dly_lsb,X+\r\n        INC     XL\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        TST     dly_lsb\r\n        BREQ    dly0\r\n<\/pre>\n<p>If we need a 40-cycle loop (delay == 1), then we can follow this with:<\/p>\n<pre>        CPI     dly_lsb,1\r\n        BREQ    dly1\r\n\r\ndly1:   NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        NOP\r\n        RJMP    dly0\r\n<\/pre>\n<p>We do the same thing for a 60-cycle loop (delay == 2). But if the delay is 80 cycles or larger (delay &gt; 2), then we set up the timer to generate an interrupt after the correct number of cycles has passed, and then we return from the ISR. This allows the main loop to get some work done instead of just burning cycles with NOPs.<\/p>\n<pre>;;; Convert delay to number of clock cycles\r\n        LDI     tmp,20\r\n        MUL     dly_lsb,tmp\r\n        MOVW    dly_lsb,r0\r\n\r\n;;; Compensate for delay in prologue and epilogue of ISR\r\n        SUBI    dly_lsb,30\r\n        SBC     dly_msb,c_zero\r\n\r\n;;; Update Output Compare for next delay\r\n        LDS     tcnt_l,TCNT1L\r\n        LDS     tcnt_h,TCNT1H\r\n        ADD     tcnt_l,dly_lsb\r\n        ADC     tcnt_h,dly_msb\r\n        STS     OCR1AH,tcnt_h\r\n        STS     OCR1AL,tcnt_l\r\n\r\n;;; Restore Status register\r\n        POP r0\r\n        OUT 0x3f,r0\r\n        RETI\r\n<\/pre>\n<p>The prologue to the ISR has to compensate for the possibility of the interrupt occurring either on a 1-cycle or 2-cycle instruction (the code uses no 3 or more cycle instructions). It does that by comparing the Output Compare value to the Timer Count inside the ISR. If the delay is one more than expected, then the interrupt happened during a 2-cycle instruction.<\/p>\n<p>The prologue of the ISR does the compensation:<\/p>\n<pre>;;; Save Status register\r\nisr:    IN      r0,0x3f\r\n        PUSH    r0\r\n;;; Compare current Timer Count to Output Compare value\r\n        LDS     tmp,TCNT1L\r\n        LDS     r0,OCR1AL\r\n        SUB     tmp,r0\r\n        SUBI    tmp,12        ; Delta for 1-cc instruction\r\n;;; If the interrupt happened on a 2-cc instruction, branch\r\n        BRNE    dly0\r\n\r\n;;; The interrupt happened on a 1-cc instruction\r\n;;; Execute 1 extra NOP to equalize the delay\r\n        NOP\r\n        NOP\r\n\r\ndly0:<\/pre>\n<p>The smallest device that can be used must have these features:<\/p>\n<ol>\n<li>At least one 16-bit Timer<\/li>\n<li>At least 512 bytes of memory (256-byte table plus 36 bytes for cnt[], tc[], and phase[])<\/li>\n<li>Supports a 20 MHz processor clock<\/li>\n<li>Has at least 12 outputs for pin toggling<\/li>\n<\/ol>\n<p>The smallest part I was able to find that meets these criteria was the ATTINY816, which costs $0.50 in 5K pricing (or $0.90 for Qty. 1 in an SOIC20 package, according to DigiKey).<\/p>\n<p>Here is the Arduino wrapper and gcc assembly source for the 20MHz 12-output version:<\/p>\n<pre>extern \"C\" {\r\n  \/\/ function prototypes\r\n  void tstart();\r\n}\r\n\r\nvoid setup() {\r\n  \/* Turn off timer0 interrupt *\/\r\n  TIMSK0 = 0;\r\n\r\n  tstart();\r\n}\r\n\r\nvoid loop() {\r\n}\r\n<\/pre>\n<pre>;;; tone_loop_20.S\r\n;;;\r\n;;;  Created: 6\/5\/2018 11:38:12 AM\r\n;;;   Author: aprimatic\r\n;;;\r\n;;; Copyright 2018 APWizardry LLC\r\n;;;\r\n;;; Redistribution and use in source and binary forms, with or\r\n;;; without modification, are permitted provided that the following\r\n;;; conditions are met:\r\n;;;\r\n;;; 1. Redistributions of source code must retain the above\r\n;;; copyright notice, this list of conditions and the following\r\n;;; disclaimer.\r\n;;;\r\n;;; 2. Redistributions in binary form must reproduce the above\r\n;;; copyright notice, this list of conditions and the following\r\n;;; disclaimer in the documentation and\/or other materials provided\r\n;;; with the distribution.\r\n;;;\r\n;;; 3. Neither the name of the copyright holder nor the names of\r\n;;; its contributors may be used to endorse or promote products\r\n;;; derived from this software without specific prior written\r\n;;; permission.\r\n;;;\r\n;;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND\r\n;;; CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\r\n;;; INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\r\n;;; MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\r\n;;; DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR\r\n;;; CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\r\n;;; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\r\n;;; NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\r\n;;; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\r\n;;; HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\r\n;;; CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR\r\n;;; OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,\r\n;;; EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r\n\r\n#define __SFR_OFFSET 0\r\n  \r\n#include &lt;avr\/io.h&gt;\r\n\r\n;;; Registers that aren't used with immediate modes\r\n#define togD      r2\r\n#define togB      r3\r\n#define prev_togD r4\r\n#define prev_togB r5\r\n#define tcnt_l    r6\r\n#define tcnt_h    r7\r\n#define cnt       r8\r\n#define tc        r9\r\n#define c_zero    r10\r\n\r\n;;; Registers that are used with immediate modes\r\n#define tmp       r16\r\n#define phase     r17\r\n#define dly_lsb   r18\r\n#define dly_msb   r19\r\n#define min_val   r20\r\n#define next_min  r21\r\n\r\n;;; Pointers to arrays\r\n#define p_cnt   0x100\r\n#define p_tc    0x110\r\n#define p_phase 0x120\r\n\r\n;;; Pointer to 256-byte buffer\r\n#define p_buf   0x200\r\n\r\n.section .text\r\n.global TIMER1_COMPA_vect\r\n\r\n;;; Timer 1 Output Compare Interrupt Service Routine\r\n;;; Preserves: Flags\r\n;;; Modifies:\r\n;;; r0, r1, tcnt_l(r8), tcnt_h(r9), tmp(r16),\r\n;;; dly_lsb(r18), dly_msb(r19), XL(r30)\r\nTIMER1_COMPA_vect:\r\n;;; PUSH FLAGS\r\n        IN      r0,0x3f                 ; 1\r\n        PUSH    r0                      ; 2\r\n;;; Read TCNT1\r\n        LDS     tmp,TCNT1L              ; 2\r\n;;; Compensate for 2-cycle instructions delaying interrupt for 1cc\r\n        LDS     r0,OCR1AL               ; 2\r\n;;; Subtract OCRA\r\n        SUB     tmp,r0                  ; 1\r\n;;; Subtract elapsed time to enter ISR\r\n        SUBI    tmp,12                  ; 1\r\n;;; If we were interrupted on a 2cc instruction, branch\r\n        BRNE    dly0                    ; 2\r\n\r\n;;; We were interrupted on a 1cc instruction\r\n;;; Add one extra NOP to equalize the paths\r\n        NOP                             ; 1-1\r\n        NOP                             ; 1\r\n                                        ;---\r\n                                        ; 11\/12\r\n;;; This is the 20cc loop if dly_val == 0\r\ndly0:   LD      r0,X+                   ; 2\r\n        OUT     PIND,r0                 ; 1\r\n        LD      r0,X+                   ; 2\r\n        OUT     PINB,r0                 ; 1\r\n        LD      dly_lsb,X+              ; 2\r\n        INC     XL                      ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        TST     dly_lsb                 ; 1\r\n        BREQ    dly0                    ; 2\r\n                                        ;---\r\n                                        ; 20\r\n;;; This is the 40cc loop if dly_val == 1\r\n        CPI     dly_lsb,1               ; 1-1\r\n        BREQ    dly1                    ; 2\r\n                                        ;---\r\n                                        ; 2\r\n;;; This is the 60cc loop if dly_val == 2\r\n        CPI     dly_lsb,2               ; 1-1\r\n        BREQ    dly2                    ; 2\r\n                                        ;---\r\n                                        ; 2\r\n;;; Multiply delay by 20\r\n        LDI     tmp,20                  ; 1-1\r\n        MUL     dly_lsb,tmp             ; 2\r\n        MOVW    dly_lsb,r0              ; 1\r\n;;; Adjust delay\r\n        SUBI    dly_lsb,30              ; 1\r\n        SBC     dly_msb,c_zero          ; 1\r\n;;; Get current timer value\r\n        LDS     tcnt_l,TCNT1L           ; 2\r\n        LDS     tcnt_h,TCNT1H           ; 2\r\n;;; Add adjusted delay to current timer value\r\n        ADD     tcnt_l,dly_lsb          ; 1\r\n        ADC     tcnt_h,dly_msb          ; 1\r\n;;; Set up next Output Compare\r\n        STS     OCR1AH,tcnt_h           ; 2\r\n        STS     OCR1AL,tcnt_l           ; 2\r\n\r\n        POP     r0                      ; 2\r\n        OUT     0x3f,r0                 ; 1\r\n        RETI                            ; 4\r\n                                        ;---\r\n                                        ; 22\r\n;;; These extra cycles keep dly1 and dly2 on 20cc boundaries\r\ndly2:   NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n                                        ;---\r\n                                        ; 18\r\n;;; Fall through\r\ndly1:   NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        NOP                             ; 1\r\n        RJMP    dly0                    ; 2\r\n                                        ;---\r\n                                        ; 18\r\n.global tstart\r\n;;; Start of code\r\ntstart: CLI\r\n        CLR     c_zero\r\n;;; Set PORTD to all outputs\r\n        LDI     tmp,0xff\r\n        OUT     DDRD,tmp\r\n;;; Set PORTB to outputs on bottom 4 bits\r\n        LDI     tmp,0x0f\r\n        OUT     DDRB,tmp\r\n        \r\n;;; Initialize tc\r\n;;; Initialize cnt\r\n        LDI     tmp,119         ; C8\r\n        STS     p_tc,tmp\r\n        STS     p_cnt,tmp\r\n        LDI     tmp,126         ; B7\r\n        STS     p_tc+1,tmp\r\n        STS     p_cnt+1,tmp\r\n        LDI     tmp,134         ; A#7\r\n        STS     p_tc+2,tmp\r\n        STS     p_cnt+2,tmp\r\n        LDI     tmp,142         ; A7\r\n        STS     p_tc+3,tmp\r\n        STS     p_cnt+3,tmp\r\n        LDI     tmp,150         ; G#7\r\n        STS     p_tc+4,tmp\r\n        STS     p_cnt+4,tmp\r\n        LDI     tmp,159         ; G7\r\n        STS     p_tc+5,tmp\r\n        STS     p_cnt+5,tmp\r\n        LDI     tmp,169         ; F#7\r\n        STS     p_tc+6,tmp\r\n        STS     p_cnt+6,tmp\r\n        LDI     tmp,179         ; F7\r\n        STS     p_tc+7,tmp\r\n        STS     p_cnt+7,tmp\r\n        LDI     tmp,189         ; E7\r\n        STS     p_tc+8,tmp\r\n        STS     p_cnt+8,tmp\r\n        LDI     tmp,201         ; D#7\r\n        STS     p_tc+9,tmp\r\n        STS     p_cnt+9,tmp\r\n        LDI     tmp,213         ; D7\r\n        STS     p_tc+10,tmp\r\n        STS     p_cnt+10,tmp\r\n        LDI     tmp,225         ; C#7\r\n        STS     p_tc+11,tmp\r\n        STS     p_cnt+11,tmp\r\n        \r\n;;; Initialize phases\r\n        LDI     tmp,1\r\n        STS     p_phase,tmp\r\n        LDI     tmp,1\r\n        STS     p_phase+1,tmp\r\n        LDI     tmp,0\r\n        STS     p_phase+2,tmp\r\n        LDI     tmp,0\r\n        STS     p_phase+3,tmp\r\n        LDI     tmp,1\r\n        STS     p_phase+4,tmp\r\n        LDI     tmp,1\r\n        STS     p_phase+5,tmp\r\n        LDI     tmp,0\r\n        STS     p_phase+6,tmp\r\n        LDI     tmp,0\r\n        STS     p_phase+7,tmp\r\n        LDI     tmp,1\r\n        STS     p_phase+8,tmp\r\n        LDI     tmp,0\r\n        STS     p_phase+9,tmp\r\n        LDI     tmp,0\r\n        STS     p_phase+10,tmp\r\n        LDI     tmp,1\r\n        STS     p_phase+11,tmp\r\n\r\n;;; Set up rptr register\r\n        LDI     XL,lo8(p_buf)\r\n        LDI     XH,hi8(p_buf)\r\n                \r\n;;; Set up wptr regsiter\r\n        LDI     ZL,lo8(p_buf)\r\n        LDI     ZH,hi8(p_buf)\r\n                \r\n;;; Set up initial min_val\r\n; uint16_t = min_val = 119;\r\n        LDI     tmp,119\r\n        MOV     min_val,tmp\r\n\r\n;;; Start with no toggle\r\n        CLR     prev_togB\r\n        CLR     prev_togD\r\n\r\n;;; Set up Timer1\r\n;;; Inital TCNT = 0\r\n        STS     TCNT1H,c_zero\r\n        STS     TCNT1L,c_zero\r\n;;; Initial OCR1A = 0x0800\r\n;;; This allows the main loop to get a few dozen entries ahead of \r\n;;; the ISR\r\n        LDI     tmp,8\r\n        STS     OCR1AH,tmp\r\n        STS     OCR1AL,c_zero\r\n\r\n;;; Normal Port Operation\r\n;;; clkIO\/1 (no prescaling)\r\n        CLR     tmp\r\n        STS     TCCR1A,tmp\r\n        LDI     tmp,1&lt;&lt;CS10\r\n        STS     TCCR1B,tmp\r\n\r\n;;; Enable OCR1A Interrupt\r\n        LDI     tmp,1&lt;&lt;OCIE1A\r\n        STS     TIMSK1,tmp\r\n\r\n;;; Enable Interrupts\r\n        SEI\r\n\r\n;;; Set up MSB of Y register (doesn't ever change)\r\n        LDI     YH,hi8(p_cnt)\r\n\r\n;;; Main loop\r\n; while (1) {\r\n;   next_min = 255;\r\nlp0:    LDI     next_min,255\r\n;   tog = 0;\r\n        CLR     togD\r\n\r\n        LDI     YL,11\r\n;   for(int ii = 11; ii &gt;= 0; ii--) {\r\n;   cnt[ii] -= min_val;\r\nlp1:    LDD     cnt,Y+(p_cnt&amp;0xff)\r\n        SUB     cnt,min_val\r\n        CLC\r\n;   if (cnt[ii] == 0) {\r\n        BRNE    ar1\r\n\r\n;     cnt[ii] = tc[ii];\r\n        LDD     tc,Y+(p_tc&amp;0xff)\r\n        MOV     cnt,tc\r\n        LDD     phase,Y+(p_phase&amp;0xff)\r\n;     tc[ii] += phase[ii];\r\n        ADD     tc,phase\r\n        STD     Y+(p_tc&amp;0xff),tc\r\n;     phase[ii] = -phase[ii];\r\n        NEG     phase\r\n        STD     Y+(p_phase&amp;0xff),phase\r\n;     tog |= (1&lt;&lt;ii);\r\n        SEC\r\n\r\nar1:    ROL     togD\r\n        ROL     togB\r\n;   }\r\n        STD     Y+(p_cnt&amp;0xff),cnt\r\n\r\n;   if (cnt[ii] &lt; next_min) {\r\n        CP      cnt,next_min\r\n        BRSH    ar2\r\n\r\n; next_min = cnt[ii];\r\n        MOV     next_min,cnt\r\n;   }\r\nar2:    SUBI    YL,1\r\n        BRCC    lp1\r\n; }\r\n;;; Store toggle bits and delay into table\r\n; buf[wptr++] = prev_tog &amp; 0xff;\r\n        ST      Z+,prev_togD\r\n; buf[wptr++] = prev_tog &gt;&gt; 8;\r\n        ST      Z+,prev_togB\r\n; buf[wptr++] = min_val - 1;\r\n        DEC     min_val\r\n        ST      Z+,min_val\r\n; wptr = (wptr + 1) &amp; 0xff;\r\n        INC     ZL\r\n\r\n; min_val = next_min;\r\n        MOV     min_val,next_min\r\n; prev_tog = tog;\r\n        MOVW    prev_togD,togD\r\n\r\n; while (rptr == wptr)\r\n;   ;\r\nlp2:    CP      XL,ZL\r\n        BREQ    lp2\r\n;;; Go back to top\r\n; }\r\n        RJMP    lp0\r\n<\/pre>\n<p>Edit: This article was the subject of an Ask Hackaday Answered article! (<a href=\"https:\/\/hackaday.com\/2018\/08\/22\/ask-hackaday-answered-the-tale-of-the-top-octave-generator\">https:\/\/hackaday.com\/2018\/08\/22\/ask-hackaday-answered-the-tale-of-the-top-octave-generator<\/a>)<\/p>\n","protected":false},"excerpt":{"rendered":"<p>After reading the hackaday entry &#8220;ASK HACKADAY: HOW DO YOU DIY A TOP-OCTAVE GENERATOR?&#8221; (https:\/\/hackaday.com\/2018\/05\/24\/ask-hackaday-diy-top-octave-generator\/), I decided to take up the challenge. On a standard 16 MHz Arduino Uno, I was able to get 10 outputs running until I ran out of CPU clock cycles. Switching to a 20 MHz clock, all 12 outputs are &hellip; <a href=\"http:\/\/apwizardry.com\/?p=4\" class=\"more-link\">Continue reading <span class=\"screen-reader-text\">AVR Top Octave Tone Generator<\/span> <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[4],"tags":[6,5],"jetpack_featured_media_url":"","_links":{"self":[{"href":"http:\/\/apwizardry.com\/index.php?rest_route=\/wp\/v2\/posts\/4"}],"collection":[{"href":"http:\/\/apwizardry.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/apwizardry.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/apwizardry.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/apwizardry.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4"}],"version-history":[{"count":22,"href":"http:\/\/apwizardry.com\/index.php?rest_route=\/wp\/v2\/posts\/4\/revisions"}],"predecessor-version":[{"id":42,"href":"http:\/\/apwizardry.com\/index.php?rest_route=\/wp\/v2\/posts\/4\/revisions\/42"}],"wp:attachment":[{"href":"http:\/\/apwizardry.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/apwizardry.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/apwizardry.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}