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 вывод батарейки
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 Итоги
- Внешний DS3231 — когда важен ход в мороз и жару: кварц по 174.4 при 0 °C уходит на −1,89 с в сутки, при −20 °C — на −6,12 с; DS3231 обещает ±2 ppm, ±0,173 с в сутки (не сверено).
- 19 регистров 0x00…0x12: время в BCD (
58 59 23 02 27 10 26для 2026-10-27 23:59:58), бит 12/24, бит века (2100-03-01 — месяц 0x83), два будильника,CTRL,STAT, Aging offset, температура. - Время — 7 байт одной транзакцией: 232,5 мкс на 400 кГц, 0 ошибок из 129 у переноса года; по байту — 29 из 129, до года вперёд. Установка — с 0x00 одной транзакцией: 0 из 66 против 23 из 66 по полю.
- Будильники: маски 1111, 1110, 1100, 1000, 0000 и DY/DT — 1440 в сутки, 24, 3 за 3 суток, 7 за год на 31-е, 52 понедельника; 12-часовой режим драйвер читает 24 из 24.
- INT/SQW — открытый сток на EXTI8: флаг не сняли — 1 пробуждение из 60;
снимать только свои флаги, остальные писать единицами — иначе 23 из 121
попыток теряют
A2F. OSFпосле первого включения: время 2000-01-01,rtc_init→RTC_E_LOST;EOSC= 1 — час простоя на батарейке.- Температура — шаг 0,25 °C, обновляется раз в 64 с; Aging offset — около 0,1 ppm на шаг: 1,37 ppm (3,551 с за 30 суток) код 14 сводит к −0,03 ppm (−0,078 с).
- MPU-6050 с AD0 = 0 — тот же адрес 0x68:
rtc_initпроходит, время — мусор; AD0 = 1.
Полные исходники
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, ®, 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».