IBM Developer Day 2018에서 배포한 IoT 뱃지는 ESP32 Devkit을 기반으로 구성되어 있습니다. 배포 당시 설치된 행사용 소프트웨어 대신 마이크로 파이썬이 포팅된 펌웨어를 설치하면 IoT 뱃지를 개발 보드로 활용할 수 있습니다. 이 튜토리얼에서는 마이크로 파이썬의 machine.I2C 모듈을 이용하여 자이로 센서와 통신하고 x,y,z 3축에 대한 기울기 정보를 얻는 방법에 대해 학습합니다.

학습 목표

이 튜토리얼을 마치게 되면 다음과 같은 것을 할 수 있습니다:

  • REPL을 이용하여 IoT Badge에 마이크로 파이썬 코드 실행
  • 마이크로 파이썬 코드로 I2C 통신
  • 마이크로 파이썬 코드와 MPU6050로 X,Y,Z 3축 각도 측정

사전 준비 사항

  1. Developer Day 2018 IoT Badge 펌웨어 설치하기
  2. Developer Day 2018 IoT Badge에 나만의 파이썬 코드 실행하기
  3. IBM Developer Day 2018 IoT Badge
  4. 데이터 통신용 USB 2.0 Micro B Type 케이블 (마이크로 5핀)
  5. J7번 6핀 2.54mm Female 소켓 헤더 연결
  6. GY-521 (MPU6050 Breakout 보드)

소요 시간

이 튜토리얼을 완료하기까지 대략 30분 정도가 소요됩니다.

단계

MPU6050 센서 정보

GY-521 보드는 MPU6050 센서를 바로 사용 할 수 있도록 기본적인 회로가 포함된 Breakout Board 입니다. MPU6050의 I2C 및 SPI 통신을 사용 할 수 있도록 VCC, GND, SCL, SDA, XDA, XCL, AD0, INT 8핀으로 노출되어 있습니다. 본 튜토리얼에서는 I2C 방식으로 통신합니다.

측정 기준은 MPU6050의 물리적 위치를 기준으로 하며 각 축에 대한 정보는 다음과 같습니다.

MPU6050 센서 연결 및 I2C 초기화 하기

I2C 통신을 위한 Pin은 SCLSDA이며 iot badge의 J7에 연결되어 다음과 같이 구성 합니다.

ESP32 MPU6050 Description
VCC VDD v3.3
GND GND Ground
GPIO26 SCL Serial Clock
GPIO25 SDA Serial Data

MPU6050은 8Pin을 사용하지만 J7 커넥터는 6Pin입니다. 서로 맞지 않지만 I2C를 이용하는 VCC, GND, SCL, SDA는 이용할 수 있으므로 나머지 4Pin 부분은 본 튜토리얼에서는 사용하지 않습니다.

ESP32 MPU6050 Description
GPIO33 XDA AUX Data
GPIO27 XCL AUX Clock
N/C AD0 Select Slave Address (1K Pull-down)
N/C INT Interrupt

마이크로 파이썬의 machine.I2C 모듈을 이용하면 간단한 코드로 I2C 통신을 위한 준비가 완료 됩니다.

from machine import I2C, Pin
import time

SCL = Pin(26) # SCL
SDA  = Pin(25) # SDA
i2c = I2C(scl=SCL, sda=SDA)

이제 MPU6050에서 사용하는 Register에 대해 확인합니다. MPU6050이 사용하는 Register는 0x0D 부터 0x75 까지 82개에 달합니다. 본 튜토리얼에서는 실습에 필요한 Register를 선택하여 사용할 것이며, 더 많은 정보가 필요한 경우 MPU6050 Register Map를 참고 하시기 바랍니다.

마이크로 파이썬에서 사용할 수 있도록 Register를 상수로 정의합니다.

MPU6050_CONFIG       = 0x1a
MPU6050_GYRO_CONFIG  = 0x1b
MPU6050_ACCEL_CONFIG = 0x1c
MPU6050_SMPLRT_DIV = 0x19

