Innehållsförteckning:

En meny i Arduino och hur man använder knapparna: 10 steg (med bilder)
En meny i Arduino och hur man använder knapparna: 10 steg (med bilder)

Video: En meny i Arduino och hur man använder knapparna: 10 steg (med bilder)

Video: En meny i Arduino och hur man använder knapparna: 10 steg (med bilder)
Video: Lesson 02 Arduino IDE Software | Robojax Arduino Step By Step Course 2024, November
Anonim
En meny i Arduino och hur man använder knappar
En meny i Arduino och hur man använder knappar

I min Arduino 101 -handledning lär du dig hur du konfigurerar din miljö i Tinkercad. Jag använder Tinkercad eftersom det är en ganska kraftfull onlineplattform som gör att jag kan visa en rad färdigheter för studenter för att bygga kretsar. Bygg gärna alla mina självstudier med Arduino IDE och en riktig Arduino!

I denna handledning kommer vi att lära oss om knappar! Vi behöver veta:

  • Hur man kopplar upp dem
  • Läser deras värde
  • Debounce, och varför det är viktigt
  • En praktisk applikation (skapa en meny)

De flesta tycker att det mest praktiska med en knapp är att tända och släcka en lampa. Vi kommer, inte här! Vi kommer att använda vår för att skapa en meny och ställa in några alternativ på Arduino.

Redo? Låt oss börja!

Steg 1: Konfigurera kortet

Sätt upp styrelsen
Sätt upp styrelsen
Sätt upp styrelsen
Sätt upp styrelsen

Det första steget är att sätta en Arduino och Breadboard Small på prototypområdet. Kontrollera bilderna ovan för att se hur du kopplar upp strömskenorna.

En Breadboard Mini har två kraftskenor upp och ner. Vi kopplar dessa till Arduino så att vi kan ge ström till fler komponenter. Senare i denna handledning kommer vi att använda 3 knappar så vi behöver mer ström. Saken att notera är att på en brödbräda liten går strömskenorna tvärs över brädet, horisontellt. Detta skiljer sig från kolumnerna i huvudprototypområdet i mitten; dessa löper vertikalt. Du kan använda vilken som helst av strömstiften för att ge ström till vilken kolumn som helst i huvudområdet i mitten.

När du lägger till ström använder du svarta och röda ledningar till de negativa respektive positiva. Lägg till ledningar i slutet som driver ström till andra sidan av brädet. Vi kommer inte att använda den sidan, men det är bra praxis.

Steg 2: Lägg till knappen och motståndet

Lägg till knappen och motståndet
Lägg till knappen och motståndet
Lägg till knappen och motståndet
Lägg till knappen och motståndet
Lägg till knappen och motståndet
Lägg till knappen och motståndet

Lägg till en liten tryckknapp från komponentfacket. Det ska se ut som det på bilden. Se till att det inte är en switch! Lägg till ett motstånd också. Klicka på den och ställ in dess värde till 10kΩ. Det räcker för att dra ner pinnen när den inte är ansluten, vilket är mycket viktigt senare i koden.

Placera komponenten mitt på panelen. Så här fungerar en knapp:

  • Hörn till hörn, knappen är inte ansluten. Genom att trycka på knappen stängs kontakterna och förbinder hörnen.
  • Knappens sidor är anslutna. Om du anslöt en kabel uppe till vänster och nedre vänster, skulle kretsen stängas.

Det är därför vi lägger komponenten över utrymmet i mitten. Det ser till att hörnen inte är anslutna under stiften i brädet.

Nästa steg ger ett par bilder som illustrerar dessa punkter.

Placera motståndet från den nedre högra stiftet över kolumnerna så att det sitter horisontellt.

Steg 3: Knappanslutningar

Knappanslutningar
Knappanslutningar
Knappanslutningar
Knappanslutningar

Bilderna ovan gör det ganska tydligt hur knapparna ansluter. Det var alltid en förvirring när man tycker att något är bra och att det inte fungerar!

Låt oss nu lägga till trådarna.

  • Placera en röd ledning från en positiv strömstift till samma kolumn som den nedre högra stiftet på knappen
  • Placera en svart ledning från en negativ strömstift till samma kolumn som motståndet.
  • Placera en färgad tråd (inte röd/svart) från den övre vänstra stiftet till Digital Pin 2 på Arduino

