esp32-馬達編碼器讀取篇

編碼器是用於讀取馬達位置的一種感應器,透過旋轉內部的機械結構即可產生訊號,詳細的工作原理相信網路上有非常多了,本篇集中在實戰的部分...

簡述

編碼器是用於讀取馬達位置的一種感應器,透過旋轉內部的機械結構即可產生訊號,詳細的工作原理相信網路上有非常多了,本篇集中在實戰的部分。

正文

編碼器介紹

編碼器有分兩種,一種是絕對型,一種是增量型,絕對型根據字面上意思就是他讀到的位置是絕對的,而增量型意思是用累加的方式去讀取位置,在同一規格下絕對型通常價格偏高,增量型價格偏低,絕對型解析度通常不會做太大,因為若解析度越大,價格通常是非常昂貴不符合成本,因此絕對型及增量型都會根據不同場合使用。

絕對型增量型
價格偏高偏低
解析度通常不高可以很高

本篇不會展示絕對型編碼器的實戰,因此都會介紹增量型,增量型編碼器又分3種,其中為A相、AB相、ABZ相。

若是只有A相位,只能讀取轉了多少,不能知道正反轉,因此通常都用於只有單一方向時的需求會使用到A相位。

圖一、A相位

若是多了一個B相位可以知道是否正反轉,而AB相位相差了90度。

圖二、AB相位

若是再多一個Z相位可以知道馬達轉一圈的次數,通常Z相位都是拿來歸零或者校正。

圖三、ABZ相位

讀取編碼器數值

接下來知道訊號產生的方式後就可以來寫程式拉,本教學是使用ESP32模組來讀取編碼器,而編碼器是選用EE3020-100-B及HTR-3A-1024A-P,其實不管選用的是甚麼編碼器,原理幾乎都相同,主要是能夠辨識規格,像是EE3020-100-B是5V的AB相編碼器,解析度為100PPR,HTR-3A-1024A-P是5V的ABZ相編碼器,解析度為1024PPR。

解析度是編碼器轉一圈會產生多少一個週期變化,從圖二中可以看到一個週期變化就是經過A相位上緣觸發、B相位上緣觸發、A相位下緣觸發及B相位下緣觸發,因此轉一圈可以說是會觸發到nPPR*4次,因此100PPR可以讀取400個變化,1024PPR可以讀取到4096個變化。

先放完整程式碼

#define LED 2
#define BOOT 0

// 在全域範圍定義互斥鎖
SemaphoreHandle_t xMutex;

TaskHandle_t Task1;

//-----------------------------------編碼器參數
const byte encoderPinA = 26;
const byte encoderPinB = 27;
const byte encoderPinZ = 14;

const int resolution = 100;

volatile int count = 0;
volatile bool ZFlage = false;

int protectedCount = 0;
int previousCount = 0;
int shareCount = 0;
int getCount = 0;

byte x = 0;
//-----------------------------------

long millisTimer = 0;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);

  Serial.print("Task1 running on core ");
  Serial.println(xPortGetCoreID());

  pinMode(LED, OUTPUT);
  pinMode(BOOT, INPUT_PULLUP);

  pinMode(encoderPinA, INPUT_PULLUP);
  pinMode(encoderPinB, INPUT_PULLUP);
  pinMode(encoderPinZ, INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(encoderPinA), isrA, CHANGE);
  attachInterrupt(digitalPinToInterrupt(encoderPinB), isrB, CHANGE);
  attachInterrupt(digitalPinToInterrupt(encoderPinZ), isrZ, RISING);

  digitalWrite(LED, LOW);

  // 初始化互斥鎖
  xMutex = xSemaphoreCreateMutex();

  xTaskCreatePinnedToCore(
    Task1code,   /* Task function. */
    "Task1",     /* name of task. */
    10000,       /* Stack size of task */
    NULL,        /* parameter of the task */
    1,           /* priority of the task */
    &Task1,      /* Task handle to keep track of created task */
    0);          /* pin task to core 0 */
  delay(500);

  millisTimer = millis();
  ZFlage = false;
}

