본문 바로가기

ESP32

ESP-32 LVGL 그래픽을 사용한 고급 기술 - 3부

반응형

ESP-32 및 LVGL 그래픽을 사용한 고급 기술 - 3부

 

2과를 마치면서 스프라이트를 사용하여 속도계 화면을 표시하고 바늘 움직임을 애니메이션으로 구현하는 코드를 개발했습니다. 3과에서는 속도와 이동 거리를 측정하고 총 실행 시간을 추적하는 코드를 추가할 것입니다. 이 정보는 ESP32의 비휘발성 플래시 메모리에 저장하여 시스템이 꺼져도 사라지지 않도록 할 것입니다. 또한 홀 효과 센서를 사용하여 이벤트를 트리거하고 측정하는 방법도 배우겠습니다. 이 과정을 마치면 최종 시스템에 한 걸음 더 다가갈 수 있을 것입니다. 시작해 보겠습니다.

 

 

 

 

코딩 접근 방식

 

새로운 코드를 한꺼번에 모두 살펴보는 대신, 여러 주제를 개별적으로 살펴보고 마지막에 통합할 것입니다. 여기서 다루는 기술 중 일부는 여러분의 프로젝트에 유용할 수 있습니다. 하지만 여기서 다루는 속도계 프로젝트와는 전혀 관련이 없습니다. 다음 순서대로 주제를 살펴보겠습니다.

 

1. ESP32의 플래시 메모리를 사용하여 정보를 정의, 저장 및 검색하는 코드를 설정합니다. 이를 사용하여 1/10시간 단위로 측정된 총 주행 시간과 1/10마일 단위로 측정된 총 거리를 저장합니다. 또한 자동차의 주행 거리계와 유사한 시스템을 구현하기 위해 주행 시간과 주행 거리 값을 저장합니다.

 

2. 6분(1/10시간)마다 코드를 트리거하는 타이머 함수를 사용하여 실행 시간 변수(총계 및 이동 시간)를 업데이트하는 코드를 설정합니다.

 

3. CrowPanel 장치의 GPIO_D 연결을 사용하여 홀 효과 센서를 시스템에 연결합니다. 이 센서를 사용하여 바퀴의 각 회전을 감지하고, 이를 통해 이동 거리와 현재 속도를 계산합니다.

 

4. 이동 거리가 1/10마일에 도달할 때마다 이동 거리 변수(총 거리와 구간 거리)를 업데이트하고, 속도계 디스플레이에 1/2초마다 현재 속도를 업데이트하도록 코드를 설정합니다.

 

5. 마지막으로, 시간당 마일(MPH)과 시간당 킬로미터(KPH)로 디스플레이를 전환할 수 있는 코드를 추가합니다.

 

마지막으로 필요한 모든 정보를 수집하고 속도계에 현재 속도를 표시하는 시스템을 구축할 것입니다. 다음 레슨에서 블루투스 연결을 구현하여 데이터를 가져올 때까지는 주행 시간과 거리 값을 읽을 수 없습니다.

 

애플리케이션 코드

 

1부. ESP32 플래시 메모리에서 비휘발성 정보를 정의, 저장 및 검색하기 위해 Arduino IDE의 ESP32 프로세서에만 있는 Preferences라는 특수 라이브러리를 사용합니다. 자세한 내용은 여기를 참조하세요. 이 라이브러리는 시스템의 표준 라이브러리이며, 아래와 같이 include 문을 추가하고 Preferences 객체를 생성하기만 하면 호출할 수 있습니다.

 

// add the include statement near top of code
#include <Preferences.h> // library to use flash memory for variables

... other code

//create a Preferences object in definitions section
Preferences preferences; // set up non-volatile storage object

 

이제 아래와 같이 환경 설정 라이브러리를 초기화하고 필요한 데이터를 가져오는 루틴을 추가하겠습니다.

 

void getFlashData()
{
  preferences.begin("logData", false);
  hoursElapsed = preferences.getFloat("hoursElapsed", 0); 
  hoursTrip = preferences.getFloat("hoursTrip", 0);
  distElapsed = preferences.getFloat("distElapsed", 0);
  distTrip = preferences.getFloat("distTrip", 0);
  distMode = preferences.getInt("distMode", 0);
}

 

 

