После прохождения Atomic Heart у меня всё вертелось в голове, чего бы такого сделать по лору игры, чтобы было прикольно и несложно, а главное, более‑менее реально реализуемо в реале (например, я крайне сомневаюсь, что можно создать тех же пчёл в реале, как в игре, чтобы они аналогичным образом летали).

С выходом DLC 1 для игры мысль, что сделать, пришла сама собой.:) Гусь! Точнее, эксперимент с надетым на гуся нейроконнектором «Мысль» и натолкнул меня на создание схожего девайса (а на носу как раз был Хэллоуин на то�� момент, что только разжигало желание создать хотя бы прототип на коленке).

Ну, собственно, начал делать девайс на Black‑Pill (STM32F401CCU6), т. к. она была под рукой, и, в принципе, всё, что необходимо по интерфейсам, МК имел на борту. Сделать кастомную плату и в итоге сделать монолитный девайс менее чем за месяц я не успел бы, поэтому изначально было решено сделать всё максимально на готовых платах‑кубиках, просто упрятав девайс «за воротник». :D

Итак, вот сходу видос с частичным результатом работы девайса на Локи (имя котэ :) ):

Железо

Собственно, структурная схема девайса:

После этой картинки думаю уже и так ясно, что будет дальше...
После этой картинки думаю уже и так ясно, что будет дальше...

Всё предельно просто. :) Начинаем с мозга - МК, открываем куб и прикидываем, чё как раскидать по пинам:

Используем МК менее чем на половину
Используем МК менее чем на половину

Основная используемая периферия:

  • SPI — SPI1 на пинах PA4...7 для SPI Flash

  • I2C — I2C1 на пинах PB6, PB7 для работы с акселерометром

  • I2S — I2S3 на пинах PB3...5, PA15 для вывода звука

  • TIM PWM — TIM1 на пинах PA8...10 для статусного светодиода

  • SWD — на стандартных пинах PA13, PA4 для отладки и подключения консоли RTT

  • RCC — на стандартных пинах PH0, PH1 с внешним кварцем 16 МГц

Для хранения аудиофайлов я использовал первую попавшуюся плату типа такой для SPI Flash:

Напаяв на неё 128 Мбит БУ флешку
Напаяв на неё 128 Мбит БУ флешку

В качестве акселерометра я взял давно валявшуюся плату на дешёво�� ADXL345, выбирая по принципу «с чем я ущё не работал и что +\‑ километр подойдёт под задачу»:

Акселерометр, просто акселерометр
Акселерометр, просто акселерометр

Для вывода звука я захотел поковыряться с I2S и поэтому нашёл офигенный моно ЦАП с I2S входом и на выходе у него уже усилитель D класса — MAX98 357A:

Ничего настраивать не нужно по I2C как у I2S ЦАП, подключил и оно просто работает!
Ничего настраивать не нужно по I2C как у I2S ЦАП, подключил и оно просто работает!

Ну и светодиод... я решил изъевернуться и взял, что было под рукой — SMD RGB светодиод FC‑B1010RGBT‑HG:

Маленький :D
Маленький :D

Зато к нему очень удобно паять проволочки:

Не ну а чё, пад аккурат под проволочку, что не так то ?
Не ну а чё, пад аккурат под проволочку, что не так то ?

Результат:

4 проволочки
4 проволочки

Собственно, фотка примерно под конец сборки макета:

Да, тут я ещё параллельно думал собрать на модуле RTL-01, прикидывал варианты
Да, тут я ещё параллельно думал собрать на модуле RTL-01, прикидывал варианты

Локи уже подозревает что-то нехорошее:

Корпус

По причине того, что макет собирается на скорую руку, в итоге не получится готовый девайс такого вида:

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

Незатейливо
Незатейливо
По-хорошему надо бы ещё нормальное углубление для статусного светодиода, но я забил
По-хорошему надо бы ещё нормальное углубление для статусного светодиода, но я забил

Получается, динамик с чёрной декоративной тряпочкой вклеивается сверху, сбоку просверливается отверстие под статусный светодиод и снизу всё заливаем клеем.

Выглядит примерно вот так (забыл сфотать итоговый результат, а сейчас уже разобрано всё):

Ну... типа... нейроконнектор
Ну... типа... нейроконнектор

