十二.UART串口通讯

一个嵌入式设备,串口基本上就是最常用到的外设了,通过串口可以将开发板和电脑连接,也有很多外设是通过串口来进行数据交互的。今天就来搞一下I.MX6UL的串口通讯,实现和电脑通讯的效果。

UART接口

I.MX6UL的串口外设叫做UART(Universal Asynchronous Receiver/Trasmitter),即异步串行收发器。UART作为串口的一种,其工作原理也是将数据位一帧一帧的进行传输,数据的发送和接收共用一条线缆,所以UART接口与外设相连的时候最少需要3根线:TXD、RXD和GND。下面的图是UART的通讯格式

起始位(StartBit)是一个逻辑0

空闲位:在起始位前面的状态为逻辑1,表示没有数据,空闲中。

数据位:实际要传输的数据,一般是按照字节阐述,一个字节8位,低位在前先传输,高位在后面传输。

停止位(StopBit)逻辑1,位数可以是1、1.5或2个bit。

UART电平标准

UART一般接口电平有TTL和RS232电平,开发板上的TXD和RXD对应的就是TTL电平,使用低电平表示0,高电平表示1;而DB9接口就是对应的RS232接口,用-3~-15V表示逻辑1,+3~+15V表示逻辑0,采用差分线连接。在使用RS232时候一定要注意接口电平,不要烧毁外设。

由于现在电脑上基本都不带COM口,而在写单片机什么的需要串口,这就需要一个USB转TTL电平的芯片。最常用的就是CH340。比如Arduino(nano版)的的背后就有个CH340C。用过这个芯片和USB连接就可以实现串口功能(很多USB转232的设备就是用的这个芯片)。

I.MX6UL的UART接口

I.MX6UL提供了8组UART接口,结构体如下:

 

 具备如下特点:

  • 兼容TIA/EIA-232-F标准,最高速率5.0M/s
  • 支持串行IR接口,兼容IrDA,最高速率115200bps
  • 支持9位或多点的RS-485模式
  • 232可以选择7或8位的字符格式,485模式9bit格式
  • 停止位1或2个bit
  • 可编程的奇偶校验
  • 最高到115200bit/s自动波特率检测
  • 等等等等,太多了

IMX.6UL的UART的功能有非常多,我们这里只用做最基础的串口通讯功能,具体的实际作用参考手册Chapter 55给了非常详细的介绍。

主要寄存器

UART相关寄存器也比较多,因为Soc一共有8组UART,这里截取;一组的寄存器映射

 

 但是要注意的是,虽然8组UART里各个寄存器功能序列是一样的,但是这个内存映射不是从UART1开始的,而是从UART7开始的。

下面看看几个我们要用到的寄存器

UARTx_URXD

接收数据寄存器UART Receiver Register,寄存器结构如下:

 

 

 寄存器全部为只读,我们主要用到就是最后的低8位,用来存储接收的数据。另外,bit[10]=1时可以在RS485模式下,数据结构为9bit时保存第九个bit的数据

UARTx_UTXD

发送数据寄存器UART Transmitter Register,用来存放待发送的数据。

 

 

 寄存器低8位有效,在7bit数据结构下,bit[7]可以忽略,如果想要将数据写入该寄存器,需要确认TRDY(UARTx_UCR1[13])必须为高电平,即当前没有数据被发送。

UARTx_UCR1

控制寄存器1(UART Control Register 1)UART提供了4组控制寄存器用来对其进行功能设置,首先是UCR1,先看寄存器结构

 

 

ADEN(bit[15])Automatic Baud Rate Detection Interrupt Enable,自动波特率侦测中断使能 ,允许ADET标志位(UARTx_USR2 bit[15])触发中断

ADBR(bit[14])Automatic Detection of Baud Rate,自动检测波特率使能,大概意思就是当该位值为1且ADET被清除时,接收器通过接收一个字符A或者a,对比其ASCII码为0x41或0x61,去确认合适的比特率。

TRDYEN(bit[13])Transmitter Ready Interrupt Enable,数据发送准备中断使能

IDEN(bit[12])Idle Condition Detected Interrupt Enable,一个什么中断使能,这个暂时没搞懂,暂时应该也用不上

中间几个中断使能就不说了,最后一个就是UART的使能UARTEN(bit[0]),整个寄存器我们暂时应该也就是能用到这一个bit。(自动获取波特率只能到115200,先关闭不用)

UARTx_UCR2
控制寄存器2,寄存器结构如下

 

 

 手册上有很详细的解释,这里只说一下需要用到的几个

IRTS(bit[14])Ignore RTS Pin,1时忽略RTS引脚,我们在使用TTL电平串口信号时只用到RXD和TXD,RTS和CTS一般是不使用的,设置为1即可。

PREN(bit[8])Parity Enable,校验使能,1时使能校验功能

PROE(bit[7])Parity Odd/Even,校验方式:1为奇校验,0为偶校验

STPB(bit[6])停止位,0时停止位1bit,1时2bit

WS(bit[5])Word Size,数据位长度,0时7bit,1时8bit(该长度不包含起始、结束及校验位)

TXEN(bit[2])Transmitter Enable,发送数据使能,1时使能

RXEN(bit[1])Receiver Enable,接收数据使能

SRET(bit[0])Software Reset,软件复位,写0时对FIFO,USR1,USR2,UBIR,UBMR,UBRC,URXD,UTXD和UTS[6:3]进行复位,但复位前保留4个时钟周期用来进行其他的操作。复位后该位自动置1。

UARTx_UCR3

控制寄存器3

 

 这个我们只用到了一个RXDMUXSEL,因为手册上说了这个应给被置1

 

其他的位我们暂时也都用不到。

UARTx_UFCR

缓存控制寄存器UART FIFO Control Register,这里我们主要用来设置分频器

 

RFDIV(bit[9:7])里定义了从CCM过来的时钟的分频

 

 注意这个分频不是按照数值+1的模式进行分频的,看具体的值,这个分频器决定的UART的参考时钟

UARTx_USR2

状态寄存器1我们也用不到,这里要用到状态寄存器2

 

ADET(bit[15])Automatic Baud Rate Detect Complete,波特率检测完毕,当1时接收到合适的A或者a字符,需要写1清除状态

TXFE(bit[14])Transmit Buffer FIFO Empty,发送缓存状态,1时表示缓存区为空

TXDC(bit[3])Transmitter Complete,发送完成标志位,1时表示发送数据完成,发送寄存器或发送缓存写入数据,该位自动清零

RDR(bit[0])Receive Data Ready,数据接收标志位,为1时表示至少还有1个数据要接收

UARTx_UBIR和UARTx_UBMR

用来凑波特率的两个寄存器,参考手册第55.5章节介绍了波特率的计算方法

 

 RefFreq就是经过分频后的参考时钟,比如我们时钟为80MHz,分频为1分频,想要用115200的波特率,就要自己凑了,正点原子给出的数据是UBMR=3124,UBIR=71,那么

 

其实NGP给了个函数,可以根据我们需要的波特率计算出对应的参数。

UART使用

使用UART的流程和其他的外设差不多也是先初始化、再使用

时钟源设置

有一点要注意:修改时钟树对应的时钟源,UART和其他的外设用到的不是一个时钟源,我们前面的用到的都是IPG_CLK,UART用到的的是UART_CLK_ROOT

 

我们需要通过CSCDR1选择6分频的pll3(480MHz),也就是80MHz,后面分频器为1分频。根据手册可以查出,UART_CLK_SEL为bit[6],值应为1,分频器UART_CLK_PODR对应bit[5:0],对应2^6+1分频,1分频值为0。

所以要修改我们的clk初始化函数clk_init

/*--------------------------UART_CLK设置--------------------------*/
    /*UART_CLK_ROOT主频设置为80MHz*/
    CCM->CSCDR1 &= ~(1<<6);                //CSCDR1[UART_CLK_SEL](bit[6])=0,时钟源80MHz
    CCM->CSCDR1 &= ~(7<<0);                //CSCDR1[UART_CLK_PODF](bit[5:0])设置为0,对应1分频
/*-------------------------UART_CLK设置完毕------------------------*/