첫 번째 줄은 logData라는 데이터 컬렉션을 찾아 읽기/쓰기 모드(false 인수)로 열도록 지시합니다. 해당 컬렉션이 없으면 시스템은 새 컬렉션을 생성합니다. 다음 각 줄은 logData 컬렉션에 저장된 변수의 값을 현재 프로그램의 변수에 할당합니다. 컬렉션에 지정된 이름의 항목이 없으면 두 번째 인수 값(0)을 반환하고 지정된 이름과 값으로 컬렉션에 새 항목을 생성합니다. 이렇게 하면 컬렉션의 초기 할당을 처리하기 위한 추가 코드가 필요하지 않습니다. 프로그램 전체에서 변수를 사용할 수 있도록 프로그램의 설정 코드에서 이 루틴을 호출합니다.

 

2부. 다음 단계는 위에서 정의한 hoursElapsed 및 hoursTrip 변수를 업데이트할 수 있도록 타이머를 추가하는 것입니다. Alexander Kiryanenko가 만든 SimpleTimer라는 라이브러리를 사용하겠습니다. Arduino IDE에서 도구 > 라이브러리 관리…로 이동하여 Simple Timer를 검색하세요. 그런 다음 설치를 클릭하여 시스템에 추가하세요.

 

 

 

 

이제 아래와 같이 SimpleTimer를 활용하기 위해 프로그램에 코드를 추가할 수 있습니다.

 

// add the include statement near top of code
#include <SimpleTimer.h> // library for interupt timer

... other code

//create a SimpleTimer object in definitions section
SimpleTimer HourTimer; // timer used to track 1/10 hour

... other code

// lines below added to the setup code
// Set an interval to 6 minutes for the timer (0.10 hours)
  HourTimer.setInterval(360000); // interval time is in milliseconds

... other code

// add a routine to be called in the main loop to check fhe timer and respond
// each 1/10th hour
void checkTimer()
{
  if (HourTimer.isReady()) 
    {                    
        Serial.println("Called every 6 minutes");
        hoursElapsed += 0.1;
        hoursTrip += 0.1;
        preferences.putFloat("hoursElapsed",hoursElapsed);
        preferences.putFloat("hoursTrip",hoursTrip);
        Serial.print("Total Hours: ");
        Serial.println(hoursElapsed,1);
        Serial.print("Trip Hours: ");
        Serial.println(hoursTrip,1);
        HourTimer.reset();                        
    }
  }

 

 

위 코드 섹션을 살펴보면, HourTimer라는 SimpleTimer 객체를 생성하고 360000밀리초(1/10시간)마다 타이머가 울리도록 설정한 후, 코드를 반복할 때마다 타이머가 울렸는지 확인합니다. 만약 울렸다면 hoursTrip과 hoursElapsed를 업데이트하고 해당 정보를 직렬 포트에 출력합니다. 마지막으로, 타이머를 다시 1/10시간으로 재설정합니다. 시간 데이터를 업데이트하는 데 사용된 코드를 살펴보세요.

 

        hoursElapsed += 0.1;
        hoursTrip += 0.1;
        preferences.putFloat("hoursElapsed",hoursElapsed);
        preferences.putFloat("hoursTrip",hoursTrip);

 

 

이는 putFloat 선호 함수가 플래시 메모리를 업데이트하는 데 어떻게 사용되는지 보여줍니다. 시스템 전원이 꺼졌다가 다시 시작되면 플래시 메모리로 전송된 마지막 값을 다시 사용할 수 있습니다. 플래시 메모리에 데이터를 얼마나 자주 쓰고 다시 쓸 수 있을까요? ESP32 사양서에는 최소 100,000회의 쓰기/삭제 주기가 명시되어 있지만, 실제로는 훨씬 더 높은 주기를 사용하는 경우가 많습니다.

 

또 다른 의견이 있습니다. 매 루프마다 한 번씩 확인하는 타이머 대신 시간 정보를 기록하는 데 시간 제한 인터럽트를 사용할 수도 있었지만, 360000밀리초 후에 정확히 1/10시간 데이터를 업데이트할 필요는 없습니다. 타이머는 시스템 요구 사항을 충족하는 더 간단한 솔루션을 제공합니다.

 

