ARM-uart

今天学习ARM裸机下UART串口。串口是嵌入式开发最基础的调试工具,打印日志、收发指令全都靠它。以前直接用printf,没关心底层怎么实现;今天搞懂串行通信基础概念,手写UART寄存器配置,还把stdio库移植到裸机,直接能用scanf、printf,收获挺大。

一、串口通信基础概念

嵌入式通信分为并行通信和串行通信。

并行通信同一时刻传输多个比特,速度快,但是占用引脚多;串行通信把数据拆成一比特一比特,在一根线上依次传输,引脚占用少,硬件简单,UART串口就属于串行里面的异步通信。

按照数据传输方向分为三种模式:

  1. 单工:只能单向传输,一端固定发送、一端固定接收;
  2. 半双工:双向可以通信,但同一时刻只能一方发、另一方收,不能同时收发,类似对讲机;
  3. 全双工:两根数据线,收发独立,可以同时发送和接收,就像打电话,我们常用的TTL‑UART、RS232都是全双工;RS485是差分信号,属于半双工。

同步通信 、异步通信

同步通信:除数据线之外额外带一根时钟线,收发双方跟着同一个时钟节拍采样数据,例如I2C、SPI;

异步通信(UART):没有时钟线,收发双方事先约定波特率,依靠起始位、停止位来区分字节,双方本地时钟误差不能超过6%。

UART一帧数据格式(TTL电平):

空闲状态:数据线高电平;

起始位:拉低1个bit时间,代表一帧开始;

数据位:从最低位LSB开始发送;

校验位:无校验、奇校验、偶校验;

停止位:拉高,标记本字节结束。

波特率bps:每秒传输的比特数,常用:9600、115200。波特率决定每一位电平持续时间。

电气标准区别:

TTL:芯片引脚电平,3.3V/5V,传输距离很短;我们开发板CH340芯片做USB转TTL,电脑USB转成TTL电平给到IMX6ULL的UART引脚;

RS232:使用正负电压,逻辑1为‑3V‑15V,逻辑0为+3V+15V,距离最远二三十米;

RS485:差分信号A/B,依靠电压差值识别信号,抗干扰强,可以传到1200米。

二、IMX6ULL UART硬件与寄存器要点

我们板子上CH340负责USB转TTL,连接到UART1的TX、RX引脚。写裸机UART,步骤固定:

  1. 开启UART外设时钟(时钟系统前面学过,CCGR时钟门控)
  2. IO引脚复用配置:把普通GPIO复用为UART_TX、UART_RX,同时配置电气属性
  3. UART软件复位,配置控制寄存器UCR1、UCR2、UCR3
  4. 配置分频寄存器UFCR,设置波特率相关UBIR、UBMR
  5. 使能发送、接收,打开UART模块总开关

重点寄存器:

UTXD:发送寄存器,写入数据就启动发送; URXD:接收寄存器,只读,存放收到字节;

UCR1:bit0 UARTEN,UART模块总开关; UCR2:软件复位位、RX接收使能、TX发送使能、数据位长度、停止位、校验位;

UCR3:UART多路复用模式,IMX6ULL必须把bit2置1; UFCR:参考时钟分频;

UBIR / UBMR:波特率生成,波特率计算公式: BaudRate = Ref Freq/(16 \times ((UBMR + 1) / (UBIR + 1))) USR2状态寄存器:bit3 TXDC发送完成标志;bit0 RDR接收数据就绪标志。

三、UART1底层驱动代码

c 复制代码
#include "uart.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"

void uart1_init(void)
{
    //1.引脚复用:配置TX RX为UART功能
    IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX, 0);
    IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX, 0);    

    //2.配置引脚电气属性
    IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX, 0x10B0);
    IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX, 0x10B0);

    //UART软件复位
    UART1->UCR2 &= ~(1 << 0);
    unsigned int r = 0;
    r = UART1->UCR2;
    r |= (1 << 14);        //忽略RTS流控
    r &= ~(1 << 8);        //关闭奇偶校验
    r &= ~(1 << 6);        //1位停止位
    r |= (1 << 5);         //8位数据位
    r |= (1 << 2);         //TX发送使能
    r |= (1 << 1);         //RX接收使能
    UART1->UCR2 = r;

    //IMX6ULL UART必须设置RXDMUXSEL=1
    UART1->UCR3 |= (1 << 2);

    //设置参考时钟分频
    UART1->UFCR &= ~(7 << 7);
    UART1->UFCR |= (5 << 7);

    //波特率参数配置
    UART1->UBIR = 999;
    UART1->UBMR = 43402;

    //打开UART模块总使能
    UART1->UCR1 |= (1 << 0);
}

