程序员视角下的车载时钟同步

程序员的学会必须是能够写的出代码。

一、前言

说到时钟同步,不同领域工程师的第一反应大概率是不一样。传统车载工程师可能想到的是can_tsync以及stbm;负责车载以太网相关的工程师会想到ptp/gptp;从事物联网、车联网相关会想到ntp、gnss。
考虑自动驾驶/ADAS领域,这类控制器,一般包含各类的传感器,如ODO,GPS,地图,摄像头,毫米波雷达,超声波雷达等,因此这些传感器数据精确的采集时间显得尤为重要,因为直接关系到最后做传感器融合以及决策规划,因此必须保证精确使用哪一个时刻的数据或者应该在同一时刻采集数据。比如摄像头采集图片的时刻跟雷达开始扫描的时刻应该是在同一个时间点,但一般摄像头和雷达都分属于不同的ecu,使用的时钟源是不一样的,所以比较将两者的时钟进行同步,从底层来看,也就是信号的跳变沿总是成整数倍。其实也就是在电视剧中经常能够看到的对表。并且随着车云一体化的逐渐展开,车端和云端总是会一起作为整体执行一个功能,所以车云的时间同步也显得尤为重要,如执行时序、日志时间戳等。
而车载的时钟同步,是上述不同种类的融合,对于不同的同步方式会有不同的应用场景,我们可以暂且将其分为两大类,vehicle time和utc,其中vehicle time是局域网内时钟同步的,具有同步精度高的特点,一般使用gptp和can tsync的方式,让整车内的各个ecu时钟都保持同步;而UTC时间,是世界统一时钟,与经过平均太阳时(以格林威治时间GMT为准)、地轴运动修正后的新时标以及以秒为单位的国际原子时所综合精算而成,一般会通过ntp或者gnss同步本地时钟,车是一个移动的单位,而ntp包或者gnss本身可能存在丢失,所以相对来说精度不会那么高。可以有个比较简单的对比:
一般来说,vehicle time与utc不会相等,也因为由于其特点,所以有不同的应用场景。如下表所示:
站在程序员角度来说,更多的是,会在了解其原理之后,会考虑落地实现。所以这篇文章算是一篇科普+实践经验的文章。
以下从最简单的开始。

二、NTP

  1. ntp时钟层级

ntp允许客户端从服务器请求和接收时间,而服务器又从权威时钟源(例如原子钟、GPS)接收精确的协调世界时UTC。
ntp以层级来组织模型结构,层级中的每层被称为Stratum。通常将从权威时钟获得时钟同步的ntp服务器的层数设置为Stratum 1,并将其作为主时间服务器,为网络中其他的设备提供时钟同步。而Stratum 2则从Stratum 1获取时间,Stratum 3从Stratum 2获取时间,以此类推。时钟层数的取值范围为1~16,取值越小,时钟准确度越高。层数为1~15的时钟处于同步状态;层数为16的时钟被认为是未同步的,不能使用的。
  1. ntp同步原理

ntp最典型的授时方式是Client/Server方式,如下图所示。
ntp同步原理
  1. 客户端首先向服务端发送一个ntp请求报文,其中包含了该报文离开客户端的时间戳t1;
  2. ntp请求报文到达ntp服务器,此时ntp服务器的时刻为t2。当服务端接收到该报文时,ntp服务器处理之后,于t3时刻发出ntp应答报文。该应答报文中携带报文离开ntp客户端时的时间戳t1、到达ntp服务器时的时间戳t2、离开ntp服务器时的时间戳t3;
  3. 客户端在接收到响应报文时,记录报文返回的时间戳t4。
客户端用上述4个时间戳参数就能够计算出2个关键参数:
  • ntp报文从客户端到服务器的往返延迟delay。
  • 客户端与服务端之间的时间差offset。根据方程组:
可以解得时间差为:
ntp客户端根据计算得到的offset来调整自己的时钟,实现与ntp服务器的时钟同步。
如果从简单实现时钟同步角度来说,ntp是最简单的,因为ntp一个request/response就能够完成同步了,ntp 使用udp协议,端口为123。ntp主要分数据报文和控制报文两大类,request/response报文都是数据报文,报文如下图:
  1. ntp实现原理

