Programming N800 FM radio receiver

The N800 internet tablet features a tea5761 FM tuner chip for listening to FM radio. The kernel driver for this chip was written by Nokia and is GPL'd open source. You can look at it by downloading the Maemo kernel source code.

The driver implements the V4L2 radio interface and is thus controlled like a V4L2 device.

Contents

[edit] How to do it in Python

By using Python's ioctl function, you can directly talk to the driver, so there is no need for wrapping some C library.

[edit] Prerequisites

Let's first import the required modules:

    import os                 # for opening the device files
    import struct             # for packing and unpacking C structs
    from fcntl import ioctl   # for invoking ioctl calls on the device files

Since we are going to talk to kernel drivers, we have to use weird numbers. C users would just include the appropriate Linux kernel header files, but Python users have to do it by hand. So let's make some defines for kernel level ioctl stuff:

    # kernel definitions for ioctl commands
    _IOC_NRBITS   = 8
    _IOC_TYPEBITS = 8
    _IOC_SIZEBITS = 14
    _IOC_DIRBITS  = 2
 
    _IOC_NRSHIFT   = 0
    _IOC_TYPESHIFT = _IOC_NRSHIFT + _IOC_NRBITS
    _IOC_SIZESHIFT = _IOC_TYPESHIFT + _IOC_TYPEBITS
    _IOC_DIRSHIFT  = _IOC_SIZESHIFT + _IOC_SIZEBITS
 
    _IOC_WRITE = 1
    _IOC_READ  = 2
 
    _IOC = lambda d,t,nr,size: (d << _IOC_DIRSHIFT) | (ord(t) << _IOC_TYPESHIFT) | \
        (nr << _IOC_NRSHIFT) | (size << _IOC_SIZESHIFT)
    _IOW  = lambda t,nr,size: _IOC(_IOC_WRITE, t, nr, size)
    _IOR  = lambda t,nr,size: _IOC(_IOC_READ, t, nr, size)
    _IOWR = lambda t,nr,size: _IOC(_IOC_READ | _IOC_WRITE, t, nr, size)

The functions _IOW, _IOR, and _IOWR (macros in C) are used for write-access, read-access, and read-write-access, respectively. They basically just take some input values and compute a number which can be sent to the device driver. You can think of the number as the signature of the function which you want to call on the driver.

Now we can generate those weird numbers for the V4L2 functions we are going to invoke on the driver:

    # V4L2 stuff for accessing the tuner driver
    _VIDIOC_G_TUNER     = _IOWR('V', 29, 84)
    _VIDIOC_G_AUDIO     = _IOR ('V', 33, 52)
    _VIDIOC_S_AUDIO     = _IOW ('V', 34, 52)
    _VIDIOC_G_FREQUENCY = _IOWR('V', 56, 44)
    _VIDIOC_S_FREQUENCY = _IOW ('V', 57, 44)
    _VIDIOC_G_CTRL      = _IOWR('V', 27, 8)
    _VIDIOC_S_CTRL      = _IOWR('V', 28, 8)
 
    # user-class control IDs defined by V4L2
    _V4L2_CTRL_CLASS_USER = 0x00980000
    _V4L2_CID_BASE        = _V4L2_CTRL_CLASS_USER | 0x900
    _V4L2_CID_AUDIO_MUTE  = _V4L2_CID_BASE + 9

And last, let's define some numbers for working with the mixer device:

    # mixer control constants
    _SOUND_MIXER_FMRADIO         = 0x06
    _SOUND_MIXER_READ            = 0x80044D00
    _SOUND_MIXER_WRITE           = 0xC0044D00

[edit] Opening the device

You gain access to the radio driver by opening the /dev/radio device. Since you need a system FD instead of a native Python FD, we use os.open instead of the built-in function open:

    radio_fd = os.open("/dev/radio", os.O_RDONLY)

If there's no tuner chip available (for example, if you run this code on the 770 or the N810), this line will throw an OSError. Don't forget to catch it.

[edit] Retrieving information about the FM tuner

Now it's time to talk to the FM tuner. We do this by placing an ioctl call on the device. ioctl calls consist of a number (those we have defined above), and some argument. The argument is a pointer for passing a C structure to the driver. The driver may read from this structure or write into it.

A C structure can be built in Python with the struct module. Because the following ioctl call only retrieves information from the driver, we may provide an empty structure of the right size, like this:

    info = struct.pack("84x")

Let's place the ioctl call, and retrieve the results. For unpacking the structure, we have to provide the correct format string (see help(struct) for details):

    data = ioctl(radio_fd, _VIDIOC_G_TUNER, info)
 
    # unpack the C struct
    fields = struct.unpack("L32sLLLLLLll4L", data)    
    tuner_index = fields[0]
    tuner_name = fields[1]
    tuner_type = fields[2]
    capability = fields[3]
    rangelow = fields[4]
    rangehigh = fields[5]
    rxsubchans = fields[6]
    audmode = fields[7]
    signal = fields[8]
    afc = fields[9]

We should save the tuner_index and tuner_name for later, because this is how we address that particular tuner.

[edit] Setting the frequency

Next, we are going to tune into a radio station. For this, we have to set a frequency on the tuner. This is done by the V4L2 _VIDIOC_G_FREQUENCY ioctl invokation.

But a word of warning first: the driver does not take the frequency in kHz. You have to multiply your value with a constant factor first. The factor is either 0.016 or 16, depending on the device driver. On the N800, it is 16:

    FREQ_FACTOR = 16

We are going to tune into 107.6 MHz now:

    freq = 107600 * FREQ_FACTOR

Set up the C structure to pass to the driver:

    data = struct.pack("LLL8L", tuner_index, tuner_type, freq, 0, 0, 0, 0, 0, 0, 0, 0)

Pass it to the driver:

    ioctl(radio_fd, _VIDIOC_S_FREQUENCY, data)

[edit] Unmuting the tuner and setting the volume

You still do not hear anything from the radio. This is because the tuner is muted and its volume is set to 0. Let's change this:

Unmuting is easy:

    data = struct.pack("Ll", _V4L2_CID_AUDIO_MUTE, 0)
    ioctl(radio_fd, _VIDIOC_S_CTRL, data)

Changing the volume requires us to open the mixer device:

    mixer_fd = os.open("/dev/mixer", os.O_RDONLY)

Now we can set the volume for the left and right speaker:

    left_volume = 50
    right_volume = 50

... and send it to the driver:

    data = struct.pack("bb", left_volume, right_volume)
    ioctl(mixer, _SOUND_MIXER_WRITE | _SOUND_MIXER_FMRADIO, data)

Do not forget to close the mixer device after this:

    os.close(mixer_fd)

Now you can listen to the radio.

[edit] Cleaning up

When you have finished listening to the radio, you should clean up and close the device again.

It's important to mute the radio, or you will hear it forever.

    data = struct.pack("Ll", _V4L2_CID_AUDIO_MUTE, 1)
    ioctl(radio_fd, _VIDIOC_S_CTRL, data)

And don't forget to close the device:

    os.close(radio_fd)

[edit] Code ready for use

There is a pure Python module available for controlling the FM tuner. It is based on the information here. You can get it from the PyFMRadio project page.