Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MCP4728 #66

Merged
merged 2 commits into from
May 21, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 84 additions & 40 deletions src/Switch-Arduino_MCP4728/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,53 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# SweepMe! driver
# *Module: Switch
# *Instrument: Arduino MCP4728

# SweepMe! device class
# Device: Arduino MCP4728

from __future__ import annotations

from pysweepme import EmptyDevice


class Device(EmptyDevice):
"""Base class to control MCP4728 boards through Arduino."""

description = """
<h3>Arduino MCP4728</h3>
<p>This driver allows to set output voltages at MCP 4728 boards with 12-bit resolution. It can control up to 8 boards, each with 4 pins.</p>
<p>
This driver allows to set output voltages at MCP 4728 boards with 12-bit resolution. It can control up to 8
boards, each with 4 pins.
</p>
<h4>Setup</h4>
<p>Load the Switch-Arduino_MCP.ino sketch onto your Arduino. Set <em>baudrate</em> to 115200 and <em>terminator</em> to "\n". Install the Adafruit_MCP4728 library on your Arduino.</p>
<p>
Load the Switch-Arduino_MCP.ino sketch onto your Arduino. Set <em>baudrate</em> to 115200 and
<em>terminator</em> to "\n". Install the Adafruit_MCP4728 library on your Arduino.
</p>
<h4>Parameters</h4>
<p>Set <em>Channel </em>to the pin number you want to set, or <em>all </em>to set all four channels. The voltage values must be passed as a colon-separated string: <em>1.0:2.5:0:4.2</em></p>
<p>The I&sup2;C address is set as integer 0-7, corresponding to the boards standard addresses 0x60-0x67 (HEX). You can check your devices' address by using an <a href="https://playground.arduino.cc/Main/I2cScanner/">I&sup2;C Scanner</a>.</p>
<p>The maximum voltage is defined by the Voltage reference, which can either be internal (2.048 V or 4.096 V by using 2x gain) or from an external source, e.g. the Arduino's 5 V or 3.3 V output. When using an external reference, the voltage must be given.</p>
<p>To use multiple MCPs, their I&sup2;C addresses need to be changed individually, as described <a href="https://github.com/jknipper/mcp4728_program_address">here</a>. Choose <em>Channel: all </em>and set a comma-separated list for <em>I2C Address: 0, 1, 2</em>.</p>
<p>
Set <em>Channel </em>to the pin number you want to set, or <em>all </em>to set all four channels. The
voltage values must be passed as a colon-separated string: <em>1.0:2.5:0:4.2</em>
</p>
<p>
The I&sup2;C address is set as integer 0-7, corresponding to the boards standard addresses 0x60-0x67 (HEX). You
can check your devices' address by using an
<a href="https://playground.arduino.cc/Main/I2cScanner/">I&sup2;C Scanner</a>.
</p>
<p>
The maximum voltage is defined by the Voltage reference, which can either be internal (2.048 V or 4.096 V by
using 2x gain) or from an external source, e.g. the Arduino's 5 V or 3.3 V output. When using an external
reference, the voltage must be given.
</p>
<p>
To use multiple MCPs, their I&sup2;C addresses need to be changed individually, as described
<a href="https://github.com/jknipper/mcp4728_program_address">here</a>. Choose <em>Channel: all </em>and
set a comma-separated list for <em>I2C Address: 0, 1, 2</em>.
</p>
"""

def __init__(self) -> None:
"""Initialize the Device Class."""
EmptyDevice.__init__(self)

self.shortname = "Arduino MCP4728"
Expand All @@ -60,6 +85,16 @@ def __init__(self) -> None:
"timeout": 5,
"baudrate": 115200,
}
self.port_str: str = ""
self.driver_name: str = ""
self.instance_key: str = ""

self.pin: int = 0
self.sweepmode: str = ""
self.reference_voltage: str = "Internal 4.096 V"
self.external_voltage: float = 5.0
self.value_list: list[float] = []
self.volt: float = 0

self.multi_pins = False
self.multi_mcp = False
Expand All @@ -71,7 +106,8 @@ def __init__(self) -> None:
"Output in %": "%",
}

def set_GUIparameter(self):
def set_GUIparameter(self) -> dict: # noqa: N802
"""Set standard GUI parameters for the SweepMe! software."""
return {
"Channel": ["0", "1", "2", "3", "all"],
"I2C Address": "0",
Expand All @@ -80,7 +116,8 @@ def set_GUIparameter(self):
"External voltage in V": 5.0,
}

def get_GUIparameter(self, parameter={}):
def get_GUIparameter(self, parameter: dict) -> None: # noqa: N802
"""Handle input from GUI."""
# Handle output pins
channel = parameter["Channel"]
if channel == "all":
Expand Down Expand Up @@ -116,18 +153,18 @@ def get_GUIparameter(self, parameter={}):
self.variables = []
self.units = []
for n in range(channel_num):
# If single channel, use the correct pin number
if channel_num == 1:
n = self.pin
self.variables.append(f"Channel{n}")
# If single channel, use the correct pin
pin = self.pin if channel_num == 1 else n
self.variables.append(f"Channel{pin}")
self.units.append(self.unit[self.sweepmode])

self.port_str = parameter["Port"]
self.driver_name = parameter["Device"]

""" here, semantic standard functions start that are called by SweepMe! during a measurement """

