Source file: /~heha/basteln/PC/SHT11/SHT11-HID-USB.zip/SHT11/sht11.c

/* Name: sht11.c (Temperatur- und Feuchtesensor mit SHT11)
 * Implementationsbeispiel für PnPSA auf ATtiny45
 * Tabsize: 8, Encoding: UTF-8, LineEnds: LF
 */

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/wdt.h>
#include <avr/sleep.h>
#include <util/delay.h>

#include "usbdrv.c"

/************
 * Hardware *
 ************
Eigenschaften des SHT11:
	Verkorkstes I²C als Schnittstelle
	Messbereich: -40 .. 120 °C, 0..100 % rH
	Auflösung: 14 bit (Temperatur), 12 bit (Feuchte)
	Genauigkeit: ± 0,4 K (Temperatur), ± 3 % (Feuchte) im mittleren Messbereich
	Zeitkonstante: 5..30 s (Temperatur), 8 s (Feuchte)
	Speisespannung: 2,4 .. 5,5 V @ 3 mW
Portpin-Zuordnung:			┌──┐ ┌──┐
PB0 (5) = Fokus-LED-Ausgang	!RESET	┤  └─┘	├ Ucc
PB1 (6) = USB D+		SCL	┤ ATMEL	├ USB D-
PB2 (7) = USB D-		SDA	┤tiny45	├ USB D+
PB3 (2) = SCK (immer Ausgang)	GND	┤	├ !Fokus
PB4 (3) = SDA (Richtung wechselnd)	└───────┘
*/

#define elemof(x) (sizeof(x)/sizeof(*(x)))
enum {false,true};
typedef uchar bool;

/* USB report descriptor */
#define W(x) (x)&0xFF,(x)>>8
#define D(x) W(x&0xFFFF),W(x>>16)

PROGMEM char usbHidReportDescriptor[USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH] = {
 0x06, W(0xFF00),	// G Usage Page (0xFF00 = hersteller-eigen)
 0x09, 0x01,		// L Usage (PnP-Sensor)
 0xa1, 0x01,		// M Collection (Application)
// Status- und Steuerbits
 0x85, 7,		// G  Report ID (7)
 0x15, 0,		// G  Logical Minimum (0)
 0x25, 1,		// G  Logical Maximum (1)
 0x75, 1,		// G  Report Size (1 bit)
 0x95, 4,		// G  Report Count (4 Bits)
 0x1A, W(0x220),	// L  Usage Minimum (Resolution Control)
 0x2A, W(0x223),	// L  Usage Maximum (Test Mode)
 0x89, 6,		// L  String Minimum (6 = Reduced resolution)
 0x99, 9,		// L  String Maximum (9 = Test Mode)
 0xB1, 0x02,		// M  Feature (Var,NoPreferred)
 
 0x95, 1,		// G  Report Count (1 Bit)
 0x09, 0xFF,		// L  Usage (Focus LED)
 0x79, 10,		// L  String Index (10 = Focus)
 0xB1, 0x02,		// M  Feature (Var,NoPreferred)

 0x09, 0xFE,		// L  Usage (Transmission Failed)
 0x79, 11,		// L  String Index (11 = Transmission Error)
 0xB1, 0x82,		// M  Feature (Var,NoPreferred,Volatile)

 0x0A, W(0x226),	// L  Usage (Low battery warning)
 0x79, 12,		// L  String Index (12 = End Of Battery)
 0xB1, 0x82,		// M  Feature (Var,NoPreferred,Volatile)

 0xB1, 0x03,		// M  Feature (Const)
// Temperatur 
 0x85, 3,		// G  Report ID (3)
 0x26, W(16380),	// G  Logical Maximum (16380)
 0x67, D(0x10001),	// G  Unit (Temperature in K)
 0x55, 0x0E,		// G  Unit Exponent (0.01 K)
 0x36, W(23315),	// G  Physical Minimum (-40.00 °C = 233.15 K)
 0x47, D(39695),	// G  Physical Maximum (123.80 °C = 396.95 K)
 0x75, 16,		// G  Report Size (16 bits)
 0x95, 1,		// G  Report Count (1 Wert)
 0x79, 4,		// L  String Index (4 = Air Temperature)
 0x09, 0xE7,		// L  Usage (Temperatur)
 0xB1, 0xA2,		// M  Feature (Var,NoPreferred,Volatile)
 0x85, 1,		// G  Report ID (1)
 0x79, 4,		// L  String Index (4 = Air Temperature)
 0x09, 0xE7,		// L  Usage (Temperatur)
 0x81, 0x22,		// M  Input (Var,NoPreferred)
// Feuchte
 0x85, 5,		// G  Report ID (5)
 0x26, W(3500),		// G  Logical Maximum (3500)
 0x65, 0x01,		// G  Unit (no dimension)
 0x55, 0x0C,		// G  Unit Exponent (0.0001, not in percent!)
 0x35, 0,		// G  Physical Minimum (0.0000 = 0.00 %)
 0x46, W(10000),	// G  Physical Maximum (1.0000 = 100.00 %)
 0x79, 5,		// L  String Index (5 = Relative Humidity)
 0x09, 0xEA,		// L  Usage (Feuchte)
 0xB1, 0xB2,		// M  Feature (Var,NoPreferred,NonLinear,Volatile)
 0x85, 1,		// G  Report ID (1)
 0x79, 5,		// L  String Index (5 = Relative Humidity)
 0x09, 0xEA,		// L  Usage (Feuchte)
 0x81, 0x22,		// M  Input (Var,NoPreferred)

 0xc0};			// M End Collection

