TD工程介绍
我们提供的TD工程里的RISC-V核默认就开启了GPIO UART扩展,可以看到还有SPI和I2C扩展。因此后面的实验中TD的工程我们基本不怎么修改TD的内容,只需要修改TD工具中Soc_Top.v文件中的TCM0_INITFILE为FD生成的固件名称即可,主要修我以为都是在FD工程进行修改即可。
FD工程介绍
这个工程我们主要演示了如何使用UART及UART中断,如何使用定时器中断,以及如何使用GPIO和GPIO中断。
FD工程导入
打开FD选择workspace为 demo_riscv\wkspace 就会自动导入该workspace下的所有项目。
由于你的FD的目录与我的FD目录不在同一个位置,会导致编译器工具链的找不着,我们需要在工程上右键选择Reset Project Toolchain,
执行完这个操作就FD会重新修正工程的工具链配置。
FD工程源代码分析
头文件、宏定义及全局变量介绍
头文件
#include "core.h" // 核心头文件
//#define USE_MTIME // 不使用MTIME
#include "uart.h" // UART 头文件
#include "gpio.h" // GPIO 头文件
#include "anl_printf.h" // 打印库头文件
#include "interrupt.h" // 中断库头文件
#ifdef USE_MTIME
#include "mtime.h" // 如果使用 MTIME,则引入 MTIME 头文件
#else
#include "systick.h" // 否则引入 SysTick 头文件
#endif
-
#include "core.h"
:这个头文件是系统的核心头文件,通常包含了与硬件相关的低级别配置和宏定义。它可能包含了处理器寄存器的定义、中断控制器配置等内容。 -
#include "uart.h"
:这个头文件包含了 UART(通用异步收发传输)相关的函数声明和宏定义。 -
#include "gpio.h"
:这个头文件包含了 GPIO(通用输入输出)相关的函数声明和宏定义。 -
#include "anl_printf.h"
:这个头文件包含了打印函数的声明和定义。 -
#include "interrupt.h"
:这个头文件包含了中断控制器相关的函数声明和宏定义。 -
#ifdef USE_MTIME
:这是一个条件编译指令,用于根据是否定义了USE_MTIME
宏来选择性地包含不同的头文件内容。 -
#include "mtime.h"
:如果定义了USE_MTIME
宏,就会包含这个头文件。它可能包含了 MTIME(Machine Timer)相关的函数声明和宏定义,用于配置和操作处理器的计时器。 -
#include "systick.h"
:如果没有定义USE_MTIME
宏,就会包含这个头文件。它可能包含了 SysTick 定时器相关的函数声明和宏定义,用于配置和操作 SysTick 定时器。 - Systick 和 MTIME 定时器的区别主要是其资源占用和操作模式。 Systick 是一个自动重装的 30bit 定时器,用户需要使用 SetSystickCfg()函数启用定时器,并在定时器中断处理程序中使用 ClrSystickInt()函数清除掉挂起的定时器状态对于 MTIME 定时器而言,它的初始化和中断响应都是使用 SetMtimeCmp()函数将下一时钟节拍的计数值装入定时器比较值中。
全局变量sys_banner
static char sys_banner[] = {"- Anlogic eMCU buildtime [" __TIME__" " __DATE__ "] " "rev 1.0 \r\n"};
这行代码定义了一个静态字符数组 sys_banner
,用于存储系统的横幅信息。该信息包含了编译时间和日期,以及版本号。
__TIME__
:编译时的时间,格式为 HH:MM:SS。__DATE__
:编译时的日期,格式为 MMM DD YYYY(月份、日期、年份)。
这两个宏是编译器提供的预定义宏,在编译时会被替换为当前的编译时间和日期。在这里,它们被用于构建一个包含编译时间和日期的字符串。
宏定义
//THIS DEMO IS FOR SYSTICK SYSTEM
#define SYS_FREQ 80000000 // 系统频率
#define UART_BAUD 115200 // UART 波特率
#define GPIO_INTSRC 0x04 // GPIO 中断源
#define UART1_INTSRC 0x01 // UART1 中断源
#define TIM_TRIG_FREQ 1 // 定时器触发频率,每秒1次
这段代码是一些预定义的常量和注释,用于描述程序的一些基本参数和特性:
-
// THIS DEMO IS FOR SYSTICK SYSTEM
:这是一个注释,用于说明这段代码是针对 SysTick 系统设计的演示程序。 -
#define SYS_FREQ 80000000
:这是一个宏定义,表示系统的频率为 80MHz。这个频率用于配置定时器、UART 通信等时序相关的功能。 -
#define UART_BAUD 115200
:这是一个宏定义,表示 UART 的波特率为 115200。波特率是串行通信中表示数据传输速率的参数。 -
#define GPIO_INTSRC 0x04
:这是一个宏定义,表示 GPIO 中断源的值为 0x04。在某些系统中,GPIO 的中断可以被多个源触发,此处定义了其中一个源的标识值。 -
#define UART1_INTSRC 0x01
:这是一个宏定义,表示 UART1 中断源的值为 0x01。类似于上面的 GPIO 中断源,这里定义了 UART1 中断的标识值。 -
#define TIM_TRIG_FREQ 1
:这是一个宏定义,表示定时器的触发频率为每秒 1 次。这个参数用于配置定时器中断的触发频率,可以根据需要进行调整。
函数说明
main()函数
- 先配置UART
// 配置 UART 参数
Uart_Config UART1_Cfg;
UART1_Cfg.BaudDivider=(SYS_FREQ/UART_BAUD);
UART1_Cfg.IntEnable=True;
UART1_Cfg.Event_ParityCheckFail=False;
UART1_Cfg.Event_RxFifoHalfFull=False;
UART1_Cfg.Event_TxFifoHalfEmpty=False;
UART1_Cfg.Event_RxBufFull=True;
UART1_Cfg.Event_TxBufEmpty=False;
UART1_Cfg.RxFifoEnable=False;
UART1_Cfg.TxFifoEnable=True;
UART1_Cfg.Parity=NONE;
UART1_Cfg.Stop=ONE;
uart_applyConfig(UART,&UART1_Cfg);
这段代码是配置 UART 参数的过程,具体的配置包括:
BaudDivider
:波特率分频器,根据系统时钟频率SYS_FREQ
和波特率UART_BAUD
计算得出。IntEnable
:使能 UART 中断。Event_ParityCheckFail
:奇偶校验失败事件的处理,设置为False
表示不处理。Event_RxFifoHalfFull
:接收 FIFO 缓冲区半满事件的处理,设置为False
表示不处理。Event_TxFifoHalfEmpty
:发送 FIFO 缓冲区半空事件的处理,设置为False
表示不处理。Event_RxBufFull
:接收缓冲区满事件的处理,设置为True
表示处理。Event_TxBufEmpty
:发送缓冲区空事件的处理,设置为False
表示不处理。RxFifoEnable
:使能接收 FIFO 缓冲区,设置为False
表示不使用 FIFO 缓冲区。TxFifoEnable
:使能发送 FIFO 缓冲区,设置为True
表示使用 FIFO 缓冲区。Parity
:奇偶校验类型,设置为NONE
表示不使用奇偶校验。Stop
:停止位数,设置为ONE
表示一个停止位。
最后,通过 uart_applyConfig
函数将以上配置应用到 UART 上。
- 配置完后打印系统信息
// 打印系统信息
anl_printf("---------------------------------------------------------------- \r\n");
anl_printf("- SoftCore Demo : RISCV(Freq 80MHz. 32kB RAM. Full Feature) \r\n");
anl_printf("- Hardware Platform : EG4S20NG88 PotatoPie V4.0 \r\n");
anl_printf("- TD Ver. : TD 5.6.4(97693) \r\n");
anl_printf("- OS+Build+Debug : Windows, OpenOCD + Riscv-none-embed. \r\n");
anl_printf("- Test Case : UART Interrupt Demo \r\n");
anl_printf(sys_banner);
anl_printf("---------------------------------------------------------------- \r\n");
这段代码使用 anl_printf
函数打印了一段系统信息。让我们来逐行解释:
-
anl_printf("---------------------------------------------------------------- \r\n");
打印了一条分隔线,用于美观和区分信息的开头。
-
anl_printf("- SoftCore Demo : RISCV(Freq 80MHz. 32kB RAM. Full Feature) \r\n");
打印了系统的软核演示信息,包括软核类型(RISCV)、频率(80MHz)、RAM 大小(32kB)和功能(全功能)。
-
anl_printf("- Hardware Platform : EG4S20NG88 PotatoPie V4.0 \r\n");
打印了硬件平台信息,包括硬件平台型号(EG4S20NG88)和版本(PotatoPie V4.0)。
-
anl_printf("- TD Ver. : TD 5.6.4(97693) \r\n");
打印了测试开发工具的版本信息,包括版本号(TD 5.6.4)和编译号(97693)。
-
anl_printf("- OS+Build+Debug : Windows, OpenOCD + Riscv-none-embed. \r\n");
打印了操作系统、构建环境和调试工具的信息,包括操作系统类型(Windows)和使用的调试工具(OpenOCD + Riscv-none-embed)。
-
anl_printf("- Test Case : UART & GPIO Interrupt, SPI Master Demo \r\n");
打印了测试用例的信息,包括测试的内容(UART & GPIO 中断、SPI 主控演示)。
-
anl_printf(sys_banner);
打印了之前定义的
sys_banner
字符串,其中包含了编译时间和日期的信息。 -
anl_printf("---------------------------------------------------------------- \r\n");
再次打印了一条分隔线,用于美观和区分信息的结尾。
SetSystickCfg((SYS_FREQ/TIM_TRIG_FREQ),True);
ClrSystickInt();
这两行代码用于配置和清除 SysTick 定时器的设置:
-
SetSystickCfg((SYS_FREQ/TIM_TRIG_FREQ),True);
:- 这行代码调用了
SetSystickCfg
函数,用于设置 SysTick 定时器的配置。 - 第一个参数
(SYS_FREQ/TIM_TRIG_FREQ)
表示每秒钟 SysTick 定时器的时钟周期数,它是系统频率SYS_FREQ
除以定时器触发频率TIM_TRIG_FREQ
的结果。 - 第二个参数
True
表示启用 SysTick 定时器。
- 这行代码调用了
-
ClrSystickInt();
:- 这行代码调用了
ClrSystickInt
函数,用于清除 SysTick 中断标志位。 - 这样做是为了确保在进入主循环之前,任何可能触发的 SysTick 中断都被处理,以免影响程序的正常执行。
- 这行代码调用了
// 设置中断掩码
SetIntMask(UART1_INTSRC | GPIO_INTSRC);
uart_ClrEvent(UART,0xFF); //清除全部UART中断
ClrIntEvent(0xFFFFFFFF); // 清除所有中断
GPIO_A->OUTPUT_ENABLE=0x0000FFFF;
GPIO_A->GPIO_INTMASK=0xFFFFFFFF; // 允许所有 GPIO 发送中断
这段代码用于配置中断掩码和清除所有中断:
-
SetIntMask(UART1_INTSRC | GPIO_INTSRC);
:- 这行代码调用了
SetIntMask
函数,用于设置中断掩码。通过将UART1_INTSRC
和GPIO_INTSRC
按位或运算,将 UART1 和 GPIO 的中断源加入到中断掩码中。 - 这样做的目的是指定哪些中断将被允许触发。
- 这行代码调用了
-
uart_ClrEvent(UART, 0xFF);
:- 这行代码调用了
uart_ClrEvent
函数,清除了 UART 的所有中断事件。 - 第一个参数
UART
指定了要清除中断事件的 UART 模块。 - 第二个参数
0xFF
表示清除所有类型的 UART 中断事件。
- 这行代码调用了
-
ClrIntEvent(0xFFFFFFFF);
:- 这行代码调用了
ClrIntEvent
函数,清除了所有的中断事件。 - 传递的参数
0xFFFFFFFF
表示清除所有中断事件。
- 这行代码调用了
-
GPIO_A->OUTPUT_ENABLE = 0x0000FFFF;
:- 这行代码设置了 GPIO_A 模块的输出使能寄存器,使得 GPIO_A 的低 16 位引脚成为输出引脚。
-
GPIO_A->GPIO_INTMASK = 0xFFFFFFFF;
:- 这行代码设置了 GPIO_A 模块的中断掩码寄存器,允许所有 GPIO_A 引脚产生中断。
while (1)
{
GPIO_A->OUTPUT=i;
i=i+1;
Delay(5000000);
anl_printf("Hello Main!\r\n");
}
这个 while
循环是程序的主循环,它会不断地执行以下操作:
-
GPIO_A->OUTPUT=i;
:- 将变量
i
的值写入 GPIO_A 模块的输出寄存器中,控制 GPIO_A 的输出状态。每次循环迭代,i
的值会递增。 - 这个操作会改变 GPIO_A 引脚的输出状态,具体的改变依赖于
i
的值。
- 将变量
-
i=i+1;
:- 递增变量
i
的值,用于改变 GPIO_A 输出状态的控制值。
- 递增变量
-
Delay(5000000);
:- 调用
Delay
函数,使程序停顿一段时间。在这里,函数的参数5000000
表示延时的周期数,具体延时时长由系统时钟频率决定。
- 调用
-
anl_printf("Hello Main!\r\n");
:- 打印字符串 “Hello Main!\r\n” 到串行终端,向用户输出一条消息。
这样,循环将一直重复执行上述步骤,不断改变 GPIO_A 的输出状态,延时一段时间,然后打印一条消息,形成一个周期性的操作。
其它函数
void Delay(int cycle)
{
int i;
for (i=0;i<cycle;i++)
asm volatile(
"add x0, x0, x0"
);
}
这个Delay
函数是一个简单的延时循环,它通过重复执行一个无操作的操作add x0, x0, x0
来等待指定数量的周期。asm volatile
语句用于将汇编语言直接嵌入到C代码中,提供一种执行可能无法直接用C表达的机器指令的方法。
它的功能说明:
for
循环执行cycle
次。- 在循环内部,它执行一个汇编指令,将寄存器
x0
的内容加到自身,并将结果存回x0
。 - 由于这是一个无操作指令(它将一个值加到自身,实际上什么都没做),它实际上是一个消耗CPU周期的循环,没有执行任何有意义的计算。
这个函数可用于在时间敏感的应用程序中创建延时,例如控制某些操作的时序。然而,需要注意的是,延迟的持续时间取决于各种因素,包括处理器速度和优化设置,因此它可能不太精确,也不适用于不同平台。
// 定时器中断服务函数
void TimerISP()
{
#ifdef USE_MTIME
uint64_t mtime_calc;
mtime_calc=GetMTimeCnt()+(SYS_FREQ/TIM_TRIG_FREQ);
SetMTimeCmp(mtime_calc);
#else
ClrSystickInt();
#endif
GPIO_A->OUTPUT = ~(GPIO_A->OUTPUT);
anl_printf("Hello Tick!\r\n");
return;
}
这是定时器中断服务函数 TimerISP
。根据编译时是否定义了 USE_MTIME
宏,函数会有不同的行为:
-
如果定义了
USE_MTIME
:- 函数首先声明了一个
uint64_t
类型的变量mtime_calc
,用于计算下一次定时器中断的触发时间。 - 调用
GetMTimeCnt()
函数获取当前的 MTIME 计数值,即当前时间。 - 根据系统频率
SYS_FREQ
和定时器触发频率TIM_TRIG_FREQ
计算下一次定时器中断触发的时间点,并将结果存储在mtime_calc
中。 - 调用
SetMTimeCmp
函数设置 MTIME 比较寄存器,使得下一次定时器中断在计算得到的时间点触发。 - 最后,将 GPIO_A 的输出取反,即翻转输出状态,并打印 “Hello Tick!\r\n” 消息。
- 函数首先声明了一个
-
如果未定义
USE_MTIME
:- 则调用
ClrSystickInt()
函数清除 SysTick 定时器中断标志。 - 接着,将 GPIO_A 的输出取反,即翻转输出状态,并打印 “Hello Tick!\r\n” 消息。
- 则调用
无论哪种情况,这个函数都用于定时器中断服务,每当定时器中断触发时,GPIO_A 的输出状态都会取反,并输出一条消息。
// UART1 中断服务函数
void UART1_ISP()
{
char recv_value;
while(uart_GetEvent(UART,UartRxVld))
{
uart_write(UART,uart_read(UART));
}
uart_ClrEvent(UART,UartRxBufFull | UartRxBufHfFull);
ClrIntEvent(UART1_INTSRC);
}
这是 UART1 的中断服务函数 UART1_ISP
。当 UART1 接收到数据时,该函数会执行以下操作:
-
声明一个
char
类型的变量recv_value
,用于存储接收到的数据。 -
使用
while
循环结构,调用uart_GetEvent
函数检查 UART 接收缓冲区是否有数据可用(即接收到有效数据)。如果接收到数据,则进入循环体。 -
在循环体内,调用
uart_read
函数读取 UART 接收缓冲区中的数据,并立即将其通过uart_write
函数写回 UART 发送缓冲区。这实现了一种称为回显(echo)的功能,即将接收到的数据原样发送回去。 -
循环继续,直到 UART 接收缓冲区中没有数据可用。
-
调用
uart_ClrEvent
函数清除 UART 接收缓冲区满和半满的标志位,以及清除 UART1 中断标志位。
总的来说,该函数实现了 UART1 接收数据并回显的功能,同时清除相关的中断标志位。
void GPIO_ISP()
{
anl_printf("Hello Click!\r\n");
ClrIntEvent(GPIO_INTSRC);
}
这是 GPIO 的中断服务函数 GPIO_ISP
。当 GPIO 触发中断时,该函数会执行以下操作:
-
使用
anl_printf
函数打印字符串 “Hello Click!\r\n”,提示发生了 GPIO 中断。 -
调用
ClrIntEvent
函数清除 GPIO 中断源(即GPIO_INTSRC
对应的中断标志位),以确认已处理完中断事件。
void ExternalISP()
{
uint32_t temp;
temp=GetIntEvent();
if(temp&UART1_INTSRC)
UART1_ISP();
if(temp&GPIO_INTSRC)
GPIO_ISP();
else
{
anl_printf("UNKNOWN INT SRC! SRC= 0x%x",temp);
while(1);
}
}
这是外部中断服务函数 ExternalISP
。当发生外部中断时,该函数会执行以下操作:
-
声明一个
uint32_t
类型的变量temp
,用于存储当前发生的中断事件。 -
调用
GetIntEvent
函数获取当前的中断事件。 -
使用条件语句检查中断事件的类型:
- 如果
temp
中包含UART1_INTSRC
标志位,则执行 UART1 的中断服务函数UART1_ISP
。 - 如果
temp
中包含GPIO_INTSRC
标志位,则执行 GPIO 的中断服务函数GPIO_ISP
。 - 如果
temp
中不包含以上任何一种中断源的标志位,则打印消息 “UNKNOWN INT SRC! SRC= 0x%x”,其中%x
是temp
的十六进制表示,表示未知的中断来源,并进入无限循环。
- 如果
工程源码在这个路径:
实验结果:
在 《PotatoPie 4.0 实验教程(40) —— FPGA实现RISC-V工程创建和调试》教程里我们知道如何创建,编译下载和调试RISCV工程,我们这里先只编译FD工程,而不用FD进行下载。
在TD中我们需要修改TD Soc_Top.v文件中的TCM0_INITFILE为,并重新编译TD工程。
parameter TCM0_INITFILE="../../../../../wkspace/hello_world/Debug/hello_world.bin.mif";
然后打开看串口助手之类的工具,设置串口波特率为115200,8N1模式。
最后后用TD工具的JTAG下载功能下载到FPGA之中,在串口工具中我们将看到如下输出:
同时可以看到板上蓝灯一秒一闪烁。
如果你的FPGA中已下载过之前的RISCV软核位流,那么可以也可以直接在FD中点击这个进行代码运行,而不用重新编译和下载FPGA程序。