ESPN DeportesKawhi Leonard: ¿Cómo será recordado cuando finalice su carrera?Daily MaverickTHE GATHERING 2026: Fixing failing cities is in business’s own interest, leaders sayESPN'Gonna let them do them': Cunningham ignoring Kanter Freedom, Whiteוואלהחיל האוויר בגל תקיפות נגד יעדי טרור בדרום לבנוןRTP DesportoPortugal perde com Espanha e falha meias da Liga Europeia de futebol de praiaInquirerBojie Dy hails Pisa improvement: ‘It’s a welcome news’BlickVon Pfäffikon ZH über Siders VS bis Susch GR: Das sind die schlimmsten Busunglücke der Schweiz20 Minuten«Wir sind ein gutes Team»: Annemarie Carpendale lobt ihren WayneAntara NewsIndian envoy: BRICS agenda aligns with Indonesia bilateral focusBillboardFlavor Flav Shares Personal 9/11 Memory & Honors ‘Heroes Who Ran Toward Danger’ on 25th AnniversaryGlobal News13-year-old charged after firearm seized at Ajax hotel: Durham policeComplete Sports2026 US Open Final: Rybakina, Sabalenka Target Grand Slam Title
The Daily Newsstand · Free, Always
Friday, September 11, 2026

UART over radio на PlutoSDR: от serial monitor до неполноценного терминала Raspberry Pi

Translate

В этой статье рассматривается процесс создания кастомной 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. Но пока не получается.

Рис. 1. Raspberry pi zero может управляться через UART

Рис. 1. 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 и переданы по радио. Но это только один из путей, а как сделали бы вы?

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.