STM32F103 DDS + PWM DAC. - Page 1
Forum section: electronics / general

author: DELTA67
author_rank: Regular Contributor
timestamp: November 08, 2024, 09:38:24 am
content: Hi everybody,I'm trying to port this arduino DDS signal generator to the Blue Pill:https://interface.khm.de/index.php/lab/interfaces-advanced/arduino-dds-sinewave-generator/I configured TIM1 to genrate a PWM carrier frequency at 281KHz and fires an interrupt each overflow. At this freq the duty resolution is 8bits so a 256 bytes sine LUT is provided.In the TIM1 interrupt handler an index in the LUT is incremented and the corresponding value is put in the CCR to adjust the duty cycle.The LPF is 1k + 0.1u.The result is the attached screen shot. a beautiful 1KHz sine wave.Code:volatile uint8_t i;void TIM1_UPhandler(void){TIM1->CCR = SINE_LUT[i++];TIM1->SR = 0;// clear interrupt flag bits}The questions, please:1- How to calculate the best values for the LPF elements?I've read some where that the value of R depends on the GPIO pin output driving capability. How?2- What's the MAXimum sine frequency I can get from this configuration? How to compute it?3- How to generate all the range from 0 to MAX Hz?I've tried the algorithm given in the above link but with no success:Code:unsigned int freq = 1000;// starting freq = 1000 Hzunsigned int refclk = 281000;volatile unsigned int phaccu; // phase accumulatorvolatile unsigned char phase;volatile unsigned int tword; // tunning word (phase increment)tword = (1<<24)*freq/refclk;void TIM1_UPhandler(void){phaccu += tword;// increment the phase accuphase = phaccu >> 24; // use upper 8 bits as indexTIM1->CCR = SINE_LUT;TIM1->SR = 0;// clear interrupt flag bits}What I'm missing?4- Can we use only 1/4 of the LUT to generate the sine wave? How?Any ideas, suggestions, algorithms .... are very appreciated.Thanks

author: gpr
author_rank: Contributor
timestamp: November 08, 2024, 12:58:38 pm
content: Some python code to generate quarter of sine lookup table with arbitrary number of bits precision:https://github.com/gproskurin/learning/blob/master/embedded/stm32_midiplayer/lookup_gen.pyCorresponding C++ code to look up those tables:https://github.com/gproskurin/learning/blob/2fa53b832005f817e942e5689e60af3b52bd38db/embedded/stm32_midiplayer/src/player.cc#L73-L122May be not ready to use in your project as-is, but you can easily tweak it for your needsAs a bonus, has generation of music notes frequencies if you need it

author: DELTA67
author_rank: Regular Contributor
timestamp: November 08, 2024, 01:21:15 pm
content: Thanks gpr for the links.Perhabs we can save the 1/4 length LUT in the flash then with some formula we generate a full LUT in the RAM.

author: pcprogrammer
author_rank: Super Contributor
timestamp: November 08, 2024, 02:57:35 pm
content: You can also use DMA to make it work independent of the processor.Take a peak at my project here:https://github.com/pecostm32/STM32F303_Sine_Square_Generator/tree/mainIt uses the integrated DAC of the F303, but DMA can also be used to generate PWM output. Have the target address be the compare register of a timer and read the samples from FLASH. Pulse the DMA based on another timer to set the output frequency.The output frequency is determined by the number of samples in the full sine table and how fast you output them to the PWM timer.The maximum frequency is basically half the carrier frequency, but it won't be a nice sine anymore unless the low pass filter has a much lower cutoff point. The amplitude will be very low then.When using DMA the quarter LUT is not very handy, but when doing it in software there is no need to unpack it to a full one. Just use the given phase to determine the quadrant and adjust the values based on it. See attached files for how this can be done.The low pass filter calculations can be simple as long as a first order is good enough for you. Just take a resistor of say 15K and calculate the capacitor to go with it to set the filter to half or a third of the carrier frequency.

