Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103 · уровень средний · 41 мин чтения
Открыть в интерактивном справочнике

22л. Внешние часы DS3231 по I2C: BCD-регистры, будильники, SQW-прерывание, термокомпенсация и старение

Встроенные часы F103 разобраны в 12.6 и в проекте 198: 32-битный счётчик секунд от часового кварца, календарь и будильник — забота программы, ход подстраивается пропуском импульсов. У G4 часы богаче (12а). Здесь — другой путь: отдельная микросхема часов DS3231 на I2C. Когда она лучше встроенных часов, что лежит в её 19 регистрах, почему время читают и пишут одной транзакцией, как устроены два будильника и их маски, как будить микроконтроллер из Stop выводом INT/SQW, что значит флаг OSF, как читать термодатчик и подстраивать ход регистром Aging offset — и что ломается на каждом из этих шагов.

Опоры: RM0008 (DocID13902 Rev 17): Stop — PLL, HSI и HSE выключены, бит LPDS, с. 73; выход из Stop на HSI, вход по SLEEPDEEP, PDDS = 0 и LPDS, при стоящем бите EXTI_PR Stop не наступает, с. 74; вход с подтяжкой, с. 160; AFIO_EXTICR3, с. 191; EXTI_FTSR, с. 211; EXTI_PR, с. 212; I2C до 400 кГц, с. 752; приём ведущим, с. 763–764; I2C_CCR, с. 783–784; I2C_TRISE, с. 784. DocID13587 Rev 17 (STM32F103x8/xB): PB6 — I2C1_SCL, PB7 — I2C1_SDA, оба FT, с. 32; PB8 — FT, с. 33. Документа на DS3231 в проекте нет. Номера регистров 0x00…0x12, биты CTRL и STAT, биты 12/24 и PM, бит века, маски будильников и правило «годны только 1111, 1110, 1100, 1000 и 0000», бит DY/DT, частоты меандра, цикл измерения температуры 64 с, бит CONV, разбор температуры, «запись секунд перезапускает отсчёт секунды», «флаги снимаются нулём, запись единицы их не трогает», открытый сток у INT/SQW и 32K, адрес 0x68 — сверены с драйверами DS3231 из Zephyr 4.4.2. Температурная кривая обычного часового кварца — из 174.4, шаг подстройки F103 — из 198.4, адрес MPU-6050 — из 99.1. Точность ±2 ppm, шаг Aging offset около 0,1 ppm, копия времени в буфер на START, значения после первого включения, правило високосного года и переключение бита века, работа на 400 кГц и токи — по документации производителя, не сверено: в модели это допущения. Все числа ниже — прогон драйвера _verify/rtc22l/rtc22l.c, собранного arm-none-eabi-gcc 12.2.1 под Cortex-M3 и запущенного в QEMU 7.2 (stm32vldiscovery), против своей модели DS3231 на Python 3.11; I2C в стенде подменён строками по USART1. Прошивка для F103 собрана armclang 6.24.

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

Оригинал Перевод
TCXO (temperature-compensated crystal oscillator) кварцевый генератор с термокомпенсацией
BCD (binary-coded decimal) двоично-десятичный код: по цифре на полубайт
12/24, AM/PM 12- или 24-часовой режим; до и после полудня
century bit бит века
alarm mask bits A1M1…A1M4, A2M2…A2M4 биты «это поле будильника не сравнивать»
DY/DT (day / date) день недели или число месяца
INTCN, INT/SQW выбор: прерывание или меандр на одном выводе
A1IE, A2IE; A1F, A2F разрешение прерывания будильника; флаг будильника
OSF (oscillator stop flag) флаг «генератор стоял»
EOSC (enable oscillator, active low) 1 — на батарейке генератор стоит
BBSQW (battery-backed square wave) меандр и на батарейке
EN32kHz выход 32 кГц
CONV, BSY измерить температуру сейчас; идёт измерение
aging offset поправка частоты: подстройка хода
VBAT вывод батарейки

Вверху — подключение DS3231 к STM32F103: I2C1 на 400 кГц, PB6 — SCL, PB7 — SDA, PB8 — вход INT/SQW на EXTI8 по спаду, у DS3231 адрес 0x68, батарейка на VBAT, выход 32K выключен битом EN32kHz = 0; MPU-6050 на той же шине переведён выводом AD0 = 1 на адрес 0x69; SCL, SDA, INT/SQW и 32K — открытый сток с подтяжками, PB6–PB8 терпят 5 В. Посередине — регистры 0x00…0x12: время в BCD 0x00–0x06 с битом 12/24 в часах и битом века в месяце, будильник 1 0x07–0x0A, будильник 2 0x0B–0x0D, CTRL 0x0E, STAT 0x0F с флагами OSF, A2F и A1F, Aging offset 0x10, температура 0x11–0x12. Внизу — 129 чтений через перенос 31.12.2026 23:59:59 → 01.01.2027: одна транзакция на 7 байт, 232,5 мкс, ошибок 0; семь транзакций по байту — 29 ошибок в окне 725 мкс, худшая 2027-12-31 23:59:59, на год вперёд; будильник 2 каждую минуту за час даёт 60 спадов INT со снятием флага и 1 без снятия


22л.1 Когда внешний чип лучше встроенных часов

У встроенных часов F103 и у DS3231 одна задача — идти, пока всё остальное обесточено. Разница — в источнике секунды. F103 считает импульсы часового кварца, и его частота зависит от температуры по параболе: около −0,035 ppm/°C² с вершиной при 25 °C (174.4). DS3231 меряет температуру раз в 64 с и подстраивает ёмкости своего генератора — так описан цикл в Zephyr; производитель обещает ±2 ppm (не сверено). Модель посчитала, что это значит в секундах:

Температура Кварц по 174.4, ppm В сутки За 30 суток
−20 °C −70,9 −6,12 с −183,7 с
0 °C −21,9 −1,89 с −56,7 с
10 °C / 40 °C −7,9 −0,68 с −20,4 с
25 °C 0 0 0
60 °C −42,9 −3,70 с −111,1 с
DS3231, худший случай ±2 ppm ±2 ±0,173 с ±5,18 с

Подстройка F103 (198.4) только замедляет ход шагом 0,954 ppm и убирает постоянную ошибку, но не температурную. У DS3231 своя поправка — Aging offset, в обе стороны (22л.6). Остальные доводы за внешний чип: календарь с днём недели и два будильника с масками считает сам чип, а не программа; своя батарейка — время переживает и сброс, и перепрошивку, и потерю резервного домена микроконтроллера (10.5). Доводы против: ещё одна микросхема и шина, чтение времени — транзакция I2C (232,5 мкс на 400 кГц), и долей секунды нет — Zephyr прямо пишет, что часы идут по одной секунде без доступа к долям.

Подключение. DS3231 садится на I2C1, как MCP23017 в 22и.2:

DS3231 STM32F103 Что
SCL, SDA PB6, PB7 I2C1, 400 кГц; адрес 0x68
INT/SQW PB8 вход с подтяжкой, EXTI8 по спаду
32K — выход 32 кГц: драйвер его выключает
VBAT батарейка ход без основного питания
VCC, GND 3,3 В и земля общая земля с микроконтроллером

INT/SQW и 32K — открытый сток (так они описаны в Zephyr): без подтяжки спада не будет. PB6, PB7 и PB8 — FT (DocID13587, с. 32–33): подтяжки к 5 В их не сожгут. I2C1 — как в 22и.2: 36 МГц / (3 × 30) = 400 кГц, TRISE = 11 (RM0008, с. 783–784). PB8 — вход с подтяжкой, код 1000 (с. 160), линия EXTI8 привязана к порту B полем AFIO_EXTICR3 = 0001 (с. 191), спад включает EXTI_FTSR (с. 211):

