Пролог

В программировании микроконтроллеров часто приходится писать драйверы для умных и навороченных периферийных ASIC чипов с управлением по I2C интерфейсу. Например NAU8814. В каждой электронной плате как правило 3-5 таких сложных умных ASICов с собственными внутренними конфигурационными регистрами.

Вот список ASICов, для которых только мне пришлось так или иначе дописывать драйвер: ad5641, ad9833, bh1750, ds3231, dw1000, ds18b20, drv8711, fda801, ksz8081, lan8720, ltr390, max9860, mx25l6433f, nau8814, pm6766 , SdCard, si4737, sx1262, tic12400, tja1101, tcan4550_q1, wm8731. В каждом из них есть регистры.

Обычно перед запуском эти чипы надо правильным образом сконфигурировать. А затем время от времени, следует вычитывать и распознавать какие-то результаты измерений этих чипов. Потом при проблемах в работе приходится выгребать диагностику и правильным образом интерпретировать значения ячеек памяти этих ASICов.

Такие чипы всегда оперируют с реальными физическими величинами: cила тока, освещённость, напряжение, расстояние, частота, громкость, и прочее и прочее. Физические величины - это всё непрерывные величины. Одновременно с этим ячейки памяти этих ASIC чипов - дискретные, двоичные. Поэтому производители микросхем, все как один, кодируют эти непрерывные физические величины дискретными бинарными кодами разной разрядности.

Иной раз получается так что LookUp таблица просто циклопических размеров. Однако значения там подчиняются линейной функции.

Практическая часть

Итак, танцуем от печки... Вот кусок реального datasheet(а) для аудиокодека NAU8814. Надо написать Cи-функцию, которая, не много не мало, по 8-ми битному бинарному коду DACGAIN даст усиление Mode в dB. И вторую функцию, которая сделает то же, только наоборот. Даешь усиление в dB, получаешь бинарный код DACGAIN.

Очевидно что писать два switch() case на 255 позиций это очень долго, дорого, громоздко и умножает вероятность наляпать ошибок и опечаток. Дело в том, что спека аудиокодека сама подсказывает, что это отображение подчиняется линейной функции.

Издёвка судьбы ещё и в том, что авторы datasheet(та) не предоставляют формулы уравнения для пересчёта кода в значение и значения в код. Вот такие вот пирожки с капустой...

Постановка задачи

При написании драйверов для сложных ASICов постоянно приходится решать вот эти две задачи:

1--Написать Си функцию, которая будет ставить в соответствие бинарному коду физическую величину.

И обратную задачу:

2--Написать Си функцию, которая будет ставить в соответствие физической величине бинарный коду.

Под словом "часто" подразумевается от 7 до 15 раз на один ASIC чип. Можно прикинуть насколько это рутинная задача...

Теоретическая часть

Основная идея в том, что надо составить функцию вида (1)

y=ax+b \qquad  \qquad \qquad (1)

которая и будет воплощать эту таблицу из datasheet. Надо лишь как-то найти коэффициенты a и b для этого набора данных. Как мы знаем ещё со школьной скамьи линейную функцию можно составить по двум точкам: (x1 y1) (x2 y2). То есть надо решить вот эту систему линейных уравнений

