Archive for the ‘hack’ Category

Programmering 2026

04/10/2026

Den klassiska programmeraren som sitter och skriver, dricker ohälsosam CocaCola, läser/googlar och själv skriver varje rad kod är antagligen död eller håller på att försöka ta sina sista andetag.

I artikeln Moores lag beskrev jag en liten mikrokontroller som jag använde till att avläsa en koldioxidsensor och logga data till ett SD-minne kopplat till mikrokontrollern. Mikrokontrollern har en extremt liten bilskärm som man nästan behöver förstoringsglas för att läsa. För att göra CO2-loggern enkel att använda behöver jag ett program som kör på min laptop och som visar loggad CO2 koncentration, tillåter mig att skicka kommandon till mikrokontrollern etc.

Det finns många alternativ jag kan använda. Det första alternativet som för mig kändes väldigt naturligt skulle vara att använda Lazarus/Pascal som erbjuder ett trevligt grafiskt användarinterface som jag har använt ett antal gånger genom åren. Andra alternativ finns naturligtvis. Med lämpliga biblioterk kunde jag skriva ett grafiskt program i språket C eller i C++ men jag har inte kodat användarinterface i dessa språk vilket skulle betyda en hel del goolande och läsande.

Av en slump stötte jag på ett intressant verktyg baserat på öppen källkod: Processing. Processing bygger på Java men detta ursprung syns egentligen inte utan Processing påminner om Arduino IDE som jag känner väl från tidigare. Det största problemet med ett nytt verktyg som t.ex. Processing är att läsa sig in på vad det egentligen gör och hur det används samt ofta ny terminologi som grumlar till de första timmarna då man försöker förstå dokumentationen som plötsligt talar ett främmande språk.

Min medhjälpare AI

Jag beslöt att i stället för att sätta igång och koda i Processing så låter jag AI göra jobbet genom att ange vad jag vill ha gjort.

Mikrokontrollern ESP32-C3_OLED är kopplad till min PC över en serielinje som kör på 115200 baud. Loggade värden skrivs ut till serieporten med 5s intervall och med 15 minuters intervall loggas data till ett SD-minne på mikrokontrollern. Jag vill ha en applikation som körs på min Linux PC som i grafisk form visar inkommande data, tillåter mig att starta, stoppa och radera mätningar samt i grafisk form kontinuerligt visa CO2 trenden d.v.s. hur CO2 koncentrationen ökar/minskar.

Min diskussion med AI löpte på följande sätt:

Programmeraren (alltså jag):

Can you write me a Processing sketch that reads serial data from Arduino and writes the serial data to a window on the PC?

AI respons:

Here is a Processing sketch that reads serial data from an Arduino and displays it live in a graphical window on your PC.

import processing.serial.*;
Serial myPort; // Create object from Serial class
String inString = ""; // Input string from serial port
PFont f; // Font for displaying text
void setup() {
size(600, 400); // Window size (width, height)
// Print a list of available serial ports to the console to help find the correct one
printArray(Serial.list());
// Open the port your Arduino is connected to.
// Change [0] to match your Arduino's index in the list if needed!
String portName = Serial.list()[0];
myPort = new Serial(this, portName, 9600);
// Read bytes until a newline character ('\n') is received
myPort.bufferUntil('\n');
// Create a simple font for the text display
f = createFont("Arial", 16, true);
textFont(f);
}
void draw() {
background(40); // Dark gray background
fill(255); // White text color
textSize(18);
// Title on the window
text("Arduino Serial Data Monitor", 20, 30);
// Display the latest received string on the window
textSize(24);
fill(0, 255, 128); // Neon green text for data
text(inString, 20, 100);
}
// This function runs automatically when new serial data ends with a newline ('\n')
void serialEvent(Serial myPort) {
inString = myPort.readStringUntil('\n');
if (inString != null) {
inString = trim(inString); // Remove whitespace and extra newlines
}
}

Resultatet blev ett enkelt grafiskt fönster nedan.

Fig. 1 Resultatet av det första försöket. Inte mycket att hänga i julgranen men programmet visar ett fönster på PC-skärmen, sätter upp seriekommunikationen korrekt och skriver ut ett mätvärde i taget helt enligt vad jag bad om.

Appen saknar helt möjligheter att från PC:n skicka textkommandon till mikrokontrollern. Saken kan fixas genom att be AI förbättra koden.

Programmeraren:

Add a field to input commands to send to the running Arduino app.

AI:

To add an input field for sending commands back to the Arduino, you can use Processing’s built-in keyPressed() function to capture typing and send the string over the serial port when you press Enter.
Here is the updated Processing sketch with a visual input box and keyboard text capture:

import processing.serial.*;
Serial myPort; // Create object from Serial class
String inString = ""; // Input string from serial port
String outString = ""; // Text being typed to send to Arduino
String lastSent = ""; // Remembers the last command sent
PFont f; // Font for displaying text
void setup() {
size(600, 400); // Window size (width, height)
// Print list of available serial ports to the console
printArray(Serial.list());
// Open the port your Arduino is connected to
String portName = Serial.list()[0]; // Change '0' if your Arduino is at a different index
myPort = new Serial(this, portName, 9600);
// Read bytes until a newline character ('\n') is received
myPort.bufferUntil('\n');
f = createFont("Arial", 16, true);
textFont(f);
}
void draw() {
background(40); // Dark gray background
// SECTION 1: Display Incoming Data
fill(255);
textSize(16);
text("Incoming From Arduino:", 30, 40);
fill(0, 255, 128); // Neon green for incoming data
textSize(24);
text(inString, 30, 80);
// SECTION 2: Input Field for Outgoing Commands
stroke(100);
fill(60);
rect(30, 180, 540, 45, 5); // Draw the text input box
fill(255);
textSize(14);
text("Type command and press ENTER to send:", 30, 165);
// Display the text currently being typed inside the box
fill(255, 255, 0); // Yellow text for typing
textSize(18);
text(outString + (frameCount % 30 < 15 ? "|" : ""), 40, 210); // Adds a blinking cursor
// SECTION 3: Confirmation of last sent command
if (!lastSent.equals("")) {
fill(180);
textSize(14);
text("Last sent command: \"" + lastSent + "\"", 30, 260);
}
}
// Captures keyboard input for the command box
void keyPressed() {
if (key == ENTER || key == RETURN) {
// Send the typed string to the Arduino, adding a newline character
myPort.write(outString + "\n");
lastSent = outString; // Save to display on screen
outString = ""; // Clear the input field for the next command
}
else if (key == BACKSPACE) {
// Remove the last character if backspace is pressed
if (outString.length() > 0) {
outString = outString.substring(0, outString.length() - 1);
}
}
else if (key != CODED && key != ESC) {
// Append valid typed characters to our outgoing string
outString += key;
}
}
// Automatically runs when incoming data hits a newline ('\n')
void serialEvent(Serial myPort) {
inString = myPort.readStringUntil('\n');
if (inString != null) {
inString = trim(inString); // Clean up whitespace
}
}