Аудио-файлы

А будем мы их брать, конечно же, из игры :) Надо получить примерно такой набор файлов:

Да, спустя месяц я нашёл, как ещё дораспаковать под 100 дополнительных звуков\фраз.. но для начала и этого хватило ;)
Да, спустя месяц я нашёл, как ещё дораспаковать под 100 дополнительных звуков\фраз.. но для начала и этого хватило ;)

Собственно, для распаковки нам нужна лицензионная игра с DLC 1 на ПК и UnrealPakTool. Распаковываем pakchunk13-WindowsNoEditor.pak командой:

UnrealPakTool\UnrealPak.exe "%path-to-game%\AtomicHeart\Content\Paks\pakchunk13-WindowsNoEditor.pak" -Extract "D:\atomic unpack\unpacked"

Далее потребуются утилиты:

  • ww2ogg - для преобразования wem (игровой формат звуковых файлов) в ogg

  • FFmpeg-Builds - для преобразования ogg в wav (скачивать ffmpeg-master-latest-win64-gpl.zip)

Преобразовываем все wem файлы в ogg следующей командой:

set folder=D:\atomic unpack\unpacked\WwiseAudio\Windows\Ru
for %%f in ("%folder%\*.wem") do ww2ogg.exe "%%f" --pcb packed_codebooks_aoTuV_603.bin

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

Далее перепаковываем полученные ogg в wav моно и 32 кГц (чтобы меньше весили, но качество голоса не слишком заметно упало, как, например, на 8 или 16 кГц) командой:

for %%f in ("*.ogg") do ffmpeg -hide_banner -loglevel error -i "%%f" -ab 128 -ar 32000 "%%f.wav"

После чего я разбиваю файлы на группы по префиксу + номеру файла:

Тут собственно уже можно догадаться, что есть 2 состояния - Idle и Move, а так же Special фраза
Тут собственно уже можно догадаться, что есть 2 состояния - Idle и Move, а так же Special фраза

Всё, теперь это можно паковать в образ файловой системы и заливать на SPI Flash любым удобным способом.:)

Примечание: с распаковкой сильно помогла данная статья — ATOMIC HEART вытаскиваем ресурсы, музыку и звуки.

Файловая система

Была взята из open‑source проекта RTL00_WEB, берём из него файлы webfs.c \ webfs.h с небольшими модификациями, чтобы работать не напрямую с памятью МК \ SoC, а внешней флешкой, подключенной по SPI.

Сами файлы, как уже было сказанно выше, проименованны специальным образом, и вот для чего. Объявляем состояния, в которых может находиться нейроконнектор (кот):

typedef enum
{
   NC_STATE_IDLE = 0,
   NC_STATE_MOVE,
   NC_STATE_ATTACK,
   NC_STATE_FREE_FALL,
   NC_STATE_ERROR = 0xff,
} nc_state_t;

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

typedef struct
{
   char prefix; // 'x'
   uint8_t start; //0 ...
   uint8_t end; /// n
   uint8_t luck; // to play audio 0...100
   uint32_t timeout; // ms to maybe play next file
} nc_state_audiofile_t;

Ну и, собственно, теперь можно обьявить экземпляры для каждого статуса (последняя строка):

typedef struct
{
   uint8_t playing_flag;
   uint8_t special_flag;
   uint32_t timeout;
   uint32_t timeout_sp;
   void *hi2s;
   void *hi2c;
   void *hspi;
   void *cs_port;
   uint16_t cs_pin;
   wav_header_t wavh;
   webfs_handle_t hfs;
   nc_state_t state;
   nc_state_audiofile_t audio[4]; // NC_STATE_IDLE ... NC_STATE_FREE_FALL
} nc_t;

А инитится это следующим образом:

   nc.audio[NC_STATE_IDLE].prefix = 'I';
   nc.audio[NC_STATE_IDLE].start = 0;
   nc.audio[NC_STATE_IDLE].end = 10;
   nc.audio[NC_STATE_IDLE].luck = 30;
   nc.audio[NC_STATE_IDLE].timeout = 50000;

   nc.audio[NC_STATE_MOVE].prefix = 'M';
   nc.audio[NC_STATE_MOVE].start = 0;
   nc.audio[NC_STATE_MOVE].end = 19;
   nc.audio[NC_STATE_MOVE].luck = 50;
   nc.audio[NC_STATE_MOVE].timeout = 10000;

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

