이글의 전부 또는 일부, 사진, 소스프로그램 등은 저작자의 동의 없이는 상업적인 사용을 금지합니다. 또한, 비상업적인 목적이라하더라도 출처를 밝히지 않고 게시하는 것은 금지합니다.


DHT22 온도 습도 센서를 구매했습니다. Aliexpress에서 무료배송으로 $2.5 정도면 구입 가능합니다. 공식 모델명은 AM2302인데 DHT22로 더 알려져 있는 듯합니다. 이 센서를 다루는 코드는 arduino 용 코드도 많고, stm32용 코드로 github 등에서 어렵지 않게 구할 수 있습니다만, 여러가지 이유로 마땅치 않아서 직접 매뉴얼을 보고 코딩했습니다. 앞으로는 DHT22 대신에 AM2302라는 명칭을 사용하겠습니다.

AM2302는 동작 전압의 범위가 3.3V ~ 5.5V이고, 최대 소비 전류가 1.5mA 밖에 안되서 blue pill의 3.3V에 직접 연결했습니다.



1. 프로젝트 만들기


앞의 글 ST7565P GLCD 제어하기(제3편) - printf() 사용하기에서 사용한 프로젝트를 그대로 사용해도 됩니다. 딱 한가지 차이가 있다면 PC15 핀을 AMD2302_DATA라는 라벨을 붙여 GPIO_Output으로 지정하는 점입니다.

PC15 핀을 통해서 AM2302와 통신하기 위해서 이 핀을 일단 GPIO_Output으로 지정했습니다. AM2302로부터 정보를 가져 오려면 먼저 MCU가 AM2302에게 시작 신호를 보낸 후에, 이 핀으로 정보를 입력 받습니다. 시작 신호를 보내기 위해서는 PC15 핀이 GPIO_Output으로 설정되어 있어야 하며, 정보를 입력 받으려면 PC15 핀이 GPIO_Input으로 설정되어 있어야 합니다. 이를 위해서 프로그램 내부에서 PC15 핀을 GPIO_Output에서 GPIO_Input으로, 또 그 반대로 전환시켜야 합니다. 본 글에서는 HAL 드라이버를 사용하지 않고, 이전의 글 RA8835 GLCD 제어하기(제2편) - STM32F103 GPIO 다루기(1)에서 다룬 내용대로 직접 레지스터에 기록하는 방법을 사용합니다. 자세한 사항은 아래에 있는 AM2302ReadData() 함수 내의 소스 프로그램을 보기 바랍니다.

본 글에서는 프로젝트를 새로 생성하는 것으로 진행하겠습니다.

1) STM32CubeMX를 실행하고 MCU로 STM32F103CbT6을 지정합니다.

2) [SYS] 항목의 [Debug]를 Serial Wire로 지정하고, [RCC] 항목의 [HSE]를 Crystal/Ceramic Resonator로 지정합니다.

3) Pinout view에서 다음 그림과 같이 지정합니다.




4) Timer2를 이용해서 3초마다 한 번씩 측정할 것이므로 Timer2를 다음과 같이 설정합니다.



 


[Parameter Setiting] 탭에서 Timer2의 [Clock Source]를 Internal Clock으로 설정하고, [Prescaler]에는 35999, [Counter period]에는 2999를 입력합니다.  이 값은 다음 단계에서 설정하는 Clock Configuration과 관계 있으며, 결국에는 3초에 한 번씩 TIM2 인터럽트를 발생시키도록 설정하는 것입니다.

[NVIC Settings] 탬에서 [TIM2 global interru[t] Enable 항목에 체크하여 인터럽트를 활성화시킵니다.



5) Clock Configuration에서 다음 그림과 같이 지정합니다.



