ロータリエンコーダによる角度検出
Systick割り込み(インターバルタイマ割り込み)利用

20140530 coskx
T.SATOH


1. はじめに


ロータリエンコーダ(カウンタ利用)は軸の回転角度を測定するセンサである。ロータ リエンコーダは大別すると,角度をそのまま読むことのできるアブソリュート型と,発生パルスを数えるインクリメント型がある。ここではインクリメント型 ロータリエンコーダの使用について解説する。

図に示すような光学式ロータリエンコーダは回転角を測定するセンサーで,軸が回転するとパルスが発生す る。1回転につき,100パルス程度のものから20000パルス程度の分解能を持つものがある。ロータリエンコーダからのパルス信号はA相B相の2つがあ り,これにより,回転方向も判別することが出来るようになっている。STM32F051はA相B相の信号を受け取り,回転方向を判断し,UPカウント・ DOWNカウントを行なう32ビットカウンタ,16ビットカウンタを持っている。
プログラムでは,初期設定をすればあとは勝手にパルスをカウントしてカウンタに保存しているので,いつで もこのカウンタを読み出せば,角度に比例した値を読み出すことができる。1回転が何パルスに対応するかはエンコーダの仕様書や本体に書いてあり 1000P/R[pulses/revolution]と書いてあったら1回転あたり1000パルスである。



ロータリエンコーダ(インクリメント型光学式エンコーダ)からの信号は次のようになっている。正転時と逆転時ではA相B相の信号の位相が異なる。
STM32F051では位相カウンタとしての動作が出来る。正転時はカウントアップされ,逆転時にはカウントダウンする。
エンコーダ信号の1周期で4つカウントする設定にするので,ロータリエンコーダの1回転でのカウントは,仕様の4倍である。(4逓倍カウント)
すなわち,1000pulses/rev(1回転で1000パルス)仕様のロータリエンコーダでは1回転で4000カウントになる。



ロータリエンコーダからの出力方式はいくつかあるが,使いやすいのはプッシュプル出力(トーテムポール出力)とNPNオープンコレクタ出力である。
プッシュプル出力の場合,信号の0,1は0Vと設定電圧として出力される。(この場合設定電圧はマイコンと同じ電圧にしなければならない)
NPNオープンコレクタ出力の場合,信号の0,1は0Vとハイインピーダンス(電気的に絶縁状態)として出力される。
NPNオープンコレクタ出力のものば,マイコン側でマイコンの電源電圧にプルアップすることで,3V系マイコンでも5V系マイコンでも問題なく使える。
ロータリエンコーダの出力形式と,受け側の入力形式の対応は次のようになる。
ロータリエンコーダの出力形式 受け側の入力形式
プッシュプル出力 プルアップもプルダウンもしないレベル入力
NPNオープンコレクタ出力 プルアップ入力
プルアップするというのは,信号線から10kΩ程度の抵抗を介してマイコンの電源電圧に接続することであるが,マイコンがプルアップ機能を持っていることが多く,この機能を有効にするだけで良い。

2. プログラムの要点

STM32F303ではTIM1,TIM2,・・・と呼ばれる多用途タイマ・カウンタがあり,この内TIM1,TIM2,TIM3,TIM4,TIM8がエンコーダの2相入力をカウントできる。

しかし,ピンがすでに利用されている場合は,TIMxを使ったカウントが出来ない場合がある。そこで,Systick割り込みを使ってロータリエンコーダのA相B相信号を観測してカウントする方法を考える。これならば,高速なSystick割り込みでポート入力をするだけで複数のロータリエンコーダの相手をすることができる。

ということで, PB4とPB5 また PC6とPC7 をA相B相信号とする2つのロータリエンコーダの読み取りを行う。

このピン割り当てに基づいて初期化プログラムを作成する。
初期化においては次の手順になる。
(1)GPIOB,GPIOCにクロックを供給
(2)GPIOのピンをエンコーダ入力用として割り当てる設定
(3)エンコーダ入力用として割り当てられたピンの設定
(4)TIMxへのクロックを供給
(5)TIMxをエンコーダとして設定


3. カウント原理

ロータリエンコーダが正方向に回転した時には次のように2つの信号が変化する。

 時間→
A相01100110
B相00110011

逆方向に回転した時には次のように2つの信号が変化する。

 時間→
A相00110011
B相01100110

高速な割り込みを用いてAB信号を連続観察した時,直前のAB信号と,現在のAB信号を基に,カウンタ値を増やすべきか減らすべきかを考えて表にすると次のようになる。
例えば,前回割り込み時にAB信号が01で現在のAB信号が00ならカウンタ値を1増やすことを示している。
ただし,赤の字のところはあり得ない,または検査周期が長すぎて,データを取り損ねていて+2または-2にすべきところである。

 今回割り込み時のAB信号 

