Математика в програмуванні — це не інтеграли й матриці, а кілька десятків простих прийомів, які щодня рятують від дивних багів: ділення з округленням, бітові маски, кільцевий буфер, масштабування сигналу АЦП, фільтр шуму чи порівняння дробових чисел. Зібрали базові прийоми з прикладами на C, які однаково корисні для прошивок мікроконтролерів, десктопних програм і скриптів.

Цілочисельне ділення: округлення вгору і до найближчого
У C і більшості мов ділення цілих чисел просто відкидає дробову частину: 7 / 2 = 3. Коли потрібне інше округлення, додають поправку до діленого:
int div_round(int a, int b) { return (a + b / 2) / b; } // до найближчого: 7/2 -> 4
int div_ceil(int a, int b) { return (a + b - 1) / b; } // вгору: скільки пакетів по b байт
Ці формули правильні для додатних чисел. Для від’ємних пам’ятайте: у C ділення округлює до нуля (-7 / 2 = -3, а -7 % 2 = -1), а в Python — вниз (-7 // 2 = -4, -7 % 2 = 1). Тому при перенесенні алгоритму між мовами від’ємні значення варто перевіряти окремо.
Переповнення змінних і таймер millis()
Змінна uint8_t після 255 стає 0, uint32_t — після 4 294 967 295. Через це лічильник millis() в Arduino обнуляється приблизно через 49,7 доби, і код, який порівнює абсолютні значення часу, дає збій. Правильно — порівнювати різницю: now - last >= PERIOD. А на AVR тип int має лише 16 біт, тож навіть 60 * 1000 переповнюється. Усі деталі, приклади і спосіб перевірки без очікування 49 днів — в окремій статті переповнення millis() в Arduino і 16-бітний int на AVR.
Математика в програмуванні: бітові операції та маски
Регістри мікроконтролерів, прапорці стану і протоколи обміну — це окремі біти. Чотири базові операції варто знати напам’ять:
reg |= (1u << 5); // встановити біт 5
reg &= ~(1u << 5); // скинути біт 5
reg ^= (1u << 5); // інвертувати біт 5
if (reg & (1u << 5)) { } // перевірити біт 5
uint8_t hi = (word >> 8) & 0xFF; // старший байт 16-бітного числа
uint8_t lo = word & 0xFF; // молодший байт
uint16_t w = ((uint16_t)hi << 8) | lo; // зібрати назад
Корисні наслідки двійкової арифметики:
- Зсув замість множення. x << 3 — це x × 8, x >> 2 — ділення на 4 для беззнакових чисел.
- Перевірка степеня двійки: x && !(x & (x – 1)) — істина для 1, 2, 4, 8…
- Вирівнювання вгору до кратного A (A — степінь двійки): (x + A – 1) & ~(A – 1). Так вирівнюють адреси й розміри буферів для DMA.
- Остача від ділення на степінь двійки: x % 16 те саме, що x & 15 для беззнакових — і значно швидше на мікроконтролерах без апаратного ділення.
Модульна арифметика: кільцевий буфер
Кільцевий (циклічний) буфер — основа прийому даних з UART, логування й фільтрів. Індекс після останнього елемента повертається на нуль. Якщо розмір буфера — степінь двійки, операцію % замінюють маскою:
#define N 64 // обов'язково степінь двійки
static uint8_t buf[N];
static volatile uint16_t head, tail;
void put(uint8_t b) { buf[head] = b; head = (head + 1) & (N - 1); }
int get(void) {
if (head == tail) return -1; // буфер порожній
uint8_t b = buf[tail]; tail = (tail + 1) & (N - 1);
return b;
}
Масштабування: від сирого АЦП до вольт
АЦП повертає ціле число, а показати треба мілівольти, градуси чи відсотки. Лінійне перетворення з одного діапазону в інший:
int32_t map_range(int32_t x, int32_t in_min, int32_t in_max,
int32_t out_min, int32_t out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
// 12-бітний АЦП, опорна 3,3 В -> мілівольти без float
uint32_t mv = (uint32_t)raw * 3300u / 4095u;
Порядок операцій важливий: спочатку множимо, потім ділимо, інакше втратимо точність. raw / 4095 * 3300 майже завжди дасть 0. Якщо на вході стоїть дільник напруги, коефіцієнт зручно порахувати нашим калькулятором дільника напруги.
Фіксована кома замість float
На мікроконтролерах без FPU (STM32F0, ATmega, ESP8266) операції з float повільні в десятки разів. Рішення — зберігати дробові числа як цілі, помножені на 2ⁿ. Формат Q16.16 означає 16 біт цілої частини і 16 біт дробової:
typedef int32_t q16;
#define Q16(x) ((q16)((x) * 65536.0)) // константа на етапі компіляції
q16 q_mul(q16 a, q16 b) { return (q16)(((int64_t)a * b) >> 16); }
q16 q_div(q16 a, q16 b) { return (q16)(((int64_t)a << 16) / b); }
q16 k = Q16(1.25);
q16 y = q_mul(x, k); // y = x * 1,25 без float
Простіший варіант для побутових задач — масштабований цілий тип: зберігати температуру в десятих частках градуса (235 = 23,5 °C), а напругу — в мілівольтах.
Обмеження, інтерполяція і таблиці
Три маленькі функції, які трапляються майже в кожному проєкті:
static inline int32_t clamp(int32_t x, int32_t lo, int32_t hi) {
return x < lo ? lo : (x > hi ? hi : x);
}
// лінійна інтерполяція між двома точками (x0,y0)-(x1,y1)
int32_t lerp(int32_t x, int32_t x0, int32_t y0, int32_t x1, int32_t y1) {
return y0 + (int32_t)((int64_t)(x - x0) * (y1 - y0) / (x1 - x0));
}
// таблиця АЦП -> температура (десяті градуса) для термістора
static const int16_t adc_t[] = { 3500, 2900, 2100, 1300, 700 };
static const int16_t deg_t[] = { -200, 0, 250, 500, 800 };
int16_t ntc_temp(int16_t adc) {
if (adc >= adc_t[0]) return deg_t[0];
for (int i = 1; i < 5; i++)
if (adc >= adc_t[i])
return lerp(adc, adc_t[i-1], deg_t[i-1], adc_t[i], deg_t[i]);
return deg_t[4];
}
Таблиця з інтерполяцією (LUT) замінює складну формулу — наприклад, рівняння Стейнхарта-Харта для термістора — кількома точками з даташиту. Значення в прикладі умовні: для свого датчика візьміть точки з його характеристики.
Фільтрація шуму: проста математика в програмуванні датчиків
Сигнал з АЦП завжди шумить. Два найпростіші фільтри:
// ковзне середнє за 8 відліками: сума оновлюється за O(1)
static int16_t win[8]; static int32_t sum; static uint8_t idx;
int16_t avg8(int16_t x) {
sum += x - win[idx]; win[idx] = x; idx = (idx + 1) & 7;
return (int16_t)(sum >> 3); // ділення на 8 зсувом
}
// експоненційний фільтр (EMA): y += (x - y) / 2^K, лише цілі числа
#define K 4
static int32_t acc; // зберігає y * 2^K
int16_t ema(int16_t x) {
acc += x - (acc >> K);
return (int16_t)(acc >> K);
}
Ковзне середнє рівномірно згладжує, але потребує буфер. Експоненційний фільтр займає одну змінну, а «інерцію» задає K: що більше K, то сильніше згладжування і повільніша реакція. Детальніше про частоту зрізу таких фільтрів — у нашому калькуляторі RC-фільтра: EMA поводиться як цифровий аналог RC-ланки.
Порівняння дробових чисел
0.1 + 0.2 у double дорівнює 0.30000000000000004, тому порівняння через == для float майже завжди помилка. Порівнюйте з допуском, причому відносним для великих чисел:
#include <math.h>
int nearly_equal(float a, float b) {
float diff = fabsf(a - b);
float scale = fmaxf(fabsf(a), fabsf(b));
return diff <= 1e-6f * scale || diff < 1e-9f;
}
Гроші й лічильники ніколи не зберігайте у float: використовуйте цілі копійки чи мілісекунди.
НСД і частоти таймерів
Найбільший спільний дільник (алгоритм Евкліда) стає в пригоді, коли треба підібрати дільник таймера, скоротити дріб передавального відношення або знайти спільний період двох подій:
uint32_t gcd(uint32_t a, uint32_t b) { while (b) { uint32_t t = a % b; a = b; b = t; } return a; }
uint32_t lcm(uint32_t a, uint32_t b) { return a / gcd(a, b) * b; } // спершу ділимо — менше ризик переповнення
Наприклад, дві задачі з періодами 12 мс і 18 мс збігаються кожні lcm(12, 18) = 36 мс.
Потрібна прошивка для вашого пристрою?
Розробляємо електроніку і програми для мікроконтролерів STM32, ESP32 та AVR: від схеми й плати до робочої прошивки.
Обговорити проєктВисновок
Математика в програмуванні мікроконтролерів і звичайних програм зводиться до кількох прийомів: правильне округлення при діленні, обережність з переповненням і порядком операцій, бітові маски, модульна арифметика для буферів, фіксована кома замість float і прості цифрові фільтри. Вони роблять код швидшим і передбачуваним, а значить — позбавляють від помилок, які проявляються раз на 49 днів.
Детальніше: Fixed-point arithmetic, Q (number format), Circular buffer (Wikipedia).
