Code optimization for a PID project featuring an LCD and a Sd card

Good afternoon,
I am currently working on the following project: an incubator which regulates the temperature and the oxygen concentration in a box using, respectively, a heater and a nytrogen tube tank with a valve (which reduces the concentration of oxygen in the box). These are controlled by a mosfet and a relay respectively. Whether we are turning the heater or the nytrogen flow on is determined by using data from a temperature and an oxygen sensor and implementing a PID feedback loop. The target temperature and oxygen concentrations can be finetuned using potentiometers. The data is plotted on an LCD, which also features an SD card, which I would love to make use of to log the acquired data.
The problem is that, as a iterated upon the code with the SD-card code, I have now exceeded the memory limit on the Arduino Uno card.
Do people of this forum see any potential iterations (other than getting an Arduino Mega) that could diminish the use of memory by the 600 bytes that are now excessive? Thanks in advance! If there are any questions regarding the project that would help you answer the question better, feel free to ask!

image

The code in question

#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include "DFRobot_OxygenSensor.h"
#include <PID_v1.h>
#include <SdFat.h>
#define SEALEVELPRESSURE_HPA (1013.25)
#define TFT_DC 9
#define TFT_CS 10
#define TFT_MISO 12
#define TFT_MOSI 11
#define TFT_CLK 13
#define TFT_RST 20
#define SD_CS 4  // SD card select pin
#define Oxygen_IICAddress ADDRESS_3
#define COLLECT_NUMBER 10
#define MG_PIN (A0)
#define BOOL_PIN (2)
#define DC_GAIN (8.5)
#define READ_SAMPLE_INTERVAL (50)
#define READ_SAMPLE_TIMES (5)
#define ZERO_POINT_VOLTAGE (0.350)
#define REACTION_VOLTGAE (0.030)
#define FAN_RELAY_PIN 6
#define O2_RELAY_PIN 8

float CO2Curve[3] = { 2.602, ZERO_POINT_VOLTAGE, (REACTION_VOLTGAE / (2.602 - 3)) };

DFRobot_OxygenSensor oxygen;
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_MOSI, TFT_CLK, TFT_RST, TFT_MISO);
Adafruit_BME280 bme;
SdFat SD;       
File dataFile; 

double tempSetpoint;
double o2Setpoint;
unsigned long previousMillis = 0;
const unsigned long updateInterval = 300;
double temperature;
double tempOutput;
double oxygenData;
double o2Output;
int co2_ppm;
double Kp_temp = 5, Ki_temp = 5, Kd_temp = 2;
double Kp_o2 = 1, Ki_o2 = 1, Kd_o2 = 1;

PID tempPID(&temperature, &tempOutput, &tempSetpoint, Kp_temp, Ki_temp, Kd_temp, DIRECT);
PID o2PID(&oxygenData, &o2Output, &o2Setpoint, Kp_o2, Ki_o2, Kd_o2, DIRECT);