Оно запускается! :O
Оно запускается! :O

Рандомайзер

Да-да.... несмотря на название и лор игры, никакими полимерами и даже просто нейросетями тут не пахнет. :(

Фальшивка...
Фальшивка...

Я не пограмист и не нейронщик... поэтому быдлокодим в лоб банальный рандомайзер для проигрывания случайного файла из текущего состояния:

void nc_RandomPlay()
{
   static char filename[10];
   static uint8_t s;
   uint8_t luck = rand() % 101;

   SysView_LogInfo("[nc] Luck: %u (Threshold: %u)\r\n", luck, nc.audio[nc.state].luck);
   if (luck <= nc.audio[nc.state].luck)
   {
      for (;;)
      {
         uint8_t n = rand() % (nc.audio[nc.state].end + 1);
         if (s != n)
         {
            s = n;
            break;
         }
      }

      if (s < nc.audio[nc.state].start)
      {
         s = nc.audio[nc.state].start;
      }

      snprintf(&filename[0], 10, "%c%u.wav", nc.audio[nc.state].prefix, s);
      //SysView_LogInfo("[nc] Generated filename: %s\r\n", filename);
      nc_PlayFile(&filename[0]);
   }
}

Который вызывается из вечного цикла в зависимости от текущего состояния через прописанные для этого состояния промежутки времени:

void nc_Routine()
{
   led_Routine();

   if ((LL_GPIO_IsInputPinSet(GPIOB, LL_GPIO_PIN_8)) && (nc.playing_flag == 0))
   {
      adxl345_GetIntSource(&adxl);

      if (((adxl.int_src & ADXL345_INT_EN_BIT_DOUBLE_TAP) > 0) &&
            SysTick_GetCurrentTick() - nc.timeout_sp > 5000)
      {
         nc_TapSpecialPlay();
         nc.state = NC_STATE_ATTACK;
         nc.timeout_sp = SysTick_GetCurrentTick();
         nc_UpdateLedStatus();
      }
      else if ((adxl.int_src & ADXL345_INT_EN_BIT_ACTIVITY) > 0)
      {
         nc.state = NC_STATE_MOVE;
         nc_UpdateLedStatus();
      }
      else if ((adxl.int_src & ADXL345_INT_EN_BIT_INACTIVITY) > 0)
      {
         nc.state = NC_STATE_IDLE;
         nc_UpdateLedStatus();
      }

      SysView_LogInfo("\r\n[nc] Set State: %u\r\n", nc.state);
      SysView_LogInfo("[nc] Int Status: 0x%x\r\n", adxl.int_src);
   }

   switch (nc.state)
   {
      case NC_STATE_IDLE:
      case NC_STATE_MOVE:
         if (nc.playing_flag == 0)
         {
            if (SysTick_GetCurrentTick() - nc.timeout > nc.audio[nc.state].timeout)
            {
               nc.timeout = SysTick_GetCurrentTick();
               nc_RandomPlay();
            }
         }
      break;

      case NC_STATE_ATTACK:
      case NC_STATE_FREE_FALL:
         if (nc.playing_flag == 0)
         {
            nc.state = NC_STATE_MOVE;
            nc_UpdateLedStatus();
            SysView_LogInfo("\r\n[nc] Set State: %u\r\n", nc.state);
            nc.timeout = SysTick_GetCurrentTick();
         }
      break;

      default: // NC_STATE_ERROR
      break;
   }
}

Увы, тут ничего прямо уж интересного и сложного нет.

Проигрывание аудио

С этим особо сложностей тоже не возникло, просто читаем из FS файл кратно выбранному буферу (в моём случае PLAYBACK_BUF_SIZE = 1024 байт), после чего копируем данные из полученного массива в буфер для I2S DMA 2 раза (в wav файле у нас mono, а для I2S в STM32 нужно stereo, поэтому полуслово данных необходимо копировать 2 раза) и выводим следующим образом:

void nc_PlayFile(char *name)
{
   nc.hfs = webfs_Open(name);
   if (nc.hfs != WEBFS_INVALID_HANDLE)
   {
      SysView_LogInfo("[wav] File %d name: %s (size %u)\r\n", nc.hfs, name, webfs_GetBytesRem(nc.hfs));
      uint8_t filedata[78];
      webfs_GetArray(nc.hfs, &filedata[0], 78);
      if (wav_parser_ParseHeader(&filedata[0], &nc.wavh) == 0x00)
      {
         SysView_LogInfo("[wav] Type %u Sample rate: %u Bits per sample: %u Channels: %u Size: %u Data Offset: %u\r\n",
                          nc.wavh.type, nc.wavh.sample_rate, nc.wavh.bits_per_sample, nc.wavh.channels, nc.wavh.size, nc.wavh.data_offset);
      }

      // wavh.size в байтах (2 на полуслово), а в playback_Start надо передавать размер в полусловах, но каналов 2 (!)
      playback_Start(read_buf, nc.wavh.size, 0);
      nc.playing_flag = 1;
      nc_UpdateLedStatus();
   }
}

Для автоматического проигрывания файла, начатого в nc_PlayFile, определяем буфер и callback:

static uint16_t buf[PLAYBACK_BUF_SIZE];
static void read_buf(uint16_t *dest, uint32_t count)
{
   // count в полусловах (2 канала)
   // webfs_GetArray запрашиваем в байтах
   // и в итоге один канал с флешки дублируем на 2

   //SysView_LogInfo("[wav] req %u\r\n", count);
   webfs_GetArray(nc.hfs, (uint8_t *)&buf[0], count);
   for (uint32_t i = 0; i < count / 2; i++)
   {
      dest[i * 2 + 0] = buf[i];
      dest[i * 2 + 1] = buf[i];
   }
}

Инитим микро-библиотеку, представляющую собой простую надстройку над I2S HAL:

   playback_Init(nc.hi2s);
   playback_SetStop_Callback(&nc_FileDone);

И, собственно, самописная библиотека для проигрывания.

playback.h

#ifndef _PLAYBACK_H_
#define _PLAYBACK_H_

#include "main.h"
#include <string.h>

#define PLAYBACK_BUF_SIZE                 1024 // 256 // 4096

typedef void (*fill_audio_buf_cb_t)(uint16_t *dest, uint32_t count);

void playback_Init(void *hi2s);
int playback_Start(fill_audio_buf_cb_t cb, const uint32_t len, uint8_t loop_audio);
void playback_Stop(void);
void playback_Pause(void);
void playback_Resume(void);
void playback_SetStop_Callback(void (*callback)(void));

void playback_I2S_Tx_Cplt_Callback(void);
void playback_I2S_Tx_HalfCplt_Callback(void);

#endif // _PLAYBACK_H_

playback.c

#include "playback.h"
#include "stm32f4xx_hal.h"

static fill_audio_buf_cb_t fill_audio_buf_cb;
static I2S_HandleTypeDef *i2s;
static uint8_t loop;
static uint32_t offset;
static uint32_t total_len;
static void (*on_playback_stopped)(void);
static uint16_t audio_bufs[2][PLAYBACK_BUF_SIZE];

void playback_Init(void *hi2s)
{
   i2s = (I2S_HandleTypeDef *) hi2s;
}

static void update_audio_buf(uint8_t buf_number)
{
   uint16_t *dest = audio_bufs[buf_number];
   size_t len = total_len - offset;

   if (!fill_audio_buf_cb)
   {
      return;
   }

   if (len > PLAYBACK_BUF_SIZE)
   {
      len = PLAYBACK_BUF_SIZE;
   }

   fill_audio_buf_cb(dest, len);
   offset += len;
}

void playback_Stop(void)
{
   HAL_I2S_DMAStop(i2s);
}

void playback_Pause(void)
{
   HAL_I2S_DMAPause(i2s);
}

void playback_Resume(void)
{
   HAL_I2S_DMAResume(i2s);
}

int playback_Start(fill_audio_buf_cb_t cb, const uint32_t len, uint8_t loop_audio)
{
   total_len = len;
   offset = 0;
   fill_audio_buf_cb = cb;
   update_audio_buf(0);
   loop = loop_audio;
   size_t count = len;
   if (count > PLAYBACK_BUF_SIZE)
   {
      count = PLAYBACK_BUF_SIZE;
   }

   return HAL_I2S_Transmit_DMA(i2s, (uint16_t *)&audio_bufs[0], count * 2);
}

void playback_SetStop_Callback(void (*callback)(void))
{
   on_playback_stopped = callback;
}

void playback_I2S_Tx_Cplt_Callback(void)
{
   if (offset >= total_len)
   {
      if (loop)
      {
         offset = 0;
      }
      else
      {
         playback_Stop();
         if (on_playback_stopped)
         {
            on_playback_stopped();
         }
      }
   }
   else
   {
      update_audio_buf(1);
   }
}

void playback_I2S_Tx_HalfCplt_Callback(void)
{
   if (loop || offset < total_len)
   {
      update_audio_buf(0);
   }
}

Да, ещё момент, простой парсер wav-заголовков.

wav_parser.h :

#ifndef _WAV_PARSER_H_
#define _WAV_PARSER_H_

#include <stdint.h>

#define WAV_PARSER_HEADER_LEN             44

typedef struct
{
   uint8_t type;
   uint16_t sample_rate;
   uint8_t bits_per_sample;
   uint8_t channels;
   uint32_t size;
   uint32_t data_offset;
} wav_header_t;

int wav_parser_ParseHeader(const uint8_t *header, wav_header_t *format);

#endif // _WAV_PARSER_H_

wav_parser.с :

#include "wav_parser.h"
#include <string.h>

int wav_parser_ParseHeader(const uint8_t *header, wav_header_t *format)
{
   const char *p = (char *)header;
   if (strncmp(p, "RIFF", 4) != 0) // 0
   {
      return 1;
   }

   p += 8;
   if (strncmp(p, "WAVE", 4) != 0) // 8
   {
      return 1;
   }

   p += 4;
   if (strncmp(p, "fmt", 3) != 0) // 12
   {
      return 1;
   }

   p += 8;
   format->type = p[0] | (p[1] << 8); // 20

   p += 2;
   format->channels = p[0] | (p[1] << 8); // 22

   p += 2;
   format->sample_rate = 0; // 24
   for (uint8_t i = 0; i < 4; i++)
   {
      format->sample_rate |= p[i] << (8 * i);
   }

   p += 10;
   format->bits_per_sample = p[0] | (p[1] << 8); // 34

   p += 2;
   for (uint8_t i = 0; i < 10; i++) // 10 chunks Max
   {
      if (strncmp(p, "data", 4) != 0)
      {
         p += 4;
         uint32_t chunk_len = (p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24));
         p += (4 + chunk_len);
      }
      else
      {
         p += 4;
         format->size = (p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24));
         //format->data_offset = (uint32_t)*p;
         format->data_offset = 78;//+= 4; // TODO: какой то баг, для файла получаю 90, а должно быть 78
         break;
      }
   }

   return 0;
}

