1. Welcome to our site! Electro Tech is an online community (with over 170,000 members) who enjoy talking about and building electronic circuits, projects and gadgets. To participate you need to register. Registration is free. Click here to register now.
    Dismiss Notice

12 simultaneous & unique frequencies (PWM)

Discussion in 'AVR' started by wip, Mar 20, 2014.

  1. NorthGuy

    NorthGuy Well-Known Member

    Joined:
    Sep 8, 2013
    Messages:
    1,218
    Likes:
    206
    Location:
    Northern Canada
    If you want to generate 200 Hz, 255-bit counter will require timer interrupt frequencies of 50kHz or less. At 50kHz, at the other end of the spectrum - 440Hz, 114 counts will give you 438.5Hz and 113 counts will give you 442.5Hz. This is 4Hz difference. Looking at the list of frequencies, it looks too much of a difference to me. So, I would go with 16-bit counters, which bears almost no overhead with the approach that I suggested.
     
  2. NorthGuy

    NorthGuy Well-Known Member

    Joined:
    Sep 8, 2013
    Messages:
    1,218
    Likes:
    206
    Location:
    Northern Canada
    What I tried to say is that the approach with using registers as varibles doesn't take into account the necessity to save and restore these registers at the entry/leave of the interrupts. Therefore, 123 is not an accurate estimate for this approach.
     
  3. NorthGuy

    NorthGuy Well-Known Member

    Joined:
    Sep 8, 2013
    Messages:
    1,218
    Likes:
    206
    Location:
    Northern Canada
    That's nothing to believe or not to believe here. Assember code that I posted runs at roughly 4 cycles per channel per interrupt, and the C code that you posted runs at roughly 15 cycles per channel per interrupt. That's a fact. It means that in this particular case Assembler code is capable of generating code which is about 4 times faster than the C code. OP can use this fact to better his product. That's it.

    I don't think it gives us any ground for generalizations and certainly has nothing to do with years of compiler development. If you believe that you can write a C (or Assembler, Python, who cares ...) program that works faster than 4 cycles per channel per interrupt, just do it, and I think OP will be happy to use it.
     
  4. dave

    Dave New Member

    Joined:
    Jan 12, 1997
    Messages:
    -
    Likes:
    0


     
  5. misterT

    misterT Well-Known Member Most Helpful Member

    Joined:
    Apr 19, 2010
    Messages:
    2,697
    Likes:
    368
    Location:
    Finland

    I have not seen any code from you that updates the channels in 4 cycles.. maybe you did not understand fully my example.. there is one increment, two comparisons and then setting or clearing a bit in register.. and setting the counter to zero. Impossible in 4 cycles.. well maybe possible if you have registers to spare, but like you said, you need to save and restore execution context when you enter/exit the function (interrupt). Write full code, simulate it and post the results.. or shut up. Or does it take you full day to write 100 lines of asm and another day to debug it?

    And at the end, my code was supossed to showcase what is possible and what is not possible. I try to put some value in my posts and help the OP. Your posts just insult my work here..
     
    Last edited: Mar 23, 2014
  6. alec_t

    alec_t Well-Known Member Most Helpful Member

    Joined:
    Jul 10, 2011
    Messages:
    9,259
    Likes:
    1,218
    Location:
    Cardiff, Wales
    Are you happy with that? It will sound dreadful :(. Personally I'd want a reasonable sine (or other melodic) wave from. That would require n waveform samples per note cycle, so would push up the data rate by a factor of n and probably require one processor per note of a top octave!
     
  7. misterT

    misterT Well-Known Member Most Helpful Member

    Joined:
    Apr 19, 2010
    Messages:
    2,697
    Likes:
    368
    Location:
    Finland
    I agree. 8 bit microcontroller can do a lot if you mix all the notes and sounds in software and output one high freq pwm signal to a proper low pass filter.

    But, the OP said he needs to drive 12 speakers dedicated to each channel.. I do not understand why.. if sound quality is the goal, then this is not the right way to do it. More expensive and less quality. Spending money for 12 speakers and then trying to save money in the driver that actually is the key factor in sound quality. asm or c.. does not matter if the idea is faulty to start with.
     
    Last edited: Mar 23, 2014
  8. alec_t

    alec_t Well-Known Member Most Helpful Member

    Joined:
    Jul 10, 2011
    Messages:
    9,259
    Likes:
    1,218
    Location:
    Cardiff, Wales
    True, but the OP wants polyphonic individual outputs to 12 speakers; not a mix.
     
  9. misterT

    misterT Well-Known Member Most Helpful Member

    Joined:
    Apr 19, 2010
    Messages:
    2,697
    Likes:
    368
    Location:
    Finland
    I am not an audiophile.. could somebody explain why you would need dedicated speakers for all notes (and then try to save money in the driver that generates the notes)..??
     
  10. atferrari

    atferrari Well-Known Member

    Joined:
    Oct 8, 2003
    Messages:
    2,812
    Likes:
    121
    Location:
    Buenos Aires - Argentina
    Me too I would like to understand that as well.
     
  11. alec_t

    alec_t Well-Known Member Most Helpful Member

    Joined:
    Jul 10, 2011
    Messages:
    9,259
    Likes:
    1,218
    Location:
    Cardiff, Wales
    It's been puzzling me, too :)
     
  12. jjw

    jjw Member

    Joined:
    Apr 16, 2012
    Messages:
    262
    Likes:
    15
    Location:
    Helsinki, Finland
    In this link:http://www.pic24.ru/doku.php/en/osa/articles/pk2_osa_piano
    he is doing exactly this.
    He is sampling the sum of the notes from sinetable ( 8 channels max ) at about 19KHz and uses PWM at 78KHz
    Could use more channels with lower sampling rate.
    12 channels with AVR should be no problem
     
    Last edited: Mar 23, 2014
  13. misterT

    misterT Well-Known Member Most Helpful Member

    Joined:
    Apr 19, 2010
    Messages:
    2,697
    Likes:
    368
    Location:
    Finland
    Yes, that is nothing compared to this:
     
  14. NorthGuy

    NorthGuy Well-Known Member

    Joined:
    Sep 8, 2013
    Messages:
    1,218
    Likes:
    206
    Location:
    Northern Canada
    Seriously? It is not enough for you to see the code for 1 channel? You want me to copy the same code 12 times over?

    Like I said, I don't work with AVRs and I don't have AVR tools installed. Therefore I cannot compile or simulate. However 1 cycle per command is sort of easy enough to figure out the speed without simulation. It's all explained in post #26 (have you read it?). If you're genuinly interested, take my code from post #26 and simulate it.

    Or, I have MPLAB ASM30 suite for PICs installed on my computer at the moment. I can compile and time similar program for PIC. I will use only 8-bit commands which have AVR equivalents, so it will be comparable to AVR. PIC is also 1 cycle per command, and 2 cycles when it branches. Exact the same timing as AVR. This won't help OP (who seems alreedy left), but it can be used to measure the timing. Do you want me to do this?
     
  15. misterT

    misterT Well-Known Member Most Helpful Member

    Joined:
    Apr 19, 2010
    Messages:
    2,697
    Likes:
    368
    Location:
    Finland
    if you post code like this:

    Code (ASM):

    process_channel_1_first_byte:
    LDS R24, channel_1_counter
    DEC R24
    STS channel_1_counter, R24
    BREQ process_channel_1_second_byte
     
    Code (ASM):

    process_channel_1_second_byte:
    LDS R24, channel_1_counter+1
    DEC R24
    STS  channel_1_counter+1, R24
    BRCC process_channel_2_first_byte ; I hope I got this opcode right?
    ; our counter has expired
    ; toggle the LED here. This is my first exercise with AVR.
    ; I assume this can be done in 6 instructions
    ; I skip this part
    LDI R24, lsb_byte_of_the_channel_1_tick_count
    STS channel_1_counter, R24
    LDI R24, msb_byte_of_the_channel_1_tick_count
    STS channel_1_counter+1, R24
    JMP process_channel_2_first_byte
     
    and say that it toggles the output pin at controlled frequency in 4 cycles.. then yes.. I do not see anything from your code. I'm blinded by stupidness.
     
    Last edited: Mar 23, 2014
  16. NorthGuy

    NorthGuy Well-Known Member

    Joined:
    Sep 8, 2013
    Messages:
    1,218
    Likes:
    206
    Location:
    Northern Canada
    Well, I cannot post the compiled/timed code for AVR because I'm not going to install AVR tools on my computer. Do you want me to post the compiled/timed code for PIC?
     
  17. misterT

    misterT Well-Known Member Most Helpful Member

    Joined:
    Apr 19, 2010
    Messages:
    2,697
    Likes:
    368
    Location:
    Finland
    no.. Like I said before, my code was the naive approach.. I just realised.. now that I bothered to go through your code.. that you had totally different approach to the problem. Can't compare different algorithms. And you just proved that choosing the correct algorithm is most important thing. C will get you there faster than asm.
     
  18. misterT

    misterT Well-Known Member Most Helpful Member

    Joined:
    Apr 19, 2010
    Messages:
    2,697
    Likes:
    368
    Location:
    Finland
    Following your "algorithm", I programmed this in 5 minutes:

    Code (C):

    #include <avr/io.h>
    #include <stdint.h>

    #define sbi(b,n) (b |= (1<<n))          /* Set bit number n in byte b   */
    #define cbi(b,n) (b &= (~(1<<n)))       /* Clear bit number n in byte b */
    #define fbi(b,n) (b ^= (1<<n))          /* Flip bit number n in byte b  */

    register uint8_t counter1 asm("r2");
    register uint8_t counter2 asm("r3");
    register uint8_t counter3 asm("r4");
    register uint8_t counter4 asm("r5");
    register uint8_t counter5 asm("r6");
    register uint8_t counter6 asm("r7");
    register uint8_t counter7 asm("r8");
    register uint8_t counter8 asm("r9");
    register uint8_t counter9 asm("r10");
    register uint8_t counter10 asm("r11");
    register uint8_t counter11 asm("r12");
    register uint8_t counter12 asm("r13");

    const uint8_t top[12] = {112, 119, 126, 134, 141, 150, 159, 168, 178, 189, 200, 212};

    int main(void)
    {
        while(1)
        {
            /* Decrement each counter */
            counter1--;
            counter2--;
            counter3--;
            counter4--;
            counter5--;
            counter6--;
            counter7--;
            counter8--;
            counter9--;
            counter10--;
            counter11--;
            counter12--;
       
            /* Check if counter is zero */
            if (counter1 == 0) {counter1 = 112; fbi(PORTB, 0);}
            if (counter2 == 0) {counter2 = 119; fbi(PORTB, 0);}
            if (counter3 == 0) {counter3 = 126; fbi(PORTB, 0);}
            if (counter4 == 0) {counter4 = 134; fbi(PORTB, 0);}
            if (counter5 == 0) {counter5 = 141; fbi(PORTB, 0);}
            if (counter6 == 0) {counter6 = 150; fbi(PORTB, 0);}
            if (counter7 == 0) {counter7 = 159; fbi(PORTB, 0);}
            if (counter8 == 0) {counter8 = 168; fbi(PORTB, 0);}
            if (counter9 == 0) {counter9 = 178; fbi(PORTB, 0);}
            if (counter10 == 0) {counter10 = 189; fbi(PORTB, 0);}
            if (counter11 == 0) {counter11 = 200; fbi(PORTB, 0);}
            if (counter12 == 0) {counter12 = 212; fbi(PORTB, 0);}
        }
    }
     
    takes 48 cycles, best case, and 99 cycles worst case (all counters zero) to update all channels. That is roughly the same as your asm code. Can't compete with C.
     
    Last edited: Mar 23, 2014
  19. NorthGuy

    NorthGuy Well-Known Member

    Joined:
    Sep 8, 2013
    Messages:
    1,218
    Likes:
    206
    Location:
    Northern Canada
    Speaking of algorithms, I think that something like this will be faster:

    Code (C):
    #include <stdio.h>
    #define F 100000 // half a period for 200kHz interrupt
    #define COUNTER_BITS 11
    #define ASIZE (1<<COUNTER_BITS)
    #define MASK (ASIZE-1)
     
    // assuming 16-bit ints
    int x[ASIZE];
    int p;
    int f[12] = { F/207, F/220, F/233, F/247, F/262, F/277, F/294, F/311, F/330, F/349, F/370, F/392 };
     
    void pwm_init() {
     
      int i;
      int mask;
     
      for (i = 0; i < ASIZE; i++) {
        x[i] = 0;
      }
     
      mask = 1;
      for (i = 0; i < 12; i++) {
        x[f[i]] |= mask;
        mask <<= 1;
      }
     
      p = 0;
    }
     
    inline void toggle(int i) {
      // toggles PWM on channel i
    }
     
    void interrupt_routine() {
     
      int i;
      int z;
      int mask;
     
      p++;
      z = x[p&MASK];
     
      if (z) {
        x[p&MASK] = 0;
        mask = 1;
        for (i = 0; i <12; i++) {
          if (z&mask) {
            toggle(i);
            x[(p+f[i])&MASK] |= mask;
          }
          mask <<= 1;
        }
      }
    }
     
     
    void main() {
     
      pwm_init();
      while (1) {
        // should really be run in the interrupt
        interrupt_routine();
      }
     
    }
    You can test it in your test suite
     
    Last edited: Mar 23, 2014
  20. NorthGuy

    NorthGuy Well-Known Member

    Joined:
    Sep 8, 2013
    Messages:
    1,218
    Likes:
    206
    Location:
    Northern Canada
    That's much better. But you're still using registers. This means that you will have to convince the USB program not to use the registers that you're using. It might be possible to do so, but if it's possible, the Assembler program can benefit from using resgisters this too:

    Instead of this:

    Code (ASM):
    process_channel_1_first_byte:
    LDS R24, channel_1_counter
    DEC R24
    STS channel_1_counter, R24
    BREQ process_channel_1_second_byte
    it could use this (assuming counter1's lsb is in R2):

    Code (ASM):
    process_channel_1_first_byte:
    DEC R2
    BREQ process_channel_1_second_byte
    This would decrease the run time almost 2 times and it would be about 26 cycles per interrupt on average.

    The difference probably stems from the fact that your new C program uses one instruction to decrement, and then another instruction to compare. The assembler code can decrement and check the result in one instruction. It would be interesting to see the disassemble of your new C program. Would you mind posting it here?
     
  21. misterT

    misterT Well-Known Member Most Helpful Member

    Joined:
    Apr 19, 2010
    Messages:
    2,697
    Likes:
    368
    Location:
    Finland
    I ran it like this:

    Code (C):

    void main() {
     
        pwm_init();
        while (1) {
            asm("nop");

            // should really be run in the interrupt
            for (int i=0; i<1000; i++) {
            interrupt_routine();
            }
         
            asm("nop");
            }
     
    }
     
    from "nop to nop" it took 31696 cycles compiled with -O3
     

Share This Page