【SoC FPGA学习】六、基于虚拟地址映射的 UART 编程应用

上一节的实验,完成了虚拟地址的映射和基于虚拟地址的按键和 LED指示灯的编程控制。 本节将继续使用该种方法,完成对 AC501_SoC_GHRD 工程中添加的 uart_0 外设进行控制。

一、UART (RS-232 Serial port) 核介绍

UART (RS-232 Serial port) 核是 Platform Designer 中提供的一个经典的字符型串行通信外设,使用该外设,能够方便的与 FPGA 片外的设备进行通信。该 IP 核实现了 RS-232 协议的时序,并提供可调整的波特率速度,校验位,停止位和数据位宽。这些参数都是可以配置的,在具体应用中,根据实际需要用到的功能,配置这些特性,在保证功能实现的前提下,降低对 FPGA 逻辑资源的占用。

IP 核提供一个 Avalon Memory-Mapped (Avalon-MM) slave 接口来和 Avalon-MM master 外设,例如 Nios II CPU、 HPS 进行通信。该 IP 核还提供中断功能,支持以中断的方式来及时和主控进行通信,以获得更高的通信效率。

需要注意的是,该 IP 核内部不含数据 FIFO, 不具备 16550 标准串口的一系列功能,每次接收到一个字符的数据,必须由处理器及时读取,否则数据将丢失,这为查询方式操作该 IP 核带来了一定的考验。另外,如果使能了接收和发送中断,则每接收或发送一个字符的数据,都会对 CPU 发起一次中断, 因此频繁的数据收发会给 CPU 的中断处理造成较重的负担。

根据上述特性介绍, 设计者在为 HPS 添加 UART IP 时, 对于仅收发送少量命令和数据的的场合,由于该控制器结构简单,占用资源少, 编程简单,使用该控制器是一个不错的选择。 而需要考虑实时性和对 CPU 的中断资源开销问题的情况下, 不推荐在高速、频繁的数据通信场合使用本 UART 控制器

对于性能有要求的场合,可以使用 Platform Designer 中提供的 Altera 16550Compatible UART 核,该 IP 核兼容标准的 16550 串口, 提供与 16550 相同的功能和性能。 不过使用该 IP 需要取得相应的授权文件(License),或是针对自己的应用编写增强型 UART 控制器。 笔者就曾针对 ModBus 通信协议,编写过自带 CRC 校验、 256 字节深度接收 FIFO、 自动帧结束判定的增强型 UART 控制器, 并应用于工业通信设备上。

二、UART (RS-232 Serial port) 寄存器映射

这个 UART IP 核提供给一个 Avalon-MM slave 接口来与 Avalon-MM master接口的主机通信。 IP 核内部设有 6 个 16 位的寄存器,包括控制寄存器(control)、 状态寄存器(status)、 接收数据寄存器(rxdata)、发送数据寄存器(txdata)、波特率分频寄存器(divisor)和数据包结束寄存器(endofpacket)。Avalon-MM master 接口的主机通过读写这些寄存器就能完数据的收发。

关于该控制器的详细功能介绍,不作为本书的重点,需要读者自行阅读《Embedded Peripherals IP User Guide》 手册中第 8 节的内容。 这里仅对其几个与编程重点相关的寄存器进行说明。

偏移名称读写属性寄存器描述
0rxdata只读接收数据寄存器, 接收器每接收到一个字符的数据就会自动存入该寄存器。 根据数据位的配置的不同,该寄存器的有效位宽为 7、 8 或 9 位
1txdata只写发送数据寄存器,将数据写入该寄存器, 发送器会自动发送。 根据数据位的配置的不同,该寄存器的有效位宽为 7、 8 或 9 位
2status可读可写状态寄存器,存储了 IP 工作中的各种状态
3control可读可写控制寄存器,可以设置各种条件的中断使能
4divisor可读可写波特率分频寄存器, 在使能了可编程波特率的情况下, 通过修改该寄存器的值,可以修改通信波特率
5endofpacket可读可写在使能了流控功能的情况下,设置流结尾的数据值

