使用IMX6UL实现UART串口通信
不管是单片机开发还是嵌入式 Linux 开发,串口都是最常用到的外设。可以通过串口将开发板与电脑相连,然后在电脑上通过串口调试助手来调试程序。还有很多的模块,比如蓝牙、
GPS、GPRS 等都使用的串口来与主控进行通信的,在嵌入式 Linux 中一般使用串口作为控制台,所以掌握串口是必备的技能。本章我们就来学习如何驱动 I.MX6U 上的串口,并使用串口和电脑进行通信。
串口全称叫串行接口,通常也叫做COM接口,串行接口指的是数据一个一个的顺序传输,通信线路简单。使用两条线即可实现双向通信,一条用于发送,一条用于接受。串口通信距离远,但是速度相对会低,串口是一种很常用的工业接口。IMX6U自带的UART外设就是串口的一种,UART 全称是 UniversalAsynchronous Receiver/Trasmitter,也就是异步串行收发器。
既然有异步串行收发器,那肯定也有同步串行收发器,之前在我们学习了STM32的过程当中,STM32除了有UART外,还有另外一个叫做USART东西。USART的全称是是 Universal Synchronous/Asynchronous Receiver/Transmitter,也就是同步/异步串行收发器。相比 UART 多了一个同步的功能,在硬件上体现出来的就是多了一条时钟线。一般 USART 是可以作为 UART使用的,也就是不使用其同步的功能。
UART作为串口的一种,其工作原理也是将数据一位一位的进行传输,发送和接收各用一条线,因此通过UART接口与外界相连最少需要三条线:TXD(发送)、RXD(接受)、和GND(底线),如下图就是UART的通信格式
上图表达的具体是什么含义,
首先空闲位:数据线再空闲状态的时候为逻辑“1”状态,也就是高电平,表示没有数据线空闲
起始位:当要传输数据的时候先传输一个逻辑“0”,也就是将数据线拉低,表示开始数据传输
数据位:数据位就是实际要传输的数据,数据位数可选择5~8位,我们一般都是按照字节传输数据的,一个字节8位,因此数据位通常是8位的。低位在前,先传输,高位最后传输。
奇偶校验位: 这是对数据中“1”的位数进行奇偶校验的时候使用的,一般可以不使用奇偶校验功能
停止位:数据传输完成标志位,停止位的位数可以选择1位,1.5位或2位高电平,一啊不能都是选择1位停止位
波特率:波特率就是UAER数据传输的速率,也就是每秒传输的数据位数,一般选择9600,19200,115200等
UART的电平标准,UART一般的接口电平有TTL和RS-232,一般开发板上都有TXD和RXD这样的引脚,这些引脚低电平表示逻辑0,高电平表示逻辑1,这个就是TTL电平。RS-232采用差分线,-3~-15V表示逻辑1,+3~+15V表示逻辑0,一般如下图就是中的接口就是TTL电平
所以如上图就是的模块就是,USB转TTL模块,TTL接口部分有VCC、GND、RXD、TXD、RTS和CTS。RTS和CTS基本用不到,使用的通过杜邦线和其他模块的TTL接口相连即可
RS-232电平需要DB9接口,I.MX6U-ALPHA开发板上的COM3(UART3)口就是RS-232接口也的,如下图
但是由于现在电脑都没有DB9接口了,取而代之的是USB接口,所以就催生了很多USB转串口TTL芯片,比如CH340 、PL2303等。通过这些芯片就可以实现串口TTL转USB。IMX6U-ALPHA开发板就使用CH340芯片来完成UART1和电脑之间的连接,只需要一条USB线即可
上面都是简单的对UART接口进行了一定的讲解,那么现在就详细介绍UART接口,IMX6U一共有8个UART,它主要具有以下特性
①、兼容TIA/EIA-232F标准(一项用于规范数据终端设备(DTE)与数据通信设备(DCE)之间串行通信的权威标准。),速度最高可达到5Mbit/S
②、支持串行IR接口,兼容IrDA,最高可到115.2Kbit/S
③、支持9位或者多节点模式(RS-485)
④、1或2位停止位。
⑤、可编程的奇偶校验(奇校验和偶校验)
⑥、自动波特率检测(最高支持115.2Kbit/S)
UART的时钟源是由寄存器CCM_CSCDR1的UART_CLK_SEL(bit)位来选择的,当为0的时候UART的时钟源为pll3_80m(80MHz),如果为1的时候UART的时钟源为osc_clk(24M),一般选择pll_80M作为UART的时钟源。寄存器CCM_CSCDR1的UART_CLK_PODF(bit5:0)位是UART的时钟分频值,可设置0~63,分别对应1~64分频,因此最终进入UART的时钟为80MHz。
接下来再看一下UART的几个比较重要的寄存器,第一个就是UART的控制寄存器1,即UARTx_UCR1(x=1~8),此寄存器的结构如下图
寄存器UARTx_UCR1我们用到的重要位如下
ADBR(bit14):自动波特率检测使能位,为0的时候自动波特率检测
UARTEN(bit0):UART使能位,为0的时候关闭自动波特率检测,为1的时候使能自动波特率检测
那么接下来看一下UART的控制寄存器2,即:UARTx_UCR2,此寄存器结构图下图
寄存器UARTx_UCR2用到的重要位如下:
IRTS(bit14):为0 的时候使用RTS引脚功能,为1的时忽略RTS引脚
PREN(bit8):奇偶校验使能位,为0的时候关闭奇偶检验,为1的时候使能奇偶校验
PROE(bit7):奇偶校验模式选择位,开启奇偶校验以后此位如果为0话就使用偶校验,此位为1的话就使能奇校验
STOP(bit6):停止位数量,为0的话1位选择停止位,为1的话2位停止位
WS(bit5):数据位长度,为0的时候选择7位数据位,为1的时候选择8位数据位
TXEN(bit2):发送使能位,为 0 的时候关闭 UART 的发送功能,为 1 的时候打开 UART的发送功能。
RXEN(bit1):接收使能位,为0的时候关闭UART的接受功能,为1的时候打开UART的接受功能
SRST(bit0):软件复位,为0的时候软件复位UART,为1的时候表示复位完成。复位完成以后此位会自动置1,表示复位完成。此位只能写0,写1会被忽略掉。
剩下的一个就是UARTx_UCR3寄存器,此寄存器结构如下图,本章我们就需要此寄存器中的位RXDMUXSEL(bit2),这个位应该始终为1
接下来看一下UARTx_USR2,这个就是UART状态寄存器2,此寄存器结构如下图
寄存器UARTx_USR2要使用的重要为如下,
TXDC(bit3):发送完成标志位,为1的时候表明发送缓冲(TxFIFO)和移位寄存器为空,也就是发送完成,向TxFIFO写入数据此位就会自动清零。
RDR(bit0):数据接收标志位,为1的时候表明至少接受到一个数据,从寄存器UARTx_URXR读取数据接收到的数据以后此为会自动清零
接下来看一下寄存器 UARTx_UFCR 、 UARTx_UBIR 和 UARTx_UBMR ,寄存器UARTx_UFCR 中我们要用到的是位 RFDIV(bit9:7),用来设置参考时钟分频,设置如表所示
通过这三个寄存器可以设置UART的波特率,波特率堵塞计算公式如下图所示
Ref Freq:经过分频以后进入UART的最终时钟频率
UBMR:寄存器UARTx_UBMR中的值
UBIR:寄存器UARTx_UBIR中的值
通过 UARTx_UFCR 的 RFDIV 位、UARTx_UBMR 和 UARTx_UBIR 这三者的配合即可得到我们想要的波特率。比如现在要设置 UART 波特率为 115200,那么可以设置 RFDIV 为5(0b101),也就是 1 分频,因此 Ref Freq=80MHz。设置 UBIR=71,UBMR=3124,根据上面的公式可以计算出115200
上面就是关于寄存器的全部介绍,接下来就是UART1的具体配置步骤
1、设置UART1的时钟源
设置 UART 的时钟源为 pll3_80m,设置寄存器 CCM_CSCDR1 的 UART_CLK_SEL 位为 0即可
2、初始化UART1
初始化 UART1 所使用 IO,设置 UART1 的寄存器 UART1_UCR1~UART1_UCR3,设置内容包括波特率,奇偶校验、停止位、数据位等等。
3、使能UART1
UART1 初始化完成以后就可以使能 UART1 了,设置寄存器 UART1_UCR1 的位 UARTEN为 1
4、编写UART1数据收发函数
编写两个函数用于 UART1 的数据收发操作
下面我们分析一下原理图,
现在开始具体编写代码,现在在BSP文件夹下方创建一个UART的文件夹,然后还是老套路创建两个文件夹,文件夹的名字分别为bsp_uart.c,bsp_uart.h然后首先还是先在.c里面编写代码,因为串口编写的代码相对复杂,所以我会对.c中的代码进行逐个讲解
/*对串口1进行初始化,波特率为115200*/
void uart_init()
{/*初始化串口IO*/uart_io_init();uart_disable(UART1);uart_softreset(UART1);UART1->UCR1 = 0; /*对寄存器进行清零*/UART1->UCR1 &= ~(1<<14); /*先关闭自动波特率检测*//**设置UART的UCR2寄存器、设置字长、停止位、校验模式、关闭硬件控流*bit14:1 忽略RTS引脚*bit8:0 关闭奇偶校验*bit6:0 1位停止位*bit5:1 8位数据位*bit2:1 打开发送*bit1:1 打开接送*/UART1->UCR2 |= (1<<14) | (1<<5) | (1<<2) | (1<<1);UART1->UCR3 |= 1<<2; /*UCR3的bit2必须为1*//** 设置波特率* 波特率计算公式:Baud Rate = Ref Treq / (16 * (UBMR + 1)/(UBMR + 1))* 如果要设置波特率为115200,那么可以使用如下参数* Ref Freq = 80M也就是寄存器UFCR的bit9:7=101,表示1分频* UBMR = 3124 UBIR = 71 * 因此波特率= 80000000/(16 * (3124+1)/71+1)=115200*/UART1->UFCR= 5<<7; /*ref freq等于ipg_clk/1=80MHz*/UART1->UBIR= 71;UART1->UBMR= 3124;#if 0uart_setbaudrate(UART1,115200,80000000);#endifuart_enable(UART1);
}
首先我们自己编写一个uart_io_init()首先这里的主要目的是为了初始化串口1所需要的引脚,分别设置两个引脚的引脚复用和电器属性,也就是UART1_TX和UART1_RX,所以这个函数的主要目的就是初始化串口1对应的引脚
void uart_io_init()
{/*初始化串口IO*UART1_RXD -> UART1_TX_DATA*UART1_TXD -> UART1_RX_DATA*/IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0);IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0);IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10B0);IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX,0X10B0);
}
下面两段代码分别是使能也就是开启UART,以及关闭UART,两个函数,在初始化串口函数中的作用主要是,在配置之前为了确保串口1确实被关闭,所以先调用uart1_disable关闭函数
以及还有一个软复位的函数,也就对串口进行软复位,和上一讲的定时器一样,处于一定的原因考虑,需要检测复位是否过后,才可以进行下一步的代码
/*关闭指定的UART*/
void uart_disable(UART_Type *base)
{base->UCR1 &= ~(1<<0);
}/*使能指定的UART*/
void uart_enable(UART_Type *base)
{base->UCR1 |= (1<<0);
}
void uart_softreset(UART_Type *base)
{base->UCR2 &= (1<<0); /*复位UART*/while(base->UCR2 & 0x1); /*等待复位完成*/
}
然后要配置UART1的UCR寄存器那么首先还是需要对于此寄存器对全部位进行清零,然后再再将UCR中的自动波特率检测给关闭
然后下一步在注释当中大部分的位也说明清楚了,大致的配置内容都和STM32中的大致一样,奇偶校验都关闭了,其次还有停止位和数据位等,注释说明了就不多说了
然后下一步的代码就是设置波特率,波特率的计算公式在上面和注释当中都有说明,而且主要影响波特率的就是三个寄存器的值,分行将我们提前计算好的三个值写入寄存器,就可以完成波特率的计算了
下面的代码可以直接设置串口1的波特率,但是寄存器那里也可以设置,所以这里我们选择的是寄存器设置,那我们就把下面的函数设置给屏蔽掉,只是告诉读者还有另外一种写法,在以上内容配置完毕后,那么就通过上面编写好的串口使函数,重新对串口1进行使能,但是编写的这个函数是相对的比较复杂的,有意愿的同学可以自己下来了解和分析一下代码,这里就不过多讲解这种较为复杂的方法了
void uart_setbaudrate(UART_Type *base, unsigned int baudrate, unsigned int 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 << 4;divisor = 1;while (denominator != 0){divisor = denominator;denominator = numerator % denominator;numerator = divisor;}numerator = srcclock_hz / divisor;denominator = (baudrate << 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) ? (baudDiff - baudrate) : (baudrate - baudDiff);if (baudDiff < (baudrate / 100) * 3){base->UFCR &= ~UART_UFCR_RFDIV_MASK;base->UFCR |= UART_UFCR_RFDIV(refFreqDiv);base->UBIR = UART_UBIR_INC(denominator - 1); //要先写UBIR寄存器,然后在写UBMR寄存器,3592页 base->UBMR = UART_UBMR_MOD(numerator / divider - 1);}
}
发送一个字符UART1->USR2,这里访问的是UART1模块的USR2寄存器。在UART模块中,USR2寄存器通常包含状态信息,用于指示UART的当前状态。>> 3:将USR2寄存器的值右移3位。这个操作是为了定位到特定的状态位,该位通常用于指示UART的发送缓冲区是否为空(即上一次发送是否完成)。&0X01:与操作0X01(即二进制的00000001)用于提取右移后的最低位。如果这一位是1,表示发送缓冲区为空,可以发送新的数据;如果是0,表示发送缓冲区仍在使用中,需要等待。while(...) == 0:这个循环会一直执行,直到USR2寄存器右移3位后的最低位变为1,即直到上一次发送完成。
UART1->UTXD:这里访问的是UART1模块的UTXD寄存器,它用于存放要发送的数据。c & 0XFF,与操作0XFF(即二进制的11111111)确保了c的值被限制在8位以内
void putc(unsigned char c)
{while(((UART1->USR2 >> 3) &0X01) == 0);/* 等待上一次发送完成 */UART1->UTXD = c & 0XFF; /* 发送数据 */
}
然后这一步是发送一个字符串,str就是要发送的字符串
/*发送一个字符串,以及要发送的字符串*/
void puts(char *str)
{char *p =str;while(*p){putc(*p++);}
}
首先函数声明一行,puts函数接受一个参数str,它是一个指向字符的指针,即字符串首地址。在C语言中,字符串通常以字符数组的形式表示。
在函数体的内部,声明了一个名为P的指针变量,并将其初始化为str,即指向要发送的字符串的首地址,这样就可以使用p来便历整个字符串,然后一个for循环,循环的条件是*p不等于0,(*p不为空字符),C语言中字符串的结束标志位是空字符\0,其ASCLL码值为0,所以这个循环会一直执行,直到遇到字符串的结束标志
在循环体内,调用了putc函数来发送当前指针P指向的字符。然后,使用后缀递增运算符将指针P向前移动到字符串的下一个字符。由于++是后缀运算符,因此putc函数会使用P递增之前的值(即当前字符的地址)。然后,p递增,指向下一个字符,这个过程会一直重复,直到*p为0(即遇到字符串的结束标志),此时while循环的条件不再满足,循环结束
总结来说,puts
函数通过遍历传入的字符串,并使用 putc
函数逐个发送字符串中的每个字符(直到遇到空字符为止),从而实现了发送整个字符串的功能。
/** @description : 接收一个字符* @param : 无* @return : 接收到的字符*/
unsigned char getc(void)
{while((UART1->USR2 & 0x1) == 0);/* 等待接收完成 */return UART1->URXD; /* 返回接收到的数据 */
}
UART1->USR2:这里访问的是UART1模块的USR2寄存器,它包含UART的状态信息。
& 0x1:与操作0x1(即二进制的00000001)用于提取USR2寄存器的最低位。在UART模块中,这个最低位用于指示接收缓冲区是否有数据可读(即是否接收到了新的字符)。
while(...) == 0:这个循环会一直执行,直到USR2寄存器的最低位变为1,即直到UART接收到了新的字符并且接收缓冲区中有数据可读。
UART1->URXD:这里访问的是UART1模块的URXD寄存器,它用于存放接收到的数据。当接收缓冲区有数据可读时,URXD寄存器中保存的就是最近接收到的字符。
/** @description : 防止编译器报错* @param : 无* @return : 无*/
void raise(int sig_nr)
{}
最后在,bsp_uart.h声明一下我们编写的这些函数就可以了
#ifndef _BSP_UART_H
#define _BSP_UART_H
#include "imx6ul.h"/* 函数声明 */
void uart_init(void);
void uart_io_init(void);
void uart_disable(UART_Type *base);
void uart_enable(UART_Type *base);
void uart_softreset(UART_Type *base);
void uart_setbaudrate(UART_Type *base, unsigned int baudrate, unsigned int srcclock_hz);
void putc(unsigned char c);
void puts(char *str);
unsigned char getc(void);#endif
然后接下来是main.c也就是主函数
#include "bsp_clk.h"#include "bsp_delay.h"#include "bsp_led.h"#include "bsp_int.h"#include "bsp_uart.h"int main(void){unsigned char a =0;unsigned char state = OFF;int_init();imx6u_clkinit();delay_init();clk_enable();led_init();uart_init();while(1) { puts("请输入一个字符:");a = getc();putc(a); puts("\r\n");/*显示输入的字符*/puts("输入的字符为");putc(a);puts("\r\n\r\n");state = !state;led_switch(LED0,state);}return 0;}
最后的最后,这一次我们需要修改Makefile的二个地方,、本章 Makefile 文件在链接的时候加入了数学库, 因为在 bsp_uart.c 中有个函数
uart_setbaudrate,在此函数中使用到了除法运算,因此在链接的时候需要将编译器的数学库也链接进来。第9行的变量LIBPATH就是数学库的目录,在第56行链接的时候使用了变量LIBPATH。在后面的学习中,我们常常要用到一些第三方库,那么在连接程序的时候就需要指定这些第三方库所在的目录,Makefile 在链接的时候使用选项“-L”来指定库所在的目录,比如“示例代码 21.4.1”中第 9 行的变量 LIBPATH 就是指定了我们所使用的编译器库所在的目录。
在第 61 行和 64 行中,加入了选项“-fno-builtin”,否则编译的时候提示“putc”、“puts”
这两个函数与内建函数冲突,错误信息如下所示:warning: conflicting types for built-in function ‘putc’
warning: conflicting types for built-in function ‘puts’在编译的时候加入选项“-fno-builtin”表示不使用内建函数,这样我们就可以自己实现 putc和 puts 这样的函数了。
链接脚本保持不变。
这样将代码烧写进去,我们的串口实验就完成了