这步一定要记得!否则波特率就乱了!我在调试的时候就是忘了这一步!

UART初始化

UART的初始化包括IO的复用设置、UART参数设置、波特率设置。主要就是设置UCR1、UCR2、UCR3、UFCR、UBIR、UBMR几个寄存器。在设置寄存器值时,应该按照下面的顺序

  • 关闭串口功能(UARTEN=0)
  • 复位UART(SRET=0),复位时等待SRET为1,即复位完毕
  • 设置相关寄存器的值
  • 使能UART

配置寄存器的过程如下:

    /*配置UART1*/
    UART1->UCR1 = 0;
    // UART1->UCR1 &= ~(1<<14);

    /*配置UCR2*/
    UART1->UCR2 = 0;                                 //清除UCR0
    UART1->UCR2 |= (1<<1) |(1<<2) |(1<<5)|(1<<14);   //从左起:RXEN=1 TXEN=1 WS=1 IRTS=1
                                                     //接收、发送使能、数据长度为8bit 忽略RTS引脚
    /*配置UCR3*/
    UART1->UCR3 |= (1<<2);                           //RXDMUXSEL=1

    //波特率设置115200
    UART1->UFCR &= ~(7<<7);                          //RFDIV进行清零
    UART1->UFCR = 5<<7;                              //设置1分频,uart_clk=80MHz

    UART1->UBIR = 71;
    UART1->UBMR = 3124;

其实还是比较简单的。

其他的几个关闭、使能等函数放在最后。

数据接收、发送

数据的发送、接收就是对URXD、UTXD的低8位进行操作

/**
 * @brief           通过UART1发送1个字符
 * 
 * @param c         待发送的字符
 */
void putc(unsigned char c)
{
    while(((UART1->USR2 >>3) & 0x01) == 0);     //等待前一个发送流程完毕
    UART1->UTXD = (c & 0xFF);
}

/*通过UART1接收一个字符*/
unsigned char getc(void)
{
    while(((UART1->USR2)&0x01) == 0);          //等待前一个接收流程完毕
    return UART1->URXD;
}

/**
 * @brief           发送字符串
 * 
 * @param str       待发送的字符串 
 */
void puts(unsigned *str)
{
    char *p = str;
    while(*p){
        putc(*p++);
    }
}

这样就完成了所有的功能定义。

文件结构:

UART功能的文件结构和其他外设一样

 

 两个文件如下:

/**
 * @file bsp_uart.c
 * @author your name (you@domain.com)
 * @brief uart功能定义
 * @version 0.1
 * @date 2022-01-17
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#include "bsp_uart.h"

//初始化uart1,波特率固定为115200
void uart_init(void)
{
    uart_io_init();             //IO初始化
    uart_disable(UART1);        //关闭串口
    uart_softreset(UART1);      //复位UART1

    /*配置UART1*/
    UART1->UCR1 = 0;
    // UART1->UCR1 &= ~(1<<14);

    /*配置UCR2*/
    UART1->UCR2 = 0;                                 //清除UCR0
    UART1->UCR2 |= (1<<1) |(1<<2) |(1<<5)|(1<<14);   //从左起:RXEN=1 TXEN=1 WS=1 IRTS=1
                                                     //接收、发送使能、数据长度为8bit 忽略RTS引脚
    /*配置UCR3*/
    UART1->UCR3 |= (1<<2);                           //RXDMUXSEL=1

    //波特率设置115200
    UART1->UFCR &= ~(7<<7);                          //RFDIV进行清零
    UART1->UFCR = 5<<7;                              //设置1分频,uart_clk=80MHz

    UART1->UBIR = 71;
    UART1->UBMR = 3124;

    uart_enable(UART1);         //使能UART1
}

/**
 * @brief IO初始化为UART
 * 
 */
void uart_io_init(void)
{   
    IOMUXC_SetPinMux(IOMUXC_UART1_TX_DATA_UART1_TX,0);//复用为UART1_TX 
    IOMUXC_SetPinConfig(IOMUXC_UART1_TX_DATA_UART1_TX,0x10b0);

    IOMUXC_SetPinMux(IOMUXC_UART1_RX_DATA_UART1_RX,0);
    IOMUXC_SetPinConfig(IOMUXC_UART1_RX_DATA_UART1_RX,0x10b0);
}


/**
 * @brief           关闭UART串口
 * 
 * @param base      UART结构体
 */
void uart_disable(UART_Type *base)
{
    base->UCR1 &= (1<<0);
}


/**
 * @brief           使能UART串口
 * 
 * @param base      UART结构体
 */
void uart_enable(UART_Type *base)
{
    base->UCR1 |= (1<<0);
}


/**
 * @brief           UART软复位
 * 
 * @param base      UART结构体
 */
void uart_softreset(UART_Type *base)
{
    base->UCR2 &= ~(1<<0);                     //SRET=0
    while((base->UCR2 & 0x01) == 0 );          //复位完毕,SRET=1
}


/**
 * @brief           通过UART1发送1个字符
 * 
 * @param c         待发送的字符
 */
void putc(unsigned char c)
{
    while(((UART1->USR2 >>3) & 0x01) == 0);     //等待前一个发送流程完毕
    UART1->UTXD = (c & 0xFF);
}

/*通过UART1接收一个字符*/
unsigned char getc(void)
{
    while(((UART1->USR2)&0x01) == 0);          //等待前一个接收流程完毕
    return UART1->URXD;
}

/**
 * @brief           发送字符串
 * 
 * @param str       待发送的字符串 
 */
void puts(unsigned *str)
{
    char *p = str;
    while(*p){
        putc(*p++);
    }
}
bsp_uart.c

头文件

/**
 * @file bsp_uart.h
 * @author your name (you@domain.com)
 * @brief uart头文件
 * @version 0.1
 * @date 2022-01-17
 * 
 * @copyright Copyright (c) 2022
 * 
 */
#ifndef __BSP_UART_H
#define __BSP_UART_H

#include "imx6ul.h"


void uart_io_init(void);
void uart_disable(UART_Type *base);
void uart_enable(UART_Type *base);
void uart_softreset(UART_Type *base);

void putc(unsigned char c);
unsigned char getc(void);
void puts(unsigned *str);
#endif
bsp_uart.h

在main函数里导入头文件以后,调用函数

int_init();
imx6u_clkinit();
clk_enable();
uart_init();

while(1)
{   
    puts("input a char");
    a=getc();
    putc(a);
    puts("\r\n");
    puts("your input is:");
    putc(a);
    puts("\r\n");
}

就可以使用串口实现数据交互了。

PC上运行SecureCRT,使用串口连接,Soc从PC串口接收一个字符,然后返回给PC,就是这么个效果。

波特率计算

前面我们已经实现了数据的通讯,但是波特率是固定在115200,并且波特率的计算也是我们凑出来了,可以如果我们需要9600的比特率,还要在凑半天。NXP给我们的SDK包里提供了一个函数,可以直接设置对应的寄存器,这个函数可以直接调用

/**
 * @brief                   设置比特率(官方代码)
 * 
 * @param base              UART结构特
 * @param baudrate          要设置的比特率
 * @param srcclock_hz       
 */
