Рабочий прототип, вместо полезной нагрузки резистор
Рабочий прототип, вместо полезной нагрузки резистор

TL;DR: здесь ноль медицины и нейробиологии, только инженерный разбор прототипа DIY tRNS: архитектура, генерация сигнала (100–640 Гц hf-tRNS), VCCS, код и выводы.

Зачем?

Есть немалая вероятность, что человеческий мозг - самый сложный объект во Вселенной. Но для транскраниальных стимуляторов tDCS/tACS/tRNS все живые ткани между электродами (включая мозг) успешно моделируются активным сопротивлением (правда, заранее неизвестным и дрейфующим за время сеанса).

В этом тексте я рассматриваю исключительно инженерную сторону задачи, т.е. генерацию и контроль токов заданной формы. Медицинские аспекты, показания и интерпретации эффектов остаются за рамками поста и подробно обсуждаются в научных публикациях, на PubMed актуальной информации достаточно.

У меня уже было несколько итераций самодельных tDCS, которые я собирал в формате хобби. Коллега по опасным биокекерским практикам закинул идею, что tRNS перспективнее и эффективнее, чем tDCS. Заинтересовал, в общем.

Близкое к совершенству устройство DIY tDCS включает стабилизатор тока, потенциометр плавного пуска и миллиамерметр. А даже базовый вариант tRNS сделать в разы сложнее, потому что токи биполярные, да ещё и сложной формы, по началу было вообще непонятно, с какой стороны подступаться.

Другими словами, биокекерский азарт объединился с азартом чисто инженерного вызова.

Почему я?

Не я придумал, что мозг для tRNS можно моделировать как резистор, так придумали нейробиологи-исследователи. Как программист с бэкграундом в DSP (цифровой обработке сигналов) я понимаю, как генерировать сигналы на контроллере с заданными свойствами (спектром и распределением). Как связист по образованию я достаточно знаком со схемотехникой, чтобы при необходимости через боль разбираться со схемами на операционных усилителях.

В какой-то момент удалось разжиться достаточной мотивацией, осциллографом Hantek 6022BE, необходимыми компонентами и достаточным количеством свободного времени.

Постановка задачи

Пусть сопротивление полезной нагрузки варьируется от единиц до десятка кОм, а задача устройства сводится к пропусканию через нагрузку тока определенной формы, например, 1 мА постоянного тока для tDCS, гармонику 140 Гц с амплитудой 1 мА для tACS или полосу 100-640 Гц из белого гауссовского шума с амплитудой 1 мА для tRNS.

В разных протоколах параметры могут отличаться, но в целом параметры в примере часто встречаются в публикациях, а для tACS и tRNS сигнал как правило биполярный без постоянной составляющей. Задача сделать именно tRNS, но если получится объединить tRNS+tACS+tDCS в одном устройстве, то это будет плюсом.

Требования к точности тут весьма размытые, но интуитивно я убеждён, что грубость самого метода допускает отклонения токов в единицы процентов, это не прецизионное устройство. Постоянные погрешности и усиления мы доводим калибровкой. Частоты довольно низкие, нижний диапазон аудиоспектра, потому импульсными помехами от цифровых контуров можем пренебречь на прототипе, а ближе к финальной версии найти способ замерить все наводки от контроллера, OLED и энкодера-кнопки по факту на готовом устройстве.

Так же я не представляю себе настоящего стимулятора мозга малыми токами без встроенного индикатора с отображением параметров этих токов, я убеждён, что пользователь должен их видеть в реальном времени. Для меня это один из базовых принципов обеспечения безопасности и эффективности устройства.

Что касается оверинжиниринга, современные контроллеры и готовые модули стоят настолько дёшево, что инженерные компромиссы перевернулись с ног на голову. Если микроскоп внезапно оказался доступнее и удобнее молотка, то я разрешу себе забивать гвозди микроскопом, по крайней мере на этапе прототипирования.

Схема устройства

Сначала я с некоторой неохотой прорабатывал схему, где ток стабилизируется прямо на контроллере по обратной связи по напряжению на шунте с известным сопротивлением (которое пропорционально току на полезной нагрузке). Это была бы схема с массой компромиссов, высокими требованиями к АЦП для обратной связи, сомнительной надёжностью и необходимостью проведения экспериментов для адекватной настройки.

Управляемый источник тока

Потом к великой радости я открыл для себя VCCS (voltage-controlled current source, управляемый напряжением источник тока) в виде одной из реализаций Howland Current Pump. По сути, этот затык с управлением током был тем самым инженерным "супербоссом", без победы над которым двигаться дальше было просто несерьёзно. VCCS – это надёжный краеугольный камень устройства, на основе которого можно собирать не только tRNS, но и универсальный tES (tRNS+tACS+tDCS).