static void pins_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN | RCC_APB1ENR_PWREN;
    /* PB6, PB7 — альтернативная функция, открытый сток, 50 МГц (1111) */
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xFF000000u;
    /* PB8 — вход с подтяжкой (1000): INT/SQW — открытый сток */
    GPIOB->BSRR = 1u << 8;
    GPIOB->CRH = (GPIOB->CRH & ~0xFu) | 0x8u;
    /* PC13 — выход 2 МГц (0010), светодиод горит нулём */
    GPIOC->BSRR = 1u << 13;
    GPIOC->CRH = (GPIOC->CRH & ~0x00F00000u) | 0x00200000u;
}

Выводов адреса у DS3231 нет: адрес всегда 0x68. Тот же адрес у MPU-6050 с AD0 = 0 (99.1), и на одной шине их не посадить — 22л.7.


22л.2 Регистры 0x00…0x12: BCD, 12/24 часа и бит века

Номер Что Биты
0x00 секунды BCD 00…59
0x01 минуты BCD 00…59
0x02 часы бит 6 — 12-часовой режим; в нём бит 5 — PM, иначе бит 5 — десяток «20»
0x03 день недели 1…7; какой день первый — решает программа
0x04 число BCD 01…31
0x05 месяц BCD 01…12; бит 7 — бит века
0x06 год BCD 00…99
0x07…0x0A будильник 1 секунды, минуты, часы, день; бит 7 каждого — «не сравнивать»
0x0B…0x0D будильник 2 минуты, часы, день — без секунд
0x0E CTRL EOSC, BBSQW, CONV, RS2 RS1, INTCN, A2IE, A1IE
0x0F STAT OSF, EN32kHz, BSY, A2F, A1F
0x10 Aging offset число со знаком
0x11, 0x12 температура целые градусы со знаком; в 0x12 биты 7:6 — четверти

BCD. Каждая десятичная цифра — свой полубайт: 59 секунд — это 0x59, а не 0x3B. После установки 2026-10-27 23:59:58 (вторник) первые семь регистров в модели — 58 59 23 02 27 10 26: читаются глазами. Прочитать их как двоичное — частая ошибка: секунды 0…59 превращаются в 60 чисел от 0 до 89 с прыжками 9 → 16, 25 → 32 и т. д. Записать двоичное — тоже: 45 минут как 0x2D — это не BCD.

12 и 24 часа. Драйвер всегда пишет 24-часовой режим, но читает оба: их мог поставить кто-то другой. В 12-часовом бит 6 = 1, бит 5 — после полудня, часы 1…12, и 12 AM — это полночь:

/* 7 байт → время. Понимает и 12-часовой режим, если его поставил кто-то другой: 12 AM — это 0 часов. */
int rtc_from_regs(const uint8_t r[7], rtc_tm_t *t)
{
    uint8_t h = r[2];
    if (!bcd_ok(r[0] & 0x7Fu) || !bcd_ok(r[1] & 0x7Fu) || !bcd_ok(r[4] & 0x3Fu) || !bcd_ok(r[5] & 0x1Fu)
        || !bcd_ok(r[6]) || !bcd_ok(h & (h & DS_H12 ? 0x1Fu : 0x3Fu)))
        return RTC_E_RANGE;
    t->sec = bcd_dec(r[0] & 0x7Fu);
    t->min = bcd_dec(r[1] & 0x7Fu);
    if (h & DS_H12) {
        uint8_t h12 = bcd_dec(h & 0x1Fu);               /* 1…12 */
        if (h12 < 1u || h12 > 12u)
            return RTC_E_RANGE;
        t->hour = (uint8_t)(h12 % 12u + (h & DS_PM ? 12u : 0u));
    } else {
        t->hour = bcd_dec(h & 0x3Fu);
    }
    t->wday = r[3] & 7u;
    t->date = bcd_dec(r[4] & 0x3Fu);
    t->mon = bcd_dec(r[5] & 0x1Fu);
    t->year = (uint16_t)(2000u + bcd_dec(r[6]) + (r[5] & DS_CENTURY ? 100u : 0u));
    return rtc_valid(t) && t->wday >= 1u ? RTC_OK : RTC_E_RANGE;
}

В модели чип перевели в 12-часовой режим и прошли все 24 часа: драйвер прочитал верно 24 из 24. Переход 11:59:59 PM дал регистр часов 0x52 (12 AM) и следующие сутки — 2026-10-28 00:00:00. Чужие драйверы с этим режимом справляются не всегда — 22л.7.

Бит века. Год в чипе — две цифры. Бит 7 месяца переключается на переходе года 99 → 00 (по документации производителя), а что он значит, решает программа. В Zephyr 4.4.2 два драйвера решили по-разному: драйвер counter читает год без бита как 1900-е, с битом — как 2000-е; драйвер rtc бит не смотрит и всегда даёт 20xx (там стоит FIXME). Драйвер этого раздела считает 2000 + год + 100 при бите века:

/* Время → 7 байт с 0x00: всегда 24-часовой режим, день недели считаем сами, век — битом месяца */
void rtc_to_regs(const rtc_tm_t *t, uint8_t r[7])
{
    r[0] = bcd_enc(t->sec);
    r[1] = bcd_enc(t->min);
    r[2] = bcd_enc(t->hour);
    r[3] = rtc_wday(t->year, t->mon, t->date);
    r[4] = bcd_enc(t->date);
    r[5] = (uint8_t)(bcd_enc(t->mon) | (t->year >= 2100u ? DS_CENTURY : 0u));
    r[6] = bcd_enc((uint8_t)(t->year % 100u));
}
Время Семь регистров
2026-10-27 23:59:58, вторник 58 59 23 02 27 10 26
2028-02-29 07:05:09, вторник 09 05 07 02 29 02 28
2099-12-31 23:59:59, четверг 59 59 23 04 31 12 99
2100-03-01 00:00:00, понедельник 00 00 00 01 01 83 00

День недели драйвер считает сам (здесь 1 — понедельник). 2027-02-29 rtc_valid не пропускает.


22л.3 Время пачкой: установка и чтение одной транзакцией

Время — семь регистров, а секунда может кончиться посреди обмена. Отсюда два правила.

Читать все 7 байт одной транзакцией. По документации производителя (не сверено) на START чип копирует время в буфер, и чтение идёт из буфера, пока счёт идёт дальше: перенос посреди пачки её не разорвёт. Zephyr читает так же — все регистры за один i2c_write_read.

Писать с 0x00 одной транзакцией. Установка времени перезапускает цепочку счёта долей секунды — так сказано в Zephyr; по документации производителя её перезапускает запись секунд (не сверено, в модели так). Первым ложится байт секунд, и следующий перенос будет только через секунду, когда остальные шесть байт давно на месте. После записи драйвер снимает OSF: время теперь верное.

/* Время — одной транзакцией с 0x00: первым ложится байт секунд, и отсчёт секунды начинается заново;
 * переноса посреди записи не будет. Потом снять OSF: время теперь верное. */
int rtc_set(rtc_t *d, const rtc_tm_t *t)
{
    uint8_t tx[8], st;
    int rc;
    if (!rtc_valid(t))
        return RTC_E_RANGE;
    tx[0] = DS_SEC;
    rtc_to_regs(t, tx + 1);
    if ((rc = wr(d, tx, 8)) != RTC_OK)
        return rc;
    if ((rc = rd(d, DS_STAT, &st, 1)) != RTC_OK)
        return rc;
    return stat_write(d, st & DS_EN32K, DS_OSF);
}
/* Все 7 байт — одной транзакцией: микросхема копирует время в буфер на START, и пока байты идут по шине,
 * перенос их не разорвёт. */
int rtc_get(rtc_t *d, rtc_tm_t *t)
{
    uint8_t r[7];
    int rc = rd(d, DS_SEC, r, 7);
    if (rc != RTC_OK)
        return rc;
    return rtc_from_regs(r, t);
}

На 400 кГц rtc_get — одна транзакция, 93 такта SCL, 232,5 мкс; rtc_set — три транзакции, 377,5 мкс на шине. На 100 кГц — 930 и 1510 мкс.

Опыт: чтение через перенос года. Модель поставила 2026-12-31 23:59:59 так, чтобы перенос пришёлся на заданный момент, и читала время, начиная за 3 мс до переноса и до 200 мкс после, с шагом 25 мкс — 129 чтений. Годным считался ответ «до переноса» или «после»:

