gdsports / USB_Host_Library_SAMD

USB host library 2.0 for Zero/M0/SAMD
126 stars 39 forks source link

Add Sketch for MIDI Using Interrupts #15

Open studiohsoftware opened 3 years ago

studiohsoftware commented 3 years ago

Here is an example sketch that uses interrupts instead of polling to read MIDI data from a single device.

The sketch requires only one small addition to usbh_midi.h to allow the sketch to fetch the device EP address.

Tested with Korg nanoKey2 and SQ-1.

The sketch illustrates how to use interrupts and might lead to further development.

dkts2000 commented 3 years ago

Ok. Thanks. I'll try that.

dkts2000 commented 3 years ago

On second thought, maybe neither is the problem.

Stop looking for a byte greater than zero

I don't read the dataString inside the greater than zero check. At the end of the handleBank0(uint32_t epAddr) and handleBank1(uint32_t epAddr) i call a function that reads rcvd bytes from the bufBk0[i] or bufBk1[i] (depending who called it).

Then i check these bytes by chunks of 4 (and this works right until the missing bytes). The bytes i read are 04 F0 52 00 04 64 28 02 04 11 00 00 04 10 73 10 04 01 08 2E 04 00 21 00 04 0C 00 00 04 00 00 00 which at this point is what i expect

I receive them in this order: [ rcvd = uhd_byte_count0(epAddr) or uhd_byte_count1(epAddr) respectively ]

04 F0 52 00 -> 04=3 bytes following, F0=start of sysex message (1byte), 52 00 2 more bytes (rcvd=4) 04 64 28 02 -> 04=3 bytes following (rcvd=4) 04 11 00 00 -> 04=3 bytes following (rcvd=4) 04 10 73 10 -> 04=3 bytes following (rcvd=4) 04 01 08 2E -> 04=3 bytes following (rcvd=4) 04 00 21 00 -> 04=3 bytes following (rcvd=4) 04 0C 00 00 04 00 00 00 00 00 00 00 ... 00 -> 04=3 bytes following + 04=3 bytes following + 56 bytes 00 which are not part of the expected sysex (rcvd=64) and next is the missing part.

I don't know if this is easy to read. I'll try to provide the working part of the code.

and stop reading in chunks of four bytes. Just read all 64 bytes every time

if i read one byte by one, i'll end up again checking 4 bytes at a time as this is the way a sysex message works.

Any other idea? I hope you have the patience to read it! Thanks.

studiohsoftware commented 3 years ago

Each time I asked uhd_byte_count for the number of bytes it was 64, with zeros if necessary to make up the 64. I thought this was a matter of the USB pipe, and would always be the case, but perhaps it is device specific.

It looks like you are sometimes getting a byte count of 4, which is great, but sometimes you get a byte count of 64 with extra zeros to make up the 64. Maybe that happens when the device has more than 4 bytes to send.

The zero padding on the end is why you get "56 bytes 00 which are not part of the expected sysex." You have to figure out how to strip those unwanted zeros off.

dkts2000 commented 3 years ago

When i get (04 0C 00 00) (04 00 00 00) 00 00 00 00 ... 00 i know that i have 2 chunks of 4 bytes and i discard the rest zero bytes because they are not valid sysex headers. (i expect 04, 05, 06 or 07 as a valid sysex header of a chunk of 4 bytes, not zero). After that part it seems to lose 42 bytes and then starts again receiving valid sysex bytes from the expected message.

The second time it runs it also misses 42 bytes but in another location. And this happens exactly the same every time. It's not random misses.

studiohsoftware commented 3 years ago

I wonder if you are trying to transfer more than 64 bytes.

dkts2000 commented 3 years ago

You mean the device responds with a message larger than 64 bytes?

The device (a multieffects pedal atually) has only USB. If i send a specific sysex command (by using amidi .... --send-hex="....") it responds with the expected sysex message which is 134 bytes. It is also listed as a MIDI device in amidi -l So i know that it can communicate right using sysex.

Also by using this code i have received a 15 bytes sysex message as a response to a sysex command that i had send to it

studiohsoftware commented 3 years ago

Yes although I am just guessing. I would uncomment lines 159-183 (the last one is an error and needs to say println instead of print). It will show you the bank statuses and the byte counts. The handlers should scoop all of the bytes in the banks on every interrupt. You might be able to see if both banks are full and something is getting missed. I tested the case of both banks full and it should be fine (it is specifically addressed on lines 186-195.

dkts2000 commented 3 years ago

ok. I'll check. Thanks.

studiohsoftware commented 3 years ago

Note that my modification here to the library is minimal. It just adds a way for the sketch to ask for the pipe endpoint so it can set up an interrupt on it.

This example sketch leverages the library to poll the device for initialization (enumeration). The library therefore does the work of configuring the SAMD21 pipe. Once the pipe is configured, the sketch does some minimal touch up on the pipe config, but does not specify the pipe size. Once the interrupt is configured, the library is no longer used, and the sketch just depends on receiving interrupts from the SAMD21 and receiving the data directly from the pipe endpoint.

The only part of this interrupt based approach that mentions pipe size is in the buffer declarations here, which are set to 64 in the sketch: //SAMD21 datasheet pg 836. ADDR location needs to be aligned. uint8_t bufBk0[64] attribute ((aligned (4))); //Bank0 uint8_t bufBk1[64] attribute ((aligned (4))); //Bank1

I am not super clear on how the library handles Sysex, since I did not try it, but you can see in the library that usbh_midi.h has the following declarations.

define MIDI_EVENT_PACKET_SIZE 64

define MIDI_MAX_SYSEX_SIZE 256

As near as I can tell, the SAMD21 does not impose a 64 byte limit. I am not sure if this library does, but that SYSEX define is encouraging. It looks like USB full speed has a max packet size on bulk endpoints of 64 though.

You might try bumping up the buffers in the sketch to 256 and try it.

The only other thing I can think of is that there is more than one IN endpoint set up when this device enumerates, and my function Midi.GetEpAddress() is not returning the one you want. But I don't think that's true because you are getting most of the data. We seem to have the correct endpoint.

dkts2000 commented 3 years ago

I tried it with 128, 256 byte buffers but no good. The receiving bytes are always the same. Basically whatever i tried leads to exactly the same result. It somehow loses, or better it doesn't get some pieces of the message. The weird thing is that the first time it misses 42 bytes here and for any other run it misses 42 bytes in a later part. Always the same.

studiohsoftware commented 3 years ago

You can try the using one of the other examples in this library and see if it works. Maybe USBH_MIDI_dump.ino. In that case, the sketch calls Midi.RecvData to get the data from the device. That call will construct a pipe, and set it up to receive data. The pipe is constructed every time you make the call. This leads to dropped data, but it may be more robust if your device is requiring a new pipe in between transfers.

Not sure if you got the print statements working. It's a really a must to troubleshoot. For example. your device might be disconnecting and reconnecting in the middle of the Sysex message. You would see that with print statements. The interrupt sketch might have trouble in that case, not sure.

dkts2000 commented 3 years ago

I had tried the print statements and was getting the same results. But i tested something else. I stopped calling a function (that is checking the received bytes) from withing handleBank0 and handleBank1 and commented most of the debugging prints i used. I left only your prints and the result is different. It now starts getting more bytes than the previous time and it now loses 21 bytes (half of the previously missed number, if that means something). This behavior applies the first time it runs. The second time (without reset) it misses a lot of bytes. And after that it keeps cycling these 2 cases.

That means that the problem might be some kind of timing?

The print statements i get:

21:56:40.848 -> Connected
21:56:40.848 -> 0:0:0:|STATUS0:0|STATUS1:0|STATUS_BK0:0|STATUS_BK1:0|BYTECOUNT0:0|BYTECOUNT1:0|TRCPT0:0|TRCPT1:0|READY0:0|READY1:0|CURRBK:0|TOGGLE:0|TOGGLE_ERROR0:0|TOGGLE_ERROR1:0|NAK:0|INTSUMMARY:0|
21:56:44.300 -> Pipe Started
21:56:44.300 -> Dump:ADDR0:200004F4:ADDR1:20000534:C:4:0
21:56:53.472 -> 8:5:55:|STATUS0:0|STATUS1:0|STATUS_BK0:0|STATUS_BK1:0|BYTECOUNT0:4|BYTECOUNT1:0|TRCPT0:1|TRCPT1:0|READY0:1|READY1:0|CURRBK:1|TOGGLE:1|TOGGLE_ERROR0:0|TOGGLE_ERROR1:0|NAK:0|INTSUMMARY:10|
21:56:53.506 -> 4f0520|
21:56:53.506 -> 8:6:90:|STATUS0:0|STATUS1:0|STATUS_BK0:0|STATUS_BK1:0|BYTECOUNT0:4|BYTECOUNT1:4|TRCPT0:0|TRCPT1:1|READY0:0|READY1:1|CURRBK:0|TOGGLE:0|TOGGLE_ERROR0:0|TOGGLE_ERROR1:0|NAK:0|INTSUMMARY:10|
21:56:53.506 -> 464282|
21:56:53.506 -> 8:5:55:|STATUS0:0|STATUS1:0|STATUS_BK0:0|STATUS_BK1:0|BYTECOUNT0:4|BYTECOUNT1:4|TRCPT0:1|TRCPT1:0|READY0:1|READY1:0|CURRBK:1|TOGGLE:1|TOGGLE_ERROR0:0|TOGGLE_ERROR1:0|NAK:0|INTSUMMARY:10|
21:56:53.637 -> 41100|
21:56:53.637 -> 8:6:90:|STATUS0:0|STATUS1:0|STATUS_BK0:0|STATUS_BK1:0|BYTECOUNT0:4|BYTECOUNT1:4|TRCPT0:0|TRCPT1:1|READY0:0|READY1:1|CURRBK:0|TOGGLE:0|TOGGLE_ERROR0:0|TOGGLE_ERROR1:0|NAK:0|INTSUMMARY:10|
21:56:53.637 -> 4107310|
21:56:53.637 -> 8:5:55:|STATUS0:0|STATUS1:0|STATUS_BK0:0|STATUS_BK1:0|BYTECOUNT0:4|BYTECOUNT1:4|TRCPT0:1|TRCPT1:0|READY0:1|READY1:0|CURRBK:1|TOGGLE:1|TOGGLE_ERROR0:0|TOGGLE_ERROR1:0|NAK:0|INTSUMMARY:10|
21:56:53.637 -> 4182e|
21:56:53.637 -> 8:6:90:|STATUS0:0|STATUS1:0|STATUS_BK0:0|STATUS_BK1:0|BYTECOUNT0:4|BYTECOUNT1:4|TRCPT0:0|TRCPT1:1|READY0:0|READY1:1|CURRBK:0|TOGGLE:0|TOGGLE_ERROR0:0|TOGGLE_ERROR1:0|NAK:0|INTSUMMARY:10|
21:56:53.637 -> 40210|
21:56:53.637 -> 8:5:55:|STATUS0:0|STATUS1:0|STATUS_BK0:0|STATUS_BK1:0|BYTECOUNT0:40|BYTECOUNT1:4|TRCPT0:1|TRCPT1:0|READY0:1|READY1:0|CURRBK:1|TOGGLE:1|TOGGLE_ERROR0:0|TOGGLE_ERROR1:0|NAK:0|INTSUMMARY:10|
21:56:53.637 -> 4c00|4000|4001|4402|4c468|41210|42c00|4000|4000|40021|40052|47280|40640|446100|4000|4000|
21:56:53.637 -> 8:6:90:|STATUS0:0|STATUS1:0|STATUS_BK0:0|STATUS_BK1:0|BYTECOUNT0:40|BYTECOUNT1:40|TRCPT0:0|TRCPT1:1|READY0:0|READY1:1|CURRBK:0|TOGGLE:0|TOGGLE_ERROR0:0|TOGGLE_ERROR1:0|NAK:0|INTSUMMARY:10|
21:56:53.637 -> 4010|4000|
21:56:53.637 -> 8:5:55:|STATUS0:0|STATUS1:0|STATUS_BK0:0|STATUS_BK1:0|BYTECOUNT0:1C|BYTECOUNT1:40|TRCPT0:1|TRCPT1:0|READY0:1|READY1:0|CURRBK:1|TOGGLE:1|TOGGLE_ERROR0:0|TOGGLE_ERROR1:0|NAK:0|INTSUMMARY:10|
21:56:53.637 -> 40701|4000|404475|465720|46d6574|4652020|60f70|
22:02:13.742 -> 208:C:15:|STATUS0:28|STATUS1:0|STATUS_BK0:0|STATUS_BK1:0|BYTECOUNT0:1C|BYTECOUNT1:40|TRCPT0:0|TRCPT1:0|READY0:0|READY1:0|CURRBK:1|TOGGLE:1|TOGGLE_ERROR0:0|TOGGLE_ERROR1:0|NAK:0|INTSUMMARY:0|
22:02:13.742 -> 200:C:15:|STATUS0:28|STATUS1:0|STATUS_BK0:0|STATUS_BK1:0|BYTECOUNT0:1C|BYTECOUNT1:40|TRCPT0:0|TRCPT1:0|READY0:0|READY1:0|CURRBK:1|TOGGLE:1|TOGGLE_ERROR0:0|TOGGLE_ERROR1:0|NAK:0|INTSUMMARY:0|
22:02:13.742 -> Disconnected
22:02:13.742 -> 0:C:15:|STATUS0:28|STATUS1:0|STATUS_BK0:0|STATUS_BK1:0|BYTECOUNT0:1C|BYTECOUNT1:40|TRCPT0:0|TRCPT1:0|READY0:0|READY1:0|CURRBK:1|TOGGLE:1|TOGGLE_ERROR0:0|TOGGLE_ERROR1:0|NAK:0|INTSUMMARY:0|
studiohsoftware commented 3 years ago

So the last two lines there before the disconnect are where the data is dropped? What caused that disconnect?

But yes if you are doing less work now within the handler, and as a result you are getting more data, it suggests that the device is trying to send more data than the SAMD21 can receive. Perhaps while you are processing the data, the device overflows or gives up.

I just noticed as well that almost immediately you see that both banks always have data. I would consider this a bad sign. I think it means the device is filling up the banks faster than you can drain them. A healthy sign would be one of the banks empty every time you handle the interrupt. It would mean that the handling is not having trouble keeping up. Suddenly not so sure about this theory though because TRCPT flags are not both 1. It's been a while since I have looked at the debug output during my successful runs. I thought that the byte count on one of the banks was always zero.

Notice this option in the sketch. USB->HOST.HostPipe[epAddr].BINTERVAL.reg = 0x01;//Zero here caused bus resets.

This is the interval that the SAMD21 polls the device for data. Counter intuitive perhaps, but maybe an increase would improve reliability. Perhaps telling the device to slow down a little (by polling it less frequently) you will give your sketch more time to process the data from the banks.

For bulk type, the datasheet pg 824 says this parameter means 1 ms to 255 ms.

dkts2000 commented 3 years ago

I got it working by adding

        usb_pipe_table[epAddr].HostDescBank[0].PCKSIZE.bit.SIZE     = USB_PCKSIZE_SIZE_256_BYTES;
        usb_pipe_table[epAddr].HostDescBank[1].PCKSIZE.bit.SIZE     = USB_PCKSIZE_SIZE_256_BYTES; 

instead of USB_PCKSIZE_SIZE_64_BYTES;

in the if (doPipeConfig) { section of loop. I saw it in void pipeConfig(uint32_t addr, uint32_t epAddr) { you had commented out. I don't fully understand what is going on during pipe configuration but this seems to solve the problem. Now i get all 134 bytes every time. It means the pipe is "larger" so all bytes are now waiting to be received and they do not flood?

By the way thanks for your interest, it kept me trying.

studiohsoftware commented 3 years ago

The pipeConfig function was pieced together from the calls I found within the library that set up the pipe. At first I thought I needed to have it to set up the pipe because I was going to abandon the library, but later I realized that I could use the library to enumerate the USB device and set up the pipe. So I no longer needed pipeConfig, but I left it commented out, because it contained a lot of interesting methods that I thought someone might be able to use. So your case is exactly what I had in mind. The commented out code is a kind of reserve toolbox that you can use to try things.

I am still a little confused about what is happening. I thought that the bulk endpoints had to be 64. Maybe I am confusing pipe size and endpoint size.

Anyway, congrats. Thanks for picking up this code and taking it further.

MickGyver commented 3 years ago

Anyway, congrats. Thanks for picking up this code and taking it further.

Thanks for trying to figure this one out! Did you ever try this out "for real"? I'm having problems with missed events if I press more than one key at the same time, seems like this code can reliably only handle one midi event at a time. I'm experimenting with an AKAI MPK Mini.

studiohsoftware commented 3 years ago

USB is serial, and the events will be transferred in series, not parallel. It is not possible to have more than one event at a time communicated over USB. Of course it is possible to press more than one key at a time, but the keyboard must be capable of detecting that situation and submitting individual NOTE ON events sequentially to the USB interface.

MickGyver commented 3 years ago

USB is serial, and the events will be transferred in series, not parallel. It is not possible to have more than one event at a time communicated over USB. Of course it is possible to press more than one key at a time, but the keyboard must be capable of detecting that situation and submitting individual NOTE ON events sequentially to the USB interface.

Thank you for the reply! Sorry, I didn't properly explain the problem. What I meant with the code only being able to handle one MIDI event at a time reliably was that as soon as I press or release two or more keys at the same time there is a big chance that some note on/off messages are missed and I can't really figure out why (same problem as with the non-interrupt based examples I have seen). The keyboard (AKAI MPK Mini) works perfectly when connected to a computer.

studiohsoftware commented 3 years ago

I have used pipe interrupts for fast MIDI clock and polyphony without any message loss. My guess is that the problem is elsewhere. You might try the same test with a different host to check.

MickGyver commented 3 years ago

I have used pipe interrupts for fast MIDI clock and polyphony without any message loss. My guess is that the problem is elsewhere. You might try the same test with a different host to check.

Thanks, yes I will do some further investigation. I have this problem with the interrupt example in your fork (and this pull request) so I was wondering if you have experienced a similar issue.

studiohsoftware commented 3 years ago

No, I have not seen any message loss problems using the interrupts approach. It's seems to be very reliable.

MickGyver commented 3 years ago

My guess is that the problem is elsewhere.

Thanks for your replies and yes you were right, the problem was on the receiving end. Your interrupt based code seems to work great, thanks a bunch for sharing it! :)