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

15б. Внешний АЦП ADS1115 по I2C: 16 бит, усилитель PGA, мультиплексор, режимы преобразования и вывод ALERT/RDY

В разделе 14 — встроенный АЦП F1 и F4: 12 бит, последовательное приближение, опора — питание. В 15а у G4 появились дифференциальный вход и 16 бит передискретизацией, но у F103 их нет. Когда на F103 нужно измерить шунт, мост или термопару — малую разность, со знаком, точнее 12 бит, — ставят внешний АЦП. ADS1115 — самый ходовой: 16 бит, четыре входа, усилитель с выбором диапазона, I2C и вывод «готово». Здесь — чем он отличается от встроенного, как подключить к F103, какие регистры нужны, как ждать результат, как перевести код в микровольты, сколько времени уходит на канал, как работают RDY и компаратор и что делать с шумом и сетевой помехой.

Опоры: RM0008 (DocID13902 Rev 17): таблица режимов выводов, с. 160; AFIO_EXTICR2, с. 190; EXTI_FTSR, с. 211; приём двух байт ведущим, с. 764; I2C_CCR, с. 783–784; I2C_TRISE, с. 784. DocID13587 Rev 17 (STM32F103x8/xB): PB6 — I2C1_SCL, PB7 — I2C1_SDA, оба FT, PB5 без FT, с. 32. Документа на ADS1115 в проекте нет: регистры, биты Config, коды и диапазоны, скорости, адреса, значения после включения, поведение OS и ALERT/RDY взяты из документации производителя и заложены в модель. Поэтому всё ниже проверяет согласие драйвера с моделью, а не с микросхемой. Все числа — прогон драйвера _verify/ads15c/ads15c.c на компьютере (gcc 12.2.0) со своей моделью ADS1115 как ведомого I2C на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3.

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

Оригинал Перевод
delta-sigma ADC дельта-сигма АЦП: много грубых измерений и цифровой фильтр
programmable gain amplifier (PGA) усилитель с программным коэффициентом — здесь выбор диапазона
full-scale range (FSR) диапазон: от −FS до +FS
input multiplexer (MUX) мультиплексор входов
single-ended / differential input вход против земли / разность двух входов
single-shot / continuous-conversion mode однократное / непрерывное преобразование
operational status (OS) бит «начать» при записи и «идёт преобразование» при чтении
data rate (DR), SPS скорость: преобразований в секунду
address pointer register регистр указателя: с каким регистром обмен
conversion ready (RDY) результат готов
comparator queue (COMP_QUE) сколько преобразований подряд до срабатывания
window / latching comparator компаратор-окно / с защёлкой
two's complement дополнительный код

15б.1 Чем он отличается от встроенного АЦП

Встроенный АЦП F1 — последовательное приближение: короткая выборка на конденсатор (180.1) и 12 шагов подбора кода, при 12 МГц — 1,17 мкс на измерение (14.2). ADS1115 по документации производителя устроен иначе: дельта-сигма модулятор всё время преобразования грубо измеряет вход, а цифровой фильтр сводит эти измерения в 16-битный код. Отсюда всё остальное:

Встроенный АЦП F1 ADS1115
разрядность 12 бит, только плюс 16 бит со знаком
принцип последовательное приближение дельта-сигма с фильтром
время на результат 1,17 мкс при 12 МГц 1/DR: от 125 мс до 1162,8 мкс
вход от 0 до VREF+ разность двух входов или вход против GND
диапазон 0…VREF+, задаёт питание от ±6,144 до ±0,256 В, задаёт PGA
связь шина микроконтроллера, DMA I2C, два байта на результат

Встроенный быстрее в тысячу раз и годится для осциллограммы. Внешний медленный, зато сам усредняет вход за время преобразования (в модели фильтр — ровно такое усреднение), меряет разность со знаком и на диапазоне ±0,256 В даёт единицу кода 7,8125 мкВ — для шунта и моста то, что нужно. Время выборки у него не настраивается: за него отвечает скорость DR.

Код — дополнительный, 32768 единиц на половину диапазона. Вход против GND пользуется только положительной половиной — это 15 бит, а не 16.

Вверху — подключение ADS1115 к STM32F103: PB6 — SCL, PB7 — SDA на I2C1 на 400 кГц, PB5 — ALERT/RDY на EXTI5 по спаду, вывод ADDR на GND задаёт адрес 0x48, входы AIN0–AIN3 не выше питания 3,3 В, SCL и SDA — открытый сток с подтяжкой. Посередине — что внутри: мультиплексор на 8 сочетаний входов, усилитель PGA от ±6,144 до ±0,256 В, дельта-сигма АЦП на 16 бит со скоростью 8–860 SPS, регистр Conversion и компаратор с порогами Lo и Hi, который управляет ALERT/RDY; единица кода при ±2,048 В — 62,5 мкВ. Внизу — поля регистра Config с номерами бит: OS, MUX, PGA, MODE, DR, COMP_MODE, COMP_POL, COMP_LAT, COMP_QUE и значение после включения 0x8583; однократное преобразование на 860 SPS и 400 кГц в модели занимает 1450,5 мкс на канал


15б.2 Подключение к F103

Вывод ADS1115 STM32F103 Назначение
SCL PB6 такт I2C1
SDA PB7 данные I2C1
ALERT/RDY PB5 «готово» или порог; EXTI5 по спаду
ADDR GND адрес 0x48
VDD, GND 3,3 В и земля общая земля с микроконтроллером

