• [技术干货] 2024年2月嵌入式项目开发专题总汇
    一、前言在嵌入式开发领域,C语言由于其高效性、可移植性和对底层硬件的直接控制能力而被广泛采用。当前通过一系列实践主题和案例,勾勒出C语言在嵌入式系统编程中的关键应用与技术要点。以下所列的文章,介绍知识应用和技术点:(1)C语言语句与位运算:在嵌入式系统中,位运算符和位操作对于高效地处理硬件寄存器配置以及数据压缩等领域至关重要。学习如何利用C语言提供的逻辑与(&)、逻辑或(|)、异或(^)、左移(<<)、右移(>>)等位运算符进行精确的二进制操作是嵌入式开发者的基本技能之一。(2)C语言数组的查找、替换、排序、拼接:数组作为基本的数据结构,在存储传感器数据、设备状态信息等方面不可或缺。掌握数组元素的高效查找、替换算法(如线性搜索、二分查找),实现快速排序、冒泡排序等排序方法,以及数组内容的合并拼接,能够帮助开发者更好地管理内存受限环境下的数据集合。(3)C语言标准时间与秒单位转换:在涉及实时处理、日志记录等功能时,正确处理时间戳至关重要。C语言的标准库提供了时间和日期相关的函数接口,用于将秒单位的时间转换成易读格式或进行精准的时间计算。(4)C语言函数封装与变量作用域:良好的程序设计依赖于模块化和信息隐藏,函数封装是这一原则的重要体现。理解并运用好局部变量、全局变量和静态变量的作用域规则,以及如何定义和调用函数来实现功能模块化,有助于提升代码的可读性、可维护性和复用性。(5)STM32控制max30102读取血氧心率数据实例:该实战项目展示了如何使用C语言配合Keil MDK开发工具,驱动特定传感器(如MAX30102)采集生理信号,并进行数据解析。这种实际案例生动演示了嵌入式软件如何与硬件交互以实现具体应用需求。(6)C语言字符串与指针练习:字符串处理和指针操作是C语言的核心部分,在嵌入式编程中尤为常见,比如处理网络数据包、文件I/O、用户界面显示等。理解和熟练运用字符串函数,以及通过指针动态管理内存,是嵌入式程序员必备的能力。(7)C语言结构体:结构体能够组合不同类型的数据,模拟复杂的实体对象,适用于描述设备配置信息、协议数据包等。掌握结构体声明、初始化、成员访问以及结构体数组和指针的使用,有助于构建复杂的数据模型。(8)C语言单向与双向链表:链表作为一种灵活的数据结构,在资源受限的嵌入式环境中可以有效替代数组,实现动态增删节点的操作,尤其适用于缓存管理、消息队列等场景。(9) C语言标准文件读写接口与文件操作函数案例:文件系统操作是嵌入式系统中常见的任务,如读取配置文件、保存设备日志、上传下载数据等。深入研究C语言标准库中的文件操作函数,如fopen、fclose、fread、fwrite等,以及如何编写安全、高效的文件读写程序,对提高嵌入式系统的数据持久化能力至关重要。(10)基于STM32的儿童智能安全防护书包设计:这个实际项目不仅涉及到C语言的综合运用,还展示了如何结合嵌入式微控制器设计智能产品,整合各类传感器数据、无线通信模块等功能,体现了嵌入式技术在物联网(IoT)和消费电子领域的广泛应用。(11)C语言处理文件目录后缀:针对文件系统的高级操作,例如根据文件扩展名进行过滤或重命名,是嵌入式系统中可能遇到的功能需求。学习如何在C语言中解析路径和文件名,进而处理文件类型和扩展名,有助于增强系统的文件管理功能。(12)Linux与Windows下C语言使用curl库的安装使用及HTTP文件下载:跨平台网络通信在现代嵌入式开发中必不可少,curl库是一个流行的HTTP客户端工具库。了解如何在不同的操作系统环境下安装和使用curl库,编写C语言代码实现HTTP请求、文件下载等网络服务功能,有利于开发具备互联网连接能力的嵌入式产品。二、文章列表【1】嵌入式开发_C语言语句与位运算cid:link_3通过一系列精心设计的C语言编程练习题,巩固和深化对C语言语法的理解和应用。文章中包含了一系列覆盖C语言基础知识点的练习题目,如素数计算、排序算法、求偶数和、可逆素数、水仙花数、大小写字母转换、变量值交换、位运算以及C语言的语法特性等。每个练习题都不仅提供了问题描述,还深入探讨了解题思路和实现方法,帮助读者掌握C语言的编程技巧。通过对这些练习题的逐一解答,读者可以加深对C语言控制结构、数据类型、函数、数组、指针等核心概念的理解。知识点:素数计算:介绍如何通过循环和条件判断来识别素数。排序算法:展示如何使用数组和循环结构实现基本的排序算法。求偶数和:讲解如何利用循环累加数组中的偶数元素。可逆素数:探索如何判断一个数是否为可逆素数,即其平方的末尾数字不变。水仙花数:分析如何查找并验证一个三位数是否为水仙花数。大小写转换:演示如何使用字符操作函数进行大小写字母的转换。变量值交换:阐述不使用临时变量的情况下如何交换两个变量的值。位运算:解释位运算的基本概念及其在C语言中的应用。语法特性:总结C语言的一些重要语法特性,帮助读者避免常见错误。【2】嵌入式开发_C语言数组的查找、替换、排序、拼接cid:link_4在这篇文章中,将深入探讨C语言中位运算的高级应用,并通过一系列实战例子来展示位运算在实际编程中的威力。总结了位运算的基本概念,包括位与(&)、位或(|)、位非(~)、位异或(^)、左移(<<)和右移(>>)等操作,然后通过几个具体的编程实例,如快速幂运算、状态压缩等,让读者体会位运算在优化程序性能方面的独到之处。接下来,将从数组的基本定义和用法开始,逐步深入到数组的各种操作技巧。这包括对数组进行排序、插入新元素、拼接两个数组、删除指定元素以及在字符串中查找和替换特定内容等实用技能。每个操作都配以详细的代码示例和解析,帮助读者理解并掌握这些关键的数组处理方法。知识点:位运算实战例子:提供多个实际问题的解决方案,让读者了解如何利用位运算简化计算过程。数组基本定义与用法:介绍数组的声明、初始化及其在内存中的存储方式。数组排序:演示如何使用标准库函数或自定义算法对数组进行排序。数组插入:解释如何在数组中插入新元素并保持原有顺序。数组拼接:展示如何将两个或多个数组合并成一个更大的数组。数组删除:讲述如何从数组中移除特定元素或子数组。字符串查找替换:探索在C语言中处理字符串的技巧,包括查找子串和替换内容。【3】嵌入式开发_C语言标准时间与秒单位的转换cid:link_5在这篇博客文章中,将深入探讨嵌入式单片机中标准时间与秒单位之间的转换方法。介绍了标准时间和秒单位的概念,以及它们在嵌入式单片机中的应用场景。接着,通过两个具体的例子来展示如何将RTC时钟的时间转换为标准时间进行显示。这两个例子都包含了详细的代码注释和思路解析,帮助读者更好地理解和掌握这种转换方法。文章知识点:标准时间与秒单位概念介绍:解释标准时间和秒单位的定义及其在嵌入式单片机中的应用。RTC时钟时间读取:演示如何从RTC时钟中读取秒单位时间,并将其转换为标准时间进行显示。代码注释和思路解析:提供详细注释和思路解析,帮助读者理解代码的实现过程。示例代码演示:给出两个具体的示例代码,展示如何在不同场景下进行标准时间与秒单位的转换。【4】嵌入式开发_C语言函数封装、变量的作用域cid:link_0在这篇文章中,将深入探讨C语言中函数封装的重要性以及变量作用域的规则。从函数的基本概念讲起,阐述如何将常用的代码块封装成可重用的函数,以及如何通过传递参数和返回值来增强函数的灵活性。接着,详细解释了变量作用域——包括局部变量和全局变量的区别,以及如何正确管理变量的生命周期来避免常见的编程错误。后面介绍了一系列关于字符串处理的实战练习,在帮助读者掌握C语言中字符串与数值之间的转换方法。这包括将字符串转换为整数、整数转换为字符串、浮点数与字符串之间的相互转换,以及如何判断给定年份是否为平年或闰年。文章介绍了如何使用标准库中的函数来高效地计算字符串的长度。知识点:函数封装:讲解如何将代码块封装成函数,提高代码的模块性和可维护性。变量作用域:分析局部变量和全局变量的定义、区别及应用场景。字符串转整数:展示如何使用标准库函数如atoi或自定义函数实现字符串到整数的转换。整数转字符串:介绍如何使用sprintf等函数将整数转换为字符串。浮点数与字符串互转:探讨使用sprintf和sscanf等函数处理浮点数与字符串之间的转换。平年闰年判断:编写函数来判断给定年份是平年还是闰年。字符串长度计算:说明如何使用strlen函数来计算字符串的长度。【5】嵌入式开发_C语言函数封装、变量的作用域cid:link_6【6】STM32控制max30102读取血氧心率数据(keil5工程)cid:link_6MAX30102是一款由Maxim Integrated推出的低功耗、高精度的心率和血氧饱和度检测传感器模块,适用于可穿戴设备如智能手环、智能手表等健康管理类电子产品。该传感器主要特性如下:(1)光学测量:MAX30102内置了两个LED光源(红光和红外光),以及一个光电检测器,通过光电容积脉搏波描记法(PPG)来实现心率和血氧饱和度的无创检测。(2)低功耗:在典型的工作模式下,其功耗非常低,有助于延长电池供电设备的使用寿命。(3)集成度高:内部集成了AFE(模拟前端)、LED驱动器、环境光抑制功能以及I²C数字接口,方便与微控制器连接通信。(4)多档位配置:支持多个LED电流输出级别和采样速率选择,可以根据实际应用需求进行灵活配置。(5)高精度:通过先进的信号处理算法,可以有效降低噪声干扰,提高测量数据的准确性。(6)小尺寸封装:采用紧凑型封装设计,便于在空间受限的产品中使用。MAX30102是一款高性能的生物医学传感器,能够帮助开发者在各种便携式和穿戴式设备上实现对人体生理参数的有效监测。【7】嵌入式开发_C语言标准时间与秒单位的转换cid:link_7在这篇博客文章中,将深入探讨嵌入式单片机中标准时间与秒单位之间的转换方法。介绍了标准时间和秒单位的概念,以及它们在嵌入式单片机中的应用场景。接着,通过两个具体的例子来展示如何将RTC时钟的时间转换为标准时间进行显示。这两个例子都包含了详细的代码注释和思路解析,帮助读者更好地理解和掌握这种转换方法。文章知识点:标准时间与秒单位概念介绍:解释标准时间和秒单位的定义及其在嵌入式单片机中的应用。RTC时钟时间读取:演示如何从RTC时钟中读取秒单位时间,并将其转换为标准时间进行显示。代码注释和思路解析:提供详细注释和思路解析,帮助读者理解代码的实现过程。示例代码演示:给出两个具体的示例代码,展示如何在不同场景下进行标准时间与秒单位的转换。【8】 嵌入式开发_C语言字符串与指针的练习cid:link_8在这篇文章中,将深入探讨C语言中字符串和指针相关的一系列重要知识点。文章通过一系列的练习题和实例,详细讲解了浮点数与字符串之间的相互转换、字符串的拷贝和比较、指针用于交换变量的值、指针的优先级规则,以及数据类型的强制转换和内存拷贝函数的使用。文章首先介绍了字符串与指针的基本概念,然后逐步引导读者学习如何操作字符串和指针来完成各种常见的编程任务。每个知识点都配以清晰的代码示例和详细的注释,帮助读者理解并掌握这些关键的C语言技能。【9】嵌入式开发_C语言结构体cid:link_1本篇文章主要介绍C语言中两个核心概念:动态堆空间的内存分配与释放,以及结构体的详细使用方法。介绍了如何在C语言中利用malloc和free函数进行动态内存的分配和回收,这对于处理数量未知或可变的数据集合至关重要。然后,深入探讨了结构体的定义、初始化、赋值操作,以及如何创建和操作结构体数组和结构体指针。在理论知识的基础上,文章通过开发一个简易的学生管理系统来综合运用这些知识点。这个实践案例不仅展示了结构体数组在实际项目中的应用场景,也演示了如何将结构体与动态内存管理结合起来,以高效地处理数据。【10】嵌入式开发_C语言单向与双向链表cid:link_9本篇文章将深入探讨C语言中链表的相关知识点,包括链表的创建、单向链表、循环链表、双向链表和单向循环链表等。总结一些链表常见问题,并提供结构体数组与链表的练习题供读者实践。在下一篇文章里,将提供完整的代码示例,帮助读者更好地理解和应用这些知识点。知识点:链表创建:讲解如何定义链表节点和头节点,以及如何初始化链表。单向链表:介绍单向链表的基本概念和操作,如插入、删除、遍历等。循环链表:解释循环链表的特点和应用场景,并演示如何实现循环链表的操作。双向链表:讲解双向链表的定义和常见操作,如在链表中查找元素、插入和删除节点等。单向循环链表:介绍单向循环链表的概念和特点,以及如何进行相关操作。链表常见问题总结:列举并解答一些常见的链表问题,帮助读者避免常见的错误。结构体数组与链表的练习题:提供一系列练习题,让读者通过实际编程来巩固所学知识。【11】嵌入式开发_C语言标准文件读写接口cid:link_10在这篇文章中探讨C语言中与文件操作相关的一系列重要知识点。文章详细介绍了最常用的文件操作函数,包括fopen、fread、fwrite和fclose等,并通过几个常见的需求场景提供实例,帮助读者更好地理解和掌握这些函数的用法。知识点:文件操作函数介绍:详细解释每个文件操作函数的功能和使用方法。fopen函数:讲解如何打开文件,并处理可能出现的错误情况。fread和fwrite函数:通过实例演示如何读取和写入文件内容。fclose函数:说明如何正确关闭一个已打开的文件。常见需求例子:提供实际应用场景的例子,如创建文件、追加内容、读取特定格式的数据等。错误处理:讨论在进行文件操作时可能遇到的错误,以及如何进行有效的错误处理。【12】嵌入式开发_C语言文件操作函数案例cid:link_11在这篇精彩的博客文章中,为希望提高C语言文件编程技能的读者准备了四个实战练习题。这些练习覆盖了从文件拷贝到学生管理系统开发的关键技能,包括文件加密和利用文件系统保存信息的链表模板。文章不仅提出了挑战,通过实践加深对文件操作函数应用的理解。文章知识点:文件拷贝实现:引导读者了解如何逐字节地读取源文件并写入目标文件,完成文件的复制过程。文件加密练习:介绍简单的文件加密技术,让读者尝试对文件内容进行加密和解密操作。学生管理系统链表模板:提供一个未添加文件操作的学生管理系统链表模板,作为后续练习的基础。学生管理系统模板:要求读者将学生信息通过文件系统进行持久化保存,增强管理系统的实用性。【13】基于STM32的儿童智能安全防护书包设计cid:link_12随着社会的进步和科技的发展,儿童安全问题日益引起广泛关注。在日常生活中,尤其是在上学放学途中、户外活动时,儿童走失事件时有发生,给家庭和社会带来了极大的困扰和担忧。随着学业负担的增加,学生时常会因为忘记携带所需书籍而影响学习。如何利用现代技术手段提高儿童安全保障水平,并辅助他们培养良好的学习习惯,成为了一个待解决的社会需求。基于此背景,当前设计并实现一款基于STM32F103RCT6微控制器为核心的儿童智能安全防护书包,显得尤为必要与实际。这款书包集成了先进的定位技术和无线通信模块,能够实时追踪并发送儿童的位置信息给家长,确保在紧急情况下快速响应 (发送短信的时候,直接通过GPS经纬度拼接百度地图的HTTP请求链接,家长收到短信可以直接打开链接,在网页查看百度地图上显示的具体位置,可以直接通过百度地图导航过去)。同时,具备智能化功能,如课程表录入存储与提醒系统,利用EEPROM(例如AT24C02)进行数据持久化存储,并通过RFID-RC522射频识别模块自动检测所携带书籍是否齐全,避免孩子因疏忽遗漏课本而耽误学习。智能书包还配备了直观易读的1.44寸LCD显示屏,用于显示当前位置信息、当日课表以及未带书籍的提醒。当检测到缺少某本书籍时,蜂鸣器模块会发出声音警报,从而强化提醒效果,帮助学生养成有序整理个人物品的习惯。这款基于STM32的儿童智能安全防护书包是一个集成物联网技术、GPS定位、无线通信和智能感知于一体的创新产品,提升儿童的安全防护等级,加强家校互动,促进学生自我管理能力的培养,充分体现了科技服务于生活、服务于教育的理念。【14】 嵌入式开发_C语言处理文件目录后缀cid:link_2在 C 语言中,对文件名、目录名、文件后缀等数据进行处理通常需要使用字符串处理函数和技巧。常用的字符串处理包括字符串拷贝、字符串连接、字符串比较、分割字符串以及获取文件后缀等操作。(1)字符串拷贝:使用 strcpy 函数可以将一个字符串复制到另一个字符串中。(2)字符串连接:使用 strcat 函数可以将两个字符串连接起来。(3)字符串比较:使用 strcmp 函数可以比较两个字符串是否相等。(4)分割字符串:可以使用 strtok 函数将字符串按照指定分隔符分割成多个子字符串。(5)获取文件后缀:通过查找字符串中最后一个.字符的位置,可以获取文件的后缀名。这些字符串处理函数和技巧能够帮助我们在 C 语言中对文件名、目录名、文件后缀等数据进行灵活处理。【15】Linux与windows下C语言使用curl库的安装使用以及访问HTTP下载文件cid:link_13cURL 是一个命令行工具和库,用于传输数据,支持多种协议,如 HTTP、HTTPS、FTP 等。可以在终端中用来发送和接收数据,执行各种网络操作,如下载文件、上传文件、发送 POST 请求等。
  • [技术干货] 嵌入式开发_C语言处理文件目录后缀
    一. 前言在 C 语言中,对文件名、目录名、文件后缀等数据进行处理通常需要使用字符串处理函数和技巧。常用的字符串处理包括字符串拷贝、字符串连接、字符串比较、分割字符串以及获取文件后缀等操作。(1)字符串拷贝:使用 strcpy 函数可以将一个字符串复制到另一个字符串中。(2)字符串连接:使用 strcat 函数可以将两个字符串连接起来。(3)字符串比较:使用 strcmp 函数可以比较两个字符串是否相等。(4)分割字符串:可以使用 strtok 函数将字符串按照指定分隔符分割成多个子字符串。(5)获取文件后缀:通过查找字符串中最后一个.字符的位置,可以获取文件的后缀名。这些字符串处理函数和技巧能够帮助我们在 C 语言中对文件名、目录名、文件后缀等数据进行灵活处理。下面是C语言的代码:(1)字符串拷贝:使用 strcpy 函数可以将一个字符串复制到另一个字符串中。char str1[100] = "Hello";char str2[100];strcpy(str2, str1); // 将 str1 复制到 str2(2)字符串连接:使用 strcat 函数可以将两个字符串连接起来。char str1[100] = "Hello";char str2[100] = "World";strcat(str1, str2); // 将 str2 连接到 str1 的末尾(3)字符串比较:使用 strcmp 函数可以比较两个字符串是否相等。char str1[100] = "Hello";char str2[100] = "Hello";if (strcmp(str1, str2) == 0){ // 字符串相等}(4)分割字符串:可以使用 strtok 函数将字符串按照指定分隔符分割成多个子字符串。char str[100] = "file.txt";char *token = strtok(str, ".");while (token != NULL){ printf("%s\n", token); token = strtok(NULL, ".");}(5)获取文件后缀:通过查找字符串中最后一个.字符的位置,可以获取文件的后缀名。char filename[100] = "example.txt";char *dot = strrchr(filename, '.');if (dot != NULL){ printf("File extension: %s\n", dot + 1);}二. 实现代码-纯C/C++示例代码:string path = "C:\\Users\\Administ.1.2.3rator\\Desktop\\text\\data.22.txt"; //string path = "http://cqpc:8001/Uploads/1/220512030806054762.mp4"; //1.获取不带路径的文件名 string::size_type iPos; if (strstr(path.c_str(), "\\")) { iPos = path.find_last_of('\\') + 1; } else { iPos = path.find_last_of('/') + 1; } string filename = path.substr(iPos, path.length() - iPos); cout <<"获取不带路径的文件名:"<<filename << endl;​​ //2.获取不带后缀的文件名 string name = filename.substr(0, filename.rfind(".")); cout <<"获取不带后缀的文件名:"<<name << endl;​ //3.获取后缀名 string suffix_str = filename.substr(filename.find_last_of('.') + 1); cout << "获取后缀名:"<<suffix_str << endl;​ //4. 获取基本名称 cout << "基本名称:"<<filename.substr(0, filename.find("."));返回结果:获取不带路径的文件名:data.22.txt获取不带后缀的文件名:data.22获取后缀名:txt基本名称:data三. C语言字符串处理案例1. 计算空格、大小写字母从键盘上输入一个字符串, 计算字符串里有多少个空格、小写字母、大写字母、数字。#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件int main(int argc,char **argv){ int len=0; int i; char str[100]; int cnt[5]={0}; //初始化赋值 //scanf("%s",str); //从键盘上录入字符串,字符串结尾: '\0' //gets(str); //从键盘上录入字符串 fgets(str,100,stdin); //从键盘上录入字符串 (标准输入) //空格、小写字母、大写字母、数字 其他数据 /*1. 计算字符串的长度*/ while(str[len]!='\0')len++; printf("len1=%d\n",len); printf("len2=%d\n",strlen(str)); //计算字符串长度 /*2. 处理字符串*/ for(i=0;i<len;i++) { if(str[i]==' ')cnt[0]++; else if(str[i]>='a'&&str[i]<='z')cnt[1]++; else if(str[i]>='A'&&str[i]<='Z')cnt[2]++; else if(str[i]>='0'&&str[i]<='9')cnt[3]++; else cnt[4]++; } /*3. 打印结果*/ printf("空格:%d\n",cnt[0]); printf("小写:%d\n",cnt[1]); printf("大写:%d\n",cnt[2]); printf("数字:%d\n",cnt[3]); printf("其他:%d\n",cnt[4]); return 0;}2. 字符串排序示例:#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件​int main(int argc,char **argv){ int len=0; int i,j; char tmp; char str[100]; fgets(str,100,stdin); //从键盘上录入字符串 (标准输入) /*1. 计算字符串的长度*/ len=strlen(str); //计算字符串长度 /*2. 字符串排序*/ for(i=0;i<len-1;i++) { for(j=0;j<len-1-i;j++) { if(str[j]<str[j+1]) { tmp=str[j]; str[j]=str[j+1]; str[j+1]=tmp; } } } /*3. 打印结果*/ printf("%s\n",str); //打印字符串(标准输出) puts(str); //打印字符串(标准输出) fputs(str,stdout); //打印字符串(标准输出) return 0;}3. 字符串插入字符串插入: “1234567890” 在第2个位置后面插入”ABC” 最终结果: “12ABC34567890”#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件int main(int argc,char **argv){ int i,j; int src_len; int new_len; /* 123456789 12 3456789 */ char src_str[100]="123456789"; char new_str[]="abcd"; int addr=2; //插入的位置 /*1. 计算字符串的长度*/ src_len=strlen(src_str); //"123" new_len=strlen(new_str); /*2. 字符串移动*/ for(i=src_len-1;i>addr-1;i--) { src_str[i+new_len]=src_str[i]; //向后移动 new_len } /*3. 插入新的数据*/ for(i=0;i<new_len;i++)src_str[addr+i]=new_str[i]; /*4. 打印字符串*/ src_str[src_len+new_len]='\0'; //在字符串结尾添加'\0' printf("src_str=%s\n",src_str); return 0;}4. 字符串查找字符串查找: “123456123abc123hbc” 查找字符串”123”的数量。数量是3#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件​int main(int argc,char **argv){ char src_str[100]; char find_str[10]; int src_len=0,find_len=0; int i,j; int cnt=0; /*1. 录入字符串*/ printf("输入源字符串:"); //123dufvdfv123dfljvb fgets(src_str,100,stdin); //从键盘上录入源字符串 //scanf("%s",src_str); printf("输入查找的字符串:"); //123 fgets(find_str,10,stdin); //从键盘上录入源字符串 //scanf("%s",find_str); /*2. 计算长度*/ src_len=strlen(src_str); src_str[src_len-1]='\0'; src_len-=1; //src_len=src_len-1; find_len=strlen(find_str); //"123\n" =4 find_str[find_len-1]='\0'; find_len-=1; printf("源字符串:%s,%d\n",src_str,src_len); printf("查找的字符串:%s,%d\n",find_str,find_len); /*3. 查找字符串*/ for(i=0;i<src_len-find_len+1;i++) { for(j=0;j<find_len;j++) { //只有一次不相等就退出 //123kdfvfd 123 if(src_str[i+j]!=find_str[j])break; } if(j==find_len) //条件成立表示查找成功 { cnt++; i+=find_len-1;//向后移动 } } /*4. 打印查找结果*/ printf("cnt=%d\n",cnt); return 0;}5. 字符串删除字符串删除: “1234567890” 删除”456” 最终结果: “1237890”示例:#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件​int main(int argc,char **argv){ char src_str[100]; char del_str[10]; int src_len=0,del_len=0; int i,j; int cnt=0; /*1. 录入字符串*/ printf("输入源字符串:"); //123dufvdfv123dfljvb fgets(src_str,100,stdin); //从键盘上录入源字符串 printf("输入查找的字符串:"); //123 fgets(del_str,10,stdin); //从键盘上录入源字符串 /*2. 计算长度*/ src_len=strlen(src_str); src_str[src_len-1]='\0'; src_len-=1; //src_len=src_len-1; del_len=strlen(del_str); //"123\n" =4 del_str[del_len-1]='\0'; del_len-=1; printf("源字符串:%s,%d\n",src_str,src_len); printf("删除字符串:%s,%d\n",del_str,del_len);​ /*3. 查找*/ for(i=0;i<src_len-del_len+1;i++) { for(j=0;j<del_len;j++) { if(src_str[i+j]!=del_str[j])break; } if(j==del_len) { cnt++; /*4.删除*/ for(j=i;j<src_len-del_len;j++) { src_str[j]=src_str[j+del_len]; } src_len-=del_len; i-=1; //继续在当前位置查找 } } src_str[src_len]='\0'; printf("src_str=%s\n",src_str); printf("cnt=%d\n",cnt); return 0;}6. 字符串替换字符串”1234567890”将456替换为”888” 最终: “1238887890”需要考虑3种情况7. 字符串转整数。从键盘上输入一个字符串”12345”, 得到整数: 12345;#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件int string_to_int(char str[]);int main(int argc,char **argv){ int data; char str[]="125abcd"; data=string_to_int(str); printf("data=%d\n",data); return 0;}/*函数功能: 字符串转为整数字符转为整数: -48 或者 -'0'1234*/int string_to_int(char str[]){ int value=0; //存放转换之后的结果 int i=0; while((str[i]!='\0')&&(str[i]>='0'&&str[i]<='9')) { value*=10; value+=str[i]-'0'; i++; } return value;}8. 整数转字符串整数转字符串。输入一个整数1234,得到字符串: "1234"1234%10=4 1234/10=123 123%10=3 123/10=12示例:#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件void int_to_string(char str[],int data);int main(int argc,char **argv){ char str[100]; int_to_string(str,12345); printf("str=%s\n",str); return 0;}/*函数功能: 整数转为字符串函数参数: char str[] //存放转换之后的整数(字符串) int data //待转换的整数*/void int_to_string(char str[],int data){ int i=0,j; char tmp; /*1. 将整数转为字符串*/ while(data) { str[i]=data%10+'0'; data/=10; i++; } str[i]='\0'; /*2. 交换顺序*/ for(j=0;j<i/2;j++) { tmp=str[j]; str[j]=str[i-j-1]; str[i-j-1]=tmp; }}9. 浮点数转字符串浮点数转字符串。输入一个浮点数123.456 得到字符串"123.456"示例:#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件void float_to_string(char str[],float data);int main(int argc,char **argv){ char str[100]; float_to_string(str,12345.123); printf("str=%s\n",str); return 0;}/*函数功能: 浮点数转为字符串函数参数: char str[] //存放转换之后的 浮点数(字符串) int data //待转换的 浮点数*/void float_to_string(char str[],float data){ int i=0,j; char tmp; int addr; int int_data=data; //得到整数 12345 int float_data=(data-int_data)*1000000;// 0.123456 *1000000 =123456 /*1. 将整数部分转为字符串*/ while(int_data) { str[i]=int_data%10+'0'; int_data/=10; i++; } str[i]='.'; //添加小数点 /*2. 交换顺序: 整数*/ for(j=0;j<i/2;j++) { tmp=str[j]; str[j]=str[i-j-1]; str[i-j-1]=tmp; } /*3. 将浮点数部分转为字符串*/ i++; //跨过小数点 addr=i; while(float_data) { str[i]=float_data%10+'0'; float_data/=10; i++; } str[i]='\0'; /*4. 交换顺序: 小数部分*/ for(j=0;j<3;j++) { tmp=str[addr+j]; str[addr+j]=str[i-j-1]; str[i-j-1]=tmp; }}10.字符串转浮点数字符串转浮点数。输入一个字符串: "123.456" 得到浮点数类型: 123.456#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件float string_to_float(char str[]);int main(int argc,char **argv){ float data; char str[]="123.456"; data=string_to_float(str); printf("data=%f\n",data); return 0;}/*函数功能: 字符串转为浮点数字符转为整数: -48 或者 -'0'*/float string_to_float(char str[]){ int int_value=0; //存放转换之后的结果 int float_value=0; int i=0; float data; int cnt=0; //记录小数部分的数量 int tmp=1; /*1. 整数部分*/ while((str[i]!='\0')&&(str[i]>='0'&&str[i]<='9')) { int_value*=10; int_value+=str[i]-'0'; i++; } /*2. 浮点数部分*/ i++; //跨过小数点 while((str[i]!='\0')&&(str[i]>='0'&&str[i]<='9')) { float_value*=10; float_value+=str[i]-'0'; i++; cnt++; } for(i=0;i<cnt;i++)tmp*=10; data=int_value; //整数部分 data+=float_value/(tmp*1.0); return data;}
  • [技术干货] 嵌入式开发_C语言字符串与指针的练习
    在这篇文章中,将深入探讨C语言中字符串和指针相关的一系列重要知识点。文章通过一系列的练习题和实例,详细讲解了浮点数与字符串之间的相互转换、字符串的拷贝和比较、指针用于交换变量的值、指针的优先级规则,以及数据类型的强制转换和内存拷贝函数的使用。文章首先介绍了字符串与指针的基本概念,然后逐步引导读者学习如何操作字符串和指针来完成各种常见的编程任务。每个知识点都配以清晰的代码示例和详细的注释,帮助读者理解并掌握这些关键的C语言技能。1. 浮点数转字符串将字符串转整数、整数转字符串、浮点数转字符串、字符串转浮点数 封装为函数。 浮点数转字符串:#include <stdio.h>#include <string.h> //字符串处理void float_to_string(char str[],float data);​​int main(void){ char buff[50]; float data=1234.5; float_to_string(buff,data);​ printf("data=%f,buff=%s\n",data,buff); return 0;}​​/*函数功能: 浮点数转字符串*/void float_to_string(char str[],float data){ int buff[50]; int int_data1=data; //提取整数部分 int int_data2=(data-int_data1)*1000000; //提取小数部分 int i=0,j=0,k=0;​ //转整数部分 while(int_data1) { buff[i]=int_data1%10+'0'; int_data1=int_data1/10; i++; }​ //倒序 for(j=0;j<i;j++) { str[j]=buff[i-j-1]; }​ str[j]='.'; //添加小数点 j++;​ i=0; //转小数部分 while(int_data2) { buff[i]=int_data2%10+'0'; int_data2=int_data2/10; i++; }​ //倒序 for(k=0; k<i; k++) { str[j+k]=buff[i-k-1]; } str[j+k]='\0'; //添加结尾符号​ //优化后面0 i=j+k; //总长度 for(j=i-1; j>=0; j--) { if(str[j]=='0')str[j]='\0'; //添加结束符 else break; }}2. 封装字符串拼接函数封装字符串拼接函数: 函数功能实现将a和b字符串拼接在一起。 比如: char a[100]=”123”; char b[]=”456”; 调用函数之后: a[]=”123456”#include <stdio.h>#include <string.h> //字符串处理void my_strcat(char str1[],char str2[]);​int main(void){ char str1[50]="12345"; char str2[]="67890"; //strcat(str1,str2); //字符串拼接 my_strcat(str1,str2); //字符串拼接 printf("%s\n",str1); return 0;}​/*函数功能:字符串拼接*/void my_strcat(char str1[],char str2[]){ int i=0,len=0; while(str1[len]!='\0') { len++; }​ while(str2[i]!='\0') { str1[len+i]=str2[i]; i++; } str1[len+i]='\0'; //结尾补上'\0'}3. 封装字符串的拷贝函数封装字符串的拷贝函数: 将a字符串拷贝到b字符串。 示例:#include <stdio.h>#include <string.h> //字符串处理void my_strcpy(char str1[],char str2[]);​int main(void){ char str1[50]="12345"; char str2[]="67890"; //strcpy(str1,str2); //字符串拷贝 my_strcpy(str1,str2); printf("%s\n",str1); return 0;}​/*函数功能:字符串拼接*/void my_strcpy(char str1[],char str2[]){ int i=0,len=0; while(str2[i]!='\0') { str1[i]=str2[i]; //依次赋值 i++; } str1[i]='\0'; //补上结尾符号}4. 封装字符串的比较函数封装字符串的比较函数: 比较a字符串和b字符串是否相等。 通过返回值进行区分。 示例:​#include <stdio.h>#include <string.h> //字符串处理int my_strcmp(char str1[],char str2[]);​int main(void){ char str1[50]="中国"; char str2[]="中国"; int a; //a=strcmp(str1,str2); a=my_strcmp(str1,str2); if(a==0)printf("字符串相等!\n"); else printf("字符串不相等!\n"); return 0;}​/*函数功能:字符串比较*/int my_strcmp(char str1[],char str2[]){ int i=0; //只要有一个数组没结束就继续 while(str1[i]!='\0'||str2[i]!='\0') { if(str1[i]!=str2[i])break; i++; } if(str1[i]=='\0' && str2[i]=='\0')return 0; //相等 else return -1; //不相等}​5. 指针特性指针: 是C语言的灵魂。 指针: 可访问计算机的底层----->硬件。 定义指针的语法: <数据类型> *<变量名称>; int *p; 指针的特性:(1)指针本身没有空间。int *p; //定义一个指针变量(2)指针本身就是地址(专门保存地址)。int *p; p=? ?必须是地址类型。​int *p; //定义一个指针变量 int data=123; //p=123; //错误赋值方式 p=&data; //正确的赋值方式​(3)取出指针指向地址的数据 ​ int *p; //定义一个指针变量 int data=123; p=&data; //正确的赋值方式 printf("%d\n",*p); //取出p指针指向空间的值 //*p=888; (4)指针类型的变量都是占4个字节#include <stdio.h>int main(){ int *p1; //定义一个整型指针变量 char *p2; float *p3; double *p4; printf("int=%d\n",sizeof(p1)); printf("char=%d\n",sizeof(p2)); printf("float=%d\n",sizeof(p3)); printf("double=%d\n",sizeof(p4)); return 0;}(5)指针支持自增和自减 ++ --#include <stdio.h>int main(){ char *p; char str[]="1234567890"; p=str; //将数组地址赋值给指针p​ //指针可以直接使用数组下标(指针可以当数组名使用) printf("%c\n",p[0]);//1 printf("%c\n",p[1]);//2​ //通过指针访问数组的成员 printf("%c\n",*p); //1 p++; //指针自增 printf("%c\n",*p); //2 p--; printf("%c\n",*p); //1 return 0;}(6) 不同类型的指针自增和自减的字节数​//指针类型自增和自减的字节数与本身数据类型有关。​#include <stdio.h>int main(){ int data1[10]; char data2[10];​ /*1. 给指针赋值合法的空间*/ int *p1=data1; char *p2=data2;​ printf("0x%X\n",p1); p1++; printf("0x%X\n",p1);​ printf("0x%X\n",p2); p2++; printf("0x%X\n",p2); return 0;}6. 通过指针交换两个变量的值#include <stdio.h>void func(int *a,int *b);​int main(){ int a=100,b=200; func(&a,&b); printf("a=%d,b=%d\n",a,b); //200,100 return 0;}​//通过指针交换两个变量的值void func(int *a,int *b){ int c; c=*a; //取出100 *a=*b; //取出200赋值给a *b=c;}​7. 指针自增优先级​#include <stdio.h>​int main(){ char buff[]="12345"; char *p=buff; printf("%c\n",*p); //1 printf("%c\n",*p++); //1 printf("%c\n",*p++); //2 printf("%c\n",*p); //3 return 0;}8. 计算字符串的长度#include <stdio.h>int my_strlen(const char *str);​int main(){ printf("%d\n",my_strlen("1234567")); return 0;}​//加const为了防止不小心修改了数据int my_strlen(const char *str){ //*str='1'; //错误,不能赋值 char *p=str; //保存地址 while(*p!='\0')p++; return p-str; //得到字符串长度}9. 数据类型的强制转换数据类型的强制转换 (欺骗编译器)char *p1=(char*)src;char *p2=(char*)new;10. 编写一个内存拷贝函数功能: 可以将任何数据的类型进行相互赋值。int a,b=100; float a,b=123.456; ………..#include <stdio.h>void my_memcpy(void *new,void *src,int len);int main(){ //int data1=123,data2=456; //my_memcpy(&data1,&data2,4); //printf("data1=%d\n",data1); int data1[100]; int data2[50]={12,34,5,6,78}; my_memcpy(data1,data2,sizeof(data2)); printf("%d\n",data1[0]); printf("%d\n",data1[1]); return 0;}/*内存拷贝将src的数据拷贝到new,拷贝len*/void my_memcpy(void *new,void *src,int len){ char *p1=(char*)src; char *p2=(char*)new; int i; //for(i=0; i<len; i++)*p2++=*p1++; for(i=0; i<len; i++) { *p2=*p1; p1++; p2++; }}
  • [技术干货] 嵌入式开发_C语言标准时间与秒单位的转换
    前言:在这篇博客文章中,将深入探讨嵌入式单片机中标准时间与秒单位之间的转换方法。介绍了标准时间和秒单位的概念,以及它们在嵌入式单片机中的应用场景。接着,通过两个具体的例子来展示如何将RTC时钟的时间转换为标准时间进行显示。这两个例子都包含了详细的代码注释和思路解析,帮助读者更好地理解和掌握这种转换方法。文章知识点:标准时间与秒单位概念介绍:解释标准时间和秒单位的定义及其在嵌入式单片机中的应用。RTC时钟时间读取:演示如何从RTC时钟中读取秒单位时间,并将其转换为标准时间进行显示。代码注释和思路解析:提供详细注释和思路解析,帮助读者理解代码的实现过程。示例代码演示:给出两个具体的示例代码,展示如何在不同场景下进行标准时间与秒单位的转换。1. 时间转换-秒与标准时间的转换1.时间转换(秒与标准时间的转换) (1)函数1: 将秒单位时间转为标准时间。 --RTC实时时钟--->秒为单位--->每秒钟cnt++; 237562867493 -----xxxx年xx月xx日xx时xx分xx秒 星期x。示例代码: (模拟电子钟)​#include <stdio.h>#include <string.h> //字符串处理#include <Windows.h> ​//时间单位int year,mon,mdeay,hour,min,t_sec;​//闰年的月份int mon_r[12]={31,29,31,30,31,30,31,31,30,31,30,31};​//平年的月份int mon_p[12]={31,28,31,30,31,30,31,31,30,31,30,31};​unsigned int TimeToSec(int year,int mon,int mdeay,int hour,int min,int sec);void SecToTime(unsigned int sec);​​int main(void){ //将标准时间转为秒单位时间 (设置时间) unsigned int time=TimeToSec(2018,6,6,16,40,20); while(1) { time++; Sleep(1000); //睡眠1秒时间。 单位是ms SecToTime(time); printf("%d-%d-%d %d:%d:%d\n",year,mon,mdeay,hour,min,t_sec); } return 0;}​/*函数功能: 判断平年和闰年函数返回值: 1表示闰年 0表示平年*/int GetYearStat(int year){ if((year%4==0&&year%100!=0)||year%400==0)return 1; //闰年 return 0; //平年}​/*将秒单位时间转为标准时间时间基准点: 1970年1月1日0时0分0秒思想: 减法*/void SecToTime(unsigned int sec){ int i; year=1970; //基准年份 /*1. 计算过了多少年*/ while(sec>=365*24*60*60) //秒还够一年 { if(GetYearStat(year)) //闰年 { if(sec>366*24*60*60) { sec-=366*24*60*60; //减去一年 year++; //年份累加 } else { break; } } else { sec-=365*24*60*60; //减去一年 year++; //年份累加 } }​ /*2. 计算过了多少月*/ mon=1; if(GetYearStat(year)) //闰年 { for(i=0; i<12; i++) { if(sec>=mon_r[i]*24*60*60) //够一个月 { sec-=mon_r[i]*24*60*60; //减去一个月 mon++;//增加一个月 } else break; } } else { for(i=0; i<12; i++) { if(sec>=mon_p[i]*24*60*60) //够一个月 { sec-=mon_p[i]*24*60*60; //减去一个月 mon++;//增加一个月 } else break; } } /*3. 计算过了多少天*/ mdeay=1; while(sec>=24*60*60) //判断是否够一天 { sec-=24*60*60; mdeay++; }​ /*4. 过了多少小时*/ hour=0; while(sec>=60*60) { sec-=60*60; hour++; } /*5. 过了多少分钟*/ min=0; while(sec>=60) { sec-=60; min++; } /*6. 过了多少秒*/ t_sec=sec; }​/*将标准时间转为秒单位时间思路: 全程加法时间基准点: 1970年1月1日0时0分0秒返回值: 得到的秒单位时间*/unsigned int TimeToSec(int year,int mon,int mdeay,int hour,int min,int sec){ int i; int sec_cnt=0; //记录秒单位的时间 /*1. 转换年*/ for(i=1970; i<year; i++) { if(GetYearStat(i)) //闰年 { sec_cnt+=366*24*60*60; } else { sec_cnt+=365*24*60*60; } }​ /*2. 转换月*/ for(i=0; i<mon-1; i++) { if(GetYearStat(year)) //闰年 { sec_cnt+=mon_r[i]*24*60*60; } else { sec_cnt+=mon_p[i]*24*60*60; } }​ /*3. 转换天数*/ sec_cnt+=(mdeay-1)*24*60*60; /*4. 转换小时*/ sec_cnt+=hour*60*60;​ /*5. 转换分钟*/ sec_cnt+=min*60;​ /*6. 转换秒*/ sec_cnt+=sec; return sec_cnt; //返回秒单位时间}​​完整的代码(添加星期):#include <stdio.h>#include <string.h> //字符串处理#include <Windows.h> ​//时间单位int year,mon,mdeay,hour,min,t_sec,week;​//闰年的月份int mon_r[12]={31,29,31,30,31,30,31,31,30,31,30,31};​//平年的月份int mon_p[12]={31,28,31,30,31,30,31,31,30,31,30,31};​unsigned int TimeToSec(int year,int mon,int mdeay,int hour,int min,int sec);void SecToTime(unsigned int sec);int GetWeek(unsigned int sec);​int main(void){ //将标准时间转为秒单位时间 (设置时间) unsigned int time=TimeToSec(2018,9,1,16,40,20); while(1) { time++; Sleep(1000); //睡眠1秒时间。 单位是ms SecToTime(time); week=GetWeek(time); //获取星期 printf("%d-%d-%d %d:%d:%d 星期%d\n",year,mon,mdeay,hour,min,t_sec,week); } return 0;}​​/*函数功能: 判断平年和闰年函数返回值: 1表示闰年 0表示平年*/int GetYearStat(int year){ if((year%4==0&&year%100!=0)||year%400==0)return 1; //闰年 return 0; //平年}​/*将秒单位时间转为标准时间时间基准点: 1970年1月1日0时0分0秒思想: 减法*/void SecToTime(unsigned int sec){ int i; year=1970; //基准年份 /*1. 计算过了多少年*/ while(sec>=365*24*60*60) //秒还够一年 { if(GetYearStat(year)) //闰年 { if(sec>366*24*60*60) { sec-=366*24*60*60; //减去一年 year++; //年份累加 } else { break; } } else { sec-=365*24*60*60; //减去一年 year++; //年份累加 } }​ /*2. 计算过了多少月*/ mon=1; if(GetYearStat(year)) //闰年 { for(i=0; i<12; i++) { if(sec>=mon_r[i]*24*60*60) //够一个月 { sec-=mon_r[i]*24*60*60; //减去一个月 mon++;//增加一个月 } else break; } } else { for(i=0; i<12; i++) { if(sec>=mon_p[i]*24*60*60) //够一个月 { sec-=mon_p[i]*24*60*60; //减去一个月 mon++;//增加一个月 } else break; } } /*3. 计算过了多少天*/ mdeay=1; while(sec>=24*60*60) //判断是否够一天 { sec-=24*60*60; mdeay++; }​ /*4. 过了多少小时*/ hour=0; while(sec>=60*60) { sec-=60*60; hour++; } /*5. 过了多少分钟*/ min=0; while(sec>=60) { sec-=60; min++; } /*6. 过了多少秒*/ t_sec=sec; }2. 时间转换-标准时间转秒(2)函数2: 将标准时间转为秒单位的时间。 2018年6月1日19点41分23秒----------xxxxxxx秒 闰年366,平年365。 区分: 每年二月份相差一天. 标准时间基准点: 1970年1月1日0时0分0秒。/*将标准时间转为秒单位时间思路: 全程加法时间基准点: 1970年1月1日0时0分0秒返回值: 得到的秒单位时间*/unsigned int TimeToSec(int year,int mon,int mdeay,int hour,int min,int sec){ int i; int sec_cnt=0; //记录秒单位的时间 /*1. 转换年*/ for(i=1970; i<year; i++) { if(GetYearStat(i)) //闰年 { sec_cnt+=366*24*60*60; } else { sec_cnt+=365*24*60*60; } }​ /*2. 转换月*/ for(i=0; i<mon-1; i++) { if(GetYearStat(year)) //闰年 { sec_cnt+=mon_r[i]*24*60*60; } else { sec_cnt+=mon_p[i]*24*60*60; } }​ /*3. 转换天数*/ sec_cnt+=(mdeay-1)*24*60*60; /*4. 转换小时*/ sec_cnt+=hour*60*60;​ /*5. 转换分钟*/ sec_cnt+=min*60;​ /*6. 转换秒*/ sec_cnt+=sec; return sec_cnt;}​/*函数功能: 根据秒单位时间获取星期函数形参: 秒单位时间返回值 :星期(1~7)*/int GetWeek(unsigned int sec){ int mdeay=sec/60/60/24; //将秒单位时间转为天数 switch(mdeay%7) { case 0: //星期4 return 4; break; case 1://星期5 return 5; break; case 2: //星期6 return 6; break; case 3://星期天 return 7; break; case 4://星期1 return 1; break; case 5://星期2 return 2; break; case 6: //星期3 return 3; break; default: break; }}
  • [技术干货] STM32控制max30102读取血氧心率数据(keil5工程)
    一、前言MAX30102是一款由Maxim Integrated推出的低功耗、高精度的心率和血氧饱和度检测传感器模块,适用于可穿戴设备如智能手环、智能手表等健康管理类电子产品。该传感器主要特性如下:(1)光学测量:MAX30102内置了两个LED光源(红光和红外光),以及一个光电检测器,通过光电容积脉搏波描记法(PPG)来实现心率和血氧饱和度的无创检测。(2)低功耗:在典型的工作模式下,其功耗非常低,有助于延长电池供电设备的使用寿命。(3)集成度高:内部集成了AFE(模拟前端)、LED驱动器、环境光抑制功能以及I²C数字接口,方便与微控制器连接通信。(4)多档位配置:支持多个LED电流输出级别和采样速率选择,可以根据实际应用需求进行灵活配置。(5)高精度:通过先进的信号处理算法,可以有效降低噪声干扰,提高测量数据的准确性。(6)小尺寸封装:采用紧凑型封装设计,便于在空间受限的产品中使用。MAX30102是一款高性能的生物医学传感器,能够帮助开发者在各种便携式和穿戴式设备上实现对人体生理参数的有效监测。二、IIC协议MAX30102 是一款由 Maxim Integrated(现为 Analog Devices 公司的一部分)制造的生物识别传感器,它采用 I2C(Inter-Integrated Circuit)协议进行通信。I2C 协议是一种常见的串行接口标准,特别适用于在嵌入式系统中连接微控制器和其他低速周边设备,如传感器、EEPROM、RTC(实时时钟)等。I2C 协议详解:(1)架构与线路:SDA (Serial Data Line): 串行数据线,用于传输数据。SCL (Serial Clock Line): 串行时钟线,由主设备控制,决定数据传输速率和每个位的时间间隔。多主从架构: 支持一个主设备和多个从设备同时连接到总线上,主设备负责发起通信并控制数据传输方向。(2)信号特性:开始条件(Start Condition): 当 SDA 线在 SCL 高电平时由高电平变为低电平,表示一次传输的开始。停止条件(Stop Condition): 反之,在 SCL 高电平时,SDA 线由低电平变为高电平,标志一次传输结束。地址字节: 每次通信开始时,主设备会通过发送包含7位从设备地址(加上一位读写位)的数据包来寻址目标从设备,例如 MAX30102。(3)数据传输:读/写操作: 地址字节的最低位决定了接下来是读操作(R/W=1)还是写操作(R/W=0)。应答(ACK/NACK): 每个被传送的数据字节后,接收方需拉低 SDA 行线以发出一个确认(ACK)信号。若不响应,则为主动非应答(NACK),可能用于指示传输结束或错误。数据位传输: 数据以高位先出(MSB-first)的方式逐位传输。(4)波特率:I2C 协议允许不同的传输速率,称为标准模式(100kHz)、快速模式(400kHz)、快速模式+(1MHz)以及其他更高性能的模式。对于MAX30102这样的传感器来说,通过I2C接口可以读取其内部寄存器数据,如配置寄存器、状态寄存器以及测量数据缓冲区等,从而实现对传感器的控制和数据采集。开发人员通常使用微控制器提供的硬件I2C模块或者软件模拟的I2C协议来与MAX30102进行通信。模拟I2C协议通常涉及到对硬件时序的精确控制,以下是一个基于软件模拟的、简化版的C语言代码示例,用于演示基本原理。#include <stdio.h>#include <unistd.h>​// 假设sda和scl是连接到GPIO的文件描述符#define SDA 3#define SCL 4​// 设置GPIO为输出模式void gpio_setup_output(int pin) { // 这部分代码依赖于具体的GPIO库或系统调用,这里仅为示意}​// 设置GPIO为输入模式并读取电平int gpio_read_input(int pin) { // 这部分代码依赖于具体的GPIO库或系统调用,这里仅为示意 return value; // 返回0或1}​// 模拟SDA线上的数据传输void sda_write(int data) { gpio_setup_output(SDA); if (data) // 将SDA置高 ; else // 将SDA置低 ;}​// 模拟SCL线上的时钟脉冲void scl_pulse(void) { gpio_setup_output(SCL); // 将SCL拉低 usleep(1); // 延迟以模拟时钟周期的一部分 // 将SCL拉高 usleep(1); // 延迟以完成时钟周期}​// 发送一个字节数据void i2c_send_byte(unsigned char byte) { for (int i = 7; i >= 0; --i) { sda_write(byte & (1 << i)); scl_pulse(); } // 等待ACK gpio_setup_output(SDA); gpio_write(SDA, 1); // 主机释放SDA,从机应答 scl_pulse(); if (gpio_read_input(SDA)) { printf("No ACK received\n"); // 处理无应答的情况... }}​// 接收一个字节数据unsigned char i2c_receive_byte(int ack) { unsigned char byte = 0; gpio_setup_input(SDA); for (int i = 7; i >= 0; --i) { byte <<= 1; scl_pulse(); byte |= gpio_read_input(SDA); } gpio_setup_output(SDA); // 发送ACK/NAK sda_write(!ack); scl_pulse(); return byte;}​// I2C开始条件void i2c_start_condition(void) { sda_write(1); scl_write(1); sda_write(0);}​// I2C停止条件void i2c_stop_condition(void) { sda_write(0); scl_write(1); sda_write(1);}​// 向设备发送地址和数据void i2c_send_address_and_data(unsigned char address, unsigned char data, int is_write) { i2c_start_condition(); i2c_send_byte((address << 1) | (is_write ? 0 : 1)); // 地址 + R/W位 i2c_send_byte(data); // 数据 i2c_stop_condition();}​// 从设备接收数据unsigned char i2c_receive_data(unsigned char address) { i2c_start_condition(); i2c_send_byte((address << 1) | 1); // 地址 + R/W=1(读操作) unsigned char data = i2c_receive_byte(0); // 接收数据并发送ACK i2c_stop_condition(); return data;}三、项目代码下面贴出了STM32工程里完整的max30102的代码,因为是才有寄存器编程。 所有兼容所有的工程,不管你是STM32标准库工程还是STM32HAL库工程,只要把下面的.c文件和.h文件加载到你的STM32工程里。将max30102接好线,按照头文件里说明调用mainx30102函数完成初始化就可以。3.1 max30102.c#include "max30102.h"#include "delay.h"​/*MAX30102心率传感器:SCL<->PB6SDA<->PB7IM<->PB9*/​​//初始化IICvoid IIC_Init(void){ RCC->APB2ENR|=1<<3; GPIOB->CRL&=0x00FFFFFF; GPIOB->CRL|=0x33000000; GPIOB->CRH&=0xFFFFFF0F; GPIOB->CRH|=0x00000080; IIC_SCL=1; IIC_SDA=1;​}​​//产生IIC起始信号void IIC_Start(void){ SDA_OUT(); //sda线输出 IIC_SDA=1; IIC_SCL=1; delay_us(4); IIC_SDA=0;//START:when CLK is high,DATA change form high to low delay_us(4); IIC_SCL=0;//钳住I2C总线,准备发送或接收数据 } //产生IIC停止信号void IIC_Stop(void){ SDA_OUT();//sda线输出 IIC_SCL=0; IIC_SDA=0;//STOP:when CLK is high DATA change form low to high delay_us(4); IIC_SCL=1; IIC_SDA=1;//发送I2C总线结束信号 delay_us(4); }//等待应答信号到来//返回值:1,接收应答失败// 0,接收应答成功u8 IIC_Wait_Ack(void){ u8 ucErrTime=0; SDA_IN(); //SDA设置为输入 IIC_SDA=1;delay_us(1); IIC_SCL=1;delay_us(1); while(READ_SDA) { ucErrTime++; if(ucErrTime>250) { IIC_Stop(); return 1; } } IIC_SCL=0;//时钟输出0 return 0; } //产生ACK应答void IIC_Ack(void){ IIC_SCL=0; SDA_OUT(); IIC_SDA=0; delay_us(2); IIC_SCL=1; delay_us(2); IIC_SCL=0;}//不产生ACK应答 void IIC_NAck(void){ IIC_SCL=0; SDA_OUT(); IIC_SDA=1; delay_us(2); IIC_SCL=1; delay_us(2); IIC_SCL=0;} //IIC发送一个字节//返回从机有无应答//1,有应答//0,无应答 void IIC_Send_Byte(u8 txd){ u8 t; SDA_OUT(); IIC_SCL=0;//拉低时钟开始数据传输 for(t=0;t<8;t++) { IIC_SDA=(txd&0x80)>>7; txd<<=1; delay_us(2); //对TEA5767这三个延时都是必须的 IIC_SCL=1; delay_us(2); IIC_SCL=0; delay_us(2); } } //读1个字节,ack=1时,发送ACK,ack=0,发送nACK u8 IIC_Read_Byte(unsigned char ack){ unsigned char i,receive=0; SDA_IN();//SDA设置为输入 for(i=0;i<8;i++ ) { IIC_SCL=0; delay_us(2); IIC_SCL=1; receive<<=1; if(READ_SDA)receive++; delay_us(1); } if (!ack) IIC_NAck();//发送nACK else IIC_Ack(); //发送ACK return receive;}​​void IIC_WriteBytes(u8 WriteAddr,u8* data,u8 dataLength){ u8 i; IIC_Start(); ​ IIC_Send_Byte(WriteAddr); //发送写命令 IIC_Wait_Ack(); for(i=0;i<dataLength;i++) { IIC_Send_Byte(data[i]); IIC_Wait_Ack(); } IIC_Stop();//产生一个停止条件 delay_ms(10); }​void IIC_ReadBytes(u8 deviceAddr, u8 writeAddr,u8* data,u8 dataLength){ u8 i; IIC_Start(); ​ IIC_Send_Byte(deviceAddr); //发送写命令 IIC_Wait_Ack(); IIC_Send_Byte(writeAddr); IIC_Wait_Ack(); IIC_Send_Byte(deviceAddr|0X01);//进入接收模式 IIC_Wait_Ack(); for(i=0;i<dataLength-1;i++) { data[i] = IIC_Read_Byte(1); } data[dataLength-1] = IIC_Read_Byte(0); IIC_Stop();//产生一个停止条件 delay_ms(10); }​void IIC_Read_One_Byte(u8 daddr,u8 addr,u8* data){ IIC_Start(); IIC_Send_Byte(daddr); //发送写命令 IIC_Wait_Ack(); IIC_Send_Byte(addr);//发送地址 IIC_Wait_Ack(); IIC_Start(); IIC_Send_Byte(daddr|0X01);//进入接收模式 IIC_Wait_Ack(); *data = IIC_Read_Byte(0); IIC_Stop();//产生一个停止条件 }​void IIC_Write_One_Byte(u8 daddr,u8 addr,u8 data){ IIC_Start(); IIC_Send_Byte(daddr); //发送写命令 IIC_Wait_Ack(); IIC_Send_Byte(addr);//发送地址 IIC_Wait_Ack(); IIC_Send_Byte(data); //发送字节 IIC_Wait_Ack(); IIC_Stop();//产生一个停止条件 delay_ms(10); }​​​​​​​​​​ uint32_t aun_ir_buffer[500]; //IR LED sensor dataint32_t n_ir_buffer_length; //data lengthuint32_t aun_red_buffer[500]; //Red LED sensor dataint32_t n_sp02; //SPO2 valueint8_t ch_spo2_valid; //indicator to show if the SP02 calculation is validint32_t n_heart_rate; //heart rate valueint8_t ch_hr_valid; //indicator to show if the heart rate calculation is validuint8_t uch_dummy;​​//variables to calculate the on-board LED brightness that reflects the heartbeatsuint32_t un_min, un_max, un_prev_data; int i;int32_t n_brightness;float f_temp;u8 temp_num=0;u8 temp[6];u8 str[100];u8 dis_hr=0,dis_spo2=0; #define MAX_BRIGHTNESS 255​​​u8 max30102_Bus_Write(u8 Register_Address, u8 Word_Data){​ /* 采用串行EEPROM随即读取指令序列,连续读取若干字节 */​ /* 第1步:发起I2C总线启动信号 */ IIC_Start();​ /* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */ IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */​ /* 第3步:发送ACK */ if (IIC_Wait_Ack() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ }​ /* 第4步:发送字节地址 */ IIC_Send_Byte(Register_Address); if (IIC_Wait_Ack() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ } /* 第5步:开始写入数据 */ IIC_Send_Byte(Word_Data);​ /* 第6步:发送ACK */ if (IIC_Wait_Ack() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ }​ /* 发送I2C总线停止信号 */ IIC_Stop(); return 1; /* 执行成功 */​cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */ /* 发送I2C总线停止信号 */ IIC_Stop(); return 0;}​​​u8 max30102_Bus_Read(u8 Register_Address){ u8 data;​​ /* 第1步:发起I2C总线启动信号 */ IIC_Start();​ /* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */ IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */​ /* 第3步:发送ACK */ if (IIC_Wait_Ack() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ }​ /* 第4步:发送字节地址, */ IIC_Send_Byte((uint8_t)Register_Address); if (IIC_Wait_Ack() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ } ​ /* 第6步:重新启动I2C总线。下面开始读取数据 */ IIC_Start();​ /* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */ IIC_Send_Byte(max30102_WR_address | I2C_RD); /* 此处是读指令 */​ /* 第8步:发送ACK */ if (IIC_Wait_Ack() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ }​ /* 第9步:读取数据 */ { data = IIC_Read_Byte(0); /* 读1个字节 */​ IIC_NAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */ } /* 发送I2C总线停止信号 */ IIC_Stop(); return data; /* 执行成功 返回data值 */​cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */ /* 发送I2C总线停止信号 */ IIC_Stop(); return 0;}​​void max30102_FIFO_ReadWords(u8 Register_Address,u16 Word_Data[][2],u8 count){ u8 i=0; u8 no = count; u8 data1, data2; /* 第1步:发起I2C总线启动信号 */ IIC_Start();​ /* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */ IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */​ /* 第3步:发送ACK */ if (IIC_Wait_Ack() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ }​ /* 第4步:发送字节地址, */ IIC_Send_Byte((uint8_t)Register_Address); if (IIC_Wait_Ack() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ } ​ /* 第6步:重新启动I2C总线。下面开始读取数据 */ IIC_Start();​ /* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */ IIC_Send_Byte(max30102_WR_address | I2C_RD); /* 此处是读指令 */​ /* 第8步:发送ACK */ if (IIC_Wait_Ack() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ }​ /* 第9步:读取数据 */ while (no) { data1 = IIC_Read_Byte(0); IIC_Ack(); data2 = IIC_Read_Byte(0); IIC_Ack(); Word_Data[i][0] = (((u16)data1 << 8) | data2); //​ data1 = IIC_Read_Byte(0); IIC_Ack(); data2 = IIC_Read_Byte(0); if(1==no) IIC_NAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */ else IIC_Ack(); Word_Data[i][1] = (((u16)data1 << 8) | data2); ​ no--; i++; } /* 发送I2C总线停止信号 */ IIC_Stop();​cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */ /* 发送I2C总线停止信号 */ IIC_Stop();}​void max30102_FIFO_ReadBytes(u8 Register_Address,u8* Data){ max30102_Bus_Read(REG_INTR_STATUS_1); max30102_Bus_Read(REG_INTR_STATUS_2); /* 第1步:发起I2C总线启动信号 */ IIC_Start();​ /* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */ IIC_Send_Byte(max30102_WR_address | I2C_WR); /* 此处是写指令 */​ /* 第3步:发送ACK */ if (IIC_Wait_Ack() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ }​ /* 第4步:发送字节地址, */ IIC_Send_Byte((uint8_t)Register_Address); if (IIC_Wait_Ack() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ } ​ /* 第6步:重新启动I2C总线。下面开始读取数据 */ IIC_Start();​ /* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */ IIC_Send_Byte(max30102_WR_address | I2C_RD); /* 此处是读指令 */​ /* 第8步:发送ACK */ if (IIC_Wait_Ack() != 0) { goto cmd_fail; /* EEPROM器件无应答 */ }​ /* 第9步:读取数据 */ Data[0] = IIC_Read_Byte(1); Data[1] = IIC_Read_Byte(1); Data[2] = IIC_Read_Byte(1); Data[3] = IIC_Read_Byte(1); Data[4] = IIC_Read_Byte(1); Data[5] = IIC_Read_Byte(0); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */ /* 发送I2C总线停止信号 */ IIC_Stop();​cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */ /* 发送I2C总线停止信号 */ IIC_Stop();​// u8 i;// u8 fifo_wr_ptr;// u8 firo_rd_ptr;// u8 number_tp_read;// //Get the FIFO_WR_PTR// fifo_wr_ptr = max30102_Bus_Read(REG_FIFO_WR_PTR);// //Get the FIFO_RD_PTR// firo_rd_ptr = max30102_Bus_Read(REG_FIFO_RD_PTR);// // number_tp_read = fifo_wr_ptr - firo_rd_ptr;// // //for(i=0;i<number_tp_read;i++){// if(number_tp_read>0){// IIC_ReadBytes(max30102_WR_address,REG_FIFO_DATA,Data,6);// } //max30102_Bus_Write(REG_FIFO_RD_PTR,fifo_wr_ptr);}​void max30102_init(void){ IIC_Init(); max30102_reset(); // max30102_Bus_Write(REG_MODE_CONFIG, 0x0b); //mode configuration : temp_en[3] MODE[2:0]=010 HR only enabled 011 SP02 enabled// max30102_Bus_Write(REG_INTR_STATUS_2, 0xF0); //open all of interrupt// max30102_Bus_Write(REG_INTR_STATUS_1, 0x00); //all interrupt clear// max30102_Bus_Write(REG_INTR_ENABLE_2, 0x02); //DIE_TEMP_RDY_EN// max30102_Bus_Write(REG_TEMP_CONFIG, 0x01); //SET TEMP_EN​// max30102_Bus_Write(REG_SPO2_CONFIG, 0x47); //SPO2_SR[4:2]=001 100 per second LED_PW[1:0]=11 16BITS​// max30102_Bus_Write(REG_LED1_PA, 0x47); // max30102_Bus_Write(REG_LED2_PA, 0x47); max30102_Bus_Write(REG_INTR_ENABLE_1,0xc0); // INTR setting max30102_Bus_Write(REG_INTR_ENABLE_2,0x00); max30102_Bus_Write(REG_FIFO_WR_PTR,0x00); //FIFO_WR_PTR[4:0] max30102_Bus_Write(REG_OVF_COUNTER,0x00); //OVF_COUNTER[4:0] max30102_Bus_Write(REG_FIFO_RD_PTR,0x00); //FIFO_RD_PTR[4:0] max30102_Bus_Write(REG_FIFO_CONFIG,0x0f); //sample avg = 1, fifo rollover=false, fifo almost full = 17 max30102_Bus_Write(REG_MODE_CONFIG,0x03); //0x02 for Red only, 0x03 for SpO2 mode 0x07 multimode LED max30102_Bus_Write(REG_SPO2_CONFIG,0x27); // SPO2_ADC range = 4096nA, SPO2 sample rate (100 Hz), LED pulseWidth (400uS) max30102_Bus_Write(REG_LED1_PA,0x24); //Choose value for ~ 7mA for LED1 max30102_Bus_Write(REG_LED2_PA,0x24); // Choose value for ~ 7mA for LED2 max30102_Bus_Write(REG_PILOT_PA,0x7f); // Choose value for ~ 25mA for Pilot LED​​ // // Interrupt Enable 1 Register. Set PPG_RDY_EN (data available in FIFO)// max30102_Bus_Write(0x2, 1<<6);​// // FIFO configuration register// // SMP_AVE: 16 samples averaged per FIFO sample// // FIFO_ROLLOVER_EN=1// //max30102_Bus_Write(0x8, 1<<4);// max30102_Bus_Write(0x8, (0<<5) | 1<<4);​// // Mode Configuration Register// // SPO2 mode// max30102_Bus_Write(0x9, 3);​// // SPO2 Configuration Register// max30102_Bus_Write(0xa,// (3<<5) // SPO2_ADC_RGE 2 = full scale 8192 nA (LSB size 31.25pA); 3 = 16384nA// | (1<<2) // sample rate: 0 = 50sps; 1 = 100sps; 2 = 200sps// | (3<<0) // LED_PW 3 = 411μs, ADC resolution 18 bits// );​// // LED1 (red) power (0 = 0mA; 255 = 50mA)// max30102_Bus_Write(0xc, 0xb0);​// // LED (IR) power// max30102_Bus_Write(0xd, 0xa0); }​void max30102_reset(void){ max30102_Bus_Write(REG_MODE_CONFIG,0x40); max30102_Bus_Write(REG_MODE_CONFIG,0x40);}​​​​​​void maxim_max30102_write_reg(uint8_t uch_addr, uint8_t uch_data){// char ach_i2c_data[2];// ach_i2c_data[0]=uch_addr;// ach_i2c_data[1]=uch_data;// // IIC_WriteBytes(I2C_WRITE_ADDR, ach_i2c_data, 2); IIC_Write_One_Byte(I2C_WRITE_ADDR,uch_addr,uch_data);}​void maxim_max30102_read_reg(uint8_t uch_addr, uint8_t *puch_data){// char ch_i2c_data;// ch_i2c_data=uch_addr;// IIC_WriteBytes(I2C_WRITE_ADDR, &ch_i2c_data, 1);// // i2c.read(I2C_READ_ADDR, &ch_i2c_data, 1);// // *puch_data=(uint8_t) ch_i2c_data; IIC_Read_One_Byte(I2C_WRITE_ADDR,uch_addr,puch_data);}​void maxim_max30102_read_fifo(uint32_t *pun_red_led, uint32_t *pun_ir_led){ uint32_t un_temp; unsigned char uch_temp; char ach_i2c_data[6]; *pun_red_led=0; *pun_ir_led=0;​ //read and clear status register maxim_max30102_read_reg(REG_INTR_STATUS_1, &uch_temp); maxim_max30102_read_reg(REG_INTR_STATUS_2, &uch_temp); IIC_ReadBytes(I2C_WRITE_ADDR,REG_FIFO_DATA,(u8 *)ach_i2c_data,6); un_temp=(unsigned char) ach_i2c_data[0]; un_temp<<=16; *pun_red_led+=un_temp; un_temp=(unsigned char) ach_i2c_data[1]; un_temp<<=8; *pun_red_led+=un_temp; un_temp=(unsigned char) ach_i2c_data[2]; *pun_red_led+=un_temp; un_temp=(unsigned char) ach_i2c_data[3]; un_temp<<=16; *pun_ir_led+=un_temp; un_temp=(unsigned char) ach_i2c_data[4]; un_temp<<=8; *pun_ir_led+=un_temp; un_temp=(unsigned char) ach_i2c_data[5]; *pun_ir_led+=un_temp; *pun_red_led&=0x03FFFF; //Mask MSB [23:18] *pun_ir_led&=0x03FFFF; //Mask MSB [23:18]}​​void dis_DrawCurve(u32* data,u8 x){ u16 i; u32 max=0,min=262144; u32 temp; u32 compress; for(i=0;i<128*2;i++) { if(data[i]>max) { max = data[i]; } if(data[i]<min) { min = data[i]; } } compress = (max-min)/20; for(i=0;i<128;i++) { temp = data[i*2] + data[i*2+1]; temp/=2; temp -= min; temp/=compress; if(temp>20)temp=20; }}​void MAX30102_data_set(){// printf("\r\n MAX30102 init \r\n"); un_min=0x3FFFF; un_max=0; n_ir_buffer_length=500; //buffer length of 100 stores 5 seconds of samples running at 100sps //read the first 500 samples, and determine the signal range for(i=0;i<n_ir_buffer_length;i++) { while(MAX30102_INT==1); //wait until the interrupt pin asserts// max30102_FIFO_ReadBytes(REG_FIFO_DATA,temp); aun_red_buffer[i] = (long)((long)((long)temp[0]&0x03)<<16) | (long)temp[1]<<8 | (long)temp[2]; // Combine values to get the actual number aun_ir_buffer[i] = (long)((long)((long)temp[3] & 0x03)<<16) |(long)temp[4]<<8 | (long)temp[5]; // Combine values to get the actual number if(un_min>aun_red_buffer[i]) un_min=aun_red_buffer[i]; //update signal min if(un_max<aun_red_buffer[i]) un_max=aun_red_buffer[i]; //update signal max } un_prev_data=aun_red_buffer[i]; //calculate heart rate and SpO2 after first 500 samples (first 5 seconds of samples) maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid); }​​​void MAX30102_get(u8 *hr,u8 *spo2){ i=0; un_min=0x3FFFF; un_max=0; //dumping the first 100 sets of samples in the memory and shift the last 400 sets of samples to the top for(i=100;i<500;i++) { aun_red_buffer[i-100]=aun_red_buffer[i]; aun_ir_buffer[i-100]=aun_ir_buffer[i]; //update the signal min and max if(un_min>aun_red_buffer[i]) un_min=aun_red_buffer[i]; if(un_max<aun_red_buffer[i]) un_max=aun_red_buffer[i]; } //take 100 sets of samples before calculating the heart rate. for(i=400;i<500;i++) { un_prev_data=aun_red_buffer[i-1];// while(MAX30102_INT==1); max30102_FIFO_ReadBytes(REG_FIFO_DATA,temp); aun_red_buffer[i] = (long)((long)((long)temp[0]&0x03)<<16) | (long)temp[1]<<8 | (long)temp[2]; // Combine values to get the actual number aun_ir_buffer[i] = (long)((long)((long)temp[3] & 0x03)<<16) |(long)temp[4]<<8 | (long)temp[5]; // Combine values to get the actual number if(aun_red_buffer[i]>un_prev_data) { f_temp=aun_red_buffer[i]-un_prev_data; f_temp/=(un_max-un_min); f_temp*=MAX_BRIGHTNESS; n_brightness-=(int)f_temp; if(n_brightness<0) n_brightness=0; } else { f_temp=un_prev_data-aun_red_buffer[i]; f_temp/=(un_max-un_min); f_temp*=MAX_BRIGHTNESS; n_brightness+=(int)f_temp; if(n_brightness>MAX_BRIGHTNESS) n_brightness=MAX_BRIGHTNESS; } //send samples and calculation result to terminal program through UART if(ch_hr_valid == 1 && n_heart_rate<120 && ch_spo2_valid == 1 && n_sp02<101)//**/ ch_hr_valid == 1 && ch_spo2_valid ==1 && n_heart_rate<120 && n_sp02<101 { dis_hr = n_heart_rate; dis_spo2 = n_sp02; }// else// {// dis_hr = 0;// dis_spo2 = 0;// }// printf("HR=%i, ", dis_hr); // printf("HRvalid=%i, ", ch_hr_valid);// printf("SpO2=%i, ", dis_spo2);// printf("SPO2Valid=%i\r\n", ch_spo2_valid); *hr = dis_hr; *spo2 = dis_spo2; } maxim_heart_rate_and_oxygen_saturation(aun_ir_buffer, n_ir_buffer_length, aun_red_buffer, &n_sp02, &ch_spo2_valid, &n_heart_rate, &ch_hr_valid); //红光在上,红外在下 dis_DrawCurve(aun_red_buffer,20); dis_DrawCurve(aun_ir_buffer,0);​}​​/** \file algorithm.c ******************************************************** Project: MAXREFDES117#* Filename: algorithm.cpp* Description: This module calculates the heart rate/SpO2 level*** --------------------------------------------------------------------** This code follows the following naming conventions:** char ch_pmod_value* char (array) s_pmod_s_string[16]* float f_pmod_value* int32_t n_pmod_value* int32_t (array) an_pmod_value[16]* int16_t w_pmod_value* int16_t (array) aw_pmod_value[16]* uint16_t uw_pmod_value* uint16_t (array) auw_pmod_value[16]* uint8_t uch_pmod_value* uint8_t (array) auch_pmod_buffer[16]* uint32_t un_pmod_value* int32_t * pn_pmod_value** ------------------------------------------------------------------------- *//******************************************************************************** Copyright (C) 2016 Maxim Integrated Products, Inc., All Rights Reserved.** Permission is hereby granted, free of charge, to any person obtaining a* copy of this software and associated documentation files (the "Software"),* to deal in the Software without restriction, including without limitation* the rights to use, copy, modify, merge, publish, distribute, sublicense,* and/or sell copies of the Software, and to permit persons to whom the* Software is furnished to do so, subject to the following conditions:** The above copyright notice and this permission notice shall be included* in all copies or substantial portions of the Software.** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.* IN NO EVENT SHALL MAXIM INTEGRATED BE LIABLE FOR ANY CLAIM, DAMAGES* OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR* OTHER DEALINGS IN THE SOFTWARE.** Except as contained in this notice, the name of Maxim Integrated* Products, Inc. shall not be used except as stated in the Maxim Integrated* Products, Inc. Branding Policy.** The mere transfer of this software does not imply any licenses* of trade secrets, proprietary technology, copyrights, patents,* trademarks, maskwork rights, or any other form of intellectual* property whatsoever. Maxim Integrated Products, Inc. retains all* ownership rights.********************************************************************************/​const uint16_t auw_hamm[31]={ 41, 276, 512, 276, 41 }; //Hamm= long16(512* hamming(5)');//uch_spo2_table is computed as -45.060*ratioAverage* ratioAverage + 30.354 *ratioAverage + 94.845 ;const uint8_t uch_spo2_table[184]={ 95, 95, 95, 96, 96, 96, 97, 97, 97, 97, 97, 98, 98, 98, 98, 98, 99, 99, 99, 99, 99, 99, 99, 99, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 99, 99, 99, 99, 99, 99, 99, 99, 98, 98, 98, 98, 98, 98, 97, 97, 97, 97, 96, 96, 96, 96, 95, 95, 95, 94, 94, 94, 93, 93, 93, 92, 92, 92, 91, 91, 90, 90, 89, 89, 89, 88, 88, 87, 87, 86, 86, 85, 85, 84, 84, 83, 82, 82, 81, 81, 80, 80, 79, 78, 78, 77, 76, 76, 75, 74, 74, 73, 72, 72, 71, 70, 69, 69, 68, 67, 66, 66, 65, 64, 63, 62, 62, 61, 60, 59, 58, 57, 56, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 31, 30, 29, 28, 27, 26, 25, 23, 22, 21, 20, 19, 17, 16, 15, 14, 12, 11, 10, 9, 7, 6, 5, 3, 2, 1 } ;static int32_t an_dx[ BUFFER_SIZE-MA4_SIZE]; // deltastatic int32_t an_x[ BUFFER_SIZE]; //irstatic int32_t an_y[ BUFFER_SIZE]; //red​void maxim_heart_rate_and_oxygen_saturation(uint32_t *pun_ir_buffer, int32_t n_ir_buffer_length, uint32_t *pun_red_buffer, int32_t *pn_spo2, int8_t *pch_spo2_valid, int32_t *pn_heart_rate, int8_t *pch_hr_valid)/*** \brief Calculate the heart rate and SpO2 level* \par Details* By detecting peaks of PPG cycle and corresponding AC/DC of red/infra-red signal, the ratio for the SPO2 is computed.* Since this algorithm is aiming for Arm M0/M3. formaula for SPO2 did not achieve the accuracy due to register overflow.* Thus, accurate SPO2 is precalculated and save longo uch_spo2_table[] per each ratio.** \param[in] *pun_ir_buffer - IR sensor data buffer* \param[in] n_ir_buffer_length - IR sensor data buffer length* \param[in] *pun_red_buffer - Red sensor data buffer* \param[out] *pn_spo2 - Calculated SpO2 value* \param[out] *pch_spo2_valid - 1 if the calculated SpO2 value is valid* \param[out] *pn_heart_rate - Calculated heart rate value* \param[out] *pch_hr_valid - 1 if the calculated heart rate value is valid** \retval None*/{ uint32_t un_ir_mean ,un_only_once ; int32_t k ,n_i_ratio_count; int32_t i, s, m, n_exact_ir_valley_locs_count ,n_middle_idx; int32_t n_th1, n_npks,n_c_min; int32_t an_ir_valley_locs[15] ; int32_t an_exact_ir_valley_locs[15] ; int32_t an_dx_peak_locs[15] ; int32_t n_peak_interval_sum; int32_t n_y_ac, n_x_ac; int32_t n_spo2_calc; int32_t n_y_dc_max, n_x_dc_max; int32_t n_y_dc_max_idx, n_x_dc_max_idx; int32_t an_ratio[5],n_ratio_average; int32_t n_nume, n_denom ; // remove DC of ir signal un_ir_mean =0; for (k=0 ; k<n_ir_buffer_length ; k++ ) un_ir_mean += pun_ir_buffer[k] ; un_ir_mean =un_ir_mean/n_ir_buffer_length ; for (k=0 ; k<n_ir_buffer_length ; k++ ) an_x[k] = pun_ir_buffer[k] - un_ir_mean ; // 4 pt Moving Average for(k=0; k< BUFFER_SIZE-MA4_SIZE; k++){ n_denom= ( an_x[k]+an_x[k+1]+ an_x[k+2]+ an_x[k+3]); an_x[k]= n_denom/(int32_t)4; }​ // get difference of smoothed IR signal for( k=0; k<BUFFER_SIZE-MA4_SIZE-1; k++) an_dx[k]= (an_x[k+1]- an_x[k]);​ // 2-pt Moving Average to an_dx for(k=0; k< BUFFER_SIZE-MA4_SIZE-2; k++){ an_dx[k] = ( an_dx[k]+an_dx[k+1])/2 ; } // hamming window // flip wave form so that we can detect valley with peak detector for ( i=0 ; i<BUFFER_SIZE-HAMMING_SIZE-MA4_SIZE-2 ;i++){ s= 0; for( k=i; k<i+ HAMMING_SIZE ;k++){ s -= an_dx[k] *auw_hamm[k-i] ; } an_dx[i]= s/ (int32_t)1146; // divide by sum of auw_hamm }​ n_th1=0; // threshold calculation for ( k=0 ; k<BUFFER_SIZE-HAMMING_SIZE ;k++){ n_th1 += ((an_dx[k]>0)? an_dx[k] : ((int32_t)0-an_dx[k])) ; } n_th1= n_th1/ ( BUFFER_SIZE-HAMMING_SIZE); // peak location is acutally index for sharpest location of raw signal since we flipped the signal maxim_find_peaks( an_dx_peak_locs, &n_npks, an_dx, BUFFER_SIZE-HAMMING_SIZE, n_th1, 8, 5 );//peak_height, peak_distance, max_num_peaks ​ n_peak_interval_sum =0; if (n_npks>=2){ for (k=1; k<n_npks; k++) n_peak_interval_sum += (an_dx_peak_locs[k]-an_dx_peak_locs[k -1]); n_peak_interval_sum=n_peak_interval_sum/(n_npks-1); *pn_heart_rate=(int32_t)(6000/n_peak_interval_sum);// beats per minutes *pch_hr_valid = 1; } else { *pn_heart_rate = -999; *pch_hr_valid = 0; } for ( k=0 ; k<n_npks ;k++) an_ir_valley_locs[k]=an_dx_peak_locs[k]+HAMMING_SIZE/2; ​​ // raw value : RED(=y) and IR(=X) // we need to assess DC and AC value of ir and red PPG. for (k=0 ; k<n_ir_buffer_length ; k++ ) { an_x[k] = pun_ir_buffer[k] ; an_y[k] = pun_red_buffer[k] ; }​ // find precise min near an_ir_valley_locs n_exact_ir_valley_locs_count =0; for(k=0 ; k<n_npks ;k++){ un_only_once =1; m=an_ir_valley_locs[k]; n_c_min= 16777216;//2^24; if (m+5 < BUFFER_SIZE-HAMMING_SIZE && m-5 >0){ for(i= m-5;i<m+5; i++) if (an_x[i]<n_c_min){ if (un_only_once >0){ un_only_once =0; } n_c_min= an_x[i] ; an_exact_ir_valley_locs[k]=i; } if (un_only_once ==0) n_exact_ir_valley_locs_count ++ ; } } if (n_exact_ir_valley_locs_count <2 ){ *pn_spo2 = -999 ; // do not use SPO2 since signal ratio is out of range *pch_spo2_valid = 0; return; } // 4 pt MA for(k=0; k< BUFFER_SIZE-MA4_SIZE; k++){ an_x[k]=( an_x[k]+an_x[k+1]+ an_x[k+2]+ an_x[k+3])/(int32_t)4; an_y[k]=( an_y[k]+an_y[k+1]+ an_y[k+2]+ an_y[k+3])/(int32_t)4; }​ //using an_exact_ir_valley_locs , find ir-red DC andir-red AC for SPO2 calibration ratio //finding AC/DC maximum of raw ir * red between two valley locations n_ratio_average =0; n_i_ratio_count =0; for(k=0; k< 5; k++) an_ratio[k]=0; for (k=0; k< n_exact_ir_valley_locs_count; k++){ if (an_exact_ir_valley_locs[k] > BUFFER_SIZE ){ *pn_spo2 = -999 ; // do not use SPO2 since valley loc is out of range *pch_spo2_valid = 0; return; } } // find max between two valley locations // and use ratio betwen AC compoent of Ir & Red and DC compoent of Ir & Red for SPO2 ​ for (k=0; k< n_exact_ir_valley_locs_count-1; k++){ n_y_dc_max= -16777216 ; n_x_dc_max= - 16777216; if (an_exact_ir_valley_locs[k+1]-an_exact_ir_valley_locs[k] >10){ for (i=an_exact_ir_valley_locs[k]; i< an_exact_ir_valley_locs[k+1]; i++){ if (an_x[i]> n_x_dc_max) {n_x_dc_max =an_x[i];n_x_dc_max_idx =i; } if (an_y[i]> n_y_dc_max) {n_y_dc_max =an_y[i];n_y_dc_max_idx=i;} } n_y_ac= (an_y[an_exact_ir_valley_locs[k+1]] - an_y[an_exact_ir_valley_locs[k] ] )*(n_y_dc_max_idx -an_exact_ir_valley_locs[k]); //red n_y_ac= an_y[an_exact_ir_valley_locs[k]] + n_y_ac/ (an_exact_ir_valley_locs[k+1] - an_exact_ir_valley_locs[k]) ; n_y_ac= an_y[n_y_dc_max_idx] - n_y_ac; // subracting linear DC compoenents from raw n_x_ac= (an_x[an_exact_ir_valley_locs[k+1]] - an_x[an_exact_ir_valley_locs[k] ] )*(n_x_dc_max_idx -an_exact_ir_valley_locs[k]); // ir n_x_ac= an_x[an_exact_ir_valley_locs[k]] + n_x_ac/ (an_exact_ir_valley_locs[k+1] - an_exact_ir_valley_locs[k]); n_x_ac= an_x[n_y_dc_max_idx] - n_x_ac; // subracting linear DC compoenents from raw n_nume=( n_y_ac *n_x_dc_max)>>7 ; //prepare X100 to preserve floating value n_denom= ( n_x_ac *n_y_dc_max)>>7; if (n_denom>0 && n_i_ratio_count <5 && n_nume != 0) { an_ratio[n_i_ratio_count]= (n_nume*20)/n_denom ; //formular is ( n_y_ac *n_x_dc_max) / ( n_x_ac *n_y_dc_max) ; ///*************************n_nume原来是*100************************// n_i_ratio_count++; } } }​ maxim_sort_ascend(an_ratio, n_i_ratio_count); n_middle_idx= n_i_ratio_count/2;​ if (n_middle_idx >1) n_ratio_average =( an_ratio[n_middle_idx-1] +an_ratio[n_middle_idx])/2; // use median else n_ratio_average = an_ratio[n_middle_idx ];​ if( n_ratio_average>2 && n_ratio_average <184){ n_spo2_calc= uch_spo2_table[n_ratio_average] ; *pn_spo2 = n_spo2_calc ; *pch_spo2_valid = 1;// float_SPO2 = -45.060*n_ratio_average* n_ratio_average/10000 + 30.354 *n_ratio_average/100 + 94.845 ; // for comparison with table } else{ *pn_spo2 = -999 ; // do not use SPO2 since signal ratio is out of range *pch_spo2_valid = 0; }}​​void maxim_find_peaks(int32_t *pn_locs, int32_t *pn_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height, int32_t n_min_distance, int32_t n_max_num)/*** \brief Find peaks* \par Details* Find at most MAX_NUM peaks above MIN_HEIGHT separated by at least MIN_DISTANCE** \retval None*/{ maxim_peaks_above_min_height( pn_locs, pn_npks, pn_x, n_size, n_min_height ); maxim_remove_close_peaks( pn_locs, pn_npks, pn_x, n_min_distance ); *pn_npks = min( *pn_npks, n_max_num );}​void maxim_peaks_above_min_height(int32_t *pn_locs, int32_t *pn_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height)/*** \brief Find peaks above n_min_height* \par Details* Find all peaks above MIN_HEIGHT** \retval None*/{ int32_t i = 1, n_width; *pn_npks = 0; while (i < n_size-1){ if (pn_x[i] > n_min_height && pn_x[i] > pn_x[i-1]){ // find left edge of potential peaks n_width = 1; while (i+n_width < n_size && pn_x[i] == pn_x[i+n_width]) // find flat peaks n_width++; if (pn_x[i] > pn_x[i+n_width] && (*pn_npks) < 15 ){ // find right edge of peaks pn_locs[(*pn_npks)++] = i; // for flat peaks, peak location is left edge i += n_width+1; } else i += n_width; } else i++; }}​​void maxim_remove_close_peaks(int32_t *pn_locs, int32_t *pn_npks, int32_t *pn_x, int32_t n_min_distance)/*** \brief Remove peaks* \par Details* Remove peaks separated by less than MIN_DISTANCE** \retval None*/{ int32_t i, j, n_old_npks, n_dist; /* Order peaks from large to small */ maxim_sort_indices_descend( pn_x, pn_locs, *pn_npks );​ for ( i = -1; i < *pn_npks; i++ ){ n_old_npks = *pn_npks; *pn_npks = i+1; for ( j = i+1; j < n_old_npks; j++ ){ n_dist = pn_locs[j] - ( i == -1 ? -1 : pn_locs[i] ); // lag-zero peak of autocorr is at index -1 if ( n_dist > n_min_distance || n_dist < -n_min_distance ) pn_locs[(*pn_npks)++] = pn_locs[j]; } }​ // Resort indices longo ascending order maxim_sort_ascend( pn_locs, *pn_npks );}​void maxim_sort_ascend(int32_t *pn_x,int32_t n_size) /*** \brief Sort array* \par Details* Sort array in ascending order (insertion sort algorithm)** \retval None*/{ int32_t i, j, n_temp; for (i = 1; i < n_size; i++) { n_temp = pn_x[i]; for (j = i; j > 0 && n_temp < pn_x[j-1]; j--) pn_x[j] = pn_x[j-1]; pn_x[j] = n_temp; }}​void maxim_sort_indices_descend(int32_t *pn_x, int32_t *pn_indx, int32_t n_size)/*** \brief Sort indices* \par Details* Sort indices according to descending order (insertion sort algorithm)** \retval None*/ { int32_t i, j, n_temp; for (i = 1; i < n_size; i++) { n_temp = pn_indx[i]; for (j = i; j > 0 && pn_x[n_temp] > pn_x[pn_indx[j-1]]; j--) pn_indx[j] = pn_indx[j-1]; pn_indx[j] = n_temp; }}​3.2 max30102.h#ifndef __MYIIC_H#define __MYIIC_H#include "sys.h"#include "usart.h"#include <stdio.h>​/*MAX30102心率传感器:SCL<->PB6SDA<->PB7IM<->PB9*/​#define MAX30102_INT PBin(9)​//IO方向设置#define SDA_IN() {GPIOB->CRL&=0x0FFFFFFF;GPIOB->CRL|=0x40000000;} #define SDA_OUT() {GPIOB->CRL&=0x0FFFFFFF;GPIOB->CRL|=0x70000000;}​​//IO操作函数 #define IIC_SCL PBout(6) //SCL#define IIC_SDA PBout(7) //SDA #define READ_SDA PBin(7) //输入SDA ​//IIC所有操作函数void IIC_Init(void); //初始化IIC的IO口 void IIC_Start(void); //发送IIC开始信号void IIC_Stop(void); //发送IIC停止信号void IIC_Send_Byte(u8 txd); //IIC发送一个字节u8 IIC_Read_Byte(unsigned char ack);//IIC读取一个字节u8 IIC_Wait_Ack(void); //IIC等待ACK信号void IIC_Ack(void); //IIC发送ACK信号void IIC_NAck(void); //IIC不发送ACK信号​void IIC_Write_One_Byte(u8 daddr,u8 addr,u8 data);void IIC_Read_One_Byte(u8 daddr,u8 addr,u8* data);​void IIC_WriteBytes(u8 WriteAddr,u8* data,u8 dataLength);void IIC_ReadBytes(u8 deviceAddr, u8 writeAddr,u8* data,u8 dataLength);​​​#define I2C_WR 0 /* 写控制bit */#define I2C_RD 1 /* 读控制bit */​#define max30102_WR_address 0xAE​#define I2C_WRITE_ADDR 0xAE#define I2C_READ_ADDR 0xAF​//register addresses#define REG_INTR_STATUS_1 0x00#define REG_INTR_STATUS_2 0x01#define REG_INTR_ENABLE_1 0x02#define REG_INTR_ENABLE_2 0x03#define REG_FIFO_WR_PTR 0x04#define REG_OVF_COUNTER 0x05#define REG_FIFO_RD_PTR 0x06#define REG_FIFO_DATA 0x07#define REG_FIFO_CONFIG 0x08#define REG_MODE_CONFIG 0x09#define REG_SPO2_CONFIG 0x0A#define REG_LED1_PA 0x0C#define REG_LED2_PA 0x0D#define REG_PILOT_PA 0x10#define REG_MULTI_LED_CTRL1 0x11#define REG_MULTI_LED_CTRL2 0x12#define REG_TEMP_INTR 0x1F#define REG_TEMP_FRAC 0x20#define REG_TEMP_CONFIG 0x21#define REG_PROX_INT_THRESH 0x30#define REG_REV_ID 0xFE#define REG_PART_ID 0xFF​void max30102_init(void); void max30102_reset(void);u8 max30102_Bus_Write(u8 Register_Address, u8 Word_Data);u8 max30102_Bus_Read(u8 Register_Address);void max30102_FIFO_ReadWords(u8 Register_Address,u16 Word_Data[][2],u8 count);void max30102_FIFO_ReadBytes(u8 Register_Address,u8* Data);​void maxim_max30102_write_reg(uint8_t uch_addr, uint8_t uch_data);void maxim_max30102_read_reg(uint8_t uch_addr, uint8_t *puch_data);void maxim_max30102_read_fifo(uint32_t *pun_red_led, uint32_t *pun_ir_led);​void dis_DrawCurve(u32* data,u8 x);void MAX30102_get(u8 *hr,u8 *spo2);void MAX30102_data_set(void);​​​​​#define true 1#define false 0#define FS 100#define BUFFER_SIZE (FS* 5) #define HR_FIFO_SIZE 7#define MA4_SIZE 4 // DO NOT CHANGE#define HAMMING_SIZE 5// DO NOT CHANGE#define min(x,y) ((x) < (y) ? (x) : (y))​//const uint16_t auw_hamm[31]={ 41, 276, 512, 276, 41 }; //Hamm= long16(512* hamming(5)');////uch_spo2_table is computed as -45.060*ratioAverage* ratioAverage + 30.354 *ratioAverage + 94.845 ;//const uint8_t uch_spo2_table[184]={ 95, 95, 95, 96, 96, 96, 97, 97, 97, 97, 97, 98, 98, 98, 98, 98, 99, 99, 99, 99, // 99, 99, 99, 99, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, // 100, 100, 100, 100, 99, 99, 99, 99, 99, 99, 99, 99, 98, 98, 98, 98, 98, 98, 97, 97, // 97, 97, 96, 96, 96, 96, 95, 95, 95, 94, 94, 94, 93, 93, 93, 92, 92, 92, 91, 91, // 90, 90, 89, 89, 89, 88, 88, 87, 87, 86, 86, 85, 85, 84, 84, 83, 82, 82, 81, 81, // 80, 80, 79, 78, 78, 77, 76, 76, 75, 74, 74, 73, 72, 72, 71, 70, 69, 69, 68, 67, // 66, 66, 65, 64, 63, 62, 62, 61, 60, 59, 58, 57, 56, 56, 55, 54, 53, 52, 51, 50, // 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 31, 30, 29, // 28, 27, 26, 25, 23, 22, 21, 20, 19, 17, 16, 15, 14, 12, 11, 10, 9, 7, 6, 5, // 3, 2, 1 } ;//static int32_t an_dx[ BUFFER_SIZE-MA4_SIZE]; // delta//static int32_t an_x[ BUFFER_SIZE]; //ir//static int32_t an_y[ BUFFER_SIZE]; //red​​void maxim_heart_rate_and_oxygen_saturation(uint32_t *pun_ir_buffer , int32_t n_ir_buffer_length, uint32_t *pun_red_buffer , int32_t *pn_spo2, int8_t *pch_spo2_valid , int32_t *pn_heart_rate , int8_t *pch_hr_valid);void maxim_find_peaks( int32_t *pn_locs, int32_t *pn_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height, int32_t n_min_distance, int32_t n_max_num );void maxim_peaks_above_min_height( int32_t *pn_locs, int32_t *pn_npks, int32_t *pn_x, int32_t n_size, int32_t n_min_height );void maxim_remove_close_peaks( int32_t *pn_locs, int32_t *pn_npks, int32_t *pn_x, int32_t n_min_distance );void maxim_sort_ascend( int32_t *pn_x, int32_t n_size );void maxim_sort_indices_descend( int32_t *pn_x, int32_t *pn_indx, int32_t n_size);​​​​#endif
  • [问题求助] 物联网与嵌入式系统有何关系。
    物联网如何利用嵌入式系统实现智能设备的连接和控制。
  • [技术干货] 2024年1月嵌入式项目开发专题总汇
    一、前言本文将对一系列深入探讨单片机与C语言编程核心技术的文章进行综合概述,这些文章涵盖了从基础知识到实际项目应用的广泛领域,为读者提供一个全面而系统的知识框架。首先从单片机与C语言编程的基础元素出发,详细剖析了C语言编程中的基本语句、运算符使用及其在单片机环境下的具体实践,并通过格式化打印函数的教学以及字符串和数组的全方位练习,帮助读者掌握扎实的数据处理能力。这一阶段的文章包括“单片机与C语言编程基础_格式化打印函数”、“字符串知识点”、“数组全方位练习”、“数组与函数传参”等,进一步深化了对函数封装、字符串操作和数据结构的理解。随着学习的深入,聚焦于单片机系统中的中断管理与通信技术,如以STM32单片机为例,展示了如何采用环形缓冲区实现串口中断数据高效接收与管理,为实时控制和数据传输打下坚实基础。在计算机视觉领域,介绍了如何利用OpenCV4.x版本的C++接口实现人脸检测算法,包括正脸、侧脸乃至眼睛识别,展示了C语言在现代智能硬件系统中的广泛应用潜力。为了拓宽读者的技术视野,还涉及到了Windows平台下的网络编程技术,通过Win32 API结合Visual Studio 2022开发环境,讲解了如何构建基于PC端的网络应用程序。系列文章并没有忽视嵌入式开发领域的另一重要方面——Linux操作系统的基本命令与C语言编程在嵌入式Linux环境下的基础应用。整体而言,本系列文章不仅致力于奠定坚实的单片机与C语言编程基石,而且引领读者逐步走向实际应用场景,诸如智慧农业大棚检测系统的设计与实施,从而全面提升读者在嵌入式系统设计、数据采集处理、通信协议栈以及多平台开发等方面的专业技能与实战经验。二、文章列表(1)基于单片机设计的智慧农业大棚检测系统cid:link_5本项目基于单片机设计一个智慧农业大棚检测系统,以提供实时监测和管理大棚环境的关键参数。系统支持环境温度、湿度检测,光照强度检测,并能根据预设的阀值进行报警提示。为了实现数据的显示和管理,该系统还利用Qt开发了一款对应的Android手机APP,通过蓝牙传输模块将单片机采集到的数据传递到手机APP上进行显示和管理。(2)STM32单片机采用环形缓冲区实现串口中断数据接收管理cid:link_0在嵌入式系统开发中,与上位机进行串口通信是非常常见的场景。上位机可以通过串口发送指令或者数据给嵌入式设备,而嵌入式设备需要可靠地接收并解析这些数据,以执行相应的操作。然而,在串口通信过程中,上位机发送数据的速率往往与嵌入式设备接收和处理数据的速率不一致,这就可能导致数据的丢失或者误解析。为了解决这个问题,决定设计并实现一个环形缓冲区来进行数据接收管理。环形缓冲区是一种高效的数据结构,适用于数据产生速率快于消费速率的场景。它具有固定大小的缓冲区,并且可以循环利用空间,保证数据的连续存储和有效利用。在本项目中,选择使用STM32微控制器来实现串口数据接收功能。STM32具有丰富的外设资源和强大的性能,非常适合用于串口通信和数据处理。(3)OpenCV4.x(C++)人脸检测(眼睛、侧脸、正脸)cid:link_1OpenCV是一款广泛使用的计算机视觉库,提供了许多强大的功能,包括人脸检测和识别。人脸分类器是OpenCV中用于人脸检测的关键工具之一,能够快速准确地检测出图像中的人脸。本文将介绍如何使用OpenCV自带的人脸分类器,并对比不同分类器的精度。在日常生活中,人脸检测的应用非常广泛,例如安防、人机交互、智能交通等领域。而在计算机视觉领域,人脸检测也是一个非常热门的研究方向。OpenCV作为一款免费、开源的计算机视觉库,为我们提供了一种方便快捷的人脸检测方法。使用OpenCV的人脸分类器,可以快速地检测出图像中的正脸、侧脸和眼睛等部位,进而实现更加智能的应用。(4)单片机与C语言编程基础_格式化打印函数、字符串、运算符cid:link_6本文是一篇全面解析C语言核心知识点与实用技巧的文章,重点围绕字符串函数的运用进行了深入探讨,包括对sprintf和sscanf等格式化输入输出函数的功能详解及实际应用案例。同时,文章系统梳理了main函数的标准形参设定规则,以及变量和常量在C语言中的定义方式和使用场景。进一步地总结了C语言中各类基本数据类型(如整型、浮点型、字符型等)所占用的字节数,加深读者对内存分配和数据存储的理解。此外,文中还详细解读了scanf函数如何实现多元化的用户输入数据处理,并通过实例演示其在实际编程中的应用。在进阶内容部分,文章全方位介绍了位运算的概念、操作符及其在程序设计中的重要作用,结合实例帮助读者掌握这一底层编程技术。最后,通过对C语言中所有基本运算符进行全面系统的介绍与解析,使读者能够更加熟练地运用各种运算符进行高效计算和逻辑判断,提升编程效率与代码质量。整篇文章构建扎实的C语言基础知识体系,助力读者在实践中游刃有余地应对各类编程挑战。(5)单片机与C语言编程基础__函数知识cid:link_7C语言是一种结构化、高效且广泛应用的编程语言,其核心特性之一是强大的函数功能。在C语言中,函数是用来封装一段可重用代码的逻辑单元,通过函数定义可以实现特定任务,例如计算、处理数据等。函数定义包括函数名、参数列表以及函数体,其中函数体内部包含了执行特定任务的一系列语句。子函数是在主函数或其它函数内部定义的函数,用于实现更细粒度的功能划分和模块化编程,增强代码的复用性和可读性。子函数可以通过return语句向调用者返回结果,并可通过形参接收外部传入的数据。变量的作用域是程序设计中的重要概念,它决定了变量在其声明位置之后的有效范围。在C语言中,变量有全局作用域和局部作用域之分。全局变量在整个程序文件中都可见,从定义处开始到文件结束;而局部变量仅在定义它的函数或代码块内有效。全局变量可以在任何函数中访问和修改,但过度依赖全局变量可能导致程序逻辑混乱。静态变量则具有局部作用域,但在函数多次调用间保留其值。只读变量(常量)一旦初始化后就不能再改变其值,这有利于提高程序的健壮性和安全性。另外,C语言允许在定义变量时进行初始化,即赋予变量一个初始值,这样在程序运行之初,变量就已经有了确定的值。这种特性使得代码更加清晰明确,减少了潜在的未初始化变量引发的问题。(6)单片机与C语言编程基础___数组全方位练习cid:link_8文章探讨了C语言中函数的使用以及数组操作的各种实践练习。剖析了C语言函数的基本概念与用法,包括函数的定义、封装性原理,如何进行参数传递和调用,以及对局部变量、全局变量、只读变量(const)和静态变量在函数内部的不同应用场景进行了详解。文章聚焦于C语言数组这一重要数据结构的实践运用。通过一系列精心设计的习题,可以亲自动手实现诸如数组插入、删除元素、数组元素的左右移动等基础操作,并进一步探索数组排序算法,如冒泡排序、选择排序,以及对其进行优化,比如采用更高效的快速排序或归并排序方法。文章还涉及到了数组数据拼接的实际问题,指导数组间的数据合并技巧。文章从基础知识出发,详细介绍了数组的基本定义方式,如何对数组元素进行赋值,并延伸至处理字符串这一特殊类型的字符数组,以确保读者对数组的使用有全面且扎实的理解和应用能力。通过这些实际的编程练习,能够提升自身的C语言编程技能,更好地理解和掌握函数与数组在实际项目开发中的综合运用。(7)单片机与C语言编程基础_数组与函数传参cid:link_9该篇文章是一篇关于C语言编程中核心概念和函数库使用的详细介绍。它深入探讨了字符串处理的标准函数,这些函数在C语言的<string.h>库中提供,涵盖了字符串的创建、复制、比较、连接、查找等操作,对于提高程序处理文本数据的能力至关重要。文章详述了指针与数组在作为函数参数时的使用方法和两者之间的内在联系,帮助读者理解和掌握如何在函数间传递和操作动态数据。文中介绍了指针的基本定义方式以及函数返回指针的机制,包括void类型指针的定义及其灵活运用,为高效内存管理打下基础。通过类型强制转换的相关内容,指导开发者在不同数据类型之间安全、准确地转换数据。文章讨论了常量声明的概念,强调了在编程过程中对不变量的合理使用和控制。最后,涉及到了extern关键字的使用,说明如何实现模块间的全局变量引用,以实现程序的不同部分共享和访问同一份数据,这对于编写结构清晰、可复用性强的代码具有重要意义。(8)单片机与C语言编程基础_数组知识cid:link_10该篇文章全面回顾了C语言中数组这一重要数据结构的基础概念与实践操作。文章详尽梳理了数组的定义规则,包括如何声明不同空间类型的数组,如静态数组、动态数组等,并阐述了数组下标的使用规范,即如何通过下标访问数组中的元素。在内容上,文章深入探讨了数组能够存放的各种数据类型,从基本类型到复合类型,并结合实例演示了数组元素的数据替换方法以及数组内容的插入操作。针对数组的输入输出功能,作者详细介绍了标准库函数和格式化输入输出语句在处理数组数据时的应用技巧。特别地,对于字符串这一特殊形式的字符数组,文章着重讲解了字符串的输入输出过程,包括如何利用C语言进行字符串的有效读取和显示,并且讨论了计算字符串长度的相关算法。整体而言,这篇文章系统性地涵盖了C语言数组的关键知识点及其实战应用,为读者理解和掌握数组这一核心数据结构提供了坚实的基础指导。(9)Windows下网络编程(win32API+VS2022)cid:link_11这篇文章系统性地介绍了在Windows操作系统下,如何利用C语言结合Win32 API进行网络编程,并以Visual Studio 2022开发环境为依托,从零起步详细阐述了TCP协议的原理及其在实际编程中的应用。作者不仅细致梳理了在VS2022中从安装配置到新建项目的完整过程,还通过实践案例,深入浅出地展示了如何创建和实现一个功能完备的TCP服务器和客户端程序。文章内容涵盖了TCP/IP基础理论,包括连接建立、数据传输与断开连接等关键环节的原理详解;同时,针对初学者,特别强调了在Visual Studio 2022环境下搭建项目、包含必要的头文件以及调用Win32 API进行套接字编程的具体步骤。通过实战示例代码,读者能够跟随教程逐步掌握在Windows平台上构建稳定、高效的TCP通信程序的方法。(10)单片机与C语言编程基础_语句、运算符cid:link_2这篇文章全面探讨了C语言中的核心概念和关键语法元素,为读者提供一个坚实的基础和深入的理解。文章首先详述了C语言的位运算符,包括按位与(&)、按位或(|)、异或(^)、取反(~)以及左移(<<)和右移(>>)等操作,解释了它们在处理二进制数据时的作用和实际应用案例。接着,文章转向C语言的基本运算符,如算术运算符(加减乘除)、关系运算符(等于、不等于、大于、小于等)、逻辑运算符(与、或、非)以及赋值运算符,并讨论了这些运算符在表达式求值和程序控制流程中的重要作用。在数据类型部分,详细介绍了C语言中各种基本数据类型(如整型、浮点型、字符型等)以及复合数据类型(如数组、结构体),并讨论了如何声明和使用变量,包括其存储方式和生命周期。文中还重点阐述了C语言中的循环结构,包括for语句和while语句的语法格式及其在实现重复执行代码块方面的不同应用场景,同时提及goto语句这一无条件跳转指令的使用及其在现代编程中的争议性。进一步地,文章剖析了switch语句的工作机制,展示了如何通过case标签进行多分支选择,并强调了default子句在异常处理和完整性保证上的价值。文章还涵盖了运算符优先级的概念,指导读者理解不同运算符之间的结合性和计算顺序,从而避免潜在的编程错误。最后,提到了强制类型转换,说明了如何在必要时显式改变变量的数据类型以满足特定的计算需求。(11)单片机与C语言编程基础_字符串函数封装练习cid:link_12这是一篇专注于C/C++编程语言中核心字符串与内存操作实践的文章,通过一系列精心设计的练习题目,深度探索了字符串处理的关键技术,如字符串的比较、连接、查找以及拷贝等操作;同时,文章也详尽探讨了内存管理的基础技能,包括内存的比较、拷贝、初始化等实际应用。此外,文中还延伸至二维数组的定义及基本使用方法,以增强对复杂数据结构的理解与操作能力。最后,通过运用位运算技巧,展示了如何巧妙地借助取模运算将中文字模打印至控制台,使得读者能直观理解并掌握位运算在实际问题解决中的强大功能。整篇文章通过实际动手练习,提升读者对底层数据操作和高级程序设计技术的综合运用能力。(12)单片机与C语言编程基础_字符串知识点cid:link_13这篇文章全面介绍了C语言字符串的各种练习方法,涵盖了字符串解析、大小写判断、字符串插入、字符串删除、字符串排序、字符串转整数、字符串转浮点数、时间转换、GPS数据解析等多个知识点。通过学习这些内容,读者可以提升对C语言字符串处理的理解和实践能力。无论是初学者还是有一定经验的开发者,都能从中获得实用的技巧和解决问题的思路。这篇文章将为读者提供一次全方位的C语言字符串练习机会,帮助他们在字符串操作方面更加熟练和自信。(13)单片机与C语言编程基础__基本语句与位运算cid:link_14这篇文章是一篇关于C语言实践与应用的深度教程,通过一系列精心设计的基础练习题,系统性地涵盖了C语言中的多个核心知识点。文章以实际编程题目为载体,引导读者动手解决计算素数、实现排序算法(如冒泡排序、选择排序等)、求解一定范围内的偶数之和、识别可逆素数(即回文素数)、查找三位数中的水仙花数以及使用位运算进行数据交换和逻辑操作等经典问题。此外,还介绍了C语言的语法特性,包括如何安全高效地交换变量值、掌握字符串操作如大小写转换等实用技术点。通过这些实例和练习,帮助读者巩固C语言基础知识,提升编程技能,并加强对C语言特性和底层机制的理解与运用能力。(14)单片机与C语言编程基础_字符串全方位练习cid:link_3本文是一篇全面而深入的C语言字符串实践指南,通过精心设计的一系列丰富练习题目,系统地涵盖了字符串处理中的多个关键技术和应用场景。文章详细探讨了字符串解析、大小写转换判断、字符串插入与删除操作、字符串排序算法等基础技术,并进一步延伸至复杂应用层面,如将字符串转换为整数和浮点数的功能实现,以及时间格式转换的实践操作。此外,文章还特别引入了GPS数据解析这一实际问题,引导读者运用C语言字符串处理技能解决现代信息技术领域的实际问题。通过这些实例和练习,读者不仅能巩固C语言字符串操作的基础知识,更能提升在实际编程中灵活高效地处理字符串数据的能力,拓宽解决问题的思路和视野。(15)嵌入式开发基础_Linux基本命令与C语言基础cid:link_4本文介绍如何在Linux环境下搭建C语言学习与开发的基本环境。逐步解析了在Linux系统中从零开始安装配置C语言编译器及相关工具链的过程,并深入浅出地讲解了一些必备的基础命令使用方法,如文件操作、目录导航等。探讨了Linux系统下用户权限配置的相关知识,帮助理解并掌握不同用户角色及其对应的操作权限,这对于安全、高效地进行C语言编程至关重要。针对C语言编程的核心要素之一——标准main函数的传参方式,也做了细致解读,能够清晰了解Linux环境下C程序参数的接收和处理机制,从而更好地运用到实际编程实践中。
  • [技术干货] Windows下网络编程(win32API+VS2022)
    一、开发环境我这里介绍下我用的环境安装过程。 所有版本的VS都可以的。我当前环境是在Windows下,IDE用的是地表最强IDE VS2022。下载地址:cid:link_1因为我这里只需要用到C++和C语言编程,那么安装的时候可以自己选择需要安装的包。安装好之后,创建项目。二、网络编程的基础知识2.1 什么是网络编程网络编程是通过使用IP地址和端口号等网络信息,使两台以上的计算机能够相互通信,按照规定的协议交换数据的编程方式。在网络编程中,程序员使用各种协议和技术,使得不同的设备可以通过网络进行数据交换和信息共享。要实现网络编程,程序员需要了解并掌握各种网络通信协议,比如TCP/IP协议族,包括TCP、UDP、IP等,这些协议是实现设备间通信的基础。网络编程内部涉及到数据的打包、组装、发送、接收、解析等一系列过程,以实现信息的正确传输。在TCP/IP协议族中,TCP和UDP是位于IP协议之上的传输层协议。 在OSI模型中,传输层是第四层,负责总体数据传输和数据控制,为会话层等高三层提供可靠的传输服务,为网络层提供可靠的目的地点信息。在TCP/IP协议族中,TCP和UDP正是位于这一层的协议。这篇文章主要介绍 TCP 和 UDP 协议 以及 使用方法。2.2 TCP 和 UDP协议介绍TCP协议:TCP(传输控制协议)是一种面向连接的、可靠的传输层协议。在传输数据之前需要先建立连接,确保数据的顺序和完整性。TCP通过三次握手建立连接,并通过确认、超时和重传机制确保数据的可靠传输。TCP采用流量控制和拥塞控制机制,以避免网络拥塞,确保数据的顺利传输。因为TCP的这些特性,通常被应用于需要高可靠性和顺序性的应用,如网页浏览、电子邮件等。UDP协议:UDP(用户数据报协议)是一种无连接的、不可靠的传输层协议。与TCP不同,UDP在传输数据之前不需要建立连接,直接将数据打包成数据报并发送出去。因此,UDP没有TCP的那些确认、超时和重传机制,也就不保证数据的可靠传输。UDP也没有TCP的流量控制和拥塞控制机制。因为UDP的简单性和高效性,通常被应用于实时性要求较高,但对数据可靠性要求不高的应用,如语音通话、视频直播等。2.3 TCP通信的实现过程要实现TCP通信,两端必须要知道对方的IP和端口号:(1)IP地址:TCP协议是基于IP协议进行通信的,因此需要知道对方的IP地址,才能建立连接。(2)端口号:每个TCP连接都有一个唯一的端口号,用于标识进程和应用程序。建立连接时,需要指定本地端口号和远端端口号。(3)应用层协议:TCP协议只提供数据传输服务,应用程序需要定义自己的应用层协议,用于解析报文和处理数据。例如,HTTP协议就是基于TCP协议的应用层协议。在正常的TCP通信过程中,第一步需要建立连接,这个过程称为“三次握手”。建立连接时,客户端向服务器发送一个SYN包,表示请求建立连接;服务器接收到SYN包后,向客户端发送一个ACK包,表示确认收到了SYN包;最后客户端再向服务器发送一个ACK包,表示确认收到了服务器的ACK包,此时连接建立成功。建立连接后,数据传输就可以开始了。三、Windows下的API介绍微软的官方文档地址:cid:link_03.1 常用的函数介绍在Windows下进行网络编程,可以使用Winsock API(Windows Sockets API)来实现。Winsock API是Windows平台上的标准网络编程接口,提供了一系列函数和数据结构,用于创建、连接、发送和接收网络数据等操作。下面是常用的Winsock API接口函数:(1)WSAStartup:初始化Winsock库,必须在使用其他Winsock函数之前调用。(2)socket:创建一个套接字,用于网络通信。(3)bind:将套接字与本地地址(IP地址和端口号)绑定。(4)listen:开始监听连接请求,将套接字设置为被动模式。(5)accept:接受客户端的连接请求,创建一个新的套接字用于与客户端通信。(6)connect:与远程服务器建立连接。(7)send:发送数据到已连接的套接字。(8)recv:从已连接的套接字接收数据。(9)sendto:发送数据到指定的目标地址。(10)recvfrom:从指定的地址接收数据。(11)closesocket:关闭套接字。(12)getaddrinfo:根据主机名和服务名获取地址信息。(13)gethostbyname:根据主机名获取主机的IP地址。(14)gethostname:获取本地主机名。3.2 函数参数介绍下面是常用的几个Winsock API函数及其函数原型和参数含义的介绍:(1)WSAStartup:int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);wVersionRequested:请求的Winsock版本号。lpWSAData:指向WSADATA结构的指针,用于接收初始化结果和相关信息。(2)socket:SOCKET socket(int af, int type, int protocol);af:地址族(Address Family),如AF_INET表示IPv4。type:套接字类型,如SOCK_STREAM表示面向连接的TCP套接字。protocol:指定协议。通常为0,表示根据type自动选择合适的协议。(3)bind:int bind(SOCKET s, const struct sockaddr* name, int namelen);s:要绑定的套接字。name:指向sockaddr结构的指针,包含要绑定的本地地址信息。namelen:name结构的长度。(4)listen:int listen(SOCKET s, int backlog);s:要监听的套接字。backlog:等待连接队列的最大长度。(5)accept:SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);s:监听套接字。addr:用于存储客户端地址信息的sockaddr结构。addrlen:addr结构的长度。(6)connect:int connect(SOCKET s, const struct sockaddr* name, int namelen);s:要连接的套接字。name:指向目标地址信息的sockaddr结构指针。namelen:name结构的长度。(7)send:int send(SOCKET s, const char* buf, int len, int flags);s:要发送数据的套接字。buf:要发送的数据缓冲区。len:要发送的数据长度。flags:额外选项,如MSG_DONTROUTE等。(8)recv:int recv(SOCKET s, char* buf, int len, int flags);s:要接收数据的套接字。buf:用于存储接收数据的缓冲区。len:要接收的数据长度。flags:额外选项。(9)sendto:int sendto(SOCKET s, const char* buf, int len, int flags, const struct sockaddr* to, int tolen);s:要发送数据的套接字。buf:要发送的数据缓冲区。len:要发送的数据长度。flags:额外选项。to:指向目标地址信息的sockaddr结构指针。tolen:to结构的长度。(10)recvfrom:int recvfrom(SOCKET s, char* buf, int len, int flags, struct sockaddr* from, int* fromlen);s:要接收数据的套接字。buf:用于存储接收数据的缓冲区。len:要接收的数据长度。flags:额外选项。from:用于存储发送方地址信息的sockaddr结构指针。fromlen:from结构的长度。(11)closesocket:int closesocket(SOCKET s);s:要关闭的套接字。(12)getaddrinfo:int getaddrinfo(const char* nodename, const char* servname, const struct addrinfo* hints, struct addrinfo** res);nodename:目标主机名或IP地址。servname:服务名或端口号。hints:指向addrinfo结构的指针,提供关于地址查找的提示。res:指向addrinfo结构链表的指针,用于接收查找结果。(13)gethostbyname:struct hostent* gethostbyname(const char* name);name:要查询的主机名。(14)gethostname:int gethostname(char* name, int namelen);name:用于接收主机名的缓冲区。namelen:name缓冲区的长度。四、基本示例代码4.1 创建TCP服务器下面代码实现一个简单的TCP服务器。实现的功能:初始化Winsock、创建套接字、绑定到本地地址和指定端口、监听连接请求、接受客户端连接、发送和接收数据,最后关闭套接字和清理Winsock资源。#include <iostream>#include <winsock2.h>#include <ws2tcpip.h>#pragma comment(lib, "ws2_32.lib") // 链接到ws2_32库int main(){ WSADATA wsaData; int result = WSAStartup(MAKEWORD(2, 2), &wsaData); // 初始化Winsock if (result != 0) { std::cout << "初始化Winsock失败 " << result << std::endl; return 1; } SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建套接字 if (listenSocket == INVALID_SOCKET) { std::cout << "创建套接字失败: " << WSAGetLastError() << std::endl; WSACleanup(); return 1; } sockaddr_in service; service.sin_family = AF_INET; service.sin_addr.s_addr = INADDR_ANY; service.sin_port = htons(12345); result = bind(listenSocket, (SOCKADDR*)&service, sizeof(service)); // 将套接字绑定到本地地址和指定端口 if (result == SOCKET_ERROR) { std::cout << "端口绑定失败: " << WSAGetLastError() << std::endl; closesocket(listenSocket); WSACleanup(); return 1; } result = listen(listenSocket, SOMAXCONN); // 监听连接请求 if (result == SOCKET_ERROR) { std::cout << "监听连接请求失败: " << WSAGetLastError() << std::endl; closesocket(listenSocket); WSACleanup(); return 1; } std::cout << "等待客户端连接:" << std::endl; SOCKET clientSocket = accept(listenSocket, NULL, NULL); // 接受客户端连接 if (clientSocket == INVALID_SOCKET) { std::cout << "accept执行失败: " << WSAGetLastError() << std::endl; closesocket(listenSocket); WSACleanup(); return 1; } std::cout << "客户端已连接..." << std::endl; char sendBuffer[1024] = "Hello, client!"; result = send(clientSocket, sendBuffer, sizeof(sendBuffer), 0); // 发送数据给客户端 if (result == SOCKET_ERROR) { std::cout << "发送消息执行错误: " << WSAGetLastError() << std::endl; closesocket(clientSocket); WSACleanup(); return 1; } char recvBuffer[1024]; result = recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0); // 接收来自客户端的数据 if (result == SOCKET_ERROR) { std::cout << "接收消息执行错误: " << WSAGetLastError() << std::endl; closesocket(clientSocket); WSACleanup(); return 1; } std::cout << "收到来着客户端发送的消息: " << recvBuffer << std::endl; closesocket(clientSocket); // 关闭客户端套接字 closesocket(listenSocket); // 关闭监听套接字 WSACleanup(); // 清理Winsock资源 return 0;}运行效果:4.2 创建TCP客户端下面代码实现一个TCP客户端,连接到指定的服务器并完成通信。#include <iostream>#include <winsock2.h>#include <ws2tcpip.h>#pragma comment(lib, "ws2_32.lib") //告诉编译器链接Winsock库int main(){ WSADATA wsaData; //创建一个结构体变量,用于存储关于Winsock库的信息 int result = WSAStartup(MAKEWORD(2, 2), &wsaData); //初始化Winsock库,指定版本号2.2,检查返回值 if (result != 0) { std::cout << "WSAStartup failed: " << result << std::endl; //输出错误信息并退出程序 return 1; } SOCKET connectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个TCP套接字,检查返回值 if (connectSocket == INVALID_SOCKET) { std::cout << "socket failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序 WSACleanup(); //清除Winsock库 return 1; } sockaddr_in service; //创建一个结构体变量,用于存储服务器地址信息 service.sin_family = AF_INET; //指定地址族为IPv4 inet_pton(AF_INET, "127.0.0.1", &service.sin_addr); //将字符串类型的IP地址转换为二进制网络字节序的IP地址,并存储在结构体中 service.sin_port = htons(12345); //将端口号从主机字节序转换为网络字节序,并存储在结构体中 result = connect(connectSocket, (SOCKADDR*)&service, sizeof(service)); //连接到服务器,检查返回值 if (result == SOCKET_ERROR) { std::cout << "connect failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序 closesocket(connectSocket); //关闭套接字 WSACleanup(); //清除Winsock库 return 1; } std::cout << "Connected to server." << std::endl; //连接成功,输出消息 char sendBuffer[1024] = "Hello, server!"; //创建发送缓冲区,存储待发送的数据 result = send(connectSocket, sendBuffer, sizeof(sendBuffer), 0); //向服务器发送数据,检查返回值 if (result == SOCKET_ERROR) { std::cout << "send failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序 closesocket(connectSocket); //关闭套接字 WSACleanup(); //清除Winsock库 return 1; } char recvBuffer[1024]; //创建接收缓冲区,用于存储从服务器接收到的数据 result = recv(connectSocket, recvBuffer, sizeof(recvBuffer), 0); //从服务器接收数据,检查返回值 if (result == SOCKET_ERROR) { std::cout << "recv failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序 closesocket(connectSocket); //关闭套接字 WSACleanup(); //清除Winsock库 return 1; } std::cout << "Received message from server: " << recvBuffer << std::endl; //输出从服务器收到的数据 closesocket(connectSocket); //关闭套接字 WSACleanup(); //清除Winsock库 return 0;}运行效果:4.3 TCP客户端循环接收消息#include <iostream>#include <winsock2.h>#include <ws2tcpip.h>#pragma comment(lib, "ws2_32.lib") //告诉编译器链接Winsock库int main(){ WSADATA wsaData; //创建一个结构体变量,用于存储关于Winsock库的信息 int result = WSAStartup(MAKEWORD(2, 2), &wsaData); //初始化Winsock库,指定版本号2.2,检查返回值 if (result != 0) { std::cout << "WSAStartup failed: " << result << std::endl; //输出错误信息并退出程序 return 1; } SOCKET connectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个TCP套接字,检查返回值 if (connectSocket == INVALID_SOCKET) { std::cout << "socket failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序 WSACleanup(); //清除Winsock库 return 1; } sockaddr_in service; //创建一个结构体变量,用于存储服务器地址信息 service.sin_family = AF_INET; //指定地址族为IPv4 inet_pton(AF_INET, "127.0.0.1", &service.sin_addr); //将字符串类型的IP地址转换为二进制网络字节序的IP地址,并存储在结构体中 service.sin_port = htons(12345); //将端口号从主机字节序转换为网络字节序,并存储在结构体中 result = connect(connectSocket, (SOCKADDR*)&service, sizeof(service)); //连接到服务器,检查返回值 if (result == SOCKET_ERROR) { std::cout << "connect failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序 closesocket(connectSocket); //关闭套接字 WSACleanup(); //清除Winsock库 return 1; } std::cout << "Connected to server." << std::endl; //连接成功,输出消息 char recvBuffer[1024]; //创建接收缓冲区,用于存储从服务器接收到的数据 while (true) { result = recv(connectSocket, recvBuffer, sizeof(recvBuffer), 0); //从服务器接收数据,检查返回值 if (result == SOCKET_ERROR) { std::cout << "recv failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出循环 break; } else if (result > 0) //判断是否有数据接收到 { std::cout << "Received message from server: " << recvBuffer << std::endl; //输出从服务器收到的数据 } else //连接断开 { std::cout << "Server disconnected." << std::endl; break; } } closesocket(connectSocket); //关闭套接字 WSACleanup(); //清除Winsock库 return 0;}4.4 TCP服务器并发处理客户端请求下面示例代码中,使用了std::vector<std::thread>来存储线程对象,在每个客户端连接时创建一个新线程来处理该连接。使用多线程可以让服务器同时处理多个客户端连接,提高并发性能。#include <iostream>#include <winsock2.h>#include <ws2tcpip.h>#include <thread>#include <vector>#pragma comment(lib, "ws2_32.lib")// 处理客户端连接的函数void HandleClient(SOCKET clientSocket){ char recvBuffer[1024]; int result; while (true) { result = recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0); if (result == SOCKET_ERROR) { std::cout << "recv failed with error: " << WSAGetLastError() << std::endl; break; } else if (result > 0) { std::cout << "Received message from client: " << recvBuffer << std::endl; } else { std::cout << "Client disconnected." << std::endl; break; } } closesocket(clientSocket);}int main(){ WSADATA wsaData; int result = WSAStartup(MAKEWORD(2, 2), &wsaData); if (result != 0) { std::cout << "WSAStartup failed: " << result << std::endl; return 1; } SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSocket == INVALID_SOCKET) { std::cout << "socket failed with error: " << WSAGetLastError() << std::endl; WSACleanup(); return 1; } sockaddr_in service; service.sin_family = AF_INET; service.sin_addr.s_addr = INADDR_ANY; service.sin_port = htons(12345); result = bind(listenSocket, (SOCKADDR*)&service, sizeof(service)); if (result == SOCKET_ERROR) { std::cout << "bind failed with error: " << WSAGetLastError() << std::endl; closesocket(listenSocket); WSACleanup(); return 1; } result = listen(listenSocket, SOMAXCONN); if (result == SOCKET_ERROR) { std::cout << "listen failed with error: " << WSAGetLastError() << std::endl; closesocket(listenSocket); WSACleanup(); return 1; } std::cout << "Server is listening for incoming connections." << std::endl; std::vector<std::thread> threads; // 存储线程对象 while (true) { SOCKET clientSocket = accept(listenSocket, NULL, NULL); if (clientSocket == INVALID_SOCKET) { std::cout << "accept failed with error: " << WSAGetLastError() << std::endl; continue; } std::cout << "Client connected." << std::endl; // 创建一个新线程来处理客户端连接 std::thread thread(HandleClient, clientSocket); // 存储线程对象 threads.push_back(std::move(thread)); } // 等待所有线程执行完毕 for (auto& thread : threads) { thread.join(); } closesocket(listenSocket); WSACleanup(); return 0;}运行效果:
  • [技术干货] 单片机与C语言编程基础_数组知识
    当前文章复盘C语言的数组: 数组定义规则、数组空间类型、数组下标使用、数组存放的数据类型、数组数据替换、插入、输入、输出、字符串输入、输出、长度计算等等。一、数组概念数组是一个相同类型元素的集合。定义数组语法: <数据类型><数组名称>[数组的大小]; int data [100];解释:定义一个int类型数组名称是data,可以存放100int类型的数据,总大小字节: 100*4。<数据类型> 可以填充那些参数?基本数据类型(char 。。float、int。。。。。)、结构体数据类型、指针类型等。数组概念说明(1)[]称为下标符号,用来操作数组。比如: b=a[10]; //将a数组里第10个元素取出来赋值给b变量。(2).访问数组时,下标的范围是从0开始。比如: int a[10]; 下标范围: 0~9(3).数组在定义时,[]下标里填充的大小必须是常量。(C89标准) 比如: int a[b]; //错误 int a[10];(4).数组只有在定义的时候,才可以整体赋值。比如: int a[10]={12,34,56,78,90};(5).数组在定义的时候,如果赋了值,[]下标里可以不用指定大小。 比如: int a[]={12,34,56};(6).数组在定义的时候,至少给一个下标赋了初始值,其他下标默认置为0。比如: int a[10]={0};​#include <stdio.h>​int main(void){ int buff_i[10]={10,20,30}; //定义整数类型数组 float buff_f[10]={10.123,20.123,30.123}; //定义浮点数类型数组 char buff_c[10]="123456";//定义字符串类型数组,结尾多了一个结束符"\0" //说明: 在C语言里面没有专用字符串类型,字符串本身还是使用char类型定义。Qstring Sting char buff[10]={'1','2','3','4'}; //定义字符类型数组 int i; //打印整型 for(i=0;i<10;i++) { printf("%d ",buff_i[i]); } printf("\n"); //打印浮点类型 for(i=0;i<10;i++) { printf("%f ",buff_f[i]); } printf("\n"); //打印字符串 printf("%s\n",buff_c); //打印字符类型 for(i=0;i<10;i++) { printf("%c ",buff[i]); } printf("\n"); return 0;}二、数组输入输出#include <stdio.h>​//从键盘上录入一串数据,倒序输出int main(void){ int i; int buff[5]; printf("请输入5个数据:"); for(i=0;i<5;i++) { scanf("%d",&buff[i]); //从键盘上录入数据 } for(i=5-1;i>=0;i--) { printf("%d ",buff[i]); //将数据打印到控制台终端 } printf("\n"); return 0;}三、数组的练习题目(1). 从键盘上录入一串数据(整数),将数组里的10替换成66,并统计替换的数量。​#include <stdio.h>int main(void){ //将数组里的10替换成66,并统计替换的数量。 int buff[10]={10,34,10,347,23,10,123,56,78,10}; int i,cnt=0; for(i=0;i<10;i++) { if(buff[i]==10) { cnt++; //记录数量 buff[i]=66; } } printf("替换的数量%d\n",cnt); for(i=0;i<10;i++) { printf("%d ",buff[i]); } printf("\n"); return 0;}(2) 计算数组的大小int main(void){ int buff[]={10,34,10,347,23,10,123,56,78,10,45,10,10}; //34,347,23,123,56,78 int len=10; printf("buff数组的总大小=%d\n",sizeof(buff)); printf("buff数组有%d个成员\n",sizeof(buff)/sizeof(buff[0])); return 0;}(3)从键盘上输入一串数据(整数),将数组里大于10的数据删除掉。//比如: char buff[]={12,34,8,4,7,100};//最终的结果:{8,4,7,0,0,0}#include <stdio.h>​/*从键盘上输入一串数据(整数),将数组里大于10的数据删除掉。*/int main(void){ int buff[13]={1,34,2,347,23,3,123,56,78,4,45,5,6}; int len=13; int i,j; for(i=0;i<len;i++) { if(buff[i]>10) //当if条件成立的时候,i就是当前需要覆盖数据下标 { //将后面的所有数据向前移动一位 for(j=i;j<len-1;j++) { buff[j]=buff[j+1]; } i--; //继续判断上一次的位置,是否需要删除 len--; //总长度-- } } printf("删除之后的数据:"); for(i=0;i<len;i++) { printf("%d ",buff[i]); } printf("\n"); return 0;}四、字符串(1)字符串大小计算示例 char str1[]={'A','B','c'}; //3 char str2[]="ABC"; //4 printf("str1=%d\n",sizeof(str1)); //3 printf("str2=%d\n",sizeof(str2)); //4 //为什么是4 ?因为字符串结尾自带一个'\0' // '\0'表示是字符串的结束符号(2)字符串的长度计算#include <stdio.h>int main(void){ char str[100]; //字符串如何从键盘上录入? scanf("%s",str); //从键盘上录入一个字符串 //str为什么不加&符号? 因为数组的名称就是地址(表示是数组首地址)。 //字符串如何输出? printf("str=%s\n",str); //如何知道从键盘上输入了多少个字符? //首先得知道一点: 字符串结尾符'\0' 字符 int i=0; while(str[i]!='\0') //判断字符串的结束符 { i++; } printf("字符串的长度=%d\n",i); return 0;}(3)字符串的衔接符号#include <stdio.h>int main(void){ char str[100]="3847tr45gt\ // \表示字符串的衔接符号 rhonrbngfbn\ gf"; return 0;}
  • [技术干货] 单片机与C语言编程基础_数组与函数传参
    字符串标准处理函数介绍(string.h)、指针和数组当做函数形参,指针定义、函数返回指针、void类型定义指针、类型强制转换、常量声明、extern外边引用声明关键字。1. 总结字符串相关的处理函数string.h里常用的字符串处理相关函数。字符串: string.hvoid *memcpy(void *restrict, const void *restrict, size_t);int memcmp(const void *, const void *, size_t);void *memset(void *, int, size_t);char *strcat(char *restrict, const char *restrict);int strcmp(const char *, const char *);char *strcpy(char *restrict, const char *restrict);size_t strlen(const char *);char *strncat(char *restrict, const char *restrict, size_t);int strncmp(const char *, const char *, size_t);char *strncpy(char *restrict, const char *restrict, size_t);char *strstr(const char *, const char *);​格式化打印(转换)函数:int sprintf(char *str, const char *format, ...);int snprintf(char *str, size_t size, const char *format, ...);int sscanf(const char *str, const char *format, ...);说明: char p; // a=aa; //乘法 char *p; //定义指针 指针用于代表一个地址。 指针可以当做数组使用,数组无法当做指针使用。数组的名称: 就是首地址 在C语言里任何类型的指针(地址)是4个字节2. 函数参数: 指针与数组类型函数的形参: 指针类型与数组类型 示例代码: ()#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件int my_strlen1(char buff[]);int my_strlen2(char *buff);​int main(int argc,char **argv){ char str[]="1234567890"; int len1=my_strlen1(str); int len2=my_strlen2(str); printf("%d\n",len1); printf("%d\n",len2); return 0;}​/*函数功能:计算字符串的长度char buff[] :传入是数组类型,也就是传入的是地址*/int my_strlen1(char buff[]){ int cnt=0; while(buff[cnt]!='\0') { cnt++; } return cnt;}​/*函数功能:计算字符串的长度char *str :传入是字符指针类型,也就是传入的是地址*/int my_strlen2(char *buff){ int cnt=0; while(buff[cnt]!='\0') { cnt++; } return cnt;}3. 数组当做函数形参的问题示例:#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件​void my_func1(char buff[100]);void my_func2(char *buff);void sum(int a);​int main(int argc,char **argv){ int a=100; char str[100]="12345"; my_func1(str); my_func2(str); sum(a); return 0;}​//char buff[100] 函数形参里是地址不会申请空间void my_func1(char buff[100]){ char buff1[100]; printf("sizeof=%d\n",sizeof(buff)); printf("sizeof1=%d\n",sizeof(buff1));}​void my_func2(char *buff){ printf("sizeof=%d\n",sizeof(buff));}4. 指针定义: 存放地址#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件​int main(int argc,char **argv){ int a=100; char str[100]="123456"; char *p; //定义的是一个指针(地址) p=str; //可以赋值 //p=&a; //可以赋值​ printf("%p\n",&a); printf("%p\n",str); printf("%p\n",p); printf("%s\n",str); printf("%s\n",p); return 0;}5. 函数形参和返回值: 都是地址(1)数组类型可以当做函数形参。void func(char buff[]){}(2)数组类型不能当做函数返回值类型。char buff[] func(void){} 错误的 函数如果要返回地址类型: 必须使用指针类型。 函数形参如果要传入地址类型: 可以使用指针类型或者数组类型。示例代码:#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件char *func(char buff[]);int main(int argc,char **argv){ char str[]="12345"; char *str1=func(str); printf("%p\n",str); printf("%p\n",str1); printf("%s\n",str1); return 0;}​//形参是一个数组类型(char的地址)//返回值是一个char类型的地址char *func(char buff[]){ return buff;}​示例代码(局部地址是不能返回的)#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件char *func(void);int func1(void);int main(int argc,char **argv){ char *str=func(); printf("str=%s\n",str); printf("str2=%p\n",str); int a=func1(); printf("a=%d\n",a); return 0;}​char *func(void){ char str[]="5thgtrgbtfbfgbgf"; //static char str[]="5thgtrgbtfbfgbgf"; //加了静态关键字之后,数据空间永久保留(与main函数生命周期一样) printf("str1=%p\n",str); return str; //将数据空间的地址返回,将地址赋值给接收返回值的指针变量}​int func1(void){ int a=88; return a; //a变量里的数据取出来返回去,赋值给接收返回值的变量。}6. void*类型指针#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件void func(void *p);​int main(int argc,char **argv){ int buff[100]; char str[100]; func(buff); func(str); return 0;}​/*void *p:传入的形参自适应*/void func(void *p){ }//void a; //语法是错误的//int a; //定义一个int类型变量,空间是4个字节//void *p //指针本身是存放地址,任何类型的指针都是4个字节7. 强制转换强制转换只是欺骗编译器,消除警告,不会改变本身的数据类型。 示例代码: 指针之间的强制转换#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件void func(int *p);​int main(int argc,char **argv){ char str[]="123456789"; func((int*)str); //强制转换语法 return 0;}​/*void *p:传入的形参自适应*/void func(int *p){ char *str=(char*)p; printf("str=%s\n",str);}8. 常量声明关键字: const#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件​int main(int argc,char **argv){ char buff[100]="123"; const char *p=buff; //表示p指向的buff的空间数据是不能修改的 //*p='A'; //不能修改 赋值是错误的 p[0]='A'; //不能修改 赋值是错误的 //const int a=99;//定义a是只读变量。 //a=88; //赋值是错误的 return 0;}9. extern外部声明关键字主要是用在头文件里,多文件编程中。#include <stdio.h> //标准输入输出#include <string.h> //字符串处理头文件extern void func(void);//外部引用声明extern int data; //外部引用声明int main(int argc,char **argv){ func(); printf("%d\n",data); return 0;}int data=888; //定义变量void func(void){ printf("123\n");}
  • [技术干货] 单片机与C语言编程基础___数组全方位练习
    复盘C语言数组的练习题:涉及到数组插入、数组删除、数组下标数据的左移右移、数组排序、数组排序优化、数组的数据拼接等等。(1)函数基本使用: 函数基本定义、封装、传参、调用、局部变量、全局变量、只读变量、静态变量 (2)数组基本使用: 数组基本定义、使用赋值、字符串。1. 数组基本知识1. 概念: 同一个类型的集合。2. 定义: int data[100]; //数组data里可以存放100个int类型 100必须是常量(C89)。定义数组时,如果没有赋初始值[]一定要填大小。 int data[]={1};3.数组赋值: 只有在定义的使用才可以整体赋值。int data[10]={12,34,56,7,89};4.数组下标[]访问从0开始。 比如: buff[100]; 下标范围: 0~995.数组的名称就是首地址。int buff[100];1.2 字符串数组1. 字符串录入: scanf(“%s”); gets() fgets2. 头文件: string.h3. 从键盘录入字符串的方法 scanf("%s",str); //从键盘上录入字符串,字符串结尾: '\0'gets(str); //从键盘上录入字符串fgets(str,100,stdin); //从键盘上录入字符串 (标准输入)​#include <stdio.h>int main(int argc,char **argv){ int int_data[100]={12,34,56,78}; //整型数组 char char_data[]={'A','B','C'}; //字符数组 char str1_data[]={'A','B','C','\0'}; //字符串数组 char str2_data[]="ABC";//字符串数组 printf("char_data=%d\n",sizeof(char_data)); //3 printf("str1_data=%d\n",sizeof(str1_data)); //4 printf("str2_data=%d\n",sizeof(str2_data)); //4 return 0;}字符串数组特性: 结尾有’\0’2. 数组相关的题目(分析)2.1 数组的数据插入 int a[]={1,2,3,4,5,6,7}; 在第2位数据后插入一个888 结果: 1,2,888,3,4,5,6,7#include <stdio.h>int main(int argc,char **argv){ int data[10]={1,2,3,4,5,6,7,8,9}; //1,2,3,4,4,5,6,7,8,9 int addr; int i; printf("输入插入的位置:"); scanf("%d",&addr); /*1. 向后移动数据,空出位置*/ for(i=8;i>=addr;i--) { data[i+1]=data[i]; } /*2. 查看移动之后的效果*/ for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); data[addr]=888; //插入的数据 /*3. 最终效果*/ for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); return 0;}2.2 数组数据的删除 int a[]={1,2,3,4,2,6,7}; 将第2位数据删除掉结果: 1,3,4,5,6,7示例:#include <stdio.h>int main(int argc,char **argv){ int data[10]={1,2,3,4,5,6,7,8,9,10}; //1,2,4,5,6,7,8,9,10,10 int addr; int i; printf("输入删除的位置:"); scanf("%d",&addr); //2​ /*1. 向前移动,将数据覆盖掉*/ for(i=addr-1;i<10-1;i++) { data[i]=data[i+1]; } /*2. 查看移动之后的效果*/ for(i=0;i<9;i++)printf("%d ",data[i]); printf("\n"); return 0;}2.3 变量排序数组数据排序。从键盘上输入一串数据(整数),从大到小排序,再输出。 int a[]={12,67,45,13,1,5}; 排序之后: 67,45,13,12,5,1排序: 计算平均数。班级成绩、温度数据......例子: 从键盘上随机输入3个数,将3个数按照从大到小的顺序输出。#include <stdio.h>int main(int argc,char **argv){ int a,b,c; int tmp; //存放临时数据 printf("请输入3个数:"); scanf("%d%d%d",&a,&b,&c); //以回车作为结束符号,以空格作为间隔符号 printf("源数据:%d,%d,%d\n",a,b,c); //从大到小 if(a<b) //b,a,c { tmp=a; a=b; b=tmp; } if(a<c) //b,c,a { tmp=a; a=c; c=tmp; } if(b<c) //c,b,a { tmp=b; b=c; c=tmp; } printf("从大到小排序:%d,%d,%d\n",a,b,c); return 0;}2.4 数组冒泡排序#include <stdio.h>int main(int argc,char **argv){ int data[10]; int tmp; //存放临时数据 int i,j; int cnt=0; //排序比较的总次数 /*1. 录入数据*/ printf("请输入10个数:"); for(i=0;i<10;i++)scanf("%d",&data[i]); /*2. 打印源数据:*/ printf("源数据:"); for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); /*3. 冒泡排序*/ for(i=0;i<10-1;i++) //比较轮数 { for(j=0;j<10-1-i;j++) //每一轮比较的次数 { if(data[j]>data[j+1]) //从小到大 { tmp=data[j]; data[j]=data[j+1]; data[j+1]=tmp; } cnt++; } } /*4. 打印结果数据:*/ printf("排序之后的数据:"); for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); printf("排序的总次数:%d\n",cnt); return 0;}分析:/*data[]={1,2,3}第一轮: [0]和[1] [1]和[2] 2 3 1 每一轮保证最小的数在最后第二轮: [0]和[1] 3 2 1*/​排序: 从大到小1 2 3 4 5 第一轮:(1) 2 1 3 4 5(2) 2 3 1 4 5(3) 2 3 4 1 5(4) 2 3 4 5 1 第二轮:(1) 3 2 4 5 1(2) 3 4 2 5 1(3) 3 4 5 2 1(4) 3 4 5 2 1​第三轮:(1) 4 3 5 2 1(2) 4 5 3 2 1(3) 4 5 3 2 1(4) 4 5 3 2 1​第四轮:(1) 5 4 3 2 1(2) 5 4 3 2 1(3) 5 4 3 2 1(4) 5 4 3 2 12.5 冒泡排序优化#include <stdio.h>int main(int argc,char **argv){ int data[10]; int tmp; //存放临时数据 int i,j; int cnt=0; //排序比较的总次数 45: int flag=0; //标志位置 /*1. 录入数据*/ printf("请输入10个数:"); for(i=0;i<10;i++)scanf("%d",&data[i]); /*2. 打印源数据:*/ printf("源数据:"); for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); /*3. 冒泡排序*/ for(i=0;i<10-1;i++) //比较轮数 { for(j=0;j<10-1-i;j++) //每一轮比较的次数 { if(data[j]>data[j+1]) //从小到大 { tmp=data[j]; data[j]=data[j+1]; data[j+1]=tmp; flag=1; } cnt++; } if(flag==0) //不需要继续排序 { break; //跳出最外层循环 } flag=0; //清除标志位,方便第2次判断 } /*4. 打印结果数据:*/ printf("排序之后的数据:"); for(i=0;i<10;i++)printf("%d ",data[i]); printf("\n"); printf("排序的总次数:%d\n",cnt); return 0;}分析:data[]={1,2,3}第一轮: [0]和[1] [1]和[2] 2 3 1 每一轮保证最小的数在最后第二轮: [0]和[1] 3 2 12.6 数组数据的拼接(整数) int a[]={1,3,4,5,6,7}; int b[]={11,13,14,15,16,17};将a和b拼接到c数组里。 最终int c[]={1,3,4,5,6,7, 11,13,14,15,16,17} 必须保证c数组空间足够大。示例代码:#include <stdio.h>int main(int argc,char **argv){ int src_data[10]={1,2,3,4,5}; int new_data[]={6,7,8,9}; // int i; for(i=0;i<sizeof(new_data)/sizeof(new_data[0]);i++) { src_data[i+5]=new_data[i]; } for(i=0;i<10;i++) { printf("%d ",src_data[i]); } printf("\n"); return 0;}
  • [技术干货] OpenCV4.x(C++)人脸检测(眼睛、侧脸、正脸)
    一、前言OpenCV是一款广泛使用的计算机视觉库,提供了许多强大的功能,包括人脸检测和识别。人脸分类器是OpenCV中用于人脸检测的关键工具之一,能够快速准确地检测出图像中的人脸。本文将介绍如何使用OpenCV自带的人脸分类器,并对比不同分类器的精度。在日常生活中,人脸检测的应用非常广泛,例如安防、人机交互、智能交通等领域。而在计算机视觉领域,人脸检测也是一个非常热门的研究方向。OpenCV作为一款免费、开源的计算机视觉库,为我们提供了一种方便快捷的人脸检测方法。使用OpenCV的人脸分类器,可以快速地检测出图像中的正脸、侧脸和眼睛等部位,进而实现更加智能的应用。OpenCV自带的Haar级联分类器模型:haarcascade_eye.xml: 这个模型用于检测眼睛。 haarcascade_eye_tree_eyeglasses.xml: 这个模型用于检测眼镜。 haarcascade_frontalcatface.xml: 这个模型用于检测猫脸。 haarcascade_frontalcatface_extended.xml: 这个模型用于扩展的猫脸检测。 haarcascade_frontalface_alt.xml: 这个模型是一个备用的面部检测模型。 haarcascade_frontalface_alt2.xml: 这个模型是另一个备用的面部检测模型。 haarcascade_frontalface_alt_tree.xml: 这个模型是用于面部检测的备用树模型。 haarcascade_frontalface_default.xml: 这个模型是用于面部检测的默认模型。 haarcascade_fullbody.xml: 这个模型用于全身检测。 haarcascade_lefteye_2splits.xml: 这个模型用于检测左眼。 haarcascade_licence_plate_rus_16stages.xml: 这个模型用于检测俄罗斯车牌。 haarcascade_lowerbody.xml: 这个模型用于下半身检测。 haarcascade_profileface.xml: 这个模型用于侧面脸部检测。 haarcascade_righteye_2splits.xml: 这个模型用于检测右眼。 haarcascade_russian_plate_number.xml: 这个模型用于检测俄罗斯车牌号码。 haarcascade_smile.xml: 这个模型用于微笑检测。这些文件在OpenCV的安装目录下。二、代码实现2.1 人脸分类器检测人脸#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>#include <opencv2/objdetect/objdetect.hpp>#include <opencv2/imgproc/imgproc.hpp>​int main(){ // 加载人脸分类器 cv::CascadeClassifier faceCascade;​ //分类器文件下载地址: https://github.com/opencv/opencv/tree/master/data/haarcascades //在OpenCV的源码目录下其实也有(opencv\build\etc\haarcascades)。 //下载后放到C盘根目录即可. faceCascade.load("C:/haarcascade_frontalface_alt2.xml");​ // 打开摄像头 cv::VideoCapture capture(0); if (!capture.isOpened()) { std::cout << "无法打开摄像头" << std::endl; return -1; }​ // 创建窗口 cv::namedWindow("Face Detection", cv::WINDOW_NORMAL);​ while (true) { cv::Mat frame; capture >> frame; // 读取视频帧​ // 将彩色图像转换为灰度图像以加快处理速度 cv::Mat grayFrame; cv::cvtColor(frame, grayFrame, cv::COLOR_BGR2GRAY);​ // 对图像进行人脸检测 std::vector<cv::Rect> faces; faceCascade.detectMultiScale(grayFrame, faces, 1.1, 3, 0, cv::Size(30, 30));​ // 在图像上绘制人脸边界框 for (size_t i = 0; i < faces.size(); i++) { cv::rectangle(frame, faces[i], cv::Scalar(0, 255, 0), 2); }​ // 显示结果图像 cv::imshow("Face Detection", frame);​ // 按下ESC键退出循环 if (cv::waitKey(1) == 27) break; }​ // 释放摄像头和窗口资源 capture.release(); cv::destroyAllWindows();​ return 0;}​​运行效果:2.2 侧脸分类器检测人脸#include <opencv2/opencv.hpp>#include <opencv2/highgui/highgui.hpp>#include <opencv2/objdetect/objdetect.hpp>#include <opencv2/imgproc/imgproc.hpp>​int main(){ // 加载人脸分类器 cv::CascadeClassifier faceCascade;​ //分类器文件下载地址: https://github.com/opencv/opencv/tree/master/data/haarcascades //在OpenCV的源码目录下其实也有(opencv\build\etc\haarcascades)。 //下载后放到C盘根目录即可. faceCascade.load("C:/haarcascade_profileface.xml");​ // 打开摄像头 cv::VideoCapture capture(0); if (!capture.isOpened()) { std::cout << "无法打开摄像头" << std::endl; return -1; }​ // 创建窗口 cv::namedWindow("Face Detection", cv::WINDOW_NORMAL);​ while (true) { cv::Mat frame; capture >> frame; // 读取视频帧​ // 将彩色图像转换为灰度图像以加快处理速度 cv::Mat grayFrame; cv::cvtColor(frame, grayFrame, cv::COLOR_BGR2GRAY);​ // 对图像进行人脸检测 std::vector<cv::Rect> faces; faceCascade.detectMultiScale(grayFrame, faces, 1.1, 3, 0, cv::Size(30, 30));​ // 在图像上绘制人脸边界框 for (size_t i = 0; i < faces.size(); i++) { cv::rectangle(frame, faces[i], cv::Scalar(0, 255, 0), 2); }​ // 显示结果图像 cv::imshow("Face Detection", frame);​ // 按下ESC键退出循环 if (cv::waitKey(1) == 27) break; }​ // 释放摄像头和窗口资源 capture.release(); cv::destroyAllWindows();​ return 0;}三、OpenCV安装3.1 OpenCV下载OpenCV下载地址:cid:link_1目前最新的版本是4.3,那么就下载最新的版本。下载下来是一个exe文件,双击就可以安装,实际就是解压,可以选择解压的路径,解压出来的文件包含源文件、库文件一大堆,比较大,可以直接放在一个固定的目录,后面程序里直接填路径来调用即可。 这个下载下来的库文件里只包含了X64的库,适用于MSVS 64位编译器。解压完成。解压后在build目录下看到有VC14和VC15的目录。这表示什么含义呢?OpenCV VC14和VC15的区别在于它们所使用的编译器版本不同。VC14使用的是Visual Studio 2015的编译器,而VC15使用的是Visual Studio 2017的编译器。这意味着VC15可以利用更先进的编译器技术,从而提高代码的性能和效率。此外,VC15还支持更多的C++11和C++14特性,使得开发更加方便和灵活。3.2 VS2022环境我这里介绍下我用的环境安装过程。 所有版本的VS都可以的,OpenCV只是个第三方库,哪里调用都行。我当前环境是在Windows下,IDE用的是地表最强IDEVS2022。下载地址:cid:link_0因为我这里只需要用到C++和C语言编程,那么安装的时候可以自己选择需要安装的包。安装好之后,创建项目。3.3 新建工程这是创建好的空工程,我写了一段OpenCV的代码。工程创建好之后需要添加OpenCV头文件的引用和OpenCV库文件的引用。点击这个属性。第一步在C++、常规 选项里添加用到的OpenCV头文件路径。这个路径具体在哪里,要看自己的OpenCV安装路径。为了方便大家粘贴,我这里贴出来。C:/opencv_4.x/opencv/build/include/opencv2C:/opencv_4.x/opencv/build/include/opencvC:/opencv_4.x/opencv/build/include第二步就是设置库文件的路径。 在链接器-输入 选项里,添加依赖选项。这个库在哪里,根据自己OpenCV解压的路径进行填。这是我的路径:C:/opencv_4.x/opencv/build/x64/vc15/lib/opencv_world430.lib3.4 运行库的拷贝如果写好了OpenCV代码,直接按下Ctrl + F5 运行程序,如果第一次运行,会报错。提示如下:这个提示是告诉我们,程序运行时找不到OpenCV的运行库。 只要使用了第三方库都需要知道这一点,运行的时候需要把用到的库拷贝到生成的exe同级目录下。把OpenCV解压目录下的opencv_world430.dll文件拷贝到编译出来的exe运行同级目录下。 否则程序运行因为找不到库而导致异常结束。拷贝到这里。再次运行,程序就正常的运行了。
  • [技术干货] STM32单片机采用环形缓冲区实现串口中断数据接收管理
    一、前言在嵌入式系统开发中,与上位机进行串口通信是非常常见的场景。上位机可以通过串口发送指令或者数据给嵌入式设备,而嵌入式设备需要可靠地接收并解析这些数据,以执行相应的操作。然而,在串口通信过程中,上位机发送数据的速率往往与嵌入式设备接收和处理数据的速率不一致,这就可能导致数据的丢失或者误解析。为了解决这个问题,决定设计并实现一个环形缓冲区来进行数据接收管理。环形缓冲区是一种高效的数据结构,适用于数据产生速率快于消费速率的场景。它具有固定大小的缓冲区,并且可以循环利用空间,保证数据的连续存储和有效利用。在本项目中,选择使用STM32微控制器来实现串口数据接收功能。STM32具有丰富的外设资源和强大的性能,非常适合用于串口通信和数据处理。通过在STM32上实现环形缓冲区,可以实现以下目标:(1)数据稳定接收:通过使用环形缓冲区,确保即使在接收数据速率慢于发送速率的情况下,数据也能够得到稳定的接收,避免数据丢失。(2)数据缓存和管理:环形缓冲区可以作为一个数据缓存区,将接收到的数据暂时存储起来,以便后续处理。这样可以降低数据处理的延迟和复杂性。(3)数据解析和应用:通过从环形缓冲区中读取数据,并进行解析和处理,嵌入式设备可以根据接收到的数据执行相应的操作,如控制外部设备或响应上位机指令。通过使用环形缓冲区管理串口接收的数据,可以实现可靠的数据接收和处理,并提高系统的稳定性和可靠性。同时,该方案也适用于其他嵌入式系统和通信场景。二、实现思路(1)定义环形缓冲区的结构体:首先,需要定义一个表示环形缓冲区的结构体,其中包含以下成员变量:缓冲区的大小(capacity):表示环形缓冲区的容量,即可以存储的最大元素数量。写指针(write_ptr):表示当前可写入数据的位置。读指针(read_ptr):表示当前可读取数据的位置。数据数组(buffer):用于存储实际的数据。(2)初始化环形缓冲区:在使用环形缓冲区之前,需要进行初始化。初始化时,将缓冲区的大小、写指针和读指针都设置为初始位置,通常都是0。(3)写入数据:当有新的数据要写入缓冲区时,需要执行以下操作:检查缓冲区是否已满,如果已满则无法写入新的数据。将数据写入当前写指针所指向的位置。更新写指针的位置,通常是将其加1,并考虑到环形特性,需要进行取模运算。(4)读取数据:当需要从缓冲区中读取数据时,需要执行以下操作:检查缓冲区是否为空,如果为空则无数据可读取。读取当前读指针所指向的数据。更新读指针的位置,通常是将其加1,并考虑到环形特性,需要进行取模运算。(5)判断缓冲区状态:为了方便使用和管理缓冲区,可以实现一些用于判断缓冲区状态的函数,例如:is_full():判断缓冲区是否已满。is_empty():判断缓冲区是否为空。实现环形缓冲区时,需要注意:写指针和读指针的位置计算要考虑到环形特性,即超过缓冲区容量时需要进行取模运算。缓冲区大小要合理选择,根据实际需求确定,以充分利用内存资源并避免数据丢失。多线程或中断环境下的并发访问要考虑数据同步和互斥操作,以避免竞争条件和数据不一致的问题。通过以上思路,可以在C语言中实现一个简单高效的环形缓冲区,用于存储和管理数据,在数据收发过程中提高系统的稳定性和可靠性。三、 C语言实现验证思路#include <stdio.h>#include <stdlib.h>​#define BUFFER_SIZE 10​typedef struct { int* buffer; // 缓冲区数组指针 int size; // 缓冲区大小 int head; // 头部索引 int tail; // 尾部索引} CircularBuffer;​// 创建环形缓冲区CircularBuffer* createCircularBuffer(int size) { CircularBuffer* cb = (CircularBuffer*)malloc(sizeof(CircularBuffer)); // 分配内存空间 cb->buffer = (int*)malloc(sizeof(int) * size); // 分配缓冲区数据的内存空间 cb->size = size; // 设置缓冲区大小 cb->head = 0; // 初始化头部索引为0 cb->tail = 0; // 初始化尾部索引为0 return cb;}​// 销毁环形缓冲区void destroyCircularBuffer(CircularBuffer* cb) { free(cb->buffer); // 释放缓冲区数据的内存空间 free(cb); // 释放缓冲区结构体的内存空间}​// 判断环形缓冲区是否已满int isCircularBufferFull(CircularBuffer* cb) { return ((cb->tail + 1) % cb->size == cb->head);}​// 判断环形缓冲区是否为空int isCircularBufferEmpty(CircularBuffer* cb) { return (cb->head == cb->tail);}​// 写入数据到环形缓冲区void writeData(CircularBuffer* cb, int data) { if (isCircularBufferFull(cb)) { // 如果缓冲区已满,则无法写入数据 printf("Circular buffer is full. Data cannot be written.\n"); return; } cb->buffer[cb->tail] = data; // 将数据写入缓冲区的尾部 cb->tail = (cb->tail + 1) % cb->size; // 更新尾部索引,循环利用缓冲区空间}​// 从环形缓冲区读取数据int readData(CircularBuffer* cb) { if (isCircularBufferEmpty(cb)) { // 如果缓冲区为空,则无数据可读取 printf("Circular buffer is empty. No data to read.\n"); return -1; // 返回一个默认值表示读取失败 } int data = cb->buffer[cb->head]; // 从缓冲区的头部读取数据 cb->head = (cb->head + 1) % cb->size; // 更新头部索引,循环利用缓冲区空间 return data;}​int main() { CircularBuffer* cb = createCircularBuffer(BUFFER_SIZE); // 创建大小为BUFFER_SIZE的环形缓冲区​ writeData(cb, 1); // 写入数据1 writeData(cb, 2); // 写入数据2 writeData(cb, 3); // 写入数据3​ printf("Read data: %d\n", readData(cb)); // 读取数据并打印 printf("Read data: %d\n", readData(cb));​ writeData(cb, 4); writeData(cb, 5);​ printf("Read data: %d\n", readData(cb)); printf("Read data: %d\n", readData(cb)); printf("Read data: %d\n", readData(cb));​ destroyCircularBuffer(cb); // 销毁环形缓冲区​ return 0;}​四、STM32串口接收#define BUFFER_SIZE 256​typedef struct { uint8_t buffer[BUFFER_SIZE]; uint16_t head; uint16_t tail;} CircularBuffer;​// 初始化环形缓冲区void CircularBuffer_Init(CircularBuffer* cb) { cb->head = 0; cb->tail = 0;}​// 判断环形缓冲区是否已满bool CircularBuffer_IsFull(const CircularBuffer* cb) { return (cb->head + 1) % BUFFER_SIZE == cb->tail;}​// 判断环形缓冲区是否为空bool CircularBuffer_IsEmpty(const CircularBuffer* cb) { return cb->head == cb->tail;}​// 向环形缓冲区写入数据bool CircularBuffer_Write(CircularBuffer* cb, uint8_t data) { if (CircularBuffer_IsFull(cb)) { // 缓冲区已满,无法写入 return false; } cb->buffer[cb->head] = data; cb->head = (cb->head + 1) % BUFFER_SIZE; return true;}​// 从环形缓冲区读取数据bool CircularBuffer_Read(CircularBuffer* cb, uint8_t* data) { if (CircularBuffer_IsEmpty(cb)) { // 缓冲区为空,无数据可读取 return false; } *data = cb->buffer[cb->tail]; cb->tail = (cb->tail + 1) % BUFFER_SIZE; return true;}​// 获取环形缓冲区剩余大小uint16_t CircularBuffer_GetRemainingSize(const CircularBuffer* cb) { if (cb->head >= cb->tail) { return BUFFER_SIZE - (cb->head - cb->tail); } else { return cb->tail - cb->head - 1; }}​// 获取环形缓冲区已写入大小uint16_t CircularBuffer_GetWrittenSize(const CircularBuffer* cb) { if (cb->head >= cb->tail) { return cb->head - cb->tail; } else { return BUFFER_SIZE - (cb->tail - cb->head - 1); }}​// 从环形缓冲区读取指定长度的数据bool CircularBuffer_ReadData(CircularBuffer* cb, uint8_t* data, uint16_t length) { if (CircularBuffer_GetWrittenSize(cb) < length) { return false; // 缓冲区中的数据不足 } for (uint16_t i = 0; i < length; ++i) { if (!CircularBuffer_Read(cb, &data[i])) { return false; // 读取数据出错 } } return true;}​// 向环形缓冲区写入指定长度的数据bool CircularBuffer_WriteData(CircularBuffer* cb, const uint8_t* data, uint16_t length) { if (CircularBuffer_GetRemainingSize(cb) < length) { return false; // 缓冲区剩余空间不足 } for (uint16_t i = 0; i < length; ++i) { if (!CircularBuffer_Write(cb, data[i])) { return false; // 写入数据出错 } } return true;}​​​// 示例:STM32串口接收中断处理函数void USART_Receive_IRQHandler(void) { uint8_t data = USART_ReceiveData(USART1); // 获取接收到的数据 if (!CircularBuffer_Write(&rxBuffer, data)) { // 缓冲区已满,处理错误 }}​​在代码中,定义了一个名为CircularBuffer的结构体来表示环形缓冲区。包含了一个具有固定大小的数组buffer用于存储数据,以及头部指针head和尾部指针tail用于管理数据的读写位置。接下来,实现了一些函数来对环形缓冲区进行操作。CircularBuffer_Init函数用于初始化环形缓冲区;CircularBuffer_IsFull和CircularBuffer_IsEmpty函数分别判断缓冲区是否已满和是否为空;CircularBuffer_Write函数用于向缓冲区写入数据;CircularBuffer_Read函数用于从缓冲区读取数据。CircularBuffer_GetRemainingSize函数用于获取环形缓冲区的剩余大小,即还能写入多少个字节的数据;CircularBuffer_GetWrittenSize函数用于获取已经写入到缓冲区的字节数;CircularBuffer_ReadData函数用于从环形缓冲区读取指定长度的数据,将其存储到提供的数据数组中;CircularBuffer_WriteData函数用于向环形缓冲区写入指定长度的数据,从提供的数据数组中复制相应的字节。使用这些方便函数,可以更方便地管理环形缓冲区,实现数据的读取和写入。最后,给出了一个示例,展示在STM32串口接收中断处理函数中将接收到的数据写入环形缓冲区。在中断处理函数中,通过USART_ReceiveData函数获取接收到的数据,调用CircularBuffer_Write函数将数据写入缓冲区。
  • [技术干货] 基于单片机设计的智慧农业大棚检测系统
    一、设计目标本项目基于单片机设计一个智慧农业大棚检测系统,以提供实时监测和管理大棚环境的关键参数。系统支持环境温度、湿度检测,光照强度检测,并能根据预设的阀值进行报警提示。为了实现数据的显示和管理,该系统还利用Qt开发了一款对应的Android手机APP,通过蓝牙传输模块将单片机采集到的数据传递到手机APP上进行显示和管理。具体功能如下:【1】环境温度和湿度检测:系统采用SHT30温湿度传感器,能够实时监测大棚内的温度和湿度,并将数据传输给单片机进行处理。【2】光照强度检测:系统采用BH1750光照传感器,能够实时监测大棚内的光照强度,并将数据传输给单片机进行处理。【3】报警阀值设置:系统支持按键操作,用户可以通过按键调整报警阀值,以适应不同的农业环境需求。【4】报警声音提示:当温度、湿度或光照强度超过设定的阀值时,系统将触发报警,通过连接的蜂鸣器发出声音提示,提醒用户注意。【5】数据显示和管理:利用Qt开发的Android手机APP能够接收通过蓝牙传输模块从单片机端传递过来的数据,并在手机上进行实时显示和管理。用户可以通过手机APP查看当前的温度、湿度和光照强度数据,同时也可以设置报警阀值。通过上述设计,该智慧农业大棚检测系统能够为农业生产提供实时的环境监测和报警功能,帮助农民有效管理大棚环境,提高农作物的产量和质量。同时,通过手机APP的使用,用户可以方便地查看和管理数据,实现远程监控和控制,提高农业生产的便捷性和智能化水平。二、总体设计方案2.1 硬件设计主控芯片采用STM32F103RCT6,具有较高的性能和丰富的外设接口。温湿度检测模块采用SHT30传感器,可准确采集环境温度和湿度数据。光照强度检测模块采用BH1750传感器,能够实时监测大棚内的光照情况。本地报警提示采用蜂鸣器,发出声音提醒农民进行处理。通过蓝牙模块HC05,将STM32采集到的数据传输到Android手机APP上。2.2 软件设计在STM32中编写固件程序,实现温湿度传感器和光照强度传感器的数据采集和处理。设计蜂鸣器的驱动程序,根据设定的阈值判断是否触发报警。使用蓝牙模块HC05与Android手机APP进行通信,将采集到的数据传输到手机APP上。在Android手机APP中,利用Qt开发界面,实现数据显示、阈值设定和报警提示等功能。2.3 整体流程STM32通过SHT30和BH1750传感器采集环境温度、湿度和光照强度数据。处理采集到的数据,判断是否触发报警条件。如果达到报警条件,则通过蜂鸣器发出声音提示。将数据通过蓝牙模块HC05传输到Android手机APP上。在Android手机APP上,实时显示大棚内的温湿度和光照强度数据。农民可以通过手机APP设置报警阈值,当超过或低于设定的阈值时,会触发报警提示。该方案实现了温湿度、光照强度的实时检测和数据传输,并提供了报警功能。通过这个智慧农业大棚检测系统,农民可以方便地监测大棚内的环境状况,及时调控和管理,提高农作物的生长效果和产量。三、硬件电路设计本项目的硬件电路设计主要涉及主控芯片STM32F103RCT6的连接及传感器模块的接口设计。以下是项目的硬件电路设计概述:3.1 主控芯片连接STM32F103RCT6作为主控芯片,负责整个系统的控制和数据处理。它与其他模块通过引脚连接进行数据的接收和发送。 需要为主控芯片提供适当的电源供电,包括正常工作电压和逻辑电压。3.2 温湿度传感器连接温湿度传感器SHT30通过I2C总线连接到主控芯片。主控芯片上的I2C接口引脚(如SDA和SCL)与传感器的对应引脚相连,以实现数据的读取和控制。3.3 光照传感器连接光照传感器BH1750通过I2C总线连接到主控芯片。主控芯片上的I2C接口引脚与传感器的对应引脚相连,以实现数据的读取和控制。3.4 报警蜂鸣器连接报警蜂鸣器通过一个GPIO引脚与主控芯片相连。当报警条件触发时,主控芯片控制该引脚输出高电平信号,以激活蜂鸣器发出声音提示。3.5 HC05蓝牙模块连接HC05蓝牙模块用于实现单片机与Android手机APP之间的数据传输。它通过串口通信与主控芯片相连,主控芯片上的对应串口引脚(如UART_TX和UART_RX)与蓝牙模块的对应引脚相连。四、软件设计4.1 主控芯片模块主控芯片(如STM32F103RCT6)负责整个系统的控制和数据处理。它与其他硬件模块相连接,接收传感器数据,进行数据处理和报警判断,并控制蜂鸣器的发声。4.2 温湿度传感器模块温湿度传感器(如SHT30)通过I2C总线与主控芯片相连,负责实时监测大棚内的温度和湿度。传感器模块将采集到的数据传输给主控芯片进行处理。实现代码如下:以下是STM32标准库驱动SHT30传感器读取温湿度,并将数据通过串口打印出来:#include "stm32f10x.h"#include "stdio.h"​#define SHT30_ADDR 0x44​void I2C1_Init(void){ GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure;​ // 使能I2C1和GPIOB时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);​ // I2C1引脚配置 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出 GPIO_Init(GPIOB, &GPIO_InitStructure);​ // I2C1配置 I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 50%占空比 I2C_InitStructure.I2C_OwnAddress1 = 0x00; // 主机模式下无效 I2C_InitStructure.I2C_Ack = I2C_Ack_Disable; // 禁止应答 I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed = 100000; // 100kHz的速度 I2C_Init(I2C1, &I2C_InitStructure);​ // 使能I2C1 I2C_Cmd(I2C1, ENABLE);}​void I2C1_Start(void){ // 发送起始信号 I2C_GenerateSTART(I2C1, ENABLE);​ // 等待起始信号发送完成 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) ;}​void I2C1_Stop(void){ // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE);}​void I2C1_WriteByte(uint8_t byte){ // 发送一个字节的数据 I2C_SendData(I2C1, byte);​ // 等待发送完成 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) ;}​uint16_t I2C1_ReadByteAck(void){ uint16_t data;​ // 使能应答 I2C_AcknowledgeConfig(I2C1, ENABLE); I2C_GenerateACK(I2C1, ENABLE);​ // 等待接收完成 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)) ;​ // 读取接收到的数据 data = I2C_ReceiveData(I2C1);​ return data;}​uint16_t I2C1_ReadByteNack(void){ uint16_t data;​ // 禁止应答 I2C_AcknowledgeConfig(I2C1, DISABLE); I2C_GenerateACK(I2C1, DISABLE);​ // 等待接收完成 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)) ;​ // 读取接收到的数据 data = I2C_ReceiveData(I2C1);​ return data;}​void SHT30_Init(void){ // 发送软件复位指令 I2C1_Start(); I2C1_WriteByte(SHT30_ADDR << 1); I2C1_WriteByte(0x30); I2C1_WriteByte(0xA2); I2C1_Stop();​ delay_ms(100);}​void SHT30_Measure(float *temperature, float *humidity){ uint8_t buf[6]; uint16_t temperature_raw, humidity_raw;​ // 发送测量指令 I2C1_Start(); I2C1_WriteByte(SHT30_ADDR << 1); I2C1_WriteByte(0x2C); I2C1_WriteByte(0x06); I2C1_Stop();​ delay_ms(20);​ // 读取测量结果 I2C1_Start(); I2C1_WriteByte((SHT30_ADDR << 1) | 0x01); buf[0] = I2C1_ReadByteAck(); buf[1] = I2C1_ReadByteAck(); buf[2] = I2C1_ReadByteAck(); buf[3] = I2C1_ReadByteAck(); buf[4] = I2C1_ReadByteAck(); buf[5] = I2C1_ReadByteNack(); I2C1_Stop();​ // 计算温度值 temperature_raw = (buf[0] << 8) | buf[1]; *temperature = -45.0 + 175.0 * (float)temperature_raw / 65535.0;​ // 计算湿度值 humidity_raw = (buf[3] << 8) | buf[4]; *humidity = 100.0 * (float)humidity_raw / 65535.0;}​void USART1_Init(void){ GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure;​ // 使能USART1和GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);​ // USART1引脚配置 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure);​ // USART1配置 USART_InitStructure.USART_BaudRate = 115200; // 波特率为115200 USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure);​ // 使能USART1 USART_Cmd(USART1, ENABLE);}​void USART1_SendChar(char ch){ // 等待发送缓冲区为空 while (!(USART1->SR & USART_FLAG_TXE)) ;​ // 发送一个字符 USART_SendData(USART1, ch);}​int fputc(int ch, FILE *f){ // 将数据通过串口发送 USART1_SendChar((char)ch); return ch;}​void delay_ms(uint32_t ms){ while (ms--) { uint32_t count = 12000; while (count--) ; }}​int main(void){ float temperature, humidity;​ I2C1_Init(); USART1_Init();​ SHT30_Init();​ printf("SHT30 Temperature and Humidity Test\n");​ while (1) { SHT30_Measure(&temperature, &humidity); printf("Temperature: %.2f°C, Humidity: %.2f%%\n", temperature, humidity);​ delay_ms(1000); // 每隔1秒测量一次温湿度 }}以上代码通过I2C总线驱动STM32F103读取SHT30温湿度传感器的数据,并通过USART1串口打印出温度和湿度值。代码设计流程介绍:【1】引入头文件:#include "stm32f10x.h"#include "stdio.h"此处引入了STM32F10x系列微控制器的相关头文件以及标准输入输出库的头文件。【2】定义宏和函数:#define SHT30_ADDR 0x44定义了SHT30传感器的I2C地址为0x44。void I2C1_Init(void);void I2C1_Start(void);void I2C1_Stop(void);void I2C1_WriteByte(uint8_t byte);uint16_t I2C1_ReadByteAck(void);uint16_t I2C1_ReadByteNack(void);定义了一系列用于控制I2C总线的函数。void SHT30_Init(void);void SHT30_Measure(float *temperature, float *humidity);定义了初始化SHT30传感器和测量温湿度的函数。void USART1_Init(void);void USART1_SendChar(char ch);int fputc(int ch, FILE *f);定义了初始化USART1串口和发送字符的函数,以及重定向标准输出流的函数。void delay_ms(uint32_t ms);定义了延时函数,用于在实现中添加延时。【3】初始化函数:void I2C1_Init(void)该函数用于初始化I2C1总线和相关的引脚。使能了I2C1和GPIOB的时钟,然后配置了I2C1的引脚,包括引脚的速度和模式等。接着对I2C1进行配置,包括模式、占空比、从机地址等参数,并最后使能I2C1总线。void USART1_Init(void)该函数用于初始化USART1串口和相关的引脚。使能了USART1和GPIOA的时钟,然后配置了USART1的引脚,包括引脚的速度和模式等。接着对USART1进行配置,包括波特率、字长、停止位、校验位等参数,并最后使能USART1串口。【4】I2C总线控制函数:void I2C1_Start(void)void I2C1_Stop(void)void I2C1_WriteByte(uint8_t byte)uint16_t I2C1_ReadByteAck(void)uint16_t I2C1_ReadByteNack(void)这些函数用于控制I2C总线的起始、停止、写数据和读数据操作。具体实现可以参考STM32的标准库函数。【5】SHT30传感器初始化和测量函数:void SHT30_Init(void)void SHT30_Measure(float *temperature, float *humidity)SHT30_Init函数用于初始化SHT30传感器。在函数中,发送软件复位指令给传感器,然后延时一段时间等待传感器重置。SHT30_Measure函数用于测量温湿度数据。在函数中,发送测量指令给传感器,然后延时等待传感器完成测量。接着从传感器读取温湿度数据,并通过指针参数返回给主程序。【6】串口控制函数:void USART1_SendChar(char ch)int fputc(int ch, FILE *f)USART1_SendChar函数用于通过USART1串口发送一个字符。在函数中,通过轮询USART状态寄存器的空闲标志位,判断发送缓冲区是否为空,然后把字符写入数据寄存器进行发送。fputc函数是C库函数的重定向函数,用于将标准输出的字符发送到USART1串口。这里对于每个调用printf函数输出的字符,都会通过USART1_SendChar函数发送出去。【7】延时函数:void delay_ms(uint32_t ms)这个函数用于实现毫秒级的延时。在函数中,通过循环等待的方式实现了延时。【8】主函数:int main(void)在主函数中,调用I2C1_Init和USART1_Init函数初始化I2C总线和USART1串口。然后调用SHT30_Init函数初始化SHT30传感器。进入主循环后,通过循环调用SHT30_Measure函数测量温湿度数据,并通过printf函数打印出来。最后通过delay_ms函数延时1秒。4.3 光照传感器模块光照传感器(如BH1750)通过I2C总线与主控芯片相连,负责实时监测大棚内的光照强度。传感器模块将采集到的数据传输给主控芯片进行处理。实现代码如下:使用STM32标准库编写代码驱动BH1750读取环境光照强度。#include "stm32f10x.h"#include "stdio.h"#define BH1750_ADDR 0x23void I2C1_Init(void){ GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; // 使能I2C1和GPIOB时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // I2C1引脚配置 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出 GPIO_Init(GPIOB, &GPIO_InitStructure); // I2C1配置 I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 50%占空比 I2C_InitStructure.I2C_OwnAddress1 = 0x00; // 主机模式下无效 I2C_InitStructure.I2C_Ack = I2C_Ack_Disable; // 禁止应答 I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed = 100000; // 100kHz的速度 I2C_Init(I2C1, &I2C_InitStructure); // 使能I2C1 I2C_Cmd(I2C1, ENABLE);}void I2C1_Start(void){ // 发送起始信号 I2C_GenerateSTART(I2C1, ENABLE); // 等待起始信号发送完成 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) ;}void I2C1_Stop(void){ // 发送停止信号 I2C_GenerateSTOP(I2C1, ENABLE);}void I2C1_WriteByte(uint8_t byte){ // 发送一个字节的数据 I2C_SendData(I2C1, byte); // 等待发送完成 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) ;}uint16_t BH1750_ReadData(void){ uint16_t data = 0; // 设置BH1750模式(连续高分辨率测量模式) I2C1_Start(); I2C1_WriteByte(BH1750_ADDR); I2C1_WriteByte(0x10); I2C1_Stop(); // 延时等待测量结束 delay_ms(20); // 读取光照强度数据 I2C1_Start(); I2C1_WriteByte(BH1750_ADDR | 1); data = (I2C_ReceiveData(I2C1) << 8); I2C_AcknowledgeConfig(I2C1, DISABLE); I2C1_Stop(); delay_ms(2); I2C_AcknowledgeConfig(I2C1, ENABLE); I2C1_Start(); I2C1_WriteByte(BH1750_ADDR | 1); data |= I2C_ReceiveData(I2C1); I2C1_Stop(); return data;}void USART1_Init(void){ GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 使能USART1和GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // USART1引脚配置 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); // USART1配置 USART_InitStructure.USART_BaudRate = 115200; // 波特率为115200 USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); // 使能USART1 USART_Cmd(USART1, ENABLE);}void USART1_SendChar(char ch){ // 等待发送缓冲区为空 while (!(USART1->SR & USART_FLAG_TXE)) ; // 发送一个字符 USART_SendData(USART1, ch);}int fputc(int ch, FILE *f){ // 将数据通过串口发送 USART1_SendChar((char)ch); return ch;}void delay_ms(uint32_t ms){ while (ms--) { uint32_t count = 12000; while (count--) ; }}int main(void){ uint16_t lightIntensity; I2C1_Init(); USART1_Init(); printf("BH1750 Light Intensity Test\n"); while (1) { lightIntensity = BH1750_ReadData(); printf("Light Intensity: %d lux\n", lightIntensity); delay_ms(1000); // 每隔1秒读取一次光照强度 }}【1】上面代码里定义了BH1750的地址BH1750_ADDR为0x23,这是BH1750的默认地址。【2】在I2C1_Init()函数中,初始化了I2C1总线和相关的GPIO引脚。通过RCC_APB1PeriphClockCmd和RCC_APB2PeriphClockCmd函数使能I2C1和GPIOB时钟,然后配置GPIOB的引脚6和7为开漏输出模式(GPIO_Mode_AF_OD)。接着,使用I2C_Init函数初始化I2C1,并设置其工作模式为I2C_Mode_I2C、占空比为50%、禁止应答、7位地址模式以及100kHz的通信速率。最后,通过I2C_Cmd函数使能I2C1。【3】I2C1_Start()函数用于发送I2C总线的起始信号。调用I2C_GenerateSTART函数发送起始信号,并使用I2C_CheckEvent函数等待起始信号发送完成。【4】I2C1_Stop()函数用于发送I2C总线的停止信号。调用I2C_GenerateSTOP函数发送停止信号。【5】I2C1_WriteByte()函数用于向I2C设备发送一个字节的数据。通过I2C_SendData函数发送数据,并使用I2C_CheckEvent函数等待发送完成。【6】BH1750_ReadData()函数用于读取光照强度数据。,发送启动测量指令和模式设置指令(连续高分辨率测量模式)。然后等待测量结束的延时时间(20ms)。接着,通过两次读取数据寄存器的方式获取光照强度数据,并将其拼接为一个16位的无符号整数。【7】USART1_Init()函数用于初始化USART1串口和相关的GPIO引脚。使用RCC_APB2PeriphClockCmd函数使能USART1和GPIOA时钟,然后配置GPIOA的引脚9为复用推挽输出模式(GPIO_Mode_AF_PP)。接着,通过USART_Init函数初始化USART1,并设置其波特率为115200、数据位长度为8位、停止位为1位、无奇偶校验、无硬件流控制。最后,通过USART_Cmd函数使能USART1。【8】USART1_SendChar()函数用于发送一个字符到USART1串口。使用USART_SR寄存器的USART_FLAG_TXE标志位检查发送缓冲区是否为空,然后通过USART_SendData函数发送字符数据。【9】fputc()函数重定向了输出流,使得通过printf函数打印的字符可以发送到USART1串口。在该函数中调用USART1_SendChar函数发送字符数据,并返回该字符。【10】delay_ms()函数用于进行延时,单位为毫秒。该函数使用嵌套循环实现了简单的延时功能,不同的系统时钟频率可能需要适当调整。【11】在main()函数中,依次调用I2C1_Init()和USART1_Init()函数进行初始化操作。然后,通过printf函数向串口发送初始信息。【12】在主循环中,通过调用BH1750_ReadData()函数读取光照强度数据,并使用printf函数将其打印到串口。然后通过delay_ms函数进行1秒的延时,等待下一次读取。4.4 报警蜂鸣器模块报警蜂鸣器通过一个GPIO引脚与主控芯片相连。当报警条件满足时,主控芯片控制该引脚输出高电平信号,以激活蜂鸣器发出声音提示。使用STM32F103标准库编写的蜂鸣器控制代码:#include "stm32f10x.h"#define BEEP_GPIO_PORT GPIOA#define BEEP_GPIO_PIN GPIO_Pin_8void BEEP_Init(void){ GPIO_InitTypeDef GPIO_InitStructure; // 使能GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 配置蜂鸣器引脚为推挽输出模式 GPIO_InitStructure.GPIO_Pin = BEEP_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(BEEP_GPIO_PORT, &GPIO_InitStructure);}void BEEP_On(void){ GPIO_SetBits(BEEP_GPIO_PORT, BEEP_GPIO_PIN);}void BEEP_Off(void){ GPIO_ResetBits(BEEP_GPIO_PORT, BEEP_GPIO_PIN);}int main(void){ BEEP_Init(); while (1) { // 控制蜂鸣器开启和关闭 BEEP_On(); Delay_ms(500); BEEP_Off(); Delay_ms(500); }}在代码中,使用了GPIOA的第8个引脚作为蜂鸣器的控制引脚。BEEP_Init函数用于初始化蜂鸣器引脚,将其配置为推挽输出模式。BEEP_On和BEEP_Off函数分别用于开启和关闭蜂鸣器。在main函数中,通过循环控制蜂鸣器以500ms的间隔进行开启和关闭操作。4.5 HC05蓝牙模块模块HC05蓝牙模块通过串口通信与主控芯片相连,负责实现与Android手机APP之间的数据传输和通信。它接收主控芯片发送的数据,并通过蓝牙与手机APP进行交互。使用STM32标准库编写代码,用于通过串口2驱动HC05模块,并进行配置和数据通信:#include "stm32f10x.h"#include "stdio.h"// HC05配置指令#define AT_CMD_MODE "AT+CMODE=0\r\n" // 配置为从模式#define AT_CMD_PW "AT+PSWD=1234\r\n" // 配对密码设置为1234// 函数声明void USART2_Init(void);void USART2_SendChar(char ch);void USART2_SendString(char* str);char USART2_Receive(void);int main(void){ // 初始化USART2串口 USART2_Init(); // 发送AT指令配置HC05为从模式 USART2_SendString(AT_CMD_MODE); for(int i = 0; i < 1000000; i++); // 等待一段时间 // 发送AT指令配置配对密码 USART2_SendString(AT_CMD_PW); for(int i = 0; i < 1000000; i++); // 等待一段时间 while(1) { // 接收手机发送的数据 char data = USART2_Receive(); // 处理接收到的数据,例如发送回应等 // ... }}void USART2_Init(void){ // 使能USART2和GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // 配置USART2的引脚 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; // USART2_TX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // GPIO速度为50MHz GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; // USART2_RX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置USART2的参数 USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = 9600; // 波特率为9600 USART_InitStructure.USART_WordLength = USART_WordLength_8b; // 数据位长度为8位 USART_InitStructure.USART_StopBits = USART_StopBits_1; // 停止位为1位 USART_InitStructure.USART_Parity = USART_Parity_No; // 无奇偶校验 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控制 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 支持收发模式 USART_Init(USART2, &USART_InitStructure); // 使能USART2 USART_Cmd(USART2, ENABLE);}void USART2_SendChar(char ch){ while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); // 等待发送缓冲区为空 USART_SendData(USART2, (uint16_t)ch); // 发送数据}void USART2_SendString(char* str){ while(*str) { USART2_SendChar(*str++); }}char USART2_Receive(void){ while (USART_GetFlagStatus(USART2, USART_FLAG_RXNE) == RESET); // 等待接收缓冲区非空 return (char)USART_ReceiveData(USART2);}以上代码通过USART2串口与HC05模块进行通信,并发送AT指令对其进行配置。其中,AT_CMD_MODE用于将HC05配置为从模式,AT_CMD_PW用于设置配对密码为1234。代码设计思路介绍:【1】引入头文件:#include "stm32f10x.h"#include "stdio.h"引入了STM32F10x系列微控制器的相关头文件和标准输入输出库的头文件。【2】定义宏和函数:#define AT_CMD_MODE "AT+CMODE=0\r\n"#define AT_CMD_PW "AT+PSWD=1234\r\n"void USART2_Init(void);void USART2_SendChar(char ch);void USART2_SendString(char* str);char USART2_Receive(void);定义了用于配置HC05模块的AT指令,以及用于初始化USART2串口、发送字符和字符串、接收字符的函数。【3】USART2串口初始化函数:void USART2_Init(void)该函数用于初始化USART2串口和相关引脚。使能了USART2和GPIOA的时钟,然后配置了USART2的引脚,包括引脚的模式和速度等。接着对USART2进行配置,设置波特率、数据位长度、停止位、奇偶校验位等参数,并最后使能USART2串口。【4】发送字符和字符串函数:void USART2_SendChar(char ch)void USART2_SendString(char* str)这些函数用于通过USART2串口发送字符和字符串。对于USART2_SendChar函数,它会等待发送缓冲区为空,然后将字符写入数据寄存器进行发送。对于USART2_SendString函数,它会遍历字符串中的每个字符,并调用USART2_SendChar函数进行发送。【5】接收字符函数:char USART2_Receive(void)该函数用于从USART2串口接收一个字符。它会等待接收缓冲区非空,然后读取数据寄存器的值并返回接收到的字符。此函数在主循环中可以用于接收HC05模块发送的数据。【6】主函数:int main(void)在主函数中,调用USART2_Init函数初始化USART2串口。然后使用USART2_SendString函数依次发送配置指令AT_CMD_MODE和AT_CMD_PW给HC05模块。发送完指令后,通过循环调用USART2_Receive函数接收HC05模块发送的数据。五、调试过程在项目设计完成后,进行测试和调试是非常重要的,以确保系统的正常运行和功能的有效性。下面是本项目的测试和调试过程流程:5.1 硬件测试检查硬件组装和连接是否正确,包括主控芯片STM32F103RCT6、传感器模块SHT30和BH1750以及蓝牙模块HC05的连接。使用示波器或多用途测试仪检测各个模块的电源供应和信号线连接是否正常。测试温湿度传感器(SHT30)和光照强度传感器(BH1750)是否能够正确采集环境数据。测试蜂鸣器是否能够发出合适的声音提示。5.2 固件程序测试在STM32开发环境中编译程序,将固件程序烧录到主控芯片STM32F103RCT6上。使用串口调试助手等工具,与STM32建立通信连接,检查数据的传输和接收是否正常。对温湿度传感器和光照强度传感器进行数据采集测试,观察是否能够准确读取传感器数据。设置阈值并测试报警功能,确保报警触发条件和报警提示的准确性。5.3 Android手机APP测试安装开发好的Android手机APP到测试设备上,确保安装过程顺利。打开APP,并与蓝牙模块HC05进行连接,观察是否能够成功建立通信。测试数据的传输和接收功能,确保从STM32接收到的数据能够在APP界面上正确显示。设置阈值并触发报警测试,确认报警提示(声音、震动、弹窗等)是否按照设定的条件正常工作。5.4 系统整体测试模拟实际环境,调整大棚温湿度和光照强度,观察系统是否能够准确检测并显示环境参数。调整报警阈值,触发报警条件,验证报警提示是否按照预期工作。进行长时间运行测试,确保系统的稳定性和可靠性。六、关键问题讨论6.1 本项目的核心与技术难点【1】硬件集成:将主控芯片、传感器模块、蓝牙模块和蜂鸣器等多个硬件模块进行正确的连接和集成是一个挑战。需要仔细设计电路连接、通信协议和接口定义,确保各个模块能够正常协同工作。【2】数据处理与算法:在主控芯片的固件程序中,需要对传感器采集到的数据进行处理和分析,判断是否触发报警条件。这可能涉及到数据滤波、阈值判定、异常检测等算法的设计和实现。【3】蓝牙通信:与手机APP之间的蓝牙通信是一个难点。需要实现稳定可靠的数据传输和通信协议,确保数据的准确性和实时性。6.2 本项目的实用性【1】实时监测和报警功能:该项目能够实时监测大棚内的温度、湿度和光照强度,并在超过设定阀值时触发报警。这对于农业生产者来说非常实用,能够帮助他们及时发现问题并采取措施,以保证大棚内环境的稳定和作物的健康生长。【2】远程监控和管理:通过与手机APP的蓝牙通信,用户可以远程监控大棚内的数据并进行管理。他们可以随时查看温湿度和光照强度的实时数据,设置报警阀值,接收报警通知,并对大棚环境进行远程调整和控制。【3】自动化和智能化:该项目利用传感器和自动化控制技术,实现了对大棚环境的智能监测和控制。这不仅提高了农业生产的效率和质量,还减轻了人工管理的负担,具有较高的实用性和应用价值。6.3 关键点总结【1】传感器选择和集成:选择适合的温湿度传感器和光照强度传感器,并合理集成到硬件设计中。需要考虑传感器的精度、响应速度以及与主控芯片的通信协议等因素。【2】数据采集和处理算法:设计合适的数据采集和处理算法,确保从传感器获取的数据准确可靠,并能够根据设定的阈值判断是否触发报警条件。【3】报警机制:设计报警机制,根据设定的阈值和实时采集的数据进行比较,当达到报警条件时,触发报警提示,如声音、震动或弹窗等方式。【4】蓝牙通信:确保蓝牙模块能够与主控芯片稳定通信,并能够成功传输采集的数据到Android手机APP上。需要考虑通信的稳定性、数据传输的速率和安全性等因素。【5】Android手机APP设计:设计用户友好的界面,在手机APP上实时显示大棚内的温湿度和光照强度数据,并提供设置界面,允许农民设置报警阈值。同时,实现报警提示的功能,保证报警条件的准确性和报警方式的有效性。【6】系统稳定性和可靠性:在测试和调试阶段,需要对整个系统进行全面的测试,包括硬件和软件的各个模块,以确保系统的稳定性和可靠性。同时,考虑到长时间运行的情况,还需进行长时间测试,以验证系统能够持续运行并正常工作。七、设计总结与体会本项目的目的是设计实现对农业大棚环境的智能监测和管理,通过传感器采集数据、主控芯片处理和判断、蓝牙通信与手机APP交互,以及报警蜂鸣器的控制,实现了对温度、湿度和光照强度等参数的实时监测和报警功能。在项目开发过程中,也遇到了一些挑战和难点,比如:硬件的连接和集成、数据处理算法的设计和实现,以及与手机APP之间的蓝牙通信。通过细致的计划和分工,最终也是成功地克服了这些困难,得以完成了整个系统的开发。在实施过程中,深刻认识到模块化设计的重要性。通过将系统划分为多个硬件模块和软件模块,能够更好地管理和调试每个模块,并且在需要时进行模块的替换和升级,提高了系统的可扩展性和可维护性。在测试阶段,也意识到用户体验的重要性。通过与潜在用户的沟通和反馈,不断优化和改进系统的界面设计和功能实现,力求使用户能够轻松使用和管理该系统。这种用户导向的设计理念提高了系统的实用性和用户满意度。总的来说,本项目的设计与实施过程对软硬件的协同工作、数据处理与算法设计、用户体验等方面有了更深入的理解。通过克服挑战和不断优化,成功地将智能农业大棚监测系统带入实际应用,并为农业生产者提供了一种方便、高效且可靠的解决方案。相信随着技术的不断发展,智慧农业将在未来发挥更大的作用,为农业生产带来更多的创新和改进。
  • [问题求助] 是否有交叉编译的支持
    主要是指嵌入式系统的开发,这个是否有计划支持呢?