Skip to content

10. Output Devices

Assignments

Here you can find a recording of the lecture from the 26th of march.

This week's assignments and learning outcomes, see here:

Group assignment:

  • Measure the power consumption of an output device.
  • Document your work on the group work page and reflect on your individual page what you learned.

You can find the documentation for our group assignments here.

Individual assignment:

  • Add an output device to a microcontroller board you've designed and program it to do something.

Questions to be answered/from Nueval:

Have you answered these questions?

  • Linked to the group assignment page
  • Documented how you determined power consumption of an output device with your group.
  • Documented what you learned from interfacing output device(s) to microcontroller and controlling the device(s).
  • Linked to the board you made in a previous assignment or documented your design and fabrication process if you made a new board.
  • Explained the programming process/es you used.
  • Explained any problems you encountered and how you fixed them.
  • Included original source code and any new design files.
  • Included a ‘hero shot’ of your board.

Hero shot

Hero shot from group assignment

Motor

The power consumption of an output device measured.

Overview of measuring Power Consumption

Hero shot from individual assignment

Motor

A motor connected to Raspberry Pi Pico and programmed with MicroPython in Thonny to spin and stop.

Motor running

LCD screen and XIAO ESP32C3

A LCD screen connected to XIAO ESP32C3 through I2C and programmed with C++ in Arduino Ide, displaying Finally Hello world.

LCD screen and Raspberry Pi Pico

An LCD screen connected to Raspberry Pi Pico displaying Hello world, using I2C and programmed with MicroPython in Thonny.

Hello World

OLED screen and Raspberry Pi Pico

An OLED screen connected to Raspberry Pi Pico to displaying Hello world, usign I2C and programmed with MicroPython in Thonny.

Summary

In the group assignment we measured the power consumption of output devices. You can find the documentation for our group assignments here.

In the individual assignment I created a breakout board for N-Mosfet and used it to control a dc motor with the Raspberry Pi Pico board that I produced in week08.

I connected a LCD screen as an output device to a Raspberry Pi Pico and programmed it to display Hello world.

I connected a LCD screen as an output device to XIAO ESP323 and programmed it to display Finally Hello (and more).

I also connected an OLED screen to XIAO ESP323 and programmed it to display Hello world.

Work process detail

N-Mosfet breakoutboard and a spinning motor

N-Mosfet and motor

I followed the directions here at Fab Lab Ísafjörður's website when I made a N-Mosfet breakout board, connected it to Raspberry Pi Pico and used a motor that I programmed as an output device.

Here is a datasheet for MOSFET N-CH 30V 1.7A SUPERSOT3.

Raspberry Pi Pico board

I used a Raspberry Pi Pico board that I produced in week 08.

Datasheet and pinout for Raspberry Pi Pico

You can find information on Raspberry Pi Pico here. The screenshot below is from this information site.

Datasheet for Raspberry Pi Pico here.

Pinout Raspberry Pi Pico

Schematic for N-Mosfet board

Here you can see an image of the design in Kicad Schematic editor:

Schematic editor

Here is an image showing the design in Kicad PCB editor:

PCB editor

Code for controlling motor

Micropython code used in Thonny for running motor. By clicking on the play button it turns on the motor and if you click on the play button again, it stops the motor:

from machine import Pin
motor = machine.Pin(0, machine.Pin.OUT)

motor.toggle()

Holy smoke!

N-Mosfet board overheated

When I connected the N-Mosfet breakoutboard to the Raspberry Pi Pico and ran the code, the board overheated. This happened so quickly. I saw smoke coming from the board and tried to unconnect it as fast as I could. When I lifted the board, the N-Mosfet fell off!

Video of motor running and stopping

N-Mosfet board overheated

I made a new board when I went to Fab Lab Ísafjörður in Machine week. Then everything went well and here you can see a video of the motor turning on and off:

Motor running

Files for N-Mosfet breakout board

Kicad pcb file

Kicad prl file

Kicad pro file

Kicad sch file

Kicad outline

