Calculating SPWM values on the fly. Is it possible with my MCU? - Page 1
Forum section: electronics / general

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 03, 2022, 04:19:17 pm
content: Hi,I'm working on a project to generate a 3phase sinusoidal pwm.F_fundemnetal = 0.1 - 100 HzF_sw = 1-100 kHz.I would like to calculate the PWM values on the fly if possible instead of a lookup table. I am generating an ISR every time TIM1 overflows and am doing my calculations in there.I'm using STM32F302R8.IT works fine at 10 kHz, however I have been struggling to get it to work at 100 kHz with more than one phase calculation. I think there is not enough time for the calculations of the following code.As far as I'm aware, the FPU should do each of these calculations in 14 clock cycles (running at 72 MHz) so that should be plenty of time to do it even at 100 kHz.In keil I have enabled the floating point hardware to use single precision in the options for target 1 menu. Then when I am initializing the clocks, I should be enabling the FPU with the following command.Code:SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */At this point I'm guessing the FPU is doing the calculations but I am not sure on how to verify this.Am I missing something? Is the uC/FPU not fast enough for these calculations? Can I somehow optimize the code to make it execute more quickly?Code:// interrupt occurs at switching frequencyvoid TIM1_UP_TIM16_IRQHandler(void){TIM1->SR = ~TIM_SR_UIF; // Clear interruptif (PWM_index >= nsamples){PWM_index = 1;}PWM_U = MaxPWM_value*sinf((6.28315/nsamples)*PWM_index);PWM_V = MaxPWM_value*sinf((6.28315/nsamples)*PWM_index);PWM_W = MaxPWM_value*sinf((6.28315/nsamples)*PWM_index);if (PWM_U <= 0){PWM_U = 0;}if (PWM_V <= 0){PWM_V = 0;}if (PWM_W <= 0){PWM_W = 0;}////Duty CyclesTIM1->CCR1 = PWM_U;TIM1->CCR2 = PWM_V;TIM1->CCR3 = PWM_W;PWM_index++;}

author: eutectique
author_rank: Frequent Contributor
timestamp: October 03, 2022, 06:27:19 pm
content: Quote from: Glenn0010 on October 03, 2022, 04:19:17 pmJust some obvious points:1. You are doing 3 calculations instead of 1 for no apparent reason.2.6.28315isdouble, while6.28315fisfloat. In the worst case, compiler will generate a call to double division, then a call to double multiplication, then a call to conversion from double to float. Disassemble the function and examine the code.3. (6.28315f / nsamples) should be a constant, as I would expect. In fact, is it?Edited clarification: call to double division is a function call, not an FPU instruction. The same applied to multiplication and d2f conversion.

author: abyrvalg
author_rank: Frequent Contributor
timestamp: October 03, 2022, 06:32:37 pm
content: Cortex-M4 FPU doesn’t support sinf(), it is done in software (and in much more than 14 cycles for sure). Why don’t you like LUT approach? You can build a pretty small table, like several KB, and use linear approximation, sine is pretty smooth.Edit: also, are you sure about stripping the negative half-wave? What are you driving with that? Wouldn’t it be better to shift everything up? Or even use some smarter thing like an SVPWM?

author: eutectique
author_rank: Frequent Contributor
timestamp: October 03, 2022, 06:58:33 pm
content: You can see how many cycles your code takes. Fire one interrupt and examine the variable.Beware of compiler-generated prologue and epilogue though, they will fall out of the cycle measurement. It is possible that FP context is saved/restored, among other registers. Again, see the assembly.Code:static volatile int32_t cycles = 0;void TIM1_UP_TIM16_IRQHandler(void){cycles -= DWT->CYCCNT;... // your code herecycles += DWT->CYCCNT;}Edit: fixed copy-paste error

author: lucazader
author_rank: Regular Contributor
timestamp: October 03, 2022, 10:24:38 pm
content: Why not just pre-compute a lookup table and then reference this. the F302R8 has both plenty of flash/ram to be able to do this effectively and efficiently.This way you can also precompute the table with the correct values for your max timer value.Saves a whole bunch of execution time.

author: ogden
author_rank: Super Contributor
timestamp: October 03, 2022, 10:46:57 pm
content: Quote from: Glenn0010 on October 03, 2022, 04:19:17 pmWhy? Please try to provide all the reasoning behind your requirement. I am asking because most of experienced engineers would pick lookup table indeed - because it is most resource/cost-efficient solution.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: October 03, 2022, 10:54:34 pm
content: As said above, be very mindful of what exactly you're telling the compiler you want, and always double-check your intentions against the compiled result (assembly listing).For example, the compiler might infer that those three calls to sinf() have identical arguments, so can be combined. But this only happens on -O1 or higher (or, which level of O exactly enables that reduction, I don't recall). And it isprohibited from happeningif the arguments arevolatile. In the latter case, consider making a local copy of the variable, then writing back when you're done. Do whatever atomic read/write or other buffering you need, separately. But also, if you're repeating a calculation... just... don't do that in the first place?You can also break down the calculation incrementally. Keep a sin and cos accumulator handy, and multiply them both as I + jQ of a complex number, times a small angle sin(dtheta) + i cos(dtheta). Renormalize it once in a while (divide by sqrt(I^2+Q^2) or something like that; there's probably a more streamlined way to do it than the full calculation), to account for rounding error, and, there you go. One register always has sin(t) and the other cos(t). More info here:https://en.wikipedia.org/wiki/CORDICThis is especially feasible on integer machines with hardware multiply, where complex multiplication is straightforward.Tim

