ARM之uart

一、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 工业现场传感器组网、远距离通信

工程关键细节

  1. TTL 电平:IMX6ULL 为 3.3V 电平,51 单片机为 5V 电平,直连时需加电平转换芯片(如 TXS0108),避免 5V 电平烧毁 3.3V IO 口;
  2. RS232 电平:MAX232 芯片内置电荷泵,可将 3.3V/5V TTL 电平转换为 ±12V RS232 电平,需外接 4 个 0.1μF 电容实现电压反转;
  3. 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 跨平台,无广告,开源免费 跨平台开发、轻量调试 配置固定波特率 / 帧格式,保存会话,避免重复设置

通用使用技巧

  1. 格式选择:传输字符 / 字符串用 ASCII 格式 ,传输二进制数据(如传感器原始数据、寄存器值)用 HEX 格式,避免 ASCII 格式的乱码;
  2. 波特率 / 帧格式:必须与代码配置一致(如 115200bps、8N1),否则会出现乱码或无数据;
  3. 数据回显:开启回显功能,验证开发板的发送功能是否正常;
  4. 清空缓存:通信异常时,清空接收 / 发送缓存,避免旧数据干扰。

二、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 开发板的主电源,原因如下:

  1. 电流限制:电脑 USB 口输出电流有限(通常 500mA~1A),而 IMX6ULL 核心板 + 外设(LED、蜂鸣器、传感器)工作电流约 300mA~500mA,长期使用会导致供电不足;
  2. 电源纹波:USB 电源纹波大(无专业稳压电路),会导致 UART 通信误码、芯片工作不稳定;
  3. 硬件损坏:开发板电流波动大,会反向冲击电脑 USB 端口,导致电脑 USB 口烧毁或开发板芯片复位;
  4. 发热严重:AMS1117 线性稳压芯片转换效率低(≈70%),大电流下损耗功率大,芯片发热严重。正确做法 :使用专用 12V/2A 电源适配器,通过开发板的 DC 电源接口供电,经开发板内部的 DCDC 模块转换为 5V/3.3V,为整个开发板提供稳定电源。
(2)信号线抗干扰设计

UART 的 TX、RX 信号线为低速模拟信号,易受电磁干扰,PCB 布线和硬件设计需遵循以下抗干扰规范:

  1. 布线长度:TX/RX 信号线应尽量短(建议 < 10cm),减少信号传输中的衰减和干扰;
  2. 布线间距:避免与电源线(5V/12V)、高频信号线(如 SPI、PWM)平行布线,间距≥20mil,减少串扰;
  3. 接地处理:信号线靠近地线(GND)布线,采用 "地线包裹" 方式,增强抗干扰能力;
  4. 硬件滤波:在 CH340 芯片的 TX/RX 引脚附近、IMX6ULL 的 UART 引脚附近,各添加 1 个 0.1μF 陶瓷去耦电容,一端接信号线,一端接地,滤除高频干扰,减少信号失真。
(3)电平严格匹配(避免 IO 口损坏 + 电平转换)

IMX6ULL 的 IO 口为 3.3V 耐压电平,不支持 5V 电平,硬件设计需严格保证电平匹配:

  1. CH340 配置:CH340 芯片的 VCC 引脚必须接 3.3V 电源,配置为 3.3V 电平输出模式,避免 5V 电平接入 IMX6ULL 的 UART 引脚,导致 IO 口烧毁;
  2. 跨电平通信:若需与 5V 电平设备(如 51 单片机)通信,需添加电平转换芯片(如 TXS0108、SN74LVC8T245),实现 3.3V 与 5V 电平的双向转换;
  3. RS232/RS485 通信:若需与 RS232 设备通信,需添加 MAX232/MAX3232 芯片;若需与 RS485 设备通信,需添加 MAX485/MAX3485 芯片,实现 TTL 与对应电平的双向转换。
(4)传输距离严格控制

不同电气标准的传输距离有严格限制,工程设计中需按实际需求选择,避免因距离过长导致通信失败:

  1. TTL 电平:开发板与外设采用 TTL 电平直连时,距离应控制在 10 米内,超过则电压衰减严重,信号失真;
  2. RS232 电平:点对点通信,距离控制在 30 米内,适合电脑与开发板的近距离调试;
  3. RS485 电平:远距离多设备组网,距离可达 1200 米,适合工业现场的传感器、执行器组网,需注意总线匹配和方向控制。
(5)共地与阻抗匹配
  1. 共地:所有通信设备(电脑、开发板、外设)必须共地 (GND 引脚相互连接),否则会出现电平参考点不一致,导致数据解析错误,这是串口通信的前提条件
  2. 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);
}
关键说明
  1. 时钟门控:IMX6ULL 的外设时钟默认关闭,需先通过 CCM_CCGR1 寄存器使能 UART1 时钟,否则后续寄存器配置无效;
  2. 分频比选择:1 分频是最常用、最优配置,无预分频损耗,保证波特率计算精度;若需降低功耗,可适当提高分频比(如 2 分频),但需重新计算 UBIR、UBMR 参数以匹配目标波特率;
  3. 时钟稳定:时钟分频配置后,需延时等待时钟稳定,避免因时钟未就绪导致的波特率发生器工作异常。