Kicad traces

Connecting an LCD to Raspberry Pi Pico

Raspberry Pi Pico board

I used a Raspberry Pi Pico board that I produced in week 08.

Datasheet and pinout for QAPASS LCD

Datasheet for Qapass LCD here.

Interface of 16x2 LCD

This webpage webpage explains well how the 16x2 LCD means that the screen has 16 columns and 2 rows, or total of 32 characters. Each character is made of 5x8 dots. It also shows an image of the backside of the Qapass with explanations on the I2C connection pins.

Directions

I followed these directions here and used the codes from that site, as described here below.

I connected the LCD screeen and the Raspberry Pi Pico as you can see in this table:

LCD Raspberry Pi Pico
SCL GPIO 5
SDA GPIO 4
VCC VBUS (5V)
GND GND

On the website it is explained that you need to know the address of I2C devices before programming them. This website was pointed out with directions on how to find the I2C address. It was mentioned that most likely the address for a LCD screen like the one I had was 0x27, but I decided to follow the directions and used this code:

# I2C Scanner MicroPython
from machine import Pin, SoftI2C

# You can choose any other combination of I2C pins
i2c = SoftI2C(scl=Pin(5), sda=Pin(4))

print('I2C SCANNER')
devices = i2c.scan()

if len(devices) == 0:
  print("No i2c device !")
else:
  print('i2c devices found:', len(devices))

  for device in devices:
    print("I2C hexadecimal address: ", hex(device))

This text appeared in the shell and I thought the I2C address was 0x77. Later I found out that this was not correct and the address was 0x27, just as was mentioned as a likely address. My guess is that the code was counting up to 77 without finding the address. Maybe the pins were connected the wrong way.

I2C address - or not

I copied this code to Thonny and saved it to the Raspberry Pi Pico as pico_i2c_lcd.py

# forked from https://github.com/T-622/RPI-PICO-I2C-LCD/
import time

