>> Gameboy IP Webcam

Aktuelles Bild der Webcam

Das Projekt basiert auf einem Atmel Controller (ATmega644) und dem ENC28J60 Ethernet-controller von Microchip. Als Bildsensor findet eine alte GAME BOY® Kamera Verwendung. Der in der Kamera verbaute Bildsensor M64282FP ist ein S/W sensor und hat eine physikalische Auflösung von 128x123 Bildpunkten. Das sind 0,015744 MegaPixel. ;-) So richtig LOFI halt. Auf der Softwareseite kommt der OpenSource TCP/Stack von Adam Dunkel (uIP) zum Einsatz. Die Bildaten (16kb pro Bild) werden auf einem externem RAM als unkomprimiertes PNG gespeichert.

aktuelles Livebild

Übersicht

Die Idee

Für die Teemaschine brauchte ich eine Webcam. Man will doch ständig über den Füllstand der Teekanne informiert sein! Und da wäre es doch ein Unding gewesen, die Teemaschinensteuerung komplett selbst zu bauen und die Webcam bei S*turn zu kaufen. Auf der Suche nach dem Thema Webcam fand ich übrigens im Internet einen Artikel über die Trojan Room Coffe Pot Camera. Die erste Webcam der Welt! Und was bekamen Internetnutzer auf ihrem Schirm zu sehen? Eine Kaffekanne! Das Bild war s/w und äußerst klein. So etwas wollte ich auch haben.  Ein wenig gesucht und schon hatte ich den passenden Sensor gefunden: Die GAME BOY® Kamera. Schwarzweiß, miese Auflösung und billig. ;-)

trojan room coffe cam
Bild der trojan room coffe camera.
GAME BOY® Kamera

Artificial Retina

Das Herzstück der Gamboykamera ist der CMOS Sensor M64283FP von Mitsubishi. Er wird im Datenblatt geheimnisvoll als Artificial Retina (künstliche Netzhaut) bezeichnet. 
Die Ansteuerung geschieht digital durch serielle register, die Bildausgabe analog, so daß zusätzlich zum Sensor noch ein AD-Wandler benötigt wird. In unseren Projekt wird der im Microcontroller vorhandene AD-Wandler diese Aufgabe übernehmen.

Hier einige technische Daten des Sensors, die dem Datenblatt entnommen sind:

Spannung

+5 Volt

Energieverbrauch

15 mW

Opt. Auflösung

128 x123 Pixel

Chipgröße

¼ Zoll

Empfindlichkeit

1-10.000 lux

Belichtungszeit

16 µs bis 1 sec.

Framerate

10 – 30 fps

So sieht der Sensor der GAME BOY® Kamera ohne ihr golfballartiges Gehäuse aus. Die winzige Plastiklinse mit einem Durchmesser von ca. 1-2 mm lässt sich verdrehen. Somit kann man die Kamera auch im Makro-Bereich gut scharf stellen. 

>> Sensor-Modul Vorderseite

Die Rückseite des Sensor-Moduls. Wie man sieht, sind auf dieser Paltine ausser dem Sensor nur noch drei weiter Bauteile verbaut. Zwei Kondensatoren C1 und C2 und eine Festinduktivität L1.

>> Sensor-Modul Rückseite

Pinbelegung

Modul Pin

M64282FP Pin

Bez.

E/A

Beschreibung

1

4

DVDD

-

Stromversorgung für Digitalteil (5V)

13

PVDD

-

Stromversorgung für Analogteil (5V)

15

AVDD

-

Stromversorgung für Analogteil (5V)

2

1

START

E

HIGH-Pegel startet die Bildaufnahme

3

3

SIN

E

Serieller Dateneingang für Parametereistellung (Belichtungszeit usw.)

4

6

LOAD

E

HIGH-Pegel setzt die durch SIN empfangenen Daten

5

7

Xrst

E

System Reset

9

RESET

E

Initialisiert die Parameterregister

6

8

Xck

E

System Takt

7

10

READ

A

Mit HIGH wird angezeigt, daß die Belichtung abgeschlossen ist und Daten zum Lesen anliegen.

8

14

Vout

A

Analoger Datenausgang. Hier werden synchron mit dem Systemtakt die einzelen analogen Pegel jedes Pixels ausgegeben.

9

5

DGND

-

Masse für Digitalteil

12

AGND1

-

Masse für Analogteil

16

AGND2

-

Masse für Analogteil

Ansteuerung

