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

15г. Внешний 24-битный АЦП ADS1220 по SPI: дельта-сигма, PGA, мультиплексор, опорное, фильтр 50/60 Гц, DRDY и логометрическое измерение тензомоста и термосопротивления

В 15б внешний АЦП ADS1115 дал F103 16 бит и усилитель до ±0,256 В. Тензомосту и термометру сопротивления этого мало: мост на полной нагрузке выдаёт единицы милливольт, PT100 меняется на 0,39 Ом на градус. Для весов в разделе 102 стоит HX711 — 24 бита, но один мост, своё двухпроводное подключение и ничего больше. ADS1220 — следующая ступень: 24 бита, усиление до 128, четыре входа, выбор опорного (своё 2,048 В, два внешних входа или питание), два источника тока для термометра сопротивления, фильтр сети 50/60 Гц и обычный SPI с выводом «готово». Здесь — драйвер: команды, четыре регистра и их поля, режим SPI 1, 24 бита в дополнительном коде, скорость и время установления фильтра, логометрическое включение моста и PT100 по трём проводам, DRDY в прерывание и чтение по DMA, калибровка смещения и грабли. Теория термометров сопротивления и пересчёт сопротивления в градусы — в 176б, тара и калибровка весов — в 102.6.

Опоры: RM0008 (DocID13902 Rev 17): выводы SPI — таблица 25, с. 166; без ремапа SPI1 — PA4–PA7, с. 185; AFIO_EXTICR1, с. 190; EXTI_FTSR, с. 211; DMA1, канал 2 — SPI1_RX, канал 3 — SPI1_TX, таблица 78, с. 281; BR[2:0] = 101 — fPCLK/64, с. 743; CPOL и CPHA, с. 744; TXDMAEN, RXDMAEN и BSY, с. 745. DocID13587 Rev 17 (STM32F103x8/xB): PA4–PA7 — выводы SPI1, PA4–PA7 и PB0 без допуска 5 В, с. 29. Документа на ADS1220 в проекте нет. Коды команд RESET, START/SYNC, RREG и WREG, нули во всех регистрах после сброса, раскладка полей MUX, GAIN, PGA_BYPASS, DR, MODE, CM и VREF, коды усилений, входов, опорных и скоростей (нормальные 20…1000 SPS, turbo — вдвое), режим SPI 1, пауза 50 мкс после RESET и выдвигание 24 бит без команды сверены с драйвером Klipper (коммит 7bc4d09, лицензия GPL-3.0: klippy/extras/ads1220.py, src/sensor_ads1220.c, docs/Config_Reference.md). Остальное — поля 50/60, PSW, IDAC, I1MUX, I2MUX, DRDYM, коды MUX 1100–1110, команды RDATA и POWERDOWN, внутреннее опорное 2,048 В, токи IDAC, пределы синфазного напряжения PGA — по документации производителя, не проверено; это заложено в модель. Все числа ниже — прогон драйвера _verify/adc15e/ads15e.c, собранного arm-none-eabi-gcc 12.2.1 под Cortex-M3 и запущенного в QEMU 7.2 (netduino2), против своей модели ADS1220 на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3. Числа говорят о модели, а не о микросхеме.

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

Оригинал Перевод
delta-sigma modulator дельта-сигма модулятор: много грубых измерений, потом фильтр
programmable gain amplifier (PGA), PGA bypass усилитель с выбором усиления; обход усилителя
input multiplexer (MUX), AINP / AINN мультиплексор; положительный и отрицательный вход
voltage reference (VREF), REFP0 / REFN0 опорное напряжение; входы внешнего опорного
ratiometric measurement логометрическое измерение: сигнал и опорное от одного источника
excitation current source (IDAC) источник тока возбуждения
data rate (DR), turbo mode скорость, преобразований в секунду; ускоренный режим
settling time, sinc filter время установления; фильтр-усреднение
50/60-Hz rejection подавление сетевой помехи
single-shot / continuous conversion mode однократное / непрерывное преобразование
common-mode voltage синфазное напряжение: среднее двух входов
two's complement дополнительный код

15г.1 Чем он отличается от ADS1115 и HX711

ADS1115 (15б) HX711 (102) ADS1220
разрядность 16 бит 24 бита 24 бита
связь I2C свой двухпроводный SPI, режим 1, и вывод DRDY
входы 4, 8 сочетаний 2 канала 4, 12 сочетаний и 3 служебных
усиление диапазон от ±6,144 до ±0,256 В 128, 64, 32 1, 2, 4 … 128
опорное своё от питания своё 2,048 В, REF0, REF1 или питание
скорость 8…860 SPS 10 или 80 SPS 20…1000 SPS, в turbo 40…2000
для датчиков — — два источника тока, фильтр 50/60 Гц

Главное отличие — выбор опорного. У HX711 и мост, и опорное — от одного питания, и ничего другого нельзя. У ADS1220 опорным может быть то напряжение, от которого питается датчик: тогда питание из результата сокращается (15г.6). Код — 24 бита в дополнительном коде, от −8 388 608 до 8 388 607; диапазон — ±VREF / усиление. С внутренним 2,048 В и усилением 128 это ±16 мВ, единица кода — 1,9073 нВ.

Вверху — подключение ADS1220 к STM32F103 по SPI1 в режиме 1 (CPOL = 0, CPHA = 1) на 1,125 МГц: PA4 — CS обычным выходом, PA5 — SCLK, PA6 — DOUT/DRDY, PA7 — DIN, PB0 — DRDY на EXTI0 по спаду; к входам AIN0 и AIN1 подключён мост 1 мВ/В от AVDD с опорным AVDD, к AIN2 и AIN3 — PT100 по трём проводам с токами IDAC1 и IDAC2 по 500 мкА и Rref 1,62 кОм на REFP0–REFN0. Посередине — цепочка внутри: мультиплексор на 15 кодов, где 1110 закорачивает входы, PGA с усилением от 1 до 128, дельта-сигма модулятор с опорным VREF, фильтр 20…1000 SPS и 24-битный результат со спадом DRDY; в модели первый результат после START на 20 SPS — через 150 мс, байт по SPI — 7,1 мкс. Внизу — поля четырёх регистров: MUX, GAIN, PGA_BYPASS; DR, MODE, CM, TS, BCS; VREF, 50/60, PSW, IDAC; I1MUX, I2MUX, DRDYM; после RESET все нули; настройки моста 0E 04 D0 00 и PT100 58 04 55 70


15г.2 Подключение к F103 и режим SPI 1

Вывод ADS1220 STM32F103 Назначение
SCLK PA5 такт SPI1
DIN PA7 MOSI: команды и данные в микросхему
DOUT/DRDY PA6 MISO: ответ; при DRDYM = 1 — ещё и «готово»
CS PA4 выбор, обычный выход
DRDY PB0 «готово», EXTI0 по спаду
AVDD, DVDD, AVSS 3,3 В и земля общая земля с микроконтроллером

Без ремапа SPI1 сидит на PA4–PA7 (RM0008, с. 185). По таблице 25 (с. 166) у ведущего SCK и MOSI — альтернативная функция push-pull, MISO — вход, а NSS при программном управлении — обычный вывод. PA4–PA7 и PB0 не терпят 5 В (DocID13587, с. 29) — ADS1220 питаем от 3,3 В.

Режим SPI — 1: такт в покое низкий, данные читаются по спаду (18.2). Klipper открывает шину к ADS1220 именно в режиме 1. Скорость — 72 МГц / 64 = 1,125 МГц (BR = 101, с. 743), байт идёт 7,1 мкс. Klipper берёт 256 или 512 кГц; на 1,125 МГц с микросхемой мы не проверяли, предел такта — по документации производителя. Сомневаетесь — BR = 111 даёт 281 кГц.