author: DELTA67
author_rank: Regular Contributor
timestamp: November 08, 2024, 05:48:42 pm
content: Quote from: pcprogrammer on November 08, 2024, 02:57:35 pmHere the speed of samples is constant and equals the frequency of the PWM carrier. How can I change the Nr of samples for each frequency?Quote from: pcprogrammer on November 08, 2024, 02:57:35 pmYes I'm planing to use it later. In this pahe I'm just trying to grasp the principles.Is there any idea how to compute R of the LPF in function of the GPIO pin fan out?Why the code taken from the arduino don't work?

author: MasterT
author_rank: Frequent Contributor
timestamp: November 08, 2024, 07:45:03 pm
content: Not sure how it could potentially work: TIM1->CCR = SINE_LUT[i++];Access to CCR is like that: TIM2->CCR4 = reg_cc;Q: 1- How to calculate the best values for the LPF elements?I've read some where that the value of R depends on the GPIO pin output driving capability. How?A: RC filter in simplest form: F = 1/ 2*pi()*R*CGPIO may affect total R in formula only if external R is low, anyway there are max current limits for GPIO that prohibits of using low values of R,Q: 2- What's the MAXimum sine frequency I can get from this configuration? How to compute it?A: Nyquist says F/2, so higher carrier freq. drives sine up. In theory. Practically, upper sine freq. limited by LPF performance, to get sharp cut-off at F/2 order of the LPF filter has to be enormously high. So cost wise F/10, or even F/100 for first order RC filter.

author: pcprogrammer
author_rank: Super Contributor
timestamp: November 08, 2024, 07:51:45 pm
content: In what I suggested with the DMA and two timers the outputting of the samples is not in sync with the period of the carrier. This way a single sample can be outputted multiple times or completely skipped because the compare timer is updated more then once during the carrier period. This is similar to using a phase accumulator setup.The using of a phase accumulator should work. Here is a project I use a similar setup in. It is not finished and more of a play thing. The sine based drum sounds do work reasonably well in comparison to the original analog drum machine it is based on. The noise part is the problem though, but not relevant for your project.There are a lot of comments in the source, but part of it is in Dutch, so might loose meaning in translation.Don't get hung up on the GPIO fan out. For a simple RC based LPF the output impedance won't play a big role as long as the resistor is not to small and the capacitor not to big. For my project I use a 3K9 resistor with a 4n7 capacitor. So around 8680Hz low pass filter. With a carrier way up in the 70kHz range it works very well for the low frequencies in drum sounds. Not so good for the snares and cymbals though.The one thing to point out, but it might be elsewhere in your code, the phase accumulator is not initialized to zero. Should only have impact on the initial phase thoughThe calculation of tword is not correct because of the low headroom in the variable. You are multiplying a 1000 with 16777216 and this is larger than 32 bits. Instead of using 32 bit integers you need to cast the calculation to 64 bits. (long long)

author: DELTA67
author_rank: Regular Contributor
timestamp: November 09, 2024, 06:37:47 am
content: Quote from: MasterT on November 08, 2024, 07:45:03 pmIn my "hal.h" the struct timer is like this:Code:typedef struct {................................volatile uint32_t RCR;// 0x30 repetition counter registervolatile uint32_t CCR;// 0x34 capture/compare registers 1 TO 4..................................} TIMER;

author: DELTA67
author_rank: Regular Contributor
timestamp: November 09, 2024, 06:43:48 am
content: Quote from: pcprogrammer on November 08, 2024, 07:51:45 pmYou mean that I've to declare these vars as long long?

author: pcprogrammer
author_rank: Super Contributor
timestamp: November 09, 2024, 06:59:11 am
content: Quote from: DELTA67 on November 09, 2024, 06:43:48 amEither that or type cast in the calculation.Code:tword = (1<<24)*(long long)freq)/refclk;//or simplifiedtword = (long long)(freq << 24) / refclk;I might be wrong but I think that casting just the freq part is enough. The divide will be based on 64 bits / 32 bits, which is ok and the result is automatically casted to 32 bits due to tword being 32 bits.If you look back at the Arduino code, you would see that dfreq and refclk are declared as double. This takes care of the range there, but the calculation will be a bit slow. Using 64 bits integer on the F103 is faster with a bit of rounding error. I also noticed that they multiply the double with 4294967296 instead of 16777216. So check if your calculation provides correct values.