00

01

10

11

前回
割り
込み
時の
AB
信号 

00

0

-1

1

0

01

1

0

0

-1

10

-1

0

0

1

11

0

1

-1

0

前回割り込み時のAB信号をBit3,Bit2,今回割り込み時のAB信号をBit1,Bit0として番号を作ると次のようになる。

AB信号の2進表示

0000000100100011010001010110011110001001101010111100110111011111

AB信号の10進表示

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

カウント増減値

0

-1

1

0

1

0

0

-1

-1

0

0

1

0

1

-1

0


あり得ないかまたは検査周期が長すぎてデータを取り損ねていている組み合わせは,エラーとなるので,エラーを示すテーブルも作っておく。
0は正常,1はエラーを表す。

 今回割り込み時のAB信号 

00

01

10

11

前回
割り
込み
時の
AB
信号 

00

0

0

0

1

01

0

0

1

0

10

0

1

0

0

11

1

0

0

0

前回割り込み時のAB信号をBit3,Bit2,今回割り込み時のAB信号をBit1,Bit0として番号を作ると次のようになる。

AB信号の2進表示

0000000100100011010001010110011110001001101010111100110111011111

AB信号の10進表示

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

カウント増減値

0

0

0

1

0

0

1

0

0

1

0

0

1

0

0

0

この2つの表を用いれば,最新2回のAB信号読み取り値の組み合わせから,カウント増減値とエラーカウント増分値を得ることができる。
エラーカウントが0出ない場合は,割り込み周期が長過ぎるか,ロータリエンコーダの分解能が高すぎるか,ロータリエンコーダが高速に動きすぎということになる。



4. プログラム
最初に示すのは,main関数である。semihostingを使って,カウンタの値を表示するが,printfが表示している間はSystick割り込みが中断するので,
本番では
semihostingを使わないことにして,テストでは,printfが動作していない時のみロータリエンコーダ軸を動作させることにする
エンコーダカウンタ機能を初期化し,カウンタをクリア(カウンタの値を0にする)した後,ループに入り,カウンタ値を読み取る。
入力ピンは
PB4とPB5
および

PC6とPC7
を使う。


main.c
/* Includes ------------------------------------------------------------------*/
#include <stm32f30x.h>
#include <stdint.h>
#include "stm32f30x_conf.h"
#include "stm32f3_discovery.h"
#include <stdio.h>
#include <rotary_encoder_soft.h>

volatile int request=0;

/*インターバルタイマー割り込み初期設定 割り込み周波数[Hz]を引数で渡す*/
void initIntervaltimer(int32_t frequency)
{
    RCC_ClocksTypeDef RCC_Clocks;
    RCC_GetClocksFreq(&RCC_Clocks);
    SysTick_Config(RCC_Clocks.HCLK_Frequency / frequency);
}

int main()
{
    initIntervaltimer(50000);  /* 50000Hz, 50000 interrupts/sec 20usec*/
    //initIntervaltimer(1000);  /* 1000Hz, 1000 interrupts/sec 1msec*/
    //initialize encoder
    Enc1_Init(GPIO_PuPd_UP); //for PB4,PB5
    Enc2_Init(GPIO_PuPd_UP); //for PC6,PC7
    Enc1_Clear();
    Enc2_Clear();
    STM_EVAL_LEDInit(LED3);
    STM_EVAL_LEDInit(LED4);
    printf("Rotary Encoder Counter\r\n");
    printf("Enc1cnt Enc2cnt (Enc1err Enc2err)\r\n");
    while(1) {
        if (request) {
            int cntr1,cntr2,err1,err2;
            cntr1=Enc1_Read();
            cntr2=Enc2_Read();
            err1=Enc1_Read_error();
            err2=Enc2_Read_error();
            printf("%5d %5d (%5d %5d)\n",cntr1,cntr2,err1,err2);
            request=0;
        }
    }
}

//インターバルタイマー割り込み関数
void SysTick_Handler(void)
{
    static int32_t count=0;
    if (request) {
        STM_EVAL_LEDOn(LED3);/*red*/
        STM_EVAL_LEDOff(LED4);/*blue*/
    } else {
        STM_EVAL_LEDOff(LED3);/*red*/
        STM_EVAL_LEDOn(LED4);/*blue*/
    }
    count++;
    if (count==200000) {
    //if (count==4000) {
         count=0;
        request=1;
    }
    Enc1_checkport();
    Enc2_checkport();
}