void setup() {
  Serial.begin(9600);
  tempPID.SetOutputLimits(-10000, 10000);
  tempPID.SetMode(AUTOMATIC);
  o2PID.SetOutputLimits(-10000, 10000);
  o2PID.SetMode(AUTOMATIC);
  pinMode(FAN_RELAY_PIN, OUTPUT);
  pinMode(O2_RELAY_PIN, OUTPUT);
  tft.begin();
  tft.setRotation(1);
  tft.fillScreen(ILI9341_BLACK);

  pinMode(BOOL_PIN, INPUT);
  digitalWrite(BOOL_PIN, HIGH);

  if (!bme.begin(0x76)) {
    Serial.println(F("a"));
    while (1)
      ;
  }

  oxygen.begin(Oxygen_IICAddress);

  tft.drawRect(tft.width() - 125, 5, 110, 80, ILI9341_WHITE);

  tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
  tft.setTextSize(1);
  tft.setCursor(tft.width() - 120, 10);
  tft.print(F("OBJ TEMP"));

  tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
  tft.setCursor(tft.width() - 120, 20);
  tft.print(F("CURR TEMP"));

  tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
  tft.setCursor(tft.width() - 120, 40);
  tft.print(F("OBJ O2"));

  tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
  tft.setCursor(tft.width() - 120, 50);
  tft.print(F("CURR O2"));

  tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
  tft.setCursor(tft.width() - 120, 70);
  tft.print(F("CURR CO2"));

  // Initialize SD card
  if (!SD.begin(SD_CS, SD_SCK_MHZ(25))) {  // SD card initialization with 25 MHz speed
    Serial.println(F("b"));
    while (true)
      ;  // Halt if SD card fails to initialize
  }
  Serial.println(F("c"));

  // Open the file for appending, or create it if it doesn't exist
  dataFile = SD.open("dataset.csv", FILE_WRITE);
  if (!dataFile) {
    Serial.println(F("d"));
    while (true)
      ;  // Halt if the file cannot be opened
  }

  // Write the header to the file (only if the file is empty)
  if (dataFile.size() == 0) {
    dataFile.println(F("T,O2,CO2"));
  }
  dataFile.close();
}

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= updateInterval) {

    tft.setTextColor(ILI9341_BLACK, ILI9341_BLACK);
    tft.setCursor(tft.width() - 50, 10);
    tft.print(tempSetpoint);

    tft.setTextColor(ILI9341_BLACK, ILI9341_BLACK);
    tft.setCursor(tft.width() - 50, 20);
    tft.print(temperature);

    if (tempOutput > 0) {
      tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK);
    } else {
      tft.setTextColor(ILI9341_RED, ILI9341_BLACK);
    }
    tft.setCursor(tft.width() - 120, 30);
    tft.print(F("HEATING STATUS"));

    tft.setTextColor(ILI9341_BLACK, ILI9341_BLACK);
    tft.setCursor(tft.width() - 50, 40);
    tft.print(o2Setpoint);

    tft.setTextColor(ILI9341_BLACK, ILI9341_BLACK);
    tft.setCursor(tft.width() - 50, 50);
    tft.print(oxygenData);

    tft.setTextColor(ILI9341_BLACK, ILI9341_BLACK);
    tft.setCursor(tft.width() - 50, 70);
    tft.print(co2_ppm);

    if (o2Output < 0) {
      tft.setTextColor(ILI9341_GREEN, ILI9341_BLACK);
    } else {
      tft.setTextColor(ILI9341_RED, ILI9341_BLACK);
    }
    tft.setCursor(tft.width() - 120, 60);
    tft.print(F("O2 STATUS"));

    tempSetpoint = ceil((analogRead(A3) / 1023 * 20 + 23) * 10) / 10;
    temperature = bme.readTemperature();
    int y_temp = (tft.height() - 20) - ((temperature - 18) / (45 - 18)) * (tft.height() - 20);
    tempPID.Compute();
    controlTemperature(tempOutput);

    o2Setpoint = ceil((analogRead(A2) / 1023 * 25) * 10) / 10;
    oxygenData = oxygen.getOxygenData(COLLECT_NUMBER);
    int y_oxygen = (tft.height() - 20) - ((oxygenData) / (25)) * (tft.height() - 20);
    o2PID.Compute();
    controlO2(o2Output);


    co2_ppm = MGGetPercentage(MGRead(MG_PIN), CO2Curve);
    int y_co2 = (tft.height() - 20) - ((co2_ppm) / (1500)) * (tft.height() - 20);

    tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
    tft.setCursor(tft.width() - 50, 10);
    tft.print(tempSetpoint);

    tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
    tft.setCursor(tft.width() - 50, 20);
    tft.print(temperature);

    tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
    tft.setCursor(tft.width() - 50, 40);
    tft.print(o2Setpoint);

    tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
    tft.setCursor(tft.width() - 50, 50);
    tft.print(oxygenData);

    tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
    tft.setCursor(tft.width() - 50, 70);
    tft.print(co2_ppm);

    dataFile = SD.open("dataset.csv", FILE_WRITE);
    if (dataFile) {
      // Log data to the file
      dataFile.print(temperature);
      dataFile.print(F(","));
      dataFile.print(oxygenData);
      dataFile.print(F(","));
      dataFile.print(co2_ppm);
      dataFile.close();
    } else {
      Serial.println(F("e"));
    }
  }
}