PB6 и PB7 — выводы I2C1, оба FT, то есть терпят 5 В (DocID13587, с. 32): модуль, запитанный от 5 В, со своими подтяжками к 5 В их не сожжёт. PB5 — без FT. ALERT/RDY по документации производителя — открытый сток; подтяжку даёт сам PB5, вход с подтяжкой (код 1000 в таблице режимов, с. 160), и тянуть его к 5 В нельзя. Проще всего питать ADS1115 от 3,3 В — тогда и входы не выйдут за питание (15б.7).

Адрес задаёт вывод ADDR (по документации производителя): на GND — 0x48, на VDD — 0x49, на SDA — 0x4A, на SCL — 0x4B. Так на одну шину садятся четыре микросхемы, 16 входов. В модели при каждом из четырёх подключений ads_probe нашла один адрес, а остальные три ответили отказом — ADS_E_NACK. В линии адрес 0x48 — байты 0x90 и 0x91 (17.2).

I2C1 — как в 17.3, но на 400 кГц, ADS1115 по документации их держит. В быстром режиме при DUTY = 0 низкий уровень SCL — 2 × CCR, высокий — CCR тактов APB1 (с. 784): 36 МГц / (3 × 30) = 400 кГц. TRISE — предельный фронт плюс один: 300 нс × 36 МГц = 10,8, а если не целое, берут целую часть (с. 784) — 11:

static void i2c1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    /* PB6, PB7 — альтернативная функция, открытый сток, 50 МГц (1111); PB5 — вход с подтяжкой (1000) */
    GPIOB->CRL = (GPIOB->CRL & 0x000FFFFFu) | 0xFF800000u;
    GPIOB->BSRR = 1u << 5;
    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;
}

RM0008 (с. 783) оговаривает: чтобы получить ровно 400 кГц, частота APB1 должна быть кратна 10 МГц, и настоящая частота может отличаться из-за фильтра на входе. В модели шина — ровно 400 кГц. ALERT/RDY идёт на EXTI5: AFIO_EXTICR2, биты 7:4 = 0001 — порт B (с. 190), спад включает EXTI_FTSR (с. 211).


15б.3 Регистры: указатель, Conversion, Config, пороги

Регистров четыре, по 16 бит, и ещё регистр указателя. Первый байт любой записи попадает в указатель и выбирает регистр: 0 — Conversion (результат, только чтение), 1 — Config, 2 — Lo_thresh, 3 — Hi_thresh. Запись трёх байт — указатель, старший и младший байт значения. Чтение отдаёт два байта регистра под указателем, старшим вперёд, и указатель не меняет.

Первый обмен в модели после включения — ads_probe, чтение Config: байты 90 01, повторный старт, 91 — и в ответ 85 83. После включения Config = 0x8583, Lo_thresh = 0x8000, Hi_thresh = 0x7FFF, Conversion = 0x0000.

Биты Поле Что задаёт После включения
15 OS запись 1 — начать однократное; чтение 0 — идёт преобразование 1
14:12 MUX 000 — AIN0 − AIN1, 001 — AIN0 − AIN3, 010 — AIN1 − AIN3, 011 — AIN2 − AIN3, 100…111 — AIN0…AIN3 против GND 000
11:9 PGA 000 — ±6,144 В, 001 — ±4,096, 010 — ±2,048, 011 — ±1,024, 100 — ±0,512, 101…111 — ±0,256 010
8 MODE 1 — однократное, 0 — непрерывное 1
7:5 DR 000…111 — 8, 16, 32, 64, 128, 250, 475, 860 SPS 100
4 COMP_MODE 0 — компаратор с гистерезисом, 1 — окно 0
3 COMP_POL 0 — ALERT активен нулём 0
2 COMP_LAT 1 — держать ALERT до чтения результата 0
1:0 COMP_QUE 00, 01, 10 — сработать после 1, 2, 4 преобразований; 11 — выключен 11

Чтение регистра в драйвере помнит, куда смотрит указатель: если туда же, лишний байт не нужен.

int ads_read(ads_t *d, uint8_t reg, uint16_t *v)
{
    uint8_t b[2];
    int rc = d->ptr == reg ? xfer(d, 0, 0, b, 2)        /* указатель уже на месте: только чтение */
                           : xfer(d, &reg, 1, b, 2);    /* иначе запись указателя и повторный старт */
    if (rc == ADS_OK) {
        d->ptr = reg;
        *v = (uint16_t)(b[0] << 8 | b[1]);
    }
    return rc;
}

Обмен с записью указателя и повторным стартом — 48 тактов SCL, 120 мкс на 400 кГц; чтение без указателя — 29 тактов, 72,5 мкс; запись Config — 38 тактов, 95 мкс. Драйвер для «однократно, AIN0, ±4,096 В, 128 SPS» записал в Config 0xC383 — модель собрала то же число по полям таблицы независимо; совпали все четыре проверенных сочетания.


15б.4 Однократное и непрерывное, четыре канала по очереди

Однократное (MODE = 1): запись Config с OS = 1 запускает одно преобразование, после него микросхема засыпает. Конец узнают по OS (чтение 1 — готово) или по выводу RDY (15б.6). Опрашивать OS сразу бессмысленно: результат будет не раньше чем через 1/DR. Драйвер сначала ждёт это время, потом опрашивает раз в 100 мкс:

/* однократное: OS = 1 запускает; сначала ждём расчётное время, потом опрашиваем OS */
int ads_single(ads_t *d, uint32_t mux, uint32_t pga, uint32_t dr, int16_t *code)
{
    uint16_t c;
    uint32_t t = ads_conv_us(dr);
    int rc = ads_write(d, ADS_CONFIG, ads_cfg(mux, pga, dr, 1) | ADS_QUE_OFF);
    if (rc)
        return rc;
    ads_port_delay_us(t);
    for (uint32_t waited = t;; waited += ADS_POLL_US) {
        if ((rc = ads_read(d, ADS_CONFIG, &c)) != ADS_OK)
            return rc;
        d->polls++;
        if (c & ADS_OS)                                 /* 1 — преобразование закончено */
            break;
        if (waited > 2u * t + 10000u)
            return ADS_E_TIMEOUT;
        ads_port_delay_us(ADS_POLL_US);
    }
    return ads_read_conv(d, code);
}

