从Hello World到文件IO调用:嵌入式开发环境全搭建

【本文基于 linux平台 验证通过,适用于主流嵌入式Linux平台】
👉 文末提供完整代码仓库+交叉编译工具链下载!


一、嵌入式Linux开发环境搭建

1.1 交叉编译链配置

交叉编译知识点

交叉编译是在一个平台上生成另一个平台可执行代码的过程,宿主平台:进行交叉编译的计算机系统,通常要求有足够的计算资源和存储空间来支持编译过程;目标平台:明确要生成可执行代码的目标硬件平台和操作系统;

为什么需要交叉编译?

嵌入式设备资源有限,无法直接在设备上高效编译代码。交叉编译允许在x86_64主机上生成ARM/RISC-V等架构的可执行文件。

配置步骤(宿主平台以ubuntu20.04版本,目标平台以ARM64为例)
  1. 下载工具链(以Linaro GCC为例)

    wget https://releases.linaro.org/components/toolchain/binaries/latest-7/aarch64-linux-gnu/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz
    
  2. 解压并安装

    tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar.xz
    sudo mv gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu /opt/
    
  3. 配置环境变量

    # 添加到 ~/.bashrc
    vim ~/.bashrc
    # 将下面一行添加到末尾
    	export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin:$PATH
    # 执行
    source ~/.bashrc
    
  4. 验证安装

    aarch64-linux-gnu-gcc --version
    # 输出应包含:gcc version 7.5.0 (Linaro GCC 7.5-2019.12)
    

1.2 Hello World测试

创建第一个嵌入式程序 hello.c

#include <stdio.h>

int main() {
    printf("Hello Embedded Linux!\n");
    return 0;
}

交叉编译并通过一定的方式把可执行文件传输到设备上:

aarch64-linux-gnu-gcc -static hello.c -o hello  # 静态链接
# 我这里使用的是adb push 方式
# 在设备上给可执行文件权限 chmod +x hello 然后运行
./hello  # 输出:Hello Embedded Linux!

二、文件IO精讲

文件 I/O(Input/Output,即输入 / 输出)是计算机编程中用于处理文件的重要操作,主要涉及到将数据从文件读取到内存,以及将内存中的数据写入文件。下面将从系统调用和库函数的角度详细介绍文件 I/O。

2.1系统调用基础

系统调用:系统调用是操作系统提供给用户程序的接口,用于访问底层的硬件资源和操作系统服务;是用户程序与操作系统内核之间进行交互的桥梁。

适合场景:偏底层控制、需要精确设置文件的权限和打开标志、操作设备文件(如串口、磁盘设备等)需要特殊的权限和控制、对性能有极高要求、想自行管理缓冲区等等。

文件IO函数 作用 典型返回值
open 打开/创建文件 成功返回fd,失败-1
read 从文件读取数据 成功返回字节数,失败-1
write 向文件写入数据 成功返回写入字节数,失败-1
lseek 移动文件读写指针的位置 成功返回文件偏移量(从文件开头字节数),失败-1
fstat 获取文件的状态信息 成功返回0,失败-1
chmod 改变文件权限 成功返回0,失败-1
close 关闭文件描述符 成功0,失败-1
open函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  • pathname:要打开或创建的文件路径名,可以是绝对路径或者相对路径
  • flags:文件的打开方式,可以组合使用,常见的标志有
    • O_RDONLY:以只读方式打开文件
    • O_WRONLY:以只写方式打开文件
    • O_RDWR:以读写方式打开文件
    • O_CREAT:若文件不存在,则创建该文件,此时需要第三个参数 mode 来指定文件的权限
    • O_TRUNC:如果文件已存在且以写方式打开,则将文件截断为零长度
    • O_APPEND:以追加方式打开文件,每次写操作都将数据追加到文件末尾
  • mode:当使用 O_CREAT 标志时,需要此参数来指定新创建文件的权限。权限用八进制数表示,例如 0644 表示文件所有者有读写权限,组用户和其他用户有读权限。
read函数
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
  • fd:要读取数据的文件的文件描述符,通常由 open 函数返回
  • buf:指向用于存储读取数据的缓冲区的指针
  • count:要读取的最大字节数。
write函数
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
  • fd:要写入数据的文件的文件描述符,通常由 open 函数返回
  • buf:指向用于存储写入数据的缓冲区的指针
  • count:要写入的字节数。
lseek函数
#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);
  • fd:要操作的文件的文件描述符
  • offset:相对于 whence 参数指定的位置的偏移量,以字节为单位。可以是正数(向后偏移)、负数(向前偏移)或零
  • whence:指定偏移量的起始位置,常用的值有
    • SEEK_SET:从文件开头开始计算偏移量
    • SEEK_CUR:从当前文件指针位置开始计算偏移量
    • SEEK_END:从文件末尾开始计算偏移量。
fstat函数
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int fstat(int fd, struct stat *statbuf);
  • fd:要获取状态信息的文件的文件描述符
  • statbuf:指向 struct stat 结构体的指针,用于存储文件的状态信息。struct stat 结构体包含了文件的各种属性,如文件大小、权限、修改时间等。
chmod函数
#include <sys/stat.h>

int chmod(const char *pathname, mode_t mode);
  • pathname:要改变权限的文件的路径名
  • mode:新的文件权限,用八进制数表示
close函数
#include <unistd.h>

int close(int fd);
  • fd:要关闭的文件描述符。

2.2 库函数基础

库函数:库函数是高级语言或开发库提供的函数,他们通常是基于系统调用实现的,目的是为了程序员方便开发,提供更高级、更易用的接口。

适合场景:跨平台开发(可移植性强)、简单文件读写、格式化的数据存储与读取等等。

文件IO函数 作用 典型返回值
fopen 按指定模式打开文件 成功返回 FILE* 指针,失败返回 NULL
fread 从文件流读数据到缓冲区 成功返回读取元素个数,出错或到文件尾可能小于指定值
fwrite 将缓冲区数据写入文件流 成功返回写入元素个数,出错可能小于指定值
fgets 从文件流读一行到缓冲区 成功返回缓冲区指针,遇文件尾或出错返回 NULL
fputs 将字符串写入文件流 成功返回非负整数,出错返回 EOF
fscanf 从文件流按格式读数据到变量 成功返回匹配赋值的输入项数量,出错或到文件尾返回 EOF
fprintf 按格式将数据写入文件流 成功返回写入字符数,出错返回负数
fseek 设置文件流的读写位置 成功返回 0,出错返回非 0
fclose 关闭打开的文件流 成功返回 0,出错返回 EOF
fopen函数
#include <stdio.h>

FILE *fopen(const char *pathname, const char *mode);
  • pathname:要打开或创建的文件路径名,可以是绝对路径或者相对路径
  • mode:字符串,用来指定文件的打开模式,常见的模式有
    • "r":以只读模式打开文件,文件必须存在
    • "w":以只写模式打开文件,若文件存在则将其内容清空;若文件不存在则创建新文件
    • "a":以追加模式打开文件,若文件存在,写入的数据添加到文件末尾;若文件不存在则创建新文件
    • "r+":以读写模式打开文件,文件必须存在
    • "w+":以读写模式打开文件,若文件存在则清空内容;若不存在则创建新文件
    • "a+":以读写模式打开文件,若文件存在,写入的数据会添加到文件末尾;若不存在则创建新文件。
fread函数
#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • ptr:指向用于存储读取数据的内存缓冲区的指针
  • size:每个要读取的元素的大小(以字节为单位)
  • nmemb:要读取的元素个数
  • stream:指向 FILE 对象的指针,代表要从中读取数据的文件流,通常为fopen返回值。
fwrite函数
#include <stdio.h>

size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
  • ptr:指向包含要写入数据的内存缓冲区的指针
  • size:每个要写入的元素的大小(以字节为单位)
  • nmemb:要写入的元素个数
  • stream:指向 FILE 对象的指针,代表要将数据写入的文件流,通常为fopen返回值。
fgets函数
#include <stdio.h>