Как читать 400 кГц: ошибок Окно 100 кГц: ошибок Окно
одна транзакция, 7 байт (rtc_get) 0 из 129 — 0 из 129 —
семь транзакций по байту 29 из 129 725 мкс 98 из 129 2450 мкс

Чтение по байту — семь START и семь копий. Если перенос пришёлся между ними, старшие поля уже новые, младшие ещё старые: 2027-01-01 00:00:59 (на 59 с вперёд), 2027-01-01 00:59:59 (на час), 2027-01-01 23:59:59 (на сутки), 2027-01-31 23:59:59 (на месяц) и 2027-12-31 23:59:59 — на год. Все эти даты годные, и проверка диапазона их не поймает. Окно — время от первого байта до последнего: 802,5 мкс у семи транзакций против 232,5 у одной.

Опыт: установка по полю. Старое время 2026-03-31 23:59:59, новое — 2026-05-15 12:00:00; запись начиналась от 1,2 мс до переноса до 100 мкс после, шаг 20 мкс — 66 записей:

Как писать Ошибок Что получилось
rtc_set: 7 байт с 0x00 0 из 66 2026-05-15 12:00:00
по полю, от года к секундам 23 из 66 2026-06-15 12:00:00 — 4 раза; 2026-05-16 12:00:00 — 9; 13:00:00 — 5; 12:01:00 — 5

Месяц уже записан (май), число ещё старое (31), и перенос суток переводит чип на 1 июня; запись числа 15 даёт 15 июня. Так же уезжают сутки, час и минута.


22л.4 Два будильника: маски A1M1…A1M4 и DY/DT

Будильник 1 — секунды, минуты, часы и день (0x07…0x0A), будильник 2 — минуты, часы и день (0x0B…0x0D): секунд у него нет, точность — минута (так в Zephyr), и срабатывает он в начале минуты. Бит 7 каждого байта — маска «поле не сравнивать», бит 6 байта дня — DY/DT: 1 — день недели 1…7, 0 — число месяца. Совпадение ставит флаг A1F или A2F в STAT; если разрешён A1IE или A2IE и INTCN = 1, вывод INT/SQW падает в ноль.

Маски ставят «сверху вниз»: если сравнивается поле, сравниваются и все мельче его. Zephyr называет годными только 1111, 1110, 1100, 1000 и 0000 (в порядке день, часы, минуты, секунды) и остальные отвергает «для предсказуемого поведения»; драйвер — тоже:

/* Будильник n (1 или 2): байты — одной транзакцией, старый флаг снять, потом разрешить AxIE */
int rtc_alarm_set(rtc_t *d, uint8_t n, const rtc_alarm_t *a)
{
    uint8_t tx[5], m = a->mask, x = (uint8_t)(~a->mask & 0x0Fu), k = 0, ctrl, st;
    uint8_t ie = n == 1u ? DS_A1IE : DS_A2IE, fl = n == 1u ? DS_A1F : DS_A2F;
    int rc;
    if (n < 1u || n > 2u || (m & 0xF0u) || ((x + 1u) & x) || (n == 2u && (m & 1u)))
        return RTC_E_MASK;                              /* годны 1111, 1110, 1100, 1000, 0000 */
    if ((!(m & 1u) && n == 1u && a->sec > 59u) || (!(m & 2u) && a->min > 59u) || (!(m & 4u) && a->hour > 23u)
        || (!(m & 8u) && (a->dy ? a->day < 1u || a->day > 7u : a->day < 1u || a->day > 31u)))
        return RTC_E_RANGE;
    tx[k++] = n == 1u ? DS_A1SEC : DS_A2MIN;
    if (n == 1u)
        tx[k++] = (uint8_t)(m & 1u ? DS_AM : bcd_enc(a->sec));
    tx[k++] = (uint8_t)(m & 2u ? DS_AM : bcd_enc(a->min));
    tx[k++] = (uint8_t)(m & 4u ? DS_AM : bcd_enc(a->hour));
    tx[k++] = (uint8_t)(m & 8u ? DS_AM : a->dy ? DS_DYDT | a->day : bcd_enc(a->day));
    if ((rc = wr(d, tx, k)) != RTC_OK)
        return rc;
    if ((rc = rd(d, DS_CTRL, tx, 2)) != RTC_OK)
        return rc;
    ctrl = tx[0];
    st = tx[1];
    if ((rc = stat_write(d, st & DS_EN32K, fl)) != RTC_OK)
        return rc;
    return wr1(d, DS_CTRL, (uint8_t)((ctrl & (uint8_t)~DS_CONV) | DS_INTCN | ie));
}

Для будильника 2 бит секунд в маске драйвера — 0 (сравнить с 00): 1110 — каждую минуту, 1100 — по минутам и так далее. Будильник 07:15:30 по маске 1000 лёг в регистры как 30 15 07 80, будильник 2 «среда, 07:15» — 15 07 43 (0x43 — DY = 1 и день 3).

В модели время шло с 2027-01-01 00:00:00 (пятница), каждое срабатывание программа снимала rtc_alarm_ack:

Будильник Маска Срок Сработал
1: каждую секунду 1111 600 с 600
1: секунды = 30 1110 сутки 1440
1: минуты и секунды 15:30 1100 сутки 24
1: 07:15:30 1000 3 суток 3
1: 31-е число, 07:15:30 0000, DY = 0 2027 год 7
1: понедельник, 07:15:30 0000, DY = 1 2027 год 52
2: каждую минуту 1110 сутки 1440
2: минуты = 15 1100 сутки 24
2: 07:15 1000 3 суток 3

Каждое срабатывание — в момент совпадения. Будильник на 31-е за год срабатывает 7 раз: в месяцах короче 31 дня такого числа нет. Маску 0001 (секунды не сравнивать, остальное — да) драйвер не принял: RTC_E_MASK, как и маску 1111 для будильника 2. Записанная мимо драйвера, в модели она сработала 60 раз за сутки — каждую секунду минуты 07:15.


22л.5 INT/SQW на EXTI: пробуждение из Stop

Вывод INT/SQW делит бит INTCN. При INTCN = 1 это прерывание будильников: ноль, пока стоит флаг разрешённого будильника. При INTCN = 0 — меандр с частотой по RS2 RS1: в модели 00 — 1 Гц, 01 — 1024 Гц, 10 — 4096 Гц, 11 — 8192 Гц (те же частоты в Zephyr); rtc_sqw пишет CTRL = 0x00, 0x08, 0x10 и 0x18. Для пробуждения нужен INTCN = 1.

Флаг надо снять, иначе спада больше не будет. EXTI ловит фронт, а не уровень (RM0008, с. 211). Пока A2F стоит, INT лежит в нуле. Опыт: будильник 2 каждую минуту, час:

Программа Совпадений Спадов INT INT в конце
снимает флаг (rtc_alarm_ack) 60 60 1
не снимает 60 1 0

Без снятия микроконтроллер проснулся один раз из 60. Драйвер снимает только сработавшие флаги, а остальные пишет единицами — запись единицы их не трогает (как в Zephyr):

/* Какие будильники сработали; снять только их флаги — INT отпустится и следующий спад придёт */
int rtc_alarm_ack(rtc_t *d, uint8_t *fired)
{
    uint8_t st;
    int rc = rd(d, DS_STAT, &st, 1);
    *fired = 0;
    if (rc != RTC_OK)
        return rc;
    *fired = st & (DS_A1F | DS_A2F);
    if (!*fired)
        return RTC_OK;
    return stat_write(d, st & DS_EN32K, *fired);
}

Stop. Вход — SLEEPDEEP, PDDS = 0, LPDS и WFI; в Stop выключены PLL, HSI и HSE (RM0008, с. 73). Будит любая линия EXTI в режиме прерывания, и после выхода система идёт от HSI (с. 74): HSE и PLL прошивка включает заново. Если бит EXTI_PR уже стоит, Stop не наступит (с. 74) — поэтому обработчик сразу снимает его записью единицы (с. 212):