3부. 이제 실제 데이터를 가져올 수 있도록 홀 효과 센서를 추가하겠습니다. 홀 효과 센서는 자기장에 비례하는 전기 신호를 생성하는 방법을 발견한 물리학자 에드윈 홀의 이름을 따서 명명되었습니다(자세한 내용은 여기 참조 ). 이 센서는 습기, 먼지, 온도, 진동에 영향을 받지 않는 비접촉 신호를 생성하는 좋은 방법을 제공하며, 골프 카트 환경에 이상적입니다. 또한 가격도 저렴합니다. 여기에 표시된 것처럼 아마존에서 약 6달러에 5개와 편리한 자석을 구입할 수 있었습니다 . 골프 카트의 뒷바퀴 허브에 자석을 붙인 다음, 바퀴가 한 바퀴 회전할 때마다 센서가 자석에서 약 1/8인치(약 0.6cm) 이내에 오도록 센서를 부착했습니다. 장착을 더 쉽게 하고 날씨로부터 보호하기 위해, 회로 기판을 짧은 PVC 파이프에 넣고 센서를 회로 기판에 90도 구부린 상태로(아래 그림 참조) 파이프 끝부분 약간 안쪽에 위치시켰습니다. 그런 다음 핫 글루건으로 파이프에 접착제를 채워 캡슐화했습니다. 두 개의 호스 클램프로 후방 차축에 고정해 견고한 마운트를 제공했고, 지금까지 몇 달 동안 잘 작동했습니다.

 

 

파이프에 보드를 밀어 넣고 제자리에 붙이기 전 모습

 

프로그래밍 관점에서, 센서를 CrowPanel 장치의 I/O 핀에 연결한 다음, 센서가 트리거될 때마다 인터럽트 이벤트를 사용하여 이동 속도와 이동 거리를 업데이트합니다. 하지만 먼저 센서 펄스를 이동 거리와 시속 마일로 변환하는 방법을 계산해 보겠습니다.

 

바퀴 지름을 알면 바퀴가 한 바퀴 회전할 때 이동한 거리를 PI(3.14)에 지름을 곱하여 계산할 수 있습니다. 바퀴 지름을 17인치로 설정하면 마일당 회전 수는 다음과 같이 구할 수 있습니다.

 

rev/mile = 5280*12/(PI*wheelDiameter) = 1187

 

 

1/10마일마다 거리를 기록하려면 바퀴가 119바퀴 회전할 때마다 업데이트해야 합니다.

 

바퀴가 한 바퀴 회전하는 데 걸리는 시간(revTime)을 밀리초 단위로 측정하면, 이를 이용해 시속 마일로 속도를 결정할 수 있습니다.

 

RPM = rev/min = 60000/revTime
MPH = miles/hour = wheelDia * PI * RPM * 60 / (12 * 5280)

 

 

숫자를 재배열하면 1MPH에서의 revTime은 약 3,033밀리초, 즉 3초가 조금 넘는다는 것을 알 수 있습니다. 20MPH에서의 revTime 값은 약 152밀리초입니다. 이 시간은 ESP32의 감지 및 반응 범위 내에 있습니다.

 

이제 센서를 연결해 보겠습니다. 홀 효과 센서는 이렇게 생겼습니다.

 

 

 

세 개의 리드에 대한 연결이 있습니다. "-"로 표시된 것은 접지선 연결입니다. 가운데 연결은 "+" 배터리 전압(3.3V)에 연결되고, "S"로 표시된 것은 장치의 I/O 핀에 연결됩니다. CrowPanel에는 GPIO_D라고 표시된 커넥터에 꽂을 수 있는 4선 커넥터가 있습니다. 이 중 세 개의 와이어를 아래와 같이 연결하여 사용하겠습니다.

 

  • 검은색 전선 - 배터리의 음극이 "-" 커넥터에 연결됨
  • 빨간색 전선 - 배터리 양극이 중앙 커넥터에 연결됨
  • 흰색 와이어 - "S" 커넥터에 연결된 센서 와이어
  • 노란색 전선 - 사용하지 않음

 

