Examples

Getting started

This is the minimum part layout.

_images/sch_getting_started.png

Although this is not a recommended design, it should work fine. See the MCP2221 data sheet for more information.

Import EasyMCP2221 module and try to create a new Device object with default parameters.

>>> import EasyMCP2221
>>> mcp = EasyMCP2221.Device()
>>> print(mcp)
{
    "Chip settings": {
        "Power management options": "enabled",
        "USB PID": "0x00DD",
        "USB VID": "0x04D8",
        "USB requested number of mA": 100
    },
    "Factory Serial": "01234567",
    "GP settings": {},
    "USB Manufacturer": "Microchip Technology Inc.",
    "USB Product": "MCP2221 USB-I2C/UART Combo",
    "USB Serial": "0000000000"
}

In case of error, make sure MCP2221A is properly connected. Use Microchip’s tool to find the device. Also read the troubleshooting section in Install / troubleshooting.

Basic GPIO

Configure pin function using set_pin_function() to GPIO_IN or GPIO_OUT. Then use GPIO_write() to change its output. Or GPIO_read() to read the status.

Digital output: LED blinking

Same as before, but use GPIO_write() in a loop to change its output periodically.

Schematic:

_images/sch_led_blink.svg

Code:

# How to blink a LED connected to GP2
import EasyMCP2221
from time import sleep

# Connect to the device
mcp = EasyMCP2221.Device()

# Reclaim GP2 for General Purpose Input Output, as an Output.
mcp.set_pin_function(gp2 = "GPIO_OUT")

while True:
    mcp.GPIO_write(gp2 = True)
    sleep(0.5)
    mcp.GPIO_write(gp2 = False)
    sleep(0.5)

Result:

_images/brd_led_blink.gif

Digital input: Mirror state

In order to illustrate how to read from GPIO digital input, let’s setup GP2 and GP3 to mimic the state of GP0 and GP1.

# GPIO output and input.
# GP0 is an output, but GP3 will be an input.
# The state of GP3 will mirror GP0.
import EasyMCP2221
from time import sleep

# Connect to device
mcp = EasyMCP2221.Device()

# GP0 and GP1 are inputs, GP2 and GP3 are outputs.
mcp.set_pin_function(
    gp0 = "GPIO_OUT",
    gp3 = "GPIO_IN")

while True:
    inputs = mcp.GPIO_read()
    mcp.GPIO_write(
        gp0 = inputs[3])

Analog signals

ADC basics

In this example, we setup GP1, GP2 and GP3 as analog inputs using set_pin_function(). Configure ADC reference with ADC_config() and lastly, read ADC values using ADC_read().

It works better if you take off the LED and connect three potentiometers to the inputs.

Remember to always put a 330 ohm resistor right in series with any GP pin. That way, if you by mistake configured it as an output, the short circuit current won’t exceed the 20mA.

# ADC input
# MCP2221 have one 10bit ADC with three channels connected to GP1, GP2 and GP3.
# The ADC is always running.
import EasyMCP2221
from time import sleep

# Connect to device
mcp = EasyMCP2221.Device()

# Use GP1, GP2 and GP3 as analog input.
mcp.set_pin_function(gp1 = "ADC", gp2 = "ADC", gp3 = "ADC")

# Configure ADC reference
# Accepted values for ref are 'OFF', '1.024V', '2.048V', '4.096V' and 'VDD'.
mcp.ADC_config(ref="VDD")

# Read ADC values
# (adc values are always available regardless of pin function, even if output)
while True:
    values = mcp.ADC_read()

    print("ADC0: %4.1f%%    ADC1: %4.1f%%    ADC2: %4.1f%%" %
        (
        values[0] / 1024 * 100,
        values[1] / 1024 * 100,
        values[2] / 1024 * 100,
        ))

    sleep(0.1)

This is the console output when you move a variable resistor in GP3.

