单片机导出Excel,再也不用求Windows了!这个纯C库让我少加班3小时
- 2026-11-05 00:01:36
嵌入式工程师的数据导出神器,纯C实现,单片机也能跑!

扎心了,这些场景你遇到过吗?
场景一: 项目验收,客户要"专业的数据报表"。你导了个CSV文件过去,客户打开一看——乱码!表格错位!客户当场黑脸:"这就是你们的专业水平?"
场景二: 老板要看测试数据,你发了CSV。老板用Excel打开,数字变成了科学计数法,日期显示成了一串数字。老板问你:"这数据怎么看?"
场景三: 产品需要导出带图表的分析报告。CSV?不好意思,不支持图表。你只能硬着头皮说:"这个功能做不了..."
场景四: 采集了10万条传感器数据,用CSV导出,文件体积爆炸,打开卡成PPT,客户直接崩溃。
兄弟们,我懂你们的痛!
CSV这玩意儿,简单是简单,但真到了要"拿得出手"的时候,它就是不行:
❌ 不支持格式设置(字体、颜色、边框) ❌ 不支持公式计算 ❌ 不支持图表 ❌ 不支持图片插入 ❌ 不支持下拉列表、数据验证 ❌ 中文乱码问题让人抓狂
那怎么办?
用Python的openpyxl?可以,但你的产品是单片机,跑不了Python。 用C++的库?可以,但你的项目是纯C,不想引入C++编译器。 用COM接口操作Excel?可以,但你的设备是嵌入式Linux,没有Office。
今天,我要给你介绍一个神器——libxlsxwriter!
libxlsxwriter是什么?
libxlsxwriter是一个纯C语言编写的库,专门用来创建Excel 2007+的XLSX文件。
划重点:纯C!纯C!纯C!
这意味着什么?
✅ 你的STM32项目可以直接用 ✅ 不需要C++编译器 ✅ 跨平台,Linux/Windows/FreeBSD通吃 ✅ 只有一个依赖:zlib(压缩库,基本都有)
核心特性一览:
许可证是FreeBSD,商用完全没问题!
先看个简单的"Hello World"