Fig. 2 Jag kan nu skicka kommandon till mikrokontrollern men t.ex. kommandot help producerar flera rader text vilket gör att jag endast ser den sista raden i appen ovan … vilket inte är till speciellt stor hjälp.

Programmeraren:

Make the incoming data area larger to fit the twenty last lines received from Arduino.

AI:

Here is the updated Processing sketch. It replaces the single-line display with a scrolling string array that stores and displays the last 20 lines received from the Arduino, formatting it like a classic terminal window.

import processing.serial.*;
Serial myPort; // Create object from Serial class
String[] lineHistory = new String[20]; // Array to store the last 20 lines
String outString = ""; // Text being typed to send to Arduino
String lastSent = ""; // Remembers the last command sent
PFont f; // Font for displaying text
void setup() {
size(700, 600); // Larger window size (width, height) to fit history
// Initialize the history array with empty strings
for (int i = 0; i < lineHistory.length; i++) {
lineHistory[i] = "";
}
// Print list of available serial ports to the console
printArray(Serial.list());
// Open the port your Arduino is connected to
String portName = Serial.list()[0]; // Change '0' if your Arduino is at a different index
myPort = new Serial(this, portName, 9600);
// Read bytes until a newline character ('\n') is received
myPort.bufferUntil('\n');
f = createFont("Courier New", 14, true); // Monospace font fits terminal history best
textFont(f);
}
void draw() {
background(30); // Dark gray background
// SECTION 1: Display Incoming Data History (Terminal Box)
fill(255);
textSize(14);
text("Incoming From Arduino (Last 20 lines):", 30, 30);
stroke(80);
fill(15);
rect(30, 45, 640, 410, 5); // Terminal history background box
// Draw the 20 lines inside the terminal box
fill(0, 255, 128); // Neon green for incoming data
int startY = 65; // Vertical starting position for the first line
int lineHeight = 20; // Space between lines
for (int i = 0; i < lineHistory.length; i++) {
text(lineHistory[i], 45, startY + (i * lineHeight));
}
// SECTION 2: Input Field for Outgoing Commands
stroke(100);
fill(50);
rect(30, 500, 640, 45, 5); // Draw the text input box
fill(255);
textSize(14);
text("Type command and press ENTER to send:", 30, 485);
// Display the text currently being typed inside the box
fill(255, 255, 0); // Yellow text for typing
text(outString + (frameCount % 30 < 15 ? "_" : ""), 45, 528); // Terminal-style blinking cursor
// SECTION 3: Confirmation of last sent command
if (!lastSent.equals("")) {
fill(180);
textSize(12);
text("Last sent: \"" + lastSent + "\"", 30, 570);
}
}
// Captures keyboard input for the command box
void keyPressed() {
if (key == ENTER || key == RETURN) {
// Send the typed string to the Arduino, adding a newline character
myPort.write(outString + "\n");
lastSent = outString; // Save to display on screen
outString = ""; // Clear the input field for the next command
}
else if (key == BACKSPACE) {
// Remove the last character if backspace is pressed
if (outString.length() > 0) {
outString = outString.substring(0, outString.length() - 1);
}
}
else if (key != CODED && key != ESC) {
// Append valid typed characters to our outgoing string
outString += key;
}
}
// Automatically runs when incoming data hits a newline ('\n')
void serialEvent(Serial myPort) {
String inString = myPort.readStringUntil('\n');
if (inString != null) {
inString = trim(inString); // Clean up whitespace and newlines
// Shift all elements in history array up by 1 index
for (int i = 0; i < lineHistory.length - 1; i++) {
lineHistory[i] = lineHistory[i + 1];
}
// Add the newest line to the very bottom of the array
lineHistory[lineHistory.length - 1] = inString;
}
}

Fig. 3 De tjugo sista mätvärdena presenteras nu på PC-bildskärmen.

Resultatet är helt OK. Jag ser vad som händer nere på mikrokontrollern men det skulle vara trevligt med en grafisk display över mätvärdena. Det är lättare att med ett ögonkast se vad som händer i grafisk form än som en rad siffror.

Programmeraren:

Add a graphical display of single numerical values to the left of the scrolling text window. The size is 200×200 pixels. Scale the Y-axis 400-1500 and the X-axis as number of measurement with ticks 100 measurements apart.

AI:

Here is the updated Processing sketch. It expands the window to place a 200×200 pixel graph directly to the left of the terminal window.
The graph is designed to automatically parse incoming numeric text, plot it sequentially over time, and draw dynamic X-axis tick markers every 100 measurements. It also clips the plotted points to the exact requested 400 to 1500 Y-axis range.