흰색 선은 ESP32의 32번 디지털 I/O 포트에 내부적으로 연결되어 있습니다. 홀 효과 센서 선(흰색 선)은 근처에 자석이 감지되면 HIGH 전압에서 LOW 전압으로 바뀌며, 센서에 빨간색 LED가 켜집니다. 센서는 자기장의 방향에 민감하므로 한쪽 자극 방향에서는 작동하고 다른 쪽 자극 방향에서는 작동하지 않는다는 점에 유의하세요. 자석을 센서 근처에 놓고 어떤 자석 방향에서 센서가 작동하는지 확인하여 테스트할 수 있습니다. 그런 다음 센서 근처에 있어야 하는 자석 면을 표시하여 올바르게 설치할 수 있도록 합니다.

 

이제 코드를 살펴보겠습니다. 이번에도 홀 효과 센서와 관련된 코드만 살펴보고 나중에 전체 코드에 통합하겠습니다.

 

업데이트 : 아래 코드는 원본 버전에서 수정되었습니다. 홀 효과 센서가 바퀴가 한 바퀴 회전할 때마다 인터럽트를 여러 번 트리거하는 것을 발견했습니다. 스위치를 디바운스하는 데 자주 사용되는 코드와 유사하게, 추가 인터럽트를 무시하는 코드를 추가했습니다.

 

// setup for hall effect sensor & speed data
const byte intPin = 32;
const float wheelDia = WHEELDIA;
float rpm;
float speedMPH;
volatile int startTime = 0;
volatile int revTime = 0;
volatile int wheelCount = 0;

// interrupt routine to get revolution time and count
void IRAM_ATTR wheelRev()
{
  noInterrupts(); // stop new interrupts while processing
  if (millis() - startTime < 100){return;} // ignore interrupts less than 100 msec apart
  revTime = (millis() - startTime);
  wheelCount += 1;
  interrupts(); // restart interrupt processing
  startTime = millis();
}

// code added to setup routine for the hall effect sensor
// initialize hall sensor for speed
  startTime = millis();
  digitalWrite(intPin, HIGH); // Enable internal pull-up resistor
  attachInterrupt(digitalPinToInterrupt(intPin),wheelRev,FALLING);

 

위에 표시된 것처럼 홀 효과 센서를 다루는 세 개의 작은 코드 섹션이 있습니다.

 

첫 번째 섹션은 프로그램에서 사용되는 변수를 정의하며, 여기에는 연결된 I/O 핀을 나타내는 인터럽트 핀 번호(intPin)도 포함됩니다. 여러 변수가 volatile로 표시되어 있는 것을 확인할 수 있습니다. 이는 컴파일러에게 인터럽트 프로세스 내에서 해당 변수들이 수정될 수 있으므로 다르게 처리해야 함을 알려줍니다.

 

두 번째 섹션은 인터럽트가 발생할 때마다 실행되는 루틴입니다. 이 코드는 작고 빠르게 작성하는 것이 가장 좋습니다. 이 경우에는 마지막 바퀴 회전 시간을 계산하고 바퀴 수를 증가시킵니다. "IRAM_ATTR" 속성은 컴파일러에게 이 코드가 인터럽트 루틴에서 사용되므로 효율적으로 처리하기 위해 다르게 처리해야 함을 알려줍니다. 또한 잘못된 트리거를 방지하기 위해 100밀리초보다 짧은 간격의 인터럽트는 무시합니다. 예상되는 최고 속도(24MPH)에서 펄스는 126밀리초 간격으로 발생합니다. 더 짧은 펄스 주기를 무시하면 발생할 수 있는 잘못된 트리거를 걸러낼 수 있습니다. 다른 회로에서 동일한 홀 효과 스위치를 사용했지만 이 문제가 발생하지 않았지만, 3.3V가 아닌 5V로 작동하는 프로세서 보드를 사용했습니다. 센서는 3.3V 또는 5V 사용에 적합하도록 설계되었지만, ESP32의 낮은 전압이 HIGH에서 LOW로의 전압 전환을 느리게 만들어 잘못된 인터럽트를 발생시키는 것으로 보입니다.

 

마지막으로, 설정 루틴에서 startTime을 현재 밀리초로 설정하고, intPin 풀업 저항에 HIGH 신호를 인가하여 설정하고, 인터럽트 프로세서를 설정합니다. intPin 변수에 매핑된 I/O 핀에서 FALLING 신호가 감지되면 wheelRev 루틴이 트리거됩니다.

 

이게 전부입니다. 이제 바퀴가 한 바퀴 돌 때마다 자동으로 업데이트되는 revTime과 wheelCount라는 전역 변수를 만들겠습니다.

 