\left\{\begin{matrix} y_1=ax_1+b\\ y_2=ax_2+b  \end{matrix}\right.   \qquad \qquad \qquad \qquad (2)

Решить (2) это можно школьными методами. Из первого уравнения выразить b и подставить его во второе уравнение. Из второго уравнения найдем a

a=\frac{y_2-y_1}{x_2-x_1}   \qquad  \qquad \qquad (3)

и подставим a (3) в первое уравнение. Таким образом найдем и b (4).

b=\frac{y_1x_2-x_1y_2}{x_2-x_1}   \qquad  \qquad \qquad (4)

На этом этапе с математикой всё понятно. Теперь надо воплотить её в коде.

Программная часть

А теперь вопрос. Кто должен будет делать эти вычисления коэффициентов a и b? DeskTop? Калькулятор Casio? В уме? А почему бы не сам микроконтроллер? Да... Дело в том, что в каждой хорошей прошивке и так есть интерфейс командной строки поверх UART. CLI(шка)! Если кто-то работал с Zephyr RTOS тот знает о чем идет речь... Для тех кто не в теме могу порекомендовать вот этот текст https://habr.com/ru/articles/694408/.

Надо написать вспомогательную UART-CLI команду с 4мя аргументами


typedef struct{
    double x;
    double y;
}Mapping_t;

typedef struct{
    double a;
    double b;
    Mapping_t M[2];
}SolverSlae_t;


bool solver_slae_calc_ab_command(int32_t argc, char* argv[]){
    bool res = false;

    SolverSlae_t SolverSlae = {0};
  
    if(1<=argc) {
        res = try_str2number(argv[0], &SolverSlae.M[0].x);
        if(false == res) {
             LOG_ERROR(SOLVER, "ParseErr X1 %s", argv[0]);
        }
    }

    if(2<=argc) {
        res = try_str2number(argv[1], &SolverSlae.M[0].y);
        if(false == res) {
             LOG_ERROR(SOLVER, "ParseErr Y1 %s", argv[1]);
        }
    }

    if(3<=argc) {
        res = try_str2number(argv[2], &SolverSlae.M[1].x);
        if(false == res) {
             LOG_ERROR(SOLVER, "ParseErr X2 %s", argv[2]);
        }
    }

    if(4<=argc) {
        res = try_str2number(argv[3], &SolverSlae.M[1].y);
        if(false == res) {
             LOG_ERROR(SOLVER, "ParseErr Y2 %s", argv[3]);
        }
    }

    if(res){
        res=solver_slae_calc_ab(&SolverSlae);
        if(res) {
        	LOG_INFO(SOLVER, "Y=(%f)*X +(%f)", SolverSlae.a, SolverSlae.b);
        }
    }else{
    	 LOG_ERROR(SOLVER, "Usage: sab x1 y1 x2 y2");
    }
    return res;
}

которая вызовет функцию solver_slae_calc_ab(), которая по двум точкам сама рассчитает нам эти коэффициенты a и b.


bool solver_slae_calc_ab(SolverSlae_t* const Solver){
    bool res = false;
    if(Solver){
    	double denominator=Solver->M[1].x-Solver->M[0].x;
    	double y1 =Solver->M[0].y;
    	double y2 =Solver->M[1].y;

      	double x1 =Solver->M[0].x;
        double x2 =Solver->M[1].x;
	    Solver->a = (y2-y1)/denominator;
    	Solver->b = (y1*x2 - x1*y2)/denominator;
    	LOG_INFO(SOLVER,"y=ax+b, y=(%f)x+(%f)",Solver->a,Solver->b);
    	res = true;
    }
	return res;
}

и выдаст ответ сюда же в UART.

Отладка

Со стороны программиста всё выглядит очень удобно. В прошивке просто появляется вот такая команда sab с 4мя аргументами.

Для получения из кода величины надо выполнять sab 1 -127 255 0. Аргументы берутся прямо из datasheet(а) и сама прошивка там мгновенно показывает нужные коэффициенты.

Теперь, зная коэффициенты а и b мы можем с лёгкостью написать ту самую функцию отображения сырых значений регистров ASIC(а) в человеко-читаемую физическую величину Gain.

/*sab 1 -127 255 0

I [SOLVER] y=ax+b, y=(0.5)x+(-127.5)
I [SOLVER] Y=(0.5)*X +(-127.5)
*/
fGain_t NauDacGainCodeToGain(uint16_t code){
    fGain_t dac_gain = 0;
    if(code) {
        dac_gain=(fGain_t) (0.5*((float)code) -127.5);
    }else {
        dac_gain=-999.0;
    }
    LOG_DEBUG(NAU8814,"Code:%u=0x%04x=0b%s->Gain:%f dB", code, code, utoa_bin8(code), dac_gain);
    return dac_gain;
}

А теперь мы, вдруг, хотим прописать физическую величину в регистр ASICа. Для получения из величины бинарный код, надо снова открыть UART-CLI и выполнять всё ту же команду sab, только с другими аргументами -127 1 0 255

И аналогично, мы получаем функцию для конвертации Gain в сырой код для ASICа


/*sab -127 1 0 255

  :I [SOLVER] y=ax+b, y=(2)x+(255)
  :I [SOLVER] Y=(2)*X +(255)
*/

 uint8_t NauGainToGainCode(fGain_t dac_gain){
    uint16_t code = 0;
    if(-127.0<=dac_gain) {
        if(dac_gain<=0.0) {
            code  = 2.0*dac_gain+255.0;
        }else{
            code = 0xFF;
        }
    }else{
        code  = 0;
    }
    LOG_DEBUG(NAU8814,"Gain:%f dB->Code:%u=0x%04x=0b%s",dac_gain, code, code, utoa_bin8(code));
    return (uint8_t) code;
}

Всё очень просто.

Что можно улучшить?

При разработке электроники также часто приходится рассчитывать делители напряжений, аналоговые фильтры первого прядка, коэффициенты PLL. Всё это широко известные формулы. Поэтому их тоже можно добавить в debug версию прошивки как отдельные команды-калькуляторы.

Пример консольного калькулятора делителей напряжения
Пример консольного калькулятора делителей напряжения

Можно и вовсе в прошивке сделать калькулятор выражений из строки, чтобы не лазить каждый раз в python.

Итоги

В результате этого простенького кодо генератора линейных функций, можно сэкономить очень много времени при написании драйверов массивных ASIC чипов.

Благодаря наличию UART-CLI вы можете использовать свою же прошивку как специфический калькулятор для решения специфических задач, как говорят: "не отходя от кассы".

Надеюсь, что этот текст поможет другим программистам-микроконтроллеров тоже применять этот трюк.

Links

http://latex.codecogs.com/eqneditor/editor.php

Калькулятор PLL https://habr.com/ru/articles/803415/

Распознавание Вещественного Числа из Строчки https://habr.com/ru/articles/757122/

UART-Shell https://habr.com/ru/articles/694408/

Комментарии (5)


  1. MasterMentor
    18.05.2024 01:30
    +6

    За ASIC плюс, за код (математику) - минус.

    В качестве подарка функция вычисления коэффицентов линейного уравнения по двум точкам:

    Hidden text

    function linear_const(x1,y1,x2,y2)
    {
    // ax+by+c=0
    var a = y2-y1;
    var b = x1-x2;
    var c = y1*(x2-x1)-x1*(y2-y1);
    return [a,b,-c];
    }

    +алгоритм решения системы линейных уравнений методом Гаусса (на вход дать матрицу строк из linear_const (...) ):

    Hidden text
    /**
    https://onecompiler.com/javascript/3zs6bubue
    */
    function gaussianElimination(A)
    {
      const M = A.length; // Number of equations
      const N = A[0].length-1; // Number of variables
    
    
      // Perform Gaussian Elimination
      for (let j = 0; j < N; j++) {
        // Find the pivot row with the largest absolute value in the current column
        let maxRow = j;
        for (let i = j + 1; i < M; i++) {
          if (Math.abs(A[i][j]) > Math.abs(A[maxRow][j])) {
            maxRow = i;
          }
        }
    
        // Swap rows if needed
        if (maxRow !== j) {
          [A[j], A[maxRow]] = [A[maxRow], A[j]];
        }
    
        // Eliminate other rows
        for (let i = 0; i < M; i++) {
          if (i !== j) {
            const factor = A[i][j] / A[j][j];
            for (let k = j; k <= N; k++) {
              A[i][k] -= factor * A[j][k];
            }
          }
        }
      }
    
      // Back substitution to find the solutions
      const solutions = new Array(N);
      for (let j = 0; j < N; j++) {
        solutions[j] = A[j][N] / A[j][j];
      }
    
      return solutions;
    }


    1. aabzel Автор
      18.05.2024 01:30

      Код должен быть на Си. Его же в прошивку вклинить надо.


      1. MasterMentor
        18.05.2024 01:30
        +4

        Напишите вместо var double. ;)


  1. Ivanii
    18.05.2024 01:30

    Для простых случаев подбираю коэффициенты в экселе, для сложных считаю/анализирую там же. Для выбора варианта удобно одновременно видеть сразу 2 - 5 табличек с коэффициентами и результатами.


  1. wataru
    18.05.2024 01:30
    +5

    Как-то вы перемудрили. Для этой таблицы все коэффициенты подбираются за секунду руками: прирост по 0.5 от соседних значений, значит у нас y=0.5*x+b. Подстаяляем x=1, y=-127 из второй строки таблицы: -127=0.5+b, и получаем b=-127.5 в 2 тривиальнейших арифметических действия.

    Можно это и в коде сделать, задав туда лишь 2 строки таблицы. И вы вместо простейшей программы ниже нагородили аж целый Solver:

    printf("Введите первые 2 любые строки таблицы в виде x0 y0 x1 y1");
    double x0, y0, x1, y1;
    scanf("%lf%lf%lf%lf", &x0, &y0, &x1, &y1);
    printf("y = %lf * x + %lf\n", (y1-y0) / (x1-x0), (x1*y0-x0*y1) / (x1-x0));