Lego Spike Prime/Essentials LPF2 - QWIIC I2C Connector
by CEEOInnovations in Circuits > Electronics
972 Views, 3 Favorites, 0 Comments
Lego Spike Prime/Essentials LPF2 - QWIIC I2C Connector
This LPF2 connector enables seamless integration of QWIIC devices with Lego Spike Prime and Spike Essential Bricks. Designed for easy and cost-effective assembly, it offers a practical and efficient solution for connecting your devices.
Supplies
Make sure to have the following parts ordered:
- 2 Custom printed circuit boards - use the gerber pcb files in the following github repository to download and order printed circuit boards. - LPF2-QWIIC-Connector/GerberFiles at main ยท tuftsceeo/LPF2-QWIIC-Connector (github.com)
- 4 Resistors - RC0402JR-0710KL YAGEO | Resistors | DigiKey
- QWIIC Connector - JST SH 4-pin Right Angle Connector (10-pack) [Qwiic Compatible] : ID 4208 : Adafruit Industries, Unique & fun DIY electronics and kits
- 2 channel digital signal booster - PCA9515APWR Texas Instruments | Integrated Circuits (ICs) | DigiKey Marketplace
Solder Some Components
Initially, use super glue to bond two PCB layers together, creating a 3.2mm thick PCB that fits perfectly into the Lego Brick's LPF2 socket.
Then solder the surface mount components as shown in the photo.
Connect Things Together
Connect your connector to a Lego brick, and attach a QWIIC device to the connector. See the photos below to make sure that the LFP2 connector is attached with correct orientation.
Upload Some Code
Make sure to have Thonny Software downloaded on your computer.
Thonny Software Download Link: Thonny, Python IDE for beginners
The following script will add I2C communication functionality to the Lego brick.
Save The following script as SoftwareI2C.py into the Lego Brick Memory using the Thonny:
from hub import pins as Pin
from hub import uart
import time
import struct
class SoftwareI2C:
SW_I2C_WAIT_TIME = 40 # Microseconds to wait, adjust based on your needs
def __init__(self, scl_pin, sda_pin):
self.en = Pin.init(0, Pin.EN, Pin.OUT)
self.en.value(1)
self.scl = Pin.init(0, scl_pin, Pin.OUT)
self.sda = Pin.init(0, sda_pin, Pin.OUT)
self.scl.value(1)
self.sda.value(1)
def scl_high(self):
self.scl.value(1)
def scl_low(self):
self.scl.value(0)
def sda_high(self):
self.sda.value(1)
def sda_low(self):
self.sda.value(0)
def sda_input(self):
self.sda = Pin.init(0, Pin.RX, Pin.IN)
def sda_output(self):
self.sda = Pin.init(0, Pin.RX, Pin.OUT)
def delay_us(self, us):
time.sleep_us(us)
def start(self):
self.sda_high()
self.scl_high()
self.delay_us(self.SW_I2C_WAIT_TIME)
self.sda_low()
self.delay_us(self.SW_I2C_WAIT_TIME)
self.scl_low()
self.delay_us(self.SW_I2C_WAIT_TIME * 2)
def stop(self):
self.sda_low()
self.scl_high()
self.delay_us(self.SW_I2C_WAIT_TIME)
self.sda_high()
self.delay_us(self.SW_I2C_WAIT_TIME)
def check_ack(self):
self.sda_input()
self.scl_high()
ack = not self.sda.value()
self.scl_low()
self.sda_output()
self.delay_us(self.SW_I2C_WAIT_TIME)
return ack
def write_byte(self, byte):
self.scl_low()
for i in range(8):
self.sda.value((byte >> (7 - i)) & 1)
self.delay_us(self.SW_I2C_WAIT_TIME)
self.scl_high()
self.delay_us(self.SW_I2C_WAIT_TIME)
self.scl_low()
return self.check_ack()
def read_byte(self, ack=True):
self.sda_input()
byte = 0
for i in range(8):
self.scl_high()
byte = (byte << 1) | self.sda.value()
self.scl_low()
self.sda_output()
if ack:
self.sda_low()
else:
self.sda_high()
self.scl_high()
self.delay_us(self.SW_I2C_WAIT_TIME)
self.scl_low()
self.sda_high()
return byte
def scan(self):
found_devices = []
for address in range(0x00, 0x78): # Valid I2C addresses
self.start()
#print("add")
#print(address)
if self.write_byte(address << 1): # Shift address for write mode
#print("found")
found_devices.append(address)
self.stop()
return found_devices
def writeto(i2c, address, data):
"""
Write data to an I2C device.
:param i2c: SoftwareI2C object instance.
:param address: 7-bit I2C device address.
:param data: Bytearray or list of data to write.
"""
i2c.start()
if i2c.write_byte(address << 1): # Shift address for write mode and send
for byte in data:
if not i2c.write_byte(byte):
print("Error: No ACK received after data byte.")
break
else:
print("Error: No ACK received for address.")
i2c.stop()
def readfrom(i2c, address, num_bytes):
"""
Read data from an I2C device.
:param i2c: SoftwareI2C object instance.
:param address: 7-bit I2C device address.
:param num_bytes: Number of bytes to read.
:return: Data read as a bytearray.
"""
data = bytearray()
i2c.start()
if i2c.write_byte((address << 1) | 1): # Shift address for read mode and send
for i in range(num_bytes):
ack = i < num_bytes - 1 # ACK all but the last byte
data.append(i2c.read_byte(ack))
else:
print("Error: No ACK received for address.")
i2c.stop()
return data
def writeto_mem(i2c, device_addr, register_addr, data):
"""
Write data to a specific register of an I2C device.
:param i2c: SoftwareI2C object instance, the custom I2C implementation.
:param device_addr: The 7-bit address of the I2C device.
:param register_addr: The register address within the I2C device where data will be written.
:param data: The data to write (as a bytes object or list of bytes).
"""
i2c.start() # Start I2C communication
# Send the device address in write mode
if not i2c.write_byte(device_addr << 1):
print("Error: No ACK received for device address.")
i2c.stop()
return
# Send the register address
if not i2c.write_byte(register_addr):
print("Error: No ACK received for register address.")
i2c.stop()
return
# Write the data bytes
for byte in data:
if not i2c.write_byte(byte):
print("Error: No ACK received after data byte.")
break
i2c.stop() # Stop I2C communication
def readfrom_mem(i2c, address, register, num_bytes):
"""
Read data from a specific register of an I2C device.
:param i2c: The SoftwareI2C object instance.
:param address: The 7-bit address of the I2C device.
:param register: The register address within the device from which to read.
:param num_bytes: The number of bytes to read from the register.
:return: A bytearray containing the data read from the device.
"""
# Start the I2C communication and send the device address in write mode
i2c.start()
if not i2c.write_byte(address << 1): # Shift address for write mode
print("Error: Device not acknowledging write mode.")
i2c.stop()
return
# Write the register address to read from
if not i2c.write_byte(register):
print("Error: Device not acknowledging register address.")
i2c.stop()
return
# Repeated start to switch to read mode
i2c.start()
if not i2c.write_byte((address << 1) | 1): # Shift address for read mode
print("Error: Device not acknowledging read mode.")
i2c.stop()
return
# Read the specified number of bytes
data = bytearray()
for i in range(num_bytes):
ack = i < num_bytes - 1 # ACK for all but the last byte
byte = i2c.read_byte(ack)
data.append(byte)
# Stop the I2C communication
i2c.stop()
return data
Run the following demo code to scan the connected i2C devices:
import SoftwareI2C
import pins as Pin
i2c = SoftwareI2C(scl_pin=Pin.TX, sda_pin=Pin.RX)
while(1)
print(i2c.scan())
print("Scanning I2C bus...", i2c.scan())