.NET Micro Framework, the Netduino, MP3s and File Reading

Microcontrollers have truly advanced to the point where it's possible to control them through high-level programming languages.  After surviving my assembly class in college (using a MIPS processor simulator), I swore off bit-shifting and jump statements altogether and never looked back.

Had it not been for the .NET Micro Framework, I don't think I'd even consider trying to get a tiny device to do anything a bigger computer could do; the learning curve was just too high.  Sure, the Arduino prototyping platform paved the way toward where we are now, but I liked the fact that I could write code in a familiar IDE with a familiar language (C#) and get a tiny device to do my bidding-- at least blink-- in a matter of minutes.

Sure, there are still new concepts for object-oriented developers to tackle, like interrupt ports and the need to pick up hardware like resistors, breadboards, and "shields" (devices that can be plugged into the top of the device), but it can bring familiar object-oriented concepts and even multithreading to an entirely new platform. It was actually fun to pick up a soldering iron again, something I haven't done in at least 15 years.

I decided to try the Netduino Plus, a very affordable device with a tiny amount of memory and a growing number of shields that are supported.  Here's my extremely minor contribution to this effort in the post, "compatible shields and accessories."

I've piled on two shields, one for SD card reading / writing and one for playing MP3s.  Provided you're only interested in playing a song encoded at about 30kbps, it's a blast.

Here's the code for Program.cs, which plays each MP3 file on an SD card in succession on a Netduino (predates the Netduino Plus with onboard SD card):

using System;
using Microsoft.SPOT;
using SecretLabs.NETMF.Hardware.Netduino;
using SecretLabs.NETMF.IO;
using System.IO;

namespace NetduinoMp3 {
    public class Program
    {
        private static readonly int FileBufferSize = Vs1053.BufferSize * 10;

        public static void Main()
        {
            Debug.Print(DateTime.Now.ToString());
            StorageDevice.MountSD("SD1", SPI_Devices.SPI1, Pins.GPIO_PIN_D10);

            string[] directories = Directory.GetDirectories(@"\");
            Debug.Print("directory count: " + directories.Length);

            for (var i = 0; i < directories.Length; i++) {
                Debug.Print("directory: " + directories[i]);
            }

            var files = Directory.GetFiles(@"\SD1");
            Debug.Print("file count: " + files.Length);

            Vs1053.Initialize();
            Vs1053.SetVolume(0xFEFE);

            for (var i = 0; i < files.Length; i++) {
                Debug.Print("filename: " + files[i]);
                var fileStream = new FileStream(files[i], FileMode.Open, FileAccess.Read, FileShare.None, _
                        FileBufferSize);

                Vs1053.SendData(fileStream);

                fileStream.Close();
            }
        }
    }
}

And the class "Vs1053.cs", used to control the MP3 shield:

using System;
using System.IO;
using Microsoft.SPOT.Hardware;
using SecretLabs.NETMF.Hardware.Netduino;
using System.Threading;

namespace NetduinoMp3 {
    public static class Vs1053 {
        // GPIO ports:
        static private OutputPort _reset;
        static private InterruptPort _dreq;

        const Cpu.Pin PinBsync = Pins.GPIO_PIN_D2;
        const Cpu.Pin PinDreq = Pins.GPIO_PIN_D3;
        const Cpu.Pin PinReset = Pins.GPIO_PIN_D6;  // NOTE: This doesn't map to an actual pin
        const Cpu.Pin PinCs = Pins.GPIO_PIN_D9;

        // Define SPI Configuration for VS1053 MP3 decoder:
        static private readonly SPI.Configuration DataConfig = new SPI.Configuration(PinBsync, false, _ 
                0, 0, false, true, 3000, SPI.SPI_module.SPI1);
        static private readonly SPI.Configuration CmdConfig = new SPI.Configuration(PinCs, false, _ 
                0, 0, false, true, 3000, SPI.SPI_module.SPI1);
        static private SPI _spi;

        // Registers:
        const int RegisterSciMode = 0x00;
        const int RegisterSciVol = 0x0B;
        const int RegisterSciClockf = 0x03;

        public static readonly int BufferSize = 96;
        private const ushort SciMode = 0x880;  // SM_SDINEW (default) + SM_EARSPEAKER_HI

        static private bool _isInitialized;
        static private readonly byte[] ReadBuffer = new byte[BufferSize];
        static private readonly byte[] CmdBuffer = new byte[4];

        static private readonly AutoResetEvent AutoResetEvent = new AutoResetEvent(false);

        public static void Initialize() {
            if (_isInitialized)
                Shutdown();

            _spi = new SPI(CmdConfig);
            _reset = new OutputPort(PinReset, true);
            _dreq = new InterruptPort(PinDreq, false, Port.ResistorMode.PullUp, Port.InterruptMode.InterruptEdgeBoth);
            _dreq.OnInterrupt += _dreq_OnInterrupt;

            _isInitialized = true;

            Reset();

            CommandWrite(RegisterSciMode, SciMode | (1 << 2));
            CommandWrite(RegisterSciClockf, 7 << 13);
            CommandWrite(RegisterSciVol, 0x2424);

            _spi.Config = DataConfig;
        }

        private static void _dreq_OnInterrupt(uint port, uint state, DateTime time) {
            if (state == 0)
            {
                AutoResetEvent.Set();
            }
            else
            {
                AutoResetEvent.WaitOne();
            }
            _dreq.ClearInterrupt();
        }

        private static void Reset() {
            _reset.Write(false);
            Thread.Sleep(1);
            _reset.Write(true);
            Thread.Sleep(1);
        }

        static private void CommandWrite(byte address, ushort data) {
            CmdBuffer[0] = 0x02;
            CmdBuffer[1] = address;
            CmdBuffer[2] = (byte)(data >> 8);
            CmdBuffer[3] = (byte)data;

            _spi.Write(CmdBuffer);
        }

        static private ushort CommandRead(byte address) {
            CmdBuffer[0] = 0x03;
            CmdBuffer[1] = address;
            CmdBuffer[2] = 0;
            CmdBuffer[3] = 0;

            _spi.WriteRead(CmdBuffer, CmdBuffer, 2);

            ushort command = CmdBuffer[0];
            command <<= 8;
            command += CmdBuffer[1];

            return command;
        }

        public static void SetVolumePercent(int volume) {
            if (volume < 0 || volume > 100)
                throw new ArgumentOutOfRangeException("volume");

            SetVolumePercent(volume, volume);
        }

        public static void SetVolumePercent(int leftChannel, int rightChannel) {
            if (leftChannel < 0 || leftChannel > 100)
                throw new ArgumentOutOfRangeException("leftChannel");
            if (rightChannel < 0 || rightChannel > 100)
                throw new ArgumentOutOfRangeException("rightChannel");

            // TODO: Invert decibel value, divide by percent, call SetVolume(ushort leftChannel, ushort rightChannel)
        }

        public static void SetVolume(ushort bothChannels) {
            CommandWrite(RegisterSciVol, bothChannels);  // TODO: This doesn't work outside the Initialize() method
        }

        public static void SetVolume(ushort leftChannel, ushort rightChannel)
        {
            SetVolume((ushort) (leftChannel*256 + rightChannel)); // TODO: Verify no loss of fidelity
        }

        public static void SendData(FileStream fileStream)
        {
            var size = fileStream.Length - fileStream.Length % BufferSize;
            for (var i = 0; i < size; i += BufferSize) {
                fileStream.Read(ReadBuffer, 0, BufferSize);

                _spi.Write(ReadBuffer);
            }
        }

        public static void Shutdown() {
            if (!_isInitialized) return;

            Reset();

            _spi.Dispose();
            _reset.Dispose();
            _dreq.Dispose();

            _isInitialized = false;
        }
    }
}

I think there's a lot to be desired, but it's hopefully at least a good starting point for somebody and at least extends on my initial post to the Netduino forum that simply stated that the MP3 shield "worked."

Additional References:

Leave a Reply

Your email address will not be published. Required fields are marked *