import processing.serial.*;
Serial myPort;
String[] lineHistory = new String[20];
String outString = "";
String lastSent = "";
PFont f;
// Graphing Variables
FloatList graphData; // Stores the history of numerical measurements
int graphX = 30; // Left edge of the 200x200 graph
int graphY = 45; // Top edge of the 200x200 graph
int graphW = 200; // Graph Width
int graphH = 200; // Graph Height
float minVal = 400; // Y-axis minimum scale
float maxVal = 1500; // Y-axis maximum scale
void setup() {
size(930, 600); // Expanded width (700 + 230) to comfortably fit the graph on the left
graphData = new FloatList();
for (int i = 0; i < lineHistory.length; i++) {
lineHistory[i] = "";
}
printArray(Serial.list());
String portName = Serial.list()[0];
myPort = new Serial(this, portName, 9600);
myPort.bufferUntil('\n');
f = createFont("Courier New", 14, true);
textFont(f);
}
void draw() {
background(30);
// -----------------------------------------------------------------
// SECTION 1: GRAPHICAL DISPLAY (200x200 Pixels)
// -----------------------------------------------------------------
fill(255);
textSize(14);
text("Live Plot:", graphX, 30);
// Draw Graph Grid Background
stroke(80);
fill(15);
rect(graphX, graphY, graphW, graphH);
// Draw Y-Axis Labels (400 to 1500)
fill(150);
textSize(10);
textAlign(RIGHT, CENTER);
text("1500", graphX - 5, graphY);
text("950", graphX - 5, graphY + graphH/2);
text("400", graphX - 5, graphY + graphH);
textAlign(LEFT, BASELINE); // Reset alignment
// Draw X-Axis Ticks (every 100 measurements)
int totalMeasurements = graphData.size();
if (totalMeasurements > 0) {
stroke(60);
textSize(9);
fill(120);
// Find the first multiple of 100 visible in the current view window
int startTick = (max(0, totalMeasurements - graphW) / 100) * 100;
if (startTick < totalMeasurements - graphW) {
startTick += 100;
}
for (int t = startTick; t <= totalMeasurements; t += 100) {
// Map the measurement sequence number to the pixel X position
int historyIndexFromRight = totalMeasurements - t;
float tickPixelX = (graphX + graphW) - historyIndexFromRight;
if (tickPixelX >= graphX && tickPixelX <= graphX + graphW) {
line(tickPixelX, graphY, tickPixelX, graphY + graphH); // Gridline
text(t, tickPixelX - 5, graphY + graphH + 12); // Measurement number label
}
}
}
// Plot the Data Line
stroke(0, 255, 255); // Cyan color line for plot data
strokeWeight(1.5);
noFill();
beginShape();
// Display only the data that fits inside the 200px width limit
int displayCount = min(graphData.size(), graphW);
int startIndex = graphData.size() - displayCount;
for (int i = 0; i < displayCount; i++) {
float val = graphData.get(startIndex + i);
// Constrain data values to the requested scale limits
val = constrain(val, minVal, maxVal);
// Convert numerical space (400-1500) to Pixel Space coordinates
float plotX = graphX + i + (graphW - displayCount);
float plotY = map(val, minVal, maxVal, graphY + graphH, graphY);
vertex(plotX, plotY);
}
endShape();
strokeWeight(1); // Reset stroke weight
// -----------------------------------------------------------------
// SECTION 2: SCROLLING TEXT WINDOW (Shifted to the right)
// -----------------------------------------------------------------
int textWindowX = 260; // Pushed right to clear the 200px graph + margins
fill(255);
textSize(14);
text("Incoming From Arduino (Last 20 lines):", textWindowX, 30);
stroke(80);
fill(15);
rect(textWindowX, 45, 640, 410, 5);
fill(0, 255, 128);
int startY = 65;
int lineHeight = 20;
for (int i = 0; i < lineHistory.length; i++) {
text(lineHistory[i], textWindowX + 15, startY + (i * lineHeight));
}
// -----------------------------------------------------------------
// SECTION 3: INPUT FIELD & UTILITIES (Spans the bottom area)
// -----------------------------------------------------------------
stroke(100);
fill(50);
rect(30, 500, 870, 45, 5);
fill(255);
textSize(14);
text("Type command and press ENTER to send:", 30, 485);
fill(255, 255, 0);
text(outString + (frameCount % 30 < 15 ? "_" : ""), 45, 528);
if (!lastSent.equals("")) {
fill(180);
textSize(12);
text("Last sent: \"" + lastSent + "\"", 30, 570);
}
}
void keyPressed() {
if (key == ENTER || key == RETURN) {
myPort.write(outString + "\n");
lastSent = outString;
outString = "";
}
else if (key == BACKSPACE) {
if (outString.length() > 0) {
outString = outString.substring(0, outString.length() - 1);
}
}
else if (key != CODED && key != ESC) {
outString += key;
}
}
void serialEvent(Serial myPort) {
String inString = myPort.readStringUntil('\n');
if (inString != null) {
inString = trim(inString);
// 1. Log Raw String to Console Feed
for (int i = 0; i < lineHistory.length - 1; i++) {
lineHistory[i] = lineHistory[i + 1];
}
lineHistory[lineHistory.length - 1] = inString;
// 2. Parse Numeric Value for Graphing
try {
float parsedValue = float(inString);
if (!Float.isNaN(parsedValue)) {
graphData.append(parsedValue);
}
} catch (Exception e) {
// Keeps going safely if text lines contain alpha commands instead of integers
}
}
}

Fig. 4 Resultatet blev en app som kör under Linux, Windows och Mac som visar inkommande text från mikrokontrollern, tillåter mig att kontrollera mikrokontrollern med en serie kommandon samt visar en grafisk bild av CO2-nivån.

Notera! Jag producerade flera hundra rader kod för ett verktyg jag aldrig tidigare har använt utan att själv ha skrivit en enda rad kod. Total tid till den sista helt fungerande varianten var under femton minuter. Själva beskrivningen av vad jag gjorde krävde betydligt mera tid än själva ”kodandet”.

Min personliga uppfattning är att framtidens kodare är en person med en klar bild av vad hen vill ha och som är kapabel att i klartext ange det önskade resultatet. En typisk arbetsgång är att förbättra programmet i små steg, inte att försöka skriva en specifikation över många sidor. Varje steg testas innan följande detalj läggs till.

Moore’s lag

29/09/2026

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 samtidigt 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 program, variabler och vilkorliga hopp i ett datorprogram, idéer som fortfarande är gångbara.

Senare 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 men på en kiselbit ryms idag miljarder transistorer.

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. Fördubblingen av kapaciteten under en given tidrymd kallas Moores lag.

Då vi ser vad en fördubbling följd av en fördubbling följd av … leder till kommer 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.

