当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]在FPGA上构建RISC-V SoC时,从复位向量到串口打印“Hello World”的启动流程,是验证软核能否“自主呼吸”的关键。本文将基于常见的PicoRV32或VexRiscv软核,详解从硬件复位到软件驱动的完整链路,助你打通FPGA软核开发的“最后一公里”。



FPGA上构建RISC-V SoC时,从复位向量到串口打印“Hello World”的启动流程,是验证软核能否“自主呼吸”的关键。本文将基于常见的PicoRV32或VexRiscv软核,详解从硬件复位到软件驱动的完整链路,助你打通FPGA软核开发的“最后一公里”。


一、RISC-V SoC最小启动架构


一个能完成基础Boot流程的FPGA最小系统包含三个核心部件:


1.  RISC-V CPU Core:如PicoRV32,运行在50-100MHz。

2.  Boot ROM:FPGA片上BRAM实现,存放复位后执行的第一段代码(Bootloader)。

3.  UART 16550兼容控制器:挂载在总线上的串口IP,用于调试输出。


地址空间规划(典型):

-   0x0000_0000:Boot ROM(64KB)

•   0x1000_0000:UART 寄存器(内存映射I/O)


•   0x8000_0000:主内存(外部SRAM/DDR,可选)


二、Boot流程:从复位到C语言环境


RISC-V软核上电后,PC指针会跳转到复位向量(通常为0x0000_0000),启动流程分为三个阶段:


1. 汇编启动(start.s):搭建栈空间


在Boot ROM的起始位置,必须有一段汇编代码负责最基础的初始化。这段代码通常命名为start.s或crt0.s。

.section .text.start

.global _start

_start:

   # 1. 设置栈指针(SP),指向RAM顶端

   la sp, _stack_top

   

   # 2. 清零BSS段(清除未初始化全局变量)

   la t0, _bss_start

   la t1, _bss_end

   bge t0, t1, clear_bss_done

clear_bss_loop:

   sw zero, 0(t0)

   addi t0, t0, 4

   blt t0, t1, clear_bss_loop

clear_bss_done:

   

   # 3. 跳转到C语言的main函数

   call main

   

   # 4. 若main返回,则进入死循环

   j .



关键点:栈指针(SP)必须在调用C函数前正确设置,因为C函数依赖栈帧(Stack Frame)保存局部变量和返回地址。


2. 链接脚本(linker.ld):定义内存布局


链接脚本告诉编译器代码和数据应该放在哪里。对于Boot ROM,需要明确指定.text(代码段)从0x00000000开始。

MEMORY {

   ROM (rx) : ORIGIN = 0x00000000, LENGTH = 64K

   RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 256K

}


SECTIONS {

   .text : {

       *(.text.start)  /* 确保_start在最前面 */

       *(.text*)

   } > ROM

   

   .bss : {

       _bss_start = .;

       *(.bss*)

       _bss_end = .;

   } > RAM

   

   _stack_top = ORIGIN(RAM) + LENGTH(RAM); /* 栈顶指向RAM末尾 */

}



3. C语言入口(main.c):驱动初始化


当栈和BSS段准备就绪后,程序跳转到C语言的main函数,此时可以安全地初始化外设。

// 定义UART寄存器基地址(内存映射)

#define UART_BASE 0x10000000

#define UART_TX_DATA (*(volatile unsigned int*)(UART_BASE + 0x00))


void uart_putchar(char c) {

   while (/* 检查发送缓冲区是否满 */); // 轮询状态位

   UART_TX_DATA = c;

}


int main() {

   // 初始化UART(设置波特率等)

   // uart_init(115200);

   

   // 打印启动信息

   const char* msg = "RISC-V Boot Success!\n";

   for (int i = 0; msg[i] != '\0'; i++) {

       uart_putchar(msg[i]);

   }

   

   return 0;

}



三、UART 16550驱动开发实战


在嵌入式RISC-V系统中,UART通常采用内存映射I/O(MMIO)方式访问,即直接读写特定内存地址来控制硬件。


1. 寄存器映射与初始化


假设UART IP核兼容16550标准,其关键寄存器偏移如下:


偏移 寄存器 作用


+0x00 TX Data / RX Data 收发数据寄存器


+0x04 Interrupt Enable 中断使能


+0x0C Line Control (LCR) 设置数据位、停止位


+0x14 Line Status (LSR) 状态查询(TX空、RX就绪)


波特率设置代码示例:

void uart_init(uint32_t baud_rate) {

   uint32_t divisor = SYSTEM_CLK / (baud_rate * 16) - 1;

   

   // 1. 设置LCR的DLAB位为1,允许访问波特率分频器

   *(volatile uint32_t*)(UART_BASE + 0x0C) |= (1 << 7);

   

   // 2. 写入分频值

   *(volatile uint32_t*)(UART_BASE + 0x00) = divisor & 0xFF;        // DLL

   *(volatile uint32_t*)(UART_BASE + 0x04) = (divisor >> 8) & 0xFF; // DLM

   

   // 3. 设置8N1格式,并清除DLAB

   *(volatile uint32_t*)(UART_BASE + 0x0C) = 0x03; // 8位数据,无校验

}