void Task1code( void * pvParameters ) {
  Serial.print("Task1 running on core ");
  Serial.println(xPortGetCoreID());

  Serial.println("等待按鈕");

  while (1)
  {
    if (digitalRead(BOOT) == 0)
    {
      break;
    }
    delay(1);
  }

  while (1)
  {
    if (digitalRead(BOOT) == 1)
    {
      break;
    }
    delay(1);
  }

  Serial.println("初始化完畢");
  while (1) {
    if (millis() - millisTimer >= 10) {
      //讀取編碼器
      if (xSemaphoreTake(xMutex, portMAX_DELAY)) {
        // 存取共享資源...
        getCount = shareCount;
        xSemaphoreGive(xMutex); // 釋放互斥鎖
      }

      Serial.print("getCount:");
      Serial.println(getCount);

      millisTimer = millis();
    }
    vTaskDelay(pdMS_TO_TICKS(1));
  }
}

void loop() {
  // put your main code here, to run repeatedly:
  noInterrupts();

  if (ZFlage)
  {
    if (count < resolution / 2 && count > -resolution / 2)
    {
      count = 0;
    }
    else
    {
      if (count % (resolution * 4) > resolution / 2)
      {
        if (count < 0)
        {
          count = count - ((resolution * 4) + count % (resolution * 4));
        }
        else
        {
          count = count + ((resolution * 4) - count % (resolution * 4));
        }
      }
    }
    ZFlage = false;
  }

  protectedCount = count;

  interrupts();

  if (protectedCount != previousCount) {
    if (xSemaphoreTake(xMutex, portMAX_DELAY)) {
      // 存取共享資源...
      shareCount = protectedCount;
      xSemaphoreGive(xMutex); // 釋放互斥鎖
    }
    //Serial.println(protectedCount);

    if (x == 1)
    {
      digitalWrite(LED, HIGH);
    }
    else if (x == 0)
    {
      digitalWrite(LED, LOW);
    }

    if (x == 0)
      x = 1;
    else
      x = 0;
  }

  previousCount = protectedCount;
}

void isrA() {
  if (digitalRead(encoderPinA) != digitalRead(encoderPinB)) {
    count ++;
  } else {
    count --;
  }
}
void isrB() {
  if (digitalRead(encoderPinA) == digitalRead(encoderPinB)) {
    count ++;
  } else {
    count --;
  }
}
void isrZ() {
  ZFlage = true;
}

在程式中只需要更改ABZ相位的GPIO及解析度PPR。

const byte encoderPinA = 26;
const byte encoderPinB = 27;
const byte encoderPinZ = 14;

const int resolution = 100;

程式架構的邏輯是利用ESP32的雙核心來達到分工作業,其中一個核心單純就接收編碼器中斷,並計算累加及校正,另一個核心則是負責處理未來可能是PID或者其他的應用,目前是只有單純將count print出來。

如果是只有AB相位也可以直接燒入並不需要更改程式碼,只是ZFlage 會無作用而已。

讀取編碼器有兩種方法,一種是當A相位上緣觸發中斷後,判斷B相位是HIGH or LOW,如果是HIGH就+1,如果是LOW就-1,這樣就可以讀取到一個週期的解析度,因為只判斷上緣觸發。如果是偵測下緣觸發中斷則是要將HIGH LOW反過來HIGH就-1,如果是LOW就+1,就會達到一樣的效果。

以上方法是只判斷一個相的變化,這樣解析度會只有原始的一個週期,如果要提升解析度可以用以下邏輯,只要A或B有發生改變,就觸發中斷。

其中當 A 相位發生變化時:

  • 如果 A 相不等於 B 相,數值加 1,反之減 1。

當 B 相位發生變化時:

  • 如果 A 相等於 B 相,數值加 1,反之減 1。

根據此邏輯即可讀取4次的變化,在轉一圈編碼器時count則會是PPR*4。

⚠️注意事項⚠️

如果編碼器是使用5V的話,開發版也是選擇ESP32的話,會需要邏輯轉換器,需要將ESP32的3.3V轉成5V,網路上搜尋Logic Level Converter就可以找到。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *