一、UART 核心概念深度解析
要熟练掌握 UART 开发,必须先吃透通信领域的核心概念,明确 UART 在各类通信方式中的定位,结合串口通信的底层逻辑进一步深化理解:
1. 通信本质与分类基础
嵌入式系统中的通信,本质是两个或多个主机之间的有序二进制数据交互,主机可包括计算机、嵌入式开发板、芯片、传感器等,核心是实现数据的可靠传输与解析。按数据传输方式,通信可分为两大类:
- 并行通信 :多个比特同时通过并行线传输,传输速率高,但占用大量芯片 IO 资源、布线复杂(多线间串扰严重),仅适用于近距离高速场景(如 CPU 与内存、FPGA 内部模块通信)。
- 串行通信 :将数据拆分为单个比特,按先后次序在一根 / 两根总线上传输,系统占用资源少、结构简单,是主机间远距离通信的常用方式。串口通信(Serial Port) 是串行通信的重要分支,属于异步通信,RS232、RS485、RS422 均为串行通信的典型实现标准,UART 是串口通信的核心硬件模块。
2. 关键通信维度分类
(1)异步 vs 同步
-
异步通信 :无需专门的时钟信号线,收发双方通过提前约定波特率、数据位、停止位、奇偶校验位 等参数实现时序同步。每个字符的传输以 起始位 开始,以 停止位 结束,字符之间可存在任意间隔(空闲位)。核心采样机制:接收端检测到起始位(低电平跳变)后,启动内部波特率发生器,在每个比特的中间时刻 采样数据(如 9600bps 时,比特时间 104.1μs,采样时刻为 52μs 处),避开比特边沿的电平不稳定区域。时钟误差容忍度:波特率误差需控制在 ±6% 以内,超过则会导致采样点偏离,数据解析错误。类比:打字时,敲击键盘的速度由使用者控制,无需严格固定间隔,只要接收端能正确识别每个字符即可。优点:硬件简单(仅需 TX/RX 两根线);缺点:传输速率受波特率误差限制,适用于中低速传输(如 9600~115200bps),UART 是异步通信的典型代表。
-
同步通信 :除数据线外,需额外一根时钟线(如 I2C 的 SCL、SPI 的 SCK),由主设备控制时钟线变化,每发送一个比特就触发一次时钟信号,从设备根据时钟边沿(上升沿 / 下降沿) 采样数据。传输特征:数据连续传输,无额外的起始 / 停止位,传输效率高、速率快(SPI 可达几十 Mbps),但硬件复杂度更高(需时钟线同步)。类比:合唱团演唱,所有成员跟随指挥员的节奏保持一致,无停顿、无间隔。典型代表:SPI、I2C、SPI Flash 通信。
(2)串行 vs 并行
| 对比维度 | 串行通信 | 并行通信 |
|---|---|---|
| 数据线数量 | 1~2 根(TX/RX 独立) | 与数据位宽一致(8 位 = 8 根)+ 控制信号线 |
| 传输方式 | 单比特依次传输,按位拼接 | 多比特同时传输,一次完成一个字节 / 字 |
| 实际有效速率 | 标称波特率为总比特率,含帧开销 | 标称速率为实际有效速率,无帧开销 |
| 抗干扰能力 | 信号路径短,串扰少,抗干扰强 | 多线并行,串扰严重,抗干扰弱 |
| 传输距离 | 远(RS485 可达 1200 米) | 极近(通常 < 1 米,需阻抗匹配) |
| 硬件工程成本 | 低(少信号线、少接口芯片、布线简单) | 高(多信号线、需阻抗匹配、布线复杂) |
| 典型应用场景 | 外设组网、远距离通信(传感器、工控) | 板内高速通信(CPU - 内存、FPGA 内部) |
(3)单工 vs 半双工 vs 全双工
-
单工通信 :数据只能沿一个固定方向传输,发送端与接收端角色永久固定,无法反向交互。硬件特征:仅需一根单向数据线,无需方向控制;典型场景:红外遥控(遥控器→电视)、广播电台(电台→收音机)、打印机(主机→打印机)、传感器单向数据上报(温湿度传感器→主控板)。
-
半双工通信 :数据可双向传输 ,但同一时刻只能单向进行 ,需通过硬件(如 RS485 的 DE/RE 引脚)或软件切换传输方向。硬件特征:共用一根数据线 / 总线,需方向控制引脚,多设备组网时需总线仲裁(避免同时发送导致数据冲突);典型场景:对讲机(按住 PTT 说话、松开接收)、RS485 无流控组网(多个传感器共享 A/B 总线)、蓝牙低功耗(BLE)广播模式。
-
全双工通信 :数据可同时在两个方向 传输,收发双方独立工作、互不干扰,需独立的发送(TX)和接收(RX)通道。硬件特征:TX/RX 两根独立数据线,各自形成单向传输通道,无需总线仲裁;典型场景:UART 串口调试(开发板与电脑)、手机通话、RS232 点对点通信(收、发、地三根线实现全双工)、以太网通信。
(4)串口通信数据帧格式
串口通信(以 TTL 电平为例)的数据传输以 "字符帧" 为基本单元,帧结构从前往后依次为空闲位→起始位→数据位→校验位(可选)→停止位 ,常用配置为 8N1(8 数据位 + 1 停止位 + 无校验),也是嵌入式开发的标准配置。
【空闲位】→【起始位】→【数据位bit0】→【数据位bit1】→...→【数据位bit7】→【校验位】→【停止位】
高电平 低电平 LSB ... MSB 可选 高电平
任意长 1bit 1bit ... 1bit 1bit 1/2bit
- 空闲位 :通信空闲时,数据线保持高电平(TTL 3.3V/5V,RS232 -3~-15V),标识无数据传输;若空闲位出现低电平,会被接收端误判为起始位,导致通信错误。
- 起始位 :发送方主动拉低数据线为低电平 ,持续 1 个比特时间,用于触发接收端开始采样(接收端检测到高→低跳变后,启动内部波特率发生器)。
- 数据位 :紧跟起始位之后,可配置为 7 位或 8 位(嵌入式常用 8 位),传输时先发送最低位(LSB),后发送最高位(MSB) ;实例:数据 0x12(二进制 00010010),传输顺序为
0→1→0→0→1→0→0→0(bit0~bit7)。 - 校验位 (可选):用于检测数据传输中的位翻转错误,嵌入式开发中常用无校验 (依赖上层协议保证完整性),工业场景可选奇 / 偶校验:
- 奇校验:确保数据位 + 校验位中 "1" 的总数为奇数;
- 偶校验:确保数据位 + 校验位中 "1" 的总数为偶数。
- 停止位 :紧跟校验位之后,拉高数据线为高电平,持续 1 个或 2 个比特时间(常用 1 个),标识一个字符帧传输结束;停止位长度越长,抗干扰能力越强,但传输效率越低。
(5)传输速率:波特率的核心意义
波特率(bit per second,记作 bps )是串口通信的核心速率指标,表示每秒钟传输的总比特数(含起始位、数据位、校验位、停止位,即帧内所有比特)。
- 常用波特率:1200、2400、4800、9600、19200、38400、57600、115200、230400bps,115200bps 是嵌入式开发中最常用的速率。
- 有效字符速率计算:波特率为总比特率,需扣除帧开销(起始位、停止位、校验位);实例:115200bps、8N1 格式(1 起始 + 8 数据 + 1 停止,无校验),每个字符占 10 比特,有效字符速率 = 115200 / 10 = 11520 字符 / 秒。
- 波特率与比特时间的关系:比特时间 = 1 / 波特率(每个比特在数据线上的持续时间);实例:9600bps 对应的比特时间 ≈ 1.041×10⁻⁴秒(104.1 微秒),115200bps 对应的比特时间 ≈ 8.68 微秒。
- 波特率误差的影响:收发双方波特率必须严格一致,误差超过 ±6% 会导致接收端采样点偏离比特中间区域,出现数据解析乱码、停止位识别失败、数据丢失等问题,通信完全中断。
(6)电气标准与传输距离
串口通信的电气标准定义了 "高低电平如何表示逻辑 0 和 1" ,直接影响传输距离和抗干扰能力,核心限制因素是导线内阻导致的电压衰减 和多线间的串扰,三大主流标准对比如下(含电平转换电路 + 工程细节):
| 电气标准 | 逻辑 0 定义 | 逻辑 1 定义 | 传输距离 | 抗干扰能力 | 核心特征 / 工程细节 | 典型电平转换芯片 | 适用场景 |
|---|---|---|---|---|---|---|---|
| TTL | 0V | 3.3V/5V(芯片决定) | <20 米 | 弱 | 板内直连,无需转换,成本最低;3.3V/5V 电平不可混接(避免 IO 口损坏) | 无(直连) | 板内 / 短距离通信(CH340→IMX6ULL) |
| RS232 | +3V ~ +15V | -3V ~ -15V | <30 米 | 中 | 点对点全双工,需 3 根线(TX/RX/GND);电平与 TTL 反向,需专用转换芯片 | MAX232/MAX3232 | 电脑 - 开发板串口调试 |
| RS485 | A<B(压差≥200mV) | A>B(压差≥200mV) | <1200 米 | 极强 | 差分传输(抵消共模干扰);半双工,需 A/B 两根总线;多设备组网(最多 32 个),需终端电阻 | MAX485/MAX3485 | 工业现场传感器组网、远距离通信 |
工程关键细节:
- TTL 电平:IMX6ULL 为 3.3V 电平,51 单片机为 5V 电平,直连时需加电平转换芯片(如 TXS0108),避免 5V 电平烧毁 3.3V IO 口;
- RS232 电平:MAX232 芯片内置电荷泵,可将 3.3V/5V TTL 电平转换为 ±12V RS232 电平,需外接 4 个 0.1μF 电容实现电压反转;
- RS485 电平:
- 差分传输:通过 A、B 两根线的电压差表示逻辑,可抵消外部电磁干扰(工业现场核心需求);
- 终端匹配:总线长度≥100 米时,需在总线两端设备的 A、B 引脚之间接 120Ω 终端电阻,减少信号反射;
- 方向控制:通过 DE/RE 引脚控制收发方向(DE=1/RE=0 发送,DE=0/RE=1 接收)。
(7)串口通信的归类与定位
串口通信(以 UART 为核心硬件)在 OSI 七层网络模型 中属于物理层 + 数据链路层 ,是嵌入式开发中最基础、最常用的通信方式,核心归类为:异步、串行、全双工通信。
- 物理层:定义电平标准(TTL/RS232/RS485)、传输介质(导线)、波特率、硬件接口;
- 数据链路层:定义帧格式(起始位 / 数据位 / 停止位)、校验方式、数据收发规则;
- 核心特征:支持 TTL/RS232/RS485 三种电气标准,通过电平转换芯片可适配不同传输距离和场景需求,是嵌入式外设通信的 "通用接口"。
3. 常用串口调试软件(开发实用工具 + 功能对比 + 使用技巧)
在 UART 开发调试中,串口调试软件是验证通信效果的关键工具,用于电脑与开发板之间的双向数据交互,Windows 平台下常用工具及特性、使用技巧如下:
| 软件名称 | 核心特性 | 优势 | 适用场景 | 专属使用技巧 |
|---|---|---|---|---|
| STC-ISP 串口助手 | 集成于 STC 下载工具,免安装,支持 ASCII/HEX 传输、数据回显 | 轻量、操作简单,无需额外安装 | 51 单片机、STM32、IMX6ULL 基础调试 | 快速切换波特率,适合初期硬件连通性测试 |
| UartAssist | 绿色免安装,单文件,支持 ASCII/HEX 转换、多种校验(CRC8/16/32/LRC/BCC)、定时自动发送、数据保存 / 导出 | 功能强大,工业级校验支持 | 复杂协议调试、工业场景稳定性测试 | 开启自动发送 + CRC 校验,测试长时通信的可靠性;保存接收数据,便于后续错误分析 |
| SecureCRT | 支持串口 / SSH/Telnet/FTP,多会话管理,日志记录,ANSI 指令支持 | 多协议兼容,适合 Linux 开发 | 嵌入式 Linux 开发、多设备同时调试 | 开启日志记录,保存全程调试信息;支持 ANSI 清屏 / 光标指令,提升交互体验 |
| Putty | 开源、跨平台(Windows/Linux/Mac),轻量,支持串口 / SSH/Telnet | 跨平台,无广告,开源免费 | 跨平台开发、轻量调试 | 配置固定波特率 / 帧格式,保存会话,避免重复设置 |
通用使用技巧:
- 格式选择:传输字符 / 字符串用 ASCII 格式 ,传输二进制数据(如传感器原始数据、寄存器值)用 HEX 格式,避免 ASCII 格式的乱码;
- 波特率 / 帧格式:必须与代码配置一致(如 115200bps、8N1),否则会出现乱码或无数据;
- 数据回显:开启回显功能,验证开发板的发送功能是否正常;
- 清空缓存:通信异常时,清空接收 / 发送缓存,避免旧数据干扰。
二、IMX6ULL UART 硬件原理图深度剖析
本次开发基于 IMX6ULL_MINI_V2.2 开发板,核心硬件模块为 "USB USART&USB POWER",需结合原理图细节、串口通信物理层特性、硬件设计原理,深入理解电脑与开发板之间的 UART 通信硬件工作机制。
1. 核心硬件组成及功能拆解
开发板的 UART 通信硬件核心是 "USB→TTL 电平转换" ,实现电脑 USB 接口与 IMX6ULL UART 引脚的协议 / 电平匹配,核心硬件包括 USB 接口、CH340 芯片、DCDC 电源稳压模块,各部分功能、器件参数、引脚连接、信号流向如下:
(1)USB 接口(USB_TTL)
- 物理接口:Type-C 接口(兼容正反插,比 Micro-USB 更耐用),核心引脚:VBUS(5V 电源)、D+(USB 数据 +)、D-(USB 数据 -)、GND(地);
- 双重作用:① 通信物理通道 :电脑 USB 口的 USB 协议信号,通过 D+/D- 引脚传输至 CH340 芯片;② 辅助电源通道 :VBUS 引脚输出 5V 直流电源,为 CH340 芯片和电平转换电路供电(严禁作为开发板主电源);
- 信号流向(通信):电脑 USB → Type-C D+/D- → CH340 USB 引脚。
(2)CH340 芯片(U8,核心:USB 转 TTL 串口)
- 器件型号:CH340N(内置晶振版,无需外部 12MHz 晶振,减少 PCB 面积和硬件复杂度);
- 核心功能:USB 协议 ↔ UART 协议 、USB 电平 ↔ TTL 电平 双向转换,是电脑与开发板 UART 通信的 "协议翻译官";
- 关键参数:支持 300bps~2Mbps 波特率,兼容 3.3V/5V TTL 电平输出,内置电源管理,工作电流 < 50mA;
- 驱动要求:Windows 系统需安装 CH340 虚拟串口驱动,Linux 内核 4.0+ 内置驱动(免安装),安装后电脑会识别出一个 "虚拟串口"(如 COM3、COM4);
- 核心引脚连接(IMX6ULL_MINI_V2.2):
- USB_D+/USB_D-:连接 Type-C 接口的 D+/D-,接收电脑 USB 信号;
- TXD/RXD:分别连接 IMX6ULL 的 UART1_RX(GPIO1_IO15)/ UART1_TX(GPIO1_IO14) (交叉连接:CH340 TXD → 开发板 RXD,CH340 RXD → 开发板 TXD,核心原则:发送端接接收端);
- VCC:接 3.3V(DCDC 稳压模块输出),配置为 3.3V 电平输出(匹配 IMX6ULL 的 3.3V IO 口);
- GND:接开发板地,与电脑、IMX6ULL 共地(共地是串口通信的前提,否则会出现数据乱码或无数据);
- 信号流向:CH340 USB 引脚 → 内部协议转换 → CH340 TXD/RXD → IMX6ULL UART1_RX/TX。
(3)DCDC 电源稳压模块(U12、U13,核心:稳定供电)
- 器件型号:AMS1117-3.3(线性稳压芯片,性价比高、稳定性好);
- 输入 / 输出:输入 5V(USB VBUS),输出 稳定的 3.3V 直流电压,最大输出电流 800mA;
- 核心作用:防抖、抗干扰、稳定供电,为 CH340 芯片和 UART 模块提供纯净的 3.3V 电源,滤除 USB 电源中的纹波和噪声(如电脑 USB 口的电源波动、外部电磁干扰);
- 外围电路:输入端(5V)、输出端(3.3V)各接 1 个 10μF 电解电容 + 0.1μF 陶瓷电容,进一步滤除电源纹波,保证电压稳定;
- 重要性:UART 通信对电源稳定性要求极高,若电源存在纹波(≥100mV),会导致传输数据的电平抖动,出现位翻转、误码、丢包等问题,DCDC 模块是保障通信稳定性的关键硬件。
2. 硬件设计注意事项
基于 IMX6ULL 硬件特性和串口通信物理层要求,开发板 UART 硬件设计需注意以下关键事项,避免通信失败、硬件损坏,同时补充 PCB 设计规范,适配工程化开发:
(1)严禁使用 USB 作为开发板主电源供电
USB 接口的 VBUS 引脚仅能为 CH340 芯片提供 3.3V 电源,严禁作为 IMX6ULL 开发板的主电源,原因如下:
- 电流限制:电脑 USB 口输出电流有限(通常 500mA~1A),而 IMX6ULL 核心板 + 外设(LED、蜂鸣器、传感器)工作电流约 300mA~500mA,长期使用会导致供电不足;
- 电源纹波:USB 电源纹波大(无专业稳压电路),会导致 UART 通信误码、芯片工作不稳定;
- 硬件损坏:开发板电流波动大,会反向冲击电脑 USB 端口,导致电脑 USB 口烧毁或开发板芯片复位;
- 发热严重:AMS1117 线性稳压芯片转换效率低(≈70%),大电流下损耗功率大,芯片发热严重。正确做法 :使用专用 12V/2A 电源适配器,通过开发板的 DC 电源接口供电,经开发板内部的 DCDC 模块转换为 5V/3.3V,为整个开发板提供稳定电源。
(2)信号线抗干扰设计
UART 的 TX、RX 信号线为低速模拟信号,易受电磁干扰,PCB 布线和硬件设计需遵循以下抗干扰规范:
- 布线长度:TX/RX 信号线应尽量短(建议 < 10cm),减少信号传输中的衰减和干扰;
- 布线间距:避免与电源线(5V/12V)、高频信号线(如 SPI、PWM)平行布线,间距≥20mil,减少串扰;
- 接地处理:信号线靠近地线(GND)布线,采用 "地线包裹" 方式,增强抗干扰能力;
- 硬件滤波:在 CH340 芯片的 TX/RX 引脚附近、IMX6ULL 的 UART 引脚附近,各添加 1 个 0.1μF 陶瓷去耦电容,一端接信号线,一端接地,滤除高频干扰,减少信号失真。
(3)电平严格匹配(避免 IO 口损坏 + 电平转换)
IMX6ULL 的 IO 口为 3.3V 耐压电平,不支持 5V 电平,硬件设计需严格保证电平匹配:
- CH340 配置:CH340 芯片的 VCC 引脚必须接 3.3V 电源,配置为 3.3V 电平输出模式,避免 5V 电平接入 IMX6ULL 的 UART 引脚,导致 IO 口烧毁;
- 跨电平通信:若需与 5V 电平设备(如 51 单片机)通信,需添加电平转换芯片(如 TXS0108、SN74LVC8T245),实现 3.3V 与 5V 电平的双向转换;
- RS232/RS485 通信:若需与 RS232 设备通信,需添加 MAX232/MAX3232 芯片;若需与 RS485 设备通信,需添加 MAX485/MAX3485 芯片,实现 TTL 与对应电平的双向转换。
(4)传输距离严格控制
不同电气标准的传输距离有严格限制,工程设计中需按实际需求选择,避免因距离过长导致通信失败:
- TTL 电平:开发板与外设采用 TTL 电平直连时,距离应控制在 10 米内,超过则电压衰减严重,信号失真;
- RS232 电平:点对点通信,距离控制在 30 米内,适合电脑与开发板的近距离调试;
- RS485 电平:远距离多设备组网,距离可达 1200 米,适合工业现场的传感器、执行器组网,需注意总线匹配和方向控制。
(5)共地与阻抗匹配
- 共地:所有通信设备(电脑、开发板、外设)必须共地 (GND 引脚相互连接),否则会出现电平参考点不一致,导致数据解析错误,这是串口通信的前提条件;
- RS485 阻抗匹配:RS485 总线长度≥100 米时,必须在总线两端的设备 上,在 A、B 引脚之间接 1 个 120Ω 终端电阻,匹配总线阻抗,减少信号反射,避免信号叠加导致的失真。
三、UART 核心代码编写
IMX6ULL 的 UART 开发为裸机开发 ,无操作系统封装,代码编写需严格参考《IMX6ULL 参考手册.pdf》,结合串口通信的帧格式、波特率要求,直接操作寄存器实现,核心流程为 "时钟初始化→引脚初始化→寄存器配置→收发函数实现" ,每个步骤都需贴合硬件特性,同时增加容错处理、超时机制,提升程序健壮性。
前置准备:头文件与基础函数
#include "imx6ull.h" // IMX6ULL 寄存器定义头文件
// 微秒级延时函数(基于寄存器循环,适配80MHz时钟)
void delay_us(unsigned int us)
{
unsigned int i, j;
for (i = 0; i < us; i++)
for (j = 0; j < 10; j++);
}
// 毫秒级延时函数
void delay_ms(unsigned int ms)
{
unsigned int i;
for (i = 0; i < ms; i++)
delay_us(1000);
}
1. 时钟初始化
IMX6ULL 的 UART 模块时钟来源于 IPG_CLK (系统外设时钟,默认 80MHz),是 UART 波特率计算的基准时钟(Ref Freq) ,时钟初始化的核心是使能 UART1 时钟 并配置时钟分频比,保证波特率计算精度。
核心代码
// UART1 时钟初始化:使能时钟+配置1分频,UART_CLK=IPG_CLK=80MHz
void uart1_clk_init(void)
{
// 1. 使能UART1时钟(CCM_CCGR1寄存器:bit26~27 控制UART1时钟门控,0b11=始终使能)
CCM->CCGR1 |= (3 << 26);
// 2. 配置UART时钟分频比(CCM_CSCDR1寄存器:bit16~17 为UART_CLK分频比,00=1分频)
CCM->CSCDR1 &= ~(3 << 16); // 清除原有分频配置,避免默认值干扰
CCM->CSCDR1 |= (0 << 16); // 0=1分频,UART_CLK=IPG_CLK=80MHz,无预分频损耗
// 3. 等待时钟稳定(硬件时钟切换需要时间,延时100us)
delay_us(100);
}
关键说明
- 时钟门控:IMX6ULL 的外设时钟默认关闭,需先通过 CCM_CCGR1 寄存器使能 UART1 时钟,否则后续寄存器配置无效;
- 分频比选择:1 分频是最常用、最优配置,无预分频损耗,保证波特率计算精度;若需降低功耗,可适当提高分频比(如 2 分频),但需重新计算 UBIR、UBMR 参数以匹配目标波特率;
- 时钟稳定:时钟分频配置后,需延时等待时钟稳定,避免因时钟未就绪导致的波特率发生器工作异常。
2. 引脚初始化(IO 口复用与配置)
IMX6ULL 的 IO 口为多功能引脚 (如 GPIO1_IO14 可作为普通 GPIO、UART1_TX、SPI1_SCK 等),需通过 IOMUXC(IO 多路复用控制器) 将指定引脚复用为 UART 功能,并配置引脚的电气属性(驱动能力、上下拉、斜率等),确保信号传输质量。
引脚选择
IMX6ULL_MINI_V2.2 开发板默认将 UART1 的 TX 引脚映射为 GPIO1_IO14 ,RX 引脚映射为 GPIO1_IO15(参考开发板原理图和 IMX6ULL 引脚复用表)。
核心代码
// IOMUXC引脚复用配置封装函数:mux_reg=复用寄存器地址,mux_val=复用模式值
void IOMUXC_SetPinMux(unsigned int mux_reg, unsigned int mux_val)
{
volatile unsigned int *reg = (volatile unsigned int *)mux_reg;
*reg &= ~0x1F; // 清除低5位(MUX_MODE,复用模式位)
*reg |= mux_val;// 设置目标复用模式
}
// IOMUXC引脚电气配置封装函数:config_reg=配置寄存器地址,config_val=电气配置值
void IOMUXC_SetPinConfig(unsigned int config_reg, unsigned int config_val)
{
volatile unsigned int *reg = (volatile unsigned int *)config_reg;
*reg = config_val;
}
// UART1 引脚初始化:GPIO1_IO14=UART1_TX,GPIO1_IO15=UART1_RX
void uart1_pin_init(void)
{
// 1. TX引脚(GPIO1_IO14)复用为UART1_TX
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO14_UART1_TX, 0); // 0=默认复用模式
// 电气配置:0x10B0(核心配置,保证信号传输质量)
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO14_UART1_TX, 0x10B0);
// 2. RX引脚(GPIO1_IO15)复用为UART1_RX
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO15_UART1_RX, 0);
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO15_UART1_RX, 0x10B0);
// 3. 等待引脚配置生效(延时10us)
delay_us(10);
}
配置参数 0x10B0 逐位详解(核心电气属性)
配置值 0x10B0 是 IMX6ULL 串口引脚的标准电气配置,对应 IOMUXC_SW_PAD_CTL_PAD_xxx 寄存器,共 16 位,每一位的含义和配置原因如下(贴合串口通信需求):
| 位段 | 位宽 | 数值 | 寄存器位定义 | 配置含义 | 配置原因(串口通信需求) |
|---|---|---|---|---|---|
| bit0~3 | 4 | 0 | SION | 关闭引脚输入输出短路,禁止信号自环 | 避免 TX/RX 信号自环,导致通信错误 |
| bit4~5 | 2 | 10 | DSE | 驱动能力等级 R0(最大驱动电流,≈20mA) | 保证信号线的驱动能力,减少远距离传输的信号衰减 |
| bit6 | 1 | 0 | SRE | 普通斜率(信号边沿变化速度慢) | 降低信号边沿的电磁干扰,减少串扰 |
| bit7 | 1 | 0 | ODE | 关闭开漏输出,使用推挽输出模式 | 推挽输出驱动能力更强,适合串口信号传输 |
| bit8 | 1 | 1 | PKE | 开启上下拉功能 | 保证引脚空闲时的电平稳定,避免浮空导致的随机电平 |
| bit9 | 1 | 1 | PUE | 选择上拉模式(而非下拉) | 串口空闲位为高电平,上拉保证空闲时稳定为高电平 |
| bit10~12 | 3 | 000 | PUS | 弱上拉(上拉电阻≈100kΩ) | 弱上拉既能保证电平稳定,又不会影响正常的信号传输 |
| bit13 | 1 | 1 | HYS | 开启迟滞功能(施密特触发器) | 增强引脚的抗干扰能力,过滤微弱的电平抖动 |
| bit14~15 | 2 | 00 | 保留位 | 无定义,默认置 0 | 遵循芯片手册要求 |
3. 寄存器配置(UART 功能核心)
IMX6ULL 的 UART 模块包含控制寄存器、状态寄存器、数据寄存器、波特率配置寄存器 等,需按固定顺序 配置(软件复位→帧格式配置→波特率配置→使能模块),每个寄存器的配置都需贴合串口通信的 8N1 帧格式(8 数据位 + 1 停止位 + 无校验),同时增加容错判断(如复位完成检查),避免配置失效。
核心寄存器功能预览
| 寄存器名称 | 地址标识 | 核心功能 |
|---|---|---|
| UCR1 | UARTx_UCR1 | 主控制寄存器,开启 / 关闭 UART 模块,禁用 / 使能中断等 |
| UCR2 | UARTx_UCR2 | 帧格式核心配置寄存器,配置数据位、停止位、奇偶校验、收发使能、软件复位等 |
| UCR3 | UARTx_UCR3 | 扩展控制寄存器,IMX6ULL 强制要求配置 RXDMUXSEL=1(多路复用模式) |
| UFCR | UARTx_UFCR | FIFO 控制寄存器,配置参考时钟分频比(RFDIV),与波特率计算相关 |
| USR2 | UARTx_USR2 | 状态寄存器,只读,判断收发状态(RDR = 接收就绪,TXDC = 发送完成) |
| URXD | UARTx_URXD | 接收数据寄存器,只读,存储接收到的 8 位数据 |
| UTXD | UARTx_UTXD | 发送数据寄存器,只写,写入待发送的 8 位数据,硬件自动添加帧格式 |
| UBIR/UBMR | UARTx_UBIR/UBMR | 波特率配置寄存器,分别为增量值、调制值,配合计算波特率 |
核心代码
// UART1 寄存器配置:按8N1格式配置+115200bps波特率+使能收发
void uart1_reg_init(void)
{
// 1. 软件复位UART1(UCR2->SRST位,bit0=1):复位所有寄存器为默认值
UART1->UCR2 |= (1 << 0);
// 等待复位完成(SRST位会自动清0,需检查,避免未复位完成就配置)
while ((UART1->UCR2 & (1 << 0)) != 0);
// 延时50ns,确保复位彻底(80MHz时钟下,1个时钟周期=12.5ns,4个周期=50ns)
delay_us(1);
// 2. 配置UCR2寄存器:8N1帧格式核心配置(最关键的寄存器)
UART1->UCR2 = 0; // 先清零,避免默认值干扰
UART1->UCR2 |= (1 << 5); // WS=1(bit5):8位数据位(0=7位,1=8位)
UART1->UCR2 &= ~(1 << 6); // STPB=0(bit6):1个停止位(0=1位,1=2位)
UART1->UCR2 &= ~(1 << 8); // PREN=0(bit8):禁用奇偶校验(0=禁用,1=使能)
UART1->UCR2 |= (1 << 1); // RXEN=1(bit1):使能接收器
UART1->UCR2 |= (1 << 2); // TXEN=1(bit2):使能发送器
UART1->UCR2 |= (1 << 14); // IRTS=1(bit14):忽略RTS流控(无流控场景必设)
UART1->UCR2 &= ~(1 << 15); // ESCEN=0(bit15):禁用转义序列(普通通信无需)
// 3. 配置UCR3寄存器:IMX6ULL强制要求(硬件设计决定)
UART1->UCR3 = 0;
UART1->UCR3 |= (1 << 2); // RXDMUXSEL=1(bit2):多路复用模式,不设置则无法接收数据
// 4. 配置UFCR寄存器:参考时钟分频比(RFDIV),与波特率计算相关
UART1->UFCR = 0;
UART1->UFCR &= ~(7 << 7); // 清除bit7~9(RFDIV位)
UART1->UFCR |= (0 << 7); // RFDIV=000:1分频,参考时钟=UART_CLK=80MHz
// 5. 配置波特率:115200bps,核心公式+精准计算UBIR/UBMR
// 波特率公式:BaudRate = RefFreq / (16 * ((UBMR + 1) / (UBIR + 1)))
// 已知:RefFreq=80MHz,BaudRate=115200bps,推导得:(UBMR+1)/(UBIR+1)≈43.4028
// 选择UBIR=26(UBIR+1=27),则UBMR+1=43.4028*27≈1171.87,取整UBMR=1171
// 实际波特率=80000000/(16*(1172/27))≈115104bps,误差≈0.08% < ±6%,符合要求
UART1->UBMR = 1171; // 调制值MOD=1171
UART1->UBIR = 26; // 增量值INC=26
// 6. 开启UART1模块(UCR1->UARTEN位,bit0=1):最后开启,避免配置过程中误工作
UART1->UCR1 = 0;
UART1->UCR1 |= (1 << 0);
// 7. 等待模块就绪(延时100us,确保波特率发生器、收发电路稳定)
delay_us(100);
}
关键配置细节
- 软件复位:必须先复位 UART 模块,清除寄存器默认值,否则后续配置会被默认值干扰,复位后需检查 SRST 位是否清 0,确保复位完成;
- UCR2 配置:帧格式的核心,需严格按 8N1 配置,RXEN/TXEN 必须同时使能,否则无法收发数据;
- UCR3->RXDMUXSEL:IMX6ULL 硬件强制要求设为 1,这是开发最易踩的坑!不设置则 UART 会将内部测试信号作为接收源,无法接收外部数据,通信完全失败;
- 波特率配置:UBIR/UBMR 的值需精准计算,保证波特率误差 < ±6%,上述配置误差仅 0.08%,通信稳定性极高;
- UARTEN 开启:必须最后配置,若提前开启,UART 模块会在配置过程中误工作,导致数据接收 / 发送异常。
4. 数据收发函数实现(基于帧格式,稳定可靠 + 超时处理 + 空指针保护)
收发函数的核心是 "先判断硬件状态,再操作数据寄存器" ,贴合串口通信的帧接收 / 发送逻辑,避免因硬件未就绪(如发送缓冲区未空、接收未就绪)导致的数据丢失或误发。同时增加超时处理 (避免死等)、空指针保护(避免程序崩溃),提升程序健壮性。
(1)发送函数:putc(单个字符)+ puts(字符串)
核心逻辑 :发送前先判断 USR2->TXDC 位(bit3)是否为 1(上一帧数据发送完成,发送缓冲区空),若为 0 则等待,直到就绪或超时;写入数据到 UTXD 寄存器后,硬件会自动添加起始位、停止位,按 LSB 优先顺序传输。
#define UART_TIMEOUT_US 100000 // 超时时间:100ms,避免死等
// 发送单个字符:d=待发送字符,返回0=成功,-1=超时
int putc(unsigned char d)
{
unsigned int timeout = UART_TIMEOUT_US;
// 等待发送完成(USR2->TXDC=1),超时则返回错误
while (((UART1->USR2 & (1 << 3)) == 0) && (timeout-- > 0))
{
delay_us(1);
}
// 超时判断
if (timeout == 0)
{
return -1; // 发送超时
}
// 写入待发送字符到UTXD寄存器,硬件自动添加帧格式(起始位+停止位)
UART1->UTXD = d;
return 0; // 发送成功
}
// 发送字符串:pStr=待发送字符串,返回0=成功,-1=超时,-2=空指针
int puts(const char *pStr)
{
// 空指针保护:避免传入NULL导致程序崩溃
if (pStr == NULL)
{
return -2;
}
// 遍历字符串,逐个发送字符,直到结束符'\0'
while (*pStr != '\0')
{
if (putc(*pStr++) != 0)
{
return -1; // 单个字符发送超时,整体返回超时
}
}
// 发送换行符:\r\n(回车+换行),兼容Windows/Linux串口工具
putc('\r');
putc('\n');
return 0; // 发送成功
}
(2)接收函数:getc(单个字符,阻塞式)
核心逻辑:接收前先判断 USR2->RDR 位(bit0)是否为 1(接收就绪,完整字符帧已接收并存储到 URXD 寄存器),若为 0 则等待,直到就绪或超时;读取 URXD 寄存器的低 8 位数据(有效数据),同时检查接收错误标志,清除错误。
// 接收单个字符:ch=存储接收字符的指针,返回0=成功,-1=超时,-2=空指针,-3=接收错误
int getc(unsigned char *ch)
{
unsigned int timeout = UART_TIMEOUT_US;
// 空指针保护
if (ch == NULL)
{
return -2;
}
// 等待接收就绪(USR2->RDR=1),超时则返回错误
while (((UART1->USR2 & (1 << 0)) == 0) && (timeout-- > 0))
{
delay_us(1);
}
// 超时判断
if (timeout == 0)
{
return -1; // 接收超时
}
// 检查接收错误标志(USR1寄存器:PE=奇偶校验错误,FE=帧错误,BRK=中断错误)
if (UART1->USR1 & ((1 << 0) | (1 << 1) | (1 << 2)))
{
UART1->USR1 |= ((1 << 0) | (1 << 1) | (1 << 2)); // 清除错误标志(写1清0)
return -3; // 接收错误
}
// 读取接收数据:仅保留低8位,避免高位无效数据干扰
*ch = (unsigned char)(UART1->URXD & 0XFF);
return 0; // 接收成功
}
(3)UART1 初始化总函数
将时钟初始化、引脚初始化、寄存器初始化封装为一个总函数,方便主函数调用,同时增加初始化成功验证。
// UART1 初始化总函数:整合所有初始化步骤,返回0=成功,-1=失败
int uart1_init(void)
{
// 按顺序初始化:时钟→引脚→寄存器
uart1_clk_init();
uart1_pin_init();
uart1_reg_init();
// 初始化成功验证:发送一个测试字节0x55(二进制10101010)
if (putc(0x55) != 0)
{
return -1; // 发送失败,初始化失败
}
return 0; // 初始化成功
}
四、stdio 库移植
原生 UART 仅支持单个字符 / 字符串的收发(putc/puts/getc),无法实现格式化输出(如打印数字、寄存器值、浮点型数据),移植 C 标准库 stdio 后,可直接使用 printf(格式化输出)、scanf(格式化输入)等标准函数,大幅简化调试和数据处理流程,提升开发效率。
1. 移植核心原理(底层接口映射 + 标准库依赖)
C 标准库的 stdio 库中,printf/scanf 等标准 IO 函数并非直接操作硬件,而是通过底层的系统调用接口 实现,移植的核心是将 stdio 库的底层接口与我们实现的 UART 收发函数绑定,让标准函数通过 UART 完成数据的输入输出。
核心底层接口
_write函数:printf等输出函数的底层依赖,负责将输出数据写入硬件,需重写该函数,让其调用我们的 UART 发送函数(putc);_read函数:scanf等输入函数的底层依赖,负责从硬件读取输入数据,需重写该函数,让其调用我们的 UART 接收函数(getc);raise函数:stdio 库的依赖函数,无实际功能,空实现即可,否则编译时会报 "undefined reference to 'raise'" 错误。
移植本质
重写 _write/_read 函数,实现标准库接口 → 自定义 UART 硬件接口的映射,让 stdio 库的所有 IO 函数都通过 UART 完成数据传输。
2. 移植完整步骤(细节避坑 + 完整代码 + Makefile 配置)
移植分为补充依赖函数 、修改汇编文件扩展名 、配置 Makefile 三个步骤,每个步骤都是必做项,缺一不可,否则会导致编译失败或 printf 无法使用。
步骤 1:补充必要函数
在 uart.c 文件中添加以下函数,实现底层接口映射和依赖函数补充,代码含详细注释:
#include <stdlib.h> // raise函数依赖
#include <stdio.h> // stdio库头文件
#include <unistd.h> // _write/_read函数依赖
// 满足stdio库依赖:空实现即可,无实际功能
void raise(int n)
{
(void)n; // 强制使用参数,避免编译器未使用参数警告
}
// 重写_write函数:printf底层调用,将数据通过UART发送
// fd:文件描述符,串口无文件描述符,忽略;ptr:待发送数据指针;len:待发送数据长度
// 返回值:成功发送的字节数,-1=失败
int _write(int fd, char *ptr, int len)
{
int i;
(void)fd; // 忽略文件描述符,避免编译器警告
// 逐个发送字节,直到发送完成或超时
for (i = 0; i < len; i++)
{
if (putc(ptr[i]) != 0)
{
return -1; // 单个字节发送超时,整体返回失败
}
}
return len; // 返回成功发送的字节数
}
// 重写_read函数:scanf底层调用,从UART读取输入数据
// fd:文件描述符,忽略;ptr:存储接收数据的指针;len:最大接收长度
// 返回值:成功接收的字节数,-1=失败
int _read(int fd, char *ptr, int len)
{
int i;
unsigned char ch;
(void)fd; // 忽略文件描述符
// 逐个接收字节,直到接收完成、超时或遇到换行符
for (i = 0; i < len; i++)
{
if (getc(&ch) != 0)
{
return -1; // 接收超时/错误,返回失败
}
ptr[i] = ch;
// 遇到回车(\r)或换行(\n),结束输入(兼容Windows/Linux串口工具)
if (ch == '\r' || ch == '\n')
{
ptr[i] = '\0'; // 添加字符串结束符
break;
}
}
return i; // 返回成功接收的字节数
}
步骤 2:修改汇编启动文件扩展名
裸机开发中,汇编启动文件默认后缀为.S(大写),编译器对大小写敏感,需将其改为小写.s,否则会出现 "文件未找到" 的编译错误。
- 原文件:
start.S - 修改后:
start.s - 作用:确保 Makefile 能正确识别并编译汇编启动文件,完成芯片的底层初始化(栈配置、中断向量表、跳转到 main 函数)。
步骤 3:Makefile 完整配置(关键编译选项 + 链接脚本)
Makefile 是裸机开发的编译核心,需配置交叉编译工具链、编译选项、链接脚本、目标文件清理 等,以下是适配 IMX6ULL UART 开发的完整可直接使用的 Makefile,包含 stdio 库移植的关键编译选项:
# 1. 配置交叉编译工具链(IMX6ULL 为 ARM 架构,使用 arm-linux-gnueabihf-)
CROSS_COMPILE = arm-linux-gnueabihf-
CC = $(CROSS_COMPILE)gcc
LD = $(CROSS_COMPILE)ld
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
# 2. 目标文件命名(最终生成可烧写的 bin 文件)
TARGET = uart_imx6ull
# 3. 源文件列表(所有.c和.s文件,新增文件直接添加)
SRCS = start.s main.c uart.c
# 4. 目标文件列表(自动将.c/.s转为.o)
OBJS = $(SRCS:.c=.o)
OBJS = $(OBJS:.s=.o)
# 5. 核心编译选项(关键:-nostdlib -fno-builtin 关闭标准库,-g 保留调试信息)
CFLAGS = -Wall -g -c -O2 -nostdlib -fno-builtin -march=armv7-a -mtune=cortex-a7
# -march=armv7-a:适配 IMX6ULL 的 ARMv7-A 架构
# -mtune=cortex-a7:优化 cortex-A7 内核编译
# -nostdlib -fno-builtin:关闭系统标准库,裸机开发必备
# 6. 链接选项(指定链接脚本,定义程序运行地址)
LDFLAGS = -T imx6ull.lds -g
# imx6ull.lds:链接脚本,指定代码段、数据段、栈的运行地址(IMX6ULL 运行地址为 0x87800000)
# 7. 默认目标:生成可烧写的 bin 文件
all: $(TARGET).bin
# 8. 编译:.c文件→.o文件
%.o: %.c
$(CC) $(CFLAGS) -o $@ $<
# 9. 编译:.s文件→.o文件
%.o: %.s
$(CC) $(CFLAGS) -o $@ $<
# 10. 链接:所有.o文件→elf文件
$(TARGET).elf: $(OBJS)
$(LD) $(LDFLAGS) -o $@ $^
# 11. 转换:elf文件→bin文件(烧写到开发板的最终文件)
$(TARGET).bin: $(TARGET).elf
$(OBJCOPY) -O binary $< $@
# 12. 反汇编:elf文件→反汇编文件(调试用,查看汇编代码)
$(TARGET).dis: $(TARGET).elf
$(OBJDUMP) -D $< > $@
# 13. 清理目标:删除所有编译生成的文件
clean:
rm -rf $(OBJS) $(TARGET).elf $(TARGET).bin $(TARGET).dis
配套链接脚本imx6ull.lds
链接脚本决定程序在内存中的运行位置,IMX6ULL 推荐运行地址为0x87800000,以下是最简可用的链接脚本:
SECTIONS {
. = 0x87800000; /* 程序运行的起始地址 */
.text : { /* 代码段:存放汇编、C 编译后的代码 */
start.o
*(.text)
}
.rodata : { /* 只读数据段:存放常量、字符串 */
*(.rodata)
}
.data : { /* 数据段:存放初始化的全局变量 */
*(.data)
}
.bss : { /* 未初始化数据段:存放未初始化的全局变量,由系统清零 */
__bss_start = .;
*(.bss)
__bss_end = .;
}
.stack 0x87804000 : { /* 栈段:分配 16KB 栈空间,地址 0x87804000 */
*(.stack)
}
}
五、主函数测试代码
编写main.c,实现 UART 初始化、printf格式化输出、scanf格式化输入,可直接编译烧写,验证串口通信和 stdio 库移植效果:
#include "imx6ull.h"
#include <stdio.h> // 引入标准库,使用printf/scanf
// 延时函数声明
void delay_ms(unsigned int ms);
// UART 初始化函数声明
int uart1_init(void);
int main(void)
{
char input_buf[32] = {0}; // 定义输入缓冲区
int num = 0; // 测试整型变量
// 1. 初始化 UART1(115200bps、8N1、无流控)
if (uart1_init() != 0)
{
while (1); // 初始化失败则死循环
}
// 2. printf 格式化输出测试(串口打印)
printf("=====================\r\n");
printf("IMX6ULL UART 测试程序\r\n");
printf("波特率:115200 8N1\r\n");
printf("stdio 库移植成功!\r\n");
printf("=====================\r\n");
// 3. 循环:接收用户输入并回显
while (1)
{
printf("请输入一个字符串和整数(用空格分隔):");
// scanf 接收串口输入(电脑串口助手发送数据)
scanf("%s %d", input_buf, &num);
// printf 回显输入的内容
printf("你输入的字符串:%s\r\n", input_buf);
printf("你输入的整数:%d,整数+10:%d\r\n\r\n", num, num+10);
delay_ms(500); // 轻微延时,避免打印刷屏
}
return 0;
}
// 毫秒级延时函数(简单实现,适配 80MHz 主频)
void delay_ms(unsigned int ms)
{
unsigned int i, j;
for (i = 0; i < ms; i++)
for (j = 0; j < 0x1000; j++);
}
六、头文件imx6ull.h(关键寄存器定义,极简版)
裸机开发需直接操作寄存器,以下是imx6ull.h的关键寄存器定义(仅包含 UART、CCM、IOMUXC 相关),保证代码编译通过:
#ifndef __IMX6ULL_H
#define __IMX6ULL_H
// 定义寄存器基地址
#define CCM_BASE 0x020C4000
#define IOMUXC_BASE 0x020E0000
#define UART1_BASE 0x02020000
// 定义寄存器类型:volatile 防止编译器优化,保证直接操作内存
typedef volatile unsigned int uint32_t;
// 1. CCM 时钟寄存器(仅 UART1 相关)
typedef struct {
uint32_t CCGR0;
uint32_t CCGR1; // UART1 时钟由 CCGR1 的 bit26~27 控制
uint32_t CCGR2;
uint32_t CCGR3;
uint32_t CCGR4;
uint32_t CCGR5;
uint32_t CCGR6;
uint32_t CSCDR1; // UART 时钟分频寄存器
} CCM_Type;
#define CCM ((CCM_Type *)CCM_BASE)
// 2. IOMUXC 引脚复用寄存器(仅 UART1 TX/RX 相关)
#define IOMUXC_GPIO1_IO14_UART1_TX 0x020E0068
#define IOMUXC_GPIO1_IO15_UART1_RX 0x020E006C
#define IOMUXC_GPIO1_IO14_UART1_TX_CONFIG 0x020E02F4
#define IOMUXC_GPIO1_IO15_UART1_RX_CONFIG 0x020E02F8
// 3. UART1 寄存器结构体
typedef struct {
uint32_t UCR1; // 主控制寄存器
uint32_t UCR2; // 帧格式/收发使能寄存器
uint32_t UCR3; // 扩展控制寄存器
uint32_t UCR4;
uint32_t UFCR; // FIFO/时钟分频寄存器
uint32_t USR1; // 状态寄存器1(错误标志)
uint32_t USR2; // 状态寄存器2(收发就绪)
uint32_t UESC;
uint32_t ULTCR;
uint32_t UTIMER;
uint32_t UBIR; // 波特率增量寄存器
uint32_t UBMR; // 波特率调制寄存器
uint32_t UBRC;
uint32_t UTS;
uint32_t UTXD; // 发送数据寄存器
uint32_t URXD; // 接收数据寄存器
} UART_Type;
#define UART1 ((UART_Type *)UART1_BASE)
// 引脚配置函数声明
void IOMUXC_SetPinMux(unsigned int mux_reg, unsigned int mux_val);
void IOMUXC_SetPinConfig(unsigned int config_reg, unsigned int config_val);
#endif // __IMX6ULL_H
七、编译与烧写步骤
1. 文件目录结构
uart_imx6ull/ # 项目根目录
├── start.s # 汇编启动文件(小写.s)
├── imx6ull.lds # 链接脚本
├── Makefile # 编译脚本
├── imx6ull.h # 寄存器头文件
├── uart.c # UART 核心代码+stdio 库移植
└── main.c # 测试主函数
2. 编译操作(Linux 环境,或 Windows 下的 MinGW/Cygwin)
- 进入项目根目录:
cd uart_imx6ull - 执行编译:
make- 编译成功会生成:
uart_imx6ull.bin(可烧写文件)、uart_imx6ull.elf(链接文件)、uart_imx6ull.dis(反汇编文件)
- 编译成功会生成:
- 清理编译文件:
make clean(需要重新编译时执行)
3. 烧写步骤(使用 IMX6ULL 专用烧写工具 MFGTool)
- 下载 MFGTool 工具,打开并选择 IMX6ULL 对应的烧写配置;
- 将开发板拨到USB 烧写模式,连接电脑 USB 和电源;
- 选择编译生成的
uart_imx6ull.bin,点击Start开始烧写; - 烧写完成后,将开发板拨到EMMC/NAND 启动模式,重启开发板。
八、串口调试验证
1. 串口助手配置
- 串口号:电脑设备管理器中查看(CH340 驱动安装后显示,如 COM3/COM4);
- 波特率:115200;
- 数据位:8;
- 停止位:1;
- 校验位:无;
- 流控:无;
- 换行符:CR+LF(回车 + 换行)。
2. 验证效果
开发板上电后,串口助手会立即打印以下内容:
=====================
IMX6ULL UART 测试程序
波特率:115200 8N1
stdio 库移植成功!
=====================
请输入一个字符串和整数(用空格分隔):
在串口助手的发送框输入内容(如test 123),点击发送,串口助手会回显:
你输入的字符串:test
你输入的整数:123,整数+10:133
说明 UART 收发、printf/scanf 均正常工作,stdio 库移植成功!
九、常见问题排查
1. 编译错误:undefined reference to `main'
- 原因:汇编启动文件未跳转到
main函数,或源文件未添加main.c; - 解决:检查
start.s中是否有bl main指令,Makefile 的SRCS是否包含main.c。
2. 串口无任何输出
- 原因 1:UART 初始化错误(如
UCR3->RXDMUXSEL未置 1); - 解决 1:检查
uart1_reg_init函数,确保UART1->UCR3 |= (1 << 2); - 原因 2:波特率不匹配、串口号选错;
- 解决 2:确认串口助手波特率为 115200,重新查看电脑设备管理器的串口号;
- 原因 3:开发板启动模式错误;
- 解决 3:确认开发板为EMMC/NAND 启动模式,而非 USB 烧写模式。
3. 串口打印乱码
- 原因 1:波特率计算错误(UBIR/UBMR 值不对);
- 解决 1:严格使用代码中的
UBMR=1171、UBIR=26(80MHz 主频对应 115200bps); - 原因 2:串口助手换行符未设为
CR+LF; - 解决 2:在串口助手设置中勾选
CR+LF。
4. scanf 无法接收输入
- 原因:
_read函数未正确处理回车 / 换行符,或输入缓冲区未初始化; - 解决:检查
uart.c中的_read函数,确保有ch == '\r' || ch == '\n'的判断,主函数中输入缓冲区初始化为 0。
5. 编译错误:undefined reference to `raise'
- 原因:未实现
raise函数,stdio 库依赖该函数; - 解决:在
uart.c中添加空实现的raise函数(代码中已包含,直接复制即可)。
十、进阶扩展
- 非阻塞式收发 :修改
getc/putc,去掉死循环等待,改为轮询状态位,适合实时系统; - 中断式 UART:配置 UART 收发中断,实现中断驱动的串口通信,减少 CPU 占用;
- 波特率可配置:封装波特率计算函数,支持 9600/19200/38400/115200 等多种波特率;
- 串口数据解析 :实现简单的串口指令解析(如发送
LED_ON点亮 LED,LED_OFF熄灭 LED); - 多串口通信:扩展 UART2/UART3,实现多设备的串口通信。
十一、核心知识点总结
- UART 本质是异步串行通信 ,无需时钟线,通过波特率同步,帧格式为
起始位+数据位+校验位+停止位; - IMX6ULL UART 关键坑:UCR3->RXDMUXSEL 必须置 1,否则无法接收外部数据;
- stdio 库移植核心:重写**_write/_read**函数,将标准 IO 映射到 UART 硬件收发;
- 裸机开发编译关键:使用交叉编译工具链 ,关闭标准库(
-nostdlib -fno-builtin),指定运行地址; - 串口通信必备条件:波特率、数据位、停止位、校验位四者一致,否则通信失败 / 乱码。
(1)什么是单工、半双工、全双工通信?请各举一个实际应用场景。
- 单工通信:数据只能沿一个固定方向传输,发送端与接收端角色固定,无法反向交互。应用场景:红外遥控(遥控器→电视)、广播电台(电台→收音机)、打印机(主机→打印机)。
- 半双工通信:数据可双向传输,但同一时刻只能单向进行,需切换传输方向。应用场景:对讲机(按住说话、松开接收)、RS485 无流控组网(多个传感器共享总线)。
- 全双工通信:数据可同时在两个方向传输,收发独立工作。应用场景:UART 串口调试(开发板与电脑)、手机通话、RS232 点对点通信(收、发、地三线)。
(2)串行通信和并行通信的核心区别是什么?为何嵌入式外设通信多采用串行通信?
核心区别:① 传输方式:串行按位依次传输(1~2 根数据线),并行按字节 / 字同时传输(需对应数量数据线 + 控制线);② 硬件复杂度:串行布线简单、占用 IO 少,并行布线复杂、IO 占用多;③ 抗干扰能力:串行抗干扰强(信号路径短),并行易串扰(多线并行);④ 传输距离:串行支持远距离(如 RS485 达 1200 米),并行仅适用于近距离(<1 米)。
嵌入式外设多采用串行通信的原因:① 嵌入式系统 IO 资源有限,串行通信仅需少量 IO 即可实现;② 外设多为分布式部署(如传感器),串行抗干扰强、传输距离远的特性更适配;③ 硬件成本低(少信号线、少接口芯片),符合嵌入式低成本设计需求;④ 主流外设(I2C 传感器、SPI flash、RS485 模块)均支持串行通信,兼容性强。
(3)异步通信无需时钟信号,如何保证收发双方的数据同步?波特率误差过大可能导致什么问题?
同步机制:通过 "约定通信参数 + 起始位 / 停止位同步" 实现:① 约定波特率,确保收发时钟频率一致;② 起始位(低电平)触发接收端采样;③ 接收端在每个比特中间时刻采样,避免边沿不稳定区域;④ 停止位(高电平)标识字符结束,为下一字节做准备。
波特率误差过大的后果:允许误差通常为 ±6%,若超过则会导致接收端采样错误:① 数据解析乱码;② 停止位识别失败,数据错位丢失;③ 通信完全中断。
(4)串口通信属于哪一类通信(结合异步 / 同步、串行 / 并行、单工 / 半双工 / 全双工回答)?
串口通信(以 UART 为例)属于异步、串行、全双工通信:
- 异步:无需时钟信号,通过波特率、起始位 / 停止位同步;
- 串行:数据按位依次传输,核心依赖 TX、RX 两根信号线;
- 全双工:TX 和 RX 独立工作,收发可同时进行。
(5)串口通信的电气表达有哪些?TTL、RS232、RS485 的适用场景分别是什么?
电气表达:TTL、RS232、RS485 三种标准。
适用场景:① TTL 电平(0V=0,3.3V/5V=1):板内或短距离通信(<10~20 米),如开发板内 CH340 与 IMX6ULL 通信;② RS232 电平(-3~-15V=1,+3~+15V=0):中短距离点对点全双工通信(<20~30 米),如电脑与开发板串口调试;③ RS485 电平(差分传输):远距离多设备组网(<1200 米),如工业传感器组网、楼宇自控系统。
(6)为什么 IMX6ULL 的 UART 控制寄存器 3(UCR3)的 RXDMUXSEL 位必须设为 1?不设置会有什么后果?
设置原因:IMX6ULL 的 UART 模块硬件设计为多路复用(MUXED)模式,RXDMUXSEL 位(bit2)用于选择接收数据路径:设为 1 时,接收数据从 UART 的 RX 引脚输入,符合正常通信逻辑;该位是硬件强制要求,默认值为 0,需软件主动设 1。
不设置的后果:RXDMUXSEL=0 时,UART 会将内部测试信号或其他引脚信号作为接收源,导致无法接收外部 UART 数据,或读取到随机无效数据,通信异常。
(7)波特率计算中,UBIR 和 UBMR 的作用是什么?如何选择这两个参数以减小波特率误差?
作用:UBIR(增量值)和 UBMR(调制值)用于精确配置波特率,公式:BaudRate=RefFreq/(16×((UBMR+1)/(UBIR+1)))。
减小误差的方法:① 固定参考时钟和目标波特率,推导 (UBMR+1)/(UBIR+1) 的理论值;② 选择 UBIR 值(0~65535),使 UBMR 也在 0~65535 范围内;③ 优先选择 UBIR+1 为理论值的分母因子,使 UBMR+1 接近整数;④ 遍历参数组合,选择误差最小的配置(如 80MHz 时钟、115200bps 时,UBMR=1171、UBIR=26,误差 < 0.1%)。
(8)大小端模式对嵌入式通信有什么影响?如何实现小端模式设备与大端模式设备的数据交互?
影响:多字节数据(如 int、float)的存储顺序不同,若收发设备大小端不一致,会导致数据解析错误(如小端发送 0x12345678,大端接收后解析为 0x78563412)。
交互方法 :以网络字节序(大端)为中间标准:① 发送端:本地字节序→网络字节序(16 位用htons,32 位用htonl);② 接收端:网络字节序→本地字节序(16 位用ntohs,32 位用ntohl);③ 无库函数时,通过指针 / 联合体拆分字节,按大端顺序重组(如小端转大端:(data&0xFF)<<24∣(data&0xFF00)<<8∣(data&0xFF0000)>>8∣(data&0xFF000000)>>24)。