Инициализация I2S и DMA выглядит следующим образом:

void SysInit_DMA(void)
{
   LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);

   hdma_spi3_tx.Instance = DMA1_Stream5;
   hdma_spi3_tx.Init.Channel = DMA_CHANNEL_0;
   hdma_spi3_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
   hdma_spi3_tx.Init.PeriphInc = DMA_PINC_DISABLE;
   hdma_spi3_tx.Init.MemInc = DMA_MINC_ENABLE;
   hdma_spi3_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
   hdma_spi3_tx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
   hdma_spi3_tx.Init.Mode = DMA_CIRCULAR;
   hdma_spi3_tx.Init.Priority = DMA_PRIORITY_HIGH;
   hdma_spi3_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; // DMA_FIFOMODE_ENABLE
   hdma_spi3_tx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
   hdma_spi3_tx.Init.MemBurst = DMA_MBURST_SINGLE;
   hdma_spi3_tx.Init.PeriphBurst = DMA_PBURST_SINGLE; 
   HAL_DMA_Init(&hdma_spi3_tx);
   __HAL_LINKDMA(&hi2s3, hdmatx, hdma_spi3_tx);

   //NVIC_SetPriority(SPI3_IRQn, 0);
   //NVIC_EnableIRQ(SPI3_IRQn);
   NVIC_SetPriority(DMA1_Stream5_IRQn, 0);
   NVIC_EnableIRQ(DMA1_Stream5_IRQn);
}