author: DELTA67
author_rank: Regular Contributor
timestamp: November 09, 2024, 09:58:59 am
content: Quote from: pcprogrammer on November 09, 2024, 06:59:11 amOk, thanks I'll try this ASAP.Quote from: pcprogrammer on November 08, 2024, 07:51:45 pmWOW!! Very interresting project. I love drum sounds.Can you share the circuit please?Once upon a time I tried to build a sound effects machine using an ATMEGA328.https://www.eevblog.com/forum/microcontrollers/avr-pcm-player-class-d-amp/When using a simple audio amp (LM386) it works but when plugged in the aux input of a car radio I've got a lot of noise and the volume was very low!

author: DELTA67
author_rank: Regular Contributor
timestamp: November 09, 2024, 10:14:13 am
content: When using:tword = (1<<24)*(long long)freq)/refclk;Her's the compilation errors:error: expected ';' before ')' tokenerror: expected statement before ')' tokenerror: expected expression before '/' tokenWhen using:tword = (long long)(freq << 24) / refclk;Her's the compilation errors:undefined reference to `__exidx_start'undefined reference to `__exidx_end'undefined reference to `abort'Note: I'm using the old bare metal toolchain YAGARTO.

author: DavidAlfa
author_rank: Super Contributor
timestamp: November 09, 2024, 10:39:10 am
content: Isn't obvious? You're missing a bracket at the very beginning,freqhas a closing bracket, but isn't matching any opening:tword = (1<<24)*(long long)freq)/refclk;See the difference?tword = ((1<<24)*(long long)freq)/refclk;Just use the simplified form:tword = (long long)freq<<24 / refclk;, it's exactly the same thing.

author: DELTA67
author_rank: Regular Contributor
timestamp: November 09, 2024, 10:46:14 am
content: Quote from: DavidAlfa on November 09, 2024, 10:39:10 amOk, now the same compilation errors:undefined reference to `__exidx_start'undefined reference to `__exidx_end'undefined reference to `abort'

author: DavidAlfa
author_rank: Super Contributor
timestamp: November 09, 2024, 10:50:36 am
content: What same compilation errors? Wasn't your code working before?Delete everything off your code (Make a backup of course), leave only an empty main(){ ; }If that comopiles, start adding our code back. Maybe you messed up, lacking some key or semicolon.Sometimes a wrong symbol can really mess compilation in a such way that it won't simply tell you wrote something wrong at line 656:33, but throw all kind of strange errors.At this point, better you post the entire code inhttps://pastecode.io/

author: DELTA67
author_rank: Regular Contributor
timestamp: November 09, 2024, 02:02:45 pm
content: Quote from: DavidAlfa on November 09, 2024, 10:50:36 amCompi errors when I add this line:tword = ((1<<24)*(long long)freq)/refclk;When it was :tword = (1<<24)*freq/refclk;No errors.Perhaps it's due to some math library missing.

author: DELTA67
author_rank: Regular Contributor
timestamp: November 09, 2024, 04:21:58 pm
content: We must cast all the result:tword = (long long)((1<<24)*freq/refclk);Now it compiles. But always no sine wave.

author: DELTA67
author_rank: Regular Contributor
timestamp: November 09, 2024, 05:04:25 pm
content: Quote from: pcprogrammer on November 09, 2024, 06:59:11 amusing:double TUN_K = (4294967296.0/refclk); // tuning coefficientand:tword = TUN_K*freq;Works without casting.

author: pcprogrammer
author_rank: Super Contributor
timestamp: November 09, 2024, 06:19:49 pm
content: The original design was published in a magazine called ELO. I don't have the full articles, but do have a bunch of pages with the old schematics and PCB designs. I redrew the schematics in EasyEDA to preserve them, but not the PCB's.I have modified and build the kit as part of a school project long ago and still have it, and it still works.Have to make two posts due to the large size of the scans.

author: pcprogrammer
author_rank: Super Contributor
timestamp: November 09, 2024, 06:21:23 pm
content: More pages of the original article.

