day52 串口通信原理与IMX6ULL UART驱动开发
本日内容聚焦于嵌入式系统中至关重要的串行通信 技术。我们将从基础概念入手,深入剖析其工作原理,并最终在 IMX6ULL 开发板上实现一个完整的 UART 驱动程序,包括数据的发送、接收以及标准库 stdio
的移植,使我们能够像在 PC 上一样使用 printf
和 scanf
进行调试和交互。
一、串口通信核心概念
串口通信是嵌入式系统中最基础、最常用的主机间数据交换方式。理解其底层逻辑是进行外设驱动开发的前提。
1.1 通信的基本维度
通信方式可以从三个主要维度进行分类:
-
传输方式 (Transmission Mode):
- 串行 (Serial): 数据位按顺序一位接一位地在单根数据线上传输。优点是布线简单、成本低、抗干扰能力强,适合长距离通信。缺点是传输速率相对较低。
- 并行 (Parallel): 数据位在多根数据线上同时传输。优点是传输速率高(理论速率为串行的 N 倍,N 为数据线数)。缺点是布线复杂、易受干扰、成本高,适合短距离高速通信(如芯片内部总线)。
-
同步方式 (Synchronization Mode):
- 异步 (Asynchronous): 发送方和接收方没有共享的时钟信号。双方必须预先约定好相同的波特率(Baud Rate),通过数据帧中的起始位和停止位来同步数据的开始和结束。这种方式简单灵活,但传输效率略低(因需额外的起始/停止位)。
- 同步 (Synchronous): 发送方和接收方共享一个时钟信号(通常由一根独立的时钟线 SCL 提供)。数据在时钟信号的边沿被采样,无需起始/停止位,传输效率高。但需要额外的时钟线,协议更复杂。例如 IIC、SPI 协议。
-
通信方向 (Direction Mode):
- 单工 (Simplex): 数据只能在一个方向上传输。一方固定为发送端,另一方固定为接收端。例如:广播电台 -> 收音机。
- 半双工 (Half-Duplex): 数据可以在两个方向上传输,但同一时刻只能有一个方向。双方需分时使用信道。例如:对讲机。
- 全双工 (Full-Duplex): 数据可以同时在两个方向上传输。双方可以同时发送和接收数据。例如:电话通话。

