2016년 2월 25일 목요일

라즈베리 파이로 Blynk 서버 만들기(Make local Blynk server using Raspberry Pi)

Blynk를 사용할 때 자신의 컴퓨터에 로컬 서버를 만들어 사용할 수도 있다. 여기서는 라즈베리 파이를 사용해 자신만의 로컬 blynk 서버를 구축하는 방법을 설명하겠다.

Blynk 서버는 java로 구현되어 있어 설치하려면 먼저 java 8이 설치되어 있어야만 한다. 현재 설치되어 있는 버젼을 확인하려면 다음과 같이 하면 된다.

% java -version
Output: java version "1.8.0_40"

설치되어 있지 않거나 버젼이 낮은 경우 다음과 같이 설치해 주면 된다.

% sudo apt-get install oracle-java8-jdk

Java 8이 설치되었으면 서버를 다운받는다.

% wget "https://github.com/blynkkk/blynk-server/releases/download/v0.13.0/server-0.13.0.jar"

다운이 완료되면 바로 실행할 수 있다.

% java -jar server-0.13.0.jar -dataFolder ~/Blynk     

 이 경우 디폴트 하드웨어 포트는 8442, 디폴트 어플리케이션 포트는 8443(SSL port)가 된다. 실행하고 잠시 기다리면 화면에 아래와 같은 메시지가 나오게 된다.

Blynk Server successfully started.
All server output is stored in current folder in 'logs/blynk.log' file.
 
만일 라즈베리 파이가 부팅될 때 blynk 서버가 자동으로 실행되게 하고 싶으면 /etc/init.d/rc.local 파일의 맨 끝 부분에 아래 줄을 추가해 주면 된다.

java -jar /home/pi/server-0.13.0.jar -dataFolder /home/pi/Blynk &

아니면 다른 방법으로는 crontab을 이용할 수도 있다.

% crontab -e 

위의 명령을 실행한 후 아래의 내용을 추가한 다음 저장하고 종료하면 된다.

@reboot java -jar /home/pi/server-0.13.0.jar -dataFolder /home/pi/Blynk &

위와 같이 실행하면 모든 설정이 디폴트로 실행되게 되는데 설정을 변경하고 싶으면 서버가 있는 디렉토리에 'server.properties' 파일을 만들어 설정 내용을 넣어주면 된다.

아래는 server.properties 예제 파일이다.


#hardware ssl port
hardware.ssl.port=8441

#hardware plain tcp/ip port
hardware.default.port=8442

#http port
http.port=8080

#web sockets ssl port
ssl.websocket.port=8081
#web sockets plain tcp port
tcp.websocket.port=8082


#https port
https.port=9443

#application ssl port
app.ssl.port=8443

#by default server uses embedded in jar cert to simplify local server installation.
#WARNNING DO NOT USE THIS CERTIFICATES ON PRODUCTION OR IN WHERE ENVIRNOMENTS REAL SECURITY REQUIRED.
#provide either full path to files either use '.' for specifying current directory. For instance "./myfile.crt"
server.ssl.cert=
server.ssl.key=
server.ssl.key.pass=
client.ssl.cert=
client.ssl.key=

#by default System.getProperty("java.io.tmpdir")/blynk used
data.folder=

#folder for logs.
logs.folder=./logs

#log debug level. trace|debug|info|error. Defines how precise logging will be.
log.level=info

#defines maximum allowed number of user dashboards. Needed to limit possible number of tokens.
user.dashboard.max.limit=10

#defines maximum allowed widget size in KBs as json string.
user.widget.max.size.limit=10

#user is limited with 100 messages per second.
user.message.quota.limit=100
#in case of consistent quota limit exceed during long term, sending warning response back to exceeding channel
#for performance reason sending only 1 message within interval. In millis
user.message.quota.limit.exceeded.warning.period=60000

#maximum allowed number of notification queue. Queue responsible for processing email, pushes, twits sending.
#Because of performance issue - those queue is processed in separate thread, this is required due
#to blocking nature of all above operations. Usually limit shouldn't be reached.
notifications.queue.limit=10000

#this setting defines how often we can send mail/tweet/push or any other notification. Specified in seconds
notifications.frequency.user.quota.limit=60

#maximum size of user profile in kb's
user.profile.max.size=128

