前言

因为项目上要使用MCU与一台仪器进行通信,而该仪器只留出了USB接口。将该仪器接入PC,发现该仪器使用的是FT232RL芯片将内部的信号转为了USB。我查询了很多资料,发现FT232使用的不是标准USB协议,而是厂商自定义的协议,不能直接使用STM32的USB HOST库来跟FT232通信,而我也试了外加一个USB从设备转TTL模块,也不能成功建立通信,最终还是只能自己修改STM32的USB HOST库从协议层面来兼容FT232。我大概调试了一周的时间,中间也撞了很多次墙,遇到了很多ai也解决不了的问题,最终还是靠翻官方手册(吐槽一下官方手册写的全是它自己的库怎么用的和PC上怎么用的,根本没写底层的通信协议,寄存器地址都没有)和看linuxUSB_Host_Shield_2.0的FT232底层驱动,成功在STM32H723ZGT6上实现了FT232的USB通信。
当时翻阅很多国内、国外的网站,并没有找到合适的资料,在STM32的官方社区上面提问也没有得到反馈,中间也看到很多在各种论坛的提问者放弃,但我们项目使用的仪器厂商不给更换接口,只能一条路走到黑,还好最后搞出来了,在此处留下记录以给后来者一点帮助。

开发环境

  • 硬件

    • STM32H723ZGT6核心板(自己画的,注意usb的d+ d-一定要画成差分等长线,不然完全通信不了)
    • FT232RL USB转TTL模块(与STM32H723通过USB CDC通信,TTL端连接CH340)
    • CH340 USB转TTL模块(TTL端连接到FT232RL的TTL上,USB端插入PC检测STM32H723USB发送和接收是否成功)
      在这里插入图片描述
  • 软件

    • STM32CubeIDE 1.19.0
    • VSCode Insiders(纯用来写代码和用copilot)
    • web串口上位机
    • wireshark(用来抓FT232RL协议包)

FT232 USB协议分析

将FT232RL接入PC,用wireshark来抓FT232协议包,抓到的FT232RL在PC上从获取硬件描述符到可以收发数据的协议包如下:

  1. 获取硬件描述符阶段
    在这里插入图片描述
  2. 不知道在干什么阶段
    在这里插入图片描述
  3. 第一次硬件参数配置阶段
    在这里插入图片描述
  4. 软重启阶段
    在这里插入图片描述
  5. 第二次硬件参数配置阶段
    在这里插入图片描述
  6. 在PC上打开上位机,抓取打开上位机之后FT232RL的协议包如下:
    在这里插入图片描述
    FT232RL在2阶段干了什么我查了官方的资料没有提及,而在linux和USB_Host_Shield_2.0库中也没有这一段内容,从linux和USB_Host_Shield_2.0库中分析,要使STM32H723能与FT232RL通信,最重要的是第1阶段和第6阶段。FT232RL的初始化流程如下:
FT232RL_Initialization_Flow cluster_main_flow  FT232RL 初始化流程   cluster_ftdi_config FT232配置流程 cluster_enum_detail 枚举设备流程 cluster_modem_detail_start Modem控制 开始设置FT232 cluster_modem_detail_end Modem控制 结束设置FT232 start 开始 connect FT232RL 连接到 USB 主机 start->connect enum_process 枚举FT232设备 connect->enum_process    检测到设备 modem_handshake1 Modem控制 开始设置FT232 enum_process->modem_handshake1    选择CDC类接口 reset_chip 复位 FTDI 芯片 (多次执行) modem_handshake1->reset_chip set_latency  设置延迟定时器   reset_chip->set_latency set_data_format 设置数据格式 set_latency->set_data_format modem_handshake Modem控制 开始设置FT232 set_data_format->modem_handshake set_flow_ctrl 设置流控 modem_handshake->set_flow_ctrl set_baud_rate 设置波特率 set_flow_ctrl->set_baud_rate final_modem_ctrl Modem控制 结束设置FT232 set_baud_rate->final_modem_ctrl set_data_format2 设置数据格式 final_modem_ctrl->set_data_format2 set_flow_ctrl2 设置流控 set_data_format2->set_flow_ctrl2 init_complete 初始化完成 (准备数据传输) set_flow_ctrl2->init_complete get_dev_desc 获取设备描述符 set_addr 设置地址 get_dev_desc->set_addr get_cfg_desc 获取配置描述符 set_addr->get_cfg_desc set_cfg 设置配置 get_cfg_desc->set_cfg modem_ctrl_start_1 设置 DTR 高 RTS 低 modem_ctrl_start_2 设置 DTR 低 RTS 高 modem_ctrl_start_1->modem_ctrl_start_2 modem_ctrl_end_1 设置 DTR 低 RTS 高 modem_ctrl_end_2 设置 DTR 高 RTS 低 modem_ctrl_end_1->modem_ctrl_end_2

硬件配置

  1. 配置时钟
    USB的时钟一定要使用48MHz的,直接用HSI48或者从PLL里面分频分48MHz的都可以

    通过查询STM32官方的手册可以发现HSI48是能直接用于USB的,但是你问ai,ai可能会告诉你HSI48不行,我们以STM32官方的手册为准,手册的名字是STM32H723/733, STM32H725/735 and STM32H730 Value line advanced Arm®-based 32-bit MCUsGetting started with STM32H723/733, STM32H725/735 and STM32H730 Value Line hardware development
    在这里插入图片描述