Det är skäl att komma ihåg att Moores lag inte går att tillämpa på godtycklig teknik. En förutsättning för att Moores lag har fungerat under lång tid är en teknisk utveckling av miniatyrisering kopplad till extremt effektiv serieproduktion. Det är alltså inte möjligt att se samma utveckling gällande t.ex. vindkraft där man startar från en 2000 år gammal teknik och där miniatyrisering inte är möjlig eftersom man samlar in vindenergi över ett stort område.

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 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 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.

Fig. 2 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. Ordlängd betyder att en instruktion har längden 12-bitar i det binära systemet där det finns endast två siffror 0 och 1. I en 12 bitars minnesplats kan man lagra tal mellan 0 och 4095.

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 frontpanel, se fig 3, användes till att mata in ett kort program för att läsa in program från t.ex. hålremsa. Program 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.

Fig. 3 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.

Fig. 4 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 blir 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 fig. 1 ovan där det i bilden anges vilka GPIO som är kopplade till vilka 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

På motsvarande sätt har jag gett namn på in/ut ben för att controllera bildskärmen och SD minneskortet.

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 log
int meas = 0; // Number of measurements since start
int shortCnt = 0; // Used for update interval on LCD
uint8_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);
}

AI datorarkeologi 2026

05/04/2026

I den föregående artikeln jämförde jag en CP/M baserad dator från tidsperioden 1975-85, dock emulerad, med dagens datorer. Min konklusion var att våra nuvarande datorer i alla relevanta avseenden är från tusen till några miljoner gånger kraftfullare än gårdagens CP/M maskin som trots dess begränsningar var ett fint hjälpmedel för många småföretag.

En CP/M maskinen erbjöd t.ex. en textbehandlare för brev vilket var himmelriket jämfört med att skriva ett helt brev felfritt på en mekanisk skrivmaskin. Mot slutet av CP/M epoken fanns det redan fullständigt användbara textbehandlare t.ex. Wordstar. CP/M maskinen erbjöd också program där mitt brev automatiskt kunde omformas för olika mottagare (mail merge) vilket igen sparade mycket tid igen i förhållande till manuellt arbete där varje brev, helst felfritt, skulle skrivas ut på skrivmaskin. Det fanns primitiva databaser och program för att hantera fakturering. Maskinen var primitiv men den var användbar.

Min jämförelse av CP/M maskinen och en modern PC gjordes på min bordsdator så att jag installerade emulatorn RunCPM under Linux. Då jag lekte med emulatorn märkte jag att det skulle ha varit trevligt att inifrån CP/M kunna skriva ut text på min skrivare samt naturligtvis också kunna lagra data på klassiskt sätt genom att stansa ut en hålremsa. En stansapparat är idag svår att komma över och de är förvånande dyra, vi talar ofta om kanske en tusenlapp. Om jag kan skapa hålremsan så behöver jag naturligtvis också ett program för att läsa av en hålremsa och återskapa data. Det kunde också vara trevligt att kunna lagra data och program på magnetband på ett möjligast tidstypiskt sätt. På samma sätt som för hålremsstansen så behöver jag naturligtvis också en läsare för magnetband.

Jag beslöt att lösa problemet med hjälp av några enkla program skrivna i programmeringsspråket Python. Dessa program körs under Linux parallellt med den emulerade CP/M maskinen. Notera att en modern PC har 4-8 processorkärnor, ibland mer, vilket betyder att den fullständigt problemfritt kan emulera min CP/M maskin utan att det märkbart påverkar Linuxens, d.v.s. PC:ns kapacitet.

Hålremsa

Det första steget för att ge min CP/M emulator tillgång till en hålremsstans och motsvarande läsare är att skriva två relativt enkla python program som tar en datafil d.v.s. en serie Bytes som i princip är godtyckliga tecken som passar in i en 8-bitars Byte. Totalt finns det max 256 tecken i en åtta bitars datastruktur.

Det slog mig att jag kunde använda detta projekt till att testa hur väl AI kunde hjälp mig med programmeringen och vilken form detta kunde ta. Min diskussion med AI är alltså den ”programmering” jag gjorde. Jag använde en gratisversion av chatGPT som medhjälpare. Texten nedan visar diskussionen med AI och resultatet. Efter varje programförslag korrigerade jag AI och bad om förbättringar. Jag använde engelska under diskussionen men jag gissar att det skulle ha fungerat lika bra på svenska.

Steg 1 jag beskriver grunddesignen för AI:

(Lars) I want to encode a string of 8 bit bytes into a png image as a column of bits. Every bit consists of 10×10 pixels. A high bit is white and a low bit is black. A maximum of 128 bytes are coded into a column. When a column is full a new column is generated to the right of the previous column. Columns are separated by a double bit wide grey separator line. The number of columns in the image is not restricted. The bytes to be encoded are read from a text file given on the command line. The resulting image file is called tape.png .

(ChatGPT) This is a neat “visual tape” encoding idea. The requirements are clear enough to implement directly. Below is a complete Python script using Pillow that does exactly what you described.

Resultatet var ett körbart pythonprogram som kodade data som strimmor vertikalt ner över skärmen. I princip användbart men motsvarar inte en gammal hålremsa. Det är lätt att be om en korrektion. Tanken är att generera en bild jag kan skriva ut på skrivare och om jag så vill läsa in med hjälp av skrivarens scanner för att sedan avkoda. Stansning/avkodning görs alltså modernt via en bild av typen png d.v.s. utan fysisk stansning. Resultatet kan dock skrivas ut och mappas in i en fysisk mapp om jag vill i likhet med forntidens hålremsa som förvarades i en ask.

(Lars) Generate columns of 128 bytes in a manner of a punched paper tape. When 128 bytes has filled a column new columns are generated to the right of the previous filled column.

(ChatGPT) What you’re describing is essentially a visual punched paper tape, where each column is like a vertical slice of tape holding 128 bytes (rows), and each byte is represented by 8 holes (bits).Let me tighten and clarify the structure and give you a clean implementation that matches that mental model exactly.

För att göra hålremsan lättare att läsa för en människa separerar jag varje kodad byte med en vit rad. Vi kunde klippa ut vertikala strimmor ur den utskrivna bilden och limma ihop dem till en hålremsa som vi kunde läsa av utan större problem på optisk väg. Någon hålstansning begövs inte för en modern läsare. En svart ruta med storleken 10×10 pixlar betyder att ifrågavarande bit=0 d.v.s. låg nivå. På motsvarande sätt betyder en vit ruta att platsen ”har stansats” d.v.s. bit=1 hög nivå. Svart/vit ruta förstås så att vi i en riktig pappersremsa har en lampa på baksidan av remsan som belyser sensorer som belyser sensorer på andra sidan. Ett stansat hål ses som ljust.

