实验室项目小结

1 嵌入式操作系统

  • 为什么要用嵌入式操作系统

普通的单片机编程:程序(软件)——单片机硬件;

嵌入式操作系统开发:程序(软件)——操作系统——嵌入式硬件(包括单片机等);

我们平时普通所学的单片机编程开发,一般情况下都需要对单片机的片载资源进行了解,了解IO口、PWM发生器、中断、定时器、串口等常用的内部资源,然后一般采用C编程的面向过程编程,程序的运行过程从进入入口函数开始运行,是顺序执行的。单片机的开发,相对来说比较简单,但是不同的单片机,要实现相同的功能,其内部程序必然存在差别,也就是所说的移植性较差,另外在开发单片机时,必然要阅读芯片手册,对可用的资源有一定的硬件上的了解。在事件的执行上也不如嵌入式开发来得效率高。

嵌入式开发,也就是在已有的硬件基础上移植操作系统开发者所写的软件程序运行在操作系统中,通过调用操作系统提供的 API 接口等调用硬件资源,实现想要实现的功能。操作系统某种程度上屏蔽了底层的硬件,可以暂时不去考虑操作系统是如何调用硬件资源问题,这其中涉及到驱动等知识。

当然,想要实现移植操作系统的单片机,相对来说其性能就要求高一些,像普通的51芯片,即使移植剪裁到很小的Linux系统,运行起来也很吃力。

开发者所写的程序是与操作系统相关,与硬件无关,只要运行在相同的操作系统中即可,因此嵌入式开发具有较好的移植性,对底层硬件也不强制要求熟悉,此外嵌入式系统通常是并发式执行任务,因此执行效率要好。比如要说得us/Os II 可同时管理64个任务,多个任务的处理在同时间段内同步完成。(这里涉及到多线程以及CPU时间片的概念)。

总结来说,与单片机开发相比,嵌入式开发有如下优点:

  1. 移植性好;
  2. 屏蔽硬件,不要求对硬件资源的熟悉性;
  3. 多任务实时性高,效率高;

除此之外,嵌入式操作系统还具备很多优越性:精简的内核,高实时性,多任务的操作系统;高可靠性;可裁剪性好;

百度百科的解释还是很有道理的:

嵌入式操作系统(Embedded Operating System,简称:EOS)是指用于嵌入式系统操作系统。嵌入式操作系统是一种用途广泛的系统软件,通常包括与硬件相关的底层驱动软件、系统内核、设备驱动接口、通信协议图形界面、标准化浏览器等。嵌入式操作系统负责嵌入式系统的全部软、硬件资源的分配、任务调度控制、协调并发活动。它必须体现其所在系统的特征,能够通过装卸某些模块来达到系统所要求的功能。目前在嵌入式领域广泛使用的操作系统有:嵌入式实时操作系统µC/OS-II、嵌入式LinuxWindows EmbeddedVxWorks等,以及应用在智能手机和平板电脑的AndroidiOS等。

2 μC/OS-II系统

2.1 μC/OS-II特点

  • 小巧的实时操作系统,整个代码分为内核层和移植层,方便移植和裁剪
  • 抢占式调度策略,保证任务的实时性
  • 可管理多达64个任务
  • 提供了用于任务通信的事件(信号量,消息队列,邮箱等)管理,内存管理,时间管理等系统服务

2.2 内核结构

  • 临界段(Critical Sections):临界区,指处理是不可分割的代码,一旦这部分代码开始执行了,则不允许任何中断打断

    1. 实现资源共享
    2. 处理临界段代码是需要关中断,处理完毕后再打开中断,系统定义了两个宏(macros)来开关中断
    OS_ENTER_CRITICAL()
    OS_EXIT_CRITCAL()
    //宏定义的具体实现取决于移植所用的微处理器,每种微处理器都要自己的OS_CPU.H文件
    
  • 任务:

    1. 任务通常是无限循环
    2. 系统可管理多达64个任务,但保留了0,1,2,3,OS_LOWEST_PRIO-3,OS_LOWEST_PRIO-2,OS_LOWEST_PRIO-1,OS_LOWEST_PRIO这8个任务以备系统使用,其中包括系统空闲任务,和系统CPU效率使用计算等任务
    3. 用户可使用多达56个任务,在创建任务时需要给任务赋予不同的优先级,优先级号越低,任务优先级越高。μC/OS-II系统的任务调度是基于优先级的。
    4. OS_LOWEST_PRO 该变量可以在中OS_CFG.H 中定义,说明应用程序中任务优先级别的数目
  • 中断处理:

    1. 中断服务子程序框架:保存全部CPU寄存器 -- 调用OSIntEnter或者OSIntNesting直接加1 -- 执行用户代码中断服务(汇编或者C编译器支持在线汇编) -- 调用OSIntExit -- 恢复CPU寄存器 -- 执行中断返回命令
    2. 注意:中断服务子程序运行结束后会发生一次任务调度,所以并不一定会继续运行被中断的任务
  • 时钟节拍:

    1. 时钟节拍是一种特殊得中断,操作系统的心脏,对任务列表进行扫描,判断是否有延时任务应该处于准备就绪状态,进行上下文切换,也就是切换任务
    2. μC/OS-II系统需要用户提供周期性信号源,系统在多任务系统启动以后再开启时钟节拍器
    3. OSTickISR():硬件定时器以时钟节拍为周期定时产生中断,该中断服务程序为OSTickISR,该函数中主要调用了OSTIMETick()函数来完成系统在每个时钟节拍需要完成的工作
    保存处理器寄存器的值
    调用OSIntEnter()或是OSIntNesting加1
    调用OSTimeTick()
    调用OSIntExit()
    恢复处理器寄存器的值
    执行中断返回指令
    
    1. OSTimeTick():时钟节拍服务函数做的工作包括:记录节拍树,任务延时减一,其任务时在每个时钟节拍了解任务的延时状态,使得其中延时结束的非挂起任务进入就绪状态
  • μC/OS-II系统初始化

    1. OSInit():建立空闲任务idle task,这个任务总是处于就绪态,空闲任务OSTaskIDLE()的优先级总是设成最低
    2. 初始化4个空数据结构缓冲区:OS_MAX_TASKS(OSTCBFreeList)任务控制块链表,OS_MAX_EVENTS(OSEventFreeList)ECB时间控制块链表,OS_MAX_QS(OSQFreeList),OS_MAX_MEM_PART(OSMemFreeList)
    3. OSStart()启动:开始多任务执行,启动之前,至少要调用OSTaskCreate()创建一个应用任务

2.3 任务管理

  • 任务状态:OSTCBStat 任务状态字


    image.png
  • 任务控制块OS_TCB:用来记录任务的堆栈指针,任务的当前状态,任务的优先级等一些与任务管理有关的属性的表,叫做任务控制块TCB。系统初始化时会按照用户提供的任务数(OS_MAX_TASKS)为任务创建相应数量的任务控制块链表,也就是空任务块链表,当创建一个任务后,会调用OSTCBInit来为任务控制块进行初始化,从链表中获取一个任务控制块,并利用属性对各个成员赋值,最后将该块链入链表头部。
image.png
image.png
  • 任务就绪表:ready List:包含两个变量,OSRdyGrp和OSRdyTb1[],每个任务的就绪标志都会放入就绪表中,任务按照优先级分组,8个任务为一组(64个任务),OSRdyGrp中的每一位表示8组任务中每一组中是否有进入就绪状态的任务

  • 任务堆栈:保存CPU寄存器中的内容及存储私有数据的需要,每个任务都应该分配有自己的堆栈,任务堆栈是任务的重要组成部分,在应用程序中定义任务堆栈方法为:OS_TSK TaskStk[TASK_STK_SIZE]定义一个OS_STK类型数组并在创建一个任务时将数组地址赋给该任务。

  • 任务创建:创建函数:OSTaskCreate + 创建函数:OSTaskCreateExt 创建任务时传递任务的堆栈指针,任务的指针,任务参数以及任务优先级等,OSTaskCreate()在创建任务时会调用任务堆栈初始化函数OSTaskStkInit()来完成任务堆栈的初始化工作,这个函数在OS_CPU.C根据处理器移植时编写

  • 任务调度(Task Scheduling):

    1. 抢占式多任务内核,优先级最高的任务一旦准备就绪,就拥有CPU的所有权开始运行,uc/OS 不支持时间片轮转,任务调度是指:查找准备就绪的最高优先级的任务并进行上下文的切换。(任务调度的所花费时间是常数)
    2. 任务就绪表:ready List:任务调度的依据,包含两个变量,OSRdyGrp和OSRdyTb1[],每个任务的就绪标志都会放入就绪表中,任务按照优先级分组,8个任务为一组(64个任务),OSRdyGrp中的每一位表示8组任务中每一组中是否有进入就绪状态的任务
    3. 系统在RAM中设立一个记录表,每个任务在表中占据一个位置,用这个位置1/0的状态判断任务是否处于就绪状态,可使用相关代码将相关任务设为就绪状态或者脱离就绪状态,以及获取到优先级别最高的就绪任务
      image.png
  • 任务切换:OS_TASK_SW()任务切换宏:中止正在运行的任务,转而去运行另外一个任务的操作,这个任务时就绪任务中优先级别最高的那个任务。简单说,就是将挂起的任务寄存器入栈,将较高优先级任务的寄存器出栈

2.4 内存管理

  • μC/OS-II系统中将连续的大块的内存按分区来管理,每个分区包含有整数个大小相同的内存块
  • 内存控制块OS_MEM: memory control blocks 系统的每个内存分区都有它自己的内存控制块
  • 使用内存管理:需要在OS_CFG.H文件中将开关量OS_MEM_EN 置1
  • 创建内存分区:OSMEMCreate()
  • 分配一个内存块:OSMemGet()
  • 释放一个内存块:OSMEMPut()

2.5 时间管理:系统服务

  • 时钟节拍:定时中断来实现延时和超时控制,这个中断叫做时钟节拍,每秒发生10 -100次,频率越高,系统负荷越重
  • OSTimeDlY():系统规定:除了空闲任务之外所有任务必须在任务中合适位置调用OSTIMEDly,使得当前任务运行延时一段时间,并进行一次任务调度,以让出CPU使用权。其中操作包括:取消当前任务就绪状态,延时节拍数存入TCB,调用调度函数
  • OSTimeDlyResume:取消任务延时
  • OSTimeGet:获取系统时间
  • OSTimeSet:设置系统时间等

2.6 任务间的通信:数据共享与通信

  • 临界段:临界资源保护

  • 操作系统通信方法 -- 事件Event:信号量,邮箱,消息队列,信号量集

  • OSSchedLock() 禁止调度保护任务级的共享资源

  • 事件(ECB 事件控制块)OS_EVENT:

typedef struct 
{
   INT8U  OSEventType;            //事件的类型
   INT16U OSEventCnt;            //信号量计数器
   void *OSEventPtr;        //消息或消息队列的指针
   INT8U  OSEventGrp;        //等待事件的任务组
   INT8U OSEventTbl[OS_EVENT_TBL_SIZE];//任务等待表
} OS_EVENT;
  1. 信号量(Semaphore):控制共享资源的使用权,标志事件的发生,使得两个任务的行为同步,信号量包括信号量计数值和等待该信号任务的等待任务表,操作:OSSEMCreate(创建一个信号量,这个时候系统从空事件控制块链表中获取一个控制块,对它初始化并描述该事件),OSSEemPend(OS_Event *pevent,INT16U timeout,INTU *err)请求信号量,pevent是被请求信号量的指针, OSSemPost(OS_EVENT *pevent)释放信号量,如果不需要某信号量了可以调用OssDel(OS_EVENT *pevent,INT8U opt,INT8U *err),注意互斥信号量与优先级反转的问题
  2. 消息邮箱:OS_EVENT_TYPE_MBOX:在两个需要通信的任务之间通过传递数据缓冲区指针的方法来通信
  3. 消息队列:OS_EVENT_TYPE_Q:相当于是共用一个任务等待列表的消息邮箱数组
  4. 信号量集

2.7 移植 μC/OS-II系统

  • 移植的条件
    1. 处理器的C编译器能产生并可重入代码
    2. 在程序中可以打开或者关闭中断
    3. 处理器支持定时中断(10-1000Hz)
    4. 处理器能够容纳一定量的数据硬件堆栈,因为每个任务都有自己的任务堆栈,在调度时用于将当前任务的CPU寄存器存放到此任务的堆栈中,再从另一个任务中恢复原来的工作寄存器,继续运行另一个任务,这是多任务调度的基础。
    5. 处理器有将堆栈指针和其他CPU寄存器存储或者独处到堆栈/内存的指令
  • 移植文件

    1. 设置OS_CPU.H与处理器和编译器相关的代码:包括定义数据类型INT8U,INT8S等,使用OS_ENTER_CRITICAL等宏开启中断关闭中断等,定义堆栈增长方向(要注意)

    2. 用C语言编写与操作系统相关的函数 OS_CPU_C.C:OSTaskStkInit(初始化任务堆栈),OSTaskCreateHook,OSTaskDelHook,OSTaskSwHoOK,OSTaskStatHook,OSTimeTickHooK,比较重要的是第一个,后面五个一般可置空

    3. 用汇编语言编写四个与处理器相关的函数 OS_CPU.ASM:OSStartHighRdy,OSCtxSw(上下文切换)OSIntCtxSw(中断级任务切换),OSTickISR

  • 系统体系: OS_CFG.H 系统配置

image.png

3 通信接口:SPI,SCI,IIC

3.1 SPI:serial peripheral interface

  • 串行外围设备接口,Motorola公司推出的一种同步串行接口,SPI总线是一种三线同步总线,硬件功能很强,CPU有更多时间处理其他事务
  • 同步需要多出一条时钟线
  • 常用于扩展外设:AD,DA,FRAM,DSP等
  • SPI总线由三条信号线组成:串行时钟(SCLK/SPSCK)、串行数据输出(SDO/MOSO)、串行数据输入(SDI/MOSI)。SPI总线可以实现多个SPI设备互相连接。提供SPI串行时钟的SPI设备为SPI主机或主设备(Master),其他设备为SPI从机或从设备(Slave)。主从设备间可以实现全双工通信,当有多个从设备时,还可以增加一条从设备选择线(Slave Select)。
  • 寄存器:SPI数据寄存器,SPI控制寄存器,状态控制寄存器

3.2 SCI:serial communication interface

  • 串行通信接口,motorola公司推出的一种通用的异步通信接口UART,异步只需要发送和接收两根线
  • 常用于串行通信:RS422,RS485,RS232
  • 波特率:每秒内传送的位数 bps
  • MCU内引脚的输入输出通常采用TTL电平,适合板内数据传输,为了信号传输更远,可以转化成RS232,RS485总线标准
  • 寄存器:SCI数据寄存器(接收移位寄存器,发送移位寄存器),SCI控制寄存器,SCI状态寄存器,SCI波特率寄存器
  • UART:UART(Universal Asynchronous Receiver & Transmitter)即通用异步收发器,是串行通信的一种协议,它规定串行通信的波特率、起始/停止位、数据位、校验位等格式,以及各种异步握手信号。

3.3 I2C:I2C总线

  • Philips公司开发的一种简单的双向二进制同步串行总线,只需要两根线即可连接于总线上的在器件之间传送信息,具备多主机系统所需要的包括总线裁决和高低速器件同步功能的高性能串行总线
  • 两根线:SDA(串行数据线) SCL(串行时钟线),连接在总线上的IC数量受总线最大电容限制,两根线都是双向数据线,
  • 每个接到I2C总线上的器件都有唯一地址,I2C有存在总线仲裁机制,以确定哪一台主机控制总线,发送数据到其他器件

4 XGATE

4.1 什么是xgate

  • 飞思卡尔9s12X系列双核中的协处理器XGATE

  • 双核架构,增加了一个RISC核的高效协处理器,XGATE模块,专门用于处理中断任务,可以将主核CPU从执行耗时的中断处理程序工作中解放出来,专注于执行与应用有关的任务,实现更好的实时事件处理,XGATE采用RISC指令核,代码高效,主频运行速率可达到主核的2倍。

4.2 xgate配置与使用

  • xgate.cxgate文件为XGATE的主文件,在其中编写需要的中断服务子程序
  • xgate.h 是对XGATE用到的一些声明
  • 在Main.c文件中定义CPU与XGate共享的数据
#pragma DATA_SEG_SHARED_DATA
volatile int shared_counter;// 定义XGATE与主核共享RAM的变量
#pragma DATA_SEG DEFAULT
  • 在Main.c 文件中设置SetupXGATE:定义xgate的中断向量表的偏移地址,以及中断权限的分配,决定是主CPU还是XGATE响应中断
#define ROUTE_INTERRUPT(vec_adr, cfdata)                \
  INT_CFADDR= (vec_adr) & 0xF0;                         \
  INT_CFDATA_ARR[((vec_adr) & 0x0F) >> 1]= (cfdata)

