You could use gain scheduling.
But output scheduling would be a good choice for pH as it is nonlinear.
Also you could use model predictive control if you have a titration curve for the fluid.
here is a really raw output scheduler. you need to adjust the sheduling tables based upon the fluids and pumps and output types.
////
struct PIDloop {
double pvh;
double pvl;
double pv;
double sp;
double lpv;
double lsp;
double out;
double kc;
double td;
double ti;
double ddcout;
double yn;
double mx;
double kd;
char *mode;
};
//output scheduling tables six segments
float fgenxf[3][7][2] = {
//first set xy
0.01,0.1,
0.22,0.3,
0.4,0.5,
0.6,0.7,
0.8,0.9,
0.90,0.11,
0.9912,0.13,
//second set xy
0.02,0.15,
0.16,0.17,
0.18,0.19,
0.20,0.21,
0.922,0.23,
0.924,0.25,
0.99926,0.27,
//third set xy
0.0003,0.29,
0.30,0.31,
0.532,0.33,
0.634,0.35,
0.836,0.37,
0.838,0.39,
0.9999940,0.41};
float x[3];
float y[3];
float pidout;
float output;
int set;
long int millisnext;
struct PIDloop aic02; /*ph loop*/
const int AI02A = A2; // name the A2 pin
const int FY01 = 7; // name the base pin
const int FY02= 9; // name the A2 pin
const int l_ts=200;//scan interval
void setup() {
delay(5000);
aic02.pvh = 8.0;
aic02.pvl = 6.0;
aic02.pv = 7.0;
aic02.sp = 7.1;
aic02.lpv = 1.0/2.0;
aic02.lsp = 1.1/2.0;
aic02.out = 0.5;
aic02.kc = 1.1;
aic02.td = 0.0001;
aic02.ti = 60.0*60.0;
aic02.ddcout = 0.0;
aic02.yn = 0.44;
aic02.mx = 0.5;
aic02.kd = 1000.0;
aic02.mode = "auto";
Serial.begin(9600);
int set = 0;
millisnext= millis()+l_ts;
}
void loop() {
aic02.pv = scalar(double (analog_measure(AI02A)), 0, 1023, aic02.pvl, aic02.pvh);
pid(&aic02, l_ts);
for (set=0;set<3;set++)
{
int index = fgn(aic02.out,set);
if (7>index && index>-1){
y[set] = fscalar(aic02.out, fgenxf [set] [index-1]
- , fgenxf [set] [index]
- , fgenxf [set] [index-1] [1], fgenxf [set][index][1]);
}
else
{
y[set] = aic02.out;
}
}
analogWrite(FY01, int (scalar (aic02.out, 0.0, 0.49, 0.0, 255.0)));
analogWrite(FY02, int (scalar (y[1], 0.51, 1.00, 0.0, 255.0)));
while( millisnext> millis())
{
}
if (millis()<l_ts){
millisnext = millis();
}
else{
millisnext+=l_ts;
}
}
void pid (struct PIDloop *lptr, double ts) /* function doing the pid loop JFL*/
{
double yn1,err;
lptr->lpv=(lptr->pv-lptr->pvl)/(lptr->pvh-lptr->pvl);
lptr->lsp=(lptr->sp-lptr->pvl)/(lptr->pvh-lptr->pvl);
err=lptr->lsp-lptr->lpv;
yn1=lptr->yn+ts/(ts+lptr->td/lptr->kd)*(lptr->lpv-lptr->yn);
lptr->mx+=err*lptr->kc*ts/lptr->ti;
lptr->out=lptr->kc*err-(yn1-lptr->yn)*lptr->kc*(lptr->td)/ts+lptr->mx;
if (lptr->out >1.0){
lptr->out=1.0;
lptr->mx=-(lptr->kc*err-(yn1-lptr->yn)*lptr->kc*(lptr->td)/ts)+lptr->out;
}
if (lptr->out <0.0){
lptr->out=0.0;
lptr->mx=-(lptr->kc*err-(yn1-lptr->yn)*lptr->kc*(lptr->td)/ts)+lptr->out;
}
lptr->yn=yn1;
}
//interpolate float schedule table
float fscalar(float x, float in_min, float in_max, float out_min, float out_max)
{
return ( (x) - (in_min)) * (out_max - out_min) / ( (in_max) - (in_min)) + out_min;
}
//scan the output schedule table
int fgn(float x, int i){
int count = 0;
int scount = -1;
boolean found = false;
for (count = 0; count<7;count++){
if (fgenxf
[count][0]< x) scount = count;
}
return (scount);
}
//scale the integer to the real value
double scalar(double x, double in_min, double in_max, double out_min, double out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
double analog_measure(int analogpin)
{
analogRead(analogpin);
}