Водонепроницаемая FM-радио для душа на Arduino Nano, RDA5807, Nokia LCD и PAM8403
Компоненты и расходники
| Arduino Nano R3 | ×1 |
| FM-модуль RDA5807 (RDA Microelectronics) | ×1 |
| Дисплей Nokia 5110 (3V–5V версия) | ×1 |
| Модуль реального времени Tiny RTC (DS1307 + 24C32 EEPROM) | ×1 |
| Усилитель PAM8403 (2×3 В, класс D) | ×1 |
| Понижающий преобразователь LM2596 (DC-DC) | ×1 |
| Накопительная батарея LIR2032 3,6 В | ×1 |
| Резисторы: 100 Ом, 1 кОм, 10 кОм (×6), 100 кОм | — |
| МОП-транзистор IRLZ44 | ×1 |
| Динамики PEIYING PY1010C 60 Вт 4 Ом | ×1 |
| Блок питания 12 В 3 А (из подручных материалов) | ×1 |
| Конденсаторы 2200 мкФ 25 В | ×2 |
| Конденсатор 100 мкФ | ×1 |
| LED-лампы 3 Вт (замена галогенок G4 10 Вт) | ×1 |
О проекте
Несколько лет назад я купил китайскую душевую кабину с системой управления TR028, в которой был встроен rádio. Однажды система вышла из строя, её не смогли починить. Купил дешёвую душевую радио — прослужило год. Семья привыкла к радио в душе, поэтому я решил собрать своё, изучив устройство TR028. Внутри нашёл модуль TEA5767, но затем открыл для себя RDA5807 — он умеет RDS, регулировку громкости и бас. Именно его я выбрал.
Мой список желаний
- Сенсорное управление (влагозащита) — использовал панель от мёртвого TR028.
- FM-радио с RDS (две строки).
- 6 пресетов любимых станций.
- Авто- и ручной поиск станций.
- Сохранение найденной станции в пресет.
- Регулировка громкости и баса.
- Управление светом кабины.
- Часы и календарь.
- Индикатор уровня сигнала (RSSI) и стерео-режима.
- Включение/выключение (standby).
- Дополнительная инфо: температура в кабине, RDS-текст.
Что получилось в итоге
- FM-радио с двустрочным RDS.
- 6 пресетов.
- Авто/ручной поиск.
- Сохранение станции в пресет (4 сек на выбор).
- Громкость и бас-буст.
- Управление светом.
- Часы/календарь.
- RSSI, индикатор стерео.
- Режим standby (экран, подсветка, усилитель, свет выключены).
Как это работает
- Включение: после подачи питания радио не играет (защита от скачков сети). Нажмите кнопку питания — заиграет последняя станция на громкости 03, режим «Громкость». Повторное нажатие — полное выключение (standby).
- Поиск станции: кнопка «Mod» переключает Auto/Manual. Кнопки «<»/«>» ищут вниз/вверх по частоте. Кнопка «Mem» — вход в режим записи (4 сек на выбор пресета 1–6).
- Дата: кнопка «I» показывает дату 4 сек (использует
delay(), можно оптимизировать). - Коррекция часов: удерживайте «D» ≥2 сек в момент точного часа (по точному источнику). При отпускании время установится на ч:00:00. Если часы отставали 1–15 мин — часы +1, мин/сек = 00. Если спешили 1–15 мин — только мин/сек = 00.
Что бы я изменил
- Резонатор RTC для точности (но есть коррекция).
- Nokia 5110 на большее/яркое цветное TFT (1,8″ или 2,0″) — мелко читать.
- PAM8403 на PAM8610 (2×15 Вт) или TDA7297.
Заключение
Работает стабильно уже месяц, кроме точности часов. Код можно оптимизировать — я не профи, учусь год, это первый публичный проект. Вопросы — в комментариях или ЛС.
Обновление 1: добавлен предохранитель 2 А на линию 12 В. В код добавлена строка menu = 1; при включении (возврат в режим громкости).
Обновление 2: Nokia 5110 деградирует. Ищу замену — планирую 1,8″ TFT, но он жрёт ресурсы. Советы приветствуются.
Код (основной скетч)
// Arduino based shower FM Radio Project
// Arduino NANO, RDA5807M, RTC, EEPROM, LCD5110, Thermistor
#include <LCD5110_Graph.h>
#include <AT24CX.h>
#include <Wire.h>
#include <RDSParser.h>
#include <radio.h>
#include <RTClib.h>
#include <RDA5807M.h>
#include <Keypad.h>
#define MAXmenu 4
#define ledPin 13
#define blPin 7
char keys[7][2] = {
{'L', 'P'}, // Light, Power
{'I', 'D'}, // Info, Date
{'1', '2'}, // Presets 1-2
{'3', '4'}, // Presets 3-4
{'5', '6'}, // Presets 5-6
{'M', 'm'}, // Mode, Mem
{'<', '>'} // Down, Up
};
byte rowPins[7] = {11, 12, 10, 17, 9, 16, 8};
byte colPins[2] = {15, 14};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, 7, 2);
boolean bass = 0, dspl = 0, memdisplay = 0, mempress = 0, adj = 0;
boolean ledPin_state, power_state;
int menu, volume, volumeOld = 5, frequency, frequencyOld;
int txtl = 0, temparray = 0;
int samples[5];
unsigned int status[6];
unsigned long timeprevious = 0, timeprev = 0;
AT24CX mem;
RTC_DS1307 rtc;
LCD5110 lcd(6, 5, 4, 2, 3); // clk, din, dc, ce, rst
RDA5807M radio;
RDSParser rds;
extern unsigned char SmallFont[];
extern uint8_t signal5[], signal4[], signal3[], signal2[], signal1[];
void setup() {
analogReference(EXTERNAL);
Serial.begin(9600);
Wire.begin();
radio.init();
radio.debugEnable();
lcd.InitLCD();
lcd.clrScr();
lcd.setFont(SmallFont);
lcd.enableSleep();
power_state = 0;
keypad.addEventListener(keypadEvent);
keypad.setHoldTime(1500);
pinMode(ledPin, OUTPUT);
pinMode(blPin, OUTPUT);
digitalWrite(ledPin, LOW);
digitalWrite(blPin, LOW);
ledPin_state = digitalRead(ledPin);
frequency = mem.readInt(201);
volume = 2;
menu = 1;
if (volume < 0) volume = 0;
if (volume > 15) volume = 15;
if (frequency < 0) frequency = 0;
if (frequency > 210) frequency = 210;
WriteReg(0x02, 0xC00D); // soft reset, enable, RDS
canal(frequency);
radio.attachReceiveRDS(RDS_process);
rds.attachServicenNameCallback(DisplayServiceName);
rds.attachTextCallback(DisplayText);
}
void loop() {
if (frequency != frequencyOld) { frequencyOld = frequency; mem.writeInt(201, frequency); canal(frequency); }
if (volume != volumeOld) { volumeOld = volume; WriteReg(5, 0x84D0 | volume); }
char key = keypad.getKey();
radio.checkRDS();
// Температура каждые 0,6 с, усреднение по 5 замеров
unsigned long timenow = millis();
if ((unsigned long)(timenow - timeprevious) > 600) {
timeprevious = timenow;
samples[temparray] = analogRead(A7);
temparray++;
}
if (temparray == 5) {
float average = 0;
for (int i = 0; i < 5; i++) average += samples[i];
printTemp(average);
temparray = 0;
}
// Таймаут 4 с для отображения MEM
unsigned long dabar = millis();
if (mempress == 1) { timeprev = dabar; memdisplay = 1; mempress = 0; }
if (memdisplay == 1) {
if ((unsigned long)(dabar - timeprev) < 4000) memdisplay = 1;
else memdisplay = 0;
}
// Serial-коррекция RTC (hXX, mXX, sXX)
if (Serial.available() > 0) {
char t = Serial.read();
DateTime now = rtc.now();
switch (t) {
case 'h': {
unsigned int hours = Serial.parseInt();
rtc.adjust(DateTime(now.year(), now.month(), now.day(), hours, now.minute(), now.second()));
Serial.println(F("Hours")); Serial.println(hours); break;
}
case 'm': {
unsigned int mins = Serial.parseInt();
rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), mins, now.second()));
Serial.println(F("Minutes")); Serial.println(mins); break;
}
case 's': {
unsigned int sec = Serial.parseInt();
rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), sec));
Serial.println(F("Seconds")); Serial.println(sec); break;
}
}
}
printSignalStrength();
printLines();
printTime();
printFreq();
printStereo();
printMode();
printMenu();
printDate();
lcd.update();
}
// --- Вспомогательные функции вывода на LCD ---
void printSignalStrength() {
unsigned int sig; Readstatus(); sig = status[1] / 1000;
if (sig <= 12) lcd.drawBitmap(1, 1, signal1, 17, 6);
else if (sig <= 24) lcd.drawBitmap(1, 1, signal2, 17, 6);
else if (sig <= 36) lcd.drawBitmap(1, 1, signal3, 17, 6);
else if (sig <= 48) lcd.drawBitmap(1, 1, signal4, 17, 6);
else lcd.drawBitmap(1, 1, signal5, 17, 6);
}
void printLines() { lcd.drawLine(0, 9, 84, 9); lcd.drawLine(0, 39, 84, 39); }
void printTemp(float average) {
average /= 5;
average = 1023 / average - 1;
average = 51700 / average;
float steinhart = average / 50000;
steinhart = log(steinhart);
steinhart /= 3950;
steinhart += 1.0 / (25 + 273.15);
steinhart = 1.0 / steinhart;
steinhart -= 273.15;
int tmp = round(steinhart);
lcd.printNumI(tmp, 60, 1, 2);
lcd.print(F("°C"), 72, 1);
}
void DisplayServiceName(char *name) { lcd.print(name, 18, 22); }
void DisplayText(char *text) { lcd.print(text, txtl, 30); txtl -= 66; if (txtl == -396) txtl = 0; }
void printTime() {
DateTime now = rtc.now();
lcd.printNumI(now.hour(), 24, 1, 2, '0');
lcd.print(":", 36, 1);
lcd.printNumI(now.minute(), 42, 1, 2, '0');
}
void printDate() {
if (dspl == 1) {
ClearRDS();
DateTime now = rtc.now();
lcd.printNumI(now.year(), 12, 22, 4);
lcd.print(".", 36, 22);
lcd.printNumI(now.month(), 42, 22, 2, '0');
lcd.print(".", 54, 22);
lcd.printNumI(now.day(), 60, 22, 2, '0');
int dw = now.dayOfTheWeek();
const __FlashStringHelper* days[] = {F("Воскресенье"), F("Понедельник"), F("Вторник"), F("Среда"), F("Четверг"), F("Пятница"), F("Суббота")};
lcd.print(days[dw], CENTER, 30);
lcd.update(); delay(4000); ClearRDS(); dspl = 0;
}
}
void printMode() { lcd.print(F("MODE "), 0, 41); }
void printMenu() {
if (menu == 1) { lcd.print(F("VOLUME "), 30, 41); lcd.printNumI(volume + 1, 72, 41, 2, '0'); }
else if (menu == 2) lcd.print(F("AUTO-TUNE"), 30, 41);
else if (menu == 3) lcd.print(F("MAN.-TUNE"), 30, 41);
else if (menu == 4) { lcd.print(F(" BASS "), 30, 41); lcd.print(bass ? F(" ON") : F("OFF"), 66, 41); }
}
void printFreq() {
int fr = 870 + frequency;
lcd.printNumI(fr / 10, 30, 12, 3);
lcd.print(F("."), 48, 12);
lcd.printNumI(fr % 10, 54, 12, 1);
lcd.print(F("MHz"), 66, 12);
}
void printStereo() {
if (memdisplay) lcd.print(F("MEM>"), 0, 12);
else if ((status[0] & 0x0400) == 0) lcd.print(F("( )"), 0, 12);
else lcd.print(F("(ST)"), 0, 12);
}
// --- Работа с RDA5807 ---
void search(byte direc) {
WriteReg(0x02, direc ? 0xC10D : 0xC30D);
for (byte i = 0; i < 10; i++) {
delay(200); Readstatus();
if (status[0] & 0x4000) { frequency = status[0] & 0x03FF; break; }
}
}
void canal(int canal) {
byte numberH = canal >> 2;
byte numberL = ((canal & 3) << 6) | 0x10;
Wire.beginTransmission(0x11);
Wire.write(0x03); Wire.write(numberH); Wire.write(numberL);
Wire.endTransmission();
}
int Readstatus() {
Wire.requestFrom(0x10, 12);
for (int i = 0; i < 6; i++) status[i] = 256 * Wire.read() + Wire.read();
Wire.endTransmission();
}
void WriteReg(byte reg, unsigned int valor) {
Wire.beginTransmission(0x11);
Wire.write(reg); Wire.write(valor >> 8); Wire.write(valor & 0xFF);
Wire.endTransmission();
}
void RDS_process(uint16_t b1, uint16_t b2, uint16_t b3, uint16_t b4) { rds.processData(b1, b2, b3, b4); }
void ClearRDS() { lcd.print(" ", 0, 22); lcd.print(" ", 0, 30); }
// --- Обработка клавиатуры ---
void keypadEvent(KeypadEvent key, KeyState kpadState) {
switch (kpadState) {
case HOLD:
if (key == 'D' && power_state) adj = 1;
break;
case RELEASED:
if (key == 'D' && adj) {
DateTime now = rtc.now();
if (now.minute() >= 45) rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour() + 1, 0, 0));
else if (now.minute() >= 1 && now.minute() <= 15) rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), 0, 0));
adj = 0;
}
break;
case PRESSED:
if (key == 'M' && power_state) { memdisplay = 0; if (++menu > MAXmenu) menu = 1; }
if (key == '>' && power_state) {
memdisplay = 0;
switch (menu) {
case 1: if (volume < 0) { WriteReg(0x02, bass ? 0xD00D : 0xC00D); volume = 0; } else volume++; if (volume > 15) volume = 15; break;
case 2: search(0); ClearRDS(); break;
case 3: if (++frequency > 210) frequency = 210; ClearRDS(); break;
case 4: if (!bass) { bass = 1; WriteReg(0x02, 0xD00D); } break;
}
}
if (key == '<' && power_state) {
memdisplay = 0;
switch (menu) {
case 1: if (--volume < 0) WriteReg(0x02, 0x800D); break;
case 2: search(1); ClearRDS(); break;
case 3: if (--frequency < 0) frequency = 0; ClearRDS(); break;
case 4: if (bass) { bass = 0; WriteReg(0x02, 0xC00D); } break;
}
}
if (key == 'L' && power_state) { digitalWrite(ledPin, !digitalRead(ledPin)); ledPin_state = digitalRead(ledPin); }
if (key == 'P') { digitalWrite(blPin, !digitalRead(blPin)); power_state = digitalRead(blPin); if (!power_state) { lcd.enableSleep(); digitalWrite(ledPin, LOW); } else lcd.disableSleep(); volume = 2; menu = 1; }
for (char k = '1'; k <= '6'; k++) if (key == k && power_state) {
int addr = 100 + (k - '0') * 10;
if (!memdisplay) { frequency = mem.readInt(addr); ClearRDS(); }
else { mem.writeInt(addr, frequency); memdisplay = 0; }
}
if (key == 'm' && power_state) mempress = 1; else mempress = 0;
if (key == 'I' && power_state) dspl = 1;
break;
}
}
Символы уровня сигнала (для LCD5110)
#if defined(__AVR__)
#include
#define imagedatatype const uint8_t
#elif defined(__PIC32MX__) || defined(__arm__)
#define PROGMEM
#define imagedatatype const unsigned char
#endif
imagedatatype signal5[] PROGMEM = { 0xC1,0xC2,0xC4,0xFF,0xC4,0xC2,0xC1,0xC0,0xE0,0xC0,0xF0,0xC0,0xF8,0xC0,0xFC,0xC0,0xFE };
imagedatatype signal4[] PROGMEM = { 0xC1,0xC2,0xC4,0xFF,0xC4,0xC2,0xC1,0xC0,0xE0,0xC0,0xF0,0xC0,0xF8,0xC0,0xFC,0xC0,0xC0 };
imagedatatype signal3[] PROGMEM = { 0xC1,0xC2,0xC4,0xFF,0xC4,0xC2,0xC1,0xC0,0xE0,0xC0,0xF0,0xC0,0xF8,0xC0,0xC0,0xC0,0xC0 };
imagedatatype signal2[] PROGMEM = { 0xC1,0xC2,0xC4,0xFF,0xC4,0xC2,0xC1,0xC0,0xE0,0xC0,0xF0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0 };
imagedatatype signal1[] PROGMEM = { 0xC1,0xC2,0xC4,0xFF,0xC4,0xC2,0xC1,0xC0,0xE0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0 };
Схема подключения

Производственный процесс
- Эволюция и инженерия радиотехнологии: от Маркони до наших дней
- Интегральные схемы (ИС): компактные мощные чипы, питающие современную электронику
- JX Wave Generator — Генератор сигналов на Arduino с DDS AD9833 и OLED-дисплеем
- MotionSense: умная система обнаружения проникновения на Arduino и ESP8266
- Сборка защищённой IoT‑головоломки (Puzzle Box) на базе Arduino MKR Bundle
- Умный регулятор душа: электромагнитный клапан для точного контроля времени и экономии воды
- Беспроводной ИК-термометр на Arduino Nano с Bluetooth и MLX90614
- Автономный танк с доставкой пива: Walabot, Arduino, Raspberry Pi и Alexa
- Продвинутая лаборатория микроконтроллеров: ESP32, Arduino, PIC и сенсорные модули
- Набор MOSMusic: MOSFET, Arduino и управление светом по музыке