有了上述理论支撑之后,我们看一下为啥说ntp从实现时间同步的角度来说是很简单的。在pc上,以windows为例,手动关闭打开一下自动更新时间,会触发一次ntp时间同步。
这个时候,我们可以拿到ntp完整的request/response报文,如下:
ntp client request:
ntp server response:
如上图,我们可以看到,我们可以拿到同步原理提到的t1, t2, t3;而t4是client接收到server response时的时间,那么上述时间分别是如何来的呢。
t1: client发送request的前一刻,记下当前utc时间,然后塞入到request transmit timestamp中去。但是这个不能作为真正的t1来使用,因为前面提到了,ntp是使用的udp,所以有可能会丢包。因此真正的t1应该ntp server收到client request时,从request报文中提取到t1,最后再给到client使用。也就是上图的t1。
t2: 也就是server收到client request的时间
t3: server发送response的时间
t4: client收到server response的时间
知道这些原理之后,我们就可以很好设计出我们的代码实现思路,如下:
// ntp server
// receive ntp request message
RecvMsg(request_message_->GetData(), MESSAGE_LENGTH);
// get receive request_frame timestamp, get t1, t2
auto time_point = std::chrono::time_point_cast<std::chrono::microseconds>(
        std::chrono::system_clock::now());
auto req_rx_ts = time_point.time_since_epoch().count();//t2
auto req_tx_ts = request_message_->GetTransTs();//t1

// send ntp response message
response_message_->SetOriginTs(req_tx_ts);//t1
response_message_->SetRecvTs(req_rx_ts);//t2
auto time_point = std::chrono::time_point_cast<std::chrono::microseconds>(
        std::chrono::system_clock::now());
auto resp_tx_ts = time_point.time_since_epoch().count();
response_message_->SetTransTs(resp_tx_ts);//t3
SendMsg(response_message_->GetData(), MESSAGE_LENGTH);
//ntp client
// send request with t1
std::chrono::time_point_cast<std::chrono::microseconds>(
        std::chrono::system_clock::now());
std::time_t time_stamp = time_point.time_since_epoch().count();
request_message_->SetTransTs(time_stamp);
int ret = SendMsg(request_message_->GetData(), MESSAGE_LENGTH);


// receive response, and get t1, t2, t3
int ret = RecvMsg(request_message_->GetData(), MESSAGE_LENGTH);
auto req_tx_ts_ = response_message_->GetOriginTs();  // t1
auto req_rx_ts_ = response_message_->GetRecvTs();    // t2
auto resp_tx_ts_ = response_message_->GetTransTs();  // t3
auto time_point = std::chrono::time_point_cast<std::chrono::microseconds>(
        std::chrono::system_clock::now());
std::time_t time_stamp = time_point.time_since_epoch().count();
auto resp_rx_ts_us_ = static_cast<std::uint64_t>(time_stamp);  // t4

auto offset =
        req_rx_ts_us_ - req_tx_ts_us_ + resp_tx_ts_us_ - resp_rx_ts_us_;
offset /= 2;

// adjust the clock
AdjustClock(offset);

 

三、can_tsync

CAN时钟同步来源,AUTOSAR cp的规范,AUTOSAR定义的基于CAN总线时间同步的CanTSyn模块处理CAN总线上的时间信息分发,它以广播的形式将时间信息从master节点(TM) 传输到各slave节点(TS),还可通过时间网关(TW)将时间同步到其他子网,以解决因各ECU节点的硬件时钟信号偏差、CAN总线传输延时如协议仲裁以及各ECU节点内的软件处理等原因导致的时间延迟。网络拓扑如下:

1. can_tsync同步原理

整体来说,can的时间同步还是比较简单的,如下图所示,整个过程如下(tips:时间戳自1970年1月1日00:00:00经过的时间,是由秒+纳秒组成的。):
time master在t01时刻以广播的形式发送一个sync报文,并把时间部分的时间放到报文上,发送到time slave;使用can confirmation的机制,记下sync报文实际从can驱动发送出去的时间,t1r.
  1. time master在t01时刻以广播的形式发送一个sync报文,并把时间部分的时间放到报文上,发送到time slave;使用can confirmation的机制,记下sync报文实际从can驱动发送出去的时间,t1r.
  2. time slave在t2r时刻接收到sync报文
  3. time master在sync发送完之后,随后发送follow up报文,并把t1r的纳秒通过报文发送出去,即t4r = t2r-s(t0r)。这里有一个潜在条件,那就是sync报文由can timesync模块组装好报文后调用发送接口,直到从can driver上出去,整个时间是不会超过1s的。所以t4r实际上就是从can timesync报文发送出去直到can driver发送出去的一个延时。
  4. time slave在t3r接收到follow up报文。
  5. 因此在t3r时刻,master此刻真正的时间t(master_now) = t3r - t2r + t4r