Kontrollera bilderna ovan för att se till att din ledning är korrekt.

Steg 4: Koden …

Koden…
Koden…
Koden…
Koden…

Låt oss titta på koden för en grundläggande knapp.

Öppna kodredigeraren och ändra från block till text. Rensa varningen som kommer. Vi är nöjda med texten!

Du känner till den grundläggande inställningen, så låt oss definiera knappen och göra en grundläggande läsning. Vi skriver ut utmatningen till Serial.

Jag lägger in några extra kommentarer i koden nedan så det är lättare att läsa än bilden.

// Definiera konstanter

#define button 2 void setup () {pinMode (button, INPUT); Serial.begin (9600); } void loop () {// Läs den digitala pinnen för att kontrollera status för knappen int intryckt = digitalRead (knapp); // Knappen returnerar HÖG om den trycks ned, LÅG om inte om (nedtryckt == HÖG) {Serial.println ("Tryckt!"); }}

Ok, det fungerar!

I huvudsak är allt vi gör att kontrollera statusen för den digitala stiftet varje gång koden slingas. Om du klickar på Starta simulering och trycker på knappen ser du Serial Monitor (klicka på knappen nedanför koden) "Pressad!" upprepat.

En funktion som du kommer att se i koden ovan är utvärderingen av () tillstånd som äger rum. Allt koden gör är att ställa en fråga och utvärdera om det är sant, i det här fallet. Vi använder är lika (dubbla lika tecken, så här: ==) för att kontrollera om variabelns värde är lika med ett visst värde. En digitalRead () returnerar antingen HIGH eller LOW.

Genom att använda if () else if / else kan vi kontrollera många villkor eller alla villkor, och om du går tillbaka till Arduino Basics ser du några av de jämförelser du kan göra.

Nu … Vår kod kan se komplett ut … Men vi har ett problem.

Se, det fungerar riktigt bra när du är i simulatorn. Men verklig elektricitet har buller, särskilt likströmselektronik. Så vår knapp kan ibland returnera en falsk läsning. Och det är ett problem, eftersom ditt projekt kanske inte svarar på rätt sätt för användaren.

Låt oss fixa det!

Steg 5: Lite debounce

En liten debounce
En liten debounce

Vi använder ett förfarande som kallas debounce för att övervinna vårt knappproblem. Detta väntar i huvudsak en viss tid mellan att knappen trycktes in och faktiskt svarade på tryckningen. Det känns fortfarande naturligt för användaren (om du inte gör tiden för lång). Du kan också använda den för att kontrollera trycklängden, så att du kan svara olika varje gång. Du behöver inte byta kabeldragning!

Låt oss titta på koden:

#definiera knapp 2#definiera debounceTimeout 100

Den första förändringen är på global nivå. Du kommer ihåg att det är där vi definierar variabler som många av våra funktioner kan använda eller de som inte kan återställas varje gång slingan utlöses. Så vi lade till debounceTimeout till de definierade konstanterna. Vi gjorde denna 100 (som senare kommer att översättas till 100 ms), men den kan vara kortare. Längre och det kommer att kännas onaturligt.

long int lastDebounceTime;

Denna variabel deklareras under konstanterna. Detta är en lång int -typ, som i princip låter oss lagra långa nummer i minnet. Vi kallade det lastDebounceTime.

Vi behöver inte ändra någonting i funktionen void setup (). Låt oss lämna den.

