En el artículo Motor paso a paso 28BYJ-48 con Arduino y driver ULN2003 (luisllamas.es) se puede encontrar información sobre los conceptos teóricos de la programación de este motor, y de los distintos tipos de programación que se pueden hacer (SECUENCIA 1-FASE, SECUENCIA 2-FASES, SECUENCIA MEDIO PASOS).

En el siguiente código se aprovecha el bucle infinito que realiza la función loop para ir enviando consecutivamente a los motores cada una de las secuencias de los pasos del motor. Para llevar el control de qué número de secuencia debe enviarse en cada momento, se ha creado la variable pasoActualMotor que empezará tomando el valor 0, se irá incrementando en cada vuelta del loop, y al llegar al valor máximo se le volverá a asignar el valor 0 para empezar de nuevo la secuencia.

// Pines de conexión del motor
const int IN1 = 2;
const int IN2 = 3;
const int IN3 = 4;
const int IN4 = 5;

// Secuencias de los pasos del motor
const int PASO[8][4] = {
// A  B  A' B'
  {1, 0, 0, 0}, // Paso 0
  {1, 1, 0, 0}, // Paso 1
  {0, 1, 0, 0}, // Paso 2
  {0, 1, 1, 0}, // Paso 3
  {0, 0, 1, 0}, // Paso 4  
  {0, 0, 1, 1}, // Paso 5  
  {0, 0, 0, 1}, // Paso 6  
  {1, 0, 0, 1}  // Paso 7
};

// Indicador del paso (0 a 3) actual del motor
int pasoActualMotor = 0;
// Microsegundos de espera entre paso y paso del motor
int retardoMotor = 1000;

void setup() {
  // Todos los pines en modo salida
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(IN3, OUTPUT);
  pinMode(IN4, OUTPUT);
}

void loop() {
  // Enviar al motor la secuencia actual de órdenes 
  digitalWrite(IN1, PASO[pasoActualMotor][0]);
  digitalWrite(IN2, PASO[pasoActualMotor][1]);
  digitalWrite(IN3, PASO[pasoActualMotor][2]);
  digitalWrite(IN4, PASO[pasoActualMotor][3]);
  // Breve espera para que tenga efecto el giro en el motor
  delayMicroseconds(retardoMotor);

  // Preparar paso a la siguiente secuencia
  pasoActualMotor++;
  if(pasoActualMotor > 7) {
    pasoActualMotor = 0;
  }
}

Para poder tener control sobre el sentido de giro del motor (sentido de las agujas del rejol, o al contrario ellas), se deben recorrer las filas de la matriz de la secuencia en orden ascendente (como en el ejemplo anterior) o descendente para girar el motor en el otro sentido.

En el siguiente código se emplea la variable sentidoGiroMotor para determinar el sentido de giro del motor. Para facilitar la lectura del código se han creado 2 constantes con los posible valores que se pueden asignar a dicha variable (SENTIDO_RELOJSENTIDO_CONTRARIO). Según el valor que tome la variable de sentido de giro, se incrementará o se decrementará la variable pasoActualMotor que determina qué secuencia debe enviarse en cada momento al motor. Ya que esa variable ahora puede que llegue a tomar un valor inferior a 0 (en caso de se esté decrementando sucesivamente), se controla que cuando llegue ese caso se le asigne de nuevo el valor más alto de la secuencia para empezar de nuevo la secuencia en orden decreciente.

Para poder probar que el funcionamiento es correcto en un sentido y en otro, cambia el valor de la variable sentidoGiroMotorSENTIDO_RELOJ o SENTIDO_CONTRARIO según el sentido de giro a probar.

// Pines de conexión del motor
const int IN1 = 2;
const int IN2 = 3;
const int IN3 = 4;
const int IN4 = 5;

// Secuencias de los pasos del motor
const int PASO[8][4] = {
// A  B  A' B'
  {1, 0, 0, 0}, // Paso 0
  {1, 1, 0, 0}, // Paso 1
  {0, 1, 0, 0}, // Paso 2
  {0, 1, 1, 0}, // Paso 3
  {0, 0, 1, 0}, // Paso 4  
  {0, 0, 1, 1}, // Paso 5  
  {0, 0, 0, 1}, // Paso 6  
  {1, 0, 0, 1}  // Paso 7
};

