-
C语言作为现代编程语言的基石,其简洁高效的特性使其在系统编程、嵌入式开发等领域占据重要地位。本文将深入剖析C语言的基础语法要素,通过丰富的代码示例帮助读者构建扎实的编程基础。一、C语言发展历史与应用领域发展历程C语言诞生于1972年,由贝尔实验室的Dennis Ritchie在开发UNIX操作系统时创建。其发展脉络如下:1978年:K&R C(《The C Programming Language》第一版发布)1989年:ANSI C(C89标准)1999年:C99标准(引入新特性)2011年:C11标准(多线程支持等)典型应用场景操作系统开发:Linux内核、Windows驱动嵌入式系统:单片机程序、物联网设备高性能计算:科学计算、算法优化编译器设计:GCC、LLVM等编译器实现数据库系统:MySQL、Redis等底层实现二、第一个C程序:结构与编译过程标准程序结构/* 预处理指令 */ #include <stdio.h> // 标准输入输出头文件 #include <stdlib.h> // 标准库头文件 /* 全局声明区 */ #define MAX 100 // 宏定义常量 int global_var; // 全局变量 /* 主函数 */ int main() { // 程序入口 /* 局部变量声明 */ int local_var = 10; /* 执行语句 */ printf("Hello World!\n"); printf("MAX value: %d\n", MAX); return EXIT_SUCCESS; // 程序结束状态 } 编译过程详解预处理阶段(gcc -E)处理#include指令,展开头文件替换宏定义删除注释gcc -E hello.c -o hello.i编译阶段(gcc -S)语法分析语义分析生成汇编代码gcc -S hello.i -o hello.s汇编阶段(gcc -c)将汇编代码转换为机器指令生成目标文件(.o)gcc -c hello.s -o hello.o链接阶段合并多个目标文件链接库函数生成可执行文件gcc hello.o -o hello三、数据类型详解与示例1. 整型数据类型类型存储大小(32位)格式说明符值范围char1字节%c-128到127unsigned char1字节%c0到255short2字节%hd-32,768到32,767unsigned short2字节%hu0到65,535int4字节%d-2,147,483,648到2,147,483,647unsigned int4字节%u0到4,294,967,295long4字节%ld同intlong long8字节%lld-9.2×10¹⁸到9.2×10¹⁸示例程序:#include <stdio.h> #include <limits.h> int main() { printf("char范围: %d 到 %d\n", CHAR_MIN, CHAR_MAX); printf("int范围: %d 到 %d\n", INT_MIN, INT_MAX); // 溢出示例 int overflow = INT_MAX + 1; printf("溢出结果: %d\n", overflow); // 输出-2147483648 return 0; } 2. 浮点类型类型存储大小精度格式说明符值范围float4字节6-7位有效数字%f1.2E-38到3.4E+38double8字节15-16位有效数字%lf2.3E-308到1.7E+308示例程序:#include <stdio.h> #include <float.h> int main() { float f = 3.1415926f; // 注意f后缀 double d = 3.141592653589793; printf("float精度: %d位小数\n", FLT_DIG); printf("double精度: %d位小数\n", DBL_DIG); printf("float值: %.7f\n", f); // 输出3.1415925 printf("double值: %.15lf\n", d); // 科学计数法表示 printf("科学计数法: %e\n", 0.00001234); // 输出1.234000e-05 return 0; } 3. 字符类型示例程序:#include <stdio.h> int main() { char c1 = 'A'; // 字符常量 char c2 = 65; // ASCII码 char c3 = '\x41'; // 十六进制表示 char c4 = '\101'; // 八进制表示 printf("字符输出: %c %c %c %c\n", c1, c2, c3, c4); printf("ASCII码: %d\n", c1); // 输出65 // 转义字符示例 printf("特殊字符: \\ \' \" \n\t换行和制表符\n"); return 0; } 4. 枚举类型示例程序:#include <stdio.h> // 定义枚举类型 enum Color { RED=1, GREEN, BLUE=5, YELLOW }; int main() { enum Color c1 = GREEN; enum Color c2 = YELLOW; printf("GREEN的值: %d\n", c1); // 输出2 printf("YELLOW的值: %d\n", c2); // 输出6 // 枚举遍历 for(enum Color c = RED; c <= YELLOW; c++) { printf("枚举值: %d\n", c); } return 0; } 5. void类型三种主要用途:函数返回类型void print_message() { printf("This function returns nothing\n"); } 函数参数int rand(void); // 无参数函数 通用指针void *ptr; // 可以指向任何数据类型 四、常量与变量详解1. 常量定义方式#define宏常量:#define PI 3.14159 #define MAX(a,b) ((a) > (b) ? (a) : (b)) // 带参数宏 int main() { double area = PI * 5.0 * 5.0; printf("最大值为: %d\n", MAX(10, 20)); return 0; } const常量:int main() { const int SIZE = 100; const float TAX_RATE = 0.2f; // SIZE = 200; // 错误:不能修改const常量 int array[SIZE]; // 可用于数组大小(C99之后) return 0; } 2. 变量命名规范合法命名示例:int student_count; float averageScore; double _width; char *ptr_to_name; 非法命名示例:int 2nd_place; // 数字开头 float total-sum; // 包含连字符 char switch; // 使用关键字 3. 变量作用域示例程序:#include <stdio.h> int global = 100; // 全局变量 void test() { int local = 50; // 局部变量 static int persistent = 0; // 静态局部变量 persistent++; printf("local: %d, persistent: %d\n", local, persistent); } int main() { int local = 10; // 与test()中的local不同 printf("global: %d\n", global); printf("main local: %d\n", local); test(); // 输出: local: 50, persistent: 1 test(); // 输出: local: 50, persistent: 2 return 0; } 五、运算符详解与示例1. 算术运算符示例程序:#include <stdio.h> int main() { int a = 10, b = 3; printf("加法: %d\n", a + b); // 13 printf("减法: %d\n", a - b); // 7 printf("乘法: %d\n", a * b); // 30 printf("除法: %d\n", a / b); // 3 printf("取模: %d\n", a % b); // 1 // 自增自减 int c = 5; printf("后置++: %d\n", c++); // 5 (先使用后增加) printf("当前值: %d\n", c); // 6 printf("前置++: %d\n", ++c); // 7 (先增加后使用) return 0; } 2. 关系运算符示例程序:#include <stdio.h> int main() { int x = 10, y = 20; printf("x > y: %d\n", x > y); // 0 (false) printf("x < y: %d\n", x < y); // 1 (true) printf("x == y: %d\n", x == y); // 0 printf("x != y: %d\n", x != y); // 1 // 浮点数比较技巧 float f1 = 1.23456789f; float f2 = 1.23456788f; float epsilon = 0.000001f; if(fabs(f1 - f2) < epsilon) { printf("浮点数相等\n"); } else { printf("浮点数不相等\n"); } return 0; } 3. 逻辑运算符示例程序:#include <stdio.h> #include <stdbool.h> // 引入bool类型(C99) int main() { bool a = true, b = false; printf("a && b: %d\n", a && b); // 0 printf("a || b: %d\n", a || b); // 1 printf("!a: %d\n", !a); // 0 // 短路求值示例 int i = 0; if(i != 0 && 10/i > 1) { // 不会发生除零错误 printf("条件成立\n"); } else { printf("条件不成立\n"); } return 0; } 4. 位运算符示例程序:#include <stdio.h> void print_binary(unsigned int num) { for(int i = 31; i >= 0; i--) { printf("%d", (num >> i) & 1); if(i % 4 == 0) printf(" "); } printf("\n"); } int main() { unsigned int a = 0b1100; // 12 unsigned int b = 0b1010; // 10 printf("a: "); print_binary(a); printf("b: "); print_binary(b); printf("a & b: "); print_binary(a & b); // 位与: 1000 (8) printf("a | b: "); print_binary(a | b); // 位或: 1110 (14) printf("a ^ b: "); print_binary(a ^ b); // 位异或: 0110 (6) printf("~a: "); print_binary(~a); // 位取反 printf("a << 2: "); print_binary(a << 2); // 左移: 110000 (48) printf("b >> 1: "); print_binary(b >> 1); // 右移: 0101 (5) // 位操作应用:设置位 unsigned int flags = 0; flags |= 0b1000; // 设置第4位 printf("设置后flags: "); print_binary(flags); // 检查位 if(flags & 0b1000) { printf("第4位已设置\n"); } return 0; } 5. 赋值运算符示例程序:#include <stdio.h> int main() { int a = 10; a += 5; // 等价于 a = a + 5 printf("a += 5: %d\n", a); // 15 a -= 3; // a = a - 3 printf("a -= 3: %d\n", a); // 12 a *= 2; // a = a * 2 printf("a *= 2: %d\n", a); // 24 a /= 4; // a = a / 4 printf("a /= 4: %d\n", a); // 6 a %= 5; // a = a % 5 printf("a %%= 5: %d\n", a); // 1 // 复合位赋值 unsigned int b = 0b1010; b &= 0b1100; // b = b & 0b1100 printf("b &= 0b1100: %d\n", b); // 8 (0b1000) return 0; } 6. 条件运算符示例程序:#include <stdio.h> int main() { int score = 85; // 传统if-else if(score >= 60) { printf("及格\n"); } else { printf("不及格\n"); } // 条件运算符实现 printf(score >= 60 ? "及格\n" : "不及格\n"); // 嵌套条件运算符 char *result = (score >= 90) ? "优秀" : (score >= 80) ? "良好" : (score >= 70) ? "中等" : (score >= 60) ? "及格" : "不及格"; printf("评价: %s\n", result); return 0; } 7. 运算符优先级与结合性优先级表(部分常见运算符):优先级运算符结合性1() [] -> . ++ – (后缀)从左到右2! ~ ++ – + - * & (type) sizeof从右到左3* / %从左到右4+ -从左到右5<< >>从左到右6< <= > >=从左到右7== !=从左到右8&从左到右9^从左到右1011&&从左到右1213?:从右到左14= += -= *= /= %= &= ^== <<= >>=15,从左到右示例程序:#include <stdio.h> int main() { int a = 5, b = 10, c = 15; // 常见优先级问题 int result1 = a + b * c; // 5 + 150 = 155 int result2 = (a + b) * c; // 15 * 15 = 225 int result3 = a * b / c; // 50 / 15 = 3 (整数除法) int result4 = a << b + c; // 5 << 25 (先计算加法) printf("result1: %d\n", result1); printf("result2: %d\n", result2); printf("result3: %d\n", result3); // 结合性示例 int x = 10, y = 20, z = 30; x = y = z; // 从右向左结合: y = z; x = y; printf("x: %d, y: %d, z: %d\n", x, y, z); // 复杂表达式 int m = 1, n = 2, p = 3; int complex = m++ + --n * (p -= 1); /* 分解步骤: 1. --n → n=1 2. p -=1 → p=2 3. --n * p → 1 * 2=2 4. m++ + 2 → 1+2=3 (然后m=2) 5. complex=3 */ printf("complex: %d\n", complex); // 3 printf("m: %d, n: %d, p: %d\n", m, n, p); // 2,1,2 return 0; } 六、最佳实践与常见错误1. 数据类型选择建议整数运算优先使用int,它是CPU处理效率最高的类型需要大范围整数时使用long long浮点数优先使用double,它比float精度更高明确无符号数时使用unsigned2. 常量定义建议宏常量全大写,用下划线分隔优先使用const而非#define,它有类型检查魔法数字应该定义为常量3. 运算符使用建议复杂表达式使用括号明确优先级避免在同一个表达式中对同一变量多次修改位操作时使用无符号类型浮点数比较要使用误差范围4. 常见错误示例// 错误1: 整数除法 double avg = (a + b) / 2; // 错误,应改为 (a + b) / 2.0 // 错误2: 自增混淆 int i = 0; int arr[5] = {1,2,3,4,5}; int val = arr[i++ + i++]; // 未定义行为 // 错误3: 溢出 short s = 32767; s += 1; // 溢出,结果不确定 // 错误4: 浮点数相等比较 float f1 = 0.1f * 3; float f2 = 0.3f; if(f1 == f2) { /* 可能不成立 */ }
-
项目开发背景随着现代生活节奏加快,越来越多宠物主人面临因工作繁忙或短期外出导致的喂养不便问题。传统手动喂食方式难以保障宠物饮食规律性,而市面基础定时喂食器又缺乏远程交互和智能调节能力,极易出现余粮不足未被及时发现、喂食量不符合宠物实际需求等情况,直接影响宠物健康。物联网技术与智能家居的快速发展为宠物用品智能化提供了技术基础。通过集成云端数据同步、语音控制及自适应算法,可构建更符合现代养宠需求的解决方案。该系统不仅能实现精准定时投喂和远程应急补粮,更能通过分析历史进食数据动态优化喂食策略,解决人工喂养的主观性和不稳定性问题。本设计响应宠物健康管理的精细化需求,结合STM32的实时控制能力、华为云的可靠数据同步以及离在线混合交互模式,旨在打造一款具备智能决策、异常预警及多端协同功能的喂食系统。其开发不仅满足养宠家庭的核心痛点,也为宠物智能硬件领域提供了可扩展的技术框架。设计实现的功能(1)基于STM32F103C8T6主控实现系统核心控制与逻辑处理(2)通过海凌科V20离线语音模块接收本地语音指令触发投喂(3)使用28BYJ-48步进电机+ULN2003驱动板控制出粮机构执行投喂动作(4)利用HX711+压力传感器实时监测储粮桶重量并计算余粮(5)0.96寸OLED显示屏通过SPI接口本地显示时间/余粮/喂食记录(6)ESP8266模块连接华为云,基于MQTT协议实现远程指令收发与数据同步(7)高电平触发蜂鸣器在余粮低于阈值时发出声光报警(8)RTC实时时钟模块支持多组定时喂食计划自动执行(9)Qt for Android开发APP,支持远程手动投喂/记录查询/语音指令下发(10)Qt5+C++开发上位机软件,实现喂食策略配置与历史数据分析项目硬件模块组成(1)主控芯片:STM32F103C8T6(2)语音识别模块:海凌科 V20 离线语音模块(3)喂食机构:28BYJ-48步进电机 + ULN2003驱动板(4)粮量检测:HX711模块 + 压力传感器模块(电阻式)(5)显示模块:0.96寸OLED显示屏(SPI接口)(6)通信模块:ESP8266模块,连接华为云,MQTT协议(7)报警模块:高电平触发蜂鸣器提示余粮不足(8)时间控制:基于RTC实时时钟定时投喂设计意义该宠物喂食系统的设计意义主要体现在以下方面:解决现代人因工作繁忙导致的宠物喂养难题是该系统的核心价值。通过STM32主控精准协调定时喂食与远程手动控制功能,用户即使外出也能通过手机APP远程触发喂食,结合RTC时钟确保每日定时投喂,有效避免宠物因主人行程变动而挨饿,保障宠物饮食规律性。提升宠物喂养的科学性与健康管理水平是系统的智能化体现。压力传感器与HX711模块实时监测余粮量并在不足时触发蜂鸣器报警,避免断粮风险;系统通过分析历史投喂数据,自动学习宠物进食习惯并动态调整喂食量,既防止过度投喂又满足营养需求,实现个性化科学喂养。构建全方位状态监控与数据追溯体系增强了系统的可靠性。本地OLED屏实时显示余粮量、投喂记录等关键状态,便于用户现场查看;同时ESP8266模块通过MQTT协议将数据同步至华为云,配合Qt开发的APP及上位机软件,用户可随时随地查询历史喂食记录、调整参数,形成完整的云端数据链,为宠物健康管理提供依据。融合多技术模块实现人性化交互提升了用户体验。离线语音模块支持APP语音指令控制,解放用户双手;步进电机驱动机构确保投粮精准可控;本地显示与云端双备份机制强化了系统容错能力。整套硬件选型兼顾成本与可靠性,体现了嵌入式系统在智能家居领域的实用价值。通过技术手段传递人文关怀是系统的深层意义。将物联网、语音识别、自适应算法等技术应用于宠物喂养场景,既缓解了饲养者的后顾之忧,又保障了宠物的生存福利,体现了科技对生活品质的提升,强化了人与宠物之间的情感联结。设计思路设计思路系统以STM32F103C8T6为核心控制器,协调各模块实现智能化喂食管理。通过实时时钟(RTC)模块精确计时,支持预设多个定时喂食任务。用户可通过Qt开发的Android APP远程触发手动喂食指令,指令经华为云平台以MQTT协议传输至ESP8266通信模块,再由主控解析执行。粮量监测采用电阻式压力传感器与HX711模块组合,实时采集储粮箱重量数据。当余粮低于阈值时,STM32触发蜂鸣器报警,并通过OLED屏显示警告图标。同时,系统将粮量状态同步至云端,供APP和上位机监控。语音控制通过海凌科V20模块实现:用户说出预设指令(如"喂食")后,模块通过串口将指令码发送至STM32,主控立即驱动ULN2003步进电机驱动板,带动28BYJ-48步进电机旋转特定角度,控制出粮口开合实现定量投喂。投喂量可基于历史进食数据动态调整——系统记录每日消耗量,通过算法平滑优化后续单次投喂量。本地交互由SPI接口的0.96寸OLED屏承担,实时显示时间、余粮百分比、下次喂食倒计时及网络状态。所有操作日志(如喂食时间、触发方式、粮量变化)均通过ESP8266上传至华为云,Qt5开发的上位机软件可调取云端数据生成喂食报表,支持远程修改定时参数。主程序采用状态机架构:常态下轮询检测传感器数据、云端指令和语音输入;到达定时节点或收到外部指令时,中断当前任务执行喂食动作,确保系统响应实时性。硬件模块间通过UART、I2C和GPIO接口通信,低功耗设计延长电池供电场景的使用时长。框架图系统框架图+-----------------------------------------------------------------------------------------+ | STM32F103C8T6 (主控) | | +-------------------+ +-----------------+ +-----------------+ +-------------------+ | | | RTC实时时钟 | | GPIO控制 | | SPI接口 | | UART串口通信 | | | | (内部/外部晶振) | | (蜂鸣器控制) | | (OLED显示驱动) | | (多路通信枢纽) | | | +-------------------+ +--------+--------+ +--------+--------+ +-------+-------+----+ | | | | | | | | +---------|-----------------------|-------------------|-------------------|-------|-------+ | | | | | | (时间基准) | (报警触发) | (状态显示) | | (配置指令) +---------|-----------+ +--------|------+ +--------|------+ +-------|-------+ +-----|-------+ | 定时喂食逻辑 | | 蜂鸣器 | | OLED显示屏 | | 海凌科V20 | | ESP8266 | | (RTC触发喂食任务) | | (粮量不足报警)| | (0.96寸 SPI) | | 离线语音模块 | | WiFi模块| +---------+-----------+ +---------------+ +---------------+ +---------+---------+ +----+----+ | | | | (电机控制指令) | (语音指令解析) | (MQTT协议) +---------|-------------------+ +--------+ | | 喂食执行子系统 | | | | +----------------+ | +---------|--------|--------+ | | ULN2003 | <------+ | 华为云IoT平台 | | | 驱动板 | | (数据存储/指令转发) | | +------+---------+ +------------+-----------+ | | | | +------|---------+ | | | 28BYJ-48 | | | | 步进电机 | | | +------+---------+ | | | | | +------|---------+ | | | 储粮仓/投食机构| | | +----------------+ | +------------------------------------------------------------------------------------+ | | +---------|---------+ +-----------|-----------+ | 粮量检测子系统 | | 远程控制终端 | | +----------------+| | +-------------------+ | | | 压力传感器 || <----------------粮量反馈--------------------------| | Android APP | | | | (底部称重) || | | (Qt for Android) | | | +--------+-------+| | +-------------------+ | | | | | +-------------------+ | | +--------|--------+| | | 上位机软件 | | | | HX711 || | | (Qt5 + C++) | | | | 称重模块 || | +-------------------+ | | +-----------------+| +-----------------------+ +-------------------+ 关键交互说明:核心控制:STM32通过RTC定时触发喂食任务,或通过UART接收语音/WiFi的远程指令。执行层:ULN2003驱动步进电机转动,控制投食机构出粮。HX711+压力传感器实时监测余粮量,触发蜂鸣器报警。交互层:OLED显示时间、粮量、系统状态(SPI通信)。海凌科V20解析语音指令(如“放粮”),通过UART发送指令到STM32。云端链路:ESP8266通过MQTT协议连接华为云,同步喂食记录、粮量数据。Android APP/上位机软件通过云端下发喂食指令或配置定时任务。自适应逻辑:STM32分析历史投食数据(存储于华为云),动态调整单次投食量。系统总体设计系统总体设计基于STM32F103C8T6主控芯片构建,核心功能包括定时喂食、远程控制、余粮监测与智能调节。主控通过I/O接口协调各模块:海凌科V20离线语音模块接收语音指令,解析后触发喂食动作;28BYJ-48步进电机配合ULN2003驱动板执行出粮操作,通过控制电机旋转圈数精准调节喂食量。粮量检测由HX711模块连接电阻式压力传感器实现,实时监测储粮仓重量变化,当余粮低于阈值时触发高电平蜂鸣器报警。RTC实时时钟模块提供精确计时,支撑每日多时段定时喂食任务。系统具备学习能力,通过分析历史喂食数据与余粮变化趋势,动态优化单次投喂量。本地交互采用0.96寸OLED显示屏(SPI接口),实时展示余粮状态、喂食计划及系统报警信息。网络通信由ESP8266模块负责,通过MQTT协议连接华为云,实现喂食记录同步与远程指令收发。Qt for Android开发的APP支持手动喂食指令下发及数据查看,同时上位机通过Qt5+C++实现远程配置与统计分析。所有硬件模块通过主控统一调度,确保定时任务、语音控制、云端交互等功能协同运作。系统功能总结功能描述实现方式(硬件/软件)定时喂食功能STM32主控通过RTC实时时钟定时触发步进电机转动,驱动喂食机构投放饲料手动远程喂食功能ESP8266模块接收APP指令(MQTT协议),触发STM32控制步进电机投喂余粮检测与报警HX711+压力传感器实时监测重量,余粮不足时STM32触发蜂鸣器报警APP语音控制投喂Android APP语音指令→华为云→ESP8266→STM32执行;海凌科V20支持本地离线语音指令直控智能调整喂食量STM32记录进食数据,通过算法分析习惯动态调整步进电机转动时长(投喂量)本地状态显示0.96寸OLED实时显示余粮/时间/下次投喂计划(SPI通信)云端数据同步ESP8266上传喂食记录/余粮量至华为云(MQTT),Qt Android APP远程查看/配置系统配置管理Qt5上位机软件远程修改定时计划、喂食量参数,数据双向同步设计的各个功能模块描述主控芯片采用STM32F103C8T6,作为系统核心控制器,负责协调各模块工作,处理传感器数据,执行喂食逻辑,并与云端通信。语音识别模块使用海凌科V20离线语音单元,通过串口接收用户语音指令,解析为投喂命令发送至主控芯片,实现APP语音控制功能。喂食机构由28BYJ-48步进电机和ULN2003驱动板组成,主控芯片通过PWM信号控制步进电机旋转角度,精确驱动投食机构完成定量喂食,支持定时触发与远程手动触发双模式。粮量检测通过HX711模块连接电阻式压力传感器,实时采集储粮仓重量数据,主控芯片计算剩余粮量,低于阈值时触发报警。显示模块采用0.96寸OLED屏(SPI接口),本地展示系统状态,包括实时时间、剩余粮量、喂食计划及网络连接状态。通信模块基于ESP8266,通过AT指令与主控串口交互,使用MQTT协议连接华为云,实现喂食记录同步和远程指令接收。报警模块由高电平触发蜂鸣器构成,当粮量检测异常时主控输出高电平信号驱动蜂鸣器报警。时间控制基于STM32内部RTC实时时钟,独立供电维持计时,主控按预设时段自动执行喂食任务。APP软件使用Qt for Android开发,提供喂食记录查询、远程手动投喂及语音指令下发功能。上位机软件采用Qt5+C++实现,通过MQTT协议连接云端,提供喂食参数配置和历史数据分析界面。上位机代码设计上位机软件设计(Qt5 + C++)#include <QtWidgets> #include <QtMqtt/QtMqtt> #include <QDateTime> #include <QJsonObject> #include <QJsonDocument> class PetFeederApp : public QMainWindow { Q_OBJECT public: PetFeederApp(QWidget *parent = nullptr) : QMainWindow(parent) { // 初始化UI setupUI(); // MQTT客户端初始化 mqttClient = new QMqttClient(this); mqttClient->setHostname("your_huaweicloud_address.com"); mqttClient->setPort(1883); mqttClient->setClientId("PetFeeder_PC_" + QString::number(qrand())); mqttClient->setUsername("device_id"); mqttClient->setPassword("device_secret"); connect(mqttClient, &QMqttClient::connected, this, &PetFeederApp::onMqttConnected); connect(mqttClient, &QMqttClient::messageReceived, this, &PetFeederApp::onMessageReceived); // 连接MQTT mqttClient->connectToHost(); } private slots: void onMqttConnected() { statusLabel->setText("已连接华为云"); mqttClient->subscribe(QMqttTopicFilter("petfeeder/status")); mqttClient->subscribe(QMqttTopicFilter("petfeeder/foodlog")); } void onMessageReceived(const QByteArray &message, const QMqttTopicName &topic) { QJsonDocument doc = QJsonDocument::fromJson(message); QJsonObject json = doc.object(); if (topic.name() == "petfeeder/status") { updateStatusDisplay(json); } else if (topic.name() == "petfeeder/foodlog") { addFeedingRecord(json); } } void onManualFeed() { QMqttTopicName topic("petfeeder/control"); QJsonObject cmd = {{"cmd", "manual_feed"}, {"amount", feedAmountSpin->value()}}; mqttClient->publish(topic, QJsonDocument(cmd).toJson()); } void onSetSchedule() { QJsonArray schedules; for (int i = 0; i < scheduleTable->rowCount(); ++i) { QTime time = QTime::fromString(scheduleTable->item(i, 0)->text(), "HH:mm"); int amount = scheduleTable->item(i, 1)->text().toInt(); schedules.append(QJsonObject{{"time", time.toString("HH:mm")}, {"amount", amount}}); } QMqttTopicName topic("petfeeder/schedule"); mqttClient->publish(topic, QJsonDocument(schedules).toJson()); } private: void setupUI() { // 主窗口设置 setWindowTitle("智能宠物喂食系统"); setFixedSize(800, 600); // 状态显示区域 QGroupBox *statusGroup = new QGroupBox("系统状态"); statusLabel = new QLabel("正在连接云端..."); foodLevelBar = new QProgressBar; foodLevelBar->setRange(0, 100); lastFeedLabel = new QLabel("上次喂食: --:--"); QVBoxLayout *statusLayout = new QVBoxLayout; statusLayout->addWidget(statusLabel); statusLayout->addWidget(new QLabel("余粮水平:")); statusLayout->addWidget(foodLevelBar); statusLayout->addWidget(lastFeedLabel); statusGroup->setLayout(statusLayout); // 手动喂食控制 QGroupBox *controlGroup = new QGroupBox("手动控制"); feedAmountSpin = new QSpinBox; feedAmountSpin->setRange(5, 100); feedAmountSpin->setSuffix(" g"); QPushButton *feedButton = new QPushButton("立即喂食"); connect(feedButton, &QPushButton::clicked, this, &PetFeederApp::onManualFeed); QHBoxLayout *controlLayout = new QHBoxLayout; controlLayout->addWidget(new QLabel("喂食量:")); controlLayout->addWidget(feedAmountSpin); controlLayout->addWidget(feedButton); controlGroup->setLayout(controlLayout); // 定时计划表 QGroupBox *scheduleGroup = new QGroupBox("喂食计划"); scheduleTable = new QTableWidget(0, 2); QStringList headers{"时间 (HH:mm)", "喂食量 (g)"}; scheduleTable->setHorizontalHeaderLabels(headers); QPushButton *addScheduleBtn = new QPushButton("添加计划"); QPushButton *saveScheduleBtn = new QPushButton("保存计划"); connect(addScheduleBtn, &QPushButton::clicked, this, [this](){ scheduleTable->insertRow(scheduleTable->rowCount()); }); connect(saveScheduleBtn, &QPushButton::clicked, this, &PetFeederApp::onSetSchedule); QVBoxLayout *scheduleLayout = new QVBoxLayout; scheduleLayout->addWidget(scheduleTable); QHBoxLayout *btnLayout = new QHBoxLayout; btnLayout->addWidget(addScheduleBtn); btnLayout->addWidget(saveScheduleBtn); scheduleLayout->addLayout(btnLayout); scheduleGroup->setLayout(scheduleLayout); // 喂食记录 QGroupBox *historyGroup = new QGroupBox("喂食记录"); recordList = new QListWidget; QVBoxLayout *historyLayout = new QVBoxLayout; historyLayout->addWidget(recordList); historyGroup->setLayout(historyLayout); // 主布局 QGridLayout *mainLayout = new QGridLayout; mainLayout->addWidget(statusGroup, 0, 0); mainLayout->addWidget(controlGroup, 1, 0); mainLayout->addWidget(scheduleGroup, 0, 1, 2, 1); mainLayout->addWidget(historyGroup, 2, 0, 1, 2); QWidget *centralWidget = new QWidget; centralWidget->setLayout(mainLayout); setCentralWidget(centralWidget); } void updateStatusDisplay(const QJsonObject &status) { foodLevelBar->setValue(status["food_level"].toInt()); lastFeedLabel->setText("上次喂食: " + status["last_feed_time"].toString()); if (status["food_low"].toBool()) { statusLabel->setText("<font color='red'>余粮不足!</font>"); } else { statusLabel->setText("系统运行正常"); } } void addFeedingRecord(const QJsonObject &record) { QString entry = QString("%1 | %2 g | %3") .arg(record["time"].toString()) .arg(record["amount"].toInt()) .arg(record["type"].toString()); recordList->insertItem(0, entry); } // UI组件声明 QLabel *statusLabel; QProgressBar *foodLevelBar; QLabel *lastFeedLabel; QSpinBox *feedAmountSpin; QTableWidget *scheduleTable; QListWidget *recordList; // MQTT客户端 QMqttClient *mqttClient; }; int main(int argc, char *argv[]) { QApplication app(argc, argv); PetFeederApp window; window.show(); return app.exec(); } #include "main.moc" 代码功能说明云端通信模块使用MQTT协议连接华为云IoT平台订阅主题:petfeeder/status:接收设备状态(余粮量、最后喂食时间)petfeeder/foodlog:接收喂食记录发布主题:petfeeder/control:发送手动喂食指令petfeeder/schedule:发送定时喂食计划核心功能界面系统状态面板:实时显示余粮进度条、连接状态和最后喂食时间手动控制区:设置喂食量并立即触发喂食喂食计划表:支持多时段定时计划配置喂食记录:按时间倒序显示历史喂食记录数据处理使用JSON格式传输数据状态更新:{"food_level": 45, "last_feed_time": "14:30", "food_low": false}喂食记录:{"time": "2023-06-15 14:30", "amount": 30, "type": "scheduled"}操作流程UserGUIHuaweiCloudDevice设置定时计划发布喂食计划(JSON)下发新配置点击"立即喂食"发布手动喂食指令触发喂食动作定时上报状态推送状态更新显示余粮不足警告UserGUIHuaweiCloudDevice使用说明配置华为云连接修改代码中的MQTT连接参数:mqttClient->setHostname("your_huaweicloud_address.com"); mqttClient->setUsername("device_id"); mqttClient->setPassword("device_secret"); 定时计划设置在表格中添加/修改行格式:时间列 (HH:mm), 喂食量 (克)点击"保存计划"同步到设备手动喂食设置喂食量(5-100克)点击"立即喂食"按钮数据查看余粮状态:进度条显示百分比喂食记录:包含时间、喂食量和触发类型(手动/定时)模块代码设计设备端模块代码设计(STM32寄存器开发)#include "stm32f10x.h" // 硬件引脚定义 #define BUZZER_PIN GPIO_Pin_13 // PC13 #define MOTOR_PIN1 GPIO_Pin_8 // PC8 #define MOTOR_PIN2 GPIO_Pin_9 // PC9 #define MOTOR_PIN3 GPIO_Pin_10 // PC10 #define MOTOR_PIN4 GPIO_Pin_11 // PC11 #define HX711_SCK GPIO_Pin_0 // PA0 #define HX711_DOUT GPIO_Pin_1 // PA1 // 步进电机相位表 const uint8_t motorPhase[8] = { 0x09, 0x08, 0x0C, 0x04, 0x06, 0x02, 0x03, 0x01 }; // HX711读取函数 uint32_t HX711_Read(void) { uint32_t data = 0; GPIOA->BRR = HX711_SCK; // SCK低电平 while(GPIOA->IDR & HX711_DOUT); // 等待DOUT为低 for(uint8_t i=0; i<24; i++) { GPIOA->BSRR = HX711_SCK; // SCK高电平 __nop(); __nop(); // 短暂延时 data <<= 1; GPIOA->BRR = HX711_SCK; // SCK低电平 if(GPIOA->IDR & HX711_DOUT) data++; } // 发送第25个脉冲选择增益128 GPIOA->BSRR = HX711_SCK; __nop(); __nop(); GPIOA->BRR = HX711_SCK; return data ^ 0x800000; // 补码转换 } // 步进电机驱动函数 void Motor_Rotate(int steps, uint8_t direction) { static uint8_t phase = 0; int step_count = (direction) ? steps : -steps; while(step_count != 0) { if(step_count > 0) { phase = (phase + 1) % 8; step_count--; } else { phase = (phase + 7) % 8; step_count++; } GPIOC->ODR &= 0xF0FF; // 清空电机控制位 GPIOC->ODR |= (motorPhase[phase] << 8); for(volatile int i=0; i<5000; i++); // 转速控制延时 } GPIOC->BRR = MOTOR_PIN1 | MOTOR_PIN2 | MOTOR_PIN3 | MOTOR_PIN4; // 断电 } // 蜂鸣器报警函数 void Buzzer_Alert(uint8_t state) { if(state) { GPIOC->BSRR = BUZZER_PIN; } else { GPIOC->BRR = BUZZER_PIN; } } // RTC初始化 void RTC_Init(void) { RCC->APB1ENR |= RCC_APB1ENR_PWREN | RCC_APB1ENR_BKPEN; PWR->CR |= PWR_CR_DBP; if((BKP->DR1 & 0xFFFF) != 0xA5A5) { RCC->BDCR |= RCC_BDCR_LSEON; while(!(RCC->BDCR & RCC_BDCR_LSERDY)); RCC->BDCR |= RCC_BDCR_RTCSEL_LSE; RCC->BDCR |= RCC_BDCR_RTCEN; RTC->CRL |= RTC_CRL_CNF; RTC->PRLL = 32767; // LSE 32.768kHz RTC->CRL &= ~RTC_CRL_CNF; while(!(RTC->CRL & RTC_CRL_RTOFF)); BKP->DR1 = 0xA5A5; } } // 系统初始化 void System_Init(void) { // 时钟配置 RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN; // 蜂鸣器输出 GPIOC->CRH &= 0xFF0FFFFF; GPIOC->CRH |= 0x00300000; // PC13推挽输出 // 步进电机输出 GPIOC->CRH &= 0xFFFF0000; GPIOC->CRH |= 0x00003333; // PC8-PC11推挽输出 // HX711输入输出 GPIOA->CRL &= 0xFFFFFF00; GPIOA->CRL |= 0x00000038; // PA0输出, PA1输入 // 初始化外设 RTC_Init(); } // 主函数 int main(void) { System_Init(); uint32_t last_feed = 0; const uint32_t feed_interval = 43200; // 12小时 (RTC秒计数) while(1) { // 1. 检测余粮 uint32_t weight = HX711_Read(); if(weight < 50000) { // 阈值判断 Buzzer_Alert(1); } else { Buzzer_Alert(0); } // 2. RTC定时喂食 uint32_t current_time = RTC->CNTH << 16 | RTC->CNTL; if((current_time - last_feed) >= feed_interval) { Motor_Rotate(512, 1); // 旋转512步 last_feed = current_time; } // 3. 其他功能处理(OLED显示、网络通信等) // ... (此处添加其他模块代码) } } 关键模块说明:HX711压力传感器使用PA0(SCK)和PA1(DOUT)24位AD转换,补码转换处理内置数据就绪检测步进电机控制四相八拍驱动方式使用PC8-PC11控制ULN2003相位表实现正反转控制断电保护逻辑RTC实时时钟LSE 32.768kHz时钟源12小时定时喂食周期后备寄存器初始化标志蜂鸣器报警高电平触发(PC13)余粮阈值检测逻辑系统初始化GPIO模式配置时钟使能外设初始化顺序控制项目核心代码以下是基于STM32F103C8T6的宠物喂食系统核心代码(寄存器开发版本),包含完整main.c实现:#include "stm32f10x.h" #include "hx711.h" #include "oled.h" #include "motor.h" #include "rtc.h" #include "usart.h" #include "esp8266.h" #include "voice.h" // 宏定义 #define FEED_INTERVAL 8 // 默认喂食步数 #define FOOD_THRESHOLD 100 // 余粮阈值(g) #define BUZZER_PIN GPIO_Pin_0 #define BUZZER_PORT GPIOA // 全局变量 uint8_t feedFlag = 0; // 喂食触发标志 uint8_t foodLowAlarm = 0; // 余粮不足标志 uint32_t feedAmount = FEED_INTERVAL; // 动态喂食量 RTC_TimeTypeDef currentTime; // 函数声明 void SystemClock_Config(void); void GPIO_Configuration(void); void NVIC_Configuration(void); void Buzzer_Alarm(uint8_t state); void Feed_Pet(void); void Update_Feed_Amount(void); void Process_MQTT_Command(uint8_t* cmd); int main(void) { // 初始化系统 SystemClock_Config(); GPIO_Configuration(); NVIC_Configuration(); // 外设初始化 USART1_Init(115200); // 语音模块串口 USART2_Init(115200); // ESP8266串口 OLED_Init(); HX711_Init(); RTC_Init(); Motor_Init(); // 显示启动界面 OLED_ShowString(0, 0, "PetFeeder V1.0"); OLED_ShowString(0, 2, "Init..."); Delay_ms(1000); // 连接华为云 ESP8266_ConnectCloud(); OLED_ShowString(0, 2, "Cloud Connected"); while(1) { // 1. 获取当前时间 RTC_GetTime(¤tTime); // 2. 检测余粮状态 uint32_t foodWeight = HX711_GetWeight(); if(foodWeight < FOOD_THRESHOLD) { foodLowAlarm = 1; Buzzer_Alarm(1); MQTT_Publish("petfeeder/alarm", "FOOD_LOW"); } else { foodLowAlarm = 0; Buzzer_Alarm(0); } // 3. 定时喂食检测 (例: 08:00和18:00) if((currentTime.hours == 8 && currentTime.minutes == 0) || (currentTime.hours == 18 && currentTime.minutes == 0)) { feedFlag = 1; } // 4. 处理喂食请求 if(feedFlag) { Feed_Pet(); feedFlag = 0; // 上传喂食记录 char feedMsg[50]; sprintf(feedMsg, "{\"time\":\"%02d:%02d\",\"amount\":%d}", currentTime.hours, currentTime.minutes, feedAmount); MQTT_Publish("petfeeder/feed", feedMsg); // 更新喂食量算法 Update_Feed_Amount(); } // 5. 处理语音指令 uint8_t voiceCmd[20]; if(VOICE_GetCommand(voiceCmd)) { if(strcmp((char*)voiceCmd, "FEED") == 0) feedFlag = 1; } // 6. 处理MQTT命令 uint8_t mqttMsg[50]; if(ESP8266_ReceiveData(mqttMsg)) { Process_MQTT_Command(mqttMsg); } // 7. 刷新显示 OLED_Clear(); char dispBuf[20]; sprintf(dispBuf, "Time: %02d:%02d", currentTime.hours, currentTime.minutes); OLED_ShowString(0, 0, dispBuf); sprintf(dispBuf, "Food: %d g", foodWeight); OLED_ShowString(0, 2, dispBuf); OLED_ShowString(0, 4, foodLowAlarm ? "LOW FOOD!" : "Normal"); Delay_ms(500); } } // 蜂鸣器控制 void Buzzer_Alarm(uint8_t state) { if(state) { GPIO_SetBits(BUZZER_PORT, BUZZER_PIN); } else { GPIO_ResetBits(BUZZER_PORT, BUZZER_PIN); } } // 执行喂食动作 void Feed_Pet(void) { Motor_Rotate(feedAmount); // 顺时针旋转指定步数 Delay_ms(500); Motor_Rotate(10); // 反向回旋防止卡粮 } // 动态调整喂食量 void Update_Feed_Amount(void) { // 简化示例:根据时间调整 if(currentTime.hours == 8) feedAmount = FEED_INTERVAL; else feedAmount = FEED_INTERVAL + 2; // 晚餐加量 // 实际应加入机器学习算法 } // 处理MQTT命令 void Process_MQTT_Command(uint8_t* cmd) { if(strstr((char*)cmd, "CMD=FEED")) { feedFlag = 1; } else if(strstr((char*)cmd, "CMD=SET_TIME")) { // 解析时间设置命令 uint8_t h, m; sscanf((char*)cmd, "CMD=SET_TIME%02d%02d", &h, &m); RTC_SetTime(h, m, 0); } } // 系统时钟配置 (72MHz HSE) void SystemClock_Config(void) { RCC->CR |= RCC_CR_HSEON; while(!(RCC->CR & RCC_CR_HSERDY)); FLASH->ACR |= FLASH_ACR_LATENCY_2; RCC->CFGR |= RCC_CFGR_PLLMULL9 | RCC_CFGR_PLLSRC; RCC->CR |= RCC_CR_PLLON; while(!(RCC->CR & RCC_CR_PLLRDY)); RCC->CFGR |= RCC_CFGR_SW_PLL; while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL); } // GPIO配置 void GPIO_Configuration(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN; // 蜂鸣器(PA0) GPIOA->CRL &= ~(0x0F << (0*4)); GPIOA->CRL |= 0x02 << (0*4); // 推挽输出 // OLED SPI引脚配置(省略) // 步进电机引脚配置(省略) } // 中断配置 void NVIC_Configuration(void) { // 串口1中断(语音模块) NVIC_EnableIRQ(USART1_IRQn); NVIC_SetPriority(USART1_IRQn, 0); // 串口2中断(ESP8266) NVIC_EnableIRQ(USART2_IRQn); NVIC_SetPriority(USART2_IRQn, 1); } // 串口1中断服务函数 void USART1_IRQHandler(void) { if(USART1->SR & USART_SR_RXNE) { VOICE_ReceiveHandler(USART1->DR); } } // 串口2中断服务函数 void USART2_IRQHandler(void) { if(USART2->SR & USART_SR_RXNE) { ESP8266_ReceiveHandler(USART2->DR); } } 关键功能说明:定时喂食:通过RTC实现08:00和18:00自动触发余粮检测:HX711读取压力传感器数据,低于阈值触发蜂鸣器报警语音控制:海凌科V20通过串口发送"FEED"指令触发喂食动态喂食量:Update_Feed_Amount函数实现基础量调整(需扩展ML算法)华为云通信:上传喂食记录:JSON格式数据接收远程命令:支持"FEED"和"SET_TIME"指令状态显示:OLED实时显示时间、余粮量和报警状态总结本设计实现了一个基于STM32F103C8T6主控芯片的智能宠物喂食系统,通过集成多模块硬件与软件,全面满足了定时喂食、远程控制、粮量监测等核心需求。系统利用28BYJ-48步进电机和ULN2003驱动板实现精确投喂动作,同时结合HX711模块和压力传感器实时检测剩余粮量,在粮量不足时触发高电平蜂鸣器报警,确保宠物不会断粮。时间控制基于RTC实时时钟,支持预设定时喂食计划,并通过ESP8266模块连接华为云平台,采用MQTT协议实现数据云端同步,保障了系统的可靠性和远程可访问性。系统还融入了智能化功能,如通过海凌科 V20离线语音模块支持APP语音控制投喂指令下发,并能基于宠物进食习惯自动调整喂食量,提升了用户体验的便捷性和个性化。本地状态显示通过0.96寸OLED显示屏(SPI接口)实现,为用户提供实时反馈,而Qt for Android开发的APP和Qt5 + C++实现的上位机软件则提供了全面的远程配置与数据查看能力,包括喂食记录和系统设置。总体而言,该设计通过硬件模块的协同工作与软件平台的优化,实现了高效、智能的宠物喂食管理。它不仅解决了日常喂食的自动化问题,还通过云端同步和语音交互增强了远程监控的灵活性,为宠物主人提供了一套安全、可靠且用户友好的解决方案。
-
我就只最后一个改为0x04
-
本地都是ok的,请问这是为什么呢
-
一、前言1.1 功能简介随着物联网技术的快速发展,无线通信模块成为了连接物理世界与数字世界的桥梁。在众多无线通信解决方案中,SIM系列模块以其紧凑的设计、良好的网络兼容性以及丰富的功能特性而受到广泛欢迎。SIM系列模块不仅支持基础的GSM服务如语音通话和短信收发,还能够通过GPRS进行数据传输,非常适合应用于各种智能设备和远程监控系统中。对于开发者而言,如何有效地利用SIM系列模块实现特定功能成为了一个重要技术点。其中,使用串行通信接口(即串口)发送AT指令来控制模块是实现这一目标的关键手段之一。AT指令集提供了一套标准化的方法,让工程师可以通过简单的文本命令与模块交互,从而执行复杂任务。本文将以SIM800C为例,详细介绍如何通过串口AT指令对SIM800C进行调试,实现打电话和接听电话这两个常见应用场景。通过对相关AT指令的学习及实践操作,将能够掌握如何设置并测试SIM800C的基本通话功能。1.2 拨打电话功能的应用场景这里以SIM800C模块为例进行介绍。在物联网(IoT)应用中,SIM800C模块的拨打电话功能可以实现多种实用的功能,尤其是在需要远程控制、监控或紧急响应的情况下。以下是几个具体的应用场景:(1)远程控制:通过拨打预设的电话号码,用户可以从远处激活或控制设备。例如,在智能家居系统中,可以通过电话呼叫来打开或关闭家中的灯光、调整空调温度或是启动安防系统。(2)安全报警:当监测到异常情况时,如入侵警报、火灾预警等,SIM800C可以自动拨打预先设置好的紧急联系人或服务中心电话,及时通报情况并请求帮助。这种方式能够迅速传达警情,提高响应速度。(3)状态查询:某些情况下,用户可能希望直接与远端设备进行互动以获取实时信息。比如农业环境监测站,农民可以通过拨打特定号码来接收土壤湿度、气温等数据报告。(4)语音提示:结合语音合成技术,SIM800C还可以用来发送语音消息给用户。这在一些公共服务领域特别有用,例如公共交通车辆到达提醒、天气预报更新等。(5)医疗辅助:对于老年人护理或者慢性病患者管理来说,SIM800C可以被集成进穿戴式健康监测器内。一旦检测到生命体征异常,设备会自动拨打医生或家属的电话,确保病人能够得到及时救助。(6)工业自动化:在复杂的工业环境中,维护人员可能需要定期检查机器运行状况。利用SIM800C建立一个简易的语音通信通道后,工作人员就可以随时随地拨入系统听取设备状态汇报了。(7)物流跟踪:货物运输途中,通过安装带有SIM800C模块的追踪装置,物流公司能够更加方便地与司机保持联系,甚至可以在必要时主动发起通话询问具体情况。借助SIM800C的拨打电话功能,开发者能够在多种IoT应用场景下构建出高效且可靠的通讯解决方案。1.3 SIM900A与SIM800C模块介绍SIM900A和SIM800C都是由SIMCom公司设计生产的GSM/GPRS通信模块,它们广泛应用于各种物联网(IoT)设备中,提供语音通话、短信服务以及数据传输功能。这些模块基于成熟可靠的GSM技术,适用于全球范围内的多种应用场合。SIM900A,它是一款四频GSM/GPRS模块,支持850MHz、900MHz、1800MHz和1900MHz频率,这意味着它可以适应世界上几乎所有地区的GSM网络。SIM900A拥有紧凑的设计,尺寸为24 x 24 x 3 mm,非常适合集成到空间有限的项目中。该模块通过标准AT指令集进行控制,允许开发者轻松实现电话拨打、短信收发及GPRS连接等功能。此外,SIM900A还具备低功耗模式,在不活跃时可以节省电池寿命,这对于依赖电池供电的远程监控系统尤其重要。SIM800C,这是SIMCom推出的另一款高性能GSM/GPRS模块,同样支持四频GSM网络,并且在某些方面对SIM900A进行了改进或扩展。例如,SIM800C不仅保持了小巧的体积,还在性能上有所提升,特别是在处理速度和稳定性方面。与SIM900A一样,SIM800C也遵循标准AT命令集,使得迁移现有应用程序变得相对容易。值得注意的是,SIM800C增加了对更多高级功能的支持,如增强型音频接口,这使其成为需要高质量语音通信的应用的理想选择。此外,SIM800C还优化了电源管理机制,进一步降低了待机状态下的能耗。除了上述特性外,无论是SIM900A还是SIM800C,都提供了丰富的外部接口选项,包括UART串口、GPIO引脚、ADC输入等,方便用户根据实际需求定制开发。同时,两者均兼容广泛的SIM卡类型,从普通SIM卡到更先进的USIM卡均可使用。这些特点使得SIM900A和SIM800C能够满足不同层次的应用需求,从简单的信息传递到复杂的物联网解决方案。SIM900A和SIM800C作为SIMCom旗下广受欢迎的GSM/GPRS模块系列成员,各自以其独特的优势服务于不同的市场细分领域。无论是在智能家居、工业自动化、移动支付终端还是车辆追踪系统等领域,这些模块都能够提供稳定可靠的无线通信能力,助力于构建更加智能互联的世界。随着技术不断进步,未来我们还可以期待看到更多具有创新特性的新型号推出,以更好地应对日益增长的市场需求。1.4 原理图三、模块调试3.1 工具软件下载项目设计里用到的工具软件,STM32的源码工程,都可以在这里下载。 https://pan.quark.cn/s/145a9b3f7f533.2 准备好模块(1)ATK-SIM800C GSM/GPRS 模块一个(2)直流稳压电源1个(推荐12V 1A电源)(3)中国移动/联通GSM SIM卡一张(未停机,开通GPRS业务)(4)耳机一副(带麦克风功能,用于通话测试)3.3 串口调试助手的设置模块接好线,连接上电脑之后,发送AT命令测试模块是否正常(注意 勾选新行)。说明: 第一次发送AT 过去时,模块会匹配波特率,模块会自适应波特率; 第二次发送AT就会返回OK。AT OK3.4 初始化配置下面是上电初始的设置指令。【1】测试模块是否正常 发送: AT 模块会返回: OK【2】 设置TTS来电有铃声 发送: AT+CTTSRING=0 模块会返回: OK【3】设置TTS声音大小、语调配置 发送: AT+CTTSPARAM=20,0,50,70,0 模块会返回: OK【4】设置来电显示 发送: AT+CLIP=1 模块会返回: OK【5】设置被叫号码显示 发送: AT+COLP=1 模块会返回: OK3.5 拨打电话的测试流程下面是介绍关于电话相关的指令。【1】拨打电话 ATD指令用于拨打任意电话号码,格式为:ATD【号码】; 末尾的’;’一定要加上,否则不能 成功拨号,如发送:ATD10086;,即可实现拨打10086。发送: ATD10086; 模块会返回: +COLP: 表示拨号成功。如果返回NO CARRIER、NO ANSWER、ERROR都表示错误。【2】应答电话 ATA指令,用于应答电话,当收到来电的时候,给模块发送:ATA,即可接听来电。 发送: ATA 模块会返回: OK如果模块收到+CLIP:就表示收到来电,可以发送ATA接听电话。【3】挂断电话 ATH指令,用于挂断电话,要想结束正在进行的通话,只需给模块发送:ATH,即可挂断。 发送: ATH 模块会返回: OK四、代码实现下面是通过STM32完成对SIM800C模块控制完成短信发送、电话拨打等操作。4.1 底层的命令发送接口//SIM800C发送命令后,检测接收到的应答 //str:期待的应答结果 //返回值:0,没有得到期待的应答结果 //其他,期待应答结果的位置(str的位置) u8* sim800c_check_cmd(u8 *str) { char *strx=0; if(USART2_RX_STA&0X8000) //接收到一次数据了 { USART2_RX_BUF[USART2_RX_STA&0X7FFF]=0;//添加结束符 strx=strstr((const char*)USART2_RX_BUF,(const char*)str); } return (u8*)strx; } //向SIM800C发送命令 //cmd:发送的命令字符串(不需要添加回车了),当cmd<0XFF的时候,发送数字(比如发送0X1A),大于的时候发送字符串. //ack:期待的应答结果,如果为空,则表示不需要等待应答 //waittime:等待时间(单位:10ms) //返回值:0,发送成功(得到了期待的应答结果) // 1,发送失败 u8 sim800c_send_cmd(u8 *cmd,u8 *ack,u16 waittime) { u8 res=0; USART2_RX_STA=0; if((u32)cmd<=0XFF) { while(DMA1_Channel7->CNDTR!=0); //等待通道7传输完成 USART2->DR=(u32)cmd; }else u2_printf("%s\r\n",cmd);//发送命令 if(ack&&waittime) //需要等待应答 { while(--waittime) //等待倒计时 { delay_ms(10); if(USART2_RX_STA&0X8000)//接收到期待的应答结果 { if(sim800c_check_cmd(ack))break;//得到有效数据 USART2_RX_STA=0; } } if(waittime==0)res=1; } return res; } 4.2 底层数据接收接口//接收SIM800C数据 //request:期待接收命令字符串 //waittimg:等待时间(单位:10ms) //返回值:0,发送成功(得到了期待的应答结果) // 1,发送失败 u8 sim800c_wait_request(u8 *request ,u16 waittime) { u8 res = 1; u8 key; if(request && waittime) { while(--waittime) { key=KEY_Scan(0); if(key==WKUP_PRES) return 2; delay_ms(10); if(USART2_RX_STA &0x8000)//接收到期待的应答结果 { if(sim800c_check_cmd(request)) break;//得到有效数据 USART2_RX_STA=0; } } if(waittime==0)res=0; } return res; }4.3 检测模块是否存在while(sim800c_send_cmd("AT","OK",100))//检测是否应答AT指令 { Show_Str(40,55,200,16,"未检测到模块!!!",16,0); delay_ms(800); LCD_Fill(40,55,200,55+16,WHITE); Show_Str(40,55,200,16,"尝试连接模块...",16,0); delay_ms(400); } 4.4 网络时间同步//NTP网络同步时间 void ntp_update(void) { sim800c_send_cmd("AT+SAPBR=3,1,\"Contype\",\"GPRS\"","OK",200);//配置承载场景1 sim800c_send_cmd("AT+SAPBR=3,1,\"APN\",\"CMNET\"","OK",200); sim800c_send_cmd("AT+SAPBR=1,1",0,200); //激活一个GPRS上下文 delay_ms(5); sim800c_send_cmd("AT+CNTPCID=1","OK",200); //设置CNTP使用的CID sim800c_send_cmd("AT+CNTP=\"202.120.2.101\",32","OK",200); //设置NTP服务器和本地时区(32时区 时间最准确) sim800c_send_cmd("AT+CNTP","+CNTP: 1",600); //同步网络时间 }4.5 拨打电话与接听电话//SIM800C拨号测试 //用于拨打电话和接听电话 //返回值:0,正常 //其他,错误代码 u8 sim800c_call_test(void) { u8 key; u16 lenx; u8 callbuf[20]; u8 pohnenumlen=0; //号码长度,最大15个数 u8 *p,*p1,*p2; u8 oldmode=0; u8 cmode=0; //模式 //0:等待拨号 //1:拨号中 //2:通话中 //3:接收到来电 LCD_Clear(WHITE); if(sim800c_send_cmd("AT+CTTSRING=0","OK",200))return 1; //设置TTS来电设置 0:来电有铃声 1:没有 if(sim800c_send_cmd("AT+CTTSPARAM=20,0,50,70,0","OK",200))return 1; //设置TTS声音大小、语调配置 if(sim800c_send_cmd("AT+CLIP=1","OK",200))return 1; //设置来电显示 if(sim800c_send_cmd("AT+COLP=1","OK",200))return 2; //设置被叫号码显示 p1=mymalloc(20); //申请20直接用于存放号码 if(p1==NULL)return 2; POINT_COLOR=RED; Show_Str_Mid(0,30,"SIM800C 拨号测试",16,240); Show_Str(40,70,200,16,"请拨号:",16,0); kbd_fn_tbl[0]="拨号"; kbd_fn_tbl[1]="返回"; sim800c_load_keyboard(0,180,(u8**)kbd_tbl1); POINT_COLOR=BLUE; while(1) { delay_ms(10); if(USART2_RX_STA&0X8000) //接收到数据 { sim_at_response(0); if(cmode==1||cmode==2) { if(cmode==1)if(sim800c_check_cmd("+COLP:"))cmode=2; //拨号成功 if(sim800c_check_cmd("NO CARRIER"))cmode=0; //拨号失败 if(sim800c_check_cmd("NO ANSWER"))cmode=0; //拨号失败 if(sim800c_check_cmd("ERROR"))cmode=0; //拨号失败 } if(sim800c_check_cmd("+CLIP:"))//接收到来电 { cmode=3; p=sim800c_check_cmd("+CLIP:"); p+=8; p2=(u8*)strstr((const char *)p,"\""); p2[0]=0;//添加结束符 strcpy((char*)p1,(char*)p); } USART2_RX_STA=0; } key=sim800c_get_keynum(0,180); if(key) { if(key<13) { if(cmode==0&&pohnenumlen<15) { callbuf[pohnenumlen++]=kbd_tbl[key-1][0]; u2_printf("AT+CLDTMF=2,\"%c\"\r\n",kbd_tbl[key-1][0]); delay_ms(55);//延时 u2_printf("AT+CTTS=2,\"%c\"\r\n",kbd_tbl[key-1][0]); //TTS语音 }else if(cmode==2)//通话中 { u2_printf("AT+CLDTMF=2,\"%c\"\r\n",kbd_tbl[key-1][0]); delay_ms(100); u2_printf("AT+VTS=%c\r\n",kbd_tbl[key-1][0]); LCD_ShowChar(40+56,90,kbd_tbl[key-1][0],16,0); } }else { if(key==13)if(pohnenumlen&&cmode==0)pohnenumlen--;//删除 if(key==14)//执行拨号 { if(cmode==0)//拨号模式 { callbuf[pohnenumlen]=0; //最后加入结束符 printf("ATD:%s\r\n",callbuf); u2_printf("ATD%s;\r\n",callbuf);//拨号 sim_at_response(1); delay_ms(10); //等待10ms cmode=1; //拨号中模式 }else { u2_printf("AT\r\n");//必须加上这句 delay_ms(10); sim800c_send_cmd("ATH","OK",100);//挂机 sim800c_send_cmd("ATH","OK",100);//挂机 cmode=0; } } if(key==15) { if(cmode==3)//接收到来电 { sim800c_send_cmd("ATA","OK",200);//发送应答指令 Show_Str(40+56,70,200,16,callbuf,16,0); cmode=2; }else { sim800c_send_cmd("ATH",0,0);//不管有没有在通话,都结束通话 break;//退出循环 } } } if(cmode==0)//只有在等待拨号模式有效 { callbuf[pohnenumlen]=0; LCD_Fill(40+56,70,239,70+16,WHITE); Show_Str(40+56,70,200,16,callbuf,16,0); } } if(oldmode!=cmode)//模式变化了 { switch(cmode) { case 0: kbd_fn_tbl[0]="拨号"; kbd_fn_tbl[1]="返回"; POINT_COLOR=RED; Show_Str(40,70,200,16,"请拨号:",16,0); LCD_Fill(40+56,70,239,70+16,WHITE); if(pohnenumlen) { POINT_COLOR=BLUE; Show_Str(40+56,70,200,16,callbuf,16,0); } break; case 1: POINT_COLOR=RED; Show_Str(40,70,200,16,"拨号中:",16,0); pohnenumlen=0; case 2: POINT_COLOR=RED; if(cmode==2)Show_Str(40,70,200,16,"通话中:",16,0); kbd_fn_tbl[0]="挂断"; kbd_fn_tbl[1]="返回"; break; case 3: POINT_COLOR=RED; Show_Str(40,70,200,16,"有来电:",16,0); POINT_COLOR=BLUE; Show_Str(40+56,70,200,16,p1,16,0); kbd_fn_tbl[0]="挂断"; kbd_fn_tbl[1]="接听"; break; } if(cmode==2)Show_Str(40,90,200,16,"DTMF音:",16,0); //通话中,可以通过键盘输入DTMF音 else LCD_Fill(40,90,120,90+16,WHITE); sim800c_load_keyboard(0,180,(u8**)kbd_tbl1); //显示键盘 oldmode=cmode; } if((lenx%50)==0) { LED0=!LED0; u2_printf("AT\r\n");//必须加上这句,不然接收不到来电显示 } lenx++; } myfree(p1); return 0; }
-
一、前言随着深度学习技术和计算能力的进步,AI生成视频(AIGV)已经从一个研究概念演变成了一种实用工具,其应用场景也在不断拓展。从自动合成新闻报道到虚拟人物的互动视频,从电影特效生成到游戏场景的实时渲染,AI生成视频正逐步成为内容创作者不可或缺的利器。不仅被专业人士所采用,也逐渐走向大众市场。这些工具能够帮助用户快速创建出高质量的视频内容,无论是用于商业宣传、艺术创作还是个人娱乐,都能够满足不同场景的需求。随着AI生成视频技术的日益成熟目前市场上较为流行的AI视频生成平台有从快手可灵、字节即梦、生数Vidu、智谱清影、PixVerse、Luma以及Runway Gen-3等。下面是这些平台工具的访问地址:快手可灵:cid:link_2字节即梦:https://jimeng.jianying.com/ai-tool/home?activeTab=video&subTab=video生数Vidu: cid:link_3智谱清影:cid:link_4PixVerse:cid:link_6Luma:cid:link_1Runway Gen-3:cid:link_5其中智谱清影 目前是完全免费的(还在内侧阶段)。 Runway Gen-3 是完全收费。其他的工具可以免费体验。二、AI视频生成工具生成视频的提示词:宁静的湖畔,阳光洒在清澈的水面上,五彩斑斓的石头点缀着湖岸。镜头缓缓扫过各种形状和颜色的石头——红、蓝、黄、绿,每一块都闪耀着独特的光芒。一只好奇的小松鼠穿梭其间,偶尔停下来捡起一颗石头仔细打量。2.1 快手可灵快手可灵:cid:link_22.2 字节即梦字节即梦:https://jimeng.jianying.com/ai-tool/home?activeTab=video&subTab=video生成卖火柴的小女孩。《卖火柴的小女孩》(画面:雪夜中的小镇,街道空旷而寂静) 旁白:“在遥远的一个小镇上,有一个寒冷的除夕夜。”(画面:小女孩穿着破旧的衣服,手中拿着一篮子火柴,独自走在街上) 旁白:“一个小女孩,赤着脚,穿着单薄的衣裳,手里拿着一把火柴,试图卖掉它们。”(画面:小女孩颤抖着点燃了一根火柴,画面转为暖色调,出现幻象中的温暖壁炉) 旁白:“她冷得无法忍受,于是她点燃了一根火柴。在火光中,她看到了一个温暖的壁炉。”(画面:火柴再次点燃,出现幻象中的美味烤鹅) 旁白:“饥饿驱使她点燃了第二根火柴,这次她看见了一只美味的烤鹅。”(画面:火柴燃烧,出现幻象中的圣诞树) 旁白:“接着是第三根火柴,一棵装饰精美的圣诞树出现在她面前。”(画面:火柴照亮了周围,出现幻象中的慈爱祖母) 旁白:“当她点燃第四根火柴时,她看到了她亲爱的祖母,那位唯一疼爱她的人。” 柴 (画面:小女孩点燃了所有的火柴,祖母的幻影变得更加清晰) 旁白:“她害怕失去祖母,于是她点燃了所有的火柴。祖母变得比以往任何时候都要明亮和温暖。”2.3 智谱清影智谱清影:cid:link_42.4 生数Vidu生数Vidu: cid:link_32.5 PixVersePixVerse:cid:link_62.6 LumaLuma:cid:link_12.7 Runway Gen-3Runway Gen-3:cid:link_5
-
一、Eclipse Paho介绍Eclipse Paho 是一个开源项目,由 Eclipse Foundation 主持,提供可靠的开源实现来处理 MQTT(Message Queuing Telemetry Transport)协议以及其他与物联网 (IoT) 相关的协议。MQTT 是一种轻量级的发布/订阅消息传输协议,专为具有低带宽和不可靠网络连接的设备设计。Paho 提供了多种语言的客户端库,使得开发者可以在各种平台上开发基于 MQTT 协议的应用程序。1.1 主要特点跨平台支持:Paho 支持多种操作系统和硬件平台,包括Windows、Linux、macOS 以及嵌入式系统。多语言实现:Paho 客户端库提供了多种编程语言的选择,如 C、C++、Java 和 Python 等。可靠性:Paho 能够在各种网络条件下可靠地工作,包括高延迟、丢包和间歇性连接等。安全性:Paho 支持 TLS/SSL 加密通信,以保证数据的安全传输。灵活性:除了基本的 MQTT 协议实现之外,Paho 还允许扩展和定制以适应特定的需求。1.2 Eclipse Paho MQTT C客户端库特点Eclipse Paho MQTT支持多种语言,其中的C客户端库是一个用于实现MQTT协议客户端的开源C语言库。跨平台支持:该库设计为可移植的,支持多种操作系统和硬件平台,包括Linux、Windows、MacOS以及嵌入式系统。易于集成:库的设计使得它易于集成到现有的C或C++项目中,为开发者提供了简单而强大的API来构建MQTT客户端。灵活的连接选项:支持TLS/SSL加密的MQTT连接,提供安全的通信通道。同时,支持QoS(服务质量)级别0(最多一次)、1(至少一次)和2(仅一次)的消息传递,确保消息传递的可靠性。异步操作:大多数库操作都是异步的,允许应用程序在等待网络响应时继续执行其他任务,提高了应用程序的响应性和效率。客户端和服务器消息处理:库支持客户端到服务器的消息发布(PUBLISH)以及从服务器到客户端的消息订阅(SUBSCRIBE)和接收(RECEIVE)。持久会话和遗嘱消息:支持持久会话,即使客户端断开连接也能保持订阅和QoS状态。同时,可以设置遗嘱消息,在客户端异常断开时发送特定消息。回调函数:通过提供回调函数来处理连接、断开连接、消息接收等事件,使得事件处理逻辑更加灵活。二、Eclipse Paho源码下载官网地址:cid:link_3库的下载地址:cid:link_4在页面上可以看到源码下载和编译好的库文件下载。提供了Linux下、Windows下编译好的库文件,可以直接使用。 如果你现在嵌入式平台上、其他平台上使用,那需要自己下载源码进行编译,使用。三、编译Eclipse Paho库文件3.1 下载编译openSSL如果要支持ssl加密的支持,那就需要先编译安装 openSSL。下载地址:cid:link_0编译步骤:1、解压缩tar zxf openssl-OpenSSL_1_1_1g.tar.gz2、进入目录,并配置输出目录和编译器cd openssl-OpenSSL_1_1_1g/./config no-asm shared no-async --prefix=`pwd`/ssl_result 如果是交叉编译器,可以指定 --cross-compile-prefix=arm-linux-。3、执行下面命令,删除Makefile文件的 -m64(如果指定了交叉编译器)sed -i 's/-m64//' Makefile执行后,可以避免出现这个编译错误:arm-linux-: error: unrecognized command line option '-m64'4、编译、安装make && make install成功编译后,在openssl-OpenSSL_1_1_1g/目录会生成一个ssl_result目录,可以看到里面生成的库.3.2 paho.mqtt.c 编译当前下载的是 paho.mqtt.c-1.3.13.tar.gz下载地址:cid:link_2wget cid:link_21、解压缩,创建要安装目录paho.mqtt.c_resulttar zxf paho.mqtt.c-1.3.13.tar.gzmkdir paho.mqtt.c_result/bin -pmkdir paho.mqtt.c_result/include -pmkdir paho.mqtt.c_result/lib -pmkdir paho.mqtt.c_result/share/man/man1 -p2、进入目录,交叉编译cd paho.mqtt.c-1.3.13/make CC=gcc CFLAGS:="-I `pwd`/../ssl_result/include" LDFLAGS:="-L `pwd`/../ssl_result/lib"如果是交叉编译需要指定交叉编译器完整名字:cd paho.mqtt.c-1.3.13/make CC=arm-linux-gcc CFLAGS:="-I `pwd`/../ssl_result/include" LDFLAGS:="-L `pwd`/../ssl_result/lib"参数介绍:CFLAGS:=“-I `pwd`/…/ssl_result/include”:指定前面编译的 openssl 的头文件;LDFLAGS:=“-L `pwd`/…/ssl_result/lib”:指定前面编译的 openssl 的库文件路径;3、make install,安装编译结果make install prefix=`pwd`/../paho.mqtt.c_result prefix=`pwd`/…/paho.mqtt.c_result :指定安装目录路径;编译完成后,会生成目录。3.3 paho.mqtt.cpp 编译当前下载的是paho.mqtt.cpp-1.3.2.tar.gz下载地址:cid:link_1如果你下载的版本跟我的一样,可以使用下面的脚本进行编译。编译之前,先创建一个脚本文件,名字为paho.mqtt.cpp_install,将下面代码粘贴进去保存。#! /bin/shRESULT_DIR=$(pwd)/result_dirRESULT_SSL=${RESULT_DIR}/ssl_resultRESULT_MQTT_C=${RESULT_DIR}/paho.mqtt.c_resultRESULT_MQTT_CPP=${RESULT_DIR}/paho.mqtt.cpp_resultCROSSS_COMPILE_TOOL=arm-linux- #如果你是需要交叉编译编译步骤:1、解压缩tar zxf paho.mqtt.cpp-1.3.2.tar.gz 2、进入目录cd paho.mqtt.cpp-1.3.2/3、执行 cmakemkdir build_arm cd build_arm编译不需要 openssl 的库cmake .. -DCMAKE_CXX_COMPILER=${CROSSS_COMPILE_TOOL}g++ \-DCMAKE_INSTALL_PREFIX=${RESULT_MQTT_CPP} \-DPAHO_MQTT_C_LIBRARIES=${RESULT_MQTT_C}/lib/libpaho-mqtt3a.so \-DPAHO_MQTT_C_INCLUDE_DIRS=${RESULT_MQTT_C}/include \-DPAHO_WITH_SSL=OFF \-DCMAKE_CXX_FLAGS="-std=gnu++11 -mcpu=cortex-a53"说明:如果你在PC机编译目标是PC机本身使用,-mcpu=cortex-a53 就不用写。 如果交叉编译的目标是嵌入式芯片,就如实写构架。编译带有 openssl 的库cmake .. -DCMAKE_CXX_COMPILER=${CROSSS_COMPILE_TOOL}g++ \-DCMAKE_INSTALL_PREFIX=${RESULT_MQTT_CPP} \-DPAHO_MQTT_C_LIBRARIES=${RESULT_MQTT_C}/lib/libpaho-mqtt3a.so \-DPAHO_MQTT_C_INCLUDE_DIRS=${RESULT_MQTT_C}/include \-DOPENSSL_SSL_LIBRARY=${RESULT_SSL}/lib/libssl.so \-DOPENSSL_INCLUDE_DIR=${RESULT_SSL}/include \-DOPENSSL_CRYPTO_LIBRARY=${RESULT_SSL}/lib/libcrypto.so \-DCMAKE_CXX_FLAGS="-std=gnu++11 -mcpu=cortex-a53"说明:如果你在PC机编译目标是PC机本身使用,-mcpu=cortex-a53 就不用写。 如果交叉编译的目标是嵌入式芯片,就如实写构架。4、编译make && make install5、将下载的源码包 paho.mqtt.cpp-1.3.2.tar.gz 和 上面保存的脚本paho.mqtt.cpp_install 放到同一目录,并且将前面编译好的openssl库、paho.mqtt.c库放在脚本指定的结果目录,当前是放到 result_dir 目录的。6、执行./paho.mqtt.cpp_install.sh 编译,编译完成后,在result_dir目录下会生成一个名为paho.mqtt.cpp_result的目录。四、代码案例4.1 订阅—async_subscribe.cpp这是使用了 libpaho-mqttpp3.so 进行订阅消息的源码,源码路径在源码的这个路径:paho.mqtt.cpp-1.3.2/src/samples/async_subscribe.cpp,只更改了服务器地址。完整代码如下:#include <iostream>#include <cstdlib>#include <string>#include <cstring>#include <cctype>#include <thread>#include <chrono>#include "mqtt/async_client.h"const std::string SERVER_ADDRESS("117.78.5.125:1883");const std::string CLIENT_ID("paho_cpp_async_subcribe");const std::string TOPIC("hello");const int QOS = 1;const int N_RETRY_ATTEMPTS = 5;/// Callbacks for the success or failures of requested actions.// This could be used to initiate further action, but here we just log the// results to the console.class action_listener : public virtual mqtt::iaction_listener{ std::string name_; void on_failure(const mqtt::token& tok) override { std::cout << name_ << " failure"; if (tok.get_message_id() != 0) std::cout << " for token: [" << tok.get_message_id() << "]" << std::endl; std::cout << std::endl; } void on_success(const mqtt::token& tok) override { std::cout << name_ << " success"; if (tok.get_message_id() != 0) std::cout << " for token: [" << tok.get_message_id() << "]" << std::endl; auto top = tok.get_topics(); if (top && !top->empty()) std::cout << "\ttoken topic: '" << (*top)[0] << "', ..." << std::endl; std::cout << std::endl; }public: action_listener(const std::string& name) : name_(name) {}};//** * Local callback & listener class for use with the client connection. * This is primarily intended to receive messages, but it will also monitor * the connection to the broker. If the connection is lost, it will attempt * to restore the connection and re-subscribe to the topic. */class callback : public virtual mqtt::callback, public virtual mqtt::iaction_listener{ // Counter for the number of connection retries int nretry_; // The MQTT client mqtt::async_client& cli_; // Options to use if we need to reconnect mqtt::connect_options& connOpts_; // An action listener to display the result of actions. action_listener subListener_; // This deomonstrates manually reconnecting to the broker by calling // connect() again. This is a possibility for an application that keeps // a copy of it's original connect_options, or if the app wants to // reconnect with different options. // Another way this can be done manually, if using the same options, is // to just call the async_client::reconnect() method. void reconnect() { std::this_thread::sleep_for(std::chrono::milliseconds(2500)); try { cli_.connect(connOpts_, nullptr, *this); } catch (const mqtt::exception& exc) { std::cerr << "Error: " << exc.what() << std::endl; exit(1); } } // Re-connection failure void on_failure(const mqtt::token& tok) override { std::cout << "Connection attempt failed" << std::endl; if (++nretry_ > N_RETRY_ATTEMPTS) exit(1); reconnect(); } // (Re)connection success // Either this or connected() can be used for callbacks. void on_success(const mqtt::token& tok) override {} // (Re)connection success void connected(const std::string& cause) override { std::cout << "\nConnection success" << std::endl; std::cout << "\nSubscribing to topic '" << TOPIC << "'\n" << "\tfor client " << CLIENT_ID << " using QoS" << QOS << "\n" << "\nPress Q<Enter> to quit\n" << std::endl; cli_.subscribe(TOPIC, QOS, nullptr, subListener_); } // Callback for when the connection is lost. // This will initiate the attempt to manually reconnect. void connection_lost(const std::string& cause) override { std::cout << "\nConnection lost" << std::endl; if (!cause.empty()) std::cout << "\tcause: " << cause << std::endl; std::cout << "Reconnecting..." << std::endl; nretry_ = 0; reconnect(); } // Callback for when a message arrives. void message_arrived(mqtt::const_message_ptr msg) override { std::cout << "Message arrived" << std::endl; std::cout << "\ttopic: '" << msg->get_topic() << "'" << std::endl; std::cout << "\tpayload: '" << msg->to_string() << "'\n" << std::endl; } void delivery_complete(mqtt::delivery_token_ptr token) override {}public: callback(mqtt::async_client& cli, mqtt::connect_options& connOpts) : nretry_(0), cli_(cli), connOpts_(connOpts), subListener_("Subscription") {}};/int main(int argc, char* argv[]){ // A subscriber often wants the server to remember its messages when its // disconnected. In that case, it needs a unique ClientID and a // non-clean session. mqtt::async_client cli(SERVER_ADDRESS, CLIENT_ID); mqtt::connect_options connOpts; connOpts.set_clean_session(false); // Install the callback(s) before connecting. callback cb(cli, connOpts); cli.set_callback(cb); // Start the connection. // When completed, the callback will subscribe to topic. try { std::cout << "Connecting to the MQTT server..." << std::flush; cli.connect(connOpts, nullptr, cb); } catch (const mqtt::exception& exc) { std::cerr << "\nERROR: Unable to connect to MQTT server: '" << SERVER_ADDRESS << "'" << exc << std::endl; return 1; } // Just block till user tells us to quit. while (std::tolower(std::cin.get()) != 'q') ; // Disconnect try { std::cout << "\nDisconnecting from the MQTT server..." << std::flush; cli.disconnect()->wait(); std::cout << "OK" << std::endl; } catch (const mqtt::exception& exc) { std::cerr << exc << std::endl; return 1; } return 0;}编译指令:g++ async_subscribe.cpp -I result_dir/paho.mqtt.cpp_result/include/ -I result_dir/paho.mqtt.c_result/include/ -L result_dir/paho.mqtt.cpp_result/lib/ -L result_dir/paho.mqtt.c_result/lib/ -l paho-mqttpp3 -l paho-mqtt3a 4.2 发布——async_publish.cpp这是使用了 libpaho-mqttpp3.so 进行发布消息的源码,源码路径在源码的这个路径:paho.mqtt.cpp-1.3.2/src/samples/async_publish.cpp,只更改了服务器地址。完整代码如下:#include <iostream>#include <cstdlib>#include <string>#include <thread>#include <atomic>#include <chrono>#include <cstring>#include "mqtt/async_client.h"using namespace std;const string DFLT_SERVER_ADDRESS { "117.78.5.125:1883" };const string CLIENT_ID { "paho_cpp_async_publish" };const string PERSIST_DIR { "./persist" };const string TOPIC { "hello" };const char* PAYLOAD1 = "Hello World!";const char* PAYLOAD2 = "Hi there!";const char* PAYLOAD3 = "Is anyone listening?";const char* PAYLOAD4 = "Someone is always listening.";const char* LWT_PAYLOAD = "Last will and testament.";const int QOS = 1;const auto TIMEOUT = std::chrono::seconds(10);//** * A callback class for use with the main MQTT client. */class callback : public virtual mqtt::callback{public: void connection_lost(const string& cause) override { cout << "\nConnection lost" << endl; if (!cause.empty()) cout << "\tcause: " << cause << endl; } void delivery_complete(mqtt::delivery_token_ptr tok) override { cout << "\tDelivery complete for token: " << (tok ? tok->get_message_id() : -1) << endl; }};//** * A base action listener. */class action_listener : public virtual mqtt::iaction_listener{protected: void on_failure(const mqtt::token& tok) override { cout << "\tListener failure for token: " << tok.get_message_id() << endl; } void on_success(const mqtt::token& tok) override { cout << "\tListener success for token: " << tok.get_message_id() << endl; }};//** * A derived action listener for publish events. */class delivery_action_listener : public action_listener{ atomic<bool> done_; void on_failure(const mqtt::token& tok) override { action_listener::on_failure(tok); done_ = true; } void on_success(const mqtt::token& tok) override { action_listener::on_success(tok); done_ = true; }public: delivery_action_listener() : done_(false) {} bool is_done() const { return done_; }};/int main(int argc, char* argv[]){ // A client that just publishes normally doesn't need a persistent // session or Client ID unless it's using persistence, then the local // library requires an ID to identify the persistence files. string address = (argc > 1) ? string(argv[1]) : DFLT_SERVER_ADDRESS, clientID = (argc > 2) ? string(argv[2]) : CLIENT_ID; cout << "Initializing for server '" << address << "'..." << endl; mqtt::async_client client(address, clientID, PERSIST_DIR); callback cb; client.set_callback(cb); auto connOpts = mqtt::connect_options_builder() .clean_session() .will(mqtt::message(TOPIC, LWT_PAYLOAD, strlen(LWT_PAYLOAD), QOS, false)) .finalize(); cout << " ...OK" << endl; try { cout << "\nConnecting..." << endl; mqtt::token_ptr conntok = client.connect(connOpts); cout << "Waiting for the connection..." << endl; conntok->wait(); cout << " ...OK" << endl; // First use a message pointer. cout << "\nSending message..." << endl; mqtt::message_ptr pubmsg = mqtt::make_message(TOPIC, PAYLOAD1); pubmsg->set_qos(QOS); client.publish(pubmsg)->wait_for(TIMEOUT); cout << " ...OK" << endl; // Now try with itemized publish. cout << "\nSending next message..." << endl; mqtt::delivery_token_ptr pubtok; pubtok = client.publish(TOPIC, PAYLOAD2, strlen(PAYLOAD2), QOS, false); cout << " ...with token: " << pubtok->get_message_id() << endl; cout << " ...for message with " << pubtok->get_message()->get_payload().size() << " bytes" << endl; pubtok->wait_for(TIMEOUT); cout << " ...OK" << endl; // Now try with a listener cout << "\nSending next message..." << endl; action_listener listener; pubmsg = mqtt::make_message(TOPIC, PAYLOAD3); pubtok = client.publish(pubmsg, nullptr, listener); pubtok->wait(); cout << " ...OK" << endl; // Finally try with a listener, but no token cout << "\nSending final message..." << endl; delivery_action_listener deliveryListener; pubmsg = mqtt::make_message(TOPIC, PAYLOAD4); client.publish(pubmsg, nullptr, deliveryListener); while (!deliveryListener.is_done()) { this_thread::sleep_for(std::chrono::milliseconds(100)); } cout << "OK" << endl; // Double check that there are no pending tokens auto toks = client.get_pending_delivery_tokens(); if (!toks.empty()) cout << "Error: There are pending delivery tokens!" << endl; // Disconnect cout << "\nDisconnecting..." << endl; client.disconnect()->wait(); cout << " ...OK" << endl; } catch (const mqtt::exception& exc) { cerr << exc.what() << endl; return 1; } return 0;}编译指令:g++ async_publish.cpp -I result_dir/paho.mqtt.cpp_result/include/ -I result_dir/paho.mqtt.c_result/include/ -L re
-
一、前言随着局域网(LAN)应用的广泛使用,网络通信已经成为软件设计中不可或缺的一部分。局域网聊天软件作为一种常见的网络应用,可以实现多个用户之间的实时通信,广泛应用于企业内部沟通和小型网络环境中。本项目设计并实现一个基于C语言的局域网群聊程序,通过UDP广播搜索在线用户,并在发现其他在线应用程序后,自动建立TCP连接,实现消息的收发。本程序展示了如何在Windows环境下使用Winsock API进行网络编程,提供了对UDP和TCP协议的实际应用,体现了网络通信中的多线程处理、广播通信和实时消息传递的关键技术点。二、好友探测功能在局域网内探测并发现其他在线用户是局域网聊天软件最主要的核心功能。该过程涉及到局域网广播(UDP广播)和TCP连接两个关键步骤。下面将详细介绍实现这一功能的方法和设计思路。2.1 使用UDP广播探测在线用户1.1 UDP广播的概念UDP(用户数据报协议)是一种无连接的、轻量级的传输协议,适用于发送小数据包。UDP广播允许将数据包发送到局域网内的所有设备,所有在监听特定端口的设备都能够接收到广播消息。这种特性使得UDP广播非常适合用于探测和发现局域网内的在线设备。1.2 探测思路在程序启动时,客户端会通过UDP广播发送一个上线通知消息,表示自己已在线。其他监听同一端口的客户端接收到这一消息后,可以获知该客户端的IP地址,并识别出它在线。具体的实现步骤如下:创建UDP套接字:为UDP通信创建一个套接字,并配置为允许广播。发送广播消息:程序向局域网内的广播地址(通常为255.255.255.255)发送一个消息,例如"HELLO, I'M ONLINE"。这个消息会被局域网内所有监听相同端口的设备接收。监听UDP消息:每个客户端都持续监听来自局域网内的UDP消息。一旦接收到广播消息,客户端会记录发送方的IP地址和端口,以确认该客户端在线。2.2 建立TCP连接实现通信2.1 TCP连接的必要性UDP广播虽然可以有效地发现在线用户,但由于其无连接的特点,不适合用于长时间的可靠通信。因此,在发现其他在线用户后,程序需要通过TCP(传输控制协议)建立可靠的点对点连接。TCP是一种面向连接的协议,能够确保数据的完整性和顺序传输,非常适合用于聊天消息的传递。2.2 连接建立的流程接收广播后尝试连接:当客户端接收到来自其他用户的UDP广播后,会通过TCP连接到该用户。客户端会使用从UDP消息中获取的IP地址和预定义的TCP端口号,发起连接请求。接受连接请求:已经在线的客户端会开启一个TCP监听套接字,等待来自其他客户端的连接请求。一旦有请求到达,程序将接受连接,并启动一个独立的线程处理与该客户端之间的消息通信。消息收发:通过建立的TCP连接,用户可以实时发送和接收聊天消息,确保消息在网络不稳定的情况下仍能可靠传输。2.3 多线程处理的必要性由于UDP广播接收、TCP连接监听和消息收发等操作需要同时进行,程序采用了多线程的设计。每个功能模块都运行在独立的线程中,确保它们可以并行处理,互不干扰。这样不仅提高了程序的响应速度,还增强了用户体验,确保通信的实时性。2.4 总结通过UDP广播发现局域网内的在线用户,然后利用TCP协议建立可靠的通信连接,这是局域网聊天软件的核心设计思路。UDP广播的轻量和广泛性使得在线用户的探测变得高效,而TCP连接则保证了后续通信的可靠性。多线程的引入进一步优化了程序的性能,使得该局域网聊天软件在实际应用中表现出色。三、代码实现下面是完整的代码。在VS2022里运行测试。#define _WINSOCK_DEPRECATED_NO_WARNINGS#include <stdio.h>#include <stdlib.h>#include <string.h>#include <winsock2.h>#include <ws2tcpip.h>#include <process.h>#pragma comment(lib, "ws2_32.lib")#define UDP_PORT 8888#define TCP_PORT 8889#define BROADCAST_ADDR "255.255.255.255"#define BUFFER_SIZE 1024typedef struct ClientInfo { SOCKET socket; struct sockaddr_in address;} ClientInfo;void udp_broadcast_listener(void* param);void tcp_connection_listener(void* param);void tcp_message_listener(void* param);int main() { WSADATA wsaData; SOCKET udp_socket, tcp_socket; struct sockaddr_in udp_addr, tcp_addr; char buffer[BUFFER_SIZE]; struct sockaddr_in client_addr; int addr_len = sizeof(client_addr); // 初始化 Winsock if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup failed\n"); return 1; } // 创建 UDP 套接字 udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (udp_socket == INVALID_SOCKET) { printf("UDP socket creation failed\n"); WSACleanup(); return 1; } // 配置 UDP 广播地址 memset(&udp_addr, 0, sizeof(udp_addr)); udp_addr.sin_family = AF_INET; udp_addr.sin_port = htons(UDP_PORT); udp_addr.sin_addr.s_addr = inet_addr(BROADCAST_ADDR); // 启动 UDP 广播监听线程 _beginthread(udp_broadcast_listener, 0, NULL); // 启动 TCP 连接监听线程 _beginthread(tcp_connection_listener, 0, NULL); // 向局域网内广播自己上线 strcpy(buffer, "HELLO, I'M ONLINE"); sendto(udp_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&udp_addr, sizeof(udp_addr)); while (1) { fgets(buffer, BUFFER_SIZE, stdin); buffer[strcspn(buffer, "\n")] = 0; // 移除换行符 sendto(udp_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&udp_addr, sizeof(udp_addr)); } closesocket(udp_socket); WSACleanup(); return 0;}void udp_broadcast_listener(void* param) { SOCKET udp_socket; struct sockaddr_in udp_addr, sender_addr; char buffer[BUFFER_SIZE]; int addr_len = sizeof(sender_addr); // 创建 UDP 套接字 udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (udp_socket == INVALID_SOCKET) { printf("UDP socket creation failed in listener\n"); return; } // 配置 UDP 地址 memset(&udp_addr, 0, sizeof(udp_addr)); udp_addr.sin_family = AF_INET; udp_addr.sin_port = htons(UDP_PORT); udp_addr.sin_addr.s_addr = INADDR_ANY; // 绑定套接字 if (bind(udp_socket, (struct sockaddr*)&udp_addr, sizeof(udp_addr)) == SOCKET_ERROR) { printf("UDP socket binding failed\n"); closesocket(udp_socket); return; } while (1) { int recv_len = recvfrom(udp_socket, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&sender_addr, &addr_len); if (recv_len > 0) { buffer[recv_len] = '\0'; printf("Received UDP broadcast from %s: %s\n", inet_ntoa(sender_addr.sin_addr), buffer); // 如果接收到"HELLO, I'M ONLINE",尝试建立TCP连接 if (strcmp(buffer, "HELLO, I'M ONLINE") == 0) { SOCKET tcp_socket; struct sockaddr_in tcp_addr; tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (tcp_socket == INVALID_SOCKET) { printf("TCP socket creation failed\n"); continue; } // 配置 TCP 地址 memset(&tcp_addr, 0, sizeof(tcp_addr)); tcp_addr.sin_family = AF_INET; tcp_addr.sin_port = htons(TCP_PORT); tcp_addr.sin_addr.s_addr = sender_addr.sin_addr.s_addr; if (connect(tcp_socket, (struct sockaddr*)&tcp_addr, sizeof(tcp_addr)) == SOCKET_ERROR) { printf("TCP connection failed to %s\n", inet_ntoa(tcp_addr.sin_addr)); closesocket(tcp_socket); } else { printf("Connected to %s\n", inet_ntoa(tcp_addr.sin_addr)); // 启动 TCP 消息监听线程 ClientInfo* client = (ClientInfo*)malloc(sizeof(ClientInfo)); client->socket = tcp_socket; client->address = tcp_addr; _beginthread(tcp_message_listener, 0, client); } } } } closesocket(udp_socket);}void tcp_connection_listener(void* param) { SOCKET tcp_socket, client_socket; struct sockaddr_in tcp_addr, client_addr; int addr_len = sizeof(client_addr); // 创建 TCP 套接字 tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (tcp_socket == INVALID_SOCKET) { printf("TCP socket creation failed\n"); return; } // 配置 TCP 地址 memset(&tcp_addr, 0, sizeof(tcp_addr)); tcp_addr.sin_family = AF_INET; tcp_addr.sin_port = htons(TCP_PORT); tcp_addr.sin_addr.s_addr = INADDR_ANY; // 绑定套接字 if (bind(tcp_socket, (struct sockaddr*)&tcp_addr, sizeof(tcp_addr)) == SOCKET_ERROR) { printf("TCP socket binding failed\n"); closesocket(tcp_socket); return; } // 开始监听 if (listen(tcp_socket, 5) == SOCKET_ERROR) { printf("TCP socket listen failed\n"); closesocket(tcp_socket); return; } printf("TCP connection listener started...\n"); while (1) { client_socket = accept(tcp_socket, (struct sockaddr*)&client_addr, &addr_len); if (client_socket == INVALID_SOCKET) { printf("TCP accept failed\n"); continue; } printf("Accepted connection from %s\n", inet_ntoa(client_addr.sin_addr)); // 启动 TCP 消息监听线程 ClientInfo* client = (ClientInfo*)malloc(sizeof(ClientInfo)); client->socket = client_socket; client->address = client_addr; _beginthread(tcp_message_listener, 0, client); } closesocket(tcp_socket);}void tcp_message_listener(void* param) { ClientInfo* client = (ClientInfo*)param; char buffer[BUFFER_SIZE]; int recv_len; while ((recv_len = recv(client->socket, buffer, BUFFER_SIZE, 0)) > 0) { buffer[recv_len] = '\0'; printf("Message from %s: %s\n", inet_ntoa(client->address.sin_addr), buffer); } printf("Connection closed by %s\n", inet_ntoa(client->address.sin_addr)); closesocket(client->socket); free(client);}程序在主函数里通过 WSAStartup 函数初始化Winsock库,这是一种Windows平台上的网络编程库,提供了网络通信所需的API。初始化成功后,程序可以使用Winsock提供的各种网络功能。创建了两个主要的套接字:UDP套接字:用于广播消息和接收其他设备的广播。TCP套接字:用于建立点对点的通信连接。在程序启动时,通过UDP广播向局域网内所有设备发送一个“HELLO, I'M ONLINE”的消息。这一消息用来告知局域网内的其他用户自己的存在,从而实现在线用户的探测。为了接收其他用户的广播消息,程序创建了一个UDP套接字并绑定到特定的端口上(UDP_PORT)。程序通过这个套接字监听局域网内的所有广播消息,提取发送者的IP地址,并处理接收到的消息。一旦接收到来自其他在线用户的UDP广播消息,程序会尝试通过TCP建立连接。步骤包括:从UDP消息中提取发送者的IP地址。使用提取的IP地址和预定义的TCP端口号发起TCP连接请求。如果连接成功,程序将建立一个可靠的点对点通信通道,用于后续的聊天消息传递。程序创建一个TCP套接字,并在特定端口上进行监听,等待其他用户的连接请求。当有新的连接请求到达时,程序接受该连接,并为每个连接创建一个新的线程,以处理与该连接相关的消息通信。用户在键盘上输入消息后,程序通过UDP套接字广播该消息到局域网内的所有在线用户。此功能确保所有在线的用户都能看到发送的消息。程序通过TCP连接接收来自其他用户的消息。接收到的消息将被显示在终端上,提供实时的聊天功能。每个TCP连接使用一个独立的线程进行处理,确保消息的及时传递和处理。程序采用了多线程技术来并行处理不同的任务,确保系统的响应性和效率。主要线程包括:UDP广播监听线程:处理UDP广播消息的接收和处理。TCP连接监听线程:接受来自其他用户的TCP连接请求。TCP消息处理线程:处理与每个已连接用户之间的消息交换。在程序退出时,所有打开的套接字都会被关闭,资源得到释放。程序通过调用 closesocket 函数关闭套接字,并调用 WSACleanup 进行Winsock库的清理,确保程序在退出时不会泄漏资源。
-
一、前言LibJPEG库是一个广泛使用的开源C库,用于处理JPEG图像的压缩和解压缩。该库由独立JPEG小组(Independent JPEG Group, IJG)开发,提供了功能强大的API,用于创建和读取JPEG文件。LibJPEG库支持JPEG的所有常见功能,包括高质量的压缩、解压缩、图像处理、颜色空间转换等。采用DCT(离散余弦变换)算法实现了高效的图像压缩,同时支持各种图像质量的调整。LibJPEG的灵活性和性能使其成为图像处理应用中的标准工具,被广泛应用于图像编辑软件、图像查看器、图像传输等多个领域。该库跨平台兼容,支持Windows、Linux、macOS等操作系统,开发者可以轻松将其集成到不同的平台和应用中。LibJPEG库还具有良好的文档支持,提供详细的编程指南和示例代码,帮助开发者快速上手和实现复杂的图像处理功能。由于其开源性质和广泛的应用,LibJPEG在业界享有很高的声誉,成为许多图像处理项目的首选库。LibJPEG库的源码下载地址:cid:link_1下载后是一个ZIP的压缩包,解压看到的数据:如果要下载编译好的二进制文件,可以去这里下载:cid:link_0二、函数接口解释LibJPEG库提供了一组函数接口,用于实现JPEG图像的压缩和解压缩操作。以下是一些常用的LibJPEG库函数接口及其详细讲解,包括函数功能、参数作用等。2.1 JPEG压缩相关函数jpeg_create_compress功能:初始化JPEG压缩对象。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。说明:必须在进行任何其他JPEG压缩操作之前调用该函数。jpeg_stdio_dest功能:设置压缩数据的目标为标准I/O流。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。FILE *outfile:目标文件的文件指针。说明:指定JPEG数据输出的目标文件。jpeg_set_defaults功能:设置JPEG压缩参数的默认值。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。说明:设置一组默认的JPEG压缩参数。jpeg_set_quality功能:设置JPEG压缩质量。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。int quality:压缩质量,范围为0(最差)到100(最佳)。boolean force_baseline:如果为TRUE,强制生成符合JPEG标准的文件。说明:调整JPEG压缩的质量参数。jpeg_start_compress功能:启动压缩过程。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。boolean write_all_tables:如果为TRUE,写入所有JPEG标记。说明:在压缩数据之前调用该函数。jpeg_write_scanlines功能:写入压缩数据行。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。JSAMPARRAY scanlines:指向存储图像数据的缓冲区。JDIMENSION num_lines:要写入的行数。说明:写入图像数据到JPEG压缩对象中。jpeg_finish_compress功能:完成压缩过程。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。说明:在完成所有数据写入后调用,关闭JPEG压缩对象。jpeg_destroy_compress功能:销毁JPEG压缩对象。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。说明:释放JPEG压缩对象分配的所有资源。2.2 JPEG解压缩相关函数jpeg_create_decompress功能:初始化JPEG解压缩对象。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。说明:必须在进行任何其他JPEG解压缩操作之前调用该函数。jpeg_stdio_src功能:设置解压数据的源为标准I/O流。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。FILE *infile:源文件的文件指针。说明:指定JPEG数据输入的源文件。jpeg_read_header功能:读取JPEG文件的头部信息。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。boolean require_image:如果为TRUE,要求图像数据存在。说明:读取JPEG文件的头部信息,解析JPEG图像的元数据。jpeg_start_decompress功能:启动解压缩过程。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。说明:在解压缩数据之前调用该函数。jpeg_read_scanlines功能:读取解压缩的数据行。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。JSAMPARRAY scanlines:指向存储解压缩数据的缓冲区。JDIMENSION max_lines:要读取的最大行数。说明:从JPEG解压缩对象中读取图像数据。jpeg_finish_decompress功能:完成解压缩过程。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。说明:在完成所有数据读取后调用,关闭JPEG解压缩对象。jpeg_destroy_decompress功能:销毁JPEG解压缩对象。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。说明:释放JPEG解压缩对象分配的所有资源。2.3 辅助函数jpeg_std_error功能:初始化JPEG错误处理对象。参数:struct jpeg_error_mgr *err:指向JPEG错误处理对象的指针。说明:设置默认的JPEG错误处理例程。这些函数共同构成了LibJPEG库的核心接口,通过它们可以实现JPEG图像的高效压缩和解压缩。在使用这些函数时,需要按照特定的调用顺序来确保正确的操作和资源管理。三、代码实现以下是将RGB565图像转换为RGB888并压缩为JPEG格式的完整示例代码:#include <stdio.h>#include <stdlib.h>#include <stdint.h>#include <jpeglib.h>// 将RGB565转换为RGB888void RGB565_to_RGB888(uint16_t rgb565, uint8_t *r, uint8_t *g, uint8_t *b) { *r = ((rgb565 >> 11) & 0x1F) << 3; *g = ((rgb565 >> 5) & 0x3F) << 2; *b = (rgb565 & 0x1F) << 3;}// 将RGB565格式图像转换为RGB888格式void convert_RGB565_to_RGB888(const uint8_t *rgb565_image, uint8_t *rgb888_image, int width, int height) { for (int i = 0; i < width * height; i++) { uint16_t rgb565 = ((uint16_t)rgb565_image[2 * i + 1] << 8) | rgb565_image[2 * i]; RGB565_to_RGB888(rgb565, &rgb888_image[3 * i], &rgb888_image[3 * i + 1], &rgb888_image[3 * i + 2]); }}// 将RGB888格式图像压缩为JPEG格式void compress_to_JPEG(const uint8_t *rgb888_image, int width, int height, const char *filename) { struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; FILE *outfile; JSAMPROW row_pointer[1]; int row_stride; cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); if ((outfile = fopen(filename, "wb")) == NULL) { fprintf(stderr, "can't open %s\n", filename); exit(1); } jpeg_stdio_dest(&cinfo, outfile); cinfo.image_width = width; cinfo.image_height = height; cinfo.input_components = 3; cinfo.in_color_space = JCS_RGB; jpeg_set_defaults(&cinfo); jpeg_set_quality(&cinfo, 75, TRUE); jpeg_start_compress(&cinfo, TRUE); row_stride = width * 3; while (cinfo.next_scanline < cinfo.image_height) { row_pointer[0] = (JSAMPROW) &rgb888_image[cinfo.next_scanline * row_stride]; jpeg_write_scanlines(&cinfo, row_pointer, 1); } jpeg_finish_compress(&cinfo); fclose(outfile); jpeg_destroy_compress(&cinfo);}int main() { // 假设图像尺寸为100x100 int width = 100; int height = 100; // 示例RGB565数据 uint8_t rgb565_image[20000]; for (int i = 0; i < 20000; i++) { rgb565_image[i] = rand() % 256; // 用随机数据填充 } // 分配RGB888图像数据的内存 uint8_t *rgb888_image = (uint8_t *)malloc(width * height * 3); if (rgb888_image == NULL) { fprintf(stderr, "Unable to allocate memory for RGB888 image\n"); return 1; } // 转换RGB565到RGB888 convert_RGB565_to_RGB888(rgb565_image, rgb888_image, width, height); // 压缩并保存为JPEG文件 compress_to_JPEG(rgb888_image, width, height, "output.jpg"); // 释放内存 free(rgb888_image); printf("Compression to JPEG completed.\n"); return 0;}
-
一、前言需求: Qt开发Android程序过程中,点击按钮就打开一个PPT文件。Qt在Windows上要打开PPT文件或者其他文件很容易。可以使用QDesktopServices打开文件,非常方便。QDesktopServices提供了静态接口调用系统级别的功能。这里用的QDesktopServices 是 Qt 框架中的一个类,用于在跨平台应用程序中方便地访问和使用主机操作系统的桌面服务和功能。该类提供了一些静态方法,用于打开网址、文件和邮件客户端等系统默认应用程序。它的主要目的是让开发者能够轻松调用系统级别的功能,而不需要直接编写与操作系统相关的代码。代码如下:QUrl url = QUrl::fromLocalFile("storage/emulated/0/Download/UseHelp.ppt"); bool IsOK = QDesktopServices::openUrl(url); 下面是QDesktopServices 的帮助文档介绍。QDesktopServices 只适用于桌面应用程序。 如果在Linux系统下想要打开PPT文件,也可以采用QDesktopServices 来实现。前提也是需要先安装LibreOffice或OpenOffice才可以。在Qt的文档里也提供了openUrl静态方法的使用说明。使用 QDesktopServices::openUrl在Linux系统上打开PPT文件。#include <QDesktopServices>#include <QUrl>#include <QDebug>int main(int argc, char *argv[]){ QApplication app(argc, argv); QString pptFilePath = "/path/to/your/presentation.ppt"; QUrl pptUrl = QUrl::fromLocalFile(pptFilePath); if (!QDesktopServices::openUrl(pptUrl)) { qWarning() << "Failed to open PPT file."; } return app.exec();}但是在Android移动平台上,这个办法就行不通了。 需要通过JNI接口与Android系统交互,才可以完成系统级别的操作。二、通过JNI与Android系统交互在Qt for Android中,可以通过Qt提供的Java Native Interface (JNI)与Android系统交互,可以调用系统功能,打开PPT文件。下面演示如何在Qt中使用C++代码,通过JNI调用Android的Intent来打开PPT文件。2.1 操作步骤在Qt项目中添加Android相关的权限和活动声明:在AndroidManifest.xml文件中添加必要的权限和活动声明。使用Qt的JNI接口调用Android Intent:在C++代码中使用Qt的JNI接口来调用Android的Intent。2.2 代码(1)AndroidManifest.xml在AndroidManifest.xml文件中添加以下权限声明,确保应用有权读取文件:<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.qtproject.example"> <application android:label="@string/app_name"> <activity android:name="org.qtproject.qt5.android.bindings.QtActivity" android:configChanges="orientation|screenSize" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/></manifest>(2)Qt C++代码在Qt C++代码中使用JNI接口调用Android的Intent来打开PPT文件。#include <QCoreApplication>#include <QAndroidJniObject>#include <QtAndroid>#include <QDebug>void openPPTFile(const QString &filePath) { QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative", "activity", "()Landroid/app/Activity;"); if (activity.isValid()) { QAndroidJniObject intent("android/content/Intent", "()V"); intent.callObjectMethod("setAction", "(Ljava/lang/String;)Landroid/content/Intent;", QAndroidJniObject::fromString("android.intent.action.VIEW").object<jstring>()); QAndroidJniObject uri = QAndroidJniObject::callStaticObjectMethod( "android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QAndroidJniObject::fromString(filePath).object<jstring>()); intent.callObjectMethod("setDataAndType", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/Intent;", uri.object<jobject>(), QAndroidJniObject::fromString("application/vnd.ms-powerpoint").object<jstring>()); intent.callObjectMethod("addFlags", "(I)Landroid/content/Intent;", 0x10000000); // Intent.FLAG_ACTIVITY_NEW_TASK activity.callMethod<void>("startActivity", "(Landroid/content/Intent;)V", intent.object<jobject>()); } else { qDebug() << "Failed to get the activity."; }}int main(int argc, char *argv[]){ QCoreApplication app(argc, argv); QString pptFilePath = "/sdcard/Download/sample.pptx"; // 指定要打开的PPT文件路径 openPPTFile(pptFilePath); return app.exec();}2.3 代码说明openPPTFile函数:该函数接受PPT文件的路径作为参数,并使用JNI接口调用Android的Intent来打开该文件。QAndroidJniObject:用于与Java对象和方法交互。setAction和setDataAndType方法:设置Intent的操作和数据类型。startActivity方法:启动Intent以打开PPT文件。2.4 编译和运行需要先在Android设备上安装合适的PPT查看应用(比如:WPS),并且PPT文件路径正确。编译和运行Qt项目时,应用将通过系统默认的应用打开指定的PPT文件。
-
一、前言BMP (Bitmap) 图像格式是一种无损压缩的位图文件格式,最初由微软公司在Windows操作系统中引入,用于存储图像数据。BMP格式的主要优点是它简单易用,且支持多种颜色深度。这种格式不包含任何压缩算法,这意味着图像的质量不会因为保存而损失,但这也导致了文件大小相对较大。当前做的项目是采用STM32F103ZET6单片机接上OV7725(带FIFO)摄像头实现图像采集,拍照功能。 OV7725摄像头输出的格式是RGB565像素格式,为了方便将OV7725摄像头返回的图像数据放在SD卡里存储,并且能够在电脑上打开,通过图片查看软件查看。就需要将RGB565像素数据封装成一张图片格式,也就是相当于加一个壳子。这样电脑上的图片查看器就可以正常查看图片了。目前的图片格式有很多,平时最常见的有JPG、PNG、BMP这些格式。 这里面的JPG是压缩格式,保存的图片可以很小,JPEG使用离散余弦变换(DCT)压缩,这个算法在单片机上实现的要求毕竟高,毕竟单片机的性能摆在这里。 而BMP是不包含任何压缩算法,存储的是原始的像素格式,作为单片机里拍照存储这是首选的图片封装格式了。整个项目设计完的核心功能是:通过OV7725摄像头采集一帧RGB565格式的图像,并将其封装成BMP格式后,利用FATFS文件系统存储到SD卡上。项目中,STM32单片机通过SPI协议与SD卡进行通信。由于OV7725摄像头输出的是RGB565格式的数据,而标准BMP文件使用RGB888格式存储像素数据,因此还涉及到了图像格式的转换问题。要完成这个项目涉及的技术其实也有几个的:(1)SD卡的驱动编写,SD卡支持SDIO和SPI两种协议。 要说简单那自然首选SPI协议,不过就是速度稍微慢一点。(2)OV7725摄像头的驱动编写,毕竟要从摄像头里读取数据。分为控制协议和数据总线。(3)FATFS文件系统的移植,如果在单片机上要以文件的形式管理SD卡,那肯定是需要文件系统了。(4)BMP图片的格式理解,要将图片保存为BMP图片格式。需要完全理解BMP图片格式的是如何的封装的。这篇文章最重要的是内容是讲解“ BMP图片如何封装,学习BMP图像格式封装,RGB565与RGB888像素点转换。二、BMP文件结构2.1 BMP图片的格式BMP 文件的内部格式组成:(1)文件头 (File Header)类型标识 (bfType): 两个字节,通常为 BM (0x424D),表明文件类型为BMP。文件大小 (bfSize): 四个字节,表示整个文件的大小(包括文件头、信息头和像素数据)。保留字段 (bfReserved1, bfReserved2): 通常是0。数据偏移量 (bfOffBits): 四个字节,指明像素数据相对于文件起始位置的偏移量。(2)信息头 (Info Header)头大小 (biSize): 四个字节,信息头的大小。宽度 (biWidth): 四个字节,图像的宽度(以像素为单位)。高度 (biHeight): 四个字节,图像的高度(以像素为单位)。高度值可以是正数也可以是负数;正数表示从左下角开始绘制,负数则表示从左上角开始绘制。平面数 (biPlanes): 通常是1。位数 (biBitCount): 每个像素的位数,常见的值有1、4、8、16、24或32。压缩方法 (biCompression): 指定使用的压缩方法,如果是0,则表示没有压缩。图像大小 (biSizeImage): 压缩后的图像大小,如果未压缩,则该值可能为0。水平分辨率 (biXPelsPerMeter): 水平方向上的分辨率(每米像素数)。垂直分辨率 (biYPelsPerMeter): 垂直方向上的分辨率(每米像素数)。调色板数目 (biClrUsed): 调色板中的颜色数目,如果为0,则表示所有可能的颜色都被使用。重要颜色数目 (biClrImportant): 重要的颜色数目,如果为0,则表示所有颜色都同样重要。(3)颜色表 (Color Table)如果位数小于24,则存在一个颜色表,其中定义了每个像素值所对应的RGB颜色。(4)像素数据 (Pixel Data)图像的实际像素数据按照从左到右、从下到上的顺序排列。为了保证每一行的字节数为4的倍数,通常会在每行末尾添加填充字节。下面是BMP文件格式的一个详细描述,包括每个字段的名称、长度、含义以及它们在文件中的位置。字段名称类型长度 (字节)描述bfType字符串2文件类型的标识,通常为 BM (0x424D)bfSizeDWORD4整个文件的大小,包括文件头、信息头和像素数据bfReserved1WORD2保留字段,应设为0bfReserved2WORD2保留字段,应设为0bfOffBitsDWORD4像素数据相对于文件起始位置的偏移量biSizeDWORD4信息头的大小,通常为40 (0x28)biWidthLONG4图像的宽度(以像素为单位),可以是正数或负数biHeightLONG4图像的高度(以像素为单位),可以是正数或负数biPlanesWORD2平面数,通常为1biBitCountWORD2每个像素的位数,常见的值有1、4、8、16、24或32biCompressionDWORD4压缩方法,如果是0,则表示没有压缩biSizeImageDWORD4压缩后的图像大小,如果未压缩,则该值可能为0biXPelsPerMeterLONG4水平方向上的分辨率(每米像素数),通常为0biYPelsPerMeterLONG4垂直方向上的分辨率(每米像素数),通常为0biClrUsedDWORD4调色板中的颜色数目,如果为0,则表示所有可能的颜色都被使用biClrImportantDWORD4重要的颜色数目,如果为0,则表示所有颜色都同样重要Color TableRGBQUAD0 or more调色板(仅当位数小于24时存在),每个颜色占用4字节Pixel DataBYTE[]变长像素数据,按从左到右、从下到上的顺序排列,每行可能有填充字节说明文件头 (File Header) : 从文件的开头到 bfOffBits 字段结束。信息头 (Info Header) : 从 biSize 字段开始,直到 biClrImportant 字段结束。颜色表 (Color Table) : 如果位数小于24,则存在一个颜色表,用于定义每个像素值所对应的RGB颜色。像素数据 (Pixel Data) : 图像的实际像素数据按照从左到右、从下到上的顺序排列。为了保证每一行的字节数为4的倍数,会在每行末尾添加填充字节。对于24位的BMP文件(即 biBitCount 的值为24),不会存在颜色表,每个像素直接由三个字节(RGB888格式)表示。2.2 RGB888与RGB565格式是什么?RGB565和RGB888都是色彩模型在计算机图形学中的具体实现方式,它们分别代表了不同位深的颜色编码方式。这两种格式主要用于存储图像数据,特别是在显示设备和图像处理软件中。(1)RGB565RGB565 是一种16位的彩色图像格式,其中红色和蓝色各占用5位,绿色占用6位。这是因为人眼对绿色更为敏感,因此给绿色分配更多的位数来提高颜色精度。这种格式通常用于节省存储空间或减少内存带宽的需求,尤其是在早期的移动设备和嵌入式系统中非常常见。位分配:11-15位: 5位红色 (R)5-10位: 6位绿色 (G)0-4位: 5位蓝色 (B)这种格式的总位数为16位,可以表示 (2^{16}) 或者 65,536 种不同的颜色。(2)RGB888RGB888 是一种24位的彩色图像格式,每种颜色(红、绿、蓝)都使用8位来表示。这意味着每种颜色都有256级灰度等级,总共可以表示 (2^{24}) 或者 16,777,216 种不同的颜色。位分配:16-23位: 8位红色 (R)8-15位: 8位绿色 (G)0-7位: 8位蓝色 (B)由于RGB888格式使用更多的位数来表示颜色,所以它能够提供更丰富的色彩细节,这对于高保真度的图像来说是非常重要的。(3)区别位深:RGB565: 使用16位,每像素5:6:5的位分配。RGB888: 使用24位,每像素8:8:8的位分配。颜色范围:RGB565: 可以表示大约65,536种颜色。RGB888: 可以表示大约16,777,216种颜色。用途:RGB565: 更适合于需要节省存储空间的应用,如旧式的显示器、手机屏幕等。RGB888: 适用于需要高色彩保真的应用,如专业摄影、图形设计等领域。性能:RGB565: 在存储和传输方面更加高效,但是颜色精度较低。RGB888: 颜色精度更高,但需要更多的存储空间和传输带宽。(4)如何构成RGB565:每个像素由两个字节组成。例如,一个红色像素可能表示为 0xF800(红色部分接近最大值,绿色和蓝色部分接近最小值)。RGB888:每个像素由三个字节组成。例如,一个红色像素可能表示为 0xFF0000(红色部分为最大值255,绿色和蓝色部分为0)。(5)示例可以创建一个简单的例子来说明这些格式是如何工作的。假设有一个像素,它的红色、绿色和蓝色分量分别为128(十六进制为0x80)。RGB565: 对于每个颜色通道,需要将8位的值转换为相应的位数。红色: 0x80 -> 0x1F (5位)绿色: 0x80 -> 0x20 (6位)蓝色: 0x80 -> 0x1F (5位)所以,一个灰色像素在RGB565格式下的值可能是 0x1F201F。RGB888: 我们直接使用8位值。红色: 0x80绿色: 0x80蓝色: 0x80这样,一个灰色像素在RGB888格式下的值将是 0x808080。三、实现代码3.1 RGB565转RGB888的代码下面是一个将 RGB565 数组转换为 RGB888 数组的 C 语言函数:#include <stdint.h>#include <stdlib.h>#include <stdio.h>// 将 RGB565 转换为 RGB888 的函数void RGB565_to_RGB888_array(const uint16_t *rgb565_array, size_t length, uint8_t *rgb888_array) { for (size_t i = 0; i < length; i++) { uint16_t rgb565 = rgb565_array[i]; // 提取 RGB565 中的颜色分量 uint8_t red = (rgb565 >> 11) & 0x1F; // 5 bits uint8_t green = (rgb565 >> 5) & 0x3F; // 6 bits uint8_t blue = rgb565 & 0x1F; // 5 bits // 将颜色分量扩展到 8 位 uint8_t r = (red << 3) | (red >> 2); // 5 bits to 8 bits uint8_t g = (green << 2) | (green >> 4); // 6 bits to 8 bits uint8_t b = (blue << 3) | (blue >> 2); // 5 bits to 8 bits // 将结果存储到 RGB888 数组 rgb888_array[i * 3] = r; rgb888_array[i * 3 + 1] = g; rgb888_array[i * 3 + 2] = b; }}int main() { // 示例 RGB565 数组 uint16_t rgb565_array[] = {0x1F3F, 0x07E0, 0xF800}; size_t length = sizeof(rgb565_array) / sizeof(rgb565_array[0]); // 分配 RGB888 数组内存 uint8_t *rgb888_array = (uint8_t *)malloc(length * 3 * sizeof(uint8_t)); if (rgb888_array == NULL) { perror("Unable to allocate memory for RGB888 array"); return 1; } // 转换 RGB565 数组到 RGB888 数组 RGB565_to_RGB888_array(rgb565_array, length, rgb888_array); // 打印 RGB888 结果 for (size_t i = 0; i < length; i++) { printf("RGB888[%zu]: R=%d, G=%d, B=%d\n", i, rgb888_array[i * 3], rgb888_array[i * 3 + 1], rgb888_array[i * 3 + 2]); } // 释放分配的内存 free(rgb888_array); return 0;}这个函数 RGB565_to_RGB888_array 接收一个 RGB565 数组和数组的长度,并返回一个 RGB888 数组。每个 RGB565 值被转换为三个 8 位的 RGB 分量,并存储在提供的 RGB888 数组中。示例中的 main 函数展示了如何调用这个转换函数并打印结果。3.2 BMP图片封装: 头文件#ifndef BMP_H#define BMP_H#include "ff.h"#include "string.h"#include "sys.h"#pragma pack(1) /* 必须在结构体定义之前使用,这是为了让结构体中各成员按1字节对齐 *//*需要文件信息头:14个字节 */typedef struct tagBITMAPFILEHEADER{ unsigned short bfType; //保存图片类似。 'BM' unsigned long bfSize; //图片的大小 unsigned short bfReserved1; unsigned short bfReserved2; unsigned long bfOffBits; //RGB数据偏移地址}BITMAPFILEHEADER;/* 位图信息头 */typedef struct tagBITMAPINFOHEADER { /* bmih */ unsigned long biSize; //结构体大小 unsigned long biWidth; //宽度 unsigned long biHeight; //高度 unsigned short biPlanes; unsigned short biBitCount; //颜色位数 unsigned long biCompression; unsigned long biSizeImage; unsigned long biXPelsPerMeter; unsigned long biYPelsPerMeter; unsigned long biClrUsed; unsigned long biClrImportant;}BITMAPINFOHEADER;#define RGB888_RED 0x00ff0000 #define RGB888_GREEN 0x0000ff00 #define RGB888_BLUE 0x000000ff #define RGB565_RED 0xf800 #define RGB565_GREEN 0x07e0 #define RGB565_BLUE 0x001f u8 photograph_BMP(u8 *filename,int Width,int Height);void photograph_open(u8 *filename,int Width,int Height);void photograph_write(u16 *buff);void photograph_close(void);#endif3.3 BMP图片封装: 源文件#include "bmp.h" unsigned short RGB888ToRGB565(unsigned int n888Color) { unsigned short n565Color = 0; // 获取RGB单色,并截取高位 unsigned char cRed = (n888Color & RGB888_RED) >> 19; unsigned char cGreen = (n888Color & RGB888_GREEN) >> 10; unsigned char cBlue = (n888Color & RGB888_BLUE) >> 3; // 连接 n565Color = (cRed << 11) + (cGreen << 5) + (cBlue << 0); return n565Color; } unsigned int RGB565ToRGB888(unsigned short n565Color) { unsigned int n888Color = 0; // 获取RGB单色,并填充低位 unsigned char cRed = (n565Color & RGB565_RED) >> 8; unsigned char cGreen = (n565Color & RGB565_GREEN) >> 3; unsigned char cBlue = (n565Color & RGB565_BLUE) << 3; // 连接 n888Color = (cRed << 16) + (cGreen << 8) + (cBlue << 0); return n888Color; } //拍摄BMP的图片u8 photograph_BMP(u8 *filename,int Width,int Height){ u32 cnt; int x,y; u8 res; char *p; u16 c16; //16位颜色值 u32 c32; //24位颜色值 BITMAPFILEHEADER BmpHead; //保存图片文件头的信息 BITMAPINFOHEADER BmpInfo; //图片参数信息 /*1. 创建BMP文件*/ FIL file; res = f_open(&file,(char*)filename, FA_OPEN_ALWAYS | FA_READ | FA_WRITE); //读写加创建 if(res!=0)return 1; /*2. 填充图片数据头*/ memset(&BmpHead,0,sizeof(BITMAPFILEHEADER)); p=(char*)&BmpHead.bfType; //填充BMP图片的类型 *p='B'; *(p+1)='M'; //BmpHead.bfType=0x4d42;//'B''M' //0x4d42 BmpHead.bfSize=Width*Height*3+54; //图片的总大小 BmpHead.bfOffBits=54; //图片数据的偏移量 res=f_write(&file,&BmpHead,sizeof(BITMAPFILEHEADER),&cnt);//写入图片文件头到文文件 if(res!=0)return 1; /*3. 填充图片参数*/ memset(&BmpInfo,0,sizeof(BITMAPINFOHEADER)); BmpInfo.biSize=sizeof(BITMAPINFOHEADER); //当前结构体大小 BmpInfo.biWidth=Width; BmpInfo.biHeight=Height; BmpInfo.biPlanes=1; BmpInfo.biBitCount=24; res=f_write(&file,&BmpInfo,sizeof(BITMAPINFOHEADER),&cnt);//写入图片文件头到文文件 if(res!=0)return 1; /*4. 读取图像参数进行填充*/ for(y=Height-1;y>=0;y--) //因为BMP图片特性,所有需要从LCD最后一行开始读 { for(x=0;x<Width;x++) { //c16=LcdReadPoint(x,y); //LCD读点函数 c32=RGB565ToRGB888(c16); //将16位的颜色转为32位 f_write(&file,&c32,3,&cnt); //写入图片数据 } } /*. 关闭文件*/ f_close(&file); return 0;}BITMAPFILEHEADER BmpHead; //保存图片文件头的信息BITMAPINFOHEADER BmpInfo; //图片参数信息#include <stdio.h>FIL BMP_file;//拍摄1: 创建文件void photograph_open(u8 *filename,int Width,int Height){ u32 cnt; u8 res; char *p; /*1. 创建BMP文件*/ res = f_open(&BMP_file,(char*)filename, FA_OPEN_ALWAYS | FA_READ | FA_WRITE); //读写加创建 if(res!=0) { printf("%s文件打开失败.!\r\n",filename); return; } /*2. 填充图片数据头*/ memset(&BmpHead,0,sizeof(BITMAPFILEHEADER)); p=(char*)&BmpHead.bfType; //填充BMP图片的类型 *p='B'; *(p+1)='M'; //BmpHead.bfType=0x4d42;//'B''M' //0x4d42 BmpHead.bfSize=Width*Height*3+54; //图片的总大小 BmpHead.bfOffBits=54; //图片数据的偏移量 res=f_write(&BMP_file,&BmpHead,sizeof(BITMAPFILEHEADER),&cnt);//写入图片文件头到文文件 if(res!=0) { printf("%s BMP文件头1写入失败.!\r\n",filename); return; } else { printf("%s BMP文件头1写入成功!.%d字节.\r\n",filename,cnt); } /*3. 填充图片参数*/ memset(&BmpInfo,0,sizeof(BITMAPINFOHEADER)); BmpInfo.biSize=sizeof(BITMAPINFOHEADER); //当前结构体大小 BmpInfo.biWidth=Width; BmpInfo.biHeight=Height; BmpInfo.biPlanes=1; BmpInfo.biBitCount=24; res=f_write(&BMP_file,&BmpInfo,sizeof(BITMAPINFOHEADER),&cnt);//写入图片文件头到文文件 if(res!=0) { printf("%s BMP文件头2数据写入失败.!\r\n",filename); return; } else { printf("%s BMP文件头2数据写入成功.!%d字节\r\n",filename,cnt); }}//拍摄2: 写文件void photograph_write(u16 *buff){ u32 c32; //24位颜色值 UINT cnt; u8 res; int x; /*4. 读取图像参数进行填充*/ for(x=0;x<320;x++) { c32=RGB565ToRGB888(buff[x]); //将16位的颜色转为32位 res=f_write(&BMP_file,&c32,3,&cnt); //写入图片数据 if(res!=0) { printf("图像数据写入失败.%d\r\n",x); break; } }}//拍摄3: 关闭文件void photograph_close(void){ /*. 关闭文件*/ f_close(&BMP_file);}
-
一、前言在当今物联网(IoT)技术快速发展的背景下,嵌入式系统在各种应用场景中扮演着越来越重要的角色。随着物联网设备数量的不断增长,数据传输成为了关键的一环,尤其是在资源受限的环境中,如何高效地传输数据变得尤为重要。本文将介绍一个基于STM32F103ZET6微控制器的图像采集系统,该系统利用OV7725摄像头采集RGB565格式的图像,并通过MQTT协议将图像数据上传至阿里云物联网平台。 但是原始RGB565图像数据量巨大,直接传输会带来较高的网络负载,需要设计有效的压缩算法来减小数据体积。考虑到单片机资源有限,无法使用第三方优秀的开源库,当前就基于Run-Length Encoding (RLE)无损压缩算法来减小图像数据的传输负担。RLE算法是一种简单有效的压缩方法,尤其适用于图像中存在大量连续重复像素值的情况。通过对重复像素的值和重复次数进行记录,可以显著减小图像数据的大小,从而降低网络传输的负载。下面会详细介绍RLE算法的设计与实现过程,以及如何在STM32F103ZET6微控制器上实现图像数据的压缩与解压,最终实现在阿里云物联网平台上进行高效的数据传输,并通过APP上位机进行图像数据的拉取与显示。当前文章最重要的知识点是介绍: 数据的压缩和解压。二、算法选型在单片机上进行图像数据压缩,需要选择是使用轻量级、简单但有效的压缩算法。以下2个是可以用的压缩算法,适合在单片机上实现并能够有效减小数据量。2.1 Run-Length Encoding (RLE)RLE 是一种非常简单的无损压缩算法,特别适用于图像中有大量连续重复像素值的情况。通过记录重复像素的值和重复次数来实现压缩。下面是算法的核心实现: 解压和压缩#include <stdint.h>#include <stdlib.h>#include <stdio.h>// RLE 压缩函数size_t RLE_compress(const uint16_t *input, size_t length, uint8_t *output) { size_t out_index = 0; for (size_t i = 0; i < length; ) { uint16_t value = input[i]; size_t run_length = 1; while (i + run_length < length && input[i + run_length] == value && run_length < 255) { run_length++; } output[out_index++] = (uint8_t)(run_length); output[out_index++] = (uint8_t)(value >> 8); output[out_index++] = (uint8_t)(value & 0xFF); i += run_length; } return out_index;}// RLE 解压函数size_t RLE_decompress(const uint8_t *input, size_t length, uint16_t *output) { size_t out_index = 0; for (size_t i = 0; i < length; i += 3) { uint8_t run_length = input[i]; uint16_t value = (input[i + 1] << 8) | input[i + 2]; for (size_t j = 0; j < run_length; j++) { output[out_index++] = value; } } return out_index;}2.2 Differential Pulse-Code Modulation (DPCM)DPCM 适用于图像中连续像素值变化较小的情况。通过记录相邻像素值的差值来实现压缩。下面是算法的核心实现: 解压和压缩#include <stdint.h>#include <stdlib.h>#include <stdio.h>// DPCM 压缩函数size_t DPCM_compress(const uint16_t *input, size_t length, int16_t *output) { output[0] = input[0]; for (size_t i = 1; i < length; i++) { output[i] = input[i] - input[i - 1]; } return length;}// DPCM 解压函数size_t DPCM_decompress(const int16_t *input, size_t length, uint16_t *output) { output[0] = input[0]; for (size_t i = 1; i < length; i++) { output[i] = output[i - 1] + input[i]; } return length;}三、采用RLE算法实现图像压缩下面这段代码采用Run-Length Encoding (RLE) 算法对 RGB565 格式图像数据进行压缩和解压。代码里定义了一个常量数组 gImage_rgb565_100x100,存储 100x100 图像的 RGB565 像素数据(通过实际摄像头采集得到的)。RLE 压缩函数 RLE_compress 遍历输入的 RGB565 数据数组,查找连续相同的像素值,并记录它们的长度和值,将这些信息存储到输出的压缩数组中。每次遇到一段连续的相同值时,它会记录该段的长度(最大为 255)和该像素值的高位和低位字节。压缩后的数据长度返回给调用者。解压函数 RLE_decompress 读取压缩后的数组,根据记录的长度和值还原原始数据,将这些值存储到输出的解压数组中。main 函数中,计算图像数据的长度,然后分配内存用于存储压缩和解压后的数据,分别调用压缩和解压函数进行处理。最后,打印出原始数据大小、压缩后的数据大小以及压缩率,以验证压缩效果。所有动态分配的内存最终被释放,以避免内存泄漏。通过这样的实现,可以在不使用第三方库的情况下,有效地减少图像数据的传输负荷,提高传输效率。实现代码:#include <stdint.h>#include <stdio.h>#include <stdlib.h>// RLE 压缩函数size_t RLE_compress(const uint16_t* input, size_t length, uint8_t* output) { size_t out_index = 0; for (size_t i = 0; i < length; ) { uint16_t value = input[i]; size_t run_length = 1; while (i + run_length < length && input[i + run_length] == value && run_length < 255) { run_length++; } output[out_index++] = (uint8_t)(run_length); output[out_index++] = (uint8_t)(value >> 8); output[out_index++] = (uint8_t)(value & 0xFF); i += run_length; } return out_index;}// RLE 解压函数size_t RLE_decompress(const uint8_t* input, size_t length, uint16_t* output) { size_t out_index = 0; for (size_t i = 0; i < length; i += 3) { uint8_t run_length = input[i]; uint16_t value = (input[i + 1] << 8) | input[i + 2]; for (size_t j = 0; j < run_length; j++) { output[out_index++] = value; } } return out_index;}int main() { size_t length = sizeof(gImage_rgb565_100x100) / 2; uint16_t* rgb565_array = (uint16_t*)gImage_rgb565_100x100; // 分配 RLE 压缩后的数组 uint8_t* rle_compressed = (uint8_t*)malloc(length * 3 * sizeof(uint8_t)); if (rle_compressed == NULL) { perror("Unable to allocate memory for RLE compressed array"); return 1; } // RLE 压缩 size_t rle_compressed_length = RLE_compress(rgb565_array, length, rle_compressed); // 分配 RLE 解压后的数组 uint16_t* rle_decompressed = (uint16_t*)malloc(length * sizeof(uint16_t)); if (rle_decompressed == NULL) { perror("Unable to allocate memory for RLE decompressed array"); return 1; } // RLE 解压 size_t rle_decompressed_length = RLE_decompress(rle_compressed, rle_compressed_length, rle_decompressed); // 打印 RLE 压缩率 printf("RLE 压缩算法:\n"); printf("原始大小: %zu bytes\n", length * 2); printf("压缩后大小: %zu bytes\n", rle_compressed_length); printf("压缩比: %.2f%%\n", (double)rle_compressed_length / (length * 2) * 100); // 释放内存 free(rle_compressed); free(rle_decompressed); return 0;}运行效果如下:四、哈夫曼编码实现压缩和解压哈夫曼编码是一种常用的无损压缩算法,通过构建哈夫曼树,根据字符频率生成最优编码,从而实现数据压缩。4.1 哈夫曼编码压缩自定义数据与还原下面代码使用哈夫曼编码对一段自定义数据进行压缩和解压,并打印压缩前后的大小和压缩率,验证压缩效果。#include <stdio.h>#include <stdlib.h>#include <string.h>// 定义树节点结构typedef struct Node { unsigned char ch; unsigned int freq; struct Node* left, * right;} Node;// 优先队列typedef struct { Node** nodes; int size; int capacity;} PriorityQueue;PriorityQueue* createPriorityQueue(int capacity) { PriorityQueue* pq = (PriorityQueue*)malloc(sizeof(PriorityQueue)); pq->nodes = (Node**)malloc(capacity * sizeof(Node*)); pq->size = 0; pq->capacity = capacity; return pq;}void swapNodes(Node** a, Node** b) { Node* t = *a; *a = *b; *b = t;}void heapify(PriorityQueue* pq, int idx) { int smallest = idx; int left = 2 * idx + 1; int right = 2 * idx + 2; if (left < pq->size && pq->nodes[left]->freq < pq->nodes[smallest]->freq) { smallest = left; } if (right < pq->size && pq->nodes[right]->freq < pq->nodes[smallest]->freq) { smallest = right; } if (smallest != idx) { swapNodes(&pq->nodes[smallest], &pq->nodes[idx]); heapify(pq, smallest); }}Node* extractMin(PriorityQueue* pq) { Node* temp = pq->nodes[0]; pq->nodes[0] = pq->nodes[pq->size - 1]; pq->size--; heapify(pq, 0); return temp;}void insertPriorityQueue(PriorityQueue* pq, Node* node) { pq->size++; int i = pq->size - 1; while (i && node->freq < pq->nodes[(i - 1) / 2]->freq) { pq->nodes[i] = pq->nodes[(i - 1) / 2]; i = (i - 1) / 2; } pq->nodes[i] = node;}int isLeaf(Node* node) { return !(node->left) && !(node->right);}PriorityQueue* buildPriorityQueue(unsigned char data[], int freq[], int size) { PriorityQueue* pq = createPriorityQueue(size); for (int i = 0; i < size; ++i) { if (freq[data[i]] > 0) { Node* node = (Node*)malloc(sizeof(Node)); node->ch = data[i]; node->freq = freq[data[i]]; node->left = node->right = NULL; pq->nodes[pq->size++] = node; } } for (int i = (pq->size - 1) / 2; i >= 0; --i) { heapify(pq, i); } return pq;}Node* buildHuffmanTree(unsigned char data[], int freq[], int size) { Node* left, * right, * top; PriorityQueue* pq = buildPriorityQueue(data, freq, size); while (pq->size != 1) { left = extractMin(pq); right = extractMin(pq); top = (Node*)malloc(sizeof(Node)); top->ch = '\0'; top->freq = left->freq + right->freq; top->left = left; top->right = right; insertPriorityQueue(pq, top); } return extractMin(pq);}void printCodes(Node* root, int arr[], int top, char** huffmanCodes) { if (root->left) { arr[top] = 0; printCodes(root->left, arr, top + 1, huffmanCodes); } if (root->right) { arr[top] = 1; printCodes(root->right, arr, top + 1, huffmanCodes); } if (isLeaf(root)) { huffmanCodes[root->ch] = (char*)malloc(top + 1); for (int i = 0; i < top; ++i) { huffmanCodes[root->ch][i] = '0' + arr[i]; } huffmanCodes[root->ch][top] = '\0'; }}char** buildHuffmanCodes(unsigned char data[], int freq[], int size) { Node* root = buildHuffmanTree(data, freq, size); int arr[100], top = 0; char** huffmanCodes = (char**)malloc(256 * sizeof(char*)); for (int i = 0; i < 256; ++i) { huffmanCodes[i] = NULL; } printCodes(root, arr, top, huffmanCodes); return huffmanCodes;}void compress(unsigned char data[], int dataSize, char** huffmanCodes, unsigned char** compressedData, int* compressedSize) { int bitCount = 0; for (int i = 0; i < dataSize; ++i) { bitCount += strlen(huffmanCodes[data[i]]); } *compressedSize = (bitCount + 7) / 8; *compressedData = (unsigned char*)malloc(*compressedSize); memset(*compressedData, 0, *compressedSize); int byteIndex = 0, bitIndex = 0; for (int i = 0; i < dataSize; ++i) { char* code = huffmanCodes[data[i]]; for (int j = 0; code[j] != '\0'; ++j) { if (code[j] == '1') { (*compressedData)[byteIndex] |= (1 << (7 - bitIndex)); } bitIndex++; if (bitIndex == 8) { bitIndex = 0; byteIndex++; } } }}void decompress(unsigned char* compressedData, int compressedSize, Node* root, unsigned char** decompressedData, int dataSize) { *decompressedData = (unsigned char*)malloc(dataSize + 1); Node* current = root; int byteIndex = 0, bitIndex = 0; for (int i = 0; i < dataSize; ++i) { while (!isLeaf(current)) { if (compressedData[byteIndex] & (1 << (7 - bitIndex))) { current = current->right; } else { current = current->left; } bitIndex++; if (bitIndex == 8) { bitIndex = 0; byteIndex++; } } (*decompressedData)[i] = current->ch; current = root; } (*decompressedData)[dataSize] = '\0';}int main() { // 自定义数据 unsigned char data[] = "我是DS小龙哥 我正在测试这一段数据,看看能不能压缩成功"; int dataSize = strlen((char*)data); // 计算频率 int freq[256] = { 0 }; for (int i = 0; i < dataSize; ++i) { freq[data[i]]++; } // 构建哈夫曼编码 char** huffmanCodes = buildHuffmanCodes(data, freq, 256); // 压缩 unsigned char* compressedData; int compressedSize; compress(data, dataSize, huffmanCodes, &compressedData, &compressedSize); // 解压 unsigned char* decompressedData; Node* root = buildHuffmanTree(data, freq, 256); decompress(compressedData, compressedSize, root, &decompressedData, dataSize); // 打印压缩前后的大小和压缩率 printf("Original size: %d bytes\n", dataSize); printf("Compressed size: %d bytes\n", compressedSize); printf("Compression ratio: %.2f%%\n", (double)compressedSize / dataSize * 100); // 验证解压结果 printf("Decompressed data: %s\n", decompressedData); // 释放内存 free(compressedData); free(decompressedData); for (int i = 0; i < 256; ++i) { if (huffmanCodes[i]) { free(huffmanCodes[i]); } } free(huffmanCodes); return 0;}测试效果:4.2 哈夫曼编码压缩完成图像的压缩和还原#include <stdio.h>#include <stdlib.h>#include <string.h>// 定义树节点结构typedef struct Node { unsigned short ch; // 使用 unsigned short 表示 RGB565 数据 unsigned int freq; struct Node* left, * right;} Node;// 优先队列typedef struct { Node** nodes; int size; int capacity;} PriorityQueue;PriorityQueue* createPriorityQueue(int capacity) { PriorityQueue* pq = (PriorityQueue*)malloc(sizeof(PriorityQueue)); pq->nodes = (Node**)malloc(capacity * sizeof(Node*)); pq->size = 0; pq->capacity = capacity; return pq;}void swapNodes(Node** a, Node** b) { Node* t = *a; *a = *b; *b = t;}void heapify(PriorityQueue* pq, int idx) { int smallest = idx; int left = 2 * idx + 1; int right = 2 * idx + 2; if (left < pq->size && pq->nodes[left]->freq < pq->nodes[smallest]->freq) { smallest = left; } if (right < pq->size && pq->nodes[right]->freq < pq->nodes[smallest]->freq) { smallest = right; } if (smallest != idx) { swapNodes(&pq->nodes[smallest], &pq->nodes[idx]); heapify(pq, smallest); }}Node* extractMin(PriorityQueue* pq) { Node* temp = pq->nodes[0]; pq->nodes[0] = pq->nodes[pq->size - 1]; pq->size--; heapify(pq, 0); return temp;}void insertPriorityQueue(PriorityQueue* pq, Node* node) { pq->size++; int i = pq->size - 1; while (i && node->freq < pq->nodes[(i - 1) / 2]->freq) { pq->nodes[i] = pq->nodes[(i - 1) / 2]; i = (i - 1) / 2; } pq->nodes[i] = node;}int isLeaf(Node* node) { return !(node->left) && !(node->right);}PriorityQueue* buildPriorityQueue(unsigned short data[], int freq[], int size) { PriorityQueue* pq = createPriorityQueue(size); for (int i = 0; i < size; ++i) { if (freq[data[i]] > 0) { Node* node = (Node*)malloc(sizeof(Node)); node->ch = data[i]; node->freq = freq[data[i]]; node->left = node->right = NULL; pq->nodes[pq->size++] = node; } } for (int i = (pq->size - 1) / 2; i >= 0; --i) { heapify(pq, i); } return pq;}Node* buildHuffmanTree(unsigned short data[], int freq[], int size) { Node* left, * right, * top; PriorityQueue* pq = buildPriorityQueue(data, freq, size); while (pq->size != 1) { left = extractMin(pq); right = extractMin(pq); top = (Node*)malloc(sizeof(Node)); top->ch = 0; top->freq = left->freq + right->freq; top->left = left; top->right = right; insertPriorityQueue(pq, top); } return extractMin(pq);}void printCodes(Node* root, int arr[], int top, char** huffmanCodes) { if (root->left) { arr[top] = 0; printCodes(root->left, arr, top + 1, huffmanCodes); } if (root->right) { arr[top] = 1; printCodes(root->right, arr, top + 1, huffmanCodes); } if (isLeaf(root)) { huffmanCodes[root->ch] = (char*)malloc(top + 1); for (int i = 0; i < top; ++i) { huffmanCodes[root->ch][i] = '0' + arr[i]; } huffmanCodes[root->ch][top] = '\0'; }}char** buildHuffmanCodes(unsigned short data[], int freq[], int size) { Node* root = buildHuffmanTree(data, freq, size); int arr[100], top = 0; char** huffmanCodes = (char**)malloc(65536 * sizeof(char*)); // 65536 表示所有可能的 RGB565 值 for (int i = 0; i < 65536; ++i) { huffmanCodes[i] = NULL; } printCodes(root, arr, top, huffmanCodes); return huffmanCodes;}void compress(unsigned short data[], int dataSize, char** huffmanCodes, unsigned char** compressedData, int* compressedSize) { int bitCount = 0; for (int i = 0; i < dataSize; ++i) { bitCount += strlen(huffmanCodes[data[i]]); } *compressedSize = (bitCount + 7) / 8; *compressedData = (unsigned char*)malloc(*compressedSize); memset(*compressedData, 0, *compressedSize); int byteIndex = 0, bitIndex = 0; for (int i = 0; i < dataSize; ++i) { char* code = huffmanCodes[data[i]]; for (int j = 0; code[j] != '\0'; ++j) { if (code[j] == '1') { (*compressedData)[byteIndex] |= (1 << (7 - bitIndex)); } bitIndex++; if (bitIndex == 8) { bitIndex = 0; byteIndex++; } } }}void decompress(unsigned char* compressedData, int compressedSize, Node* root, unsigned short** decompressedData, int dataSize) { *decompressedData = (unsigned short*)malloc(dataSize * sizeof(unsigned short)); Node* current = root; int byteIndex = 0, bitIndex = 0; for (int i = 0; i < dataSize; ++i) { while (!isLeaf(current)) { if (compressedData[byteIndex] & (1 << (7 - bitIndex))) { current = current->right; } else { current = current->left; } bitIndex++; if (bitIndex == 8) { bitIndex = 0; byteIndex++; } } (*decompressedData)[i] = current->ch; current = root; }}int main() { int dataSize = sizeof(gImage_rgb565_100x100) / 2; unsigned short* data = (unsigned short*)gImage_rgb565_100x100; // 计算频率 int freq[65536] = { 0 }; for (int i = 0; i < dataSize; ++i) { freq[data[i]]++; } // 构建哈夫曼编码 char** huffmanCodes = buildHuffmanCodes(data, freq, 65536); // 压缩 unsigned char* compressedData; int compressedSize; compress(data, dataSize, huffmanCodes, &compressedData, &compressedSize); // 解压 unsigned short* decompressedData; Node* root = buildHuffmanTree(data, freq, 65536); decompress(compressedData, compressedSize, root, &decompressedData, dataSize); // 打印压缩前后的大小和压缩率 printf("Original size: %d bytes\n", dataSize * 2); printf("Compressed size: %d bytes\n", compressedSize); printf("Compression ratio: %.2f%%\n", ((double)compressedSize / (dataSize * 2)) * 100); // 验证解压结果(仅在开发时使用) for (int i = 0; i < dataSize; ++i) { if (data[i] != decompressedData[i]) { printf("Error: Decompressed data does not match original data at index %d.\n", i); break; } } // 释放内存 free(compressedData); free(decompressedData); for (int i = 0; i < 65536; ++i) { if (huffmanCodes[i]) { free(huffmanCodes[i]); } } free(huffmanCodes); return 0;}
-
一、前言霍夫曼编码(Huffman Coding)是一种广泛使用的数据压缩算法,特别适用于无损数据压缩。它是由David A. Huffman在1952年提出的,并且通常用于文件压缩和传输中减少数据量。霍夫曼编码的核心思想是使用变长编码表对源数据进行编码,其中较频繁出现的数据项会被赋予较短的编码,而较少出现的数据项则会被赋予较长的编码。该编码方法通过构建一种特殊的二叉树——霍夫曼树,为数据中的各个符号分配长度可变的前缀码,使得高频出现的符号具有较短的编码,而低频出现的符号则具有较长的编码。这种特性使得霍夫曼编码非常适合于压缩具有不均匀符号频率分布的数据集,能够有效地减小数据的存储空间或传输所需的带宽。霍夫曼编码的工作原理如下:首先统计输入数据中每个符号出现的频率;然后基于这些频率值构造一个最小堆,其中堆中的每个元素都是一个只包含一个符号及其频率的树节点;接着反复从堆中取出频率最小的两个节点,合并成一个新的内部节点,该节点的频率为两个子节点频率之和,并将这个新节点放回堆中;重复这一过程直到堆中只剩下一个节点,这个节点即为霍夫曼树的根节点;最后,从根节点出发遍历整棵树,定义从根到任一叶节点的路径上,向左走标记为0,向右走标记为1,这样每个叶节点就对应了一个唯一的二进制编码,这就是霍夫曼编码。霍夫曼编码的一个关键特征是其编码具有前缀性质,即没有任何一个符号的编码是另一个符号编码的前缀,这保证了在解码过程中可以唯一确定每一个编码所代表的符号,从而确保了压缩和解压过程的一致性。此外,霍夫曼编码是熵编码的一种形式,它利用了数据的统计特性来进行压缩,理论上可以达到接近于信息熵的压缩效率,即在理想情况下,压缩后的数据量等于数据的信息熵。霍夫曼编码在文件压缩软件中,如WinZip、7-Zip,使用霍夫曼编码来压缩文件;在网络通信中,为了提高传输效率,会采用霍夫曼编码来压缩数据流;在图像处理和视频编码中,霍夫曼编码经常与其他编码技术结合使用,比如JPEG图像压缩标准就使用了霍夫曼编码来进一步压缩量化后的离散余弦变换系数。下面是霍夫曼编码的基本步骤:统计字符频率:首先需要统计输入数据中每个字符出现的次数。创建霍夫曼树(也称为最优二叉树) :创建一个叶子节点集合,每个叶子节点包含一个字符和它的频率。反复执行以下操作,直到所有节点合并成一棵树:从集合中选出两个频率最低的节点作为新节点的左右子节点。新节点的频率是其两个子节点频率之和。将这个新节点加入集合中。最终剩下的那棵树就是霍夫曼树。生成编码规则:从霍夫曼树的根节点出发,向左走标记为0,向右走标记为1。每个叶子节点对应的路径上的数字序列即为其霍夫曼编码。编码数据:使用生成的霍夫曼编码表对原始数据进行编码。解码数据:解码时只需按照霍夫曼树从根节点开始,根据0或1沿着树向下移动即可还原出原始数据。霍夫曼编码的一个重要特点是它是前缀编码,这意味着没有一个编码是另一个编码的前缀。这样可以确保编码后的数据可以唯一地解码回原始数据。举个简单的例子来说明霍夫曼编码的过程:假设我们有这样一个字符串:“ABACDAACAC”,其中字符A出现了5次,B出现了1次,C出现了4次,D出现了1次。统计字符频率:A: 5B: 1C: 4D: 1创建霍夫曼树:构建初始节点集合:{A(5), B(1), C(4), D(1)}合并B(1)和D(1),得到一个新节点BD(2)合并C(4)和BD(2),得到一个新节点CBDB(6)最后合并A(5)和CBDB(6),得到霍夫曼树的根节点。生成编码规则:A: 0B: 110C: 10D: 111编码数据:“ABACDAACAC”编码后变为:“0110100011101000100”二、算法设计(C语言)2.1 霍夫曼编码算法实现下面代码里实现了创建霍夫曼树、生成霍夫曼编码、对输入文本进行编码和解码的功能。编译运行这段代码可以得到每个字符的霍夫曼编码。#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_TREE_HT 100 // 霍夫曼树节点 struct MinHeapNode { char data; unsigned freq; struct MinHeapNode* left, * right; }; // 最小堆 struct MinHeap { unsigned size; unsigned capacity; struct MinHeapNode** array; }; // 创建新节点 struct MinHeapNode* newNode(char data, unsigned freq) { struct MinHeapNode* temp = (struct MinHeapNode*)malloc(sizeof(struct MinHeapNode)); temp->left = temp->right = NULL; temp->data = data; temp->freq = freq; return temp; } // 创建最小堆 struct MinHeap* createMinHeap(unsigned capacity) { struct MinHeap* minHeap = (struct MinHeap*)malloc(sizeof(struct MinHeap)); minHeap->size = 0; minHeap->capacity = capacity; minHeap->array = (struct MinHeapNode**)malloc(minHeap->capacity * sizeof(struct MinHeapNode*)); return minHeap; } // 交换两个最小堆节点 void swapMinHeapNode(struct MinHeapNode** a, struct MinHeapNode** b) { struct MinHeapNode* t = *a; *a = *b; *b = t; } // 堆化 void minHeapify(struct MinHeap* minHeap, int idx) { int smallest = idx; int left = 2 * idx + 1; int right = 2 * idx + 2; if (left < minHeap->size && minHeap->array[left]->freq < minHeap->array[smallest]->freq) smallest = left; if (right < minHeap->size && minHeap->array[right]->freq < minHeap->array[smallest]->freq) smallest = right; if (smallest != idx) { swapMinHeapNode(&minHeap->array[smallest], &minHeap->array[idx]); minHeapify(minHeap, smallest); } } // 检查大小是否为1 int isSizeOne(struct MinHeap* minHeap) { return (minHeap->size == 1); } // 提取最小值节点 struct MinHeapNode* extractMin(struct MinHeap* minHeap) { struct MinHeapNode* temp = minHeap->array[0]; minHeap->array[0] = minHeap->array[minHeap->size - 1]; --minHeap->size; minHeapify(minHeap, 0); return temp; } // 插入最小堆 void insertMinHeap(struct MinHeap* minHeap, struct MinHeapNode* minHeapNode) { ++minHeap->size; int i = minHeap->size - 1; while (i && minHeapNode->freq < minHeap->array[(i - 1) / 2]->freq) { minHeap->array[i] = minHeap->array[(i - 1) / 2]; i = (i - 1) / 2; } minHeap->array[i] = minHeapNode; } // 构建最小堆 void buildMinHeap(struct MinHeap* minHeap) { int n = minHeap->size - 1; int i; for (i = (n - 1) / 2; i >= 0; --i) minHeapify(minHeap, i); } // 检查是否是叶子节点 int isLeaf(struct MinHeapNode* root) { return !(root->left) && !(root->right); } // 创建和构建最小堆 struct MinHeap* createAndBuildMinHeap(char data[], int freq[], int size) { struct MinHeap* minHeap = createMinHeap(size); for (int i = 0; i < size; ++i) minHeap->array[i] = newNode(data[i], freq[i]); minHeap->size = size; buildMinHeap(minHeap); return minHeap; } // 构建霍夫曼树 struct MinHeapNode* buildHuffmanTree(char data[], int freq[], int size) { struct MinHeapNode* left, * right, * top; struct MinHeap* minHeap = createAndBuildMinHeap(data, freq, size); while (!isSizeOne(minHeap)) { left = extractMin(minHeap); right = extractMin(minHeap); top = newNode('$', left->freq + right->freq); top->left = left; top->right = right; insertMinHeap(minHeap, top); } return extractMin(minHeap); } // 打印编码 void printCodes(struct MinHeapNode* root, int arr[], int top) { if (root->left) { arr[top] = 0; printCodes(root->left, arr, top + 1); } if (root->right) { arr[top] = 1; printCodes(root->right, arr, top + 1); } if (isLeaf(root)) { printf("%c: ", root->data); for (int i = 0; i < top; ++i) printf("%d", arr[i]); printf("\n"); } } // 霍夫曼编码主函数 void HuffmanCodes(char data[], int freq[], int size) { struct MinHeapNode* root = buildHuffmanTree(data, freq, size); int arr[MAX_TREE_HT], top = 0; printCodes(root, arr, top); } // 主函数 int main() { char arr[] = { 'a', 'b', 'c', 'd', 'e', 'f' }; int freq[] = { 5, 9, 12, 13, 16, 45 }; int size = sizeof(arr) / sizeof(arr[0]); HuffmanCodes(arr, freq, size); return 0; }复制2.2 数据的编码与还原以下是使用霍夫曼编码算法对一段数据进行压缩和还原。代码包括生成霍夫曼树、生成编码表、对数据进行压缩和还原的功能。#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_TREE_HT 100 #define MAX_CHAR 256 // 霍夫曼树节点 struct MinHeapNode { unsigned char data; unsigned freq; struct MinHeapNode* left, * right; }; // 最小堆 struct MinHeap { unsigned size; unsigned capacity; struct MinHeapNode** array; }; // 创建新节点 struct MinHeapNode* newNode(unsigned char data, unsigned freq) { struct MinHeapNode* temp = (struct MinHeapNode*)malloc(sizeof(struct MinHeapNode)); temp->left = temp->right = NULL; temp->data = data; temp->freq = freq; return temp; } // 创建最小堆 struct MinHeap* createMinHeap(unsigned capacity) { struct MinHeap* minHeap = (struct MinHeap*)malloc(sizeof(struct MinHeap)); minHeap->size = 0; minHeap->capacity = capacity; minHeap->array = (struct MinHeapNode**)malloc(minHeap->capacity * sizeof(struct MinHeapNode*)); return minHeap; } // 交换两个最小堆节点 void swapMinHeapNode(struct MinHeapNode** a, struct MinHeapNode** b) { struct MinHeapNode* t = *a; *a = *b; *b = t; } // 堆化 void minHeapify(struct MinHeap* minHeap, int idx) { int smallest = idx; int left = 2 * idx + 1; int right = 2 * idx + 2; if (left < minHeap->size && minHeap->array[left]->freq < minHeap->array[smallest]->freq) smallest = left; if (right < minHeap->size && minHeap->array[right]->freq < minHeap->array[smallest]->freq) smallest = right; if (smallest != idx) { swapMinHeapNode(&minHeap->array[smallest], &minHeap->array[idx]); minHeapify(minHeap, smallest); } } // 检查大小是否为1 int isSizeOne(struct MinHeap* minHeap) { return (minHeap->size == 1); } // 提取最小值节点 struct MinHeapNode* extractMin(struct MinHeap* minHeap) { struct MinHeapNode* temp = minHeap->array[0]; minHeap->array[0] = minHeap->array[minHeap->size - 1]; --minHeap->size; minHeapify(minHeap, 0); return temp; } // 插入最小堆 void insertMinHeap(struct MinHeap* minHeap, struct MinHeapNode* minHeapNode) { ++minHeap->size; int i = minHeap->size - 1; while (i && minHeapNode->freq < minHeap->array[(i - 1) / 2]->freq) { minHeap->array[i] = minHeap->array[(i - 1) / 2]; i = (i - 1) / 2; } minHeap->array[i] = minHeapNode; } // 构建最小堆 void buildMinHeap(struct MinHeap* minHeap) { int n = minHeap->size - 1; int i; for (i = (n - 1) / 2; i >= 0; --i) minHeapify(minHeap, i); } // 检查是否是叶子节点 int isLeaf(struct MinHeapNode* root) { return !(root->left) && !(root->right); } // 创建和构建最小堆 struct MinHeap* createAndBuildMinHeap(unsigned char data[], int freq[], int size) { struct MinHeap* minHeap = createMinHeap(size); for (int i = 0; i < size; ++i) minHeap->array[i] = newNode(data[i], freq[i]); minHeap->size = size; buildMinHeap(minHeap); return minHeap; } // 构建霍夫曼树 struct MinHeapNode* buildHuffmanTree(unsigned char data[], int freq[], int size) { struct MinHeapNode* left, * right, * top; struct MinHeap* minHeap = createAndBuildMinHeap(data, freq, size); while (!isSizeOne(minHeap)) { left = extractMin(minHeap); right = extractMin(minHeap); top = newNode('$', left->freq + right->freq); top->left = left; top->right = right; insertMinHeap(minHeap, top); } return extractMin(minHeap); } // 生成编码表 void generateCodes(struct MinHeapNode* root, int arr[], int top, char* codes[]) { if (root->left) { arr[top] = 0; generateCodes(root->left, arr, top + 1, codes); } if (root->right) { arr[top] = 1; generateCodes(root->right, arr, top + 1, codes); } if (isLeaf(root)) { codes[root->data] = (char*)malloc(top + 1); for (int i = 0; i < top; ++i) { codes[root->data][i] = arr[i] + '0'; } codes[root->data][top] = '\0'; } } // 压缩数据 void compressData(const char* data, char* codes[], char* compressed) { while (*data) { strcat(compressed, codes[(unsigned char)*data]); data++; } } // 解码霍夫曼树 void decodeHuffmanTree(struct MinHeapNode* root, char* encoded, char* decoded) { struct MinHeapNode* current = root; while (*encoded) { if (*encoded == '0') { current = current->left; } else { current = current->right; } if (isLeaf(current)) { *decoded++ = current->data; current = root; } encoded++; } *decoded = '\0'; } // 打印编码表 void printCodes(char* codes[]) { for (int i = 0; i < MAX_CHAR; i++) { if (codes[i]) { printf("%c: %s\n", i, codes[i]); } } } // 主函数 int main() { const char* data = "我是DS小龙哥-这是一段测试的数据,如果你可以正确的看到我,说明解码已经成功了"; int freq[MAX_CHAR] = { 0 }; for (int i = 0; data[i]; i++) { freq[(unsigned char)data[i]]++; } unsigned char uniqueChars[MAX_CHAR]; int uniqueFreqs[MAX_CHAR]; int size = 0; for (int i = 0; i < MAX_CHAR; i++) { if (freq[i]) { uniqueChars[size] = i; uniqueFreqs[size] = freq[i]; size++; } } struct MinHeapNode* root = buildHuffmanTree(uniqueChars, uniqueFreqs, size); char* codes[MAX_CHAR] = { 0 }; int arr[MAX_TREE_HT], top = 0; generateCodes(root, arr, top, codes); printf("Huffman Codes:\n"); printCodes(codes); char compressed[1024] = { 0 }; compressData(data, codes, compressed); printf("\nCompressed Data: %s\n", compressed); char decoded[1024] = { 0 }; decodeHuffmanTree(root, compressed, decoded); printf("\nDecoded Data: %s\n", decoded); for (int i = 0; i < MAX_CHAR; i++) { if (codes[i]) { free(codes[i]); } } return 0; }复制这段代码实现了霍夫曼编码算法的压缩和解码功能。霍夫曼编码是一种无损数据压缩算法,利用字符在数据中出现的频率构建霍夫曼树,从而生成字符的二进制编码。代码先定义了霍夫曼树节点和最小堆的结构,包含创建新节点、交换节点、堆化节点、提取最小值节点、插入最小堆和构建最小堆的功能。然后,通过辅助函数isLeaf检查节点是否为叶子节点。通过buildHuffmanTree函数构建霍夫曼树,将字符和其对应的频率插入最小堆,反复提取两个最小频率节点,并将它们合并成一个新节点,再插回最小堆,直到堆中只剩一个节点,即为霍夫曼树的根节点。通过generateCodes函数递归遍历霍夫曼树,生成每个字符的霍夫曼编码,并存储在编码表中。compressData函数使用生成的编码表将输入数据压缩为霍夫曼编码。decodeHuffmanTree函数通过遍历霍夫曼树解码压缩后的数据,恢复原始数据。printCodes辅助函数用于打印生成的霍夫曼编码表。在主函数中,统计输入数据中每个字符的频率,然后构建霍夫曼树,生成编码表,对输入数据进行压缩,最后再对压缩后的数据进行解码,并打印结果。代码在实现霍夫曼编码和解码过程中,展示了从频率统计、树的构建、编码生成到数据压缩和解码的完整流程。
-
前言当前文章介绍如何在Linux下使用FFmpeg转码其他视频格式到AVS格式的指南,包括编译FFmpeg以支持XAVS编码和如何使用FFmpeg进行转码。AVS (Audio Video Coding Standard) 格式是一种由中国主导制定的视频编码标准,全称为“中国数字音视频编解码技术标准”(China Digital Audio Video Coding Standard),主要应用于高清电视广播、数字电视、网络视频传输等领域。AVS 标准提高视频压缩效率,降低计算复杂度,并减少专利费用,为中国及其他国家提供一种自主可控的视频编码技术。AVS 视频编码标准由多个子标准组成,其中 AVS1-P2(也称为 AVS+)是最广泛使用的版本之一,被设计用于高清晰度电视广播服务,并被中国数字地面电视广播标准 DTMB 采纳为推荐的视频编码格式。AVS1-P2 提供了与 H.264/AVC 相当的压缩效率,同时减少了计算复杂度,使得其在硬件实现上更为经济高效。技术特点高效压缩:AVS 标准采用了多种先进的压缩技术,如帧内预测、帧间预测、运动补偿、熵编码等,以实现高效的视频数据压缩。低计算复杂度:与 H.264/AVC 相比,AVS 设计上更加注重计算效率,减少了复杂的运算过程,从而降低了硬件实现的成本。自主知识产权:AVS 作为一种中国主导的标准,避免了因专利费而产生的高额成本,对于国内厂商来说具有一定的成本优势。支持多种应用场景:AVS 支持多种视频分辨率和帧率,适用于从标清到高清甚至超高清的各种视频应用场合。一个avs2编码的视频信息如下:[davs2 info]: Manager 26c5cacfc00: Sequence Resolution: 3840x2160.[davs2 info]: Dec[ 0] 26c5cad0780: COI of the first frame is 25.[davs2 info]: davs2: 1.6.205 5313a0a9f7e63110.10, 2022-02-20 13:39:38[davs2 info]: CPU Capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX AVX2 FMA3 LZCNT BMI2[davs2 info]: Manager 26c5cacfc00: using 1 thread(s): 1(frame/AEC)+0(pool/REC), 2 tasks "streams": [ { "index": 0, "codec_name": "avs2", "codec_long_name": "AVS2-P2/IEEE1857.4", "codec_type": "video", "codec_tag_string": "[0][0][0][0]", "codec_tag": "0x0000", "width": 3840, "height": 2160, "coded_width": 3840, "coded_height": 2160, "closed_captions": 0, "film_grain": 0, "has_b_frames": 0, "pix_fmt": "yuv420p10le", "level": -99, "refs": 1, "r_frame_rate": "50/1", "avg_frame_rate": "50/1", "time_base": "1/1000", "start_pts": 23, "start_time": "0.023000", "extradata_size": 45, "disposition": { "default": 0, "dub": 0, "original": 0, "comment": 0, "lyrics": 0, "karaoke": 0, "forced": 0, "hearing_impaired": 0, "visual_impaired": 0, "clean_effects": 0, "attached_pic": 0, "timed_thumbnails": 0, "captions": 0, "descriptions": 0, "metadata": 0, "dependent": 0, "still_image": 0 }, "tags": { "DURATION": "00:00:06.163000000" } }, { "index": 1, "codec_name": "ac3", "codec_long_name": "ATSC A/52A (AC-3)", "codec_type": "audio", "codec_tag_string": "[0][0][0][0]", "codec_tag": "0x0000", "sample_fmt": "fltp", "sample_rate": "48000", "channels": 6, "channel_layout": "5.1(side)", "bits_per_sample": 0, "r_frame_rate": "0/0", "avg_frame_rate": "0/0", "time_base": "1/1000", "start_pts": 24, "start_time": "0.024000", "bit_rate": "448000", "disposition": { "default": 0, "dub": 0, "original": 0, "comment": 0, "lyrics": 0, "karaoke": 0, "forced": 0, "hearing_impaired": 0, "visual_impaired": 0, "clean_effects": 0, "attached_pic": 0, "timed_thumbnails": 0, "captions": 0, "descriptions": 0, "metadata": 0, "dependent": 0, "still_image": 0 }, "tags": { "DURATION": "00:00:06.008000000" } } ]}一、如何编译FFmpeg以支持XAVS编码1. 下载FFmpeg源代码使用Subversion(SVN)从FFmpeg官方仓库下载FFmpeg源代码:svn checkout svn://svn.ffmpeg.org/ffmpeg/trunk ffmpeg2. 下载XAVS源代码从SourceForge下载XAVS源代码:svn co https://xavs.svn.sourceforge.net/svnroot/xavs/trunk xavs3. 编译XAVS代码安装XAVS并将其头文件(.h)和静态库文件(.a)安装到系统目录,例如 /usr/local:cd xavs./configure --enable-sharedmakemake install4. 配置FFmpeg在配置FFmpeg时添加以下选项以启用GPL许可证和XAVS支持:cd ../ffmpeg./configure --enable-gpl --enable-libxavs确保FFmpeg项目也安装到与XAVS库相同的目录,本例中为 /usr/local:makemake install5. 检查XAVS支持运行以下命令来验证FFmpeg是否成功构建了AVS视频编码器:./ffmpeg -formats | less如果看到类似 cavsvideo raw Chinese AVS video 的输出,则表示AVS视频编码器已成功集成。二、如何使用FFmpeg转码视频到AVS2.1 转码命令示例下面的命令展示了如何使用FFmpeg将视频转码成AVS格式:./ffmpeg -vframes 300 -cqp 29 -refs 2 -bf 2 -g 16 -s widthxheight -i "INPUT_FILE_NAME" -vcodec libxavs "OUTPUT_FILE_NAME"其中:-vframes: 设置要编码的帧数。-cqp: 设置常量量化参数。-refs: 设置参考帧的数量。-bf: 设置启用的B帧数量。-g: 设置GOP(图像组)大小,即I帧之间的距离。-s: 设置分辨率,格式为宽度x高度。-i: 指定输入文件名。-vcodec libxavs: 指定使用XAVS编码器。2.2 常用参数一些常用的FFmpeg参数及其含义:-b 或 -vb: 设置比特率或视频比特率。-s: 分辨率,格式为宽度x高度。-r: 帧速率。-refs: 参考帧的数量。-bf: 启用的B帧数量。-g: GOP大小,即I帧之间的距离。-cqp: 常量量化参数,应为大于等于0的整数。-me_method: 运动估计方法。-qmin/-qmax: 最小/最大量化步长。-vframes: 要编码的帧数。2.3 示例H.264转AVS(不含B帧):./ffmpeg -i test0.avi -g 60 -b 600k -vcodec libxavs output1.cavsH.264转AVS(含B帧):./ffmpeg -i test0.avi -b 600k -bf 2 -vcodec libxavs output2.cavs以上步骤和命令可以用来在Linux环境下使用FFmpeg将视频文件转码为AVS格式。
-
一、前言随着大数据时代的到来,交通数据量急剧增加,由此带来的交通安全问题日益凸显。传统的驾驶人信用管理系统在数据存储和管理上存在着诸多不足之处,例如中心化存储方案无法有效地进行信用存证及数据溯源。区块链技术以其去中心化和不可篡改的特性,在数据存储和管理方面展现出了巨大的潜力。区块链的固有特性也带来了另一个挑战——一旦数据被写入区块链,几乎不可能对其进行修改,这在某些情况下是不利的。为了解决这一问题,当前文章重点研究了如何构建具有高随机性的哈希算法——高度随机哈希函数(HRHF)。HRHF算法通过结合纠错码与SM3算法的Merkle-Damgård迭代结构,不仅增强了哈希值的随机性,还保证了算法的安全性和执行效率。实验结果显示,与经典的SHA-256算法相比,HRHF算法在多个关键指标上均有显著提升。HRHF算法结合了纠错码与SM3算法的Merkle-Damgård迭代结构,通过这种方式增强了哈希值的随机性。选用了纠错能力更强的线性分组码与SM3算法相结合,并构造生成哈希值具有更强随机性的哈希函数。实验结果显示该算法不仅具有理想的雪崩效应特性,而且攻击者更难以逆推出原始消息,从而具备了更高的算法安全性。二、算法设计原理2.1 算法的创新点(1)通过调整循环左移位数来进一步提升哈希值的随机性;(2)通过优化迭代结构来提高算法的执行效率。算法的具体使用流程:初始化状态向量,为生成256位哈希值准备。计算纠错码的生成矩阵。对生成矩阵进行循环左移操作以增加随机性。对输入数据进行迭代压缩操作。输出最终的256位哈希值。2.2 实现代码(C++)#include <iostream>#include <vector>#include <cstdint>#include <cassert>// 哈希值长度constexpr size_t HASH_LENGTH = 32; // 256 bits// 线性分组码参数constexpr size_t CODE_LENGTH = 32; // 码长constexpr size_t CODE_DIMENSION = 6; // 维度// 生成矩阵初始化std::vector<std::vector<uint32_t>> generateMatrix() { // 实际应用中需要通过算法计算得到 std::vector<std::vector<uint32_t>> matrix(CODE_DIMENSION, std::vector<uint32_t>(CODE_LENGTH)); // 示例矩阵 for (size_t i = 0; i < CODE_DIMENSION; ++i) { matrix[i][i] = 1; } return matrix;}// 生成码字std::vector<uint32_t> generateCodeWord(const std::vector<std::vector<uint32_t>>& matrix) { std::vector<uint32_t> codeWord(CODE_LENGTH); // 假设这里使用生成矩阵生成码字 for (size_t i = 0; i < CODE_LENGTH; ++i) { codeWord[i] = 0; // 初始化码字 for (size_t j = 0; j < CODE_DIMENSION; ++j) { // 模2加法 codeWord[i] ^= matrix[j][i]; } } return codeWord;}// 循环左移uint32_t rotateLeft(uint32_t value, int shift) { return (value << shift) | (value >> (32 - shift));}// 消息预处理std::vector<uint32_t> preprocessMessage(const std::vector<uint8_t>& message) { // 添加消息长度,这里简化处理 std::vector<uint8_t> extendedMessage = message; extendedMessage.push_back(0x80); // 添加结束标志 extendedMessage.insert(extendedMessage.end(), 8, 0x00); // 添加长度占位符 // 将消息转换为32位整数数组 std::vector<uint32_t> words; for (size_t i = 0; i < extendedMessage.size(); i += 4) { uint32_t word = 0; for (size_t j = 0; j < 4 && i + j < extendedMessage.size(); ++j) { word |= static_cast<uint32_t>(extendedMessage[i + j]) << (24 - 8 * j); } words.push_back(word); } return words;}// 消息扩展void extendMessage(std::vector<uint32_t>& words) { const size_t WORD_COUNT = 64; // 扩展后的字数量 while (words.size() < WORD_COUNT) { uint32_t w = words.back(); uint32_t w16 = words[words.size() - 16]; words.push_back(w16 ^ rotateLeft(w16, 9) ^ rotateLeft(w16, 17) ^ rotateLeft(w, 15) ^ rotateLeft(w, 23)); }}// 布尔函数uint32_t ff(uint32_t x, uint32_t y, uint32_t z, size_t j) { if (j >= 16 && j <= 63) { return x ^ y ^ z; } else { return (x & y) | ((~x) & z); }}// 布尔函数uint32_t gg(uint32_t x, uint32_t y, uint32_t z, size_t j) { if (j >= 16 && j <= 63) { return x ^ y ^ z; } else { return (x & y) | (x & z) | (y & z); }}// 压缩函数void compress(std::vector<uint32_t>& state, const std::vector<uint32_t>& words) { const size_t ROUND_COUNT = 64; // 迭代次数 const size_t STATE_SIZE = 8; // 状态寄存器大小 const uint32_t T[ROUND_COUNT] = { /* 常量表 */ }; std::vector<uint32_t> ss1(STATE_SIZE), ss2(STATE_SIZE), tt1(STATE_SIZE), tt2(STATE_SIZE); for (size_t i = 0; i < ROUND_COUNT; ++i) { // 中间变量更新 ss1[i] = rotateLeft(state[0], 7) + rotateLeft(state[4], 12); ss2[i] = ss1[i] ^ rotateLeft(state[0], 12); tt1[i] = ff(state[0], state[1], state[2], i) + state[3] + ss2[i] + words[i]; tt2[i] = gg(state[4], state[5], state[6], i) + state[7] + ss1[i] + words[i + 64]; // 状态寄存器更新 state[0] = state[1]; state[1] = state[2]; state[2] = state[3]; state[3] = tt1[i]; state[4] = state[5]; state[5] = state[6]; state[6] = state[7]; state[7] = tt2[i]; }}// 主算法std::vector<uint8_t> hrhf(const std::vector<uint8_t>& message) { std::vector<uint32_t> state(HASH_LENGTH / 4, 0); // 初始化寄存器 std::vector<std::vector<uint32_t>> matrix = generateMatrix(); std::vector<uint32_t> codeWord = generateCodeWord(matrix); // 循环左移操作以增加随机性 for (size_t i = 0; i < CODE_DIMENSION; ++i) { codeWord[i] = rotateLeft(codeWord[i], 6); // 循环左移6位 } // 将码字分配给初始寄存器 for (size_t i = 0; i < HASH_LENGTH / 4; ++i) { state[i] = codeWord[i % CODE_DIMENSION]; } // 消息预处理 std::vector<uint32_t> words = preprocessMessage(message); // 打印预处理后的消息 std::cout << "Preprocessed Message: "; for (const auto& word : words) { std::cout << word << " "; } std::cout << std::endl; // 消息扩展 extendMessage(words); // 打印扩展后的消息 std::cout << "Extended Message: "; for (const auto& word : words) { std::cout << word << " "; } std::cout << std::endl; // 迭代压缩 compress(state, words); // 将32位整数转换为256位哈希值 std::vector<uint8_t> hash(HASH_LENGTH); for (size_t i = 0; i < HASH_LENGTH / 4; ++i) { uint32_t word = state[i]; for (size_t j = 0; j < 4; ++j) { hash[i * 4 + j] = word >> (24 - 8 * j); } } return hash;}int main() { std::vector<uint8_t> message = { 'q', 'y', 'x' }; std::vector<uint8_t> hash = hrhf(message); std::cout << "Hash Value: "; for (auto b : hash) { printf("%02x", b); } std::cout << std::endl; return 0;}运行结果:2.3 创新部分(1)循环左移位数的优化: 通过调整循环左移位数可以进一步提升哈希值的随机性。实验结果显示,在循环左移6位时,信息熵数值最高,这表明构造的初始常量值随机性最高,符合HRHF算法的设计目标。(2)迭代结构的优化: 通过优化迭代结构,提升了算法的执行效率。实验结果表明,在输入消息长度为401080字节的条件下,HRHF算法可以在1秒内完成4502000次运算,与SM3算法的运算效率基本一致,这表明HRHF算法可以支持快速运算。2.4 对比实验结果在相同的迭代结构下,HRHF算法的输出哈希值熵值相对于SM3算法有所增加,同时在哈希值长度都为256位的情况下,HRHF算法的轮函数复杂性更高,所产生的哈希值信息熵也高于SHA-256算法,这表明HRHF算法基于线性分组码在哈希值长度和迭代结构之间达到了有效的平衡,使得哈希值具有更高的随机性,同时也更好地隐藏了输入输出之间的关联性。 HRHF算法在运算效率和内存损耗方面也表现出了优势。2.5 Python代码实现import struct# 哈希值长度HASH_LENGTH = 32 # 256 bits# 线性分组码参数CODE_LENGTH = 32 # 码长CODE_DIMENSION = 6 # 维度# 生成矩阵初始化def generate_matrix(): matrix = [[0] * CODE_LENGTH for _ in range(CODE_DIMENSION)] # 示例矩阵 for i in range(CODE_DIMENSION): matrix[i][i] = 1 return matrix# 生成码字def generate_code_word(matrix): code_word = [0] * CODE_LENGTH # 使用生成矩阵生成码字 for i in range(CODE_LENGTH): for j in range(CODE_DIMENSION): # 模2加法 code_word[i] ^= matrix[j][i] return code_word# 循环左移def rotate_left(value, shift): return ((value << shift) & 0xFFFFFFFF) | (value >> (32 - shift))# 消息预处理def preprocess_message(message): extended_message = list(message) + [0x80] + [0x00] * 8 # 添加长度占位符 words = [] for i in range(0, len(extended_message), 4): word = 0 for j in range(min(4, len(extended_message) - i)): word |= extended_message[i + j] << (24 - 8 * j) words.append(word) return words# 消息扩展def extend_message(words): # 确保words至少有16个元素 while len(words) < 16: words.append(0) # 添加零填充 while len(words) < 128: w = words[-1] w16 = words[-16] words.append((w16 ^ rotate_left(w16, 9) ^ rotate_left(w16, 17)) ^ (rotate_left(w, 15) ^ rotate_left(w, 23)))# 布尔函数def ff(x, y, z, j): if 16 <= j <= 63: return x ^ y ^ z else: return (x & y) | ((~x) & z)# 布尔函数def gg(x, y, z, j): if 16 <= j <= 63: return x ^ y ^ z else: return (x & y) | (x & z) | (y & z)# 压缩函数def compress(state, words): for i in range(64): # 中间变量更新 ss1 = rotate_left(state[0], 7) + rotate_left(state[4], 12) ss1 &= 0xFFFFFFFF # Ensure 32-bit ss2 = ss1 ^ rotate_left(state[0], 12) tt1 = ff(state[0], state[1], state[2], i) + state[3] + ss2 + words[i] tt1 &= 0xFFFFFFFF # Ensure 32-bit tt2 = gg(state[4], state[5], state[6], i) + state[7] + ss1 + words[i + 64] tt2 &= 0xFFFFFFFF # Ensure 32-bit # 状态寄存器更新 state = [ state[1], state[2], state[3], tt1, state[5], state[6], state[7], tt2 ] return state# 主算法def hrhf(message): state = [0] * (HASH_LENGTH // 4) # 初始化寄存器 matrix = generate_matrix() code_word = generate_code_word(matrix) # 循环左移操作以增加随机性 for i in range(CODE_DIMENSION): code_word[i] = rotate_left(code_word[i], 6) # 循环左移6位 # 将码字分配给初始寄存器 for i in range(HASH_LENGTH // 4): state[i] = code_word[i % CODE_DIMENSION] # 消息预处理 words = preprocess_message(message) # 消息扩展 extend_message(words) # 迭代压缩 state = compress(state, words) # 将32位整数转换为256位哈希值 hash_value = bytearray(HASH_LENGTH) for i in range(HASH_LENGTH // 4): word = state[i] for j in range(4): hash_value[i * 4 + j] = (word >> (24 - 8 * j)) & 0xFF return hash_valueif __name__ == "__main__": message = b'qyx222' hash_value = hrhf(message) print(hash_value.hex())
推荐直播
-
华为云码道-AI时代应用开发利器2026/03/18 周三 19:00-20:00
童得力,华为云开发者生态运营总监/姚圣伟,华为云HCDE开发者专家
本次直播由华为专家带你实战应用开发,看华为云码道(CodeArts)代码智能体如何在AI时代让你的创意应用快速落地。更有华为云HCDE开发者专家带你用码道玩转JiuwenClaw,让小艺成为你的AI助理。
回顾中 -
Skill 构建 × 智能创作:基于华为云码道的 AI 内容生产提效方案2026/03/25 周三 19:00-20:00
余伟,华为云软件研发工程师/万邵业(万少),华为云HCDE开发者专家
本次直播带来两大实战:华为云码道 Skill-Creator 手把手搭建专属知识库 Skill;如何用码道提效 OpenClaw 小说文本,打造从大纲到成稿的 AI 原创小说全链路。技术干货 + OPC创作思路,一次讲透!
回顾中 -
码道新技能,AI 新生产力——从自动视频生成到开源项目解析2026/04/08 周三 19:00-21:00
童得力-华为云开发者生态运营总监/何文强-无人机企业AI提效负责人
本次华为云码道 Skill 实战活动,聚焦两大 AI 开发场景:通过实战教学,带你打造 AI 编程自动生成视频 Skill,并实现对 GitHub 热门开源项目的智能知识抽取,手把手掌握 Skill 开发全流程,用 AI 提升研发效率与内容生产力。
回顾中
热门标签