Arduino

Arduino Serial Part 4: ASCII data and using markers to separate data

기하 2022. 3. 13. 00:56

http://www.martyncurrey.com/arduino-serial-part-4-ascii-data-and-using-markers-to-separate-data/

 

파트 3 에서 우리는

상당히 간단한 기술을 사용하여 LED를 제어하기 위해 단일 문자를 보내고 받았습니다. 

필요한 것이 몇 가지 사항을 원격으로 켜고 끄는 것이라면 이 방법이 가장 좋습니다. 

간단하고 프로그래밍하기 쉽고 신뢰할 수 있습니다. 

때로는 단일 문자로는 충분하지 않고

더 복잡한 명령을 사용해야 하거나 둘 이상의 문자로 구성된 센서 데이터를 보내고 싶을 수 있습니다.

 

이 게시물에서는 복잡한 데이터와 명령을 보내는 몇 가지 다른 기술을 살펴봅니다.

 Arduino 언어로 구축된 기능에서 시작하여

IMHO가 더 잘 수행하고 더 나은 코드를 허용하는 자체 기능으로 이동합니다.

 

 

여러 문자

많은 초보자가 저지르는 일반적인 실수는 데이터를 갖기 전에 테스트하는 것입니다. 

직렬을 통해 하나 이상의 문자를 수신할 때 모든 데이터가 한 번에 도착한다고 가정하기 쉽습니다. 

그렇지 않습니다. 장치가 "HELLO"를 보내면 한 번에 한 문자씩 전송되고 한 번에 한 문자씩 수신됩니다. 

그런 다음 수신 장치는 모든 문자를 조합하여 "HELLO"라는 단어를 형성해야 합니다. 

Arduino 표준에 따르면 직렬은 매우 느리고

Arduino는 모든 문자를 수신하는 데 걸리는 시간 내에 수천 가지 작업을 수행할 수 있습니다. 

이것은 주의하지 않으면 실제로 수신된 데이터를 수신하기 전에

코드가 수신된 데이터를 검사하기 시작할 수 있음을 의미합니다.

 

내가 본 또 다른 문제는 serial.read가 사용 가능한 모든 데이터를 읽는다고 생각하는 것입니다. 

그렇지 않습니다. 하나의 문자 또는 바이트만 읽고 모든 데이터를 읽고 조합하는 것은 사용자에게 달려 있습니다.

 

 

Serial.readBytesUntil(…)

직렬 모니터에서 문자열을 수신하는 간단한 예부터 시작하겠습니다. 

여기에 사용자가 이름을 입력하고 보내기를 클릭합니다. 

Arduino는 사용자 이름의 길이를 알지 못하므로

모든 데이터가 있는지 확인할 방법이 필요합니다. 

 

상당히 간단한 방법은 Serial.readBytesUntil(…)을 사용하는 것입니다.

이를 통해 종료 문자를 마커로 사용할 수 있습니다.

 

파트 1을 읽으면 Serial.readBytesUntil(..)이

3가지 조건 중 1가지가 발생할 때까지 직렬 버퍼에서 읽는다는 것을 기억할 수 있습니다.


1 – 종료 문자를 찾습니다.
2 – 지정된 수의 문자를 읽었습니다.
3 – times out

 

직렬 모니터에서 입력에 추가할 EOL 문자를 선택할 수 있습니다. 

여기서는 Newline만 사용하고 있습니다. 

이것은 입력에 개행("\n")을 추가하고

이것을 Serial.readBytesUntil(..) 함수에서 종료 문자로 사용할 수 있습니다. 

"\n"은 10진수 값을 가집니다.

// Arduino_Serial_Part_4_001_Serial_input
 
int length = 30;
char buffer [31];
char termChar = '\n';
 
void setup()
{
  Serial.begin(115200);
  Serial.println("Set line endings Newline");
  Serial.println("");
  Serial.println("Please enter your name and click Send");
}
 
