시골 처갓집에서 올라오는 동안 대충 머리로 구상을 하고 인터넷을 좀 찾아보며 스펙을 구상해 봅니다.
필요한 사양
- 37.5 도 정도의 온도를 유지할 것
습도는 60프로이상, 부화 2~3일 전부터는 70프로 이상을 유지할 것
간단하네요.
아마 이런식으로 만들면 되지 않을까 하고 딸내미랑 앉아서 구상을 해봤습니다.
대충 감이 오시죠? 할로겐 램프가 열이 많이 나므로 조명 역할도 하고 내부를 덥혀주는 역할을 하며 내부 공기가 균일한 온도를 갖도록 작은 팬으로 공기를 순환 시킵니다. 온도 센서를 이용하여 특정 온도보다 높아지면 램프를 꺼주고, 낮아지면 켜주는 간단한 회로가 되겠네요.
저는 지난번에 구입해 놓은 마이크로비트를 이용하기로 했습니다. 디스플레이가 달려있으므로 현재 온도를 바로 알수 있다는 장점이 있고요, 또 코딩하는 과정을 딸내미에게 보여주며 설명을 해주기 위함입니다. 아무래도 아두이노 보다는 마이크로 비트의 코딩 블럭이 아이들에게는 더 재미있고 쉽겠죠? ㅎ
재료준비
필요한 재료를 모아 봅니다.
적당한 크기의 스티로폼 박스
마이크로 비트 (또는 아두이노)
온습도센서 (DHT11)
할로겐 램프
12V 소형 냉각 팬 (공기 순환용)
N 채널 MOSFET (그냥 릴레이 사용 가능)
3.3V 정전압 장치 (아두이노 사용할 경우 불필요)
전선들과 12V 아답터
네. 재료들도 심플하네요. 다행히 저는 모두 집에 있는 재료여서 따로 구입은 필요하지 않았고 바로 제작에 들어가기로 했습니다.
적당히 주워모은 재료들 입니다.
먼저 오늘의 주인공
몸값도 귀하신 마이크로비트 되시겠고요~
그 다음 선수들은요.
네. 마이크로비트에 비하면 하찮기 그지 없는 떨거지 부품들이고요~
끝으로 여기서 안쓰이면 쓰레기가 될 스티로폼 상자가 되시겠습니다.
제작 시작!
자 먼저 상자 뚜껑에 fan 과 램프를 장착합니다.
원래 설계에는 옆에 fan 을 그려놨는데 할로겐 램프에서 열이 많이 나면 스티로폼이 녹을 것 같아 할로겐 램프의 열을 아래쪽으로 바로 쏴주기로 했습니다.
적당한 나사로 팬을 고정하고 램프를 터미널에 물려 아이스크림 막대기에 본드로 붙여 박스 뚜껑에 꼽았습니다.
다음은 메인 보드 입니다.
12V 전원을 인가 받아 12V 팬을 상시 구동하여 주고, 마이크로 비트에는 3.3V 를 보내주며 중간에 스위치가 마이크로 비트의 제어를 받아 할로겐 램프를 켰다 껐다 할겁니다.
보드를 너무 작게 자른 바람에 땜질이 조금 힘들긴했지만 필요한건 다 넣었네요.
참고로 3.3V 레귤레이터는 마이크로 비트 구동을 위한 것이므로 아두이노를 사용하시면 그냥 12V 를 vcc 에 연결하시면 됩니다.
그리고 중간에 작은 MOSFET 이 하나 있는데, 마이크로 비트의 신호 1, 0 을 받아 할로겐 램프를 켰다 껏다 하는 스위치 역할을 할겁니다. 그냥 일반적인 릴레이를 사용하셔도 아무 문제 없습니다.
자 그럼 의도한 데로 동작하는지 테스트를 해봤습니다. 물론 아직 온도 제어는 못해봤기에 할로겐램프, 팬, 마이크로비트가 켜지는지 정도만 테스트를 해봤습니다.
자 그럼 이제 프로그래밍을 하고 박스에 나머지 부품을 설치해보겠습니다.
마이크로 비트를 컴퓨터에 연결하고 아래와 같이 코드를 작성합니다.
뭐 별건 없고요. 먼저 온습도 센서인 DHT11 라이브러리를 넣어주어야 해서 우측 톱니바퀴 -> 확장프로그램 으로 들어가신 뒤에 상단검색창에 DHT11 을 검색하신뒤 나오는 것을 클릭해주면 주황색으로 된 블럭들이 추가된 것을 볼 수 있습니다.
온도와, 습도를 저장하기 위한 변수를 2개 만들었고요, 디스플레이에 한번은 온도, 한번은 습도가 표시되도록 상태를 기록할 변수도 하나 만들었습니다.
현재온도가 -999 같은 이상한 값이 들어오기도 하여서 0 보다 클때만 동작하도록 했고요, 온도가 37도보다 낮으면 램프를 켜주고 38도 보다 높으면 램프를 끄도록 해 놨습니다.
위에서 말했지만 저의 경우 P0 핀을 MOSFET 에 연결하여 램프를 켜고 끌껀데요, 릴레이 쉴드 같은걸 이용해서 만드셔도 무방합니다. 다만 릴레이의 딱딱 거리는 소리가 듣고 싶지 않아 MOSFET을 사용했다고 보시면 됩니다.
간단하죠??
아직 헷갈리시는 분을 위해 간단히 구성도를 다시 올려보겠습니다.
자 이제 코딩도 완료 되었고 박스에 설치를 해보겠습니다.
마이크로 비트의 온도가 항상 잘 보이도록 상다 위에 꼽아 주고요, 각종 선들을 구멍을 뚫어 연결합니다.
그리고 옆쪽에 내부가 보이도록 창을 크게 내고 안쪽과 바깥쪽에 각각 투명 시트를 붙여서 단열을 해주었습니다.
내부에는 계란 판을 잘라서 한번에 8개를 넣을 수 있도록 해주었고 종지를 하나 넣어 물을 담아 놓았습니다.
그 결과는~
두구두구
쨔잔~ ㅋ
딸내미가 꾸며 준다고 이것 저것 하고 있습니다. ㅋㅋㅋ
온도도 프로그래밍 한데로 잘 동작하는 것 같습니다. 습도도 60프로 이상 잘 올라가네요.
다음에 처갓집에 갈때 가져가서 금방 낳은 신선한 계란을 담아가지고 올라오면 될것 같네요. 어짜피 12V 로 동작하므로 차량의 트렁크에 넣어서 시거잭에 연결하면 올라오는 동안도 잘 부화가 진행될거라 생각합니다.
취미생활을 즐기는 것은 매우 즐거운 일인데요, 아이와 함께 하는 취미생활은 더욱 즐거운 것 같습니다.
냉동실에 문제가 생긴건 아닐까 하여 살펴 보았지만 냉동실 내에 넣어 둔 식품들은 별다른 문제 없이 꽁꽁 잘 얼어 있더군요.
어쨌거나 찝찝하기 그지 없어 잘 살펴 보니 바닥쪽에 물이 고여 얼어있었습니다.
아마 그쪽에서 흘러 나온듯 하여 문제를 해결해보기로 합니다.
먼저 김치 냉장고를 냉동 모드로 변경한 뒤 냉동실에 있는 것들을 모조리 옮겨놓고 냉장고 전원을 내립니다.
우선 냉동실 내부의 서랍과 선반들을 모두 분리해 냅니다.
그런 다음 뒤쪽 덮개를 떼어내야 하는데요. 상단, 하단으로 구분되어 있는데, 우선 상단 가운데 있는 나사를 풀어내고 위쪽부터 뜯어냅니다.
뜯어 낼때는 양쪽에 고정 걸쇠가 있는데요, 별다른 도구는 필요하지 않고 살살힘을 주어 한쪽씩 당기면 투툭! 하며 빠집니다.
전선 정리 홀더 같은 곳에 있는 전선 (내부 조명용) 을 함께 제거하면 위와 같은 모습이 됩니다.
그다음 아래쪽 덮개를 떼어내야 하는데 아래쪽은 별도로 나사 같은 것으로 고정되어 있지는 않고 그냥 윗부분을 좌우 한쪽씩 잡고 당기면서 떼어내면 하나씩 걸쇠가 풀리면서 분리가 됩니다. 분리 방법은 제품마다 다르겠으나 큰 차이는 없을 것으로 생각됩니다.
하부 덮개를 떼어내니 안쪽에 열교환기가 보이는데요, 아래쪽에 어마어마 한 얼음이 얼어 있습니다.
반응형
딱봐도 여기가 문제군요.
엄청나게 얼어있는 얼음을 살살 제거해 줍시다.
드라이를 이용해서 얼음과 주변을 좀 녹여 준 뒤 1자 드라이버와 망치를 이용해서 살살 쳐서 얼음을 깨 주었습니다.
드라이버로 녹이는게 가장 안전하겠지만 아주 단단히 얼어있는 얼음이기 때문에 녹이는데 시간이 많이 걸릴 것으로 보여 벽면이나 부품에 달라 붙어있는 부분만 우선 녹여낸 뒤 얼음을 깨 내면 부품에 손상 없이 얼음만 똑 떼어낼 수 있습니다.
중간 중간 아래쪽에 있는 얼음도 깨주었습니다. 주변이 살짝 녹을 정도면 조금만 톡톡 쳐줘도 덩어리로 얼음이 툭 깨져 나옵니다.
10여분 정도 사투한 결과 이제 거의 얼음이 해결되어 가고 있습니다.
과연 저것만 깨내면 문제가 해결되는 것일까요?
보이는 얼음을 일단 다 제거하고 보니 ... 수정한 구멍이 발견되었습니다.
네.. 얼음을 모두 제거 하고 보니 아래쪽으로 구멍이 보이는군요.
그런데 구멍 역시 얼어 있습니다. 뭔가 구조상 물이 빠지는 구멍처럼 생겼는데 얼음이 여길 막고 있는 느낌이네요. 아마 여길 따라 흘러내려가야 할 물이 얼어서 구멍을 막고 , 막힌 구멍 때문에 물이 고여서 얼고, 또 그위로 물이 흘러 넘쳐 문 밖 까지 나온것으로 보여지네요.
네, 그럼 구멍 안쪽의 얼음도 녹여보기로 합니다.
드라이를 이용하여 좀 녹여 보았지만 녹은 물이 고여 아래쪽으로 열이 잘 전달이 되지 않는 것 같습니다.
그래서 도구를 하나 추가하였습니다.
네 보이는 그대로 어항용 호스를 낀 주사기를 하나 준비하였습니다. 호스로 녹은 물을 빨아내서 드라이의 열이 바로 얼음에 닿을 수 있도록 합니다.
요렇게 몇변을 반복하다 보니 드라이 바람이 구멍 안쪽까지 잘 안들어 가는 것 같네요.
그래서 물을 조금 끓여서 부어 주고 드라이버로 살살 저어 주었더니 갑자기 아래로 쭉 하고 물이 내려가더군요.
네.. 호스가 끝까지 들어갔고 물은 빨아내지지 않네요.
뭔가 해결이 된 것 같습니다. ㅎㅎ
이제 떼어낸 모든 부품을 다시 조립하면 끝!
냉동실은 다시 정상 적으로 잘 구동되는 걸 보니 문제가 해결된것 같습니다. (물론 전에도 문제 없이 동작했지만요 -_-;;)
한시간 정도 걸렸으려나요? 냉장실에 있는 음식들이 상할까봐 급하게 진행을 했는데요, 저와 같은 문제가 생기셨다면 당황하지 마시고 저처럼 진행하시면 되겠습니다.
끝으로 오늘 저런 사태가 왜 나타난건지 생각해 보았습니다.
요즘 냉장고를 사용하다 보면 예전처럼 안쪽에 성애가 꽉 끼거나 하는 문제가 없죠? 왜 그럴까요?
아까 사진에 보았던 열 교환기에 분명히 성애가 잔뜩 붙어 있을 줄 알았는데 아니더라고요.
바로 아래쪽에 있는 부품에서 주기적으로 열을 발생시켜 위쪽 열교환기에 붙은 성애를 녹여주는거죠. 그럼 성애가 녹아서 물이되고 물은 흘러내려 아래 구멍으로 빠져 나가게 됩니다.
그런데 예전 냉장고 처럼 아래쪽에 물받이 같은 것은 또 없죠? 오래전 냉장고들은 아래쪽에 넓적한 물받이 같은게 있어서 어떤 이유에서 생겼든 물이 고이도록 되어있는데요, 이렇게 흘러나온 물이 과연 어디로 가는 걸까요?
냉동실 제일 아래 부분이 불룩한 것으로 보면 그 안에 컴프레셔가 들어있을 텐데요. 아마 컴프레셔 열로 흘러나온 물을 증발시키는 것이 아닌가 생각됩니다. 그럼 따로 물받이 같은걸 통해 물을 받아서 버려야할 필요는 없겠습니다.
오늘은 앞에서 소개해드린 mp6050 을 이용하여 측정한 회전 정보를 이용하여 서보모터를 제어하는 것을 응용하여 무선으로 서보모터를 제어하는 과정을 소개해 드릴 예정인데요, 여기에서는 아주아주 놀랍고 멋진 무선 송수신 칩인 nrf24L01 칩을 이용하여 구현을 할 계획입니다.
nrf24L01 칩의 경우 저렴하면서도 놀라운 성능을 보여주는 아주아주 애정하는 부품입니다.
실제로 해당 칩을 이용하여 RC 카 조종장치와 수신 장치를 만들기도 한적이 있습니다. 제어되는 범위, 거리도 아주 훌륭하고 신호 수준도 좋으며 중요한 것은 1개의 송신부에서 여러개의 수신장치를 제어할 수 있는 멋진 칩 입니다. 게다가 송신과 수신을 하나의 칩에서 지원하고 있으므로 양방향 통신을 구현하는데에도 어려움이 없습니다.
일단 오늘 도전할 목표는 앞서 말씀 드렸던것 처럼 mp6050 자이로 센서에서 측정된 회전 값을 NRF24L01 칩을 이용하여 보내고 또다른 NRF24L01 칩에서 이 신호를 받아 2축 서보모터를 제어하는 과제가 되겠습니다.
이전 포스팅에서 소개해 드린 것에서 무선 송수신이 추가만 되었을 뿐 크게 달라지는 건 없습니다.
무선 송수신을 위한 라이브러리는 웹상에도 많이 있고요, 아래 첨부해둔 라이브러리를 받으셔서 바로 내문서-아두이노 폴더에 넣으셔도 됩니다.
자 이젠 무선 송수신 칩이 들어오면서 배선이 조금 복잡해 집니다.
차근차근 따라 오시면 됩니다.
NRF24L01 칩에 대하여
여기서 NRF24L01 칩 외에 추가 준비물이 필요한데요, 준비물은 바로 10uf 전해콘덴서 입니다. 이 전해 콘덴서를 nrf24l01 의 전원 (3.3v) 단에 연결을 해 주셔야 문제 없이 잘 동작합니다. 용량은 가능하면 10uf 로 준비해 주세요, 제가 처음 테스트 할 때 전해 콘덴서 용량이야 뭔들 중요하랴... 싶어 용량이 안 맞는 부품을 적당히 연결해서 해보았지만 잘 안되서 정말 골머리를 썩었는데요, 10uf 콘덴서를 달면 정말 거짓말처럼 동작이 잘 됩니다.
물론 소스코드나 배선에 실수는 없어야 겠지요.
한가지더, nrf24l01 칩은 몇가지 타입이 있는데요, 크기가 작은 SMD 타입과 일반 핀이 달려있는 Dip 타입, 그리고 원거리 송수신이 가능한 PA LN 타입이 있습니다. 핀 구성은 모두 동일하고 배열만 약간 차이가 있으며 코드는 모두 동일하게 지원 합니다. 저는 소형화된 부품이 좋아 보통은 SMD 타입 부품을 사용하고요, 배선이 일렬로 되어 있어 브래드 보드 등에 테스트 하기도 편리한 점이 장점이라 할 수 있습니다. 해외 어떤 사용자 분은 성능도 SMD 타입이 좋다고 하시는데.. 실제로 그런지는 잘 모르겠습니다. (- -)>;;
DIP 타입을 이용하실 경우 확장 보드가 있으면 조금더 편리하게 사용하실 수 있지만 부피가 조금 커진다고 보시면 되고, PA-LNA 보드는 DIP 타입의 확장보드와 호환이 가능하며 아래쪽에 SMD 타입과 같은 보드 실장을 위한 연결 단자가 있으므로 보드에 바로 연결이 가능합니다. PA-LNA 보드의 경우 NRF24L01 칩이 기본 탑제되어 있고 위에서 말씀드린것과 같이 코드 및 배선은 동일하게 하시면 되며 원거리까지 송수신이 가능하므로 무선으로 멀리까지 신호를 보내고 받아야 하는 경우 사용하시면 되겠습니다.
배선 시작
자 이제 배선을 해보겠습니다.
먼저 제가 사용할 SMD 타입은 만능기판에 바로 핀을 꼽기가 어려운데요. 그래서 생각해낸 것이 아래와 같은부품을 이용하는 것 입니다.
가운데 있는 보드를 가운데를 잘라주고 반쪽만 사용하게 되는데요, 이 pcb 를 이용하면 NRF24L01 SMD 칩의 핀 간격과 정확히 일치 하게 되며 실제로 사용하지 않는 가장 오른쪽 IRQ 는 무시하고 왼쪽부터 맞추어 납을 흘려넣어 납땜을 해주면 됩니다.
하단의 구멍에는 1.24mm pin 이 정확히 맞으므로 일렬로 배열된 핀을 만능기판에 꼽아 사용할 수 있게 됩니다.
위에서 말씀드린 10uF 전해 콘덴서까지 연결하게 되면 아래와 같은 모양이 됩니다.
NRF24L01 칩은 아래와 같은 핀 구성을 하고 있고요, 각각 아두이노와 그림처럼 연결하시면 됩니다.
CE, CSN 은 각각 7번 8번에 연결하여야 하는데 이 두핀은 위치가 바뀌어도 상관 없습니다. 코드상에서 정의해준 핀의 번호와 일치하기만 하면 되고요, 나머지 핀은 지정된 핀에 연결하시면 됩니다.
저처럼 SMD 타입을 사용할 경우 전원은 아두이노의 반드시 3.3V 에 연결해 주셔야 하고 dip 타입을 사용하시는 경우 확장보드를 이용하시면 5V 를 바로 연결하셔도 되며 10uf 전해 콘덴서는 필요 없습니다.
특별 초대손님이 있다고?
자~ 오늘의 특별 초대손님이 계신데요...
두구두구
두구
두구
짜잔~
네~ 바로 아두이노로 제작한 RC 카 수신기 입니다. 물론 송신기도 아두이노로 만들었었습니다.
해당 보드는 왼쪽 상단에 3핀 소켓을 통해 ESC 로부터 전원을 입력 받고 신호 선을 통해 모터스피드(속도)를 제어하게 되고요, 우측 아래 두개의 3핀 소켓을 통해 조향서보와 2-speed gear 의 조작용 서보에 연결하게 제작되었습니다. 오늘은 해당 두개의 서보모터 핀을 이용하여 x,y 축을 제어해 보도록 할 계획입니다. 보드 중간에 가로로 보이는 구멍이 NRF24L01 보드를 장착하기 위한 소켓입니다.
무선 모듈을 연결하면 아래와 같은 모양이 됩니다.
한동안 즐겁게 가지고 놀았었는데요, 일반적인 RC 카 수신기에 비하면 덩치가 조금 크기는 하지만 4개의 2pin 포트를 통해 헤드라이트나 후미등, 좌우측 깜박이 등을 이 보드 하나로 제어할 수 있어 멀펑 보드가 별도로 필요하지 않으므로 나름 쓸만하다고 할 수 있겠습니다.
언제 시간이 나면 핀 사진의 커넥터 대신 일반 적인 RC 카에 많이 사용하는 후타바 짹이나 JR 커넥터 등을 이용하여 부피를 줄여볼 계획입니다. 나중에 작업하게 되면 소개해 드리겠습니다.,
배선은 송신부, 수신부 두개의 아두이노에 동일하게 해주시면 되고 송신부와 수신부는 코드에서 정의를 해주게 됩니다.
배선이 완료된 송신부 입니다.
네 사진상으로는 조금 복잡하지만 기존 설명드린데로 잘 연결 하셨다면 어려움 없이 잘 하실 수 있으리라 믿습니다.
수신기 쪽을 볼까요?
네 수신부에는 예상하시다 시피 두개의 서보모터와 NRF24L01 보드가 연결되어 있습니다.
코딩을 해보자
코드를 볼까요?
먼저 송신부 입니다. 기존 작성해 놓은 mp6050 코드에 무선 송수신 코드만 추가할 예정입니다.
// init for nrf24L01
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
RF24 radio(8,7); // CE, CSN
const byte address[6] = "00001"; //송신기와 수신기 동일한 주소 사용
int msg[8];
//pinout smd version
//3.3v --- 3.3v
//GND --- GND
//CE --- 8
//CSN --- 7
//SCK --- 13
//MOSI --- 11
//MISO --- 12
//IRQ --- none
// init for mpu6050
#include "I2Cdev.h"
#include "MPU6050_6Axis_MotionApps_V6_12.h"
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
#include "Wire.h"
#endif
MPU6050 mpu;
#define OUTPUT_READABLE_YAWPITCHROLL
#define INTERRUPT_PIN 2 // use pin 2 on Arduino Uno & most boards
#define LED_PIN 13 // (Arduino is 13, Teensy is 11, Teensy++ is 6)
bool blinkState = false;
// MPU control/status vars
bool dmpReady = false; // set true if DMP init was successful
uint8_t mpuIntStatus; // holds actual interrupt status byte from MPU
uint8_t devStatus; // return status after each device operation (0 = success, !0 = error)
uint16_t packetSize; // expected DMP packet size (default is 42 bytes)
uint16_t fifoCount; // count of all bytes currently in FIFO
uint8_t fifoBuffer[64]; // FIFO storage buffer
// orientation/motion vars
Quaternion q; // [w, x, y, z] quaternion container
VectorInt16 aa; // [x, y, z] accel sensor measurements
VectorInt16 gy; // [x, y, z] gyro sensor measurements
VectorInt16 aaReal; // [x, y, z] gravity-free accel sensor measurements
VectorInt16 aaWorld; // [x, y, z] world-frame accel sensor measurements
VectorFloat gravity; // [x, y, z] gravity vector
float euler[3]; // [psi, theta, phi] Euler angle container
float ypr[3]; // [yaw, pitch, roll] yaw/pitch/roll container and gravity vector
volatile bool mpuInterrupt = false; // indicates whether MPU interrupt pin has gone high
void dmpDataReady() {
mpuInterrupt = true;
}
void setup() {
// join I2C bus (I2Cdev library doesn't do this automatically)
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
Wire.begin();
Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties
#elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
Fastwire::setup(400, true);
#endif
Serial.begin(115200);
while (!Serial); // wait for Leonardo enumeration, others continue immediately
// initialize device
Serial.println(F("Initializing I2C devices..."));
mpu.initialize();
pinMode(INTERRUPT_PIN, INPUT);
// verify connection
Serial.println(F("Testing device connections..."));
Serial.println(mpu.testConnection() ? F("MPU6050 connection successful") : F("MPU6050 connection failed"));
// wait for ready
Serial.println(F("\nSend any character to begin DMP programming and demo: "));
//while (Serial.available() && Serial.read()); // empty buffer
//while (!Serial.available()); // wait for data
//while (Serial.available() && Serial.read()); // empty buffer again
// load and configure the DMP
Serial.println(F("Initializing DMP..."));
devStatus = mpu.dmpInitialize();
// supply your own gyro offsets here, scaled for min sensitivity
mpu.setXGyroOffset(51);
mpu.setYGyroOffset(8);
mpu.setZGyroOffset(21);
mpu.setXAccelOffset(1150);
mpu.setYAccelOffset(-50);
mpu.setZAccelOffset(1060);
// make sure it worked (returns 0 if so)
if (devStatus == 0) {
// Calibration Time: generate offsets and calibrate our MPU6050
mpu.CalibrateAccel(6);
mpu.CalibrateGyro(6);
Serial.println();
mpu.PrintActiveOffsets();
// turn on the DMP, now that it's ready
Serial.println(F("Enabling DMP..."));
mpu.setDMPEnabled(true);
// enable Arduino interrupt detection
Serial.print(F("Enabling interrupt detection (Arduino external interrupt "));
Serial.print(digitalPinToInterrupt(INTERRUPT_PIN));
Serial.println(F(")..."));
attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING);
mpuIntStatus = mpu.getIntStatus();
// set our DMP Ready flag so the main loop() function knows it's okay to use it
Serial.println(F("DMP ready! Waiting for first interrupt..."));
dmpReady = true;
// get expected DMP packet size for later comparison
packetSize = mpu.dmpGetFIFOPacketSize();
} else {
// ERROR!
// 1 = initial memory load failed
// 2 = DMP configuration updates failed
// (if it's going to break, usually the code will be 1)
Serial.print(F("DMP Initialization failed (code "));
Serial.print(devStatus);
Serial.println(F(")"));
}
// Radio setup
//setupRadio();
msg[0] = 0;
msg[1] = 0;
msg[2] = 0;
msg[3] = 0;
msg[4] = 0;
msg[5] = 0;
msg[6] = 0;
msg[7] = 0;
radio.begin();
radio.openWritingPipe(address); //이전에 설정한 5글자 문자열인 데이터를 보낼 수신의 주소를 설정
radio.setPALevel(RF24_PA_MIN); //전원공급에 관한 파워레벨을 설정합니다. 모듈 사이가 가까우면 최소로 설정합니다.
radio.stopListening(); //모듈을 송신기로 설정
}
void loop() {
// if programming failed, don't try to do anything
if (!dmpReady) return;
// read a packet from FIFO
if (mpu.dmpGetCurrentFIFOPacket(fifoBuffer)) { // Get the Latest packet
#ifdef OUTPUT_READABLE_YAWPITCHROLL
// display Euler angles in degrees
mpu.dmpGetQuaternion(&q, fifoBuffer);
mpu.dmpGetGravity(&gravity, &q);
mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
float angle_x = (ypr[0] * 180 / M_PI) * -1 + 90 ; //* -1 + 90;
float angle_y = (ypr[1] * 180 / M_PI) + 90 ; //* -1 + 30;
if (angle_y < 60) angle_y = 60;
if (angle_y > 120) angle_y = 120;
if (angle_x < 30) angle_x = 30;
if (angle_x > 150) angle_x = 150;
msg[0] = int(angle_x);
msg[1] = int(angle_y);
radio.write(&msg, sizeof(msg)); //해당 메시지를 수신기로 보냄
Serial.print("ypr\tx:");
Serial.print(int(angle_x));
Serial.print("\ty:");
Serial.print(int(angle_y));
Serial.print("\t");
#endif
}
Serial.println(".");
delay(10);
}
아무래도 셋업해주는 부분이 약간 생소할 수 있겠습니다만 저도 인터넷에서 긁어 모은 코드를 조합하였을 뿐 상세하게는 모른답니다. 중요한 것은 노드의 이름을 정해주었는데요, 수신부에서도 동일한 이름을 지정해 주어야 한다는 점과, 다른 분들과의 혼선을 막기 위하여는 해당 이름을 유니크한 이름으로 정해주는 것이 좋다는 것 정도 입니다.
제가 사용한 코드는 (존경하는)새다리 님의 코드를 참고 하였고 NRF24L01 로 검색하시면 해외 여러 개발자 분들이 올려주신 멋진 코드들이 많이 있으므로 참고하시면 좋을 것 같습니다.
이번에는 수신부 입니다.
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
RF24 radio(8,7); // CE, CSN
const byte address[6] = "00001"; //송신기와 수신기 동일한 주소 사용
int msg[8];
//pinout smd version
//NRF24L01 ARDUINO
//3.3v --- 3.3v
//GND --- GND
//CE --- 8
//CSN --- 7
//SCK --- 13
//MOSI --- 11
//MISO --- 12
//IRQ --- none
#include <Servo.h>
Servo myservo_LR;
Servo myservo_UD;
int pin_servo_LR = 9;
int pin_servo_UD = 10;
void setup() {
radio.begin();
radio.openReadingPipe(1, address);
radio.setPALevel(RF24_PA_MIN); //
// RF24_PA_MIN / RF24_PA_LOW / RF24_PA_HIGH / RF24_PA_MAX
radio.startListening(); //수신기로 설정
//setting 2 servo
myservo_LR.attach(pin_servo_LR);
myservo_LR.write (90);
myservo_UD.attach(pin_servo_UD);
myservo_UD.write (90);
}
void loop() {
if (radio.available()) {
radio.read(&msg, sizeof(msg));
for(int i = 0; i < 8 ; i ++)
{
Serial.print(msg[i]);
Serial.print("\t");
}
int angle_x = int(msg[0]);
int angle_y = int(msg[1]);
if (angle_x < 150 && angle_x > 30)
{
myservo_LR.write (angle_x);
}
if (angle_y < 120 && angle_y > 60)
{
myservo_UD.write(angle_y);
}
}
}
수신부는 서보모터 제어코드와 무선 수신 코드가 있는데요, 송신부에 비하면 간단하게 구성되어 있습니다.
무선 패킷이 들어오면 동작하도록 되어 있으므로 수신에 실패하면 아무런 동작도 하지 않도록 되어 있으며 만약을 대비하여 잘못된 값이 수신되더라도 서보모터에 전달되지 않도록 최대, 최소 범위를 제한하여 서보에 입력되도록 하였습니다.
먼저 라이브러리를 받아야 할텐데요, 아래 첨부된 라이브러리를 다운 받으셔서 바로 내문서- Arduino-libraries 폴더에 압축을 해제하여 넣으시면 됩니다.
혹시 Wire.h 라이브러리가 없으시면 아래 파일도 다운 받으시면 됩니다.
반응형
mpu6050 라이브러리를 받으시면 예제 소스코드가 들어있을 텐데요, 저는 여기서 시작 이후 실제 회전하는 값, 즉 상대 회전 값만을 이용할 계획이고 아래의 코드를 이용하여 회전 값을 추출할 계획입니다. 본 포스트에서 소개해 드리는 예제 외에도 라이브러리가 제공하는 예제 코드를 실행해보시면 다른 창작품 제작에도 도움이 될 수 있을 것 같습니다. ^^
기존 조이스틱 대신 이번에는 mpu6050 보드를 연결해야 하는데요. I2C 방식으로 연결할 것이므로 선은 4가닥만 있으면 되며 vcc 는 3.3v 에 연결해 주시고, GND 는 아두이노의 GND 에 연결, SDA 는 아두이노의 A4 번에 SCL 은 아두이노의 A5에 연결하시면 됩니다. (다른 핀은 안됩니다. 정확히 A4, A5에 연결해 주셔야 합니다)
코딩을 해보자
mpu6050 에 대한 자세한 내용은 이미 여러분들께서 다루어 주시고 있으므로 저는 실제 이번 프로젝트에 사용한 코드만 보여드리도록 하겠습니다.
#include <Servo.h>
#include "I2Cdev.h"
#include "MPU6050_6Axis_MotionApps_V6_12.h"
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
#include "Wire.h"
#endif
MPU6050 mpu;
#define OUTPUT_READABLE_YAWPITCHROLL
#define INTERRUPT_PIN 2 // use pin 2 on Arduino Uno & most boards
#define LED_PIN 13 // (Arduino is 13, Teensy is 11, Teensy++ is 6)
bool blinkState = false;
// MPU control/status vars
bool dmpReady = false; // set true if DMP init was successful
uint8_t mpuIntStatus; // holds actual interrupt status byte from MPU
uint8_t devStatus; // return status after each device operation (0 = success, !0 = error)
uint16_t packetSize; // expected DMP packet size (default is 42 bytes)
uint16_t fifoCount; // count of all bytes currently in FIFO
uint8_t fifoBuffer[64]; // FIFO storage buffer
// orientation/motion vars
Quaternion q; // [w, x, y, z] quaternion container
VectorInt16 aa; // [x, y, z] accel sensor measurements
VectorInt16 gy; // [x, y, z] gyro sensor measurements
VectorInt16 aaReal; // [x, y, z] gravity-free accel sensor measurements
VectorInt16 aaWorld; // [x, y, z] world-frame accel sensor measurements
VectorFloat gravity; // [x, y, z] gravity vector
float euler[3]; // [psi, theta, phi] Euler angle container
float ypr[3]; // [yaw, pitch, roll] yaw/pitch/roll container and gravity vector
volatile bool mpuInterrupt = false; // indicates whether MPU interrupt pin has gone high
void dmpDataReady() {
mpuInterrupt = true;
}
Servo myservo_LR; // streering servo
Servo myservo_UD; // 2speed gear box servo
int pin_servo_LR = 9;
int pin_servo_UD = 10;
int pin_x = A3;
int pin_y = A4;
int angle_x = 512 ;
int angle_y = 512 ;
void setup() {
// join I2C bus (I2Cdev library doesn't do this automatically)
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
Wire.begin();
Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties
#elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
Fastwire::setup(400, true);
#endif
Serial.begin(115200);
while (!Serial); // wait for Leonardo enumeration, others continue immediately
// initialize device
Serial.println(F("Initializing I2C devices..."));
mpu.initialize();
pinMode(INTERRUPT_PIN, INPUT);
// verify connection
Serial.println(F("Testing device connections..."));
Serial.println(mpu.testConnection() ? F("MPU6050 connection successful") : F("MPU6050 connection failed"));
// wait for ready
Serial.println(F("\nSend any character to begin DMP programming and demo: "));
//while (Serial.available() && Serial.read()); // empty buffer
//while (!Serial.available()); // wait for data
//while (Serial.available() && Serial.read()); // empty buffer again
// load and configure the DMP
Serial.println(F("Initializing DMP..."));
devStatus = mpu.dmpInitialize();
// supply your own gyro offsets here, scaled for min sensitivity
mpu.setXGyroOffset(51);
mpu.setYGyroOffset(8);
mpu.setZGyroOffset(21);
mpu.setXAccelOffset(1150);
mpu.setYAccelOffset(-50);
mpu.setZAccelOffset(1060);
// make sure it worked (returns 0 if so)
if (devStatus == 0) {
// Calibration Time: generate offsets and calibrate our MPU6050
mpu.CalibrateAccel(6);
mpu.CalibrateGyro(6);
Serial.println();
mpu.PrintActiveOffsets();
// turn on the DMP, now that it's ready
Serial.println(F("Enabling DMP..."));
mpu.setDMPEnabled(true);
// enable Arduino interrupt detection
Serial.print(F("Enabling interrupt detection (Arduino external interrupt "));
Serial.print(digitalPinToInterrupt(INTERRUPT_PIN));
Serial.println(F(")..."));
attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING);
mpuIntStatus = mpu.getIntStatus();
// set our DMP Ready flag so the main loop() function knows it's okay to use it
Serial.println(F("DMP ready! Waiting for first interrupt..."));
dmpReady = true;
// get expected DMP packet size for later comparison
packetSize = mpu.dmpGetFIFOPacketSize();
} else {
// ERROR!
// 1 = initial memory load failed
// 2 = DMP configuration updates failed
// (if it's going to break, usually the code will be 1)
Serial.print(F("DMP Initialization failed (code "));
Serial.print(devStatus);
Serial.println(F(")"));
}
myservo_LR.attach(pin_servo_LR);
myservo_LR.write (90);
myservo_UD.attach(pin_servo_UD);
myservo_UD.write (90);
}
void loop() {
// if programming failed, don't try to do anything
if (!dmpReady) return;
// read a packet from FIFO
if (mpu.dmpGetCurrentFIFOPacket(fifoBuffer)) { // Get the Latest packet
#ifdef OUTPUT_READABLE_YAWPITCHROLL
// display Euler angles in degrees
mpu.dmpGetQuaternion(&q, fifoBuffer);
mpu.dmpGetGravity(&gravity, &q);
mpu.dmpGetYawPitchRoll(ypr, &q, &gravity);
float angle_x = (ypr[0] * 180 / M_PI) * -1 + 90 ; //* -1 + 90;
float angle_y = (ypr[1] * 180 / M_PI) + 90 ; //* -1 + 30;
if (angle_y < 60) angle_y = 60;
if (angle_y > 120) angle_y = 120;
if (angle_x < 30) angle_x = 30;
if (angle_x > 150) angle_x = 150;
myservo_LR.write (angle_x);
myservo_UD.write (angle_y);
Serial.print("ypr\tx:");
Serial.print(int(angle_x));
Serial.print("\ty:");
Serial.print(int(angle_y));
Serial.print("\t");
#endif
}
Serial.println(".");
delay(10);
}
코드가 조금 길어졌기는 하지만 기존 첫번째 과정에 mpu6050 관련 코드가 추가되었고 크게 달라진것은 없습니다. mpu6050 관련된 상세한 코드는 저도 잘 모르고요. 그냥 예제에서 긁어온 코드입니다. ^^;; (불필요한 내용이 들어 있을 수도 있고요... 어쨌든 위의 코드면 잘 실행됩니다.)
위 코드에서 아래 부분이 실제 회전 값을 받아서 내가 원하는 각도로 세팅하는 부분인데요. 입력 값이 최초 실행된 위치로부터의 상대 값이므로 가만히 있으면 0 이 들어오게 됩니다. 그러므로 기준이 되는 앵글을 더해주면 기준이 되는 앵글에 + - 로 각도가 변경되게 되며 저의 x 위치에 -1 이 들어있는 것은 서보 모터의 설치 방향 때문에 방향을 뒤집어 주기 위함입니다. Y 값 역시 서보모터의 방향이 저와 다른 방향으로 설치되어 반대로 움직인다면 센서의 값에 -1 을 곱해 주시면 됩니다.
어쨌든 서보모터 제어를 위한 부분은 이전에 소개해 드린 코드 그대로 가져왔고요, 회전 정보를 받아 서보모터로 전달하는 과정에서 축의 기준위치, 방향 등을 변경해주기 위하여 위와 같이 약간의 코드가 추가되었습니다. if 구문을 이용하여 입력된 값 + 기준값이 모터의 최대 회전 범위 보다 크다면 최대 회전 범위의 값으로 설정을 해주면 됩니다.
저는 기준위치를 서보의 상하, 좌우 90도를 기준으로 설정하였고 상하로는 ±30도씩, 좌우로는 ±60도씩 움직일 수 있도록 하였습니다.
참 쉽죠?
과연 잘 동작할런지??
자 구동 되는 모습은 아래와 같습니다.
아주 잘 되죠?
기존 조이스틱이 움직이는 범위에 비하여 출력되는 범위가 리니어 하지 않은지 휙휙 움직이던것에 비하면 아주 아주 잘 동작하는 것을 확인할 수 있습니다.
이렇게 mpu6050 6축 지자기센서를 이용하여 2축 서보모터를 제어하는 것까지 마쳤습니다.
RC 카에 헤드트래킹을 이용한 FPV 를 구현하려는 목표가 생기고나니 과정을 잘 정리하는게 필요하겠다 싶어 포스트를 남깁니다.
우선 첫번째 스텝으로 2축으로 제어가 가능한 서보모터 마운트를 만들고 서보모터를 제어하는 것인데요, 이미 언젠가 사용하겠지 싶어서 구입해 둔 2축 서보모터 마운트가 있기에 이 부분은 간단히 해결될 거라 생각했습니다.
바로 중국에서 구입한 이런 제품이죠.
9g 서보모터를 이용하여 바로 연결이 가능하도록 나온 제품이고 약간 헷갈리기는 하지만 조립이 어렵지는 않습니다.
그런데 너무 쉽게 생각한게 오산이었을까요?
일단 어떻게 연결해도 약간 제가 생각하는 머리의 움직임과 괴리감이 있었습니다.
사람은 목이라는 놀라운 구조에 의해 상하좌우 회전이 거의 동일한 한 점에서 이루어 집니다. 심지어는 틸트까지 되죠, 3축이 하나의 구조에서 이루어지는 놀라운 구조가 아닐수 없습니다.
제가 구입하였던 2축 서보 마운트는 그런 개념에서 완전히 빗나가 있더군요. 일단 x, y 축의 회전 축이 상당히 어긋나 있다는 점이 첫번째 문제점이었고 두번째로는 기본 각도가 수평에서 시작한다는 점 이었습니다.
위의 사진에서 보이는 것처럼 상단 평평한 면이 이미 최대 상향(y축) 각도인데요, 말하자면 더이상 고개를 들거나 또는 내리는건 불가능한 상태 입니다. 물론 90도를 꺽은 상태에서 카메라를 부착하는 방법도 있지만 그렇게 되면 축에서 더욱 멀어지게 되므로 이미 상당한 수준의 거북목이 진행된 것처럼 되어 버립니다. 무슨 목을 쭉 앞으로 뺀 거북이처럼 회전이 되게 되는 것이죠.
세번째 문제는 전체 부품의 부피가 너무 크다는 것인데요, RC 카 운전석 안에 장착을 해야 하는데 이런 저런 부품이 너무 자리를 많이 차지하더군요, 음...
반응형
2축 서보 연결을 위한 브라켓 제작하기
그래서 아주 간단한 구조에 부피도 아주 작은 2축 회전축을 만들어 보기로 하였습니다.
제 포스트를 보시는 분도 제가 만든 것처럼 구현하시면 축의 위치를 거의 같은 선상에 두고 사람의 머리처럼 제어되는 축을 만드실 수 있을 거에요.
저는 3D 프린터를 이용하여 핵심 부품을 출력하였습니다만, 구조상 그냥 L 자 꺽쇠를 이용하여 만드시는 것도 가능합니다. 필요하신 크기로 가공만 하면 되는 것이죠.
저는 부품의 부피를 최소화 하기 위하여 서보모터의 고정 부품 안쪽으로 브라켓이 장착되도록 제작을 하였습니다.
이렇게 되면 상단의 서보모터는 Y 축(상하회전)을 제어하고 하단의 서보모터는 X 축(좌우회전)을 제어하게 되는데요, 우리의 목의 구조를 봐도 보통 회전은 귀를 중심으로 머리통이 상하로 움직이고 좌우 회전은 목 전체가 회전되므로 상대적으로 제가 만든것과 유사한 구조로 움직이는 것을 알 수 있습니다. 물론 사람의 목은 대단히 유연하고 훌륭한 관절이어서 상하회전이 꼭 한군데서 이루어지는 것은 아닙니다만 유사한 움직임이 구현되기는 합니다.
제가 만든 것처럼 브라켓의 상하 길이를 작게 만들게 되면 상하 회전 움직임에 제약이 있기는 합니다. 서보모터에 부딪힐 수 있기 때문인데요, 사람의 목이 상하로 180도를 움직이지 않는 것을 고려해보면 크게 문제될 것은 없어 보입니다.
만약 서보모터의 동작 범위 전체를 커버하기를 원하시면 브라켓의 상하 길이를 서보모터의 회전 반경보다 크게 제작하면 문제 없이 동작하게 됩니다.
아두이노와 연결을 해보자
자 이제 아두이노로 잘 제어가 되는지 확인해 보도록 하겠습니다.
2축 제어를 위하여 간단하게 2축 아날로그 조이스틱을 연결하고 제어를 해봅니다.
연결은 아래와 같이 하시면 됩니다.
소스코드는 아주 간단합니다.
아날로그 신호를 받아 서보모터를 움직이는 기본 소스코드 그대로 약간만 응용하면 구현이 가능합니다.