static void spi1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_SPI1EN;
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    GPIOA->BSRR = 1u << 4;                              /* CS вверх до настройки вывода */
    /* PA4 — выход 50 МГц (0011), PA5 и PA7 — альтернативная функция push-pull (1011), PA6 — вход (0100) */
    GPIOA->CRL = (GPIOA->CRL & 0x0000FFFFu) | 0xB4B30000u;
    GPIOB->CRL = (GPIOB->CRL & ~0xFu) | 0x8u;           /* PB0 — вход с подтяжкой (1000) */
    GPIOB->BSRR = 1u << 0;
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_CPHA | SPI_CR1_BR_2 | SPI_CR1_BR_0 | SPI_CR1_SSM | SPI_CR1_SSI;  /* режим 1, /64 */
    SPI1->CR2 = SPI_CR2_RXDMAEN | SPI_CR2_TXDMAEN;
    SPI1->CR1 |= SPI_CR1_SPE;
}

CS ведём сами (18.4): команда и её данные должны идти в одном кадре (15г.7). Перед подъёмом CS ждём BSY = 0 (18.5). DRDY — на PB0: AFIO_EXTICR1, биты 3:0 = 0001 — порт B (с. 190), спад включает EXTI_FTSR (с. 211).


15г.3 Команды и четыре регистра

Команда — один байт в кадре CS:

Байт Команда Что делает
0x06 RESET все регистры — в ноль, преобразование остановлено
0x08 START/SYNC начать преобразование (или начать заново)
0x02 POWERDOWN остановить и уснуть
0x10 RDATA следующие три байта — результат
0x20 | rr << 2 | nn RREG прочитать nn + 1 байт с регистра rr
0x40 | rr << 2 | nn WREG записать nn + 1 байт с регистра rr

Регистров четыре, по байту. После RESET все четыре — нули (так их проверяет и Klipper).

Рег. Биты Поле Что задаёт
0 7:4 MUX 0000…0111 — пары AIN0−AIN1, AIN0−AIN2, AIN0−AIN3, AIN1−AIN2, AIN1−AIN3, AIN2−AIN3, AIN1−AIN0, AIN3−AIN2; 1000…1011 — AIN0…AIN3 против AVSS; 1100 — (REFP − REFN) / 4; 1101 — (AVDD − AVSS) / 4; 1110 — входы закорочены на (AVDD + AVSS) / 2
0 3:1 GAIN 000…111 — 1, 2, 4, 8, 16, 32, 64, 128
0 0 PGA_BYPASS 1 — обойти усилитель; действует только при усилении 1, 2, 4
1 7:5 DR 000…110 — 20, 45, 90, 175, 330, 600, 1000 SPS; в turbo — 40, 90, 180, 350, 660, 1200, 2000
1 4:3 MODE 00 — нормальный, 01 — экономичный, 10 — turbo
1 2 CM 0 — однократное, 1 — непрерывное
1 1, 0 TS, BCS датчик температуры вместо входов; токи поиска обрыва
2 7:6 VREF 00 — своё 2,048 В, 01 — REFP0 − REFN0, 10 — REFP1 − REFN1 (это AIN0 и AIN3), 11 — AVDD − AVSS
2 5:4 50/60 00 — нет, 01 — 50 и 60 Гц, 10 — 50 Гц, 11 — 60 Гц
2 3 PSW ключ на AVSS — отключать питание моста
2 2:0 IDAC 000 — выключены, 001…111 — 10, 50, 100, 250, 500, 1000, 1500 мкА
3 7:5, 4:2 I1MUX, I2MUX куда выведен ток: 000 — никуда, 001…100 — AIN0…AIN3, 101 — REFP0, 110 — REFN0
3 1 DRDYM 1 — «готово» ещё и на DOUT/DRDY

Поля MUX, GAIN, PGA_BYPASS, DR, MODE, CM и VREF, коды MUX 0000–1011, усилений, скоростей и опорных совпадают с Klipper; там же сказано, что REF1 занимает AIN0 и AIN3 и что усилитель при усилении 8…128 обойти нельзя. Остальное — по документации производителя.

Сброс и настройка — как у Klipper: после RESET прочитать четыре нуля, после WREG прочитать регистры обратно и сравнить.

/* RESET, пауза, и все четыре регистра должны прочитаться нулями */
int ads_reset(ads_t *d)
{
    uint8_t r[4];
    ads_cmd(d, ADS_CMD_RESET);
    ads_port_delay_us(ADS_RESET_US);
    ads_rreg(d, 0, r, 4);
    for (uint32_t i = 0; i < 4u; i++) {
        d->cfg[i] = 0;
        if (r[i])
            return ADS_E_RESET;
    }
    return ADS_OK;
}

/* записать четыре регистра одним WREG и прочитать обратно — как делает Klipper */
int ads_config(ads_t *d, const uint8_t cfg[4])
{
    uint8_t r[4];
    ads_wreg(d, 0, cfg, 4);
    ads_rreg(d, 0, r, 4);
    for (uint32_t i = 0; i < 4u; i++) {
        d->cfg[i] = cfg[i];
        if (r[i] != cfg[i])
            return ADS_E_VERIFY;
    }
    return ADS_OK;
}

В модели сброс — RESET 0x06, пауза 50 мкс и RREG 23 00 00 00 00 в ответ 00 00 00 00 после мусорного байта — занял 92,7 мкс. Настройка моста — кадр WREG 43 0E 04 D0 00, обратно прочитано 0E 04 D0 00; у PT100 — 43 58 04 55 70. Те же байты модель собрала по полям таблицы независимо от драйвера. Один байт на 1,125 МГц — 7,111 мкс; команда — 7,1 мкс, три байта результата без команды — 21,3 мкс, RDATA с результатом — 28,4 мкс, WREG всех четырёх регистров — 35,6 мкс.


15г.4 Результат: 24 бита, вольты, DRDY и поток по DMA

Результат — три байта, старший первым, в дополнительном коде. Знак 24-го бита надо распространить на старшие биты int32_t:

/* 24 бита в дополнительном коде, старший байт первым → int32_t */
int32_t ads_s24(const uint8_t b[3])
{
    int32_t v = (int32_t)((uint32_t)b[0] << 16 | (uint32_t)b[1] << 8 | b[2]);
    return v & 0x800000 ? v - 0x1000000 : v;
}

/* RDATA и три байта результата — в одном кадре */
int ads_read_data(ads_t *d, int32_t *code)
{
    uint8_t tx[4] = {ADS_CMD_RDATA, 0, 0, 0}, rx[4];
    ads_frame(d, tx, rx, 4);
    *code = ads_s24(rx + 1);
    return range(*code);
}

/* после спада DRDY результат выдвигается сам: 24 такта без команды, на DIN — нули */
int ads_read_direct(ads_t *d, int32_t *code)
{
    uint8_t tx[3] = {0, 0, 0}, rx[3];
    ads_frame(d, tx, rx, 3);
    *code = ads_s24(rx);
    return range(*code);
}

Klipper делает то же самое — ИЛИ с 0xFF000000, если взведён бит 0x800000. Без этого отрицательное число становится огромным положительным:

Байты ads_s24 Как uint32_t без знака
7F FF FF 8 388 607 8 388 607
FF FF FF −1 16 777 215
FF FC 18 −1000 16 776 216
80 00 00 −8 388 608 8 388 608

Два способа прочитать. RDATA — команда и 24 такта в одном кадре, работает в любой момент. Без команды: после спада DRDY результат выдвигается сам — 24 такта, на DIN нули; так читает Klipper. Без команды кадр на байт короче — 21,3 мкс против 28,4.

Однократное (CM = 0): START, ждать спада DRDY, RDATA. Непрерывное (CM = 1): один START, дальше каждый спад DRDY — новый результат.

В вольты. Вход = код × VREF / (усиление × 2²³). Усиление — степень двойки, поэтому деление — сдвиг:

int32_t ads_ratio(int32_t code, uint32_t scale, uint32_t gain)
{
    uint32_t sh = 23u + (gain & 7u);
    int64_t x = (int64_t)code * scale;
    int64_t half = (int64_t)1 << (sh - 1u);
    return (int32_t)(x >= 0 ? (x + half) >> sh : -((-x + half) >> sh));
}

scale — опорное в микровольтах, и получаются микровольты на входе; для моста и PT100 тот же вызов даёт отношение (15г.6). В модели — внутреннее 2,048 В, однократное на 1000 SPS:

Усиление Диапазон Единица кода +1 мВ: код −1 мВ: код +10 мВ: код
1 ±2048 мВ 244,1406 нВ 4145 −4047 41 009
8 ±256 мВ 30,5176 нВ 33 161 −32 375 328 073
32 ±64 мВ 7,6294 нВ 132 645 −129 499 1 312 293
128 ±16 мВ 1,9073 нВ 530 579 −517 997 5 249 171

Обратно ads_ratio дала 1012 мкВ, −988 мкВ и 10 012 мкВ при любом усилении: в модели заложено смещение +12 мкВ на входе (15г.6). Крайние коды — ±2 048 000 мкВ. MUX = 1101 мерит (AVDD − AVSS) / 4 с обойдённым усилителем: при 3,3 В — код 3 379 249, 825 012 мкВ, то есть AVDD = 3 300 048 мкВ. Так проверяют питание без лишнего провода.

DRDY в прерывание, результат по DMA. В непрерывном режиме опрашивать нечего: спад DRDY поднимает EXTI0, обработчик опускает CS и запускает DMA на три байта (канал 2 — SPI1_RX, канал 3 — SPI1_TX, таблица 78, с. 281), а конец приёма поднимает CS и кладёт отсчёт в кольцо. Процессор в обмене не участвует.

/* приём — канал 2 (SPI1_RX), передача — канал 3 (SPI1_TX); приём включается первым */
void ads_port_dma_start(const uint8_t *tx, uint8_t *rx, uint32_t n)
{
    DMA1_Channel2->CCR = 0;
    DMA1_Channel3->CCR = 0;
    DMA1->IFCR = DMA_IFCR_CGIF2 | DMA_IFCR_CGIF3;
    DMA1_Channel2->CPAR = (uint32_t)&SPI1->DR;
    DMA1_Channel2->CMAR = (uint32_t)rx;
    DMA1_Channel2->CNDTR = n;
    DMA1_Channel2->CCR = DMA_CCR2_MINC | DMA_CCR2_TCIE | DMA_CCR2_EN;
    DMA1_Channel3->CPAR = (uint32_t)&SPI1->DR;
    DMA1_Channel3->CMAR = (uint32_t)tx;
    DMA1_Channel3->CNDTR = n;
    DMA1_Channel3->CCR = DMA_CCR3_MINC | DMA_CCR3_DIR | DMA_CCR3_EN;
}

/* DRDY: спад на PB0 */
void EXTI0_IRQHandler(void)
{
    EXTI->PR = 1u << 0;
    if (streaming)
        ads_on_drdy(&adc);                              /* CS вниз и DMA на три байта */
    else
        drdy = 1;
}

/* приняты все три байта: последний бит уже на месте, BSY не ждём */
void DMA1_Channel2_IRQHandler(void)
{
    DMA1->IFCR = DMA_IFCR_CGIF2 | DMA_IFCR_CGIF3;
    DMA1_Channel2->CCR = 0;
    DMA1_Channel3->CCR = 0;
    ads_on_dma_done(&adc);                              /* CS вверх, отсчёт — в кольцо */
}

void ads_on_drdy(ads_t *d)
{
    if (d->dma_busy) {                                  /* прошлый отсчёт ещё не дочитан */
        d->overruns++;
        return;
    }
    d->dma_busy = 1;
    ads_port_cs(0);
    ads_port_dma_start(d->dma_tx, d->dma_rx, 3);
}

void ads_on_dma_done(ads_t *d)
{
    ads_port_cs(1);
    d->ring[d->head % ADS_RING] = ads_s24(d->dma_rx);
    d->head++;
    d->frames++;
    d->bytes += 3;
    d->dma_busy = 0;
}

Приём включается раньше передачи: иначе первый байт уйдёт, а принять его будет некому. TXDMAEN и RXDMAEN (с. 745) включены один раз при настройке. В модели на 2000 SPS (turbo) за 1 001 028,4 мкс пришло 2000 отсчётов — 2001 кадр (START и 2000 по три байта), 6001 байт, ни одного пропуска. Чтение отсчёта занимает шину на 21,33 мкс из 500 — 4,3 %. Если конец DMA приходит позже следующего спада (в модели — через 600 мкс при периоде 500), ads_on_drdy видит занятый DMA и считает переполнение: из 100 прочитанных отсчётов 100 переполнений, 99 результатов пропало.

DRDYM. Если свободного вывода под DRDY нет, DRDYM = 1 выводит «готово» на DOUT/DRDY, пока CS внизу: ads_wait_dout опускает CS, ждёт нуля на MISO (PA6 — обычный вход, его уровень виден в IDR) и сразу выдвигает 24 бита — в модели один кадр, три байта. С DRDYM = 0 вывод «готово» не показывает, и в модели функция вернула ADS_E_TIMEOUT.


15г.5 Фильтр: скорость, время установления, 50/60 Гц, turbo

Дельта-сигма отдаёт не мгновенное значение, а среднее за окно фильтра. В модели фильтр — три окна-усреднения подряд по 1/DR (sinc3); настоящий фильтр микросхемы по документации производителя устроен сложнее, и его времена — в таблице производителя, не проверено. Из такого фильтра следуют три вещи.

Первый результат после START — через всё окно, а не через 1/DR. Однократное в модели, от START до конца RDATA:

Режим Скорость 1/DR Окно фильтра Однократное
нормальный 20 SPS 50 000 мкс 150 000 мкс 150 035,6 мкс
нормальный 45 SPS 22 222,2 мкс 66 666,7 мкс 66 702,2 мкс
нормальный 90 SPS 11 111,1 мкс 33 333,3 мкс 33 368,9 мкс
нормальный 175 SPS 5714,3 мкс 17 142,9 мкс 17 178,4 мкс
нормальный 330 SPS 3030,3 мкс 9090,9 мкс 9126,5 мкс
нормальный 600 SPS 1666,7 мкс 5000 мкс 5035,6 мкс
нормальный 1000 SPS 1000 мкс 3000 мкс 3035,6 мкс
turbo 40 SPS 25 000 мкс 75 000 мкс 75 035,6 мкс
turbo 2000 SPS 500 мкс 1500 мкс 1535,6 мкс

35,6 мкс сверх окна — START (7,1 мкс) и RDATA (28,4 мкс). Смена входа или опорного — то же самое: в модели запись регистров в непрерывном режиме начинает преобразование заново, и следующий результат — через целое окно. Два канала по очереди на 20 SPS — 150 мс на канал, а не 50.

После скачка на входе — три результата. Непрерывное, вход скачком от нуля до полной нагрузки моста сразу после первого результата; доля скачка в каждом следующем:

Где скачок в периоде 1-й 2-й 3-й 4-й 5-й
в самом начале 0 % 16,7 % 83,3 % 100 % 100 %
посередине 0 % 2,1 % 50 % 97,9 % 100 %

Одинаково на 20 и 1000 SPS и в turbo: считается не время, а число периодов. Весам, которые должны показать новый груз, нужны три результата после того, как груз лёг.

Сеть 50 и 60 Гц. Окно длиной в целое число периодов помехи гасит её полностью. Помеха амплитудой 1 мВ в разности моста, усиление 128, 32 результата подряд, наибольшее отклонение:

Скорость 50 Гц 60 Гц
20 SPS, 50/60 = 00 0,93 мкВ 0
45 SPS 0,93 мкВ 7,85 мкВ
90 SPS 178,4 мкВ 62,8 мкВ
175 SPS 660,7 мкВ 546,2 мкВ
330 SPS 892,1 мкВ 844,2 мкВ
600 SPS 946,4 мкВ 939 мкВ
1000 SPS 987,7 мкВ 981,7 мкВ
20 SPS, 50/60 = 01 (оба) 0 0
20 SPS, 50/60 = 10 (50 Гц) 0 0
20 SPS, 50/60 = 11 (60 Гц) 3,09 мкВ 0
turbo 40 SPS, 50/60 = 00 или 01 5,54 мкВ 4,39 мкВ

Поле 50/60 по документации производителя действует только на 20 SPS в нормальном режиме, и модель на других скоростях его не замечает. Как именно оно устроено в микросхеме, не проверено; в модели одно из трёх окон заменено окном 20 мс (50 Гц) или 16,667 мс (60 Гц), и окно однократного стало 86 666,7 мкс (01), 120 000 мкс (10) или 116 666,7 мкс (11). На 1000 SPS помеха проходит почти целиком — 987,7 мкВ из 1000. Если нужна скорость, гасят программно: среднее 20 результатов подряд на 1000 SPS — ровно 20 мс — в модели оставило 0.

Turbo (MODE = 10) — внутренний такт вдвое быстрее (так пишет и Klipper), скорость по тем же кодам DR вдвое выше. 40 SPS в turbo — не 20 SPS: фильтр 50/60 не работает, остаток 5,54 мкВ. И шум растёт со скоростью: ниже — мост с шумом.

Шум. В модели шум на входе микросхемы — 0,08 мкВ × √(DR / 20), у моста свой — 0,05 мкВ × √(DR / 20) (допущения модели, не данные документа). Мост 1 мВ/В на 5 кг, AVDD 3,3 В, усиление 128 — 214,75 кода на грамм; по 400 результатов:

Скорость Шум, кодов Шум, нВ Шум, г
20 SPS 29 89,2 0,135
90 SPS 66,4 204,1 0,309
330 SPS 119,4 366,8 0,556
1000 SPS 229,3 704,9 1,068

Двадцать четыре бита не значат 24 бита точности: на 20 SPS шумят пять младших бит. Для весов — 20 SPS с фильтром 50/60, это и сеть, и шум.


15г.6 Логометрия: мост и PT100, токи IDAC, калибровка смещения

Мост от того же опорного. Мост питается от AVDD, и опорное — AVDD (VREF = 11): сигнал моста и опорное растут вместе, и код от AVDD не зависит. В модели мост 1 мВ/В, однократное на 20 SPS с 50/60:

Опорное AVDD Без груза Половина Полная Полная − без груза
AVDD 3,0 В 4295 541 166 1 078 037 1 073 742
AVDD 3,3 В 3905 540 775 1 077 646 1 073 741
AVDD 3,6 В 3579 540 450 1 077 321 1 073 742
своё 2,048 В 3,0 В 6291 792 723 1 579 155 1 572 864
своё 2,048 В 3,3 В 6291 871 367 1 736 442 1 730 151
своё 2,048 В 3,6 В 6291 950 010 1 893 728 1 887 437

С опорным AVDD размах не меняется — ±1 код на ±9 % питания. Со своим 2,048 В он идёт за питанием один к одному: на 3,6 В весы покажут на 9,1 % больше. Код без груза — смещение (ниже). ads_ratio(code, 1 000 000 000, ×128) даёт отношение в нВ на вольт питания: без груза — 3637 нВ/В, полная — 1 003 636 нВ/В, разность — 999 999 нВ/В, то есть 1 мВ/В. Граммы из этого — тарой и грузом, 102.6.

Если мост далеко и провода питания длинные, падение на них тоже уходит в результат. Тогда опорное снимают прямо на мосту отдельной парой на REFP0/REFN0 (VREF = 01) — это шестипроводный тензодатчик; в модели не проверялось.

PT100 по трём проводам. Ток для термометра даёт сама микросхема: IDAC = 101 — 500 мкА, IDAC1 выведен на AIN2, IDAC2 — на AIN3 (I1MUX = 011, I2MUX = 100). IDAC1 идёт по проводу L1 через PT100, IDAC2 — по проводу L2 на нижний вывод PT100; оба сходятся в третий провод и через Rref = 1,62 кОм — на землю. Вход — AIN2 − AIN3 (MUX = 0101), опорное — падение на Rref, REFP0 − REFN0 (VREF = 01):

Ток в результат не входит — это та же логометрия, что в 176б.3, только делитель заменён источниками тока. Провода L1 и L2 вычитаются — та же трёхпроводная схема, что в 176б.4. Rref внизу поднимает оба входа: при 200 °C в модели AIN2 = 1,7086 В, AIN3 = 1,6205 В, REFP0 = 1,62 В, синфазное 1,6646 В, разность 88,105 мВ при диапазоне ±101,25 мВ (1,62 В / 16) — усилитель в своих пределах (ниже, 15г.7).

/* PT100 по трём проводам: среднее с токами как есть и с переставленными I1MUX и I2MUX */
int ads_rtd3(ads_t *d, uint32_t n, int32_t out[3])
{
    uint8_t keep = d->cfg[3];
    uint8_t swap = (uint8_t)((keep >> ADS_I1MUX_POS & 7u) << ADS_I2MUX_POS | (keep >> ADS_I2MUX_POS & 7u) << ADS_I1MUX_POS
                             | (keep & 3u));
    int rc = ads_average(d, n, &out[0]);
    if (rc == ADS_OK)
        rc = ads_set(d, 3, swap);
    if (rc == ADS_OK)
        rc = ads_average(d, n, &out[1]);
    int rc2 = ads_set(d, 3, keep);
    int64_t s = (int64_t)out[0] + out[1];
    out[2] = (int32_t)(s >= 0 ? (s + 1) / 2 : -((-s + 1) / 2));
    return rc != ADS_OK ? rc : rc2;
}

Модель сравнила опорное REF0 с внутренним 2,048 В (усиление 8, R — по закону Ома с номинальными 500 мкА), когда оба тока отличаются от номинала на ±1 %:

T R, мОм REF0: −1 % / 0 / +1 % своё 2,048 В: −1 % / 0 / +1 %
0 °C 100 000 100 024 / 100 024 / 100 024 99 024 / 100 024 / 101 024
100 °C 138 505 138 530 / 138 530 / 138 529 137 144 / 138 530 / 139 914
200 °C 175 856 175 880 / 175 880 / 175 880 174 122 / 175 880 / 177 638

С REF0 ток не важен совсем. Со своим опорным 1 % тока — ±1 Ом при 0 °C, около 2,6 °C. Постоянные +24 мОм — смещение 12 мкВ на токе 0,5 мА.

Разброс двух токов. Если IDAC1 и IDAC2 различаются, провода вычитаются не до конца, а разность токов умножается на R. ads_rtd3 мерит дважды — с токами как есть и с переставленными I1MUX/I2MUX — и берёт среднее: разность уходит. В модели, 100 °C, провода по 0,35 Ом, четыре результата на каждое измерение:

Случай Как есть Переставлены Среднее
токи равны 138 530 мОм, 0,0646 °C 138 530 мОм 138 530 мОм, 0,0646 °C
токи различаются на 0,4 % 138 808 мОм, 0,7976 °C 138 251 мОм, −0,671 °C 138 530 мОм, 0,0646 °C
токи равны, L1 на 5 % длиннее 138 547 мОм, 0,1094 °C 138 547 мОм 138 547 мОм, 0,1094 °C

0,4 % разброса — почти 0,8 °C, перестановка убирает их полностью. Неравные провода перестановка не лечит: +17 мОм, 0,0448 °C сверх смещения — ровно столько, сколько в 176б.4. 0,0646 °C во всех строках — смещение; его снимает калибровка.

Калибровка смещения. MUX = 1110 закорачивает оба входа на (AVDD + AVSS) / 2: всё, что микросхема при этом покажет, — её смещение. Мерить его надо с тем же усилением и на той же скорости, что и сигнал.

/* смещение: MUX = 1110 замыкает входы на (AVDD + AVSS) / 2; усиление то же, что у измерения */
int ads_offset(ads_t *d, uint32_t n, int32_t *off)
{
    uint8_t keep = d->cfg[0];
    int rc = ads_set(d, 0, (uint8_t)(ADS_SHORT << ADS_MUX_POS | (keep & 0x0Fu)));
    if (rc == ADS_OK)
        rc = ads_average(d, n, off);
    int rc2 = ads_set(d, 0, keep);
    return rc != ADS_OK ? rc : rc2;
}

В модели (смещение +12 мкВ, шум включён) мост без груза дал в среднем из 16 результатов 3894 кода — 18,13 г на пустых весах. ads_offset из 16 результатов — 3914 кодов, 12,029 мкВ; регистры вернулись к 0E 04 D0 00. После вычитания восемь средних из 16 — от −16 до 3 кодов, меньше 0,075 г. Это не тара: тара снимает и смещение, и начальный перекос моста (102.6), а калибровка по MUX = 1110 — только АЦП, зато без разгрузки весов, когда угодно и для PT100 тоже.


15г.7 Грабли

Нет паузы после RESET. Klipper пишет: сброс длится 50 мкс. На 1,125 МГц команда RESET кончается через 7,1 мкс, и WREG сразу за ней попадает в глухое время. В модели все пять байт WREG пропали, и регистры через 100 мкс прочитались 00 00 00 00 вместо 0E 04 D0 00. С паузой ADS_RESET_US настройка прошла и проверилась.

CS поднят посреди команды. Подъём CS обрывает команду. RREG одним кадром, а нули для ответа — другим: в модели пришло 00 00 00 00. Три байта результата по кадру на байт: прочитан только старший — 524 288 (0x080000) вместо 540 775 (0x084067). Кадр — одна команда со всеми её данными (ads_frame). Так ли поведёт себя микросхема во втором и третьем кадре, не проверено; в модели DOUT без данных отдаёт нули.

Вход за пределами усилителя по синфазному. У включённого PGA выходы Vcm ± усиление × Vd / 2 должны оставаться от AVSS + 0,2 В до AVDD − 0,2 В (по документации производителя). Разность 5 мВ, усиление 128 — пределы синфазного от 0,52 до 2,78 В:

Синфазное Код Показано
1,65 В 2 627 731 5012 мкВ
0,60 В 2 627 731 5012 мкВ
0,45 В 2 341 011 4465 мкВ
0,30 В 1 726 611 3293 мкВ
0,10 В 907 411 1731 мкВ

Ошибки нет, флага нет — просто неверное число. Вход против AVSS — тот же случай: 5 мВ на AIN0 против AVSS с усилением 128 показали 969 мкВ, с усилением 4 и включённым PGA — 12 мкВ (одно смещение), с усилением 4 и PGA_BYPASS = 1 — 5012 мкВ. Klipper поэтому при входе против AVSS обходит PGA принудительно. Мост от AVDD сидит на AVDD / 2, PT100 поднят Rref — у них этой беды нет.

Turbo вместо нормального. DR = 000 в turbo — 40 SPS, а не 20, и поле 50/60 не действует: 5,54 мкВ помехи 50 Гц вместо нуля (15г.5). ads_sps учитывает режим; таблица скоростей без MODE — нет.

SPI в режиме 0. Микросхема читает DIN по спаду, а ведущий в режиме 0 меняет данные тоже по спаду — команды доходят. Но ответ ведущий защёлкивает на такт раньше (18.2): в модели проверка после RESET прошла — нули сдвигом не испортить, — а настройка вернула 07 02 68 00 вместо 0E 04 D0 00 и ADS_E_VERIFY. Режим 1: CPHA = 1.

Результат как uint32_t. −1 мВ на усилении 128 — код −517 997 (0xF81893); без знакового расширения — 16 259 219 (15г.4).

Упёрлись в край. 20 мВ на диапазоне ±16 мВ — код 8 388 607, и ads_read_data вернула ADS_E_RANGE: край диапазона — не измерение.

Опоздал с чтением. Отсчёт, не дочитанный к следующему спаду DRDY, теряется; в модели при конце DMA через 600 мкс на 2000 SPS — 100 переполнений на 100 отсчётов (15г.4).


15г.8 Итоги

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

ads15e.h — команды, поля четырёх регистров, коды, порт и функции драйвера.

/* ads15e.h — раздел 15г: драйвер внешнего 24-битного АЦП ADS1220 по SPI без регистров микроконтроллера.
 * Драйвер говорит с микросхемой только через функции порта (ads_port_*): в прошивке fw15e.c их делают
 * SPI1, EXTI0 и DMA1 STM32F103, в стенде bench15e.c — модель model15e.py.
 * Коды команд RESET, START/SYNC, RREG, WREG, раскладка полей MUX, GAIN, PGA_BYPASS, DR, MODE, CM, VREF,
 * значения после сброса и скорости сверены с драйвером Klipper (klippy/extras/ads1220.py); остальное —
 * по документации производителя, документа в проекте нет, с микросхемой не сверено. */
#ifndef ADS15E_H
#define ADS15E_H
#include <stdint.h>

/* ── команды: один байт в кадре CS ─────────────────────────────────────── */
#define ADS_CMD_POWERDOWN 0x02u
#define ADS_CMD_RESET     0x06u
#define ADS_CMD_START     0x08u         /* START/SYNC: начать (заново) преобразование */
#define ADS_CMD_RDATA     0x10u         /* следующие три байта — результат */
#define ADS_CMD_RREG      0x20u         /* 0010 rrnn: с регистра rr, nn + 1 байт */
#define ADS_CMD_WREG      0x40u         /* 0100 rrnn */

/* ── регистр 0 ───────────────────────────────────────────────────────── */
#define ADS_MUX_POS       4u            /* 7:4 — входы AINP и AINN */
#define ADS_GAIN_POS      1u            /* 3:1 — усиление 1…128 */
#define ADS_PGA_BYPASS    0x01u         /* обойти PGA: только при усилении 1, 2, 4 */
/* ── регистр 1 ───────────────────────────────────────────────────────── */
#define ADS_DR_POS        5u            /* 7:5 — скорость */
#define ADS_MODE_POS      3u            /* 4:3 — 00 нормальный, 01 экономичный, 10 turbo */
#define ADS_CM            0x04u         /* 1 — непрерывное преобразование */
#define ADS_TS            0x02u         /* датчик температуры вместо входов */
#define ADS_BCS           0x01u         /* токи 10 мкА для поиска обрыва */
/* ── регистр 2 ───────────────────────────────────────────────────────── */
#define ADS_VREF_POS      6u            /* 7:6 — опорное */
#define ADS_5060_POS      4u            /* 5:4 — подавление 50/60 Гц (только 20 SPS) */
#define ADS_PSW           0x08u         /* ключ на AVSS для питания моста */
#define ADS_IDAC_POS      0u            /* 2:0 — величина обоих токов возбуждения */
/* ── регистр 3 ───────────────────────────────────────────────────────── */
#define ADS_I1MUX_POS     5u            /* 7:5 — куда выведен IDAC1 */
#define ADS_I2MUX_POS     2u            /* 4:2 — куда выведен IDAC2 */
#define ADS_DRDYM         0x02u         /* «готово» ещё и на DOUT/DRDY */

enum { ADS_AIN0_AIN1, ADS_AIN0_AIN2, ADS_AIN0_AIN3, ADS_AIN1_AIN2, ADS_AIN1_AIN3, ADS_AIN2_AIN3,
       ADS_AIN1_AIN0, ADS_AIN3_AIN2, ADS_AIN0_AVSS, ADS_AIN1_AVSS, ADS_AIN2_AVSS, ADS_AIN3_AVSS,
       ADS_MON_REF4, ADS_MON_AVDD4, ADS_SHORT };        /* MUX 0000…1110; 1110 — входы закорочены */
enum { ADS_G1, ADS_G2, ADS_G4, ADS_G8, ADS_G16, ADS_G32, ADS_G64, ADS_G128 };   /* GAIN 000…111 */
enum { ADS_DR_20, ADS_DR_45, ADS_DR_90, ADS_DR_175, ADS_DR_330, ADS_DR_600, ADS_DR_1000 };  /* в turbo — вдвое */
enum { ADS_MODE_NORMAL, ADS_MODE_DUTY, ADS_MODE_TURBO };
enum { ADS_VREF_INT, ADS_VREF_REF0, ADS_VREF_REF1, ADS_VREF_AVDD };  /* 2,048 В; REFP0−REFN0; REFP1−REFN1; AVDD−AVSS */
enum { ADS_5060_OFF, ADS_5060_BOTH, ADS_5060_50, ADS_5060_60 };
enum { ADS_IDAC_OFF, ADS_IDAC_10, ADS_IDAC_50, ADS_IDAC_100, ADS_IDAC_250, ADS_IDAC_500, ADS_IDAC_1000,
       ADS_IDAC_1500 };                                  /* мкА */
enum { ADS_IMUX_OFF, ADS_IMUX_AIN0, ADS_IMUX_AIN1, ADS_IMUX_AIN2, ADS_IMUX_AIN3, ADS_IMUX_REFP0,
       ADS_IMUX_REFN0 };

#define ADS_CFG0(mux, g, byp)   ((uint8_t)((mux) << ADS_MUX_POS | (g) << ADS_GAIN_POS | (byp)))
#define ADS_CFG1(dr, mode, cm)  ((uint8_t)((dr) << ADS_DR_POS | (mode) << ADS_MODE_POS | (cm)))
#define ADS_CFG2(vref, f, idac) ((uint8_t)((vref) << ADS_VREF_POS | (f) << ADS_5060_POS | (idac)))
#define ADS_CFG3(i1, i2, drdym) ((uint8_t)((i1) << ADS_I1MUX_POS | (i2) << ADS_I2MUX_POS | (drdym)))

#define ADS_RESET_US      50u           /* после RESET микросхема глуха — пауза, как в Klipper */
#define ADS_TIMEOUT_US    200000u       /* дольше любого первого результата, с запасом */
#define ADS_RING          64u           /* кольцо отсчётов потока по DMA */

enum { ADS_OK = 0, ADS_E_TIMEOUT = -1, ADS_E_VERIFY = -2, ADS_E_RESET = -3, ADS_E_RANGE = -4 };

typedef struct {
    uint8_t cfg[4];                     /* что драйвер записал в регистры 0…3 */
    uint32_t frames, bytes;             /* кадров CS и байт по SPI */
    /* поток по DRDY и DMA: заполняют обработчики прерываний */
    uint8_t dma_tx[3], dma_rx[3];
    volatile uint8_t dma_busy;
    volatile uint32_t head, overruns;
    int32_t ring[ADS_RING];
} ads_t;

/* ── порт: в прошивке — SPI1, EXTI0 и DMA1, в стенде — строки к модели ─── */
void ads_port_cs(int level);                                     /* 0 — микросхема выбрана */
void ads_port_xfer(const uint8_t *tx, uint8_t *rx, uint32_t n);  /* n байт туда и обратно, CS не трогает */
void ads_port_delay_us(uint32_t us);
int ads_port_wait_drdy(uint32_t timeout_us);    /* спад DRDY (флаг EXTI) за timeout_us: 1 — был и снят */
int ads_port_miso(void);                        /* уровень DOUT/DRDY: нужен для DRDYM */
void ads_port_dma_start(const uint8_t *tx, uint8_t *rx, uint32_t n);   /* запустить и не ждать */

/* ── драйвер ──────────────────────────────────────────────────────────── */
void ads_frame(ads_t *d, const uint8_t *tx, uint8_t *rx, uint32_t n);
void ads_cmd(ads_t *d, uint8_t cmd);
void ads_wreg(ads_t *d, uint32_t reg, const uint8_t *v, uint32_t n);
void ads_rreg(ads_t *d, uint32_t reg, uint8_t *v, uint32_t n);
int ads_reset(ads_t *d);
int ads_config(ads_t *d, const uint8_t cfg[4]);
int ads_set(ads_t *d, uint32_t reg, uint8_t v);
int32_t ads_s24(const uint8_t b[3]);
int ads_read_data(ads_t *d, int32_t *code);
int ads_read_direct(ads_t *d, int32_t *code);
int ads_single(ads_t *d, int32_t *code);
int ads_next(ads_t *d, int32_t *code);
int ads_average(ads_t *d, uint32_t n, int32_t *mean);
int ads_offset(ads_t *d, uint32_t n, int32_t *off);
int ads_rtd3(ads_t *d, uint32_t n, int32_t out[3]);
int ads_wait_dout(ads_t *d, uint32_t timeout_us, int32_t *code);
uint32_t ads_sps(const ads_t *d);
int32_t ads_ratio(int32_t code, uint32_t scale, uint32_t gain);
void ads_stream_start(ads_t *d);
void ads_on_drdy(ads_t *d);
void ads_on_dma_done(ads_t *d);

#endif

ads15e.c — драйвер: кадры, сброс и настройка, 24 бита, однократное и непрерывное, смещение, PT100, поток.

/* ads15e.c — раздел 15г: драйвер ADS1220 по SPI. Один и тот же файл собирается в прошивку (armclang,
 * Cortex-M3) и в стенд (arm-none-eabi-gcc, QEMU): микроконтроллера он не знает, только порт ads_port_*. */
#include "ads15e.h"

static const uint16_t sps_normal[7] = {20, 45, 90, 175, 330, 600, 1000};   /* DR 000…110 */

/* один кадр: CS вниз, n байт туда и обратно, CS вверх — команда и её данные не разрываются */
void ads_frame(ads_t *d, const uint8_t *tx, uint8_t *rx, uint32_t n)
{
    ads_port_cs(0);
    ads_port_xfer(tx, rx, n);
    ads_port_cs(1);
    d->frames++;
    d->bytes += n;
}

void ads_cmd(ads_t *d, uint8_t cmd)
{
    uint8_t rx;
    ads_frame(d, &cmd, &rx, 1);
}

void ads_wreg(ads_t *d, uint32_t reg, const uint8_t *v, uint32_t n)
{
    uint8_t tx[5], rx[5];
    tx[0] = (uint8_t)(ADS_CMD_WREG | (reg & 3u) << 2 | ((n - 1u) & 3u));
    for (uint32_t i = 0; i < n; i++)
        tx[1 + i] = v[i];
    ads_frame(d, tx, rx, n + 1u);
}

void ads_rreg(ads_t *d, uint32_t reg, uint8_t *v, uint32_t n)
{
    uint8_t tx[5] = {(uint8_t)(ADS_CMD_RREG | (reg & 3u) << 2 | ((n - 1u) & 3u)), 0, 0, 0, 0}, rx[5];
    ads_frame(d, tx, rx, n + 1u);
    for (uint32_t i = 0; i < n; i++)
        v[i] = rx[1 + i];                               /* в ответ на байт команды пришёл мусор */
}

/* RESET, пауза, и все четыре регистра должны прочитаться нулями */
int ads_reset(ads_t *d)
{
    uint8_t r[4];
    ads_cmd(d, ADS_CMD_RESET);
    ads_port_delay_us(ADS_RESET_US);
    ads_rreg(d, 0, r, 4);
    for (uint32_t i = 0; i < 4u; i++) {
        d->cfg[i] = 0;
        if (r[i])
            return ADS_E_RESET;
    }
    return ADS_OK;
}

/* записать четыре регистра одним WREG и прочитать обратно — как делает Klipper */
int ads_config(ads_t *d, const uint8_t cfg[4])
{
    uint8_t r[4];
    ads_wreg(d, 0, cfg, 4);
    ads_rreg(d, 0, r, 4);
    for (uint32_t i = 0; i < 4u; i++) {
        d->cfg[i] = cfg[i];
        if (r[i] != cfg[i])
            return ADS_E_VERIFY;
    }
    return ADS_OK;
}

int ads_set(ads_t *d, uint32_t reg, uint8_t v)
{
    uint8_t r;
    ads_wreg(d, reg, &v, 1);
    ads_rreg(d, reg, &r, 1);
    d->cfg[reg & 3u] = v;
    return r == v ? ADS_OK : ADS_E_VERIFY;
}

/* 24 бита в дополнительном коде, старший байт первым → int32_t */
int32_t ads_s24(const uint8_t b[3])
{
    int32_t v = (int32_t)((uint32_t)b[0] << 16 | (uint32_t)b[1] << 8 | b[2]);
    return v & 0x800000 ? v - 0x1000000 : v;
}

static int range(int32_t code)
{
    return code == 0x7FFFFF || code == -0x800000 ? ADS_E_RANGE : ADS_OK;   /* упёрлись в край */
}

/* RDATA и три байта результата — в одном кадре */
int ads_read_data(ads_t *d, int32_t *code)
{
    uint8_t tx[4] = {ADS_CMD_RDATA, 0, 0, 0}, rx[4];
    ads_frame(d, tx, rx, 4);
    *code = ads_s24(rx + 1);
    return range(*code);
}

/* после спада DRDY результат выдвигается сам: 24 такта без команды, на DIN — нули */
int ads_read_direct(ads_t *d, int32_t *code)
{
    uint8_t tx[3] = {0, 0, 0}, rx[3];
    ads_frame(d, tx, rx, 3);
    *code = ads_s24(rx);
    return range(*code);
}

/* однократное (CM = 0): START, спад DRDY, RDATA */
int ads_single(ads_t *d, int32_t *code)
{
    (void)ads_port_wait_drdy(0);                        /* снять старый флаг */
    ads_cmd(d, ADS_CMD_START);
    if (!ads_port_wait_drdy(ADS_TIMEOUT_US))
        return ADS_E_TIMEOUT;
    return ads_read_data(d, code);
}

/* непрерывное (CM = 1): следующий результат по спаду DRDY */
int ads_next(ads_t *d, int32_t *code)
{
    if (!ads_port_wait_drdy(ADS_TIMEOUT_US))
        return ADS_E_TIMEOUT;
    return ads_read_direct(d, code);
}

int ads_average(ads_t *d, uint32_t n, int32_t *mean)
{
    int64_t sum = 0;
    int32_t c;
    for (uint32_t i = 0; i < n; i++) {
        int rc = d->cfg[1] & ADS_CM ? ads_next(d, &c) : ads_single(d, &c);
        if (rc != ADS_OK)
            return rc;
        sum += c;
    }
    *mean = (int32_t)(sum >= 0 ? (sum + n / 2u) / n : -((-sum + n / 2u) / n));
    return ADS_OK;
}

/* смещение: MUX = 1110 замыкает входы на (AVDD + AVSS) / 2; усиление то же, что у измерения */
int ads_offset(ads_t *d, uint32_t n, int32_t *off)
{
    uint8_t keep = d->cfg[0];
    int rc = ads_set(d, 0, (uint8_t)(ADS_SHORT << ADS_MUX_POS | (keep & 0x0Fu)));
    if (rc == ADS_OK)
        rc = ads_average(d, n, off);
    int rc2 = ads_set(d, 0, keep);
    return rc != ADS_OK ? rc : rc2;
}

/* PT100 по трём проводам: среднее с токами как есть и с переставленными I1MUX и I2MUX */
int ads_rtd3(ads_t *d, uint32_t n, int32_t out[3])
{
    uint8_t keep = d->cfg[3];
    uint8_t swap = (uint8_t)((keep >> ADS_I1MUX_POS & 7u) << ADS_I2MUX_POS | (keep >> ADS_I2MUX_POS & 7u) << ADS_I1MUX_POS
                             | (keep & 3u));
    int rc = ads_average(d, n, &out[0]);
    if (rc == ADS_OK)
        rc = ads_set(d, 3, swap);
    if (rc == ADS_OK)
        rc = ads_average(d, n, &out[1]);
    int rc2 = ads_set(d, 3, keep);
    int64_t s = (int64_t)out[0] + out[1];
    out[2] = (int32_t)(s >= 0 ? (s + 1) / 2 : -((-s + 1) / 2));
    return rc != ADS_OK ? rc : rc2;
}

/* DRDYM = 1: «готово» — ноль на DOUT/DRDY при выбранной микросхеме; отдельный провод DRDY не нужен */
int ads_wait_dout(ads_t *d, uint32_t timeout_us, int32_t *code)
{
    uint8_t tx[3] = {0, 0, 0}, rx[3];
    ads_port_cs(0);
    for (uint32_t t = 0; ads_port_miso(); t += 10u) {
        if (t >= timeout_us) {
            ads_port_cs(1);
            return ADS_E_TIMEOUT;
        }
        ads_port_delay_us(10);
    }
    ads_port_xfer(tx, rx, 3);
    ads_port_cs(1);
    d->frames++;
    d->bytes += 3;
    *code = ads_s24(rx);
    return range(*code);
}

/* скорость по DR и MODE: в turbo вдвое; экономичный режим здесь не разбирается — 0 */
uint32_t ads_sps(const ads_t *d)
{
    uint32_t dr = d->cfg[1] >> ADS_DR_POS & 7u, mode = d->cfg[1] >> ADS_MODE_POS & 3u;
    if (dr > 6u || mode == ADS_MODE_DUTY || mode == 3u)
        return 0;
    return mode == ADS_MODE_TURBO ? 2u * sps_normal[dr] : sps_normal[dr];
}

/* code × scale / (усиление × 2^23) с округлением, без деления: усиление — степень двойки.
 * scale = опорное в микровольтах — получаются микровольты на входе; 2 × Rref — сопротивление */
int32_t ads_ratio(int32_t code, uint32_t scale, uint32_t gain)
{
    uint32_t sh = 23u + (gain & 7u);
    int64_t x = (int64_t)code * scale;
    int64_t half = (int64_t)1 << (sh - 1u);
    return (int32_t)(x >= 0 ? (x + half) >> sh : -((-x + half) >> sh));
}

/* поток: спад DRDY запускает DMA на три байта, конец DMA кладёт отсчёт в кольцо */
void ads_stream_start(ads_t *d)
{
    d->head = d->overruns = 0;
    d->dma_busy = 0;
    d->dma_tx[0] = d->dma_tx[1] = d->dma_tx[2] = 0;
    ads_cmd(d, ADS_CMD_START);
}

void ads_on_drdy(ads_t *d)
{
    if (d->dma_busy) {                                  /* прошлый отсчёт ещё не дочитан */
        d->overruns++;
        return;
    }
    d->dma_busy = 1;
    ads_port_cs(0);
    ads_port_dma_start(d->dma_tx, d->dma_rx, 3);
}

void ads_on_dma_done(ads_t *d)
{
    ads_port_cs(1);
    d->ring[d->head % ADS_RING] = ads_s24(d->dma_rx);
    d->head++;
    d->frames++;
    d->bytes += 3;
    d->dma_busy = 0;
}

fw15e.c — прошивка STM32F103: SPI1 в режиме 1, DRDY на EXTI0, поток по DMA1, мост и PT100.

