Skip to content

Chapter 11

Hongjayeong-02 edited this page Oct 18, 2026 · 2 revisions

11. ADC — Analog-to-Digital Converter

Goal

NUCLEO-F439ZI의 PA1 / ADC1_IN1에 입력되는 아날로그 전압을 12-bit ADC로 변환하고, 결과값을 USART3로 PC terminal에 출력한다.

Analog Voltage
0 ~ 3.3 V
     ↓
PA1
     ↓
ADC1_IN1
     ↓
12-bit ADC
     ↓
0 ~ 4095
     ↓
USART3
     ↓
PC Terminal

이번 실습의 핵심:

PA1          → Analog Mode
ADC1 Channel → Channel 1
Resolution   → 12-bit
Mode         → Continuous Conversion
Status       → EOC Polling
Result       → ADC1->DR

1. ADC 핵심

CPU는 아날로그 전압 자체를 직접 처리하지 못한다.

ADC가:

Analog Voltage
       ↓
Digital Number

로 변환한다.

STM32F439ZI ADC를 12-bit로 사용할 경우:

2^12
=
4096 levels

따라서 결과 범위:

0 ~ 4095

이다.

대략적으로:

0 V
→ ADC ≈ 0

1.65 V
→ ADC ≈ 2048

3.3 V
→ ADC ≈ 4095

로 생각할 수 있다.


2. ADC Registers

이번 코드에서 사용하는 register:

Register 역할
GPIOA->MODER PA1을 Analog mode로 설정
RCC->APB2ENR ADC1 clock enable
ADC1->SQR3 첫 번째 변환 channel 선택
ADC1->SQR1 Conversion sequence length
ADC1->CR2 ADC enable / continuous / conversion start
ADC1->SR ADC status
ADC1->DR Conversion result

주요 bit:

CR2.ADON
→ ADC Enable

CR2.CONT
→ Continuous Conversion

CR2.SWSTART
→ Software Start

SR.EOC
→ End Of Conversion

3. Board Mapping

이번 실습:

PA1
 ↓
ADC1_IN1

원서 F411 예제와 F439ZI에서 이 실습에 필요한 PA1 → ADC1 channel 1 mapping과 기본 register programming 방식은 그대로 사용할 수 있었다.

즉 Ch10 UART처럼 USART/pin을 크게 porting할 필요가 없었다.


4. Practice Code

Project:

Inc/
├── adc.h
├── uart.h
└── systick.h

Src/
├── adc.c
├── uart.c
├── systick.c
└── main.c

Ch10의 uart.c, Ch08의 systick.c를 그대로 재사용한다.


5. adc.h

#ifndef ADC_H__
#define ADC_H__

#include <stdint.h>
#include "stm32f4xx.h"

/*
 * Configure:
 *
 *     PA1
 *      ↓
 *     ADC1_IN1
 *
 * for analog-to-digital conversion.
 */
void pa1_adc_init(void);


/*
 * Start ADC1 continuous conversion.
 *
 * After SWSTART is set once,
 * CONT mode keeps starting new conversions.
 */
void start_conversion(void);


/*
 * Wait until conversion is complete,
 * then return the ADC result.
 *
 * Return range for 12-bit conversion:
 *
 *     0 ~ 4095
 */
uint32_t adc_read(void);

#endif

6. adc.c

#include "adc.h"

/*
 * ============================================================
 * RCC Clock Enable Bits
 * ============================================================
 */

/*
 * RCC AHB1ENR bit0:
 *
 * GPIOA clock enable.
 *
 * PA1 belongs to GPIOA.
 */
#define GPIOAEN         (1U << 0)


/*
 * RCC APB2ENR bit8:
 *
 * ADC1 peripheral clock enable.
 */
#define ADC1EN          (1U << 8)


/*
 * ============================================================
 * ADC Channel / Sequence Configuration
 * ============================================================
 */

/*
 * ADC channel 1.
 *
 * In this practice:
 *
 *     PA1 → ADC1_IN1
 *
 * SQR3 SQ1 field selects the first channel
 * in the conversion sequence.
 *
 * Channel number = 1.
 */
#define ADC_CH1         (1U << 0)