4부. 이동 거리와 현재 속도를 계산하는 코드는 updateWheel() 이라는 루틴에 포함되어 있으며 , 이 루틴은 아래와 같이 약 500밀리초마다 메인 루프에서 호출됩니다.

 

void updateWheel()
{
  if (wheelCount > 119) //check for 0.1 mile distance
    {
      preferences.putFloat("distElapsed", distElapsed += 0.1);
      preferences.putFloat("distTrip", distTrip += 0.1);
      wheelCount = 0;
    }
    
  if (revTime >0)
    {
    rpm = float(60000/revTime);
    speedMPH = wheelDia * PI * rpm * 60 / (12 * 5280);
    if (speedMPH < maxSpeed)
    {
      updateGauge(int(speedMPH));
      yield();
    }
  }
}

 

첫 번째 섹션에서는 wheelCount를 마지막으로 재설정한 이후로 1/10마일을 이동했는지 확인하고, 그렇다면 이전에 본 것처럼 preference 객체를 사용하여 플래시 메모리에 있는 distElapsed 및 distTrip 변수를 업데이트합니다.

 

두 번째 부분에서는 이전에 논의한 수학을 기반으로 속도를 업데이트합니다. 먼저 revTime이 0이 아닌지 확인하여 0으로 나누는 오류를 방지합니다. 또한 계산된 속도가 표시 가능한 범위 내에 있는지 확인합니다. 잘못된 신호가 수신되어 이상한 결과가 나오는 경우 무시합니다. 계산된 속도가 올바른 범위 내에 있으면 2과에서 작성한 updateGauge 루틴을 사용하여 화면에 표시합니다.

 

고려해야 할 사항이 하나 더 있습니다. 바퀴가 회전을 멈추면 어떻게 될까요? wheelRev 인터럽트 루틴이 트리거되지 않았으므로 표시된 속도는 동일하게 유지됩니다. 마지막 트리거 이후 경과 시간이 3.5초보다 큰지 확인하여 이 문제를 해결할 수 있습니다. 경과 시간이 3.5초를 초과하면 바퀴가 정지했거나 시속 1마일(1MPH) 미만으로 회전하고 있음을 의미합니다. 0이라고 가정하겠습니다. 이 확인 작업은 아래와 같이 updateWheel() 루틴을 호출하기 전에 메인 루프 루틴에서 수행됩니다.

 

void loop() 
{
  // check to see if 6 minute timer is done
  checkTimer();
  // check to see if wheel is stopped
  if (millis() - startTime > 3500) 
  {
    updateGauge(0); // if stopped set gauge to 0
  }
  else 
  {
    updateWheel(); // not stopped update speed display
  }
  delay(500);
}

 

5부. 마지막 루틴에서는 속도를 KPH(시속 킬로미터)로, 거리를 KM(킬로미터)로 표시합니다. 관련 코드는 아래와 같습니다.

 

// add define statements for speedometer fuctions
#define WHEELDIA 17.0 // wheel diameter in inches
#define MI 0 // display distance in miles
#define KM 1 // display distance in kilometers

// variables used for the display conversion 
int distMode = MI;
String speedLabel = "MPH";

int displaySpeed(float speedMPH)
{
  if (distMode == KM)
    {
      speedLabel = "KPH";
      return int(speedMPH * 1.609)
    }
  else
    {return int(speedMPH)}
}

// updated code to check the distMode when updating the speedometer display
void updateWheel()
{
  if (wheelCount > 119) //check for 0.1 mile distance
    {
      preferences.putFloat("distElapsed", distElapsed += 0.1);
      preferences.putFloat("distTrip", distTrip += 0.1);
      wheelCount = 0;
    }
    
  if (revTime >0)
    {
    rpm = float(60000/revTime);
    speedMPH = wheelDia * PI * rpm * 60 / (12 * 5280);
    //Serial.println(speedMPH);
    if (speedMPH < maxSpeed)
    {
      updateGauge(displaySpeed(speedMPH)); // modified to check distMode
      yield(); 
    }
  }
}

 

코드에서 거리 모드를 전환할 수 있도록 두 가지 정의(MI와 KM)를 정의합니다. 그런 다음 distMode 변수를 생성하고 기본값을 MI로, speedLabel 변수를 "MPH"로 설정합니다.

 