class LcdApi:

    # Implements the API for talking with HD44780 compatible character LCDs.
    # This class only knows what commands to send to the LCD, and not how to get
    # them to the LCD.
    #
    # It is expected that a derived class will implement the hal_xxx functions.
    #
    # The following constant names were lifted from the avrlib lcd.h header file,
    # with bit numbers changed to bit masks.

    # HD44780 LCD controller command set
    LCD_CLR             = 0x01  # DB0: clear display
    LCD_HOME            = 0x02  # DB1: return to home position

    LCD_ENTRY_MODE      = 0x04  # DB2: set entry mode
    LCD_ENTRY_INC       = 0x02  # DB1: increment
    LCD_ENTRY_SHIFT     = 0x01  # DB0: shift

    LCD_ON_CTRL         = 0x08  # DB3: turn lcd/cursor on
    LCD_ON_DISPLAY      = 0x04  # DB2: turn display on
    LCD_ON_CURSOR       = 0x02  # DB1: turn cursor on
    LCD_ON_BLINK        = 0x01  # DB0: blinking cursor

    LCD_MOVE            = 0x10  # DB4: move cursor/display
    LCD_MOVE_DISP       = 0x08  # DB3: move display (0-> move cursor)
    LCD_MOVE_RIGHT      = 0x04  # DB2: move right (0-> left)

    LCD_FUNCTION        = 0x20  # DB5: function set
    LCD_FUNCTION_8BIT   = 0x10  # DB4: set 8BIT mode (0->4BIT mode)
    LCD_FUNCTION_2LINES = 0x08  # DB3: two lines (0->one line)
    LCD_FUNCTION_10DOTS = 0x04  # DB2: 5x10 font (0->5x7 font)
    LCD_FUNCTION_RESET  = 0x30  # See "Initializing by Instruction" section

    LCD_CGRAM           = 0x40  # DB6: set CG RAM address
    LCD_DDRAM           = 0x80  # DB7: set DD RAM address

    LCD_RS_CMD          = 0
    LCD_RS_DATA         = 1

    LCD_RW_WRITE        = 0
    LCD_RW_READ         = 1

    def __init__(self, num_lines, num_columns):
        self.num_lines = num_lines
        if self.num_lines > 4:
            self.num_lines = 4
        self.num_columns = num_columns
        if self.num_columns > 40:
            self.num_columns = 40
        self.cursor_x = 0
        self.cursor_y = 0
        self.implied_newline = False
        self.backlight = True
        self.display_off()
        self.backlight_on()
        self.clear()
        self.hal_write_command(self.LCD_ENTRY_MODE | self.LCD_ENTRY_INC)
        self.hide_cursor()
        self.display_on()

    def clear(self):
        # Clears the LCD display and moves the cursor to the top left corner
        self.hal_write_command(self.LCD_CLR)
        self.hal_write_command(self.LCD_HOME)
        self.cursor_x = 0
        self.cursor_y = 0

    def show_cursor(self):
        # Causes the cursor to be made visible
        self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY |
                               self.LCD_ON_CURSOR)

    def hide_cursor(self):
        # Causes the cursor to be hidden
        self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY)

    def blink_cursor_on(self):
        # Turns on the cursor, and makes it blink
        self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY |
                               self.LCD_ON_CURSOR | self.LCD_ON_BLINK)

    def blink_cursor_off(self):
        # Turns on the cursor, and makes it no blink (i.e. be solid)
        self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY |
                               self.LCD_ON_CURSOR)

    def display_on(self):
        # Turns on (i.e. unblanks) the LCD
        self.hal_write_command(self.LCD_ON_CTRL | self.LCD_ON_DISPLAY)

    def display_off(self):
        # Turns off (i.e. blanks) the LCD
        self.hal_write_command(self.LCD_ON_CTRL)

    def backlight_on(self):
        # Turns the backlight on.

        # This isn't really an LCD command, but some modules have backlight
        # controls, so this allows the hal to pass through the command.
        self.backlight = True
        self.hal_backlight_on()

    def backlight_off(self):
        # Turns the backlight off.

        # This isn't really an LCD command, but some modules have backlight
        # controls, so this allows the hal to pass through the command.
        self.backlight = False
        self.hal_backlight_off()

    def move_to(self, cursor_x, cursor_y):
        # Moves the cursor position to the indicated position. The cursor
        # position is zero based (i.e. cursor_x == 0 indicates first column).
        self.cursor_x = cursor_x
        self.cursor_y = cursor_y
        addr = cursor_x & 0x3f
        if cursor_y & 1:
            addr += 0x40    # Lines 1 & 3 add 0x40
        if cursor_y & 2:    # Lines 2 & 3 add number of columns
            addr += self.num_columns
        self.hal_write_command(self.LCD_DDRAM | addr)

    def putchar(self, char):
        # Writes the indicated character to the LCD at the current cursor
        # position, and advances the cursor by one position.
        if char == '\n':
            if self.implied_newline:
                # self.implied_newline means we advanced due to a wraparound,
                # so if we get a newline right after that we ignore it.
                pass
            else:
                self.cursor_x = self.num_columns
        else:
            self.hal_write_data(ord(char))
            self.cursor_x += 1
        if self.cursor_x >= self.num_columns:
            self.cursor_x = 0
            self.cursor_y += 1
            self.implied_newline = (char != '\n')
        if self.cursor_y >= self.num_lines:
            self.cursor_y = 0
        self.move_to(self.cursor_x, self.cursor_y)

    def putstr(self, string):
        # Write the indicated string to the LCD at the current cursor
        # position and advances the cursor position appropriately.
        for char in string:
            self.putchar(char)

    def custom_char(self, location, charmap):
        # Write a character to one of the 8 CGRAM locations, available
        # as chr(0) through chr(7).
        location &= 0x7
        self.hal_write_command(self.LCD_CGRAM | (location << 3))
        self.hal_sleep_us(40)
        for i in range(8):
            self.hal_write_data(charmap[i])
            self.hal_sleep_us(40)
        self.move_to(self.cursor_x, self.cursor_y)

    def hal_backlight_on(self):
        # Allows the hal layer to turn the backlight on.
        # If desired, a derived HAL class will implement this function.
        pass

    def hal_backlight_off(self):
        # Allows the hal layer to turn the backlight off.
        # If desired, a derived HAL class will implement this function.
        pass

    def hal_write_command(self, cmd):
        # Write a command to the LCD.
        # It is expected that a derived HAL class will implement this function.
        raise NotImplementedError

    def hal_write_data(self, data):
        # Write data to the LCD.
        # It is expected that a derived HAL class will implement this function.
        raise NotImplementedError

    def hal_sleep_us(self, usecs):
        # Sleep for some time (given in microseconds)
        time.sleep_us(usecs)

