Sedan slutet av 1700-talet har man kunnat följa en nästan ofattbar utveckling på datateknikens område. Charles Babbage visade att man i princip kunde skapa en mekanisk programmerbar dator. En praktisk mekanisk dator skulle ha varit mycket stor, tänk en fotbollsplan, och ur modern synvinkel extremt långsam. Det har visat sig att Babbages mekaniska dator var korrekt konstruerad och med tillräcklig finansiering och långvarig utveckling skull den ha varit möjlig att bygga. Babbage fick finansiering för att bygga datorn 1823 men den färdigställdes aldrig.
Matematikern Ada Lovelace (1815-1852) uppfattas som världens första programmerare. Tillsammans med Babbage utvecklade de idéer kring variabler och villkorlioga hopp i ett datorprogram, idéer som fortfarande är gångbara.
På 1930-talet studerade man Babbages idéer och insåg att Babbages mekaniska dator skulle vara mycket enklare att implementera i elektronisk form. De första elektroniska datorerna använde mekaniska reläer och elektronrör men då transistorn uppfanns kunde en mycket snabb miniatyrisering inledas, först så att elektronrör byttes mot transistorer men relativt snabbt lärde man sig att lägga in många transistorer på samma kiselbit d.v.s. man började skapa integrerade kretsar. Efter några år had man lärt sig att skapa centralenheter för enkla datorer som innehöll några tusen transistorer, Utvecklingsprocessen fortgår fortfarande. Det har visat sig att ”kapaciteten” hos en dator fördubblades på ca 2-3 år. Då utvecklingen fortgår under många år kommer kapacitetsökningen att vara ofantlig. Man kommer här att tänka på legenden om uppfinnaren av schackspelet Sissa Ibn Dahir som av kungen bad om en enkel belöning för det fantastiska spelet … sätt ett sädeskon på den första rutan, två på den andra rutan, fyra på följande o.s.v. så att mängden sädeskorn fördubblas för varje varje följande ruta. Resultatet blev att det på den sista sextiofjärde rutan fanns ungefär 1844600000000000000 sädeskorn och kungen gick i konkurs.
Under 1960-talet hade man utvecklat programmerbara datorer som gick att programmera i programmeringsspråk som fortfarande används t.ex. FORTRAN. Kapaciteten var mycket, mycket mindre än kapaciteten i en modern mobiltelefon. Hjärnan i en modern mobiltelefon skulle med glädje ha utnyttjats som centraldator för ett helt universitet på 1960-talet.
För några veckor sedan råkade jag se en reklam för ett mycket litet mikrokontrollerkort d.v.s. egentligen en liten dator med storleken 25 * 20 mm som lätt kan kopplas till en konventionell dator för programmering. Det speciella med just den här mikrokontrollern är att den har en inbyggd bildskärm som i praktiken kan visa två korta rader text. Jag kunde naturligtvis inte motstå frestelsen att prova den så jag beställde tio stycken till ett styckepris på 1.52 Euro.

