1. 从零开始:认识嵌入式设备里的“硬盘”eMMC
如果你玩过树莓派或者自己做过一些智能硬件的小项目,那你对SD卡肯定不陌生。但当你开始接触更专业的嵌入式设备,比如工业网关、边缘计算盒子或者一些定制化的智能终端时,你可能会发现,它们内部用的存储介质不再是那个可以随意插拔的SD卡,而是一颗直接焊在主板上的、小小的、方方正正的芯片。没错,这就是我们今天要聊的主角——eMMC。
你可以把它理解成嵌入式设备内置的“固态硬盘”。它的全称是 Embedded Multi Media Card,直译过来就是“嵌入式多媒体卡”。它可不是简单的存储芯片,而是一个“套装”。简单来说,eMMC = NAND Flash(存储颗粒)+ 闪存控制器 + 标准接口封装。这个集成的控制器帮我们干了件大好事:它把底层复杂的闪存管理(比如坏块管理、磨损均衡、ECC纠错)全都包揽了,给我们开发者提供了一个非常简单的、类似于操作SD卡的标准接口。这就好比你去餐厅吃饭,eMMC是后厨已经配好、炒好、装好盘直接端上来的菜;而如果你直接用原始的NAND Flash,那就相当于给你一堆生鲜食材,你得自己从切菜、生火开始折腾。
在实际项目中,为什么大家都爱用eMMC呢?我总结下来就三点:省事、稳定、够快。省事体现在硬件设计上,你不需要再为Flash设计复杂的外围电路和驱动;稳定是因为控制器帮你处理了所有脏活累活;速度上,现在的eMMC 5.1接口速度能到400MB/s以上,对于大多数嵌入式应用来说绰绰有余。当你拿到一块新的核心板,准备把自己的系统灌进去时,第一件大事就是管理好这块eMMC:给它分区、格式化,最后把系统镜像“烧录”进去。这个过程,从手动一行行敲命令,到写成一键执行的自动化脚本,正是嵌入式工程师从“会用”到“高效”的必经之路。接下来,我就带你完整走一遍这个实战流程。
2. 手动操作篇:亲手“雕刻”你的eMMC
刚开始接触的时候,我强烈建议你用手动操作的方式完整走几遍流程。这就像学开车先学手动挡一样,虽然初期麻烦,但你能彻底搞清楚每一个环节在干什么,以后出了问题你才知道从哪里下手排查。这里我假设你的开发环境已经搭好,可以通过串口或者SSH登录到你的嵌入式Linux设备上,并且设备已经识别到了eMMC(通常是/dev/mmcblk1,SD卡可能是/dev/mmcblk0)。
2.1 第一步:用fdisk进行分区规划
分区就像是给一块空白的土地划分不同的功能区:哪里是引导区(boot),哪里是系统根目录(rootfs),哪里存放用户数据(data)。我们最常用的分区工具就是fdisk。
首先,查看一下eMMC设备:
fdisk -l /dev/mmcblk1你会看到这个设备的总容量信息。然后我们开始分区操作:
fdisk /dev/mmcblk1进入交互界面后,输入m可以查看所有命令帮助。我们的操作通常遵循一个固定顺序:
- 创建新分区表(如果是全新设备):输入
o创建旧的DOS格式分区表,或者g创建新的GPT分区表。对于大多数嵌入式Linux,DOS(MBR)格式就够用了。 - 新建分区:输入
n。它会问你是主分区(primary)还是扩展分区(extended),我们一般选主分区p。然后需要输入分区号(1-4)。 - 设置起始扇区:通常第一个分区从2048扇区开始,这是为了留出空间给分区表等信息,直接按回车用默认值就行。
- 设置分区大小:这是关键!你可以用扇区数,但更直观的是用
+size{K,M,G}的格式。比如,我想分一个128MB的boot分区,就输入+128M。系统会自动计算出结束扇区。
我举个例子,创建一个128MB的FAT32引导分区和一个占用剩余空间的ext4根文件系统分区:
命令(m 获取帮助): n 分区类型: p 分区号(1-4,默认1): 1 起始扇区(默认2048): (直接回车) Last sector, +sectors or +size{K,M,G}: +128M 命令(m 获取帮助): n 分区类型: p 分区号(1-4,默认2): 2 起始扇区(默认266240): (直接回车) Last sector, +sectors or +size{K,M,G}: (直接回车,用完所有剩余空间)分完区先别急,还有两个重要设置:
- 设置分区类型:输入
t,然后选择分区号。对于第一个引导分区,我们设成c(W95 FAT32 LBA)。对于第二个Linux根分区,设成83(Linux)。 - 设置引导标志:输入
a,然后选择分区号1,给boot分区加上可引导标志。
最后,输入p预览分区表,确认无误后,输入w保存并退出。注意:w是真正写入磁盘的操作,一旦执行就无法撤销,务必谨慎!
2.2 第二步:格式化分区并挂载
分区只是画好了格子,格式化才是给格子里铺上地板(文件系统)。我们分别格式化两个分区:
# 格式化第一个分区为FAT32,卷标设为BOOT mkfs.vfat -F 32 -n BOOT /dev/mmcblk1p1 # 格式化第二个分区为ext4,卷标设为ROOTFS mkfs.ext4 -L ROOTFS /dev/mmcblk1p2格式化完成后,我们需要把分区挂载到系统的某个目录下,才能访问它。通常挂载到/mnt或/media下:
mkdir -p /mnt/boot /mnt/rootfs mount /dev/mmcblk1p1 /mnt/boot mount /dev/mmcblk1p2 /mnt/rootfs现在,/mnt/boot和/mnt/rootfs就分别对应eMMC的两个分区了,你可以像操作普通文件夹一样往里面拷贝文件。
2.3 第三步:手动烧录系统镜像
烧录,其实就是把准备好的系统文件拷贝到对应的分区里。通常,我们会用一张已经做好系统的SD卡来启动设备,然后把SD卡里的内容“克隆”到eMMC中。
假设你的SD卡有两个分区:mmcblk0p1(FAT32 boot) 和mmcblk0p2(ext4 rootfs),并且都已经挂载好了。那么烧录过程就是简单的拷贝:
# 烧录boot分区内容 cp -r /run/media/mmcblk0p1/* /mnt/boot/ # 烧录rootfs分区内容。注意用-a参数保留文件属性 cp -a /run/media/mmcblk0p2/. /mnt/rootfs/拷贝完成后,记得执行sync命令确保所有数据都写入磁盘,然后卸载分区:
umount /mnt/boot /mnt/rootfs至此,手动操作全部完成。重启设备,如果启动顺序设置正确,设备就应该从eMMC启动了。这个过程我走过很多遍,虽然步骤清晰,但每次都要重复输入一长串命令,不仅容易敲错,而且效率低下。特别是当你需要给几十上百台设备烧录系统时,手动操作简直就是噩梦。
3. 效率飞跃:编写自动化格式化和烧录脚本
当你熟练掌握了手动操作后,就该追求效率了。把这一系列命令写成Shell脚本,实现一键操作,这才是工程师该干的事。脚本化的核心思想是:用代码描述你的操作逻辑,增加错误检查,让整个过程可靠且可重复。
3.1 脚本设计思路与核心函数
一个好的脚本不应该只是命令的堆砌。我设计脚本时通常会考虑这几个模块:
- 参数检查与环境准备:检查目标设备是否存在,卸载可能已经挂载的分区,避免操作冲突。
- 分区操作:使用更强大的
parted工具进行非交互式分区,比fdisk更适合脚本。 - 格式化操作:在格式化前后加入等待和检查,确保设备节点就绪。
- 文件拷贝:明确源路径和目标路径,并验证拷贝结果。
- 日志与错误处理:每一步都输出明确日志,任何一步出错都立即停止并提示。
我们先看一个核心的utils.sh函数库,它包含一些通用功能:
#!/bin/bash # utils.sh - 通用工具函数 # 错误信息打印函数,带时间戳,输出到标准错误 err() { echo "[$(date +'%Y-%m-%dT%H:%M:%S')]: ERROR: $*" >&2 exit 1 } # 检查文件或设备是否存在 check_device() { if [ ! -b "$1" ]; then err "设备 $1 不存在!请检查连接。" fi } # 安全卸载函数,如果挂载了则卸载 safe_umount() { if mountpoint -q "$1"; then umount "$1" echo "已卸载 $1" fi } # 等待设备节点出现,超时则报错 wait_for_device() { local device=$1 local timeout=10 local count=0 while [ ! -b "$device" ] && [ $count -lt $timeout ]; do sleep 1 ((count++)) done if [ ! -b "$device" ]; then err "等待设备 $device 超时!" fi echo "设备 $device 已就绪。" }3.2 实战:一个完整的自动化分区格式化脚本
下面这个partition_and_format.sh脚本展示了如何用parted进行自动化分区。parted支持命令行参数,非常适合脚本调用。
#!/bin/bash # partition_and_format.sh - 自动分区并格式化eMMC set -e # 任何命令失败则立即退出脚本 # 引入工具函数 SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) source "$SCRIPT_DIR/utils.sh" # 配置变量 EMMC_DEV="/dev/mmcblk1" BOOT_SIZE="128MiB" BOOT_LABEL="BOOT" ROOTFS_LABEL="ROOTFS" echo "=== 开始自动化eMMC分区与格式化 ===" # 1. 检查设备 check_device "$EMMC_DEV" # 2. 卸载所有相关分区 for part in "${EMMC_DEV}p"*; do if [ -b "$part" ]; then mount_point=$(findmnt -n -o TARGET "$part" 2>/dev/null || true) if [ -n "$mount_point" ]; then safe_umount "$mount_point" fi fi done # 3. 清空分区表前几MB(重要!清除旧数据) echo "清空设备头部信息..." dd if=/dev/zero of="$EMMC_DEV" bs=1M count=10 status=progress sync # 4. 使用parted创建分区表和新分区 echo "创建新的GPT分区表..." parted -s "$EMMC_DEV" mklabel gpt echo "创建Boot分区 (FAT32)..." parted -s "$EMMC_DEV" mkpart primary fat32 1MiB "$BOOT_SIZE" parted -s "$EMMC_DEV" set 1 boot on # 设置引导标志 echo "创建Rootfs分区 (ext4),占用剩余空间..." parted -s "$EMMC_DEV" mkpart primary ext4 "$BOOT_SIZE" 100% # 5. 通知内核重新读取分区表 partprobe "$EMMC_DEV" sleep 2 # 等待分区设备节点生成 # 6. 等待分区设备出现并格式化 wait_for_device "${EMMC_DEV}p1" wait_for_device "${EMMC_DEV}p2" echo "格式化Boot分区为FAT32..." mkfs.vfat -F 32 -n "$BOOT_LABEL" "${EMMC_DEV}p1" echo "格式化Rootfs分区为ext4..." mkfs.ext4 -F -L "$ROOTFS_LABEL" "${EMMC_DEV}p2" echo "=== 分区与格式化完成! ===" echo "分区布局:" parted -s "$EMMC_DEV" print这个脚本比手动操作健壮得多。它用dd清除了旧分区信息,避免了残留数据导致的问题;使用partprobe强制内核更新分区表;并且每一步都有状态检查和等待。
3.3 实战:自动化烧录脚本
分区格式化好后,就是烧录。burn_system.sh脚本假设你的系统文件已经在SD卡上。
#!/bin/bash # burn_system.sh - 从SD卡烧录系统到eMMC set -e source "$(dirname "$0")/utils.sh" EMMC_BOOT_PART="/dev/mmcblk1p1" EMMC_ROOTFS_PART="/dev/mmcblk1p2" SD_BOOT_PART="/dev/mmcblk0p1" SD_ROOTFS_PART="/dev/mmcblk0p2" MOUNT_DIR="/tmp/emmc_burn" echo "=== 开始系统烧录 ===" # 检查源分区(SD卡)是否存在且已挂载 check_device "$SD_BOOT_PART" check_device "$SD_ROOTFS_PART" # 创建临时挂载点 mkdir -p "${MOUNT_DIR}/boot" "${MOUNT_DIR}/rootfs" "${MOUNT_DIR}/sd_boot" "${MOUNT_DIR}/sd_rootfs" # 挂载eMMC分区 mount "$EMMC_BOOT_PART" "${MOUNT_DIR}/boot" mount "$EMMC_ROOTFS_PART" "${MOUNT_DIR}/rootfs" # 挂载SD卡分区(如果未挂载) if ! findmnt -n "$SD_BOOT_PART"; then mount "$SD_BOOT_PART" "${MOUNT_DIR}/sd_boot" SD_BOOT_MOUNTED=1 else # 如果已经挂载,找到它的挂载点 SD_BOOT_PATH=$(findmnt -n -o TARGET "$SD_BOOT_PART") fi # 对rootfs做同样处理... # 开始拷贝 echo "烧录Bootloader和内核..." if [ -n "$SD_BOOT_PATH" ]; then cp -rv "$SD_BOOT_PATH"/* "${MOUNT_DIR}/boot/" else cp -rv "${MOUNT_DIR}/sd_boot"/* "${MOUNT_DIR}/boot/" fi echo "烧录根文件系统(这可能需要几分钟)..." rsync -a --info=progress2 "${MOUNT_DIR}/sd_rootfs"/ "${MOUNT_DIR}/rootfs"/ # 同步并卸载 sync umount "${MOUNT_DIR}/boot" "${MOUNT_DIR}/rootfs" if [ $SD_BOOT_MOUNTED -eq 1 ]; then umount "${MOUNT_DIR}/sd_boot" fi rm -rf "$MOUNT_DIR" echo "=== 系统烧录全部完成!请重启设备从eMMC启动。 ==="这个脚本我优化过几个地方:一是使用rsync代替cp来拷贝根文件系统,因为它可以显示进度,并且在中断后可以续传;二是仔细处理了挂载点,避免重复挂载或挂载失败。
4. 进阶:打造健壮的生产级烧录工具
当你需要为团队或者量产设计烧录方案时,脚本的健壮性和易用性就至关重要了。我们需要考虑更多边界情况。
4.1 增加交互与安全确认
生产脚本不能盲目执行,尤其是涉及擦除磁盘的操作。我会在脚本开头增加交互确认和参数解析。
#!/bin/bash # flash_emmc_pro.sh - 生产级eMMC烧录工具 TARGET_DEVICE="/dev/mmcblk1" SOURCE_SD="/dev/mmcblk0" # 使用getopts解析命令行参数,比如 -y 跳过确认 FORCE_CONFIRM=false while getopts "y" opt; do case $opt in y) FORCE_CONFIRM=true ;; *) echo "用法: $0 [-y]" ; exit 1 ;; esac done # 危险操作确认 if [ "$FORCE_CONFIRM" = false ]; then echo "警告:此操作将完全擦除设备 $TARGET_DEVICE 上的所有数据!" read -p "请确认设备型号正确,并输入 'YES' 继续: " confirmation if [ "$confirmation" != "YES" ]; then echo "操作已取消。" exit 0 fi fi # 再次通过lsblk显示设备信息,让用户二次确认 echo "当前系统存储设备列表:" lsblk -o NAME,SIZE,MODEL,VENDOR,TRAN echo "" read -p "请确认目标设备是 $TARGET_DEVICE (回车继续,Ctrl+C取消): " dummy4.2 实现烧录进度记录与校验
对于量产,记录每一台设备的烧录结果和校验信息是必须的。我们可以在脚本里加入MD5校验和日志记录。
# 在拷贝完成后,计算并对比关键文件的MD5 echo "开始文件校验..." BOOT_FILES="MLO u-boot.img zImage" for file in $BOOT_FILES; do if [ -f "${MOUNT_DIR}/sd_boot/$file" ]; then md5_src=$(md5sum "${MOUNT_DIR}/sd_boot/$file" | awk '{print $1}') md5_dst=$(md5sum "${MOUNT_DIR}/boot/$file" | awk '{print $1}') if [ "$md5_src" = "$md5_dst" ]; then echo "√ $file 校验通过" else err "文件 $file 校验失败!烧录可能不完整。" fi fi done # 将烧录结果和日期写入eMMC的一个特定文件,便于后期追溯 echo "写入烧录信息标签..." cat > "${MOUNT_DIR}/rootfs/etc/flash_info" << EOF FLASH_DATE=$(date) FLASH_SCRIPT_VERSION=1.2 TARGET_DEVICE=$TARGET_DEVICE EOF4.3 处理异常与错误恢复
脚本必须能处理各种异常,比如拷贝过程中SD卡被拔出。使用trap命令可以捕获中断信号,执行清理操作。
# 定义清理函数 cleanup() { echo "捕获到中断信号,正在清理..." # 尝试卸载所有挂载点 umount -A -l "${MOUNT_DIR}"/* 2>/dev/null || true rm -rf "$MOUNT_DIR" echo "清理完成。" exit 1 } # 设置trap,捕获SIGINT (Ctrl+C) 和 SIGTERM 信号 trap cleanup INT TERM # 在脚本主体中,如果出错也调用清理 # ... 脚本主要操作 ... # 脚本正常结束时,移除trap并执行清理 trap - INT TERM cleanup_for_exit() { umount -A -l "${MOUNT_DIR}"/* 2>/dev/null || true rm -rf "$MOUNT_DIR" } cleanup_for_exit把这些模块组合起来,你就得到了一个可以在产线上放心使用的烧录工具。它会有清晰的提示,严格的操作确认,完整的日志,以及可靠的错误恢复机制。我自己在多个量产项目中打磨类似的脚本,最大的体会就是:脚本的健壮性不是一蹴而就的,都是在解决一个个具体的、奇葩的现场问题中积累起来的。比如,有的设备内核识别eMMC分区较慢,就需要在partprobe后增加更长的等待时间,或者用循环检查代替固定sleep。再比如,拷贝大量小文件时,rsync比cp更可靠,但需要处理好符号链接和设备节点的拷贝问题。
从手动操作到自动化脚本,这个过程中你收获的远不止是效率的提升。每一次调试脚本,你都会对Linux的存储子系统、分区表结构、文件系统有更深的理解。当你最终看着自己编写的脚本在几分钟内干净利落地完成原本需要半小时手动重复劳动的工作时,那种成就感,就是工程师的快乐源泉。希望这篇长文能帮你少走些弯路,把时间花在更创造性的工作上。如果在实践中遇到什么古怪的问题,不妨多看看dmesg日志,那里面通常藏着答案。