Project: USB Volt/Ammeter
I have a Bitcoin mining rig consisting of a Raspberry Pi, a USB Hub and 4 Sapphire Block Erupter USB and was interested in determining how much power the entire setup was using. After finding the details of a similar project, I decided to construct my own version to monitor and log the power usage via USB.
Hardware
To accomplish(*) this, I used the following hardware:
- Atmel ATTiny45
- LDV1086V33 Voltage Regulator
- 50V 10µF Capacitor (x2)
- 68 ohm resistor (x2)
- Bi-color LED
- 2.2K ohm resistor
- USB plug
- Power resistor (* see note below)
- 1K ohm resistor
- 220 ohm resistor (x2)
- 10k potentiometer
- Barrel connector and plug
- Breadboard (82mm x 53mm)
- Altoids tin
The circuit isn't too complicated. The top half with the 2 capacitors and voltage regulator is for powering the MCU and USB communication. On the bottom (the blue highlighted section) are the components needed to determine the voltage/amperage of the sense input and feed the values into the ADC's on the ATTiny.
Sensing Voltage
In order to determine the voltage of our sense input, we need to translate it into a range that can feed into the A/D converter of the ATTiny. Since I am using the internal 2.56v reference voltage and I wanted to be able to measure voltages from 0-12v, that means using a voltage divider (R3 and POT2).
[jm_extra feature="vdc"]
R3 is 47K Ohms and POT2 is a 50K Ohm Potentiometer. By punching in 47000 for R1 into the calculator above, it will show that we need ~17.8K Ohms for R2. It also show us that we will get a resolution of 0.01 volts through our ADC.
Sensing Amperage
We now approach the part of the project that I know the least about. I (think) I understand the theory of what needs to happen, but I am not completely confident that I am implementing it correctly. This much I am pretty sure about: If I only wanted to measure voltage, I could just connect our voltmeter in parallel with our source circuit, but since I was more interested in seeing how much current I was drawing, it is necessary to place components in the path of our source. This is where the power resistor and Ohm's law comes into play. The power resistor is placed in the path between the - sense input and - sense output in the schematic above. According to Ohm's law and the relationship between current, resistance and voltage, the more current that is being drawn through the resistor should linearly increase the voltage.
The power resistor I chose for my project is this one. It is a .1 ohm resistor rated at 3W. So for every ampere that is drawn through the resistor, the voltage increases by .1 volts. Fed into the ADC of the ATTiny with a resolution of 10 bits and an internal 2.56v reference voltage, I should be able to effectively sample at ~4/100th of an amp. Please be sure to read the note below regarding my choice of resistor.
Software
V-USB is an awesome GNU GPL library for AVR-based MCUs that allows for low-speed USB communication and control with minimal additional hardware and low computational overhead. In short, it is a fabulous tool for getting inexpensive devices to talk to modern computers. Since I am not implementing anything overly magical, the firmware for the ATTiny and the host software are just modified versions of the hid-custom-request example.
/* Name: main.c * Project: hid-custom-rq example * Author: Christian Starkjohann * Creation Date: 2008-04-07 * Tabsize: 4 * Copyright: (c) 2008 by OBJECTIVE DEVELOPMENT Software GmbH * License: GNU GPL v2 (see License.txt), GNU GPL v3 or proprietary (CommercialLicense.txt) */ /* This example should run on most AVRs with only little changes. No special hardware resources except INT0 are used. You may have to change usbconfig.h for different I/O pins for USB. Please note that USB D+ must be the INT0 pin, or at least be connected to INT0 as well. We assume that an LED is connected to port B bit 0. If you connect it to a different port or bit, change the macros below: */ #define LED_PORT_DDR DDRB #define LED_PORT_OUTPUT PORTB #define GREEN_LED_BIT 1 #define RED_LED_BIT 5 #include <avr/io.h> #include <avr/wdt.h> #include <avr/eeprom.h> #include <avr/interrupt.h> /* for sei() */ #include <util/delay.h> /* for _delay_ms() */ #include <avr/pgmspace.h> /* required by usbdrv.h */ #include "usbdrv.h" #include "oddebug.h" /* This is also an example for using debug macros */ #include "requests.h" /* The custom request numbers we use */ /* ------------------------------------------------------------------------- */ /* ----------------------------- USB interface ----------------------------- */ /* ------------------------------------------------------------------------- */ PROGMEM const char usbHidReportDescriptor[22] = { /* USB report descriptor */ 0x06, 0x00, 0xff, // USAGE_PAGE (Generic Desktop) 0x09, 0x01, // USAGE (Vendor Usage 1) 0xa1, 0x01, // COLLECTION (Application) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x26, 0xff, 0x00, // LOGICAL_MAXIMUM (255) 0x75, 0x08, // REPORT_SIZE (8) 0x95, 0x01, // REPORT_COUNT (1) 0x09, 0x00, // USAGE (Undefined) 0xb2, 0x02, 0x01, // FEATURE (Data,Var,Abs,Buf) 0xc0 // END_COLLECTION }; uchar adc2_h; uchar adc2_l; uchar adc3_h; uchar adc3_l; unsigned int volts; unsigned int amps; unsigned int amps_array[20]; unsigned int amps_array_i=0; static uchar gdataBuffer[4]; /* The descriptor above is a dummy only, it silences the drivers. The report * it describes consists of one byte of undefined data. * We don't transfer our data through HID reports, we use custom requests * instead. */ static void calibrateOscillator(void) { uchar step = 128; uchar trialValue = 0, optimumValue; int x, optimumDev, targetValue = (unsigned)(1499 * (double)F_CPU / 10.5e6 + 0.5); /* do a binary search: */ do{ OSCCAL = trialValue + step; x = usbMeasureFrameLength(); // proportional to current real frequency if(x < targetValue) // frequency still too low trialValue += step; step >>= 1; }while(step > 0); /* We have a precision of +/- 1 for optimum OSCCAL here */ /* now do a neighborhood search for optimum value */ optimumValue = trialValue; optimumDev = x; // this is certainly far away from optimum for(OSCCAL = trialValue - 1; OSCCAL <= trialValue + 1; OSCCAL++){ x = usbMeasureFrameLength() - targetValue; if(x < 0) x = -x; if(x < optimumDev){ optimumDev = x; optimumValue = OSCCAL; } } OSCCAL = optimumValue; } void usbEventResetReady(void) { cli(); // usbMeasureFrameLength() counts CPU cycles, so disable interrupts. calibrateOscillator(); sei(); eeprom_write_byte(0, OSCCAL); // store the calibrated value in EEPROM } /* ------------------------------------------------------------------------- */ void doADC() { // // This is the routine that actually does the sampling. int amptotaler; int i; // // ADC2 is PB4, Pin 3, the voltage measurement ADMUX = (0 << ADLAR) | // left shift result (1 << REFS2) | // Sets ref. voltage to Int Ref 2.56, bit 1 (1 << REFS1) | // Sets ref. voltage to Int Ref 2.56, bit 1 (0 << REFS0) | // Sets ref. voltage to Int Ref 2.56, bit 0 (0 << MUX3) | // use ADC2 for input (PB4), MUX bit 3 (0 << MUX2) | // use ADC2 for input (PB4), MUX bit 2 (1 << MUX1) | // use ADC2 for input (PB4), MUX bit 1 (0 << MUX0); // use ADC2 for input (PB4), MUX bit 0 ADCSRA = (1 << ADEN) | // Enable ADC (1 << ADPS2) | // set prescaler to 64, bit 2 (0 << ADPS1) | // set prescaler to 64, bit 1 (1 << ADPS0); // set prescaler to 64, bit 0 _delay_ms(1); ADCSRA |= (1 << ADSC); // start ADC measurement while (ADCSRA & (1 << ADSC) ); // wait till conversion complete adc2_l=ADCL; adc2_h=ADCH; volts=((unsigned int)(adc2_h<<8) + adc2_l); // // ADC3 is PB3, Pin 2, the current measurement ADMUX = (0 << ADLAR) | // left shift result (1 << REFS2) | // Sets ref. voltage to Int Ref 2.56, bit 1 (1 << REFS1) | // Sets ref. voltage to Int Ref 2.56, bit 1 (0 << REFS0) | // Sets ref. voltage to Int Ref 2.56, bit 0 (0 << MUX3) | // use ADC3 for input (PB4), MUX bit 3 (0 << MUX2) | // use ADC3 for input (PB4), MUX bit 2 (1 << MUX1) | // use ADC3 for input (PB4), MUX bit 1 (1 << MUX0); // use ADC3 for input (PB4), MUX bit 0 ADCSRA = (1 << ADEN) | // Enable ADC (1 << ADPS2) | // set prescaler to 64, bit 2 (0 << ADPS1) | // set prescaler to 64, bit 1 (1 << ADPS0); // set prescaler to 64, bit 0 _delay_ms(1); ADCSRA |= (1 << ADSC); // start ADC measurement while (ADCSRA & (1 << ADSC) ); // wait till conversion complete adc3_l=ADCL; adc3_h=ADCH; gdataBuffer[0]=adc3_l; gdataBuffer[1]=adc3_h; gdataBuffer[2]=adc2_l; gdataBuffer[3]=adc2_h; // // Average of 20 consecutive samples amps_array[amps_array_i]=((unsigned int)(adc3_h<<8) + adc3_l); amps_array_i++; if (amps_array_i==20) {amps_array_i=0;} amptotaler=0; for (i=0; i<20; i++) { amptotaler+=amps_array[i]; } amps=(unsigned int)(amptotaler/20); } usbMsgLen_t usbFunctionSetup(uchar data[8]) { usbRequest_t *rq = (void *)data; if((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_VENDOR){ DBG1(0x50, &rq->bRequest, 1); /* debug output: print our request */ if(rq->bRequest == CUSTOM_RQ_SET_STATUS){ if (rq->wValue.bytes[0] == 1) { /* set LED GREEN */ LED_PORT_OUTPUT |= _BV(GREEN_LED_BIT); LED_PORT_OUTPUT &= ~_BV(RED_LED_BIT); } else if (rq->wValue.bytes[0] == 2) { /* set LED RED */ LED_PORT_OUTPUT |= _BV(RED_LED_BIT); LED_PORT_OUTPUT &= ~_BV(GREEN_LED_BIT); } else if (rq->wValue.bytes[0] == 3) { /* set LED GREEN AND RED*/ LED_PORT_OUTPUT |= _BV(RED_LED_BIT); LED_PORT_OUTPUT |= _BV(GREEN_LED_BIT); } else if (rq->wValue.bytes[0] == 0) { /* set LED OFF */ LED_PORT_OUTPUT &= ~_BV(RED_LED_BIT); LED_PORT_OUTPUT &= ~_BV(GREEN_LED_BIT); } } else if(rq->bRequest == CUSTOM_RQ_GET_STATUS) { // // In the original hd-custom-rq code, this function returned the status of the LEDs // Now, it returns the values of the ADCs static uchar dataBuffer[4]; /* buffer must stay valid when usbFunctionSetup returns */ //dataBuffer[0] = (unsigned short int)(amps); //dataBuffer[1] = (unsigned short int)(amps>>8); //dataBuffer[2] = (unsigned short int)(volts); //dataBuffer[3] = (unsigned short int)(volts>>8); usbMsgPtr = gdataBuffer; /* tell the driver which data to return */ return 4; /* tell the driver to send 1 byte */ } }else{ /* calss requests USBRQ_HID_GET_REPORT and USBRQ_HID_SET_REPORT are * not implemented since we never call them. The operating system * won't call them either because our descriptor defines no meaning. */ } return 0; /* default for not implemented requests: return no data back to host */ } /* ------------------------------------------------------------------------- */ int __attribute__((noreturn)) main(void) { uchar i; wdt_enable(WDTO_1S); /* Even if you don't use the watchdog, turn it off here. On newer devices, * the status of the watchdog (on/off, period) is PRESERVED OVER RESET! */ /* RESET status: all port bits are inputs without pull-up. * That's the way we need D+ and D-. Therefore we don't need any * additional hardware initialization. */ usbInit(); usbDeviceDisconnect(); /* enforce re-enumeration, do this while interrupts are disabled! */ i = 0; while(--i){ /* fake USB disconnect for > 250 ms */ wdt_reset(); _delay_ms(1); } usbDeviceConnect(); LED_PORT_DDR |= _BV(RED_LED_BIT); /* make the LED bit an output */ LED_PORT_DDR |= _BV(GREEN_LED_BIT); /* make the LED bit an output */ sei(); for(;;){ /* main event loop */ wdt_reset(); doADC(); usbPoll(); } } /* ------------------------------------------------------------------------- */
/* Name: set-led.c * Project: hid-custom-rq example * Author: Christian Starkjohann * Creation Date: 2008-04-10 * Tabsize: 4 * Copyright: (c) 2008 by OBJECTIVE DEVELOPMENT Software GmbH * License: GNU GPL v2 (see License.txt), GNU GPL v3 or proprietary (CommercialLicense.txt) */ /* General Description: This is the host-side driver for the custom-class example device. It searches the USB for the LEDControl device and sends the requests understood by this device. This program must be linked with libusb on Unix and libusb-win32 on Windows. See http://libusb.sourceforge.net/ or http://libusb-win32.sourceforge.net/ respectively. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <usb.h> /* this is libusb */ #include "opendevice.h" /* common code moved to separate module */ #include "../firmware/requests.h" /* custom request numbers */ #include "../firmware/usbconfig.h" /* device's VID/PID and names */ static void usage(char *name) { fprintf(stderr, "usage:n"); fprintf(stderr, " %s red ....... turn on RED LEDn", name); fprintf(stderr, " %s green ..... turn on GREEN LEDn", name); fprintf(stderr, " %s orange .... turn on RED and GREEN LEDn", name); fprintf(stderr, " %s off ....... turn off LEDsn", name); fprintf(stderr, " %s status .... ask current status of LEDn", name); } int main(int argc, char **argv) { usb_dev_handle *handle = NULL; const unsigned char rawVid[2] = {USB_CFG_VENDOR_ID}, rawPid[2] = {USB_CFG_DEVICE_ID}; char vendor[] = {USB_CFG_VENDOR_NAME, 0}, product[] = {USB_CFG_DEVICE_NAME, 0}; char buffer[4]; int cnt, vid, pid; usb_init(); if(argc < 2){ /* we need at least one argument */ usage(argv[0]); exit(1); } /* compute VID/PID from usbconfig.h so that there is a central source of information */ vid = rawVid[1] * 256 + rawVid[0]; pid = rawPid[1] * 256 + rawPid[0]; /* The following function is in opendevice.c: */ if(usbOpenDevice(&handle, vid, vendor, pid, product, NULL, NULL, NULL) != 0){ fprintf(stderr, "Could not find USB device "%s" with vid=0x%x pid=0x%xn", product, vid, pid); exit(1); } /* Since we use only control endpoint 0, we don't need to choose a * configuration and interface. Reading device descriptor and setting a * configuration and interface is done through endpoint 0 after all. * However, newer versions of Linux require that we claim an interface * even for endpoint 0. Enable the following code if your operating system * needs it: */ #if 0 int retries = 1, usbConfiguration = 1, usbInterface = 0; if(usb_set_configuration(handle, usbConfiguration) && showWarnings){ fprintf(stderr, "Warning: could not set configuration: %sn", usb_strerror()); } /* now try to claim the interface and detach the kernel HID driver on * Linux and other operating systems which support the call. */ while((len = usb_claim_interface(handle, usbInterface)) != 0 && retries-- > 0){ #ifdef LIBUSB_HAS_DETACH_KERNEL_DRIVER_NP if(usb_detach_kernel_driver_np(handle, 0) < 0 && showWarnings){ fprintf(stderr, "Warning: could not detach kernel driver: %sn", usb_strerror()); } #endif } #endif if(strcasecmp(argv[1], "status") == 0){ cnt = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, CUSTOM_RQ_GET_STATUS, 0, 0, buffer, sizeof(buffer), 5000); if(cnt < 4){ if(cnt < 0){ fprintf(stderr, "USB error: %sn", usb_strerror()); }else{ fprintf(stderr, "only %d bytes received.n", cnt); } }else{ printf("Buffer is %.2x %.2x %.2x %.2xn", buffer[0], buffer[1], buffer[2], buffer[3]); printf("Voltage is: %2.2fvn", ((float)( (unsigned int)(((unsigned char)buffer[3]<<8)+(unsigned char)buffer[2])*0.01171875 ) )); printf("Current is: %2.2fAn", ((float)((unsigned int)(((unsigned char)buffer[1]<<8)+(unsigned char)buffer[0])*.0025))); } } else if(strcasecmp(argv[1], "green") == 0) { // turn on GREEN LED cnt = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT, CUSTOM_RQ_SET_STATUS, 1, 0, buffer, 0, 5000); if(cnt < 0){ fprintf(stderr, "USB error: %sn", usb_strerror()); } } else if(strcasecmp(argv[1], "red") == 0) { // turn on RED LED cnt = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT, CUSTOM_RQ_SET_STATUS, 2, 0, buffer, 0, 5000); if(cnt < 0){ fprintf(stderr, "USB error: %sn", usb_strerror()); } } else if(strcasecmp(argv[1], "orange") == 0) { // turn on RED and GREEN LED cnt = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT, CUSTOM_RQ_SET_STATUS, 3, 0, buffer, 0, 5000); if(cnt < 0){ fprintf(stderr, "USB error: %sn", usb_strerror()); } } else if(strcasecmp(argv[1], "off") == 0) { // turn off LEDs cnt = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT, CUSTOM_RQ_SET_STATUS, 0, 0, buffer, 0, 5000); if(cnt < 0){ fprintf(stderr, "USB error: %sn", usb_strerror()); } } else{ usage(argv[0]); exit(1); } usb_close(handle); return 0; }
Notes
When I finished this project and connected to my Bitcoin mining rig, the resistor seemed to be getting hotter than I thought it should. The power usage for the set up should be around 3A at 5v, which would be around 15W. Re-reading the data sheet for the power resistor, I saw that it is only rated for 15W in overload power (sustainable for 5 seconds.) Oops. Rather than sourcing a new power resistor, I believe I am going to switch to a different method to sense the current draw. I will update the blog when I implement it.