joelmarchewka.com
15Dec/13Off

Project: USB Volt/Ammeter

20140220-140541.jpg

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:

USB Volt/AMmeter Project Schematic

USB Volt/AMmeter Project Schematic

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.

Comments (0) Trackbacks (0)

Sorry, the comment form is closed at this time.

Trackbacks are disabled.