ㅇ공부#임베디드/Embedded 실습

4. 아두이노로 알아보는 임베디드 시스템의 동작원리

BrainKimDu 2023. 6. 27. 16:58

참고도서 : 임베디드 엔지니어 교과서, 와타나베 노보루, 제이펍, 2020

책을 내용을 참고하였으며, 추가 정보가 필요할 경우 ChatGPT와 구글 검색을 활용합니다. 

이 글은 포토폴리오 목적이며, 별도의 수익을 창출하지 않습니다.


이전 시간에

FreeRTOS를 깔아보고 아두이노에 적용까지 해보았습니다. 이번 시간에는 IDE에 작성한 코드가 어떻게 Arduino까지 전달이 되는지 알아보도록 하겠습니다.

 

임베디드 소프트웨어

임베디드 소프트웨어는 두 가지로 나눌 수 있습니다. 운영체제를 사용하는 타입과 운영체제를 사용하지 않는 타입입니다. 일반적으로 아두이노가 운영체제가 없이 동작하며(FreeRTOS의 경우 제외) 라즈베리 파이가 운영체제가 있게 동작을 합니다.

운영체제가 없는 경우는 동작이 단순한 경우일 것이며, 동작이 복잡한 경우에는 운영체제를 사용할 것입니다. 운영체제를 사용할 경우에 애플리케이션과 마이크로컴퓨터의 하드웨어를 연결하는 것으로 미들웨어를 사용했었고, 제 블로그에서 예시를 찾자면 라즈베리파이에 우분투 22.04를 설치하고 ROS2를 설치해서 사용한 적이 이에 해당할 것입니다.

보통 임배디드 시스템을 개발할때는 데스크탑(PC)에서 작업이 진행되고 이를 임베디드 시스템용의 실행 형식으로 변환해서 사용합니다. 

프로그램을 임베디드 시스템의 CPU가 이해할 수 있는 형태로 만드는 것을 빌드 작업이라고 하는데, (다른 말로는 컴파일이라고도 한다) 

보통 전처리-> 컴파일 -> 어셈블 -> 링크 -> HEX 작성까지의 일련의 작업을 하는 것을 빌드라고 한니다

 

아두이노

보통 전기전자를 전공했다면, 부품을 받았을 때 가장먼저 하는 행동이 데이터시트를 확인하는 것입니다.

https://docs.arduino.cc/resources/datasheets/A000066-datasheet.pdf

보통 사람들이 많이 확인하는 방법은 

하드웨어적으로 어떤 스펙을 가지고 있는지, 어떤 주변장치들을 가지고 있는지 확인합니다. 여기서 보면 아두이노에는 ATMega328P CPU가 달려있는 것을 확인할 수 있습니다.

임베디드 SW를 만지는 입장에서는 다음과 같은 핀번호들이 박힌 이미지가 가장 유용하다고 볼 수 있습니다. 여기서는 핀번호를 어떻게 꼳아야하고, GND란 무엇이고 VCC란 무엇인지 등등의 이야기는 하지 않고 넘어가도록 합니다.

ATMega328P의 데이터시트를 한 번 확인해보면 다음과 같습니다.

다음과 모양의 핀이 현재 아두이노 보드에 꼳혀있는 것입니다. 여기서 이 CPU를 아두이노에 어떻게 꼳았는가를 확인하려면 아두이노 홈페이지에서 schematic을 검색하면 됩니다.

https://store.arduino.cc/products/arduino-uno-rev3?queryID=undefined 

 

Arduino Uno Rev3

Arduino Uno is a microcontroller board based on the ATmega328P (datasheet). It has 14 digital input/output pins (of which 6 can be used as PWM outputs), 6 analog inputs, a 16 MHz ceramic resonator (CSTCE16M0V53-R0), a USB connection, a power jack, an ICSP

store.arduino.cc

다음의 홈페이지에서 schematics in pdf를 다운받으면 됩니다.

이를 확인해보면 ATMega328P 라는 CPU가 아두이노에 어떻게 연결이 되어 있는지 확인을 할 수 있습니다.  간단하게 다음을 확인해보면 input output 의 관계를 알 수 있습니다.

 