/*
 * ADC sequence length.
 *
 * SQR1 L[3:0]:
 *
 *     0000
 *
 * means one conversion in the sequence.
 *
 * Therefore:
 *
 *     Sequence Length = 1
 */
#define ADC_SEQ_LEN_1   0x00U


/*
 * ============================================================
 * ADC CR2 Bits
 * ============================================================
 */

/*
 * CR2 bit0:
 *
 * ADON = ADC ON
 *
 * 0 → ADC disabled
 * 1 → ADC enabled
 */
#define CR2_ADON        (1U << 0)


/*
 * CR2 bit1:
 *
 * CONT = Continuous Conversion
 *
 * 0 → one conversion
 * 1 → automatically start next conversion
 */
#define CR2_CONT        (1U << 1)


/*
 * CR2 bit30:
 *
 * SWSTART = Software Start
 *
 * Setting this bit starts ADC conversion
 * through software.
 */
#define CR2_SWSTART     (1U << 30)


/*
 * ============================================================
 * ADC Status Bit
 * ============================================================
 */

/*
 * SR bit1:
 *
 * EOC = End Of Conversion
 *
 * Hardware sets EOC when conversion finishes
 * and valid data is available in DR.
 */
#define SR_EOC          (1U << 1)


/*
 * ============================================================
 * PA1 + ADC1 Initialization
 * ============================================================
 */

void pa1_adc_init(void)
{
    /*
     * --------------------------------------------------------
     * Step 1. Enable GPIOA Clock
     * --------------------------------------------------------
     *
     * PA1 cannot be configured until
     * GPIOA receives its peripheral clock.
     */
    RCC->AHB1ENR |= GPIOAEN;


    /*
     * --------------------------------------------------------
     * Step 2. Configure PA1 as Analog Mode
     * --------------------------------------------------------
     *
     * GPIO MODER uses 2 bits per pin.
     *
     * PA1:
     *
     *     MODER[3:2]
     *
     * GPIO mode:
     *
     *     00 → Input
     *     01 → Output
     *     10 → Alternate Function
     *     11 → Analog
     *
     * Therefore both bit2 and bit3 are set.
     */
    GPIOA->MODER |= (1U << 2);
    GPIOA->MODER |= (1U << 3);


    /*
     * The original practice code uses OR only.
     *
     * Result:
     *
     *     MODER[3:2] = 11
     *
     *     PA1 → Analog Mode
     *
     * When writing more defensive driver code,
     * clear-then-set can also be used:
     *
     * GPIOA->MODER &= ~(0x3U << (1 * 2));
     * GPIOA->MODER |=  (0x3U << (1 * 2));
     *
     * But this Wiki keeps the original practice
     * implementation above.
     */


    /*
     * --------------------------------------------------------
     * Step 3. Enable ADC1 Clock
     * --------------------------------------------------------
     *
     * ADC1 is connected to APB2.
     *
     * APB2ENR bit8:
     *
     *     ADC1EN = 1
     */
    RCC->APB2ENR |= ADC1EN;


    /*
     * --------------------------------------------------------
     * Step 4. Select ADC Channel
     * --------------------------------------------------------
     *
     * ADC regular sequence first conversion:
     *
     *     SQ1 = Channel 1
     *
     * PA1 is connected to ADC1_IN1.
     */
    ADC1->SQR3 = ADC_CH1;


    /*
     * --------------------------------------------------------
     * Step 5. Set Sequence Length
     * --------------------------------------------------------
     *
     * Only one ADC channel is converted.
     *
     * SQR1 L[3:0] = 0000
     *
     * This represents sequence length = 1.
     */
    ADC1->SQR1 = ADC_SEQ_LEN_1;


    /*
     * --------------------------------------------------------
     * Step 6. Enable ADC1
     * --------------------------------------------------------
     *
     * CR2.ADON = 1
     *
     * ADC peripheral becomes active.
     */
    ADC1->CR2 |= CR2_ADON;
}


/*
 * ============================================================
 * Start ADC Conversion
 * ============================================================
 */