그런 다음 distMode 설정(KM 또는 MI)을 확인하기 위해 displaySpeed() 루틴 을 만든 다음 speedLabel을 전환하고 필요한 경우 속도에 변환 계수를 적용합니다.

 

마지막으로 속도계 판독값을 업데이트할 때 displaySpeed ​​루틴을 호출하도록 updateGauge() 루틴 의 코드를 수정합니다 .

 

모든 것을 합치면 완전한 코드는 다음과 같습니다.

 

// code for Gauge_CrowPanel_Lesson3.ino

// add define statements for speedometer fuctions
#define WHEELDIA 17.0 // wheel diameter in inches
#define MI 0 // display distance in miles
#define KM 1 // display distance in kilometers 

// add includes for Arduino libraries
#include <SimpleTimer.h> // library for interupt timer
#include <Preferences.h> // library to use flash memory for variables
#include "BluetoothSerial.h" // library to use ESP32 bluetooth

// add variable definitions for odometer functions
float hoursElapsed;
float hoursTrip;
float distElapsed;
float distTrip;
int distMode = MI;

// setup for hall effect sensor & speed data
const byte intPin = 32;
const float wheelDia = WHEELDIA;
float rpm;
float speedMPH;
volatile int startTime = 0;
volatile int revTime = 0;
volatile int wheelCount = 0;

// define new objects related to speedometer functions
Preferences preferences; // set up non-volatile storage object
BluetoothSerial SerialBT;
SimpleTimer HourTimer; 

// setup for LCD screen
#include <TFT_eSPI.h> // include graphics library
#define DEG2RAD 0.0174532925
#define COLOR_BORDER TFT_BLUE
#define COLOR_NEEDLE TFT_RED
#define BACKGROUND TFT_DARKGREY

// constants for the display geometry
const int lcdWidth = 240;
const int lcdHeight = 320;
const int gaugeRad = 110;
const int gaugeWidth = 25;
const int gaugeWidth2 = 10;
const int xLoc = lcdWidth / 2;
const int yLoc = lcdHeight / 2;
const int numOffset = 40;

// define TFT objects for display
TFT_eSPI tft = TFT_eSPI(); // Device display
TFT_eSprite display = TFT_eSprite(&tft); //  Sprite for dial face
TFT_eSprite dial = TFT_eSprite(&tft); // Sprite for dial background
TFT_eSprite needle = TFT_eSprite(&tft); // Sprite for needle

// setup constants for the speedometer app
const float minSpeed = 0;
const float maxSpeed = 24;
const float speedRange = maxSpeed - minSpeed;
String speedLabel = "MPH";

void getFlashData()
{
  preferences.begin("logData", false);
  hoursElapsed = preferences.getFloat("hoursElapsed", 0); 
  hoursTrip = preferences.getFloat("hoursTrip", 0);
  distElapsed = preferences.getFloat("distElapsed", 0);
  distTrip = preferences.getFloat("distTrip", 0);
  distMode = preferences.getInt("distMode", 0);
}

void checkTimer()
{
  if (HourTimer.isReady()) 
    {                    
        Serial.println("Called every 6 minutes");
        hoursElapsed = preferences.getFloat("hoursElapsed", 0); 
        hoursTrip = preferences.getFloat("hoursTrip", 0);
        hoursElapsed += 0.1;
        hoursTrip += 0.1;
        preferences.putFloat("hoursElapsed",hoursElapsed);
        preferences.putFloat("hoursTrip",hoursTrip);
        Serial.print("Total Hours: ");
        Serial.println(hoursElapsed,1);
        Serial.print("Trip Hours: ");
        Serial.println(hoursTrip,1);
        HourTimer.reset();                        
    }
  }

// interrupt routine to get revolution time and count
void IRAM_ATTR wheelRev()
{
  noInterrupts(); // stop new interrupts while processing
  if (millis() - startTime < 100){return;} // ignore interrupts less than 100 msec apart
  revTime = (millis() - startTime);
  wheelCount += 1;
  interrupts(); // restart interrupt processing
  startTime = millis();
}

