Easy MCP2221

EasyMCP2221 is the most complete Python module to interface with Microchip MCP2221 and MCP2221A.

The MCP2221 is a fully integrated USB-to-UART/I2C serial converter with four GP pins providing miscellaneous functionalities.

MCP2221’s peripherals:
  • 4 General Purpose Input/Output (GPIO) pins

  • 3 channel 10 bit ADC

  • One 5 bit DAC

  • I2C

  • UART

  • Clock Output with PWM

  • One interrupt detection flag

  • USB remote Wake-up

With this chip and this library you can practice the basics of digital electronics, microcontrollers, and robotics with a regular computer and regular Python. See Examples.

This is MCP2221 and MCP2221A pinout:

_images/MCP2221_pinout.svg

Disclaimer

I am not related to Microchip Inc. in any way. This library is unofficial and for personal use only.

Some examples in this documentation show bare connections from your USB port to a breadboard. Most USB port controllers are protected against short-circuit between power and/or data lines, but some are not. I am not responsible for any damage you may cause to your computer. To be safe, always use an isolated powered USB hub for experimentation.

Many thanks to Microchip for providing free samples of MCP2221A, and for openly publishing the datasheet and documentation used to write this library.

MIT License

Copyright (c) 2023 Reinoso Guzman

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Install / troubleshooting

Regular installation via PIP

Pip command for Linux:

$ pip install EasyMCP2221

On Linux, additional steps may be required. See Troubleshooting section below.

Pip command for Windows:

> py -m pip install EasyMCP2221

Troubleshooting

Device not found

First step, try to use any of the Microchip’s official tools to verify that everything is working fine.

_images/mcp_utility_win.png

On Linux, use lsusb to find any device with ID 04D8:xxxx:

$ lsusb
Bus 001 Device 004: ID 04d8:00dd Microchip Technology, Inc.
...
Hidapi backend error

EasyMCP2221 depends on hidapi, which in fact needs some backend depending on OS. Sometimes this is troublesome.

If you get an error like this:

ImportError: Unable to load any of the following libraries:libhidapi-hidraw.so libhidapi-hidraw.so.0 libhidapi-libusb.so libhidapi-libusb.so.0 libhidapi-iohidmanager.so libhidapi-iohidmanager.so.0 libhidapi.dylib hidapi.dll libhidapi-0.dll

Try to install the following packages using pip:

  • libusb

  • libusb1

If that doesn’t work, try manually installing libhidapi from https://github.com/libusb/hidapi/releases.

Sometimes, yo need to manually copy libusb-1.0.dll to C:\Windows\System32. It used to be in C:\Users\[username]\AppData\Local\Programs\Python\Python39\Lib\site-packages\libusb\_platform\_windows\x64\libusb-1.0.dll or similar path.

Open failed for non-root users (Linux)

On Linux, only root can open these devices. Trying to run the software without privileges will raise the following error:

$ python3 pruebas.py
Traceback (most recent call last):
  File "pruebas.py", line 7, in <module>
    mcp = EasyMCP2221.Device(trace_packets = False)
  File "/home/pi/EasyMCP2221/EasyMCP2221/MCP2221.py", line 82, in __init__
    self.hidhandler.open_path(hid.enumerate(VID, PID)[devnum]["path"])
  File "hid.pyx", line 142, in hid.device.open_path
OSError: open failed

To change that, you need to add a udev rule. Create a new file under /etc/udev/rules.d/99-mcp.rules with this content:

SUBSYSTEM=="usb", ATTRS{idVendor}=="04d8", MODE="0666", GROUP="plugdev"
Delay at the end of script (Linux)

If you experience delays on script startup or exit, use lsmod to check for conflicting drivers.

# lsmod | grep hid
hid_mcp2221            20480  1
hid_generic            16384  0
usbhid                 57344  0
hid                   139264  3 usbhid,hid_generic,hid_mcp2221

This library may conflict with hid_mcp2221 kernel module.

To blacklist this module, create a file named /etc/modprobe.d/blacklist-mcp2221.conf with this content:

blacklist hid_mcp2221

Run rmmod hid_mcp2221 to unload the module.

Local installation and testing

You may want to install this library from a cloned GitHub repository, usually for testing or development purposes.

First create and activate a new virtual environment. Update pip if needed.

> python -m venv init easymcp_dev
> cd easymcp_dev
> Scripts\activate
> python -m pip install --upgrade pip

Then, clone the home repository inside that virtual environment and perform the installation in editable (-e) mode.

$ git clone https://github.com/electronicayciencia/EasyMCP2221

$ pip install -e EasyMCP2221

If you get this error: “File “setup.py” not found. Directory cannot be installed in editable mode”, update PIP.

> python -m pip install --upgrade pip

If you get this one: “EasyMCP2221 does not appear to be a Python project: neither ‘setup.py’ nor ‘pyproject.toml’ found.”, please check working directory. You must be in the root of the cloned GitHub repository.

Local documentation

This is an optional step. To compile documentation locally you will need sphinx and RTD theme.

pip install -U sphinx
pip install -U sphinx_rtd_theme

Compilation:

cd docs
make html

Main HTML file is EasyMCP2221/docs/build/html/index.html.

Testing

There is a test suite to check ADC, DAC, I2C, and some other features like start-up and persistence after a reset.

In order to pass the tests, you need a working MCP2221 or MCP2221A and a serial EEPROM 24LC128 or bigger. Use the following schematic:

_images/sch_testing.svg

GP0 and GP1 are used to test I2C in several scenarios. GP3 is used as a DAC. GP2, connected to a simple RC low pass filter, is used as an ADC to test different voltaje references.

Run all tests:

$ python -m unittest

Run specific test suite, verbose and fail-fast:

$ python -m unittest test.test_gpio -fv

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(30,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]

Graphical interface

EasyMCP2221 Workbench is a graphical application to configure and play with MCP2221 and MCP2221A chips based on EasyMCP2221 library.

EasyMCP2221 Workbench GitHub (source code, screenshots and pre-compiled binaries for Windows available).

https://github.com/electronicayciencia/EasyMCP2221-GUI/raw/main/screenshots/main_window.png

Full API reference

Device Initialization

class Device(VID=None, PID=None, devnum=None, trace_packets=None)

MCP2221(A) device

Parameters:
  • VID (int, optional) – Vendor Id (default to 0x04D8)

  • PID (int, optional) – Product Id (default to 0x00DD)

  • devnum (int, optional) – Device index if multiple device found with the same PID and VID. Default is first device (index 0).

  • trace_packets (bool, optional) – For debug only. See trace_packets.

Raises:

RuntimeError – if no device found with given VID and PID.

Example

>>> import EasyMCP2221
>>> mcp = EasyMCP2221.Device()
>>> print(mcp)
{
    "Chip settings": {
        "Interrupt detection edge": "both",
        "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"
}

Pin configuration

set_pin_function(gp0=None, gp1=None, gp2=None, gp3=None, out0=False, out1=False, out2=False, out3=False)

Configure pin function and, optionally, output value.

You can set multiple pins at once.

Accepted functions depends on the pin.

_images/MCP2221_pinout.svg
GP0 functions:
  • GPIO_IN (in) : Digital input

  • GPIO_OUT (out): Digital output

  • SSPND (out): Signals when the host has entered Suspend mode

  • LED_URX (out): UART Rx LED activity output (factory default)

GP1 functions:
  • GPIO_IN (in) : Digital input

  • GPIO_OUT (out): Digital output

  • ADC (in) : ADC Channel 1

  • CLK_OUT (out): Clock Reference Output

  • IOC (in) : External Interrupt Edge Detector

  • LED_UTX (out): UART Tx LED activity output (factory default)

GP2 functions:
  • GPIO_IN (in) : Digital input

  • GPIO_OUT (out): Digital output

  • ADC (in) : ADC Channel 2

  • DAC (out): DAC Output 1

  • USBCFG (out): USB device-configured status (factory default)

GP3 functions:
  • GPIO_IN (in) : Digital input

  • GPIO_OUT (out): Digital output

  • ADC (in) : ADC Channel 3

  • DAC (out): DAC Output 2

  • LED_I2C (out): USB/I2C traffic indicator (factory default)

Parameters:
  • gp0 (str, optional) – Function for pin GP0. If None, don’t alter function.

  • gp1 (str, optional) – Function for pin GP1. If None, don’t alter function.

  • gp2 (str, optional) – Function for pin GP2. If None, don’t alter function.

  • gp3 (str, optional) – Function for pin GP3. If None, don’t alter function.

  • out0 (bool, optional) – Logic output for GP0 if configured as GPIO_OUT (default: False).

  • out1 (bool, optional) – Logic output for GP1 if configured as GPIO_OUT (default: False).

  • out2 (bool, optional) – Logic output for GP2 if configured as GPIO_OUT (default: False).

  • out3 (bool, optional) – Logic output for GP3 if configured as GPIO_OUT (default: False).

Raises:
  • ValueError – If invalid function for that pin is specified.

  • ValueError – If given out value for non GPIO_OUT pin.

Examples

Set all pins at once:

>>> mcp.set_pin_function(
...     gp0 = "GPIO_IN",
...     gp1 = "GPIO_OUT", out1 = True,
...     gp2 = "ADC",
...     gp3 = "LED_I2C")
>>>

Change pin function at runtime:

>>> mcp.set_pin_function(gp1 = "GPIO_IN")
>>>

It is not permitted to set the output of a non GPIO_OUT pin.

>>> mcp.set_pin_function(
...     gp1 = "GPIO_OUT", out1 = True,
...     gp2 = "ADC", out2 = True)
Traceback (most recent call last):
...
ValueError: Pin output value can only be set if pin function is GPIO_OUT.
>>>

Only some functions are allowed for each pin.

>>> mcp.set_pin_function(gp0 = "ADC")
Traceback (most recent call last):
...
ValueError: Invalid function for GP0. Could be: GPIO_IN, GPIO_OUT, SSPND, LED_URX
>>>

Hint

Pin assignments are active until reset or power cycle. Use save_config() to make this configuration the default at next start.

save_config()

Write current status (pin assignments, GPIO output values, DAC reference and value, ADC reference, etc.) to flash memory.

You can save a new configuration as many times as you wish. That will be the default state at power up.

Raises:
  • RuntimeError – if command failed.

  • AssertionError – if an accidental flash protection attempt was prevented.

Example

Set all GPIO pins as digital inputs (high impedance state) at start-up to prevent short circuits while breadboarding.

>>> mcp.set_pin_function(
...     gp0 = "GPIO_IN",
...     gp1 = "GPIO_IN",
...     gp2 = "GPIO_IN",
...     gp3 = "GPIO_IN")
>>> mcp.DAC_config(ref = "OFF")
>>> mcp.ADC_config(ref = "VDD")
>>> mcp.save_config()

GPIO

GPIO_read()

Read all GPIO pins logic state.

Returned values can be True, False or None if the pin is not set for GPIO operation. For an output pin, the returned status is the actual value.

Returns:

4 logic values for the pins status gp0, gp1, gp2 and gp3.

Return type:

tuple of bool

Example

>>> mcp.GPIO_read()
(None, 0, 1, None)
GPIO_write(gp0=None, gp1=None, gp2=None, gp3=None)

Set pin output values.

If a pin is omitted, it will preserve the value.

To change the output state of a pin, it must be assigned to GPIO_IN or GPIO_OUT (see set_pin_function()).

Parameters:
  • gp0 (bool, optional) – Set GP0 logic value.

  • gp1 (bool, optional) – Set GP1 logic value.

  • gp2 (bool, optional) – Set GP2 logic value.

  • gp3 (bool, optional) – Set GP3 logic value.

Raises:

RuntimeError – If given pin is not assigned to GPIO function.

Examples

Configure GP1 as output (defaults to False) and then set the value to logical True.

>>> mcp.set_pin_function(gp1 = "GPIO_OUT")
>>> mcp.GPIO_write(gp1 = True)

If will fail if the pin is not assigned to GPIO:

>>> mcp.set_pin_function(gp2 = 'DAC')
>>> mcp.GPIO_write(gp2 = False)
Traceback (most recent call last):
    ...
RuntimeError: Pin GP2 is not assigned to GPIO function.

ADC - Analog input

ADC_read()

Read all Analog to Digital Converter (ADC) channels.

Analog value is always available regardless of pin function (see set_pin_function()). If pin is configured as output (GPIO_OUT or LED_I2C), the read value is always the output state.

ADC is 10 bits, so the minimum value is 0 and the maximum value is 1023.

Returns:

Value of 3 channels (gp1, gp2, gp3).

Return type:

tuple of int

Examples

All three pins configured as ADC inputs.

>>> mcp.ADC_config(ref = "VDD")
>>> mcp.set_pin_function(
...    gp1 = "ADC",
...    gp2 = "ADC",
...    gp3 = "ADC")
>>> mcp.ADC_read()
(185, 136, 198)

Reading the ADC value of a digital output gives the actual voltage in the pin. For a logic output 1 is equal to Vdd unless something is pulling that pin low (i.e. a LED).

>>> mcp.set_pin_function(
...    gp1 = "GPIO_OUT", out1 = True,
...    gp2 = "GPIO_OUT", out2 = False)
>>> mcp.ADC_read()
(1023, 0, 198)
ADC_config(ref='VDD')

Configure ADC reference voltage.

ref values:
  • “OFF”

  • “1.024V”

  • “2.048V”

  • “4.096V”

  • “VDD”

Parameters:

ref (str, optional) – ADC reference value. Default to supply voltage (Vdd).

Raises:

ValueError – if ref value is not valid.

Examples

>>> mcp.ADC_config()
>>> mcp.ADC_config("1.024V")
>>> mcp.ADC_config(ref = "5V")
Traceback (most recent call last):
...
ValueError: Accepted values for ref are 'OFF', '1.024V', '2.048V', '4.096V' and 'VDD'.

Hint

ADC configuration is saved when you call save_config() and reloaded at power-up. You only need to call this function if you want to change it.

DAC - Analog output

DAC_write(out)

Set the DAC output value.

Valid out values are 0 to 31.

To use a GP pin as DAC, you must assign the function “DAC” (see set_pin_function()). MCP2221 only have 1 DAC. So if you assign to “DAC” GP2 and GP3 you will see the same output value in both.

Parameters:

out (int) – Value to output (max. 32) referenced to DAC ref voltage.

Examples

>>> mcp.set_pin_function(gp2 = "DAC")
>>> mcp.DAC_config(ref = "VDD")
>>> mcp.DAC_write(31)
>>>
>>> mcp.DAC_write(32)
Traceback (most recent call last):
...
ValueError: Accepted values for out are from 0 to 31.
DAC_config(ref='VDD', out=None)

Configure Digital to Analog Converter (DAC) reference.

ref values:
  • “OFF”

  • “1.024V”

  • “2.048V”

  • “4.096V”

  • “VDD”

MCP2221’s DAC is 5 bits. So valid values for out are from 0 to 31.

out parameter is optional and defaults last value. Use DAC_write() to set the DAC output value.

Parameters:
  • ref (str, optional) – Reference voltage for DAC. Default to supply voltage (Vdd).

  • out (int, optional) – value to output. Default is last value.

Raises:

ValueError – if ref or out values are not valid.

Examples

>>> mcp.set_pin_function(gp2 = "DAC")
>>> mcp.DAC_config(ref = "4.096V")
>>> mcp.DAC_config(ref = 0)
Traceback (most recent call last):
...
ValueError: Accepted values for ref are 'OFF', '1.024V', '2.048V', '4.096V' and 'VDD'.

Hint

DAC configuration is saved when you call save_config() and reloaded at power-up. You only need to call this function if you want to change it.

Interrupt On Change

IOC_config(edge='both')

Configure Interruption On Change edge.

Valid values for edge:
  • none: disable interrupt detection

  • rising: fire interruption on rising edge (i.e. when GP1 goes from Low to High).

  • falling: fire interruption on falling edge (i.e. when GP1 goes from High to Low).

  • both: fire interruption on both (i.e. when GP1 state changes).

Remember to call save_config() to persist this configuration when reset the chip.

Parameters:

edge (str) – which edge triggers the interruption (see description).

Raises:

ValueError – if edge detection value is not valid.

Example

>>> mcp.IOC_config(edge = "rising")
>>>
IOC_read()

Read Interruption On Change flag.

To enable Interruption Detection mechanism, pin designation must be IOC. See set_pin_function().

Returns:

Value of interrupt flag.

Return type:

int

Example

>>> mcp.IOC_read()
1
IOC_clear()

Clear Interruption On Change flag.

Example

>>> mcp.IOC_read()
1
>>> mcp.IOC_clear()
>>> mcp.IOC_read()
0
>>>

Clock output

clock_config(duty, freq)

Configure clock output frequency and Duty Cycle.

duty values:
  • 0

  • 25

  • 50

  • 75

freq values:
  • “375kHz”

  • “750kHz”

  • “1.5MHz”

  • “3MHz”

  • “6MHz”

  • “12MHz”

  • “24MHz”

To output clock signal, you also need to assign GP1 function to CLK_OUT (see set_pin_function()).

Parameters:
  • duty (int) – Output duty cycle in percent.

  • freq (str) – Output frequency.

Raises:

ValueError – if any of the parameters is not valid.

Examples

>>> mcp.set_pin_function(gp1 = "CLK_OUT")
>>> mcp.clock_config(50, "375kHz")
>>>
>>> mcp.clock_config(100, "375kHz")
Traceback (most recent call last):
...
ValueError: Accepted values for duty are 0, 25, 50, 75.
>>> mcp.clock_config(25, "175kHz")
Traceback (most recent call last):
...
ValueError: Freq is one of 375kHz, 750kHz, 1.5MHz, 3MHz, 6MHz, 12MHz or 24MHz

I2C bus

I2C_Slave(addr, force=False, speed=100000)

Create a new I2C_Slave object.

See EasyMCP2221.I2C_Slave.I2C_Slave for detailed information.

Parameters:

addr (int) – Slave’s I2C bus address

Returns:

I2C_Slave object.

Example

>>> pcf    = mcp.I2C_Slave(0x48)
>>> eeprom = mcp.I2C_Slave(0x50)
>>> eeprom
EasyMCP2221's I2C slave device at bus address 0x50.
I2C_write(addr, data, kind='regular', timeout_ms=20)

Write data to an address on I2C bus.

Valid values for kind are:

regular

It will send start, data, stop (this is the default)

restart

It will send repeated start, data, stop

nonstop

It will send start, data to write, (no stop). Please note that you must use ‘restart’ mode to read or write after a nonstop write.

Parameters:
  • addr (int) – I2C slave device base address.

  • data (bytes) – bytes to write. Maximum length is 65535 bytes, minimum is 1.

  • kind (str, optional) – kind of transfer (see description).

  • timeout_ms (int, optional) – maximum time to write data chunk in milliseconds (default 20 ms). Note this time applies for each 60 bytes chunk. The whole write operation may take much longer.

Raises:
  • ValueError – if any parameter is not valid.

  • NotAckError – if the I2C slave didn’t acknowledge.

  • TimeoutError – if the writing timeout is exceeded.

  • LowSDAError – if I2C engine detects the SCL line does not go up (read exception description).

  • LowSCLError – if I2C engine detects the SDA line does not go up (read exception description).

  • RuntimeError – if some other error occurs.

Examples

>>> mcp.I2C_write(0x50, b'This is data')
>>>

Writing data to a non-existent device:

>>> mcp.I2C_write(0x60, b'This is data'))
Traceback (most recent call last):
...
EasyMCP2221.exceptions.NotAckError: Device did not ACK.

