I am trying to implement a PI controller in C that outputs / controls a PWM's duty cycle on a microcontroller. The duty cycle that I can write to the certain PWM control register is limited to 10bit (values 0 - 1023 correspond to 0% - 100% duty cycle). My controllers output "Stell", that should correspond to a duty cycle, exceeds this bit limit due to calculations of the controlled values. Here is a simplified version of my controller function that is called regularly with a base clock:

void controller(void)
    int32_t stell_1, stell_2, stell; 
    int32_t Integral; 
    int16_t delta_e; 
    uint32_t DZSoll, DZIst; 
    uint16_t duty_cycle; 
    int32_t  temp_duty_cycle; 
    //Error = Setpoint - actual_value
    delta_e = DZSoll - DZIst; 

    //PI controller
    stell_1 = delta_e * Kp; 
    Integral += delta_e; 
    stell_2 = Ki * Integral; 
    stell = stell_1 + stell_2; 

    //Max values 
    Max_S1 = Max_delta_e * Kp; 
    Max_S2 = (Max_delta_e + Integral) * Ki; 
    Max_Stell = Max_S1 + Max_S2; 

    //Scaling of the controller output to a 10bit duty cycle 
    temp_duty_cycle = stell<<10; //*1023
    duty_cycle = temp_duty_cycle/Max_Stell; 


Now, I thought that I would need to scale the controller output "Stell" to the desired 10bit "duty cycle" by setting them into a relation: duty_cycle/2^10 = Stell/Max_Stell (basically to receive the scaling factor). Nevertheless, this only works for positive values of "stell" as my duty cycle by definition has to be a positive value (uint16_t). In the case that my actual value "DZIst" is bigger that the setpoint "DZSoll" (for instance on a overshoot of the integral part of the PI controller), this results in a negative "Stell" which is then incorrectly calculated into a duty cycle. This is especially a problem on lower setpoints as the Integral value cannot compensate the negative control error.

Am I scaling "Stell" correctly to the 10bit duty cycle? Could I compensate the negative "Stell" values with an offset (and how?)?

  • 1
    \$\begingroup\$ Since you know you can't physically output a negative PWM, couldn't you just limit 'Stell' to a minimum of zero? \$\endgroup\$
    – brhans
    Commented Dec 16, 2022 at 15:59
  • \$\begingroup\$ Love the should-be DZ and the is DZ. I also think it was appropriate to provide a simplified version of the code, because all you are trying to do is to get the point across about getting negative values in certain situations. But what bothers me is that you haven't drawn out diagrams to illustrate what the PWM should be in various sitautions. I could just recommend limiting the output with a simple if..else.., and perhaps recommend another such for the integrator, too. But I'd like to know more because simple suggestions like that have a habit of yielding bad results in real processes. \$\endgroup\$
    – jonk
    Commented Dec 16, 2022 at 18:45

2 Answers 2


You didn't provide any declarations for Kp, Ki, Max_delta_e, Max_S1, Max_S2, Max_Stell, DZSoll, or DZIst. Missing declarations/definitions like that worries me that I may make mistakes in offering code that won't experience truncation of some kind. I also don't feel comfortable because I don't feel I understand the range of your input source (ADC, I assume, but how many bits?) Also, I don't see anything about sampling time.

That said, I'll just get to a few points.

It's important that you keep the time between the ADC reading (assuming that's where you get your input values) and your DAC update both fixed and short. The control computation code will likely have a variable execution time. But you really don't want a situation where the updates occur with widely varying delays from ADC-in to DAC-out. That kind of thing messes with the usual assumptions made in PI analysis. And you don't want long delays. PI just works very badly with long delays. So it is very important to keep things unvaryingly tight.

It is also likely you want to do something about integral windup. There are a variety of techniques. But 'jacketing' is common enough that I'm sure you can easily find something on the topic.