0-rxdata:对于接收数据寄存器, 根据在添加控制器时配置的数据位宽不同,该寄存器的有效位数为 7、 8、 9 三种情况。 UART 接收模块每接收到一个字符的数据就会自动存入该寄存器。需要注意的是,存入该寄存器的数据必须由Avalon MM 主机及时读取, 否则当新接收完一个字符的数据后,该数据会被覆盖。

1-txdata: 对于发送数据寄存器, 根据在添加控制器时配置的数据位宽不同,该寄存器的有效位数为 7、 8、 9 三种情况。当该寄存器为空时, Avalon MM 主机向该寄存器中写入一个字符的数据,则该数据会被传入 UART 发送模块并发送出去。 当该寄存器中不为空时, 向该寄存器写入数据会导致前一个数据的丢失.

2-status: 状态寄存器,该寄存器存储了 UART 控制器运行时的各种状态, 每一位代表了一个特殊状态的值,在设计该控制器的应用程序时,比较重要的两个状态位为 bit7 的 rrdy 状态和 bit6 的 trdy 状态。

  • bit7-rrdy 状态位指示了 rxdata 寄存器的当前状态,当 rxdata 寄存器为空时,表明还没有接收到有效数据,则此时 Avalon MM 主机不能去读取 rxdata 寄存器中的值, rrdy 位的值为 0, 当 rxdata 寄存器中存储了有效的接收数据时,该位自动置1因此 Avalon MM 主机可以通过读取该位的值,来判断 rxdata 寄存器中是否有有效数据可读,如果 rrdy 位为 1, 则 Avalon MM 主机应尽快将 rxdata 中的值读取掉。
  • bit6-trdy 状态位指示了发送寄存器 txdata 的状态,当发送寄存器为空时,Avalon MM 主机可以向该寄存器中写入新的需要发送的数据, 此时 trdy 位的值为 1。 而一旦 txdata 寄存器中被写入了新的数据,则该位变为 0。因此当 AvalonMM 主机检测到该位为 0 时, 不能向 txdata 寄存器中写入新的值,只有当该位为 1 时,才能写入新的要发送的值.

3-control: 控制寄存器,该寄存器随名为控制寄存器, 但更像一个中断屏蔽寄存器, 在 bit0~bit12 中,除了 bit11 是 RTS 信号控制位以外,其他每一位都对应了状态寄存器中一位状态的中断使能信号。 一旦本寄存器中某一位被设置为了 1,那么当 status 寄存器中对应位变为 1 时,就会向 cpu 发出中断。

4-divisor:波特率分频寄存器, 该寄存器是否存在,与 Platform Designer 中添加 IP 核时是否勾选固定波特率选项有关,如果勾选了固定波特率选项,则该寄存器不存在,只有在没勾选固定波特率选项时, Avalon MM 主机才能通过写该寄存器的值来修改 UART 收发的波特率。 波特率值的计算关系为:

baud_rate = (clock_frequency) / (divisor + 1)

其中 baud_rate 为期望设定的波特率的值, clock_frequency 为 UART 串口模块的输入时钟频率。由此可以得出 divisor 更准确的计算公式为:

divisor  = int((clock_frequency/baud_rate) + 0.5)

三、UART IP 核应用实例

通过对上述寄存器的解读, 了解了每个寄存器及其中各个位的功能意义。然后就可以据此来编写相关的数据收发代码了。

3.1、在 DS-5 中建立 UART 应用工程

要设计 UART IP 应用工程,第一步是在 DS-5 软件中创建工程。在 DS-5 软件中选中上一节创建的 pio 工程, 复制并粘贴为新的工程,命名为 fpga_uart,以建立好基本的工程。 在复制完成之后,先将新工程下的 Debug 目录删除,因为这些文件是之前工程编译出来的,在新工程中既不会被使用,也不会被自动删除,因此需要手动删除。 避免与新工程的生成文件混淆。

直接复制已有工程并重新命名以得到新工程最大的优点是可以直接使用已有工程的设置,而不用再新建工程后再进行一系列的硬件设置,例如添加和包含 HPS 库文件等。

3.2、虚拟地址映射