중요한건 아두이노는 메인보드일 뿐이고 이 안에 달린 CPU인 ATMega328P 라는 친구가 진짜 주인공입니다. 이 친구의 블록선도를 확인하면 어떻게 동작을하는지 알 수 있습니다.

위에서 보는 것처럼 마이크로컴퓨터의 내부 구성이 이렇게 되어 있구나 라는 것을 알 수 있습니다. 각각의 기능들을 자세히 알아보겠습니다. 이 부분은 테블릿을 그림에 직접 적어서 설명하도록 하겠습니다.

 

테블릿 종이질감을 제거했더니 글씨체가 별로네요..  여튼 일단 다음과 같은 동작을 합니다. 

 

아두이노의 LED 점멸하기 

(많이 어렵고 저도 이해를 확실하게 못한 부분입니다.)

아두이노를 참고하는 것이 아니라 ATmega168에 직접 명령을 내리는 코드를 작성해봅시다. ATmega168의 PB1은 digital pin9번이니 여기 꼳고 GND에 꼳습니다.

그리고 다음처럼 코드를 작성했습니다.

#include<avr/io.h>

int main()
{
  int i, j;
  DDRB |= (1 <<PB1);
  
  while(1)
  {
    PORTB ^= (1 << PB1);
    for( i = 0; i<10; i++){
      for(j=0; j<10000; j++); // 이 부분 덕분에 LED까 점멸하는 것 같습니다. 
    }
  }
}

(실제로 하실 때는 절대로 다이렉트로 LED를 꼳지 마세요., 저항을 단 후에 LED를 달아야합니다.  저는 빵판이 없어서)
아마 저는 저항이 없어서 점멸하는 것이 보이지 않는 것 같습니다.

 

다시 코드를 살펴보면 다음처럼 생겼습니다

#include<avr/io.h>

int main()
{
  int i, j;
  DDRB |= (1 <<PB1);
  
  while(1)
  {
    PORTB ^= (1 << PB1);
    // XOR 연산자 ^ PB1의 초기상태가 1이면 0으로, 0이면 1로 바꾸는 역할을 합니다. 
    // if 연산을 하지 않아서 간결해지니 매우 유용한 방식이라고 합니다.
    for( i = 0; i<10; i++){
      for(j=0; j<10000; j++);
    }
  }
}

DDRB, PORTB, PB1이라는 친구들인데, 이게 무엇인지 확인을 해봅시다. 이에 대한 확인은 ATmega328의 데이터 시트에서 확인할 수 있습니다. 여기서 GPIO라고 불리는 I/O핀의 제어를 볼 수 있습니다. 

(조금 간단하게 이야기해서 DDRx가 해당 포트의 입출력 방향을 설정합니다. 1이면 출력으로 설정 0이면 입력으로 설정됩니다. PORTx는 핀들의 상태를 제어하는 레지스터 입니다.)

여기서 확인할 수 있는 것은 CPU와의 접속은 DATA Bus를 통해 접속되어 있다는 것을 알 수 있습니다. 여기서 위의 그림은 Pxn이라고 하는 하나의 I/O 포트 핀에 대한 기능 설명을 보여줍니다. 

각각픠 포트는 DDxn, PORTxn, PINxn으로 구성되어 있습니다. 

DDRx 레지스터의 DDxn 비트는 Pin의 방향을 선택합니다. DDxn 이 논리적으로 1일 경우 Pxn은 output pin으로 구성됩니다. DDxn이 논리적으로 0일 경우에는 Pxn 은 input pin으로 구성됩니다.

PORTxn이 output pin일 때  논리적으로 1이면 high가 출력되고,  input pin일 때 논리적으로 0이면 LOW가 출력됩니다.

일단 여기까지 읽었을 때 Pxn에 신호를 출력하기 위해서는 DDxn과 PortXn을 설정해야한다는 것을 알 수 있습니다.

