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

文章目录

概要

在 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

  1. BSP 的基本含义
    BSP 的全称是 Board Support Package,通常翻译为板级支持包。

不过在 Zynq 平台中,如果说得更准确一点,它更像是一个硬件系统支持包。因为 Zynq 的软件开发并不是只针对"某一块开发板",而是针对"当前 Vivado 中搭建出来的那个硬件系统"。

例如,一个 Zynq 工程中的硬件系统可能包括:

  • PS 端的 GPIO
  • UART
  • 定时器
  • 中断控制器
  • DDR
  • 某些自定义 PL 外设
  • AXI 接口扩展 IP

这些模块共同构成了一个完整的硬件平台,而 BSP 就是针对这个平台生成的软件支持集合。

  1. BSP 的作用

Xilinx 在 SDK 中会根据当前硬件平台,自动生成相应的 BSP。

这个 BSP 一般包含以下几类内容:

  • 硬件参数信息
  • 外设寄存器地址定义
  • 外设驱动程序
  • 常用函数接口
  • 启动、异常、中断等基础支持

也就是说,当你在应用程序里调用某个外设驱动函数时,本质上就是在使用 BSP 中已经提供好的支持代码。

可以把 BSP 理解成:

c 复制代码
它帮你把"硬件系统"和"C 程序"连接起来了。
  1. 为什么有时用 BSP,有时又直接读写寄存器

在实际开发中,通常有两种方法控制硬件:

第一种是使用 BSP 提供的驱动和函数;

第二种是直接读写寄存器,自行编写底层控制代码。

这两种方式各有特点:

使用 BSP 驱动:

  • 开发更方便
  • 代码更清晰
  • 已经过原厂验证
  • 安全判断和兼容性处理更完善

直接读写寄存器:

  • 更接近底层原理
  • 代码执行效率可能更高
  • 程序体积更小
  • 更适合对性能敏感的场合

因此,一般可以这样理解:

bash 复制代码
对程序尺寸和性能要求不高时,优先使用 BSP;
对效率和可控性要求较高时,可以直接进行寄存器级开发。

三、如何实现对指定地址的读写

裸机开发的本质,归根到底就是一件事:

bash 复制代码
对指定地址的寄存器或存储单元进行读写。

CPU 并不会"直接理解 LED、串口、按键"这些概念,它真正能做的事情,就是访问某个地址,把数据写进去,或者从某个地址把数据读出来。

因此,只要知道了某个寄存器的地址,我们就可以通过 C 语言访问它。

四、方法一:使用指针直接读写地址

最原始、最直接的做法,就是使用指针访问固定地址。

例如,假设某个寄存器地址是:

c 复制代码
0x00000020

那么就可以用类似下面的形式进行访问:

  1. 读寄存器
c 复制代码
value = *(volatile unsigned char *)0x00000020;
  1. 写寄存器
c 复制代码
*(volatile unsigned char *)0x00000020 = 0x12;
  1. 为什么要加 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);
  1. 这些函数本质上是什么

如果你点进去看这些函数的定义,就会发现它们本质上仍然是对指针操作的封装。

例如 Xil_In32() 的内部思路,本质上类似于:

c 复制代码
static INLINE u32 Xil_In32(UINTPTR Addr)
{
    return *(volatile u32 *)Addr;
}

也就是说,它只是帮你把底层指针访问包了一层,更方便写代码,也更统一。

  1. 为什么推荐用这些函数

虽然它们本质上还是指针读写,但实际开发中使用 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

这些文件通常会提供:

  • 基地址偏移定义
  • 控制寄存器偏移定义
  • 各位掩码定义
  • 某些底层访问宏

这对编程非常有帮助,因为你不需要每次都去翻大部头手册,很多关键信息在头文件里已经整理好了。

  1. 为什么有些头文件找不到

有时候初学者会发现,某个外设相关的头文件在 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秒
  1. 为什么更推荐这种方式

