Lab 5.1 — STM32 ADC Single Sample
← Course 2 syllabus · Module 5 · Prev: « Lab 4.4 · Next: Lab 5.2 »
Goal
Take the first real step from analog into the digital domain: configure the STM32’s on-chip 12-bit ADC, trigger one conversion under software control, and turn the raw integer code back into a voltage. You will feed the ADC a known DC level from the MCP4725 DAC (built in Lab 3.3) and compare the STM32’s reading against the Fluke 117 as ground truth. This is the “hello world” of data acquisition — every later DSP lab is just this conversion, repeated deterministically and processed. Getting the reference, the full-scale range, and the code→volts mapping exactly right here is what makes every downstream measurement trustworthy.
Recommended reading
- Kuo Ch. 2 — analog-to-digital conversion, quantization, and the data-acquisition front end of a real-time DSP system.
- O-DSP Ch. 4 — sampling of continuous-time signals and the A/D conversion model (read §4.0–4.3 for the ideal C/D converter; the sampling-theorem detail is picked up in Lab 5.4).
- Course 1 Weeks 1–3 — linear least-squares, for the two-point / best-fit ADC calibration line in Going further.
Equipment & parts
Safety & don’t-break-it
- The analog pins are a 3.3 V world — they are not 5 V tolerant. VDDA/VREF+ on the Nucleo is 3.3 V. Any voltage on an ADC input above ~3.3 V (strictly, above VDDA + 0.3 V) forward-biases the input protection diode and can destroy the pin or the whole chip. Everything you connect to
A0must be guaranteed to stay within 0 – 3.3 V. - Power the MCP4725 from the Nucleo’s 3.3 V rail, not 5 V. The MCP4725 output is rail-to-rail: on a 5 V supply its full-scale output is ~5 V, which would over-drive the ADC. Running it from 3.3 V makes its maximum possible output 3.3 V — physically incapable of exceeding VREF+. Verify with the Fluke that the DAC’s full-scale (code 4095) output is ≈ 3.3 V before you connect it to the ADC pin.
- Share grounds. MCP4725 GND, Nucleo GND, and the Fluke’s COM must all be common, or your reading is meaningless.
- Never let the DAC output float into the pin while the code is uninitialized — set the DAC to a known mid-scale (~1.65 V) before wiring it to
A0.
Background
The STM32L476’s ADC is a 12-bit successive-approximation converter referenced to VREF+ = VDDA = 3.3 V. A conversion produces an integer code in \([0, 4095]\). Treating the full-scale span as \(V_\text{REF}\) across \(2^{12}\) quantization levels, the reconstructed voltage is
\[V = \frac{\text{code}}{2^{12}}\, V_\text{REF} = \frac{\text{code}}{4096}\times 3.3\ \text{V}.\]
The quantization step (1 LSB) is
\[\Delta = \frac{V_\text{REF}}{2^{12}} = \frac{3.3}{4096} \approx 0.806\ \text{mV},\]
so no single reading can be trusted below roughly a millivolt — that is the noise floor the digital side imposes before any circuit noise.
A conversion is not instantaneous. Its time is the sampling (acquisition) time plus the conversion time:
\[t_\text{conv} = \frac{t_\text{smp} + t_\text{SAR}}{f_\text{ADC}},\qquad t_\text{SAR} = 12.5\ \text{cycles (12-bit)}.\]
During \(t_\text{smp}\) the internal sample-and-hold capacitor charges through the source resistance \(R_\text{src}\) and the ADC’s sampling resistance. If \(t_\text{smp}\) is too short for a high-impedance source, the cap never fully settles and the reading reads low. The MCP4725 is a low-impedance (buffered, rail-to-rail) source, so a modest sampling time is fine — but choose a generous SamplingTime (e.g. 47.5 or 92.5 cycles) for margin and note how it trades against throughput.
Procedure
Part A — Configure the ADC in CubeMX.
- New STM32CubeIDE project for NUCLEO-L476RG. In the
.ioc, enable ADC1 and tick IN5 (pin PA0, the Arduino A0 header). Leave it single-ended. - ADC settings: Resolution 12 bits, Continuous Conversion Mode = Disabled (we want one conversion per request), Scan = Disabled, External Trigger = Software. Set the channel Sampling Time to 47.5 Cycles to start.
- Set the ADC clock so \(f_\text{ADC}\) is within spec (e.g. sync clock / 4). Enable USART2 for the VCP so you can print. Generate code.
Part B — Read one sample (illustrative firmware).
- A single polled conversion in HAL looks like:
// Illustrative only — you write and debug the real project.
HAL_ADC_Start(&hadc1);
if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) { // 10 ms timeout
uint16_t code = HAL_ADC_GetValue(&hadc1); // 0..4095
float volts = ((float)code / 4096.0f) * 3.3f; // code -> volts
printf("code=%4u V=%.4f\r\n", code, volts);
}
HAL_ADC_Stop(&hadc1);The LL equivalent is the same three ideas — LL_ADC_REG_StartConversion(), poll LL_ADC_IsActiveFlag_EOC(), LL_ADC_REG_ReadConversionData12() — with no HAL overhead.
Part C — Present a known DC level and read it.
- With the MCP4725 powered from 3.3 V and its output wired to PA0, set the DAC to a known code. Start with mid-scale (code 2048 → predicted ≈ 1.65 V).
- Measure the DAC output node with the Fluke (COM + VΩ, DC volts). Record the true voltage \(V_\text{Fluke}\).
- Trigger a conversion; read
codeandvoltsover the VCP. - Repeat for several DAC codes spanning the range — e.g. 0, 1024, 2048, 3072, 4095 — recording DAC code, \(V_\text{Fluke}\), ADC
code, and computedvoltsfor each.
Deliverable & expected results
A bench note (docs/lab-5-1.md) with the code→volts table across the sweep, and a one-line statement of your worst-case STM32-vs-Fluke error in millivolts and in LSBs.
| DAC code | Predicted DAC out | ADC code (pred.) | STM32 volts | Fluke volts |
|---|---|---|---|---|
| 0 | 0.000 V | ~0 | … | … |
| 1024 | 0.825 V | ~1272 | … | … |
| 2048 | 1.650 V | ~2543 | … | … |
| 3072 | 2.475 V | ~3815 | … | … |
| 4095 | 3.300 V | ~4095 | … | … |
(Predicted ADC code = \(\text{round}(V_\text{Fluke}/3.3 \times 4096)\); e.g. 1.65 V → 2048–2543 depending on the 4096 vs. 4095 convention — see Analysis.)
Analysis & reconciliation
Compute each volts by hand from the code and compare to the Fluke. Expect agreement to within a few LSBs (a handful of mV). Sources of the gap, in rough order: (1) VREF+ is not exactly 3.300 V — measure the actual VDDA with the Fluke and re-scale; this is usually the dominant error. (2) The 4096 vs. 4095 convention: dividing by 4096 maps full-scale code 4095 to 3.2992 V, not 3.3000 V — a fixed 1-LSB tilt at the top. (3) ADC offset and gain error (datasheet, in LSBs). (4) Insufficient sampling time if you shortened it — re-run at 92.5 cycles and see if the low-end codes rise. A single conversion also carries the full quantization + input noise; averaging is deferred to later labs.
Going further
- Two-point calibration. Fit
volts = a·code + bfrom your measured (code, Fluke) pairs by least squares (Course 1 Weeks 1–3) and report the residual. This calibration line is what you would ship. - Internal channels. The L476 exposes an internal VREFINT and a temperature sensor channel — read VREFINT to compute the true VDDA at runtime and auto-correct the scale factor.
- Oversample. Average 256 conversions and watch the effective resolution improve by ~4 bits (½ bit per 4× averaging) — the bridge to the noise-floor work in Lab 6.4.