文章目录
-
- 概要
- 一、前言
- 二、什么是硬件编程
- 三、为什么要学习硬件编程
- [四、GPIO 为什么适合作为入门外设](#四、GPIO 为什么适合作为入门外设)
- [五、典型 GPIO 结构](#五、典型 GPIO 结构)
- [六、GPIO 往往不是"一个引脚",而是一组引脚](#六、GPIO 往往不是“一个引脚”,而是一组引脚)
- [七、增强型 GPIO 结构](#七、增强型 GPIO 结构)
- [八、Zynq7000 的 GPIO 结构特点](#八、Zynq7000 的 GPIO 结构特点)
- [九、GPIO 控制的基本编程思路](#九、GPIO 控制的基本编程思路)
- [十、输出型 GPIO 场景:使用 GPIO 控制 LED](#十、输出型 GPIO 场景:使用 GPIO 控制 LED)
- [十一、LED 控制场景下的初始化思路](#十一、LED 控制场景下的初始化思路)
概要
在 Zynq 裸机开发中,真正实现对 LED、按键、串口、I2C、SPI 等外设的控制,本质上都离不开对硬件寄存器的读写操作。这种面向外设控制器寄存器的程序设计方式,就是硬件编程。本文结合 Zynq 平台的 GPIO 外设,介绍什么是硬件编程、为什么要学习硬件编程、GPIO 的典型结构与增强结构,以及基于 GPIO 控制 LED 的基本编程思路和方法,为后续外设驱动开发打下基础。
关键词
Zynq;裸机开发;硬件编程;GPIO;寄存器;LED;MIO
一、前言
在学习 Zynq 裸机开发时,很多初学者一开始会觉得"写程序就是写 C 代码",但真正进入外设控制之后就会发现,裸机程序和普通应用程序有一个很大的不同:
bash
裸机程序不是只和变量、函数、流程打交道,而是直接和硬件打交道。
比如:
- 想让 LED 亮,就要控制对应 GPIO 输出高低电平;
- 想读取按键状态,就要读取对应输入寄存器的值;
- 想让串口发送数据,就要向 UART 发送寄存器写入数据;
- 想和传感器通信,就要配置 I2C 或 SPI 控制器的寄存器。
也就是说,CPU 并不是直接"懂"LED、按键和传感器,而是通过访问各种外设控制器的寄存器,实现对外部世界的控制和感知。这个过程,就是所谓的硬件编程。
在前面的内容中,我们已经接触了 BSP、寄存器读写、地址访问等基础知识。本文就在此基础上,进一步讲清楚 Zynq 裸机开发中的硬件编程原理,并以 GPIO 为例,建立最基本的外设控制思路。
二、什么是硬件编程
所谓硬件编程,简单来说,就是:
bash
通过对硬件外设控制器寄存器进行读写,来配置其工作模式并完成数据交互。
在 Zynq 开发中,这里的"硬件"通常包括两大类:
- PS 端自带的外设控制器,例如 GPIO、UART、I2C、SPI 等;
- PL 端自定义或扩展的外设控制器 IP。
CPU 要想控制这些硬件,或者从这些硬件获得状态信息,就必须通过对应的控制器寄存器来完成。也就是说,硬件编程并不是"直接操作外设本体",而是"通过寄存器去控制外设控制器,再由控制器去操作外部设备"。
例如:
- 控制 LCD 显示字符;
- 控制继电器吸合或断开;
- 通过串口打印调试信息;
- 通过 I2C 读取温湿度传感器数据;
- 通过 SPI 控制 ADC 进行采样;
- 读取按键状态;
这些场景,本质上都属于硬件编程。
三、为什么要学习硬件编程
对于嵌入式开发来说,学习硬件编程不是"可选技能",而是"基础能力"。
因为不管你最终做的是:
- LED/按键基础实验,
- 串口打印调试,
- 传感器采集,
- 屏幕显示,
- 工业控制,
- 还是 FPGA 与 ARM 协同系统设计,
都离不开对底层外设的控制。
换句话说,如果不会硬件编程,你就很难真正理解:
- 程序为什么能控制硬件;
- 某个外设为什么这样初始化;
- 某一位寄存器为什么要写 1;
- 为什么有的功能要先配置方向,再配置数据;
- 为什么有的场景要开中断,有的场景不用。
当然,不同的外设功能不同,寄存器设计也不同,很难存在一个通用的程序模板去控制所有外设。因此只能遵循一个原则:
bash
具体硬件具体分析,具体功能具体编程。
而在所有外设中,GPIO 可以说是最基础、最简单、最适合入门的一类,因此也非常适合作为硬件编程的第一站。
四、GPIO 为什么适合作为入门外设
GPIO 是 General Purpose Input/Output 的缩写,也就是通用输入输出接口。
几乎所有基于 CPU 的芯片里都会有 GPIO,它是处理器和外部世界进行最直接电平交互的一类基础外设。比如:
- 点亮一个 LED;
- 检测一个按键是否按下;
- 控制一个继电器吸合;
- 读取一个简单开关状态;
这些最基本的功能,通常都可以通过 GPIO 来完成。
之所以把 GPIO 作为硬件编程的入门对象,是因为它结构相对简单,但又足够典型。通过学会 GPIO 的控制方法,后面再去学习 UART、I2C、SPI、定时器等外设时,思路会顺畅很多。
五、典型 GPIO 结构
要理解 GPIO 的编程方法,首先要理解它的结构。
从功能上看,我们通常希望 GPIO 做两类事:
输出功能:
比如控制 LED、继电器,只需要让 GPIO 输出高电平或低电平。
输入功能:
比如读取按键状态,只需要检测当前引脚电平是高还是低。
为了支持这两类功能,一个最基础的 GPIO 一般至少需要三类寄存器:
- 方向控制寄存器
用于设置 GPIO 当前是输入模式还是输出模式。
因为一个引脚不可能同时既当输入又当输出,所以必须先配置方向。
- 输出数据寄存器
用于存储 CPU 希望输出到引脚上的电平值。
如果 GPIO 处于输出模式,那么这个寄存器里的值就会影响引脚输出状态。
- 输入数据寄存器
用于反映外部引脚当前实际电平状态。
如果 GPIO 处于输入模式,那么 CPU 就可以通过读取这个寄存器获知外部信号状态。
从抽象上看,可以把它理解成三部分:
- R1:方向/输出使能控制寄存器
- R2:输出状态/数据寄存器
- R3:输入状态/数据寄存器
这就是最典型的 GPIO 结构。
六、GPIO 往往不是"一个引脚",而是一组引脚
严格来说,我们平时说"一个 GPIO",很多时候其实说的是一组引脚资源。
例如一个 8 位 GPIO 端口,可能有:
bash
8 位方向控制
8 位输出值
8 位输入值
那么对应的寄存器通常也会是按位管理的:
bash
OE[7:0]:每一位控制对应引脚是否输出
WDATA[7:0]:每一位控制对应引脚输出什么值
RDATA[7:0]:每一位反映对应引脚当前输入状态
这样 CPU 只要通过读写这些寄存器,就能统一管理一组 GPIO 引脚。
这也是为什么在寄存器编程中,经常会遇到"改某一位不影响其他位"的问题,因为多个引脚常常共用同一个 32 位寄存器。
七、增强型 GPIO 结构
在实际芯片中,GPIO 通常不只是"方向 + 输入 + 输出"这么简单。为了提高使用效率和扩展功能,很多芯片厂家会在基础结构上增加一些增强功能。
- 置位/清零寄存器
假设一个 8 位输出寄存器中,只想把 bit2 置为 1,而不影响其他位,那么最基本的做法通常是:
- 先读出整个寄存器值;
- 修改目标位;
- 再把修改后的值写回去。
例如:
c
value = value | (1 << 2);
同样,如果想把 bit3 清零,也要进行读-改-写操作。
这种方法虽然通用,但缺点也明显:
- 写法繁琐;
- 编译后指令更多;
- 执行效率较低;
- 高频翻转场景下不够高效。
因此很多 MCU 或 SoC 都会额外设计专门的 SET 寄存器 和 CLR 寄存器。
只需要向对应位写 1,就能实现置位或清零,而不会影响其他位。
这本质上是:
bash
用更复杂一点的硬件设计,换取更简单、更高效的软件控制。
- 中断屏蔽寄存器
在一些输入场景中,CPU 需要对外部电平变化快速响应。比如按键触发、中断检测、边沿检测等。
这时就需要 GPIO 具备触发中断的能力。
但并不是 GPIO 的每一位都需要开中断,因此通常会设计一个 中断屏蔽寄存器,用来决定哪些位允许产生中断请求,哪些位禁止产生中断。
这也是后续学习 GPIO 中断编程时的重要基础。
八、Zynq7000 的 GPIO 结构特点
Zynq7000 作为一款 SoC FPGA,其 GPIO 结构比最基础的 GPIO 模型更完整、更实用。资料中提到,可以在 Zynq 官方技术参考手册 UG585 中查看其 GPIO 功能框图和相关寄存器说明。
对学习者来说,当前阶段不必一开始就把所有 GPIO 寄存器全部背下来,更重要的是先建立这样的认识:
- Zynq 的 GPIO 仍然符合"方向控制 + 数据控制 + 输入读取"的基本思想;
- 同时它还提供了中断、掩码写入等更高效的控制机制;
- 真正编程时,需要结合数据手册和头文件来确定寄存器地址、偏移和功能。
九、GPIO 控制的基本编程思路
无论是控制 LED,还是读取按键,GPIO 编程大体都可以归纳成两部分:
- 初始化
初始化阶段要根据场景完成对应设置,例如: - 设置方向;
- 设置输出使能;
- 是否开启中断;
- 中断类型如何配置;
- 是否需要关闭无关中断。
- 工作阶段
初始化完成后,GPIO 进入实际工作阶段:
如果是输入型应用:
- 不开中断时,程序轮询读取输入寄存器;
- 开中断时,编写中断处理函数响应输入变化。
如果是输出型应用: - 通过数据寄存器或快速置位/清零寄存器修改输出值。
可以看到,不管外设多复杂,思路其实都很统一:
bash
先初始化,再运行控制。
十、输出型 GPIO 场景:使用 GPIO 控制 LED
资料中给出的典型例子是:在 ACZ702 开发板上,PS 端 LED 连接到 Zynq 的 MIO7 引脚。
当 MIO7 输出高电平时,LED 点亮;当 MIO7 输出低电平时,LED 熄灭。
这是一个非常典型的单输出型应用场景。
在这个场景下,我们只需要控制 GPIO 输出高低电平即可,不需要用到输入检测和中断触发,因此它是最适合入门的例子。
十一、LED 控制场景下的初始化思路
- 关闭中断
因为本实验只要求 LED 亮灭控制,并不需要 GPIO 产生中断,所以在初始化时应该先关闭对应 GPIO 位的中断功能。
资料中提到,对于 Zynq GPIO,可以通过 INT_DIS 寄存器对某一位写 1 的方式,禁止该位产生中断请求。对于 MIO7,对应的控制思路就是让 bit7 的中断处于关闭状态。
这一步的意义在于:
把当前场景下"不需要的功能"先关掉,避免系统出现多余行为。
- 设置方向
LED 是被 CPU 驱动的,所以 GPIO 必须工作在输出模式。
这意味着方向寄存器中对应 bit7 必须配置为输出。
- 设置输出使能
仅有方向为输出还不够,还需要让输出驱动真正被使能。
在 Zynq GPIO 中,输出相关控制通常还涉及 OUTEN 寄存器,因此初始化时需要让 bit7 的输出使能打开。
也就是说,点亮 LED 的最基础初始化逻辑其实就是:
- 关闭中断;
- 设置方向为输出;
- 打开输出使能。