void loop () {// Läs den digitala pinnen för att kontrollera status för knappen int intryckt = digitalRead (knapp); long int currentTime = millis (); // Knappkod}

Den första ändringen vi gör i loop () -funktionen är under samtalet för att läsa knappen. Vi måste hålla reda på den aktuella tiden. Funktionen millis () returnerar den aktuella tiden för klockan sedan Arduino startade i millisekunder. Vi måste lagra detta i en lång variabel av typen int.

Nu måste vi se till att vi är medvetna om tiden sedan knappen trycktes, så vi återställer timern när den inte trycks in. Ta en titt:

void loop () {// Läs den digitala pinnen för att kontrollera status för knappen int intryckt = digitalRead (knapp); long int currentTime = millis (); if (nedtryckt == LÅG) {// Återställ räkningstiden medan knappen inte är nedtryckt lastDebounceTime = currentTime; } // Knappkod}

Algoritmen if (nedtryckt == LÅG) kontrollerar om knappen inte trycks in. Om det inte är det, lagrar koden den aktuella tiden sedan senaste avvisningen. På så sätt har vi varje gång knappen trycks in en tidpunkt från vilken vi kan kontrollera när knappen trycktes in. Vi kan sedan göra en snabb matematisk beräkning för att se hur länge knappen trycktes in och svara korrekt. Låt oss titta på resten av koden:

void loop () {// Läs den digitala pinnen för att kontrollera status för knappen int intryckt = digitalRead (knapp); long int currentTime = millis (); if (nedtryckt == LÅG) {// Återställ räkningstiden medan knappen inte är nedtryckt lastDebounceTime = currentTime; } // Knappen har tryckts under en viss tid om (((currentTime - lastDebounceTime)> debounceTimeout)) {// Om timeout har nåtts, tryck på knappen! Serial.println ("Pressad!"); }}

Det sista kodblocket tar den aktuella tiden, subtraherar den senaste avvisningstiden och jämför den med den tidsgräns vi ställde in. Om den är större antar koden att knappen har tryckts in under den tiden och svarar. Propert!

Kör din kod och kontrollera att den fungerar. Om du har fel, kontrollera din kod!

Låt oss nu titta på ett praktiskt exempel.

Steg 6: Att göra en meny

Att göra en meny
Att göra en meny

Knappar är intressanta, eftersom det finns så många möjligheter med dem! I det här exemplet ska vi göra en meny. Låt oss säga att du har skapat den här fantastiska enheten och behöver användare för att kunna ändra alternativ för att slå på eller av vissa saker eller ställa in ett visst värde för en inställning. Denna tre knapps design kan göra det!

Så för detta projekt behöver vi:

  • Tre knappar
  • Tre motstånd inställda på 10kΩ

Vi har redan en av dessa, vi behöver bara de andra två. Så lägg till dem på tavlan. Anslutningen är lite mer komplex, men bara för att jag ville behålla den riktigt kompakt. Du kan följa samma mönster för den första knappen eller följa bilden ovan.

De tre knapparna är ett menyöppet/nästa alternativ, ett ändringsalternativ (som i, ändra inställningen) och en spara/stäng menyknapp.

Koppla ihop det, låt oss titta på koden!

Steg 7: Kodfördelning - global

Ok, det här kommer att bli ett långt steg, men jag kommer att gå igenom varje kodavsnitt.

Låt oss först titta på de globala variabler som behövs.

// Definiera konstanter #definiera menuButton 2 #define menuVälj 3 #definiera menuSave 4 #define debounceTimeout 50 // Define variables int menuButtonPreviousState = LOW; int menuSelectPreviousState = LÅG; int menuSavePreviousState = LÅG; long int lastDebounceTime; // Menyalternativ char * menuOptions = {"Kontrollera temp", "Kontrollampa"}; bool featureSetting = {false, false}; bool menuMode = false; bool menuNeedsPrint = false; int optionSelected = 0;

Dessa tre block är ganska lika det vi har sett tidigare. I den första har jag definierat de tre knapparna och timeout. För den här delen av projektet har jag satt det till 50 ms så det krävs en avsiktlig press för att få det att fungera.

Det andra blocket är alla variabler. Vi måste hålla reda på knappenPreviousState, och vi måste hålla reda på den senasteDebounceTime. Dessa är alla int -typvariabler, men den sista är en lång typ eftersom jag antar att vi behöver utrymmet i minnet.

Menyalternativblocket har några nya funktioner. Först, char * (ja, det är en avsiktlig asterisk), som är en tecken/sträng bokstavlig variabel. Det är en pekare till en statisk lagring i minnet. Du kan inte ändra det (som du kan till exempel i Python). Denna rad *menuOptions skapar en rad strängbokstavar. Du kan lägga till så många menyalternativ som du vill.

Bool featureSetting -variabeln är bara den mängd värden som representerar varje menyalternativ. Ja, du kan lagra vad du vill, bara ändra variabeln (de måste alla vara av samma typ). Nu kan det finnas bättre sätt att hantera detta, som ordböcker eller tupler, men det här är enkelt för den här applikationen. Jag skulle förmodligen skapa en av de senare i en distribuerad applikation.

Jag har hållit koll på menuMode, så om jag ville ha andra saker på min skärm kunde jag göra det. Om jag hade sensorlogik kan jag också pausa det under menyoperationen, om något skulle komma i konflikt. Jag har en menyNeedsPrint -variabel eftersom jag vill skriva ut menyn vid specifika tidpunkter, inte bara hela tiden. Slutligen har jag en optionSelected variabel, så jag kan hålla reda på det valda alternativet när jag öppnar det på ett antal platser.

Låt oss titta på nästa uppsättning funktioner.

Steg 8: Kodfördelning - installation och anpassade funktioner

Setup () -funktionen är enkel nog, bara tre ingångsdeklarationer:

void setup () {pinMode (menuSelect, INPUT); pinMode (menuSave, INPUT); pinMode (menuSelect, INPUT); Serial.begin (9600); }

Därefter är de tre anpassade funktionerna. Låt oss titta på de två första, sedan den sista separat.

Vi behöver två funktioner som returnerar viss information. Anledningen är att vi vill se till att detta är på ett sätt läsbart för människor. Det hjälper också med felsökning av koden om vi har ett problem. Koda:

// Funktion för att returnera det aktuella valda alternativet *ReturnOptionSelected () {char *menuOption = menuOptions [optionSelected]; // Returalternativ Vald returmeny Alternativ; } // Funktion för att returnera status för det aktuella valda alternativet char *ReturnOptionStatus () {bool optionSetting = featureSetting [optionSelected]; char *optionSettingVal; if (optionSetting == false) {optionSettingVal = "Falskt"; } else {optionSettingVal = "True"; } // Return optionSetting return optionSettingVal; }

Funktionen char *ReturnOptionSelected () kontrollerar det valda alternativet (om du ser ovan ställer vi in en variabel för att hålla reda på det) och drar strängen bokstavligt från matrisen som vi skapade tidigare. Den returnerar den sedan som en rödingstyp. Vi vet detta eftersom funktionen anger returtypen.

Den andra funktionen, char *ReturnOptionStatus () läser statusen för det alternativ som sparas i matrisen och returnerar en strängbokstav som representerar värdet. Till exempel, om inställningen vi har lagrat är falsk, skulle jag returnera "Falskt". Detta beror på att vi visar användaren denna variabel och det är bättre att hålla all denna logik ihop. Jag skulle kunna göra det senare, men det är mer meningsfullt att göra det här.

// Funktion för att växla aktuell optionbool ToggleOptionSelected () {featureSetting [optionSelected] =! FeatureSetting [optionSelected]; återvända sant; }

Funktionen bool ToggleOptionSelected () är en bekvämlighetsfunktion för att ändra värdet på inställningen som vi har valt i menyn. Det vänder bara värdet. Om du hade en mer komplex uppsättning alternativ kan det vara ganska annorlunda. Jag returnerar true i den här funktionen, eftersom mitt återuppringning (samtalet senare i koden som avfyrar denna funktion) förväntar sig ett sant/falskt svar. Jag är 100% säker på att detta kommer att fungera, så jag redogjorde inte för att det inte fungerade, men jag skulle i en distribuerad applikation (för säkerhets skull).

Steg 9: The Loop …

Loop () -funktionen är ganska lång, så vi gör det i delar. Du kan anta allt nedan bo inom denna funktion:

void loop () {

// Arbeta här <-----}

Ok, vi såg det här förut:

// Läs knapparna int menuButtonPressed = digitalRead (menuButton); int menuSelectPressed = digitalRead (menuSelect); int menuSavePressed = digitalRead (menuSave); // Få den aktuella tiden long int currentTime = millis (); if (menuButtonPressed == LOW && menuSelectPressed == LOW && menuSavePressed == LOW) {// Återställ räkningstiden medan knappen inte är nedtryckt lastDebounceTime = currentTime; menuButtonPreviousState = LÅG; menuSelectPreviousState = LÅG; menuSavePreviousState = LÅG; }

Allt jag behövde göra här var att lägga till de tre digitalRead () -samtalen och se till att jag redogjorde för det faktum att om alla knappar var låga, skulle vi återställa timern (lastDebounceTime = currentTime) och ställa in alla tidigare tillstånd till låg. Jag lagrar också millis () i currentTime.

Nästa avsnitt häckar inuti linjen

if (((currentTime - lastDebounceTime)> debounceTimeout)) {

// Arbeta här <----}

Det finns tre sektioner. Ja, jag kunde ha flyttat dem till sina egna funktioner, men för enkelhetens skull behöll jag de tre huvudknappalgoritmerna här.

if ((menuButtonPressed == HIGH) && (menuButtonPreviousState == LOW)) {if (menuMode == false) {menuMode = true; // Låt användaren veta Serial.println ("Menyn är aktiv"); } annars om (menuMode == true && optionSelected = 1) {// Återställ option optionSelected = 0; } // Skriv ut menyn menuNeedsPrint = true; // Växla knappen föreg. ange att endast visa meny // om knappen släpps och trycks ner igen menuButtonPreviousState = menuButtonPressed; // Skulle vara HÖG}

Den första hanteras när menuButtonPressed är HIGH, eller när menyknappen trycks ned. Det kontrollerar också att det tidigare tillståndet var LÅGT, så att knappen måste släppas innan den trycks igen, vilket hindrar programmet från att ständigt avfyra samma händelse om och om igen.

Den kontrollerar sedan att om menyn inte är aktiv, aktiveras den. Det kommer att skriva ut det första valda alternativet (vilket är det första alternativet i menyn Alternativmatris som standard. Om du trycker på knappen en andra eller tredje (etc) gång får du nästa alternativ i listan. Något jag kan fixa är att när det kommer till slutet, cyklar det tillbaka till början. Detta kan läsa längden på matrisen och göra det enklare att cykla tillbaka om du ändrade antalet alternativ, men det var enkelt för nu.

Det sista lilla avsnittet (// Skriver ut menyn) skriver uppenbarligen menyn, men det ställer också in det föregående läget till HÖG så att samma funktion inte slingas (se min anteckning ovan om att kontrollera om knappen tidigare var LÅG).

// menuSelect trycks in, ange logicif ((menuSelectPressed == HIGH) && (menuSelectPreviousState == LOW)) {if (menuMode) {// Ändra det valda alternativet // Just nu är detta bara sant/falskt // men kan vara vad som helst bool toggle = ToggleOptionSelected (); if (växla) {menuNeedsPrint = true; } else {Serial.println ("Något gick fel. Försök igen"); }}} // Växla tillstånd för att bara växla om det släpps och trycks in igen menuSelectPreviousState = menuSelectPressed; }

Denna kodbit hanterar menuSelectPressed -knappen på samma sätt, förutom den här gången avfyrar vi bara funktionen ToggleOptionSelected (). Som jag sa tidigare kan du ändra den här funktionen så att den gör mer, men det är allt jag behöver för att göra.

Det viktigaste att notera är växlingsvariabeln, som spårar återuppringningens framgång och skriver ut menyn om den är sann. Om det inte returnerar något eller falskt kommer det att skrivas ut felmeddelandet. Det är här du kan använda din återuppringning för att göra andra saker.

if ((menuSavePressed == HIGH) && (menuSavePreviousState == LOW)) {// Avsluta menyn // Här kan du städa upp // eller spara till EEPROM menuMode = false; Serial.println ("Meny avslutad"); // Växla tillstånd så att menyn bara går ut när menuSavePreviousState = menuSavePressed; }}

Denna funktion hanterar menuSave -knappen, som precis lämnar menyn. Det är här du kan ha ett alternativ för att avbryta eller spara, kanske städa eller spara i EEPROM. Jag skriver bara ut "Meny avslutad" och ställer knappläget till HÖG så att det inte går i loop.

if (menuMode && menuNeedsPrint) {// Vi har skrivit ut menyn, så om det inte händer något // behöver du inte skriva ut den igen menuNeedsPrint = false; char *optionActive = ReturnOptionSelected (); char *optionStatus = ReturnOptionStatus (); Serial.print ("Selected:"); Serial.print (optionActive); Serial.print (":"); Serial.print (optionStatus); Serial.println (); }

Detta är menuPrint -algoritmen, som bara aktiveras när menyn är aktiv och när variabeln menuNeedsPrint är satt till true.

Detta skulle definitivt kunna flyttas till sin egen funktion, men för enkelhetens skull..!

Tja, det är det! Se nästa steg för hela kodblocket.

Steg 10: Slutkodblock

// Definiera konstanter

#define menuButton 2 #define menuSelect 3 #define menuSave 4 #define debounceTimeout 50 int menuButtonPreviousState = LOW; int menuSelectPreviousState = LÅG; int menuSavePreviousState = LÅG; // Definiera variabler long int lastDebounceTime; bool lightSensor = true; bool tempSensor = true; // Menyalternativ char * menuOptions = {"Kontrollera temp", "Kontrollampa"}; bool featureSetting = {false, false}; bool menuMode = false; bool menuNeedsPrint = false; int optionSelected = 0; // Inställningsfunktion

void setup () {pinMode (menuSelect, INPUT); pinMode (menuSave, INPUT); pinMode (menuSelect, INPUT); Serial.begin (9600); }

// Funktion för att returnera det aktuella valda alternativet char *ReturnOptionSelected () {char *menuOption = menuOptions [optionSelected]; // Returalternativ Vald returmeny Alternativ; } // Funktion för att returnera status för det aktuella valda alternativet char *ReturnOptionStatus () {bool optionSetting = featureSetting [optionSelected]; char *optionSettingVal; if (optionSetting == false) {optionSettingVal = "Falskt"; } else {optionSettingVal = "True"; } // Return optionSetting return optionSettingVal; } // Funktion för att växla nuvarande alternativ bool ToggleOptionSelected () {featureSetting [optionSelected] =! FeatureSetting [optionSelected]; återvända sant; } // Huvudslingan

void loop () {// Läs knapparna int menuButtonPressed = digitalRead (menuButton); int menuSelectPressed = digitalRead (menuSelect); int menuSavePressed = digitalRead (menuSave); // Få den aktuella tiden long int currentTime = millis (); if (menuButtonPressed == LOW && menuSelectPressed == LOW && menuSavePressed == LOW) {// Återställ räkningstiden medan knappen inte är nedtryckt lastDebounceTime = currentTime; menuButtonPreviousState = LÅG; menuSelectPreviousState = LÅG; menuSavePreviousState = LÅG; } if (((currentTime - lastDebounceTime)> debounceTimeout)) {// Om tidsgränsen nås, tryck på knappen!

// menyknappen trycks in, ange logik

// Avfyras endast när knappen har släppts tidigare om ((menuButtonPressed == HIGH) && (menuButtonPreviousState == LOW)) {if (menuMode == false) {menuMode = true; // Låt användaren veta Serial.println ("Menyn är aktiv"); } annars om (menuMode == true && optionSelected = 1) {// Återställ option optionSelected = 0; } // Skriv ut menyn menuNeedsPrint = true; // Växla knappen föreg. ange att endast visa meny // om knappen släpps och trycks ner igen menuButtonPreviousState = menuButtonPressed; // Skulle vara HÖG} // menuSelect trycks in, ange logik om ((menuSelectPressed == HIGH) && (menuSelectPreviousState == LOW)) {if (menuMode) {// Ändra det valda alternativet // För tillfället är detta bara true/false // men kan vara vad som helst bool toggle = ToggleOptionSelected (); if (växla) {menuNeedsPrint = true; } else {Serial.print ("Något gick fel. Försök igen"); }}} // Växla tillstånd för att bara växla om det släpps och trycks in igen menuSelectPreviousState = menuSelectPressed; } if ((menuSavePressed == HIGH) && (menuSavePreviousState == LOW)) {// Avsluta menyn // Här kan du städa upp // eller spara till EEPROM menuMode = false; Serial.println ("Meny avslutad"); // Växla tillstånd så att menyn bara går ut när menuSavePreviousState = menuSavePressed; }} // Skriv ut det aktuella menyalternativet aktivt, men skriv det bara ut en gång om (menuMode && menuNeedsPrint) {// Vi har skrivit ut menyn, så om inget händer // behöver du inte skriva ut det igen menuNeedsPrint = false; char *optionActive = ReturnOptionSelected (); char *optionStatus = ReturnOptionStatus (); Serial.print ("Selected:"); Serial.print (optionActive); Serial.print (":"); Serial.print (optionStatus); Serial.println (); }}}

Kretsen är tillgänglig på Tinkercad -webbplatsen. Jag har inbäddat kretsen nedan för att du också ska se!

Som alltid, vänligen meddela mig om du har frågor eller problem!

Rekommenderad: