一、串口介绍
串口是单片机和嵌入式 Linux 开发中最常用的外设,可用于:
- 将开发板与电脑相连,通过串口调试助手调试程序
- 蓝牙、GPS、GPRS 等模块与主控通信
- 嵌入式 Linux 中作为控制台使用
I.MX6U 共有 8 个 UART,主要特性:
- 兼容 TIA/EIA-232F 标准,速度最高可到 5Mbit/S
- 支持串行 IR 接口,兼容 IrDA,最高可到 115.2Kbit/s
- 支持 9 位或者多节点模式(RS-485)
- 1 或 2 位停止位
- 可编程的奇偶校验(奇校验和偶校验)
- 自动波特率检测(最高支持 115.2Kbit/S)
本篇目标:实现 115200,8,N,1 与电脑之间通信。
二、配置串口寄存器
时钟配置
- 时钟源由寄存器 CCM_CSCDR1 的
UART_CLK_SEL(bit)位选择:- 为 0:UART 时钟源为
pll3_80m(80MHz)← 一般选择此项 - 为 1:UART 时钟源为
osc_clk(24MHz)
- 为 0:UART 时钟源为
UART_CLK_PODF(bit5:0):时钟分频值,可设置 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 模块、蓝牙模块等)才需要配置
2. 配置三个 UART 控制寄存器
UARTx_UCR1
| 位 | 名称 | 说明 |
|---|---|---|
| bit14 | ADBR | 自动波特率检测使能位:0=关闭,1=使能 |
| bit0 | UARTEN | UART 使能位:0=关闭,1=使能 |
⚠️ 注意:要最后配好所有东西再使能。
UART1->UCR1 |= (1 << 0); // 使能 UART(最后执行)
UARTx_UCR2
| 位 | 名称 | 说明 |
|---|---|---|
| bit14 | IRTS | 0=使用 RTS 引脚功能,1=忽略 RTS 引脚 |
| bit8 | PREN | 奇偶校验使能位:0=关闭,1=使能 |
| bit7 | PROE | 奇偶校验模式选择:0=偶校验,1=奇校验 |
| bit6 | STOP | 停止位数量:0=1位,1=2位 |
| bit5 | WS | 数据位长度:0=7位,1=8位 |
| bit2 | TXEN | 发送使能位:0=关闭,1=打开 |
| bit1 | RXEN | 接收使能位:0=关闭,1=打开 |
| bit0 | SRST | 软件复位:0=软件复位 UART,1=复位完成(自动置1) |
UART1->UCR2 &= ~(1 << 0); // 软件复位
delay_us(1);
unsigned int tmp = UART1->UCR2;
tmp |= (1 << 14); // 忽略 RTS
tmp &= ~(1 << 8); // 关闭奇偶校验
tmp &= ~(1 << 6); // 1 位停止位
tmp |= (1 << 5); // 8 位数据位
tmp |= (1 << 2); // 使能发送
tmp |= (1 << 1); // 使能接收
UART1->UCR2 = tmp;
UARTx_UCR3
该寄存器的第二位 RXDMUXSEL 必须始终为 1(手册规定)
| 位 | 名称 | 说明 |
|---|---|---|
| bit3 | TXDC | 发送完成标志位:为1时发送缓冲(TxFIFO)和移位寄存器为空(发送完成),写入TxFIFO后自动清零 |
| bit0 | RDR | 数据接收标志位:为1时表明至少接收到一个数据,读取 UARTx_URXD 后自动清零 |
UART1->UCR3 |= (1 << 2); // RXDMUXSEL 置 1
3. 波特率配置
工作频率计算:
- PLL3 时钟 480MHz → 静态 6 分频 → 80MHz
- UART 自身分频器由
UARTx_UFCR[(bit9~bit7)]决定 → 需要 1分频 ,即b101
波特率计算公式:
(UBMR + 1) / (UBIR + 1) = RefFreq / (16 × BaudRate)
RefFreq:UART 实际工作频率(80MHz)BaudRate:波特率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); // 设置 1 分频(b101)
UART1->UFCR = tmp;UART1->UBIR = 999;
UART1->UBMR = 43404; // 实际代码中用 43404
4. 读写函数
UARTx_USR2(UART 状态寄存器2)
| 位 | 名称 | 说明 |
|---|---|---|
| bit3 | TXDC | 发送完成标志位,为1时发送缓冲和移位寄存器为空 |
| bit0 | RDR | 数据接收标志位,为1时至少接收到一个数据 |
写函数(JTXD:写数据寄存器)
void uart_send_byte(UART_Type * base, unsigned char data)
{
while(!(base->USR2 & (1 << 3))); // 等待发送完成(第3位为1则跳出)
base->UTXD = data;
}
读函数(URXD:读数据寄存器)
unsigned char uart_recv_byte(UART_Type * base)
{
unsigned char data = 0;
while(!(base->USR2 & (1 << 0))); // 等待接收到数据(第0位为1则跳出)
data = base->URXD & 0xff; // 只取低8位(有效数据)
return data;
}
注意 :
data = UART1->URXD & 0xff,因为只有低八位才是有效数据。
三、完整 uart.c 代码
#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); // 忽略 RTS
tmp &= ~(1 << 8); // 关闭奇偶校验
tmp &= ~(1 << 6); // 1 位停止位
tmp |= (1 << 5); // 8 位数据位
tmp |= (1 << 2); // 使能发送
tmp |= (1 << 1); // 使能接收
base->UCR2 = tmp;
base->UCR3 |= (1 << 2); // RXDMUXSEL 置 1
tmp = base->UFCR;
tmp &= ~(0x7 << 7);
tmp |= (0x5 << 7); // 1 分频
base->UFCR = tmp;
base->UBIR = 999;
base->UBMR = 43404;
base->UCR1 |= (1 << 0); // 使能 UART
}
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;
}
四、格式化输入输出(printf/scanf 移植)
移植原理
将 printf 和 scanf 函数映射到 UART1 上,之后用串口调试助手等软件就可以非常方便地和开发板通信。
- 配套资料中的 stdio 文件夹里的文件是要移植的源码(从 uboot 移植)
- stdio 中并没有实现完整版的格式化函数,比如 printf 不支持浮点数,但基本够用
移植步骤(修改项)
-
在 uart.c 中添加函数 :
void raise(int n);函数体为空即可,否则会报错 -
汇编文件扩展名改大写 :
start.s→start.S- S 会先做预处理,再进行编译;s 则省略预处理步骤
-
Makefile 修改:由于增加了新的源码和头文件,Makefile 需要更新
-
所有引用
.s的地方改成.S -
编译选项增加 :
-Wa,-mimplicit-it=thumb --nostdlib,否则报错thumb conditional instruction should be in IT block -
增加源程序路径和头文件路径,并添加链接目录
如果报错缺少 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;
}
五、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
六、main.c 实战:串口命令控制 LED 和蜂鸣器
#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"
int main(void)
{
char data[50] = {0};
clk_init();
led_init();
beep_init();
gpt1_init();
uart_init(UART1);
while (1)
{
printf("input ur command:");
scanf("%s", data);
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");
}
}
return 0;
}
效果 :下载后通过串口调试助手输入命令(ledon/ledoff/beepon/beepoff),开发板即可响应控制。
七、关键寄存器速查表
| 寄存器 | 位 | 功能 |
|---|---|---|
| CCM_CSCDR1 | UART_CLK_SEL | 时钟源选择(0=pll3_80m,1=osc_clk) |
| CCM_CSCDR1 | UART_CLK_PODF | 时钟分频(0~63 → 1~64分频) |
| UCR1 | bit0 UARTEN | UART 使能 |
| UCR1 | bit14 ADBR | 自动波特率检测使能 |
| UCR2 | bit0 SRST | 软件复位 |
| UCR2 | bit1 RXEN | 接收使能 |
| UCR2 | bit2 TXEN | 发送使能 |
| UCR2 | bit5 WS | 数据位(0=7位,1=8位) |
| UCR2 | bit6 STOP | 停止位(0=1位,1=2位) |
| UCR2 | bit7 PROE | 奇偶校验模式(0=偶,1=奇) |
| UCR2 | bit8 PREN | 奇偶校验使能 |
| UCR2 | bit14 IRTS | RTS忽略位 |
| UCR3 | bit2 RXDMUXSEL | 必须始终为1 |
| UCR3 | bit3 TXDC | 发送完成标志 |
| UCR3 | bit0 RDR | 数据接收标志 |
| UFCR | bit9~bit7 | UART 自身分频器 |
| UBIR | - | 波特率计算分子 |
| UBMR | - | 波特率计算分母 |
| USR2 | bit3 TXDC | 发送完成标志(写函数轮询) |
| USR2 | bit0 RDR | 数据接收标志(读函数轮询) |
| UTXD | - | 发送数据寄存器 |
| URXD | - | 接收数据寄存器(只取低8位) |