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

Att mäta plattors rörelser på fiol

17/09/2019

Det här är en inledning till något som torde bli en serie artiklar.

Jag har i olika sammanhang funderat över hur man enkelt kunde mäta amplituden hos vibrationer i plattorna på en fiol så att fiolen hela tiden är spelbar. Jag är alltså inte i första hand intresserad av att mäta vibrationerna i ett fritt svängande lock eller bottenplatta som inte har limmats till sargerna. Naturligtvis kan samma mätteknik användas också för detta men jag är mera intresserad av ett spelbart instrument.

Jag har tidigare tittat på möjligheterna att använda en lysdiod och en ljusdetektor (en ljuskänslig diod i revers bias). Tanken var att mäta variationerna i ljusstyrka då plattan till följd av vibrationerna rör sig i förhållande till detektorn. Jag fick aldrig mätaren att fungera pålitligt varför jag gick vidare till andra problem …

För en tid sedan råkade jag hitta en intressant video på youtube som behandlar precis mitt problem och där mätningen sker på nästan identiskt sätt jämfört med mitt tänkta system ovan. Skillnaden är att man i stället för en LED (lysdiod) använder en kombinerad laserdiod och ljusdetektor i samma förpackning. En laser producerar koherent ljus d.v.s. alla ljusvågor från lasern är i fas vilket gör att ljuset som reflekteras från ytan också kommer att vara (ungefär) i fas. Ungefär i fas eftersom ytan vi belyser är ojämn i mikrometerskala. Eftersom det reflekterade ljuset är någorlunda fasrelaterat till laserljuset så kommer vi att se interferens mellan laserljuset och det reflekterade ljuset. Då laserljuset ligger i samma fas som det reflekterade ljuset får vi ett signalmaximum. Då laserljuset ligger i motfas får vi ett minimum där ljuset delvis släcks ut.

Titta på videon så att du förstår vad som händer … det är fråga om en relativt enkel process.

Vilka delkomponenter behöver jag för en mätare av plattors rörelser

Jag behöver:

  • En laserdetektor i stort sett byggd som på videon. I praktiken använder jag 3-d skrivare till att bygga en hållare för lasern och hjälpelektroniken. Eventuellt använder jag någon tunn tejp som laserreflektor. En liten tunn tejpbit bör inte ändra svängningsförhållandena märkbart.
  • En separat mikrofon för att mäta ljudnivån så att olika instrument kan jämföras. Eventuellt kan lasersignalens lågfrekventa signal användas också som mikrofon men experiment får utvisa om detta fungerar.
  • En apparat för att generera en specifik ton i instrumentet. Jag har som första approximation tänk mig en enkel apparat för att knäppa på strängen med reproducerbar amplitud. Jag tänker mig ett mekaniskt finger som är fjäderbelastat  så att strängen släpps att vibrera vid en känd kraft (Usom bestäms av fjäderns töjning). Ögonblicket då fingret släpper från strängen detekteras med en mikrobrytare som då startar datainsamlingen.

Hur görs mätningen

Då plattan svänger ger laserns ljusdetektor ut en växelspänning där antalet toppar mellan svängningspunkterna ger antalet våglängder som plattan rör sig. Eftersom jag knäpper på strängen så kommer amplituden att först vara stor d.v.s. vi får ett stort antal våglängdstoppar/dalar under plattans rörelse. Rätt snabbt kommer svängningsamplituden att avklinga mot noll.

Om vi vet, mätt med mikrofon, vilken strängens grundfrekvens d.v.s. tiden för en svängningsperiod på strängen är så kan vi beräkna plattans svängningsamplitud på följande sätt.

