IMX6ULL学习笔记(20)——UART串口使用

一、UART简介

i.MX6U 芯片具有多达 8 个 UART 外设用于串口通讯,UART 是在 USART 基础上裁剪掉了同步通信功能,只支持异步通信。简单区分同步和异步就是看通信时需不需要对外提供时钟输出,我们平时用的串口通信基本都是 UART。

UART 满足外部设备对工业标准 NRZ 异步串行数据格式的要求,并且使用了小数波特率发生器,可以提供多种波特率,使得它的应用更加广泛。UART 支持异步单向通信和半双工单线通信;还支持局域互连网络 LIN、智能卡(SmartCard)协议与 lrDA(红外线数据协会)SIR ENDEC 规范。

i.MX6U 的 UART 主要特性如下:

  • 兼容高速 TIA/EIA-232-F,最高 5.0 Mbit/s。
  • 兼容 IrDA 串行低速红外接口,最高 115.2 Kbit/s。
  • 支持 9 位或多点(Multidrop mode)模式(RS-485)(自动从地址检测)。
  • 支持 7 位或 8 位 RS-232 格式或 9 位的 RS-485 格式。
  • 支持 1 或 2 位停止位。
  • 可编程奇校验或偶校验。
  • 支持硬件流控(CTS/RTS)。
  • RTS支持边缘中断检测。
  • 自动波特率检测(最高为115.2kbit/s)。
  • rx_data 输入和 tx_data 输出可分别在 rs-232/rs-485 模式下进行交换(软件可交换 TX、RX 引脚)。
  • 两个 DMA 请求(TxFIFODMA 请求和 RxFIFODMA 请求)。
  • 支持软件复位 (Srst_B)。
  • 两个独立的 32 输入 FIFO,用于发送和接收。

二、引脚分布


相比 STM32,i.MX6U 的串口增加了引脚交换功能。通过配置 UFCR[DCEDTE] 寄存器可以交换 TX、RX 引脚以及 CTS、RTS 引脚,如下图所示。


三、波特率设置

3.1 串口时钟

串口模块共有两个时钟输入 Peripheral ClockModule Clock

  • Peripheral Clock
    外部时钟。这个时钟主要用于读、写接收和发送的数据,例如读接收FIFO、写发送FIFO。 这个时钟与波特率设置无关,如果没有特殊需求我们将这个时钟保持默认即可,在初始代码中并没有特意设置这个时钟。

  • Module Clock
    模块时钟,它既可用于接收、发送数据也用于设置波特率,这个时钟决定了串口最高支持的波特率。Module Clock 时钟来自根时钟 UART_CLK_ROOT

从图中可以看出,从PLL时钟到UART时钟共用用到了两个时钟选择寄存器(标号①和③),两个时钟分频寄存器(标号②和标号④)。 我们最终目的是将PLL3时钟作为UART根时钟(UART_CLK_ROOT)的根时钟。按照标号顺序讲解如下:

  1. 标号①选择 PLL3 时钟还是 CCM_PLL3_BYP。我们选择 PLL3 输出时钟,寄存器 CCSR[PLL3_SW_CLK_SEL] = 0, 则表示选择 PLL3 时钟。默认情况下是这样设置的。所以我们代码中并没有设置该寄存器。
  2. 标号②设置时钟分频,根据之前的设置,PLL3 的输出频率为 480MHz ,这里的时钟分频是固定的 6 分频, 经过分频后的时钟为 480MHz / 6 = 80MHz
  3. 标号③ 再次选择时钟源。一个是 PLL3 分频得到的 80MHz 时钟,另外一个是 OSC 时钟即 24MHz 的系统参考时钟。 设置 CSCDR1[UART_CLK_SEL] = 0,选择第一个(80MHz)时钟。
  4. 标号④再次进行时钟分频。这是一个 6 位的时钟分频寄存器。分频值为 CSCDR1[UART_CLK_PODF] 寄存器值加一。 程序中将其设置为 1,则分频系数为 2,UART_CLK_ROOT 时钟频率实际为 80MHz / 2 = 40MHz

3.2 波特率计算公式

UART的发送器和接收器使用相同的波特率。计算公式如下:

  • BaudRate:要设置的波特率。
  • Ref Freq:参考时钟,这个时钟是 Module Clock 经过 RFDIV 寄存器分频后得到的频率。
  • UBMR:UBMR寄存器值。
  • UBIR:UBIR 寄存器值。

3.3 配置UARTx_UFCR寄存器

假设目标波特率为 115200

3.3.1 设置时钟分频寄存器RFDIV

通过设置时钟分频寄存器 RFDIV,得到参考时钟 Ref Freq。

Ref Freq 时钟应该被设置为多少,没有一个准确的数字,遵守以下几条:

  1. Ref Freq 时钟要大于波特率的 16 倍

依据是 module_clock 必须大于等于 16 倍波特率【参考《IMX6ULRM》(参考手册)53.4.2.1 Clock requirements,而 module_clock 经过 Clock Gating & Divider 之后变为 ref_clk,从功能框图看 ref_clk 最终作为 uart 模块参考时钟】。


  1. UBMR 和 UBIR 的值必须小于 0xFFFF

module_clock 时钟经过 UARTx_UFCR[RFDIV] 寄存器分频后得到 Ref Freq 时钟,如下图所示。

在程序中 Module Clock 被设置为 40MHz 而波特率只有 115200,所以这里将分频值设置为最大值 7,即UARTx_UFCR[RFDIV] = 110B。得到参考时钟 Ref Freq = 40MHz / 7 = 5.71MHz

3.3.2 计算UBMR和UBIR的值

已知波特率 115200,参考时钟 Ref Freq = 5.71MHz,可以计算得到 (UBMR+1) / (UBIR+1) 约为 3.10。 我们设置 (UBIR+1) = 10,(UBMR+1) = 31

四、接收和发送FIFO

