From e6abe360f10ac18adfe609dcc8aac9e61e8698c9 Mon Sep 17 00:00:00 2001 From: George Sexton Date: Sun, 25 May 2025 15:12:19 -0600 Subject: [PATCH 1/2] mcp472x: Initial Add --- mcp472x/example_test.go | 47 +++ mcp472x/mcp472x.go | 382 ++++++++++++++++++++++ mcp472x/mcp472x_test.go | 402 ++++++++++++++++++++++++ mcp472x/mcp472xbustestrecording_test.go | 44 +++ 4 files changed, 875 insertions(+) create mode 100644 mcp472x/example_test.go create mode 100644 mcp472x/mcp472x.go create mode 100644 mcp472x/mcp472x_test.go create mode 100644 mcp472x/mcp472xbustestrecording_test.go diff --git a/mcp472x/example_test.go b/mcp472x/example_test.go new file mode 100644 index 0000000..26090c0 --- /dev/null +++ b/mcp472x/example_test.go @@ -0,0 +1,47 @@ +// 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. + +package mcp472x_test + +import ( + "log" + "time" + + "periph.io/x/conn/v3/i2c/i2creg" + "periph.io/x/conn/v3/physic" + "periph.io/x/devices/v3/mcp472x" + "periph.io/x/host/v3" +) + +// Example demonstrating how to initialize the MCP4728 and set an output +// voltage. +func Example() { + if _, err := host.Init(); err != nil { + log.Fatal("Error calling host.init()") + } + bus, err := i2creg.Open("") + if err != nil { + log.Fatal(err) + } + defer bus.Close() + // For a MCP4725, or to use VCC for the reference voltage, specify it as: + // 3_300 * physic.MilliVolt, etc. + dev, err := mcp472x.New(bus, mcp472x.DefaultAddress, mcp472x.MCP4728, mcp472x.MCP4728InternalRef) + if err != nil { + log.Fatal(err) + } + + // Program channel 3 to output 512mV + op := mcp472x.SetOutputParam{DAC: 3, V: 512 * physic.MilliVolt, UseInternalRef: true} + err = dev.SetOutput(op) + if err != nil { + log.Println(err) + } + time.Sleep(10 * time.Second) + + // Power down the channel + op.V = 0 + op.PDMode = mcp472x.PDMode1K + _ = dev.SetOutput(op) +} diff --git a/mcp472x/mcp472x.go b/mcp472x/mcp472x.go new file mode 100644 index 0000000..41370a5 --- /dev/null +++ b/mcp472x/mcp472x.go @@ -0,0 +1,382 @@ +// 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(¶ms[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(¶m) + 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(¶m) + 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) +} diff --git a/mcp472x/mcp472x_test.go b/mcp472x/mcp472x_test.go new file mode 100644 index 0000000..e8704a3 --- /dev/null +++ b/mcp472x/mcp472x_test.go @@ -0,0 +1,402 @@ +// 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. + +package mcp472x + +import ( + "encoding/json" + "math" + "testing" + "time" + + "periph.io/x/conn/v3/i2c/i2ctest" + "periph.io/x/conn/v3/physic" +) + +var liveDevice bool + +func getDev(testName string, variant Variant, vRef physic.ElectricPotential) (*Dev, error) { + addr := DefaultAddress + if variant == MCP4725 { + addr = 0x62 + } + d, err :=New(&i2ctest.Playback{Ops:recordingData[testName], DontPanic: true},addr,variant,vRef) + return d, err +} + +func TestBasic(t *testing.T) { + d, err := New(nil, 0, MCP4728, MCP4728InternalRef) + if err != nil { + t.Fatal(err) + } + + s := d.String() + if len(s) == 0 { + t.Error("expected string received \"\"") + } +} + +func TestPotentialToCount(t *testing.T) { + + d, err := New(nil, 0, MCP4728, MCP4728InternalRef) + if err != nil { + t.Fatal(err) + } + + count, boost, err := d.PotentialToCount(0) + if err != nil { + t.Error(err) + } + if count != 0 { + t.Errorf("v=0, count=%d", count) + } + if boost { + t.Errorf("v=0 boost=%t", boost) + } + + count, boost, err = d.PotentialToCount(MCP4728InternalRef) + if err != nil { + t.Error(err) + } + if count != maxCount { + t.Errorf("v=%s, count=%d", MCP4728InternalRef, count) + } + if boost { + t.Errorf("count=%d boost=%t", count, boost) + } + count, boost, _ = d.PotentialToCount(MCP4728InternalRef >> 1) + if count != (stepCount >> 1) { + t.Errorf("invalid count expected %d received %d", (stepCount>>1)-1, count) + } + if boost { + t.Errorf("count=%d boost=%t", count, boost) + } + + _, _, err = d.PotentialToCount(physic.ElectricPotential(-1)) + if err == nil { + t.Error("expected error on negative voltage") + } + _, _, err = d.PotentialToCount(3 * MCP4728InternalRef) + if err == nil { + t.Error("expected error on out of range voltage") + } + _, boost, _ = d.PotentialToCount(4 * physic.Volt) + if !boost { + t.Error("expected boost for 4v output") + } + d, _ = New(nil, 0, MCP4725, 3300*physic.MilliVolt) + _, _, err = d.PotentialToCount(5 * physic.Volt) + if err == nil { + t.Error("expected error on out of range voltage") + } +} + +func TestOutputParams4725(t *testing.T) { + d, err := New(nil, 0, MCP4725, 3_300*physic.MilliVolt) + if err != nil { + t.Fatal(err) + } + testCases := []struct { + outputParam SetOutputParam + expectedBytes []byte + }{ + {SetOutputParam{DAC: 0, + V: 0, + PDMode: PDMode1K, + }, + []byte{0x42, 0x0, 0x0}, + }, + {SetOutputParam{DAC: 1, + V: MCP4728InternalRef >> 1, + PDMode: PDMode500K, + }, + []byte{0x46, 0x4f, 0x70}, + }, + {SetOutputParam{DAC: 2, + V: MCP4728InternalRef >> 2, + PDMode: PDModeNormal, + }, + []byte{0x40, 0x27, 0xb0}, + }, + {SetOutputParam{DAC: 3, + V: MCP4728InternalRef >> 9, + PDMode: PDMode100K, + }, + []byte{0x44, 0x0, 0x50}, + }, + } + for _, tc := range testCases { + bytes := d.paramToBytes(&tc.outputParam) + if len(bytes) == len(tc.expectedBytes) { + for ix := range bytes { + if bytes[ix] != tc.expectedBytes[ix] { + t.Errorf("for OutputParam %s, byte %d got 0x%x %.8b expected 0x%x %.8b", tc.outputParam.String(), ix, bytes[ix], bytes[ix], tc.expectedBytes[ix], tc.expectedBytes[ix]) + } + } + } else { + t.Errorf("testcase %s expected %d bytes, received %d", tc.outputParam.String(), len(tc.expectedBytes), len(bytes)) + } + } + + // Test Equality + d1 := SetOutputParam{DAC: 1, V: physic.Volt, UseInternalRef: true, BoostGain: false} + d2 := SetOutputParam{DAC: 1, V: physic.Volt, UseInternalRef: true, BoostGain: false} + if !d1.Equal(d2) { + t.Errorf("expected d1==d2") + } + // Test Inequality + d1.V = physic.MilliVolt + if d1.Equal(d2) { + t.Errorf("expected d1!=d2") + } + + // Test the String method works and we can unmarshal the JSON into + // a new SetOutputParam + s2 := d2.String() + d3 := SetOutputParam{} + err = json.Unmarshal([]byte(s2), &d3) + if err != nil { + t.Error(err) + } + if !d3.Equal(d2) { + t.Errorf("Expected unmarshal from string to equal original\ns2=%s, d3=%s", s2, d3.String()) + } +} + +func TestOutputParams4728(t *testing.T) { + d, err := New(nil, 0, MCP4728, MCP4728InternalRef) + if err != nil { + t.Fatal(err) + } + testCases := []struct { + outputParam SetOutputParam + expectedBytes []byte + }{ + {SetOutputParam{DAC: 0, + V: 0, + BoostGain: false, + UseInternalRef: true, + PDMode: PDMode1K, + }, + []byte{0x0, 0xa0, 0x0}, + }, + {SetOutputParam{DAC: 1, + V: MCP4728InternalRef >> 1, + BoostGain: false, + UseInternalRef: true, + PDMode: PDMode500K, + }, + []byte{0x02, 0xe8, 0x00}, + }, + {SetOutputParam{DAC: 2, + V: MCP4728InternalRef >> 2, + PDMode: PDModeNormal, + }, + []byte{0x04, 0x04, 0x00}, + }, + {SetOutputParam{DAC: 3, + V: MCP4728InternalRef >> 6, + PDMode: PDMode100K, + }, + []byte{0x06, 0x40, 0x40}, + }, + } + for _, tc := range testCases { + bytes := d.paramToBytes(&tc.outputParam) + if len(bytes) == len(tc.expectedBytes) { + for ix := range bytes { + if bytes[ix] != tc.expectedBytes[ix] { + t.Errorf("for OutputParam %s, byte %d got 0x%x expected 0x%x", tc.outputParam.String(), ix, bytes[ix], tc.expectedBytes[ix]) + } + } + } else { + t.Errorf("testcase %s expected %d bytes, received %d", tc.outputParam.String(), len(tc.expectedBytes), len(bytes)) + } + + } +} + +func testSetGetOutput(d *Dev, t *testing.T) { + outputs := make([]SetOutputParam, d.maxChannels) + for i := range physic.ElectricPotential(d.maxChannels) { + outputs[i] = SetOutputParam{DAC: byte(i), V: (i + 1) * 256 * physic.MilliVolt, UseInternalRef: true, PDMode: PDModeNormal} + t.Logf("outputs[%d]=%s", i, outputs[i].String()) + } + err := d.SetOutput(outputs...) + if err != nil { + t.Fatal(err) + } + + cur, eeprom, err := d.GetOutput() + if err != nil { + t.Error(err) + } + t.Logf("cur=%#v cur[0].V=%s", cur, cur[0].V.String()) + if len(cur) != d.maxChannels { + t.Errorf("expected %d channels of current output values", d.maxChannels) + } + + for ix, op := range outputs { + curChannel := cur[ix] + diffMilliVolt := math.Abs(float64((curChannel.V - op.V) / physic.MilliVolt)) + if diffMilliVolt > 2.0 { + t.Errorf("Channel %d Read after program. Expected < 2mV, got %f", op.DAC, diffMilliVolt) + } + } + + t.Logf("eeprom=%#v", eeprom) + if len(eeprom) != d.maxChannels { + t.Errorf("expected %d channels of eeprom output values", d.maxChannels) + } + if liveDevice { + time.Sleep(30 * time.Second) + } +} + +func testSetEEPROM(d *Dev, dac byte, t *testing.T) { + op := SetOutputParam{DAC: dac, V: 512 * physic.MilliVolt, UseInternalRef: true, PDMode: PDModeNormal} + for range 2 { + err := d.SetOutputWithSave(op) + if err != nil { + t.Fatal(err) + } + + cur, eeprom, err := d.GetOutput() + if err != nil { + t.Error(err) + } + curChannel := cur[op.DAC] + diffMilliVolt := math.Abs(float64((curChannel.V - op.V) / physic.MilliVolt)) + if diffMilliVolt > 2.0 { + t.Errorf("Read after program. Expected difference <2mV, got %f", diffMilliVolt) + } + + curEeprom := eeprom[op.DAC] + diffMilliVolt = math.Abs(float64((curEeprom.V - op.V) / physic.MilliVolt)) + if diffMilliVolt > 2.0 { + t.Errorf("Read EEPROM after program. Expected <2mV, got %f", diffMilliVolt) + } + if curEeprom.PDMode != op.PDMode { + t.Errorf("Read EEPROM after program. Expected PDMode %d, got %d", op.PDMode, curEeprom.PDMode) + } + op.V *= 2 + op.PDMode += 1 + } +} + +func testFastWrite(d *Dev, t *testing.T) { + t.Logf("testFastWrite(variant=%s)", d.variant) + + vals := make([]uint16, d.maxChannels) + t.Log("Writing 0V to all channels") + err := d.FastWrite(vals...) + if err != nil { + t.Fatal(err) + } + cur, _, err := d.GetOutput() + if err != nil { + t.Error(err) + } + for ix, val := range cur { + if val.V != 0 { + t.Errorf("Channel %d expected 0V, got %s", ix, val.V) + } + } + if liveDevice { + time.Sleep(10 * time.Second) + } + + // Now, write 768mV to all channels. + vTest := 768 * physic.MilliVolt + count, _, _ := d.PotentialToCount(vTest) + for ix := range len(vals) { + vals[ix] = count + } + t.Logf("Writing %s to all channels", vTest) + err = d.FastWrite(vals...) + if err != nil { + t.Fatal(err) + } + cur, _, err = d.GetOutput() + if err != nil { + t.Error(err) + } + for ix := range len(cur) { + diffMillivolts := math.Abs(float64((cur[ix].V - vTest) / physic.MilliVolt)) + if diffMillivolts > 2 { + t.Errorf("Channel %d expected %s, received %s", ix, vTest, cur[ix].V) + } + } + if liveDevice { + time.Sleep(10 * time.Second) + } +} + +func TestSetGetOutput4728(t *testing.T) { + d, err := getDev("TestSetGetOutput4728", MCP4728, MCP4728InternalRef) + if err != nil { + t.Fatal(err) + } + testSetGetOutput(d, t) + + err = d.SetOutput() + if err == nil || err != errInvalidInputCount { + t.Error("SetOutput() failed to error on zero outputs") + } + err = d.SetOutput(SetOutputParam{}, SetOutputParam{}, SetOutputParam{}, SetOutputParam{}, SetOutputParam{}) + if err == nil || err != errInvalidInputCount { + t.Error("SetOutput() failed to error on too many outputs") + } + +} + +func TestSetEEPROM4728(t *testing.T) { + d, err := getDev("TestSetEEPROM4728", MCP4728, MCP4728InternalRef) + if err != nil { + t.Fatal(err) + } + testSetEEPROM(d, 2, t) + + err = d.SetOutputWithSave() + if err == nil || err != errInvalidInputCount { + t.Error("SetOutputWithSave() failed to error on zero outputs") + } + err = d.SetOutputWithSave(SetOutputParam{}, SetOutputParam{}, SetOutputParam{}, SetOutputParam{}, SetOutputParam{}) + if err == nil || err != errInvalidInputCount { + t.Error("SetOutputWithSave() failed to error on too many outputs") + } + +} + +func TestFastWrite4728(t *testing.T) { + d, err := getDev("TestFastWrite4728", MCP4728, MCP4728InternalRef) + if err != nil { + t.Fatal(err) + } + testFastWrite(d, t) +} + +func TestSetGetOutput4725(t *testing.T) { + d, err := getDev("TestSetGetOutput4725", MCP4725, 3_300*physic.MilliVolt) + if err != nil { + t.Fatal(err) + } + testSetGetOutput(d, t) +} + +func TestSetEEPROM4725(t *testing.T) { + d, err := getDev("TestSetEEPROM4725", MCP4725, 3_300*physic.MilliVolt) + if err != nil { + t.Fatal(err) + } + testSetEEPROM(d, 0, t) +} + +func TestFastWrite4725(t *testing.T) { + d, err := getDev("TestFastWrite4725", MCP4725, 3_300*physic.MilliVolt) + if err != nil { + t.Fatal(err) + } + testFastWrite(d, t) +} diff --git a/mcp472x/mcp472xbustestrecording_test.go b/mcp472x/mcp472xbustestrecording_test.go new file mode 100644 index 0000000..c368cf4 --- /dev/null +++ b/mcp472x/mcp472xbustestrecording_test.go @@ -0,0 +1,44 @@ +// 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. + +package mcp472x + +import ( + "periph.io/x/conn/v3/i2c/i2ctest" +) + +// Auto-Generated by i2ctest.BusTest + +var recordingData = map[string][]i2ctest.IO{ + "TestSetGetOutput4728": { + {Addr: 0x60, W: []uint8{0x40, 0x82, 0x0, 0x2, 0x84, 0x0, 0x4, 0x86, 0x0, 0x6, 0x88, 0x0}}, + {Addr: 0x60, R: []uint8{0xc0, 0x82, 0x0, 0xc8, 0x82, 0x0, 0xd0, 0x84, 0x0, 0xd8, 0x84, 0x0, 0xe0, 0x86, 0x0, 0xe8, 0xa8, 0x0, 0xf0, 0x88, 0x0, 0xf8, 0x88, 0x0}}}, + "TestSetEEPROM4728": { + {Addr: 0x60, W: []uint8{0x5c, 0x84, 0x0}}, + {Addr: 0x60, R: []uint8{0x40, 0x82, 0x0, 0x4f, 0x7d, 0xff, 0x50, 0x84, 0x0, 0x5f, 0x7b, 0xff, 0x60, 0x84, 0x0, 0x6f, 0x7b, 0xff, 0x70, 0x88, 0x0, 0x7f, 0x77, 0xff}}, + {Addr: 0x60, R: []uint8{0xc0, 0x82, 0x0, 0xc8, 0x82, 0x0, 0xd0, 0x84, 0x0, 0xd8, 0x84, 0x0, 0xe0, 0x84, 0x0, 0xe8, 0x84, 0x0, 0xf0, 0x88, 0x0, 0xf8, 0x88, 0x0}}, + {Addr: 0x60, W: []uint8{0x5c, 0xa8, 0x0}}, + {Addr: 0x60, R: []uint8{0x40, 0x82, 0x0, 0x4f, 0x7d, 0xff, 0x50, 0x84, 0x0, 0x5f, 0x7b, 0xff, 0x60, 0xa8, 0x0, 0x6f, 0x57, 0xff, 0x70, 0x88, 0x0, 0x7f, 0x77, 0xff}}, + {Addr: 0x60, R: []uint8{0xc0, 0x82, 0x0, 0xc8, 0x82, 0x0, 0xd0, 0x84, 0x0, 0xd8, 0x84, 0x0, 0xe0, 0xa8, 0x0, 0xe8, 0xa8, 0x0, 0xf0, 0x88, 0x0, 0xf8, 0x88, 0x0}}}, + "TestFastWrite4728": { + {Addr: 0x60, W: []uint8{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}, + {Addr: 0x60, R: []uint8{0xc0, 0x80, 0x0, 0xc8, 0x82, 0x0, 0xd0, 0x80, 0x0, 0xd8, 0x84, 0x0, 0xe0, 0x80, 0x0, 0xe8, 0xa8, 0x0, 0xf0, 0x80, 0x0, 0xf8, 0x88, 0x0}}, + {Addr: 0x60, W: []uint8{0x6, 0x0, 0x6, 0x0, 0x6, 0x0, 0x6, 0x0}}, + {Addr: 0x60, R: []uint8{0xc0, 0x86, 0x0, 0xc8, 0x82, 0x0, 0xd0, 0x86, 0x0, 0xd8, 0x84, 0x0, 0xe0, 0x86, 0x0, 0xe8, 0xa8, 0x0, 0xf0, 0x86, 0x0, 0xf8, 0x88, 0x0}}}, + "TestSetGetOutput4725": { + {Addr: 0x62, W: []uint8{0x40, 0x13, 0xe0}}, + {Addr: 0x62, R: []uint8{0xc0, 0x13, 0xe0, 0x24, 0xf7}}}, + "TestSetEEPROM4725": { + {Addr: 0x62, W: []uint8{0x60, 0x27, 0xb0}}, + {Addr: 0x62, R: []uint8{0x40, 0x27, 0xb0, 0x6d, 0x84}}, + {Addr: 0x62, R: []uint8{0xc0, 0x27, 0xb0, 0x2, 0x7b}}, + {Addr: 0x62, W: []uint8{0x62, 0x4f, 0x70}}, + {Addr: 0x62, R: []uint8{0x42, 0x4f, 0x70, 0x4b, 0x8}}, + {Addr: 0x62, R: []uint8{0xc2, 0x4f, 0x70, 0x24, 0xf7}}}, + "TestFastWrite4725": { + {Addr: 0x62, W: []uint8{0x0, 0x0}}, + {Addr: 0x62, R: []uint8{0xc0, 0x0, 0x0, 0x24, 0xf7}}, + {Addr: 0x62, W: []uint8{0x3, 0xb9}}, + {Addr: 0x62, R: []uint8{0xc0, 0x3b, 0x90, 0x24, 0xf7}}}, +} From 11994ba117e5528ea3297f809cdb8a253dc6dc79 Mon Sep 17 00:00:00 2001 From: George Sexton Date: Sun, 25 May 2025 15:33:30 -0600 Subject: [PATCH 2/2] gofmt --- mcp472x/example_test.go | 2 +- mcp472x/mcp472x_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mcp472x/example_test.go b/mcp472x/example_test.go index 26090c0..55a34c1 100644 --- a/mcp472x/example_test.go +++ b/mcp472x/example_test.go @@ -25,7 +25,7 @@ func Example() { log.Fatal(err) } defer bus.Close() - // For a MCP4725, or to use VCC for the reference voltage, specify it as: + // For a MCP4725, or to use VCC for the reference voltage, specify it as: // 3_300 * physic.MilliVolt, etc. dev, err := mcp472x.New(bus, mcp472x.DefaultAddress, mcp472x.MCP4728, mcp472x.MCP4728InternalRef) if err != nil { diff --git a/mcp472x/mcp472x_test.go b/mcp472x/mcp472x_test.go index e8704a3..8564183 100644 --- a/mcp472x/mcp472x_test.go +++ b/mcp472x/mcp472x_test.go @@ -21,7 +21,7 @@ func getDev(testName string, variant Variant, vRef physic.ElectricPotential) (*D if variant == MCP4725 { addr = 0x62 } - d, err :=New(&i2ctest.Playback{Ops:recordingData[testName], DontPanic: true},addr,variant,vRef) + d, err := New(&i2ctest.Playback{Ops: recordingData[testName], DontPanic: true}, addr, variant, vRef) return d, err }