//发送单个字符
void putc(unsigned char c)
{
    //等待发送完成标志TXDC
    while((UART1->USR2 & (1 << 3)) == 0);
    UART1->UTXD = c;
}

//发送字符串
void puts(const char *s)
{
    while (*s)
    {
        putc(*s++);
    }   
    putc('\n');
}

//接收单个字符,阻塞等待
unsigned char getc(void)
{
    //等待接收数据就绪RDR
    while((UART1->USR2 & (1 << 0)) == 0);
    return UART1->URXD & 0xFF;
}

//移植stdio库必须提供空的raise函数,否则编译报错
void raise(int n){
}

裸机移植C标准库stdio要注意:

  1. 必须实现空函数void raise(int n);
  2. 启动汇编文件后缀要用大写start.S,大写S会做预处理;小写s不会预处理;
  3. Makefile需要增加stdio头文件路径、源码路径,链接时加入libgcc库。

底层写好putc、getc之后,标准库的printf、scanf内部就会调用这两个底层函数,于是裸机就可以直接使用格式化输入输出。

四、main主函数测试代码

这里复用我们之前写好的时钟、GPT定时器驱动,初始化顺序:中断控制器 → 时钟初始化 → 外设初始化,最后初始化uart1。

测试功能:串口输入两个整数,板子计算加法,把结果打印回串口工具。

c 复制代码
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "led.h"
#include "beep.h"
#include "key.h"
#include "interrupt.h"
#include "clock.h"
#include "epit.h"
#include "gpt.h"
#include "uart.h"
#include "stdio.h"

int main(void)
{
    system_interrupt_init();
    clock_init();
    led_init();
    beep_init();
    key_init();
    //epit1_init();
    gpt1_init();
    uart1_init();

    unsigned int num1 = 0, num2 = 0;
    while(1)
    {
        led_nor();
        //串口输入两个数字,板子做加法运算回显
        scanf("%d %d", &num1, &num2);
        printf("%d + %d = %d\n",num1, num2, num1 + num2);
    }
    return 0;
}

现象:打开串口调试助手,波特率和代码配置保持一致,输入两个数字按回车,开发板计算加法并打印算式结果。也可以把注释打开,直接调用getc()接收单字符,puts()输出字符串。

小结

  1. UART是异步串行通信,没有时钟线,全靠双方约定好波特率,依靠起始、停止位识别每一帧数据;和I2C/SPI这类同步通信要区分开。TTL、RS232、RS485是不同电气物理标准,逻辑电平、传输距离各不相同。
  2. IMX6ULL裸机UART开发流程:时钟 → IO复用配置 → 寄存器复位配置 → 波特率配置 → 使能收发模块,任何一步出错串口都无法正常收发。特别记住UCR3的bit2必须置1,这是IMX6ULL的坑点。
  3. putc、getc是最底层字符收发,实现这两个接口之后,就可以移植stdio,直接使用printf、scanf格式化输入输出,极大方便裸机调试。
相关推荐
@MMiL2 小时前
为什么-200 mod 4000 等于3800
单片机
AOI小白新手上路3 小时前
韦东山《ARM 架构与编程》基于I.MX6ULL 3-1 硬件知识_LED 原理图 · 学习笔记
arm开发·学习·架构
殷忆枫5 小时前
基于ESP32 的局域网全双工对讲系统
单片机·嵌入式硬件·esp32·对讲系统
硅基手札6 小时前
ROS2 全面解析 - 从架构到源码
单片机·架构·机器人
一条破秋裤8 小时前
TCP 缓冲区:用户空间 stdio 与内核 Socket 缓冲
arm开发·网络协议·tcp/ip
Molesidy9 小时前
【Embedded Development】【高级MCU篇】基于野火STM32H750XBH6_Pro开发板的高级MCU开发学习之1点亮led
stm32·单片机·学习
FPGA小徐9 小时前
【硬件实战】立创 EDA 绘制 TF/SD 卡模块电路|SPI 模式原理图 + 阻抗匹配 + PCB 布局布线全教程
单片机·嵌入式硬件·fpga开发
铅笔小新z10 小时前
【stm32】I2C 中断模式数据收发
stm32·单片机·嵌入式硬件
铅笔小新z11 小时前
【stm32】DMA
stm32·单片机·嵌入式硬件