VCCS в симуляторе
VCCS в симуляторе

Так выглядит испытательный стенд в симуляторе. VCCS на кадый 1 В на входе выдает 1 мА на выходе и состоит из:

  • 2-х операционных усилителей OPA134 (или двух в одном OPA2134);

  • 4-х R2 номиналом 10 кОм, 1-го R1 (1 кОм), и 1-го R3 (9 кОм).

Генератор сигналов DCA_OUT тут выдает пилообразный сигнал, на таком сигнале легко увидеть нелинейные искажения. Потенциометр плавного пуска 100 кОм и шунт 100 Oм (на стенде напряжение на нём будем использовать для мониторинга тока через линию "B" осциллографа, в реальном устройстве - через АЦП контролл��ра) включены последовательно с полезной нагрузкой Rload. В симуляторе этот VCCS показал себя отлично, я менял в широком диапазоне Rload и ток через полезную нагрузку вёл себя стабильно, как раз то что нужно, и это успех!

Функциональная схема

Функциональная схема. Синие стрелки - аналоговый сигнал, черные - цифровой
Функциональная схема. Синие стрелки - аналоговый сигнал, черные - цифровой
  1. Генератор сигнала формирует сигнал с требуемыми для протокола стимуляции характеристиками. Для прототипа это только один сигнал hf-tRNS, шумоподобный с нормальным распределением, равномерный в полосе 100-640 Гц. ESP32-S2 mini выбран за компактность, возможность подключать к смартфону через OTG (не пригодилось), доступность и неплохую вычислительную мощность, сомневаюсь, что это окончательное решение, кроме того, можно сделать несколько вариантов прошивок для разных контроллеров.

  2. По I2S контроллер загружает сигнал в буфер Аудио ЦАП на готовом модуле c PCM5102A. Доступное и компактное решение, выдающее качественный биполярный сигнал, я решил что для выполения требований достаточно 8000 Гц (для частоты среза 640 Гц хватило бы частоты дискретизации 1280 Гц по теореме Котельникова) и 16 бит разрядности (хотя модуль способен аж на 32 бит).

  3. Аналоговый сигнал (синие стрелки – аналоговый) с Аудио ЦАП поступает на VCCS, где сигнал из +/–1 В преобразуется в сигнал +/–1 мА той же формы.

  4. Потенциометр выступает в роли ручного плавного пуска и стопа и может служить аварийным токоограничителем. Для того, чтобы он работал как токоограничитель, достаточно при старте крутить его не на максимум (не до 0 кОм), а до тех пор, пока монитор не покажет требуемую для сеанса амплитуду, плюс небольшой запас на случай колебания сопротивления полезной нагрузку (там может оставаться, например, 2 кОм запаса). Пусть максимальное напряжение, которое может в аварийной ституации попасть в нагрузку 12 В, тогда при минимальном сопротивлении полезной нагрузки те же 2 кОм, получим 4.1 кОм вместе с шунтом и потенциометром, даже если сопротивление источника питания равна нулю, через Полезную нагрузку нагрузку пойдет менее 3 мА. Вообще, конечно, стоит дополнительно залить часть схемы с большими напряжениями (питание операционных усилителей +12 В и –12 В) теплопроводным компаундом, чтобы гарантировать, что там ничего не коротнёт, например, от случайного попадания солевого раствора.

  5. Блок Полезной нагрузки включает в себя коннекторы для электродов, электроды с губками и солевым раствором и собственно те живые ткани, через которые течет ток заданной формы и амплитуды.

  6. Роль Шунта в том, что ток через него течет такой же как через полезную нагрузку, т.к. он соединяет с ней последовательно и уходит на землю, а значит напряжение на нем будет пропорционально току. Напряжение на нём для случая tRNS будет биполярным.

  7. Чтобы биполярное напряжение снова стало униполярным и могло быть считано контроллером применяется Дифференциальный усилитель сдвигающий сигнал с Шунта в область положительных напряжений.

  8. Униполярный сигнал считывается в контроллере через встроенный АЦП на 12 бит с заведомо избыточной частотой дискретизации, чтобы его можно было эффективнее почистить от шумов, в том числе квантовых.

  9. Для Расчета метрик сигнала с АЦП сигнал фильтруется от шумов рассчитываются максимумы, минимумы и формируется гистограмма для оценки распределения.

  10. На дисплее покажем гистограмму, среднее, минимум и максимум сигнала, чтобы пользователь знал, что на Полезную нагрузку подается безопасный ток, что устройство исправно, что контакт в порядке, что мощности питания достаточно и что требования по эффективности стимуляции соблюдаются.