void loop()
{    
  if (Serial.available())
  {
     int numChars = Serial.readBytesUntil(termChar, buffer, length);
     buffer[numChars]='\0';
     Serial.print("Hello ");  Serial.println(buffer); 
  }

 

이것은 매우 기본적이며 오류 트래핑을 포함하지 않습니다. 

버퍼는 최대 30자로 설정되어 있으나 사용자가 이 이상 입력할 수 있습니다. 

이로 인해 스케치가 오작동할 수 있습니다.

위의 시도를 제공합니다. 제한 사항을 기억하는 한 잘 작동해야 합니다

개행 문자를 입력하지 않으면 어떻게 됩니까?  

시리얼 모니터에서 "No line Ending"을 선택하고 새 이름을 입력합니다. 

여전히 작동하지만 Arduino가 응답하기 전에 지연이 있습니다. 

 

Serial.readBytesUntil(..) 함수가 시간 초과될 때까지 기다리고 있기 때문입니다. 

기본 시간 초과는 1000ms 또는 1초이므로 지연은 1초여야 합니다. 

이 예에서 1초 지연은 큰 문제가 아니지만

데이터가 충분히 빨리 전송되지 않으면 문제가 될 수 있습니다. 

함수는 모든 데이터를 수신하기 전에 시간 초과됩니다. 

줄 끝을 선택하지 않고 이름을 한 번에 한 글자씩 천천히 입력해 보십시오. 

각 문자 뒤에 보내기를 클릭합니다. 

당신의 이름이 부분적으로 나타날 가능성이 있습니다..

 

Function to read serial until a terminating character

코드를 구현하는 방법에 매우 주의하지 않는 한

Serial.readBytesUntil(..) 시간 초과로 인해 문제가 발생할 수 있습니다. 

시간 초과를 늘릴 수 있습니다.

이것은 일종의 작동이지만 기다리는 동안 기능 블록을 기억하십시오. 

이것은 Arduino가 다른 것을 할 수 없다는 것을 의미합니다. 

더 나은 방법은 다른 작업을 수행하면서 시간 제한 없이

직렬 입력을 수집하는 자체 함수를 만드는 것입니다.

// Arduino_Serial_Part_4_002_Serial_input
 
char c = ' ';
int length = 30;
char buffer [31];
char termChar = 10;
 
byte index = 0;
 
void setup()
{
  Serial.begin(115200);
  Serial.println("Set EOL to Newline");
  Serial.println("Please enter your name and click Send");
}
 
void loop()
{    
  if (Serial.available())
  {
     c = Serial.read();
     if (c != termChar)
     {
       buffer[index] = c;
       index = index + 1;
     }
 
     else
     {
       buffer[index] = '\0';
       index = 0;
       processNewData();
     }
  }
}
 
 
void processNewData()
{
  Serial.print("Hello ");  Serial.println(buffer); 
}

루프 직렬의 모든 반복에서 데이터가 확인되는 것을 볼 수 있어야 합니다. 

데이터가 있는 경우 단일 문자/바이트를 읽습니다. 

새로 읽은 문자를 확인하여 종료 문자인지 확인합니다. 

그렇다면 새로운 데이터가 있음을 의미합니다. 

그렇지 않으면 방금 읽은 문자가 버퍼라는 char 배열에 추가되고 프로세스가 계속됩니다. 

읽은 마지막 문자가 종료 문자인 경우 새 데이터로 작업을 수행합니다.

 

또한 데이터가 도착하기를 기다리고 있지 않다는 것을 알 수 있어야 합니다. 

우리는 새로운 데이터를 확인하고 데이터가 있으면 처리합니다. 

새로운 데이터가 없으면 계속 진행합니다. 

이것은 우리가 모든 직렬 데이터가 도착하기를 기다리지 않고 다른 일을 할 수 있음을 의미합니다.

스케치는 작동하지만 개발하기가 정말 쉽지 않습니다. 

스케치를 정리하고 주요 부품을 각자의 기능에 넣어 봅시다.

 

// Arduino_Serial_Part_4_003_Serial_input
 
char c = ' ';
int length = 30;
char buffer [31];
char termChar = 10;
 
byte index = 0;
boolean haveNewData = false;
 
void setup()
{
  Serial.begin(115200);
  Serial.println("Set EOL to Newline");
  Serial.println("Please enter your name and click Send");
}
 
void loop()
{    
  readSerial();
  if ( haveNewData ) {  processNewData();  }
}
 
 
void readSerial()
{
    if (Serial.available()) 
    {  
       c = Serial.read();
       if (c != termChar)
       {
         buffer[index] = c;
         index = index + 1;
       }
       else
       {
         buffer[index] = '\0';
         index = 0;
         haveNewData = true;
       }
    }
}
 
 
void processNewData()
{
  Serial.print("Hello ");  Serial.println(buffer); 
  haveNewData = false;
}

이것은 정확히 동일하며 코드를 읽기가 훨씬 쉽습니다. 메인 루프에 있는 것은

따라서 더 많은 코드를 훨씬 쉽게 추가할 수 있습니다.

 

코드 설명


변수 c는 직렬 입력 버퍼에서 읽은 최신 문자를 저장하는 데 사용됩니다.
변수 길이는 버퍼의 최대 길이입니다.
변수 버퍼는 들어오는 데이터를 저장하는 데 사용되는 문자 배열입니다.
변수 termChar는 종료 문자입니다
.변수 인덱스는 인덱스 위치입니다. 버퍼의. 다음 문자를 복사할 위치입니다.
변수 haveNewData는 새 데이터가 있을 때 나머지 스케치에 알리는 데 사용되는 플래그입니다.

직렬 데이터를 사용할 수 있는 경우 c에서 한 문자를 읽습니다. 그런 다음 c가 종료 문자가 아닌지 확인하고 그렇지 않으면 c를 인덱스로 지정된 위치의 버퍼 char 배열에 복사합니다. 그런 다음 인덱스는 다음 문자에 대한 준비로 증가됩니다.

c가 종료 문자인 경우 버퍼에 복사할 필요가 없습니다. 단순히 버퍼를 닫습니다(버퍼 끝에 '\0' 추가) 인덱스를 0으로 설정하고 다음 시간을 위해 준비하고 haveNewData = true를 설정하여 보여줍니다. 새로운 데이터가 있습니다.

메인 루프에서 우리는 haveNewData가 설정되어 있는지 그리고 그것이 processData() 함수를 호출하는지 확인합니다. 새로운 데이터 processData() 함수를 처리할 뿐만 아니라 haveNewData도 재설정합니다.

시도 해봐. 이전과 정확히 동일해야 합니다.

 

Why is this better than using Serial.readBytesUntil(…)?

 

위에서 언급했듯이 Serial.readBytesUntil(…)에는 시간 초과가 있습니다. 

이것은 시리얼 모니터를 사용할 때 문제가 될 것 같지 않지만

다른 장치에서 시리얼 데이터를 수신할 때 문제가 됩니다. 

우리의 새로운 방법에는 시간 제한이 없습니다. 

분당 한 문자 또는 한 시간에 한 문자를 수신할 수 있으며 여전히 잘 작동합니다. 

직렬 모니터를 사용하여 직접 테스트할 수 있습니다. 

 

EOL을 "줄 끝 없음"으로 설정하고

A 클릭 보내기를 입력하고 B를 입력하고 보내기를 클릭하고 C를 입력하고 보내기를 클릭합니다. 

이제 EOL을 다시 "Newline"으로 변경하고 D를 입력하고 보내기를 클릭합니다. 

직렬 모니터에 "ABCD"가 나타나야 합니다.

 

주의 사항

1 – 버퍼가 보유할 수 있는 것보다 더 많은 데이터를 수신할 수 있습니다.
2 – 데이터의 맨 처음을 받았는지 여부는 알 수 없습니다. 우리는 우리가 끝을 받았다는 것만 압니다.

 

버퍼를 사용하는 동안 문제 #1에 대해 100% 만족스러운 방법은 없습니다. 

Arduino에는 매우 큰 버퍼를 처리할 메모리가 없으며

단순히 버퍼를 더 크게 만들 수 없습니다(나머지 스케치의 크기에 따라 조금 더 커도 괜찮습니다). 

 

스케치가 대용량 데이터 세트를 처리하지 못할 수도 있지만

적어도 Arduino 메모리를 손상시키는 대용량 데이터는 중지할 수 있습니다. 

불행히도 그렇게 하면 데이터가 손실될 수 있습니다.

 

큰 데이터 블록을 처리해야 하는 경우

버퍼에 저장하지 않고 데이터가 도착하는 대로 관리합니다.

문제 #2 우리는 조금 후에 다루겠습니다

 

버퍼 크기로 수신 데이터 제한

크기를 제한하려면 버퍼의 최대 크기에 대해 현재 인덱스 위치를 확인하기만 하면 됩니다. 

버퍼의 끝에 도달한 경우 인덱스를 늘리지 마십시오. 

이것은 데이터의 끝이 누락되지만 적어도 스케치가 화염에 휩싸이지 않을 것임을 의미합니다.

 

Char 배열 오버런은 디버그하기가 매우 어렵고 성가시다. 

Arduino는 모든 종류의 혼란을 일으킬 수 있는 30자 배열에 40 또는 50자를 기꺼이 복사하려고 할 것입니다. 

char 배열 바로 뒤의 메모리는 다른 변수에 의해 사용될 가능성이 높으므로

배열 크기를 초과하면 다른 변수를 덮어쓰기 시작합니다.

다음을 추가하기만 하면 됩니다.

 

그리고 우리의 최종 스케치는

// Arduino_Serial_Part_4_004_Serial_input
 
char c = ' ';
int length = 30;
char buffer [31];
char termChar = 10;
 
byte index = 0;
boolean haveNewData = false;
 
void setup()
{
  Serial.begin(115200);
  Serial.println("Set EOL to Newline");
  Serial.println("Please enter your name and click Send");
}
 
void loop()
{    
  readSerial();
  if ( haveNewData ) {  processNewData();  }
}
 
void readSerial()
{
    if (Serial.available()) 
    {  
       c = Serial.read();
       if (c != termChar)
       {
       if (index < length)
       {
          buffer[index] = c;
          index = index + 1;
       }
    }
    else
    {
      buffer[index] = '\0';
      index = 0;
      haveNewData = true;
    }
  }
 
}
 
 
void processNewData()
{
  Serial.print("Hello ");  Serial.println(buffer); 
  haveNewData = false;
}

Arduino 시간을 시작하고 직렬이 시작되었는지 확인하면 위의 내용을 상당히 신뢰할 수 있습니다. 

조금 더 완벽할 수 있으며 데이터의 시작을 끝으로만 받았다고 100% 확신할 수는 없습니다. 

이 문제를 해결하기 위해 다음 단계는 시작 마커와 끝 마커를 사용하는 것입니다.

 

End Marker를 사용하여 Arduino에서 Arduino로 직렬 연결

직렬 모니터로 노는 것은 모두 훌륭하지만 그다지 실용적이지 않습니다. 

다음은 LCD 화면에 센서 값을 표시하는 다른 Arduino에 센서 판독값을 보내는 예입니다.

(녹색 LED는 아무 동작도 하지 않습니다. 나중에 다른 회로에서 사용하고 제거하는 것을 잊었습니다.)

 

이 예에서는 두 Arduino가 동일한 컴퓨터에 연결되어 있습니다. 

이것은 그들의 GND가 USB 연결을 통해 연결되었음을 의미하며 브레드보드에 추가할 필요가 없습니다. 

Arduino에 별도의 전원 공급 장치가 있는 경우 GND를 연결해야 합니다.

다음 스케치는 AltSoftSerial 및 NewliquidCrystal을 사용합니다.

 

LCD 화면 및 NewliquidCrystal 라이브러리에 대한 자세한 내용 

 Arduino with HD44780 based Character LCDs 참조하십시오.

AltSoftSerial에 대한 자세한 내용은 1 부를 참조하십시오.

 

마스터 장치에는 버튼 스위치와 전위차계가 있습니다. 

스케치는 이러한 장치의 상태를 읽고 AltSoftSerial을 사용하여 판독값을 슬레이브 장치로 보냅니다. 

슬레이브 장치는 데이터를 수신하여 LCD 화면에 표시합니다.

 

Arduino_Serial_Using_EOL_Master

// Arduino_Serial_Using_EOL_Master
 
#include <AltSoftSerial.h>
AltSoftSerial ALTserial; 
 
byte switchPin = 2;              //  input pin for the switch
boolean newSwitchState1 = LOW;   // used for simple debouce
boolean newSwitchState2 = LOW;
boolean newSwitchState3 = LOW;
boolean oldSwitchState = LOW;    // variable to hold the switch state
 
int potPin = A0;    // input pin for the potentiometer
int val = 0;        // variable to store the value coming from the pot
int oldval = 0;     // variable to store the old pot value 
 
unsigned long startTime = 0;
unsigned long nowTime = 0;
unsigned long waitTime = 500;
 
void setup()
{
  Serial.begin(9600);
  Serial.println("Press the button switch or twiddle the pot.");
  ALTserial.begin(9600); 
 
  pinMode(switchPin, INPUT); 
  startTime = millis();
}
 
 
void loop()
{    
    newSwitchState1 = digitalRead(switchPin);     delay(1);
    newSwitchState2 = digitalRead(switchPin);     delay(1);
    newSwitchState3 = digitalRead(switchPin);  
 
    // Simple debouce - if all 3 values are the same we can continue
    if (  (newSwitchState1==newSwitchState2) && (newSwitchState1==newSwitchState3) )
    {
        // only interested if the switch has changed state. HIGH to LOW or LOW to HIGH
        if ( newSwitchState1 != oldSwitchState ) 
        {
           oldSwitchState = newSwitchState1;
 
           // has the button switch been closed?
           if ( newSwitchState1 == HIGH )
           {
                 Serial.println("S=HIGH");
                 ALTserial.print("S=HIGH\n");
           }
           else
           {
                Serial.println("S=LOW");
                ALTserial.print("S=LOW\n");
           }
        }  
    } 
 
 
    // only want to check the pot every 500 ms 
    // and only want to send if the value has changed
    nowTime = millis();
    if (nowTime - startTime > waitTime)
    {
      startTime = nowTime;
      val = analogRead(potPin);  
      if ((val) != (oldval) )
      {
        oldval = val;
        Serial.print("P="); 
        HS_printFixedFormat(val);
        Serial.print("\r\n"); 
 
        ALTserial.print("P="); 
        ALT_printFixedFormat(val);
        ALTserial.print("\n"); 
      }
    }
}
 
void ALT_printFixedFormat(int num)
{
  if (num <1000) {  ALTserial.print("0");    }
  if (num <100)  {  ALTserial.print("0");    }
  if (num <10)   {  ALTserial.print("0");    }
  ALTserial.print(num); 
}
 
void HS_printFixedFormat(int num)
{
  if (num <1000) {  Serial.print("0");    }
  if (num <100)  {  Serial.print("0");    }
  if (num <10)   {  Serial.print("0");    }
  Serial.print(num);
}

마스터 스케치는 루프마다 버튼 스위치를 읽고 변경 사항이 감지되면 즉시 반응합니다. 

스케치는 0.5초마다 전위차계를 읽습니다. 

값이 변경되었는지 확인하고 값이 변경된 경우에만 판독값을 슬레이브 Arduino로 보냅니다. 

판독값이 변경된 경우에만 데이터를 전송하면 전송해야 하는 직렬 데이터의 양이 줄어듭니다.

 

전위차계에 고정 길이 값을 사용하고 있습니다.

 ALT_printFixedFormat()은 숫자를 채우기 위해 선행 0을 추가합니다. 

1은 "0001"이 되고 999는 "0999"가 됩니다

 

직렬 모니터를 열어 마스터 스케치를 테스트할 수 있습니다. 

AlrSoftSerial 연결로 전송된 모든 것은 하드웨어 직렬로 복사됩니다. 

이것은 모니터링 및 디버깅에 적합하지만 실제로는 필요하지 않습니다.

 

Arduino_Serial_Using_EOL_Slave

// Arduino_Serial_Using_EOL_Slave
 
#include <AltSoftSerial.h>
AltSoftSerial ALTserial; 
 
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
//LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE); 
 
// Set the pins on the I2C chip used for LCD connections:
//                    addr, en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);  // Set the LCD I2C address
 
 
char c;
int length = 30;
char buffer [31];
char termChar = '\n';
 
byte index = 0;
boolean haveNewData = false;
 
 
void setup()
{
  Serial.begin(9600);
  Serial.println("Ready");
 
  ALTserial.begin(9600); 
 
  lcd.begin(20, 4);
  lcd.setCursor(0, 0); 
  lcd.print("Arduino Serial"); 
  lcd.setCursor(0, 1);  lcd.print("Switch="); 
  lcd.setCursor(0, 2);  lcd.print("Pot="); 
}
 
 
void loop()
{    
  readSerial();
  if ( haveNewData ) {  processNewData();  }
}
 
 
void readSerial()
{
    if (ALTserial.available()) 
    {  
       c = ALTserial.read();
       if (c != termChar)
       {
       if (index < length)
       {
          buffer[index] = c;
          index = index + 1;
       }
    }
    else
    {
      buffer[index] = '\0';
      index = 0;
      haveNewData = true;
    }
  }
}
 
void processNewData()
{
  Serial.println(buffer); 
  if (buffer[0] == 'S')
  {
    lcd.setCursor(7, 1); 
    if (buffer[2] == 'H') {   lcd.print("HIGH");    }
    if (buffer[2] == 'L') {   lcd.print("LOW ");    }
  }
 
  if (buffer[0] == 'P')
  {
    lcd.setCursor(4, 2); 
    char temp[5];
    temp[0] = buffer[2];
    temp[1] = buffer[3];
    temp[2] = buffer[4];
    temp[3] = buffer[5];    
    temp[4] = '\0' ;
    lcd.print( temp);  
  }
 
  haveNewData = false;
  buffer[0] = '\0';
}

슬레이브 스케치는 위의 Arduino_Serial_Part_4_004_Serial_input 예제와 매우 유사합니다. 차이점은 processNewData() 함수에 있습니다. 새 스케치에서 수신된 데이터가 스위치 판독값("S"로 시작)이면 스케치는 LCD에 HIGH 또는 LOW를 표시합니다. 판독값이 팟 판독값인 경우("P"로 시작) 화면에 팟 값을 표시합니다.

버튼 스위치 데이터를 처리할 때 전체 명령을 확인하지 않고 첫 글자(H 및 L)만 확인하는 것을 볼 수 있습니다. 즉, 실제로 전체 명령이 필요하지 않으며 "S=H" 및 "S=L"을 사용할 수 있습니다. "="도 사용하지 않기 때문에 제거할 수 있고 명령은 "SH" 및 "SL"이 될 수 있습니다. 나는 여기에서 속도에 대해 걱정하지 않으며 "S=HIGH" 및 "S=LOW"는 코드를 검토할 때 읽기가 더 쉽지만 성능이 우선이라면 전송되는 데이터의 양을 가능한 한 낮게 유지해야 합니다.

AltSoftSerial을 사용하면 문제 없이 전송 속도를 38400bps로 높일 수 있습니다. 이 외에도 문제를 발견할 수 있습니다. 얼마나 멀리 보낼 수 있는지 확인하려면 더 높은 전송 속도를 실험해 볼 가치가 있습니다.

언급할 가치가 있는 문제는 추가 EOL 문자입니다. 표준 Arduino EOL은 "\n"과 "\r" 두 문자가 순서대로 줄 끝에 추가됩니다. 즉, 위의 방법을 사용하고 데이터에 일반 EOL 차터가 있는 경우 함수는 직렬 버퍼에 "\r" 문자를 남겨두고 다음 데이터의 시작 부분에 추가됩니다. 이것은 ASCII 형식을 지정하고 값을 값으로 변환하는 데 문제를 일으키며 디버그하기 매우 어려울 수 있습니다.

 

 

Serial Arduino to Arduino Using Start and End Markers

 

시작 및 종료 마커는 속도가 우선순위가 아닌 경우 직렬 데이터에 대해 선호하는 방법입니다. 

나 자신을 좀 더 쉽게 만들기 위해 숫자에 ASCII를 사용하고

가능한 한 고정 길이 데이터를 사용하려고 합니다. 

여기  여기 에서 더 많은 예를 볼 수 있습니다 .

 

시작 및 종료 마커는 원본이 아니며 

Arduino 포럼의 Robin2 게시물을 기반으로 합니다(또는 단순히 복사). 

나는 이 기능을 얼마 전에 사용하기 시작했고 그것을 좋아했고 그 이후로 계속 사용하고 있습니다.

 

Robin2의 기능

void recvWithStartEndMarkers()
{
     static boolean recvInProgress = false;
     static byte ndx = 0;
     char startMarker = '[';
     char endMarker = ']';
     char rc;
 
     if (Serial.available() > 0) 
     {
          rc = Serial.read();
          if (recvInProgress == true) 
          {
               if (rc != endMarker) 
               {
                    receivedChars[ndx] = rc;
                    ndx++;
                    if (ndx > maxDataLength) { ndx = maxDataLength; }
               }
               else 
               {
                     receivedChars[ndx] = '\0'; // terminate the string
                     recvInProgress = false;
                     ndx = 0;
                     newData = true;
               }
          }
          else if (rc == startMarker) { recvInProgress = true; }
     }
}

시작 및 끝 마커로 대괄호([ 및 ])를 사용합니다. 

이 함수는 시작 마커를 찾을 때까지 직렬 입력 버퍼를 읽은 다음,

데이터를 수신된Chars라는 버퍼에 복사하기 시작합니다. 

종료 마커를 찾으면 복사를 중지하고 newData를 true로 설정합니다. 

마커 안에 있지 않은 것은 무시됩니다.

시도해보십시오


다음 스케치를 업로드하고 직렬 모니터를 열고 무언가를 입력하십시오. 

시작 및 종료 마커를 사용하는 경우 마커에 포함된 내용이 직렬 모니터에 표시됩니다. 

마커 외부의 모든 것은 무시됩니다.

 

// Arduino_Serial_Part_4_005_SerialMonitor_input
 
const byte maxDataLength = 30;  // maxDataLength is the maximum length allowed for received data.
char receivedChars[31] ;        
boolean newData = false;        // newData is used to determine if there is a new command
 
void setup()  
{
   Serial.begin(115200);
   Serial.println("Serial using start and end markers");
   newData = false;
}
 
void loop()  
{
   recvWithStartEndMarkers();                // check to see if we have received any new commands
   if (newData)  {   processCommand();  }    // if we have a new command do something
}
 
void processCommand()
{
   Serial.print("Recieved data = ");   Serial.println(receivedChars);
   newData = false;
}
 
 
// function recvWithStartEndMarkers by Robin2 of the Arduino forums
// See  http://forum.arduino.cc/index.php?topic=288234.0
void recvWithStartEndMarkers() 
{
     static boolean recvInProgress = false;
     static byte ndx = 0;
     char startMarker = '[';
     char endMarker = ']';
 
     if (Serial.available() > 0) 
     {
          char rc = Serial.read();
          if (recvInProgress == true) 
          {
               if (rc != endMarker) 
               {
                    if (ndx < maxDataLength) { receivedChars[ndx] = rc; ndx++;  }
               }
               else 
               {
                     receivedChars[ndx] = '\0'; // terminate the string
                     recvInProgress = false;
                     ndx = 0;
                     newData = true;
               }
          }
          else if (rc == startMarker) { recvInProgress = true; }
     }
 
}

직렬 모니터에서 "[hello]"를 입력하십시오.

이제 “This will be ignored[This will be processed]”를 입력해 보십시오 .

시작 및 끝 마커 외부의 데이터는 무시됩니다.

이 방법의 유일한 단점은

제어 코드와 같은 특수 기술을 사용하지 않고

데이터에 시작 및 끝 마커를 포함할 수 없다는 것입니다. 

물론 마커에 대해 모든 값이나 문자를 사용할 수 있습니다.

시작 및 종료 마커를 사용하도록 EOL 예제를 적용해 보겠습니다.

 

Arduino_Serial_Using_StartAndEndMarkers_Master

// Arduino_Serial_Using_StartAndEndMarkers_Master
 
#include <AltSoftSerial.h>
AltSoftSerial ALTserial; 
 
byte switchPin = 2;              //  input pin for the switch
boolean newSwitchState1 = LOW;   // used for simple debouce
boolean newSwitchState2 = LOW;
boolean newSwitchState3 = LOW;
boolean oldSwitchState = LOW;    // variable to hold the switch state
 
int potPin = A0;    // input pin for the potentiometer
int val = 0;        // variable to store the value coming from the pot
int oldval = 0;     // variable to store the old pot value 
 
unsigned long startTime = 0;
unsigned long nowTime = 0;
unsigned long waitTime = 500;
 
 
void setup()
{
  Serial.begin(9600);
  Serial.println("Press the button switch or twiddle the pot.");
 
  ALTserial.begin(9600); 
 
  pinMode(switchPin, INPUT); 
  startTime = millis();
}
 
 
void loop()
{    
    newSwitchState1 = digitalRead(switchPin);     delay(1);
    newSwitchState2 = digitalRead(switchPin);     delay(1);
    newSwitchState3 = digitalRead(switchPin);  
 
    // Simple debouce - if all 3 values are the same we can continue
    if (  (newSwitchState1==newSwitchState2) && (newSwitchState1==newSwitchState3) )
    {
        // only interested if the switch has changed state. HIGH to LOW or LOW to HIGH
        if ( newSwitchState1 != oldSwitchState ) 
        {
           oldSwitchState = newSwitchState1;
 
           // has the button switch been closed?
           if ( newSwitchState1 == HIGH )
           {
                 Serial.println("[S=HIGH]");
                 ALTserial.print("[S=HIGH]");
           }
           else
           {
                Serial.println("[S=LOW]");
                ALTserial.print("[S=LOW]");
           }
        }  
    } 
 
 
 
    // only want to check the pot every 500 ms (change this if you like)
    // and only want to send if the value has changed
 
    nowTime = millis();
    if (nowTime - startTime > waitTime)
    {
      startTime = nowTime;
      val = analogRead(potPin);  
      if ((val) != (oldval) )
      {
        oldval = val;
        Serial.print("[P="); 
        HS_printFixedFormat(val);
        Serial.println("]"); 
 
        ALTserial.print("[P="); 
        ALT_printFixedFormat(val);
        ALTserial.print("]"); 
      }
    }
 
}
 
 
 
void ALT_printFixedFormat(int num)
{
  if (num <1000) {  ALTserial.print("0");    }
  if (num <100)  {  ALTserial.print("0");    }
  if (num <10)   {  ALTserial.print("0");    }
  ALTserial.print(num); 
 
}
 
void HS_printFixedFormat(int num)
{
  if (num <1000) {  Serial.print("0");    }
  if (num <100)  {  Serial.print("0");    }
  if (num <10)   {  Serial.print("0");    }
  Serial.print(num); 
}

Arduino_Serial_Using_StartAndEndMarkers_Slave

/ Arduino_Serial_Using_StartAndEndMarkers_Slave
 
#include <AltSoftSerial.h>
AltSoftSerial ALTserial; 
 
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
//LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE); 
 
// Set the pins on the I2C chip used for LCD connections:
//                    addr, en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);  // Set the LCD I2C address
 
char c;
int length = 30;
char buffer [31];
 
boolean haveNewData = false;
 
void setup()
{
  Serial.begin(9600);
  Serial.println("Ready");
 
  ALTserial.begin(9600); 
 
  lcd.begin(20, 4);
  lcd.setCursor(0, 0); 
  lcd.print("Arduino Serial"); 
  lcd.setCursor(0, 1);  lcd.print("Switch="); 
  lcd.setCursor(0, 2);  lcd.print("Pot="); 
}
 
 
void loop()
{    
  recvWithStartEndMarkers();
  if ( haveNewData ) {  processNewData();  }
}
 
 
void recvWithStartEndMarkers()
{
     static boolean recvInProgress = false;
     static byte ndx = 0;
     char startMarker = '[';
     char endMarker = ']';
     char rc;
 
     if (ALTserial.available() > 0) 
     {
          rc = ALTserial.read();
 
          if (recvInProgress == true) 
          {
               if (rc != endMarker) 
               {
                    buffer[ndx] = rc;
                    ndx++;
                    if (ndx > length) { ndx = length; }
               }
               else 
               {
                     buffer[ndx] = '\0'; // terminate the string
                     recvInProgress = false;
                     ndx = 0;
                     haveNewData = true;
               }
          }
          else if (rc == startMarker) { recvInProgress = true; }
     }
}
 
 
 
void processNewData()
{
  Serial.println(buffer); 
  if (buffer[0] == 'S')
  {
    lcd.setCursor(7, 1); 
    if (buffer[2] == 'H') {   lcd.print("HIGH");    }
    if (buffer[2] == 'L') {   lcd.print("LOW ");    }
  }
 
  if (buffer[0] == 'P')
  {
    lcd.setCursor(4, 2); 
    char temp[5];
    temp[0] = buffer[2];
    temp[1] = buffer[3];
    temp[2] = buffer[4];
    temp[3] = buffer[5];    
    temp[4] = '\0' ;
    lcd.print( temp);  
  }
 
  haveNewData = false;
  buffer[0] = '\0';
}

스케치는 이전 예와 정확히 동일합니다. 

이제 시작 및 종료 마커를 사용하는 유일한 차이점

마스터 스케치에서 시작 및 종료 마커가 데이터에 추가되었습니다.

 

슬레이브 스케치에서 recvWithStartEndMarkers() 함수는 readSerial()을 대체합니다. 

다른 모든 것은 동일합니다

 

고급 예. 논블로킹 시리얼

위에서 언급했듯이 Arduino 코어에 내장된 기능보다

자체 기능을 사용하는 이점 중 하나는

Arduino가 직렬 데이터를 수신하면서 다른 작업을 수행할 수 있다는 것입니다. 

 

readBytesUntil()과 같은 Arduino 코어의 기능을 사용하여 Arduino를 차단합니다.

차단하지 않는 것이 얼마나 유용한지 보여주기 위해

다음 예에서는 깜박이는 LED를 추가합니다. 

포트 값이 일정 수준 이상으로 올라가면 LED가 깜박이기 시작합니다. 

마스터 스케치는 위와 동일하게 유지됩니다. 

슬레이브 스케치는 새 코드를 포함하도록 업데이트됩니다

(마스터 스케치에서 사용되는 전위차계 타이밍 확인 코드와 매우 유사함).

포트 값이 임계값 미만이면 LED가 꺼집니다.

 

포트 값이 임계값을 초과하면 LED가 깜박이기 시작합니다.

 

Arduino_Serial_Using_StartAndEndMarkers_Slave_flashingLED

// Arduino_Serial_Using_StartAndEndMarkers_Slave_flashingLED
 
#include <AltSoftSerial.h>
AltSoftSerial ALTserial; 
 
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
//LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE); 
 
// Set the pins on the I2C chip used for LCD connections:
//                    addr, en,rw,rs,d4,d5,d6,d7,bl,blpol
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);  // Set the LCD I2C address
 