#period in millis for saving all user DB to disk.
profile.save.worker.period=60000

#period in millis for saving stats to disk.
stats.print.worker.period=60000

#specifies maximum period of time when application socket could be idle. After which
#socket will be closed due to non activity. In seconds. Default value 600 if not provided.
#leave it empty for infinity timeout
app.socket.idle.timeout=600
#specifies maximum period of time when hardware socket could be idle. After which
#socket will be closed due to non activity. In seconds. Default value 15 if not provided.
#leave it empty for infinity timeout
hard.socket.idle.timeout=15

#Enables native socket transport for Linux using JNI. Should be turned on only if you 100% sure.
#may not work on some environments. Used to increase server performance. Performance boost is ~20-40%.
enable.native.epoll.transport=false

#Enabled native openSSL support for SSL handlers. Should be turned on only if you 100% sure.
#may not work on some environments. Used to increase server performance. Performance boost is ~16%.
#For more details see - http://netty.io/wiki/forked-tomcat-native.html
enable.native.openssl=false

#mostly required for local servers setup in case user want to log raw data in CSV format
#from his hardware
enable.raw.data.store=true

#size of async logger ring buffer. should be increased for loads >2-3k req/sec
async.logger.ring.buffer.size=8192


#ADMINISTRATION SECTION

admin.rootPath=/admin

#administration https port
administration.https.port=7443

#comma separated list of administrator IPs. allow access to admin UI only for those IPs.
#you may set it for 0.0.0.0/0 to allow access for all.
#you may use CIDR notation. For instance, 192.168.0.53/24
allowed.administrator.ips=127.0.0.1


#comma separated list of users allowed to create accounts. leave it empty if no restriction required.
allowed.users.list=

이제 설치한 로컬 서버를 사용하려면 먼저 스마트폰 앱을 실행하고 로컬 서버에 어카운트를 만들어 줘야 한다.


 Create New Account 버튼을 누른다.


디폴트로는 blynk 서버에 어카운트가 만들어 지는데 여기서는 로컬 서버에 만들어야 하기 때문에 먼저 위에 표시한 버튼을 눌러준다.


 로컬 서버를 지정해 준다. IP주소는 로컬 blynk 서버가 설치된 라즈베리 파이의 ip 주소이고 포트 번호는 따로 server.properties 파일에서 수정해 주지 않았다면 디폴트 어플리케이션 포트인 8443을 넣어주고 OK 버튼을 누른다.


 이제 로컬 서버에서 사용할 어카운트 이름(이메일주소)와 암호를 입력하고 'Create New Account' 버튼을 누른다.


 어카운트가 만들어 지고 자동으로 로그인이 되었다. 이제 'Create New Project' 버튼을 눌러 프로젝트를 만들어 주면 된다.


 프로젝트 이름, 이 프로젝트에서 사용할 보드를 지정하고 'Create project' 버튼을 누르면 된다. 화면 중간에 AUTH TOKEN이라는 값이 있는데 (여기서는 53b32b....5996c95) 이 값이 일종의 암호 역할을 하기 때문에 보드의 프로그램 코드에 넣어줘야 하는 값이다.


 이제 위에서 지정한 'test1'이란 이름의 프로젝트가 만들어 진걸 볼 수 있다.

하드웨어 보드의 프로그램 역시 blynk서버 대신 로컬 서버를 사용하도록 변경해 줘야 한다.

스케치 코드의 설정 부분에는 Blynk.begin(auth); 아니면 Blynk.begin(auth, ssid, pass); 가 들어 있는데 각각 뒤쪽에 로컬 서버를 지정해 줘야 한다. 즉 아래와 같이 수정해 줘야만 한다.

Blynk.begin(auth);    -> Blynk.begin(auth, "hostname"); 또는 Blynk.begin(auth, IPAddress(192,168,0,222));

Blynk.begin(auth, ssid, pass);    -> Blynk.begin(auth, ssid, pass, "hostname"); 또는 Blynk.begin(auth, ssid, pass, IPAddress(192,168,0,222));

위에서 auth는 프로젝트 생성시의 AUTH TOKEN값...즉 여기서는 "53b32b....5996c95" 이고, ssid, pass는 공유기의 SSID와 접속 암호가 된다. hostname 이나 IPAddress()의 파라미터 값은 각자 라즈베리 파이의 hostname 또는 IP주소로 바꿔줘야 한다.
























