Всем привет, это моя первая статья, в рамках которой захотелось поделиться необычным для меня опытом. Возможно, кто-то найдёт здесь для себя что-то полезное. По работе пришлось столкнуться с крайне необычной микросхемой для коммутации высокочастотных (RF) сигналов. Одна проблема – проприетарный интерфейс управления MIPI RFFE. Появился вопрос “А как этим управлять, не имея специализированных средств?”. Ответ узнаем вместе.
Микросхема, о которой пойдёт речь – QPC1220Q, осуществляющая маршрутизацию сигнала из 4 входов в 2 выхода. И делать она это может одновременно для двух линий.
Функционально, она выглядит так:

Микросхема работает в довольно широком диапазоне от 617 МГц и до 6 ГГц с высокой линейностью – то есть, вносит очень мало собственных искажений в сигнал. Также стоит выделить компактность (корпус 2x2 мм) и энергоэффективность данной схемы. Максимальный ток потребления которой составляет 60 мкА, а в режиме низкого энергопотребления – 10 мкА. Недурно! Но дальше в область характеристик углубляться не будем, ибо я совсем не мой профиль.
Ключевой вопрос для меня – а как же проуправлять этой штукой?
Ответ таков – через интерфейс MIPI RFFE v2.1. Впервые слышите про такой? На момент работы, я тоже. Потому давайте разбираться.
MIPI RFFE - это двухпроводной интерфейс, использующий тактовый сигнал (SCLK) и двунаправленную линию данных (SDATA) для управления до 15-ти устройствами на одной шине, де-факто, являющийся современным и крайне широко распространённым стандартом управления подобными схемами внутри мобильных устройств с сотовой связью (и не только). Большое число современных смартфонов имеют данный интерфейс на своём борту.
Круто, очень популярный в профессиональной сфере интерфейс(пускай и нишевый), значит, наверное, ничего придумывать самому и не придётся, правда же?

К сожалению, нет, готового решения в виде аппаратного модуля, библиотеки и т.д. от производителя или сообщества - нет. Да и, в целом, по данному интерфейсу мало информации. Потому пришлось придумывать, как можно эмулировать работу этого интерфейса либо через другие интерфейсы, либо через GPIO контроллера. А значит надо искать спецификацию на сам интерфейс. И именно что искать, да ещё и ночью с собаками да фонариком. Потому что просто так получить её не выйдет при всём желании. Ибо альянс MIPI, занимающийся стандартизацией и развитием данного интерфейса, предоставляет её лишь организациям-членам альянса, и просто так на открытом месте в интернете она не лежит.
Долгий поиск на китайских и других зарубежных форумах всё же вывел к искомому. А значит пора перейти к делу. В данной статье будут некоторые выдержки оттуда, но далеко не всё, ибо она на ~230 страниц.
Если мы обратимся к даташиту на микросхему QPC1220Q, то, для управления, нам нужно писать биты в соответствующие позиции регистра 0x0001 – SW_CTRL:

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


Также, для коммуникации нам понадобится знать адрес устройства на шине, он определяется, в моём случае, приходящим на ногу USID напряжением. Высокий уровень – адрес 0x7, низкий – 0x6.
Вернёмся к триггерам. Триггеры – механизм, позволяющий осуществлять более сложные алгоритмы управления. В зависимости от устройства, к некоторым регистрам могут быть привязаны триггеры и ассоциируемые с ними “теневые” регистры. Как это работает? Если триггер включён, то, при записи в регистр, данные будут записаны в него не сразу, а помещены в промежуточный буфер. Из буфера они будут помещены уже в регистр только при обращении (записи единицы) непосредственно к триггеру.
С регистром SW_CTRL связаны триггеры 0, 1 и 2 из регистра 0x001C ─ PM_TRIG:

Как можно заметить, тут нам нужна обычная команда записи, в отличие от SW_CTRL, в котором требуется команда WM (Write Masked) – она же команда записи через маску.
Мне для работы триггеры не нужны и даже будут мешать, потому их потребуется отключать при начале работы устройства. После чего уже напрямую работать с SW_CTRL без посредника.
Предпочту есть слона по частям, потому давайте начнём с отключения триггеров, для чего, наконец-то, заглянем в спецификацию интерфейса и посмотрим, как выглядит команда записи в регистр:

