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

В этой статье рассматривается процесс создания кастомной 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 и переданы по радио. Но это только один из путей, а как сделали бы вы?
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.