signal generation driven from uC - Page 1
Forum section: electronics / projects

author: Greg J
author_rank: Regular Contributor
timestamp: June 01, 2021, 04:12:18 pm
content: Yellow,I'm trying to create a simple solution to generate sound 24kHz to 65kHz output - changing frequency every 1s or so for short period of time. This is currently driven by a PIC16F18323 uC (soon to be PIC24). However, doing it via PWM generator is a bit cumbersome - I find.What is the way cool-kidsdo signal generation from uC these days, that does not involve rather expensive (and over-specified for this purpose) parts like AD9833. Ideally something that just uses SPI - and I get sine-weave out (something suitable for piezo driver).Any suggestions please ?

author: T3sl4co1l
author_rank: Super Contributor
timestamp: June 01, 2021, 04:28:44 pm
content: Who cares if it's cumbersome, it's just a dumb MCU, make it sweat.If you need to do other activities and can't spare the CPU cycles, another solution would be preferable to adding another whole-ass MCU to handle those activities.Add a DAC (or use the internal if provided) and run a DDS in software, triggered by timer interrupt so as to get consistent sample rate. Bonus if it has DMA, just keep a buffer filled with precalculated waveform data and let it scan through freely.Right, PWMDAC would have to run very fast indeed to get any ENOB at that frequency, even with a sharp filter. You'll still need an antialiasing filter for the DAC, mind -- just that it can be relaxed a bit, depending on exactly how fast you can get the DAC going.A few hundred kSps should be reasonable, but that's not much overhead (and will still need a sharp AA filter), and a sine wave oscillator or dedicated chip may well be a better solution.Tim

author: Greg J
author_rank: Regular Contributor
timestamp: June 01, 2021, 04:48:45 pm
content: I did think if there's any easy way to get 555 to work off uC input. But that requires PWM. I was also surprised how expensive AD9833 is. Wish it was 50c or so.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: June 01, 2021, 05:04:32 pm
content: What good would a 555 be, when you have a half dozen square wave generators on board?Tim

author: Kleinstein
author_rank: Super Contributor
timestamp: June 01, 2021, 05:12:01 pm
content: Genrating a square wave signal via PWM is very easy for the µC. Only if the PWM outputs are all occuplied, I would consider an external generator. Even SPI or UART could genrate a tone if really needed - though this may indeed be a bit cumberson.

author: SiliconWizard
author_rank: Super Contributor
timestamp: June 01, 2021, 05:16:43 pm
content: Quote from: Greg J on June 01, 2021, 04:12:18 pmWhy is that cumbersome?Also, you're not telling us what the signal you generate really is. Is that a sine wave? If so, how do you compute the samples?Quote from: Greg J on June 01, 2021, 04:12:18 pmOK, now we can get it's actually a sine wave.The point is not whether it's cool or not. Any additional part to the MCU will add cost. Replacing PWM with a true DAC is justified only if your requirements in terms of noise/distortion are not met. If they are, why bother? Now if they aren't, you can think of some MCU with an embedded DAC, there are a few out there. Will save you an external DAC.Now I asked about how you generate the samples, because chances are, unless you implemented something pretty fancy, that the noise/distortion introduced by errors in computing the samples is likely to prevail over that introduced by PWM encoding. Of course that's just something to ponder; maybe you did something fancy there in terms of sample computation.Note that instead of pure PWM, you could implement some sigma-delta encoder instead and get better performance. You'll probably need a beefier MCU though.