В модели на 400 кГц, вход 1 В, ±2,048 В — код 16000 при любой скорости:

DR 1/DR OS после паузы обменов OS без паузы, опросов по RDY обменов
8 SPS 125 000 мкс 125 287,5 мкс 3 1726 125 212,5 мкс 2
128 SPS 7812,5 мкс 8100,5 мкс 3 109 8025 мкс 2
250 SPS 4000 мкс 4287,5 мкс 3 57 4212,5 мкс 2
860 SPS 1162,8 мкс 1450,5 мкс 3 18 1375,3 мкс 2

1450,5 мкс на 860 SPS — это 95 мкс записи Config, пауза 1163 мкс, 72,5 мкс чтения Config и 120 мкс чтения результата с записью указателя. Опрос без паузы на 8 SPS — 1726 чтений подряд: шина занята всё преобразование, и остальным устройствам на ней места нет.

Непрерывное (MODE = 0): преобразования идут одно за другим, Conversion всегда хранит последнее. Это для одного канала. Сменили вход — в регистре ещё старый результат: в модели непрерывное на AIN0 (код 4000) переключили на AIN3 и сразу прочитали — 4000; после ближайшего RDY — 16000. В модели запись Config начинает преобразование заново; так ли у микросхемы, не проверено — надёжнее выбросить первый результат после смены входа.

Четыре канала по очереди удобнее однократными преобразованиями: каждый начинается уже с новым входом.

/* четыре несимметричных входа по очереди, каждый — однократным преобразованием */
int ads_scan4(ads_t *d, uint32_t pga, uint32_t dr, int16_t out[4])
{
    for (uint32_t ch = 0; ch < 4u; ch++) {
        int rc = ads_single(d, ADS_AIN0 + ch, pga, dr, &out[ch]);
        if (rc != ADS_OK && rc != ADS_E_OVER)
            return rc;
    }
    return ADS_OK;
}

Входы 0,5, 1,0, 1,5 и 2,0 В на ±4,096 В — коды 4000, 8000, 12000 и 16000, на круг 12 обменов и 4 опроса. Время на канал:

DR 1/DR на канал, 100 кГц на канал, 400 кГц
8 SPS 125 000 мкс 126 150 мкс 125 287,5 мкс
128 SPS 7812,5 мкс 8963 мкс 8100,5 мкс
250 SPS 4000 мкс 5150 мкс 4287,5 мкс
475 SPS 2105,3 мкс 3256 мкс 2393,5 мкс
860 SPS 1162,8 мкс 2313 мкс 1450,5 мкс

Шина добавляет к преобразованию 287,7 мкс на 400 кГц и 1150,2 мкс на 100 кГц: на 860 SPS при 100 кГц канал занимает почти вдвое больше времени, чем само преобразование. Все четыре входа на 860 SPS и 400 кГц — 5802 мкс.


15б.5 Код в микровольты, знак, шум и скорость

Единица кода — диапазон PGA / 32768. Перевод целыми: код × FS в микровольтах / 32768 с округлением. Произведение не влезает в 32 бита (32767 × 6 144 000), поэтому оно 64-битное, а деление на 32768 — сдвигом:

/* код → микровольты целыми: код × FS / 32768 с округлением, без деления */
int32_t ads_uv(int16_t code, uint32_t pga)
{
    int64_t x = (int64_t)code * fs_mv[pga > 5u ? 5u : pga] * 1000;    /* PGA 110 и 111 — тоже ±0,256 В */
    return (int32_t)(x >= 0 ? (x + 16384) >> 15 : -((-x + 16384) >> 15));
}

armclang собрал ads_uv в 84 байта: умножения SMULL и UMULL, без вызова библиотеки. В модели, вход против GND:

PGA Единица кода 0,2 В: код обратно, мкВ 1,0 В: код
±6,144 В 187,5 мкВ 1067 200 063 5333
±4,096 В 125 мкВ 1600 200 000 8000
±2,048 В 62,5 мкВ 3200 200 000 16000
±1,024 В 31,25 мкВ 6400 200 000 32000
±0,512 В 15,625 мкВ 12800 200 000 32767 — край
±0,256 В 7,8125 мкВ 25600 200 000 32767 — край

Крайние значения: ads_uv(32767, ±6,144 В) = 6 143 813 мкВ, ads_uv(−32768, ±2,048 В) = −2 048 000 мкВ, одна единица на ±0,256 В — 8 мкВ после округления.

Знак. Код — int16_t, не uint16_t. Разность AIN0 − AIN1 = 1,25 − 1,00 В на ±2,048 В — код 4000 (0x0FA0), 250 000 мкВ; входы наоборот — −4000 (0xF060), −250 000 мкВ. И вход против GND бывает отрицательным: −2 мВ на AIN0 дали −32 (0xFFE0), −2000 мкВ. Прочитанный как uint16_t, этот код стал бы 65504.

Шум. В модели шум микросхемы задан так: 2 мкВ × √DR (с. к. о.) — это допущение, не данные документа. По 400 результатов на разность 100 мВ:

DR Шум модели ±0,256 В: с. к. о., кодов разных кодов ±2,048 В: с. к. о., кодов разных кодов ±0,256 В, среднее из 16
8 SPS 5,66 мкВ 0,78 5 0 1 1,62 мкВ
128 SPS 22,63 мкВ 2,91 15 0,43 3 5,2 мкВ
860 SPS 58,65 мкВ 7,56 40 1,01 7 12,16 мкВ

