STM32F4+DP83848以太网通信指南系列(九):自己写一个ARP协议

前言:项目需求使用STM32F407进行以太网通信,并涉及到数据链路层的工业以太网通信,使用LWIP协议栈并不能满足需求,因此需要自己摸清STM32F407调用外部PHY进行网络收发包的过程,并在此基础上尝试自己构建适用于项目的网络协议栈。我基本上是从零开始着手这个项目的,之前只有一些STC51系列和STM32F1系列单片机开发的经验,项目开发过程中学习、参考、借鉴了很多网络上的教程和博客,在此尤其感谢正点原子团队发布的相关视频教程。公司买的开发板主芯片是一颗STM32F407,搭配了一颗DP83848的PHY,因此本系列教程将使用DP83848进行适配,同时原子哥的学习板和教程中是使用LAN8720这颗PHY进行适配的,本系列指南也会花一些篇幅介绍各种PHY与STM32芯片进行适配的方法。

为了您更好地阅读本系列,请点击原创连接进行浏览:

本章为系列指南的第九章,终结篇,本章主要来分析一下完整的ARP协议,并在STM32F4中实现一个精简的ARP协议响应流程。

ARP协议的本质是使局域网内的其他主机能够知道我在哪儿,比如在局域网上有人冲着所有人喊了一句「IP为XXXX的家伙,你在哪儿」,我一听,XXXX不是我的IP吗,我得回答他啊,于是我冲着所有人(也可以是单独的这个人)喊一句「我在这儿呢,我的MAC是YYYY」,这样局域网内所有用户,包括交换机就知道IP为XXXX的家伙MAC地址是YYYY,并且交换机知道了我连在它的第N个端口上,以后有人要通过交换机向IP为XXXX的我发信息,交换机就把数据包发到N port上去了。没有上面这套流程,局域网上就没人知道你是谁,你的IP多少,你的MAC多少,你连在交换机大佬的第几个端口上,自然你也收不到任何非广播包数据了。

ARP请求

如果我要向一个之前没有任何联系的主机主动发一个数据包,一般情况下都会触发一个ARP询问,比如我们上一章《STM32F4+DP83848以太网通信指南第八章:收包流程》中,最后的实验,我打开CMD命令行,输入了一个指令ping 192.168.1.201,这时候PC的底层设备(一般是网卡)首先判断一下自己的缓存中有没有192.168.1.201这个IP的缓存信息,如果没有通信记录或者时间过长失效了,就得在网络上广播一下,找找看当前有没有谁在用192.168.1.201这个IP地址,包内容如下:

ff ff ff ff ff ff 00 0e c6 d4 1d d4 08 06 00 01
08 00 06 04 00 01 00 0e c6 d4 1d d4 c0 a8 01 c8
00 00 00 00 00 00 c0 a8 01 C9

WireShark分析以上报文的解析如下图所示:

ARP响应

目标主机的MAC地址为FF FF FF FF FF FF时,交换机会将此包广播到所有端口,这时候所有节点的网卡上都能收到这个广播包,如果某一节点的IP地址为192.168.1.201则需要对此包进行响应,判断依据为上述包的以下特征:

  • 第一行倒数第4,第3字节:0806,代表的是ARP协议
  • 第二行第6字节:01,代表的是ARP请求
  • 第三行最后4字节:c0 a8 01 C9,代表的是192.168.1.1,跟自己寄存器中的值匹配

响应数据包为以下内容:

00 0e c6 d4 1d d4 00 11 0e 0b 03 8a 08 06 00 01
08 00 06 04 00 02 00 11 0e 0b 03 8a c0 a8 01 c9
00 0e c6 d4 1d d4 c0 a8 01 c8 00 00 00 00 00 00
00 00 00 00 00 00 00 00 20 20 20 20

上述中,以下数据是动态变化的:

  • 第一行前6个字节:00 0e c6 d4 1d d4,根据请求包中的第7-12六个字节决定,表示目标MAC地址
  • 第一行第7-12六个字节:00 11 0e 0b 03 8a,自由设置的自己的MAC地址
  • 第二行第7-12六个字节:00 11 0e 0b 03 8a,自由设置的自己的MAC地址
  • 第二行倒数4个字节:c0 a8 01 c9,自己的IP地址,也就是请求包中所呼唤的IP地址,192.168.1.201
  • 第三行前6个字节:00 0e c6 d4 1d d4,根据请求包中的第7-12六个字节决定,表示目标MAC地址
  • 第三行第7-10四个字节:c0 a8 01 c8,根据请求包第二行倒数四个字节决定,表示目标IP地址