void SysInit_I2S(void)
{
   LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_SPI3);

   // I2S3: SD (PB5), extSD (PB4), CK (PB3), WS (PA15)
   LL_GPIO_SetPinMode(GPIOB, LL_GPIO_PIN_5, LL_GPIO_MODE_ALTERNATE);
   LL_GPIO_SetPinMode(GPIOB, LL_GPIO_PIN_3, LL_GPIO_MODE_ALTERNATE);
   LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_15, LL_GPIO_MODE_ALTERNATE);
   LL_GPIO_SetAFPin_0_7(GPIOB, LL_GPIO_PIN_5, LL_GPIO_AF_6);
   LL_GPIO_SetAFPin_0_7(GPIOB, LL_GPIO_PIN_3, LL_GPIO_AF_6);
   LL_GPIO_SetAFPin_8_15(GPIOA, LL_GPIO_PIN_15, LL_GPIO_AF_6);

   hi2s3.Instance = SPI3;
   hi2s3.Init.Mode = I2S_MODE_MASTER_TX;
   hi2s3.Init.Standard = I2S_STANDARD_PHILIPS;
   hi2s3.Init.DataFormat = I2S_DATAFORMAT_16B;
   hi2s3.Init.MCLKOutput = I2S_MCLKOUTPUT_DISABLE;
   hi2s3.Init.AudioFreq = I2S_AUDIOFREQ_32K;
   hi2s3.Init.CPOL = I2S_CPOL_LOW;
   hi2s3.Init.ClockSource = I2S_CLOCK_PLL;
   hi2s3.Init.FullDuplexMode = I2S_FULLDUPLEXMODE_DISABLE;
   HAL_I2S_Init(&hi2s3);
}