2016년 2월 16일 화요일

Atmel Studio 7.0으로 아두이노 메가/우노 사용하기 (Programming Arduino Mega/Uno using Atmel Studio 7.0)

아두이노 IDE를 사용하면 아두이노 보드들을 쉽게 프로그래밍 할 수 있지만 레지스터를 직접 조작해 다양한 peripheral들을 세부적으로 조작하려면 Atmel studio를 사용할 수 밖에 없는 경우가 생긴다.

먼저 arduino ide를 설치해 준다. 아두이노 메가의 플래쉬에 프로그램을 집어 넣기 위해 avrdude라는 툴을 사용하는데 아두이노 IDE에 포함되어 있기 때문이다. avrdude의 위치는 c:\Program files\Arduino\hardware\tools\avr\bin 폴더에 들어 있다. 사용하기 쉽게 하기 위해 저 폴더에 있는 avrdude.exe 파일과 c:\Program files\Arduino\hardware\tools\avr\etc 폴더에 있는 avrdude.conf 파일을 사용하기 편한 위치의같은 폴더에 집어 넣어 준다.



여기서는 C:\Utils 폴더에 avrdude.exe와 avrdude.conf 파일을 넣어 주었다.

이제 Atmel studio 7.0을 실행한 후 상단의 메뉴 항목에서 Tools->External Tools 를 선택한다.

창이 열리면 우측의 'Add' 버튼을 누른 후 아래쪽 4개의 칸을 다음과 같이 입력해 준다.



Title: MEGA writing  <- 임의의 이름을 지정
Command: C:\Utils\avrdude.exe <- avrdude.exe 파일의 위치를 지정
Arguments: -v -v -patmega2560 -cwiring -PCOM9 -D -Uflash:w:$(TargetDir)\$(TargetName).hex:i <- avrdude를 호출할 때 사용할 파라미터들. 여기서 COM9는 자신의 환경(아두이노 메가의 시리얼 포트)에 맞게 변경해 줘야 함
Initial directory: $(TargetDir) <- 이대로 지정

그리고 Use Output window 항목을 체크해 주고 'OK' 버튼을 눌러주면 된다.



다시 메뉴에서 Tools를 선택하면 MEGA writing 이라는 항목이 생긴걸 볼 수 있다.

이제 프로젝트를 만들어 프로그램을 빌드한 후 'MEGA writing' 을 선택하면 빌드 된 hex 파일이 아두이노 메가의 플래쉬에 기록되어 실행되게 된다.

프로젝트를 만드는 방법은 이전 포스트(Atmel Studio에서 아두이노 듀에 사용하기)를 참고하면 된다. 다만 프로세서는 아두이노 메가에 맞는 것으로 선택해 줘야 한다.



프로그램을 완성해서 빌드한 후 방금 추가한 'MEGA writing' 을 선택하면 아두이노에서보다 훨씬 빨리 hex 파일이 플래쉬에 기록되는걸 확인할 수 있다.

----

그리고 프로그램을 만들어 컴파일 하다 보면 _delay_ms() 함수등에서 F_CPU가 선언되어 있지 않다고 에러가 나는 경우가 있다. 이런 경우 상단 메뉴바에서 Project 항목을 누른 후 맨 아래쪽에 프로젝트 Properties... 항목을 선택한다. (여기서는 megatest라는 이름의 프로젝트이므로 megatest Properties...라고 나왔지만 실제 megatest 대신 각자 프로젝트 이름이 나온다)



 Properties 탭이 열리면 Toolchain -> AVR/GNU C Compiler -> Symbols를 선택한다.
 기본적으로 Defined symbols 항목에 'DEBUG' 하나만 들어 있다.



Defined Symbols 오른쪽 끝의 + 버튼을 누르면 'Add Defined Symbols'라는 창이 열린다.


여기에 위와 같이 'F_CPU=16000000UL' 이라고 넣어주고 'OK' 버튼을 눌러주면 된다. 이제 다시 빌드 해 보면 에러가 없어졌을 것이다.

또한 printf() 함수로 출력되는 내용을 시리얼 포트로 보내주려고 하는 경우 '-Wl,-u,vfprintf -lprintf_flt -lm' 같은 내용의 링커 옵션을 추가해 줘야 하는 경우도 있다.