def connect(self):
def connect(self) -> None:
"""Initialize Arduino and register the device in the communication manager."""
# Set Name/Number of COM Port as key
self.instance_key = f"{self.driver_name}_{self.port_str}"

Expand All @@ -136,11 +173,13 @@ def connect(self):
self.port.read()
self.device_communication[self.instance_key] = "Connected"

def disconnect(self):
def disconnect(self) -> None:
"""Unregister the device from the communication manager."""
if self.instance_key in self.device_communication:
self.device_communication.pop(self.instance_key)

def configure(self):
def configure(self) -> None:
"""Configure the MCP4728 reference voltages."""
# Initialize single MCP4728 with given I2C address - multi MCPs are set in apply
if not self.multi_mcp:
self.set_address(self.addresses[0])
Expand All @@ -158,8 +197,8 @@ def configure(self):
self.set_vref(use_internal_vref=False)
self.max_voltage = self.external_voltage

def unconfigure(self):
# Set all outputs to 0
def unconfigure(self) -> None:
"""Set all outputs to 0 V after measurement finishes."""
if self.multi_pins:
for address in self.addresses:
self.set_address(address)
Expand All @@ -168,17 +207,20 @@ def unconfigure(self):
else:
self.set_voltage(self.pin, 0)

def apply(self):
def apply(self) -> None:
"""Set the voltages to the pins."""
if self.multi_pins:
# Receive values as list, split by ":" and convert to float
split_values = self.value.split(":")
self.value_list = list(map(float, split_values))
# Replace , with . to enable float conversion
dot_separated_values = self.value.replace(",", ".")
# Input values are separated by colons
sweep_values = dot_separated_values.split(":")
self.value_list = list(map(float, sweep_values))

# Adjust number of channels depending on number of boards and pins
number_of_channels = 4 * len(self.addresses) if self.multi_mcp else 4

if len(self.value_list) != number_of_channels:
msg = f"Incorrect number of voltages received. Expected {number_of_channels}, got {len(value_list)}"
msg = f"Incorrect number of voltages received. Expected {number_of_channels} got {len(self.value_list)}"
raise Exception(msg)

for n, value in enumerate(self.value_list):
Expand All @@ -188,7 +230,7 @@ def apply(self):
else:
volt_bit = self.voltage_to_12bit(value)

# Iterate through pin numbers (0-3) and initialize MCP for each pin=0
# Iterate through pins (0-3) and initialize MCP for each pin=0
pin = n % 4
if pin == 0:
mcp_num = int(n / 4)
Expand All @@ -211,24 +253,25 @@ def apply(self):

self.set_voltage(self.pin, volt_bit)

def call(self):
# Return values that are set to the pins
def call(self) -> list[float]:
"""Return the voltages set to the pins."""
return self.value_list if self.multi_pins else [self.volt]

""" here, convenience functions start """

def voltage_to_12bit(self, voltage, relative_voltage=False):
# Convert given voltage to integer between 0 and 4095 (max voltage)
def voltage_to_12bit(self, voltage: float, relative_voltage: bool = False) -> int:
"""Convert given voltage to integer between 0 and 4095 (max voltage)."""
voltage_bit = int(voltage / 100 * 4095) if relative_voltage else int(4095 / self.max_voltage * voltage)

if not 0 <= voltage_bit < 4096:
max_bit = 4095
if not 0 <= voltage_bit < max_bit:
msg = f"Voltage {voltage} out of bound. Either larger than max voltage or negative."
raise ValueError(msg)

return voltage_bit

def set_voltage(self, pin, bit_value):
# Send command to set voltage (in bit value) at given pin
def set_voltage(self, pin: int, bit_value: int) -> None:
"""Send command to set voltage (in bit) at given pin."""
command_string = f"CH{pin}={bit_value}"
self.port.write(command_string)

Expand All @@ -243,9 +286,10 @@ def set_voltage(self, pin, bit_value):
msg = f"Failed to set voltage of {bit_value} at pin {pin}. Arduino response: {ret}"
raise Exception(msg)

def set_address(self, address: int):
# Initialize MCP at Arduino to receive further commands
if not 0 <= address <= 7:
def set_address(self, address: int) -> None:
"""Initialize MCP at Arduino to receive further commands."""
max_address = 7
if not 0 <= address <= max_address:
msg = "I2C Address must be 0-7"
raise Exception(msg)

Expand All @@ -267,8 +311,8 @@ def set_address(self, address: int):
msg = f"Failed to set address to {address}. Arduino response: {ret}"
raise Exception(msg)

def set_vref(self, use_internal_vref=True):
# Set reference voltage as internal or external
def set_vref(self, use_internal_vref: bool = True) -> None:
"""Set reference voltage as internal or external."""
v_ref = "I" if use_internal_vref else "E"

command_string = f"VR={v_ref}"
Expand All @@ -280,8 +324,8 @@ def set_vref(self, use_internal_vref=True):
msg = f"Failed to set reference voltage (vref). Arduino response: {ret}"
raise Exception(msg)

def set_gain(self, gain):
# For internal reference voltage, choose 1x or 2x gain
def set_gain(self, gain: int) -> None:
"""For internal reference voltage, choose 1x or 2x gain."""
if gain not in [1, 2]:
msg = "gain can only be 1 or 2"
raise ValueError(msg)
Expand Down
Loading