author: PCB.Wiz
author_rank: Super Contributor
timestamp: October 04, 2022, 05:07:27 am
content: Quote from: Glenn0010 on October 03, 2022, 04:19:17 pmSee above posts for the problems. You can use a pin to time the actual code time.Quote from: Glenn0010 on October 03, 2022, 04:19:17 pmStep back a bit, and ask why do you need to re-calc sineevery PWM period?If you want variable voltage and variable frequency, yes it can help to create a new sine-fit table in RAM for each modulation depth setting, otherwise you need many tables, butyou do not need to do that every 10us.The motor mechanical inertia and control loops are usually in the many-millisecond regions, and fine adjustments can be managed with simple linear interpolation.

author: langwadt
author_rank: Super Contributor
timestamp: October 04, 2022, 07:28:48 am
content: Quote from: abyrvalg on October 03, 2022, 06:32:37 pmEven the DSP lib uses a table,https://github.com/ARM-software/CMSIS/tree/master/CMSIS/DSP_Lib/Source/FastMathFunctions

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 04, 2022, 07:51:32 am
content: Quote from: eutectique on October 03, 2022, 06:27:19 pmHiregards to point 1- this was just to test the speed of the calculation, the phase shift would be added later once it was would be working.2 - I will give this a try and see any perforamcne benfits I get.3 - yes it would be depending on what switching frequency I chose. I can simplify it further.Thanks for the reply and help.

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 04, 2022, 07:54:26 am
content: Quote from: abyrvalg on October 03, 2022, 06:32:37 pmI can use the LUT but I was wanting to see if tis method is possible to avoid linear interpolation. If it doesn't work I will go back to LUT. Another reason is I want to have a wide range of switching frequency and fundamental so that leads to a lot of interpolation.So you are right about the negative half wave, this was an error I had carried on from before. However, it works nicely to visualize where one fundamental period has ended on the scope to measure timings. In the proper version I will need to shit it up like you suggested.

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 04, 2022, 07:56:13 am
content: Quote from: eutectique on October 03, 2022, 06:58:33 pmThanks so much for this. I always struggled with figuring out how many cycles some code would take and how to figure out how much time has passed for a specific piece of code. This will be very helpfull

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 04, 2022, 07:57:54 am
content: Quote from: lucazader on October 03, 2022, 10:24:38 pmThe thinking for doing it this way is that I want to operate from anywhere between 2 and 100 kHz, therefore, this would require a lot of interpolation at the higher switching frequency.

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 04, 2022, 07:58:44 am
content: Quote from: ogden on October 03, 2022, 10:46:57 pmAs I replied to some other users, The thinking for doing it this way is that I want to operate from anywhere between 2 and 100 kHz, therefore, this would require a lot of interpolation at the higher switching frequency. Unless I am missing something

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 04, 2022, 08:02:04 am
content: Quote from: T3sl4co1l on October 03, 2022, 10:54:34 pmThanks Tim, I will try a few quicker solutions to this then, if those fail I will try your method. Thanks for the sugggestion

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 04, 2022, 08:04:31 am
content: Quote from: PCB.Wiz on October 04, 2022, 05:07:27 amThe only issue I was seeing is that if I want to operate at 100 Hz at 2kHz, that's 20 different values for the PWM if I want to operate at 100 Hz at 100 kHz that's 1000 values for the PWM, and was thinking that is a lot of interpolation that needs to occur and thought it would distort my output.

author: abyrvalg
author_rank: Frequent Contributor
timestamp: October 04, 2022, 09:15:19 am
content: There should be no problem having a big fine-grained table for the highest frequency (that’s <2KB for a 1000-point table of 16-bit CCRx values) and just incrementing the PWM_index in bigger steps at lower frequencies. And, as langwadt pointed already, the math library sinf() is using a LUT+approximation already (with some unknown step, perhaps, less than 1000 points), so you can do it even better - choose the number of points matching your setup, dropping the approximation completely, eliminate all intermediate operations and use direct CCRx values (already clipped/shifted etc).