На ±2,048 В и 8 SPS все 400 результатов одинаковы: шум меньше единицы кода, и складывать нечего (14а.6, 165.8). На ±0,256 В шум — несколько кодов, и среднее из 16 снижает его примерно вчетверо. Узкий диапазон PGA и медленная скорость — первое, что даёт точность.

Сетевая помеха. Фильтр-усреднение за 1/DR гасит синус тем сильнее, чем больше его периодов укладывается в одно преобразование. Помеха 50 Гц амплитудой 1 мВ в разности, 64 результата:

DR Периодов 50 Гц за преобразование Остаток помехи
8 SPS 6,25 31,2 мкВ
16 SPS 3,125 39,1 мкВ
32 SPS 1,5625 195,3 мкВ
64 SPS 0,7812 257,8 мкВ
128 SPS 0,3906 765,6 мкВ
250 SPS 0,2 929,7 мкВ
475 SPS 0,1053 984,4 мкВ
860 SPS 0,0581 992,2 мкВ

Нуля нет ни на одной скорости: 50 Гц не кратна ни одной из них. Ноль даёт среднее ровно за 20 мс подряд идущих результатов: на 250 SPS ads_average из 5 — остаток 1,6 мкВ, на 860 SPS из 17 (19,767 мс) — 12,4 мкВ, на 128 SPS из 3 (23,438 мс) — 140,6 мкВ. Но приём держится на точной частоте преобразований: в модели с частотой на 3 % ниже (окно 20,6 мс) остаток на 250 SPS — 29,7 мкВ. Насколько частота микросхемы отличается от номинальной, не проверено.


15б.6 ALERT/RDY: «готово» и компаратор

У вывода ALERT/RDY две работы. «Готово» включается особым сочетанием порогов (по документации производителя): старший бит Hi_thresh = 1, старший бит Lo_thresh = 0, и COMP_QUE ≠ 11:

/* ALERT/RDY как «готово»: старший бит Hi_thresh = 1, старший бит Lo_thresh = 0, COMP_QUE ≠ 11 */
int ads_rdy_mode(ads_t *d)
{
    int rc = ads_write(d, ADS_HI_THRESH, 0x8000u);
    return rc ? rc : ads_write(d, ADS_LO_THRESH, 0x0000u);
}

int ads_single_rdy(ads_t *d, uint32_t mux, uint32_t pga, uint32_t dr, int16_t *code)
{
    uint32_t t = ads_conv_us(dr);
    (void)ads_port_wait_rdy(0);                         /* снять старый флаг EXTI */
    int rc = ads_write(d, ADS_CONFIG, ads_cfg(mux, pga, dr, 1) | ADS_QUE_1);
    if (rc)
        return rc;
    if (!ads_port_wait_rdy(2u * t + 10000u))
        return ADS_E_TIMEOUT;
    return ads_read_conv(d, code);
}

В прошивке спад на PB5 поднимает флаг в прерывании, порт ждёт его с тайм-аутом:

/* ALERT/RDY: спад на PB5 */
void EXTI9_5_IRQHandler(void)
{
    EXTI->PR = 1u << 5;
    rdy = 1;
}

int ads_port_wait_rdy(uint32_t timeout_us)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!rdy) {
        if (expired(t0, timeout_us))
            return 0;
    }
    rdy = 0;
    return 1;
}

Однократное по RDY — 2 обмена вместо 3, на 860 SPS 1375,3 мкс вместо 1450,5 (15б.4), и шина между ними свободна. В непрерывном режиме RDY — короткий импульс в конце каждого преобразования: в модели ads_average на 860 SPS собрала 860 результатов за 1 000 070 мкс, по одному спаду и одному обмену на результат, среднее — код 16000.

Две ошибки, при которых RDY молчит: COMP_QUE = 11 (компаратор выключен, вывод отпущен) или пороги после включения не тронуты — тогда это обычный компаратор с порогом 0x7FFF. В модели в обоих случаях прошло 10 преобразований, спадов 0, а ads_average вернула ADS_E_TIMEOUT через 12 326 мкс.

Компаратор. С обычными порогами ALERT следит за уровнем сам, без опроса. С гистерезисом (COMP_MODE = 0) он срабатывает, когда код выше Hi_thresh, и отпускает, когда ниже Lo_thresh; окно (COMP_MODE = 1) — когда код вне промежутка. COMP_QUE требует 1, 2 или 4 преобразования подряд, COMP_LAT держит срабатывание до чтения результата. В модели вход медленно качается от 1,35 до 1,55 В (синус 0,5 Гц) с шумом датчика 5 мВ, 128 SPS, ±2,048 В, 10 с — 1280 преобразований и пять подъёмов через 1,5 В:

Настройка Lo_thresh Hi_thresh Спадов ALERT
без гистерезиса, оба порога 1,50 В 24000 24000 14
гистерезис 1,40–1,50 В 22400 24000 5
без гистерезиса, COMP_QUE — 4 подряд 24000 24000 5
окно 1,40–1,50 В 22400 24000 25
гистерезис и защёлка 22400 24000 1

Без гистерезиса шум около порога даёт лишние срабатывания — 14 на пять подъёмов. Гистерезис или очередь из четырёх оставляют ровно пять. С защёлкой вывод сработал один раз и так и остался в нуле: результат никто не читал.


15б.7 Грабли

Вход за пределами питания. По документации производителя напряжение на входах — от GND − 0,3 В до VDD + 0,3 В, и диапазон ±6,144 В этого не отменяет. При питании 3,3 В модель отметила нарушение на 5 В, а на 3,3 и 3,6 В — нет; вход 3,3 В на ±6,144 В — код 17600 из 32767. Что сделает с 5 В настоящая микросхема, не проверено, — делитель или питание ADS1115 от 5 В.

±0,256 В на большом сигнале. Код упирается в 32767, и это похоже на настоящее число. В модели 1 В на ±0,256 В — 32767, и ads_read_conv вернула ADS_E_OVER. Край диапазона драйвер должен отличать от измерения.

Шина зависла. Микроконтроллер сбросили посреди чтения, а ADS1115 выдавал ноль — он держит SDA и ждёт тактов (17.6). В модели из 1000 случайных сбросов 508 оставили SDA в нуле. Пока шина не освобождена, каждый обмен кончается ADS_E_BUS по тайм-ауту порта (1000 мкс). Девять тактов SCL из 17.6 освободили все 508, больше 7 тактов не понадобилось, и следующее измерение прошло во всех 508.

Перепутан порядок байт. ADS1115 шлёт старший байт первым, а F103 — как и компьютер — хранит младший первым. Байты 0F A0, скопированные в int16_t как есть, дали −24561 вместо 4000; F0 60 — 24816 вместо −4000. Собирать сдвигом, как ads_be16.

Сменил вход в непрерывном режиме и сразу прочитал. Прочитан старый вход: 4000 вместо 16000 (15б.4).

RDY молчит. COMP_QUE = 11 или пороги не переставлены — спадов 0, тайм-аут через 12 326 мкс (15б.6).

Устройство не отвечает. Вывод ADDR подключён не туда — по 0x48 ADS_E_NACK. Или в драйвер передан сдвинутый адрес 0x90 вместо 0x48 (17.2).


15б.8 Итоги

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

ads15c.h — регистры, биты Config, коды MUX, PGA и DR, порт и функции драйвера.

/* ads15c.h — раздел 15б: драйвер внешнего АЦП ADS1115 по I2C без регистров микроконтроллера.
 * Драйвер говорит с микросхемой только через три функции порта (ads_port_*): в прошивке fw15c.c их
 * делают I2C1 и EXTI5 STM32F103, в стенде host15c.c — модель model15c.py.
 * Карта регистров, биты Config, коды PGA, скорости и адреса — по документации производителя;
 * документа в проекте нет, с микросхемой не сверено. */
#ifndef ADS15C_H
#define ADS15C_H
#include <stdint.h>

/* ── регистры: номер пишется первым байтом записи в регистр указателя ── */
#define ADS_CONV          0x00u         /* результат, 16 бит, дополнительный код; только чтение */
#define ADS_CONFIG        0x01u
#define ADS_LO_THRESH     0x02u
#define ADS_HI_THRESH     0x03u

/* ── Config ───────────────────────────────────────────────────────────── */
#define ADS_OS            0x8000u       /* запись 1 — начать однократное; чтение 0 — идёт преобразование */
#define ADS_MUX_POS       12u           /* 14:12 — вход */
#define ADS_PGA_POS       9u            /* 11:9 — диапазон */
#define ADS_MODE_SINGLE   0x0100u       /* 1 — однократное с засыпанием, 0 — непрерывное */
#define ADS_DR_POS        5u            /* 7:5 — скорость */
#define ADS_COMP_WINDOW   0x0010u       /* компаратор: 0 — с гистерезисом, 1 — окно */
#define ADS_COMP_POL_HIGH 0x0008u       /* 0 — ALERT активен нулём */
#define ADS_COMP_LAT      0x0004u       /* 1 — держать ALERT до чтения результата */
#define ADS_QUE_1         0x0000u       /* COMP_QUE 1:0: сработать после 1, 2 или 4 преобразований */
#define ADS_QUE_2         0x0001u
#define ADS_QUE_4         0x0002u
#define ADS_QUE_OFF       0x0003u       /* компаратор выключен, ALERT отпущен */
#define ADS_CONFIG_RESET  0x8583u       /* после включения питания */

enum { ADS_AIN0_AIN1, ADS_AIN0_AIN3, ADS_AIN1_AIN3, ADS_AIN2_AIN3,     /* MUX 000…011 — разность */
       ADS_AIN0, ADS_AIN1, ADS_AIN2, ADS_AIN3 };                        /* MUX 100…111 — против GND */
enum { ADS_FS_6144, ADS_FS_4096, ADS_FS_2048, ADS_FS_1024, ADS_FS_512, ADS_FS_256 };   /* PGA 000…101 */
enum { ADS_DR_8, ADS_DR_16, ADS_DR_32, ADS_DR_64, ADS_DR_128, ADS_DR_250, ADS_DR_475, ADS_DR_860 };

#define ADS_ADDR_GND      0x48u         /* вывод ADDR на GND; на VDD — 0x49, на SDA — 0x4A, на SCL — 0x4B */
#define ADS_POLL_US       100u          /* шаг опроса OS после расчётного времени */

enum { ADS_OK = 0, ADS_E_NACK = -1, ADS_E_BUS = -2, ADS_E_TIMEOUT = -3, ADS_E_OVER = -4 };

typedef struct {
    uint8_t addr;                       /* семибитный адрес: 0x48…0x4B */
    uint8_t ptr;                        /* что сейчас в регистре указателя; 0xFF — неизвестно */
    uint16_t cfg;                       /* последнее, что драйвер записал в Config */
    uint32_t xfers, polls, errors;      /* обменов по I2C, опросов OS, ошибок шины */
} ads_t;