author: pcprogrammer
author_rank: Super Contributor
timestamp: November 09, 2024, 06:39:12 pm
content: And here are some images of the machine I made.

author: pcprogrammer
author_rank: Super Contributor
timestamp: November 09, 2024, 07:00:19 pm
content: Quote from: DELTA67 on November 09, 2024, 10:46:14 amThis has to do with exception handling and it looks like that when using 64 bit variables the compiler references these "objects". How to resolve this I don't know, but maybe when you search on the net a solution can be found.I have used 64 bit calculations before, but it might not have been on Cortex-M based devices. Can't find the project right now. Maybe it is due to the toolchain you are using. I'm on Linux and use the GNU compilers (arm-none-eabi), but not on the machine I'm typing this on.This is my leisure, watch TV and browse the web machine.

author: DavidAlfa
author_rank: Super Contributor
timestamp: November 10, 2024, 12:32:13 am
content: Casting is fine. I'd simply #include <stdint.h> and use (uint64_t)(...) instead.

author: DELTA67
author_rank: Regular Contributor
timestamp: November 10, 2024, 06:52:25 am
content: @pcprogrammerI need the circuit of the STM32 based drum please.Other questins:- Is there an advantage using 2 timers: 1 for the PWM carrier and the other for the modulation?- Have you any idea how to use the uC as a class D amplifier given that the PWM carrier is already generated?

author: pcprogrammer
author_rank: Super Contributor
timestamp: November 10, 2024, 07:33:21 am
content: Quote from: DELTA67 on November 10, 2024, 06:52:25 amAttached below are the schematic and the EasyEDA project. Also pictures of the setup I made. Nothing fancy, just a bluepill, a display and some simple modules. The potentiometers are connected between gnd and 3.3V and have a small capacitor on the wiper.Quote from: DELTA67 on November 10, 2024, 06:52:25 amOnly when using DMA.Quote from: DELTA67 on November 10, 2024, 06:52:25 amI'm not up to speed with class D amplifiers, but if it is about having two signals to driving the output transistors timer 1 has provisions for things like dead time and outputting inverted signals.

author: DELTA67
author_rank: Regular Contributor
timestamp: November 10, 2024, 08:39:45 am
content: Quote from: pcprogrammer on November 10, 2024, 07:33:21 amThanks.How to solve the problem of noise and low volume when I connect the output of the PWM DAC to a power amplifier?

author: DavidAlfa
author_rank: Super Contributor
timestamp: November 10, 2024, 11:50:16 am
content: What's the effective frequency range you're seeking?You can get a cheap PCM5102A I2S DAC (Up to 32-bit, 384KHz), then feed it with DMA.Another think is, it's a bad idea to compute the value in the ISR before feeding the value to the register, might cause buffer underrun specially at a such high ISR frequency.Do otherwise, have a precomputed value, load it instantly, then you have plenty of time to compute and store the next value.Code:void TIM1_UPhandler(void){static uint16_t next_pwm_value;TIM1->CCR = next_pwm_value;TIM1->SR = 0;// clear interrupt flag bitsphaccu += tword;// increment the phase accuphase = phaccu >> 24; // use upper 8 bits as indexnext_pwm_value = SINE_LUT;}

