当前位置: 首页 > news >正文

STM32HAL库 -- 8.串口UART通信并开启printf功能

目录

1.简介

2.串口和UART 

2.1串口的简介

2.2UART的简介

2.3UART通信协议

2.3.1波特率

2.3.2空闲位

2.3.3起始位

2.3.4数据位

2.3.5校验位

2.3.6停止位

3.STM32的UART

4.HAL库中常用的操作UART的函数

4.1UART初始化函数 -- HAL_UART_Init

4.2硬件初始化回调函数 -- HAL_UART_MspInit

4.3发送数据函数 -- HAL_UART_Transmit 

4.4接收数据的函数 -- HAL_UART_Receive

4.5使能中断源 -- __HAL_UART_ENABLE_IT

4.6获取UART的状态标志 -- __HAL_UART_GET_FLAG

4.7清除指定的 UART 状态标志位 -- __HAL_UART_CLEAR_FLAG

5.发送接收数据实验 并 打开串口printf功能

5.1硬件连接

5.2代码编写

5.2.1配置UART

5.2.2中断函数

5.2.3重定义fputc -- 支持printf函数

6.整体代码 && 实验现象

6.1uart1.h

6.2uart1.c

6.3main.c

6.4实验现象


1.简介

        这个文章会介绍串口通信,然后编写代码完成单片机通过串口发送到电脑,电脑发送数据到单片机的过程。

        作者使用的开发板是正点原子的精英版,写文章用于记录学习和经验分享。

2.串口和UART 

2.1串口的简介

        串口(Serial Port) 是一种常见的数据通信接口,用于实现设备之间的串行通信(Serial Communication)。所谓“串行”,是指数据是按 一位一位的顺序进行发送或接收的,与并行通信(一次性传输多个比特)相对。

2.2UART的简介

        UART(Universal Asynchronous Receiver/Transmitter),UART 全称是通用异步收发器,是一种硬件模块,用于实现异步串行通信。它是实现串口通信的核心逻辑电路。

        UART也是一种广泛应用于嵌入式系统中的异步串行通信协议,通过定义数据帧格式和波特率实现设备之间的点对点数据传输。它仅需两根信号线(TXD发送、RXD接收),无需共享时钟,具有结构简单、使用方便、兼容性强等优点,常用于调试输出、传感器通信、模块交互等场景。

        设备1的TX(发送端)接入设备2的RX(接收端),设备1的RX(接收端)接入设备2的(发送端),这样就实现了全双工通信。也可以单工通信,就是一发一收,接通一路TX、RX即可。

2.3UART通信协议

        UART的一个完整的数据帧通常包括起始位、数据位、可选的校验位和停止位,通信双方必须保持波特率和数据格式一致才能正常工作。

2.3.1波特率

        波特率(Baud Rate) 是串行通信中的一个关键参数,用于表示每秒传输的信号变化次数,单位是 bps(bits per second) 。在 UART 通信中,波特率决定了数据传输的速度。

        在 UART 通信中,发送端和接收端必须设置相同的波特率,否则会导致数据接收不到或者接收内容与发送的内容不一致的问题。

2.3.2空闲位

        空闲位(Idle State) 是指当串口未发送或接收任何数据时,数据线(通常是 TXD 或 RXD)处于高电平状态。数据帧以一个起始位(Start Bit,低电平)开始,空闲位作为帧与帧之间的间隔,确保接收方能正确识别下一帧数据。

2.3.3起始位

        起始位(Start Bit) 是 UART 数据帧中的第一个信号位,用于通知接收端:一帧数据即将开始传输。电平状态固定为低电平(0);长度为 1 位。

2.3.4数据位

        数据位(Data Bits) 是 UART 数据帧中承载实际信息的部分,表示一次通信中传输的有效数据内容。数据的长度通常为 5 到 8 位,8位用的最广泛,因为正好是1字节的大小。

        发送的顺序是低位先发(LSB First)。例如字符 'A' 的 ASCII 码为:0x41(十六进制)二进制表示为:01000001     UART 发送顺序(LSB 先发):1 0 0 0 0 0 1 0

2.3.5校验位

        校验位(Parity Bit)是 UART 数据帧中一个可选的错误检测位,用于在通信过程中简单判断数据是否出错。校验位的位置在数据位之后、停止位之前。可以选择无校验(None)、偶校验(Even)、奇校验(Odd)。

校验方式要求总“1”个数为...示例(数据位:01000001 → 有两个“1”)
偶校验(Even)偶数个“1”校验位 = 0(使总数仍为偶数)
奇校验(Odd)奇数个“1”校验位 = 1(使总数变为奇数)

        接收端收到后会重新计算并比对校验位,若不一致则判定为传输错误。即使数据吃错,也不会纠正错误,就仅仅简单的校验而已。

2.3.6停止位

        停止位(Stop Bit) 是 UART 数据帧中的最后一个部分,用于标识一次数据传输的结束,同时为下一次通信提供时间间隔。电平状态固定为高电平(1);长度可选为 1 位、1.5 位 或 2 位。

3.STM32的UART

        作者使用的是STM32F103ZET6为微控制器的开发板,该微控制器内置了3个通用同步/异步收发器(USART1USART2USART3),和2个通用异步收发器(UART4UART5)。

        UART 只支持异步串行通信,而USART 是 UART 的“升级版”,既支持异步也支持同步通信,功能更强大。在这里就把USART当作UART用就好。

        附上框图,虽然我没咋看懂。

4.HAL库中常用的操作UART的函数

        打开HAL库的stm32f1xx_hal_uart.c文件。

4.1UART初始化函数 -- HAL_UART_Init

HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);

        这个函数是 STM32 HAL 库中用于 初始化 UART 外设 的核心函数。主要配置使用哪个UART、波特率、数据位、校验位、停止位、CTS/RTS硬件流控位和收发模式。

        函数的参数 UART_HandleTypeDef *huart 是指向一个 UART 句柄结构体的指针,该结构体包含了 UART 的配置信息和运行状态。

        函数的返回值(HAL_StatusTypeDef):

返回值含义
HAL_OK初始化成功
HAL_ERROR初始化失败(如参数错误、硬件故障)
HAL_BUSYUART 正在被使用
HAL_TIMEOUT操作超时

4.2硬件初始化回调函数 -- HAL_UART_MspInit

__weak void HAL_UART_MspInit(UART_HandleTypeDef *huart);

        这个函数是 HAL 库中 UART 的底层硬件初始化函数,必须由用户根据实际硬件平台实现,用于配置 GPIO、时钟、中断等。这是一个公共的函数,通过HAL_UART_Init函数进行调用。同时也是虚函数,意思是要我们自己写函数的内容。

        函数的参数 UART_HandleTypeDef *huart 是指向一个 UART 句柄结构体的指针,该结构体包含了 UART 的配置信息和运行状态。

4.3发送数据函数 -- HAL_UART_Transmit 

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)

        该函数用于 通过 UART 以阻塞方式发送一组数据(一个字节数组),直到全部发送完成或发生错误/超时。使用简单,适合 不使用中断或 DMA 的场景。

参数说明:

参数名类型说明
huartUART_HandleTypeDef*UART 句柄指针,指向已配置好的 UART 实例(如 &huart1
pDatauint8_t*指向要发送数据的缓冲区(字节数组)
Sizeuint16_t要发送的数据长度(字节数)
Timeoutuint32_t等待发送完成的最大时间(单位:ms),设为 HAL_MAX_DELAY 表示无限等待

4.4接收数据的函数 -- HAL_UART_Receive

HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);

        该函数用于 通过 UART 以阻塞方式接收指定数量的字节数据。会一直等待直到:接收到指定数量的数据(Size 字节)或发生错误 / 超时。

参数说明:

huartUART_HandleTypeDef*UART 句柄指针(如 &huart1
pDatauint8_t*指向接收缓冲区的指针(用于存储接收到的数据)
Sizeuint16_t要接收的数据长度(字节数)
Timeoutuint32_t接收等待最大时间(单位:ms),设为 HAL_MAX_DELAY 表示无限等待

4.5使能中断源 -- __HAL_UART_ENABLE_IT

__HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__)

        这是一个宏函数。该宏用于直接使能 UART 的某个中断源。

参数说明:

__HANDLE__:就是 UART的句柄指针 UART_HandleTypeDef* 。

__INTERRUPT__:中断源 

宏定义中断名称触发条件是否常用
UART_IT_CTSCTS 变化中断当 CTS 引脚电平变化时触发(硬件流控相关)❌ 较少使用
UART_IT_LBDLIN 断点检测中断检测到 LIN 总线断点标志(用于 LIN 协议)❌ 特定场景
UART_IT_TXE发送缓冲区空中断当发送寄存器为空,可以写入新数据时触发✅ 常用
UART_IT_TC发送完成中断整个数据帧传输完成后触发✅ 常用
UART_IT_RXNE接收缓冲区非空中断当收到一个字节数据时触发✅ 最常用
UART_IT_IDLE空闲总线检测中断当 UART 接收线上检测到空闲时触发(常用于接收不定长数据帧)✅ 高级应用
UART_IT_PE校验错误中断接收到的数据发生奇偶校验错误时触发⚠️ 错误检测
UART_IT_ERR错误中断(综合)包括帧错误、噪声错误、溢出错误等⚠️ 错误处理

4.6获取UART的状态标志 -- __HAL_UART_GET_FLAG

__HAL_UART_GET_FLAG(__HANDLE__, __FLAG__)

        用于读取 UART 状态标志位的宏,常用于轮询或中断中判断事件是否发生,是实现串口通信控制和错误检测的关键工具。

参数说明:

__HANDLE__:就是 UART的句柄指针 UART_HandleTypeDef* 。

__FLAG__:

宏定义对应标志描述
UART_FLAG_CTSCTS 标志位CTS 引脚状态变化(硬件流控)
UART_FLAG_LBDLIN 断点标志LIN 总线断点检测
UART_FLAG_TXE发送缓冲区空标志表示可以写入新数据
UART_FLAG_TC发送完成标志整帧数据发送完成
UART_FLAG_RXNE接收缓冲区非空标志表示已接收到一个字节
UART_FLAG_IDLE空闲总线标志UART 总线空闲检测(常用于不定长接收)
UART_FLAG_ORE溢出错误标志接收缓冲区溢出
UART_FLAG_NE噪声错误标志接收到噪声干扰
UART_FLAG_FE帧错误标志数据格式错误
UART_FLAG_PE校验错误标志奇偶校验失败

4.7清除指定的 UART 状态标志位 -- __HAL_UART_CLEAR_FLAG

__HAL_UART_CLEAR_FLAG(__HANDLE__, __FLAG__)

        该宏用于 清除指定的 UART 状态标志位。在某些中断或轮询操作中,需要手动清除标志以避免重复触发或状态错误。

参数说明:

__HANDLE__:就是 UART的句柄指针 UART_HandleTypeDef* 。

__FLAG__:

宏定义对应标志是否需要手动清除说明
UART_FLAG_CTSCTS 标志✅ 是清除 CTS 中断标志
UART_FLAG_LBDLIN 断点标志✅ 是清除 LIN 检测标志
UART_FLAG_TC发送完成标志✅ 是清除发送完成标志
UART_FLAG_IDLE空闲总线标志✅ 是常用于接收不定长数据帧后需清除
UART_FLAG_RXNE接收缓冲区非空标志❌ 否读取 DR 寄存器自动清除
UART_FLAG_TXE发送缓冲区空标志❌ 否写入 DR 自动清除
UART_FLAG_ORE溢出错误标志✅ 是清除溢出标志
UART_FLAG_NE噪声错误标志✅ 是清除噪声错误标志
UART_FLAG_FE帧错误标志✅ 是清除帧错误标志
UART_FLAG_PE校验错误标志✅ 是清除奇偶校验错误标志

5.发送接收数据实验 并 打开串口printf功能

5.1硬件连接

        一般地,STM32处理器的PA9和PA10就是对应的USART的TX和RX引脚。而且带有type-c USB线直接连接到电脑上进行调试的功能。如果读者使用的是最小系统板,就用USB转TTL再进行连线到核心板就好,功能是一样的。

5.2代码编写

5.2.1配置UART

        使用上面介绍的HAL_UART_Init配置UART1。

UART_HandleTypeDef g_uart1_handle;						//UART1句柄
/*** @brief       串口1初始化函数* @param       baudrate: 波特率, 根据自己需要设置波特率值* @retval      无*/
void uart1_init(unsigned int baudrate)
{g_uart1_handle.Instance = USART1;						//配置的是USART1g_uart1_handle.Init.BaudRate = baudrate;				//波特率g_uart1_handle.Init.WordLength = UART_WORDLENGTH_8B;	//数据位为8位g_uart1_handle.Init.StopBits = UART_STOPBITS_1;			//停止位为1位g_uart1_handle.Init.Parity = UART_PARITY_NONE;			//无校验位g_uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;	//不使用CTS/RTS硬件流控位g_uart1_handle.Init.Mode = UART_MODE_TX_RX;				//UART发送和接收HAL_UART_Init(&g_uart1_handle);							//调用初始化函数进行初始化
}

        再使用HAL_UART_MspInit进行底层硬件初始化,进行打开时钟,配置GPIO和打开中断。

        对于发送数据引脚PA9,要求是推挽复用输出模式,上拉模式,速度中速高速都行。

        对于 接收数据引脚PA10,要求是复用输入模式,上拉模式。

        要使用HAL_NVIC_EnableIRQ函数打开USART1的中断,并设置中断优先级,然后再使用宏函数打开具体的UART中断。这里打开的是空闲中断和接收中断。

*** @brief       UART底层初始化函数* @param       huart: UART句柄类型指针* @note        此函数会被HAL_UART_Init()调用*              完成时钟使能,引脚配置,中断配置* @retval      无*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{if(huart->Instance == USART1){/*	1.打开时钟	*/__HAL_RCC_USART1_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/*	2.配置GPIO	*/GPIO_InitTypeDef gpio_handle = {0};gpio_handle.Pin = UART1_TX_PIN;gpio_handle.Mode = GPIO_MODE_AF_PP;gpio_handle.Pull = GPIO_PULLUP;gpio_handle.Speed = GPIO_SPEED_FREQ_MEDIUM;HAL_GPIO_Init(UART1_TX_PORT, &gpio_handle);gpio_handle.Pin = UART1_RX_PIN;gpio_handle.Mode = GPIO_MODE_AF_INPUT;gpio_handle.Pull = GPIO_PULLUP;HAL_GPIO_Init(UART1_RX_PORT, &gpio_handle);/*	3.打开中断	*/HAL_NVIC_SetPriority(USART1_IRQn, 2, 2);HAL_NVIC_EnableIRQ(USART1_IRQn);__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);		//打开接收中断__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);		//打开空闲中断}
}

5.2.2中断函数

        打开了接收中断之后,当接收到数据时,就跳到中断函数进行接收数据。当接收完成后,即进入空闲中断。在空闲中断中可进行其他操作。

        这里的逻辑比较简单,当触发了接收缓冲区非空中断(接收到1字节数据),就进行接收数据的操作。数据一字节一字节的拷贝进g_uart1_rx_buf数组中,用于存储接收的数据。接收中断的标志会自动的清除。

        当触发空闲总线检测中断(数据接收完成),就进行打印操作,并且清空用于存储接收数据的数组。然后要手动的清除空闲中断标志,否则程序会一直在这里。

/*** @brief       串口1中断服务函数* @note        在此使用接收中断,实现不定长数据收发* @param       无* @retval      无*/
void USART1_IRQHandler(void)
{unsigned char recv_data;if(__HAL_UART_GET_FLAG(&g_uart1_handle, UART_FLAG_RXNE) != RESET){if(g_uart1_rx_len >= sizeof(g_uart1_rx_buf))g_uart1_rx_len = 0;HAL_UART_Receive(&g_uart1_handle, &recv_data, 1, 1000);g_uart1_rx_buf[g_uart1_rx_len++] = recv_data;}if(__HAL_UART_GET_FLAG(&g_uart1_handle, UART_FLAG_IDLE) != RESET){printf("recv:%s\r\n", g_uart1_rx_buf);uart1_clear_rx_buf();__HAL_UART_CLEAR_IDLEFLAG(&g_uart1_handle);}
}

5.2.3重定义fputc -- 支持printf函数

        fputc() 是 printf() 的底层输出函数通过重写该函数,可以将 printf() 输出重定向到串口,便于调试和日志输出。

/*** @brief       重定义fputc函数* @note        printf函数最终会通过调用fputc输出字符串到串口*/
int fputc(int ch, FILE *f)
{while ((USART1->SR & 0X40) == 0);                 /* 等待上一个字符发送完成 */USART1->DR = (uint8_t)ch;                         /* 将要发送的字符 ch 写入到DR寄存器 */return ch;
}

        USART1->SR 是 UART 的状态寄存器(Status Register);0x40 对应的是 TXE(Transmit Data Register Empty)标志位。当 TXE=1 表示 DR 寄存器为空,即可以写入新数据。将字符 ch 写入数据寄存器(Data Register),开始通过USART1向外发送。

6.整体代码 && 实验现象

6.1uart1.h

        主要进行了UART1的引脚宏定义和函数声明。

#ifndef __UART1_H__
#define __UART1_H__#include "stdio.h"
#include "sys.h"/**********************宏定义**************************************/
/*		引脚定义	*/
#define		UART1_TX_PORT			GPIOA
#define		UART1_TX_PIN			GPIO_PIN_9#define		UART1_RX_PORT			GPIOA
#define		UART1_RX_PIN			GPIO_PIN_10/* UART收发缓冲大小 */
#define UART1_RX_BUF_SIZE            128
#define UART1_TX_BUF_SIZE            64/* 错误代码 */
#define UART1_EOK                     0      /* 没有错误 */
#define UART1_ERROR                   1      /* 通用错误 */
#define UART1_ETIMEOUT                2      /* 超时错误 */
#define UART1_EINVAL                  3      /* 参数错误 *//**********************函数声明**************************************/
void uart1_init(unsigned int baudrate);            /* 串口初始化函数 */#endif

6.2uart1.c

        操作USART1的函数。

#include "sys.h"
#include "uart1.h"
#include "string.h"/*		全局变量		*/
unsigned char g_uart1_rx_buf[UART1_RX_BUF_SIZE];		//保存接收的数据
unsigned short g_uart1_rx_len = 0;						//接收数据的长度
UART_HandleTypeDef g_uart1_handle;						//UART1句柄/*** @brief       重定义fputc函数* @note        printf函数最终会通过调用fputc输出字符串到串口*/
int fputc(int ch, FILE *f)
{while ((USART1->SR & 0X40) == 0);                                       /* 等待上一个字符发送完成 */USART1->DR = (uint8_t)ch;                                               /* 将要发送的字符 ch 写入到DR寄存器 */return ch;
}/*** @brief       串口1初始化函数* @param       baudrate: 波特率, 根据自己需要设置波特率值* @retval      无*/
void uart1_init(unsigned int baudrate)
{g_uart1_handle.Instance = USART1;						//配置的是USART1g_uart1_handle.Init.BaudRate = baudrate;				//波特率g_uart1_handle.Init.WordLength = UART_WORDLENGTH_8B;	//数据位为8位g_uart1_handle.Init.StopBits = UART_STOPBITS_1;			//停止位为1位g_uart1_handle.Init.Parity = UART_PARITY_NONE;			//无校验位g_uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;	//不使用CTS/RTS硬件流控位g_uart1_handle.Init.Mode = UART_MODE_TX_RX;				//UART发送和接收HAL_UART_Init(&g_uart1_handle);							//调用初始化函数进行初始化
}/*** @brief       UART底层初始化函数* @param       huart: UART句柄类型指针* @note        此函数会被HAL_UART_Init()调用*              完成时钟使能,引脚配置,中断配置* @retval      无*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{if(huart->Instance == USART1){/*	1.打开时钟	*/__HAL_RCC_USART1_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/*	2.配置GPIO	*/GPIO_InitTypeDef gpio_handle = {0};gpio_handle.Pin = UART1_TX_PIN;gpio_handle.Mode = GPIO_MODE_AF_PP;gpio_handle.Pull = GPIO_PULLUP;gpio_handle.Speed = GPIO_SPEED_FREQ_MEDIUM;HAL_GPIO_Init(UART1_TX_PORT, &gpio_handle);gpio_handle.Pin = UART1_RX_PIN;gpio_handle.Mode = GPIO_MODE_AF_INPUT;gpio_handle.Pull = GPIO_PULLUP;HAL_GPIO_Init(UART1_RX_PORT, &gpio_handle);/*	3.打开中断	*/HAL_NVIC_SetPriority(USART1_IRQn, 2, 2);HAL_NVIC_EnableIRQ(USART1_IRQn);__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);		//打开接收中断__HAL_UART_ENABLE_IT(huart, UART_IT_IDLE);		//打开空闲中断}
}/*** @brief       UART1接收缓冲区清除* @param       无* @retval      无*/
void uart1_clear_rx_buf(void)
{memset(g_uart1_rx_buf, 0, sizeof(g_uart1_rx_buf));g_uart1_rx_len = 0;
}/*** @brief       串口1中断服务函数* @note        在此使用接收中断,实现不定长数据收发* @param       无* @retval      无*/
void USART1_IRQHandler(void)
{unsigned char recv_data;if(__HAL_UART_GET_FLAG(&g_uart1_handle, UART_FLAG_RXNE) != RESET){if(g_uart1_rx_len >= sizeof(g_uart1_rx_buf))g_uart1_rx_len = 0;HAL_UART_Receive(&g_uart1_handle, &recv_data, 1, 1000);g_uart1_rx_buf[g_uart1_rx_len++] = recv_data;}if(__HAL_UART_GET_FLAG(&g_uart1_handle, UART_FLAG_IDLE) != RESET){printf("recv:%s\r\n", g_uart1_rx_buf);uart1_clear_rx_buf();__HAL_UART_CLEAR_IDLEFLAG(&g_uart1_handle);}
}

6.3main.c

        在主函数中调用uart1_init函数,传递的参数115200是波特率。

#include "main.h"int main(void)
{HAL_Init();                         /* 初始化HAL库 */stm32_clock_init(RCC_PLL_MUL9); 	/* 设置时钟, 72Mhz */uart1_init(115200);while(1){delay_ms(10);}
}

6.4实验现象

        首先在KEIL5中魔术棒中勾选 Use MicroLIB选项。

        写好程序之后下载程序到开发板,然后打开电脑的串口调试助手,并且按照上面配置的波特率、停止位、数据位、校验位打开串口。

        之后发送数据,对应的接收区会有 recv:[数据]返回。就正常了。

相关文章:

  • 一次使用 RAFT 和 Qwen3 实现端到端领域RAG自适应
  • Nginx 基础知识
  • AWS认证系列:考点解析 - cloud trail,cloud watch,aws config
  • RA4M2开发IOT(6)----涂鸦模组快速上云
  • 肖臻《区块链技术与应用》第六讲:比特币网络
  • EXPLAIN优化 SQL示例
  • moduo之Socket类以及Sockets命名空间
  • [project-based-learning] docs | 教程列表 | 格式规范 | 锚点分类体系
  • VTK链接程序问题记录
  • 元素-标签-复制
  • [Linux] Vim编辑器 Linux输入输出重定向
  • Nginx-5 Nginx 的4层反向代理
  • 【node】Mac m1 安装nvm 和node
  • 64-Oracle Redo Log
  • 示波器测量市电需要隔离变压器
  • langchain从入门到精通(十三)——Runnable组件
  • 提升 RAG 检索质量的 “七种武器”
  • Java面试复习:基础、面向对象、多线程、JVM与Spring核心考点
  • 关于Spring JBDC
  • Unity Addressable使用之检测更新流程
  • 怎么把qq空间做成企业网站/爱站网seo工具
  • 做网站一般是怎么盈利/网站维护的主要内容
  • 濮阳做网站的公司有哪些/超链接友情外链查询
  • 免费做网站哪家好/佛山做网站的公司哪家好
  • 临海受欢迎营销型网站建设/按效果付费的网络推广方式
  • 湖南做网站 多少钱磐石网络/公众号推广平台