micropython-vs1053 icon indicating copy to clipboard operation
micropython-vs1053 copied to clipboard

Rpi Pico

Open OlafM2015 opened this issue 3 years ago • 64 comments

Hi, I like to use the vs1053 in combination with the Pico. Do you have recommendations before I start? Many thanks, Olaf

OlafM2015 avatar Jul 28 '22 05:07 OlafM2015

This is a combination that I haven't yet tested but it should work. The first step is getting an SD card working. It's probably best to start with the synchronous driver and MP3 files, creating a modified pbaudio_syn.py to match the RP2.

If you get this working please let me know the details and your code and I'll document it (crediting you, of course).

peterhinch avatar Jul 28 '22 08:07 peterhinch

Hi, I have it working. Needed to change pyb module and adding the sdcard module and the pins. I managed to get I2S out of the VS1053, such that I can use a digital amp (Max98357). Up till now, I get a mp3 bitrate of max 128 kbps. I would like to achieve 192 kbps. When I look at the dreq, I see that it stays a long time high, so, it seems the VS1053 speed of data in is not enough. Furthermore, the SD card stops sometimes. Which is probably also a speed setting issue. (I tested the HW with a "standard" library, there it worked ok). So still some work to do. What would be your recommendation worth increasing the speed/ bitrate? Thanks

OlafM2015 avatar Aug 15 '22 18:08 OlafM2015

Congratulations on getting it working.

the SD card stops sometimes

That sounds serious. I suggest you ask in the forum about the SD card. It's very likely that others have got this working and would have suggestions about maximising performance. I would aim to get the SD card working as reliably and as fast as possible first, because without that you have no chance of achieving good results at higher bitrates.

peterhinch avatar Aug 15 '22 19:08 peterhinch

Indeed, checked the SDcard HW. Made the wiring much shorter and good ground. Now it plays for hours without any error :-) at 128 kbps. In the current config, I see that the dreq is about 5 ms low and then high again. I would like to get it up to 192 kbps, I tried to double the numbers double _DATA_BAUDRATE ,_SCI_BAUDRATE , however, seems that it is more complex. Does it make sense to start experimenting? Probably I need to change also _SCI_CLOCKF. not sure. What would be your advise?

OlafM2015 avatar Aug 16 '22 08:08 OlafM2015

I'll have a look at this - it clearly makes sense for me to test and document its use with RP2. You could try overclocking the Pico - I find they work fine at 250MHz but of course this isn't guaranteed. The _DATA_BAUDRATE is the nominal rate used to read the SD card. This could be increased but it all depends on the quality of the SD card itself. However given that, in my testing, 9-10MHz was adequate for 250Kbps MP3 it really shouldn't be necessary to increase this.

It's a while since I last did anything with the VS1053b but I can't see why the RP2 shouldn't be comparable with Pyboards in this application.

Hopefully I'll have something to report by the weekend.

peterhinch avatar Aug 16 '22 16:08 peterhinch

I've set up the hardware and can replicate your problem. I'll get some testgear on the case on Thursday.

peterhinch avatar Aug 16 '22 18:08 peterhinch

For your information, tested with the pbaudio and the Adafruit_vs_1053.h . See attached pictures. (both on same HW) I have difficulty to understand fully the details of the difference. The sound is for 128 kbps ok for both. At 192 and 256 and 320 the Adafruit is ok. It would be great that the 192 would work for the pbaudio. But maybe/probably the speed of the micropython is not enough. Hopefully it can be optimized.

AdaFruit_vs1053h_128kbps AdaFruit_vs1053h_320kbps pbaudio128kbps pbaudio192kbps

OlafM2015 avatar Aug 18 '22 02:08 OlafM2015

By the way, we do this work for a music player for people with dementia. We are a foundation, working with volunteers to design and produce this music player. We have now produced 100 pieces, based upon the Raspberry pi Zero 2 and an interface board (with Tough interface and backlight leds). The Audio path is I2S to a 2 x Max 98357 amp and two good speakers.
As the Raspberries are not available and we want to start producing 200-300 pieces we started the Pico + VS1053 route. See for more info foundation (sorry for the Dutch) www.stichtingoradio.nl and https://www.youtube.com/watch?v=LoT91xpWNf4 We have the intention to make an Open source kit available, such that people (youngsters) can make the music player for them parents/grandparents. see www.oradio.tech However, we cannot promote this as the Rpi Zero2 is not available. The Pico + VS1053 + interface board would to the trick. Especially with the micropython code it would be a good platform to learn and further increase the Open source activities. Just a brief motivation :-) ToughInterfaceBoard1 ToughInterfaceBoard2 OradioFront )

OlafM2015 avatar Aug 18 '22 02:08 OlafM2015

The code I use to test:

main.zip

OlafM2015 avatar Aug 18 '22 03:08 OlafM2015

An interesting and worthwhile project. I will study this today. I need to be sure I understand exactly what you're doing.

I hadn't appreciated that you're using the asynchronous driver. I'm puzzled by the LA traces. With the ones labelled "Adafruit" are you using the Adafruit CircuitPython driver? I take it the other traces use my vs1053.py asynchronous driver.

Am I right in assuming that all traces use a Pico and the Adafruit VS1053 adaptor?

peterhinch avatar Aug 18 '22 08:08 peterhinch

For the VS1053 I use the Adafruit Music maker, as this one has connections for the I2S of the VS1053. As I had troubles with the SD card, I used another board (the red one) with only resistors as and not the level shift buffers on the adafruit board. The LA traces are as indicated as in your pbaudio script. The Adafruit measurements are from acactly the same HW, but than with the Ardiuno lib and the IDE, in which the driver is in C(++) made. Hope that this helps IMG_0776 IMG_0775 .

OlafM2015 avatar Aug 18 '22 08:08 OlafM2015

I've had some success. I've pushed an updated vs1053.py to a pico branch.

The problem is that the play loop needs to iterate very fast. The old code uses awrite which does a buffer copy. Some time ago I submitted this uasyncio PR for a non-allocating write, but Damien has not implemented it. The solution is a hack, at least until the PR is accepted. I've also used micropython.native: the loop can now iterate in 720us as measured with micropython-monitor.

I will do some more work on this (e.g. to check whether micropython.native is doing anything useful) before merging it with the main branch, but I thought you could make use of a preview.

My test script is

from machine import SPI, Pin
from vs1053 import *
import uasyncio as asyncio

reset = Pin(15, Pin.OUT, value=1)  # Active low hardware reset
sdcs = Pin(5, Pin.OUT, value=1)  # SD card CS
xcs = Pin(14, Pin.OUT, value=1)  # Labelled CS on PCB, xcs on chip datasheet
xdcs = Pin(2, Pin.OUT, value=1)  # Data chip select xdcs in datasheet
dreq = Pin(3, Pin.IN)  # Active high data request
player = VS1053(SPI(0), reset, dreq, xdcs, xcs, sdcs, '/fc')

async def heartbeat():
    led = Pin(25, Pin.OUT)
    while(True):
        led(not(led()))
        await asyncio.sleep_ms(500)

async def main():
    player.volume(-10, -10)  # -10dB (0dB is loudest)
    locn = '/fc/192kbps/'
    asyncio.create_task(heartbeat())
    with open(locn + '01.mp3', 'rb') as f:
        await player.play(f)
    # player.mode_set(SM_EARSPEAKER_LO | SM_EARSPEAKER_HI)  # You decide.
    # player.response(bass_freq=150, bass_amp=15)  # This is extreme.
    with open(locn + '02.mp3', 'rb') as f:
        await player.play(f)
    with open(locn + '03.mp3', 'rb') as f:
        await player.play(f)
    print('All done.')

asyncio.run(main())

peterhinch avatar Aug 18 '22 13:08 peterhinch

Great, could I test it? I am not sure how to test/ find the files? If seen that sdcard.py is updated so I uploaded it. But where can I find the vs1053.py ? (I am not so experienced with Github)

OlafM2015 avatar Aug 18 '22 15:08 OlafM2015