float MGRead(int mg_pin) {
  int i;
  float v = 0;

  for (i = 0; i < READ_SAMPLE_TIMES; i++) {
    v += analogRead(mg_pin);
    delay(READ_SAMPLE_INTERVAL);
  }
  v = (v / READ_SAMPLE_TIMES) * 5 / 1024;
  return v;
}

int MGGetPercentage(float volts, float *pcurve) {
  if ((volts / DC_GAIN) >= ZERO_POINT_VOLTAGE) {
    return pow(10, ((volts / DC_GAIN) - pcurve[1]) / pcurve[2] + pcurve[0]);
  } else {
    return pow(10, ((volts / DC_GAIN) - pcurve[1]) / pcurve[2] + pcurve[0]);
  }
}
void controlTemperature(double output) {
  if (output > 10) {
    digitalWrite(FAN_RELAY_PIN, HIGH);
  }
  if (output < 10 && output > 0) {
    analogWrite(FAN_RELAY_PIN, 128);
  } else {
    digitalWrite(FAN_RELAY_PIN, LOW);
  }
}

void controlO2(double output) {
  if (output < 0) {
    digitalWrite(O2_RELAY_PIN, HIGH);
  } else {
    digitalWrite(O2_RELAY_PIN, LOW);
  }
}

Your code looks pretty tight. Already using 'F'.
Is your LCD a shield? If so, it may be a bit more trouble than you think to switch to a Mega.

Hello Tim, the LCD is a breakout board (the 2.8 inch version of the one included in the link: Overview | Adafruit 2.8" and 3.2" Color TFT Touchscreen Breakout v2 | Adafruit Learning System). I would preferrably not want to switch to a Mega, because I onl need to lower the size of the code by 600 bytes which seems doable.

Don't know if this will be enough, but after this, you don't need to keep setting the text color. Just set cursor and print.

  tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
  tft.setTextSize(1);

You also need more RAM, you are using 90%. Usually anything over 75% will cause unpredictable behavior.

You can eliminate all those black-on-black calls. These:

    tft.setTextColor(ILI9341_BLACK, ILI9341_BLACK);
    tft.setCursor(tft.width() - 50, 20);
    tft.print(temperature);

Are you using this to erase? Really no need. You already have the background on the white prints set to to black rather than transparent. I use a fixed width font and a set length for the variables. It will overwrite (erase) the old text.

Its possible to modify the font file for the display library to remove all the characters that are not used.

My code uses a different library. Try this code:

#include <Adafruit_GFX.h>
#include <MCUFRIEND_kbv.h>

MCUFRIEND_kbv tft;

#define BLACK   0x0000
#define WHITE   0xFFFF

void setup() {
  Serial.begin(38400);

  uint16_t ID = tft.readID();
  Serial.print("TFT ID = 0x");
  Serial.println(ID, HEX);
//  Serial.println("Calibrate for your Touch Panel");
  if (ID == 0xD3D3) ID = 0x9486; // write-only shield
  tft.begin(ID);
  tft.setRotation(0);            //PORTRAIT
  tft.fillScreen(BLACK);

  tft.setCursor(30, 30);
  tft.setTextColor(WHITE,BLACK);
  tft.setTextSize(2);
  tft.print("Hello World!");
}

byte loopCount = 0;
char cBuf[8];

void loop() {
  tft.setCursor(30, 90);
  sprintf(cBuf,"%3d",loopCount);
  tft.print(cBuf);
  loopCount++;
  delay(500);
}

Can anyone confirm the program memory usage for the original code? I'm getting 28058 bytes instead of 32842 bytes, using Arduino IDE version 1.8.19 and the following versions of the libraries:

Using library SPI at version 1.0
Using library Adafruit_GFX_Library at version 1.11.11
Using library Adafruit_BusIO at version 1.16.2
Using library Wire at version 1.0
Using library Adafruit_ILI9341 at version 1.6.1
Using library Adafruit_Unified_Sensor at version 1.1.14
Using library Adafruit_BME280_Library at version 2.2.4
Using library DFRobot_OxygenSensor at version 1.0.1
Using library PID at version 1.2.0
Using library SdFat at version 2.2.3