author: Siwastaja
author_rank: Super Contributor
timestamp: October 04, 2022, 09:24:11 am
content: Just use large enough look-up table, simply pick the nearest smaller index. No linear interpolation needed.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: October 04, 2022, 09:55:36 am
content: Quote from: Glenn0010 on October 04, 2022, 08:04:31 amIf you're worried about distortion, well the great thing is you can run through all the numbers ahead of time, in a spreadsheet or whatever, and not wonder at all. You can prove it. Not to mention you have much greater analytical tools at hand than you may in the lab: run an FFT on the series? No problem! Error from ideal (sin)? Easily done. Simulate inductor ripple (say with the aim of compensating for its centerline value)? Can do! Anything else, done practically, means you have to build the whole damn circuit, and program the system, AND fix all the bugs that you put in that also made it blow up multiple times, oh and replace the components that died in the process -- and why make so much work for yourself?!And there are other ways to interpolate:First off, the naive interpolation requires a set of (x, y) points, performs a division (slow!) to find the distance between points, then multiplies by the offset to get the difference. And some arithmetic to patch up the differences.Well, if the table is fixed spacing, the division can be reduced to a constant. That speeds things up greatly. (Note that division by a constant is equivalent to multiplication by its reciprocal, give or take some shifting to account for the decimal point.)If you want to minimize the size of the table (say you're short on memory -- note with 32/64kB Flash to spare, you'll have to be quite busy with other functions to be sweating about merely a couple kB!), maybe you store precalculated divisions for arbitrary sized steps, doubling the amount of data to store per point but greatly reducing computational overhead and number of points required (i.e., more points can be clustered closer in regions of rapid change).Maybe you choose something more nuanced than a linear interpolation. We can rank types of interpolation by the order of the function used to smooth it.Zeroeth order is simply taking the nearest point. Pretty gross, right, well, think of it as the rounding error of a conversion table. It's a resolution thing, you're always giving up something. You could always pick more points -- potentially as many as you have possible inputs, which isn't even bad if you had say an 8-bit phase accumulator here.First order is linear: we take the extra bits (maybe our lookup table is 8-bit (256 elements) but our phase accumulator is 16-bit so we take the low byte (remainder)), and use that to interpolate between adjacent elements. Nice, but if the curve is supposed to be, well, curved in this spot, well, we're missing a lot; the line segment has to cross through that arc segment in order to best-fit to it. And it can only do that in two points (or very rarely, three or more, only where the curve doubles back on itself).Second order is quadratic: we record a value for that curvature, and do a quadratic spline interpolation.Third order is cubic: we record not just the curvature, but the curvature's curvature, as it were.And so on. We can generalize this to any order polynomial, and we merely need to store the start/end points of each segment, and the curvature rates (coefficients) in each segment.In fact, if we have a smooth enough function, we might not need divide it into any points at all. We can trivially compute the squaring function f(x) = x^2 by just, well, computing the square. A quadratic spline will fit any range of this curve perfectly; they're equivalent for this purpose, and the same is true of other locally-quadratic-like curves.Maybe we need a couple points yet, or a couple higher-order terms, to get a better fit. A circle for example, has the form f(x) = sqrt(r^2 - x^2). sqrt() is a heavy-weight function, but we know splines fit that shape very well (indeed, perfectly, for parametric quadratic splines, i.e. {x(t), y(t)}; the explicit function y = f(x) has a harder time, however).As it happens, sin(x) has the form: x - x^3 / 3! + x^5 / 5! + ..., and since the factorial denominator increases quite quickly (compared to small x), we know this function converges quite quickly. If we're only interested in the range from say -pi to pi, we might only need a handful of terms to solve it -- even for bit-exact accuracy!Which in fact, is exactly what they do, in most floating point libraries -- as far as I know. Floats (i.e., ~24 bits accuracy) might need 5 terms or so.But if you're only feeding say a 16-bit DAC, do you really need the time taken to compute those higher terms?Or, for a 100kHz PWM counter clocked at even 100MHz, that's still only 10 bits of timing accuracy (granted, some timers support vernier or extended-precision timing features), so it seems unlikely you need anywhere close to evenhalfthe accuracy afforded by full floating point precision!So, there is advantage to be gained by truncating the calculation earlier, taking matters into your own hands.Also, as it happens, when the sin(x) Taylor series is truncated, the coefficients can be tweaked a bit to achieve a best-fit condition (the Taylor theorem merely equates a function to a polynomial series plus an error term; only if that error term decreases sufficiently fast with the number of terms, will the truncated series serve as an effective approximation, and at that, with error given by the value of that error term!). When you're computing values from approxmiations like this, the best idea is to just plug everything into a spreadsheet, or MATLAB/Octave, or Python or whatever programming/mathing environment you prefer, and just let a solver (root finder algorithm) search for optimal values.Anyway, the value of polynomials, for embedded platforms, is the cheapness of multiplication and addition, at least on most. Even on AVR, I can calculate the 5th order correction for, say, a thermistor transfer curve, with guaranteed worst-case error comparable to the ADC (12 bits ~ 0.05°C), needless to say, greatly outperforming the spec of the thermistor itself (~1% say). And all this in just 300 cycles or so -- maybe a bit more than you'd want to spend in an ISR (about 15µs on AVR), but also something that can be done, evidently, many thousands of times a second! And obviously that can be greatly improved on a 32-bit platform like STM32, by both the higher clock frequency and the wider and more efficient operations.For reference, earlier this year I did a digital control on AVR, where the ISR computes a new timer value at up to 50kHz. ADC acquisitions are interleaved with timer interrupts, bringing in process values (power supply stuff, so, voltages and currents, also interleaved with housekeeping stuff like temperature measurements). The control consists of basic DSP operations: low-pass filters and a PID loop. It's pretty tight on CPU cycles at max frequency (~80% used), but as you can see, even for such a humble platform, real practical DSP work can be done! Granted, this is an optimized case (hand ASM), but that degree of optimization will not be necessary on an STM32, at least before going to much higher sample rates.The low-hanging fruit is to just reduce work required at all. Avoid floating point (fixed point is absolutely worth understanding), simplify whatever math you can, reduce data flows, and finally only then, look into numerical tricks like these, or the last resort, hand-written ASM.Tim

