Is there anything left to optimize here?

I want to make a VFD (this code is without knobs for now) and I am running into a problem where the ESP32C6 is far too slow for this. How can I optimize this further? Here is the code.


float phi = 1;  // degrees
int uhol = 0;  // integer angle

int castprac1 = 1;  // duty cycle (from 0 to 2047 parts)
int castprac2 = 1;  // duty cycle (from 0 to 2047 parts)
int castprac3 = 1;  // duty cycle (from 0 to 2047 parts)

float frequencypwm = 1;

unsigned long past = 1;

float frekvenciavlny = 50;
float amplituda = 1;

unsigned long cas = 0;

float sinval[362];  // 10 stupňov na 1 hodnotu tejto "tabuľky"

float casvlny = 0;
float caspolvlny = 0;
float casvovlne = 0;
float castvlny = 0;

void setup() {

  pinMode(18, OUTPUT);
  pinMode(19, OUTPUT);
  pinMode(20, OUTPUT);
  pinMode(21, OUTPUT);
  pinMode(22, OUTPUT);
  pinMode(23, OUTPUT);

  byte anres = 11;  // bits // 2048 rôznych hodnôt

  Serial.begin(115200);

  analogWriteResolution(18, anres);
  analogWriteResolution(19, anres);
  analogWriteResolution(20, anres);
  analogWriteResolution(21, anres);
  analogWriteResolution(22, anres);
  analogWriteResolution(23, anres);

  frequencypwm = 10000;  // Hz

  analogWriteFrequency(18, frequencypwm);
  analogWriteFrequency(19, frequencypwm);
  analogWriteFrequency(20, frequencypwm);
  analogWriteFrequency(21, frequencypwm);
  analogWriteFrequency(22, frequencypwm);
  analogWriteFrequency(23, frequencypwm);

  for (int i = 0; i < 361; i = i + 1) {
    int z = i * 1;
    double radiany = (((double)z * 71.0) / 4068.0);
    sinval[i] = 2047 * (abs(sin(radiany)));
    Serial.println(sinval[i]);
  }
}

void loop() {

  cas = micros();

  if (Serial.available() > 2) {
    frekvenciavlny = Serial.parseFloat();
    amplituda = Serial.parseFloat();
    Serial.println(frekvenciavlny);
    Serial.println(amplituda);
  }

  //frekvenciavlny = analogRead(A0) / 4;

  casvlny = 1000000.0 / (float)frekvenciavlny;
  caspolvlny = (float)casvlny / 2.0;
  casvovlne = fmod(cas, casvlny);
  castvlny = ((float)casvovlne / (float)casvlny);

  phi = 360.0 * castvlny;
  phi = fmod(phi, 360.0);

  uhol = int(phi);

  castprac1 = amplituda * sinval[uhol];
  castprac2 = amplituda * sinval[((uhol + 120) % 360)];
  castprac3 = amplituda * sinval[((uhol + 240) % 360)];

  if (uhol < 180) {
    analogWrite(18, castprac1);
    analogWrite(19, 0);
  } else {
    analogWrite(18, 0);
    analogWrite(19, castprac1);
  }

  if (((uhol + 120) % 360) < 180) {
    analogWrite(20, castprac2);
    analogWrite(21, 0);
  } else {
    analogWrite(20, 0);
    analogWrite(21, castprac2);
  }

  if (((uhol + 240) % 360) < 180) {
    analogWrite(22, castprac3);
    analogWrite(23, 0);
  } else {
    analogWrite(22, 0);
    analogWrite(23, castprac3);
  }
}

A couple of comments

Do you have to do the calculations using floats or could you use integers ?

The modulo operator has a reputation for being slow. Could you use a different method ?

Where is the serial data coming from and how is it terminated ?

The Serial.parseFloat() will cause a major delay while waiting for the entire number to arrive. It would be better to read the Serial data into a buffer, then parse the numbers after a complete line has been received.

The Serial.parseFloat() runs only when there is something to receive, and is only for testing.

