IIC通信协议
本文最后更新于23天前,其中的信息可能已经有所发展或是发生改变。

同步时序和异步时序

同步时序

存储电路中所有触发器的时钟输入端都接同一个时钟脉冲源,因而所有触发器的状态的变化都与所加的时钟脉冲信号同步。存储电路中所有触发器的时钟使用统一的clk,状态变化发生在同一时刻。

同步时序就可以极大地降低单片机对硬件电路的依敕。

同步时序的好处就是,对时间要求不严格,对硬件电路不怎么依赖,在一些低端单片机没有硬件资源的情况下,也很容易使用软件的模拟时序,缺点就是多一根时钟线

同步逻辑电路的特点:

电路所有的触发器的输入输出都只与系统的时钟脉冲有关系

电路可以有多个时钟信号,但都与系统时钟有着因果关系

同步电路的状态会持续,直到下一个时钟脉冲信号到来才会改变状态

异步时序

电路没有统一的时钟,有些触发器的时钟输入端与时钟脉冲源相连,这有这些触发器的状态变化与时钟脉冲同步,而其他的触发器的状态变化不与时钟脉冲同步。没有统一的clk,触发器状态的变化有先有后。

异步时序的好处就是节省资源,缺点就是对时间要求严格,对硬件电路的依赖比较严重

异步逻辑电路的特点:

电路中没有统一的共同时钟

寄存器既有时钟脉冲驱动,也有不带时钟驱动;

I2C通信

I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线

可以用来连接存储器(EEPROM、FLASH)、A/D、D/A转换器、LCD驱动器、传感器等等。

比如下图中的MPU6050模块、OLED模块、AT24C02存储器模块、DS3231实时时钟模块

两根通信线:SCL(Serial Clock)、SDA(Serial Data)I2C一共有只有两个总线: 一条是双向的串行数据线SDA,一条是串行时钟线SCL。

SDA(Serial data)是数据线,D代表Data也就是数据,Send Data 也就是用来传输数据的

SCL(Serial clock line)是时钟线,C代表Clock 也就是时钟 也就是控制数据发送的时序的

所有接到I2C总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。I2C总线上的每个设备都自己一个唯一的地址,来确保不同设备之间访问的准确性。

同步,半双工

带数据应答

支持总线挂载多设备(一主多从、多主多从)

硬件电路

所有I2C设备的SCL连在一起,SDA连在一起

设备的SCL和SDA均要配置成开漏输出模式

上图是,I2C的一个典型电路模型,这是一个一主多从的模型,左边cpu就是单片机,作为总线的主机,主机的权力很大,任何时候都是主机完全掌控SCL线,另外在空闲状态,下主机可以主动发起对SDA的控制,只有在从机发送数据和从机应答的时候,主机才会转交SDA的控制权给从机,这是主机的权利

主机拥有SCL的绝对控制权,所以主机的SCL可以配置成推挽输出,从机的SCL都配置成浮空输入或者上拉输入。数据流向是,主机发送,所有从机接收。

主机的SDA在发送的时候是输出,接收的时候是输入。从机的SDA也会在输入和输出之间反复切换,如果总线时序没协调好,极有可能发生两个引脚同时处于输出的状态,如果这时又正好是一个输出高电平,一个输出低电平,那这个状态就是电源短路,这个状态是要极力避免的。

所以为了避免总线没协调好导致电源短路这个问题。I2C的设计是,禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出的电路结构(图中右上角)

SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右

所有的设备,包括CPU和被控IC,它引脚的内部结构都是上图这样的,左边这一块是scl的结构,这里SCLK就是SCL的意思,右边这一块是SDA的结构,这里DATA IN就是SDA的意思

首先引脚的信号进来,都可以通过一个数据缓冲器或者施密特触发器,进行输入,因为输入对电路没有任何影响,所以任何设备在任何时刻都是可以输入的。

但是在输出的这部分,采用的是开漏输出的配置

正常的推挽输出是下图所示。

上面导通,输出高电平。

下面导通,输出低电平。

因为这是通过开关管直接接到正极和负极的,所以这个是,强上拉和强下拉的模式。

而开漏输出呢,就是去掉这个强上拉的开关管

输出低电平时,下管导通,是强下拉

输出高电平时,下管断开,但是没有上管了,此时引脚处于浮空的状态,这就是开漏输出

和图示里SCLKN1 OUT是一样的。

输出低电平,开关管导通,引脚直接接地,是强下拉。

输出高电平,开关管断开,引脚什么都不接,处于浮空状态,这样的话所有的设备都只能输出低电平,而不能输出高电平。

为了避免高电平造成的引脚浮空,这时就需要在总线外面,SCL和SDA各外置一个上拉电阻,这是通过一个电阻拉到高电平的,所以这是一个弱上拉

为了防止多个外设存在SCL同时输出高电平和低电平状态,造成冲突。

规定,所有的外设,不准向SCL输出高电平,此时SCL是弱上拉,不影响数据传输。

第一完全杜绝了电源短路现象,保证电路的安全,所有设备无论怎么样,都不会处于一个被同时强拉和强推的状态。

第二避免了引脚模式的频繁切换,开漏加弱上拉的模式,同时兼具了输入和输出的功能,因为开漏模式下,输出高电平就相当于断开硬件,所以在输入之前可以直接输出高电平,不需要再切换成输入模式

第三这个模式会有个“线与”的现象,只要有任意一个或多个设备输出了低电平,总线就处于低电平 ,只有所有的设备都输出高电平,总线才处于高电平。,I2C可以利用这个电路特征,执行多主机模式下的时钟同步和总线仲裁,所以这里SCL虽然在一主多重模式下可以用推挽输出,但是它仍然采用了开漏加上拉输出的模式,因为在多主机模式下会利用到这个特征

I2C时序基本单元

起始条件

起始条件:SCL高电平期间,SDA从高电平切换到低电平

空闲状态时,SCL和SDA都处于高电平状态,总线处于平静的高电平状态状态1),当主机需要进行数据收发时,首先就要打破总线的宁静,产生一个起始条件,当从机捕获到这个SCL高电平SDA下降沿信号时,就会进行自身的复位,等待主机的召唤(状态2)。

然后在SDA下降沿之后,主机要再把SCL拉低电平状态3),在拉低SCL电平,一方面是占用这个总线,另一方面也是为了方便我们这些基本单元的拼接,就是我们之后会保证除了起始和终止条件,每个时序单元的SCL都是以低电平开始,低电平结束。

终止条件

终止条件:SCL高电平期间,SDA从低电平切换到高电平

SCL先放手回弹到高电平状态1),SDA再放手回弹高电平,产生一个上升沿状态2)。这个上升沿触发终止条件,同时终止条件之后,SCL和SDA都是高电平,回归到最初的平静状态(状态3)。

这个起始条件和终止条件,就类似串口时序里的起始位和停止,一个完整的数据帧总是以起始条件开始,终止条件结束,另外,起始和终止都是由主机产生的,从机不允许产生起始和终止,所以在总线空闲状态时,从机必须始终双手放开,不允许主动跳出来去碰这总线,如果允许的话,那就是多主机模型了,不在本节的讨论范围之内,这就是起始条件和终止条件。

在起始条件之后,这时就可以紧跟着一个发送一个字节的时序单元,如何发送一个字节呢?

发送一个字节

SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节

起始条件之后,第一个字节也必须是主机发送的,主机如何发送呢,就是最开始,SCL低电平,主机如果想发送0,就拉低SDA到低电平,如果想发送1就放手SDA回弹到高电平。在SCL低电平期间,允许改变SDA的电平状态1)。

当这1位放好之后,主机就松手,时钟线SCL回弹到高电平。在高电平期间是从机读取SDA的时候,所以高电平期间SDA不允许变化。SCL处于高电平之后,从机需要尽快的读取SDA,一般都是在上升沿这个时刻,从机就已经读取完成了,因为始终是主机控制的从机,并不知道什么时候就会产生下降沿了,从机在上升沿时就会立刻把数据读走(状态2)。

那主机在放手SCL一段时间后,就可以继续拉低SCL,传输下一位。主机也需要在SCL下降沿之后,尽快把数据放在SDA上,但是主机有时钟的主导权,所以主机并不需要那么着急,只需要在低电平的任意时刻,把数据放在SDA上就行了,晚点也没关系,数据放完之后,主机再放手SCL,SCL高电平从机读取这一位(状态3),就这样的流程。

主机拉低SCL把数据放在SDA上;主机松开SCL,从机读取SDA的数据。在SCL的同步下,依次进行主机发送和从机接收,循环八次就发送了八位数据,也就是一个字节。

高位先行,所以第一位是一个字节的最高位b7,然后依次是次高位b6 等等,最后发送最低位b0 ,这个和串口是不一样的,串口时序是低位先行,这里I2C是高位先行。

另外由于这里有时钟线进行同步,所以如果主机一个字节发送一半,突然进中断了,不操作SCL和SDA了,那时序就会在中断的位置不断拉长,SCL和SDA电平都暂停变化,传输也完全暂停,等中断结束后,主机回来继续操作,传输仍然不会出问题,这就是同步时序的好处

就是说老师在上课的时候突然拉稀了,老师可以不管学生直接中断去厕所,上完厕所再回来接着上课。

最后就是,由于这整个时序是主机发送一个字节,所以在这个单元里,SCL和SDA全程都有主机掌控,从机只能被动读取。

接收一个字节

SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA

释放SDA其实就相当于切换成输入模式,或者这样来理解所有设备处于输入模式,当主机需要发送的时候,就可以主动去拉低SDA,而主机在被动接收的时候,就必须先释放SDA,不要去动它,以免影响别人发送来的数据。因为总线是线与的特征,任何一个设备拉低了总线就是低电平,如果接收的时候还拽着SDA不放手,那别人无论发什么数据,总线都始终是低电平,所以主机在接收之前需要释放SDA

从流程上来看,接收一个字节和发送一个字节,是非常相似的。

区别就是:

发送一个字节是低电平主机放数据,高电平从机读数据

接收一个字节是低电平从机放数据,高电平主机读数据

时序和发送的基本一样,区别就是SDA线。

主机在接收之前需要释放SDA,然后这时从机就取得了SDA的控制权,从机需要发送零就把SDA拉低,从机需要发送1就放手,SDA回弹高电平。

然后同样的低电平变换数据,高电平读取数据。(这里实线部分表示主机控制的电平啊,虚线部分表示从机控制的电平。)SCL全程由主机控制,SDA主机在接收前要释放,交由主机控制

之后还是一样,因为SCL始终是由主机控制的,所以从机的数据变换基本上都是贴着SCL,下降也进行的,而主机可以在SCL高电平的任意时刻读取,这是接收一个字节的时序。

发送应答

主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答

在接收一个字节之后,我们也要给从机发送一个应答位,发送应答位的目的是告诉从机,你是不是还要继续发,如果从机发送一个数据后,得到了主机的应答,那从机就还会继续发送,如果从机没得到主机的应答,那从机就会认为那我发生了一个数据,但是主机不理我,可能主机不想要了吧。这时从机就会释放SDA,交出SDA的控制权,防止干扰主机之后的操作,这就是应答位的执行逻辑。

接收应答

主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)

当调用发送一个字节之后,就要紧跟着调用接收应答的时序,用来判断从机有没有收到刚才给打的数据,如果从机收到了,那在应答位这里,主机释放SDA的时候从机就应该立刻把SDA拉下来,然后在SCL高电平期间,主机读取应答位,如果应答位为零,就说明从机确实收到了。

I2C数据帧

I2C是一主多从的模型,通过设备地址确定设备,主机可以访问总线上的任何一个设备,把每个从设备都确定一个唯一的设备地址,主机在起始条件之后,要先发送一个字节叫一下从机地址, 所有从机都会收到第一个字节,和自己的地址进行比较。如果一样,就说明,主机现在在请求和这个从机通信,就响应之后主机的读写操作。

从机设备地址,在12C协议标准里分为7位地址和10位地址。每个I2C设备出厂时,都会确定一个7位I2C地址,这个地址具体是什么,可以在芯片手册里找到,比如后面使用MPU6050这个芯片的7位地址是1101 000,AT24C02的7位地址1010 000。

相同的芯片挂载在同一条总线,需要用到地址中的可变部分,一般器件地址的最后几位是可以在电路中改变的,即使相同型号的芯片,挂载在同一个总线上,也可以通过切换地址低位的方式,保证每个设备的地址都不一样。

指定地址写

对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)

对于指定设备通从机地址(Slave Address )确定。对指定设备的内部寄存器地址(Reg Address)写入数据。

起始条件(Start,S)

空闲状态,都是高电平,然后主机需要给从机写入数据的时候,首先SCL高电平期间,拉低SDA产生起始条件(Start,S)

发送一个字节(Send Byte)

在起始条件之后,紧跟着的时序,必须是发送一个字节(8位)的时序,字节的内容必须是从机地址加读写位(Slave Address + R / W),正好从机地址是七位,读写位是一位,加起来是一个字节(8位)

发送从机地址,就是确定通信的对象。发送读写位,就是确认接下来是要写入还是要读出。

具体发送的时候,在这里低电平期间SDA变换数据,高电平期间从机读取SDA,(这里使用绿色的线来标明了从机读到的数据),比如图中的波形,那从机收到的

第一位就是高电平1,然后SCL低电平,主机继续变换数据

第二位还是1,所以这里SDA电平并没有变化,然后SCL高电平,从机读到第二位是1。

之后继续按低电平变换数据,高电平读取数据

第三位就是0。

这样持续八次就发送了一个字节数据,其中这个数据的定义是,高七位表示从机地址

这个波形下,主机寻找的从机地址就是1101000,这个就是MPU6050的地址,最低位表示读写位0,表示之后的时序,主机要进行写入操作。(1表示之后的时序主机要进行读出操作,0表示之后要进行写入操作

目前主机是发送了一个字节,高位先行就是0xD0(1101 0000)

应答位

根据协议规定,紧跟着的单元就得是接收从机的应答位(RA:0),在这个时刻,主机要释放SDA,

如果单看主机的波,应该是这样,释放SDA之后引脚电平回弹到高电平。但是根据协议规定,从机要在这个位拉低SDA。

单看从机的波形,应该是如上图 。应该应答的时候,从机立刻拽住SDA,然后应答结束之后,从机再放开SDA。

现在综合两者的波形,结合线与的特性,在主机释放SDA之后,由于SDA也被从机拽住了,所以主机松手后,SDA并没有回弹,高电平这个过程就代表从机产生的应答,最终高电平期间主机读取SDA发现是0,就说明我进行寻址,有应答传输没问题。

如果主机读取SDA发现是1,就说明主机进行寻址,应答位期间,主机松手了,但是没回弹高电平,没有应答,那就直接产生停止条件吧,并提示一些信息

继续紧接着上升沿,就是应答位结束后,从机释放SDA产生的,从机交出了SDA的控制权,因为从机要在低电平尽快变换数据,所以这个上升沿和SCL的下降沿,几乎是同时发生的。

第二个字节

由于之前读写位给了0,所以应答结束后,要继续发送一个字节,同样的时序再来一遍,第二个字节就可以送到指定设备的内部来,从机设备可以自己定义,第二个字节和后续字节的用途,一般第二个字节可以是寄存器地址,或者是指令控制字等,比如mpu6050定义的第二个字节就是寄存器地址。比如AD转换器,第二个字节可能就是指令控制字。比如存储器,第二个字节可能就是存储器地址。

那图示,主机发送这样一个波,一一判定数据为0001 1001,即主机向从机发送的0x19 ,这个数据在MPU6050里,就表示,要操作你0x19地址下的寄存器,这同样是从机应答,主机释放SDA从机拉低SDA,SDA表现为低电平,主机收到应答位为0,表示收到了从机的应答,然后继续同样的流程,再来一遍,主机再发送一个字节,这个字节就是主机想要写入到,0x19地址下寄存器的内容了,比如这里发送了0xAA的波形,就表示我在0x19地址下写入0xAA,最后是接收应答位,如果主机不需要继续传输了,就可以产生停止条件(Stop,P)。

停止条件(P)

在停止条件之前先拉低SDA,为后续SDA的上升沿做准备。然后释放SCL,再释放SDA。这样就产生了,SCL高电平期间,SDA的上升沿,这样一个完整的数据帧就拼接完成了。

这个数据帧的目的,就是对于指定从机地址为1101 000的设备,在其内部0x19地址下的寄存器中,写入0xAA这个数据,这是指定地址写的时序

当前地址读

完成的任务是对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)

这就是当前地址读的时序,如果主机想要读取从机的数据,就可以执行这个时序

起始条件(S)

那最开始还是SCL高电平期间,拉低SDA产生起始条件,起始条件开始后。 ,主机必须首先调用发送一个字节,来进行从机的寻址和指定读写标志位。

比如图示的波形表示,本次寻址的目标是100 1000的设备,同时最后一位读写标志为1,表示主机接下来想要读取数据,紧跟着,发送一个字节之后,接收一下从机应答位,从机应答0,代表从机收到了第一个字节,在从机应答之后。

从这里开始数据的传输方向就要反过来了,因为刚才主机发出了读的命令,所以这之后主机就不能继续发送了,要把SDA的控制权交给从机,主机调用接收一个字节的时序进行接收操作,在这一块从机就得到了主机的允许,可以在SCL低电平之间写入SDA,然后主机在SCL高电平期间读取SDA,那最终主机在SCL高电平期间,一次读取八位,就接收到了从机发送的一个字节数据,00001111,也就是0x0f,

那现在问题就来了,这个0x0f是从机哪个寄存器的数据呢?

当前地址指针

在读的时序中,I2C协议的规定是,主机进行寻址时,一旦读写标志位给1,下一个字节就要立马转为读的时序,所以主机还来不及指定想要读哪个寄存器,就得开始接收了。所以这里就没有指定地址这个环节,那主机并没有指定寄存器的地址,从机到底该发哪个寄存器的数据,

这需要用到上面说的当前地址指针,在从机中,所有的寄存器被分配到了一个线性区域中,并且会有个单独的指针变量,指示着其中一个寄存器,这个指针上电默认,一般指向0地址。并且每写入一个字节和读出一个字节后,这个指针就会自动自增一次,移动到下一个位置,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值。

那假设刚刚调用了这个指定地址写的时序,在0x19的位置写出了0xAA,那么指针就会+1,移动到0x1A的位置,返回的就是0x1A地址下的值。再调用这个当前地址读的时序,返回的就是0x1B地址下的值,以此类推,这就是当前地址读时序的操作逻辑。由于当前地址读并不能指定读的地址,所以这个时序用的不是很多。

指定地址读

这个时序的目的就是对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)

指定地址写中,在这前面一部分就是指定地址的时序,把最后面的写数据的这一部分给去掉,然后把前面这一段设置地址,还没有指定写什么数据的时序,追加到这个当前地址读时序的前面,就得到了指定地址读的时序。一般也把它称作复合格式

将上面的时序分隔一下,分成两个部分

前面的部分是指定地址写,但是只指定了地址,还没来得及写。

后面的部分是当前地址读,因为刚指定的地址,两者加在一起就是指定地址读了。

所以指定地址读的时序会复杂一些,详细分析一下看看

第一部分寻找从机地址和从机寄存器地址

首先最开始仍然是启始条件,然后发送一个字节进行寻址,这里指定从机地址是100 1000,读写标志位是0,代表要进行写的操作,经过从机应答之后,再发送一个字节第二个字节用来指定寄存器地址,这个数据就写入到了从机的地址指针里了,也就是说从机接收到这个数据之后,它的寄存器指针就指向了0x19。

第二部分切换读写方向,读取上一部分确认的寄存器

这个位置之后,我们要写入的数据,不给他发,而是直接再来个起始条件,这个Sr(Start Repeat)的意思就是重复起始条件,相当于另起一个时序

因为指定读写标志位,只能是跟着起始条件的第一个字节,所以如果想切换读写方向,只能再来个起始条件。

然后起始条件后,重新寻址(100 1000),并且指定读写标志位1,代表要开始读了,接着主机接收一个字节。这个字节就是0x19 地址下的数据,这就是指定地址读。

也可以在蓝框再+1个停止条件,这样的话就是两个完整的时序了。

先起始(S),写入地址(Send),停止(P)。

因为写入的地址会存在地址指针里面,所以这个地址并不会因为时序的停止而消失。

再起始(Sr),读当前位置(Receive),停止(P)。

这样两条时序也可以完成任务,但是I2C协议官方规定的复合格式是一整个数据帧。即:先起始(S)再重复起始(Sr),再停止(P)。相当于把两条时序拼接成一条。

这些就是3个I2C完整时序的介绍。,其中第一个指定地址写和第三个指定地址读用的比较多,除了这三个时区,I2C还有这些时序的进阶版本,大概介绍一下。

应答位(ACK)的作用

如果只想读一个字节就停止的话,在读完一个字节之后,一定要给从机发个非应答,(Send Ack,SA)。

非应答就是该主机应答的时候,主机不把SDA拉低,从机读到SDA为1,就代表主机没有应答,从机收到非应答之后,就知道主机不想要继续了,从机就会释放总线,把SDA的控制权交还给主机。

如果主机读完仍然给从机应答了,就会认为主机还想要数据,就会继续发送下一个数据,而这时,主机如果想产生停止条件,SDA可能就会因为被从机拽住了,而不能正常弹回高电平,这个注意一下

如果主机想连续读取多个字节,就需要在最后一个字节给非应答,而之前的所有字节都要给应答。

简单来说就是主机给应答了,从机就会继续发。主机给非应答了,从机就不会再发了,并交出SDA的控制权

从机控制SDA发送一个字节的权利,开始于读写标志位为1,结束于主机给应答位为1,这是主机给从机发送应答位的作用

I2C时序的进阶版本

这些时序,指定地址写只是写一个字节,当前地址读和指定地址读,也都是读一个字节,那进阶版本就是指定地址写多个字节当前地址读多个字节指定地址读多个字节

实际上和单个字节读写都非常相似,只需要增加一些小细节就行。

指定地址写多个字节

指定地址,然后写入一个字节,如果只想写一个字节,那就停止就行,如果想写多个字节,就可以把这最后一部分(Send Byte)多重复几次,比如重复三遍,发送一个字节和接受应答。这样第一个数据就写入到了指定地址0x19 的位置,然后不要忘了,写入一个数据后,地址指针会自动+1,变成0x1A,所以这第二个数据就写入到了,0x1A的位置。同理,第三个数据就写入的是0x1b的地址,以此类推,这样这个时序就进阶为在指定的位置开始,按顺序连续写入多个字节。

比如你需要连续写入多个寄存器,就可以考虑这样来操作,这样在一条数据帧里,就可以同时写入多个字节,执行效率就会比较高。

同理当前位置读和指定位置读,也可以多次执行这最后一部分时序,由于地址指针在读后也会自增,所以这样就可以连续读出一片区域的寄存器,效率也会非常高。

总结

以上就是I2C总线的硬件规定和软件规定,有了这些规定,就可以按照硬件规定来连接线路,用软件规定来操作总线,以此实现指定位置写寄存器和指定位置读寄存器,有了这两个功能,主机就可以完全掌控外挂模块的运行,也就实现了这个协议的目的。

点击数:7

    暂无评论

    发送评论 编辑评论

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