2. 引脚初始化(IO 口复用与配置)

IMX6ULL 的 IO 口为多功能引脚 (如 GPIO1_IO14 可作为普通 GPIO、UART1_TX、SPI1_SCK 等),需通过 IOMUXC(IO 多路复用控制器) 将指定引脚复用为 UART 功能,并配置引脚的电气属性(驱动能力、上下拉、斜率等),确保信号传输质量。

引脚选择

IMX6ULL_MINI_V2.2 开发板默认将 UART1 的 TX 引脚映射为 GPIO1_IO14RX 引脚映射为 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);
}
关键配置细节
  1. 软件复位:必须先复位 UART 模块,清除寄存器默认值,否则后续配置会被默认值干扰,复位后需检查 SRST 位是否清 0,确保复位完成;
  2. UCR2 配置:帧格式的核心,需严格按 8N1 配置,RXEN/TXEN 必须同时使能,否则无法收发数据;
  3. UCR3->RXDMUXSEL:IMX6ULL 硬件强制要求设为 1,这是开发最易踩的坑!不设置则 UART 会将内部测试信号作为接收源,无法接收外部数据,通信完全失败;
  4. 波特率配置:UBIR/UBMR 的值需精准计算,保证波特率误差 < ±6%,上述配置误差仅 0.08%,通信稳定性极高;
  5. 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)

  1. 进入项目根目录:cd uart_imx6ull
  2. 执行编译:make
    • 编译成功会生成:uart_imx6ull.bin(可烧写文件)、uart_imx6ull.elf(链接文件)、uart_imx6ull.dis(反汇编文件)
  3. 清理编译文件:make clean(需要重新编译时执行)

3. 烧写步骤(使用 IMX6ULL 专用烧写工具 MFGTool)

  1. 下载 MFGTool 工具,打开并选择 IMX6ULL 对应的烧写配置;
  2. 将开发板拨到USB 烧写模式,连接电脑 USB 和电源;
  3. 选择编译生成的uart_imx6ull.bin,点击Start开始烧写;
  4. 烧写完成后,将开发板拨到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函数(代码中已包含,直接复制即可)。

十、进阶扩展

  1. 非阻塞式收发 :修改getc/putc,去掉死循环等待,改为轮询状态位,适合实时系统;
  2. 中断式 UART:配置 UART 收发中断,实现中断驱动的串口通信,减少 CPU 占用;
  3. 波特率可配置:封装波特率计算函数,支持 9600/19200/38400/115200 等多种波特率;
  4. 串口数据解析 :实现简单的串口指令解析(如发送LED_ON点亮 LED,LED_OFF熄灭 LED);
  5. 多串口通信:扩展 UART2/UART3,实现多设备的串口通信。

十一、核心知识点总结

  1. UART 本质是异步串行通信 ,无需时钟线,通过波特率同步,帧格式为起始位+数据位+校验位+停止位
  2. IMX6ULL UART 关键坑:UCR3->RXDMUXSEL 必须置 1,否则无法接收外部数据;
  3. stdio 库移植核心:重写**_write/_read**函数,将标准 IO 映射到 UART 硬件收发;
  4. 裸机开发编译关键:使用交叉编译工具链 ,关闭标准库(-nostdlib -fno-builtin),指定运行地址;
  5. 串口通信必备条件:波特率、数据位、停止位、校验位四者一致,否则通信失败 / 乱码。
(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)。

相关推荐
S火星人S2 小时前
软件调试基础(四【断点和单步执行】4.4【实模式调试器例析】)
stm32·单片机·嵌入式硬件
杜子不疼.3 小时前
【Linux】基础IO(三):文件描述符与重定向
linux·c语言·开发语言·人工智能
猫猫的小茶馆5 小时前
【Linux 驱动开发】七. 中断下半部
linux·arm开发·驱动开发·stm32·嵌入式硬件·mcu
沃尔特。13 小时前
直流无刷电机FOC控制算法
c语言·stm32·嵌入式硬件·算法
CW32生态社区13 小时前
CW32L012的PID温度控制——算法基础
单片机·嵌入式硬件·算法·pid·cw32
麒qiqi13 小时前
嵌入式 I2C 通信全解析:从硬件原理到驱动实现
stm32·单片机·嵌入式硬件
蓬荜生灰15 小时前
STM32(6)-- GPIO外设
单片机·嵌入式硬件
2501_9277730715 小时前
嵌入式——交叉编译
arm开发
轻微的风格艾丝凡16 小时前
C语言内联函数(inline)与宏函数(#define)技术文档
c语言