本文试图从易于理解的角度讲解k60单片机的串口通信(UART),从相关寄存器的简单配置到完成一个字节的收发,再到多个字节的应用。由于使用的单片机是K60,所以是"基于K60",但是串口通信的原理和基本概念都是的通用的。
因为是简单讲解,所以阉割了一些内容,不会特别全面与深入。
参考资料
K60-UART串口通信讲解
STM32的串口通信
串口是串行通信接口的简称,是一种外设接口。串行通信可以分为异步通信与同步通信,此处只涉及到异步通信原理。实现异步串行通信功能的模块在一部分 MCU 中被称为通用异步收发器(Universal Asynchronous Receiver/Transmitters,UART)。
实现串行通信的数据传输只需要两条线,发送线(TX)、接收线(RX)。
串行通信的特点是:数据以字节为单位,按位的顺序(例如从最低位开始)从一条传输线上发送出去。
即通过一个移位寄存器,在发送时,将并行的字节数据(8位二进制)移位成串行的线性数据发送出去。接受时同理,将串行数据移位成并行数据再存储到数据寄存器中。
从开始位到停止位结束的时间间隔称为一帧(frame)。所以,也称这种格式为帧格式。
一帧数据里面包含起始位、数据位、校验位和停止位(可以没有校验位),所以一帧数据至少10位二进制
每发送一个字节,都要发送“开始位”与“停止位”,这是影响异步串行通信传送速度的因素之一。同时因为每发送一个字节,必须首先发送“开始位”,所以称之为“异步”(Asynchronous)通信。
MCU 引脚输入/输出一般使用 TTL(Transistor Transistor Logic)电平,即晶体管-晶体管逻辑电平
逻辑“0” | 逻辑“1” | |
---|---|---|
TTL电平 | 小于0.4V | 大于2.4V |
可以发现 : 0.4V~2.4V处于一种中间态,既不是1也不是0,此状态可以认为用于间隔相邻发送位。
可以观察发现:逻辑“0”和逻辑“1”中间的电平部分就是这样一种中间态,每发送完一个位数据都要回到这样一种中间态,这是很有必要的,否则就无法判断停止位。
TTL电平传输数据的距离有限,为了延长传输距离,可以将TTL转RS-232C,这里不予拓展
开始位: 作用是告诉接收方接下来要传输数据了,开始位的标志是保持空闲状态时(逻辑“1”),下拉电平到逻辑“0”并且保持发送一个位的时间(这个时间是由波特率决定的,后续会说明),如果该低电平没有保持一定的时间,则认为是偶然因素造成而忽略掉。
停止位: 发送器发送 1 到 2位的停止位(逻辑"1"),表示一个字节传送结束。若继续发送下一字节,则重新发送开始位,开始一个新的字节传送。若不发送新的字节,则维持“1”的状态,使发送数据线处于空闲。
校验位:
在异步串行通信中,如何知道传输是正确的?最常见的方法是增加一个位(奇偶校验位),供错误检测使用。字符奇偶校验检查(Character Parity Checking)称为垂直冗余检查(VerticalRedundancy Checking,VRC),它是为每个字符增加一个额外位使字符中“1”的个数为奇数或偶数。奇数或偶数依据使用的是“奇校验检查”还是“偶校验检查”而定。
- 当使用“奇校验检查”时,如果字符数据位中“1”的数目是偶数,校验位应为“1”,如果“1”的数目是奇数,校验位应为“0”。
- 当使用“偶校验检查”时,如果字符数据位中“1”的数目是偶数,则校验位应为“0”,如果是奇数则为“1”。
这里列举奇偶校验检查的一个实例,看看 ASCII 字符“R”,其位构成是1010010。由于字符“R”中有 3 个位为“1”,若使用奇校验检查,则校验位为 0;如果使用偶校验检查,则校验位为 1。在传输过程中,若有 1 位(或奇数个数据位)发生错误,使用奇偶校验检查,可以知道发生传输错误。若有 2 位(或偶数个数据位)发生错误,使用奇偶校验检查,就不能知道已经发生了传输错误。但是奇偶校验检查方法简单,使用方便,发生 1 位错误的概率远大于 2位的概率,所以“奇偶校验”这种方法还是最为常用的校验方法。几乎所有 MCU 的串行异步通信接口,都提供这种功能。
单位是:位/秒,记为bps
波特率可以认为是传输数据的速率,所以发送一位二进制的时间就是波特率的倒数。通常使用的波特率有 600、900、1200、1800、2400、4800、9600、19200、38400、57600、115200 等。
UART0 和 UART1 时钟源为内核时钟,UART2~UART5 的时钟源为外设时钟(总线时钟)。波特率由一个 13 位的模数计数器(SBR)和一个 5 位的分数微调计数器(BRFD)共同决定。13 位SBR[SBR]范围 1~8191,它决定了模块的时钟分频。微调计数器给波特率时钟增加一个细微的延时,以便匹配系统波特率。波特率时钟与模块时钟同步并驱动接收器。计算公式如下:
- 波特率 = UART 模块时钟/(16*(SBR[SBR]+BRFD))
SBR由波特率寄存器配置,BRFD由控制寄存器4配置
波特率一致是发送方与接收方实现通信的必要条件
uart_init(UART3,115200); 以串口模块3为例,波特率=115200
case UART3:
SIM_SCGC4 |= SIM_SCGC4_UART3_MASK; //使能UART3时钟
if(UART3_RX == PTE5)
{
port_init( PTE5, ALT3); //在PTE5上使能UART3_RXD
}
else if(UART3_RX == [...]){
...}
...
else ASSERT(0); //上诉条件都不满足,直接断言失败了,设置管脚有误?
if(UART3_TX == PTE4)
{
port_init( PTE4, ALT3); //在PTE4上使能UART3_TXD
}
if(UART3_TX == [...]){
...}
...
else ASSERT(0); //上诉条件都不满足,直接断言失败了,设置管脚有误?
break;
SIM_SCGC4
经过多层宏定义可以知道是一个系统时钟寄存器
//配置成8位无校验模式
//设置 UART 数据格式、校验方式和停止位位数。通过设置 UART 模块控制寄存器 C1 实现;
UART_C1_REG(UARTN[uratn]) |= (0
//| UART_C1_M_MASK //9 位或 8 位模式选择 : 0 为 8位 ,1 为 9位(注释了表示0,即8位) (如果是9位,位8在UARTx_C3里)
//| UART_C1_PE_MASK //奇偶校验使能(注释了表示禁用)
//| UART_C1_PT_MASK //校验位类型 : 0 为 偶校验 ,1 为 奇校验
);
UARTN[uratn]
是串口模块3的基地址,uratn
是传入的枚举元素,此处为UART3
UART_C1_REG()
是宏定义,最后跳转至UART3控制寄存器1
D7—LOOPS,循环模式选择。当 LOOPS 被置位时,RxD 引脚从 UART 分离,发送器输出内部连接到接收器输入。发送器和接收器必须使能来使用循环功能。0:正常操作。1:发送器输出内部连接到接收器输入的循环模式。接收器输入由 RSRC 位决定。
D6—UARTSWAI,UART 在等待模式停止。0:在等待模式 UART 时钟继续运行。1:当 CPU 处于等待模式时,UART 时钟冻结。
D5—RSRC,接收器信号源选择。这个位在 LOOPS 位被置位时才有意义。当 LOOPS
被置位时,RSRC 位决定接收器移位寄存器输入的信号源。0:选择内部回环模式,接收器
输入内部连接到发送器输出。1:单线 UART 模式,接收器输入连接到发送器引脚输入信号。D4—M,9 位或 8 位模式选择。当 7816E 被置位(使能)时,此位必须被置位。0:正常模式-起始位+8 位数据位(由 MSBF 决定 MSB/LSB 优先)+停止位。1:使用模式-起始位+9 位数据位(由 MSBF 决定 MSB/LSB 优先)+停止位。
D3—WAKE,接收器唤醒方法选择。WAKE 决定哪种条件唤醒 UART:接收数据字符最高位的地址标记或者接收引脚输入信号上的空闲情况。0:空闲线唤醒。1:地址标记唤醒。
D2—ILT,空闲线类型选择。ILT 决定接收器何时开始计数当作空闲字符的逻辑 1。在一个有效的起始位或者停止位之后计数开始。如果起始位之后计数开始,那么停止位前的逻辑1 的字符串可能导致空闲字符的错误识别。停止位后开始计数避免了错误的空闲字符识别,但是需要合适的同步传输。0:起始位后开始计数空闲字符位。1:停止位后开始计数空闲字符位。
D1—PE,奇偶校验使能。使能奇偶校验功能。当奇偶校验使能时,停止位前会被插入一个奇偶校验位。7816E 被置位(使能)时,此位必须被置位。0:奇偶校验功能禁止。1:奇偶校验功能使能。
D0—PT,校验类型。PT 决定了 UART 是否产生并检查奇校验位或者偶校验位。偶校验位中,偶数个 1 会清校验位,奇数个 1 会置位校验位。奇校验位中,奇数个 1 会清校验位,偶数个 1 会置位校验位。当 7816E 被置位(使能)时,此位必须清零。0:偶校验。1:奇校验。
以UART_C1_M_MASK
为例,从名称上可以发现该掩码是控制D4位-M,即发送的数据是9位还是8位的选择
#define UART_C1_M_MASK 0x10u //代码中的宏定义
转化为二进制是 00010000,其位为1的地方刚好对应着寄存器D4-M的地方
UART_C1_REG(UARTN[uratn]) |= UART_C1_M_MASK;
这样做 或 运算就会把 控制寄存器1 的 D4-M位 置为1,表示开启9位数据模式
其他掩码运算配置过程同理,不做举例。但是此处代码只是简单设置成8位无校验模式,所以全部置0即可。
波特率配置的原理部分上文已经介绍过,请自行翻看
上文
SBR由波特率寄存器配置,BRFD由控制寄存器4配置
//计算波特率,串口0、1使用内核时钟,其它串口使用bus时钟
if ((uratn == UART0) || (uratn == UART1))
{
sysclk = core_clk_khz * 1000; //内核时钟
}
else
{
sysclk = bus_clk_khz * 1000; //bus时钟
}
//UART 波特率 = UART 模块时钟 / (16 × (SBR[12:0] + BRFA))
//不考虑 BRFA 的情况下, SBR = UART 模块时钟 / (16 * UART 波特率)
sbr = (uint16)(sysclk / (baud * 16));
if(sbr > 0x1FFF)sbr = 0x1FFF; //SBR 是 13bit,最大为 0x1FFF
//已知 SBR ,则 BRFA = = UART 模块时钟 / UART 波特率 - 16 ×SBR[12:0]
brfa = (sysclk / baud) - (sbr * 16);
ASSERT( brfa <= 0x1F); //断言,如果此值不符合条件,则设置的条件不满足寄存器的设置
//可以通过增大波特率来解决这个问题
//写 SBR
temp = UART_BDH_REG(UARTN[uratn]) & (~UART_BDH_SBR_MASK); //缓存 清空 SBR 的 UARTx_BDH的值
UART_BDH_REG(UARTN[uratn]) = temp | UART_BDH_SBR(sbr >> 8); //先写入SBR高位
UART_BDL_REG(UARTN[uratn]) = UART_BDL_SBR(sbr); //再写入SBR低位
//写 BRFD
temp = UART_C4_REG(UARTN[uratn]) & (~UART_C4_BRFA_MASK) ; //缓存 清空 BRFA 的 UARTx_C4 的值
UART_C4_REG(UARTN[uratn]) = temp | UART_C4_BRFA(brfa); //写入BRFA
//设置FIFO(FIFO的深度是由硬件决定的,软件不能设置)
UART_PFIFO_REG(UARTN[uratn]) |= (0
| UART_PFIFO_TXFE_MASK //使能TX FIFO(注释表示禁止)
//| UART_PFIFO_TXFIFOSIZE(0) //(只读)TX FIFO 大小,0为1字节,1~6为 2^(n+1)字节
| UART_PFIFO_RXFE_MASK //使能RX FIFO(注释表示禁止)
//| UART_PFIFO_RXFIFOSIZE(0) //(只读)RX FIFO 大小,0为1字节,1~6为 2^(n+1)字节
);
清空缓存部分的掩码做反码运算~
,再与寄存器做 与 运算,相当于全部置0,然后再做 或 运算进行寄存器的配置。UART_BDH_SBR()
、UART_BDL_SBR()
、UART_C4_BRFA()
对sbr和brfa做相关位转换的操作
#define UART_BDH_SBR(x) (((uint8_t)(((uint8_t)(x))<
#define UART_BDL_SBR(x) (((uint8_t)(((uint8_t)(x))<
#define UART_C4_BRFA(x) (((uint8_t)(((uint8_t)(x))<
具体讲解部分与上类同
D7—LBKDIE,LIN 中止检测中断使能。LBKDIE 根据 LBKDDMAS 的状态使能 LIN 中止检测标识 LBKDIF 来产生中断请求。0:LBKDIF 中断请求禁止。1:LBKDIF 中断请求使能。
D6—RXEDGIE,RxD输入有效边沿中断使能。RXEDGIE使能接收输入有效边沿RXEDGIF
来产生中断请求。0:RXEDGIF 硬件中断禁止(使用轮询)。1:RXEDGIF 中断请求使能.72D4~D0—SBR,UART 波特率位。UART 的波特率由这 5 位和 UARTx_BDL 共 13 位决
定。UARTx_BDL 的复位值为 0x04。
- D4~D0—BRFA,波特率微调。这个位用来对平均波特率以 1/32 的增量增加时间精度。
/* 允许发送和接收 */
UART_C2_REG(UARTN[uratn]) |= (0
| UART_C2_TE_MASK //发送使能
| UART_C2_RE_MASK //接收使能
//| UART_C2_TIE_MASK //发送中断或DMA传输请求使能(注释了表示禁用)
//| UART_C2_TCIE_MASK //发送完成中断使能(注释了表示禁用)
//| UART_C2_RIE_MASK //接收满中断或DMA传输请求使能(注释了表示禁用)
);
补充说明:这里使能发送以后,后续只需要把数据存到数据寄存器,就会在硬件层面自动实现数据的发送,无需人为设置高低电平。使能接收以后,后续也只需要直接读取数据寄存器里的值。除非在串口模块不够用的情况下设置虚拟串口,这样可能需要软件改变电平,此处不予实践。
D7—TIE,发送器中断或 DMA 传送使能。TIE 根据 C5[TDMAS]使能 S1[TDRE]标志产生中断请求或者 DMA 传送请求。0:TDRE 中断和 DMA 传送请求禁止。1:TDRE 中断或者 DMA 传送使能。
D6—TCIE,传送结束中断使能。TCIE 使能 S1[TC]传送完成标志产生中断请求。0:TC中断请求禁止。1:TC 中断请求使能。
D5—RIE,接收器满中断或 DMA 传送使能。RIE 根据 C5[RDMAS]使能 S1[RDRF]标志产生中断请求或 DMA 传送请求。0:RDRF 中断和 DMA 传送请求禁止。1:RDRF 中断或DMA 传送请求使能。
D4—ILIE,空闲线中断使能。ILIE 根据 C5[ILDMAS]的状态使能 S1[IDLE]空闲线标志
产生中断请求。0:IDLE 中断请求禁止。1:IDLE 中断请求使能。D3—TE,发送器使能。TE 使能 UART 发送器。TE 位可以通过清 0 然后置位 TE 位来排列一个空闲前导。当 7816E 被置位(使能)并且 C7816[TTYPE]=1 时,TE 位在请求块被发送之后会被自动清 0。当 TL7816[TLEN]=0 并且四个附加字符被发送时,TE 位自动清 0的条件会一直被检测是否达到。0:发送器关闭。1:发送器开启。
D2—RE,接收器使能。RE 使能 UART 接收器。0:接收器关闭。1:接收器开启。
D1—RWU,接收器唤醒控制。RWU 可以被置位来使得 UART 接收器处于备用状态。当 RWU 事件(C1[WAKE]被清 0 的 IDLE 事件或者 C1[WAKE]被置位时的地址匹配)发生时,RWU 自动清 0。当 7816E 被置位时,此位必须被清 0。0:正常操作。1:RWU 使能唤醒功能并禁止接收器中断请求。通常,硬件通过自动清 0RWU 来唤醒接收器。
D0—SBK,发送中止。锁住的 SBK 发送一个中止字符(S2[BRK13]清 0 时,10、11 或12 个逻辑 0;S2[BRK13]置位时,13 或 14 个逻辑 0)。锁住意味着在中止字符发送结束之前清除 SBK 位。只要 SBK 被置位,发送器会继续发送完整的中止字符。当 7816E 被置位时,此位必须被清 0。0:正常发送器操作。1:发送排列好的中止字符。
/*!
* @brief 串口发送一个字节
* @param UARTn_e 模块号(UART0~UART5)
* @param ch 需要发送的字节
* @since v5.0
* @note printf需要用到此函数
* @see fputc
* Sample usage: uart_putchar (UART3, 'A'); //发送字节'A'
*/
void uart_putchar (UARTn_e uratn, char ch)
{
//等待发送缓冲区空
while(!(UART_S1_REG(UARTN[uratn]) & UART_S1_TDRE_MASK));
//发送数据
UART_D_REG(UARTN[uratn]) = (uint8)ch;
}
此处涉及到状态寄存器1的 TDRE 位
D7—TDRE,发送数据寄存器空标志。当发送缓冲区(D 和 C3[T8])中的数据字的数目等于或少于 TWFIFO[TXWATER]中的数目时,TDRE 会被置位。 正在传输过程中的字符不包含在此计数中。TDRE 置位时读 S1,然后写 UART 数据寄存器(D)会清 TDRE。为了能够获得更有效的中断服务,除了写到缓冲区的最终值之外的所有数据应该都写入 D/C3[T8]。然后在写最终数据前读 S1 可以清 TRDE 标志。0:发送缓冲区中的数据数目比TWFIFO[TXWATER]中的数目大。1:自从标志清零后,发送缓冲区中的数据数目等于或少于 TWFIFO[TXWATER]中的数目。
UART_S1_REG(UARTN[uratn])
与 UART_S1_TDRE_MASK(二进制为10000000)
做与运算可以排除TDRE位以外的其他位的干扰。缓冲区空后退出while循环,最后把数据直接写进数据寄存器D。
其实就是循环发送单个字节,但是为了接收方好解析出想要的数据,往往会添加标志字节,这个点在接收多个字节的时候细讲。
/*!
* @brief 发送指定len个字节长度数组 (包括 NULL 也会发送)
* @param UARTn_e 模块号(UART0~UART5)
* @param buff 数组地址
* @param len 发送数组的长度
* @since v5.0
* Sample usage: uart_putbuff (UART3,"1234567", 3); //实际发送了3个字节'1','2','3'
*/
void uart_putbuff (UARTn_e uratn, uint8 *buff, uint32 len)
{
while(len--)
{
uart_putchar(uratn, *buff);
buff++;
}
}
具体调用:发送一个整型变量的数据。此处没有发送标志字节。
char str_temp[4];
sprintf((char *)str_temp,"%3d",OpenmvX); // 先转化成字符数组类型
str_temp[3] = ' ';
uart_putbuff(UART3,(uint8 *)str_temp,sizeof(str_temp));
void uart_getchar (UARTn_e uratn, char *ch)
{
while (!(UART_S1_REG(UARTN[uratn]) & UART_S1_RDRF_MASK)); //等待接收满了
// 获取接收到的8位数据
*ch = UART_D_REG(UARTN[uratn]);
// 获取 9位数据,应该是(需要修改函数的返回类型):
// *ch = ((( UARTx_C3_REG(UARTN[uratn]) & UART_C3_R8_MASK ) >> UART_C3_R8_SHIFT ) << 8) | UART_D_REG(UARTN[uratn]); //返回9bit
}
此处涉及到状态寄存1的 RDRF 位
补充说明:状态寄存器的值会根据实际情况自动更新,无需手动改变D5—RDRF,接收数据寄存器满标志。当接收缓冲区中数据字数目等于或多于TWFIFO[TXWATER]中的数目时,RDRF 被置位。正在接收中的字符不包含在此计数中。当73S2[LBKDE]为 1 时,RDRF 只能为 0。另外,当 S2[LBKDE]为 1 时,接收的数据字会被存储在接收缓冲区中但会彼此覆盖。RDRF 为 1 时读 S1,然后读 UART 数据寄存器(D)会清RDRF。为了获得更有效的中断和 DMA 操作,除了最终从缓冲区读出的数据之外的所有数据都使用 D/C3[T8]/ED。读 S1 和最终数据值会清 RDRF。0:接收缓冲区中数据字的数目少于 RXWATER 中的数目。1:此标志被清之后,接收缓冲区中数据字的数目等于或多于RXWATER 中的数目。
很多时候往往会把连续的几个字节当作一个整体才是有意义的数据,所以单片机在接收数据的时候需要保证获取的数据的完整性与连续性。但是在传输过程中经常会有意外的情况例如错位、数据改变、丢包等。所以只是简单的按照数据字节个数来还原数据是不可靠的,这个时候需要有标志字节。
例如我发送一个坐标:“[x,y]”
(x,y是字符串),“[” 和 “]” 就是我的头尾标志,“,”是分隔标志。所以接收数据的时候可以先一连串读取多个字节的数据,这多个字节的数据可以是所需要的数据的两倍字节长度【比如我想要的数据是5个字节长度(包含标志字节),那就可以连续读取10个字节长度的数据】,之所以是两倍长度,是因为即使发生最坏的错位,我至少可以获得一个有效数据。通过判断头标志,可以知道接下来就是我想要的数据。判断尾标志,可以知道我一个数据已经接收完成。
处理数据的代码我放在了接收中断里,除开设置普通中断的必要操作,关键的部分是使能接收中断。涉及到控制寄存器2的RIE位。
/*!
* @brief 开串口接收中断
* @param UARTn_e 模块号(UART0~UART5)
* @since v5.0
* Sample usage: uart_rx_irq_en(UART3); //开串口3接收中断
*/
void uart_rx_irq_en(UARTn_e uratn)
{
UART_C2_REG(UARTN[uratn]) |= UART_C2_RIE_MASK; //使能UART接收中断
enable_irq((IRQn_t)((uratn << 1) + UART0_RX_TX_IRQn)); //使能IRQ中断
}
中断服务函数
补充说明:此处又判断了状态寄存器1里的 RDRF 位等
void uart3_test_handler(void)
{
UARTn_e uratn = UART3;
if(UART_S1_REG(UARTN[uratn]) & UART_S1_RDRF_MASK) //接收数据寄存器满
{
//用户需要处理接收数据
GetPosixy();
}
if(UART_S1_REG(UARTN[uratn]) & UART_S1_TDRE_MASK ) //发送数据寄存器空
{
//用户需要处理发送数据
}
}
具体实现
static char openmv_buf[10] = {
0};
int openmv_i = 0;
char pointindex = 0,frameindex = 0,save;
int x = 0,y = 0;
if(uart_querychar(Openmvuart, &openmv_buf[0]))
{
if (openmv_buf[0]!= '[') //帧头不对
{
openmv_i = 0;
return 0; //返回退出
}
openmv_i = 1;
while( openmv_i < 10 ) uart_getchar(Openmvuart, &openmv_buf[openmv_i++]); //等待采集
for(openmv_i=1;openmv_i < 10;openmv_i++)
{
if(openmv_buf[openmv_i] == ',' ) pointindex = openmv_i;
else if(openmv_buf[openmv_i] == ']')
{
frameindex = openmv_i;
break;
}
else if (openmv_buf[openmv_i] == '[') return 0;
}
uart_querychar()
是一个查询接受函数,与uart_putchar ()
类似,只不过这个函数在接收到数据时会返回1否则返回0。
最后在openmv_buf[]
中存储了接收到的一连串的10个字节,同时记录了下了有效数据的下标索引,之后通过索引再把有效数据提取出来。 具体的提取过程就不贴了,因为这一部分的代码是要根据实际数据情况来做调整的。