/* Stop: регулятор в экономичный режим (LPDS), PDDS = 0, SLEEPDEEP (RM0008, с. 73–74).
 * Если бит EXTI_PR уже стоит, Stop не наступит — программа просто пойдёт дальше (с. 74). */
static void enter_stop(void)
{
    PWR->CR = (PWR->CR & ~PWR_CR_PDDS) | PWR_CR_LPDS;
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
    __WFI();
    SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
}
/* INT/SQW: спад на PB8 — только флаг, обмен по I2C идёт в основном цикле */
void EXTI9_5_IRQHandler(void)
{
    EXTI->PR = 1u << 8;
    irq_flag = 1;
}

Перед сном прошивка смотрит сам вывод PB8: если INT уже в нуле (флаг с прошлого раза), спать нельзя — спада не будет; сначала rtc_alarm_ack. Пробуждение: такты, снятие флага (2 транзакции, 170 мкс), чтение времени (232,5 мкс), и снова Stop.

Снять один флаг и не потерять другой. Наивное снятие A1F — прочитать STAT, сбросить бит, записать прочитанное — пишет ноль и в A2F, если тот встал между чтением и записью. Опыт: A1F уже стоит, A2F встаёт на переносе минуты, снятие начинается от 500 мкс до переноса до 100 мкс после, шаг 5 мкс — 121 попытка:

Снятие 400 кГц: потеряно A2F Окно 100 кГц: потеряно Окно
rtc_alarm_ack 0 — 0 —
прочитать — сбросить — записать 23 115 мкс 43 215 мкс

«Потеряно» — флаг снят, а программа о нём не узнала: будильник 2 пропал. rtc_alarm_ack в том же окне видит A2F в прочитанном и снимает его сам — 35 раз на 400 кГц и 78 на 100 кГц.


22л.6 OSF, батарейка, температура и Aging offset

OSF. Флаг «генератор стоял с тех пор, как флаг снимали» (так в Zephyr). По документации производителя он стоит и после первой подачи питания. В модели (по той же документации) после включения без батарейки STAT = 0x88, а время — 2000-01-01 00:00:00: rtc_get вернул его без ошибки — по виду дата как дата. Поэтому rtc_init смотрит OSF и возвращает RTC_E_LOST:

/* Запуск: генератор на батарейке не останавливать (EOSC = 0), INT/SQW — прерывание будильников, оба
 * выключены; A1F и A2F снять — иначе INT с прошлого раза держится в нуле и спада больше не будет. */
int rtc_init(rtc_t *d, uint8_t addr7)
{
    uint8_t r[2];
    int rc;
    d->addr = addr7;
    d->xfers = d->errors = 0;
    if ((rc = rd(d, DS_CTRL, r, 2)) != RTC_OK)          /* CTRL и STAT одной транзакцией */
        return rc;
    if (r[0] != DS_INTCN && (rc = wr1(d, DS_CTRL, DS_INTCN)) != RTC_OK)
        return rc;
    if ((rc = stat_write(d, 0, DS_A1F | DS_A2F)) != RTC_OK)    /* заодно выключить выход 32 кГц */
        return rc;
    return r[1] & DS_OSF ? RTC_E_LOST : RTC_OK;
}
Что было rtc_init Время после
первое включение RTC_E_LOST 2000-01-01 00:00:00
rtc_set 12:00:00, потом снова rtc_init RTC_OK —
питание снято на час, батарейка есть RTC_OK 13:00:01
то же, но EOSC = 1 RTC_E_LOST 13:00:02 — час простоял
питание снято, батарейки нет RTC_E_LOST 2000-01-01 00:00:00

EOSC = 1 останавливает генератор, когда чип переходит на батарейку: часы стоят, OSF встаёт. rtc_init пишет CTRL = 0x04 — EOSC = 0, INTCN = 1, будильники выключены — и снимает A1F/A2F. Выход 32 кГц после включения работает (так в Zephyr); rtc_init его выключает — бит 0x08 в STAT снят, а OSF остаётся до rtc_set.

Температура. Чип меряет её сам раз в 64 с — для подстройки генератора; результат — в 0x11 (целые со знаком) и битах 7:6 0x12 (четверти), шаг 0,25 °C, как в разборе Zephyr. Драйвер обходится без сдвига отрицательного числа:

/* Температура: 0x11 — целые градусы со знаком, в 0x12 биты 7:6 — четверти */
int rtc_temp(rtc_t *d, int16_t *q4)
{
    uint8_t r[2];
    int rc = rd(d, DS_TEMP_MSB, r, 2);
    if (rc == RTC_OK) {
        int16_t whole = (int16_t)(r[0] >= 128u ? (int16_t)r[0] - 256 : (int16_t)r[0]);
        *q4 = (int16_t)(whole * 4 + (r[1] >> 6));
    }
    return rc;
}
Температура в модели 0x11 0x12 rtc_temp, четверти °C
25,25 19 40 101 25,25
0 00 00 0 0,00
−0,25 FF C0 −1 −0,25
−10,75 F5 40 −43 −10,75
85 55 00 340 85,00
−40 D8 00 −160 −40,00

Регистр отстаёт от воздуха: температуру в модели подняли с 25 до 35 °C, и через 10 и 40 с чип ещё показывал 25,00 — новое значение пришло с очередным измерением (через 70 с — 35,00). CONV = 1 меряет сразу; второй запрос, пока BSY = 1, драйвер не шлёт — RTC_E_BUSY.

Aging offset — поправка частоты, число со знаком. По документации производителя (не сверено) плюс замедляет ход, минус ускоряет, шаг около 0,1 ppm, и действует она с ближайшего измерения температуры — поэтому rtc_aging_set сразу ставит CONV. Код — ошибка хода, делённая на шаг:

/* Шаг Aging offset — около 0,1 ppm (по документации производителя, не сверено): код = ошибка / 100 ppb.
 * Плюс замедляет ход, минус ускоряет. */
int8_t rtc_aging_code(int32_t err_ppb)
{
    int32_t c = err_ppb >= 0 ? (err_ppb + 50) / 100 : -((-err_ppb + 50) / 100);
    return (int8_t)(c > 127 ? 127 : c < -128 ? -128 : c);
}

Ошибка в миллиардных: 1370 → 14, −2550 → −26, 49 → 0, 51 → 1, 20 000 → 127 (предел). Опыт: чип в модели спешит на 1,37 ppm. За 30 суток он ушёл вперёд на 3,551 с (rtc_get показал +4 с — целые секунды). Код 14 оставил −0,03 ppm: −0,078 с за те же 30 суток, rtc_get — 0 с. Ошибку хода меряют так, как в 198.4: сравнивают с точными часами через неделю или дольше.


22л.7 Грабли

После первого включения часы показывают 2000-01-01, и программа этому верит. OSF не проверили. rtc_get возвращает годную по виду дату — rtc_init должен вернуть RTC_E_LOST, и время ставят заново.

Секунды скачут 9 → 16, минут бывает 89. BCD прочитали как двоичное число. Каждое поле — через bcd_dec, каждая запись — через bcd_enc.

Изредка время улетает на минуту, час, сутки — или на год. Время читают по байту: семь START, семь копий. На 400 кГц окно 725 мкс у каждого переноса; у года ошибка — ровно год вперёд (22л.3). Только 7 байт одной транзакцией.

После установки месяц на один больше. Писали по полю, от года к секундам, и перенос попал между записями: 4 из 66 записей дали июнь вместо мая, ещё 19 — неверные сутки, час или минуту. Время пишут одной транзакцией с 0x00: секунды первыми.

После полудня часы показывают 21 вместо 13. Чип в 12-часовом режиме, а разбор его не понимает. Модель проверила разбор двух драйверов Zephyr 4.4.2 на всех 24 часах: в counter бит десятка в 12-часовом режиме взят тем же 0x20, что и PM, — верно 11 из 24, 13 часов читаются как 23; в rtc часы маскируются GENMASK(5, 0), бит 12/24 теряется — верно 11 из 24, 13 часов — как 21. Драйвер этого раздела — 24 из 24, а пишет всегда 24-часовой режим.

