하나 만들어보고 싶은게 생겼습니다.

 

Shift Lighting Indicator의 줄임말인 SLI는 게임 속 차량 정보와 랩 타임 등의 여러 데이터를 가독성있게 출력해주는 일종의 계기판입니다.

SLI가 있으면 게임 속 렌더링 되는 화면 일부를 없애서 시야를 넓히고 레이싱에 더 몰입할 수도 있죠.

특히, 헬멧 시점으로 레이싱 할 때 가려지지 않아서 좋고, 더욱 현실감을 더해줍니다.

 

 

기본 시야에서는 이렇게 보이지가 않아요........ 물론 VR을 사서 밑을 쳐다보면 보이겠죠?^^

아 SLI가 기본 장착된 스티어링 휠도 있습니다!!

 

 

맙소사... 가격이....... 너무합니다 ㅠㅠ 모션기어 사의 SLI 모듈도 13만원이 넘습니다 ㅠㅠ

그래서 직접 만들어보기로 합니다.

우선은 Project Cars 2 게임에서 제공하는 데이터를 받아서 모듈로 전송이 필요하겠죠?

 

 

 

이렇게 게임 옵션에 들어가면 어떤 방식을 사용할지 설정할 수 있습니다.

 

1. 직접 개발한 PC 소프트웨어에서 공유 메모리를 열어 데이터 가공하고 모듈로 다시 전송하는 방법

2. 모듈에서 직접 데이터를 받아 출력하는 방법

 

이 두 가지 방법에서 크게 달라지지 않습니다.

2번 같은 경우는 데이터를 직접 처리해므로 컴퓨팅이 필요하겠지요.

 

하지만 라즈베리 파이는 정말 물건입니다. 이 쪼그만한게 별 걸 다하지요.

그래서 라즈베리에서 UDP 데이터를 직접 받고 직접 LED를 컨트롤하도록 개발하고자 합니다.

 

지금 필요한 준비물은.

라즈베리 파이와 이더넷을 micro usb로 바꿔주는 어댑터 입니다.

 

이더넷을 usb로 바꿔주는 제품을 참 많을텐데, 라즈베리에서 사용하기 가장 적절한 제품은 USB 허브와 이더넷이 합쳐진 게 아닐까요?

 

 

https://www.adafruit.com/product/2992

 

 

이 제품으로 PC와 라즈베리를 같은 네트워크에 묶어주면 UDP 패킷을 받을 수 있습니다.

와이어샤크로 패킷을 찍어본 결과, 브로드캐스트(255.255.255.255) 패킷이기 때문에 네트워크로 연결된 모든 장비에서 데이터를 받을 수 있습니다.

 

 

 

그러면 이제 Project Cars 2 데이터를 라즈베리에서 받을 수 있도록 코딩해봅시다!

공식 홈페이지에서 UDP 패킷 구조가 담겨있는 헤더 파일과 패킷을 받을 수 있는 Windows console 응용프로그램 샘플소스를 제공하지만,

그 샘플을 gcc 등의 Windows가 아닌 환경에서 컴파일하려고 했다가는 엄청난 에러를 만나겠죠 :)

라즈베리파이에서 돌아가는 소스를 올려드립니다.

 

main.cpp

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <stdlib.h>
 
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
#include <wiringPi.h>
 
#include "SMS_UDP_Definitions.hpp"
 
int sockfd_udp_cli;
 
void intHandler(int dummy)
{
    close(sockfd_udp_cli);
    exit(0);
}
 
