8_UART串口编程

文章目录[隐藏]

第八章 UART串口编程

8.1 UART介绍

8.1.1 UART串口简介

​ UART全称是通用异步收发传输器(Universal Asynchronous Receiver/Transmitter)。串口顾名思义数据串行接口,即数据的传输是一位接一位传输,属于一种串行的数据总线,属于异步通讯,同时支持全双工数据传输(全双工数据传输:允许发送数据和接收数据在同一时刻发生) 。

​ 除了UART,另外还有一种叫USART,全称是通用同步/异步串行接收/发送器(Universal Synchronous/Asynchronous Receiver/Transmitter),USART比 UART多了同步通信功能,但是百分之90的工程应用中不会应用该同步功能,都是将USART当做UART使用,即采取异步串行通讯。一般开发板或者产品中都会将UART串口标为serial或COM。

8.1.2 UART硬件连接

​ 1) UART串口最精简的连接是TTL电平三线连接

​ UARTx_TXD:用于发送数据,应连接到接收设备的UARTx_RXD引脚上;

​ UARTx_RXD:用于接收数据,应连接到发送设备的UARTx_TXD引脚上;

​ GND:为双方提供一个相同的参考电平。

​ 上图为UART串口TTL电平硬件连接,此时使用标准的TTL电平来表示数据,高电平表示1,低电平表示0,标准TTL输入高电平最小2V,输出高电平最小2.4V,典型值为3.4V,输入低电平最大0.8V,输出低电平最大0.4V,典型值为0.2V。

​ 直接采用TTL电平进行串行通讯,由于其抗干扰能力差,导致传输距离短,且容易出现数据不可靠的情况。

​ 为提高抗干扰能力和传输距离,一般采用下面两种硬件连接方式。

​ 2)TTL电平转RS232电平

​ 硬件连接如下图

​ RS232电平规定逻辑“1”的电平为-5V~-15 V,逻辑“0”的电平为+5 V~+15 V,选用该电气标准以提高抗干扰能力。常用的TTL转RS232芯片有:MAX232,SP3232等。

​ 3)TTL电平转USB电平

​ 将TTL电平转换为USB电平(D+与D-一对差分信号采用NRZI编码实现通讯),提高抗干扰能力,常用的TTL转USB芯片有:PL2303,CH340, CP2102等

​ 100ASK_IMX6ULL采用的是上述方案中的第三种(TTL转USB方案)。

8.1.3 UART通讯数据格式

​ UART之间为何能够准确可靠的的发送和接收数据?

​ 首先我们需要约定好传输速率(每一秒传输的数据位数,即波特率),一般选择9600,19200,115200等。

​ 确定好传输速率后,我们还需要确定传输数据的格式,UART串口通信的数据包以帧为单位,常用的帧结构为:1位起始位+8位数据位+1位奇偶校验位(可选)+1位停止位。

​ 举例波形说明如下图:

​ 上图为:1位起始位+8位数据位+1位偶校验位+1位停止位 的波形。

​ 根据查找ASCII码表得知’ A’字符的ASCII值为41(十进制),将其转换成二进制应该为0100 0001,小端传输,即低位(LSB)在前,高位(MSB)在后,和上图所示一致。

​ 上图中各位的详细说明如下:

​ 1) 空闲位:

​ 平时没有数据时,数据线为高电平(逻辑1);

​ 2) 起始位:

​ 当需要发送数据时,UART将改变UARTx_TXD的状态,变为低电平,即为上图中的起始位(逻辑0);

​ 3) 数据位:

​ 可以有5、6、7或8位的数据,一般我们是按字节(8位)传输数据,发送方一位一位的改变数据线上的状态(高电平或低电平)将它们发送出去,传输数据时先传最低位,最后传送最高位。

​ 字符’A’的8位二进制字符是0100 0001,先发送最低位bit 0,其值为1;再发送bit 1,其值为0,如此继续;最后发送最高位bit 7,其值为0。

​ 4) 奇偶校验位

​ 如果使用了奇偶校验功能,有效数据位发送完毕后,还要发送1个校验位(奇偶校验位)。

​ 有两种校验方法:奇校验,偶校验-------数据位连同校验位中,“1”的数目等于奇数或偶数。奇偶校验只能检错,不能纠错的。而且只能检测1位误码,检测出有错后只能要求重发,没法纠正的。

​ 上图中使用的是偶较验,即8个数据位和1个校验位中,一共有偶数个“1”:2个。

​ 5) 停止位

​ 停止位(逻辑1)用来表示当前数据传输完毕。

​ 停止位的长度有三种:1位,1.5位,2位,通常我们选择1位即可。

8.2 IMX6ULL UART寄存器介绍

​ UART:Universal Asynchronous Receiver/Transmitter,通用异步收发传输器。

8.2.1 IMX6ULL UART模块简介

​ IMX6ULL共8个独立的UART通道,即8个UART,主要特性如下:

​ a.兼容高速串口标准TIA/EIA-232-F,高达5Mbit/s;

​ b.低速串行红外接口(IR),兼容Ir-DA(速度高达115.2Kbit/s);

​ c.支持9位或多点模式(RS-485)(自动从机地址检测);

​ d.1或2位停止位;

​ e.可编程奇偶校验(偶校验,奇校验,不校验);

​ f.自动波特率检测(最高支持115.2Kbit/s);

​ g.可屏蔽中断

​ h.软复位(SRST_B)

​ 以上只是列举了部分常用的特性,如需要更加详细的了解需要查看芯片手册《Chapter 55 Universal Asynchronous Receiver/Transmitter(UART)》。

8.2.2 IMX6ULL UART寄存器简介

​ IMX6ULL 8路UART通道,每一路通道都有17个寄存器,掌握其中一路通道,其他通道都是照葫芦画瓢,只是基地址变了和IO复用管脚不同了。

​ UART1的寄存器如下图:

​ 而我们本次实验使用到的只有上面红色框框9项。

8.2.2.1 UART1_URXD:

​ 主要用于接收串口数据的寄存器,只有低八位的空间是存储接收数据的,其他是一些判断位,基本用不上;

8.2.2.2 UART1_UTXD:

​ 用于发送串口数据的寄存器,只有低八位的空间用于发送数据,其他位保留不使用;

8.2.2.3 UART1_UCR1:

​ 控制寄存器1,用于设置串口各类功能的使能,例如自动波特率检测的使能,发送中断,串口DMA使能,串口使能等。而我们本章节只使用bit0,串口使能即可。

8.2.2.4 UART1_UCR2:

​ 控制寄存器2:主要用于设置串口的发送帧格式,帧长,是否奇偶校验,是否忽略有RTS,软复位等,本章只使用其中的5位,后续UART编程会详细讲解。

8.2.2.5 UART1_UCR3:

​ 控制寄存器3:我们只设置bit2,官方要求设置,属于芯片特点。

8.2.2.6 UART1_UFCR:

​ 串口FIFO控制寄存器,设置发送与接收的fifo的大小,最大32字节,串口时钟分频系数等,只要把RFDIV此位设置为不分频,其他用默认值即可,更详细使用会在后面的UART编程中讲解。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3WY9zM06-1642060106751)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image011.png)]

8.2.2.7 UART1_USR2:

​ 串口状态寄存器,该寄存器里面主要是一些串口的状态位,我们本章只使用了TXDC发送完成位与ROR接收数据就绪位。详细使用会在后面UART编程中讲解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RRrkCTPv-1642060106752)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image012.png)]

8.2.2.8 UART1_UBIR与UART1_UBMR:

​ 用于设置波特率即每秒可传输的位数,后面UART编程会详细讲解。

​ 基本我们使用的寄存器就这些,其他都是一些功能扩展,想详细了解的可以查看芯片手册的《Chapter 55 Universal Asynchronous Receiver/Transmitter(UART)》

8.3 IMX6ULL UART编程

8.3.1 看原理图确定UART引脚

​ 从上图可知,采用的UART转USB的方案,使用的是UART1。查看IMX6ULL芯片手册《Chapter 55 Universal Asynchronous Receiver/Transmitter(UART)》中涉及关键字UART1的寄存器并设置它。

8.3.2 涉及的UART1寄存器配置

​ 寄存器配置共分为4步骤,该小节中的代码都在程序文件uart.c中

8.3.2.1 步骤1:配置并使能UART1时钟

​ 配置并使能UART1时钟,参考资料:芯片手册《Chapter 18: Clock Controller Module (CCM)》

​ ①需要配置UART模块的时钟(寄存器:CCM_CSCDR1)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-db7puYcS-1642060106753)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image014.png)]

​ 根据上图可知,我们需要设置CCM_CSCDR1 [UART_CLK_SEL]和CCM_CSCDR1 [UART_CLK_PODF]。

​ 由上图CCM_CSCDR1寄存器,我们可以了解到

​ CCM_CSCDR1 [UART_CLK_SEL]默认值为0;CCM_CSCDR1 [UART_CLK_PODF]默认值为0

​ 我们一般选择 pll3_80m 作为 UART 的时钟源,UART_CLK_PODF分频系数选1分频(不分频),最后得到UART的时钟频率为80MHz。

​ 正好默认值都为0满足我们的时钟需求,所以后续的编程实验,串口时钟这部分可以不设置,用默认值就可以了。

​ ②需要使能UART模块的时钟(寄存器:CCM_CCGR5)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q28rkb2b-1642060106755)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image017.png)]

​ 由上图CCM_CCGR5寄存器,我们可以了解到CCM_CCGR5[CG12] 的默认值为11

​ 参考章节《4-1.3 CCM用于设置是否向GPIO模块提供时钟》我们了解到11表示该模块全程使能,使用默认值,无需设置。

​ 因此时钟这块我们都不需要配置,直接使用默认值即可。

8.3.2.2 步骤2:复用相关GPIO为UART1功能

​ 复用相关GPIO为UART1功能,参考资料:芯片手册《Chapter 32: IOMUX Controller (IOMUXC)》

​ 根据前面硬件连接讲解,我们知道串口通信精简接法需要3根信号线,其中GND已由硬连接了,所以接下来我们需要配置剩余的两个GPIO引脚 (UART1_TXD与UART1_RXD)

​ ①需要配置UART1_TX复用功能

​ (寄存器:IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA)

​ 由上图我们得知IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA[MUX_MODE]的默认值为0101,因此我们需要将其改为0,用于UART_TX功能

​ ②需要配置UART1_RX复用功能

​ (寄存器:IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JPiLTrbP-1642060106756)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image019.png)]

​ 由上图我们得知IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA[MUX_MODE]的默认值为0101,因此我们需要将其改为0,用于UART_RX功能。(程序文件:uart.c)

IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA		= (volatile unsigned int *)(0x20E0084);
IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA		= (volatile unsigned int *)(0x20E0088);

*IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA = 0;
*IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA = 0;

​ ③ 需要配置UART1_TX硬件参数(寄存器:IOMUXC_SW_PAD_CTL_PAD_UART1_TX_DATA)

​ 参考章节《4-1.3 IOMUXC:引脚的模式(Mode、功能)》,经过比对,我们使用默认值(0x10b0)即可,无需配置。

​ ④ 需要配置UART1_RX硬件参数(寄存器:IOMUXC_SW_PAD_CTL_PAD_UART1_RX_DATA)

​ 参考章节《4-1.3 IOMUXC:引脚的模式(Mode、功能)》, 经过比对,我们使用默认值(0x10b0)即可,无需配置。

8.3.2.3 步骤3:设置UART1传输格式,波特率

​ 设置UART1传输格式,波特率

​ ① 配置寄存器UART1_UCR2(0x2020084)

​ 设置UART1传输格式

UART1->UCR2 |= (1<<14) |(1<<5) |(1<<2)|(1<<1);

​ [14] 1:忽略RTS引脚

​ [8] : 0: 关闭奇偶校验 默认为0,无需设置

​ [6] : 0: 停止位1位 默认为0,无需设置

​ [5] : 1: 数据长度8位

​ [2] : 1: 发送数据使能

​ [1] : 1: 接收数据使能

​ ②配置寄存器UART1_UCR3(0x2020088)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gfzaWjbF-1642060106758)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image023.png)]

​ 根据官方文档表示 [RXDMUXSEL]需要设置为1。

​ [2] : 1:IM6ULL的UART用了这个MUXED模型,因此这一位需要置位为1

​ 设置串口模型

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

​ ③寄存器UART1_UFCR(0x2020090)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6PQ2LvIN-1642060106758)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image024.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kO7MU8TS-1642060106758)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image025.png)]

​ UART1_UFCR[9-7]:UART的时钟源分频系数

​ 我们选择101(即十进制的5),表示不分频。

UART1->UFCR = 5 << 7;    /* Uart的时钟clk:80MHz */

​ ④寄存器UART1_UBIR(0x20200A4), UART1_UBMR(0x20200A8)

