Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32G474 · уровень средний · 18 мин чтения
Открыть в интерактивном справочнике

198н. Мини-проект: приёмник GPS — NMEA, PPS и точное время

Модуль GPS отдаёт две вещи. По UART — текст NMEA: время, координаты, качество решения, раз в секунду по нескольку строк. На отдельном выводе — импульс PPS, фронт которого стоит на границе секунды куда точнее, чем любой кварц на плате. Из этого получается прибор, который знает, где он и который час, и попутно меряет собственные часы: сколько на самом деле тикает таймер за секунду и насколько спешит часовой кварц RTC.

Здесь приёмник собран на NUCLEO-G474RE. Текст идёт через кольцо DMA, разбирается по байту с проверкой суммы, PPS ловит захват 32-битного TIM2 и метка времени RTC, а по меткам ядро считает калибровку RTC_CALR и сдвиг RTC_SHIFTR из раздела 12а.

Проверено без платы и без модуля. Ядро (gps_core.c) прогнано в опыте sim_gps.c на синтетике: свои строки NMEA с порчей, поток байтов на 9600 бит/с через кольцо DMA, PPS с дрожанием, пропуском и помехой, часы RTC со спешащим кварцем. Опыт собран gcc на ПК и arm-none-eabi-gcc для Cortex-M4, в qemu-system-arm -M netduinoplus2 вывод совпал с ПК строка в строку. Модель на Python (gps_host.py) заново считает суммы и повторяет оценки PPS и RTC бит в бит. Прошивка gps_g474.c собрана armclang по заголовкам пака G4 без предупреждений; на плате не запускалась.

Опоры: стандарт NMEA 0183 платный и здесь не сверялся — формат строк взят по общеизвестному описанию, примеры в разделе свои, а не из документов модулей. RM0440 — таблица входов DMAMUX, TIM2, RTC; DS12288 — выводы и альтернативные функции, точность HSI16 (табл. 43); BSP NUCLEO-G474RE из пака — такт примера и выводы платы. UART и кольцо DMA — разделы 16 и 16б, захват таймера — 13.6, RTC, CALR, SHIFTR и метка времени — разделы 12 и 12а. Проверка — _verify/gps198n_check.py.

Как это называется в документации

Оригинал Перевод
NMEA sentence, talker ID строка NMEA, кто говорит: GP — GPS, GN — несколько систем
RMC, GGA, GSA минимум навигации, решение с высотой, спутники в решении
checksum контрольная сумма: XOR символов между $ и *
PPS, pulse per second импульс в начале каждой секунды
fix, HDOP решение есть или нет; ухудшение точности по горизонтали
circular DMA, HT, TC, IDLE DMA по кругу; половина, конец буфера; тишина на линии
input capture захват: счётчик таймера в момент фронта
smooth calibration, shift гладкая калибровка RTC; сдвиг на долю секунды

198н.1 Что делает прибор и из чего он

Функция Как
текст NMEA USART1 на 9600 бит/с, DMA1 канал 1 по кругу, кольцо 128 байт
разбор по байту, сумма XOR; из RMC — время, дата, координаты, из GGA — решение, спутники, высота
частота такта PPS на захват TIM2 (32 бита, 170 МГц), усреднение по окну
часы метка времени RTC на фронте PPS: ход кварца → CALR, фаза → SHIFTR
вывод строка раз в секунду в LPUART1 — виртуальный COM ST-LINK

Приёмник GPS на NUCLEO-G474RE: модуль отдаёт текст NMEA в USART1 на 9600 бит/с, DMA1 кладёт байты в кольцо 128 байт, ядро разбирает строки RMC и GGA по событиям половина, конец и тишина; импульс PPS идёт на захват TIM2 на 170 МГц и на метку времени RTC на PC13, по захватам считается ошибка такта, по меткам — калибровка CALR 0x81D9 и сдвиг SHIFTR; за сутки часы уходят на −3,214 с до калибровки и на −518 мкс после

Сигнал Вывод Периферия
TX модуля → приём PA10 USART1_RX, AF7
приём модуля ← передача PA9 USART1_TX, AF7
PPS PA0 TIM2_CH1, AF1
PPS PC13 RTC_TS — метка времени RTC
виртуальный COM PA2, PA3 LPUART1, AF12

Выводы и функции — по таблицам DS12288, LPUART1 — по BSP платы. PPS заводится на два вывода сразу: захват TIM2 и метка времени RTC берут фронт каждый со своего вывода. На PC13 у NUCLEO-G474RE по BSP сидит и кнопка пользователя — о ней ниже, в граблях.

Такт — как в примере BSP платы: HSI16 / 4 × 85 / 2 = 170 МГц. Точность HSI16 по DS12288 (табл. 43) — от 15,88 до 16,08 МГц при 30 °C, то есть от −0,75 до +0,5 %, а шаг подстройки — 0,2…0,4 %. Для времени нужен кварц; в опыте такт таймера спешит на +12,5 млн⁻¹, как у кварца, а фильтр импульсов PPS в ядре пропускает отклонения до ±1 % — годится и для HSI16.


198н.2 Строка NMEA и её сумма

Строка начинается с $, дальше поля через запятую, после * — две шестнадцатеричные цифры суммы и конец строки \r\n. Сумма — XOR всех символов между $ и *. Пример из опыта:

$GPRMC,120005.00,A,5545.1249,N,03737.5678,E,0.12,254.3,280926,,,A*6F

Поля RMC по порядку: время hhmmss.ss, статус (A — данные верны, V — нет), широта ddmm.mmmm и полушарие, долгота dddmm.mmmm и полушарие, скорость в узлах, курс, дата ddmmyy. GGA добавляет качество решения, число спутников, HDOP и высоту. Ядро разбирает строку в целых числах, без float: время — в сотых секунды от полуночи, координаты — в градусах × 10⁷. Минуты переводятся в доли градуса делением на 60:

Поле Строка Ядро
время 120005.00 4 320 500 сотых — 12:00:05,00
широта 5545.1249,N 557 520 817 — 55,7520817°
долгота 03737.5678,E 376 261 300 — 37,6261300°
решение, спутники, HDOP 1, 09, 0.9 1, 9, 9 десятых
высота 156.3,M 1563 дм

Модель на Python посчитала те же суммы своим кодом (0x6F у RMC, 0x35 у строки GSA из 51 символа) и те же градусы через деление с плавающей точкой.

Разбор идёт автоматом по байту: ждём $, копим тело и XOR, после * — две цифры, потом конец строки. Всё, что не сходится, считается и выбрасывается: неверная сумма, $ посреди строки (прошлая оборвалась), строка длиннее буфера 96 байт. Предел длины строки по общеизвестному описанию — 82 символа, запас на случай нестандартных модулей. Источник (GP, GN, GL…) ядро не смотрит: RMC есть RMC.

    case 4:
        r->state = 0;
        if (c != '\r' && c != '\n') {
            r->broken++;
            return NMEA_NONE;
        }
        if (r->cs != r->cs_rx) {
            r->bad_cs++;
            return NMEA_NONE;
        }

При статусе V ядро координаты из RMC не берёт — оставляет прежние.


198н.3 Приём: кольцо DMA

Модуль шлёт пачку строк раз в секунду, а между пачками молчит. Байты кладёт DMA1 в кольцо 128 байт по кругу, запрос USART1_RX идёт через DMAMUX (вход 24 по RM0440). Где сейчас пишет DMA, говорит счётчик CNDTR: позиция записи — 128 − CNDTR. Ядро забирает всё от своей позиции чтения до позиции записи (ring_take, приём по кругу — раздел 16б).

Вопрос — когда забирать. При 9600 бит/с байт идёт 1,04 мс, в секунду — до 960 байтов. Кольцо 128 байт заполняется за 133,3 мс: забрать надо раньше, иначе DMA молча перепишет непрочитанное. Опыт — 200 секунд, по три строки в секунду (RMC, GGA, GSA — 199 байт, 207,3 мс на линии), с порчей: каждая 37-я строка с неверной суммой, каждая 101-я без конца строки, раз в 50 с — строка из 120 символов. Всего 600 строк, 40 282 байта.

Когда забирать RMC GGA GSA Неверная сумма Оборвана Длинная
половина, конец кольца, тишина на линии 192 193 194 16 5 4
опрос раз в 250 мс 0 0 190 5 5 0

По событиям DMA (половина и конец кольца — HT, TC) и USART (тишина на линии — IDLE) ядро поймало ровно подложенные ошибки: 16, 5 и 4, и ничего не потеряло. Во время пачки события приходят каждые 66,7 мс, а IDLE отдаёт хвост пачки сразу, не дожидаясь заполнения половины.

Опрос раз в 250 мс — это до 240 байтов между опросами при кольце на 128. Из пачки выживает только конец: все RMC и GGA потеряны, остались 190 строк GSA. Сумма здесь не помогла: переписанные строки не пришли с неверной суммой — они не пришли вовсе. Заметить это можно только по счётчику строк.


198н.4 PPS на захват TIM2

TIM2 на G474 — 32-битный. Он считает 170 МГц без предделителя (такт 5,88 нс) и на фронте PPS защёлкивает счётчик в CCR1. Разность соседних захватов — сколько тактов таймера уложилось в секунду GPS. Разность по модулю 2³² верна и через переполнение: счётчик обходит круг за 25,26 с, а секунда — много меньше.

Ядро копит разности в окне и отдаёт ошибку такта в млрд⁻¹. Разность, которая дальше ±1 % от 170 000 000, — не секунда: пропущенный импульс даёт две, помеха — половину. Такая разность сбрасывает окно. Опыт: 1300 секунд, такт спешит на +12 500 млрд⁻¹, фронт PPS дрожит на ±100 нс, на 700-й секунде импульса нет, на 900,5-й — лишний.

Окно Окон Оценка, млрд⁻¹ Граница ошибки
1 с 1296 12 300…12 694 ±205,9
16 с 79 12 489…12 510 ±12,9
64 с 19 12 498…12 502 ±3,2
256 с 3 12 500 ±0,8

Отброшено три разности: одна на пропуске и две на помехе. Граница — дрожание двух фронтов и один такт таймера, делённые на окно: сумма разностей за окно — это просто последний захват минус первый, и дрожание внутренних фронтов сокращается. Поэтому точность растёт как 1/окно, а не как корень. Модель на Python повторила все четыре строки.

Сумма — в 64 битах. За 64 с набегает 10 880 136 016 тактов, а разность 32-битных захватов «последний минус первый» даёт 2 290 201 424 — два полных круга по 2³² потеряны. Поэтому ядро складывает посекундные разности в int64_t, а не вычитает концы окна.


198н.5 Часы RTC по PPS

Часы идут от кварца LSE, делители — PREDIV_A = 3, PREDIV_S = 8191: секунда делится на 8192 шага по 122 мкс, и калибровка работает полностью (12а.2). PPS на выводе RTC_TS защёлкивает в метке времени секунды календаря и счётчик SS. Фаза часов на фронте — секунды × 8192 + (8191 − SS); сравнение двух фаз через 1024 с даёт ход кварца с шагом 1 / (1024 × 8192) — 0,119 млн⁻¹.

Дальше — формулы 12а: CALM − 512 · CALP ≈ 2²⁰ × ошибку, а сдвиг на долю секунды — SHIFTR. Опыт: кварц отстаёт на 37,2 млн⁻¹, часы стартовали с фазой 5027/8192 с.

Шаг Что вышло
ход за 1024 с −37 193 млрд⁻¹ (на деле −37 200)
CALR 0x81D9: CALP = 1, CALM = 473, действие +37 195 млрд⁻¹
полный перебор 1024 сочетаний то же 0x81D9
ход после калибровки −6 млрд⁻¹; второе окно 1024 с меряет 0 — меньше шага
уход за сутки −3,214 с до калибровки, −518 мкс после
фаза на фронте SS = 3476 — часы на 0,5756 с за границей секунды
SHIFTR 0x8000126B: ADD1S и SUBFS = 4715 — вперёд на 1 − 0,5756 с
следующий фронт SS = 8191 — граница секунды RTC на фронте PPS

Больше половины секунды — выгоднее сдвинуть вперёд с ADD1S, меньше — назад одним SUBFS. Число секунд после сдвига сверяется с временем из RMC: SHIFTR правит только долю, календарь ставится отдельно (12а).


198н.6 Какой секунде принадлежит строка. Прошивка

PPS отмечает начало секунды, а строка со временем приходит позже — в опыте первая строка пачки начинается через 50 мс после фронта. Какую секунду она описывает — ту, что началась, или следующую, — решает модуль, и общеизвестного правила для всех модулей нет. В модели строка описывает только что начавшуюся секунду; для настоящего модуля это надо взять из его описания или проверить: сравнить время RMC с номером импульса за несколько минут.

