# Tizen IoT 아날로그 센서 사용하기
:::hw
Board|Raspberry 3/3+|| Dev. Board
:::hw
Sensor|Analog sound sensor || sound sensor
Sensor|SEN0159||CO2 sensor
:::hw
Chip|MCP 3008||Analog to Digital Converter
:::hw
Etc.|BreadBoard||
:::hw
Etc.|Jumper Wires||
:::
:::sw
OS|Tizen 5.0 or higher| <https://developer.tizen.org/tizen/tizen/release-notes>
SDK|Tizen Studio 3.3|<https://developer.tizen.org/development/tizen-studio/download>
SDK|IoT-Headless-5.0 extension SDK|<https://docs.tizen.org/iot>
:::
## [강좌 Source Code](https://github.com/jay4peace/tizeniot_pio_spi/releases)
## Analog sound sensor
> * Operating voltage:5V
> * Detects the sound intensity
> * Interface: **Analog** and Digital

AO : Analog Output
G : Ground
+ : VCC
DO : Digital Output
본 강좌에서는 센서의 인터페이스 중 아날로그 인터페이스를 사용하려고 합니다.
하지만 아날로그 출력을 입력으로 받을 수 있는 부분이 Raspberry 3/3+ 보드에 없으므로 센서를 보드에 직접 연결하여 사용할 수는 없습니다.

## Analog to Digital Converter (MCP3008)
이번 강좌에서는 MCP3008이라는 ADC를 이용해 아날로그 센서와 디지털 신호를 받는 보드 사이를 연결해보도록 하겠습니다.
### [MCP3008 제조사 Datasheet](https://cdn-shop.adafruit.com/datasheets/MCP3008.pdf)

> * **10-bit resolution (0 ~ 1023)**
> * 8 input channels
> * **SPI serial interface**
아날로그 - 디지털 컨버터는 아래 그림처럼 특정 주기마다 아날로그 신호 값를 디지털 신호값으로 변환하여 주는 장치입니다.
(수학적으로 적분하는 동작을 통해 그래프의 특정 구간 크기를 구합니다.)

MCP3008은 보드와 연결할때 SPI 인터페이스를 사용합니다. SPI 인테페이스를 사용하기에 앞서 SPI가 어떤 것인지 알아보겠습니다.
## SPI (Serial Peripheral Interface)

위 그림에서 SPI Master가 보드쪽, SPI Slave가 MCP3008쪽입니다. 각각의 Pin 이름에 대한 설명은 아래와 같습니다.
* MISO (Master Input, Slave Output) : 마스터의 입력, 슬레이브의 출력 - DOUT
* MOSI (Master Output, Slave Input) : 마스터의 출력, 슬레이브의 입력 - DIN
* CLK : 통신 동기화 클럭
* CE (Chip Enable) : 슬래이브 선택 출력(CS, SS)
그림의 화살표 방향으로 실제 데이터가 전송됩니다. 마스터 장치의 CE 갯수에 따라 연결가능한 슬래이브 갯수가 정해지는데, Raspberry Pi 3는 2개의 CE pin이 있습니다.
예를 들어,Raspberry Pi 3와 MCP3008은 아래와 같은 핀들을 사용하여 연결 할 수 있습니다.

SPI 인터페이스는 일반적으로 아래 그림과 같이 2개의 register를 이용한 circular 형태로 통신을 하고 있어 매우 빠른 속도의 양방향 통신을 지원합니다. (이런 구조에서는 한 클럭에 1bit를 서로 주고 받을 수 있으므로 동기화 클럭에 따라 속도가 달라집니다.)

[위키피디아](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface)의 SPI 부분을 참조하시면 더 자세한 내용을 확인하실 수 있습니다.
## SPI API on Tizen
Tizen은 주변장치 연결을 위한 Peripheral IO API를 제공하고 있고, 그 중 SPI 인테페이스를 위한 API는 아래와 같습니다.
```c
/* Open & Close interface */
int peripheral_spi_open(int bus, int cs, peripheral_spi_h *spi);
int peripheral_spi_close(peripheral_spi_h spi);
/* Setup interface */
int peripheral_spi_set_mode(peripheral_spi_h spi, peripheral_spi_mode_e mode);
int peripheral_spi_set_bit_order(peripheral_spi_h spi, peripheral_spi_bit_order_e bit_order);
int peripheral_spi_set_bits_per_word(peripheral_spi_h spi, uint8_t bits);
int peripheral_spi_set_frequency(peripheral_spi_h spi, uint32_t freq_hz);
/* Transfer data */
int peripheral_spi_read(peripheral_spi_h spi, uint8_t *data, uint32_t length);
int peripheral_spi_write(peripheral_spi_h spi, uint8_t *data, uint32_t length);
int peripheral_spi_transfer(peripheral_spi_h spi, uint8_t *txdata, uint8_t *rxdata, uint32_t length);
```
위 API를 이용해서 보드와 MCP3008간에 어떻게 데이터 송수신을 할 수 있는 지 알아보겠습니다.
먼저 `peripheral_spi_open()`의 첫번째와 두번째 파라미터인 `bus`와 `cs(CE)` 값을 알기 위해서는 보드의 설정 정보가 필요합니다.
Raspberry Pi 3의 [설정 정보](https://developer.tizen.org/development/iot-extension-sdk/api-guides/tizen-peripheral-io-native-api/spi)는 아래와 같습니다.

다음으로 인터페이스를 설정하기 위한 아래 4개의 API는 Slave 장치의 정보가 필요합니다.
```c
int peripheral_spi_set_mode(peripheral_spi_h spi, peripheral_spi_mode_e mode);
int peripheral_spi_set_bit_order(peripheral_spi_h spi, peripheral_spi_bit_order_e bit_order);
int peripheral_spi_set_bits_per_word(peripheral_spi_h spi, uint8_t bits);
int peripheral_spi_set_frequency(peripheral_spi_h spi, uint32_t freq_hz);
```
저희가 사용 중인 MCP3008의 정보는 datasheet에서 필요한 정보를 찾아야 합니다.

위와 같이 datasheet에서 설정 정보를 찾을 수 있습니다.
`peripheral_spi_set_mode()`의 두번째 파라미터는 mode 0,0에 해당하는 `PERIPHERAL_SPI_MODE_0`을 사용하면 됩니다.
`peripheral_spi_set_bit_order()`의 두번째 파라미터는 datasheet의 다른 부분에서도 설명이되고 있지만, 그림의 데이터 값 순서(B9 ~ B0)를 보면 `PERIPHERAL_SPI_BIT_ORDER_MSB` 임을 알 수 있습니다.
`peripheral_spi_set_bits_per_word()`의 두번째 파라미터는 위 그림에서 8bit임을 알 수 있습니다.
`peripheral_spi_set_frequency()`는 datasheet의 3page **ELECTRICAL SPECIFICATIONS** 표에서 확인할 수 있으며 3.6Mhz가 최대 clock frequency입니다.
위의 정보를 바탕으로 아래와 같이 MCP3008을 초기화 하는 함수를 만들 수 있습니다.
```c
static peripheral_spi_h MCP3008_H = NULL;
static unsigned int ref_count = 0;
int adc_mcp3008_init(void)
{
int ret = 0;
char *model_name = NULL;
if (MCP3008_H) {
ref_count++;
return 0;
}
ret = peripheral_spi_open(0, 0, &MCP3008_H);
if (PERIPHERAL_ERROR_NONE != ret) {
return -1;
}
ret = peripheral_spi_set_mode(MCP3008_H, PERIPHERAL_SPI_MODE_0);
if (PERIPHERAL_ERROR_NONE != ret) {
goto error_after_open;
}
ret = peripheral_spi_set_bit_order(MCP3008_H, PERIPHERAL_SPI_BIT_ORDER_MSB);
if (PERIPHERAL_ERROR_NONE != ret) {
goto error_after_open;
}
ret = peripheral_spi_set_bits_per_word(MCP3008_H, 8);
if (PERIPHERAL_ERROR_NONE != ret) {
goto error_after_open;
}
ret = peripheral_spi_set_frequency(MCP3008_H, 3600000);
if (PERIPHERAL_ERROR_NONE != ret) {
goto error_after_open;
}
ref_count++;
return 0;
error_after_open:
peripheral_spi_close(MCP3008_H);
MCP3008_H = NULL;
return -1;
}
```
SPI인터페이스를 이용하여 MCP3008을 초기화 하였으니, 데이터를 송/수신하는 방법에 대해 알아 보겠습니다.
SPI 인터페이스는 송/수신이 동시에 가능하므로, `peripheral_spi_read()` , `peripheral_spi_write()` 함수 보다는 `peripheral_spi_transfer()`함수를 주로 사용합니다.
MCP3008에게 보드가 전달할 데이터는 MCP3008의 8개 입력 채널 중 어떤 채널의 값을 전달 받을 지 선택하는 입력 데이터 입니다.
위 그림의 trasmitted data 부분에 start bit 1 이후의 4 bit 데이터가 그 선택 입력 데이터 입니다.
아래 datasheet 내용과 같이 4bit 데이터를 각 채널에 따라 선택하면 됩니다.

8bit word에서 상위 4bit 데이터의 값이므로 아래와 같은 값을 사용하면 됩니다.
```c
#define MCP3008_TX_CH0 0x80 /* 0b10000000 */
#define MCP3008_TX_CH1 0x90 /* 0b10010000 */
#define MCP3008_TX_CH2 0xA0 /* 0b10100000 */
#define MCP3008_TX_CH3 0xB0 /* 0b10110000 */
#define MCP3008_TX_CH4 0xC0 /* 0b11000000 */
#define MCP3008_TX_CH5 0xD0 /* 0b11010000 */
#define MCP3008_TX_CH6 0xE0 /* 0b11100000 */
#define MCP3008_TX_CH7 0xF0 /* 0b11110000 */
```
MCP3008에서부터 데이터를 받는 부분은 조금 복잡합니다.
먼저 datasheet에서 수신 데이터의 형식을 살펴보면 아래 그림과 같습니다.

3개의 word중 첫번째 word는 무시할 수 있는 값들입니다.
두번째 word는 끝에서 3번째 bit에 NULL(0)로 데이터 시작을 알리는 bit가 있고, 10bit 데이터 중 상위 2bit 데이터가 포함되어 있습니다.
세번째 word는 10bit 데이터 중 하위 8bit 데이터가 포함되어 있습니다.
이 정보를 바탕으로 MCP3008의 특정 채널 입력데이터를 얻는 코드를 아래와 같이 작성 할 수 있습니다.
```c
#define MCP3008_TX_WORD1 0x01 /* 0b00000001 */
#define MCP3008_TX_CH0 0x80 /* 0b10000000 */
#define MCP3008_TX_CH1 0x90 /* 0b10010000 */
#define MCP3008_TX_CH2 0xA0 /* 0b10100000 */
#define MCP3008_TX_CH3 0xB0 /* 0b10110000 */
#define MCP3008_TX_CH4 0xC0 /* 0b11000000 */
#define MCP3008_TX_CH5 0xD0 /* 0b11010000 */
#define MCP3008_TX_CH6 0xE0 /* 0b11100000 */
#define MCP3008_TX_CH7 0xF0 /* 0b11110000 */
#define MCP3008_TX_WORD3 0x00 /* 0b00000000 */
#define MCP3008_RX_WORD1_MASK 0x00 /* 0b00000000 */
#define MCP3008_RX_WORD2_NULL_BIT_MASK 0x04 /* 0b00000100 */
#define MCP3008_RX_WORD2_MASK 0x03 /* 0b00000011 */
#define MCP3008_RX_WORD3_MASK 0xFF /* 0b11111111 */
#define UINT10_VALIDATION_MASK 0x3FF
int adc_mcp3008_read(int ch_num, unsigned int *out_value)
{
unsigned char rx[3] = {0, };
unsigned char tx[3] = {0, };
unsigned char rx_w1 = 0;
unsigned char rx_w2 = 0;
unsigned char rx_w2_nb = 0;
unsigned char rx_w3 = 0;
unsigned short int result = 0;
if (out_value == NULL)
return -1;
if (ch_num < 0 || ch_num > 7)
return -1;
tx[0] = MCP3008_TX_WORD1;
switch (ch_num) {
case 0:
tx[1] = MCP3008_TX_CH0;
break;
case 1:
tx[1] = MCP3008_TX_CH1;
break;
case 2:
tx[1] = MCP3008_TX_CH2;
break;
case 3:
tx[1] = MCP3008_TX_CH3;
break;
case 4:
tx[1] = MCP3008_TX_CH4;
break;
case 5:
tx[1] = MCP3008_TX_CH5;
break;
case 6:
tx[1] = MCP3008_TX_CH6;
break;
case 7:
tx[1] = MCP3008_TX_CH7;
break;
default:
tx[1] = MCP3008_TX_CH0;
break;
}
tx[2] = MCP3008_TX_WORD3;
peripheral_spi_transfer(MCP3008_H, tx, rx, 3);
rx_w1 = rx[0] & MCP3008_RX_WORD1_MASK;
if (rx_w1 != 0)
return -1;
rx_w2_nb = rx[1] & MCP3008_RX_WORD2_NULL_BIT_MASK;
if (rx_w2_nb != 0)
return -1;
rx_w2 = rx[1] & MCP3008_RX_WORD2_MASK;
rx_w3 = rx[2] & MCP3008_RX_WORD3_MASK;
result = ((rx_w2 << 8) | (rx_w3)) & UINT10_VALIDATION_MASK;
*out_value = result;
return 0;
}
```
조금 복잡해 보이지만 `switch`문에서 채널을 선택하여 `tx` 버퍼를 완성하고,
`peripheral_spi_transfer()`를 통해 `tx`버퍼와 `rx`버퍼를 입력하면, 선택된 채널의 데이터가 `rx`버퍼에 담겨 리턴됩니다.
앞에서 설명한 것과 같이 3개의 word로 구성된 `rx`버퍼에서 필요한 bit만 선택하여 데이터를 추출하면 됩니다.
```c
rx_w2_nb = rx[1] & MCP3008_RX_WORD2_NULL_BIT_MASK;
```
이 부분에서 두번째 word의 6번째 bit가 정상적으로 NULL(0)로 시작하는 지 체크하고,
```c
rx_w2 = rx[1] & MCP3008_RX_WORD2_MASK;
rx_w3 = rx[2] & MCP3008_RX_WORD3_MASK;
```
이부분에서 두번째 word의 하위 2bit와 3번째 word의 8 bit를 추출합니다.
```c
result = ((rx_w2 << 8) | (rx_w3)) & UINT10_VALIDATION_MASK;
```
앞서 추출한 10 bit 데이터를 최종적으로 한 변수에 저장하면 됩니다.
앞에서 설명한 내용의 완성본 코드는 `adc-mcp3008.c` 파일입니다.
## 회로 연결하기
앞서 SPI 인터페이스를 이용해 데이터를 읽는 방법을 알아봤으니 실제 회로 연결을 어떻게 하는지 알아보겠습니다.
SPI는 앞서 언급했던 최소 4개의 핀(MISO, MOSI, CLK, CE)의 연결이 필요하고, 회로의 전원과 접지 등을 위해서
많은 수의 연결이 필요하니 유의하여 연결하셔야 합니다.
회로의 최종 연결은 위의 그림과 같이 연결되어야 합니다. MCP3008의 핀을 바로 연결 할 수 없으니, Bread board를 이용하여 연결하도록 하겠습니다.
### Raspberry pi 3 연결도

위 연결도를 참조하시여 연결하시면 어렵지 않게 연결 할 수 있습니다.
> #### RPI3 연결 사진




## Sample application 작성
센서의 값을 수집하는 sample application의 `analog-sensor.c` 파일의 코드 일부를 발췌하였습니다.
```c
static Eina_Bool __get_value(void *data)
{
int ret = 0;
unsigned int value = 0;
app_data *ad = data;
if (!ad) {
_E("failed to get app_data");
service_app_exit();
return ECORE_CALLBACK_CANCEL;
}
ret = sound_sensor_read(0, &value);
retv_if(ret != 0, ECORE_CALLBACK_RENEW);
// _D("sensor value - [%u]", value);
return ECORE_CALLBACK_RENEW;
}
static void service_app_control(app_control_h app_control, void *data)
{
app_data *ad = data;
ret_if(!ad);
if (ad->idler) {
ecore_idler_del(ad->idler);
ad->idler = NULL;
}
ad->idler = ecore_idler_add(__get_value, ad);
if (!ad->idler)
_E("Failed to add idler");
return;
}
```
위 코드에서 `service_app_control()` 콜백 함수가 불리면 `ecore_idler_add()`함수를 통해 main loop이 `idle` 상태일때마다 `__get_value()` 콜백 함수가 호출되고
`__get_value()` 함수에서 SPI를 이용하여 MCP3008에 연결된 sound sensor의 값을 읽어 오게됩니다.
sound sensor는 아날로그 센서이므로 무수히 많은 데이터가 지속적으로 출력되는데, 위 코드에서는 main loop이 여력이 있는 만큼 센서 데이터를 읽어오게 됩니다.
`__get_value()` 함수에서 호출하는 `sound_sensor_read()`함수의 내부는 아래와 같습니다.
```c
int sound_sensor_read(int ch_num, unsigned int *out_value)
{
unsigned int read_value = 0;
int ret = 0;
if (!initialized) {
ret = adc_mcp3008_init();
retv_if(ret != 0, -1);
initialized = 1;
}
ret = adc_mcp3008_read(ch_num, &read_value);
retv_if(ret != 0, -1);
print_bar(read_value);
*out_value = read_value;
return 0;
}
```
MCP3008에서 수집된 값을 그대로 출력하는 아주 간단한 코드입니다.
`print_bar()`은 입력된 sound sensor 값을 bar형태로 출력해주는 간단한 함수입니다.
로그를 통해 어떤 데이터가 출력되는지 확인해보겠습니다.
## Appendix
### CO2 sensor(SEN0159)
> * [Source Code](https://git.tizen.org/cgit/apps/native/st-things-co2-meter)
> * [제조사 Datasheet](https://media.digikey.com/pdf/Data%20Sheets/DFRobot%20PDFs/SEN0159_Web.pdf)

> * Operating voltage:5V
> * **The output voltage of the module falls as the concentration of the CO2 increases**
> * **Interface: Analog**
#### 연결
SPI 연결은 위 Sound sensor의 연결도를 참고하여 연결하면 됩니다.
MCP3008과의 연결은 아래와 같이 간단하게 연결됩니다.
> #### CO2센서와 MCP3008 연결 사진

#### Sample application
co2 센서의 값을 수집하는 sample application의 `co2.c` 파일의 코드 일부를 발췌하였습니다.
```c
#define SENSOR_CH_CO2 (0)
#define SENSOR_GATHER_COUNT (60)
#define SENSOR_GATHER_INTERVAL (0.05f)
static Eina_Bool __get_co2(void *data)
{
int ret = 0;
unsigned int value = 0;
static unsigned int sum = 0;
static unsigned int count = 0;
app_data *ad = data;
if (!ad) {
_E("failed to get app_data");
service_app_exit();
return ECORE_CALLBACK_CANCEL;
}
if (!ad->co2_data) {
_E("failed to get co2_data");
service_app_exit();
ad->getter_co2 = NULL;
return ECORE_CALLBACK_CANCEL;
}
ret = co2_sensor_read(SENSOR_CH_CO2, &value);
retv_if(ret != 0, ECORE_CALLBACK_RENEW);
count++;
sum += value;
if (count == SENSOR_GATHER_COUNT) {
unsigned int avg = 0;
avg = sum/SENSOR_GATHER_COUNT;
_D("co2 avg - [%u]", avg);
sensor_data_set_uint(ad->co2_data, avg);
count = 0;
sum = 0;
}
return ECORE_CALLBACK_RENEW;
}
static void gathering_start(void *data)
{
app_data *ad = data;
ret_if(!ad);
ad->getter_co2 = ecore_timer_add(SENSOR_GATHER_INTERVAL, __get_co2, ad);
if (!ad->getter_co2)
_E("Failed to add getter_co2");
}
```
위 코드에서 `gathering_start()` 함수가 불리면 `SENSOR_GATHER_INTERVAL`마다 `__get_co2()` 함수가 호출되고
`__get_co2()` 함수에서 SPI를 이용하여 MCP3008에 연결된 co2센서의 값을 읽어 오게됩니다.
co2센서는 아날로그 센서이므로 무수히 많은 데이터가 지속적으로 출력되는데 위 코드에서는 `SENSOR_GATHER_INTERVAL`마다 수집한 데이터를 `SENSOR_GATHER_COUNT`만큼 수집하여 평균값을 취하고 있습니다.
(실질적으로 3초동안 50ms 간격으로 수집한 값의 평균을 co2센서의 값으로 출력)
`__get_co2()` 함수에서 호출하는 `co2_sensor_read()`함수의 내부는 아래와 같습니다.
```c
int co2_sensor_read(int ch_num, unsigned int *out_value)
{
unsigned int read_value = 0;
int ret = 0;
if (!initialized) {
ret = adc_mcp3008_init();
retv_if(ret != 0, -1);
initialized = true;
}
ret = adc_mcp3008_read(ch_num, &read_value);
retv_if(ret != 0, -1);
*out_value = read_value;
return 0;
}
```
MCP3008에서 수집된 값을 그대로 출력하는 코드입니다.
하지만 통상적으로 co2값은 ppm(parts per million)이라는 단위를 사용하는데 이를 위해서는 MCP3008에서 출력된 값을 ppm으로 변환하여야 합니다.
co2센서 값을 ppm으로 변환하는 방법은 `co2-sensor.c`파일에 예제코드로 구현되어 있으나, co2센서는 실제 환경에서 calibration이 필요하므로 예제 코드와 [제조사 설명](https://sandboxelectronics.com/?p=147#Theory) 링크를 참조하여 필요한 부분을 수정후 사용하면 됩니다.
Notice
Are you sure to delete this post?
How to use analog sensor on TizenIoT (Korean)
1
0
|
Last modified on August 14, 2019
Craft info. | |
Maker |
![]() |
Status | Complete |
Period | ~ |
About This Craft | |
How to use analog sensor on Tizen IoT | |
Making Note
본 강좌의 소스코드는 공개를 위한 내부 확인 절차 진행 중입니다. 절차가 완료되면 Tizen git repository를 통해 공개하도록 하겠습니다.
본 강좌의 소스코드가 업로드 되었습니다.
https://git.tizen.org/cgit/apps/native/st-things-co2-meter
본 강좌의 소스코드 중 smartthings 관련 부분이 5.0 IoT extenstion 버전에 맞게 업데이트 되었습니다.
https://git.tizen.org/cgit/apps/native/st-things-co2-meter 에서 최신 코드를 확인 부탁드립니다.