第二步是完成虚拟地址映射。映射方式和 led_pio 外设一致, 可以直接在 PIO 实验中以有代码的基础上添加 uart_0 的部分即可,代码如下所示:

static volatile unsigned long *led_pio_virtual_base = NULL;	//led_pio虚拟地址
static volatile unsigned long *button_pio_virtual_base = NULL;	//button_pio虚拟地址
static volatile unsigned long *uart_0_virtual_base = NULL;	//uart_0虚拟地址

int fpga_init(long int *virtual_base) {
	int fd;
	void *periph_virtual_base;	//外设空间虚拟地址

	//打开MPU
	if ((fd = open("/dev/mem", ( O_RDWR | O_SYNC))) == -1) {
		printf("ERROR: could not open \"/dev/mem\"...\n");
		return (1);
	}

	//将外设地址段映射到用户空间
	periph_virtual_base = mmap( NULL, HW_REGS_SPAN, ( PROT_READ | PROT_WRITE),
	MAP_SHARED, fd, HW_REGS_BASE);
	if (periph_virtual_base == MAP_FAILED) {
		printf("ERROR: mmap() failed...\n");
		close(fd);
		return (1);
	}

	//映射得到led_pio外设虚拟地址
	led_pio_virtual_base = periph_virtual_base
			+ ((unsigned long) ( ALT_LWFPGASLVS_OFST + LED_PIO_BASE)
					& (unsigned long) ( HW_REGS_MASK));
	//映射得到button_pio外设虚拟地址
	button_pio_virtual_base = periph_virtual_base
			+ ((unsigned long) ( ALT_LWFPGASLVS_OFST + BUTTON_PIO_BASE)
					& (unsigned long) ( HW_REGS_MASK));
	//映射得到uart_0外设虚拟地址
	uart_0_virtual_base = periph_virtual_base
			+ ((unsigned long) ( ALT_LWFPGASLVS_OFST + UART_0_BASE)
					& (unsigned long) ( HW_REGS_MASK));
	*virtual_base = periph_virtual_base;	//将外设虚拟地址保存,用以释放时候使用
	return fd;
}

可以看到,代码中仅仅是在 PIO 核实验的代码基础上,新增定义了一个uart_0_virtual_base 指针,并在进行外设虚拟地址映射时增加了uart_0_virtual_base 一项的计算赋值。 所以,通过这两个实验中虚拟地址映射的对比可以知道, 当程序中需要得到多个外设的虚拟地址时,仅需先打开 MPU,然后得到外设地址空间的基地址 periph_virtual_base, 然后再依次将所需用到的外设的虚拟地址通过与 periph_virtual_base 执行相应运算得到。 无需对每个外设重新执行打开 MPU 和映射外设虚拟地址的操作。

3.3、设置波特率

在完成了 uart_0 的虚拟地址映射后, 要使用 UART IP 核进行正确的数据收发,首先需要设置相应的收发波特率, 当添加 UART IP 核时没有选择固定波特率选项时,就可以通过写 divisor 的值来实现。 例如,设置波特率为 9600bps,就可以使用下面的代码来实现:

//设置 uart_0 的波特率为 9600bps
*(uart_0_virtual_base + 4) = (int)(UART_0_FREQ/9600 + 0.5);

其中 UART_0_FREQ 是从外设信息头文件 hps_0.h 中得到的,这是 UART控制器的 Avalon MM 总线所使用的时钟频率。

由于基于虚拟地址映射的操作是在用户空间完成对各种外设的操作,而用户空间是无法进行中断的注册和使用的,因此本实验中不使用中断功能,直接使用查询状态寄存器的形式完成数据的收发。 所以对于 control 寄存器,无需进行任何设置,使用默认的全 0 值即可。

3.4、字符发送

串口发送数据是以基本的字符为单位的,最底层的操作一般就是 putc 函数来发送一个字符,然后上层再循环调用该函数来完成数据串的发送。 使用UART 控制器发送一个字符,基本思路是循环读取状态寄存器的值,一旦检测到状态寄存器中的 trdy 值为 1, 表明 txdata 寄存器可以写入新的待发送数据了,就将需要发送的新的数据写入 txdata 寄存器即可。循环读取状态寄存器并发送数据的代码如下所示:

