ARM嵌入式学习(十三)--- IMX6ULL串口

目录

一、串口介绍

二、配置串口寄存器

1.选择串口引脚功能和电气属性:

2.配置三个UART控制寄存器:

UARTx_UCR1:

UARTx_UCR2:

UARTx_UCR3:

3.波特率配置:

4.读写函数:

写函数:

读函数:

注意:

5.整体代码:

三、格式化输入输出

整体的makefile代码:

四、总结


一、串口介绍

不管是单片机开发还是嵌入式 Linux 开发,串口都是最常用到的外设。可以通过串口将开发板与电脑相连,然后在电脑上通过串口调试助手来调试程序。还有很多的模块,比如蓝牙、GPS、GPRS 等都使用的串口来与主控进行通信的,在嵌入式 Linux 中一般使用串口作为控制台,所以掌握串口是必备的技能。本节我们就来学习如何驱动 I.MX6U 上的串口,并使用串口和电脑进行通信。

I.MX6U 的 UART 接口,I.MX6U 一共有 8 个 UART,其主要特性如下:

①兼容 TIA/EIA-232F 标准,速度最高可到 5Mbit/S;

②支持串行 IR 接口,兼容 IrDA(Infrared Data Association红外数据组织),最高可到 115.2Kbit/s;

③支持 9 位或者多节点模式(RS-485);

④1 或 2 位停止位;

⑤可编程的奇偶校验(奇校验和偶校验);

⑥自动波特率检测(最高支持 115.2Kbit/S)。

I.MX6U 的 UART 功能很多,但是我们本篇就只用到其最基本的串口功能 ,关于 UART 其它功能的介绍请参考《I.MX6ULL 参考手册》第 3561 页的"Chapter 55 Universal Asynchronous Receiver/Transmitter(UART)"章节。 本篇我们的目的是实现115200,8,N,1(这些数字看不懂的话先去学习51入门串口:ARM嵌入式学习(三) --- 入门51(串口)-CSDN博客)与电脑之间通信。

二、配置串口寄存器

UART 的时钟源是由寄存器 CCM_CSCDR1 的 UART_CLK_SEL(bit)位来选择的,当为 0 的时候 UART 的时钟源为 pll3_80m(80MHz,看手册的时钟树可以知道),如果为 1 的时候 UART 的时钟源为 osc_clk(24M),一般选择 pll3_80m 作为 UART 的时钟源 。寄存器 CCM_CSCDR1 的 UART_CLK_PODF(bit5:0)位是 UART 的时钟分频值,可设置 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 模块、蓝牙模块等通信时。

接下来看一下 UART 几个重要的寄存器

2.配置三个UART控制寄存器:

在手册第3618页,这里只写我们比较关心的:

UARTx_UCR1:

(1)ADBR(bit14):自动波特率检测使能位,为 0 的时候关闭自动波特率检测,为 1 的时候使能自动波特率检测;

(2)UARTEN(bit0):UART 使能位,为 0 的时候关闭 UART,为 1 的时候使能 UART。

这里我们关闭自动波特率检测,代码为:

复制代码
    UART1->UCR1 |= (1 << 0);

注意要最后配好了所有东西再使能。

UARTx_UCR2:

(1)IRTS(bit14):为 0 的时候使用 RTS 引脚功能,为 1 的时候忽略 RTS 引脚;

(2)PREN(bit8):奇偶校验使能位,为 0 的时候关闭奇偶校验,为 1 的时候使能奇偶校验;

(3)PROE(bit7):奇偶校验模式选择位,开启奇偶校验以后此位如果为 0 的话就使用偶校验,此位为 1 的话就使能奇校验。

(4)STOP(bit6):停止位数量,为 0 的话 1 位停止位,为 1 的话 2 位停止位;

(5)WS(bit5):数据位长度,为 0 的时候选择 7 位数据位,为 1 的时候选择 8 位数据位;

(6)TXEN(bit2):发送使能位,为 0 的时候关闭 UART 的发送功能,为 1 的时候打开 UART的发送功能;

(7)RXEN(bit1):接收使能位,为 0 的时候关闭 UART 的接收功能,为 1 的时候打开 UART的接收功能;