I can try to use integers everywhere, maybe it'll speed the program up a bit.

  for (int i = 0; i < 361; i = i + 1) {
    int z = i * 1;
    double radiany = (((double)z * 71.0) / 4068.0);
    sinval[i] = 2047 * (abs(sin(radiany)));
    Serial.println(sinval[i]);
  }

double radiany = z * (71.0 / 4068.0); // compiler can now optimize compile time.

but that is executed only once in setup()

More performance gain is by removing the % and fmod as those are expensive.
You could use 3 variables for the 3 different phases, better use arrays.
something like

  uhol[0] = int(phi);
  uhol[1] = uhol[0] + 120;
  if (uhol[1] > 360) uhol[1] -= 360;
  uhol[2] = uhol[0] + 240;
  if (uhol[2] > 360) uhol[2] -= 360;

  for (int i = 0; i < 3; i++)
  {
    castprac[i] = amplituda * sinval(uhol[i]);
    if (uhol[i] < 180)
    {
      analogWrite(18 + i*2, castprac[i]);
      analogWrite(19 + i*2, 0);
    } else {
      analogWrite(18 + i*2, 0);
      analogWrite(19 + i*2, castprac[i]);
    }
  }

I literally try to minimize the amount of variables, calculation and variable modification to try to speed it up. Every cycle counts here...

These 2 lines get computed even if their values don't change.

Chances are if the order in which the operations are performed is correct, that 32-bit integer will suffice.

It is similar to a division.

I would say yes there is optimization to be had.

variable not used

It literally is division, but returns the remainder instead.

It is used

Memory is cheap on an ESP32, use three different arrays for sinval offset by 120 degrees to eliminate calculating the array index.

Valid point, but that still is at least 725 more bytes of data.

But I only need to calculate 180 values, and just copy it however many times I need it.

You’re saying the ESP32C6 is too slow for your VFD code, yeah this can happen if timing isn’t optimized properly. I think the main issue is using float values and sin calculations in real-time, which makes it slower. It’s better to use a precomputed lookup table with integers and avoid float math inside the loop. Also try using hardware timers or PWM (like LEDC) instead of updating manually, it reduces CPU load. If it’s still slow, maybe lower the resolution a bit or tweak the loop timing, small changes can help alot.

Even a calculated lookup table will do the trick.

Since memory really is not a scarce resource, anything that can be pre-calculated and stored will be fine.

Also avoid float computations, many times you know where how many decimals you will be using. 32-bit integer computations should easily suffice if you decide on the accuracy up front (a float is anyway only 23-bit accurate but has a floating decimal point, so you should get plenty of accuracy)

Actually, I only need 11 bits, but the smallest data type that can fit it is a two byte word. I will look into this later.

You appear to be calculating the degrees within the waveform from the value of micros(). Unless the rollover period of micros() is an even number of cycles there will be a disruption in the output waveform when rollover occurs. The use of a timer to give a consistent timing would seem to be a better idea.

  cas = micros();

  casvlny = 1000000.0 / (float)frekvenciavlny;
  casvovlne = fmod(cas, casvlny);
  castvlny = ((float)casvovlne / (float)casvlny);

  phi = 360.0 * castvlny;

If true, then unwrap any loops that use an index into some stringy code with numeric values for indexes. Only useful when the programmed loop only loops a few times. Yeah, doesn’t look so nice, but runs fast!

On top of the other remarks, why don’t you use LEDC? It will give you hardware-driven PWM with far less overhead and much more stable timing than analogWrite on the ESP32-C6.

Have you also considered using a hardware timer instead of rebuilding the phase from micros() on every loop iteration? Driving the waveform from a fixed-rate timer interrupt with a phase accumulator is a common approach. It removes the need for fmod, divisions, and repeated time reads, and it gives deterministic timing without jitter.

I don’t know enough about the C6 to be certain, but have you looked at MCPWM as well? It should be preferred if you need true three-phase complementary outputs with proper dead time - as long as it’s in the feature set on ESP32-C6 .