PotatoPie 4.0 实验教程(41) —— FPGA实现RISC-V 扩展 GPIO UART Timer功能-Anlogic-安路社区-FPGA CPLD-ChipDebug

PotatoPie 4.0 实验教程(41) —— FPGA实现RISC-V 扩展 GPIO UART Timer功能

TD工程介绍

我们提供的TD工程里的RISC-V核默认就开启了GPIO UART扩展,可以看到还有SPI和I2C扩展。因此后面的实验中TD的工程我们基本不怎么修改TD的内容,只需要修改TD工具中Soc_Top.v文件中的TCM0_INITFILE为FD生成的固件名称即可,主要修我以为都是在FD工程进行修改即可。

20240424092340235-image

FD工程介绍

这个工程我们主要演示了如何使用UART及UART中断,如何使用定时器中断,以及如何使用GPIO和GPIO中断。

20240426200028886-image

FD工程导入

打开FD选择workspace为 demo_riscv\wkspace 就会自动导入该workspace下的所有项目。

20240424132640670-image

20240424132734751-image

由于你的FD的目录与我的FD目录不在同一个位置,会导致编译器工具链的找不着,我们需要在工程上右键选择Reset Project Toolchain

20240424132908514-image

执行完这个操作就FD会重新修正工程的工具链配置。

20240424133023351-image

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
  1. #include "core.h":这个头文件是系统的核心头文件,通常包含了与硬件相关的低级别配置和宏定义。它可能包含了处理器寄存器的定义、中断控制器配置等内容。

  2. #include "uart.h":这个头文件包含了 UART(通用异步收发传输)相关的函数声明和宏定义。

  3. #include "gpio.h":这个头文件包含了 GPIO(通用输入输出)相关的函数声明和宏定义。

  4. #include "anl_printf.h":这个头文件包含了打印函数的声明和定义。

  5. #include "interrupt.h":这个头文件包含了中断控制器相关的函数声明和宏定义。

  6. #ifdef USE_MTIME:这是一个条件编译指令,用于根据是否定义了 USE_MTIME 宏来选择性地包含不同的头文件内容。

  7. #include "mtime.h":如果定义了 USE_MTIME 宏,就会包含这个头文件。它可能包含了 MTIME(Machine Timer)相关的函数声明和宏定义,用于配置和操作处理器的计时器。

  8. #include "systick.h":如果没有定义 USE_MTIME 宏,就会包含这个头文件。它可能包含了 SysTick 定时器相关的函数声明和宏定义,用于配置和操作 SysTick 定时器。

  9. 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次

这段代码是一些预定义的常量和注释,用于描述程序的一些基本参数和特性:

  1. // THIS DEMO IS FOR SYSTICK SYSTEM:这是一个注释,用于说明这段代码是针对 SysTick 系统设计的演示程序。

  2. #define SYS_FREQ 80000000:这是一个宏定义,表示系统的频率为 80MHz。这个频率用于配置定时器、UART 通信等时序相关的功能。

  3. #define UART_BAUD 115200:这是一个宏定义,表示 UART 的波特率为 115200。波特率是串行通信中表示数据传输速率的参数。

  4. #define GPIO_INTSRC 0x04:这是一个宏定义,表示 GPIO 中断源的值为 0x04。在某些系统中,GPIO 的中断可以被多个源触发,此处定义了其中一个源的标识值。

  5. #define UART1_INTSRC 0x01:这是一个宏定义,表示 UART1 中断源的值为 0x01。类似于上面的 GPIO 中断源,这里定义了 UART1 中断的标识值。

  6. #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 定时器的设置:

  1. SetSystickCfg((SYS_FREQ/TIM_TRIG_FREQ),True);

    • 这行代码调用了 SetSystickCfg 函数,用于设置 SysTick 定时器的配置。
    • 第一个参数 (SYS_FREQ/TIM_TRIG_FREQ) 表示每秒钟 SysTick 定时器的时钟周期数,它是系统频率 SYS_FREQ 除以定时器触发频率 TIM_TRIG_FREQ 的结果。
    • 第二个参数 True 表示启用 SysTick 定时器。
  2. 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 发送中断