void start_conversion(void)
{
    /*
     * --------------------------------------------------------
     * Step 1. Enable Continuous Conversion
     * --------------------------------------------------------
     *
     * CONT = 1
     *
     * After one conversion completes,
     * ADC automatically starts another conversion.
     *
     * Therefore SWSTART does NOT need to be issued
     * every time adc_read() is called.
     */
    ADC1->CR2 |= CR2_CONT;


    /*
     * --------------------------------------------------------
     * Step 2. Start First Conversion
     * --------------------------------------------------------
     *
     * SWSTART = 1
     *
     * This starts the first ADC conversion.
     *
     * After that:
     *
     * CONT mode keeps conversions running.
     */
    ADC1->CR2 |= CR2_SWSTART;
}


/*
 * ============================================================
 * Read ADC Result
 * ============================================================
 */

uint32_t adc_read(void)
{
    /*
     * --------------------------------------------------------
     * Step 1. Wait for Conversion Complete
     * --------------------------------------------------------
     *
     * ADC conversion takes a finite amount of time.
     *
     * EOC:
     *
     *     0 → conversion not complete
     *     1 → conversion complete
     *
     * The CPU waits here until valid conversion
     * data is ready.
     *
     * This is Polling / Busy Waiting.
     */
    while (!(ADC1->SR & SR_EOC))
    {
        /*
         * Wait for End Of Conversion.
         */
    }


    /*
     * --------------------------------------------------------
     * Step 2. Read Conversion Data
     * --------------------------------------------------------
     *
     * ADC1->DR contains the digital conversion result.
     *
     * In 12-bit mode:
     *
     *     minimum = 0
     *     maximum = 4095
     */
    return ADC1->DR;
}

7. main.c

원본 Notion에는:

uart_print_value(sensor_value);

호출은 기록되어 있지만 uart_print_value()의 실제 함수 본문은 남아 있지 않다.

따라서 아래에서는 기존 uart_write_string()만 이용해 동일한 출력을 만들 수 있도록 Wiki용 helper를 보완했다.

#include <stdint.h>

#include "adc.h"
#include "systick.h"
#include "uart.h"


/*
 * ============================================================
 * Wiki Supplement
 * uint32_t → Decimal ASCII Output
 * ============================================================
 *
 * The original Notion record contains:
 *
 *     uart_print_value(sensor_value);
 *
 * but its implementation body was not archived.
 *
 * This helper is added for the Wiki so the example
 * can be built without printf().
 */


/*
 * Send one unsigned integer as decimal text.
 *
 * Example:
 *
 *     value = 1234
 *
 * UART output:
 *
 *     1234
 */
static void uart_write_uint32(uint32_t value)
{
    /*
     * Maximum uint32_t decimal representation:
     *
     * 4294967295
     *
     * = 10 digits
     */
    char buffer[10];

    uint32_t i = 0;


    /*
     * Special case:
     *
     * Without this check, value = 0 would
     * generate no digits because the while-loop below
     * would never execute.
     */
    if (value == 0U)
    {
        uart_write_string("0");
        return;
    }


    /*
     * Convert integer digits in reverse order.
     *
     * Example:
     *
     * value = 123
     *
     * first:
     *     123 % 10 = 3
     *
     * second:
     *     12 % 10 = 2
     *
     * third:
     *     1 % 10 = 1
     *
     * buffer initially contains:
     *
     *     '3' '2' '1'
     */
    while ((value > 0U) && (i < sizeof(buffer)))
    {
        buffer[i] = (char)('0' + (value % 10U));

        value /= 10U;

        i++;
    }


    /*
     * Send the buffer backwards so the digits
     * appear in normal order.
     */
    while (i > 0U)
    {
        char out[2];

        i--;

        out[0] = buffer[i];
        out[1] = '\0';

        uart_write_string(out);
    }
}


/*
 * Print ADC value in the same logical format
 * recorded in the original practice:
 *
 *     Sensor Value: NNNN
 */
static void uart_print_value(uint32_t value)
{
    uart_write_string("Sensor Value: ");

    uart_write_uint32(value);

    uart_write_string("\r\n");
}