char c;
int length = 30;
char buffer [31];
boolean haveNewData = false;
 
int potVal = 0;
int warningThreshold = 800;
unsigned long startTime = 0;
unsigned long nowTime = 0;
unsigned long flashRate = 250;
 
byte LEDpin = 2;
boolean LEDflash = false;
boolean LEDstate = false;
 
 
void setup()
{
  Serial.begin(9600);
  Serial.println("Ready");
 
  ALTserial.begin(9600); 
 
  lcd.begin(20, 4);
  lcd.setCursor(0, 0); 
  lcd.print("Arduino Serial"); 
  lcd.setCursor(0, 1);  lcd.print("Switch=LOW"); 
  lcd.setCursor(0, 2);  lcd.print("Pot=0000"); 
  lcd.setCursor(0, 3);  lcd.print("Threshold="); lcd.print(warningThreshold); 
 
  pinMode(LEDpin, OUTPUT);
  boolean LEDstate = LOW;
}
 
 
void loop()
{    
  recvWithStartEndMarkers();
  if ( haveNewData == true )  { processNewData();  }
  if ( LEDflash == true)      { flashTheLED(); }
}
 
 
 
void flashTheLED()
{
    nowTime = millis();
    if (nowTime - startTime > flashRate)
    {
      startTime = nowTime;
 
      if (LEDstate == LOW) 
      { 
         LEDstate = HIGH;
         digitalWrite(LEDpin, HIGH);
      }
      else
      {
         LEDstate = LOW;
         digitalWrite(LEDpin, LOW);
      }
    }
}
 
 
 