Прерывания:

void DMA1_Stream5_IRQHandler(void)
{
   HAL_DMA_IRQHandler(&hdma_spi3_tx);
}
void HAL_I2S_TxCpltCallback(I2S_HandleTypeDef * i2s)
{
   playback_I2S_Tx_Cplt_Callback();
}

void HAL_I2S_TxHalfCpltCallback(I2S_HandleTypeDef *hi2s)
{
   playback_I2S_Tx_HalfCplt_Callback();
}

Вывод

Получилось увлекательно и, блин, угарно, когда ты играешься с котом и он тебя кроет матом иногда очень в тему кидает фразы:D

Был как‑то такой момент, я отлаживал работу специальной фразы (что по двойному тапу по коту из начала видео) и для теста нацепил девайс на кота, который валялся на пледике рядом. Немного «потестировав», я отошёл обратно к компу и залип в написание прошивки, и так удачно совпало, что рандомизатор довольно долгое время в Idle состоянии ничего не выводил и я забыл о включенном девайсе.:) Сижу я значит, сижу и тут:

БУ! Испугался?!

Я аж вздрогнул XD

Саму статью я думал написать сильно раньше (с Хэллоуина так‑то уже немало времени прошло), но навалилось работы и потом мне просто было лень. Зато до Нового года я сделал следующую, нормальную версию железа нейроконнектора:

Пока микро-сикрет, на чём собрано)
Пока микро-сикрет, на чём собрано)

Успел пока только проверить основные узлы и, главное, работу SWD поверх Type-C с самодельным адаптером, выглядит это как то так:

К адаптеру подключается напрямую J-Link, а также USB Type-C, data линии которого проброшены до целевого МК (можно проверять работу USB Device)
К адаптеру подключается напрямую J-Link, а также USB Type-C, data линии которого проброшены до целевого МК (можно проверять работу USB Device)

Адаптер автоматически определяет подключение Type‑C кабеля, умеет определять его ориентацию и автоматически перекидывает SWDIO\SWCLK линии в соответствии с подключением. Главный косяк только в том, что тут не хватает одной линии для Reset, и если на целевом МК отключен отладочный интерфейс, то его так не прошить и никак не сбросить... можно, конечно, нарушить спецификации USB и задействовать линии CC1\2, но я так делать не стал.

В общем это, когда проверю плату полноценно и перенесу прошивку нейроконнектора для этой версии, то думаю тогда и сделать репу на гите, да статейку о нём и адаптере.;)

Спасибо, что прочитали! Надеюсь, было интересно хоть немного.