(8)SRST(bit0):软件复位,为 0 的是时候软件复位 UART,为 1 的时候表示复位完成。复位完成以后此位会自动置 1,表示复位完成。此位只能写 0,写 1 会被忽略掉。

复制代码
    UART1->UCR2 &= ~(1 << 0);
    delay_us(1);
    unsigned int tmp = UART1->UCR2;
    tmp |= (1 << 14);
    tmp &= ~(1 << 8);
    tmp &= ~(1 << 6);
    tmp |= (1 << 5);
    tmp |= (1 << 2);
    tmp |= (1 << 1);
    UART1->UCR2 = tmp;

UARTx_UCR3:

该寄存器的第二位RXDMUXSEL必须始终为1,手册规定。

(1)TXDC(bit3):发送完成标志位,为 1 的时候表明发送缓冲(TxFIFO)和移位寄存器为空,也就是发送完成,向 TxFIFO 写入数据此位就会自动清零;

(2)RDR(bit0):数据接收标志位,为 1 的时候表明至少接收到一个数据,从寄存器UARTx_URXD 读取数据接收到的数据以后此位会自动清零。

复制代码
UART1->UCR3 |= (1 << 2);

第0位和第3位的使用我们写到读写函数中去

3.波特率配置:

首先需要把UART的工作频率确定下来,PLL3时钟为480MHz,经由一个静态6分频的分频器之后为80MHz。此后UART还有一个自己的分频器,由寄存器UARTx_UFCR(bit9\~bit7决定分频值。我们需要1分频,即为b101。

接着波特率由寄存器UARTx_UBIR 和 UARTx_UBMR计算得出,计算公式为:(手册第3593页)

公式中Ref Freq为UART的实际工作频率(80MHz),RaudRate是波特率,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);
    UART1->UFCR = tmp;

    UART1->UBIR = 999;
    UART1->UBMR = 43404;

4.读写函数:

先介绍一下UARTx_USR2(UART 的状态寄存器2 ):

(1)TXDC(bit3): 发送完成标志位,为 1 的时候表明发送缓冲(TxFIFO)和移位寄存器为空,也就是发送完成,向 TxFIFO 写入数据此位就会自动清零;

(2)RDR(bit0): 数据接收标志位,为 1 的时候表明至少接收到一个数据,从寄存器UARTx_URXD 读取数据接收到的数据以后此位会自动清零

写函数:

UTXD:写数据寄存器,

复制代码
void uart_send_byte(unsigned char data)
{
    while(!(UART1->USR2 & (1 << 3))); //当第3位不为0时,跳出循环
    UART1->UTXD = data;
}

读函数:

URXD:读数据寄存器,

复制代码
unsigned char uart_recv_byte
{
    unsigned char data = 0;
    while(!(UART1->USR2 & (1 << 0))); //当第3位不为0时,跳出循环
    data = UART1->URXD & 0xff;
    return data;
}

注意:

这里为什么要使用data = UART1->URXD & 0xff;这样的操作,是因为只有低八位才是有效数据,不&0xff会读出来一些我们不需要的东西

5.整体代码:

复制代码
#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);
    tmp &= ~(1 << 8);
    tmp &= ~(1 << 6);
    tmp |= (1 << 5);
    tmp |= (1 << 2);
    tmp |= (1 << 1);
    base->UCR2 = tmp;

    base->UCR3 |= (1 << 2);

    tmp = base->UFCR;
    tmp &= ~(0x7 << 7);
    tmp |= (0x5 << 7);
    base->UFCR = tmp;

    base->UBIR = 999;
    base->UBMR = 43404;

    base->UCR1 |= (1 << 0);
}

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;
}

这里改了一下函数的参数,让我们可以选择打开任意一个串口

最后main.c:

下载随便一个串口调试助手,就可以看到我们发送出去的16进制数据会被加1再返回给我们。

三、格式化输入输出

虽然可以用来调试程序,但是功能太单一了,只能输出字符 。如果需要输出数字的时候就需要我们自己先将数字转换为字符,非常的不方便 。如果可以使用printf 函数来完成格式化输出了,那就非常方便了。本篇我们来继续学习如何将 printf 这样的格式化函数移植到 I.MX6U 开发板上。