이 때는 Toolchain -> AVR/GNU Linker -> Miscellaneous 를 선택한 후 'Other Linker Flags'에 원하는 옵션을 추가해 주면 된다.

 


---
아두이노 우노를 사용하는 경우도 위와 동일하다. 다만 차이점은

Arguments: -v -v -patmega328p -carduino -PCOM9 -D -Uflash:w:$(TargetDir)\$(TargetName).hex:i

저기서 atmega2560 대신 atmega328p로, wiring을 arduino로 바꿔주는것 뿐이다. 그 이후는 프로젝트를 만들 때 프로세서만 atmega2560 대신 아두이노 우노의 프로세서인 atmega328p를 선택해 주면 된다.

2016년 2월 3일 수요일

Atmel Studio에서 아두이노 듀에(arduino due) 사용하기

Atmel studio에서 아두이노 듀에에 바이너리 파일을 넣으려면 bossa라는 프로그램이 필요하다. BOSSA(http://www.shumatech.com/web/products/bossa)는 Basic Open-Source Samba Application의 약자로 ATMEL의 ARM 프로세서 플래쉬에 writing을 위한 프로그램이다. 원래 ATMEL에서 SAMBA라는 툴을 제공하나 설정이나 사용이 복잡해 이 SAMBA를 기반으로 사용하기 쉽게 만든 프로그램이 BOSSA이다.

다만 기존의 bossa로는 아두이노 듀에에 사용할 수 없어 아두이노용으로 수정된 bossa가 필요하다. 혹시 자신의 컴퓨터에 bossac (BOSSA command line)이 설치되어 있으면 프롬프트에서 bossac -h 명령으로 버젼을 확인할 수 있다.


이렇게 나오면 아두이노 용으로 수정된 버젼이다. 만일 그렇지 않으면 아두이노용 bossa를 구해야 한다.

가장 쉬운 방법은 먼저 아두이노를 설치해서 실행한다. 실행한 후 화면 상단 메뉴 '툴->보드->보드 매니저'를 선택한다. 아래 스크린샷은 이미 설치가 되어 있지만 아두이노를 처음 설치하면 Arduino Due는 사용할 수 없는 상태이기 때문에 먼저 패키지를 설치해 줘야 한다.


 보드 매니저 윈도우에서 Arduino SAM Boards (32-bits ARM Cortex-M3) 를 선택한다.


 선택하면 오른쪽에 '설치' 버튼이 나타나므로 버튼을 눌러 패키지를 설치한다.


 설치가 끝나면 '닫기'를 눌러 보드매니저 윈도우를 닫고 다시 '툴->보드' 메뉴로 가 보면 이제 보드 목록 아래쪽에 Arduino Due 항목들이 보일 것이다.


 아두이노 듀에를 위한 툴체인이 설치된 것이다. 이제 설치된 툴체인에서 bossac을 찾으면 되는데 아두이노 1.6.5부터 툴체인 설치 위치가 이전 버젼과 달라졌을 뿐 아니라 시스템 숨김 파일로 되어 있어 그냥 찾으려고 하면 컴퓨터를 다 뒤져 봐도 찾을 수가 없다.

먼저 탐색기를 열어 사용자->[자신의ID] 폴더로 간다.


 탐색기의 '구성->폴더 및 검색 옵션' 메뉴을 선택한다.


폴더 옵션 윈도우가 열리면 '보기' 탭을 선택한 다음 '고급설정' 리스트를 아래로 스크롤 해 보면 아래 항목이 보일 것이다. 따로 설정을 변경하지 않았으면 디폴트로는 '보호된 운영 체제 파일 숨기기(권장)'가 체크 되어 있고, '숨김 파일 및 폴더'는 '숨김 파일, 폴더 또는 드라이브 표시 안 함'이 선택되어 있다.


 '보호된 운영 체제 파일 숨기기(권장)'는 체크를 없애고, '숨김 파일 및 폴더'는 '숨김 파일, 폴더 또는 드라이브 표시'를 선택한 다음 '확인' 버튼을 눌러준다.


이제 약간 희미한 폴더 아이콘들이 몇개 더 보이게 되었다. 여기서 'AppData' 폴더를 연다.


그 아래로 계속 쫒아가면 '...\AppData\Local\Arduino15\packages\arduino\tools' 에 bossac 폴더가 보인다. bossac 폴더 안에 폴더가 하나 더 있고 그 안에 bossac.exe 파일이 들어있다. 그냥 그 위치에 두고 사용해도 되지만 매번 찾기 귀찮으니 c 드라이브의 최 상단에 \utils 같은 폴더를 하나 만들어 그곳에 bossac.exe를 복사해 놓고 사용하면 편리하다.


이제 Atmel Studio를 실행한 후 상단 메뉴에서 'Tools->External Tools...'를 선택한다.


External Tools 윈도우가 열리면 초기 상태는 아래와 같다.


내용을 다음과 같이 변경한 후 'OK'버튼을 눌러 준다.

Title: Due writing <- 자신이 원하는 임의의 이름
Command: C:\Utils\bossac.exe   <- 아두이노용 bossac.exe 파일의 위치
Arguments: -e -w -v -b $(TargetDir)\$(TargetName).bin -R  <- 이대로 똑같이 입력
Initial directory: $(TargetDir)       <- 이대로 똑같이 입력

Use Output window는 체크


이제 Tools 메뉴를 보면 자신이 선택한 이름의 항목이 추가된 걸 볼 수 있다.


듀에에 USB 케이블은 Native USB 쪽에 꼽아주면 된다.


이제 SAM3X8E 프로세서용으로 프로젝트를 만들어 Build 한 후 위에서 추가한 메뉴항목(여기서는 Due writing)을 선택하면 bin 파일을 듀에의 플래쉬에 writing 해 준다. 만일 writing 하는데 포트를 찾을 수 없다고 에러가 나오면 먼저 ERASE 버튼을 잠시 눌러 준 후 듀에를 리셋 시키고 writing 을 하면 된다.




2016년 2월 2일 화요일

아두이노에서 FreeRTOS 사용하기 - 4. Mutual Exclusion (상호배제)


이번 주제인 mutual exclusion(상호 배제)는 여러 태스크가 같은 자원에 동시에 억세스 하지 못하도록 보장해 주는 메커니즘이다. 

두개의 태스크가 동시에 같은 자원(여기서는 가상의 프린터 )를 사용하는 경우 태스크가 실행되는 도중에 RTOS에 의해 선점되면 출력 결과가 아래와 같은 식으로 되어 버린다.

Printer 1: +++++++++++Printer 2: -----------------------------
++++++++++++++++++
Printer 1: Printer 2: -----------------------------
+++++Printer 2: -----------------------------
++++++++++++++++++++++++
Printer 2: -----------------------------
PrintePrinter 2: -----------------------------
r 1: ++++++++Printer 2: -----------------------------
+++++++++++++++++++++
Printer 1: Printer 2: -----------------------------
+++++++++++++++++++++++++++++
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 1: +++++++++++++++Printer 2: -----------------------------
++++++++++++++
Printer 1: ++++++Printer 2: -----------------------------
+++++++++++++++++++++++
Printer 2: -----------------------------

위의 결과가 나온 코드이다.

#include <FreeRTOS_AVR.h>

#define MS(x) ((unsigned long)(x)/portTICK_PERIOD_MS)

void vPrint(void *);

void setup()
{
  Serial.begin(115200);

  xTaskCreate(vPrint, "Printer1", 200, (void *)"Printer 1: +++++++++++++++++++++++++++++", 1, NULL);
  xTaskCreate(vPrint, "Printer2", 200, (void *)"Printer 2: -----------------------------", 2, NULL);

  vTaskStartScheduler();
  while (1) ;
}

void loop()
{
}

void vPrint(void *pvParameters)
{
  unsigned char led_state = 0;

  while (1) {
    printer((char *)pvParameters);
    vTaskDelay(MS(random(100)));
  }
}

void printer(char *p)
{
  while (*p != '\0') {
    Serial.print(*p++);
    delay(1);
  }
  Serial.println(" ");
}

하지만 원하는 것은 한 태스크가 프린터로 출력하는 동안 다른 태스크가 방해하지 못하게 하는 것이다.

Printer 1: +++++++++++++++++++++++++++++
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 2: -----------------------------
Printer 1: +++++++++++++++++++++++++++++
Printer 1: +++++++++++++++++++++++++++++

이런식으로 출력되게 하기 위해 한 태스크가 프린터를 사용하는 동안 다른 태스크는 프린터에 접근하지 못하고 대기하게 하기 위해 MUTEX를 사용한다.

#include <FreeRTOS_AVR.h>

#define MS(x) ((unsigned long)(x)/portTICK_PERIOD_MS)

SemaphoreHandle_t xMutex;

void vPrint(void *);

void setup()
{
  Serial.begin(115200);

  xMutex = xSemaphoreCreateMutex();
  if (NULL != xMutex) {
    xTaskCreate(vPrint, "Printer1", 200, (void *)"Printer 1: +++++++++++++++++++++++++++++", 1, NULL);
    xTaskCreate(vPrint, "Printer2", 200, (void *)"Printer 2: -----------------------------", 2, NULL);

    vTaskStartScheduler();
  } else {
    Serial.println("Create mutex failed.");
  }
  while (1) ;
}

void loop()
{
}

void vPrint(void *pvParameters)
{
  unsigned char led_state = 0;

  while (1) {
    xSemaphoreTake( xMutex, portMAX_DELAY);       // block forever until the semaphore is given
    // Critical section start
    printer((char *)pvParameters);
    // Critical section end
    xSemaphoreGive( xMutex );
    vTaskDelay(MS(random(500)));
  }
}

void printer(char *p)
{
  while (*p != '\0') {
    Serial.print(*p++);
    delay(1);
  }
  Serial.println(" ");
}

위의 코드에서 빨간색으로 표시된 부분을 Critical Section(임계영역)이라고 한다. 여러 태스크가 공유하지만 동시에 억세스하면 안되는 자원을 억세스 하는건 critical section 내에서만 해야 한다. C.S.는 xSemaphoreTake()로 시작하고 xSemaphoreGive()로 끝난다. 한 태스크가 C.S. 내에 있는 코드를 실행하고 있는 동안 다른 태스크가 C.S. 안으로 들어가려고 하면 xSemaphoreTake()에서 블럭되어 현재 C.S. 내에서 실행중이 태스크가 빠져 나오면서 xSemaphoreGive()를 호출할 때 까지 sleep 상태로 대기하게 되므로 동시에 두개 이상의 태스크가 C.S. 내로 진입할 수 없게 된다.  
 
 
1. CS에 아무 태스크도 진입하지 않은 상태

 2.태스크1이 xSemaphoreTake() 를 호출해 키를 가지고 CS에 진입

 3. 태스크1이 CS 내부에서 공유자원인 프린터를 사용

 4. 태스크1이 아직 CS 내에 있는데 태스크2도 CS에 진입하기 위해 xSemaphoreTake()를 호출했으나 키가 없어 CS에 진입하지 못하고 sleep 상태로 키가 돌아오기를 대기


 5. 태스크1이 CS에서 빠져나오며 xSemaphoreGive()를 호출해 키를 돌려줌

 6. Sleep상태로 기다리던 태스크2가 키가 돌아왔으므로 깨어나 CS에 진입
 
 
위의 예제에서는 태스크간에 공유하는 자원으로 프린터를 예로 들었지만 그 이외에 서로 공유하는 전역변수들 역시 억세스 할 때는 C.S. 내에서 해야 한다. 
 
그리고 태스크가 C.S.  내에 들어 있는 동안 그 C.S.에 진입하려고 하는 태스크는 블럭되어 실행되지 못하므로 ISR과 마찬가지로 C.S. 내에서도 가능한 빨리 작업을 마치고 빠져 나와야 한다.

2016년 2월 1일 월요일

아두이노에서 FreeRTOS 사용하기 - 3. 태스크 동기화


멀티태스킹 프로그램을 만들다 보면 태스크 간 또는 태스크와 ISR 간에 동기화가 필요한 경우가 있다. 

이 예제에는 LED라는 이름의 태스크가 실행되는데, 이 태스크는 외부에 연결된 Push button이 눌린 이벤트가릴 발생할 때 마다 led의 상태를 토글한다. 또 하나의 태스크인 PBSW는 스위치의 상태를 감시하다 버튼이 눌리면 그 이벤트를 LED 태스크에 통보한다.

LED 태스크는 버튼이 눌린 이벤트가 발생할 때 까지 대기해야 하는데 이를 위해 세마포어를 사용하게 된다. 여기서는 binary semaphore를 사용한다.



세마포어는 다음 함수로 만들 수 있다.

vSemaphoreCreateBinary ( xSemaphoreHandle xSemaphore );

세마포어에 대한 동작은 ‘take’ 또는 ‘give’ 두가지 밖에 없다.

xSemaphoreTake ( vSemaphoreHandle xSemaphore, portTickType xBlockTime );
xSemaphoreGive ( vSemaphoreHandle xSemaphore );

위의 두 매크로 모두 정상적으로 실행되었으면 pdTrue를 리턴하고, 에러가 발생하면 pdFalse를 리턴한다. xBlockTime 파라미터는 semaphore가 available할 때 까지 대기 시간이다. Queue 함수에서 사용된 것과 동일한 역할을 한다.

만일 Queue 또는 semaphore등을 ISR에서 사용해야 하는 경우에는 특별한 함수나 매크로를 사용해야만 한다. 이 함수나 매크로들은 이름의 마지막이 FromISR 로 끝난다. 예) xSemaphoreTakeFromISR, xSemaphoreGiveFromISR 