//channel id 需要到xgate.cxgate文件向量列表中查找
//自定义向量名
#define RTI_VEC  0xF0 /* vector address= 2 * channel id */
#define MSCAN0_VEC  0xB2 /* vector address= 2 * channel id */
#define SCI2_VEC  0x8A /* vector address= 2 * channel id */
#define SCI0_VEC  0xD6

static void SetupXGATE(void) 
{   
    //初始化Xgate向量表模块并将XGVBR 寄存器设置到初始地址
    XGVBR= (unsigned int)(void*__far)(XGATE_VectorTable - XGATE_VECTOR_OFFSET); 
    // RTI_VEC 为自定义中断名 设置xgate中断以及中断优先级 
    ROUTE_INTERRUPT(RTI_VEC, 0x86); /* RQST=1 and PRIO=1 */                            //实时时钟中断
    ROUTE_INTERRUPT(MSCAN0_VEC, 0x81); /* RQST=1 and PRIO=1 */                         //CAN接收中断
    ROUTE_INTERRUPT(SCI2_VEC, 0x84); /* RQST=1 and PRIO=1 */
    ROUTE_INTERRUPT(SCI0_VEC, 0x83); /* RQST=1 and PRIO=1 */
    XGMCTL= 0xFBC1; /* XGE | XGFRZ | XGIE */
}
  • xgate.cxgate中实现中断函数,同时修改中断向量表vectorTable
interrupt void SCI2_Handler(void){
}

const XGATE_TableEntry XGATE_VectorTable[] ={
...
{(XGATE_Function)SCI2_Handler},  // Channel 45 - SCI2 
...
}

5 CAN总线

5.1 什么是CAN总线:controller Area Network

  • can总线是一种串行数据通信协议,特点包括:多主机方式工作,任意节点可以在任意时刻主动向网络上其他节点发送信息;串行,同步,半双工,CRC
    节点信息分成不同的优先级:仲裁总线结构机制,节点同时传送信息时,优先级低的节点主动停止数据发送,优先级高的节点不受影响继续传送数据。

  • CAN总线系统:由多个电子控制单元EMU同时控制多个工作装置或系统,各控制单元ECU的共用信息通过总线互相传递。

  • 传输速度快;相关控制单元可共用传感器;更少的线束,更小的控制单元,节省了空间。

  • CAN-BUS系统的组成:CAN收发器(控制器内部,接收和发送数据),数据传输终端,数据传输线(双向数据线)

  • 参考:https://wenku.baidu.com/view/bab35fb765ce0508763213cf.html

5.2 CAN总线的特点

  • Can总线是一种实时应用的串行通信协议总线,可以使用双绞线来传输信号,是应用最广泛的现场总线之一。
  • 实时性强,传输距离远,抗电磁干扰强,成本低,通信速度最高可达40M
  • 双线串行通信,检错能力强
  • 具有优先权和仲裁功能,多个模块通过can控制器挂在can-bus上,形成多主机局部网络
  • 根据报文ID决定接收或屏蔽报文
  • 报文使用标识符来指示功能信息,优先级信息
  • 短帧结构,出错时可自动关闭节点

5.3 工作原理

  • 当CAN总线上的一个节点(站)发送数据时,它以报文形式广播给网络中所有节点。对每个节点来说,无论数据是否是发给自己的,都对其进行接收。

  • 每组报文开头的11位字符为标识符,定义了报文的优先级,这种报文格式称为面向内容的编址方案。标识符在网络中是唯一的,描述了数据的特定含义,也决定报文优先级(标识符数值越小,优先级越高)。注意:最高优先级的报文获得总线访问权,低优先级报文在下一个总线周期自动重发

  • 当一个站要向其它站发送数据时,该站的CPU将要发送的数据和自己的标识符传送给本站的CAN芯片,并处于准备状态;当它收到总线分配时,转为发送报文状态。CAN芯片将数据根据协议组织成一定的报文格式发出,这时网上的其它站处于接收状态。每个处于接收状态的站对接收到的报文进行检测,判断这些报文是否是发给自己的,以确定是否接收它。

  • CAN 总线组成:

    1. 传输线:双绞线,两条导线为CAN-High和CAN-Low线,减少干扰,差分电压传输,信号 = CAN_H - CAN_L
    2. 数据传输终端:传输线的两头,两个终端电阻
    3. 分支线:连接主机或者通信结点的分支线,不能太长,不超过6m
    4. CAN控制器和CAN收发器
image.png
  • CAN 总线帧结构:


    image.png
image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容