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.
Lyons Ch. 2 — periodic sampling: the A/D conversion model and how a continuous signal becomes a sequence (the sampling-theorem and aliasing detail is picked up in Lab 5.4).
Course 1 Lesson 6 — linear least squares, for the two-point / best-fit ADC calibration line in Going further.
Equipment & parts
STM32 NUCLEO-L476RG + USB cable (ST-LINK debug + virtual COM port).
MCP4725 DAC breakout on a breadboard, wired and working from Lab 3.2/3.3.
The signal chain: the Nucleo drives the MCP4725 over I²C, and the DAC’s output loops straight back into the Nucleo’s own ADC pin — with the Fluke tapping the same node as ground truth.
Most MCP4725 breakouts carry on-board I²C pull-ups (same bus you validated in Lab 3.1); if yours is bare, add 4.7 kΩ SCL→3V3 and SDA→3V3. Land the OUT → A0 loopback jumper last, after the DAC is at mid-scale (see Safety below).
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 A0 must 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.
Project & environment setup
Firmware — create the Module 5 project (firmware/m5-daq/; CubeMX → board NUCLEO-L476RG → Toolchain/IDE = CMake, per the project workflow). Labs 5.2–5.4 build on this same project — they only add to this .ioc. Configure:
Host — only serial logging, in the course venv (see Toolchain):
source venv/bin/activate # pyserial is all this lab needsmkdir-p labs/lab-5-1/host labs/lab-5-1/captures
Put a small logger in labs/lab-5-1/host/log_vcp.py (pyserial: open /dev/tty.usbmodem* at 115200 and append each printed line to labs/lab-5-1/captures/sweep.log — you write the script; it’s ~10 lines). Copy-pasting from a serial terminal into the log file is fine too.
Keep this lab’s reconciliation in labs/lab-5-1/host/analysis.ipynb — the notebook convention — and export final figures next to it.
Where results go:
Artifact
Path
Bench note (sweep table + worst-case error)
labs/lab-5-1/notes.md
VCP printout of the sweep (code, volts per DAC code)
labs/lab-5-1/captures/sweep.log
(Optional) STM32-vs-Fluke plot
labs/lab-5-1/host/adc-vs-fluke.png
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
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 CubeMX project for NUCLEO-L476RG, generated as CMake into firmware/m5-daq/ per Project & environment setup, opened in CLion. 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 timeoutuint16_t code = HAL_ADC_GetValue(&hadc1);// 0..4095float 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 (per Wiring & bench setup), 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 code and volts over 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 computed volts for each.
Deliverable & expected results
A bench note (labs/lab-5-1/notes.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
~1024
…
…
2048
1.650 V
~2048
…
…
3072
2.475 V
~3072
…
…
4095
3.300 V
~4095
…
…
(Predicted ADC code = \(\text{round}(V_\text{Fluke}/3.3 \times 4096)\). Since the ADC and the DAC share the same 3.3 V reference, the ADC code should track the DAC code almost exactly; e.g. 1.65 V → 2048, with the 4096-vs-4095 convention shifting it by at most 1 LSB — 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.
NoteWhy there is no Jetson/Pi port of this lab
The Jetson Orin Nano and Pi 5 have no on-chip ADC — there is no register to configure and no A0 pin; that absence is exactly why Lab 3.4 exists, whose Jetson procedure (ADS1115 over /dev/i2c-7) is this lab’s counterpart on embedded Linux. Module 5 stays on the MCU because its subject — the on-chip converter, its reference, its sampling time — only exists there.
Going further
Two-point calibration. Fit volts = a·code + b from your measured (code, Fluke) pairs by least squares (Course 1 Lesson 6) 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.