코코아팹은 누구나 창의적 아이디어를 현실로 만들어 낼 수 있도록
만들고, 공유하고, 배울 수 있는 터전이
되고자 합니다.
아이디와 비밀번호를 잊으셨나요?아이디 / 비밀번호 찾기
코코아팹 회원이 아니신가요? 회원가입
2016-02-10 10:54:14
시중 제품 중에 Cube Timer라는 제품이 있습니다. 아래와 같은 사진 모양인데요.
모양은 그냥 각 면에 숫자가 적혀있는 주사위 같이 생겼지만 이 제품은 타이머 알람의 기능을 수행합니다.
별도의 조작이 없이 그냥 윗면의 적혀진 숫자대로 타이머 기능을 수행합니다.
예를 들어 Cube의 윗면이 15min이 적혀있는 면일 경우에는 15분 뒤에 알람이 울리게 됩니다.
인터페이스가 상당히 단순하기 때문에 조작하기 쉽고 Fun한 요소까지 추가되어 있기 때문에 상당히 매력적인 제품입니다.
(국내에서는 Funshop에서 볼 수 있습니다. 링크 : http://www.funshop.co.kr/goods/detail/7014?t=s)
이 제품을 보고 어쩌면 저런 큐브를 통해 LED를 제어해 볼 수 있지 않을까 라는 생각에 이번에는 큐브 모양의 컨트롤러를 만들어 보았습니다.
(사실 만든건 작년 2월에 만들었는데 이제서야 뭐 올릴거 없나 생각하다가 이것이 생각나서 올려봅니다!)
기능은 위에서 보여드린 CubeTimer와 동일합니다. 큐브의 윗면의 색깔대로 LED는 바뀌게 됩니다.
또한 큐브를 제자리에서 돌리게되면 밝기와 LED 픽셀이 점점 켜졌다가 꺼지는 효과도 있습니다.(무지개의 경우에는 LED가 무지개색깔대로 자연스럽게 변화하게 됩니다.)
1. 3축 가속도 센서
2. NeoPixel
3. Zigbee 사용하기
NO | 부품명 | 수량 | 상세설명 |
1 | 오렌지 보드 | 1 | UNO 호환보드 |
2 | Zigbee쉴드 | 2 | |
3 | Zigbee모듈 | 2 | Series1 |
4 | 3축 가속도 센서 | 1 | MPU-6050 |
5 | LED Strip | 1 | Adafruit Noepixel |
6 | 1.5V 배터리 | 4 | |
7 | 5V 어댑터 | 2 | LEDStrip용, 오렌지 보드용 |
8 | 브레드 보드 | 1 | |
9 | 점퍼 케이블 | 약 10개 |
부품명 | 오렌지 보드 | Zigbee쉴드 | Zigbee모듈 | 3축 가속도 센서 | LEDStrip |
파트 | |||||
부품명 | 1.5V 배터리 | 5V어댑터 | 브레드 보드 | 점퍼 케이블 | |
파트 |
내부는 상당히 단순한 편입니다. 오렌지 보드와 1.5v 4개 배터리 홀더, 브레드 보드, 가속도 센서를 사용하여 구성하였습니다.
겉 표면은 아크릴로 주문 제작하여 CUBE를 제작하였습니다.
#include <math.h> // (no semicolon)
#include <Wire.h>
/* MPU-6050 sensor */
#define MPU6050_ACCEL_XOUT_H 0x3B // R
#define MPU6050_PWR_MGMT_1 0x6B // R/W
#define MPU6050_PWR_MGMT_2 0x6C // R/W
#define MPU6050_WHO_AM_I 0x75 // R
#define MPU6050_I2C_ADDRESS 0x68
/* Kalman filter */
struct GyroKalman{
/* These variables represent our state matrix x */
float x_angle, x_bias;
/* Our error covariance matrix */
float P_00, P_01, P_10, P_11;
float Q_angle, Q_gyro;
float R_angle;
};
struct GyroKalman angX;
struct GyroKalman angY;
struct GyroKalman angZ;
static const float R_angle = 0.3; //.3 default
static const float Q_angle = 0.01; //0.01 (Kalman)
static const float Q_gyro = 0.04; //0.04 (Kalman)
//These are the limits of the values I got out of the Nunchuk accelerometers (yours may vary).
const int lowX = -2150;
const int highX = 2210;
const int lowY = -2150;
const int highY = 2210;
const int lowZ = -2150;
const int highZ = 2550;
/* time */
unsigned long prevSensoredTime = 0;
unsigned long curSensoredTime = 0;
typedef union accel_t_gyro_union
{
struct
{
uint8_t x_accel_h;
uint8_t x_accel_l;
uint8_t y_accel_h;
uint8_t y_accel_l;
uint8_t z_accel_h;
uint8_t z_accel_l;
uint8_t t_h;
uint8_t t_l;
uint8_t x_gyro_h;
uint8_t x_gyro_l;
uint8_t y_gyro_h;
uint8_t y_gyro_l;
uint8_t z_gyro_h;
uint8_t z_gyro_l;
}
reg;
struct
{
int x_accel;
int y_accel;
int z_accel;
int temperature;
int x_gyro;
int y_gyro;
int z_gyro;
}
value;
};
int xInit[5] = {
0,0,0,0,0};
int yInit[5] = {
0,0,0,0,0};
int zInit[5] = {
0,0,0,0,0};
int initIndex = 0;
int initSize = 5;
int xCal = 0;
int yCal = 0;
int zCal = 1800;
int cnt = 0;
int valGx, valGy, valGz;
char transChar;
char volume;
void setup()
{
int error;
uint8_t c;
initGyroKalman(&angX, Q_angle, Q_gyro, R_angle);
initGyroKalman(&angY, Q_angle, Q_gyro, R_angle);
initGyroKalman(&angZ, Q_angle, Q_gyro, R_angle);
Serial.begin(9600);
Wire.begin();
// default at power-up:
// Gyro at 250 degrees second
// Acceleration at 2g
// Clock source at internal 8MHz
// The device is in sleep mode.
//
error = MPU6050_read (MPU6050_WHO_AM_I, &c, 1);
Serial.print(F("WHO_AM_I : "));
Serial.print(c,HEX);
Serial.print(F(", error = "));
Serial.println(error,DEC);
// According to the datasheet, the 'sleep' bit
// should read a '1'. But I read a '0'.
// That bit has to be cleared, since the sensor
// is in sleep mode at power-up. Even if the
// bit reads '0'.
error = MPU6050_read (MPU6050_PWR_MGMT_2, &c, 1);
Serial.print(F("PWR_MGMT_2 : "));
Serial.print(c,HEX);
Serial.print(F(", error = "));
Serial.println(error,DEC);
// Clear the 'sleep' bit to start the sensor.
MPU6050_write_reg (MPU6050_PWR_MGMT_1, 0);
}
void loop()
{
int error;
double dT;
accel_t_gyro_union accel_t_gyro;
curSensoredTime = millis();
error = MPU6050_read (MPU6050_ACCEL_XOUT_H, (uint8_t *) &accel_t_gyro, sizeof(accel_t_gyro));
if(error != 0) {
Serial.print(F("Read accel, temp and gyro, error = "));
Serial.println(error,DEC);
}
// Swap all high and low bytes.
// After this, the registers values are swapped,
// so the structure name like x_accel_l does no
// longer contain the lower byte.
uint8_t swap;
#define SWAP(x,y) swap = x; x = y; y = swap
SWAP (accel_t_gyro.reg.x_accel_h, accel_t_gyro.reg.x_accel_l);
SWAP (accel_t_gyro.reg.y_accel_h, accel_t_gyro.reg.y_accel_l);
SWAP (accel_t_gyro.reg.z_accel_h, accel_t_gyro.reg.z_accel_l);
SWAP (accel_t_gyro.reg.t_h, accel_t_gyro.reg.t_l);
SWAP (accel_t_gyro.reg.x_gyro_h, accel_t_gyro.reg.x_gyro_l);
SWAP (accel_t_gyro.reg.y_gyro_h, accel_t_gyro.reg.y_gyro_l);
SWAP (accel_t_gyro.reg.z_gyro_h, accel_t_gyro.reg.z_gyro_l);
Serial.print(F("hh : "));
Serial.print(accel_t_gyro.value.x_gyro, DEC);
Serial.print(F(", "));
Serial.print(accel_t_gyro.value.y_gyro, DEC);
Serial.print(F(", "));
Serial.print(accel_t_gyro.value.z_gyro, DEC);
Serial.println(F(""));
if(prevSensoredTime > 0) {
int gx1=0, gy1=0, gz1 = 0;
float gx2=0, gy2=0, gz2 = 0;
int loopTime = curSensoredTime - prevSensoredTime;
gx2 = angleInDegrees(lowX, highX, accel_t_gyro.value.x_gyro);
gy2 = angleInDegrees(lowY, highY, accel_t_gyro.value.y_gyro);
gz2 = angleInDegrees(lowZ, highZ, accel_t_gyro.value.z_gyro);
predict(&angX, gx2, loopTime);
predict(&angY, gy2, loopTime);
predict(&angZ, gz2, loopTime);
gx1 = update(&angX, accel_t_gyro.value.x_accel) / 10;
gy1 = update(&angY, accel_t_gyro.value.y_accel) / 10;
gz1 = update(&angZ, accel_t_gyro.value.z_accel) / 10;
/////////////////////////////////////////////////////////////////////////////
// ���� ����� �� �����Ǵ� n���� ���� ��� => ���� �����Ǵ� ���� ����
/////////////////////////////////////////////////////////////////////////////
if(initIndex < initSize) {
xInit[initIndex] = gx1;
yInit[initIndex] = gy1;
zInit[initIndex] = gz1;
if(initIndex == initSize - 1) {
int sumX = 0;
int sumY = 0;
int sumZ = 0;
for(int k=1; k <= initSize; k++) {
sumX += xInit[k];
sumY += yInit[k];
sumZ += zInit[k];
}
xCal -= sumX/(initSize -1);
yCal -= sumY/(initSize -1);
zCal = (sumZ/(initSize -1) - zCal);
}
initIndex++;
}
/////////////////////////////////////////////////////////////////////////////
// �������� ��� �ʿ��� �۾��� ó���ϴ� ��ƾ
/////////////////////////////////////////////////////////////////////////////
else {
// ������ ����
gx1 += xCal;
gy1 += yCal;
}
if(cnt < 10) {
cnt++;
}
else if(cnt == 10) {
valGx = gx1;
valGy = gy1;
valGz = gz1;
Serial.print("eeeee : ");
Serial.print(valGx);
Serial.print(F(", "));
Serial.print(valGy);
Serial.print(F(", "));
Serial.print(valGz);
Serial.println(F(""));
cnt = 100;
}
Serial.print(gx1);
Serial.print(F(", "));
Serial.print(gy1);
Serial.print(F(", "));
Serial.print(gz1);
Serial.println(F(""));
if(abs(valGx-gx1) < 500 && abs(valGy-gy1) < 500 && abs(valGz-gz1) < 500) {
transChar = 'a';
Serial.print(transChar);
}
else if(abs(valGx-gx1-1500) < 500 && abs(valGy-gy1-1500) < 500 && abs(valGz-gz1) < 500) {
transChar = 'r';
Serial.print(transChar);
if(accel_t_gyro.value.y_gyro > 3000) {
volume = 'u';
Serial.print(volume);
}
else if(accel_t_gyro.value.y_gyro < -3000) {
volume = 'd';
Serial.print(volume);
}
else {
volume = 'n';
Serial.print(volume);
}
}
else if(abs(valGx-gx1-1500) < 500 && abs(valGy-gy1+1500) < 500 && abs(valGz-gz1) < 500) {
transChar = 'g';
Serial.print(transChar);
if(accel_t_gyro.value.y_gyro < -3000) {
volume = 'u';
Serial.print(volume);
}
else if(accel_t_gyro.value.y_gyro > 3000) {
volume = 'd';
Serial.print(volume);
}
else {
volume = 'n';
Serial.print(volume);
}
}
else if(abs(valGx-gx1-3000) < 500 && abs(valGy-gy1) < 500 && abs(valGz-gz1) < 500) {
transChar = 'b';
Serial.print(transChar);
if(accel_t_gyro.value.x_gyro > 3000) {
volume = 'u';
Serial.print(volume);
}
else if(accel_t_gyro.value.x_gyro < -3000) {
volume = 'd';
Serial.print(volume);
}
else {
volume = 'n';
Serial.print(volume);
}
}
else if(abs(valGx-gx1-1500) < 500 && abs(valGy-gy1) < 500 && abs(valGz-gz1+1500) < 500) {
transChar = 'x';
Serial.print(transChar);
if(accel_t_gyro.value.z_gyro < -3000) {
volume = 'u';
Serial.print(volume);
}
else if(accel_t_gyro.value.z_gyro > 3000) {
volume = 'd';
Serial.print(volume);
}
else {
volume = 'n';
Serial.print(volume);
}
}
else if(abs(valGx-gx1-1500) < 500 && abs(valGy-gy1) < 500 && abs(valGz-gz1-1500) < 500) {
transChar = 'y';
Serial.print(transChar);
}
else if(abs(accel_t_gyro.value.x_gyro) > 5000 && abs(accel_t_gyro.value.y_gyro) > 5000 && abs(accel_t_gyro.value.z_gyro) > 5000) {
transChar = 'z';
Serial.print(transChar);
}
}
prevSensoredTime = curSensoredTime;
delay(300);
} // End of loop()
/**************************************************
* Sensor read/write
**************************************************/
int MPU6050_read(int start, uint8_t *buffer, int size) {
int i, n, error;
Wire.beginTransmission(MPU6050_I2C_ADDRESS);
n = Wire.write(start);
if (n != 1)
return (-10);
n = Wire.endTransmission(false); // hold the I2C-bus
if (n != 0)
return (n);
// Third parameter is true: relase I2C-bus after data is read.
Wire.requestFrom(MPU6050_I2C_ADDRESS, size, true);
i = 0;
while(Wire.available() && i<size) {
buffer[i++]=Wire.read();
}
if ( i != size)
return (-11);
return (0); // return : no error
}
int MPU6050_write(int start, const uint8_t *pData, int size) {
int n, error;
Wire.beginTransmission(MPU6050_I2C_ADDRESS);
n = Wire.write(start); // write the start address
if (n != 1)
return (-20);
n = Wire.write(pData, size); // write data bytes
if (n != size)
return (-21);
error = Wire.endTransmission(true); // release the I2C-bus
if (error != 0)
return (error);
return (0); // return : no error
}
int MPU6050_write_reg(int reg, uint8_t data) {
int error;
error = MPU6050_write(reg, &data, 1);
return (error);
}
/**************************************************
* Raw data processing
**************************************************/
float angleInDegrees(int lo, int hi, int measured) {
float x = (hi - lo)/180.0;
return (float)measured/x;
}
void initGyroKalman(struct GyroKalman *kalman, const float Q_angle, const float Q_gyro, const float R_angle) {
kalman->Q_angle = Q_angle;
kalman->Q_gyro = Q_gyro;
kalman->R_angle = R_angle;
kalman->P_00 = 0;
kalman->P_01 = 0;
kalman->P_10 = 0;
kalman->P_11 = 0;
}
/*
* The kalman predict method.
* kalman the kalman data structure
* dotAngle Derivitive Of The (D O T) Angle. This is the change in the angle from the gyro.
* This is the value from the Wii MotionPlus, scaled to fast/slow.
* dt the change in time, in seconds; in other words the amount of time it took to sweep dotAngle
*/
void predict(struct GyroKalman *kalman, float dotAngle, float dt) {
kalman->x_angle += dt * (dotAngle - kalman->x_bias);
kalman->P_00 += -1 * dt * (kalman->P_10 + kalman->P_01) + dt*dt * kalman->P_11 + kalman->Q_angle;
kalman->P_01 += -1 * dt * kalman->P_11;
kalman->P_10 += -1 * dt * kalman->P_11;
kalman->P_11 += kalman->Q_gyro;
}
/*
* The kalman update method
* kalman the kalman data structure
* angle_m the angle observed from the Wii Nunchuk accelerometer, in radians
*/
float update(struct GyroKalman *kalman, float angle_m) {
const float y = angle_m - kalman->x_angle;
const float S = kalman->P_00 + kalman->R_angle;
const float K_0 = kalman->P_00 / S;
const float K_1 = kalman->P_10 / S;
kalman->x_angle += K_0 * y;
kalman->x_bias += K_1 * y;
kalman->P_00 -= K_0 * kalman->P_00;
kalman->P_01 -= K_0 * kalman->P_01;
kalman->P_10 -= K_1 * kalman->P_00;
kalman->P_11 -= K_1 * kalman->P_01;
return kalman->x_angle;
}
#include <Adafruit_NeoPixel.h>
#define PIN 6
Adafruit_NeoPixel strip = Adafruit_NeoPixel(30, PIN, NEO_GRB + NEO_KHZ800);
char temp;
int rainbowChange = 255;
int bright = 255;
int index;
char ch;
char volumeChar;
char color;
uint16_t numPixels = strip.numPixels();
uint16_t currentOnPixels;
void setup() {
Serial.begin(9600);
strip.begin();
strip.show(); // Initialize all pixels to 'off'
}
void loop() {
if(Serial.available()) {
ch = Serial.read();
Mode(ch, numPixels, strip.Color(0, 0, 0), strip.Color(255, 0, 0), strip.Color(0, 255, 0), strip.Color(0, 0, 255), strip.Color(255, 255, 0), strip.Color(127, 127, 127));
}
}
void volumeUpDown(char volumeChar) {
if(volumeChar == 'u' && numPixels < strip.numPixels()) {
numPixels += 4;
if(temp == 'r') {
colorWipe(strip.Color(255, 0, 0), 1, numPixels);
}
else if(temp =='b') {
colorWipe(strip.Color(0, 0, 255), 1, numPixels);
}
}
else if(volumeChar == 'd' && numPixels-6 > 0) {
numPixels -= 4;
colorDelete(strip.Color(0, 0, 0), 1, numPixels);
}
}
void brightnessUpDown(char brtChar) {
if(brtChar == 'u' && bright <= 225) {
bright+=30;
colorWipe(strip.Color(0, bright, 0), 1, numPixels);
}
else if(brtChar == 'd' && bright > 40) {
bright -= 30;
colorWipe(strip.Color(0, bright, 0), 1, numPixels);
}
}
void rainbowUpDown(char rainbowChar) {
if(rainbowChar == 'u') {
rainbowChange += 20;
rainbowChange %= 255;
rainbow(2, numPixels, rainbowChange);
}
else if(rainbowChar == 'd' && rainbowChange > 25) {
rainbowChange -= 20;
rainbowChange %= 255;
rainbow(2, numPixels, rainbowChange);
}
}
// Fill the dots one after the other with a color
void colorWipe(uint32_t c, uint8_t wait, uint16_t numPixels) {
for(uint16_t i=0; i<=numPixels; i++) {
strip.setPixelColor(i, c);
strip.show();
delay(wait);
}
}
void colorDelete(uint32_t c, uint8_t wait, uint16_t numPixels) {
for(uint16_t i=strip.numPixels(); i>numPixels; i--) {
strip.setPixelColor(i, c);
strip.show();
delay(wait);
}
}
void rainbow(uint8_t wait, uint16_t numPixels,uint16_t rainbowChange) {
uint16_t i;
for(i=0; i<=numPixels; i++) {
strip.setPixelColor(i, Wheel((i+rainbowChange) & 255));
}
strip.show();
delay(wait);
}
//Theatre-style crawling lights.
void theaterChase(uint32_t c, uint8_t wait, uint16_t numPixels) {
for (int rainbowChange=0; rainbowChange<10; rainbowChange++) {
for (int q=0; q < 3; q++) {
for (int i=0; i <= numPixels; i=i+3) {
strip.setPixelColor(i+q, c);
}
strip.show();
delay(wait);
for (int i=0; i <= numPixels; i=i+3) {
strip.setPixelColor(i+q, 0);
}
}
}
}
uint32_t Wheel(byte WheelPos) {
if(WheelPos < 85) {
return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
else if(WheelPos < 170) {
WheelPos -= 85;
return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
}
else {
WheelPos -= 170;
return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
}
void Mode(char ch, uint16_t numPixels, uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f) {
if(ch == 'a') {
colorWipe(a, 15, numPixels); //OFF
}
else if(ch == 'r') {
colorWipe(b, 15, numPixels); // Red
temp = 'r';
while(Serial.available() <=0);
volumeChar = Serial.read();
volumeUpDown(volumeChar);
}
else if(ch == 'g') {
colorWipe(strip.Color(0, bright, 0), 15, numPixels); // Green
while(Serial.available() <=0);
volumeChar = Serial.read();
brightnessUpDown(volumeChar);
}
else if(ch == 'b') {
colorWipe(d, 15, numPixels); // Blue
temp = 'b';
while(Serial.available() <=0);
volumeChar = Serial.read();
volumeUpDown(volumeChar);
}
else if(ch == 'x') { //rainbow
while(Serial.available() <=0);
volumeChar = Serial.read();
rainbowUpDown(volumeChar);
}
else if(ch == 'y') {
theaterChase(f, 15, numPixels); //Starlight
}
}
소스의 경우 상당히 길어 보이는데 기능은 사실 간단합니다.
큐브의 소스에서는 가속도 센서를 이용하여 큐브의 윗면이 무엇인지 파악하게 됩니다.
다만 가속도 센서의 값을 그대로 사용할 경우 값이 불안정하기 때문에 Kalman-Filter를 이용하여 안정된 값을 얻습니다.(필터로 인해 소스가 길어지는 듯 합니다.)
X값, Y값, Z값 3개를 비교하여 6개의 면을 구분합니다.
LED 부분의 소스에서는 큐브에서 Zigbee를 통해 날라오는 데이터 값으로 LED를 제어하게 되는데
'r'이라는 데이터가 날라올 경우 빨간색으로 LED를 켜고
'g'가 날라오면 녹색으로 켜고
'b'가 날라오면 파란색으로 키는 간단한 수준의 통신입니다.
Zigbee를 사용할 경우 Serial.print()만으로도 데이터 전송이 가능하기 때문에 아두이농 Serial통신을 하실 수 있다면 쉽게 통신을 만들 수 있습니다.
녹색 켜기
빨강 켜기
무지개색 조절하기
수박쨈