Scrub that - there was a problem. It seems that when you overclock the RP2 the clock frequency survives a soft reset. Consequently my testing was inadvertently done at 250MHz - so the build I pushed did not work after a power cycle.

I now have a build which does work at stock frequency, but I had to replace the StreamWriter code with a synchronous write. Tomorrow I'll do a bit more work on optimising it and push an update with instructions on how to get it.

seen that sdcard.py is updated so I uploaded it

? Not since May 2020 according to my git log.

peterhinch avatar Aug 18 '22 18:08 peterhinch

Ok, thanks for update

OlafM2015 avatar Aug 18 '22 18:08 OlafM2015

I've now pushed an update. To access it online you can change to the pico branch using the dropdown at the top left which initially contains master. You can then copy the code. You'll also find my test scripts, pico.py being the normal one. There are also scripts that use monitor.py and logic analyser images.

An alternative way to access the code is to clone the repo to your PC and issue

$ git checkout pico

You will then find the updated code, test scripts and two logic analyser images in the images directory. These illustrate behaviour when playing a 192Kbps MP3 file. A point worth noting is that dreq spends quite a lot of time False. This is always worth checking: if it spends long periods True it probably means that the VS1053's internal buffer has underrun - distortion is likely. The images show the time taken reading the SD card, writing to the VS1053, and also the points where the driver yields to the scheduler.

I will need to do more testing before merging, notably cancellation and checking whether it can handle more demanding files such as VBR and FLAC. I'll also need to document its use with the Pico.

Please let me know how you get on.

peterhinch avatar Aug 19 '22 09:08 peterhinch

Perhaps worth adding that I'm using the Adafruit 1381 card with an SD card in its slot. I've used this in all my testing including with other hosts: I don't think there is any issue with its design.

The problem is entirely down to the performance of this code (drawn from the play() method in the original version):

       while s.readinto(buf):  # Read <=32 bytes
            cnt += 1
            await sw.awrite(buf)

Use of stream writing had a problem whereby a buffer copy takes place. Even with a mod to prevent copying, it was still slower than a synchronous write. I also found it necessary to use micropython.native. With these changes it is able to play 192Kbps files with the Pico running at stock clock rate. Judging by the behaviour of dreq there is a good margin. The critical part of the revised code is:

        while s.readinto(buf):  # Read <=32 bytes
            cnt += 1
            # Yield when forced to wait or after N iterations
            while (not dreq()) or cnt > 10:
                await asyncio.sleep_ms(0)
                cnt = 0
            self.write(buf)

There are two optimisations: writing is now synchronous and the frequency of yielding to the scheduler is reduced. If it is waiting on dreq it yields. If dreq has remained True for too long (about 6ms) it yields. Margins could be improved further by increasing the cnt value at which it yields. If you have no tasks running which require low latency it could be raised to (say) 30 (~18ms). Possibly it's best viewed as a backstop to prevent .play from stalling the scheduler in the event that dreq stays True.

However .play is quite demanding: you need a good quality SD card. You also need to ensure that any other tasks you create do not use up too much CPU time. In your final code I suggest you monitor dreq to judge whether there is a good margin.

I don't think there is any merit in adjusting SPI clock rates. The docs show that there is a good margin. I checked the actual rate on the Pico and it is over 10MHz.

peterhinch avatar Aug 19 '22 10:08 peterhinch

Great, this works perfect. :-) Did a quick check tested 192, 256 and 320. Even 320, shows a considerable margin on DREQ (see picture). Will further study and like to use the monitor.py to check the yield of the scripts. After that, I will start the design of the first proto of the HW, consisting of a Pico + VS1053 and SDcard slot (and power conditioning). With the I2S output and interfacing to the Tough PCB.
Just for now, this is a really great step for the project. pbaudioV2-320kbps

OlafM2015 avatar Aug 19 '22 17:08 OlafM2015

Excellent!

I originally used the ioread mechanism because it is the uasyncio way of doing stream I/O. You prompted me to do some measurements and figure out how to maximise speed. In this instance ioread is not ideal - abandoning it will benefit all platforms.

