Hola de nuevo. Perdona que no te haya contestado antes (he estado estos días sin ordenador portátil en casa, y el teclado del teléfono es un coñazo).
Retomando el tema, como te dije puedes tener dos tipos de problema de memoria. Una es la memoria flash, que es donde se guarda el programa (también hay posibilidad de guardar algunos datos usando progmem) y memoria ram, que es donde se guardan los datos temporales (variables) del programa. Pero te digo un par de cosillas, según voy revisando tu código, que yo suelo hacer cuando necesito meter un programa en corsés de espacio o velocidad (no son mejores ni peores técnicas, e incluso algunas pueden pecar contra la programación "purista", pero igual alguna te viene bien).
Una manera de ahorrar en memoria de programa y de datos (como te dijo Dabauza) es "medir" tus variables y utilizar el tipo más ajustado. Por ejemplo, todas las variables que usas para el reloj creo que pueden estar en un rango entre 0 y 256, así que podrías definirlas como byte.
Otro tema a tener en cuenta con las variables es su alcance y tiempo de vida. Si una variable sólo va a ser utilizada en una determinada función (o incluso en una parte concreta de una función delimitada por llaves) y no tiene que guardar su valor de una llamada a otra, lo lógico es declararla dentro de esas llaves, pues la memoria ram que utilizan se libera al salir. Por ejemplo variable global String dataString podrías declararla justamente donde la "llenas": String dataString = String(monthDay) + ... Incluso si una variable debe conservar su valor, pero sólo se va a utilizar dentro de una determinada función, yo suelo declararlas dentro de ella como static, y así no la tengo por ahí fuera expuesta. Por ejemplo, las variables estadoSd y estadoReloj sólo se usan dentro de loop, así que podrías declararlas e iniciarlas dentro de loop (static boolean estadoSd=false;). No te preocupes, que sólo se inicializarán la primera vez. Además te evitas dos líneas en setup.
En resumen, yo intento no abusar de variables globales y usarlas sólo si voy a necesitar compartirlas entre varias funciones. Cosa diferente son las constantes o definiciones, que sí se entienden mejor si están juntitas al principio del código.
La utilización de objetos String suele simplificar las cosas para según qué tipo de operaciones, pero en muchas ocasiones es más efectivo trabajar con cadenas de caracteres. Requiere un poco de conocimiento de ellas, pero creo que merece la pena.
En tu código, por ejemplo, podrías construir tu dataString con sprintf:
char dataString[100]; // guardo 100 bytes de memoria para escribir en ella con sprintf
sprintf(dataString, "%d/%d/%d , %d:%dx:%d , %d , %d , %d , %d , %d , %d , %d , %d , %d , %d",
monthDay, month, year, hour, minute, second, int(t), int(h), hSuelo, LDR,
sensorPozo, sensorBidon, salRiego, salMotor, Cc, Pmp);
Y bueno; por hoy dejo de darte la murga, que igual te estoy liando.
Saludos.