[PLL Source Mux]의 입력 값으로 HSE를 지정하고, [System Clock Mux]의 입력 값으로 PLLCLK를 지정합니다. HCLK(MHz)를 72로 지정합니다. 우리가 사용할 TIM2 타이머는 APB1 BUS Clock을 사용합니다. 이 버스의 clock을 36MHz가 되도록 조정합니다. 위의 4)단계에서 이 클럭을 프리스케일러가 36000로 나누도록 하여 1mS마다 TIM2 카운터가 1씩 증가하고, Couter Period를 2999로 설정하여 3000번에 한 번씩 인터러브가 발생하도록 했습니다. 즉, 최종적으로 3초에 한 번씩 TIM2인터럽트가 발생하도록 설정했습니다.

STM32F103 시리즈의 모든 GPIO들은 APB2 BUS에 연결되어 있으므로, APB2 peripheral clock에 따라 동작합니다. 이 clock을 최저 속도인 4.5MHz로  맞춥니다. 앞 글에서 언급한 바가 있습니다만, 이 clock 값을 높이면 M19264Display.c에 있는 GlcdWriteCommand() 함수와 GlcdWriteData() 함수, GlcdReadData() 함수에 적절히 시간 지연 루틴들을 넣어야 합니다.



6) [Project Manager]에서 다음 그림과 같이 지정하고 [GENERATE CODE] 버튼을 클릭하여 프로젝트를 생성합니다.





2. 파일 복사하기


앞의 글 ST7565P GLCD 제어사기(제3편) - printf() 사용하기에서 사용하던 다음의 파일들을 본 프로젝트로 복사합니다. 헤더 파일들은 [Inc] 폴더 아래로, c 파일들은 [Src] 폴더 아래로 복사합니다. 윈도우즈 파일 탐색기로 복사하면 별도의 조치를 취하지 않아도, 자동으로 True Studio가 인식합니다.

① GldDisplay.h
② M19264Display.h
③ ST7565Font.h

④ GlcdDisplay.c
⑤ M19264Display.c
⑥ ST7565Font.c




3. AM2302 제어 함수 만들기


AM2302는 단 하나의 선으로 MCU와 통신 합니다. AM2302로부터 값을 가져오려면 먼저 MCU에서 다음과 같이 시작 신호를 보내고, AM2302으로부터 응답 신호를 받아야 합니다.


① MCU가 low 신호를 1mS 이상 보낸다.

② MCU가 high 신호를 보낸다.

③ AM2302가 low 신호를 80uS 보낸다.

④ AM2302가 high 신호를 80uS 보낸다.


위 단계를 마친 후에는 다음과 같은 신호를 40 개 보내옵니다.



AM2302가 50uS의 low 신호를 보낸 후에 26uS의  high 신호를 보내오면 0이고, 70uS의 high 신호를 보내오면 1입니다. 즉, 50uS의 low 신호 이후의 high 신호의 길이로 0과 1을 판별합니다.

위 내용대로 구현하려면 uS 단위로 축정하는 함수가 필요합니다. 인터넷 검색을 해보면 uS 단위의 대기 함수들이 여러 종류 있습니다. 대표적인 예가 다음의 경우입니다.

void Delay_us(uint32_t us){  
                if(us>1){
                         uint32_t count=us*8-6;
                         while(count--); 
                 }else{
                         uint32_t count=2;
                         while(count--); 
                  }
          }

(출처: https://m.blog.naver.com/compass1111/221070105883)



오실로스코프로 측정하여도 정확한 결과가 나온다고 합니다.


(출처: https://m.blog.naver.com/compass1111/221070105883)


하지만 True Studio로 실행해보면 제대로 동작하지 않습니다. 이유는 optimization 때문입니다. 기본적으로 True Studio는 프로젝트를 생성할 때에 optimizing 옵션을 -Os(optimize for size)로 정합니다. [Project] 메뉴의 [Properties], [C/C++ Build], [Settings], [Tool Settings], [C Compiler], [Optimization]을 차례대로 선택해가면 확인할 수 있습니다.

Optimization이 되면 단순히 루프를 돌고 있는 루틴을 컴파일러가 과감히(?) 없애 버리는 듯합니다. 위의 uS 대기 함수는 제대로 동작하지 않습니다. Optimization을 -O0(none)으로 하거나, Timer 인터럽트나 Systick을 사용하는 방법을 사용해야할 듯합니다. 본 글에서는 루프를 돌면서 대기하는 가장 간단한 방법을 사용하기로 했습니다. 루프의 최대치를 기다려도 응답하지 않는 경우에는 에러로 처리하고 빠져 나오도록 설계했습니다.


AM2302가 보내오는 40비트의 내용은 다음과 같습니다.



간단히 정리해 보면, 맨 앞의 16비트는 상대 습도(%) * 10, 다음 16비트는 온도(C) * 10, 나머지 8비트는 checksum입니다.



[Inc] 폴더에 AM2302.h 파일을 만들고 다음의 내용들을 입력합니다.

#define AM2302_SUCCESS                0      // return value when reading was success
#define AM2302_NOT_REPLY              1      // return value when AM2302 did not send low signal
#define AM2302_SIGNAL_ERR             2      // return value when AM2302 did not send high signal
#define AM2302_DATA_NOT_START         3      // return value when AM2302 did not send 
#define AM2302_CHKSUM_ERR                       4      // return value when checksum is not correct
#define AM2302_DATA_HIGH_ERR          100    // return value when AM2302 signal high time was not proper

#define AM2302_MAX_REPLY_TIME         100                    // max repeat times to wait AM2302 to reply
#define AM2302_SIGNAL_LENGTH          (120 + 120 * 0.1)      // max repeat times for AM2302 reply 50uS
#define AM2302_DATA_LOW_LENGTH        (100 + 100 * 0.1)      // max repeat times for AM2302 low signal 50uS
#define AM2302_DATA_HIGH_LENGTH_1     AM2302_SIGNAL_LENGTH   // max repeat times for AM2302 high signal 1, 70 uS
#define AM2302_DATA_HIGH_LENGTH_0     (AM2302_DATA_HIGH_LENGTH_1 / 2)      // max repeat times for AM2302 high signal 1, 26 uS

#define AM2302_DATA_BITS              40

typedef struct {
	int16_t humid, temp;
	uint8_t checksum;
}AM2302DATA;

uint8_t AM2302ReadData(AM2302DATA *pData);
uint32_t WaitForLow(uint32_t max);
uint32_t WaitForHigh(uint32_t max);



[Src] 폴더 안에 AM2302.c 파일을 만들고 다음의 함수들을 만듭니다.


#include "stm32f1xx_hal.h"
#include "main.h"
#include "AM2302.h"

uint8_t AM2302ReadData(AM2302DATA *pData)
{
	uint32_t tmp;
	uint32_t i;
	uint16_t data[AM2302_DATA_BITS] = {0,};

	pData->humid = pData->temp = pData->checksum = 0;
	// set AM2302_DATA Pin for output
	tmp = AM2302_DATA_GPIO_Port->CRH;
	tmp &= 0x0FFFFFFF;
	tmp |= 0x20000000;
	AM2302_DATA_GPIO_Port->CRH = tmp;

	// transmit start signal
	AM2302_DATA_GPIO_Port->BSRR = AM2302_DATA_Pin;
	HAL_Delay(250);
	AM2302_DATA_GPIO_Port->BRR = AM2302_DATA_Pin;
	HAL_Delay(1);
	AM2302_DATA_GPIO_Port->BSRR = AM2302_DATA_Pin;

	// set AM2302_DATA Pin for  pull-up input
	tmp = AM2302_DATA_GPIO_Port->CRH;
	AM2302_DATA_GPIO_Port->ODR = AM2302_DATA_Pin;
	tmp &= 0x0FFFFFFF;
	tmp |= 0x80000000;
	AM2302_DATA_GPIO_Port->CRH = tmp;

	// wait for AM2302's reply
	if(WaitForLow(AM2302_MAX_REPLY_TIME) > AM2302_MAX_REPLY_TIME)
		return AM2302_NOT_REPLY;

	// AM2302's low signal
	if(WaitForHigh(AM2302_SIGNAL_LENGTH) > AM2302_SIGNAL_LENGTH)
		return AM2302_SIGNAL_ERR;

	// wait for data
	if(WaitForLow(AM2302_SIGNAL_LENGTH) > AM2302_SIGNAL_LENGTH)
		return AM2302_DATA_NOT_START;

	// put 40 wait repeat times to array data[]
	for(i = 0;i < AM2302_DATA_BITS;i++)
	{
		WaitForHigh(AM2302_DATA_LOW_LENGTH);
		data[i] = WaitForLow(AM2302_DATA_HIGH_LENGTH_1);
		if(data[i] > AM2302_DATA_HIGH_LENGTH_1)                 // error occurred
			return (AM2302_DATA_HIGH_ERR + i);
	}

	// calculate humidity
	for(i = 0;i < 16;i++)
	{
		if(i > 0) pData->humid <<= 1;
		if(data[i] > AM2302_DATA_HIGH_LENGTH_0)
			pData->humid |= 1;
	}

	// calculate temp
	for(;i < 32;i++)
	{
		if(i > 16) pData->temp <<= 1;
		if(data[i] > AM2302_DATA_HIGH_LENGTH_0)
			pData->temp |= 1;
	}

	// calculate checksum
	for(;i < AM2302_DATA_BITS;i++)
	{
		if(i > 32) pData->checksum <<= 1;
		if(data[i] > AM2302_DATA_HIGH_LENGTH_0)
			pData->checksum |= 1;
	}

	// check checksum
	if(pData->checksum != (pData->humid / 256 + pData->humid % 256 + pData->temp / 256 + pData->temp % 256) % 256)
		return AM2302_CHKSUM_ERR;
	return AM2302_SUCCESS;
}

uint32_t WaitForLow(uint32_t max)
{
	uint32_t time = 0;
	while((AM2302_DATA_GPIO_Port->IDR & AM2302_DATA_Pin) && (time++ < max));
	return time;
}

uint32_t WaitForHigh(uint32_t max)
{
	uint32_t time = 0;
	while(!(AM2302_DATA_GPIO_Port->IDR & AM2302_DATA_Pin) && (time++ < max));
	return time;
}


AM2302이 MCU와 신호를 주고 받으면서 전송하는 것이 아니라, 일방적으로 신호를 보내기 때문에 신호를 받는 도중에 MCU가 다른 작업을 하면 신호를 놓칩니다. 따라서 위의 작업 도중에 인터럽트가 발생하지 않도록 조치할 필요가 있습니다. AVR의 어셈블리어에서는 CLI 한 줄이면 되었는데, STM32는 아직 그 기능을 어떻게 구현해야할지 감이 잡히지 않아 처리하지 못했습니다.




4. main.c 수정하기 및 TIM2 인터럽트 처리하기


위의 AM2302 제어 함수들을 사용하기 위해서 다음과 같이 main.c 파일을 수정합니다.

① printf() 함수를 사용할 수 있도록 _write() 함수를 정의합니다. 자세한 내용은 앞의 글 ST7565P GLCD 제어하기(제3편) - printf() 사용하기를 참조하십시오.


② 필요한 파일들을 포함시킵니다.

/* USER CODE BEGIN Includes */
#include "M19264Display.h"
#include "GlcdDisplay.h"
#include "AM2302.h"

/* USER CODE END Includes */


③ TIM2 인터럽트 처리를 위해서 전역 변수 uint8_t intFlag을 정의하고, DisplayChar() 함수 원형을 선언합니다. 주석은 삽입한 위치를 알리기 위해 같이 기술해 놓은 것입니다.

/* USER CODE BEGIN PV */
uint8_t intFlag;

/* USER CODE END PV */

/* USER CODE BEGIN PFP */
uint8_t DisplayChar(uint8_t ch);

/* USER CODE END PFP */


④ main() 함수 안에 필요한 변수를 선언합니다

  /* USER CODE BEGIN 1 */
  uint8_t ret;
  AM2302DATA data;

  /* USER CODE END 1 */


⑤ 전역 변수초기화 및 Glcd 초기화, TIM2 인터럽트를 시작하는 루틴을 넣습니다.

  /* USER CODE BEGIN 2 */
  HAL_Delay(2000);
  intFlag = NO_INTERRUPT;
  HAL_TIM_Base_Start_IT(&htim2);
  GlcdInitialize();

  /* USER CODE END 2 */


⑥ while(1) 문 안에 루프를 돌면서 intFlag의 값을 검사하여 적절한 시기에 AM2302Read() 함수를 호출하고, 그 결과 값을 그래픽 lcd에 printf() 함수를 이용해서 출력하는 루틴을 넣습니다.

    if(intFlag & INT_TIM2_AM2302)      // if TIM2 INT signal is set
    {
      ret = AM2302ReadData(&data);
  	  HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
      if(ret == AM2302_SUCCESS)
      {
        GlcdGraphicGotoxy(0, 0);
        printf("온도:%3d.%d\n", data.temp / 10, data.temp % 10);
        GlcdGraphicGotoxy(TEMP_SECOND_COL, 0);
        printf("습도:%3d.%d\n", data.humid / 10, data.humid % 10);
      }
      else
      {
        GlcdGraphicClear();
        printf("Read Error(%d)\n", ret);
      }
	  intFlag &= ~INT_TIM2_AM2302;      // clear TIM2 INT signal
    }
    HAL_Delay(1);


AM2302Read() 함수에서 에러가 발생하지 않았으면, 그 리턴 값이 AM2302_SUCCESS이므로 이 경우에 그래픽 lcd에 온도와 습도를 출력하고 있습니다.


⑦ main.h 파일에 전역 변수 intFlag 선언을 추가하고, 인터럽트 처리 및 그래픽 lcd 출력에 사용한 매크로들을 추가합니다. 주석 처리한 문장들은 추가한 위치를 알리기 위해 표시해 놓았습니다.

/* USER CODE BEGIN EFP */
extern uint8_t intFlag;

/* USER CODE END EFP */


/* USER CODE BEGIN Private defines */
#define NO_INTERRUPT                  0
#define INT_TIM2_AM2302               (1 << 0)

#define TEMP_SECOND_COL               (M19264_RESOLUTION_X / 2)

/* USER CODE END Private defines */



⑧ TIM2 인터럽트 처리를 위해 [Src] 폴더 안에 있는 stm32f1xx_it.c 파일을 열고, TIM2_IRQHandler() 함수에 다음과 같이 한 줄을 추가합니다.

  /* USER CODE BEGIN TIM2_IRQn 0 */
  intFlag |= INT_TIM2_AM2302;
  /* USER CODE END TIM2_IRQn 0 */

인터럽트는 말 그대로 다른 작업을 하다가 빠져 나오는 것이므로 가급적 인터럽트 서비스 루틴은 짧게 끝나야 합니다. 본 글에서는 TIM2 인터럽트가 발생하면 intFlag를 설정하는 작업만 간단히 실행하고 서비스 루틴을 종료니다. 나머지 시간이 걸리는 작업은 main() 함수의 while 루프에서 실행합니다.



동작하는 사진입니다.




동작하는 소스프로그램을 zip 파일로 압축하여 첨부합니다.

STM32F103DHT22.zip




블로그 이미지

엠쿠스

Microprocessor(STM32, AVR)로 무엇인가를 만들어 보고자 학습 중입니다.

,