Tx Block 与 Rx Block 包括三部分:

  • 控制单元Control
    控制整个串口的工作, 我们编写软件不必过多关心。

  • 电源管理单元 Power Saving

  • TxFIFO (和RxFIFO)
    TxFIFO 与 RxFIFO 大小均为 32 字节,以发送为例,数据通过 UTXD 寄存器自动写入 TxFIFO,如果 TxFIFO没有满,则可以不断将数据写入 UTXD 寄存器。UTS[4] 寄存器是 TxFIFO 满的标志位。如果关闭了发送,仍然可以向 TxFIFO 写入数据,但这样做将会产生传输错误信息。当 TxFIFO 中的数据发送完成后将会自动设置发送缓冲区空中断标志位,向 TxFIFO 写入数据将自动清除发送缓冲区空标志位。

五、DMA和中断请求

每个串口有两个 DMA 请求,txfifo dma 请求和 rxfifo dma 请求,有多个中断请求。

这里只介绍几个常用的中断:

中断事件 使能位 标志位
空闲中断 UARTx_UCR1[TRDYEN] UARTx_USR2[TXFE]
接收溢出中断 UARTx_UCR4[OREN] UARTx_USR2[ORE]
接收缓冲区非空中断 UARTx_UCR4[DREN] UARTx_USR2[RDR]

完整内容请参考《IMX6ULRM》(参考手册)53.4.1 Interrupts and DMA Requests。

六、编程流程

1. 创建工程文件夹
2. 移植官方SDK寄存器定义文件
3. 移植野火PAD属性配置文件
4. 编写启动文件
5. 编写链接文件
6. 编写makefile文件
7. 编写C语言代码
(1) 设置UART时钟源
(2) 初始化UART
(3) 使能UART
(4) UART数据接收
(5) UART数据发送

七、创建工程文件夹

  1. 创建一个文件夹 uart
  2. 创建一个用于存放头文件的文件夹 include
  3. 创建一个用于存放驱动源码的文件 device
  4. 创建一个启动文件 start.S
  5. 创建一个源文件 main.c
  6. 创建一个链接脚本 base.lds

八、移植官方SDK寄存器定义文件

/uart/include 目录下添加官方SDK寄存器定义文件 MCIMX6Y2.h,位于 SDK_2.2_MCIM6ULL_EBF6ULL/devices/MCIMX6Y2 目录下。

在官方SDK的头文件 MCIMX6Y2.h 文件多达4万多行,包含了i.MX6U芯片几乎所有的寄存器定义以及中断编号的定义。

这里只列 GPIO1相关寄存器 的部分代码。其他寄存器定义与此类似。 添加这些定义之后我们就可以 直接使用 “GPIO1->DR” 语句操作GPIO1的DR寄存器。操作方法与STM32非常相似。

typedef struct {
   __IO uint32_t DR;     /**< GPIO data register, offset: 0x0 */
   __IO uint32_t GDIR;   /**< GPIO direction register, offset: 0x4 */
   __I  uint32_t PSR;    /**< GPIO pad status register, offset: 0x8 */
   __IO uint32_t ICR1;   /**< GPIO interrupt configuration register1,*/
   __IO uint32_t ICR2;   /**< GPIO interrupt configuration register2, */
   __IO uint32_t IMR;   /**< GPIO interrupt mask register, offset: 0x14 */
   __IO uint32_t ISR; /**< GPIO interrupt status register, offset: 0x18 */
   __IO uint32_t EDGE_SEL;/**< GPIO edge select register, offset: 0x1C */
} GPIO_Type;

/*********************以下代码省略***************************8*/
/** Peripheral GPIO1 base address */
#define GPIO1_BASE                               (0x209C000u)
/** Peripheral GPIO1 base pointer */
#define GPIO1                                    ((GPIO_Type *)GPIO1_BASE)

九、移植野火PAD属性配置文件

/uart/device 目录下添加 pad_config.h

通常情况下一个引脚要设置8种PAD属性,而这些属性只能通过数字指定。为简化PAD属性设置野火编写了一个PAD属性配置文件 pad_config.h (embed_linux_driver_tutorial_imx6_code/bare_metal/led_rgb_c/pad_config.h)【源码下载:https://gitee.com/Embedfire/embed_linux_driver_tutorial_imx6_code.git】,这里使用宏定义了引脚可选的PAD属性值,并且通过宏定义的名字很容易知道宏代表的属性值:

/* SPEED 带宽配置 */
#define SPEED_0_LOW_50MHz       IOMUXC_SW_PAD_CTL_PAD_SPEED(0)
#define SPEED_1_MEDIUM_100MHz   IOMUXC_SW_PAD_CTL_PAD_SPEED(1)
#define SPEED_2_MEDIUM_100MHz   IOMUXC_SW_PAD_CTL_PAD_SPEED(2)
#define SPEED_3_MAX_200MHz      IOMUXC_SW_PAD_CTL_PAD_SPEED(3)


/* PUE 选择使用保持器还是上下拉 */
#define PUE_0_KEEPER_SELECTED       IOMUXC_SW_PAD_CTL_PAD_PUE(0)
#define PUE_1_PULL_SELECTED         IOMUXC_SW_PAD_CTL_PAD_PUE(1)


/* PUS 上下拉配置 */
#define PUS_0_100K_OHM_PULL_DOWN  IOMUXC_SW_PAD_CTL_PAD_PUS(0)
#define PUS_1_47K_OHM_PULL_UP     IOMUXC_SW_PAD_CTL_PAD_PUS(1)
#define PUS_2_100K_OHM_PULL_UP    IOMUXC_SW_PAD_CTL_PAD_PUS(2)
#define PUS_3_22K_OHM_PULL_UP     IOMUXC_SW_PAD_CTL_PAD_PUS(3)

完整的代码请阅读源文件,这里只列出了文件“pad_config.h”部分代码(embed_linux_driver_tutorial_imx6_code/bare_metal/led_rgb_c/pad_config.h)【源码下载:https://gitee.com/Embedfire/embed_linux_driver_tutorial_imx6_code.git】。

十、编写启动文件

