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

15д. Прецизионный АЦП ADS1256 по SPI: 24 бита, 8 входов, PGA до 64, буфер, скорости до 30 000 отсчётов в секунду, самокалибровка и синхронизация

В 15г ADS1220 мерил мост и PT100: четыре входа, до 2000 отсчётов в секунду, опорное от датчика. ADS1256 — для другой задачи: много входов и высокая скорость. Восемь входов (или четыре пары), усиление до 64, входной буфер, скорости от 2,5 до 30 000 отсчётов в секунду, свой кварц 7,68 МГц и калибровка нуля и усиления по команде. Здесь — драйвер: команды и одиннадцать регистров, пауза t6 между командой и чтением, режим SPI 1 и предел такта, плата-модуль с питанием 5 В и логикой 3,3 В, буфер и пределы входа, скорость и шум, непрерывное чтение RDATAC, обход восьми входов со SYNC и WAKEUP — сколько результатов отбросить после смены входа и какая скорость остаётся на канал, калибровка после смены усиления и скорости и грабли. Что такое дельта-сигма, фильтр-усреднение, PGA и 24 бита в дополнительном коде — в 15г; как встроенный АЦП догоняет соседний канал — в 180.3, здесь причина другая.

Опоры: RM0008 (DocID13902 Rev 17): выводы SPI — таблица 25, с. 166; без ремапа SPI1 — PA4–PA7, с. 185; AFIO_EXTICR1, с. 190; EXTI_FTSR, с. 211; BR[2:0] = 100 — fPCLK/32, 101 — fPCLK/64, с. 743; CPOL и CPHA, с. 744; BSY, с. 745. DocID13587 Rev 17 (STM32F103x8/xB): PA4–PA7 — выводы SPI1, PA4–PA7 и PB0 без допуска 5 В, с. 29. Документа на ADS1256 в проекте нет, открытого драйвера на диске тоже: в Klipper и других открытых проектах его нет. Коды команд, адреса и поля регистров, значения после сброса, коды DRATE, такт 7,68 МГц, паузы t6 и t11, предел такта SPI, пределы входа с буфером и без, входное сопротивление — по документации производителя, не проверено; это заложено в модель и сверено с ней. Все числа ниже — прогон драйвера _verify/adc15f/ads15f.c, собранного arm-none-eabi-gcc 12.2.1 под Cortex-M3 и запущенного в QEMU 7.2 (netduino2), против своей модели ADS1256 на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3. Числа говорят о модели, а не о микросхеме.

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

Оригинал Перевод
analog input common (AINCOM) общий вход: минус для несимметричных измерений
input buffer enable (BUFEN) включение входного буфера: вход почти не нагружает источник
absolute input voltage напряжение входа относительно AGND
master clock input (CLKIN), τCLKIN вход тактового генератора; период его такта
DIN-to-DOUT delay (t6) пауза от команды чтения до первого бита ответа
read data continuous (RDATAC), stop read data continuous (SDATAC) выдача каждого результата без команды; её остановка
SYNC / WAKEUP commands остановить фильтр; запустить его заново
multiplexer cycling обход входов по кругу
self offset / self gain calibration (SELFOCAL, SELFGCAL) самокалибровка нуля / усиления
system offset / system gain calibration (SYSOCAL, SYSGCAL) калибровка по сигналу на входе
offset / full-scale calibration registers (OFC, FSC) регистры поправки нуля и шкалы
auto-calibration (ACAL) калибровка сама после смены настроек
noise-free resolution разрядность без шума: размах шума меньше единицы кода

15д.1 Чем он отличается от ADS1220

ADS1220 (15г) ADS1256
входы 4, пары и против AVSS 8 несимметричных против AINCOM или 4 пары, любой со любым
усиление 1…128 1…64
входной буфер нет есть, BUFEN
скорость 20…1000 SPS, в turbo 40…2000 2,5…30 000 SPS, 16 кодов DRATE
такт свой внутренний кварц или генератор 7,68 МГц на CLKIN
опорное своё 2,048 В, REF0, REF1, питание только внешнее, VREFP − VREFN
калибровка нет (смещение — входы накоротко) команды SELFCAL, SYSOCAL, SYSGCAL и регистры OFC, FSC
питание 3,3 В AVDD 5 В, DVDD 1,8…3,6 В

Диапазон — ±2·VREF / PGA: с опорным 2,5 В и усилением 1 это ±5 В, единица кода — 596,047 нВ. Код — 24 бита в дополнительном коде, как в 15г.4; +2·VREF / PGA даёт 0x7FFFFF.

Вверху — подключение платы ADS1256 к STM32F103 по SPI1 в режиме 1 (CPOL = 0, CPHA = 1) на 1,125 МГц при пределе CLKIN / 4 = 1,92 МГц: PA4 — CS обычным выходом, PA5 — SCLK, PA6 — DOUT, PA7 — DIN, PB0 — DRDY на EXTI0 по спаду; на плате кварц 7,68 МГц и опорное 2,5 В, входы AIN0…AIN7 и AINCOM, питание AVDD 5 В и DVDD 3,3 В, потому что PA4–PA7 и PB0 не терпят 5 В. Посередине — цепочка внутри: мультиплексор на восемь входов и AINCOM, буфер BUFEN, PGA от 1 до 64, дельта-сигма модулятор на CLKIN / 4, фильтр sinc5 с усреднением на 2,5…30 000 SPS; в модели первый результат после WAKEUP на 1000 SPS — через 1133,3 мкс. Внизу — шаг обхода входов по кругу: спад DRDY, WREG MUX, SYNC, пауза t11 = 3,125 мкс, WAKEUP, RDATA, пауза t6 = 6,51 мкс и три байта прежнего входа, пока фильтр копит новый; в модели на 1000 SPS шаг — 1175,1 мкс, восемь входов — по 106,4 Гц на вход


15д.2 Подключение к F103, плата и буфер

Вывод ADS1256 STM32F103 Назначение
SCLK PA5 такт SPI1
DIN PA7 MOSI: команды и данные в микросхему
DOUT PA6 MISO: ответ
CS PA4 выбор, обычный выход
DRDY PB0 «готово», EXTI0 по спаду
AVDD, DVDD, AGND, DGND 5 В, 3,3 В, земля общая земля с микроконтроллером

Выводы — как в 15г.2: SPI1 без ремапа на PA4–PA7 (RM0008, с. 185), SCK и MOSI — альтернативная функция push-pull, MISO — вход (таблица 25, с. 166), DRDY — EXTI0 по спаду (AFIO_EXTICR1, с. 190, EXTI_FTSR, с. 211). Режим SPI — 1 (18.2): микросхема выдаёт бит по фронту такта, а читать его надо по спаду (по документации производителя).

static void spi1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_SPI1EN;
    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->CR1 |= SPI_CR1_SPE;
}

Такт SPI. Период SCLK — не меньше 4 τCLKIN (по документации производителя): τ = 1 / 7,68 МГц = 130,208 нс, предел — 520,8 нс, то есть 1,92 МГц. SPI1 на F103 сидит на 72 МГц: BR = 100 даёт 72 / 32 = 2,25 МГц — уже слишком быстро, BR = 101 — 1,125 МГц (с. 743). Байт на 1,125 МГц идёт 7,111 мкс.

Питание. Аналоговой части нужно AVDD = 5 В, цифровой — DVDD от 1,8 до 3,6 В (по документации производителя). Выводы DOUT и DRDY выдают уровень DVDD, а PA4–PA7 и PB0 у F103 5 В не терпят (DocID13587, с. 29): DVDD — 3,3 В. Распространённые платы-модули питаются от 5 В и делают 3,3 В для DVDD своим стабилизатором, на них же кварц 7,68 МГц и опорное 2,5 В — но платы разные: схему своей проверьте, мы платы не проверяли. Если DVDD на плате 5 В, к F103 её напрямую не подключить.

Буфер. Без буфера вход — переключаемые конденсаторы, и источник их заряжает: входное сопротивление около 150 кОм / PGA, с буфером — 80 МОм (по документации производителя). Зато с буфером вход должен лежать от AGND до AVDD − 2 В, без него — от AGND − 0,1 В до AVDD + 0,1 В. В модели, AVDD = 5 В, вход AIN0 против AINCOM, ×1:

AIN0 С буфером Без буфера
1 В 1 000 000 мкВ 1 000 000 мкВ
2,9 В 2 900 000 мкВ 2 900 000 мкВ
3,3 В 3 000 000 мкВ 3 300 000 мкВ
4,5 В 3 000 000 мкВ 4 500 000 мкВ

Выше 3 В с буфером показывает 3 В — без ошибки и без флага. Источник 20 мВ с сопротивлением Rs:

Rs С буфером, ×1 С буфером, ×64 Без буфера, ×1 Без буфера, ×64
0 20 000 мкВ 20 000 мкВ 20 000 мкВ 20 000 мкВ
1 кОм 20 000 мкВ 20 000 мкВ 19 867 мкВ 14 019 мкВ
10 кОм 19 997 мкВ 19 998 мкВ 18 750 мкВ 3797 мкВ

Делитель на 10 кОм без буфера теряет 6,25 % на ×1 и 81 % на ×64. Правило: источник с сопротивлением больше нуля — буфер; сигнал выше 3 В — делитель и буфер или без буфера, но с низкоомным источником (180.2).


15д.3 Команды, регистры и пауза t6

Байт Команда Что делает
0x00, 0xFF WAKEUP пуск после SYNC или STANDBY
0x01 RDATA после паузы t6 — три байта результата
0x03 RDATAC дальше каждый результат по спаду DRDY без команды
0x0F SDATAC перестать; снова слушать команды
0x1r, 0n RREG прочитать n + 1 регистров с адреса r; после паузы t6
0x5r, 0n WREG записать n + 1 регистров с адреса r
0xF0 SELFCAL самокалибровка нуля и усиления
0xF1, 0xF2 SELFOCAL, SELFGCAL только нуля; только усиления
0xF3, 0xF4 SYSOCAL, SYSGCAL ноль и шкала по тому, что сейчас на входе
0xFC SYNC остановить фильтр до WAKEUP
0xFD STANDBY уснуть до WAKEUP
0xFE RESET регистры — по умолчанию
Адрес Регистр Поля После RESET
0 STATUS 7:4 — ID (у ADS1256 — 3), 3 — ORDER, 2 — ACAL, 1 — BUFEN, 0 — DRDY 0x3x
1 MUX 7:4 — PSEL, 3:0 — NSEL: 0…7 — AIN0…AIN7, 1xxx — AINCOM 0x01
2 ADCON 6:5 — CLK (такт на D0/CLKOUT), 4:3 — SDCS (токи обрыва), 2:0 — PGA: 1, 2, 4 … 64 0x20
3 DRATE код скорости (15д.4) 0xF0
4 IO 7:4 — направления D3…D0, 3:0 — их уровни 0xE0
5–7 OFC0…OFC2 поправка нуля, 24 бита со знаком, младший байт первым —
8–10 FSC0…FSC2 поправка шкалы, 24 бита, младший байт первым —

Всё это — по документации производителя; модель и драйвер с ней согласны, но микросхема не подключалась. ADCON = 0x20 после сброса выводит такт 7,68 МГц на D0/CLKOUT; если он не нужен, драйвер пишет CLK = 00 — лишний сигнал рядом со входами ни к чему.

t6. После RDATA и RREG микросхеме нужно 50 τCLKIN, чтобы приготовить ответ: 50 × 130,208 нс = 6,51 мкс. SPI F103 шлёт байты подряд, и следующий такт пришёл бы через полтакта — поэтому драйвер делит кадр на две передачи и ждёт между ними ADS_T6_US = 7 мкс:

/* RREG: два байта команды, пауза t6 — и только потом ответ, всё в одном кадре */
void ads_rreg(ads_t *d, uint32_t reg, uint8_t *v, uint32_t n)
{
    uint8_t tx[11] = {(uint8_t)(ADS_CMD_RREG | (reg & 15u)), (uint8_t)((n - 1u) & 15u)}, rx[11];
    ads_port_cs(0);
    ads_port_xfer(tx, rx, 2);
    ads_port_delay_us(ADS_T6_US);
    ads_port_xfer(tx + 2, v, n);                        /* на DIN — нули */
    ads_port_cs(1);
    d->frames++;
    d->bytes += n + 2u;
}

После SYNC и RDATAC до следующей команды — t11 = 24 τ = 3,125 мкс (ADS_T11_US = 4 мкс, его ждёт ads_cmd); после остальных команд — 4 τ = 0,52 мкс, и столько уходит на подъём и опускание CS и так.

/* SDATAC на случай, если микросхема осталась в RDATAC, RESET, первый результат — и пять регистров */
int ads_reset(ads_t *d, uint8_t r[5])
{
    ads_cmd(d, ADS_CMD_SDATAC);
    (void)ads_port_wait_drdy(0);
    ads_cmd(d, ADS_CMD_RESET);
    if (!ads_port_wait_drdy(ADS_TIMEOUT_US))
        return ADS_E_TIMEOUT;
    ads_rreg(d, ADS_STATUS, r, 5);
    for (uint32_t i = 0; i < 5u; i++)
        d->reg[i] = r[i];
    return r[0] >> ADS_ID_POS == ADS_ID ? ADS_OK : ADS_E_ID;
}

В модели сброс — SDATAC, RESET, первый результат на 30 000 SPS через 166,7 мкс и RREG пяти регистров — занял 237,7 мкс и прочитал 30 01 20 F0 E0: ID = 3, MUX, ADCON, DRATE и IO — как в таблице. Настройка ads_config — кадр WREG 50 03 02 08 00 A1 (буфер, AIN0 против AINCOM, CLKOUT выключен, ×1, 1000 SPS) и чтение обратно — 92,3 мкс; прочитано 32 08 00 A1 E0: в STATUS DRDY = 0, и драйвер сравнивает только ORDER, ACAL и BUFEN. Кадры на 1,125 МГц: команда — 7,1 мкс, RREG одного регистра — 28,3 мкс, RDATA — 35,4 мкс, три байта без команды — 21,3 мкс, WREG четырёх регистров — 42,7 мкс.


15д.4 Код, скорость и шум

Код в вольты. Вход = код × 2·VREF / (PGA × (2²³ − 1)):

/* код → микровольты: вход = код × 2·VREF / (PGA × (2^23 − 1)); PGA 110 и 111 — оба ×64 */
int32_t ads_uv(int32_t code, uint32_t vref_uv, uint32_t pga)
{
    uint32_t sh = pga > ADS_PGA64 ? ADS_PGA64 : pga;
    int64_t x = (int64_t)code * 2 * vref_uv;
    int64_t den = (int64_t)ADS_FS << sh;
    return (int32_t)(x >= 0 ? (x + den / 2) / den : -((-x + den / 2) / den));
}

В модели, VREF = 2,5 В, разность AIN0 − AIN1 после SELFCAL:

PGA Диапазон Единица кода +1 мВ +50 мВ −50 мВ
1 ±5000 мВ 596,047 нВ 1678 83 886 −83 886
8 ±625 мВ 74,506 нВ 13 422 671 089 −671 088
64 ±78,125 мВ 9,313 нВ 107 374 5 368 709 −5 368 708

Крайние коды — 5 000 000 и −5 000 001 мкВ. Несимметричный вход против AINCOM на земле отрицательным не бывает: AIN0 = 0, 1, 3,3 и 4,9 В дали коды 0, 1 677 721, 5 536 481 и 8 220 835 — работает половина шкалы, 23 бита. Нужен знак — AINCOM на 2,5 В или пара входов.

Скорость. В модели фильтр — sinc5 с шагом 1/30 000 с и усреднение N его результатов, N = 30 000 / DR (допущение модели, настоящий фильтр по документации производителя устроен так же, но его времена мы не проверяли). Первый результат после WAKEUP — через (N + 4) / 30 000 с, дальше — каждые N / 30 000 с:

Код DRATE SPS N После WAKEUP Период
0xF0 30 000 1 166,7 мкс 33,3 мкс
0xE0 15 000 2 200 мкс 66,7 мкс
0xD0 7500 4 266,7 мкс 133,3 мкс
0xC0 3750 8 400 мкс 266,7 мкс
0xB0 2000 15 633,3 мкс 500 мкс
0xA1 1000 30 1133,3 мкс 1000 мкс
0x92 500 60 2133,3 мкс 2000 мкс
0x82 100 300 10 133,3 мкс 10 000 мкс
0x72 60 500 16 800 мкс 16 666,7 мкс
0x63 50 600 20 133,3 мкс 20 000 мкс
0x53 30 1000 33 466,7 мкс 33 333,3 мкс
0x43 25 1200 40 133,3 мкс 40 000 мкс
0x33 15 2000 66 800 мкс 66 666,7 мкс
0x23 10 3000 100 133,3 мкс 100 000 мкс
0x13 5 6000 200 133,3 мкс 200 000 мкс
0x03 2,5 12 000 400 133,3 мкс 400 000 мкс

На 30 000 SPS первый результат — через пять периодов, на 1000 — через 1,13 периода, на 2,5 — почти через один.

Шум. В модели шум на входе — √((30 мкВ / PGA)² + (1,5 мкВ)²) × √(DR / 30 000) (допущение модели, не данные документа). По 256 результатов через RDATAC, вход закорочен на 2,5 В; эффективные биты — log₂(4·VREF / PGA / шум), без шума — по размаху 6,6 шума:

SPS ×1: шум бит без шума ×64: шум бит без шума
30 000 31,214 мкВ 18,3 15,6 1,528 мкВ 16,6 13,9
7500 15,343 мкВ 19,3 16,6 0,79 мкВ 17,6 14,9
1000 5,5 мкВ 20,8 18,1 0,285 мкВ 19,1 16,3
100 1,668 мкВ 22,5 19,8 0,094 мкВ 20,7 17,9
10 0,562 мкВ 24,1 21,4 0,031 мкВ 22,3 19,5
2,5 0,296 мкВ 25 22,3 0,015 мкВ 23,3 20,6

Чем медленнее, тем тише: в модели каждое замедление в четыре раза даёт бит. Больше 24 эффективных бит на ×1 и 2,5 SPS — не ошибка: шум меньше единицы кода, и 24 бита его уже не различают. Усиление ×64 на 30 000 SPS снижает шум на входе в 20 раз, но и диапазон — в 64: бит становится меньше.


15д.5 DRDY, RDATA и RDATAC

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

RDATA — команда, пауза t6, три байта: 35,4 мкс на 1,125 МГц. На 30 000 SPS период — 33,3 мкс, и RDATA не успевает: в модели из 3000 результатов подряд ушло 106 333,3 мкс и 190 результатов пропало — их затёрли новые, пока шло чтение.

RDATAC — один раз команда, дальше по каждому спаду DRDY три байта без команды: 21,3 мкс, шина занята на 64 %. В модели 3000 результатов за 100 010,2 мкс, пропал 1 — тот, что лежал непрочитанным ещё до RDATAC.

/* RDATAC: дальше каждый результат выдвигается по спаду DRDY без команды; команды не слышны до SDATAC */
void ads_rdatac_start(ads_t *d)
{
    (void)ads_port_wait_drdy(0);
    ads_cmd(d, ADS_CMD_RDATAC);
}

int ads_rdatac_read(ads_t *d, int32_t *code)
{
    uint8_t z[3] = {0, 0, 0}, b[3];
    if (!ads_port_wait_drdy(ADS_TIMEOUT_US))
        return ADS_E_TIMEOUT;
    ads_frame(d, z, b, 3);
    *code = ads_s24(b);
    return range(*code);
}

int ads_rdatac_stop(ads_t *d)
{
    if (!ads_port_wait_drdy(ADS_TIMEOUT_US))
        return ADS_E_TIMEOUT;
    ads_cmd(d, ADS_CMD_SDATAC);
    return ADS_OK;
}

В RDATAC микросхема слышит только SDATAC и RESET (по документации производителя): RREG и WREG до SDATAC не дойдут (15д.7). После ads_rdatac_stop регистры снова читаются — 33 08 00 F0 E0. Время работы процессора в модели не считается: на настоящем F103 чтение в обработчике EXTI добавит свои микросекунды.

STANDBY. Микросхема засыпает до WAKEUP: в модели ожидание DRDY после STANDBY вернуло ADS_E_TIMEOUT, а после WAKEUP первый результат пришёл через 1133,3 мкс — как после SYNC.


15д.6 Каналы по кругу, SYNC и калибровка

Смена входа без SYNC. Фильтр копит отсчёты непрерывно, и запись MUX его не останавливает (допущение модели). Сразу после записи результат — смесь старого входа и нового. В модели AIN0 = 1 В, AIN1 = 2 В, ads_set переключает MUX, дальше результаты подряд; доля нового входа:

SPS Когда записан MUX 1-й 2-й 3-й 4-й 5-й
30 000 в начале периода 0 % 3 % 38 % 88,8 % 100 %
30 000 в середине периода 0,4 % 16,6 % 70,6 % 98,4 % 100 %
3750 в начале периода 53,7 % 100 % 100 % 100 % 100 %
3750 в середине периода 17 % 100 % 100 % 100 % 100 %
1000 в начале периода 87,7 % 100 % 100 % 100 % 100 %
1000 в середине периода 41,2 % 100 % 100 % 100 % 100 %
10 в начале периода 99,9 % 100 % 100 % 100 % 100 %
10 в середине периода 49,9 % 100 % 100 % 100 % 100 %

Ниже 30 000 SPS отбрасывать надо один результат, на 30 000 — четыре. Проще не гадать: SYNC обнуляет фильтр, WAKEUP запускает его заново, и первый результат после WAKEUP — уже нового входа целиком.

По кругу. Так обходят входы по документации производителя: на спаде DRDY — WREG MUX следующего входа, SYNC, WAKEUP и только потом RDATA. Читается результат прежнего входа: он лежит в микросхеме, пока фильтр копит новый.

/* выбрать вход и начать заново: WREG MUX, SYNC (фильтр стоит и пуст), WAKEUP (пошёл) */
void ads_scan_start(ads_t *d, uint8_t mux)
{
    ads_wreg(d, ADS_MUX, &mux, 1);
    d->reg[ADS_MUX] = mux;
    ads_cmd(d, ADS_CMD_SYNC);
    (void)ads_port_wait_drdy(0);                        /* старый спад — не наш */
    ads_cmd(d, ADS_CMD_WAKEUP);
}

/* по кругу: спад DRDY — следующий вход, SYNC, WAKEUP, и только потом RDATA — результат прежнего входа
 * лежит в микросхеме, пока новый фильтр копит отсчёты */
int ads_scan_next(ads_t *d, uint8_t next, int32_t *code)
{
    if (!ads_port_wait_drdy(ADS_TIMEOUT_US))
        return ADS_E_TIMEOUT;
    ads_wreg(d, ADS_MUX, &next, 1);
    d->reg[ADS_MUX] = next;
    ads_cmd(d, ADS_CMD_SYNC);
    ads_cmd(d, ADS_CMD_WAKEUP);
    return ads_read_data(d, code);
}

Восемь входов AIN0…AIN7 против AINCOM, на них 0,2…2,3 В, по два круга:

SPS Шаг Все входы На вход Окно фильтра
30 000 208,4 мкс 4797,6 Гц 599,7 Гц 166,7 мкс
7500 308,4 мкс 3242,1 Гц 405,3 Гц 266,7 мкс
1000 1175,1 мкс 851 Гц 106,4 Гц 1133,3 мкс
100 10 175,1 мкс 98,3 Гц 12,3 Гц 10 133,3 мкс
50 20 175,1 мкс 49,6 Гц 6,2 Гц 20 133,3 мкс
10 100 175,1 мкс 10 Гц 1,2 Гц 100 133,3 мкс

Ошибка ни на одном входе не больше 1 кода: смешения нет. Цена — окно фильтра на каждый шаг: на 30 000 SPS восемь входов дают по 599,7 Гц, а не 3750; на 1000 SPS — по 106,4 Гц. Ровно столько в прошивке fw15f.c:

/* восемь входов по кругу: результат k-го приходит, когда фильтр уже копит (k + 1)-й */
static int scan8(void)
{
    int32_t code;
    for (uint32_t k = 0; k < 8u; k++) {
        int rc = ads_scan_next(&adc, ADS_SE((k + 1u) % 8u), &code);
        if (rc != ADS_OK)
            return rc;
        ch_uv[k] = ads_uv(code, VREF_UV, ADS_PGA1);
    }
    rounds++;
    return ADS_OK;
}

Калибровка. У модели свои ошибки: смещение 10 + 50 / PGA мкВ и усиление на 0,3 % × (1 + 0,2·log₂PGA) больше (допущения модели). Вход 50 мВ, разность AIN0 − AIN1:

Что PGA Показано OFC FSC
до калибровки 1 50 210 мкВ 0 4 194 304
SELFCAL 1 50 000 мкВ 101 4 181 759
сменили PGA, без калибровки 64 50 189 мкВ то же то же
SELFCAL 64 50 000 мкВ 1165 4 166 803

После RESET в модели OFC = 0 и FSC = 0x400000; что в них в микросхеме, не проверено. SELFCAL на ×1 убрал 210 мкВ ошибки, но после смены усиления на ×64 поправки ×1 уже не годятся — 50 189 мкВ вместо 50 000. Снова SELFCAL — и 50 000 мкВ. Калибруют после каждой смены PGA, скорости и буфера. Можно поручить это микросхеме: ACAL = 1, и запись ADCON сама запустила SELFCAL (в модели — команда SELFCAL), результат — 50 000 мкВ.

/* SELFCAL, SELFOCAL, SELFGCAL, SYSOCAL, SYSGCAL: калибровка кончилась — DRDY упал */
int ads_calibrate(ads_t *d, uint8_t cmd)
{
    (void)ads_port_wait_drdy(0);
    ads_cmd(d, cmd);
    return ads_port_wait_drdy(ADS_TIMEOUT_US) ? ADS_OK : ADS_E_TIMEOUT;
}

Калибровка длится по окну фильтра на ноль и на усиление, и ещё окно до первого результата (допущение модели): SELFCAL на 1000 SPS — 3407,1 мкс, SELFOCAL и SELFGCAL — по 2273,8 мкс, на 30 000 SPS — 507,1 мкс, на 100 — 30 407,1 мкс, на 2,5 — 1 200 407,1 мкс. Отсюда ADS_TIMEOUT_US = 2 с.

Системная калибровка. SELFCAL замыкает входы внутри и не видит датчик. Мост со своим смещением 150 мкВ на AIN2 − AIN3, ×64: после SELFCAL ноль показал 150 мкВ, после SYSOCAL при разгруженном мосте — 0 мкВ (OFC = 17 378). SYSGCAL — то же для шкалы: подать на вход полную шкалу и отдать команду.


15д.7 Грабли

Нет паузы t6. RREG STATUS и сразу ответ в том же кадре: в модели прочитано 00 вместо 30 — ID = 0, «микросхемы нет». RDATA без паузы: 13 107 (0x003333) вместо 1 677 721 (0x199999) — результат сдвинут на семь бит: семь тактов прошли раньше, чем микросхема приготовила ответ.

Такт SPI выше CLKIN / 4. BR = 100 — 2,25 МГц при пределе 1,92 МГц. Что сделает микросхема, не проверено; в модели она молчит: сброс прочитал 00 00 00 00 00 и вернул ADS_E_ID. На 1,125 МГц тот же сброс прочитал 30 01 20 F0 E0.

SPI в режиме 0. Ответ защёлкивается на такт раньше (18.2): сброс прочитал 18 00 90 78 70 — каждый байт сдвинут на бит, ID = 1, ADS_E_ID. Режим 1: CPHA = 1.

Регистры в RDATAC без SDATAC. ads_set записал MUX = 0x18 и вернул ADS_E_VERIFY: в модели запись не дошла, MUX остался 0x08, и следующие результаты — всё тот же AIN0, 1 677 721 кода. Сначала ads_rdatac_stop, потом регистры.

Смена входа без отбрасывания. Результат сразу после записи MUX — смесь двух входов: на 1000 SPS 87,7 % нового, на 30 000 — четыре результата подряд неверны (15д.6). SYNC и WAKEUP после смены MUX — или отбросить.

WAKEUP сразу после SYNC. Без паузы t11 в модели WAKEUP не услышан, преобразование не пошло, ожидание DRDY кончилось тайм-аутом.

Буфер и пределы входа. С BUFEN = 1 вход выше AVDD − 2 В обрезается: 3,3 и 4,5 В показали 3 000 000 мкВ. Без буфера источник 10 кОм на ×64 показал 3797 мкВ из 20 000 (15д.2).

Калибровка старого усиления. Сменили PGA без SELFCAL — на ×64 50 189 мкВ вместо 50 000 (15д.6).


15д.8 Итоги

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

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

/* ads15f.h — раздел 15д: драйвер прецизионного 24-битного АЦП ADS1256 по SPI без регистров микроконтроллера.
 * Драйвер говорит с микросхемой только через функции порта (ads_port_*): в прошивке fw15f.c их делают
 * SPI1 и EXTI0 STM32F103, в стенде bench15f.c — модель model15f.py.
 * Документа на ADS1256 в проекте нет: коды команд, адреса и поля регистров, значения после сброса, таблица
 * DRATE, паузы t6 и t11 и предел такта SPI — по документации производителя, не проверено; модель повторяет их. */
#ifndef ADS15F_H
#define ADS15F_H
#include <stdint.h>

/* ── команды ──────────────────────────────────────────────────────────── */
#define ADS_CMD_WAKEUP    0x00u         /* пуск после SYNC или STANDBY (0xFF — то же) */
#define ADS_CMD_RDATA     0x01u         /* следующие три байта — результат; перед ними пауза t6 */
#define ADS_CMD_RDATAC    0x03u         /* выдавать каждый результат без команды */
#define ADS_CMD_SDATAC    0x0Fu         /* перестать: снова слушать команды */
#define ADS_CMD_RREG      0x10u         /* 0001 rrrr, второй байт 0000 nnnn: nnnn + 1 регистров */
#define ADS_CMD_WREG      0x50u         /* 0101 rrrr, второй байт 0000 nnnn */
#define ADS_CMD_SELFCAL   0xF0u         /* самокалибровка нуля и усиления */
#define ADS_CMD_SELFOCAL  0xF1u         /* только нуля */
#define ADS_CMD_SELFGCAL  0xF2u         /* только усиления */
#define ADS_CMD_SYSOCAL   0xF3u         /* ноль по тому, что сейчас на входе */
#define ADS_CMD_SYSGCAL   0xF4u         /* полная шкала по тому, что сейчас на входе */
#define ADS_CMD_SYNC      0xFCu         /* остановить и обнулить фильтр до WAKEUP */
#define ADS_CMD_STANDBY   0xFDu         /* уснуть до WAKEUP */
#define ADS_CMD_RESET     0xFEu

/* ── регистры ─────────────────────────────────────────────────────────── */
#define ADS_STATUS        0x00u
#define ADS_MUX           0x01u
#define ADS_ADCON         0x02u
#define ADS_DRATE         0x03u
#define ADS_IO            0x04u
#define ADS_OFC0          0x05u         /* OFC0…OFC2 — поправка нуля, младший байт первым */
#define ADS_FSC0          0x08u         /* FSC0…FSC2 — поправка шкалы, младший байт первым */
/* STATUS */
#define ADS_ID_POS        4u            /* 7:4 — номер микросхемы, только чтение */
#define ADS_ID            3u
#define ADS_ORDER         0x08u         /* 1 — младший бит первым; здесь всегда 0 */
#define ADS_ACAL          0x04u         /* самокалибровка сама после записи PGA, DRATE, BUFEN */
#define ADS_BUFEN         0x02u         /* входной буфер */
#define ADS_DRDY_BIT      0x01u         /* копия вывода DRDY, только чтение */
#define ADS_STATUS_RW     (ADS_ORDER | ADS_ACAL | ADS_BUFEN)
/* MUX: 7:4 — положительный вход, 3:0 — отрицательный; 0…7 — AIN0…AIN7, 8 — AINCOM */
#define ADS_AINCOM        8u
#define ADS_DIFF(p, n)    ((uint8_t)((p) << 4 | (n)))
#define ADS_SE(ch)        ADS_DIFF(ch, ADS_AINCOM)
/* ADCON */
#define ADS_CLK_POS       5u            /* 6:5 — такт на D0/CLKOUT: 00 выключен, 01 — CLKIN, 10 — /2, 11 — /4 */
#define ADS_SDCS_POS      3u            /* 4:3 — токи поиска обрыва: 00 выкл., 0,5, 2, 10 мкА */
#define ADS_PGA_POS       0u            /* 2:0 — усиление 1, 2, 4 … 64 */
enum { ADS_PGA1, ADS_PGA2, ADS_PGA4, ADS_PGA8, ADS_PGA16, ADS_PGA32, ADS_PGA64 };
/* DRATE — коды скоростей, отсчётов в секунду */
#define ADS_DR_30000      0xF0u
#define ADS_DR_15000      0xE0u
#define ADS_DR_7500       0xD0u
#define ADS_DR_3750       0xC0u
#define ADS_DR_2000       0xB0u
#define ADS_DR_1000       0xA1u
#define ADS_DR_500        0x92u
#define ADS_DR_100        0x82u
#define ADS_DR_60         0x72u
#define ADS_DR_50         0x63u
#define ADS_DR_30         0x53u
#define ADS_DR_25         0x43u
#define ADS_DR_15         0x33u
#define ADS_DR_10         0x23u
#define ADS_DR_5          0x13u
#define ADS_DR_2_5        0x03u

/* ── времена: такт CLKIN 7,68 МГц, τ = 130,2 нс ──────────────────────── */
#define ADS_CLKIN_HZ      7680000u
#define ADS_T6_US         7u            /* t6 = 50 τ = 6,51 мкс: от команды RDATA/RREG до чтения */
#define ADS_T11_US        4u            /* t11 = 24 τ = 3,13 мкс: после SYNC и RDATAC до следующей команды */
#define ADS_TIMEOUT_US    2000000u      /* дольше любой калибровки на 2,5 SPS, с запасом */
#define ADS_FS            8388607       /* код полной шкалы: +2·VREF / PGA */

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

typedef struct {
    uint8_t reg[5];                     /* что драйвер записал в STATUS…IO */
    uint8_t rdatac;                     /* 1 — микросхема в RDATAC и команд не слушает */
    uint32_t frames, bytes;             /* кадров CS и байт по SPI */
} ads_t;

/* ── порт: в прошивке — SPI1 и EXTI0, в стенде — строки к модели ───── */
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 — был и снят */

/* ── драйвер ──────────────────────────────────────────────────────────── */
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, uint8_t r[5]);
int ads_config(ads_t *d, uint8_t status, uint8_t mux, uint8_t adcon, uint8_t drate);
int ads_set(ads_t *d, uint32_t reg, uint8_t v);
int ads_calibrate(ads_t *d, uint8_t cmd);
void ads_read_cal(ads_t *d, int32_t *ofc, uint32_t *fsc);
int32_t ads_s24(const uint8_t b[3]);
int ads_read_data(ads_t *d, int32_t *code);
int ads_convert(ads_t *d, uint8_t mux, int32_t *code);
void ads_scan_start(ads_t *d, uint8_t mux);
int ads_scan_next(ads_t *d, uint8_t next, int32_t *code);
void ads_rdatac_start(ads_t *d);
int ads_rdatac_read(ads_t *d, int32_t *code);
int ads_rdatac_stop(ads_t *d);
uint32_t ads_sps_x10(uint8_t drate);
int32_t ads_uv(int32_t code, uint32_t vref_uv, uint32_t pga);

#endif

ads15f.c — драйвер: кадры, t6, сброс и настройка, калибровка, RDATA и RDATAC, обход по кругу, микровольты.

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

/* DRATE: код и скорость × 10 (2,5 SPS — 25) */
static const uint8_t dr_code[16] = {0xF0, 0xE0, 0xD0, 0xC0, 0xB0, 0xA1, 0x92, 0x82,
                                    0x72, 0x63, 0x53, 0x43, 0x33, 0x23, 0x13, 0x03};
static const uint32_t dr_x10[16] = {300000, 150000, 75000, 37500, 20000, 10000, 5000, 1000,
                                    600, 500, 300, 250, 150, 100, 50, 25};

/* один кадр: 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;
}

/* команда без данных; после SYNC и RDATAC — пауза t11 */
void ads_cmd(ads_t *d, uint8_t cmd)
{
    uint8_t rx;
    ads_frame(d, &cmd, &rx, 1);
    if (cmd == ADS_CMD_SYNC || cmd == ADS_CMD_RDATAC)
        ads_port_delay_us(ADS_T11_US);
    if (cmd == ADS_CMD_RDATAC)
        d->rdatac = 1;
    if (cmd == ADS_CMD_SDATAC || cmd == ADS_CMD_RESET)
        d->rdatac = 0;
}

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

/* RREG: два байта команды, пауза t6 — и только потом ответ, всё в одном кадре */
void ads_rreg(ads_t *d, uint32_t reg, uint8_t *v, uint32_t n)
{
    uint8_t tx[11] = {(uint8_t)(ADS_CMD_RREG | (reg & 15u)), (uint8_t)((n - 1u) & 15u)}, rx[11];
    ads_port_cs(0);
    ads_port_xfer(tx, rx, 2);
    ads_port_delay_us(ADS_T6_US);
    ads_port_xfer(tx + 2, v, n);                        /* на DIN — нули */
    ads_port_cs(1);
    d->frames++;
    d->bytes += n + 2u;
}

/* SDATAC на случай, если микросхема осталась в RDATAC, RESET, первый результат — и пять регистров */
int ads_reset(ads_t *d, uint8_t r[5])
{
    ads_cmd(d, ADS_CMD_SDATAC);
    (void)ads_port_wait_drdy(0);
    ads_cmd(d, ADS_CMD_RESET);
    if (!ads_port_wait_drdy(ADS_TIMEOUT_US))
        return ADS_E_TIMEOUT;
    ads_rreg(d, ADS_STATUS, r, 5);
    for (uint32_t i = 0; i < 5u; i++)
        d->reg[i] = r[i];
    return r[0] >> ADS_ID_POS == ADS_ID ? ADS_OK : ADS_E_ID;
}

/* STATUS, MUX, ADCON, DRATE одним WREG и обратно; в STATUS сравниваются только ORDER, ACAL и BUFEN */
int ads_config(ads_t *d, uint8_t status, uint8_t mux, uint8_t adcon, uint8_t drate)
{
    uint8_t w[4] = {status, mux, adcon, drate}, r[4];
    ads_wreg(d, ADS_STATUS, w, 4);
    ads_rreg(d, ADS_STATUS, r, 4);
    for (uint32_t i = 0; i < 4u; i++)
        d->reg[i] = w[i];
    if ((r[0] ^ status) & ADS_STATUS_RW || r[1] != mux || r[2] != adcon || r[3] != drate)
        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);
    if (reg < 5u)
        d->reg[reg] = v;
    return (reg == ADS_STATUS ? (r ^ v) & ADS_STATUS_RW : r ^ v) ? ADS_E_VERIFY : ADS_OK;
}

/* SELFCAL, SELFOCAL, SELFGCAL, SYSOCAL, SYSGCAL: калибровка кончилась — DRDY упал */
int ads_calibrate(ads_t *d, uint8_t cmd)
{
    (void)ads_port_wait_drdy(0);
    ads_cmd(d, cmd);
    return ads_port_wait_drdy(ADS_TIMEOUT_US) ? ADS_OK : ADS_E_TIMEOUT;
}

/* OFC — 24 бита со знаком, FSC — без знака; оба младшим байтом вперёд */
void ads_read_cal(ads_t *d, int32_t *ofc, uint32_t *fsc)
{
    uint8_t r[6], b[3] = {0, 0, 0};
    ads_rreg(d, ADS_OFC0, r, 6);
    b[0] = r[2];
    b[1] = r[1];
    b[2] = r[0];
    *ofc = ads_s24(b);
    *fsc = (uint32_t)r[5] << 16 | (uint32_t)r[4] << 8 | r[3];
}

/* 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 == ADS_FS || code == -ADS_FS - 1 ? ADS_E_RANGE : ADS_OK;   /* упёрлись в край */
}

/* RDATA, пауза t6, три байта — в одном кадре */
int ads_read_data(ads_t *d, int32_t *code)
{
    uint8_t c = ADS_CMD_RDATA, z[3] = {0, 0, 0}, rx, b[3];
    ads_port_cs(0);
    ads_port_xfer(&c, &rx, 1);
    ads_port_delay_us(ADS_T6_US);
    ads_port_xfer(z, b, 3);
    ads_port_cs(1);
    d->frames++;
    d->bytes += 4;
    *code = ads_s24(b);
    return range(*code);
}

/* выбрать вход и начать заново: WREG MUX, SYNC (фильтр стоит и пуст), WAKEUP (пошёл) */
void ads_scan_start(ads_t *d, uint8_t mux)
{
    ads_wreg(d, ADS_MUX, &mux, 1);
    d->reg[ADS_MUX] = mux;
    ads_cmd(d, ADS_CMD_SYNC);
    (void)ads_port_wait_drdy(0);                        /* старый спад — не наш */
    ads_cmd(d, ADS_CMD_WAKEUP);
}

/* по кругу: спад DRDY — следующий вход, SYNC, WAKEUP, и только потом RDATA — результат прежнего входа
 * лежит в микросхеме, пока новый фильтр копит отсчёты */
int ads_scan_next(ads_t *d, uint8_t next, int32_t *code)
{
    if (!ads_port_wait_drdy(ADS_TIMEOUT_US))
        return ADS_E_TIMEOUT;
    ads_wreg(d, ADS_MUX, &next, 1);
    d->reg[ADS_MUX] = next;
    ads_cmd(d, ADS_CMD_SYNC);
    ads_cmd(d, ADS_CMD_WAKEUP);
    return ads_read_data(d, code);
}

/* один установившийся результат с входа mux */
int ads_convert(ads_t *d, uint8_t mux, int32_t *code)
{
    ads_scan_start(d, mux);
    if (!ads_port_wait_drdy(ADS_TIMEOUT_US))
        return ADS_E_TIMEOUT;
    return ads_read_data(d, code);
}

/* RDATAC: дальше каждый результат выдвигается по спаду DRDY без команды; команды не слышны до SDATAC */
void ads_rdatac_start(ads_t *d)
{
    (void)ads_port_wait_drdy(0);
    ads_cmd(d, ADS_CMD_RDATAC);
}

int ads_rdatac_read(ads_t *d, int32_t *code)
{
    uint8_t z[3] = {0, 0, 0}, b[3];
    if (!ads_port_wait_drdy(ADS_TIMEOUT_US))
        return ADS_E_TIMEOUT;
    ads_frame(d, z, b, 3);
    *code = ads_s24(b);
    return range(*code);
}

int ads_rdatac_stop(ads_t *d)
{
    if (!ads_port_wait_drdy(ADS_TIMEOUT_US))
        return ADS_E_TIMEOUT;
    ads_cmd(d, ADS_CMD_SDATAC);
    return ADS_OK;
}

/* скорость × 10 по коду DRATE; неизвестный код — 0 */
uint32_t ads_sps_x10(uint8_t drate)
{
    for (uint32_t i = 0; i < 16u; i++)
        if (dr_code[i] == drate)
            return dr_x10[i];
    return 0;
}

/* код → микровольты: вход = код × 2·VREF / (PGA × (2^23 − 1)); PGA 110 и 111 — оба ×64 */
int32_t ads_uv(int32_t code, uint32_t vref_uv, uint32_t pga)
{
    uint32_t sh = pga > ADS_PGA64 ? ADS_PGA64 : pga;
    int64_t x = (int64_t)code * 2 * vref_uv;
    int64_t den = (int64_t)ADS_FS << sh;
    return (int32_t)(x >= 0 ? (x + den / 2) / den : -((-x + den / 2) / den));
}

fw15f.c — прошивка STM32F103: SPI1 в режиме 1, DRDY на EXTI0, восемь входов по кругу.

/* fw15f.c — раздел 15д: ADS1256 на STM32F103 (BluePill), 72 МГц, APB2 = 72 МГц.
 * SPI1 без ремапа: PA5 — SCLK, PA6 — DOUT (MISO), PA7 — DIN (MOSI), режим 1 (CPOL = 0, CPHA = 1),
 * 72 МГц / 64 = 1,125 МГц — ниже предела CLKIN / 4 = 1,92 МГц; PA4 — CS, обычный выход; PB0 — DRDY, вход с
 * подтяжкой, EXTI0 по спаду. Опорное на плате — 2,5 В. Восемь несимметричных входов AIN0…AIN7 против AINCOM
 * по кругу с SYNC и WAKEUP, буфер включён, ×1, 1000 SPS; после смены усиления или скорости — SELFCAL.
 * Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "ads15f.h"

#define CPU_HZ          72000000u
#define VREF_UV         2500000u        /* опорное платы: сюда — измеренное значение */

static ads_t adc;
static volatile uint8_t drdy;
volatile int32_t ch_uv[8];
volatile uint32_t rounds, 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;
    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->CR1 |= SPI_CR1_SPE;
}

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

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 */
    }
}

/* DRDY: спад на PB0 */
void EXTI0_IRQHandler(void)
{
    EXTI->PR = 1u << 0;
    drdy = 1;
}

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;
}

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

/* восемь входов по кругу: результат k-го приходит, когда фильтр уже копит (k + 1)-й */
static int scan8(void)
{
    int32_t code;
    for (uint32_t k = 0; k < 8u; k++) {
        int rc = ads_scan_next(&adc, ADS_SE((k + 1u) % 8u), &code);
        if (rc != ADS_OK)
            return rc;
        ch_uv[k] = ads_uv(code, VREF_UV, ADS_PGA1);
    }
    rounds++;
    return ADS_OK;
}

int main(void)
{
    uint8_t r[5];
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    spi1_init();
    irq_init();
    while (ads_reset(&adc, r) != ADS_OK)
        ads_port_delay_us(100000u);                     /* нет ответа: провод, питание или такт */
    while (ads_config(&adc, ADS_BUFEN, ADS_SE(0), 0 << ADS_CLK_POS | ADS_PGA1, ADS_DR_1000) != ADS_OK
           || ads_calibrate(&adc, ADS_CMD_SELFCAL) != ADS_OK)
        errors++;
    ads_scan_start(&adc, ADS_SE(0));
    for (;;) {
        if (scan8() != ADS_OK) {
            errors++;
            ads_scan_start(&adc, ADS_SE(0));
        }
    }
}

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

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