这张图主要展示了计算机通信中的几种数据传输方式及串口通信的典型模式,可从"串行/并行传输""同步/异步传输""通信制式(单工/半双工/全双工)"和"串口(UART)通信"几个维度解析:
1. 串行传输与并行传输(图中前两个模块对比)
- 串行传输(上方模块) :
主机A与主机B之间仅用1根数据线 传输数据,数据位(如1 0 1 0 0 0 1 1
)按"位"依次发送(先送最高位或最低位,逐位传递)。
优点:布线简单(仅需少数线)、适合长距离传输;缺点:传输速率相对低(因为一位一位发)。 - 并行传输(中间模块) :
主机A与主机B之间用多根数据线 (图中是8根,对应1字节)同时传输数据,每个数据位在各自的线上同步发送。
优点:传输速率高(8位同时发,是串行的8倍理论速率);缺点:布线复杂(线多)、长距离易受干扰(多线间串扰),适合短距离高速场景(如计算机内部总线)。
2. 同步与异步传输(结合串行模块理解)
- 同步传输 :
依赖统一的时钟信号(图中上方的"方波"就是时钟),发送方和接收方用同一个时钟来同步数据位的收发(时钟跳变时采样数据)。优点:传输效率高;缺点:必须有时钟线,布线或协议更复杂。 - 异步传输 :
不依赖统一时钟,而是通过起始位、停止位等"帧格式"来同步(如串口UART)。发送方随机发数据,但每段数据前加起始位,后加停止位,接收方通过检测起始位来同步采样。优点:无需时钟线,布线简单;缺点:每帧有额外的起始/停止位,传输效率略低。
3. 通信制式(单工、半双工、全双工)
- 单工:只能单向传输(如广播,电台→收音机,只能电台发,收音机收)。
- 半双工 :能双向传输,但同一时间只能一个方向(如对讲机,A讲时B只能听,B讲时A只能听)。
- 全双工 :能同时双向传输(如打电话,你说的同时也能听对方说)。
4. 串口(UART)通信(下方模块)
图中标注"串口:异步串行全双工通信方式",是最典型的异步串行全双工场景:
- 硬件连接 :主机A的
TXD
(发送数据)连主机B的RXD
(接收数据),主机A的RXD
连主机B的TXD
,实现双向同时传输(全双工)。 - 异步特性:无统一时钟,靠"帧格式"同步。
- 参数示例 :图中"
115200 N 8 1
""9600 E 7 1.5 O
"是串口参数:115200
/9600
:波特率(每秒传输的位数,越高速率越快)。N
/E
/O
:校验位(None无校验、Even偶校验、Odd奇校验,用于检错)。8
/7
:数据位(每帧传输的有效数据位数,通常8位对应1字节)。1
/1.5
:停止位(数据帧结束的标志位,告诉接收方"这一帧结束了")。
5. 应用指令(底部文字)
"从终端输入 led on 输出:亮灯......" 是串口通信的应用场景示例 :通过串口给设备发指令(如led on
),设备解析后执行对应动作(亮灯、蜂鸣器开关等),体现了串口在"指令控制"中的实用价值。
总结:这张图通过对比和实例,把"串行/并行""同步/异步""通信制式""串口UART"这几个通信核心概念串联起来,帮助理解不同传输方式的特点和应用场景。
1.2 串口数据帧格式 (TTL电平为例)
串口通信的数据是以"帧"为单位进行传输的。一个典型的帧包含以下几个部分:
- 空闲状态 (Idle State): 在没有数据传输时,数据线保持高电平。
- 起始位 (Start Bit): 一个低电平脉冲,标志着一个数据帧的开始。接收方检测到这个下降沿,便开始准备接收后续数据。
- 数据位 (Data Bits): 实际要传输的数据,通常是 7 或 8 位。按照 LSB (Least Significant Bit, 最低位) 优先的原则发送。
- 校验位 (Parity Bit, 可选): 用于简单的错误检测。
- 无校验 (None, N): 不使用校验位。
- 奇校验 (Odd, O): 保证数据位 + 校验位中"1"的总数为奇数。
- 偶校验 (Even, E): 保证数据位 + 校验位中"1"的总数为偶数。
- 固定值校验: 校验位恒为 0 或 1。
- 停止位 (Stop Bit): 一个或多个高电平脉冲,标志着一个数据帧的结束。常见的有 1 位、1.5 位或 2 位停止位。
1 | 0 | 1 | 0 | 0 | 0 | 1 | 1 |
---|---|---|---|---|---|---|---|
主机A - 主机B | |||||||
串行 | |||||||
并行 | |||||||
11000101 | |||||||
串口:异步串行全双工通信方式 |
1.3 电气标准与物理层问题
不同的电气标准解决了不同距离和抗干扰需求的问题。
-
TTL (Transistor-Transistor Logic):
- 定义: 由芯片引脚直接产生的电压电平。具体电压值取决于芯片的工作电压(如 5V、3.3V、1.8V)。
- 特点: 电平范围小(如 3.3V 系统下,高电平 ~3.3V,低电平 ~0V),抗干扰能力弱 ,传输距离短(通常限制在 10~20 米内,实际应用中多用于同一块电路板上的芯片间通信)。
- 问题: 导线存在内阻,长距离传输会导致电压衰减和串扰,使得接收端无法正确识别高低电平。
-
RS232:
- 定义: IEEE 制定的标准,规定了逻辑电平。
- 逻辑高电平 (1): -3V 至 -15V
- 逻辑低电平 (0): +3V 至 +15V
- 特点: 使用负逻辑 (负电压表示高,正电压表示低),电压幅度大 ,抗干扰能力强,传输距离可达 20~30 米。采用三根线(TX, RX, GND),是全双工通信。
- 定义: IEEE 制定的标准,规定了逻辑电平。
-
RS485:
- 定义: 一种差分传输标准。
- 特点: 使用两根信号线 A 和 B,通过比较 A 和 B 之间的电压差 来识别信息。电压范围分别为 +7V 到 +12V 和 -7V 到 -12V。这种差分信号传输方式极大地提高了抗共模干扰能力 。传输距离可达 1200 米。由于在同一时刻,两根线都用于传输同一个比特的信息,因此它是半双工通信。
二、IMX6ULL UART 外设原理分析
IMX6ULL 的 UART 模块是一个复杂的异步串行收发器,我们需要理解其寄存器配置才能正确驱动它。
2.1 时钟树分析
UART 模块需要一个稳定的时钟源。根据《IMX6ULL参考手册》,其时钟路径如下:
- 时钟源: 来自 CCM (Clock Control Module) 的
uart_clk_root
。 - 默认配置:
uart_clk_root
默认由pll3_sw_clk
(即 PLL3) 提供,频率为 480MHz。 - 分频: 经过一个静态分频器(6分频),得到 80MHz 的
uart_serial_clk
。 - 最终时钟:
uart_serial_clk
直接作为 UART 模块的时钟源(UARTx_CLK
),用于产生波特率。
注意: 我们在初始化时通常不需要修改这个时钟路径,直接使用默认的 80MHz 时钟即可满足常见波特率的需求。
2.2 关键寄存器详解
以下是 UART 初始化和操作所需的关键寄存器及其位域说明:
2.2.1 UART Receiver Register (URXD)
- 地址:
UARTx_URXD
- 功能: 只读寄存器,用于读取接收到的数据。
- 关键位域:
[RX_DATA] b0-7
: 已接收的数据。在 7 位模式下,最高位 (MSB) 被强制为 0;在 8 位模式下,所有位均有效。
2.2.2 UART Transmitter Register (UTXD)
- 地址:
UARTx_UTXD
- 功能: 写寄存器,用于发送数据。
- 关键位域:
[TX_DATA] b0-7
: 需要发送的数据。当软件向此字段写入数据时,UART 硬件会自动开始发送过程。
2.2.3 UART Control Register 1 (UCR1)
- 地址:
UARTx_UCR1
- 功能: 控制 UART 模块的总开关。
- 关键位域:
[UARTEN] b0
: UART 模块使能位。置 1 开启 UART,清 0 关闭 UART。这是最后一步操作。
2.2.4 UART Control Register 2 (UCR2)
- 地址:
UARTx_UCR2
- 功能: 控制接收器、发送器、数据格式等。
- 关键位域:
[UARTEN] b0
: 软件复位位。写 0 触发复位,复位完成后硬件自动置 1。复位需持续 4 个模块时钟周期。[RXEN] b1
: 接收器使能位。置 1 启用接收功能。[TXEN] b2
: 发送器使能位。置 1 启用发送功能。[WS] b5
: 字长选择位。置 1 选择 8 位数据,清 0 选择 7 位数据。[STPB] b6
: 停止位数量选择位。清 0 选择 1 位停止位,置 1 选择 2 位停止位。[PREN] b8
: 奇偶校验使能位。清 0 禁用校验,置 1 启用校验。[IRTS] b14
: 忽略 RTS 引脚流控位。置 1 忽略 RTS 流控信号(我们通常不使用流控,故置 1)。
2.2.5 UART Control Register 3 (UCR3)
- 地址:
UARTx_UCR3
- 功能: 控制多路复用模式等。
- 关键位域:
[RXDMUXSEL] b2
: 接收数据多路复用选择位。在 IMX6ULL 中,UART 工作于多路复用模式,此位必须始终设置为 1。
2.2.6 UART FIFO Control Register (UFCR)
- 地址:
UARTx_UFCR
- 功能: 控制 FIFO 和参考时钟分频。
- 关键位域:
[RFDIV] b7-b9
: 参考时钟分频器。控制参考时钟的分频比。我们使用默认的 80MHz 时钟,故设置为 1 分频(值为 5,因为RFDIV = 0b101
对应 1 分频)。
2.2.7 UART Status Register 2 (USR2)
- 地址:
UARTx_USR2
- 功能: 只读寄存器,反映 UART 的当前状态。
- 关键位域:
[TXDC] b3
: 发送完成标志位。置 1 表示发送移位寄存器和发送缓冲区均为空,可以发送下一个字符。[RDR] b0
: 接收数据就绪标志位。置 1 表示接收缓冲区中有有效数据,可以读取。
2.2.8 波特率配置寄存器
-
UART BRM Incremental Register (UBIR):
- 地址:
UARTx_UBIR
- 功能: 存储波特率调制增量值。
- 关键位域:
[INC] b0-b15
: UBIR 值。
- 地址:
-
UART BRM Modulator Register (UBMR):
- 地址:
UARTx_UBMR
- 功能: 存储波特率调制器值。
- 关键位域:
[MOD] b0-b15
: UBMR 值。
- 地址:
-
计算公式:
BaudRate = Ref Freq / (16 x ((UBMR + 1) / (UBIR + 1)))
其中
Ref Freq
是模块时钟频率(80MHz)。对于 115200 波特率,经过计算,UBIR = 999
,UBMR = 43401
。
三、代码实现:UART 驱动开发
3.1 引脚复用与电气配置
在使用 UART 之前,必须将对应的 GPIO 引脚复用为 UART 功能,并配置其电气特性。
c
// 引脚复用配置
IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0); // 将 UART1_RX 引脚复用为 UART1_RX 功能
IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0); // 将 UART1_TX 引脚复用为 UART1_TX 功能
// 引脚电气特性配置 (0x10B0 是一个常用配置,代表 100KΩ 下拉电阻,100MHz 频率,开漏输出等)
IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX, 0x10B0);
IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10B0);
3.2 UART 初始化函数 (uart1_init
)
该函数负责初始化 UART1 外设,使其处于可用状态。
c
#include "uart.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
void uart1_init(void)
{
// 1. 配置引脚复用和电气特性
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);
// 2. 软件复位 UART (写 0 到 UCR2[0])
UART1->UCR2 &= ~(1 << 0);
// 3. 配置 UCR2 寄存器
unsigned int t;
t = UART1->UCR2; // 读取当前值
t |= (1 << 14); // [IRTS] 忽略 RTS 流控
t &= ~(1 << 8); // [PREN] 清除奇偶校验使能 (无校验)
t &= ~(1 << 6); // [STPB] 清除停止位选择 (1位停止位)
t |= (1 << 5); // [WS] 设置字长为 8 位
t |= (1 << 2); // [TXEN] 使能发送器
t |= (1 << 1); // [RXEN] 使能接收器
UART1->UCR2 = t; // 写回配置
// 4. 配置 UCR3 寄存器
UART1->UCR3 |= (1 << 2); // [RXDMUXSEL] 设置为多路复用模式 (必须为 1)
// 5. 配置 UFCR 寄存器 (设置分频为 1)
UART1->UFCR &= ~(7 << 7); // 清除 RFDIV 位域 (b7-b9)
UART1->UFCR |= (5 << 7); // 设置 RFDIV = 0b101 (1分频)
// 6. 配置波特率 (UBIR 和 UBMR)
// 注意: 必须先写 UBIR,再写 UBMR
UART1->UBIR = 999; // 设置 UBIR
UART1->UBMR = 43401; // 设置 UBMR
// 7. 使能 UART 模块 (最后一步)
UART1->UCR1 |= (1 << 0); // [UARTEN] 使能 UART
}
3.3 数据发送函数 (putc
, puts
)
putc
: 发送单个字符。puts
: 发送字符串,并在末尾追加换行符。
c
// 发送单个字符
void putc(unsigned char ch)
{
// 等待发送完成标志位 (TXDC) 置位
while ((UART1->USR2 & (1 << 3)) == 0)
; // 循环等待,直到发送缓冲区和移位寄存器都为空
// 将字符写入发送寄存器,触发发送过程
UART1->UTXD = ch;
}
// 发送字符串
void puts(const char *pStr)
{
// 遍历字符串,逐个字符发送
while (*pStr)
{
putc(*pStr++); // 发送当前字符,并指针后移
}
// 发送换行符
putc('
');
}
3.4 数据接收函数 (getc
)
接收单个字符。
c
// 接收单个字符
unsigned char getc(void)
{
// 等待接收数据就绪标志位 (RDR) 置位
while ((UART1->USR2 & (1 << 0)) == 0)
; // 循环等待,直到接收缓冲区中有有效数据
// 从接收寄存器读取数据,并只保留低8位
return (unsigned char)(UART1->URXD & 0xFF);
}
3.5 移植 stdio
标准库
为了让我们的程序能够使用 printf
, scanf
等标准库函数,需要将其重定向到我们自己实现的 putc
和 getc
函数。
3.5.1 添加必要函数
在 uart.c
文件中添加 raise
函数(否则链接时会报错)。
c
// stdio 库可能调用的空函数
void raise(int n)
{
// 为空即可,避免链接错误
}
3.5.2 修改 Makefile
为了支持 stdio
库,需要在编译和链接阶段添加相应的选项和库路径。
makefile
# ... (其他部分保持不变)
# 定义库路径变量
libpath = -lgcc -L/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/4.9.4/
# 指定头文件和源文件目录
incdirs = bsp imx6ull stdio/include
srcdirs = bsp project stdio/lib
# 编译规则 (增加 -fno-builtin 选项,禁用内置函数)
$(sobjs) : obj/%.o : %.S
@mkdir -p obj
$(cc) -Wall -Wa,-mimplicit-it=thumb -fno-builtin -nostdlib -c $(include) -o $@ $<
# 链接规则 (添加 libpath)
$(target).bin : $(objs)
$(ld) -Timx6ull.lds -o$(target).elf $^ $(libpath)
$(objcopy) -O binary -S -g $(target).elf $@
$(objdump) -D $(target).elf > $(target).dis
3.5.3 在主程序中使用 stdio
现在可以在 main.c
中包含 <stdio.h>
并使用 printf
和 scanf
。
c
#include "stdio.h" // 包含标准输入输出头文件
int main(void)
{
system_interrupt_init();
clock_init();
led_init();
beep_init();
gpt1_init();
uart1_init(); // 初始化 UART
int num1 = 0, num2 = 0;
while(1)
{
// 从串口接收两个整数
scanf("%d%d", &num1, &num2);
// 通过串口打印计算结果
printf("%d + %d = %d
", num1, num2, num1 + num2);
}
return 0;
}
四、实验验证
- 编译下载: 编译整个工程,生成
.bin
文件并烧录到开发板。 - 连接串口: 使用 USB-TTL 线将开发板的
USB_TTL
接口连接到电脑。 - 打开串口助手: 在电脑上打开串口调试助手,设置波特率为
115200
,数据位8
,停止位1
,无校验。 - 观察结果: 上电后,程序会进入循环,等待用户输入。在串口助手中输入两个数字(如
12 34
),然后按回车。程序会计算它们的和并通过串口返回结果(如12 + 34 = 46
)。
通过以上步骤,我们成功实现了 IMX6ULL 的 UART 通信功能,并完成了 stdio
标准库的移植,为后续的嵌入式开发打下了坚实的基础。