目录
引言
测试套件中虽然有摄像头,但是没有屏幕,所以暂时还无法测试视觉相关的内容,只能等屏幕到货之后再说。今天测试一下SD卡的功能,这个也是我此次测试需要的。
SD卡的插槽在开发板的背面:

运行测试程序
官方提供了一个Titan_driver_sdcard的例子,我先编译运行一下。

程序运行之后,正确的识别了我的SD卡,进入sdcard目录后,也可以列出所有子目录。不过df命令没有实现,所以无法看剩余的空间。
EXIF信息的显示
接下来在当前的RT-Thread工程中实现一个EXIF信息显示命令,用于显示SD卡中图片文件的EXIF元数据信息。该命令支持JPEG格式图片,能够解析并显示相机参数、拍摄时间、GPS信息等EXIF标签。
有两个技术方案可以考虑:
- 方案一:使用轻量级EXIF解析库(如libexif或tinyexif)
- 方案二:实现简化的EXIF解析器,仅支持常用标签
这里采用方案二,实现精简版EXIF解析器,减少代码体积和内存占用。
我们将使用RT-Thread的MSH(Micro Shell)命令系统,增加一个命令exif,其命令格式:
exif <图片文件路径>
模块设计
增加如下模块:
exif_cmd/
├── exif_parser.c # EXIF解析核心模块
├── exif_parser.h # EXIF解析头文件
├── exif_cmd.c # MSH命令实现
└── SConscript # 构建脚本
EXIF解析模块(exif_parser)
该模块功能是解析JPEG文件的EXIF数据段,其主要接口:
- exif_parse_file(const char* filepath, exif_data_t* data):解析指定文件的EXIF信息
- exif_print_info(const exif_data_t* data):打印EXIF信息
MSH命令模块(exif_cmd)
该模块功能是实现MSH命令接口,其主要接口:
- exif(int argc, char** argv):命令处理函数
- 使用 `MSH_CMD_EXPORT` 宏导出命令
EXIF标签支持
基础标签(IFD0)包括:
- 相机厂商(Make)
- 相机型号(Model)
EXIF子IFD标签包括:
- 拍摄时间(DateTimeOriginal)
- 曝光时间(ExposureTime)
- 光圈值(FNumber)
- ISO感光度(ISOSpeedRatings)
- 焦距(FocalLength)
- 闪光灯(Flash)
工程集成说明
需要添加的文件
board/ports/exif_cmd/
├── exif_parser.c # EXIF解析核心模块
├── exif_parser.h # EXIF解析头文件
├── exif_cmd.c # MSH命令实现
└── SConscript # 构建脚本
需要修改的文件
board/Kconfig - 已添加以下配置项:
config BSP_USING_EXIF_CMD
bool "Enable EXIF command"
select BSP_USING_FILESYSTEM
default n
help
Enable EXIF information display command for image files
board/ports/SConscript - 无需修改(已自动扫描子目录)
集成步骤
-
确认文件已添加 board/ports/exif_cmd/ 目录及所有文件已创建
-
重新配置工程
menuconfig
启用:Hardware Drivers Config → Onboard Peripheral Drivers → Enable EXIF command
-
重新编译
-
烧录并测试
核心代码
exif_cmd.c
cpp
#include "exif_parser.h"
#include <rtthread.h>
#include <finsh.h>
static void exif(int argc, char **argv)
{
if (argc != 2) {
rt_kprintf("Usage: exif <image_file_path>\n");
rt_kprintf("Example: exif /sdcard/photo.jpg\n");
return;
}
const char *filepath = argv[1];
exif_data_t exif_data;
int ret = exif_parse_file(filepath, &exif_data);
if (ret != RT_EOK) {
switch (ret) {
case -RT_ENOENT:
rt_kprintf("Error: File not found: %s\n", filepath);
break;
case -RT_EIO:
rt_kprintf("Error: Failed to read file: %s\n", filepath);
break;
case -RT_ENOMEM:
rt_kprintf("Error: Not enough memory\n");
break;
case -RT_EINVAL:
rt_kprintf("Error: Invalid JPEG file\n");
break;
case -RT_ERROR:
rt_kprintf("Error: No EXIF data found or invalid EXIF format\n");
break;
default:
rt_kprintf("Error: Failed to parse EXIF data (code: %d)\n", ret);
break;
}
return;
}
ret = exif_print_info(&exif_data);
if (ret != RT_EOK) {
rt_kprintf("Error: Failed to print EXIF information\n");
}
}
MSH_CMD_EXPORT(exif, Display EXIF information of image file);
exif_parser.c
cpp
#include "exif_parser.h"
#include <dfs_file.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define JPEG_SOI 0xFFD8
#define JPEG_APP1 0xFFE1
#define EXIF_ID "Exif\x00\x00"
#define TIFF_LITTLE_ENDIAN 0x4949
#define TIFF_BIG_ENDIAN 0x4D4D
#define SCAN_BUFFER_SIZE 1024
typedef struct {
int fd;
uint8_t *buffer;
uint32_t buffer_size;
uint32_t buffer_offset;
uint32_t file_offset;
uint32_t buffer_start;
int is_little_endian;
} exif_reader_t;
typedef struct {
uint16_t tag;
uint16_t type;
uint32_t count;
uint32_t value_offset;
} ifd_entry_t;
static int read_data(exif_reader_t *reader, uint32_t offset, uint8_t *data, uint32_t size)
{
uint32_t file_offset = reader->file_offset + offset;
if (lseek(reader->fd, file_offset, SEEK_SET) < 0) {
return -RT_EIO;
}
if (read(reader->fd, data, size) != size) {
return -RT_EIO;
}
return RT_EOK;
}
static uint16_t read_u16(exif_reader_t *reader, uint32_t offset)
{
uint8_t data[2];
if (read_data(reader, offset, data, 2) != RT_EOK) {
return 0;
}
if (reader->is_little_endian) {
return data[0] | (data[1] << 8);
} else {
return (data[0] << 8) | data[1];
}
}
static uint32_t read_u32(exif_reader_t *reader, uint32_t offset)
{
uint8_t data[4];
if (read_data(reader, offset, data, 4) != RT_EOK) {
return 0;
}
if (reader->is_little_endian) {
return data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
} else {
return (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
}
}
static int find_exif_marker(int fd, uint32_t *exif_offset, uint32_t *exif_size)
{
uint8_t *buffer = rt_malloc(SCAN_BUFFER_SIZE);
if (buffer == RT_NULL) {
return -RT_ENOMEM;
}
uint32_t file_offset = 2;
uint32_t bytes_read;
int ret = -RT_ERROR;
while (1) {
if (lseek(fd, file_offset, SEEK_SET) < 0) {
ret = -RT_EIO;
break;
}
bytes_read = read(fd, buffer, SCAN_BUFFER_SIZE);
if (bytes_read < 4) {
break;
}
for (uint32_t i = 0; i + 4 <= bytes_read; ) {
uint16_t marker = (buffer[i] << 8) | buffer[i + 1];
if (marker == JPEG_APP1) {
if (i + 10 <= bytes_read &&
memcmp(buffer + i + 4, EXIF_ID, 6) == 0) {
*exif_offset = file_offset + i + 10;
*exif_size = (buffer[i + 2] << 8) | buffer[i + 3];
if (*exif_size > 8) {
*exif_size -= 8;
}
ret = RT_EOK;
break;
}
}
if (marker == 0xFFDA) {
ret = -RT_ERROR;
break;
}
if (i + 4 <= bytes_read) {
uint16_t length = (buffer[i + 2] << 8) | buffer[i + 3];
if (length < 2) {
length = 2;
}
i += length;
} else {
i++;
}
}
if (ret == RT_EOK || ret == -RT_EIO) {
break;
}
file_offset += bytes_read;
}
rt_free(buffer);
return ret;
}
static int parse_tiff_header(exif_reader_t *reader, uint32_t tiff_offset, uint32_t *ifd_offset)
{
uint16_t byte_order = read_u16(reader, tiff_offset);
if (byte_order == TIFF_LITTLE_ENDIAN) {
reader->is_little_endian = 1;
} else if (byte_order == TIFF_BIG_ENDIAN) {
reader->is_little_endian = 0;
} else {
return -RT_ERROR;
}
uint16_t magic = read_u16(reader, tiff_offset + 2);
if (magic != 42) {
return -RT_ERROR;
}
*ifd_offset = read_u32(reader, tiff_offset + 4);
if (*ifd_offset == 0 || *ifd_offset > reader->buffer_size) {
return -RT_ERROR;
}
return RT_EOK;
}
static void read_string_value(exif_reader_t *reader, uint32_t offset, uint32_t count, char *dest, uint32_t max_len)
{
uint32_t len = count;
if (len >= max_len) {
len = max_len - 1;
}
if (read_data(reader, offset, (uint8_t *)dest, len) == RT_EOK) {
dest[len] = '\0';
} else {
dest[0] = '\0';
}
}
static void read_rational_value(exif_reader_t *reader, uint32_t offset, char *dest, uint32_t max_len)
{
uint32_t numerator = read_u32(reader, offset);
uint32_t denominator = read_u32(reader, offset + 4);
if (denominator == 0) {
rt_snprintf(dest, max_len, "0");
} else {
rt_snprintf(dest, max_len, "%u/%u", numerator, denominator);
}
}
static void parse_ifd_entry(exif_reader_t *reader, uint32_t offset, ifd_entry_t *entry)
{
entry->tag = read_u16(reader, offset);
entry->type = read_u16(reader, offset + 2);
entry->count = read_u32(reader, offset + 4);
entry->value_offset = read_u32(reader, offset + 8);
}
static int parse_ifd(exif_reader_t *reader, uint32_t ifd_offset, exif_data_t *data,
uint32_t *exif_ifd_offset, uint32_t *gps_ifd_offset)
{
uint16_t num_entries = read_u16(reader, ifd_offset);
for (uint16_t i = 0; i < num_entries; i++) {
uint32_t entry_offset = ifd_offset + 2 + i * 12;
ifd_entry_t entry;
parse_ifd_entry(reader, entry_offset, &entry);
uint32_t value_size = 0;
switch (entry.type) {
case 1: value_size = entry.count * 1; break;
case 2: value_size = entry.count; break;
case 3: value_size = entry.count * 2; break;
case 4: value_size = entry.count * 4; break;
case 5: value_size = entry.count * 8; break;
}
uint32_t value_offset = (value_size <= 4) ? entry_offset + 8 : entry.value_offset;
switch (entry.tag) {
case 0x010F:
if (entry.type == 2 && entry.count < 64) {
read_string_value(reader, value_offset, entry.count, data->make, sizeof(data->make));
}
break;
case 0x0110:
if (entry.type == 2 && entry.count < 64) {
read_string_value(reader, value_offset, entry.count, data->model, sizeof(data->model));
}
break;
case 0x8769:
*exif_ifd_offset = entry.value_offset;
break;
case 0x8825:
*gps_ifd_offset = entry.value_offset;
break;
}
}
return RT_EOK;
}
static int parse_exif_ifd(exif_reader_t *reader, uint32_t ifd_offset, exif_data_t *data)
{
uint16_t num_entries = read_u16(reader, ifd_offset);
for (uint16_t i = 0; i < num_entries; i++) {
uint32_t entry_offset = ifd_offset + 2 + i * 12;
ifd_entry_t entry;
parse_ifd_entry(reader, entry_offset, &entry);
uint32_t value_size = 0;
switch (entry.type) {
case 1: value_size = entry.count * 1; break;
case 2: value_size = entry.count; break;
case 3: value_size = entry.count * 2; break;
case 4: value_size = entry.count * 4; break;
case 5: value_size = entry.count * 8; break;
}
uint32_t value_offset = (value_size <= 4) ? entry_offset + 8 : entry.value_offset;
switch (entry.tag) {
case 0x829A:
if (entry.type == 5) {
read_rational_value(reader, value_offset, data->exposure_time, sizeof(data->exposure_time));
}
break;
case 0x829D:
if (entry.type == 5) {
read_rational_value(reader, value_offset, data->f_number, sizeof(data->f_number));
}
break;
case 0x8827:
if (entry.type == 3) {
data->iso = (uint16_t)value_offset;
}
break;
case 0x920A:
if (entry.type == 5) {
read_rational_value(reader, value_offset, data->focal_length, sizeof(data->focal_length));
}
break;
case 0x9003:
if (entry.type == 2 && entry.count < 32) {
read_string_value(reader, value_offset, entry.count, data->datetime, sizeof(data->datetime));
}
break;
case 0x9209:
if (entry.type == 3) {
data->flash = (uint16_t)value_offset;
}
break;
}
}
return RT_EOK;
}
static int parse_gps_ifd(exif_reader_t *reader, uint32_t ifd_offset, exif_data_t *data)
{
uint16_t num_entries = read_u16(reader, ifd_offset);
char lat_ref = 'N';
char lon_ref = 'E';
uint32_t lat_offset = 0;
uint32_t lon_offset = 0;
uint32_t alt_offset = 0;
for (uint16_t i = 0; i < num_entries; i++) {
uint32_t entry_offset = ifd_offset + 2 + i * 12;
ifd_entry_t entry;
parse_ifd_entry(reader, entry_offset, &entry);
uint32_t value_size = 0;
switch (entry.type) {
case 1: value_size = entry.count * 1; break;
case 2: value_size = entry.count; break;
case 3: value_size = entry.count * 2; break;
case 4: value_size = entry.count * 4; break;
case 5: value_size = entry.count * 8; break;
}
uint32_t value_offset = (value_size <= 4) ? entry_offset + 8 : entry.value_offset;
switch (entry.tag) {
case 0x0001:
if (entry.type == 2 && entry.count > 0) {
uint8_t ref;
if (read_data(reader, value_offset, &ref, 1) == RT_EOK) {
lat_ref = ref;
}
}
break;
case 0x0002:
if (entry.type == 5 && entry.count == 3) {
lat_offset = value_offset;
}
break;
case 0x0003:
if (entry.type == 2 && entry.count > 0) {
uint8_t ref;
if (read_data(reader, value_offset, &ref, 1) == RT_EOK) {
lon_ref = ref;
}
}
break;
case 0x0004:
if (entry.type == 5 && entry.count == 3) {
lon_offset = value_offset;
}
break;
case 0x0006:
if (entry.type == 5) {
alt_offset = value_offset;
}
break;
}
}
if (lat_offset != 0) {
uint32_t deg_num = read_u32(reader, lat_offset);
uint32_t deg_den = read_u32(reader, lat_offset + 4);
uint32_t min_num = read_u32(reader, lat_offset + 8);
uint32_t min_den = read_u32(reader, lat_offset + 12);
uint32_t sec_num = read_u32(reader, lat_offset + 16);
uint32_t sec_den = read_u32(reader, lat_offset + 20);
if (deg_den != 0 && min_den != 0 && sec_den != 0) {
rt_snprintf(data->gps_latitude, sizeof(data->gps_latitude),
"%c %u°%u'%u\"", lat_ref,
deg_num / deg_den,
min_num / min_den,
sec_num / sec_den);
}
}
if (lon_offset != 0) {
uint32_t deg_num = read_u32(reader, lon_offset);
uint32_t deg_den = read_u32(reader, lon_offset + 4);
uint32_t min_num = read_u32(reader, lon_offset + 8);
uint32_t min_den = read_u32(reader, lon_offset + 12);
uint32_t sec_num = read_u32(reader, lon_offset + 16);
uint32_t sec_den = read_u32(reader, lon_offset + 20);
if (deg_den != 0 && min_den != 0 && sec_den != 0) {
rt_snprintf(data->gps_longitude, sizeof(data->gps_longitude),
"%c %u°%u'%u\"", lon_ref,
deg_num / deg_den,
min_num / min_den,
sec_num / sec_den);
}
}
if (alt_offset != 0) {
uint32_t num = read_u32(reader, alt_offset);
uint32_t den = read_u32(reader, alt_offset + 4);
if (den != 0) {
rt_snprintf(data->gps_altitude, sizeof(data->gps_altitude), "%um", num / den);
}
}
return RT_EOK;
}
int exif_parse_file(const char *filepath, exif_data_t *data)
{
int fd = open(filepath, O_RDONLY, 0);
if (fd < 0) {
return -RT_ENOENT;
}
uint8_t header[2];
if (read(fd, header, 2) != 2) {
close(fd);
return -RT_EIO;
}
uint16_t soi = (header[0] << 8) | header[1];
if (soi != JPEG_SOI) {
close(fd);
return -RT_EINVAL;
}
uint32_t exif_offset, exif_size;
int ret = find_exif_marker(fd, &exif_offset, &exif_size);
if (ret != RT_EOK) {
close(fd);
return ret;
}
exif_reader_t reader;
reader.fd = fd;
reader.buffer_size = exif_size;
reader.buffer_offset = 0;
reader.file_offset = exif_offset;
reader.buffer_start = 0;
reader.is_little_endian = 0;
uint32_t ifd_offset;
ret = parse_tiff_header(&reader, 0, &ifd_offset);
if (ret != RT_EOK) {
close(fd);
return ret;
}
rt_memset(data, 0, sizeof(exif_data_t));
uint32_t exif_ifd_offset = 0;
uint32_t gps_ifd_offset = 0;
parse_ifd(&reader, ifd_offset, data, &exif_ifd_offset, &gps_ifd_offset);
if (exif_ifd_offset != 0) {
parse_exif_ifd(&reader, exif_ifd_offset, data);
}
if (gps_ifd_offset != 0) {
parse_gps_ifd(&reader, gps_ifd_offset, data);
}
close(fd);
return RT_EOK;
}
int exif_print_info(const exif_data_t *data)
{
rt_kprintf("EXIF Information:\n");
rt_kprintf("==================\n");
if (data->make[0] != '\0') {
rt_kprintf("Make: %s\n", data->make);
}
if (data->model[0] != '\0') {
rt_kprintf("Model: %s\n", data->model);
}
if (data->datetime[0] != '\0') {
rt_kprintf("DateTime: %s\n", data->datetime);
}
if (data->exposure_time[0] != '\0') {
rt_kprintf("ExposureTime: %s sec\n", data->exposure_time);
}
if (data->f_number[0] != '\0') {
rt_kprintf("FNumber: f/%s\n", data->f_number);
}
if (data->iso != 0) {
rt_kprintf("ISO: %u\n", data->iso);
}
if (data->focal_length[0] != '\0') {
rt_kprintf("FocalLength: %s mm\n", data->focal_length);
}
if (data->flash != 0) {
rt_kprintf("Flash: %s\n", data->flash & 1 ? "On" : "Off");
}
if (data->gps_latitude[0] != '\0' || data->gps_longitude[0] != '\0') {
rt_kprintf("GPS: %s %s", data->gps_latitude, data->gps_longitude);
if (data->gps_altitude[0] != '\0') {
rt_kprintf(" Alt: %s", data->gps_altitude);
}
rt_kprintf("\n");
}
return RT_EOK;
}
exif_parser.h
cpp
#ifndef __EXIF_PARSER_H__
#define __EXIF_PARSER_H__
#include <rtthread.h>
#include <stdint.h>
#define EXIF_MAX_FILE_SIZE (100 * 1024 * 1024)
#define EXIF_SCAN_BUFFER_SIZE 4096
typedef struct {
char make[64];
char model[64];
char datetime[32];
char exposure_time[32];
char f_number[32];
uint16_t iso;
char focal_length[32];
uint16_t flash;
char gps_latitude[64];
char gps_longitude[64];
char gps_altitude[32];
} exif_data_t;
int exif_parse_file(const char *filepath, exif_data_t *data);
int exif_print_info(const exif_data_t *data);
#endif
SConscript
python
from building import *
src = []
cwd = GetCurrentDir()
# add exif_cmd src files.
src += Glob('*.c')
# add exif_cmd include path.
path = [cwd]
# add src and include to group.
group = DefineGroup('exif_cmd', src, depend = ['BSP_USING_EXIF_CMD'], CPPPATH = path)
Return('group')
关键实现要点
在资源受限的嵌入式系统中实现EXIF解析,主要面临以下挑战:
-
内存限制 :嵌入式系统内存有限,无法一次性加载大文件
-
栈空间不足 :大缓冲区会导致栈溢出
-
复杂的文件格式 :EXIF数据嵌套在JPEG文件中,需要多层解析
-
偏移量计算 :不同层级的偏移量基准点不同,容易出错
流式读取设计
传统方法会一次性读取整个文件到内存,这在嵌入式系统中是不可行的。我们采用流式读取方案:
cpp
uint8_t *buffer = rt_malloc(BUFFER_SIZE);
while (read(fd, buffer, BUFFER_SIZE) > 0) {
if (find_exif_marker(buffer, BUFFER_SIZE, &offset)) {
break;
}
}
优势 :
-
内存占用恒定(1KB缓冲区)
-
支持任意大小的文件
-
避免内存溢出
堆分配避免栈溢出
初始实现使用栈分配大缓冲区,导致硬件故障:
cpp
uint8_t buffer[4096]; // 栈溢出!
解决方案 :改用堆分配:
cpp
uint8_t *buffer = rt_malloc(BUFFER_SIZE);
if (buffer == NULL) {
return -RT_ENOMEM;
}
// 使用buffer...
rt_free(buffer);
关键点 :
-
嵌入式系统栈空间通常只有几KB
-
大缓冲区必须使用堆分配
-
使用后必须及时释放
精确的偏移量计算
EXIF格式采用多层嵌套结构,每层都有自己的偏移量基准:
文件起始
└─ JPEG SOI标记
└─ APP1段(EXIF数据)
└─ TIFF头
└─ IFD0(基础IFD)
└─ 子IFD(EXIF IFD、GPS IFD)
cpp
static int read_data(exif_reader_t *reader, uint32_t offset,
uint8_t *data, uint32_t size)
{
uint32_t file_offset = reader->file_offset + offset;
if (lseek(reader->fd, file_offset, SEEK_SET) < 0) {
return -RT_EIO;
}
if (read(reader->fd, data, size) != size) {
return -RT_EIO;
}
return RT_EOK;
}
IFD条目值定位
EXIF IFD条目中的值有两种存储方式:
- 值大小 ≤ 4字节:值直接存储在条目的最后4字节
- 值大小 > 4字节:值存储在value_offset指向的位置
cpp
uint32_t value_size = 0;
switch (entry.type) {
case 1: value_size = entry.count * 1; break; // BYTE
case 2: value_size = entry.count; break; // ASCII
case 3: value_size = entry.count * 2; break; // SHORT
case 4: value_size = entry.count * 4; break; // LONG
case 5: value_size = entry.count * 8; break; // RATIONAL
}
uint32_t value_offset = (value_size <= 4) ?
entry_offset + 8 : entry.value_offset;
运行结果
程序的运行结果如下:
cpp
\ | /
- RT - Thread Operating System
/ | \ 5.1.0 build Jan 26 2026 19:38:57
2006 - 2024 Copyright by RT-Thread team
[I/SDIO] SD card capacity 15267840 KB.
found part[0], begin: 4194304, size: 14.570GB
[I/app.filesystem] SD card mount to '/sdcard'
Hello RT-Thread!
==================================================
This example project is the sdio routine!
==================================================
msh />exif /sdcard/DCIM/100MSDCF/DSC07468.JPG
EXIF Information:
==================
Make: SONY
Model: ILCE-5100
DateTime: 2025:02:15 17:21:32
ExposureTime: 1/40 sec
FNumber: f/56/10
ISO: 410
FocalLength: 380/10 mm
Flash: Off
总结
在嵌入式系统中实现EXIF解析,关键在于:
-
内存管理:使用流式读取和堆分配,避免栈溢出
-
偏移量计算:精确理解EXIF格式的多层嵌套结构
-
错误处理:完善的错误检查和资源释放
-
性能优化:平衡内存占用和解析效率
通过合理的架构设计和细致的实现,我们成功在资源受限的RT-Thread系统中实现了完整的EXIF信息解析功能,为嵌入式图像处理提供了有力支持。 未来的扩展方向包括支持更多图片格式(PNG、TIFF),添加更多EXIF标签,实现EXIF信息导出功能以及支持自定义标签解析。