Названия музыкальных произведений, которые проигрываются на радиостанциях, можно получать прямо из радиоэфира с помощью radio data system (англ. Radio Data System, RDS). Представим ситуацию, когда из радиоприёмника играет музыка, которая очень понравилась, и стало интересно кто её автор. Конечно, можно "зашазамить", но ведь это не наш метод. Интереснее будет взять микросхему si4735 и подключить её к Arduino. Почему бы и нет. Есть хорошая библиотека SI4735. При таком подходе самым сложным будет качественно оформить устройство в корпусе с кнопками для изменения частоты и громкости, а ещё дисплеем и батареей, чтобы устройство было автономным. Далее будет описан способ с использованием Flipper Zero и созданием простого шилда для него. Тогда не надо терять время на кнопки и дисплей, но нужно немного потрудиться и перенести программную часть на Flipper Zero.
Скрытый текст
Ещё одним интересным вариантом может стать использование в качестве системной платы носимой радиостанции Quansheng UV-K5. Это вообще будет законченное устройство в хорошо защищённом корпусе. Программистам микроконтроллеров будет очень интересно, потому что сразу видно два варианта исполнения такого устройства. В первом варианте можно оставить плату радиостанции как есть и понадобится просто написать прошивку под STM32. А во втором варианте можно доработать радиостанцию с помощью модуля модификации и тогда откроются возможности КВ диапазона. Такие варианты могут оказаться очень интересными!
Работа с микросхемами SI473x очень хорошо описана в статье журнала Хакер. Всё, что там написано, проверено и работает. Но там нет работы с RDS. Так что эту статью можно считать просто продолжением той статьи в части работы с RDS.
Схемотехника
Использование Flipper Zero в качестве управляющего микроконтроллера упрощает схемотехнику. Необходимо только позаботиться о подключении через i2c и при этом не забыть подтягивающие резисторы, а ещё подобрать аудиоусилитель D-класса с подходящим динамиком. Остальное - обвязка. Эта схема во многом напоминает уже проверенную схему из журнала Хакер:

Здесь усилителем D-класса выступает модуль из Амперки. Если начать использовать этот шилд для прослушивания FM-радио, например, перед сном, то приёмник иногда захочется выключать, может быть даже по таймеру. Но тогда из динамика будет раздаваться шум, даже когда приложение для si4735 не будет выполняться Flipper Zero. Чтобы избежать этого ненужного шума, можно использовать управляющий вход S модуля. Тогда на обратной стороне модуля необходимо запаять перемычку. В таком случае при выходе из приложения управляющий пин не будет перетягивать резистор и модуль усилителя станет неактивным, а динамик будет молчать:

Ещё есть вход M, этот вход управляет функцией Mute. Это тоже можно использовать для удобства.
При реализации такой схемы в отладочной плате самым сложным может показаться обвязка для AM антенного входа. По большому счёту с ним можно и не мучиться, потому что при приёме RDS в FM диапазоне этот вход никак не участвует. Но при определённой доле усидчивости и сноровки всё возможно. Тогда можно будет выходить в парк подальше от помех и пробовать принять приводные маяки аэропортов, например, на частоте около 700 КГц работает приводной маяк Шереметьево. Или можно принять сигнал радиопередатчика RWM на частоте 4996 КГц.
Скрытый текст
В итоге очень просто и быстро можно получить вот такой прототип:



Программа
Как создавать приложения для Flipper Zero, описывалось уже много раз и можно на этом не останавливаться. Даже у автора этой статьи есть такие туториалы. Только хотелось бы отметить интересную особенность. При git clone актуальной версии прошивки Flipper Zero можно оказаться на branch dev и пока не была переключена release, приложение на железе не запускалось.
Все функции для инициализации и работы с si4735 можно взять из статьи журнала Хакер и немного доработать. Так, функция void delay(uint16_t ms)
остаётся без изменений, а в void si4734_reset()
надо просто правильно включить и выключить необходимый пин, например, вот так:
void si4734_reset(si4735App* app){
furi_hal_gpio_write(app->output_pin, false); // SI4734_RST_CLR();
delay(10); // delay(10); // furi_delay_ms(10);
furi_hal_gpio_write(app->output_pin, true); // SI4734_RST_SET();
delay(10); // delay(10); // furi_delay_ms(10);
}
В оригинальной статье использовалась библиотека LibopenCM3. Поэтому необходимо переделать все функции для работы с i2c. Тогда функции инициализации в FM и AM режимах могут выглядеть так:
uint8_t si4734_fm_mode(){
FURI_LOG_E(TAG, "si4734_fm_mode()");
// ARG1 (1<<4)|0 AN322 p130
// ARG2 00000101
uint8_t cmd[3]={POWER_UP,0x10,0x05};
uint8_t status=0, tray=0;
uint32_t timeout = 100;
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
furi_hal_i2c_tx(&furi_hal_i2c_handle_external, (SI4734ADR<<1), cmd, 3, timeout);
furi_delay_ms(1000); // furi_delay_ms(1000); // delay(1000);
do{
furi_hal_i2c_rx(&furi_hal_i2c_handle_external, ((SI4734ADR<<1)|0x1), &status, 1, timeout); //
tray++;
if(tray==255){
FURI_LOG_E(TAG, "tray==255");
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
return 0xff;
}
delay(50); // furi_delay_ms(50); // delay(50);
}while(status!=0x80);
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
return status; // status;
}
uint8_t si4734_am_mode(){
// ARG1 (1<<4)|1 AN322 p130
// ARG2 00000101
uint8_t cmd[3]={POWER_UP,0x11,0x05};
uint8_t status, tray=0;
uint32_t timeout = 100;
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
furi_hal_i2c_tx(&furi_hal_i2c_handle_external, (SI4734ADR<<1), cmd, 3, timeout);
furi_delay_ms(1000); // delay(1000);
do{
furi_hal_i2c_rx(&furi_hal_i2c_handle_external, ((SI4734ADR<<1)|0x1), &status, 1, timeout);
tray++;
if(tray==255) {
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
return 0xff;
}
delay(50);
}while(status!=0x80);
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
return status;
}
Вот в этом месте, когда осциллографом проверялся кварц и было обнаружено, что необходимо переключиться на ветку release. Далее совершался довольно рутинный процесс по переписыванию всех функций, которые касаются работы с i2c. Предлагается не останавливаться на этом и сосредоточиться на написании функций для работы с RDS.
В AN332 есть таблица 56. В этой таблице есть поле "RDS (Si4706/31/32/35/41/43/45/49 Only)". Согласно этому примеру, необходимо записать в регистр 0x1500 значение 0x0001. Для этого создана функция uint8_t si4735_RDS_set_interrupt(),
которая включает прерывание, связанное с RDS:
uint8_t si4735_RDS_set_interrupt(){
uint8_t status=0;
/*
Enable RDSRECV interrupt (set RDSINT bit when RDS has filled the
FIFO by the amount set on FM_RDS_INTERRUPT_FIFO_COUNT
Reply Status. Clear-to-send high
*/
status = si4734_set_prop(0x1500, 0x0001); // FM_RDS_INT_SOURCE
// usart_transmit(&tx_rb, "RDS_interrupt_setup: COMPLITED\r\n");
// char buff[30];
// sprintf(buff, "status = %02X\r\n", status);
return status;
}
Далее, согласно таблице 56, необходимо задать минимальное количество групп RDS, сохраняемых в RDS FIFO перед установкой RDSRECV. Для этого можно использовать функцию uint8_t si4735_RDS_set_group()
:
uint8_t si4735_RDS_set_group(){
uint8_t status=0;
/*
Sets the minimum number of
RDS groups stored in the
receive FIFO required before
RDSRECV is set.
*/
status = si4734_set_prop(0x1501, 0x0001); // FM_RDS_INT_FIFO_COUNT // 0x0004
// usart_transmit(&tx_rb, "RDS_set_group: COMPLITED\r\n");
// char buff[30];
// sprintf(buff, "status = %02X\r\n", status);
return status;
}
Дальше по таблице необходимо настроить параметры RDS для включения обработки RDS (RDSEN) и установки пороговых значений ошибок блоков RDS. Для этого в регистр 0x1502 пишется значение 0xFF01. Удобно воспользоваться функцией uint8_t si4735_Configures_RDS_setting():
uint8_t si4735_Configures_RDS_setting(){
uint8_t status=0;
/*
Configures RDS setting.
*/
status = si4734_set_prop(0x1502, 0xFF01); // FM_RDS_CONFIG // 0xEF01
// usart_transmit(&tx_rb, "Configures_RDS_setting: COMPLITED\r\n");
// char buff[30];
// sprintf(buff, "status = %02X\r\n", status);
return status;
}
Эти функции необходимо вызвать при запуске чипа в режиме FM или при переключении в другой режим, например, вот так:
void reciver_set_mode(si4735App* app, uint8_t rec_mod){
static uint16_t amfreq=8432,fmfreq=9920;//запоминаем старое значение // 8910
si4734_powerdown();
//частоты
// if(app->reciver_mode==_FM_MODE)fmfreq=app->freq_khz; else amfreq=app->freq_khz;
if(rec_mod==_AM_MODE){
//o_printf("AM mode\n");
app->reciver_mode=_AM_MODE;
si4734_am_mode();
si4734_set_prop(AM_CHANNEL_FILTER, 0x0100);
si4734_set_prop(AM_SOFT_MUTE_MAX_ATTENUATION, 0);//soft mute off
si4734_set_prop(AM_AUTOMATIC_VOLUME_CONTROL_MAX_GAIN, 0x5000); //60дб
si4734_set_prop(RX_VOLUME, app->vol);
//si4734_set_prop(AM_SEEK_BAND_TOP, 30000);
MIN_LIMIT=200;
MAX_LIMIT=30000;
//encoder=15200;
// app->freq_khz=amfreq-bfo/1000;//поправка на bfo // encoder=
app->freq_khz=amfreq;
bfo=bfo%1000;
FURI_LOG_I(TAG, "freq_khz:%d\r", app->freq_khz);
si4734_am_set_freq(app->freq_khz); // encoder
coef=1;
app->coef=coef;
encoder_mode=0;
} else if(rec_mod==_FM_MODE){
//oled_clear();
//o_printf("FM mode\n");
app->reciver_mode=_FM_MODE;
si4734_fm_mode();
si4734_set_prop(FM_DEEMPHASIS,0x0001);//01 = 50 µs. Used in Europe, Australia, Japan
si4734_set_prop(RX_VOLUME, app->vol);
MIN_LIMIT=6000;
MAX_LIMIT=11100;
coef=1; // coef=1;
app->coef=coef;
//encoder=8910;
// encoder=fmfreq;
app->freq_khz = fmfreq;
si4734_fm_set_freq(app->freq_khz); // encoder
encoder_mode=0;
//-----RDS-----
// uint8_t status = 0;
// char buff[14];
si4735_RDS_set_interrupt();
// sprintf(buff, "status = %c\r\n", status);
// usart_transmit(&tx_rb, buff);
si4735_RDS_set_group();
// sprintf(buff, "status = %2X\r\n", status);
// usart_transmit(&tx_rb, buff);
si4735_Configures_RDS_setting();
// sprintf(buff, "status = %2X\r\n", status);
// usart_transmit(&tx_rb, buff);
// status = si4734_get_int_status();
// sprintf(buff, "status = %2X\r\n", status);
// usart_transmit(&tx_rb, buff);
}else{
app->reciver_mode=_SSB_MODE;
//bfo=0;
si4734_ssb_patch_mode(ssb_patch_content);
si4734_set_prop(0x0101,((1<<15)|(1<<12)|(1<<4)|2));//ssb man page 24
si4734_set_prop(SSB_BFO, bfo);
si4734_set_prop(AM_SOFT_MUTE_MAX_ATTENUATION, 0);//soft mute off
si4734_set_prop(AM_AUTOMATIC_VOLUME_CONTROL_MAX_GAIN, 0x7000); //84дб
si4734_set_prop(RX_VOLUME, app->vol);
MIN_LIMIT=200;
MAX_LIMIT=30000;
//encoder=7100;
app->freq_khz=amfreq; // app->freq_khz // encoder
si4734_ssb_set_freq(app->freq_khz);
coef=1;
app->coef=coef;
encoder_mode=0;
}
}
Эта функция из оригинальной статьи в журнале Хакер. С дополнением в строках 46, 49 и 52, где вызываются вышеописанные функции.
А дальше необходимо вернуть информацию RDS для текущего канала и считать запись из FIFO RDS. Информация RDS включает в себя статус синхронизации, статус FIFO, групповые данные (блоки A, B, C и D) и исправленные ошибки блоков. Для этого реализована важная функция:
uint8_t si4735_RDS_status(uint16_t *BLOCKA, uint16_t *BLOCKB, uint16_t *BLOCKC,
uint16_t *BLOCKD, uint8_t *RDSFIFOUSED, uint8_t *RESP1,
uint8_t *RESP2, uint8_t *RESP12){
uint8_t cmd[3]={0x24,0x1};
uint8_t tray=0;
uint8_t answer[13];
uint32_t timeout = 100;
furi_hal_i2c_acquire(&furi_hal_i2c_handle_external);
furi_hal_i2c_tx(&furi_hal_i2c_handle_external, (SI4734ADR<<1), cmd, 2, timeout); // i2c_transfer7(SI4734I2C,SI4734ADR,cmd,2,0,0);
delay(50);
answer[0]=0;
while(answer[0]==0){
furi_hal_i2c_rx(&furi_hal_i2c_handle_external, ((SI4734ADR<<1)|0x1), answer, 13, timeout); // i2c_transfer7(SI4734I2C,SI4734ADR,0,0,answer,13);
tray++;
if(tray==255) {
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
return 0xff;
}
delay(50);
}
/*
int val = ADCL + (ADCH << 8);
*/
*BLOCKA=answer[5] + (answer[4] << 8);
*BLOCKB=answer[7] + (answer[6] << 8);
*BLOCKC=answer[9] + (answer[8] << 8);
*BLOCKD=answer[11] + (answer[10] << 8);
*RDSFIFOUSED=answer[3];
*RESP1=answer[1];
*RESP2=answer[2];
*RESP12=answer[12];
furi_hal_i2c_release(&furi_hal_i2c_handle_external);
return answer[0];
}
Теперь, когда информация записана в переменные, можно выводить её в человекочитаемый вид. Для такой реализации вывода RDS информации можно воспользоваться этой страницей. Там автор очень хорошо описывает RDS и делится исходным кодом для Arduino. Только там используется другая микросхема, но для тех, кто хочет поразбираться с RDS и si473x это даже хорошо. Используя даташиты и примеры можно создать вообще всё что угодно. И там выводится информация только из групп 0A, 0B и 4A. Чтобы выводить ещё и радиотекст, можно взять часть кода из библиотеки SI4735.
В Arduino вся обработка происходит в функции loop(), поэтому можно создать функцию void show_RDS_hum_2(si4735App* app)
, которая будет вызываться в бесконечном цикле приложения si4735. Если внимательно сравнить функции из статьи и ту, которая ниже, то можно заметить, что для групп 0A, 0B и 4A они эквивалентны, с небольшими изменениями под используемый микроконтроллер. А вот кусок кода для группы 2A уже взят из библиотеки Si4735:
void show_RDS_hum_2(si4735App* app){
// UNUSED(app);
uint8_t errLevelA, errLevelB, errLevelC, errLevelD, groupType;
UNUSED(errLevelA);
UNUSED(errLevelB);
UNUSED(errLevelC);
UNUSED(errLevelD);
bool groupVer;
// char buff[30];
// uint16_t BLOCKA, BLOCKB, BLOCKC, BLOCKD;
// get_recivier_RDS_status(&BLOCKA, &BLOCKB, &BLOCKC, &BLOCKD);
// print_RDS();
uint16_t BLOCKA, BLOCKB, BLOCKC, BLOCKD;
uint8_t status,RDSFIFOUSED,RESP1,RESP2,RESP12;
UNUSED(status);
//-----добавил на втором этапе--------------------------------------
si47x_rds_blockb blkB;
//------------------------------------------------------------------
status = get_recivier_RDS_status(app, &BLOCKA, &BLOCKB, &BLOCKC, &BLOCKD, &RDSFIFOUSED, &RESP1, &RESP2, &RESP12);
if(RESP1&RDSRECV_MASK){
if(RESP2&RDSSYNC_MASK && RDSFIFOUSED > 0){
if (BLOCKA == MaybeThisIDIsReal) {
if (IDRepeatCounter < REPEATS_TO_BE_REAL_ID) {
IDRepeatCounter++; // Значения совпадают, отразим это в счетчике
if (IDRepeatCounter == REPEATS_TO_BE_REAL_ID)
ID = MaybeThisIDIsReal; // Определились с ID станции
}
}
else {
IDRepeatCounter = 0; // Значения не совпадают, считаем заново
MaybeThisIDIsReal = BLOCKA;
}
if (ID == 0) return; // Пока не определимся с ID, разбирать RDS не будем
if (BLOCKA != ID) return; // ID не совпадает. Пропустим эту RDS группу
// ID станции не скачет, вероятность корректности группы в целом выше
if (!ID_printed) { // Выведем ID
// Serial.print("ID: ");
// Serial.println(ID, HEX);
// sprintf(buff, "ID: %X\r\n", ID);
// usart_transmit(&tx_rb, buff);
// app->ID=ID;
ID_printed = true; // Установим флаг чтобы больше не выводить ID
}
if((RESP12&(BLEB_MASK<3))||true){ // с проверкой на ошибку не работает, у меня ошибки или неправильно запрограммировал?
// Блок B корректный, можем определить тип и версию группы
// status = get_recivier_RDS_status(&BLOCKA, &BLOCKB, &BLOCKC, &BLOCKD); // 1
if (!PTy_printed) { // Но сначала считаем PTy
if (PTy == (BLOCKB & RDS_ALL_PTY_MASK) >> RDS_ALL_PTY_SHIFT) {
// Считаем PTy корректным, выведем его
char *PTy_buffer = (char*) malloc(30);
// strcpy_P(PTy_buffer, (char*)pgm_read_word(&(PTyList[PTy])));
strcpy(PTy_buffer, PTyList[PTy]);
// Serial.print("PTy: ");
// usart_transmit(&tx_rb, "PTy: ");
// Serial.println(PTy_buffer);
// usart_transmit(&tx_rb, PTy_buffer);
// usart_transmit(&tx_rb, "\r\n");
// app->PTy_buffer=PTy_buffer; // запись ввобще неверна for(int i=0;i<size;++i){app->PTy_buffer[i]=PTy_buffer}
strcpy(app->PTy_buffer, PTy_buffer); // здесь ловится NULL pointer
free(PTy_buffer);
PTy_printed = true;
}
else PTy = (BLOCKB & RDS_ALL_PTY_MASK) >> RDS_ALL_PTY_SHIFT;
}
groupType = (BLOCKB & RDS_ALL_GROUPTYPE_MASK) >> RDS_ALL_GROUPTYPE_SHIFT;
groupVer = (BLOCKB & RDS_ALL_GROUPVER) > 0;
// ************* 0A, 0B - PSName, PTY ************
if ((groupType == 0)) { //if((groupType == 0) and (errLevelD < 3))
//blockD = getRegister(RDA5807M_REG_BLOCK_D);
// status = get_recivier_RDS_status(&BLOCKA, &BLOCKB, &BLOCKC, &BLOCKD); // 1
// Сравним новые символы PSName со старыми:
char c = (uint8_t)(BLOCKD >> 8); // новый символ // char c = uint8_t(BLOCKD >> 8); // новый символ
uint8_t i = (BLOCKB & (uint16_t)RDS_GROUP0_C1C0_MASK) << 1; // его позиция в PSName
if (PSName[i] != c) { // символы различаются
PSNameUpdated &= !((1 << i) != 0); // сбросим флаг в PSNameUpdated // здесь может быть необходимо ==0
PSName[i] = c;
}
else // символы совпадают, установим флаг в PSNameUpdated:
PSNameUpdated |= 1 << i;
// Аналогично для второго символа
c = (uint8_t)(BLOCKD & 255); // c = uint8_t(BLOCKD & 255);
i++;
if (PSName[i] != c) {
PSNameUpdated &= !((1 << i)!=0); // здесь может быть необходимо ==0
PSName[i] = c;
}
else
PSNameUpdated |= (1 << i); // здесь может быть надо !=0
// Когда все 8 флагов в PSNameUpdated установлены, считаем что PSName получено полностью
if (PSNameUpdated == 255) {
// Дополнительное сравнение с предыдущим значением, чтобы не дублировать в Serial
if (strcmp(PSName, PSName_prev) != 0) {
//Serial.print("PSName: ");
// usart_transmit(&tx_rb, "PSName: ");
//Serial.println(PSName);
// usart_transmit(&tx_rb, PSName);
// usart_transmit(&tx_rb, "\r\n");
// for(uint8_t i=0;i<9;++i){
// app->PSName[i]=PSName[i];
// }
strcpy(app->PSName, PSName);
strcpy(PSName_prev, PSName);
}
}
} // PSName, PTy end
// ******************************************
// ******** 2A - Gets the Text processed for the 2A group ********
/**
* @ingroup group16 RDS status
*
* @brief Gets the Text processed for the 2A group
*
* @return char* string with the Text of the group A2
*/
if ((groupType == 2) /*&& (groupVer == 0)*/ /* && getRdsVersionCode() == 0 */) {
// Process group 2A
// Decode B block information
// blkB.raw.highValue = currentRdsStatus.resp.BLOCKBH;
// blkB.raw.lowValue = currentRdsStatus.resp.BLOCKBL;
blkB.raw.highValue = BLOCKB >> 8;
blkB.raw.lowValue = BLOCKB;
rdsTextAdress2A = blkB.group2.address;
if (rdsTextAdress2A >= 0 && rdsTextAdress2A < 16)
{
getNext4Block(&rds_buffer2A[rdsTextAdress2A * 4], &BLOCKC, &BLOCKD);
rds_buffer2A[63] = '\0';
// return rds_buffer2A;
strcpy(app->rds_buffer2A, rds_buffer2A);
#if 0
if (strcmp(rds_buffer2A, rds_buffer2A_prev) != 0) {
strcpy(app->rds_buffer2A, rds_buffer2A);
strcpy(rds_buffer2A_prev, rds_buffer2A);
}
#endif
}
}
// ******************************************
// ******** 4A - Clock time and date ********
if ((groupType == 4) && (groupVer == 0)) { // (groupType == 4) and (groupVer == 0) and (errLevelC < 3) and (errLevelD < 3)
// blockC = getRegister(RDA5807M_REG_BLOCK_C);
// blockD = getRegister(RDA5807M_REG_BLOCK_D);
// status = get_recivier_RDS_status(&BLOCKA, &BLOCKB, &BLOCKC, &BLOCKD); // 1
// char buf[30];
unsigned long MJD;
uint16_t year;
uint8_t month, day;
MJD = (BLOCKB & RDS_GROUP4A_MJD15_16_MASK);
MJD = (MJD << 15) | (BLOCKC >> RDS_GROUP4A_MJD0_14_SHIFT);
// Serial.print("Date: ");
// usart_transmit(&tx_rb, "Date: "); // вывожу
if ((MJD < 58844) || (MJD > 62497)){
// Serial.println("decode error");
// usart_transmit(&tx_rb, "decode error\r\n"); // вывожу
}
else {
MJDDecode(MJD, &year, &month, &day);
if ((day <=31) && (month <= 12)) {
// sprintf(buf, "%02d.%02d.%04d\r\n", day, month, year); // вывожу
// Serial.println(buf);
// usart_transmit(&tx_rb, buf); // вывожу
}
else{
// Serial.println("decode error");
// usart_transmit(&tx_rb, "decode error\r\n"); // вывожу
}
}
long timeInMinutes;
uint8_t hours, minutes, LTO;
UNUSED(LTO);
hours = (BLOCKC & RDS_GROUP4A_HOURS4_MASK) << 4;
hours |= (BLOCKD & RDS_GROUP4A_HOURS0_3_MASK) >> RDS_GROUP4A_HOURS0_3_SHIFT;
minutes = (BLOCKD & RDS_GROUP4A_MINUTES_MASK) >> RDS_GROUP4A_MINUTES_SHIFT;
if ((hours > 23) || (minutes > 59)){
// Serial.println("Time: decode error");
// usart_transmit(&tx_rb, "Time: decode error\r\n"); // вывожу
}
else {
timeInMinutes = hours * 60 + minutes;
LTO = BLOCKD & RDS_GROUP4A_LTO_MASK;
if (BLOCKD & RDS_GROUP4A_LTO_SIGN_MASK) {
timeInMinutes -= (BLOCKD & RDS_GROUP4A_LTO_MASK) * 30;
if (timeInMinutes < 0) timeInMinutes += 60 * 24;
}
else {
timeInMinutes += (BLOCKD & RDS_GROUP4A_LTO_MASK) * 30;
if (timeInMinutes > 60 * 24) timeInMinutes -= 60 * 24;
}
hours = timeInMinutes / 60;
minutes = timeInMinutes % 60;
// sprintf(buf, "Time: %02d:%02d\r\n", hours, minutes); // вывожу
// Serial.println(buf);
// usart_transmit(&tx_rb, buf); // вывожу
}
} // Clock end
// ******************************************
}
}
// After this call, the control will be returned back to event_loop_timers_app_run()
furi_event_loop_stop(app->event_loop);
}
}
В статье используется функция void MJDDecode(unsigned long MJD, uint16_t & year, uint8_t & month, uint8_t & day).
Она работала не всегда правильно, поэтому была переписана. Информация о времени выводилась только ради любопытства через последовательный порт и на дисплей Flipper Zero в приложении данные из группы 4A не выводятся:
void MJDDecode(unsigned long MJD, uint16_t * year, uint8_t * month, uint8_t * day){
#if 0
unsigned long L = 2400000 + MJD + 68570;
unsigned long N = (L * 4) / 146097;
L = L - (146097.0 * N + 3) / 4;
(*year) = 4000 * (L + 1) / 1461001;
L = L - 1461 * (*year) / 4 + 31;
(*month) = 80.0 * L / 2447.0;
(*day) = L - 2447 * (*month) / 80;
L = (*month) / 11;
(*month) = (*month) + 2 - 12 * L;
(*year) = 100 * (N - 49) + year + L;
#endif
// Добавляем смещение для перехода к юлианской дате
int jd = MJD + 2400001;
// Переменные для промежуточных вычислений
int A, B, C, D, E;
// Преобразование JD в григорианскую дату
A = jd + 32044;
B = (4 * A + 3) / 146097;
C = A - (146097 * B) / 4;
D = (4 * C + 3) / 1461;
E = C - (1461 * D) / 4;
int monthDay = (5 * E + 2) / 153;
*day = E - (153 * monthDay + 2) / 5 + 1;
*month = monthDay + 3 - 12 * (monthDay / 10);
*year = 100 * B + D - 4800 + (monthDay / 10);
}
Интерфейс
На текущий момент микросхема инициализирована для работы с RDS и данные уже записаны в переменные. Остаётся лишь вывести их на дисплей. Эта функция использовалась во многих туториалах по работе с Flipper Zero, ничего нового, кроме самой нижней строки:
static void si4735_app_draw_callback(Canvas* canvas, void* ctx) {
// UNUSED(ctx);
furi_assert(ctx);
si4735App* app = ctx;
static int text_offset = 0;
canvas_clear(canvas);
// canvas_draw_icon(canvas, 0, 29, &I_amperka_ru_logo_128x35px);
canvas_draw_icon(canvas, 0, 0, &I_main_interface);
canvas_draw_icon(canvas, 102, 0, &I_RDS);
// canvas_set_font(canvas, FontPrimary);
// canvas_draw_str(canvas, 4, 8, "RUN");
// canvas_set_font(canvas, FontSecondary);
// elements_multiline_text_aligned(canvas, 127, 15, AlignRight, AlignTop, "Some long long long long \n aligned multiline text");
// uint16_t freq_khz;
// app->freq_khz = 9920;
char string[30];
snprintf(string, 10, "%d", app->freq_khz * app->multiplier_freq); // app->freq_khz // app->multiplier_freq
// FURI_LOG_I(TAG, string);
canvas_set_font(canvas, FontBigNumbers);
canvas_draw_str(canvas, 35, 50, string); // 35 54
// elements_multiline_text_aligned(canvas, 45, 38, AlignRight, AlignTop, string);
canvas_set_font(canvas, FontSecondary); // FontSecondary
canvas_draw_str(canvas, 110, 49, "kHz");
canvas_set_font(canvas, FontSecondary);
// snprintf(string, 30, "SNR:%2ddB SI: %2duVdB", app->snr, app->rssi);
snprintf(string, 30, "SNR:%2ddB", app->snr);
canvas_draw_str(canvas, 73, 31, string);
snprintf(string, 30, "RSSI:%2duVdB", app->rssi);
canvas_draw_str(canvas, 71, 21, string);
// snprintf(string, 30, "status x%x %dKHz ", app->status, app->coef * app->n);
snprintf(string, 30, "x%x", app->status);
canvas_draw_str(canvas, 2, 42, string); // 4, 36
if(app->coef * app->n * 10 == 100){
snprintf(string, 30, "%dKHz ", app->coef * app->n * 10);
}else if(app->coef * app->n * 10 == 1000){
snprintf(string, 30, "%dMHz ", app->coef * app->n * 10 / 1000);
}else if(app->coef * app->n * 10 == 10000){
snprintf(string, 30, "%dMHz ", app->coef * app->n * 10 / 1000);
}
canvas_draw_str(canvas, 2, 50, string); // 4, 36
canvas_draw_str(canvas, 2, 9, app->PTy_buffer);
canvas_set_font(canvas, FontPrimary); // FontSecondary
canvas_draw_str(canvas, 2, 19, app->PSName);
canvas_set_font(canvas, FontSecondary); // FontSecondary
snprintf(string, 30, "VOL:%d", app->vol);
canvas_draw_str(canvas, 2, 31, string); // 4, 36
// strncpy(string, app->rds_buffer2A, 25);
// canvas_draw_str(canvas, 2, 58, string); // 4, 36
// strncpy(string, app->rds_buffer2A[33], 32);
// canvas_draw_str(canvas, 2, 60, string); // 4, 36
// Рассчитываем ширину текста в пикселях (примерно, 6px на символ)
int text_width = strlen(app->rds_buffer2A) * 6;
// Рисуем бегущую строку
draw_scrolling_text(canvas, app->rds_buffer2A, text_width, 60, &text_offset);
}
Данных в RDS группе 2A на 65 символов. Столько не помещается на дисплее за один раз. Вообще подходов к размещению информации на дисплее Flipper Zero очень много, а здесь рассмотрен один из самых простых вариантов. Информация из RDS группы 2A выводится в самом низу бегущей строкой. Это реализовано в функции void draw_scrolling_text(Canvas* canvas, const char* text, int text_width, int y_pos, int* offset)
:
void draw_scrolling_text(Canvas* canvas, const char* text, int text_width, int y_pos, int* offset) {
// Первая часть текста (уходящая за экран)
canvas_draw_str(canvas, -(*offset), y_pos, text);
// Вторая часть (если текст короче, чем смещение)
canvas_draw_str(canvas, text_width - (*offset), y_pos, text);
// Увеличиваем смещение и сбрасываем, если текст полностью прошел
(*offset) += SCROLL_STEP;
if (*offset >= text_width) {
*offset = 0;
}
}
Громкость регулируется кнопками вверх и вниз, длительное нажатие влево переключит режим SSB, FM, AM. Кнопки влево-вправо меняют частоту с шагом 100 КГц, 1 МГц, 10МГц (в FM режиме), который меняется длительным нажатием вправо. Центральная кнопка управляет функцией Mute. С помощью кнопки назад можно выйти из приложения, но микросхема продолжит работать и из динамика будет играть музыка, Flipper при этом можно использовать для других целей. А длительное нажатие кнопки назад полностью выйдет из приложения.
if (furi_message_queue_get(app->event_queue, &event, 100) == FuriStatusOk) {
if (event.type == InputTypePress) { // EventTypeInput // InputTypeLong // InputTypeRelease
if (event.key == InputKeyBack){ // .input
// si4734_powerdown();
// break;
}else if(event.key == InputKeyUp){ // .input
// si4734_volume(7);//громче
app->vol++;
if(app->vol>0x3f){
app->vol=0x3f;
}
}else if(event.key == InputKeyDown){ // .input
// si4734_volume(-7);//тише
app->vol--;
if(app->vol<1){
app->vol=1;
}
}else if(event.key == InputKeyOk){ // .input
app->mute_value = !app->mute_value;
furi_hal_gpio_write(app->mute_pin, app->mute_value);
}else if(event.key == InputKeyRight){ // .input
}else if(event.key == InputKeyLeft){ // .input
}
}else if(event.type == InputTypeLong){
SwitchingModes mode = app->switching_mode;
CoefModes coef = app->coef_mode;
if (event.key == InputKeyBack){ // .input
si4734_powerdown();
furi_hal_gpio_write(app->SHND_pin, false);
// si4735_app_free(app);
break;
}else if (event.key == InputKeyLeft){
app->switching_mode = (mode - 1 + TOTAL_SWITCHING_MODES) % TOTAL_SWITCHING_MODES;
reciver_set_mode(app, app->switching_mode);
}else if (event.key == InputKeyRight){
app->coef_mode = (coef - 1 + TOTAL_COEF_MODES) % TOTAL_COEF_MODES;
}
}else if(event.type == InputTypeRelease){
if (event.key == InputKeyBack){ // .input
// si4734_powerdown();
// si4735_app_free(app);
break;
}
else if(event.key == InputKeyRight){
app->freq_khz+=(app->coef * app->n); // увеличивает частоту на 10 КГц // перенесу в событие при отжатии
}else if(event.key == InputKeyLeft){
app->freq_khz-=(app->coef * app->n); // перенесу в событие при отжатии
}
}
#if 0
else if(event.type == EventTypeTick) {
// Отправляем нотификацию мигания синим светодиодом
// notification_message(app->notifications, &sequence_blink_blue_100);
// FURI_LOG_I(TAG, "timer action");
show_RDS_hum_2(app);
}
#endif
}
Итоги