​ 通过设置UART1_UBIR与UART1_UBMR,最终确定波特率。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WlW63GRa-1642060106759)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image027.png)]

​ IMX6ULL波特率计算公式:

​ a.设置115200的波特率即BaudRate = 115200;

​ b.UART1的时钟频率前面内容已确定80Mhz即Ref Freq = 80000000;

​ c.IMX6ULL波特率计算公式得115200 = 80000000 /(16*(UBMR + 1)/(UBIR+1));

​ d.选取一组满足上式的参数:UBMR、UBIR即可;

​ e. UART1_UBIR = 71 ; UART1_UBMR = 3124

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

8.3.2.4 步骤4:使能UART1

​ 使能UART1,UART1_UCR1(0x2020080)寄存器如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wM33Ha0e-1642060106759)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image029.png)]

​ 配置UART1_UCR1[0]:1表示使能UART, 0表示关闭UART。

Base->UCR1 |= (1 << 0);		/*使能当前串口*/

8.3.3 实现串口发送功能

8.3.3.1 步骤1: 编写UART1发送单字节函数

​ 写这个函数之前,我们需要了解什么时候才能发,什么时候不能发。

​ 只有当上一个数据发完的时候,我们才能继续发送,因此需要用到UART1_USR2寄存器中表示UART1发送状态的只读状态位[TXDC]。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r1AMDs8l-1642060106760)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image031.png)]

​ UART1_USR2[3] : 0表示发送未完成 , 1表示发送已完成

​ 程序文件:uart.c

void PutChar(int c)						
{
	while (!((UART1->USR2) & (1<<3))); /*等待上个字节发送完毕*/
	UART1->UTXD = (unsigned char)c;		
}

8.3.3.2 步骤2:编写用于测试的main函数

​ 程序文件:main.c

#include "uart.h"

int  main()
{
	unsigned char cTestData = 'A'; /*用于测试发送的数据*/
 	Uart_Init()	 ;

	while(1)
	{	
		PutChar(cTestData);
	}
					
	return 0;
}

**注明:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/001_uart_txd_char)文件夹下。

8.3.3.3 步骤3:参考章节《4-1.4编译程序》编译程序
8.3.3.4 步骤4:参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hpYXwfyO-1642060106760)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image032.png)]

​ 如果实验结果如上图电脑串口将会不断收到 ‘A’,表明实验成功。

8.3.4 实现串口接收功能

8.3.4.1 步骤1:编写UART1接收单字节函数

​ 编写UART1接收单字节函数,接收单字节时,我们也需要去判定UART1_USR2寄存器中的只读状态位[ROR]。

​ UART1_USR2[0] : 0表示没有接收数据就绪, 1表示接收数据准备就绪。

​ 程序文件:uart.c

unsigned char GetChar(void)						
{	
	while (!(UART1->USR2 & (1<<0)));  /*等待接收数据*/
	return (unsigned char)UART1->URXD;
}

8.3.4.2 步骤2:编写用于测试main函数

​ 编写用于测试main函数,我们让串口接收到的数据,交给上一小节中所写的发送函数中,从而实现串口回显。

​ 程序文件:main.c

#include "uart.h"

int  main()
{
	unsigned char cTestData ;       /*用于测试发送的数据*/
	Uart_Init()	 ;

	while(1)
	{	
		cTestData = GetChar() ;				/*等待从串口获取数据*/
		PutChar(cTestData)    ;				/*从串口发送数据*/
	}
					
	return 0;
}

​ 接着我们参照参考《4.4.4 编译程序》与参考《3.4 映像文件烧写、运行》,上机验证

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kpsbGItQ-1642060106761)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image034.png)]

​ 在键盘中输入的数据,会在终端回显出来表明实验成功,例如:如上图,在键盘上输入100ask.6ull,终端会完整回显出来。

**注明:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/002_uart_txd_char)文件夹下。

8.3.5 完善回显功能

​ 我们会发现上一小节所写的函数中有点问题,当我们按下回车的时候不会换行到行首,而是直接到行首,所以我们稍微修改main函数完善它。

​ 程序文件:main.c

while(1)
{	
    cTestData = GetChar() ;				/*等待从串口获取数据*/

    if (cTestData == '\r')  		/*添加回车换行\n\r*/
    { 
        PutChar('\n');
    }

    if (cTestData == '\n')
    {
        PutChar('\r');
    }

    PutChar(cTestData)    ;				/*从串口发送数据*/
}

​ 在获得数据的时候,判定回车符‘\r’和换行符‘\n’即可,重新烧写上机实验,按下回车键就能回车换行

**注明:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/003_uart_better_char)文件夹下。

8.3.6 实现串口发送字符串功能

8.3.6.1 步骤1:实现打印字符串函数

​ 实现打印字符串函数**,**在发送单字节的基础上,加上判断语句,实现连续打印字符。

​ 程序文件:my_printf.c

void PutStr(const char *s)				
{
	while (*s)
	{
		PutChar(*s);
		s++;
	}
}

8.3.6.2 步骤2:在main函数中添加打印字符串函数的调用

​ 在main函数中添加打印字符串函数的调用。

​ 程序文件:main.c

PutStr("Hello, world!\n\r");  /*发送字符串*/

​ **注明:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/004_uart_str)文件夹下。

8.3.6.3 步骤3:参考章节《4-1.4编译程序》编译程序
8.3.6.4 步骤4:参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 终端将打印出Hello,world!

Hello, world!

8.4 移植printf

8.4.1 在1.3章节工程文件基础上移植

​ ①在uart.c中加入raise函数,用于防止编译失败。

​ 程序文件:uart.c

119 int raise(int signum)/* raise函数,防止编译报错 */
120 {
121   return 0;
122 }

​ ②修改Makefile

​ my_printf.c中用到除法的求模运算,需要提供除法库。一般的交叉工具链里都提示有基本的数学运算,它们位于libgcc.a中。我们需要把libgcc.a也链接进程序里,需要修改Makefile。

​ 注意:链接指令中,每年“-L”表示库在哪里,即它的目录;“-l”表示哪个库,即库的名称,-lgcc 表示会链接“libgcc.a”库。

​ 对Makefile作如下修改:

​ a. 增加$(CC) -nostdlib -g -c -o my_printf.o my_printf.c

​ b. 在$(LD) -T imx6ull.lds -g start.o uart.o main.o my_printf.o -o my_printf.elf后添加

​ -lgcc –L<libgcc.a的路径>

​ 例如:$(LD) -T imx6ull.lds -g start.o uart.o main.o my_printf.o -o my_printf.elf -lgcc