Прошивка gps_g474.c — всё на регистрах CMSIS. Прерывания DMA и USART имеют один приоритет, поэтому ring_take не вызовется сама из себя. Захват PPS читает CCR1, и задержка прерывания на результат не влияет: время фронта уже защёлкнуто. Метка RTC читается раньше сброса TSF — сброс её обнуляет (12а.6). Проверки при сборке:

armclang 6.24 собрал ядро и прошивку без предупреждений: 2010 и 1712 байт кода, 352 байта ОЗУ.


198н.7 Грабли

Опрос кольца реже, чем оно заполняется. 250 мс против 133 мс: все RMC и GGA потеряны, а неверной суммы нет — строки просто исчезли. Забирать по HT, TC и IDLE.

Разность через 32 бита. Захваты за 64 с вычитать нельзя — два круга по 2³² пропадут. Складывать посекундные разности в 64 бита.

Каждый импульс — секунда. Пропуск PPS даёт две секунды, помеха — половину. Без проверки на ±1 % одна помеха испортит оценку окна.

Мало секунд в окне. За 1 с оценка гуляет на ±0,2 млн⁻¹, за 256 с — меньше чем на тысячную (±0,8 млрд⁻¹).

Статус V. Координаты в такой строке не годятся. Смотреть статус RMC и качество решения GGA.

Сдвиг без проверки секунды. SHIFTR правит долю секунды, а номер секунды надо сверить с RMC.

Метка сброшена раньше, чем прочитана. Сброс TSF обнуляет TSSSR и TSTR (12а).

Кнопка на PC13. На NUCLEO-G474RE по BSP кнопка пользователя — тот же вывод, что RTC_TS: нажатие даст ложную метку.

Время от HSI16. Ошибка до −0,75…+0,5 % — PPS её покажет, но шаг подстройки 0,2…0,4 % не даёт её убрать. Для времени нужен кварц.


198н.8 Итоги

Полные исходники

gps_core.c — ядро приёмника GPS.

/* gps_core.c — раздел 198н: ядро приёмника GPS (см. gps_core.h). */
#include "gps_core.h"

_Static_assert(NMEA_MAX >= 82u, "строка NMEA 0183 — до 82 символов, буфер не меньше");

/* ── NMEA: поля ────────────────────────────────────────────────────────── */
static uint32_t field(const char *s, uint32_t n, unsigned idx, const char **p)
{
    uint32_t i = 0;
    while (idx && i < n) {                                     /* пропустить idx запятых */
        if (s[i++] == ',')
            idx--;
    }
    if (idx)
        return 0;
    *p = s + i;
    uint32_t k = i;
    while (k < n && s[k] != ',')
        k++;
    return k - i;
}

/* десятичное число с frac знаками после точки: «4507.1234», frac = 4 → 45071234 */
static int fixed(const char *p, uint32_t len, unsigned frac, int32_t *out)
{
    int32_t v = 0, sign = 1;
    unsigned got = 0, dot = 0, digits = 0;
    if (len && *p == '-') {
        sign = -1;
        p++;
        len--;
    }
    for (uint32_t i = 0; i < len; i++) {
        char c = p[i];
        if (c == '.' && !dot) {
            dot = 1;
            continue;
        }
        if (c < '0' || c > '9')
            return -1;
        if (dot && got == frac)
            continue;                                          /* лишние знаки отбросить */
        v = v * 10 + (c - '0');
        digits++;
        got += dot;
    }
    if (!digits)
        return -1;
    while (got++ < frac)
        v *= 10;
    *out = sign * v;
    return 0;
}

/* ddmm.mmmm или dddmm.mmmm → градусы × 10⁷ */
static int coord(const char *p, uint32_t len, const char *h, uint32_t hl, int32_t *out)
{
    int32_t v;
    if (fixed(p, len, 5, &v) || v < 0 || hl != 1)
        return -1;
    int32_t deg = v / 10000000, min_e5 = v % 10000000;         /* минуты × 10⁵ */
    int32_t e7 = deg * 10000000 + (int32_t)(((int64_t)min_e5 * 100 + 30) / 60);
    *out = (*h == 'S' || *h == 'W') ? -e7 : e7;
    return 0;
}