/* ── порт: в прошивке — I2C1 и EXTI5, в стенде — строки к модели ─────── */
/* записать ntx байт; если nrx > 0 — повторный старт и прочитать nrx байт; 0, ADS_E_NACK или ADS_E_BUS */
int ads_port_xfer(uint8_t addr7, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx);
void ads_port_delay_us(uint32_t us);
/* ждать спада на ALERT/RDY (флаг EXTI) не дольше timeout_us; 1 — был, флаг снят; 0 — не было */
int ads_port_wait_rdy(uint32_t timeout_us);
/* девять тактов SCL вручную и STOP (раздел 17.6): число тактов до отпускания SDA или −1 */
int ads_port_bus_recover(void);

/* ── драйвер ──────────────────────────────────────────────────────────── */
uint16_t ads_cfg(uint32_t mux, uint32_t pga, uint32_t dr, int single);
int ads_probe(ads_t *d, uint8_t addr7);
int ads_write(ads_t *d, uint8_t reg, uint16_t v);
int ads_read(ads_t *d, uint8_t reg, uint16_t *v);
uint32_t ads_conv_us(uint32_t dr);
int ads_single(ads_t *d, uint32_t mux, uint32_t pga, uint32_t dr, int16_t *code);
int ads_rdy_mode(ads_t *d);
int ads_single_rdy(ads_t *d, uint32_t mux, uint32_t pga, uint32_t dr, int16_t *code);
int ads_continuous(ads_t *d, uint32_t mux, uint32_t pga, uint32_t dr, uint16_t comp);
int ads_read_conv(ads_t *d, int16_t *code);
int ads_average(ads_t *d, uint32_t n, int32_t *sum);
int ads_scan4(ads_t *d, uint32_t pga, uint32_t dr, int16_t out[4]);
int ads_thresholds(ads_t *d, int16_t lo, int16_t hi);
int ads_recover(ads_t *d);
int32_t ads_uv(int16_t code, uint32_t pga);
int16_t ads_be16(const uint8_t b[2]);

#endif

ads15c.c — драйвер: указатель, однократное и непрерывное, RDY, четыре канала, микровольты.

/* ads15c.c — раздел 15б: драйвер ADS1115 по I2C. Один и тот же файл собирается в прошивку (armclang,
 * Cortex-M3) и в стенд на компьютере (gcc): микроконтроллера он не знает, только порт ads_port_*. */
#include "ads15c.h"

static const uint16_t fs_mv[6] = {6144, 4096, 2048, 1024, 512, 256};    /* PGA 000…101 */
static const uint16_t sps[8] = {8, 16, 32, 64, 128, 250, 475, 860};       /* DR 000…111 */

uint16_t ads_cfg(uint32_t mux, uint32_t pga, uint32_t dr, int single)
{
    uint32_t c = (mux & 7u) << ADS_MUX_POS | (pga & 7u) << ADS_PGA_POS | (dr & 7u) << ADS_DR_POS;
    return (uint16_t)(single ? c | ADS_OS | ADS_MODE_SINGLE : c);
}

static int xfer(ads_t *d, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx)
{
    int rc = ads_port_xfer(d->addr, tx, ntx, rx, nrx);
    d->xfers++;
    if (rc) {
        d->errors++;
        d->ptr = 0xFF;                                  /* неизвестно, дошёл ли указатель */
    }
    return rc;
}

int ads_write(ads_t *d, uint8_t reg, uint16_t v)
{
    uint8_t b[3] = {reg, (uint8_t)(v >> 8), (uint8_t)v};   /* старший байт первым */
    int rc = xfer(d, b, 3, 0, 0);
    if (rc == ADS_OK) {
        d->ptr = reg;
        if (reg == ADS_CONFIG)
            d->cfg = v;
    }
    return rc;
}

int ads_read(ads_t *d, uint8_t reg, uint16_t *v)
{
    uint8_t b[2];
    int rc = d->ptr == reg ? xfer(d, 0, 0, b, 2)        /* указатель уже на месте: только чтение */
                           : xfer(d, &reg, 1, b, 2);    /* иначе запись указателя и повторный старт */
    if (rc == ADS_OK) {
        d->ptr = reg;
        *v = (uint16_t)(b[0] << 8 | b[1]);
    }
    return rc;
}

int16_t ads_be16(const uint8_t b[2])
{
    return (int16_t)(uint16_t)(b[0] << 8 | b[1]);
}

int ads_probe(ads_t *d, uint8_t addr7)
{
    uint16_t v;
    d->addr = addr7;
    d->ptr = 0xFF;
    d->xfers = d->polls = d->errors = 0;
    int rc = ads_read(d, ADS_CONFIG, &v);               /* ответил ли кто-то по этому адресу */
    if (rc == ADS_OK)
        d->cfg = v;
    return rc;
}

uint32_t ads_conv_us(uint32_t dr)
{
    return (1000000u + sps[dr & 7u] - 1u) / sps[dr & 7u];
}

int ads_read_conv(ads_t *d, int16_t *code)
{
    uint16_t v;
    int rc = ads_read(d, ADS_CONV, &v);
    if (rc)
        return rc;
    *code = (int16_t)v;
    return v == 0x7FFFu || v == 0x8000u ? ADS_E_OVER : ADS_OK;   /* упёрлись в край диапазона PGA */
}

