保姆级教程十二:USB摄像头接入!ZYNQ+OpenCV+FPGA硬件加速图像处理实战(视觉终极篇)
文章目录
- 保姆级教程十二:USB摄像头接入!ZYNQ+OpenCV+FPGA硬件加速图像处理实战(视觉终极篇)
-
- [🛠️ 第一步:让 Linux 内核认识你的 USB 摄像头](#🛠️ 第一步:让 Linux 内核认识你的 USB 摄像头)
- [📷 第二步:硬件连接与开机查岗](#📷 第二步:硬件连接与开机查岗)
- [🐍 第三步:Python + OpenCV + FPGA 终极融合代码](#🐍 第三步:Python + OpenCV + FPGA 终极融合代码)
- [🚀 第四步:运行并见证奇迹](#🚀 第四步:运行并见证奇迹)
- [❓ 进阶答疑 (FAQ)](#❓ 进阶答疑 (FAQ))
如果你一路跟到了第十二篇,请先给自己鼓个掌!你即将超越 90% 的 ZYNQ 初学者。
很多新手学 FPGA 图像处理,卡在了怎么把摄像头的图像传给 FPGA。今天我们不搞复杂的 HDMI 或 MIPI 摄像头底层时序,我们就用最普通的、几十块钱的免驱 USB 摄像头(WebCam) ,结合上一篇配置好的 OpenCV,打造一条真正的 "硬加速机器视觉流水线" !
我们要实现的功能:
USB摄像头拍照 -> Python(OpenCV)提取灰度图 -> DMA 瞬间搬运到 FPGA -> FPGA 硬件乘法器将所有像素变亮(乘2) -> DMA 搬回内存 -> Python 保存为图片!

发车!
🛠️ 第一步:让 Linux 内核认识你的 USB 摄像头
虽然 USB 摄像头在 Windows 下是免驱的,但在 PetaLinux 里,我们必须手动给内核"打个勾",开启 UVC (USB Video Class) 驱动。
-
在 Ubuntu 虚拟机的 PetaLinux 工程目录下,输入命令配置内核:
bashpetalinux-config -c kernel -
在弹出的蓝色菜单中,依次进入以下路径(按回车进入,按
Y选中为[*],按Esc退出当前层):Device Drivers --->Multimedia support --->Media USB Adapters --->- 找到并勾选
[*] USB Video Class (UVC)
-
保存退出。
-
重新编译系统并打包生成
BOOT.BIN和image.ub:bashpetalinux-build petalinux-package --boot --fsbl images/linux/zynq_fsbl.elf --fpga images/linux/system.bit --u-boot --force -
把新系统拷入 SD 卡。
📷 第二步:硬件连接与开机查岗
-
找一个普通的 USB 摄像头 ,插到 ZYNQ-7030 开发板的 USB HOST / USB OTG 接口上。
(注意:有些开发板的 USB 接口默认是 Device 模式,旁边可能有个跳线帽或拨码开关,需要拨到 HOST 模式,具体请看板子说明书) -
给开发板上电,登录
root账户。 -
在终端输入终极查岗命令:
bashls /dev/video*如果你在屏幕上看到了
/dev/video0,恭喜你!! 你的摄像头已经成功被 ZYNQ 识别,可以开始视觉开发了!
🐍 第三步:Python + OpenCV + FPGA 终极融合代码
还记得我们在第 9 篇用 HLS 写的那个 硬件乘法器(所有数据乘以 2) 吗?
如果把这个乘法器用在图像上会发生什么?像素值乘以 2 = 画面亮度翻倍!
我们现在就用 Python 写一段极其优雅的代码,把图片扔给这个硬件去处理!
在开发板终端输入 vim fpga_vision.py,粘贴以下代码:
python
import cv2
import numpy as np
import mmap
import os
import struct
import time
# --- 物理地址定义 (与第10篇一致) ---
DMA_REG_BASE = 0x40400000 # DMA 控制寄存器
HLS_REG_BASE = 0x40000000 # HLS 乘法器(亮度调节器)
DMA_MEM_BASE = 0x10000000 # 我们在设备树预留的火车站
TX_BUFFER = DMA_MEM_BASE + 0x0000000 # 发送区
RX_BUFFER = DMA_MEM_BASE + 0x0800000 # 接收区
# --- DMA 寄存器偏移量 ---
MM2S_CR, MM2S_SA, MM2S_LENGTH = 0x00, 0x18, 0x28
S2MM_CR, S2MM_DA, S2MM_LENGTH = 0x30, 0x48, 0x58
# ==========================================
# 1. OpenCV 捕获图像并预处理
# ==========================================
print("[1] 正在初始化 USB 摄像头...")
cap = cv2.VideoCapture(0)
ret, frame = cap.read()
cap.release() # 拍完一张就释放摄像头
if not ret:
print("❌ 摄像头画面获取失败!")
exit()
# 转换为灰度图,并调整为 512x512 大小
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
resized = cv2.resize(gray, (512, 512))
# ⚠️ 核心细节:我们的 HLS IP核接口是 32位 (AXI_VAL)
# OpenCV 的灰度图默认是 8位 (uint8),我们要把它转换成 32位 (uint32) 才能完美喂给 FPGA
img_32bit = resized.astype(np.uint32)
# 保存一张原始图片作为对比
cv2.imwrite("original.jpg", resized)
print("[2] 图像预处理完成,尺寸: 512x512,准备发往 FPGA...")
# ==========================================
# 2. 内存映射 (打通 Python 与底层硬件)
# ==========================================
fd = os.open("/dev/mem", os.O_RDWR | os.O_SYNC)
# 映射控制寄存器 (4KB) 和 图像收发内存 (每块 1MB,512*512*4字节刚好是 1MB)
dma_reg = mmap.mmap(fd, 4096, offset=DMA_REG_BASE)
hls_reg = mmap.mmap(fd, 4096, offset=HLS_REG_BASE)
tx_mem = mmap.mmap(fd, 1024*1024, offset=TX_BUFFER)
rx_mem = mmap.mmap(fd, 1024*1024, offset=RX_BUFFER)
# ==========================================
# 3. 将图像装载到 DMA 发送区
# ==========================================
tx_mem.seek(0)
# tobytes() 会把 NumPy 矩阵直接变成底层字节流,瞬间写满 1MB 内存!
tx_mem.write(img_32bit.tobytes())
# ==========================================
# 4. 点火!唤醒 HLS 并启动 DMA 传输
# ==========================================
print("[3] 正在启动 FPGA 硬件加速器进行图像处理...")
# 唤醒 HLS 乘法器 (写入 0x81)
hls_reg.seek(0)
hls_reg.write(struct.pack('<I', 0x81))
def write_dma(offset, value):
dma_reg.seek(offset)
dma_reg.write(struct.pack('<I', value))
# 启动 DMA
write_dma(MM2S_CR, 1)
write_dma(S2MM_CR, 1)
# 告诉 DMA 地址在哪里
write_dma(MM2S_SA, TX_BUFFER)
write_dma(S2MM_DA, RX_BUFFER)
# 告诉 DMA 传输多大?512 * 512 个像素 * 4 字节 = 1048576 字节
transfer_bytes = 512 * 512 * 4
write_dma(S2MM_LENGTH, transfer_bytes) # 先开接收
write_dma(MM2S_LENGTH, transfer_bytes) # 再开发送
# 等待 FPGA 处理完毕 (对于 1MB 数据,DMA 传输其实只需不到 10 毫秒)
time.sleep(0.1)
# ==========================================
# 5. 回收处理完的图像数据
# ==========================================
print("[4] 从 FPGA 接收处理结果...")
rx_mem.seek(0)
result_bytes = rx_mem.read(transfer_bytes)
# 将底层字节流重新还原成 512x512 的 32 位矩阵
result_32bit = np.frombuffer(result_bytes, dtype=np.uint32).reshape(512, 512)
# ⚠️ 核心细节:像素乘 2 后可能超过 255 导致画面花屏(溢出)
# 我们使用 np.clip 把它限制在 0~255 之间,然后再转回 8位 图像
result_8bit = np.clip(result_32bit, 0, 255).astype(np.uint8)
# 保存 FPGA 加速处理后的图片
cv2.imwrite("fpga_processed.jpg", result_8bit)
print("[5] 大功告成!已保存 original.jpg 和 fpga_processed.jpg")
# 6. 打扫战场
dma_reg.close()
hls_reg.close()
tx_mem.close()
rx_mem.close()
os.close(fd)

🚀 第四步:运行并见证奇迹
在开发板终端直接运行:
bash
python3 fpga_vision.py
等待两秒钟,你会看到屏幕打印:
text
[1] 正在初始化 USB 摄像头...
[2] 图像预处理完成,尺寸: 512x512,准备发往 FPGA...
[3] 正在启动 FPGA 硬件加速器进行图像处理...
[4] 从 FPGA 接收处理结果...
[5] 大功告成!已保存 original.jpg 和 fpga_processed.jpg
此时输入 ls,你会发现当前目录下多了两张照片:
original.jpg:摄像头拍下的原图。fpga_processed.jpg:经过 FPGA 硬件乘法器处理后的图。
怎么看这两张图呢?
利用我们之前强推的终端神器 MobaXterm !它的左侧边栏自带 SFTP 文件传输功能。
你只需要在左侧列表找到这两个 .jpg 文件,双击它们,或者直接拖拽到你的 Windows 电脑桌面上。
打开图片你会惊奇地发现:
fpga_processed.jpg 的整体画面亮度,完美地变成了 original.jpg 的两倍!

❓ 进阶答疑 (FAQ)
Q1:代码里为什么要进行 8位 和 32位 的来回转换?
- 这正是软硬件协同最需要注意的数据位宽对齐。OpenCV 灰度图的每一个像素占 1 个字节(8-bit)。
- 但是,我们在第 9 篇用 Vitis HLS 生成乘法器时,默认使用了
ap_axiu<32,1,1,1>(32-bit 的 AXI 流)。如果我们直接把 8 位的数据灌进去,FPGA 会把 4 个像素拼成 1 个数字去乘,画面出来的绝对是乱码花屏。 - 企业级优化 :如果你觉得在 Python 里转 32 位浪费内存,正确的做法是回到 Vitis HLS,把接口改为
ap_axiu<8,1,1,1>重新生成 IP 核。这样 DMA 就可以原封不动地搬运 OpenCV 的 8 位数据了!
Q2:如果我想让 FPGA 做更牛逼的操作(比如边缘检测),该怎么做?
- 思路完全一样!你需要回到 Vitis HLS,利用 HLS 自带的
xfOpenCV库(Xilinx 专门为 FPGA 优化的 OpenCV 硬件库),写一个 Sobel 边缘检测的 C++ 函数。 - 导出 IP 核,替换掉现在的"乘法器",重新编译 Linux。
- 此时你 Python 里的代码一行都不用改! DMA 送进去的依然是图像,收回来的就是 FPGA 瞬间抽取的硬件级边缘轮廓线了!
结语:
兄弟们,祝贺你!当你看到那两张图片的一瞬间,你已经跨越了"单片机玩家"的门槛,正式成为了一名**"具备异构计算能力的 AI/机器视觉底层工程师"**。