Antag att plattans svängningsamplitud är 0,1 mm d.v.s. rörelsen 0,2 mm (200 um) och svängningsfrekvensen är 200 Hz (lös G-sträng på en fiol). En svängningsperiod är då 5 ms. Laserns våglängd är 0,65 um  (mikrometer, rött ljus) vilket betyder att vi bör få 200/0,65 = 307 signaltoppar/dalar från lasern. Då svängningen i plattan klingar av minskar amplituden och antalet toppar/dalar från lasern minskar successivt mot noll. Om vi lyssnar på lasersignalen så kommer vi att höra ett frekvenssvep från maximifrekvensen

fmax = 307/0.005 = 61,5 kHz

Om vi tar den mätta lasersignalen och beräknar Fourier-spektret på signalen så bör vi utan problem kunna se vilken maximifrekvensen är. Maximifrekvensen ger ett direkt mått på svängningsamplituden om vi känner grundtonen:

s = f_max*lamda*T

där

f_max = maximifrekvensen i lasersignalens fourier spektrum.

lamda = laserns våglängd 0,65 um

T = 1/f_grundton

Exempel (numeriska värden tagna ovan):

s = 61500 Hz * 0,65 um * 0,005 s = 200 um

Svängningsrörelsen är 200 um från minimum till maximum.

Jag väntar på laserdioder av typen ADL65052TL. Det enda kriteriet jag hade gällande val av laserdiod var att det skall finnas en ljusdiod i samma kapsel. Notera att många laserdioder som säljs nuförtiden saknar ljusdetektorn och således inte lämpar sig för ovanstående applikation.

Mätsystemet

Ovanstående övningsexempel ger data för hur mätsystemet måste konstrueras. Om vi vill täcka hela instrumentet från låga G upp till A på E-strängen så kommer den svängande strängens grundfrekvens att ligga mellan 200 Hz och ca. 1000 Hz.

Exemplet ovan visade att vi om vi tänker oss en amplitud på +/- 0,1 mm får ut en signal på 61,5 kHz från den lösa G-strängen. Om vi vill mäta G på E-strängen så ligger strängens frekvens på 800 Hz vilket med samma amplitudantagande ger en lasersensorfrekvens på 4*61,5 kHz = 246 kHz och ur samplingskriteriet ser vi att lasersignalen bör mätas minst med frekvensen 2*246 kHz = 492 kHz. För att vara på den säkra sidan bör vi sampla signalen i kanske 800 kHz under den tid då vi ligger nära maximal svängningsamplitud. Om vi antar att vi mäter 1/50 sekund så kommer vi att lagra 16000 mätvärden a’ 2 bytes d.v.s. vi behöver lagringsutrymme för 32000 mätvärden.

Ovanstående betyder att vi kan göra ett grovt val av hårdvara för vår mätare. Jag gissar att en Arduino Due som använder en ARM processor och kör på 84 MHz bör vara kapabel att sampla tillräckligt snabbt. Enligt specifikationerna bör processorn vara kapabel att sampla upp till 1 MHz. Processorn har 96 kbyte snabbt RAM minne vilket bör räcka till för buffert och analys. En Arduino Due kostar mellan 10 och 50 Euro beroende på varifrån man köper den … och om det är fråga om en orginal Due eller en kinesisk kopia. Mätsignalen måste förstärkas så att vi ligger vettigt i förhållande till AD-konverterns arbetsområde och upplösning 10 eller 12 bitar beroende av hur processorn konfigureras.

Kommande artiklar:

  • Laserdetektorns konstruktion och eventuella problem i samband med bygge.
  • Detektorns elektronik och datainsamling.
  • Knäppmekaniken
  • mätresultat.

 

 

Äggplotter, bygg själv!

05/02/2019

Det är alltid kul att lära sig något nytt och att tvingas motionera de små grå cellerna innanför det grå håret med problemlösning och felsökning av något man aldrig har lekt med tidigare.

Det är snart påsk igen, som baha’i ser jag fram mot baha’i nyår Naw Ruz … i båda fallen har ägg en plats i måltiderna i anslutning till dessa högtider. Det är för övrigt intressant att se hur många sedvänjor med en ursprungligen Zoroastrisk bakgrund (från det antika Persien) lever kvar i olika sedvänjor från Afghanistan till Spanien. Sedvänjorna har helt enkelt modifierats så att de passar ihop med den lokala religionen varefter man kan glömma varifrån sedvänjorna kommer.