Будильник сработал один раз и замолчал. A2F не сняли — INT лежит в нуле, нового спада нет: 1 пробуждение из 60 за час. К тому же при стоящем бите EXTI_PR Stop не наступает вовсе (RM0008, с. 74).

Иногда пропадает второй будильник. A1F снимали «прочитать — сбросить — записать», и в окне 115 мкс на 400 кГц ноль ложится и в A2F. Флаги, которые не снимают, пишут единицами.

rtc_init прошёл, а время — мусор. На той же шине MPU-6050 с AD0 = 0, и оба отвечают на 0x68. В модели MPU-6050 на этих номерах отдаёт нули, открытый сток складывает ответы по «И»: CTRL и STAT прочитались нулями (OSF = 0 — «всё хорошо»), а rtc_get вернул RTC_E_RANGE — месяц 0. С AD0 = 1 (адрес 0x69) — 2026-10-27 12:00:00. Запись времени при этом легла бы и в регистры MPU-6050 с теми же номерами. Нет ответа на адрес вовсе — RTC_E_NACK.

Часы стоят, пока прибор выключен. Кто-то поставил EOSC = 1: на батарейке генератор стоит. За час без питания — час отставания и OSF = 1. rtc_init пишет EOSC = 0.

Лишний ток через подтяжку 32K. После включения выход 32 кГц работает (так в Zephyr). Подтяжка 10 кОм к 3,3 В при скважности 2 — в среднем 165 мкА от основного питания. Не нужен — EN32kHz = 0, это делает rtc_init.

CONV не меряет, зато включился меандр на батарейке. В Zephyr 4.4.2 CONV в rtc_ds3231.h описан как бит 6 — это BBSQW; в maxim_ds3231.h того же Zephyr он — бит 5. Запись 0x40 в модели дала CTRL = 0x44, BBSQW = 1, BSY = 0: измерения нет.

На INT нет спадов вообще. INT/SQW — открытый сток. Без подтяжки (на модуле или входом с подтяжкой, как PB8) линия висит.


22л.8 Итоги

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

rtc22l.h — регистры, биты и маски DS3231, типы и прототипы драйвера.

/* rtc22l.h — раздел 22л: внешние часы DS3231 по I2C без регистров микроконтроллера.
 * Драйвер говорит с микросхемой через rtc_port_write / rtc_port_read: в прошивке fw22l.c их делает I2C1
 * STM32F103, в стенде qhost22l.c — строки по USART1 к модели model22l.py (QEMU).
 * Номера регистров 0x00…0x12, биты CTRL и STAT, маски будильников AxMy и DY/DT, бит века и разбор
 * температуры сверены с драйверами DS3231 из Zephyr 4.4.2; документа на DS3231 в проекте нет. */
#ifndef RTC22L_H
#define RTC22L_H
#include <stdint.h>

#define DS_ADDR        0x68u            /* 7 бит; выводов адреса у DS3231 нет */

/* ── регистры 0x00…0x12 ───────────────────────────────────────────────── */
enum {
    DS_SEC = 0x00, DS_MIN, DS_HOUR, DS_DAY, DS_DATE, DS_MONTH, DS_YEAR,   /* время: 7 байт BCD */
    DS_A1SEC = 0x07, DS_A1MIN, DS_A1HOUR, DS_A1DAY,                        /* будильник 1: до секунды */
    DS_A2MIN = 0x0B, DS_A2HOUR, DS_A2DAY,                                  /* будильник 2: без секунд */
    DS_CTRL = 0x0E, DS_STAT = 0x0F, DS_AGING = 0x10, DS_TEMP_MSB = 0x11, DS_TEMP_LSB = 0x12
};
#define DS_H12         0x40u            /* часы: 1 — 12-часовой режим */
#define DS_PM          0x20u            /* часы в 12-часовом: 1 — после полудня; в 24-часовом — десяток «20» */
#define DS_CENTURY     0x80u            /* месяц: бит века */
#define DS_AM          0x80u            /* AxMy — старший бит байта будильника: «поле не сравнивать» */
#define DS_DYDT        0x40u            /* день будильника: 1 — день недели 1…7, 0 — число месяца */
/* CTRL, 0x0E */
#define DS_EOSC        0x80u            /* 1 — на батарейке генератор стоит */
#define DS_BBSQW       0x40u            /* 1 — меандр и на батарейке */
#define DS_CONV        0x20u            /* 1 — измерить температуру и пересчитать подстройку сейчас */
#define DS_RS_SHIFT    3u               /* RS2 RS1: 00 — 1 Гц, 01 — 1024, 10 — 4096, 11 — 8192 Гц */
#define DS_INTCN       0x04u            /* 1 — INT/SQW = прерывание будильников, 0 — меандр */
#define DS_A2IE        0x02u
#define DS_A1IE        0x01u
/* STAT, 0x0F */
#define DS_OSF         0x80u            /* генератор стоял: время недостоверно */
#define DS_EN32K       0x08u            /* выход 32 кГц */
#define DS_BSY         0x04u            /* идёт измерение температуры */
#define DS_A2F         0x02u
#define DS_A1F         0x01u
#define DS_FLAGS       (DS_OSF | DS_A2F | DS_A1F)  /* снимаются записью нуля; запись единицы их не трогает */

enum { RTC_OK = 0, RTC_E_NACK = -1, RTC_E_BUS = -2, RTC_E_RANGE = -3, RTC_E_LOST = -4, RTC_E_MASK = -5, RTC_E_BUSY = -6 };

typedef struct {
    uint16_t year;                      /* 2000…2199 */
    uint8_t mon, date, hour, min, sec;  /* hour — всегда 0…23 */
    uint8_t wday;                       /* 1 — понедельник … 7 — воскресенье */
} rtc_tm_t;

typedef struct {
    uint8_t addr;
    uint32_t xfers, errors;             /* транзакций и ошибок шины */
} rtc_t;

/* маска будильника: бит 0 — секунды не сравнивать, 1 — минуты, 2 — часы, 3 — день.
 * Годны только 1111, 1110, 1100, 1000 и 0000 — как в Zephyr. У будильника 2 секунд нет: бит 0 = 0,
 * и он срабатывает в начале минуты (секунды 00): 1110 — каждую минуту, 1100 — по минутам и т. д. */
enum { RTC_AL_EVERY = 0x0F, RTC_AL_S = 0x0E, RTC_AL_MS = 0x0C, RTC_AL_HMS = 0x08, RTC_AL_DHMS = 0x00 };
typedef struct {
    uint8_t sec, min, hour, day;        /* day: число 1…31 или день недели 1…7 */
    uint8_t mask;
    uint8_t dy;                         /* 1 — day означает день недели */
} rtc_alarm_t;

/* ── порт: в прошивке — I2C1, в стенде — строки к модели ─────────────── */
int rtc_port_write(uint8_t addr7, const uint8_t *tx, uint32_t n);            /* START, адрес, n байт, STOP */
int rtc_port_read(uint8_t addr7, uint8_t reg, uint8_t *rx, uint32_t n);      /* номер, повторный START, n байт */

/* ── календарь и BCD ─────────────────────────────────────────────────── */
uint8_t bcd_enc(uint8_t v);
uint8_t bcd_dec(uint8_t b);
int rtc_leap(uint16_t y);
uint8_t rtc_mdays(uint16_t y, uint8_t m);
uint8_t rtc_wday(uint16_t y, uint8_t m, uint8_t d);
int rtc_valid(const rtc_tm_t *t);
void rtc_to_regs(const rtc_tm_t *t, uint8_t r[7]);
int rtc_from_regs(const uint8_t r[7], rtc_tm_t *t);

/* ── драйвер ─────────────────────────────────────────────────────────── */
int rtc_init(rtc_t *d, uint8_t addr7);  /* RTC_E_LOST — OSF стоит, время надо поставить заново */
int rtc_set(rtc_t *d, const rtc_tm_t *t);
int rtc_get(rtc_t *d, rtc_tm_t *t);
int rtc_alarm_set(rtc_t *d, uint8_t n, const rtc_alarm_t *a);
int rtc_alarm_ack(rtc_t *d, uint8_t *fired);
int rtc_temp(rtc_t *d, int16_t *q4);    /* температура в четвертях градуса */
int rtc_temp_start(rtc_t *d);
int8_t rtc_aging_code(int32_t err_ppb); /* спешит на err_ppb миллиардных — код Aging offset */
int rtc_aging_set(rtc_t *d, int8_t code);
int rtc_sqw(rtc_t *d, int rs);          /* rs 0…3 — меандр, rs < 0 — снова прерывание будильников */
int rtc_32k(rtc_t *d, int on);

#endif

rtc22l.c — драйвер DS3231: BCD и календарь, время пачкой, будильники, флаги, температура и Aging offset — общий для прошивки и стенда.

/* rtc22l.c — раздел 22л: драйвер DS3231 — время пачкой, будильники, флаги, температура, Aging offset.
 * Один и тот же файл собирается в прошивку (armclang, Cortex-M3) и в стенд (arm-none-eabi-gcc, QEMU):
 * микроконтроллера он не знает, только порт rtc_port_*. */
#include "rtc22l.h"

static int wr(rtc_t *d, const uint8_t *tx, uint32_t n)
{
    int rc = rtc_port_write(d->addr, tx, n);
    d->xfers++;
    if (rc != RTC_OK)
        d->errors++;
    return rc;
}

static int rd(rtc_t *d, uint8_t reg, uint8_t *rx, uint32_t n)
{
    int rc = rtc_port_read(d->addr, reg, rx, n);
    d->xfers++;
    if (rc != RTC_OK)
        d->errors++;
    return rc;
}

static int wr1(rtc_t *d, uint8_t reg, uint8_t v)
{
    uint8_t tx[2] = {reg, v};
    return wr(d, tx, 2);
}

/* ── BCD и календарь ──────────────────────────────────────────────────── */
uint8_t bcd_enc(uint8_t v)
{
    return (uint8_t)((v / 10u) << 4 | v % 10u);
}

uint8_t bcd_dec(uint8_t b)
{
    return (uint8_t)((b >> 4) * 10u + (b & 0x0Fu));
}

static int bcd_ok(uint8_t b)
{
    return (b & 0x0Fu) <= 9u && (b >> 4) <= 9u;
}

int rtc_leap(uint16_t y)
{
    return (y % 4u == 0u && y % 100u != 0u) || y % 400u == 0u;
}

