" В программировании нет царского пути. "

Пролог

В математике есть такая тема как Диофантовы уравнения. В самом простом виде выглядят они так.

ax+by=d \qquad \qquad\qquad (1)

Тут одно уравнение и две неизвестные: x,y. При этом a, b, d - это известные константы.

На первый взгляд может показаться как бесполезная вещь для программирования Micro Controller Unit (MCU), но не тут-то было.

Когда программируешь микроконтроллер (MCU), то первое, что приходится делать - это конфигурировать тактирование процессорного ядра. Тактированием заправляет устройство с названием phase-locked loop (он же PLL,он же ФАПЧ). Это подсистема SoC(а).

И там надо подобрать три константы (натуральных числа): M, N и F. Вот например в китайском микроконтроллере Artery, абстрактно изображая, эта электрическая цепь выглядит вот так. Умножители и делители частот.

По формуле (2)

\frac{(N\frac{F_{quartz}}{M})   }  {F} = F_{sys}    \qquad  \qquad  (2)

Вычисляется системная частота F_sys (sclk). Это уравнение (2) преобразуется в уравнение (3)

F_{quartz}   \frac{N}{M} -F_{sys}F     = 0   \qquad  \qquad  (3)

А уравнение (3), в свою очередь, это самое настоящее Диофантово уравнение! Да.. Тут F_quartz - это частота кварцевого резонатора, который задается схемотехникой электронной платы.
F_sys задается программистом для достижения желаемой производительности прошивки. Остаются целочисленные неизвестные N, M и F.

Далее накладываются ещё ограничения уже производителем микроконтроллера. Для Artery это

Переменная

Минимальное

Максимальное

M

1

15

N

31

500

FR (степени двойки)

1

32

Это ограничение даже прописано комментариями в исходных кодах на Cи, которые бесплатно предоставляет производитель Artery Tech.


/**
  *    config crm pll
  *                        pll_rcs_freq * pll_ns
  *         pll clock = --------------------------------
  *                           pll_ms * pll_fr_n
  *         attemtion:
  *                  31 <= pll_ns <= 500
  *                  1  <= pll_ms <= 15
  *
  *                       pll_rcs_freq
  *         2mhz <=  ---------------------- <= 16mhz
  *                          pll_ms
  *
  *                       pll_rcs_freq * pll_ns
  *         500mhz <=  -------------------------------- <= 1200mhz
  *                               pll_ms
  * @param  clock_source
  *         this parameter can be one of the following values:
  *         - CRM_PLL_SOURCE_HICK
  *         - CRM_PLL_SOURCE_HEXT
  * @param  pll_ns (31~500)
  * @param  pll_ms (1~15)
  * @param  pll_fr
  *         this parameter can be one of the following values:
  *         - CRM_PLL_FR_1
  *         - CRM_PLL_FR_2
  *         - CRM_PLL_FR_4
  *         - CRM_PLL_FR_8
  *         - CRM_PLL_FR_16
  *         - CRM_PLL_FR_32
  * @retval none
  */
void crm_pll_config(crm_pll_clock_source_type clock_source, 
                    uint16_t pll_ns, 
                    uint16_t pll_ms,
                    crm_pll_fr_type pll_fr)
  

Итак, постановка задачи:

Есть электронная плата с Artery MCU на борту. На PCB также припаян кварцевый резонатор c частотой 8 MHz и подключен к микроконтроллеру. Необходимо программно сконфигурировать PLL так, чтобы базовая системная частота ядра стала ровно на 100 MHz.

Спрашивается, какими при этом должны быть коэффициенты PLL: N, M и RF?

Решение

По сути, задача свелась к тому, что надо решить Диофантово уравнение (4). Вот оно перед вами.

8000000 \frac{N}{M} -100000000F     = 0   \qquad  \qquad  (4)

Можно попробовать прибегнуть к помощи пресловутого Artificial intelligence (AI) на сайте Wolfram Alfa

Однако это не решение в частном виде. На практике же нужно именно численное решение, чтобы проинициализировать им аргументы функции crm_pll_config().

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

Для этого я написал свой простой численный решатель Диофантова уравнения для PLL прямо на Си.

uint64_t ipow(uint32_t base, uint32_t exponenta) {
	uint64_t ret = 1, i = 0;
    if(0 != exponenta) {
        for(i = 1; i <= exponenta; i++) {
            ret *= base;
        }
    }
    return ret;
}

typedef struct{
    uint32_t ms;
    uint32_t ns;
    uint32_t fr;
}PllArtety_t;

bool pll_calc_artery(uint32_t freq_xtal_hz, 
                     uint32_t freq_sys_hz, 
                     PllArtety_t* const PllArtety) {
    bool res = false;

    LOG_INFO(PLL_CALC, "FreqXtal:%u Hz,FreqSys:%u  Hz", 
             freq_xtal_hz, freq_sys_hz);
    cli_printf("{ [ (  {Xtal:%uHz} /M )*N ]/FR  }= Sys:%u Hz" CRLF, 
               freq_xtal_hz, freq_sys_hz);
    uint32_t solution_cnt = 0;
    if(PllArtety) {
        uint32_t m = 0;
        uint32_t temp_hz = 0;
        uint32_t temp_m_hz = 0;
        uint32_t cur_freq_sys_hz = 0;
        for(m = 1; m <= 15; m++) {
            uint32_t n = 0;
            temp_m_hz = freq_xtal_hz/m;
            if(2000000<=temp_m_hz) {
                if(temp_m_hz<=16000000){
                    for(n = 31; n <= 500; n++) {
                        uint32_t f = 0;
                        for(f = 0; f <= 5; f++) {
                            uint32_t fr = ipow(2, f);
                            cur_freq_sys_hz = ((n * freq_xtal_hz) / (m * fr));
                            if(freq_sys_hz == cur_freq_sys_hz) {
                                temp_hz = freq_xtal_hz * n / m;
                               /*condition from Artery New Clock Config*/
                                if(500000000 <= temp_hz) {
                                    if(temp_hz <= 1200000000) {
                                        solution_cnt++;
                                        cli_printf("%u: MS:%2u,NS:%3u,FR:%2u" CRLF,
                                                   solution_cnt, m, n, fr);
                                        PllArtety->ms = m;
                                        PllArtety->ns = n;
                                        PllArtety->fr = fr;
                                        res = true;
                                    }
                                }
                            }
                        }
                    }
            	}
            }
        }
    }
    if(res) {
        LOG_INFO(PLL_CALC, "SpotPllVals! %u Solutions",solution_cnt);
    } else {
        LOG_ERROR(PLL_CALC, "NoPllVals!");
    }
    return res;
}

Поверьте, микроконтроллер достаточно мощный, чтобы самостоятельно подобрать себе коэффициенты для PLL. Настолько мощный, что можно найти коэффициенты даже обыкновенным перебором. 

Стоит отметить, что настройки PLL нельзя просто так взять и пере инициализировать. Если вы аккуратно пере инициализируете PLL далеко в run-time, то у вас слетят настройки UART, заклинит CLI, поменяется тактирование SPI, I2S, SysTick станет тикать быстрее (или медленнее), HeartBeat LED сойдет с ума, аппаратные таймеры станут мерять время в попугаях. Это будет коллапс системы.

Поэтому, если вы хотите поменять частоту ядра, то надо как-то передать значение частоты через отработку ResetHandler-а. Как известно, ResetHandler при пуске обнуляет всю RAM память. Как же можно сохранить значение переменной в RAM памяти между программными пере сбросами ядра? Ответ прост. Следует передать настройки целевой частоты ядра ��ерез NVRAM.

Отладка

Вот так, в UART-CLI прошивки я исполнил команду обсчета PLL и получил аж 4 решения на выбор.

Вот эти решения:

1: MS:1, NS:100, FR:8
2: MS:2, NS:200, FR:8
3: MS:3, NS:300, FR:8
4: MS:4, NS:400, FR:8

Теперь я выполняю команду сохранить настройки желаемой частоты в NVRAM и применить настройки перезагрузив микроконтроллер.

Далее подставив четвёртое решение в утилиту проверки PLL я увидел, что конфиг валидный!

Успех!

Еще я собрал отдельную консольную утилиту для вычисления коэффициентов PLL. Там есть поддержка для различных микроконтроллеров в том числе и Artery.