(Lars) Separate bytes with white line between rows of bits.

(ChatGPT) Got it — you want the tape to be more readable by clearly separating each byte (row) with a horizontal white line.

Slutresultatet blev ett kodningsprogram som jag kunde köra från kommandoraden under Linux:

python3 encoder3.py encoder3.py

Det första kommandot python3 anger att jag vill använda python3 som tolk, det finns flera generationer av python i bruk därav python3. Jag kan enkelt modifiera programmet så att det kan köras utan explicit python3. Jag ber programmet koda sig självt och resultatet blir en bildfil tape.png . Bildfilen har följande utseende:

Bilden innehåller alltså hela texten till programmet som kodar bilden. Hålremsan skall läsas uppifrån till vänster en kolumn i taget. Det finns inga inbyggda begränsningar för hur många kolumner det får finnas i bilden. Utskrivet på en A4 sida ryms ca. 3000 tecken d.v.s. ett typisk litet program skrivet t.ex. i Pascal eller C. Jag har inte editerat en enda rad i den genererade koden, allt är skapat av chatGPT enligt mina instruktioner.

Det genererade programmet har följande utseende utan en enda editering från min sida:

import sys
from PIL import Image

# Geometry
BIT_SIZE = 10
BITS_PER_BYTE = 8
BYTES_PER_COLUMN = 128
SEPARATOR_WIDTH_BITS = 2
ROW_SEPARATOR_BITS = 1 # NEW

# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (128, 128, 128)


def byte_to_bits(byte):
return [(byte >> (7 - i)) & 1 for i in range(8)]


def main():
if len(sys.argv) < 2:
print("Usage: python tape.py input.bin")
return

with open(sys.argv[1], "rb") as f:
data = f.read()

# Split into columns
columns = [
data[i:i + BYTES_PER_COLUMN]
for i in range(0, len(data), BYTES_PER_COLUMN)
]

num_columns = len(columns)

# Height in bits (bytes + separators)
height_bits = (
BYTES_PER_COLUMN +
(BYTES_PER_COLUMN - 1) * ROW_SEPARATOR_BITS
)

# Width in bits
width_bits = (
num_columns * BITS_PER_BYTE +
(num_columns - 1) * SEPARATOR_WIDTH_BITS
)

# Convert to pixels
width_px = width_bits * BIT_SIZE
height_px = height_bits * BIT_SIZE

img = Image.new("RGB", (width_px, height_px), BLACK)
pixels = img.load()

current_x_bit = 0

for col_idx, column in enumerate(columns):
y_bit = 0

for row_idx, byte in enumerate(column):
bits = byte_to_bits(byte)

# Draw byte row
for bit_idx, bit in enumerate(bits):
color = WHITE if bit else BLACK

x_bit = current_x_bit + bit_idx

for dx in range(BIT_SIZE):
for dy in range(BIT_SIZE):
x = x_bit * BIT_SIZE + dx
y = y_bit * BIT_SIZE + dy
pixels[x, y] = color

y_bit += 1

# Draw horizontal separator (except after last byte)
if row_idx < len(column) - 1:
for x_bit in range(current_x_bit, current_x_bit + BITS_PER_BYTE):
for dx in range(BIT_SIZE):
for dy in range(BIT_SIZE):
x = x_bit * BIT_SIZE + dx
y = y_bit * BIT_SIZE + dy
pixels[x, y] = WHITE

y_bit += ROW_SEPARATOR_BITS

current_x_bit += BITS_PER_BYTE

# Draw vertical separator between columns
if col_idx < num_columns - 1:
for sep in range(SEPARATOR_WIDTH_BITS):
for yb in range(height_bits):
for dx in range(BIT_SIZE):
for dy in range(BIT_SIZE):
x = (current_x_bit + sep) * BIT_SIZE + dx
y = yb * BIT_SIZE + dy
pixels[x, y] = GRAY

current_x_bit += SEPARATOR_WIDTH_BITS

img.save("tape.png")
print("Saved tape.png")


if __name__ == "__main__":
main()

Följande steg är naturligtvis att låta chatGPT göra motsatsen d.v.s. skriva ett pythonprogram som läser en hålremsa lagrad som en bildfil och återskapar den kodade informationen.

(Lars) Now generate a decoder for the last version. It takes a png-file as input and produces a text file RDTAPE.txt containing the decoded bytes.

(ChatGPT) Great — now we’ll reverse the exact format you just defined.

This decoder will:

Read tape.png

Detect bits based on pixel blocks (10×10)

Skip:

horizontal white row separators

vertical gray column separators

Reconstruct bytes

Write them to RDTAPE.txt (binary-safe)

Resultatet blev ett fungerande avkodningsprogram som korrekt återskapade det kodade programmet i bildfilen tape.png.

import sys
from PIL import Image

# Geometry (must match encoder!)
BIT_SIZE = 10
BITS_PER_BYTE = 8
BYTES_PER_COLUMN = 128
ROW_SEPARATOR_BITS = 1
SEPARATOR_WIDTH_BITS = 2

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (128, 128, 128)


def is_white(pixel):
return pixel[0] > 200 and pixel[1] > 200 and pixel[2] > 200


def is_black(pixel):
return pixel[0] < 50 and pixel[1] < 50 and pixel[2] < 50


def is_gray(pixel):
return abs(pixel[0] - 128) < 40 and abs(pixel[1] - 128) < 40 and abs(pixel[2] - 128) < 40


def read_bit_block(pixels, x_bit, y_bit):
"""Sample center of a bit block"""
x = x_bit * BIT_SIZE + BIT_SIZE // 2
y = y_bit * BIT_SIZE + BIT_SIZE // 2
pixel = pixels[x, y]

if is_white(pixel):
return 1
else:
return 0


