• [专题汇总] 2024年3月嵌入式项目开发专题总汇【合集】
    一、前言物联网(IoT)和嵌入式系统技术正在以惊人的速度改变着我们的日常生活和工作方式。随着传感器技术的不断进步和智能算法的不断发展,我们见证着一个全新的数字化时代的崭露头角。在这个时代,传感器不再只是简单地收集数据,而是成为了连接物理世界和数字世界的桥梁,为我们提供了丰富的信息和智能的决策支持。本系列文章深入探讨了传感器技术在嵌入式系统中的应用,涵盖了从光敏传感器到温度传感器、姿态传感器等多种类型的传感器。通过这些文章,我们可以了解到如何利用单片机和嵌入式系统读取和处理传感器数据,从而实现各种智能化的应用,如环境监测、智能安全防护、车载导航、自动化采摘等。还介绍了物联网中常用的传感器和通信协议,探讨了物联网技术的发展趋势和未来展望。通过这些内容,可以更好地了解物联网技术的核心组成部分,并为未来的物联网应用开发提供参考和指导。本系列文章为读者提供关于传感器技术和物联网应用的全面了解,帮助读者掌握相关的基础知识和技能,从而在物联网领域有所建树。二、技术文章合集【1】Qt(C++)计算一段程序执行经过的时间cid:link_5在许多应用程序和系统中,需要对经过的时间进行计算和记录。例如 可能想要测量某个操作的执行时间,或者记录一个过程中经过的时间以进行性能分析。在这些场景下,准确地计时是非常重要的。Qt提供了一个功能强大的计时器类QElapsedTimer,可以方便地记录经过的时间。QElapsedTimer使用高精度的计时器来测量时间片段之间的经过时间。为了更直观地呈现经过的时间,可以使用QTime类来处理时间,并通过其toString()函数将时间格式化为时分秒毫秒的形式。这样,可以获得更具可读性的时间输出。【2】基于STM32的儿童智能安全防护书包设计cid:link_6随着社会的进步和科技的发展,儿童安全问题日益引起广泛关注。在日常生活中,尤其是在上学放学途中、户外活动时,儿童走失事件时有发生,给家庭和社会带来了极大的困扰和担忧。随着学业负担的增加,学生时常会因为忘记携带所需书籍而影响学习。如何利用现代技术手段提高儿童安全保障水平,并辅助他们培养良好的学习习惯,成为了一个待解决的社会需求。基于此背景,当前设计并实现一款基于STM32F103RCT6微控制器为核心的儿童智能安全防护书包,显得尤为必要与实际。这款书包集成了先进的定位技术和无线通信模块,能够实时追踪并发送儿童的位置信息给家长,确保在紧急情况下快速响应 (发送短信的时候,直接通过GPS经纬度拼接百度地图的HTTP请求链接,家长收到短信可以直接打开链接,在网页查看百度地图上显示的具体位置,可以直接通过百度地图导航过去)。同时,具备智能化功能,如课程表录入存储与提醒系统,利用EEPROM(例如AT24C02)进行数据持久化存储,并通过RFID-RC522射频识别模块自动检测所携带书籍是否齐全,避免孩子因疏忽遗漏课本而耽误学习。智能书包还配备了直观易读的1.44寸LCD显示屏,用于显示当前位置信息、当日课表以及未带书籍的提醒。当检测到缺少某本书籍时,蜂鸣器模块会发出声音警报,从而强化提醒效果,帮助学生养成有序整理个人物品的习惯。这款基于STM32的儿童智能安全防护书包是一个集成物联网技术、GPS定位、无线通信和智能感知于一体的创新产品,提升儿童的安全防护等级,加强家校互动,促进学生自我管理能力的培养,充分体现了科技服务于生活、服务于教育的理念。【3】基于视觉识别的自动采摘机器人设计与实现cid:link_7随着科技的进步和农业现代化的发展,农业生产效率与质量的提升成为重要的研究对象。其中,果蔬采摘环节在很大程度上影响着整个产业链的效益。传统的手工采摘方式不仅劳动强度大、效率低下,而且在劳动力成本逐渐上升的背景下,越来越难以满足大规模种植基地的需求。人工采摘还可能因不规范的操作导致果实损伤,影响商品果率。基于视觉识别技术的自动采摘机器人的研发,正是针对这一问题提出的创新解决方案。本项目采用树莓派4B作为主控芯片,因其具有强大的计算能力和丰富的扩展接口,可以方便地集成各种传感器和执行机构,实现对复杂环境下的实时图像采集与处理。项目利用百度飞浆(PaddlePaddle)深度学习框架中的目标检测和分类算法,通过安装在机器人上的高清摄像头获取果树图像,并进行实时分析,精准识别出果实的位置、大小以及成熟度等信息。当成功识别到目标果实后,主控系统将根据识别结果快速计算出机械手臂的最佳运动路径,控制其移动至指定位置,以最适宜的方式完成果实的高效、无损采摘。基于视觉识别的自动采摘机器人设计与实现项目旨在解决传统农业中人工采摘的瓶颈问题,通过人工智能与自动化技术的深度融合,提高果园管理的智能化水平,降低劳动成本,提高生产效率,从而推动我国乃至全球农业产业向更加智能、高效的现代农业转型。【4】基于嵌入式的车载导航定位系统设计cid:link_8随着汽车工业的飞速发展和智能化技术的不断突破,车载导航系统作为现代汽车不可或缺的一部分,在人们的日常生活中扮演着越来越重要的角色。它不仅能够提供精确的路线导航,还能提供丰富的地理信息和娱乐服务,为驾驶者带来了极大的便利和乐趣。传统的车载导航系统主要依赖于内置的地图数据和GPS定位技术,但随着移动互联网的普及和智能设备的快速发展,用户对车载导航系统的要求也在不断提高。希望车载导航系统能够具备更高的定位精度、更丰富的地图信息、更便捷的操作体验以及更强的可扩展性。开发一款基于嵌入式技术的车载导航定位系统,以满足现代用户对高效、智能、个性化导航服务的需求,成为了当前行业发展的一个重要方向。本项目就是通过集成高性能的主控开发板、精准的GPS定位模块以及强大的Qt开发框架,实现一个功能丰富、性能稳定、用户体验优越的车载导航系统。【5】绿色再生·安卓4G智能远程操作巡视机器人小车cid:link_0随着物联网技术与移动通信技术的快速发展,远程遥控设备在日常生活及工业应用中的普及度日益提高。无论是家用扫地机器人实现自主导航清扫,还是目前抖音平台上展示的实景互动小车等创新应用,都体现了远程控制和实时视频监控技术对现代智能设备的重要性。本项目设计并实现一款基于STM32微控制器的远程遥控安卓小车系统。该系统充分利用了淘汰下来的安卓旧手机作为车载信息处理单元,不仅实现了资源的有效再利用,还结合4G网络技术以及先进的流媒体服务和物联网技术,搭建起一套集远程操控、实时视频音频传输于一体的高效解决方案。项目的小车搭载了STM32主控板以精确控制四个电机的动作,通过L298N驱动芯片确保了底座移动的稳定性和灵活性。同时,小车的动力源采用两节18650锂电池提供充足的电力支持。车载的旧安卓手机通过USB线连接到STM32主控板上,接收并执行来自远端手机APP的指令。这款由Qt开发的Android APP能够利用4G网络实现实时在线,并通过摄像头采集音视频数据,通过RTMP协议将这些数据推送到华为云ECS服务器上的NGINX流媒体服务器,从而实现高清流畅的远程视频监控。为了实现双向交互和低延迟控制,整个系统还借助MQTT协议连接至华为云IOT服务器。另一台安装了同样由Qt开发的Android手机APP的终端设备,可以通过该APP拉取小车端的实时音视频流进行播放,并通过方向键菜单实现对小车的精准远程操控。这种设计不仅极大地拓展了传统遥控小车的功能性与实用性,还为其他类似应用场景提供了可借鉴的技术框架。【6】Linux系统编程-(pthread)线程创建与使用cid:link_9前面文章介绍了Linux下进程的创建、管理、使用、通信,了解了多进程并发;这篇文章介绍Linux下线程的基本使用。线程与进程的区别 (1)进程: 是操作系统调度最小单位。 Linux下可以通过ps、top等命令查看进程的详细信息。 (2)线程: 是进程调度的最小单位,每个进程都有一个主线程。在进程里主要做事情就是线程。(3)在全系统中,进程ID是唯一标识,对于进程的管理都是通过PID来实现的。每创建一个进程,内核去中就会创建一个结构体来存储该进程的全部信息,每一个存储进程信息的节点也都保存着自己的PID。需要管理该进程时就通过这个ID来实现(比如发送信号)。当子进程结束要回收时(子进程调用exit()退出或代码执行完),需要通过wait()系统调用来进行,未回收的消亡进程会成为僵尸进程,其进程实体已经不复存在,但会虚占PID资源,因此回收是有必要的。对于线程而言,若要主动终止需要调用pthread_exit() ,主线程需要调用pthread_join()来回收(前提是该线程没有设置 “分离属性”)。像线发送线程信号也是通过线程ID实现进程间的通信方式: A.共享内存 B.消息队列 C.信号量 D.有名管道 E.无名管道 F.信号 G.文件 H.socket 线程间的通信方式: A.互斥量 B.自旋锁 C.条件变量 D.读写锁 E.线程信号 F.全局变量进程间采用的通信方式要么需要切换内核上下文,要么要与外设访问(有名管道,文件)。所以速度会比较慢。而线程采用自己特有的通信方式的话,基本都在自己的进程空间内完成,不存在切换,所以通信速度会较快。也就是说,进程间与线程间分别采用的通信方式,除了种类的区别外,还有速度上的区别。说明: 当运行多线程的进程捕获到信号时,只会阻塞主线程,其他子线程不会影响会继续执行。【7】Linux系统编程-(pthread)线程的使用案例(分离属性、清理函数等)cid:link_1使用pthread库的简单示例,演示了如何创建和管理线程,包括设置线程的属性、使用分离属性、清理函数等。【8】传感器技术: STM32读取BH1750光敏传感器数据cid:link_10使用IIC模拟时序驱动,方便移植到其他平台,采集的光照度比较灵敏. 合成的光照度返回值范围是 0~255。 0表示全黑 255表示很亮。【9】物联网中常用的传感器cid:link_11物联网中常用的传感器主要包括以下几种类型:温度传感器:用于测量环境、物体或设备的温度,如热电偶、热敏电阻和红外温度传感器,广泛应用于工业自动化、智能家居、冷链物流、医疗健康等多个领域。光学传感器:用于检测光强度、颜色、方向等光学属性,包括紫外光、可见光、红外光传感器,常见于光照控制、安防系统、火灾报警系统等。图像传感器:将光学图像转化为数字信号,如CMOS或CCD图像传感器,常用于智能摄像头、无人驾驶汽车、无人机等视觉系统中。距离传感器:通过超声波、激光、红外等方式测量物体间的距离,用于自动门、无人机避障、停车辅助系统等。振动传感器:检测机械振动,可用于设备状态监测、地震预警、结构健康监测系统中。声音传感器(麦克风):捕捉声音信号,应用于语音识别系统、噪声监测、安全警报系统。烟雾传感器:检测空气中的烟雾粒子,常用于火警报警系统。人体红外传感器:探测人体发出的红外辐射,应用于智能照明、安防监控、智能家居等人体存在感应场景。湿度传感器:测量空气中水分含量,对环境调控、农业生产、仓储管理等方面至关重要。压力传感器:测量气体或液体的压力,广泛应用于气象站、水位监测、车辆胎压监测系统等。运动传感器(加速度计、陀螺仪):感知物体的移动、倾斜和旋转,常见于智能手机、游戏控制器、运动手环等设备。cid:link_11【10】物联网里常用的协议介绍cid:link_12物联网中常用的协议包括多种,每种协议都有其特定的应用场景和优势。以下是一些主要的物联网协议及其详细介绍:MQTT协议:MQTT(Message Queuing Telemetry Transport)是一种轻量级的发布/订阅模式的消息传输协议,专为低带宽、高延迟或不稳定的网络环境设计。 它工作在TCP/IP协议族上,以极少的代码和带宽为远程设备提供实时可靠的消息服务。 MQTT协议广泛应用于物联网、移动应用、小型设备、智能家电、传感器等领域。 CoAP协议: CoAP(Constrained Application Protocol)是一种针对受限设备的应用层协议,特别适用于低功耗、低带宽的物联网环境。 它类似于Web的HTTP协议,但为物联网设备做了优化,支持请求/响应模型,并提供了可靠传输、数据重传和块传输等功能。 CoAP适用于资源受限的物联网设备,如传感器、智能电表等。 LWM2M协议: LWM2M(Lightweight Machine to Machine)是一种物联网设备管理协议,用于远程管理和监控物联网设备。 它定义了设备管理的基本功能和接口,使得物联网设备可以被远程配置、监控和更新。 ZigBee协议: ZigBee是一种低功耗、近距离的无线通信协议,适用于家庭自动化和工业控制领域。 它采用RF频段进行通信,能够实现设备之间的互联和远程控制,具有低复杂度、自组织、高可靠性和低成本等特点。 Bluetooth协议: Bluetooth是一种短距离无线通信协议,广泛应用于手机、电脑、耳机等个人消费电子设备之间的通信。 蓝牙技术提供了设备间的无线连接和数据传输功能,使得设备可以方便地进行数据交换和协同工作。 物联网中还有其他一些常用的协议,如LoRaWAN(用于低功耗广域网通信)、Wi-Fi(用于无线局域网通信)、RFID(无线射频识别)等。【11】传感器技术:STC89C52单片机读取MPU6050姿态数据cid:link_2MPU6050是一个6轴姿态传感器,能够测量芯片自身的X、Y、Z轴的加速度以及角速度参数。通过数据融合,它可以进一步得到姿态角。这种传感器常应用于平衡车、飞行器等需要检测自身姿态的场景。MPU6050由两部分组成:3轴加速度计和3轴陀螺仪传感器。3轴加速度计用于测量X、Y、Z轴的加速度,而3轴陀螺仪传感器则用于测量X、Y、Z轴的角速度。这使得MPU6050能够全面感知设备的运动状态。在MPU6050中,陀螺仪的工作原理基于一个物理原理:一个旋转物体的旋转轴所指的方向在不受外力影响时是不会改变的。这个原理使得陀螺仪能够保持方向,进而读取轴所指示的方向,并自动将数据信号传给控制系统。而加速度传感器则是一种能够测量加速度的传感器,通常由质量块、阻尼器、弹性元件、敏感元件和适调电路等部分组成。在加速过程中,通过对质量块所受惯性力的测量,利用牛顿第二定律,传感器能够获得加速度值。此外,MPU6050还具有一些重要参数。它采用16位ADC采集传感器的模拟信号,其量化范围为-32768到32767。加速度计和陀螺仪都有多种满量程选择,可以根据实际应用场景进行调整。MPU6050还具有可配置的数字低通滤波器、时钟源和采样分频等功能,这些功能使得MPU6050能够适应不同的应用需求。由于其高性能、小型化、低功耗等特点,MPU6050被广泛应用于各种需要运动跟踪和姿态解算的智能设备中,如无人机、机器人、智能手机、平板电脑和可穿戴设备等。在无人机中,MPU6050可以提供角速度和加速度数据,帮助无人机实现精确的运动控制;在机器人中,MPU6050可以实现姿态感知和运动控制。【12】 传感器技术:单片机读取DHT11温度传感器数据cid:link_13DHT11是一款集成了已校准数字信号输出的温湿度传感器。它主要基于湿度敏感元件和温度敏感元件的电阻值变化来工作。具体来说,湿度敏感元件是一种聚合物材料,当湿度增加时,它会吸收水分并膨胀,导致电阻值增加;而温度敏感元件是一种热敏电阻,当温度升高时,它的电阻值会减小。DHT11通过内部的微控制器读取这两种敏感元件的电阻值,并将其转换为数字信号输出。DHT11的主要参数包括:湿度分辨率为8bit,测量精度在25℃时为±4%RH,最大的测量范围为2090%RH,响应时间为616秒;温度分辨率为8bit,测量精度为±1℃,测量范围为050℃,响应时间为630秒。此外,DHT11传感器的工作电压范围为35V,工作电流为0.5~2.5mA,采样周期为1秒,即每次刷新温湿度数据的时间至少为1秒。DHT11的应用非常广泛。在家庭中,它可以集成在智能家居系统中,用于监测室内温度,根据温度变化自动调节空调或供暖系统,实现节能和舒适的生活环境。在工业生产中,DHT11传感器可以安装在设备或机器上,实时监测温度变化,帮助预测和防止设备过热或过冷引起的故障,提高生产效率和产品质量。此外,DHT11传感器还可应用于气象、环境监测、农业等领域,如气象监测、室内空气质量检测、土壤湿度监测等。DHT11温度传感器是一款功能强大、应用广泛的传感器,适用于多种场景下的温湿度测量。如需更多信息,建议查阅DHT11传感器的技术手册或咨询传感器领域的专家。【13】传感器技术:单片机读取DS18B20温度数据cid:link_3DS18B20传感器是由DALLAS半导体公司推出的一种数字化温度传感器,它采用“一线总线(单总线)”接口,具有体积小、适用电压宽、与微处理器接口简单等优点。DS18B20传感器的主要特点和应用如下:特点:单线传输:使用单线传输协议(1-Wire)进行通信,只需要一个数据线就可以实现数据传输和电源供应,降低了硬件开销和布线的复杂性。 高精度:可以测量范围为-55°C至+125°C的温度,精度为±0.5°C(在-10°C至+85°C范围内)。此外,其可编程的分辨率为9~12位,对应的可分辨温度分别为0.5℃、0.25℃、0.125℃和0.0625℃,使得它能够实现高精度测温。 数字输出:DS18B20输出的是数字温度信号,可以直接与数字系统集成,无需进行模拟信号转换,简化了数据处理流程。 多点组网功能:通过1-Wire总线,可以连接多个DS18B20传感器,实现多点温度测量,适用于需要监测多个温度点的应用场景。 低功耗:DS18B20具有低功耗特性,工作时只需要极低的能量,可以通过1-Wire总线实现供电。 内部存储:DS18B20具有内部存储器,可以存储自身序列号和温度校准系数等信息,方便管理和配置。 强抗干扰能力:测量结果直接输出数字温度信号,以“一根总线”串行传送给CPU,同时可传送CRC校验码,具有极强的抗干扰纠错能力。 适应性广:DS18B20适应电压范围宽,封装形式多样,可以应用于多种场合,如管道式、螺纹式、磁铁吸附式、不锈钢封装式等。 应用: 多种场合测温:DS18B20传感器可用于多种非极限温度场合的测温,如电缆沟测温、高炉水循环测温、锅炉测温、机房测温、农业大棚测温、洁净室测温以及弹药库测温等。 工业控制领域:由于其耐磨耐碰、体积小、使用方便的特点,DS18B20传感器也适用于各种狭小空间设备数字测温和控制领域。 DS18B20传感器凭借其高精度、数字输出、单线传输等优点,在多种温度测量和控制应用中发挥着重要作用。如需更多关于DS18B20传感器的信息,建议查阅相关技术手册或咨询传感器领域的专家。【14】传感器技术:单片机读取MMA7660传感器姿态数据cid:link_4MMA7660是一款具有数字输出的I²C、低功耗、紧凑型电容式微机械加速度传感器。它提供了低通滤波器、零重力加速度偏移和增益误差补偿,并可以将数据转化为6位数字值。用户还可以配置输出数据的传输速率。该传感器包含一个微小的质量块和一组微小的弹簧。当物体受到加速度时,质量块会受到力的作用而发生位移。这个位移会被转换成电信号,通过电路进行放大和处理,最终输出一个与加速度大小成正比的电压信号。这种原理使得MMA7660能够精确地感知和测量加速度。此外,MMA7660还具有中断引脚(INT),可以识别传感器的数据变化、产品的朝向和姿态等信息。通过中断机制,当物体发生倾斜、晃动或改变方向时,MMA7660能够产生中断信号,并给出当前物体的姿态(如水平/垂直,上下,左右等)。在硬件设计方面,MMA7660采用了非常小的3mm x 3mm x 0.9mm DFN封装,便于集成到各种小型设备中。此外,还可以通过在9号管脚(DVDD)和8号管脚(DVSS)之间放置0.1μF陶瓷电容器来优化其性能。从应用角度来看,MMA7660因其低功耗、小型化和精确测量的特点,被广泛应用于多种场景。例如,它可以用于手机、掌上电脑、车载导航等设备的防盗功能;自动自行车刹车灯可以根据加速度的变化自动调整刹车灯的亮度;运动检测手环可以利用MMA7660来监测用户的运动状态和步数;数码机和自动叫醒闹钟等也可以利用它来实现更智能的功能。MMA7660是一款功能强大、应用广泛的加速度传感器,能够提供稳定和精确的加速度测量结果,满足不同设备对运动感知和姿态识别的需求。如需更多信息,建议查阅传感器技术手册或咨询相关领域的专家。
  • [技术干货] 传感器技术:单片机读取DHT11温度传感器数据
    DHT11是一款集成了已校准数字信号输出的温湿度传感器。它主要基于湿度敏感元件和温度敏感元件的电阻值变化来工作。具体来说,湿度敏感元件是一种聚合物材料,当湿度增加时,它会吸收水分并膨胀,导致电阻值增加;而温度敏感元件是一种热敏电阻,当温度升高时,它的电阻值会减小。DHT11通过内部的微控制器读取这两种敏感元件的电阻值,并将其转换为数字信号输出。DHT11的主要参数包括:湿度分辨率为8bit,测量精度在25℃时为±4%RH,最大的测量范围为2090%RH,响应时间为616秒;温度分辨率为8bit,测量精度为±1℃,测量范围为050℃,响应时间为630秒。此外,DHT11传感器的工作电压范围为35V,工作电流为0.5~2.5mA,采样周期为1秒,即每次刷新温湿度数据的时间至少为1秒。DHT11的应用非常广泛。在家庭中,它可以集成在智能家居系统中,用于监测室内温度,根据温度变化自动调节空调或供暖系统,实现节能和舒适的生活环境。在工业生产中,DHT11传感器可以安装在设备或机器上,实时监测温度变化,帮助预测和防止设备过热或过冷引起的故障,提高生产效率和产品质量。此外,DHT11传感器还可应用于气象、环境监测、农业等领域,如气象监测、室内空气质量检测、土壤湿度监测等。DHT11温度传感器是一款功能强大、应用广泛的传感器,适用于多种场景下的温湿度测量。如需更多信息,建议查阅DHT11传感器的技术手册或咨询传感器领域的专家。以下是使用STC89C52单片机读取DHT11温湿度传感器数据的完整代码:#include <reg52.h>#define DHT11_DATA P2_0sbit LED = P1^0;unsigned char T_data_H, T_data_L, RH_data_H, RH_data_L, check_sum;void Delay10us() { unsigned char a; for (a = 2; a > 0; a--);}void StartSignal() { DHT11_DATA = 0; // 拉低总线 Delay10us(); DHT11_DATA = 1; // 拉高总线 Delay10us();}bit CheckResponse() { unsigned char timeout = 0; while (!DHT11_DATA && timeout < 200) { // 等待DHT11拉低总线响应 timeout++; } if (timeout >= 200) { return 0; // 响应超时 } else { timeout = 0; while (DHT11_DATA && timeout < 200) { // 等待DHT11拉高总线响应 timeout++; } if (timeout >= 200) { return 0; // 响应超时 } } return 1; // 响应正常}unsigned char ReadByte() { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { while (!DHT11_DATA); // 等待DHT11拉高总线 Delay10us(); dat <<= 1; if (DHT11_DATA) { dat |= 1; // 如果DHT11拉高总线,则接收到了1 while (DHT11_DATA); // 等待DHT11拉低总线 } } return dat;}void main() { EA = 1; // 开启总中断 while (1) { StartSignal(); // 发送起始信号 if (!CheckResponse()) { // 检查响应 continue; // 如果响应异常,则重新发送起始信号 } RH_data_H = ReadByte(); // 读取湿度高八位 RH_data_L = ReadByte(); // 读取湿度低八位 T_data_H = ReadByte(); // 读取温度高八位 T_data_L = ReadByte(); // 读取温度低八位 check_sum = ReadByte(); // 读取校验和 if (check_sum == RH_data_H + RH_data_L + T_data_H + T_data_L) { // 校验和匹配 LED = 1; // 校验通过,点亮LED } else { LED = 0; // 校验不通过,熄灭LED } }}
  • [技术干货] 传感器技术:单片机读取DS18B20温度数据
    DS18B20传感器是由DALLAS半导体公司推出的一种数字化温度传感器,它采用“一线总线(单总线)”接口,具有体积小、适用电压宽、与微处理器接口简单等优点。DS18B20传感器的主要特点和应用如下:特点:单线传输:使用单线传输协议(1-Wire)进行通信,只需要一个数据线就可以实现数据传输和电源供应,降低了硬件开销和布线的复杂性。高精度:可以测量范围为-55°C至+125°C的温度,精度为±0.5°C(在-10°C至+85°C范围内)。此外,其可编程的分辨率为9~12位,对应的可分辨温度分别为0.5℃、0.25℃、0.125℃和0.0625℃,使得它能够实现高精度测温。数字输出:DS18B20输出的是数字温度信号,可以直接与数字系统集成,无需进行模拟信号转换,简化了数据处理流程。多点组网功能:通过1-Wire总线,可以连接多个DS18B20传感器,实现多点温度测量,适用于需要监测多个温度点的应用场景。低功耗:DS18B20具有低功耗特性,工作时只需要极低的能量,可以通过1-Wire总线实现供电。内部存储:DS18B20具有内部存储器,可以存储自身序列号和温度校准系数等信息,方便管理和配置。强抗干扰能力:测量结果直接输出数字温度信号,以“一根总线”串行传送给CPU,同时可传送CRC校验码,具有极强的抗干扰纠错能力。适应性广:DS18B20适应电压范围宽,封装形式多样,可以应用于多种场合,如管道式、螺纹式、磁铁吸附式、不锈钢封装式等。应用:多种场合测温:DS18B20传感器可用于多种非极限温度场合的测温,如电缆沟测温、高炉水循环测温、锅炉测温、机房测温、农业大棚测温、洁净室测温以及弹药库测温等。工业控制领域:由于其耐磨耐碰、体积小、使用方便的特点,DS18B20传感器也适用于各种狭小空间设备数字测温和控制领域。DS18B20传感器凭借其高精度、数字输出、单线传输等优点,在多种温度测量和控制应用中发挥着重要作用。如需更多关于DS18B20传感器的信息,建议查阅相关技术手册或咨询传感器领域的专家。以下是使用STC89C52单片机读取DS18B20温度传感器数据的完整代码:#include <reg52.h>#define DQ P2_0unsigned char ReadOneChar();void WriteOneChar(unsigned char dat);void Delay1ms(unsigned int i);void Delay500us();bit Init_DS18B20();void Write_DS18B20(unsigned char dat);unsigned char Read_DS18B20();sbit LED = P1^0;void main() { unsigned char temp_low, temp_high; int temp; while (1) { if (Init_DS18B20()) { Write_DS18B20(0xcc); // 跳过ROM操作命令 Write_DS18B20(0x44); // 启动温度转换命令 Delay500us(); Delay500us(); Init_DS18B20(); Write_DS18B20(0xcc); // 跳过ROM操作命令 Write_DS18B20(0xbe); // 读取温度命令 temp_low = Read_DS18B20(); // 读取温度低字节 temp_high = Read_DS18B20(); // 读取温度高字节 temp = (temp_high << 8) + temp_low; if (temp & 0x8000) { // 判断温度是否为负数 temp = temp & 0x7fff; // 清除最高位 LED = 1; // 负数温度时点亮LED } else { LED = 0; // 正数温度时熄灭LED } // 将温度数据处理成实际温度值,具体处理方法根据DS18B20的工作原理和数据手册中的说明进行计算 // 在这里可以将temp转换为实际温度值,然后进行相应的处理 // 注意:这里仅仅读取了DS18B20的温度数据,如果需要更精确的温度信息,可以考虑加入更复杂的温度计算方法或者校准算法 // 延时等待,可以根据需要调整 Delay1ms(500); } }}bit Init_DS18B20() { bit ack; DQ = 1; Delay500us(); DQ = 0; Delay500us(); DQ = 1; Delay500us(); ack = DQ; Delay500us(); DQ = 1; return ack;}void Write_DS18B20(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DQ = 0; _nop_(); DQ = dat & 0x01; Delay500us(); DQ = 1; dat >>= 1; }}unsigned char Read_DS18B20() { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { DQ = 0; _nop_(); DQ = 1; _nop_(); dat >>= 1; if (DQ) { dat |= 0x80; } Delay500us(); } return dat;}void Delay1ms(unsigned int i) { unsigned int j; while (i--) { for (j = 0; j < 123; j++); }}void Delay500us() { _nop_(); _nop_(); _nop_(); _nop_(); _nop_();}
  • [技术干货] 传感器技术:单片机读取MMA7660传感器姿态数据
    MMA7660是一款具有数字输出的I²C、低功耗、紧凑型电容式微机械加速度传感器。它提供了低通滤波器、零重力加速度偏移和增益误差补偿,并可以将数据转化为6位数字值。用户还可以配置输出数据的传输速率。该传感器包含一个微小的质量块和一组微小的弹簧。当物体受到加速度时,质量块会受到力的作用而发生位移。这个位移会被转换成电信号,通过电路进行放大和处理,最终输出一个与加速度大小成正比的电压信号。这种原理使得MMA7660能够精确地感知和测量加速度。此外,MMA7660还具有中断引脚(INT),可以识别传感器的数据变化、产品的朝向和姿态等信息。通过中断机制,当物体发生倾斜、晃动或改变方向时,MMA7660能够产生中断信号,并给出当前物体的姿态(如水平/垂直,上下,左右等)。在硬件设计方面,MMA7660采用了非常小的3mm x 3mm x 0.9mm DFN封装,便于集成到各种小型设备中。此外,还可以通过在9号管脚(DVDD)和8号管脚(DVSS)之间放置0.1μF陶瓷电容器来优化其性能。从应用角度来看,MMA7660因其低功耗、小型化和精确测量的特点,被广泛应用于多种场景。例如,它可以用于手机、掌上电脑、车载导航等设备的防盗功能;自动自行车刹车灯可以根据加速度的变化自动调整刹车灯的亮度;运动检测手环可以利用MMA7660来监测用户的运动状态和步数;数码机和自动叫醒闹钟等也可以利用它来实现更智能的功能。MMA7660是一款功能强大、应用广泛的加速度传感器,能够提供稳定和精确的加速度测量结果,满足不同设备对运动感知和姿态识别的需求。如需更多信息,建议查阅传感器技术手册或咨询相关领域的专家。使用STC89C52单片机读取MMA7660姿态数据的完整代码:#include <reg52.h>#include <intrins.h>#define MMA7660_ADDRESS 0x98 // MMA7660 I2C地址(写入模式)#define MMA7660_XOUT_REG 0x00 // X轴输出寄存器地址#define MMA7660_YOUT_REG 0x01 // Y轴输出寄存器地址#define MMA7660_ZOUT_REG 0x02 // Z轴输出寄存器地址#define MMA7660_TILT_REG 0x03 // 倾斜寄存器地址sbit SCL = P2^1; // I2C时钟线sbit SDA = P2^0; // I2C数据线void delay_ms(unsigned int ms) { unsigned int i, j; for (i = 0; i < ms; i++) for (j = 0; j < 110; j++);}void I2C_start() { SDA = 1; _nop_(); SCL = 1; _nop_(); SDA = 0; _nop_(); SCL = 0; _nop_();}void I2C_stop() { SDA = 0; _nop_(); SCL = 1; _nop_(); SDA = 1; _nop_(); SCL = 0; _nop_();}void I2C_write(uint8_t data) { uint8_t i; for (i = 0; i < 8; i++) { SDA = (data & 0x80) >> 7; _nop_(); SCL = 1; _nop_(); SCL = 0; _nop_(); data <<= 1; } SDA = 1; _nop_(); SCL = 1; _nop_(); SCL = 0; _nop_();}uint8_t I2C_read() { uint8_t i, data = 0; SDA = 1; for (i = 0; i < 8; i++) { _nop_(); SCL = 1; _nop_(); data = (data << 1) | SDA; _nop_(); SCL = 0; } return data;}void MMA7660_init() { I2C_start(); I2C_write(MMA7660_ADDRESS); I2C_write(0x07); // 模式寄存器地址 I2C_write(0x01); // 进入正常模式 I2C_stop();}int read_mma7660_data(uint8_t reg_addr) { int data; I2C_start(); I2C_write(MMA7660_ADDRESS); I2C_write(reg_addr); I2C_start(); I2C_write(MMA7660_ADDRESS | 1); data = (int)I2C_read(); I2C_stop(); return data;}void main() { int x_accel, y_accel, z_accel, tilt; MMA7660_init(); while (1) { x_accel = read_mma7660_data(MMA7660_XOUT_REG); y_accel = read_mma7660_data(MMA7660_YOUT_REG); z_accel = read_mma7660_data(MMA7660_ZOUT_REG); tilt = read_mma7660_data(MMA7660_TILT_REG); // 处理姿态数据 // 在这里可以添加你的代码来处理姿态数据,比如判断倾斜方向等 // 注意:这里仅仅读取了加速度X、Y、Z轴数据和倾斜寄存器的数据,如果需要更精确的姿态信息,可以考虑加入滤波算法或者更复杂的数据处理逻辑 delay_ms(100); // 延时等待,可以根据需要调整 }}
  • [技术干货] 物联网中常用的传感器
    物联网中常用的传感器主要包括以下几种类型:温度传感器:用于测量环境、物体或设备的温度,如热电偶、热敏电阻和红外温度传感器,广泛应用于工业自动化、智能家居、冷链物流、医疗健康等多个领域。光学传感器:用于检测光强度、颜色、方向等光学属性,包括紫外光、可见光、红外光传感器,常见于光照控制、安防系统、火灾报警系统等。图像传感器:将光学图像转化为数字信号,如CMOS或CCD图像传感器,常用于智能摄像头、无人驾驶汽车、无人机等视觉系统中。距离传感器:通过超声波、激光、红外等方式测量物体间的距离,用于自动门、无人机避障、停车辅助系统等。振动传感器:检测机械振动,可用于设备状态监测、地震预警、结构健康监测系统中。声音传感器(麦克风):捕捉声音信号,应用于语音识别系统、噪声监测、安全警报系统。烟雾传感器:检测空气中的烟雾粒子,常用于火警报警系统。人体红外传感器:探测人体发出的红外辐射,应用于智能照明、安防监控、智能家居等人体存在感应场景。湿度传感器:测量空气中水分含量,对环境调控、农业生产、仓储管理等方面至关重要。压力传感器:测量气体或液体的压力,广泛应用于气象站、水位监测、车辆胎压监测系统等。运动传感器(加速度计、陀螺仪):感知物体的移动、倾斜和旋转,常见于智能手机、游戏控制器、运动手环等设备。下面介绍具体的传感器:温度传感器型号:PT100 (铂电阻温度传感器)DS18B20 (单总线数字温度传感器)MLX90614 (非接触式红外温度传感器)光学传感器型号:TSL2561 (光强传感器)BH1750FVI (环境光亮度传感器)AS7262 (多通道色彩传感器)图像传感器型号:OV2640 (OmniVision生产的200万像素CMOS图像传感器)MT9V034 (Microchip生产的全局快门成像传感器)距离传感器型号:HC-SR04 (超声波测距模块)VL53L0X (意法半导体生产的ToF激光测距传感器)振动传感器型号:ADXL355 (ADI公司的低噪声三轴MEMS加速度计,可间接用于振动检测)声音传感器型号:ICS-43434 (InvenSense公司出品的高性能MEMS麦克风)烟雾传感器型号:MQ-2 (用于检测烟雾和可燃气体的传感器模块)人体红外传感器型号:AMG8833 (Panasonic的人体红外热释电传感器)湿度传感器型号:DHT11/DHT22 (数字温湿度传感器模块)压力传感器型号:BMP280 (博世公司的数字气压传感器,同时具备温度测量功能)MS5611 (MEAS瑞士迈来芯的高度计/气压传感器)
  • [技术干货] 传感器技术: STM32读取BH1750光敏传感器数据
    一、环境介绍MCU:  STM32F103ZET6光敏传感器:  BH1750数字传感器(IIC接口)开发软件: Keil5代码说明: 使用IIC模拟时序驱动,方便移植到其他平台,采集的光照度比较灵敏.  合成的光照度返回值范围是 0~255。 0表示全黑  255表示很亮。实测:   手机闪光灯照着的状态返回值是245左右,手捂着的状态返回值是10左右. 二、BH1750介绍三、核心代码BH1750说明:  ADDR引脚接地,地址就是0x463.1 iic.c#include "iic.h"/*函数功能:IIC接口初始化硬件连接:SDA:PB7SCL:PB6*/void IIC_Init(void){ RCC->APB2ENR|=1<<3;//PB GPIOB->CRL&=0x00FFFFFF; GPIOB->CRL|=0x33000000; GPIOB->ODR|=0x3<<6;}/*函数功能:IIC总线起始信号*/void IIC_Start(void){ IIC_SDA_OUTMODE(); //初始化SDA为输出模式 IIC_SDA_OUT=1; //数据线拉高 IIC_SCL=1; //时钟线拉高 DelayUs(4); //电平保持时间 IIC_SDA_OUT=0; //数据线拉低 DelayUs(4); //电平保持时间 IIC_SCL=0; //时钟线拉低}/*函数功能:IIC总线停止信号*/void IIC_Stop(void){ IIC_SDA_OUTMODE(); //初始化SDA为输出模式 IIC_SDA_OUT=0; //数据线拉低 IIC_SCL=0; //时钟线拉低 DelayUs(4); //电平保持时间 IIC_SCL=1; //时钟线拉高 DelayUs(4); //电平保持时间 IIC_SDA_OUT=1; //数据线拉高}/*函数功能:获取应答信号返 回 值:1表示失败,0表示成功*/u8 IIC_GetACK(void){ u8 cnt=0; IIC_SDA_INPUTMODE();//初始化SDA为输入模式 IIC_SDA_OUT=1; //数据线上拉 DelayUs(2); //电平保持时间 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 DelayUs(2); //电平保持时间,等待从机发送数据 IIC_SCL=1; //时钟线拉高,告诉从机,主机现在开始读取数据 while(IIC_SDA_IN) //等待从机应答信号 { cnt++; if(cnt>250)return 1; } IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 return 0;}/*函数功能:主机向从机发送应答信号函数形参:0表示应答,1表示非应答*/void IIC_SendACK(u8 stat){ IIC_SDA_OUTMODE(); //初始化SDA为输出模式 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要发送数据 if(stat)IIC_SDA_OUT=1; //数据线拉高,发送非应答信号 else IIC_SDA_OUT=0; //数据线拉低,发送应答信号 DelayUs(2); //电平保持时间,等待时钟线稳定 IIC_SCL=1; //时钟线拉高,告诉从机,主机数据发送完毕 DelayUs(2); //电平保持时间,等待从机接收数据 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据}/*函数功能:IIC发送1个字节数据函数形参:将要发送的数据*/void IIC_WriteOneByteData(u8 data){ u8 i; IIC_SDA_OUTMODE(); //初始化SDA为输出模式 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要发送数据 for(i=0;i<8;i++) { if(data&0x80)IIC_SDA_OUT=1; //数据线拉高,发送1 else IIC_SDA_OUT=0; //数据线拉低,发送0 IIC_SCL=1; //时钟线拉高,告诉从机,主机数据发送完毕 DelayUs(2); //电平保持时间,等待从机接收数据 IIC_SCL=0; //时钟线拉低,告诉从机,主机需要发送数据 DelayUs(2); //电平保持时间,等待时钟线稳定 data<<=1; //先发高位 }}/*函数功能:IIC接收1个字节数据返 回 值:收到的数据*/u8 IIC_ReadOneByteData(void){ u8 i,data; IIC_SDA_INPUTMODE();//初始化SDA为输入模式 for(i=0;i<8;i++) { IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 DelayUs(2); //电平保持时间,等待从机发送数据 IIC_SCL=1; //时钟线拉高,告诉从机,主机现在正在读取数据 data<<=1; if(IIC_SDA_IN)data|=0x01; DelayUs(2); //电平保持时间,等待时钟线稳定 } IIC_SCL=0; //时钟线拉低,告诉从机,主机需要数据 (必须拉低,否则将会识别为停止信号) return data;}3.2 iic.h#ifndef _IIC_H#define _IIC_H#include "stm32f10x.h"#include "sys.h"#include "delay.h"#define IIC_SDA_OUTMODE() {GPIOB->CRL&=0x0FFFFFFF;GPIOB->CRL|=0x30000000;}#define IIC_SDA_INPUTMODE() {GPIOB->CRL&=0x0FFFFFFF;GPIOB->CRL|=0x80000000;}#define IIC_SDA_OUT PBout(7) //数据线输出#define IIC_SDA_IN PBin(7) //数据线输入#define IIC_SCL PBout(6) //时钟线void IIC_Init(void);void IIC_Start(void);void IIC_Stop(void);u8 IIC_GetACK(void);void IIC_SendACK(u8 stat);void IIC_WriteOneByteData(u8 data);u8 IIC_ReadOneByteData(void);#endif3.3 BH1750.h#ifndef _BH1750_H#define _BH1750_H#include "delay.h"#include "iic.h"#include "usart.h"u8 Read_BH1750_Data(void);#endif3.4 BH1750.c#include "bh1750.h"u8 Read_BH1750_Data(){ unsigned char t0; unsigned char t1; unsigned char t; u8 r_s=0; IIC_Start(); //发送起始信号 IIC_WriteOneByteData(0x46); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:1\r\n"); IIC_WriteOneByteData(0x01); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:2\r\n"); IIC_Stop(); //停止信号 IIC_Start(); //发送起始信号 IIC_WriteOneByteData(0x46); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:3\r\n"); IIC_WriteOneByteData(0x01); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:4\r\n"); IIC_Stop(); //停止信号 IIC_Start(); //发送起始信号 IIC_WriteOneByteData(0x46); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:5\r\n"); IIC_WriteOneByteData(0x10); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:6\r\n"); IIC_Stop(); //停止信号 DelayMs(300); //等待 IIC_Start(); //发送起始信号 IIC_WriteOneByteData(0x47); r_s=IIC_GetACK();//获取应答 if(r_s)printf("error:7\r\n"); t0=IIC_ReadOneByteData(); //接收数据 IIC_SendACK(0); //发送应答信号 t1=IIC_ReadOneByteData(); //接收数据 IIC_SendACK(1); //发送非应答信号 IIC_Stop(); //停止信号 t=(((t0<<8)|t1)/1.2); return t; }3.5 main.ccpp#include "stm32f10x.h"#include "led.h"#include "delay.h"#include "key.h"#include "usart.h"#include "at24c02.h"#include "bh1750.h"int main(){ u8 val; LED_Init(); BEEP_Init(); KeyInit(); USARTx_Init(USART1,72,115200); IIC_Init(); while(1) { val=KeyScan(); if(val) { val=Read_BH1750_Data(); printf("光照强度=%d\r\n",val);// BEEP=!BEEP; LED0=!LED0; LED1=!LED1; } }}  3.6  运行效果图 
  • [技术干货] Linux系统编程-(pthread)线程的使用案例(分离属性、清理函数等)
    这篇文章介绍Linux下线程的创建与基本使用案例,主要是案例代码为主;相关的函数详细介绍在上篇文章里已经介绍过了。1. 案例代码: 线程的创建下面这份代码演示如何创建线程。在编译的时候需要加上-lpthread函数原型:#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);示例代码:#include <stdio.h>#include <unistd.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <dirent.h>#include <stdlib.h>​//[wbyq@wbyq linux_c]$ gcc app.c -lpthread​/*线程工作函数*/void *thread_work_func(void *dev){ int i; for(i=0;i<5;i++) { sleep(1); printf("子线程正在运行.%d \n",i); } //终止当前线程执行 pthread_exit(NULL);}​int main(int argc,char **argv){ /*1. 创建子线程*/ pthread_t thread_id; if(pthread_create(&thread_id,NULL,thread_work_func,NULL)!=0) { printf("子线程创建失败.\n"); return -1; } /*2. 等待子线程结束-清理子线程的空间*/ pthread_join(thread_id,NULL);//--wait printf("主线程正常终止.\n"); return 0;}2. 如何接收子线程的返回值?线程运行的时候默认是结合模式,也可以设置成分离模式,如果是默认的模式,在线程执行完毕后需要回收资源,顺便可以介绍子线程结束时,返回的状态值。函数原型:int pthread_join(pthread_t thread, void **retval);示例代码:#include <stdio.h>#include <unistd.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <dirent.h>#include <stdlib.h>​//[wbyq@wbyq linux_c]$ gcc app.c -lpthread​/*线程工作函数*/void *thread_work_func(void *dev){ int i; for(i=0;i<5;i++) { sleep(1); printf("子线程正在运行.%d \n",i); } //终止当前线程执行 pthread_exit("1234567890");}​int main(int argc,char **argv){ /*1. 创建子线程*/ pthread_t thread_id; if(pthread_create(&thread_id,NULL,thread_work_func,NULL)!=0) { printf("子线程创建失败.\n"); return -1; } /*2. 等待子线程结束-清理子线程的空间*/ char *p; pthread_join(thread_id,&p);//--wait printf("主线程正常终止.子线的返回值:%s\n",p); return 0;}3. 设置线程的分离属性默认情况下,子线程是结合模式,需要手动等待子线程结束,清理空间;子线程也支持设置为分离属性,在子线程运行结束后,自己清理空间,下面的例子就演示如何设置子线程为分离模式。函数原型:int pthread_detach(pthread_t thread);示例代码:#include <stdio.h>#include <unistd.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <dirent.h>#include <stdlib.h>​//[wbyq@wbyq linux_c]$ gcc app.c -lpthread​/*线程工作函数*/void *thread_work_func(void *dev){ int i; for(i=0;i<5;i++) { sleep(1); printf("子线程正在运行.%d \n",i); } //终止当前线程执行 pthread_exit(NULL);}​int main(int argc,char **argv){ /*1. 创建子线程*/ pthread_t thread_id; if(pthread_create(&thread_id,NULL,thread_work_func,NULL)!=0) { printf("子线程创建失败.\n"); return -1; } /*2. 设置线程的分离属性*/ pthread_detach(thread_id); while(1) { printf("主线程正在运行.\n"); sleep(1); } return 0;}4. 注册线程的清理函数线程清理函数,可以在线程退出时自动调用或者手动调用,用于清理一些需要释放的资源。函数原型://注册void pthread_cleanup_push(void (*routine)(void *),void *arg); //释放void pthread_cleanup_pop(int execute);示例代码:#include <stdio.h>#include <unistd.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <dirent.h>#include <stdlib.h>#include <pthread.h>​//[wbyq@wbyq linux_c]$ gcc app.c -lpthread​//线程的清理工作函数void thread_clear_work_func(void *dev){ printf("线程的清理工作函数被调用.\n"); /* 做一些资源清理工作。 比如: 释放malloc申请的空间,关闭打开的文件等等. */}​/*线程工作函数*/void *thread_work_func(void *dev){ int i; //注册清理函数 pthread_cleanup_push(thread_clear_work_func,NULL); for(i=0;i<5;i++) { sleep(1); printf("子线程正在运行.%d \n",i); }​ //终止当前线程执行 pthread_exit(NULL); //释放清理函数 pthread_cleanup_pop(1); }​int main(int argc,char **argv){ /*1. 创建子线程*/ pthread_t thread_id; if(pthread_create(&thread_id,NULL,thread_work_func,NULL)!=0) { printf("子线程创建失败.\n"); return -1; } /*2. 设置线程的分离属性*/ pthread_detach(thread_id); sleep(3); //取消指定子线程结束 pthread_cancel(thread_id);​ while(1) { printf("主线程正在运行.\n"); sleep(1); } return 0;}5. 通过ulimit命令设置栈空间大小pthread_create 创建线程时,若不指定分配堆栈大小,系统会分配默认值,查看默认值方法如下:[root@tiny4412 ]#ulimit -s10240上面的10240单位是KB,也就是默认的线程栈空间大小为10M也可以通过ulimit -a命令查看,其中的stack size也表示栈空间大小。[root@tiny4412 ]#ulimit -a-f: file size (blocks) unlimited-t: cpu time (seconds) unlimited-d: data seg size (kb) unlimited-s: stack size (kb) 10240-c: core file size (blocks) 0-m: resident set size (kb) unlimited-l: locked memory (kb) 64-p: processes 7512-n: file descriptors 1024-v: address space (kb) unlimited-w: locks unlimited-e: scheduling priority 0-r: real-time priority 0设置栈空间大小: ulimit -s <栈空间大小>[root@tiny4412 ]#ulimit -s 8192 //设置栈空间大小[root@tiny4412 ]#ulimit -s //查看栈空间大小8192 //大小为8M注意: 栈空间设置只能在超级管理员用户权限下设置。每个线程的栈空间都是独立的,如果栈空间溢出程序会出现段错误。如果一个进程有10个线程,那么分配的栈空间大小就是10*<每个线程栈大小>例如:int main(int argc,char **argv){ char buff[1024*1024*10]; //在栈空间定义数组,如果超出了栈空间总大小程序会奔溃。 printf("hello world!\n"); return 0;}
  • [技术干货] Qt(C++)计算一段程序执行经过的时间
    一、前言在许多应用程序和系统中,需要对经过的时间进行计算和记录。例如 可能想要测量某个操作的执行时间,或者记录一个过程中经过的时间以进行性能分析。在这些场景下,准确地计时是非常重要的。Qt提供了一个功能强大的计时器类QElapsedTimer,可以方便地记录经过的时间。QElapsedTimer使用高精度的计时器来测量时间片段之间的经过时间。为了更直观地呈现经过的时间,可以使用QTime类来处理时间,并通过其toString()函数将时间格式化为时分秒毫秒的形式。这样,可以获得更具可读性的时间输出。二、代码实现【1】框架模板示例1以下示例代码,展示在Qt中使用QElapsedTimer来记录经过的一段时间:#include <QCoreApplication>#include <QElapsedTimer>#include <QDebug>​int main(int argc, char *argv[]){ QCoreApplication a(argc, argv);​ QElapsedTimer timer; timer.start();​ // 在这里执行想要计时的代码​ qint64 elapsedTime = timer.elapsed(); qDebug() << "经过的时间:" << elapsedTime << "毫秒";​ return a.exec();}​【2】框架模板示例2要将经过的时间以时分秒毫秒的格式打印出来,可以使用Qt中的QTime类来处理时间,并通过toString()函数以特定格式输出。#include <QCoreApplication>#include <QElapsedTimer>#include <QDebug>#include <QTime>​int main(int argc, char *argv[]){ QCoreApplication a(argc, argv);​ QElapsedTimer timer; timer.start();​ // 模拟耗时操作 for (int i = 0; i < 100000000; ++i) { // do something }​ qint64 elapsedTime = timer.elapsed();​ // 将毫秒转换为时分秒毫秒格式 int hours = (elapsedTime / (1000 * 60 * 60)) % 24; int minutes = (elapsedTime / (1000 * 60)) % 60; int seconds = (elapsedTime / 1000) % 60; int milliseconds = elapsedTime % 1000;​ // 输出格式化的时间 QString formattedTime = QTime(hours, minutes, seconds, milliseconds).toString("hh:mm:ss.zzz"); qDebug() << "经过的时间:" << formattedTime;​ return a.exec();}​【3】实际案例:测量ffmpeg消耗时间#include <QApplication>​#include <QtWidgets/QApplication>#include <QtWidgets/QMainWindow>#include <QtWidgets/QLabel>#include <QSysInfo>#include <QProcess>#include <QDebug>#include <QProcess>#include <QFile>​#include <QRegularExpressionMatch>#include <QRegularExpression>#include <QFileInfo>#include <QElapsedTimer>#include <QDir>#include <QTime>​int main(int argc, char *argv[]){ QApplication a(argc, argv);​​ // 设置新的工作目录 QString newWorkingDir = "D:\\Video_out"; QDir::setCurrent(newWorkingDir);​​ QElapsedTimer timer; timer.start();​ // 在这里执行想要计时的代码​ QString cmd="ffmpeg -hwaccel dxva2 -i \"D:/Video_out/baf4c54b164740dc83e6e6094ecb3a0f/cbf20d3085b64626ab2ad42d2ff49e4c.MOV\" -i \"2cea7987170b48f2915923f12f532cd6.png\" -filter_complex \"[0:v][1:v]overlay=0:0\" -y -c:v h264_amf \"D:/Video_out/sub.MOV\"";​ //执行合并命令 QProcess process; process.setProcessChannelMode(QProcess::MergedChannels); process.start(cmd.toUtf8()); while (!process.waitForFinished(100)) { QString sourceString = process.readAll(); qDebug() << sourceString;​ // 进程未结束,继续等待 }​​ qint64 elapsedTime = timer.elapsed(); qDebug() << "经过的时间:" << elapsedTime << "毫秒";​ // 将毫秒转换为时分秒毫秒格式 int hours = (elapsedTime / (1000 * 60 * 60)) % 24; int minutes = (elapsedTime / (1000 * 60)) % 60; int seconds = (elapsedTime / 1000) % 60; int milliseconds = elapsedTime % 1000;​ // 输出格式化的时间 QString formattedTime = QTime(hours, minutes, seconds, milliseconds).toString("hh:mm:ss.zzz"); qDebug() << "经过的时间:" << formattedTime;​ return a.exec();}​三、总结在示例代码中,展示了如何使用QElapsedTimer和QTime来计算和打印出经过的时间。使用QElapsedTimer来开始计时,并在需要记录经过的时间的地方调用elapsed()函数获取经过的毫秒数。然后,将毫秒数转换为时、分、秒和毫秒的格式,并使用QTime的toString()函数将其格式化为易于阅读的字符串。使用这个功能,可以轻松地测量代码块的执行时间、记录操作所需的时间或进行其他时间相关的操作。这对于性能优化、调试和监控等任务非常有用。QElapsedTimer和QTime是Qt提供的功能强大的类,能够处理时间和计时需求。
  • [技术干货] MFC程序中的中文路径问题解决方案
    前言在Windows平台上,使用Microsoft Foundation Classes (MFC) 开发应用程序时,经常会遇到中文路径问题。由于历史原因和编码方式的差异,中文路径在MFC程序中可能会导致一些不可预见的问题,如文件无法打开、路径识别错误等。本文将探讨在MFC程序中如何处理中文路径问题,确保应用程序能够正确地处理包含中文的路径。一、问题背景在MFC程序中,如果直接使用字符串来表示文件路径,并且路径中包含中文字符,那么可能会遇到以下问题:编码问题:Windows API在内部使用宽字符(Unicode)来处理字符串,而MFC中的CString等类默认使用多字节字符集(MBCS)。当路径中包含非ASCII字符(如中文)时,编码不一致可能导致路径识别错误。API兼容性:一些旧的Windows API函数可能不支持Unicode字符串,只支持ANSI字符串。在使用这些API时,如果路径包含中文,就可能出现问题。二、解决方案针对上述问题,可以采取以下策略来解决MFC程序中的中文路径问题:使用Unicode编码将MFC项目设置为使用Unicode编码。这可以确保字符串在内部以宽字符形式处理,从而避免编码不一致的问题。在MFC项目中,可以通过修改项目属性来启用Unicode支持。使用宽字符API尽量使用支持宽字符的API函数。这些函数通常以“W”结尾,如CreateFileW、OpenFileW等。这些函数可以直接处理宽字符字符串,无需进行额外的编码转换。转换字符串编码如果必须使用不支持Unicode的API函数,可以将宽字符字符串转换为多字节字符集(MBCS)字符串。可以使用WideCharToMultiByte函数进行转换。但请注意,这种方法可能会导致信息丢失或乱码,特别是在处理非ASCII字符时。更新第三方库和组件如果项目中使用了第三方库或组件,确保它们也支持Unicode。如果库或组件不支持Unicode,考虑寻找替代方案或更新到支持Unicode的版本。测试与调试在开发过程中,充分测试应用程序对中文路径的处理能力。使用不同长度的中文路径、特殊字符和常见场景进行测试,确保应用程序能够正确识别和处理这些路径。三、最佳实践除了上述解决方案外,以下是一些最佳实践,有助于在MFC程序中更好地处理中文路径:使用标准路径分隔符在构建文件路径时,使用反斜杠(\)作为路径分隔符,而不是正斜杠(/)。虽然Windows通常能够识别正斜杠作为路径分隔符,但使用标准分隔符有助于提高代码的兼容性和可读性。避免硬编码路径尽量不要在代码中硬编码文件路径。相反,使用配置文件、环境变量或用户输入来获取路径。这样可以提高程序的灵活性和可移植性。错误处理当处理文件或路径相关操作时,始终检查返回值并处理潜在的错误。例如,在打开文件时检查CreateFile函数的返回值,以便在失败时采取相应的措施。文档和注释在代码中添加必要的文档和注释,说明如何处理中文路径以及可能遇到的问题。这有助于其他开发人员理解代码并避免重复造轮子。四、总结中文路径问题在MFC程序中是一个常见且需要仔细处理的问题。通过采用Unicode编码、使用宽字符API、转换字符串编码以及遵循最佳实践,可以有效地解决这些问题,并确保应用程序能够正确地处理包含中文的路径。在开发过程中,充分测试和调试也是至关重要的,以确保应用程序的稳定性和可靠性。
  • [技术干货] Linux与windows下C语言使用curl库的安装使用以及访问HTTP下载文件
    一、前言cURL 是一个命令行工具和库,用于传输数据,支持多种协议,如 HTTP、HTTPS、FTP 等。可以在终端中用来发送和接收数据,执行各种网络操作,如下载文件、上传文件、发送 POST 请求等。以下是一些常用的 cURL 命令选项:(1)发送 GET 请求:curl [URL](2)发送 POST 请求:curl -X POST [URL] --data "key1=value1&key2=value2"(3)下载文件:curl -O [URL](4)显示响应头信息:curl -i [URL](5)设置请求头信息:curl -H "Content-Type: application/json" [URL](6)设置超时时间:curl --connect-timeout 10 [URL](7)跟踪重定向:curl -L [URL](8)使用代理服务器:curl -x proxy_host:port [URL](9)保存输出到文件:curl -o output.txt [URL]cURL 提供了丰富的选项和功能,可以用于测试 API、进行网络调试、批量下载文件等各种场景。通过结合不同的选项,可以实现许多复杂的网络操作。如果在命令行中使用 cURL,可以查看 cURL 的文档以了解多详细信息和选项用法。二、Linux下curl下载要在 Linux 系统下编译和安装 cURL 源码,可以按照以下步骤进行操作:(1)下载 cURL 源码: 首先,在 cURL 官方网站(cid:link_0)上下载最新版本的 cURL 源码压缩包,并解压到本地目录。(2)进入源码目录: 使用终端进入解压后的 cURL 源码目录,例如:cd curl-7.x.x # 进入解压后的 cURL 源码目录(3)配置编译参数: 运行以下命令配置 cURL 的编译参数:./configure可以通过 ./configure --help 查看可用的配置选项。(4)编译源码: 运行以下命令编译 cURL 源码:make这将编译 cURL 并生成可执行文件。(5)安装 cURL: 运行以下命令安装编译后的 cURL 文件到系统中:sudo make install这将把 cURL 的可执行文件、库文件和头文件安装到系统路径中。(6)验证安装: 运行以下命令验证 cURL 是否成功安装:curl --version如果成功安装,会显示 cURL 的版本信息。通过以上步骤,就可以在 Linux 系统下编译和安装 cURL 源码。在执行 make install 命令时,Ubuntu下需要使用管理员权限(sudo),以便将文件安装到系统目录。下面是 使用 libcurl 库来下载文件:#include <stdio.h>#include <curl/curl.h>​int main(void){ CURL *curl; FILE *fp; CURLcode res;​ const char *url = "https://www.example.com/file-to-download.txt"; const char *output_filename = "downloaded_file.txt";​ curl = curl_easy_init(); if (curl) { fp = fopen(output_filename, "wb"); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);​ res = curl_easy_perform(curl); if (res != CURLE_OK) { fprintf(stderr, "Failed to download: %s\n", curl_easy_strerror(res)); }​ curl_easy_cleanup(curl); fclose(fp); } else { fprintf(stderr, "Failed to initialize libcurl\n"); }​ return 0;}在这个示例代码中,包含了必要的头文件 <stdio.h> 和 <curl/curl.h>,定义了要下载的文件 URL 和保存到本地的文件名。创建了一个 libcurl 的句柄 CURL *curl,打开了一个文件指针 FILE *fp 用于保存下载的文件。初始化 libcurl 并设置下载的 URL 和保存文件的操作,执行下载操作并将结果写入到文件中。最后清理资源并关闭文件指针。可以将以上代码保存到一个 .c 文件中,然后使用以下方式编译和运行:gcc -o download_program download_program.c -lcurl./download_program三、Windows下curl下载curl for windows : cid:link_1下载页面如图:解压后的可执行文件位置:下面是解压后的文件目录:在命令行使用curl测试下载文件:四、通过命令行使用curlcurl可以直接调用函数库完成功能设计、也可以直接调用可执行文件完成需要的功能,下面这里就介绍,在windows下,通过CreateProcess调用curl命令函数完成文件下载。使用curl实现HTTP协议文件下载成功,通过给定的连接地址,可以完成文件下载,百分比进度返回等等。 /************************************************** 作者: DS小龙哥 功能: 执行命令 参数解释: CallBackFunction_p func_p :回调函数,用于通知进度执行过程 char *text //进度的转码过程,详细描述.描述当前这个操作是做什么. char *total_time //执行的总时间 char *cmd //执行的命令 **************************************************/ int file_down_func(CallBackFunction_p func_p, const char *text, const char *total_time, const char *cmd) { BOOL run_pipe; PROCESS_INFORMATION pi; STARTUPINFO si; BOOL ret = FALSE; DWORD flags = CREATE_NO_WINDOW; char pBuffer[210]; SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; HANDLE hReadPipe, hWritePipe; run_pipe = CreatePipe(&hReadPipe, &hWritePipe, &sa, 0); if (run_pipe != 1) { printf("创建匿名管道文件失败=%d\n", run_pipe); return -1; } ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); si.dwFlags |= STARTF_USESTDHANDLES; si.hStdInput = NULL; si.hStdError = hWritePipe; si.hStdOutput = hWritePipe; wchar_t cmd_wchar[1024]; CharToWchar(cmd, cmd_wchar); //TCHAR cmd[] = TEXT("ffmpeg -i D:\123.mp4 -vf reverse D:\out\out1.mp4"); ret = CreateProcess(NULL, cmd_wchar, NULL, NULL, TRUE, flags, NULL, NULL, &si, &pi); if (ret) { while (true) { DWORD ExitCode = 0; //判断进程是否执行结束 printf("正在执行...GetExitCodeProcess\r\n"); GetExitCodeProcess(pi.hProcess, &ExitCode); printf("ExitCode:%d\r\n", ExitCode); if (ExitCode == STILL_ACTIVE) //正在运行 { DWORD RSize = 0; BOOL run_s = 0; printf("正在执行...ReadFile\r\n"); run_s = ReadFile(hReadPipe, pBuffer, sizeof(pBuffer), &RSize, NULL); pBuffer[RSize - 1] = '\0'; printf("执行过程:%s,%d,%d,%s\n", version_str, run_s, RSize, pBuffer); char number_buff[10]="\0"; //存放百分比 printf("pBuffer=%s\r\n", pBuffer); //通过回调函数向外部返回进度提示 for (size_t i = 0; i < 10 && pBuffer[i]!='\0'; i++) { if (pBuffer[i] >= '0' && pBuffer[i] <= '9') { //得到百分比值 for (size_t j = 0; j < 9 && pBuffer[i+j] != '\0'; j++) { //printf("@@%c@@\r\n", pBuffer[i + j]); if (pBuffer[i+j] >= '0' && pBuffer[i+j] <= '9') { number_buff[j] = pBuffer[i + j]; } else { number_buff[j] = '\0'; break; } } break; } } // 0 926M 0 6463k 0 0 7378k 0 0:02:08 --:--:-- 0:02:08 7386 //如果找到进度的位置 if (strlen(number_buff)>0) { std::string out_str; out_str = text; out_str += ","; out_str += "100"; out_str += ","; out_str += number_buff; printf("回调:%s\r\n", out_str.c_str()); //将执行的结果再回调出去 if (func_p) { func_p(out_str.c_str()); } } } else //结束 { printf("执行完毕,ExitCode=%d\r\n", ExitCode); break; } } printf("正在等待子进程结束....\n"); //等待结束 WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); printf("子进程执行完毕....\n"); return 0; } printf("子进程创建失败:%d\n", ret); return -1; } int main() { string VideoCacheFilePath = "D:\out"; //下载的文件名称 string file_path = "http://192.168.1.110:8001/Uploads/1/哈哈哈.MP4"; //如果返回为真就表示是网络地址 if (strstr(file_path.c_str(), "http:") || strstr(file_path.c_str(), "https:")) { //1.获取不带路径的文件名 string::size_type iPos; if (strstr(file_path.c_str(), "\")) { iPos = file_path.find_last_of('\') + 1; } else { iPos = file_path.find_last_of('/') + 1; } //得到文件名称 string base_file = file_path.substr(iPos, file_path.length() - iPos); //得到完整的文件下载存储路径 string VideoPath_tmp = VideoCacheFilePath; VideoPath_tmp += "\"; VideoPath_tmp += base_file; //判断文件是否存在 printf("文件存储路径:%s\r\n", VideoPath_tmp.c_str()); FILE *file_p = fopen(VideoPath_tmp.c_str(),"rb"); //不存在就下载 if (file_p == nullptr) { //切换目录,进入到curl命令所在的目录 _chdir(VideoCacheFilePath.c_str()); string cmd = "curl -O "; cmd += file_path; //启动下载文件 file_down_func(NULL, base_file.c_str(),"100",cmd.c_str()); } else { printf("文件存在不需要下载.\r\n"); fclose(file_p); } } return 0; }
  • [技术干货] 基于视觉识别的自动采摘机器人设计与实现
    一、前言1.1 项目介绍【1】项目功能介绍随着科技的进步和农业现代化的发展,农业生产效率与质量的提升成为重要的研究对象。其中,果蔬采摘环节在很大程度上影响着整个产业链的效益。传统的手工采摘方式不仅劳动强度大、效率低下,而且在劳动力成本逐渐上升的背景下,越来越难以满足大规模种植基地的需求。人工采摘还可能因不规范的操作导致果实损伤,影响商品果率。基于视觉识别技术的自动采摘机器人的研发,正是针对这一问题提出的创新解决方案。本项目采用树莓派4B作为主控芯片,因其具有强大的计算能力和丰富的扩展接口,可以方便地集成各种传感器和执行机构,实现对复杂环境下的实时图像采集与处理。项目利用百度飞浆(PaddlePaddle)深度学习框架中的目标检测和分类算法,通过安装在机器人上的高清摄像头获取果树图像,并进行实时分析,精准识别出果实的位置、大小以及成熟度等信息。当成功识别到目标果实后,主控系统将根据识别结果快速计算出机械手臂的最佳运动路径,控制其移动至指定位置,以最适宜的方式完成果实的高效、无损采摘。基于视觉识别的自动采摘机器人设计与实现项目旨在解决传统农业中人工采摘的瓶颈问题,通过人工智能与自动化技术的深度融合,提高果园管理的智能化水平,降低劳动成本,提高生产效率,从而推动我国乃至全球农业产业向更加智能、高效的现代农业转型。【2】设计实现的功能(1)视觉识别:借助高性能的摄像头和图像处理算法(本项目采用百度飞浆的目标识别和分类算法),机器人能够捕捉到果园中的果实图像,并准确地从中识别出目标果实。(2)定位与导航:在识别到果实后,系统会通过计算果实的空间坐标和距离,确定机械手臂需要到达的精确位置。同时,机器人会根据果园内的环境信息和路径规划算法,自动导航至目标果实附近。(3)机械手臂控制:一旦机器人到达目标位置,机械手臂会在系统的精确控制下,自动调整姿态和动作,以轻柔而准确的方式采摘果实。这一过程涉及到复杂的机械动力学和协同控制算法,确保采摘动作的高效和安全。(4)果实收集与处理:采摘下来的果实会被机器人收集到专门的容器中,以便后续的分拣、包装和处理。系统还可以对采摘的果实进行数量统计和质量评估,为农业生产提供有价值的数据支持。本项目实现的功能是一个完整的自动采摘机器人系统,从视觉识别到机械手臂控制,再到果实收集与处理,形成了一个高效、智能的自动化采摘流程。这不仅大大提高了农业生产的效率和质量,也展示了人工智能技术在现代农业领域的广阔应用前景。【3】项目硬件模块组成(1)主控板:采用树莓派4B开发板作为整个系统的主控芯片。树莓派是一款功能强大且易于使用的计算机主板,具备高性能的处理器、充足的内存和存储空间,以及丰富的接口和扩展功能,可以满足本项目对计算和控制的需求。(2)视觉系统:视觉系统包括高性能的摄像头和图像处理单元。摄像头负责捕捉果园中的图像信息,而图像处理单元则基于百度飞浆的目标识别和分类算法,对图像进行处理和分析,以识别和定位目标果实。(3)机械手臂:机械手臂是实现自动采摘的关键部件,由多个关节和执行器组成,可以在三维空间内自由移动和旋转。通过精确的控制算法,机械手臂能够准确地到达目标果实的位置,并执行采摘动作。(4)传感器和导航系统:为了实现自动导航和精确定位,项目中还集成了多种传感器和导航系统。这些传感器可以感知环境信息,如距离、方位、障碍物等,而导航系统则根据这些信息规划出机器人的最佳路径。(4)电源和供电系统:为了保证机器人的持续工作,项目中还包括了电源和供电系统。电源负责为各个硬件模块提供稳定的电力供应,而供电系统则可以根据实际需要调整电力输出,以满足机器人在不同工作状态下的能耗需求。本项目的硬件模块组成包括主控板、视觉系统、机械手臂、传感器和导航系统、电源和供电系统以及其他辅助模块。这些硬件模块相互协作,共同实现了基于视觉识别的自动采摘机器人系统的功能。【3】功能总结系统集成了先进的视觉识别技术、机械手臂控制技术以及自动导航技术,能够自动识别和定位果园中的目标果实,并通过机械手臂完成采摘动作。整个过程无需人工干预,实现了果园采摘的自动化和智能化。功能包括果实的自动识别和定位、机械手臂的自动导航和控制以及果实的自动收集和处理。通过高性能的摄像头和图像处理算法,系统能够准确捕捉和识别目标果实的图像信息;借助精确的导航和控制算法,机械手臂能够自动导航至果实位置并完成采摘;最后,采摘下来的果实会被自动收集并进行后续处理。本项目的功能实现不仅提高了果园采摘的效率和准确性,降低了人力成本,同时也为农业生产的现代化和智能化发展提供了新的解决方案和思路。该系统的成功应用将为农业生产带来革命性的变革,推动农业向更高效、更环保、更可持续的方向发展。1.2 设计思路(1)需求分析:对果园采摘的实际需求进行分析,明确项目需要解决的问题和达到的目标。了解果园的环境特点、果实类型和生长状况,以及采摘作业的流程和要求,为后续设计提供基础依据。(2)技术选型:根据需求分析的结果,选择合适的技术方案。选用树莓派4B开发板作为主控芯片,利用其高性能的处理器和丰富的接口资源,实现机器人的控制和管理。同时,采用百度飞浆的目标识别和分类算法,通过视觉系统实现对目标果实的准确识别和定位。(3)硬件设计:根据技术选型,设计机器人的硬件结构。包括摄像头的选型和布局,确保能够捕捉到清晰、稳定的图像信息;机械手臂的设计和选型,使其能够适应果园环境和采摘需求;导航和传感器系统的设计和选型,实现机器人的自动导航和精确定位。(4)软件设计:编写机器人的控制程序和算法。通过图像处理算法实现对目标果实的识别和定位,将结果传递给导航和控制系统;根据导航和传感器系统提供的信息,规划机器人的运动路径和动作,控制机械手臂完成采摘动作;实现果实的计数、分类和收集等功能,以及数据的存储和传输。(5)系统集成与测试:将各个硬件模块和软件程序进行集成,并进行系统测试和调试。确保各个模块之间的通信和协作正常,机器人能够准确识别和采摘目标果实,并实现自动导航和收集等功能。1.3 系统功能总结功能模块功能描述视觉识别- 通过高性能摄像头捕捉果园图像- 利用百度飞浆的目标识别和分类算法,识别目标果实- 确定果实的空间坐标和距离导航与定位- 根据果园环境信息和路径规划算法,自动导航至目标果实附近- 集成多种传感器,感知环境信息,如距离、方位、障碍物等机械手臂控制- 在系统精确控制下,自动调整姿态和动作,采摘果实- 确保采摘动作的高效和安全果实收集与处理- 采摘下来的果实被自动收集到专门容器中- 对采摘的果实进行数量统计和质量评估- 提供数据支持,为农业生产决策提供参考通信与监控- 实现远程监控和控制功能二、树莓派4B环境搭建【1】硬件环境介绍树莓派是什么?Raspberry Pi(中文名为“树莓派”,简写为RPi,或者RasPi/RPi)是为学生计算机编程教育而设计,只有信用卡大小的卡片式电脑,其系统基于Linux。【2】资料下载第一步,先将树莓派4B需要使用的资料下载下来。【3】准备需要的配件(1)准备一张至少32G的TFT卡,用来烧写系统。(2)准备一个读卡器,方便插入TFT卡,好方便插入到电脑上拷贝系统(3)树莓派主板一个(4)一根网线(方便插路由器上与树莓派连接)(5)一根type-C的电源线。用自己Android手机的数据线就行,拿手机充电器供电。【4】准备烧写系统(1)安装镜像烧写工具(2)格式化SD卡将TFT卡通过读卡器插入到电脑上,将TFT卡格式化。(3)烧写系统接下来准备烧写的系统是这一个系统:将系统解压出来。然后打开刚才安装好的镜像烧写工具,在软件中选择需要安装的 img(镜像)文件,“Device”下选择SD的盘符,然后选择“Write”,然后就开始安装系统了,根据你的SD速度,安装过程有快有慢。注意:从网盘下载下来的镜像如果没有解压就先解压,释放出img文件。下面是烧写的流程:点击YES,开始烧写。烧写过程中:安装结束后会弹出完成对话框,说明安装就完成了,如果不成功,需要关闭防火墙一类的软件,重新插入SD进行安装。需要注意的是,安装完,windows系统下看到SD只有74MB了,这是正常现象,因为linux下的磁盘分区win下是看不到的。 烧录成功后windows系统可能会因为无法识别分区而提示格式化分区,此时千万不要格式化!不要格式化!不要格式化!点击取消,然后弹出内存卡,插入到树莓派上。至此,树莓派烧写成功。【5】启动系统(1)树莓派供电由于我买的树莓派开发板不带电源线,就采用Android手机的充电线供电。 使用Type-C供电时,要求电源头的参数要求,电压是5V,电流是3A。我的充电器是小米的120W有线快充,刚好满足要求。(2)启动树莓派(以Type-C供电示例)烧写完后把MicroSD卡直接插入树莓派的MicroSD卡插槽,如果有显示器就连接显示器,有DHMI线机也可以连接外接的显示器,有鼠标、键盘都可以插上去,就可以进入树莓派系统了。但是,我这块板子就一个主板,什么都没有。就拿网线将树莓派的网口与路由器连接。上电之后,开发板的指示灯会闪烁,说明已经启动。(3)查看开发板的IP地址现在板子没屏幕,想要连接板子,只能通过SSH远程登录的方式,当前烧写的这个系统默认开机就启动了SSH,所以只要知道开发板的IP地址就可以远程登录进去。如何知道树莓派板子的IP地址?方法很多,最简单是直接登录路由器的后台界面查看连接进入的设备。我使用的小米路由器,登录后台,看到了树莓派的IP地址。(4)SSH方式登录开发板当前烧写系统的登录账号和密码如下:账号:pi 密码:yahboom打开SSH远程登录工具:PuTTY_0.67.0.0.exe。输入IP地址和端口号,点击open。然后输入账号和密码。输入用户名 pi按下回车,然后再输入密码 yahboom。 注意:Linux下为了保护隐私,输入密码是不可见的,你只需要正常输入,按下回车键确定 即可。正常情况下,就登录成功了。接下来看看联网情况。 ping一下百度测试互联网是否畅通,因为接下来要在线安装软件包。ping www.baidu.com可以看到网络没有问题。提示: 按下 Ctrl + C 可以终止命令行。 这算是Linux基础。【6】windows远程登录桌面为了方便图形化方式开发,可以使用windows系统通过远程桌面登录树莓派,就可以看到界面了,不过需要先安装工具。(1)安装xdrp在树莓派的命令行终端输入命令:sudo apt-get install xrdp按下回车之后,会弹出确认窗口。输入 y之后,按下回车,继续安装。安装完毕:(2)打开windows远程桌面在windows电脑上打开运行命令的窗口,输入mstsc来打开远程桌面。打开远程桌面的窗口:(3)连接树莓派远程桌面打开远程桌面后,输入树莓派开发板的IP地址,点击连接。如果弹出窗口,就选择是。接下来就进入到树莓派开发板的远程桌面的登录窗口了。接下来输入面账号和密码。账号:pi 密码:yahboom输入后点击OK按钮登录。正常情况下,就顺利的进入树莓派的桌面了。接下来就可以进行远程桌面开发了。【7】扩展树莓派SD卡可用空间树莓派系统默认启动时,树莓派默认没有把整个存储空间拓展到整张卡中,如果需要使用整个SD卡,这时候可以通过人为的把存储空间拓展到整张卡上。(1)查看内存使用情况打开命令行终端,输入df -h 命令。(2)扩展内存<1> 打开树莓派命令行终端输入:pi@raspberrypi:~ $ sudo raspi-config<2> 在弹出的命令行里选择Advanced Options<3> 选择第一个选项。<4> 点击确定<5> 点击右边的Finish按钮保存退出。确定之后,关闭界面,系统会自动重启,重启之后,使用df命令查看是否扩展成功(我这里插的是32G的SD卡)。可以看到,我的系统已经扩展成功了,目前可以内存空间是19G。【8】树莓派连接WIFI(1)配置需要连接的WIFI点击右上角的数据连接图标,打开WIFI列表,点击想要的WIFI进行连接。输入密码:连接成功后的效果:(2)通过WIFI的IP地址登录远程桌面在路由器的后台可以看到,目前树莓派连入了两个IP地址。接下来把网线拔掉,使用WIFI无线也可以直接连接无线桌面,这样就不用插网线了。账号和密码:账号:pi 密码:yahboom三、代码设计3.1 舵机控制代码(机械手臂控制)C语言代码: 使用wiringPi库控制树莓派上的GPIO引脚,实现对舵机的控制。通过servo_rotate()函数可以控制舵机旋转到指定的角度。在main()函数中,使用键盘输入获取目标角度,并调用servo_rotate()函数控制舵机旋转到目标角度。舵机的控制方式为PWM脉冲宽度调制,即将角度转换为脉宽值并输出对应的高低电平信号。将舵机信号线连接到GPIO18引脚,通过digitalWrite()函数输出高低电平来控制舵机旋转。#include <wiringPi.h>#include <stdio.h>​#define SERVO_PIN 18 // SG90舵机信号线连接的GPIO引脚​void servo_rotate(int angle) { int pulse_width = (angle * 11) + 500; // 将角度转换为脉宽值 digitalWrite(SERVO_PIN, HIGH); // 输出高电平 delayMicroseconds(pulse_width); // 延时脉宽值对应的时间 digitalWrite(SERVO_PIN, LOW); // 输出低电平 delay(20 - pulse_width / 1000); // 延时剩余时间}​int main(void) { wiringPiSetupGpio(); // 初始化wiringPi库 pinMode(SERVO_PIN, OUTPUT); // 将舵机信号线接口设为输出模式​ while(1) { // 从键盘输入目标角度 printf("Enter the angle to rotate (0-180): "); fflush(stdout); int angle; scanf("%d", &angle);​ // 旋转到目标角度 if(angle >= 0 && angle <= 180) { servo_rotate(angle); } else { printf("Invalid angle! Please enter an angle between 0 and 180.\n"); } }​ return 0;}Pyhon代码: 使用RPi.GPIO库来控制树莓派上的GPIO引脚,实现对舵机的控制。通过setup()函数进行初始化设置,并通过set_angle()函数控制舵机旋转到指定的角度。在main()函数中,使用键盘输入获取目标角度,并调用set_angle()函数控制舵机旋转到目标角度。import RPi.GPIO as GPIOimport time​SERVO_PIN = 18 # SG90舵机信号线连接的GPIO引脚​def setup(): GPIO.setmode(GPIO.BCM) GPIO.setup(SERVO_PIN, GPIO.OUT) global servo_pwm servo_pwm = GPIO.PWM(SERVO_PIN, 50) # 创建PWM对象,频率设置为50Hz servo_pwm.start(0) # 启动PWM输出,初始占空比设为0​def set_angle(angle): duty_cycle = (angle / 18) + 2.5 # 将角度转换为占空比值 servo_pwm.ChangeDutyCycle(duty_cycle) time.sleep(0.3) # 等待舵机转到指定角度​def main(): setup()​ while True: # 从键盘输入目标角度 angle = int(input("Enter the angle to rotate (0-180): "))​ # 旋转到目标角度 if 0 <= angle <= 180: set_angle(angle) else: print("Invalid angle! Please enter an angle between 0 and 180.")​if __name__ == '__main__': try: main() finally: servo_pwm.stop() # 停止PWM输出 GPIO.cleanup() # 清理GPIO资源3.2 调用算法识别目标(1)安装PaddlePaddle和PaddleDetection库:先安装Python和pip。然后,打开终端并执行以下命令安装PaddlePaddle和PaddleDetection库:pip install paddlepaddle paddlepaddle-gpupip install paddlehubpip install paddlehub -i https://pypi.tuna.tsinghua.edu.cn/simple(2)下载预训练模型:百度飞桨提供了预训练的目标检测模型,可以从PaddleDetection的GitHub页面下载这些模型。选择适合的任务的模型,并将其解压到合适的目录中。(3)编写调用代码:创建一个Python脚本文件,例如detect_fruits.py,并使用以下代码编写脚本:import paddlehub as hubimport cv2def detect_fruits(image_path, model_path): # 加载模型 module = hub.Module(name='yolov3_mobilenet_v1_coco2017') input_dict = {'image': [image_path]} # 目标检测 results = module.object_detection(data=input_dict) # 处理结果 for result in results: if len(result['data']) > 0: for obj in result['data']: label = obj['label'] confidence = obj['confidence'] left, top, right, bottom = obj['left'], obj['top'], obj['right'], obj['bottom'] print(f"Label: {label}, Confidence: {confidence:.2f}") print(f"Bounding Box: ({left}, {top}), ({right}, {bottom})") # 可视化结果 img = cv2.imread(image_path) for result in results: module.visualize(data=result, output_dir='output', score_thresh=0.5, use_visualize=True, visualization=True, plot_bbox=True, save_bbox_txt=True, image=img)if __name__ == '__main__': image_path = 'path/to/your/image.jpg' # 替换为你的图片路径 model_path = 'path/to/your/model' # 替换为你的模型路径 detect_fruits(image_path, model_path)在上面的代码中,使用PaddleHub库加载了预训练的yolov3_mobilenet_v1_coco2017模型,并将其应用于指定的图像。然后,处理检测结果并进行输出。最后,使用OpenCV库可视化结果并保存到指定目录中。(4)运行脚本:将目标果实图像放置在与脚本相同的目录下(或根据需要修改图像路径)。然后,在终端中执行以下命令运行脚本:python detect_fruits.py脚本将分析图像并输出检测到的目标果实的标签、置信度和边界框。会生成一个带有目标果实标注的图像。3.3 机器人小车控制代码小车的电机驱动采用L298N模块,连接在GPIO17、GPIO18、GPIO27和GPIO22上。 使用了wiringPi库来控制树莓派上的GPIO引脚,实现对小车电机驱动的控制。通过setup()函数进行初始化设置,并通过forward()、backward()、turn_left()和turn_right()函数控制小车前进、后退和转弯。其中,stop()函数用于停止小车运动。#include <wiringPi.h>#define MOTOR_ENA_PIN 0 // L298N模块ENA引脚连接的GPIO引脚#define MOTOR_ENB_PIN 2 // L298N模块ENB引脚连接的GPIO引脚#define MOTOR_IN1_PIN 3 // L298N模块IN1引脚连接的GPIO引脚#define MOTOR_IN2_PIN 4 // L298N模块IN2引脚连接的GPIO引脚#define MOTOR_IN3_PIN 5 // L298N模块IN3引脚连接的GPIO引脚#define MOTOR_IN4_PIN 6 // L298N模块IN4引脚连接的GPIO引脚void setup() { wiringPiSetup(); // 初始化wiringPi库 pinMode(MOTOR_ENA_PIN, OUTPUT); pinMode(MOTOR_ENB_PIN, OUTPUT); pinMode(MOTOR_IN1_PIN, OUTPUT); pinMode(MOTOR_IN2_PIN, OUTPUT); pinMode(MOTOR_IN3_PIN, OUTPUT); pinMode(MOTOR_IN4_PIN, OUTPUT);}void forward() { digitalWrite(MOTOR_IN1_PIN, HIGH); digitalWrite(MOTOR_IN2_PIN, LOW); digitalWrite(MOTOR_IN3_PIN, LOW); digitalWrite(MOTOR_IN4_PIN, HIGH); digitalWrite(MOTOR_ENA_PIN, HIGH); digitalWrite(MOTOR_ENB_PIN, HIGH);}void backward() { digitalWrite(MOTOR_IN1_PIN, LOW); digitalWrite(MOTOR_IN2_PIN, HIGH); digitalWrite(MOTOR_IN3_PIN, HIGH); digitalWrite(MOTOR_IN4_PIN, LOW); digitalWrite(MOTOR_ENA_PIN, HIGH); digitalWrite(MOTOR_ENB_PIN, HIGH);}void turn_left() { digitalWrite(MOTOR_IN1_PIN, LOW); digitalWrite(MOTOR_IN2_PIN, HIGH); digitalWrite(MOTOR_IN3_PIN, LOW); digitalWrite(MOTOR_IN4_PIN, HIGH); digitalWrite(MOTOR_ENA_PIN, HIGH); digitalWrite(MOTOR_ENB_PIN, HIGH);}void turn_right() { digitalWrite(MOTOR_IN1_PIN, HIGH); digitalWrite(MOTOR_IN2_PIN, LOW); digitalWrite(MOTOR_IN3_PIN, HIGH); digitalWrite(MOTOR_IN4_PIN, LOW); digitalWrite(MOTOR_ENA_PIN, HIGH); digitalWrite(MOTOR_ENB_PIN, HIGH);}void stop() { digitalWrite(MOTOR_ENA_PIN, LOW); digitalWrite(MOTOR_ENB_PIN, LOW);}int main() { setup(); while (1) { // 从键盘输入指令 char cmd = getchar(); getchar(); // 忽略回车符 // 根据指令执行动作 switch (cmd) { case 'w': // 前进 forward(); break; case 's': // 后退 backward(); break; case 'a': // 左转 turn_left(); break; case 'd': // 右转 turn_right(); break; case 'x': // 停止 stop(); break; default: break; } } return 0;}四、总结随着农业技术的不断进步,自动化、智能化已成为现代农业生产的重要趋势。本项目通过结合视觉识别技术、机器人技术和自动化控制技术,成功设计并实现了基于视觉识别的自动采摘机器人系统。这一创新性的成果不仅提高了果园采摘的效率和准确性,降低了人力成本,还为农业生产的现代化和智能化发展提供了新的解决方案和思路。
  • [技术干货] 基于STM32的儿童智能安全防护书包设计
    一、前言1.1 项目介绍【1】项目功能介绍随着社会的进步和科技的发展,儿童安全问题日益引起广泛关注。在日常生活中,尤其是在上学放学途中、户外活动时,儿童走失事件时有发生,给家庭和社会带来了极大的困扰和担忧。随着学业负担的增加,学生时常会因为忘记携带所需书籍而影响学习。如何利用现代技术手段提高儿童安全保障水平,并辅助他们培养良好的学习习惯,成为了一个待解决的社会需求。基于此背景,当前设计并实现一款基于STM32F103RCT6微控制器为核心的儿童智能安全防护书包,显得尤为必要与实际。这款书包集成了先进的定位技术和无线通信模块,能够实时追踪并发送儿童的位置信息给家长,确保在紧急情况下快速响应 (发送短信的时候,直接通过GPS经纬度拼接百度地图的HTTP请求链接,家长收到短信可以直接打开链接,在网页查看百度地图上显示的具体位置,可以直接通过百度地图导航过去)。同时,具备智能化功能,如课程表录入存储与提醒系统,利用EEPROM(例如AT24C02)进行数据持久化存储,并通过RFID-RC522射频识别模块自动检测所携带书籍是否齐全,避免孩子因疏忽遗漏课本而耽误学习。智能书包还配备了直观易读的1.44寸LCD显示屏,用于显示当前位置信息、当日课表以及未带书籍的提醒。当检测到缺少某本书籍时,蜂鸣器模块会发出声音警报,从而强化提醒效果,帮助学生养成有序整理个人物品的习惯。这款基于STM32的儿童智能安全防护书包是一个集成物联网技术、GPS定位、无线通信和智能感知于一体的创新产品,提升儿童的安全防护等级,加强家校互动,促进学生自我管理能力的培养,充分体现了科技服务于生活、服务于教育的理念。【2】设计实现的功能(1)实时定位与紧急求助功能:通过集成GPS模块,该智能书包能够实时获取并更新儿童的位置信息,并通过无线通信(GSM短信模块如Air724UG 4G)将位置数据发送给家长。当儿童在陌生环境中迷路或者遇到紧急情况时,只需按下求救按钮,系统立即向预设的家长手机发送包含当前位置信息的短信,方便家长迅速找到孩子。(2)课程表管理与提醒功能:设计有课程表录入存储模块,利用EEPROM芯片AT24C02进行非易失性数据存储,家长或学生可以预先将每日课程表输入到系统中。每天早上,书包会根据存储的课程表自动检查当天所需的书籍是否已放入书包内。通过射频识别RFID-RC522模块读取贴在书籍上的标签信息,若发现缺少某科书籍,则蜂鸣器会发出声音警报,同时LCD显示屏也会显示相应的提示信息,确保学生不会忘记携带必要的学习资料。(3)可视化信息展示:配备了1.44寸LCD显示屏,可实时显示当前地理位置信息、时间以及当日的课程表内容,使得学生和家长可以直观地查看重要信息。这款基于STM32的儿童智能安全防护书包实现了儿童安全监护和学业辅助两大核心功能,既有助于保障孩子的安全出行,又能培养他们的自我管理和规划能力,体现了科技产品在教育领域的深度应用价值。【3】项目硬件模块组成(1)主控芯片:STM32F103RCT6微控制器作为整个系统的“大脑”,负责控制和协调各个功能模块的运作,处理GPS定位数据、GSM短信通信、RFID识别信息等,并通过程序逻辑实现课程表管理、提醒以及数据显示等功能。(2)定位模块:GPS模块,用于实时获取并更新儿童所在位置信息,确保家长可以随时查看孩子的位置状态。(3)无线通信模块:Air724UG 4G GSM短信模块,提供远程通信能力,当发生紧急情况时,儿童可通过书包上的求救按钮触发发送带有位置信息的短信至预设的家长手机。(4)存储模块:AT24C02 EEPROM芯片,用作非易失性存储器,存储孩子的课程表信息,即使在断电情况下也能保存数据不丢失。(5)射频识别模块:RFID-RC522模块,配合贴在书籍上的RFID标签,检测书包内是否携带齐全当日所需的书籍资料,如果发现缺少书籍,会触发报警提示。(6)报警提示模块:高电平触发的蜂鸣器模块,在检测到未带书籍或其它异常情况时,通过发出声音警报来提醒学生。(7)显示模块:采用1.44寸LCD显示屏,实时展示当前位置、时间、当天课表及未带书籍等重要信息,方便用户直观了解当前状况。【3】功能总结基于STM32的儿童智能安全防护书包设计(1)定位模块:定位模块实时获取儿童位置信息,能够家长在放学时找到儿童位置,或者丢失时及时查找儿童的位置。 (2)GSM短信模块:儿童如果意识到自己走丢,就可以按下求救按钮发送位置短信给家长,进而使家长尽快找到儿童位置 (3)课程表录入存储模块:将每天的课程表录入系统,以便提醒学生第二天要带什么书籍。(eeprom) (4)射频识别模块:将每一科的书籍贴上标签再与系统中的课表进行对比,如果检测到当天某一科书本没有带,蜂鸣器会响,呼吸灯会亮。(RFID) (5)显示模块:LCD屏显示实时位置课表信息以及提示没有带的书籍。硬件选型:(1)主控芯片采用STM32F103RCT6(2)定位模块采用: GPS模块(3)短信发送模块采用 Air724UG 4G(4)存储模块采用AT24C02(5)射频识别模块采用RFID-RC522(6)报警提示采用高电平触发的蜂鸣器模块(7)显示屏采用1.44寸LCD显示屏1.2 设计思路(1)需求分析阶段:对目标用户群体(儿童与家长)的需求进行深入研究和理解,确定主要功能点:儿童安全定位、紧急求助、课程表管理与提醒、书籍携带检测等。(2)系统架构设计:根据需求,选择STM32F103RCT6作为主控芯片,因其具有强大的处理能力、丰富的外设接口及低功耗特性,能够满足项目所需的复杂计算任务和多模块协调工作。(3)功能模块划分:定位模块设计采用GPS接收器,实时获取并解析位置信息。无线通信模块选用4G GSM短信模块Air724UG,实现实时位置信息的远程发送和接收紧急求助信号。数据存储模块使用EEPROM AT24C02,确保课程表数据的安全可靠存储。为实现书籍携带检测,利用RFID-RC522射频识别模块,结合预置在书籍上的RFID标签,自动识别书包内书籍是否齐全。报警提示模块通过高电平触发的蜂鸣器来发出声音警告,提醒学生遗漏书籍。显示模块配备1.44寸LCD显示屏,直观展示位置信息、课程表以及未带书籍的提醒。(4)软硬件协同设计:硬件方面,合理布局各模块,优化电源管理,确保设备稳定运行;软件方面,编写高效的嵌入式程序,实现对各个硬件模块的控制和交互,包括GPS数据解析、GSM通信协议栈开发、RFID读取与比对算法、数据显示逻辑等。(5)人机交互设计:设计简洁易用的界面和操作流程,如一键求救按钮、清晰的课程表显示、直观的报警提示等,便于儿童和家长快速理解和操作。1.3 系统功能总结功能模块功能描述定位模块(GPS)实时获取并更新儿童地理位置信息,通过无线通信模块发送至家长设备,以便家长随时掌握孩子位置动态。短信通信模块(GSM)儿童在紧急情况下按下求救按钮,系统通过4G GSM模块向预设的家长手机号码发送包含定位信息的短信进行求助。课程表管理模块(EEPROM)学生或家长可以录入每日课程表至系统中,利用AT24C02 EEPROM芯片存储数据,确保断电后仍能保留课程信息。书籍检测模块(RFID)通过RFID-RC522射频识别模块读取书本上的标签信息,自动比对与当日课表要求的书籍是否齐全,如有遗漏则触发报警提示。报警提示模块(蜂鸣器)当检测到学生未携带某科书籍或发生其他异常情况时,高电平触发的蜂鸣器会发出声音警告提醒学生。显示模块(LCD显示屏)配备1.44寸LCD显示屏实时显示当前位置、时间、当天课程表以及未带书籍的提示信息,方便学生查看和确认。1.4 开发工具的选择STM32的编程语言选择C语言,C语言执行效率高,大学里主学的C语言,C语言编译出来的可执行文件最接近于机器码,汇编语言执行效率最高,但是汇编的移植性比较差,目前在一些操作系统内核里还有一些低配的单片机使用的较多,平常的单片机编程还是以C语言为主。C语言的执行效率仅次于汇编,语法理解简单、代码通用性强,也支持跨平台,在嵌入式底层、单片机编程里用的非常多,当前的设计就是采用C语言开发。开发工具选择Keil,keil是一家世界领先的嵌入式微控制器软件开发商,在2015年,keil被ARM公司收购。因为当前芯片选择的是STM32F103系列,STMF103是属于ARM公司的芯片构架、Cortex-M3内核系列的芯片,所以使用Kile来开发STM32是有先天优势的,而keil在各大高校使用的也非常多,很多教科书里都是以keil来教学,开发51单片机、STM32单片机等等。目前作为MCU芯片开发的软件也不只是keil一家独大,IAR在MCU微处理器开发领域里也使用的非常多,IAR扩展性更强,也支持STM32开发,也支持其他芯片,比如:CC2530,51单片机的开发。从软件的使用上来讲,IAR比keil更加简洁,功能相对少一些。如果之前使用过keil,而且使用频率较多,已经习惯再使用IAR是有点不适应界面的。二、代码设计2.1 GPS解析代码基于STM32 HAL库进行GPS NMEA协议数据解析的代码#include "stm32f1xx_hal.h"#include <string.h>#include <stdio.h>​// GPS 数据结构体定义typedef struct { char GPGGA[100]; // 用于存储GPGGA语句 double latitude; // 纬度 double longitude; // 经度 float UTC_time[7]; // UTC时间 int fix_quality; // 定位质量 float hdop; // 水平精度因子} GPS_Data_TypeDef;​// 全局变量声明GPS_Data_TypeDef GPS_Data;​// 串口接收缓冲区uint8_t RxBuffer[256];​// GPS NMEA数据处理函数void ProcessGPSData(void) { static uint8_t index = 0; // 从串口接收到的数据中查找特定的NMEA语句,例如GPGGA if (strstr((char*)RxBuffer, "$GPGGA")) { // 分割NMEA语句获取所需字段 char *token; token = strtok((char*)RxBuffer, ","); while(token != NULL) { // 解析各字段 if (/* 判断当前token是否为纬度 */) { GPS_Data.latitude = atof(token); // 考虑南北纬转换及格式化 } else if (/* 判断当前token是否为经度 */) { GPS_Data.longitude = atof(token); // 考虑东西经转换及格式化 } else if (/* 判断当前token是否为UTC时间 */) { // 处理时间信息 } token = strtok(NULL, ","); }​ // 清空接收缓冲区以便下次接收新的数据 memset(RxBuffer, 0, sizeof(RxBuffer)); index = 0; }}​// 串口中断服务函数void USART1_IRQHandler(void) { static uint8_t UART_Rx_STA = 0; uint8_t temp=0;​ if ((__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET) && (__HAL_UART_GET_IT_SOURCE(&huart1, UART_IT_RXNE) != RESET)) { temp = huart1.Instance->RDR & 0xff; // 读取接收到的数据​ // 将接收到的数据添加到缓冲区,并检查是否有完整的NMEA句子结束符'\r\n' if (temp == '\n') { // 根据实际使用情况可能需要同时检测'\r'和'\n' RxBuffer[index++] = temp; // 添加换行符 RxBuffer[index] = '\0'; // 字符串结束符 ProcessGPSData(); // 调用解析函数 } else if (index < (sizeof(RxBuffer)-1)) { RxBuffer[index++] = temp; } }}​int main(void) { // 初始化系统时钟和USART1 MX_GPIO_Init(); MX_USART1_UART_Init();​ // 开启串口中断 __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);​ while (1) { // 主循环 // ... }}2.2 AT24C02存储代码#include "stm32f1xx_hal.h"#include "stm32f1xx_hal_i2c.h"​#define EEPROM_ADDRESS 0xA0 // AT24C02的I2C地址​I2C_HandleTypeDef hi2c1;​void SystemClock_Config(void);​void I2C_Init() { hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(&hi2c1);}​void EEPROM_Write(uint16_t addr, uint8_t data) { HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDRESS, addr, I2C_MEMADD_SIZE_8BIT, &data, 1, HAL_MAX_DELAY);}​uint8_t EEPROM_Read(uint16_t addr) { uint8_t data; HAL_I2C_Mem_Read(&hi2c1, EEPROM_ADDRESS, addr, I2C_MEMADD_SIZE_8BIT, &data, 1, HAL_MAX_DELAY); return data;}​int main(void) { HAL_Init(); SystemClock_Config(); I2C_Init();​ uint16_t address = 0x00; uint8_t data_to_write = 0x55; uint8_t data_read;​ // 写入数据到EEPROM EEPROM_Write(address, data_to_write);​ // 从EEPROM读取数据 data_read = EEPROM_Read(address);​ // 检查读取的数据 if (data_read == data_to_write) { // 读取成功 } else { // 读取失败 }​ while (1) { }}2.3 RFID-RC522代码STM32与RFID-RC522的交互涉及到很多步骤,包括初始化RFID模块、检测卡片、读取卡片信息、以及将这些信息存储起来。#include "stm32f10x.h" #include "SPI.h" #include "RFID.h" #include "usart.h" // 如果你使用了串口打印,可以包含这个头文件 #define SS_PIN PA4 // 根据你的连接修改这个引脚 #define RST_PIN PA3 // RFID模块的复位引脚 RFID rfid(SS_PIN, RST_PIN); // 创建RFID对象 uint8_t uid[5]; // 用于存储卡片的UID int main(void) { SystemInit(); // 初始化系统 usart_init(115200); // 初始化串口,用于调试 SPI_Init(); // 初始化SPI接口 rfid.PCD_Init(); // 初始化RFID模块 while (1) { if (rfid.PICC_IsNewCardPresent()) // 检测是否有新卡片 { rfid.PICC_ReadCardSerial(); // 读取卡片UID for (uint8_t i = 0; i < rfid.uid.size; i++) // 将UID存储到uid数组中 { uid[i] = rfid.uid.uidByte[i]; } // 可以在这里添加代码将uid存储到EEPROM、Flash或其他存储介质中 // 也可以通过串口打印UID进行调试 printf("Card UID: "); for (uint8_t i = 0; i < rfid.uid.size; i++) { printf("%02X ", uid[i]); } printf("\r\n"); } HAL_Delay(100); // 延时一段时间,避免过度占用CPU } }2.4 Air724UG 4G代码使用STM32的HAL库和USART外设(串口)与Air724UG 4G模块进行通信。通过UART_SendString()函数可以发送字符串到串口,通过GSM_SendCommand()函数可以向4G模块发送AT指令。在main()函数中,设置短信格式为文本模式,设置短信接收方号码,发送短信内容。#include "stm32f1xx_hal.h"#include <string.h>​UART_HandleTypeDef huart1;​void SystemClock_Config(void);static void MX_GPIO_Init(void);static void MX_USART1_UART_Init(void);​void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { // UART发送完成回调函数}​void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // UART接收完成回调函数}​void UART_SendString(const char *str) { HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), HAL_MAX_DELAY);}​void GSM_SendCommand(const char *cmd) { UART_SendString(cmd); UART_SendString("\r\n"); HAL_Delay(100); // 等待响应}​int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init();​ char phone_number[] = "13800138000"; char message[] = "Hello, this is a test message from STM32!";​ // 初始化Air724UG 4G模块 GSM_SendCommand("AT+CMGF=1"); // 设置短信格式为文本模式 GSM_SendCommand("AT+CMGS=\"+86"); // 设置短信接收方号码 GSM_SendCommand(phone_number); GSM_SendCommand("\"");​ // 发送短信内容 GSM_SendCommand(message); HAL_UART_Transmit(&huart1, (uint8_t)26, 1, HAL_MAX_DELAY); // 发送Ctrl+Z结束符​ while (1) { }}​void SystemClock_Config(void) { // 系统时钟配置}​static void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 9600; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&huart1);}​三、总结基于STM32的儿童智能安全防护书包设计通过先进的技术手段,为儿童的安全提供全方位的保障。本项目融合了GPS定位、GSM短信通信、EEPROM存储、RFID射频识别以及LCD显示等多项功能,力求在儿童的日常生活和学习中提供便捷与安全。通过GPS模块,书包能够实时追踪儿童的位置,让家长随时了解孩子的行踪,确保在关键时刻能够迅速找到孩子。而GSM短信模块则为儿童提供了一个紧急求助的途径,一旦孩子意识到自己走丢或有其他紧急情况,只需按下求救按钮,即可将位置信息以短信的形式发送给家长,从而迅速获得帮助。在学习方面,本项目通过EEPROM存储模块实现了课程表的录入与存储功能。儿童或家长可以将每天的课程表录入系统,书包便会在需要时提醒孩子携带相应的书籍。而RFID射频识别模块则负责检测书包内是否已携带当天所需的各科书本。如果检测到某一科书本缺失,书包上的蜂鸣器会发出警报声,呼吸灯也会亮起,以提醒孩子及时补充。此外,本项目还采用了一块1.44寸的LCD显示屏,用于实时显示儿童的位置信息、课表内容以及未携带书籍的提示信息。这使得儿童在使用书包的过程中能够随时了解自己的位置和当天的学习任务,为他们的安全和学习提供了双重保障。
  • [技术干货] 嵌入式开发_C语言文件操作函数案例
    在这篇精彩的博客文章中,为希望提高C语言文件编程技能的读者准备了四个实战练习题。这些练习覆盖了从文件拷贝到学生管理系统开发的关键技能,包括文件加密和利用文件系统保存信息的链表模板。文章不仅提出了挑战,通过实践加深对文件操作函数应用的理解。文章知识点:文件拷贝实现:引导读者了解如何逐字节地读取源文件并写入目标文件,完成文件的复制过程。文件加密练习:介绍简单的文件加密技术,让读者尝试对文件内容进行加密和解密操作。学生管理系统链表模板:提供一个未添加文件操作的学生管理系统链表模板,作为后续练习的基础。学生管理系统模板:要求读者将学生信息通过文件系统进行持久化保存,增强管理系统的实用性。1.1 文件拷贝实现#include <stdio.h>#include <stdlib.h>#include <string.h>int main(){ /*1. 打开源文件*/ FILE *file_src; FILE *file_new; unsigned char buff[1024]; unsigned int cnt;​ file_src=fopen("D:/123.pdf","rb"); if(file_src==NULL) { printf("源文件打开失败!\n"); return -1; } /*2. 创建新文件*/ file_new=fopen("D:/456.pdf","wb"); if(file_new==NULL) { printf("新文件创建失败!\n"); return -1; } /*3. 拷贝文件*/ while(!feof(file_src)) { cnt=fread(buff,1,1024,file_src); fwrite(buff,1,cnt,file_new); } /*4. 关闭文件*/ fclose(file_new); fclose(file_src); printf("文件拷贝成功!\n"); return 0;}1.2 文件加密使用: 异或 ^ 密码类型: (1) 整型密码 (2) 字符串密码 比如: 银行提款机的密码、QQ密码 加密代码:#include <stdio.h>#include <stdlib.h>#include <string.h>int main(){ /*1. 打开源文件*/ FILE *file_src; FILE *file_new; unsigned char buff[1024]; unsigned int cnt; unsigned int password=123456; //密码数据 unsigned int data; //存放读取的数据​ file_src=fopen("D:/123.pdf","rb"); if(file_src==NULL) { printf("源文件打开失败!\n"); return -1; } /*2. 创建新文件*/ file_new=fopen("D:/456.pdf","wb"); if(file_new==NULL) { printf("新文件创建失败!\n"); return -1; } /*3. 文件加密*/ while(!feof(file_src)) { cnt=fread(&data,1,4,file_src); data=data^password;//文件数据加密 fwrite(&data,1,cnt,file_new); } /*4. 关闭文件*/ fclose(file_new); fclose(file_src); printf("文件加密成功!\n"); return 0;}解密代码:​#include <stdio.h>#include <stdlib.h>#include <string.h>int main(){ /*1. 打开源文件*/ FILE *file_src; FILE *file_new; unsigned char buff[1024]; unsigned int cnt; unsigned int password=123456; //密码数据 unsigned int data; //存放读取的数据​ file_src=fopen("D:/456.pdf","rb"); if(file_src==NULL) { printf("源文件打开失败!\n"); return -1; } /*2. 创建新文件*/ file_new=fopen("D:/789.pdf","wb"); if(file_new==NULL) { printf("新文件创建失败!\n"); return -1; } /*3. 文件加密*/ while(!feof(file_src)) { cnt=fread(&data,1,4,file_src); data=data^password;//文件数据加密 fwrite(&data,1,cnt,file_new); } /*4. 关闭文件*/ fclose(file_new); fclose(file_src); printf("文件解密成功!\n"); return 0;}​1.3 学生管理系统链表模板(未添加文件操作)​#include <stdio.h>#include <stdlib.h>#include <string.h>​//存放信息的结构体struct MyStruct{ char Name[50]; //存放姓名 int Number; //存放编号 struct MyStruct *next; //存放下一个节点的地址};​//链表相关的函数接口struct MyStruct *ListHead=NULL; //链表头struct MyStruct *CreateListHead(struct MyStruct *head);void AddrListInfo(struct MyStruct *head,struct MyStruct data);void DeleteListInfo(struct MyStruct *head,int number);void PrintListAllInfo(struct MyStruct *head);​​int main(){ struct MyStruct data1={"张三",123}; struct MyStruct data2={"李四",456}; struct MyStruct data3={"小王",789}; ListHead=CreateListHead(ListHead); //添加信息 AddrListInfo(ListHead,data1); AddrListInfo(ListHead,data2); AddrListInfo(ListHead,data3);​ //删除节点 DeleteListInfo(ListHead,123); DeleteListInfo(ListHead,789);​ //打印 PrintListAllInfo(ListHead); return 0;}​​/*函数功能: 创建链表头*/struct MyStruct *CreateListHead(struct MyStruct *head){ if(head==NULL) { head=malloc(sizeof(struct MyStruct)); head->next=NULL; } return head;}​/*函数功能: 在链表结尾添加节点*/void AddrListInfo(struct MyStruct *head,struct MyStruct data){ struct MyStruct *p=head; struct MyStruct *tmp=NULL;​ while(p->next!=NULL) { p=p->next; } tmp=malloc(sizeof(struct MyStruct)); memcpy(tmp,&data,sizeof(struct MyStruct)); p->next=tmp; tmp->next=NULL;}​/*函数功能: 根据结构体里特有的成员区分进行删除链表节点信息函数参数: int numbe 编号*/void DeleteListInfo(struct MyStruct *head,int number){ struct MyStruct *p=head; struct MyStruct *tmp=NULL;​ while(p->next!=NULL) { tmp=p; //保存上一个节点的信息 p=p->next; if(p->Number==number) { tmp->next=tmp->next->next; free(p); p=head; //链表头归位 } }}​/*函数功能: 打印所有节点信息函数参数: int numbe 编号*/void PrintListAllInfo(struct MyStruct *head){ struct MyStruct *p=head; int cnt=0; printf("\n链表全部信息如下:\n"); while(p->next!=NULL) { p=p->next; cnt++; printf("第%d个节点信息: %s,%d\n",cnt,p->Name,p->Number); }}1.4 学生管理系统模板(通过文件系统保存信息)​#include <stdio.h>#include <stdlib.h>#include <string.h>​//存放信息的结构体struct MyStruct{ char Name[50]; //存放姓名 int Number; //存放编号 struct MyStruct *next; //存放下一个节点的地址};​//链表相关的函数接口struct MyStruct *ListHead=NULL; //链表头struct MyStruct *CreateListHead(struct MyStruct *head);void AddrListInfo(struct MyStruct *head,struct MyStruct data);void DeleteListInfo(struct MyStruct *head,int number);void PrintListAllInfo(struct MyStruct *head);​//文件操作相关函数void SaveListAllInfo(struct MyStruct *head,char *path);void GetListAllInfo(struct MyStruct *head,char *path);​#if 0int main(){ struct MyStruct data1={"张三",123}; struct MyStruct data2={"李四",456}; struct MyStruct data3={"小王",789}; ListHead=CreateListHead(ListHead); AddrListInfo(ListHead,data1); AddrListInfo(ListHead,data2); AddrListInfo(ListHead,data3);​ //保存节点信息 SaveListAllInfo(ListHead,"D:/list.ini");​ //打印 PrintListAllInfo(ListHead); return 0;}#endif​#if 1int main(){ ListHead=CreateListHead(ListHead);​ //获取节点信息 GetListAllInfo(ListHead,"D:/list.ini");​ //打印 PrintListAllInfo(ListHead); return 0;}#endif​/*函数功能: 创建链表头*/struct MyStruct *CreateListHead(struct MyStruct *head){ if(head==NULL) { head=malloc(sizeof(struct MyStruct)); head->next=NULL; } return head;}​/*函数功能: 在链表结尾添加节点*/void AddrListInfo(struct MyStruct *head,struct MyStruct data){ struct MyStruct *p=head; struct MyStruct *tmp=NULL;​ while(p->next!=NULL) { p=p->next; } tmp=malloc(sizeof(struct MyStruct)); memcpy(tmp,&data,sizeof(struct MyStruct)); p->next=tmp; tmp->next=NULL;}​/*函数功能: 根据结构体里特有的成员区分进行删除链表节点信息函数参数: int numbe 编号*/void DeleteListInfo(struct MyStruct *head,int number){ struct MyStruct *p=head; struct MyStruct *tmp=NULL;​ while(p->next!=NULL) { tmp=p; //保存上一个节点的信息 p=p->next; if(p->Number==number) { tmp->next=tmp->next->next; free(p); p=head; //链表头归位 } }}​/*函数功能: 打印所有节点信息函数参数: int numbe 编号*/void PrintListAllInfo(struct MyStruct *head){ struct MyStruct *p=head; int cnt=0; printf("\n链表全部信息如下:\n"); while(p->next!=NULL) { p=p->next; cnt++; printf("第%d个节点信息: %s,%d\n",cnt,p->Name,p->Number); }}​​/*函数功能: 保存链表节点信息*/void SaveListAllInfo(struct MyStruct *head,char *path){ struct MyStruct *p=head; FILE *file; file=fopen(path,"a+b"); if(file==NULL) { printf("保存信息的文件打开失败!\n"); return ; } while(p->next!=NULL) { p=p->next; fwrite(p,1,sizeof(struct MyStruct),file); } fclose(file);}​/*函数功能: 从文件里获取链表节点信息*/void GetListAllInfo(struct MyStruct *head,char *path){ struct MyStruct *p=head; FILE *file; struct MyStruct data;​ file=fopen(path,"rb"); if(file==NULL) { printf("保存信息的文件打开失败!\n"); return; } //循环读取文件里的数据 while(!feof(file)) { fread(&data,1,sizeof(struct MyStruct),file); //读取链表节点数据 AddrListInfo(head,data); //添加链表节点 } fclose(file);}
  • [技术干货] 嵌入式开发_C语言标准文件读写接口
    在这篇文章中探讨C语言中与文件操作相关的一系列重要知识点。文章详细介绍了最常用的文件操作函数,包括fopen、fread、fwrite和fclose等,并通过几个常见的需求场景提供实例,帮助读者更好地理解和掌握这些函数的用法。知识点:文件操作函数介绍:详细解释每个文件操作函数的功能和使用方法。fopen函数:讲解如何打开文件,并处理可能出现的错误情况。fread和fwrite函数:通过实例演示如何读取和写入文件内容。fclose函数:说明如何正确关闭一个已打开的文件。常见需求例子:提供实际应用场景的例子,如创建文件、追加内容、读取特定格式的数据等。错误处理:讨论在进行文件操作时可能遇到的错误,以及如何进行有效的错误处理。1. 文件IO总结文件IO操作: 对文件系统里的文件进行: 打开、创建、读、写、关闭等运用。C语言下标准文件IO接口(函数): (1)头文件: stdio.h 输入输出函数: printf 、scanf(2)相关函数: fopen、fread、fwrite、fclose2.1 标准文件操作有两套函数:1.标准C语言下的文件操作接口。fopen系列常用于: 对普通文件的读写。2.Linux操作系统下的文件操作接口。open系列常用于: 对设备文件进行读写。 (鼠标、键盘、声卡、..)2. C语言标准文件操作接口2.1 最常用的4个函数#include <stdio.h>//打开文件FILE *fopen(const char *path, const char *mode); //读文件size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);//写文件size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);//关闭文件int fclose(FILE *fp);2.3 写函数的基本运用#include <stdio.h>#include <stdlib.h>#include <string.h>​int main(){ FILE *file; int cnt; /*1. 打开文件*/ file=fopen("D:/123.txt","a+b"); if(file==NULL) { printf("文件打开失败!\n"); return -1; } /*2. 写数据*/ cnt=fwrite("1234567890",1,10,file); /*3. 关闭文件*/ fclose(file);​ printf("cnt=%d\n",cnt); return 0;}2.4 读函数基本运用​#include <stdio.h>#include <stdlib.h>#include <string.h>​int main(){ FILE *file; int cnt; char buff[100];​ /*1. 打开文件*/ file=fopen("D:/123.txt","rb"); //malloc if(file==NULL) { printf("文件打开失败!\n"); return -1; } /*2. 写数据*/ cnt=fread(buff,1,100,file); /*3. 关闭文件*/ fclose(file); //free ​ buff[cnt]='\0'; printf("%s\n",buff); printf("cnt=%d\n",cnt); return 0;}2.5 文件指针位置偏移 (自动向后偏移)​#include <stdio.h>#include <stdlib.h>#include <string.h>int main(){ FILE *file; int cnt; char data;​ /*1. 打开文件*/ file=fopen("D:/123.txt","rb"); //malloc if(file==NULL) { printf("文件打开失败!\n"); return -1; } /*2. 读数据---验证文件指针是否可否自动向后偏移*/ cnt=fread(&data,1,1,file); printf("data=%c\n",data); cnt=fread(&data,1,1,file); printf("data=%c\n",data); cnt=fread(&data,1,1,file); printf("data=%c\n",data); cnt=fread(&data,1,1,file); printf("data=%c\n",data); cnt=fread(&data,1,1,file); printf("data=%c\n",data);​ /*3. 关闭文件*/ fclose(file); //free return 0;}2.6 设置文件指针位置​#include <stdio.h>#include <stdlib.h>#include <string.h>int main(){ FILE *file; int cnt; char data;​ /*1. 打开文件*/ file=fopen("D:/123.txt","rb"); //malloc if(file==NULL) { printf("文件打开失败!\n"); return -1; } /*2. 偏移文件指针*/ fseek(file,5,SEEK_SET);​ /*3. 读数据---验证文件指针是否可否自动向后偏移*/ cnt=fread(&data,1,1,file); printf("data=%c\n",data);​ /*4. 关闭文件*/ fclose(file); //free return 0;}2.7 以上午所学的函数,如何判断文件读完了?到文件结尾?​​#include <stdio.h>#include <stdlib.h>#include <string.h>int main(){ FILE *file; int cnt; char data;​ /*1. 打开文件*/ file=fopen("D:/123.txt","rb"); //malloc if(file==NULL) { printf("文件打开失败!\n"); return -1; } /*2. 偏移文件指针*/ fseek(file,5,SEEK_SET);​ /*3. 读数据---验证文件指针是否可否自动向后偏移*/ while(1) { cnt=fread(&data,1,1,file); if(cnt!=1)break; printf("data=%c\n",data); } /*4. 关闭文件*/ fclose(file); //free return 0;}2.8 文件读写结构体数据​//写结构体数据#include <stdio.h>#include <stdlib.h>#include <string.h>struct MyStruct{ int a; int b; char c[100];};​int main(){ FILE *file; int cnt; struct MyStruct stu={666,888,"C语言文件操作学习"};​ /*1. 打开文件*/ file=fopen("D:/123.txt","wb"); if(file==NULL) { printf("文件打开失败!\n"); return -1; }​ /*2. 读数据*/ cnt=fwrite(&stu,1,sizeof(struct MyStruct),file); printf("cnt=%d\n",cnt);​ /*3. 关闭文件*/ fclose(file); //free return 0;}​//读结构体数据#include <stdio.h>#include <stdlib.h>#include <string.h>struct MyStruct{ int a; int b; char c[100];};​int main(){ FILE *file; int cnt; struct MyStruct stu;​ /*1. 打开文件*/ file=fopen("D:/123.txt","rb"); if(file==NULL) { printf("文件打开失败!\n"); return -1; }​ /*2. 读数据*/ cnt=fread(&stu,1,sizeof(struct MyStruct),file); printf("cnt=%d\n",cnt);​ printf("%d,%d,%s\n",stu.a,stu.b,stu.c); /*3. 关闭文件*/ fclose(file); //free return 0;}2.9 文件操作的作业练习1. 学习文件基本读写使用2. 编写文件拷贝程序。 实现文件拷贝。3. 文件加密解密实现。 需要编写一个菜单。4. 完善学生管理系统。需要将所有学生信息保存到文件里,完善功能。
  • [技术干货] 嵌入式开发_C语言单向与双向链表
    本篇文章将深入探讨C语言中链表的相关知识点,包括链表的创建、单向链表、循环链表、双向链表和单向循环链表等。总结一些链表常见问题,并提供结构体数组与链表的练习题供读者实践。在下一篇文章里,将提供完整的代码示例,帮助读者更好地理解和应用这些知识点。知识点:链表创建:讲解如何定义链表节点和头节点,以及如何初始化链表。单向链表:介绍单向链表的基本概念和操作,如插入、删除、遍历等。循环链表:解释循环链表的特点和应用场景,并演示如何实现循环链表的操作。双向链表:讲解双向链表的定义和常见操作,如在链表中查找元素、插入和删除节点等。单向循环链表:介绍单向循环链表的概念和特点,以及如何进行相关操作。链表常见问题总结:列举并解答一些常见的链表问题,帮助读者避免常见的错误。结构体数组与链表的练习题:提供一系列练习题,让读者通过实际编程来巩固所学知识。1. 链表1.1 结构体对比数组特性: 内存空间连续、只能存放相同类型的数据 结构体特性: 内存空间连续、可以存放不同类型的数据#include <stdio.h>struct MyStruct{ int a; char b;};​int main(){ struct MyStruct *p; struct MyStruct data={12,'A'}; data.a=123; data.b='B';​ p=&data; printf("%d\n",p->a); printf("%c\n",p->b); return 0;}数组学生管理系统作业:作业: 学生管理系统需求: (每一个功能都是使用函数进行封装)1.实现从键盘上录入学生信息。 (姓名、性别、学号、成绩、电话号码)2.将结构体里的学生信息全部打印出来。3.实现根据学生的姓名或者学号查找学生,查找到之后打印出学生的具体信息。4.根据学生的成绩对学生信息进行排序。5.根据学号删除学生信息。1.2 单向链表的创建与运用链表: 单链表、双链表 区分: 循环和不循环链表 链表的特性: 一种数据结构的运行--->结构体。学习结构体数组(编写学生管理系统): 学生的人数问题不好确定。 链表本身就是一个结构体。单向链表的创建与运用:​#include <stdio.h>#include <stdlib.h>#include <string.h>​//定义结构体struct MyListStruct{ int a; struct MyListStruct *next; //结构体指针。存放下一个节点的地址};​//定义链表头struct MyListStruct *ListHead=NULL; ​//函数声明struct MyListStruct *CreateListHead(struct MyListStruct *head);void PintListInfo(struct MyListStruct *head);void AddListNode(struct MyListStruct *head,struct MyListStruct NewNode);void DeleteListNode(struct MyListStruct *head,int a);​​int main(){ int i; struct MyListStruct temp; int a; //1. 创建链表头 ListHead=CreateListHead(ListHead);​ //2. 添加节点 for(i=0; i<5; i++) { printf("输入新节点数据:"); scanf("%d",&temp.a); AddListNode(ListHead,temp); }​ //3. 打印所有节点数据 PintListInfo(ListHead);​ //4. 删除节点数据 printf("输入删除的节点数据值:"); scanf("%d",&a); DeleteListNode(ListHead,a);​ //5. 打印所有节点数据 PintListInfo(ListHead); return 0;}​​/*函数功能: 创建链表头返回值 : 链表头指针*/struct MyListStruct *CreateListHead(struct MyListStruct *head){ if(head==NULL) { head=malloc(sizeof(struct MyListStruct)); head->next=NULL; } return head; //返回链表头}​/*函数功能: 添加链表节点说明: 链表头一般不存放数据*/void AddListNode(struct MyListStruct *head,struct MyListStruct NewNode){ struct MyListStruct *p=head; //保存头地址 struct MyListStruct *new_p=NULL; //新的节点​ /*1. 寻找链表尾*/ while(p->next!=NULL) { p=p->next; //移动到下一个地址 } /*2. 创建新节点*/ new_p=malloc(sizeof(struct MyListStruct)); if(new_p==NULL) { printf("新节点内存申请失败!\n"); return; } /*3. 新节点赋值*/ memcpy(new_p,&NewNode,sizeof(struct MyListStruct)); //内存拷贝 new_p->next=NULL; //尾节点指向空​ /*4. 新节点添加*/ p->next=new_p;}​/*函数功能: 删除链表节点*/void DeleteListNode(struct MyListStruct *head,int a){ struct MyListStruct *p=head; //保存头地址 struct MyListStruct *tmp=NULL; /*查找数据存在的节点位置*/ while(p->next!=NULL) { tmp=p; //保存上一个节点的地址 p=p->next; if(p->a==a) //查找成功 { tmp->next=p->next; //将要删除节点的指针值赋值给删除节点的上一个节点指针域 //tmp->next=tmp->next->next; free(p); //直接释放p节点空间 //break; p=head; //重新指向链表头 } }}​​/*函数功能: 遍历所有链表信息*/void PintListInfo(struct MyListStruct *head){ int cnt=0; struct MyListStruct *p=head; printf("\n链表遍历的数据如下:\n"); while(p->next!=NULL) { p=p->next; cnt++; printf("第%d个节点的数据=%d\n",cnt,p->a); }}作业:1.看代码、理解链表的创建流程2.编写出单向链表的基础运用3.将之前的学生管理系统使用链表方式做出来​链表的功能:(1)创建链表头(2)在链表结尾添加一个节点(3)删除指定的一个链表节点(4)遍历链表,打印出所有的数据。(5)在链表的指定节点的后面添加一个节点。(6)根据链表节点里的数据对链表进行排序。 双向链表:(1)使用逆向+顺向两种遍历方式删除链表节点,目的: 提高效率。 类似于二分法查找。2. 链表问题总结动态空间分配:#include <stdio.h>#include <stdlib.h>#include <string.h>​int main(){ char *p=malloc(100); //申请动态空间。 if(p==NULL) { return -1; } strcpy(p,"1234567890"); printf("%s\n",p); return 0;}​#include <stdio.h>#include <stdlib.h>#include <string.h>​int main(){ char *p1=malloc(100); //申请动态空间。 printf("%p\n",p1); char *p2=malloc(100); //申请动态空间。 printf("%p\n",p2); char *p3=malloc(100); //申请动态空间。 printf("%p\n",p3); char *p4=malloc(100); //申请动态空间。 printf("%p\n",p4); return 0;}错误解决:1.第一个错误开始解决2.常规错误: 函数没有声明、分号每加、括号没有加、=3.括号没有对齐。3. 双向链表和循环链表3.1 单向循环链表:#include <stdio.h>#include <stdlib.h>#include <string.h>​//定义结构体struct MyListStruct{ int a; struct MyListStruct *next; //结构体指针。存放下一个节点的地址};​//定义链表头struct MyListStruct *ListHead=NULL;​//函数声明struct MyListStruct *CreateListHead(struct MyListStruct *head);void PintListInfo(struct MyListStruct *head);void AddListNode(struct MyListStruct *head,struct MyListStruct NewNode);void DeleteListNode(struct MyListStruct *head,int a);​​int main(){ int i; struct MyListStruct temp; int a; //1. 创建链表头 ListHead=CreateListHead(ListHead);​ //2. 添加节点 for(i=0; i<5; i++) { printf("输入新节点数据:"); scanf("%d",&temp.a); AddListNode(ListHead,temp); }​ //3. 打印所有节点数据 PintListInfo(ListHead);​ //4. 删除节点数据 printf("输入删除的节点数据值:"); scanf("%d",&a); DeleteListNode(ListHead,a);​ //5. 打印所有节点数据 PintListInfo(ListHead); return 0;}​​/*函数功能: 创建链表头返回值 : 链表头指针*/struct MyListStruct *CreateListHead(struct MyListStruct *head){ if(head==NULL) { head=malloc(sizeof(struct MyListStruct)); head->next=head; } return head; //返回链表头}​/*函数功能: 添加链表节点说明: 链表头一般不存放数据*/void AddListNode(struct MyListStruct *head,struct MyListStruct NewNode){ struct MyListStruct *p=head; //保存头地址 struct MyListStruct *new_p=NULL; //新的节点 /*1. 寻找链表尾*/ while(p->next!=head) { p=p->next; //移动到下一个地址 } /*2. 创建新节点*/ new_p=malloc(sizeof(struct MyListStruct)); if(new_p==NULL) { printf("新节点内存申请失败!\n"); return; } /*3. 新节点赋值*/ memcpy(new_p,&NewNode,sizeof(struct MyListStruct)); //内存拷贝 new_p->next=head; //尾节点指向头​ /*4. 新节点添加*/ p->next=new_p;}​/*函数功能: 删除链表节点*/void DeleteListNode(struct MyListStruct *head,int a){ struct MyListStruct *p=head; //保存头地址 struct MyListStruct *tmp=NULL;​ /*查找数据存在的节点位置*/ while(p->next!=head) { tmp=p; //保存上一个节点的地址 p=p->next; if(p->a==a) //查找成功 { tmp->next=p->next; //将要删除节点的指针值赋值给删除节点的上一个节点指针域 //tmp->next=tmp->next->next; free(p); //直接释放p节点空间 //break; p=head; //重新指向链表头 } }}​​/*函数功能: 遍历所有链表信息*/void PintListInfo(struct MyListStruct *head){ int cnt=0; struct MyListStruct *p=head; printf("\n链表遍历的数据如下:\n"); while(p->next!=head) { p=p->next; cnt++; printf("第%d个节点的数据=%d\n",cnt,p->a); }}3.2 双向链表​#include <stdio.h>#include <stdlib.h>#include <string.h>​//定义结构体struct MyListStruct{ int a; struct MyListStruct *next; //结构体指针。存放下一个节点的地址 struct MyListStruct *old; //结构体指针。存放上一个节点的地址};​​//定义链表头struct MyListStruct *ListHead=NULL;​//函数声明struct MyListStruct *CreateListHead(struct MyListStruct *head);void PintListInfo(struct MyListStruct *head);void AddListNode(struct MyListStruct *head,struct MyListStruct NewNode);void DeleteListNode(struct MyListStruct *head,int a);void PintListInfo_old(struct MyListStruct *head);​​int main(){ int i; struct MyListStruct temp; int a; //1. 创建链表头 ListHead=CreateListHead(ListHead);​ //2. 添加节点 for(i=0; i<5; i++) { printf("输入新节点数据:"); scanf("%d",&temp.a); AddListNode(ListHead,temp); }​ //3. 打印所有节点数据 PintListInfo(ListHead); PintListInfo_old(ListHead);​ //4. 删除节点数据 printf("输入删除的节点数据值:"); scanf("%d",&a); DeleteListNode(ListHead,a);​ //5. 打印所有节点数据 PintListInfo(ListHead); PintListInfo_old(ListHead); return 0;}​​/*函数功能: 创建链表头返回值 : 链表头指针*/struct MyListStruct *CreateListHead(struct MyListStruct *head){ if(head==NULL) { head=malloc(sizeof(struct MyListStruct)); head->next=NULL; //尾节点指向空 head->old=head; //上一个节点指向头 } return head; //返回链表头}​/*函数功能: 添加链表节点说明: 链表头一般不存放数据*/void AddListNode(struct MyListStruct *head,struct MyListStruct NewNode){ struct MyListStruct *p=head; //保存头地址 struct MyListStruct *new_p=NULL; //新的节点 /*1. 寻找链表尾*/ while(p->next!=NULL) { p=p->next; //移动到下一个地址 } /*2. 创建新节点*/ new_p=malloc(sizeof(struct MyListStruct)); if(new_p==NULL) { printf("新节点内存申请失败!\n"); return; } /*3. 新节点赋值*/ memcpy(new_p,&NewNode,sizeof(struct MyListStruct)); //内存拷贝 new_p->next=NULL; //尾节点指向NULL new_p->old=p; //保存上一个节点的地址 /*4. 新节点添加*/ p->next=new_p;}​/*函数功能: 删除链表节点顺向删除。*/void DeleteListNode(struct MyListStruct *head,int a){ struct MyListStruct *p=head; //保存头地址 struct MyListStruct *tmp=NULL;​ /*查找数据存在的节点位置*/ while(p->next!=NULL) { tmp=p; //保存上一个节点的地址 p=p->next; if(p->a==a) //查找成功 { tmp->next=p->next; //将要删除节点的指针值赋值给删除节点的上一个节点指针域 //tmp->next=tmp->next->next; p->next->old=tmp; //保存上一个节点的地址​ free(p); //直接释放p节点空间 //break; p=head; //重新指向链表头 } }}​​/*函数功能: 遍历所有链表信息从头到尾遍历*/void PintListInfo(struct MyListStruct *head){ int cnt=0; struct MyListStruct *p=head; printf("\n(顺向)链表遍历的数据如下:\n"); while(p->next!=NULL) { p=p->next; cnt++; printf("第%d个节点的数据=%d\n",cnt,p->a); }}​/*函数功能: 遍历所有链表信息从尾到头遍历*/void PintListInfo_old(struct MyListStruct *head){ int cnt=0; struct MyListStruct *p=head; printf("\n(逆向)链表遍历的数据如下:\n"); /*1. 找到链表尾*/ while(p->next!=NULL) { p=p->next; } /*2. 通过链表尾遍历链表的数据*/ while(p!=head) { cnt++; printf("第%d个节点的数据=%d\n",cnt,p->a); p=p->old; //访问上一个节点 }}