#define WAIT wait();
#define SCKL {PORTB&=~0x08; WAIT;}
#define SCKH {PORTB|= 0x08; WAIT;}
#define SDAQ (PINB&0x10)
#define SDAL {PORTB&=~0x10; DDRB|=0x10; WAIT;}	// Low
#define SDAH {DDRB&=~0x10; PORTB|=0x10; WAIT;}	// HiZ

void wait(void) {
 _delay_us(2.5);	// seltsamerweise funktioniert es nict mit 1 µs!
}

// n==1: Sendet Bit 7, schiebt <b> nach links und empfängt Bit 0
// n==8: Sendet und empfängt 8 Bits
static uchar I2cBits(uchar b, uchar n) {
 do{
  if (b&0x80) SDAH else SDAL;
  SCKH;
  b<<=1;
  if (SDAQ) b|=1;
  SCKL;
 }while(--n);
 return b;
}

// Sendet 8 Bits, empfängt 1 Bit (Bit 0 vom Ergebnis, übrige Bits = 0)
static uchar I2cSend(uchar b) {
 I2cBits(b,8);	// ohne Kollision sollte wieder <b> rauskommen!
 return I2cBits(0x80,1);
}

// Empfängt 8 Bits, sendet 1 Bit (Bit 7 von <nak>)
static uchar I2cRecv(uchar nak) {
 uchar b=I2cBits(0xFF,8);
 I2cBits(nak,1);
 return b;
}

static volatile unsigned tick;
#define TIMEOUT 400	// ms

static void Yield(void);

EMPTY_INTERRUPT(PCINT0_vect);

static short WaitReady(void) {
 unsigned t=tick;
 GIFR=0x20;		// PCIF löschen
 GIMSK=0x60;		// USB und SDA
 PINB|=0x01;		// LED aufblitzen lassen
 while (SDAQ) {		// warten solange High
  if (tick-t>TIMEOUT) {
   GIMSK=0x40;		// nur USB
   return -2;
  }
  Yield();
 }
 PINB|=0x01;		// LED wieder aus
 GIMSK=0x40;		// nur USB
 return 0;
}