A problem with jacketing is:

  1. It is possible to get stuck at the saturation limit when the control input is saturated for a while, leading to slower response and worse steady-state error. This can be further addressed by anti-reset windup or by non-linear anti-windup methods.
  2. It can cause the control output to bounce off of the saturation limits if the control output is near the limits and the error changes rapidly, leading to oscillation, among other things. This can be further addressed by lead-lag compensation, pre-filtering, and non-linear control methods.

This is why I wrote in comments that I'd like to know more about your situation and why I wrote, "simple suggestions [...] have a habit of yielding bad results in real processes."

Let me suggest a simple bit of code. Here is where I make those uncomfortable assumptions, now. I'm assuming 16-bit ADC, 10-bit DAC, 16 bit unsigned values for Kp and Ki, and a direct-acting PI controller so that Kp and Ki are positive-only. But what do I know? You need to clarify things. But I'll just push on ahead, ignorantly:

/* Declarations and definitions of static lifetime variables
   which need to be set up BEFORE calling the PI routine below. */
uint16_t DZSoll;       /* setpoint -- set once? */
uint16_t DZIst;        /* must be set by caller before calling pi() */
uint16_t Kp;           /* P-term -- set once? */
uint16_t Ki;           /* I-term -- set once? */
int16_t Integral;      /* updated as a side-effect of calling pi() */

uint16_t pi ( void ) {
  int16_t error= DZSoll - DZIst;
  int32_t control_output= Kp * (int32_t) error + Ki * (int32_t) Integral;
  if (control_output > 1023) {
    control_output= 1023;
    Integral += error - ((int32_t) 1023 - Kp * (int32_t) error) / Ki;
  } else if (control_output < 0) {
    control_output = 0;
    Integral += error - Kp * (int32_t) error / Ki;
  } else {
    Integral += error;
  return (uint16_t) control_output;

I didn't bother adding jacketing code to the Integral term in the above code. It's easy enough to add (just wrap it with the usual if..else.. code.) Feel free. I also don't like the idea of not passing in DZIst. But I decided to keep things the way you have them and just choke down my consternations. Also, pi() will only return values from 0 to 1023. So you may be able to feed that directly to your DAC. I also did add a back-calculation for the integral term (hopefully, I wrote it right.)

I've not tested any of the above code. So just treat this as a suggestion for thought.


Some things to do before implementing any form of electronics or firmware:

  • Step 1: decide a minimum acceptable resolution for the regulator. All hardware has to be picked according to this resolution, including PWM, ADC, voltage dividing resistors, CPU clock, ADC reference. And in the software: the amount of digits/bits used by your types, how large rounding errors you can accept, whether digital filters should be used or not etc. And also real-time requirements: if you are allowed to discard reads and just read at a fixed point in time, or if you have to use every sample - MCU execution speed vs ADC sample rate.

    If you haven't established this from the start then you are just fumbling around in the dark from there on.

    It's usually fine and recommended to have the ADC read with greater resolution than what's required. Regulating with higher resolution than you read back isn't really helpful, it could even be problematic.

  • Step 2: establish a unit to use everywhere by your firmware. For example the amount of ticks in a PWM period could be one such unit. Or raw ADC values. Either way, very likely unsigned. You should ensure that the PI(D) is also working on that same unit and also that ADC reads are re-scaled to that unit.

  • Step 3: make some qualified decision about whether to use PI or PID. There's tonnes of written material about this and I'm far from a PID guru so I won't go into that.

  • Step 4: decide how to synchronise it all. For example have a cyclic timer which grabs the calculated values and updates the PWM every x miliseconds. This could also be when you do the ADC reads, if that's sufficient, depending on what you previously came up with in step 1.

Now... your question doesn't mention any of the above, except you seem to have gone for PI. Consider what (lack of) design decisions lead you to the point where you are currently and then take it from there. You might be doing fine or you might have to toss your project in the garbage, or somewhere in between.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.