◄ PAGE 09 PAGE 11 ►
WEEK #10

OUTPUT DEVICES

Giving movement to the goal and data to the screen: Servos, PWM, and Native I2C OLEDs.

ALL ACCESS MISSION FILES (GITLAB REPO)

MISSION BRIEFING

This week's objective: Add an output device to a microcontroller board and program it to do something. I tackled two different communication protocols: PWM to control the angle of a Servomotor dynamically via Serial Monitor, and I2C to program an OLED screen natively from scratch.

EVALUATOR NOTE: THE DC MOTOR & H-BRIDGE MYSTERY

Addressing the feedback: "Why are you using your board for a servo if it's an H-bridge?" and "You promised a DC motor and I didn't see one."

Since the Batman PCB was already manufactured, I completely discarded the DC motor idea and left the H-Bridge inactive (a vestige of the old design). Instead, I used the exposed GPIO pins on the board to fulfill this week's requirements by testing PWM communication with a Servo and I2C with an OLED. The servo was strictly a test actuator for Week 10 learning purposes, not a component for my final project!

00. GROUP ASSIGNMENT

What I learned from the group assignment: We probed the analog levels and digital signals of an output device using an oscilloscope and a multimeter. My main takeaway was visualizing the difference between a constant voltage reading on a multimeter versus seeing the actual PWM square waves and voltage spikes on the oscilloscope when a motor is running. This taught me why calculating power limits is so critical before connecting outputs.

📂 OPEN GROUP ASSIGNMENT

01. WIRING & POWER CALCULATIONS

Before running the actuators, we must mathematically prove that the system can handle the current draw (Power = Voltage * Current):

  • OLED Display (I2C): Consumes ~20mA at 3.3V.
  • Servo Motor (PWM): Draws ~500mA during movement at 5V (can spike up to 1A if physically stalled).
  • Total Estimated Draw: ~520mA.

*Conclusion: A standard USB port provides exactly 500mA. While this is enough for empty-load testing on a desk, adding physical resistance to the servo in the final goal will require an external battery pack to prevent the microcontroller from browning out and resetting.*

System Wiring Diagram
Block diagram mapping the precise pin routing to the OLED (I2C) and Servo (PWM).

PART 1: THE SERVOMOTOR (PWM)

WIRING THE ACTUATOR

THE TARGET ACTUATOR

This is the Servo that I used for testing. It is an SG90, a common micro servo that can rotate approximately 180 degrees. It receives PWM signals from the microcontroller to control the angle of the servo horn.



Connecting the SG90 Servo to the Batman PCB is straightforward once you bypass the H-Bridge. The servo uses three wires, which I connected directly to the exposed GPIO headers on my board:

  • Brown Wire (GND): Connected to the common Ground of the board.
  • Red Wire (VCC): Connected to the 5V power supply to ensure the motor has enough torque without browning out the logic circuit.
  • Orange Wire (Signal): Connected directly to Pin D9 on the microcontroller. This pin sends the PWM (Pulse Width Modulation) signal that tells the servo what angle to hold.
Servo wiring to Batman PCB
Direct PWM connection from the GPIO header to the SG90 Servo.

To make the output interactive, I used the Serial Monitor to control the SG90 servomotor dynamically. The code reads the exact angles (from 0 to 180) that I type on the computer and sends the corresponding PWM signal to move the servo horn, demonstrating successful hardware-software integration.

Hero Test: Controlling the servo target dynamically via Serial Monitor!
// SERVOMOTOR SERIAL CONTROL (ARDUINO IDE)

Code Explanation: This code waits for user input over the Serial Monitor. It uses Serial.parseInt() to extract the integer value typed by the user. If the value is within the safe mechanical range (0 to 180 degrees), it uses the Servo library to translate that angle into a PWM duty cycle, moving the motor to the desired position.

#include <Servo.h>

Servo miServo;  

const int pinSenal = D9; 

void setup() {
  Serial.begin(9600);
  miServo.attach(pinSenal);
  miServo.write(0); // Posición inicial
  delay(2000); 
  
  Serial.println("========================================");
  Serial.println("READY TO WORK  ");
  Serial.println("Write a number from 0 to 180");
}

void loop() {
  if (Serial.available() > 0) {
    int angulo = Serial.parseInt(); // Lee el número ingresado
    
    // Limpiamos la basura del buffer
    while (Serial.available() > 0) {
      Serial.read();
    }

    // Filtro de seguridad mecánica
    if (angulo >= 0 && angulo <= 180) {
      Serial.print("-> MOVING THE SERVO TO: ");
      Serial.println(angulo);
      
      // Ejecutamos el movimiento PWM
      miServo.write(angulo);
      
    } else {
      Serial.println("-> ¡Error! THE MECHANISM ONLY ACCEPTS NUMBERS FROM 0 TO 180");
    }
  }
}

PART 2: OLED DISPLAY (I2C)

