Enkel USB-ansluten A/D-omvandlare
Postat: 17 mars 2010, 19:01:00
Detta projekt handlar om en USB-ansluten A/D-omvandlare till datorn. Den kan till exempel användas för att läsa av ljuset i rummet via ett LDR eller temperaturen via en analog tempgivare.
Projektet är absolut low-end, frågan är om det kan göras enklare och billigare?
Hårdvara:
Här används en AVR-processor, ATTiny25. Liten och billig och rätt söt. I övrigt består kretsen av några dioder, motstånd och kondensatorer, samt en USB-B-kontakt:
Mjukvara:
Mjukvaran består av två delar, en firmware för MCU:n samt ett litet program för PC:n. Det förstnämnda baseras på V-USB (http://www.obdev.at/products/vusb/index.html), här är huvudkoden (kräver ytterligare filer från VUSB för att fungera):
På PC-sidan körs Linux med Libusb, borde gå att få igång i Windows också:
Det är ungefär det... när PC-mjukvaran körs ser det ut som följer:
./ad adc2
Device connected
Reading adc2
buffer: 220 - 0
Programmet visar avläsningen från MCU:ns 10-bitars ADC, låg byte följt av hög byte. Detta kan naturligtvis snyggas till, annars får man räkna om för hand (Slå ihop ADCH och ACDL, omvandla till decimalt och ta gånger 0,0032).
Nogrannheten blir hyfsad, har mätt tex 0,70 volt med MCU:n när multimetern säger 0,75.
Projektet är absolut low-end, frågan är om det kan göras enklare och billigare?
Hårdvara:
Här används en AVR-processor, ATTiny25. Liten och billig och rätt söt. I övrigt består kretsen av några dioder, motstånd och kondensatorer, samt en USB-B-kontakt:
Mjukvara:
Mjukvaran består av två delar, en firmware för MCU:n samt ett litet program för PC:n. Det förstnämnda baseras på V-USB (http://www.obdev.at/products/vusb/index.html), här är huvudkoden (kräver ytterligare filer från VUSB för att fungera):
Kod: Markera allt
/* Name: main.c
The goal is to create the simplest possible USB device, this time with ADC-conversion.
Patrik Hermansson 2010
*/
#include "usbdrv.h"
#include <avr/eeprom.h> //Eeprom read
#include <avr/io.h>
#include <avr/wdt.h>
#include <avr/interrupt.h> //sei()
#include <avr/pgmspace.h>
#include <util/delay.h> //_delay_ms
#include <stdlib.h>
// Defines
#define UTIL_BIN4(x) (uchar)((0##x & 01000)/64 + (0##x & 0100)/16 + (0##x & 010)/4 + (0##x & 1))
#define UTIL_BIN8(hi, lo) (uchar)(UTIL_BIN4(hi) * 16 + UTIL_BIN4(lo))
#define ADVALUE2 0
#define ADVALUE3 1
#define ADVALUE4 2
/*
The ATTiny 25 has the following ADC inputs:
ADC0 PB5 - pin 1, used for reset.
ADC2 PB4 - pin 3
ADC3 PB3 - pin 2
ADC1 PB2 - pin 7, used for USB-connection.
ADC4 is internal temp sensor
Thus we can use ADC2, ADC3 and internal temp sensor ADC4.
*/
//uchar ADH, ADL;
/* ------------------------------------------------------------------------- */
/* Function that runs when data is requested from host */
uchar usbFunctionSetup(uchar data[8])
{
usbRequest_t *request= (usbRequest_t*) data;
//static uchar one= 1;
static uchar replyBuf[64];
usbMsgPtr = replyBuf;
switch (request->bRequest) {
case ADVALUE2:
//Set up ADC
ADMUX = UTIL_BIN8(0000, 0010);
// Vcc as Vref -> REFS0 = REFS1 = 0. REFS2 = X.
// ADLAR = 0 -> right adjusted.
// MUX3:0: 0010 -> PB4, pin3, ADC2.
// Start AD-conversion
ADCSRA |= (1 << ADSC);
//Wait while the conversion takes place
do {
}while ((ADCSRA & (1 << ADSC))) ;
//send AD-value to host(the computer)
replyBuf[0] = ADCL;
replyBuf[1] = ADCH;
return 2;
//Done
break;
case ADVALUE3:
PORTB&=~_BV(1); // led ON
// usbMsgLen_t len = 64; // we return up to 64 bytes
//Set up ADC, this is done in Init but changed if INTTEMP is called
ADMUX = UTIL_BIN8(0011, 0011);
// Vcc as Vref -> REFS0 = REFS1 = 0. REFS2 = X.
// ADLAR = 1 -> left adjusted.
// ADC3 selected.
// Start AD-conversion
// (ADSC = ADC Start conversion, ADCSRA = ADC control and Status Register #A)
ADCSRA |= (1 << ADSC);
//Wait while the conversion takes place
do {
}while ((ADCSRA & (1 << ADSC))) ;
//send AD-value to host(the computer)
replyBuf[0] = ADCL;
replyBuf[1] = ADCH;
return 2;
/*
len = 2;
//usbMsgPtr = ADCH;
usbMsgPtr = (uchar *)&ADCL;
//return ADCH;
return len;
*/
//Done
break;
case ADVALUE4:
PORTB|=_BV(1); // led OFF
/*
The on-chip temperature sensor is selected by writing the code "1111" to the MUX3..0 bits in
ADMUX register when the ADC4 channel is used as an ADC input. PDF chapter 17.12
*/
//Internal 1.1V reference, ADC4
ADMUX = UTIL_BIN8(1010, 1111);
// ADMUX = [REFS1 REFS0 ADLAR REFS2 MUX3 MUX2 MUX1 MUX0]. PDF chapter 17.13.1
// 1,1V internal Vref -> REFS0 = 0, REFS1 = 1. REFS2 = 0.
// ADLAR, ADC Left Adjust Result. ADLAR = 0 -> right adjusted.
// MUX3:0: Analog Channel and Gain Selection Bits. 1111 -> ADC4, internal temp sensor
// Start AD-conversion
// (ADSC = ADC Start conversion, ADCSRA = ADC control and Status Register #A)
ADCSRA |= (1 << ADSC);
//Wait while the conversion takes place
do {
}while ((ADCSRA & (1 << ADSC))) ;
//send AD-value to host(the computer)
replyBuf[0] = ADCL;
replyBuf[1] = ADCH;
return 2;
//Done
break;
}
return 0;
}
/* ------------------------------------------------------------------------- */
/* ------------------------ Oscillator Calibration ------------------------- */
/* ------------------------------------------------------------------------- */
/* Calibrate the RC oscillator to 8.25 MHz. The core clock of 16.5 MHz is
* derived from the 66 MHz peripheral clock by dividing. Our timing reference
* is the Start Of Frame signal (a single SE0 bit) available immediately after
* a USB RESET. We first do a binary search for the OSCCAL value and then
* optimize this value with a neighboorhod search.
* This algorithm may also be used to calibrate the RC oscillator directly to
* 12 MHz (no PLL involved, can therefore be used on almost ALL AVRs), but this
* is wide outside the spec for the OSCCAL value and the required precision for
* the 12 MHz clock! Use the RC oscillator calibrated to 12 MHz for
* experimental purposes only!
*/
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;
}
/*
Note: This calibration algorithm may try OSCCAL values of up to 192 even if
the optimum value is far below 192. It may therefore exceed the allowed clock
frequency of the CPU in low voltage designs!
You may replace this search algorithm with any other algorithm you like if
you have additional constraints such as a maximum CPU clock.
For version 5.x RC oscillators (those with a split range of 2x128 steps, e.g.
ATTiny25, ATTiny45, ATTiny85), it may be useful to search for the optimum in
both regions.
*/
void usbEventResetReady(void)
{
calibrateOscillator();
eeprom_write_byte(0, OSCCAL); /* store the calibrated value in EEPROM */
}
//static void timerInit(void)
//{
// TCCR1 = 0x0b; /* select clock: 16.5M/1k -> overflow rate = 16.5M/256k = 62.94 Hz */
// TCCR1 = Timer/Counter1 Control Register
//}
static void adcInit(void)
{
ADMUX = UTIL_BIN8(0001, 0011);
// ADMUX = [REFS1 REFS0 ADLAR REFS2 MUX3 MUX2 MUX1 MUX0]. PDF chapter 17.13.1
// Vcc as Vref -> REFS0 = REFS1 = 0. REFS2 = X.
// ADLAR, ADC Left Adjust Result. ADLAR = 1 -> left adjusted.
// MUX3:0: Analog Channel and Gain Selection Bits. 0011 -> PB3(pin 2), single ended.
/* enable ADC, not free running, interrupt disable, rate = 1/128 */
ADCSRA = UTIL_BIN8(1000, 0111);
/*
ADCSRA = ADC Control and Status Register A
[ADEN ADSC ADATE ADIF ADIE ADPS2 ADPS1 ADPS0]
-ADEN: The ADC is enabled by setting the ADC Enable bit, ADEN in ADCSRA
-ADSC: ADC Start Conversion. In Single Conversion mode, write this bit to one to start each conversion. In Free Running mode,
write this bit to one to start the first conversion. ADSC will read as one as long as a conversion is in progress. When the conversion is complete,
it returns to zero.
-ADATE: ADC Auto Trigger Enable. The ADC can be trigged by a signal.
-ADIF: ADC Interrupt Flag. This bit is set when an ADC conversion completes.
-ADIE: ADC Interrupt Enable. When this bit is written to one and the I-bit in SREG is set, the ADC Conversion Complete Interrupt is activated.
0 -> not activated.
-ADPS2:0: ADC Prescaler Select Bits. Division factor between system clock and ADC clock. 111 -> 128.
*/
// Disable digital input on PB3, ADC3
DIDR0 = 0x04;
}
/* Main */
uchar main(void)
{
// Set PB1 (pin 6) as output. DDRB - Port B Data Direction Register
DDRB= (1<<1);
uchar i;
uchar calibrationValue;
//Clock calibration setup
calibrationValue = eeprom_read_byte(0); /* calibration value from last time */
if(calibrationValue != 0xff){
OSCCAL = calibrationValue;
}
usbDeviceDisconnect();
for(i=0;i<20;i++){ /* 300 ms disconnect */
_delay_ms(15);
}
usbDeviceConnect();
usbInit();
//Init ADC
adcInit();
sei(); // enable interrupts
for(;;){ // main event loop
usbPoll(); // USB communication
}
//return 0;
}
Kod: Markera allt
/*
* V-USB-ADC
*
* Inspired by USB-relay, © Johannes Krude 2008 and USBTemp by Mathias Dalheimer
*
* This software makes it possible to use a simple ATTiny25-based design to read in two analogue values.
* It's also possible to read the MCU:s internal temperature sensor.
*
* © Patrik Hermansson 2010
*
* V-USB-ADC is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* V-USB-ADC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with V-USB-ADC. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <usb.h>
#include "lib/obdev_usb.h"
#define USB_SUCCESS 0
#define USB_ERROR_NOTFOUND 1
#define USB_ERROR_ACCESS 2
#define USB_ERROR_IO 3
// Number of attempts to complete an USB operation before giving up.
#define USB_MAX_RETRIES 3
#define USBDEV_SHARED_VENDOR 0xc016 /* VOTI */
#define USBDEV_SHARED_PRODUCT 0xdf05 /* Obdev's free shared PID */
/* Use obdev's generic shared VID/PID pair and follow the rules outlined
* in firmware/usbdrv/USBID-License.txt.
*/
#define USB_TIMEOUT 1000
/* These are the vendor specific SETUP commands implemented by our USB device */
#define USBADC2 0
#define USBADC3 1
#define USBADC4 2
static char str_error[128]= {0};
int i, v, r, adcl, adch;
int number;
unsigned char action;
void usage()
{
printf("Shows A/D-values from MCU\n");
printf("usage: ./ad adc2|adc3|adc4 \n");
exit(1);
}
int main (int argc, char *argv[])
{
usb_dev_handle *handle = NULL;
unsigned char buffer[8];
int nBytes;
// Check number of arguments given at comand line
if (argc != 2) {
usage();
}
//Initialize and find the correct hardware
usb_init();
char attempt=0, error=0;
do {
attempt++;
error=usbOpenDevice(&handle, USBDEV_SHARED_VENDOR, "patrikhermansson.se", USBDEV_SHARED_PRODUCT, "usb-test");
if(error != 0){
fprintf(stderr, "Could not open USB device \"usbtemp\" with vid=0x%x pid=0x%x, retrying\n", USBDEV_SHARED_VENDOR, USBDEV_SHARED_PRODUCT);
sleep(2*attempt);
}
else {
printf ("Device connected\n");
}
} while (error && attempt < USB_MAX_RETRIES);
if (error) {
fprintf(stderr, "Permanent problem opening USBTemp device. Giving up.\n");
exit(1);
}
// Check which argument given and act on that
if(strcmp(argv[1], "adc2") == 0){
printf("Reading %s\n", argv[1]);
nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, USBADC2, 0, 0, (char *)buffer, sizeof(buffer), 5000);
adcl = buffer[0];
adch = buffer[1];
printf ("buffer: %i - %i\n", buffer[0], buffer[1]);
}
else if(strcmp(argv[1], "adc3") == 0){
printf("Reading %s\n", argv[1]);
nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, USBADC3, 0, 0, (char *)buffer, sizeof(buffer), 5000);
adcl = buffer[0];
adch = buffer[1];
printf ("buffer: %i - %i\n", buffer[0], buffer[1]);
}
else if(strcmp(argv[1], "adc4") == 0){
printf("Reading %s\n", argv[1]);
nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, USBADC4, 0, 0, (char *)buffer, sizeof(buffer), 5000);
adcl = buffer[0];
adch = buffer[1];
printf ("buffer: %i - %i\n", buffer[0], buffer[1]);
}
// No correct argument was found
else {
usage();
}
if (nBytes < 0) {
snprintf(str_error, sizeof(str_error), "usb error: %s", usb_strerror());
return i;
}
return 0;
}
./ad adc2
Device connected
Reading adc2
buffer: 220 - 0
Programmet visar avläsningen från MCU:ns 10-bitars ADC, låg byte följt av hög byte. Detta kan naturligtvis snyggas till, annars får man räkna om för hand (Slå ihop ADCH och ACDL, omvandla till decimalt och ta gånger 0,0032).
Nogrannheten blir hyfsad, har mätt tex 0,70 volt med MCU:n när multimetern säger 0,75.