|
|
View previous topic :: View next topic |
Author |
Message |
dbotkin
Joined: 08 Sep 2003 Posts: 197 Location: Omaha NE USA
|
PWM for audio tones |
Posted: Tue Oct 28, 2008 12:30 pm |
|
|
Hi all...
As I have mentioned before, my application requires generating audio tones within a range of, say, 500 to 2500 or 3000 Hz. In the past I've just used CCP1 in PWM mode on 16F parts. Life was good, though of course the square wave audio sounded pretty ragged without heavy filtering. Fine for what I was doing, but now I need something better.
I'm using to an 18F part for my latest project and plan to run it at 32 MHz. That means the PWM module can't go down much below 2 kHz, so I need to take a different approach. I looked briefly at DACs, but instead I'm looking at running in PWM mode at some fairly high rate, and varying the duty cycle to give a stepped wave form after it's been through an RC filter. A pass through an LM386 with additional filtering will clean it up even more. I figure to get 16 steps per cycle without getting the processor too bogged down changing the CCP duty cycle.
What I'm looking for is advice from people who have been here. What's a good PWM period to use - is 10 kHz enough? 20, 50, 100 kHz? I don't want to worry about EMI, but I want to simplify the filtering as much as possible. I don't need a perfect sine wave, but are 16 steps enough to make it sound decent? |
|
|
FvM
Joined: 27 Aug 2008 Posts: 2337 Location: Germany
|
|
Posted: Tue Oct 28, 2008 1:01 pm |
|
|
I think, a 1-bit (Sigma-Delta) DA would the most simple solution. A first order modulator is made by summing the input signal in an accumulator. The carry bit is copied to the output. |
|
|
dbotkin
Joined: 08 Sep 2003 Posts: 197 Location: Omaha NE USA
|
|
Posted: Tue Oct 28, 2008 1:30 pm |
|
|
How is that simpler than setting the PWM duty cycle using value from a 16-element table?
I have to confess, not being an analog engineer (or a software engineer for that matter) that I'm a little fuzzy on how a 1-bit DAC is fundamentally different than a PWM output - except it seems to me a 1-bit DAC done in firmware would need to be updated at a much higher rate than the PWM duty cycle. Is my thinking here wrong? |
|
|
FvM
Joined: 27 Aug 2008 Posts: 2337 Location: Germany
|
|
Posted: Tue Oct 28, 2008 2:45 pm |
|
|
The required update rate is higher, but not much higher, rewarded by a more simple signal generation algorithm. Both techniques have their adavantages. 1-Bit audio coding schemes have been frequently used and also recently discussed in this forum. |
|
|
RLScott
Joined: 10 Jul 2007 Posts: 465
|
Re: PWM for audio tones |
Posted: Tue Oct 28, 2008 2:47 pm |
|
|
I have used a 32-level DAC to generate a sine wave to be used as a tuning fork replacement in tuning musical instruments and it worked just fine. 16 levels would probably be OK too, if you are not trying to eliminate every harmonic. As for the PWM period, I would not worry about EMI. You are going to put your low-pass filter so close to the CCP pin that there won't be much of an opportunity for radiation - no more than, say, your clock. I would just make the PWM period as short as possible, consistent with having 16 different duty cycles, i.e. PR2 = 16. (or 4 if you want to spend the extra time twiddling with the low-order 2 bits of duty-cycle). That should be very easy to filter. _________________ Robert Scott
Real-Time Specialties
Embedded Systems Consulting |
|
|
dbotkin
Joined: 08 Sep 2003 Posts: 197 Location: Omaha NE USA
|
|
Posted: Tue Oct 28, 2008 10:42 pm |
|
|
So here's my idea. No, I haven't tested it yet, I'm waiting on the chip and in the mean time slogging through porting what I have done for this project from the 16F887 I started with to the 18F4620.
The idea is to load TIMER0 with a value that will determine its overflow interrupt rate, from 125 uS down to maybe 25 uS or so. This equates to 16 interrupts per cycle at 500 to 2500 Hz. Here's the code I have to do the work:
Code: |
char s_tableidx;
const int s_table[] = {0,97,180,235,255,235,180,97,
0,-98,-181,-236,-255,-236,-181,-98};
int t0preload;
#int_rtcc
void pwm_load(void) {
set_pwm1_duty(s_table[s_tableidx++ & 0x0f]);
set_timer0(t0preload);
}
|
Than, somewhere else in the program:
Code: | t0preload = 256 - (62500 / frequency); |
The ISR uses 28 instructions. Worst case (2.5 kHz tone), that's 14% of the processor plus whatever the interrupt latency is, but in most cases it will be less than 5%.
Now the only question is if it will work, I guess... or if someone's got a more efficient idea. Efficient in this case means less processor overhead. |
|
|
Rohit de Sa
Joined: 09 Nov 2007 Posts: 282 Location: India
|
|
Posted: Tue Oct 28, 2008 11:46 pm |
|
|
dbotkin,
Have a look here http://www.romanblack.com/picsound.htm .
This is a link to the BTc algorithm by Roman Black. Its pretty good for low fidelity sound. Nothing beats the simplicity of the output hardware, and low memory consumption. And if you are working with low frequencies, the sound quality is reasonable.
Rohit |
|
|
RLScott
Joined: 10 Jul 2007 Posts: 465
|
|
Posted: Wed Oct 29, 2008 6:35 am |
|
|
dbotkin wrote: |
Code: |
#int_rtcc
void pwm_load(void) {
set_pwm1_duty(s_table[s_tableidx++ & 0x0f]);
set_timer0(t0preload);
}
|
|
From the time that Timer 0 overflows until the time your code gets to the set_timer0(t0preload), a lot of time has already gone by, and that time is not being counted in your calculation of the overall period. How important is it to you to have the frequency of the 500 to 3000 Hz tone be accurate? Also, you have not shown what period you have chosen for the PWM. But based on the values in s_table[], I would guess it is not very high, so you will have difficulty filtering it out. _________________ Robert Scott
Real-Time Specialties
Embedded Systems Consulting |
|
|
dbotkin
Joined: 08 Sep 2003 Posts: 197 Location: Omaha NE USA
|
|
Posted: Wed Oct 29, 2008 7:39 am |
|
|
RLScott wrote: |
From the time that Timer 0 overflows until the time your code gets to the set_timer0(t0preload), a lot of time has already gone by, and that time is not being counted in your calculation of the overall period.
|
Very true, though it will be a constant amount and easily allowed for when setting the t0preload value.
Quote: | How important is it to you to have the frequency of the 500 to 3000 Hz tone be accurate? |
Not teribly, thoguh of course the closer it is, the better. A few Hz one way or the other below 1 kHz is not at all critical.
Quote: | Also, you have not shown what period you have chosen for the PWM. But based on the values in s_table[], I would guess it is not very high, so you will have difficulty filtering it out. |
125 kHz, selected to get the duty cycle down to 8 bit resolution. This vastly reduces the number of instructions required to set the duty cycle.
Rohit,
I did look at Roman's method. Just for fun, I found a sine wave .WAV file and found that his method will output alternating strings of 7 to 8 0s and 1s, giving a nice triangle wave. I'd still have the interrupt to deal with, and the filter arrangement would be similar if not identical. I think I'd be able to get closer to a sine wave with PWM at the same interrupt rate. |
|
|
RLScott
Joined: 10 Jul 2007 Posts: 465
|
|
Posted: Wed Oct 29, 2008 10:35 am |
|
|
[quote="dbotkin"]
Code: |
const int s_table[] = {0,97,180,235,255,235,180,97,
0,-98,-181,-236,-255,-236,-181,-98};
set_pwm1_duty(s_table[s_tableidx++ & 0x0f]);
|
Here's another problem. The duty cycle parameter to set_pwm1_duty() is an unsigned value. But you are feeding it signed values. They will be interpreted incorrectly and you won't get anything like a sine wave. For 8-bit resolution you should use offset unsigned numbers centered around 128, not signed values centered around 0. _________________ Robert Scott
Real-Time Specialties
Embedded Systems Consulting |
|
|
dbotkin
Joined: 08 Sep 2003 Posts: 197 Location: Omaha NE USA
|
|
Posted: Wed Oct 29, 2008 11:07 am |
|
|
Excellent point! Thanks for catching that. It would seem I'd have built a firmware full-wave rectifier.
Code: | const int s_table[] = {128,176,218,246,255,246,218,176,
128,79,37,9,0,9,37,79}; |
|
|
|
dbotkin
Joined: 08 Sep 2003 Posts: 197 Location: Omaha NE USA
|
|
Posted: Fri Oct 31, 2008 11:54 am |
|
|
My 18F4620 arrived yesterday afternoon. Took a little playing around to get the code that had been working on the 16F887 working on it, then I played around with the CCS bootloader for a while - with zero success. It seems I can't get the PC's serial buffers disabled enough to work. I'm using an FT232RL to talk to the PIC, and the bootloader gets every other line. Oh, well.
But I did get the interrupt driven, table lookup sine wave audio thingy tested. Once I got it all sorted out, here's what I get:
I do love it when a plan comes together... :) Sounds SO nice. It'll be even better after a pass through an active filter/amp stage. Now to see how well it plays with the rest of the functions. I may need to adjust the preload value a little to get the frequency on the mark.
RLScott, thanks for pointing out the problem with the duty cycle numbers. I also made the table long ints. I was getting only eight coarse steps making up the lower half of the wave. After reading the manual I discovered that if you set the PWM duty cycle with 8-bit value, it gets shifted left two bits to make it 10 bits. |
|
|
Guest
|
|
Posted: Fri Oct 31, 2008 4:43 pm |
|
|
dbotkin : nice achievement,
could you please post the full source code?
cheers |
|
|
dbotkin
Joined: 08 Sep 2003 Posts: 197 Location: Omaha NE USA
|
|
Posted: Fri Oct 31, 2008 8:56 pm |
|
|
Sure...
Code: |
const long int s_table[] = {128,176,218,246,255,246,218,176,
128,79,37,9,0,9,37,79};
char s_table_idx;
#int_rtcc
void pwm_tone(void) {
set_pwm1_duty(s_table[s_table_idx++ % 8]);
set_timer0(t0preload);
}
...
t0preload = 268 - (62500 / frequency);
|
You will likely need to adjust the value (268) in that last line to tune it to your proc & speed. Set up timer0 for 1 microsecond resolution, 256 uS overflow. You could go with a lower resolution, say 2 uS and adjust accordingly.
I cut s_table down to 8 entries. This gives fewer RTCC interrupts, but still with pretty respectable results. There's a very distinct stair-step pattern to the wave, but it souds so much better than a square wave! Again, I'm sure once I run it through an amp with some active filtering it will be even better.
Code: |
const long int s_table[] = {128,218,256,218,128,37,0,37};
char s_table_idx;
#int_rtcc
void pwm_tone(void) {
set_pwm1_duty(s_table[s_table_idx++ % 16]);
set_timer0(t0preload);
}
...
t0preload = 268 - (62500 / (frequency/2));
|
I first used it by leaving the RTCC interrupt turned on, and just used output_high() and output_float() to enable and disable the PWM output. It works, but for higher audio frequencies it really loaded down the processor - and bear in mind, this is an 18F4620 running at 32 MHz. Instead, I enable & disable the INT_RTCC. Before I enable it I set s_table_idx to 0 and load timer0 with t0preload, just to avoid annoying clicks. Now I can get 2.5 or 3 kHz without any real impact - though I'm not doing much else while the sound is on. Point is, there's no performance hit when the sound is off.
Overall I'm really happy with it. I'm able to generate very short, very quick bursts of pretty good sounding tones. |
|
|
|
|
You cannot post new topics in this forum You cannot reply to topics in this forum You cannot edit your posts in this forum You cannot delete your posts in this forum You cannot vote in polls in this forum
|
Powered by phpBB © 2001, 2005 phpBB Group
|