Схема дифференциального усилителя

Дифф. усилитель в симуляторе. Делает из биполярного сигнала униполярный
Дифф. усилитель в симуляторе. Делает из биполярного сигнала униполярный

Как видно из иллюстрации симуляции, дифференциальный усилитель решает проблему преобразования биполярного сигнала в униполярный для последующей оцифровки на АЦП контроллера. Кстати, –0.5В удобно брать с PCM5102A, он же стерео, R-канал для управления током через нагрузку, L – для смещения в дифф. усилителе.

Идеи дальнейших доработок схемы

  • Можно использовать в дифф. усилителе OPA2134 (2-а OPA134 в одном корпусе), первый усилитель в роли буфера для повышения входного сопротивления, второй всё так же в роли дифференциального. Получится что-то вроде инструментального усилителя. Не то чтобы это было значимое улучшение, но это практически бесплатно.

  • Добавить энкодер с кнопкой в качестве ввода для пользовательского интерфейса. Для универсального устройства tES (tRNS+tACS+tDCS) это в той или иной форме станет необходимостью.

  • Поразмышлять над комфортом и безопасностью устройства, поизучать коммерческие аналоги, какой функционал там нужен только для медицинских исследований (sham-режим, например), а что может быть полезно в DIY для биокек-энтузиастов.

  • Разработать "материнскую плату", на которую будут сажаться модули и компоненты для повышения качества и скорости сборки. Схема сложновата для паечных макеток.

Код

Исходники

Исходники прошивки доступны в репозитории . По большей части я вайбкодил на ИИшках, а потом где-то упрощал и вычищал лишнее, потому комментарии в коде подробные.

  • файл ардуино проекта в /ESP32tRNS/ESP32tRNS.ino;

  • adc_control.cpp, dac_control.cpp и display_control.cpp в той же папке – функции работы с буферами АЦП (через I2S), буферами ЦАП и отрисовки на дисплее соответственно;

  • presets_embedded.cpp – пока единственный пресет tRNS 100-640Hz normal, грубо захардкоженный в виде вектора на 16384 элементов. Это временно, пока я не одолел SPIFFS или не придумал более изящное решение;

  • /py_experiments/generator.ipynb – способ, которым я создавал тот самый пресет tRNS 100-640Hz normal, о чем подробнее расписано в следующем разделе.

Концепция генерации пресетов

Я решил, что сигнал tRNS проще будет не генерировать на контроллере, а в любом виде загрузить в виде файла или захардкодить в переменную в виде двухсекундного лупа (2,048 c, подгонял по старой привычке число отсчетов 16384 под степень числа двойки для более быстрого FFT). Да, это периодический сигнал, он будет повторяться каждые две секунды, но по всем прочим характеристикам это будет честный псевдослучайный сигнал и по спектру правильный, и по распределению.

Иллюстрация пресета hf-tRNS 100-640 Гц. Временной ряд сигнала, его распределение (квазинормальное) и PSD (диапазон верный).
Иллюстрация пресета hf-tRNS 100-640 Гц. Временной ряд сигнала, его распределение (квазинормальное) и PSD (диапазон верный).

Распределение у сигнала нормальное, спектр – квазибелый в полосе 100–640 Гц, за пределами этой полосы – нулевая мощность. Это нормально, что у шума даже сглаженный Уэлчем спектр всё равно лохматый, а не выглядит ровной прямоугольной полочкой.

Другие пресеты tRNS (0.1-100 Гц и 0.1-640 Гц) я генерировал в тем же файлом, но в прототипе решил их не добавлять, т.к. на данный момент согласно исследованиям они не представляют интереса.

Работа с буферами ЦАП и АЦП

Главное назначение прошивки устройства – качественное проигрывание пресетов через внешний АЦП через I2S.

Мы заранее готовим стереобуфер на основе пресета в правый канал и константы смещения дифференциального усилитея в левый канал и в процессе работы следим, чтобы буфер ЦАП всегда был заполнен. Далее код из dac_control.cpp.