author: DavidAlfa
author_rank: Super Contributor
timestamp: October 04, 2022, 10:00:42 am
content: Unless you're tight on space, why overcomplicate things up?A lookup table should be no issue for a modern stm32 mcu with plenty of storage.I doubt it needs steps finer than 1°, but in any case you only need a lookup table for 90°, the other quadrants are symmetric/complementary.Also remember the flash needs some access cycles, not sure how the cache works for data constants, you might consider using the CCM ram for storing the table if not getting fast enough.Another solution would be DMA, so the PWM self-updates without CPU intervention, multiple PWM can be updated at the same time.You only need to feed 2 buffers (Actually one, but updated in halves) so the CPU can fill the next buffer while the current one is being transferred, removing the crazy 100KHz ISR overhead and reducing it to just 195Hz using 2x512byte buffers.I played with this some time ago, feeding two pwm channels at 96KHz as a cheap stereo audio DAC to test this feature.You might want to check it out:https://github.com/deividAlfa/STM32F411-Black-pill-USB-wav-player

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 04, 2022, 11:27:02 am
content: Hi All!Thanks for your great feedback. So I did some work this morning and got some decent results.I am able to execute all 3 PWM calculations in 9.12 us. To measure this at the start of the ISR I turn on a pin then turn it off at the end and measure it with a scope. Though this doesn't leave much margin at 100 kHz. I have used the envelope feature on a scope and this seems to be the absolute maximum.For reference here is the new code. I tried to do as many calculations as possible outside of the ISR.Code:void TIM1_UP_TIM16_IRQHandler(void){TIM1->SR = ~TIM_SR_UIF; // Clear interruptGPIOB->BSRR = (1<<2); // turn onif (PWM_index > nsamples){PWM_index = 1;}if (PWM_index_V > nsamples){PWM_index_V = 0;}if (PWM_index_W > nsamples){PWM_index_W = 0;}PWM_index_V = PWM_index_V+PWM_index;PWM_index_W = PWM_index_W+PWM_index;PWM_U = PWM_Value*sinf((PWM_Freq*PWM_index))+PWM_Value;PWM_V = PWM_Value*sinf((PWM_Freq*PWM_index_V))+PWM_Value;PWM_W = PWM_Value*sinf((PWM_Freq*PWM_index_W))+PWM_Value;TIM1->CCR1 = PWM_U;TIM1->CCR2 = PWM_V;TIM1->CCR3 = PWM_W;PWM_index++;GPIOB->BSRR = (1<<(2+16)); // turn off}However, as many of you suggested it looks like this is doing it in a difficult way for no reason.Therefore like many suggested I will use a look up table of 360 Values and go from there

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 04, 2022, 11:46:48 am
content: I would also like to add that as I was testing, most of you were correct about 6.28315 being processed as a double. As soon as I put 6.28315f the processing time for one phase calculation went from 6.4 us to 1.88 us.here is the disassembler view first for without the f and with the f. Is there a resource which states what each instruction does?

author: eutectique
author_rank: Frequent Contributor
timestamp: October 04, 2022, 12:03:08 pm
content: Quote from: Glenn0010 on October 04, 2022, 11:46:48 amYes, ARM web site has all the TRMs (Technical Reference Manuals), Arm Cortex-M4 Processor Technical Reference Manual including.Table of processor instructionsFPU instruction set tableYou can also useobjdumpto disassemble the object file and intermix the listing with the source code:Code:arm-none-eabi-objdump -S your_object_file.o

author: eutectique
author_rank: Frequent Contributor
timestamp: October 04, 2022, 12:07:54 pm
content: Quote from: Glenn0010 on October 04, 2022, 11:27:02 amAnd then a table of 90 values, for 1° resolution, as was already suggested.

author: Siwastaja
author_rank: Super Contributor
timestamp: October 04, 2022, 01:52:09 pm
content: I'm assuming you are driving a motor. If so, consider this:Brushed motors, and simple BLDC motors are driven with full DC bus voltage of either polarity, in typically 6 steps per electrical rotation, i.e., every 60 degrees. And they are mostly just fine, a some torque ripple can be observed.Now if you choose to spend just 256 bytes of RAM to implement a 8-bit resolution sine table with 256 elements, no interpolation, no "only store one quarter of cycle" tricks - you have 12600% better amplitude resolution, and 4200% better temporal resolution, than those simple brushed or BLDC implementations. It is quite obvious you won't be able to see any further improvement in vibration etc. even if you increase accuracy further. In fact, such budget implementation is already nearly overkill!And the implementation is trivial: make the 256-step table, and use uint8_t to denote rotor angle so that 0 - 255 maps to 0 - 358.59 degrees. You get natural wrap-around of angles while calculating, in CPU registers, without any if-elses. Practical use for this wrap-around is you can go between sine and cosine by just adding/subtracting 64 (equiv. to 90 degrees) from the index. Then just index the table with this uint8_t angle directly. No range-checking necessary. And the performance is just excellent.Beware of some C footguns, however, namely signed integer subtraction overflowing is undefined behavior (unsigned is OK, and defined to work as modulo operation as explained above), and integer promotion rules.

author: Siwastaja
author_rank: Super Contributor
timestamp: October 04, 2022, 01:58:27 pm
content: Quote from: eutectique on October 04, 2022, 12:07:54 pmIf at all possible, I always suggest using angles where full rotation matches the numerical range of the fundamental numerical type. This makes wrap-arounds trivial to handle and offers significant speed benefit, too. You can convert from/to degrees whenever degrees are needed, but this is usually only in UIs. Internally, you rarely actually need such weird unit as 1/360 circle. Only us humans are used to it.If you need to keep count of position (including count of full rotations, plus sub-position within current rotation), it won't get any easier: highest bits denote the number of full counts. E.g. uint64_t can count +/- 2 billion full rotations plus sub-position with 360/(2^32) degree resolution.You can even do things like interpreting the same variable as unsigned or signed, depending on if you want 0..360 deg or -180..+180 deg indication - latter is useful for differences between two angles.

author: langwadt
author_rank: Super Contributor
timestamp: October 04, 2022, 01:58:50 pm
content: Quote from: Siwastaja on October 04, 2022, 01:52:09 pmand if feeling lazy, just use the CMSIS DSP lib,https://github.com/ARM-software/CMSIS/blob/master/CMSIS/DSP_Lib/Source/FastMathFunctions/arm_sin_q15.c