다음을 확인하면 DDxn 과 PORTxn의 조합으로 어떻게 조합이 되는지 나오게 됩니다. 그래서 ATmega328p에서 15번핀에 (PB1, 아두이노의 9번핀)에 High 와 LOW를 출력하고 싶을 때 어떻게 설정을 해야하는지  위를 통해 확인할 수 있습니다. 

 

 

#include<avr/io.h>

int main()
{
  int i, j;
  DDRB |= (1 <<PB1);
  
  while(1)
  {
    PORTB ^= (1 << PB1);
    for( i = 0; i<10; i++){
      for(j=0; j<10000; j++);
    }
  }
}

다시 코드로 돌아와보면 우리는 PB1을 건들이는 것이 목적입니다. 그래서 블록선돌의 가장 밑의 부분을 확인해보니 PB0~7은 PROT B 라는 것을 알 수 있습니다. 그래서 PORT B를 대상으로 제어를 하게됩니다. 

그래서 다시 코드를 보면 DDRB와 PORTB에 쓰기를 실시하고 있습니다. 

 

그래서 요약을 하자면 원래 우리가 아두이노 코드를 작성할때는 아두이노 보드를 참고해서 작성했습니다. 그러나 이번에는 아두이노에 들어가는 ATmega CPU를 직접 조작하는 방법을 진행해봤습니다. 굉장히 복잡하면서도 재미있네요.

우선 우리가 꼳을 핀을 알아보고 그 핀에 해당하는 데이터 시트를 통해서 찾아보면서 말이죠.. 그러면 책에는 없지만 한 가지 응용을 해봅시다.

 

응용 

PD7번 포트(12번)에 LED를 접속하고 불이 들어오게 해봅시다.  

PD는  PORT D에 해당합니다. PORTD로 수정을 해봅시다.

#include<avr/io.h>

int main()
{
  int i, j;
  DDRD |= (1 <<PD7);
  
  while(1)
  {
    PORTD ^= (1 << PD7);
    for( i = 0; i<10; i++){
      for(j=0; j<10000; j++);
    }
  }
}

근데, 문제가 생겼습니다. LED가 죽어버렸습니다.. 역시 저항을 달고 해야하하나봅니다. 이럴 줄 알고 준비한 사이트가 팅커캐드입니다.

www.tinkercad.com

 

Tinkercad | From mind to design in minutes

Tinkercad is a free, easy-to-use app for 3D design, electronics, and coding.

www.tinkercad.com

잘 작동합니다.

여기서 ATmega의 포트를 변경하는 일을 할때는 무조건 비트연산자를 이용하여 수정을 해주어야합니다.

#include<avr/io.h>

int main()
{
  int i, j;
  DDRD |= (1 <<PD7);
  
  while(1)
  {
    PORTD ^= (1 << PD7);
    for( i = 0; i<10; i++){
      for(j=0; j<10000; j++);
    }
  }
}

그래서 위와 같은 코드를 if문으로 구성하게 되는 경우 다음과 같습니다.

#include <avr/io.h>

int main()
{
  int i, j;
  bool on_off = true;
  DDRD |= (1 << PD7);

  while (1)
  {
    if (on_off == true){
      on_off = false;
      PORTD |= (1 << PD7); // PD7을 HIGH(1)로 설정
    }
    else{
      on_off = true;
      PORTD &= ~(1 << PD7); // PD7을 LOW(0)으로 설정
    }
    for (i = 0; i < 10; i++)
    {
      for (j = 0; j < 10000; j++);
    }
  }
}

다음처럼 구성된다고 합니다. 보통 ATmega단에서 연산을 하는 경우 비트연산을 한다. 이 점을 알고 지나가야 할 것 같습니다. 

 

아두이노 우노에 코드가 전달되기 까지

이 동작들을 하나하나 어떻게 확인해야할지 몰라 글만 적습니다.

