STM VCP移植笔记
0x01 为什么要写这篇文章
- 官方sample针对的是stm提供的开发板,而我们往往使用的是自己买的或者diy的开发板,硬件上有一些不同,而对于新手来说,官方的sample有许多宏来配置针对他自己的开发板,导致新手看代码看起来很晕。
- 很多人并不想从头了解USB的协议,如何枚举,如何写描述符,只是想快速的搭建一个VCP。
0x02 sample的思路
以下内容都是从网上收集的

目前发布的STM32_USB-FS-Device_Lib中有一个USB虚拟串口的例程,这个例程演示了把STM32配置为一个USB虚拟串口设备,STM32从它的USART接口接收数据并通过USB传送到上位机,反之STM32也从USB接收上位机送来的数据并从USART接口发送出去。
在从USART接口接收数据再向USB端口发送数据的这个方向上,例程采取的策略是:在每次从USART接口收到一个字节后,就做成一个USB数据包并发送出去。这种方法的好处是程序简单明了,但如果USART端出现连续的数据流时,容易造成数据丢失的问题。
0x03 软硬件环境
所谓移植就是因为我们用的开发板和STM的开发板的硬件上是有区别,STM开发板可以通过IO口控制上拉电阻,以达到控制USB连接的目的,我们这个板子是1.5k电阻直接接在VCC上。
硬件环境(我的开发板)


软件环境
- eclipse-cpp-kepler-SR2-macosx-cocoa.tar.gz
- gcc-arm-none-eabi–4_8–2014q3–20140805-mac.tar.bz2
- ilg.gnuarmeclipse.repository–2.4.2–201411261616.zip
- JLink_MacOSX_V510c.pkg
- STM32_USB-FS-Device_Lib_V3.3.0
0x04 开始移植

     
修改项目include配置
基础工作做好了,可以开始修改里面的代码了
现在我们开始从上到下修改一遍文件
hw_config.c
去掉include文件
#include "platform_config.h"
#include "stm32_eval.h"
添加include文件
#include "stm32f10x_conf.h"
#include "usb_conf.h"
注释掉下面这段,因为我们不用GPIO控制USB通断
/* Enable USB_DISCONNECT GPIO clock */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIO_DISCONNECT, ENABLE);
/* Configure USB pull-up pin */
GPIO_InitStructure.GPIO_Pin = USB_DISCONNECT_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_Init(USB_DISCONNECT, &GPIO_InitStructure);
注释掉下面这段,因为我们不用从USART接收数据再发送到USB,我们移植后肯定有自己的逻辑
/* Enable USART Interrupt */
NVIC_InitStructure.NVIC_IRQChannel = EVAL_COM1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_Init(&NVIC_InitStructure);
注释掉下面这段,同上,我们不用GPIO控制USB通断
if (NewState != DISABLE)
{
GPIO_ResetBits(USB_DISCONNECT, USB_DISCONNECT_PIN);
}
else
{
GPIO_SetBits(USB_DISCONNECT, USB_DISCONNECT_PIN);
}
注释掉函数内部的内容,同上,USART是例子中的我们不用
void USART_Config_Default(void)
注释掉函数内部的内容,原理同上
bool USART_Config(void)
注释掉函数内部的内容
void USB_To_USART_Send_Data(uint8_t* data_buffer, uint8_t Nb_bytes)
注释掉函数内部的内容
void USART_To_USB_Send_Data(void)
这句没啥用,也注释掉吧
extern LINE_CODING linecoding;
stm32f10x_it.c
去掉include文件
#include "platform_config.h"
#include "stm32_eval.h"
注释掉函数内部的内容
void EVAL_COM1_IRQHandler(void)
main.c
#include "usb_lib.h"
#include "usb_desc.h"
#include "hw_config.h"
#include "usb_pwr.h"
int main(int argc, char* argv[]) {
Set_System();
Set_USBClock();
USB_Interrupts_Config();
USB_Init();
while (1) {
// Add your code here.
}
}
至此项目就可以编译生成hex了,接下来我们把hex传到开发板上,我用的是JLink SWD方式
在项目的根目录建一个文本文件,命名为JLinkCommand.jlink,内容如下
r
speed 4000
device STM32F103RC
loadbin /Users/xuwen/Documents/workspace/MCU/stm32f103rcvirtualcom/Debug/stm32f103rcvirtualcom.hex 0x08000000
g
qc

xuwendeMacBook-Pro:~ xuwen$ /Applications/SEGGER/JLink/JLinkExe -device STM32F103RC -if SWD -speed 4000 -autoconnect 1 -CommanderScript /Users/xuwen/Documents/workspace/MCU/vcpdemo/JLinkCommand.jlink

将开发板接到机器上就可以看结果了,这里我测试win7可以自动安装驱动,osx不需要安装驱动。 但是上面的移植改造只是让系统能识别到串口,并没有啥实际功能,这里再放一段代码实现个简单的数据发送功能。
#include "usb_lib.h"
#include "usb_desc.h"
#include "hw_config.h"
#include "usb_pwr.h"
#include "usb_conf.h"
#include "stdio.h"
uint8_t buff[16] = { 0 };
void delay_ms(u16 nms) {
u32 temp;
SysTick->LOAD = 9000 * nms;
SysTick->VAL = 0X00; //清空计数器
SysTick->CTRL = 0X01; //使能,减到零是无动作,采用外部时钟源
do {
temp = SysTick->CTRL; //读取当前倒计数值
} while ((temp & 0x01) && (!(temp & (1 << 16)))); //等待时间到达
SysTick->CTRL = 0x00; //关闭计数器
SysTick->VAL = 0X00; //清空计数器
}
int main(int argc, char* argv[]) {
Set_System();
Set_USBClock();
USB_Interrupts_Config();
USB_Init();
while (1) {
// Add your code here.
sprintf(buff, "temp:%9.3f\r\n", 3.145);
UserToPMABufferCopy(buff, ENDP1_TXADDR, 16);
SetEPTxCount(ENDP1, 16);
SetEPTxValid(ENDP1);
delay_ms(1000);
}
}


0x05 网上资料
下面是我在做上面工作的时候在网上搜索的内容,感觉对移植工作理解有所帮助也就拷贝下来了
网上参考:usb stm32f103rc 虚拟串口的数据收发 2011–12–06 17:19阅读:855 1、时钟定义 //由72M分频1.5后得到 RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_1Div5); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USB, ENABLE); 2、中断向量定义 NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); 3、中断函数 void USB_LP_CAN1_RX0_IRQHandler(void) {//添加usb中断的处理函数 USB_Istr(); } 4、例程中用的是端点1发数据,端点3收数据,数据的处理函数放入下表中 void (pEpInt_IN[7])(void) = { EP1_IN_Callback,—– }; void (pEpInt_OUT[7])(void) = { EP3_OUT_Callback,—- }; 5、USB的相关初始化在STM公司提供的例程中有,主线是把握中断方式,在主机隔一段时间查询是否有数据收发时处理数据。
网上参考:使用STM32F102/103 USB函数库 进行USB通信
第一步: 根据应用的需求,定义使用到的端点数量
usb_conf.h
define EP_NUM (3)
以上意味着应用需要使用到EP0,EP1和EP2 第二步: 初始化每个使用到的端点
usb_prop.c
SetEPType(ENDP2, EP_INTERRUPT); 定义端点2为中断端点
SetEPTxAddr(ENDP2, ENDP2_TXADDR); 如果需要进行EP2 IN通信,需要定义端点2的发送缓存区的地址,也就是在Packet Buffer中的偏移地址
SetEPRxAddr(ENDP2, ENDP2_RXADDR); 如果需要进行EP2 OUT通信,需要定义端点2的接收缓存区在Packet Buffer中的偏移地址
SetEPRxStatus(ENDP2, EP_RX_NAK); 设置端点2的接收状态为NAK,设备将以NAK来响应主机发起的所有OUT通信。
SetEPTxStatus(ENDP2, EP_TX_NAK); 设置端点2的发送状态为NAK,设备将以NAK来响应主机发起的所有IN通信。
第三步: 使能端点的通信
对于IN端点的使能: UserToPMABufferCopy(Send_Buffer, ENDP2_TXADDR, 8); 拷贝用户数据到端点2的发送缓存区
SetEPTxCount(ENDP2, 8); 设置端点2发送数据长度
SetEPTxValid(ENDP2); 设置端点2的发送状态为VALID
以上三句可以在应用代码的任意位置调用,一旦调用,即使能了一次USB IN通信。
USB设备将在收到主机的IN TOKEN后,自动发送缓存区中的数据到主机,并在发送完毕后产生EP2_IN_Callback中断,同时将端点2的发送状态自动改为NAK。
如果需要再次进行数据传送,需要再次调用以上的三句函数。
对于OUT端点的使能:
SetEPRxValid(ENDP2); 设置端点2的接收状态为VALID。
以上的这句函数即使能了端点2的OUT通信,可以在任意位置调用。
一旦调用,即使能了一次OUT通信。USB设备将以ACK来响应主机随后的OUT通信,并在接收数据完毕后,产生EP2_OUT_Callback中断,同时自动将端点的接收状态改为NAK。
在EP2_OUT_Callback中断函数中调用 USB_SIL_Read(EP2_OUT, Receive_Buffer); 可以将端点2接收缓存区中收到的数据拷贝到用户数据区