int main(void)
{
    /*
     * Variable that stores one ADC conversion result.
     *
     * Expected range:
     *
     *     0 ~ 4095
     */
    uint32_t sensor_value;


    /*
     * --------------------------------------------------------
     * Step 1. Initialize USART3
     * --------------------------------------------------------
     *
     * Reuse Ch10 driver:
     *
     *     USART3
     *     PD8 TX
     *     115200 bps
     */
    uart_init();


    /*
     * --------------------------------------------------------
     * Step 2. Initialize ADC Input
     * --------------------------------------------------------
     *
     * PA1:
     *
     *     GPIO Analog Mode
     *     ADC1 Channel 1
     */
    pa1_adc_init();


    /*
     * --------------------------------------------------------
     * Step 3. Start Continuous Conversion
     * --------------------------------------------------------
     *
     * ADC:
     *
     *     CONT = 1
     *     SWSTART = 1
     *
     * ADC now repeatedly converts PA1 voltage.
     */
    start_conversion();


    while (1)
    {
        /*
         * ----------------------------------------------------
         * Step 4. Read Latest ADC Conversion
         * ----------------------------------------------------
         *
         * adc_read():
         *
         *     Wait EOC
         *        ↓
         *     Read DR
         */
        sensor_value = adc_read();


        /*
         * ----------------------------------------------------
         * Step 5. Print ADC Value
         * ----------------------------------------------------
         *
         * Example:
         *
         *     Sensor Value: 2048
         */
        uart_print_value(sensor_value);


        /*
         * ----------------------------------------------------
         * Step 6. Slow Down Terminal Output
         * ----------------------------------------------------
         *
         * ADC itself converts continuously,
         * but there is no need to flood the PC terminal.
         *
         * Print approximately every 200 ms.
         */
        systick_msec_delay(200);
    }
}

8. ADC Initialization Flow

pa1_adc_init()
      │
      ├── GPIOA Clock Enable
      │
      ├── PA1 → Analog Mode
      │
      ├── ADC1 Clock Enable
      │
      ├── SQ1 → Channel 1
      │
      ├── Sequence Length → 1
      │
      └── ADON = 1

그다음:

start_conversion()
      │
      ├── CONT = 1
      └── SWSTART = 1

9. Runtime Flow

ADC continuously converting
       │
       ▼
Analog Voltage on PA1
       │
       ▼
ADC1_IN1
       │
       ▼
ADC Conversion
       │
       ▼
EOC = 1
       │
       ▼
ADC1->DR
       │
       ▼
sensor_value
       │
       ▼
USART3
       │
       ▼
PC Terminal

10. Why Analog Mode?

PA1을 ADC input으로 사용할 때:

GPIOA->MODER

를:

11

로 설정한다.

즉:

PA1
→ Analog Mode

이다.

GPIO digital input/output logic 대신 analog peripheral이 pin의 voltage를 사용할 수 있도록 한다.


11. Why Channel 1?

STM32 ADC는 여러 analog channel 중 어떤 것을 변환할지 선택해야 한다.

이번 연결:

PA1
=
ADC1_IN1
=
ADC Channel 1

따라서:

ADC1->SQR3 = ADC_CH1;

을 사용한다.


12. Conversion Sequence

ADC는 여러 channel을 순서대로 변환할 수도 있다.

예:

Channel 1
↓
Channel 3
↓
Channel 7
↓
...

하지만 이번 실습은 channel 하나만 사용한다.

Sequence:

SQ1
 ↓
Channel 1

그래서:

ADC1->SQR1 = ADC_SEQ_LEN_1;

즉:

Sequence Length = 1

로 설정한다.


13. Continuous Conversion

핵심:

ADC1->CR2 |= CR2_CONT;
ADC1->CR2 |= CR2_SWSTART;

처음:

SWSTART
   ↓
Conversion #1

Conversion #1이 끝나면:

CONT = 1
   ↓
Conversion #2
   ↓
Conversion #3
   ↓
Conversion #4
   ↓
...

자동으로 계속 진행된다.

따라서 main loop에서 매번:

ADC1->CR2 |= CR2_SWSTART;

할 필요가 없다.


14. EOC Polling

핵심 코드:

while (!(ADC1->SR & SR_EOC))
{
}

의미:

Conversion started
       ↓
EOC = 0
       ↓
CPU waits
       ↓
Conversion complete
       ↓
EOC = 1
       ↓
Read DR

Ch10 UART의:

while (!(USART3->SR & SR_TXE))

와 구조적으로 매우 비슷하다.

공통 pattern:

Check Status Flag
       ↓
Not Ready?
       ↓
Wait
       ↓
Ready?
       ↓
Access Data Register