Fig. 1 Mikrokontrollern ESP32-C3 OLED.
För att ha något vettigt att göra med min lilla kontroller beslöt jag att utnyttja en koldioxidsensor jag råkade ha liggande i miljonlådan och bygga en koldioxidmätare som på den pyttelilla bildskärmen kontinuerligt kunde visa koldioxidhalten i luften.
Det gick väldigt snabbt och enkelt att få fram kod för att avläsa sensorn och skriva ut det mätta värdet på bildskärmen. Jag gissar att jag fick det hela att fungera på mindre än en halv timme. Då kommer naturligtvis följande fråga. Om jag vill följa med hur coldioxidhalten förändras under en längre tidsperiod så vore det kul att kunna lagra mätvärdena på ett mikro SD minneskort.
Följande steg visade sig vara något besvärligare. Mikrokontrollern är dåligt dokumenterad. Det visade sig att jag försökte koppla in SD kortet på In/Ut pinnar som användes internt av mikrokontrollern. Efter diverse sökande hittade jag ovanstående bild som visar att det inte lönar sig att använda GPIO6 och GPIO5 eftersom de redan används av bildskärmen som är kopplad via en såkallar I2C seriedatabuss.
Då jag nu kunde skriva CO2 mätvärden till SD-minneskort visade det sig att mätningarna ”drev” tidsmässigt väldigt mycket d.v.s. det gick inte att tvinga fram mätningar med ett enkelt regelbundet mätintervall på t.ex. 5 s per mätning. Lösningen var att starta en timer (klockfunktion) som jag ställde in så att den skulle meddela att det var tid att mäta med 5 s intervall. Arbetsslingan ”loop” i programmet måste i viss mån modifieras så att mätintervallet inte styrdes av väntefunktionen längst ner i programmet. Funktionen delay() väntar en tiondels sekund innan huvudslingan loop() startas på nytt automatiskt. Väntefunktionen är relativt kort för att jag inte skall behöva vänta på kommandon jag skriver över USB serieporten. Att sköta mätningarna via interrupt visade sig fungera bra.
Eftyer ett antal successiva förbättringar kom jag fram till ett mätprogram som, om mikrokontrollern är kopplad till en dator, kan tömma mätfilen, skriva ut innehållet i filen, stoppa och återstarta mätningarna samt att starta mätningarna så att det bokförs tid/datum då mätningarna startas. Tanken är att jag senare enkelt skall kunna rita ut grafer över hur CO2 halten förändras över lång tid kanske några veckor.

Det fördiga systemet kopplat på ett prototypkort. Längst ner på bilden ses USB-anslutningen till min dator. USB matar den ström mikrokontrollern behöver. Man ser den pyttelilla bildskärmen på processorkortet. Strax ovanför mikrokontrollern syns ett litet kort från vilket det sticker ut ett SD-minnskort till vänster. Överst finns en silverfärgad enhet som är en optisk koldioxidmätare.
More’s lag igen
Då man ser på den extremt lilla mikrokontrollern så kan man plötsligt reagera på hur otroligt mycket tekniken har gått framåt. Operativsystemet UNIX utvecklades på minidatorn PDP-8 som har en ordlängd på 12-bitar. Den maximala minnesstorleken i form av magnetkärnor var 32 000 12-bitars minnesplatser. Program kunde lagras på pappersremsa eller på magnetband men PDP-8 frontpanelen användes till att mata in ett kort program för att läsa in program från t.ex. hålremsa. Programen skrevs i assembler d.v.s. direkt i symbolisk maskinkod. Vi kan jämföra denna PDP-8 minidator från 1965 som kostade 18500 dollar med min lilla mikrokontroller. PDP-8 hade en instruktionscykel på 1.5 mikrosekunder vilket motsvarar ungefär 600 000 minnesoperationer per sekund.

Bilden visar en PDP-8 minidator från datormusét i Stuttgart. Notera att kringutrustningen i form av läsare av hålremsa, Teletype terminal och eventuellt en bandstation saknas.