For my second output device, I decided to drive an SH1106 1.3" OLED display natively. The screen communicates via the I2C protocol. I wired the SDA and SCL pins to the microcontroller and wrote a native C driver using the Pico SDK to draw the text pixel by pixel.

THE TARGET ACTUATOR

This is the SH1106 1.3" OLED display that I drove natively using the Pico SDK.

THE FINAL RENDER

Hero shot of the OLED Display
Hero Test: The OLED display successfully running the native C code!
// NATIVE OLED DRIVER (PICO C/C++ SDK)

Code Explanation: This is raw C code. It initializes the hardware I2C bus at 400kHz. The most critical part is the oled_set_cursor function: The SH1106 chip has RAM for 132 pixels, but my physical screen only has 128. I wrote a manual software patch (col += 2;) to shift the cursor 2 pixels to the right, perfectly centering the image on the glass. The oled_write_char function reads my custom 5x7 HEX font array and sends the bytes to the display's RAM.

#include <stdio.h>
#include <string.h>
#include "pico/stdlib.h"
#include "hardware/i2c.h"

// Hardware Config
#define I2C_PORT i2c1
#define I2C_SDA 6
#define I2C_SCL 7
#define OLED_ADDR 0x3C

// ==========================================
// SH1106 NATIVE CONTROLLER
// ==========================================
void oled_cmd(uint8_t cmd) {
    uint8_t buf[2] = {0x00, cmd}; // 0x00 indicates Command
    i2c_write_blocking(I2C_PORT, OLED_ADDR, buf, 2, false);
}

void oled_init() {
    uint8_t cmds[] = {
        0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40,
        0x8D, 0x14, 0x20, 0x00, 0xA1, 0xC8, 0xDA, 0x12,
        0x81, 0xCF, 0xD9, 0xF1, 0xDB, 0x40, 0xA4, 0xA6, 0xAF
    };
    for (int i = 0; i < sizeof(cmds); i++) oled_cmd(cmds[i]);
}

void oled_set_cursor(uint8_t page, uint8_t col) {
    col += 2; // SH1106 Patch (Offset of 2 pixels)
    oled_cmd(0xB0 + page);
    oled_cmd(0x00 | (col & 0x0F));
    oled_cmd(0x10 | ((col >> 4) & 0x0F));
}

void oled_clear() {
    uint8_t vacio[128];
    memset(vacio, 0, 128);
    for (uint8_t page = 0; page < 8; page++) {
        oled_set_cursor(page, 0);
        uint8_t buf[129];
        buf[0] = 0x40; // 0x40 indicates Data
        memcpy(&buf[1], vacio, 128);
        i2c_write_blocking(I2C_PORT, OLED_ADDR, buf, 129, false);
    }
}

// ==========================================
// MICRO ASCII FONT 5x7 (Snippet)
// ==========================================
const uint8_t font_5x7[] = {
    0x00, 0x00, 0x00, 0x00, 0x00, // (Space, ASCII 32)
    0x00, 0x00, 0x5F, 0x00, 0x00, // !
    // ... [Font array omitted for brevity] ...
    0x00, 0x44, 0x7D, 0x40, 0x00, // i
    0x20, 0x40, 0x44, 0x3D, 0x00, // j
    0x38, 0x44, 0x44, 0x44, 0x38, // o
    0x1C, 0x20, 0x40, 0x20, 0x1C  // v
};

// ==========================================
// TEXT FUNCTIONS
// ==========================================
void oled_write_char(char c, uint8_t page, uint8_t col) {
    if (page > 7 || col > 122) return;
    int font_idx = (c - 32) * 5;
    if (font_idx < 0) font_idx = 0; 

    uint8_t buf[7]; 
    buf[0] = 0x40;  
    
    for (int i=0; i<5; i++) {
        buf[i+1] = font_5x7[font_idx + i];
    }
    buf[6] = 0x00; 

    oled_set_cursor(page, col);
    i2c_write_blocking(I2C_PORT, OLED_ADDR, buf, 7, false);
}

void oled_write_string(const char *str, uint8_t page, uint8_t col) {
    while (*str) {
        oled_write_char(*str, page, col);
        col += 6;
        str++;
        if (col > 122) {
            col = 0;
            page++;
            if (page > 7) break;
        }
    }
}

// ==========================================
// MAIN ROUTINE
// ==========================================
int main() {
    stdio_init_all();
    
    // Init I2C1 at 400kHz
    i2c_init(I2C_PORT, 400 * 1000);
    gpio_set_function(I2C_SDA, GPIO_FUNC_I2C);
    gpio_set_function(I2C_SCL, GPIO_FUNC_I2C);
    gpio_pull_up(I2C_SDA);
    gpio_pull_up(I2C_SCL);

    sleep_ms(500); 
    
    oled_init();    
    oled_clear();   
    
    // Tactical Mission: Write the message!
    oled_write_string("hello, i'm javi", 3, 19);

    while (true) {
        sleep_ms(1000); 
    }
    return 0;
}

MISSION ACCOMPLISHED

PWM tracking verified. I2C natively rendered. The goal's feedback system is online.