#include <FreeRTOS_AVR.h>

#define MS(x) ((unsigned long)(x)/portTICK_PERIOD_MS)

#define LED_PIN 13
#define SW_PIN 3

SemaphoreHandle_t xSemaphore;

void vLED(void *);
void vPBSW(void *);

void setup()
{
  Serial.begin(115200);

  vSemaphoreCreateBinary( xSemaphore );

  xTaskCreate(vLED, "LED", 200, NULL, 1, NULL);
  xTaskCreate(vPBSW, "PBSW", 200, NULL, 1, NULL);

  vTaskStartScheduler();
  while (1) ;
}

void loop()
{
}

void vLED(void *pvParameters)
{
  unsigned char led_state = 0;

  Serial.print("Task ");
  Serial.print((char *)pvParameters);
  Serial.println(" started...");

  pinMode(LED_PIN, OUTPUT);          // init LED
  while (1) {
    xSemaphoreTake( xSemaphore, portMAX_DELAY);       // block forever until the semaphore is given
    led_state =! led_state;                          // toggle LED state
    digitalWrite(LED_PIN, led_state);         // apply LED state
  }
}

void vPBSW(void *pvParameters)
{
  unsigned char last_state = 1;
  unsigned char cur_state;

  Serial.print("Task ");
  Serial.print((char *)pvParameters);
  Serial.println(" started...");

  pinMode(SW_PIN, INPUT_PULLUP);  // init Pushbutton switch
  while (1) {
    cur_state = digitalRead(SW_PIN);
    if ((1==last_state) && (0==cur_state)) {    // button pressed
      xSemaphoreGive( xSemaphore );
      last_state = 0;
    } else if ((1==last_state) && (0==cur_state)) {    // button released
      last_state = 1;
    }
    vTaskDelay(10/portTICK_PERIOD_MS);
  }
}

