Arduino – Direkt Port I/O

Detta är inte ett labb i den bemärkelsen att vi skall ansluta externa komponenter utan mer som information om hur man kan snabba upp I/O hantering genom att inte använda de interna digitalWrite() funktionerna i Arduino. Koden blir inte lätt porterbar mellan de olika Arduinokorten som finns (t.ex. mellan kort som använder Mega328P och Mega2560), men har man behov av att uppdatera en I/O pinne riktigt snabbt (som vid t.ex. SPI eller annan liknande seriell kommunikation) eller om man behövre uppdatera flera I/O pinnar samtidigt. Till hjälp var vi en logik analysator som kan visa grafiskt vilken digital nivå en I/O pinne har och mäta tider. I vårt fall använde vi oss utav Logic från Saleae, den har 8 kanaler (finns även en version med 16 kanaler). Det finns många olika märken på marknaden och de flesta kan mycket mer än att bara mäta tider och nivåer på signaler, de kan även analysera ett bitmönster och tolka t.ex. SPI, I2C, RS232, CAN och mycket mer (som vår Logic som vi använder kan). Förr var dessa logikanalysatorer mycket dyra men idag har de kommit ner på ett mycket acceptabelt pris.

Vi skall ansluta logikanalysatorn till I/O pinne 2 på en Arduino Uno med ett mycket enkelt program som bara initierar upp I/O pinne 2 som utgång i setup()

void setup() {
// Pin 2 as output to Logic Analyser
   pinMode(2, OUTPUT);
}

Sedan i loop() kommer vi att “toggla” (skifta) I/O pinne 2 så snabbt vi bara kan utan fördröjningar.

void loop() {
// Toggle pin 2 as fast as you can with Arduino built in functions.
 digitalWrite(2, HIGH);
 digitalWrite(2, LOW);
}

När vi kör ovanstående program och mäter med hjälp utav logikanalysatorn så får vi fram nedanstående information. Vi ser tydligt en fyrkantsvåg där en I/O pinne 2 är hög en tid och låg en annan tid. Den är inte helt 50% i förhållandet hög/låg utan den är cirka 46% hög, detta kanske man kan tycka är konstigt, men det beror på hur funktionen digitalWrite() fungerar samt att vi loopar runt i funktionen loop(), den tar slut och lämnar över till var den kom ifrån i Arduino och anropas igen, dvs. den fungerar inte som en ren main() som i ett C/C++ program. Men vi noterar att tiden för att den skall vara hög och sedan låg (dvs. periodtiden T1-T2) är 8,46uS.

Nu byter vi ut de interna funktionerna mot direkt port access och det finns information på Arduino’s hemsida om hur detta fungerar mer i detalj. Programmet nedan i loop() finns nedan och för att sätta en I/O pinne hög gör vi en logisk ELLER med den aktuella I/O porten och sätter den aktuella I/O biten till en logisk etta (boolesk algebra), då blir den hög oavsätt vad den hade för värde innan. För att sätta I/O pinnen låg gör vi en bitvis OCH med det aktuella värdet på porten, dvs. genom att läsa värdet på porten och maska den aktuella I/O biten med en logisk nolla, då blir den biten 0 och andra oförändrade. Man kan även skriva värden hexadecimalt om man föredrar detta, men vi visar binärt för att visa att man manipulerar en bit (bit 2 om man utgår från bit 0-7 i en byte).

void loop() {
// Toggle pin 2 as fast as you can with direct I/O access.
  PORTD = PORTD | B00000100; // Set I/O pin 2 HIGH (bitwise OR)
  PORTD = PORTD & B11111011;  // Set I/O pin 2 LOW (bitwise AND)
}

När vi nu kör programmet igen får vi nedanstående fyrkantspuls på utgången och här ser man mycket tydligare att tiden för att den är låg är mycket längre än den är hög, den är endast hög 12,5% av periodtiden. Anledningen är som innan att det tar tid att återlämna från loop() till var vi kom ifrån när vi startade upp (bakom kulisserna i Arduino) och till att loop() anropades igen. Vill man inte ha detta kan man skapa sin egen evighetsloop med t.ex. while(1){…} i loop(). Vi noterar att vi nu har en periodtid på exakt 1uS vilket är en ren tillfällighet. Men vi får en frekvens som är cirka 8.5ggr mer genom att göra så här.

För att eliminera tiden från det att loop() avslutas och returnerar till main() och anropas igen så tar vi och testar detta lilla program som togglar I/O pinne 2 på samma sätt som innan men 5ggr samt gör en fördröjning på 1mS innan den börjar om igen.

void loop() {
// Toggle pin 2 as fast as you can with direct I/O access.
  PORTD = PORTD | B00000100;
  PORTD = PORTD & B11111011;
  PORTD = PORTD | B00000100;
  PORTD = PORTD & B11111011;
  PORTD = PORTD | B00000100;
  PORTD = PORTD & B11111011;
  PORTD = PORTD | B00000100;
  PORTD = PORTD & B11111011;
  PORTD = PORTD | B00000100;
  PORTD = PORTD & B11111011;
  delay(1);
}

Som vi kan utläsa från logikanalysatorn nedan ser vi nu ett rent 50% förhållande mellan hög och låg och att periodtiden är 0.25uS, dvs. det går 34ggr snabbare att använda direkt port access än att använda digitalWrite() funktionen. Vilket är en hel del när man vill få iväg ett pulståg ut eller uppdatera t.ex. en LED matris osv. i kristiska situationer.

För att förstå varför loop() tar tid att lämna över innan den anropas igen kan se hur den koden ser ut här (detta från filen main.cpp som finns “harware\arduino\cores\arduino\” katalogen):

int main(void) {
  init();
#if defined(USBCON)
  USB.attach();
#endif
  setup();
  for (;;) {
    loop();
    if (serialEventRun) serialEventRun();
  }
  return 0;
}

Här ser man att setup() anropas en gång och sedan finns det en evighetsloop som anropar loop() och en check mot serieporten. För att se hur digitalWrite() ser ut kan vi titta i filen wiring_digital.c som finns på samma plats och då förstår man att det tar lite tid att exekvera denna.

void digitalWrite(uint8_t pin, uint8_t val) {
  uint8_t timer = digitalPinToTimer(pin);
  uint8_t bit = digitalPinToBitMask(pin);
  uint8_t port = digitalPinToPort(pin);
  volatile uint8_t *out;

  if (port == NOT_A_PIN) return;

  // If the pin that support PWM output, we need to turn it off
  // before doing a digital write.
  if (timer != NOT_ON_TIMER) turnOffPWM(timer);

  out = portOutputRegister(port);

  uint8_t oldSREG = SREG;
  cli();

  if (val == LOW) {
    *out &= ~bit;
  } else {
    *out |= bit;
  }

  SREG = oldSREG;
}

Har man inte behov av snabb I/O hantering bör man använda de funktioner som finns för att lätt kunna portera programmet till andra miljöer, men behöver man hastighet för att få ut info på en pinne eller hantera flera pinnar samtidigt, ja då är direkt port access det enda vettiga att använda.

Återgå till Huvudmenyn