/uart 下创建 start.S 文件用于编写启动文件。
在汇编文件中设置“栈地址”并执行跳转命令跳转到main函数执行C代码。

10.1 完整代码

/***********************第一部分*********************/
  .text            //代码段
  .align 2         //设置2字节对齐
  .global _start   //定义一个全局标号

/*************************第二部分*************************/
  _start:          //程序的开始
    b reset      //跳转到reset标号处

/*************************第三部分*************************/
reset:
   mrc     p15, 0, r0, c1, c0, 0     /*  将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中   */
   bic     r0,  r0, #(0x1 << 12)     /*  清除第12位(I位)禁用 I Cache  */
   bic     r0,  r0, #(0x1 <<  2)     /*  清除第 2位(C位)禁用 D Cache  */
   bic     r0,  r0, #0x2             /*  清除第 1位(A位)禁止严格对齐   */
   bic     r0,  r0, #(0x1 << 11)     /*  清除第11位(Z位)分支预测   */
   bic     r0,  r0, #0x1             /*  清除第 0位(M位)禁用 MMU   */
   mcr     p15, 0, r0, c1, c0, 0     /*  将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中   */

/***********************第四部分*********************/
      ldr sp, =0x84000000   //设置栈地址64M
      b main                //跳转到main函数

/***********************第五部分*******************/
    /*进入死循环*/
  loop:
      b loop

10.2 分析代码

  • 第一部分
    .text 定义代码段。
    .align 2 设置字节对齐。
    .global _start 生命全局标号_start。
/*************************第一部分*************************/
.text            //代码段
.align 2         //设置2字节对齐
.global _start   //定义一个全局标号
  • 第二部分
    _start: 定义标号_start: ,它位于汇编的最前面,说以会首先被执行。
    b reset 使用b指令将程序跳转到reset标号处。
/*************************第二部分*************************/
_start:          //程序的开始
   b reset      //跳转到reset标号处
  • 第三部分
    通过修改CP15寄存器(系统控制寄存器) 关闭 I Cache 、D Cache、MMU 等等。
    我们暂时用不到的功能,如果开启可能会影响我们裸机运行,为避免不必要的麻烦暂时关闭这些功能。
/*************************第三部分*************************/
reset:
   mrc     p15, 0, r0, c1, c0, 0     /*  将 CP15 协处理器中的寄存器数据读到 ARM 寄存器中   */
   bic     r0,  r0, #(0x1 << 12)     /*  清除第12位(I位)禁用 I Cache  */
   bic     r0,  r0, #(0x1 <<  2)     /*  清除第 2位(C位)禁用 D Cache  */
   bic     r0,  r0, #0x2             /*  清除第 1位(A位)禁止严格对齐   */
   bic     r0,  r0, #(0x1 << 11)     /*  清除第11位(Z位)分支预测   */
   bic     r0,  r0, #0x1             /*  清除第 0位(M位)禁用 MMU   */
   mcr     p15, 0, r0, c1, c0, 0     /*  将 ARM 寄存器的数据写入到 CP15 协处理器寄存器中   */
  • 第四部分
    ldr sp, =0x84000000 用于设置栈指针。野火i.MX6ULL开发板标配512M的DDR内存,裸机开发用不了这么多。程序中我们将栈地址设置到DDR的64M地址处。 这个值也可以根据需要自行定义。
    b main 只用跳转指令跳转到main函数中执行。
/***********************第四部分*********************/
      ldr sp, =0x84000000   //设置栈地址64M
      b main                //跳转到main函数
  • 第五部分
    b loop 是“无返回”的跳转指令。正常情况下,不会执行第五部分代码。
/***********************第五部分*******************/
  /*进入死循环*/
  loop:
      b loop

十一、编写链接脚本

写好的代码(无论是汇编还是C语言)都要经过编译、汇编、链接等步骤生成二进制文件或者可供下载的文件。在编译阶编译器会对每个源文件进行语法检查并生成对应的汇编语言,汇编是将汇编文件转化为机器码。

使用 arm-none-eabi-gcc -g -c led.S -o led.o 命令完成源码的编译、汇编工作,生成了 .o文件。编译和汇编是针对单个源文件,也就编译完成后一个源文件(.c.S.s)对应一个 .o 文件。程序链接阶段就会将这些 .o 链接成一个文件。

链接脚本的作用就是告诉编译器怎么链接这些文件,比如那个文件放在最前面,程序的代码段、数据段、bss段分别放在什么位置等等。

/uart 下创建 base.lds 链接脚本。

11.1 完整代码

 ENTRY(_start)
 SECTIONS {
   . = 0x80000000;

   . = ALIGN(4);
   .text :
   {
   start.o (.text)
   *(.text)
   }

   . = ALIGN(4);
   .data :
   {
   *(.data)
   }

   . = ALIGN(4);
   .bss :
   {
   *(.bss)
   }
 }

11.2 分析代码

  • 指定程序的入口
    ENTRY(_start) 用于指定程序的入口,ENTRY() 是设置入口地址的命令, “_start” 是程序的入口,led程序的入口地址位于 start.S“_start” 标号处。
 ENTRY(_start)
  • 定义SECTIONS
    SECTIONS 可以理解为是一块区域,我们在这块区域排布我们的代码,链接时链接器就会按照这里的指示链接我们的代码。
 SECTIONS {
···
···
}
  • 定义链接起始地址
    “.” 运算符代表当前位置。 我们在SECTION的最开始使用 “.= 0x80000000” 就是将链接起始地址设置为0x80000000。