author: Nominal Animal
author_rank: Super Contributor
timestamp: October 04, 2022, 02:47:16 pm
content: If you have hardware floating-point support (STM32F302R8 does):A polynomial sine approximation can give you the precision you need with very little effort – and you can drop the π multiplier also.First, we split the sine wave into four phases. This gives us just the initial rising edge, one fourth of the full period, to approximate. If we choose full period to be 4, then this rising edge is between 0 and 1. Because sine is an odd function (f(-x) = -f(x)), the approximation is valid between -1 and +1, and is always an odd function (a polynomial with only odd powers ofx).Pick one of the below, and rename itapprox_sine_internal(). (I didn't bother to look up the exact optimal ones, I've got them somewhere on backup media; these are plain polynomial least squares fits to first quarter sine scaled to period 4.)Code:// -1.0f <= x <= 1.0f, for a sine wave with period 4.0f// 21.5 bits of precision; absolute error less than 1/2917777 or sostatic inline float approx_sine_internal_5(const float x){const float xx = -x*x;return x*(1.5707963f + xx*(0.6459635f + xx*(0.079689085f + xx*(0.0046731215f + xx*0.00015125547f))));}// 19.1 bits of precision; absolute error less than 1/578525 or sostatic inline float approx_sine_internal_4(const float x){const float xx = -x*x;return x*(1.5707923f + xx*(0.64590585f + xx*(0.07946442f + xx*0.0043524974f)));}// 12.6 bits of precision; absolute error less than 1/6284static inline float approx_sine_internal_3(const float x){const float xx = -x*x;return x*(1.5704362f + xx*(0.6427036f + xx*0.07242646f));}// 6.8 bits of precision; absolute error less than 1/111static inline float approx_sine_internal_2(const float x){const float xx = -x*x;return x*(1.5531573f + xx*0.5621494f);}I'd suggestapprox_sine_internal_4(), which has a computational cost of five multiplications, four additions, and one negation, but the best,approx_sine_internal_5(), only costs one multiplication and one addition more, giving over 21 bits of precision.Note that the above covers only half of the full period, from -1 to +1: approx_sine_internal(-1) == -1, approx_sine_internal(0) == 0, and approx_sine_internal(1) = 1.Next, we need a function that calculates the sine given periodNand phasep, 0≤p≤N,edited:Code:float approx_sine(float period, float phase){float x = (2.0f * period - 4.0f * phase) / period; /* == 2 - 4*phase/period, shift by half period and negate */if (x > 1.0f)x = 2.0f - x;elseif (x < -1.0f)x = -2.0f - x;return approx_sine_internal(x);}The idea here is that we shift the phase by half period (which negates the output, seen in the default case, the lastreturnstatement), so we get a symmetric range around zero. The outermost quadrants do need to be shifted by half a period again (which again negates the output, cancelling the earlier negation).There isn't any easy but precise way to avoid the division by period, unless your period is always the same. Check your MCU documentation on how slow the floating-point division is; it typically is much slower than a (floating-point) multiplication, but not too slow to be used in this instance (as you do need hardware FP support to use this approach).For comparison with library-built-in sinf function, use e.g.Code:float expected_sine(float period, float phase){return sinf(6.283185307179586f * phase / period);}

author: Nominal Animal
author_rank: Super Contributor
timestamp: October 04, 2022, 03:18:47 pm
content: Edited: If it matters,Compiler Explorerproduces the following assembly with trunk GCC with-O2 -mthumb -mtune=cortex-m4, usingapprox_sine_internal_5(), with my guesses on the cycle counts:Code:approx_sine(float, float): ; Cyclesvmov.f32 s15, #4.0e+0 ; 1vadd.f32 s14, s0, s0 ; 1+1vmls.f32 s14, s1, s15 ; 3vdiv.f32 s15, s14, s0 ; 14vmov.f32 s14, #1.0e+0 ; 1vcmpe.f32 s15, s14 ; 1vmrs APSR_nzcv, FPSCR ; 1ble .L8 ; 2-4vmov.f32 s14, #2.0e+0 ; 1+1.L9:vsub.f32 s15, s14, s15 ; 1+1.L4:vnmul.f32 s14, s15, s15 ; 1vldr.32 s12, .L10 ; 2vldr.32 s13, .L10+4 ; 2vldr.32 s0, .L10+8 ; 2vmla.f32 s13, s14, s12 ; 3vldr.32 s12, .L10+12 ; 2vmla.f32 s12, s13, s14 ; 3vldr.32 s13, .L10+16 ; 2vmla.f32 s13, s12, s14 ; 3+1vmla.f32 s0, s13, s14 ; 3+1vmul.f32 s0, s0, s15 ; 1bx lr.L8:vmov.f32 s14, #-1.0e+0 ; 1+1vcmpe.f32 s15, s14 ; 1vmrs APSR_nzcv, FPSCR ; 1bpl .L4 ; 2-4vmov.f32 s14, #-2.0e+0 ; 1b .L9 ; 2-4.L10:.word 958306901.word 999891196.word 1070141402.word 1034105864.word 1059413469with cycle counts based on Cortex-M4 Processor Technical Reference Manual.If I counted right, the (edited version of) with approx_sine() takes on the order of 55-65 cycles perapprox_sine(period,phase)call. On STM32F302R8 at 72 MHz, that would be about 0.76-0.90 µs.Using approx_sine_internal_4() instead of approx_sine_internal_5(), you save five cycles (0.07µs) or so.

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 04, 2022, 04:11:19 pm
content: Hi Thanks for that description, it's helpful and simple.So I want to have a fundamental output frequency ranging from 0.1 to 100 Hz.To get the desired fundamental frequency I can vary the speed at which I go through the LUT. Either skip samples or stay at the same sample for a n amount of switching cycles depending on the switching frequency I selected.