// Indicador del paso (0 a 3) actual del motor
int pasoActualMotor = 0;
// Microsegundos de espera entre paso y paso del motor
int retardoMotor = 1000;

// Posible valores para sentido de giro del motor
const char SENTIDO_RELOJ = 'R';
const char SENTIDO_CONTRARIO = 'C';

// Sentido de giro actual del motor
int sentidoGiroMotor = SENTIDO_RELOJ;

void setup() {
  // Todos los pines en modo salida
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(IN3, OUTPUT);
  pinMode(IN4, OUTPUT);
}

void loop() {
  // Enviar al motor la secuencia actual de órdenes 
  digitalWrite(IN1, PASO[pasoActualMotor][0]);
  digitalWrite(IN2, PASO[pasoActualMotor][1]);
  digitalWrite(IN3, PASO[pasoActualMotor][2]);
  digitalWrite(IN4, PASO[pasoActualMotor][3]); 
  // Breve espera para que tenga efecto el giro en el motor
  delayMicroseconds(retardoMotor);
  
  // Preparar paso a la siguiente secuencia
  if(sentidoGiroMotor == SENTIDO_RELOJ) {
    pasoActualMotor++;
  } else if(sentidoGiroMotor == SENTIDO_CONTRARIO) {
    pasoActualMotor--;
  }
  // Controlar los valores límites de la secuencia
  if(pasoActualMotor > 7) {
    pasoActualMotor = 0;
  } else if(pasoActualMotor < 0) {
    pasoActualMotor = 7;
  }
}

Para conseguir una vuelta completa del eje del motor hay que tener en cuenta que este modelo debe realizar 4096 pasos para completar un giro completo. Por tanto, es conveniente declarar una constante que almacene ese valor. 

const int PASOS_POR_VUELTA = 4096;

Debemos crear una variable que almacene el número de pasos que se desea realizar sobre el motor, con el fin de que podamos parar su movimiento o realizar cualquier otra acción cuando finalice esa cuenta del número de pasos. Se declarar de tipo unsigned long para poder almacenar un número elevado de pasos (hasta 4.294.967.295). Vamos a asignarle el mismo valor que la constante anterior para probar a dar una vuelta completa del motor.

unsigned long contadorPasosRestantes = PASOS_POR_VUELTA;

Ahora se debe implementar el código necesario para ir decrementando el contador contadorPasosRestantes, y que sólo se efectúe el movimiento del motor si ese contador es superior a cero.

void loop() {
  if(contadorPasosRestantes > 0) {
    // Enviar al motor la secuencia actual de órdenes 
    digitalWrite(IN1, PASO[pasoActualMotor][0]);
    digitalWrite(IN2, PASO[pasoActualMotor][1]);
    digitalWrite(IN3, PASO[pasoActualMotor][2]);
    digitalWrite(IN4, PASO[pasoActualMotor][3]); 
    // Breve espera para que tenga efecto el giro en el motor
    delayMicroseconds(retardoMotor);

    // Preparar paso a la siguiente secuencia
    if(sentidoGiroMotor == SENTIDO_RELOJ) {
      pasoActualMotor++;
    } else if(sentidoGiroMotor == SENTIDO_CONTRARIO) {
      pasoActualMotor--;
    }
    // Controlar los valores límites de la secuencia
    if(pasoActualMotor > 7) {
      pasoActualMotor = 0;
    } else if(pasoActualMotor < 0) {
      pasoActualMotor = 7;
    }  
      
    contadorPasosRestantes--;
  }
}

Una vez conseguido el movimiento de una vuelta completa, podemos añadir el código necesario para que realice una vuelta en un sentido, luego otra vuelta en el sentido contrario, y así sucesivamente. Cuando el contador alcance el valor cero, se le vuelve a asignar el número de pasos correspondiente a una vuelta completa, y se cambia el sentido de giro del motor.

  if(contadorPasosRestantes > 0) {
    // ...
  } else {
    contadorPasosRestantes = PASOS_POR_VUELTA;
    if(sentidoGiroMotor == SENTIDO_RELOJ) {
      sentidoGiroMotor = SENTIDO_CONTRARIO;
    } else {
      sentidoGiroMotor = SENTIDO_RELOJ;
    }
  }