void uart_setbaudrate(UART_Type *base, unsigned int baudrate, unsigned int srcclock_hz)
{
    uint32_t numerator = 0u;        //分子
    uint32_t denominator = 0U;        //分母
    uint32_t divisor = 0U;
    uint32_t refFreqDiv = 0U;
    uint32_t divider = 1U;
    uint64_t baudDiff = 0U;
    uint64_t tempNumerator = 0U;
    uint32_t tempDenominator = 0u;

    /* get the approximately maximum divisor */
    numerator = srcclock_hz;
    denominator = baudrate << 4;
    divisor = 1;

    while (denominator != 0)
    {
        divisor = denominator;
        denominator = numerator % denominator;
        numerator = divisor;
    }

    numerator = srcclock_hz / divisor;
    denominator = (baudrate << 4) / divisor;

    /* numerator ranges from 1 ~ 7 * 64k */
    /* denominator ranges from 1 ~ 64k */
    if ((numerator > (UART_UBIR_INC_MASK * 7)) || (denominator > UART_UBIR_INC_MASK))
    {
        uint32_t m = (numerator - 1) / (UART_UBIR_INC_MASK * 7) + 1;
        uint32_t n = (denominator - 1) / UART_UBIR_INC_MASK + 1;
        uint32_t max = m > n ? m : n;
        numerator /= max;
        denominator /= max;
        if (0 == numerator)
        {
            numerator = 1;
        }
        if (0 == denominator)
        {
            denominator = 1;
        }
    }
    divider = (numerator - 1) / UART_UBIR_INC_MASK + 1;

    switch (divider)
    {
        case 1:
            refFreqDiv = 0x05;
            break;
        case 2:
            refFreqDiv = 0x04;
            break;
        case 3:
            refFreqDiv = 0x03;
            break;
        case 4:
            refFreqDiv = 0x02;
            break;
        case 5:
            refFreqDiv = 0x01;
            break;
        case 6:
            refFreqDiv = 0x00;
            break;
        case 7:
            refFreqDiv = 0x06;
            break;
        default:
            refFreqDiv = 0x05;
            break;
    }
    /* Compare the difference between baudRate_Bps and calculated baud rate.
     * Baud Rate = Ref Freq / (16 * (UBMR + 1)/(UBIR+1)).
     * baudDiff = (srcClock_Hz/divider)/( 16 * ((numerator / divider)/ denominator).
     */
    tempNumerator = srcclock_hz;
    tempDenominator = (numerator << 4);
    divisor = 1;
    /* get the approximately maximum divisor */
    while (tempDenominator != 0)
    {
        divisor = tempDenominator;
        tempDenominator = tempNumerator % tempDenominator;
        tempNumerator = divisor;
    }
    tempNumerator = srcclock_hz / divisor;
    tempDenominator = (numerator << 4) / divisor;
    baudDiff = (tempNumerator * denominator) / tempDenominator;
    baudDiff = (baudDiff >= baudrate) ? (baudDiff - baudrate) : (baudrate - baudDiff);

    if (baudDiff < (baudrate / 100) * 3)
    {
        base->UFCR &= ~UART_UFCR_RFDIV_MASK;
        base->UFCR |= UART_UFCR_RFDIV(refFreqDiv);
        base->UBIR = UART_UBIR_INC(denominator - 1); //要先写UBIR寄存器,然后在写UBMR寄存器,3592页 
        base->UBMR = UART_UBMR_MOD(numerator / divider - 1);
    }
}

 

make的事项

在导入上面自动设置波特率的函数以后,在make的时候会报错

错误提示是变量未定义,原因是我们调用uart_setbaudrate这个函数时候需要进行除法运算,而ARM没有除法运算的硬件结构,进行除法运算需要借助软件编译器,软浮点的实现是在一个叫做libgcc.a的库中。这个库需要我们在编译的时候指定。因为我直接用到树莓派自带的交叉编译器,库的地址可以在/lib路径下搜一下:

 

 

 教程用的交叉编译器版本是4.9.4,我用的是8.3

 

暂时还没出现什么问题, 记录下libgcc.a的路径,添加在makefile中

 

 1 CC                := $(CROSS_COMPILE)gcc
 2 LD                 := $(CROSS_COMPILE)ld
 3 OBJCOPY            := $(CROSS_COMPILE)objcopy 
 4 OBJDUMP            := $(CROSS_COMPILE)objdump 
 5 
 6 LIBPATH            := -lgcc -L /lib/gcc/arm-linux-gnueabihf/8   #制定依赖库路径
 7 
 8 $(TARGET).bin : $(OBJS)
 9 
10     $(LD) -Timx6ul.lds -o $(TARGET).elf $^ $(LIBPATH)         #将所有依赖文件链接,生成.elf文件
11     $(OBJCOPY) -O binary -S $(TARGET).elf $@                #将elf转换为依赖的目标集合
12     $(OBJDUMP) -D -m arm $(TARGET).elf > $(TARGET).dis        #将elf文件反汇编

要对原先的通用Makefile进行修改:

  • 添加第6行,通过一个变量指定依赖的路径
  • 修改第4行,在链接的时候引用变量

修改完了make一下,看到会报一个错!

 