Здесь мы можем заметить довольно много нюансов, которые надо учесть:
SSC (Sequence Start Condition) – стартовая последовательность
-
Register Write Command Frame, состоящий из:
SA (slave address) – адрес ведомого устройства (нашей QPC) из 4 бит
Код команды Register Write из 3 бит – 010
Адрес целевого регистра из 5 бит
Бит чётности P
-
Data Frame, состоящий из:
Полезной нагрузки, 8 бит
Бит чётности P
BPC (Bus Park Cycle) – завершающая последовательность
Чтобы разобраться, что мы перед собой вообще видим, нужно углубиться в архитектуру построения самих команд интерфейса, а она тут довольно таки сильно отличается от привычных интерфейсов общего назначения. Немаловажны технические требования интерфейса: от них зависит успех операции.
SSC (Sequence Start Condition)
SSC – уникальная стартовая последовательность, которую может тактировать только ведущий на линии. Она характеризуется высоким уровнем сигнала на линии SDATA в течение одного периода SCLK, а после низким уровнем так же в течение одного периода.

BPC (Bus Park Cycle)
BPC инициируется ведущим устройством шины в конце передачи данных, сигнализирую о завершении отправки посылки. Для этого на линии SDATA устанавливается низкий уровень, а на линии SCLK создаётся кратковременный высокий уровень.

Кадры
В интерфейсе описано три базовых типа кадров:
Command Frame, (13 бит)
Data/Address Frame, (9 бит)
No Response Frame, (9 бит)
Command Frame
Кадр команды должен состоять из 4 бит адреса ведомого устройства и 8 бит полезной нагрузки, которая может состоять только из кода команды, или из кода команды и адреса регистра, и одного бита чётности – всего 13 бит.

Data/Address Frame
Кадр данных (или адреса), состоит из 8 бит информации, завершаясь битом чётности. Если кадр имеет в качестве нагрузки адрес – называется адресным, если данные – кадром данных, соответственно. В остальном, они идентичны и различаются лишь своим местом в общей посылке.

No Response Frame
Все биты кадра заполнены “нулями”. Этот кадр представляет собой стандартный вид ответа на некорректную команду. Честно говоря, не совсем понятно, что подразумевали разработчики интерфейса, включая его в спецификацию, поскольку, на практике, его применение ну совсем уж ограничено.

Parity Bit
Каждый кадр должен заканчиваться одним битом чётности. Представляет собой результат общего количества бит посылки, приведённых к высокому уровню. Так, если посылка 0x63 (0b0110_0011), то бит чётности должен быть равен “1”. Если же, например, посылка 0x4C (0b0100_1100), то бит чётности должен быть равен “0”.
Теперь, когда мы изучили всё необходимое, можно уже и думать, а как бы нам так взять и сделать то, что мы хотим, а именно эмулировать команду записи в регистр PM_TRIG.
Нам требуется протактировать:
Стартовую последовательность SSC, (4 бита)
Командный кадр, (13 бит)
Кадр данных, (9 бит)
BPC (1 бит)
И вот на этом этапе самые внимательные уже могут увидеть некоторые проблемы. Формат используемых сообщений больно уникальный, и не позволяет себя эмулировать через SPI или I2C . Поскольку у первого, посылка либо 8, либо 16 бит, а у второго фиксированная 8 плюс 7 или 10 адрес. Если SSC и BPC ещё как-то можно протактировать, переключая GPIO из режима интерфейса обратно в GPIO, то вот сама посылка уже не укладывается. А значит придётся использовать только GPIO.
В контексте этого, стоит обсудить физические требования интерфейса. И начнём с требования по скорости работы:

Как можно заметить, есть два режима работы – стандартный и расширенный частотные диапазоны. Мы будем рассматривать попадание только в стандартный диапазон. Добиться скоростей выше одного МГц при эмуляции на микроконтроллере, с учётом того, что он должен решать и другие задачи, будет очень сложно. Можно было бы подумать, что GPIO должны переключаться между состояниями высокого и низкого уровня на той частоте, которой тактируется их шина. Однако же нет, так это не работает на практике, и не все производители напишут возможную скорость. Собственно, а что мы хотели за 3 рубля и булочку с сосиской на ARM? Хочется реальной скорости – бери FPGA. Так что обращаем на это внимание в первую очередь, не каждый контроллер сможет уложиться в требуемый диапазон.
Теперь к временным требованиям сигнала:


Ну тут уже не всё так страшно, хотя скорость нарастания и спада должна быть ну очень большой.
В процессе работы, пришлось создавать реализацию сразу под два микроконтроллера на базе ядра ARM:
STM32F411CEU6
AT32F413KCU7-4
Вторая – клон из поднебесной на бюджетную серию контроллеров от STM, о чём не сложно догадаться по названию, их довольно просто найти на популярных маркетплейсах, они сопоставимы по характеристикам с STM, в каких-то местах могут быть и лучше. Но углубляться в это не будем. Рассматривать реализацию будем на примере чёрной таблетки, но работает и на том, и на том. Для работы с STM использовался CubeIDE и HAL, для AT – CMSIS и их библиотека с драйверами.
Для точного и своевременного тактирования нам требуется использовать таймер. Я буду использовать TIM2 на шине APB1 с максимальной частотой 50 МГц. Собственно, нам более и не надо.
Экспериментально было определено, что достаточно ровный меандр получается при интервале прерывания 10 микросекунд.
Соответственно, конфигурация таймера для меня следующая:
Prescaler |
0 |
Counter Mode |
Up |
Counter Period |
999 |
Internal Clock Division |
No Division |
Auto-reload preload |
Disable |
В моей реализации я использовал несколько структур:
typedef struct RFFE_config_struct{ TIM_HandleTypeDef *tim; //Указатель на сущность таймера TIM2 GPIO_TypeDef *GPIO_SCLK_BASE; //Указатель на Typedef GPIO, наш желаемый выход SCLK GPIO_TypeDef *GPIO_SDATA_BASE; //Указатель на Typedef GPIO, наш желаемый выход SDAT uint16_t rffe_sdata_pin; // GPIO_pins_define желаемого SDATA uint16_t rffe_sclk_pin; // GPIO_pins_define желаемого SCLK uint8_t rffe_slave_addr; // Адрес ведомого устройства }RFFE_cnfg_s;
А также структура:
typedef struct RFFE_send_statemachine_struct{ uint8_t curr_send_state; //Отслеживание текущего этапа отправки uint8_t ssq_first_enter; //Флаг начала отправки SSC uint8_t ssq_ticks_till_end; //Количество тиков до конца отправки SSC uint8_t bpc_first_enter; //Флаг начала отправки BPC uint8_t bpc_ticks; //Количество тиков до конца отправки BPC uint8_t pkg_bits_to_send; //Количество бит для отправки uint8_t pkg_iterator; //Итератор по отправляемым данным uint8_t rffe_sdata_direction; //Направление SDATA: отправка/чтение }RFFE_send_statemachine_s;
И из глобальных переменных остаётся массив для хранения бит на отправку:
#define PKG_DEFAULT_SIZE 40 //Стандартный размер посылки uint8_t rffe_data_package[PKG_DEFAULT_SIZE] = {0};
Вначале нам потребуется инициализировать структуры, делаем это через функцию:
void rffe_init(TIM_HandleTypeDef* tim, GPIO_TypeDef* gpio_sclk_base, GPIO_TypeDef* gpio_sdata_base, uint16_t sdata_pin, uint16_t sclk_pin, uint8_t rffe_slave_addr) { rffe_cfg.tim = tim; rffe_cfg.GPIO_SCLK_BASE = gpio_sclk_base; rffe_cfg.GPIO_SDATA_BASE = gpio_sdata_base; rffe_cfg.rffe_sclk_pin = sclk_pin; rffe_cfg.rffe_sdata_pin = sdata_pin; rffe_cfg.rffe_slave_addr = rffe_slave_addr; rffe_stmn_cfg.bpc_first_enter = 0; rffe_stmn_cfg.bpc_ticks = BPC_DEFAULT_TICKS_AMOUNT; rffe_stmn_cfg.ssq_first_enter = 0; rffe_stmn_cfg.ssq_ticks_till_end = 0; rffe_stmn_cfg.pkg_bits_to_send = 0; rffe_stmn_cfg.pkg_iterator = 0; rffe_stmn_cfg.curr_send_state = 0; rffe_stmn_cfg.rffe_sdata_direction = 0; }
В функцию, соответственно, передаём, в моём случае:
rffe_init(&htim2, //Ссылка на Instance таймера GPIOB, //GPIO_TypeDef SDATA GPIOB, //GPIO_TypeDef SCLK RFFE_SDATA_Pin, //Номер пина SDATA RFFE_SCLK_Pin, //Номер пина SCLK 0x06 //Адрес устройства QPC1220Q );
Обращу внимание, что пины SCLK и SDATA должны обязательно быть с подтяжкой к земле!
Теперь по шагам разберём функцию отправки команды обычной записи в регистр. В функцию мы передаём адрес регистра для записи, а также сами данные, которые мы хотим записать.
void rffe_send_write_cmd(uint8_t reg_addr, uint8_t payload) { config_output(); rffe_clear_data_package(rffe_data_package); rffe_set_statemachine(WRITE_TICKS_AMOUNT, BPC_DEFAULT_TICKS_AMOUNT, RFFE_SDATA_DIRECTION_OUTPUT ); create_cmd_frame(rffe_cfg.rffe_slave_addr, reg_addr, COMMAND_FRAME_CMD_PAYLOAD_BITS_SIZE, rffe_data_package, WRITE_CMD ); create_data_frame(rffe_data_package, payload, DATA_FRAME_START_POS, DATA_FRAME_END_POS, DATA_FRAME_PARITY_POS ); __HAL_TIM_SET_COUNTER(rffe_cfg.tim, 0); HAL_TIM_Base_Start_IT(rffe_cfg.tim); }
Конфигурируем пин SDATA на выход:
void config_output() { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = rffe_cfg.rffe_sdata_pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_PULLDOWN; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(rffe_cfg.GPIO_SDATA_BASE, &GPIO_InitStruct); }
Предварительно, очищаем массив-буфер:
void rffe_clear_data_package(uint8_t package[]) { for(uint8_t i = 0; i < PKG_DEFAULT_SIZE; i++){ package[i] = 0; } }
Устанавливаем первоначальное состояние структуры:
void rffe_set_statemachine(uint8_t pkg_bits_to_send, uint8_t bpc_ticks, uint8_t sdata_direction) { rffe_stmn_cfg.bpc_first_enter = 0; rffe_stmn_cfg.bpc_ticks = bpc_ticks; rffe_stmn_cfg.ssq_first_enter = 0; rffe_stmn_cfg.ssq_ticks_till_end = 0; rffe_stmn_cfg.pkg_bits_to_send = pkg_bits_to_send; rffe_stmn_cfg.pkg_iterator = 0; rffe_stmn_cfg.curr_send_state = 0; rffe_stmn_cfg.rffe_sdata_direction = sdata_direction; }
Внутрь функции передаются предопределённые значения конкретно для команды записи:
WRITE_TICKS_AMOUNT, 44
BPC_DEFAULT_TICKS_AMOUNT, 1
RFFE_SDATA_DIRECTION_OUTPUT, 0
Откуда они взялись?
Всё просто – по количеству импульсов линии SCLK для команды Register Write. Кому интересно – можете посчитать :)
Далее, создаём командный кадр:
void create_cmd_frame(uint8_t slave_addr, uint8_t cmd_frame_payload, uint8_t cmd_frame_bits_count, uint8_t cmd_frame[], uint8_t cmd_mask ) { uint8_t tmp = cmd_mask | cmd_frame_payload; uint8_t parity_counter = 0; for(int16_t i = 11; i >= 0; i--){ if (cmd_frame_bits_count > 0){ cmd_frame[i] = (tmp & 0x1); tmp >>= 1; cmd_frame_bits_count--; } else { cmd_frame[i] = (slave_addr & 0x1); slave_addr >>= 1; } if (cmd_frame[i]){ parity_counter++; } } if ((parity_counter % 2) == 0){ cmd_frame[12] = 1; } else { cmd_frame[12] = 0; } }
В функцию передаются, соответственно:
COMMAND_FRAME_CMD_PAYLOAD_BITS_SIZE, 8 – размер полезной нагрузки командного кадра
WRITE_CMD, 0b01000000 – код команды записи
После остаётся только создать кадр данных:
void create_data_frame(uint8_t cmd_frame[], uint8_t data_frame_payload, uint8_t start_pos, uint8_t end_pos, uint8_t parity_pos ) { uint8_t parity_counter = 0; for (uint8_t i = start_pos; i >= end_pos; i--){ cmd_frame[i] = (data_frame_payload & 0x1); data_frame_payload >>= 1; if (cmd_frame[i]){ parity_counter++; } } if ((parity_counter % 2) == 0){ cmd_frame[parity_pos] = 1; } else { cmd_frame[parity_pos] = 0; } }
В функцию передаются:
DATA_FRAME_START_POS, 20
DATA_FRAME_END_POS, 13
DATA_FRAME_PARITY_POS, 21
После чего обнуляем таймер, запускаем и включаем прерывание.
Рассмотрим код функции прерываний, и остановимся на ней подробнее:
void TIM2_IRQHandler(void) { /* USER CODE BEGIN TIM2_IRQn 0 */ if (__HAL_TIM_GET_FLAG(rffe_cfg.tim, TIM_FLAG_UPDATE) != RESET) { if (__HAL_TIM_GET_ITSTATUS(rffe_cfg.tim, TIM_IT_UPDATE) != RESET) { __HAL_TIM_CLEAR_FLAG(rffe_cfg.tim, TIM_FLAG_UPDATE); switch (rffe_stmn_cfg.curr_send_state) { case 0: if (!rffe_stmn_cfg.ssq_first_enter){ GPIOB->BSRR = rffe_cfg.rffe_sdata_pin; rffe_stmn_cfg.ssq_first_enter++; }else if (rffe_stmn_cfg.ssq_ticks_till_end <= SSQ_DEFAULT_TICKS_AMOUNT){ if (rffe_stmn_cfg.ssq_ticks_till_end == 1){ GPIOB->BSRR = (uint32_t)rffe_cfg.rffe_sdata_pin << 16U; } rffe_stmn_cfg.ssq_ticks_till_end++; } else { rffe_stmn_cfg.curr_send_state++; } break; case 1: if (rffe_stmn_cfg.pkg_bits_to_send > 0){ GPIOB->ODR ^= rffe_cfg.rffe_sclk_pin; if (GPIOB->ODR & rffe_cfg.rffe_sclk_pin){ if (rffe_data_package[rffe_stmn_cfg.pkg_iterator]){ GPIOB->BSRR = rffe_cfg.rffe_sdata_pin; } else { GPIOB->BSRR = (uint32_t)rffe_cfg.rffe_sdata_pin << 16U; } rffe_stmn_cfg.pkg_iterator++; } rffe_stmn_cfg.pkg_bits_to_send--; } else { rffe_stmn_cfg.curr_send_state++; } break; case 2: if (!rffe_stmn_cfg.bpc_first_enter){ GPIOB->BSRR = (uint32_t)rffe_cfg.rffe_sdata_pin << 16U; rffe_stmn_cfg.bpc_first_enter++; if (rffe_stmn_cfg.rffe_sdata_direction){ config_input(); } } if (rffe_stmn_cfg.bpc_ticks > 0){ rffe_stmn_cfg.bpc_ticks--; GPIOB->ODR ^= rffe_cfg.rffe_sclk_pin; } else { GPIOB->BSRR = (uint32_t)rffe_cfg.rffe_sclk_pin << 16U; HAL_TIM_Base_Stop_IT(rffe_cfg.tim); } break; default: break; } config_output(); } } }
Глобальным регулятором этапа отправки пакета внутри функции является switch case. Он разделяет три состояния отправки:
0 – отправка SSC
1 – отправка самого пакета из rffe_data_package
2 – отправка BPS
Отправка SSC:
//Если первое вхождение – выставляем высокий уровень на SDATA if (!rffe_stmn_cfg.ssq_first_enter){ GPIOB->BSRR = rffe_cfg.rffe_sdata_pin; rffe_stmn_cfg.ssq_first_enter++; }else if (rffe_stmn_cfg.ssq_ticks_till_end <= SSQ_DEFAULT_TICKS_AMOUNT){ //На втором прерывании выставляем низкий уровень //Меняем этап отправки if (rffe_stmn_cfg.ssq_ticks_till_end == 1){ GPIOB->BSRR = (uint32_t)rffe_cfg.rffe_sdata_pin << 16U; } rffe_stmn_cfg.ssq_ticks_till_end++; } else { rffe_stmn_cfg.curr_send_state++; } break;
Отправка основного пакета:
//Пока есть биты для отправки, отправляем if (rffe_stmn_cfg.pkg_bits_to_send > 0){ //С каждым входом инвертируем состояние SCLK GPIOB->ODR ^= rffe_cfg.rffe_sclk_pin; //Начинаем тактирование по переднему фрону SCLK if (GPIOB->ODR & rffe_cfg.rffe_sclk_pin){ //В зависимости от бита, выставляем высокий или низкий уровень if (rffe_data_package[rffe_stmn_cfg.pkg_iterator]){ GPIOB->BSRR = rffe_cfg.rffe_sdata_pin; } else { GPIOB->BSRR = (uint32_t)rffe_cfg.rffe_sdata_pin << 16U; } rffe_stmn_cfg.pkg_iterator++; } rffe_stmn_cfg.pkg_bits_to_send--; } else { //Если не осталось битов для отправки – переходим в новое состояние rffe_stmn_cfg.curr_send_state++; } break;
Отправка BPS:
//Если первое вхождение, то опускаем SDATA в низкий уровень if (!rffe_stmn_cfg.bpc_first_enter){ GPIOB->BSRR = (uint32_t)rffe_cfg.rffe_sdata_pin << 16U; rffe_stmn_cfg.bpc_first_enter++; if (rffe_stmn_cfg.rffe_sdata_direction){ config_input(); } } //Пока возможно, тактируем линию SCLK //По завершении, выключаем прерывание и останавливаем таймер if (rffe_stmn_cfg.bpc_ticks > 0){ rffe_stmn_cfg.bpc_ticks--; GPIOB->ODR ^= rffe_cfg.rffe_sclk_pin; } else { GPIOB->BSRR = (uint32_t)rffe_cfg.rffe_sclk_pin << 16U; HAL_TIM_Base_Stop_IT(rffe_cfg.tim); } break;
Как видим, всё довольно просто! Посмотрим, что по характеристикам у нашего сигнала. Проверял я их через осциллограф Tektronix TDS1012.
Частота сигнала SCLK показывает стабильные 50кГц, что с некоторым запасом попадает в требуемый RFFE диапазон для стандартного режима. И это очень хорошо.

А вот передний и задний фронты завалились и не попали в требуемые параметры, превысив значение в ~10 раз, однако это не повлияло на работу, как бы странно то ни было.


Для записи через маску, сначала передаётся один байт данных, который и является маской, за которым следует байт информации, к которой и будет применена маска. Если в маске на месте определённого бита стоит 0, соответственный ему бит будет записан в целевой регистр. Если единица – данные будут проигнорированы.

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

void rffe_send_write_masked_cmd(uint8_t reg_addr, uint8_t mask, uint8_t payload) { config_output(); rffe_clear_data_package(rffe_data_package); rffe_set_statemachine(MASKED_WRITE_TICKS_AMOUNT, BPC_DEFAULT_TICKS_AMOUNT, RFFE_SDATA_DIRECTION_OUTPUT ); create_cmd_frame(rffe_cfg.rffe_slave_addr, 0x0, COMMAND_FRAME_CMD_PAYLOAD_BITS_SIZE, rffe_data_package, MASKED_WRITE_CMD //код команды 0b00011001 ); create_data_frame(rffe_data_package, reg_addr, ADDR_FRAME_START_POS, //20 ADDR_FRAME_END_POS, //13 ADDR_FRAME_PARITY_POS //21 ); create_data_frame(rffe_data_package, mask, MASK_FRAME_START_POS, //29 MASK_FRAME_END_POS, //22 MASK_FRAME_PARITY_POS //30 ); create_data_frame(rffe_data_package, payload, MDATA_FRAME_START_POS, //38 MDATA_FRAME_END_POS, //31 MDATA_FRAME_PARITY_POS //39 ); __HAL_TIM_SET_COUNTER(rffe_cfg.tim, 0); HAL_TIM_Base_Start_IT(rffe_cfg.tim); }
Для проверки, переключается ли QPC1220Q, был использован векторный анализатор цепей Rohde Schwarz ZVL13. Последовательно подавалась команда записи через маску, переключая ключ.
Пример переключения устройства с данной микросхемой между несколькими диапазонами частот:


Таким образом можно эмулировать работу и прочих команд данного интерфейса на сопоставимых по характеристикам микроконтроллерах. В чём желаю удачи всем.
Комментарии (11)

alcotel
09.04.2026 12:57С таким интерфейсом я ещё не сталкивался, почитать было интересно.
Ногодрыг прям сложный получился. Если не удаётся эмулировать такой спец.интерфейс аппаратно, типа через SPI или что-нибудь подобное, я пользуюсь запросом DMA от таймера. Сама DMA-передача идёт либо в регистры таймера, либо прямо в порт GPIO. Это сильно быстрее и точнее, чем прерывания.
Передний фронт SCLK Задний фронт SCLK
Что-то вы здесь намерили не то. По описанию мк фронт должен быть короче 4 нс. Либо на осциллографе включено ограничение полосы, либо забыли щуп в 1:10 переключить.
STM32F103 не на самой большой скорости

И микросхеме QPC1220Q вроде нужны логические уровни КМОП 1,8 В, а не 3,3 В.

TheGoblinEngineer Автор
09.04.2026 12:57Спасибо за комментарий. Рад, что было интересно)
Про DMA идея отличная, на момент реализации, почему-то, для себя выбрал реализацию с таймером. Собственно, на лучшую реализацию не претендую. Главное, что стабильно работает.
Что касается измерений на линии SCLK, как будет момент, постараюсь перепроверить для STM32F411CEU6. Активная работа над реализацией была давненько, что-то мог упустить) Однако точно помню, что на AT32F413KCU7-4 тайминги точно укладывались в спецификацию интерфейса.

noxep
09.04.2026 12:57Очень интересно! Спасибо за такой подарок всей тусовке! В моей волосатой голове начали бродить мысли где ещё это можно применить? Давайте накидаем вариантов?

nixtonixto
09.04.2026 12:57Этот кадр представляет собой стандартный вид ответа на некорректную команду. Честно говоря, не совсем понятно, что подразумевали разработчики интерфейса, включая его в спецификацию, поскольку, на практике, его применение ну совсем уж ограничено.
Это нужно для того, чтобы шина не подвисала до окончания таймаута, ожидая ответа от слэйва, когда к нему по каким-то причинам пришла некорректная команда и он её проигнорировал.
Добиться скоростей выше одного МГц при эмуляции на микроконтроллере, с учётом того, что он должен решать и другие задачи, будет очень сложно.
А зачем вам мегагерц? На вашем же скрине написано, что тактовая может быть от 32 кГц.

TheGoblinEngineer Автор
09.04.2026 12:57Добрый день, спасибо за комментарий
Это нужно для того, чтобы шина не подвисала до окончания таймаута, ожидая ответа от слэйва, когда к нему по каким-то причинам пришла некорректная команда и он её проигнорировал.
Не могли бы раскрыть подробнее? Насколько я понимаю, поводов для подвисания там вроде присутствовать не должно, т.к. шина всегда притянута вверх или вниз мастером или слейвом.
А зачем вам мегагерц? На вашем же скрине написано, что тактовая может быть от 32 кГц.
В статье я и не утверждаю, что целюсь в такие скорости. Однако, поскольку информации в интернете по данному интерфейсу особо нет, на момент реализации, я старался с запасом попасть в требуемые RFFE параметры. Чтобы исключить лишние неопределённости. Потому старался добиться максимальных скоростей тактирования на шину.
Добиться ровного и стабильного тактирования на скорости в 1МГц и более не вышло.

nixtonixto
09.04.2026 12:57Мастер после запроса ждёт ответ и, если слэйв проигнорировал запрос из-за помех в линии - может ждать бесконечно долго или до срабатывания таймаута. Поэтому, чтобы такого не было - слэйв всегда максимально быстро отвечает или запрошенными данными, или таким вот ответом.

TheGoblinEngineer Автор
09.04.2026 12:57В RFFE мастер всегда тактирует линию SCLK, так что он бы не ждал слейва в любом случае, на самом деле. Потому, собственно, я и не очень вижу смысла именно в таком формате ответа. Если бы отрицательный ответ слейва имел бы более характерную последовательность, не состоящую только из нулей, тогда хотя бы было бы понятно, что слейв жив. А так это какой-то пользы не особо представляет, имхо
HardWrMan
Э как у них I2C покукожило то...
TheGoblinEngineer Автор
Самое обидное, что в I2C у тебя хотя бы есть механизм ACK/NACK. У RFFE устройств такого нет. И сиди гадай, ты не прав, или все же микросхема. А она просто молчать будет до победного.
HardWrMan
Это, получается, как с женщинами.
pvvv
ну вот не хочешь ты платить филипсу (кстати, где он(с)) отчисления, возьми, как все остальные, просто придумай другое название и будет у тебя вместо i2c свой очередной twi/sccb/pmbus/... при этом совместимый, но нет же - "@#$нутым нет покоя"