良许Linux教程网 干货合集 对串口接收FIFO处理机制的解读

对串口接收FIFO处理机制的解读

一、FIFO队列

FIFO是First Input First Output(先入先出)的缩写,是一种常见的队列数据结构。

FIFO队列主要用于不同时钟域之间的数据传输,特别适用于一个时钟域读写速度快,另一个时钟域读写速度慢的场景。

FIFO的本质操作是将接收到的数据存储在一个线性数组中,在读取或写入时通过指针的自增来遍历数据。

它的作用是作为一个缓冲区,在数据传输过程中防止数据丢失,并且可以减少频繁的总线操作。同时,FIFO也适合用于支持DMA(直接内存访问)操作。

在FIFO中,需要理解两个重要的成员:

  1. 宽度:指一次读写操作的数据位数。

  2. 深度:表示FIFO队列可以存储多少个宽度的数据。例如,一个深度为8的FIFO可以存储8个16位宽度的数据。

第一类、FIFO处理机制如下:

FIFO信息的定义:

/*
该结构体定义成员有
缓存区,
长度,
输出,
输入的计数。
*/
typedef struct fifo_t {
    uint8_t *buf;
 uint32_t size;
 uint32_t in;
 uint32_t out;
} _fifo_str;
#define min(x,y) ((x) 
1234567891011121314

1、初始化FIFO

fifo_str fifo_str;

int FifoInit(uint8_t *fifo_addr, uint32_t fifo_size)//初始化fifo
{
 _fifo_str *p = &fifo_str;
 
 if(fifo_addr == NULL || fifo_size == 0)//判断数据是否为空
  return -1;

 memset((char *)p, 0, sizeof(_fifo_str));//初始化结构体
 p->buf = fifo_addr;//对应宽度
    p->in = 0;//输入计数
    p->out = 0;//输出计数
    p->size = fifo_size;//对应深度
 return 0;
}
12345678910111213141516

2、数据的长度获取

//数据的实际使用数据空间长度
int FifoDataLen(void)
{
 _fifo_str *p = &fifo_str;
 return (p->in - p->out);//输入计数-输出计数
}
//剩余数据空间长度
int FifoSpaceLen(void)
{
 _fifo_str *p = &fifo_str;
 
 return (p->size - (p->in - p->out));//定义长度-(实际长度)
}
12345678910111213

3、FIFO的进和出处理

//获取fifo数据
//数据的内容缓存区,要读的长度
int FifoRead(uint8_t *buf, uint32_t len)
{
 uint32_t i = 0, j = 0;
 _fifo_str *p = &fifo_str;

 j = (p->out % p->size);//获取剩余空间长度未读量
 len = min(len, p->in - p->out);//防止长度为超出实际拥有的数据长度,即让读取的长度在  (0size - j);//获取实际内容的长度,的数据长度
 memcpy(buf, p->buf + j, i);//将数据通道里的数据拷贝给缓存区
 memcpy(buf + i, p->buf, len - i);//将未有数据的区域存入,对应为写入数据的区域数据(即,有数据的填数据,没数据的地方补0)
 p->out += len;//已读的数据量
 return len;//实际读到的数据长度
}
//对fifo写入数据
int FifoWrite(uint8_t *buf, uint32_t len)
{
 uint32_t i = 0, j = 0;
 _fifo_str *p = &fifo_str;

 j = p->in % p->size;//获取要写入的剩余空间长度
 len = min(len, p->size - p->in + p->out);//得到实际写入的长度
 i = min(len, p->size - j);//实际写入数据的长度
 memcpy(p->buf + j, buf, i);//将写入的数据的内容拷贝值数据中。
 memcpy(p->buf, buf + i, len - i);//补充多余空间的内容
 p->in += len;//记录实际写入数据的数量
 return len;//返回写入的长度
}

123456789101112131415161718192021222324252627282930

4、置位记录量

//清空fifo 中的记录量
void FifoClear(void)
{
 _fifo_str *p = &fifo_str;
    p->in = 0;
    p->out = 0;
}
1234567

5、应用处理机制

#define LEN 2048
uint8_t pdata[LEN] = {0};
FifoInit(pdata, LEN);//初始化FIFO
uint8_t buf[32] = {0}; 
int tx_len = 0;
uint8_t tx_buf[100] = {0};

HAL_UART_Receive_IT(&huart1, buf, IT_LEN);//串口的接收中断开启
while(1)
 { 
  tx_len = FifoDataLen();//获取数据长度
  if (tx_len > 0)
  {
   tx_len = (tx_len > 100) ? 100 : tx_len;//判读数据长度是否越界
   FifoRead(tx_buf, tx_len);//读取在中断中写入FIFO缓存的数据
   HAL_UART_Transmit(&huart1, tx_buf, tx_len, 1000);//将读到的数据通过串口发送出来
  }
 }

接收回调函数中的处理
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
 if (FifoSpaceLen() >= 串口记录的接收数据长度)//判断写入FIFO空间的数据量是否大于接收的数据量
 {
  FifoWrite(huart->pRxBuffPtr, huart->RxXferCount);//想FIFO中写入数据
 }
} 
123456789101112131415161718192021222324252627

该FIFO的处理机制中用的记录是通过uint32t类型进行记录的,可能在遇到超出其数据极限的情况,导致数据通信异常。(该类型的数据极限较大,为特殊情况可能出现的情况)。

第二类、FIFO处理机制如下:

/* 定义串口波特率和FIFO缓冲区大小,
分为发送缓冲区和接收缓冲区*/ 
#if UART1_FIFO_EN == 1  
#define UART1_BAUD 115200  
#define UART1_TX_BUF_SIZE 1*1024  
#define UART1_RX_BUF_SIZE 1*1024
#endif 

/* 串口设备结构体
 设置发送、接收缓存区(长度),
 并设置两个变量,一个是指针,一个是计数
 */ 
typedef struct 
{  
 USART_TypeDef *uart; /* STM32内部串口设备指针 */  

 uint8_t *pTxBuf; /* 发送缓冲区 */ 
  uint8_t *pRxBuf; /* 接收缓冲区 */ 
  uint16_t usTxBufSize; /* 发送缓冲区大小 */  
 uint16_t usRxBufSize; /* 接收缓冲区大小 */

   __IO uint16_t usTxWrite; /* 发送缓冲区写指针 */ 
  __IO uint16_t usTxRead; /* 发送缓冲区读指针 */  
  __IO uint16_t usTxCount; /* 等待发送的数据个数 */ 
 
   __IO uint16_t usRxWrite; /* 接收缓冲区写指针 */ 
   __IO uint16_t usRxRead; /* 接收缓冲区读指针 */
   __IO uint16_t usRxCount; /* 还未读取的新数据个数 */ 
  
 void (*SendBefor)(void); /* 开始发送之前的回调函数指针(主要用于RS485切换到发送模式) */  
 void (*SendOver)(void); /* 发送完毕的回调函数指针(主要用于RS485将发送模式切换为接收模式) */ 
 void (*ReciveNew)(uint8_t _byte); /* 串口收到数据的回调函数指针 */  
 uint8_t Sending; /* 正在发送中 */

 }UART_T;

/* 定义每个串口结构体变量 */ 
#if UART1_FIFO_EN == 1
static UART_T g_tUart1; 
static uint8_t g_TxBuf1[UART1_TX_BUF_SIZE]; /* 发送缓冲区 */  
static uint8_t g_RxBuf1[UART1_RX_BUF_SIZE]; /* 接收缓冲区 */ 
#endif

12345678910111213141516171819202122232425262728293031323334353637383940414243

怎样才叫做回调函数?回调函数,是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数, 当这个指针被用为调用它所指向的函数时, 我们就说这是回调函数。

区别指针函数和函数指针:

//指针函数:
int *fun(int x,int y)
int *x=fun(4,5);
在调用指针函数时,需要同类型的指针来接收函数返回值
是函数,返回值时指针
属于数据类型
123456
//函数指针:
int (*fun)(int x,int y)
int x(int x,int y);
x=fun;
fun=&x;
x=(*fun)(1,3);
是指针,指向函数。
属于函数名称
12345678

1.初始化串口FIFO对应的相关的变量

static void UartVarInit(void) 
{  
 #if UART1_FIFO_EN == 1 
  g_tUart1.uart = USART1; /* STM32 串口设备 */ 
  g_tUart1.pTxBuf = g_TxBuf1; /* 发送缓冲区指针 */ 
  g_tUart1.pRxBuf = g_RxBuf1; /* 接收缓冲区指针 */ 
  g_tUart1.usTxBufSize = UART1_TX_BUF_SIZE; /* 发送缓冲区大小 */ 
  g_tUart1.usRxBufSize = UART1_RX_BUF_SIZE; /* 接收缓冲区大小 */ 
  g_tUart1.usTxWrite = 0; /* 发送FIFO写索引 */  
 g_tUart1.usTxRead = 0; /* 发送FIFO读索引 */  
 g_tUart1.usRxWrite = 0; /* 接收FIFO写索引 */  
 g_tUart1.usRxRead = 0; /* 接收FIFO读索引 */  
 g_tUart1.usRxCount = 0; /* 接收到的新数据个数 */  
 g_tUart1.usTxCount = 0; /* 待发送的数据个数 */  
 g_tUart1.SendBefor = 0; /* 发送数据前的回调函数 */  
 g_tUart1.SendOver = 0; /* 发送完毕后的回调函数 */  
 g_tUart1.ReciveNew = 0; /* 接收到新数据后的回调函数 */  
 g_tUart1.Sending = 0; /* 正在发送中标志 */ 
 #endif 
} 
1234567891011121314151617181920

明确中断服务程序的顺序:中断函数处理:void USART1_IRQHandler(void)—》 UART中断请求:HAL_UART_IRQHandler(UART_HandleTypeDef *huart)—》 中断使能:UART_Receive_IT— 》 中断回调函数 HAL_UART_RxCpltCallback(huart);

#if UART1_FIFO_EN == 1
void USART1_IRQHandler(void) //系统中串口的中断函数入口
{
    UartIRQ(&g_tUart1);
}
#endif

1234567

2.编辑自定义的UART中断请求

static void UartIRQ(UART_T *_pUart)
{
    uint32_t isrflags = READ_REG(_pUart->uart->ISR);
    uint32_t cr1its = READ_REG(_pUart->uart->CR1);
    uint32_t cr3its = READ_REG(_pUart->uart->CR3);

    if ((isrflags & USART_ISR_RXNE) != RESET)
    {
        /* 从串口接收数据寄存器读取数据存放到接收FIFO */
        uint8_t ch;
        ch = READ_REG(pUart->uart->RDR);
        /* 读串口接收数据寄存器 */
        _pUart->pRxBuf[_pUart->usRxWrite] = ch;         /* 填入串口接收FIFO */
        if (++_pUart->usRxWrite >= _pUart->usRxBufSize) /* 接收FIFO的写指针+1 */
        {
            _pUart->usRxWrite = 0;
        }
        if (_pUart->usRxCount usRxBufSize) /* 统计未处理的字节个数 */
        {
            _pUart->usRxCount++;
        }
        /* 回调函数,通知应用程序收到新数据,一般是发送1个消息或者设置一个标记 */
        // if (_pUart->usRxWrite == _pUart->usRxRead)
        // if (_pUart->usRxCount == 1)
        {
            if (_pUart->ReciveNew)
            {
                _pUart->ReciveNew(ch); /* 比如,交给MODBUS解码程序处理字节流 */
            }
        }
    }
    /* 处理发送缓冲区空中断 */
    if (((isrflags & USART_ISR_TXE) != RESET) && (cr1its & USART_CR1_TXEIE) != RESET)
    {
        // if (_pUart->usTxRead == _pUart->usTxWrite)
        if (_pUart->usTxCount == 0) /* 发送缓冲区已无数据可取 */
        {
            /* 发送缓冲区的数据已取完时, 禁止发送缓冲区空中断 (注意:此时最后1个数据还未真正发送完毕)*/
            // USART_ITConfig(_pUart->uart, USART_IT_TXE, DISABLE);
            CLEAR_BIT(_pUart->uart->CR1, USART_CR1_TXEIE); /* 使能数据发送完毕中断 */
            // USART_ITConfig(_pUart->uart, USART_IT_TC, ENABLE);
            SET_BIT(_pUart->uart->CR1, USART_CR1_TCIE);
        }
        Else /* 还有数据等待发送 */
        {
            _pUart->Sending = 1; /* 从发送FIFO取1个字节写入串口发送数据寄存器 */
            // USART_SendData(_pUart->uart, _pUart->pTxBuf[_pUart->usTxRead]);
            _pUart->uart->TDR = _pUart->pTxBuf[_pUart->usTxRead];
            if (++_pUart->usTxRead >= _pUart->usTxBufSize)
            {
                _pUart->usTxRead = 0;
            }
            _pUart->usTxCount--;
        }
    }
    /* 数据bit位全部发送完毕的中断 */
    if (((isrflags & USART_ISR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET))
    {
        // if (_pUart->usTxRead == _pUart->usTxWrite)
        if (_pUart->usTxCount == 0)
        { /* 如果发送FIFO的数据全部发送完毕,禁止数据发送完毕中断 */
            // USART_ITConfig(_pUart->uart, USART_IT_TC, DISABLE);
            CLEAR_BIT(_pUart->uart->CR1, USART_CR1_TCIE); /* 回调函数, 一般用来处理RS485通信,将RS485芯片设置为接收模式,避免抢占总线 */
            if (_pUart->SendOver)
            {
                _pUart->SendOver();
            }
            _pUart->Sending = 0;
        }

        else
        { /* 正常情况下,不会进入此分支 */
            /* 如果发送FIFO的数据还未完毕,则从发送FIFO取1个数据写入发送数据寄存器 */
            // USART_SendData(_pUart->uart, _pUart->pTxBuf[_pUart->usTxRead]);
            _pUart->uart->TDR = _pUart->pTxBuf[_pUart->usTxRead];
            if (++_pUart->usTxRead >= _pUart->usTxBufSize)
            {
                _pUart->usTxRead = 0;
            }
            _pUart->usTxCount--;
        }
    } /* 清除中断标志 */
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_PEF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_FEF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_NEF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_OREF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_IDLEF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_TCF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_LBDF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_CTSF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_CMF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_WUF);
    SET_BIT(_pUart->uart->ICR, UART_CLEAR_TXFECF);
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394

3.填写数据到UART发送缓冲区。

并启动发送中断,中断处理函数发送完毕后,自动关闭发送中断 .

static void UartSend(UART_T *_pUart, uint8_t *_ucaBuf, uint16_t _usLen) 
{  
 uint16_t i;  
  for (i = 0; i while (1)  
  {  
   __IO uint16_t usCount;   DISABLE_INT(); 
    usCount = _pUart->usTxCount; 
    ENABLE_INT();   
   if (usCount usTxBufSize)  
   {  
    break;  
   }  
   else if(usCount == _pUart->usTxBufSize)/* 数据已填满缓冲区 */  
   { 
     if((_pUart->uart->CR1 & USART_CR1_TXEIE) == 0) 
     {  
     SET_BIT(_pUart->uart->CR1, USART_CR1_TXEIE);  
    } 
    }  
  }   /* 将新数据填入发送缓冲区 */ 
   _pUart->pTxBuf[_pUart->usTxWrite] = _ucaBuf[i];  
   DISABLE_INT();  
  if (++_pUart->usTxWrite >= _pUart->usTxBufSize) 
   {  
   _pUart->usTxWrite = 0;  
  } 
   _pUart->usTxCount++;  
  ENABLE_INT(); 
  }  
  SET_BIT(_pUart->uart->CR1, USART_CR1_TXEIE); /* 使能发送中断(缓冲区空) */ 
} 

12345678910111213141516171819202122232425262728293031323334

4.向串口发送一组数据。

数据放到发送缓冲区后立即返回,由中断服务程序在后台完成发送

void comSendBuf(COM_PORT_E _ucPort, uint8_t *_ucaBuf, uint16_t _usLen) 
{  
 UART_T *pUart;  
  pUart = ComToUart(_ucPort); 
  if (pUart == 0) 
  {
    return; 
  }  
  if (pUart->     != 0) 
  {
    pUart->SendBefor(); /* 如果是RS485通信,可以在这个函数中将RS485设置为发送模式 */  
 }  
  UartSend(pUart, _ucaBuf, _usLen); 
}

123456789101112131415

向串口发送1个字节。数据放到发送缓冲区后立即返回, 由中断服务程序在后台完成发送

void comSendChar(COM_PORT_E _ucPort, uint8_t _ucByte)
 {  
 comSendBuf(_ucPort, &_ucByte, 1); 
} 

123456

函数comSendChar是发送一个字节, 通过调用函数comSendBuf实现, 而函数comSendBuf又是通过调用函数UartSend实现, 这个函数是重点。

5.将COM端口号转换为UART指针

UART_T *ComToUart(COM_PORT_E _ucPort) 
{  
 if (_ucPort == COM1)  
 { 
  #if UART1_FIFO_EN == 1  
  return &g_tUart1; 
   #else 
   return 0;  
  #endif 
  }  
 else  
 {  
  Error_Handler(__FILE__, __LINE__);  
  return 0; 
  } 
}

1234567891011121314151617

6.从串口接收缓冲区读取1字节数据

static uint8_t UartGetChar(UART_T *_pUart, uint8_t *_pByte) 
{  

 uint16_t usCount;   /* usRxWrite 变量在中断函数中被改写,主程序读取该变量时,必须进行临界区保护 */  

 DISABLE_INT();  usCount = _pUart->usRxCount;  ENABLE_INT();   /* 如果读和写索引相同,则返回0 */ 
 
 //if (_pUart->usRxRead == usRxWrite)  
 if (usCount == 0) /* 已经没有数据 */  
 {  
  return 0; 
  } 
  else  
 {  
  *_pByte = _pUart->pRxBuf[_pUart->usRxRead]; /* 从串口接收  FIFO取1个数据 */   
  /* 改写FIFO读索引 */  

  DISABLE_INT();
 
   if (++_pUart->usRxRead >= _pUart->usRxBufSize) 
   {  
   _pUart->usRxRead = 0;  
  }  
  _pUart->usRxCount--; 
   ENABLE_INT();  
  return 1;  
 }
 
} 
1234567891011121314151617181920212223242526272829

从接收缓冲区读取1字节,非阻塞。无论有无数据均立即返回。

uint8_t comGetChar(COM_PORT_E _ucPort, uint8_t *_pByte) 
{

 UART_T *pUart;   
 pUart = ComToUart(_ucPort); 
  if (pUart == 0)
 {  
  return 0;  
 } 
   return UartGetChar(pUart, _pByte); 
}
123456789101112

接收数据的调用顺序是:comGetChar–》UartGetChar

7.判断发送缓冲区是否为空

uint8_t UartTxEmpty(COM_PORT_E _ucPort)
{
   UART_T *pUart;
   uint8_t Sending;
   
   pUart = ComToUart(_ucPort);
   if (pUart == 0)
   {
      return 0;
   }

   Sending = pUart->Sending;

   if (Sending != 0)
   {
      return 0;
   }
   return 1;
}

1234567891011121314151617181920

8.清零串口接收缓冲区

void comClearRxFifo(COM_PORT_E _ucPort)
{
 UART_T *pUart;
 pUart = ComToUart(_ucPort);
 if (pUart == 0)
 {
  return;
 }
 pUart->usRxWrite = 0;
 pUart->usRxRead = 0;
 pUart->usRxCount = 0;
}
123456789101112

9.清零串口发送缓冲区

void comClearTxFifo(COM_PORT_E _ucPort)
{
 UART_T *pUart;

 pUart = ComToUart(_ucPort);
 if (pUart == 0)
 {
  return;
 }

 pUart->usTxWrite = 0;
 pUart->usTxRead = 0;
 pUart->usTxCount = 0;
}
1234567891011121314

10.输入输出的重定向

int fputc(int ch, FILE *f)
{
#if 1 /* 将需要printf的字符通过串口中断FIFO发送出去,printf函数会立即返回 */
 comSendChar(COM1, ch);
 return ch;
#else /* 采用阻塞方式发送每个字符,等待数据发送完毕 */
 /* 写一个字节到USART1 */
 USART1->DR = ch;
 /* 等待发送结束 */
 while((USART1->SR & USART_SR_TC) == 0)
 {}
 return ch;
#endif
}


int fgetc(FILE *f)
{
#if 1 /* 从串口接收FIFO中取1个数据, 只有取到数据才返回 */
 uint8_t ucData;
 while(comGetChar(COM1, &ucData) == 0);
 return ucData;
#else
 /* 等待接收到数据 */
 while((USART1->SR & USART_SR_RXNE) == 0)
 {}
 return (int)USART1->DR;
#endif
}
1234567891011121314151617181920212223242526272829

11.应用层初始化:

//FIFO串口初始化
UartVarInit(void);
//串口参数配置
void bsp_SetUartParam(USART_TypeDef *Instance,  uint32_t BaudRate, uint32_t Parity, uint32_t Mode)
{
 UART_HandleTypeDef UartHandle; 
 /*##-1- 配置串口硬件参数 ######################################*/
 /* 异步串口模式 (UART Mode) */
 /* 配置如下:
   - 字长    = 8 位
   - 停止位  = 1 个停止位
   - 校验    = 参数Parity
   - 波特率  = 参数BaudRate
   - 硬件流控制关闭 (RTS and CTS signals) */
 UartHandle.Instance        = Instance;
 UartHandle.Init.BaudRate   = BaudRate;
 UartHandle.Init.WordLength = UART_WORDLENGTH_8B;
 UartHandle.Init.StopBits   = UART_STOPBITS_1;
 UartHandle.Init.Parity     = Parity;
 UartHandle.Init.HwFlowCtl  = UART_HWCONTROL_NONE;
 UartHandle.Init.Mode       = Mode;
 UartHandle.Init.OverSampling = UART_OVERSAMPLING_16;
    
 if (HAL_UART_Init(&UartHandle) != HAL_OK)
 {
  Error_Handler(__FILE__, __LINE__);
 }
}
//对应的串口波特率配置
void comSetBaud(COM_PORT_E _ucPort, uint32_t _BaudRate)
{
 USART_TypeDef* USARTx;
 USARTx = ComToUSARTx(_ucPort);
 if (USARTx == 0)
 {
  return;
 }
 bsp_SetUartParam(USARTx,  _BaudRate, UART_PARITY_NONE, UART_MODE_TX_RX);
}
//硬件初始化
void InitHardUart(void)
{
GPIO 复用....
 /* 配置NVIC the NVIC for UART */   
 HAL_NVIC_SetPriority(USART1_IRQn, 0, 1);
 HAL_NVIC_EnableIRQ(USART1_IRQn);
  
 /* 配置波特率、奇偶校验 */
 bsp_SetUartParam(USART1,  UART1_BAUD, UART_PARITY_NONE, UART_MODE_TX_RX);

 CLEAR_BIT(USART1->SR, USART_SR_TC);   /* 清除TC发送完成标志 */
    CLEAR_BIT(USART1->SR, USART_SR_RXNE); /* 清除RXNE接收标志 */
 // USART_CR1_PEIE | USART_CR1_RXNEIE
 SET_BIT(USART1->CR1, USART_CR1_RXNEIE); /* 使能PE. RX接受中断 */
}
在主循环中

comGetChar(COM1, &read);//获取一个字节数据
comSendBuf(COM1, (uint8_t *)buf, strlen(buf));//发送数据
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部