编译运行:
/** 使用 libxlsxwriter 将一些数据写入一个简单的 Excel 文件的示例。** 版权所有 2014-2026,John McNamara,jmcnamara@cpan.org**/#include"xlsxwriter.h"intmain(){lxw_workbook *workbook = workbook_new("hello_world.xlsx");lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL);worksheet_write_string(worksheet, 0, 0, "Hello", NULL);worksheet_write_number(worksheet, 1, 0, 123, NULL);workbook_close(workbook);return 0;}
打开生成的demo.xlsx,你会看到:
第一行:Hello 第二行:123
就问你简单不简单?
实战案例:温度采集系统生成专业报表
好了,光说不练假把式。我们来做一个完整的实战案例——温度采集数据生成Excel报表。
需求分析
假设你正在做一个工业温度监控系统:
每10秒采集一次温度 需要记录时间、温度值、状态 要计算平均值、最大值、最小值 要生成温度趋势图 超温的数据要标红 报表要专业,拿得出手
用CSV?做梦吧!用libxlsxwriter?分分钟搞定!
完整代码实现
#include"xlsxwriter.h"#include<time.h>#include<stdio.h>// 模拟温度数据结构typedef struct {char timestamp[32]; // 时间戳float temperature; // 温度值int status; // 状态:0-正常,1-警告,2-报警} TempRecord;// 模拟生成温度数据voidgenerate_temp_data(TempRecord *records, int count){time_t now = time(NULL);for (int i = 0; i < count; i++) {// 生成时间戳time_t t = now - (count - i) * 10; // 每10秒一条struct tm *tm_info = localtime(&t);strftime(records[i].timestamp, 32, "%Y-%m-%d %H:%M:%S", tm_info);// 生成温度值(模拟20-80度之间的波动)records[i].temperature = 35.0f + (float)(rand() % 450) / 10.0f;// 根据温度设置状态if (records[i].temperature < 50.0f) {records[i].status = 0; // 正常} else if (records[i].temperature < 70.0f) {records[i].status = 1; // 警告} else {records[i].status = 2; // 报警}}}intmain(){// 模拟100条温度数据const int RECORD_COUNT = 100;TempRecord records[RECORD_COUNT];generate_temp_data(records, RECORD_COUNT);// 创建工作簿lxw_workbook *workbook = workbook_new("temperature_report.xlsx");// ========== 创建各种格式 ==========// 标题格式:大号、粗体、蓝色背景lxw_format *title_format = workbook_add_format(workbook);format_set_bold(title_format);format_set_font_size(title_format, 16);format_set_bg_color(title_format, LXW_COLOR_BLUE); // Excel蓝format_set_font_color(title_format, LXW_COLOR_WHITE);format_set_align(title_format, LXW_ALIGN_CENTER);format_set_align(title_format, LXW_ALIGN_VERTICAL_CENTER);// 表头格式:粗体、灰色背景lxw_format *header_format = workbook_add_format(workbook);format_set_bold(header_format);format_set_bg_color(header_format, LXW_COLOR_GRAY);format_set_border(header_format, LXW_BORDER_THIN);format_set_align(header_format, LXW_ALIGN_CENTER);// 普通数据格式lxw_format *data_format = workbook_add_format(workbook);format_set_border(data_format, LXW_BORDER_THIN);format_set_align(data_format, LXW_ALIGN_CENTER);// 温度数字格式:保留1位小数lxw_format *temp_format = workbook_add_format(workbook);format_set_border(temp_format, LXW_BORDER_THIN);format_set_align(temp_format, LXW_ALIGN_CENTER);format_set_num_format(temp_format, "0.0");// 警告格式:黄色背景lxw_format *warning_format = workbook_add_format(workbook);format_set_border(warning_format, LXW_BORDER_THIN);format_set_bg_color(warning_format, LXW_COLOR_YELLOW);format_set_align(warning_format, LXW_ALIGN_CENTER);format_set_num_format(warning_format, "0.0");// 报警格式:红色背景、白色字体lxw_format *alarm_format = workbook_add_format(workbook);format_set_border(alarm_format, LXW_BORDER_THIN);format_set_bg_color(alarm_format, LXW_COLOR_RED);format_set_font_color(alarm_format, LXW_COLOR_WHITE);format_set_align(alarm_format, LXW_ALIGN_CENTER);format_set_num_format(alarm_format, "0.0");// 统计格式:粗体、绿色lxw_format *stat_format = workbook_add_format(workbook);format_set_bold(stat_format);format_set_font_color(stat_format, LXW_COLOR_GREEN);format_set_align(stat_format, LXW_ALIGN_CENTER);// ========== 数据表工作表 ==========lxw_worksheet *data_sheet = workbook_add_worksheet(workbook, "温度数据");// 设置列宽worksheet_set_column(data_sheet, 0, 0, 20, NULL); // 时间列worksheet_set_column(data_sheet, 1, 1, 12, NULL); // 温度列worksheet_set_column(data_sheet, 2, 2, 12, NULL); // 状态列// 写入标题worksheet_merge_range(data_sheet, 0, 0, 0, 2, "温度监控数据报表", title_format);worksheet_set_row(data_sheet, 0, 30, NULL); // 设置行高// 写入表头worksheet_write_string(data_sheet, 2, 0, "时间", header_format);worksheet_write_string(data_sheet, 2, 1, "温度(°C)", header_format);worksheet_write_string(data_sheet, 2, 2, "状态", header_format);// 写入数据float sum = 0, max_temp = 0, min_temp = 100;int warning_count = 0, alarm_count = 0;for (int i = 0; i < RECORD_COUNT; i++) {int row = i + 3;// 根据状态选择格式lxw_format *current_format = data_format;const char *status_str = "正常";if (records[i].status == 1) {current_format = warning_format;status_str = "警告";warning_count++;} else if (records[i].status == 2) {current_format = alarm_format;status_str = "报警";alarm_count++;}// 写入数据worksheet_write_string(data_sheet, row, 0, records[i].timestamp, data_format);worksheet_write_number(data_sheet, row, 1, records[i].temperature, current_format);worksheet_write_string(data_sheet, row, 2, status_str, data_format);// 统计计算sum += records[i].temperature;if (records[i].temperature > max_temp) max_temp = records[i].temperature;if (records[i].temperature < min_temp) min_temp = records[i].temperature;}float avg_temp = sum / RECORD_COUNT;// 添加空行int stat_row = RECORD_COUNT + 4;// 写入统计信息worksheet_write_string(data_sheet, stat_row, 0, "统计信息", header_format);worksheet_merge_range(data_sheet, stat_row, 0, stat_row, 2, "", header_format);worksheet_write_string(data_sheet, stat_row + 1, 0, "平均温度", data_format);worksheet_write_number(data_sheet, stat_row + 1, 1, avg_temp, stat_format);worksheet_write_string(data_sheet, stat_row + 1, 2, "°C", data_format);worksheet_write_string(data_sheet, stat_row + 2, 0, "最高温度", data_format);worksheet_write_number(data_sheet, stat_row + 2, 1, max_temp, stat_format);worksheet_write_string(data_sheet, stat_row + 2, 2, "°C", data_format);worksheet_write_string(data_sheet, stat_row + 3, 0, "最低温度", data_format);worksheet_write_number(data_sheet, stat_row + 3, 1, min_temp, stat_format);worksheet_write_string(data_sheet, stat_row + 3, 2, "°C", data_format);worksheet_write_string(data_sheet, stat_row + 4, 0, "警告次数", data_format);worksheet_write_number(data_sheet, stat_row + 4, 1, warning_count, stat_format);worksheet_write_string(data_sheet, stat_row + 4, 2, "次", data_format);worksheet_write_string(data_sheet, stat_row + 5, 0, "报警次数", data_format);worksheet_write_number(data_sheet, stat_row + 5, 1, alarm_count, stat_format);worksheet_write_string(data_sheet, stat_row + 5, 2, "次", data_format);// 添加自动筛选worksheet_autofilter(data_sheet, 2, 0, RECORD_COUNT + 2, 2);// ========== 图表工作表 ==========lxw_worksheet *chart_sheet = workbook_add_worksheet(workbook, "温度趋势图");// 创建图表lxw_chart *chart = workbook_add_chart(workbook, LXW_CHART_LINE);chart_title_set_name(chart, "温度变化趋势");chart_set_size(chart, 800, 400);// 添加数据系列lxw_chart_series *series = chart_add_series(chart,"=温度数据!$A$4:$A$103", // X轴:时间"=温度数据!$B$4:$B$103"); // Y轴:温度chart_series_set_name(series, "温度(°C)");// 设置Y轴范围chart_set_y_axis(chart, 15, 85, 10);// 插入图表worksheet_insert_chart(chart_sheet, 1, 1, chart);// ========== 保存并关闭 ==========workbook_close(workbook);printf("报表生成成功:temperature_report.xlsx\n");printf("共 %d 条数据\n", RECORD_COUNT);printf("平均温度:%.1f°C\n", avg_temp);printf("最高温度:%.1f°C\n", max_temp);printf("最低温度:%.1f°C\n", min_temp);return 0;}
运行效果
编译运行后,你会得到一个temperature_report.xlsx文件,打开看看:


数据表:
漂亮的标题栏,蓝色背景白字 整齐的数据表格,带边框 超温数据自动标红 底部有完整的统计信息 支持自动筛选
图表表:
温度变化趋势折线图 清晰的时间轴 专业的配色
就问你,这报表拿出去,客户还能挑出毛病吗?
移植到STM32平台
好了,PC上跑通了,但我们的目标是嵌入式设备!下面讲解如何移植到STM32。
1. 准备源码
从GitHub下载libxlsxwriter源码:
git clone https://github.com/jmcnamara/libxlsxwriter.git2. 需要的文件
libxlsxwriter的核心文件不多,只需要这些:
src/├── xlsxwriter.h # 主头文件├── workbook.c/h # 工作簿操作├── worksheet.c/h # 工作表操作├── format.c/h # 格式设置├── chart.c/h # 图表├── shared_strings.c/h # 字符串共享├── styles.c/h # 样式├── app.c/h # 应用属性├── content_types.c/h # 内容类型├── core.c/h # 核心属性├── relationships.c/h # 关系定义├── drawing.c/h # 绘图├── utility.c/h # 工具函数├── xmlwriter.c/h # XML写入├── tmpfileplus.c/h # 临时文件└── third_party/└── minizip/ # zip压缩(可选,可用FatFS替代)
3. 移植要点
要点一:文件系统
libxlsxwriter默认使用标准C的文件操作(fopen/fwrite等)。在STM32上,你需要:
使用FatFS文件系统 重定义文件操作函数
创建一个my_io.c:
#include"xlsxwriter.h"#include"ff.h"// FatFS头文件// 自定义文件句柄结构typedef struct {FIL fil;int is_open;} my_file_handle;// 打开文件void *my_open(constchar *filename, constchar *mode){my_file_handle *handle = malloc(sizeof(my_file_handle));if (!handle) return NULL;BYTE fatfs_mode = 0;if (strchr(mode, 'w')) fatfs_mode |= FA_WRITE | FA_CREATE_ALWAYS;if (strchr(mode, 'r')) fatfs_mode |= FA_READ;if (strchr(mode, 'a')) fatfs_mode |= FA_WRITE | FA_OPEN_APPEND;FRESULT res = f_open(&handle->fil, filename, fatfs_mode);if (res != FR_OK) {free(handle);return NULL;}handle->is_open = 1;return handle;}// 写入文件size_tmy_write(void *handle, constvoid *ptr, size_t size){my_file_handle *h = (my_file_handle *)handle;UINT written;f_write(&h->fil, ptr, size, &written);return written;}// 关闭文件intmy_close(void *handle){my_file_handle *h = (my_file_handle *)handle;if (h->is_open) {f_close(&h->fil);h->is_open = 0;}free(h);return 0;}// 注册自定义IOvoidinit_xlsxwriter_io(void){lxw_workbook_options options = {.constant_memory = LXW_FALSE,.tmpdir = NULL,.use_zip64 = LXW_FALSE,.output_buffer = NULL,.output_buffer_size = NULL};// 需要在libxlsxwriter中修改或使用自定义编译选项}
要点二:内存管理
STM32内存有限,建议:
- 使用常量内存模式:
lxw_workbook_options options = {.constant_memory = LXW_TRUE, // 启用常量内存模式.tmpdir = "/tmp" // 临时文件目录};lxw_workbook *workbook = workbook_new_opt("report.xlsx", &options);
常量内存模式会将数据写入临时文件,而不是全部保存在内存中,适合大文件。
限制同时打开的工作表数量
避免大量格式对象:复用格式对象,不要每个单元格都创建新格式
要点三:压缩库
libxlsxwriter依赖zlib压缩。在STM32上:
使用精简版zlib(如miniz) 或者关闭压缩(文件会变大,但省内存)
要点四:堆栈大小
确保堆栈足够大,建议:
堆(Heap):至少32KB 栈(Stack):至少8KB
4. STM32完整示例
#include"main.h"#include"xlsxwriter.h"#include"ff.h"#include<stdio.h>#include<string.h>// 温度传感器接口externfloatTEMP_SENSOR_Read(void);externvoidRTC_GetTimestamp(char *buf, uint8_t len);// SD卡文件系统extern FATFS SDFatFS;// 生成温度报表voidGenerate_Temperature_Report(void){FRESULT res;// 挂载SD卡res = f_mount(&SDFatFS, "0:", 1);if (res != FR_OK) {printf("SD卡挂载失败!\n");return;}printf("正在生成报表...\n");// 创建工作簿(使用常量内存模式)lxw_workbook_options options = {.constant_memory = LXW_TRUE,.tmpdir = "0:/tmp"};lxw_workbook *workbook = workbook_new_opt("0:/report.xlsx", &options);if (!workbook) {printf("创建工作簿失败!\n");return;}// 创建工作表lxw_worksheet *worksheet = workbook_add_worksheet(workbook, "温度数据");// 创建格式lxw_format *header_format = workbook_add_format(workbook);format_set_bold(header_format);format_set_bg_color(header_format, LXW_COLOR_GRAY);format_set_align(header_format, LXW_ALIGN_CENTER);lxw_format *temp_format = workbook_add_format(workbook);format_set_num_format(temp_format, "0.00");format_set_align(temp_format, LXW_ALIGN_CENTER);// 设置列宽worksheet_set_column(worksheet, 0, 0, 20, NULL);worksheet_set_column(worksheet, 1, 1, 12, NULL);// 写入表头worksheet_write_string(worksheet, 0, 0, "时间", header_format);worksheet_write_string(worksheet, 0, 1, "温度(°C)", header_format);// 读取并写入数据(假设有100条历史数据)for (int i = 0; i < 100; i++) {char timestamp[32];float temp;// 从Flash或EEPROM读取历史数据// 这里简化处理,实际项目中从存储介质读取RTC_GetTimestamp(timestamp, sizeof(timestamp));temp = TEMP_SENSOR_Read();worksheet_write_string(worksheet, i + 1, 0, timestamp, NULL);worksheet_write_number(worksheet, i + 1, 1, temp, temp_format);// 每10条打印一次进度if ((i + 1) % 10 == 0) {printf("已写入 %d/100 条数据...\n", i + 1);}HAL_Delay(10); // 小延时,避免看门狗复位}// 关闭工作簿workbook_close(workbook);printf("✅ 报表生成成功: 0:/report.xlsx\n");// 卸载SD卡f_mount(NULL, "0:", 0);}intmain(void){HAL_Init();SystemClock_Config();// 初始化外设MX_GPIO_Init();MX_SPI1_Init(); // SD卡SPIMX_FATFS_Init();MX_RTC_Init();MX_ADC1_Init(); // 温度传感器MX_USART1_UART_Init(); // 调试串口printf("=== 温度监控系统启动 ===\n");while (1) {// 按键触发生成报表if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) {HAL_Delay(20); // 消抖if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) {Generate_Temperature_Report();while (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET);}}HAL_Delay(10);}}
5. 内存占用参考
在STM32F407(168MHz,192KB RAM)上测试:
结论:STM32F1/F4系列完全没问题!
进阶技巧
1. 条件格式
让超温数据自动变色:
// 创建条件格式lxw_conditional_format *conditional_format =workbook_add_conditional_format(workbook);conditional_format->type = LXW_CONDITIONAL_TYPE_CELL;conditional_format->criteria = LXW_CONDITIONAL_CRITERIA_GREATER_THAN;conditional_format->value = 70.0;conditional_format->format = alarm_format;// 应用到温度列worksheet_conditional_format_range(worksheet, 3, 1, 102, 1, conditional_format);
2. 数据验证(下拉列表)
添加状态选择的下拉列表:
lxw_data_validation *validation = workbook_add_data_validation(workbook);validation->validate = LXW_VALIDATION_TYPE_LIST;validation->criteria = LXW_VALIDATION_CRITERIA_EQUAL_TO;validation->value_list = "正常,警告,报警";validation->input_title = "请选择状态";validation->input_message = "从列表中选择当前状态";worksheet_data_validation_cell(worksheet, 3, 2, validation);
3. 公式计算
使用Excel公式自动计算:
// 写入平均值公式worksheet_write_formula(worksheet, stat_row, 1,"=AVERAGE(B4:B103)", stat_format);// 写入最大值公式worksheet_write_formula(worksheet, stat_row + 1, 1,"=MAX(B4:B103)", stat_format);
4. 超链接
添加跳转链接:
worksheet_write_url(worksheet, row, 3,"https://www.example.com/detail?id=123",link_format);
📈 性能对比
libxlsxwriter是嵌入式设备生成Excel的最佳选择,没有之一!
总结
兄弟们,今天给大家介绍了libxlsxwriter这个神器:
✅ 纯C编写,单片机完美支持 ✅ 功能强大,图表、公式、格式一应俱全 ✅ 100%兼容Excel,客户挑不出毛病 ✅ FreeBSD许可,商用无忧 ✅ 内存优化模式,大文件也能处理
以后别再给客户导CSV了,直接用libxlsxwriter生成专业Excel报表,让老板和客户都满意!
相关资源
- GitHub仓库:
https://github.com/jmcnamara/libxlsxwriter - 官方文档:
https://libxlsxwriter.github.io/ - 示例代码:
https://github.com/jmcnamara/libxlsxwriter/tree/main/examples
往期推荐