Всем привет, это моя первая статья, в рамках которой захотелось поделиться необычным для меня опытом. Возможно, кто-то найдёт здесь для себя что-то полезное. По работе пришлось столкнуться с крайне необычной микросхемой для коммутации высокочастотных (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. Последовательно подавалась команда записи через маску, переключая ключ.
Пример переключения устройства с данной микросхемой между несколькими диапазонами частот:


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