//串口字符发送函数
void uart_putc(char c) {
	unsigned short uart_status;	//状态寄存器值
	do {
		uart_status = *(uart_0_virtual_base + 2);	//读取状态寄存器
	} while (!(uart_status & 0x40));	//等待状态寄存器bit6(trdy)为1

	*(uart_0_virtual_base + 1) = c;	//发送一个字符
}

代码中使用了一个 do{}while()的循环结构来循环读取 uart_0 的状态寄存器, 并检测其中 trdy 位(bit6) 的值,一旦该位为 1, 表明 txdata 寄存器可以被写入新的数据了,然后跳出循环, 使用指针的形式将一个数据写入 UART 控制器的 txdata 寄存器。

3.5、字符串发送

有了基本的字符发送函数, 就可以实现字符串发送了。 基本的字符串发送函数设计思路很简单,只需要持续检测待发送的数据是否为空字符,如果为空字符表明一串字符串发送结束, 退出函数。如果非空字符,则发送当前指针指向的字符,然后指针递增 1。简单的字符串发送函数如下所示:

//串口字符串发送函数
void uart_printf(char *str) {
	while (*str != '\0')	//检测当前指针指向的数是否为空字符
	{
		uart_putc(*str);	//发送一个字符
		str++;	//字符串指针+1
	}
}

使用该函数发送字符串就很简单了, 只需调用该函数并将需要发送的字符串作为参数传入即可,例如使用该发送函数发送“Hello World!” 的代码如下所示:

uart_printf("Hello World!\n"); //打印 hello world 字符

3.6、字符接收

串口接收数据也是以字符为基本单位的, 当需要从串口接收一个字符的数据时,最底层的函数一般就是 getc 函数,然后上层再循环调用该函数来完成数据串的接收。 使用 UART 控制器接收一个字符,基本思路是循环读取状态寄存器的值,一旦检测到状态寄存器中的 rrdy 值为 1, 表明 rxdata 寄存器中有新的数据可以读取了,就将 rxdata 寄存器中的数据读取出来,返回给上层函数。循环读取状态寄存器并读取数据的代码如下所示:

//串口字符接收函数
int uart_getc(void) {
	unsigned short uart_status;	//状态寄存器值
	do {
		uart_status = *(uart_0_virtual_base + 2);	//读取状态寄存器
	} while (!(uart_status & 0x80));	//等待状态寄存器bit7(rrdy)为1

	return *(uart_0_virtual_base + 0);	//读取一个字符并作为函数返回值返回
}

代码中使用了一个 do{}while()的循环结构来循环读取 uart_0 的状态寄存器, 并检测其中 rrdy 位(bit7) 的值,一旦该位为 1, 表明 rxdata 寄存器中有新的数据可以读取了,然后跳出循环, 使用指针的形式读取 rxdata 寄存器中的值并将该值作为函数返回值返回给上层函数。

3.7、字符串接收

有了基本的字符接收函数, 就可以实现字符串接收了。 基本的字符串接收函数设计思路很简单, 使用 uart_getc()函数从串口读取一个字符存入接收缓存指针指向的地址,并检测新接受到的数据是否为换行符“\n”,如果为换行符表明一行字符串接收结束, 退出函数, 返回当前接收到的字符个数。如果换行符,则继续读取新的数据, 而且接收缓存指针递增 1。简单的字符串接收函数如下所示:

//串口字符串接收函数
int uart_scanf(char *p) {
	int cnt = 0;	//接收个数计数器
	while (1) {
		*p = uart_getc();	//读取一个字符的数据
		cnt++;
		if (*p == '\n')	//判断数据是否为换行
			return cnt;	//换行则停止计数,返回当前接收的字符个数
		else
			p++;	//接收指针增1
	}
}

使用该函数接收字符串非常简单, 只需调用该函数并将接收缓存的指针作为参数传入即可,例如使用该接收函数接收一行数据并存入 rx_buf 数组的代码如下所示:

char rx_buf[128]={0}; //定义一个 128 字节的接收数组
memset(rx_buf,0,128); //清除数组中内容
uart_scanf(&rx_buf); //接收一行数据到 rx_buf 中
printf(rx_buf); //打印当前接收到的字符串内容

程序首先定义了一个 128 字节的数组,然后使用 memset()函数清除了数组中的值, 接着调用 uart_scanf()函数读取一行字符串到 rx_buf 中。 最后使用系统printf()函数将 rx_buf 中的字符串内容打印出来。 注意,使用了 memset()函数,需要包含头文件 string.h。

3.8、整个应用程序

//gcc标准头文件
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>

//hps 厂家提供的底层定义头文件
#define soc_cv_av	//定义使用soc_cv_av硬件平台

#include "hwlib.h"
#include "socal/socal.h"
#include "socal/hps.h"

//与用户具体HPS应用系统相关的硬件描述头文件
#include "hps_0.h"

#define HW_REGS_BASE (ALT_STM_OFST )	//HPS外设地址段基地址
#define HW_REGS_SPAN (0x04000000 )		//HPS外设地址段地址空间
#define HW_REGS_MASK (HW_REGS_SPAN - 1 )	//HPS外设地址段地址掩码

static volatile unsigned long *led_pio_virtual_base = NULL;	//led_pio虚拟地址
static volatile unsigned long *button_pio_virtual_base = NULL;	//button_pio虚拟地址
static volatile unsigned long *uart_0_virtual_base = NULL;	//uart_0虚拟地址

int fpga_init(long int *virtual_base) {
	int fd;
	void *periph_virtual_base;	//外设空间虚拟地址

	//打开MPU
	if ((fd = open("/dev/mem", ( O_RDWR | O_SYNC))) == -1) {
		printf("ERROR: could not open \"/dev/mem\"...\n");
		return (1);
	}

	//将外设地址段映射到用户空间
	periph_virtual_base = mmap( NULL, HW_REGS_SPAN, ( PROT_READ | PROT_WRITE),
	MAP_SHARED, fd, HW_REGS_BASE);
	if (periph_virtual_base == MAP_FAILED) {
		printf("ERROR: mmap() failed...\n");
		close(fd);
		return (1);
	}

	//映射得到led_pio外设虚拟地址
	led_pio_virtual_base = periph_virtual_base
			+ ((unsigned long) ( ALT_LWFPGASLVS_OFST + LED_PIO_BASE)
					& (unsigned long) ( HW_REGS_MASK));
	//映射得到button_pio外设虚拟地址
	button_pio_virtual_base = periph_virtual_base
			+ ((unsigned long) ( ALT_LWFPGASLVS_OFST + BUTTON_PIO_BASE)
					& (unsigned long) ( HW_REGS_MASK));
	//映射得到uart_0外设虚拟地址
	uart_0_virtual_base = periph_virtual_base
			+ ((unsigned long) ( ALT_LWFPGASLVS_OFST + UART_0_BASE)
					& (unsigned long) ( HW_REGS_MASK));
	*virtual_base = periph_virtual_base;	//将外设虚拟地址保存,用以释放时候使用
	return fd;
}

//串口字符发送函数
void uart_putc(char c) {
	unsigned short uart_status;	//状态寄存器值
	do {
		uart_status = *(uart_0_virtual_base + 2);	//读取状态寄存器
	} while (!(uart_status & 0x40));	//等待状态寄存器bit6(trdy)为1

	*(uart_0_virtual_base + 1) = c;	//发送一个字符
}

//串口字符串发送函数
void uart_printf(char *str) {
	while (*str != '\0')	//检测当前指针指向的数是否为空字符
	{
		uart_putc(*str);	//发送一个字符
		str++;	//字符串指针+1
	}
}

//串口字符接收函数
int uart_getc(void) {
	unsigned short uart_status;	//状态寄存器值
	do {
		uart_status = *(uart_0_virtual_base + 2);	//读取状态寄存器
	} while (!(uart_status & 0x80));	//等待状态寄存器bit7(rrdy)为1

	return *(uart_0_virtual_base + 0);	//读取一个字符并作为函数返回值返回
}