static void ConnectionReset(void) {
 uchar i;
 for (i=0;i<9;i++) {	// Startsequenz
  SCKH;
  WAIT;
  SCKL;
 }
}

register uchar crc_start asm("r2");
register uchar crc asm("r3");
register bool failed asm("r4");


static void TransmissionStart(void) {
 if (failed) ConnectionReset();
 SCKH;
 SDAL;
 SCKL;
 WAIT;
 SCKH;
 SDAH;
 SCKL;
}

static void cs_put(uchar x) {	// Prüfsummen-Maschinerie
 uchar i;
 for (i=0; i<8; i++) {
  crc=(crc>>1)^(((signed char)(x^(crc<<7))>>7)&0x8C);
  x<<=1;
 }
}

// Holt Messwert (code=3: Temperatur, code=5: Feuchte) oder Statusbits (code=7)
static short getVal(uchar code) {
 short w;
 uchar b;
 TransmissionStart();
 if (I2cSend(code)) {failed=true; return -1;}	// "Übertragungsfehler!"
 crc=crc_start;
 cs_put(code);
 if (WaitReady()==-2) {failed=true; return -2;}	// "Zeitüberschreitung!"
 w=b=I2cRecv(0);
 cs_put(b);
 if (code==7) {
  crc_start=b&0x0F;
 }else{
  b=I2cRecv(0);
  cs_put(b);
  w=(w<<8)|b;
 }
 b=I2cRecv(0xFF);
 failed = crc-b;			// "Prüfsummenfehler!"
 return w;
}

// Setzt Statusbits
static void setVal(uchar v) {
 TransmissionStart();
 failed = I2cSend(6) || I2cSend(v);	// "Übertragungsfehler!"
 if (!failed) crc_start=v&0x0F;
}

static uchar replyBuf[128] __attribute((section(".noinit")));

static uchar idleRate;		// in 4 ms, Minimum 100 ms via Deskriptor, 0=∞
static unsigned idleTimer __attribute((section(".noinit")));	// in 1 ms

static struct{
 uchar id;
 short temp,humi;
}inputReport __attribute((section(".noinit")));

static bool fillInputReport() {
 short w=getVal(3);
 if (w>=0) {
  inputReport.temp=w;
  w=getVal(5);
  if (w>=0) {
   inputReport.humi=w;
   return true;
  }
 }
 return false;
}

