Chapter 11 - Hongjayeong-02/Basic_SoC_implementation GitHub Wiki

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<span>[3:2]</span>
 *
 * GPIO mode:
 *
 *     00 β†’ Input
 *     01 β†’ Output
 *     10 β†’ Alternate Function
 *     11 β†’ Analog
 *
 * Therefore both bit2 and bit3 are set.
 */
GPIOA-&gt;MODER |= (1U &lt;&lt; 2);
GPIOA-&gt;MODER |= (1U &lt;&lt; 3);


/*
 * The original practice code uses OR only.
 *
 * Result:
 *
 *     MODER<span>[3:2]</span> = 11
 *
 *     PA1 β†’ Analog Mode
 *
 * When writing more defensive driver code,
 * clear-then-set can also be used:
 *
 * GPIOA-&gt;MODER &amp;= ~(0x3U &lt;&lt; (1 * 2));
 * GPIOA-&gt;MODER |=  (0x3U &lt;&lt; (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-&gt;APB2ENR |= ADC1EN;


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


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


/*
 * --------------------------------------------------------
 * Step 6. Enable ADC1
 * --------------------------------------------------------
 *
 * CR2.ADON = 1
 *
 * ADC peripheral becomes active.
 */
ADC1-&gt;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-&gt;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-&gt;DR contains the digital conversion result.
 *
 * In 12-bit mode:
 *
 *     minimum = 0
 *     maximum = 4095
 */
return ADC1-&gt;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 &gt; 0U) &amp;&amp; (i &lt; sizeof(buffer)))
{
    buffer<span>[i]</span> = (char)('0' + (value % 10U));

    value /= 10U;

    i++;
}


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

    i--;

    out<span>[0]</span> = buffer<span>[i]</span>;
    out<span>[1]</span> = '\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 κ°’ λ³€ν™”κΉŒμ§€ ν™•μΈν–ˆλ‹€.

⚠️ **GitHub.com Fallback** ⚠️