MPU6050_I2C_MST_STATUS = 0x36
MPU6050_INT_STATUS = 0x3a
MPU6050_ACCEL_XOUT_H = 0x3b
MPU6050_ACCEL_XOUT_L = 0x3c
MPU6050_ACCEL_YOUT_H = 0x3d
MPU6050_ACCEL_YOUT_L = 0x3e
MPU6050_ACCEL_ZOUT_H = 0x3f
MPU6050_ACCEL_ZOUT_L = 0x40
MPU6050_TEMP_OUT_H = 0x41
MPU6050_TEMP_OUT_L = 0x42
MPU6050_GYRO_XOUT_H = 0x43
MPU6050_GYRO_XOUT_L = 0x44
MPU6050_GYRO_YOUT_H = 0x45
MPU6050_GYRO_YOUT_L = 0x46
MPU6050_GYRO_ZOUT_H = 0x47
MPU6050_GYRO_ZOUT_L = 0x48
MPU6050_PWR_MGMT_1 = 0x6b
MPU6050_PWR_MGMT_2 = 0x6c
MPU6050_WHO_AM_I = 0x75

MPU6050_ACCEL_XOUT = 0x3b
MPU6050_ACCEL_YOUT = 0x3d
MPU6050_ACCEL_ZOUT = 0x3f
MPU6050_TEMP_OUT = 0x41
MPU6050_GYRO_XOUT = 0x43
MPU6050_GYRO_YOUT = 0x45
MPU6050_GYRO_ZOUT = 0x47

I2C 데이터 통신

MPU6050센서와 통신은 SPI와 I2C 방식을 선택해서 사용 할 수 있습니다. 본 튜토리얼에서는 I2C를 사용합니다. I2C는 데이터를 주고 받는 입장에서 Master와 Slave로 구분합니다. iot badge가 센서를 제어하므로 Master, MPU6050 센서가 Slave가 됩니다.

I2C 통신을 위해서는 Slave에 대한 주소 정보가 필요한데, MPU6050 센서는 다음과 같이 정해져 있으며 AD0 Pin의 상태 값에 따라 달라집니다.

AD0 Slave Address Hex
HIGH 1101001 0x69
LOW 1101000 0x68

기본적으로 GY-521 보드에서는 AD0 Pin을 1K 저항으로 Pull-down 하고 있으므로 Slave 주소 값은 0x68이 됩니다. iot badge의 J7과 센서 보드를 연결하는 경우 AD0 Pin이 직접 연결되지 않고 노출됩니다. 만약 Slave Address를 0x69로 접근해야 한다면 별도의 Jump Cable을 AD0 Pin에 연결 후 HIGH 값을 입력해야 합니다.

MPU6050이 초기화 되는 경우 PWR_MGMT_1WHO_AM_I Register를 제외하면 모든 Register의 값은 0x00으로 초기화됩니다. 따라서, PWR_MGMT_1의 경우 0x40으로 초기화 되는데 이는 Sleep 모드로 진입되어 있는 것을 나타냅니다. 따라서 처음 리셋 후 Sleep 모드를 해제 하는 것이 첫 번째 명령이 되어야 합니다.

모든 준비가 되었다면 다음과 같은 단계로 MPU6050과 통신합니다.

  • Master에서 Slave로 PWR_MGMT_10x00 전달하여 Wake up
  • Master에서 Slave에 MPU6050_WHO_AM_I 값을 요청하여 0x68을 수신하는지 확인

마이크로 파이썬에서는 I2C 통신 초기화로 얻은 instance의 writeto_mem() 함수를 통해 Slave의 Register에 데이터를 전달하고 readfrom_mem() 함수로 Slave의 Register의 정보를 수신합니다.

이를 이용하여 필요한 명령 시퀀스을 마이크로 파이썬 코드로 구성하면 다음과 같습니다.

sl_addr = 0x68 # slave address
i2c.writeto_mem(sl_addr, MPU6050_PWR_MGMT_1, bytearray([0x00]))
data = i2c.readfrom_mem(sl_addr, MPU6050_WHO_AM_I, 1)[0]
if data != 0x68:
    raise Exception('MPU6050 initialize failure')