Note

MCP2221 writes data in 60-byte chunks.

The default timeout of 20 ms is twice the time required to send 60 bytes at the minimum supported rate (47 kHz).

MCP2221’s internal I2C engine has additional timeout controls.

I2C_read(addr, size=1, kind='regular', timeout_ms=20)

Read data from I2C bus.

Valid values for kind are:

regular

It will send start, data, stop (this is the default)

restart

It will send repeated start, data, stop

Parameters:
  • addr (int) – I2C slave device base address.

  • size (int, optional) – how many bytes to read. Maximum is 65535 bytes. Minimum is 1 byte.

  • kind (str, optional) – kind of transfer (see description).

  • timeout_ms (int, optional) – time to wait for the data in milliseconds (default 20 ms). Note this time applies for each 60 bytes chunk. The whole read operation may take much longer.

Returns:

data read

Return type:

bytes

Raises:
  • ValueError – if any parameter is not valid.

  • NotAckError – if the I2C slave didn’t acknowledge.

  • TimeoutError – if the writing timeout is exceeded.

  • LowSDAError – if I2C engine detects the SCL line does not go up (read exception description).

  • LowSCLError – if I2C engine detects the SDA line does not go up (read exception description).

  • RuntimeError – if some other error occurs.

Examples

>>> mcp.I2C_read(0x50, 12)
b'This is data'