static int utc(const char *p, uint32_t len, uint32_t *cs)
{
    int32_t v;
    if (fixed(p, len, 2, &v) || v < 0)
        return -1;
    uint32_t hh = (uint32_t)v / 1000000u, mm = (uint32_t)v / 10000u % 100u, ss = (uint32_t)v / 100u % 100u;
    if (hh > 23u || mm > 59u || ss > 60u)
        return -1;
    *cs = ((hh * 60u + mm) * 60u + ss) * 100u + (uint32_t)v % 100u;
    return 0;
}

static int parse(const char *s, uint32_t n, nmea_fix *f)
{
    const char *p, *h;
    uint32_t l, hl;
    int32_t v;
    if (n < 6u || s[5] != ',')
        return NMEA_OTHER;
    int rmc = s[2] == 'R' && s[3] == 'M' && s[4] == 'C';      /* GP, GN, GL… — любой источник */
    int gga = s[2] == 'G' && s[3] == 'G' && s[4] == 'A';
    if (!rmc && !gga)
        return NMEA_OTHER;
    nmea_fix t = *f;
    l = field(s, n, 1, &p);
    if (utc(p, l, &t.time_cs))
        return NMEA_OTHER;
    if (rmc) {
        l = field(s, n, 2, &p);
        t.status = l == 1u ? *p : 'V';
        l = field(s, n, 3, &p);
        hl = field(s, n, 4, &h);
        if (t.status == 'A' && coord(p, l, h, hl, &t.lat_e7))
            return NMEA_OTHER;
        l = field(s, n, 5, &p);
        hl = field(s, n, 6, &h);
        if (t.status == 'A' && coord(p, l, h, hl, &t.lon_e7))
            return NMEA_OTHER;
        l = field(s, n, 9, &p);
        if (fixed(p, l, 0, &v) == 0)
            t.date = (uint32_t)v;
    } else {
        l = field(s, n, 6, &p);
        t.fix = (uint8_t)(fixed(p, l, 0, &v) ? 0 : v);
        l = field(s, n, 7, &p);
        t.sats = (uint8_t)(fixed(p, l, 0, &v) ? 0 : v);
        l = field(s, n, 8, &p);
        t.hdop_x10 = (uint16_t)(fixed(p, l, 1, &v) ? 0 : v);
        if (t.fix) {
            l = field(s, n, 2, &p);
            hl = field(s, n, 3, &h);
            if (coord(p, l, h, hl, &t.lat_e7))
                return NMEA_OTHER;
            l = field(s, n, 4, &p);
            hl = field(s, n, 5, &h);
            if (coord(p, l, h, hl, &t.lon_e7))
                return NMEA_OTHER;
            l = field(s, n, 9, &p);
            if (fixed(p, l, 1, &t.alt_dm))
                return NMEA_OTHER;
        }
    }
    *f = t;
    return rmc ? NMEA_RMC : NMEA_GGA;
}

uint8_t nmea_checksum(const char *body, uint32_t n)
{
    uint8_t cs = 0;
    while (n--)
        cs ^= (uint8_t)*body++;
    return cs;
}

static int hex(char c)
{
    return c >= '0' && c <= '9' ? c - '0' : c >= 'A' && c <= 'F' ? c - 'A' + 10 : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1;
}

int nmea_feed(nmea_rx *r, char c, nmea_fix *f)
{
    if (c == '$') {
        if (r->state)
            r->broken++;                                       /* начало новой строки посреди старой */
        r->state = 1;
        r->n = 0;
        r->cs = 0;
        return NMEA_NONE;
    }
    switch (r->state) {
    case 1:
        if (c == '*') {
            r->state = 2;
        } else if (c == '\r' || c == '\n') {
            r->broken++;
            r->state = 0;
        } else if (r->n >= NMEA_MAX) {
            r->overflow++;
            r->state = 0;
        } else {
            r->buf[r->n++] = c;
            r->cs ^= (uint8_t)c;
        }
        return NMEA_NONE;
    case 2:
    case 3: {
        int h = hex(c);
        if (h < 0) {
            r->broken++;
            r->state = 0;
            return NMEA_NONE;
        }
        r->cs_rx = (uint8_t)(r->state == 2 ? h << 4 : r->cs_rx | h);
        r->state++;
        return NMEA_NONE;
    }
    case 4:
        r->state = 0;
        if (c != '\r' && c != '\n') {
            r->broken++;
            return NMEA_NONE;
        }
        if (r->cs != r->cs_rx) {
            r->bad_cs++;
            return NMEA_NONE;
        }
        r->buf[r->n] = 0;
        {
            int t = parse(r->buf, r->n, f);
            r->ok += t != NMEA_OTHER;
            r->other += t == NMEA_OTHER;
            return t;
        }
    default:
        return NMEA_NONE;                                      /* мусор между строками */
    }
}