만약, MPU6050_ACCEL_XOUT와 같이 High, Low 두 바이트로 나누어져 있는 경우 다음과 같이 2 byte 값을 읽고 이를 16 bits signed 로 변환할 수 있습니다.

import ustruct
data = i2c.readfrom_mem(sl_addr, MPU6050_ACCEL_XOUT_H, 2)
acc_x = ustruct.unpack('>h', data)[0]

사실 MPU6050에서 제공하는 값은 가속도 3개, 온도, 각속도 3개 총 7개의 2 byte 데이터의 주소가 순차적으로 이어집니다. 따라서, 한 번에 통신 할 때 14 바이트 데이터를 읽고 이를 각각 분리해서 사용하는 것이 더 유용합니다.

data = i2c.readfrom_mem(sl_addr, MPU6050_ACCEL_XOUT_H, 14)
acc_x, acc_y, acc_z, tp, gyro_x, gyro_y, gyro_z = ustruct.unpack('>hhhhhhh', data)

읽어 들인 값 중 온도 값은 다음과 같은 변환 수식을 이용해야 합니다.

Temperature in degrees C = (TEMP_OUT Register Value as a signed quantity)/340 + 36.53

이를 파이썬 코드로 반영하면 다음과 같습니다.

def measure():
    data = i2c.readfrom_mem(sl_addr, MPU6050_ACCEL_XOUT_H, 14)
    acc_x, acc_y, acc_z, tp, gyro_x, gyro_y, gyro_z = ustruct.unpack('>hhhhhhh', data)

    # Convert in degrees C
    tp = tp/340 + 36.53

    return (acc_x, acc_y, acc_z, tp, gyro_x, gyro_y, gyro_z)

센서 정보 표시

다음과 같이 measure() 함수를 호출하여 센서 정보를 출력 할 수 있습니다.

while True:
    acc_x, acc_y, acc_z, temp, gyro_x, gyro_y, gyro_z = measure()
    
    print('{},{},{}'.format(acc_x, acc_y, acc_z))
    # print('{},{},{}'.format(gx, gy, gz))
    time.sleep_ms(10)

그리고 이렇게 출력되는 값을 Ardino 프로그램의 Serial Plotter를 이용하면 이를 시계열 차트로 볼 수 있습니다.

센서로 측정한 값이 Analog 값이라 일정한 범위 안에서 값이 흔들리는 것을 볼 수 있습니다. 이를 위해 일정 과거 시점 부터 현재까지의 평균 값을 사용하곤 합니다. 본 튜토리얼에서는 지수 이동 평균(Exponential Moving Average)이라는 방법을 이용합니다. 지수 이동 평균에 대한 수식으로 기간을 N이라 할 때 평활 계수 k2 / (N+1)이 되고, t회차 지수 이동 평균 값 EMA(t)는 Value(t) x k + EMA(t-1) x (1.0 – k) 가 됩니다.

이를 이용하여 N19일 때 다음과 같은 코드를 적용 해 볼 수 있습니다.

def ema(prev, curr):
    # Exponential Moving Average
    # EMA(t) = Value(t)*k + EMA(y) * (1-k)
    # k = 2 / (N+1)
    # k = 0.1 where N = 19
    return curr * 0.1 + prev * 0.9

그리고, 이를 적용하면

ax = 0;ay = 0;az = 0
while True:

    acc_x, acc_y, acc_z, temp, gyro_x, gyro_y, gyro_z = measure()
    
    ax = ema(ax, acc_x)
    ay = ema(ay, acc_y)
    az = ema(az, acc_z)

    print('{},{},{}'.format(ax, ay, az))
    time.sleep_ms(10)

이 경우 그래프는 다음과 같이 좀 더 안정적으로 출력되는 것을 볼 수 있습니다.

가속도계(Accelerometer) 값 측정