ADC0:  0.3%    ADC1:  0.2%    ADC2:  0.0%
ADC0:  0.3%    ADC1:  0.1%    ADC2:  0.0%
ADC0:  0.3%    ADC1:  0.2%    ADC2:  9.9%
ADC0:  0.2%    ADC1:  0.1%    ADC2: 21.7%
ADC0:  0.3%    ADC1:  0.3%    ADC2: 31.7%
ADC0:  0.2%    ADC1:  0.0%    ADC2: 38.2%
ADC0:  0.4%    ADC1:  0.3%    ADC2: 45.5%
ADC0:  0.2%    ADC1:  0.0%    ADC2: 52.3%
ADC0:  0.3%    ADC1:  0.3%    ADC2: 56.2%
ADC0:  0.1%    ADC1:  0.0%    ADC2: 58.8%
ADC0:  0.4%    ADC1:  0.2%    ADC2: 61.6%
ADC0:  0.1%    ADC1:  0.0%    ADC2: 64.6%
ADC0:  0.3%    ADC1:  0.2%    ADC2: 67.1%
ADC0:  0.2%    ADC1:  0.2%    ADC2: 70.4%
ADC0:  0.3%    ADC1:  0.1%    ADC2: 74.5%
ADC0:  0.2%    ADC1:  0.1%    ADC2: 79.2%
ADC0:  0.2%    ADC1:  0.1%    ADC2: 80.6%

Mixed signal: level meter

We will use the analog level in GP3 to set the state or three leds connected to GP0, GP1 and GP2.

# This could be a voltage level meter.
# GP0 and GP1 and GP2 are digital outputs.
# GP2 is analog input.
# Connect:
#   A red    LED between GP0 and positive (with a resistor).
#   A yellow LED between GP1 and positive (with a resistor).
#   A green  LED between GP2 and positive (with a resistor).
#   A potentiometer to GP3, between positive and ground.
# If potentiometer is below 25%, red led will blink.
# Between 25% and 50%, only red will light still.
# Between 50% and 75%, red and yellow light.
# Above 75%, all three leds light.
#
# Tip: you could connect a LDR instead of a potentiometer to
# make a light level indicator.
#
import EasyMCP2221
from time import sleep

# Connect to device
mcp = EasyMCP2221.Device()

# GP0 and GP1 are inputs, GP2 and GP3 are outputs.
mcp.set_pin_function(
    gp0 = "GPIO_OUT",
    gp1 = "GPIO_OUT",
    gp2 = "GPIO_OUT",
    gp3 = "ADC")

mcp.ADC_config(ref="VDD")

while True:
    pot = mcp.ADC_read()[2]   # ADC channel 2 is GP3
    pot_pct = pot / 1024 * 100

    if pot_pct < 25:
        red_led_status = mcp.GPIO_read()[0]
        mcp.GPIO_write(
            gp0 = not red_led_status,
            gp1 = False,
            gp2 = False)

        sleep(0.1)

    elif 25 < pot_pct < 50:
        mcp.GPIO_write(
            gp0 = True,
            gp1 = False,
            gp2 = False)

    elif 50 < pot_pct < 75:
        mcp.GPIO_write(
            gp0 = True,
            gp1 = True,
            gp2 = False)

    elif pot_pct > 75:
        mcp.GPIO_write(
            gp0 = True,
            gp1 = True,
            gp2 = True)

DAC: LED fading

We use DAC_config() and DAC_write() to make a LED (connected to GP3 or GP2) to fade-in and fade-out with a triangular wave.

# DAC output
# MCP2221 only have 1 DAC, connected to GP2 and/or GP3.
import EasyMCP2221
from time import sleep

# Connect to device
mcp = EasyMCP2221.Device()

# Use GP2 and GP3 as DAC output.
mcp.set_pin_function(gp2 = "DAC", gp3 = "DAC")

# Configure DAC reference (max. output)
# Accepted values for ref are 'OFF', '1.024V', '2.048V', '4.096V' and 'VDD'.
mcp.DAC_config(ref="VDD")

while True:
    for v in range(0,32):
        mcp.DAC_write(v)
        sleep(0.01)

    for v in range(31,0,-1):
        mcp.DAC_write(v)
        sleep(0.01)

Advanced analog

Sinusoidal generator

In the following example, we will use DAC to generate a sin waveform with a period of 1 second.

DAC’s maximum update rate is 500Hz, one sample every 2ms on average. It really depends on the load of the host and USB bus controller.

DAC’s resolution is only 5 bit. That means 32 different values.

_images/DAC_sin_1Hz.png

Noise comes from USB traffic and it is in kHz region. Since ADC output frequency is much lower, it can be greatly reduced with a simple RC low pass filter.

_images/DAC_sin_1Hz_lowpass.png

Notice the usage of time.perf_counter() instead of sleep to get a more or less constant rate in a multitask operating system.