実行結果例 Enc2のみ接続(PC6とPC7のみ接続)
Rotary Encoder Counter
Enc1cnt Enc2cnt (Enc1err Enc2err)
    0     0 (    0     0)
    0  1788 (    0     0)
    0   601 (    0     0)
    0  -719 (    0     0)
    0  -719 (    0     0)
    0 -1689 (    0     0)
    0 -1689 (    0     0)
    0  -412 (    0     0)
    0  -412 (    0     0)
    0  -412 (    0     0)
    0  -412 (    0     0)



初期化関数,クリア関数,カウント値読み取り関数,エラーカウント値読み取り関数を以下に示す。


rotary_encoder_soft.c
初期化関数,クリア関数,カウント値読み取り関数
Enc1:入力はPB4とPB5
Enc2
:入力はPC6とPC7
/*
 *         File:    rotary_encoder_soft.c
 *
 */

//  Enc1
//  Two phase inputs from the encoder are connected to GPIO B Pin4 ,Pin5.
//
//  Enc2
//  Two phase inputs from the encoder are connected to GPIO C Pin6 ,Pin7.

#include <stm32f30x.h>
#include <stdint.h>
#include "stm32f30x_conf.h"
#include "stm32f3_discovery.h"

#include "rotary_encoder_soft.h"
static int32_t Enc1_count=0;
static int32_t Enc2_count=0;
static int32_t Enc1_err=0;
static int32_t Enc2_err=0;
static int Enc1_status=0;
static int Enc2_status=0;

const static int EncoderIndexTable[]={
    0,-1,1,0,  1,0,0,-1,  -1,0,0,1,  0,1,-1,0
}; /*2回のAB信号によるカウンタの増減表*/
const static int EncoderErrorIndexTable[]={
    0,0,0,1,  0,0,1,0,  0,1,0,0,  1,0,0,0
}; /*2回のAB信号によるカウンタのエラー表*/

void Enc1_Init(GPIOPuPd_TypeDef GPIO_PuPd)
{
    /* GPIOB Periph clock enable */
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE);

    /* Configure PB4 PB5*/
    GPIO_InitTypeDef  GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4|GPIO_Pin_5;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;       //*4
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
}

void Enc2_Init(GPIOPuPd_TypeDef GPIO_PuPd)
{
    /* GPIOC Periph clock enable */
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOC, ENABLE);

    /* Configure PC6 PC7*/
    GPIO_InitTypeDef  GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;       //*4
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

}

void Enc1_checkport(void)
{
    int in,in0;
    in0 = GPIOB->IDR;
    in = (in0>>4)&3;
    Enc1_status = ((Enc1_status<<2)+in)&0xf;
    Enc1_count += EncoderIndexTable[Enc1_status];
    Enc1_err += EncoderErrorIndexTable[Enc1_status];
}

void Enc2_checkport(void)
{
    int in,in0;
    in0 = GPIOC->IDR;
    in = (in0>>6)&3;
    Enc2_status = ((Enc2_status<<2)+in)&0xf;
    Enc2_count += EncoderIndexTable[Enc2_status];
    Enc2_err += EncoderErrorIndexTable[Enc2_status];
}

int32_t Enc1_Read(void)
{
    return Enc1_count;
}

int32_t Enc2_Read(void)
{
    return Enc2_count;
}

int32_t Enc1_Read_error(void)
{
    return Enc1_err;
}

int32_t Enc2_Read_error(void)
{
    return Enc2_err;
}

void Enc1_Clear(void)
{
    Enc1_count = 0;
    Enc1_err = 0;
    Enc1_status = ((GPIOB->IDR)>>4)&3;
}

void Enc2_Clear(void)
{
    Enc2_count = 0;
    Enc2_err = 0;
    Enc2_status = ((GPIOC->IDR)>>6)&3;
}


.hファイルは次のようになる。
rotary_encoder_soft.h
/*
 *         File:    rotary_encoder.h
 *
 *   Enc1_Init(),Enc2_Init()の引数GPIO_PuPdについて
 *   エンコーダ出力がプッシュプル型(トーテムポール型)の場合は PuPd_NOPULL (なにもしない)
 *   エンコーダ出力がオープンコレクタ(オープンドレイン)型の場合は GPIO_PuPd_UP (プルアップする)
 *   を設定する。 Kosaka
 */

#ifndef __ROTARY_ENCODER_H__
#define __ROTARY_ENCODER_H__

#include <stm32f30x.h>

void Enc1_Init(GPIOPuPd_TypeDef GPIO_PuPd);
void Enc2_Init(GPIOPuPd_TypeDef GPIO_PuPd);
void Enc1_checkport(void);
void Enc2_checkport(void);
int32_t Enc1_Read(void);
int32_t Enc2_Read(void);
int32_t Enc1_Read_error(void);
int32_t Enc2_Read_error(void);
void Enc1_Clear(void);
void Enc2_Clear(void);

#endif /* __ROTARY_ENCODER_H__*/