Archive for the ‘Dator arkeologi’ 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);
}

Datorarkeologi 2026

02/04/2026

Jag har lekt med en Clockwork PicoCalc som man kan köpa som en enkel byggsats. PicoCalc är egentligen ett tangentbord och en skärm med 320×320 pixlar d.v.s. en skärm som motsvarar en typisk hemdator från 1980-talets början t.ex. en Commodore 64. Byggsatsen görs till en fungerande dator genom att lägga till något av processorkorten Rasapberry Pi Pico, Pico2 eller Pico W. Hobbyister har också kört andra processorer och lyckats köra Linux utan större problem. Användning av andra processorer än Pico kräver dock i allmänhet att man hackar hårdvaran d.v.s. löder om själva PicoCalc. Jag tycker av princip illa om att gå in och löda i fungerande hårdvara vilket är orsaken till att jag kör en normal PicoCalc och inte Linux.

Bilden visar att Turbo pascal d.v.s. turbo har startat och programmet frågar efter den fil vi vill editera och kompilera. I vårt fall TEST.PAS. Vi ser en CP/M mikroskärm med radlängden 40 tecken. En speciell font har skapats för att få möjligast mycket text att rymmas på en rad.

PicoCalc ser ut som en normalstor kalkylator från 1980-talet i vilken man kan programmera och köra Basic, Python och Lua. Under Basic och Python fungerar den förvånande bra som en programmerbar kalkylator. Det visade sig snabbt att processorn är tillräckligt snabb och det finns tillräckligt minne för att köra operativsystemet CP/M som var dominerande från mitten av 1970-talet till mitten av 1980-talet. CP/M trängdes ut av IBM:s PC kring mitten av 1980-talet. IBM PC körde ett mycket liknande operativsystemsom CP/M kallat Ms-DOS.

https://en.wikipedia.org/wiki/IBM_Personal_Computer

https://en.wikipedia.org/wiki/CP/M

ARM processorn i en RPi Pico klarar av att emulera en Intel 8080 eller en Zilog Z80 i realtid. Pico:ns lilla minne räcker till för att ge CP/M ett fullt arbetsminne trots att en stor del av Pico kapaciteten går åt till att emulera en främmande processor.

Storleken på en Raspberry Pi Pico är ungefär två normalstora frimärken i bredd. Bildens Pico har storleken 20×50 mm.

Ett mycket vanligt programmeringsspråk på 1980-talet både under CP/M och PC-DOS var Turbo Pascal som erbjöd en enligt dåtida mått mycket trevlig programmeringsomgivning för programspråket Pascal. Pascal är ett i många avseenden mycket klarare och ”renare” programspråk än t.ex. programspråket C som användes då Unix och senare Linux skapades. Med hjälp av Turbo Pascal kunde man i akademiska sammanhang enkelt t.ex. göra statistiska analyser på en liten bordsmaskin, analyser som tidigare krävde tillgång till en stordator med all den byrokrati och kostnad detta medförde. Med hjälp av Turbo Pascal kunde man också med hjälp av diverse instickskort använda en PC för förvånande avancerade automatiska mätningar. Turbo Pascal fanns tillgängligt både för CP/M baserade datorer och för Ms-DOS maskiner d.v.s. IBM-PC samt mängder av liknande kloner.

Att köra program i 64 kilo Byte minne

Dagens datorer t.ex. en PC tenderar att ha ett centralminne på 4-32 GigaByte ibland mera. Min personliga PC som kör Linux har 32 GigaByte minne som är 500 000 gånger större än än det totala minnet i forntidens CP/M dator.

Jag kompilerade på skoj ett program TEST.PAS som ger ett nytt kommando till CP/M systemet som enligt Unix tradition kallas cat d.v.s. catenate eller skriv ut/kombinera ihop. Programmet kan läsa en eller flera textfiler och skriva ut dem till skärmen eller skicka resultatet till en annan fil. Under Linux skulle jag kunna skicka filen utan förändringar till t.ex. LPR dvs skriva ut till skrivare allt det som annars skulle gå till skärmen. Jag antar att jag inte kommer riktigt så enkelt undan under CP/M. Länken nedan diskuterar hur man kommer åt en skrivare, stans för hålremsa etc. under CP/M.

https://www.mark-ogden.uk/mirrors/www.cirsovius.de/CPM/Projekte/Artikel/Grundlagen/IOByte/IOByte-de.html

Då jag kompilerar programmet under Turbo Pascal under CP/M på PicoCalc får jag ett körbart program med storleken 635 bytes vilket motsvarar en vanlig text utskriven med 80 tecken par rad som då blir ungefär åtta rader lång. Jag testade programmet under CP/M så att jag använde det kompilerade programmet till att lista ut sig själv på PicoCalc skärmen. Texten är väldigt liten och ganska svårläst men programmet fungerade OK.

En intressant fråga blir då: Om jag tar samma program och kompilerar det under Linux (eller Windows eller Mac) hur stort blir då programmet på en modern dator? Notera att jag kompilerar för användning på kommandoraden helt utan GUI (grafiskt användargränssnitt). Om jag skulle skapa ett GUI för programmet gissar jag att det skulle växa ytterligare med kanske en faktor tio.

För kompileringen under Linux använde jag fpc d.v.s. Free Pascal Compiler som är basen i bl.a. Lazarus som idag är en trevlig omgivning för att generera program med grafiskt användargränssnitt GUI. Lazarus är mycket nära besläktad med Delphi (tidigare Borland Delphi). Lazarus är trevligt eftersom program enkelt kan skrivas för Linux, Windows, Mac, Android och FreeBSD och det finns funktionalitet som i hög grad förenklar designen av ett program med GUI. Fpc är således funktionsmässigt nära släkt med Borlands Pascal d.v.s. Turbo Pascal.

Jag kompilerade om programmet TEST.PAS under fpc med:

fpc -gl TEST.PAS

Kompileringen gick igenom utan problem men jag putsade bort en del onödiga fyllnadstecken i slutet av filen, tecken som i sig inte störde kompileringen. Då jag körde programmet:

./TEST TEST.PAS

Skrevs texten till TEST.PAS ut korrekt men jag fick ett felmeddelande på den nästsista raden (se programlistningen nedan) där programmet försöker stänga filen wrf d.v.s. en fil som används om man skulle vilja att resultatet går till en annan fil och inte på skärmen. Om jag skriver till skärmen behövs inte wrf och om jag försöker stänga wrf som aldrig har öppnats uppstår det ett fel. Felet är enkelt att åtgärda genom att kontrollera om vi har alternativet redirect innan vi försöker stänga filen. Längden på det körbara programmet blev nu under 64-bitars linux med kompilatorswitchen -gl som jag använde för att komma åt vilken rad i programmet som gav felet 420968 Byte d.v.s. ungefär 663 gånger större än under CP/M.

Om jag kompilerar om programmet efter rättning utan flaggan -gl blir resultatet 188304 Byte d.v.s. ungeför 297 ggr större än under CP/M.

Vad beror storleksskillnaderna på? En självklar orsak är naturligtvis att datorns s.k. ordlängd d.v.s. hur lång en typisk instruktion är har vuxit från 8-bitar till 64-bitar. Om vi antar att vårt program fortfarande behöver ungefär samma maskinspråksinstruktioner så kan vi få en vettigare jämförelse genom att dividera Linux-varianten med åtta d.v.s. ungefär 23538 instruktioner vilket är 37 ggr större än antalet instruktioner i CP/M varianten. Jag gissar att man genom väl genomtänkta optimeringar vid kompileringen kan krympa Linuxvarianten ytterligare. En stor orsak till att programmet har blivit större är antagligen kontroll av olika fel d.v.s. debuggning som utvecklats mycket under åren. De bibliotek vi länkar mot under Linux har bättre funktionalitet men de har också vuxit betydligt och innehåller sannolikt kod vi inte behöver. Vid kompileringen kan vi ange olika nivåer av optimering som påverkar snabbhet , minnesåtgång samt stöd för olika typer av felsökning. Körning av programmet ger:

./TEST TEST.PAS
ParamCnt=1/home/lasi/Prog/Pascal/TEST
Testing for file redirection
Got last par=TEST.PAS
TEST.PAS
program cat(input,output);
(* Make a new CP/M command cat
cat xxx yyy zzz … List all files to screen
cat xxx yyy >zzz List files xxx, yyy to the file zzz
*)
var i,nfiles:integer;
red:Boolean;
rdf: Text;
wrf: Text;
line: String[255];
destF: String[10];
function CkRedirect: boolean;
(* Is the destination a file or stdout?
The last file is assumed to be the redirection file if it exists.
Write all non redirection files to a temporary file and then
dump that to stdout or rename it to the destination file.
*)
var line:String[80];
begin
writeln('Testing for file redirection');
line := ParamStr(ParamCount);
writeln('Got last par=',line);
if (line[1] = '>') then
begin
writeln('Found redirect');
CkRedirect := True;
end;
end;
begin
writeln('ParamCnt=',ParamCount,ParamStr(0));
nfiles := ParamCount;
red := False;
if CkRedirect then
begin
writeln('Redirect');
nfiles := ParamCount - 1;
red := True;
destF := ParamStr(ParamCount);
Delete(destF,1,1); (* Remove '>') ) writeln('Redirect to:', destF); end; for i:=1 to nfiles do begin ( Type one file ) writeln(ParamStr(i)); Assign(rdf,ParamStr(i)); Reset(rdf); if red=True then begin Assign(wrf,destF); Rewrite(wrf); end; while not Eof(rdf) do begin Readln(rdf,line); if red=True then begin Write(wrf,line); end else Writeln(line); end; close(rdf); Writeln(' '); end; ( Caused error on the fpc compiler *)
if CkRedirect then close(wrf);
Writeln('Done …');
end.

Testing for file redirection
Got last par=TEST.PAS
Done …

Konklusion

