Accept FS#7178 - Sansa e200 FM tuner support by Ivan Zupan. Do the needed integration work into recording and the AS3514 audio driver. Do a little AS3514 fiq_record tweak to have it all work nicely from the start.

git-svn-id: svn://svn.rockbox.org/rockbox/trunk@13573 a1c6a512-1295-4272-9138-f99709370657
diff --git a/apps/debug_menu.c b/apps/debug_menu.c
index 2279a27..32ab46c 100644
--- a/apps/debug_menu.c
+++ b/apps/debug_menu.c
@@ -1990,16 +1990,48 @@
 
     lcd_setmargins(0, 0);
 
+    fm_detected = radio_hardware_present();
+
     while(1)
     {
         int row = 0;
 
         lcd_clear_display();
-        fm_detected = radio_hardware_present();
 
         snprintf(buf, sizeof buf, "HW detected: %s", fm_detected?"yes":"no");
         lcd_puts(0, row++, buf);
+#if (CONFIG_TUNER & LV24020LP)
+        if (fm_detected)
+        {
+            snprintf(buf, sizeof buf, "CTRL_STAT: %02X",
+                     sanyo_get(RADIO_ALL) );
+            lcd_puts(0, row++, buf);
 
+            snprintf(buf, sizeof buf, "RADIO_STAT: %02X",
+                     sanyo_get(RADIO_REG_STAT));
+            lcd_puts(0, row++, buf);
+
+            snprintf(buf, sizeof buf, "MSS_FM: %d kHz",
+                     (sanyo_get(RADIO_MSS_FM) ) );
+            lcd_puts(0, row++, buf);
+
+            snprintf(buf, sizeof buf, "MSS_IF: %d Hz",
+                     (sanyo_get(RADIO_MSS_IF) ) );
+            lcd_puts(0, row++, buf);
+
+            snprintf(buf, sizeof buf, "MSS_SD: %d Hz",
+                     (sanyo_get(RADIO_MSS_SD) ) );
+            lcd_puts(0, row++, buf);
+
+            snprintf(buf, sizeof buf, "if_set: %d Hz",
+                     (sanyo_get(RADIO_IF_SET) ) );
+            lcd_puts(0, row++, buf);
+
+            snprintf(buf, sizeof buf, "sd_set: %d Hz",
+                     (sanyo_get(RADIO_SD_SET) ) );
+            lcd_puts(0, row++, buf);
+        }
+#endif
 #if (CONFIG_TUNER & S1A0903X01)
         snprintf(buf, sizeof buf, "Samsung regs: %08X",
                  samsung_get(RADIO_ALL));
diff --git a/apps/keymaps/keymap-e200.c b/apps/keymaps/keymap-e200.c
index 55e30ca..318f495 100644
--- a/apps/keymaps/keymap-e200.c
+++ b/apps/keymaps/keymap-e200.c
@@ -219,6 +219,17 @@
     LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_STD)
 }; /* button_context_recscreen */
 
+/** FM Radio Screen **/
+static const struct button_mapping button_context_radio[]  = {
+    { ACTION_FM_MENU,        BUTTON_DOWN,                       BUTTON_NONE },
+    { ACTION_FM_PRESET,      BUTTON_SELECT,                     BUTTON_NONE },
+    { ACTION_FM_STOP,        BUTTON_UP|BUTTON_REPEAT,           BUTTON_UP  },
+    { ACTION_FM_MODE,        BUTTON_REC,                        BUTTON_NONE },
+    { ACTION_FM_EXIT,        BUTTON_POWER,                      BUTTON_NONE },
+    { ACTION_FM_PLAY,        BUTTON_UP|BUTTON_REL,              BUTTON_UP },
+    LAST_ITEM_IN_LIST__NEXTLIST(CONTEXT_SETTINGS)
+}; /* button_context_radio */
+
 static const struct button_mapping button_context_keyboard[]  = {
     { ACTION_KBD_LEFT,         BUTTON_LEFT,                      BUTTON_NONE },
     { ACTION_KBD_LEFT,         BUTTON_LEFT|BUTTON_REPEAT,        BUTTON_NONE },
@@ -283,7 +294,9 @@
             return button_context_settings_time;
 
         case CONTEXT_YESNOSCREEN:
-            return button_context_yesno;            
+            return button_context_yesno;
+        case CONTEXT_FM:
+            return button_context_radio;
         case CONTEXT_BOOKMARKSCREEN:
             return button_context_bmark;
         case CONTEXT_QUICKSCREEN:
diff --git a/apps/recorder/radio.c b/apps/recorder/radio.c
index a0e6c81..9f3228b 100644
--- a/apps/recorder/radio.c
+++ b/apps/recorder/radio.c
@@ -90,6 +90,13 @@
 #elif CONFIG_KEYPAD == ONDIO_PAD
 #define FM_RECORD_DBLPRE
 #define FM_RECORD
+#elif (CONFIG_KEYPAD == SANSA_E200_PAD)
+#define FM_MENU     
+#define FM_PRESET    
+#define FM_STOP
+#define FM_MODE
+#define FM_EXIT
+#define FM_PLAY
 #endif
 
 #define RADIO_SCAN_MODE 0
@@ -97,10 +104,14 @@
 
 static const struct fm_region_setting fm_region[] = {
     /* Note: Desriptive strings are just for display atm and are not compiled. */
-    FM_REGION_ENTRY("Europe",    87500000, 108000000,  50000, 0, 0),
-    FM_REGION_ENTRY("US/Canada", 87900000, 107900000, 200000, 1, 0),
-    FM_REGION_ENTRY("Japan",     76000000,  90000000, 100000, 0, 1),
-    FM_REGION_ENTRY("Korea",     87500000, 108000000, 100000, 0, 0),
+    [REGION_EUROPE] =
+        FM_REGION_ENTRY("Europe",    87500000, 108000000,  50000, 0, 0),
+    [REGION_US_CANADA] =
+        FM_REGION_ENTRY("US/Canada", 87900000, 107900000, 200000, 1, 0),
+    [REGION_JAPAN] =
+        FM_REGION_ENTRY("Japan",     76000000,  90000000, 100000, 0, 1),
+    [REGION_KOREA] =
+        FM_REGION_ENTRY("Korea",     87500000, 108000000, 100000, 0, 0),
     };
 
 static int curr_preset = -1;
@@ -158,13 +169,18 @@
     return in_screen;   
 } 
 