int main(int argc, char** argv)
{
    signal(SIGINT, intHandler);
    
    if( wiringPiSetup() < )
    {
        return 1;
    }
    
    puts("\nLocoField SLI\n");
 
    char udp_recvbuff[SMS_UDP_MAX_PACKETSIZE];
    PacketBase packetHeader;
    memset(&packetHeader, 0sizeof(packetHeader));
 
    sockfd_udp_cli = socket(AF_INET, SOCK_DGRAM, 0);
    if( sockfd_udp_cli < )
    {
        puts("ERROR: create socket.\n");
        return 1;
    }
    
    //int flags = fcntl(sockfd_udp_cli, F_GETFL, 0);
    //fcntl(sockfd_udp_cli, F_SETFL, flags | O_NONBLOCK);
    
    struct timeval timeout;      
    timeout.tv_sec = 1;
    timeout.tv_usec = 0;
    setsockopt(sockfd_udp_cli, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout));
 
    
    sockaddr_in receiver;
    receiver.sin_family = AF_INET;
    receiver.sin_addr.s_addr = INADDR_ANY;
    receiver.sin_port = htons(SMS_UDP_PORT);
 
    int retval = bind(sockfd_udp_cli, (sockaddr*&receiver, sizeof(receiver));
    if( retval < )
    {
        puts("ERROR: bind socket.\n");
        return 1;
    }
    
    
    sTelemetryData data1;
    memset(&data1, 0sizeof(data1));
    
    while)
    {
        retval = recvfrom(sockfd_udp_cli, (char*)&udp_recvbuff, sizeof(udp_recvbuff), 0, NULL, 0);    
        if( retval > )
        {
            memcpy(&packetHeader, udp_recvbuff, sizeof(PacketBase));
            switch( packetHeader.mPacketType )
            {
            case eCarPhysics:
            {
                memcpy(&data1, udp_recvbuff, sizeof(data1));
            }
            default:
                continue;
            }
        }
    }
 
    intHandler(0);
    return 0;
}
 
cs

 

프로젝트의 종속성은 WiringPi 뿐이네요.

사실 이 프로젝트에서는 전혀 필요가 없어요. 라즈베리에서 GPIO 다룰 일이 많기 때문에 습관적으로 넣는 코드인거죠.

 

코드 자체는 정말 간단합니다.

 

signal 함수로 Ctrl+C로 종료할 때 소켓을 닫구요. 나중에 LED를 사용하게 되면 LED 끄는 코드를 넣으면 됩니다.

 

패킷은 UDP이므로 소켓은 데이터그램으로 만들고, 타임아웃을 설정합니다.

사실 논블록 소켓을 만들고 싶었는데 잘 되지는 않았네요...

 

그리고 recvfrom 함수로 데이터를 받았을 때 Project Cars 2에서 제공하는 구조체에 여러 값들을 복사해줍니다.

제가 소켓에 타임아웃을 설정했으니 받을 데이터가 없으면(게임에서 레이싱 상태가 아니라면)

조건문에서 계속 검사하며 while 문에서 계속 돌고 있겠지요.

 

라즈베리에서 바로 사용하실 수 있게 프로젝트 파일(CMakeLists.txt, MakeFile)도 올려드려요.

질문은 언제든지 환영입니다!

 

project.zip

 

 

참고 웹페이지:

[1] https://www.projectcarsgame.com/project-cars-2-api/

 

'DIY > Project Cars 2' 카테고리의 다른 글

Project Cars 2 SLI 완성!  (0) 2018.05.16
Raspberry Pi에서 Project Cars 2 UDP 데이터 받기  (2) 2018.03.20
  1. Parkseokhyeon 2018.10.24 20:28 신고

    프로젝트 카스2에서 라즈베리 파이로 속도와 RPM 변수를 받아와서 아날로그 계기판을 움직여

    속도 계기판을 만들고 싶습니다.

    가능할까요? 가능하다면 준비해야하는것은 무었일까요?

    • 로코필드 2018.10.26 16:18 신고

      결국엔 아날로그 계기판을 컨트롤 할 수 있어야겠죠? 차에 들어가는 계기판을 떼서 쓸 수 있을지는 모르겠네요.
      제가 당장 만들어야 된다면 스텝 모터를 이용해서 직접 계기판을 만들고 컨트롤하면 되겠네요.

라즈베리파이나 아두이노를 다루면서 숫자 출력할 일은 꼭 있습니다.

그럴 때 엘리베이터에서 쉽게 볼 수 있는 'seven-segment display' 또는 'flexible numeric display (FND)'를 이용합니다.

 

 

이렇게 무식하게 생겼고, 10개 핀이 달렸습니다.

그리고 이 모듈을 여러 개 달아서 대량의 정보를 표시하자니, 배선이 지옥입니다.

그래서 multiplexer를 이용하거나 Multiplexed 4 digit display 모듈을 판매하고 있습니다.

 

하지만, 4자리도 보여주고 싶은 정보를 보여주기에 부족하다면요?

MAX7219 칩이 달린 제품을 이용하시면 됩니다.

 

두 제품을 소개합니다.

1) MAX7219 dot matrix module

2) MAX7219 8-digit 7-segment LED display

 

앞에 MAX7219 이름이 붙는 이유는 이 이름의 칩을 사용한 모듈이라는 것입니다.

LED 형태에 따라 쉽게 제어를 할 수 있도록 만들어놨죠.