Дальнейшее развитие.

STM32
PLL присутствует не только в MCU Artery. У микроконтроллеров STM32, внезапно, тоже есть настройка тактирования через PLL. Можно сделать встроенный в прошивку калькулятор и для микроконтроллеров семейства STM32.

Решаем как и раньше, перебором в четырех циклах for, отсеивая при этом решения, который выходят за рамки вот этих четырёх ограничений.

Перед вам результат вычисления PLL для частоты ядра 100 MHz и и кварца 8MHz. Как видно, точного решения тут быть не может. Зато программа мгновенно подобрала ближайшее решение с погрешностью всего-навсего 4MHz.

Есть и точные решения настроек PLL для различных частот ядра. При условии, что кварц 8MHz и нужна частота USB на pllq (48MHz). Вот все решения для целых значений мегагерц.

код для вычислений во вложении

Скрытый текст

const uint32_t p_vals[4] = { 2, 4, 6, 8 };
bool pll_calc_stm32_config(const uint32_t freq_xtal_hz, const uint32_t in_need_freq_sys_hz,
        PllStm32Config_t* const Config) {
    bool res = false;

    PllStm32Config_t TempConfig={0};
    TempConfig.core_freq_hz=in_need_freq_sys_hz;
    TempConfig.xtall_freq_hz=freq_xtal_hz;
    TempConfig.error_freq_hz=INT_MAX;
    Config->core_freq_hz = in_need_freq_sys_hz;
    Config->xtall_freq_hz = freq_xtal_hz;

    int32_t need_freq_sys_hz = (int32_t) in_need_freq_sys_hz;
    LOG_DEBUG(PLL_CALC, "FreqXtal:%u Hz,FreqSys:%u Hz", freq_xtal_hz, need_freq_sys_hz);
#ifdef HAS_CUSTOM_PRINTF
    LOG_DEBUG(PLL_CALC, "SolveEquation: [      ( {Xtal:%uHz} /M )*N    ]/P  = SysClk:%u Hz" CRLF, freq_xtal_hz, need_freq_sys_hz);
#endif

    if(freq_xtal_hz) {
        if(need_freq_sys_hz) {
            if(Config) {
                res = true;
            }
        }
    }

    if(res) {
        res = false;
        int32_t min_abs_error_freq_hz = INT_MAX;
        uint32_t m = 0;
        for (m = 2; m <= 63; m++) {
            uint32_t n = 0;
            int32_t pllm_out = freq_xtal_hz / m;
            res = is_range_uint32(pllm_out, 950000, 2100000);
            if(res) {
                res = false;
                for (n = 50; n <= 432; n++) {
                    int32_t plln_out = pllm_out * n;
                    res = is_range_uint32(plln_out, 100000000, 432000000);
                    if(res) {
                        res = false;
                        uint32_t q = 0;
                        for (q = 2; q <= 15; q++) {
                            int32_t pllq_out = plln_out / q;
                            res = is_valid_pllq(pllq_out);
                            if(res) {
                                uint32_t p_i = 0;
                                for (p_i = 0; p_i < 4; p_i++) {
                                    int32_t pllp_out = plln_out / p_vals[p_i];
                                    res = is_range_uint32(pllp_out, 24000000, 168000000);
                                    if(res) {
                                        res = false;
                                        TempConfig.M = m;
                                        TempConfig.N = n;
                                        TempConfig.P = p_vals[p_i];
                                        TempConfig.Q = q;
                                        bool cfg_valid = pll_stm32_is_valid_config(&TempConfig);
                                        if(cfg_valid) {
                                            int32_t cur_error_freq_hz = (need_freq_sys_hz - pllp_out);
                                            int32_t cur_abs_error_freq_hz = abs(cur_error_freq_hz);
                                            TempConfig.error_freq_hz = cur_error_freq_hz;
                                            if(cur_abs_error_freq_hz < min_abs_error_freq_hz) {
                                                memcpy(Config,&TempConfig,sizeof(PllStm32Config_t));
                                                LOG_DEBUG(PLL_CALC, "ValidConfig,AbsErr:%d,%s",cur_abs_error_freq_hz, PllStm32ConfigToStr(Config));
                                                min_abs_error_freq_hz = cur_abs_error_freq_hz;
                                                if(0 == cur_error_freq_hz) {
                                                    LOG_DEBUG(PLL_CALC, "SpotSolution!,%s",
                                                            PllStm32ConfigToStr(Config));
                                                    res = true;
                                                    return res;
                                                }
                                            }
                                        } // if(cfg_valid)
                                    }
                                } //for(p_i=0;p_i<4;p_i++)
                            } // if(PLL_USB_CLOCK_HZ==usb_freq_hz)
                        } //for(q=2;q<=4;q++)
                    }//res = is_range_uint32(plln
                } //for(n=50;n<=432;n++)
            }
        } // for(m=2;m<=63;m++)
    }
    return res;
}