Очень рекомендуется прочитать оригинальную статью из журнала Хакер, потому что она очень интересная, понятная и вдохновляет на работу с микросхемой si4735. Ну а просто подробности реализации можно посмотреть на GitHub. На фото выше результат прочтения этой статьи. В микросхеме множество функций, которые можно менять и тем самым исследовать радиоэфир.
Приложение получилось сыроватым и там есть ещё что исправлять и дорабатывать. Начиная от интерфейса, который можно сделать многостраничным с отдельным меню для параметров и заканчивая управлением. Ведь в описанном приложении есть и баги, и просто неудобные моменты, например, при изменении шага частота перескакивает на значение нового шага и приходится после изменения шага нажимать на кнопку влево. Исходный код этого приложения тоже хранится на GitHub.
В идеале необходимо уделить внимание режимам SSB и AM. Там и интерфейс хромает, и управление. К тому же для тестов надо выходить из дома подальше от помех. Потому что вчера для видео в самом начале статьи получилось принять хотя бы сигнал на частоте 4996 КГц, а сегодня в видео демонстрации интерфейса и управления этот самый сигнал сначала принимался, но очень слабо, а потом и вовсе перестал быть слышен. Сигнал приводного маяка Шереметьево принять из квартиры так и не получилось ни вчера, ни сегодня. Хотя прошлым летом он принимался очень уверенно.
На дисплей выводятся RDS группы 0A, 0B, 2A и 4A. Но ведь групп намного больше. Усилитель D-класса тоже хочется поставить самый лучший с хорошим динамиком. В общем, здесь ещё работы непочатый край, так что давайте писать об этом на Хабр.
Спасибо.
С.Н.
R820T2
А с телефона управление нельзя сделать?
Ну и еще бы soft mute победить полностью
Pisikak Автор
Flipper Zero из коробки управляется с телефона. Mute работает если нажать на центральную кнопку Flipper Zero
R820T2
Не, я имел ввиду управление микросхемой SI, напрямую, без флиппера
Pisikak Автор
si473x + ESP32-S3, например, этой связкой тоже можно управлять с телефона. Только приложение для телефона написать надо.