author: langwadt
author_rank: Super Contributor
timestamp: October 04, 2022, 04:31:15 pm
content: Quote from: Glenn0010 on October 04, 2022, 04:11:19 pmor make it a DDS. angle is 32bit int, add a number to that every cycle, how much you add sets the frequency. Use the MSBs of the angle to index the LUT

author: Nominal Animal
author_rank: Super Contributor
timestamp: October 04, 2022, 10:26:30 pm
content: For three phases, I'd do as T3sl4co1l suggested, but using a sine approximation to get 20+ bits of precision:Code:/* sinf(x*PI) for a full period -1 <= x <= 1, 20+ bits of precision. */static inline float approx_sine_internal(float x){const float xx = -x*x;return x*(3.1415918f + xx*(5.167685f + xx*(2.5499268f + xx*(0.59839785f + xx*(0.08060531f + xx*0.0060412507f)))));}float phase; /* 0..1 */float step; /* Desired frequency divided by PWM frequency, 0..1 */float sinA, sinB, sinC; /* Three-phase outputs */void update_phases(void){/* Advance phase by one step, wrapping phase to [0,1) */phase += step;phase -= (int)phase;float y = phase - (phase >= 0.5f); /* -0.5 <= y < 0.5 */float x = y + 0.25f - (y >= 0.25f); /* -0.5 <= x < 0.5, quarter phase further than y */float temp_im = approx_sine_internal(y + y);float temp_re = approx_sine_internal(x + x);float temp_a = -0.5f * temp_im;float temp_b = 0.8660254f * temp_re;sinA = temp_im;sinB = temp_a + temp_b;sinC = temp_a - temp_b;}Each call toupdate_phase()advancesphasebystep, wrapping it to between -1 and +1.The complex phase istemp_re+temp_im*I, which gets rotated 120° and 240° forward (or -240° and -120° backwards) via complex number multiplication (except that the above only calculates the imaginary part), and their imaginary components assigned to sinA, sinB, and sinC.The required complex math is simple:e2πpi= cos(2πp) + i sin(2πp)ei120°= e-i240°= -0.5 + i sqrt(3/4) ≃ -0.5 + i 0.8660254e-i120°= ei240°= -0.5 - i sqrt(3/4) ≃ -0.5 - i 0.8660254(a + i A)(b + i B) = (a b - A B) + i (a B + A b)Looking at the code ARM trunk gcc with-Os -mthumb -mtune=cortex-m4generates usingCompiler explorer, it looks like each call toupdate_phase()takes about 120 cycles, or about 1.7 µs when running at 72 MHz. Calling it 100,000 times a second would consume 17% (plus interrupt overhead); say about 20%, of available CPU time.(I did not optimize the six coefficients, they're just the nonlinear least squares fit to the first sine half-wave. I have optimized such coefficient before, using a directed walk andnextafterf(), to minimize the absolute error within two full periods (-1 to +1) in the parameter space near these coefficients.)

author: PCB.Wiz
author_rank: Super Contributor
timestamp: October 05, 2022, 12:41:46 am
content: Quote from: Glenn0010 on October 04, 2022, 04:11:19 pmYes, exactly. That is what DDS systems do. The adder value varies, and sweeps the pointer through the LUT at varying rates.At very low frequencies, you do read the same sample multiple times and at higher frequencies (where the table is too large), some are skipped.Another benefit of this DDS approach, is the samples picked do not need to repeat every sine cycle, which is what allows DDS to have very fine Hz steps.As an example, DSS at 100.00kHz with a target of 50.50Hz will take 1980 samples 80% of the time & 1981 samples 20% of the time, for a ~1ppm error from 50.50Hz

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 05, 2022, 07:23:08 am
content: Hi All,I'd like to give my thanks out to all of you for all the detailed help you've given me. I spent some time on it yesterday still using the calculation method and got it down to 5.8 us execution time. I used the sint library provided here which I believe is simpler method to calculating the sine value similar to what some of you suggested. Details on this can be found here:https://prog.world/stm32-about-sine.For the moment all I need to do is generate this PWM and the I'm using COMP2 with the DAC as a reference to generate an interrupt when a certain current threshold is exceeded. At this point, I turn off the PWM in the COMP2 interrupt.That is to say that I don't necessarily need to optimize the code but would like to so I can learn a bit more. When I've got more time I will look at all the options you guys suggested and come up with a more optimized code.

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 05, 2022, 07:27:35 am
content: Quote from: PCB.Wiz on October 05, 2022, 12:41:46 amThanks for this, could you point me to any resources explaining DDS as I'm not familiar with it

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 05, 2022, 08:00:06 am
content: Quote from: langwadt on October 04, 2022, 04:31:15 pmHi I'm not familiar with DDS but I think I understand what you're getting at. However if I have a LUT with 256 values, won't I need to use the 8 MSB of the 32 bit angle variable to index the array?CheersGG

author: langwadt
author_rank: Super Contributor
timestamp: October 05, 2022, 08:38:27 am
content: Quote from: Glenn0010 on October 05, 2022, 08:00:06 amyes, if your 256 value LUT is 360 degrees of sine use the 8 MSB of the 32 bit angle variable to index the array

author: Siwastaja
author_rank: Super Contributor
timestamp: October 05, 2022, 10:25:11 am
content: Is this a motor controller? If yes, then everything else in the algorithm is much more interesting and important than how you calculate sin().Are you doing BLDC FOC, or something totally different?

author: Siwastaja
author_rank: Super Contributor
timestamp: October 05, 2022, 10:28:51 am
content: Quote from: Glenn0010 on October 05, 2022, 08:00:06 amThe only reason you would use 32-bit (or even 64-bit) angle variable is so that small incremental changes do not get lost in rounding errors. If you have a 32-bit angle variable, you can still use, say, 256-element table (or any other 2^n length). Just use the 8 most significant bits of the angle variable:static const sin_lut = {generated by whatever};static uint32_t angle;sin = sin_lut[angle>>24];angle += 1; // do this 17 million times and the index will increase by 1. Such tiny steps will not get lost!

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 05, 2022, 12:13:52 pm
content: Quote from: Siwastaja on October 05, 2022, 10:25:11 amI want to create a basic spwm output that will drive a power state that will accelerate from 0 Hz to say 50 Hz to drive an induction machine in open loop.Then I will use this power stage to test for conducted and radiated emissions. And see what effect the power stage characteristics (layout, switching speed, filtering etc) has on these tests.Then I would move on to see what effect the smps that generates the power rails has on these tests etc.So the point of this is just to drive an induction machine in open loop and to keep the control as simple as possible.

author: Nominal Animal
author_rank: Super Contributor
timestamp: October 05, 2022, 12:24:33 pm
content: If it matters any, I think I prefer the look-up method too. I posted the floating-point stuff because it is an useful option, when one simply does not have the Flash/ROM (or RAM) available for a look-up table large enough, and OP does have hardware floating point available.If anyone is interested, I can show exactly why and how the extra bits in in the index actually act as a pulse density modulation register. As mentioned before, it is not just to allow smaller steps, it also adds to the practical precision, via optimal "jitter" in the generated intervals.When using the interrupt to adjust the next period, I would recommend one precalculates one period in advance, and in the interrupt, immediately sets that period, and then precalculates the next period, before returning from the interrupt handler. This does increase the latency (reaction time to speed/velocity changes) by one PWM period, though, and affects how you implement yourPID control loop. If such a delay throws you into a looplook intopredictor-corrector methodson how to deal with it (wrt. integration and derivation). Oscillation in the controlled variable (angular velocity in this case here) is often an indicator of issues in feedback, and the added latency is one way the feedback may be off.With an open loop (no feedback), the latency is basically irrelevant.

author: langwadt
author_rank: Super Contributor
timestamp: October 05, 2022, 08:22:30 pm
content: Quote from: Nominal Animal on October 05, 2022, 12:24:33 pmThe STM32 timers can do that in HW. There's a bit to enable period and compare registers to be buffered until the update event at the end of the pwm period

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 06, 2022, 10:02:03 am
content: Hi folks.I've Implemented the LUT method using DDS (Direct Digital Synthesis) as you folks suggested. Firstly I didn't know about this method, but now that I've read up on it I've found its very clever way to achieve the desired result and get very high resolution.However I've still not got it to work 100% yet. I'm having issues at higher Fsw.I have a 256 position LUT with the max value being 36000. This is the maximum Timer ARR value that I will have at 2 kHz with a 72 MHz timer clock. (72,000,000 / 2,000) = 36,000I then calculate the phase offset for the phase accumulator and a scaling value for the PWM so that I can convert the values in the look up table to match the new ARR values for different switching frequencies.Code://calcualtions for PWM geneationPhase_Offset = (Fout*(pow(2,32))/Fsw);MaxPWM_Value = (int) (72000000 / Fsw);PWM_Scaling = (36000/MaxPWM_Value); //dividing 36000 by MaxPWM_Value to scale down to right pwm value for different FswTIM1->ARR = MaxPWM_Value; // ARR = Clock / (PRC * Fsw) = 72 MHz / (1 * Fsw) = ARRSo this works nicely at 2kHz you can see here I am generating a 50Hz sine wave. CH2 = PWM, CH3 PWM filtered through a LPF. CH1 = TIM 1 ISR execution time, TIM1 ISR that increments the LUT executes in about 550nsThis is the code in the ISRCode:// interrupt occurs at switching frequencyvoid TIM1_UP_TIM16_IRQHandler(void){TIM1->SR = ~TIM_SR_UIF; // Clear interruptGPIOB->BSRR = (1<<2); // turn onPWM_U = ((SPWM_LUT[Phase_Accumulator>>24])/PWM_Scaling);TIM1->CCR1 = PWM_U;Phase_Accumulator = Phase_Accumulator+Phase_Offset;GPIOB->BSRR = (1<<(2+16)); // turn off}However, at higher Fsw, the Sine Wave Starts to distort and gets clamped towards 0 as shown below.At 10 kHz we start seeing some clamping happeningAt 100 kHz it is very visibleSo this seems very odd to me. So I thought the PWM values weren't being generated properly, however it looks like they are, as I put all my equations in excel and they are correct.At 100 kHz I know one of the PWM values will be 51. Therefore I set an I/O pin to go to high when the PWM value is 51. The I/O pin is now connected to CH1Code:void TIM1_UP_TIM16_IRQHandler(void){TIM1->SR = ~TIM_SR_UIF; // Clear interruptPWM_U = ((SPWM_LUT[Phase_Accumulator>>24])/PWM_Scaling);if (PWM_U==51){GPIOB->BSRR = (1<<2); // turn on}else GPIOB->BSRR = (1<<(2+16)); // turn offTIM1->CCR1 = PWM_U;Phase_Accumulator = Phase_Accumulator+Phase_Offset;}This is what I get.As can be seen even though I/O pin goes high for CH1 indicating the PWM value is 51, meaning there should be an output on CH2, however there is no output on CH2. Why would this be?The pin stays high for a good 80 us so this should be plenty of timeAny thoughts?