这些信息都可以用WireShark分析出来,WireShark对每一个字节的含义都有明确的解释。

编码实现

有了特征匹配及填充规则,配合我们之前的STM32发包和收包DEMO,我们就可以编程实现ARP数据包的响应了。

#define LEN_ARP 42

/* 以下为业务逻辑需要用到的全局变量 */
u8 IPAddr[4];

vu8  SystemStatus; /* 系统状态,bit0:是否需要发送被动响应数据包,bit1:是否打开了PNIO开关,bit2:是否需要发送PNControl request */
vu16 sendLen;      /* 发送包长度 */
u8 sendBuffer[1024];   /* 发送包BUFFER */
u8 arp_answer[LEN_ARP]={
    0,0,0,0,0,0, /* ArpAskerMac */ 0,0,0,0,0,0, /* myMac */ 0x08, 0x06, 0x00, 0x01,
    0x08, 0x00, 0x06, 0x04, 0x00, 0x02, 0,0,0,0,0,0, /* myMac */ 0,0,0,0, /* IP address */
    0,0,0,0,0,0, /* ArpAskerMac */ 0,0,0,0/* ArpAskerIP */
};

int main() {
    SystemStatus = SS_NOTHING;
    sendLen = 0;    

    /* 默认调用SystemInit,系统时钟168MHz */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);        //4位抢占,0位响应

    DelayInit(168);        //初始化168MHz

    NVIC_SetPriority (SysTick_IRQn, 0);        //系统滴答定时器优先级最高  

    DP83848Init(MyMacAddr);        //初始化DP83848

    while(1) {
        if (0x01 == (SystemStatus & 0x01)){
            /* 如果需要发送被动网卡响应 */
            DP83848Send(sendBuffer, sendLen);            
            sendLen = 0;
            SystemStatus = SystemStatus & 0xFE; /* bit0置为0 */
        }
    }
}

void Pkt_Handle(void) {
    FrameTypeDef frame;
    __IO ETH_DMADESCTypeDef *DMARxNextDesc;

    /* get received frame */
    frame = ETH_Get_Received_Frame();
    /* Obtain the size of the packet and put it into the "len" variable. */
    receiveLen = frame.length;
    receiveBuffer = (u8 *)frame.buffer;

    if (Match(receiveBuffer, ArpProtocol, 12, 2) && Match(receiveBuffer, IPAddr, 38, 4)){
        /* ARP */
        //printf("ARP test received\n");
        memcpy(sendBuffer, arp_answer, LEN_ARP);

        for (int i = 0; i < 6; i++) {
            /* ArpAskerMac */
            sendBuffer[i] = receiveBuffer[6 + i];    
            sendBuffer[32 + i] = receiveBuffer[6 + i];
        }
        for (int i = 0; i < 4; i++) {
            /* ArpAskerIP */
            sendBuffer[38 + i] = receiveBuffer[28 + i];    
        }

        sendLen = LEN_ARP;
        SystemStatus = (SystemStatus | 0x01); /* 打开网卡被动数据发送开关 */
    }

    /* Check if frame with multiple DMA buffer segments */
    if (DMA_RX_FRAME_infos->Seg_Count > 1) {
        DMARxNextDesc = DMA_RX_FRAME_infos->FS_Rx_Desc;
    } else {
        DMARxNextDesc = frame.descriptor;
    }

    /* Set Own bit in Rx descriptors: gives the buffers back to DMA */
    for (i = 0; i < DMA_RX_FRAME_infos->Seg_Count; i++) {
        DMARxNextDesc->Status = ETH_DMARxDesc_OWN;
        DMARxNextDesc = (ETH_DMADESCTypeDef *)(DMARxNextDesc->Buffer2NextDescAddr);
    }

    /* Clear Segment_Count */
    DMA_RX_FRAME_infos->Seg_Count = 0;

    /* When Rx Buffer unavailable flag is set: clear it and resume reception */
    if ((ETH->DMASR & ETH_DMASR_RBUS) != (u32)RESET) {
        /* Clear RBUS ETHERNET DMA flag */
        ETH->DMASR = ETH_DMASR_RBUS;
        /* Resume DMA reception */
        ETH->DMARPDR = 0;
    }
}

编码的总体思想就是中断中快进快出,用变量来标记状态,主循环中不断判断状态并复位状态。

使用这种方式,可以继续扩展ICMP协议,实现PING指令,以及更多按需裁剪的TCP/IP协议,当然扩展的协议越多,这种纯粹的if判断也会越多,以后会细分各个层,在层次中继续if判断,当到了顶层应用层之后,就只管TCP/UDP协议了,内容一般都是应用协议自己规划的比如HTTP,FTP这些协议,只需要把上层协议的数据结构填充到下层协议的内容区域就行了。当遇到数据包比较庞大的时候,还需要分包传输,暂时我们没有用到,LWIP中有相应的实现,有需求的话可以去研究。此外,我们实验中的代码都是基于数组进行数据装箱拆箱操作的,而网络包的协议是按层次来的,使用数组不断的进行数据的填充是非常消耗性能的,因此在遇到数据量比较大,需要实现的协议比较多时,仍然建议按照LWIP的思路使用收尾相接的链式结构进行拆装箱。

系列总结

到此我们这个系列就算告一段落了,后面的工业以太网的协议的分析和实现我不方便做公开的教程,也是自己一步一步摸索过来的,目前自己在设计PCB板,后期这个工程需要制板贴片并做成最终的工业产品。

我们来回顾总结一下这个系列教程,首先我们对STM32F407的时钟、中断等相关知识做了一个梳理,接着我们认识了STM32中的MAC,以及跟MAC搭配的PHY,同时还对DMA技术做了一个粗略的了解。试着初始化相关的GPIO,使能了MAC,DMA,PHY,这样一个DP83848Init()函数就搞定了;再后来我们又完成DP83848Send()函数,能够发包了,接着配置了以太网中断,能够在Pkt_Handle()函数中进行以太网收包了,以上函数的编写,我们都是参考的ethernetif.c文件,它的几个底层函数low_level_initlow_level_outputlow_level_input给我们提供了重要的线索。最后对TCP/IP协议栈中的ARP协议进行了分析,并运用之前的全部知识,进行了ARP响应的编码实现,并对以后其他协议的扩展实现提出了思路和优化建议。

好了,期待今后有更多的系列教程跟大家分享。再见!

STM32F4+DP83848以太网通信指南系列(八):收包流程

前言:项目需求使用STM32F407进行以太网通信,并涉及到数据链路层的工业以太网通信,使用LWIP协议栈并不能满足需求,因此需要自己摸清STM32F407调用外部PHY进行网络收发包的过程,并在此基础上尝试自己构建适用于项目的网络协议栈。我基本上是从零开始着手这个项目的,之前只有一些STC51系列和STM32F1系列单片机开发的经验,项目开发过程中学习、参考、借鉴了很多网络上的教程和博客,在此尤其感谢正点原子团队发布的相关视频教程。公司买的开发板主芯片是一颗STM32F407,搭配了一颗DP83848的PHY,因此本系列教程将使用DP83848进行适配,同时原子哥的学习板和教程中是使用LAN8720这颗PHY进行适配的,本系列指南也会花一些篇幅介绍各种PHY与STM32芯片进行适配的方法。

为了您更好地阅读本系列,请点击原创连接进行浏览:

本章为系列指南的第八章,讲述如何使用STM32F407芯片配合DP83848进行以太网数据的收包流程,将监听到的网络包数据通过UART传给PC,同时辅以WireShark监听对比验证。

关于UART,也就是串口通信的使用,这里不做赘述,需要补充知识的朋友可以先浏览一下我在STM32F1系列的UART教程,F4系列也是大同小异,资源和引脚有所调整而已。我们这里预设两个函数分别为UART6Init()UART6Send(),实现的功能是串口6的初始化和发送。

以太网中断

《STM32F4+DP83848以太网通信指南第五章:MAC+DMA配置》中,我们已经添加了以太网中断,其思路就是想让每次以太网上有收到包都能触发中断,我们可以在中断中将DMA中的数据包取出来进行分析,然后复位,让芯片下一次继续响应中断。

配置中断的代码非常简单,跟其他任何中断都一样,这里再复习一次:

void ETH_NVIC_Config(void) {
    NVIC_InitTypeDef   NVIC_InitStructure;

    /* Enable the Ethernet global Interrupt */
    NVIC_InitStructure.NVIC_IRQChannel = ETH_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

整个工程的优先级组别选用的NVIC_PriorityGroup_4,有4位抢占位,0位响应位,也就是可以分配16个可以互相嵌套的中断等级。这里以太网中断的主优先级为1,相当于第二高,前面预留了个优先级为0的,用来分配给系统计时器,毕竟不能因为以太网数据的响应影响系统走时。

UART6Init()串口初始化函数中,给串口的中断等级是2,低于以太网中断,因为串口的波特率是9600,要远远低于以太网速率,如果给串口的优先级过高,会影响以太网的使用。

配置了中断后,我们还需要知道中断的入口函数,这个函数名是固定死的,不能乱写,我们去找找。

《STM32F4+DP83848以太网通信指南第五章:MAC+DMA配置》我提到,中断配置代码中的ETH_IRQn变量,我们可以在stm32f4xx.h文件中找到定义,是在一个枚举结构中。那与之对应的中断入口名称该怎么找呢,原来所有的中断入口的定义,都用汇编入口的方式定义在启动文件中,这份启动文件我们之前一直没有关注过,现在打开看一看,在startup_stm32f40_41xxx.s中148行有其定义,截图如下。

中断中的数据处理

配置好了以太网中断,也知道了中断入口函数的名称,下面我们就来编写以太网中断函数。打开工程中的stm32f4xx_it.c文件,一般每一个使用了中断的STM32工程都会有这么一个文件,用来集中管理中断入口。追加以下代码:

/**
  * @brief  This function handles ethernet DMA interrupt request.
  * @param  None
  * @retval None
  */
void ETH_IRQHandler(void)
{
    /* Handles all the received frames */
    /* check if any packet received */
      while(ETH_CheckFrameReceived()){ 
        /* process received ethernet packet */
        Pkt_Handle();
    }
    /* Clear the Eth DMA Rx IT pending bits */
    ETH_DMAClearITPendingBit(ETH_DMA_IT_R);
    ETH_DMAClearITPendingBit(ETH_DMA_IT_NIS);
}

上面的代码配上英文的注释也很好理解,以太网中断中首先检查是否接受到以太网的数据包,如果是,就调用Pkt_Handle()函数进行下一层的分析和处理,最后两行Clear复位中断标记,让下一次中断能够产生。那么问题就集中到Pkt_Handle()函数上来了。

Pkt_Handle()函数是我自己命名的,这个函数的原型取自LWIP中的LWIP_Pkt_Handle(),我们先来观察一下LWIP中的包分析函数怎么写的:

STM32F4x7_ETH_LwIP_V1.1.1/Project/Standalone/udp_echo_client/src/netconf.c中,有如下代码:

/**
* @brief  Called when a frame is received
* @param  None
* @retval None
*/
void LwIP_Pkt_Handle(void)
{
  /* Read a received packet from the Ethernet buffers and send it to the lwIP for handling */
  ethernetif_input(&gnetif);
}

可以看到LWIP继续调用了下层的ethernetif_input(),继续追踪到我们之前提到的最底层文件ethernetif.c,是不是有种似曾相识的感觉,这个文件前几章我们不止一次遇到过,分别为我们提供了low_level_initlow_level_output多个重要函数,我们现在又一次遇到它了,看上去它这次要为我们的以太网监听提供low_level_input了。

果不其然,在ethernetif_input函数中,我们看到了这个预料中的函数调用,截图如下:

我这里把路径为STM32F4x7_ETH_LwIP_V1.1.1/Utilities/Third_Party/lwip-1.4.1/port/STM32F4x7/Standalone/ethernetif.c的原版low_level_input()的所有代码都贴出来,感兴趣的朋友可以仔细研读:

/**
 * Should allocate a pbuf and transfer the bytes of the incoming
 * packet from the interface into the pbuf.
 *
 * @param netif the lwip network interface structure for this ethernetif
 * @return a pbuf filled with the received packet (including MAC header)
 *         NULL on memory error
 */
static struct pbuf * low_level_input(struct netif *netif)
{
  struct pbuf *p, *q;
  u16_t len;
  int l =0;
  FrameTypeDef frame;
  u8 *buffer;
  uint32_t i=0;
  __IO ETH_DMADESCTypeDef *DMARxNextDesc;


  p = NULL;

  /* get received frame */
  frame = ETH_Get_Received_Frame();

  /* Obtain the size of the packet and put it into the "len" variable. */
  len = frame.length;
  buffer = (u8 *)frame.buffer;

  /* We allocate a pbuf chain of pbufs from the Lwip buffer pool */
  p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);

  /* copy received frame to pbuf chain */
  if (p != NULL)
  {
    for (q = p; q != NULL; q = q->next)
    {
      memcpy((u8_t*)q->payload, (u8_t*)&buffer[l], q->len);
      l = l + q->len;
    }    
  }

  /* Release descriptors to DMA */
  /* Check if frame with multiple DMA buffer segments */
  if (DMA_RX_FRAME_infos->Seg_Count > 1)
  {
    DMARxNextDesc = DMA_RX_FRAME_infos->FS_Rx_Desc;
  }
  else
  {
    DMARxNextDesc = frame.descriptor;
  }

  /* Set Own bit in Rx descriptors: gives the buffers back to DMA */
  for (i=0; i<DMA_RX_FRAME_infos->Seg_Count; i++)
  {  
    DMARxNextDesc->Status = ETH_DMARxDesc_OWN;
    DMARxNextDesc = (ETH_DMADESCTypeDef *)(DMARxNextDesc->Buffer2NextDescAddr);
  }

  /* Clear Segment_Count */
  DMA_RX_FRAME_infos->Seg_Count =0;

  /* When Rx Buffer unavailable flag is set: clear it and resume reception */
  if ((ETH->DMASR & ETH_DMASR_RBUS) != (u32)RESET)  
  {
    /* Clear RBUS ETHERNET DMA flag */
    ETH->DMASR = ETH_DMASR_RBUS;
    /* Resume DMA reception */
    ETH->DMARPDR = 0;
  }
  return p;
}

同样的,配合注释应该也容易理解。22行的frame变量取到了以太网数据包,ETH_Get_Received_Frame函数,不用LWIP也是有的,在之前我们提到过的stm32f4x7_eth.c文件中,lenbuffer两个变量一个是包长度,一个是包内容头指针。

下面/* copy received frame to pbuf chain */那一段是用链表遍历的方式,将以太网包数据放入LWIP处理数据的pbuf链表中,方便LWIP上层逻辑获取数据,这里我们不使用LWIP,这一段可忽略。

接下来所有的操作都是针对DMA进行的,将DMA复位,因此我们需要保留,否则会一直产生重复的中断。

通过以上的分析,我们可以轻松写出自己的Pkg_Handle()函数了:

void Pkt_Handle(void) {
    FrameTypeDef frame;

    /* get received frame */
    frame = ETH_Get_Received_Frame();
    /* Obtain the size of the packet and put it into the "len" variable. */
    receiveLen = frame.length;
    receiveBuffer = (u8 *)frame.buffer;

    printf("0011%d0022\n", receiveLen);    //将每一个的包长度发往串口

    if(receiveBuffer[41] == 201){        //如果第42字节是十进制201,则将整个包内容发往串口
        for (i = 0; i < receiveLen; i++) {
            printf("%c", receiveBuffer[i]);
        }
    }

    /* Check if frame with multiple DMA buffer segments */
    if (DMA_RX_FRAME_infos->Seg_Count > 1) {
        DMARxNextDesc = DMA_RX_FRAME_infos->FS_Rx_Desc;
    } else {
        DMARxNextDesc = frame.descriptor;
    }

    /* Set Own bit in Rx descriptors: gives the buffers back to DMA */
    for (i = 0; i < DMA_RX_FRAME_infos->Seg_Count; i++) {
        DMARxNextDesc->Status = ETH_DMARxDesc_OWN;
        DMARxNextDesc = (ETH_DMADESCTypeDef *)(DMARxNextDesc->Buffer2NextDescAddr);
    }

    /* Clear Segment_Count */
    DMA_RX_FRAME_infos->Seg_Count = 0;

    /* When Rx Buffer unavailable flag is set: clear it and resume reception */
    if ((ETH->DMASR & ETH_DMASR_RBUS) != (u32)RESET) {
        /* Clear RBUS ETHERNET DMA flag */
        ETH->DMASR = ETH_DMASR_RBUS;
        /* Resume DMA reception */
        ETH->DMARPDR = 0;
    }
}

验证和总结

上述函数,配合ETH_IRQHandler中断中的调用,完成了以太网的收包,并且将接受的包的长度使用0011%d0022通过printf函数通过UART发往了PC端,因为如果将整个包内容发往PC的话,串口数据会非常多。同时如果为了验证buffer中的内容能正确获取,我们写了一个if判断,判断如果数据包中的第42个字节为201,则将包内容转发到串口中去。

我们用JLink烧录进STM32F4,将PC的有线网卡与STM32直接,打开WireShark和串口通信助手进行观察和验证,截图如下。

红色框框部分是关注重点,我们在CMD命令窗口ping 192.168.1.201,可以触发一个ARP包,这个包中的第42个字节就是201,因此可以触发STM32中的if判断,将包内容通过串口转发给PC,而其他普通包,STM32则使用0011%d0022的格式将包长度发给了PC,整个实验顺利完成。

总结一下,本章我们依旧分析到了ethernetif.c文件,这次是观察的它的low_level_input()函数,借助这个函数,我们编写了我们自己的处理包的逻辑函数Pkg_Handle(),并通过以太网中断入口函数ETH_IRQHandler调用它,最后我们成功的使用WireShark配合串口进行了收包的验证。

我们可以发现这个系列教程的后半段几乎都在不停地围绕LWIP库中的ethernetif.c文件进行分析,到目前为止,它的几个重要底层函数low_level_initlow_level_outputlow_level_input已经分别为我们的以太网初始化、以太网发包、以太网收包等代码提供了重要的核心逻辑。

下一章,是我们这个系列的最后一章,我们将在STM32F4上,利用之前实验过的各个功能,自己构建一个能响应ARP协议的程序。有了ARP协议的处理过程,我们就能在此基础上扩展更多其他的协议,即使遇到工业以太网跑在链路层的各个协议,我们也能捡其重点,按照自己的意愿,随心所欲的搭建了。

大姨妈微信语录,芳华

写一写我们溱潼的芳华岁月

纱厂的厂房标志性的锯齿屋顶,是为了釆光,远一看红砖红瓦中相嵌透明的玻璃特别壮观。

嚓嚓作响的机器磨擦和撞击声音在这红色的建筑物中产生了共鸣,不习惯的人是一刻都呆不住,但是我们广大的纺织产业工人拿它当着了爵士乐!每天为我们的工作打着节奏,振奋着人们的精神。

有很长一段时间厂里掀起了义务劳动的活动,工人们下班后继续留下来义务劳动两小时(当然是自愿的)。

回忆当时场景:沒有出工不出力的,沒有发牢骚的。八小时工作下来应该很疲惫了。这两个小时是什么思想在支撑的行为。肯定是毛泽东思想。

我羡慕当时的政治引导。人们信仰毛泽东。信仰为人民服务。思想指导行为。

我的青春芳华年代在这样一个大家庭中渡过,其乐融融。每周总有一到两天的政治学习。一个月会开全厂职工大会。会间休息一会儿,杨靖 王莲英二人对唱《夫妻识字》经久不衰。全場的撑声将大会推向了高潮,也给全厂职工注入了新鲜血液。抓革命促生产的热朝一浪高过一浪。沒有一个人提到钱字。大家唯恐落后。

有一本书上面科学地分析过人的行为,说是分七个阶段,首先是温饱,然后重视欲,再然后领导欲,……这些分析用在我们当时真是一派胡言。当年的我们单纯的如一张白纸,党叫干啥就干啥,非常祟拜中国共产党,身边的人谁入党立马感到他神一般。我们的精神领域被领导了,人们有信仰有压缩。虽然沒有现在的时装,山珍海味,你敢说我们当年不快乐吗?粗茶淡饭爽口,偶尔来一顿红烧肉感觉人间美味。

当年的我们特容易满足,无忧无虑。芳华的岁月在那红砖红瓦的厂房里充满了芳香。

丁丁生于 1987.07.01 ,30岁,英文ID:newflydd

  • 现居住地 江苏 ● 泰州 ● 姜堰
  • 创建了 Jblog 开源博客系统
  • 坚持十余年的 独立博客 作者
  • 大学本科毕业后就职于 中国电信江苏泰州分公司,前两年从事Oracle数据库DBA工作,两年后公司精简技术人员,被安排到农村担任支局长(其本质是搞销售),于2016年因志向不合从国企辞职,在小城镇找了一份程序员的工作。
  • Git OSChina 上积极参与开源社区