2. 轮询式发送函数


在简单的Boot阶段,通常采用轮询(Polling)方式,避免复杂的中断处理。

void uart_putchar(char c) {

   volatile uint32_t* lsr = (volatile uint32_t*)(UART_BASE + 0x14);

   

   // 等待发送缓冲区为空(THR Empty)

   while ((*lsr & (1 << 5)) == 0) {

       // 空循环,等待硬件就绪

   }

   

   *(volatile uint32_t*)(UART_BASE + 0x00) = c;

}


void uart_puts(const char* str) {

   while (*str) {

       if (*str == '\n') {

           uart_putchar('\r'); // 在终端中,换行前通常需要回车

       }

       uart_putchar(*str++);

   }

}



四、FPGA工程集成与调试避坑


1.  BRAM初始化:在Vivado或Quartus中,将编译生成的.bin或.hex文件作为Boot ROM(BRAM)的初始化文件。上电后,FPGA配置完成时,BRAM中即已存好程序。

2.  复位时序:确保FPGA的sys_rst_n信号在时钟稳定后延迟释放,防止CPU在时钟不稳定时启动。

3.  串口调试:如果UART无输出,首先检查:

   ◦   波特率计算:SYSTEM_CLK是否与硬件PLL输出一致?


   -   引脚约束:TX引脚是否被正确分配到FPGA的IO并设置为正确的电平标准(如LVCMOS33)?

   -   终端配置:PC端串口助手是否设置为相同的波特率(如115200)、8N1格式?


五、结语


RISC-V软核在FPGA上的Boot流程,本质是“硬件地址映射”与“软件初始化顺序”的精确配合。掌握从汇编启动到C语言驱动的完整链条,意味着你不仅能让CPU跑起来,更能为其注入“五官”(UART)和“记忆”(RAM),为后续移植RTOS或运行复杂应用打下坚实基础。


本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
换一批
延伸阅读

在软件无线电(SDR)和雷达接收机中,数字下变频(DDC)是连接高速ADC与基带处理的关键桥梁。其核心任务是将高频宽带信号搬移到基带,并降低数据率。本文将详解如何利用NCO(数控振荡器)生成正交载波,并结合CORDIC(...

关键字: FPGA DDC

在FPGA高速数据流设计中,AXI4-Stream(AXIS)是连接DMA、DSP和视频IP的“血管”。但很多工程师只关注TDATA和TLAST,却忽略了TKEEP信号,导致在非对齐数据传输时出现数据错位、CRC校验失败...

关键字: FPGA AXI4-Stream协议

在FPGA开发中,Vivado HLS(High-Level Synthesis)是将C/C++算法转化为硬件IP核的利器。然而,未经优化的C代码综合后往往性能低下。流水线(Pipelining)是打破顺序执行瓶颈、将吞...

关键字: Vivado HLS C++ FPGA

在异构计算与高速数据采集领域,PCIe Gen3 x4 提供了接近 4GB/s 的理论带宽,是连接 FPGA 与 CPU 的“高速公路”。对于大多数开发者而言,XDMA(DMA/Bridge Subsystem for...

关键字: FPGA PCIe 异构计算

在FPGA开发中,时钟域交叉(CDC)是导致亚稳态和数据错乱的“头号杀手”。当信号从clk_a跨越到异步的clk_b时,若处理不当,轻则数据跳变,重则系统死锁。本文将对比4种最核心的CDC方案,帮你从“能用”进阶到“可靠...

关键字: FPGA CDC 时钟域交叉

在工业伺服驱动与多轴运动控制系统中,EtherCAT凭借其纳秒级同步精度成为主流。为了兼顾协议处理效率与复杂控制算法的实时性,“FPGA(ESC)+ MCU(应用)”的双核架构已成为高性能从站设计的标准范式。本文将深入探...

关键字: 工业总线协议 EtherCAT FPGA

在本教程中,我将展示如何创建并使用适用于 Zynq Ultrascale+ XCZU4EV 中 GTH 传输器的示例项目。

关键字: 处理器 收发器 FPGA

另一个重要的学习要点是理解 Zynq 设备的系统级行为。由于该设计依赖于处理系统来提供时钟,因此必须对处理系统进行初始化,以便可编程逻辑能够正常运行。

关键字: LED FPGA AUP-ZU3

为回馈广大开发者长期以来的支持,米尔电子将在本次展会现场举办福利活动,限时免费赠送15套MYD-YM90G开发板。诚邀各位行业伙伴与技术爱好者前往展台了解详情并参与领取。

关键字: FPGA 开发板

在FPGA设计流程中,布局布线阶段往往是决定设计成败的关键环节。特别是对于Xilinx UltraScale+这类高端器件,资源密度高、时钟网络复杂、时序要求严格,传统的实现策略往往难以满足高性能需求。Vivado De...

关键字: Xilinx UltraScale+ Smart Exploration FPGA
关闭