dsMP3, my LW/MW/FM/SW radio with MP3 recording/playback [Part 2 – Firmware Design]

0.00 avg. rating (0% score) - 0 votes

In my previous post I described how I designed my home-built LW/MW/FM/SW radio which supports MP3 recording/playback using the dsPIC33EP512MC502 microcontroller, the Si4735 chipset as well as the CH376 USB controller. Despite the limited capabilities of the chosen microcontroller (48KB of RAM, 512KB of flash memory, overclocked to 180MHz), the device can perform reasonably well and achieve good audio quality, both for radio/MP3 playback as well as for radio stations recording. In this post, I will describe how the firmware was written, software design challenges as well as some useful tips and tricks when writing applications for the dsPIC.

Microcontroller selection

I picked the dsPIC33EP512MC502 simply because it’s the fastest dsPIC device that is still available in DIP package. It officially supports up to 70MIPS (140MHz) but can be overclocked to 180MHz. With 48KB SRAM and 512KB of flash memory, the device can play 128Kbps MP3 files smoothly using optimized assembly code. On the other hand, although the PIC32MX250F128B with 32KB SRAM and 128KB of flash memory is also available in DIP, its lower speed makes MP3 playback using the Helix MP3 library impractical. During testing, I could only play 32Kbps mono MP3 files from USB smoothly using the PIC32MX250F128B.  Granted, if DIP availability is not a concern, I could have chosen a faster PIC32 which is only available for SMD, and made full use of the higher pin count, faster speed, as well as the integrated USB controller. Using the dsPIC33EP512MC502, an external USB controller such as the CH376 is needed, although SD card interface is still done via the dsPIC SPI module.

The dsPIC is overclocked to 180MHz, on the internal oscillator, using the following code:

// Highest possible clock speed, for MP3 playback
// also check mmc_pic.h for SD card speed
// The CPU needs 1 (PIC32), 2 (dsPIC33/PIC24F) or 4 (8-bit PIC) clocks per instruction cycle, but the peripherals only need 1 clock.
#define FINT                7372800ULL				// internal oscillator frequency, Hz
#define MPLLFBD_HIGH	    (98)                                // M = PLLFBD + 2. Overclocked to ~184MHz (98). Official max. 74 (140MHz, 70MHz). Reliable overclock @ 180MHz
#define	MPLLPOST            (0)					// N2 = 2
#define	MPLLPRE             (0)					// N1 = 2
#define FOSC_HIGH           (FINT * (MPLLFBD_HIGH + 2) / 2 / 2) // FINT * M / (N1 * N2)

// Low Clock Speed, for radio management (except when recording), and for drawing clock occasionally in standby
#define MPLLFBD_LOW         (7)
#define FOSC_LOW            (FINT * (MPLLFBD_LOW + 2) / 2 / 2)		// FINT * M / (N1 * N2)

// Init PLL
PLLFBD = MPLLFBD_HIGH;                	// M = PLLFBD + 2
CLKDIVbits.PLLPOST = MPLLPOST;     	// N2 = 2
CLKDIVbits.PLLPRE = MPLLPRE;  		// N1 = 2
CLKDIVbits.DOZE   = 0;

If you want to try this, use a stripboard (not a breadboard) and keep connection as neat as possible. Take note that not all devices can work at 180MHz, so you might want to reduce the speed or try with a different device. Most devices will work fine for 10-20MIPS above the maximum supported speed of 70MIPS, after which there will be a “transit” range where the device will appear to work fine for basic operation (LED toggling) but is actually running at a much slower speed (e.g. 40MHz instead of 160MHz), which can be verified by monitoring the clock-out signal at the CLKO/OSC2 pin. Above this range, the PLL oscillator will not start at all and the PIC will not work. Once you have confirmed that your device can be overclocked, run some simple logic program (e.g. output prime numbers on the serial port) and test the dsPIC for reliability first before using the device at the selected speed.

Clock switching

When the user chooses to turn off the device, output of pin 15 (RB6) of the dsPIC will be set to 0V, removing power to the peripherals. The dsPIC will also be switched to the low-power RC oscillator at 32kHz to save power. The low clock rate is still sufficient for the dsPIC to function as an alarm clock, displaying the current time on the LCD, waking up the PIC upon alarms and when a key is pressed. An interrupt is now configured on the keypad, allowing the PIC to resume operation when the POWER key is pressed.  During radio playback (without recording), the device can also run at a much lower speed of 16MHz (instead of 180MHz) to save power and reduces interference. All this functionality can be performed using the following code snippets:

void setClockMode(unsigned char mode)

    // Wait for Clock switch to occur
    while (OSCCONbits.COSC != mode);                     

    // Wait until clock switch is complete
    while (OSCCONbits.OSWEN);           

    // adjust PLL divider

During my test, the code works well for the most part. However, sometimes when switching from LPRC (32kHz) to high speed (16Mhz or 180MHz), the PIC will crash upon first attempt but will resume booting normally after that. This does not affect functionality so I did not study the issue in details. I simple assume that the problem is due to overclocking or power rail instabilities.

Memory management

Although 48KB of SRAM and 512KB of flash memory is a lot compared with low-end devices such as the PIC24, memory is still tight for MP3 playback and has to be used sparingly. Page 49 of the datasheet (PROGRAM MEMORY MAP FOR dsPIC33EP512GP50X …) sets out the memory organization and will have to be careful studied before any serious code is written:

dsPIC memory organization

For my case, the left and right PCM buffer (for audio playback and recording) as well as the frame buffer (for LCD display) is allocated in data memory (SRAM) whereas other values (fonts, station database, icons, MP3 constants) will be allocated into program memory (e.g. flash memory). By default, the compiler/linker will try to allocate array into data memory, which will consume precious SRAM space. take note that 3 bytes of flash will be required for 2 bytes of data written into program memory, as the data will essentially have to be stored as instructions by the compiler/linker, each of which takes 3 bytes on this 24-bit microcontroller.

It is worth highlighting that only 16KB of the 48KB of SRAM on the dsPIC33EP512MC502 can be accessed via normal methods. The upper 32KB of SRAM memory belongs to the Extended Data Space (EDS) and has to be accessed via special means, described on section 3.3 (Data Space Addressing) on page 35 of the datasheet. Although you should certainly read the datasheet for a full understanding of EDS operation, using C, all you need to to is to add __eds__ to your array declaration to have the compiler take care of it. The following is how I declared the left/right PCM audio buffer queue, located in SRAM, in Program Space Visibility (PSV) or normal memory, depending on whether EDS is available:

#define QUEUE_CAPACITY   3840

#define queue_post_t unsigned int
#define queue_val_t  signed int

#ifdef USE_EDS
__eds__ volatile queue_val_t __attribute__((eds, section("pcmQueueLeft"), space(ymemory))) queue_members_left[QUEUE_CAPACITY];
__eds__ volatile queue_val_t __attribute__((eds, section("pcmQueueRight"), space(ymemory))) queue_members_right[QUEUE_CAPACITY];
volatile queue_val_t queue_members_left[QUEUE_CAPACITY];
volatile queue_val_t queue_members_right[QUEUE_CAPACITY];

This is how I declared the various icons in flash memory:

__prog__ static const unsigned char __attribute__((section("sdErrorIcon"), space(prog))) sdErrorIcon[] =  {
56,     // Width
48,     // Height, should be divisible by 8
....    // raw data bytes of icon

__prog__ static const unsigned char __attribute__((section("usbErrorIcon"), space(prog))) usbErrorIcon[] =  {
86,     // Width
48,     // Height
....    // raw data bytes of icon

You should specified the name of the memory section (for your reference only), which will be shown in the linker output. The linker will locate the correct address (within program memory) to store the data.

You can also manually set the address of your array by using the address() construct. The following will declare the Tbl[] table in PSV memory at the address specified byFATFS_TBLDEF_MEM_ADDR:

#define FATFS_TBLDEF_MEM_ADDR       (0x6084)

const WCHAR __attribute__((section("tbldefConst"), space(psv), address(FATFS_TBLDEF_MEM_ADDR))) Tbl[] = { 

With these declarations you can read values from these arrays the normal way, e.g. using tbl[index] without having to worry about where these arrays are located. If you however use C pointers to access these arrays, then you might need to use the __psv__ keyword. Refer to the XC16 compiler user guide for more details.

It is also very important to make sure that include files with PSV section declarations are not included multiple times otherwise the linker will attempt to process the declaration multiple times and generate a very misleading error message (address for input section “array” (0xfae0) conflicts with output section “array” (0xfb10)) as the “array” section is processed twice from repeated attempts to include that file. More detailed explanations are available in file psv_mem_addr.h, included in the source code package at the end of this article.

SRAM and flash usage

This is the linker output, showing that my code uses 81% of program memory and 86% of data memory:

"program" Memory  [Origin = 0x200, Length = 0x555ec]

section                    address   length (PC units)   length (bytes) (dec)
-------                    -------   -----------------   --------------------
.text                        0x200              0x2a00          0x3f00  (16128)
.const                      0x2c00              0x23e4          0x35d6  (13782)
.init.delay32               0x4fe4                0x1c            0x2a  (42)
mp3data                     0x5000              0x1084          0x18c6  (6342)
tbldefConst                 0x6084               0x100           0x180  (384)
exCvtConst                  0x6184                0x80            0xc0  (192)
cvt1Const                   0x6204               0x1f2           0x2eb  (747)
cvt2Const                   0x63f8                0xbc           0x11a  (282)
madCrcTable                 0x64b4               0x200           0x300  (768)
.text                       0x66b4              0x9e6c          0xeda2  (60834)
swFreqDb2                  0x10520              0x7ff8          0xbff4  (49140)
swFreqDb1                  0x18518              0x7fc4          0xbfa6  (49062)
swFreqDb3                  0x204dc              0x7ef4          0xbe6e  (48750)
swFreqDb4                  0x283d0              0x788e          0xb4d5  (46293)
.text                      0x2fc5e              0x486a          0x6c9f  (27807)
stationNames               0x344c8              0x2c0c          0x4212  (16914)
ssbPatch                   0x370d4              0x2288          0x33cc  (13260)
.text                      0x3935c              0x1fa6          0x2f79  (12153)
extraLargeFont             0x3b302              0x1800          0x2400  (9216)
.text                      0x3cb02              0x1680          0x21c0  (8640)
largeFont                  0x3e182               0xe00          0x1500  (5376)
.text                      0x3ef82               0xdbc          0x149a  (5274)
siteNames                  0x3fd3e               0xa52           0xf7b  (3963)
.text                      0x40790              0x1e42          0x2d63  (11619)
worldMapIcon               0x425d2               0x402           0x603  (1539)
.text                      0x429d4               0x400           0x600  (1536)
swCountries                0x42dd4               0x2ee           0x465  (1125)
.text                      0x430c2               0x2ba           0x417  (1047)
swLanguages                0x4337c               0x2a8           0x3fc  (1020)
usbErrorIcon               0x43624               0x206           0x309  (777)
noBatteryIcon              0x4382a               0x1e2           0x2d3  (723)
.text                      0x43a0c               0x1e0           0x2d0  (720)
mediumFont                 0x43bec               0x1e0           0x2d0  (720)
.dinit                     0x43dcc               0x1c6           0x2a9  (681)
radioErrorIcon             0x43f92               0x1b4           0x28e  (654)
.text                      0x44146               0x2e2           0x453  (1107)
sdErrorIcon                0x44428               0x152           0x1fb  (507)
noFilesIcon                0x4457a               0x152           0x1fb  (507)
.text                      0x446cc               0x152           0x1fb  (507)
helpMenuMedia              0x4481e               0x142           0x1e3  (483)
helpMenuRadio              0x44960               0x142           0x1e3  (483)
.text                      0x44aa2               0x274           0x3ae  (942)
bitrate_table              0x44d16               0x12c           0x1c2  (450)
.text                      0x44e42               0x602           0x903  (2307)
smallFont                  0x45444                0xc0           0x120  (288)
.text                      0x45504               0x2c0           0x420  (1056)
rawRemoteCodes             0x457c4                0x3e            0x5d  (93)
swBandsConst               0x45802                0x38            0x54  (84)
.text                      0x4583a                0x42            0x63  (99)
radioIcon                  0x4587c                0x20            0x30  (48)
battery33Icon              0x4589c                0x12            0x1b  (27)
battery66Icon              0x458ae                0x12            0x1b  (27)
battery100Icon             0x458c0                0x12            0x1b  (27)
batteryEmptyIcon           0x458d2                0x12            0x1b  (27)
batteryInvalidIcon         0x458e4                0x12            0x1b  (27)
usbIcon                    0x458f6                0x10            0x18  (24)
signalIcon                 0x45906                0x10            0x18  (24)
sdIcon                     0x45916                 0xe            0x15  (21)
stereoIcon                 0x45924                 0xe            0x15  (21)
monoIcon                   0x45932                 0xc            0x12  (18)
fineTuneIcon               0x4593e                 0xc            0x12  (18)
forwardIcon                0x4594a                 0xc            0x12  (18)
backwardIcon               0x45956                 0xc            0x12  (18)
checkMarkIcon              0x45962                 0xc            0x12  (18)
recordingIcon              0x4596e                 0xa             0xf  (15)
stopIcon                   0x45978                 0xa             0xf  (15)
alarmIcon                  0x45982                 0xa             0xf  (15)
alarmEmptyIcon             0x4598c                 0xa             0xf  (15)
az000Icon                  0x45996                 0xa             0xf  (15)
az045Icon                  0x459a0                 0xa             0xf  (15)
az090Icon                  0x459aa                 0xa             0xf  (15)
az135Icon                  0x459b4                 0xa             0xf  (15)
az180Icon                  0x459be                 0xa             0xf  (15)
az225Icon                  0x459c8                 0xa             0xf  (15)
az270Icon                  0x459d2                 0xa             0xf  (15)
playIcon                   0x459dc                 0x8             0xc  (12)
pauseIcon                  0x459e4                 0x8             0xc  (12)

                 Total "program" memory used (bytes):        0x683df  (426975) 81%

"data" Memory  [Origin = 0x1000, Length = 0xc000]

section                    address      alignment gaps    total length  (dec)
-------                    -------      --------------    -------------------
.nbss                       0x1000                   0             0x2  (2)
.xbss                       0x1002                   0          0x1c08  (7176)
.bss                        0x2c0a                   0          0x4836  (18486)
.data                       0x7440                   0           0x14a  (330)
.bss                        0x758a                   0            0x26  (38)
.data                       0x75b0                   0            0x54  (84)
.bss                        0x7604                   0            0x28  (40)
.data                       0x762c                   0             0x6  (6)
.bss                        0x7632                   0             0x2  (2)
.data                       0x7634                   0             0x2  (2)
lcdFrameBuffer              0x9000                   0           0x400  (1024)
pcmQueueRight               0x9400                   0          0x1e00  (7680)
pcmQueueLeft                0xb200                   0          0x1e00  (7680)

                 Total "data" memory used (bytes):         0xa636  (42550) 86%

Dynamic Memory Usage

region                     address                      maximum length  (dec)
------                     -------                      ---------------------
heap                             0                                   0  (0)
stack                       0x7636                               0x9ca  (2506)

                 Maximum dynamic memory (bytes):          0x9ca  (2506)

You can see that the linker has found suitable address to locate the various section that we have declared. The smallFont, mediumFont and largeSmall arrays store the x35, 5×7 and 8×16 bitmap font respectively. Also, the .const section consolidates various “constants” which were used by built-in library functions. In my experience, the size and address of this const section needs to be kept as low as possible. If the const section grows large, the linker will put it at very high program memory location (e.g. 0x40000) and your code will crash (most likely because the assembly instructions generated by the compilers can’t reach these address). The issue will happen for example if you have a lot of integer arrays in your code which is not manually located in flash memory, use certain format specifiers for sprintf or if you make excessive use of functions in math.h (see below). Keeping this in mind, I was able to reduce the .const section size to less than 16KB, which is then placed at 0x2c00 by the linker, which works well.

You should also take care not to use above 88-89% of data memory. Among other things, the stack is allocated by the linker to be at the end of available data memory space. Consuming too much data memory and there will not be enough memory for the stack, resulting in your code crashing after just a few function calls. In older version of xc16, the linker will allocate a fixed amount of memory for the stack. However, in recent version, the linker will allocate all available data memory for the stack, subject to a minimum. These options can be changed in Project Properties > Conf > XC16 > xc16-ld.

On a side note, you can’t use C-style memory allocation functions (e.g. malloc) on the PIC with default settings, as a heap is not allocated by the linker. For it to work, set Heap Size to some value (e.g. 256) in the same dialog. I however seldom use this in my microcontroller career, as I often use arrays allocated on the stack instead.

doku MP3 library

Decoding MP3 is done with the help of the doku library, initially designed for the dsPIC33FJ256GP710, which I ported to the dsPIC33EPMC502. Apart from replacing PSVPAG with DSRPAG, I had to add a few interesting NOP instructions. The issue was with the following lines of code:

do #(8-1), 2f /* internal loop */

mpy W4*W5, A, [W8]+=2, W4 /* ACCA = xr_up[i] * cs */
mpy W4*W6, B, [W8], W4 /* ACCB = xr_dw[i] * cs, prepare ca[i] */

msc W4*W6, A, [W8]+=2, W4 /* ACCA = (xr_up[i]*cs) - ca*(xr_down[i]*cs) */
mac W4*W5, B, [W8], W4 /* ACCB = ca*(xr_up[i]*cs) + (xr_down[i]*cs), prepare cs[i+1] */

The above code performs some calculations and stores the final value in accumulator A and B. When the above code is run on the simulator, I receive the expected values for both accumulators and the ported code works entirely. However, when run on an actual device, value for accumulator A is correct but value for accumulator B is wrong. I have checked and confirmed that both the starting values of all required registers and the values for these registers after exiting the loop are the same on both simulator and device, just that the end result stored in accumulator B is wrong!

What is weird is that if the debugger (PicKit4) is used to step through the above line of codes when running on the device, then the final value for both ACCA and ACCB will be the same as the simulator’s result and therefore correct! This makes it impossible for me to use the debugger to troubleshoot the issue. What is even stranger is that simply adding NOP after each instruction in the above code will result in different final values for both ACCA and ACCB, even though the NOP is not supposed to do anything. Adding NOP when running on the simulator will not result in changes in final values of the accumulators, as expected.

I posted on the Microchip forum only to be met with unhelpful replies by folks who did not bother to even bother to read the detailed information I provided in my question. Eventually, after a lot of debugging and researching, I managed to locate the issue. The solution was mentioned in a single sentence in the migration guide which was overlooked. The first instruction right after the DO instruction on the dsPIC33E/PIC24E cannot be an instruction that accesses the PSV data space. Apparently the simulator did not care and so things worked properly on the simulator. On my code, [W2++] and [W3++] pointed to PSV and there was the problem. Adding a single NOP right after the DO instruction and the code now worked properly with same result on both device and simulator. And well, the NOPs I tried to add during the debugging phase were in various places but never right after the DO instruction, otherwise I would have figured it out much sooner. Once this issue was sorted, things worked as it should and adding further NOPs did not affect anything.

Further studying the migration guide, I realized that the dsPIC33E has a new instruction called MOVPAG, which can be used to simplify the following two lines of codes:

mov     #psvpage(mad_const_psv),W8
mov     W8,                     DSRPAG          /* set required PSV page */

into a single line:

MOVPAG #psvpage(mad_const_psv), DSRPAG

The MOVPAG change is however optional as the original code with two MOV instructions still works fine on the dsPIC3EP512MC502. With these changes, the doku MP3 library now works properly on the dsPIC3EP512MC502. Running at 180MHz, the dsPIC can now play good quality stereo audio using 10-bit PWM for audio output.

Audio playback

MP3 audio playback is done with the help of the interrupt-safe ring buffers implementation, adapted from here. Essentially, in function pcm_put from the doku MP3 library, decoded PCM samples are added to the left and right channel PCM queue. In the Timer 4 interrupt function, shared between playback, these samples are then retrieved from the queue and send via the two PWM channels for audio output. As WAV playback is also supported, suitable functions are written to convert either 8-bit (for WAV) or 16-bit (for decoded MP3) PCM samples to 10-bit or 12-bit values, before writing to the PWM registers:

// 12-bit PWM (~45 kHz)
#define PWM_BITS_REM8       4
#define PWM_BITS_REM16      4
#define PWM_VALUE           4096       

unsigned int from16toPWM1(int inp)
    return ((inp + 32768) >> PWM_BITS_REM16);

// convert PCM 8-bit unsigned value to PWM unsigned value
unsigned int from8toPWM(int inp)
    return ((unsigned int)inp << PWM_BITS_REM8);

void __attribute__((interrupt, no_auto_psv))  _T4Interrupt (void)
    if (is16bitPCM)
        // 16-bit audio data, convert to PWM data
        PDC1 = from16toPWM1(lastLeftVal);
        PDC2 = from16toPWM1(lastRightVal);
        // 8-bit audio data, bit shift to match PWM value
        PDC1 = from8toPWM(lastLeftVal);
        PDC2 = from8toPWM(lastRightVal);

To save power, the PWM should only be enabled when there is active audio. Otherwise, the amplifier will be active all the time even without audio (due to the presence of PWM carrier signal), wasting unnecessary current.

Audio recording

Mono audio recording is achieved by feeding the mono output from the amplifier module into the dsPIC’s ADC, using a timer to store the ADC sampling data into a buffer, and finally by occasionally writing the buffer data into a WAV file periodically. Filtering algorithm is also applied on the ADC output prior to writing to the WAV file so as to remove the DC offset which was introduced due to our circuit design, and also to convert 12-bit ADC into 16-bit PCM values. The following code demonstrates, in part, how to do this:

void initTmr4()
    // See Timers and Interrupts on the PIC32MZ post
    // https://www.aidanmocke.com/blog/2018/11/15/timers/
	// enable timer 4
	T4CONbits.TON = 0; // Disable Timer
	T4CONbits.TCS = 0; // Select internal instruction cycle clock
	T4CONbits.TGATE = 0; // Disable Gated Timer mode
	T4CONbits.TCKPS = 0b00; // Select 1:1 Prescaler
	TMR4 = 0x00; // Clear timer register

	IPC6bits.T4IP = 0x05; // Set Timer Interrupt Priority Level, 7 is highest (do not use), must match the function header declaration
	IFS1bits.T4IF = 0;   // Clear Timer Interrupt Flag
	IEC1bits.T4IE = 1;   // Enable Timer interrupt
	T4CONbits.TON = 1;   // Start Timer

// Timer 4 for PCM playback/recorder
void __attribute__((interrupt, no_auto_psv))  _T4Interrupt (void)
    if (isRecording)
        recAvgVal += readAnalogANPinSigned(RECORDER_INPUT_AN_PIN);      // recording input on AN0 (RA0)

	queue_put_left(recAvgVal << 1);

void main()
	while (1)
		if (queue_get_len_left() > BUFFER_TOPUP_THRESHOLD)
			UINT bytesWritten = 0;
			FRESULT fWRes = f_write(&fMediaFile, recBuf, count*2, &bytesWritten);
			if (fWRes != FR_OK || bytesWritten < (count*2) )
				debugPrint("wrEr:%d", fWRes);


Once recording has been stopped by the user, the code would then calculate the recording duration and update the WAV file header (which has been generated with default values when recording was started). The recording process produces a 8kHz 16-bit mono WAV file with sufficiently clear audio quality, thanks to the optimization and enhanced audio filtering implemented.

Custom math library

There is a feature on the radio which display the distance of the currently received shortwave station from the current location, based on the station coordinates (as retrieved from the hard-coded database in flash memory) and the current coordinates (as entered by user). This function uses several math function (e.g. sqrt() and tan()). However, as mentioned above, using math.h function to any degree and the linker will put the const section into high program memory, causing the code to crash. I resorted to writing my own equivalent of these functions, which is not very accurate but good enough. For example, this is how I implemented my own fast_sqrt function

float fast_sqrt(float x)  {
    if (x < 0)
        return 0;

    float z = 1.0;
    unsigned int i = 0;
    for (i=1; i <= 10; i++) {
      z -= (z*z - x) / (2*z);
    return z;

Si4735 SSB patch

In this project I used the Si4735 interface library adapted from this Github repository, originally written for the Arduino. I have improved auto-tuning methods as well as removed unused codes. The Si47XX Programming Guide (AN332) Revision 1.0 has also been useful for me during the development.

The Si4735 is configured to work in I2C mode, for which I wrote my own custom I2C functions using bit-banging. I have to admit that I have never gotten the built-in I2C module to work reliably during my years working with the PIC. My custom I2C library works reliably for my needs, which simply involving exchanging small amount of data with external EEPROMs and sensors.

During initialization, the code retrieves the SI4735 version number and prints to the screen. On my version of the  Si4735-D60, the version number should read 54.48 if everything works properly. If the Si4735 (or the EEPROM) does not respond via I2C, try adding a small capacitor (< 100pF) to GND on either the SDA or SCK line. This trick has helped me many times during my previous projects.

Among the devices in the Si473x family, the Si4735-D60 supports SSB by loading a patch that can be sent via I2C after the chip has been initialized. The patch is approximately 9KB and will take a while to be transferred via I2C to the chip. This delay (up to 5 seconds) can also be observed on the Tecsun PL365.

Interfacing SD card and CH376 USB controller

In my design the CH376 USB controller and the SD card is interfaced in SPI mode. I did not use the Microchip Peripherals Library (available at C:\Program Files (x86)\Microchip\mplabc30\v3.31\src\peripheral_30F_24H_33F\include\spi.h) which introduces function such as OpenSPI1(), ReadSPI1() and WriteSPI1(), making the code just a little bit easier to read. Instead, my code reads and writes the SPI registers directly. In my opinion, the helper function seldom works out of the box. As getting them to work still involves reading the datasheet and understanding various SPI register configuration, I might as well setting the SPI registers directly. My custom SPI library is included in the full source code for those who are interested.

For things to work, in the mmc_pic.h header file of the FatFs library, you should declare your SPI initialization and SPI write functions:

/* Socket controls  (Platform dependent) */
#define SD_CS_LOW         LATAbits.LATA2 = 0                             /* Chip Select Low */
#define SD_CS_HIGH        LATAbits.LATA2 = 1                             /* Chip Select High */
#define CD               1                                              /* Card detected   (yes:true, no:false, default:true) */
#define WP               0                                              /* Write protected (yes:true, no:false, default:false) */

/* SPI bit rate controls */
#define	FCLK_SLOW		spi1Init(SYNC_MODE_SLOW, 1, 0)                     /* Set slow clock, 100k-400k (Nothing to do) */
#define	FCLK_FAST		spi1Init(SYNC_MODE_FAST, 1, 0)                     /* Set fast clock, depends on the CSD (Nothing to do) */
#define sd_tx(d)        spi1Write(d)
#define sd_rx()         spi1Write(0xFF)

Don’t ask me why there is no separate declaration for reading an SPI byte. If you are asking this question, go back and practice some LED toggling. In SPI, one must always write a byte before he can read back anything at all.

Although the CH376 can also read SD cards, SD cards are still interfaced directly to the dsPIC, to allow the dsPIC to turn off the CH376 to save current when SD card is in use. If you wish to save some pins, feel free to connect the SD card to the CH376, and update the code accordingly.

Remote control decoding

Remote control input is decoded by using the TFMS5400 infrared receiver. This module decodes infrared signals generated by a wide range of remote controls with a carrier frequency between 38-42KHz. I bought a pack of 50 for a cheap price from AliExpress. The module demodulates the signal received, removes the carrier signal and produces the original signal on its output pin, which will be assigned an interrupt on the dsPIC. If the exact TFMS5400 part is not available, you can find similar modules on eBay, or build equivalent circuit using some diodes and photo-transistors.

The remote control that I have chosen, as well as most other remote controls, follows the Sony SIRC protocol, described here and also in my previous article. The output is normally high if there’s no input and a long period of low signal signifies that a key has been pressed:


Implementing the remote control as a state machine, I wrote an interrupt function which decodes the input signals and detects the key that has been pressed:

void __attribute__((interrupt, no_auto_psv))  _INT2Interrupt(void)
	IFS1bits.INT2IF = 0;        // Reset Interrupt Flag

    isTFMSInt = TRUE;

    unsigned char c = 0xFF;

    if (TFMS_INP)
        // high input, we now look for high-to-low (e.g. falling edge)
        INTCON2bits.INT2EP = 1;
        // SendUART('/');
    else {
        // low input, we now look for low-to-input (e.g. rising edge)
        INTCON2bits.INT2EP = 0;
        // SendUART('*');

    if (TFMS_INP == 0 && remoteState == REMOTE_IDLE)								// wait until low data line (begins of transmission)
        // SendUART('1');
        // SendUART(dataBit ? 'W' : 'X');

        TMR3_DELAY_CNT = 0;
        remoteState = REMOTE_INIT_LOW;
    else if (TFMS_INP == 1 && remoteState == REMOTE_INIT_LOW) 					// first low phase (9ms) to second high phase
        if (TMR3_LAPSED_x10MS() > 80)
            // only if initial low phase is long enough
            // SendUART('2');

            TMR3_DELAY_CNT = 0;
            remoteState = REMOTE_INIT_HIGH;
        else {
            // otherwise timeout, do nothing
    else if (TFMS_INP == 0 && remoteState == REMOTE_INIT_HIGH)					// second high phase (4ms) to first sign data bit
        if (TMR3_LAPSED_x10MS() > 30)
            // only if second high phase is long enough
            // SendUART('3');

            TMR3_DELAY_CNT = 0;
            remoteState = REMOTE_SIGN1;		

            tfmsBitCount = 0;
        else {
            // otherwise timeout, do nothing
    else if (remoteState >= REMOTE_SIGN1 && TFMS_INP == 0)						// sign data byte & main dara byte
        // SendUART('4');

        // Binary 0 is represented by a pulse for 0.5ms and binary 1 by a pulse for 1.6ms.
        unsigned char bitVal = (TMR3_LAPSED_x10MS() > 12 ? 1 : 0);
        if (remoteState == REMOTE_SIGN1)
            tfmsSignByte1 = tfmsSignByte1 + (bitVal << tfmsBitCount);
        else if (remoteState == REMOTE_SIGN2)
            tfmsSignByte2 = tfmsSignByte2 + (bitVal << tfmsBitCount);
        else if (remoteState == REMOTE_DATABYTE1)
            tfmsDataByte1 = tfmsDataByte1 + (bitVal << tfmsBitCount);
        else if (remoteState == REMOTE_DATABYTE2)
            tfmsDataByte2 = tfmsDataByte2 + (bitVal << tfmsBitCount);

        TMR3_DELAY_CNT = 0;

        // exceed 8 bit, switch to next remote stage
        if (tfmsBitCount >= 8)
            // SendUART('|');
            tfmsBitCount = 0;
    else if (remoteState == REMOTE_EXTRADATA)
        // dummy transmission at the end (20ms), ignore
        // status will be reset at main upon timeout
        // SendUART('Y');
        TMR3_DELAY_CNT = 0;

    // if we are in the receive stage, validate information
	if (remoteState == REMOTE_EXTRADATA)
		if (tfmsSignByte1 == 1 && tfmsSignByte2 == 254 && tfmsDataByte1 == (unsigned char)~tfmsDataByte2)
			// valid data
			c = tfmsDataByte1;
            // SendUART('^');
        else {
            // invalid data
            // SendUART('!');

        // debugPrint("%d %d %d %d", tfmsSignByte1, tfmsSignByte2, tfmsDataByte1, tfmsDataByte2);
        // debugPrint("%d", tfmsDataByte1);

    if (c != 0xFF)
        // convert raw remote code to associated key
        unsigned char ind = 0;
        lastRemoteChar = KEY_INVALID;
        for (ind = 0; ind < sizeof(rawRemoteCodes) / 2; ind++)
            unsigned char rawCode = rawRemoteCodes[ind*2];
            KEYCODE remoteChar = rawRemoteCodes[ind*2+1];             

            if (c == rawCode)
                lastRemoteChar = remoteChar;

        // power off from remote control (must turn on again using hardware keypad)
        if (lastRemoteChar == KEY_RED_OFF)
            isPwrBtnPressed = TRUE;
            keepPwrOnInStandby = FALSE;
            lastRemoteChar = KEY_INVALID;

        // enter mixer mode (analog only) using gray off key
        if (lastRemoteChar == KEY_GRAY_OFF)
            shouldEnterStandby = TRUE;
            keepPwrOnInStandby = TRUE;
            lastRemoteChar = KEY_INVALID;
    else {
        // Invalid or no key pressed yet
        lastRemoteChar = KEY_INVALID;

The exact code generated for each key will be different for each remote control; however the protocol remains the same. You will need to test your remote control to find out the various key codes it generates and update the key code assignment accordingly.

The dsPIC main loop will then watch out for changes in the lastRemoteChar variable and act on the key presses accordingly.


You can download the MPLAB X project source code as well as the KiCad PCB design files here. Some useful notes about the project can be found in the notes.txt file located in the same ZIP file.

Read also my previous article on this project, which covers the hardware design aspects and contains some videos showing the radio in action.

0.00 avg. rating (0% score) - 0 votes


A tough developer who likes to work on just about anything, from software development to electronics, and share his knowledge with the rest of the world.

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>