Dennis Ritchie och Ken Thompson, utvecklarna av UNIX, arbetar på en något mera kraftfull PDP-11 minidator. I bilden syns två Teletype terminaler. Man se mängder av rullar med hålremsor. Uppe till vänster ser man en serie magnetband.
Mikrokontroller ESP32-C3 är en RISC processor som är konstruerad för att köra en operation per klockcykel. Ett minnesord i ESP32-C3 är 32-bitar långt d.v.s. 2.6 ggr. längre an PDP-8 12-bitar. ESP32-C3 kör på en klockfrekvens på 160 MHz d.v.s. ungefär 270 ggr snabbare än PDP-8. Programminnet i mikrokontrollern är maximalt ca. 1 000 000 miljon ord jämfört med PDP-8 32 000 ord skillnaden är ungefär 30 ggr till mikrokontrollerns fördel.
Prisskillnaden mellan mikrokontrollerns 1.52 Euro och PDP-8 pris på 18500 dollar är svår att uppskatta. Om man uppskattar vad PDP-8 skulle ha kostat i dagens pris får man alltså lägga till ungefär en faktor tio d.v.s. priset omräknat till modernt pris vore ungefär 200 000 dollar. PDP-8 skulle alltså ha varit ungefär 130 000 ggr dyrare än mikrokontrollern ESP32-C3.
Om vi kombinerar hur mycket mera kapacitet den lilla ESP32-C3 har än PDP-8 på vilken den första Unix varianten skrtevs, Unix är förebilden för Linux som nu dominerar världens datorer, så kommer vi fram till en kapacitetsförbättring tekniskt och prismässigt på ungefär 1 000 000 000 ggr.
Uppskattningen av en kapacitetsförbättring pris/snabbhet/minne etc. på en miljard gånger är, tycker jag, ett bra exempel på vad Moore’s lag är!
Programkoden till CO2-mätaren finns nedan. Notera att det är ett snabbt hack där det bl.a. finns funktioner relaterade till SD-minnet som inte används i detta skede. Programmet innehåller mycket kommentarer d.v.s. det torde vara möjligt att förstå koden utan någon djup erfarenhet av programmering.
Några kommentarer till koden:
En rad som börjar med // är en kommentar avsedd att läsas av människor. En kommentar körs inte av processorn.
En funktion har utseendet namn()
Då man hanterar hårdvara behöver man veta vilka ”ben” på processorn man vill styra. De flesta ben är logiskt kopplade till generell In/Ut funktionalitet ofta kallad GPIOxx där xx är in/ut enheten nummer se bilden ovan där det i bilden anges vilka GPIO som är kopplade till viket ben.
Det är mycket lättare att läsa ett program om man hänvisar till t.ex. den inbyggda blå lysdionen med LED_BUILTIN i stället för In/Ut funktionens nummer som är 8. Jag kan alltså definiera att på varje plats i programmet där jag styr lysdioden så byter kompilatorn ut LED_BUILTIN mot talet 8 exempelvis kan vi definiera:
#define LED_BUILTIN 8
Funktionen setup() körs alltid automatiskt då processorn startar. Funktionen används för att ställa in önskad funktionalitet t.ex. serieportens hastighet innan processorn börjar köra sin eviga slinga loop() . Olika funktioner, i vårt fall i huvudsak kopplade till kontroll av SD minnet, finns definierade mellan setup() och loop().
// Name: ESP32_Mini_OLED.ino// // This is a hoppy project to test a ESP32-C3 with built in ultra miniature// LCD display. A C8-CO2-5K CO2 sensor is used to produce some data to display. The CO2// sensor produces pulse width modulated square wave pulses. The sensor is connected// to GPIO 0.// The result is written to USB serial and written to the miniature// LCD display of the type U8G2_SSD1306_72X40_ER_F_HW_I2C. // Measurement data is logged to a SD memory card if available.//// The display uses the pins SDA_PIN = 5 and SCL_PIN = 6 for I2C communication. If several// devices use I2C then make sure they use different I2C addresses.// Notice that there is no automatic line feed and everything has to be// written to positions set by program setCursor() or drawString()//// The SD memory is connected through a SPI interface. The connections are:// CS_SD 3// MOSI_SD 7// MISO_SD 2// CLK_SD 9//// To connect several devices to SPI use different pins as chip select. MOSI, MISO and CLK// are common to all units using the SPI interface.//// The internal bnlue LED is connectewd to LED_BUILTIN (GPIO8). It is blinking when the system runs normally.// The Blue led is on when LED_BUILTIN = LOW and off when LED_BUILTIN = HIGH.//// Measurements are done once every 5 seconds and logged to screen. Logging to// SD memory is done once every 15 minutes controlled by the cnt=180 counter variable// in line.// The number of the measurement is logged in the variable meas. To get the actual time in// seconds since start multiply the first number of the number pair with 5.//// Remember that:// 60 s --> 1 minute// 3600 s --> 1 hour// 86400 s --> 1 day//// Example when was the measurement 896 , 838 taken?// The first number 896 is the number of the measurement. Measurements are done at 5 seconds// intervals which means that the measurement was done 5*896 seconds since start = 4480 seconds// since measurement start roughly = 1 hour 15 minutes sincew start.//// Adding USB-serial control to start/stop measurements, clear the /log.txt file// and dump the /log.txt to the serial monitor.// file to the serial monitor.// Commands:// HELP print a list of known kommands.// LOG print the contents of the log file.// CLEAR erase the log file and generate a new one.// START Comment including start of measurement.// PAUSE pause measurements to dump file to USB serial.// In PAUSE mode it is safe to remove the SD memory.// CONTINUE contyinue measurements after PAUSE.//// Measurements are controlled by interrupt to ensure a reasonably accurate clock// when no RTC is in use. The measurement interval is set to 5 seconds ensuring// that the previous measurement is processed before starting the next measurement.//// Lars Silen 2026. Feel free to use on your own risk. #include <Arduino.h>#include <Wire.h>#include <U8g2lib.h>#include "FS.h"#include <SPI.h>#include <SD.h>#include <sd_defines.h>#include <sd_diskio.h>// Needed for the real time clock and temperature#include <Wire.h>//#include <RTClib.h> // Possibly added later.// Initialize the U8g2 library for the 72x40 SSD1306 display using hardware I2C// Pin connections on "The Egg" ESP32-C3: SDA = GPIO5, SCL = GPIO6 I2C address 0x78 (actually 2*0x3C)U8G2_SSD1306_72X40_ER_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);#define SDA_PIN 5#define SCL_PIN 6#define CO2_PIN 0#define CS_SD 3#define MOSI_SD 7 #define MISO_SD 2 #define CLK_SD 9#define LED_BUILTIN 8#define FALSE 0#define TRUE 1// If test is defined debugging information is written to USB serial.//#define TEST// No real time clock is presently used. THis means that when we start measurements and logging CO2// we need to enter the start time of the measurements. The clock in the microcontroller is probably// accurate enough to measure for weeks or months without major timing errors. A RTC uses its own// battery to keep the clock running even when the micro controller is powered down.//RTC_DS3231 rtc;unsigned long duration;int cnt = -1; // We want to make measurements stable before starting to logint meas = 0; // Number of measurements since start int shortCnt = 0; // Used for update interval on LCDuint8_t cardType = 0; // Not known yet.int SD_available = FALSE;String line="Dummy";String inputLine="";int timerTrig=FALSE;int tmpPause = FALSE;const int ledPin = LED_BUILTIN;hw_timer_t *MyTimer = NULL;void IRAM_ATTR onTimer() { timerTrig = TRUE;}void setup(void) { // Set up control pins for the display Wire.begin(SDA_PIN, SCL_PIN); u8g2.setI2CAddress(0x78); // Actually 2*0x3C u8g2.begin(); // Allow debugging asnd control over USB serial Serial.begin(115200); while (!Serial); // Ensure that USB serial is running. // We use pin 0 to read the pulse width from the C8-CO2-5K CO2 sensor pinMode(CO2_PIN, INPUT); // Set up SPI for the SD memory SPI.begin(CLK_SD, MISO_SD, MOSI_SD, CS_SD); if (!SD.begin(CS_SD)) { Serial.println("Card Mount failed!"); SD_available = FALSE; } else { Serial.println("SD Card initialized."); Serial.print("SD Card Type: "); Serial.println(cardType); } pinMode(LED_BUILTIN, OUTPUT); // LED is on when the pin is LOW off when HIGH. cardType = SD.cardType(); // Card types not checked at the moment. Max size 32 GB. // Initialize the timer with a 1 MHz frequency (1 tick = 1 microsecond) MyTimer = timerBegin(1000000); // Attach the interrupt service routine (ISR). The ISR should be kept very short. We simply set the // state of the timerTrig to TRUE indicating that a measurement should be taken. The time accuracy of // the measurement is in the order of one second because the frequency of the PWM output from // the sensor is roughly 1 Hz. For a slowly changing signal like the CO2 concentration this is perfectly OK. timerAttachInterrupt(MyTimer, &onTimer); // Set the alarm limit in microseconds (e.g., 5000,000 us = 5 s) and enable auto-reload (true) // As can be seen in the main loop below measurements are stored to SD memory at 15 minutes intervals. // The measurements done at 5s intervals are only used to update the display and USB serial. timerAlarm(MyTimer, 5000000, true, 0); // Start the timer timerStart(MyTimer);}// Presently not used.void listDir(fs::FS &fs, const char * dirname, uint8_t levels){ Serial.printf("Listing directory: %s\n", dirname); File root = fs.open(dirname); if(!root){ Serial.println("Failed to open directory"); return; } if(!root.isDirectory()){ Serial.println("Not a directory"); return; } File file = root.openNextFile(); while(file){ if(file.isDirectory()){ Serial.print(" DIR : "); Serial.println(file.name()); if(levels){ listDir(fs, file.name(), levels -1); } } else { Serial.print(" FILE: "); Serial.print(file.name()); Serial.print(" SIZE: "); Serial.println(file.size()); } file = root.openNextFile(); }}// Presently not used.void createDir(fs::FS &fs, const char * path){ Serial.printf("Creating Dir: %s\n", path); if(fs.mkdir(path)){ Serial.println("Dir created"); } else { Serial.println("mkdir failed"); }}// Presently not used.void removeDir(fs::FS &fs, const char * path){ Serial.printf("Removing Dir: %s\n", path); if(fs.rmdir(path)){ Serial.println("Dir removed"); } else { Serial.println("rmdir failed"); }}// Dump CO2 measurements to USB serial.void readFile(fs::FS &fs, const char * path){ Serial.printf("Reading file: %s\n", path); File file = fs.open(path); if(!file){ Serial.println("Failed to open file for reading"); return; } Serial.print("Read from file: "); while(file.available()){ Serial.write(file.read()); } file.close();}// Log measurements to the SD memory. If the file doesn't exist create it.void writeFile(fs::FS &fs, const char * path, const char * message){ Serial.printf("Writing file: %s\n", path); File file = fs.open(path, FILE_WRITE); if(!file){ Serial.println("Failed to open file for writing"); return; } if(file.print(message)){ Serial.println("File written"); } else { Serial.println("Write failed"); } file.close();}// Write measurements to the end of the file (append).void appendFile(fs::FS &fs, const char * path, const char * message){ Serial.printf("Appending to file: %s\n", path); File file = fs.open(path, FILE_APPEND); if(!file){ Serial.println("Failed to open file for appending"); return; } if(file.print(message)){ Serial.println("Message appended"); } else { Serial.println("Append failed"); } file.close();}// Not presently used.void renameFile(fs::FS &fs, const char * path1, const char * path2){ Serial.printf("Renaming file %s to %s\n", path1, path2); if (fs.rename(path1, path2)) { Serial.println("File renamed"); } else { Serial.println("Rename failed"); }}// Clear the file.void deleteFile(fs::FS &fs, const char * path){ Serial.printf("Deleting file: %s\n", path); if(fs.remove(path)){ Serial.println("File deleted"); } else { Serial.println("Delete failed"); }}// The main loop of the program. A program in a micro controller never stops. When the end of// the main loop is reached all local variables, local to loop() are cleared and execution starts// from the beginning of the loop(). Global variables are not cleared. void loop(void) { digitalWrite(LED_BUILTIN, HIGH); // Measurements are timer trigged at set interval for example 5 s see setup. if(timerTrig==TRUE){ duration = 0; while(duration == 0){ duration = pulseIn(CO2_PIN, HIGH, 1004000); } cnt++; meas++; shortCnt++; //duration = pulseIn(CO2_PIN, HIGH, 1004000); if ( duration != 0) { duration = ((duration -2000)*5)/1000; //if(shortCnt>=5){ if(tmpPause==FALSE){ Serial.println(duration); } //shortCnt = 1; } u8g2.clearBuffer(); // Clear the internal memory u8g2.setFont(u8g2_font_ncenB08_tr); // Choose a suitable font //u8g2.drawStr(0, 10, "LASSE"); // Write text to x=0, y=10 u8g2.setCursor(0,30); // set cursor position u8g2.print(duration); u8g2.drawStr(0, 10, "CO2 ppm"); // Write text to x=0, y=30 u8g2.sendBuffer(); // Transfer internal memory to the display // Log data interval in seconds is 5* limit below. // Limit=12 --> interval 60 s. 180 --> 15 minutes. if (((cnt>=180) || (cnt<1)) && (tmpPause==FALSE)){ // Convert time to seconds since start. %s measurement interval line = String(meas*5)+String(" , ")+String(duration); File dataFile = SD.open("/log.txt", FILE_APPEND); if (dataFile) { //Serial.println("File opened"); //dataFile.println(duration); dataFile.println(line);#ifdef TEST Serial.print("Wrote:"); Serial.println(line);#endif dataFile.close(); //Serial.println("File closed"); } else { Serial.println("File append failed!"); } cnt = 1; } timerTrig = FALSE; } // Check for commands over the USB serial the main loop runs at 1/10 of a second // Measurements are done at 5 s intervals controlled by the timer. // CO2 is displayed at every measurtement but storage is done if(Serial.available()){ String inputLine = Serial.readStringUntil('\n'); // Read until newline inputLine.trim(); inputLine.toUpperCase();#ifdef TEST Serial.println(inputLine);#endif if(inputLine.substring(0,3)=="HEL"){ Serial.println(); Serial.println("\nCommands:"); Serial.println("HELP print a list of known kommands."); Serial.println("LOG print the contents of the log file."); Serial.println("CLEAR erase the log file and generate a new one."); Serial.println("START Comment including start of measurement."); Serial.println("PAUSE pause measurements to dump file to USB serial."); Serial.println(" In PAUSE mode it is safe to remove the SD memory."); Serial.println("CONTINUE contyinue measurements after PAUSE."); } if(inputLine.substring(0,3)=="CLE"){#ifdef TEST Serial.println("Command CLEAR"); #endif deleteFile(SD,"/log.txt"); // Generate a new empty file writeFile(SD,"/log.txt","\n"); } if(inputLine.substring(0,3)=="LOG"){#ifdef TEST Serial.println("Command LOG");#endif tmpPause = TRUE; readFile(SD, "/log.txt"); tmpPause = FALSE;#ifdef TEST Serial.println("Printed the log file to USB serial");#endif Serial.println(); } if(inputLine.substring(0,3)=="PAU"){#ifdef TEST Serial.println("Command PAUSE");#endif tmpPause = TRUE; } if(inputLine.substring(0,3)=="CON"){#ifdef TEST Serial.println("Command CONTINUE");#endif tmpPause = FALSE; } if(inputLine.substring(0,3) == "STA"){#ifdef TEST Serial.println("Command START");#endif appendFile(SD,"/log.txt",inputLine.c_str()); appendFile(SD,"/log.txt","\n"); tmpPause = FALSE; // Ensure that measurements start regardless of history. } inputLine = ""; } delay(100); digitalWrite(LED_BUILTIN, LOW);}
Lämna en kommentar