基于ARM的裸机程序设计和开发(三):C编程基础与Zynq裸机开发常用方法

发布时间:2026/5/19 8:29:40

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

相关新闻