위의 그래프를 참고하여 실제 MPU6050의 X, Y, Z 축에 대한 가속도계 값이 어떻게 측정되는지 눈으로 확인 해 볼 수 있습니다. 색상에 따라 각각 다른 축의 정보를 나타냅니다.

색깔
파랑 X
빨강 Y
초록 Z

IDLE 상태의 그림에서 X와 Y는 0에 근접하지만 Z 축은 값이 18,000 정도를 유지하고 있는 것을 볼 수 있습니다. 이는 MPU6050 센서의 가속도계가 지구 중력에 반응하는 것으로 MPU6050의 가속도계 기본 scale 값이 ±2G 이므로 값 18,000 정도는 대략 1G를 의미하는 것을 확인 할 수 있습니다.

X 축 이동

다음 그림은 IDLE 상태에서 X 축을 윗쪽으로 향할 때에 대한 그래프 입니다.

X축을 의미하는 파란색 그래프가 상승하고 상대적으로 Z축(초록)이 낮아지는 것을 볼 수 있습니다.

다음은 X 축을 아랫쪽으로 향할 때 그래프입니다.

X축이 낮아지면서 0에 가까워지면 Z축이 상승하다가 X축이 음수로 전환되어 -1G 값이 되면 다시 0으로 낮아지는 것을 볼 수 있습니다.

Y 축 이동

다음 그림은 IDLE 상태에서 Y 축을 윗쪽으로 향할 때에 대한 그래프 입니다.

Y축을 의미하는 빨간색 그래프가 상승하고 상대적으로 Z축(초록)이 낮아지는 것을 볼 수 있습니다.

다음은 Y 축을 아랫쪽으로 향할 때 그래프입니다.

Z 축 이동

다음 그림은 IDLE 상태에서 Z축을 아래로 향할 때에 대한 그래프 입니다.

Z축이 낮아지면서 -1G에 가까워지면서 X, Y 축도 같이 낮아지는 듯 하지만 Z축이 음수로 전환되어 -1G 값이 되면 다시 0으로 수렵하는 것을 볼 수 있습니다.

자이로스코프(Gyroscope) 값 측정

가속도계와 유사하게 다음과 같은 코드로 테스트를 해 볼 수 있습니다.

gx = 0;gy = 0;gz = 0
while True:

    acc_x, acc_y, acc_z, temp, gyro_x, gyro_y, gyro_z = measure()
    
    gx = ema(gx, gyro_x)
    gy = ema(gy, gyro_y)
    gz = ema(gz, gyro_z)

    print('{},{},{}'.format(gx, gy, gz))
    time.sleep_ms(10)

MPU6050의 자이로스코프 기본 scale 값은 ±250 DPS 입니다. DPS란 Degree per second로서 초당 회전 각을 의미하며 각속도라고 말합니다. Full-scale 값을 적용하면 센서 측정 값 100이 대략 0.76 DPS 정도가 됩니다. 단위 시간당 회전한 각도이므로 지속적으로 회전하고 있는 경우가 아니라면 시간이 지나면 측정 값은 0으로 수렴하게 됩니다. 따라서, 실시간으로 측정한 각속도만으로는 해당 물체가 어느 방향으로 회전하고 있는지 여부만 알 수 있을 뿐 얼마나 회전한 것인지는 알 수 없습니다.

X 축 각속도 측정

X 축을 기준으로 반시계 방향으로 회전 후 다시 시계 방향으로 복귀한 그래프입니다.

16,000 가까이 이동했다가 다시 -16,000 정도로 값이 변했으며 환산하면 +120 DPS에서 -120 DPS 만큼 변동된 것을 알 수 있습니다.

Y 축 각속도 측정

Y 축을 기준으로 반시계 방향으로 회전 후 다시 시계 방향으로 복귀한 그래프입니다.

12,000 가까이 이동했다가 다시 -12,000 정도로 값이 변했으며 환산하면 +91 DPS에서 다시 -91 DPS 만큼 기울어진 것을 알 수 있습니다.

Z 축 각속도 측정

Z 축을 기준으로 반시계 방향으로 회전 후 다시 시계 방향으로 복귀한 그래프입니다.