중국산(?)이라 그런지 자유롭게 사용가능한 제품 이미지도 구하기 힘들지만, 국내에서도 쉽게 구할 수 있는 좋은 제품입니다.

 

통신 방식은 DATA(MOSI), CS(chip select), CLK(clock) 3핀을 사용하는 SPI입니다.

그리고 MAX7219 제품끼리 여러 개를 물려서 사용할 수 있는 큰 장점이 있습니다.

 

저는 소개한 두 제품을 가지고 Project Cars 2 전용 SLI 제작하려고 합니다.

이번 포스팅에서는 라즈베리 파이에서 라이브러리 없이 C++로 MAX7219 모듈을 제어하는 방법을 소개합니다.

 

SLI가 무엇인지 궁금하다면?

2018/03/20 - [DIY/Project Cars 2] - Raspberry Pi에서 Project Cars 2 UDP 데이터 받기

 

 

 

MAX7219와 라즈베리 파이를 연결

 

라즈베리 파이에서 SPI 통신하는 핀은 정해져있기 때문에 핀맵은 달라질 수가 없겠네요.

WiringPI 기준으로 12 14 10번. 물리적인 핀 번호는 19 23 24번 입니다.

나머지 2개인 5v와 GND만 빠지지 않게 잘 연결해주면 되겠습니다.

 

 

 

MAX7219.h

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// define pins for Raspberry PI
#define DATA        12
#define CLOCK       14
#define LOAD        10
 
// MAX7219 Registers
#define DECODE_MODE   0x09
#define INTENSITY     0x0a
#define SCAN_LIMIT    0x0b
#define SHUTDOWN      0x0c
#define DISPLAY_TEST  0x0f
 
void send_SPI_16bits(unsigned short data)
{
    for (int i = 16; i > 0; i--)
    {
        /* bitmask */
        unsigned short mask = << (i - 1);
 
        /* send data */
        digitalWrite(CLOCK, 0);
        digitalWrite(DATA, (data & mask) ? 0);
        digitalWrite(CLOCK, 1);
    
        /* no receive data */
        //digitalWrite(CLOCK, 0);
    }
}
 
