В этой статье рассматривается процесс создания кастомной no-OS прошивки для клона PlutoSDR, которая позволит принимать UART данные и передавать их по радио, а на другой стороне — принимать данные из радиоканала и выводить их через UART.
Я не эксперт в области программирования ad9361. Это та область, которую я хочу изучить. Я пытаюсь разобраться в основах и мне проще учиться, когда я вижу, как это работает. Вот почему я это делаю. Не потому, что я уже всё знаю об ad9361. И если вы тоже чему‑то научитесь, прочитав мою статью, это будет здорово. Но пожалуйста, не полагайтесь на мои статьи, чтобы понять теорию. Есть множество отличных книг и других источников по этой теме, где вы сможете найти более подробную информацию. Я надеюсь, что благодаря этой статье вы научитесь применять некоторые базовые принципы программирования ad936x в реальных системах. Если вы заметите ошибку или что‑то, что можно улучшить, пожалуйста, напишите мне в комментариях или личных сообщениях. Тогда мы сможем учиться вместе, и, возможно, я смогу исправить это и показать в новых статьях.
Чтобы принимать данные по UART можно воспользоваться прерываниями. Первым делом инициализируем контроллер прерываний
int Init_GIC(void) { XScuGic_Config *p_GICConfig = NULL; int status; p_GICConfig = XScuGic_LookupConfig(XPAR_PS7_SCUGIC_0_DEVICE_ID); if (p_GICConfig == NULL) { status = XST_FAILURE; return status; } status = XScuGic_CfgInitialize(p_xInterruptController, p_GICConfig, p_GICConfig->CpuBaseAddress); status = XScuGic_SelfTest(p_xInterruptController); if (status != XST_SUCCESS) { xil_printf("GIC config init failed \r\n"); return XST_FAILURE; } Xil_ExceptionInit(); Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT,(Xil_ExceptionHandler)XScuGic_InterruptHandler,p_xInterruptController); return XST_SUCCESS; }
После выполнения этой функции система будет готова принимать прерывания. Потом инициализируем UART-контроллер
int Init_Uart(void) { XUartPs_Config *p_UartConfig = NULL; int status; u32 IntrMask; p_UartConfig = XUartPs_LookupConfig(XPAR_PS7_UART_1_DEVICE_ID); if (p_UartConfig == NULL) { status = XST_FAILURE; return status; } status = XUartPs_CfgInitialize(p_Uart, p_UartConfig, p_UartConfig->BaseAddress); if (status != XST_SUCCESS) { xil_printf("Uart config init failed \r\n"); return XST_FAILURE; } status = XUartPs_SelfTest(p_Uart); if (status != XST_SUCCESS) { return status; } IntrMask = XUARTPS_IXR_RXOVR | XUARTPS_IXR_RXFULL | XUARTPS_IXR_OVER; if (p_Uart->Platform == XPLAT_ZYNQ_ULTRA_MP) { IntrMask |= XUARTPS_IXR_RBRK; } XUartPs_SetInterruptMask(p_Uart, IntrMask); // Set FIFO threshold. XUartPs_SetFifoThreshold(p_Uart, 1); /* Set the UART in Normal Mode */ XUartPs_SetOperMode(p_Uart, XUARTPS_OPER_MODE_NORMAL); return XST_SUCCESS; }
Понадобится обработчик прерываний UART (Uart_ISR) и функция подключения UART к системе прерываний (addUartToInterruptSystem).
void Uart_ISR(void *call_back_ref,u32 Event, unsigned int EventData) { if (Event == XUARTPS_EVENT_RECV_DATA) { XUartPs_Recv(p_Uart, (ReceiveBuffer+ReceiveBufferPtr), 1); //read the data available in UART receive buffer if(ReceiveBuffer[ReceiveBufferPtr] == '\n') { ReceiveBufferPtr = 0; UartRecvIntrFlag = 1; } else if(++ReceiveBufferPtr == 64) { ReceiveBufferPtr = 0; } } } int addUartToInterruptSystem(void) { int status; status = XScuGic_Connect(p_xInterruptController,XPAR_PS7_UART_1_INTR,(Xil_ExceptionHandler)XUartPs_InterruptHandler ,(void *) p_Uart); // if (status != XST_SUCCESS) { return XST_FAILURE; } XScuGic_SetPriorityTriggerType(p_xInterruptController,XPAR_PS7_UART_1_INTR,UART_INTR_PRI,UART_INTR_TRIG); XUartPs_SetHandler(p_Uart, (XUartPs_Handler)Uart_ISR, p_xInterruptController); XScuGic_Enable(p_xInterruptController, XPAR_PS7_UART_1_INTR); return status; }
Эти функции необходимо вызвать и включить прерывания
void init_sec_stage(){ /* Initialize SCUGIC */ if( Init_GIC() == XST_SUCCESS) { xil_printf("\nSCUGIC initialized \r\n"); } /* Initialize UART1 */ if(Init_Uart() == XST_SUCCESS) { xil_printf("Uart initialized \r\n"); } /* Add UART1 to interrupt system */ if (addUartToInterruptSystem() == XST_SUCCESS) { xil_printf("Uart added to interrupt system \r\n"); } /* Enable interrupts */ xil_printf("Enabling interrupts \r\n"); Xil_ExceptionEnable(); xil_printf("Entering main loop \r\n"); }
После этого необходима функция для работы с UART, например, такая
void uart_control(struct ad9361_rf_phy *ad9361_phy, uint8_t *UartRecvIntrFlag, uint8_t *ReceiveBuffer, uint8_t *SendBuffer, XUartPs *p_Uart, bool *flagTx, unsigned char *payload, uint16_t *num_pack ){ if(*UartRecvIntrFlag) { (*UartRecvIntrFlag) = 0; //---------------------------------------------------------- for(uint8_t i = 0;i < 64;++i){ printf("%c", (char*)ReceiveBuffer[i]); if((char*)ReceiveBuffer[i]=='\n'){ break; } } //---------------------------------------------------------- } }
Весь этот исходный код можно добавить к коду, который использовался в статье «Как инициализировать ad9361». Тогда можно отправлять в Serial Monitor данные и они будут выводиться на экран.
На видео видно, что после загрузки можно вводить данные в Serial Monitor, но важно заканчивать их символом ‘\n‘. Именно этого ожидает обработчик прерывания, чтобы взвести флаг, который перехватывается в UART функции.
Подводя промежуточный итог, имеется функционал, который позволяет принимать и обрабатывать данные, поступающие через UART.
Чтобы передать данные через радиоканал и принять на другой плате, можно воспользоваться исходным кодом из статьи про алгоритм обхода в ширину. Для формирования пакета данных, готового к передаче по радио снова воспользуемся библиотекой Liquid-DSP. В UART функции скопируем поступившие данные в payload и взведём флаг для передачи данных по радио.
void uart_control(struct ad9361_rf_phy *ad9361_phy, uint8_t *UartRecvIntrFlag, uint8_t *ReceiveBuffer, uint8_t *SendBuffer, XUartPs *p_Uart, bool *flagTx, unsigned char *payload, uint16_t *num_pack ){ if(*UartRecvIntrFlag) { (*UartRecvIntrFlag) = 0; //---------------------------------------------------------- uint8_t len = 0; for(uint8_t i = 0;i < 64;++i){ if((char*)ReceiveBuffer[i]=='\n'){ len = i; break; } } // printf("debug: %d\n", len); // strlen((char*)ReceiveBuffer) if(len<60) memcpy(payload+3, ReceiveBuffer, len+1); // +1 для копирования в payload \n // strlen((char*)ReceiveBuffer) payload[0]=len; // strlen((char*)ReceiveBuffer); payload[1]= (*num_pack >> 8); // byteHigh // младшие байты отсекутся при сдвиге payload[2]= (*num_pack)++ & 0xff; // byteLow // берём маской младший байт, 0xff == 0b11111111 *flagTx=true; //---------------------------------------------------------- } }
В бесконечном цикле отслеживается состояние переменной flagTx. Когда она принимает значение 1, то выполняется framegen64_execute(fg, header, payload, buf) для формирования фрейма. Затем находится максимальная амплитуда (подробнее про это в статье про ретрансляцию под заголовком Передача и приём фрейма — прим. автора), вычисляется коэффициент нормализации, преобразуются данные, это загружается через DMA. Включается передатчик и фрейм 100 раз уходит в эфир. После этого SDR переводится в режим приёма.
if(flagTx){ flagTx =false; // execute generator and assemble the frame framegen64_execute(fg, header, payload, buf); // 1. Находим максимальную амплитуду float max_amp = 0.0f; for (i = 0; i < buf_len; i++) { float amp = cabsf(buf[i]); if (amp > max_amp) max_amp = amp; } // 2. Вычисляем коэффициент нормализации float normalize_gain = 0.9f / max_amp; // преобразуем данные for (i = 0; i < buf_len; i++) { // Масштабируем и квантуем // int16_t i_val = (int16_t)(crealf(buf[i]) * 32767.0f); // int16_t q_val = (int16_t)(cimagf(buf[i]) * 32767.0f); float i_norm = crealf(buf[i]) * normalize_gain; float q_norm = cimagf(buf[i]) * normalize_gain; int16_t i_val = (int16_t)(i_norm * 32767.0f); int16_t q_val = (int16_t)(q_norm * 32767.0f); // Проверка переполнения if (i_val > 32767 || i_val < -32768) { printf("WARNING: i_val overflow at %d: %d\n", i, i_val); } if (q_val > 32767 || q_val < -32768) { printf("WARNING: q_val overflow at %d: %d\n", i, q_val); } // Упаковываем в uint32_t frame_buffer[i] = ((uint32_t)(q_val & 0xFFFF) << 16) | (uint32_t)(i_val & 0xFFFF); } axi_dac_load_custom_data(ad9361_phy->tx_dac, frame_buffer, FRAME_LEN, (uintptr_t)dac_buffer); #if 1 ad9361_set_en_state_machine_mode(ad9361_phy, ENSM_MODE_TX); // ad9361_get_en_state_machine_mode(ad9361_phy, &ensm_mode); // printf("SPI control - TX: %s\n", ensm_mode == ENSM_MODE_TX ? "OK" : "Error"); no_os_mdelay(1); #endif uint16_t count_tx = 100; while(count_tx--){ // count_tx-- Xil_DCacheFlush(); /* Transfer the data. */ axi_dmac_transfer_start(tx_dmac, &transfer); /* Flush cache data. */ // Xil_DCacheInvalidateRange((uintptr_t)dac_buffer,sizeof(sine_lut_iq)); Xil_DCacheInvalidateRange((uintptr_t)dac_buffer, transfer.size); } #if 1 ad9361_set_en_state_machine_mode(ad9361_phy, ENSM_MODE_ALERT); no_os_mdelay(1); #endif #if 1 ad9361_set_en_state_machine_mode(ad9361_phy, ENSM_MODE_RX); no_os_mdelay(1); #endif }
Семплы принимаются, конвертируются для обработки в liquid-dsp и выполняется framesync64_execute(fs, rx1_samples, (ADC_BUFFER_SAMPLES * ADC_CHANNELS)/2).
/* Read the data from the ADC DMA. */ axi_dmac_transfer_start(rx_dmac, &read_transfer); /* Wait until transfer finishes */ status = axi_dmac_transfer_wait_completion(rx_dmac, 500); if(status < 0) return status; Xil_DCacheInvalidateRange((uintptr_t)adc_buffer, sizeof(adc_buffer)); convert_adc_buffer(adc_buffer, rx1_samples, (ADC_BUFFER_SAMPLES * ADC_CHANNELS)); // execute synchronizer and receive the entire frame at once framesync64_execute(fs, rx1_samples, (ADC_BUFFER_SAMPLES * ADC_CHANNELS)/2); // делю на 2, потому что не привожу данные с второго канала ацп к float
Если в принятых семплах обнаруживаются фреймы, то срабатывает callback, в котором можно вывести данные на экран.
// user-defined static callback function static int mycallback(unsigned char * _header, int _header_valid, unsigned char * _payload, unsigned int _payload_len, int _payload_valid, framesyncstats_s _stats, void * _userdata) { if(_header_valid){ #if 1 uint16_t val = (((uint16_t)_payload[1]) << 8) | _payload[2]; if(old_num_pac!=val){ old_num_pac = val; for(unsigned int i = 3; i < _payload[0]+3; i++) { // смещение на единицу для того чтобы выводить только полезную нагрузку и пропускать 1 байт, в котором количество printf("%c", _payload[i]); } printf("\n"); } #endif // type-cast, de-reference, and increment frame counter unsigned int * counter = (unsigned int *) _userdata; (*counter)++; } return 0; }
В эфир уходит 100 фреймов, чтобы гарантированно принять хотя бы один валидный фрейм, но иногда может быть принято больше одного фрейма. Происходит дублирование. Для защиты в каждое сообщение помимо длины, 2 байта отведены для нумерации сообщений. То есть в каждом из 100 дубликатов будет один и тот же номер. Поэтому на приёмной стороне очень просто дедуплицировать сообщения путём сравнения переменных.
К одному хосту подключены два PlutoSDR. На видео загружается прошивка в первую плату (на 31 секунде — прим. автора), затем во вторую (на 1:13 - прим. автора). Затем на 1:21 видно, как в UART первой платы отправляется “test“ и это сообщение появляется во втором serial monitor. А когда на 1:30 во вторую плату отправляется несколько раз “test“, то ничего не происходит, потому что строка не заканчивается символом ‘\n‘. Когда на 1:46 serial monitor настраивается на Line ending = LF, то сообщения из UART второй платы по радиоканалу попадают в первую плату. Потом отправляются короткие и длинные сообщения с обеих плат.
Подводя итоги, получилось реализовать приёмопередатчик на основе ad9361 и zynq-7000, который позволяет передавать всё, что приходит по UART, принимать данные из радиоканала и выводить на экран. Количество передаваемых байтов пока что ограничено только возможностями liquid-sdr, а они сравнительно огромные.
Честно говоря, хотелось управлять таким образом raspberry pi zero через uart. Но пока не получается.

На видео терминал подключён через ssh к raspberry pi zero. На raspberry pi запускается agetty, и в serial monitor на ноутбуке видно приглашение ввести логин и пароль. После авторизации можно работать через uart.
Так почему бы не включить место usb-to-com трансиверы с кастомной прошивкой?
На видео видно, что сначала оба приёмопередатчика подключены к одному хосту и тестируется связь. После этого один и приёмопередатчиков переключается в raspberry pi zero и в терминале RPI на 51 секунде запускается aggtty. Вместо длинного приглашения приходит только один символ ‘\n‘ на 52 секунде. Хорошо, видимо, не всё дошло. На 58 секунде вводится логин — в ответ что-то приходит, но только до символа ‘\n‘. На 1-02 вводится пароль — в ответ даже что-то приходит, но явно меньше чем надо. Дальнейшие попытки ввести команду “pwd“ остаются безуспешными. И лишь только на 1-28 приходит какое-то невнятное сообщение “Passwerrypi login: test“. Дальнейшие команды также безуспешны.
Так что же произошло? Agetty отправляет в UART по одному байту. Без какого-либо символа конца строки. Zynq-7000 настроен на приём символа ‘\n‘ как сигнала об окончании приёма данных по UART и их дальнейшей обработки. Плюс ко всему, буфер сделан лишь на 64 байта, а agetty отправляет гораздо больше. Почему через радиоканал было вообще что-то видно? Если проанализировать данные, которые приходят по UART, то там есть символы перехода на новую строку. Именно на это и реагирует zynq-7000 и отправляет данные, которые успел накопить к этому моменту, но это абсолютно не работает. Так что же делать? Как решение в лоб напрашивается писать очень хитрый обработчик UART под zynq-7000, но это неверно с точки зрения архитектуры в целом хоть это и может сработать. Гораздо корректнее будет разделить транспортный уровень радиоканала и уровень формирования данных agetty. Скорее всего, надо попробовать заставить работать agetty не с физическим /dev/ttyUSB0, а с виртуальным pty (на самом деле существует mingetty, которая уже из коробки работает с pty — прим. автора). А вот данные из pty выдёргивать простой программой на python3, например. Эта программа и будет формировать пачки байтов, которые корректно могут быть приняты по uart на zyqn-7000 и переданы по радио. Но это только один из путей, а как сделали бы вы?
Спасибо.
С.Н.