void drawBackground(float speed, String label)
{
  // draw outline and labels onto the dial sprite
  dial.setColorDepth(8);
  dial.createSprite(lcdWidth, lcdHeight);
  dial.setPivot(lcdWidth/2, lcdHeight/2);
  dial.fillScreen(BACKGROUND);
  dial.fillCircle(lcdWidth/2, lcdHeight/2, gaugeRad + 10, COLOR_BORDER);
  dial.fillCircle(lcdWidth/2, lcdHeight/2, gaugeRad, TFT_BLACK);
  //dial.fillCircle(lcdWidth/2, lcdHeight/2, 10, TFT_WHITE); // mark dial center
  dial.setTextDatum(MC_DATUM); // locate text graphics by center position
  dial.drawString("MPH",120,240,4); //label gauge MPH

  //draw major and minor tick marks - 3 minor ticks per major mark
    for (float i = 45; i < 316; i+=(3*270/speedRange)) // major marks
    {
      float sx = cos((i - 270) * DEG2RAD);
      float sy = sin((i - 270) * DEG2RAD);
      uint16_t x0 = sx * (gaugeRad - gaugeWidth -1) + xLoc;
      uint16_t y0 = sy * (gaugeRad - gaugeWidth -1) + yLoc;
      uint16_t x1 = sx * (gaugeRad-1) + xLoc;
      uint16_t y1 = sy * (gaugeRad-1) + yLoc;    
      dial.drawWideLine(x0, y0, x1, y1,3,TFT_WHITE); 
    }
    for (float j = 45; j < 315; j+=(270/speedRange)) //minor marks
    {
      float sx = cos((j - 270) * DEG2RAD);
      float sy = sin((j - 270) * DEG2RAD);
      uint16_t x0 = sx * (gaugeRad - gaugeWidth2-1) + xLoc;
      uint16_t y0 = sy * (gaugeRad - gaugeWidth2-1) + yLoc;
      uint16_t x1 = sx * (gaugeRad-1) + xLoc;
      uint16_t y1 = sy * (gaugeRad-1) + yLoc;    
      dial.drawLine(x0, y0, x1, y1, TFT_WHITE);
    }
  //draw number labels
    // int dspNum = minSpeed;
    // for (float i = 45; i < 316; i+=(3*270/speedRange)) // number labels by major marks
    // {
    //   float sx = cos((i - 270) * DEG2RAD);
    //   float sy = sin((i - 270) * DEG2RAD);
    //   uint16_t x0 = sx * (gaugeRad - numOffset -1) + xLoc;
    //   uint16_t y0 = sy * (gaugeRad - numOffset -1) + yLoc;
    //   dial.drawNumber(dspNum, x0, y0,2);
    //   dspNum += 3; 
    // }
}

void createNeedle()
{
  needle.setColorDepth(8); // use 8 bit colors to reduce memory rqd
  needle.createSprite(20,120); // make a sprite 20 by 120 pixels
  needle.fillSprite(TFT_TRANSPARENT); //make sprite background transparent
  //needle.drawWedgeLine(10,0,10,110,6,2,COLOR_NEEDLE,TFT_TRANSPARENT); // define needle geometry
  needle.drawWedgeLine(10,70,10,110,8,4,COLOR_NEEDLE,TFT_TRANSPARENT);  // define needle geometry
  needle.setPivot(10,10); // set pivot point at needle axis
}

void updateGauge(float speed)
{
  // calculate needle angle based on speed
  int angle = (speed-minSpeed)*270/(maxSpeed-minSpeed) + 45; // calculate needle angle   
  dial.pushToSprite(&display,0,0,TFT_TRANSPARENT); // add the dial background to display sprite 
  display.setTextDatum(MC_DATUM); // place text based on middle center origin
  display.drawNumber(speed, 120, 160, 8); // insert the speed text
  needle.pushRotated(&display,angle, TFT_TRANSPARENT); // add the needle to display sprite at correct angle
  display.pushSprite(0,0,TFT_TRANSPARENT); // push the display sprite to the screen (this method reduces flicker) 
}

void updateWheel()
{
  if (wheelCount > 119) //check for 0.1 mile distance
    {
      preferences.putFloat("distElapsed", distElapsed += 0.1);
      preferences.putFloat("distTrip", distTrip += 0.1);
      wheelCount = 0;
    }
    
  if (revTime >0)
    {
    rpm = float(60000/revTime);
    speedMPH = wheelDia * PI * rpm * 60 / (12 * 5280);
    //Serial.println(speedMPH);
    if (speedMPH < maxSpeed)
    {
      updateGauge(displaySpeed(speedMPH));
      yield(); 
    }
  }
}