移植的基本原理很简单,就是把printf和scanf函数映射到UART1上,之后用串口调试助手等软件就可以非常方便地和开发板之间通信了。

配套资料中的文件夹 stdio 里面的文件就是我们要移植的源码文件(需要资料请私信我),将该文件夹复制到工程目录下。stdio 里面的文件其实是从 uboot 里面移植过来的。后面学习 uboot 以后大家有兴趣的话可以自行从 uboot 源码里面"扣"出相应的文件,完成格式化函数的移植。这里要注意一点,stdio 中并没有实现完全版的格式化函数,比如 printf 函数并不支持浮点数,但是基本够我们使用了。

文件拷贝完成以后要做一下几点修改,否则编译或者链接时会出错:

1.需要在uart.c中添加一个函数:void raise(int n);函数体为空即可,否则会报错;
2.如果之前编写的project/start.s文件扩展名为小写s,则需改成大写。即start.S。对于汇编文件来说,扩展名S和s说不一样的,S会先做预处理,在进行编译而s则省略预处理步骤;
3.由于增加了新的源码和头文件,所以Makefile也需要修改。

4.之前所有引用.s的地方都需要改成.S
5.编译选项增加-Wa,-mimplicit-it=thumb --nostdlib,否则的话会有如下类似的错误提示thumb conditional instruction should be in IT block --`addcsr5,r5,#65536'

6.增加源程序路径和头文件路径,并添加链接目录

整体的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

如果报错缺少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;
}

移植好了以后,我们就可以实现一个电脑输入命令来控制开发板led和蜂鸣器的函数了。

main.c:

复制代码
#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"

void gpio1_io18_handler(void)
{
  GPIO1->DR ^= (1 << 3);
  GPIO5->DR ^= (1 << 1);
}

void epit1_irq_handler()
{
  GPIO1->DR ^= (1 << 3);
  GPIO5->DR ^= (1 << 1);
}

int main(void)
{
  char data[50] = {0};
  // system_irq_init();

  clk_init();
  led_init();
  beep_init();
  // key_irq_init(gpio1_io18_handler);
  // epit1_init(epit1_irq_handler);
  gpt1_init();
  uart_init(UART1);
  int num = 0;
  while (1)
  {
    //  scanf("%d %d\r\n", &num1, &num2);
    //  num1++;
    //  printf("%d + %d = %d\n", num1, num2, num1 + num2);
    printf("input ur command:");
    scanf("%s", data);
    // printf("\n%s\n", data);
#if 1
    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");
    }
#endif
    // uart_send_byte(UART1, data + 1);
    // delay_ms(1000);
  }

  return 0;
}

四、总结

串口通信的原理一定要搞明白,明白之后就算换了一个开发板我们也能配置

相关推荐
AOwhisky16 小时前
Python 学习笔记(第一期与第二期)——基础语法——核心知识点自测与详解
开发语言·笔记·python·学习·云原生·运维开发
STLearner17 小时前
ICML 2026 | LLM×Graph论文总结[1]【图基础模型,文本属性图,多模态属性图,图对齐,图提示学习,关系深度学习
论文阅读·人工智能·python·深度学习·学习·机器学习·数据挖掘
什巳19 小时前
JAVA练习278- 和为 K 的子数组
java·学习·算法·leetcode
碎光拾影19 小时前
CPU与MCU核心区别揭秘
单片机·学习·51单片机
我命由我1234521 小时前
复利极简理解
经验分享·笔记·学习·职场和发展·求职招聘·职场发展·学习方法
AOwhisky21 小时前
Python 学习笔记(第三期)——流程控制核心知识点自测与详解
开发语言·笔记·python·学习·云原生·运维开发·流程控制
tyqtyq2221 小时前
药品说明书解读 —— AI 安全用药助手,鸿蒙原生应用深度解析
人工智能·学习·华为·生活·harmonyos
小心亦新1 天前
STM32学习13 定时器1中断
stm32·嵌入式硬件·学习
我命由我123451 天前
72 法则极简理解
经验分享·笔记·学习·职场和发展·求职招聘·职场发展·学习方法
qq_263_tohua1 天前
第112期 CNN学习,不错的文章
学习