commit 3c1e42ac5300958eac45267312cb04f0dd4f5730 Author: wei Date: Fri Aug 28 23:16:37 2026 +0800 Update files. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03f4a3c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.pio diff --git a/include/KT0803/CHANGELOG.md b/include/KT0803/CHANGELOG.md new file mode 100644 index 0000000..edf8306 --- /dev/null +++ b/include/KT0803/CHANGELOG.md @@ -0,0 +1,47 @@ +# Change Log KT0803 + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + + +## [0.3.1] - 2025-11-26 +- update GitHub actions +- add derived classes KT0803L and KT0803M (wrappers for now) +- update readme.md +- minor edits + +## [0.3.0] - 2024-03-27 +- fix #5, getChannel() +- fix KT0803K setChannel() +- fix readData() +- add several KT0803K specific functions (experimental) + - bool setMono() + - bool setStereo() + - bool isStereo() + - bool setBass(uint8_t bass) + - uint8_t getBass() + - bool powerOK() + - bool silenceDetected() +- update GitHub actions +- update readme.md +- update keywords.txt +- minor edits + +---- + +## [0.2.0] - 2024-03-09 +- Fix #2 frequency to channel formula +- add derived **class KT0803K** +- add examples +- add parameter checks functions. +- add default frequency + mute to **begin()** +- update documentation +- update GitHub actions +- minor edits + +---- + +## [0.1.0] - 2023-12-27 +- initial version diff --git a/include/KT0803/KT0803.cpp b/include/KT0803/KT0803.cpp new file mode 100644 index 0000000..6c88c96 --- /dev/null +++ b/include/KT0803/KT0803.cpp @@ -0,0 +1,350 @@ +// +// FILE: KT0803.cpp +// AUTHOR: Rob Tillaart +// VERSION: 0.3.1 +// PURPOSE: Arduino Library for KT0803 and KT0803K FM transmitter. +// URL: https://github.com/RobTillaart/KT0803 + + +#include "src/KT0803.h" + + +// REGISTERS on page 7 datasheet + + +KT0803::KT0803(TwoWire * wire) +{ + _address = 0x3E; + _wire = wire; +} + + +bool KT0803::begin(float freq, bool mute) +{ + if (! isConnected()) return false; + if (! setMute(mute)) return false; + return setFrequency(freq); +} + + +bool KT0803::isConnected() +{ + _wire->beginTransmission(_address); + return (_wire->endTransmission() == 0); +} + + +/////////////////////////////////////////////////////////// +// +// FREQUENCY +// +bool KT0803::setFrequency(float MHz) +{ + if ((MHz < 70) || (MHz > 108)) return false; + // steps 50 KHz although KT0803 will truncate to 100 KHz. + return setChannel(round(MHz * 20)); +} + + +// MHz +float KT0803::getFrequency() +{ + return getChannel() * 0.05; +} + + +// steps of 50 KHz. +bool KT0803::setChannel(uint16_t channel) +{ + if ((channel < 1400) || (channel > 2160)) return false; + // need to split over 3 registers + // register 2 part skipped (always 0) for KT0803 + uint16_t ch = channel >> 1; + + uint8_t register0 = ch & 0xFF; // CHSEL[8:1] + if (writeData(0x00, register0) == false) return false; + + ch >>= 8; + uint8_t register1 = readData(0x01); + register1 &= 0xF8; // CHSEL[11:9] + register1 |= (ch & 0x07); // CHSEL[11:9] + return writeData(0x01, register1); +} + + +uint16_t KT0803::getChannel() +{ + uint16_t channel = readData(0x01) & 0x07; + channel <<= 8; + channel |= readData(0x00); + channel <<= 1; + return channel; +} + + +/////////////////////////////////////////////////////////// +// +// GAIN +// +bool KT0803::setPGA(uint8_t pga) +{ + if (pga > 7) return false; + uint8_t data = readData(0x01); + data &= 0xC7; // keep other bits + data |= (pga << 3); + return writeData(0x01, data); +} + + +uint8_t KT0803::getPGA() +{ + return (readData(0x01) >> 3) & 0x07; +} + + +bool KT0803::setRFGain(uint8_t rfgain) +{ + if (rfgain > 15) return false; + // bits 0 and 1 + uint8_t data = readData(0x01) & 0x3F; + data |= (rfgain & 0x03) << 6; + writeData(0x01, data); + // bit 2 + data = readData(0x13) & 0x7F; + data |= (rfgain & 0x04) << 5; + writeData(0x13, data); + // bit 3 + data = readData(0x02) & 0xBF; + data |= (rfgain & 0x08) << 3; + writeData(0x02, data); + return true; +} + + +uint8_t KT0803::getRFgain() +{ + uint8_t data = readData(0x01) >> 6; // bit 0, 1 + data |= ((readData(0x13) & 0x80) >> 5); // bit 2 + data |= ((readData(0x02) & 0x40) >> 3); // bit 3 + return data; +} + + +bool KT0803::setPHTCNST(bool on) +{ + uint8_t data = readData(0x02); + // is the bit already OK + if ((on == true) && (data & 0x01) == 0x01) return true; + if ((on == false) && (data & 0x01) == 0x00) return true; + // otherwise flip the bit + data = data ^ 0x01; + return writeData(0x02, data); +} + + +/////////////////////////////////////////////////////////// +// +// MISC +// +bool KT0803::getPHTCNST() +{ + return (readData(0x02) & 0x01) > 0; +} + + +bool KT0803::setPilotToneAdjust(uint8_t mode) +{ + if (mode > 1) return false; + uint8_t data = readData(0x02); + // is the bit already OK + if ((mode == 1) && (data & 0x04) == 0x04) return true; + if ((mode == 0) && (data & 0x04) == 0x00) return true; + // otherwise flip the bit + data = data ^ 0x04; + return writeData(0x02, data); +} + + +uint8_t KT0803::getPilotToneAdjust() +{ + return (readData(0x02) & 0x04) > 0; +} + + +/////////////////////////////////////////////////////////// +// +// MUTE +// +bool KT0803::setMute(bool mute) +{ + uint8_t data = readData(0x02); + // is the bit already OK + if ((mute == true) && (data & 0x08) == 0x08) return true; + if ((mute == false) && (data & 0x08) == 0x00) return true; + // otherwise flip the bit + data = data ^ 0x08; + return writeData(0x02, data); +} + + +bool KT0803::getMute() +{ + return (readData(0x02) & 0x08) > 0; +} + + +/////////////////////////////////////////////////////////////////////// +// +// PROTECTED +// +bool KT0803::writeData(uint8_t reg, uint8_t data) +{ + _wire->beginTransmission(_address); + _wire->write(reg); + _wire->write(data); + return (_wire->endTransmission() == 0); +} + + +int KT0803::readData(uint8_t reg) +{ + _wire->beginTransmission(_address); + _wire->write(reg); + _wire->endTransmission(false); // explicit no STOP fig 3 page 4 + + if (_wire->requestFrom(_address, (uint8_t) 1) == 1) + { + uint8_t data = _wire->read(); + return data; + } + return -1; +} + + +///////////////////////////////////////////////////////////////////////////// +// +// DERIVED CLASS KT0803K +// +KT0803K::KT0803K(TwoWire * wire) : KT0803(wire) +{ +} + + +bool KT0803K::setChannel(uint16_t channel) +{ + if ((channel < 1400) || (channel > 2160)) return false; + // need to split over 3 registers + uint16_t ch = channel; + + uint8_t register2 = readData(0x02) & 0x7F; + register2 |= (channel & 0x01) << 7; // CHSEL[0] + if (writeData(0x02, register2) == false) return false; + ch >>= 1; + + uint8_t register0 = ch & 0xFF; // CHSEL[8:1] + if (writeData(0x00, register0) == false) return false; + + ch >>= 8; + uint8_t register1 = readData(0x01); + register1 &= 0xF8; // CHSEL[11:9] + register1 |= (ch & 0x07); // CHSEL[11:9] + return writeData(0x01, register1); +} + + +uint16_t KT0803K::getChannel() +{ + uint16_t channel = readData(0x01) & 0x07; + channel <<= 8; + channel |= readData(0x00); + channel <<= 1; + channel |= (readData(0x02) >> 7); + return channel; +} + + +/////////////////////////////////////////////////////////// +// +// KT0803K SPECIFIC +// +bool KT0803K::setMono() +{ + uint8_t register4 = readData(0x04); + if (register4 & (1 << 6)) + { + register4 &= ~(1 << 6); + return writeData(0x04, register4); + } + return true; +} + + +bool KT0803K::setStereo() +{ + uint8_t register4 = readData(0x04); + if ((register4 & (1 << 6)) == 0) + { + register4 |= (1 << 6); + return writeData(0x04, register4); + } + return true; +} + +bool KT0803K::isStereo() +{ + uint8_t register4 = readData(0x04); + return (register4 & (1 << 6)); +} + + +bool KT0803K::setBass(uint8_t bass) // 0..3 = 0, 5, 11, 17 dB +{ + if (bass > 3) return false; + uint8_t register4 = readData(0x04); + register4 &= ~0x03; // mask off bits + register4 |= bass; + return writeData(0x04, register4); +} + + +uint8_t KT0803K::getBass() +{ + uint8_t register4 = readData(0x04); + return register4 & 0x03; +} + + +bool KT0803K::powerOK() +{ + uint8_t register0F = readData(0x0F); + return (register0F & (1 << 4)) > 0; +} + + +bool KT0803K::silenceDetected() +{ + uint8_t register0F = readData(0x0F); + return (register0F & (1 << 2)) > 0; +} + + +///////////////////////////////////////////////////////////////////////////// +// +// DERIVED CLASS KT0803L +// +KT0803L::KT0803L(TwoWire * wire) : KT0803K(wire) +{ +} + + +///////////////////////////////////////////////////////////////////////////// +// +// DERIVED CLASS KT0803M +// +KT0803M::KT0803M(TwoWire * wire) : KT0803K(wire) +{ +} + + +// -- END OF FILE -- + diff --git a/include/KT0803/KT0803.h b/include/KT0803/KT0803.h new file mode 100644 index 0000000..09d155c --- /dev/null +++ b/include/KT0803/KT0803.h @@ -0,0 +1,129 @@ +#pragma once +// +// FILE: KT0803.h +// AUTHOR: Rob Tillaart +// VERSION: 0.3.1 +// PURPOSE: Arduino Library for KT0803 and KT0803K FM transmitter +// URL: https://github.com/RobTillaart/KT0803 + + +#include "Arduino.h" +#include "Wire.h" + + +#define KT0803_LIB_VERSION (F("0.3.1")) + + +class KT0803 +{ +public: + KT0803(TwoWire * wire = &Wire); + + bool begin(float freq = 90.0, bool mute = true); + bool isConnected(); + + + // FM FREQUENCY (70-108) + // KT0803 truncates to 0.1 MHz + // KT0803K (L&M) truncates to 0.05 MHz + bool setFrequency(float MHz); + float getFrequency(); + + + // FM CHANNEL (1400-2160) + // KT0803 only supports even channels + // KT0803K (L&M) supports all channels + bool setChannel(uint16_t channel); + uint16_t getChannel(); + + + // GAIN + bool setPGA(uint8_t pga); // 0-3 = 0..-12dB 4-7 = 0..12dB + // 111: 12dB + // 110: 8dB + // 101: 4dB + // 100: 0dB + // 000: 0dB + // 001: -4dB + // 010: -8dB + // 011: -12dB + uint8_t getPGA(); + + bool setRFGain(uint8_t rfgain); // 0-15 + uint8_t getRFgain(); + + + // REGION SELECTION + // first four are wrappers + void setEurope() { setPHTCNST(true); }; + void setAustralia() { setPHTCNST(true); }; + void setUSA() { setPHTCNST(false); }; + void setJapan() { setPHTCNST(false); }; + bool setPHTCNST(bool on); + bool getPHTCNST(); + + + // PILOT TONE ADJUST (PLTADJ) + bool setPilotToneAdjust(uint8_t mode); // HIGH = 1 LOW = 0 + uint8_t getPilotToneAdjust(); + + + // MUTE software + bool setMute(bool mute); // true == muted + bool getMute(); // isMuted(). + + + // direct access to registers - debug / develop + // to access not implemented functions. + bool writeData(uint8_t reg, uint8_t data); + int readData(uint8_t reg); + +protected: + + uint8_t _address = 0x3E; // fixed address for KT0803. + TwoWire * _wire = NULL; +}; + + +///////////////////////////////////////////////////////////////////////////// +// +// DERIVED CLASSES +// +class KT0803K : public KT0803 +{ +public: + KT0803K(TwoWire * wire = &Wire); + + // CHANNEL + bool setChannel(uint16_t channel); + uint16_t getChannel(); + + // KT0803K SPECIFIC + bool setMono(); + bool setStereo(); + bool isStereo(); + + bool setBass(uint8_t bass); // 0..3 = 0, 5, 11, 17 dB + uint8_t getBass(); + + bool powerOK(); + bool silenceDetected(); +}; + + +class KT0803M : public KT0803K +{ +public: + KT0803M(TwoWire * wire = &Wire); +}; + + +class KT0803L : public KT0803K +{ +public: + KT0803L(TwoWire * wire = &Wire); +}; + + +// -- END OF FILE -- + diff --git a/include/KT0803/LICENSE b/include/KT0803/LICENSE new file mode 100644 index 0000000..0117adb --- /dev/null +++ b/include/KT0803/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-2025 Rob Tillaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/include/KT0803/README.md b/include/KT0803/README.md new file mode 100644 index 0000000..c757db5 --- /dev/null +++ b/include/KT0803/README.md @@ -0,0 +1,377 @@ + +[![Arduino CI](https://github.com/RobTillaart/KT0803/workflows/Arduino%20CI/badge.svg)](https://github.com/marketplace/actions/arduino_ci) +[![Arduino-lint](https://github.com/RobTillaart/KT0803/actions/workflows/arduino-lint.yml/badge.svg)](https://github.com/RobTillaart/KT0803/actions/workflows/arduino-lint.yml) +[![JSON check](https://github.com/RobTillaart/KT0803/actions/workflows/jsoncheck.yml/badge.svg)](https://github.com/RobTillaart/KT0803/actions/workflows/jsoncheck.yml) +[![GitHub issues](https://img.shields.io/github/issues/RobTillaart/KT0803.svg)](https://github.com/RobTillaart/KT0803/issues) + +[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/RobTillaart/KT0803/blob/master/LICENSE) +[![GitHub release](https://img.shields.io/github/release/RobTillaart/KT0803.svg?maxAge=3600)](https://github.com/RobTillaart/KT0803/releases) +[![PlatformIO Registry](https://badges.registry.platformio.org/packages/robtillaart/library/KT0803.svg)](https://registry.platformio.org/libraries/robtillaart/KT0803) + + + +# KT0803 + +Arduino Library for KT0803 and KT0803K FM transmitter. + + +### Legal point of attention + +In different countries there are different laws with respect to using transmitting devices +and their range. +Please inform yourself of the local rules and laws if and how you may or may not use a +device like the KT0803 in your projects, either hobby, commercial or otherwise. + + +## Description + +**Experimental** + +This experimental library allows basic control of the KT0803 and / or the KT0803K +FM transmitter device. +It is primary written to understand the possibilities and the interface of the device. + +The library is not tested by me with hardware yet. See future below. +However it is verified to work by others. + +There are newer, more capable, follow up devices like model K, L and M. +From these only the KT0803K is supported as a derived class. +The L and the M versions of the device will work with the KT0803K class too +as their functionality is equal or exceeds the K version. +As far as investigated the L and M versions are backwards compatible. + +Version 0.3.0 implements a few KT0803K specific functions, see section below. +These will work for L and M devices too. + +For ATTinyX5 series there exists the TinyKT0803 class which is derived from this one. +It uses a different I2C implementation. See - https://github.com/RobTillaart/TinyKT0803 + + +### Hardware + +Read datasheet for details. + +**Warning** +The KT0803 is an 3.3 Volt device and cannot be connected directly to 5V MCU's. + + +``` + +----------+ + | KT0803 | + GND --| 1 16 |-- PA_OUT RF analog output + Crystal XI --| 2 15 |-- GND + Crystal XO --| 3 14 |-- SCL I2C Clock + 3.3V IOVDD --| 4 13 |-- SCA I2C Data + GND --| 5 12 |-- GND + in left INL --| 6 11 |-- GND + in right INR --| 7 10 |-- RSTB Reset + enable SW --| 8 9 |-- GND + | | + +----------+ +``` + + +### Frequency range + +The frequency range stated on the front page of the datasheet ==> 70 MHz - 108 MHz. +The frequency range stated in table 2 ==> 76 MHz - 108 MHz. +So the datasheet is at least ambiguous on this point. + +Keep in mind that the frequency range allowed differs per country. +The library does not provide this filtering, explicit responsibility of the user. + + +### Differences + +The KT0803K device has far more options, which are not all implemented. +There is one important, the resolution or step-size of the frequency. + +| device | step-size | Notes | +|:---------:|:-----------:|:--------| +| KT0803 | 100 KHz | in code all math is done with 50 KHz +| KT0803K | 50 KHz | + +Backwards compatible. +According to the datasheet code for the KT0803 should work for the KT0803K. +Code with the KT0803K class will probably not work on a KT0803. + + +### Transmit frequency + +The transmit frequency can be set with **setFrequency(MHz)** or by **setChannel(channel)**. +Note that the channel and frequency math of the KT0803 and the KT0803K is aligned +in this library. This allows exchange of channel data between device types. + +Note that the KT0803 will internally round to use 100 KHz steps. + +Some examples: + +| Frequency | Channel | Notes | +|:------------:|:---------:|:-------:| +| 70.00 MHz | 1400 | channel = freq (Mhz) \* 20 +| 70.05 MHz | 1401 | freq = channel \* 0.05 +| 70.10 MHz | 1402 | +| 76.00 MHz | 1520 | +| 80.00 MHz | 1600 | +| 89.70 MHz | 1794 | default (see registers datasheet) +| 100.00 MHz | 2000 | +| 101.30 MHz | 2026 | +| 105.70 MHz | 2114 | +| 108.00 MHz | 2160 | + + +### Related + +- https://github.com/RobTillaart/KT0803 +- https://github.com/RobTillaart/TinyKT0803 +- https://www.hackster.io/hesam-moshiri/stereo-digital-fm-transmitter-circuit-arduino-code-2dbd8d +- https://www.hackster.io/hesam-moshiri/full-digital-fm-receiver-with-arduino-and-tea5767-52be37 +- https://www.hackerstore.nl/Artikel/388 +- https://en.wikipedia.org/wiki/FM_broadcasting + + +## Interface KT0803 + +```cpp +#include "KT0803.h" +``` + +### Constructor + +- **KT0803(TwoWire \*wire = &Wire)** constructor, +optional Wire interface. +- **KT0803K(TwoWire \*wire = &Wire)** constructor, +optional Wire interface. +- **KT0803L(TwoWire \*wire = &Wire)** constructor, +optional Wire interface. +- **KT0803M(TwoWire \*wire = &Wire)** constructor, +optional Wire interface. +- **bool begin(float freq = 90.0, bool mute = true)** initializes the library. +Furthermore it checks if the deviceAddress is available on the I2C bus. +Default it sets the frequency to 90 MHz and **mutes the signal**. +Returns true if deviceAddress is found on the bus, false otherwise. +- **bool isConnected()** test to see if deviceAddress is found on the I2C-bus. + + +### Frequency + +- **bool setFrequency(float MHz)** converts the frequency in MHz to +call **setChannel(channel)**. The value of channel is rounded off depending +on the resolution of the device. +Returns false if MHz is out of range or **setChannel()** fails. +- **float getFrequency()** returns the current frequency in MHz, can be slightly different +from the set value due to rounding math mentioned above. +The return value is derived from a call to **getChannel()** +- **bool setChannel(uint16_t channel)** writes the channel to broadcast on to the device. +This involves two or three writes to different device registers. +- **uint16_t getChannel()** reads the selected channel from the device and +returns it. + + +### PGA + +Read Datasheet. + +The KT0803K has a **PGA_LSB** (2 bits) setting, which allows setting the gain +with single (1) dB steps. This is not yet implemented in the library. + +- **bool setPGA(uint8_t pga)** sets gain according to table below. +Returns false if pga is out of range (0..7). +- **uint8_t getPGA()** returns 0..7, default 0. + + +| PGA | gain | notes | +|:-----:|:-------:|:-------:| +| 111 | 12dB | +| 110 | 8dB | +| 101 | 4dB | +| 100 | 0dB | +| 000 | 0dB | default +| 001 | -4dB | +| 010 | -8dB | +| 011 | -12dB | + + +### RFGain + +Read Datasheet. + +Note: the RFGain value (4 bits) is distributed over three registers. +PA_BIAS (register 0x05) is only supported in the KT0803K device. +It is not yet supported in the library. + +- **bool setRFGain(uint8_t rfgain)** sets rfgain according to table below. +Returns false if rfgain is out of range (0..15). +- **uint8_t getRFgain()** returns 0..15, default 15. + + +| RFGAIN | RFout | PA_BIAS = 1 | notes | +|:--------:|:------------:|:------------:|:-------:| +| 0000 | 95.5 dBuV | - | +| 0001 | 96.5 dBuV | - | +| 0010 | 97.5 dBuV | - | +| 0011 | 98.2 dBuV | - | +| 0100 | 98.9 dBuV | - | +| 0101 | 100.0 dBuV | - | +| 0110 | 101.5 dBuV | - | +| 0111 | 102.8 dBuV | - | +| 1000 | 105.1 dBuV | 107.2 dBuV | +| 1001 | 105.6 dBuV | 108.0 dBuV | +| 1010 | 106.2 dBuV | 108.7 dBuV | +| 1011 | 106.5 dBuV | 109.5 dBuV | +| 1100 | 107.0 dBuV | 110.3 dBuV | +| 1101 | 107.4 dBuV | 111.0 dBuV | +| 1110 | 107.7 dBuV | 111.7 dBuV | +| 1111 | 108.0 dBuV | 112.5 dBuV | default + + +### Region selection + +Read datasheet for details. + +Note that not all frequencies are allowed in all regions / countries! + +The first four are convenience wrappers for **setPHTCNST()** +If some region is missing please let me know the details and I can add +a wrapper for it. + +- **void setEurope()** +- **void setAustralia()** +- **void setUSA()** +- **void setJapan()** +- **bool setPHTCNST(bool on)** See table below. +- **bool getPHTCNST()** returns set value. + +| PHTCNST | time | Region | +|:---------:|:-------:|:--------:| +| 0 | 75 μs | USA, Japan, (default) +| 1 | 50 μs | Europe, Australia + + +### PilotToneAdjust + +Read datasheet. + +- **bool setPilotToneAdjust(uint8_t mode)** HIGH = 1 LOW = 0 +- **uint8_t getPilotToneAdjust()** + + +### Mute + +Default the device is not muted, but **begin()** will default mute it. +See interface section above. + +- **bool setMute(bool mute)** enables or disables the transmitting +by muting the signal. +- **bool getMute()** returns the current state of muting. + + +## Preference channels + +The device and library do not implement the persistant store of user +selectable preferences (frequencies or channels). +This can be implemented by the user in EEPROM or another persistent medium. + +Think of a class that holds an array of channels and optional descriptions. +A minimal hardcoded preset sketch is in the examples. + + +## Derived classes + +Three derived classes are made: + +- KT0803K has some extended functions. +- KT0803L is backwards compatible with KT0803K. +It has far more registers in use than the KT0803/KT0803K. +For now it is a wrapper around KT0803K. +- KT0803M is identical to the KT0803K (no new registers). + + +## Interface KT0803K / KT0803L / KT0803M + +Added functions in 0.3.0 (not tested), check datasheet. + +### Mono Stereo + +- **bool setMono()** idem +- **bool setStereo()** idem +- **bool isStereo()** idem + +### Bass + +- **bool setBass(uint8_t bass); // 0..3 = 0, 5, 11, 17 dB +- **uint8_t getBass()** idem + +### Misc + +- **bool powerOK()** idem +- **bool silenceDetected()** idem + + +## Future + +#### Must + +- improve documentation +- buy hardware +- test and verify. + + +#### Should + +- add examples for KT0803K specific functions. +- investigate KT0803L +- RESET pin as optional parameter in constructor? +- SW pin (ON/OFF) as optional parameter in constructor? + - add functions for sw on/off, + - what is impact on settings? + - call begin () again? => default + - explain well doc. + +#### Could + +- improve error handling +- unit tests possible? +- extend settings upon request **bold** are interesting, see table + +| device | setting | register | Notes | +|:---------:|:-------------:|:---------------:|:--------| +| KT0803 | PA_CTRL | 13, bit 2 | **WARNING in datasheet** +| | | | Should it be added in API? +| KT0803K | PGA_LSB | 04, bit 4+5 | gain fine tuning -> see PGA_MOD +| KT0803K | FDEV | 04, bit 2+3 | Frequency deviation adjustment +| KT0803K | PDPA | 0B, bit 5 | Power Amplifier Power Down ? +| KT0803K | PA_BIAS | 0E, bit 1 | PA bias current enhancement. +| KT0803K | LMTLVL | 10, bit 3+4 | **Internal audio limiter level control** +| KT0803K | PGAMOD | 10, bit 0 | PGA mode selection (use PGA_LSB/ not) +| KT0803K | SLNCDIS | 12, bit 7 | Silence detection disable +| KT0803K | SLNCTHL | 12, bit 4+5+6 | Silence detection low threshold +| KT0803K | SLNCTHH | 12, bit 1+2+3 | Silence detection high threshold +| KT0803K | SW_MOD | 12, bit 0 | **Switching channel mode selection** +| KT0803K | SLNCTIME | 14, bit 5+6+7 | silence detection time +| KT0803K | SLNCCNTHIGH | 14, bit 2+3+4 | silence detection count high +| KT0803K | SLNCCNTLOW | 15, bit 0+1+2 | silence detection count low + + +#### Wont (for now) + +- investigate tea5767 FM receiver (Out of scope for this library). +- investigate efficiency of register access. + - caching all (allowed) registers in **begin()** + - 3 bytes for KT0803 + - 12 bytes for KT0803K + - cache frequency. + - only writing is needed. +- send binary data over FM? +- preset frequency array in .h file (hardcoded) +- enums for parameters - readability? + + +## Support + +If you appreciate my libraries, you can support the development and maintenance. +Improve the quality of the libraries by providing issues and Pull Requests, or +donate through PayPal or GitHub sponsors. + +Thank you, + diff --git a/include/KT0803/examples/KT0803K_minimal/KT0803K_minimal.ino b/include/KT0803/examples/KT0803K_minimal/KT0803K_minimal.ino new file mode 100644 index 0000000..073ae83 --- /dev/null +++ b/include/KT0803/examples/KT0803K_minimal/KT0803K_minimal.ino @@ -0,0 +1,38 @@ +// +// FILE: KT0803K_minimal.ino +// AUTHOR: Rob Tillaart +// PURPOSE: minimal demo +// URL: https://github.com/RobTillaart/KT0803 + + +#include "Arduino.h" +#include "Wire.h" +#include "KT0803.h" + + +KT0803K FM_SEND; + + +void setup() +{ + Serial.begin(115200); + while(!Serial); + Serial.println(); + Serial.println(__FILE__); + Serial.print("KT0803_LIB_VERSION: "); + Serial.println(KT0803_LIB_VERSION); + Serial.println(); + + Wire.begin(); + + FM_SEND.begin(); + FM_SEND.setChannel(2000); // * 0.05 100.00 MHz + FM_SEND.setMute(false); +} + +void loop() +{ +} + + +// -- END OF FILE -- diff --git a/include/KT0803/examples/KT0803K_presets/KT0803K_presets.ino b/include/KT0803/examples/KT0803K_presets/KT0803K_presets.ino new file mode 100644 index 0000000..95ec1e0 --- /dev/null +++ b/include/KT0803/examples/KT0803K_presets/KT0803K_presets.ino @@ -0,0 +1,55 @@ +// +// FILE: KT0803K_minimal.ino +// AUTHOR: Rob Tillaart +// PURPOSE: minimal demo hardcoded presets +// URL: https://github.com/RobTillaart/KT0803 + + +#include "Arduino.h" +#include "Wire.h" +#include "KT0803.h" + + +KT0803L FM_SEND; + +// hardcoded presets adjust to your need. +// note: presets by channel would take half the memory +float preset[10] = +{ + 88.6, 91.3, 92.2, 96.0, 97.3, + 98.8, 100.1, 100.3, 105.4, 107.6 +}; + + +void setup() +{ + Serial.begin(115200); + while(!Serial); + Serial.println(); + Serial.println(__FILE__); + Serial.print("KT0803_LIB_VERSION: "); + Serial.println(KT0803_LIB_VERSION); + Serial.println(); + + Wire.begin(); + + FM_SEND.begin(preset[0], false); +} + + +void loop() +{ + if (Serial.available()) + { + char c = Serial.read(); + + if (c == 'm') FM_SEND.setMute(false); + if (c == 'M') FM_SEND.setMute(true); + // select preset 0..9 + int p = c - '0'; // convert to digit + if ((0 <= p) && (p <= 9)) FM_SEND.setFrequency(preset[p]); + } +} + + +// -- END OF FILE -- diff --git a/include/KT0803/examples/KT0803K_setFrequency/KT0803K_setFrequency.ino b/include/KT0803/examples/KT0803K_setFrequency/KT0803K_setFrequency.ino new file mode 100644 index 0000000..27d615f --- /dev/null +++ b/include/KT0803/examples/KT0803K_setFrequency/KT0803K_setFrequency.ino @@ -0,0 +1,44 @@ +// +// FILE: KT0803_setFrequency.ino +// AUTHOR: Rob Tillaart +// PURPOSE: minimal demo +// URL: https://github.com/RobTillaart/KT0803 + + +#include "Arduino.h" +#include "Wire.h" +#include "KT0803.h" + + +KT0803K FM_SEND; + + +void setup() +{ + Serial.begin(115200); + while(!Serial); + Serial.println(); + Serial.println(__FILE__); + Serial.print("KT0803_LIB_VERSION: "); + Serial.println(KT0803_LIB_VERSION); + Serial.println(); + + Wire.begin(); + + FM_SEND.begin(); + FM_SEND.setFrequency(105.75); + FM_SEND.setMute(false); + + Serial.print("Freq: "); + Serial.println(FM_SEND.getFrequency()); + Serial.print("Channel: "); + Serial.println(FM_SEND.getChannel()); +} + + +void loop() +{ +} + + +// -- END OF FILE -- diff --git a/include/KT0803/examples/KT0803_minimal/KT0803_minimal.ino b/include/KT0803/examples/KT0803_minimal/KT0803_minimal.ino new file mode 100644 index 0000000..15d9192 --- /dev/null +++ b/include/KT0803/examples/KT0803_minimal/KT0803_minimal.ino @@ -0,0 +1,38 @@ +// +// FILE: KT0803_minimal.ino +// AUTHOR: Rob Tillaart +// PURPOSE: minimal demo +// URL: https://github.com/RobTillaart/KT0803 + + +#include "Arduino.h" +#include "Wire.h" +#include "KT0803.h" + + +KT0803 FM_SEND; + + +void setup() +{ + Serial.begin(115200); + while(!Serial); + Serial.println(); + Serial.println(__FILE__); + Serial.print("KT0803_LIB_VERSION: "); + Serial.println(KT0803_LIB_VERSION); + Serial.println(); + + Wire.begin(); + + FM_SEND.begin(); + FM_SEND.setChannel(2000); // * 0.05 = 100.00 MHz + FM_SEND.setMute(false); +} + +void loop() +{ +} + + +// -- END OF FILE -- diff --git a/include/KT0803/examples/KT0803_setFrequency/KT0803_setFrequency.ino b/include/KT0803/examples/KT0803_setFrequency/KT0803_setFrequency.ino new file mode 100644 index 0000000..4e224dd --- /dev/null +++ b/include/KT0803/examples/KT0803_setFrequency/KT0803_setFrequency.ino @@ -0,0 +1,43 @@ +// +// FILE: KT0803_setFrequency.ino +// AUTHOR: Rob Tillaart +// PURPOSE: minimal demo +// URL: https://github.com/RobTillaart/KT0803 + + +#include "Arduino.h" +#include "Wire.h" +#include "KT0803.h" + + +KT0803 FM_SEND; + + +void setup() +{ + Serial.begin(115200); + while(!Serial); + Serial.println(); + Serial.println(__FILE__); + Serial.print("KT0803_LIB_VERSION: "); + Serial.println(KT0803_LIB_VERSION); + Serial.println(); + + Wire.begin(); + + FM_SEND.begin(); + FM_SEND.setFrequency(105.75); + FM_SEND.setMute(false); + + Serial.print("Freq: "); + Serial.println(FM_SEND.getFrequency()); + Serial.print("Channel: "); + Serial.println(FM_SEND.getChannel()); +} + +void loop() +{ +} + + +// -- END OF FILE -- diff --git a/include/KT0803/examples/KT0803_sweep_2_seconds/KT0803_sweep_2_seconds.ino b/include/KT0803/examples/KT0803_sweep_2_seconds/KT0803_sweep_2_seconds.ino new file mode 100644 index 0000000..2349e6a --- /dev/null +++ b/include/KT0803/examples/KT0803_sweep_2_seconds/KT0803_sweep_2_seconds.ino @@ -0,0 +1,42 @@ +// +// FILE: KT0803_sweep_2_seconds.ino +// AUTHOR: Rob Tillaart +// PURPOSE: minimal demo +// URL: https://github.com/RobTillaart/KT0803 + + +#include "Arduino.h" +#include "Wire.h" +#include "KT0803.h" + + +KT0803 FM_SEND; + + +void setup() +{ + Serial.begin(115200); + while(!Serial); + Serial.println(); + Serial.println(__FILE__); + Serial.print("KT0803_LIB_VERSION: "); + Serial.println(KT0803_LIB_VERSION); + Serial.println(); + + Wire.begin(); + FM_SEND.begin(); + FM_SEND.setMute(false); +} + +void loop() +{ + // adjust to your local allowed frequencies + for (float freq = 88.0; freq <= 108.0; freq += 0.1) + { + FM_SEND.setFrequency(freq); + delay(2000); + } +} + + +// -- END OF FILE -- diff --git a/include/KT0803/examples/KT0803_up_down/KT0803_up_down.ino b/include/KT0803/examples/KT0803_up_down/KT0803_up_down.ino new file mode 100644 index 0000000..3480944 --- /dev/null +++ b/include/KT0803/examples/KT0803_up_down/KT0803_up_down.ino @@ -0,0 +1,75 @@ +// +// FILE: KT0803_up_down.ino +// AUTHOR: Rob Tillaart +// PURPOSE: minimal demo +// URL: https://github.com/RobTillaart/KT0803 + + +#include "Arduino.h" +#include "Wire.h" +#include "KT0803.h" + + +KT0803 FM_SEND; + +// connect two buttons to GND +// adjust pins to your needs, +uint8_t UP_PIN = 6; +uint8_t DOWN_PIN = 7; +float freq = 100.0; +float newFreq = 100.0; + + +void setup() +{ + Serial.begin(115200); + while(!Serial); + Serial.println(); + Serial.println(__FILE__); + Serial.print("KT0803_LIB_VERSION: "); + Serial.println(KT0803_LIB_VERSION); + Serial.println(); + + pinMode(UP_PIN, INPUT_PULLUP); + pinMode(DOWN_PIN, INPUT_PULLUP); + Wire.begin(); + + FM_SEND.begin(freq, false); +} + + +void loop() +{ + // adjust frequency with up and down button. + uint8_t u = digitalRead(UP_PIN); + uint8_t d = digitalRead(DOWN_PIN); + if ((u == 0) && (d == 0)) + { + newFreq = 100.0; + } + else if (u == 0) + { + newFreq += 0.1; + if (newFreq > 108.0) newFreq = 108.0; + } + else if (d == 0) + { + newFreq -= 0.1; + if (newFreq < 88.0) newFreq = 88.0; + } + if (newFreq != freq) + { + freq = newFreq; + FM_SEND.setFrequency(freq); + + Serial.print("Freq: "); + Serial.print(FM_SEND.getFrequency()); + Serial.print("\t"); + Serial.println(FM_SEND.getChannel()); + + delay(100); + } +} + + +// -- END OF FILE -- diff --git a/include/KT0803/keywords.txt b/include/KT0803/keywords.txt new file mode 100644 index 0000000..a9be2b9 --- /dev/null +++ b/include/KT0803/keywords.txt @@ -0,0 +1,55 @@ +# Syntax Colouring Map For KT0803 + + +# Data types (KEYWORD1) +KT0803 KEYWORD1 +KT0803K KEYWORD1 +KT0803L KEYWORD1 +KT0803M KEYWORD1 + + +# Methods and Functions (KEYWORD2) +begin KEYWORD2 +isConnected KEYWORD2 + +setFrequency KEYWORD2 +getFrequency KEYWORD2 +setChannel KEYWORD2 +getChannel KEYWORD2 + +setPGA KEYWORD2 +getPGA KEYWORD2 + +setRFGain KEYWORD2 +getRFgain KEYWORD2 + +setEurope KEYWORD2 +setAustralia KEYWORD2 +setUSA KEYWORD2 +setJapan KEYWORD2 +setPHTCNST KEYWORD2 +getPHTCNST KEYWORD2 + +setPilotToneAdjust KEYWORD2 +getPilotToneAdjust KEYWORD2 + +setMute KEYWORD2 +getMute KEYWORD2 + + +# KT0803K specific +setMono KEYWORD2 +setStereo KEYWORD2 +isStereo KEYWORD2 + +setBass KEYWORD2 +getBass KEYWORD2 + +powerOK KEYWORD2 +silenceDetected KEYWORD2 + + +# Constants (LITERAL1) +KT0803_LIB_VERSION LITERAL1 + + diff --git a/include/KT0803/library.json b/include/KT0803/library.json new file mode 100644 index 0000000..a90066c --- /dev/null +++ b/include/KT0803/library.json @@ -0,0 +1,23 @@ +{ + "name": "KT0803", + "keywords": "FM,Broadcast,KT0803K,KT0803L,KT0803M", + "description": "Arduino library for the KT0803 and KT0803K FM transmitter.", + "authors": + [ + { + "name": "Rob Tillaart", + "email": "Rob.Tillaart@gmail.com", + "maintainer": true + } + ], + "repository": + { + "type": "git", + "url": "https://github.com/RobTillaart/KT0803.git" + }, + "version": "0.3.1", + "license": "MIT", + "frameworks": "*", + "platforms": "*", + "headers": "KT0803.h" +} diff --git a/include/KT0803/library.properties b/include/KT0803/library.properties new file mode 100644 index 0000000..65dd456 --- /dev/null +++ b/include/KT0803/library.properties @@ -0,0 +1,11 @@ +name=KT0803 +version=0.3.1 +author=Rob Tillaart +maintainer=Rob Tillaart +sentence=Arduino library for the KT0803 and KT0803K FM transmitter. +paragraph=KT0803K, KT0803L, KT0803M +category=Communication +url=https://github.com/RobTillaart/KT0803 +architectures=* +includes=KT0803.h +depends= diff --git a/include/README b/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/include/README @@ -0,0 +1,37 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the convention is to give header files names that end with `.h'. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into the executable file. + +The source code of each library should be placed in a separate directory +("lib/your_library_name/[Code]"). + +For example, see the structure of the following example libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +Example contents of `src/main.c` using Foo and Bar: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +The PlatformIO Library Dependency Finder will find automatically dependent +libraries by scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..5e23c4f --- /dev/null +++ b/platformio.ini @@ -0,0 +1,31 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:rymcu-esp32-s3-devkitc-1] +platform = https://github.com/pioarduino/platform-espressif32/releases/download/53.03.11/platform-espressif32.zip +board = rymcu-esp32-s3-devkitc-1 +framework = arduino +board_build.flash_mode = dio +monitor_speed = 115200 +lib_deps = + olikraus/U8g2 + RobTillaart/KT0803 + https://github.com/pschatzmann/arduino-libhelix.git +build_unflags = + -std=gnu++11 + -std=gnu++14 + -std=gnu++17 + -std=c++11 + -std=c++14 + -std=c++17 +build_flags = + -std=gnu++20 + -D CORE_DEBUG_LEVEL=0 + -D BOARD_HAS_PSRAM diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..3b62d36 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,970 @@ +/* +AudioFMSynth +This project only tested and working on ESP32 (Original) and ESP32-S3. +If you would like to use this code to other board. It's necessary to ensue all PINs are able to use for this project. +Some code may need to be rewritten to support the board. + +Some code is from my old game project, but now it 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 "MP3DecoderHelix.h" + +#define VERSION "1.2" + +KT0803L fm(&Wire); + +// ======================= Display ======================= +#define SCREEN_W 128 +#define SCREEN_H 64 + +// For esp32 (original) +// #define OLED_MOSI 23 // SDA ESP32 +// #define OLED_CLK 18 // SCK +// #define OLED_DC 27 +// #define OLED_CS 5 +// #define OLED_RST 14 // RES + +// esp32-s3 +#define OLED_MOSI 11 // SDA ESP32 +#define OLED_CLK 12 // SCK +#define OLED_DC 16 +#define OLED_CS 14 +#define OLED_RST 17 // RES + +U8G2_SH1106_128X64_NONAME_F_4W_HW_SPI u8g2( + U8G2_R0, + OLED_CS, + OLED_DC, + OLED_RST +); + +// ======================= Joystick Pins ======================= +// esp32 (original) +// #define JOY_X 34 +// #define JOY_Y 35 +// #define JOY_SW 32 + +// esp32-s3 +#define JOY_X 1 +#define JOY_Y 2 +#define JOY_SW 7 + +// esp32 (original) +// #define FM_SDA 21 +// #define FM_SCL 22 + +// esp32-s3 +#define FM_SDA 8 +#define FM_SCL 9 + +// ======================= SD (microSD) Card ======================= +// esp32 (original) +// #define SD_MOSI 23 // sd reader shares the same spi pin with the display +// #define SD_MISO 19 +// #define SD_SCK 18 +// #define SD_CS 33 + +// esp32-s3 +#define SD_MOSI 11 +#define SD_MISO 13 +#define SD_SCK 12 +#define SD_CS 15 + +// ======================= PCM5102A DAC ======================= +// esp32 (original) +// #define I2S_BCLK 26 +// #define I2S_LRCK 25 +// #define I2S_DOUT 17 + +// esp32-s3 +#define I2S_BCLK 4 +#define I2S_LRCK 5 +#define I2S_DOUT 6 + +// ====================== Button(s) ========================== +#define BUTTON_1 10 + +using namespace libhelix; + +constexpr uint32_t SAMPLE_RATE = 44100; +constexpr float TONE_FREQ = 440.0f; +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; + +File playingFile; +bool musicPlaying = false; +bool musicPaused = false; +bool musicButtonWasPressed = false; +uint32_t currentAudioRate = SAMPLE_RATE; + +// 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 Broadcaster", + "Debug PCM5102", + "Volume", + "Play Music" +}; +int buttonPressCount = 0; +bool wasPressed = false; +unsigned long buttonPressTime = 0; + +I2SClass I2S; + +void mp3DataCallback(MP3FrameInfo &info, int16_t *pcm, size_t len, void*) { + if (!pcm || !len || info.nChans < 1) return; + + if ((uint32_t)info.samprate != currentAudioRate) { + if (I2S.configureTX(info.samprate, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO)) + currentAudioRate = info.samprate; + } + + constexpr size_t CHUNK_SAMPLES = 256; + int16_t out[CHUNK_SAMPLES * 2]; + + if (info.nChans == 1) { + for (size_t pos = 0; pos < len;) { + size_t n = min((size_t)CHUNK_SAMPLES, len - pos); + for (size_t i = 0; i < n; i++) { + int32_t v = (int32_t)pcm[pos + i] * dacVolume / 255; + out[i * 2] = out[i * 2 + 1] = (int16_t)v; + } + I2S.write( + reinterpret_cast(out), + n * 2 * sizeof(int16_t) + ); + pos += n; + } + } else { + for (size_t pos = 0; pos < len;) { + size_t n = min((size_t)(CHUNK_SAMPLES * 2), len - pos); + for (size_t i = 0; i < n; i++) + out[i] = (int16_t)((int32_t)pcm[pos + i] * dacVolume / 255); + I2S.write( + reinterpret_cast(out), + n * sizeof(int16_t) + ); + pos += n; + } + } +} + +MP3DecoderHelix mp3(mp3DataCallback); + +typedef struct CursorPos { + int x; + int y; +} CursorPos; + +void stopMusic() { + if (playingFile) playingFile.close(); + if (musicPlaying) mp3.end(); + musicPlaying = false; +} + +void pauseMusic() { + if (!musicPlaying) return; + musicPaused = true; +} + +void resumeMusic() { + if (!musicPlaying) return; + musicPaused = false; +} + +void feedMusicDecoder() { + if (!musicPlaying || !playingFile || musicPaused) return; + + uint8_t input[1024]; + if (const size_t n = playingFile.read(input, sizeof(input)); n > 0) { + mp3.write(input, n); + } else { + stopMusic(); + Serial.println("Playback finished"); + } +} + +/* This function stores some task that needs to be run in the background */ +void moduleLoop() { + feedMusicDecoder(); +} + +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); + } +} + +float roundToOneDecimal(const float num) { + return (num * 10 + 0.5) / 10.0; +} + +int handleQuitMenuEvent() { + const 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 the menu if pressed time more than over 1.5 sec + if (const unsigned long duration = millis() - buttonPressTime; duration >= 1000) { + currentOptionPage=0; + } + + wasPressed = false; + } + return currentOptionPage; +} + +void setFM(float freq, bool mute) { + fm.setUSA(); + fm.setFrequency(freq); + fm.setMute(mute); +} + +void fmBroadcast() { + 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) { + const int adc = analogRead(JOY_X); + + if (const int offset = adc - 2048; abs(offset) > 200) + { + const float speed = static_cast(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); + currentOptionPage = handleQuitMenuEvent(); + } +} + +void joyStickDebugMenu() { + 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); + currentOptionPage = handleQuitMenuEvent(); + } +} + +CursorPos printOptionMenu(int lastX, int lastY, int availableHeight, int textSize) { + CursorPos pos = {lastX, lastY}; + + constexpr int optionCount = std::size(menuNames); + const int maxDisplayOptionCount = availableHeight / textSize; + + // 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; +} + +int handleOptionSelectEvent() { + const JoyStickStatus joyStatus = getJoyStickerStatus(JOY_X, JOY_Y, JOY_SW); + constexpr size_t lengthOfOptions = std::size(optionIDList); + + 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; + } + + return currentOptionPage; +} + +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("AudioFMSynth v"); + u8g2.print(VERSION); +} + +void homeMenu() { + 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) { + moduleLoop(); + const unsigned long time = millis(); + if (time - lastTickUpdate >= perTickTime) { + lastTickUpdate = millis(); + currentOptionPage = 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 "); + } + + const auto + [x, y] = printOptionMenu(0, 14, 36, 12); + printLogo(x, 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 = static_cast(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); + + const auto 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() { + 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); + + const int adc = readJoystickAxis(JOY_X); + int offset = adc - 2048; + + if (abs(offset) > 200) + { + const float speed = static_cast(offset) / 2048.0f; + + frequency += speed * 0.1f; + frequency = roundToOneDecimal(frequency); + } + + const int adcY = readJoystickAxis(JOY_Y); + offset = adcY - 2048; + + if (offset > 0) { + // For triangle + offset = abs(offset); + + const float speed = static_cast(offset) / 2048.0f; + triangleFreq += speed * 0.1f; + triangleFreq = roundToOneDecimal(triangleFreq); + } else { + // For square + offset = abs(offset); + + const float speed = static_cast(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; + default: ; + } + + 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; + + currentOptionPage = handleQuitMenuEvent(); + } + } +} + +void volumeMenu() { + static unsigned long lastVolumeUpdate = 0; + while (currentOptionPage == 4) { + if (millis() - lastVolumeUpdate >= 30) { + lastVolumeUpdate = millis(); + + const 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); + + const 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()); + + currentOptionPage = handleQuitMenuEvent(); + delay(1); + } + } +} + +void scanMusicFiles() { + musicFileCount = 0; + File dir = SD.open("/music"); + if (!dir || !dir.isDirectory()) return; + while (musicFileCount < MAX_MUSIC_FILES) { + File file = dir.openNextFile(); + if (!file) break; + if (!file.isDirectory()) { + const char* name = file.name(); + if (const char* ext = strrchr(name, '.'); ext && !strcasecmp(ext, ".mp3")) { + strncpy(musicFiles[musicFileCount], name, MAX_FILENAME_LEN - 1); + musicFiles[musicFileCount++][MAX_FILENAME_LEN - 1] = '\0'; + } + } + file.close(); + } + dir.close(); +} + +void handleMusicSelect() { + int y = readJoystickAxis(JOY_Y); + if (musicFileCount == 0) return; + if (y < 500) { selectedMusic = (selectedMusic + musicFileCount - 1) % musicFileCount; delay(200); } + else if (y > 3800) { selectedMusic = (selectedMusic + 1) % musicFileCount; delay(200); } +} + +void drawMusicMenu() { + u8g2.clearBuffer(); + u8g2.setCursor(0, 10); u8g2.print("Music"); + if (!musicFileCount) { + u8g2.setCursor(0, 30); u8g2.print("No MP3 files"); + } else { + 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('>'); } + const char* name = musicFiles[index]; + if (!strncmp(name, "/music/", 7)) name += 7; + char shown[18]; + strncpy(shown, name, 17); shown[17] = '\0'; + u8g2.setCursor(10, y); u8g2.print(shown); + } + } + u8g2.sendBuffer(); +} + +bool startMusic(const int index) { + stopMusic(); + + char path[MAX_FILENAME_LEN + 8]; + const char* name = musicFiles[index]; + if (name[0] == '/') snprintf(path, sizeof(path), "%s", name); + else if (!strncmp(name, "music/", 6)) snprintf(path, sizeof(path), "/%s", name); + else snprintf(path, sizeof(path), "/music/%s", name); + + playingFile = SD.open(path, FILE_READ); + if (!playingFile) { + Serial.print("Unable to open: "); + Serial.println(path); + return false; + } + + Serial.print("Playing: "); + Serial.println(path); + mp3.begin(); + musicPlaying = true; + return true; +} + +void musicMenu() { + scanMusicFiles(); + musicButtonWasPressed = digitalRead(BUTTON_1) == LOW; + int lastMusic = 0; + + while (currentOptionPage == 5) { + if (!musicPlaying) handleMusicSelect(); + drawMusicMenu(); + + const bool pressed = digitalRead(BUTTON_1) == LOW; + if (pressed && !musicButtonWasPressed && musicFileCount > 0) { + if (!musicPlaying || selectedMusic != lastMusic) { + // Handle new music play and music switch + if (const bool ok = startMusic(selectedMusic); ok) { + lastMusic = selectedMusic; + musicPaused = false; + } + } else { + // Handle music pause + musicPaused = !musicPaused; + } + } + + + musicButtonWasPressed = pressed; + + feedMusicDecoder(); + + currentOptionPage = handleQuitMenuEvent(); + delay(1); + } + + // stopMusic(); +} + +void setup() { + // debug + Serial.begin(115200); + + // version info + Serial.printf("AudioFMSynth v%s\n", VERSION); + + // 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: %f MB\n", roundToOneDecimal(ESP.getPsramSize() / (1024 * 1024))); + Serial.printf("Free PSRAM: %f MB\n", roundToOneDecimal(ESP.getFreePsram() / (1024 * 1024))); + } else { + Serial.println("No PSRAM found. Is highly recommend to enable PSRAM if you will use MP3 Player."); + } + + // scan i2c devices + int nDevices = 1; + + for(byte address = 1; address < 127; address++ ) { + Wire.beginTransmission(address); + if (const byte error = Wire.endTransmission(); 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"); + + // Set font and enable Unicode support + u8g2.enableUTF8Print(); + u8g2.setFont(u8g2_font_6x12_tf); + + u8g2.clearBuffer(); + + // Joystick + pinMode(JOY_SW, INPUT_PULLUP); + pinMode(BUTTON_1, 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); + + if (!I2S.begin( + I2S_MODE_STD, + SAMPLE_RATE, + I2S_DATA_BIT_WIDTH_16BIT, + I2S_SLOT_MODE_STEREO + )) { + Serial.println("Unable to init PCM5102 PINs"); + } else { + Serial.println("I2S for PCM5102 is initialized."); + } + + // 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?"); + } else { + Serial.println("Found SD card module."); + + if (const uint8_t cardType = SD.cardType(); 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() { + 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(); + } +} \ No newline at end of file