CAN bit Rate

--Диофантовы уравнения также приходится решать при настройке битовой скорости CAN шины. Да, а Вы как хотели? Там надо из одного числа (суммы квантов) найти аж 4 числа: sync, prop, seg1, seg2.

Достоинства вычисления настроек PLL в run-time
1++Вы можете менять частоту ядра без пере прошивки микроконтроллера. Это ускоряет разработку.
2++Появляется возможность снижать требования к электропитанию за счет уменьшения производительности процессорного ядра.
3++Вы можете устанавливать любые частоты ядра, даже экзотические значения, например 155 MHz.

Недостатки

--Вычисление коэффициентов PLL требует времени (порой доходит до пары секунд). На малых частотах ядра это будет явно заметно. Типичный пользователь начинает беситься если его прошивка перезагружается дольше чем полторы секунды. Вот например тут конфиг для PLL вычисляется аж 4,1 сек.

Казалось бы неприятное залипание при каждом старте прошивки. Что же делать? Но даже это не проблема. Вы можете вычислить конфиг далеко в run time в любое удобное для вас время, а затем просто сохранить настройки PLL в NVRAM. При старте прошивки просто взять готовый конфиг для PLL из NVRAM и сразу применить его.

--Надо инициализировать NVRAM до настройки PLL.

Итоги

Как видите, программирование микроконтроллеров требует математической подготовки. А как Вы хотели?

В программировании нет царского пути.

Удалось сделать автоматическое вычисление многочисленных коэффициентов PLL исходя из одной настройки - желаемой частоты процессорного ядра. Это открывает прямую дорогу для изменения частоты ядра во время исполнения программы. Таким образом можно элегантно регулировать потреблением мощности электронной платы без пере сборки всего проекта. Красота да и только! Можно, например, перейти в режим энергосбережения специально понизив частоту ядра. Или, напротив, увеличить тактирование для ускорения расчета какой-то сложной математической процедуры (например программная криптография).

Суть истории в том, что настройки PLL можно рассчитывать прямо на микроконтроллере. Настройки целевой частоты ядра, как и сами коэффициенты, можно хранить прямо в NVRAM памяти.

Как сами видите, программирование микроконтроллеров это не только варка прошивок, а так же составление всяческих вспомогательных инструментальных консольных утилит: lader-ы, калькуляторы, подпись, обновление версий и прочее и прочее.

Благодаря способности решать Диофантовы уравнения, Вы можете переконфигурировать частоту процессора далеко в run-time.

Вот так..

Links/URLs

Название

URL

Диофантово уравнения

https://ru.wikipedia.org/wiki/Диофантово_уравнение

Бинарь утилиты pll_calc.exe

https://github.com/aabzel/Artifacts/tree/main/pll_calc

Пуск LittleFS (NVRAM с запретом до-записи flash)

https://habr.com/ru/articles/925372/

Фазовая автоподстройка частоты

https://en.wikipedia.org/wiki/Phase-locked_loop

Вольфрам математика

https://www.wolframalpha.com/input?i=8000000x-100000000y%3D0

Редактор формул

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вам приходилось на работе решать Диофантовы уравнения?
34.78%да32
65.22%нет60
Проголосовали 92 пользователя. Воздержались 11 пользователей.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Вы делали программное вычисление PLL конфига при программировании микроконтроллеров?
33.33%да1
66.67%нет2
Проголосовали 3 пользователя. Воздержавшихся нет.