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 нВ.
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):
- разность = I1 · (R + L1) − I2 · L2 = I · R, если токи и провода равны;
- опорное = (I1 + I2) · Rref = 2 · I · Rref;
- код / (усиление × 2²³) = R / (2 · Rref), R = код × 2 · Rref / (усиление × 2²³).
Ток в результат не входит — это та же логометрия, что в 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 Итоги
- ADS1220 — 24 бита, усиление 1…128, четыре входа и три служебных сочетания, опорное своё 2,048 В, REF0, REF1 или AVDD, 20…1000 SPS (turbo — 40…2000), два источника тока до 1500 мкА; SPI в режиме 1.
- Команды: RESET 0x06, START 0x08, POWERDOWN 0x02, RDATA 0x10, RREG 0x20, WREG 0x40; после RESET — пауза 50 мкс и четыре нуля; после WREG — прочитать обратно.
- Результат —
int32_tиз трёх байт со знаковым расширением; вход = код × VREF / (усиление × 2²³), на 2,048 В и ×128 — 1,9073 нВ на код. - Первый результат — через окно фильтра: 150 035,6 мкс на 20 SPS в модели; после скачка — третий результат; канал на 20 SPS — 150 мс.
- 50 Гц на 1000 SPS проходит почти вся (987,7 мкВ из 1000); 20 SPS с 50/60 = 01 — 0; в turbo поле не работает.
- Мост от AVDD с опорным AVDD: размах 1 073 741 ± 1 код на 3,0–3,6 В; своё опорное — плюс 9,1 % на 3,6 В.
- PT100: два IDAC по 500 мкА, Rref на REF0, R = код × 2 · Rref / (усиление × 2²³); перестановка токов снимает 0,8 °C разброса.
MUX= 1110 — смещение: 3914 кодов вместо 18,13 г на пустых весах.- DRDY на EXTI0, три байта по DMA: 2000 SPS без пропусков, шина занята 4,3 %.
Полные исходники
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» — самый первый интерфейс, который подключают.