Continue to Site

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.

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

12 simultaneous & unique frequencies (PWM)

Status
Not open for further replies.
8bit counter will fit in 8 bit variable. 8 bit counter is enough if you are clever.

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.
 
Ok.. so how many cycles your code takes to update 12 channels? What is confusing for me is that you say that your code takes ~50 cycles, but then you say that my 128 cycles is not feasible..

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.
 
Believe me or not, but I know the compiler and the hardware and I know exactly what every line of C code does and how it translates to asm. And I can tell you that asm programmers vs C programmers.. the C compiler wins. The compiler is the result of many years of development and written by experienced asm programmers etc.

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.
 
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
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:
all you get is 12 square wave frequency outputs
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!
 
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!

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:
8 bit microcontroller can do a lot if you mix all the notes and sounds in software and output one high freq pwm signal
True, but the OP wants polyphonic individual outputs to 12 speakers; not a mix.
 
True, but the OP wants polyphonic individual outputs to 12 speakers; not a mix.

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)..??
 
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)..??
Me too I would like to understand that as well.
 
It's been puzzling me, too :)
 
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.
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:
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?

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?
 
Seriously? It is not enough for you to see the code for 1 channel?

if you post code like this:

Code:
process_channel_1_first_byte:
LDS R24, channel_1_counter
DEC R24
STS channel_1_counter, R24
BREQ process_channel_1_second_byte

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

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?
 
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.
 
Following your "algorithm", I programmed this in 5 minutes:

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:
Speaking of algorithms, I think that something like this will be faster:

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

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:
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:
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?
 
I ran it like this:

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
 
Status
Not open for further replies.

New Articles From Microcontroller Tips

Back
Top