Update files.

This commit is contained in:
wei committed 2026-08-28 23:16:37 +08:00
commit 3c1e42ac53
20 files changed
+2433

No files matched your search

+1
View File
@@ -0,0 +1 @@
.pio
+47
View File
@@ -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
+350
View File
@@ -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 --
+129
View File
@@ -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 --
+21
View File
@@ -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.
+377
View File
@@ -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,
@@ -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 --
@@ -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 --
@@ -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 --
@@ -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 --
@@ -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 --
@@ -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 --
@@ -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 --
+55
View File
@@ -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
+23
View File
@@ -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": "[email protected]",
"maintainer": true
}
],
"repository":
{
"type": "git",
"url": "https://github.com/RobTillaart/KT0803.git"
},
"version": "0.3.1",
"license": "MIT",
"frameworks": "*",
"platforms": "*",
"headers": "KT0803.h"
}
+11
View File
@@ -0,0 +1,11 @@
name=KT0803
version=0.3.1
author=Rob Tillaart <[email protected]>
maintainer=Rob Tillaart <[email protected]>
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=
+37
View File
@@ -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
+46
View File
@@ -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 <Foo.h>
#include <Bar.h>
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
+31
View File
@@ -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
+970
View File
@@ -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 <SPI.h>
#include <SD.h>
#include <U8g2lib.h>
#include <Wire.h>
#include "KT0803.h"
#include <ESP_I2S.h>
#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<const uint8_t*>(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<const uint8_t*>(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<float>(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<float>(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<int16_t>(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<uint8_t *>(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<float>(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<float>(offset) / 2048.0f;
triangleFreq += speed * 0.1f;
triangleFreq = roundToOneDecimal(triangleFreq);
} else {
// For square
offset = abs(offset);
const float speed = static_cast<float>(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();
}
}