이것이 bare-metal peripheral programming에서 매우 자주 나오는 방식이다.


15. ADC Digital Value

12-bit ADC:

0 ~ 4095

전압과 결과의 이상적인 관계는 대략:

ADC Value
≈
(VIN / VREF) × 4095

예를 들어 VREF를 3.3V로 가정하면:

VIN = 0 V
→ 0

VIN ≈ 0.825 V
→ ≈ 1024

VIN ≈ 1.65 V
→ ≈ 2048

VIN ≈ 2.475 V
→ ≈ 3071

VIN ≈ 3.3 V
→ ≈ 4095

실제 측정값은 전원, reference, noise 등에 따라 약간 달라질 수 있다.


16. Why DR?

변환이 끝나면 ADC hardware가 결과를:

ADC1->DR

에 저장한다.

따라서:

return ADC1->DR;

이:

Analog Input
     ↓
ADC Hardware
     ↓
Digital Result Register
     ↓
CPU

의 마지막 단계다.


17. Polling Limitation

현재 코드는:

while (!(ADC1->SR & SR_EOC))
{
}

로 CPU가 conversion 완료를 기다린다.

즉:

ADC Working
    +
CPU Waiting

구조다.

이후에는:

ADC Interrupt
DMA

를 사용하면 CPU가 직접 매 conversion마다 기다리지 않아도 된다.

특히 Ch17 DMA에서 ADC와 연결할 수 있는 구조로 확장 가능하다.


18. Build Result

기존 실습 기록:

text = 1420 bytes
data = 0 bytes
bss  = 2560 bytes

그리고 실제 NUCLEO-F439ZI에서 build/download/run을 수행했으며, 포텐셔미터 입력 변화에 따라 UART의 ADC 값이 대략 0 ~ 4095 범위에서 변하는 것을 확인했다.


19. Verification

1. Build

make clean
make

Expected:

Build PASS

text = 1420
data = 0
bss  = 2560

2. GPIO Register

Debugger:

GPIOA->MODER

PA1 field:

MODER[3:2]
=
11

Expected:

PA1 = Analog Mode

3. ADC Sequence

확인:

ADC1->SQR3
ADC1->SQR1

Expected:

SQ1 = Channel 1
Sequence Length = 1

4. ADC Control

확인:

ADC1->CR2

Expected:

ADON    = 1
CONT    = 1
SWSTART = conversion started

5. ADC Result

Watch:

ADC1->DR
sensor_value

포텐셔미터를 움직일 때:

0
↓
500
↓
1500
↓
2500
↓
4095

처럼 값이 변하는지 확인한다.


6. UART Terminal

Expected:

Sensor Value: 213
Sensor Value: 506
Sensor Value: 1058
Sensor Value: 2037
Sensor Value: 3120
Sensor Value: 4051

실제 값은 입력 전압에 따라 달라진다.


20. Screenshots to Add

images/ch11_build.png
images/ch11_adc_registers.png
images/ch11_adc_min.png
images/ch11_adc_mid.png
images/ch11_adc_max.png
images/ch11_terminal.png

가장 좋은 증거:

Potentiometer Position
+
ADC1->DR
+
UART Terminal Value

를 같이 보여주는 것이다.


21. Key Points

Analog Voltage
      ↓
PA1
      ↓
Analog Mode
      ↓
ADC1 Channel 1
      ↓
SWSTART
      ↓
Conversion
      ↓
EOC
      ↓
DR
      ↓
0 ~ 4095
      ↓
UART
  • PA1은 ADC1_IN1로 사용한다.
  • ADC input pin은 Analog mode로 설정한다.
  • ADC1은 APB2 clock을 사용한다.
  • SQR3에서 첫 번째 conversion channel을 선택한다.
  • SQR1에서 sequence length를 설정한다.
  • ADON은 ADC를 enable한다.
  • CONT는 continuous conversion을 enable한다.
  • SWSTART는 software conversion start다.
  • EOC는 conversion 완료를 나타낸다.
  • DR에는 ADC conversion 결과가 저장된다.
  • 12-bit ADC 결과 범위는 0 ~ 4095다.
  • 이번 구현은 EOC polling 방식이다.
  • 실제 board에서 포텐셔미터 입력에 따른 ADC 값 변화까지 확인했다.

Clone this wiki locally