相比 while 死循环,usleep() 和 sleep() 的优点很明显:

  • 延时精度更高
  • 底层通常基于系统定时机制实现
  • 代码更清晰
  • 可移植性更好
  • 在很多平台和环境中都通用

因此,在 Zynq 裸机开发中,如果只是做普通延时控制,优先推荐使用这些函数。

  1. 更高精度怎么办

如果后续需要更高精度的定时,或者希望在等待期间 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 更通用。

  1. 实际开发中的建议

因此,在自己的代码里,尤其是想长期维护、以后可能迁移到其他平台的代码中,建议优先使用:

bash 复制代码
uint8_t
uint16_t
uint32_t
uint64_t
int8_t
int16_t
int32_t
int64_t

这样的代码风格会更加规范。

十四、这一课真正要建立的思维方式

这节内容虽然还没有正式开始控制某个具体外设,但它实际上是在帮我们建立裸机开发中最关键的几个思维方式。

  1. 裸机开发的核心是寄存器读写

无论是 GPIO、UART、SPI,还是定时器、中断控制器,本质上都离不开地址访问。

  1. BSP 是开发加速器,不是魔法黑盒

它的作用是帮你更方便地访问当前硬件系统,但你仍然要理解底层原理。

  1. 外设信息一定要学会查

不能只会抄例程。会查手册、会看头文件,才是真正具备独立开发能力的开始。

  1. 代码风格会影响后续维护与移植

例如延时函数、数据类型、头文件使用习惯,这些看似小事,长期看影响很大。

十五、知识小结

本文围绕 Zynq 裸机开发中的 C 编程基础,总结了以下几个关键点:

  • BSP 是板级支持包,在 Zynq 中更准确地说是当前硬件系统的软件支持集合。
  • BSP 中包含外设驱动、硬件参数、常用函数等内容,可以帮助开发者更方便地进行编程。
  • 裸机开发的本质是对指定地址的寄存器进行读写。
  • 对指定地址的访问既可以通过指针直接完成,也可以通过 Xil_In32、Xil_Out32 等函数完成。
  • 查找寄存器地址和位定义时,最可靠的方法是查看官方手册,其次可以查看 BSP 中生成的 _hw.h 文件。
  • BSP 只会针对当前硬件系统中已经使能的外设生成对应头文件。
  • 程序延时既可以通过死循环实现,也可以使用 usleep()、sleep() 等更规范的函数。
  • 对于需要可移植性的工程,更推荐使用 stdint.h 中定义的标准整型,而不是仅依赖特定平台定义的 u8、u32 等类型。
相关推荐
sprite_雪碧2 小时前
排版类问题(机试高频)
c语言·数据结构·算法
EnglishJun2 小时前
ARM嵌入式学习(八)--- 汇编应用:点亮led
汇编·arm开发·学习
weixin_537590452 小时前
《C程序设计语言》练习答案(练习1-7)
linux·c语言·算法
Crazyong4 小时前
FreeRTOS-CPU使用率统计
单片机·嵌入式硬件
C++ 老炮儿的技术栈10 小时前
volatile使用场景
linux·服务器·c语言·开发语言·c++
_Ningye11 小时前
STM32 — 6.1 TIM定时中断
stm32·单片机·嵌入式硬件
AI科技星11 小时前
全尺度角速度统一:基于 v ≡ c 的纯推导与验证
c语言·开发语言·人工智能·opencv·算法·机器学习·数据挖掘
ARM+FPGA+AI工业主板定制专家12 小时前
基于ARM+FPGA+AI的船舶状态智能监测系统(二)软硬件设计,模拟量,温度等采集与分析
arm开发·人工智能·目标检测·fpga开发
FreakStudio12 小时前
把 Flask 搬进 ESP32,高中生自研嵌入式 Web 框架 MicroFlask !
python·单片机·嵌入式·cortex-m3·异步编程·电子diy