这段代码用于配置中断掩码和清除所有中断:

  1. SetIntMask(UART1_INTSRC | GPIO_INTSRC);

    • 这行代码调用了 SetIntMask 函数,用于设置中断掩码。通过将 UART1_INTSRCGPIO_INTSRC 按位或运算,将 UART1 和 GPIO 的中断源加入到中断掩码中。
    • 这样做的目的是指定哪些中断将被允许触发。
  2. uart_ClrEvent(UART, 0xFF);

    • 这行代码调用了 uart_ClrEvent 函数,清除了 UART 的所有中断事件。
    • 第一个参数 UART 指定了要清除中断事件的 UART 模块。
    • 第二个参数 0xFF 表示清除所有类型的 UART 中断事件。
  3. ClrIntEvent(0xFFFFFFFF);

    • 这行代码调用了 ClrIntEvent 函数,清除了所有的中断事件。
    • 传递的参数 0xFFFFFFFF 表示清除所有中断事件。
  4. GPIO_A->OUTPUT_ENABLE = 0x0000FFFF;

    • 这行代码设置了 GPIO_A 模块的输出使能寄存器,使得 GPIO_A 的低 16 位引脚成为输出引脚。
  5. 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 循环是程序的主循环,它会不断地执行以下操作:

  1. GPIO_A->OUTPUT=i;

    • 将变量 i 的值写入 GPIO_A 模块的输出寄存器中,控制 GPIO_A 的输出状态。每次循环迭代,i 的值会递增。
    • 这个操作会改变 GPIO_A 引脚的输出状态,具体的改变依赖于 i 的值。
  2. i=i+1;

    • 递增变量 i 的值,用于改变 GPIO_A 输出状态的控制值。
  3. Delay(5000000);

    • 调用 Delay 函数,使程序停顿一段时间。在这里,函数的参数 5000000 表示延时的周期数,具体延时时长由系统时钟频率决定。
  4. 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 宏,函数会有不同的行为:

  1. 如果定义了 USE_MTIME

    • 函数首先声明了一个 uint64_t 类型的变量 mtime_calc,用于计算下一次定时器中断的触发时间。
    • 调用 GetMTimeCnt() 函数获取当前的 MTIME 计数值,即当前时间。
    • 根据系统频率 SYS_FREQ 和定时器触发频率 TIM_TRIG_FREQ 计算下一次定时器中断触发的时间点,并将结果存储在 mtime_calc 中。
    • 调用 SetMTimeCmp 函数设置 MTIME 比较寄存器,使得下一次定时器中断在计算得到的时间点触发。
    • 最后,将 GPIO_A 的输出取反,即翻转输出状态,并打印 “Hello Tick!\r\n” 消息。
  2. 如果未定义 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 接收到数据时,该函数会执行以下操作:

  1. 声明一个 char 类型的变量 recv_value,用于存储接收到的数据。

  2. 使用 while 循环结构,调用 uart_GetEvent 函数检查 UART 接收缓冲区是否有数据可用(即接收到有效数据)。如果接收到数据,则进入循环体。

  3. 在循环体内,调用 uart_read 函数读取 UART 接收缓冲区中的数据,并立即将其通过 uart_write 函数写回 UART 发送缓冲区。这实现了一种称为回显(echo)的功能,即将接收到的数据原样发送回去。

  4. 循环继续,直到 UART 接收缓冲区中没有数据可用。

  5. 调用 uart_ClrEvent 函数清除 UART 接收缓冲区满和半满的标志位,以及清除 UART1 中断标志位。

总的来说,该函数实现了 UART1 接收数据并回显的功能,同时清除相关的中断标志位。

void GPIO_ISP()
{
anl_printf("Hello Click!\r\n");
ClrIntEvent(GPIO_INTSRC);
}

这是 GPIO 的中断服务函数 GPIO_ISP。当 GPIO 触发中断时,该函数会执行以下操作:

  1. 使用 anl_printf 函数打印字符串 “Hello Click!\r\n”,提示发生了 GPIO 中断。

  2. 调用 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。当发生外部中断时,该函数会执行以下操作:

  1. 声明一个 uint32_t 类型的变量 temp,用于存储当前发生的中断事件。

  2. 调用 GetIntEvent 函数获取当前的中断事件。

  3. 使用条件语句检查中断事件的类型:

    • 如果 temp 中包含 UART1_INTSRC 标志位,则执行 UART1 的中断服务函数 UART1_ISP
    • 如果 temp 中包含 GPIO_INTSRC 标志位,则执行 GPIO 的中断服务函数 GPIO_ISP
    • 如果 temp 中不包含以上任何一种中断源的标志位,则打印消息 “UNKNOWN INT SRC! SRC= 0x%x”,其中 %xtemp 的十六进制表示,表示未知的中断来源,并进入无限循环。

工程源码在这个路径:

20240424115928131-image

实验结果:

在 《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之中,在串口工具中我们将看到如下输出:

20240424093929278-image

同时可以看到板上蓝灯一秒一闪烁。

20240424094128629-image

如果你的FPGA中已下载过之前的RISCV软核位流,那么可以也可以直接在FD中点击这个进行代码运行,而不用重新编译和下载FPGA程序。

20240424102953331-image

请登录后发表评论

    没有回复内容