author: Nominal Animal
author_rank: Super Contributor
timestamp: June 01, 2021, 07:08:14 pm
content: A simple Pulse Density modulation for e.g. SPI output (using only the MOSI/DO line, no CS/SCK/MISO/DI) is actually very simple:LetSTATEbe an unsigned integer type state variableLetRATEbe the duty cycle, also an unsigned integerLoop:AddRATEtoSTATECarry flag reflects the output bit stateFor PDM'ing an arbitrary waveform, theRATEreflects the signal amplitude for each output bit.LimitingRATEto 87.5% of the available dynamic range (6.25% to 93.75%) means that the output stays low or high at most 16 cycles consecutively.LimitingRATEto 75% of the available dynamic range (12.5% to 87.5%) means the output stays low or high at most 8 cycles consecutively.Reducing the signal dynamic range this way can make it easier to use a passive low-pass filter and still get good output signal.It is interesting to note that when using a sine wave within 0..1, \$y = (1 - \cos \pi x) / 2\$, the cubic curve \$y = 3 x^2 - 2 x^3\$ matches the rising part to within \$\pm 0.01001\$; the absolute error is about 1% maximum. This means that a simple cubic function can be used to approximate the sinusoidal amplitude. Technically, a single period of a sinusoidal wave with period \$2 X\$ (\$x = 0 \dots 2 X\$) and dynamic range \$Y\$ (amplitude varying between \$0\$ and \$Y\$) can be written as$$y(x) = \begin{cases}3 Y \frac{x^2}{X^2} - 2 Y \frac{x^3}{X^3}, & x \le X \\- 4 Y + 12 Y \frac{x}{X} - 9 Y \frac{x^2}{X^2} + 2 Y \frac{x^3}{X^3}, & x \ge X \\\end{cases}$$Because this is a third degree polynomial, we can discretize (for consecutive integerx) usingRATE=RATE+DELTA1DELTA1=DELTA1+DELTA2DELTA2=DELTA2+DELTA3whereDELTA3is a constant that only changes sign at the beginning (\$x = 0\$) and at the middle (\$x = X\$) of each period. All threeDELTAvariables do need to be set up for a specific dynamic range and period used.This means that emitting a pulse density modulated signal representing a cubic approximation to a pure sinusoidal wave only needs four additions per output bit (found in the carry flag). Using native register size on most microcontroller, this means that an unrolled loop (generating say one full register of bits) takes four additions and one shift-via-carry per bit; or five additions and one shift. This is surprisingly lightweight.It is also possible to keep the rate constant for a specific number of bits, and only update the rate once per "word", so word cost would be three additions plus either one addition and one shift-through carry or two additions and one shift, per bit. This basically replaces the cubic curve with a polyline (linear segments) along the same curve, with each line being "word" bits long along the time axis (\$x\$).If we furthermore limit the dynamic range to 87.5% or 75%, the generated pulse stream quantization noise is pretty much all at the high end of the spectrum, so it should be much easier to filter out using a passive lowpass filter.Now, although I do not have a spectrum analyzer, I can simulate this. I'd only need to know the bit depth of the registers used (8, 16, or 32 bits), and the rate at which the microcontroller can generate (and emit via SPI) those bits. A simple program can produce the exact bitstream, and fftw3 library can calculate its exact discrete Fourier transform, giving its spectrum; only excluding practical effects like output pin slew rate and stray inductances and capacitances.(In the Raspberry Pi Pico thread I mentioned it would be so nice if the peripheral could do this; but it cannot, the instruction set is too limited. But I believe an FPGA implementation along these lines – but acknowledging the output is not a true sine wave, but a 1% absolute error cubic approximation of one – would be pretty nifty as a free-to-use IP block, eh?)

author: Greg J
author_rank: Regular Contributor
timestamp: June 01, 2021, 07:40:05 pm
content: Quote from: SiliconWizard on June 01, 2021, 05:16:43 pmAt the moment I'm using PWM generator built into PIC16.I currently do something like this in a loop (see below) - as a test. Wish I could just program the sequence - push and forget. At the end of the day, its just suppose to produce some sounds when it detects pattern of movements. Nothing fancy. But sine weave would be better.Also, it seems like programming PWM on a chip is rather convoluted. Of course PWM is not designed to be just a signal generator. And it does not produce anything approximating sine-weave .Code:int pr2_initial_value=0x61;int pr2_value=pr2_initial_value;while (1) {LATCbits.LATC0 = 1;LATCbits.LATC1 = 0;PWM1_LoadDutyValue(150);__delay_ms(100);LATCbits.LATC0 = 0;LATCbits.LATC1 = 1;PWM1_LoadDutyValue(249);__delay_ms(100);PR2 = pr2_value;pr2_value--;if (pr2_value < 0x40) {pr2_value = pr2_initial_value;}

author: Nominal Animal
author_rank: Super Contributor
timestamp: June 01, 2021, 08:48:11 pm
content: Why not just set the period to the desired frequency, and duty cycle to half that, then?That generates a square wave with the desired frequency, and will keep "playing" while the MCU does something else; no loop, no interaction needed.I don't use PICs, but it appears that if your crystal is 20 MHz, and your prescale is 16, the frequencies you can generate this way are 24038 Hz (PR2=13), 26042 Hz (PR2=12), 28409 Hz (PR2=11), 32150 Hz (PR2=10), 34722 Hz (PR2=9), 39063 Hz (PR2=8), 44643 Hz (PR2=7), 52083 Hz (PR2=6), and 62500 Hz (PR2=5); or more generally, 312500/PR2 in Hz.Using duty cycles between 1 and PR2-1 (instead of half PR2) gives you different harmonics.Edited: Ah, I think I understand: You don't want a constant frequency signal, but a signal that ramps its frequency between 24kHz and 65kHz.Instead of a loop, you can put the period (PR2) update in an interrupt, using another CCP module. Because the values are small, you could use say Q4.12 unsigned integer fixed point format, so that PR2 is set toperiod>>12 (binary, not arithmetic shift; shift zero bits in), withperiodincremented byratein each interrupt. The interrupt is very short and lightweight, giving you basically "fire and forget" behaviour. Except it's not a ramp, but ten different tones in sequence.