author: newbrain
author_rank: Super Contributor
timestamp: November 10, 2024, 01:10:59 pm
content: Quote from: pcprogrammer on November 09, 2024, 06:59:11 am"Just casting the freq part" is, in fact, enough, but the simplified code is not OK, as it's not doing that:freqis casted to 64 bits only after being shifted left 24 positions, this means that all bits 0-23 will be 0, as will all bits 32-63.That is to say, the cast has accomplished nothing (a good compiler will simplyignore it), and iffreqis less than (1u << 24) = 16777216, the result will be, of course, 0.The non-simplified code, as we know, is short of a bracket - or, alternatively has an extra bracket afterfreq- but correct as principle.Quote from: DavidAlfa on November 09, 2024, 10:39:10 amNo it's not.Here we have a different problem:The left shift operator has a lower priority than the division one, sofreqwill be shifted left by theresult of of 24/refclock.For any reasonable values ofrefclock(i.e. geater than 24[twordwill be set to value offreq.The cast is once again immaterial.You need the brackets. The following code will cast freq to 64 bits, without losing bits,thenleft shift it 24 positions, which is guaranteed not to overflow,thendivide it by refclk.The result will then be cast back to 32 bits.Code:tword = ((long long)freq<<24) / refclk;I usually refer to the standard, but understanding operator priorities from it is cumbersome, as they are embedded in the way the grammar is defined.This tableat cppreference.com is what I use - and yes, I admit I sometimes need it.

author: pcprogrammer
author_rank: Super Contributor
timestamp: November 10, 2024, 03:08:17 pm
content: Quote from: DavidAlfa on November 10, 2024, 11:50:16 amThe STM32F103 is not the best fit for I2S. It won't do 384kSa/s and there is no separate pll to get a proper sampling rate like 44.1kSa/s. You would need an external clock synthesizer to do this right.I have a proto board with the bluepill, a PCM1808 module and a PCM5102A module on it and yes it works, but it is better to use a MCU with proper I2S implementation if you want some decent sample rate.

author: DavidAlfa
author_rank: Super Contributor
timestamp: November 10, 2024, 04:15:49 pm
content: Quote from: newbrain on November 10, 2024, 01:10:59 pmRight!

author: DELTA67
author_rank: Regular Contributor
timestamp: November 12, 2024, 12:54:02 pm
content: Now I'm using TIM1 to generate the PWM carrier @ 70KHz (for 10bit resolution) and TIM2 to trigger a DMA transfer from the LUT[] to TIM1->CCR1.Many questions:1- to synthetize frequencies from 0 to 35KHz, what's the best frequency for TIM2 (sampling freq)? can I use a single value for the full range or it's better to devide the range to multi parts and adjust the TIM2 frequency? How to calculate the TIM2 freq in both cases?2- How to generate the LUT for 1Hz resolution? what's the best compromise between LUT size and frequency range?3- How to adjust the amplitude of the generated signal?I'm a total newebie in this domaine please forgive my ignorance.

author: pcprogrammer
author_rank: Super Contributor
timestamp: November 12, 2024, 03:32:17 pm
content: Quote from: DELTA67 on November 12, 2024, 12:54:02 pmOk with this working you can set the output frequency by changing the rate of TIM2.Quote from: DELTA67 on November 12, 2024, 12:54:02 pmThe way this works is that every time TIM2 counts out the DMA will transfer another sample to the "DAC". The length of the lookup table determines what the frequency will then be. Divide TIM2 rate by the number of samples and you have your output frequency. The PWM generator TIM1 will take care of either up or down sampling so to speak.Quote from: DELTA67 on November 12, 2024, 12:54:02 pmThis depends on your timer resolution. Just think about it in relation to the answer to question 1.Quote from: DELTA67 on November 12, 2024, 12:54:02 pmThis requires recalculating the LUT values. The DMA can't do it for you, so if you want to set the amplitude you just lower or raise the value of all the samples. An external voltage or digitally controlled amplifier will give better results though. Search for digitally controlled potentiometer.

author: DELTA67
author_rank: Regular Contributor
timestamp: November 13, 2024, 07:54:35 am
content: Quote from: pcprogrammer on November 12, 2024, 03:32:17 pmWhat's the formula?Quote from: pcprogrammer on November 12, 2024, 03:32:17 pmThis initial LUT values are for MAX amplitude?

author: pcprogrammer
author_rank: Super Contributor
timestamp: November 13, 2024, 08:09:19 am
content: The documentation about the timers answers your first question. You have to consider the setting of the system clock, timer prescaler and the timer counter to get the required timer rate. Look at my projects on how the timers are set for different frequencies.You will have to write code to do the calculations for you to set the timer registers dynamically, based on some input.Creating an initial look up table with maximum output values is the easiest way to do it. Keep in mind that when lowering the amplitude, by lowering the values in the LUT, the distortion will go up.There is also a pitfall with the asynchronous setup of the two timers, that can give problems at certain output frequencies. This has to do with when the CCR register is updated by the DMA in relation to where the PWM timer is in it's cycle.Play with the concept to find these problems.