위의 코드에서 vLED 태스크는 xSemaphoreTake() 함수에서 실행을 중단하고 vPBSW 태스크에서 xSemaphoreGive() 함수를 호출해 줄 때 까지는 잠자는 상태로 대기하게 된다. 물론 세마포어를 사용하지 않고 전역변수를 이용해서도 유사하게 동작하게 할 수 있지만 그 경우는 busy waiting이 되기 때문에 상당히 비효율적이다.

void LED()
{
  ...
  while (1) {
    if (1 == gFlag) {
    led_state =! led_state;                          // toggle LED state
    digitalWrite(LED_PIN, led_state);         // apply LED state
    gFlag = 0; 
  }

void PBSW()
{
  ...
  while (1) {    
    if ((1==last_state) && (0==cur_state)) {    // button pressed
      gFlag = 1;
      last_state = 0;
    } else if ((1==last_state) && (0==cur_state)) {    // button released
      last_state = 1;
    }
    ...
  }
}

이 경우 LED 태스크는 쉬지 않고 계속 gFlag 변수값이 1이 되는지를 확인해야 한다. 그에 비해 세마포어를 사용하면 스위치가 눌리지 않는 한 LED 테스크는 별도로 CPU 자원을 사용하지 않고 sleep 상태를 유지하므로 그 시간에 다른 작업을 더 할 수 있다.