/* однократное: OS = 1 запускает; сначала ждём расчётное время, потом опрашиваем OS */
int ads_single(ads_t *d, uint32_t mux, uint32_t pga, uint32_t dr, int16_t *code)
{
    uint16_t c;
    uint32_t t = ads_conv_us(dr);
    int rc = ads_write(d, ADS_CONFIG, ads_cfg(mux, pga, dr, 1) | ADS_QUE_OFF);
    if (rc)
        return rc;
    ads_port_delay_us(t);
    for (uint32_t waited = t;; waited += ADS_POLL_US) {
        if ((rc = ads_read(d, ADS_CONFIG, &c)) != ADS_OK)
            return rc;
        d->polls++;
        if (c & ADS_OS)                                 /* 1 — преобразование закончено */
            break;
        if (waited > 2u * t + 10000u)
            return ADS_E_TIMEOUT;
        ads_port_delay_us(ADS_POLL_US);
    }
    return ads_read_conv(d, code);
}

/* ALERT/RDY как «готово»: старший бит Hi_thresh = 1, старший бит Lo_thresh = 0, COMP_QUE ≠ 11 */
int ads_rdy_mode(ads_t *d)
{
    int rc = ads_write(d, ADS_HI_THRESH, 0x8000u);
    return rc ? rc : ads_write(d, ADS_LO_THRESH, 0x0000u);
}

int ads_single_rdy(ads_t *d, uint32_t mux, uint32_t pga, uint32_t dr, int16_t *code)
{
    uint32_t t = ads_conv_us(dr);
    (void)ads_port_wait_rdy(0);                         /* снять старый флаг EXTI */
    int rc = ads_write(d, ADS_CONFIG, ads_cfg(mux, pga, dr, 1) | ADS_QUE_1);
    if (rc)
        return rc;
    if (!ads_port_wait_rdy(2u * t + 10000u))
        return ADS_E_TIMEOUT;
    return ads_read_conv(d, code);
}

int ads_continuous(ads_t *d, uint32_t mux, uint32_t pga, uint32_t dr, uint16_t comp)
{
    return ads_write(d, ADS_CONFIG, ads_cfg(mux, pga, dr, 0) | (comp & 0x1Fu));
}

/* n результатов подряд в непрерывном режиме по RDY; сумма — в *sum */
int ads_average(ads_t *d, uint32_t n, int32_t *sum)
{
    uint32_t t = ads_conv_us(d->cfg >> ADS_DR_POS & 7u);
    int16_t code;
    *sum = 0;
    (void)ads_port_wait_rdy(0);
    for (uint32_t i = 0; i < n; i++) {
        if (!ads_port_wait_rdy(2u * t + 10000u))
            return ADS_E_TIMEOUT;
        int rc = ads_read_conv(d, &code);
        if (rc != ADS_OK && rc != ADS_E_OVER)
            return rc;
        *sum += code;
    }
    return ADS_OK;
}

/* четыре несимметричных входа по очереди, каждый — однократным преобразованием */
int ads_scan4(ads_t *d, uint32_t pga, uint32_t dr, int16_t out[4])
{
    for (uint32_t ch = 0; ch < 4u; ch++) {
        int rc = ads_single(d, ADS_AIN0 + ch, pga, dr, &out[ch]);
        if (rc != ADS_OK && rc != ADS_E_OVER)
            return rc;
    }
    return ADS_OK;
}

int ads_thresholds(ads_t *d, int16_t lo, int16_t hi)
{
    int rc = ads_write(d, ADS_LO_THRESH, (uint16_t)lo);
    return rc ? rc : ads_write(d, ADS_HI_THRESH, (uint16_t)hi);
}

int ads_recover(ads_t *d)
{
    d->ptr = 0xFF;
    return ads_port_bus_recover() < 0 ? ADS_E_BUS : ADS_OK;
}

/* код → микровольты целыми: код × FS / 32768 с округлением, без деления */
int32_t ads_uv(int16_t code, uint32_t pga)
{
    int64_t x = (int64_t)code * fs_mv[pga > 5u ? 5u : pga] * 1000;    /* PGA 110 и 111 — тоже ±0,256 В */
    return (int32_t)(x >= 0 ? (x + 16384) >> 15 : -((-x + 16384) >> 15));
}

fw15c.c — прошивка STM32F103: I2C1 на 400 кГц, EXTI5 на ALERT/RDY, восстановление шины.

/* fw15c.c — раздел 15б: ADS1115 на STM32F103 (BluePill), 72 МГц, APB1 = 36 МГц.
 * I2C1: PB6 — SCL, PB7 — SDA (открытый сток, 400 кГц); PB5 — ALERT/RDY (вход с подтяжкой, EXTI5 по спаду).
 * ADDR микросхемы — на GND: адрес 0x48. Раз в 100 мс — четыре входа по очереди (±4,096 В, 128 SPS) и
 * разность AIN0 − AIN1 по RDY (±2,048 В) в микровольтах. Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "ads15c.h"

#define CPU_HZ          72000000u
#define APB1_MHZ        36u
#define I2C_TIMEOUT_US  1000u           /* дольше любого флага на 400 кГц; столько ждёт и модель */

static ads_t adc;
static volatile uint8_t rdy;
volatile int32_t uv[5];                 /* AIN0…AIN3 и AIN0 − AIN1, мкВ */
volatile uint32_t bus_errors;

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

void ads_port_delay_us(uint32_t us)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!expired(t0, us)) {
    }
}

static void i2c1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    /* PB6, PB7 — альтернативная функция, открытый сток, 50 МГц (1111); PB5 — вход с подтяжкой (1000) */
    GPIOB->CRL = (GPIOB->CRL & 0x000FFFFFu) | 0xFF800000u;
    GPIOB->BSRR = 1u << 5;
    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 int sr1_wait(uint16_t mask)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!(I2C1->SR1 & mask)) {
        if (I2C1->SR1 & I2C_SR1_AF)
            return ADS_E_NACK;                          /* адрес или байт не подтверждён */
        if (expired(t0, I2C_TIMEOUT_US))
            return ADS_E_BUS;
    }
    return ADS_OK;
}

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

