Advanced Motorized Turntable Controller

HARDWARE REQUIRED:

  • PICUNO Microcontroller board
  • 1 × 4x4 Button Matrix Module
  • 1 × 5V Stepper Motor
  • 1 × ULN2003 Driver Board
  • 1 × 4xAA Battery Pack (For external supply)
  • Jumper wires
  • USB cable

DESCRIPTION:

This project creates an advanced controller for a motorized turntable. Using the 4x4 keypad, you can command the stepper motor to perform precise actions. You can enter a specific mode to move the motor an exact number of degrees, toggle continuous rotation, change direction, and return the motor to its home position. The Thonny IDE's Shell or Serial Monitor provides real-time feedback on the current mode, direction, position, and other statuses.

For C / Arduino IDE:
No external libraries required.

For Micropython / Thonny IDE:
stepper.py: The library file for the ULN2003 Stepper Motor driver, saved to the PICUNO board.

CIRCUIT DIAGRAM:

Advanced Motorized Turntable Controller
  • ULN2003 Driver Board:
    • Connect IN1, IN2, IN3, IN4 to PICUNO pins GPIO 15, 16, 18, and 19 respectively.
    • Connect the driver's + and GND pins to positive terminal of the 4xAA Battery pack and GND respectively.
    • Plug the stepper motor's connector into the socket on the driver board.
    • Connect the negative terminal of the 4xAA Battery Pack to Common GND on breadboard.
  • 4x4 Button Matrix Module:
    • Connect the Row pins (5-8) to GPIO 6, 7, 8 and 9 respectively.
    • Connect the Column pins (1-4) to GPIO 10, 11, 12 and 13 respectively.

    SCHEMATIC:

    ULN2003 Driver & Power:

    ULN2003 IN1 → PICUNO GPIO 15

    ULN2003 IN2 → PICUNO GPIO 16

    ULN2003 IN3 → PICUNO GPIO 18

    ULN2003 IN4 → PICUNO GPIO 19

    ULN2003 Power + Pin → 4xAA Battery Pack (+)

    ULN2003 Ground - Pin → GND

    4x4 Button Matrix Module:

    Pin 1 (Column 4) → GPIO 13

    Pin 2 (Column 3) → GPIO 12

    Pin 3 (Column 2) → GPIO 11

    Pin 4 (Column 1) → GPIO 10

    Pin 5 (Row 1) → GPIO 9

    Pin 6 (Row 2) → GPIO 8

    Pin 7 (Row 3) → GPIO 7

    Pin 8 (Row 4) → GPIO 6

    Common Ground Connection:

    4xAA Battery Pack (-) → PICUNO Board GND

    CODE -- C:

    const int IN1 = 15;
    const int IN2 = 16;
    const int IN3 = 18;
    const int IN4 = 19;

    const byte ROWS = 4;
    const byte COLS = 4;
    byte rowPins[ROWS] = {6, 7, 8, 9};
    byte colPins[COLS] = {10, 11, 12, 13};

    char keys[ROWS][COLS] = {
      {'1','2','3','A'},
      {'4','5','6','B'},
      {'7','8','9','C'},
      {'*','0','#','D'}
    };

    const int stepsPerRevolution = 4096;
    const int step_sequence[8][4] = {
      {1, 0, 0, 1},
      {1, 0, 0, 0},
      {1, 1, 0, 0},
      {0, 1, 0, 0},
      {0, 1, 1, 0},
      {0, 0, 1, 0},
      {0, 0, 1, 1},
      {0, 0, 0, 1}
    };
    int motor_pins[4] = {IN1, IN2, IN3, IN4};
    int current_motor_step = 0;

    // --- State Machine & Global Variables ---
    enum Mode { MODE_IDLE, MODE_ANGLE };
    Mode currentMode = MODE_IDLE;
    String inputBuffer = "";
    bool isRotating = false;
    int direction = 1;
    long currentPosition_steps = 0;
    unsigned long lastStepTime = 0;
    const int stepDelay = 2;

    // --- Helper Functions ---
    void stepMotor(int dir) {
      current_motor_step += dir;
      if (current_motor_step > 7) {
        current_motor_step = 0;
      }
      if (current_motor_step < 0) {
        current_motor_step = 7;
      }

      for (int i = 0; i < 4; i++) {
        digitalWrite(motor_pins[i], step_sequence[current_motor_step][i]);
      }
    }

    void printStatus() {
      String modeStr = (currentMode == MODE_ANGLE) ? "Angle" : "Direct";
      String dirStr = (direction == 1) ? "CW" : "CCW";
      String rotStr = (isRotating) ? "ON" : "OFF";
      
      Serial.print("Mode: " + modeStr);
      Serial.print(" | Dir: " + dirStr);
      Serial.print(" | Rotate: " + rotStr);
      Serial.print(" | Pos: ");
      Serial.print(currentPosition_steps);
      Serial.print(" | Input: [");
      Serial.print(inputBuffer);
      Serial.println("]");
    }

    void executeAngleCommand() {
      if (inputBuffer.length() > 0) {
        long degreesToMove = inputBuffer.toInt();
        int stepsToMove = map(degreesToMove, 0, 360, 0, stepsPerRevolution);
        Serial.print("--> Moving ");
        Serial.print(degreesToMove);
        Serial.println(" degrees...");
        
        for (int i = 0; i < stepsToMove; i++) {
          stepMotor(direction);
          delay(stepDelay);
        }
        currentPosition_steps += (direction * stepsToMove);
        
        Serial.println("--> Motion complete.");
      }
    }

    void returnToHome() {
      Serial.print("--> Returning to Home from position ");
      Serial.print(currentPosition_steps);
      Serial.println("...");

      int homeDirection = (currentPosition_steps > 0) ? -1 : 1;
      long stepsToHome = abs(currentPosition_steps);

      for (long i = 0; i < stepsToHome; i++) {
        stepMotor(homeDirection);
        delay(stepDelay);
      }
      currentPosition_steps = 0;
      Serial.println("--> At Home position.");
    }

    char scanKeypad() {
      for (byte r = 0; r < ROWS; r++) {
        digitalWrite(rowPins[r], LOW);
        for (byte c = 0; c < COLS; c++) {
          if (digitalRead(colPins[c]) == LOW) {
            digitalWrite(rowPins[r], HIGH);
            return keys[r][c];
          }
        }
        digitalWrite(rowPins[r], HIGH);
      }
      return '\\0';
    }

    void handleKeyPress(char key) {
      Serial.print("Key Pressed: '");
      Serial.print(key);
      Serial.println("'");
      if (isDigit(key) && currentMode == MODE_ANGLE) {
        inputBuffer += key;
      } else {
        switch (key) {
          case 'A':
            currentMode = MODE_ANGLE; isRotating = false; inputBuffer = "";
            break;
          case 'B':
            returnToHome();
            break;
          case 'C':
            direction *= -1;
            break;
          case 'D':
            isRotating = !isRotating;
            if (isRotating) { currentMode = MODE_IDLE; inputBuffer = ""; }
            break;
          case '*':
            inputBuffer = "";
            break;
          case '#':
            if (currentMode == MODE_ANGLE) {
              executeAngleCommand();
              inputBuffer = "";
              currentMode = MODE_IDLE;
            }
            break;
        }
      }
      printStatus();
    }

    // --- Main Program ---
    void setup() {
      Serial.begin(9600);
      while (!Serial);
      
      // Configure Keypad Pins
      for (byte r = 0; r < ROWS; r++) {
        pinMode(rowPins[r], OUTPUT);
        digitalWrite(rowPins[r], HIGH);
      }
      for (byte c = 0; c < COLS; c++) {
        pinMode(colPins[c], INPUT_PULLUP);
      }
      
      for (int i = 0; i < 4; i++) {
        pinMode(motor_pins[i], OUTPUT);
      }

      Serial.println("--- Stepper Controller Ready ---");
      printStatus();
    }

    char lastKeyState = '\\0';
    unsigned long lastKeyPressTime = 0;
    const long debounceDelay = 200;

    void loop() {
      if (isRotating && (millis() - lastStepTime >= stepDelay)) {
        stepMotor(direction);
        currentPosition_steps += direction;
        lastStepTime = millis();
      }

      char currentKeyState = scanKeypad();
      if (currentKeyState != lastKeyState) {
        if (millis() - lastKeyPressTime > debounceDelay) {
          if (currentKeyState != '\\0') {
            handleKeyPress(currentKeyState);
          }
          lastKeyPressTime = millis();
        }
      }
      lastKeyState = currentKeyState;
    }
    step_sequence array - This 2D array holds the 8 specific ON/OFF patterns for the motor's four coils. Cycling through these patterns is what creates smooth rotation.

    stepMotor() function - It advances one step in the sequence and directly sets the motor pins HIGH or LOW.

    scanKeypad() function - This function manually scans the keypad's rows and columns to find which button is currently pressed.

    pinMode(..., INPUT_PULLUP) - This command is essential for the manual scan. It activates an internal resistor in the Arduino, keeping the column pins HIGH until a pressed button pulls them LOW.

    Non-Blocking Timers (millis()) - The code uses two separate timers based on millis(). One creates the steady pulse for continuous motor rotation, and the other handles the delay between key presses. This allows the motor to spin while the keypad is still responsive.

    currentPosition_steps variable - This variable acts as a counter or "memory," tracking the motor's exact position relative to its starting point.

    returnToHome() function - This function uses the currentPosition_steps variable to calculate the exact number of reverse steps needed to return the motor precisely to its zero position.

    CODE -- PYTHON:

    from machine import Pin, PWM
    from time import sleep_ms, ticks_ms, ticks_diff
    from stepper import Stepper

    motor = Stepper(15, 16, 18, 19)
    steps_per_revolution = 4096

    keypad_map = [['1','2','3','A'],['4','5','6','B'],['7','8','9','C'],['*','0','#','D']]
    row_pins = [Pin(pin, Pin.OUT) for pin in [6, 7, 8, 9]]
    col_pins = [Pin(pin, Pin.IN, Pin.PULL_DOWN) for pin in [10, 11, 12, 13]]

    is_in_angle_mode = False
    input_buffer = ""
    is_rotating = False
    direction = 1
    currentPosition_steps = 0

    # --- Timer variables ---
    last_key_press_time = 0
    last_step_time = 0
    DEBOUNCE_DELAY_MS = 200
    STEP_DELAY_MS = 2

    # --- Helper Functions ---
    def scan_keypad():
        for r, row_pin in enumerate(row_pins):
            row_pin.high()
            for c, col_pin in enumerate(col_pins):
                if col_pin.value() == 1:
                    row_pin.low()
                    return keypad_map[r][c]
            row_pin.low()
        return None

    def execute_angle_command():
        global currentPosition_steps
        if input_buffer:
            degrees_to_move = int(input_buffer)
            steps_to_move = int((degrees_to_move / 360) * steps_per_revolution)
            print(f"--> Moving {degrees_to_move} degrees...")
            motor.step(steps_to_move, direction)
            currentPosition_steps += (steps_to_move * direction)
            print("--> Motion complete.")

    def return_to_home():
        global currentPosition_steps
        print(f"--> Returning to Home from position {currentPosition_steps}...")
        motor.step(-currentPosition_steps)
        currentPosition_steps = 0
        print("--> At Home position.")

    def print_status():
        mode_str = "Angle" if is_in_angle_mode else "Direct"
        dir_str = "CW" if direction == 1 else "CCW"
        rot_str = "ON" if is_rotating else "OFF"
        print(f"Mode: {mode_str} | Dir: {dir_str} | Rotate: {rot_str} | Pos: {currentPosition_steps} | Input: [{input_buffer}]")

    # --- Main Program ---
    print("--- Stepper Controller Ready ---")
    print_status()

    last_key_state = None

    while True:
        if is_rotating and ticks_diff(ticks_ms(), last_step_time) >= STEP_DELAY_MS:
            motor.step(1, direction)
            currentPosition_steps += direction
            last_step_time = ticks_ms()

        current_key = scan_keypad()
        
        if current_key and current_key != last_key_state:
            if ticks_diff(ticks_ms(), last_key_press_time) > DEBOUNCE_DELAY_MS:
                last_key_press_time = ticks_ms()
                key = current_key
                print(f"Key Pressed: '{key}'")

                if key.isdigit() and is_in_angle_mode:
                    input_buffer += key
                else:
                    if key == 'A':
                        is_in_angle_mode = True; is_rotating = False; input_buffer = ""
                    elif key == 'B':
                        return_to_home()
                    elif key == 'C':
                        direction *= -1
                    elif key == 'D':
                        is_in_angle_mode = False
                        is_rotating = not is_rotating
                    elif key == '*':
                        input_buffer = ""
                    elif key == '#':
                        if is_in_angle_mode:
                            execute_angle_command()
                            input_buffer = ""; is_in_angle_mode = False
                
                print_status()
        
        last_key_state = current_key
    stepper.py library - A custom library used to simplify the 8-step sequence required to make the stepper motor turn.

    scan_keypad() function - A non-blocking function that quickly scans the keypad for a press without pausing the program, which is essential for the continuous rotation to work smoothly.

    is_in_angle_mode - A boolean variable that acts as a "state" for our program. It tracks whether the controller should be accepting numbers for an angle command or just listening for direct commands.

    currentPosition_steps - A variable that acts as the motor's memory. It is updated every time the motor moves to keep a precise count of its current position relative to its start.

    return_to_home() - The function for the 'B' button. It uses the currentPosition_steps variable to calculate the exact reverse motion needed to get back to the starting point.

    Non-Blocking Timers (ticks_ms) - This logic is the key to the whole project. It allows the main loop to run very fast, handling both the perfectly-timed pulses for continuous rotation and instantly checking for keypad presses without either task interfering with the other.