우리가 작성한 코드는 먼저 전처리(프리프로세싱)을 거치게됩니다. (#inldude <stdio.h>와 같은 모듈 불러오기)
그리고 컴파일을 진행해서 어셈블러어로 변환을 합니다. 

어셈블리어로 변환된 결과를 OBJ 현식으로 변환합니다. 이를 어셈블 처리라고 합니다.

그리고 의존하고 있는 라이브러리등을 합체하여 실행할 수 있는 파일로 변환하는데, 이를 링크 처리 단계라고 합니다. 

여기까지 마무리되면 임베디드 시스템에 ROM에 실행 형식의 파일을 기록해두어야합니다. 그래서 ROM에 기록하기 위해 HEX 파일로의 변환을 실시합니다.

보통 어셈블리어 영역에서 디버깅을 진행할 수 있다고 합니다. 스타트업 루틴 이라고 하는 행위는 임베디드 시스템은 main 함수를 호출하기 전에 하드웨어 초기 설정, 소프트웨어가 동작하기 위한 초기 설정이 진행됩니다. 

 

스택

스택은 함수를 호출할 때에 이용되는 메모리 영역입니다. 자신의 함수에서 다른 함수를 호출할 때 파라미터를 건네는 번지나 돌아올 번지를 기억해야합니다. 이 때 스택이 사용됩니다. 

또한 인터럽트 발생시에도 스택이 이용되는데, 인터럽트가 발생하기 전에 처리하고 있던 번지를 되돌아갈 번지로 스택에 등록합니다. 추가적으로 CPU가 갖고 있던 내부 상태도 스택에 넣어둡니다. 

여튼 CPU에도 연산 결과를 스택에 보관해 두면 인터럽트가 종료한 후에 연산 결과를 CPU에 복원해, 중단하기 전의 상태로부터 올바로 처리할 수 있게 됩니다. 

 

C언어와 임베디드 시스템

대부분의 임베디드 시스템은 제약 사항으로 메모리 용량에 제한이 있거나 처리 시간에 제약이 있기 때문에 프로그램을 최적화해야합니다. 

volatile 선언을 통해 주기적으로 하드웨어를 감시해 상태가 변한 것을 감시하는 처리가 빈번히 실행됩니다. 이를 폴링이라고 하는데 이는 인터럽트 기능이 없는 주변장치를 감시할 때 사용합니다. 

unsigned와 signed는 산술연산에서 +인지 -인지 가려내는 방법

pragma 하드웨어나 메모리 주소를 지정하여 데이터나 코드를 배치하고 싶은 경우 사용

포인터와 배열처럼 임베디드 시스템의 코드를 작성할 때는 하드웨어의 성능을 고려한 코딩을 하는 것이 중요하다.

인터럽트 처리는 간단하고 최소한의 처리만을 해야하도록 해야한다. 

 

 

Delay 를 활용하는 방법

#include<avr/io.h>

int main()
{
  int i, j;
  DDRD |= (1 <<PD7);
  
  while(1)
  {
    PORTD ^= (1 << PD7);
    for( i = 0; i<10; i++){
      for(j=0; j<10000; j++);
    }
  }
}

앞선 코드에서 for(j=0; j<10000; j++)을 사용하여 LED를 점멸 시켰었습니다. 이를 활용하지 말고 delay를 활용해서 코드를 작성할 수 있습니다.

#include<avr/io.h>
#include<util/delay.h>

int main()
{
  DDRD |= (1 <<PD7);
  
  while(1)
  {
    PORTD ^= (1 << PD7);
    _delay_ms(1000);
    
  }
}

이 경우 앞서 for문을 사용하는 것보다 쉽게 처리가 가능합니다. for문을 사용하는 경우에는 cpu의 처리속도에 영향을 받습니다. 그래서 내가 아두이노로 코드를 작성해놓고, CPU가 개선된 아두이노에서 다시 사용하려 한다면 같은 코드일지라도 정확한 시간으로 제어하는 것이 불가능해집니다.

그래서 delay를 사용하는 것이 좋은데, delay가 사용되는 동안은 CPU를 점유하기 때문에 단순한 작업에서만 사용하는 것이 권장되며, Arduino 보드에 내장된 타이머를 사용하는 것이 추천됩니다.