12,000 가까이 이동했다가 다시 -12,000 정도로 값이 변했으며 환산하면 +91 DPS에서 다시 -91 DPS 만큼 기울어진 것을 알 수 있습니다.

Full scale 반영

센서로 측정한 값이 raw data가 아닌 Full scale 값에 따라 나타날 수 있도록 코드를 변경합니다.

# Full scale range & sensitivity
MPU6050_AFS_SEL = {
    0: (2, 16384),
    1: (4, 8192),
    2: (8, 4096),
    3: (16, 2048),
}

MPU6050_FS_SEL = {
    0: (250, 131),
    1: (500, 65.5),
    2: (1000, 32.8),
    3: (2000, 16.4),
}

def acc_full_scale_range():
    cfg = i2c.readfrom_mem(sl_addr, MPU6050_ACCEL_CONFIG, 1)[0]
    return MPU6050_AFS_SEL.get((cfg >> 3) & 0x03, MPU6050_AFS_SEL[0])[0]

def gyro_full_scale_range():
    cfg = i2c.readfrom_mem(sl_addr, MPU6050_GYRO_CONFIG, 1)[0]
    return MPU6050_FS_SEL.get((cfg >> 3) & 0x03, MPU6050_FS_SEL[0])[0]

afs_range = acc_full_scale_range()
gfs_range = gyro_full_scale_range()

def measure_scaled():
    acc_x, acc_y, acc_z, temp, gyro_x, gyro_y, gyro_z = measure()
    
    # LSB Sensitivity : 16384 LSB/g
    # max: 32767 # 0x7f
    # min: -32768 # 0x80

    acc_x = acc_x * afs_range / 32768
    acc_y = acc_y * afs_range / 32768
    acc_z = acc_z * afs_range / 32768

    gyro_x = gyro_x * gfs_range / 32768
    gyro_y = gyro_y * gfs_range / 32768
    gyro_z = gyro_z * gfs_range / 32768

    return (acc_x, acc_y, acc_z, temp, gyro_x, gyro_y, gyro_z)

이를 Sensitivity를 이용하는 방식으로 다시 정리하면 다음과 같습니다.

def acc_full_scale_sensitivity():
    cfg = i2c.readfrom_mem(sl_addr, MPU6050_ACCEL_CONFIG, 1)[0]
    return MPU6050_AFS_SEL.get((cfg >> 3) & 0x03, MPU6050_AFS_SEL[0])[1]

def gyro_full_scale_sensitivity():
    cfg = i2c.readfrom_mem(sl_addr, MPU6050_GYRO_CONFIG, 1)[0]
    return MPU6050_FS_SEL.get((cfg >> 3) & 0x03, MPU6050_FS_SEL[0])[1]

afs_sensitivity = acc_full_scale_sensitivity()
gfs_sensitivity = gyro_full_scale_sensitivity()

def measure_scaled():
    data = i2c.readfrom_mem(sl_addr, MPU6050_ACCEL_XOUT_H, 14)
    measured = list(ustruct.unpack('>hhhhhhh', data))

    measured[0:3] = [(m * afs_range / 32768) for m in measured[0:3]]
    measured[3] = measured[3]/340 + 36.53 # Convert in degrees C
    measured[4:] = [(m * gfs_range / 32768) for m in measured[4:]]

    return tuple(measured)
gx = 0;gy = 0;gz = 0
while True:

    acc_x, acc_y, acc_z, temp, gyro_x, gyro_y, gyro_z = measure_scaled()
    
    ax = ema(ax, acc_x)
    ay = ema(ay, acc_y)
    az = ema(az, acc_z)

    print('{},{},{}'.format(ax, ay, az))
    time.sleep_ms(10)

Roll & Pitch

축에 대한 가속도와 각속도 값을 알면 기기가 얼만큼 기울어지는지 확인 할 수 있습니다만, 측정 값에 대한 오차 및 방위각 측정에 따른 적분 계산등이 필요하므로 본 튜토리얼에서는 아래와 같이 X, Y 축 기울어짐 정도만 확인 합니다.

