簡述
編碼器是用於讀取馬達位置的一種感應器,透過旋轉內部的機械結構即可產生訊號,詳細的工作原理相信網路上有非常多了,本篇集中在實戰的部分。
正文
編碼器介紹
編碼器有分兩種,一種是絕對型,一種是增量型,絕對型根據字面上意思就是他讀到的位置是絕對的,而增量型意思是用累加的方式去讀取位置,在同一規格下絕對型通常價格偏高,增量型價格偏低,絕對型解析度通常不會做太大,因為若解析度越大,價格通常是非常昂貴不符合成本,因此絕對型及增量型都會根據不同場合使用。
絕對型 | 增量型 | |
價格 | 偏高 | 偏低 |
解析度 | 通常不高 | 可以很高 |
本篇不會展示絕對型編碼器的實戰,因此都會介紹增量型,增量型編碼器又分3種,其中為A相、AB相、ABZ相。
若是只有A相位,只能讀取轉了多少,不能知道正反轉,因此通常都用於只有單一方向時的需求會使用到A相位。

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

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

讀取編碼器數值
接下來知道訊號產生的方式後就可以來寫程式拉,本教學是使用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就可以找到。