Přesuvna řízená Arduinem

Pro potřeby svého minikolejiště Malá Paka jsem potřeboval vyrobit přesuvnu. Abych mohl na jednom konci tratě měnit vozy a tvořit nové vlaky. Věc samotná není složitá, stačí jen přesně pracovat. Ale chtěl jsem od toho víc – když bude celé kolejiště řízené digitálně, nebudu přece tahat nějaký šuplík ručně.

Ideu přinesl známý modelář-elektronik-programátor Fulda svým řešením ovládání segmentové točny s jednočipovým procesorem a skrz tlačítka, kterou vytvořil pro Honzíkovy vláčky. To byl první nápad – a nakonec, tohle by mi stačilo, tři polohy, tři tlačítka a místo tlačítek se dají zapojit výstupy dekodéru příslušenství a je to. Jenže když jsem Fuldovi napsal, tak mne navedl na to, že on to nově chce řešit s Arduinem a že ať se na to podívám a dal mi nějaké tipy. První dojem byl – no to nemám šanci dát dohromady. Ne elektricky, ale programově. Jednočipáky neumím a programoval jsem naposledy v Pascalu v době, kdy se na náměstí zvolilo klíčema.

No ale nešlo jinak. Takže sehnat Arduino, driver pro krokové motory a hlavně ten motor. Ten jsem si vyhlídnul ještě dřív, Přesuvna bude malinká, tak proč shánět pojezdy na šuplíky a motor a závitovou tyč, když se to dá v Číně sehnat komplet a tisíckrát přesnější? Dá se koupit i tohle:

Lineární posuv s krokovým motorem

Kompletní lineární posuv pro 3D tiskárny s přesným kuličkovým vedením a krokovým motorem, to vše sesazeno a funkční za 40 dolarů. Most přesuvny s kolejemi se na něj uchytí přes nějaký plech, více informací k tomu najdete v článku o kolejišti.

Druhým krokem bylo Arduino – ono je to nakonec snadné. Zapojíte to do USB a nahrajete první program z příkladů, které jsou obsaženy v „přiloženém“ softwaru. A jakmile rozblikáte LED na desce Arduina, už je to jasné, jste lapeni a nedá vám to a ležíte v tom tak dlouho, dokud se to nenaučíte. Protože se na webu dají najít hotové příklady, jak přesuvnu či točnu řešit. Jenže – jeden řídí točnu skrz DCC, ale ne tlačítky a navíc je polohování točny jiné – má dva konce a otáčí se dokola. Druhý program je sice pro přesuvnu a pro ovládání tlačítky, ale zas nepoužívá plynulý rozjezd a dojezd. Člověk by přitom chtěl jak ovládání skrz DCC, tak i tlačítky a i ty plynulé rozjezdy a dojezdy. Takže zjistit, jak na to, vyzkoušet to na příkladech, krok po kroku a na cizích řešeních poznávat, jak to vlastně pracuje. A protože přesuvna funguje v reálném čase, musíte se naučit i to, jak vykonáváním jedné funkce neblokovat funkci jinou. A nejlepší je nakonec zjistit, že jste zvládli vyřešit a naprogramovat něco, co se i na fórech uvádí jako nevyřešená otázka – nouzové zastavení.

Po asi měsíci bádání a zkoušení je výsledek na světě. Arduino Nano je doplněno driverem krokových motorů A4988 a optosnímačem pro zjištění koncové polohy. Program využívá knihovnu Accelstepper pro plynulé řízení motoru a knihovnu NMRA DCC pro čtení povelů digitální centrály. Na vstupu je oddělovací optočlen, který ze signálu v kolejích převádí povely do Arduina. Když na ovladači stisknu příslušný povel pro přesun do jedné ze tří poloh, tak Arduino si vyslanou adresu převede na číslo polohy a motor se vydá tím směrem. Současně se stále čte stav tlačítek, pro každou pozici je určeno jedno, a přesuvnu je možno posouvat i tímto „místním“ ovládáním. Další z tlačítek slouží pro najetí přesuvny na optosenzor při začátku provozu – přesuvna si najde konec, vynuluje počitadlo polohy a je připravena k práci.

Chtěl jsem, aby bylo možné přesuvnu okamžitě zastavit, když se například dostane něco do její dráhy, nebo když si ji člověk spustí omylem v nevhodnou chvíli. Tlačítko je jedna možnost, ale člověk má v ruce DCC ovladač, který Stop funkci taky zná – její aktivací odpojíte z kolejiště napájení. Převod tohoto stavu do Arduina je snadný – další optočlen a když není v kolejích signál, vydej povel. Jenže co s tím dál – Accelstepper nezná něco jako okamžitý stop a různí autoři to řeší různě, ale ne uspokojivě. Chtěl jsem, aby po obnovení napájení bylo možné přesuvnu poslat do původně zamýšlené (nebo jiné) pozice bez nutnosti kalibrace polohy. Po dalším týdnu ležení v programu a zkoušení, co funguje a co ne, se to povedlo.

Elektronika pohonu - pokus

Dosti povídání, přejděme k faktům:

Co to umí:
– přesunout most přesuvny do požadované pozice na pokyn DCC ovladače (zadáním adresy) nebo po stisknutí tlačítka místního ovládání
– pomalé rozjezdy a dojezdy
– zkalibrovat výchozí polohu najetím na koncový optosenzor, opět skrz DCC nebo tlačítko
– okamžitě zastavit při povelu stop z ovladače (při odpojení DCC signálu z kolejiště) nebo při stisku nouzového tlačítka

Co to neumí:
– nastavovat jakékoliv parametry skrz DCC centrálu; základní adresa (prozatím 200) je zadaná v konfiguraci programu a další pozice jsou adresy po ní následující do počtu poloh přesuvny
– řídit mikrokrokování nastavením programu (dle mých zkušeností je nejlíp to vyzkoušet a pak zapojit natvrdo). Doplnit by to šlo, ale nemá to cenu.

Potřebný další software:
– knihovna NMRA DCC pro spolupráci s DCC protokolem. Obsažená v Arduino IDE, více zde: https://mrrwa.org/
– knihovna Accelstepper pro plynulé rozjezdy a zastavení a pro přímé zadávání cíle. Obsažená v Arduino IDE, dokumentace zde: https://www.airspayce.com/mikem/arduino/AccelStepper/

Potřebný hardware:
– Arduino Nano, stačí čínská verze (nutno stáhnout USB drivery)
– driver krokových motorů A4988 (trimrem je potřeba nastavit proudové omezení podle proudu motoru)
– koncový spínač (optosenzor, např. https://www.aliexpress.com/item/1908175930.html)
– optočlen pro vstup DCC signálu z kolejiště – schéma níže, je dobré využít tento obvod, protože tvaruje signál, Fulda doporučuje i low-pass filtr pro odstranění rušení
– druhý optočlen pro získání informace o napětí v koleji (schéma doplním, ale v principu to samé, je-li napětí v kolejích, tak tranzistor optočlenu zkratuje určený vstup Arduina proti zemi)
– tlačítka
– krokový motor, dvojfázový (4 dráty)

Co je kam zapojeno, je popsáno na začátku programu a okomentováno (zatím anglicky, můžu vám to přepsat, pokud bude zájem). Taky dále na obrázku.

Popis kódu:
Na začátku je definice vstupů a výstupů, následuje definice proměnných a parametrů.
DCC část – funguje s využitím přerušení na pinu 2, jakmile dorazí paket, převede se na číslo pozice. Je-li výsledek 0 (nula), zavolá se rutina pro najetí na optoseznzor (moveToHome), je-li výsledek jiné číslo z rozsahu pozic přesuvny, volá se rutina pro najetí na pozici (moveToPosition).
Tlačítková část – obsažena v hlavní smyčce, volají se tlačítka a získané číslo se opět převede na číslo pozice. Je-li žádaná pozice nula, skáče se hned na rutinu pro najetí na optosenzor, jinak se jede na zadanou pozici.
Tlačítka se načítají pomocí opakovaného čtení stavu vstupu pro potlačení zákmitů (pollButtons).

No a to je vše. Nejsem programátor, tohle je můj první program po 30 letech, tak tam určitě najdete spoustu chyb, ale funguje to.

Poděkovat musím zejména Fuldovi za nakopnutí, že jsem se do toho vůbec pustil a za nasměrování kde hledat. Pak už to bylo jen o zavaření vlastní hlavy.
Využil jsem i funkce pro volání tlačítek od Michaela Hardwicka a pak příklady z knihoven.
Naopak, funkci nouzového stopu při užití knihovny Accelstepper pokud vím nikdo nikde nepopisuje, tak jsem možná první.

Kód pro Arduino:

 // Model railroad traverser control code for Arduino
 // Version 1.1
 //
 // Features:
 // - Manual control buttons
 // - Acceleration and deceleration using the Accelstepper library
 // - Emergency stop function 
 // - DCC interface using NMRA library 
 //
 // (c) Jiri Zlamal, e-mail: jiri@zlamal.cz
 // January 2021
 // Creative Commons License BY-NC
 // For commercial usage, contact the author.
 
 #include <AccelStepper.h>                          // Including the Accellstepper library
 #include <NmraDcc.h>                               // Including the NMRA library
 
                                                    // Input/output pins used:
 const int DIR_PIN = 4;                             // Direction pin for A4988 driver
 const int STEP_PIN = 5;                            // Step control pin for A4988 driver
 const int MS1_PIN = 8;                             // Microstepping control pin for A4988 driver (for future use)
 const int MS2_PIN = 7;                             // Microstepping control pin for A4988 driver (for future use)
 const int MS3_PIN = 6;                             // Microstepping control pin for A4988 driver (for future use)
 const int ENABLE_PIN = 9;                          // Enable pin for A4988 driver
 const int SENSOR_PIN = 10;                         // Limit switch - HOME position, active in HIGH state
 const int BUTTON_PIN[4] = {A0, A1, A2, A3};        // Buttons: POS0 (Home), POS1, POS2, POS3 
 const int STOP_PIN = A4;                           // Emergency switch - stops the motor if connected to GND (low)

                                                    // Values:
 const int TOTAL_POS = 3;                           // Total amount of working positions used;                                                   
 const int POS[4] = {-25000, 1000, 11133, 21266};   // Traverser positions in steps (microsteps), to be calculated manually
 const int MAX_SPEED = 2400;                        // Set the standard full speed of the traverser
 const int SPEED = 1200;                            // Set the speed for slow approach to the sensor
 const int ACCELERATION = 800;                      // Set the acceleration in steps/second^2
 int MS_MODE = 8;                                   // Default microstepping mode, fot future development
 int STOP_ACTIVE = HIGH;                            // Emergency stop input 
 const int BASE_DCC_ADDR = 200;                     // Base DCC address = position 0 address
 int posNumber = 0;                                 // Initial position;
 
                                                    // Set up the AccelStepper object for the A4988 Stepper Motor Driver
 AccelStepper stepper = AccelStepper(motorInterfaceType, STEP_PIN, DIR_PIN);
 
 #define motorInterfaceType 1                       // Definition of how is the motor controlled, must be "1" if a driver is used

 
 NmraDcc  Dcc ;                                     // Library initialisation

 void setup() {                                     // Set up the parameters:
 stepper.setMaxSpeed(MAX_SPEED);                    // Stepper motor driver max speed (normal traverser speed)
 stepper.setAcceleration(ACCELERATION);             // Stepper moror driver acceleration
 
 Serial.begin(9600);                                // Enable serial communication for debugging
 Serial.println("Begin setup...");
 pinMode(SENSOR_PIN, INPUT);                        // Set up the digital input and output pins...
 pinMode(ENABLE_PIN, OUTPUT);
 pinMode(STEP_PIN, OUTPUT);
 pinMode(DIR_PIN, OUTPUT);
 pinMode(MS1_PIN, OUTPUT);
 pinMode(MS1_PIN, OUTPUT);
 pinMode(MS1_PIN, OUTPUT);
 pinMode(STOP_PIN, INPUT_PULLUP);                  
  
 for (int i=0; i<(TOTAL_POS +1); i++) {             // Set up the analogue push button pins using pullup resistors
 pinMode(BUTTON_PIN[i], INPUT_PULLUP);
 }                                                  // End of the pushbutton loop
 
 Serial.println("NMRA DCC initializing...");
 Dcc.pin(0, 2, 1);                                  // Set up which External Interrupt, the Pin it's associated with that we're using and enable the Pull-Up
 Dcc.init( MAN_ID_DIY, 10, CV29_ACCESSORY_DECODER | CV29_OUTPUT_ADDRESS_MODE, 0 );   // Call the main DCC Init function to enable the DCC Receiver
 Serial.println("Init Done");

 //moveToHome();                                    // Move the traverser to the home position after rebooting/powering up.
                                                    // Uncomment this function if it is required to move the traverser home after rebooting
 }                                                  // End of the Setup void


                                                    // This function is called whenever a normal DCC Turnout Packet is received
 void notifyDccAccTurnoutOutput( uint16_t Addr, uint8_t Direction, uint8_t OutputPower ) 
 {
 Serial.print("notifyDccAccTurnoutOutput: ") ;      // Print the received packet values...
 Serial.print(Addr,DEC) ;
 Serial.print(',');
 Serial.print(Direction,DEC) ;
 Serial.print(',');
 Serial.println(OutputPower, HEX) ;

 posNumber = Addr - BASE_DCC_ADDR;                  // Get the position number from the received address
 if (posNumber == 0) {                              // Check if the required position is 0 (Home)...  
  moveToHome();                                     // Call the moveToHome function to move to the HOME position.
  }                                      
 else if 
 ((posNumber > 0)&&(posNumber < (TOTAL_POS +1))) {  // Check if the posNumber is within the allowed range to control the traverser
  moveToPosition (posNumber);                       // Call the moveToPosition function to move to the selected track.
  }
 }                                                  // End of void ==================================================

 

 int pollButtons() {                                // Function by Michael Hardwick
                                                    // To debounce the button input signals by repeated reading of their status
 for (int i=0; i<4; i++) {                          // Cycle through the buttons
 if (!digitalRead(BUTTON_PIN[i])) {                 // Look closer if the button appears pushed
 int counter = 0;                                   // If yes, set a counter to zero
 while (!digitalRead(BUTTON_PIN[i])) {              // Check the button every 10 ms for 100 ms
 if (counter == 10){                                // If the button was checked 10 times, it is pressed
 return i;                                          // So return the button index
 }
 counter++;                                         // Increment the counter  how many times was the button checked
 delay(10);                                         // Delay 10 milliseconds
 }
 }
 }                                                  // End of button loop
 return -1;                                         // No button was pressed, so return -1. Necessary for correct operation!
 }                                                  // End of void ==================================================


 void moveToHome() {                                // This routine moves the traverser to the home position in two phases:
 
 digitalWrite (ENABLE_PIN, LOW);                    // Enable the stepper motor
 if(!digitalRead(SENSOR_PIN)) {                     // If the limit sensor is not active (i.e. the traverser is not at home)...
 stepper.moveTo (POS[0]);                           // Move to a distant position behind the sensor
 while(!digitalRead(SENSOR_PIN)) {                  // While the limit switch (sensor) is not reached...
  if (digitalRead(STOP_PIN)) {                      // Check for the Emergency Stop signal (button or command) after each step
   Serial.println("EMERGENCY STOP");                // Print a message if the STOP function is activated
   stepper.stop ();                                 // Stop the motor...
   stepper.setCurrentPosition(stepper.currentPosition ()); // immediately by setting the current position as a target position
   digitalWrite (ENABLE_PIN, HIGH);                 // Disable the stepper motor driver
  return;                                           // Exit the loop if the stop function is activated       
 } 
 stepper.run();                                     // Run the motor until the sensor is reached...
 }
 stepper.stop ();                                   // Then stop the traverser...
 stepper.setCurrentPosition(0);                     // and reset the stepper motor position counter to zero
 }
 stepper.moveTo (400);                              // Move a bit back... (This is done anyway, even if the traverser is at home, to ensure the position is correct
 stepper.runToPosition();                           // Run there
 stepper.moveTo (-800);                             // And go again towards the sensor
 while(!digitalRead(SENSOR_PIN)) {stepper.run();}   // Run the motor until the sensor is reached again
 stepper.stop ();                                   // Then stop the traverser...
 stepper.setCurrentPosition(0);                     // and reset the stepper motor position counter to zero
 digitalWrite (ENABLE_PIN, HIGH);                   // Disable the stepper motor driver
 Serial.println("Traverser is at HOME position.");  // Print a message 
 }                                                  // End of void ==================================================


 void moveToPosition(int posNumber) {               // This routine moves the traverser into the selected position
  
 Serial.print("Traverser moves to POS ");           // Print out where is the traverser going
 Serial.println(posNumber);                         // The position is defined by the button pressed 
 digitalWrite (ENABLE_PIN, LOW);                    // Enable the motor driver 
 stepper.moveTo (POS[posNumber]);                   // Run to the selected position
  while (stepper.distanceToGo () != 0 ) {           // While the final position is not reached...
   if (digitalRead(STOP_PIN)) {                     // check for the Emergency Stop signal (button or command) after each step
   Serial.println("EMERGENCY STOP");                // Print a message if the STOP function is activated
   stepper.stop ();                                 // Stop the motor...
   stepper.setCurrentPosition(stepper.currentPosition ());  // immediately by setting the current position as a target position
  }                     
 stepper.run ();                                    // If the Emergency stop is not active, run normally to the desired position
 }
 digitalWrite (ENABLE_PIN, HIGH);                   // Disable the stepper motor driver
 Serial.print("Position ");                         // Print a message that the position was reached
 Serial.print (posNumber);
 Serial.println (" reached");  
 }                                                  // End of void ==================================================

                                                    
 void loop() {                                      // Main loop 
  
 Dcc.process();                                     // Call the DCC process
  
                                                    // Poll for button presses. If a button is pressed, go home if the home button is pressed
                                                    // or go to selected position
                                                    
 int posNumber = pollButtons();                     // Returns the button number (0-3)
 if (posNumber == 0) {                              // HOME button was pressed
  Serial.println("Home position selected");         // Print a message 
  moveToHome();                                     // Call the moveToHome function to move to the HOME position.
  }
  else if (posNumber > 0) {                         // Track position button was pressed
  Serial.print("Position ");                        // Print a message which position was selected
  Serial.print(posNumber);
  Serial.println(" selected");
  moveToPosition(posNumber);                        // Call the moveToPosition function to move to the selected track
  }                                                 // End of void ==================================================
 
 }                                                  // End of code

Schéma zapojení DCC vstupu:
Pozor, schéma je ilustrační, je odněkud staženo, na Arduinu využijte vstup D2, kde je vyvedeno přerušení INT0

Zapojení DCC vstupu pro Arduino

Zapojení na zkušební destičce:
Určeno pro začátečníky a pro pokusy, bez DCC, jen na tlačítka:

Zapojení na zkušební desce

Pozn: šedé vodiče mezi Arduinem a driverem A4988 není třeba zapojovat, pokud nepoužijete mikrokrokování (viz výše, program to nevyžaduje, nastavíte to napevno).
Motor zapojte podle toho, jaký máte (viz popis výstupů u destičky driveru).
Driver A4988 vyžaduje kondenzátor cca 100 uF u vstupu napájení pro motor.

Doplním sem ještě schéma celého zařízení včetně vazby na DCC a Stop funkce.

Plánuji vše sestavit na desce plošných spojů jako kompaktní modul pro vestavbu do kolejiště. Deska je již hotová, stačí ji jen osadit.

Budoucí vývoj:

Přesuvna bude v běžném provozu částečně skrytá pohledu návštěvníka i obsluhy a proto bude dobré ji doplnit indikací polohy a provozního stavu někde na viditelném místě. První úvaha směřovala ke třem (čtyřem) LED pro zobrazení pozic 1, 2, 3 a „doraz“, to ale vyžaduje 4 výstupy Arduina a nic víc neumí.

Současná digitální doba ale nabízí mnohem víc – Arduino obsahuje a umí řídit I2C sériový výstup na displej, k tomu stačí dráty jen dva (+ napájení) a možnosti jsou nesrovnatelné. Displej bude maticový 8×8 bodů a zobrazí číslo pozice či stav, co bylo stisknuto a kam se právě jede. Tedy očekávejte novinky!

Video ze zkoušek:

Ovládání pomocí tlačítek, ovládání DCC ovladačem, funkce nouzového zastavení a opakovaný rozjezd, najetí do výchozí pozice pro kalibraci.

Další informace a zdroje:

http://www.honzikovyvlacky.cz/2016/10/12/inteligentni-ovladani-segmentove-tocny-n-a-dalsich-zarizeni-na-modelovem-kolejisti/ – řešení segmentové točny a jejího řízení tlačítky
http://www.fucik.name/masinky/presuvna/ – schéma a bližší popis k výše uvedenému
https://mrrwa.org/2017/12/23/dcc-contro … or-driver/ – DCC točna
https://mrr.trains.com/videos/layout-vi … aging-yard – přesuvna na ovládání tlačítky, tohle jsem upravil pro svůj první pokus
https://mrr.trains.com/-/media/Files/PD … olcode.pdf – kód k přesuvně výše
https://how2electronics.com/control-ste … r-arduino/ – jak řídit motor s plynulým rozjezdem a dojezdem, dobrá záležitost.
https://www.rmweb.co.uk/community/index … duino-uno/ – fórum o stavbě točny, ale trochu se to pak rozvolnilo

Doporučuji si projít řešení a informace na uvedených odkazech, člověk se pak ve věci lépe orientuje.

To je zatím vše, pokud se pustíte do zkoumání a dostanete-li se do nesnází, napište, pokusím se poradit.
Pokud by někdo z výrobců dekodérů chtěl kód či jeho část využít komerčně, je to možné, ale po dohodě a za poplatek.

(c) Jiří Zlámal leden 2021

Zlámalík Autor článku:

Napište první komentář

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *