很多人第一次听说“在FPGA上跑Linux”都觉得是魔法:一堆查找表、进位链和逻辑块,怎么就能蹦出bash提示符?但拆开看,它本质上就是一场“把软件塞进自己亲手搭的硬件房子里”的工程。难的不是Linux本身,而是你得同时跨过硬件描述、处理器架构、引导链、驱动模型和调试手段这几道坎。下面我把这条线从头到尾拆给你看,尽量不绕弯子,直接上能落地的东西。
为什么总觉得难?因为你在同时维护两套系统
传统嵌入式开发,芯片厂商已经把CPU、DDR控制器、串口、网卡、USB PHY全给你固化好了,你只需要配寄存器、写应用层代码。但FPGA上的Linux,你得自己“造房子”。
硬件不是现成的,是“画”出来的。用Vivado做Block Design时,AXI总线的握手时序、中断映射、时钟域交叉、复位同步,这些细节错一个,U-Boot可能直接卡在Starting kernel ...前面,连报错的机会都不给你。
启动链像多米诺骨牌。PS端(ARM核)上电后先跑FSBL,再跳U-Boot,U-Boot加载内核和设备树,最后挂载rootfs。任何一个环节版本不对、路径写错、设备树节点漏了,屏幕只给你一串乱码或者干脆黑屏。
外设适配是真正的深水区。GPIO点灯?三行代码的事。但要让PL端的自定义IP通过AXI-Lite被Linux内核识别,你得写内核模块、注册platform device、配设备树、处理中断和DMA。这里没有“一键生成”的魔法。
调试手段比PC开发苦得多。PC崩了有GDB、有dmesg、有日志文件。FPGA上Linux崩了,可能连串口都没初始化成功。你得靠LED闪烁、ILA抓取AXI信号、U-Boot环境变量、甚至示波器看时钟。
新手选板:别一上来就追旗舰
跑Linux的FPGA,严格来说分两类:纯FPGA靠软核跑RISC-V/Linux(慢,适合理解原理),以及FPGA+硬ARM核(主流,适合做项目)。我推荐你从Xilinx Zynq-7000系列起步。
- Zynq-7010 / 7020:双核ARM Cortex-A9,540MHz~667MHz,足够跑精简版Linux。开发板推荐Digilent ZYBO Z7-10/20、Trenz Electronic TE0720,或者国产的正点原子/野火ZYNQ开发板。价格800~2000元,中文社区资料最多。
- Zynq UltraScale+ MPSoC:四核A53+R5+FPGA,性能强但工具链重(Vitis + PetaLinux 2023+),新手容易卡在SD卡分区、eMMC启动模式和PMU配置上。
- Intel Cyclone V SoC:双核ARM Cortex-A9,Quartus + Buildroot生态完整,但国内踩坑记录比Xilinx少。
- Lattice / iCE40 纯FPGA:跑VexRiscv或Rocket软核能起Linux,但DDR通常只有几十MB,启动要几分钟,适合教学演示,不适合实战。
我的建议很直白:买一块带大容量SD卡槽和稳定USB转串口的Zynq-7020开发板。电源别省,杂牌板的电源纹波会让DDR调参变成玄学,AXI总线采样不稳还会让你怀疑人生。
从空白工程到login:提示符
我以Xilinx Zynq-7000 + PetaLinux为例,这是最省心的路线。如果你不想被PetaLinux的庞大依赖坑到怀疑人生,Buildroot也是好选择,但配置更碎片化。
环境落地
# 安装基础依赖(Ubuntu 22.04 安全区)
sudo apt install -y tcl tk libncurses5-dev libssl-dev \
zlib1g-dev libfdt-dev git cpio python3 python3-pip \
python3-setuptools u-boot-tools mtd-utils flex bison
# 加载Xilinx工具链环境变量
source /opt/Xilinx/Vivado/2023.1/settings.sh
source /opt/Xilinx/PetaLinux/2023.1/settings.sh
这里有个铁律:PetaLinux版本必须和Vivado版本严格对应。2023.1的PetaLinux去编译2022.2生成的硬件工程,大概率会在bitstream解析阶段报错。Xilinx官网上有明确的Compatibility Matrix,下载前看一眼。
硬件平台搭建
打开Vivado → Create Block Design → 添加zynq7_processing_system7 IP。双击配置:
- MIO Configuration:勾选UART1(PL011,默认115200)、SD0、QSPI Flash(可选)、ENET0(如果板子有网口)
- DDR:选板载颗粒,比如MT41K256M16 RE-125,时序一般选
JEDEC DDR3 - Clock:100MHz主频,PL端预留100MHz给自定义逻辑
- Interrupts:如果需要PL中断,提前映射到GIC,否则后面设备树里写
interrupts会报错
配置完后点Run Connection Automation,让Vivado自动生成AXI互联。导出硬件:File → Export → Export Hardware,务必勾选Include Bitstream。这一步会生成.xsa文件,PetaLinux全靠它来知道你的硬件长什么样。
PetaLinux工程创建与编译
petalinux-create -t project -n linux_fpga --template zynq
cd linux_fpga
petalinux-config --get-hw-description=/path/to/project.xsa
进入菜单后,重点调这几项:
Subsystem AUTO Hardware Settings → Boot device → SD(新手先用SD卡,Flash配置太折腾)Kernel Configuration → Device Drivers → Character devices → Xilinx AXI UARTLite(如果你用UART Lite而不是PS端PL011)Filesystem Packages → extra → busybox(提供基础shell命令)Image Configuration → Filesystem type → ext4
编译镜像:
petalinux-build
这一步可能跑20分钟到2小时,取决于机器性能和Sstate缓存是否命中。编译完成后,images/linux/目录下会有BOOT.BIN、uImage、devicetree.dtb、rootfs.cpio.gz。
制作启动介质
SD卡分区必须按Xilinx规范来,乱分会导致U-Boot找不到FSBL。官方提供了一个脚本sd_card.sh,在PetaLinux工程的images/linux/目录下,直接跑最稳:
cd images/linux
sudo ./sd_card.sh /dev/sdX
替换/dev/sdX为你的SD卡设备名。千万别手滑写成/dev/sda,那是在格式化系统盘。脚本会自动生成:
| 分区 | 大小 | 内容 |
|---|---|---|
| FAT32 | 512MB | BOOT.BIN, system.dtb, uEnv.txt |
| ext4 | 剩余空间 | rootfs |
插卡上电,串口终端用screen /dev/ttyUSB0 115200或minicom。看到U-Boot滚动,内核解压,最后出现root@zynq:~#,你就跨过了最难的一道门槛。
让Linux认识你的PL端逻辑
假设你在PL端写了一个简单的计数器IP,通过AXI-Lite暴露两个寄存器:STATUS和CONTROL。Linux怎么访问它?
设备树是软硬件之间的“契约”。在project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi里追加:
&amba_pl {
my_counter@43C00000 {
compatible = "myvendor,counter-1.0";
reg = <0x43C00000 0x1000>;
interrupt-parent = <&gic>;
interrupts = <0 29 4>;
status = "okay";
};
};
compatible是内核驱动的“身份证”,驱动靠它匹配。reg是基地址,interrupts = <0 29 4>表示GICv2的第29号中断、电平触发(4=高电平,1=上升沿)。很多新手会把触发方式写错,导致中断脉冲被吃掉,后面排查能查到崩溃。
内核驱动骨架:
#include <linux/platform_device.h>
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/module.h>
static void __iomem *base_addr;
static irqreturn_t counter_irq(int irq, void *dev_id) {
u32 status = readl(base_addr + 0x00);
pr_info("Counter interrupt fired: %u\n", status);
writel(0x1, base_addr + 0x04); /* 清除中断标志 */
return IRQ_HANDLED;
}
static int my_counter_probe(struct platform_device *pdev) {
struct resource *res;
int irq;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base_addr = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base_addr))
return PTR_ERR(base_addr);
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
devm_request_irq(&pdev->dev, irq, counter_irq, 0, "my_counter", NULL);
pr_info("My counter driver loaded at %p\n", base_addr);
return 0;
}
static const struct of_device_id my_counter_ids[] = {
{ .compatible = "myvendor,counter-1.0" },
{},
};
MODULE_DEVICE_TABLE(of, my_counter_ids);
static struct platform_driver my_counter_driver = {
.probe = my_counter_probe,
.driver = {
.name = "my_counter",
.of_match_table = my_counter_ids,
},
};
module_platform_driver(my_counter_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simple AXI Counter Driver");
编译成模块后,insmod my_counter.ko,dmesg | grep my_counter看打印。如果要开机自动加载,在PetaLinux工程里新建project-spec/meta-user/recipes-modules/my_counter/my_counter_1.0.bb,内容就几行:
SUMMARY = "Simple AXI Counter Driver"
SRC_URI = "file://my_counter.c"
inherit module
FILES_${PN} += "${base_libdir}/modules/${KERNEL_VERSION}"
然后petalinux-build,重启后模块就会自动跑起来。
常见翻车现场与急救包
跑Linux的过程中,90%的问题集中在引导链和外设匹配。我按症状给你列个速查逻辑:
串口完全无输出
先查物理层:波特率是不是115200?TX/RX有没有交叉?USB转串口芯片是CH340、CP2102还是FT232?Linux下lsusb看设备,Windows下设备管理器有没有黄色感叹号。再查U-Boot环境变量:进U-Boot命令行敲printenv baudrate,确认和终端一致。很多时候不是系统崩了,是串口线松了。
卡在Starting kernel...
内核没加载成功。检查SD卡FAT分区里有没有uImage和system.dtb,文件名对不对。PetaLinux默认可能生成Image而不是uImage,需要在uEnv.txt里显式指定kernel_image=uImage。设备树和硬件不匹配也会卡在这里,尤其是DDR型号选错时,U-Boot初始化DDR失败就直接挂死。
根文件系统挂载失败:VFS: Cannot open root device
通常是ext4分区没正确挂载,或者内核没编译CONFIG_EXT4_FS。看U-Boot输出:bootargs里root=/dev/mmcblk0p2 rw指向的分区号是不是对的?在U-Boot命令行敲fdt print /chosen或者重启进Linux后lsblk确认分区顺序。Zynq的SD卡分区编号从1开始,但设备树里有时写mmc@e0100000的索引是0,别搞混。
PL端IP能读不能写,或者中断不触发
用Vivado ILA抓AXI总线。重点看AWVALID/AWREADY、WVALID/WREADY、BVALID/BREADY握手信号。如果AXI-Lite写事务一直卡在WREADY=0,说明PL端逻辑没准备好接收数据。中断不触发?检查MIO/EMIO映射、GIC优先级、设备树interrupts属性。还可以在内核里加cat /proc/interrupts,看对应中断号有没有计数增长。
网络不通
Zynq的GEM MAC在PS端,PL端做RGMII接口要严格遵守时序。先看ifconfig eth0 up,再ping网关。dmesg | grep gem看驱动是否加载。如果PHY地址不对,修改设备树phy-handle或phy-mode。很多开发板PHY地址是0x01或0x02,默认0x00会探测不到。
U-Boot命令行可以操作,但Linux起不来
在U-Boot里手动设置bootargs试试:
setenv bootargs "console=ttyPS0,115200 earlycon root=/dev/mmcblk0p2 rw rootwait"
saveenv
boot
如果这样能起来,说明是uEnv.txt或设备树里的启动参数有问题。rootwait很重要,它让内核等SD卡就绪再挂载根文件系统,否则经常出现Requesting system shutdown。
少走弯路的几条真话
先跑通官方参考设计。Xilinx官网有Zynq-7000 AP SoC Reference Designs,直接烧录验证板子没问题,再改自己的Block Design。别拿一块从来没亮过机的板子直接搭复杂工程。
版本锁定。Vivado 2022.2 + PetaLinux 2022.2 + SDK 2022.2,别混用。Xilinx的工具链兼容性是出了名的“差一个月就得重新配环境”。
别一上来就搞QSPI/eMMC。SD卡容错率高,断了重刷就行。Flash分区配置复杂,新手后期再碰。
学会看日志,但别迷信日志。dmesg只能告诉你内核层面的事,硬件层面的问题得靠ILA、逻辑分析仪、甚至万用表。AXI总线上的一个建立时间违例,内核日志里根本不会提。
社区资源比教程重要。Xilinx Answer Records、EDAC论坛、Stack Overflow的xilinx标签,比B站视频更新快。遇到具体错误码,直接复制粘贴搜,大概率有人踩过。
FPGA跑Linux不是炫技,它是你理解“软硬件边界”的最佳练习场。当你亲手把一段Verilog逻辑变成Linux里的一个字符设备,你会突然明白操作系统是怎么和硬件对话的。这个过程确实磨人,但每次login:亮起的那一刻,那种踏实感是纯软件项目给不了的。具体到哪一步卡住了,把报错信息和板子型号贴出来,咱们对着看。