int displaySpeed(float speedMPH)
{
  if (distMode == KM)
    {
      speedLabel = "KPH";
      return int(speedMPH * 1.609);
    }
  else
    {return int(speedMPH);}
}

void setup(void) 
{
  Serial.begin(115200);
  delay(200);
  // load the flash data values into memory
  getFlashData();
  // Set an interval to 6 minutes for the timer (0.10 hours)
  HourTimer.setInterval(360000);
  // initialize hall sensor for speed
  startTime = millis();
  digitalWrite(intPin, HIGH); // Enable internal pull-up resistor
  attachInterrupt(digitalPinToInterrupt(intPin),wheelRev,FALLING);
  // initialize gauge screen
  tft.begin();
  tft.setRotation(0);
  // initialize display sprite
  display.setColorDepth(8);
  display.createSprite(lcdWidth, lcdHeight);
  display.setPivot(lcdWidth/2, lcdHeight/2);
  display.fillSprite(BACKGROUND);
  drawBackground(0,speedLabel); // create dial sprite 
  createNeedle(); // create needle sprite
  updateGauge(0);
}

void loop() 
{
  // check to see if 6 minute timer is done
  checkTimer();
  
  // check to see if wheel is stopped
  if (millis() - startTime > 3500) 
  {
    updateGauge(0); // if stopped set gauge to 0
  }
  else 
  {
    updateWheel(); // not stopped update speed display
  }
  delay(500);
}

 

테스트하려면 코드를 복사하여 Gauge_CrowPanel_Lesson3.ino라는 새 스케치에 붙여넣고, 이전과 같은 디렉터리에 tft_setup.h 파일도 복사합니다. 코드를 컴파일하여 장치에 로드하고 CrowPanel에서 제공하는 짧은 케이블을 사용하여 홀 효과 센서를 연결합니다. 센서 앞에서 자석을 손으로 앞뒤로 움직여 속도계 기능을 테스트할 수 있습니다. 약간의 연습을 통해 다양한 속도로 주행하는 시뮬레이션을 할 수 있습니다. 플래시 메모리 저장 공간을 확인하려면 Arduino IDE에서 직렬 모니터를 엽니다. 6분마다 업데이트된 실행 시간 값이 출력될 것입니다.

 

이 시점에서 거리와 주행 시간 값을 내부적으로 저장하는 완벽한 기능의 속도계가 완성되었습니다. 하지만 이 값들을 표시할 방법이 없고, 주행 거리계를 재설정하거나 킬로미터와 마일 단위로 디스플레이를 전환할 방법도 없습니다. 이 문제는 4과에서 기기와 안드로이드 폰 간의 블루투스 통신 기능을 추가할 때 다루겠습니다. 블루투스 통신 기능을 추가하는 것이 얼마나 쉬운지 놀라실 겁니다.

 

여러분께서 이 시리즈를 흥미롭고 유익하게 읽으셨으면 좋겠습니다.

 

 

반응형

캐어랩 고객 지원

취업, 창업의 막막함, 외주 관리, 제품 부재!

당신의 고민은 무엇입니까? 현실과 동떨어진 교육, 실패만 반복하는 외주 계약, 아이디어는 있지만 구현할 기술이 없는 막막함.

우리는 알고 있습니다. 문제의 원인은 '명확한 학습, 실전 경험과 신뢰할 수 있는 기술력의 부재'에서 시작됩니다.

이제 고민을 멈추고, 캐어랩을 만나세요!

코딩(펌웨어), 전자부품과 디지털 회로설계, PCB 설계 제작, 고객(시장/수출) 발굴과 마케팅 전략으로 당신을 지원합니다.

제품 설계의 고수는 성공이 만든 게 아니라 실패가 만듭니다. 아이디어를 양산 가능한 제품으로!

귀사의 제품을 만드세요. 교육과 개발 실적으로 신뢰할 수 있는 파트너를 확보하세요.

지난 30년 여정, 캐어랩이 얻은 모든 것을 함께 나누고 싶습니다.

카카오 채널 추가하기

카톡 채팅방에서 무엇이든 물어보세요

당신의 성공을 위해 캐어랩과 함께 하세요.

캐어랩 온라인 채널 바로가기

캐어랩