From 6d971f802e584df31e9e9e51809951f5e37e6e8c Mon Sep 17 00:00:00 2001 From: Thorsten von Eicken Date: Fri, 17 Nov 2023 18:21:04 -0800 Subject: [PATCH] INA233 power monitoring driver --- examples/drivers/sensors/ina233/main.ts | 33 +++ examples/drivers/sensors/ina233/manifest.json | 11 + modules/drivers/sensors/ina233/README.md | 32 +++ modules/drivers/sensors/ina233/ina233.ts | 241 ++++++++++++++++++ modules/drivers/sensors/ina233/manifest.json | 7 + 5 files changed, 324 insertions(+) create mode 100644 examples/drivers/sensors/ina233/main.ts create mode 100644 examples/drivers/sensors/ina233/manifest.json create mode 100644 modules/drivers/sensors/ina233/README.md create mode 100644 modules/drivers/sensors/ina233/ina233.ts create mode 100644 modules/drivers/sensors/ina233/manifest.json diff --git a/examples/drivers/sensors/ina233/main.ts b/examples/drivers/sensors/ina233/main.ts new file mode 100644 index 0000000000..f736fd4da8 --- /dev/null +++ b/examples/drivers/sensors/ina233/main.ts @@ -0,0 +1,33 @@ +// TI INA233 power monitor usage example +// Copyright © 2023 by Thorsten von Eicken. + +import Timer from "timer" +import Time from "time" +import INA233, { ADC, POLARITY } from "embedded:sensor/Power-Management/INA233" + +const sensor = new INA233({ + sensor: { + ...device.I2C.default, + io: device.io.SMBus, + }, + onError: err => trace(`INA233: ${err}\n`), + + shuntOhms: 0.003, + maxCurrent: 30, // max current in amps (default: 10A) + averaging: 64, // number of samples to average (1,4,16,64,...1024), default:1 + vTime: ADC.TIME_588, // voltage ADC conversion time (ADC.TIME_*), default:1.1ms + aTime: ADC.TIME_588, // current ADC conversion time (ADC.TIME_*), default:1.1ms + polarity: POLARITY.BOTH, // polarity (POLARITY.*), default:POLARITY.BOTH +}) + +trace(`INA233: sampleIval=${sensor.sampleInterval}us maxIval=${sensor.maxInterval(600)}ms\n`) + +const ticker = Timer.repeat(() => { + const sample = sensor.sample() + trace(`Sample: ${JSON.stringify(sample)}\n`) +}, 1000) + +Timer.set(() => { + Timer.clear(ticker) + trace("=== The END ===\n") +}, 20000) diff --git a/examples/drivers/sensors/ina233/manifest.json b/examples/drivers/sensors/ina233/manifest.json new file mode 100644 index 0000000000..7b791d5043 --- /dev/null +++ b/examples/drivers/sensors/ina233/manifest.json @@ -0,0 +1,11 @@ +{ + "include": [ + "$(MODDABLE)/examples/manifest_base.json", + "$(MODDABLE)/examples/manifest_typings.json", + "$(MODDABLE)/modules/io/manifest.json", + "$(MODDABLE)/modules/drivers/sensors/ina233/manifest.json" + ], + "modules": { + "*": ["./*.ts"] + } +} diff --git a/modules/drivers/sensors/ina233/README.md b/modules/drivers/sensors/ina233/README.md new file mode 100644 index 0000000000..55afc8dc61 --- /dev/null +++ b/modules/drivers/sensors/ina233/README.md @@ -0,0 +1,32 @@ +TI INA233 Power Monitor +======================= + +The INA233 is a power monitor chip that measures voltage and current and calculates power. +A special feature of this chip is to accumulate power internally allowing accurate +energy consumption measurements with relatively infrequent polling. + +TI product information: https://www.ti.com/product/INA233 + +This driver is intended to conform with ECMA419. + +The samples returned contain voltage, current, power, and energy measurements. +The energy is accumulated in the chip and must be cleared periodically (or can +be cleared automatically at every reading). + +Alerts/interrupts are not supported. + +### Power accumulator roll-over + +The INA233 has a 24-bit accumulator for power measurements and a 24-bit sample count register. +The `sample()` method must be called frequently enough to prevent the accumulator from +overflowing. +This interval can be calculated as follows: +- power = voltage * current (expected max value) +- current_LSB = current / 32768 (current resolution) +- adc_interval = voltage_ADC_conversion_time + current_ADC_conversion_time +- power_interval = adc_interval * number_of_averages +- power_increment = power / (25 * current_LSB) +- samples_to_overflow = 2^24 / power_increment +- time_to_overflow = samples_to_overflow * power_interval + +The `maxInterval()` method returns this value given the expected power consumption. diff --git a/modules/drivers/sensors/ina233/ina233.ts b/modules/drivers/sensors/ina233/ina233.ts new file mode 100644 index 0000000000..29515e7ab8 --- /dev/null +++ b/modules/drivers/sensors/ina233/ina233.ts @@ -0,0 +1,241 @@ +// TI INA233 power monitor driver +// Copyright © 2023 by Thorsten von Eicken. + +import Time from "time" +import TextDecoder from "text/decoder" + +// The INA233 uses a PMBus interface, which means it has commands that take/return +// data instead of having a set of registers. +const COMMANDS = { + CLEAR_FAULTS: 0x03, + RESTORE_DEFAULT_ALL: 0x12, + STATUS_MFR_SPECIFIC: 0x80, // read status (manufacturer specific) + READ_EIN: 0x86, // read energy measurement + READ_VIN: 0x88, // read voltage measurement + READ_IIN: 0x89, // read current measurement + READ_PIN: 0x97, // read power measurement + MFR_MODEL: 0x9a, // read model number ("INA233") + MFR_ADC_CONFIG: 0xd0, // read/write ADC configuration + READ_VSHUNT: 0xd1, // read raw shunt voltage + MFR_ALERT_MASK: 0xd2, // read/write alert mask + MFR_CALIBRATION: 0xd4, // read/write calibration + MFR_DEVICE_CONFIG: 0xd5, // read/write device configuration + CLEAR_EIN: 0xd6, // clear energy accumulator +} +Object.freeze(COMMANDS) + +export const ADC = { + TIME_140: 0, // 140us + TIME_204: 1, // 204us + TIME_332: 2, // 332us + TIME_588: 3, // 588us + TIME_1100: 4, // 1.1ms + TIME_2116: 5, // 2.116ms + TIME_4156: 6, // 4.156ms + TIME_8244: 7, // 8.244ms + AVG_1: 0, // 1 sample + AVG_4: 1, // 4 samples + AVG_16: 2, // 16 samples + AVG_64: 3, // 64 samples + AVG_128: 4, // 128 samples + AVG_256: 5, // 256 samples + AVG_512: 6, // 512 samples + AVG_1024: 7, // 1024 samples +} +Object.freeze(ADC) +const ADC_TIME = [140, 204, 332, 588, 1100, 2116, 4156, 8244] +Object.freeze(ADC_TIME) +const ADC_AVG = [1, 4, 16, 64, 128, 256, 512, 1024] +Object.freeze(ADC_AVG) + +export const POLARITY = { + BOTH: 0x00, // accumulate absolute value of power (default) + POSITIVE: 0x01, // accumulate only positive power (positive current) + NEGATIVE: 0x02, // accumulate only negative power (negative current) +} +Object.freeze(POLARITY) + +export interface Options { + sensor: { + io: any + address?: number + } + onError?: (error: string) => void + + // shutOhms and maxCurrent must be specified together, or neither + shuntOhms?: number // shunt resistor value in ohms (default: 0.01) + maxCurrent?: number // max current in amps (default: 10A) + // averaging, vTime and aTime must be specified together, or neither + averaging?: number // number of samples to average (1,4,16,64,...1024), default:1 + vTime?: number // voltage ADC conversion time (ADC.TIME_*), default:1.1ms + aTime?: number // current ADC conversion time (ADC.TIME_*), default:1.1ms + // polarity and clearEnergy must be specified together, or neither + polarity?: number // polarity (POLARITY.*), default:POLARITY.BOTH +} + +export interface Sample { + volts: number // voltage in volts + amps: number // current in amps + watts: number // average power in watts, since previous sample + joules: number // energy in watt-hours, since previous sample + w: number // instantaneous power in watts +} + +export default class INA233 { + #io + #einRaw = new Uint8Array(7) + #onError?: (error: string) => void + + #accum_ival = 1 + #iin_fct = 1 + #pin_fct = 25 + #e_at = 0 // ticks when energy accumulator was last read + #positive = true // sign of current accumulator + + constructor(options: Options) { + const io = (this.#io = new options.sensor.io({ + hz: 400_000, + address: 0x40, + ...options.sensor, + })) + + this.#onError = options.onError + + // verify that we're talking to an ina233 + const modelBuf = this.#io.readBuffer(COMMANDS.MFR_MODEL, 7) + const model = new TextDecoder().decode(new Uint8Array(modelBuf, 1)) + if (model != "INA233") { + this.#onError?.(`Wrong model: ${model}`) + this.close() + return + } + + this.#io.sendByte(COMMANDS.RESTORE_DEFAULT_ALL) + + // ensure certain configuration options get set now + if (options.shuntOhms == undefined) options.shuntOhms = 0.01 + if (options.maxCurrent == undefined) options.maxCurrent = 10 + if (options.averaging == undefined) options.averaging = 1 + + this.configure(options) + } + + close() { + this.#io?.close() + this.#io = null + } + + configure(options: Options) { + // configure the shunt resistor value + if (options.shuntOhms && options.maxCurrent) { + const currentLsb = options.maxCurrent / 32768 + const cal = Math.round(0.00512 / (currentLsb * options.shuntOhms)) + this.#io.writeUint16(COMMANDS.MFR_CALIBRATION, cal) + this.#iin_fct = currentLsb + this.#pin_fct = currentLsb * 25 + } + + // device config + if (options.polarity != undefined) { + const pol = (options.polarity || POLARITY.BOTH) & 0x03 + const clr = 6 // auto-clear accumulator and latch alerts + this.#io.writeUint8(COMMANDS.MFR_DEVICE_CONFIG, (pol << 4) | clr) + } + + // configure the ADC conversion times + if ( + options.vTime != undefined || + options.aTime != undefined || + options.averaging != undefined + ) { + const avg = (options.averaging ?? ADC.AVG_1) & 7 + const vTime = (options.vTime ?? ADC.TIME_1100) & 7 + const aTime = (options.aTime ?? ADC.TIME_1100) & 7 + this.#accum_ival = (ADC_TIME[vTime] + ADC_TIME[aTime]) * ADC_AVG[avg] // usec + const mode = 0x07 // continuous mode + const v = (avg << 9) | (vTime << 6) | (aTime << 3) | mode + this.#io.writeUint16(COMMANDS.MFR_ADC_CONFIG, v) + //trace(`INA233: ADC config 0x${v.toString(16)}\n`) + } + + // always clear the energy accumulator + this.#io.sendByte(COMMANDS.CLEAR_EIN) + this.#e_at = Time.ticks + } + + // given a steady-state power consumption, calculate the max interval for calling + // sample() in ticks (milliseconds) + maxInterval(power: number): number { + const samples = Math.floor(2 ** 24 / (power / this.#pin_fct)) + const ticks = (samples * this.#accum_ival) / 1000 + return ticks + } + + // return the sampling interval in microseconds + get sampleInterval(): number { + return this.#accum_ival + } + + // clear energy accumulator + clear(): void { + this.#io.write(COMMANDS.CLEAR_EIN) + this.#e_at = Time.ticks + } + + td_sum = 0 + td_count = 0 + + sample(s?: Object): Sample | undefined { + // read raw data + const t0 = Time.ticks + const status = this.#io.readUint8(COMMANDS.STATUS_MFR_SPECIFIC) + if ((status & 0x80) == 0) return undefined // no new data + + const vin = this.#io.readUint16(COMMANDS.READ_VIN) + const iin = (this.#io.readUint16(COMMANDS.READ_IIN) << 16) >> 16 + const pin = this.#io.readUint16(COMMANDS.READ_PIN) + const einRaw = new Uint8Array(7) + const e_at = Time.ticks + this.#io.readBuffer(COMMANDS.READ_EIN, einRaw) + if (einRaw[0] != 6) throw new Error("INA233: bad ein length") + const positive = this.#positive + this.#positive = iin >= 0 // applies to next sample + this.#io.writeUint8(COMMANDS.STATUS_MFR_SPECIFIC, 0xff) + + // put together accumulated energy reading + const count = (einRaw[6] << 16) | (einRaw[5] << 8) | einRaw[4] + const sum = (einRaw[3] << 16) | (einRaw[2] << 8) | einRaw[1] + const ein = count == 0 ? 0 : sum / count + const p_avg = ein * this.#pin_fct + const dt = e_at - this.#e_at + this.#e_at = e_at + + //trace(`INA233: ${dt}ms sum=${sum} cnt=${count}\n`) + + // return scaled values + const sample = (s ?? {}) as Sample + sample.volts = vin * 1.25e-3 // 1.25mV/LSB + sample.amps = iin * this.#iin_fct + sample.w = pin * this.#pin_fct + sample.watts = p_avg + sample.joules = dt == 0 ? 0 : (p_avg * dt) / 1000 + if (!positive) { + sample.w = -sample.w + sample.watts = -sample.watts + sample.joules = -sample.joules + } + // print some stats about how long this function takes + // const td = Time.ticks - t0 + // this.td_sum += td + // this.td_count++ + // if (this.td_count == 50) { + // trace(`INA233: ${(this.td_sum / this.td_count).toFixed(1)}ms\n`) + // this.td_sum = 0 + // this.td_count = 0 + // } + + // const vs = (this.#io.readUint16(COMMANDS.READ_VSHUNT) << 16) >> 16 + // trace(`INA233: Vs=${(vs * 0.0025).toFixed(3)}mV ${JSON.stringify(sample)}\n`) + return sample + } +} diff --git a/modules/drivers/sensors/ina233/manifest.json b/modules/drivers/sensors/ina233/manifest.json new file mode 100644 index 0000000000..6ceb80d076 --- /dev/null +++ b/modules/drivers/sensors/ina233/manifest.json @@ -0,0 +1,7 @@ +{ + "include": ["$(MODULES)/data/text/decoder/manifest.json"], + "modules": { + "embedded:sensor/Power-Management/INA233": "./ina233" + }, + "preload": ["embedded:sensor/Power-Management/INA233"] +}