Repository navigation
Chapter 11
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
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
로 생각할 수 있다.
이번 코드에서 사용하는 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
이번 실습:
PA1
↓
ADC1_IN1
원서 F411 예제와 F439ZI에서 이 실습에 필요한 PA1 → ADC1 channel 1 mapping과 기본 register programming 방식은 그대로 사용할 수 있었다.
즉 Ch10 UART처럼 USART/pin을 크게 porting할 필요가 없었다.
Project:
Inc/
├── adc.h
├── uart.h
└── systick.h
Src/
├── adc.c
├── uart.c
├── systick.c
└── main.c
Ch10의 uart.c, Ch08의 systick.c를 그대로 재사용한다.
#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#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;
}원본 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);
}
}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
ADC continuously converting
│
▼
Analog Voltage on PA1
│
▼
ADC1_IN1
│
▼
ADC Conversion
│
▼
EOC = 1
│
▼
ADC1->DR
│
▼
sensor_value
│
▼
USART3
│
▼
PC Terminal
PA1을 ADC input으로 사용할 때:
GPIOA->MODER
를:
11
로 설정한다.
즉:
PA1
→ Analog Mode
이다.
GPIO digital input/output logic 대신 analog peripheral이 pin의 voltage를 사용할 수 있도록 한다.
STM32 ADC는 여러 analog channel 중 어떤 것을 변환할지 선택해야 한다.
이번 연결:
PA1
=
ADC1_IN1
=
ADC Channel 1
따라서:
ADC1->SQR3 = ADC_CH1;을 사용한다.
ADC는 여러 channel을 순서대로 변환할 수도 있다.
예:
Channel 1
↓
Channel 3
↓
Channel 7
↓
...
하지만 이번 실습은 channel 하나만 사용한다.
Sequence:
SQ1
↓
Channel 1
그래서:
ADC1->SQR1 = ADC_SEQ_LEN_1;즉:
Sequence Length = 1
로 설정한다.
핵심:
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;할 필요가 없다.
핵심 코드:
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에서 매우 자주 나오는 방식이다.
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 등에 따라 약간 달라질 수 있다.
변환이 끝나면 ADC hardware가 결과를:
ADC1->DR
에 저장한다.
따라서:
return ADC1->DR;
이:
Analog Input
↓
ADC Hardware
↓
Digital Result Register
↓
CPU
의 마지막 단계다.
현재 코드는:
while (!(ADC1->SR & SR_EOC))
{
}로 CPU가 conversion 완료를 기다린다.
즉:
ADC Working
+
CPU Waiting
구조다.
이후에는:
ADC Interrupt
DMA
를 사용하면 CPU가 직접 매 conversion마다 기다리지 않아도 된다.
특히 Ch17 DMA에서 ADC와 연결할 수 있는 구조로 확장 가능하다.
기존 실습 기록:
text = 1420 bytes
data = 0 bytes
bss = 2560 bytes
그리고 실제 NUCLEO-F439ZI에서 build/download/run을 수행했으며, 포텐셔미터 입력 변화에 따라 UART의 ADC 값이 대략 0 ~ 4095 범위에서 변하는 것을 확인했다.
make clean
makeExpected:
Build PASS
text = 1420
data = 0
bss = 2560
Debugger:
GPIOA->MODER
PA1 field:
MODER[3:2]
=
11
Expected:
PA1 = Analog Mode
확인:
ADC1->SQR3
ADC1->SQR1
Expected:
SQ1 = Channel 1
Sequence Length = 1
확인:
ADC1->CR2
Expected:
ADON = 1
CONT = 1
SWSTART = conversion started
Watch:
ADC1->DR
sensor_value
포텐셔미터를 움직일 때:
0
↓
500
↓
1500
↓
2500
↓
4095
처럼 값이 변하는지 확인한다.
Expected:
Sensor Value: 213
Sensor Value: 506
Sensor Value: 1058
Sensor Value: 2037
Sensor Value: 3120
Sensor Value: 4051
실제 값은 입력 전압에 따라 달라진다.
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
를 같이 보여주는 것이다.
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 값 변화까지 확인했다.