// Записать подготовленный фрагмент во внутренний DMA буфер I2S
// timeout_ticks = 0 → неблокирующий; больше 0 → ждём указанное время
static bool writeFragmentToDMA(TickType_t timeout_ticks) {
	const uint32_t start_pos = stereo_buffer_pos;
	copyFragmentFromStereoBuffer(start_pos);
	size_t bytes_written = 0;

	const size_t bytes_to_write = FRAGMENT_SAMPLES * sizeof(int16_t);

	esp_err_t result = i2s_write(I2S_NUM,
	                             stereo_buffer_fragment,
								 bytes_to_write,
								 &bytes_written,
	                             timeout_ticks);

	if (result == ESP_OK && bytes_written > 0) {
		uint32_t samples_written = bytes_written / sizeof(int16_t);
		stereo_buffer_pos = (start_pos + samples_written) % STEREO_BUFFER_SIZE;
		return true;
	}

	if (result == ESP_ERR_TIMEOUT || bytes_written == 0) {
		// DMA заполнен - ничего страшного, позицию не меняем
		return false;
	}
	return false;
}

Если буфер всегда поддерживается заполненным, то время контроллера освобождается для того, чтобы прочитать АЦП, рассчитать метрики и показать их на дисплее (ещё буду энкодер и кнопку опрашивать в следующих версиях) без риска, что проигрывание пресета прервётся.

Заполнение буфера АЦП (код в adc_control.cpp) происходит через DMA в continuous mode, т.е. буфер заполнятеся автоматически аппаратно с нужной частотой асинхронно и без блокировок. Когда в основном loop в ESP32tRNS.ino приходит время показать метрики на дисплей, мы берем самые свежие N сэмплов из этого буфера и по ним высчитываем всё, чт�� хотим показать, матожидание, минимум, максимум и гистограмму. Это чисто пользовательский "показометр".

Идеи дальнейших доработок кода

  • Изначально я хотел реализовать заливку новых пресетов прямо через USB без перепрошивки, чтобы устройство сохраняло бинари в SPIFFS. Сделать возможность залить вообще произвольный сигнал в виде файла с лупом и метаданными, но ничего не вышло по загадочным причинам. Можно либо придумать, как забороть эту проблему, либо поступить проще и для tRNS ограничиться пресетом rf-tRNS, а tACS и tDCS ноль проблем генерировать на лету параметрически, или вообще все сигналы собирать параметрически, чуть больше кода, зато без менеджмента бинарей.

  • Совершенно точно нужна "пресетошная", т.е. возможность выбирать режимы и настройки tRNS (амплитуда), tACS (частота+амплитуда), tDCS (сила тока), думаю, которая немыслима без кнопочек или сенсорного экрана или энкодера с кнопкой. Склоняюсь к энкодеру с кнопкой, крутилка выбирает вариант, нажатие применяет выбор, в коде это надо будет отрисовывать интерфейс и отслеживать события энкодера. Кроме того, смещения, усиления и прочие константы тоже следует разрешить редактировать и обеспечить персистентное сохранение в EEPROM.

  • Можно переделать расчет гистограммы с O(N) в O(1), в теории это возможно, вычитаем влияние выбывающего из окна отсчёта и учитываем новый.

  • Было бы совсем отлично показывать фактический частотный спектр с АЦП на дисплее, а не только гистограмму, наверное, стоит поглядеть в сторону двухъядерных и более мощных ESP32, пока не тестировал реалистичность этой идеи.

Сборка

Прототип tRNS без корпуса-баночки. Всё собрано на крышке
Прототип tRNS без корпуса-баночки. Всё собрано на крышке

Прототип я собрал в прозрачной баночке. Винтовые крепления потенциометра и клемм для электродов радикально упростили монтаж на крышечке. Угловой адаптер USB-C потребовал прямоугольного отверстия, но тоже много сил и времени не отнял. Думаю, следующая версия будет требовать от USB только питания и можно будет заменить адаптер на винтовое гнездо. Кстати, феноменально удобное решение для прототипирования, открутил банку от крыши и вот всё перед глазами с лёгким доступом, кнопочки BOOT и RST нажал – обновил прошивку, никаких 4-х винтов не надо откручивать, никаких проводов от одной половинки корпуса к другой при вскрытии не тянется.

Компоненты смонтировал на дырчатой паечной макетной плате, пришлось тянуть "дорожки" одножильными разноцветными проводками в изоляции, что долго и муторно. Знал, что это займёт почти два дня, сделал бы лазерно-утюжный вариант "материнской платы" устройства, заодно и герберы были бы уже готовы.

Вывод

Сейчас у меня на руках прототип, который выдает правильный сигнал hf-tRNS, удалось реализовать не все хотелки, но в итоге ставка оказалась скорее выигрышной, чем нет. Остался гигантский простор для улучшений. Изначально делал устройство для себя, но пройдя весь путь, хочу познакомить сообщество с моими подходами и получить ценную обратную связь. Код и материалы доступны в открытом репозитории под permissive-лицензией, чтобы любой мог использовать и развивать решения дальше.