I pushed an update last night that fixes bugs in cancellation and the sine test - these resulted from abandoning the ioread mechanism. I have now pushed a further update which provides an enable_i2s method. I have no means of testing this so I'd be grateful if you could review and test. Run with default args to replicate your 48KHz usage.

There will be further updates, but none which will affect speed - I think I've optimised .play as far as it can be taken. I also need to look at the synchronous driver. I'll document use with the Pico and merge.

A test of a VBR MP3 file was OK at stock clock speed with dreq inactive for a good proportion of the time. I attempted to play FLAC files. It almost works if I overclock to 250MHz but dreq sometimes stays True and it eventually fails. These are very demanding with a data rate of 50% of CD audio (705Kbps).

peterhinch avatar Aug 20 '22 08:08 peterhinch

Tested with player.enable_i2s(rate=48, mclock=False) Works excellent, much easier than all the _write_reg. See attached picture of the I2S clock at 48. I2S_CLK_48

OlafM2015 avatar Aug 20 '22 17:08 OlafM2015

Tried to make a version, in which a button can switch the music on and another button to switch it off (cancel). Was a bit a struggle. Used the following (not so elegant) construction. What would be your advise, to make this ? while not PlayMusic: await asyncio.sleep_ms(100) # waiting until PlayMusic == True No music with open(RandomFile, 'rb') as f: FilePlay = asyncio.create_task(player.play(f)) while not FilePlay.done() and PlayMusic: # Play Music loop until file is playied or PlayMusic is False await asyncio.sleep_ms(100) else: await player.cancel()

OlafM2015 avatar Aug 22 '22 06:08 OlafM2015

Have you seen my Switch and Pushbutton classes here? For your purposes a Switch will do. Then you might write:

from primitives import Switch
play = Switch(Pin(10, Pin.IN, Pin.PULL_UP))
play.close_func(None)  # Creates a bound Event close
can = Switch(Pin(11, Pin.IN, Pin.PULL_UP))
can.close_func(None)

async def play_loop():
   while True:
        play.close.clear()  # Ignore any pending switch closures
        await play.close.wait()  # Wait on Play switch closure
        with open(RandomFile, 'rb') as f:
            await player.play(f)

async def cancel_loop():
   while True:
        can.close.clear()
        await can.close.wait()  # Wait on Cancel switch closure
        await player.cancel()

Both these loops run forever. Obviously I haven't tested this, but I don't believe any interlocks are needed. If .cancel is run when no music is playing, nothing will happen (the driver checks for this state). Pressing "Play" while a song is playing will have no effect because the Event is cleared down after the song ends.

To keep you in the loop I'm still working on the driver with the aim of achieving FLAC file playback on the Pico. I plan to use a 1024 byte buffer. The buffer is topped up from the SD card file when dreq is False. 32 byte blocks are taken from the buffer and fed to the VS1053 when dreq is True. I believe this will deliver even faster performance.

I have pushed an update containing the current version code: there are some minor fixes and improvements you might like to grab. Ignore the pico.py test script: I was experimenting with a second SD card on the other SPI bus. It offered no benefit.

peterhinch avatar Aug 22 '22 08:08 peterhinch

The Switch works really nice, see code below. I added a while PlayMusic loop to make that the music keeps on laying and that the 5 switches can control the stop of the loop. For a proof of concept, for now, it works excellent. Thanks for the guidenance. Next step is to monitor the yield with the monitor.py tools.

async def play_sel1_loop():
   global MusicDirPointer, MusicDir, PlayMusic 
   while True:
        play_sel1.close.clear()  # Ignore any pending switch closures
        await play_sel1.close.wait()  # Wait on Play switch closure
        PlayMusic = False
        MusicDirPointer = 0
        await player.cancel()
        PlayMusic = True
        while PlayMusic and MusicDirPointer == 0:
            RandomFile = MusicDir[MusicDirPointer] + '/' + getRandomFile(MusicDir[MusicDirPointer])
            allLedsOff()
            Led_sel1.on()
            print ("Playing",RandomFile)
            with open(RandomFile, 'rb') as f:
                await player.play(f)

OlafM2015 avatar Aug 22 '22 16:08 OlafM2015