void send_MAX7219(unsigned short reg_number, unsigned short data1, unsigned short data2)
{
    digitalWrite(LOAD, 1);
    send_SPI_16bits((reg_number << 8+ data2);
    send_SPI_16bits((reg_number << 8+ data1);
    digitalWrite(LOAD, 0); // to latch
    digitalWrite(LOAD, 1);
}
 
cs

 

MAX7219를 제어하기 위한 함수 2개가 구현된 헤더 파일입니다.

네, 알고 있어요... 억지로 2개를 제어하겠다는 막코드죠 ㅋㅋㅋㅋ

 

MAX7219를 제어하기 위한 함수가 있구요, 모듈이 2개이니 data1과 data2를 같이 입력받습니다.

언젠가..는 초기화 과정에서 모듈 개수를 입력받아 유연하게 데이터를 입력받는 클래스로 구현해야겠죠?

 

다른 함수는 SPI 프로토콜에 맞게 16비트 데이터를 보내는 함수입니다.

MAX7219에 맞게 구현했으니 당연히 데이터를 받지 않고 보내기만 하고 있습니다. 16번 반복하면서요.

 

그럼 이 함수를 사용하는 예제를 살펴볼까요?

 

 

main.cpp

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <stdlib.h>
 
#include <wiringPi.h>
 
#include "max7219.h"
 
void intHandler(int dummy)
{
    send_MAX7219(SHUTDOWN, 00);
    exit(0);
}
 
int main(int argc, char** argv)
{
    signal(SIGINT, intHandler);
    
    if( wiringPiSetup() < )
    {
        return 1;
    }
 
    pinMode(DATA, OUTPUT);
    pinMode(CLOCK, OUTPUT);
    pinMode(LOAD, OUTPUT);
 
    send_MAX7219(SCAN_LIMIT, 77);
    send_MAX7219(DECODE_MODE, 00);
    send_MAX7219(INTENSITY, 88);
    send_MAX7219(SHUTDOWN, 11);
 
    int digit_table[11= { 12648109121519195114127123};
    int dot_table[9][9=
    { // 128 64 32 16 8 4 2 1
    { 000015000}, // N
    { 01111111}, // 1
    { 01511158815}, // 2
    { 01511151115}, // 3
    { 099915111}, // 4
    { 01588151115}, // 5
    { 01588159915}, // 6
    { 015999111}, // 7
    { 000015888}, // R
    };
    
    send_MAX7219(1, digit_table[0], dot_table[0][1]);
    send_MAX7219(2, digit_table[0], dot_table[0][2]);
    send_MAX7219(3, digit_table[0], dot_table[0][3]);
    send_MAX7219(4, digit_table[0], dot_table[0][4]);
    send_MAX7219(5, digit_table[0], dot_table[0][5]);
    send_MAX7219(6, digit_table[0], dot_table[0][6]);
    send_MAX7219(7, digit_table[0], dot_table[0][7]);
    send_MAX7219(8, digit_table[0], dot_table[0][8]);
 
    //intHandler(0);
    return 0;
}
cs

 

25-27 줄에서 SPI 통신에 사용할 3핀을 WiringPI로 초기화해줍니다.

그 다음으로, MAX7219 모듈을 초기화하는 코드가 나옵니다.

 

1) SCAN_LIMIT 명령어로 초기화를 해주는데요, 값을 바꿀 이유는 없을 것 같습니다.

 

2) DECODE_MODE는 제가 작성한 예제에서는 사용하지 않습니다.

디코드 모드를 사용하면 FND 모듈에서 출력 가능한 숫자를 포함한 문자를 그대로 출력할 수 있습니다.

0부터 15까지 데이터를 보내서 7 segment display 답게 0-9 숫자 또는 A B C D E F 문자를 출력할 수 있습니다

라고 알고 있는데, 그럼 .은 어떻게 출력하지?

디코드 모드는 자세히 살펴보지 않아 정확한 내용은 datasheet를 봐야할 것 같네요.

 

디코드 모드를 사용하지 않는다면, 8비트 마스크를 OR 비트 연산한 값인 (비트가 겹치지 않으니 더하면 됩니다.) 0-255 사이의 데이터를 보내야 합니다.

필요에 따라 출력할 숫자의 비트 마스크를 미리 계산하고 테이블로 만들어두고 배열로 출력하면 됩니다. 저처럼 말이죠, 이게 더 편해요.

 

3) INTENSITY는 말그대로 밝기입니다.

15까지 올릴 수 있는데, 너무 밝으니 눈이 아프더라구요.

 

4) SHUTDOWN은 ON OFF 입니다.

define 이름이 이상하게 보이는데, 1을 보내서 키고 0을 보내서 쉽게 끌 수 있습니다.

 

 

34 라인은 FND 모듈의 숫자 테이블입니다. 0-9 숫자에다가 부분적으로 끄기 위해 맨 마지막에 0을 추가했습니다.

. 을 찍고 싶으면 테이블 값에 128을 더하면 됩니다.

 

35 라인은 도트 매트릭스에서 자동차 기어를 표시하기 위해 ---- (Neutral), 1-7, ┌ (Reverse) 를 추가했습니다.

8x8 도트이니 당연히 2차원 배열이죠.

숫자를 예쁘게 출력하기 위해 오른쪽에 치우쳐 출력했고, 왼쪽 빈 공간에는 다른 정보를 출력할 수 있겠군요.

 

48-55 라인은 초기화가 완료되었음을 표시하기 위해

위에서 만들어놓은 테이블을 참조하여 FND 모듈과 도트 매트릭스 모듈에 각각 00000000 과 ---- 를 표시하는 코드입니다.

 

그리고 LED는 켜진 채 종료됩니다.

종료를 시키지 않는 경우에는 57 라인의 주석을 해제하여 메인함수가 종료되기 전 LED를 끌 수 있습니다.

물론, 핸들러 덕분에 Ctrl+C로 종료할 때 역시 LED가 꺼집니다.

 

 

지금까지 두 개의 MAX7219 모듈에 제가 원하는 정보를 출력할 수 있었습니다.

그리고, 아주 간단했습니다. 제 주관적인 생각으로는 라이브러리를 사용하여 이 모듈을 다루면 너무 무거워지는 것 같네요.

물론 테이블을 만들 때 약간의 노가다가 필요하죠.

여러분은 어떤 정보를 출력하시나요? 유용한 테이블은 공유해요~

 

 

참고자료:

[1] https://en.wikipedia.org/wiki/Seven-segment_display

[2] https://en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus

 

'코딩 > Raspberry PI' 카테고리의 다른 글

Raspberry Pi에서 MAX7219 제어하기  (0) 2018.03.16

+ Recent posts