dsMP3, my LW/MW/FM/SW radio with MP3 recording/playback [Part 2 – Firmware Design]
UPDATE (May 2025) – Check out Hackaday’s write-up of the dsMP3 project here
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.FRCDIV = 0; 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)
{
__builtin_disi(0x3FFF);
__builtin_write_OSCCONH(mode);
__builtin_write_OSCCONL(0b001);
__builtin_disi(0x0);
// Wait for Clock switch to occur
while (OSCCONbits.COSC != mode);
// Wait until clock switch is complete
while (OSCCONbits.OSWEN);
...
// adjust PLL divider
PLLFBD = (mode == OP_MODE_FASTEST ? MPLLFBD_HIGH : MPLLFBD_LOW);
}
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:
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];
#else
volatile queue_val_t queue_members_left[QUEUE_CAPACITY];
volatile queue_val_t queue_members_right[QUEUE_CAPACITY];
#endif
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)
.......
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.
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)
{
queue_get_left(&lastLeftVal);
queue_get_right(&lastRightVal);
...
if (is16bitPCM)
{
// 16-bit audio data, convert to PWM data
PDC1 = from16toPWM1(lastLeftVal);
PDC2 = from16toPWM1(lastRightVal);
}
else
{
// 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)
recAvgValCount++;
...
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;
tfmsBitCount++;
// exceed 8 bit, switch to next remote stage
if (tfmsBitCount >= 8)
{
// SendUART('|');
tfmsBitCount = 0;
remoteState++;
}
}
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;
break;
}
}
// 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.
Downloads
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.