//串口字符串接收函数
int uart_scanf(char *p) {
	int cnt = 0;	//接收个数计数器
	while (1) {
		*p = uart_getc();	//读取一个字符的数据
		cnt++;
		if (*p == '\n')	//判断数据是否为换行
			return cnt;	//换行则停止计数,返回当前接收的字符个数
		else
			p++;	//接收指针增1
	}
}

int main(int argc, char ** argv) {

	int fd;
	int virtual_base = 0;	//虚拟基地址

	//完成fpga侧外设虚拟地址映射
	fd = fpga_init(&virtual_base);

	//设置uart_0的波特率为9600bps
	*(uart_0_virtual_base + 4) = (int) (UART_0_FREQ / 9600 + 0.5);

	uart_printf("Hello World!\n");	//打印hello world!字符串
	uart_printf("Hello SoC FPGA!\n");	//打印Hello SoC FPGA!字符串
	uart_printf("www.corecourse.cn\n");	//打印www.corecourse.cn字符串

	char rx_buf[128] = { 0 };	//定义一个128字节的接收数组
	memset(rx_buf, 0, 128);	//清除数组中内容
	uart_scanf(&rx_buf);	//接收一行数据到rx_buf中
	printf(rx_buf);	//打印当前接收到的字符串内容

	//程序退出前,取消虚拟地址映射
	if (munmap(virtual_base, HW_REGS_SPAN) != 0) {
		printf("ERROR: munmap() failed...\n");
		close(fd);
		return (1);
	}

	close(fd); //关闭MPU
	return 0;
}

3.9、运行程序

在进行试验时,由于 AC501_SoC_GHRD 中 uart_0 外设的引脚是分配到了GPIO0 上的 FPGA_GPIO0_D6 和 FPGA_GPIO0_D7,为了能够进行串口调试看到对应的现象,需要使用串口调试模块来配合调试,例如常见的 USB 转 TTL串口模块。 例如对于一个常见的基于 CH340 的 USB 转 TTL 串口模块, 按照如下图所示的连接方式进行连接:

如何知道uart_0的TX RX 对应GPIO引脚呢?

打开 AC501_SoC_GHRD 工程,然后点击下图所示选项
在这里插入图片描述
而后在下方找到fpga_uart0_rxdfpga_uart0_txd对应的Location,从下图看分别是PINAB20PIN_Y19
在这里插入图片描述
然后,在用户手册中,找到管脚分配如下图,进而得出uart_0的RX和TX分别对应GPIO0 上的 FPGA_GPIO0_D6 和 FPGA_GPIO0_D7!
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

实验时, 将该 USB 转串口模块连接到 PC 的 USB 端口,通过设备管理器查看到准确的串口号之后, 打开串口调试助手,设置发送和接收均为 ASCII 格式,波特率为 9600,然后打开该端口号。将 DS-5 中编译好的 fpga_uart 可执行文件使用 DS-5 中的 SSH 工具或者 winSCP 工具将其拷贝到开发板中, 使用“chmod+x fpga_uart”命令为其添加可执行权限后, 在开发板的终端窗口中输入“./fpga_uart” 命令运行该程序,就可以在串口调试软件中看到打印的以下内容了

Hello World!Hello SoC FPGA!www.corecourse.cn

在串口调试助手的发送窗口中输入一串字符串并发送,例如输入“Hello,AC501-SoC”, 按下回车键后点击发送。 在开发板的终端窗口中就可以看到系统打印的接收到的数据内容了,也为“Hello, AC501-SoC”, 如下图所示。需要注意的是,输入完字符串后一定得加入换行后再发送,不然程序将无法正确正确识别字符串的结束。

在这里插入图片描述

四、总结

本节实验通过 uart_0 外设的编程实验,进一步复习了基于虚拟地址方式操作外设寄存器的方法,同时给出了简单的 uart 串口编程实例。由于无法使用中断,因此在同时兼顾发送和接收上存在一定的难点,本节内容并未针对该为内容做探讨。本节实验中的代码适合点对点一主一从式的简单数据收发,如需完整的串口应用功能,建议使用 Linux 提供的该 IP 核的内核驱动来实现。 使用内核驱动程序来控制该串口的相关内容,将在后续章节讲到。

©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页