在设计带有微控制器 (MCU) 的设备时,我喜欢使用一些模数转换器 (ADC) 输入来测量板载电压以及所有所需的传感器输入。这意味着我经常用完 ADC 输入。因此,这里介绍的是一种无需添加外部芯片即可添加更多 ADC 的方法,成本不到 5 美分,并且在 PCB 上占用的空间可以忽略不计!
您正在使用的 MCU 中有两个组件:脉冲宽度调制器 (PWM) 输出和板载模拟比较器。一些具有这些功能的 MCU 系列包括 Microchip 的 PIC、AVR 和 ATmega MCU。此外,TI 的 Piccolo 系列和 STMicroElectronics STM32L5 都具有 PWM 和比较器。
那么,让我们看看它是如何配置的。
图 1使用带有板载 PWM 和比较器的 MCU 以及 RC 滤波器来创建 ADC 的电路的基本概念。
电阻器和电容器形成单极低通滤波器。因此,电路概念采用板载 PWM 的输出,对其进行滤波以创建由 PWM 占空比设置的直流信号。然后使用板载比较器将直流电平与输入信号进行比较。该电路非常简单,所以我们来讨论一下用于根据这种布置创建 ADC 的代码。
为了获得输入信号的样本读数,我们首先将 PWM 设置为 50% 占空比。该方波 PWM 信号将由 RC 低通滤波器进行滤波,以产生 MCU 系统电压 1/2 的电压。如果滤波后的直流电平大于瞬时输入信号电压,比较器输出将变高(或输出数字1),否则比较器输出将变低(输出数字0)。
现在,代码将读取比较器输出并执行搜索以查找强制比较器输出相反输出的新级别。换句话说,如果比较器为 0,则代码将调整 PWM 占空比,直到比较器输出 1。如果比较器当前显示 1,则 PWM 占空比将减小,直到比较器输出 0。如果PWM 的占空比能够达到 256 个步长(或更多),此搜索可能需要一些相当长的时间。为了缓解这个问题,我们将进行二分搜索,因此如果 PWM 中有 256 个可用步骤,则只需使用 log 2 (256) 或 8 个步骤来测试级别。
对二分搜索的简单描述是,在个 50% 级别读取之后,下测试将在 25% 或 75% 级别进行,具体取决于比较器输出的状态。此后的步骤将再次测试剩余级别的中间部分。
电路功能示例
让我们展示一个简单的示例并假设以下情况:
系统电压:5V
PWM 可用级别:256
瞬时输入信号:1V
个测试将以约 50% 占空比(设置为 128)的 PWM 执行,创建一个应用于比较器“+”输入的 2.50V 信号。这意味着比较器将输出高电平,这意味着 PWM 占空比太高。因此,我们将占空比设置为 64,将占空比减半,这会在“+”输入上产生 1.25 V 电压。比较器将再次输出 1…至高电平,因此我们将 PWM 占空比再次降低一半至 32。这给出了 0.625 V 的“+”电平。现在比较器将输出 0,因此我们知道我们的电压太低了,并且我们增加 PWM 占空比。我们知道 64 太高,32 太低,所以我们去中心,或者 (64+32)/2 = 48,给出 0.9375 V。我们仍然太低,所以我们将 64 和 48 的差值分开,得到56 或约 1.094 V……太高了。继续下去,得到 (56+48)/2=52,给出 1.016 V……太高了。同样,PWM 设置为 (52+48)/2=50,给出 0.9766 V。一步,(52+50)/2=51,给出 0.9961 V。
这 8 个步骤让我们尽可能接近答案。因此,我们的 ADC 设置将返回瞬时输入信号为 0.9961 V 的答案。
使用 Arduino Nano 的示例电路
让我们看一个现实世界的例子。此示例使用 Arduino Nano,其中使用 ATmega328P,该 ATmega328P 具有多个 PWM 输出和一个模拟比较器。我们将使用的 PWM 可以以各种速率计时,并且我们希望快速计时,因为这将使过滤更容易。它还将加快滤波器输出稳定到终水平的时间。我们将选择大约 31.4 kHz 的 PWM 时钟速率。图 2显示了带有单极 RC 低通滤波器的原理图。
图 2使用 Arduino Nano 和单极 RC 低通滤波器的示例电路原理图。
在此原理图中,D11 是 PWM 输出,D6 是比较器的“+”输入,而 D7 是比较器的“-”输入。该滤波器由 20kΩ 电阻器和 0.1F 电容器组成。我通过 LTspice 模拟得出这些值,试图限度地减少 PWM 脉冲(纹波)的残余,同时保持相当快的稳定时间。纹波的目标是 PWM 中 1 位变化的分辨率或更少。使用 5 V 系统电压和 PWM 具有 8 位(256 个设置)可调性的信息,我们得到 5 V/256 = ~20 mV。在 LTspice 仿真中,我得到了 18 mV 的纹波,而直流电平在 15 ms 时稳定在其终值的几毫伏以内。因此,在编写代码时,我使用 15 毫秒作为样本之间的延迟(您将在下面看到一个小改动)。由于需要 8 个读数才能获得终可用样本,因此需要 8*15 ms = 120 ms,即每秒 8.3 个样本。正如一开始所指出的,您不会以音频速率进行采样,但您当然可以监视板上的直流电压或缓慢移动的模拟信号。
[这可能是一个值得注意的地方,模拟输入不像大多数 ADC 那样具有采样和保持功能,因此读数是一个不断变化的目标。此外,输入信号没有抗混叠滤波器。如果需要,抗混叠滤波器可以消除噪声,还可以充当粗略采样和保持的作用。]
示例代码 下面是在 Arduino 开发环境中使用的代码清单。您也可以在这里。它将读取输入信号,进行二分查找,将其转换为电压,然后显示终的 8 位 DAC 值、相应的电压读数以及移动较慢的滤波值。
下面对代码进行更深入的描述:
第 1-8 行定义了我们用于 PWM 的引脚并声明了我们的变量。请注意,第 3 行设置系统电压。该值应在 MCU 的 Vcc 引脚上测量。
第 11 行和第 12 行将 PWM 设置为所需的频率。
第 15 行和第 16 行设置了我们正在使用的板载比较器。
第 18 行初始化我们将在其上打印结果的串行端口。
第 22 行是主代码开始的地方。首先,我们每次都会初始化一些变量以开始二分搜索。
第 29 行我们开始 8 步二分搜索,第 30 行设置 PWM 的占空比。然后引入 15 毫秒的延迟以使低通滤波器稳定下来。
第 34 行是上面暗示的“小转折”。这会引入 0 到 31 微秒之间的第二个随机延迟。之所以将其包含在内,是因为滤波器之后出现的 PWM 纹波与 16 MHz MCU 的时钟相关,因此,为了帮助将其从终读数中滤除,我们注入了此延迟以打破相关性。
第 37 和 38 行将在执行延迟后检查比较器。根据比较检查,调整下一个 PWM 占空比的范围。
第 40 行计算这个新范围内的新 PWM 占空比。然后代码循环 8 次以完成二分查找。
第 43 行和第 44 行计算当前瞬时电压读数以及滤波后的平均电压读数的电压。该电压平均是使用非常简单的 IIR 滤波器来完成的。
第46-51行将信息发送到Arduino串行监视器进行显示。
1 #define PWMpin 11 // pin 11 is D11
2
3 float systemVoltage = 4.766; // Actual voltage powering the MCU for calibrating printedoutput voltage
4 float ADCvoltage = 0; // Final discovered voltage
5 float ADCvoltageAve = 0; // Final discovered voltage averaged
6 uint8_t currentPWMnum = 0; // Number sent to the PWM to generate the requested voltage
7 uint8_t minPWMnum = 0;
8 uint8_t maxPWMnum = 255;
9
10 void setup() {
11 pinMode(PWMpin, OUTPUT); // Set up PWM for output
12 TCCR2B = (TCCR2B & B11111000) | B00000001; // Set timer 1 to 31372.55 Hz which is now the D11 PWM frequency
13
14 // Set up comparator
15 ADCSRB = 0b01000000; // (Disable) ACME: Analog Comparator Multiplexer disabled
16 ACSR = 0b00000000; //enable AIN0 and AIN1 comparison with interrupts disabled
17
18 Serial.begin(9600); // open the serial port at 9600 bps:
19 }
20
21
22 void loop() {
23
24 currentPWMnum = 127; // Start binary search at the halfway point
25 minPWMnum = 0;
26 maxPWMnum = 255;
27
28 // Perform a binary search for matching comparator setting
29 for (int8_t i = 0; i < 8; i++) { // Loop 8 times
30 analogWrite(PWMpin, currentPWMnum); // Adjust PWM to new dutycycle setting
31
32 // Now wait
33 delay(15); // Wait 15 ms to let the low-pass filter to settle.
34 delayMicroseconds(random(0,32)); // Delay a random number of microseconds (0 thru 31) to break possible correlation (dithering)
35
36 // Check to see if comparator shows AIN0 > AIN1 ( if so ACO in ACSR is set to 1)
37 if (ACSR & (1<<ACO)) maxPWMnum = currentPWMnum; // (AIN0 > AIN1) Move max pointer
38 else minPWMnum = currentPWMnum; // Move min pointer
39
40 currentPWMnum= minPWMnum + ((maxPWMnum - minPWMnum) / 2); // Set new test number to the middle of PWMmin and PWMmax
41 }
42
43 ADCvoltage = systemVoltage * ((float)currentPWMnum/255); // Set the PWM for binary search of voltage (assumes 0 to 5v signal
44 ADCvoltageAve = (ADCvoltageAve * 0.95) + (ADCvoltage * 0.05); // Generate an average value to smooth reading
45
46 Serial.print("PWM Setting = ");
47 Serial.print(currentPWMnum);
48 Serial.print(" ADC Voltage = ");
49 Serial.print(ADCvoltage, 4);
50 Serial.print(" ADC Voltage Filtered = ");
51 Serial.println(ADCvoltageAve, 4);
52 }
测试结果
步是测量 +5-V 引脚或 Arduino Nano 上的系统电压。该值 (4.766 V) 在代码的第 3 行输入。然后,我在 Arduino Nano V3 上运行代码并监视 Arduino 串行监视器上的输出。为了测试代码和系统,我首先将 2.5V 参考电压连接到信号输入。首先对该基准进行预热,并在校准的 5 ? 位数字万用表上读取电压读数。参考读数为 2.5001 V。串行监视器显示瞬时电压从 2.5232 到 2.4858 V 变化,平均电压从 2.5061 到 2.5074 V 变化。瞬时电压读数误差约为 0.9%,平均电压读数误差约为 0.3% 。这表明我们得到的瞬时电压读数误差约为 ±1 LSB,滤波读数约为 ±0.4 LSB。当输入各种其他电压时,我得到了类似的精度。
我还使用 Vcc (4.766 V) 输入进行测试,并查看了 4.7473 V 的结果,这意味着它可以非常接近上轨工作。输入接地时,瞬时电压和滤波电压显示为 0.000 V。