Then I copied this code and clicked on the small play button at the top in Thonny.

# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/raspberry-pi-pico-i2c-lcd-display-micropython/

from machine import Pin, SoftI2C
from pico_i2c_lcd import I2cLcd
from time import sleep

# Define the LCD I2C address and dimensions
I2C_ADDR = 0x27
I2C_NUM_ROWS = 2
I2C_NUM_COLS = 16

# Initialize I2C and LCD objects
i2c = SoftI2C(sda=Pin(4), scl=Pin(5), freq=400000)
lcd = I2cLcd(i2c, I2C_ADDR, I2C_NUM_ROWS, I2C_NUM_COLS)

lcd.putstr("It's working :)")
sleep(4)

try:
    while True:
        # Clear the LCD
        lcd.clear()
        # Display two different messages on different lines
        # By default, it will start at (0,0) if the display is empty
        lcd.putstr("Hello World!")
        sleep(2)
        lcd.clear()
        # Starting at the second line (0, 1)
        lcd.move_to(0, 1)
        lcd.putstr("Hello World!")
        sleep(2)

except KeyboardInterrupt:
    # Turn off the display
    print("Keyboard interrupt")
    lcd.backlight_off()
    lcd.display_off()

LCD not working

I was not very happy when this did not work. I thought about switching to another type of LCD, Lumex, but decided to continue with the QAPASS. My next attempt was to connect the LCD without the I2C connection, as you can see here below, but that did not work either. I used this tutorial here. I could see the pixels but no text was visible.

Pixels visible but no text

Connecting the LCD to XIAO ESP32C3

The next project was to connect the LCD to a XIAO ESP32C3 and use Arduino Ide to program it. I followed these instructions here.

Datasheet and pinout for XIAO ESP32C3

You can find information and datasheet for XIAO ESP32C3 here. The screenshot below is from this information site.

Datasheet XIAO ESP32C3

I connected the LCD and XIAO ESP32C3 as followed:

LCD XIAO ESP32C3
SCL GPIO 7 (also marked as SCL)
SDA GPIO 6 (also marked as SDA)
VCC VBUS (5V)
GND GND

Then I added the XIAO ESP32C3 in Arduino:

Adding XIAO ESP32C3 in Arduino

The next step was to install the Liquid Chrystal Library in Arduino:

alt text

Next thing to do was to find the I2C address for the LCD. I skipped this step and used 0x27 as the I2C address and it worked when I used this code here below. I used an example code under Liquid Chrystal Library in Arduino Ide, clicked on the checkmark (Verify) and then clicked on the arrow (Upload).

Liquid Crystal Library example

When it worked, I was so glad that I added the word "Finally" to the code, uploaded it and saved the code:

The code:

//YWROBOT
//Compatible with the Arduino IDE 1.0
//Library version:1.1
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27,20,4);  // set the LCD address to 0x27 for a 16 chars and 2 line display

void setup()
{
  lcd.init();                      // initialize the lcd 
  // Print a message to the LCD.
  lcd.backlight();
  lcd.setCursor(3,0);
  lcd.print("Finally hello");
  lcd.setCursor(2,1);
  lcd.print("Ywrobot Arduino!");
   lcd.setCursor(0,2);
  lcd.print("Arduino LCM IIC 2004");
   lcd.setCursor(2,3);
  lcd.print("Power By Ec-yuan!");
}