author: Nominal Animal
author_rank: Super Contributor
timestamp: October 06, 2022, 10:26:17 am
content: Most likely the PWM counter has passed the value before your code assigns it to CC1, and thus never triggers. (The hardware uses == triggering, not >= . And the interrupt does not stop the PWM counter; it continues running all the time.)Like I wrote in #40, you need to be one period ahead of the actual PWM. See #41, and find out how you can use the internal CC1 buffering, so that on the new value is automatically loaded at the end of the previous period, and the ISR just assigns/buffers the value for the next period.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: October 06, 2022, 11:51:10 am
content: Quote from: Glenn0010 on October 06, 2022, 10:02:03 amYipe! Note the return type of pow(). The compiler may infer the value of this constant integer expression, but it has to return a float (or double!), and convert that back to int in the end.Use this instead:Code:#include <inttypes.h>...Phase_Offset = ((int64_t)Fout << 32) / Fsw;If you don't feel like using inttypes,long long(instead of justint) should be the same size on this platform.Tim

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 06, 2022, 12:19:42 pm
content: Quote from: Nominal Animal on October 06, 2022, 10:26:17 amSo I have done as you suggested to no avail. I was already using the buffered CCR1 and have advanced the calculation by one.This cannot be the issue. So with a LUT of 256 Values at 100 kHz and 50 Hz fundamental, the LUT index should increment by 1 roughly every 8 switching cycles meaning that we have the value PWM value 51 in this case there for 8 switching cycles and this can be seen in the measurement done above, however , during this, I am getting absolutley no PWM output at all.I'm totally confused.

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 06, 2022, 12:22:44 pm
content: Quote from: T3sl4co1l on October 06, 2022, 11:51:10 amSo I tried what you suggested to no avail. I don't think the calculation is the issue here. The PWM calculations are being generated correctly as I have checked.As I explained to Nominal Animal.With a LUT of 256 Values at 100 kHz and 50 Hz fundamental, the LUT index should increment by 1 roughly every 8 switching cycles meaning that we have the PWM value of 51 in the measurement above for 8 switching , however , during this, I am getting absolutely no PWM output at all.This is what is confusing to me

author: langwadt
author_rank: Super Contributor
timestamp: October 06, 2022, 02:03:13 pm
content: Quote from: Glenn0010 on October 06, 2022, 12:22:44 pmmissing an offset? the PWM should end up in the range 0-MaxPWM_Value, so really: MaxPWM_Value/2 +/- MaxPWM_Value/2

author: Nominal Animal
author_rank: Super Contributor
timestamp: October 06, 2022, 02:07:48 pm
content: With embedded C, you'll want to use<stdint.h>(which is provided by the C compiler) and not<inttypes.h>(which is provided by the standard C library, and may not be available in freestanding implementations).For Cortex-M4, there are two good options on calculating the phase offset. First is the 64-bit integer arithmetic T3sl4co1l suggested,Phase_Offset = ((int64_t)Fout << 32) / Fsw;although I'd use(uint64_t)and<stdint.h>instead. The other is to usefloat,Phase_Offset = 4294967296.0f * (float)Fout / (float)Fsw;where the(float)casts are not strictly necessary, and I only use them to help us humans with reduced cognitive load.The former approach is exact, and compiles to a__aeabi_uldivmod()call. On Cortex-M4, the exact implementation depends on the compiler, but is typically implemented using binary division, not hardware division operations. It can be a surprisingly slow operation, but it depends on the values of Fout and Fsw.The latter approach is limited to 24 bits of precision, but uses the hardware floating-point unit. That is, if Fout < Fsw < 16777216, it is exact. It takes about 35 cycles to run.The one I'd use, depends on what Fsw is. If I'd use the float approach, I'd also add 0.5f to Phase_Offset before it is (implictly) cast to an integer type, for correct rounding.(Note, Phase_Offset is a misleading name; consider something like Phase_Step instead.)Quote from: Glenn0010 on October 06, 2022, 12:19:42 pmDouble-check, please.If CCR1 is not auto-loaded from the buffered value when the PWM cycle restarts, then the symptoms match: at a low enough PWM value, the counter has advanced past the assigned value, and will not trigger.You can verify this by replacing your function withCode:// interrupt occurs at switching frequencyvoid TIM1_UP_TIM16_IRQHandler(void){TIM1->SR = ~TIM_SR_UIF;TIM1->CCR1 = 51;}and seeing if you get any PWM output on the scope. Increment the value (51, above) until you do; the idea is to find the smallest number that you can reliably see the PWM output on the scope.You then add a small delay just before the TIM1->CCR1 line.If that makes the PWM again disappear on the scope, you are not using a buffered CCR1.If even a quarter-of-PWM-period delay makes no change, you are using a buffered CCR1.

author: Glenn0010
author_rank: Regular Contributor
timestamp: October 06, 2022, 02:16:58 pm
content: Hi Folks,So I figured it out. I was just being stupidTurns out it was working fine and I forgot that I had enabled dead time on Timer 1. I only pickup this code again after a year or so and I forgot I had put in dead time.The dead time was set to 1.7us across frequency hence why you see the sine wave getting dragged down. Because 1.7 us is a much larger portion of 10 us versus say 500 us, that's why we this difference between 2 and 100 kHz.I found the issue by doing this: I set a pin high whenever we should have 100% duty cycle. I trigger the scope using this pin. Then I checked the duty cycle and notices it wasn't anywhere near 100% (it was 83%) which made me realize it was dead time. Sure enough when I put dead time to its lowest value of 13.9 ns, it works fine now at 100 kHz too.I guess now I understand how to do deadtime compensation. To account for dead time I can increase the duty cycle of each pulse by the amount of dead time. and this should make it more "sinusoidal"Cheers for the help you folks have been giving