def is_separator_column(pixels, x_bit, height_bits):
"""Detect gray separator column"""
count_gray = 0
for yb in range(height_bits):
x = x_bit * BIT_SIZE + BIT_SIZE // 2
y = yb * BIT_SIZE + BIT_SIZE // 2
if is_gray(pixels[x, y]):
count_gray += 1

return count_gray > height_bits * 0.8


def main():
if len(sys.argv) < 2:
print("Usage: python decode.py tape.png")
return

img = Image.open(sys.argv[1])
pixels = img.load()

width_px, height_px = img.size

width_bits = width_px // BIT_SIZE
height_bits = height_px // BIT_SIZE

x_bit = 0
decoded_bytes = []

while x_bit < width_bits:
# Skip gray separator columns
if is_separator_column(pixels, x_bit, height_bits):
x_bit += 1
continue

# Read one data column (8 bits wide)
column_bytes = []

y_bit = 0
row_count = 0

while y_bit < height_bits and row_count < BYTES_PER_COLUMN:
bits = []

# Read 8 bits across
for bit_idx in range(BITS_PER_BYTE):
bits.append(read_bit_block(pixels, x_bit + bit_idx, y_bit))

# Convert to byte
value = 0
for b in bits:
value = (value << 1) | b

column_bytes.append(value)

row_count += 1
y_bit += 1

# Skip white separator row
if row_count < BYTES_PER_COLUMN:
y_bit += ROW_SEPARATOR_BITS

decoded_bytes.extend(column_bytes)

x_bit += BITS_PER_BYTE

# Write output
with open("RDTAPE.txt", "wb") as f:
f.write(bytes(decoded_bytes))

print("Decoded to RDTAPE.txt")


if __name__ == "__main__":
main()

Konklusion

Vi kan ser hur arbetsprocessen då jag använder AI för programmering förändras radikalt. Jag behöver inte längre i detalj kunna programmeringsaspråkets syntax och semantik. Jag försöker endast förklara för AI, så väl jag kan, exakt vad jag vill ha på en hög nivå i form av naturligt språk. Det är självklart att jag fortfarande måste känna problemkontexten. Jag måste ha en mental bild av hur data är kodat i Byte, Bit samt veta hur forntidens hålremsa i princip såg ut. Jag behöver däremot inte i detalj veta hur problemet skall splittras up i form av datastrukturer och funktioner för att skapa ett fungerande program, detta görs av AI.

Min uppfattning är att AI är ett verktyg som motsvarar övergången från handverktyg till långt automatiserade verkstäder. Det krävs mycket kunnande för att styra AI men kunskapen som behövs ligger på en betydligt högre abstraktionsnivå.

Jag kör den nya programkod AI ger vid varje iteration och försöker förklara för AI vad jag vill korrigera. Jag uppfattar arbetsprocessen som den jag körde med i forntiden då jag sålde egna industri mätsystem till kund. Det gällde då att själv vara i samma situation som chatGPT d.v.s. försöka förstå vad kunden egentligen försökte köpa. Då jag visste vad problemet var i detalj var inte implementationen något väldigt stort problem.

Fortsättning

Min tanke är att skapa automatik där jag använder Linux på huvuddatorn till att hantera utskrift till ”hålremsestansen”. På motsvarande sätt vill jag enkelt komma åt lagrade program och filer i mitt virtuella ”bibliotek” bestående av lagrade hålremsor. I forntiden skulle jag ha varit tvungen att gå till ett skåp och söka fram rätt hålremsa och hoppas att den var korrekt märkt, lägga in hålremsan i läsaren samt starta avläsningen. I mitt system bygger jag upp det så att jag kan be hålremsläsaren hämta rätt hålremsa och läsa av den. Mitt system motsvarar inte helt det som användes i forntiden men det är relativ nära. Jag behöver inte stiga upp ur stolen för att hämta rätt hålremsa. Jag har inte heller implementerat läsning via CP/M BIOS-anrop d.v.s. min arbetsprocess kommere att skilja sig från motsvarande process i forntiden.

Min plan är att förutom en stans för hålremsor också skapa ett enkelt sätt att inifrån CP/M skriva ut textfiler till Linux skrivare. Även här tänker jag fuska i viss mån dock så att systemet ungefär fungerar som i forntiden.

Om energi och intresse räcker till kan det hända att jag också skapar system för att lagra CP/M program och data till filer på magnetband. Jag använder i såfall linuxens ljudsystem för att skapa ljudfiler som kan läsas och återskapas till program eller data.

Mera om implementation och AI-hjälp i nästa artikel.

Global morsesändare

09/02/2022

Ett inlägg i serien datorarkeologi.

En gammal traditionell morsesändare sänder morse via en radiosändare. Om vi går mer än hundra år tillbaka i tiden så kunde sändningen gå över en enda tråd d.v.s. mellan två punkter vilket är ganska begränsat … så långt tillbaka går vi dock inte.

Om jag vill skicka iväg ett meddelande till i princip hela världen så hur gör jag? Problemet är främst hur jag skall kunna lagra den skapade ljudfilen automatiskt på en åtkomlig plats ute på nätet. Jag hade ursprungligen tänkt mig att jag lagrar filen på spegling.blog men jag råkade ut för en del tekniska komplikationer då jag försökte göra detta automatiskt. Jag kom då på att jag har en urgammal blog http://www.kolumbus.fi/larsil som stöder ftp-protokoll. Jag kan alltså flytta morse ljudfilen till kolumbus servern med ftp på samma sätt som jag flyttade filen från PDP11 till min huvuddator deNeb.

Att flytta en nygenererad morse ljudfil till kolumbus betyder att jag kör ett skript send.ftp som automatiskt flyttar filen upp till kolumbus där den är ”synlig för hela världen”.

#!/bin/bash
HOST=www.kolumbus.fi
USER=xxxxxxxx
PASSWORD=xxxxxxxxxxxx
ftp -inv $HOST <<EOF
user $USER $PASSWORD
put morse_message.mp3
bye
EOF
echo "Cleaning system variables"
unset HOST
unset USER
unset PASSWORD

Orsaken till att jag använder unset HOST etc. är att jag inte vill att dessa systemvariabler skall bli liggande i maskinen trots att maskinen naturligtvis är skyddad. Utan unset kunde en person som har vägen förbi, och terminalfönstret är öppet, får reda på t.ex. kolumbusserverns användarnamn och password genom att ge kommandona:

echo HOST
echo USER
echo PASSWORD

För att översätta en textrad till morse och flytta resultatet till Internet ger jag följande kommandon:

./smorse.run
Skriv text att sända som morse:sssss lars silen sssss
Ord=sssss
s  ***  ti ti ti 
s  ***  ti ti ti 
s  ***  ti ti ti 
s  ***  ti ti ti 
s  ***  ti ti ti 
Ord=lars
l  *-**  ti taa ti ti 
a  *-  ti taa 
r  *-*  ti taa ti 
s  ***  ti ti ti 
--- etc ...
Jag valde att omge namnet med 's' eftersom bokstanen 's' i morsealfabetet är '***' d.v.s. ti ti ti vilket är lätt att känna igen. 
Flyttning till servern kolumbus:
./send.ftp
Connected to xxxxxxxxxxxxxxxxxxx
220 ProFTPD Server (Elisa Oyj FTP Service)
331 Password required for xxxxxxxx
230 User xxxxxxxx logged in
Remote system type is UNIX.
Using binary mode to transfer files.
local: morse_message.mp3 remote: morse_message.mp3
200 PORT command successful
150 Opening BINARY mode data connection for morse_message.mp3
226 Transfer complete
169291 bytes sent in 0.10 secs (1.6789 MB/s)
221 Goodbye.
Cleaning system variables

Läsaren kan lyssna på hur morsesändaren låter nedan:

Datorarkeologi

10/12/2021

Dessa funderingar skrev jag som ett Facebook inlägg men de kan eventuellt intressera någon läsare på bloggen varför jag kopierar över inlägget också hit.

Jag bekantar mig med hur det känns att köra en dator från mitten av 1970-talet. Maskinen är en Digital PDP-11/70 minidator som kostade ca. $14000 då jag var en fattig student på Helsingfors Universitet. Det är fråga om en relativt sen variant med mycket centralminne som vid denna tid var RAM och inte ferritkärnminne.

Mycket centralminne i maxkonfiguration betyder 4 megabyte minne vilket dock skall jämföras med IBM:s första PC som kom ca. 10 år senare och rätt länge hade en max konfiguration på 640 kilobyte d.v.s. 1/6 av PDP-11. PDP-11 kördes ofta med tidsdelning d.v.s. flera terminaler var kopplade till samma maskin och flera personer kunde alltså samtidigt dela på maskinen.

Då jag i början av 1980-talet fungerade som data-assistent på Ammattikoulujen opettajaopisto i Tavastehus så hade man en, om jag kommer rätt ihåg, Eclipse Nova minidator (motsvarade ungefär PDP-11) som en hel dataklass i den tekniska skolan bredvid kompilerade Fortran mot … 20 elever per klass. Resultatet var att maskinen kroknade d.v.s. man kunde vänta flera minuter på något livstecken på den egna terminalen men maskinen kraschade inte! Resultatet var att jag skrev en del program på en åtta bitars CP/M maskin (som ungefär motsvarade en Commodore 64) som var härlig att jobba med eftersom jag hade tillgång till den helt ensam.

Min PDP-11/70 kostar idag ungefär 50E. Maskinen emuleras i en Raspberry Pi model 3 med 1 Gigabyte minne och 16 Gigabyte ”skivminne” på ett mikro sd-kort. Vid den här tiden hade en hårdskiva typiskt en kapacitet på 5-10 Megabyte d.v.s. 1/1000 av Raspberry Pi:s mikro SD korts kapacitet. Den lilla Raspberry Pi maskinen har en kapacitet som man sannolikt endast kunde drömma om på Tekniska högskolan på den tiden. På SD-kortet ryms mängder med operativsystem och program för PDP-11.

För tillfället kör jag BSD 2.11 från slutet av 1980-talet. Som linuxanvändare är det inget problem att köra BSD-unix. Kommandona, då man kör i terminal, är de samma. Ett grafiskt användargränssnitt saknas men jag kan köra grafiska program i en emulerad vektorgrafisk terminal på Linuxsidan i ett fönster.

Jag jobbar mot PDP-11 så att jag från min vanliga dator loggar in över nätet med ssh. Till planerna hör att då nästa sats komponenter fås fram så bygger jag en frontpanel med blinkande ledar med plats för Raspberry Pi. Det är främst strömbrytarna på fronten som ger problem, de är specialkonstruerade för den här byggsatsen för att möjligast väl matcha den ursprungliga modellen. Det är intressant att se att emuleringen är så exakt att det är möjligt att via ett adapterkort använda ursprungliga kort för en riktig PDP-11. Man har t.ex. kopplat in ett ursprungligt kärnminne (ferritringminne) och kört program från detta.

Man får en bild av den extrema utvecklingen på datorområdet då man jämför en modern Raspberry Pi med PDP-11 (jag använder en äldre Raspberry Pi modell 3 som jag råkade ha liggande). Raspberry Pi har fyra processorkärnor med ordlängden 64 bitar jämfört med PDP-11 som hade 16 bitar. Ordlängden som sådan betyder att RPi är kanske 4x effektivare och då vi har fyra processorer i RPi så kan processorn utföra 4x mera arbete.Ursprungligen så körde PDP-11 med en cykeltid på 800 ns vilket betyder en klockfrekvens på ca. 1,25 MHz vilket skall jämföras med RPi som klockas ca. 1000 ggr högre. Jag nämnde att PDP-11/70 hade ett maximalt minne på ca. 4 megabyte vilket skall jämföras med dagens RPi modell 4 som har 4 gigabyte (1000 ggr mer).

Om vi lägger ihop skillnaden mellan dagens 50Euros RPi och gårdagens $14000 minidator så ser vi att RPi totalt sett har en kapacitet som är ungefär 4x4x1000x1000 = 16 000 000 större än gårdagens PDP-11/70. Vi förstår också hur stor kapaciteten hos en Raspberry Pi är idag då vi noterar att jag kör Linux på RPi med grafiskt användargränssnitt och under Linux på RPi kör PDP-11 emulatorn SIMH som alltså låtsas vara en äkta PDP-11/70 och på den emulerade PDP-11/70:an kör jag BSD Unix v. 2.11. Den emulerade PDP-11/70 kör med ungefär dubbel hastighet mot orginalet!