/* ── кольцо DMA ────────────────────────────────────────────────────────── */
uint32_t ring_take(const uint8_t *ring, uint32_t size, uint32_t *rd, uint32_t wr, byte_sink sink, void *ctx)
{
    uint32_t n = 0;
    while (*rd != wr) {
        sink(ring[*rd], ctx);
        *rd = *rd + 1u == size ? 0u : *rd + 1u;
        n++;
    }
    return n;
}

/* ── PPS ───────────────────────────────────────────────────────────────── */
void pps_init(pps_est *p, uint32_t nominal, uint32_t window)
{
    p->nominal = nominal;
    p->window = window;
    p->last = p->n = p->rejected = 0;
    p->have = 0;
    p->sum = 0;
}

int pps_add(pps_est *p, uint32_t capture, int32_t *ppb)
{
    if (!p->have) {
        p->have = 1;
        p->last = capture;
        return 0;
    }
    uint32_t d = capture - p->last;                            /* по модулю 2³²: переполнение счётчика не мешает */
    uint32_t tol = p->nominal / 100u;                          /* ±1 %: пропуск импульса даёт 2×, помеха — доли */
    p->last = capture;
    if (d < p->nominal - tol || d > p->nominal + tol) {
        p->rejected++;
        p->n = 0;
        p->sum = 0;
        return 0;
    }
    p->sum += d;
    if (++p->n < p->window)
        return 0;
    int64_t exp = (int64_t)p->nominal * p->n, num = (p->sum - exp) * 1000000000LL;
    *ppb = (int32_t)(num >= 0 ? (num + exp / 2) / exp : -((-num + exp / 2) / exp));
    p->n = 0;
    p->sum = 0;
    return 1;
}

/* ── RTC ───────────────────────────────────────────────────────────────── */
int64_t rtc_phase(uint32_t sec, uint32_t ss, uint32_t prediv_s)
{
    return (int64_t)sec * (prediv_s + 1u) + (int64_t)prediv_s - ss;   /* SS считает вниз */
}

int32_t rtc_drift_ppb(int64_t ph0, int64_t ph1, uint32_t seconds, uint32_t prediv_s)
{
    int64_t den = (int64_t)seconds * (prediv_s + 1u);
    int64_t num = (ph1 - ph0 - den) * 1000000000LL;
    return (int32_t)(num >= 0 ? (num + den / 2) / den : -((-num + den / 2) / den));
}

uint32_t rtc_calr_for(int32_t ppb)
{
    int64_t x = (int64_t)ppb * 1048576;                        /* CALM − 512·CALP ≈ 2²⁰ × ошибку */
    int32_t v = (int32_t)(x >= 0 ? (x + 500000000) / 1000000000 : -((-x + 500000000) / 1000000000));
    if (v > 511)
        v = 511;
    if (v < -511)
        v = -511;
    return v >= 0 ? (uint32_t)v : 0x8000u | (uint32_t)(512 + v);
}

int32_t rtc_calr_ppb(uint32_t calr)
{
    int64_t p = calr >> 15 & 1u, m = calr & 0x1FFu;
    int64_t num = (512 * p - m) * 1000000000LL, den = 1048576 + m - 512 * p;
    return (int32_t)(num >= 0 ? (num + den / 2) / den : -((-num + den / 2) / den));
}

uint32_t rtc_shift_for(uint32_t ss, uint32_t prediv_s)
{
    uint32_t frac = prediv_s - ss;                             /* тактов ck_apre после границы секунды */
    uint32_t add1s = 2u * frac >= prediv_s + 1u;               /* больше полсекунды — проще вперёд */
    return (add1s ? 0x80000000u : 0u) | frac;                  /* назад на frac или вперёд на 1 − frac */
}

Дальше: раздел 198о «Мини-проект: ИК-пульт — приём и передача протокола NEC».

Открыть в интерактивном справочнике