目录
一、串口介绍
不管是单片机开发还是嵌入式 Linux 开发,串口都是最常用到的外设。可以通过串口将开发板与电脑相连,然后在电脑上通过串口调试助手来调试程序。还有很多的模块,比如蓝牙、GPS、GPRS 等都使用的串口来与主控进行通信的,在嵌入式 Linux 中一般使用串口作为控制台,所以掌握串口是必备的技能。本节我们就来学习如何驱动 I.MX6U 上的串口,并使用串口和电脑进行通信。
I.MX6U 的 UART 接口,I.MX6U 一共有 8 个 UART,其主要特性如下:
①兼容 TIA/EIA-232F 标准,速度最高可到 5Mbit/S;
②支持串行 IR 接口,兼容 IrDA(Infrared Data Association红外数据组织),最高可到 115.2Kbit/s;
③支持 9 位或者多节点模式(RS-485);
④1 或 2 位停止位;
⑤可编程的奇偶校验(奇校验和偶校验);
⑥自动波特率检测(最高支持 115.2Kbit/S)。
I.MX6U 的 UART 功能很多,但是我们本篇就只用到其最基本的串口功能 ,关于 UART 其它功能的介绍请参考《I.MX6ULL 参考手册》第 3561 页的"Chapter 55 Universal Asynchronous Receiver/Transmitter(UART)"章节。 本篇我们的目的是实现115200,8,N,1(这些数字看不懂的话先去学习51入门串口:ARM嵌入式学习(三) --- 入门51(串口)-CSDN博客)与电脑之间通信。
二、配置串口寄存器
UART 的时钟源是由寄存器 CCM_CSCDR1 的 UART_CLK_SEL(bit)位来选择的,当为 0 的时候 UART 的时钟源为 pll3_80m(80MHz,看手册的时钟树可以知道),如果为 1 的时候 UART 的时钟源为 osc_clk(24M),一般选择 pll3_80m 作为 UART 的时钟源 。寄存器 CCM_CSCDR1 的 UART_CLK_PODF(bit5:0)位是 UART 的时钟分频值,可设置 0~63,分别对应 1~64 分频,一般设置为 1 分频,因此最终进入 UART 的时钟为 80MHz。 (参考时钟树)
1.选择串口引脚功能和电气属性:
这里选择UART1
IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0);
IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0);
IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX, 0x10b0);
IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10b0);
这里我们不配置UART1的CTS_B和RTS_B:
-
CTS/RTS 引脚用于硬件流控制,不是 UART 通信的必要引脚。
-
不配置它们也能正常通信,只有在需要防止数据溢出的高速或大数据量传输场景,才需要额外配置这些引脚并启用硬件流控制,比如与某些 GSM 模块、GPS 模块、蓝牙模块等通信时。
接下来看一下 UART 几个重要的寄存器
2.配置三个UART控制寄存器:
在手册第3618页,这里只写我们比较关心的:
UARTx_UCR1:
(1)ADBR(bit14):自动波特率检测使能位,为 0 的时候关闭自动波特率检测,为 1 的时候使能自动波特率检测;
(2)UARTEN(bit0):UART 使能位,为 0 的时候关闭 UART,为 1 的时候使能 UART。
这里我们关闭自动波特率检测,代码为:
UART1->UCR1 |= (1 << 0);
注意要最后配好了所有东西再使能。
UARTx_UCR2:
(1)IRTS(bit14):为 0 的时候使用 RTS 引脚功能,为 1 的时候忽略 RTS 引脚;
(2)PREN(bit8):奇偶校验使能位,为 0 的时候关闭奇偶校验,为 1 的时候使能奇偶校验;
(3)PROE(bit7):奇偶校验模式选择位,开启奇偶校验以后此位如果为 0 的话就使用偶校验,此位为 1 的话就使能奇校验。
(4)STOP(bit6):停止位数量,为 0 的话 1 位停止位,为 1 的话 2 位停止位;
(5)WS(bit5):数据位长度,为 0 的时候选择 7 位数据位,为 1 的时候选择 8 位数据位;
(6)TXEN(bit2):发送使能位,为 0 的时候关闭 UART 的发送功能,为 1 的时候打开 UART的发送功能;
(7)RXEN(bit1):接收使能位,为 0 的时候关闭 UART 的接收功能,为 1 的时候打开 UART的接收功能;
(8)SRST(bit0):软件复位,为 0 的是时候软件复位 UART,为 1 的时候表示复位完成。复位完成以后此位会自动置 1,表示复位完成。此位只能写 0,写 1 会被忽略掉。
UART1->UCR2 &= ~(1 << 0);
delay_us(1);
unsigned int tmp = UART1->UCR2;
tmp |= (1 << 14);
tmp &= ~(1 << 8);
tmp &= ~(1 << 6);
tmp |= (1 << 5);
tmp |= (1 << 2);
tmp |= (1 << 1);
UART1->UCR2 = tmp;
UARTx_UCR3:
该寄存器的第二位RXDMUXSEL必须始终为1,手册规定。
(1)TXDC(bit3):发送完成标志位,为 1 的时候表明发送缓冲(TxFIFO)和移位寄存器为空,也就是发送完成,向 TxFIFO 写入数据此位就会自动清零;
(2)RDR(bit0):数据接收标志位,为 1 的时候表明至少接收到一个数据,从寄存器UARTx_URXD 读取数据接收到的数据以后此位会自动清零。
UART1->UCR3 |= (1 << 2);
第0位和第3位的使用我们写到读写函数中去
3.波特率配置:
首先需要把UART的工作频率确定下来,PLL3时钟为480MHz,经由一个静态6分频的分频器之后为80MHz。此后UART还有一个自己的分频器,由寄存器UARTx_UFCR[(bit9~bit7]决定分频值。我们需要1分频,即为b101。
接着波特率由寄存器UARTx_UBIR 和 UARTx_UBMR计算得出,计算公式为:(手册第3593页)
公式中Ref Freq为UART的实际工作频率(80MHz),RaudRate是波特率,UBMR:寄存器 UARTx_UBMR 中的值;UBIR:寄存器 UARTx_UBIR 中的值。

例如:波特率为115200,工作频率是80MHz,那么上面公式右边为(80000000)/(16*115200)= 43.402。就是说如果(UBIR+1)=1000的话,(UBMR+1)是43402。那么UBIR=999,UBMR=43401。
tmp = UART1->UFCR;
tmp &= ~(0x7 << 7);
tmp |= (0x5 << 7);
UART1->UFCR = tmp;
UART1->UBIR = 999;
UART1->UBMR = 43404;
4.读写函数:
先介绍一下UARTx_USR2(UART 的状态寄存器2 ):
(1)TXDC(bit3): 发送完成标志位,为 1 的时候表明发送缓冲(TxFIFO)和移位寄存器为空,也就是发送完成,向 TxFIFO 写入数据此位就会自动清零;
(2)RDR(bit0): 数据接收标志位,为 1 的时候表明至少接收到一个数据,从寄存器UARTx_URXD 读取数据接收到的数据以后此位会自动清零。
写函数:
UTXD:写数据寄存器,
void uart_send_byte(unsigned char data)
{
while(!(UART1->USR2 & (1 << 3))); //当第3位不为0时,跳出循环
UART1->UTXD = data;
}
读函数:
URXD:读数据寄存器,
unsigned char uart_recv_byte
{
unsigned char data = 0;
while(!(UART1->USR2 & (1 << 0))); //当第3位不为0时,跳出循环
data = UART1->URXD & 0xff;
return data;
}
注意:
这里为什么要使用data = UART1->URXD & 0xff;这样的操作,是因为只有低八位才是有效数据,不&0xff会读出来一些我们不需要的东西
5.整体代码:
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "uart.h"
#include "gpt.h"
void uart_init(UART_Type * base)
{
if (base == UART1)
{
IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0);
IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0);
IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX, 0x10b0);
IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10b0);
}
else if(base == UART2)
{
IOMUXC_SetPinMux(IOMUXC_UART2_RX_DATA_UART2_RX, 0);
IOMUXC_SetPinMux(IOMUXC_UART2_TX_DATA_UART2_TX, 0);
IOMUXC_SetPinConfig(IOMUXC_UART2_RX_DATA_UART2_RX, 0x10b0);
IOMUXC_SetPinConfig(IOMUXC_UART2_TX_DATA_UART2_TX, 0x10b0);
}
base->UCR2 &= ~(1 << 0);
delay_us(1);
unsigned int tmp = base->UCR2;
tmp |= (1 << 14);
tmp &= ~(1 << 8);
tmp &= ~(1 << 6);
tmp |= (1 << 5);
tmp |= (1 << 2);
tmp |= (1 << 1);
base->UCR2 = tmp;
base->UCR3 |= (1 << 2);
tmp = base->UFCR;
tmp &= ~(0x7 << 7);
tmp |= (0x5 << 7);
base->UFCR = tmp;
base->UBIR = 999;
base->UBMR = 43404;
base->UCR1 |= (1 << 0);
}
void uart_send_byte(UART_Type * base, unsigned char data)
{
while(!(base->USR2 & (1 << 3)));
base->UTXD = data;
}
unsigned char uart_recv_byte(UART_Type * base)
{
unsigned char data = 0;
while(!(base->USR2 & (1 << 0)));
data = base->URXD & 0xff;
return data;
}
这里改了一下函数的参数,让我们可以选择打开任意一个串口
最后main.c:

下载随便一个串口调试助手,就可以看到我们发送出去的16进制数据会被加1再返回给我们。
三、格式化输入输出
虽然可以用来调试程序,但是功能太单一了,只能输出字符 。如果需要输出数字的时候就需要我们自己先将数字转换为字符,非常的不方便 。如果可以使用printf 函数来完成格式化输出了,那就非常方便了。本篇我们来继续学习如何将 printf 这样的格式化函数移植到 I.MX6U 开发板上。
移植的基本原理很简单,就是把printf和scanf函数映射到UART1上,之后用串口调试助手等软件就可以非常方便地和开发板之间通信了。
配套资料中的文件夹 stdio 里面的文件就是我们要移植的源码文件(需要资料请私信我),将该文件夹复制到工程目录下。stdio 里面的文件其实是从 uboot 里面移植过来的。后面学习 uboot 以后大家有兴趣的话可以自行从 uboot 源码里面"扣"出相应的文件,完成格式化函数的移植。这里要注意一点,stdio 中并没有实现完全版的格式化函数,比如 printf 函数并不支持浮点数,但是基本够我们使用了。
文件拷贝完成以后要做一下几点修改,否则编译或者链接时会出错:
1.需要在uart.c中添加一个函数:void raise(int n);函数体为空即可,否则会报错;
2.如果之前编写的project/start.s文件扩展名为小写s,则需改成大写。即start.S。对于汇编文件来说,扩展名S和s说不一样的,S会先做预处理,在进行编译而s则省略预处理步骤;
3.由于增加了新的源码和头文件,所以Makefile也需要修改。4.之前所有引用.s的地方都需要改成.S
5.编译选项增加-Wa,-mimplicit-it=thumb --nostdlib,否则的话会有如下类似的错误提示thumb conditional instruction should be in IT block --`addcsr5,r5,#65536'
6.增加源程序路径和头文件路径,并添加链接目录
整体的makefile代码:
target = uart
cross_compiler = arm-linux-gnueabihf-
cc = $(cross_compiler)gcc
ld = $(cross_compiler)ld
objcopy = $(cross_compiler)objcopy
objdump = $(cross_compiler)objdump
incdirs = bsp sdk stdio/include
srcdirs = bsp project stdio/lib
include = $(patsubst %, -I%, $(incdirs))
cfiles = $(foreach dir, $(srcdirs), $(wildcard $(dir)/*.c))
sfiles = $(foreach dir, $(srcdirs), $(wildcard $(dir)/*.S))
cfilenodir = $(notdir $(cfiles))
sfilenodir = $(notdir $(sfiles))
cobjs = $(patsubst %, obj/%, $(cfilenodir:.c=.o))
sobjs = $(patsubst %, obj/%, $(sfilenodir:.S=.o))
objs = $(cobjs) $(sobjs)
VPATH = $(srcdirs)
gcc_lib = -lgcc -L /home/linux/tools/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/4.9.4
$(target).bin : $(objs)
$(ld) -Timx6ull.lds -o$(target).elf $^ $(gcc_lib)
$(objcopy) -O binary -S -g $(target).elf $@
$(objdump) -D $(target).elf > $(target).dis
$(sobjs) : obj/%.o : %.S
@mkdir -p obj
$(cc) -Wa,-mimplicit-it=thumb -nostdlib -fno-builtin -c $(include) -o $@ $<
$(cobjs) : obj/%.o : %.c
@mkdir -p obj
$(cc) -Wa,-mimplicit-it=thumb -nostdlib -fno-builtin -c $(include) -o $@ $<
.PHONY : clean
clean:
rm -rf $(objs) $(target).elf $(target).bin $(target).dis
load:
./imxdownload ./$(target).bin /dev/sdb
如果报错缺少putc和getc就自己写一个(就是读写函数):
void putc(unsigned char data)
{
while(!(UART1->USR2 & (1 << 3)));
UART1->UTXD = data;
}
unsigned char getc(void)
{
unsigned char data = 0;
while(!(UART1->USR2 & (1 << 0)));
data = UART1->URXD & 0xff;
return data;
}
移植好了以后,我们就可以实现一个电脑输入命令来控制开发板led和蜂鸣器的函数了。
main.c:
#include "MCIMX6Y2.h"
#include "beep.h"
#include "clk.h"
#include "core_ca7.h"
#include "epit.h"
#include "fsl_iomuxc.h"
#include "gpt.h"
#include "irq.h"
#include "key.h"
#include "led.h"
#include "stdio.h"
#include "uart.h"
void gpio1_io18_handler(void)
{
GPIO1->DR ^= (1 << 3);
GPIO5->DR ^= (1 << 1);
}
void epit1_irq_handler()
{
GPIO1->DR ^= (1 << 3);
GPIO5->DR ^= (1 << 1);
}
int main(void)
{
char data[50] = {0};
// system_irq_init();
clk_init();
led_init();
beep_init();
// key_irq_init(gpio1_io18_handler);
// epit1_init(epit1_irq_handler);
gpt1_init();
uart_init(UART1);
int num = 0;
while (1)
{
// scanf("%d %d\r\n", &num1, &num2);
// num1++;
// printf("%d + %d = %d\n", num1, num2, num1 + num2);
printf("input ur command:");
scanf("%s", data);
// printf("\n%s\n", data);
#if 1
if (strncmp(data, "ledon", 5) == 0)
{
led_on();
printf("open led already\r\n");
}
else if (strncmp(data, "ledoff", 6) == 0)
{
led_off();
printf("close led already\r\n");
}
else if (strncmp(data, "beepon", 6) == 0)
{
beep_on();
printf("open beep already\r\n");
}
else if (strncmp(data, "beepoff", 7) == 0)
{
beep_off();
printf("close beep already\r\n");
}
else
{
printf("invalid command\r\n");
}
#endif
// uart_send_byte(UART1, data + 1);
// delay_ms(1000);
}
return 0;
}
四、总结
串口通信的原理一定要搞明白,明白之后就算换了一个开发板我们也能配置