Den här lilla programmeringsövningen visar egentligen hur ofattbart mycket tekniken har utvecklats under en livstid. Generellt kan man antagligen säga att alla intressanta parametrar har förbättrats med en faktor tusen eller mera. Några exempel:

  • Processons klockfrekvens 1 MHz –> 4 GHz (dagen datorer är ungefär 4000 ggr snabbare sett till processorns klockfrekvens.
  • Maskinens centralminne RAM 64 kByte –> 64 GByte (1 000 000 gånger större minne i dagens persondatorer av topp klass).
  • Skivminne/Lagringsminne 100 kByte –> 4 TByte (4 000 000 ggr större idag).
  • Maskinens pris $5000 –> $100 (50 ggr dyrare än idag). Notera att jämförelsen är felaktig eftersom penningvärdet har försämrats kraftigt på 40 år. Jag gissar att en korrekt jämförelse kunde vara åtminstone 100 ggr.

Om vi glömmer dagens AI boom där AI av vanliga användare körs över nätet och således inte belastrar den egna maskinen i någon större utsträckning så används datorerna fortfarande till liknande uppgifter som i ”forntiden” d.v.s. textbehandling, skicka brev och hantera kalkylark av olika typer. Gårdagens datorer kändes responsmässigt ungefär lika som dagens datorer. Vart har en prestandaökning på mellan 1000 och 1 000 000 ggr. försvunnit? Min uppfattning är att vi har fått en ofantlig mängd ”bling” som egentligen endast gör skärmen trevligare att titta på. Vi har halvtransparenta fönster som uppdateras hela tiden medan vi flyttar dem. En gammal dator skulle ha storknat genast. I våra datorer körs mängder av program i bakgrunden hela tiden. Program som meddelar oss att vi har fått meddelanden, program som skickar information om var vi gör till Microsoft etc. Det är lätt att bränna datorkapacitet och minneskapacitet i en värld där maskinen jag använder egentligen redan länge har varit tillräckligt kraftfull för det den skapades för.

Min laptop som också kör Linux fungerar helt OK trots att den är strax över tio år gammal. En bekant fick problem d.v.s. den heliga röken som datorn antagligen kör på slapp ut och maskinen slutade fungera. Jag hjälpte personen att rädda foton och dokument från datorn och fick sedan överta skrotet. Efter egen reparation har maskinen fungerat klanderfritt i många år. Jag kör en linuxvariant där allt onödigt ”bling” är bortplockat och maskinen fungerar bra som den är.

Ett annat intressant exempel på datorns ofantliga utveckling är ”mini”-datorn PDP-11 som operativsystemet Unix utvecklades på. Maskinen skulle ha kostat en privatperson tiotals tusen dollar att köpa. Jag har idag en replica av en PDP-11/70 d.v.s. den sista PDP:n som hade en frontpanel som tillåter mig att knappa in maskinkod bit för bit. Min replika kör, liksom CP/M på PicoCalc, en emulator men i detta fall på en Raspberry Pi 4. Den emulerade minidatorn från 1970-talet kör engefär tio gånger snabbare än orginalet trots att varje maskininstruktion emuleras av ett emulatorprogram på Raspberry Pi. En raspberry pi kostar idag beroende på variant kring 100 Euro.

Världens första egentliga superdator var Cray-1. Maskinen användes för mängder av vetenskapliga beräkningar. Man har uppskattat att en Raspberry Pi 5 (100 Euro), då man använder display hårdvaran (GPU) som en vektorprocessor är snabbare än en Cray-1 superdator. En Cray-1 behövde ungefär 115 kW eleffekt. En Raspberry Pi behöver ungefär 7W. Även på effektområdet ser vi alltså en förbättring på ungefär sextontusen gånger.

https://en.wikipedia.org/wiki/Cray-1

Steganografi och hemliga meddelanden

10/02/2022

Datorarkeologi, hemliga meddelanden.

Jag tror att rätt många barn som nyligen har lärt sig läsa har gjort sina egna hemliga alfabet där det vanliga latinska/nordiska alfabetet ersätts med ”hemliga tecken” så att meddelandet blir oläsligt. Alternativt kan man använda Cesars kryptering där man tänker sig alfabetet som två ringar där man förskjuter den ena med ett visst antal steg. Om vi antar att vårt vanliga alfabet är den första ringen och vi vrider kryptoalfabetsringen tillbaka ett steg så får vi ett kryptoalfabet där

Vanlig text    Krypterad text
a          --> b
b          --> c
osv.

Ett hemligt meddelande av denna typ är naturligtvis urenkelt att lösa. Med dator kan vi naturligtvis enkelt och snabbt testa alla kombinationer men också för hand går det snabbt att titta på tex. fördelningen av de vanligaste bokstäverna i det gissade språket i meddelandet vilket ger information om hur alfabetet skall förskjutas. Ett barns hemliga alfabet är likaså väldigt enkelt att läsa om vi har en vettig längd på meddelandet. Vi kan ur ordlängd etc. gissa bokstäver och rätt enkelt få fram det hemliga alfabetet.

Enkla substitutionsskiffer dvs. förskjutning av bokstäverna i alfabetet eller hemliga symboler i stället för det normala alfabetet är så enkla att lösa att man kan säga att ett meddelande som kodats på detta sätt egentligen endast visar att meddelandet inte är tänkt att läsas rakt upp och ner.

En intressant detalj är Stanley Kubricks film ”Ett rymdäventyr 2001” där skeppets tänkande dator heter ”HAL”. Av en slump 😉 så är HAL ordet IBM förskjutet en bokstav i riktning mot A d.v.s. IBM har kodats met ett Caesarskiffer försjutet endast ett steg i riktning mot A. Om man vill göra en textfil besvärlig att läsa men helt utan risk att man tappar lösenordet till texten så kan man koda en text med Unixverktyget rot13 som alltså förskjuter alfabetet med 13 steg. Säkerheten är lika med noll men det går inte att se vad texten innehåller bara genom att skumma av den med ögat.

Steganografi

Steganografi är tekniken att gömma ett meddelande tex inom ett skenbart oskyldigt meddelande. Antag att en medeltida krigsherre börjar ha slut på krutet och skickar ett meddelande till den egna kungen där han ber om 1500 kaggar fläsk, 20 kärror bröd … och på slutet konstaterar han krigståget måste avbrytas om han inte snabbt får mera mjöl, smör och salt … där den hemliga betydelsen kunde vara ‘mjöl=kol’, ‘smör=svavel’ och ‘salt=salpeter’ (de tre ingredienserna i svartkrut).

Under antiken kunde en kung t.ex. skicka en slav med ett hemligt meddelande så att håret rakades av huvudet på slaven varefter meddelandet skrevs på skalpen. Då håret hade vuxit ut skickades slaven till mottagaren som rakade av håret igen och läste meddelandet. Andra alternativ kunde vara att lägga in ett meddelande i ett bakat bröd … alternativen är många.

Vi såg i föregående inlägg hur man kan generera morsekod med hjälp av ett dataprogram. Ett kort meddelande, några ord, går på kanske 20 sekunder och ljudfilens längd är ungefär en miljon tecken. Antag att jag, den hemliga agenten, vill skicka en femsidig rapport till min uppdragsgivare utan att väcka uppmärksamhet. Jag kan då ta i bruk mitt morseprogram och regelbundet under en tid skicka t.ex. hälsningar till olika personer med morse. Meddelandet byts med någon dags mellanrum och fienden tittar på varje meddelande … men hittar inget.

Jag har redan tidigare, med tanke på användning för steganografi, modifierat både ljudfilerna och pauserna jag använder för att bygga upp meddelandena så att det finns brus i signalerna t.ex. med toppvärdet ungefär 8- bitar av den 16 bitars amplitud ljudfilen tillåter. Bruset ligger då på ungefär 0.5% vilket inte är speciellt störande.

Antag att jag vill skicka min rapport som innehåller 10000 tecken inbakad i morsesignalen. Min text består av tecken som alla ryms i en byte (8 bitar) som i sin tur ryms inom bottenbruset i min morsesignal. Jag behöver nu endast via någon annan kanal meddela min uppdragsgivare att ”Kom ihåg att gratulera den brittiska drottningen då hon fyller 100 år”. Hundra i detta meddelande betyder att jag byter ut vart hundrade talvärde i bruset i meddelandet mot en bokstav. Bruset kommer att öka marginellt men inte så att man enkelt hör skillnaden. Jag måste då skriva ett morsemeddelande som producerar en morse wav-fil med längden större än en miljon talvärden … enkelt.

Jag lägger ut den mixade morsefilen på den vanliga platsen för min morsehobby och min uppdragsgivare laddar ner filen och kör ett program som plockar ut vart hundrade talvärde i wav-filen och dumpar resultatet i en textfil som är min återskapade rapport.

Jag har alltså dolt en textrapport på 10000 tecken i ett oskyldigt morsemeddelande som gick på kanske 20-30 bokstäver. Notera att den sända dolda texten naturligtvis i sig kan vara krypterad innan den läggs till bruset i morsekoden. Det är naturligtvis självklart att texten inte behöver läggas in med 100 teckens intervall utan jag kan använda en slumptalsgenerator som genererar en lämplig slumptalssekvens som säger hur långt avståndet är mellan det hemliga dokumentets bokstäver. Det är också sannolikt att jag endast vill lägga in text i bruset i pauser inte i den reguljära sinusvågen jag använder som ljud eftersom det kan verka misstänkt in enstaka sampel ligger mycket nära noll då sinusvågen ligger nära ett. Jag kan hoppa över områden där jag har en morseton. Både jag och min uppdragsgivare måste naturligtvis kunna generera samma sekvens. Det är självklart att det enkelt går att gömma stora mängder text i en ljudfil som ovan men också självklart att det krävs en del funderande över hur man bäst döljer att det ligger ett meddelande inbakat i filen.

Bilden visar ljusrött brus med amplituden 0.125 (signalen har amplituden 1.0). Om vi antar att ljudfilen är kodad som 16-bitars PCM så kan vi lägga in data med ett största värde 8192 utan att vårt hemliga meddelande sticker ut på något sätt.

Digitalbilder innehåller också enorma mängder data. En modern digitalkamera producerar bilder vars storlek är tiotals megabyte d.v.s. tiotals gånger större än morseljudfilen. Varje bildpunkt innehåller information om tre olika färger och varje färg kodas i en eller flera byte. Det är självklart att jag kan koda information, om jag så vill i pixlar i bilden t.ex. så att jag modifierar färgdata så att jag modifierar en färg så att om färgens värde är ”jämnt” d.v.s. 2, 4, 6 … 254 så skall färgen tolkas som en etta d.v.s. en bit med värdet = 1. På motsvarande sätt kan jag koda en nolla så att jag modifierar ett färgvärde till ”udda” som då tolkas som en bit=0. Förändringarna jag behöver göra är extremt små och sannolikt odetekterbara speciellt som mitt meddelande matar in varierande bitar. Slutresultatet är en bild som ser helt normal ut men i vilken jag kan koda in mängder av dolda data. Olika typer av steganografiska metoder används idag till att koda in t.e.x. copyright information i bilden vilket gör att det i efterhand går att bevisa vem som ägde rättigheterna till bilden.

Notera att det inte är självklart att ovan beskrivna kodningar överlever packning. Om jag konverterar min morse ljudfil till mp3 formatet så kommer filen att packas ihop betydligt. En mp3 kodning är inte förlustfri vilket betyder att om jag packar upp filen tillbaka till wav så har sannolikt mitt hemliga meddelande också förstörts. Samma princip gäller bilder. Enkla hemliga kodningar kan förstöras av packning till något icke förlustfritt format.

En motståndare som försöker hitta mina dolda meddelanden skulle sannolikt bli mycket misstänksam om jag ibland skulle skicka mp3 morsemeddelanden och andra gånger wav meddelanden. Lösningen skulle naturligtvis vara att alltid skicka wav-filer som är opackade.

Morseljudsignal till text

09/02/2022

Detta är ett inlägg i serien datorarkeologi.

I tidigare inlägg har jag visat hur man automatiskt kan översätta skriven text till morse och därefter generera en ljudfil som man t.ex. kunde köra igenom en radiosändare och om effekten är tillräckligt stark och reflexionerna i atmosfären optimala så kan man i princip höra morsemeddelandet överallt på jorden.

Vad gör jag om jag inte kan morse d.v.s. meddelandet är bara en okänd serie blippar. Kan jag skriva ett program som läser in morsekoden som ljud och översätter ljudmeddelandet till text? Det visar sig att detta, om signalen är optimal, är mycket enkelt att göra. Amplituden på alla ljudsignaler är exakt lika och alla pauser har någon av tre exakt definierade längder.

Signalen har följande utseende om jag tittar på ljudfilen med hjälp av programmet Audacity:

Vi ser att signalen är extremt ren och störningsfri. Pauser har ljudnivån noll helt utan något brus och signalen är en sinusvåg med konstant amplitud.

För att kunna läsa av signalen likriktar vi den först d.v.s. vi tar absolutvärdet av signalen så att alla negativa värden under strecket i figuren blir positiva. Signalen varierar våldsamt mellan 0 och ca. 0.5 och för att vi skall kunna bedöma om vi detekterar ljud eller tystnad filtrerar vi signalen så att vi beräknar medelvärdet av ett antal ljudvärden. Om medelvärdet är klart positivt så hör programmet ljud och då medelvärdet ligger mycket nära noll så är det tyst.

Programmet har en funktion/subrutin. Jag skrev denna gång programmet i språket python. Översättningen går till så att jag samlar ihop fragment av korta ljdsignaler (*) och långa ljudsignaler (-). Ljudsignalerna läggs till en textsträng ända tills vi stöter på en ”teckenpaus” d.v.s. en paus mellan bokstäver. Vi skickar nu textsträngen t.ex. ‘*-‘ till funktionen translate_char(tecken) som jämför morsetecknet med alla morsetecken i morsealfabetet och därefter skriver ut resultatet som bokstaven ‘a’ i detta fall. Vi nollar nu morsesträngen och börjar samla * rep – för följande tecken. För att läsa vad programmet gör så hoppar vi över funktionen translate_char och börjar läsa kommandoraden på vilken vi vill ha endast en parameter d.v.s. ljudfilens namn. Ljudfilen kan vara en mp3- eller en wav-fil. Om vi ger en mp3-fil så konverterar programmet automatiskt mp3 filen till en wav-fil eftersom wav-filen är lättare att hantera rent tekniskt.

#!/home/lasi/miniconda3/bin/python
# Name=morse_receiver.py
# The program reads an audio file and converts the audio back to plain text.
# The analysis works as follows:
# Read the message and record the lengths of sound and silence to a file.
# Determine the length of dots and dashes and the lengths of silence.
# Create a new file vith * - and the character interval + word interval.
# The file can now be analyzed for morse patterns and converted into text.
# This is free code. Use on your own risk.

import wavfile
import sys
import os


def translate_char(m_string):
	m_string.strip()
	if (m_string=="*-"):
		return "A"
	if (m_string=="-***"):
		return "B"
	if (m_string=="-*-*"):
		return "C"
	if (m_string=="-**"):
		return "D"
	if (m_string=="*"):
		return "E"
	if (m_string=="**-*"):
		return "F"
	if (m_string=="--*"):
		return "G"
	if (m_string=="****"):
		return "H"
	if (m_string=="**"):
		return "I"
	if (m_string=="*---"):
		return "J"
	if (m_string=="-*-"):
		return "K"
	if (m_string=="*-**"):
		return "L"
	if (m_string=="--"):
		return "M"
	if (m_string=="-*"):
		return "N"
	if (m_string=="---"):
		return "O"
	if (m_string=="*--*"):
		return "P"
	if (m_string=="--*-"):
		return "Q"
	if (m_string=="*-*"):
		return "R"
	if (m_string=="***"):
		return "S"
	if (m_string=="-"):
		return "T"
	if (m_string=="**-"):
		return "U"
	if (m_string=="***-"):
		return "V"
	if (m_string=="*--"):
		return "W"
	if (m_string=="-**-"):
		return "X"
	if (m_string=="-*--"):
		return "Y"
	if (m_string=="--**"):
		return "Z"
	if (m_string=="*--*-"):
		return "Å"
	if (m_string=="*-*-"):
		return "Ä"
	if (m_string=="---*"):
		return "Ö"
	if (m_string=="*----"):
		return "1"
	if (m_string=="**---"):
		return "2"
	if (m_string=="***--"):
		return "3"
	if (m_string=="****-"):
		return "4"
	if (m_string=="*****"):
		return "5"
	if (m_string=="-****"):
		return "6"
	if (m_string=="--***"):
		return "7"
	if (m_string=="---**"):
		return "8"
	if (m_string=="----*"):
		return "9"
	if (m_string=="-----"):
		return "0"
	# Primitive error check
	print("Error m_string=",m_string)
	print("Len=",len(m_string))
	return "?"


if (len(sys.argv)<1) or (len(sys.argv)>=3):
	print("Usage: morse_receiver.py snd_file")
	exit(0)

# We only process wav-files. If I get a mp3 then convert it to wav
# Add further conversions here as nedessary.
# Filtypen bestäms utifrån filnamnet inte från magisk filtyp.
snd_file = sys.argv[1]
print("File to process: ",snd_file)

if snd_file.endswith('.mp3'):
	print("MP3 file detected")
	# Convert to a wav file
	# model ffmpeg -i song.mp3 -ar 44100 song.wav
	cmd = "ffmpeg -i "+snd_file+" -loglevel quiet -ar 44100 -y "+snd_file+".wav >/dev/null"
	snd_file=snd_file+".wav"
	print("Converted file="+snd_file)
	# Convert the mp3 file to wav before processing.
	os.system(cmd)

f = wavfile.open(snd_file, 'r')
frames=f.num_frames
wav_data=f.read_float(frames)
ampl=0
i=0
snd = False
nosnd = True
pstart=0
sstart=0
my_ch = "";
for d in wav_data:
	ampl=(9*ampl+abs(d[0]))/10
	i=i+1
	if((ampl>0.1) and (nosnd==True)):
		sstart=i
		l = i-pstart
		if(l<5000):
			sp=0
		elif((l>5000) and (l<20000)):
			print(translate_char(my_ch)+"  "+my_ch)
			my_ch=""

		else:
			print(translate_char(my_ch)+"  "+my_ch+"\n")
			my_ch=""
		snd = True
		nosnd = False
	elif((ampl<0.01) and (snd==True)):
		pstart = i
		if((i-sstart)<5000):
			my_ch = my_ch + "*"
		else:
			my_ch = my_ch + "-"
		snd=False
		nosnd=True

Vi kontrollerar om vi fick en mp3-fil som parameter. Om detta är fallet så bygger vi upp ett kommando som en textsträng där programmet ffmpeg används för att göra en wav-kopia av mp3-filen. Kommandot utförs av det externa programmet ffmpeg genom att anropa det via system() d.v.s. vi gör inifrån programmet detsamma som vi skulle ha kunnat göra på kommandoraden.

if snd_file.endswith('.mp3'):
	print("MP3 file detected")
	# Convert to a wav file
	# model ffmpeg -i song.mp3 -ar 44100 song.wav
	cmd = "ffmpeg -i "+snd_file+" -loglevel quiet -ar 44100 -y "+snd_file+".wav >/dev/null"
	snd_file=snd_file+".wav"
	print("Converted file="+snd_file)
	# Convert the mp3 file to wav before processing.
	os.system(cmd)

Vi läser nu in hela ljudfilen i minnet, PDP11 skulle storkna i detta skede eftersom användarminnet skulle ta slut innan ens halva filen är läst … fint att ha lite mera minne i en modern dator! Vi skapar också några hjälpvariabler som vi behöver lite senare. Om jag skulle dekoda en fil på PDP11 så skulle jag läsa in data från skiva i stället för att ha filen i datorns minne. Att använda skiva i stället för minnet fungerar precis lika bra men hastigheten är kanske en tusendel jämfört med att jobba direkt mot minne. PDP11 från 1970-talet skulle tugga länge på en dekodning av en 20 sekunders ljudfil. Gissar någon minut.

f = wavfile.open(snd_file, 'r')
frames=f.num_frames
wav_data=f.read_float(frames)
ampl=0
i=0
snd = False
nosnd = True
pstart=0
sstart=0
my_ch = "";

Vi börjar nu läsa in värden, ett datavärde i taget från filen som alltså ligger i centralminnet (RAM) och beräknar ett flytande medelvärde över tio ljudvärden. Experiment visade att detta gav en pålitlig detektion. Jag är övertygad om att en annan filtrering skulle fungera lika bra. För en annan ljudfil genererad av en utomstående producemt så skulle vi antagligen behöve experimentera här.

for d in wav_data:
	ampl=(9*ampl+abs(d[0]))/10

Vi går nu vidare och ser när vi stöter på ljud och lägger då på minnet vilket ljudvärde 0 … vi hade och kontrollerar samtidigt om vi går från noll (icke ljud) mot ljud (större än ca. 0.1). Vi kan nu skilja på en ljudpuls och en paus. Genom att vi lagrade start och slut på pulsen så kan vi genom subtraktion beräkna längden på en ljudpuls eller en paus. Om vi stötte på en kort ljudpuls så lägger vi till ‘*’ i slutet av variablen my_ch . Om vi stötte på en lång ljudpuls så lägger vi till ‘-‘. Om vi stötte på en mellanlång eller lång paus så vet vi att tecknet är färdigt för översättning. Om vi stöter på en riktigt lång paus så vet vi att ett ord har passerats och då skriver vi ett radbyte för att underlätta läsningen.

Hela detektorn har då följande utseende:

for d in wav_data:
	ampl=(9*ampl+abs(d[0]))/10
	i=i+1
	if((ampl>0.1) and (nosnd==True)):
		sstart=i
		l = i-pstart
		if(l<5000):
			sp=0
		elif((l>5000) and (l<20000)):
			print(translate_char(my_ch)+"  "+my_ch)
			my_ch=""

		else:
			print(translate_char(my_ch)+"  "+my_ch+"\n")
			my_ch=""
		snd = True
		nosnd = False
	elif((ampl<0.01) and (snd==True)):
		pstart = i
		if((i-sstart)<5000):
			my_ch = my_ch + "*"
		else:
			my_ch = my_ch + "-"
		snd=False
		nosnd=True

Intresserade läsare kan lyssna på morsekoden nedan. Min reaktion på denna morsemottagare är egentligen att det visade sig vara mycket lättare att skriva mottagaren än jag hade väntat mig.

Om någon läsare vill skriva en mottagare för svårare morsekod t.ex. morsekod där alla tidsvärden varierar då en människa sänder morse så gissar jag att jag skulle spela in alla meddelanden. Därefter skulle jag skriva en dynamisk analysator som gissar längden på kort/långt ljud samt länden av pause. Dv.s. jag skulle mäta längden på alla tidsvärden separat för vatje fil.

En annan komplikation är att signalamplituden sannolikt skulle kunna variera rätt mycket. Även detta skulle kräva separat hantering så att gränsvärdet för ljud/tystnad skulle kunna väljas utgående från signalen i stället för att ges ett fast värde som i detta exemple.

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:

Att porta till BSD 2.11 Unix

07/02/2022

I en tidigare artikel visade jag hur man rätt enkelt kan skapa ett verktyg som möjliggör att tex. barn kan programmera på sitt eget modersmål i detta fall svenska. Språket ”sv” och kompilatorn ”csv” förstår svenska nyckelord och kompileras i två steg. Det första steget producerar c-kod och det andra skedet kompilerar från c-språket till körbar maskinkod (se den tidigare artikeln).

Hela paketet i den tidigare artikeln inkluderande lex-programmet som sköter översättningen från sv till c går på några hundra rader kod. Hur mycket jobb kräver det att modifiera programmen så att de kör på en urgammal Unix samt vilka är de begränsningar som är av sådan art att ny design behövs?

Kända begränsningar i PDP11

En maskin från 1970-talet var konstruerad för en specifikt engelsk/amerikansk miljö utan någon tanke på yttre nationella alfabet. Bokstäverna ÅÄÖ kunde stödas men sällan problemfritt. Jag valde att helt enkelt lämna bort prickar över åäöÅÄÖ d.v.s. texten ser ut som mina handskrivna skoluppsater på 1960-70-talet.

Maskinen stöder inte ljud vilket betyder att sändaren måste modifieras. I praktiken betyder detta att ljudet genereras på en annan (modern) maskin. Morsekoden som sådan lagras i en textfil och flyttas automatiskt till en annan maskin som tolkar den färdiga koden och genererar ljud. Jag kunde skriva kod på PDP11 emulatorn som styr in/ut pinnar på Raspberry Pi men jag uppfattar inte detta vara värt besväret.

Oväntade små problem

Det första problem jag stötte på var att det program lex genererade från swe.lex –> lex.yy.c inte kompilerade på PDP11 trots att jag körde en lex version som hörde hemma i BSD 2.11. Vad hände?

Jag kom inte ihåg att enradiga kommentarer ”// … till slutet av rad” är ett nytt påhitt. Jag löste problemet genom att ta bort denna typ av kommentarer. Samma sak måste göras med morseprogrammet. Det är naturligtvis möjligt att definiera om swe.lex så att jag genererar den gamla typens kommentarer i stället för nya enradiga. Detta blir en övning för framtiden.

Urgamla versioner av C-språket deklarerar parameterlistor till funktioner på ett annat sätt än moderna versioner. Ett exempel på detta är om jag vill läsa in någon parameter från kommandoraden.

I modern C-kod skulle jag deklarera kommandoradsparametrar på följande sätt:

main(int argc, char argv[])
{
  ... huvudprogrammet ...
}

I den gamla varianten av C-språket visar det sig att jag måste skriva på följande sätt:

main(argc, argv)
int argc;
char **argv;
{
  ... huvudprogrammet ...
}

Samma modifikation som i det senare exemplet måste jag göra för alla funktionsanrop som har parameterlista.

Funktionen getline som finns färdig i stdio.h i moderna varianter av C-språket eller egentligen i moderna varianter av C-biblioteket. Denna färdiga funktion introducerades ungefär 2010 d.v.s. långt efter att man slutade uppdatera min BSD 2.11. Lösningen är naturligtvis att skriva funktionen själv. Jag utgick från boken ”The C Programming Language” av Brian W. Kernighan och Dennis M. Ritchie. Boken är en klassiker! Min nya funktion fick följande utseende i språket sv:

heltal hamtarad(s,l)
tecken *s;
heltal l;
{
  heltal i,c;
  upprepa(i=0;i<=l-1 && (c=getchar())!=EOF && c!='\n';++i)
    s[i]=c;
  om(c=='\n'){
    s[i]=c;
    ++i;
    s[i]='\0';
    ++i;
  }
  s[i]='\0';
  s[i+1]='\0';
  return i;
}

Jag märker att jag använde return i stället för returnera vilket dock är ok eftersom return i såfall faller igenom oöversatt vilket ger korrekt C-språk … Notera att parametrarna är deklarerade på gammalt vis. Notera hur man gör två saker samtidigt i upprepa-slingan. Jag läser in ett tecken ‘c’ och kontrollerar samtidigt att inte filen har tagit slut. Detta kallas bieffekt och är något man försöker undvika i de flesta programmeringsspråk eftersom bieffekterna kan ge upphov till programmeringsfel som kan vara besvärliga att hitta om en bit programkod t.ex. förutom en tydlig jämförelse också ändrar värdet på en variabel.

Programmet kompilerar

Jag plockar bort ljudrutinerna i programmet som kör på PDP11 d.v.s. aplay eftersom det inte finns något ljudstöd.

Då programmet kompilerar första gången vet jag att jag börjar vara nära målet. Provkörning visade att den text jag matade in via getline splittrades upp i ord helt korrekt men vid körning fick jag diverse skräp efter det sista ordet. Problemet berodde på funktionen strtok som delar upp en sträng i en serie segment separerade av en eller flera separatorer i den gamla versionen ville ha två separatorer d.v.s. mellanslag ‘ ‘ och radbyte. I den nya C-versionen behövdes endast mellanslag som separator.

Då kompileringen går igenom märker jag att det inte lönar sig att kompilera om swe.lex varje gång eftersom kompileringen av lex.yy.c till programmet sv tar kanske 20 sekunder i stället för en blinkning under Linux på min normala PC. Lösningen är naturligtvis att kompilera om sv endast då detta behövs.

Då jag kör morseprogrammet blir resultatet:

./smorse
Skriv text att sanda som morse:lasse skriver
*-**
*-
***
***
*
+
***
-*-
*-*
**
***-
*
*-*
+

Tecknet ‘+’ betyder att det är en lång paus på 0.7 sekunder mellan ord.

Morsesekvensen ovan skickar jag i stället till en fil s.morse som jag sedan kan skicka vidare till morsesändaren på en annan maskin d.v.s. min Linux PC. Jag skriver ett enkelt shellskript som kör morseprogrammet och automatiskt skickar resultatet med ftp till min Linuxmaskin.

echo "Enter the text to send:"
./smorse >s.morse
sh send.ftp

Skriptet send.ftp använder ftp som har diskuterats tidigare för att skicka filen s.morse till Linuxmaskinen. Skriptet send.ftp har följande utseende:

ftp -inv $HOST <<EOF
user $USER $PASSWORD
put s.morse
bye
EOF

Jag har definierat systemvariablerna HOST, USER och PASSWORD som jag behöver för att kunna logga in på Linuxmaskinen. Filen morse.txt läggs för närvarande hemkatalogen men jag ändrar detta senare till att peka in till en egen katalog för sändaren. Från linux kan jag sedan med hjälp av ett enkelt litet program skicka resultatet vidare med epost som text eller skicka ljudet vidare tex. med radio. Det var roligt att se att det var enkelt att automatisera ftp-funktionen på PDP11.

Jag skrev en enkel morsetolk/sändare som kontinuerligt väntar på meddelanden att sända på min bordsdator deNeb. Programmet är listat nedan. Jag skrev på skoj programmet i programspråket Python som idag är en väldigt populär ersättare till forntidens Basic. Programmet sender.py visar hur man i Python kan generera ljud men också hur man enkelt kan använda kommandon ur operativsystemet eller något annat program på maskinen. Jag anropar operativsystemet för att kontrollera om det har kommit in något nytt sändningsjobb från PDP11 och putsar i såfall bort det gamla meddelandet. Programmet aplay genererar ljud till högtalarna och programmet ffmpeg skapar en ljudfil innehållande hela ljudsekvensen med bättre tidsprecision än anropen till aplay. Det tar ett ögonblick att starta upp aplay vilket gör timingen inexakt. Det kompletta meddelandet finns i ljudfilerna morse_message.wav samt i den komprimerade filen morse_message.mp3 . Vill man spara utrymme kan kvaliteten sänkas betydligt utan att meddelandet blir oläsligt. De råa ljudfilerna är så stora att de inte ryms i PDP11 normala användarminne (ca. 400 kb då ljudfilerna är 1.7 MByte och den packade mp3 filen är 462 kByte. I en 30 Euros Raspberry Pi läser man utan att blinka in ljudfilerna och behandlar dem i minnet om detta behövs. På 1970-talets PDP11 skulle man ha kört från skiva till skiva och behandlat små minnessegment och successivt skrivit data till skiva eller magnetband.

#!/home/lasi/miniconda3/bin/python
# Modify the python used to match your system (first line). Check with "which python".
# Name=sender.py
# The program "sends" morse code produced by the PDP11
# and automatically transferred to deNeb by ftp.
# This program checks if the file s.morse exists and
# if that is the case sends the morse code, deletes the
# morse message and then starts looking for the next message
# Author: Lars Silen
# This is free code. Feel free to use or modify on your own risk.

import os
import time
from os.path import exists

run = True

def send_morse(line):
	for i in range(0,len(line)):
		if line[i]=='*':
			os.system("aplay  0_1.wav")
			os.system("aplay  sp.wav")
			os.system('echo "file 0_1.wav" >>wav_files.txt\n')
			os.system('echo "file sp.wav" >>wav_files.txt\n')
		elif line[i]=='-':
			os.system("aplay  0_3.wav")
			os.system("aplay  sp.wav")
			os.system('echo "file 0_3.wav" >>wav_files.txt\n')
			os.system('echo "file sp.wav" >>wav_files.txt\n')
		elif line[i]=='\n':
			os.system("aplay  cp.wav")
			os.system('echo "file cp.wav" >>wav_files.txt\n')
		elif line[i]=="+":
			os.system("aplay  wp.wav")
			os.system('echo "file wp.wav" >>wav_files.txt\n')


while run:
	os.system("rm wav_files.txt")
	if exists("s.morse"):
		os.system("rm s.morse.old")
		os.system("rm morse_message.wav")
		os.system("rm morse_message.mp3")
		f=open("s.morse","r") 
		lines = f.readlines()
		# print(lines)
		for i in range(2,len(lines)):
			send_morse(lines[i])
			os.system("mv s.morse s.morse.old")
		# Create a wav file containing the audio of the message
		os.system("ffmpeg -f concat -i wav_files.txt -c copy morse_message.wav")
		os.system("ffmpeg -i morse_message.wav -vn -ar 44100 -ac 2 -b:a 192k morse_message.mp3")
		print("Generated audio message! Listen to: morse_message.wav or morse_message.mp3")
	else:
		print("Sleep 5 seconds")
		time.sleep(5)
		os.system("clear")

En intressant jämförelse mellan det ursprungliga morseprogrammet kompilerat via C till maskinkod är att man hör skillnad mod Pythonprogrammets direkta ljudgenerering. Pythonprogrammet kör hörbart långsammare vilket i sig är naturligt då Python är ett tolkat språk som kör kanske 20 ggr långsammare än kompilerad C-kod.

Antik datorteknik: Att skriva program på svenska

04/02/2022

Detta är en artikel i serien ”Antik datateknik”.

Jag kommer i denna artikel att visa hur jag enkelt kan skapa en svenskspråkig variant av programmeringsspråket C, språket kallar jag sv och kompilatorn blir då csv. Notera att avsikten med denna övning är att illustrera hur enkelt det vore att erbjuda en nationell nybörejarplattform för unga programmerare. För att verifiera programspråket sv skriver jag ett program som läser in en textsträng och sänder resultatet som morsekod. Morsekoden visas också i textform. Följande steg blir att undersöka vilka förändringar jag behöver göra i koden för att programmet skall fungera på min emulerade minidator PDP11/70 som kör en gammal BSD Unix 2.11 från början av 1990-talet. Unix utvecklades i tiden på minidatorn PDP11 som klarade av att hatera flera användare på olika terminaler allt detta på en maskin med 4 MB (4 miljoner byte) minne. En modern hemdator har typiskt 2000 gånger mera minne … eller ännu mer.

Kompilering av ett program skrivet i sv görs på någon sekund. Jag kompilerar morsesändaren smorse.sv dock utan filtyp eftersom mitt kompileringsskript förenklas en aning om jag inte behöver kontrollera filtypen utan kan anta att användaren vet vad hen gör.

./csv smorse
Källkod:smorse.sv
Översättaren är definierad i swe.lex
Kompilerar översätteren swe.lex till c-kod
Kompilerar lex analysatorn till körbar maskinkod: sv
Översätter sv-programmet smorse.sv till c-kod: smorse.c
C-kod finns nu i smorse.c
Kompilerar nu till körbar maskinkod!
The executable is: smorse.run

Jag kan nu köra programmet med smorse.run

Notera att morsesändningen i ljudfilen är en annan än på skärmen ovan. En utmaning för lyssnare är att bena ut vad som sänts!

Texten nedan visar hur ”kompilatorn” för språket sv är konstruerad. Jag skriver därefter programmet smorse i språket sv för att verifiera att kompileringen fungerar. Notera att programmet på min Linux-dator genererar hörbar morsekod. PDP11 har inte ljudstöd varför jag tänker generera ett simulerat magnetband som huvuddatorn kan sända som ljud.

En elev som lär sig att programmera kommer att stöta på flera samtidiga utmaningar. Vid programmering dyker det upp nya begrepp såsom ”variabel”, ”fil”, ”adress” och många andra begrepp som inte är bekanta från vardagslivet. Eleven lär sig också ett nytt språk, mycket enkelt dock, som beskriver logiken i programmet. Det språk som ligger under de flesta programmeringsspråk idag är engelska. Om vi börjar undervisa tex. en 10-12 årig elev programmering så kan engelskan som sådan vara ett visst problem som kan göra det svårare för eleven att komma över den första tröskeln in i programmeringens värld.

En enkel lösning på problemet programmering på ett främmande språk är naturligtvis att vi i inledningsaskedet skulle låta eleverna programmera på svenska/finska/norska/estniska för att därefter då grunderna finns gå över till engelska som idag är en de facto standard.

Skulle programmering på t.ex. svenska kräva våldsamma ekonomiska resurser? Är inte utveckling av ett nytt programmeringsspråk samt kompilator för det ”nya” språket extremt dyrt? Svaret är att en enkel tvåstegskompilator kan skrivas i ungefär hundra rader kod … inte dyrt alltså!

Om någon läsare är intresserad av att experimentera med språket sv eller modifiera det för något annat språk så kan det löna sig att ta kontakt via en kommentar så skickar jag alla filerna som ett paket med epost. Jag gör inte dessa förfrågningar synliga. Programlistningar på vebben tenderar att vara opålitliga vilket kan leda till onödig felsökning om man kopierar koden direkt från skärmen.

Språket ”sv” samt kompilatorn ”csv”

Min emulerade PDP11/70 minidator kör idag på mitt arbetsbord och jag måste således hitta på något vettigt leksaksprojekt. Jag kör BSD Unix v2.11 på PDP11:an vilket betyder att jag har tillgång till i princip samma programmeringsverktyg som på min huvuddator d.v.s. en kompilator för språket ”c” (kompilatorn heter cc) samt verktyget lex (lex har jag använt mycket sällan).

Ett trevligt miniprojekt kunde då vara att skapa ett svenskt programmeringsspråk som strukturellt är identiskt med programmeringsspråket ”c” under Unix/Linux. Tanken är att det skall vara möjligt att koda helt i ”sv” eller koda i en hybridmiljö där det är möjligt att använda ”c” direkt utan att programmets funktion påverkas. Tanken är att översätta ”sv” till normalt ”c” som därefter kompileras till maskinspråk som vilket normalt c-program som helst.

Fördelen med att använda c som mellanliggande språk är naturligtvis at jag inte behöver skriva den egentliga kompilatorn och maskinkodsgeneratorn (jag har för många år sedan skrivit ett komplett programmeringsspråk ”sil=simple language). Problemet är alltså att skriva en översättare från
språket sv till språket c.

Unix ger tillgång till två klassiska verktyg för att skapa kompilatorer
lex (lexical analyzer) och yacc (yacc=yet another compiler compiler). Jag har aldrig tidigare aktivt använt någondera. Programmet ”lex” kan enkelt sköta översättningen från sv till c d.v.s. jag behöver inte yacc eftersom min kompilator redan finns under förutsättning att det program lex producerar förmår att skapa kompilerbar c-kod.

Jag definierar ett antal ”REGLER” i lex som beskriver hur t.ex. ett nyckelord, en kommentar, en textsträng o.s.v. definieras. Då en regel passar in på källtexten skriver mitt genererade analysatorprogram ut motsvarande element för språket c.

Då jag skriver min översättare från sv till c i lex blir programmets längd ungefär hundra rader kod d.v.s. programmet är väldigt litet och överskådligt. Mitt sv-språk är i detta skede ett subset av språket c men redan i nedanstående form kan man skriva riktiga program i sv. Språket kan enkelt utvidgas genom att modifiera filen swe.lex. Då jag använder lex för att kompilera min definition av sv blir resultatet ett c-program som heter lex.yy.c . Jag kompilerar därefter lex.yy.c till ett körbart program sv som sköter översättningen av en källkodsfil i sv till motsvarande c-program.

Lex-program för översättning av sv till c

Filen swe.lex kan enkelt modofieras så att en större del av c-språket stöds. Notera att jag har skrivit swe.lex så att resulterande c-filen som ett sv-program översätts till har exakt samma antal rader som källkoden i sv. Detta betyder att då jag kompilerar mitt sv program så stämmer radnumren för felmeddelanden för både sv och c.

Min definition av sv-språket ser ut på följande sätt:

%{
/* A lexical analyzer for the computer language "sv". This is a simple translation */
/* of the "c" language into swedish. The corresponding "compiler" translates a */
/* sv-source into c-language that can be compiled using an ordinary c-compiler */
/* need this for the call to atof() below */
#include <math.h>
/* need this for printf(), fopen() and stdin below */
#include <stdio.h>
%}
WHITESPACE [ \t\n]+
DIGIT	  [0-9]
ID	  [a-zA-Z][a-zA-Z0-9]*
CHAR	  [a-zåäö][A-ZÅÄÖ][0-9][\ ][!?][\n]
COMMENT	  \/\/.*[\n]
STR1	  \".[\\]*\"
APP	  [\"]
LPAR	  [(]
RPAR	  [)]
LWAV	  [{]
RWAV	  [}]
TERMINATOR [;]
EXCL	  [!]
AE	  [Ä]
ae        [ä]
Aring	  [Å]
aring	  [å]

COMMA	  [,]

%%
{DIGIT}+		printf("%d",atoi(yytext));
{DIGIT}+"."{DIGIT}*	printf("%s", yytext);
{STR1}			printf("%s",yytext);
{LPAR}			printf("%s",yytext);
{RPAR}			printf("%s",yytext);
{LWAV}			printf("%s",yytext);
{RWAV}			printf("%s",yytext);
{EXCL}			printf("!");
{COMMA}			printf(",");
#inkludera		printf("#include");
#definiera		printf("#define");
program			printf("\nmain");
funktion		printf(" ");
resultat		printf("return ");
alternativ		printf("switch");
valt			printf("case ");
bryt			printf("break");
om			printf("if ");
medan			printf("while");
upprepa			printf("for");
annars			printf("else ");
skrivf			printf("printf");
skrivr			printf("printf");
fskrivf			printf("fprintf");
hämtarad		printf("getline");
hämta			printf("get");
sätt			printf("put");
läs			printf("read");
läsrd			printf("readln");
dröj			printf("delay");
heltal			printf("int ");
tecken			printf("char ");
byte			printf("byte ");
sanningsvärde		printf("boolean ");
register		printf("int");
bitmask			printf("int");
likamed			printf("==");
mindre_än		printf("<");
mindre_än_eller_likamed	printf("<=");
större_än		printf(">");
större_än_eller_likamed	printf(">=");
och			printf("&&");
binär_och		printf("&");
and			printf("&");
or			printf("|");
xor			printf("^");		
binär_exclusiv_och	printf("^\n");
vflytta			printf("<<");
hflytta			printf(">>");
storlek_t		printf("size_t");
\<\<			printf("<<");
\>\>			printf(">>");
=			printf("=");
\<			printf("<");
\>			printf(">");
\.			printf(".");
\&\&			printf("&&");
\&			printf("&");		
{ID}			printf("%s", yytext);
{TERMINATOR}		printf("%s",yytext);
{COMMENT}		printf("%s",yytext);
{WHITESPACE}		printf("%s",yytext);

"+"|"-"|"*"|"/"	printf("%s", yytext);
"{"[^}\n]*"}"	        /* eat up one-line comments */
%%
int main(int argc, char *argv[])
{
		++argv, --argc;	/* skip over program name */
		if (argc > 0)
			yyin = fopen(argv[0], "r");
		else
		yyin = stdin;
		yylex();
		return(0);
}

Verifiering av språket ”sv”

Ett enkelt sätt att visa att det nya språket ”fungerar” är naturligtvis att skriva ett riktigt program i
programspråket sv. Jag byggde för kanske tjugo år sedan några led-ficklampor med några barnhemsbarn där vi använde en liten mikroprocessor som programmerades i mitt språk ”sil” (simple language) för Microchips processor 16F84. Ficklamporna programmerades så att de olika barnens ficklampor kunde blinka ägarens namn i morsekod.

Nedan visar jag hur man kan skriva ett program i språket sv som läser in en textrad från användaren och
”sänder” texten som morsekod till skärmen men också som morseljud via datorns högtalare. Jag kommer att flytta programmet till PDP11 och något modifiera det så att PDP11 som saknar högtalare i stället skickar ett magnetband till huvuddatorn för sändning, detta beskrivs eventuellt i en senare artikel.

Morsesändare skriven i sv

En morseöversättare gör man enklast så att man tabellerar tecknen ”A-Z”, ”a-z” samt siffrorna 0-9 samt deras motsvarande teckenkoder. För en specifik bokstav vill vi alltså ha en översättarfunktion skrivMorse() av ungefär följande typ (ti=’*’ och taa=’-‘):

heltal skrivMorse(tecken c){
heltal i;
tecken morse[16];
alternativ (c){
valt ‘a’: strcpy(morse, ”-”); bryt;

valt ‘b’: strcpy(morse,”-”); bryt;
valt ‘c’: strcpy(morse,”–”); bryt;
valt ‘d’: strcpy(morse,”-”); bryt;
valt ‘e’: strcpy(morse,”*”); bryt;
…
}

skrivf(”Morsekod:%s\n”,morse);
…

Som indata till vår funktion ”skrivMorse” ges ett tecken/en bokstav ”c” vid anrop till funktionen. Resultatet av översättningen finns efter exekvering nu i variabeln ”morse”. Jag antar att funktionen är ganska läslig även för personer som inte kan programmera. Nyckelordet ”bryt” betyder att rätt alternativ hittades och exekveringen fortsätter vid ”skrivf(…). Om vi vill översätta bokstaven ”s” till morse så anropar vi vår funktion med:

skrivMorse(‘s’);

Resultatet skulle bli Morsekod:***

Vi börjar vårt egentliga program med att låta programmet be om en text att sända samt läsa in en textrad som innehåller texten. Funktionen skrivf() skriver ut text som kan formatteras för heltal, flyttal etc. Vi kan göra detta med:

skrivf(”Skriv text att sända som morse:”);
l=hämtaRad(&line,&len,stdin);

Kommandot ”hämtaRad” använder biblioteksfunktionen ”getline” som finns definierad i biblioteket stdio.h. Vi måste då komma ihåg att deklarera att vi använder biblioteket stdio.h . Vi inkluderar ett standardbibliotek med:

#inkludera <stdio.h>

Vårt program har nu ungefär följande utseende och det utför inte ännu något vettigt :

#inkludera <stdio.h>

#inkludera <stdlib.h>

#inkludera <string.h>

heltal skrivMorse(tecken c){
heltal i;
char morse[16];
alternativ (c){
valt ‘a’: strcpy(morse, ”-”); bryt;
valt ‘b’: strcpy(morse,”-**”); bryt;
… fler definitioner av morsekoder …
}
}

heltal program(){
heltal l=0;
storlek_t len=0;
tecken *rad=NULL;
skrivf(”Skriv text att sända som morse:”);
l=hämtarad(&rad,&len,stdin);
// Skriv ut den lästa raden för att verifiera att inläsningen lyckades
skrivf(”Inläst rad:%s\n”,rad);
}

Variablerna l, len och *rad behövs för anropet till hämtarad().

Då vi skriver in vår text som skall skickas så vill vi gärna hantera texten ordvis d.v.s. vi skickar ett ord i taget och genererar en standardiserad paus mellan orden. För att splittra up vår textsträng i separata ord inkluderar vi biblioteket string (#inkludera <string.h> se ovan). String-biblioteket har en användbar funktion strtok() som splittrar upp den text vi ger som funktionsparameter i en tabell med separata textsträngar (ordsträngar) separerade med mellanslag ‘ ‘. Råtexten bryts alltså vid mellanslag. Jag splittrar upp den ingående råtexten genast då jag deklarerar ordtabellen som jag kallar ett_ord:

tecken *ett_ord = strtok(rad,” ”);

Om jag har matat in texten ”Lasse skickar morse” så kommer ett_ord efter anrop att innehålla textsträngar som jag kommer åt med:

ett_ord[0] = ”Lasse”
ett_ord[1] = ”skickar”
ett_ord[2] = ”morse”

Vi kan nu skriva en ny funktion som vi kallar skicka_text_som_ord(”någon text …”) .

heltal skicka_text_som_ord(tecken rad[]){
heltal i;
tecken *ett_ord = strtok(rad,” ”);
medan (ett_ord != NULL){
skrivf(”Ord=%s\n”,ett_ord);
upprepa(i=0; i<strlen(ett_ord);i++){
// Skriv ett tecken i nuvarande ord
skrivMorse(ett_ord[i]);
system(teckenpaus);
skrivf(”\n”);
}
skrivf(”\n”);
ett_ord = strtok(NULL, ” ”);
}
}

Efter att vi splittrade upp råtexten i ord så tar vi ett ord i taget och splittrar upp det i bokstäver som skickas för konvertering till morse.

Vi går igenom alla orden i vår råtext med:

medan (ett_ord != NULL){
skrivf(”Ord=%s\n”,ett_ord);
…
}

Motsvarande konstruktion i språket c är ”while(ett_ord != NULL){ … }” .
Vi skickar vidare orden för sändning så länge som ett_ord inte är tomt (NULL).

Vi lägger nu till en slinga för att skicka iväg varje ord bokstav för bokstav för översättning till morse och sändning. Slingan har följande utseende:

upprepa(i=0; i<strlen(ett_ord);i++){
  // Skriv ett tecken i nuvarande ord
  skrivMorse(ett_ord[i]);
  system(teckenpaus);
  skrivf("\n");
}

Vi använder konstruktionen ”upprepa” som motsvarar c-språkets ”for” slinga.
Slingan går igenom ordsträngen ett_ord bokstav för bokstav tills vi har nått den fulla längden på strängen ett_ord.

Slingan stegar alltså igenom ett_ord på följande sätt:

i=0 ett_ord[i] = ‘L’ som skickas till skrivMorse(‘L’)
i=1 ett_ord[i] = ‘a’ som skickas till skrivMorse(‘a’)
i=2 ett_ord[i] = ‘s’ som skickas till skrivMorse(‘s’)
i=3 ett_ord[i] = ‘s’ som skickas till skrivMorse(‘s’)
i=4 ett_ord[i] = ‘e’ som skickas till skrivMorse(‘e’)

Efter att vi har skickat ett helt ord så håller vi paus genom att anropa operativsystemets funktion sleep.

Vi låter systemet sova i 0.7 sekunder mellan ord. Sovtiden mellan bokstäver är 0.1 sekunder.

Vi spelar upp ”tit” och ”taa” under Linux så att jag med hjälp av programmet ”Audacity” genererade en ton med frekvensen 880 Hz. Från denna ton klippte jag två stumpar 0.1 repektive 0.3 sekunder långa som jag sparade som ljudfilerna 0_1.wav samt 0_3.wav . Jag kan spela upp en wavfil under Linux med hjälp av programmet ”aplay”.

Programmet i dess helhet (under linux i detta skede) har då följande utseende:

// Morse
// Programspråket "sv" är språket "c" med svenska kommando-ord.
// Språket översätts till standard "c" som sedan kompileras till maskinspråk
// för att köras.
// Språket "sv" är egentligen ett experiment med Unixverktyget "lex" som 
// har konstruerats för att känna igen ord och strukturer i en text.
// Strukturer som hittas skulle normalt skickas vidare till programmet "yacc"
// (yet another compiler compiler = en annan kompilator kompilator).
// Eftersom jag översätter språket "sv" till "c" så behöver jag inte
// någon kompilator eftersom denna redan finns och likaså behöver jag inte
// någon kodgenerator som skulle generera maskinspråk eftersom även den redan
// finns. Notera att jag kan använda c-språk direkt om motsvarande
// sv-konstruktion inte har definierats.
// Notera att endast en delmängd av SV-C har skrivits. 
// Vill man ha en mera fullständig 
// motsvarighet så måste filen swe.lex utvidgas med nya nyckelord.
// Jag kör för närvarande en emulerad minidator PDP11/70 från 1970-talet.
// Operativsystemet är BSD Unix 2.11.
// Detta är ett experiment i att skriva ett enkelt svenskt 
// programmeringsspråk som är körbart på denna urgamla dator.   
// 
// Lars Silen 2022
// Detta är öppen källkod som fritt får distribueras
// Författaren tar inget ansvar för eventuella fel i genererad kod 

#inkludera <stdio.h>
#inkludera <stdlib.h>
#inkludera <string.h>

// Definiera tidslängden på olika element i Morse 
tecken teckenpaus[] = "sleep 0.1";
tecken ordpaus[] = "sleep 0.7";

heltal skrivMorse(tecken c){
  heltal i;
  char morse[16];
  alternativ (c){
    valt 'a': strcpy(morse, "*-"); bryt;
    valt 'b': strcpy(morse,"-***"); bryt;
    valt 'c': strcpy(morse,"-*-*"); bryt;
    valt 'd': strcpy(morse,"-**"); bryt;
    valt 'e': strcpy(morse,"*"); bryt;
    valt 'f': strcpy(morse,"**-*"); bryt;
    valt 'g': strcpy(morse,"--*"); bryt;
    valt 'h': strcpy(morse,"****"); bryt;
    valt 'i': strcpy(morse,"**"); bryt;
    valt 'j': strcpy(morse,"*---"); bryt;
    valt 'k': strcpy(morse,"-*-"); bryt;
    valt 'l': strcpy(morse,"*-**"); bryt;
    valt 'm': strcpy(morse,"--"); bryt;
    valt 'n': strcpy(morse,"-*"); bryt;
    valt 'o': strcpy(morse,"---"); bryt;
    valt 'p': strcpy(morse,"*--*"); bryt;
    valt 'q': strcpy(morse,"--*-"); bryt;
    valt 'r': strcpy(morse,"*-*"); bryt;
    valt 's': strcpy(morse,"***"); bryt;
    valt 't': strcpy(morse,"-"); bryt;
    valt 'u': strcpy(morse,"**-"); bryt;
    valt 'v': strcpy(morse,"***-"); bryt;
    valt 'w': strcpy(morse,"*--"); bryt;
    valt 'x': strcpy(morse,"-**-"); bryt;
    valt 'y': strcpy(morse,"-*--"); bryt;
    valt 'z': strcpy(morse,"--**"); bryt;
    // Lägg till ÅÄÖ här om du behöver dem

    valt 'A': strcpy(morse, "*-"); bryt;
    valt 'B': strcpy(morse,"-***"); bryt;
    valt 'C': strcpy(morse,"-*-*"); bryt;
    valt 'D': strcpy(morse,"-**"); bryt;
    valt 'E': strcpy(morse,"*"); bryt;
    valt 'F': strcpy(morse,"**-*"); bryt;
    valt 'G': strcpy(morse,"--*"); bryt;
    valt 'H': strcpy(morse,"****"); bryt;
    valt 'I': strcpy(morse,"**"); bryt;
    valt 'J': strcpy(morse,"*---"); bryt;
    valt 'K': strcpy(morse,"-*-"); bryt;
    valt 'L': strcpy(morse,"*-**"); bryt;
    valt 'M': strcpy(morse,"--"); bryt;
    valt 'N': strcpy(morse,"-*"); bryt;
    valt 'O': strcpy(morse,"---"); bryt;
    valt 'P': strcpy(morse,"*--*"); bryt;
    valt 'Q': strcpy(morse,"--*-"); bryt;
    valt 'R': strcpy(morse,"*-*"); bryt;
    valt 'S': strcpy(morse,"***"); bryt;
    valt 'T': strcpy(morse,"-"); bryt;
    valt 'U': strcpy(morse,"**-"); bryt;
    valt 'V': strcpy(morse,"***-"); bryt;
    valt 'W': strcpy(morse,"*--"); bryt;
    valt 'X': strcpy(morse,"-**-"); bryt;
    valt 'Y': strcpy(morse,"-*--"); bryt;
    valt 'Z': strcpy(morse,"--**"); bryt;
    // Lägg till åäö här om du behöver dem

    valt '1': strcpy(morse,"*----"); bryt;
    valt '2': strcpy(morse,"**---"); bryt;
    valt '3': strcpy(morse,"***--"); bryt;
    valt '4': strcpy(morse,"****-"); bryt;
    valt '5': strcpy(morse,"*****"); bryt;
    valt '6': strcpy(morse,"-****"); bryt;
    valt '7': strcpy(morse,"--***"); bryt;
    valt '8': strcpy(morse,"---**"); bryt;
    valt '9': strcpy(morse,"----*"); bryt;
    valt '0': strcpy(morse,"-----"); bryt;
    // Lägg till skiljetecken etc här
  }

  // Skriv bokstaven som sänds (finns som variabelparametern "c" vid anropet)
  skrivf("%c  ",c);
  skrivf("%s  ",morse);
  // Generera ljud 
  upprepa(i=0; i<strlen(morse);i++){
    om (morse[i] == '*'){
       skrivf("ti ");
       system("aplay -q 0_1.wav >/dev/null");
    } annars {
       skrivf("taa ");
       system("aplay -q 0_3.wav >/dev/null");
    } 
    // skrivf("\n");
  } 
}

heltal skicka_text_som_ord(tecken rad[]){
  heltal i;
  tecken *ett_ord = strtok(rad," ");
  medan (ett_ord != NULL){
    skrivf("Ord=%s\n",ett_ord);
    upprepa(i=0; i<strlen(ett_ord);i++){
      // Skriv ett tecken i nuvarande ord
      skrivMorse(ett_ord[i]);
      system(teckenpaus);
      skrivf("\n");
    }
    skrivf("\n");
    ett_ord = strtok(NULL, " ");
  }  
}

heltal program(){
  heltal l=0;
  storlek_t len=0;
  tecken *rad=NULL;
  skrivf("Skriv text att sända som morse:");
  l=hämtarad(&rad,&len,stdin);
  skicka_text_som_ord(rad);
}

Den genererade c-koden är strukturellt identisk med sv-programmets kod d.v.s. vi gör en ord för ord översättning. Detta betyder att c-kompilatorn ger felmeddelanden som pekar till rätt rad också i sv källkoden. Min editor bör naturligtvis konfigureras så att den visar radnummer för att felsökning skall vara effektiv.

Översättaren bör sannolikt expanderas med ytterligare c-konstruktioner. Det är oklart i hur hög utsträckning det är värt att översätta funktioner i bibliotek men exemplet ”hämtaRad” visar att detta naturligtvis är möjligt. Det är naturligtvis också möjligt att översätta namnen på standardbiblioteken på samma sätt men knappast vettigt eftersom målet är att eleven också bekantar sig med c-språket och dess bibliotek.

Oversättarprogram komplett:

#!/bin/bash
# Name=csv
# Detta är en kompilator för programspråket "sv" som är en svensk översätytning av språket "c".
# Språket "sv" kan enkelt utvidgas genom att modifiera filen "swe.lex".
# Användning: ./csv program           
#             Notera att källkoden antas vara program.sv .
#             Resultatet blir det körbara programmet program.run
#
# En textfil som är en översättning till språket "c" genereras som program.c .
# Lars Silen 2022
# Detta är fri källkod som fritt får användas och modifieras på egen risk.

echo "Källkod:"$1.sv
echo "Översättaren är definierad i swe.lex"
echo "Kompilerar översätteren swe.lex till c-kod"
lex swe.lex
echo "Kompilerar lex analysatorn till körbar maskinkod: sv"
gcc lex.yy.c -ll -o sv
echo "Översätter sv-programmet " $1.sv " till c-kod: " $1.c
./sv $1.sv >$1.c
echo "C-kod finns nu i " $1.c
echo "Kompilerar nu till körbar maskinkod!"
gcc $1.c -o $1.run
echo "The executable is:" $1.run

Notera att skriptet kompilerar om swe.lex varje gång. Användaren kan alltså enkelt lägga till
nya definitioner som blir en del av språket. Om användaren uppfattar att språkdefinitionen är stabil så kan man naturligtvis lämna bort raderna lex swe.lex samt gcc lex.yy.c -ll -o sv , tidsvinsten blir dock marginell.

Antik datorteknik, flytta filer över nätet

11/01/2022

I föregående inlägg visade jag hur jag kan använda en simulerad magnetbandstation för att flytta filer mellan min PDP11/70 och den Raspberry Pi som kör emuleringen. Systemet är något omständligt men det fungerar i princip till belåtenhet. Systemet kunde t.ex. användas till att flytta över källkoden till en lite trevligare editor till PDP11 och därefter kompilera den. Resultatet skulle vara en lite bekvämare arbetsmiljö.

Jag kan utan problem köra min PDP11 också från min ”huvuddator” d.v.s. bordsdatorn. Jag ansluter via SSH d.v.s. en krypterad dataförbindelse till Raspberry Pi och använder samma fönster till att logga in mig på PDP11. Detta betyder att jag kan jobba vid min stora bildskärm med ett ensamt terminalfönster in till PDP11. Om jag vill flytta data direkt från huvuddatorn till PDP11 utan att behöva logga in på Raspberry Pi för att fixa magnetbandet, hur gör jag då?

Det faktum att jag kan koppla upp mig till PDP11 via en SSH förbindelse till Raspberry Pi betyder självklart att jag har en fungerande nätverksförbindelse. Jag kan också kontrollera om min PDP11 syns på nätverket med hjälp av verktyget ”ping”:

ping 192.168.0.xxx

där 192.168.0.xxx är PDP11:ns IP-adress. PDP11 har en egen IP-adress trots att den kör ”inom” Raspberry Pi d.v.s. all dess IP trafik routas via Raspberry Pi. Om PDP11 svarar på en ping så betyder detta att nätverksförbindelsen i princip fungerar. PDP11 känner till kommandot ftp d.v.s. det borde vara möjligt att flytta filer direkt mellan huvuddatorn och PDP11 utan att Raspberry Pi behöver delta i operationen. En intressant detalj är att jag inte kan pinga PDP11 från Raspberry Pi d.v.s. det kort på vilket både Raspberry Pi och PDP11 kör. Däremot kan jag pinga PDP11 från min bordsdator som sitter på samma nätverk som Raspberry Pi. Problemet går att åtgärda men så långt har jag inte ännu hunnit.

En ftp server på min huvuddator

Jag började med att installera en ftp server på min huvuddator. Det finns ett antal varianter för Linux och jag valde att installera en server som heter VSFTPD. Jag kör Linux Mint som stöder denna ftp-server. Installationen gick behändigt med:

sudo apt update

Detta kommando uppdaterar Linux Mint katalog över tillgängliga applikationer.

sudo apt install -y vsftpd

Installation av ftp-servern från kommandoraden. Jag kunde likaväl ha använt ett grafiskt verktyg.

Konfiguration av servern görs i filen /etc/vsftpd.conf

sudo nano /etc/vsftpd.conf

Det lönar sig att googla på vsftpd om man behöver information om konfigurationen. Jag måste installera och konfigurera som superuser ”root” eftersom jag installerar vsftpd för alla användare på maskinen och konfigurationsfilerna i /etc är skrivskyddade eftersom jag inte vill att en eventuell besökare skall kunna gå in och konfigurera min maskin. För att jag skall kunna flytta filer från måste jag åtminstone editera /etc/vsftpd.conf så att jag tillåter skrivning till min katalog på deNeb. Detta kan jag göra genom att sätta write_enable=YES . Denna modifikation behövs om jag vill flytta filer från PDP11 till deNeb.

Flytta filer till/från PDP11

Min PDP11 representerar tidsperioden 1970 till slutet av 1990-talet. I praktiken betyder detta att de programverktyg jag har tillgång till kör i en textterminal inte i ett modernt grafiskt fönster som på min huvuddator. Det visar sig dock att man mycket bekvämt kan flytta filer mellan datorer direkt från kommandoraden på PDP11 utan att detta egentligen är komplicerat.

PTF-inloggning till min huvuddator från PDP11

Jag kopplar upp till min huvuddator ”deNeb” från PDP11. Situationen är intressant genom att det terminalfönster i vilket PDP11 kör finns på deNeb men är kopplad över en ssh-förbindelse. PDP11 kör alltså på Raspberry Pi men terminalfönstret finns på deNeb vilket man kan se i fönstrets rubrik.

Jag jobbar alltså i PDP11 och lägger upp en ftp-förbindelse till deNeb. Steg för steg gör jag:

ftp deNeb

Jag ger mitt användarnamn och password till deNeb för att släppas in i maskinen. Inloggningen är avklarad då jag får svaret ”230 Login successful”.

ftp>

Datorn kommer visa ftp> på varje rad för att indikera att jag har en aktiv ftp-förbindelse. I kommandona nedan är denna indikator bortlämnad. tex. följande kommando nedan kommer att i terminalen vara: ftp>cd Prog/PDP11 men användaren skriver endast cd Prog/PDP11 .

Jag ger nu ett kommando till ftp-servern att jag vill flytta mig till katalogen Prog/PDP11 på datorn deNeb från vilken jag vill hämta en textfil.

cd Prog/PDP11

Svaret blir ”250 Directory successfully changed”. Jag listar nu innehållet (ls) i katalogen dit jag flyttade mig på deNeb.

ls

Katalogen innehåller endast en fil med namnet deNeb.txt. Jag kan nu flytta filen från deNeb till PDP11 där jag kör mitt ftp-program med kommandor ”get”.

get deNeb.txt

… 226 Transfer complete.

Jag har nu flyttat filen till PDP11 och jag kan undersöka vad jag fick. Först går jag ut ur PDP11 med kommandot ctrl-D.

cat deNeb.txt
Resultatet blir:

This text document is moved from the Linux computer ”deNeb” to the emulated PDP11.

Filens innehåll är korrekt. Vi kan ladda in filen i texteditorn ”vi” och göra en liten förändring. Vi ändrar texten till: This text document is moved from PDP11 to ”deNeb”. Jag byter namn på filen till PDP11.txt så att jag kan verifiera att jag faktiskt har flyttat filen tillbaka till PDP11.

Vi kan nu flytta den modifierade filen tillbaka till deNeb genom att på nytt starta ftp på PDP11. Vi flyttar oss till katalogen Prog/PDP11 och använder ”put” för att flytta filen från PDP11 till deNeb.

Jag kan kontrollera vad jag har i min lokala katalog på PDP11 inifrån ftp-programmet genom att ge normala UNIX-kommandon som inleds med ”!”.

!ls
hello hello.c

På PDP11 har jag endast två filer i min arbetskatalog d.v.s. ett ”Hello world!” c-program och samma program i kompilerad körbar form.

Jag flyttar filen PDP11.txt till deNeb med ftpkommandot put PDP11.txt . Jag kan nu i ett annat terminalfönster kontrollera att filen faktiskt levererades korrekt.

Efter kommandot på PDP11 ser vi att vi har fått en ny fil på deNeb d.v.s. överflyttning i båda riktningarna lyckas utan problem.

Det är igen intressant att se hur jag utan större problem kan kommunicera mellan min huvuddator deNeb och en 30 år gammal UNIX. Det finns klara fördelar av att operativsystemen är nära besläktade. Jag tror inte att det hade varit riktigt lika enkelt att idag försöka sätta upp en förbindelse mellan PDP11 och t.ex. Windows 3.1 från 1992 …

Att köra en PDP11 dator från 1970-talet

05/01/2022

Jag har under en tid bekantat mig med en emulerad/simulerad PDP11 minidator från mitten av 1970-talet. PDP11 datorn var mycket populär innan IBM PC:n slog igenom och i praktiken sopade bort konkurrenterna mot slutet av 1980-talet då minidatorn PDP11 redan hade kring tjugo år på nacken. Konstruktionen kom att kopieras på olika håll bl.a. tillverkade man kopior i Sovjetunionen t.ex. Electronica 60 som jag tittade på i slutet av 1980-talet som alternativ för kontroll av mätsystem för export till Sovjetunionen. Jag jobbade aldrig med en rysk dator eftersom det visade sig att vi kunde kringgå exportrestriktionerna genom att byta ut en HP 9836 dator baserad på Motorola 68000 processor, som omfattades av amerikanska restriktioner, mot en standad 8 MHz långsam första generationens PC 8088. Jag kommer inte ihåg vilken tillverkares PC vi använde men detta är inte viktigt eftersom de olika PC:na var lika som bär. Lösningen var säkerligen bra eftersom Electronica 60 torde ha haft en hel del problem med pålitligheten. Det skulle dock ha varit roligt att ha fått konkret erfarenhet av dåtidens PDP11.

Den PDP11 jag skriver om är en emulerad PDP11/70 från ungefär 1975 som hörde till den tidsperiodens tungviktare använd som laboratoriepersondator. Datorn kunde köra UNIX och den orkade med flera användare eftersom en PDP11/70 med max minne på 4 MByte kunde stöda flera samtidiga användare. Användarna har under 2.11BSD Unix tillgång till något mer än 300 kByte minne vilket på den tiden var mycket. En PC som lanserades ett antal år senare kunde ha 64 kB – 256 kB (-640kB) för användare och operativsystem. Eftersom det normalt inte fanns något grafiskt användargränssnitt så var minnesbehovet litet och det gick att skriva stora program i 300 kByte. Dagens persondatorers behov av ett stort minne 2000-4000 ggr större än PDP11:ns är en följd av den mycket sofistikerade grafiken som kräver väldigt mycket minne utan att egentligen ge annat mervärde än bling bling. Samma kommentar gäller själva processorn som klockades på några megaherz. Moderna datorer kör med en klocka som tickar igen kanske 1000 ggr snabbare. Det intressanta är dock att då man håller sig till en text-terminal och använder kommandoraden så känns maskinen väldigt OK.

Hur emuleras en antik dator

Redan på slutet av 1960-talet d.v.s. för mer än 50 år sedan skrevs den första emulatorn MIMIC som bl.a. användes för testning och utveckling av nya datorkonstruktioner. 1993 startades projektet simH för att bevara minnet av ålderdomlig hårdvara och program. Den första generationens hårdvara höll snabbt på att försvinna och mängder av data som lagrats på magnetband riskerade att förstöras tex. 1960-talets data från månprojektet. Programmet simH emulerar en mängd gamla datorer bl.a.:

  • Data General Nova, Eclipse
  • Digital Equipment Corporation PDP-1, PDP-4, PDP-7, PDP-8, PDP-9, PDP-10, PDP-11, PDP-15 (and UC15), VAX11/780, VAX3900
  • GRI Corporation GRI-909, GRI-99
  • IBM 1401, 1620, 7090/7094, System 3
  • Interdata (Perkin-Elmer) 16b and 32b systems
  • Hewlett-Packard 2114, 2115, 2116, 2100, 21MX, 1000, 3000
  • Honeywell H316/H516
  • MITS Altair 8800, 8080 only
  • Royal-Mcbee LGP-30, LGP-21
  • Scientific Data Systems SDS 940
  • Xerox Data Systems Sigma 32b systems

Listan är inte fullständig.

Det är intressant att konkret se hur extremt mycket mera processorkapacitet vi har tillgång till idag, nästan gratis, än för femtio år sedan. En PDP11/70 kostade med fullt utbyggt minne mer än en högklassig ny personbil medan min på en Raspberry Pi emulerad PDP11/70 med maximalt minne en mängd operativsystem etc. kostar kanske 50E (500 SEK). Min emulerade maskin kör dessutom ungefär dubbelt snabbare än orginalet. Min Raspberry Pi har en kapacitet där RAM-minnet är 1000 ggr större än orginalet. Massminnet (skivminnet) är kanske 10000 ggr större och processorns hastighet är kanske 1200 ggr högre än orginalets. Ordlängden i PDP11 var 16 bitar medan en Raspberry Pi kör med ordlängden 64 bitar d.v.s. 4 ggr längre. Totalt kan man eventuellt säga att dagens lilla Raspberry Pi, beroende på vilken applikation man betraktar har en kapacitet som är mellan 1000 och 1000000 ggr högre än 1970-talets PDP11. Sannolikt ligger sanningen närmare 1000000 ggr än 1000 ggr …

Hello world

Min PiDP11/70 minidator i form av en modern mikrodator av typen Raspberry Pi model 3B. Notera att jag inte kör på den snabbaste varianten av Raspberry Pi. Valet av maskin att emulera PDP11 på gjordes utgående från vad jag råkade ha i miljonlådan … Raspberry Pi ligger på en fusklapp med kommandon man behöver för att använda den klassiska Unixeditorn ”vi” beskriven nedan.

Ett första test av en okänd dator och ifrågavarande dators programvara är ofta att skriva ett mycket kort program, kompilera det och sedan köra det. Sekvensen testar på ett allmänt plan att själva maskinen är någorlunda vettigt konfigurerad, det finns en fungerande text editor, kompilator (eller t.ex. basic tolk) samt förbindelse till bildskärm och tangentbord fungerar.

Jag har kört BSD Unix version 2.11 på min PiDP11. Alla någorlunda färska Unixvarianter är skrivna i programspråket ”c” och motsvarande kompilator heter cc (c compiler). I ett system som kör Unix brukar det alltid finnas en text editor som heter ”vi”. Editorn ”vi” är urgammal men förvånande kraftfull om användaren känner till den. Utmärkande för vi är att den är modal d.v.s. den har en kommandomod och en insättningsmod. I kommandomoden kan man med kursortangenterna och via andra tangentbordskommandon flytta sig i texten, ta bort text, söka text, byta ut text etc. Då man vill skriva text går man in i insättningsmod med kommandot ”i”. Då man vill tillbaka till kommandomoden så trycker man på ”ESC” tangenten.

Man sparar texten med kommandot ESC :w .

Man kommer ut ur programmet med kommandot ESC :q .

Avslutning så att texten sparas kan göras med kombinationen ESC :wq .

Att tvinga stängning av programmet kan man göra med ESC :q! .

Många Unixanvändare uppfattar att det enda ”vi” kommando man behöver känna till är ESC :q eftersom ingen människa med förståndet i behåll vill göra något annat än att snabbt ta sig ur editorn.

Terminalfänster från min bordsdator deNeb över nätet ner till PDP11/70. Programmet hello.c i listningen är skrivet med vi-editorn beskriven ovan.

Vi startar vi-editorn med kommandot ”vi hello.c” och kan då studera själva programmet som innehåller två utskriftskommandon som skriver ut textraderna

Hello world!
Another line!

Vi kompilerar programmet till körbar form d.v.s. maskinspråk med kommandot:

cc hello.c -o hello

Programmet körs med kommandot:

./hello

Och resultatet blir:

Kompilering av programmet hello.c . Vi ger tilläggsinformationen -o hello åt kompilatorn för att lagra resultatet i den körbara filen ”hello”. Standardnamnet på resultatet av compileringen skulle ha varit a.out vilket blir onödigt kryptiskt. Det körbara programmet körs därefter med kommandot ./hello . Orsaken till att jag skriver ./ före hello är att jag säger åt systemet att jag vill köra det program ”hello” som finns i den katalog i vilken jag befinner mig för tillfället. En Unix maskin brukar konfigureras så att man inte av misstag kör program i den katalog där man jobbar.

Jag nämnde ovan att många moderna Unixanvändare anser att det enda vi-commando man behöver känna till är ESC :q så att man med äran i behåll kan ta sig ut ur editorn. Problemet blir då hur jag skall skriva program om jag inte vill använda vi. Alternativet i 211BSD Unix på min PDP11/70 är editorn ”jove” som jag personligen har samma relation till som ”vi” d.v.s. jag vill ut ur den så snabbt som möjligt. Lösningen är naturligtvis att sätta upp kommunikation mellan min normala bordsdator eller laptop rill min PDP11 och då använda en modern editor i den moderna maskinen för att sedan flytta över program för kompilering på PDP11. Olika processer för att flytta över data mellan maskinerna presenteras i nästa avsnitt.


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