uint8_t rtc_mdays(uint16_t y, uint8_t m)
{
    static const uint8_t md[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    return (uint8_t)(m == 2u && rtc_leap(y) ? 29u : md[m - 1u]);
}

/* День недели по Сакамото: 0 — воскресенье; наружу 1 — понедельник … 7 — воскресенье */
uint8_t rtc_wday(uint16_t y, uint8_t m, uint8_t d)
{
    static const uint8_t t[12] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
    uint32_t yy = y - (m < 3u);
    uint8_t w = (uint8_t)((yy + yy / 4u - yy / 100u + yy / 400u + t[m - 1u] + d) % 7u);
    return w == 0u ? 7u : w;
}

int rtc_valid(const rtc_tm_t *t)
{
    return t->year >= 2000u && t->year <= 2199u && t->mon >= 1u && t->mon <= 12u && t->date >= 1u
        && t->date <= rtc_mdays(t->year, t->mon) && t->hour <= 23u && t->min <= 59u && t->sec <= 59u;
}

/* Время → 7 байт с 0x00: всегда 24-часовой режим, день недели считаем сами, век — битом месяца */
void rtc_to_regs(const rtc_tm_t *t, uint8_t r[7])
{
    r[0] = bcd_enc(t->sec);
    r[1] = bcd_enc(t->min);
    r[2] = bcd_enc(t->hour);
    r[3] = rtc_wday(t->year, t->mon, t->date);
    r[4] = bcd_enc(t->date);
    r[5] = (uint8_t)(bcd_enc(t->mon) | (t->year >= 2100u ? DS_CENTURY : 0u));
    r[6] = bcd_enc((uint8_t)(t->year % 100u));
}

/* 7 байт → время. Понимает и 12-часовой режим, если его поставил кто-то другой: 12 AM — это 0 часов. */
int rtc_from_regs(const uint8_t r[7], rtc_tm_t *t)
{
    uint8_t h = r[2];
    if (!bcd_ok(r[0] & 0x7Fu) || !bcd_ok(r[1] & 0x7Fu) || !bcd_ok(r[4] & 0x3Fu) || !bcd_ok(r[5] & 0x1Fu)
        || !bcd_ok(r[6]) || !bcd_ok(h & (h & DS_H12 ? 0x1Fu : 0x3Fu)))
        return RTC_E_RANGE;
    t->sec = bcd_dec(r[0] & 0x7Fu);
    t->min = bcd_dec(r[1] & 0x7Fu);
    if (h & DS_H12) {
        uint8_t h12 = bcd_dec(h & 0x1Fu);               /* 1…12 */
        if (h12 < 1u || h12 > 12u)
            return RTC_E_RANGE;
        t->hour = (uint8_t)(h12 % 12u + (h & DS_PM ? 12u : 0u));
    } else {
        t->hour = bcd_dec(h & 0x3Fu);
    }
    t->wday = r[3] & 7u;
    t->date = bcd_dec(r[4] & 0x3Fu);
    t->mon = bcd_dec(r[5] & 0x1Fu);
    t->year = (uint16_t)(2000u + bcd_dec(r[6]) + (r[5] & DS_CENTURY ? 100u : 0u));
    return rtc_valid(t) && t->wday >= 1u ? RTC_OK : RTC_E_RANGE;
}

/* ── драйвер ──────────────────────────────────────────────────────────── */
/* STAT пишется целиком: флаги, которые снимать не надо, пишем единицами — запись единицы их не трогает,
 * и флаг, вставший между чтением и записью, не потеряется (так делает Zephyr). */
static int stat_write(rtc_t *d, uint8_t en32k, uint8_t clear)
{
    return wr1(d, DS_STAT, (uint8_t)(en32k | (DS_FLAGS & (uint8_t)~clear)));
}

/* Запуск: генератор на батарейке не останавливать (EOSC = 0), INT/SQW — прерывание будильников, оба
 * выключены; A1F и A2F снять — иначе INT с прошлого раза держится в нуле и спада больше не будет. */
int rtc_init(rtc_t *d, uint8_t addr7)
{
    uint8_t r[2];
    int rc;
    d->addr = addr7;
    d->xfers = d->errors = 0;
    if ((rc = rd(d, DS_CTRL, r, 2)) != RTC_OK)          /* CTRL и STAT одной транзакцией */
        return rc;
    if (r[0] != DS_INTCN && (rc = wr1(d, DS_CTRL, DS_INTCN)) != RTC_OK)
        return rc;
    if ((rc = stat_write(d, 0, DS_A1F | DS_A2F)) != RTC_OK)    /* заодно выключить выход 32 кГц */
        return rc;
    return r[1] & DS_OSF ? RTC_E_LOST : RTC_OK;
}

/* Время — одной транзакцией с 0x00: первым ложится байт секунд, и отсчёт секунды начинается заново;
 * переноса посреди записи не будет. Потом снять OSF: время теперь верное. */
int rtc_set(rtc_t *d, const rtc_tm_t *t)
{
    uint8_t tx[8], st;
    int rc;
    if (!rtc_valid(t))
        return RTC_E_RANGE;
    tx[0] = DS_SEC;
    rtc_to_regs(t, tx + 1);
    if ((rc = wr(d, tx, 8)) != RTC_OK)
        return rc;
    if ((rc = rd(d, DS_STAT, &st, 1)) != RTC_OK)
        return rc;
    return stat_write(d, st & DS_EN32K, DS_OSF);
}

/* Все 7 байт — одной транзакцией: микросхема копирует время в буфер на START, и пока байты идут по шине,
 * перенос их не разорвёт. */
int rtc_get(rtc_t *d, rtc_tm_t *t)
{
    uint8_t r[7];
    int rc = rd(d, DS_SEC, r, 7);
    if (rc != RTC_OK)
        return rc;
    return rtc_from_regs(r, t);
}

/* Будильник n (1 или 2): байты — одной транзакцией, старый флаг снять, потом разрешить AxIE */
int rtc_alarm_set(rtc_t *d, uint8_t n, const rtc_alarm_t *a)
{
    uint8_t tx[5], m = a->mask, x = (uint8_t)(~a->mask & 0x0Fu), k = 0, ctrl, st;
    uint8_t ie = n == 1u ? DS_A1IE : DS_A2IE, fl = n == 1u ? DS_A1F : DS_A2F;
    int rc;
    if (n < 1u || n > 2u || (m & 0xF0u) || ((x + 1u) & x) || (n == 2u && (m & 1u)))
        return RTC_E_MASK;                              /* годны 1111, 1110, 1100, 1000, 0000 */
    if ((!(m & 1u) && n == 1u && a->sec > 59u) || (!(m & 2u) && a->min > 59u) || (!(m & 4u) && a->hour > 23u)
        || (!(m & 8u) && (a->dy ? a->day < 1u || a->day > 7u : a->day < 1u || a->day > 31u)))
        return RTC_E_RANGE;
    tx[k++] = n == 1u ? DS_A1SEC : DS_A2MIN;
    if (n == 1u)
        tx[k++] = (uint8_t)(m & 1u ? DS_AM : bcd_enc(a->sec));
    tx[k++] = (uint8_t)(m & 2u ? DS_AM : bcd_enc(a->min));
    tx[k++] = (uint8_t)(m & 4u ? DS_AM : bcd_enc(a->hour));
    tx[k++] = (uint8_t)(m & 8u ? DS_AM : a->dy ? DS_DYDT | a->day : bcd_enc(a->day));
    if ((rc = wr(d, tx, k)) != RTC_OK)
        return rc;
    if ((rc = rd(d, DS_CTRL, tx, 2)) != RTC_OK)
        return rc;
    ctrl = tx[0];
    st = tx[1];
    if ((rc = stat_write(d, st & DS_EN32K, fl)) != RTC_OK)
        return rc;
    return wr1(d, DS_CTRL, (uint8_t)((ctrl & (uint8_t)~DS_CONV) | DS_INTCN | ie));
}

/* Какие будильники сработали; снять только их флаги — INT отпустится и следующий спад придёт */
int rtc_alarm_ack(rtc_t *d, uint8_t *fired)
{
    uint8_t st;
    int rc = rd(d, DS_STAT, &st, 1);
    *fired = 0;
    if (rc != RTC_OK)
        return rc;
    *fired = st & (DS_A1F | DS_A2F);
    if (!*fired)
        return RTC_OK;
    return stat_write(d, st & DS_EN32K, *fired);
}

/* Температура: 0x11 — целые градусы со знаком, в 0x12 биты 7:6 — четверти */
int rtc_temp(rtc_t *d, int16_t *q4)
{
    uint8_t r[2];
    int rc = rd(d, DS_TEMP_MSB, r, 2);
    if (rc == RTC_OK) {
        int16_t whole = (int16_t)(r[0] >= 128u ? (int16_t)r[0] - 256 : (int16_t)r[0]);
        *q4 = (int16_t)(whole * 4 + (r[1] >> 6));
    }
    return rc;
}

/* Измерить температуру и пересчитать подстройку сейчас, не дожидаясь своего цикла; занято — не трогать */
int rtc_temp_start(rtc_t *d)
{
    uint8_t r[2];
    int rc = rd(d, DS_CTRL, r, 2);
    if (rc != RTC_OK)
        return rc;
    if ((r[0] & DS_CONV) || (r[1] & DS_BSY))
        return RTC_E_BUSY;
    return wr1(d, DS_CTRL, (uint8_t)(r[0] | DS_CONV));
}

/* Шаг Aging offset — около 0,1 ppm (по документации производителя, не сверено): код = ошибка / 100 ppb.
 * Плюс замедляет ход, минус ускоряет. */
int8_t rtc_aging_code(int32_t err_ppb)
{
    int32_t c = err_ppb >= 0 ? (err_ppb + 50) / 100 : -((-err_ppb + 50) / 100);
    return (int8_t)(c > 127 ? 127 : c < -128 ? -128 : c);
}

int rtc_aging_set(rtc_t *d, int8_t code)
{
    int rc = wr1(d, DS_AGING, (uint8_t)code);
    return rc != RTC_OK ? rc : rtc_temp_start(d);       /* иначе новая поправка — со следующего измерения */
}

int rtc_sqw(rtc_t *d, int rs)
{
    uint8_t c;
    int rc = rd(d, DS_CTRL, &c, 1);
    if (rc != RTC_OK)
        return rc;
    c &= (uint8_t)~(DS_CONV | DS_INTCN | 3u << DS_RS_SHIFT);
    c |= rs < 0 ? DS_INTCN : (uint8_t)((unsigned)rs << DS_RS_SHIFT & 0x18u);
    return wr1(d, DS_CTRL, c);
}

int rtc_32k(rtc_t *d, int on)
{
    return stat_write(d, on ? DS_EN32K : 0u, 0);
}

fw22l.c — прошивка STM32F103: I2C1, INT/SQW на EXTI8, будильник каждую минуту и сон в Stop.

/* fw22l.c — раздел 22л: DS3231 на STM32F103 (BluePill), 72 МГц, APB1 = 36 МГц.
 * I2C1: PB6 — SCL, PB7 — SDA (открытый сток, 400 кГц). Адрес DS3231 — 0x68.
 * INT/SQW → PB8: вход с подтяжкой, EXTI8 по спаду; у DS3231 этот выход — открытый сток.
 * Будильник 2 — каждую минуту; между срабатываниями микроконтроллер спит в Stop, будит его EXTI8.
 * Светодиод PC13 мигает на каждом пробуждении. Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "rtc22l.h"

#define CPU_HZ          72000000u
#define APB1_MHZ        36u
#define I2C_TIMEOUT_US  1000u           /* самая длинная транзакция — чтение 7 байт, 93 такта SCL, 232,5 мкс */

static rtc_t rtc;
static rtc_tm_t now;
static volatile uint8_t irq_flag;
volatile uint32_t wakeups, bus_errors;

static const rtc_tm_t first_time = {2026, 10, 27, 12, 0, 0, 0};    /* в приборе — с консоли или от GPS */

static int expired(uint32_t t0, uint32_t us)
{
    return DWT->CYCCNT - t0 >= us * (CPU_HZ / 1000000u);
}

/* После выхода из Stop система на HSI 8 МГц (RM0008, с. 74): HSE и PLL — заново */
static void clock_72mhz(void)
{
    if ((RCC->CFGR & RCC_CFGR_SWS) == RCC_CFGR_SWS_PLL)
        return;                                         /* Stop не наступил — такт и так от PLL */
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY)) {
    }
    FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2;            /* 72 МГц — два такта ожидания Flash */
    RCC->CFGR = RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9 | RCC_CFGR_PPRE1_DIV2;
    RCC->CR |= RCC_CR_PLLON;
    while (!(RCC->CR & RCC_CR_PLLRDY)) {
    }
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
    }
}

/* INT/SQW: спад на PB8 — только флаг, обмен по I2C идёт в основном цикле */
void EXTI9_5_IRQHandler(void)
{
    EXTI->PR = 1u << 8;
    irq_flag = 1;
}

static void pins_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN | RCC_APB1ENR_PWREN;
    /* PB6, PB7 — альтернативная функция, открытый сток, 50 МГц (1111) */
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xFF000000u;
    /* PB8 — вход с подтяжкой (1000): INT/SQW — открытый сток */
    GPIOB->BSRR = 1u << 8;
    GPIOB->CRH = (GPIOB->CRH & ~0xFu) | 0x8u;
    /* PC13 — выход 2 МГц (0010), светодиод горит нулём */
    GPIOC->BSRR = 1u << 13;
    GPIOC->CRH = (GPIOC->CRH & ~0x00F00000u) | 0x00200000u;
}

static void i2c1_init(void)
{
    I2C1->CR1 = 0;                                      /* настраивать при PE = 0 */
    I2C1->CR2 = APB1_MHZ;
    I2C1->CCR = I2C_CCR_FS | 30u;                       /* DUTY = 0: 36 МГц / (3 × 30) = 400 кГц */
    I2C1->TRISE = APB1_MHZ * 300u / 1000u + 1u;         /* 300 нс × 36 МГц = 10,8 → 10 + 1 = 11 */
    I2C1->CR1 = I2C_CR1_PE;
}

static void exti_init(void)
{
    AFIO->EXTICR[2] = (AFIO->EXTICR[2] & ~0xFu) | 0x1u;    /* EXTI8 ← PB8 */
    EXTI->FTSR |= 1u << 8;
    EXTI->IMR |= 1u << 8;
    NVIC_EnableIRQ(EXTI9_5_IRQn);
}

static int sr1_wait(uint16_t mask)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!(I2C1->SR1 & mask)) {
        if (I2C1->SR1 & I2C_SR1_AF)
            return RTC_E_NACK;                          /* адрес или байт не подтверждён */
        if (expired(t0, I2C_TIMEOUT_US))
            return RTC_E_BUS;
    }
    return RTC_OK;
}

static int fail(int rc)
{
    I2C1->CR1 |= I2C_CR1_STOP;
    I2C1->CR1 &= (uint16_t)~(I2C_CR1_POS | I2C_CR1_ACK);
    I2C1->SR1 = (uint16_t)~I2C_SR1_AF;                  /* AF снимается записью нуля */
    return rc;
}

/* START, адрес на запись и номер регистра (или все байты записи) */
static int start_write(uint8_t addr7, const uint8_t *tx, uint32_t n)
{
    int rc;
    uint32_t t0 = DWT->CYCCNT;
    while (I2C1->SR2 & I2C_SR2_BUSY) {                  /* SDA держит ведомый — раздел 17.6 */
        if (expired(t0, I2C_TIMEOUT_US))
            return RTC_E_BUS;
    }
    I2C1->CR1 |= I2C_CR1_START;
    if ((rc = sr1_wait(I2C_SR1_SB)) != RTC_OK)
        return rc;
    I2C1->DR = (uint8_t)(addr7 << 1);
    if ((rc = sr1_wait(I2C_SR1_ADDR)) != RTC_OK)
        return rc;
    (void)I2C1->SR2;
    for (uint32_t i = 0; i < n; i++) {
        if ((rc = sr1_wait(I2C_SR1_TXE)) != RTC_OK)
            return rc;
        I2C1->DR = tx[i];
    }
    return sr1_wait(I2C_SR1_BTF);
}

int rtc_port_write(uint8_t addr7, const uint8_t *tx, uint32_t n)
{
    int rc = start_write(addr7, tx, n);
    if (rc != RTC_OK)
        return fail(rc);
    I2C1->CR1 |= I2C_CR1_STOP;
    return RTC_OK;
}

/* Приём ведущим по RM0008, с. 763–764: один байт, два байта и больше двух — три разные процедуры */
int rtc_port_read(uint8_t addr7, uint8_t reg, uint8_t *rx, uint32_t n)
{
    int rc;
    if (n == 0u)
        return RTC_E_BUS;
    if ((rc = start_write(addr7, &reg, 1)) != RTC_OK)
        return fail(rc);
    I2C1->CR1 |= I2C_CR1_START;                         /* повторный старт: DS3231 снова копирует время */
    if ((rc = sr1_wait(I2C_SR1_SB)) != RTC_OK)
        return fail(rc);
    I2C1->CR1 |= n == 2u ? (I2C_CR1_POS | I2C_CR1_ACK) : I2C_CR1_ACK;
    I2C1->DR = (uint8_t)(addr7 << 1 | 1u);
    if ((rc = sr1_wait(I2C_SR1_ADDR)) != RTC_OK)
        return fail(rc);
    if (n == 1u) {
        I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;            /* ACK снять до сброса ADDR */
        (void)I2C1->SR2;
        I2C1->CR1 |= I2C_CR1_STOP;
        if ((rc = sr1_wait(I2C_SR1_RXNE)) != RTC_OK)
            return fail(rc);
        rx[0] = (uint8_t)I2C1->DR;
        return RTC_OK;
    }
    (void)I2C1->SR2;
    if (n == 2u) {
        I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
        if ((rc = sr1_wait(I2C_SR1_BTF)) != RTC_OK)
            return fail(rc);
        I2C1->CR1 |= I2C_CR1_STOP;
        rx[0] = (uint8_t)I2C1->DR;
        rx[1] = (uint8_t)I2C1->DR;
        I2C1->CR1 &= (uint16_t)~I2C_CR1_POS;
        return RTC_OK;
    }
    for (uint32_t i = 0; i + 3u < n; i++) {             /* все, кроме трёх последних */
        if ((rc = sr1_wait(I2C_SR1_RXNE)) != RTC_OK)
            return fail(rc);
        rx[i] = (uint8_t)I2C1->DR;
    }
    if ((rc = sr1_wait(I2C_SR1_BTF)) != RTC_OK)         /* N−2 в DR, N−1 в сдвиговом регистре */
        return fail(rc);
    I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
    rx[n - 3u] = (uint8_t)I2C1->DR;
    if ((rc = sr1_wait(I2C_SR1_BTF)) != RTC_OK)
        return fail(rc);
    I2C1->CR1 |= I2C_CR1_STOP;
    rx[n - 2u] = (uint8_t)I2C1->DR;
    if ((rc = sr1_wait(I2C_SR1_RXNE)) != RTC_OK)
        return fail(rc);
    rx[n - 1u] = (uint8_t)I2C1->DR;
    return RTC_OK;
}

/* Stop: регулятор в экономичный режим (LPDS), PDDS = 0, SLEEPDEEP (RM0008, с. 73–74).
 * Если бит EXTI_PR уже стоит, Stop не наступит — программа просто пойдёт дальше (с. 74). */
static void enter_stop(void)
{
    PWR->CR = (PWR->CR & ~PWR_CR_PDDS) | PWR_CR_LPDS;
    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
    __WFI();
    SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk;
}

int main(void)
{
    static const rtc_alarm_t every_minute = {0, 0, 0, 1, RTC_AL_S, 0};   /* будильник 2: маска 1110 */
    uint8_t fired;
    int rc;
    clock_72mhz();
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    pins_init();
    i2c1_init();
    rc = rtc_init(&rtc, DS_ADDR);
    if (rc == RTC_E_LOST)                               /* OSF: генератор стоял — время в регистрах не время */
        rc = rtc_set(&rtc, &first_time);
    while (rc != RTC_OK || rtc_alarm_set(&rtc, 2, &every_minute) != RTC_OK) {
        uint32_t t0 = DWT->CYCCNT;                      /* нет ответа: провод, подтяжки, адрес, питание */
        while (!expired(t0, 100000u)) {
        }
        rc = rtc_init(&rtc, DS_ADDR);
        if (rc == RTC_E_LOST)
            rc = rtc_set(&rtc, &first_time);
    }
    exti_init();
    for (;;) {
        if (!irq_flag && (GPIOB->IDR & (1u << 8)))      /* INT отпущен и спада не было — спать */
            enter_stop();
        clock_72mhz();
        irq_flag = 0;
        wakeups++;
        if (rtc_alarm_ack(&rtc, &fired) != RTC_OK || rtc_get(&rtc, &now) != RTC_OK)
            bus_errors++;
        GPIOC->ODR ^= 1u << 13;
    }
}

Дальше: раздел 43 «Аппаратная криптография: CRC, RNG, HASH, CRYP».

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