stm32 usb转串口的程序设计解析

2023-08-16  

  串口调试在项目中被使用越来越多,串口资源的紧缺也变的尤为突出。很多本本人群,更是深有体会,不准备一个USB转串口工具就没办法进行开发。


  了解USB虚拟串口,为了在项目中用一下这个USB,调试方便一些,供电可直供。公司以后的产品开发就基于STM32这个平台,从contex_M3到contex-M4。不管速度、功耗、价格、采购的方便性都有竞争力,不想再修改了(除非它无法满足要求)。


  STM32基本上都带有USB串口。如果不把它用上而另外加一个USB转串口单元,未免显得太落后了而且也是一种资源的浪费。顺便说一下,根据公司的状况,合适的才是好的。以前一直用TI的,从430到ARM到28XX。这个ST的包含了以前的所有,TI的ARM速度比较慢,而它的普通的DSP的速度与M4相当,有些还比不过M4,耗电却惊人,何况M4带了浮点后比定点在某些地方要快很多。难怪TI在推M4时故意将频率放慢,DSP功能减少,我想各个公司都有自已的考虑吧。而ST就把CMOS传感器接口也放进去。赶明儿啥时有空了再做块PCB试试。ST最让人不爽的是它的开发例程做的太浅,例如超速USB2。0,CMOS接口资料几乎没有,TCP/IP也浅尝即止,网页也设计得世界上最烂,让人找个芯片,找个资料很累。在应用上比NXP差远了,毕竟有个ZLG帮忙,FREESCALE在网络通讯方面的应用做得最好,看他们的TCP/IP源代码是一种享受。看来做芯片是ST的强项,做应用还非常有待加强。

  说明:

  1.跳过驱动。使用低速传输。最大可以设为921000即100KB/S。应该也差不多了。只传一些简单的东西。因为我们工作的重点是工业控制,无需高速,而我们是以完成任务为主要目的。而不是非要学什么东西,完成任务才是第一重要的。

  2.由于例程中没有操作系统支持,也许以后可以用keil的操作系统。比较简单,最重要的是ucOS不支持M4的浮点运算。当然对于M3我们可以将USB部分移到ucOS上。而对于M4,我们直接用KeilOS,不想花很大力气去做OS移植了。

  总是先从main开始

  Set_System();///设置系统

  Set_USBClock();///设置USB时钟

  USB_Interrupts_Config();///配置USB中断

  USB_Init();///USB初始化

  while(1)

  {

  if((count_out!=0)&&(bDeviceState==CONFIGURED))

  {

  USB_To_USART_Send_Data(&buffer_out[0],count_out);//如果有数据将它发送到串口中去

  count_out=0;/////发送完后这个清零

  }

  }

  初看一下,还算比较好理解,但是由于这个例子好象只有发,没有收。我想它的收大约在中断中进行的(也就是串口向USB发的过程,估计在串口中断中进行,后面我们可以再分析,不行可能需要自行加上这部分代码,希望不要这样)

  我们还是一条一条的来看,首先看Set_System()这个函数,如果没猜错的话,应该是设置时钟吧。

  果然如此,我们下面一条一条看一下,它先是允许外部晶振---这里哆嗦一下,外部晶振我公司采用12M。而一般开发版采用8M。所以配置stm32f10x_conf.h文件中,要将外部晶振频率从8000000改为12000000。

  然后,等外部晶振起来,如果晶振没焊接好,此时就会死在这里。如果你一运行就死,可找一下这个地方。

  然后它做下列工作:

  允许FLASH取指缓冲

  FLASH时钟分频2倍-------这是否意味着FLASH的时钟是36M呢,记得好象有ST的文章中说FLASH可工作在50M的时钟下。

  系统频率HCLK配置成SYSCLK

  APB2的时钟配置成SYSCLK不分频

  APB1的时钟配置成2分频。但要注意它下面的定时器2,3,4。。频率仍是72M因有倍频

  ADC的时钟配置成6分频,即它是12MHz。注意AD需13.5个时钟完成意味着差不多1us可完成一次AD转换。

  PLL配置成9倍频。8X9=72MHz。注意到对于12MHz就只能是6了。此处一定要注意。

  允许PLL然后等PLL都OK了再做别的。--此后由PLL进行工作而不是HSI。

  然后,我们要允许GPIOA,GPIOB和串口的时钟。因为我们只用到了这几个资源。当然USB是另外的,我想总会在某个地方允许的。下面这个就不好理解了:

  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIO_DISCONNECT,ENABLE);这个叫允许USB断开线。从原理图上看好象是PE7来控制的。因为PE7连接到一个DP+的一个上拉电阻控制的三极管上面。请参考原理图。但是比较奇怪的是在platform_config.h中有下列定义:

  #defineRCC_APB2Periph_GPIO_DISCONNECTRCC_APB2Periph_GPIOD

  它将这个设为端口D。所以这个USB断开引脚到底是由谁来控制的还未确定。这里留下一个问号,等以后再解决。

  接下来,将USB的断开脚配置成上拉的。这也就意味着在开始时,这个上拉电阻使得三极管导通,从而使这个DP脚被加了一个1.5K的电阻,可以开始枚举的。再看这个断开脚指的是哪一个脚,它不是PD9就是PB14。怎么也没有PE7的说法(原理图)。所以这里就有点不知所以然了。难道这个原理图与程序有冲突?

  接下来配置PA10为输入浮空,配置PA9为PP输出。这两个脚是串口。这个是正确的。我们几孚所有的板子都用UART0,它就是这两个脚的。至此这个部分结束了。


  下面再来看第2个函数Set_USBClock()-------两句话:

  RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_1Div5);/*EnableUSBclock*/

  RCC_APB1PeriphClockCmd(RCC_APB1Periph_USB,ENABLE);72M除1。5=48然后允许USB的时钟。这个要看一下数据手册。从数据手册比较好理解。它是直接从PCLK中除一个数,可以除1也可以除1.5.。这段还是比较好理解的。

  再看第3个函数USB_Interrupts_Config()-------配置USB中断

  在该函数中使能了两个中断,一个是USB,一个是串口。至于中断放在RAM或FLASH当然一般是后者,所以在这也就没有什么意义了。串口优先级为1,USB为0。中断分组中我们将一位作为可重入的优先组,另3位作为优先级组。由于一个为0一个为1,故这两个中断都不会相互被另一个中断打断。

  现在既然允许了两个中断,估计在中断中将做很多事情,大多数事情都在中断中完成的。这个中断程序等一会马上就来解读。最后是USB_Init()这个函数了。在这是里先看到:

  pInformation=&Device_Info;这个是设备资料部分,里面的全部变量,静态的。pInformation-》ControlState=2;

  pInformation是一个指向Device_Info的指针。不知为什么要这样大费周折。不可以直接这样写吗:

  Device_Info.ControlState=2

  接下来:

  pProperty=&Device_Property;

  pUser_Standard_Requests=&User_Standard_Requests;pProperty是一个指针,指向DEVICE_PROP这个是设备属性部分

  设备属性部分包含一些方法,即函数。也包括两个参数,一个是接收区的缓冲区地址,一个是最大的包的长度。都是用字节表示的。

  这个属性是一个通用的属性,它指以相当于实例化套到USB这个头上去。故我们在这里看上去运行一个pProperty-》Init();实际上运行的是DEVICE_PROP中的Virtual_Com_Port_init()这个函数。我们看在这个函数中做了什么:

  Get_SerialNum();设置芯片序列号,将描述符中的例如STM等字符串修改没太大意义。

  PowerOn(void)先使能芯片(三极管B极变高)强迫USB复位。再将全部USB中断都屏蔽后,将中断清除,最后再允许以下中断:CNTR_RESETM|CNTR_SUSPM|CNTR_WKUPM;

  接下来,再一次清除中断标志,然后使能中断(CNTR_CTRM|CNTR_SOFM|CNTR_RESETM)这几个中断就是复位中断,正确传输中断,SOF中断

  配置串口至缺省状态---在这里波特率被设为9600,并且允许了接收中断。发送中断没有允许。

  将当前的状态定义为未连接状态。bDeviceState=UNCONNECTED;什么时候连接不知道。

  至此,初始化结束。我们现在要看的是中断函数了。中断函数不外于一个是串口的接收中断。串口的发送中断是没有允许的。

  串口是如何发送的呢?它直接写串口寄存器,显然如果有大量数据发送时就会出问题的。因为它根本不判断是否发送缓冲区为空。因此,感觉这个程序要进行大量的数据交互不可能,最多是敲打一下键盘可能还差不多。

  看它的库中的函数:直接发送,也不管是否空。难道它有16字节缓冲吗?没有。voidUSART_SendData(USART_TypeDef*USARTx,u16Data){

  /*TransmitData*/

  USARTx-》DR=(Data&(u16)0x01FF);}

  先从简单的看起,串口的接收中断,它在哪里呢?发现它在stm32f10x_it.c中有如下:

  USART_To_USB_Send_Data();表示从串口向USB端发送数据。

  再看这个定义如下:

  voidUSART_To_USB_Send_Data(void){

  if(USART_InitStructure.USART_WordLength==USART_WordLength_8b){

  buffer_in[count_in]=USART_ReceiveData(USART1)&0x7F;}

  elseif(USART_InitStructure.USART_WordLength==USART_WordLength_9b){

  buffer_in[count_in]=USART_ReceiveData(USART1);}

  count_in++;

  UserToPMABufferCopy(buffer_in,ENDP1_TXADDR,count_in);SetEPTxCount(ENDP1,count_in);SetEPTxValid(ENDP1);}

  输入的数据长度++即count_in++

  将收到的数据拷贝到端口1的发送缓冲区中。设置发送缓冲区的长度

  发送数据,它是从ENDP1发送。

  我相信,发送完后,这个count_in会被清零。

  果然,在EP1_IN_Callback()函数中,它被清零。

  最后,我们就来看一下USB的中断。USB的中断入口有一个还是多个?它有一个高优先级中断和一个低优先级中断。应该只用其中一个。

  看程序中有下列:

  voidUSB_LP_CAN_RX0_IRQHandler(void){

  USB_Istr();}

  这个说明,在程序中将USB设为相对低的优先级中断。回想起我们好象在什么地方这样设过?果然,在USB_Interrupts_Config()中,有这么一段:

  NVIC_InitStructure.NVIC_IRQChannel=USB_LP_CAN_RX0_IRQChannel;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;NVIC_InitStructure.NVIC_IRQChannelSubPriority=0;NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_Init(&NVIC_InitStructure);

  所以所有的中断都是进到这个中断中去了。赶紧看一看,这个是怎么处理的?它被发现在usb_istr.c中。我想所有的最麻烦的部分就是比较精彩的部分应该就在这里了(对初学者)下面我们就来分析这个部分,分析完之后就可以回去了,做完今天的工作。因为精彩,所以拷贝在下面了:

  voidUSB_Istr(void){

  wIstr=_GetISTR();

  #if(IMR_MSK&ISTR_RESET)

  if(wIstr&ISTR_RESET&wInterrupt_Mask){

  _SetISTR((u16)CLR_RESET);Device_Property.Reset();#ifdefRESET_CALLBACK

  RESET_Callback();#endif}#endif

  /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/

  #if(IMR_MSK&ISTR_DOVR)

  if(wIstr&ISTR_DOVR&wInterrupt_Mask){

  _SetISTR((u16)CLR_DOVR);#ifdefDOVR_CALLBACK

  DOVR_Callback();#endif}#endif

  /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/

  #if(IMR_MSK&ISTR_ERR)

  if(wIstr&ISTR_ERR&wInterrupt_Mask){

  SetISTR((u16)CLR_ERR);#ifdefERR_CALLBACK

  ERR_Callback();#endif}#endif

  /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/#if(IMR_MSK&ISTR_WKUP)

  if(wIstr&ISTR_WKUP&wInterrupt_Mask){

  _SetISTR((u16)CLR_WKUP);Resume(RESUME_EXTERNAL);#ifdefWKUP_CALLBACK

  WKUP_Callback();#endif}#endif

  /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/#if(IMR_MSK&ISTR_SUSP)

  if(wIstr&ISTR_SUSP&wInterrupt_Mask){

  /*checkifSUSPENDispossible*/if(fSuspendEnabled){

  Suspend();}else{

  /*ifnotpossiblethenresumeafterxxms*/Resume(RESUME_LATER);}

  /*clearoftheISTRbitmustbedoneaftersettingofCNTR_FSUSP*/_SetISTR((u16)CLR_SUSP);#ifdefSUSP_CALLBACK

  SUSP_Callback();#endif}#endif

  /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/#if(IMR_MSK&ISTR_SOF)

  if(wIstr&ISTR_SOF&wInterrupt_Mask){

  _SetISTR((u16)CLR_SOF);

  bIntPackSOF++;

  #ifdefSOF_CALLBACK

  SOF_Callback();#endif}#endif

  /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/#if(IMR_MSK&ISTR_ESOF)

  if(wIstr&ISTR_ESOF&wInterrupt_Mask){

  _SetISTR((u16)CLR_ESOF);

  /*resumehandlingtimingismadewithESOFs*/

  Resume(RESUME_ESOF);/*requestwithoutchangeofthemachinestate*/#ifdefESOF_CALLBACK

  ESOF_Callback();#endif}#endif

  /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/#if(IMR_MSK&ISTR_CTR)

  if(wIstr&ISTR_CTR&wInterrupt_Mask){

  /*servicingoftheendpointcorrecttransferinterrupt*//*clearoftheCTRflagintothesub*/CTR_LP();

  #ifdefCTR_CALLBACK

  CTR_Callback();#endif}#endif

  }/*USB_Istr*/


  下面象城管一样,逐一分拆每户。

  wIstr=_GetISTR();得到中断的原因,这个根本不是函数,而是得到ISTR的值。

  由于我们没有外挂复位,故外挂的复位就不进行了。我们只处理这个:voidVirtual_Com_Port_Reset(void),这个函数在usb_prop.c这个文件中。它的目的是恢复上电时的缺省设置。这个我们就不去深究了。因为好象也没有必要。

  首先将全局变量pInformation(它定义在初始化中usb_init.c)中的配置值置为0表示设备还没配置过。(这个变量猜想应该在枚举之类的地方用于判断是否已枚举过)其次将当前的特征值赋值Virtual_Com_Port_ConfigDescriptor[7]。(含义先不管它)然后再将当前的通讯口设为端口0即pInformation-》Current_Interface=0;端口0大约就是控制口吧。

  接下来设置缓冲表的地址或寄存器为00。这个我们暂不管它含义是什么放一边去。

  再接下来,初始化三个端口,它们是端口0,1和2。其中端口0是控制口,端口1是发送口,端口3是接收口。

  在.h中定义了缓冲区表的基地址为00,而端口0收为0x40,端口0的发为0x80,端口1的发地址为0xC0.端口2的发地址为0x100.端3的收地址为0x110.可以看到其缓冲区端口2的为16字节,其它的都为64字节。感觉ST公司太节省了点吧。这么短的包,如果用480M的超速的话怎么够?

  最后一句:bDeviceState=ATTACHED;表示USB进入一个新状态。

  接下来,看中断是否响应DMA上溢下溢,错误处理,唤醒、挂起中断我们都没有用到,故全部不看它。现在主要要看的一个终于出现了,它就是我们三个要响应的中断之一(三个中断分别是复位中断,帧头SOF中断,正确收发中断)SOF中断。这个表示帧的起始中断。不过这个中断的处理却是异常的简单,就是将这个SOF标志清除后再bIntPackSOF++;即可

  当然最重要的总是在总后的,接下来的一个中断可要费点周章了。它就是正确的收发到数据的中断,它调用了CTR_LP()函数,这个函数它定义在usb_int.c中。我们重点解读它:这个程序名为低优先级正确接收中断

  ★首先这个中断是一个循环,它一直在等ISTR_CTR==0为止。因为当它等于0时,表示里面的数据已经全部取完。没取完它是不会罢休的。就象强盗进了金库要将它搬空为止,结果是阿里巴巴胜出一样。

  ★清除这个CTR标志位,为了这个,我们去看一下数据手册,庆幸是中文的看得快一点(如果老是这么想,也许是不幸的开始)。发现CTR标志位只是一个只读的位,要清除它,它能是去清除USB_EpnR中的对应位。所以为什么用一个while()的原因是中断一个处理完后,可能还有其它的中断未处理完,如果是这样的话,这个CTR位就一直是高电平。可是程序中却将ISTR的CTR位清除(在数据手册中它被说明为只读位)难道这是ST的一个小失误?别人不信,反正我是信了。

  ★根据端点的ID号(ISTR寄存器)决定它是控制端点0的响应呢还是其它端点的响应。原来控制端点0的响应在这里,估计枚举就在这里进行的吧。不过枚举过程我暂不想看,因为我相信ST会把所有的过程都给搞定的。读程序时,如果过分的分支再分支,最后就一无所有,有时要反复4~5遍才知道,如此就只好先舍去一些确定性的,象两平行线一定不相交的证明就不要看了。先看与要达到的目的密切相关的才行。如果要看枚举过程,“圈圈的教我玩USB”写得非常好,完全是由浅入深,建议买这本书看一看(赞一个,尽管不很深入,谈到教书育人,比清华的教授要强得多了,某些教授就是只会骗国家经费,找学生做事,大学搞出来的科研成果99%没有价值)。

  ★重点看其它端点的响应中断由于前面我们已经得知了ID号,我们就到对应的端点寄存器中去找,即臂如是端点2的响应,我们就到端点2的USB_EP2R中去找。看它是发送中断还是接收中断。它是B15位就是它的CTR_RX,如果不等于0说明它就是该端点的接收中断。

  ★接收中断处理的过程:

  _ClearEP_CTR_RX(EPindex);///清除这个接收标志

  (*pEpInt_OUT[EPindex-1])();///调用相应的接收中断的处理函数

  注意这个函数数组的用法。它的定义如下:void(*pEpInt_OUT[7])(void)={

  EP1_OUT_Callback,EP2_OUT_Callback,EP3_OUT_Callback,。。。

  };回忆一下,大学C语言学过的函数的定义:void*function(void)学的时候没用功吧。其实,要是我来做的话,还不如用几个if语句来得简明。

  ★所幸,我们在这里只用到两个回调函数,只需看2个即可,一个是EP1_IN_Callback()另一个是EP3_OUT_Callback()

  而EP1这个,只是执行这么简单的一句:count_in=0;这个是当串口向USB发时时,串口的数据,在串口中断中已经做了处理。它就是我们前面看过的:

  buffer_in[count_in]=USART_ReceiveData(USART1);count_in++;

  UserToPMABufferCopy(buffer_in,ENDP1_TXADDR,count_in);SetEPTxCount(ENDP1,count_in);SetEPTxValid(ENDP1);

  如果串口上我们连一个键盘,当敲打它时,就产生了串口中断,这个中断中将数据接收好,然后拷到缓冲区中(这是一个64字节的缓冲区)然后只需设置EP1的长度,开始发送就可以了。注意到这个发送字节的个数的寄存器在缓冲区中的某个地方。它在[USB_BTABLE]+n×16+4处。这点还请参考数据手册。

  SetEPTxValid(ENDP1)这个函数还有点烦。要是将它全面解剖开来,就有下列东东:(为简化起见,中间的一些变量我用实际的数表表示了)#define_SetEPTxStatus(1,0x0030){////我们这里是将两位都置为11

  registeru16_wRegVal;

  _wRegVal=_GetENDPOINT(1)&EPTX_DTOGMASK;///这个数就是0x0030/*togglefirstbit?*/if((EPTX_DTOG1&wState)!=0)///如果原来的值不为0_wRegVal^=EPTX_DTOG1;

文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。