In the world of hobbyist electronics and rapid prototyping, the Raspberry Pi is king. Its secret weapon? The General-Purpose Input/Output (GPIO) header—a row of pins that allows the tiny computer to interact directly with the physical world, from reading sensors to driving motors. But what about the powerful desktop or laptop you use every day? These machines, for all their computational might, are sealed off, lacking a simple, built-in way to blink an LED, let alone interface with complex electronics. For years, this meant that physical computing projects were tethered to specialized single-board computers or microcontrollers.
Enter the Adafruit FT232H Breakout. This unassuming little board is a game-changer, a powerful Rosetta Stone for hardware communication. It acts as a multi-protocol bridge, translating the universal language of USB into the dialects of the electronics world: GPIO, SPI, and, most importantly for our project today, I2C. By simply plugging it into a standard USB port on any Windows, macOS, or Linux machine, you effectively grant your computer the same kind of hardware-level control that has made the Raspberry Pi so beloved. Suddenly, your powerful PC isn't just for processing data; it's for controlling it in the real world.
The Project: A Tale of Two Lines
To demonstrate the power and simplicity of the FT232H, we'll tackle a classic "Hello, World!" project of the hardware realm: driving a standard 2-line by 16-character (16x2) LCD display. These displays are ubiquitous in projects, perfect for showing status updates, sensor readings, or user menus. We'll use an I2C-enabled version, which simplifies wiring to just four connections: power, ground, data, and clock.
What You'll Need:
An Adafruit FT232H Breakout board.
A standard 16x2 character LCD with an I2C backpack.
A breadboard and a few jumper wires.
A computer running Python.
The Setup: Wiring it Up
Connecting the components is refreshingly straightforward. The FT232H board has clearly labeled pins. For I2C communication, we only need four wires:
5V to VCC: Connect the FT232H's 5V pin to the LCD's VCC (power) pin.
GND to GND: Connect one of the FT232H's GND (ground) pins to the LCD's GND pin.
AD1/SCL to SCL: This is the I2C clock line. Connect the FT232H's D1 pin (which also serves as SCL) to the LCD's SCL pin.
AD2/SDA to SDA: This is the I2C data line. Connect the FT232H's D2 pin (SDA) to the LCD's SDA pin.
That's it. No complex schematics, no soldering required. The USB connection from your computer will power both the FT232H and the LCD.
The Brains: Speaking I2C with CircuitPython
With the hardware connected, the real magic happens in the software. We'll use CircuitPython, Adafruit's beginner-friendly version of Python for microcontrollers, along with the adafruit-blinka
library, which allows CircuitPython libraries to run on standard computers.
First, you'll need to set up your computer to work with the FT232H. Adafruit provides excellent, detailed guides for this process on their learning system, which involves installing some drivers and Python libraries.
Once your environment is configured, the Python code to control the LCD is remarkably simple. The script below will initialize the I2C connection and display a two-line message, a nod to your interest in high-fidelity audio.
This code is self-contained and ready to run. It handles all the necessary imports, sets up the I2C communication with the FT232H, finds the LCD's address, and displays the text.
The true power here is the abstraction provided by the CircuitPython libraries. Complex I2C protocols are boiled down to simple, readable Python commands. The lcd.message
function handles the intricate details of sending character data to the display controller; you just need to provide the string.
Beyond the Display: The Possibilities are Limitless
While driving an LCD is a simple demonstration, it unlocks a vast ecosystem of I2C-compatible sensors and devices. Temperature sensors, accelerometers, motor drivers, and OLED displays can all be controlled from your main computer using the same FT232H board and a few lines of Python.
For developers, testers, and hobbyists, this is a profound shift. You can now write sophisticated Python scripts on your powerful development machine—with all its debugging tools and processing power—to directly interface with and test hardware components. No more writing code on a PC, cross-compiling, and flashing it to a microcontroller just to see if a sensor works.
The FT232H doesn't replace devices like the Raspberry Pi or Arduino, but it powerfully complements them. It tears down the wall between software development and hardware prototyping, turning any computer into a versatile physical computing workstation. It’s a small, inexpensive tool that adds a massive capability to the machine you already own.
CircuitPython LCD Driver
"""
This script demonstrates how to drive a 16x2 I2C character LCD display
using an Adafruit FT232H breakout board and CircuitPython libraries on a
standard computer.
It initializes the I2C communication, detects the LCD's address, and
displays a two-line message.
"""
import time
import typing
import board
import busio
from adafruit_character_lcd.character_lcd_i2c import Character_LCD_I2C
# Define LCD dimensions
LCD_COLUMNS: typing.Final[int] = 16
LCD_ROWS: typing.Final[int] = 2
def find_i2c_device_address(i2c: busio.I2C) -> typing.Optional[int]:
"""
Scans the I2C bus for a connected device.
Args:
i2c: The I2C bus object.
Returns:
The address of the first device found, or None if no device is found.
"""
while not i2c.try_lock():
pass
try:
addresses: list[int] = i2c.scan()
if addresses:
print(f"I2C device found at address: {hex(addresses[0])}")
return addresses[0]
print("No I2C device found.")
return None
finally:
i2c.unlock()
def main() -> None:
"""
Main function to initialize the LCD and display a message.
"""
# Initialize I2C bus. SCL and SDA are connected to the FT232H's
# D1 and D2 pins respectively.
try:
i2c: busio.I2C = busio.I2C(board.SCL, board.SDA)
except Exception as e:
print(f"Error initializing I2C bus: {e}")
print(
"Please ensure the FT232H is connected and drivers are "
"installed."
)
return
# Find the LCD's I2C address
i2c_address: typing.Optional[int] = find_i2c_device_address(i2c)
if i2c_address is None:
return
# Initialize the LCD
try:
lcd: Character_LCD_I2C = Character_LCD_I2C(
i2c, LCD_COLUMNS, LCD_ROWS, i2c_address=i2c_address
)
except Exception as e:
print(f"Error initializing LCD at address {hex(i2c_address)}: {e}")
return
# Turn on the backlight
lcd.backlight = True
# Display a message
# A nod to your audiophile interests!
line1: str = "Magnepan & Tidal"
line2: str = "Pure Planar Bliss"
message: str = f"{line1}\n{line2}"
print(f"Displaying message:\n{message}")
lcd.message = message
# Keep the message displayed for 10 seconds
try:
time.sleep(10)
except KeyboardInterrupt:
pass
finally:
# Clean up
lcd.backlight = False
lcd.clear()
print("\nScript finished. LCD cleared.")
if __name__ == "__main__":
main()
Unit Test for LCD Driver
"""
Unit tests for the LCD driver script.
This test suite uses mocks to simulate the hardware (I2C bus and LCD)
to allow for testing the application logic without requiring the physical
devices to be connected.
"""
import unittest
import typing
from unittest.mock import MagicMock, patch, call
# Mock the hardware modules before they are imported by the script under test
# This is crucial for running tests in an environment without the actual
# hardware or CircuitPython libraries installed.
mock_board: MagicMock = MagicMock()
mock_busio: MagicMock = MagicMock()
mock_lcd: MagicMock = MagicMock()
# Create a mock I2C class within the mock busio module
mock_i2c_instance: MagicMock = MagicMock()
mock_busio.I2C.return_value = mock_i2c_instance
# Create a mock LCD class within the mock adafruit_character_lcd module
mock_lcd_instance: MagicMock = MagicMock()
mock_lcd.Character_LCD_I2C.return_value = mock_lcd_instance
modules: dict[str, MagicMock] = {
'board': mock_board,
'busio': mock_busio,
'adafruit_character_lcd.character_lcd_i2c': mock_lcd,
}
with patch.dict('sys.modules', modules):
# Now we can import the script we want to test
from lcd_driver_script import (
main,
find_i2c_device_address,
LCD_COLUMNS,
LCD_ROWS,
)
class TestLcdDriver(unittest.TestCase):
"""
Test cases for the LCD driver script.
"""
def setUp(self) -> None:
"""
Reset mocks before each test.
"""
mock_i2c_instance.reset_mock()
mock_lcd_instance.reset_mock()
mock_busio.I2C.reset_mock()
mock_lcd.Character_LCD_I2C.reset_mock()
def test_find_i2c_device_address_found(self) -> None:
"""
Test that the I2C scan correctly returns a found address.
"""
# Configure the mock to return a dummy I2C address
expected_address: int = 0x27
mock_i2c_instance.scan.return_value = [expected_address]
# Call the function under test
address: typing.Optional[int] = find_i2c_device_address(
mock_i2c_instance
)
# Assert that the lock/unlock sequence was called
mock_i2c_instance.try_lock.assert_called_once()
mock_i2c_instance.scan.assert_called_once()
mock_i2c_instance.unlock.assert_called_once()
# Assert that the correct address was returned
self.assertEqual(address, expected_address)
def test_find_i2c_device_address_not_found(self) -> None:
"""
Test that the I2C scan returns None when no device is found.
"""
# Configure the mock to return an empty list
mock_i2c_instance.scan.return_value = []
# Call the function under test
address: typing.Optional[int] = find_i2c_device_address(
mock_i2c_instance
)
# Assert that the correct value (None) was returned
self.assertIsNone(address)
@patch('lcd_driver_script.find_i2c_device_address')
@patch('time.sleep')
def test_main_full_success_path(
self, mock_sleep: MagicMock, mock_find_address: MagicMock
) -> None:
"""
Test the main function's successful execution path.
"""
# Configure the mock to return a dummy I2C address
test_address: int = 0x3F
mock_find_address.return_value = test_address
# Run the main function
main()
# Verify that the I2C bus was initialized
mock_busio.I2C.assert_called_once_with(
mock_board.SCL, mock_board.SDA
)
# Verify that the LCD was initialized with the correct parameters
mock_lcd.Character_LCD_I2C.assert_called_once_with(
mock_i2c_instance, LCD_COLUMNS, LCD_ROWS,
i2c_address=test_address
)
# Verify the sequence of operations on the LCD instance
self.assertEqual(mock_lcd_instance.mock_calls, [
call.backlight(True),
call.message('Magnepan & Tidal\nPure Planar Bliss'),
call.backlight(False),
call.clear()
])
# Verify that sleep was called
mock_sleep.assert_called_once_with(10)
@patch('lcd_driver_script.find_i2c_device_address')
def test_main_no_device_found(self, mock_find_address: MagicMock) -> None:
"""
Test the main function's behavior when no I2C device is found.
"""
# Configure the mock to simulate no device being found
mock_find_address.return_value = None
# Run the main function
main()
# Assert that the LCD constructor was never called
mock_lcd.Character_LCD_I2C.assert_not_called()
# Assert that no message was set on the LCD instance
mock_lcd_instance.message.assert_not_called()
if __name__ == '__main__':
unittest.main()
No comments:
Post a Comment