En annan intressant jämförelse är att en normal Android mobiltelefon har ungefär samma hårdvara som en Raspberry Pi (eventuellt något bättre). Mobiltelefonen har alltså en total beräkningskapacitet som är miljoner gånger större än hos 1970-talets minidator. Största delen av processorkraften i dagens datorer/telefoner bränns dock på all världens bling bling och gårdagens maskin känns förvånande OK bara man inte tvingar den att rita ett grafiskt användargränssnitt med halvtransparenta fönster etc.

Jag kör ”adventure” som är ett textbaserat spel med något som påminner om Morias grottor i Sagan om ringen. Programmet körs under UNIX (BSD 2.11 som de facto är detsamma som BSD 4.3 med alla PDP-11 upgraderingar). Fönstret finns på min vanliga bordsdator som kör Linux och jag kopplar upp mot PDP-11/70 över nätverket med ssh.

Dagens ”Minidator” en Raspberry Pi model 3 emulerar gårdagens mycket använda Minidator PDP-11/70 i dubbel hastighet. Notera att det inte finns någon fläkt då Raspberry Pi förbrukar endast ca. 5W. Orginalet d.v.s. den riktiga PDP-11:n var högljudd eftersom hela maskinen ursprungligen var byggd från diskreta TTL-kretsar med mycket låg integrationsgrad. Senare modeller av PDP-11 hade processorer integrerade i en eller ett litet antal kretsar. Med högre integrationsgrad så minskade också strömförbrukningen.

Några källor:

Om någon är intresserad av att försöka sig på lödning så är detta ett relativt enkelt projekt:

https://obsolescence.wixsite.com/obsolescence/pidp-11-technical-details

Wikipedia har en rätt bra artikel om PDP-11:

https://en.wikipedia.org/wiki/PDP-11

Ett nygammalt verktyg

03/05/2021

Jag gjorde mig ett nytt verktyg idag. Bilden nedan visar hur verktyget ser ut. Jag gissar att man för 50-100 år sedan utan problem skulle ha identifierat verktyget som något välbekant och väldigt användbart.

Vad använder man verktyget till? Jag fyller på artikeln efter några dagar med mera info.

Fig. Ett gammalt även idag användbart verktyg tillverkat av en ”vantskruv” dock icke marin sådan. Vantskruven har modifierats med vinkelslip och 3D-skrivare (handtag). Kostnad totalt ca. 10 Euro (100 SEK).

Reparation av en Pentax K5 kamera

27/11/2020

Pentax har i många år byggt fina systemkameror som ofta har varit vattentäta/väderskyddade. Statusmässigt ligger Pentax antagligen lite i skuggan av Nikon och Canon samt engefär jämbördigt med Sony. Pentax sägs ha en mycket pålitlig användargrupp som sägs bestå främst av äldre män som använde Pentax i sin ungdom. Pentax var en av de stora tekniska innovatörerna på 1960-70-talet dock så att Nikon på det professionella området körde förbi.

I likhet med alla moderna systemkameror så monteras objektiv via en snabbfattning, bajonett. Den av Pentax utvecklade K-bajonetten har använts på licens av flera andra tillverkare vilket betyder att man har tillgång till mängder av högklassig optik på den begagnade marknaden ofta för en spottstyver.

Min Pentax K5 stomme fick plötsligt ett fel. Den tryck-knapp som frigör objektivet lossnade och försvann. Kameran är helt användbar men för att byta objektiv behövdes en liten bit tex. pianotråd som man kunde använda för att trycka ner ett frigöringslås. Inte bekvämt och allt annat än vattentätt.

Jag vet att somliga Pentaxägare har fått reservdel gratis via Pentax men det har varit oklart vilka vägar detta har skett. Då jag googlade kring detta stötte jag på nedanstående länk:

https://www.thingiverse.com/thing:1731720

Länken går till en 3D-utskriven frigöringstangent. Eftersom jag har en egen 3D-skrivare så var det naturligt att testa om det här kunde vara en lösning.

Den 3D-utskrivna nya tangenten har en axel som är mycket tunn. Detta är oftast ett stort problem vid utskrift. Utskriften av en tunn spets går så snabbt att plasten inte hinner stelna mellan plastlagren vilket leder till att komponenten ändrar form och sjunker ihop. Lösningen på detta är väldigt enkel. Om man har något annat storre objekt som man kan skriva ut samtidigt så lönar det sig att göra detta. Det större föremålet ger tid för det lilla att svalna vilket höjer kvaliteten. Ett annat lika enkelt alternativ är att i stället för att skriva ut endast ett enda litet föremål så skriver man ut, i mitt fall nio st. Efter utskrift väljer man det exemplar som blev bäst.

Frigöringsknappen för objektivet syns nere yngeför vid kl. 7 strax vänster om objektivet. Den utskrivna delen är redan på plats då jag glömde att ta en bild av situationen före den nya komponenten monterades.

Notera hur jag skrev ut 9 st nya tryck-knappar av vilka jag använde endast en.

Belysningen råkar framhäva den sandpapprade sidan av den nya frigöringsknappen. I normal belysning reagerar man inte på att delen är 3D-utskriven och inte orginal.

Lyckat ”hack”! Det är alltid trevligt då man lyckas förlänga livet på en teknisk mojäng som har ett litet men mycket störande fel. Nu blir det naturligtvis intressant att se hur länge den nya delen håller.


Pointman's

A lagrange point in life

THE HOCKEY SCHTICK

Lars Silén: Reflex och Spegling

NoTricksZone

Lars Silén: Reflex och Spegling

Big Picture News, Informed Analysis

Canadian journalist Donna Laframboise. Former National Post & Toronto Star columnist, past vice president of the Canadian Civil Liberties Association.

JoNova

Lars Silén: Reflex och Spegling

Climate Audit

by Steve McIntyre

Musings from the Chiefio

Techno bits and mind pleasers

Bishop Hill

Lars Silén: Reflex och Spegling

Watts Up With That?

The world's most viewed site on global warming and climate change

TED Blog

The TED Blog shares news about TED Talks and TED Conferences.

Larsil2009's Blog

Lars Silén: Reflex och Spegling