Hello all!
I have undertaken the project of creating a throttle body controller for a project car of mine, and have been experimenting with different code and motor drivers for around a month now.
I have been using PID control and a feedback loop from a throttle position sensor to calculate the error from the setpoint (which is a potentiometer as of now).
The issue I am having is in the response time/how well the throttle "follows" the pot input. I just can't quite seem to get it to follow the input accurately, without overshooting or oscillating rapidly.
I have tried to follow various tuning algorithms and they all seem to end up in too much overshoot for my liking.
As of now, I have a positive and negative coefficient for all 3 parts of the PID, this is because on the closing of the throttle the spring assists the action, so I figured less gain was needed in that direction of motion.
This is an image of what the response looks like. I have turned off both the Derivative and feed forward part of the tune, as I was just attempting to get a response with no oscillations.
I can get the throttle to follow the pedal quite well by upping the proportional gain, but that results in a bunch of overshoot, which is not good enough for me to use in an actual setting.
Below is the code I am running
/*
Program: TB controller program
Motor Driver: Cytron 13Amp 6V-30V DC Motor Driver
Date: 07/05/2025
Author: Kaden Van Domselaar
Wiring: Refer to pin definitions
*/
#include "CytronMotorDriver.h"
// Pin definitions
#define PEDAL_PIN A0 //currently POT input (full size signal)
#define PEDAL_PIN2 A1 //Half size signal pedal sensor
#define TPS_PIN A2 // TPS1 - Blue grey
#define TPS_PIN2 A3 // TPS2 - Yellow white
#define PWM 3
#define DIR 4
#define Setsim 13 //simulate WOT
CytronMD motor(PWM_DIR, 3, 4); //define PWM and DIR pins to driver
struct {
int Kp, Kpe, Ki, Kie, integralofe, Kd, Kde, derivativeofe, Kff, Kffe; //PID coefficents
int KpNeg, KpPos, KiNeg, KiPos, KdNeg, KdPos, KffNeg, KffPos; //PID coefficent Limits
double PreviousTime, deltaT, CurrentTime; //Used for Integral Action
float PreviousSetpoint, DeltaSetpoint; //Used for Kff
float PreviousError; //Used for Kd
} PID;
struct {
int Pedal1Raw, Pedal2Raw, TPS1Raw, TPS2Raw; //Raw Readings from sensors
int TruePedal, TrueTPS; //Outputs of Filters - Used to show filter action on Raw signal
int Pedal1Min, Pedal1Max, Pedal2Min, Pedal2Max, TPS1Min, TPS1Max, TPS2Min, TPS2Max; //Mina and max analog values for all 4 sensors
}IN;
int dutycyclelow = -255, dutycyclehigh = 255;
int u = 0; //control signal
int e = 0; //error
void setup() {
TCCR2B = TCCR2B & 0b11111000 | 0x01; //PWM to 32kHz
pinMode(PEDAL_PIN, INPUT); //
pinMode(PEDAL_PIN2, INPUT); //
pinMode(TPS_PIN, INPUT); //
pinMode(TPS_PIN2, INPUT); //
pinMode(PWM, OUTPUT); // 0-255
pinMode(DIR, OUTPUT); // High or Low
Serial.begin(9600);
// PID gains
PID.KpPos = 300; // 100 = 1:1 ratio
PID.KpNeg = 150;
PID.KiPos = 100; // 100 = 1:1 ratio
PID.KiNeg = 50;
PID.KdPos = 0; // 1000 = 1:1 ratio
PID.KdNeg = 0;
PID.KffPos = 0; // 1000 = 1:1 ratio NOT BEING USED
PID.KffNeg = 0;
PID.PreviousTime = 0;
PID.deltaT = 0;
PID.integralofe = 0;
PID.derivativeofe = 0;
PID.PreviousSetpoint = 0;
PID.PreviousError = 0;
PID.DeltaSetpoint = 0;
PID.CurrentTime = 0;
PID.Kpe = 0;
PID.Kie = 0;
PID.Kde = 0;
PID.Kffe = 0;
IN.TruePedal = 0;
IN.TrueTPS = 0;
IN.Pedal1Min = 148;
IN.Pedal1Max = 534;
IN.Pedal2Min = 0; //Not being used right now
IN.Pedal2Max = 0;
IN.TPS1Min = 790;
IN.TPS1Max = 360;
IN.TPS2Min = 0; //not being used right now
IN.TPS2Max = 0;
}
void loop() {
//Calculate Delta Time
PID.CurrentTime = millis();
PID.deltaT = (PID.CurrentTime-PID.PreviousTime)/1000;
PID.PreviousTime = PID.CurrentTime;
//Scan Pedal Signals
//IN.Pedal1Raw = map(analogRead(PEDAL_PIN),IN.Pedal1Min,IN.Pedal1Max,0,100); //Pedal one
IN.Pedal1Raw = map(analogRead(PEDAL_PIN),0,1020,1,100); //Reading Pot for testing
IN.Pedal1Raw = min(IN.Pedal1Raw,100);
IN.Pedal1Raw = max(IN.Pedal1Raw,0);
IN.Pedal2Raw = map(analogRead(PEDAL_PIN2),235,500,0,100); //Read pedal two
IN.Pedal2Raw = min(IN.Pedal2Raw,100);
IN.Pedal2Raw = max(IN.Pedal2Raw,0);
IN.TPS1Raw = map(analogRead(TPS_PIN), IN.TPS1Min, IN.TPS1Max, 0, 100); //read TPS one
IN.TPS2Raw = map(analogRead(TPS_PIN2), 777, 987, 0, 100); //read TPS two
//Filters not being used bc they slowed down the response time and were not needed, will remove extra variables in final version
//Filtering Pedal signal
IN.TruePedal = IN.Pedal1Raw;
//Filtering TPS signal
IN.TrueTPS = IN.TPS1Raw;
if(digitalRead(13)) {
IN.TruePedal = 80;
} //force WOT (for testing PID loop)
//ERROR CALC
e = IN.TruePedal - IN.TrueTPS;
//Kp error calculaton
if(e>0)
{
PID.Kp = PID.KpPos; //positive error gain
}
else
{
PID.Kp = PID.KpNeg; //negative error gain
}
PID.Kpe = PID.Kp * e / 100;
//Ki error calculation
if(abs(e) < 7)
{
PID.integralofe += e * PID.deltaT; //If we are within 7 percent error then start integrating the error
}
else
{
PID.integralofe = 0;
}
//add reset integralofe to 0 if error changes direction
if(e>0)
{
PID.Ki = PID.KiPos; //postive error gain
}
else
{
PID.Ki = PID.KiNeg; //negative error gain
}
PID.Kie = PID.Ki * PID.integralofe / 100;
//Kd error calculation
if(e>0)
{
PID.Kd = PID.KdPos; //postive error gain
}
else
{
PID.Kd = PID.KdNeg; //negative error gain
}
PID.derivativeofe = (e - PID.PreviousError)/PID.deltaT;
PID.PreviousError = e;
PID.Kde = PID.Kd * PID.derivativeofe / 1000;
//Kf calculations NOT BEING USED
/*
if(e>0) {PID.Kff = PID.KffPos;}
else {PID.Kff = PID.KffNeg;}
PID.DeltaSetpoint = (IN.TruePedal - PID.PreviousSetpoint);
PID.PreviousSetpoint = IN.TruePedal;
PID.Kffe = PID.DeltaSetpoint * PID.Kff / 1000;
*/
//PID summing
u = PID.Kpe + PID.Kie + PID.Kde + PID.Kffe;
//Cap PWM signal
if(u>dutycyclehigh)
{
u = dutycyclehigh;
}
if(u<dutycyclelow)
{
u = dutycyclelow;
}
//Drive Motor
motor.setSpeed(u);
//Tuning stuff for the graph
Serial.print(PID.Kpe);
Serial.print(" ");
Serial.print(PID.Kie);
Serial.print(" ");
Serial.print(PID.Kde);
Serial.print(" ");
Serial.print(IN.TruePedal);
Serial.print(" ");
Serial.print(IN.TrueTPS);
Serial.print(" ");
Serial.println(u);
}
I saw in someone else code in a similar project that they had limited the Integral to only working within 7 percent of the setpoint, so I tried to compliment that into my code as well.
I would eventually like my setup to be as accurate as the one in this video:
This person though has developed their own microcontroller and has integrated it with an ECU tuning software, with CANBUS capability, fancy stuff. In the video you can see how well the TPS follows the pedal, unlike mine. When I try and replicate the coefficients he uses, it results in a ton of overshoot, which makes me think my code is poorly written.
On top of that he is able to get his TPS to read within 5 percent or less of the setpoint with just proportional gain, something that seems to be impossible for me to do.
Right now, I am a bit stumped as to what I should do to get this setup to work better. I was hoping by posting this to the forum you all could give me suggestions on how to make it better.
This is my first Arduino/electronics project, so any input helps, Thanks!



