You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
devices/mcp472x/mcp472x.go

383 lines
11 KiB
Go

// Copyright 2025 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
// This package provides a driver for the Microchip MCP472x Series of Digital
// to Analog converters. It works with the MCP4725 and MCP4728 chips. The
// MCP4728 shares a common command interface, but offers 4 outputs and has
// a precision internal voltage reference. The MCP4725 uses VCC for vRef.
//
// # Datasheets
//
// # MCP4725
//
// https://ww1.microchip.com/downloads/en/devicedoc/22039d.pdf
//
// # MCP4728
//
// https://www.digikey.com/htmldatasheets/production/623709/0/0/1/mcp4728.html
package mcp472x
import (
"encoding/json"
"errors"
"fmt"
"time"
"periph.io/x/conn/v3/i2c"
"periph.io/x/conn/v3/physic"
)
// Variant represents the model of the device.
type Variant string
const (
// The internal precision reference for the MCP4728
MCP4728InternalRef physic.ElectricPotential = 2048 * physic.MilliVolt
MCP4725 Variant = "MCP4725"
MCP4728 Variant = "MCP4728"
stepCount = 1 << 12 // 12-bit D/A
maxCount = stepCount - 1
// DefaultAddress is the default I²C address (0x60) for MCP472x devices.
DefaultAddress i2c.Addr = 0x60
// Number of output channels for each model.
Channels4728 = 4
Channels4725 = 1
boostBit byte = 0x10
busyFlag byte = 0x80
cmdInternalRef byte = 0x80
cmdMultiWrite byte = 0x40
cmdSingleWrite byte = 0x08
cmdWriteWithSave4725 byte = 0x60
cmdWriteWithSave4728 byte = 0x50
dacMask byte = 0x03
pdMask byte = 0x03
)
var (
errInvalidVoltage = errors.New("mcp472x: voltage out of range")
errBusy = errors.New("mcp472x: device busy")
errInvalidInputCount = errors.New("mcp472x: invalid number of inputs provided")
errInvalidVariant = errors.New("mcp472x: invalid variant")
)
// Channel PowerDown mode.
type PDMode byte
const (
PDModeNormal PDMode = iota
// The remaining values specify resistance value used to tie the output pin
// to ground.
PDMode1K
PDMode100K
PDMode500K
)
// Dev represents an MCP472X D/A converter.
type Dev struct {
d i2c.Dev
variant Variant
maxChannels int
vRef physic.ElectricPotential
}
// SetOutputParam represents a parameter for programming a DAC output
// channel.
type SetOutputParam struct {
// DAC is the D/A channel number. Always 0 for MCP4725.
DAC byte
// V is the voltage to set the output to. For external reference, the
// value can be 0-VCC. For the MCP4728 using the internal precision
// reference, the value can be 0 - (2 * MCP4728_INTERNAL_REF)
V physic.ElectricPotential
// For the MCP4728 using the internal reference, you can boost the
// gain of the output to 2 * MCP4728_INTERNAL_REF, at the cost of
// resolution. vOut cannot exceed VCC. If you need vOut > 3.3V,
// ensure VCC=5v.
BoostGain bool
// True to use the internal reference. Used only for MCP4728.
UseInternalRef bool
// Powerdown mode for this channel
PDMode PDMode
}
// New creates and returns a representation of a MCP472X Digital to Analog
// converter. vRef sets the reference voltage used by the device. The value of
// vRef is used to convert a voltage parameter to a count value that the
// device internally uses for settings.
//
// For the MCP4728, the internal 2.048v reference can be used, or you can
// specify that VCC be used. For the internal reference, you can use the default
// gain which will have full scale voltage be 0-2.048v, or set gain to 2, which
// will make output be from 0-4.096v. Note that VCC must be 5V in this case.
//
// For the MCP4725, VCC is used as vRef.
func New(bus i2c.Bus, addr i2c.Addr, variant Variant, vRef physic.ElectricPotential) (*Dev, error) {
if variant != MCP4725 && variant != MCP4728 {
return nil, errInvalidVariant
}
d := &Dev{
d: i2c.Dev{Bus: bus, Addr: uint16(addr)},
variant: variant,
vRef: vRef,
maxChannels: Channels4728,
}
if variant == MCP4725 {
d.maxChannels = Channels4725
}
return d, nil
}
// PotentialToCount converts the specified voltage to the count for the
// A/D converter. It returns the required count, whether boostGain should
// be enabled. If the voltage is negative, or otherwise out of range, an
// error is returned. The count will roughly be v/(vRef/4095).
func (d *Dev) PotentialToCount(v physic.ElectricPotential) (uint16, bool, error) {
if (v < 0) || (v > d.vRef && d.variant == MCP4725) || (v > (2 * d.vRef)) {
return 0, false, errInvalidVoltage
}
boost := false
stepValue := d.vRef / maxCount
count := uint16(float64(v)/float64(stepValue) + 0.5)
if count > maxCount && d.variant == MCP4728 {
boost = true
count = count >> 1
}
return count, boost, nil
}
// Convert the current and EEPROM registers for a 4725. The bit structure is
// different between the two models...
func (d *Dev) convert4725(bytes []byte) ([]SetOutputParam, []SetOutputParam, error) {
busy := bytes[0]&busyFlag == 0x0
step := float64(d.vRef) / float64(maxCount)
count := float64((uint16(bytes[1]&0xf0) << 4) | (uint16(bytes[1]&0x0f) << 4) | (uint16(bytes[2]) >> 4))
op := SetOutputParam{V: physic.ElectricPotential(step * count)}
op.PDMode = PDMode((bytes[0] >> 1) & pdMask)
current := []SetOutputParam{op}
count = float64(uint16(bytes[4]) | (uint16(bytes[3]&0x0f) << 8))
op = SetOutputParam{V: physic.ElectricPotential(step * count)}
op.PDMode = PDMode((bytes[3] >> 5) & pdMask)
eeprom := []SetOutputParam{op}
var err error
if busy {
err = errBusy
}
return current, eeprom, err
}
// convert4728 converts the bytes read for current and EEPROM registers for
// an MCP4728 to SetOutputParams Refer to the datasheet for more information.
func (d *Dev) convert4728(bytes []byte) ([]SetOutputParam, []SetOutputParam, error) {
step := d.vRef / maxCount
current, eeprom := make([]SetOutputParam, 0), make([]SetOutputParam, 0)
pos := 0
vals := make([]SetOutputParam, 2)
busy := false
// A-D
for channelID := range d.maxChannels {
// Current Output Parameters, and EEPROM Parameters
for i := range 2 {
busy = busy || bytes[pos]&busyFlag == 0x0
count := physic.ElectricPotential(uint16(bytes[pos+2]) | uint16(bytes[pos+1]&0x0f)<<8)
pdMode := PDMode(bytes[pos+1] >> 5 & pdMask)
boost := bytes[pos+1]&boostBit == boostBit
vref := bytes[pos+1]&cmdInternalRef == cmdInternalRef
if boost && vref {
// Boost Gain is turned on, and it's using the internal reference, so
// double the count...
count *= 2
}
vals[i] = SetOutputParam{
DAC: byte(channelID),
V: physic.ElectricPotential(step * count),
PDMode: pdMode,
BoostGain: boost,
UseInternalRef: vref,
}
pos += 3
}
current = append(current, vals[0])
eeprom = append(eeprom, vals[1])
}
var err error
if busy {
err = errBusy
}
return current, eeprom, err
}
// FastWrite sends raw A/D count values to the converter. Bits 0-11 represent,
// the count, and bits 12 and 13 the PowerDown mode, if applicable. Use
// PotentialToCount to convert a specific voltage value to the count value.
//
// The number of values supplied must exactly match the number of channels
// supported by the device.
func (d *Dev) FastWrite(values ...uint16) (err error) {
if len(values) != d.maxChannels {
err = errInvalidInputCount
return
}
w := make([]byte, len(values)*2)
for ix, val := range values {
w[ix*2] = byte(val>>8) & 0x3f // mask off the two high bits.
w[ix*2+1] = byte(val & 0xff)
}
err = d.d.Tx(w, nil)
if err != nil {
err = fmt.Errorf("mcp472x: %w", err)
}
return
}
// GetOutput reads the configured output values and programmed EEPROM values,
// and returns the results. If the device signals it is busy with an EEPROM
// write, the function will retry up to 9 times to read values.
func (d *Dev) GetOutput() (current []SetOutputParam, eeprom []SetOutputParam, err error) {
var r []byte
bytesChannel := 5
if d.variant == MCP4728 {
bytesChannel = 6
}
r = make([]byte, bytesChannel*d.maxChannels)
for range 10 {
err = d.d.Tx(nil, r)
if err != nil {
err = fmt.Errorf("mcp472x: %w", err)
return
}
if d.variant == MCP4725 {
current, eeprom, err = d.convert4725(r)
} else {
current, eeprom, err = d.convert4728(r)
}
if err == nil {
break
}
// The device is busy with an eeprom write. Wait and try again.
time.Sleep(100 * time.Millisecond)
}
return
}
// SetOutput sets the output of the specified output channels. For an MCP4725, you may
// pass only one parameter.
func (d *Dev) SetOutput(params ...SetOutputParam) (err error) {
if len(params) == 0 || len(params) > d.maxChannels {
err = errInvalidInputCount
return
}
if d.variant == MCP4725 {
w := d.paramToBytes(&params[0])
err = d.d.Tx(w, nil)
if err != nil {
err = fmt.Errorf("mcp472x: %w", err)
}
return
}
var w []byte
w = make([]byte, 0)
for _, param := range params {
bytes := d.paramToBytes(&param)
w = append(w, bytes...)
}
w[0] |= cmdMultiWrite
err = d.d.Tx(w, nil)
if err != nil {
err = fmt.Errorf("mcp472x: error writing to device: %w", err)
}
return
}
// SetOutputWithSave sets the channel output values AND saves the values to
// EEPROM. On power-up, the output value will be set to the saved settings.
func (d *Dev) SetOutputWithSave(params ...SetOutputParam) (err error) {
if len(params) == 0 || len(params) > d.maxChannels {
err = errInvalidInputCount
return
}
var w []byte
for _, param := range params {
bytes := d.paramToBytes(&param)
w = append(w, bytes...)
}
if d.variant == MCP4725 {
w[0] |= cmdWriteWithSave4725
err = d.d.Tx(w, nil)
if err != nil {
err = fmt.Errorf("mcp472x: error writing to device: %w", err)
}
return
}
w[0] |= cmdWriteWithSave4728
if len(params) == 1 {
w[0] |= cmdSingleWrite
}
err = d.d.Tx(w, nil)
if err != nil {
err = fmt.Errorf("mcp472x: error writing to device: %w", err)
}
return
}
// Implements conn.Resource
func (d *Dev) Halt() error {
return nil
}
// String returns the variant name
func (d *Dev) String() string {
return string(d.variant)
}
// paramToBytes converts a SetOutputParam to the appropriate bit structure for
// write. The bit structure for writes is different depending on the variant.
func (d *Dev) paramToBytes(op *SetOutputParam) (bytes []byte) {
count, boost, _ := d.PotentialToCount(op.V)
if d.variant == MCP4725 {
b := cmdMultiWrite
b |= (byte(op.PDMode&PDMode(pdMask)) << 1)
bytes = append(bytes, b)
bytes = append(bytes, byte(count>>4)&0xff)
bytes = append(bytes, byte(count<<4)&0xf0)
return
}
// 4728
// set the DAC Channel bits, and UDAC
bytes = append(bytes, byte(((op.DAC&dacMask)<<1)&0xff))
b := byte(count>>8) & 0xff
if op.UseInternalRef {
b |= cmdInternalRef
}
if boost {
b |= boostBit
}
b |= byte(op.PDMode&PDMode(pdMask)) << 5
bytes = append(bytes, b)
bytes = append(bytes, byte(count&0xff))
return
}
// Equal compares two SetOutputParam values for equality.
func (op SetOutputParam) Equal(op2 SetOutputParam) bool {
return op.V == op2.V &&
op.BoostGain == op2.BoostGain &&
op.UseInternalRef == op2.UseInternalRef &&
op.PDMode == op2.PDMode
}
// String returns a JSON representation of the Output Parameter
func (op SetOutputParam) String() string {
bytes, _ := json.Marshal(&op)
return string(bytes)
}