. = 0x80000000;
  • 设置字节对齐
    “. = ALIGN(4);” 它表示从当前位置开始执行四字节对齐。假设当前位置为0x80000001,执行该命令后当前地址将会空出三个字节转到0x80000004地址处。

  • 设置代码段
    “.text :” 用于定义代码段,固定的语法要求,我们按照要求写即可。在“{}”中指定那些内容放在代码段。
    start.o 中的代码放到代码段的最前面。start.S是启动代码应当首先被执行,所以通常情况下要把它放到代码段的最前面,其他源文件的代码按照系统默认的排放顺序即可,通配符 “*” 在这里表示其他剩余所有的 .o文件。

   . = ALIGN(4);
   .text :
   {
   start.o (.text)
   *(.text)
   }
  • 设置数据段
    同设置代码段类似,首先设置字节对齐,然后定义代码段。在数据段里使用 “*” 通配符, 将所有源文件中的代码添加到这个数据段中。
   . = ALIGN(4);
   .data :
   {
   *(.data)
   }
  • 设置BSS段
    设置方法与设置数据段完全相同。
. = ALIGN(4);
   .bss :
   {
   *(.bss)
   }

十二、编写makefile文件

程序编写完成后需要依次输入编译、链接、格式转换命令才能最终生成二进制文件。这种编译方式效率低、容易出错。

使用makefile只需要在所在文件夹下执行make命令,makefile工具便会自动完成程序的编译、链接、格式转换等工作。正常情况下我们可以在当前目录看到生成的一些中间文件以及我们期待的.bin文件。

修改makefile主要包括两部分

  • 第一部分,在“device”文件夹下添加并编写子makefile。
  • 第二部分,修改主makefile。

12.1 编写子makefile

/uart/device 下创建 makefile

子makefile: 用于将“device”文件夹下的驱动源文件编译为一个“.o”文件

all : led.o system_MCIMX6Y2.o uart.o
    arm-none-eabi-ld -r $^  -o device.o
    
%.o : %.c
    arm-none-eabi-gcc ${header_file} -c $^
    
%.o : %.S
    arm-none-eabi-gcc ${header_file} -c $^

clean:
    -rm -f *.o *.bak
  • 添加最终目标以及依赖文件
    生成最终目标“device.o”。如果程序中新增了某个外设驱动程序,只需要将对应的“.o”文件填入“依赖”处即可。
    “$^” 代表所有的依赖文件。
    “-o” 指定输出文件名。
all : button.o  led.o system_MCIMX6Y2.o
    arm-none-eabi-ld -r $^  -o device.o
  • 添加编译C文件的命令
    编译“device”文件夹下的所有“.c”文件并生成对应的“.o”文件,其中“header_file”是头文件路径,它是定义在主makefile的变量。
    “$^” 替代要编译的源文件。
%.o : %.c
  arm-none-eabi-gcc ${header_file} -c $^
  • 添加汇编文件编译命令
    编译“device”文件夹下的所有“.S”文件并生成对应的“.o”文件,其中“header_file”是头文件路径,它是定义在主makefile的变量。
    “$^” 替代要编译的源文件。
%.o : %.S
  arm-none-eabi-gcc ${header_file} -c $^
  • 添加清理命令
    “clean” 为目标用于删除make生成的文件。
clean:
  -rm -f *.o *.bak

12.2 修改主makefile

主makefile的改动主要有两点:

  1. 在编译命令中指明头文件位置。
  2. 使用命令调用子makefile,生成依赖文件。
#定义变量,用于保存编译选项和头文件保存路径
header_file := -fno-builtin -I$(shell pwd)/include
export header_file

/*arm-none-eabi 安装位置*/
libgcc_address := /usr/lib/gcc/arm-none-eabi/6.3.1

all : start.o main.o device/device.o 
    arm-none-eabi-ld -Tbase.lds $^ -o base.elf -static -L $(libgcc_address) -lgcc
    arm-none-eabi-objcopy -O binary -S -g base.elf base.bin


%.o : %.S
    arm-none-eabi-gcc -g -c $^ 
%.o : %.c
    arm-none-eabi-gcc $(header_file) -c $^  

#调用其他文件的makefile
device/device.o :
    make -C device all


.PHONY: copy
copy:
    cp ./base.bin  /home/pan/download/embedfire

#定义清理伪目标
.PHONY: clean
clean:
    make -C device clean
    -rm -f *.o *.elf *.bin 
  • 添加编译选项和头文件保存路径
    定义变量 “header_file”。在makefile中“变量”更像C原因中的宏定义。
    “-fno-builtin” 是一个编译选项,用于解决库函数与自己编写函数同名问题。
    “-I$(shell pwd)/include” 用于指定头文件路径。
    “export header_file” 声明后可以在其他makefile中调用。
header_file := -fno-builtin -I$(shell pwd)/include
export header_file
  • 添加链接库路径
/*arm-none-eabi 安装位置*/
libgcc_address := /usr/lib/gcc/arm-none-eabi/6.3.1

“libgcc_address” 用于保存”arm-none-eabi”编译工具的安装位置,我们需要的libgcc.a位于该目录下。如果使用”sudo apt-get install gcc-arm-none-eabi”安装则默认位于程序中所指目录,如果使用其他方式安装,找到对应的路径填入该变量即可。

  • 添加最终目标以及依赖文件
all : start.o main.o device/device.o
  • 添加链接命令
    “-Tbase.lds” 表示使用base.lds链接脚本链接程序。
    “$^” 代表所有的依赖文件。
    “-o” 指定输出文件名。
    “-static -L $(libgcc_address) -lgcc” 添加静态链接libgcc.a库文件。
arm-none-eabi-ld -Tbase.lds $^ -o base.elf -static -L $(libgcc_address) -lgcc
  • 添加格式转换命令
    “-O binary” 指定输出二进制文件。
    “-S” 不从源文件中复制重定位信息和符号信息。
    “-g” 不从源文件中复制可调试信息。
arm-none-eabi-objcopy -O binary -S -g base.elf base.bin
  • 添加汇编文件编译命令
    “$^” 替代要编译的源文件。
%.o : %.S
  arm-none-eabi-gcc -g -c $^
  • 添加编译C文件的命令
    “$^” 替代要编译的源文件。
%.o : %.c
  arm-none-eabi-gcc $(header_file) -c $^
  • 添加调用其他文件的makefile
    定义生成“device/device.o”的命令,“device.o”文件由子makefile生成,所以这里只需要调用子makefile即可。
device/device.o :
  make -C device all
  • 添加清理命令
    在清理命令中不但要清理主makefile所在文件夹的内容还要调用子makefile的清理命令以清理子makefile所在文件夹的内容。
    “.PHONY” 定义了伪目标“clean”。伪目标一般没有依赖,并且 “clean” 伪目标一般放在Makefile文件的末尾。
    “clean” 为目标用于删除make生成的文件。
.PHONY: clean
clean:
  make -C device clean
  -rm -f *.o *.elf *.bin

十三、编写C语言代码

13.1 添加串口初始化和接收发送代码

13.1.1 uart.h

/uart/include 下创建 uart.h

#ifndef uart_h
#define uart_h


#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "pad_config.h"

#define  uint32_t  unsigned int
#define  uint64_t  unsigned long int



/*定义 UART1 RX 引脚*/
#define UART1_RX_GPIO                GPIO1
#define UART1_RX_GPIO_PIN            (17U)
#define UART1_RX_IOMUXC              IOMUXC_UART1_RX_DATA_UART1_RX

/*定义 UART1 TX 引脚*/
#define UART1_TX_GPIO              GPIO1
#define UART1_TX_GPIO_PIN          (16U)
#define UART1_TX_IOMUXC            IOMUXC_UART1_TX_DATA_UART1_TX


/*******************************************************************************
 * uart引脚配置
 ******************************************************************************/
#define UART_RX_PAD_CONFIG_DATA            (SRE_0_SLOW_SLEW_RATE| \
                                        DSE_6_R0_6| \
                                        SPEED_1_MEDIUM_100MHz| \
                                        ODE_0_OPEN_DRAIN_DISABLED| \
                                        PKE_1_PULL_KEEPER_ENABLED| \
                                        PUE_1_PULL_SELECTED| \
                                        PUS_3_22K_OHM_PULL_UP| \
                                        HYS_0_HYSTERESIS_DISABLED) 
    /* 配置说明 : */
    /* 转换速率: 转换速率慢
        驱动强度: R0/6 
        带宽配置 : medium(100MHz)
        开漏配置: 关闭 
        拉/保持器配置: 使能
        拉/保持器选择: 上下拉
        上拉/下拉选择: 22K欧姆上拉(选择了保持器此配置无效)
        滞回器配置: 禁止 */ 

#define UART_TX_PAD_CONFIG_DATA            (SRE_0_SLOW_SLEW_RATE| \
                                        DSE_6_R0_6| \
                                        SPEED_1_MEDIUM_100MHz| \
                                        ODE_0_OPEN_DRAIN_DISABLED| \
                                        PKE_1_PULL_KEEPER_ENABLED| \
                                        PUE_0_KEEPER_SELECTED| \
                                        PUS_3_22K_OHM_PULL_UP| \
                                        HYS_0_HYSTERESIS_DISABLED)
    /* 配置说明 : */
    /* 转换速率: 转换速率慢
        驱动强度: R0/6 
        带宽配置 : medium(100MHz)
        开漏配置: 关闭 
        拉/保持器配置: 使能
        拉/保持器选择: 保持器
        上拉/下拉选择: 22K欧姆上拉(选择了保持器此配置无效)
        滞回器配置: 禁止 */ 



void uart_init(void);
int32_t UART_SetBaudRate(UART_Type *base, uint32_t baudRate_Bps, uint32_t srcClock_Hz);

void UART_WriteBlocking(UART_Type *base, const uint8_t *data, uint8_t length);
void UART_ReadBlocking(UART_Type *base, uint8_t *data, uint8_t length);

#endif

13.1.2 uart.c

/uart/device 下创建 uart.c

#include "uart.h"

void uart_init(void)
{
    /*时钟初始化,设置 UART 根时钟,并设置为40MHz*/
    CCM->CSCDR1 &= ~(0x01 << 6); //设置UART选择 PLL3 / 6 = 80MHz
    CCM->CSCDR1 &= ~(0x3F);      //清零
    CCM->CSCDR1 |= (0x01 << 0);  //设置串口根时钟分频值为1,UART根时钟频率为:80M / (dev + 1) = 40MHz

    /*开启 UART1 的时钟*/
    CCM_CCGR5_CG12(0x3); //开启UART1的时钟

    UART1->UCR1 &= ~UART_UCR1_UARTEN_MASK; //禁用 UART1

    /*软件复位*/
    UART1->UCR2 &= ~UART_UCR2_SRST_MASK;
    while ((UART1->UCR2 & UART_UCR2_SRST_MASK) == 0)
    {
    }

    UART1->UCR1 = 0x0;
    UART1->UCR2 = UART_UCR2_SRST_MASK;
    UART1->UCR3 = UART_UCR3_DSR_MASK | UART_UCR3_DCD_MASK | UART_UCR3_RI_MASK;
    UART1->UCR4 = UART_UCR4_CTSTL(32);
    UART1->UFCR = UART_UFCR_TXTL(2) | UART_UFCR_RXTL(1);
    UART1->UESC = UART_UESC_ESC_CHAR(0x2B);
    UART1->UTIM = 0x0;
    UART1->ONEMS = 0x0;
    UART1->UTS = UART_UTS_TXEMPTY_MASK | UART_UTS_RXEMPTY_MASK;
    UART1->UMCR = 0x0;

    /*引脚初始化*/
    IOMUXC_SetPinMux(UART1_RX_IOMUXC, 0);
    IOMUXC_SetPinConfig(UART1_RX_IOMUXC, UART_RX_PAD_CONFIG_DATA);

    IOMUXC_SetPinMux(UART1_TX_IOMUXC, 0);
    IOMUXC_SetPinConfig(UART1_TX_IOMUXC, UART_TX_PAD_CONFIG_DATA);

    /*******uart初始化******/
    /*设置控制寄存器到默认值*/
    UART1->UCR2 |= (1 << 5);  //8位数宽度
    UART1->UCR2 &= ~(1 << 6); //一位停止位
    UART1->UCR2 &= ~(1 << 8); //禁用奇偶校验位

    UART1->UCR2 |= (1 << 2);  //使能发送
    UART1->UCR2 |= (1 << 1);  //使能接收
    UART1->UCR2 |= (1 << 14); //忽略流控

    /* For imx family device, UARTs are used in  mode, so that this bit should always be set.*/
    UART1->UCR3 |= UART_UCR3_RXDMUXSEL_MASK;

    //只有FIFO的数据超过阈值才会产生相应的中断,由于没有使用中断,所以这里将阈值设置为1即可。
    UART1->UFCR = (UART1->UFCR & ~UART_UFCR_TXTL_MASK) | UART_UFCR_TXTL(1); //设置发送FIFO 阀值
    UART1->UFCR = (UART1->UFCR & ~UART_UFCR_TXTL_MASK) | UART_UFCR_TXTL(1); //设置接收FIFO 阀值

    UART1->UCR1 &= ~UART_UCR1_ADBR_MASK; //禁用可变波特率
    // UART1->UCR1 |= UART_UCR1_ADBR_MASK;

    /*波特率设置方式 1 。 使用官方SDK设置波特率函数*/
    UART_SetBaudRate(UART1, 115200, 40000000);

    /*波特率设置方式 2 。 手动计算,填入寄存器*/
    /*设置串口波特率
    * Ref Freq时钟 40MHz
    * UFCR RFDIV   110  0x06 7分频    5.714MHz
    * BaudRate     115200bps
    * UBMR         31-1 = 0x09
    * UBIR         10-1 = 0x1E
    */
    UART1->UFCR &= ~(0x07 << 7); //清零分频值
    UART1->UFCR |= (0x06 << 7);  //设置分频值,40MHz /7 =  5.714MHz

    UART1->UBIR = 0x09;
    UART1->UBMR = 0x1E;

    /*开启串口*/
    UART1->UCR1 |= UART_UCR1_UARTEN_MASK;
}


/*!
 * 功能:官方SDK 串口字符串读取函数
 * @brief Reads the receiver register.
 *
 * This function is used to read data from receiver register.
 * The upper layer must ensure that the receiver register is full or that
 * the RX FIFO has data before calling this function.
 *
 * @param base UART peripheral base address.
 * @return Data read from data register.
 */
static inline uint8_t UART_ReadByte(UART_Type *base)
{
    return (uint8_t)((base->URXD & UART_URXD_RX_DATA_MASK) >> UART_URXD_RX_DATA_SHIFT);
}


/*函数功能:串口接收函数
 *参数: base,指定串口。data,保存接收到的数据。 length,要接收的数据长度
 *
*/
void UART_ReadBlocking(UART_Type *base, uint8_t *data, uint8_t length)
{
    while (length--)
    {
        /* 等待接收完成 */
        while (!(base->USR2 & UART_USR2_RDR_MASK))
        {
        }
        /*读取接收到的数据 */
        *(data++) = UART_ReadByte(base);
    }
}


/*!
 * 功能:官方SDK 串口发送函数
 * 参数:base,指定串口。data,指定要发送的字节
 * This function is used to write data to transmitter register.
 * The upper layer must ensure that the TX register is empty or that
 * the TX FIFO has room before calling this function.
 */
static inline void UART_WriteByte(UART_Type *base, uint8_t data)
{
    base->UTXD = data & UART_UTXD_TX_DATA_MASK;
}

/*
 *功能:官方SDK 串口字符串发送函数
 *参数说明:
*/
void UART_WriteBlocking(UART_Type *base, const uint8_t *data, uint8_t length)
{

    while (length--)
    {
        /* Wait for TX fifo valid.
         * This API can only ensure that the data is written into the data buffer but can't
         * ensure all data in the data buffer are sent into the transmit shift buffer.
         */
        while (!(base->USR2 & UART_USR2_TXDC_MASK))
        {
        }
        UART_WriteByte(base, *(data++));
    }
}

/* 官方SDK 波特率设置函数,
 * 修改内容:修改了函数的返回值,波特率设置成功,返回1 。波特率设置失败返回 0
 *This UART instantiation uses a slightly different baud rate calculation.
 * Baud Rate = Ref Freq / (16 * (UBMR + 1)/(UBIR+1)).
 * To get a baud rate, three register need to be writen, UFCR,UBMR and UBIR
 * At first, find the approximately maximum divisor of src_Clock and baudRate_Bps.
 * If the numerator and denominator are larger then register maximum value(0xFFFF),
 * both of numerator and denominator will be divided by the same value, which
 * will ensure numerator and denominator range from 0~maximum value(0xFFFF).
 * Then calculate UFCR and UBIR value from numerator, and get UBMR value from denominator.
 */
int32_t UART_SetBaudRate(UART_Type *base, uint32_t baudRate_Bps, uint32_t srcClock_Hz)
{
    uint32_t numerator = 0u;
    uint32_t denominator = 0U;
    uint32_t divisor = 0U;
    uint32_t refFreqDiv = 0U;
    uint32_t divider = 1U;
    uint64_t baudDiff = 0U;
    uint64_t tempNumerator = 0U;
    uint32_t tempDenominator = 0u;

    /* get the approximately maximum divisor */
    numerator = srcClock_Hz;
    denominator = baudRate_Bps << 4;
    divisor = 1;

    while (denominator != 0)
    {
        divisor = denominator;
        denominator = numerator % denominator;
        numerator = divisor;
    }

    numerator = srcClock_Hz / divisor;
    denominator = (baudRate_Bps << 4) / divisor;

    /* numerator ranges from 1 ~ 7 * 64k */
    /* denominator ranges from 1 ~ 64k */
    if ((numerator > (UART_UBIR_INC_MASK * 7)) || (denominator > UART_UBIR_INC_MASK))
    {
        uint32_t m = (numerator - 1) / (UART_UBIR_INC_MASK * 7) + 1;
        uint32_t n = (denominator - 1) / UART_UBIR_INC_MASK + 1;
        uint32_t max = m > n ? m : n;
        numerator /= max;
        denominator /= max;
        if (0 == numerator)
        {
            numerator = 1;
        }
        if (0 == denominator)
        {
            denominator = 1;
        }
    }
    divider = (numerator - 1) / UART_UBIR_INC_MASK + 1;

    switch (divider)
    {
        case 1:
            refFreqDiv = 0x05;
            break;
        case 2:
            refFreqDiv = 0x04;
            break;
        case 3:
            refFreqDiv = 0x03;
            break;
        case 4:
            refFreqDiv = 0x02;
            break;
        case 5:
            refFreqDiv = 0x01;
            break;
        case 6:
            refFreqDiv = 0x00;
            break;
        case 7:
            refFreqDiv = 0x06;
            break;
        default:
            refFreqDiv = 0x05;
            break;
    }
    /* Compare the difference between baudRate_Bps and calculated baud rate.
     * Baud Rate = Ref Freq / (16 * (UBMR + 1)/(UBIR+1)).
     * baudDiff = (srcClock_Hz/divider)/( 16 * ((numerator / divider)/ denominator).
     */
    tempNumerator = srcClock_Hz;
    tempDenominator = (numerator << 4);
    divisor = 1;
    /* get the approximately maximum divisor */
    while (tempDenominator != 0)
    {
        divisor = tempDenominator;
        tempDenominator = tempNumerator % tempDenominator;
        tempNumerator = divisor;
    }
    tempNumerator = srcClock_Hz / divisor;
    tempDenominator = (numerator << 4) / divisor;
    baudDiff = (tempNumerator * denominator) / tempDenominator;
    baudDiff = (baudDiff >= baudRate_Bps) ? (baudDiff - baudRate_Bps) : (baudRate_Bps - baudDiff);

    if (baudDiff < (baudRate_Bps / 100) * 3)
    {
        base->UFCR &= ~UART_UFCR_RFDIV_MASK;
        base->UFCR |= UART_UFCR_RFDIV(refFreqDiv);
        base->UBIR = UART_UBIR_INC(denominator - 1);
        base->UBMR = UART_UBMR_MOD(numerator / divider - 1);
        base->ONEMS = UART_ONEMS_ONEMS(srcClock_Hz / (1000 * divider));

        return 1;
    }
    else
    {
        return 0;
    }
}
  • 第一部分:设置 UART 时钟源
    设置 UART 的时钟源为 pll3_80m,设置寄存器 CCM_CSCDR1 的 UART_CLK_SEL 位为 0 即可。
/*时钟初始化,设置 UART 根时钟,并设置为40MHz*/
CCM->CSCDR1 &= ~(0x01 << 6); //设置UART选择 PLL3 / 6 = 80MHz
CCM->CSCDR1 &= ~(0x3F);      //清零
CCM->CSCDR1 |= (0x01 << 0);  //设置串口根时钟分频值为1,UART根时钟频率为:80M / (dev + 1) = 40MHz

/*开启 UART1 的时钟*/
CCM_CCGR5_CG12(0x3); //开启UART1的时钟

详细查看 IMX6ULL学习笔记(19)——时钟系统

  • 第二部分:初始化 UART
    初始化 UART 所使用 IO,设置 UART1 的寄存器 UARTx_UCR1~UARTx_UCR3,设置内容包括波特率,奇偶校验、停止位、数据位等等。
UART1->UCR1 &= ~UART_UCR1_UARTEN_MASK; //禁用 UART1

/*软件复位*/
UART1->UCR2 &= ~UART_UCR2_SRST_MASK;
while ((UART1->UCR2 & UART_UCR2_SRST_MASK) == 0)
{
}

UART1->UCR1 = 0x0;
UART1->UCR2 = UART_UCR2_SRST_MASK;
UART1->UCR3 = UART_UCR3_DSR_MASK | UART_UCR3_DCD_MASK | UART_UCR3_RI_MASK;
UART1->UCR4 = UART_UCR4_CTSTL(32);
UART1->UFCR = UART_UFCR_TXTL(2) | UART_UFCR_RXTL(1);
UART1->UESC = UART_UESC_ESC_CHAR(0x2B);
UART1->UTIM = 0x0;
UART1->ONEMS = 0x0;
UART1->UTS = UART_UTS_TXEMPTY_MASK | UART_UTS_RXEMPTY_MASK;
UART1->UMCR = 0x0;

/*引脚初始化*/
IOMUXC_SetPinMux(UART1_RX_IOMUXC, 0);
IOMUXC_SetPinConfig(UART1_RX_IOMUXC, UART_RX_PAD_CONFIG_DATA);

IOMUXC_SetPinMux(UART1_TX_IOMUXC, 0);
IOMUXC_SetPinConfig(UART1_TX_IOMUXC, UART_TX_PAD_CONFIG_DATA);

/*******uart初始化******/
/*设置控制寄存器到默认值*/
UART1->UCR2 |= (1 << 5);  //8位数宽度
UART1->UCR2 &= ~(1 << 6); //一位停止位
UART1->UCR2 &= ~(1 << 8); //禁用奇偶校验位

UART1->UCR2 |= (1 << 2);  //使能发送
UART1->UCR2 |= (1 << 1);  //使能接收
UART1->UCR2 |= (1 << 14); //忽略流控

/* For imx family device, UARTs are used in  mode, so that this bit should always be set.*/
UART1->UCR3 |= UART_UCR3_RXDMUXSEL_MASK;

//只有FIFO的数据超过阈值才会产生相应的中断,由于没有使用中断,所以这里将阈值设置为1即可。
UART1->UFCR = (UART1->UFCR & ~UART_UFCR_TXTL_MASK) | UART_UFCR_TXTL(1); //设置发送FIFO 阀值
UART1->UFCR = (UART1->UFCR & ~UART_UFCR_TXTL_MASK) | UART_UFCR_TXTL(1); //设置接收FIFO 阀值

UART1->UCR1 &= ~UART_UCR1_ADBR_MASK; //禁用可变波特率
// UART1->UCR1 |= UART_UCR1_ADBR_MASK;

/*波特率设置方式 1 。 使用官方SDK设置波特率函数*/
UART_SetBaudRate(UART1, 115200, 40000000);

/*波特率设置方式 2 。 手动计算,填入寄存器*/
/*设置串口波特率
* Ref Freq时钟 40MHz
* UFCR RFDIV   110  0x06 7分频    5.714MHz
* BaudRate     115200bps
* UBMR         31-1 = 0x09
* UBIR         10-1 = 0x1E
*/
UART1->UFCR &= ~(0x07 << 7); //清零分频值
UART1->UFCR |= (0x06 << 7);  //设置分频值,40MHz /7 =  5.714MHz

UART1->UBIR = 0x09;
UART1->UBMR = 0x1E;

串口初始化配置了多个寄存器,结合代码和《IMX6ULRM》(参考手册)53.15 UART Memory Map/Register Definition 章节可以查看寄存器的详细介绍。不必花太多时间在这些寄存器,在需要时能够找到即可。

  • 第三部分:使能 UART
    UART 初始化完成以后就可以使能 UART 了,设置寄存器 UARTx_UCR1 的位 UARTEN 为 1。
/*开启串口*/
UART1->UCR1 |= UART_UCR1_UARTEN_MASK;
  • 第四部分:UART 数据接收
    串口接收函数仅实现简单的接收字符串功能,没有使用中断。
/*!
 * 功能:官方SDK 串口字符串读取函数
 * @brief Reads the receiver register.
 *
 * This function is used to read data from receiver register.
 * The upper layer must ensure that the receiver register is full or that
 * the RX FIFO has data before calling this function.
 *
 * @param base UART peripheral base address.
 * @return Data read from data register.
 */
static inline uint8_t UART_ReadByte(UART_Type *base)
{
    return (uint8_t)((base->URXD & UART_URXD_RX_DATA_MASK) >> UART_URXD_RX_DATA_SHIFT);
}


/*函数功能:串口接收函数
 *参数: base,指定串口。data,保存接收到的数据。 length,要接收的数据长度
 *
*/
void UART_ReadBlocking(UART_Type *base, uint8_t *data, uint8_t length)
{
    while (length--)
    {
        /* 等待接收完成 */
        while (!(base->USR2 & UART_USR2_RDR_MASK))
        {
        }
        /*读取接收到的数据 */
        *(data++) = UART_ReadByte(base);
    }
}
  • 第五部分:UART 数据发送
/*!
 * 功能:官方SDK 串口发送函数
 * 参数:base,指定串口。data,指定要发送的字节
 * This function is used to write data to transmitter register.
 * The upper layer must ensure that the TX register is empty or that
 * the TX FIFO has room before calling this function.
 */
static inline void UART_WriteByte(UART_Type *base, uint8_t data)
{
    base->UTXD = data & UART_UTXD_TX_DATA_MASK;
}

/*
 *功能:官方SDK 串口字符串发送函数
 *参数说明:
*/
void UART_WriteBlocking(UART_Type *base, const uint8_t *data, uint8_t length)
{

    while (length--)
    {
        /* Wait for TX fifo valid.
         * This API can only ensure that the data is written into the data buffer but can't
         * ensure all data in the data buffer are sent into the transmit shift buffer.
         */
        while (!(base->USR2 & UART_USR2_TXDC_MASK))
        {
        }
        UART_WriteByte(base, *(data++));
    }
}

13.2 main.c

/uart 下创建 main.c

main函数中实现“发送接收到的数据”功能。

#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "pad_config.h"

#include "uart.h"

/*简单延时函数*/
void delay(uint32_t count)
{
    volatile uint32_t i = 0;
    for (i = 0; i < count; ++i)
    {
        __asm("NOP"); /* 调用nop空指令 */
    }
}

/*提示字符串*/
uint8_t txbuff[] = "Uart polling example\r\nBoard will send back received characters\r\n";
int main()
{
    uint8_t ch; //用于暂存串口收到的字符

    uart_init();
    UART_WriteBlocking(UART1, txbuff, sizeof(txbuff) - 1);

    while (1)
    {
        UART_ReadBlocking(UART1, &ch, 1);
        if (ch == '\r') /*添加回车换行\n\r*/
        {
        UART_WriteBlocking(UART1, '\n', 1);
        }
        if (ch == '\n')
        {
        UART_WriteBlocking(UART1, '\r', 1);
        }
        UART_WriteBlocking(UART1, &ch, 1);
    }

    return 0;
}

十四、编译下载验证

14.1 编译代码

make

执行make命令,生成base.bin文件。

14.2 代码烧写

编译成功后会在当前文件夹下生成.bin文件,这个.bin文件也不能直接放到开发板上运行, 这次是因为需要在.bin文件缺少启动相关信息。

为二进制文件添加头部信息并烧写到SD卡。查看 IMX6ULL学习笔记(12)——通过SD卡启动官方SDK程序

进入烧写工具目录,执行 ./mkimage.sh <烧写文件路径> 命令,例如要烧写的 base.bin 位于 home 目录下,则烧写命令为 ./mkimage.sh /home/button.bin

执行上一步后会列出linux下可烧写的磁盘,选择你插入的SD卡即可。这一步 非常危险!!!一定要确定选择的是你插入的SD卡!!,如果选错很可能破坏你电脑磁盘内容,造成数据损坏!!! 确定磁盘后SD卡以“sd”开头,选择“sd”后面的字符即可。例如要烧写的sd卡是“sdb”则输入“b”即可。

14.3 实验现象

使用USB数据线连接电脑和开发板的USB转串口接口,接通电源,打开串口调试助手,正常情况下可以串口调试助手可以收到来自开发板的提示信息,通过串口调试助手发送字符会立即收到发送的字符。



• 由 Leung 写于 2023 年 3 月 29 日

• 参考:12. UART—串口通讯

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

推荐阅读更多精彩内容