void recvWithStartEndMarkers()
{
     static boolean recvInProgress = false;
     static byte ndx = 0;
     char startMarker = '[';
     char endMarker = ']';
     char rc;
 
     if (ALTserial.available() > 0) 
     {
          rc = ALTserial.read();
 
          if (recvInProgress == true) 
          {
               if (rc != endMarker) 
               {
                    buffer[ndx] = rc;
                    ndx++;
                    if (ndx > length) { ndx = length; }
               }
               else 
               {
                     buffer[ndx] = '\0'; // terminate the string
                     recvInProgress = false;
                     ndx = 0;
                     haveNewData = true;
               }
          }
          else if (rc == startMarker) { recvInProgress = true; }
     }
}
 
 
 
void processNewData()
{
  Serial.println(buffer); 
  if (buffer[0] == 'S')
  {
    lcd.setCursor(7, 1); 
    if (buffer[2] == 'H') {   lcd.print("HIGH");    }
    if (buffer[2] == 'L') {   lcd.print("LOW ");    }
  }
 
  if (buffer[0] == 'P')
  {
      lcd.setCursor(4, 2); 
      char temp[5];
      temp[0] = buffer[2];
      temp[1] = buffer[3];
      temp[2] = buffer[4];
      temp[3] = buffer[5];    
      temp[4] = '\0' ;
      lcd.print( temp);  
 
      potVal = atoi(temp);
      if (potVal >= warningThreshold) 
      { 
       LEDflash = true;   
      }  
      else 
      { 
        LEDflash = false;   
        // just in case the LED is on when the pot value goes below the threshold
        digitalWrite(LEDpin,LOW);
      } 
  }
 
  haveNewData = false;
  buffer[0] = '\0';
}