import math
RADIANS_TO_DEGREES = 180/math.pi

def calc_acc_angles(acc_x, acc_y, acc_z):
    angle_x = math.atan(acc_x / math.sqrt(math.pow(acc_y, 2) + math.pow(acc_z, 2))) * RADIANS_TO_DEGREES
    angle_y = math.atan(acc_y / math.sqrt(math.pow(acc_x, 2) + math.pow(acc_z, 2))) * RADIANS_TO_DEGREES
    angle_z = math.atan(math.sqrt(math.pow(acc_x, 2) + math.pow(acc_y, 2)) / acc_z) * RADIANS_TO_DEGREES

    return (angle_x, angle_y, angle_z)

ax = 0;ay = 0;az = 0
while True:

    acc_x, acc_y, acc_z, temp, gyro_x, gyro_y, gyro_z = measure()
    
    ax = ema(ax, acc_x)
    ay = ema(ay, acc_y)
    az = ema(az, acc_z)

    pitch, roll, z = calc_acc_angles(ax, ay, az)

    print('{},{},{}'.format(pitch, roll, z))

    time.sleep_ms(10)

공 굴리기 게임 만들기

uGFX를 이용하여 화면에 간단한 게임을 만들어 보도록 합니다. 하얀색 화면에 빨간 공이 가운데 나타나면 iot badge를 기울여 공이 기울어진 방향으로 이동하도록 합니다. 기울어진 각도에 따라 공의 이동 단위를 변경하여 약간의 가감속 효과를 적용했습니다.

ugfx.init()
ugfx.set_default_font('IBMPlexMono_Bold24')
ugfx.clear()
ugfx.Label(40, 0, 240, 60, text='MPU6050 Demo')

ugfx.set_default_font('IBMPlexMono_Regular24')
l = ugfx.Label(40, 60, 240, 120, text='')

# Wait 3 seconds
for i in range(3):
    l.text('Waits {}'.format('.'*i))
    time.sleep(1)

ugfx.clear()

width = ugfx.width()
height = ugfx.height()

px = width//2
py = height//2
cx = px; cy = py
ax = 0;ay = 0;az = 0
gx = 0;gy = 0;gz = 0
while True:

    acc_x, acc_y, acc_z, temp, gyro_x, gyro_y, gyro_z = measure_scaled()
    
    ax = ema(ax, acc_x)
    ay = ema(ay, acc_y)
    az = ema(az, acc_z)

    gx = ema(gx, gyro_x)
    gy = ema(gy, gyro_y)
    gz = ema(gz, gyro_z)

    #print('{},{},{}'.format(ax, ay, az))
    # print('{},{},{}'.format(gx, gy, gz))
    # print('{},{},{},{},{},{}'.format(ax, ay, az, gx, gy, gz))

    pitch, roll, z = calc_acc_angles(ax, ay, az)
    pitch = int(pitch)
    roll = int(roll)
    
    # Move ball
    cx += roll//5
    cy += pitch//5
    
    # Border
    if cx < 0:
        cx = 0
    if cy < 0:
        cy = 0
    if cx >= width:
        cx = width-1
    if cy >= height:
        cy = height-1

    # Erase
    if px != cx or py != cy:
        #ugfx.pixel(px, py, ugfx.WHITE)
        #ugfx.area(px-5, py-5, 10, 10, ugfx.WHITE)
        ugfx.fill_circle(px, py, 5, ugfx.WHITE)

    # Draw
    #ugfx.pixel(cx, cy, ugfx.BLACK)
    #ugfx.area(cx-5, cy-5, 10, 10, ugfx.RED)
    ugfx.fill_circle(cx, cy, 5, ugfx.RED)

    # Renew
    px = cx; py = cy

iot badge에서 실행 중인 모습은 다음과 같습니다.

맺음말

이 튜토리얼에서는 IBM Developer Day 2018 IoT 뱃지와 MPU6050 센서 연동 방법에 대해 알아보았습니다.