USB_PUBLIC uchar usbFunctionSetup(uchar data[8]) {
 usbRequest_t *rq = (void *)data;
/* class request type (x01x xxxx) */
 if ((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_CLASS){
  switch (rq->bRequest) {
   case USBRQ_HID_GET_REPORT: {
    if (rq->wValue.bytes[1]==3) {	// Feature
     uchar b=rq->wValue.bytes[0];	// wValueH: ReportType, wValueL: ReportID
     *replyBuf=b;
     bool f=failed;			// vorher
     short w=getVal(b);
     if (b==7) {
      if (f||failed) w|=0x20;		// Bit ohne Deskriptor
      if (!(PORTB&0x01)) w|=0x10;	// Fokus-LED-Bit (zz. unschön!)
     }
     *(short*)(replyBuf+1)=w;
     usbMsgPtr = replyBuf;
     return rq->wLength.bytes[0];
    }else{
     if (!idleRate && !fillInputReport()) break;
     usbMsgPtr=&inputReport.id;
     return sizeof inputReport;
    }
   }break;
   case USBRQ_HID_SET_REPORT: {
    if (rq->wValue.word=0x0307) return 0xFF;	// usbFunctionWrite() tut es
   }break;
   case USBRQ_HID_GET_IDLE: {
    usbMsgPtr = &idleRate;
    return 1;
   }break;
   case USBRQ_HID_SET_IDLE: {
    idleRate = rq->wValue.bytes[1];	// wValueH
    idleTimer = 1;		// sofort Wert auslesen (Doku richtig gelesen?)
   }break;
  }
 }
 return 0;
}

USB_PUBLIC uchar usbFunctionWrite(uchar *data, uchar len) {
 if (len==2 && data[0]==7) {	// nur für einziges Feature-Out
  setVal(data[1]&0xCF);
  if (data[1]&0x10) PORTB&=~0x01; else PORTB|=0x01;	// Fokus-LED schalten
  return 1;		// OK
 }
 return 0xFF;		// STALL
}

#include <stdlib.h>	// wchar_t

// itoa() ist zu fett! Daher diese Hex-Ersatzroutine.
static wchar_t hexDigit(uint16_t sn) {
 sn>>=12;
 if (sn>=10) sn+=7;
 sn+='0';
 return sn;
}
// Die 16-Bit-Seriennummer liegt diesmal an EEPROM-Adresse FFF8.
// Damit ist Platz für 32 Bits sowie für den avrdude-Zykluszähler
static wchar_t* insertSerial(wchar_t*d) {
// uint16_t sn=eeprom_read_byte((const uint8_t*)0xFFF8)|eeprom_read_byte((const uint8_t*)0xFFF9)<<8;
 uint16_t sn;
 EEAR=0xFFF8;
 EECR|=0x01;
 sn=EEDR;
 EEARL|=0x01;
 EECR|=0x01;
 sn|=EEDR<<8;
 char i=4;
 do{
  *d++=hexDigit(sn);
  sn<<=4;
 }while(--i);
 return d;
}

// Variante nur für BMP (alles was ohne Surrogates in UTF-16 passt)
// Ohne jedwede Überlaufprüfung! Strings müssen kurz genug sein.
static uchar fromUtf8(PGM_P s) {
 uchar l;
 wchar_t c,*d=(wchar_t*)(replyBuf+2);
 while (c=pgm_read_byte(s++)) {
  if (c==1) d=insertSerial(d);
  else{
   if (c&1<<7) {	// UTF-8-Führungsbyte?
    c=c<<6&0xFFF|pgm_read_byte(s++)&0x3F;	// UTF-8-Trailbytes
    if (c&1<<11) c=c<<6|pgm_read_byte(s++)&0x3F;
   }
   *d++=c;	// avr-gcc ist little-endian, passt!
  }
 }
 l=(uchar*)d-replyBuf;
 replyBuf[0]=l;
 replyBuf[1]=USBDESCR_STRING;
 usbMsgPtr=replyBuf;
 return l;
}

static prog_char en0[]="\xD0\x87\xD0\x89";	// 0x407 0x409
static prog_char en1[]="haftmann#software (www.tu-chemnitz.de/~heha)";
static prog_char en2[]="SHT11 Temperature/Humidity";
static prog_char en3[]="heha@hrz.tu-chemnitz.de:\1";
static prog_char en4[]="Air Temperature";
static prog_char en5[]="Relative Humidity";
static prog_char en6[]="Reduced resolution";
static prog_char en7[]="No OTP reload";
static prog_char en8[]="Heater";
static prog_char en9[]="Test mode, do not use";
static prog_char enA[]="Focus";
static prog_char enB[]="Transfer failed";
static prog_char enC[]="End Of Battery";

static prog_char de2[]="SHT11 Temperatur/Luftfeuchte";
static prog_char de4[]="Lufttemperatur";
static prog_char de5[]="Relative Feuchte";
static prog_char de6[]="Reduzierte Auflösung";
static prog_char de7[]="Kein OTP-Reload";
static prog_char de8[]="Heizer";
static prog_char de9[]="Testmodus, nicht benutzen!";
static prog_char deA[]="Fokus";
static prog_char deB[]="Übertragungsfehler";
static prog_char deC[]="Unterspannung";

static const PROGMEM const char*de[]={en0,en1,de2,en3,de4,de5,de6,de7,de8,de9,deA,deB,deC};
static const PROGMEM const char*en[]={en0,en1,en2,en3,en4,en5,en6,en7,en8,en9,enA,enB,enC};

USB_PUBLIC uchar usbFunctionDescriptor(struct usbRequest *rq) {
 int idx=rq->wValue.bytes[0];
 if (idx>=elemof(en)) return 0;
 return fromUtf8((PGM_P)pgm_read_word(rq->wIndex.bytes[0]==7?de+idx:en+idx));
}

// Diese Routine beachtet Grenzen von OSCCAL und die zwei Bereiche
// der ATtinyX5 nicht! Bis jetzt geht's trotzdem.
static void tuneOscillator(void) {
 static uchar lastValue;
#define SOLL 14	// usbSofDiff sollte 16500/8%256 = 14.5 sein
#define MABW 7	// Abweichung kleiner ±6 ist OK und wird ignoriert
 schar t = OCR0B-lastValue-SOLL;	// zentriere Ergebnis um die Null
 lastValue = OCR0B;
 GPIOR0&=~0x20;
 if (t<-MABW) OSCCAL++;	// schneller machen
 if (t> MABW) OSCCAL--;	// langsamer machen
}

/*****************
 * Hauptprogramm *
 *****************/

int main(void) __attribute__((noreturn));
int main(void) {
 uchar mcusr = MCUSR;
 MCUSR = 0;
 ACSR |= 0x80;		// Analogvergleicher abschalten: – 70 µA
 WDTCR = 0x18;		// Watchdog überlebt RESET: abschalten!
 WDTCR = 0;
 GPIOR0= 0;
 if (mcusr & (1 << WDRF)) {
  if (USBIN & USBMASK) {	// no SE0 state?
   GIMSK = 0x40;
   MCUCR = 0x30;	// SLEEP_MODE_PWR_DOWN
   PORTB = 0x18;	// SCK und DATA auf High per Pull-Up
   sei();		// Ohne Interruptfreigabe kein WakeUp
   sleep_cpu();		// Alle Takte abschalten
   cli();
  }
 }
 MCUCR  = 0x20;		// SLEEP_MODE_STANDBY (wir brauchen T0)
 PRR    = 0x0B;		// USI,ADC,T1 aus
 PORTB  = 0x11;		// SCK = LOW, DATA = PullUp, kein !Fokus
 DDRB   = 0x09;		// SCK, !Fokus = Ausgang
 PCMSK  = 0x10;		// SDA mit Pegelwechsel-Interrupt
 GIMSK  = 0x40;
 TCCR0B = 0x02;		// T0 mit Achtel-Taktfrequenz
 usbInit();
 WDTCR  = 0x08;		// Watchdog mit kürzestmöglicher Zeit (16 ms) aktivieren
 inputReport.id = 1;
 sei();
 for(;;) {
  Yield();
  usbPoll();
  if (GPIOR0&0x40) {	// TimeOut: Messwerte holen?
   if (fillInputReport()) GPIOR0|=0x80;	// Daten verfügbar
   GPIOR0&=~0x40;
  }
  if (usbInterruptIsReady() && GPIOR0&0x80) {	// neuer Messwert verfügbar
   usbSetInterrupt((void*)&inputReport,sizeof inputReport);
   GPIOR0&=~0x80;	// abgeschickt
  }
 }
}

// typischerweise 1 ms bis zum SOF schlafen
static void Yield(void) {
 if (USBIN&USBMASK) sleep_cpu();	// schlafen, AUSSER bei SE0, bis SOF
 wdt_reset();
 if (GPIOR0&0x20) {	// SOF detektiert?
  tuneOscillator();
  tick++;
  if (idleRate && !--idleTimer) {
   idleTimer=idleRate<<2;	// 4-ms-Schritte
   GPIOR0|=0x40;	// Messwert auslesen lassen, aber nur in der Hauptschleife
  }
 }
}
Detected encoding: UTF-80