STM32 ADC DMA를 이용한 PT100 온도 측정 구현

서론

이전 글에서는 STM32와 PT100 아날로그 인터페이스 보드를 연결하여 ADC 값을 읽는 방법을 두 가지 방식으로 실험해 보았습니다.

첫 번째는 가장 단순한 방식인 Polling 방식으로, 100ms 주기마다 ADC를 읽을 때 16번 연속 샘플링하여 평균을 내는 구조였습니다.

이 방식은 구현이 단순하다는 장점이 있지만, Blocking 방식이기 때문에 동시에 동작하는 다른 프로세스에 영향을 줄 수 있습니다.

두 번째는 Time-Distributed Sampling 방식으로, 10ms마다 한 번씩 샘플링하고 100ms 주기에서 평균을 계산하는 Non-Blocking 구조였습니다.

이 방식은 샘플링 작업을 시간적으로 분산하여 순간적인 CPU 부하를 줄일 수 있다는 장점이 있지만, 여전히 CPU가 ADC 데이터를 직접 읽고 처리해야 한다는 한계가 있습니다.

이번 글에서는 CPU 개입을 최소화하고 보다 효율적인 샘플링 구조를 구현하기 위해 DMA(Direct Memory Access)를 이용한 ADC 샘플링 방식을 적용해 보겠습니다.

STM32F103의 ADC와 DMA 관련 상세한 사항은 Reference Manual을 참조하면 됩니다.
아래는 Reference Manual의 링크 입니다.
RM0008 Reference Manual

본론

실험환경

이번 실험 환경은 이전 두 번의 실험과 동일한 하드웨어 구성을 사용하였습니다.

PT100 대신 정밀 저항(149.3Ω)을 연결하여 온도 변화에 따른 저항값을 모사하였으며, PT100 아날로그 인터페이스 보드의 출력 전압을 STM32F103 개발보드의 ADC 입력(PC0)으로 연결하였습니다.

ADC DMA 동작 상태 및 측정 결과는 디버거의 Live Expression 기능을 이용하여 확인하였습니다.

ADC DMA 구현

CubeMX ADC DMA 설정

ADC에 DMA를 적용하기 위해서는 CubeMX에서 ADC 설정과 DMA 설정을 추가로 구성해야 합니다.

Parameter Settings에서는 기존 Polling 방식과 동일한 설정을 유지하되, Continuous Conversion Mode를 Enable로 변경하여 ADC가 연속적으로 변환을 수행하도록 설정합니다.

이 설정을 통해 ADC는 한 번 시작된 이후 별도의 CPU 개입 없이 지속적으로 샘플링할 수 있습니다.

DMA Settings 구성

ADC의 변환 데이터를 CPU가 직접 읽지 않고 메모리로 자동 전송하기 위해 DMA 설정이 필요합니다.

CubeMX의 DMA Settings 탭에서 ADC1에 연결할 DMA 채널을 추가합니다.

DMA Request는 ADC1에 연결된 기본 DMA 채널을 선택하면 되며, STM32F103에서는 DMA1 Channel 1이 자동으로 할당됩니다.

데이터 전송 방향은 ADC 데이터 레지스터에서 메모리로 이동하는 구조이므로 Peripheral to Memory로 설정합니다.

DMA Mode는 Circular Mode로 설정합니다.

Circular Mode는 지정한 버퍼 영역을 모두 채운 후 처음 위치로 돌아가 순환하면서 반복 저장하는 방식입니다.

이 설정을 통해 ADC는 연속 변환을 수행하고 DMA는 별도의 CPU 개입 없이 ADC 데이터를 지속적으로 메모리 버퍼에 저장할 수 있습니다.

Peripheral Increment는 Disable로 설정합니다.

ADC 데이터 레지스터 주소는 고정되어 있으므로 주소 증가가 필요하지 않습니다.

반면 Memory Increment는 Enable로 설정합니다.

ADC 데이터가 버퍼의 다음 위치에 순차적으로 저장되어야 하기 때문입니다.

데이터 폭은 ADC의 변환 결과에 맞추어 Peripheral과 Memory 모두 Half Word(16bit) 로 설정합니다.

STM32의 ADC는 12bit 해상도를 가지지만 실제 저장은 16bit 단위로 처리됩니다.

Priority는 Low로 설정하였습니다.

PT100 온도 측정은 고속 응답이 필요한 데이터가 아니므로 낮은 우선순위로도 충분합니다.

이렇게 설정하면 ADC와 DMA가 결합되어 CPU가 ADC 데이터를 직접 읽지 않아도 지속적인 샘플링 구조를 구현할 수 있습니다.

아래는 설정을 한 화면입니다.

설정을 완료한 후 프로젝트 코드를 생성하면 CubeMX가 DMA 관련 초기화 코드를 자동으로 생성합니다.

main.c 파일에서는 아래와 같이 MX_DMA_Init() 함수와 MX_ADC1_Init() 함수가 추가된 것을 확인할 수 있습니다.

DMA 초기화 함수는 ADC 초기화 이전에 먼저 호출되어야 하며, 이후 ADC 초기화와 DMA 시작 함수를 통해 ADC DMA 동작을 시작할 수 있습니다.

코드작성

이제 ADC DMA 동작을 위한 사용자 코드를 작성합니다.

프로젝트의 모듈화를 위해 ADC 관련 기능을 별도의 모듈로 분리하였습니다.

pt100_dmaadc.hpt100_dmaadc.c 파일을 생성하여 ADC DMA 초기화, 데이터 평균 처리, 전압 변환 기능을 구현하겠습니다.

이 방식은 향후 PT100 온도 변환 및 보정 기능을 추가할 때 확장성을 높이는 장점이 있습니다.

헤더파일(pt100_dmaadc.h)

아래는 pt100_dmaadc.h의 코드 입니다.

버퍼 크기는 64개 샘플로 정의하였으며, DMA는 이 버퍼를 순환하면서 ADC 데이터를 지속적으로 저장하게 됩니다.

ADC 기준 전압은 3.3V로 정의하고, 12bit ADC의 최대 값은 4095로 설정하였습니다.

구조체 PT100_DMAADC_t는 평균 ADC 값(raw_avg), 계산된 전압 값(voltage), 그리고 새로운 데이터 갱신 여부를 나타내는 플래그(update_flag)로 구성됩니다.

이 구조체를 사용하면 ADC 처리 결과를 하나의 객체로 관리할 수 있어 코드의 가독성과 유지보수성이 향상됩니다.

초기화 함수 PT100_DMAADC_Init()는 ADC DMA 동작을 시작하는 역할을 하며, PT100_DMAADC_Process_100ms() 함수는 일정 주기마다 DMA 버퍼의 평균값을 계산하여 전압으로 변환하는 역할을 담당합니다.

소스파일(pt100_dmaadc.c)

헤더 파일에서 선언한 함수와 변수를 소스 파일에서 실제로 구현합니다.

먼저 헤더 파일을 include하고 ADC 핸들 포인터를 내부 static 변수로 선언합니다.

이 포인터는 ADC 초기화 함수에서 전달받은 ADC 핸들을 저장하여 DMA 시작 함수에서 사용합니다.

또한 DMA가 ADC 데이터를 저장할 버퍼를 정의하고, 평균 ADC 값과 전압 값을 저장할 구조체 변수를 생성합니다.

DMA 버퍼는 모듈 내부에서만 사용되므로 static으로 선언하여 외부 접근을 제한하였습니다.

반면 측정 결과 구조체는 다른 모듈에서도 접근할 수 있도록 전역 변수로 선언하였습니다.

다음은 DMA로 ADC를 읽기 위한 초기화 함수를 구현합니다.

초기화 함수에서는 ADC 핸들을 내부 변수에 저장하고 측정 결과 구조체를 초기값으로 설정합니다.

raw_avgvoltage를 초기화하고 update_flag를 false로 설정하여 아직 유효한 측정값이 없음을 표시합니다.

이후 HAL_ADC_Start_DMA() 함수를 호출하여 ADC DMA 동작을 시작합니다.

첫 번째 인자는 ADC 핸들, 두 번째 인자는 DMA가 데이터를 저장할 버퍼의 시작 주소, 세 번째 인자는 버퍼의 크기입니다.

이 함수가 호출되면 ADC는 연속 변환을 시작하고 DMA는 ADC 변환 결과를 지정된 버퍼에 자동으로 저장합니다.

DMA가 Circular Mode로 설정되어 있기 때문에 버퍼 끝까지 저장한 후 다시 처음 위치로 돌아가 반복적으로 데이터를 갱신하게 됩니다.

즉, CPU는 ADC 데이터를 직접 읽지 않아도 항상 최신 ADC 데이터를 메모리 버퍼에서 확인할 수 있습니다.

다음 함수는 100ms 주기에서 호출되어 DMA 버퍼에 저장된 ADC 데이터를 처리하는 함수입니다.

DMA는 ADC 데이터를 버퍼에 계속 저장하지만, 개별 샘플은 노이즈 영향을 받을 수 있기 때문에 여러 샘플의 평균값을 계산하여 안정적인 측정값을 얻도록 하였습니다.

먼저 버퍼 전체를 순회하면서 모든 ADC 값을 누적합니다.

누적된 값을 버퍼 크기로 나누어 평균 ADC 값을 계산하고 raw_avg에 저장합니다.

이후 ADC 평균값을 실제 전압값으로 변환하여 voltage 변수에 저장합니다.

전압 변환은 ADC 기준 전압(3.3V)과 12bit ADC 최대값(4095)을 기준으로 계산합니다.

마지막으로 update_flag를 true로 설정하여 새로운 측정 데이터가 준비되었음을 알립니다.

메인 루프에서는 이 플래그를 확인하여 새로운 측정값이 준비되었을 때만 후속 처리를 수행할 수 있습니다.

이 구조를 사용하면 ADC 샘플링은 DMA가 처리하고 CPU는 일정 주기마다 평균 계산만 수행하므로 CPU 부하를 크게 줄일 수 있습니다.

Application 프로그램

ADC DMA 동작을 위한 모듈 작성이 완료되었으므로 이제 Application 프로그램에서 실제로 모듈을 호출하여 ADC 데이터를 처리합니다.

아래는 Application 프로그램인 app.c 코드입니다.

먼저 프로그램 초기화 함수인 appInit()에서 PT100_DMAADC_Init(&hadc1) 함수를 호출하여 ADC DMA 동작을 시작합니다.

이 함수가 호출되면 ADC는 연속 변환을 시작하고 DMA는 ADC 데이터를 메모리 버퍼에 자동으로 저장하기 시작합니다.

이후 메인 루프인 appMain()에서는 100ms 주기 루틴에서 PT100_DMAADC_Process_100ms() 함수를 호출하여 DMA 버퍼의 평균값을 계산하고 전압으로 변환합니다.

새로운 측정 데이터가 준비되면 update_flag가 true로 설정되며, Application 프로그램은 이 플래그를 확인하여 온도 변환 또는 PID 제어 입력과 같은 후속 처리를 수행할 수 있습니다.

이 구조를 적용하면 ADC 샘플링은 DMA가 자동으로 처리하고 CPU는 일정 주기마다 평균 계산과 후속 처리만 수행하게 되어 시스템 전체 부하를 줄일 수 있습니다.

ADC 읽기 실험

펌웨어 작성이 완료되었으므로 프로젝트를 빌드한 후 Debug 모드를 통해 STM32에 다운로드합니다.

ADC DMA 동작 결과를 실시간으로 확인하기 위해 PT100_DMAADC_Process_100ms() 함수에서 갱신되는 pt100_dmaadc.raw_avgpt100_dmaadc.voltage 변수를 Live Expression에 등록하여 관찰하였습니다.

프로그램을 실행(Resume)하면 ADC DMA는 지속적으로 샘플링을 수행하고, 100ms 주기로 평균 ADC 값과 전압 값이 계산되어 Live Expression 창에서 실시간으로 갱신되는 것을 확인할 수 있습니다.

아래는 입력 저항이 99.3Ω일 때의 측정 결과입니다.
ADC 평균값은 1968, 계산된 전압은 1.5859V로 확인되었습니다.

아래는 입력 저항이 149.3Ω일 때의 측정 결과입니다.
ADC 평균값은 2937, 계산된 전압은 2.3668V로 확인되었습니다.

아래는 아날로그 보드의 입출력 전압과 ADC DMA로 읽은 값을 비교하는 테이블 입니다.

조건 저항값 (Ω) 입력 전압 (V) 보드 출력 (V) ADC Raw ADC 전압 (V)
기준 1 99.3 0.1155 1.617 1968 1.586
기준 2 149.3 0.171 2.392 2937 2.367
Open 3.3 4080 3.29

99.3Ω과 149.3Ω에서 모두 ADC 전압값은 보드 출력 전압과 유사하게 측정되었으며, 저항값이 증가함에 따라 출력 전압과 ADC Raw 값도 함께 증가하는 것을 확인할 수 있었습니다.
또한 Open 상태에서는 ADC Raw가 4080까지 올라가며 입력이 상단으로 포화되는 것을 확인할 수 있었습니다.

이를 통해 ADC DMA 기반 샘플링과 평균 처리 구조가 정상적으로 동작함을 검증할 수 있었습니다.

결론

이번 실험에서는 STM32의 ADC와 DMA 기능을 이용하여 PT100 아날로그 인터페이스 보드의 출력 전압을 효율적으로 읽는 방법을 구현해 보았습니다.

기존 Polling 방식에서는 CPU가 직접 ADC 값을 읽어야 했고, Time-Distributed Sampling 방식에서는 샘플링 작업을 분산하여 CPU 부하를 줄일 수 있었지만 여전히 CPU가 ADC 처리에 직접 관여해야 했습니다.

반면 DMA 방식을 적용하면 ADC 샘플링과 메모리 저장이 자동으로 이루어지므로 CPU는 일정 주기마다 평균 계산과 후속 처리만 수행하면 됩니다.

이번 실험을 통해 ADC DMA 기반 샘플링 구조가 정상적으로 동작함을 확인하였으며, 입력 저항 변화에 따라 ADC 값과 전압 값이 안정적으로 변화하는 것도 검증할 수 있었습니다.

이 구조는 PT100 온도 측정뿐 아니라 다양한 아날로그 센서 입력에도 적용할 수 있으며, 여러 작업이 동시에 수행되는 임베디드 시스템에서 CPU 자원을 효율적으로 사용할 수 있는 장점이 있습니다.

PT100 Temperature Measurement Series
PT100 온도 측정을 위한 아날로그 프론트엔드 설계부터 STM32 ADC Polling, Time-Distributed Sampling, ADC DMA 구현까지 단계별 실험 과정을 정리한 시리즈입니다.

1편: PT100 온도 측정을 위한 AFE(Analog Front End) 설계
PT100 온도 측정을 위한 AFE(Analog Front End) 설계

2편: STM32F103 ADC로 PT100 신호 읽기 (Polling 방식 실험)
STM32F103 ADC로 PT100 신호 읽기(폴링 방식 실험)

3편: STM32 ADC 분산 샘플링(Time-Distributed Sampling) 설계 방법
STM32 ADC 분산 샘플링(Time-Distributed Sampling) 설계 방법

4편: STM32 ADC DMA를 이용한 PT100 온도 측정 구현

“STM32 ADC DMA를 이용한 PT100 온도 측정 구현”에 대한 1개의 생각

댓글 남기기