# DAC output, advanced example.
# Generate SIN signal using a recurrence relation to avoid calculate sin(x) in the main loop.
import EasyMCP2221
import time
from math import sqrt, cos, pi

# Output freq
sample_rate = 500 # Hz (unstable above 500Hz)
freq        = 1   # Hz

# Configure device pins and DAC reference.
# MCP2221 have only 1 DAC, connected to GP2 and/or GP3.
mcp = EasyMCP2221.Device()
mcp.set_pin_function(gp2 = "DAC", gp3 = "DAC")
mcp.DAC_config(ref="VDD")

# Initial values
W              = cos(2*pi*freq/sample_rate)
last_s         = sqrt(1-W**2)  # y_n-1  (y1)
before_last_s  = 0             # y_n-2  (y0)

# No trigonometric function in the main loop
while True:
    # set-up next sample time before doing anything else
    next_sample = time.perf_counter() + 1/sample_rate
    
    # Calculate next output value and write it to DAC
    s = 2*W*last_s - before_last_s    # s between -1 and 1
    out = (s + 1) / 2   # out between 0 and 1 now
    out = out * 31      # 5 bit DAC, 0 to 31
    out = round(out)    # integer
    mcp.DAC_write(out)

    # Update recurrence values
    (before_last_s, last_s) = (last_s, s)
    
    # Warn if we can't keep up with the sample rate!
    if time.perf_counter() > next_sample:
        print("Undersampling!")
    
    # Wait fixed delay for next sample (do not use sleep)
    while time.perf_counter() < next_sample: 
        pass

Capacitor charge

A GPIO output can be used to charge or discharge a capacitor through a resistor while we are sampling ADC values at regular intervals:

_images/sch_capacitor.svg

Program:

# Plotter for capacitor change/discharge
import EasyMCP2221
import time
import matplotlib.pyplot as plt
import numpy as np

capture_time = 1
Vdd = 5

# Configure device pins
mcp = EasyMCP2221.Device()
mcp.ADC_config()
mcp.set_pin_function(gp2 = "GPIO_OUT", gp3 = "ADC")

V = []
T = []

print("Initial discharge on course. Press enter to start charging.")
mcp.GPIO_write(gp2 = False)

input()
print("Charging...")
mcp.GPIO_write(gp2 = True)

start = time.perf_counter()

while time.perf_counter() - start <= capture_time:

    t = time.perf_counter()
    (_, _, V3) = mcp.ADC_read()

    # 10 bit, 5V ref
    V3 = V3 / 1024 * Vdd

    T.append(t - start)
    V.append(V3)


mcp.GPIO_write(gp2 = False)


plt.plot(T, V, 'o-')
plt.axis([-0.05, capture_time, 0, Vdd + 0.5])
plt.xticks(np.arange(0,capture_time,0.1))
plt.xlabel("Time (s)")
plt.ylabel("V (V)")
plt.title("Capacitor charge plot")
plt.grid()
plt.show()

This will produce the classic capacitor charge curve:

_images/v_t_c.png

LED V/I plotter

We can read the ADC values ​​while we are changing the DAC output to characterize some part.

Note that the DAC output impedance is 5k (according to the datasheet), so you can’t draw much current from it.

_images/sch_led_adc.svg

The breadboard connections are pretty straightforward:

_images/brd_led_adc.png

Program:

# V/I plotter DAC/ADC example.
import EasyMCP2221
from time import sleep

import matplotlib.pyplot as plt

# Configure device pins ADC and DAC reference.
# DAC output impedance is about 5k according to datasheet
# so measurements could be inaccurate as the current increases.
mcp = EasyMCP2221.Device()
mcp.set_pin_function(gp2 = "DAC", gp3 = "ADC")
mcp.DAC_config()
mcp.ADC_config()

R = 1000

V = 32 * [0]
I = 32 * [0]

for step in range(0,32):
    mcp.DAC_write(step)
    (_, V2, V3) = mcp.ADC_read()
    
    # 10 bit, 5V ref
    V2 = V2 / 1024 * 5
    V3 = V3 / 1024 * 5
    
    # I = V/R
    I_r = (V2 - V3) / R
    
    V[step] = V2
    I[step] = I_r * 1000 # mA
    
    print("Step:", step+1, "/ 32")
    
    sleep(0.05)


mcp.DAC_write(0)