I have now merged the pico branch and pushed an update so please use the master branch. The code is little changed from what you are running. I have removed monitored versions and test images.

I abandoned the buffered .play method: it added complexity for little benefit. The problems I was experiencing with FLAC files proved to be down to an SD card which couldn't take the pace. The Pico can play FLAC files with both the synchronous and asynchronous driver.

peterhinch avatar Aug 24 '22 17:08 peterhinch

I use now the async v21053.py from the master branch. Running duration test to check stability. Runs excellent. Looking at dreq, with the LA, first impression is that it is also more efficient. Nice work. Will test FLAC files later. First, I like to finalize the proto HW design, as it takes 4-6 weeks to get it the components and produced.

OlafM2015 avatar Aug 25 '22 05:08 OlafM2015

First design of PCB: Pico + VS1053b + SD card + HW watchdog. Any recommendations/ suggestions are welcome. PCB_Design_1_0 PCB_Design_1_0_bottom PCB_Design_1_0_top

OlafM2015 avatar Aug 31 '22 06:08 OlafM2015

I haven't done any hardware design at chip level with the vs1053b so I haven't much to contribute here. A couple of general observations.

I would provide for use of micropython-monitor by bringing out to test points either a UART txd pin or three arbitrary GPIO pins (plus a gnd). That gives you the option to use monitor if the need arises.

I2S is quite sensitive to layout. I had problems using standard jumper wires to link an amplifier to I2S - these went away when I designed a PCB. I think the issue is that amplifier chips have a phase locked loop which uses the edges of the I2S clock for synchronisation, so the edges have to be quite clean. Without a schematic I can't see the location of your audio amplifier but if it's off-board you may need to take precautions.

Re the driver the changes cause problems with ESP32. These are down to a firmware bug with my use of the micropython.native decorator on a coroutine. The solution I have adopted (but not yet pushed) is to provide an option to do buffered reading. This is via an additional constructor arg. From the docs:

Optional args:

  • sdcs=None A Pin instance defined as Pin.OUT with value=1.
  • mp=None A string defining the mount point (e.g. /fc).
  • buffered=False Setting this True causes the .play method to use a 2KiB buffer. This may improve performance; it is necessary on ESP32 as a firmware bug causes the normal .play method to fail.

This means that your application will run unchanged with the default False value.

peterhinch avatar Aug 31 '22 08:08 peterhinch

"I would provide for use of micropython-monitor by bringing out to test points either a UART txd pin" Just to make sure, the Txd pin is pin 1 (GPIO 0)? Can I use also another UART0 for example pin 16 ( GPIO 12)?image

Ps. In the description of monitor.py I read that pin 2 is used for Txd? How should I interpreted this? image

OlafM2015 avatar Aug 31 '22 13:08 OlafM2015

"I2S is quite sensitive to layout. I had problems using standard jumper wires to link an amplifier to I2S "
Indeed, good point tested in HW the effect of additional capacitive load on the line, soon it began to deform. On the reference (with RPI zero2) , I checked the signal and that is much cleaner and sharper edges. It is difficult for now to judge the reason, first check the first proto. And in the layout, I will make sure the cap load is minimised.

OlafM2015 avatar Aug 31 '22 17:08 OlafM2015

I guess this reference is ambiguous. My intention is that txd on the device under test is connected to pin 2 (GPIO 1, UART0 Rx) on the Pico running the monitor firmware. The DUT may be any hardware, and you can use any UART so the doc can't specify a pin for that. In your case where the DUT is a Pico you could use pin 1, UART0 Tx. Your image suggests other pins can be specified here, in which case bring out any legal UART Tx pin.

I'll clarify the doc.

Re I2S I was surprised by its sensitivity. It's the first time I've hit trouble using standard jumper leads on a digital signal. Mike Teachman is the expert and I did some testing with him when he was developing the I2S support. His comments on wiring are here.

[EDIT] I have now pushed V0.1.5 which includes the optional buffered mode. The docs have been updated to detail the capability of the driver improvements we have implemented.

peterhinch avatar Aug 31 '22 17:08 peterhinch