-L/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1*-2016.11-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/6.2.1

8.4.2 变参数函数移植

​ 函数参数列表包括了字符串(format)和变参(…)组合而成

​ 在vc6.0的头文件stdarg.h中找到

typedef char *  va_list;

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap)      ( ap = (va_list)0 )

​ 1) _INTSIZEOF(n) 用于获取其中一个变参类型占用的空间长度

​ 2) va_start(ap,v) 令ap指向第一个变参地址

​ 3) va_arg(ap,t) 取出一个变参,同时指针指向下一个变参

​ 4) va_end(ap) 将指针指向NULL,防止野指针

​ 移植以上的代码,编写一个属于自己的printf

​ 参考int printf(const char *format, …)库函数,实现my_printf

​ 程序文件:my_printf.c

int printf(const char *fmt, ...) 
{
    va_list ap;

    va_start(ap, fmt);
    my_vprintf(fmt, ap);	
    va_end(ap);
    return 0;
}

8.4.3 编写my_vprintf(fmt, ap)

​ 参考int vprintf(const char *format, va_list ap)实现my_vprintf

​ 程序文件:my_printf.c

static int my_vprintf(const char *fmt, va_list ap) 
{
	char lead=' ';
	int  maxwidth=0;
	
	 for(; *fmt != '\0'; fmt++)		
	 {
		if (*fmt != '%') {			
			outc(*fmt);				
			continue;
	 	}

		 lead=' ';
		 maxwidth=0;
	 
		//format : %08d, %8d,%d,%u,%x,%f,%c,%s 
		    fmt++;
		if(*fmt == '0'){
			lead = '0';
			fmt++;	
		}

	
		while(*fmt >= '0' && *fmt <= '9'){
			maxwidth *=10;
		maxwidth += (*fmt - '0');
		fmt++;
	}
	
	switch (*fmt) {
	case 'd': out_num(va_arg(ap, int),          10,lead,maxwidth); break;
    case 'o': out_num(va_arg(ap, unsigned int),  8,lead,maxwidth); break;			
	case 'u': out_num(va_arg(ap, unsigned int), 10,lead,maxwidth); break;
	case 'x': out_num(va_arg(ap, unsigned int), 16,lead,maxwidth); break;
	case 'c': outc(va_arg(ap, int   )); break;		
    case 's': outs(va_arg(ap, char *)); break;		  		
			
    default:  
		outc(*fmt);
	    break;
		}
	}
	return 0;
}

8.4.4 编写out_c,outs与out_num函数

​ 利用之前我们实现的单字节打印函数void PutChar(int c)实现out_c,outs与out_num函数outc用于格式化输出中的%c的输出。

​ 程序文件:my_printf.c

static int outc(int c) 
{
	PutChar(c);
	return 0;
}

​ outs用于格式化输出中的%s的输出。

​ 程序文件:my_printf.c

static int outs (const char *s)
{
	while (*s != '\0')	
		PutChar(*s++);
	return 0;
}

​ out_num用于格式化输出中的%d,%o,%u,%x的输出。

​ 程序文件:my_printf.c

static int out_num(long n, int base,char lead,int maxwidth) 
{
    unsigned long m=0;
    char buf[MAX_NUMBER_BYTES], *s = buf + sizeof(buf);
    int count=0,i=0;


    *--s = '\0';

    if (n < 0){
        m = -n;
    }
    else{
        m = n;
    }

    do{
        *--s = hex_tab[m%base];
        count++;
    }while ((m /= base) != 0);

    if( maxwidth && count < maxwidth){
        for (i=maxwidth - count; i; i--)	
            *--s = lead;
    }

    if (n < 0)
        *--s = '-';

    return outs(s);
}

8.4.5 编写my_printf_test的测试函数

​ 程序文件:my_printf.c

int my_printf_test(void)
{
    printf("This is www.100ask.org   my_printf test\n\r") ;	
    printf("test char           =%c,%c\n\r", 'A','a') ;	
    printf("test decimal number =%d\n\r",    123456) ;
    printf("test decimal number =%d\n\r",    -123456) ;	
    printf("test hex     number =0x%x\n\r",  0x55aa55aa) ;	
    printf("test string         =%s\n\r",    "www.100ask.org") ;	
    printf("num=%08d\n\r",   12345);
    printf("num=%8d\n\r",    12345);
    printf("num=0x%08x\n\r", 0x12345);
    printf("num=0x%8x\n\r",  0x12345);
    printf("num=0x%02x\n\r", 0x1);
    printf("num=0x%2x\n\r",  0x1);

    printf("num=%05d\n\r", 0x1);
    printf("num=%5d\n\r",  0x1);

    return 0;
}

8.4.6 编写main测试程序

​ 在main函数中只需要调用8.3.5中实现的my_printf_test即可。

​ 程序文件:main.c

#include "my_printf.h"
#include "uart.h"
int  main()
{
    Uart_Init();
    my_printf_test();			
    return 0;
}

注明: 此例程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/005_myprintf_test)。

8.4.6.1 步骤1:参考章节《4-1.4编译程序》编译程序
8.4.6.2 步骤2:参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 最后编译程序烧写至开发板,观察终端输出信息。

This is www.100ask.org   my_printf test
test char           =A,a
test decimal number =123456
test decimal number =-123456
test hex     number =0x55aa55aa
test string         =www.100ask.org
num=00012345
num=   12345
num=0x00012345
num=0x   12345
num=0x01
num=0x 1
num=00001
num=    1

​ 串口终端如果打印以上信息,证明实验成功!

​ **注意:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/005_printf_test文件夹下,可用于其它程序使用串口来打印或接收字符串。

版权声明:本文为CSDN博主「韦东山」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/thisway_diy/article/details/122475598

第八章 UART串口编程

8.1 UART介绍

8.1.1 UART串口简介

​ UART全称是通用异步收发传输器(Universal Asynchronous Receiver/Transmitter)。串口顾名思义数据串行接口,即数据的传输是一位接一位传输,属于一种串行的数据总线,属于异步通讯,同时支持全双工数据传输(全双工数据传输:允许发送数据和接收数据在同一时刻发生) 。

​ 除了UART,另外还有一种叫USART,全称是通用同步/异步串行接收/发送器(Universal Synchronous/Asynchronous Receiver/Transmitter),USART比 UART多了同步通信功能,但是百分之90的工程应用中不会应用该同步功能,都是将USART当做UART使用,即采取异步串行通讯。一般开发板或者产品中都会将UART串口标为serial或COM。