注意:实际上,上面的时间大多都是不精确的:
  1. 时间戳是软件加上的,并不是由硬件加上的
  2. 没有考虑can总线上的延迟
  3. 没有考虑到从t3r到adjust时钟这段时间的误差。
  1. SYNC和FOLLOW_UP消息分为两种格式,Type=0x10为不安全的不带CRC校验的报文格式,对应FUP消息类型为0x18;Type=0x20为带CRC校验的安全报文格式,对应FUP消息类型为0x28。
  2. Byte0:时间同步类型:0x20代表当前发送的是带CRC校验的TSync同步消息, 0x28代表当前发送的是对应0x20 SYNC消息的FUP同步消息;0x10代表当前发送的是不带CRC校验的TSync同步消息, 0x18代表当前发送的是对应0x10 SYNC消息的FUP同步消息;
  3. Byte1:byte0为0x20或0x28时,Byte1为该消息的CRC校验值;
  4. Byte2:高4位为时间同步域Time Domain;低4位为Sequence Counter,随发送次数循环累加;
  5. Byte3:byte0为0x10或0x20时,Byte3为UserByte0;同步类型为0x28或0x18时,高5位保留, bit3 SGW为时间同步状态(0:SyncToGTM, 1:SyncToSubDomain),bit1-bit0 OVS为时间同步溢出时间overflow of seconds;
  6. Byte4-Byte7为同步时间,同步类型为SYNC消息时为32bits 秒时间,同步类型为FUP消息时为30bits ns时间。

2. can_tsync实现原理

有了上述基础之后,我们会理解到can timesync实现起来也不会太难,在autosar cp的框架下,所有的时间都是从StdmM(Synchronized Time-Base Manager)获取的。所以,我们可以如下伪代码
// can time master
TimeRaw_t t0r;
StbM_GetCurrentTimeRaw(&t0r);
CanTsync_SetT0r(t0r);
CanIf_Transmit(sync_message);
CanTsync_TxComfirmation()
{
    TimeRaw_t t1r;
    StbM_GetCurrentTimeRaw(&t1r);
    uint32_t t4r = CanTsync_GetT4r(t0r, t1r);
    CanTsync_SetT4r(t4r);
    CanIf_Transmit(fup_message)
}
// can time slave
TimeRaw_t t2r, t3r, t4r, t0r;
CanSync_RxIndication(sync_msg)
{
    StbM_GetCurrentTimeRaw(&t2r);
    t0r = CanTsync_GetT0r(sync_msg);
}
CanSync_RxIndication(fup_msg)
{
    StbM_GetCurrentTimeRaw(&t3r);
    t4r = CanTsync_GetT0r(fup_msg);
    TimeRaw_t real_time = CanTsync_CalcuTimeOffset(t0r, t2r, t3r, t4r);
    StbM_SetGlobalTime(real_time);
}

 

can tsync来源于autosar cp,所以上述实现风格也是仿照autosar cp的风格。StbM是整个cp的时间基础管理,负责抽象底层不同的时间同步协议,为上层提供统一的时间戳接口以及当前的时间同步状态的接口。整体框架如下:

四、ptp/gptp

在车载,vehicle time使用gptp来做vehicle time的同步,gptp算是ptp的简化版,规范定义来源于IEEE 802.1 AS,理论上可以达到ns级的误差。针对不通的ptp版本和gptp的对比如下:
  1. gptp同步原理

针对gptp,所有slave节点,都与master(grandmaster)的时钟保持同步;在车载领域,master节点都是静态指定的,并且从功能安全的角度来看,会选择具备功能安全的mcu来做为master节点。所以会一般选用gw(gateway)或者tbox来做master,而选择gw或者tbox对后续整车整个时间管理是策略会有影响。
master节点的sync报文(sync+follow up,以下用sync报文代替)会使用二层报文传遍整个时钟树,gptp中,sync报文使用二层报文,mac地址是指定的广播mac地址,但是实际上sync报文都是以单播的形式发送到下一跳节点,如果下一条节点是Bridage,则将重新修正correctionField(路由处理所消耗的时间),然后再将原来信息添加到sync报文从而路由到下一跳节点,直至到终端节点--End-Station。sync报文会包含master preciseOriginTimestamp、correctionField等。如下图:
slave节点会根据sync报文带上的preciseOriginTimestamp、correctionField来调整自己的时钟频率以及偏移;为了消除总线上的传输时延,slave节点会发送延迟测量报文,由于在车载每一跳都会有gptp协议栈,所以理论上测出的时钟同步是单向、精确的,如下图:
Pdelay=((t(4)-t(1)) - (t(3)-t(2)))/2
Pdelay测量的仅仅相邻两跳之间的传输时延,所以Pdelay是不会穿透Bridge的,从上面可以看到,gptp相对于can tsync不仅仅消除了传输延迟和路由报文时的处理延迟,同时时间戳是由硬件加上的,所以其时钟同步精度远远大于can tsync。
有了上述基础后,我们将所有gptp报文放一起,如下所示,并推导出slave节点用于调幅和调相的公式。
Pdelay = ((t6-t3)-(t5-t4))/2        
Gm = t1 + Pdelay + CorrectionField // 主时钟时刻+线缆传输时间+路由报文花销掉的时间
TimeOffset = t2 - Gm //用于调相或者调幅

Ratio = (Gm-Gm_last) / (t2-t2_last) //Gm_last和t2_last可以更久之前的。
FreqOffset = (1-Ratio)*1e9//用于调频

根据规范,sync报文一般是125ms发送一次;而Pdelay报文是1s发送一次,也可以是每次sync报文触发一次Pdelay报文,并且一般来说说同步精度是可配置的,当超过threshold时才去调整本地时钟。gptp调整的时钟(gptp时钟),是与网卡时钟源同一层级的时钟树端点,在linux上一般会抽象成设备,也就是/dev/ptpx;在使用硬件时钟戳时,当网络报文发送或者接收时的采样点到达时,会从gptp时钟上获取时间戳。采样点如下图所示:

额外:上图不仅仅展示了采样点,还展示了latency,如果是为了追求超高精度的时钟同步,需要将ingress_latency和egress_latency在实际计算时进行补偿。
gptp报文格式略微复杂,在这里不再具体展开,对于了解gptp原理的角度来说,可以暂时不用关注报文格式。
  1. gptp实现原理

有了上述基础后,写出gptp的代码就显得不是那么复杂了。以下以硬时钟同步为例:
// get t1, t2, t3, t4, t5, t6, adjust clock with /dev/ptp0
int clock_fd = open("/dev/ptp0", O_RDWR);
int RecvMsg(std::chrono::nanoseconds& hw_timestamp,
            std::array<std::uint8_t, MAX_PTP_LENGTH>& message, int flag)
{
    recvmsg(sock_fd, message, flag);
    // get hardware timestamp
    for (struct cmsghdr* control_message = CMSG_FIRSTHDR(&message);
         control_message;
         control_message = CMSG_NXTHDR(&message, control_message)) {
        if ((control_message->cmsg_level) == SOL_SOCKET) {
            // Fetch the Sync Rx/Tx timestamp
            if ((control_message->cmsg_type) == SO_TIMESTAMPING) {
                struct scm_timestamping* stamp =
                        (struct scm_timestamping*)CMSG_DATA(control_message);
                hw_timestamp = timespecToDuration(
                        stamp->ts[m_soTsConfig.tx_scm_ts_record_index]);
            }
        }
    }
}
// get t2
int OnSyncMsg(std::chrono::nanoseconds& t2)
{
    std::array<std::uint8_t, MAX_PTP_LENGTH> message;
    return RecvMsg(t2, message, 0);
}

// get t1, correction and adjust clock
int OnFollowUpMsg(std::chrono::nanoseconds& t1)
{
    std::array<std::uint8_t, MAX_PTP_LENGTH> message;
    std::chrono::nanoseconds unuse_timestamp;
    auto ret = RecvMsg(unuse_timestamp, message, 0);
    std::unique_ptr<PtpSlaveMsg> ptp_msg =
            std::make_unique<PtpSlaveMsg>(message);
    t1 = PtpSlaveMsg->GetPreciseOriginTimestamp();
    auto correction = PtpSlaveMsg->getCorrectionField();

    auto CaculateOffset = [
        t1, correction, &Gm, &ratio
    ](std::chrono::nanoseconds t2, std::chrono::nanoseconds pdelay) -> auto
    {
        Gm = t1 + pdelay + correction;
        ratio = (Gm - Gm_lat) / (t2 - t2_last);
        return (t2 - Gm);
    };
    auto AdjustClock = [ratio](std::chrono::nanoseconds offset) {
        AdjustClockOffset(offset);
        adjustClockFreq(ratio);
    };
    if (auto offset = CaculateOffset() > threshold) {
        AdjustClock(offset);
    }
    Gm_last = Gm;
    t2_last = t2;
}
// get t3
int SendPdelayReq()
{
    SetTriggerTimeStamp(pdelay_msg);
    auto ret = sendmsg(sock_fd, pdelay_msg, 0);
    GetTimestampWithSendmsg(t3)
    {
        std::array<std::uint8_t, MAX_PTP_LENGTH> message;
        RecvMsg(socket_fd, message, MSG_ERRQUEUE)
    }
}
// get t4, t6
int OnPdelayResp(std::chrono::nanoseconds& t4, std::chrono::nanoseconds& t6)
{
    std::array<std::uint8_t, MAX_PTP_LENGTH> message;
    auto ret = RecvMsg(t4, message, 0);
    std::unique_ptr<PtpSlaveMsg> ptp_msg =
            std::make_unique<PtpSlaveMsg>(message);
    t6 = PtpSlaveMsg->GetPreciseOriginTimestamp();
}

// get t5 and  pdelay
int OnPdelayResp(std::chrono::nanoseconds& pdelay)
{
    std::array<std::uint8_t, MAX_PTP_LENGTH> message;
    std::chrono::nanoseconds unuse_timestamp;
    auto ret = RecvMsg(unuse_timestamp, message, 0);
    std::unique_ptr<PtpSlaveMsg> ptp_msg =
            std::make_unique<PtpSlaveMsg>(message);
    t5 = PtpSlaveMsg->GetPreciseOriginTimestamp();

    pdelay = ((t6 - t3) - (t5 - t4)) / 2
}
int clock_fd = open("/dev/ptp0", O_RDWR);
int RecvMsg(std::chrono::nanoseconds & hw_timestamp,
            std::array<std::uint8_t, MAX_PTP_LENGTH> & message, int flag)
{
    recvmsg(sock_fd, message, flag);
    // get hardware timestamp
    for (struct cmsghdr* control_message = CMSG_FIRSTHDR(&message);
            control_message;
            control_message = CMSG_NXTHDR(&message, control_message)) {
        if ((control_message->cmsg_level) == SOL_SOCKET) {
            // Fetch the Sync Rx/Tx timestamp
            if ((control_message->cmsg_type) == SO_TIMESTAMPING) {
                struct scm_timestamping* stamp =
                        (struct scm_timestamping*)CMSG_DATA(
                                control_message);
                hw_timestamp = timespecToDuration(
                        stamp->ts[m_soTsConfig.tx_scm_ts_record_index]);
            }
        }
    }
}

// get t1
int SendSyncMsg(std::chrono::nanoseconds & t1)
{
    SetTriggerTimeStamp(sync_msg);
    auto ret = sendmsg(sock_fd, sync_msg, 0);
    GetTimestampWithSendmsg(t1)
    {
        std::array<std::uint8_t, MAX_PTP_LENGTH> message;
        RecvMsg(socket_fd, message, MSG_ERRQUEUE)
    }
}

// get t5
int SendPdelayRespMsg(std::chrono::nanoseconds & t5)
{
    SetTriggerTimeStamp(fup_msg);
    auto ret = sendmsg(sock_fd, req_msg, 0);
    GetTimestampWithSendmsg(t5)
    {
        std::array<std::uint8_t, MAX_PTP_LENGTH> message;
        RecvMsg(socket_fd, message, MSG_ERRQUEUE)
    }
}
// sedn t5
int SendPdelayFupMsg()
{
    SetPreciseOriginTimestamp(t5);  // set t4
    auto ret = sendmsg(sock_fd, fup_msg, 0);
}

// get t4 and send t4
int OnPdelayReq(std::chrono::nanoseconds & t5)
{
    std::array<std::uint8_t, MAX_PTP_LENGTH> message;
    std::chrono::nanoseconds t4;
    auto ret = RecvMsg(t4, message, 0);  // get t4
    SetPreciseOriginTimestamp(t4);       // set t4
    SendPdelayRespMsg(t5);                // send pdelay response with t4
    SendPdelayFupMsg();
}

 

上述代码示例,仅仅是为了说明在代码实现时,如何获取到同步原理提到的各个时间戳,并且如何通过时间戳计算出来time offset以及freq offset。而真正的代码实现时,在调整时钟时,使用使用到clock的一系列接口,并且为了提高精度,还需要考虑消除接口调用带来的时间消耗,并且使用一些手段来补偿上去。

五、时间融合和使用

对于整车来说,Tsync模块需要将整车所有vehicle time和utc同步好,对于用户来说,开发者最好提供获取vehicle time和utc时间的接口,用户无需要关注时钟同步的过程和细节。
  1. 时间融合与utc同步

前面提到vehicle time和utc的精度不一样,所以使用场景各有不同,也就意味着同一个ecu内,应该同时存在上述两种时间,我们以网关(GW),座舱控制器(CDC),智驾控制器(ADC)以及TBOX为例分析。
如上图所示,vehicle time可以借助gptp以及can_tsync让所有ecu保持同步,而utc时间必须借助外部环境先同步TBOX。但是如何让GW, CDC ,ADC也能同步UTC时间呢?
针对上述拓扑,vehicle master和utc master不在同一个ecu,可以借助当前最火的SOA思想,在TBOX上部署UtcServiceProvider,提供GetUtc和PubUtc两种接口,而GW, ADC, CDC则可以部署UtcServiceConsumer。为了消除SOA传输带来的延迟,我们可以将TBOX的utc和vehicle time一同给到consumer端,这样的话,UTC(consumer)=UTC(provider)+(vehicle_time(consumer)-vehicle_time(provider))。
如果vehicle master和utc master在同一个ecu,如下图,这种方式相对来说,会更加简单一点。比如我们可以将utc的时间在tbox上于vehicle time同步,然后通过gptp的报文,简介的同步所有的ecu的utc时间。
不管上述任何一种情况,实际并不复杂,前一种让架构部门输出soa的描述语言(arxml,idl),由下游直接生成服务和实现服务即可;而后一种情况从方式较为简单了,但是需要考虑时间跳变的问题。
  1. 对外接口

针对使用者来说,期望能够直接获取utc时间或者vehicle time,所以开发者理应再提供接口直接获取,屏蔽使用者无需关心的细节。代码示例如下:
class JinbaoClock {
public:    static struct timespec GetVehicleTime()
    {
        struct timespec ts_ptp;
        if (clock_gettime(clk_id_, &ts_ptp)) {
            return {};
        }
        return ts_ptp;
    }

    static struct timespec GetUtc()
    {
        struct timespec ts_utc;
        if (clock_gettime(CLOCK_REALTIME, &ts_utc)) {
            return {};
        }
        return ts_utc;
    }
};

 

六、写在最后

程序员的学会永远是可以动手写代码,所以今天从程序员视角下,总结了一下车载内的时钟同步。上述方案其实都是基于在工作中以及实现之后的一个总结,只不过写出来的是一些common部分。但是受限于篇幅很多细节也没有全部展现出来,比如stbm的细节描述,gptp的报文格式。除了can tsync是mcu风格的代码,其他都是linux/c++风格的代码。
同时也简化了处理过程,比如,如果真正要实现一个ntp协议栈,仅仅有ntp client req和ntp server response是不够的;ptp调整时钟的过程,可以加上算法,比如pid,同时也需要想尽办法测试出来接口调用的耗时,以便做补偿,也正因为接口调用耗时,所以对于不同的系统,能够达到的时钟同步精度是不一致的。
另外,既然是调整了时基,就不得不考虑时钟跳变带来的影响,甚至调整的时钟不是线性增长的,这对数据库以及传感器来说可能是个灾难,所以时钟调整的规则也要针对不同的应用场景逐步适配。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