文章目录
-
- 概要
- 一、前言
- [二、什么是 BSP](#二、什么是 BSP)
- 三、如何实现对指定地址的读写
- 四、方法一:使用指针直接读写地址
- [五、方法二:使用 Xilinx 提供的 IO 读写函数](#五、方法二:使用 Xilinx 提供的 IO 读写函数)
- 六、如何知道外设的硬件信息
- 七、方法一:查看数据手册
- [八、方法二:查看 BSP 生成的硬件信息头文件](#八、方法二:查看 BSP 生成的硬件信息头文件)
- 九、程序中的延时应该怎么实现
- 十、低精度延时:死循环
- [十一、推荐方式:使用 BSP 提供的延时函数](#十一、推荐方式:使用 BSP 提供的延时函数)
- 十二、为什么推荐使用跨平台数据类型
- [十三、推荐使用 stdint.h 中的标准类型](#十三、推荐使用 stdint.h 中的标准类型)
- 十四、这一课真正要建立的思维方式
- 十五、知识小结
概要
在 Zynq 裸机开发中,真正开始控制硬件之前,首先需要掌握一些最基础、最常用的 C 编程方法。例如什么是 BSP,如何对指定地址进行读写,如何查找外设寄存器地址,如何实现程序延时,以及为什么要尽量使用跨平台的数据类型。这些内容虽然不直接对应某一个具体外设实验,但却是后续 GPIO、UART、定时器等所有裸机程序设计的基础。本文结合 Zynq 裸机开发环境,对这些常见问题进行归纳和整理,为后续进行独立外设开发打下基础。
关键词:
Zynq;裸机开发;BSP;寄存器读写;Xil_In32;Xil_Out32;延时函数;stdint.h
一、前言
在前面的 Zynq 裸机开发学习中,我们已经接触到了 Vivado、SDK、硬件平台、应用工程等基本概念。接下来再往前走,就会真正进入"写代码控制硬件"的阶段。
很多初学者在这个阶段会遇到几个典型问题:
- BSP 到底是什么,有什么作用?
- CPU 是怎么访问外设寄存器的?
- 一个 GPIO、UART 或定时器的基地址到底去哪里找?
- 程序里的延时应该怎么写?
- 为什么有的代码里用 u8,有的代码里用 uint8_t?
这些问题看起来零散,但其实都指向同一个核心:
Zynq 裸机开发中的 C 编程基础。
如果把裸机程序比作"直接和硬件打交道"的代码,那么这些内容就是最基础的工具和规则。只有把这些基本方法弄清楚,后面在编写 GPIO、串口、中断、定时器程序时,思路才会更清晰。
本文就围绕这一部分内容展开,总结 Zynq 裸机开发中最常见的几类基础知识。
二、什么是 BSP
- BSP 的基本含义
BSP 的全称是 Board Support Package,通常翻译为板级支持包。
不过在 Zynq 平台中,如果说得更准确一点,它更像是一个硬件系统支持包。因为 Zynq 的软件开发并不是只针对"某一块开发板",而是针对"当前 Vivado 中搭建出来的那个硬件系统"。
例如,一个 Zynq 工程中的硬件系统可能包括:
- PS 端的 GPIO
- UART
- 定时器
- 中断控制器
- DDR
- 某些自定义 PL 外设
- AXI 接口扩展 IP
这些模块共同构成了一个完整的硬件平台,而 BSP 就是针对这个平台生成的软件支持集合。
- BSP 的作用
Xilinx 在 SDK 中会根据当前硬件平台,自动生成相应的 BSP。
这个 BSP 一般包含以下几类内容:
- 硬件参数信息
- 外设寄存器地址定义
- 外设驱动程序
- 常用函数接口
- 启动、异常、中断等基础支持
也就是说,当你在应用程序里调用某个外设驱动函数时,本质上就是在使用 BSP 中已经提供好的支持代码。
可以把 BSP 理解成:
c
它帮你把"硬件系统"和"C 程序"连接起来了。
- 为什么有时用 BSP,有时又直接读写寄存器
在实际开发中,通常有两种方法控制硬件:
第一种是使用 BSP 提供的驱动和函数;
第二种是直接读写寄存器,自行编写底层控制代码。
这两种方式各有特点:
使用 BSP 驱动:
- 开发更方便
- 代码更清晰
- 已经过原厂验证
- 安全判断和兼容性处理更完善
直接读写寄存器:
- 更接近底层原理
- 代码执行效率可能更高
- 程序体积更小
- 更适合对性能敏感的场合
因此,一般可以这样理解:
bash
对程序尺寸和性能要求不高时,优先使用 BSP;
对效率和可控性要求较高时,可以直接进行寄存器级开发。
三、如何实现对指定地址的读写
裸机开发的本质,归根到底就是一件事:
bash
对指定地址的寄存器或存储单元进行读写。
CPU 并不会"直接理解 LED、串口、按键"这些概念,它真正能做的事情,就是访问某个地址,把数据写进去,或者从某个地址把数据读出来。
因此,只要知道了某个寄存器的地址,我们就可以通过 C 语言访问它。
四、方法一:使用指针直接读写地址
最原始、最直接的做法,就是使用指针访问固定地址。
例如,假设某个寄存器地址是:
c
0x00000020
那么就可以用类似下面的形式进行访问:
- 读寄存器
c
value = *(volatile unsigned char *)0x00000020;
- 写寄存器
c
*(volatile unsigned char *)0x00000020 = 0x12;
- 为什么要加 volatile
这里的 volatile 非常重要
因为寄存器的值可能会随时变化,它和普通变量不一样。编译器如果发现某个值"看起来没变",可能会自作主张进行优化,导致程序行为不符合硬件访问要求。
而 volatile 的作用就是告诉编译器:
c
这个地址对应的数据可能随时变化,不要随便优化,每次都要真实去读写。
所以,在寄存器级编程中,volatile 几乎是必不可少的。
五、方法二:使用 Xilinx 提供的 IO 读写函数
虽然用指针直接读写地址很原始、很直接,但在工程实践中,很多人更常使用 Xilinx 提供的 IO 读写函数。
这些函数定义在 xil_io.h 中,例如:
c
Xil_In8(addr);
Xil_In16(addr);
Xil_In32(addr);
Xil_In64(addr);
Xil_Out8(addr, data);
Xil_Out16(addr, data);
Xil_Out32(addr, data);
Xil_Out64(addr, data);
这些函数的作用非常直观:
- Xil_In32(addr):从指定地址读取 32 位数据
- Xil_Out32(addr, data):向指定地址写入 32 位数据
例如:
c
u32 value;
value = Xil_In32(0x00000020);
Xil_Out32(0x00000020, 0x12345678);
- 这些函数本质上是什么
如果你点进去看这些函数的定义,就会发现它们本质上仍然是对指针操作的封装。
例如 Xil_In32() 的内部思路,本质上类似于:
c
static INLINE u32 Xil_In32(UINTPTR Addr)
{
return *(volatile u32 *)Addr;
}
也就是说,它只是帮你把底层指针访问包了一层,更方便写代码,也更统一。
- 为什么推荐用这些函数
虽然它们本质上还是指针读写,但实际开发中使用 Xil_In32 / Xil_Out32 有几个好处:
- 代码更规范
- 可读性更好
- 统一了 8 位、16 位、32 位、64 位访问方式
- 和 Xilinx 的示例工程保持一致
- 便于后续维护和理解
因此,在 Zynq 裸机开发中,很多寄存器级例程都会采用这一套接口。
六、如何知道外设的硬件信息
当我们知道"裸机开发本质上是读写寄存器"之后,就会自然产生一个问题:
那这些寄存器地址到底从哪里来?
或者进一步说:
- 哪个外设对应哪个地址范围?
- 某个寄存器偏移量是多少?
- 某一位代表什么功能?
- 某个控制位写 1 还是写 0 才有效?
这些信息必须准确掌握,否则程序就无从下手。
七、方法一:查看数据手册
最基础、最原始、也最可靠的方法,就是直接查看芯片官方手册
对于 Zynq-7000 系列来说,一个非常重要的文档就是
UG585
这个文档是 Zynq SoC 的技术参考手册,其中包含了:
- 各个外设的寄存器地址范围
- 每个寄存器的偏移地址
- 每一位的功能定义
- 默认值与读写属性
- 模块工作原理说明
在安装 Vivado 后,一般也会安装对应的文档导航工具。通过文档工具可以找到这些官方手册。
从学习角度看,查手册虽然慢一点,但它有一个非常大的优点:
c
你看到的是最原始、最准确、最完整的信息。
所以当 BSP 文件看不懂、例程不完整、网上资料互相矛盾时,最可靠的办法往往还是回到手册。
八、方法二:查看 BSP 生成的硬件信息头文件
除了查手册,SDK 生成的 BSP 中也会包含大量硬件描述头文件。
这些文件通常会以 x 开头,以 _hw.h 结尾,用来描述某个外设的寄存器和位定义。比如:
- GPIO:xgpiops_hw.h
- UART:xuartps_hw.h
- SD/MMC:xsdps_hw.h
这些文件通常会提供:
- 基地址偏移定义
- 控制寄存器偏移定义
- 各位掩码定义
- 某些底层访问宏
这对编程非常有帮助,因为你不需要每次都去翻大部头手册,很多关键信息在头文件里已经整理好了。
- 为什么有些头文件找不到
有时候初学者会发现,某个外设相关的头文件在 BSP 里找不到,比如没有 xuartps_hw.h 或 xsdps_hw.h。
这通常不是工具出错,而是因为:
bash
你的硬件系统里根本没有使能这个外设。
SDK 在生成 BSP 时,一般只会针对当前硬件平台中已经启用的模块生成相应支持文件。
如果 Vivado 里的硬件系统没有配置 UART、SD 等模块,那么 BSP 里也就不会生成对应文件。
这也是为什么 Zynq 开发里总说:
bash
软件依赖硬件平台。
九、程序中的延时应该怎么实现
在裸机程序中,延时几乎是最常见的需求之一。
例如最基础的 LED 闪烁程序,往往就是:
- 输出高电平
- 延时一段时间
- 输出低电平
- 再延时一段时间
那么延时该怎么写呢?
十、低精度延时:死循环
最简单的方法,就是写一个空循环,例如:
c
for(i = 0; i < 1000000; i++)
{
;
}
或者:
c
while(count--)
{
;
}
这种方法确实能实现一定程度上的延时,但它有明显缺点:
- 延时不精确
- 与编译优化有关
- 与 CPU 主频有关
- 可移植性差
- 不同平台效果差异大
因此,这种方法只适合非常粗略的延时场景,不适合做较规范的工程代码。
十一、推荐方式:使用 BSP 提供的延时函数
在 Xilinx 的 BSP 中,已经提供了比较方便的延时函数,例如:
- usleep(unsigned long useconds):微秒级延时
- sleep(unsigned int seconds):秒级延时
这两个函数通常需要包含头文件:
c
#include <unistd.h>
例如:
bash
usleep(500000); // 延时500ms
sleep(1); // 延时1秒
- 为什么更推荐这种方式
相比 while 死循环,usleep() 和 sleep() 的优点很明显:
- 延时精度更高
- 底层通常基于系统定时机制实现
- 代码更清晰
- 可移植性更好
- 在很多平台和环境中都通用
因此,在 Zynq 裸机开发中,如果只是做普通延时控制,优先推荐使用这些函数。
- 更高精度怎么办
如果后续需要更高精度的定时,或者希望在等待期间 CPU 还能去做其他任务,那么单纯的阻塞式延时就不够用了。
这种情况下,就要进一步学习:
- 定时器
- 中断
- 周期调度
这通常会在后面的定时器实验中展开。
十二、为什么推荐使用跨平台数据类型
在很多 Xilinx 例程中,经常能看到如下类型:
c
u8
u16
u32
u64
s8
s16
s32
s64
这些类型在 Xilinx SDK 中使用很方便,因为相关头文件里已经帮你定义好了。
例如:
c
u8 i;
u32 value;
在 SDK 里通常没有问题。
但问题在于,这些类型并不是所有平台都默认支持。
也就是说,如果你把代码移植到别的开发环境里,例如其他芯片厂商的 IDE、不同编译器环境,可能就会直接报错。
这说明:
bash
u8 这类写法虽然在 Xilinx 平台里常见,但跨平台兼容性并不一定好。
十三、推荐使用 stdint.h 中的标准类型
为了增强代码可移植性,更推荐使用 C99 标准头文件 stdint.h 中定义的数据类型。
例如:
bash
#include <stdint.h>
uint8_t a;
uint16_t b;
uint32_t c;
uint64_t d;
int8_t e;
int16_t f;
int32_t g;
int64_t h;
这些类型的优点是非常明显的:
- 含义明确,位宽固定
- 跨平台兼容性好
- 几乎所有现代编译环境都支持
- 更适合写可移植代码
比如:
- uint8_t 表示无符号 8 位整数
- uint32_t 表示无符号 32 位整数
- int16_t 表示有符号 16 位整数
这比单纯写 unsigned int、unsigned char 更清楚,也比平台自定义的 u8、u32 更通用。
- 实际开发中的建议
因此,在自己的代码里,尤其是想长期维护、以后可能迁移到其他平台的代码中,建议优先使用:
bash
uint8_t
uint16_t
uint32_t
uint64_t
int8_t
int16_t
int32_t
int64_t
这样的代码风格会更加规范。
十四、这一课真正要建立的思维方式
这节内容虽然还没有正式开始控制某个具体外设,但它实际上是在帮我们建立裸机开发中最关键的几个思维方式。
- 裸机开发的核心是寄存器读写
无论是 GPIO、UART、SPI,还是定时器、中断控制器,本质上都离不开地址访问。
- BSP 是开发加速器,不是魔法黑盒
它的作用是帮你更方便地访问当前硬件系统,但你仍然要理解底层原理。
- 外设信息一定要学会查
不能只会抄例程。会查手册、会看头文件,才是真正具备独立开发能力的开始。
- 代码风格会影响后续维护与移植
例如延时函数、数据类型、头文件使用习惯,这些看似小事,长期看影响很大。
十五、知识小结
本文围绕 Zynq 裸机开发中的 C 编程基础,总结了以下几个关键点:
- BSP 是板级支持包,在 Zynq 中更准确地说是当前硬件系统的软件支持集合。
- BSP 中包含外设驱动、硬件参数、常用函数等内容,可以帮助开发者更方便地进行编程。
- 裸机开发的本质是对指定地址的寄存器进行读写。
- 对指定地址的访问既可以通过指针直接完成,也可以通过 Xil_In32、Xil_Out32 等函数完成。
- 查找寄存器地址和位定义时,最可靠的方法是查看官方手册,其次可以查看 BSP 中生成的 _hw.h 文件。
- BSP 只会针对当前硬件系统中已经使能的外设生成对应头文件。
- 程序延时既可以通过死循环实现,也可以使用 usleep()、sleep() 等更规范的函数。
- 对于需要可移植性的工程,更推荐使用 stdint.h 中定义的标准整型,而不是仅依赖特定平台定义的 u8、u32 等类型。