새 스케치에 대해 주의할 몇 가지 사항입니다.
냄비의 실제 숫자 값을 사용하고 있습니다. 

즉, ASCII 값을 실제 값으로 변환해야 합니다. 

atoi() 또는 ascii를 정수로 사용하여 이 작업을 수행합니다.
LEDflash는 LED가 깜박여야 하는지 여부를 표시하는 데 사용됩니다.

 

팟의 값이 임계값 아래로 내려가 깜박임이 멈출 때 LED가 켜질 수 있습니다. 

이런 일이 발생했을 때 LED가 꺼져 있는지 확인하기 위해 LEDpin을 LOW로 설정했습니다.

 원하는 경우 LED 상태를 확인하고 LED가 켜져 있는 경우에만

핀을 LOW로 설정할 수 있지만 실제로 그럴 이유가 없습니다.

함수를 사용한 덕분에 main loop()는 매우 기본적이라면 단 세 줄의 코드입니다.

 

직렬 데이터를 확인하십시오.
새 데이터 확인
LED가 깜박일 수 있습니다.

flashTheLED() 함수는 시간이 얼마나 지났는지 확인하고

시간이 깜박임 속도보다 크면 그에 따라 LED를 켜거나 끕니다.

 

마스터 스케치는 위와 동일합니다. 

한 단계 더 나아가서 마스터 장치의 버튼 스위치가 임계값 제한을 설정하도록 기능을 추가해 보십시오. 

IE는 pot 값을 원하는 임계값으로 설정하고 버튼 스위치를 클릭하여 설정합니다. 

물론 새로운 임계값을 슬레이브 장치로 보내야 합니다. 

나는 당신이 이것을 시도하도록 떠날 것입니다.

이 부분에 대한 것입니다. 

지금까지의 모든 내용은 직렬 데이터에 대한 적절한 소개와

자신의 프로젝트에서 이를 구현하는 방법을 제공해야 합니다.