I den Nordiska traditionen hör ägg, ofta snyggt målade, till påsktraditionen. Man färgar ägg på olika sätt. Själv minns jag hur man kokade ägg tillsammans med lämpliga växter för att ge äggen en trevlig färg. Senare har jag många gånger ritat figurer på ägg tillsammans med barnen. Vad är då naturligare än att använda moderna hjälpmedel för att göra mera avancerade utsmyckningar t.ex. i form av geometriska mönster som är alltför arbetsdryga eller komplicerade för att göras för hand.

Videon ovan visar en Ägg skrivare som dock ser relativt komplicerad ut 🙂 . Det här projektet använder dock betydligt bättre, och dyrare stegmotorer än vad jag använde.

Jag hittade en variant som kan skrivas ut med 3d skrivare och som såg snygg och väl genomtänkt ut. Bygget av den varianten och problem i anslutning till detta bygge beskrivs i den här artikeln. Jag utgick från den här äggplottern:

Vad behöver jag för projektet

Filerna som behövs för 3d utskrift av mekaniken hittar man på thingiverse i form av en stl-fil. Stl är en beskrivning av hur de mekaniska delar som skall skrivas ut med 3d skrivare ser ut. Innan skrivaren kan skriva ut komponenten måste stl-filen vidarebehandlas av ett lämpligt program som skär upp föremålet i en serie skikt  som 3d skrivaren sedan kan rita ut skikt för skikt och på såsätt långsamt bygga upp hela det föremål som skrivs ut. Jag använder programmet slic3r för att vidarebehandla stl-filen. Skrivarprogrammet jag använder är Repetier-Host V1.6.2.

Problem #1

Min 3d skrivare klarar av att skriva ut föremål med dimensionerna ca. 200x200x200 mm³.  Det visade sig att alla delar till Ägg skrivaren fanns i samma fil som kräver en betydligt större skrivare än den jag har. Alternativen är då att antingen hitta någon firma med en skrivare med tillräckligt stor byggyta … eller att modifiera stl-filen så att bitarna kan skrivas ut på min skrivare. Jag valde det senare alternativet.

Då jag själv konstruerar föremål som skall skrivas ut på 3d skrivare så använder jag programspråket openscad för att designa föremålen. Jag kan enkelt läsa in en stl-fil i openscad:

storbit1

Jag använder helt enkelt kommandot import( … ); för att läsa in stl-filen som ett enda stort föremål. Därefter använder jag kommandot difference() på den inlästa filen och använder stora lådor (klossar) som skär bort de delar jag  för tillfället inte vill se. Det spelar inte någon roll på vilket område på skärmen den kvarvarande delen finns, den centreras automatiskt av skrivaren.

Jag klippte söner stl-filen i tre olika föremålsgrupper d.v.s. skalet blev två olika bitar och de små mekaniska delarna blev en egen grupp. De olika beskurna delarna skrevs sedan ut separat. Jag antar att motsvarande funktionalitet finns i de flesta CAD-program som stöder 3d utskrift.

Utöver plastdelarna behövde jag två stycken små kullager, skruvar i olika dimensioner som jag klippte till lämpliga längder med hjälp av en Abico-tång. Lager hittade jag på en lokal järnaffär och skruvar har jag på lager i olika dimensioner.

Hopplockning av styrelektroniken

Skalet är konstruerat för en Arduino UNO med stor USB (typ B) kontakt. Jag har olika varianter av processorkortet på lager så jag valde helt enkelt en som fysiskt var kompatibel med en Arduino Uno. Processorkortet kostar kanske sex dollar på ebay.

