dsMP3, my LW/MW/FM/SW radio with MP3 recording/playback [Part 2 – Firmware Design]
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) 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) { 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.