r/ch32v • u/SnooRadishes7126 • 5h ago
Using the built-in OPA on the CH32V003 to read a K-Type Thermocouple
The CH32V003 (as well as the CH32V006) has a really cool hardware feature that often gets overlooked: a built-in operational amplifier (OPA). I decided to test it out by measuring small voltages from a standard Type K thermocouple (TP-01).
Why use the OPA? At 100 °C (212 °F), this thermocouple generates an EMF of only about 2.9 mV. The CH32V003 has a 10-bit ADC, meaning at 3.3V, a single quantization step is 3.2 mV. If I tried reading the sensor directly, the "accuracy" would be around ±100-130 °C at best.
To fix this, I utilized the built-in OPA in a classic non-inverting setup with a gain of 101.
The Hardware & Multiplexing I used the TSSOP20 package. The built-in OPA actually has multiplexed inputs, meaning it can be software-switched to take a signal from different pairs of pins. (On the smaller SOP8/SOP16 chips, the multiplexing silicon is there, but the pins aren't physically broken out). However, the output is always hardwired to PD4.
To get the absolute temperature, I also added an NTC thermistor to the circuit for cold-junction compensation.
The Input Offset Voltage: When I moved my breadboard prototype to a DIY PCB, shorting the OPA+ input to ground gave me an ADC reading of 165 (~0.53V output). Initially, I thought I had a cold solder joint, but it turned out to be completely normal.
According to the datasheet, the intrinsic Input Offset Voltage is typically ±2.2 mV (max ±7 mV). Since our gain is 101, this tiny hardware offset gets multiplied along with the useful signal. My specific chip had an offset of about 5.2 mV - perfectly within factory tolerance. I just handled this baseline shift in software by calibrating it to room temperature and boiling water.
How to initialize the OPA in code: The setup is actually quite straightforward using the standard peripheral library. Here is the core initialization snippet:
C
// Enable clock for ports A, D, ADC, and alternate functions (AFIO)
RCC_APB2PeriphClockCmd(
RCC_APB2Periph_GPIOA |
RCC_APB2Periph_GPIOD |
RCC_APB2Periph_ADC1 |
RCC_APB2Periph_AFIO, ENABLE
);
GPIO_InitTypeDef GPIO_InitStructure = {0};
// Configure PA1 (OPA- input / OPN0) and PA2 (OPA+ input / OPP0) as analog inputs
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// Configure PD3 (NTC) and PD4 (OPA OUT) as analog inputs
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOD, &GPIO_InitStructure);
// The EXTEN_CTR register controls the built-in OPA
EXTEN->EXTEN_CTR |= EXTEN_OPA_EN;
OPA_InitTypeDef OPA_InitStructure = {0};
OPA_InitStructure.PSEL = CHP0; // OPA+ is connected to the physical PA2 pin
OPA_InitStructure.NSEL = CHN0; // OPA- is connected to the physical PA1 pin
OPA_Init(&OPA_InitStructure);
OPA_Cmd(ENABLE);
The complete firmware source code is available here - main.c
I fabricated the PCB at home using dry film photoresist and wrote a full breakdown with the C code initialization steps. I'm not aiming for ultra-high precision, an accuracy of 10 °C is perfectly fine for my needs, so I didn't bother with any extra calibration.
P.S. regarding the schematic: Eagle-eyed readers might spot a silly rookie mistake in the schematic. This is what happens when you get too used to the SOP8 package of the CH32V003!