资讯详情

Zynq开发板按键点灯实操:PS端GPIO与PL端AXI GPIO寄存器配置对比

📅 2026/10/12 9:01:28 | 华诺云谱 👁 阅读
Zynq开发板按键点灯实操:PS端GPIO与PL端AXI GPIO寄存器配置对比
拿一块带 PS 和 PL 两层结构的 Zynq 开发板上来就想做一个“软件按键点灯”按一下按键LED 亮再按一下灭。听着跟单片机课设似的可真在 PYNQ 环境下动手会发现有个问题绕不开——按键和 LED 到底接在 PS 端还是 PL 端用 PS 端 GPIO 写软件跟在 FPGA 里挂一个 PL 端 GPIO 控制器两条路都能实现同一个效果但代码、硬件、调试方式完全不一样。这篇就当做一个实操记录把两种实现都完整跑一遍从接线、Overlay 加载、寄存器读写到踩坑点一次性说清楚。不管是刚接触 Zynq 的初学者还是想把点灯逻辑真正玩明白的开发者都可以照着来。1. 一开始就要想清楚按键和 LED 到底交给 PS 还是 PL很多人习惯把 Zynq 当成“ARM FPGA 两块芯片拼在一起”实际它是一颗芯片只是内部被分成处理系统和可编程逻辑两部分。PS 端就是 ARM 处理器那一套能跑 Linux、跑 PythonPL 端就是 FPGA 那套逻辑可以自己搭电路。GPIO 这个功能两边都有但含义不同。1.1 PS 端 GPIO 的资源MIO、EMIO 和寄存器PS 端的 GPIO 是由芯片里一个固定硬件模块管理的对应一组寄存器基地址在 0xE000A000。你不需要在 FPGA 里写任何逻辑CPU 直接读写这个地址就能控制引脚。PS 端 GPIO 管脚分成几组MIO 引脚是专用物理引脚数量固定很多还和启动模式、UART、SD 卡这类功能复用所以不是所有 MIO 都能随便拿来点灯EMIO 则是把 PS 端 GPIO 控制器的一部分信号引到 PL 里再由 PL 映射到实际物理引脚。这意味着哪怕你用的是“PS 端 GPIO 控制器”信号也可能经过 PL 才落到引脚上。设计上有时候很拧巴你光看“PS GPIO”四个字以为不碰 PL 就能点灯结果一看引脚表板子上大部分按钮、LED 都连到 PL 侧引脚根本没有引出到 MIO。这就是为什么在 PYNQ 这类开发板上直接能用 PS GPIO 点亮的板载外设往往很少更多时候得走 EMIO 或者干脆做 PL 端 GPIO。做实验之前一定要看原理图别凭感觉。1.2 PL 端 GPIO 是怎么回事PL 端 GPIO 不是在芯片里预置的硬件而是你用 FPGA 逻辑“搭”出来的一个控制器一般直接在开发工具里调用厂家提供的 AXI GPIO IP 来生成。它内部有几个可读写的寄存器通过 AXI 总线暴露给 PS 端PS 端软件把 AXI GPIO 的物理地址当成普通内存来访问往地址里写数PL 端引脚电平就会变化读地址就能拿到按键状态。所以“PL 端 GPIO”本质上是三件事的组合一段 FPGA 逻辑、一组寄存器、一组物理引脚。软件控制逻辑并没有变变的只是写入的地址和寄存器语义。看完下面两条实现路径这个体会会更深。1.3 一个表格先把两种方案拉出来对比对比维度PS 端 GPIOPL 端 AXI GPIO是否需要重新编译 PL 工程用 MIO 直连时不需要用 EMIO 时仍需要必须先把 bitstream 生成出来引脚数量MIO 有限且复用严重取决于 FPGA 资源通常非常充足软件访问方式读写 0xE000A000 附近寄存器读写 AXI 分配到的地址比如 0x41200000适合场景少量固定引脚、系统级启动配置大量 IO、自定义协议、复杂时序逻辑修改灵活性改动软件马上生效改逻辑要重新综合实现一遍2. 准备阶段最容易出的三个问题接线、镜像和 Overlay在写 Python 代码之前硬件环境和工程环境必须就绪。这块看起来简单实际踩过坑的人都知道接线错误和 Overlay 没加载是点灯失败最高频的原因。2.1 按键和 LED 的接法按键这段通常有两种接法。一种是“低电平有效”按键一端接 GPIO 引脚另一端接 GND同时给 GPIO 配置内部上拉电阻。这样平时引脚读到 1按下时被拉到地读到 0。另一种是“高电平有效”按键一端接 3.3V另一端接 GPIO平时靠下拉电阻或输入缓冲保持 0按下时读到 1。开发板上板载按键一般已经处理好上拉或下拉你不用管但从板子外面飞线接按键时务必把共地做好否则读到的是悬空电平一会 1 一会 0。LED 这边最省事的是 GPIO 高电平时 LED 亮GPIO 和 GND 之间串联一个限流电阻。电阻太小会烧引脚太大又太暗220 到 470 欧姆是比较常用的范围。有些板载 LED 是从 VCC 到 GPIO 的接法这时候控制逻辑是反的写入 0 才亮写入 1 反而灭。程序里出现“灯反着亮”时别急着查代码先看原理图上的共阳共阴接法。还有一点很关键这些 GPIO 绝大部分都是 3.3V 电平别拿 5V 信号硬怼进去轻则读不到正确电平重则烧掉引脚。2.2 镜像、bitstream 和 hwh 文件的关系PYNQ 环境跑在开发板 SD 卡里的 Linux 上最常见的操作方式是浏览器打开 Jupyter Notebook然后在 Notebook 里跑 Python 控制硬件。上电后默认系统里已经带了一些官方 Overlay但如果你自己用 FPGA 工具生成了一个新的 bitstream就得把它和配套的 hwh 文件放到同一个目录再手动加载。Overlay 这个词可以理解成“一套已经配置好的 FPGA 逻辑镜像”一个 Overlay 对应一个 bit 文件。加载时用类似from pynq import Overlay; over Overlay(my_design.bit)的语句完成。真正容易漏的是bit 文件只是 FPGA 的配置如果缺少 hwh 文件里的地址信息PYNQ 就不知道该给寄存器暴露什么名称代码里引用over.xxx_ip就会报错。写完自定义 PL 工程后常见问题是只拷了.bit忘了.hwh加载没报错一访问 IP 就崩。3. PS 端 GPIO 实现直接操作寄存器点灯PYNQ 的优势是能用 Python 直接跟硬件打交道。PS 端 GPIO 的实现我习惯直接用 mmap 把物理地址映射到用户空间然后对寄存器做读写。这样做的好处是不依赖某个特定版本的封装库只要知道寄存器偏移任何时候都能跑。3.1 用 mmap 把 GPIO 控制器当一块内存来访问现代 Linux 里访问物理内存不是直接给地址就能读需要用/dev/mem配合mmap。GPIO 控制器基地址 0xE000A000 是一个已经按页对齐的地址所以可以直接映射 4KB 页面。import mmap import os import struct import time PS_GPIO_BASE 0xE000A000 DATA_RO 0x00 # 输入状态只读 DATA 0x04 # 输出数据寄存器 DIRM 0x204 # 方向控制掩码置1表示该位由软件控制 OEN 0x208 # 输出使能掩码置1表示输出模式 fd os.open(/dev/mem, os.O_RDWR | os.O_SYNC) gpio_map mmap.mmap(fd, 4096, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE, offsetPS_GPIO_BASE) def read32(offset): return struct.unpack(I, gpio_map[offset:offset 4])[0] def write32(offset, value): gpio_map[offset:offset 4] struct.pack(I, value 0xFFFFFFFF)很多教程会把这一层封装叫peek/poke意思就是看一个地址、改一个地址。理解这里比看懂点灯本身重要所有外设寄存器在你眼里不过是内存地址后续操作全是 32 位整数的读写。3.2 方向配置和按键点灯的完整循环PS 端 GPIO 控制器的方向和输出使能需要独立设置。用掩码方式理解这几个寄存器会比较快DIRM 里某一位置 1代表这个引脚的方向由软件接管OEN 里某一位置 1代表该脚是输出。复位后这些位默认是 0意味着方向不被软件控制引脚也不输出如果你只写 DATA 不动 DIRM 和 OENLED 电平根本不会变这几乎是 PS 端点灯失败的头号原因。假设手头某个 MIO 引脚接了 LED另一个 MIO 引脚接按键初始化和轮询代码可以这样写LED_PIN 0 KEY_PIN 1 # 把 LED 和 KEY 两个引脚都交给软件控制 write32(DIRM, (1 LED_PIN) | (1 KEY_PIN)) # 只有 LED 作为输出按键保持输入 write32(OEN, 1 LED_PIN) # 灯先灭 write32(DATA, 0) while True: key (read32(DATA_RO) KEY_PIN) 1 if key 0: # 按下低电平有效 write32(DATA, 1 LED_PIN) # 亮 else: write32(DATA, 0) # 灭 time.sleep(0.02)这个循环已经能跑但直接这样用会有抖动问题。按键是机械结构按下和松开的一瞬间电平会来回跳变几次这个时间通常只有几毫秒。我前面的循环每 20ms 读一次看起来已经不快但在快速按放、或者按到一半接触不良时依然可能误触发。电磁开关和机械按键的细微振动是点灯这种小实验里最容易被忽略的稳定性杀手。3.3 给按键读取加上软件消抖最朴素的消抖方式就是“连续采样多次值一致才认为有效”。可以抽象成一个小函数def read_key_stable(key_pin, check_times5, interval0.01): first (read32(DATA_RO) key_pin) 1 for _ in range(check_times - 1): time.sleep(interval) now (read32(DATA_RO) key_pin) 1 if now ! first: return None return first在轮询主循环里每次只读取一次消抖后的结果如果返回 None 就继续采样直到拿到稳定值。这样按下和松开会各有一个短暂的等待窗口避免了抖动带来的重复触发。实际调试时如果按键按下偶尔不响应就适当减少check_times如果出现一次按下触发两次动作就增加采样次数或者拉长interval。4. PL 端 GPIO 实现在 FPGA 里做一个可控的 IO 控制器相比 PS 端直接操作寄存器PL 端实现多了一道工序先在 FPGA 工具里搭一个 AXI GPIO IP生成 bitstream再回到 Python 按 AXI 地址读写。这条链路听起来复杂其实跑通一次后会比 PS 端灵活很多。4.1 Block Design 里怎么搭一个按键和 LED 控制器打开 FPGA 开发工具新建工程选对应的 Zynq 器件。在 Block Design 中添加 Zynq PS 处理系统然后添加一个 AXI GPIO IP配置成双通道第一通道作为按键输入宽度设为 4第二通道作为 LED 输出宽度也设为 4。这里的通道数量可以按你实际按钮和 LED 的数量调整只是寄存器偏移会跟着变化。如果工程里有自动连接功能让工具自动把 AXI GPIO 接到 PS 端的 AXI 主接口上并分配复位和时钟。地址编辑器会给这个 IP 分配一个地址段常见的结果类似 0x41200000。这个地址一定要记下来后面 Python 里访问 PL 外设全靠它。这一步的本质是什么你是在 PL 里创建了一组可读写的寄存器这些寄存器的读写引脚最终连到芯片物理引脚上。ARM 端看到的是一个标准 AXI 从设备写进去的数据由 FPGA 内部逻辑转成引脚电平读回来的数据也由内部逻辑从引脚采样得到。对 PS 端来说它不关心引脚实际在哪只关心地址。4.2 物理引脚约束和 bitstream 生成IP 里拖出来的接口必须绑定到开发板的具体引脚上。不同板卡引脚不同需要参考板卡原理图在约束文件里给每个端口分配物理位置和电平标准类似这样set_property PACKAGE_PIN D18 [get_ports {gpio_io_0[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {gpio_io_0[0]}]引脚分配完成后运行综合、布局布线生成 bitstream。这一步完成会得到两个文件.bit和.hwh。把它俩放到 PYTHON 能找到的目录通常就是 PYNQ 的overlays目录或 Notebook 当前目录。加载时用 Overlay 接口代码里这个 Overlay 对象会解析 hwh 文件里的 AXI 地址把寄存器接口暴露出来。如果你只是想在开发板上玩按钮和 LED也可以直接用官方基础 Overlay它已经提前把板载按钮和 LED 接进 AXI GPIO 了。自己搭一个 IP 的意义在于理解内部结构以及后续可以往里加更多自定义逻辑。4.3 Python 侧访问 AXI GPIO 的寄存器AXI GPIO 的寄存器布局比 PS 端 GPIO 控制器简单每个通道只有数据寄存器和方向寄存器双通道时两个通道依次偏移排列。第一通道的数据寄存器偏移 0x00、方向寄存器偏移 0x04第二通道数据寄存器偏移 0x08、方向寄存器偏移 0x0C。方向寄存器里写 1 代表输入写 0 代表输出跟 PS 端的方向掩码逻辑略有不同。地址 0x41200000 可能跟我们前面 mmap 的是同一类物理地址所以复用一个读写封装PL_GPIO_BASE 0x41200000 CH1_DATA 0x00 CH1_TRI 0x04 CH2_DATA 0x08 CH2_TRI 0x0C fd os.open(/dev/mem, os.O_RDWR | os.O_SYNC) pl_map mmap.mmap(fd, 4096, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE, offsetPL_GPIO_BASE) def pl_read32(offset): return struct.unpack(I, pl_map[offset:offset 4])[0] def pl_write32(offset, value): pl_map[offset:offset 4] struct.pack(I, value 0xFFFFFFFF)初始化时把第一通道全部设成输入第二通道全部设成输出然后循环读按键值、写 LED 状态。因为板载按钮通常按下为低电平LED 又多半是点亮为高所以让按下时输出 1、松开时输出 0 的话直接对按键值取反就行pl_write32(CH1_TRI, 0xFFFFFFFF) # 按键通道全为输入 pl_write32(CH2_TRI, 0x00000000) # LED 通道全为输出 while True: btns pl_read32(CH1_DATA) 0xF # 按下为0松开为1取反后按下点亮松开熄灭 pl_write32(CH2_DATA, (~btns) 0xF) time.sleep(0.01)这时候你再回头看PL 端实现最核心的东西已经不是“GPIO 怎么操作”而是地址映射和寄存器布局。只要地址对、方向对剩下就是赋值判断。也正因为整个控制逻辑都跑在 FPGA 里你可以把按键滤波、防抖、甚至多键组合判断全部用硬件逻辑实现软件只负责最终业务判断。5. 两种方案跑完后怎么选、怎么改把同样的按键点灯分别用 PS 端 GPIO 和 PL 端 GPIO 各跑一遍之后能明显感觉到两者不是简单的“等价替换”而是在开发流程、调试难度、扩展方向上大不一样。5.1 我的实际取舍逻辑先说结论如果只是控制几个引脚读按键、亮灯而且这组引脚刚好被引到了 MIO 上那就用 PS 端 GPIO最省事连 PL 工程都不用动。但现实中更多情况是板上器件都接在 PL 侧你不得不在 FPGA 里建 PL 端控制器或者你之后要加编码器、PWM、串口时序这些自定义逻辑那无论如何都要走 PL 端。PL 端方案前期要综合、布线一次跑下来可能比写 Python 慢很多可一旦逻辑固化下来后期改动就非常灵活。比如你想让按键按下后不是立刻亮灯而是延迟 3 秒亮PL 里加一个计数器模块比 Python 里睡 3 秒可靠得多因为它不占用 ARM 的 CPU 时间也不受系统调度影响。我实际调试时只要涉及毫秒级以下时序就基本不再考虑 PS 端轮询方案。5.2 想复用的话建议把“读按键”和“写 LED”拆开不管用哪条路我都习惯在 Python 里把硬件操作封装成两个函数read_key_state()和set_led_state(value)。后面的业务逻辑不关心底层是 PS 还是 PL 寄存器只调用这两个函数。这样做的好处是以后从 PS 端切换到 PL 端时只需要替换函数内部实现主循环一行不用改测试时也可以直接 mock 一个假按键状态来验证逻辑。按键消抖的代码也建议放在这层封装里。用前面提到的连续采样法或者维护一个简单状态机记录上一次稳定电平只有在新电平持续一段时间后才认为状态变化。不要在主循环里散落一堆time.sleep(0.02)时间一长你就分不清哪段 sleep 是消抖、哪段是业务节奏。6. 点灯过程中反复踩过的几个问题以及排查思路最后记录几个我实际调试中碰到的问题。很多问题看起来像是“寄存器没写对”其实根因五花八门排查思路比答案更重要。6.1 按键读出来始终是 1按下去也没反应这个问题最常见的原因是方向配置不对。PS 端 GPIO 里如果 DIRM 没把按键对应位置 1或者 OEN 错置为 1引脚会被配置成输出输入缓冲读不到外部电平。PL 端 AXI GPIO 则要确认方向寄存器里那一对应位是 1而不是 0。硬件方面也不要忽略按键另一端是不是真连到了地或电源共地没做好电平会飘板上按键已带上拉时你还从外部并联一个下拉电阻会改变默认电平。先用万用表量一下引脚电压是最快的排查方式。6.2 写入地址没反应甚至直接报总线错误如果 Python 里 mmap 映射一个根本没分配的物理地址或者 bitstream 没加载读写时可能触发总线错误也可能读到全 0。我的排查顺序一般是先确认.bit已经加载成功再确认 BLOCK DESIGN 地址编辑器和 Python 里用的地址完全一致最后看映射 offset 是否页对齐。mmap 的 offset 参数必须按 4KB 页对齐否则虽然有时不报错但访问的物理页根本不是你想的那个。如果在 PL 端读写寄存器时数据总是一成不变可能是 PL 端 IP 时钟没跑起来检查 Zynq PS 的时钟输出有没有正确连到 IP 上而不是只盯着 Python 代码。6.3 后面还想做点什么从点灯到中断和自定义时序按键盘点灯的下一步通常是想让 CPU 不空转等按键触发中断再响应。PS 端 GPIO 控制器本身支持触发中断但需要配置对应 bank 的中断使能并且把中断信号接入 ARM 的中断系统PL 端则更常见的是用一个 GPIO IP 的中断输出接到 PS 的 IRQ 引脚软件里注册中断服务函数按键变化时硬件自动上报。如果需求更复杂比如按键短按、长按、双击区分或按键控制的是 PWM 呼吸灯我建议直接在 PL 端写一个小状态机把按键消抖、计时、PWM 生成全部放在 FPGA 逻辑里Python 只管读状态结果。这样既发挥 PL 的时序优势又保持 Python 侧代码简单也算是对“PS 端PL 端 GPIO 使用”最完整的一种理解两边各干各擅长的事。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