void loop()
{
}

Finally, hello!

Connecting an OLED to a Raspberry Pi Pico

Raspberry Pi Pico board

I used a Raspberry Pi Pico board that I produced in week 08.

Directions

I used the SSD1306 model of an OLED screen. It has 128x64 pixels. I followed these directions here

The I2C pins on the OLED screen are all well marked on the front of the screen. I connected the OLED and Raspberry Pi Pico as followed:

OLED Raspberry Pi Pico
SCL GPIO 5
SDA GPIO 4
VCC 3V3
GND GND

I began by adding the OLED library. First a created a new sketch in Thonny and copied this code into it:

# MicroPython SSD1306 OLED driver, I2C and SPI interfaces created by Adafruit

import time
import framebuf

# register definitions
SET_CONTRAST        = const(0x81)
SET_ENTIRE_ON       = const(0xa4)
SET_NORM_INV        = const(0xa6)
SET_DISP            = const(0xae)
SET_MEM_ADDR        = const(0x20)
SET_COL_ADDR        = const(0x21)
SET_PAGE_ADDR       = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP       = const(0xa0)
SET_MUX_RATIO       = const(0xa8)
SET_COM_OUT_DIR     = const(0xc0)
SET_DISP_OFFSET     = const(0xd3)
SET_COM_PIN_CFG     = const(0xda)
SET_DISP_CLK_DIV    = const(0xd5)
SET_PRECHARGE       = const(0xd9)
SET_VCOM_DESEL      = const(0xdb)
SET_CHARGE_PUMP     = const(0x8d)


class SSD1306:
    def __init__(self, width, height, external_vcc):
        self.width = width
        self.height = height
        self.external_vcc = external_vcc
        self.pages = self.height // 8
        # Note the subclass must initialize self.framebuf to a framebuffer.
        # This is necessary because the underlying data buffer is different
        # between I2C and SPI implementations (I2C needs an extra byte).
        self.poweron()
        self.init_display()

    def init_display(self):
        for cmd in (
            SET_DISP | 0x00, # off
            # address setting
            SET_MEM_ADDR, 0x00, # horizontal
            # resolution and layout
            SET_DISP_START_LINE | 0x00,
            SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
            SET_MUX_RATIO, self.height - 1,
            SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
            SET_DISP_OFFSET, 0x00,
            SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12,
            # timing and driving scheme
            SET_DISP_CLK_DIV, 0x80,
            SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1,
            SET_VCOM_DESEL, 0x30, # 0.83*Vcc
            # display
            SET_CONTRAST, 0xff, # maximum
            SET_ENTIRE_ON, # output follows RAM contents
            SET_NORM_INV, # not inverted
            # charge pump
            SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14,
            SET_DISP | 0x01): # on
            self.write_cmd(cmd)
        self.fill(0)
        self.show()

    def poweroff(self):
        self.write_cmd(SET_DISP | 0x00)

    def contrast(self, contrast):
        self.write_cmd(SET_CONTRAST)
        self.write_cmd(contrast)

    def invert(self, invert):
        self.write_cmd(SET_NORM_INV | (invert & 1))

    def show(self):
        x0 = 0
        x1 = self.width - 1
        if self.width == 64:
            # displays with width of 64 pixels are shifted by 32
            x0 += 32
            x1 += 32
        self.write_cmd(SET_COL_ADDR)
        self.write_cmd(x0)
        self.write_cmd(x1)
        self.write_cmd(SET_PAGE_ADDR)
        self.write_cmd(0)
        self.write_cmd(self.pages - 1)
        self.write_framebuf()

    def fill(self, col):
        self.framebuf.fill(col)

    def pixel(self, x, y, col):
        self.framebuf.pixel(x, y, col)

    def scroll(self, dx, dy):
        self.framebuf.scroll(dx, dy)

    def text(self, string, x, y, col=1):
        self.framebuf.text(string, x, y, col)


class SSD1306_I2C(SSD1306):
    def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False):
        self.i2c = i2c
        self.addr = addr
        self.temp = bytearray(2)
        # Add an extra byte to the data buffer to hold an I2C data/command byte
        # to use hardware-compatible I2C transactions.  A memoryview of the
        # buffer is used to mask this byte from the framebuffer operations
        # (without a major memory hit as memoryview doesn't copy to a separate
        # buffer).
        self.buffer = bytearray(((height // 8) * width) + 1)
        self.buffer[0] = 0x40  # Set first byte of data buffer to Co=0, D/C=1
        self.framebuf = framebuf.FrameBuffer1(memoryview(self.buffer)[1:], width, height)
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.temp[0] = 0x80 # Co=1, D/C#=0
        self.temp[1] = cmd
        self.i2c.writeto(self.addr, self.temp)

    def write_framebuf(self):
        # Blast out the frame buffer using a single I2C transaction to support
        # hardware I2C interfaces.
        self.i2c.writeto(self.addr, self.buffer)

    def poweron(self):
        pass


class SSD1306_SPI(SSD1306):
    def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
        self.rate = 10 * 1024 * 1024
        dc.init(dc.OUT, value=0)
        res.init(res.OUT, value=0)
        cs.init(cs.OUT, value=1)
        self.spi = spi
        self.dc = dc
        self.res = res
        self.cs = cs
        self.buffer = bytearray((height // 8) * width)
        self.framebuf = framebuf.FrameBuffer1(self.buffer, width, height)
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs.high()
        self.dc.low()
        self.cs.low()
        self.spi.write(bytearray([cmd]))
        self.cs.high()

    def write_framebuf(self):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs.high()
        self.dc.high()
        self.cs.low()
        self.spi.write(self.buffer)
        self.cs.high()

    def poweron(self):
        self.res.high()
        time.sleep_ms(1)
        self.res.low()
        time.sleep_ms(10)
        self.res.high()

Then I saved this sketch to the Raspberry Pi Pico under the name ssd1306.py.

The next step was to scan the I2C address of the OLED. I created a new sketch an copied this code into it:

# I2C Scanner MicroPython
from machine import Pin, SoftI2C

# You can choose any other combination of I2C pins
i2c = SoftI2C(scl=Pin(5), sda=Pin(4))

print('I2C SCANNER')
devices = i2c.scan()

if len(devices) == 0:
  print("No i2c device !")
else:
  print('i2c devices found:', len(devices))

  for device in devices:
    print("I2C hexadecimal address: ", hex(device))

When I clicked on the play button in Thonny, the I2C address was displayed. It was 0x3c, as you can see in the image below:

I2C address on OLED is 0x3c

Then I created a new sketch in Thonny and pasted this code into it:

# Complete project details at https://RandomNerdTutorials.com/raspberry-pi-pico-ssd1306-oled-micropython/

from machine import Pin, SoftI2C
import ssd1306

#You can choose any other combination of I2C pins
i2c = SoftI2C(scl=Pin(5), sda=Pin(4))

oled_width = 128
oled_height = 64
oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c)

oled.text('Hello, World 1!', 0, 0)
oled.text('Hello, World 2!', 0, 10)
oled.text('Hello, World 3!', 0, 20)

oled.show()
Then I clicked on the play button and the text "Hello, World 1, Hello World 2 and Hello World 3" was displayed on the OLED screen:

Hello, World

Connection overview

Learning outcome

Learning outcome from the group assignment

I learned how to measure the power consumption of an output device (a LCD screen). I learned how to calculate the power in Watts by using the V (Voltage) x I (Amper) = P (Power) formula. 5V x 24,2 mA = 121 mW.

Learning outcome from the individual assignment

I learned how to make a motor, an LCD device and OLED device work! I learned to use different interfaces, using three devices, two different microcontrollers and two ways to program the devices; MicroPython in Thonny and C++ in Arduino Ide.