author: Nominal Animal
author_rank: Super Contributor
timestamp: June 01, 2021, 09:36:22 pm
content: I took a look at thePIC16F18323datasheet, to see the details of its PWM capabilities.Simplifying equation 29-1 on page 287 shows that the PWM frequency \$f\$ with PR2 value \$n\$ (\$1 \le n \le 255\$), oscillator frequency \$F\$ in Hertz, and prescaler \$p\$ (1, 4, or 16) are related via$$\frac{1}{f} = \frac{4 (p + 1) n}{F}$$so$$f = \frac{F}{4 n (p + 1)} \quad \iff \quad \frac{F}{4 f (p + 1)}$$Assuming \$F = 20\text{ MHz}\$ and prescaler \$p = 1\$, we have$$f = \frac{2500000}{n} \quad \iff \quad n = \frac{2500000}{n}$$This gives a much better frequency resolution between 24 kHz (\$n = 104\$ yields \$f = 24038\$) and 65 kHz (\$n = 38\$ yields \$f = 65789\$), or 67 individual frequencies.With \$F = 32\text{ MHz}\$, we get 107 frequencies between 24 kHz (\$n = 167\$ yields \$f = 23952\$) and 65 kHz (\$n = 61\$ yields \$f = 65574\$), and therefore a "smoother" ramp.Using an unsigned 16 bit period variable (with high 8 bits specifying \$n\$, the value to write to the PR2 register) and a signed 16 bit rate change variable, the timer interrupt ramping approach sounds very workable to me. Note, however, that because the PR2 gets shrunk by one every now and then, one must be careful with the timer overflow. Ramping the frequency the other way, from high frequencies down to low frequencies, would be easier because then one does not need to worry about the timer overflow – especially if the update is done in the timer overflow interrupt.If the timer overflow interrupt is used (so an interrupt is generated at end of each PWM period), the frequency update interval is dependent on the frequency, so higher frequencies would change faster; the ramp is no longer linear. There are ways to work around this, but before getting into any of those, it'd make sense to consider what kind of frequency ramp would be desirable anyway. At the simpler end, we could just have a look up table listing how many periods we'll generate at each frequency; eight bits would suffice for up to 0.01 seconds at 24kHz, 0.004 seconds at 65kHz.Which gets us to an arbitrary PWM waveform generation via timer overflow interrupts. You have a 16-bit counter, incremented every interrupt. An 8-bit counter specifies the index to a look-up array. Each look-up array entry contains the PR2 value (period, corresponding to the frequency generated), and the duration (a 16-bit value, the number of cycles produced at that frequency). The interrupt routine checks if the counter exceeds the duration, and if so, increments the index, and resets the counter to zero. If the new slot has zero duration, this interrupt and PWM output is disabled. To start the waveform generation, you simply ensure the interrupt and PWM generation is disabled, then reset the two counters to zero, and then enable the timer overflow interrupt and PWM output. Ta-dah!At 20 MHz, there are 67 individual frequencies, so for a simple ramp the look-up table needs 3×68 = 204 bytes of storage (can be Flash). At 30 MHz, there 107 frequencies, so 3×108 = 324 bytes of storage is needed. Note, the final slot is the end-of-tone marker; you could use a separate byte for the tone length, but that adds a bit more work to the interrupt. (If you have enough storage for multiple ramps, and want to choose between them, then instead of a counter, use a pointer.)

author: ucanel
author_rank: Regular Contributor
timestamp: June 01, 2021, 10:00:01 pm
content: SimplyPWM Period = [(PR2) + 1] • 4 • TOSC • (TMR2 Prescale Value)And Pwm Duty = %PR2 set via CCPxex: for PR2 = 20, CCPx = 10 means %50 duty,for PR2 = 20, CCpx = 05 means %25 duty,for PR2 = 48, CCpx = 24 means %50 duty.So while changing PR2 value to change the PWM frequencyyou need to change also the CCPx register for correct duty cycle.Odd and small (like 3,5,7) PR2 values will cause uneven pwm duty values.