+/* TODO: Move some more of the control functionality to an HAL and clean up the
+   mess */
+
 /* secret flag for starting paused - prevents unmute */
 #define FMRADIO_START_PAUSED 0x8000
 void radio_start(void)
 {
     const struct fm_region_setting *fmr;
     bool start_paused;
+#if CONFIG_TUNER != LV24020LP
     int mute_timeout;
+#endif
 
     if(radio_status == FMRADIO_PLAYING)
         return;
@@ -182,8 +198,14 @@
         * fmr->freq_step + fmr->freq_min;
 
     radio_set(RADIO_SLEEP, 0); /* wake up the tuner */
+#if (CONFIG_TUNER & LV24020LP)
+    radio_set(RADIO_REGION, global_settings.fm_region);
+    radio_set(RADIO_FORCE_MONO, global_settings.fm_force_mono);
+#endif
     radio_set(RADIO_FREQUENCY, curr_freq);
 
+#if CONFIG_TUNER != LV24020LP
+
     if(radio_status == FMRADIO_OFF)
     {
 #if (CONFIG_TUNER & S1A0903X01)
@@ -209,6 +231,7 @@
              break;
         yield();
     }
+#endif /* CONFIG_TUNER != LV24020LP */
 
     /* keep radio from sounding initially */
     if(!start_paused)
@@ -1311,6 +1334,9 @@
 
 void set_radio_region(int region)
 {
+#if (CONFIG_TUNER & LV24020LP)
+    radio_set(RADIO_REGION, global_settings.fm_region);
+#endif
 #if (CONFIG_TUNER & TEA5767)
     radio_set(RADIO_SET_DEEMPHASIS, 
         fm_region[region].deemphasis);
diff --git a/docs/CREDITS b/docs/CREDITS
index d5ad63b..5ac1fab 100644
--- a/docs/CREDITS
+++ b/docs/CREDITS
@@ -297,6 +297,7 @@
 Dagni McPhee
 Alex Gerchanovsky
 Gerhard Dirschl
+Ivan Zupan
 The libmad team
 The wavpack team
 The ffmpeg team
diff --git a/firmware/SOURCES b/firmware/SOURCES
index 70bd12e..2b948e7 100644
--- a/firmware/SOURCES
+++ b/firmware/SOURCES
@@ -156,6 +156,9 @@
 drivers/fmradio_i2c.c
 tuner_philips.c
 #endif /* (CONFIG_TUNER & TEA5767) */
+#if (CONFIG_TUNER & LV24020LP)
+tuner_sanyo.c
+#endif /* (CONFIG_TUNER & LV24020LP) */
 #endif /*SIMULATOR */
 #endif /* CONFIG_TUNER */
 
diff --git a/firmware/drivers/audio/as3514.c b/firmware/drivers/audio/as3514.c
index 982bbe1..12a72fe 100644
--- a/firmware/drivers/audio/as3514.c
+++ b/firmware/drivers/audio/as3514.c
@@ -402,7 +402,6 @@
 
     if (enable) {
         source = SOURCE_LINE_IN1_ANALOG;
-        audiohw_set_master_vol(as3514.vol_l, as3514.vol_r);
 
         /* LI1R_Mute_off */
         line_in1_r |= (1 << 5);
@@ -415,4 +414,7 @@
     as3514_write(AUDIOSET1, audioset1);
     as3514_write(LINE_IN1_R, line_in1_r);
     as3514_write(LINE_IN1_L, line_in1_l);
+
+    /* Sync mixer volume */
+    audiohw_set_master_vol(as3514.vol_l, as3514.vol_r);
 }
diff --git a/firmware/export/config-e200.h b/firmware/export/config-e200.h
index 001c89b..71b1270 100644
--- a/firmware/export/config-e200.h
+++ b/firmware/export/config-e200.h
@@ -22,7 +22,7 @@
 
 /* Define bitmask of input sources - recordable bitmask can be defined
    explicitly if different */
-#define INPUT_SRC_CAPS (SRC_CAP_MIC)
+#define INPUT_SRC_CAPS (SRC_CAP_MIC | SRC_CAP_FMRADIO)
 
 /* define this if you have a bitmap LCD display */
 #define HAVE_LCD_BITMAP
@@ -88,8 +88,8 @@
 #define AB_REPEAT_ENABLE 1
 
 /* FM Tuner */
-/*#define CONFIG_TUNER TEA5767
-#define CONFIG_TUNER_XTAL  32768 *//* TODO: what is this? */
+#define CONFIG_TUNER LV24020LP
+#define HAVE_TUNER_PWR_CTRL
 
 /* Define this for LCD backlight available */
 #define HAVE_BACKLIGHT
diff --git a/firmware/export/config.h b/firmware/export/config.h
index dd4eaf4..4652359 100644
--- a/firmware/export/config.h
+++ b/firmware/export/config.h
@@ -29,6 +29,7 @@
 /* CONFIG_TUNER (note these are combineable bit-flags) */
 #define S1A0903X01 0x01 /* Samsung */
 #define TEA5767    0x02 /* Philips */
+#define LV24020LP  0x04 /* Sanyo */
 
 /* CONFIG_CODEC */
 #define MAS3587F 3587
diff --git a/firmware/export/tuner.h b/firmware/export/tuner.h
index 2e286b4..9f6d29f 100644
--- a/firmware/export/tuner.h
+++ b/firmware/export/tuner.h
@@ -17,28 +17,42 @@
  * KIND, either express or implied.
  *
  ****************************************************************************/
-#ifndef __TUNER_SAMSUNG_H__
-#define __TUNER_SAMSUNG_H__
+#ifndef __TUNER_H__
+#define __TUNER_H__
 
 #include "hwcompat.h"
 
 /* settings to the tuner layer */
-#define RADIO_ALL -1 /* debug */
-#define RADIO_SLEEP 0
-#define RADIO_FREQUENCY 1
-#define RADIO_MUTE 2
-#define RADIO_IF_MEASUREMENT 3
-#define RADIO_SENSITIVITY 4
-#define RADIO_FORCE_MONO 5
-#define RADIO_SCAN_FREQUENCY 6
+#define RADIO_ALL               -1 /* debug */
+#define RADIO_SLEEP             0
+#define RADIO_FREQUENCY         1
+#define RADIO_MUTE              2
+#define RADIO_IF_MEASUREMENT    3
+#define RADIO_SENSITIVITY       4
+#define RADIO_FORCE_MONO        5
+#define RADIO_SCAN_FREQUENCY    6
 #if (CONFIG_TUNER & TEA5767)
-#define RADIO_SET_DEEMPHASIS 7
-#define RADIO_SET_BAND 8
+#define RADIO_SET_DEEMPHASIS    7
+#define RADIO_SET_BAND          8
+#endif
+#if (CONFIG_TUNER & LV24020LP)
+#define RADIO_REGION            9 /* to be used for all tuners */
+#define RADIO_REG_STAT          100
+#define RADIO_MSS_FM            101
+#define RADIO_MSS_IF            102
+#define RADIO_MSS_SD            103
+#define RADIO_IF_SET            104
+#define RADIO_SD_SET            105
 #endif
 /* readback from the tuner layer */
-#define RADIO_PRESENT 0
-#define RADIO_TUNED 1
-#define RADIO_STEREO 2
+#define RADIO_PRESENT           0
+#define RADIO_TUNED             1
+#define RADIO_STEREO            2
+
+#define REGION_EUROPE           0
+#define REGION_US_CANADA        1
+#define REGION_JAPAN            2
+#define REGION_KOREA            3
 
 #if CONFIG_TUNER
 
@@ -49,6 +63,9 @@
 #if CONFIG_TUNER == S1A0903X01 /* FM recorder */
 #define radio_set samsung_set
 #define radio_get samsung_get
+#elif CONFIG_TUNER == LV24020LP  /* Sansa */
+#define radio_set sanyo_set
+#define radio_get sanyo_get
 #elif CONFIG_TUNER == TEA5767  /* iRiver, iAudio */
 #define radio_set philips_set
 #define radio_get philips_get
@@ -57,14 +74,19 @@
 #define radio_get _radio_get
 int (*_radio_set)(int setting, int value);
 int (*_radio_get)(int setting);
-#endif
-#endif
+#endif /* CONFIG_TUNER == */
+#endif /* SIMULATOR */
 
 #if (CONFIG_TUNER & S1A0903X01)
 int samsung_set(int setting, int value);
 int samsung_get(int setting);
 #endif /* CONFIG_TUNER & S1A0903X01 */
 
+#if (CONFIG_TUNER & LV24020LP)
+int sanyo_set(int setting, int value);
+int sanyo_get(int setting);
+#endif /* CONFIG_TUNER & LV24020LP */
+
 #if (CONFIG_TUNER & TEA5767)
 struct philips_dbg_info
 {
@@ -98,4 +120,4 @@
 
 #endif /* #if CONFIG_TUNER */
 
-#endif
+#endif /* __TUNER_H__ */
diff --git a/firmware/target/arm/pcm-pp.c b/firmware/target/arm/pcm-pp.c
index 9027ff1..0608c20 100644
--- a/firmware/target/arm/pcm-pp.c
+++ b/firmware/target/arm/pcm-pp.c
@@ -377,7 +377,7 @@
     if (audio_channels == 2) {
         /* RX is stereo */
         while (p_size > 0) {
-            if (FIFO_FREE_COUNT < 2) {
+            if (FIFO_FREE_COUNT < 8) {
                 /* enable interrupt */
                 IISCONFIG |= (1 << 0);
                 goto fiq_record_exit;
@@ -401,7 +401,7 @@
     else {
         /* RX is left channel mono */
         while (p_size > 0) {
-            if (FIFO_FREE_COUNT < 2) {
+            if (FIFO_FREE_COUNT < 8) {
                 /* enable interrupt */
                 IISCONFIG |= (1 << 0);
                 goto fiq_record_exit;
diff --git a/firmware/target/arm/sandisk/sansa-e200/audio-e200.c b/firmware/target/arm/sandisk/sansa-e200/audio-e200.c
index a3f3284..f046f0d 100644
--- a/firmware/target/arm/sandisk/sansa-e200/audio-e200.c
+++ b/firmware/target/arm/sandisk/sansa-e200/audio-e200.c
@@ -42,11 +42,8 @@
 void audio_set_source(int source, unsigned flags)
 {
     static int last_source = AUDIO_SRC_PLAYBACK;
-#if 0
     static bool last_recording = false;
     bool recording = flags & SRCF_RECORDING;
-#endif
-    (void)flags;
 
     switch (source)
     {
@@ -70,13 +67,9 @@
             }
             break;
 
-#if 0
         case AUDIO_SRC_FMRADIO:         /* recording and playback */
             audio_channels = 2;
 
-            if (!recording)
-                audiohw_set_recvol(23, 23, AUDIO_GAIN_LINEIN);
-
             if (source == last_source && recording == last_recording)
                 break;
 
@@ -92,9 +85,7 @@
                 audiohw_disable_recording();
                 audiohw_set_monitor(true); /* line 1 analog audio path */
             }
-
             break;
-#endif
     } /* end switch */
 
     last_source = source;
diff --git a/firmware/tuner_sanyo.c b/firmware/tuner_sanyo.c
new file mode 100644
index 0000000..1f4533b
--- /dev/null
+++ b/firmware/tuner_sanyo.c
@@ -0,0 +1,909 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ * $Id$
+ * Tuner driver for the Sanyo LV24020LP
+ *
+ * Copyright (C) 2007 Ivan Zupan
+ *
+ * All files in this archive are subject to the GNU General Public License.
+ * See the file COPYING in the source tree root for full license agreement.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+
+#include <stdbool.h>
+#include <stdlib.h>
+#include "config.h"
+#include "thread.h"
+#include "kernel.h"
+#include "tuner.h" /* tuner abstraction interface */
+#include "fmradio.h" /* physical interface driver */
+#include "mpeg.h"
+#include "sound.h"
+#include "pp5024.h"
+#include "system.h"
+#include "as3514.h"
+
+#ifndef BOOTLOADER
+
+#if 0
+/* define to enable tuner logging */
+#define SANYO_TUNER_LOG
+#endif
+
+#ifdef SANYO_TUNER_LOG
+#include "sprintf.h"
+#include "file.h"
+
+static int fd_log = -1;
+
+#define TUNER_LOG_OPEN()    if (fd_log < 0) \
+                                fd_log = creat("/tuner_dump.txt")
+/* syncing required because close() is never called */
+#define TUNER_LOG_SYNC()    fsync(fd_log)
+#define TUNER_LOG(s...)     fdprintf(fd_log, s)
+#else
+#define TUNER_LOG_OPEN()
+#define TUNER_LOG_SYNC()
+#define TUNER_LOG(s...)
+#endif /* SANYO_TUNER_LOG */
+
+/** tuner register defines **/
+
+/* pins on GPIOH port */
+#define FM_NRW_PIN      3
+#define FM_CLOCK_PIN    4
+#define FM_DATA_PIN     5
+#define FM_CLK_DELAY    1
+
+/* block 1 registers */
+
+/* R */
+#define CHIP_ID     0x00
+
+/* W */
+#define BLK_SEL     0x01
+    #define BLK1 0x01
+    #define BLK2 0x02
+
+/* W */
+#define MSRC_SEL    0x02
+    #define MSR_O   (1 << 7)
+    #define AFC_LVL (1 << 6)
+    #define AFC_SPD (1 << 5)
+    #define MSS_SD  (1 << 2)
+    #define MSS_FM  (1 << 1)
+    #define MSS_IF  (1 << 0)
+
+/* W */
+#define FM_OSC      0x03
+
+/* W */
+#define SD_OSC      0x04
+
+/* W */
+#define IF_OSC      0x05
+
+/* W */
+#define CNT_CTRL    0x06
+    #define CNT1_CLR  (1 << 7)
+    #define CTAB(x)   ((x) & (0x7 << 4))
+        #define CTAB_STOP_2     (0x0 << 4)
+        #define CTAB_STOP_8     (0x1 << 4)
+        #define CTAB_STOP_32    (0x2 << 4)
+        #define CTAB_STOP_128   (0x3 << 4)
+        #define CTAB_STOP_512   (0x4 << 4)
+        #define CTAB_STOP_2048  (0x5 << 4)
+        #define CTAB_STOP_8192  (0x6 << 4)
+        #define CTAB_STOP_32768 (0x7 << 4)
+    #define SWP_CNT_L (1 << 3)
+    #define CNT_EN    (1 << 2)
+    #define CNT_SEL   (1 << 1)
+    #define CNT_SET   (1 << 0)
+
+/* W */
+#define IRQ_MSK 0x08
+    #define IM_MS   (1 << 6)
+    #define IRQ_LVL (1 << 3)
+    #define IM_AFC  (1 << 2)
+    #define IM_FS   (1 << 1)
+    #define IM_CNT2 (1 << 0)
+
+/* W */
+#define FM_CAP      0x09
+
+/* R */
+#define CNT_L       0x0a /* Counter register low value */
+
+/* R */
+#define CNT_H       0x0b /* Counter register high value */
+
+/* R */
+#define CTRL_STAT   0x0c
+    #define AFC_FLG (1 << 0)
+
+/* R */
+#define RADIO_STAT  0x0d
+    #define RSS_MS        (1 << 7)
+    #define RSS_FS(x)     ((x) & 0x7f)
+    #define RSS_FS_GET(x) ((x) & 0x7f)
+    #define RSS_FS_SET(x) (x)
+/* Note: Reading this register will clear field strength and mono/stereo interrupt. */
+
+/* R */
+#define IRQ_ID      0x0e
+    #define II_CNT2  (1 << 5)
+    #define II_AFC   (1 << 3)
+    #define II_FS_MS (1 << 0)
+
+/* W */
+#define IRQ_OUT     0x0f
+
+/* block 2 registers - offset added in order to id and avoid manual
+   switching */
+#define BLK2_START  0x10
+
+/* W */
+#define RADIO_CTRL1 (0x02 + BLK2_START)
+    #define EN_MEAS (1 << 7)
+    #define EN_AFC  (1 << 6)
+    #define DIR_AFC (1 << 3)
+    #define RST_AFC (1 << 2)
+
+/* W */
+#define IF_CENTER   (0x03 + BLK2_START)
+
+/* W */
+#define IF_BW       (0x05 + BLK2_START)
+
+/* W */
+#define RADIO_CTRL2 (0x06 + BLK2_START)
+  #define VREF2      (1 << 7)
+  #define VREF       (1 << 6)
+  #define STABI_BP   (1 << 5)
+  #define IF_PM_L    (1 << 4)
+  #define AGCSP      (1 << 1)
+  #define AM_ANT_BSW (1 << 0) /* ?? */
+
+/* W */
+#define RADIO_CTRL3 (0x07 + BLK2_START)
+    #define AGC_SLVL (1 << 7)
+    #define VOLSH    (1 << 6)
+    #define TB_ON    (1 << 5)
+    #define AMUTE_L  (1 << 4)
+    #define SE_FM    (1 << 3)
+    #define SE_BE    (1 << 1)
+    #define SE_EXT   (1 << 0) /* For LV24000=0, LV24001/24002=Ext source enab. */
+
+/* W */
+#define STEREO_CTRL (0x08 + BLK2_START)
+    #define FRCST       (1 << 7)
+    #define FMCS(x)     ((x) & (0x7 << 4))
+    #define FMCS_GET(x) (((x) & (0x7 << 4)) >> 4)
+    #define FMCS_SET(x) ((x) << 4)
+    #define AUTOSSR     (1 << 3)
+    #define PILTCA      (1 << 2)
+    #define SD_PM       (1 << 1)
+    #define ST_M        (1 << 0)
+
+/* W */
+#define AUDIO_CTRL1 (0x09 + BLK2_START)
+    #define TONE_LVL(x)     ((x) & (0xf << 4))
+    #define TONE_LVL_GET(x) (((x) & (0xf << 4)) >> 4)
+    #define TONE_LVL_SET(x) ((x) << 4)
+    #define VOL_LVL(x)      ((x) & 0xf)
+    #define VOL_LVL_GET(x)  ((x) & 0xf)
+    #define VOL_LVL_SET(x)  ((x) << 4)
+
+/* W */
+#define AUDIO_CTRL2 (0x0a + BLK2_START)
+    #define BASS_PP     (1 << 0)
+    #define BASS_P      (1 << 1) /* BASS_P, BASS_N are mutually-exclusive */
+    #define BASS_N      (1 << 2)
+    #define TREB_P      (1 << 3) /* TREB_P, TREB_N are mutually-exclusive */
+    #define TREB_N      (1 << 4)
+    #define DEEMP       (1 << 5)
+    #define BPFREQ(x)   ((x) & (0x3 << 6))
+    #define BPFREQ_2_0K (0x0 << 6)
+    #define BPFREQ_1_0K (0x1 << 6)
+    #define BPFREQ_0_5K (0x2 << 6)
+    #define BPFREQ_HIGH (0x3 << 6)
+
+/* W */
+#define PW_SCTRL    (0x0b + BLK2_START)
+    #define SS_CTRL(x)     ((x) & (0x7 << 5))
+    #define SS_CTRL_GET(x) (((x) & (0x7 << 5)) >> 5)
+    #define SS_CTRL_SET(x) ((x) << 5)
+    #define SM_CTRL(x)     ((x) & (0x7 << 2))
+    #define SM_CTRL_GET(x) (((x) & (0x7 << 2)) >> 2)
+    #define SM_CTRL_SET(x) ((x) << 2)
+    #define PW_HPA         (1 << 1) /* LV24002 only */
+    #define PW_RAD         (1 << 0)
+
+/* shadow for writeable registers */
+#define TUNER_POWERED           (1 << 0)
+#define TUNER_PRESENT           (1 << 1)
+#define TUNER_AWAKE             (1 << 2)
+#define TUNER_PRESENCE_CHECKED  (1 << 3)
+static unsigned tuner_status = 0;
+
+static unsigned char sanyo_regs[0x1c];
+
+static const int sw_osc_low  = 10;  /* 30; */
+static const int sw_osc_high = 240; /* 200; */
+static const int sw_cap_low  = 0;
+static const int sw_cap_high = 191;
+
+/* linear coefficients used for tuning */
+static int coef_00, coef_01, coef_10, coef_11;
+
+/* DAC control register set values */
+int if_set, sd_set;
+
+static inline bool tuner_awake(void)
+{
+    return (tuner_status & TUNER_AWAKE) != 0;
+}
+
+/* send a byte to the tuner - expects write mode to be current */
+static void tuner_sanyo_send_byte(unsigned int byte)
+{
+    int i;
+
+    byte <<= FM_DATA_PIN;
+
+    for (i = 0; i < 8; i++)
+    {
+        GPIOH_OUTPUT_VAL &= ~(1 << FM_CLOCK_PIN);
+
+        GPIOH_OUTPUT_VAL = (GPIOH_OUTPUT_VAL & ~(1 << FM_DATA_PIN)) |
+                            (byte & (1 << FM_DATA_PIN));
+
+        GPIOH_OUTPUT_VAL |= (1 << FM_CLOCK_PIN);
+        udelay(FM_CLK_DELAY);
+
+        byte >>= 1;
+    }
+}
+
+/* end a write cycle on the tuner */
+static void tuner_sanyo_end_write(void)
+{
+    /* switch back to read mode */
+    GPIOH_OUTPUT_EN &= ~(1 << FM_DATA_PIN);
+    GPIOH_OUTPUT_VAL &= ~(1 << FM_NRW_PIN);
+}
+
+/* prepare a write cycle on the tuner */
+static unsigned int tuner_sanyo_begin_write(unsigned int address)
+{
+    /* Get register's block, translate address */
+    unsigned int blk = (address >= BLK2_START) ?
+                            (address -= BLK2_START, BLK2) : BLK1;
+
+    for (;;)
+    {
+        /* Prepare 3-wire bus pins for write cycle */
+        GPIOH_OUTPUT_VAL |= (1 << FM_NRW_PIN);
+        GPIOH_OUTPUT_EN |= (1 << FM_DATA_PIN);
+
+        udelay(FM_CLK_DELAY);
+
+        /* current block == register block? */
+        if (blk == sanyo_regs[BLK_SEL])
+            return address;
+
+        /* switch block */
+        sanyo_regs[BLK_SEL] = blk;
+
+        /* data first */
+        tuner_sanyo_send_byte(blk);
+        /* then address */
+        tuner_sanyo_send_byte(BLK_SEL);
+
+        tuner_sanyo_end_write();
+
+        udelay(FM_CLK_DELAY);
+    }
+}
+
+/* write a byte to a tuner register */
+static void tuner_sanyo_write(unsigned int address, unsigned int data)
+{
+    /* shadow logical values but do logical=>physical remappings on some
+       registers' data. */
+    sanyo_regs[address] = data;
+
+    switch (address)
+    {
+    case FM_OSC:
+        /* L: 000..255
+         * P: 255..000 */
+        data = 255 - data;
+        break;
+    case FM_CAP:
+        /* L: 000..063, 064..191
+         * P: 255..192, 127..000 */
+        data = ((data < 64) ? 255 : (255 - 64)) - data;
+        break;
+    case RADIO_CTRL1:
+        /* L: data
+         * P: data | always "1" bits */
+        data |= (1 << 4) | (1 << 1) | (1 << 0);
+        break;
+    }
+
+    address = tuner_sanyo_begin_write(address);
+
+    /* data first */
+    tuner_sanyo_send_byte(data);
+    /* then address */
+    tuner_sanyo_send_byte(address);
+
+    tuner_sanyo_end_write();
+}
+
+/* helpers to set/clear register bits */
+static void tuner_sanyo_write_or(unsigned int address, unsigned int bits)
+{
+    tuner_sanyo_write(address, sanyo_regs[address] | bits);
+}
+
+static void tuner_sanyo_write_and(unsigned int address, unsigned int bits)
+{
+    tuner_sanyo_write(address, sanyo_regs[address] & bits);
+}
+
+/* read a byte from a tuner register */
+static unsigned int tuner_sanyo_read(unsigned int address)
+{
+    int i;
+    unsigned int toread;
+
+    address = tuner_sanyo_begin_write(address);
+
+    /* address */
+    tuner_sanyo_send_byte(address);
+
+    tuner_sanyo_end_write();
+
+    /* data */
+    toread = 0;
+    for (i = 0; i < 8; i++)
+    {
+        GPIOH_OUTPUT_VAL &= ~(1 << FM_CLOCK_PIN);
+        udelay(FM_CLK_DELAY);
+
+        toread |= (GPIOH_INPUT_VAL & (1 << FM_DATA_PIN)) << i;
+
+        GPIOH_OUTPUT_VAL |= (1 << FM_CLOCK_PIN);
+    }
+
+    return toread >> FM_DATA_PIN;
+}
+
+/* enables auto frequency centering */
+static void enable_afc(bool enabled)
+{
+    unsigned int radio_ctrl1 = sanyo_regs[RADIO_CTRL1];
+
+    if (enabled)
+    {
+        radio_ctrl1 &= ~RST_AFC;
+        radio_ctrl1 |= EN_AFC;
+    }
+    else
+    {
+        radio_ctrl1 |= RST_AFC;
+        radio_ctrl1 &= ~EN_AFC;
+    }
+
+    tuner_sanyo_write(RADIO_CTRL1, radio_ctrl1);
+}
+
+static int calculate_coef(unsigned fkhz)
+{
+    /* Overflow below 66000kHz --
+       My tuner tunes down to a min of ~72600kHz but datasheet mentions
+       66000kHz as the minimum. ?? Perhaps 76000kHz was intended? */
+    return fkhz < 66000 ?
+        0x7fffffff : 0x81d1a47efc5cb700ull / ((uint64_t)fkhz*fkhz);
+}
+
+static int interpolate_x(int expected_y, int x1, int x2, int y1, int y2)
+{
+    return y1 == y2 ?
+        0 : (int64_t)(expected_y - y1)*(x2 - x1) / (y2 - y1) + x1;
+}
+
+static int interpolate_y(int expected_x, int x1, int x2, int y1, int y2)
+{
+    return x1 == x2 ?
+        0 : (int64_t)(expected_x - x1)*(y2 - y1) / (x2 - x1) + y1;
+}
+
+/* this performs measurements of IF, FM and Stereo frequencies
+ * Input can be: MSS_FM, MSS_IF, MSS_SD */
+static int tuner_measure(unsigned char type, int scale, int duration)
+{
+    int64_t finval;
+
+    if (!tuner_awake())
+        return 0;
+
+    /* enable measuring */
+    tuner_sanyo_write_or(MSRC_SEL, type);
+    tuner_sanyo_write_and(CNT_CTRL, ~CNT_SEL);
+    tuner_sanyo_write_or(RADIO_CTRL1, EN_MEAS);
+
+    /* reset counter */
+    tuner_sanyo_write_or(CNT_CTRL, CNT1_CLR);
+    tuner_sanyo_write_and(CNT_CTRL, ~CNT1_CLR);
+
+    /* start counter, delay for specified time and stop it */
+    tuner_sanyo_write_or(CNT_CTRL, CNT_EN);
+    udelay(duration*1000 - 16);
+    tuner_sanyo_write_and(CNT_CTRL, ~CNT_EN);
+
+    /* read tick count */
+    finval = (tuner_sanyo_read(CNT_H) << 8) | tuner_sanyo_read(CNT_L);
+
+    /* restore measure mode */
+    tuner_sanyo_write_and(RADIO_CTRL1, ~EN_MEAS);
+    tuner_sanyo_write_and(MSRC_SEL, ~type);
+
+    /* convert value */
+    if (type == MSS_FM)
+        finval = scale*finval*256 / duration;
+    else
+        finval = scale*finval / duration;
+
+    return (int)finval;
+}
+
+/* set the FM oscillator frequency */
+static void sanyo_set_frequency(int freq)
+{
+    int coef, cap_value, osc_value;
+    int f1, f2, x1, x2;
+    int count;
+
+    if (!tuner_awake())
+        return;
+
+    TUNER_LOG_OPEN();
+
+    TUNER_LOG("set_frequency(%d)\n", freq);
+
+    enable_afc(false);
+
+    /* MHz -> kHz */
+    freq /= 1000;
+
+    TUNER_LOG("Select cap:\n");
+
+    coef = calculate_coef(freq);
+    cap_value = interpolate_x(coef, sw_cap_low, sw_cap_high,
+                    coef_00, coef_01);
+
+    osc_value = sw_osc_low;
+    tuner_sanyo_write(FM_OSC, osc_value); 
+
+    /* Just in case - don't go into infinite loop */
+    for (count = 0; count < 30; count++)
+    {
+        int y0 = interpolate_y(cap_value, sw_cap_low, sw_cap_high,
+                                coef_00, coef_01);
+        int y1 = interpolate_y(cap_value, sw_cap_low, sw_cap_high,
+                                coef_10, coef_11);
+        int coef_fcur, cap_new, coef_cor, range;
+         
+        tuner_sanyo_write(FM_CAP, cap_value);
+
+        range     = y1 - y0;
+        f1        = tuner_measure(MSS_FM, 1, 16);
+        coef_fcur = calculate_coef(f1);
+        coef_cor  = calculate_coef((f1*1000 + 32*256) / 1000);
+        y0        = coef_cor;
+        y1        = y0 + range;
+
+        TUNER_LOG("%d %d %d %d %d %d %d %d\n",
+                  f1, cap_value, coef, coef_fcur, coef_cor, y0, y1, range);
+
+        if (coef >= y0 && coef <= y1) 
+        {
+            osc_value = interpolate_x(coef, sw_osc_low, sw_osc_high,
+                                        y0, y1);
+
+            if (osc_value >= sw_osc_low && osc_value <= sw_osc_high)
+                break;
+        }
+
+        cap_new = interpolate_x(coef, cap_value, sw_cap_high,
+                                coef_fcur, coef_01);
+
+        if (cap_new == cap_value)
+        {
+            if (coef < coef_fcur)
+                cap_value++;
+            else
+                cap_value--;
+        }
+        else
+        {
+            cap_value = cap_new;
+        }
+    }
+
+    TUNER_LOG("osc_value: %d\n", osc_value);
+
+    TUNER_LOG("Tune:\n");
+
+    x1 = sw_osc_low, x2 = sw_osc_high;
+    /* FM_OSC already at SW_OSC low and f1 is already the measured
+       frequency */
+
+    do
+    {
+        int x2_new;
+
+        tuner_sanyo_write(FM_OSC, x2);
+        f2 = tuner_measure(MSS_FM, 1, 16);
+
+        if (abs(f2 - freq) <= 16)
+        {
+            TUNER_LOG("%d %d %d %d\n", f1, f2, x1, x2);
+            break;
+        }
+
+        x2_new = interpolate_x(freq, x1, x2, f1, f2);
+
+        x1 = x2, f1 = f2, x2 = x2_new;
+        TUNER_LOG("%d %d %d %d\n", f1, f2, x1, x2);
+    }
+    while (x2 != 0);
+
+    if (x2 == 0)
+    {
+        /* May still be close enough */
+        TUNER_LOG("tuning failed - diff: %d\n", f2 - freq);
+    }
+
+    enable_afc(true);
+
+    TUNER_LOG("\n");
+
+    TUNER_LOG_SYNC();
+}
+
+static void fine_step_tune(int (*setcmp)(int regval), int regval, int step)
+{
+    /* Registers are not always stable, timeout if best fit not found soon
+       enough */
+    unsigned long abort = current_tick + HZ*2;
+    int flags = 0;
+
+    while (TIME_BEFORE(current_tick, abort))
+    {
+        int cmp;
+
+        regval = regval + step;
+
+        cmp = setcmp(regval);
+
+        if (cmp == 0)
+            break;
+
+        step = abs(step);
+
+        if (cmp < 0)
+        {
+            flags |= 1;
+            if (step == 1)
+                flags |= 4;
+        }
+        else
+        {
+            step = -step;
+            flags |= 2;
+            if (step == -1)
+                step |= 8;
+        }
+
+        if ((flags & 0xc) == 0xc)
+            break;
+
+        if ((flags & 0x3) == 0x3)
+        {
+            step /= 2;
+            if (step == 0)
+                step = 1;
+            flags &= ~3;
+        }
+    }
+}
+
+static int if_setcmp(int regval)
+{
+    tuner_sanyo_write(IF_OSC, regval);
+    tuner_sanyo_write(IF_CENTER, regval);
+    tuner_sanyo_write(IF_BW, 65*regval/100);
+
+    if_set = tuner_measure(MSS_IF, 1000, 32);
+
+    /* This register is bounces around by a few hundred Hz and doesn't seem
+       to be precisely tuneable. Just do 110000 +/- 500 since it's not very
+       critical it seems. */
+    if (abs(if_set - 109500) <= 500)
+        return 0;
+
+    return if_set < 109500 ? -1 : 1;
+}
+
+static int sd_setcmp(int regval)
+{
+    tuner_sanyo_write(SD_OSC, regval);
+
+    sd_set = tuner_measure(MSS_SD, 1000, 32);
+
+    if (abs(sd_set - 38300) <= 31)
+        return 0;
+
+    return sd_set < 38300 ? -1 : 1;
+}
+
+static void sanyo_sleep(bool sleep)
+{
+    if (sleep || tuner_awake())
+        return;
+
+    if ((tuner_status & (TUNER_PRESENT | TUNER_POWERED)) !=
+        (TUNER_PRESENT | TUNER_POWERED))
+        return;
+
+    tuner_status |= TUNER_AWAKE;
+
+    enable_afc(false);
+
+    /* 2. Calibrate the IF frequency at 110 kHz: */
+    tuner_sanyo_write_and(RADIO_CTRL2, ~IF_PM_L);
+    fine_step_tune(if_setcmp, 0x80, 8);
+    tuner_sanyo_write_or(RADIO_CTRL2, IF_PM_L);
+
+    /* 3. Calibrate the stereo decoder clock at 38.3 kHz: */
+    tuner_sanyo_write_or(STEREO_CTRL, SD_PM);
+    fine_step_tune(sd_setcmp, 0x80, 8);
+    tuner_sanyo_write_and(STEREO_CTRL, ~SD_PM);
+
+    /* calculate FM tuning coefficients */
+    tuner_sanyo_write(FM_CAP, sw_cap_low);
+    tuner_sanyo_write(FM_OSC, sw_osc_low);
+    coef_00 = calculate_coef(tuner_measure(MSS_FM, 1, 64));
+
+    tuner_sanyo_write(FM_CAP, sw_cap_high);
+    coef_01 = calculate_coef(tuner_measure(MSS_FM, 1, 64));
+
+    tuner_sanyo_write(FM_CAP, sw_cap_low);
+    tuner_sanyo_write(FM_OSC, sw_osc_high);
+    coef_10 = calculate_coef(tuner_measure(MSS_FM, 1, 64));
+
+    tuner_sanyo_write(FM_CAP, sw_cap_high);
+    coef_11 = calculate_coef(tuner_measure(MSS_FM, 1, 64));
+
+    /* set various audio level settings */
+    tuner_sanyo_write(AUDIO_CTRL1, TONE_LVL_SET(0) | VOL_LVL_SET(0));
+    tuner_sanyo_write_or(RADIO_CTRL2, AGCSP);
+    tuner_sanyo_write_or(RADIO_CTRL3, VOLSH);
+    tuner_sanyo_write(STEREO_CTRL, FMCS_SET(7) | AUTOSSR);
+    tuner_sanyo_write(PW_SCTRL, SS_CTRL_SET(3) | SM_CTRL_SET(1) |
+                      PW_RAD);
+}
+
+/** Public interfaces **/
+bool radio_power(bool status)
+{
+    static const unsigned char tuner_defaults[][2] =
+    {
+        /* Block 1 writeable  registers */
+        { MSRC_SEL,    AFC_LVL },
+        { FM_OSC,      0x80 },
+        { SD_OSC,      0x80 },
+        { IF_OSC,      0x80 },
+        { CNT_CTRL,    CNT1_CLR | SWP_CNT_L },
+        { IRQ_MSK,     0x00 }, /* IRQ_LVL -> Low to High */
+        { FM_CAP,      0x80 },
+     /* { IRQ_OUT,     0x00 }, No action on this register (skip) */
+        /* Block 2 writeable registers */
+        { RADIO_CTRL1, EN_AFC },
+        { IF_CENTER,   0x80 },
+        { IF_BW,       65*0x80 / 100 }, /* 65% of IF_OSC */
+        { RADIO_CTRL2, IF_PM_L },
+        { RADIO_CTRL3, AGC_SLVL | SE_FM },
+        { STEREO_CTRL, FMCS_SET(4) | AUTOSSR },
+        { AUDIO_CTRL1, TONE_LVL_SET(7) | VOL_LVL_SET(7) },
+        { AUDIO_CTRL2, BPFREQ_HIGH },   /* deemphasis 50us */
+        { PW_SCTRL,    SS_CTRL_SET(3) | SM_CTRL_SET(3) | PW_RAD },
+    };
+
+    unsigned i;
+    bool powered = tuner_status & TUNER_POWERED;
+
+    if (status == powered)
+        return powered;
+
+    if (status)
+    {
+        /* init mystery amplification device */
+        outl(inl(0x70000084) | 0x1, 0x70000084);
+        outl(inl(0x70000080) | 0x4, 0x70000080);
+        udelay(5);
+
+        /* When power up, host should initialize the 3-wire bus in host read
+           mode: */
+
+        /* 1. Set direction of the DATA-line to input-mode. */
+        GPIOH_OUTPUT_EN &= ~(1 << FM_DATA_PIN); 
+        GPIOH_ENABLE |= (1 << FM_DATA_PIN); 
+
+        /* 2. Drive NR_W low */
+        GPIOH_OUTPUT_VAL &= ~(1 << FM_NRW_PIN); 
+        GPIOH_OUTPUT_EN |= (1 << FM_NRW_PIN); 
+        GPIOH_ENABLE |= (1 << FM_NRW_PIN); 
+
+        /* 3. Drive CLOCK high */
+        GPIOH_OUTPUT_VAL |= (1 << FM_CLOCK_PIN); 
+        GPIOH_OUTPUT_EN |= (1 << FM_CLOCK_PIN); 
+        GPIOH_ENABLE |= (1 << FM_CLOCK_PIN);
+
+        tuner_status |= TUNER_POWERED;
+
+        /* if tuner is present, CHIP ID is 0x09 */
+        if (tuner_sanyo_read(CHIP_ID) == 0x09)
+        {
+            tuner_status |= TUNER_PRESENT;
+
+            /* After power-up, the LV2400x needs to be initialized as
+               follows: */
+
+            /* 1. Write default values to the registers: */
+            sanyo_regs[BLK_SEL] = 0; /* Force a switch on the first */
+            for (i = 0; i < ARRAYLEN(tuner_defaults); i++)
+                tuner_sanyo_write(tuner_defaults[i][0], tuner_defaults[i][1]);
+
+            /* Complete the startup calibration if the tuner is woken */
+            udelay(100000);
+        }
+    }
+    else
+    {
+        /* Power off and set all as inputs */
+        if (tuner_status & TUNER_PRESENT)
+            tuner_sanyo_write_and(PW_SCTRL, ~PW_RAD);
+
+        GPIOH_OUTPUT_EN &= ~((1 << FM_DATA_PIN) | (1 << FM_NRW_PIN) |
+                             (1 << FM_CLOCK_PIN));
+        GPIOH_ENABLE &= ~((1 << FM_DATA_PIN) | (1 << FM_NRW_PIN) |
+                          (1 << FM_CLOCK_PIN)); 
+
+        outl(inl(0x70000084) & ~0x1, 0x70000084);
+
+        tuner_status &= ~(TUNER_POWERED | TUNER_AWAKE);
+    }
+
+    return powered;
+}
+
+bool radio_powered(void)
+{
+    return (tuner_status & TUNER_POWERED) != 0;
+}
+
+int sanyo_set(int setting, int value)
+{
+    int val = 1;
+
+    switch(setting)
+    {
+    case RADIO_SLEEP:
+        sanyo_sleep(value);
+        break;
+
+    case RADIO_FREQUENCY:
+        sanyo_set_frequency(value);
+        break;
+
+    case RADIO_SCAN_FREQUENCY:
+        /* TODO: really implement this */
+        sanyo_set_frequency(value);
+        val = sanyo_get(RADIO_TUNED);
+        break;
+
+    case RADIO_MUTE:
+        if (value)
+            tuner_sanyo_write_and(RADIO_CTRL3, ~AMUTE_L);
+        else
+            tuner_sanyo_write_or(RADIO_CTRL3, AMUTE_L);
+        break;
+
+    case RADIO_REGION:
+        switch (value)
+        {
+        case REGION_EUROPE:
+        case REGION_JAPAN:
+        case REGION_KOREA:
+            tuner_sanyo_write_and(AUDIO_CTRL2, ~DEEMP);
+            break;
+        case REGION_US_CANADA:
+            tuner_sanyo_write_or(AUDIO_CTRL2, DEEMP);
+            break;
+        default:
+            val = -1;
+        }
+        break;
+
+    case RADIO_FORCE_MONO:
+        if (value)
+            tuner_sanyo_write_or(STEREO_CTRL, ST_M);
+        else
+            tuner_sanyo_write_and(STEREO_CTRL, ~ST_M);
+        break;
+
+    default:
+        val = -1;
+    }
+
+    return val;
+}
+
+int sanyo_get(int setting)
+{
+    int val = -1;
+
+    switch(setting)
+    {
+    case RADIO_ALL:
+        return tuner_sanyo_read(CTRL_STAT);
+
+    case RADIO_TUNED:
+        /* TODO: really implement this */
+        val = RSS_FS(tuner_sanyo_read(RADIO_STAT)) < 0x1f;
+        break;
+
+    case RADIO_STEREO:
+        val = (tuner_sanyo_read(RADIO_STAT) & RSS_MS) != 0;
+        break;
+
+    case RADIO_PRESENT:
+        val = (tuner_status & TUNER_PRESENT) != 0;
+        break;
+
+    /* tuner-specific debug info */
+    case RADIO_REG_STAT:
+        return tuner_sanyo_read(RADIO_STAT);
+
+    case RADIO_MSS_FM:
+        return tuner_measure(MSS_FM, 1, 16);
+
+    case RADIO_MSS_IF:
+        return tuner_measure(MSS_IF, 1000, 16);
+
+    case RADIO_MSS_SD:
+        return tuner_measure(MSS_SD, 1000, 16);
+
+    case RADIO_IF_SET:
+        return if_set;
+
+    case RADIO_SD_SET:
+        return sd_set;
+    }
+
+    return val;
+}
+#endif /* BOOTLOADER */