- // ===================================================================================
- // Project: tinyFMradio - FM Tuner with RDS based on ATtiny45/85 and RDA5807
- // Version: v1.0
- // Year: 2019 - 2021
- // Author: Stefan Wagner
- // Github: https://github.com/wagiminator
- // EasyEDA: https://easyeda.com/wagiminator
- // License: http://creativecommons.org/licenses/by-sa/3.0/
- // ===================================================================================
- //
- // Description:
- // ------------
- // This is just a demo sketch that implements basic functionality. By pressing
- // the rotary encoder button the RDA5807 seeks the next radio station. Turning
- // the rotary encoder increases/decreases the volume. Selected frequency and
- // volume are stored in the EEPROM. Station name, frequency, signal strength,
- // volume and battery state of charge are shown on an OLED display.
- //
- // References:
- // -----------
- // RDA5807 datasheet:
- // https://datasheet.lcsc.com/szlcsc/1806121226_RDA-Microelectronics-RDA5807MP_C167245.pdf
- //
- // SSD1306 OLED datasheet:
- // https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf
- //
- // The OLED font was adapted from Neven Boyanov and Stephen Denne:
- // https://github.com/datacute/Tiny4kOLED
- //
- // The RDA8507 implementation was adapted from Maarten Janssen:
- // https://hackaday.io/project/9009-arduino-radio-with-rds
- //
- // Wiring:
- // -------
- // +-\/-+
- // --- RST ADC0 PB5 1|° |8 Vcc
- // Encoder A ------- ADC3 PB3 2| |7 PB2 ADC1 -------- OLED/RDA SCK
- // Encoder B ------- ADC2 PB4 3| |6 PB1 AIN1 OC0B --- Encoder SW
- // GND 4| |5 PB0 AIN0 OC0A --- OLED/RDA SDA
- // +----+
- //
- // Compilation Settings:
- // ---------------------
- // Core: ATtinyCore (https://github.com/SpenceKonde/ATTinyCore)
- // Board: ATtiny25/45/85 (No bootloader)
- // Chip: ATtiny 45 or ATtiny85 (depending on your chip)
- // Clock: 1 MHz (internal)
- // B.O.D: disabled
- //
- // Leave the rest on default settings. Don't forget to "Burn bootloader"!
- // No Arduino core functions or libraries are used. Use the makefile if
- // you want to compile without Arduino IDE.
- //
- // Fuse settings: -U lfuse:w:0x62:m -U hfuse:w:0xd7:m -U efuse:w:0xff:m
- // ===================================================================================
- // Libraries, Definitions and Macros
- // ===================================================================================
- // Libraries
- #include <avr/io.h> // for GPIO
- #include <avr/eeprom.h> // for storing user settings into EEPROM
- #include <avr/pgmspace.h> // for reading data from program memory
- #include <avr/interrupt.h> // for interrupt functions
- #include <util/delay.h> // for delays
- // Pin assignments
- #define PIN_SDA PB0 // I2C Serial Data, connect to OLED/RDA
- #define PIN_SCL PB2 // I2C Serial Clock, connect to OLED/RDA
- #define PIN_ENC_SW PB1 // pin connected to rotary encoder switch
- #define PIN_ENC_A PB3 // pin connected to rotary encoder A
- #define PIN_ENC_B PB4 // pin connected to rotary encoder B
- // EEPROM identifier
- #define EEPROM_IDENT 0x6CE7 // to identify if EEPROM was written by this program
- // Text strings
- const char HEADER[] PROGMEM = "Tiny FM Radio v1.0";
- // Variables
- uint16_t channel; // 0 .. 1023
- uint8_t volume = 1; // 0 .. 15
- // Pin manipulation macros
- #define pinInput(x) DDRB &= ~(1<<(x)) // set pin to INPUT
- #define pinOutput(x) DDRB |= (1<<(x)) // set pin to OUTPUT
- #define pinLow(x) PORTB &= ~(1<<(x)) // set pin to LOW
- #define pinHigh(x) PORTB |= (1<<(x)) // set pin to HIGH
- #define pinPullup(x) PORTB |= (1<<(x)) // enable PULLUP resistor
- #define pinIntEn(x) PCMSK |= (1<<(x)) // enable pin change interrupt
- #define pinIntDis(x) PCMSK &= ~(1<<(x)) // disable pin change interrupt
- #define pinRead(x) (PINB & (1<<(x))) // READ pin
- // ===================================================================================
- // I2C Master Implementation
- // ===================================================================================
- // I2C macros
- #define I2C_SDA_HIGH() pinInput(PIN_SDA) // release SDA -> pulled HIGH by resistor
- #define I2C_SDA_LOW() pinOutput(PIN_SDA) // SDA as output -> pulled LOW by MCU
- #define I2C_SCL_HIGH() pinInput(PIN_SCL) // release SCL -> pulled HIGH by resistor
- #define I2C_SCL_LOW() pinOutput(PIN_SCL) // SCL as output -> pulled LOW by MCU
- #define I2C_SDA_READ() pinRead(PIN_SDA) // read SDA line
- #define I2C_CLOCKOUT() I2C_SCL_HIGH();I2C_SCL_LOW() // clock out
- // I2C transmit one data byte to the slave, ignore ACK bit, no clock stretching allowed
- void I2C_write(uint8_t data) {
- for(uint8_t i=8; i; i--, data<<=1) { // transmit 8 bits, MSB first
- (data&0x80)?I2C_SDA_HIGH():I2C_SDA_LOW(); // SDA depending on bit
- I2C_CLOCKOUT(); // clock out -> slave reads the bit
- }
- I2C_SDA_HIGH(); // release SDA for ACK bit of slave
- I2C_CLOCKOUT(); // 9th clock pulse is for the ignored ACK bit
- }
- // I2C start transmission
- void I2C_start(uint8_t addr) {
- I2C_SDA_LOW(); // start condition: SDA goes LOW first
- I2C_SCL_LOW(); // start condition: SCL goes LOW second
- I2C_write(addr); // send slave address
- }
- // I2C restart transmission
- void I2C_restart(uint8_t addr) {
- I2C_SDA_HIGH(); // prepare SDA for HIGH to LOW transition
- I2C_SCL_HIGH(); // restart condition: clock HIGH
- I2C_start(addr); // start again
- }
- // I2C stop transmission
- void I2C_stop(void) {
- I2C_SDA_LOW(); // prepare SDA for LOW to HIGH transition
- I2C_SCL_HIGH(); // stop condition: SCL goes HIGH first
- I2C_SDA_HIGH(); // stop condition: SDA goes HIGH second
- }
- // I2C receive one data byte from the slave (ack=0 for last byte, ack>0 if more bytes to follow)
- uint8_t I2C_read(uint8_t ack) {
- uint8_t data = 0; // variable for the received byte
- I2C_SDA_HIGH(); // release SDA -> will be toggled by slave
- for(uint8_t i=8; i; i--) { // receive 8 bits
- data <<= 1; // bits shifted in right (MSB first)
- I2C_SCL_HIGH(); // clock HIGH
- if(I2C_SDA_READ()) data |= 1; // read bit
- I2C_SCL_LOW(); // clock LOW -> slave prepares next bit
- }
- if(ack) I2C_SDA_LOW(); // pull SDA LOW to acknowledge (ACK)
- I2C_CLOCKOUT(); // clock out -> slave reads ACK bit
- return data; // return the received byte
- }
- // ===================================================================================
- // OLED Implementation
- // ===================================================================================
- // OLED definitions
- #define OLED_ADDR 0x78 // OLED write address
- #define OLED_CMD_MODE 0x00 // set command mode
- #define OLED_DAT_MODE 0x40 // set data mode
- #define OLED_INIT_LEN 9 // length of init command array
- // OLED init settings
- const uint8_t OLED_INIT_CMD[] PROGMEM = {
- 0xC8, 0xA1, // flip screen
- 0xA8, 0x1F, // set multiplex ratio
- 0xDA, 0x02, // set com pins hardware configuration
- 0x8D, 0x14, // set DC-DC enable
- 0xAF // display on
- };
- // Standard ASCII 5x8 font (adapted from Neven Boyanov and Stephen Denne)
- const uint8_t OLED_FONT[] PROGMEM = {
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2F, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0x00,
- 0x14, 0x7F, 0x14, 0x7F, 0x14, 0x24, 0x2A, 0x7F, 0x2A, 0x12, 0x23, 0x13, 0x08, 0x64, 0x62,
- 0x36, 0x49, 0x55, 0x22, 0x50, 0x00, 0x05, 0x03, 0x00, 0x00, 0x00, 0x1C, 0x22, 0x41, 0x00,
- 0x00, 0x41, 0x22, 0x1C, 0x00, 0x14, 0x08, 0x3E, 0x08, 0x14, 0x08, 0x08, 0x3E, 0x08, 0x08,
- 0x00, 0x00, 0xA0, 0x60, 0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x60, 0x60, 0x00, 0x00,
- 0x20, 0x10, 0x08, 0x04, 0x02, 0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00, 0x42, 0x7F, 0x40, 0x00,
- 0x42, 0x61, 0x51, 0x49, 0x46, 0x21, 0x41, 0x45, 0x4B, 0x31, 0x18, 0x14, 0x12, 0x7F, 0x10,
- 0x27, 0x45, 0x45, 0x45, 0x39, 0x3C, 0x4A, 0x49, 0x49, 0x30, 0x01, 0x71, 0x09, 0x05, 0x03,
- 0x36, 0x49, 0x49, 0x49, 0x36, 0x06, 0x49, 0x49, 0x29, 0x1E, 0x00, 0x36, 0x36, 0x00, 0x00,
- 0x00, 0x56, 0x36, 0x00, 0x00, 0x08, 0x14, 0x22, 0x41, 0x00, 0x14, 0x14, 0x14, 0x14, 0x14,
- 0x00, 0x41, 0x22, 0x14, 0x08, 0x02, 0x01, 0x51, 0x09, 0x06, 0x32, 0x49, 0x59, 0x51, 0x3E,
- 0x7C, 0x12, 0x11, 0x12, 0x7C, 0x7F, 0x49, 0x49, 0x49, 0x36, 0x3E, 0x41, 0x41, 0x41, 0x22,
- 0x7F, 0x41, 0x41, 0x22, 0x1C, 0x7F, 0x49, 0x49, 0x49, 0x41, 0x7F, 0x09, 0x09, 0x09, 0x01,
- 0x3E, 0x41, 0x49, 0x49, 0x7A, 0x7F, 0x08, 0x08, 0x08, 0x7F, 0x00, 0x41, 0x7F, 0x41, 0x00,
- 0x20, 0x40, 0x41, 0x3F, 0x01, 0x7F, 0x08, 0x14, 0x22, 0x41, 0x7F, 0x40, 0x40, 0x40, 0x40,
- 0x7F, 0x02, 0x0C, 0x02, 0x7F, 0x7F, 0x04, 0x08, 0x10, 0x7F, 0x3E, 0x41, 0x41, 0x41, 0x3E,
- 0x7F, 0x09, 0x09, 0x09, 0x06, 0x3E, 0x41, 0x51, 0x21, 0x5E, 0x7F, 0x09, 0x19, 0x29, 0x46,
- 0x46, 0x49, 0x49, 0x49, 0x31, 0x01, 0x01, 0x7F, 0x01, 0x01, 0x3F, 0x40, 0x40, 0x40, 0x3F,
- 0x1F, 0x20, 0x40, 0x20, 0x1F, 0x3F, 0x40, 0x38, 0x40, 0x3F, 0x63, 0x14, 0x08, 0x14, 0x63,
- 0x07, 0x08, 0x70, 0x08, 0x07, 0x61, 0x51, 0x49, 0x45, 0x43, 0x00, 0x7F, 0x41, 0x41, 0x00,
- 0x02, 0x04, 0x08, 0x10, 0x20, 0x00, 0x41, 0x41, 0x7F, 0x00, 0x04, 0x02, 0x01, 0x02, 0x04,
- 0x40, 0x40, 0x40, 0x40, 0x40, 0x00, 0x01, 0x02, 0x04, 0x00, 0x20, 0x54, 0x54, 0x54, 0x78,
- 0x7F, 0x48, 0x44, 0x44, 0x38, 0x38, 0x44, 0x44, 0x44, 0x20, 0x38, 0x44, 0x44, 0x48, 0x7F,
- 0x38, 0x54, 0x54, 0x54, 0x18, 0x08, 0x7E, 0x09, 0x01, 0x02, 0x18, 0xA4, 0xA4, 0xA4, 0x7C,
- 0x7F, 0x08, 0x04, 0x04, 0x78, 0x00, 0x44, 0x7D, 0x40, 0x00, 0x40, 0x80, 0x84, 0x7D, 0x00,
- 0x7F, 0x10, 0x28, 0x44, 0x00, 0x00, 0x41, 0x7F, 0x40, 0x00, 0x7C, 0x04, 0x18, 0x04, 0x78,
- 0x7C, 0x08, 0x04, 0x04, 0x78, 0x38, 0x44, 0x44, 0x44, 0x38, 0xFC, 0x24, 0x24, 0x24, 0x18,
- 0x18, 0x24, 0x24, 0x18, 0xFC, 0x7C, 0x08, 0x04, 0x04, 0x08, 0x48, 0x54, 0x54, 0x54, 0x20,
- 0x04, 0x3F, 0x44, 0x40, 0x20, 0x3C, 0x40, 0x40, 0x20, 0x7C, 0x1C, 0x20, 0x40, 0x20, 0x1C,
- 0x3C, 0x40, 0x30, 0x40, 0x3C, 0x44, 0x28, 0x10, 0x28, 0x44, 0x1C, 0xA0, 0xA0, 0xA0, 0x7C,
- 0x44, 0x64, 0x54, 0x4C, 0x44, 0x08, 0x36, 0x41, 0x41, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00,
- 0x00, 0x41, 0x41, 0x36, 0x08, 0x08, 0x04, 0x08, 0x10, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
- };
- // OLED variables
- uint8_t OLED_x, OLED_y; // current cursor position
- const uint16_t DIVIDER[] PROGMEM = {10000, 1000, 100, 10, 1}; // BCD conversion array
- // OLED init function
- void OLED_init(void) {
- I2C_start(OLED_ADDR); // start transmission to OLED
- I2C_write(OLED_CMD_MODE); // set command mode
- for (uint8_t i = 0; i < OLED_INIT_LEN; i++)
- I2C_write(pgm_read_byte(&OLED_INIT_CMD[i]));// send the command bytes
- I2C_stop(); // stop transmission
- }
- // OLED set the cursor
- void OLED_setCursor(uint8_t xpos, uint8_t ypos) {
- I2C_start(OLED_ADDR); // start transmission to OLED
- I2C_write(OLED_CMD_MODE); // set command mode
- I2C_write(xpos & 0x0F); // set low nibble of start column
- I2C_write(0x10 | (xpos >> 4)); // set high nibble of start column
- I2C_write(0xB0 | (ypos & 0x07)); // set start page
- I2C_stop(); // stop transmission
- OLED_x = xpos; OLED_y = ypos; // set the cursor variables
- }
- // OLED clear rest of the current line
- void OLED_clearLine(void) {
- I2C_start(OLED_ADDR); // start transmission to OLED
- I2C_write(OLED_DAT_MODE); // set data mode
- while(OLED_x++ < 128) I2C_write(0); // clear rest of the line
- I2C_stop(); // stop transmission
- if(++OLED_y > 3) OLED_y = 0; // calculate next line
- OLED_setCursor(0, OLED_y); // set cursor to start of next line
- }
- // OLED clear screen
- void OLED_clearScreen(void) {
- OLED_setCursor(0, 0); // set cursor to home position
- for(uint8_t i=4; i; i--) OLED_clearLine(); // clear all 4 lines
- }
- // OLED plot a single character
- void OLED_plotChar(char c) {
- uint16_t ptr = c - 32; // character pointer
- ptr += ptr << 2; // -> ptr = (ch - 32) * 5;
- I2C_write(0x00); // write space between characters
- for (uint8_t i=5 ; i; i--) I2C_write(pgm_read_byte(&OLED_FONT[ptr++]));
- OLED_x += 6; // update cursor
- if (OLED_x > 122) { // line end ?
- I2C_stop(); // stop data transmission
- OLED_setCursor(0,++OLED_y); // set next line start
- I2C_start(OLED_ADDR); // start transmission to OLED
- I2C_write(OLED_DAT_MODE); // set data mode
- }
- }
- // OLED print a string
- void OLED_printStr(uint8_t* str) {
- I2C_start(OLED_ADDR); // start transmission to OLED
- I2C_write(OLED_DAT_MODE); // set data mode
- while(*str) OLED_plotChar(*str++); // plot each character
- I2C_stop(); // stop transmission
- }
- // OLED print a string from program memory
- void OLED_print(const char* p) {
- I2C_start(OLED_ADDR); // start transmission to OLED
- I2C_write(OLED_DAT_MODE); // set data mode
- char ch = pgm_read_byte(p); // read first character from program memory
- while (ch) { // repeat until string terminator
- OLED_plotChar(ch); // print character on OLED
- ch = pgm_read_byte(++p); // read next character
- }
- I2C_stop(); // stop transmission
- }
- // OLED print a string from program memory with new line
- void OLED_println(const char* p) {
- OLED_print(p);
- OLED_clearLine();
- }
- // OLED print 8-bit value as 2-digit decimal (BCD conversion by substraction method)
- void OLED_printVal(uint8_t value) {
- if(value > 99) value = 99; // limit 2-digit value
- I2C_start(OLED_ADDR); // start transmission to OLED
- I2C_write(OLED_DAT_MODE); // set data mode
- uint8_t digitval = 0; // start with digit value 0
- while(value >= 10) { // if current divider fits into the value
- digitval++; // increase digit value
- value -= 10; // decrease value by divider
- }
- if(digitval) OLED_plotChar(digitval + '0'); // print first digit
- else OLED_plotChar(' '); // leading space if zero
- OLED_plotChar(value + '0'); // print second digit
- I2C_stop(); // stop transmission
- }
- // OLED print frequency (BCD conversion by substraction method)
- void OLED_printFrequency(uint16_t value) {
- uint8_t leadflag = 0; // flag for leading spaces
- I2C_start(OLED_ADDR); // start transmission to OLED
- I2C_write(OLED_DAT_MODE); // set data mode
- for(uint8_t digit = 0; digit < 5; digit++) { // 5 digits
- uint8_t digitval = 0; // start with digit value 0
- uint16_t divider = pgm_read_word(&DIVIDER[digit]); // current divider
- while(value >= divider) { // if current divider fits into the value
- leadflag = 1; // end of leading spaces
- digitval++; // increase digit value
- value -= divider; // decrease value by divider
- }
- if(leadflag || (digit > 1)) OLED_plotChar(digitval + '0'); // print the digit
- else OLED_plotChar(' '); // or print leading space
- if(digit == 2) OLED_plotChar('.'); // print decimal after 3rd digit
- }
- I2C_stop(); // stop transmission
- }
- // ===================================================================================
- // RDA5807 Implementation
- // ===================================================================================
- // RDA definitions
- #define RDA_ADDR_SEQ 0x20 // RDA I2C write address for sequential access
- #define RDA_ADDR_INDEX 0x22 // RDA I2C write address for indexed access
- #define RDA_VOL 1 // start volume
- // RDA register definitions
- enum{ RDA_REG_2, RDA_REG_3, RDA_REG_4, RDA_REG_5, RDA_REG_6, RDA_REG_7 };
- enum{ RDA_REG_A, RDA_REG_B, RDA_REG_C, RDA_REG_D, RDA_REG_E, RDA_REG_F };
- uint16_t RDA_read_regs[6]; // RDA registers for reading
- uint16_t RDA_write_regs[6] = { // RDA registers for writing:
- 0b1101001000001101, // RDA register 0x02 preset
- 0b0001010111000000, // RDA register 0x03 preset
- 0b0000101000000000, // RDA register 0x04 preset
- 0b1000100010000000, // RDA register 0x05 preset
- 0b0000000000000000, // RDA register 0x06 preset
- 0b0000000000000000 // RDA register 0x07 preset
- };
- // RDA state macros
- #define RDA_hasRdsData ( RDA_read_regs[RDA_REG_A] & 0x8000 )
- #define RDA_isTuning (~RDA_read_regs[RDA_REG_A] & 0x4000 )
- #define RDA_tuningError ( RDA_read_regs[RDA_REG_A] & 0x2000 )
- #define RDA_hasRdsBlockE ( RDA_read_regs[RDA_REG_A] & 0x0800 )
- #define RDA_isStereo ( RDA_read_regs[RDA_REG_A] & 0x0400 )
- #define RDA_channel ( RDA_read_regs[RDA_REG_A] & 0x03FF )
- #define RDA_isTunedToChannel ( RDA_read_regs[RDA_REG_B] & 0x0100 )
- #define RDA_rdsBlockE ( RDA_read_regs[RDA_REG_B] & 0x0010 )
- #define RDA_rdsBlockErrors ( RDA_read_regs[RDA_REG_B] & 0x000F )
- #define RDA_signalStrength ((RDA_read_regs[RDA_REG_B] & 0xFE00 ) >> 9 )
- // RDA variables
- uint8_t RDA_stationName[9]; // string for the station name
- uint8_t RDA_rdsStationName[8]; // just for internal use
- // RDA write specified register
- void RDA_writeReg(uint8_t reg) {
- I2C_start(RDA_ADDR_INDEX); // start I2C for index write to RDA
- I2C_write(0x02 + reg); // set the register to write
- I2C_write(RDA_write_regs[reg] >> 8); // send high byte
- I2C_write(RDA_write_regs[reg]); // send low byte
- I2C_stop(); // stop I2C
- }
- // RDA write all registers
- void RDA_writeAllRegs(void) {
- I2C_start(RDA_ADDR_SEQ); // start I2C for sequential write to RDA
- for(uint8_t i=0; i<6; i++) { // write to 6 registers
- I2C_write(RDA_write_regs[i] >> 8); // send high byte
- I2C_write(RDA_write_regs[i]); // send low byte
- }
- I2C_stop(); // stop I2C
- }
- // RDA read all registers
- void RDA_readAllRegs(void) {
- I2C_start(RDA_ADDR_SEQ | 1); // start I2C for sequential read from RDA
- for(uint8_t i=0; i<6; i++) // read 6 registers
- RDA_read_regs[i] = (uint16_t)(I2C_read(1) << 8) | I2C_read(5-i);
- I2C_stop(); // stop I2C
- }
- // RDA clear station
- void RDA_resetStation(void) {
- for(uint8_t i=0; i<8; i++) RDA_stationName[i] = ' ';
- }
- // RDA initialize tuner
- void RDA_init(void) {
- RDA_resetStation(); // reset station available
- RDA_stationName[8] = 0; // set string terminator
- RDA_write_regs[RDA_REG_2] |= 0x0002; // set soft reset
- RDA_write_regs[RDA_REG_5] |= RDA_VOL; // set start volume
- RDA_writeAllRegs(); // write all registers
- RDA_write_regs[RDA_REG_2] &= ~0x0002; // clear soft reset
- RDA_writeReg(RDA_REG_2); // write to register 0x02
- }
- // RDA set volume
- void RDA_setVolume(uint8_t vol) {
- RDA_write_regs[RDA_REG_5] &= ~0x000F; // clear volume bits
- RDA_write_regs[RDA_REG_5] |= vol; // set volume
- RDA_writeReg(RDA_REG_5); // write to register 0x05
- }
- // RDA tune to a specified channel
- void RDA_setChannel(uint16_t chan) {
- RDA_resetStation();
- RDA_write_regs[RDA_REG_3] &= ~0xFFC0; // clear channel
- RDA_write_regs[RDA_REG_3] |= (chan << 6) | 0x0010; // set channel and tune enable
- RDA_writeReg(RDA_REG_3); // write register
- }
- // RDA seek next channel
- void RDA_seekUp(void) {
- RDA_resetStation();
- RDA_write_regs[RDA_REG_2] |= 0x0100; // set seek enable bit
- RDA_writeReg(RDA_REG_2); // write to register 0x02
- }
- // RDA update status and handle RDS
- void RDA_updateStatus(void) {
- RDA_readAllRegs();
- // When tuned disable tuning and stop seeking
- if (!RDA_isTuning) {
- RDA_write_regs[RDA_REG_3] &= ~0x0010; // clear tune enable flag
- RDA_writeReg(RDA_REG_3);
- RDA_write_regs[RDA_REG_2] &= ~0x0100; // clear seek enable flag
- RDA_writeReg(RDA_REG_2);
- }
- // Check for RDS data
- if(RDA_hasRdsData) { // RDS ready?
- // Toggle RDS flag to request new data
- RDA_write_regs[RDA_REG_2] &= ~0x0008; // clear RDS flag
- RDA_writeReg(RDA_REG_2); // write to register 0x02
- RDA_write_regs[RDA_REG_2] |= 0x0008; // set RDS flag
- RDA_writeReg(RDA_REG_2); // write to register 0x02
- // Decode RDS message (station name)
- if(!RDA_rdsBlockE) { // REG_B..F carrying blocks A-D?
- if( (RDA_read_regs[RDA_REG_D] & 0xF800) == 0x0000) { // is it station name?
- uint8_t offset = (RDA_read_regs[RDA_REG_D] & 0x03) << 1; // get character position
- uint8_t c1 = RDA_read_regs[RDA_REG_F] >> 8; // get character 1
- uint8_t c2 = RDA_read_regs[RDA_REG_F]; // get character 2
- // Copy station name characters only if received twice in a row...
- if(RDA_rdsStationName[offset] == c1) // 1st char received twice?
- RDA_stationName[offset] = c1; // copy to station name
- else RDA_rdsStationName[offset] = c1; // save for next test
- if(RDA_rdsStationName[offset + 1] == c2) // 2nd char received twice?
- RDA_stationName[offset + 1] = c2; // copy to station name
- else RDA_rdsStationName[offset + 1] = c2; // save for next test
- }
- }
- }
- }
- // Calculate frequency in 10kHz
- uint16_t RDA_getFrequency(void) {
- return(8700 + (RDA_channel << 3) + (RDA_channel << 1));
- }
- // Waits until tuning completed
- void RDA_waitTuning(void) {
- do {
- _delay_ms(100);
- RDA_updateStatus();
- } while(RDA_isTuning);
- }
- // ===================================================================================
- // ADC Implementation for Supply Voltage Measurement
- // ===================================================================================
- // Init ADC
- void ADC_init(void) {
- ADCSRA = (1<<ADPS1) | (1<<ADPS0); // set ADC clock prescaler to 8
- ADMUX = (1<<MUX3) | (1<<MUX2); // set 1.1V Vref against Vcc
- }
- // Read Vcc voltage in dV by measuring 1.1V reference against Vcc
- uint8_t ADC_readVcc(void) {
- PRR &= ~(1<<PRADC); // power on ADC
- ADCSRA |= (1<<ADEN); // enable ADC
- _delay_ms(2); // wait for vref to settle
- ADCSRA |= (1<<ADSC); // start sampling
- while(ADCSRA & (1<<ADSC)); // wait for sampling to complete
- uint16_t vcc = ADC; // read sampling result
- ADCSRA &= ~(1<<ADEN); // disable ADC
- PRR |= (1<<PRADC); // power off ADC
- vcc = 11253 / vcc; // calculate Vcc in dV; 11253 = 1.1*1023*10
- return vcc; // divide by 8 and return result
- }
- // ===================================================================================
- // Rotary Encoder Implementation using Pin Change Interrupt
- // ===================================================================================
- // Global variables
- volatile uint8_t ENC_a0, ENC_b0, ENC_ab0;
- volatile int16_t ENC_count, ENC_countMin, ENC_countMax, ENC_countStep;
- // Init rotary encoder
- void ENC_init(void) {
- pinPullup(PIN_ENC_A); // enable pullup on encoder pins ...
- pinPullup(PIN_ENC_B);
- pinPullup(PIN_ENC_SW);
- pinIntEn(PIN_ENC_A); // enable pin change interrupt on ENC A
- ENC_a0 = !pinRead(PIN_ENC_A); // set initial values ...
- ENC_b0 = !pinRead(PIN_ENC_B);
- ENC_ab0 = (ENC_a0 == ENC_b0);
- GIMSK |= (1<<PCIE); // enable pin change interrupts
- }
- // Set parameters for rotary encoder
- void ENC_set(int16_t rmin, int16_t rmax, int16_t rstep, int16_t rvalue) {
- ENC_countMin = rmin << 1; // min value
- ENC_countMax = rmax << 1; // max value
- ENC_countStep = rstep; // count steps (negative if CCW)
- ENC_count = rvalue << 1; // actual count value
- }
- // reads current rotary encoder value
- int ENC_get(void) {
- return(ENC_count >> 1);
- }
- // Pin change interrupt service routine for rotary encoder
- ISR(PCINT0_vect) {
- uint8_t a = !pinRead(PIN_ENC_A);
- uint8_t b = !pinRead(PIN_ENC_B);
- if(a != ENC_a0) { // A changed?
- ENC_a0 = a;
- if(b != ENC_b0) { // B changed?
- ENC_b0 = b;
- ENC_count += (a == b) ? -ENC_countStep : ENC_countStep;
- if((a == b) != ENC_ab0) ENC_count += (a == b) ? -ENC_countStep : ENC_countStep;
- if(ENC_count < ENC_countMin) ENC_count = ENC_countMin;
- if(ENC_count > ENC_countMax) ENC_count = ENC_countMax;
- ENC_ab0 = (a == b);
- }
- }
- }
- // ===================================================================================
- // EEPROM Functions
- // ===================================================================================
- // updates frequency and volume stored in EEPROM
- void EEPROM_update() {
- eeprom_update_word((uint16_t*)0, EEPROM_IDENT);
- eeprom_update_word((uint16_t*)2, RDA_channel);
- eeprom_update_byte((uint8_t*)4, volume);
- }
- // reads frequency and volume stored in EEPROM
- uint8_t EEPROM_get() {
- uint16_t identifier = eeprom_read_word((const uint16_t*)0);
- if (identifier == EEPROM_IDENT) {
- channel = eeprom_read_word((const uint16_t*)2);
- volume = eeprom_read_byte((const uint8_t*)4);
- return 1;
- }
- return 0;
- }
- // ===================================================================================
- // Main Function
- // ===================================================================================
- int main(void) {
- // Setup
- ADC_init(); // setup ADC
- ENC_init(); // setup rotary encoder
- sei(); // enable global interrupts
- // Disable unused peripherals to save power
- ACSR = (1<<ACD); // disable analog comperator
- PRR = (1<<PRADC) // shut down ADC
- | (1<<PRUSI) // shut down USI
- | (1<<PRTIM0) // shut down timer0
- | (1<<PRTIM1); // shut down timer1
- // Prepare and start OLED
- OLED_init();
- OLED_clearScreen();
- OLED_println(HEADER);
- OLED_print(PSTR("Starting ..."));
- // Start the tuner
- RDA_init();
- if(EEPROM_get()) RDA_setChannel(channel);
- else RDA_seekUp();
- ENC_set(0, 15, 1, volume);
- RDA_setVolume(volume);
- RDA_waitTuning();
- // Loop
- while(1) {
- // Update information on OLED
- RDA_updateStatus();
- uint8_t vcc = ADC_readVcc();
- OLED_setCursor(0, 1);
- OLED_print(PSTR("Station: "));
- OLED_printStr(RDA_stationName);
- OLED_clearLine();
- OLED_print(PSTR("Vol: "));
- OLED_printVal(volume);
- OLED_print(PSTR(" Frq: "));
- OLED_printFrequency(RDA_getFrequency());
- OLED_print(PSTR("Sig: "));
- OLED_printVal(RDA_signalStrength);
- OLED_print(PSTR(" Bat: "));
- if(vcc < 32) OLED_println(PSTR("weak"));
- else OLED_println(PSTR("OK"));
- // Check rotary encoder switch for channel seek
- if (!pinRead(PIN_ENC_SW)) { // seek up if encoder button is pressed
- OLED_setCursor(0, 1);
- OLED_println(PSTR("Tuning ..."));
- OLED_clearLine(); OLED_clearLine();
- RDA_seekUp();
- RDA_waitTuning();
- while (!pinRead(PIN_ENC_SW));
- EEPROM_update();
- }
- // Check rotary encoder for volume change
- if (volume != ENC_get()) { // change volume if encoder was turned
- volume = ENC_get();
- RDA_setVolume(volume);
- EEPROM_update();
- }
- }
- }