Ett litet RC-servo av lämplig storlek hittade jag i miljonlådan (Tower Pro micro servo 9g). Ett servo kan beställas från ebay för någon dollar.

Jag hittade stegmotorn inklusive drivkort ULN2003 hos en elektronikaffär i Åbo och beställde den över nätet (https://www.triopak.fi/fi).

Jag monterade styrelektroniken utan att ännu bygga in den i det 3d utskrivna skalet. Jag ville enkelt kunna testa servot och stegmotorerna utan att ideligen öppna skalet på apparaten. Det började nu dyka upp problem …

Problem #2

Jag kunde utan problem kompilera drivprogrammet och ladda ner det på Arduinokortet. Då jag försökte få kontakt med äggskrivaren genom programmet Inkscape dök det upp problem. Ritprogrammet Inkscape för vilket det finns en tilläggsdel för att driva äggskrivaren verkade inte över huvudtaget kunna se Arduinoprocessorn.

Problemet visade sig bero på att då jag via min dator försöker komma i kontakt med Arduinon så skickar datorn via USB en DTR signal (data terminal ready) som på UNO är kopplad till Reset d.v.s. UNOn startade om då jag öppnade kommunikationen mot UNO. I sig är detta inget problem, jag behöver bara vänta på att Arduino Uno startar om varefter linjen är öppen och bör fungera i fortsättningen.

På en linuxdator söker Inkscape efter äggskrivaren genom att öppna en serieförbindelse i taget varefter programmet väntar på att UNO startar om varefter Inkscape skriver ‘v\r\n’ d.v.s. ber om äggskrivarens versionsinformation. Om svaret kommer inom en vettig tid så vet Inkscape var äggskrivaren finns och kan börja skicka kommandon till skrivaren. Orsaken till problemet är att en Linuxdator har tillgång till en stor mängd potentiella serieportar ocxh det krävs lång tid för att beta av alla portar … så lång tid att jag gav upp 😉 . Den lösning jag fastnade för var att läsa av om Arduino Uno var kopplad till min dator. Detta kan kontrolleras med hjälp av kommandot:

lsusb

Bus 002 Device 005: ID 248a:8367
Bus 002 Device 004: ID 046d:c52b Logitech, Inc. Unifying Receiver
Bus 002 Device 060: ID 1a86:7523 QinHeng Electronics HL-340 USB-Serial adapter
Bus 002 Device 002: ID 1a40:0101 Terminus Technology Inc. Hub
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 004 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 003 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub

Man kan enkelt identifiera den egna anslutna USB-enheten i mitt fall Arduino Uno-kortet genom att först ge kommandot lsusb utan att kortet är anslutet varefter man ansluter kortet och ger kommandot på nytt. Den nya rad som dyker upp innehåller den kod jag vill ha. Jag har märkt ut den aktuella raden med fet stil ovan.

Kommandot listar alla USB anslutna apparater på datorn. I princip har UNOn en unik identifikationsadress. Jag modifierade Inkscapes sökprogram så att det först tittade efter min specifika Arduino UNO innan den försöker söka igenom alla andra serieportar. Äggskrivaren hittas nu snabbt och bekvämt utan problem.

Eggbot tilläggsdelen för Inkscape finns i en linuxdator i katalogen:

~/.config/inkscape/extensions

Programmet jag modifierade var: ~/.config/inkscape/extensions/ebb_serial.py .

Jag lade in mitt Arduino UNOs id i raden:

if port[2].startswith(”USB VID:PID=1A86:7523”):

Koden 1A86:7523 fick jag från lsusb-kommandot ovan.

Problemet med att Arduino UNO startar om då en yttre enhet försöker ansluta över USB kan elimineras genom att bryta en förbindelse på kretskortet om Arduinon är en äkta Arduino Uno. I mitt fall är det fråga om en kopia från vilken man har lämnat bort den här egenskapen. Nackdelen med att kapa ifrågavarande hopptråd är att programmeringen blir besvärligare. Inom ca. en sekund efter att man börjar ladda upp program till Arduinon måste man trycka på reset på Arduinokortet … reset-tryckningen har eliminerats i Arduino Uno genom att låta DTR sköta saken men då dyker det upp ovanstående problem.

Problem #3

Min ena stegmotor av typen 28BYJ-48 var defekt. Stegmotorn råkade vara kopplad till armen som styrde pennan. Något kugghjul inne i motorns kuggväxel (64 ggr nedväxling) är defekt vilket leder till att motorn stoppar i ett specifikt läge.  Jag bytte ut motorn varefter det fungerade.

Problem #4

Då jag testade stegmotorerna med en verklig bild genom Inkscape så visade det sig att motorerna gick relativt varma och de snurrade endast i en riktning. Orsaken var att instruktionerna för hur styrsignalerna fron Arduino skulle kopplas till drivkortet var felaktiga. In/ut pinnarna 2,3,4,5 styr IN1, IN2, IN3 och IN4 på stegmotorns drivkort. Det visade sig att jag för korrekt funktion måste kasta om pinnarna 3 och 4. Kopplingen till stegmotorn blev då:

pin 2 –> IN1

pin4 –> IN2

pin3 –> IN3

pin5 –> IN4

På mitsvarande sätt måste pinnarna för den andra stegmotorn kopplas:

pin 8 –> IN1 (drivkort #2)

pin10–> IN2

pin9 –> IN3

pin11–>IN4

IMGP0849_mod.JPG

Bilden visar processorkortet Artduino UNO till vänster (blått) samt två stegmotordrivkort (vita) baserade på kretsen ULN2003. Drivkorten är inget stegmotorkontrollkort utan uttryckligen en serie transistorer som kan kopplas på och av under processorkontroll. Arduinin kontrollerar bitmönstret för varje steg.  För att en stegmotor skall stega måste en serie bestämda bitmönster genereras som genererar önskade magnetfält inne i stegmotorn och bitmönstrena måste skickas till motorn i korrekt ordning och ordningsföljden byter om man vill backa …

En stegmotorkontroller behöver egentligen endast två signaler steg och riktning en kontrollkrets kan oftast också justera strömmen genom stegmotorn vilket kan användas till bl.a. att justera motorns effekt och motorns temperatur. En riktig stegmotorkontroller innehåller egen intelligens och den genererar själv korrekta bitmönster för stegning framåt och bakåt. Det finns avancerade stegmotorkontrollers som också på egen hand klarar av att accelerera och decelerera. Man kan inte abrupt ändra stegfrkvensen på en stegmotor eftersom den då kan tappa bort steg vilket blir pinsamt om maskinen som styrs inte mäter den exakta positionen … vilket i allmänhet inte görs om man använder stegmotorer.

De fyrpoliga sladdarnas ledare i mitten (på de vita korten eller på arduino men inte båda) måste kastas om för att stegmotorn skall stega i båda riktningarna.

Efter att ett antal gånger ha plockat ihop äggskrivaren och sedan isär igen började grundfunktionerna se korrekta ut. Hålen i de 3d utskrivna mekaniska bitarna tenderade att vara för små vilket krävde filande samt varmluftsblåsare. Då ett hål nästan är korrekt kan man värma komponenten varefter man pressar på den med milt våld varvid passningen blir exakt. Någon bit värmde jag så mycket att den ändrade form vilket löstes genom att skriva ut en ny bit.

Ägghållaren vid rotationsstegmotorn använder en O-ring som friktionsyta mot ägget. Vid den fria axeländan som är kullagrad finns motsvarande något mindre O-ringsyta som trycks mot ägget med hjälp av en metallfjäder. Metallfjäderns längd kan justeras med hjälp av en mellanbricka som kan skrivas ut med 3d skrivare i olika tjocklekar. Tanken  är att ägget skall vara stadigt monterat men man får inte trycka så hårt att ägget går sönder. Det är extremt viktigt att ägget inte rör sig medan äggskrivaren skriver eftersom detta dödar all precision. Mer om injustering senare i artikeln. Jag är inte övertygad om att min konstreuktion som använder O-ringar nödvändigtvis fungerar speciellt bra. Tiden får utvisa och då måste något annat system prövas.

IMGP0858_mod.JPG

Pennans drivmekanik. Den lilla stegmotorn till höger i bilden vrider pennan kring centrum av ägget. Vi ser  den vita servoarmen som används för att höja/sänka pennan. Pennan trycks mot ägget av dess egen tyngd. Vi ser o-ringen  vid rotationsmotorn som fungerar som friktionsyta för att hålla ägget på plats.  Den fjäderbelastade hållaren för äggets spetsiga ända syns nere till höger.

IMGP0857_mod.JPG

Äggskrivaren sedd från en annan vinkel. Observera att pennans lyftservo är monterat på den vertikala armen som är monterad på stegmotorn i mitten.

Arbetsprocess för att rita på ett ägg

Vi använder programmet Inkscape som är ett öppet källkodsprogram som finns att ladda ner gratis för Windows, Masc och Linux. Jag kör sedan många år endast Linux och jag har inte ens Windows installerad på mina två arbetsmaskiner. Nedanstående är inte någon ingående instruktion för hur man använder Inkscape. Det finns goda instruktioner för detta om man googlar Inkscape.

Då vi monterar ägget är det viktigt att det är möjligast väl centrerat vilket vi kan kontrollera t.ex genom att utan penna vrida ägget ett varv. Detta kan vi enkelt göra genom att skapa en Inkscape bild med storleken 4070×800. Från dokumentets vänstra kant till dokumentets högra kant dras en rät linje mitt på dokumentet. Använd View/Zoom Out så att hela bildytan kan ses då strecket ritas från kant till kant.  Om vi utan penna ”skriver ut” detta dokument så kommer ägget att vridas 360 grader runt sin axel. Genom att hålla ett lämpligt föremål som referens mot skrivarbasen kan vi kolla om avståndet mellan referensföremålet hålls konstant genom varvet eller om avståndet varierar. Om vi sder betydande variationer lönar det sig att försiktigt flytta ägget vid någondera ändan så att felet blir så litet som möjligt. Det lönar sig att experimentera.

OneTurnInkscape

En hjälpbild som består av endast ett enda streck tvärs över bilden kan användas till att kontrollera att ägget roterar precis ett varv och att det sitter korrekt monterat utan att kasta av och an då det roterar.

Under Extensions/Eggbot/ hittar vi EggBot control. Med denna kontroll kan vi kontrollera att lyftning/sänkning av penna fungerar samt skriva ut en bild i Inkscape.  Vi börjar med att skriva ut (utan penna) vår räta linje för att kontrollera att ägget är centrerat och att det roterar precis ett varv.

Vi öppnar nu ett nytt dokument antingen så att vi skapar det själv eller så att vi laddar ner ett mönster vi gillar från nätet. Vi måste nu justera bildens storlek så att den precis fyller hela ägget utan att överlappa. För min skrivare med 28BYJ-48 stegmotorer och ULM2003 styrkretsar skall dokumentets storlek sättas till ungefär 4070(bredd) x 800 (höjd). Den bild vi har läst in kan manuellt justeras till den bredd vi vill ha.

Ett test av utskrift från Inkscape visar att skrivaren i princip fungerar men det är extremt viktigt att:

  • Ägget centreras korrekt så att det inte blir några kast då det roterar
  • Pennan ställs in till lämplig höjd så att den ritar också vid ytterlägena
  • Pennan ställs i mittläge innan ritningen påbörjas. Det finns annars risk för att pennan slår i ägghållaren i ändläget vilket kan lösgöra ägget delvis så att det gungar av och an.

Provritning gav resultatet nedan vilket inte ännu är så bra.

IMGP0863.JPG

Då man tittar noga på bilden ser man att det är en springa mellan ägget och tjockändans O-ring vilket gör att denna ända av ägget abrupt hoppar av och an med intressanta resultat vid utritningen.  Jag glömde att dra fast skruven som håller pennan vilket ledde till att pennan gungade av och an beroende av pennans rörelseriktning.

Jag kommer att modifiera tjockändans fastsättning för att åtgärda felen. Jag är helt optimistisk på att skrivaren ännu kommer att fungera helt OK.

Tack till alla kända och okända på nätet som har lagt ner mycket arbete på både utveckling av programvara och hårdvara.

Den extremt billiga och samtidigt kompakta stegmotorn 28BYJ-48 är inte bra till följd av att den är utväxlad. Kugghjulen i kuggväxeln är aldrig perfekta och speciellt i en sådan här billig motor så finns det glapp d.v.s. om vi ritar en linje i en rörelseriktning och sedan byter riktning så kommer det att finnas ett obestämt område till följd av glapp. Lösningen skulle vara bättre stegmotorer som inte är utväxlade men detta betyder samtidigt att en stor del av mekaniken måste förnyas. Byter man stegmotorerna så får man dock mycket bättre exakthet vid ritandet.

Modifierad fastsättning av ägget

Jag konstaterade ovan att ägget gungade av och an i upphängningen gjord av O-ringar. Orsaken var att O-ringens dimension i förhållande till ägget blev fel. Ägget låg antagligen delvis direkt mot ändplattan vilket gjorde att det kunde glida sidlänges vilket i sin tur gjorde ritandet mycket inexakt.

Jag hittade röda flaskkorkar av gummi (se bild nedan). I finland är det här traditionella korkar som man speciellt hittar på hösten vid saftningstider. Jag köpte en påse korkar och klippte sönder en som blev en ca. 7 mm djup mjuk ägghållare. Resultatet blev mycket bra och precisionen förbättrades radikalt.

IMGP0883.JPG

Med den modifierade hållaren blir ritandet reproducerbart, räta linjer som går runt hela ägget träffar startpunkten igen efter ett varv … något som inte hände tidigare.

IMGP0882.JPG

Notera den 3d utskrivna pennhållaren. Jag skrev också ut en hållare för tre pennor i vilka pennorna kan förvaras utan kork. Den extra pennadaptern gör det mycket enklare att montera pennerna exakt och lika för alla pennor.

Planerade vidare modifiationer

Det finns fortfarande ett tydligt kännbart glapp i pennans stegmotor till följd av stegmotorns växellåda med utväxlingen 1:64 d.v.s. stegmotorn roterar 64 varv då den utgående axeln roterar ett varv.
Jag beställde en Nema 13 stegmotor som har ungefär samma dimensioner som den nu använda 28BYJ-48 men helt utan utväxling.  Då jag får stegmotorn så planerar jag att först köra den med den existerande drivkretsen och om det fungerar OK så använder jag den. Alternativet är att jag byter ULN2003 drivkortet mot en kontroller av den typ som finns i dagens 3d-skrivare. Exempelvis A4988 eller DRV8825 av vilka jag uppfattar den senare som marginellt bättre.

En stegmotor av annan typ kommer att kräva modifikation av ritarmen d.v.s. jag blir tvungen att designa en egen variantmed något annorlunda dimensioner. Fastsättningen av armen i stegmotoraxeln kommer att säkras med skruv vilket inte är falllet nu.

Beslutar jag mig för att byta ut stegmotorkontrollern så blir jag antagligen tvungen att skriva ut en modifierad bottenplatta eftersom A4988 och DRV8825 saknar skruvhål och därför kräver en annan fastsättning.

Några källor:

Byggbeskrivningen för den här presenterade Egg Painter Mini.

Den ursprungliga EggBot skrivaren.

Mjukvara till äggskrivaren.

Byggbeskrivning i PDF.

 

 


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