Die Ansteuerung des Sensors läuft in folgenden Schritten ab:

  1. Reset
    Die RESET-Leitung wird für kurze Zeit auf HIGH gezogen. Damit wird das System und alle zur Belichtung nötigen Register initialisiert. 
  2. Setzen der Parameter
    Die Leitungen SIN und Xck stellen eine Synchrone Serielle Schnittstelle dar. Mit jeder ansteigenden Flanke von Xck werden Daten von SIN Bit für Bit in ein internes Schieberegister übernommen. Nachdem 11 Bits übertragen worden sind (3 Adressbits und 8 Datenbits) wird LOAD vom ansteuernden Microcntroller auf HIGH gesetzt. Damit übernimmt der Sensor die übertragenen Daten.

    Somit werden alle wichtigen Parameter wie Belichtungszeit und Gainwerte gesetzt. Genaueres findet man im Datenblatt.
  3. Starten der Belichtung
    Sind alle Paramer gesetzt zieht man START auf HIGH-Pegel. Dieses startet die Bildaufnahme. Hierbei ist zu beachten, dass die Belichtungszeit des Sensors durch den Takt von Xck gesteuert wird. Xck sollte bei der Belichtung (und später beim Auslesen) eine Frequenz von ca. 500 kHz  haben. Ich habe zwar auch schon gute Ergebnisse bei 100kHz erzielt, jedoch muss man beachten, dass sich hierbei die Belichtungszeit um den Faktor 5 verlängert. Gut für dunkle Situationen, schlecht für Sonnenschein, weil hier die kürzest-mögliche Belichtungszeit immer noch zu einem überbelichtetem Bild führt.
    Nachdem die (über Register 2 und 3) eingestellte Belichtungszeit verstrichen ist, zieht der Sensor READ auf HIGH.
  4. Auslesen der Pixel-Daten
    Nachdem der Sensor durch READ das Ende der Belichtung angezeigt hat, werden im Takt von Xck die einzelnen Spannungswerte (sind analog der Helligkeitswerte jedes Pixels) an Vout ausgegeben. Im Datenblatt wird ein Spitze-Spitze Wert (Vpp) von 2 Volt angegeben, d. h. bei einem Bild mit max. Kontrast wird sich am Ausgang der Spannungswert des dunkelsten Pixels von dem des hellsten Pixels durch max. 2 Volt unterscheiden.

In AVR-GCC (Atmel GNU-C Compiler) sieht das ganze so aus (Beispielcode):

/*
 * cam_getphoto();
 * take a photo
 *
 ***************************************************/

void cam_getphoto(void)
{
cam_reset();

cam_write(0,0x80); // positive offset
cam_write(1,0x04); // gain and edge
cam_write(2,exposure >> 8); //high byte exp.
cam_write(3,exposure &  0xff); //low bytes exp.
cam_write(4,0x1); // p-reg
cam_write(5,0x0); // m-reg
cam_write(6,0x1); // x-reg
cam_write(7,0x03); // voltage offset

cam_start();
cam_readsend();
}

/*
 * cam_write();
 * write cam-register
 *
 ***************************************************/

void cam_write(unsigned char reg, unsigned char data){
char i;

// adress write
for(i=2;i>=0;i--)
 {
  cam_low(CAM_CLK);
  
  if((reg>>i) & 0x01 ) cam_high(CAM_SIN);
  else                 cam_low(CAM_SIN);
  
  cam_high(CAM_CLK);
 }

// data write
for(i=7;i>=0;i--)
 {
  cam_low(CAM_CLK);

  if((data>>i) & 0x01 ) cam_high(CAM_SIN);
  else                  cam_low(CAM_SIN);

  if(i==0) cam_high(CAM_LOAD);
  cam_high(CAM_CLK);
 }

 cam_low(CAM_CLK);
 cam_low(CAM_LOAD);
}

/*
 * cam_reset();
 * reset camera-module
 *
 ***************************************************/

void cam_reset(void)
{
 cam_low(CAM_CLK);
 cam_low(CAM_RESET);
 cam_high(CAM_CLK);
 cam_high(CAM_RESET);
}

/*
 * cam_start();
 * start image sensing and wait for READ-Signal
 *
 ***************************************************/

void cam_start(void)
{
 cam_high(CAM_START);
 cam_high(CAM_CLK);
 cam_low(CAM_START);

 while(!(PINA & CAM_READ))
  {
   cam_low(CAM_CLK);
   cam_high(CAM_CLK);
  }
}

/*
 * cam_readsend();
 * reads (digitizes) all pixels from the sensor-module
 *
 *******************************************************/

void cam_readsend(void){
u08 pixel,line,value;

ADMUX=0x60;

for(line=0;line<123;line++)
{
 for(pixel=1;pixel<129;pixel++)
 {
   cam_low(CAM_CLK);
   ADCSRA=0xc4; // start conversion
   while(ADCSRA & 0x40); //wait to be finished
   cam_high(CAM_CLK);
  
   value=ADCH; // Pixelwert
 
   ..  // Hier werden die Daten verarbeitet
   ..  // im RAM gespeichert oder über eine serielle Schnittstelle gesendet... 
  
  }
 }
}