8.1.2 UART硬件连接

​ 1) UART串口最精简的连接是TTL电平三线连接

​ UARTx_TXD:用于发送数据,应连接到接收设备的UARTx_RXD引脚上;

​ UARTx_RXD:用于接收数据,应连接到发送设备的UARTx_TXD引脚上;

​ GND:为双方提供一个相同的参考电平。

​ 上图为UART串口TTL电平硬件连接,此时使用标准的TTL电平来表示数据,高电平表示1,低电平表示0,标准TTL输入高电平最小2V,输出高电平最小2.4V,典型值为3.4V,输入低电平最大0.8V,输出低电平最大0.4V,典型值为0.2V。

​ 直接采用TTL电平进行串行通讯,由于其抗干扰能力差,导致传输距离短,且容易出现数据不可靠的情况。

​ 为提高抗干扰能力和传输距离,一般采用下面两种硬件连接方式。

​ 2)TTL电平转RS232电平

​ 硬件连接如下图

​ RS232电平规定逻辑“1”的电平为-5V~-15 V,逻辑“0”的电平为+5 V~+15 V,选用该电气标准以提高抗干扰能力。常用的TTL转RS232芯片有:MAX232,SP3232等。

​ 3)TTL电平转USB电平

​ 将TTL电平转换为USB电平(D+与D-一对差分信号采用NRZI编码实现通讯),提高抗干扰能力,常用的TTL转USB芯片有:PL2303,CH340, CP2102等

​ 100ASK_IMX6ULL采用的是上述方案中的第三种(TTL转USB方案)。

8.1.3 UART通讯数据格式

​ UART之间为何能够准确可靠的的发送和接收数据?

​ 首先我们需要约定好传输速率(每一秒传输的数据位数,即波特率),一般选择9600,19200,115200等。

​ 确定好传输速率后,我们还需要确定传输数据的格式,UART串口通信的数据包以帧为单位,常用的帧结构为:1位起始位+8位数据位+1位奇偶校验位(可选)+1位停止位。

​ 举例波形说明如下图:

​ 上图为:1位起始位+8位数据位+1位偶校验位+1位停止位 的波形。

​ 根据查找ASCII码表得知’ A’字符的ASCII值为41(十进制),将其转换成二进制应该为0100 0001,小端传输,即低位(LSB)在前,高位(MSB)在后,和上图所示一致。

​ 上图中各位的详细说明如下:

​ 1) 空闲位:

​ 平时没有数据时,数据线为高电平(逻辑1);

​ 2) 起始位:

​ 当需要发送数据时,UART将改变UARTx_TXD的状态,变为低电平,即为上图中的起始位(逻辑0);

​ 3) 数据位:

​ 可以有5、6、7或8位的数据,一般我们是按字节(8位)传输数据,发送方一位一位的改变数据线上的状态(高电平或低电平)将它们发送出去,传输数据时先传最低位,最后传送最高位。

​ 字符’A’的8位二进制字符是0100 0001,先发送最低位bit 0,其值为1;再发送bit 1,其值为0,如此继续;最后发送最高位bit 7,其值为0。

​ 4) 奇偶校验位

​ 如果使用了奇偶校验功能,有效数据位发送完毕后,还要发送1个校验位(奇偶校验位)。

​ 有两种校验方法:奇校验,偶校验-------数据位连同校验位中,“1”的数目等于奇数或偶数。奇偶校验只能检错,不能纠错的。而且只能检测1位误码,检测出有错后只能要求重发,没法纠正的。

​ 上图中使用的是偶较验,即8个数据位和1个校验位中,一共有偶数个“1”:2个。

​ 5) 停止位

​ 停止位(逻辑1)用来表示当前数据传输完毕。

​ 停止位的长度有三种:1位,1.5位,2位,通常我们选择1位即可。

8.2 IMX6ULL UART寄存器介绍

​ UART:Universal Asynchronous Receiver/Transmitter,通用异步收发传输器。

8.2.1 IMX6ULL UART模块简介

​ IMX6ULL共8个独立的UART通道,即8个UART,主要特性如下:

​ a.兼容高速串口标准TIA/EIA-232-F,高达5Mbit/s;

​ b.低速串行红外接口(IR),兼容Ir-DA(速度高达115.2Kbit/s);

​ c.支持9位或多点模式(RS-485)(自动从机地址检测);

​ d.1或2位停止位;

​ e.可编程奇偶校验(偶校验,奇校验,不校验);

​ f.自动波特率检测(最高支持115.2Kbit/s);

​ g.可屏蔽中断

​ h.软复位(SRST_B)

​ 以上只是列举了部分常用的特性,如需要更加详细的了解需要查看芯片手册《Chapter 55 Universal Asynchronous Receiver/Transmitter(UART)》。

8.2.2 IMX6ULL UART寄存器简介

​ IMX6ULL 8路UART通道,每一路通道都有17个寄存器,掌握其中一路通道,其他通道都是照葫芦画瓢,只是基地址变了和IO复用管脚不同了。

​ UART1的寄存器如下图:

​ 而我们本次实验使用到的只有上面红色框框9项。

8.2.2.1 UART1_URXD:

​ 主要用于接收串口数据的寄存器,只有低八位的空间是存储接收数据的,其他是一些判断位,基本用不上;

8.2.2.2 UART1_UTXD:

​ 用于发送串口数据的寄存器,只有低八位的空间用于发送数据,其他位保留不使用;

8.2.2.3 UART1_UCR1:

​ 控制寄存器1,用于设置串口各类功能的使能,例如自动波特率检测的使能,发送中断,串口DMA使能,串口使能等。而我们本章节只使用bit0,串口使能即可。

8.2.2.4 UART1_UCR2:

​ 控制寄存器2:主要用于设置串口的发送帧格式,帧长,是否奇偶校验,是否忽略有RTS,软复位等,本章只使用其中的5位,后续UART编程会详细讲解。

8.2.2.5 UART1_UCR3:

​ 控制寄存器3:我们只设置bit2,官方要求设置,属于芯片特点。

8.2.2.6 UART1_UFCR:

​ 串口FIFO控制寄存器,设置发送与接收的fifo的大小,最大32字节,串口时钟分频系数等,只要把RFDIV此位设置为不分频,其他用默认值即可,更详细使用会在后面的UART编程中讲解。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3WY9zM06-1642060106751)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image011.png)]

8.2.2.7 UART1_USR2:

​ 串口状态寄存器,该寄存器里面主要是一些串口的状态位,我们本章只使用了TXDC发送完成位与ROR接收数据就绪位。详细使用会在后面UART编程中讲解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RRrkCTPv-1642060106752)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image012.png)]

8.2.2.8 UART1_UBIR与UART1_UBMR:

​ 用于设置波特率即每秒可传输的位数,后面UART编程会详细讲解。

​ 基本我们使用的寄存器就这些,其他都是一些功能扩展,想详细了解的可以查看芯片手册的《Chapter 55 Universal Asynchronous Receiver/Transmitter(UART)》

8.3 IMX6ULL UART编程

8.3.1 看原理图确定UART引脚

​ 从上图可知,采用的UART转USB的方案,使用的是UART1。查看IMX6ULL芯片手册《Chapter 55 Universal Asynchronous Receiver/Transmitter(UART)》中涉及关键字UART1的寄存器并设置它。

8.3.2 涉及的UART1寄存器配置

​ 寄存器配置共分为4步骤,该小节中的代码都在程序文件uart.c中

8.3.2.1 步骤1:配置并使能UART1时钟

​ 配置并使能UART1时钟,参考资料:芯片手册《Chapter 18: Clock Controller Module (CCM)》

​ ①需要配置UART模块的时钟(寄存器:CCM_CSCDR1)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-db7puYcS-1642060106753)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image014.png)]

​ 根据上图可知,我们需要设置CCM_CSCDR1 [UART_CLK_SEL]和CCM_CSCDR1 [UART_CLK_PODF]。

​ 由上图CCM_CSCDR1寄存器,我们可以了解到

​ CCM_CSCDR1 [UART_CLK_SEL]默认值为0;CCM_CSCDR1 [UART_CLK_PODF]默认值为0

​ 我们一般选择 pll3_80m 作为 UART 的时钟源,UART_CLK_PODF分频系数选1分频(不分频),最后得到UART的时钟频率为80MHz。

​ 正好默认值都为0满足我们的时钟需求,所以后续的编程实验,串口时钟这部分可以不设置,用默认值就可以了。

​ ②需要使能UART模块的时钟(寄存器:CCM_CCGR5)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q28rkb2b-1642060106755)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image017.png)]

​ 由上图CCM_CCGR5寄存器,我们可以了解到CCM_CCGR5[CG12] 的默认值为11

​ 参考章节《4-1.3 CCM用于设置是否向GPIO模块提供时钟》我们了解到11表示该模块全程使能,使用默认值,无需设置。

​ 因此时钟这块我们都不需要配置,直接使用默认值即可。

8.3.2.2 步骤2:复用相关GPIO为UART1功能

​ 复用相关GPIO为UART1功能,参考资料:芯片手册《Chapter 32: IOMUX Controller (IOMUXC)》

​ 根据前面硬件连接讲解,我们知道串口通信精简接法需要3根信号线,其中GND已由硬连接了,所以接下来我们需要配置剩余的两个GPIO引脚 (UART1_TXD与UART1_RXD)

​ ①需要配置UART1_TX复用功能

​ (寄存器:IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA)

​ 由上图我们得知IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA[MUX_MODE]的默认值为0101,因此我们需要将其改为0,用于UART_TX功能

​ ②需要配置UART1_RX复用功能

​ (寄存器:IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JPiLTrbP-1642060106756)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image019.png)]

​ 由上图我们得知IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA[MUX_MODE]的默认值为0101,因此我们需要将其改为0,用于UART_RX功能。(程序文件:uart.c)

IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA		= (volatile unsigned int *)(0x20E0084);
IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA		= (volatile unsigned int *)(0x20E0088);

*IOMUXC_SW_MUX_CTL_PAD_UART1_RX_DATA = 0;
*IOMUXC_SW_MUX_CTL_PAD_UART1_TX_DATA = 0;

​ ③ 需要配置UART1_TX硬件参数(寄存器:IOMUXC_SW_PAD_CTL_PAD_UART1_TX_DATA)

​ 参考章节《4-1.3 IOMUXC:引脚的模式(Mode、功能)》,经过比对,我们使用默认值(0x10b0)即可,无需配置。

​ ④ 需要配置UART1_RX硬件参数(寄存器:IOMUXC_SW_PAD_CTL_PAD_UART1_RX_DATA)

​ 参考章节《4-1.3 IOMUXC:引脚的模式(Mode、功能)》, 经过比对,我们使用默认值(0x10b0)即可,无需配置。

8.3.2.3 步骤3:设置UART1传输格式,波特率

​ 设置UART1传输格式,波特率

​ ① 配置寄存器UART1_UCR2(0x2020084)

​ 设置UART1传输格式

UART1->UCR2 |= (1<<14) |(1<<5) |(1<<2)|(1<<1);

​ [14] 1:忽略RTS引脚

​ [8] : 0: 关闭奇偶校验 默认为0,无需设置

​ [6] : 0: 停止位1位 默认为0,无需设置

​ [5] : 1: 数据长度8位

​ [2] : 1: 发送数据使能

​ [1] : 1: 接收数据使能

​ ②配置寄存器UART1_UCR3(0x2020088)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gfzaWjbF-1642060106758)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image023.png)]

​ 根据官方文档表示 [RXDMUXSEL]需要设置为1。

​ [2] : 1:IM6ULL的UART用了这个MUXED模型,因此这一位需要置位为1

​ 设置串口模型

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

​ ③寄存器UART1_UFCR(0x2020090)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6PQ2LvIN-1642060106758)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image024.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kO7MU8TS-1642060106758)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image025.png)]

​ UART1_UFCR[9-7]:UART的时钟源分频系数

​ 我们选择101(即十进制的5),表示不分频。

UART1->UFCR = 5 << 7;    /* Uart的时钟clk:80MHz */

​ ④寄存器UART1_UBIR(0x20200A4), UART1_UBMR(0x20200A8)

​ 通过设置UART1_UBIR与UART1_UBMR,最终确定波特率。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WlW63GRa-1642060106759)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image027.png)]

​ IMX6ULL波特率计算公式:

​ a.设置115200的波特率即BaudRate = 115200;

​ b.UART1的时钟频率前面内容已确定80Mhz即Ref Freq = 80000000;

​ c.IMX6ULL波特率计算公式得115200 = 80000000 /(16*(UBMR + 1)/(UBIR+1));

​ d.选取一组满足上式的参数:UBMR、UBIR即可;

​ e. UART1_UBIR = 71 ; UART1_UBMR = 3124

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

8.3.2.4 步骤4:使能UART1

​ 使能UART1,UART1_UCR1(0x2020080)寄存器如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wM33Ha0e-1642060106759)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image029.png)]

​ 配置UART1_UCR1[0]:1表示使能UART, 0表示关闭UART。

Base->UCR1 |= (1 << 0);		/*使能当前串口*/

8.3.3 实现串口发送功能

8.3.3.1 步骤1: 编写UART1发送单字节函数

​ 写这个函数之前,我们需要了解什么时候才能发,什么时候不能发。

​ 只有当上一个数据发完的时候,我们才能继续发送,因此需要用到UART1_USR2寄存器中表示UART1发送状态的只读状态位[TXDC]。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r1AMDs8l-1642060106760)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image031.png)]

​ UART1_USR2[3] : 0表示发送未完成 , 1表示发送已完成

​ 程序文件:uart.c

void PutChar(int c)						
{
	while (!((UART1->USR2) & (1<<3))); /*等待上个字节发送完毕*/
	UART1->UTXD = (unsigned char)c;		
}

8.3.3.2 步骤2:编写用于测试的main函数

​ 程序文件:main.c

#include "uart.h"

int  main()
{
	unsigned char cTestData = 'A'; /*用于测试发送的数据*/
 	Uart_Init()	 ;

	while(1)
	{	
		PutChar(cTestData);
	}
					
	return 0;
}

**注明:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/001_uart_txd_char)文件夹下。

8.3.3.3 步骤3:参考章节《4-1.4编译程序》编译程序
8.3.3.4 步骤4:参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hpYXwfyO-1642060106760)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image032.png)]

​ 如果实验结果如上图电脑串口将会不断收到 ‘A’,表明实验成功。

8.3.4 实现串口接收功能

8.3.4.1 步骤1:编写UART1接收单字节函数

​ 编写UART1接收单字节函数,接收单字节时,我们也需要去判定UART1_USR2寄存器中的只读状态位[ROR]。

​ UART1_USR2[0] : 0表示没有接收数据就绪, 1表示接收数据准备就绪。

​ 程序文件:uart.c

unsigned char GetChar(void)						
{	
	while (!(UART1->USR2 & (1<<0)));  /*等待接收数据*/
	return (unsigned char)UART1->URXD;
}

8.3.4.2 步骤2:编写用于测试main函数

​ 编写用于测试main函数,我们让串口接收到的数据,交给上一小节中所写的发送函数中,从而实现串口回显。

​ 程序文件:main.c

#include "uart.h"

int  main()
{
	unsigned char cTestData ;       /*用于测试发送的数据*/
	Uart_Init()	 ;

	while(1)
	{	
		cTestData = GetChar() ;				/*等待从串口获取数据*/
		PutChar(cTestData)    ;				/*从串口发送数据*/
	}
					
	return 0;
}

​ 接着我们参照参考《4.4.4 编译程序》与参考《3.4 映像文件烧写、运行》,上机验证

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kpsbGItQ-1642060106761)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/08_UART_Promgram_image034.png)]

​ 在键盘中输入的数据,会在终端回显出来表明实验成功,例如:如上图,在键盘上输入100ask.6ull,终端会完整回显出来。

**注明:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/002_uart_txd_char)文件夹下。

8.3.5 完善回显功能

​ 我们会发现上一小节所写的函数中有点问题,当我们按下回车的时候不会换行到行首,而是直接到行首,所以我们稍微修改main函数完善它。

​ 程序文件:main.c

while(1)
{	
    cTestData = GetChar() ;				/*等待从串口获取数据*/

    if (cTestData == '\r')  		/*添加回车换行\n\r*/
    { 
        PutChar('\n');
    }

    if (cTestData == '\n')
    {
        PutChar('\r');
    }

    PutChar(cTestData)    ;				/*从串口发送数据*/
}

​ 在获得数据的时候,判定回车符‘\r’和换行符‘\n’即可,重新烧写上机实验,按下回车键就能回车换行

**注明:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/003_uart_better_char)文件夹下。

8.3.6 实现串口发送字符串功能

8.3.6.1 步骤1:实现打印字符串函数

​ 实现打印字符串函数**,**在发送单字节的基础上,加上判断语句,实现连续打印字符。

​ 程序文件:my_printf.c

void PutStr(const char *s)				
{
	while (*s)
	{
		PutChar(*s);
		s++;
	}
}

8.3.6.2 步骤2:在main函数中添加打印字符串函数的调用

​ 在main函数中添加打印字符串函数的调用。

​ 程序文件:main.c

PutStr("Hello, world!\n\r");  /*发送字符串*/

​ **注明:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/004_uart_str)文件夹下。

8.3.6.3 步骤3:参考章节《4-1.4编译程序》编译程序
8.3.6.4 步骤4:参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 终端将打印出Hello,world!

Hello, world!

8.4 移植printf

8.4.1 在1.3章节工程文件基础上移植

​ ①在uart.c中加入raise函数,用于防止编译失败。

​ 程序文件:uart.c

119 int raise(int signum)/* raise函数,防止编译报错 */
120 {
121   return 0;
122 }

​ ②修改Makefile

​ my_printf.c中用到除法的求模运算,需要提供除法库。一般的交叉工具链里都提示有基本的数学运算,它们位于libgcc.a中。我们需要把libgcc.a也链接进程序里,需要修改Makefile。

​ 注意:链接指令中,每年“-L”表示库在哪里,即它的目录;“-l”表示哪个库,即库的名称,-lgcc 表示会链接“libgcc.a”库。

​ 对Makefile作如下修改:

​ a. 增加$(CC) -nostdlib -g -c -o my_printf.o my_printf.c

​ b. 在$(LD) -T imx6ull.lds -g start.o uart.o main.o my_printf.o -o my_printf.elf后添加

​ -lgcc –L<libgcc.a的路径>

​ 例如:$(LD) -T imx6ull.lds -g start.o uart.o main.o my_printf.o -o my_printf.elf -lgcc

-L/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1*-2016.11-x86_64_arm-linux-gnueabihf/lib/gcc/arm-linux-gnueabihf/6.2.1

8.4.2 变参数函数移植

​ 函数参数列表包括了字符串(format)和变参(…)组合而成

​ 在vc6.0的头文件stdarg.h中找到

typedef char *  va_list;

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap)      ( ap = (va_list)0 )

​ 1) _INTSIZEOF(n) 用于获取其中一个变参类型占用的空间长度

​ 2) va_start(ap,v) 令ap指向第一个变参地址

​ 3) va_arg(ap,t) 取出一个变参,同时指针指向下一个变参

​ 4) va_end(ap) 将指针指向NULL,防止野指针

​ 移植以上的代码,编写一个属于自己的printf

​ 参考int printf(const char *format, …)库函数,实现my_printf

​ 程序文件:my_printf.c

int printf(const char *fmt, ...) 
{
    va_list ap;

    va_start(ap, fmt);
    my_vprintf(fmt, ap);	
    va_end(ap);
    return 0;
}

8.4.3 编写my_vprintf(fmt, ap)

​ 参考int vprintf(const char *format, va_list ap)实现my_vprintf

​ 程序文件:my_printf.c

static int my_vprintf(const char *fmt, va_list ap) 
{
	char lead=' ';
	int  maxwidth=0;
	
	 for(; *fmt != '\0'; fmt++)		
	 {
		if (*fmt != '%') {			
			outc(*fmt);				
			continue;
	 	}

		 lead=' ';
		 maxwidth=0;
	 
		//format : %08d, %8d,%d,%u,%x,%f,%c,%s 
		    fmt++;
		if(*fmt == '0'){
			lead = '0';
			fmt++;	
		}

	
		while(*fmt >= '0' && *fmt <= '9'){
			maxwidth *=10;
		maxwidth += (*fmt - '0');
		fmt++;
	}
	
	switch (*fmt) {
	case 'd': out_num(va_arg(ap, int),          10,lead,maxwidth); break;
    case 'o': out_num(va_arg(ap, unsigned int),  8,lead,maxwidth); break;			
	case 'u': out_num(va_arg(ap, unsigned int), 10,lead,maxwidth); break;
	case 'x': out_num(va_arg(ap, unsigned int), 16,lead,maxwidth); break;
	case 'c': outc(va_arg(ap, int   )); break;		
    case 's': outs(va_arg(ap, char *)); break;		  		
			
    default:  
		outc(*fmt);
	    break;
		}
	}
	return 0;
}

8.4.4 编写out_c,outs与out_num函数

​ 利用之前我们实现的单字节打印函数void PutChar(int c)实现out_c,outs与out_num函数outc用于格式化输出中的%c的输出。

​ 程序文件:my_printf.c

static int outc(int c) 
{
	PutChar(c);
	return 0;
}

​ outs用于格式化输出中的%s的输出。

​ 程序文件:my_printf.c

static int outs (const char *s)
{
	while (*s != '\0')	
		PutChar(*s++);
	return 0;
}

​ out_num用于格式化输出中的%d,%o,%u,%x的输出。

​ 程序文件:my_printf.c

static int out_num(long n, int base,char lead,int maxwidth) 
{
    unsigned long m=0;
    char buf[MAX_NUMBER_BYTES], *s = buf + sizeof(buf);
    int count=0,i=0;


    *--s = '\0';

    if (n < 0){
        m = -n;
    }
    else{
        m = n;
    }

    do{
        *--s = hex_tab[m%base];
        count++;
    }while ((m /= base) != 0);

    if( maxwidth && count < maxwidth){
        for (i=maxwidth - count; i; i--)	
            *--s = lead;
    }

    if (n < 0)
        *--s = '-';

    return outs(s);
}

8.4.5 编写my_printf_test的测试函数

​ 程序文件:my_printf.c

int my_printf_test(void)
{
    printf("This is www.100ask.org   my_printf test\n\r") ;	
    printf("test char           =%c,%c\n\r", 'A','a') ;	
    printf("test decimal number =%d\n\r",    123456) ;
    printf("test decimal number =%d\n\r",    -123456) ;	
    printf("test hex     number =0x%x\n\r",  0x55aa55aa) ;	
    printf("test string         =%s\n\r",    "www.100ask.org") ;	
    printf("num=%08d\n\r",   12345);
    printf("num=%8d\n\r",    12345);
    printf("num=0x%08x\n\r", 0x12345);
    printf("num=0x%8x\n\r",  0x12345);
    printf("num=0x%02x\n\r", 0x1);
    printf("num=0x%2x\n\r",  0x1);

    printf("num=%05d\n\r", 0x1);
    printf("num=%5d\n\r",  0x1);

    return 0;
}

8.4.6 编写main测试程序

​ 在main函数中只需要调用8.3.5中实现的my_printf_test即可。

​ 程序文件:main.c

#include "my_printf.h"
#include "uart.h"
int  main()
{
    Uart_Init();
    my_printf_test();			
    return 0;
}

注明: 此例程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/005_myprintf_test)。

8.4.6.1 步骤1:参考章节《4-1.4编译程序》编译程序
8.4.6.2 步骤2:参考章节《3-1.4映像文件烧写、运行》烧写、运行程序

​ 最后编译程序烧写至开发板,观察终端输出信息。

This is www.100ask.org   my_printf test
test char           =A,a
test decimal number =123456
test decimal number =-123456
test hex     number =0x55aa55aa
test string         =www.100ask.org
num=00012345
num=   12345
num=0x00012345
num=0x   12345
num=0x01
num=0x 1
num=00001
num=    1

​ 串口终端如果打印以上信息,证明实验成功!

​ **注意:**整个完整工程代码目录在裸机Git仓库 NoosProgramProject/(8_UART串口编程/005_printf_test文件夹下,可用于其它程序使用串口来打印或接收字符串。

版权声明:本文为CSDN博主「韦东山」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/thisway_diy/article/details/122475598

生成海报
点赞 0

韦东山

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

7_时钟体系

第七章 时钟体系 ​ 时钟信号是数字时序电路的“脉搏”,电路每接收到一个周期的时钟信号,就做一个相应的动作。因此,在允许的范围内,时钟信号的快慢直接决定着电路性能的好坏。在片上系统&#x

18_SPI编程

第十八章 SPI编程(有误) 18.1 SPI接口简介 ​ SPI(Serial Peripheral Interface)接口是全双工的同步串行通讯总线,支持通过多个不同的片选信号来连接多个外设。

Arduino入门教程

Arduino编译环境下载 当前最新版ArduinoIDE 下载地址: Software | Arduino 认识Arduino IDE 点亮13号引脚的LED灯,持续一秒,然后灭掉,再持续一秒

零基础入门STM32编程——工具篇(四)

前情回顾 上篇学习了STM32F103的系统架构,时钟树等知识点,了解了内部外设所挂载的总线。下面我们将继续输入学习STM32编程。 一 工具篇 “工欲善其事必先利其器”,开始STM32F103编