Write then Read without releasing the bus:

>>> mcp.I2C_write(0x50, position, 'nonstop')
>>> mcp.I2C_read(0x50, length, 'restart')
b'En un lugar de la Mancha...'

Hint

You can use I2C_read() with size 1 to check if there is any device listening with that address.

There is a device in 0x50 (EEPROM):

>>> mcp.I2C_read(0x50)
b'1'

No device in 0x60:

>>> mcp.I2C_read(0x60)
Traceback (most recent call last):
...
EasyMCP2221.exceptions.NotAckError: Device did not ACK.

Note

MCP2221 reads data in 60-byte chunks.

The default timeout of 20 ms is twice the time required to receive 60 bytes at the minimum supported rate (47 kHz). If a timeout or other error occurs in the middle of character reading, the I2C may get locked. See LowSDAError.

I2C_speed(speed=100000)

Set I2C bus speed.

Acceptable values are between 47kHz and 400kHz. This is not stored on the flash configuration.

Parameters:

speed (int) – Bus clock frequency in Hz. Default bus speed is 100kHz.

Raises:
  • ValueError – if speed parameter is out of range.

  • RuntimeError – if command failed (I2C engine is busy).

Example

>>> mcp.I2C_speed(100000)
>>>

Note

The recommended values are between 47kHz and 400kHz. Out of this range, the minimum value is 46693, which corresponds to a clock of approximately 46.5kHz. And the maximum is 6000000, that generates about 522kHz clock.

USB wake-up

enable_power_management(enable=True)

Enable or disable USB Power Management options for this device.

Set or clear Remote Wake-up Capability bit. Remember to call save_config() after this function to save the new settings.

Remote wake-up is triggered by Interrupt detection on GP1 (see set_pin_function() and IOC_config()).

When enabled, Power Management Tab is available for this device in the Device Manager (Windows). To wake-up the computer “Allow this device to wake the computer” option must be set in Device Manager.

USB power attributes are only read while USB device enumeration. So reset() (or power supply cycle) is needed in order for changes to take effect.

Parameters:

enable (bool) – Enable or disable Power Management.

Raises:

RuntimeError – If flash read command failed.

Example