/* fw15e.c — раздел 15г: ADS1220 на STM32F103 (BluePill), 72 МГц, APB2 = 72 МГц.
 * SPI1 без ремапа: PA5 — SCLK, PA6 — DOUT/DRDY (MISO), PA7 — DIN (MOSI), режим 1 (CPOL = 0, CPHA = 1),
 * 72 МГц / 64 = 1,125 МГц; PA4 — CS, обычный выход; PB0 — DRDY, вход с подтяжкой, EXTI0 по спаду.
 * Мост на AIN0/AIN1 от AVDD, опорное — AVDD (логометрия); PT100 по трём проводам на AIN2/AIN3, токи IDAC1 и
 * IDAC2 по 500 мкА, Rref 1,62 кОм на REFP0/REFN0. Мост — поток по DRDY и DMA1 (каналы 2 и 3) секунду, затем
 * PT100 — одним измерением с перестановкой токов. Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "ads15e.h"

#define CPU_HZ          72000000u
#define RREF_MOHM       1620000u        /* Rref 1,62 кОм: сюда — измеренное значение (176б.3) */

static ads_t adc;
static volatile uint8_t drdy, streaming;
static const uint8_t cfg_bridge[4] = {ADS_CFG0(ADS_AIN0_AIN1, ADS_G128, 0), ADS_CFG1(ADS_DR_20, ADS_MODE_NORMAL, ADS_CM),
                                      ADS_CFG2(ADS_VREF_AVDD, ADS_5060_BOTH, ADS_IDAC_OFF), ADS_CFG3(0, 0, 0)};
static const uint8_t cfg_rtd[4] = {ADS_CFG0(ADS_AIN2_AIN3, ADS_G16, 0), ADS_CFG1(ADS_DR_20, ADS_MODE_NORMAL, ADS_CM),
                                   ADS_CFG2(ADS_VREF_REF0, ADS_5060_BOTH, ADS_IDAC_500),
                                   ADS_CFG3(ADS_IMUX_AIN2, ADS_IMUX_AIN3, 0)};
volatile int32_t bridge_nvv, rtd_mohm, offset_code;
volatile uint32_t 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 spi1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_SPI1EN;
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    GPIOA->BSRR = 1u << 4;                              /* CS вверх до настройки вывода */
    /* PA4 — выход 50 МГц (0011), PA5 и PA7 — альтернативная функция push-pull (1011), PA6 — вход (0100) */
    GPIOA->CRL = (GPIOA->CRL & 0x0000FFFFu) | 0xB4B30000u;
    GPIOB->CRL = (GPIOB->CRL & ~0xFu) | 0x8u;           /* PB0 — вход с подтяжкой (1000) */
    GPIOB->BSRR = 1u << 0;
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_CPHA | SPI_CR1_BR_2 | SPI_CR1_BR_0 | SPI_CR1_SSM | SPI_CR1_SSI;  /* режим 1, /64 */
    SPI1->CR2 = SPI_CR2_RXDMAEN | SPI_CR2_TXDMAEN;
    SPI1->CR1 |= SPI_CR1_SPE;
}

void ads_port_cs(int level)
{
    if (level)
        GPIOA->BSRR = 1u << 4;
    else
        GPIOA->BRR = 1u << 4;
}

/* обмен опросом флагов: только когда поток по DMA остановлен */
void ads_port_xfer(const uint8_t *tx, uint8_t *rx, uint32_t n)
{
    for (uint32_t i = 0; i < n; i++) {
        while (!(SPI1->SR & SPI_SR_TXE)) {
        }
        SPI1->DR = tx[i];
        while (!(SPI1->SR & SPI_SR_RXNE)) {
        }
        rx[i] = (uint8_t)SPI1->DR;
    }
    while (SPI1->SR & SPI_SR_BSY) {                     /* перед подъёмом CS — раздел 18.5 */
    }
}

/* приём — канал 2 (SPI1_RX), передача — канал 3 (SPI1_TX); приём включается первым */
void ads_port_dma_start(const uint8_t *tx, uint8_t *rx, uint32_t n)
{
    DMA1_Channel2->CCR = 0;
    DMA1_Channel3->CCR = 0;
    DMA1->IFCR = DMA_IFCR_CGIF2 | DMA_IFCR_CGIF3;
    DMA1_Channel2->CPAR = (uint32_t)&SPI1->DR;
    DMA1_Channel2->CMAR = (uint32_t)rx;
    DMA1_Channel2->CNDTR = n;
    DMA1_Channel2->CCR = DMA_CCR2_MINC | DMA_CCR2_TCIE | DMA_CCR2_EN;
    DMA1_Channel3->CPAR = (uint32_t)&SPI1->DR;
    DMA1_Channel3->CMAR = (uint32_t)tx;
    DMA1_Channel3->CNDTR = n;
    DMA1_Channel3->CCR = DMA_CCR3_MINC | DMA_CCR3_DIR | DMA_CCR3_EN;
}

/* DRDY: спад на PB0 */
void EXTI0_IRQHandler(void)
{
    EXTI->PR = 1u << 0;
    if (streaming)
        ads_on_drdy(&adc);                              /* CS вниз и DMA на три байта */
    else
        drdy = 1;
}

/* приняты все три байта: последний бит уже на месте, BSY не ждём */
void DMA1_Channel2_IRQHandler(void)
{
    DMA1->IFCR = DMA_IFCR_CGIF2 | DMA_IFCR_CGIF3;
    DMA1_Channel2->CCR = 0;
    DMA1_Channel3->CCR = 0;
    ads_on_dma_done(&adc);                              /* CS вверх, отсчёт — в кольцо */
}

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

int ads_port_miso(void)
{
    return (GPIOA->IDR >> 6) & 1u;
}

static void irq_init(void)
{
    AFIO->EXTICR[0] = (AFIO->EXTICR[0] & ~0xFu) | 0x1u; /* EXTI0 ← PB0 */
    EXTI->FTSR |= 1u << 0;
    EXTI->IMR |= 1u << 0;
    NVIC_SetPriority(DMA1_Channel2_IRQn, 1);
    NVIC_SetPriority(EXTI0_IRQn, 2);
    NVIC_EnableIRQ(DMA1_Channel2_IRQn);
    NVIC_EnableIRQ(EXTI0_IRQn);
}

/* секунда моста потоком: 20 отсчётов на 20 SPS, среднее за вычетом смещения — в нВ/В */
static int bridge_second(void)
{
    if (ads_config(&adc, cfg_bridge) != ADS_OK)
        return ADS_E_VERIFY;
    streaming = 1;
    ads_stream_start(&adc);
    uint32_t t0 = DWT->CYCCNT;
    while (adc.head < 20u) {
        if (expired(t0, 2000000u))
            break;
    }
    streaming = 0;
    while (adc.dma_busy) {
    }
    if (adc.head < 20u)
        return ADS_E_TIMEOUT;
    int64_t s = 0;
    for (uint32_t i = 0; i < 20u; i++)
        s += adc.ring[i];
    bridge_nvv = ads_ratio((int32_t)(s / 20) - offset_code, 1000000000u, ADS_G128);
    return ADS_OK;
}

static int rtd_once(void)
{
    int32_t out[3];
    if (ads_config(&adc, cfg_rtd) != ADS_OK)
        return ADS_E_VERIFY;
    ads_cmd(&adc, ADS_CMD_START);
    int rc = ads_rtd3(&adc, 4, out);
    if (rc == ADS_OK)
        rtd_mohm = ads_ratio(out[2], 2u * RREF_MOHM, ADS_G16);   /* ток в Rref — сумма двух IDAC */
    return rc;
}

int main(void)
{
    int32_t off;
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    spi1_init();
    irq_init();
    while (ads_reset(&adc) != ADS_OK)
        ads_port_delay_us(100000u);                     /* нет ответа: провод или питание */
    if (ads_config(&adc, cfg_bridge) == ADS_OK) {
        ads_cmd(&adc, ADS_CMD_START);
        if (ads_offset(&adc, 16, &off) == ADS_OK)
            offset_code = off;
    }
    for (;;) {
        if (bridge_second() != ADS_OK)
            errors++;
        if (rtd_once() != ADS_OK)
            errors++;
    }
}

Дальше: раздел 16 «UART» — самый первый интерфейс, который подключают.

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