原因是我们定义的putc和puts两个函数和libgcc.a库里的原生的函数重名了。要解决这个问题还是修改Makefile文件

1 # 静态模式 <Targets...>:<tatgets-pattern>:<prereq-patterns...>下面两天为自写
2 $(SOBJS) :    obj/%.o    :    %.s  #将所有的.s文件编译成.o文件放在obj文件夹内
3     $(CC) -Wall -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $<
4 
5 $(COBJS) : obj/%.o : %.c 
6     $(CC) -Wall -nostdlib -fno-builtin -c -O2  $(INCLUDE) -o $@ $<

在第3、6行加上参数-fno-builtin,意思是不调用C语言的内建函数。这样调用的函数就是我们自己定义的函数了。

make以后还是有个错误

 

 通过提示大概意思就是要定义一个异常处理的函数raise,对应的idiv0我觉得意思是当除数为0时候的异常处理。我们定义一个空函数就可以了。

void raise(int sig_nr) 
{

}

在头文件里声明,搞定!

printf格式化函数的移植

我们前面的串口驱动,只能发送一般的字符,如果需要输出数字的时候还要将数字转换为字符,很不方便。一般很常用的方法就是把printf函数映射到串口上,那样就可以直接使用printf函数来完成格式化输出了。

库移植

将教程提供的stdio文件夹复制到项目根目录下,修改Makefile文件

 

 从文件名称就可以看出来,目录下include文件夹里的是头文件,lib里是源代码,将该路径添加到Makefile里。进行make。

函数调用

导入这个库以后就可以直接使用格式化输入和输出了

    int a,b;
    while(1)
    { 
        printf("请输入两个值,用空格隔开");
        scanf("%d %d",&a,&b);
        printf("\r\n 数据%d+%d=%d\r\n",a,b,a+b);
    }

上面的代码是在main函数中的,前面初始化串口、时钟什么的我没有截取,主要就是看一下怎么使用两个函数。但是要注意一点:被移植的printf不支持浮点类运算!!!

这里跟教程有些区别:

前面说过,正点原子提供的教程上使用的交叉编译器什4.9.4,而我用到时8.3,我对照在X86架构下使用4.9.4在make的时候会报错:

 

 错误信息thumb conditional instruction should be in IT block -- `addcs r5,r5,#65536',这个指令集错误我没有找到出处,解决办法是在编译C文件时候加上一个参数:Wa,-mimplicit-it=thumb(百度上直接给的方案,没有找到具体的解决流程和原因)

修改后的Makefile

# 静态模式 <Targets...>:<tatgets-pattern>:<prereq-patterns...>下面两天为自写
$(SOBJS) :    obj/%.o    :    %.s  #将所有的.s文件编译成.o文件放在obj文件夹内
    $(CC) -Wall -nostdlib -fno-builtin -c -O2 $(INCLUDE) -o $@ $<

$(COBJS) : obj/%.o : %.c 
    $(CC) -Wall -Wa,-mimplicit-it=thumb -nostdlib -fno-builtin -c -O2  $(INCLUDE) -o $@ $<

烧录sd卡就行了。

调试时候的BUG

在最后调试的时候出现了一个大BUG,这里记录一下吧,免得以后忘了!

开始怎么也没搞清,现象就是添加玩stdio库以后make能成功,但是用imxdownload下载一直报错

 

先后试过教程提供的源码,更换了交叉编译器,重新编译了下载软件一直都不行,按理说报dd错误是磁盘写入失败,烧录前面的所有程序都可以,在后来发现只要写入到文件size没有超过10000Bytes都正常,就没想过是卡的问题。直到发现烧录完以前的程序发现上电后初始化非常慢,想到卡可能出问题了,用fdisk格式化失败,Ubuntu下使用好几个磁盘工具格式化都报错,换了个读卡器也不行。没办法找了个win10的PC,格式化了一下,还是不行,用Imager烧录了个树莓派的镜像,没问题,回来重新烧录一遍,竟然好了!估计是最近经常用读卡器是不是有什么问题了。

posted @ 2022-01-18 14:38  银色的音色  阅读(2048)  评论(0编辑  收藏  举报