>>> mcp.enable_power_management(True)
>>> mcp.save_config()
>>> print(mcp)
...
    "Chip settings": {
        "Power management options": "enabled",
...
>>> mcp.reset()

Device reset

reset()

Reset MCP2221.

Reboot the device and load stored configuration from flash.

This operation do not reset any I2C slave devices.

Note

The host needs to re-enumerate the device after a reset command. There is a 5 seconds timeout to do that.

Low level and debug

read_flash_info(raw=False, human=False)

Read flash data.

Return USB enumeration strings, power-up GPIO settings and internal chip configuration.

Parameters:
  • raw (bool, optional) – If False, return only parsed data (this is the default). If True, return all data unparsed.

  • human (bool, optional) – If False, return variable names untranslated, for API (this is the default). If True, return variable names in readable text.

Returns:

Flash data (parsed or raw)

Return type:

dict

Example

>>> mcp.read_flash_info()
{
    "CHIP_SETTINGS": {
        "adc_ref": "VDD",
        "clk_duty": 50,
        "clk_freq": "12MHz",
        "dac_ref": "VDD",
        "dac_val": 0,
        "ioc": "both",
        "ma": 100,
        "pid": "0x00DD",
        "pwr": "disabled",
        "vid": "0x04D8"
    },
    "GP_SETTINGS": {
        "GP0": {
            "func": "GPIO_IN",
            "outval": 0
        },
        "GP1": {
            "func": "GPIO_IN",
            "outval": 0
        },
        "GP2": {
            "func": "GPIO_IN",
            "outval": 0
        },
        "GP3": {
            "func": "GPIO_IN",
            "outval": 0
        }
    },
    "USB_FACT_SERIAL": "01234567",
    "USB_PRODUCT": "MCP2221 USB-I2C/UART Combo",
    "USB_SERIAL": "0000033333",
    "USB_VENDOR": "Microchip Technology Inc."
}

Hint

When called with human = true parameter, this is the function used to stringfy the object.

revision()

Get the hardware and firmware revision number.

Returns:

Value of mayor and minor revisions of hardware and software.

Return type:

dict

Example

>>> mcp.revision()
{'firmware': {'mayor': 'A', 'minor': '6'},
'hardware': {'mayor': '1', 'minor': '2'}}
SRAM_config(clk_output=None, dac_ref=None, dac_value=None, adc_ref=None, int_conf=None, gp0=None, gp1=None, gp2=None, gp3=None)

Low level SRAM configuration.

Configure Runtime GPIO pins and parameters. All arguments are optional. Apply given settings, preserve the rest.

Parameters:
  • clk_output (int, optional) – settings

  • dac_ref (int, optional) – settings

  • dac_value (int, optional) – settings

  • adc_ref (int, optional) – settings

  • int_conf (int, optional) – settings

  • gp0 (int, optional) – settings

  • gp1 (int, optional) – settings

  • gp2 (int, optional) – settings

  • gp3 (int, optional) – settings

Raises:

RuntimeError – if command failed.

Examples

>>> from EasyMCP2221.Constants import *
>>> mcp.SRAM_config(gp1 = GPIO_FUNC_GPIO | GPIO_DIR_IN)
>>> mcp.SRAM_config(dac_ref = ADC_REF_VRM | ADC_VRM_2048)

Note

Calling this function to change GPIO when DAC is active and DAC reference is not Vdd will create a 2ms gap in DAC output.

send_cmd(buf)

Write a raw USB command to device and get the response.

Write 64 bytes to the HID interface, starting by buf bytes. Then read 64 bytes from HID and return them as a list. In case of failure (USB read/write or command error) it will retry. To prevent this, set cmd_retries to zero.

Parameters:

buf (list of bytes) – Full data to write, including command (64 bytes max).

Returns:

Full response data (64 bytes).

Return type:

list of bytes

Example

>>> from EasyMCP2221.Constants import *
>>> r = mcp.send_cmd([CMD_GET_GPIO_VALUES])
[81, 0, 238, 239, 238, 239, 238, 239, 238, 239, 0, 0, 0, ... 0, 0]

See also

Class variables cmd_retries, debug_messages and trace_packets.

Hint

The response does not wait until the actual command execution is finished. Instead, it is generated right after the device receives the command. So an error response might indicate:

  • the most recent command is not valid

  • the previous command finished with an error condition (case of I2C write).

_i2c_release()

Try to make the I2C bus ready for the next operation.

This is a private method, the API can change without previous notice.

If there is an active transfer, cancel it. Try multiple times.

Determine if the bus is ready monitoring SDA and SCL lines.

Raises:
  • LowSDAError – if SCL line is down (read exception description).

  • LowSCLError – if SDA line is down (read exception description).

  • RuntimeError – if multiple cancel attempts did not work. Undetermined cause.

Note

Calling Cancel command on an uninitialized I2C engine can make it crash in 0x62 status until next reset. This function uses _i2c_status() heuristics to determine if it can issue a Cancel now or not.

_i2c_status()

Return I2C status based on POLL_STATUS_SET_PARAMETERS command.

This is a private method, the API could change without previous notice.

Returns:

Dictionary with I2C internal details.

{
  'rlen' : 65,  <- Value of the requested I2C transfer length
  'txlen': 0,   <- Value of the already transferred (through I2C) number of bytes
  'div'  : 118, <- Current I2C communication speed divider value
  'ack'  : 0,   <- If ACK was received from client value is 0, else 1.
  'st'   : 98,  <- Internal state of I2C status machine
  'scl'  : 1,   <- SCL line value as read from the pin
  'sda'  : 0,   <- SDA line value as read from the pin
  'confused': False,   <- see note
  'initialized': True  <- see note
}

Hint

If your project does not use I2C, you could reuse SCL and SDA as digital inputs. Call this method to get its logic value.

Note

About confused status.

For some reason, ticking SDA line while I2C bus is initialized but idle will cause the next transfer to be bogus. To prevent this, you need to issue a Cancel command before the next read or write command.

Unfortunately, there is no official way to determine that we are in this situation. The only byte that changes when it happens seems to be byte 18, which is not documented.

About initialized status:

Same way, calling cancel when the I2C engine has not been used yet will make it to stall and stay in status 0x62 until next reset.

Unfortunately, there is no official way to determine when it is appropriate to call Cancel and when it’s not. Moreover, MCP2221’s I2C status after a reset is different from MCP2221A’s (the last one clears the last transfer length and the former does not). I found that Cancel fails when byte 21 is 0x00 and works when it is 0x60. This is, again, not documented.

Device.cmd_retries = 1

Times to retry an USB command if it fails.

Type:

int

Device.debug_messages = False

Print debugging messages.

Type:

bool

Device.trace_packets = False

Print all binary commands and responses.

Type:

bool

Exceptions

To capture EasyMCP2221.exceptions you must qualify them as EasyMCP2221.exceptions:

try:
    mcp.I2C_read(0x51, 1)
except EasyMCP2221.exceptions.NotAckError:
    print("No device")
    exit()
except EasyMCP2221.exceptions.LowSCLError:
    print("SCL low")

or import them explicitly:

from EasyMCP2221.exceptions import *

...

try:
    mcp.I2C_read(0x51, 1)
except NotAckError:
    print("No device")
    exit()
except LowSCLError:
    print("SCL low")
exception NotAckError

I2C slave device did not acknowledge last command or data. Possible causes are incorrect I2C address, device missing or busy.

exception TimeoutError

I2C transaction timed out.

Possible causes:
  • I2C bus noise

  • incorrect command, protocol or speed

  • slave device busy (e.g. EEPROM write cycle)

exception LowSCLError

SCL remains low.

SCL should go up when I2C bus is idle.

Possible causes:
  • Missing pull-up resistor or too high.

  • Signal integrity issues due to noise.

  • A slave device is using clock stretching to indicate it is busy.

  • Another device is using the bus.

exception LowSDAError

SDA remains low.

SDA should go up when I2C bus is idle.

Possible causes:
  • Missing pull-up resistor or too high.

  • Signal integrity issues due to noise.

  • An I2C read transfer timed out while slave was sending data, and now the I2C bus is locked-up. Read the Hint.

Hint

About the I2C bus locking-up.

Sometimes, due to a glitch or premature timeout, the master terminates the transfer. But the slave was in the middle of sending a byte. So it is expecting a few more clocks cycles to send the rest of the byte.

Since the master gave up, it will not clock the bus anymore, and so the slave won’t release SDA line. The master, seeing SDA line busy, refuses to initiate any new I2C transfer. If the slave does not implement any timeout (SMB slaves do have it, but I2C ones don’t), the I2C bus is locked-up forever.

MCP2221’s I2C engine cannot solve this problem. You can either manually clock the bus using any GPIO line, or cycle the power supply.

I2C Slave helper class

class I2C_Slave(mcp, addr, force=False, speed=100000)

EasyMCP2221’s I2C slave device.

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

Usually you create new instances of this class using EasyMCP2221.Device.I2C_Slave() function. See examples section.

Parameters:
  • mcp (EasyMCP2221.Device) – MCP2221 connected to this slave

  • addr (int) – Slave’s I2C bus address

  • force (bool, optional) – Create an I2C_Slave even if the target device does not answer. Default: False.

  • speed (int, optional) – I2C bus speed. Valid values from 50000 to 400000. See EasyMCP2221.Device.I2C_speed().

Raises:

RuntimeError – If the device didn’t acknowledge.

Examples

You should create I2C_Slave objects from the inside of an EasyMCP2221.Device:

>>> import EasyMCP2221
>>> mcp = EasyMCP2221.Device()
>>> eeprom = mcp.I2C_Slave(0x50)
>>> eeprom
EasyMCP2221's I2C slave device at bus address 0x50.

Or in a stand-alone way:

>>> import EasyMCP2221
>>> from EasyMCP2221 import I2C_Slave
>>> mcp = EasyMCP2221.Device()
>>> eeprom = I2C_Slave.I2C_Slave(mcp, 0x50)

Note

MCP2221 firmware exposes a subset of predefined I2C operations, but does not allow I2C primitives (i.e. start, stop, read + ack, read + nak, clock bus, etc.).

is_present()

Check if slave is present.

Perform a read operation (of 1 bytes length) to the slave address and expect acknowledge.

Returns:

True if the slave answer, False if not.

Return type:

bool

read(length=1)

Read from I2C slave.

See EasyMCP2221.Device.I2C_read().

Parameters:

length (int) – How many bytes to read. Default 1 byte.

Returns:

bytes string

Raises:

RuntimeError – if the I2C slave didn’t acknowledge or the I2C engine was busy.

read_register(register, length=1, reg_bytes=1, reg_byteorder='big')

Read from a specific register, position or command.

Sequence:

  • Start

  • Send device I2C address + R/W bit 0

  • Send register byte, memory position or command

  • Repeated start

  • Send device I2C address + R/W bit 1

  • Read length bytes

  • Stop

See EasyMCP2221.Device.I2C_read() for more information.

Parameters:
  • register (int) – Register to read, memory position or command.

  • length (int, optional) – How many bytes is the answer to read (default read 1 byte).

  • reg_bytes (int, optional) – How many bytes is the register, position or command to send (default 1 byte).

  • reg_byteorder (str, optional) – Byte order of the register address. ‘little’ or ‘big’. Default ‘big’.

Returns:

bytes string

Examples

Read from a regular i2c device, register 0x0D:

>>> bme.read_register(0x0D)
>>> b'ÿ'

Read 10 bytes from I2C EEPROM (2 bytes memory position):

>>> eeprom.read_register(2000, 25, reg_bytes=2)
>>> b'en muchas partes hallaba '
write(data)

Write to I2C slave.

See EasyMCP2221.Device.I2C_write() for more information.

Parameters:

data (bytes) – Data to write. Bytes, int from 0 to 255, or list of ints from 0 to 255.

Raises:

RuntimeError – if the I2C slave didn’t acknowledge or the I2C engine was busy.

write_register(register, data, reg_bytes=1, reg_byteorder='big')

Write to a specific register, position or command.

Sequence:

  • Start

  • Send device I2C address + R/W bit 0

  • Send register byte, memory position or command

  • Repeated start

  • Send device I2C address + R/W bit 0

  • Write data

  • Stop

See EasyMCP2221.Device.I2C_write() for more information.

Parameters:
  • register (int) – Register to read, memory position or command.

  • data (bytes) – Data to write. Bytes, int from 0 to 255, or list of ints from 0 to 255.

  • reg_bytes (int, optional) – How many bytes is the register, position or command to send (default 1 byte).

  • reg_byteorder (str, optional) – Byte order of the register address. ‘little’ or ‘big’. Default ‘big’.

Examples

Set PCF8591’s DAC output to 255. Command 0bx1xxxxxx.

>>> pcf.write_register(0b01000000, 255)

Write a stream of bytes to an EEPROM at position 0x1A00 (2 bytes memory position):

>>> eeprom.write_register(0x1A00, b'Testing 123...', reg_bytes=2)
>>> eeprom.read_register(0x1A00, 14, reg_bytes=2)
b'Testing 123...'

SMBus compatible class

Usage

This is a smbus compatibility class. You can use it to run any I2C Python library for Raspberry Pi or micropython just using MCP2221’s I2C device interface.

Usage:

from EasyMCP2221 import SMBus

bus = SMBus()

or

from EasyMCP2221 import smbus

bus = smbus.SMBus()

Note

To use other mcp functions in addition to SMBus, do not create a new MCP Device. It will interfere with existing bus resulting in unpredictable behavior. Always re-use bus.mcp object (see example 2).

Example 1: Basic weather station

In this example, we are using a library from Pimoroni/BME280 to read Temperature, Barometric Pressure and Relative Humidity from a BME280 sensor.

That library is designed for Raspberry Pi or any other system that supports SMBus protocol. It works together with EasyMCP2221 via SMBus class.

Install:

pip install pimoroni-bme280 EasyMCP2221

Example code:

import time
from EasyMCP2221 import SMBus
from bme280 import BME280

bus = SMBus(1)
bme280 = BME280(i2c_dev=bus)

while True:
    temperature = bme280.get_temperature()
    pressure = bme280.get_pressure()
    humidity = bme280.get_humidity()
    print('{:05.2f}*C {:05.2f}hPa {:05.2f}%'.format(temperature, pressure, humidity))
    time.sleep(1)

Output:

17.93*C 933.76hPa 51.57%
17.92*C 933.76hPa 51.57%
17.91*C 933.77hPa 51.53%
17.91*C 933.77hPa 51.50%
17.91*C 933.77hPa 51.54%
...

Example 2: Real Time Clock with LCD

This is a digital clock with two I2C chips:

  • DS1307 as RTC

  • LCD display based on with PCF8574 I2C adapter.

It also shows how to re-use mcp object to further configure MCP2221.

Main loop:

  • DS1307 is configured as 1Hz square oscillator.

  • MCP2221’s GP2 is configured as Interrupt on Change.

  • The rising edge of DS1307’s output triggers the update cycle.

Full code on EasyMCP2221 examples/clock

from EasyMCP2221 import SMBus
from lcd_driver import LCD
from DS1307 import DS1307

# Create SMBus and instances
bus = SMBus()
lcd = LCD(bus, addr=0x3F)
ds = DS1307(bus, addr=0x68)

bus.mcp.I2C_speed(100_000) # DS1307 only supports 100kHz

bus.mcp.set_pin_function(
    gp0 = "GPIO_IN",  # unused
    gp1 = "IOC",      # trigger update LCD each second
    gp2 = "DAC",      # simulate backup battery
    gp3 = "LED_I2C")  # i2c traffic indicator

bus.mcp.DAC_write(21) # about 3.28V with 5V Vcc
bus.mcp.IOC_config(edge = "rising")

# Initialization after a complete power loss
if ds.halted():
    ds.write_now()
    ds._write(0x07, 0b0001_0000) # sqwe 1Hz
    print("RTC initialized with current timestamp")
else:
    print("RTC was already initialized")

lcd.clear()

# Update only when GP1 changes using Interrupt On Change
while True:
    if bus.mcp.IOC_read():
        bus.mcp.IOC_clear()
        (year, month, day, dow, hours, minutes, seconds) = ds.read_all()

        lcd.display_string("%02d/%02d/20%02d" % (day, month, year), 1)
        lcd.display_string("%02d:%02d:%02d" % (hours, minutes, seconds), 2)

Full reference

Based on kplindegaard/smbus2 interface.

See The SMBus Protocol for more information.

class SMBus(bus=None, force=False, VID=1240, PID=221, devnum=0, clock=100000)

Initialize and open an i2c bus connection.

Parameters:
  • bus (int or str) – (for compatibility only, not used) i2c bus number (e.g. 0 or 1) or an absolute file path (e.g. /dev/i2c-42). If not given, a subsequent call to open() is required.

  • force (boolean) – (for compatibility only, not used) force using the slave address even when driver is already using it.

  • VID – Vendor Id (default to 0x04D8)

  • PID – Product Id (default to 0x00DD)

  • clock – I2C clock frequency (default to 100kHz)

block_process_call(i2c_addr, register, data, force=None)

Executes a SMBus Block Process Call, sending a variable-size data block and receiving another variable-size response

Parameters:
  • i2c_addr (int) – i2c address

  • register (int) – Register to read/write to

  • data (list) – List of bytes

  • force (Boolean) –

Returns:

List of bytes

Return type:

list

close()

(For compatibility only, no effects) Close the i2c connection.

open(bus)

(For compatibility only, no effects) Open a given i2c bus.

Parameters:

bus (int or str) – i2c bus number (e.g. 0 or 1) or an absolute file path (e.g. ‘/dev/i2c-42’).

Raises:

TypeError – if type(bus) is not in (int, str)

process_call(i2c_addr, register, value, force=None)

Executes a SMBus Process Call, sending a 16-bit value and receiving a 16-bit response

Parameters:
  • i2c_addr (int) – i2c address

  • register (int) – Register to read/write to

  • value (int) – Word value to transmit

  • force (Boolean) –

Return type:

int

read_block_data(i2c_addr, register, force=None)

Read a block of up to 32-bytes from a given register.

Parameters:
  • i2c_addr (int) – i2c address

  • register (int) – Start register

  • force (Boolean) –

Returns:

List of bytes

Return type:

list

read_byte(i2c_addr, force=None)

Read a single byte from a device.

Return type:

int

Parameters:
  • i2c_addr (int) – i2c address

  • force (Boolean) –

Returns:

Read byte value

read_byte_data(i2c_addr, register, force=None)

Read a single byte from a designated register.

Parameters:
  • i2c_addr (int) – i2c address

  • register (int) – Register to read

  • force (Boolean) –

Returns:

Read byte value

Return type:

int

read_i2c_block_data(i2c_addr, register, length, force=None)

Read a block of byte data from a given register.

Parameters:
  • i2c_addr (int) – i2c address

  • register (int) – Start register

  • length (int) – Desired block length

  • force (Boolean) –

Returns:

List of bytes

Return type:

list

read_word_data(i2c_addr, register, force=None)

Read a single word (2 bytes) from a given register.

Parameters:
  • i2c_addr (int) – i2c address

  • register (int) – Register to read

  • force (Boolean) –

Returns:

2-byte word

Return type:

int

write_block_data(i2c_addr, register, data, force=None)

Write a block of byte data to a given register.

Parameters:
  • i2c_addr (int) – i2c address

  • register (int) – Start register

  • data (list) – List of bytes

  • force (Boolean) –

Return type:

None

write_byte(i2c_addr, value, force=None)

Write a single byte to a device.

Parameters:
  • i2c_addr (int) – i2c address

  • value (int) – value to write

  • force (Boolean) –

write_byte_data(i2c_addr, register, value, force=None)

Write a byte to a given register.

Parameters:
  • i2c_addr (int) – i2c address

  • register (int) – Register to write to

  • value (int) – Byte value to transmit

  • force (Boolean) –

Return type:

None

write_i2c_block_data(i2c_addr, register, data, force=None)

Write a block of byte data to a given register.

Parameters:
  • i2c_addr (int) – i2c address

  • register (int) – Start register

  • data (list) – List of bytes

  • force (Boolean) –

Return type:

None

write_word_data(i2c_addr, register, value, force=None)

Write a single word (2 bytes) to a given register.

Parameters:
  • i2c_addr (int) – i2c address

  • register (int) – Register to write to

  • value (int) – Word value to transmit

  • force (Boolean) –

Return type:

None

Limitations and bugs

Chip or software limitations

USB speed limits

MCP2221’s command rate is limited by USB polling rate. Each command requires two USB slots. One to send the command to the device, and the other to get the response. See Internal details for details.

On xHCI controllers (USB 3.0), polling rate is 1000Hz. So you can issue one command every 2ms. That command could be a DAC update, ADC read, GPIO, I2C operation, etc.

On eHCI (USB 2.0), the maximum update rate I measured is 333 commands per second.

These ratios depend on multiple parameters. Like your USB hardware (including cable and hub), operating system, or the number of devices connected to the same bus.

I2C speed limit

Each I2C interaction requires multiple USB commands. See Internal details for details.

Sending one byte will require: setup, send data, and get the result. Reading one byte will require: setup, finish test, and read data.

Depending on your USB polling rate, each of these commands can take 2ms or more.

I2C speed (100kHz / 400kHz) only matters when you are transmitting a lot of bytes in a row. For a few bytes interaction, speed is limited by the USB polling rate.

Internal reference reset

From MCP2221A’s datasheet (section 1.8):

When the Set SRAM settings command is used for GPIO control, the reference voltage for VRM is always reinitialized to the default value (VDD) if it is not explicitly set.

This is compensated by software. But, due to the calling interval, there will be always a 2ms gap in the DAC output if it is using internal reference (not Vdd) when you change any pin function.

I2C crashes

Eventually, due to a glitch or unexpected timeout, the MCP2221 cancels an I2C transfer. The slave may be in the middle of sending a byte, and expecting some clocks cycles to send the rest of the byte.

MCP2221 is unable to start a new I2C transfer while SDA line is still busy. And the slave won’t release SDA until next clock cycle. So the whole bus hangs.

See LowSDAError.

Misc
  • The ADC seems to be always connected. So leakage current for GP1, GP2 and GP3 is greater than for GP0. Think of it as a very weak pull-down resistor on these pins.

  • This library does not work with password protected devices. You cannot use it to set or clear MCP2221’s Flash password.

  • Changing VID/PID not supported.

  • Maximum length for single I2C read or write operations is 65535 bytes.

  • Regardless of the output frequency, MCP2221(A)’s clock output has glitch every 1ms.

  • Cannot clear the Interrupt flag when ADC reference value is 4.096V and GP1 is low.

Software Bugs

Bug tracking system: https://github.com/electronicayciencia/EasyMCP2221/issues

Internal details

USB transactions are always initiated by the Host. MCP2221 supports Full-Speed USB at 1000Hz polling rate. That is, a transaction every one millisecond.

Each USB transaction can be:

Output:

the host will send data to the peripheral. Used for commands.

Input:

the host wants the peripheral to send data. Used for replies.

We need 2 USB transactions (2ms) for each command/reply. Thus, the highest DAC update rate for or ADC sampling rate is 500Hz.

The maximum length of a data payload is 64 bytes.

I2C transfers

Write transfer
_images/I2C_write_100bytes_usb3.svg

Timeline of a 100 bytes I2C write. Open the image in a new tab to see it full size.

While the I2C engine is sending data1, subsequent write commands will fail and will be ignored. Only when data1 has already been sent, the device responds with OK and proceed to send the next data chunk data2.

Read transfer
_images/I2C_read_75bytes_usb3.svg

Timeline of a 75 bytes I2C read. Open the image in a new tab to see it full size.

While the MCP2221 is reading bytes, subsequent calls to Read I2C data buffer (40) will fail. When the buffer is ready (or full), the call succeed, the data is returned and the reading of next chunk begins.

Transfer failure

Note that since the USB host only send or requests data at fixed intervals of 1 ms, the state of the last issued command may or may not match the actual state of the I2C transfer.

For example, in a read operation to a nonexistent device. The I2C transfer starts and even ends before the response to the write command 91 is read.

_images/I2C_read_NAK.svg

I2C read failure. Not acknowledge.

Moreover, like in the above successful reading, the I2C read command (91) succeed but the I2C read data command (40) fails, exactly as before.

I use internal I2C engine status code to differentiate between both cases. Unfortunately, not all the states are fully documented.

Changelog

V1.7

V1.7.1
I2C:
  • Set I2C default speed to 100kHz. In some chips, the default speed is 500kHz and can cause trouble with some slave devices or noisy buses. You can adjust it with I2C_speed() function.

  • Added clock frequency parameter in SMBus class.

Misc:
  • Added function revision() to get the mayor and minor hardware and firmware revision.

  • Fixed test about Interrupt On Change.

  • Moved GUI to a separate application on its own repository.

V1.7.0
ADC/DAC:
  • Fixed bug. When GP1, 2 and 3 are all in ADC mode and ADC reference is VDD. If just after reset VRM reference is selected, ADC stops working.

Interrupt on Change:
  • Added new section about Interrupt On Change (IOC) detection.

  • Renamed function wake_up_config to IOC_config().

  • Added IOC_read() to read Interrupt On Change flag.

  • Added IOC_clear() to clear Interrupt On Change flag.

  • Replaced raising edge with rising edge.

I2C:
  • Removed deprecated functions I2C_is_idle and I2C_cancel.

Misc:
  • Solved bug USB remote Wake-up is not saved with save_config().

  • New behavior for enable_power_management(). Changes are not saved immediately to Flash. Call to save_config() is needed instead.

  • New function read_flash_info().

  • Device information now returns GPIO designation and default status.

  • Document as limitation a weird MCP2221’s bug related to interrupt flag and ADC reference.

V1.6

V1.6.3
I2C:
  • New SMBus compatible class. Useful to use other Python I2C modules with MCP2221 interface.

  • Fixed. I2C slave class exception when device is not present.

Documentation:
  • Conflict with kernel module hid_mcp2221. See Delay at the end of script (Linux) in Install / troubleshooting.

  • Explain I2C speed limit for very short transfers.

  • Document SMBus compatible class. Include example code for BME280 (Temperature, Pressure, Humidity sensor).

V1.6.2
ADC/DAC:
  • Fixed bug: when ADC reference is VDD and DAC reference is VRM and a new GPIO configuration is applied, DAC stops working. It seems to be related with a known MCP limitation.

  • Fixed bug: Restore DAC reference value after reset.

  • In some cases MCP2221A’s firmware does not restore DAC or ADC pin assignment when it boots. Software workaround.

I2C:
  • Deprecated I2C_cancel() and I2C_is_idle(). Bus is now managed automatically. You can use _i2c_release and _i2c_status as replacement if needed.

  • Fixed. Low SCL and low SDA exceptions were swapped.

  • When the I2C bus detects SDA activity, the next transfer does not work fine. Prevented via software. See _i2c_status.

Documentation:
  • Instructions and schematic for testing in the Install / troubleshooting section.

  • Replaced PNG schematics by SVG versions in Examples.

  • Troubleshooting section to run as an unprivileged user in Linux (udev rule).

  • Added developers section with details about I2C transfers. See Internal details.

Misc:
  • Added test suite.

  • Added IOC edge detection setting in device representation.

  • Fixed. Bug when reset a device with customized VID/PID.

  • Multiple tries to find the device after a reset() (until timeout).

V1.6.1
Improved USB stability:
  • Added timeout in HID read.

  • Added retries in send_cmd.

  • Better USB trace_commands output format.

  • Removed sleep parameter in send_cmd().

GPIO / ADC and DAC:
More reliable I2C functions:
  • Rewritten I2C_read() to take into account internal I2C engine status.

  • Rewritten I2C_write() to prevent infinite loop, quicker write and ACK checking.

  • Timeout and early failure check in read and write to prevent infinite loop.

  • Custom exceptions for better error handling (see Exceptions in Full API reference)

  • Automatically try to recover from an I2C error in past operation.

New features:
  • Function to save current state: save_config().

  • Added speed parameter in I2C Slave class.

Documentation:
  • Removed self argument from autodoc methods.

  • Added pictures and schematics.

  • Added MCP2221 pinout guide.

  • Added advanced ADC/DAC examples section.

  • Added license.

  • Corrected typos.

  • Formatting.

V1.6.0

Released 1.5.1 again by mistake.

V1.5

V1.5.1

Add I2C Slave helper class.

V1.5.0

First EasyMCP2221 version.

Older releases

This project was initially a fork of PyMCP2221A library by Yuta KItagami (https://github.com/nonNoise/PyMCP2221A).

I made a few changes, then a few more, until I ended up rewriting almost all the code. Since the API is no longer compatible with PyMCP2221A, I decided to create a new package.

Tags v1.4 and earlier are from PyMCP2221A.