plt.plot(V, I, 'o-')
plt.axis([0,5,0,1])
plt.xlabel("V (V)")
plt.ylabel("I (mA)")
plt.title("I vs V diagram")
plt.grid()
plt.show()

This is the output for an infrared, red, green and blue LEDs.

_images/v_i_leds.png

I2C bus

To make these examples work, you need to get an EEPROM (e.g. 24LC128) and connect it properly to the SCA and SCL lines, as well as power supply.

_images/sch_eeprom.svg

This is it in the breadboard. Don’t forget to connect WP pin to either Vcc or Gnd.

_images/brd_eeprom.png

I2C bus scan

We will use I2C_read() to send a read command to any possible I2C address in the bus. The moment we get an acknowledge, we know there is some slave connected.

# Very simple I2C scan
import EasyMCP2221

# Connect to MCP2221
mcp = EasyMCP2221.Device()

# Optionally configure GP3 to show I2C bus activity.
mcp.set_pin_function(gp3 = "LED_I2C")

print("Searching...")

for addr in range(0, 0x80):
    try:
        mcp.I2C_read(addr)
        print("I2C slave found at address 0x%02X" % (addr))

    except EasyMCP2221.exceptions.NotAckError:
        pass

This is my output:

$ python I2C_scan.py
Searching...
I2C slave found at address 0x50

Write to an EEPROM

In this example, we will use I2C_write() to write some string in the first memory position of an EEPROM.

# Simple EEPROM storage.
import EasyMCP2221

# Connect to MCP2221
mcp = EasyMCP2221.Device()

# Configure GP3 to show I2C bus activity.
mcp.set_pin_function(gp3 = "LED_I2C")

MEM_ADDR = 0x50
MEM_POS  = 0

# Take a phrase
phrase = input("Tell me a phrase: ")
# Encode into bytes using preferred encoding method
phrase_bytes = bytes(phrase, encoding = 'utf-8')

# Store in EEPROM
# Note that internal EEPROM buffer is only 64 bytes.
mcp.I2C_write(MEM_ADDR,
    MEM_POS.to_bytes(2, byteorder = 'little') +  # position to write
    bytes(phrase, encoding = 'utf-8') +          # data
    b'\0')                                       # null

print("Saved to EEPROM.")

Result:

$ python EEPROM_write.py
Tell me a phrase: This is an example.
Saved to EEPROM.

Read from an EEPROM

Same as before but reading

We seek the first position writing 0x0000, then I2C_read() 100 bytes and print until the first null.

# Simple EEPROM reading.
import EasyMCP2221

# Connect to MCP2221
mcp = EasyMCP2221.Device()

# Configure GP3 to show I2C bus activity.
mcp.set_pin_function(gp3 = "LED_I2C")

MEM_ADDR = 0x50
MEM_POS  = 0

# Seek EEPROM to position
mcp.I2C_write(
    addr = MEM_ADDR,
    data = MEM_POS.to_bytes(2, byteorder = 'little'))

# Read max 100 bytes
data = mcp.I2C_read(
    addr = MEM_ADDR,
    size = 100)

data = data.split(b'\0')[0]
print("Phrase stored was: " + data.decode('utf-8'))

Output:

$ python EEPROM_read.py
Phrase stored was: This is an example.

I2C Slave helper

EasyMCP2221.I2C_Slave.I2C_Slave class allows you to interact with I2C devices in a more object-oriented way.

# How to use I2C Slave helper class.
# Data logger: Read 10 ADC values from a PCF8591 with 1 second interval
# and store them in an EEPROM. Then, print the stored values.
import EasyMCP2221
from time import sleep

# Connect to MCP2221
mcp = EasyMCP2221.Device()

# Create two I2C Slaves
pcf    = mcp.I2C_Slave(0x48) # 8 bit ADC
eeprom = mcp.I2C_Slave(0x50) # serial memory

# Setup analog reading (and ignore the first value)
pcf.read_register(0b00000001)

print("Storing...")
for position in range (0, 10):
    v = pcf.read()
    eeprom.write_register(position, v, reg_bytes=2)
    sleep(1)

# Dump the 10 values
v = eeprom.read_register(0x0000, 10, reg_bytes=2)
print("Data: ")
print(list(v))

Output:

$ python I2C_Slave_example.py
Storing...
Data:
[78, 78, 78, 78, 82, 102, 81, 31, 56, 77]