/* ESP32 Frequency Modulation Broadcaster Some code is from my old game project but now is almost dead. Wei - 2026 You can do anything you want to this source code. BUT you MUST cite the original source of the code. */ #include #include #include #include #include "KT0803.h" #include #include "Audio.h" #define VERSION "1.2" KT0803L fm(&Wire); Audio audio; // ======================= Display ======================= #define SCREEN_W 128 #define SCREEN_H 64 #define OLED_MOSI 23 // SDA #define OLED_CLK 18 // SCK #define OLED_DC 27 #define OLED_CS 5 #define OLED_RST 14 // RES U8G2_SH1106_128X64_NONAME_F_4W_HW_SPI u8g2( U8G2_R0, OLED_CS, OLED_DC, OLED_RST ); // ======================= Joystick Pins ======================= #define JOY_X 34 #define JOY_Y 35 #define JOY_SW 32 #define FM_SDA 21 #define FM_SCL 22 // ======================= SD (microSD) Card ======================= #define SD_MOSI 23 // sd reader share spi pin with the display #define SD_MISO 19 #define SD_SCK 18 #define SD_CS 33 // ======================= PCM5102A DAC ======================= #define I2S_BCLK 26 #define I2S_LRCK 25 #define I2S_DOUT 17 constexpr uint32_t SAMPLE_RATE = 44100; constexpr float TONE_FREQ = 440.0f; I2SClass I2S; typedef struct CursorPos { int x; int y; } CursorPos; void printScreen(int x, int y, const char* text, bool clear_screen, bool send_buffer) { if (clear_screen) { u8g2.clearBuffer(); } u8g2.setCursor(x, y); u8g2.print(text); if (send_buffer) { u8g2.sendBuffer(); } } struct JoyStickStatus; // Change its value if your joystick's position is not same as you expected. bool swapJoyStickVertical = false; bool swapJoyStickHorizontal = false; JoyStickStatus getJoyStickerStatus(int pin_x, int pin_y, int pin_sw); struct JoyStickStatus { bool up; bool down; bool left; bool right; bool pressed; }; int readJoystickAxis(int pin) { constexpr int SAMPLE_COUNT = 4; int sum = 0; for (int i = 0; i < SAMPLE_COUNT; i++) { sum += analogRead(pin); } return sum / SAMPLE_COUNT; } JoyStickStatus getJoyStickerStatus(int pin_x, int pin_y, int pin_sw) { JoyStickStatus status = {0}; constexpr int CENTER_LOW = 1500; constexpr int CENTER_HIGH = 2600; int x = readJoystickAxis(pin_x); int y = readJoystickAxis(pin_y); bool pressed = (digitalRead(pin_sw) == LOW); status.up = x >= CENTER_LOW && x <= CENTER_HIGH && y < 500; status.down = x >= CENTER_LOW && x <= CENTER_HIGH && y > 3800; status.left = y >= CENTER_LOW && y <= CENTER_HIGH && x < 500; status.right = y >= CENTER_LOW && y <= CENTER_HIGH && x > 3800; if (swapJoyStickVertical) { // Wow, c++ have this cool thing std::swap(status.up, status.down); } if (swapJoyStickHorizontal) { std::swap(status.left, status.right); } status.pressed = pressed; return status; } void getJoyStickerDebugInfo(int start_x, int start_y, int pin_x, int pin_y, int pin_sw, bool print_in_serial) { int x = readJoystickAxis(pin_x); int y = readJoystickAxis(pin_y); bool pressed = (digitalRead(pin_sw) == LOW); u8g2.setCursor(start_x, start_y); u8g2.print("X: "); u8g2.print(x); u8g2.setCursor(start_x, start_y + 12); u8g2.print("Y: "); u8g2.print(y); u8g2.setCursor(start_x, start_y + 24); u8g2.print("SW: "); u8g2.print(pressed ? "PRESSED" : "RELEASED"); JoyStickStatus status = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); u8g2.setCursor(start_x, start_y + 36); if (status.up) { u8g2.print("Direction: UP"); } else if (status.down) { u8g2.print("Direction: DOWN"); } else if (status.left) { u8g2.print("Direction: LEFT"); } else if (status.right) { u8g2.print("Direction: RIGHT"); } if (print_in_serial) { char buffer[100]; snprintf(buffer, sizeof(buffer), "X:%d , Y:%d, SW:%s", x, y, pressed ? "PRESSED" : "RELEASED"); Serial.println(buffer); } } // Audio int dacVolume = 12; // Option & Menu int selectOptionID = 1; int currentOptionPage = 0; // 0: Home, 1: JoyStick Debug, 2: FM Broadcast, 3: PCM5102 Debug const int optionIDList[] = {1,2,3,4,5}; const char* menuNames[] = { "Debug Joystick", "FM Brocaster", "Debug PCM5102", "Volume", "Play Music" }; int buttonPressCount = 0; bool wasPressed = false; unsigned long buttonPressTime = 0; float roundToOneDecimal(float num) { return (int)(num * 10 + 0.5) / 10.0; } void hanleQuitMenuEvent() { JoyStickStatus joyStatus = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); if (joyStatus.pressed && !wasPressed) { // Start time record buttonPressTime = millis(); wasPressed = true; } if (!joyStatus.pressed && wasPressed) { // Quit menu if pressed time more than over 1.5 sec unsigned long duration = millis() - buttonPressTime; if (duration >= 1000) { currentOptionPage=0; } wasPressed = false; } } void setFM(float freq, bool mute) { fm.setUSA(); fm.setFrequency(freq); fm.setMute(mute); } void fmBroadcast(void) { float fm_freq = 98.1; float old_freq = 98.1; if (!fm.begin(fm_freq, false)) { Serial.println("FM Module KT0803 not found. Exiting..."); printScreen(0, 14, "FM Module KT0803 not", true, false); u8g2.setCursor(0, 26); u8g2.print(" found. Exiting..."); u8g2.sendBuffer(); delay(3500); currentOptionPage=0; return; } setFM(fm_freq, false); while (currentOptionPage == 2) { int adc = analogRead(JOY_X); int offset = adc - 2048; if (abs(offset) > 200) { float speed = offset / 2048.0f; fm_freq += speed * 0.1f; fm_freq = roundToOneDecimal(fm_freq); // Max and min value check if (fm_freq < 88.0) { fm_freq = 88.0; } else if (fm_freq > 108.0) { fm_freq = 108.0; } } if (old_freq != fm_freq) { // Update frequency setFM(fm_freq, false); old_freq = fm_freq; Serial.print("Frequency: "); Serial.println(fm_freq); } printScreen(0, 14, "Brocasting_", true, false); u8g2.setCursor(0, 26); u8g2.print("Frequency: "); u8g2.print(fm.getFrequency()); u8g2.setCursor(0, 38); u8g2.print("Now you can leave"); u8g2.setCursor(0, 50); u8g2.print("this page."); u8g2.sendBuffer(); delay(300); printScreen(0, 14, "Brocasting ", true, false); u8g2.setCursor(0, 26); u8g2.print("Frequency: "); u8g2.print(fm.getFrequency()); u8g2.setCursor(0, 38); u8g2.print("Now you can leave"); u8g2.setCursor(0, 50); u8g2.print("this page."); u8g2.sendBuffer(); delay(300); hanleQuitMenuEvent(); } } void joyStickDebugMenu(void) { while (currentOptionPage == 1) { printScreen(0, 14, "Debugging joystick.", true, false); getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); u8g2.sendBuffer(); delay(300); printScreen(0, 14, "Debugging joystick..", true, false); getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); u8g2.sendBuffer(); delay(300); printScreen(0, 14, "Debugging joystick...", true, false); getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); u8g2.sendBuffer(); delay(300); printScreen(0, 14, "Debugging joystick....", true, false); getJoyStickerDebugInfo(0, 26, JOY_X, JOY_Y, JOY_SW, true); u8g2.sendBuffer(); delay(300); hanleQuitMenuEvent(); } } CursorPos printOptionMenu(int lastX, int lastY, int availableHeight, int textSize) { CursorPos pos = {lastX, lastY}; int optionCount = sizeof(menuNames) / sizeof(menuNames[0]); int maxDisplayOptionCount = availableHeight / textSize; int usedHeight = 0; // Below code is for scrolling menu support int startIndex = selectOptionID - maxDisplayOptionCount / 2; // border check if (startIndex < 0) { startIndex = 0; } if (startIndex + maxDisplayOptionCount > optionCount) { startIndex = optionCount - maxDisplayOptionCount; } if (startIndex < 0) { startIndex = 0; } int endIndex = min(startIndex + maxDisplayOptionCount, optionCount); for (int i = startIndex; i < endIndex; i++) { pos.y += 12; u8g2.setCursor(pos.x, pos.y); // Content u8g2.print(i+1); u8g2.print(": "); u8g2.print(menuNames[i]); if (selectOptionID == i + 1) { u8g2.print(" [X]"); } else { u8g2.print(" [ ]"); } } return pos; } void handleOptionSelectEvent() { JoyStickStatus joyStatus = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); size_t lengthOfOptions = sizeof(optionIDList) / sizeof(optionIDList[0]); if (joyStatus.down) { if (selectOptionID+1 <= lengthOfOptions) { selectOptionID+=1; } } else if (joyStatus.up) { if (selectOptionID-1 >= 1) { selectOptionID-=1; } } if (joyStatus.pressed) { buttonPressCount+=1; } // Do option page replace if button is pressed twice. if (buttonPressCount>=2) { buttonPressCount=0; currentOptionPage=selectOptionID; // Reset selectOptionID=1; } } void printLogo(int x, int y) { // Draw background u8g2.setDrawColor(1); u8g2.drawBox(x, y, 128 - x, 1); // Draw text u8g2.setCursor(x + 2, y + 10); u8g2.print("FM Brocaster v"); u8g2.print(VERSION); } void homeMenu(void) { bool frameMoment = false; unsigned long lastDisplayUpdate = millis(); unsigned long lastTickUpdate = millis(); unsigned long lastAnimateUpdate = millis(); unsigned long perFrameTime = 33; unsigned long perTickTime = 500; unsigned long perAnimateTime = 500; while (currentOptionPage == 0) { unsigned long time = millis(); if (time - lastTickUpdate >= perTickTime) { lastTickUpdate = millis(); handleOptionSelectEvent(); } if (time - lastAnimateUpdate >= perAnimateTime) { lastAnimateUpdate = millis(); frameMoment = !frameMoment; } if (time - lastDisplayUpdate >= perFrameTime) { lastDisplayUpdate = millis(); u8g2.clearBuffer(); u8g2.setCursor(0, 14); if (frameMoment) { u8g2.print("Running_"); } else { u8g2.print("Running "); } CursorPos currentPos = printOptionMenu(0, 14, 36, 12); printLogo(currentPos.x, currentPos.y + 2); u8g2.sendBuffer(); } delay(1); } } void test_pcm5102(float sineFreq, float squareFreq, float triangleFreq) { static float sinePhase = 0.0f; static float squarePhase = 0.0f; static float trianglePhase = 0.0f; constexpr size_t FRAMES = 256; int16_t samples[FRAMES * 2]; const float sineStep = TWO_PI * sineFreq / SAMPLE_RATE; const float squareStep = TWO_PI * squareFreq / SAMPLE_RATE; const float triangleStep = TWO_PI * triangleFreq / SAMPLE_RATE; for (size_t i = 0; i < FRAMES; i++) { float sine = 0.0f; float square = 0.0f; float triangle = 0.0f; if (sineFreq > 0.0f) { sine = sinf(sinePhase) * 2500.0f; } if (squareFreq > 0.0f) { square = (squarePhase < PI ? 1.0f : -1.0f) * 1000.0f; } if (triangleFreq > 0.0f) { triangle = (trianglePhase < PI ? (2.0f * trianglePhase / PI - 1.0f) : (3.0f - 2.0f * trianglePhase / PI)) * 1000.0f; } float mixed = sine + square + triangle; mixed = constrain(mixed, -32768.0f, 32767.0f); int16_t sample = static_cast(mixed); samples[i * 2] = sample; samples[i * 2 + 1] = sample; sinePhase += sineStep; squarePhase += squareStep; trianglePhase += triangleStep; if (sinePhase >= TWO_PI) sinePhase -= TWO_PI; if (squarePhase >= TWO_PI) squarePhase -= TWO_PI; if (trianglePhase >= TWO_PI) trianglePhase -= TWO_PI; } I2S.write( reinterpret_cast(samples), sizeof(samples) ); } void pcm5102DebugMenu(void) { unsigned long lastDisplayUpdate = 0; int frameMoment = 0; float frequency = TONE_FREQ; float squareFreq = 0.0f; float triangleFreq = 0.0f; while (currentOptionPage == 3) { test_pcm5102(frequency, squareFreq, triangleFreq); u8g2.clearBuffer(); u8g2.setCursor(0, 14); int adc = readJoystickAxis(JOY_X); int offset = adc - 2048; if (abs(offset) > 200) { float speed = offset / 2048.0f; frequency += speed * 0.1f; frequency = roundToOneDecimal(frequency); } int adcY = readJoystickAxis(JOY_Y); offset = adcY - 2048; if (offset > 0) { // For triangle offset = abs(offset); float speed = offset / 2048.0f; triangleFreq += speed * 0.1f; triangleFreq = roundToOneDecimal(triangleFreq); } else { // For square offset = abs(offset); float speed = offset / 2048.0f; squareFreq += speed * 0.1f; squareFreq = roundToOneDecimal(squareFreq); } if (millis() - lastDisplayUpdate >= 300) { lastDisplayUpdate = millis(); u8g2.clearBuffer(); u8g2.setCursor(0, 14); switch (frameMoment) { case 0: u8g2.print("Debugging PCM5102."); break; case 1: u8g2.print("Debugging PCM5102.."); break; case 2: u8g2.print("Debugging PCM5102..."); break; case 3: u8g2.print("Debugging PCM5102...."); break; } u8g2.setCursor(0, 28); u8g2.print("Sine Freq: "); u8g2.print(frequency); u8g2.setCursor(0, 40); u8g2.print("Square Freq: "); u8g2.print(squareFreq); u8g2.setCursor(0, 52); u8g2.print("Triangle Freq: "); u8g2.print(triangleFreq); u8g2.sendBuffer(); frameMoment = (frameMoment + 1) % 4; hanleQuitMenuEvent(); } } } void volumeMenu() { static unsigned long lastVolumeUpdate = 0; while (currentOptionPage == 4) { if (millis() - lastVolumeUpdate >= 30) { lastVolumeUpdate = millis(); JoyStickStatus status = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); int offset=0; if (status.left) { offset = 1; } else if (status.right){ offset = -1; } dacVolume += offset; dacVolume = constrain(dacVolume, 0, 255); int barWidth = (dacVolume * 84) / 255; u8g2.firstPage(); do { u8g2.setCursor(0, 14); u8g2.print("Volume"); u8g2.drawFrame(20, 30, 88, 12); u8g2.drawBox(22, 32, barWidth, 8); } while (u8g2.nextPage()); hanleQuitMenuEvent(); delay(1); } } } constexpr int MAX_MUSIC_FILES = 20; constexpr int MAX_FILENAME_LEN = 48; char musicFiles[MAX_MUSIC_FILES][MAX_FILENAME_LEN]; int musicFileCount = 0; int selectedMusic = 0; void scanMusicFiles() { musicFileCount = 0; File dir = SD.open("/music"); if (!dir || !dir.isDirectory()) { return; } File file = dir.openNextFile(); while (file && musicFileCount < MAX_MUSIC_FILES) { if (!file.isDirectory()) { const char* name = file.name(); const char* ext = strrchr(name, '.'); if (ext && strcasecmp(ext, ".mp3") == 0) { strncpy( musicFiles[musicFileCount], name, MAX_FILENAME_LEN - 1 ); musicFiles[musicFileCount][MAX_FILENAME_LEN - 1] = '\0'; musicFileCount++; } } file.close(); file = dir.openNextFile(); } dir.close(); } void handleMusicSelect() { int y = readJoystickAxis(JOY_Y); if (y < 500) { selectedMusic--; if (selectedMusic < 0) { selectedMusic = musicFileCount - 1; } delay(200); } if (y > 3800) { selectedMusic++; if (selectedMusic >= musicFileCount) { selectedMusic = 0; } delay(200); } } void drawMusicMenu() { u8g2.firstPage(); do { u8g2.setCursor(0, 10); u8g2.print("Music"); if (musicFileCount == 0) { u8g2.setCursor(0, 30); u8g2.print("No MP3 files"); continue; } for (int row = -1; row <= 1; row++) { int index = selectedMusic + row; if (index < 0 || index >= musicFileCount) { continue; } int y = 32 + row * 12; if (index == selectedMusic) { u8g2.setCursor(0, y); u8g2.print(">"); } String name = musicFiles[index]; if (name.startsWith("/music/")) { name.remove(0, 7); } if (name.length() > 17) { name = name.substring(0, 17); } u8g2.setCursor(10, y); u8g2.print(name); } } while (u8g2.nextPage()); } void musicMenu() { scanMusicFiles(); while (currentOptionPage == 5) { handleMusicSelect(); drawMusicMenu(); if (digitalRead(JOY_SW) == LOW) { if (musicFileCount > 0) { Serial.print("Selected: "); Serial.println(musicFiles[selectedMusic]); audio.connecttoFS(SD, musicFiles[selectedMusic]); } delay(200); } hanleQuitMenuEvent(); delay(1); } } void setup() { // debug Serial.begin(115200); // Initialize display u8g2.begin(); u8g2.setContrast(255); // fm module Wire.begin(); Wire.setClock(100000); // ram check if (psramFound()) { Serial.println("PSRAM found!"); Serial.printf("PSRAM size: %u bytes\n", ESP.getPsramSize()); Serial.printf("Free PSRAM: %u bytes\n", ESP.getFreePsram()); } else { Serial.println("No PSRAM found."); } // scan i2c devices byte error, address; int nDevices; for(address = 1; address < 127; address++ ) { Wire.beginTransmission(address); error = Wire.endTransmission(); if (error == 0) { Serial.print("I2C device found at address 0x"); if (address<16) Serial.print("0"); Serial.println(address,HEX); nDevices++; } } if (nDevices == 0) Serial.println("No I2C devices found\n"); // Set font and en-able unicode support u8g2.enableUTF8Print(); u8g2.setFont(u8g2_font_6x12_tf); u8g2.clearBuffer(); // Joystick pinMode(JOY_SW, INPUT_PULLUP); analogReadResolution(12); // Set center position of the joystick Serial.println("Correcting joystick... (Don't touch joystick when processing...)"); int rawX = analogRead(JOY_X); int rawY = analogRead(JOY_Y); int cx = 2048; int cy = 2048; // pcm5102 I2S.setPins( I2S_BCLK, I2S_LRCK, I2S_DOUT, -1, -1 ); audio.setPinout(I2S_BCLK, I2S_LRCK, I2S_DOUT); audio.setVolume(dacVolume); if (!I2S.begin( I2S_MODE_STD, SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO )) { Serial.println("Unable to init PCM5102!\n"); } else { Serial.println("Found PCM5102\n"); } // Init SPI bus for sd card reader SPI.begin(SD_SCK, SD_MISO, SD_MOSI); // sd card if (!SD.begin(SD_CS, SPI)) { Serial.println("Unable to init SD card module. Did the SD module is installed?\n"); return; } else { Serial.println("Found SD card module."); uint8_t cardType = SD.cardType(); if (cardType == CARD_NONE) { Serial.println("No SD card detected."); } else { Serial.printf("SD Card Size: %llu MB\n", SD.cardSize() / (1024ULL * 1024ULL)); scanMusicFiles(); for (int i = 0; i < musicFileCount; i++) { Serial.println(musicFiles[i]); } } } Serial.println("Initialization Finished"); } void loop(void) { if (currentOptionPage == 0) { homeMenu(); } else if (currentOptionPage == 1) { joyStickDebugMenu(); } else if (currentOptionPage == 2) { fmBroadcast(); } else if (currentOptionPage == 3) { pcm5102DebugMenu(); } else if (currentOptionPage == 4) { volumeMenu(); } else if (currentOptionPage == 5) { musicMenu(); } }