int ads_port_xfer(uint8_t addr7, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx)
{
    int rc;
    uint32_t t0 = DWT->CYCCNT;
    if (nrx != 0u && nrx != 2u)
        return ADS_E_BUS;                               /* драйвер читает только по два байта */
    while (I2C1->SR2 & I2C_SR2_BUSY) {                  /* SDA держит ведомый — раздел 17.6 */
        if (expired(t0, I2C_TIMEOUT_US))
            return ADS_E_BUS;
    }
    if (ntx) {
        I2C1->CR1 |= I2C_CR1_START;
        if ((rc = sr1_wait(I2C_SR1_SB)) != ADS_OK)
            return fail(rc);
        I2C1->DR = (uint8_t)(addr7 << 1);
        if ((rc = sr1_wait(I2C_SR1_ADDR)) != ADS_OK)
            return fail(rc);
        (void)I2C1->SR2;                                /* ADDR снят: SR1 прочитан в sr1_wait, теперь SR2 */
        for (uint32_t i = 0; i < ntx; i++) {
            if ((rc = sr1_wait(I2C_SR1_TXE)) != ADS_OK)
                return fail(rc);
            I2C1->DR = tx[i];
        }
        if ((rc = sr1_wait(I2C_SR1_BTF)) != ADS_OK)
            return fail(rc);
    }
    if (!nrx) {
        I2C1->CR1 |= I2C_CR1_STOP;
        return ADS_OK;
    }
    I2C1->CR1 |= I2C_CR1_START;                         /* после записи — повторный старт */
    if ((rc = sr1_wait(I2C_SR1_SB)) != ADS_OK)
        return fail(rc);
    I2C1->CR1 |= I2C_CR1_POS | I2C_CR1_ACK;             /* два байта: RM0008, с. 764 */
    I2C1->DR = (uint8_t)(addr7 << 1 | 1u);
    if ((rc = sr1_wait(I2C_SR1_ADDR)) != ADS_OK)
        return fail(rc);
    (void)I2C1->SR2;
    I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
    if ((rc = sr1_wait(I2C_SR1_BTF)) != ADS_OK)         /* оба байта приняты, SCL прижат */
        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 ADS_OK;
}

/* ALERT/RDY: спад на PB5 */
void EXTI9_5_IRQHandler(void)
{
    EXTI->PR = 1u << 5;
    rdy = 1;
}

int ads_port_wait_rdy(uint32_t timeout_us)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!rdy) {
        if (expired(t0, timeout_us))
            return 0;
    }
    rdy = 0;
    return 1;
}

/* то же, что в разделе 17.6, но возвращает число тактов */
int ads_port_bus_recover(void)
{
    int n = 0;
    I2C1->CR1 &= (uint16_t)~I2C_CR1_PE;
    GPIOB->BSRR = 1u << 6 | 1u << 7;
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0x77000000u;    /* PB6, PB7 — выход, открытый сток (0111) */
    while (n < 9 && !(GPIOB->IDR & 1u << 7)) {         /* пока SDA в нуле */
        GPIOB->BRR = 1u << 6;
        ads_port_delay_us(5);
        GPIOB->BSRR = 1u << 6;
        ads_port_delay_us(5);
        n++;
    }
    int stuck = !(GPIOB->IDR & 1u << 7);
    GPIOB->BRR = 1u << 6;                               /* STOP: SDA вверх при высоком SCL */
    GPIOB->BRR = 1u << 7;
    ads_port_delay_us(5);
    GPIOB->BSRR = 1u << 6;
    ads_port_delay_us(5);
    GPIOB->BSRR = 1u << 7;
    ads_port_delay_us(5);
    I2C1->CR1 = I2C_CR1_SWRST;
    I2C1->CR1 = 0;
    i2c1_init();
    return stuck ? -1 : n;
}

static void irq_init(void)
{
    AFIO->EXTICR[1] = (AFIO->EXTICR[1] & ~0xF0u) | 0x10u;    /* EXTI5 ← PB5 */
    EXTI->FTSR |= 1u << 5;
    EXTI->IMR |= 1u << 5;
    NVIC_EnableIRQ(EXTI9_5_IRQn);
}

int main(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    i2c1_init();
    irq_init();
    while (ads_probe(&adc, ADS_ADDR_GND) != ADS_OK) {
        (void)ads_recover(&adc);                        /* нет ответа: шина или провод */
        ads_port_delay_us(100000u);
    }
    (void)ads_rdy_mode(&adc);
    for (;;) {
        int16_t c[4], d = 0;
        int rc = ads_scan4(&adc, ADS_FS_4096, ADS_DR_128, c);
        if (rc == ADS_OK)
            for (uint32_t i = 0; i < 4u; i++)
                uv[i] = ads_uv(c[i], ADS_FS_4096);
        if (rc == ADS_OK)
            rc = ads_single_rdy(&adc, ADS_AIN0_AIN1, ADS_FS_2048, ADS_DR_128, &d);
        if (rc == ADS_OK || rc == ADS_E_OVER)
            uv[4] = ads_uv(d, ADS_FS_2048);
        if (rc == ADS_E_BUS || rc == ADS_E_NACK) {
            bus_errors++;
            if (ads_recover(&adc) == ADS_OK)
                (void)ads_rdy_mode(&adc);               /* вдруг микросхема перезапустилась: пороги заново */
        }
        ads_port_delay_us(100000u);
    }
}

Дальше: раздел 15в «Внешний ЦАП MCP4725 по I2C: 12 бит, быстрая запись и EEPROM, опорное от питания, генерация сигнала» — обратная задача на той же шине: число в напряжение на F103, у которого своего ЦАП нет.

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