据说USB虚拟串口的最高速度可以达到40MB/s.
于是兴奋的买了块微雪的开发板. 单片机是STM32F407IGT6, USB采用高速USB3300芯片, (于是就掉坑里了. 差点没爬上来, 爬了2个多月的业余时间. 目前基本上算是爬上来了.)
这个USB通信有三个大问题.
第一个问题是: 电脑无法识别USB虚拟出来的串口. 时不时的掉线.不识别偶尔又能识别.
第二个问题是: 在调用CDC_Transmit_HS这个函数的时候,总是会卡在return USBD_BUSY ;代码在下面
第三个问题是: 如果快速的重复调用CDC_Transmit_HS 还是会卡死…
uint8_t CDC_Transmit_HS(uint8_t* Buf, uint16_t Len)
{
uint8_t result = USBD_OK;
/* USER CODE BEGIN 12 */
USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceHS.pClassData;
if (hcdc->TxState != 0){
return USBD_BUSY; //总是会卡在这里
}
//解决发 字节是64整数倍的bug
if((Len % 64) ==0)
{
USBD_CDC_SetTxBuffer(&hUsbDeviceHS, Buf, Len);
result = USBD_CDC_TransmitPacket(&hUsbDeviceHS);
while(hcdc->TxState !=0);
USBD_CDC_SetTxBuffer(&hUsbDeviceHS, Buf, 0);
result = USBD_CDC_TransmitPacket(&hUsbDeviceHS);
}
else
{
USBD_CDC_SetTxBuffer(&hUsbDeviceHS, Buf, Len);
result = USBD_CDC_TransmitPacket(&hUsbDeviceHS);
}
//下面部分的确保全部发完再继续, 但是实际测试没啥效果.
uint8_t time = HAL_GetTick();
while(hcdc->TxState != 0){
if(HAL_GetTick() - time > 1000){
return result;
}
}
/* USER CODE END 12 */
return result;
}
针对第一个问题: 电脑无法识别USB虚拟出来的串口.
第一次启动偶尔是OK的.
经过插插拔拔的无数次实验
最后总结出一条规律,
纯软件重启单片机不行.
按板子上的Rest按钮也不行,
开发IDE工具里面重启也不行.
拔掉USB数据线也不行.
重新烧录也不行.
唯一可行的方法就是烧写之后, 拔掉烧录的数据线,重新插上就可以了.
目前我唯一的解决方案是,先编译,下载烧录好代码过后,再重新插拔一下JTAG烧录调试线. 再打开上位机就可以正常打开端口进行通信了.
一天来来回回几百次. USB口都松了.
网上有人说,初始化USB的时候强制重置一下USB的时钟就可以了. 这个实在不太懂怎么重置USB的时钟. 我直觉告诉我这应该是解决方案. 可我不会初始化USB时钟啊. 哪位神秘的大哥也没说怎么重置USB的时钟(后来证明不是这个问题)
总结(吐槽)一下 坑太多. 而自己又没有掌握太多跳出坑的方法(经验太少).差点放弃.
单片机的坑比纯软件开发的坑要难得多.
因为纯软件开发顺着数据流来理就能理清楚. 很容易找到问题点. 而单片机开发, 不单单有数据流, 还有硬件,电子, 干扰,静电,接口不牢固,底层库有bug, 别人的代码看不懂, 整套环境不理解. C语言不精通. 这些问题纠缠在一起 没个1年半载的想很顺利的排除问题,掌握它,真的是非常困难. 并且这些问题的纠缠不是串行的是并行的. 看似完全不相干的东西也会相互干扰…(经验的价值随之体现)
后来我发现,USB能识别了,但是虚拟串口却时不时的打不开. 再后来在参考了网上的几篇文章之后,发现端口总是打不开的最大原因是
在usbd_conf.h中 开启了内存动态分配
/** Alias for memory allocation. */
#define USBD_malloc malloc //就是这个宏分配内存的.
/** Alias for memory release. */
#define USBD_free free
/** Alias for memory set. */
#define USBD_memset memset
/** Alias for memory copy. */
#define USBD_memcpy memcpy
/** Alias for delay. */
#define USBD_Delay HAL_Delay
/* DEBUG macros */
参考文章如下.
https://blog.csdn.net/a136498491/article/details/85304905
思路:偶然间看到有人说在USB库的初始化代码那里,malloc函数返回值那里打个断电。果然,多次插拔USB会发生malloc失败。具体原因没有深究,我早就看不顺heap了,决定去掉heap,使用静态分配。
过程:
1、USBD_CDC_Init中找到USBD_malloc调用的地方,改成静态分配;
2、USBD_CDC_DeInit中注释掉USBD_free函数的调用;
3、IAR(我用的IAR)配置中,General Options->Library Options 2 -> Heap
selection中选择No-free heap;4、Linker中把heap设置为0.
然后上电插拔打开测试30次,问题消失。
既然跟内存动态分配有关,干脆直接去掉动态分配,改成静态的.
另外这篇文章里写了如何将USBD_malloc改成静态的. 多谢前辈的文章.
https://blog.csdn.net/anobodykey/article/details/50700304
方法知道了我这边的做法很简单
在usbd_conf.c文件中加入如下代码即可
放在 /* USER CODE BEGIN Includes */中即可
.....
.....
.....
/* USER CODE BEGIN Includes */
//#include "MyUsbd_conf.h"
void * USBD_static_malloc(uint32_t size)
{
//size 参数= sizeof (USBD_CDC_HandleTypeDef);
//因为是静态分配的,无法知道sizeof (USBD_CDC_HandleTypeDef)的大小初始值. 这里的540是调试出来的, 每种单片机都需要调试一下看看传进来的size是多少,然后改成这个数值.
static uint32_t mem[540];
return mem;
}
void USBD_static_free(void *p)
{
}
#define USBD_malloc (uint32_t *)USBD_static_malloc //#define USBD_malloc malloc 被替换掉
#define USBD_free USBD_static_free //#define USBD_free free 被替换掉
/* USER CODE END Includes */
.....
.....
.....
注意替换里面的 540 参数…
要调试一次才知道具体的数值.
我估计改成动态的也是因为程序员头疼怎么实现这个效果了… 因为根本就无法在 设计的时候知道运行时size…
设计成这种 动态分配内存也是无奈之举…
搞成静态分配以后虚拟串口确实稳定多了. 也不会识别不了接口了.无法打开端口的问题就没有了 经过我长达10几天的测试基本上可以说第一个问题已经彻底解决了,USB识别妥妥的.虚拟的串口也能识别了.
针对第二个问题: 在调用CDC_Transmit_HS这个函数的时候,总是会卡在return USBD_BUSY ;
最后找到了.这篇文章.
http://news.eeworld.com.cn/mcu/2018/ic-news092041381.html
大概意思是USB库为了确保数据能完整发出去, 做了个循环检测, 如果数据没发送完,后面的数据是无法再发送的, 也就是说的FIFO (先进先出). (纯粹我自己瞎猜, 没看懂代码.) 如果强行修改此处的逻辑就无法保证收到的数据是完整的. 或者被覆盖的.
我感觉应该是上位机处理的速度太慢了吧. 导致数据发送不完. 堆在那里.
所以根本的解决之道是提高上位机的处理速度. 或者能加个缓冲什么的. 或者把算法放一部分到单片机里面做.压缩或减少要发送的数据…
下面的修改要慎重.我纯粹是瞎猜的, (7天后证明下面的修改无效). 然后就在要放弃的时候, 我找到了另外一篇文章.全靠百度啊. 算是彻底解决了USB发送端的问题.
找到 PCD_WriteEmptyTxFifo函数. 找不到就全局搜索喽…我的是在stm32f4xx_hal_pcd.c文件中.
/**
* @brief Check FIFO for the next packet to be loaded.
* @param hpcd PCD handle
* @param epnum endpoint number
* @retval HAL status
*/
static HAL_StatusTypeDef PCD_WriteEmptyTxFifo(PCD_HandleTypeDef *hpcd, uint32_t epnum)
{
USB_OTG_GlobalTypeDef *USBx = hpcd->Instance;
uint32_t USBx_BASE = (uint32_t)USBx;
USB_OTG_EPTypeDef *ep;
uint32_t len;
uint32_t len32b;
uint32_t fifoemptymsk;
ep = &hpcd->IN_ep[epnum];
if (ep->xfer_count > ep->xfer_len)
{
return HAL_ERROR;
}
len = ep->xfer_len - ep->xfer_count;
if (len > ep->maxpacket)
{
len = ep->maxpacket;
}
len32b = (len + 3U) / 4U;
while (((USBx_INEP(epnum)->DTXFSTS & USB_OTG_DTXFSTS_INEPTFSAV) >= len32b) &&
(ep->xfer_count < ep->xfer_len) && (ep->xfer_len != 0U))
{
/* Write the FIFO */
len = ep->xfer_len - ep->xfer_count;
if (len > ep->maxpacket)
{
len = ep->maxpacket;
}
len32b = (len + 3U) / 4U;
(void)USB_WritePacket(USBx, ep->xfer_buff, (uint8_t)epnum, (uint16_t)len,
(uint8_t)hpcd->Init.dma_enable);
ep->xfer_buff += len;
ep->xfer_count += len;
/**加入这一行代码解决发送慢的问题 start 事实证明没啥用*/
if (ep->xfer_len <= ep->xfer_count)
{
fifoemptymsk = (uint32_t)(0x1UL << (epnum & EP_ADDR_MSK));
USBx_DEVICE->DIEPEMPMSK &= ~fifoemptymsk;
break;
}
/**加入这一行代码解决发送慢的问题, end 事实证明没啥用*/
}
/*上面加入的代码其实跟下面这句话是一样的. 我纯粹是瞎猜的.*/
if (ep->xfer_len <= ep->xfer_count)
{
fifoemptymsk = (uint32_t)(0x1UL << (epnum & EP_ADDR_MSK));
USBx_DEVICE->DIEPEMPMSK &= ~fifoemptymsk;
}
return HAL_OK;
}
真正解决第二个问题的是自己写了一个函数(百度来的).
/*全速的USB VCP一次最多可以发送64个字节,高速的USB VCP 一次可以最多发送512个字节,
* 如果一次发送整个数据包的数据,需要再发送一个数据个数为0的数据包,否则上位机可能没法接收数据。
* USB发送数据主要是调用下面的函数:
* 根据上面USB发送数据包的要求,我们使用下面的函数来调用上面的函数来发送任意大小的数据,这个函数会自动在发送最大数据包后再发送一个空的数据包。
* USB_PACK_SIZE根据USB模式可能为64或者512
* 可发送任意字节的数据到PC端
* */
//建议使用 直接使用 USB2PC 发送数据到 PC
uint8_t _UsbSendData(uint8_t *pbuff,uint16_t buffsize)
{
uint16_t retry = 0;
USBD_HandleTypeDef *pdev = &hUsbDeviceHS;
USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*) pdev->pClassData;
hcdc->TxBuffer = pbuff;
hcdc->TxLength = buffsize;
if (pdev->pClassData != NULL) {
if (hcdc->TxState == 0) {
/* Tx Transfer in progress */
hcdc->TxState = 1;
/* Transmit next packet */
USBD_StatusTypeDef usb_status = USBD_LL_Transmit(pdev, CDC_IN_EP,
hcdc->TxBuffer, hcdc->TxLength);
//return usb_status;
} else {
return USBD_BUSY;
}
}
//等待发送结束
while (hcdc->TxState == 1) {
retry++;
if (retry == 0xfff0) {
return USBD_FAIL;
}
}
return USBD_OK;
}
#define USB_PACK_SIZE 512
void USB2PC(uint8_t *str,uint16_t len)
{
uint16_t j = 0;
if(len < USB_PACK_SIZE)
{
_UsbSendData(str,len);
}
else
{
for (j = 0;j < len/USB_PACK_SIZE;j++)
{
_UsbSendData((str+USB_PACK_SIZE*j),USB_PACK_SIZE);
}
_UsbSendData((str+USB_PACK_SIZE*j),len%USB_PACK_SIZE);
}
}
uint16_t TestADC_Values[1];
void main()
{
....
....
//测试用变量, 逐个递增.测试上位机收到的数据有没有丢失的现象
uint16_t v = 0;
while (1)
{
v++;
TestADC_Values[0] = v; //
int size = sizeof(TestADC_Values);
USB2PC(&TestADC_Values, size) ;
}
}
上位机主要代码 ,上位机用的是C# 和 LibUsbDotNet 接收USB数据的. 至于如何使用LibUsbDotNet. 我就不详细讲解了. 这篇文章主旨是解决问题, 不是长篇大论 .
......
......
if (MyUsbDevice != null)
{
writer = MyUsbDevice.OpenEndpointWriter(WriteEndpointID.Ep01);
reader = MyUsbDevice.OpenEndpointReader(ReadEndpointID.Ep01);
reader.ReadBufferSize = 2;
reader.DataReceived += (onDataReceived);
reader.DataReceivedEnabled = true;
}
......
......
List<int> datas = new List<int>();
//这里只接收数据
private void onDataReceived(object sender, EndpointDataEventArgs e)
{
var helper = new ByteHelper(e.Buffer, e.Count);
while (helper.HasNextInt16())
{
datas.Add(helper.ReadUint16());
}
return;
}
//然后加个定时器1秒计算一次看看是否有丢失数据的情况
//判断逻辑,最后一个数字是(最大的)减去第一个数字(最小的),的差应该等于收到的数字个数. 也就是说数字是一个一个递增过来的, 中间没有丢失过数据.
private void timer1_Tick(object sender, EventArgs e)
{
var temp = this.datas;
this.datas = new List<int>();
this.label1.Text = this.dataCount.ToString();
if (temp.Count > 1)
{
var maxcha = temp.Last() - temp.First();
this.label2.Text = maxcha == temp.Count - 1 ? "未丢失数据" : "丢失数据" + (maxcha - temp.Count - 1) + "最大值为:" + temp.Max(); //事实证明最大值达到65535 会溢出一次识别为丢失数据.其它正常 也就是未丢失数据.
}
this.dataCount = 0;
}
之前用Nlog 写日志到文件里面发现数字总是断断续续的不连续, 我都快要怀疑人生了. 后来怀疑可能是Nlog的问题, 然后就将数据直接写到内存里.每秒判断一次是否丢失过数据. 事实证明最大值达到65535 会溢出一次识别为丢失数据.其它正常 也就是未丢失数据.证明 USB2PC 这个函数是稳定的,可靠的, 而且是不卡的. 比官方HAL库提供的CDC_Transmit_HS 要稳定的多. 也简单高效的多. 看来HAL还不是很成熟啊. 毕竟刚开始,有点BUG也能理解. 还希望早日弥补这个bug… 不要浪费更多人的时间. 感谢USB2PC 的作者. 参考连接
http://www.stmcu.org.cn/module/forum/forum.php?mod=viewthread&tid=609496&highlight=usb%2B%E9%80%9F%E5%B%20…
ByteHelper.cs 主要代码
public class ByteHelper
{
public int Index = 0;
private byte[] buffer;
private int dataLength = 0;
public ByteHelper( byte[] buffer,int datalength)
{
this.buffer = buffer;
this.dataLength = datalength;
Index = 0;
}
public byte ReadByte()
{
var b = buffer[Index];
Index = Index + 1;
return b;
}
public UInt16 ReadUint16( )
{
var b = BitConverter.ToUInt16(buffer, Index);
Index = Index + 2;
return b;
}
public Int16 ReadInt16()
{
var b = BitConverter.ToInt16(buffer, Index);
Index = Index + 2;
return b;
}
public UInt32 ReadUint32( )
{
var b = BitConverter.ToUInt32(buffer, Index);
Index = Index + 4;
return b;
}
public int ReadInt32()
{
var b = BitConverter.ToInt32(buffer, Index);
Index = Index + 4;
return b;
}
public Int64 ReadInt64()
{
var b = BitConverter.ToInt64(buffer, Index);
Index = Index + 8;
return b;
}
public UInt64 ReadUInt64()
{
var b = BitConverter.ToUInt64(buffer, Index);
Index = Index + 8;
return b;
}
public bool HasNextShort()
{
return Index + 1 <= dataLength;
}
public bool HasNextInt16()
{
return Index + 2 <= dataLength;
}
public bool HasNextByte()
{
return Index + 1 <= dataLength;
}
public bool HasNextInt32()
{
return Index + 4 <= dataLength;
}
public bool HasNextInt64()
{
return Index + 8 <= dataLength;
}
}