STM32官方手册对于HSI48的描述
跟据STM32官方的描述,要使用HSI48作为USB的时钟必须加CRS时钟恢复系统,但是经我测试不加也是能正常工作的,对于低负载的情况应该是够了,要是怕出问题还是用外部时钟PLL之后给USB吧。

  1. USB HOST CDC配置
    Connectivity/USB_OTG_HS中USB配置为内部FS Phy的Host_Only模式,Host的设置可以保持默认,End Point中断可以不用打开。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    Middleware and Software Packs/USB_HOST中设置Class For HS IPCommunication Host Class (Virtual Port Com),然后设置USBH_DEBUG_LEVEL(USBH Debug Level)3: All messages and internal debug messages are shown (这项设置是为了方便调试)
    在这里插入图片描述
    然后设置USB的供电使能引脚,我的PCB使用了一个MT9700-N芯片给USB Device设备供电,这个芯片需要使用一个使能信号才会输出5V给USB设备,我使用的是PA8引脚,而高使能和低使能在配置硬件的时候可以忽略。
    在这里插入图片描述
  2. 其余硬件配置
    按照实际使用情况配置串口1作为USB的Debug信息输出,波特率为115200
    在这里插入图片描述
    在这里插入图片描述

配置SW调试下载引脚
在这里插入图片描述


STM32 USB HOST库修改(C语言版)

为了使FT232RL能够与STM32单片机进行通信,需要对STM32 USB HOST库进行修改以支持FT232的USB协议,以下修改内容均建立在不影响STM32Cube重新生成底层的情况下进行。
为了便于修改和调试,创建一个FT232.cFT232.h文件,下述的函数若无特殊说明,均在FT232.c中实现,在FT232.h中声明
先在FT232.h中包含必要的USB相关头文件

#include "usbh_cdc.h"
#include "usbh_conf.h"
#include "usbh_def.h"

串口重定向

为了使用STM32的USB Debug Message功能,需要对串口进行重定向。在main.c开头添加这段代码

int fputc(int ch, FILE *f) {
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);// 可以跟据实际情况修改这里的串口句柄
	return ch;
}

int _write(int file, char* ptr, int len) {
	HAL_UART_Transmit(&huart1, (uint8_t*)ptr, len, 100);// 可以跟据实际情况修改这里的串口句柄
	return len;
}

fputc_write可以只用其中一个,看你的板子哪个有效就用哪个,对我的板子是_write有效。

修改VBUS使能极性

USB_HOST/Target文件夹中打开usbh_platform.c文件,找到其中的void MX_DriverVbusHS(uint8_t state)函数,跟据你的EN极性来修改使能信号极性,以我使用的MT9700-N为例,MT9700-N是高使能,低失能。

void MX_DriverVbusHS(uint8_t state)
{
  uint8_t data = state;// state为1使能,为2失能
  /* USER CODE BEGIN PREPARE_GPIO_DATA_VBUS_HS */
  if(state == 0)
  {
    /* Drive high Charge pump */
    data = GPIO_PIN_RESET; // Cube生成的默认为GPIO_PIN_SET
  }
  else
  {
    /* Drive low Charge pump */
    data = GPIO_PIN_SET; // Cube生成的默认为GPIO_PIN_RESET
  }
  /* USER CODE END PREPARE_GPIO_DATA_VBUS_HS */
  HAL_GPIO_WritePin(GPIOA,GPIO_PIN_8,(GPIO_PinState)data);
}

完成这一步后把FT232接入STM32H7,用串口打印调试信息,如果前面的步骤都对了,这个时候串口的输出应该是:
在这里插入图片描述
如果到这一步输出的不是这样的文字,那说明前面的步骤有问题。先看串口有没有供上电,没供上电就是使能信号极性反了,如果供上电了那大概就是FT232有问题或者d+、d-没有画成差分等长线。
经过上面的步骤,STM32H723已经能成功获取FT232RL的硬件描述符了,但是在STM32的USB CDC协议中找不到FT232RL的协议,所以这时候会提示No registered class for this device.

创建专门用于FT232的USB CDC对象

USB_HOST/App文件夹中打开usb_host.c文件,找到其中的MX_USB_HOST_Init函数,其中if (USBH_RegisterClass(&hUsbHostHS, USBH_CDC_CLASS) != USBH_OK)中输入的USBH_CDC_CLASS形参即为STM32 USB HOST库默认的CDC对象

//Cube默认生成的MX_USB_HOST_Init函数
void MX_USB_HOST_Init(void)
{
  /* USER CODE BEGIN USB_HOST_Init_PreTreatment */

  /* USER CODE END USB_HOST_Init_PreTreatment */

  /* Init host Library, add supported class and start the library. */
  if (USBH_Init(&hUsbHostHS, USBH_UserProcess, HOST_HS) != USBH_OK)
  {
    Error_Handler();
  }
  if (USBH_RegisterClass(&hUsbHostHS, USBH_CDC_CLASS) != USBH_OK)
  {
    Error_Handler();
  }
  if (USBH_Start(&hUsbHostHS) != USBH_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USB_HOST_Init_PostTreatment */
  
  /* USER CODE END USB_HOST_Init_PostTreatment */
}

Middlewares/ST/STM32_USB_HOST_Library/Class/CDC/Inc文件夹中打开usbh_cdc.h文件,找到USBH_CDC_CLASSdefine,将其复制一份到FT232.h中,命名为USBH_CDC_FT232_CLASS,值改为&FT232_CDC_Class

#define USB_CDC_FT232_CLASS     0xFFU
#define USBH_CDC_FT232_CLASS    &FT232_CDC_Class

Middlewares/ST/STM32_USB_HOST_Library/Class/CDC/Src文件夹中打开usbh_cdc.c文件,找到CDC_Class的定义,将其复制一份到FT232.c中,命名为FT232_CDC_Class,修改其中的USB_CDC_CLASSUSB_CDC_FT232_CLASS,并将usbh_cdc.c文件包含的static函数声明与定义全部复制到FT232.c

// 将以下函数的定义也复制到usbh_cdc.c文件中,函数定义部分太长,此处省略
static USBH_StatusTypeDef USBH_CDC_InterfaceInit(USBH_HandleTypeDef *phost);

static USBH_StatusTypeDef USBH_CDC_InterfaceDeInit(USBH_HandleTypeDef *phost);

static USBH_StatusTypeDef USBH_CDC_Process(USBH_HandleTypeDef *phost);

static USBH_StatusTypeDef USBH_CDC_SOFProcess(USBH_HandleTypeDef *phost);

static USBH_StatusTypeDef USBH_CDC_ClassRequest(USBH_HandleTypeDef *phost);

//这个函数后面用不上,但是此时也要复制过来
static USBH_StatusTypeDef GetLineCoding(USBH_HandleTypeDef *phost,
                                        CDC_LineCodingTypeDef *linecoding);
//这个函数后面用不上,但是此时也要复制过来
static USBH_StatusTypeDef SetLineCoding(USBH_HandleTypeDef *phost,
                                        CDC_LineCodingTypeDef *linecoding);

static void CDC_ProcessTransmission(USBH_HandleTypeDef *phost);

static void CDC_ProcessReception(USBH_HandleTypeDef *phost);

USBH_ClassTypeDef  FT232_CDC_Class =
{
  "CDC",
  USB_CDC_FT232_CLASS,
  USBH_CDC_InterfaceInit,
  USBH_CDC_InterfaceDeInit,
  USBH_CDC_ClassRequest,
  USBH_CDC_Process,
  USBH_CDC_SOFProcess,
  NULL,
};

现在需要把刚刚创建的FT232_CDC_Class的实例加入MX_USB_HOST_Init中。STM32 USB HOST库使用USBH_RegisterClass(&hUsbHostHS, USBH_CDC_CLASS)来向USB主机协议栈注册一个设备类驱动,我们可以找到这个函数的定义如下:

/**
  * @brief  USBH_RegisterClass
  *         Link class driver to Host Core.
  * @param  phost : Host Handle
  * @param  pclass: Class handle
  * @retval USBH Status
  */
USBH_StatusTypeDef USBH_RegisterClass(USBH_HandleTypeDef *phost, USBH_ClassTypeDef *pclass)
{
  USBH_StatusTypeDef status = USBH_OK;

  if (pclass != NULL)
  {
    if (phost->ClassNumber < USBH_MAX_NUM_SUPPORTED_CLASS)
    {
      /* link the class to the USB Host handle */
      phost->pClass[phost->ClassNumber++] = pclass;
      status = USBH_OK;
    }
    else
    {
      USBH_ErrLog("Max Class Number reached");
      status = USBH_FAIL;
    }
  }
  else
  {
    USBH_ErrLog("Invalid Class handle");
    status = USBH_FAIL;
  }

  return status;
}

实际注册USB设备类驱动的代码就是phost->pClass[phost->ClassNumber++] = pclass;,我们不需要STM32 USB HOST库默认的CDC类,所以在MX_USB_HOST_Init中加入如下代码:

/* USER CODE BEGIN Includes */
#include "FT232.h"//引入刚写的FT232.h头文件
/* USER CODE END Includes */
/*
中间部分无改动,略
*/
void MX_USB_HOST_Init(void)
{
  /* USER CODE BEGIN USB_HOST_Init_PreTreatment */

  /* USER CODE END USB_HOST_Init_PreTreatment */

  /* Init host Library, add supported class and start the library. */
  if (USBH_Init(&hUsbHostHS, USBH_UserProcess, HOST_HS) != USBH_OK)
  {
    Error_Handler();
  }
  if (USBH_RegisterClass(&hUsbHostHS, USBH_CDC_CLASS) != USBH_OK)
  {
    Error_Handler();
  }
  if (USBH_Start(&hUsbHostHS) != USBH_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USB_HOST_Init_PostTreatment */
  hUsbHostHS.pClass[hUsbHostHS.ClassNumber - 1] = USBH_CDC_FT232_CLASS;//新增代码
  /* USER CODE END USB_HOST_Init_PostTreatment */
}

执行完这步之后,将程序下载至STM32H723,会发现USB Debug Message变化如下:
在这里插入图片描述
这代表STM32 USB HOST库已经成功注册FT232_CDC_Class了。

修改FT232 CDC类的初始化函数

打开FT232.c中的USBH_CDC_InterfaceInit函数,跟据USB Debug Message可以发现,现在FT232_CDC_Class在初始化时在下面这里失败了:

/**
  * @brief  USBH_CDC_InterfaceInit
  *         The function init the CDC class.
  * @param  phost: Host handle
  * @retval USBH Status
  */
static USBH_StatusTypeDef USBH_CDC_InterfaceInit(USBH_HandleTypeDef *phost)
{

  USBH_StatusTypeDef status;
  uint8_t interface;
  CDC_HandleTypeDef *CDC_Handle;

  interface = USBH_FindInterface(phost, COMMUNICATION_INTERFACE_CLASS_CODE,
                                   ABSTRACT_CONTROL_MODEL, COMMON_AT_COMMAND);
// FT232_CDC_Class在这个if语句初始化失败
  if ((interface == 0xFFU) || (interface >= USBH_MAX_NUM_INTERFACES)) /* No Valid Interface */
  {
    USBH_DbgLog("Cannot Find the interface for Communication Interface Class.", phost->pActiveClass->Name);
    return USBH_FAIL;
  }
// 下略

因为FT232使用的厂商自定义interface,跟据使用wireshark抓取的FT232RL协议包可以发现,FT232RL的bInterfaceClassbInterfaceSubClassbInterfaceProtocol都是0xff,在STM32 USB HOST库中使用USBH_FindInterface来寻找设备,而此时USBH_FindInterface输入的形参还是STM32 USB HOST库默认的CDC设备的参数,所以找不到设备导致初始化失败了。
在这里插入图片描述
USBH_FindInterface的定义如下:

/**
  * @brief  USBH_FindInterface
  *         Find the interface index for a specific class.
  * @param  phost: Host Handle
  * @param  Class: Class code
  * @param  SubClass: SubClass code
  * @param  Protocol: Protocol code
  * @retval interface index in the configuration structure
  * @note : (1)interface index 0xFF means interface index not found
  */
uint8_t USBH_FindInterface(USBH_HandleTypeDef *phost, uint8_t Class, uint8_t SubClass, uint8_t Protocol)
{
  USBH_InterfaceDescTypeDef *pif;
  USBH_CfgDescTypeDef *pcfg;
  uint8_t if_ix = 0U;

  pif = (USBH_InterfaceDescTypeDef *)NULL;
  pcfg = &phost->device.CfgDesc;

  while (if_ix < USBH_MAX_NUM_INTERFACES)
  {
    pif = &pcfg->Itf_Desc[if_ix];
    if (((pif->bInterfaceClass == Class) || (Class == 0xFFU)) &&
        ((pif->bInterfaceSubClass == SubClass) || (SubClass == 0xFFU)) &&
        ((pif->bInterfaceProtocol == Protocol) || (Protocol == 0xFFU)))
    {
      return  if_ix;
    }
    if_ix++;
  }
  return 0xFFU;
}

可以看到USBH_FindInterface的输入参数正好是ClassSubClassProtocol,所以将USBH_CDC_InterfaceInit函数中USBH_FindInterface输入的默认形参全部改为FT232RL的0xff

// 原来的USBH_FindInterface
// interface = USBH_FindInterface(phost, COMMUNICATION_INTERFACE_CLASS_CODE,
//                                    ABSTRACT_CONTROL_MODEL, COMMON_AT_COMMAND);

// 修改后的USBH_FindInterface
interface = USBH_FindInterface(phost, 0XFF, 0XFF, 0XFF);

将修改后的程序下载至STM32H723,可以看到USB Debug Message变化如下:
在这里插入图片描述
继续在USBH_CDC_InterfaceInit中查找这条Debug Message在出现的地方,可以发现是在这里:

  interface = USBH_FindInterface(phost, DATA_INTERFACE_CLASS_CODE,
                                   RESERVED, NO_CLASS_SPECIFIC_PROTOCOL_CODE);
  if ((interface == 0xFFU) || (interface >= USBH_MAX_NUM_INTERFACES)) /* No Valid Interface */
  {
    USBH_DbgLog("Cannot Find the interface for Data Interface Class.", phost->pActiveClass->Name);
    return USBH_FAIL;
  }

标准 CDC 设备有两个独立的接口,分别处理通信协议和收发数据,需要分别查找和初始化,但是FT232RL简化了操作,只需要查找一次interface就能解析出全部的接口,所以直接把上面这段代码从USBH_CDC_InterfaceInit删除,然后下载程序,可以看到USB Debug Message变化如下:
在这里插入图片描述
至此FT232_CDC_Class已经初始化完成。

FT232RL参数设置

在完成FT232_CDC_Class的初始化之后,需要设置FT232RL工作时的参数,也就是硬件流控、数据停止位、数据位、校验位、停止位、波特率和工作模式。FT232RL存在标准串口和bit bang两种工作模式,默认是以标准串口模式工作,因项目上只用到标准串口模式,bit bang模式没有研究。
首先需要解决ERROR: Control error: CDC: Device Get Line Coding configuration failed这个错误,在FT232.c中的USBH_CDC_ClassRequest函数,因为FT232RL不支持GetLineCoding功能,注释或删除掉这个函数,直接给status赋值跳过request过程,就不会输出ERROR: Control error: CDC: Device Get Line Coding configuration failed了。

/**
  * @brief  USBH_CDC_ClassRequest
  *         The function is responsible for handling Standard requests
  *         for CDC class.
  * @param  phost: Host handle
  * @retval USBH Status
  */
static USBH_StatusTypeDef USBH_CDC_ClassRequest(USBH_HandleTypeDef *phost)
{
  // USBH_StatusTypeDef status;// 原始的status定义
  USBH_StatusTypeDef status = USBH_OK;// 直接给status赋值USBH_OK
  CDC_HandleTypeDef *CDC_Handle = (CDC_HandleTypeDef *) phost->pActiveClass->pData;

  /* Issue the get line coding request */
  // status = GetLineCoding(phost, &CDC_Handle->LineCoding);// 注释或删除掉这行代码
  if (status == USBH_OK)
  {
    phost->pUser(phost, HOST_USER_CLASS_ACTIVE);
  }
  else if (status == USBH_NOT_SUPPORTED)
  {
    USBH_ErrLog("Control error: CDC: Device Get Line Coding configuration failed");
  }
  else
  {
    /* .. */
  }

  return status;
}

我们需要自行创建一个初始化状态机来设置FT232RL的参数。先创建一个枚举类型FT232_CDC_Init_StateTypeDef用来表示FT232RL的初始化状态,创建一个枚举类型FT232_CDC_Init_CommandTypeDef表示FT232RL的指令,创建一个结构体FT232_CDC_Init_Param用于保存FT232RL的初始化参数,创建一个结构体FT232_CDC_Init_Node用于保存实际调用的指令函数指针和输入指令函数的参数,创建一个结构体FT232_CDC_Init_State_Machine用于管理FT232RL初始化状态机:

// 写在FT232.h中
typedef enum
{
	FT232_CDC_RESET              = 0x00,
	FT232_CDC_SET_MODEM_CTRL     = 0x01,
	FT232_CDC_SET_FLOW_CTRL      = 0x02,
	FT232_CDC_SET_BAUD_RATE      = 0x03,
	FT232_CDC_SET_DATA           = 0x04,
	FT232_CDC_GET_MODEM_STATUS   = 0x05,
	FT232_CDC_SET_LATENCY_STATUS = 0x09,
	FT232_CDC_SET_BITMODE        = 0x0b,
}
FT232_CDC_Init_CommandTypeDef;

typedef enum
{
	FT232_CDC_UNDER_INIT         = 0x00,
	FT232_CDC_INIT_COMPLETE      = 0x01,
	FT232_CDC_INIT_ERROR         = 0x02
}
FT232_CDC_Init_StateTypeDef;

typedef struct{
	uint32_t baudrate;
	uint32_t modem;
	uint32_t flow;
	uint32_t latency;
	struct{
		uint32_t dataBits;
		uint32_t stopBits;
		uint32_t parity;
	}data;
}FT232_CDC_Init_Param;

typedef struct _FT232_CDC_Init_Node{
	FT232_CDC_Init_CommandTypeDef command;
	USBH_StatusTypeDef state;
	USBH_StatusTypeDef (*method)(USBH_HandleTypeDef*, struct _FT232_CDC_Init_Node*);
	FT232_CDC_Init_Param param;
}FT232_CDC_Init_Node;

typedef struct{
    FT232_CDC_Init_StateTypeDef FT232_CDC_Init_State;
    FT232_CDC_Init_Node* currentNode;

}FT232_CDC_Init_State_Machine;

然后声明和定义需要执行的初始化指令函数,并初始化FT232RL的初始化状态机列表:

// 写在FT232.c中
static USBH_StatusTypeDef FTDI_ResetDevice(USBH_HandleTypeDef *phost, FT232_CDC_Init_Node* param);
static USBH_StatusTypeDef FTDI_SetLatency(USBH_HandleTypeDef *phost, FT232_CDC_Init_Node* node);
static USBH_StatusTypeDef FTDI_SetModemCtrl(USBH_HandleTypeDef *phost, FT232_CDC_Init_Node* node);
static USBH_StatusTypeDef FTDI_SetBaudrate(USBH_HandleTypeDef *phost, FT232_CDC_Init_Node* node);
static USBH_StatusTypeDef FTDI_SetData(USBH_HandleTypeDef *phost, FT232_CDC_Init_Node* node);
static USBH_StatusTypeDef FTDI_SetFlowCtrl(USBH_HandleTypeDef *phost, FT232_CDC_Init_Node* node);

FT232_CDC_Init_Node FT232_CDC_Init_Node_List[] = {
  {.command = FT232_CDC_RESET,              .state = USBH_OK, .method = &FTDI_ResetDevice,
   .param = {.data = {0, 0, 0}, .baudrate =      0, .flow = 0, .latency =  0, .modem = 0x0000}}, // reset
  {.command = FT232_CDC_SET_LATENCY_STATUS, .state = USBH_OK, .method = &FTDI_SetLatency,
   .param = {.data = {0, 0, 0}, .baudrate =      0, .flow = 0, .latency = 10, .modem = 0x0000}}, // setLatency
  {.command = FT232_CDC_SET_MODEM_CTRL,     .state = USBH_OK, .method = &FTDI_SetModemCtrl,
   .param = {.data = {0, 0, 0}, .baudrate =      0, .flow = 0, .latency =  0, .modem = 0x0101}}, // setModemCtrl
  {.command = FT232_CDC_SET_MODEM_CTRL,     .state = USBH_OK, .method = &FTDI_SetModemCtrl,
   .param = {.data = {0, 0, 0}, .baudrate = 115200, .flow = 0, .latency =  0, .modem = 0x0202}}, // setModemCtrl
  {.command = FT232_CDC_SET_BAUD_RATE,      .state = USBH_OK, .method = &FTDI_SetBaudrate,
   .param = {.data = {0, 0, 0}, .baudrate =      0, .flow = 0, .latency =  0, .modem = 0x0000}}, // setBaudrate
  {.command = FT232_CDC_SET_MODEM_CTRL,     .state = USBH_OK, .method = &FTDI_SetModemCtrl,
   .param = {.data = {0, 0, 0}, .baudrate =      0, .flow = 0, .latency =  0, .modem = 0x0202}}, // setModemCtrl
  {.command = FT232_CDC_SET_MODEM_CTRL,     .state = USBH_OK, .method = &FTDI_SetModemCtrl,
   .param = {.data = {0, 0, 0}, .baudrate =      0, .flow = 0, .latency =  0, .modem = 0x0101}}, // setModemCtrl
  {.command = FT232_CDC_SET_DATA,           .state = USBH_OK, .method = &FTDI_SetData,
   .param = {.data = {8, 1, 0}, .baudrate =      0, .flow = 0, .latency =  0, .modem = 0x0000}}, // setData
  {.command = FT232_CDC_SET_FLOW_CTRL,      .state = USBH_OK, .method = &FTDI_SetFlowCtrl,
   .param = {.data = {0, 0, 0}, .baudrate =      0, .flow = 0, .latency =  0, .modem = 0x0000}}, // setFlow
};

inline static USBH_StatusTypeDef FTDI_EncodeRequest(USBH_HandleTypeDef *phost, uint8_t requestType, uint8_t request, uint16_t value, uint16_t index, uint16_t length)
{
	phost->Control.setup.b.bmRequestType = requestType;
	phost->Control.setup.b.bRequest = request;
	phost->Control.setup.b.wValue.w = value;
	phost->Control.setup.b.wIndex.w = index; // 接口号
	phost->Control.setup.b.wLength.w = length;

	return USBH_CtlReq(phost, NULL, 0);
}

static USBH_StatusTypeDef FTDI_ResetDevice(USBH_HandleTypeDef *phost, FT232_CDC_Init_Node* node)
{
	return FTDI_EncodeRequest(phost, 0x40, node->command, 0, 0, 0);
}

static USBH_StatusTypeDef FTDI_SetLatency(USBH_HandleTypeDef *phost, FT232_CDC_Init_Node* node)
{
	return FTDI_EncodeRequest(phost, 0x40, node->command, node->param.latency, 0, 0);
}

static USBH_StatusTypeDef FTDI_SetModemCtrl(USBH_HandleTypeDef *phost, FT232_CDC_Init_Node* node)
{
	return FTDI_EncodeRequest(phost, 0x40, node->command, node->param.modem, 0, 0);
}

static USBH_StatusTypeDef FTDI_SetBaudrate(USBH_HandleTypeDef *phost, FT232_CDC_Init_Node* node)
{
	static const uint8_t frac_code[] = {0, 3, 2, 4, 1, 5, 6, 7};
	uint16_t value, index;
	uint32_t divisor;

	if (node->param.baudrate == 0){
		return USBH_FAIL; // 防止除零
	}
	// 特殊处理3M波特率
	if (node->param.baudrate == 3000000){
		divisor = 0; // 实际分频数是1,但寄存器值设为0
	}
	else
	{
		// 计算分频数,公式为 Divisor = 3000000 / Baudrate
		// 为了进行整数运算并四舍五入,我们将分子乘以8
		divisor = (3000000 * 8) / node->param.baudrate;
		divisor = (divisor + 1) / 2 * 2; // 四舍五入
	}

	// 最终要写入寄存器的值
	value = (uint16_t)(divisor / 8);
	// 如果计算出的整数部分大于最大值 (2^14 - 1),则出错
	if (value >= 0x4000)
	{
		return USBH_FAIL;
	}

	// 将小数部分的编码放入 value 的高位
	// divisor % 8 的结果是 0-7,对应 frac_code 数组的索引
	index = divisor & 0x07;
	value |= (uint16_t)(frac_code[index] << 14);
	return FTDI_EncodeRequest(phost, 0x40, node->command, value, 0, 0);
}

static USBH_StatusTypeDef FTDI_SetData(USBH_HandleTypeDef *phost, FT232_CDC_Init_Node* node)
{
	uint16_t value = 0;
	uint8_t data_bits = node->param.data.dataBits;
	uint8_t parity    = node->param.data.parity;
	uint8_t stop_bits = node->param.data.stopBits;
	value |= data_bits;
	value |= (uint16_t)(parity << 8);
	value |= (uint16_t)(stop_bits << 11);
	return FTDI_EncodeRequest(phost, 0x40, node->command, value, 0, 0);
}

static USBH_StatusTypeDef FTDI_SetFlowCtrl(USBH_HandleTypeDef *phost, FT232_CDC_Init_Node* node)
{
	return FTDI_EncodeRequest(phost, 0x40, node->command, node->param.flow, 0, 0);
}

然后创建状态机变量与状态机执行函数:

// 写在FT232.c中
static void FTDI_Init_State_Machine(USBH_HandleTypeDef* phost);

FT232_CDC_Init_State_Machine sm = {
	.FT232_CDC_Init_State = FT232_CDC_UNDER_INIT,
	.currentNode = FT232_CDC_Init_Node_List,
	.currentNodeIndex = 0,
	.nodeLength = sizeof(FT232_CDC_Init_Node_List) / sizeof(FT232_CDC_Init_Node_List[0]),
};

static void FTDI_Init_State_Machine(USBH_HandleTypeDef* phost)
{
	if(sm.currentNode != NULL){
		USBH_StatusTypeDef status = sm.currentNode->method(phost, sm.currentNode);
		if(status == USBH_OK){
			sm.currentNode++;
			sm.currentNodeIndex++;
			if(sm.currentNodeIndex >= sm.nodeLength){
				sm.FT232_CDC_Init_State = FT232_CDC_INIT_COMPLETE;
				USBH_UsrLog("FTDI Init Complete");
			}
		}
		else if(status == USBH_BUSY){
			// 继续等待
		}
		else{
			sm.FT232_CDC_Init_State = FT232_CDC_INIT_ERROR;
			USBH_ErrLog("FTDI Init State Machine Error: %d", status);
		}
	}
}

然后把状态机放到USBH_CDC_Process中运行:

// 写在FT232.c中
static USBH_StatusTypeDef USBH_CDC_Process(USBH_HandleTypeDef *phost)
{
	USBH_StatusTypeDef status = USBH_BUSY;
	USBH_StatusTypeDef req_status = USBH_OK;
	CDC_HandleTypeDef *CDC_Handle = (CDC_HandleTypeDef *) phost->pActiveClass->pData;
	// 添加的代码
	if(sm.FT232_CDC_Init_State == FT232_CDC_UNDER_INIT){
		FTDI_Init_State_Machine(phost);
	}
	// 添加的代码结束
	switch (CDC_Handle->state)
	// 后略

下载程序,可以看到USB Debug Message变化如下:
在这里插入图片描述
至此FT232RL的初始化部分全部完成了。

用修改的STM32 USB HOST CDC库收发数据

发送

main.c中写一个简单的USB发送程序如下:

// 写在main.c中
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
extern USBH_HandleTypeDef hUsbHostHS;
extern FT232_CDC_Init_State_Machine sm;
uint8_t isSend = 0, sendover = 0;
/* USER CODE END PV */

int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MPU Configuration--------------------------------------------------------*/
  MPU_Config();

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_TIM2_Init();
  MX_TIM3_Init();
  MX_USART1_UART_Init();
  MX_USART2_UART_Init();
  MX_USART3_UART_Init();
  MX_USB_HOST_Init();
  /* USER CODE BEGIN 2 */
  const char* message = "Hello World!\r\n";
  uint32_t message_len = strlen(message);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
    MX_USB_HOST_Process();
    if(hUsbHostHS.gState == HOST_CLASS && isSend == 0 && sm.FT232_CDC_Init_State == FT232_CDC_INIT_COMPLETE){
		USBH_StatusTypeDef status = USBH_CDC_Transmit(&hUsbHostHS, message, message_len);
		if(status == USBH_OK){
			isSend = 1;
		}
		else{
			USBH_UsrLog("USBH_CDC_Transmit call FAILED with status: %d", status);
			HAL_Delay(1000); // 失败后等待一下再重试
		}
	}
	else if(hUsbHostHS.gState == HOST_CLASS && isSend == 1 && sm.FT232_CDC_Init_State == FT232_CDC_INIT_COMPLETE){
		if(sendover == 1){
			CDC_HandleTypeDef *CDC_Handle = (CDC_HandleTypeDef *) hUsbHostHS.pActiveClass->pData;
			USBH_URBStateTypeDef status = USBH_LL_GetURBState(&hUsbHostHS, CDC_Handle->DataItf.OutPipe);
			if(status == USBH_URB_DONE){
			  isSend = 0;
			  sendover = 0;
			  HAL_Delay(1000);
			}
		}

	}

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/* USER CODE BEGIN 4 */
void USBH_CDC_TransmitCallback(USBH_HandleTypeDef *phost)
{
    printf("Data transmission completed!\r\n");
    sendover = 1;
}
/* USER CODE END 4 */

如果前面的设置都没问题,PC上位机就能成功接收STM32H723发送的数据了:
在这里插入图片描述

接收

main.c中创建一个接收缓冲区和接收标志位:

	uint8_t recievebuf[64];
	uint8_t recieveEn = 0;

main函数的while(1)中添加接收的函数:

	if(hUsbHostHS.gState == HOST_CLASS && sm.FT232_CDC_Init_State == FT232_CDC_INIT_COMPLETE && recieveEn == 0){
		USBH_CDC_Receive(&hUsbHostHS, recievebuf, 64);
		recieveEn = 1;
	}

USBH_CDC_Receive为STM32 USB HOST CDC库的接收函数,这个函数不能每次循环都调用,STM32 USB HOST CDC库的接收有两个状态,分别为CDC_RECEIVE_DATACDC_RECEIVE_DATA_WAIT,如果每次循环都调用USBH_CDC_Receive,USB将会一直处于CDC_RECEIVE_DATA状态,导致无法完成一次接收。

USBH_StatusTypeDef USBH_CDC_Receive(USBH_HandleTypeDef *phost, uint8_t *pbuff, uint32_t length)
{
  USBH_StatusTypeDef Status = USBH_BUSY;
  CDC_HandleTypeDef *CDC_Handle = (CDC_HandleTypeDef *) phost->pActiveClass->pData;

  if ((CDC_Handle->state == CDC_IDLE_STATE) || (CDC_Handle->state == CDC_TRANSFER_DATA))
  {
    CDC_Handle->pRxData = pbuff;
    CDC_Handle->RxDataLength = length;
    CDC_Handle->state = CDC_TRANSFER_DATA;
    CDC_Handle->data_rx_state = CDC_RECEIVE_DATA;
    Status = USBH_OK;

#if (USBH_USE_OS == 1U)
    USBH_OS_PutMessage(phost, USBH_CLASS_EVENT, 0U, 0U);
#endif /* (USBH_USE_OS == 1U) */
  }
  return Status;
}

//USB接收的状态处理函数
static void CDC_ProcessReception(USBH_HandleTypeDef *phost)
{
  CDC_HandleTypeDef *CDC_Handle = (CDC_HandleTypeDef *) phost->pActiveClass->pData;
  USBH_URBStateTypeDef URB_Status = USBH_URB_IDLE;
  uint32_t length;

  switch (CDC_Handle->data_rx_state)
  {

    case CDC_RECEIVE_DATA:

      (void)USBH_BulkReceiveData(phost,
                                 CDC_Handle->pRxData,
                                 CDC_Handle->DataItf.InEpSize,
                                 CDC_Handle->DataItf.InPipe);

#if defined (USBH_IN_NAK_PROCESS) && (USBH_IN_NAK_PROCESS == 1U)
      phost->NakTimer = phost->Timer;
#endif  /* defined (USBH_IN_NAK_PROCESS) && (USBH_IN_NAK_PROCESS == 1U) */

      CDC_Handle->data_rx_state = CDC_RECEIVE_DATA_WAIT;

      break;

    case CDC_RECEIVE_DATA_WAIT:

      URB_Status = USBH_LL_GetURBState(phost, CDC_Handle->DataItf.InPipe);

      /*Check the status done for reception*/
      if (URB_Status == USBH_URB_DONE)
      {
        length = USBH_LL_GetLastXferSize(phost, CDC_Handle->DataItf.InPipe);

        if (((CDC_Handle->RxDataLength - length) > 0U) && (length == CDC_Handle->DataItf.InEpSize))
        {
          CDC_Handle->RxDataLength -= length;
          CDC_Handle->pRxData += length;
          CDC_Handle->data_rx_state = CDC_RECEIVE_DATA;
        }
        else
        {
          CDC_Handle->data_rx_state = CDC_IDLE;
          USBH_CDC_ReceiveCallback(phost);
        }

#if (USBH_USE_OS == 1U)
        USBH_OS_PutMessage(phost, USBH_CLASS_EVENT, 0U, 0U);
#endif /* (USBH_USE_OS == 1U) */
      }
#if defined (USBH_IN_NAK_PROCESS) && (USBH_IN_NAK_PROCESS == 1U)
      else if (URB_Status == USBH_URB_NAK_WAIT)
      {
        CDC_Handle->data_rx_state = CDC_RECEIVE_DATA_WAIT;

        if ((phost->Timer - phost->NakTimer) > phost->NakTimeout)
        {
          phost->NakTimer = phost->Timer;
          USBH_ActivatePipe(phost, CDC_Handle->DataItf.InPipe);
        }

#if (USBH_USE_OS == 1U)
        USBH_OS_PutMessage(phost, USBH_CLASS_EVENT, 0U, 0U);
#endif /* (USBH_USE_OS == 1U) */
      }
#endif /* defined (USBH_IN_NAK_PROCESS) && (USBH_IN_NAK_PROCESS == 1U) */
      else
      {
        /* .. */
      }
      break;

    default:
      break;
  }
}

然后定义USB的接收中断回调函数,在回调函数中重置标注位并打印接收到的数据:

void USBH_CDC_ReceiveCallback(USBH_HandleTypeDef *phost)
{
	uint16_t recieveLen = USBH_CDC_GetLastReceivedDataSize(phost) - 2;
	USBH_CDC_Transmit(&hUsbHostHS, recievebuf + 2, recieveLen);
	isSend = 1;
	recieveEn = 0;
}

通过上位机接收到的数据来看,STM32H723 USB接收到的一帧数据前两位为0x01 0x60,推测为CDC模式的帧头标志,故在接收时跳过前两位,只处理后面的数据。

在这里插入图片描述

后续待更新

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