简介
在开发项目中,有时会遇到需要使用程控放大的情况,如果没有opa那种可编程放大器,那么就需要通过继电器来控制放大倍数。而在继电器程控中,常用的是二级程控,三级程控相较于二级就复杂了许多。
在二级程控中,每级继电器都有两种状态,因此每级都可以设置两个倍数,两级级联共4种放大倍数。对于具体应用场景来说,难的是如何设置合适的不同放大倍数和划分不同的子区间,使其可以对输入区间的不同子区间进行选择性放大,并且每个档位都能合理覆盖这些子区间,进而使得增益后的子区间均处于适合的范围。
具体场景
举个具体例子,现在ADC采集电压峰峰值范围为0~2V,而输入的电压为30mV到600mV。对于电压30mV和600mV来说,放大倍数应分别接近66.7和3.3,那么就需要对其不同区间进行选择性放大。那么这就设计到了如何选取区间,4个放大倍数自然需要把输入区间划分为4个连续子区间。放大之后还需要确保,增益的子区间应在ADC采集的范围内。
也就是说,我们能获得的信息是输入的区间范围(如30mV~600mV)和输出的区间范围(如0~2V),而我们需要的信息则是不同的放大倍数和划分的子区间。在此例中,为了能及时察觉到放大倍数是否过大,那么应让每个区间放大后的上限小于ADC的上限(2V),以便可以通过检测ADC电压是否大于子区间上限并接近上限电压,进而及时调整程控倍数。下限亦是如此。
建模
根据具体的应用场景,可以很轻松地为其建模:
对于一个输入电压范围x1,x2,可以把它分为4个连续区间范围(左闭右开),而现在有一级放大倍数O1min和O1max,二级放大倍数O2min和O2max,这四个数可以排列组合得到不同的乘积a,b,c,d。那么如何找到四个乘积a、b、c、d(由一级放大倍数O1和二级放大倍数O2的组合产生),以及四个区间的分割点,使得每个分割后的区间内的x乘以对应的乘积后落在[Vmin, Vmax]内,并且分割后的四个区间连续覆盖输入范围。
解决
进行建模后,并不好从数学角度快速解出4个放大倍数和4个连续子区间。但可以通过编程来遍历所有合适的条件。如图,这是个简单的C++程序,可以向其指定输入区间、输出区间、步长和线程数。不过需要注意的是,找到的结果是对称的,也就是说两级放大倍数互换,在程序里是两种情况,而非一种情况。
小工具的使用
简介
使用方法很简单,与一般工具无异,通过指定参数来进行相应操作。此处,小工具的名称为auto_OPA.exe
输入区间
以前面的例子而言,输入区间为30mV到600mV,那么可以写成下面形式(这里以mV为单位,也可以以V为单位,即-i 0.03 0.6。需要确保后面也使用相同单位)
.\auto_OPA.exe -i 30 600
输出区间
各个子区间增益后的区间范围
.\auto_OPA.exe -o 875 1900
步长
决定了放大倍数的最小分辨率
.\auto_OPA.exe -s 0.5
步长不建议选择太小,比如0.1,其耗时远远高于步长为0.5用时的5倍,并且消耗内存会会大幅增加,因为找到的结果会先保存起来。
另一方面,整数放大或者半整数放大在实际电路设计中较为常见,也比较好设计。
线程数
决定了程序运行快慢。由于程序里内嵌套了多个for循环,建议有多少核就用多少核,如果满核的话,CPU占用率会达到100%,这很正常。
.\auto_OPA.exe -j 16
说明
参数也可以联合指定,不区分先后顺序
.\auto_OPA.exe -i 30 600 -o 875 1900 -s 0.5 -j 16
此外,程序还有默认值(就是任何参数都不指定的情况下),可自行到源码里设置,然后编译运行
其中放大倍数的上下限是根据输入区间范围和输出区间范围动态决定的,并不需要额外指定。
如果需要,那么可以显示设定上下限
vector<double> generate_O_values(double x_min, double x_max, double Vmin, double Vmax, double step) {vector<double> values;double min_O = ceil((Vmin/x_max)/step)*step; // 显式下限double max_O = floor((Vmax/x_min)/step)*step; // 精确上限for (double v = min_O; v <= max_O + 1e-9; v += step) {values.push_back(v);}return values; }
运行结果
程序运行
保存的结果,其中O1为一级放大器的两个放大倍数,O2位二级放大器的放大倍数
源代码
gitcode:GitCode - 全球开发者的开源社区,开源代码托管平台
github:ichliebedich-DaCapo/auto_OPA
#include <iostream> #include <vector> #include <algorithm> #include <thread> #include <mutex> #include <atomic> #include <fstream> #include <string> #include <cmath> #include <iomanip> #include <getopt.h>using namespace std;// ANSI颜色代码 #define BLUE "\033[34m" #define GREEN "\033[32m" #define RESET "\033[0m"struct Result {double O1min{}, O1max{}, O2min{}, O2max{};vector<double> gains;vector<double> split_points; };vector<Result> global_results; mutex results_mutex; atomic<int> processed(0); atomic<int> found_results(0);vector<double> generate_O_values(double x, double Vmax, double step) {vector<double> values;double max_O = Vmax / x;for (double v = step; v <= max_O + 1e-9; v += step){values.push_back(v);}return values; }void display_progress(int total, int found) {const float progress = static_cast<float>(processed) / total;const int bar_width = 50;cout << BLUE << "\r[";const int pos = bar_width * progress;for (int i = 0; i < bar_width; ++i){if (i < pos) cout << "=";else if (i == pos) cout << ">";else cout << " ";}cout << "] " << static_cast<int>(progress * 100.0) << "%" << RESET;cout << " " << GREEN << "Found: " << found << RESET << flush; }void display_thread_func(int total) {while (processed < total){this_thread::sleep_for(chrono::milliseconds(100));int current_processed = processed.load();int current_found = found_results.load();display_progress(total, current_found);}display_progress(total, found_results.load());cout << endl; }void worker(const vector<pair<int, int> > &O1_combs, const vector<pair<int, int> > &O2_combs,const vector<double> &O1_values, const vector<double> &O2_values,double x1, double x2, double Vmin, double Vmax, int total_O1_combs) {while (true){int idx = processed.fetch_add(1);if (idx >= total_O1_combs) break;const auto &[fst, snd] = O1_combs[idx];const double O1min = O1_values[fst];const double O1max = O1_values[snd];for (auto &O2_pair: O2_combs){const double O2min = O2_values[O2_pair.first];const double O2max = O2_values[O2_pair.second];const double k1 = O1min * O2min;const double k2 = O1min * O2max;const double k3 = O1max * O2min;const double k4 = O1max * O2max;if (k1 == k2 || k1 == k3 || k1 == k4 || k2 == k3 || k2 == k4 || k3 == k4) continue;vector<double> gains = {k1, k2, k3, k4};ranges::sort(gains);do{if (gains[3] < Vmin / x2 - 1e-9 || gains[3] > Vmax / x2 + 1e-9) continue;if (gains[0] < Vmin / x1 - 1e-9) continue;double d0 = x1;double d1_low = max(d0, Vmin / gains[1]);double d1_high = Vmax / gains[0];if (d1_low > d1_high + 1e-9) continue;double d1 = d1_low;double d2_low = max(d1, Vmin / gains[2]);double d2_high = Vmax / gains[1];if (d2_low > d2_high + 1e-9) continue;double d2 = d2_low;const double d3_low = max(d2, Vmin / gains[3]);const double d3_high = min(Vmax / gains[2], x2);if (d3_low > d3_high + 1e-9) continue;double d3 = d3_low;if (d3 > x2 + 1e-9) continue;Result res;res.O1min = O1min;res.O1max = O1max;res.O2min = O2min;res.O2max = O2max;res.gains = gains;res.split_points = {d1, d2, d3};lock_guard<mutex> lock(results_mutex);global_results.push_back(res);found_results.fetch_add(1);} while (ranges::next_permutation(gains).found);}} }int main(int argc, char *argv[]) {double x1 = 30, x2 = 600, Vmin = 875, Vmax = 1950, step = 0.5;int threads = 16;// 解析命令行参数int opt;while ((opt = getopt(argc, argv, "i:o:s:j:h")) != -1){switch (opt){case 'i':x1 = stod(optarg);x2 = stod(argv[optind++]);break;case 'o':Vmin = stod(optarg);Vmax = stod(argv[optind++]);break;case 's':step = stod(optarg);break;case 'j':threads = stoi(optarg);break;case 'h':// 中文会乱码// cout << "[-i]:输入区间范围,比如-i 0.03 0.6\n"// << "[-o]:输出区间范围,比如-o 0.9 2\n"// << "[-s]:步长,比如-s 0.5\n"// << "[-j]:线程数,比如-j 16\n"// << "[-h]:帮助信息\n";;cout << "Two-Stage Programmable Amplifier Configuration Finder\n\n"<< "Usage:\n"<< " ./auto_OPA -i <x_low> <x_high> -o <Vmin> <Vmax> [options]\n\n"<< "Required Parameters:\n"<< " -i Input voltage range (left-closed right-open interval)\n"<< " Example: -i 0.03 0.6\n"<< " -o Desired output voltage range\n"<< " Example: -o 1.0 3.3\n\n"<< "Options:\n"<< " -s Step size for gain search (default: 0.1)\n"<< " -j Number of parallel threads (default: CPU core count)\n"<< " -h Display this help message\n\n"<< "Validation Criteria:\n"<< " 1. Input coverage: [x_low, x_high] must be fully covered\n"<< " 2. Output constraint: ∀x∈[x_low,x_high], Vmin ≤ x·gain ≤ Vmax\n"<< " 3. Gain continuity: Adjacent regions must have overlapping gains\n";exit(1);default:cerr << "Usage: " << argv[0]<< " -i x_low x_high -o Vmin Vmax -s step -j threads\n";exit(1);}}// 打印参数cout << "Vin [" << x1 << "," << x2 << "]\nVout [" << Vmin << "," << Vmax << "]\nstep=" << step << "\nthreads=" <<threads << endl;auto O1_values = generate_O_values(x1, Vmax, step);auto O2_values = generate_O_values(x1, Vmax, step);vector<pair<int, int> > O1_combs;for (int i = 0; i < O1_values.size(); ++i)for (int j = i + 1; j < O1_values.size(); ++j)O1_combs.emplace_back(i, j);vector<pair<int, int> > O2_combs;for (int i = 0; i < O2_values.size(); ++i)for (int j = i + 1; j < O2_values.size(); ++j)O2_combs.emplace_back(i, j);int total_O1_combs = O1_combs.size();thread display_thread(display_thread_func, total_O1_combs);vector<thread> workers;for (int i = 0; i < threads; ++i)workers.emplace_back(worker, ref(O1_combs), ref(O2_combs), ref(O1_values),ref(O2_values), x1, x2, Vmin, Vmax, total_O1_combs);for (auto &t: workers) t.join();display_thread.join();ofstream out("results.txt");for (int i = 0; i < global_results.size(); ++i){auto &[O1min, O1max, O2min, O2max, gains, split_points] = global_results[i];out << "Result " << i + 1 << ":\n";out << fixed << setprecision(6);out << "O1: [" << O1min << ", " << O1max << "]\n";out << "O2: [" << O2min << ", " << O2max << "]\n";out << "Gains: ";for (auto g: gains) out << g << " ";out << "\nSplit Zone: ";// 子区间double zone[]={x1, split_points[0], split_points[1], split_points[2], x2};for (int n = 0; n < 4; ++n){out << "\n[" << zone[n] << "," << zone[n + 1] << "]\t\t"<<"[" << gains[n] * zone[n] << "," << gains[n]*zone[n + 1] << "]";}out << "\n\n";}return 0; }
下载小工具
程序已上传gitcode和github,可通过下面链接进行下载
Release详情 - auto_OPA - GitCode
Release auto_OPA v1.0.0 · ichliebedich-DaCapo/auto_OPA
三级程控放大倍数(无)
依照相同的原理,三级也能实现,不过计算量实在太大,不建议编程来确定最佳区间。