完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
1)实验平台:【正点原子】 NANO STM32F103 开发板
2)摘自《正点原子STM32 F1 开发指南(NANO 板-HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子 第三十一章 USB 虚拟串口实验 STM32F103 系列芯片都自带了 USB,不过 STM32F103 的 USB 都只能用来做设备,而不能用作主机。既便如此,对于一般应用来说已经足够了。本章,我们将向大家介绍如何在ALIENTEK NANO STM32 开发板上利用 STM32 自生的 USB 功能实现一个虚拟串口。本章分为如下几个部分: 31.1 USB 简介 31.2 硬件设计 31.3 软件设计 31.4 下载验证 31.1 USB 简介 USB ,是英文 Universal Serial BUS(通用串行总线)的缩写,而其中文简称为“通串线,是一个外部总线标准,用于规范电脑与外部设备的连接和通讯。是应用在 PC 领域的接口技术。USB 接口支持设备的即插即用和热插拔功能。USB 是在 1994 年底由英特尔、康柏、IBM、Microsoft 等多家公司联合提出的。 USB 发展到现在已经有 USB1.0/1.1/2.0/3.0 等多个版本。目前用的最多的就是 USB1.1 和 USB2.0,USB3.0 目前已经开始普及。STM32F103 自带的 USB 符合 USB2.0 规范。 标准 USB 共四根线组成,除 VCC/GND 外,另外为 D+,D-;这两根数据线采用的是差分电压的 方式进行数据传输的。在 USB 主机上,D-和 D+都是接了 15K 的电阻到低的,所以在没有设备 接入的时候,D+、D-均是低电平。而在 USB 设备中,如果是高速设备,则会在 D+上接一个 1.5K 的电阻到 VCC,而如果是低速设备,则会在 D-上接一个 1.5K 的电阻到 VCC。这样当设 备接入主机的时候,主机就可以判断是否有设备接入,并能判断设备是高速设备还是低速设备。 接下来,我们简单介绍一下 STM32 的 USB 控制器。 STM32F103 的 MCU 自带 USB 从控制器,符合 USB 规范的通信连接;PC 主机和微控制器 之间的数据传输是通过共享一专用的数据缓冲区来完成的,该数据缓冲区能被 USB 外设直接访 问。这块专用数据缓冲区的大小由所使用的端点数目和每个端点最大的数据分组大小所决定, 每个端点最大可使用 512 字节缓冲区(专用的 512 字节,和 CAN 共用),最多可用于 16 个单 向或 8 个双向端点。USB 模块同 PC 主机通信,根据 USB 规范实现令牌分组的检测,数据发送 /接收的处理,和握手分组的处理。整个传输的格式由硬件完成,其中包括 CRC 的生成和校验。 每个端点都有一个缓冲区描述块,描述该端点使用的缓冲区地址、大小和需要传输的字节 数。当 USB 模块识别出一个有效的功能/端点的令牌分组时,(如果需要传输数据并且端点已配 置)随之发生相关的数据传输。USB 模块通过一个内部的 16 位寄存器实现端口与专用缓冲区的 数据交换。在所有的数据传输完成后,如果需要,则根据传输的方向,发送或接收适当的握手 分组。在数据传输结束时,USB 模块将触发与端点相关的中断,通过读状态寄存器和/或者利用 不同的中断来处理。 USB 的中断映射单元:将可能产生中断的 USB 事件映射到三个不同的 NVIC 请求线上: 1、USB 低优先级中断(通道 20):可由所有 USB 事件触发(正确传输,USB 复位等)。固件 在处理中断前应当首先确定中断源。 2、USB 高优先级中断(通道 19):仅能由同步和双缓冲批量传输的正确传输事件触发,目 的是保证最大的传输速率。 3、USB 唤醒中断(通道 42):由 USB 挂起模式的唤醒事件触发。 USB 设备框图如图 31.1.1 所示: 图 31.1.1 USB 设备框图 STM32F1 USB 的其他介绍,请大家参考《STM32 中文参考手册》第 21 章内容,我们这里 就不再详细介绍了。 要正常使用 STM32F1 的 USB,就得编写 USB 驱动,而整个 USB 通信的详细过程是很复 杂的,本书篇幅有限,不可能在这里详细介绍,有兴趣的朋友可以去看看电脑圈圈的《圈圈教 你玩 USB》这本书,该书对 USB 通信有详细讲解。如果要我们自己编写 USB 驱动,那是一件 相当困难的事情,尤其对于从没了解过 USB 的人来说,基本上不花个一两年时间学习,是没法 搞定的。不过,ST 提供了我们一套完整的 USB 驱动库,通过这个库,我们可以很方便的实现 我们所要的功能,而不需要详细了解 USB 的整个驱动,大大缩短了我们的开发时间和精力。 ST 提供的 USB 驱动库,可以在: http://www.stmcu.org/document/detail/index/id-213156 这里 下载到(STSW-STM32121)。不过,我们已经帮大家下载到开发板光盘:7,STM32 参考资料→STM32 USB 学习资料,文件名:STSW-STM32121.zip(源代码)和 CD00158241.pdf(教程)。 STSW-STM32121.zip 这个压缩包文件里面,ST 提供了 8 个参考例程,如图 31.1.2 所示: 图 31.1.2 ST 提供的 USB 参考例程 ST 不但提供源码,还提供了说明文件:CD00158241.pdf(UM0424),专门讲解 USB 库怎 么使用。有了这些资料对我们了解 STM32F103 的 USB 会有不少帮助,尤其在不懂的时候,看 看 ST 的例程,会有意想不到的收获。本实验的 USB 部分就是移植 ST 的 Virtual_COM_Port 例 程相关部分而来,完成一个 USB 虚拟串口的功能。 31.2 硬件设计 本章实验功能简介:本实验利用 STM32 自带的 USB 功能,连接电脑 USB,虚拟出一个 USB 串口,实现电脑和开发板的数据通信。本例程功能完全同实验 4(串口通信实验),只不过串 口变成了 STM32 的 USB 虚拟串口。当 USB 连接电脑(USB 线插入 USB_SLAVE 接口),开 发板将通过 USB 和电脑建立连接虚拟出一个串口(注意:需要先安装:光盘5,软件资料1,软 件STM32 USB 虚拟串口驱动VCP_V1.4.0_Setup.exe 这个驱动软件),USB 和电脑连接成功 后,DS1 常亮。 在找到虚拟串口后,即可打开串口调试助手,实现同实验 4 一样的功能,即:STM32 通 过 USB 虚拟串口和上位机对话,STM32 在收到上位机发过来的字符串(以回车换行结束)后, 原原本本的返回给上位机。下载后,DS0 闪烁,提示程序在运行,同时每隔一定时间,通过 USB 虚拟串口输出一段信息到电脑。 所要用到的硬件资源如下: 1) 指示灯 DS0 、DS1 2) 串口 3) USB SLAVE 接口 前面 3 部分,在之前的实例中都介绍过了,我们在此就不介绍了。接下来看看我们电脑 USB 与 STM32 的 USB SLAVE 连接口。ALIENTEK NANO STM32F103 采用的是 5PIN 的 MicroUSB 接头,用来和电脑的 USB 相连接,连接电路如图 31.2.1 所示: 图 31.2.1 MicroUSB 接口与 STM32 的连接电路图 31.3 软件设计 本章,我们在:实验 4 串口通信显示实验的基础上修改,代码移植自 ST 官方例程:光盘 8,STM32 参考资料2,STM32 USB 学习资料 STM32_USB-FS-Device_Lib_V4.0.0Projects Virtual_COM_Port,我打开该例程即可知道 USB 相关的代码有哪些,如图 31.3.1 所示: 图 31.3.1 ST 官方例程 USB 相关代码 有了这个官方例程做指引,我们就知道具体需要哪些文件,从而实现本章例程。 首先,在本章例程(即实验 4 串口通信实验)的工程文件夹下面,新建 USB 文件夹,并 拷贝官方 USB 驱动库相关代码到该文件夹下,即拷贝:光盘→ 7,STM32 参考资料→STM32 USB 学习资料→ STM32_USB-FS-Device_Lib_V4.0.0→Libraries 文件夹下的 STM32_USB-FS Device_Driver 文件夹到该文件夹下面。 然后,在 USB 文件夹下,新建 CONFIG 文件夹存放 Virtual COM 实现相关代码,即: STM32_USB-FS-Device_Lib_V4.0.0→Projects→Virtual_COM_Port→src 文件夹下的部分代码: hw_config.c、u***_desc.c、u***_endp.c、u***_istr.c、u***_prop.c 和 u***_pwr.c 等 6 个.c 文件,同时 拷贝:STM32_USB-FS-Device_Lib_V4.0.0→Projects→Virtual_COM_Port→inc 文件夹下面的: hw_config.h、platform_config.h、u***_conf.h、u***_desc.h、u***_istr.h、u***_prop.h 和 u***_pwr.h 等 7 个头文件到 CONFIG 文件夹下,最后 CONFIG 文件夹下的文件如图 31.3.2 所示: 图 31.3.2 CONFIG 文件夹代码 之后,根据 ST 官方 Virtual_COM_Port 例程,在我们本章例程的基础上新建分组添加相关 代码,具体细节,这里就不详细介绍了,添加好之后,如图 31.3.3 所示: 图 31.3.3 添加 USB 驱动等相关代码 移植时,我们重点要修改的就是 CONFIG 文件夹下面的代码,USB_CORE 文件夹下的代 码一般不用修改。现在,我们先来简单介绍一下 USB_CORE 文件夹下的几个.c 文件。 u***_regs.c 文件,该文件主要负责 USB 控制寄存器的底层操作,里面有各种 USB 寄存器 的底层操作函数。 u***_init.c 文件,该文件里面只有一个函数:USB_Init,用于 USB 控制器的初始化,不过对 USB 控制器的初始化,是 USB_Init 调用用其他文件的函数实现的,USB_Init 只不过是把他们 连接一下罢了,这样使得代码比较规范。 u***_int.c 文件,该文件里面只有两个函数 CTR_LP 和 CTR_HP,CTR_LP 负责 USB 低优先 级中断的处理。而 CTR_HP 负责 USB 高优先级中断的处理。 u***_mem.c 文件,该文件用于处理 PMA 数据,PMA 全称为 Packet memory area,是 STM32 内部用于 USB/CAN 的专用数据缓冲区,该文件内也只有 2 个函数即: PMAToUserBufferCopy 和 UserToPMABufferCopy,分别用于将 USB 端点的数据传送给主机和主机的数据传送到 USB 端点。 u***_croe.c 文件,该文件用于处理 USB2.0 协议。 u***_sil.c 文件,该文件为 USB 端点提供简化的读写访问函数。 以上几个文件具有很强的独立性,除特殊情况,不需要用户修改,直接调用内部的函数即 可。接着我们介绍 CONFIG 文件夹里面的几个.c 文件。 hw_config.c 文件,该文件用于硬件的配置,比如初始化 USB 时钟、USB 中断、低功耗模 式处理等。 u***_desc.c 文件,该文件用于 Virtual Com 描述符的处理。 u***_endp.c 文件,该文件用于非控制传输,处理正确传输中断回调函数。 u***_pwr.c 文件,该文件用于 USB 控制器的电源管理; u***_istr.c 文件,该文件用于处理 USB 中断。 u***_prop.c 文件,该文件用于处理所有 Virtual Com 的相关事件,包括 Virtual Com 的初始 化、复位等等操作。 另外官方例程用到 stm32_it.c 来处理 USB 相关中断,包括两个中断服务函数,第一个是: USB_LP_CAN1_RX0_IRQHandler 函数,我们在该函数里面调用 USB_Istr 函数,用于处理 USB 发生的各种中断。另外一个就是 USBWakeUp_IRQHandler 函数,我们在该函数就做了一件事: 清除中断标志。为了方便,我们直接将 USB 中断相关代码,全部放到 hw_config.c 里面,所以, 本例程直接用不到 stm32_it.c。 USB 相关代码,就给大家介绍到这里,详细的介绍,请大家参考:CD00158241.pdf 这个文 档。 注意,以上代码,有些是经过修改了的,并非完全照搬官方例程。接着我们在工程文件里 面新建 USB_CORE 和 USB_CONFIG 分组,分别加入 USB STM32_USB-FS-Device_Driversrc 下面的代码和 USBCONFIG 下面的代码,然后把 USBSTM32_USB-FS-Device_Driverinc 和 USBCONFIG 文件夹加入头文件包含路径。 最后修改 main.c 里面代码如下: int main(void) { u16 t; u16 len; u16 times=0; u8 u***status=0; HAL_Init(); //初始化 HAL 库 Stm32_Clock_Init(RCC_PLL_MUL9); //设置时钟,72M delay_init(72); //初始化延时函数 uart_init(115200); //串口初始化为 115200 LED_Init(); //初始化与 LED 连接的硬件接口 printf("NANO STM32rn"); printf("USB Virtual USART TESTrn"); printf("USB Connecting...rn"); //提示 USB 开始连接 delay_ms(1800); USB_Port_Set(0); //USB 先断开 delay_ms(700); USB_Port_Set(1); //USB 再次连接 Set_USBClock(); USB_Interrupts_Config(); USB_Init(); while(1) { if(u***status!=bDeviceState)//USB 连接状态发生了改变. { u***status=bDeviceState;//记录新的状态 if(u***status==CONFIGURED) { printf("USB Connectedrn");//提示 USB 连接成功 LED1=0;//DS1 亮 }else { printf("USB disConnectedrn");//提示 USB 断开 LED1=1;//DS1 灭 } } if(USB_USART_RX_STA&0x8000) { len=USB_USART_RX_STA&0x3FFF;//得到此次接收到的数据长度 u***_printf("rn 您发送的消息为:%drnrn",len); for(t=0;t USB_USART_SendData(USB_USART_RX_BUF[t]); //以字节方式,发送给 USB } u***_printf("rnrn");//插入换行 USB_USART_RX_STA=0; }else { times++; if(times%5000==0) { u***_printf("rnNANO STM32 开发板 USB 虚拟串口实验rn"); u***_printf("正点原子@ALIENTEKrnrn"); } if(times%200==0)u***_printf("请输入数据,以回车键结束rn"); if(times%30==0)LED0=!LED0;//闪烁 LED,提示系统正在运行. delay_ms(10); } } } 在此部分代码用于实现我们在硬件设计部分提到的功能,USB 的配置通过三个函数完成: USB_Interrupts_Config()、Set_USBClock()和 USB_Init(),第一个函数用于设置 USB 唤醒中断和 USB 低优先级数据处理中断,Set_USBClock 函数用于 配置 USB 时钟,也就是从 72M 的主频 得到 48M 的 USB 时钟(1.5 分频)。最后 USB_Init()函数用于初始化 USB,最主要的就是调用 了 Virtual_Com_Port_init 函数,开启了 USB 部分的电源等。这里需要特别说明的是,USB 配置 并没有对 PA11 和 PA12 这两个 IO 口进行设置,是因为,一旦开启了 USB 电源(USB_CNTR 的 PDWN 位清零)PA11 和 PA12 将不再作为其他功能使用,仅供 USB 使用,所以在开启了 USB 电源之后不论你怎么配置这两个 IO 口,都是无效的。要在此获取这两个 IO 口的配置权,则需 要关闭 USB 电源,也就是置位 USB_CNTR 的 PDWN 位,我们通过 USB_Port_Set 函数来禁止/ 允许 USB 连接,在复位的时候,先禁止,再允许,这样每次我们按复位电脑都可以识别到 USB 鼠标,而不需要我们每次都拔 USB 线。USB_Port_Set 函数在 hw_config.c 里面实现,代码请参 考本例程源码。 USB 虚拟串口的数据发送,我们通过函数:USB_USART_SendData 来实现,该函数在 hw_config.c 里面实现,该函数代码如下: //发送一个字节数据到 USB 虚拟串口 void USB_USART_SendData(u8 data) { uu_txfifo.buffer[uu_txfifo.writeptr]=data; uu_txfifo.writeptr++; if(uu_txfifo.writeptr==USB_USART_TXFIFO_SIZE)//超过 buf 大小了,归零. { uu_txfifo.writeptr=0; } } 该函数实现发送 1 个字节到虚拟串口,这里,我们用到了一个 uu_txfifo 的结构体,该结构 体是我们在 hw_config 里面定义的一个 USB 虚拟串口发送数据 FIFO 结构体,定义如下: //定义一个 USB USART FIFO 结构体 typedef struct { u8 buffer[USB_USART_TXFIFO_SIZE]; //buffer vu16 writeptr; //写指针 vu16 readptr; //读指针 }_u***_usart_fifo; extern _u***_usart_fifo uu_txfifo; //USB 串口发送 FIFO 该结构体用于处理 USB 串口要发送的数据,所有要通过 USB 串口发送的数据,都将先存 放在该结构体的 buffer 数组(FIFO 缓存区)里面,USB_USART_TXFIFO_SIZE 定义了该数组 的大小,通过 writeptr 和 readptr 来控制 FIFO 的写入和读出,该结构体 buffer 数据的写入,是 通过 USB_USART_SendData 函数实现,而 buffer 数据的读出(然后发送到 USB)则是通过端 点 1 回调函数:EP1_IN_Callback 函数实现,该函数在 u***_endp.c 里面实现,代码如下: void EP1_IN_Callback (void) { u16 USB_Tx_ptr; u16 USB_Tx_length; if(uu_txfifo.readptr==uu_txfifo.writeptr) return; //无任何数据要发送,直接退出 if(uu_txfifo.readptr { USB_Tx_length=uu_txfifo.writeptr-uu_txfifo.readptr; //得到要发送的数据长度 }else //超过数组了 读指针>写指针 { USB_Tx_length=USB_USART_TXFIFO_SIZE-uu_txfifo.readptr;//发送的数据长度 } if(USB_Tx_length>VIRTUAL_COM_PORT_DATA_SIZE) //超过 64 字节? { USB_Tx_length=VIRTUAL_COM_PORT_DATA_SIZE; //此次发送数据量 } USB_Tx_ptr=uu_txfifo.readptr; //发送起始地址 uu_txfifo.readptr+=USB_Tx_length; //读指针偏移 if(uu_txfifo.readptr>=USB_USART_TXFIFO_SIZE) //读指针归零 { uu_txfifo.readptr=0; } UserToPMABufferCopy(&uu_txfifo.buffer[USB_Tx_ptr], ENDP1_TXADDR, USB_Tx_ length); SetEPTxCount(ENDP1, USB_Tx_length); SetEPTxValid(ENDP1); } 这个函数由 USB 中断处理相关函数调用,将要通过 USB 发送给电脑的数据拷贝到端点 1 的发送区,然后通过 USB 发送给电脑,从而实现串口数据的发送。因为 USB 每次传输数据长 度不超过 VIRTUAL_COM_PORT_DATA_SIZE,所以 USB 发送数据长度:USB_Tx_length 的最 大值只能是 VIRTUAL_COM_PORT_DATA_SIZE。 以上,就是 USB 虚拟串口的数据发送过程,而 USB 虚拟串口数据的接收,则是通过端点 3 来实现的,端点 3 的回调函数为 EP3_OUT_Callback,该函数也是在 u***_endp.c 里面定义,代 码如下: void EP3_OUT_Callback(void) { u16 USB_Rx_Cnt; USB_Rx_Cnt = USB_SIL_Read(EP3_OUT, USB_Rx_Buffer); //得到 USB 接收到的数据及其长度 USB_To_USART_Send_Data(USB_Rx_Buffer, USB_Rx_Cnt); //处理数据(其实就是保存数据) SetEPRxValid(ENDP3); //时能端点 3 的数据接收 } 该函数也是被 USB 中断处理调用,该函数通过调用 USB_To_USART_Send_Data 函数,实 现 USB 接收数据的保存,USB_To_USART_Send_Data 函数在 hw_config.c 里面实现,代码如下: //用类似串口 1 接收数据的方法,来处理 USB 虚拟串口接收到的数据. u8 USB_USART_RX_BUF[USB_USART_REC_LEN]; //接收缓冲,最大 USART_REC_LEN 个字节. //接收状态 //bit15, 接收完成标志 //bit14, 接收到 0x0d //bit13~0, 接收到的有效字节数目 u16 USB_USART_RX_STA=0; //接收状态标记 //处理从 USB 虚拟串口接收到的数据 //databuffer:数据缓存区 //Nb_bytes:接收到的字节数. void USB_To_USART_Send_Data(u8* data_buffer, u8 Nb_bytes) { u8 i; u8 res; for(i=0;i res=data_buffer; if((USB_USART_RX_STA&0x8000)==0) //接收未完成 { if(USB_USART_RX_STA&0x4000) //接收到了 0x0d { if(res!=0x0a)USB_USART_RX_STA=0;//接收错误,重新开始 else USB_USART_RX_STA|=0x8000; //接收完成了 }else //还没收到 0X0D { if(res==0x0d)USB_USART_RX_STA|=0x4000; else { USB_USART_RX_BUF[USB_USART_RX_STA&0X3FFF]=res; USB_USART_RX_STA++; if(USB_USART_RX_STA>(USB_USART_REC_LEN-1)) USB_USART_RX_STA=0;//接收数据错误,重新开始接收 } } } } } 该函数接收数据的方法,同第九章串口通信实验的串口中断接收数据方法完全一样,在这 里就不详细介绍了,请参考第九章相关内容即可。USB_To_USART_Send_Data 函数类似于串口 通信实验的串口中断服务函数(USART1_IRQHandler),完成 USB 虚拟串口的数据接收。 软件设计部分,就给大家介绍到这里。 31.4 下载验证 本例程的测试,需要在电脑上先安装 ST 提供的 USB 虚拟串口驱动软件,该软件路径:光 盘→5,软件资料→1,软件→STM32 USB 虚拟串口驱动→ VCP_V1.4.0_Setup.exe,双击安装 即可。 然后,在代码编译成功之后,我们下载代码到 NANO STM32 V1 上,然后将 USB 数据线, 插入 USB_SLAVE 口,连接电脑和开发板(注意:不是插 USB_JTAG 端口!),此时电脑会 提示找到新硬件,并自动安装驱动。不过,如果自动安装不成功(有惊叹号),如图 31.4.1 所 示: 图 31.4.1 自动安装失败 此时,我们可手动选择驱动(以 WIN7 为例),进行安装,在如图 53.4.1 所示的条目上面, 右键→更新驱动程序软件→浏览计算机以查找驱动程序软件→浏览,选择 STM32 虚拟串口的驱 动的路径为:C:Program Files (x86)STMicroelectronicsSoftwareVirtual comport driverWIN7,然 后点击下一步,即可完成安装。安装完成后,可以看到设备管理器里面多出了一个 STM32 的 虚拟串口,如图 31.4.2 所示: 图 31.4.2 发现 STM32 USB 虚拟串口 如图 31.4.2,STM32 通过 USB 虚拟的串口,被电脑识别了,端口号为:COM60(可变), 字符串名字为:STMicroelectronics Virtual COM Port(固定)。此时,开发板的 DS1 常亮,以 表示 USB 已连接,同时开发板串口 1 会打印 USB 连接的状态信息,如图 31.4.3 所示: 图 31.4.3 USB 虚拟串口连接成功 我们另外打开多一个 XCOM,选择 USB 的虚拟串口,这里选择 COM60(需根据自己的电 脑识别到的串口号选择),并打开串口(注意:波特率可以随意设置),就可以进行测试了, 如图 31.4.4 所示: 图 31.4.4 STM32 虚拟串口通信测试 可以看到,我们的串口调试助手,收到了来自 STM32 开发板的数据,同时,按发送按钮 (串口助手必须勾选:发送新行),也可以收到电脑发送给 STM32 的数据(原样返回),说 明我们的实验是成功的。实验现象同第九章完全一样。 至此,USB 虚拟串口实验就完成了,通过本实验,我们就可以利用 STM32 的 USB,直接 和电脑进行数据互传了,具有广泛的应用前景。 |
|
相关推荐
|
|
1812 浏览 0 评论
如何解决MPU-9250与STM32通讯时,出现HAL_ERROR = 0x01U
952 浏览 1 评论
hal库中i2c卡死在HAL_I2C_Master_Transmit
1340 浏览 1 评论
LL库F030进行3个串口收发,2个串口为232,一个为485,长时间后,会出现串口1停止运行,另外两个正常,只有重启复原
1801 浏览 1 评论
560 浏览 0 评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-11-15 06:14 , Processed in 0.590904 second(s), Total 64, Slave 46 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号