Hardware

Die Hardware baut auf meinem Atmel Webserver mit ENC28J60 Ethernetcontroller auf. Er wurde um einen statischen RAM-Baustein mit 32 kb und einem 8bit Latch zur dessen Ansteuerung erweitert, da der ATMega664 "nur" 4 kb RAM hat, für ein 128x123 Pixel Graustufenbild aber ca. 16 kb benötigt werden. Leider verfügt der ATMega664 von Haus aus nicht über eine möglichkeit externes RAM anzusprechen. So musste diese Funktionalität in der Software gelöst werden. Das RAM dient einzig und allein nur zur Zwischenspeicherung des Kamerabildes.

Für das Sensor-Modul werden 6 digitale I/O Leitungen und ein Analoger Eingang benötigt. Der interne A/D-Wandler der AVR's ist leider nicht besonders schnell, so dass die im Datenblatt des Sensors erforderliche Taktrate von 500 kHz beim Digitalisieren nicht hundertprozentig erreicht wird (sind so 100-200 kHZ) obwohl der Controller mit 25 MHz schon satte 5 MHz übertaktet ist. Es funktioniert aber trotzdem und es konnte somit auf einen externen A/D-Wandler verzichtet werden. Die Kamera lässt sich somit "glueless" an den AVR anbinden.

Die Anbindung des Ethernetcontrollers ENC28J60 wird hier erklärt.

Layout

Da die Platine sehr kompakt werden sollte, habe ich sämtliche ICs in SMD realisiert. Ganz schön schwierig zu löten ;-) Aber mit genug Lötpaste und einer ruhigen Hand geht das schon. Sowas ist allerdings dann nichts mehr für Anfänger.
Beim Platinenlayout ist mir leider zwei Fehler unterlaufen:

Software

Wie schon erwähnt, basiert die Webcam auf dem Design meines Atmel Webservers. Als TCP/IP Stack wurde der OpenSource Stack uIP von Adam Dunkel eingesetzt. Es waren nur kleine Änderungen nötig um den Stack auf den ENC28J60 anzupassen. Darauf aufbauend wurde ein HTTP-Server aufgesetzt. Er serviert Dateien, die mittels selbstgeschriebenen Filesystems (MiniFileSystem) im externen EEPROM (32 kb) abgelegt wurden.

Hier gab es nun ein Problem: Die von der Webcam gelesenen Daten müssen erst in ein Browserkompatibles Datenformat umgewandelt werden, wie z.B. JPEG oder GIF. JPEG fiel wegen seiner Komplexität schonmal gleich flach. GIF war auch nicht viel einfacher. Ich wollte die Daten am liebsten gar nicht kompriemieren. BMP ist so ein unkomprimiertes Grafikformat, doch leider wird es nur vom InternetExplorer unterstützt. Das kam also auch nicht in Frage. Die letzte Idee war PNG, ein freies Grafikformat, welches als Alternative zu GIF entwickelt worden ist. Doch leider sieht der PNG-Standard erst mal keine unkomprimierte Speicherung vor. Alle Daten müssen durch den Verlustfreien Deflat-Algoritmus komprimiert werden. Man kann allerdings durch einen Trick PNG trotzdem dazu bewegen, unkomprimierte Daten abzuspeichern, indem man den beim Deflate Algoritmus die Blocks mit UNKOMPRESSED (BTYPE=00) markiert. Bleiben aber immernoch die nicht ganz einfache Berechnung der Prüfsummen crc32 (PNG) und adler32 (Deflate).

/*
 *
 * png.c
 *
 * simlple png writer that uses few ram
 * - only writes uncompressed files
 *
 * (c) 2007 by Ulrich Esser
 * all rights reserved
 *
 */

#include 
#include "png.h"
#include "ram.h"

//unsigned char *ptr;
u16 png_addr=0;

char png_signature[] = {137,80,78,71,13,10,26,10};
struct png_ihdr png_ihdr;
struct png_zlib png_zlib;

/*
 *
 * very simple crc routine (not very fast, but uses very few ram)
 *
 *********************************************************************/

unsigned long crc=0;


void crc32_add(unsigned char byte) {
  unsigned char j;
  unsigned long mask;

  crc = crc ^ byte;

  for (j = 8; j > 0; j--)
  {
        mask = 0-(crc & 1);
         crc = (crc >> 1) ^ (0xEDB88320 & mask);
  }

}

/*
 * very simple adler32 routine
 *
 ************************************************************************/

unsigned long s1=1;
unsigned long s2=0;

void adler32_add(unsigned char byte)
{
      s1 = (s1 + byte) % 65521;
      s2 = (s2 + s1)   % 65521;
}


/*
 * png_adddata()
 * adds data with crc und adler32 to the output buffer
 *
 *********************************************************************/

