I had plans to implement a small WAV playback application making use of a FAT32/SD card file access. Considering my published demo with SD card Mass Storage, the plan evolved to a more ambitious endeavour: implementing the basic blocks of a WAV player.
The archetype model for this project is the MP3 player device. It has a storage device (HDD, SD card, internal flash) that can be read through file system primitives to access audio data. It has a user interface to control the playback of files. It can be plugged to a PC in order to update its media content. It can draw power from the USB connection to charge its internal battery.
So are my various goals to implement a WAV player:
- Showing how to combine the SD card Mass Storage profile and an independent functionality in one project,
- Organizing the common code and external libraries to be easily shared between different sub-projects.
- Implementing WAV playback and DAC libraries.
The charging feature will be let aside: the Teensy2 target will be powered through its USB port either by the host PC or by an external battery using a MintyBoost.
Strategies to combine Mass Storage and a main application
On constraint from the Mass Storage profile is that it requires to be the master of the flash storage. You are not allowed to access the file system when mounted on the PC host: obviously you can not write data to the flash from an embedded application because the host PC would not be aware of the modifications to data blocks that could be cached on the PC.
On the other side, read access from your application should be possible as long as you support some concurrency in the access to the flash driver. Block read request coming either from USB requests or from your FAT abstraction layer could be queued and served sequentially. But what if data accessed by your embedded application is suddenly deleted by the host ? Your file system should be able to switch from a read-write state to read-only state when connecting to a USB host. What if you are updating the file system at this very moment ? You would need carefully crafted interactions between the USB stack, the file system stack and your application to manage conflict cases. So is the simplest solution: forbid access to the file system when starting the Mass Storage profile.
If you device is battery powered and is already on when plugging to the host, you have to detect the activation of the Mass Storage profile in the USB code and issue a notification to your main application to request it to stop. If you take the example of a digital camera, when you plug it, the camera application is stopped and you switch to a sub-application displaying the status of the connection with the host. No other features are available from the camera. The camera has become a USB key.
As my Teensy2 device is not self-powered, it is always off when I connect it to an host. So I can take advantage of this by commuting between the Mass Storage code and my application during the boot process. But what if I want to connect the device just to draw power from the USB bus and don’t want it to be mounted ? There is no easy solution here: you need to tell the device how you want it to boot with an external signal or user input.
How to boot
Until now my programs had a main function that setup the hardware and then enter an infinite loop to execute an application. But nothing forbids you to implement a very basic main function that check for a user input and then choose between several applications to start. This is the principle used by many devices that allow to enter a maintenance mode when switching on a device while pressing some keyboard combination (but usually it’s a secret combination ).
On the Teensy2, I setup the PORTB5 pin as my boot mode pin. A push button is connected to it. If the button is pressed when I power on the device, then the boot mode value is 1 and the main function of the USB Mass Storage code is called. In the other case, application_main() is called.
The main() function only computes a bootmode value and then calls the boot() function. The boot() function then determines which application to start depending on the bootmode value.
Here is the layout of my project:
apps +- bootmode +- boot.c +- Makefile boot +- main.c => usb +- LowLevelMassStorage utils +- delay.c / delay.h => utility code vendor +- LUFA_091223 => LUFA library +- sd-reader_source_20090330-teensy
This organization has been crafted to ease the re-use of code among several sub-projects of which bootmode is the first one.
The external libraries are grouped in the vendor directory. Sub-projects are gathered in the apps directory. Other source files are grouped by features.
Compiling object files in the project directory
The Makefile I am using is originally a template from WinAVR released to the public domain. By default all object files are generated in the same directory of the source files. So LUFA objects are in the LUFA directory and sd-reader files in the sd-reader directory.
But if you want to compile two different projects sharing a set of source files, you will found out that it is not always desirable to share the resulting objects. For instance, your projects may not use the same compilation flags, one would like to enable the FAT32 support in a library while the other one must stay in FAT16.
The variable OBJDIR in the Makefile specifies the directory in which you want your object files to be put, but the original Makefile assume a flat organization of your source files in the root directory of your project. If you change OBJDIR, the OBJDIR string will be appended to the path of your source files by the following rule:
OBJ = $(SRC:%.c=$(OBJDIR)/%.o) $(ASRC:%.S=$(OBJDIR)/%.o)
If we apply this rule to relative paths, it does not work very well:
OBJDIR = obj SRC = foo.c ../../bar.c => OBJ = obj/foo.o obj/../../bar.o GOOD BAD!!!
To cleanly use the OBJDIR variable, I use only path relative to the root path of my project. The compilation rule has been modified to add the root path and create the object directory:
$(OBJDIR)/%.o : $(ROOT_PATH)/%.c @echo @echo "$(MSG_COMPILING) $< => $@" mkdir -p $(dir $@) $(CC) -c $(ALL_CFLAGS) $< -o $@
Testing the bootmode application
Go to the end of the post to get the source code.
Go in the apps/bootmode subdirectory and type make.
Upload the resulting file bootmode.hex with the Teensy loader.
The serial port is by default configured at 9600 bauds to output text traces to the PC.
Connect the Teensy2 to a PC host: the application start.
$ sudo cu -s 9600 -l /dev/cu.usbserial Connected. MODE APPLICATION Start application Do something... Do something... Do something... Do something...
Connect the Teensy2 to a PC host while pushing the switch: the SD card is mounted on the host.
$ sudo cu -s 9600 -l /dev/tty.usbserial Connected. MODE USB_MS MMC/SD initialization failed USB Connect USB_Device_SetConfiguration USB Ready R 0 1 W 0 1 R 0 48 R 0 1 R 0 1
In the next post I will describe my WAV player implementation.
The source code is published in the following repository: http://bitbucket.org/elasticsheep/teensy2-usb-wavplayer/.
An archive can be directly downloaded from: http://bitbucket.org/elasticsheep/teensy2-usb-wavplayer/get/V1.zip. A compiled binary is also available in the download section.