文号
830025
阅读
37512
回复
2
收藏
4
基于STM32的USB MIDI协议栈设计
2017/02/02
IP:山东

Q:为什么要使用USB MIDI?

A:传统MIDI接口使用异步串行方式通信,波特率31.25kbps,而一般一条指令的长度为3字节,所以换算出来最快1ms传输一条指令,而USB MIDI(工作在Full Speed模式)在每1ms的SOF帧后可搭载n个数据包,所以带来的演奏延迟是远小于传统接口。

Q:USB MIDI与传统MIDI协议区别大么?

A:USB MIDI采用4字节对齐的事件帧作为最小传输单元,第1字节为Cable Num+CIN,第2~4字节为原MIDI协议内容,对于长度大于3字节的MIDI数据被拆分为若干个事件,而小于3字节的后面以0补齐。


附件附USB MIDI 1.0协议原本和源码(MDK v5.14工程),源码在STM32F103C8T6核心板上调试通过,模拟了一个USB转传统MIDI接口的适配器,使用USART1作为传统MIDI接口。


attachment icon midi10.pdf 175.85KB PDF 1130次下载 预览
attachment icon USB-MIDI.zip 357.11KB ZIP 937次下载

USB部分没有太多要讲的,毕竟都是用例程改的(这里是拿ST官方的Joystick例程来修改),修改好设备描述符和端点配置就可以了。例程有休眠唤醒支持,实际调试时发现有问题,遂注释掉相关代码,还没有测试实际在主机休眠后再唤醒USB是否能继续正常工作。


这里来分析下USB MIDI事件与MIDI指令互相转换的代码:

static void uart_send(uint8_t *buf, uint8_t len)
{
	uint8_t i;
	
	USART_ClearFlag(USART1, USART_FLAG_TC);
	for(i = 0; i < len; i ++){
		USART_SendData(USART1, buf[i]);
		while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
	}
}

static void usb_send(uint8_t *buf, uint8_t len)
{
	while(!USB_TC);
	USB_TC = 0;
	USB_SIL_Write(ENDP1, buf, len);
	SetEPTxValid(ENDP1);
}

int main(void)
{
	uint8_t i, m, buff[4];
	
	Set_System();
	USB_Interrupts_Config();
	Set_USBClock();
	USB_Init();

	while(1){
		if(bDeviceState == CONFIGURED){
			/*****  UART->USB Routine  ***/
			if(MIDI_SendLen){
				if(MIDI_SendBuff[0] == 0xF0 && 
						MIDI_SendBuff[MIDI_SendLen - 1] == 0xF7){ /* SysEx */
					m = (MIDI_SendLen - 1) / 3;
					for(i = 0; i <= 1 2 3 m; i ++){ if(i="=" m) switch(midi_sendlen % 3){ case 0: * sysex ends with following bytes buff[0]="0x07;" break; 1: 2: } else starts or continues buff[1]="MIDI_SendBuff[i" 3]; buff[2]="MIDI_SendBuff[1" + buff[3]="MIDI_SendBuff[2" usb_send(buff, 4); for(i="0;" < midi_sendlen; ++) midi_sendbuff[i]="0;" }else general switch(midi_sendbuff[0] & 0xf0){ 0xc0: program change 0xd0: channel if(midi_sendlen>= 2){
								buff[0] = MIDI_SendBuff[0] >> 4;
								buff[1] = MIDI_SendBuff[0];
								buff[2] = MIDI_SendBuff[1];
								buff[3] = 0;
								MIDI_SendLen = 0;
								usb_send(buff, 4);
							}
							break;
							
						case 0x80: /* Note-off */
						case 0x90: /* Note-on */
						case 0xA0: /* Poly-KeyPress */
						case 0xB0: /* Control Change */
						case 0xE0: /* PitchBend Change */
							if(MIDI_SendLen >= 3){
								buff[0] = MIDI_SendBuff[0] >> 4;
								buff[1] = MIDI_SendBuff[0];
								buff[2] = MIDI_SendBuff[1];
								buff[3] = MIDI_SendBuff[2];
								MIDI_SendLen = 0;
								usb_send(buff, 4);
							}
							break;
							
						default: MIDI_SendLen = 0;
					}
			}
			/***  USB->UART Routine  ***/
			if(RecvBufNE){
				i = 0;
				do
					switch(MIDI_RecvBuff[i]){
						case 0x05:
						case 0x0F:
							uart_send(&MIDI_RecvBuff[i + 1], 1);
							i += 2;
							break;
						
						case 0x06:
						case 0x0C:
						case 0x0D:
							uart_send(&MIDI_RecvBuff[i + 1], 2);
							i += 3;
							break;
						
						case 0x04:
						case 0x07:
						case 0x08:
						case 0x09:
						case 0x0A:
						case 0x0B:
						case 0x0E:
							uart_send(&MIDI_RecvBuff[i + 1], 3);
							i += 4;
							break;
						
						default: i ++; /* ignore 1 byte */
					}
				while(i < MIDI_RecvLen);
				RecvBufNE = 0;
			}
		}
	}
}
</=>

比较麻烦的地方是SysEx要与其他指令分开对待,因为SysEx的长度是不确定的,以F0开始以F7结束,而USB MIDI对SysEx进行了拆分,一帧SysEx会被拆分为多个事件帧,并且前面的若干帧和最后一帧通过CIN区分,最后上位机软件根据CIN来把被拆分的SysEx进行组装,还原回原SysEx帧。具体CIN的定义可以参考USB MIDI V1.0协议第4部分。剩下的0x8~0xE的指令就比较好对待啦,长度都是固定的,CIN和指令也都是一一对应的,按协议定义来解析即可~~~

9年10个月前IP:陕西
830028
1楼
Nice work.Thanks a lot for your efforts about MIDI Controller. It's much useful for the smart musical TC project。
引用
评论
笔记
{{note.content}}
{{n.user.username}}
{{fromNow(n.toc)}} {{n.status === noteStatus.disabled ? "已屏蔽" : ""}} {{n.status === noteStatus.unknown ? "正在审核" : ""}} {{n.status === noteStatus.deleted ? '已删除' : ''}}
  • 编辑
  • 删除
  • {{n.status === 'disabled' ? "解除屏蔽" : "屏蔽" }}
我也是有底线的