char *fgets(char *s, int size, FILE *stream);
  • s:指向用于存储读取的字符串的字符数组的指针
  • size:要读取的最大字符数(包含字符串结束符 '\0'
  • stream:指向 FILE 对象的指针,代表要从中读取数据的文件流。
fputs函数
#include <stdio.h>

int fputs(const char *s, FILE *stream);
  • s:指向要写入文件流的以 '\0' 结尾的字符串的指针
  • stream:指向 FILE 对象的指针,代表要将字符串写入的文件流。
fscanf函数
#include <stdio.h>

int fscanf(FILE *stream, const char *format, ...);
  • stream:指向 FILE 对象的指针,代表要从中读取数据的文件流
  • format:格式控制字符串,用于指定输入数据的格式
  • ...:可变参数列表,用于存储从文件流中读取的数据。
fprintf函数
#include <stdio.h>

int fprintf(FILE *stream, const char *format, ...);
  • stream:指向 FILE 对象的指针,代表要将数据写入的文件流
  • format:格式控制字符串,用于指定输出数据的格式
  • ...:可变参数列表,包含要按照 format 格式写入文件流的数据。
fseek函数
#include <stdio.h>

int fseek(FILE *stream, long offset, int whence);
  • fd:要获取状态信息的文件的文件描述符
  • statbuf:指向 struct stat 结构体的指针,用于存储文件的状态信息。struct stat 结构体包含了文件的各种属性,如文件大小、权限、修改时间等。
  • stream:指向 FILE 对象的指针,代表要操作的文件流
  • offset:相对于 whence 指定位置的偏移量(以字节为单位),可为正、负或零
  • whence:指定偏移量的起始位置,有以下取值
    • SEEK_SET:从文件开头开始计算偏移量
    • SEEK_CUR:从当前文件指针位置开始计算偏移量
    • SEEK_END:从文件末尾开始计算偏移量。
fclose函数
#include <stdio.h>

int fclose(FILE *stream);
  • stream:指向 FILE 对象的指针,代表要关闭的文件流。

2.3 系统调用代码示例

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define BUFFER_SIZE 100

int main() {
    int fd;
    char buffer[BUFFER_SIZE] = "Hello, this is a test using system calls.";
    char read_buffer[BUFFER_SIZE];

    // 打开文件,如果文件不存在则创建,以读写模式打开
    fd = open("test_sys.txt", O_RDWR | O_CREAT, 0666);
    if (fd == -1) {
        perror("open");
        return EXIT_FAILURE;
    }

    // 写入数据到文件
    ssize_t bytes_written = write(fd, buffer, strlen(buffer));
    if (bytes_written == -1) {
        perror("write");
        close(fd);
        return EXIT_FAILURE;
    }

    // 将文件指针移动到文件开头
    if (lseek(fd, 0, SEEK_SET) == -1) {
        perror("lseek");
        close(fd);
        return EXIT_FAILURE;
    }

    // 从文件中读取数据
    ssize_t bytes_read = read(fd, read_buffer, BUFFER_SIZE);
    if (bytes_read == -1) {
        perror("read");
        close(fd);
        return EXIT_FAILURE;
    }
    read_buffer[bytes_read] = '\0';

    printf("Read from file: %s\n", read_buffer);

    // 关闭文件
    if (close(fd) == -1) {
        perror("close");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}    

2.4 库函数代码示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFFER_SIZE 100

int main() {
    FILE *fp;
    char buffer[BUFFER_SIZE] = "Hello, this is a test using library functions.";
    char read_buffer[BUFFER_SIZE];

    // 打开文件,以读写模式打开
    fp = fopen("test_lib.txt", "w+");
    if (fp == NULL) {
        perror("fopen");
        return EXIT_FAILURE;
    }

    // 写入数据到文件
    size_t items_written = fwrite(buffer, sizeof(char), strlen(buffer), fp);
    if (items_written != strlen(buffer)) {
        perror("fwrite");
        fclose(fp);
        return EXIT_FAILURE;
    }

    // 将文件指针移动到文件开头
    if (fseek(fp, 0, SEEK_SET) != 0) {
        perror("fseek");
        fclose(fp);
        return EXIT_FAILURE;
    }

    // 从文件中读取数据
    size_t items_read = fread(read_buffer, sizeof(char), BUFFER_SIZE, fp);
    if (items_read == 0 && ferror(fp)) {
        perror("fread");
        fclose(fp);
        return EXIT_FAILURE;
    }
    read_buffer[items_read] = '\0';

    printf("Read from file: %s\n", read_buffer);

    // 关闭文件
    if (fclose(fp) != 0) {
        perror("fclose");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}    

三、实战:文件复制器和文本编辑器

3.1 文件数据复制代码

/****** 文本复制器 ******/
#include <stdio.h>      // 标准输入输出库
#include <stdlib.h>     // 提供 EXIT_SUCCESS 和 EXIT_FAILURE
#include <fcntl.h>      // 文件控制选项(如 O_RDONLY, O_WRONLY, O_CREAT, O_TRUNC)
#include <unistd.h>     // 提供 `read`, `write`, `close` 等系统调用

#define BUFFER_SIZE 1024  // 定义缓冲区大小,每次读取 1024 字节,提高效率

int main(int argc, char *argv[])
{
    // 检查命令行参数是否正确(程序名 + 2 个参数)
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <source_file> <destination_file>\n", argv[0]);
        return EXIT_FAILURE;  // 参数错误,退出程序
    }

    const char *source_file = argv[1];      // 获取源文件名
    const char *destination_file = argv[2]; // 获取目标文件名

    // 以只读模式打开源文件
    int source_fd = open(source_file, O_RDONLY);
    if (source_fd == -1) {      // 检查是否打开失败
        perror("Failed to open source file");   // 输出错误信息
        return EXIT_FAILURE;    // 退出程序
    }

    // 以写入模式打开目标文件(如果不存在则创建,如果存在则清空)
    int destination_fd = open(destination_file, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (destination_fd == -1) {  // 检查是否打开失败
        perror("Failed to open destination file");
        close(source_fd);  // 关闭源文件,防止资源泄漏
        return EXIT_FAILURE;
    }

    char buffer[BUFFER_SIZE];  // 用于存储读取的文件内容
    ssize_t bytes_read;        // 存储 `read` 返回的字节数

    // 逐块读取源文件,并写入目标文件
    while ((bytes_read = read(source_fd, buffer, BUFFER_SIZE)) > 0) {
        ssize_t bytes_written = write(destination_fd, buffer, bytes_read);  // 写入文件
        if (bytes_written == -1) {  // 写入失败
            perror("Failed to write to destination file");
            close(source_fd);
            close(destination_fd);
            return EXIT_FAILURE;
        }
        if (bytes_written != bytes_read) {  // 检查是否完全写入
            fprintf(stderr, "Warning: Written bytes do not match read bytes\n");
        }
    }

    // 检查 `read` 是否遇到错误
    if (bytes_read == -1) {
        perror("Failed to read from source file");
        close(source_fd);
        close(destination_fd);
        return EXIT_FAILURE;
    }

    // 关闭源文件
    if (close(source_fd) == -1) {
        perror("Failed to close source file");
        close(destination_fd);
        return EXIT_FAILURE;
    }

    // 关闭目标文件
    if (close(destination_fd) == -1) {
        perror("Failed to close destination file");
        return EXIT_FAILURE;
    }

    printf("File copied successfully.\n");  // 复制成功
    return EXIT_SUCCESS;
}

3.2 交叉编译与部署

# 交叉编译
aarch64-linux-gnu-gcc -static file_copier.c -o copy
# 使用自己的方式将可执行文件传递到开发板
# 给权限后执行
./copy 1.txt 2.txt

# 输出:File copied successfully.

文本复制、文本编辑器源码都在github上,自行下载。


四、资源下载

  • 完整代码仓库GitHub链接
  • 工具链合集
    • ARMv8:Linaro GCC 7.5
    • RISC-V:SiFive Freedom Tools

🔥 下篇预告:《进程与线程:多任务开发的避坑指南》——揭秘嵌入式系统中的并发陷阱!
📢 关注专栏,评论区留言“嵌入式”获取《嵌入式Linux开发速查手册》!


“掌握环境搭建,就握住了嵌入式开发的第一把钥匙。从今天起,让你的代码在硬件上起舞!” 💻🔧

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