void png_adddata(char *source, int len,unsigned char mode){
char *ptr;

if(mode==REVERSE_ORDER) // copy reverse byte order
 {
  source+=(len-1);
  for(;len>0;len--)
   {
    ptr=source;
  	ram_write(png_addr,*ptr);
    crc32_add(*ptr);
	adler32_add(*ptr);
	png_addr++; source--;
   }
 }
else // copy normal byte order
 {
  for(;len>0;len--)
   {
    ptr=source;
  	ram_write(png_addr,*ptr);
    crc32_add(*ptr);
	adler32_add(*ptr);
	png_addr++; source++;
   }
 }
}

/*
 * png_startchunk()
 * starts a new chunk
 *
 *********************************************************************/

void png_startchunk(char *name,unsigned long len){
png_adddata((char *)&len,4,1);
crc32_init();
png_adddata(name,4,0);
}

/*
 * png_endchunk()
 * ends a chunk
 *
 **********************************************************************/

void png_endchunk(void){
unsigned long c;
c=crc32_result();
png_adddata((char *)&c,4,1);
}

/*
 * png_startfile()
 * starts a new png file
 *
 **********************************************************************/

void png_startfile(int width,int height,unsigned char bd,unsigned char ct){
png_addr=0;//ptr=buffer;
png_ihdr.width       = width;
png_ihdr.height      = height;
png_ihdr.bitdepth    = bd;
png_ihdr.colortype   = ct;
png_ihdr.compression = 0;
png_ihdr.filter      = 0;
png_ihdr.interlace   = 0;

png_zlib.cmf         = 0x78;
png_zlib.flg		 = 0xda;
png_zlib.btype		 = 0x01;
png_zlib.len		 = DATA_PERLINE * png_ihdr.height;
png_zlib.nlen		 = ~png_zlib.len;

// first write PNG file signature
png_adddata(png_signature,SIGNATURE_SIZE,NORMAL_ORDER);

// image header chunk
png_startchunk("IHDR",IHDR_SIZE);
png_adddata((char *)&png_ihdr.width,4,REVERSE_ORDER); // add with
png_adddata((char *)&png_ihdr.height,4,REVERSE_ORDER); // add height
png_adddata((char *)&png_ihdr.bitdepth,5,NORMAL_ORDER); // add the rest
png_endchunk();

// start image data chunk
png_startchunk("IDAT",png_zlib.len + ZLIB_SIZE + ADLER32_SIZE);

// zlib uncompressed parameters
png_adddata((char *)&png_zlib,3,NORMAL_ORDER); // write first 3 parameters
png_adddata((char *)&png_zlib.len,2,NORMAL_ORDER); // write len field
png_adddata((char *)&png_zlib.nlen,2,NORMAL_ORDER); // write inverse len field

adler32_init();
}

/*
 * png_endfile()
 * ends the png file
 *
 **********************************************************************/

void png_endfile(void){
unsigned long adler;

adler=adler32_result();

// write adler32 to end zlib uncompressed block
png_adddata((char *)&adler,4,REVERSE_ORDER);
// end image data chunk
png_endchunk();

// end chunk
png_startchunk("IEND",NORMAL_ORDER);
png_endchunk();
}

/*
 * png_test()
 * write a png test file (128x123 bw-ramp)
 *
 *********************************************************************/

void png_test(void)
{
int i,k;
char scanline[130];

png_startfile(128,123,8,0);

// the imgage data

for(k=0; k < png_ihdr.height ; k++)
{
 for(i=0; i < DATA_PERLINE;i++) scanline[i]=k;
 scanline[0]=0; // filter type : NO FILTER
 png_adddata(scanline,DATA_PERLINE,0);
}

png_endfile();
}

Bilder der Webcam

 
Jaa. Das ist das allererste Bild, welches ich dem Sensor entlocken konnte ;-) Mein Oszi Makroaufnahme durch Nachjustierung der Linse So sollte eigentlich das Bild der Potcam aussehen Witziger Effekt der entsteht, wenn man sich vor der Kamera zu schnell bewegt

Und hier nochwas ganz besonderes: Hab die einzelnen Bewegungsphasen einer Bürostuhldrehung zu einem animiertem GIF zusammen gerechnet ;-) Damit sind also auch Filme möglich...

Einbau

Ich wollte die fertige Webcam ursprünglich in irgendeine alte Kleinbildkamera einbauen, doch leider sind die Ausmasse der Platine etwas groß geraten. Also hab ich die Kamera kurzerhand in eine CD-Spindel eingebaut. Das hat ausserdem noch den Vorteil, dass man jetzt auch gut das Innere der Cam sehen kann.

 


GAME BOY® ist eine eingetragene Marke der Firma Nintendo.

Downloads

Links


(c) 2007 by Ulrich Esser