相关文章推荐
孤独的大海  ·  ubuntu 安装apt-utils - ...·  3 月前    · 
可爱的香瓜  ·  ORA-01779: ...·  7 月前    · 
欢乐的枇杷  ·  如何清洁地处理Java 8 ...·  9 月前    · 
没读研的火柴  ·  java 读取音频/视频 ...·  1 年前    · 

FFMPEG音视频开发:获取flv视频格式的时长

flv格式的视频不能像其他视频一样直接通过ffprobe输出的json获取,可以通过它的命令行输出截取时间段转换得到时间。直接上代码: QProcess process; process.setProcessChannelMode(QProcess::MergedChannels); process.start("C:/FFMPEG/ffmpeg_x86_4.2.2/bin/ffprobe.exe -i D:/test1080.flv"); process.waitForFinished(); process.waitForReadyRead(); QString str = process.readAllStandardOutput(); process.close(); //分析字符串 const char *src=str.toStdString().c_str(); char *p1=strstr(src,"Duration"); if(p1) int addr=p1-src; str = str.mid(addr+10); str =str.section(',', 0,0); qDebug("视频的时长(标准时间):%s",str.toStdString().c_str()); //解析数据 QTime t1=QTime::fromString(str); int time_ms=t1.hour()*60*60*1000+t1.minute()*60*1000+t1.second()*1000+t1.msec(); qDebug()<<"最视频的时长(MS):"<<time_ms; //反向解析回来对比 qDebug()<<"还原视频的时长(标准时间):"<<QTime(0,0,0,0).addMSecs(int(time_ms)).toString(QString::fromLatin1("HH:mm:ss.zzz")); }输出结果:视频的时长(标准时间):00:06:37.15 最视频的时长(MS): 397150 还原视频的时长(标准时间): "00:06:37.150"

QT软件开发: QProcess启动进程完成交互并获取输出

一、QProcess介绍QProcess类用于启动外部程序并与它们通信。QProcess允许将进程视为I/O设备。可以像使用qtcsocket访问网络连接一样对进程进行写入和读取。然后可以通过调用write()写入进程的标准输入,并通过调用read()、readLine()和getChar()读取标准输出。因为QProcess继承了QIODevice,所以它还可以用作QXmlReader的输入源,或者用于生成要使用QNetworkAccessManager上载的数据。当进程退出时,QProcess重新进入NotRunning状态(初始状态),并发出finished()。finished()信号提供进程的退出代码和退出状态作为参数,还可以调用exitCode()来获取最后一个完成的进程的退出代码,并调用exitStatus()来获取其退出状态。如果在任何时间点发生错误,QProcess将发出errorOccurred()信号。还可以调用error()来查找上次发生的错误类型,调用state()来查找当前进程状态。进程有两个预定义的输出通道:标准输出通道(stdout)提供常规控制台输出,标准错误通道(stderr)通常提供进程打印的错误。这些通道代表两个独立的数据流。可以通过调用setReadChannel()在它们之间切换。当前读取通道上有可用数据时,QProcess发出readyRead()。当新的标准输出数据可用时,它还发出readyReadStandardOutput(),当新的标准错误数据可用时,发出readyReadStandardError()。不必调用read()、readLine()或getChar(),可以通过调用readAllStandardOutput()或readAllStandardError()显式读取两个通道中的任何一个通道的所有数据。QProcess提供了一组函数,允许在没有事件循环的情况下使用它,方法是挂起调用线程,直到发出某些信号:waitForStarted()会一直阻塞,直到进程启动。waitForReadyRead()阻塞,直到新数据可用于当前读取通道上的读取。waitForBytesWrite()阻塞,直到一个有效负载的数据被写入进程。waitForFinished()阻塞,直到进程完成。从主线程(调用QApplication::exec()的线程)调用这些函数可能会导致用户界面冻结。下面通过几个例子介绍QProcess的使用场景和方法。1.  第一个例子调用ipconfig命令获取本地IP信息,演示如何阻塞执行命令并得到命令的输出,并解决输出的中文乱码问题。2.  第二个例子调用ffmpge获取视频文件的信息,演示如何阻塞执行命令并得到命令的输出。3.  第三个例子调用ping命令ping百度,获取网络连接情况,演示如何实时获取命令的输出。4. 第四个例子调用ffmpge命令完成视频转码,演示如何实时获取命令的输出,并写数据给进程,完成交互--->就是如何中途正常的退出ffmpge命令的执行。工程下载地址:  https://download.csdn.net/download/xiaolong1126626497/20632376二、使用示例: windows下调用ipconfig获取系统IP#include <QProcess> #include <QTextCodec> QProcess process; process.start("ipconfig"); process.waitForFinished(); process.waitForReadyRead(); QByteArray qba = process.readAll(); //解决中文乱码问题 QTextCodec* pTextCodec = QTextCodec::codecForName("System"); assert(pTextCodec != nullptr); QString str = pTextCodec->toUnicode(qba); qDebug("%s\n",str.toStdString().c_str());输出结果:Windows IP 配置 无线局域网适配器 WLAN: 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::f887:2337:ca8f:e8d5%10 IPv4 地址 . . . . . . . . . . . . : 10.0.0.4 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 10.0.0.1 以太网适配器 以太网: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 本地连接* 1: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : 无线局域网适配器 本地连接* 4: 媒体状态 . . . . . . . . . . . . : 媒体已断开连接 连接特定的 DNS 后缀 . . . . . . . : 以太网适配器 VMware Network Adapter VMnet1: 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::5c33:8a5b:a8a6:3026%19 IPv4 地址 . . . . . . . . . . . . : 192.168.112.1 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 以太网适配器 VMware Network Adapter VMnet8: 连接特定的 DNS 后缀 . . . . . . . : 本地链接 IPv6 地址. . . . . . . . : fe80::754a:6573:6487:c8f0%18 IPv4 地址 . . . . . . . . . . . . : 192.168.24.1 子网掩码 . . . . . . . . . . . . : 255.255.255.0 默认网关. . . . . . . . . . . . . : 三、使用示例: 调用ffmpge查看视频文件信息#include <QProcess> #include <QTextCodec> QProcess process; process.start("D:\\linux-share-dir\\C++_v5\\ECRS_Object\\Release\\ffprobe.exe -v quiet -of json -i D:/123.mp4 -show_streams "); process.waitForFinished(); process.waitForReadyRead(); QByteArray qba = process.readAll(); QTextCodec* pTextCodec = QTextCodec::codecForName("System"); assert(pTextCodec != nullptr); QString str = pTextCodec->toUnicode(qba); qDebug("%s\n",str.toStdString().c_str());输出结果:{ "streams": [ "index": 0, "codec_name": "aac", "codec_long_name": "AAC (Advanced Audio Coding)", "profile": "LC", "codec_type": "audio", "codec_time_base": "1/88200", "codec_tag_string": "mp4a", "codec_tag": "0x6134706d", "sample_fmt": "fltp", "sample_rate": "88200", "channels": 2, "channel_layout": "stereo", "bits_per_sample": 0, "r_frame_rate": "0/0", "avg_frame_rate": "0/0", "time_base": "1/44100", "start_pts": 0, "start_time": "0.000000", "duration_ts": 4142070, "duration": "93.924490", "bit_rate": "127916", "max_bit_rate": "132760", "nb_frames": "4045", "disposition": { "default": 1, "dub": 0, "original": 0, "comment": 0, "lyrics": 0, "karaoke": 0, "forced": 0, "hearing_impaired": 0, "visual_impaired": 0, "clean_effects": 0, "attached_pic": 0, "timed_thumbnails": 0 "tags": { "creation_time": "2015-04-30T02:43:22.000000Z", "language": "und", "handler_name": "GPAC ISO Audio Handler" "index": 1, "codec_name": "h264", "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", "profile": "Main", "codec_type": "video", "codec_time_base": "2349/70450", "codec_tag_string": "avc1", "codec_tag": "0x31637661", "width": 1280, "height": 720, "coded_width": 1280, "coded_height": 720, "has_b_frames": 0, "sample_aspect_ratio": "1:1", "display_aspect_ratio": "16:9", "pix_fmt": "yuv420p", "level": 51, "chroma_location": "left", "refs": 1, "is_avc": "true", "nal_length_size": "4", "r_frame_rate": "25/1", "avg_frame_rate": "35225/2349", "time_base": "1/30000", "start_pts": 0, "start_time": "0.000000", "duration_ts": 2818800, "duration": "93.960000", "bit_rate": "581978", "bits_per_raw_sample": "8", "nb_frames": "1409", "disposition": { "default": 1, "dub": 0, "original": 0, "comment": 0, "lyrics": 0, "karaoke": 0, "forced": 0, "hearing_impaired": 0, "visual_impaired": 0, "clean_effects": 0, "attached_pic": 0, "timed_thumbnails": 0 "tags": { "creation_time": "2015-04-30T02:43:23.000000Z", "language": "und", "handler_name": "GPAC ISO Video Handler" }四、使用示例: 调用ping命令获取实时输出想要实时获取process的标准输出,需要关联readyReadStandardOutput信号;并且process需要动态的new出来。4.1 做了个简单的ui界面4.2 cpp文件代码#ifndef WIDGET_H #define WIDGET_H #include <QProcess> #include <QTextCodec> #include <QWidget> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACE class Widget : public QWidget Q_OBJECT public: Widget(QWidget *parent = nullptr); ~Widget(); QProcess *process; private slots: void slot_readyRead(); void on_pushButton_start_clicked(); void on_pushButton_stop_clicked(); void on_pushButton_exit_clicked(); private: Ui::Widget *ui; #endif // WIDGET_H4.3 .h文件代码#ifndef WIDGET_H #define WIDGET_H #include <QProcess> #include <QTextCodec> #include <QWidget> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACE class Widget : public QWidget Q_OBJECT public: Widget(QWidget *parent = nullptr); ~Widget(); QProcess *process; private slots: void slot_readyRead(); void on_pushButton_start_clicked(); void on_pushButton_stop_clicked(); void on_pushButton_exit_clicked(); private: Ui::Widget *ui; #endif // WIDGET_H4.4 运行效果五、使用示例:  调用ffmpge命令完成视频转码下面的例子演示如何调用ffmpge命令完成视频转码,并且实时获取转码的进度输出,解析之后可以制作进度条界面,还可以向进程写命令进去(写q可以中断ffmpge的执行,正常保存退出),与ffmpge进程交互。5.1 UI界面5.2 widget.h 代码#ifndef WIDGET_H #define WIDGET_H #include <QProcess> #include <QTextCodec> #include <QWidget> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACE class Widget : public QWidget Q_OBJECT public: Widget(QWidget *parent = nullptr); ~Widget(); QProcess *process; private slots: void slot_readyRead(); void on_pushButton_start_clicked(); void on_pushButton_stop_clicked(); void on_pushButton_exit_clicked(); private: Ui::Widget *ui; #endif // WIDGET_H5.3 widget.cpp代码#include "widget.h" #include "ui_widget.h" #include <QDebug> Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) ui->setupUi(this); process=new QProcess(this); QObject::connect(process,SIGNAL(readyReadStandardOutput()),this, SLOT(slot_readyRead())); process->setProcessChannelMode(QProcess::MergedChannels); Widget::~Widget() delete ui; 工程: untitled1 日期: 2021-07-28 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 启动转码 void Widget::on_pushButton_start_clicked() //process->start("C:/FFMPEG/ffmpeg_x86_4.2.2/bin/ffmpeg.exe -i \"D:/test1080.flv\" -y -qscale 0 -vcodec libx264 -acodec aac -ac 1 -ar 22050 -b:v 0 -s 1280x720 -r 25 \"D:/linux-share-dir/video_file/test/out.mp4\""); process->start(ui->lineEdit_start->text()); 工程: untitled1 日期: 2021-07-28 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 有数据可读 void Widget::slot_readyRead() QByteArray qba = process->readAllStandardOutput(); QTextCodec* pTextCodec = QTextCodec::codecForName("System"); assert(pTextCodec != nullptr); QString str = pTextCodec->toUnicode(qba); ui->plainTextEdit->insertPlainText(str); 工程: untitled1 日期: 2021-07-28 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 写数据 void Widget::on_pushButton_stop_clicked() process->write(ui->lineEdit_write->text().toLocal8Bit()); 工程: untitled1 日期: 2021-07-28 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 停止命令 void Widget::on_pushButton_exit_clicked() process->close(); process->waitForFinished(); }5.4 程序运行效果​

Linux系统开发: 命令进阶学习(一)

一、解压缩命令介绍Linux下最常用的打包程序是tar命令,使用tar打出来的包我们常称为tar包,tar包文件的命令通常都是以.tar结尾的,生成tar包后,就可以用其它的程序来进行压缩了。1.1.1 tar命令介绍        功能:tar是一个压缩解压工具。利用tar,用户可以为某一特定文件创建档案(备份文件),也可以在档案中改变文件,或者向档案中加入新的文件。tar最初被用来在磁带上创建档案,现在,用户可以在任何设备上创建档案,如软盘。利用tar命令,可以把一大堆的文件和目录全部打包成一个文件,这对于备份文件或将几个文件组合成为一个文件以便于网络传输是非常有用的。Linux上的tar是GNU版本的。语法:tar  [主选项+辅选项]  <目标文档>  <源文件或者目录>使用该命令时,主选项是必须要有的,它告诉tar要做什么事情,辅选项是辅助使用的,可以选用。参数:c 创建新的档案文件。如果用户想备份一个目录或是一些文件,就要选择这个选项。r 把要存档的文件追加到档案文件的未尾。例如用户已经作好备份文件,又发现还有一个目录或是一些文件忘记备份了,这时可以使用该选项,将忘记的目录或文件追加到备份文件中。t 列出档案文件的内容,查看已经备份了哪些文件。u 更新文件。就是说,用新增的文件取代原备份文件,如果在备份文件中找不到要更新的文件,则把它追加到备份文件的最后。x 从档案文件中释放文件。注意:c/x/t 仅能存在一个!不可同时存在!辅助选项:b 该选项是为磁带机设定的。其后跟一数字,用来说明区块的大小,系统预设值为20(20*512 bytes)。f 使用档案文件或设备,这个选项通常是必选的。请留意,在 f 之后要立即接档名喔!不要再加参数!k 保存已经存在的文件。例如我们把某个文件还原,在还原的过程中,遇到相同的文件,不会进行覆盖。m 在还原文件时,把所有文件的修改时间设定为现在。M 创建多卷的档案文件,以便在几个磁盘中存放。v 详细报告tar处理的文件信息。如无此选项,tar不报告文件信息。w 每一步都要求确认。z 用gzip来压缩/解压缩文件,后缀名为.gz,加上该选项后可以将档案文件进行压缩,但还原时也一定要使用该选项进行解压缩。j 用bzip2来压缩/解压缩文件,后缀名为.bz2,加上该选项后可以将档案文件进行压缩,但还原时也一定要使用该选项进行解压缩。1.1.2 tar命令解压/压缩使用范例将/test目录下的所有文件打包为test.tar文件。注意:如果打包的文件或者目录是绝对路径,可能会出现提示:tar: 从成员名中删除开头的“/”在参数中加上-P即可消除。示例:解压打包的.tar文件更新文件就是说,用新增的文件取代原备份文件,如果在备份文件中找不到要更新的文件,则把它追加到备份文件的最后。列出已经打包的文件,可以用于查看已经备份了哪些文件。​使用gzip来压缩/解压缩文件使用bzip2来压缩/解压缩文件1.1.3 ZIP格式压缩/解压linux下提供了zip和unzip程序对ZIP格式压缩包进行处理,zip是压缩程序,unzip是解压程序。它们的参数选项很多,下面只做简单介绍。将所有.jpg的文件压缩成一个zip包将all.zip中的所有文件解压出来常用参数:-r 递 归处理,将指定目录下的所有文件和子目录一并处理。压缩指定目录下的所有文件二、磁盘操作相关命令介绍1.2.1 fdisk命令:磁盘分区Linux下的fdisk功能是极其强大的,用它可以划分出最复杂的分区。查看设备的详细信息。# fdisk -l在console上输入fdisk/dev/sda,可进入分割硬盘模式。输入m显示所有命令列示。输入p显示硬盘分割情形。输入a设定硬盘启动区。输入n设定新的硬盘分割区。输入e硬盘为[延伸]分割区(extend)。输入p硬盘为[主要]分割区(primary)。输入t改变硬盘分割区属性。输入d删除硬盘分割区属性。输入q结束不存入硬盘分割区属性。输入w结束并写入硬盘分割区属性。1.2.2 dd命令:磁盘备份命令dd是Linux/UNIX 下的一个非常有用的命令,作用是将一个指定文件拷贝到磁盘的指定块。可以用于磁盘备份、程序烧写等应用。基本语法:  dd iflag=dsync oflag=dsync if=<输入文件> of=<输出的文件> seek=<跳过的块数量>dd命令的主要选项:数字:b=512 ,k=1024运用实例修复硬盘当硬盘较长时间(比如1,2年)放置不使用后,磁盘上会产生magnetic flux point(磁通点)。当磁头读到这些区域时会遇到困难,并可能导致I/O错误。当这种情况影响到硬盘的第一个扇区时,可能导致硬盘报废。上边的命令有可能使这些数据起死回生。且这个过程是安全,高效的。清除磁盘数据利用随机的数据填充硬盘,在某些必要的场合可以用来销毁数据。执行此操作以后,/dev/sdb将无法挂载,创建和拷贝操作无法执行。 其中的/dev/urandom是产生随机数的文件。也可以直接获取随机数据:其中bs表示每一个块的大小是1024kb。count就表示块数量。 加起来就会拷贝(1024*2)kb的数据磁盘备份将磁盘数据备份到当前目录的disk.img文件。备份之后可以使用压缩软件打开。如果需要恢复直接将参数变换个位置。从光盘拷贝iso镜像拷贝光盘数据到root文件夹下,并保存为cd.iso文件。程序烧写将123.bin文件拷贝到/dev/sdb设备中,seek表示跳过1057个块之后再进行拷贝。1.2.3 mount命令:挂载硬盘或镜像mount命令用于挂载磁盘分区或者网络文件系统。语法:mount -t [文件系统类型] [将要挂载的设备] [-o 选项] [挂载的目标目录]注: 通过-t这个参数,我们来指定文件系统的类型,一般的情况下不指定也能自动识加。-t 后面跟 ext3 、ext2 、reiserfs、vfat 、ntfs、nfs 等;可以通过查看mount的帮助文档进行查看详细信息。-o  主要用来描述设备或档案的挂接方式。常用的参数有:loop:用来把一个文件当成硬盘分区挂接上系统ro:采用只读方式挂接设备rw:采用读写方式挂接设备iocharset:指定访问文件系统所用字符集运用实例挂载SD卡分区到指定目录将sdb2设备挂载到/test目录下。如果挂载之后需要取消,可以使用umount命令。挂载光盘映像文件到指定目录将123.iso文件当成硬盘挂载到/test目录下。将一个目录挂载到另一个目录下相当于使用ln建立链接一样效果。使用mount命令挂载NFS网络文件系统如果挂载提示设备资源上锁,可以使用下面方式进行挂载:其中192.168.11.123表示服务器的IP地址,/work表示NFS服务器共享的目录。/test/表示本地挂载的目录。搭建NFS服务器方法首先设置将要共享的路径。编辑/etc/exports文件,示例:重启NFS服务器1.3 文件搜索与修改1.3.1 find命令:搜索文件命令功能:用于在文件树种查找文件,并作出相应的处理。语法:find [查找的路径] [选项] [表达式]命令选项根据文件名查找文件在/work目录下查找名称为123.c的文件。根据通配符进行查找文件在/work目录下查找以.c为后缀的文件。注意:该查找方式区分大小写。不区分大小写的方式:同时查找多个文件在/work目录下查找以.c、.txt、.h为后缀的文件。 主要参数是-o。匹配路径或者文件在/work目录下查找包含mplayer字符的文件和目录。根据正则表达式查找否定参数在/work目录下查找不是以.c为后缀的文件。根据文件类型搜索在/work目录下查找类型为f的普通文件。常用的类型:f 普通文件、 l 符号连接、 d 目录、 c 字符设备、 b 块设备、 s 套接字、 p  Fifo管道文件根据文件大小进行匹配在/work目录下查找类型为f的普通文件,并且文件的大小为200k。文件大小单元:b —— 块(512字节)c —— 字节w —— 字(2字节)k —— K字节M —— 兆字节G —— G字节常用大小搜索方式:根据文件时间戳进行搜索UNIX/Linux文件系统每个文件都有三种时间戳:访问时间(-atime/天,-amin/分钟):用户最近一次访问时间。修改时间(-mtime/天,-mmin/分钟):文件最后一次修改时间。变化时间(-ctime/天,-cmin/分钟):文件数据元(例如权限等)最后一次修改时间。 查找并删除指定文件在work目录下查找以.c为后缀的文件,并将其删除。根据权限进行查找在/work目录下搜索出权限为777的文件。借助-exec选项与其他命令结合使用查找并删除指定的文件在work目录下查找以.txt为后缀的文件,将其全部删除。其中-ok和-exec行为一样,不过-ok会给出提示,是否执行相应的操作,而-exec不会提示,直接执行。{} 用于与-exec和-ok选项结合使用来匹配所有文件。查找并拷贝文件在work目录下查找以.txt为后缀的文件,将其全部拷贝到/opt目录下。执行多条命令的方法因为单行命令中-exec参数中无法使用多个命令,我们可以将命令写成脚本,然后使用-exec进行调用。{}是匹配所有文件,传递给脚本之后,在脚本中使用$1获取传入的参数信息。脚本文件中的代码示例: echo $11.3.2 grep命令:文本搜索工具Linux系统中grep命令是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。grep全称是Global Regular Expression Print,表示全局正则表达式版本,它的使用权限是所有用户。正则表达式主要参数:使用实例搜索文本并添加文本语法:grep [选项] [文件]在/work/test.txt文件中搜索12345这个字符串数据,如果有就不执行||后面的代码,如果没有,就执行||后面的代码。-q在这里是将grep变为类似于if的效果。同时搜索多个文件主要选项:在work目录下以.c后缀的文件中搜索A这个数据,如果搜索成功,会将A出现的行打印到屏幕上。搜索输出行与行号在123.c文件中搜索data数据,搜索成功后将data所在的行与行号全部打印出来。大小写敏感输出所有含有data或DAT的字符串的行。1.3.3 sed命令:在线编辑器sed 是一种在线编辑器,它一次处理一行内容。处理时,把当前处理的行存储在临时缓冲区中,称为“模式空间”(pattern space),接着用sed命令处理缓冲区中的内容,处理完成后,把缓冲区的内容送往屏幕。接着处理下一行,这样不断重复,直到文件末尾。文件内容并没有改变,除非你使用重定向存储输出。Sed主要用来自动编辑一个或多个文件;简化对文件的反复操作;编写转换程序等。Sed本质上是一个编辑器,但是它是非交互式的,这点与VIM不同;同时它又是面向字符流的,输入的字符流经过Sed的处理后输出。这两个特性使得Sed成为命令行下面非常有用的一个处理工具。sed的处理流程,简化后是这样的:读入新的一行内容到缓存空间;从指定的操作指令中取出第一条指令,判断是否匹配pattern;如果不匹配,则忽略后续的编辑命令,回到第2步继续取出下一条指令;如果匹配,则针对缓存的行执行后续的编辑命令;完成后,回到第2步继续取出下一条指令;当所有指令都应用之后,输出缓存行的内容;回到第1步继续读入下一行内容;当所有行都处理完之后,结束;语法:sed [-hnV][-e<script>][-f<script文件>][文本文件]参数说明:-e<script>或--expression=<script> 以选项中指定的script来处理输入的文本文件。-f<script文件>或--file=<script文件> 以选项中指定的script文件来处理输入的文本文件。-h或--help 显示帮助。-n或--quiet或--silent 仅显示script处理后的结果。-V或--version 显示版本信息。动作说明:字符集运行实例删除指定行删除123.c文件的第2行。其中d表示删除命令,2表示行号。123.c是处理的文件。修改成功会将结果打印到终端。注意:以上代码修改是不会改变源文件的代码。直接修改源文件示例:替换指定的数据将123.c文件中全部的std字符替换为inc字符。如果没有g标记,则只有每行第一个匹配的std被替换成inc。查找并追加数据将123.c文件中192.168.1.1数据的后面加上:8080。替换之后的效果为: 192.168.1.1:8080&符号表示替换换字符串中被找到的部份。#号分隔符 “#”在这里表示是分隔符,代替了默认的“/”分隔符。表示把所有1234替换成5678。替换所有文件将当前目录下所有以.c为后缀文件中的8080替换为88888。这样可以大大提高我们的工作效率。1.3.4 awk命令:文本分析工具awk是一个强大的文本分析工具,相对于grep的查找,sed的编辑,awk在其对数据分析并生成报告时,显得尤为强大。简单来说awk就是把文件逐行的读入,以空格为默认分隔符将每行切片,切开的部分再进行各种分析处理。awk有3个不同版本: awk、nawk和gawk,未作特别说明,一般指gawk,gawk 是 AWK 的 GNU 版本。AWK 拥有自己的语言: AWK 程序设计语言。它允许创建简短的程序,这些程序读取输入文件、为数据排序、处理数据、对输入执行计算以及生成报表,还有无数其他的功能。语法:awk '{pattern + action}' {filenames}其中 pattern 表示 AWK 在数据中查找的内容,而 action 是在找到匹配内容时所执行的一系列命令。花括号({})不需要在程序中始终出现,但它们用于根据特定的模式对一系列指令进行分组。 pattern就是要表示的正则表达式,用斜杠括起来。awk语言的最基本功能是在文件或者字符串中基于指定规则浏览和抽取信息,awk抽取信息后,才能进行其他文本操作。完整的awk脚本通常用来格式化文本文件中的信息。通常,awk是以文件的一行为处理单位的。awk每接收文件的一行,然后执行相应的命令,来处理文本。1.4 网络相关命令介绍1.4.1 ifconfig命令:设置网卡IP地址功能ifconfig用于查看和更改网络接口的地址和参数,包括IP地址、网络掩码、广播地址,使用权限是超级用户。语法:fconfig -interface [options] address主要参数应用说明ifconfig是用来设置和配置网卡的命令行工具。为了手工配置网络,这是一个必须掌握的命令。使用该命令的好处是无须重新启动机器。要赋给eth0接口IP地址207.164.186.2,并且马上激活它,使用下面命令:该命令的作用是设置网卡eth0的IP地址、网络掩码和网络的本地广播地址。若运行不带任何参数的ifconfig命令,这个命令将显示机器所有激活接口的信息。带有“-a”参数的命令则显示所有接口的信息,包括没有激活的接口。注意,用ifconfig命令配置的网络设备参数,机器重新启动以后将会丢失。查看网卡的IP地址信息关闭与启动网卡修改网卡MAC地址在一张网卡上绑定多个IP地址在Linux下,可以使用ifconfig方便地绑定多个IP地址到一张网卡。例如,eth0接口的原有IP地址为192.168.0 .254,可以执行下面命令:1.4.2 ping命令功能:ping检测主机网络接口状态,使用权限是所有用户。语法:ping [-dfnqrRv][-c][-i][-I][-l][-p][-s][-t] IP地址主要参数ping命令是使用最多的网络指令,通常我们使用它检测网络是否连通,它使用ICMP协议。但是有时会有这样的情况,我们可以浏览器查看一个网页,但是却无法ping通,这是因为一些网站处于安全考虑安装了防火墙。使用实例1.4.3 网卡启动与关闭除了使用ifconfig配置之外,也可以使用ifup、ifdown命令来实现。1.4.4 关闭防火墙在红帽系统中,可以直接使用setup命令关闭防火墙。通过命令方式关闭防火墙:

音视频开发: ffmpeg保持原视频比例增加黑色背景(画黑边)

命令示例:C:/FFMPEG/ffmpeg_x86_4.2.2/bin/ffmpeg.exe -i D:/123.mp4 -vf "scale=100:100,pad=500:500:0:300:black" D:/linux-share-dir/video_file/output.mp4参数解释:500:500 画布的尺寸  .                                         (w:h)100:100 视频帧在画布里的尺寸--按比例缩放的. (w:h)0:300   视频帧在画面上的位置.                            (x:y)black   画布的背景颜色如果有多个视频文件加黑边之后,后续还需要合成一个视频,可以在输出的时候统一重新采样:C:/FFMPEG/ffmpeg_x86_4.2.2/bin/ffmpeg.exe -i D:/Produce.mpg -vf "scale=100:100,pad=500:500:0:300:black" -y -qscale 0 -vcodec libx264 -acodec aac -ac 1 -ar 22050 -s 500*500 -r 30 D:/linux-share-dir/video_file/test/output3.mp4分辨率、帧率、音频采样率不一致是无法正常合并视频的。

QT软件开发:基于libVLC内核设计视频播放器

一、环境介绍操作系统: win10 64位QT版本:  QT5.12.6编译器:  MinGW 32libvlc版本:  3.0.12完整工程下载地址(下载即可编译运行): VLC_Core_VideoPlayer.zip-QT文档类资源-CSDN下载二、播放器运行效果与功能介绍播放器的功能介绍:1. 图像旋转播放(90°、0°、180°、360°)2. 视频画面截图保存到本地3. 倍速切换、速度切换不会改变声音音色4. 音量调整,静音切换5. 快进、快退支持6. 点击按钮加载文件、鼠标拖拽文件进行播放7. 重播按钮支持8. 暂停与继续切换9. 单帧切换、前一帧、后一帧10. 流媒体播放,输入流媒体链接11. 进度条显示,支持鼠标点击任意跳转到点击位置12. 播放时间实时更新显示其他功能可自己增加.....三、libVLC介绍3.1 下载VLC的SDK文件VLC官网地址: VLC: Official site - Free multimedia solutions for all OS! - VideoLAN所有的VLC版本下载地址: http://ftp.heanet.ie/pub/videolan/vlc3.0.12的SDK下载地址: Index of /pub/videolan/vlc/last/win32 3.2 libvlc介绍来至官网的介绍:libVLC是核心引擎,也是VLC 媒体播放器所基于的多媒体框架的接口。libVLC被模块化为数百个插件,可以在运行时加载。这种架构为开发人员(VLC 开发人员和使用该库的开发人员)提供了极大的灵活性。它允许开发人员使用VLC功能创建范围广泛的多媒体应用程序。播放每种媒体文件格式、每种编解码器和每种流媒体协议。在各种平台上运行,从桌面(Windows、Linux、Mac)到移动设备(Android、iOS)和电视。每个平台上的硬件和高效解码,高达 8K。远程文件系统(SMB、FTP、SFTP、NFS...)和服务器(UPnP、DLNA)的网络浏览。使用菜单导航播放音频 CD、DVD 和蓝光。支持 HDR,包括 SDR 流的色调映射。具有 SPDIF 和 HDMI 的音频直通,包括音频高清编解码器,如 DD+、TrueHD 或 DTS-HD。支持视频和音频过滤器。支持 360 度视频和 3D 音频播放,包括 Ambisonics。能够投射和流式传输到远程渲染器,如 Chromecast 和 UPnP 渲染器。libVLC是一个 C 库,可以嵌入到您自己的应用程序中。它适用于大多数流行的操作系统平台,包括移动设备和桌面设备。它在LGPL2.1 许可下。libVLC版本控制本质上与 VLC 应用程序版本控制相关联。libVLC 当前稳定的主要版本是 version 3,预览/开发版本是 version 4。libVLC 的各种编程语言绑定可用于在您选择的生态系统中无缝使用该库。VideoLAN 绑定用于 C++ 的libvlcpp用于 Apple 平台的VLCKit,使用 Objective-C/Swift。用于 Android 平台的libvlcjni,使用 Java/Kotlin。LibVLCSharp适用于大多数操作系统平台,使用 .NET/Mono。社区绑定vlcj用于使用 Java 的桌面平台。python-vlc用于使用 Python 的桌面平台。vlc-rs使用 Rust 编程语言。libvlc-go使用 Go 编程语言。四、播放器程序设计#include "widget.h" #include "ui_widget.h" Widget* Widget::pThis = nullptr; Widget::Widget(QString filename,QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) ui->setupUi(this); pThis=this; //设置窗口的标题名称 this->setWindowTitle("VLC内核视频播放器"); //获取VLC版本号 qDebug()<<"VLC内核版本:"<<libvlc_get_version(); qDebug()<<"编译LIBVLC的编译器版本:"<<libvlc_get_compiler(); //加载样式表 SetStyle(":/resource/VideoPlayer.qss"); //读取配置文件 ReadConfig(); //播放器初始化 VLC_InitConfig(); //UI界面相关初始化 UI_InitConfig(); //如果构造函数传入的视频文件就直接加载 if(!filename.isEmpty()) load_video_file(0,filename); //拖放文件需要使用 setAcceptDrops(true); Widget::~Widget() delete ui; libvlc_release(vlc_base); //减少libvlc实例的引用计数,并销毁它 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 加载样式表 void Widget::SetStyle(const QString &qssFile) QFile file(qssFile); if (file.open(QFile::ReadOnly)) QByteArray qss=file.readAll(); qApp->setStyleSheet(qss); file.close(); 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: UI界面初始化 void Widget::UI_InitConfig() //音量滑块范围设置 ui->horizontalSlider_AudioValue->setMaximum(100); ui->horizontalSlider_AudioValue->setMinimum(0); //安装事件过滤器 ui->horizontalSlider_AudioValue->installEventFilter(this); //播放速度设置 ui->MediaSpeedBtn->setCheckable(true); m_TimeSpeedGrp = new QActionGroup(this); QStringList strSpeedItems; strSpeedItems << tr("0.5X") << tr("1.0X") << tr("1.5X") << tr("2.0X") << tr("4.0X"); float speeds[] = {0.5,1.0,1.5,2.0,4.0}; for (int i = 0; i < strSpeedItems.size(); i++) QAction *pSpeedItem = m_SpeedMenu.addAction(strSpeedItems.at(i)); pSpeedItem->setData(QVariant::fromValue(speeds[i])); pSpeedItem->setCheckable(true); m_TimeSpeedGrp->addAction(pSpeedItem); if (i == 1) pSpeedItem->setChecked(true); connect(m_TimeSpeedGrp, SIGNAL(triggered(QAction *)), this, SLOT(slot_onSetTimeSpeed(QAction *))); //图像的旋转方向 m_RotateGrp = new QActionGroup(this); QStringList strDegrees; strDegrees << tr("0~") << tr("90~") << tr("180~") << tr("270~"); int Degrees[] = {0, 90, 180, 270 }; for (int i = 0; i < strDegrees.size(); i++) QAction *pItem = m_RotateMenu.addAction(strDegrees.at(i)); pItem->setData(QVariant::fromValue(Degrees[i])); pItem->setCheckable(true); m_RotateGrp->addAction(pItem); if (i == 0) pItem->setChecked(true); connect(m_RotateGrp, SIGNAL(triggered(QAction *)), this, SLOT(slot_onMediaRotate(QAction *))); //功能设置 // ui->toolButton_set->setCheckable(true); m_ConfigurationFunctionGrp = new QActionGroup(this); QAction *pToKeyFrame = m_ConfigurationFunctionMenu.addAction(tr("保留1")); QAction *pAppInfo = m_ConfigurationFunctionMenu.addAction(tr("保留2")); pToKeyFrame->setData(MENU_TO_KeyFrame); //保存到剪切板 pAppInfo->setData(MENU_APP_INFO); //保存到文件 m_ConfigurationFunctionGrp->addAction(pToKeyFrame); //添加到分组 m_ConfigurationFunctionGrp->addAction(pAppInfo); //添加到分组 connect(m_ConfigurationFunctionGrp, SIGNAL(triggered(QAction *)), this, SLOT(slot_onConfigurationFunction(QAction *))); //截图保存 // ui->MediaSnapshotBtn->setCheckable(true); m_SnapshotGrp = new QActionGroup(this); QAction *pClipboard = m_SnapshotMenu.addAction(tr("截图保存到剪切板")); QAction *pFileDirectory = m_SnapshotMenu.addAction(tr("截图保存到文件")); pClipboard->setData(MENU_COPY_CLIPBOARD); //保存到剪切板 pFileDirectory->setData(MENU_SAVE_FILE_SYSTEM); //保存到文件 m_SnapshotGrp->addAction(pClipboard); //添加到分组 m_SnapshotGrp->addAction(pFileDirectory); //添加到分组 connect(m_SnapshotGrp, SIGNAL(triggered(QAction *)), this, SLOT(slot_onMediaSnapshot(QAction *))); //安装事件监听器 事件筛选器是接收发送到此对象的所有事件的对象 ui->horizontalSlider_PlayPosition->installEventFilter(this); ui->widget_videoDisplay->installEventFilter(this); //状态信息初始化 MediaInfo.state=MEDIA_NOLOAD; //工具提示信息 ui->toolButton_load->setToolTip(tr("加载视频,也可以直接将视频文件拖拽到窗口")); ui->MediaPrevBtn->setToolTip(tr("快退5秒")); ui->MediaPlayBtn->setToolTip(tr("快进5秒")); ui->MediaPauseBtn->setToolTip(tr("暂停/继续")); ui->MediaSpeedBtn->setToolTip(tr("倍速选择")); ui->MediaResetBtn->setToolTip(tr("从头播放")); ui->MediaSnapshotBtn->setToolTip(tr("截图")); ui->MediaRotateBtn->setToolTip(tr("画面旋转")); ui->ReverseFrameBtn->setToolTip(tr("上一帧")); ui->ForwardFrameBtn->setToolTip(tr("下一帧")); ui->VolumeBtn->setToolTip(tr("静音切换")); ui->toolButton_link->setToolTip(tr("流媒体链接")); //播放进度条滑块初始化 connect(ui->horizontalSlider_PlayPosition, SIGNAL(sliderMoved(int)), SLOT(seek(int))); connect(ui->horizontalSlider_PlayPosition, SIGNAL(sliderPressed()), SLOT(seek())); this->setMouseTracking(true); 工程: ECRS_VideoPlayer 日期: 2021-03-15 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: MediaPlayer初始化 void Widget::VLC_InitConfig(void) const char *tempArg = ""; // tempArg = "--demux=h264"; const char *vlc_args[9] = {"-I", "dummy", "--no-osd", "--no-stats", "--ignore-config", "--no-video-on-top", "--no-video-title-show", "--no-snapshot-preview", tempArg}; //VLC相关的初始化 //vlc_base=libvlc_new(0, nullptr); //创建并初始化libvlc实例 vlc_base=libvlc_new(sizeof(vlc_args) / sizeof(vlc_args[0]), vlc_args); if(!vlc_base) qDebug()<<"libvlc_new 执行错误."; //图像缓冲区申请空间 ctx.pixels = new uchar[MAX_WIDTH * MAX_HEIGHT * 4]; 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 更新播放进度 void Widget::updateSliderPosition(qint64 value) //设置进度条的时间 ui->horizontalSlider_PlayPosition->setValue(int(value)); //设置右上角的时间 ui->label_current_Time->setText(QTime(0, 0, 0,0).addMSecs(int(value)).toString(QString::fromLatin1("HH:mm:ss:zzz"))); 工程: ECRS_VideoPlayer 日期: 2021-03-15 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 媒体总时间 void Widget::media_durationChanged(qint64 value) //最大值最小值 ui->horizontalSlider_PlayPosition->setMinimum(int(0)); ui->horizontalSlider_PlayPosition->setMaximum(int(value)); ui->label_Total_Time->setText(QTime(0, 0, 0,0).addMSecs(int(value)).toString(QString::fromLatin1("HH:mm:ss:zzz"))); 工程: QtVLC_Player 日期: 2021-03-24 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: VLC的事件回调 void Widget::vlcEvents(const libvlc_event_t *ev, void *param) libvlc_time_t play_ms_pos=0; switch (ev->type){ case libvlc_MediaPlayerTimeChanged: //获取当前视频的播放位置 play_ms_pos=libvlc_media_player_get_time(pThis->vlc_mediaPlayer); //设置右上角的时间 pThis->ui->label_current_Time->setText(QTime(0, 0, 0,0).addMSecs(int(play_ms_pos)).toString(QString::fromLatin1("HH:mm:ss:zzz"))); //设置进度条 pThis->ui->horizontalSlider_PlayPosition->setValue(int(play_ms_pos)); break; case libvlc_MediaPlayerEndReached: qDebug() << "VLC播放完毕."; break; case libvlc_MediaPlayerStopped: qDebug() << "VLC停止播放"; //获取当前视频的播放位置 play_ms_pos=libvlc_media_player_get_time(pThis->vlc_mediaPlayer); //设置右上角的时间 pThis->ui->label_current_Time->setText(QTime(0, 0, 0,0).addMSecs(int(play_ms_pos)).toString(QString::fromLatin1("HH:mm:ss:zzz"))); //设置进度条 pThis->ui->horizontalSlider_PlayPosition->setValue(int( pThis->ui->horizontalSlider_PlayPosition->maximum())); break; case libvlc_MediaPlayerPlaying: qDebug() << "VLC开始播放"; break; case libvlc_MediaPlayerPaused: qDebug() << "VLC暂停播放"; break; case libvlc_MediaParsedChanged: qDebug() << "获取到元数据"; int state=ev->u.media_parsed_changed.new_status; if(libvlc_media_parsed_status_done==state) qDebug()<<"媒体元数据:"<<libvlc_media_get_meta(pThis->vlc_media,libvlc_meta_Date); break; void Widget::display(void *opaque, void *picture) (void)opaque; void *Widget::vlc_lock(void *opaque, void **planes) struct Context *ctx = (struct Context *)opaque; ctx->mutex.lock(); // 告诉 VLC 将解码的数据放到缓冲区中 *planes = ctx->pixels; return nullptr; //获取 argb 帧 void Widget::vlc_unlock(void *opaque, void *picture, void *const *planes) struct Context *ctx = (struct Context *)opaque; unsigned char *data = static_cast<unsigned char *>(*planes); quint32 w=pThis->video_width; quint32 h=pThis->video_height; if(w>0 && h>0 && data!=nullptr) QImage image(data, int(w),int(h), QImage::Format_RGB32); if(!image.isNull()) pThis->ui->widget_videoDisplay->slotGetOneFrame(image.copy()); ctx->mutex.unlock(); 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 加载视频文件 flag=0 重新加载文件 flag=1 重新开始播放 QString file_path 这参数可以传入文件名称,因为窗口支持拖放文件进来 返回值: true 成功 false 失败 bool Widget::load_video_file(bool flag,QString file_path) if(flag==false) QString filename=file_path; if(filename.isEmpty()) //获取路径,如果没有默认路径,就使用当前C盘根目录 if(open_dir_path.isEmpty()) open_dir_path="C:/"; filename=QFileDialog::getOpenFileName(this,"选择播放的视频",open_dir_path,tr("*.mkv *.flv *.mp4 *.wmv *.*")); if(filename.isEmpty())return false; open_dir_path=QFileInfo(filename).path(); qDebug()<<"路径:"<<open_dir_path; media_filename=filename; //判断文件是否存在 if(QFileInfo::exists(media_filename)==false) if(media_filename.contains("rtsp:", Qt::CaseInsensitive)==false&& media_filename.contains("rtmp:", Qt::CaseInsensitive)==false) return false; qDebug()<<"播放的流媒体:"<<media_filename; /* 将 / 转为windows下的右斜杆 */ std::replace(media_filename.begin(), media_filename.end(), QChar('/'), QChar('\\')); qDebug()<<"播放的本地媒体:"<<media_filename; MediaInfo.state=MEDIA_LOAD; MediaInfo.mediaName=media_filename; //设置当前播放的视频名称 QFileInfo info(media_filename); ui->label_FileName->setText(QString("%1").arg(info.fileName())); //宽高清0 video_width=0; video_height=0; /*为特定文件路径创建媒体*/ if(vlc_media)libvlc_media_release(vlc_media); vlc_media=libvlc_media_new_path(vlc_base,media_filename.toUtf8()); if(vlc_media==nullptr) qDebug()<<"libvlc_media_new_path 执行错误."; return false; /*根据给定的媒体对象创建一个播放器对象*/ if(vlc_mediaPlayer)libvlc_media_player_release(vlc_mediaPlayer); vlc_mediaPlayer=libvlc_media_player_new_from_media(vlc_media); //设置回调,用于提取帧或者在界面上显示。 libvlc_video_set_callbacks(vlc_mediaPlayer, vlc_lock, vlc_unlock, display, &ctx); /*获取媒体播放器事件管理器*/ libvlc_event_manager_t *em = libvlc_media_player_event_manager(vlc_mediaPlayer); libvlc_event_attach(em, libvlc_MediaPlayerTimeChanged, vlcEvents, this); //进度改变 libvlc_event_attach(em, libvlc_MediaPlayerEndReached, vlcEvents, this); //播放完毕 libvlc_event_attach(em, libvlc_MediaPlayerStopped, vlcEvents, this); //停止 libvlc_event_attach(em, libvlc_MediaPlayerPlaying, vlcEvents, this); //开始播放 libvlc_event_attach(em, libvlc_MediaPlayerPaused, vlcEvents, this); //暂停 libvlc_event_attach(em, libvlc_MediaParsedChanged, vlcEvents, this); //获取到元数据 /*播放媒体文件*/ if(vlc_mediaPlayer)libvlc_media_player_play(vlc_mediaPlayer); //等待VLC解析文件.否则下面的时间获取不成功 QThread::msleep(100); //获取媒体文件总长度 ms libvlc_time_t length = libvlc_media_player_get_length(vlc_mediaPlayer); qDebug()<<"媒体文件总长度:"<<length; ui->label_Total_Time->setText(QTime(0, 0, 0,0).addMSecs(int(length)).toString(QString::fromLatin1("HH:mm:ss:zzz"))); //每次加载新文件设置播放进度条为0 ui->horizontalSlider_PlayPosition->setValue(0); //设置进度条的范围 ui->horizontalSlider_PlayPosition->setMaximum(int(length)); ui->horizontalSlider_PlayPosition->setMinimum(0); //获取当前媒体播放的位置 libvlc_time_t current_movie_time=libvlc_media_player_get_time(vlc_mediaPlayer); qDebug()<<"获取当前媒体播放的位置:"<<current_movie_time; libvlc_video_get_size(vlc_mediaPlayer,0,&video_width,&video_height); qDebug()<<"视频尺寸:"<<"width:"<<video_width<<"height:"<<video_height; memset(ctx.pixels, 0, MAX_WIDTH * MAX_HEIGHT * 4); //设置图像颜色格式 libvlc_video_set_format(vlc_mediaPlayer, "RV32", video_width, video_height, video_width * 4); libvlc_media_add_option(vlc_media, ":rtsp=tcp"); //连接方式 libvlc_media_add_option(vlc_media, ":network-caching=200"); //缓存 //设置按钮状态为播放状态 ui->MediaPauseBtn->setChecked(false); //隐藏标签控件 ui->label->setVisible(false); //获取媒体的元数据信息--异步方式 libvlc_media_parse_with_options(vlc_media,libvlc_media_parse_local,1000); qDebug()<<"媒体元数据获取---媒体标题:"<<libvlc_media_get_meta(vlc_media,libvlc_meta_Title); //获取媒体描述符的基本流描述 libvlc_media_track_t **tracks; //正常的视频获取的流的数量是2. 一个视频流 一个音频流 if(libvlc_media_tracks_get(vlc_media,&tracks)) qDebug()<<"视频宽:"<<tracks[0]->video->i_width; qDebug()<<"视频高:"<<tracks[0]->video->i_height; //手机拍的视频是翻转(宽高反过来的),电脑上播放需要翻转回去 qDebug()<<"旋转度数:"<<tracks[0]->video->i_orientation; //逆时针旋转90° if(tracks[0]->video->i_orientation==libvlc_video_orient_right_top) // video_width=tracks[0]->video->i_height; // video_height=tracks[0]->video->i_width; //ui->widget_videoDisplay->Set_Rotate(90); //libvlc_media_add_option(vlc_media, ":transform-type=90"); // libvlc_video_set_scale(vlc_mediaPlayer,1); libvlc_media_add_option(vlc_media, ":rtsp=tcp"); //连接方式 libvlc_media_add_option(vlc_media, ":codec=ffmpeg"); libvlc_media_add_option(vlc_media, ":avcodec-threads=1"); libvlc_media_add_option(vlc_media, ":avcodec-hw=any"); //硬件解码 libvlc_media_add_option(vlc_media, ":network-caching=200"); //缓存 libvlc_media_add_option(vlc_media, ":prefetch-buffer-size=1024"); //预装取缓冲大小512K libvlc_media_add_option(vlc_media, ":prefetch-read-size=65535"); //预装取读取大小64K return true; 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 加载视频文件 void Widget::on_toolButton_load_clicked() qDebug()<<"加载视频文件状态:"<<load_video_file(0,""); 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 暂停播放 void Widget::on_MediaPauseBtn_clicked() //暂停与继续 if(vlc_mediaPlayer)libvlc_media_player_pause(vlc_mediaPlayer); 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 重新开始播放 void Widget::on_MediaResetBtn_clicked() //加重新开始播放 load_video_file(true,""); 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 快退播放 void Widget::on_MediaPrevBtn_clicked() //得到播放进度的当前位置 int value=ui->horizontalSlider_PlayPosition->value(); seek(value-1000*5); 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 快进播放 void Widget::on_MediaPlayBtn_clicked() //得到播放进度的当前位置 int value=ui->horizontalSlider_PlayPosition->value(); seek(value+1000*5); 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 向左一帧 void Widget::on_ReverseFrameBtn_clicked() //得到播放进度的当前位置 int value=ui->horizontalSlider_PlayPosition->value(); value+=100; if(value<=ui->horizontalSlider_PlayPosition->maximum()) ui->horizontalSlider_PlayPosition->setValue(value); ui->label_current_Time->setText(QTime(0, 0, 0,0).addMSecs(int(value)).toString(QString::fromLatin1("HH:mm:ss:zzz"))); //跳转播放器 float f_value=(float)value/(float)ui->horizontalSlider_PlayPosition->maximum(); qDebug()<<"f_value:"<<f_value; libvlc_media_player_set_position(vlc_mediaPlayer,f_value); 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 向右一帧 void Widget::on_ForwardFrameBtn_clicked() //得到播放进度的当前位置 int value=ui->horizontalSlider_PlayPosition->value(); value-=100; if(value>=0) ui->horizontalSlider_PlayPosition->setValue(value); ui->label_current_Time->setText(QTime(0, 0, 0,0).addMSecs(int(value)).toString(QString::fromLatin1("HH:mm:ss:zzz"))); //跳转播放器 float f_value=(float)value/(float)ui->horizontalSlider_PlayPosition->maximum(); qDebug()<<"f_value:"<<f_value; libvlc_media_player_set_position(vlc_mediaPlayer,f_value); 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 音量设置 void Widget::on_VolumeBtn_clicked() bool checked=ui->VolumeBtn->isChecked(); if(checked) if(vlc_mediaPlayer)libvlc_audio_set_volume(vlc_mediaPlayer,0); //设置正常音量 int volume_val=ui->horizontalSlider_AudioValue->value(); //音量设置 if(vlc_mediaPlayer)libvlc_audio_set_volume(vlc_mediaPlayer,volume_val); 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 音量设置 void Widget::on_horizontalSlider_AudioValue_valueChanged(int value) //音量设置 if(vlc_mediaPlayer)libvlc_audio_set_volume(vlc_mediaPlayer,value); 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 播放速度设置菜单选择 void Widget::slot_onSetTimeSpeed(QAction *action) action->setChecked(true); ui->MediaSpeedBtn->setToolTip(action->text()); ui->MediaSpeedBtn->setText(action->text()); /*设置播放速率*/ if(vlc_mediaPlayer)libvlc_media_player_set_rate(vlc_mediaPlayer,float(action->data().toFloat())); 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 点击了速度设置按钮 void Widget::on_MediaSpeedBtn_clicked() QPoint ptWgt = ui->MediaSpeedBtn->mapToGlobal(QPoint(0, 0)); ptWgt -= QPoint(10, 94); QAction *pSelect = m_SpeedMenu.exec(ptWgt); if (pSelect == nullptr) return; 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 点击了旋转选择菜单 void Widget::slot_onMediaRotate(QAction *action) action->setChecked(true); ui->MediaRotateBtn->setToolTip(action->text()); ui->widget_videoDisplay->Set_Rotate(action->data().toInt()); 工程: ECRS_VideoPlayer 日期: 2021-02-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 点击了画面旋转菜单 void Widget::on_MediaRotateBtn_clicked() QPoint ptWgt = ui->MediaRotateBtn->mapToGlobal(QPoint(0, 0)); ptWgt -= QPoint(10, 94); QAction *pSelect = m_RotateMenu.exec(ptWgt); if (pSelect == nullptr) return; 工程: ECRS_VideoPlayer 日期: 2021-03-08 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 点击了功能设置菜单 void Widget::slot_onConfigurationFunction(QAction *action) if (action == nullptr) return; //得到按下的序号 MENU_ITEM item = MENU_ITEM(action->data().toInt()); //转视频为关键帧 if (item == MENU_TO_KeyFrame) //APP功能介绍 else if(item==MENU_APP_INFO) 工程: ECRS_VideoPlayer 日期: 2021-02-24 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 点击了截图菜单 void Widget::slot_onMediaSnapshot(QAction *action) if (action == nullptr) return; //得到按下的序号 MENU_ITEM item = MENU_ITEM(action->data().toInt()); QImage Pic=ui->widget_videoDisplay->GetImage(); if (Pic.isNull() || Pic.height() <= 0)return; //保存到剪切板 if (item == MENU_COPY_CLIPBOARD) QApplication::clipboard()->setImage(Pic); //保存到文件 else if (item == MENU_SAVE_FILE_SYSTEM) QString strFile = QDateTime::currentDateTime().toString("yyyyMMddHHmmss") + ".png"; QString strFileName = QFileDialog::getSaveFileName(nullptr, "保存图片", strFile, "PNG(*.png);;BMP(*.bmp);;JPEG(*.jpg *.jpeg)"); if (!strFileName.isEmpty()) strFileName = QDir::toNativeSeparators(strFileName); QFileInfo fInfo(strFileName); Pic.save(strFileName, fInfo.completeSuffix().toStdString().c_str(), 80); 工程: ECRS_VideoPlayer 日期: 2021-02-24 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 点击了截图按钮 void Widget::on_MediaSnapshotBtn_clicked() QPoint ptWgt = ui->MediaSnapshotBtn->mapToGlobal(QPoint(0, 0)); ptWgt -= QPoint(10, 48); QAction *pSelect = m_SnapshotMenu.exec(ptWgt); if (pSelect == nullptr) return; 工程: ECRS_VideoPlayer 日期: 2021-02-24 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 拦截事件 bool Widget::eventFilter(QObject *obj, QEvent *event) //判断是否在视频窗口范围内按下的鼠标 if(obj==ui->widget_videoDisplay) //点击的是音量滑块 else if(obj==ui->horizontalSlider_AudioValue) if (event->type()==QEvent::MouseButtonPress) //判断类型 QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event); if (mouseEvent->button() == Qt::LeftButton) //判断左键 int value = QStyle::sliderValueFromPosition(ui->horizontalSlider_AudioValue->minimum(), ui->horizontalSlider_AudioValue->maximum(), mouseEvent->pos().x(), ui->horizontalSlider_AudioValue->width()); ui->horizontalSlider_AudioValue->setValue(value); return QObject::eventFilter(obj,event); 工程: ASS_SubtitleVideoPlayer 日期: 2021-06-15 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 按键事件 void Widget::keyPressEvent(QKeyEvent *event) switch(event->key()) case Qt::Key_Space: //视频加载成功才能进行下面的操作 if(MediaInfo.state==MEDIA_LOAD) QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); if (keyEvent->key() == Qt::Key_Space) if(vlc_mediaPlayer) //暂停与继续切换 libvlc_media_player_pause(vlc_mediaPlayer); //4表示暂停 3表示继续 5表示停止 int state=libvlc_media_player_get_state(vlc_mediaPlayer); if(state==4 || state==5) //设置按钮状态 ui->MediaPauseBtn->setChecked(false); else if(state==3) //设置按钮状态 ui->MediaPauseBtn->setChecked(true); break; 工程: ECRS_VideoPlayer 日期: 2021-03-08 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 点击了设置按钮 void Widget::on_toolButton_set_clicked() // QPoint ptWgt = ui->toolButton_set->mapToGlobal(QPoint(0, 0)); // ptWgt -= QPoint(10, 48); // QAction *pSelect = m_ConfigurationFunctionMenu.exec(ptWgt); // if (pSelect == nullptr) // return; void Widget::seek(int value) if(vlc_mediaPlayer) float pos=value*1.0/ui->horizontalSlider_PlayPosition->maximum(); libvlc_media_player_set_position(vlc_mediaPlayer,pos); qDebug()<<"跳转的位置:"<<pos; ui->horizontalSlider_PlayPosition->setValue(value); ui->label_current_Time->setText(QTime(0, 0, 0,0).addMSecs(int(value)).toString(QString::fromLatin1("HH:mm:ss:zzz"))); //跳转播放器 float f_value=(float)value/(float)ui->horizontalSlider_PlayPosition->maximum(); qDebug()<<"f_value:"<<f_value; libvlc_media_player_set_position(vlc_mediaPlayer,f_value); void Widget::seek() seek(ui->horizontalSlider_PlayPosition->value()); void Widget::dragEnterEvent(QDragEnterEvent *e) if (e->mimeData()->hasUrls()) e->acceptProposedAction(); void Widget::dropEvent(QDropEvent *e) foreach (const QUrl &url, e->mimeData()->urls()) QString fileName = url.toLocalFile(); qDebug() << "拖入的文件名称:" << fileName; //加载视频文件 load_video_file(false,fileName); 工程: ECRS_VideoPlayer 日期: 2021-02-24 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 保存配置 void Widget::SaveConfig() //从UI界面获取用户的个性化配置参数 /*保存数据到文件,方便下次加载*/ QString text; text=QCoreApplication::applicationDirPath()+"/"+ConfigFile; QFile filesrc(text); filesrc.open(QIODevice::WriteOnly); QDataStream out(&filesrc); out << open_dir_path; //序列化 filesrc.flush(); filesrc.close(); 工程: ECRS_VideoPlayer 日期: 2021-02-24 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 读取配置 void Widget::ReadConfig() //读取配置文件 QString text; text=QCoreApplication::applicationDirPath()+"/"+ConfigFile; //判断文件是否存在 if(QFile::exists(text)) QFile filenew(text); filenew.open(QIODevice::ReadOnly); QDataStream in(&filenew); // 从文件读取序列化数据 in >>open_dir_path; //提取写入的数据 filenew.close(); 工程: ASS_SubtitleVideoPlayer 日期: 2021-06-16 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 窗口关闭事件 void Widget::closeEvent(QCloseEvent *event) //窗口关闭事件 int ret = QMessageBox::question(this, tr("重要提示"), tr("是否退出播放器?"), QMessageBox::Yes | QMessageBox::No); if(ret==QMessageBox::Yes) SaveConfig(); event->accept(); //接受事件 event->ignore(); //清除事件 工程: VLC_Core_VideoPlayer 日期: 2021-07-26 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 打开链接 void Widget::on_toolButton_link_clicked() bool ok; QString text = QInputDialog::getText(this, tr("流媒体播放"),tr("请输入流媒体地址:"), QLineEdit::Normal, tr("rtmp://58.200.131.2:1935/livetv/cctv14"),&ok); if (ok && !text.isEmpty()) //load_video_file(0,text); //打开RTMP流媒体:libvlc_media_new_location (inst, "rtmp://10.0.0.4:554/cam");

QT软件开发:解决隐藏QWidget标题栏后窗体无法移动问题

一、前言一般自定义标题栏时,就需要将原窗口的标题栏隐藏掉。默认情况下窗口无法被鼠标拖动的,需要自己处理鼠标事件,移动窗口达到拖动效果。二、实现代码只实现了窗口的拖动,随意拉伸没有实现。如果要完美实现拉伸效果,可以看这里:2.1 widget.h代码1.#ifndef LOGIN_MAIN_H #define LOGIN_MAIN_H #include <QWidget> namespace Ui { class login_main; class login_main : public QWidget Q_OBJECT public: ......... private slots: ......... protected: //截取鼠标事件绘制窗口位置. 因为标题栏隐藏后.窗口是无法拖动的。 void mouseReleaseEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void mousePressEvent(QMouseEvent *event); private: ......... bool isPressedWidget; QPoint m_lastPos; #endif // LOGIN_MAIN_H2.2 widget.cpp代码#include "login_main.h" #include "ui_login_main.h" login_main::login_main(QWidget *parent) : QWidget(parent), ui(new Ui::login_main) ui->setupUi(this); setWindowFlags(Qt::FramelessWindowHint); void login_main::mousePressEvent(QMouseEvent *event) m_lastPos = event->globalPos(); isPressedWidget = true; // 当前鼠标按下的即是QWidget而非界面上布局的其它控件 void login_main::mouseMoveEvent(QMouseEvent *event) if (isPressedWidget) { this->move(this->x() + (event->globalX() - m_lastPos.x()), this->y() + (event->globalY() - m_lastPos.y())); m_lastPos = event->globalPos(); void login_main::mouseReleaseEvent(QMouseEvent *event) qDebug()<<"鼠标松开"; m_lastPos = event->globalPos(); isPressedWidget = false; // 鼠标松开时,置为false

QT软件开发: 获取媒体详细信息(视频/音频)

一、环境介绍操作系统介绍:win10 64位QT版本: 5.12.6编译器:  MinGW32FFMPEG: 4.2.2下载地址: 二、软件效果 ​三、核心代码 3.1 widget.cpp1.#include "widget.h" #include "ui_widget.h" Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) ui->setupUi(this); ui->plainTextEdit->setReadOnly(true); this->setWindowTitle("获取媒体文件信息(支持拖动文件到窗口里)"); setAcceptDrops(true); Widget::~Widget() delete ui; 工程: GetMediaInformation 日期: 2021-07-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 选择路径 void Widget::on_pushButton_select_clicked() QString filename=QFileDialog::getOpenFileName(this,"选择打开的文件","C:/",tr("*.*")); //filename==选择文件的绝对路径 LoadName(filename); 工程: GetMediaInformation 日期: 2021-07-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 传入媒体名称 void Widget::LoadName(QString in_file) if(!in_file.isEmpty()) ui->lineEdit_info->setText(in_file); QString json=MediainFormation(in_file); ui->plainTextEdit->clear(); ui->plainTextEdit->insertPlainText(json); 工程: GetMediaInformation 日期: 2021-07-23 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 获取媒体信息 QString Widget::MediainFormation(QString in_file) QString ffmpeg_path=QCoreApplication::applicationDirPath(); //QString strCmd="C:/FFMPEG/ffmpeg_x86_4.2.2/bin/ffprobe.exe -v quiet -of json -i D:/jiyi.mp4 -show_streams"; ffmpeg_path+="ffmpeg422/"; QString strCmd=QString("ffmpeg422/ffprobe.exe -v quiet -of json -i \"%1\" -show_streams").arg(in_file); qDebug()<<"strCmd:"<<strCmd; QProcess process; // process.setWorkingDirectory(ffmpeg_path); // process.setReadChannel(QProcess::StandardOutput); process.start(strCmd); process.waitForFinished(); return process.readAll(); void Widget::dragEnterEvent(QDragEnterEvent *e) if (e->mimeData()->hasUrls()) e->acceptProposedAction(); void Widget::dropEvent(QDropEvent *e) foreach (const QUrl &url, e->mimeData()->urls()) QString fileName = url.toLocalFile(); LoadName(fileName); }3.2 widget.h #ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QFileDialog> #include <QProcess> #include <QCoreApplication> #include <QDebug> #include <QDragEnterEvent> #include <QUrl> #include <QMimeData> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACE class Widget : public QWidget Q_OBJECT public: Widget(QWidget *parent = nullptr); ~Widget(); QString MediainFormation(QString in_file); void LoadName(QString in_file); private slots: void on_pushButton_select_clicked(); protected: void dragEnterEvent(QDragEnterEvent *e); void dropEvent(QDropEvent *e); private: Ui::Widget *ui; #endif // WIDGET_H

C语言编程: CreateProcess标准输出重定向到文件

说明:  CreateProcess创建进程执行不支持简单的 > 符号重定向,system之类的函数执行外部进程可以使用 > 符号直接重定向到文件,但是system这类函数执行命令时,都会弹出控制台窗口,而CreateProcess创建执行进程可以设置属性隐藏这个控制台窗口,后台执行。示例代码:#include<windows.h> #include<shellapi.h> #include<stdio.h> #include <string.h> int my_CreateProcess() SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; _unlink("D:/output.log"); HANDLE h = CreateFile((L"D:/output.log"), FILE_APPEND_DATA, FILE_SHARE_WRITE | FILE_SHARE_READ, &sa, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); PROCESS_INFORMATION pi; STARTUPINFO si; BOOL ret = FALSE; DWORD flags = CREATE_NO_WINDOW; ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); si.dwFlags |= STARTF_USESTDHANDLES; si.hStdInput = NULL; si.hStdError = h; si.hStdOutput = h; TCHAR cmd[]= TEXT("ipconfig"); ret = CreateProcess(NULL, cmd, NULL, NULL, TRUE, flags, NULL, NULL, &si, &pi); if (ret) WaitForSingleObject(pi.hProcess, INFINITE); qDebug()<<"执行成功...."; CloseHandle(pi.hProcess); CloseHandle(pi.hThread); //关闭文件 CloseHandle(h); return 0; //关闭文件 CloseHandle(h); qDebug()<<"执行失败...."; return -1;

音视频开发: ffmpge创建空白视频(指定颜色与时长)

命令行如下:C:/FFMPEG/ffmpeg_x86_4.2.2/bin/ffmpeg.exe -ss 0 -t 00:00:05.300 -f lavfi -i color=c=0x000000:s=1280x720:r=25 -vcodec libx264 D:/linux-share-dir/video_file/video/test.mp4 -t 是视频的时长color=c=0x000000:s=1280x720:r=25  画面颜色、尺寸、帧率

一、方式1: system#include <stdlib.h> system("ping 127.0.0.1");  阻塞等待程序执行完再退出.二、方式2: WinExec#include<windows.h> #include<shellapi.h> #include<stdio.h> #include<string.h> WinExec("ping 127.0.0.1", SW_SHOWNOACTIVATE);调用完立即返回。三、方式3: CreateProcess​ #include<windows.h> #include<shellapi.h> #include<stdio.h> #include<string.h> STARTUPINFO si; PROCESS_INFORMATION pi; LPTSTR szCmdline=_wcsdup(TEXT("ping 127.0.0.1")); ZeroMemory( &si, sizeof(si) ); si.cb = sizeof(si); ZeroMemory( &pi, sizeof(pi) ); si.dwFlags = STARTF_USESHOWWINDOW; // 指定wShowWindow成员有效 si.wShowWindow = TRUE; // FALSE,此成员设为TRUE的话则显示新建进程的主窗口 BOOL bRet = CreateProcess( NULL, // 不在此指定可执行文件的文件名 szCmdline,// 命令行参数 NULL, // 默认进程安全性 NULL, // 默认进程安全性 FALSE, // 指定当前进程内句柄不可以被子进程继承 CREATE_NEW_CONSOLE, // 为新进程创建一个新的控制台窗口 NULL, // 使用本进程的环境变量 NULL, // 使用本进程的驱动器和目录 &si, &pi); if (bRet) WaitForSingleObject( pi.hProcess, INFINITE ); // 不使用的句柄最好关掉 CloseHandle(pi.hThread); CloseHandle(pi.hProcess); qDebug("新进程的ID号:%d\n", pi.dwProcessId); qDebug("新进程的主线程ID号:%d\n", pi.dwThreadId); }功能更加强大,可以隐藏控制台弹窗,可以等待进程执行完毕。

Linux系统开发: 搭建NFS服务器实现文件共享

一、NFS服务器介绍1.1 什么是NFS服务器NFS 是Network File System的缩写,即网络文件系统。一种使用于分散式文件系统的协定,由Sun公司开发,于1984年向外公布。功能是通过网络让不同的机器、不同的操作系统能够彼此分享个别的数据,让应用程序在客户端通过网络访问位于服务器磁盘中的数据,是在类Unix系统间实现磁盘文件共享的一种方法。NFS在文件传送或信息传送过程中依赖于RPC协议。RPC,远程过程调用 (Remote Procedure Call) 是能使客户端执行其他系统中程序的一种机制。NFS本身是没有提供信息传输的协议和功能的。1.2 NFS挂载原理NFS最大的功能就是可以通过网络,让不同的机器、不同的操作系统可以共享彼此的文件。NFS服务器可以让PC将网络中的NFS服务器共享的目录挂载到本地端的文件系统中,而在本地端的系统中来看,那个远程主机的目录就好像是自己的一个磁盘分区一样,在使用上相当便利。当我们在NFS服务器设置好一个共享目录/home/public后,其他有权访问NFS服务器的NFS客户端就可以将这个目录挂载到自己文件系统的某个挂载点,这个挂载点可以自己定义,如上图客户端1与客户端2挂载的目录就不相同。并且挂载好后我们在本地能够看到服务端/home/public的所有数据。如果服务器端配置的客户端只读,那么客户端就只能够只读。如果配置读写,客户端就能够进行读写。挂载后,NFS客户端可以通过#df –h命令查看挂载的磁盘信息。既然NFS是通过网络来进行服务器端和客户端之间的数据传输,那么两者之间要传输数据就要有相对应的网络端口,NFS服务器到底使用哪个端口来进行数据传输呢?基本上NFS这个服务器的端口开在2049,但由于文件系统非常复杂。因此NFS还有其他的程序去启动额外的端口,这些额外的用来传输数据的端口是随机选择的,是小于1024的端口;既然是随机的那么客户端又是如何知道NFS服务器端到底使用的是哪个端口呢?这时就需要通过远程过程调用(Remote Procedure Call,RPC)协议来实现了!查看/etc/services文件可以看到NFS服务器的端口号。1.3 什么是RPC? 为什么要用RPC?RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数。在研究RPC前,我们先看看本地调用是怎么调的。假设我们要调用函数Multiply来计算lvalue * rvalue的结果。那么在第8行时,我们实际上执行了以下操作:将 lvalue 和 rvalue 的值压栈进入Multiply函数,取出栈中的值10 和 20,将其赋予 l 和 r执行第2行代码,计算 l * r ,并将结果存在 y将 y 的值压栈,然后从Multiply返回第8行,从栈中取出返回值 200 ,并赋值给 l_times_r以上5步就是执行本地调用的过程。在远程调用时,我们需要执行的函数体是在远程的机器上的,也就是说,Multiply是在另一个进程中执行的。这就带来了几个新问题:Call ID映射。我们怎么告诉远程机器我们要调用Multiply,而不是Add或者FooBar呢?在本地调用中,函数体是直接通过函数指针来指定的,我们调用Multiply,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <--> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。序列化和反序列化。客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。网络传输。远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而GRPC干脆就用了HTTP2。Java的Netty也属于这层的东西。所以,要实现一个RPC框架,其实只需要把以上三点实现了就基本完成了。Call ID映射可以直接使用函数字符串,也可以使用整数ID。映射表一般就是一个哈希表。1.4 RPC与NFS如何通讯因为NFS支持的功能相当多,而不同的功能都会使用不同的程序来启动,每启动一个功能就会启用一些端口来传输数据,因此NFS的功能对应的端口并不固定,客户端要知道NFS服务器端的相关端口才能建立连接进行数据传输,而RPC就是用来统一管理NFS端口的服务,并且统一对外的端口是111,RPC会记录NFS端口的信息,如此我们就能够通过RPC实现服务端和客户端沟通端口信息。PRC最主要的功能就是指定每个NFS功能所对应的port number,并且通知客户端,记客户端可以连接到正常端口上去。  那么RPC又是如何知道每个NFS功能的端口呢?  首先当NFS启动后,就会随机的使用一些端口,然后NFS就会向RPC去注册这些端口,RPC就会记录下这些端口,并且RPC会开启111端口,等待客户端RPC的请求,如果客户端有请求,那么服务器端的RPC就会将之前记录的NFS端口信息告知客户端。如此客户端就会获取NFS服务器端的端口信息,就会以实际端口进行数据的传输了。提示:在启动NFS SERVER之前,首先要启动RPC服务(即portmap服务,下同)否则NFS SERVER就无法向RPC服务区注册,另外,如果RPC服务重新启动,原来已经注册好的NFS端口数据就会全部丢失。因此此时RPC服务管理的NFS程序也要重新启动以重新向RPC注册。特别注意:一般修改NFS配置文档后,是不需要重启NFS的,直接在命令执行/etc/init.d/nfs  reload或exportfs –rv即可使修改的/etc/exports生效。1.5 NFS客户端和NFS服务端通讯过程简图1)首先服务器端启动RPC服务,并开启111端口2)服务器端启动NFS服务,并向RPC注册端口信息3)客户端启动RPC(portmap服务),向服务端的RPC(portmap)服务请求服务端的NFS端口4)服务端的RPC(portmap)服务反馈NFS端口信息给客户端。5)客户端通过获取的NFS端口来建立和服务端的NFS连接并进行数据的传输。1.6 RPC服务工作原理简图二、部署NFS服务器2.1 查看内核版本信息先查看系统版本和内核参数。同一个软件在不同版本,内核之间是有差异的,所以部署的方法也不一样。2.2 安装NFS服务器Ubuntu 系统在线安装NFS服务器:Centos系统在线安装NFS服务器:RedHat6.3系统,可以在系统安装光盘映像里得到NFS服务器的安装包:2.3 启动NFS服务Ubuntu系统操作步骤:CentOS系统操作步骤:RedHat6.3系统操作步骤:三、编写NFS服务器的配置文件3.1 NFS服务器配置文件NFS服务的配置文件为/etc/exports,系统没有默认值,所以这个文件不一定会存在,可以使用vim手动建立,然后在文件里面写入配置内容。配置实例:NFS服务器共享/home/wbyq/rootfs目录给所有NFS客户端访问,权限为读写。3.2 指定 NFS客户端地址的配置详细说明3.3 NFS配置参数说明3.4 NFS服务器配置文件常见编写案例四、NFS服务器挂载实例4.1 启动NFS服务器实例以Redhat6.3系统为例:1. 编写NFS配置文件2. 手动(静态)设置服务器端IP地址也可以通过系统右上角的小电脑或者setup命令进行设置。3. 关闭防火墙也可以通过setup命令,打开图形配置页面关闭。4. 重新NFS服务器4.2 exportfs 命令在NFS服务器端,exportfs很常用,比如你想要重新修改/etc/exports 文件,当重新设定完 /etc/exports 后需不需要重新启动 nfs ? 不需要 ,如果重新启动 nfs 的话,要得再向 RPC 注册!很麻烦~这个时候我们可以透过 exportfs 这个指令来帮忙。exportfs命令用法如下:实例操作:1. 重新挂载一次 /etc/exports 文件的设定内容2. 将已经分享的 NFS 目录资源,通通都卸除4.3 NFS的联机观察在你的 NFS 服务器设定妥当之后,我们可以在 server 端先自我测试一下是否可以联机!就是利用 showmount 这个指令来查阅!示例:当你要扫瞄某一部主机他提供的 NFS 分享的目录时,就使用showmount -e IP (或 hostname) 即可!非常的方便!这也是 NFS client 端最常用的指令。4.4 客户端访问实例客户端访问服务器前,需要知道NFS服务器的IP地址和共享的目录路径。如果是局域网访问,那么需要保证客户端与服务器在同一网段下,如果是公网访问,需要保证客户端这个能过正常连接外网网络。1. NFS客户端挂载实例: 挂载NFS服务器的目录到本地目录将192.168.10.11服务器的/home/wbyq/project目录,挂载到本地/home/wbyq/mnt/目录下。如果挂载服务器时,提示mount命令卡住了,或者报错提示无法加锁等提示时,需要在挂载命令上加一个-o nolock参数。2. 取消目录挂载如果不再使用服务器的目录,一定要取消挂载。4.5 查看客户端的挂载情况/var/lib/nfs/etab、/var/lib/nfs/rmtab这两个文件就能够查看服务器上共享了什么目录,到底有多少客户端挂载了共享,能查看到客户端挂载的具体信息。1、etab这个文件能看到服务器上共享了哪些目录,执行哪些人可以使用,并且设定的参数为何。2、rmtab这个文件就是能够查看到共享目录被挂载的情况。[wbyq@wbyq ~]$ cat /var/lib/nfs/rmtab 192.168.10.10:/home/wbyq/rootfs:0x0000007b 192.168.10.123:/home/wbyq/rootfs:0x00000001 192.168.10.10:/home/wbyq/project:0x00000029 [wbyq@wbyq ~]$ cat /var/lib/nfs/etab /home/wbyq/project *(rw,sync,wdelay,hide,nocrossmnt,secure,no_root_squash,no_all_squash,no_subtree_check,secure_locks,acl,anonuid=65534,anongid=65534) /home/wbyq/rootfs *(rw,sync,wdelay,hide,nocrossmnt,secure,no_root_squash,no_all_squash,no_subtree_check,secure_locks,acl,anonuid=65534,anongid=65534)

Linux系统开发: linux下正则表达式

1.1 正则表达式介绍正则表达式就是为了处理大量的文本|字符串而定义的一套规则和模板。通过定义的这些特殊符号的辅助,系统管理员就可以快速过滤,替换或输出需要的字符串。Linux正则表达式一般以行为单位处理。正则表达式应用非常广泛,存在于各种语言中:php perl Python 等。现在学的是Linux中的正则表达式,最常应用正则表达式的命令是linux三剑客:grep(egrep),sed,awk。Linux系统开发: 学习linux三剑客(awk、sed、grep)(上)Linux系统开发: 学习linux三剑客(awk、sed、grep)(下)正则表达式是一个模版,这个模版是由一些普通字符和一些元字符组成。普通字符包括大小写的字母和数字,而元字符则具有特殊的含义。在少数情况下,正则表达式可能失效,多数与字符集有关。1.2 区分通配符与正则表达式这里一定要弄清楚正则表达式和linux下使用的通配符有本质区别。正则表达式用来找:【文件】内容,文本,字符串。一般只有三剑客支持。示例:#grep 123* demo.c 123* 则表明匹配12或1234/1235等字符串,但是不能匹配123通配符用来找:文件目录名,普通命令都支持。示例:#ls * 表明递归列出当前目录下的所有不以.号开头的文件信息在三剑客awk,sed,grep,egrep都是正则表达式,其他都是通配符1.3 正则表达式的分类基本的正则表达式(Basic Regular Expression 又叫Basic RegEx 简称BREs)扩展的正则表达式(Extended Regular Expression 又叫Extended RegEx 简称EREs)Perl的正则表达式(Perl Regular Expression 又叫Perl RegEx 简称PREs)1.3.1 基本的正则表达式​只有在用反斜杠\进行转义的情况下,字符(),{}才会在BRE被当作元字符处理,而ERE中,任何元符号前面加上反斜杠反而会使其被当作普通字符来处理。所以ERE中直接使用(),{},而BRE则 ,\{\}grep中调用-E参数指定使用扩展正则表达式。sed中调用-r参数指定使用扩展正则表达式。或者直接使用egrep。1.3.2 POSIX字符类POSIX字符类是一个形如[:...:]的特殊元序列(meta sequence),他可以用于匹配特定的字符范围。注意运用时还得在外层加一对[]号。1.3.3 Perl的正则表达式: 元字符元字符(meta character)是一种Perl风格的正则表达式,只有一部分文本处理工具支持它,并不是所有的文本处理工具都支持。

音视频开发:大华摄像头配置RTSP与RTMP地址访问视频画面

一、大华摄像头的RTSP地址格式rtsp地址格式:rtsp://username:password@ip:port/cam/realmonitor?channel=1&subtype=0参数解释:username: 摄像头登录用户名 (就是登录摄像头web管理页面的用户名和密码)password: 摄像头登录密码ip:  摄像头设备本身的IPport: 端口号channel: 通道号,起始为1。例如通道2,则为channel=2subtype: 码流类型,主码流(subtype=0),辅码流(subtype=1)示例:  这是我的摄像头访问地址rtsp://admin:abcd12345@10.0.0.4:554/cam/realmonitor?channel=1&subtype=0 二、web页面配置摄像头登录摄像头的管理页面,直接访问摄像头的IP地址.  例如: http://10.0.0.4登录之后看到摄像头画面:配置摄像头码流参数:网络配置页面上可以看到各个协议的默认端口: 平台接入里可以配置RTMP地址:三、RTSP地址访问摄像头效果直接采用VLC播放器访问: 也可以采用ffmpge自己编写拉流软件,获取摄像头数据:流媒体播放器设计:https://blog.csdn.net/xiaolong1126626497/article/details/105412560四、大华摄像头的RTMP地址设置大华摄像头支持推流到RTMP服务器,要使用这个功能,需要先自己搭建一个RTMP服务器再配置。windows如何搭建RTMP服务器?看这里: https://xiaolong.blog.csdn.net/article/details/106391149linux如何搭建RTMP服务器?看这里: https://xiaolong.blog.csdn.net/article/details/105378894配置好之后就可以使用VLC或者自己开发的流媒体播放器拉取刚才设置地址上的视频画面了。RTMP拉流和推流的地址是一样的。

QT软件开发:QtCreator使用VS2017编译器中文乱码解决

MinGW编译器中文显示正常:使用VS2017编译器中文乱码:解决办法:在头文件里添加:#pragma execution_character_set("utf-8") 解决后,VS2017编译器编译运行效果: MinGw编译器编译运行效果:

QT软件开发: 打开系统默认浏览器搜索内容

一、环境介绍Qt: 5.12.6操作系统:  win10 (64位)编译器:  mingw 32二、功能介绍使用代码一键打开系统默认浏览器,使用百度搜索想要内容,也可以打开百度翻译自动翻译内容,也可以打开CSDN搜索内容等。三、功能实现需要使用的头文件#include <QDesktopServices> #include <QUrl>一行代码QDesktopServices::openUrl(QUrl("https://www.baidu.com/s?ie=UTF-8&wd=我是搜索的内容")); 运行效果:其他常用的功能总结: //百度自动搜索想要的内容 https://www.baidu.com/s?ie=UTF-8&wd=我是搜索的内容 //CSDN自动搜索想要的内容 https://so.csdn.net/so/search?from=chrome_plugin&q=我是搜索的内容 //打开百度自动翻译 //中文翻译 https://fanyi.baidu.com/translate#zh/auto/我就是翻译的内容 //英文翻译 https://fanyi.baidu.com/translate#en/auto/我就是翻译的内容 //英文翻译 https://fanyi.baidu.com/translate#en/zh/I am the content of translation //中文翻译 https://fanyi.baidu.com/translate#zh/en/我就是翻译的内容QDesktopServices::openUrl(QUrl("https://so.csdn.net/so/search?from=chrome_plugin&q=DS小龙哥"));

Linux系统开发: 学习linux三剑客(awk、sed、grep)(上)

一、前言Linux中的三个命令awk、sed、grep在业界被称为“三剑客”,grep擅长查找,sed擅长取行和替换,awk擅长运算。我们知道Linux下一切皆文件,对Linux的操作就是对文件的处理,那么怎么能更好的处理文件呢?这就要用到三剑客命令。三剑客与正则表达式息息相关,正则表达式是为了处理大量的文本|字符串而定义的一套规则和模版,这个模版是由一些普通字符和一些元字符组成。普通字符包括大小写的字母和数字,而元字符则具有特殊的含义。正则表达式详情可参看资料《linux正则表达式》。三剑客与正则表达式是什么关系呢?三剑客就是普通的命令,有的把他们叫做工具。而正则表达式就好比一个模版,而linux下一般只有三剑客能读懂这个模版。二、grep命令2.1 grep命令功能grep(global search regular expression(RE) and print out the line,全面搜索正则表达式并把行打印出来)是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。Unix的grep家族包括grep、egrep和fgrep。egrep和fgrep的命令只跟grep有很小不同。egrep是grep的扩展,支持更多的re元字符, fgrep就是fixed grep或fast grep,它们把所有的字母都看作单词,也就是说,正则表达式中的元字符表示回其自身的字面意义,不再特殊。linux使用GNU版本的grep。它功能更强,可以通过-G、-E、-F命令行选项来使用egrep和fgrep的功能。查找内容可以用双引号括起来,也可以不用,建议使用双引号,双引号中一些特殊符号要注意使用转义字符。格式:grep  [OPTIONS]  PATTERN  [FILE...]grep默认不支持扩展正则,因此扩展正则表达式的符号对于grep来说就等同于普通字符含义,因此,想让grep直接处理正则符号必须通过转义字符\{\}来处理。grep -E 强制让grep直接认识正则符号,不需要再进行转义,egrep 等效grep -E 天生就能认识正则符号;我们平时备份可以通过cp 文件名{,.bak}的形式进行,避免再打一次文件名2.2 支持的选项参数-a 不要忽略二进制数据。-A <显示行数> 除了显示符合范本样式的行之外,并显示该行之后的指定几行内容。-B<显示行数> 除了显示符合范本样式的行之外,并显示该行之前的指定几行内容。-C<显示行数>  除了显示符合范本样式的那一行之外,并显示该行前后指定几行的内容。-b 在显示符合范本样式的那一行之外,并显示字节偏移量。-c 只计算显示符合范本样式的行数,不显示详细内容-d<进行动作> 当指定要查找的是目录而非文件时,必须使用这项参数,否则grep命令将回报信息并停止动作。-e<范本样式> 指定字符串作为查找文件内容的范本样式。-E 将范本样式为延伸的普通表示法来使用,意味着能使用扩展正则表达式。-f <范本文件> 指定范本文件,其内容有一个或多个范本样式,让grep查找符合范本条件的文件内容,格式为每一列的范本样式。-F 将范本样式视为固定字符串的列表。-G 将范本样式视为普通的表示法来使用。-h 在显示符合范本样式的那一列之前,不标示该列所属的文件名称。-H 在显示符合范本样式的那一列之前,标示该列的文件名称。-i 忽略字符大小写的差别。-l 列出文件内容符合指定的范本样式的文件名称。-L 列出文件内容不符合指定的范本样式的文件名称。-n 在显示符合范本样式的那一列,标示出该列的编号。-q 不显示任何信息。-R/-r 此参数的效果和指定“-d recurse”参数相同,表明查找路径为目录-s 不显示错误信息。-v 反转查找,显示不符合模式的所有信息-w 只显示全字符合的列。-x 只显示全列符合的列。-y 此参数效果跟“-i”相同。-o 只输出文件中匹配到的部分。--color=auto 把匹配部分标记出来,要想当前终端后续使用都要标记匹配部分,可用alias命令重新封装grep。#alias grep=’grep --color=auto’2.3 常用示例在文件中查找内容成功会输出所有包含查找内容的行,否则输出为空。$ grep bash file_read.sh #在file_read.sh内查找bash $ grep "bash" file_read.sh --color=auto #两者效果相同,并且标记颜色 $ grep "bash" file_read.sh demo.sh #在file_read.sh demo.sh 内查找bash 在目录下查找内容成功会输出文件名:所有包含内容的行,否则输出为空需运用-r/-R/-d recurse 选项参数,指明查找路径为目录$ grep "bash" -r ./ #在当前目录下查找文件内容bash $ grep "bash" -R ./ $ grep "bash" -d recurse ./显示查找内容所在行的行号需运用-n参数,显示行号,可单独也可与其他选项参数写在一起。$ grep "bash" -n file_read.sh #在file_read.sh内查找bash $ grep "bash" -Rn ./ $ grep "bash" -r -n ./反转显示,显示与查找内容不符合的所有内容需运用-v参数。$ grep "bash" -vn demo.sh #显示demo.sh内不包含bash的行,并显示行号查找以某内容开头的行需运用正则表达式^...。$ grep "^#" demo.sh #查找demo.sh内以#开头的行,注意前面不能有空白字符,必须是最开头查找空白行需运用正则表达式^...。需运用正则表达式...$。$ grep "^$" 123.txt查找非指定字符开头的行$ grep "^[^#]" demo.sh #在demo.sh中查找不以#开头的行查找以某内容结尾的行需运用正则表达式...$。$ grep "name$" demo.sh #查找demo.sh内以name结尾的行,注意必须是最后且后面不能有空白字符需运用-c参数,不显示详细内容,只显示行数$ grep "name" demo.sh -c # 在demo.sh中查找name出现的行数显示查找内容及其前后行内容需运用 -A 行数/-B 行数/-C 行数 参数$ grep "name" demo.sh -A 2 #显示查找内容及其后两行内容 $ grep "name" demo.sh -B 2 #显示查找内容及其前两行内容 $ grep "name" demo.sh -C 2 #显示查找内容及其前后两行内容 查找阿拉伯数字需要用到正则表达式[m]与{n},选项参数-E(指定使用正则表达式)[]正则表达式:[m]表明查找匹配m字符的内容。[^m]表明匹配不是m字符的内容。[m-f]表示匹配m到f的内容,m可以是数字,可以是字符。{}正则表达式:{m}表示匹配之前的项m次{m,}表示匹配之前的项至少m次{m,f}表示匹配之前的项m次到f次。m是可以为0的正整数。$ grep "[1-3]\{2\}" 123.txt #在123.txt中查找1-3之间数字出现两次的内容,注意{}前后一定要加转义字符 $ grep -E "[1-3]{2}" 123.txt #或者直接使用-E参数指定使用正则表达式,则可不加转义 $ grep "[1-3][1-3]" 123.txt #与上面相同效果,也表示在123.txt中查找1-3之间数字出现两次的内容 $ifconfig | grep "[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}" grep用于常规的查询操作固然方便,但是最大的弊端就是查出来不能增删改,导致如果是写一些脚本就会很不方便,这个时候就需要sed和awk这样的工具来实现。三、sed命令3.1 sed命令介绍sed是一种流编辑器,它是文本处理中非常中的工具,能够完美的配合正则表达式使用,功能不同凡响。处理时,把当前处理的行存储在临时缓冲区中,称为“模式空间”(pattern space),接着用sed命令处理缓冲区中的内容,处理完成后,把缓冲区的内容送往屏幕。接着处理下一行,这样不断重复,直到文件末尾。文件内容并没有改变,除非你使用重定向存储输出。Sed主要用来自动编辑一个或多个文件;简化对文件的反复操作;编写转换程序等。命令格式:sed [options] 'command' file(s) sed [options] -f scriptfile file(s)注意:查找的内容前后一定要用/包含起来,示例/sh/  。替换的时候可用@与#替换/。命令与查找内容可连在一起也可分开,但是中间必须有/作为间隔。3.2 sed的工作流程1、sed默认不编辑原文件,而是逐行操作,复制一份到指定内存(pattern space,模式空间)2、pattern space内进行模式匹配,即和指定条件做匹配  不满足模式:输出到标准输出STDOUT  满足模式:进行指定的模式操作,再输出到STDOUT3、第二个特殊的内存空间 :保持空间(hold space),临时保存操作在另一处内存4、当执行pattern space和 hold space相关选项时候会进行之间的数据流编辑操作5、最后根据操作执行hold space空间操作,选择性显示到STDOUT3.3 选项参数-c/--copy 用拷贝代替重命名 -e<script>/--expression=<script> 以选项中的指定的脚本来处理输入的文本文件; -f<script文件>/--file=<script文件> 以选项中指定的脚本文件来处理输入的文本文件; --follow-symlinks 处理输入的文本文件时,追踪软链接,断开硬链接 -h/--help 显示帮助; -i[SUFFIX]/ --in-place[=SUFFIX] 就地编辑文件,提供了后缀名(.bak)则备份文件 -l N/ --line-length=N 为l命令指定换行的长度n -n/--quiet/——silent 不自动打印模式空间内容,仅显示脚本处理后的结果,sed默认打印全部内容 --posix 禁用所有GNU扩展 -u/ --unbuffered 从输入文件中加载最小的数据并频繁刷新输出缓冲区 -V/--version 显示版本信息。 -r/--regexp-extended 支持使用扩展正则表达式 -s/--separate 把文件作为单独的个体而不是作为单个连续的长流 3.4 命令参数的使用命令建议用单引号’’或双引号括起来方便区分,多个命令用;隔开。a\ 在当前行下面插入文本。 i\ 在当前行上面插入文本。 c\ 把选定的行改为新的文本。 d 删除,删除选择的行。 D 删除模板块的第一行。 s 替换指定字符,字符间可用/或@或#隔开 h 拷贝模板块的内容到内存中的缓冲区。 H 追加模板块的内容到内存中的缓冲区。 g 获得内存缓冲区的内容,并替代当前模板块中的文本。 G 获得内存缓冲区的内容,并追加到当前模板块文本的后面。 l 列表不能打印字符的清单。 n 读取下一个输入行,用下一个命令处理新的行而不是用第一个命令。 N 追加下一个输入行到模板块后面并在二者间嵌入一个新行,改变当前行号码。 p 打印模板块的行。前面可加数字,指定打印第几行 P(大写) 打印模板块的第一行。 q 退出Sed。 b lable 分支到脚本中带有标记的地方,如果分支不存在则分支到脚本的末尾。 r file 从file中读行。 t label if分支,从最后一行开始,条件一旦满足或者T,t命令,将导致分支到带有标号的命令处,或者到脚本的末尾。 T label 错误分支,从最后一行开始,一旦发生错误或者T,t命令,将导致分支到带有标号的命令处,或者到脚本的末尾。 w file 写并追加模板块到file末尾。 W file 写并追加模板块的第一行到file末尾。 ! 表示后面的命令对所有没有被选定的行发生作用。 示例:1!表明对文中所有行起作用,3!表示对文中第三行及以下行起作用 = 打印当前行号码。 # 把注释扩展到下一个换行符以前。 3.5 替换标记g 表示行内全面替换。 p 表示打印行。 w 表示把行写入一个文件。 x 表示互换模板块中的文本和缓冲区中的文本。 y 表示把一个字符翻译为另外的字符(但是不用于正则表达式) \1 子串匹配标记 & 已匹配字符串标记3.6 元字符集^ 匹配行开始,如:/^sed/匹配所有以sed开头的行。 $ 匹配行结束,如:/sed$/匹配所有以sed结尾的行。 . 匹配一个非换行符的任意字符,如:/s.d/匹配s后接一个任意字符,最后是d。 * 匹配0个或多个字符,如:/*sed/匹配所有模板是一个或多个空格后紧跟sed的行。 [] 匹配一个指定范围内的字符,如/[sS]ed/匹配sed和Sed。 [^] 匹配一个不在指定范围内的字符,如:/[^A-RT-Z]ed/匹配不包含A-R和T-Z的一个字母开头,紧跟ed的行。 .... 匹配子串,保存匹配的字符,如s/loveloveable/\1rs,loveable被替换成lovers。 & 保存搜索字符用来替换其他字符,如s/love/**&**/,love这成**love**。 \< 匹配单词的开始,如:/\<love/匹配包含以love开头的单词的行。 \> 匹配单词的结束,如/love\>/匹配包含以love结尾的单词的行。 x\{m\} 重复字符x,m次,如:/0\{5\}/匹配包含5个0的行。 x\{m,\} 重复字符x,至少m次,如:/0\{5,\}/匹配至少有5个0的行。 x\{m,n\} 重复字符x,至少m次,不多于n次,如:/0\{5,10\}/匹配5~10个0的行。 3.7 脚本地址定界/ 在sed中作为定界符使用,也可以使用任意的定界符:| / 定界符出现在样式内部时,需要进行转义,示例:sed 's/\/bin/\/usr\/local\/bin/g' 不给地址:对全文进行处理 $:表示最后一行 地址范围: 选定行的范围:,(逗号) /pattern/:被此处模式所能够匹配到的每一行 /pattern/,m:被模式匹配到的第一行起到m行 n,m 表示从n行到第m行 n,+m 表示从n行起往后增加m行 n~m:步进:以n行为基准值,每次增加m行 3.8 组合多个表达式sed '表达式' | sed '表达式' 等价于: sed '表达式; 表达式'3.9 常用示例 显示输入文件的行号需用到命令:=:打印当前行号码(包括空白行)需用到元字符集:. :匹配一个非换行符的任意字符需用到命令:!: 表示后面的命令对所有没有被选定的行发生作用。$ sed '=' 123.txt #显示文本的每一行行号 $ sed '3=' 123.txt #显示文本的第三行行号 $ sed "/./=" 123.txt #只显示非空白行的行号 $ sed -n "/./!=" 123.txt #只显示空白行行号 显示文件总行数需用到元字符集:$:匹配到行结束$ sed '$=' 123.txt #可显示123.txt内总共有多少行,也就是最后一行的行号打印输入文件的指定行内容需用到-n参数:不自动打印,需用到p命令:打印模块的行$ sed -n '2p' 123.txt #注意一定要加-n,否则会默认自动打印所有内容 $ sed -n '2 p' 123.txt #注意一定要加-n,否则会默认自动打印所有内容打印输入文件的指定几行内容$sed -n '2,7 p' 123.txt #注意一定要加-n,否则会默认自动打印所有内容 $ sed -n '2,7p' 123.txt $ sed -n '2,7 {p}' 123.txt #命令也可单独用{}括起来替换输入文件中内容需用到-i参数:就地编辑文件,会对源文件作更改需用到s命令:替换指定字符,注意字符之间可用/@#隔开,注意如果没有其他命令或者替换标记作为结尾,最后也必须得由它们作为尾字符,需用到g替换标记:替换行内的所有匹配内容,前面可加数字,表明第几个匹配位置$ sed -i 's/bck/sh/' 123.txt 666.txt #替换123.txt、666.txt内的bck为sh,每行只替换一个 $ sed -i 's/bck/sh/g' 123.txt #替换123.txt内的bck为sh,每行都进行全面替换 $ sed -i 's/bck/sh/3g' 123.txt #替换123.txt内的bck为sh,从第3个匹配位置开始替换 $ sed -i 's@bck@sh@g' 123.txt #替换123.txt内的bck为sh,每行都进行全面替换 $ sed -i 's#bck#sh#g' 123.txt #替换123.txt内的bck为sh,每行都进行全面替换 替换输入文件中指定行的内容$ sed -i '1,5 s/bck/sh/g' 123.txt 666.txt #替换123.txt、666.txt内的第一行到第五行的bck为sh,每行全面替换 $ sed -i '2,+2 {s/bck/sh/g}' 123.txt 666.txt #替换123.txt、666.txt内的第二行往后两行的bck为sh,每行全面替换,命令也可以单独用{}括起来,表示边界 $ sed -i '2~2 s/bck/sh/g' 123.txt 666.txt #替换123.txt、666.txt内的第二行往后每次增加两行的bck为sh,每行全面替换 给文件名\单词前统一替换加前缀或后缀或前后缀需用到元字符集:^ 匹配行开始,如:/^sed/匹配所有以sed开头的行。需用到元字符集:$ 匹配行结束,如:/sed$/匹配所有以sed结尾的行。需用到替换标记:& 已匹配字符串标记,代替之前已匹配内容需用到正则表达式:\w\+:匹配每一个单词$ ls | sed 's/^/666_&/g' #表示给当前文件下的文件名统一添加前缀 $ ls | sed 's/$/666_&/g' #表示给当前文件下的文件名统一添加后缀 $ ls | sed 's/\w\+/666_&/g' #表明给所有的单词添加前缀 $ ls | sed 's/\w\+/[&]/' #表明给每个匹配到的单词用[]括起来 显示指定区间以指定内容开头或结尾的行需用到元字符集^:匹配行开始如果/前面有地址定界,则在/外面必须加上{}$ sed -n '1,10 {/^10/p}' 123.txt #显示123.txt内第1到第10行中以10开头的行 $ sed -n '/^10/p' 123.txt # /前没有地址定界则可以不加{},如果有则必须加上 $ sed -n '1,10 {/sh$/p}' 123.txt #显示123.txt内第1到第10行中以结尾的行 显示查找内容的所有行、显示找到的第一行及以下指定行需用到脚本地址定界:/pattern/:被此处模式所能够匹配到的每一行需用到脚本地址定界:/pattern/,m:被模式匹配到的第一行起到m行需用到脚本地址定界:$ 匹配到末尾行$ sed -n '/sh/p' 123.txt #显示123.txt内的所有包含sh的所有行 $ sed -n '/sh/ ,$ p' 123.txt #显示123.txt里第一条包含sh的行及以下到末尾的所有行 $ sed -n '/sh/ ,$p' 123.txt 逆序输出文本内容需用到命令:!:!前跟非零数字,表示后面的命令对所有没有被选定的行发生作用需用到命令:G:获得内存缓冲区的内容,并追加到当前模板块文本的后面需用到命令:h: 拷贝模板块的内容到内存中的缓冲区需用到命令:d :删除,删除选择的行。多个命令之前用分号;隔开。$ sed '1!G;h;$!d' 123.txt逆序输出每行内容$ sed '/\n/!G;s/..∗\n/&\2\1/;//D;s/.//' 123.txt删除指定行需用到命令:d 删除,删除选择的行。$ grep -n "sh" 123.txt | sed '4,5d' #删除grep查找到的内容的第4到5行 $ grep -n "sh" 123.txt | sed '1,2d' #删除grep查找到的内容的第1行后每次隔两行删一行 $ sed '/^$/d' 123.txt #删除空白行 在指定行前后插入内容需用到命令:a\ 在当前行下面插入文本。默认当前行为最末行需用到命令:i\ 在当前行上面插入文本。默认当前行为最末行需用到命令:c\ 把选定的行改为新的文本。默认当前行为所有行$ sed 'a\hello\' 123.txt #在123.txt的末行后增加一行hello $ sed 'i\hello\' 123.txt #在123.txt的末行前增加一行hello $ sed 'c\hello\' 123.txt #替换123.txt的所有行为hello $ sed '8a\hello\' 123.txt #在123.txt的第8行后增加一行hello $ sed '8,10a\hello\' 123.txt #在123.txt的第8行到10行每一行后增加一行hello $ sed '8,10c\hello\' 123.txt #在123.txt的第8行到10行替换为一行hello 一行内执行多条命令需用到选项参数-e<script>/--expression=<script>   以选项中的指定的脚本来处理输入的文本文件;$ sed -e '1,5d' -e 's/sh/bck/' 123.txt #先删除1到5行再替换sh为bck $ sed '1,5d;s/sh/bck/' 123.txt #三者效果相同 $ sed --expression='1,5d' --expression='s/sh/bck/' 123.txt #三者效果相同 四、awk命令续集: Linux入门开发: 学习linux三剑客(awk、sed、grep)(下)

ffmpeg音视频开发: 使用ffprobe获取媒体信息

一、环境介绍操作系统: win10 64位ffmpge:  4.2.2 (官网下载的可执行文件命令) win32下使用FFMPEG 4.2.2库下载地址:https://download.csdn.net/download/xiaolong1126626497/12321684二、ffprobe用法2.1 基本使用: 查看媒体信息用法:C:/FFMPEG/ffmpeg_x86_4.2.2/bin/ffprobe.exe -i .\jiyi.mp4示例:PS D:\> C:/FFMPEG/ffmpeg_x86_4.2.2/bin/ffprobe.exe -i .\jiyi.mp4 built with gcc 9.2.1 (GCC) 20200122 configuration: --disable-static --enable-shared --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt libavutil 56. 31.100 / 56. 31.100 libavcodec 58. 54.100 / 58. 54.100 libavformat 58. 29.100 / 58. 29.100 libavdevice 58. 8.100 / 58. 8.100 libavfilter 7. 57.100 / 7. 57.100 libswscale 5. 5.100 / 5. 5.100 libswresample 3. 5.100 / 3. 5.100 libpostproc 55. 5.100 / 55. 5.100 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '.\jiyi.mp4': Metadata: major_brand : isom minor_version : 512 compatible_brands: isomiso2avc1mp41 encoder : Lavf58.29.100 Duration: 00:00:54.15, start: 0.000000, bitrate: 574 kb/s Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709), 960x544, 518 kb/s, 29.97 fps, 29.97 tbr, 19200 tbn, 38400 tbc (default) Metadata: handler_name : Core Media Video Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, mono, fltp, 47 kb/s (default) Metadata: handler_name : Core Media Audio PS D:\>2.2  使用JSON格式输出信息 用法:C:/FFMPEG/ffmpeg_x86_4.2.2/bin/ffprobe.exe -v quiet -of json -i .\jiyi.mp4 -show_streams示例:PS D:\> C:/FFMPEG/ffmpeg_x86_4.2.2/bin/ffprobe.exe -v quiet -of json -i .\jiyi.mp4 -show_streams "streams": [ "index": 0, "codec_name": "h264", "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", "profile": "High", "codec_type": "video", "codec_time_base": "8123/486900", "codec_tag_string": "avc1", "codec_tag": "0x31637661", "width": 960, "height": 544, "coded_width": 960, "coded_height": 544, "has_b_frames": 1, "pix_fmt": "yuv420p", "level": 31, "color_range": "tv", "color_space": "bt709", "color_transfer": "bt709", "color_primaries": "bt709", "chroma_location": "left", "refs": 1, "is_avc": "true", "nal_length_size": "4", "r_frame_rate": "30000/1001", "avg_frame_rate": "243450/8123", "time_base": "1/19200", "start_pts": 0, "start_time": "0.000000", "duration_ts": 1039744, "duration": "54.153333", "bit_rate": "518796", "bits_per_raw_sample": "8", "nb_frames": "1623", "disposition": { "default": 1, "dub": 0, "original": 0, "comment": 0, "lyrics": 0, "karaoke": 0, "forced": 0, "hearing_impaired": 0, "visual_impaired": 0, "clean_effects": 0, "attached_pic": 0, "timed_thumbnails": 0 "tags": { "language": "und", "handler_name": "Core Media Video" "index": 1, "codec_name": "aac", "codec_long_name": "AAC (Advanced Audio Coding)", "profile": "LC", "codec_type": "audio", "codec_time_base": "1/44100", "codec_tag_string": "mp4a", "codec_tag": "0x6134706d", "sample_fmt": "fltp", "sample_rate": "44100", "channels": 1, "channel_layout": "mono", "bits_per_sample": 0, "r_frame_rate": "0/0", "avg_frame_rate": "0/0", "time_base": "1/44100", "start_pts": 0, "start_time": "0.000000", "duration_ts": 2387471, "duration": "54.137664", "bit_rate": "47025", "max_bit_rate": "47025", "nb_frames": "2332", "disposition": { "default": 1, "dub": 0, "original": 0, "comment": 0, "lyrics": 0, "karaoke": 0, "forced": 0, "hearing_impaired": 0, "visual_impaired": 0, "clean_effects": 0, "attached_pic": 0, "timed_thumbnails": 0 "tags": { "language": "und", "handler_name": "Core Media Audio" PS D:\>

Linux系统开发: 基础命令学习

一、Linux系统介绍Linux是一套免费使用和自由传播的Unix操作系统,是一个基于POSIX和UNIX的多用户、多任务、支持多线程和多CPU的操作系统。支持32位和64位硬件,Linux继承了Unix以网络为核心的设计思想,是一个性能稳定的多用户网络操作系统;Linux存在着许多不同的版本,但它们都使用了Linux内核。Linux可安装在各种计算机硬件设备中,比如手机、平板电脑、路由器、视频游戏控制台、台式计算机、大型机和超级计算机,可移植性较强。Linux目录结构/bin 该目录中存放Linux的常用命令,在有的版本中是一些和根目录下相同的目录。/boot 该目录下存放的都是系统启动时要用到的程序,当用lilo引导Linux时,会用到这里的一些信息。/dev 该目录包含了Linux系统中使用的所有外部设备,它实际上是访问这些外部设备的端口,你可以访问这些外部设备,与访问一个文件或一个目录没有区别。例如在系统中键入“cd /dev/cdrom”,就可以看到光驱中的文件;键入“cd /dev/mouse”即可看鼠标的相关文件。/etc 该目录存放了系统管理时要用到的各种配置文件和子目录,例如网络配置文件、文件系统、X系统配置文件、设备配置信息、设置用户信息等。/sbin 该目录用来存放系统管理员的系统管理程序。/home 如果建立一个名为“xx”的用户,那么在/home目录下就有一个对应的“/home/xx”路径,用来存放该用户的主目录。/lib 该目录用来存放系统动态连接共享库,几乎所有的应用程序都会用到该目录下的共享库 。/lost+found 该目录在大多数情况下都是空的。但当突然停电、或者非正常关机后,有些文件就临时存放在这里。/mnt 该目录在一般情况下也是空的,你可以临时将别的文件系统挂在该目录下。/proc 可以在该目录下获取系统信息,这些信息是在内存中由系统自己产生的/root 如果你是以超级用户的身份登录的,这个就是超级用户的主目录/tmp 用来存放不同程序执行时产生的临时文件。/usr 用户的很多应用程序和文件都存放在该目录下。/usr/X11R6:X-Window目录;/usr/src:Linux源代码;/usr/include:系统头文件;/usr/lib:存放常用动态链接共享库、静态档案库;二、Linux基本命令介绍2.1 su命令:切换用户。语法:su [用户名]  [ ]表示可选linux下有两种帐号:1.root--超级用户帐号(系统管理员),使用这个帐号可以在系统中做任何事情。2.普通用户--这个帐号供普通用户使用,可以进行有限的操作。su命令的常见用法是变成超级用户,如果普通用户发出不带用户名的su命令 ,则系统提示输入根口令,输入之后则可切换为根用户。2.2 ls命令:遍历目录功能:ls是英文单词list的简写,其功能为列出目录的内容。这是用户最常用的一个命令之一,因为用户需要不时地查看某个目录的内容。该命令类似于DOS下的dir命令。语法:ls [选项] [目录或是文件]说明:对于每个目录,该命令将列出其中的所有子目录与文件。对于每个文件,ls将输出其文件名以及所要求的其他信息。默认情况下,输出条目按字母顺序排序。当未给出目录名或是文件名时,就显示当前目录的信息。常用选项:用ls - l命令显示的信息中,开头是由10个字符构成的字符串,其中第一个字符表示文件类型:- 普通文件、d 目录 、l 符号链接、b 块设备文件、c 字符设备文件、p 命名管道(FIFO)、s  socket文件后面的9个字符表示文件的访问权限,分为3组,每组3位。第一组表示文件所有者的权限,第二组表示同组用户的权限,第三组表示其他用户的权限。每一组的三个字符分别表示对文件的读、写和执行权限。各权限如下所示:r 读        (4)w 写      (2)x 执行    (1) 对于目录,表示进入权限。- 没有设置权限。2.3 man命令:查看帮助功能:用于查看命令、函数、头文件的使用帮助信息。用法:  man  [页码]  <命令、函数名称、头文件名称>页码一般为1-7页。如果当前页看不到想要的信息,可以切换到其他页查看。2.4 cd命令:切换目录功能:切换目录语法:cd [目录路径]说明:该命令将当前工作目录切换至指定的目录。若没有指定目录路径, 则回到用户的主目录~。为了改变到指定目录,用户必须拥有对指定目录的执行和读权限。常用的目录表示符号:cd ..   到父目录,即上一级目录,相当于“向上”cd -   到上一次目录,相当于“后退”cd /   到根目录cd ~或者只写cd  回到到用户主目录下2.5 mkdir命令:创建目录功能:创建一个目录语法:mkdir [选项] dirname说明:该命令创建由dirname命名的目录。要求创建目录的用户在当前目录中 (dirname的父目录中)具有写权限,并且dirname不能是当前目录中已有的目录或 文件名称。参数:- m 对新建目录设置存取权限。也可以用chmod命令设置。- p 可以是一个路径名称。此时若路径中的某些目录尚不存在, 加上此选项后, 系统将自动建立好那            些尚不存在的目录,即一次可以建立多个目录。创建多层目录示例:mkdir  ./dir1/dir2/dir3  -p2.6 touch命令:创建普通文件功能:创建一个文件。语法:touch [文件名称]2.7 rm命令:删除文件/目录功能:在linux中创建文件很容易,系统中随时会有文件变得过时且毫无用处。用户可以用rm命令将其删除。该命令的功能为删除一个目录中的一个或多个文件或目录,它也可以将某个目录及其下的所有文件及子目录均删除。对于链接文件,只是删除了链接,原有文件均保持不变。语法:rm [选项]  <文件或者目录>说明:如果没有使用- r选项,则rm不会删除目录。参数:- f 忽略不存在的文件,从不给出提示。- r 指示rm将参数中列出的全部目录和子目录均递归地删除。- i 进行交互式删除。-v 输出已经删除的文件使用rm命令要格外小心。因为一旦一个文件被删除,它是不能被恢复的。为了防止此种情况的发生,可以使用rm命令中的 -i选项来确认要删除的每个文件。如果用户输入y,文件将被删除。如果输入任何其他东西,文件将被保留。2.8 cat命令:查看文件内容功能:查看文件内容语法:cat [选项] [文件]参数:-b 对非空输出行编号-E 在每行结束处显示$-n 对输出的所有行编号-s 不输出多行空行2.9 pwd命令:显示工作目录功能:在Linux层次目录结构中,用户可以在被授权的任意目录下利用mkdir命令创建新目录,也可以利用cd命令从一个目录转换到另一个目录。然而,没有提示符来告知用户目前处于哪一个目录中。要想知道当前所处的目录,可以使用pwd命令,该命令显示整个路径名。语法:pwd说明:此命令显示出当前工作目录的绝对路径。举例:pwd根目录以开头的“/”表示。如果pwd后面什么都没有,则显示当前所在位置。如果屏幕信息很多,用clear可以清除。2.10 cp命令:拷贝文件/目录功能:将给出的文件或目录拷贝到另一文件或目录中。语法:cp  [选项]  <源文件或目录>  <目标文件或目录>说明:该命令把指定的源文件复制到目标文件或把多个源文件复制到目标目录中。参数:- a 该选项通常在拷贝目录时使用。它保留链接、文件属性,并递归地拷贝目录。- d 拷贝时保留链接。- f 删除已经存在的目标文件而不提示。- i 和f选项相反,在覆盖目标文件之前将给出提示要求用户确认。回答y时目标文件将被覆盖,是交互式拷贝。- r 若给出的源文件是一目录文件,此时cp将递归复制该目录下所有的子目录和文件。此时目标文件必须为一个目录名。一般使用-a参数。举例:2.11 mv命令:改名、移动、文件/目录功能:为文件或目录改名或将文件由一个目录移入另一个目录中。语法:mv [选项]  <源文件或目录>  <目标文件或目录>说明:视mv命令中第二个参数类型的不同(是目标文件还是目标目录),mv命令将文件重命名或将其移至一个新的目录中。当第二个参数类型是文件时,mv命令完成文件重命名,此时,源文件只能有一个(也可以是源目录名),它将所给的源文件或目录重命名为给定的目标文件名。当第二个参数是已存在的目录名称时,源文件或目录参数可以有多个,mv命令将各参数指定的源文件均移至目标目录中。在跨文件系统移动文件时,mv先拷贝,再将原有文件删除,而链至该文件的链接也将丢失。参数:-i 交互方式操作。如果mv操作将导致对已存在的目标文件的覆盖,此时系统询问是否重写,要求用户回答y或n,这样可以避免误覆盖文件。-f 禁止交互操作。在mv操作要覆盖某已有的目标文件时不给任何指示,指定此选项后,i选项将不再起作用。如果所给目标文件(不是目录)已存在,此时该文件的内容将被新文件覆盖。为防止用户在不经意的情况下用mv命令破坏另一个文件,建议用户在使用mv命令移动文件时,最好使用i选项。2.12 chmod命令:修改文件/目录权限功能:改变文件或目录的访问权限语法:chmod  [权限]  <文件或者目录>通过ls -l命令可以查看目录或者文件的详细信息,其中第2~10个字符代表了文件的访问权限,当中的每3个为一组,左边三个字符表示所有者权限,中间3个字符表示与所有者同一组的用户的权限,右边3个字符是其他用户的权限。这三个一组共9个字符,代表的意义如下:其中的权限可以使用数字的组合方式进行表示:数字设定的关键是取值,一开始许多初学者会被搞糊涂,其实很简单,我们将rwx看成二进制数,如果有则有1表示,没有则有0表示,那么rwx r-x r- -则可以表示成为:111 101 100再将其每三位转换成为一个十进制数,就是754。例如,我们想让123.txt这个文件的权限为:我们先根据上表得到权限串为:rwx-rw-rw--,那么转换成二进制数就是111 110 100,再每三位转换成为一个十进制数,就得到764,因此我们执行命令:[root@xiaolong test_code]# chmod 764 123.txt2.13 ln命令:建立符号链接ln连接文件或目录,分为软链接和硬链接。软连接语法:ln -s <源文件> <目标文件> (删除源文件之后,链接变成无效的了),相当于快捷方式。硬链接语法:ln <源文件> <目标文件>(删除源文件之后,目标没有影响)举例:#ln -s a.txt p创建软链接之后通过ls -l命令,可以查看到p带有一个箭头指向a.txt。2.14 eog命令:查看图片功能:打开图片浏览器查看图片。语法:eog  <图片文件>例如:eog 123.png2.15 echo命令:输出调试语句功能:echo命令的功能是在显示器上显示一段文字,一般起到一个提示的作用。语法:echo [参数] <输出的数据>参数:-n :表示在最后输出时不自动换行。说明:使用echo输出字符串时,双引号会自动省略掉。(1)示例:以上两条命令输出的效果一样。(2)示例:如果想要显示符号,需要使用转义符号。2.16 重定义到文件在终端中常用的重定义符号为: > 和 >>其中:>符号表示覆盖、>>符号表示追加。示例:2.17 du命令:查看磁盘/文件的大小功能:查看文件的大小信息。用法: du [参数]  <文件名称>常用参数如下:-k 用1024字节单位计算块数。-h 选择合适的单位计算大小。-b 选择字节单位计算大小。三、文件编辑器相关命令介绍在linux下常用的文本编辑器命令有: vi、vim、gedit等。其中vi与vim命令是基于命令行的编辑器。gedit命令是基于鼠标键盘的编辑器,类似于windows下的记事本。3.1 gedit编辑器介绍gedit编辑器用法示例:打开的界面如下:设置显示行号:打开文件时,如果需要进入到文件的指定行位置,可以在命令的最后面写上需要进入的行号。示例:3.2 vim编辑器介绍vim分为两种状态,即命令状态和编辑状态,在命令状态下,所键入的字符系统均作命令来处理,如:q代表退出,而编辑状态则是用来编辑文本的。当你进入vim时,会首先进入命令状态。在命令状态下,按”i”(插入)或”a”(添加)可以进入编辑状态,在编辑状态,按ESC键进入命令状态。插入文本:a              从光标后面开始添加文本A             从光标所在行的末尾开始添加文本i               从光标前面开始插入文本I              从光标所在行的开始处插入文本o               在目前光标所在的下一行处插入新的一行O              在目前光标所在处的上一行插入新的一行s               删除游标所在字符,并进入编辑模式S              删除游标所在的行,并进入编辑模式r               输入字符,取代光标所在的那一个字符R              一直取代光标所在的字符,直到按下 ESC 为止删除与修改:x              删除光标处的字符dd            删除光标所在的整行3dd          删除光标所在行以及下面的两行D或d$   删除光标到行尾的文本,常用语删除注释语句d^或d0   删除光标到行首的文本光标的移动:h 或 向左方向键(←) → 光标向左移动一个字符j 或 向下方向鍵(↓) → 光标向下移动一个字符k 或 向上方向鍵(↑) → 光标向上移动一个字符l 或 向右方向鍵(→) → 光标向右移动一个字符w              光标往后移一个字b               光标往前移一个字^              光标移动到行首$              光标移动到行尾Ctrl+f     向下翻一页  forwardCtrl+b      向上翻一页  backCtrl+d     向下翻半页  downCtrl+u     向上翻半页  upgg             光标定位到文档头G              光标定位到文档尾H              光标定位到当前页首L              光标定位到当前页的最后一行的行首[n]+         光标向后移动n行,[n]表示一个整数,比如10+[n]-          光标向前移动n行,[n]表示一个整数,比如10+[n]G         光标定位到第n行行首, [n]表示一个整数,比如10+查找与替换:/[str]    查找字符串str,[str]表示要查找的字符串,回车后会加亮显示所有找到的字符串,命令n移动到下一个找到的字符串,命令N移动到上一个找到的字符串示例:  /hello块操作:          v               可视化块选择状态,选中块之后,可以对块进行删除(d),复制(y),剪切(x)        u               撤销上次操作        ctrl + r     恢复上次操作结束编辑:        :q             在未修改文档的情况下退出        :q!            放弃文档的修改,强行退出        :w             保存        :wq          保存并退出其他:        :help 命令       查看该命令的帮助提示        :%!xxd            十六进制模式        :%!xxd -r         返回文本模式如果在编辑过程中不小心按了Ctrl+s,vi会处于僵死状态,按Ctrl+q可以恢复。        执行 vim  +3 main.c      //表示定位到main.c的第3行        执行 vim  +/printf main.c  //表示定位到第一个printf处        在命令模式下输入:new 2.c   //表示再打开一个vi,是横向的 用vnew 2.c 表示纵向        进行切换用Ctrl+w然后再按w即可切换在命令模式中输入gg=G可以自动对齐配置vim显示行号在/etc/vimrc文件中加上以下代码:四、gcc命令基本使用在linux系统下通常使用gcc作为主要编译器。GCC原名为 GNU C语言编译器(GNU C Compiler),因为它原本只能处理 C语言。GCC 很快地扩展,变得可处理 C++。后来又扩展能够支持更多编程语言。使用GCC编译器的时候,我们必须给出一系列必要的调用参数和文件名称。GCC编译器的调用参数大约有100多个,这里只介绍其中最基本、最常用的参数。GCC最基本的用法∶ gcc [参数] [文件名称]常用的参数如下-c 只编译:不链接成为可执行文件,编译器只是由输入的.c等源代码文件生成.o为后缀的目标文件,通常用于编译不包含主程序的子程序文件。-o output_filename:确定输出文件的名称为output_filename,同时这个名称不能和源文件同名。如果不给出这个选项,gcc就给出预设的可执行文件a.out。五、解压缩命令介绍Linux下最常用的打包程序是tar命令,使用tar打出来的包我们常称为tar包,tar包文件的命令通常都是以.tar结尾的,生成tar包后,就可以用其它的程序来进行压缩了。5.1 tar命令介绍        功能:tar是一个压缩解压工具。利用tar,用户可以为某一特定文件创建档案(备份文件),也可以在档案中改变文件,或者向档案中加入新的文件。tar最初被用来在磁带上创建档案,现在,用户可以在任何设备上创建档案,如软盘。利用tar命令,可以把一大堆的文件和目录全部打包成一个文件,这对于备份文件或将几个文件组合成为一个文件以便于网络传输是非常有用的。Linux上的tar是GNU版本的。语法:tar  [主选项+辅选项]  <目标文档>  <源文件或者目录>使用该命令时,主选项是必须要有的,它告诉tar要做什么事情,辅选项是辅助使用的,可以选用。参数:c 创建新的档案文件。如果用户想备份一个目录或是一些文件,就要选择这个选项。r 把要存档的文件追加到档案文件的未尾。例如用户已经作好备份文件,又发现还有一个目录或是一些文件忘记备份了,这时可以使用该选项,将忘记的目录或文件追加到备份文件中。t 列出档案文件的内容,查看已经备份了哪些文件。u 更新文件。就是说,用新增的文件取代原备份文件,如果在备份文件中找不到要更新的文件,则把它追加到备份文件的最后。x 从档案文件中释放文件。注意:c/x/t 仅能存在一个!不可同时存在!辅助选项:b 该选项是为磁带机设定的。其后跟一数字,用来说明区块的大小,系统预设值为20(20*512 bytes)。f 使用档案文件或设备,这个选项通常是必选的。请留意,在 f 之后要立即接档名喔!不要再加参数!k 保存已经存在的文件。例如我们把某个文件还原,在还原的过程中,遇到相同的文件,不会进行覆盖。m 在还原文件时,把所有文件的修改时间设定为现在。M 创建多卷的档案文件,以便在几个磁盘中存放。v 详细报告tar处理的文件信息。如无此选项,tar不报告文件信息。w 每一步都要求确认。z 用gzip来压缩/解压缩文件,后缀名为.gz,加上该选项后可以将档案文件进行压缩,但还原时也一定要使用该选项进行解压缩。j 用bzip2来压缩/解压缩文件,后缀名为.bz2,加上该选项后可以将档案文件进行压缩,但还原时也一定要使用该选项进行解压缩。5.2 tar命令解压/压缩使用范例将/test目录下的所有文件打包为test.tar文件。注意:如果打包的文件或者目录是绝对路径,可能会出现提示:tar: 从成员名中删除开头的“/”在参数中加上-P即可消除。示例:解压打包的.tar文件更新文件就是说,用新增的文件取代原备份文件,如果在备份文件中找不到要更新的文件,则把它追加到备份文件的最后。列出已经打包的文件,可以用于查看已经备份了哪些文件。使用gzip来压缩/解压缩文件使用bzip2来压缩/解压缩文件5.3 ZIP格式压缩/解压linux下提供了zip和unzip程序对ZIP格式压缩包进行处理,zip是压缩程序,unzip是解压程序。它们的参数选项很多,下面只做简单介绍。将所有.jpg的文件压缩成一个zip包将all.zip中的所有文件解压出来常用参数:-r 递 归处理,将指定目录下的所有文件和子目录一并处理。压缩指定目录下的所有文件-g:产生符号调试工具(GNU的gdb)所必要的符号信息,要想对源代码进行调试,我们就必须加入这个选项。-O:对程序进行优化编译、链接,采用这个选项,整个源代码会在编译、链接过程中进行优化处理,这样产生的可执行文件的执行效率可以提高,但是,编译、链接的速度就相应地要慢一些。-O2:比-O更好的优化编译、链接,当然整个编译、链接过程会更慢。-E:仅执行编译预处理;-S:将C代码转换为汇编代码;示例:编译时指定库与头文件路径-L:指定动态库路径。示例:gcc test.c -o app -L/usr/lib-I: 指定头文件存放的路径。示例:gcc test.c -o app -I/usr/include-l: 指定库名称。示例:示例:gcc test.c -o app -lpthread

C语言入门开发: printf、sprintf补0、补空格占位

一般在处理时间的时候,界面上显示,打印输出这些场景下,左边补0或者补空格占位是很常见的。补0或者补空格之后,长度是固定的;这样显示更加美观、不会因为数字变短、变长造成闪烁感。示例代码:int main() printf("%d\n",12345); //正常打印 printf("%10d\n",12345); //右对齐.位数不够,左边自动补空格 printf("%-10d,%c\n", 12345,'A');//左对齐.位数不够,右边自动补空格 printf("%010d\n",12345); //右对齐.位数不够,左边自动补0 //sprintf用法一样. return 0; 输出结果: 12345 12345 12345 ,A 0000012345在vs2017里使用sprintf需要在属性--C/C++---预处理器---增加(_CRT_SECURE_NO_WARNINGS)案例: 将ms时间转为时分秒.  控制位数std::string MStoString(long nMicroSecond) int second = nMicroSecond / 1000; int hours, mins, secs, minSecs; secs = second % 60; mins = (second / 60) % 60; hours = second / 3600; minSecs = nMicroSecond - (hours * 3600 + mins * 60 + secs) * 1000; char buff[1024]; //sprintf数字补0 sprintf(buff,"%02d:%02d:%02d.%02d", hours, mins, secs, minSecs); std::string strTime = buff; return strTime; int main() printf("%s\n", MStoString(50000).c_str()); return 0;

python入门开发:ubuntu下搭建python开发环境(vscode)

系统环境: ubuntu18.04python: 3.8.1  ubuntu系统安装教程: https://xiaolong.blog.csdn.net/article/details/1183950241. 下载vscode安装包先去官网下载Linux下的安装包。下载地址: https://code.visualstudio.com/2. 开始安装vscode3. 设置vscode支持中文语言设置 Visual Studio 支持中文语言,打开 Visual Studio 软件, 再按下 F1 或者 Shift + Ctrl + P:然后在命令行输入 “Configure Display Language”  后回车确认,选择安装语言选项。4. 安装python扩展支持并运行代码测试5. 解决vscode内置终端字体间隔过大问题设置终端的字体为等线字体monospace6. 编写第一个Python程序任何一种编程语言都有自己的一套语法,编译器或者解释器负责把符合语法的程序代码转换成CPU能够执行的机器码然后执行,Python也不例外,也有自己的语法规则和解析器。Python程序是大小写敏感的,如果写错了大小写,程序会报错。python最具特色的就是使用缩进来表示代码块,不需要使用大括号 {}缩进的空格数是可变的,但是同一个代码块的语句必须包含相同的缩进空格数, 缩进的空格数不一致,会导致运行错误。一份Python代码示例:#!/usr/bin/python3 print("hello Python"); if True: print ("True") else: print ("False")缩进的空格数不一致,下面的代码运行会报错:#!/usr/bin/python3 print("hello Python"); if True: print ("True") else: print ("False") #此处代码块没有与上面的代码块对齐 运行报错提示: PS C:\Users\11266> & C:/Users/11266/AppData/Local/Programs/Python/Python38-32/python.exe d:/linux-share-dir/Python/python_code.py File "d:/linux-share-dir/Python/python_code.py", line 7 print ("False") IndentationError: expected an indented block

Python入门开发: windows下搭建python开发环境(vscode)

一、环境介绍操作系统: win10 64位python版本: 3.8IDE:  采用vscode用到的相关安装包CSDN打包下载地址:  https://download.csdn.net/download/xiaolong1126626497/19942575二、 搭建python开发环境2.1 Python版本介绍因为Python是跨平台的,它可以运行在Windows、Mac和各种Linux/Unix系统上。在Windows上写Python程序,放到Linux上也是能够运行的。要开始学习Python编程,首先就得把Python安装到你的电脑里。安装后,你会得到Python解释器(就是负责运行Python程序的),一个命令行交互环境,还有一个简单的集成开发环境。目前,Python有两个版本,一个是2.x版,一个是3.x版,这两个版本是不兼容的。由于3.x版越来越普及,后面就选择 3.x版本进行安装。2.2 在windows下安装Python环境Python官网下载地址: https://www.python.org/downloads/勾上Add Python 3.8 to PATH,然后点“Install Now”即可完成安装。安装完成之后,打开windows的命令行窗口。在命令行运行python,出现下面的提示就表示安装成功。进入终端之后,输入exit()可以退出Python命令行。Python安装成功后,在python交互式环境模式下,可以简单学习一下Python的第一份代码:C:\Users\11266>python Python 3.8.0 (tags/v3.8.0:fa919fd, Oct 14 2019, 19:21:23) [MSC v.1916 32 bit (Intel)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> 100+500 #直接输入有效数字进行运算 >>> 300-100 >>> print("hello python") #打印文本 hello python >>> print('hello python') #打印文本 hello python >>> exit() #退出交互式命令行 C:\Users\11266>2.3 windows下安装VSCode代码编辑器下载地址: https://code.visualstudio.com/安装包下载之后,直接鼠标双击运行。软件安装之后下面设置 Visual Studio 支持中文语言首先打开 Visual Studio 软件, 再按下 F1 或者 Shift + Ctrl + P:然后在命令行输入 Configure Display Language选择安装语言选项。安装之后右下角有提示重启,点击重启即可。新建文本文件,保存的后缀为.py。修改vscode的颜色主题下面介绍更改颜色vscode的内置颜色主题方法。

Linux入门开发: 从0开始搭建ubuntu系统环境(编写第一个C程序)

前言本篇文作为C语言、Linux入门环境搭建参考文章;真正的从0开始搭建Linux环境。现在开发学习阶段,笔记本主要还是使用windows系统,为了方便学习Linux系统,重装系统或者安装双系统都不方便;最方便,也不会对本机产生影响的方式就是安装VMware虚拟机软件,模拟一台计算机,安装自己想要的系统。VMware虚拟机软件功能非常强大,可以安装Linux、Windows、macOS等系统,这样就可以在调试开发阶段用来测试软件在不同环境下的运行情况。我的笔记本安装的win10 64位系统,下面文章中安装先VMware虚拟机,再创建ubuntu18.04系统,再完成ubuntu系统环境配置、最后安装VScode软件编写运行第一个C语言程序。一、windows系统下安装VMware虚拟机软件VMware软件下载地址: https://www.vmware.com/products/workstation-pro/workstation-pro-evaluation.html​下载之后,双击安装包安装即可,路径不要出现中文。二、ubuntu系统介绍Ubuntu是一个以桌面应用为主的Linux操作系统,其名称来自非洲南部祖鲁语或豪萨语的“ubuntu"一词,意思是“人性”“我的存在是因为大家的存在",是非洲传统的一种价值观。Ubuntu基于Debian发行版和Gnome桌面环境,而从11.04版起,Ubuntu发行版放弃了Gnome桌面环境,改为Unity。从前人们认为Linux难以安装、难以使用,在Ubuntu出现后这些都成为了历史。Ubuntu也拥有庞大的社区力量,用户可以方便地从社区获得帮助。作为Linux发行版中的后起之秀,Ubuntu Linux在短短几年时间里便迅速成长为从Linux初学者到资深专家都十分青睐的发行版。由于Ubuntu Linux是开放源代码的自由软件,用户可以登录Ubuntu Linux的官方网址免费下载该软件的安装包。用户在使用过程中,没有人对该软件进行技术维护,用户只能自己解决遇到的技术故障。Ubuntu Linux是由南非人Mark Shuttleworth创办的基于Debian Linux的操作系统,开于2004年10月公布Ubuntu的第一个版本(Ubuntu4.10 Warty Warthog)。Ubuntu适用于笔记本电脑、桌面电脑和服务器,特别是为桌面用户提供尽善尽美的使用体验。Ubuntu几乎包含了所有常用的应用软件:文字处理、电子邮件、软件开发工具和Web服务等。用户下载、使用、分享Ubuntu系统,以及获得技术支持与服务,无需支付任何许可费用。Ubuntu提供了一个健壮、功能丰富的计算环境,既适合家庭使用又适用于商业环境。Ubuntu社区承诺每6个月发布一个新版本,以提供最新最强大的软件。Ubuntu被视为一种传统的非洲民族理念,同时也被认为是南非共和国的建国准则之一,并且与非洲复兴的理想密切相关。该词源于祖鲁语和科萨语,它的核心理念是“人道待人”,着眼于人们之间相互的忠诚与交流。南非总统曼德拉这样解释:Ubuntu是一个概念,它包含了尊重、互助、分享、交流、关怀、信任、无私的众多内涵:Ubuntu是一种生活方式,提倡宽容和同情他人。可见,Ubuntu精神已经渗透到了南非的政治和日常生活当中。Ubuntu精神与软件开源精神恰恰不谋而合。作为一个基于Linux的操作系统,Ubuntu Linux试图将这种精神延伸到计算机世界“软件应当被分享,井能够为任何需要的人所获得”。Ubuntu的目标是让世界上的每个人都能得到一个易于使用的Linux版本,不论他所处的地理位置和身体状况。在这种Ubuntu精神的指导下,Ubuntu Linux承诺如下所示:Ubuntu将永远免费,包括企业版和安全升级。Ubuntu将全球数百个公司提供商业支持。Ubuntu包含由自由软件团体提供的最佳翻译和本地化。Ubuntu光盘仅仅包含自由软件,鼓励用户使用自由和开源软件,并改善和传播它。Ubuntu在桌面办公、服务器方面有着不俗的表现,总能够将最新的应用特性囊括其中,主要包括以下几方面:1、桌面系统使用最新的Gnome、KDE、Xfce等桌面环境组件。2、集成搜索工具Tracker,为用户提供方便、智能的桌面资源搜索。3、抛弃繁琐的X桌面配置流程,可以轻松使用图形化界面完成复杂的配置。4、集成最新的Compiz稳定版本,让用户体验酷炫的3D桌面。5、“语言选择”程序提供了常用语言支持的安装功能,让用户可以在系统安装后,方便地安装多语言支持软件包。6、提供了全套的多媒体应用软件工具,包括处理音频、视频、图形、图像的工具。7、集成了Openffice办公套件,帮助用户完成文字处理、电子表格、幻灯片播放等日常办公任务。8、含有辅助功能,为残障人士提供辅助性服务,例如,为存在弱视力的用户提供屏显键盘,能够支持Windows NTFS分区的读/写操作,使Windows资源完全共享成为可能。9、支持蓝牙(Bluetooth)输入设备,如蓝牙鼠标、蓝牙键盘。10、拥有成熟的网络应用工具,从网络配置工具到Firefox网页浏览器、Gaim即时聊天工具、电子邮件工具、BT下载工具等。11、加入更多的打印机驱动,包括对HP的一体机(打印机、扫描仪集成)的支持。12、进一步加强系统对笔记本电脑的支持,包括系统热键以及更多型号笔记本电脑的休眠与唤醒功能。13、与著名的开源软件项目LTSP合作,内置了Linux终端服务器功能,提高老式PC机的利用率。三、安装ubuntu系统搭建环境3.1 下载ubuntu系统18.04最新长期支持版本: http://mirrors.aliyun.com/ubuntu-releases/18.04/3.2 在vmware虚拟机上安装ubuntu18.04​​四、ubuntu下安装VSCode代码编辑器先去官网下载Linux下的安装包。下载地址: https://code.visualstudio.com/设置 Visual Studio 支持中文语言,打开 Visual Studio 软件, 再按下 F1 或者 Shift + Ctrl + P:然后在命令行输入 “Configure Display Language”  后回车确认,选择安装语言选项。解决vs code 内置终端,字体间隔过大问题。设置终端的字体为等线字体monospace

C语言入门开发:Windows下安装vscode编写C语言代码

一、Visual Studio Code 介绍Microsoft在2015年4月30日Build 开发者大会上正式宣布了 Visual Studio Code 项目:一个运行于 Mac OS X、Windows和 Linux 之上的,针对于编写现代 Web 和云应用的跨平台源代码编辑器。下载地址: https://code.visualstudio.com/二、Visual Studio Code 安装安装包下载之后,直接鼠标双击运行。软件安装之后下面设置 Visual Studio 支持中文语言首先打开 Visual Studio 软件, 再按下 F1 或者 Shift + Ctrl + P:然后在命令行输入 Configure Display Language选择安装语言选项。安装之后右下角有提示重启,点击重启即可。​​软件安装之后,新建一个.c文件保存到指定目录下;这时候软件的右下角会提示安装 C/C++的扩展支持,点击安装即可。下面介绍更改颜色vscode的颜色主题。三、下载安装GCC编译器3.1 Mingw-w64编译器下载VS Code只是一个编辑器,并不是IDE(集成开发环境);不含编译器(和许多其它功能),要编译C/C++程序,需要单独下载编译器。在Windows 下一般使用Mingw-w64工具集,搭建 C 语言开发环境;Mingw-w64提供在 Windows下的 C 语言开发环境,工具集包含了头文件、库、运行时和一些工具,支持64位开发,是 MinGW 的升级项目。其实MinGW和MinGW-w64只是名字像,它们是两个不同的项目;MinGW本身已经很久没有更新了,故不推荐。Vscode官方的说明与使用文档: https://code.visualstudio.com/docs/languages/cppWindow下MinGW离线安装包下载地址:https://download.csdn.net/download/xiaolong1126626497/19942191注意: 这是win10_64位系统的MinGW。下载的压缩包名称: i686-8.1.0-release-posix-dwarf-rt_v6-rev0.7z3.2 添加编译器路径到系统环境变量将压缩包解压到指定目录下,推荐存放到C盘,在C盘创建一个名称为“MinGW”的目录。将gcc/g++可执行文件的路径添加到系统环境变量中。3.3 测试编译器打开vscode编写代码,写完代码在终端编译运行。3.4 vs code 终端PowerShell介绍Windows下Vs code的内置终端使用的就是PowerShell。PowerShell,从名字可以知道,他首先是一个shell,shell的意思就是和Linux的bash等一样、和原来的cmd一样就是在里边敲命令(可执行文件)使用;而Power就意味他是一个功能强大的shell,从面向用户而言,个人觉得其功能强大体现在以下几方面:(1) 微软态度。微软是真正的在推行PowerShell,包括Office等更多自家软件,底层都是调用PowerShell来实现。(2) 兼容性cmd。PowerShell包含原先cmd的所有命令,原先命令使用形式不变,在是在其基础上添加命令。(3) 对标Linux。PowerShell使用了Linux Shell的思想,也就是所有的系统操作、配置,都可以在shell中敲写命令实现。(4) 统一的命令格式和自包含的文档。基于前3点我们即可以说PowerShell已可与Linux Bash等一较高下,如果再加上后发优势那就可以让人相信PowerShell可以成功。当然powershell很好,但也有着其劣势:(1)Linux和Windows系统本身定位的差异。Linux的免费稳定使其牢牢占据了服务器领域,Linux Shell命令是没有很多统一格式的;因此工程师们要费很大的劲去学习,而当Linux占据工程师的大部分精力并塑造完他们的习惯后,工程师们也许并没有那么多精神和动力去学powershell。(2)来自Windows GUI的竞争。Windows上命令能干的事GUI也都能干也许效率慢一些,但普通用户可不想去黑漆漆的界面敲感觉不受控制的命令。

STM32入门开发: LWIP网络协议栈移植(网卡采用DM9000)

一、环境介绍MCU: STM32F103ZET6代码开发工具: Keil5TCP/IP协议栈: LWIP网卡: DM9000本篇文章主要讲解如何在STM32F103工程里添加移植LWIP协议,最终完成TCP服务器、TCP客户端的通信测试。 网卡采用的是DM9000,工程代码中,采用STM32的FSMC接口来驱动DM900网卡,DM9000是并口网卡,引脚多,但是速度快,也可以采用其他网卡,SPI协议的、UART协议的等。 比如:ENC28J60。 因为主要是讲LWIP协议栈的移植,所以网卡相关的代码就没有细说(需要准备一个网卡可以正常通信的工程,再移植)。工程源码、LWIP资料包下载地址:https://download.csdn.net/download/xiaolong1126626497/19907087资料包里的内容如下:二、D9000网卡2.1 DM9000简介    DM9000 是一款完全集成的、性价比高、引脚数少、带有通用处理器接口的单芯片快速以太网控制器。 自带一个 10/100M PHY 和 4K 双字的 SRAM ,DM9000A 为适应各种处理器提供了8位、16 位数据接口访问内部存储器,DM9000拥有自动协商功能,DM9000特性如下:1、集成自适应10/100M收发器。2、内置16k字节的SRAM。3、支持硬件帧校验。4、兼容3.3V和5.0V输入输出电压。DM9000 有多种型号,有 100 引脚和 48 引脚的, 开发板选择的是 48 引脚的 DM9000,型号为 DM9000CEP。2.2 DM9000 中断引脚电平设置DM9000的34(INT)引脚为中断输出引脚,默认情况下该引脚高电平有效。可以通过设置DM9000 的 20(EECK)引脚来改变 INT 的有效电平,当 EECK 拉高以后, INT 低电平有效,否则的话 INT 是高电平有效的。开发板上 R66 电阻为 EECK 的上拉电阻,因此开发板上 DM9000 的 INT 引脚是低电平有效的。2.3 DM9000 数据位宽设置前面我们提了一下 DM9000 支持 8 位和 16 位两种数据位宽,可以通过 DM9000 的 21(EECS)引脚设置其数据位宽,当 EECS 上拉的时候 DM9000 选择 8 位数据位宽,否则的话选择 16 位数据位宽。开发板上的 R65 电阻为 EECS 的上拉电阻,但是此电阻并未焊接! DM9000 芯片的数据位宽为 16 位。2.4 DM9000寄存器表2.5 DM9000常用寄存器介绍NCR、 NSR、 TCR、 RCR、 FCTR、 BPTR、 TCR2、 ISR、 IMR。NCR(网络控制寄存器)寄存器FCOL:强制冲突模式,用于检测。FDX:内部 PHY 全双工模式。LBK:回环模式(LoopBack)00 正常;01 MAC 内部回环;10 内部 PHY100M 模式数字回环;11 保留;RST:置 1 软件复位, 10us 后自动清零。NSR 寄存器(网络状态寄存器)SPEED:网络速度,在使用内部 PHY 情况下,0 表示 100Mbps,1 表示 100Mbps,当 LINKST=0时,此位无意义。LINKST:连接状态, 0 为连接失败, 1 位已连接。TX2END: TX(发送)数据包 2 完成标志,读取或写 1 将清零该位。TX1END: TX(发送)数据包 1 完成标志,读取或写 1 将清零该位。RXOV: RX(接收)FIFO 溢出标志。TCR 寄存器(发送控制寄存器)TJDIS: Jabber 传输禁止。1,禁止 Jabber 传输定时器(2048 字节)。0,使能。EXCECM:严重冲突模式控制0,当冲突计数多于 15 则终止本次数据包。1,始终尝试发送本次数据包。PAD_DIS2:禁止为数据包 II 添加填充。CRC_DIS2:禁止为数据包 II 添加 CRC 校验。PAD_DIS1:禁止为数据包 I 添加填充。CRC_DIS1:禁止为数据包 I 添加 CRC 校验。TXREQ: TX(发送)请求,发送完成后自动清零该位RCR 寄存器(发送控制寄存器)WTDIS:看门狗定时器(2048 字节)禁止。1,进制0,使能DIS_LONG:丢弃长数据包, 1,丢弃数据包长度超过 1522 字节的数据包。DIS_CRC:丢弃 CRC 校验错误数据包。ALL:允许广播。RUNT:允许小于最小长度的数据包。PRMSC:各种模式。RXEN:接收使能。FCTR 寄存器(流控制阈值寄存器)HWOT:RX FIFO 缓存高位溢出门限当 RX SRAM 空闲空间小于该门限值时则发送一个暂停时间为 FFFFH 的暂停包,若该值为 0,则无接收控件。 1=1k 字节,默认值为 3H,即 3K 字节空闲空间,不要超过 S RAM 大小。LWOT:RX FIFO 缓存低位溢出门限当 RX SRAM 空闲空间大于该门限值时则发送一个暂停时间为 0000H 的暂停包。当溢出门限最高值的暂停包发送之后,溢出门限最低值的暂停包才有效,默认值为 8K,不要超过 SRAM 大小。BPTR 寄存器(背压阈值寄存器)BPHW:背压阈值最高值当接收 SRAM 空闲空间低于该阈值,则 MAC 将产生一个拥挤状态, 1=1k 字节。默认值为 3H,即 3K 字节空闲空间,不要超过 SRAM 大小。JPT:拥挤状态时间,模式为 200us, JPT 值与其对应的拥挤状态时间表TCR2 寄存器(发送控制寄存器 2)LED: LED 模式1,设置 LED 引脚为模式 10,设置 LED 引脚为模式 0 或根据 EEPROM 的设定。RLCP:重试冲突延时数据包, 1 重新发送有冲突延迟的数据包。DTU: 1 禁止重新发送“underruned”数据包。ONEPM:单包模式。1,发送完成前发送一个数据包的命令能被执行。0,发送完成前发送最多两个数据包的命令能被执行。IFGS:帧间间隔设置。0XXX 为 96bit, 1000 为 64bit, 1001 为 72bit1010 为 80bit, 1011 为 88bit, 1100 为 96bit1101 为 104bit, 1110 位 112bit, 1111 为 120bitISR 寄存器(中断状态寄存器)IOMODE: 0,16 位模式; 1,8 位模式。LNKCHG:连接状态改变。UDRUN:发送“Underrun”ROO:接收溢出计数器溢出ROS:接收溢出。PT:数据包发送。PR:数据包接收。IMR 寄存器(中断状态寄存器)PAR:使能 SRAM 的读/写指针在指针地址超过 SRAM 的大小时自动跳回起始位置。需要驱动程序设置该位,若设置该位, REG_F5 将自动置为 0XH。LNKCHGI:使能连接状态改变中断。UDRUNI:使能发送“Underrun”中断。ROOI:使能接收溢出计数器溢出中断。ROI:使能接收溢出中断。PTI:使能数据包发送中断。PRI:使能数据包接收中断。2.6 DM9000 直接内存访问控制(DMAC)DM9000 直接内存访问控制(DMAC)DM9000 支持 DMA 方式简化对内部存储器的访问。在我们编程写好内部存储器地址后,就可以用一个读/写命令伪指令把当前数据加载到内部数据缓冲区,这样,内部存储器指定位置就可以被读/写命令寄存器访问。存储器地址将会自动增加,增加的大小与当前总线操作模式相同(比如:8-bit、 16-bit 或 32-bit),接着下一个地址数据将会自动加载到内部数据缓冲区。要注意的是在连续突发式第一次访问的数据应该被忽略,因为,这个数据是最后一次读写命令的内容。内部存储器空间大小 16K 字节。前 3K 字节单元用作发送包的缓冲区,其他 13K 字节用作接收包的缓冲区。所以在写存储器操作时,如果地址越界(即超出 3K 空间),在 IMR 寄存器 bit7 置位的情况下,地址指针将会返回到存储器 0 地址处。同样,在读存储器操作时,如果地址越界(即超出 16K 空间),在 IMR 寄存器 bit7 置位的情况下,地址指针将会返回到存储器 0x0C00 地址处。DM9000 数据包发送DM9000 有两个发送数据包: index1 和 index2,同时存储在 TX SRAM 中。发送控制寄存器(02h)控制循环冗余校验码(CRC)和填充(pads)的插入,其状态分别记录在发送状态寄存器I(03H)和发送状态寄存器 II(04H)中。发送器的起始地址为 0x00H,在软件或硬件复位后,默认的数据发送包为 index1。首先,使用 DMA 端口将数据写 TX SRAM 中,然后,在发送数据包长度寄存器中把数据字节数写入字节计数寄存器。置位发送控制寄存器的 bit0 位,则 DM9000 开始发送 index1 数据包。在 index1数据包发送结束之前,数据发送包 index2 被移入 TX SRAM 中。在 index1 数据包发送结束后,将 index2 数据字节数写入字节计数寄存器中,然后,置位发送控制寄存器的 bit0 位,则 index2数据包开始发送。以此类推,后面的数据包都以此方式进行发送。DM9000 数据包接收RX SRAM 是一个环形数据结构。在软件或硬件复位后, RX SRAM 的起始地址为 0X0C00。每个接收数据包都包含有 CRC 校验域,数据域,以及紧跟其后的 4 字节包头域。 4 字节包头格式为: 01h、状态、 BYTE_COUNT 低、 BYTE_COUNT 高。请注意:每个接收包的起始地址处在适当的地址边界,这取决于当前总线操作模式(8bit 或者 16bit)2.7 DM9000原理图介绍各信号线描述如下:PWRST: DM9000 复位信号。CS: DM9000 的片选信号。WR(IOW): 处理器写命令。RD(IOR): 处理器读命令。CMD: 命令/数据标志, 0,读写命令; 1,读写数据。SD0~SD15: 16 位双向数据线。信号线对应的GPIO口对应关系FSMC接口框图DM9000网卡接在FSMC的第2块上,数据线地址: 0x64000000PA7地址线作为命令与数据线切换引脚。外接16位宽度存储器:HADDR[25:1]  FSMC_A[24:0]外接8位宽度存储器: HADDR[25:0]  FSMC_A[25:0]2.8 DM9000时序图介绍IOR和IOW是DM9000的读写选择引脚,低电平有效,即低电平时进行读(IOR)写(IOW)操作;AEN是芯片选通引脚,低电平有效,该引脚为低时才能进行读写操作;CMD的命令/数据切换引脚,低电平时读写命令操作,高电平时读写数据操作。读时序:写时序:三、LWIP(TCP/IP)网络协议栈介绍根据以太网帧头携带的上层协议类型值传递数据。以太网帧格式定义:目的MAC地址 源MAC地址           类型/长度                  数据                     校验   6字节          6字节               2字节                   46-1500字节              4字节 ip:0x0800 ARP:0x0806最大帧长1518字节 最小字节64字节3.1 LWIP介绍lwip是瑞典计算机科学院网络嵌入式系统小组(SICS)的Adam Dunkels(亚当·邓克尔) 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。LwIP是Light Weight (轻型)IP协议,有无操作系统的支持都可以运行。LwIP实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用,它只需十几KB的RAM和40K左右的ROM就可以运行,这使LwIP协议栈适合在低端的嵌入式系统中使用。lwip提供三种API:1)RAW API  2)(NETCONN)lwip API  3)BSD API。RAW 编程接口使得程序效率高,但是需要对 LWIP 有深入的了解,而且不适合大数据量等场合。 NETCONN 编程接口,使用 NETCONN API 时需要有操作系统的支持。RAW API把协议栈和应用程序放到一个进程里边,该接口基于函数回调技术,使用该接口的应用程序可以不用进行连续操作。不过,这会使应用程序编写难度加大且代 码不易被理解。为了接收数据,应用程序会向协议栈注册一个回调函数。该回调函数与特定的连接相关联,当该关联的连接到达一个信息包,该回调函数就会被协议 栈调用。这既有优点也有缺点。优点是既然应用程序和TCP/IP协议栈驻留在同一个进程中,那么发送和接收数据就不再产生进程切换。主要缺点是应用程序不 能使自己陷入长期的连续运算中,这样会导致通讯性能下降,原因是TCP/IP处理与连续运算是不能并行发生的。这个缺点可以通过把应用程序分为两部分来克 服,一部分处理通讯,一部分处理运算。lwip API把接收与处理放在一个线程里面。这样只要处理流程稍微被延迟,接收就会被阻塞,直接造成频繁丢包、响应不及时等严重问题。因此,接收与协议处理必须 分开。LwIP的作者显然已经考虑到了这一点,他为我们提供了 tcpip_input() 函数来处理这个问题, 虽然他并没有在 rawapi 一文中说明。 讲到这里,读者应该知道tcpip_input()函数投递的消息从哪里来的答案了吧,没错,它们来自于由底层网络驱动组成的接收线程。我们在编写网络驱动时, 其接收部分以任务的形式创建。 数据包到达后, 去掉以太网包头得到IP包, 然后直接调用tcpip_input()函数将其 投递到mbox邮箱。投递结束,接收任务继续下一个数据包的接收,而被投递得IP包将由TCPIP线程继续处理。这样,即使某个IP包的处理时间过长也不 会造成频繁丢包现象的发生。这就是lwip API。BSD API提供了基于open-read-write-close模型的UNIX标准API,它的最大特点是使应用程序移植到其它系统时比较容易,但用在嵌入式系统中效率比较低,占用资源多。这对于我们的嵌入式应用有时是不能容忍的lwIP协议栈主要关注的是怎么样减少内存的使用和代码的大小,这样就可以让lwIP适用于资源有限的小型平台例如嵌入式系统。为了简化处理过程和内存要求,lwIP对API进行了裁减,可以不需要复制一些数据。其主要特性如下:(1)支持多网络接口下的IP转发;(2)支持ICMP协议;(3)包括实验性扩展的UDP(用户数据报协议);(4)包括阻塞控制、RTT 估算、快速恢复和快速转发的TCP(传输控制协议);(5)提供专门的内部回调接口(Raw API),用于提高应用程序性能;(6)可选择的Berkeley接口API (在多线程情况下使用) 。(7)在最新的版本中支持ppp(8) 新版本中增加了的IP fragment(IP分片)的支持.(9) 支持DHCP协议,动态分配ip地址.3.2 几种开源TCPIP协议概述1、BSD TCP/IP协议栈    BSD栈历史上是商业栈的起点,大多数专业TCP/IP栈(VxWorks内嵌的TCP/IP栈)是BSD栈派生的。这是因为BSD栈在BSD许可协议下提供了这些专业栈的雏形,BSD许用证允许BSD栈以修改或未修改的形式结合这些专业栈的代码而无须向创建者付版税。同时,BSD也是许多TCP/IP协议中的创新(如广域网中饿拥塞控制和避免)的点。2、uC/IP    uC/IP是由Guy Lancaster编写的一套基于uC/OS且开放源码的TCP/IP协议栈,亦可移植到操作系统,是一套完全免费的、可供研究的TCP/IP协议栈,uC/IP大部分源码是从公开源码BSD发布站点和KA9Q(一个基于DOS单任务环境运行的TCP/IP协议栈)移植过来。uC/IP具有如下一些特点:带身份验证和报头压缩支持的PPP协议,优化的单一请求/回复交互过程,支持IP/TCP/UDP协议,可实现的网络功能较为强大,并可裁减。UCIP协议栈被为一个带最小化用户接口及可应用串行链路网络模块。根据采用CPU、编译器和系统所需实现协议的多少,协议栈需要的代码容量空间在30-60KB之间。http://ucip.sourceforge.net3、LwIP     LwIP是瑞士计算机科学院(Swedish Institute of Computer Science)的Adam Dunkels等开发的一套用于嵌入式系统的开放源代码TCP/IP协议栈。LwIP的含义是Light Weight(轻型)IP协议,相对于uip。LwIP可以移植到操作系统上,也可以在无操作系统的情况下独立运行。LwIP TCP/IP实现的重点是在保持TCP协议主要功能的基础上减少对RAM的占用,一般它只需要几十K的RAM和40K左右的ROM就可以运行,这使LwIP协议栈适合在低端嵌入式系统中使用。LwIP的特性如下:支持多网络接口下的IP转发,支持ICMP协议 ,包括实验性扩展的的UDP(用户数据报协议),包括阻塞控制,RTT估算和快速恢复和快速转发的TCP(传输控制协议),提供专门的内部回调接口(Raw API)用于提高应用程序性能,并提供了可选择的Berkeley接口API。http://www.sics.se/~adam/lwip/或http://savannah.nongnu.org/projects/lwip/4、uIP    uIP是专门为8位和16位控制器设计的一个非常小的TCP/IP栈。完全用C编写,因此可移植到各种不同的结构和操作系统上,一个编译过的栈可以在几KB ROM或几百字节RAM中运行。uIP中还包括一个HTTP服务器作为服务内容。许可:BSD许用证http://www.sics.se/~adam/uip/uIP是一个完全由C语言编写的开源软件, 它的文档和源代码可用于商业和非商业用途, 它已经移植到了大部分的8位微控制器, 而且已在很多的嵌入式产品和项目中使用.5、TinyTcp   TinyTcp 栈是TCP/IP的一个非常小和简单的实现,它包括一个FTP客户。TinyTcp是为了烧入ROM设计的并且现在开始对大端结构似乎是有用的(初始目标是68000芯片)。TinyTcp也包括一个简单的以太网驱动器用于3COM多总线卡http://ftp.ecs.soton.ac.uk/pub/elks/utils/tiny-tcp.txt选择一个开源协议栈可以从四个方面来考虑:  是否提供易用的底层硬件API,即与硬件平台的无关性;协议栈需要调用的系统函数接口是否容易构造,另一个对于应用支持程度。 最关键的是占用的系统资源是否在可接受范围内,有裁减优化的空间否? 其中,BSD 栈可完整实现TCP/IP协议,但代码庞大,70KB-150KB之间,裁减优化有难度, uIP和TinyTcp代码容量小巧,实现功能精简,限制了在一些较高要求场合下的应用,如可靠性与大容量数据传输。LwIP和uC/IP是同量级别的两个开源协议栈,两者代码容量和实现功能相似,LwIP没有操作系统针对性,它将协议栈与平台相关的代码抽象出来,用户如果要移植到自己的系统,需要完成该部分代码的封装,并为网络应用支持提供了API接口的可选性。 uC/IP协议最初是针对uC/OS设计,为方便用户移植实现,同样也抽象了协议栈与平台相关代码,但是协议栈所需调用的系统函数大多参照uC/OS内核函数原型设计,并提供了协议栈的函数,方便用户参考,其不足在于该协议栈对网络应用支持不足。 根据以上分析,从应用和开发的角度看,似乎LWIP更得到了网上很多朋友使用的青睐;uC/IP在文档支持与软件升级管理上有很多不足,但是它最初是针对UC/OS而设计,如果选用UC/OS作为软件基础的话,在系统函数构造方面有优势。当然你选择其他操作系统的话,可参照OS_NULL文件夹下的文件修改。 以上的这些开源协议栈也并非免费,拿来就可以用,据我所知,UC/OS的母公司推出UC/OS-TCP/IP花了6人*2年的工作量,国内某公司使用LWIP作为移植的参照,花了4-5人*2年的工作量来测试与优化协议,使用商用TCP/IP栈的高费用就不足为奇了。 作为广大的爱好者学习而言,如果只是跑跑原型,实验一下效果,以上的几种开源协议栈都提供了测试的例子,应该是不错的选择。终上所述:LWIP可优先考虑,参考的资料较多四、LWIP协议栈移植4.1  LWIP源码下载源码下载地址: http://ftp.yzu.edu.tw/nongnu/lwip/下载LWIP1.4.1版本、并下载contrib-1.4.1版本。4.2 将LWIP源码加入到工程目录4.3 配置lwipopts.h文件4.4 修改ethernetif.c文件ethernetif.c文件默认是不编译的,该文件是网卡底层接口的模板文件,需要根据修改网卡发送接口和接收接口。4.5 修改sys_arch.c文件修改sys_arch.c只是留下sys_now()函数,其他代码全部删除掉。删除windows.h头文件。sys_now()函数用于返回一个32位的系统时钟,单位是ms。没有操作系统的情况下,使用定时器提供时间即可。4.6 新建lwip_config.c文件在LWIP/app目录下新建一个lwip_config.c/lwip_config.h文件。用于编写动态IP地址分配处理代码,和LWIP事物轮询、初始化代码。编写一个LWIP初始化配置函数,向LWIP协议栈添加一个新的网卡设备1./* 函数功能: LWIP协议栈初始化 void lwip_config_init(void) ip_addr_t ipaddr; //IP地址 ip_addr_t netmask; //子网掩码 ip_addr_t gw; //网关 //全部初始化为0 -因为使用了动态IP地址分配 ipaddr.addr=0; netmask.addr=0; gw.addr=0; /*1. 初始化LWIP内核*/ lwip_init(); /*2. 向网卡列表中添加一个网络设备*/ netif_add(&lwip_netif,&ipaddr,&netmask,&gw,NULL,&ethernetif_init,&ethernet_input); /*3. 开启DHCP服务 */ dhcp_start(&lwip_netif); /*4. 设置netif为默认网口*/ netif_set_default(&lwip_netif); /*5. 打开netif网口*/ netif_set_up(&lwip_netif); }编写LWIP事物轮询函数与DHCP处理函数u32 TCPTimer=0; //TCP查询计时器 u32 ARPTimer=0; //ARP查询计时器 u32 DHCPfineTimer=0; //DHCP精细处理计时器 u32 DHCPcoarseTimer=0; //DHCP粗糙处理计时器 u32 DHCP_State=1; //保存DHCP状态 1表示没有分配成功 0表示分配成功 函数功能: LWIP轮询任务 void lwip_periodic_handle() //每250ms调用一次tcp_tmr()函数 if(TCPTimer >= TCP_TMR_INTERVAL) TCPTimer=0; tcp_tmr(); //处理TCP协议请求 //ARP每5s周期性调用一次 if(ARPTimer >= ARP_TMR_INTERVAL) ARPTimer=0; etharp_tmr(); //每500ms调用一次dhcp_fine_tmr() if(DHCPfineTimer >= DHCP_FINE_TIMER_MSECS) DHCPfineTimer=0; dhcp_fine_tmr(); //动态IP地址分配的事物处理 if(DHCP_State)lwip_dhcp_process_handle(); //DHCP处理 //每60s执行一次DHCP粗糙处理 if(DHCPcoarseTimer >= DHCP_COARSE_TIMER_MSECS) DHCPcoarseTimer=0; dhcp_coarse_tmr(); //lwip控制结构体 typedef struct u8 remoteip[4]; //服务器主机IP地址 u8 ip[4]; //本机IP地址 u8 netmask[4]; //子网掩码 u8 gateway[4]; //默认网关的IP地址 }__lwip_dev; extern __lwip_dev lwipdev; //lwip信息结构体 __lwip_dev lwipdev; //lwip信息结构体 函数功能: DHCP处理任务 void lwip_dhcp_process_handle(void) u32 ip=0,netmask=0,gw=0; ip=lwip_netif.ip_addr.addr; //读取新IP地址 netmask=lwip_netif.netmask.addr; //读取子网掩码 gw=lwip_netif.gw.addr; //读取默认网关 if(ip!=0) //正确获取到IP地址的时候 DHCP_State=0; //表示分配成功 //解析出通过DHCP获取到的IP地址 lwipdev.ip[3]=(uint8_t)(ip>>24); lwipdev.ip[2]=(uint8_t)(ip>>16); lwipdev.ip[1]=(uint8_t)(ip>>8); lwipdev.ip[0]=(uint8_t)(ip); printf("动态分配 IP:..............%d.%d.%d.%d\r\n",lwipdev.ip[0],lwipdev.ip[1],lwipdev.ip[2],lwipdev.ip[3]); //解析通过DHCP获取到的子网掩码地址 lwipdev.netmask[3]=(uint8_t)(netmask>>24); lwipdev.netmask[2]=(uint8_t)(netmask>>16); lwipdev.netmask[1]=(uint8_t)(netmask>>8); lwipdev.netmask[0]=(uint8_t)(netmask); printf("子网掩 码............%d.%d.%d.%d\r\n",lwipdev.netmask[0],lwipdev.netmask[1],lwipdev.netmask[2],lwipdev.netmask[3]); //解析出通过DHCP获取到的默认网关 lwipdev.gateway[3]=(uint8_t)(gw>>24); lwipdev.gateway[2]=(uint8_t)(gw>>16); lwipdev.gateway[1]=(uint8_t)(gw>>8); lwipdev.gateway[0]=(uint8_t)(gw); printf("网 关.........%d.%d.%d.%d\r\n",lwipdev.gateway[0],lwipdev.gateway[1],lwipdev.gateway[2],lwipdev.gateway[3]); }4.7 配置一个定时器提供时间基准4.8 初始化lwip动态获取IP地址​4.9 LWIP内存配置选择LWIP可以选择使用系统库自带的函数malloc/free进行管理空间,也可以使用lwip自己的内存管理函数进行管理,源码默认就是使用lwip自己的内存管理方法,就是在初始化内存的时候定义一个数组,数组的大小在lwipopts.h文件MEM_SIZE宏定义的。五、LWIP函数使用(RAW编程接口)5.1  LWIP初始化配置ip_addr_t ipaddr; //IP地址 ip_addr_t netmask; //子网掩码 ip_addr_t gw; //网关 //全部初始化为0 -因为使用了动态IP地址分配 ipaddr.addr=0; netmask.addr=0; gw.addr=0; /*1. 初始化LWIP内核*/ lwip_init(); /*2. 向网卡列表中添加一个网络设备*/ netif_add(&lwip_netif,&ipaddr,&netmask,&gw,NULL,&ethernetif_init,&ethernet_input); /*3. 开启DHCP服务 */ dhcp_start(&lwip_netif); /*4. 设置netif为默认网口*/ netif_set_default(&lwip_netif); /*5. 打开netif网口*/ netif_set_up(&lwip_netif); 5.2 LWIP轮询函数处理LWIP轮询期间:1. 推荐每250ms周期性调用一次tcp_tmr()函数,处理TCP协议请求。    超时时间LWIP使用TCP_TMR_INTERVAL宏进行了定义。2. 推荐每5s周期性调用一次etharp_tmr()函数,清除ARP表中过期的数据。    超时时间LWIP使用ARP_TMR_INTERVAL宏进行了定义。3. (如果开启了动态IP分配功能)推荐每500ms周期性调用一次dhcp_fine_tmr()函数,处理DHCP动态IP地址分配请求。  如果IP地址获取成功,将会放在初始化时注册的网络设备结构体里(struct netif)。    超时时间LWIP使用DHCP_FINE_TIMER_MSECS宏进行了定义。4. (如果开启了动态IP分配功能)推荐每60s调用一次dhcp_coarse_tmr()函数,用于检查DHCP租约时间,并进行重新绑定。     超时时间LWIP使用DHCP_COARSE_TIMER_MSECS宏进行了定义。5. 在LWIP运行期间,当网卡收到数据时,还需要调用ethernetif_input函数读取网卡数据。在函数ethernetif_input()主要完成两个工作1、调用low_level_input(); 读取网卡实际数据。2、调用netif->input();所以,为了能够实时的读取数据,需要最快的速度轮询调用ethernetif_input函数。5.3  LWIP编程RAW接口函数tcp_new() 创建一个 TCP 的 PCB 控制块 tcp_bind() 为 TCP 的 PCB 控制块绑定一个本地 IP 地址和端口号 tcp_listen() 开始 TCP 的 PCB 监听 tcp_accept() 控制块 accept字段注册的回调函数,侦听到连接时被调用 tcp_accepted() 通知 LWIP 协议栈一个 TCP 连接被接受了 tcp_conect() 连接远端主机 tcp_write() 构造一个报文并放到控制块的发送缓冲队列中 tcp_sent() 控制块 sent 字段注册的回调函数,数据发送成功后被回调 tcp_output() 将发送缓冲队列中的数据发送出去 tcp_recv()控制块 recv 字段注册的回调函数,当接收到新数据时被调用 tcp_recved()当程序处理完数据后一定要调用这个函数,通知内核更新接收窗口 tcp_poll() 控制块 poll 字段注册的回调函数,该函数周期性调用 tcp_close() 关闭一个 TCP 连接 tcp_err() 控制块 err 字段注册的回调函数,遇到错误时被调用 tcp_abort() 中断 TCP 连接5.4 创建TCP服务器示例下面演示了TCP服务器创建步骤,测试服务器是否正常。u8 TCP_Create(u16_t port) struct tcp_pcb *pcb=NULL; pcb=tcp_new(); //创建套接字 if(pcb==NULL)return 1; if(tcp_bind(pcb,IP_ADDR_ANY,port)!=ERR_OK)return 2; //绑定端口号 pcb=tcp_listen(pcb); //开始监听 tcp_accept(pcb,TCP_accept);//等待连接 return 0; err_t TCP_accept(void *arg, struct tcp_pcb *newpcb, err_t err) u8 addr[4]; //tcp_setprio(newpcb, TCP_PRIO_MIN); 设置优先级 printf("有新的客户端连接!\n"); addr[3]=(newpcb->remote_ip.addr>>24)&0xFF; addr[2]=(newpcb->remote_ip.addr>>16)&0xFF; addr[1]=(newpcb->remote_ip.addr>>8)&0xFF; addr[0]=(newpcb->remote_ip.addr>>0)&0xFF; printf("ip地址:%d.%d.%d.%d\n",addr[0],addr[1],addr[2],addr[3]); printf("端口号:%d\n",newpcb->remote_port); printf("当前队列剩余字节:%d\n",tcp_sndbuf(newpcb)); tcp_write(newpcb,"1234567890",10,1); //将要发送的数据提交到发送队列(不会立即发送) tcp_output(newpcb); //提示系统现在,发送数据 tcp_sent(newpcb,TCP_sent); //发送成功的回调函数 tcp_recv(newpcb,TCP_recv); return ERR_OK; err_t TCP_sent(void *arg, struct tcp_pcb *tpcb,u16_t len) printf("成功发送:%d字节\n",len); //tcp_close(tpcb); //关闭客户端连接 return ERR_OK; u8 rx_buff[1024]; err_t TCP_recv(void *arg, struct tcp_pcb *tpcb,struct pbuf *p, err_t err) u32 rx_cnt=0; struct pbuf *q; memset(rx_buff,0,sizeof(rx_buff)); if(p==NULL) printf("客户端已经断开连接!\n"); for(q=p;q!=NULL;q=q->next) memcpy(rx_buff+rx_cnt,q->payload,q->len); rx_cnt+=q->len; pbuf_free(p); //释放PUFF printf("成功接收:%d字节\n",rx_cnt); printf("收到的数据=%s\n",rx_buff); return ERR_OK; }5.5 创建TCP客户端示例u8 TCP_Create(u16_t port) struct tcp_pcb *pcb=NULL; pcb=tcp_new(); //创建套接字 ip_addr_t ipaddr; if(pcb==NULL)return 1; IP4_ADDR(&ipaddr,192,168,31,54); //在ip_addr.h里定义 tcp_connect(pcb,&ipaddr,port,TCP_connected); return 0; err_t TCP_connected(void *arg, struct tcp_pcb *tpcb, err_t err) u8 addr[4]; //tcp_setprio(newpcb, TCP_PRIO_MIN); 设置优先级 printf("服务器连接成功!\n"); addr[3]=(tpcb->remote_ip.addr>>24)&0xFF; addr[2]=(tpcb->remote_ip.addr>>16)&0xFF; addr[1]=(tpcb->remote_ip.addr>>8)&0xFF; addr[0]=(tpcb->remote_ip.addr>>0)&0xFF; printf("服务器ip地址:%d.%d.%d.%d\n",addr[0],addr[1],addr[2],addr[3]); printf("服务器端口号:%d\n",tpcb->remote_port); printf("当前队列剩余字节:%d\n",tcp_sndbuf(tpcb)); tcp_write(tpcb,"1234567890",10,1); //将要发送的数据提交到发送队列(不会立即发送) tcp_output(tpcb); //提示系统现在,发送数据 tcp_sent(tpcb,TCP_sent); //发送成功的回调函数 tcp_recv(tpcb,TCP_recv); return ERR_OK; err_t TCP_sent(void *arg, struct tcp_pcb *tpcb,u16_t len) printf("成功发送:%d字节\n",len); //tcp_close(tpcb); //关闭客户端连接 return ERR_OK; u8 rx_buff[1024]; err_t TCP_recv(void *arg, struct tcp_pcb *tpcb,struct pbuf *p, err_t err) u32 rx_cnt=0; struct pbuf *q; memset(rx_buff,0,sizeof(rx_buff)); if(p==NULL) printf("服务器已经断开连接!\n"); for(q=p;q!=NULL;q=q->next) memcpy(rx_buff+rx_cnt,q->payload,q->len); rx_cnt+=q->len; printf("成功接收:%d字节\n",rx_cnt); printf("收到的数据=%s\n",rx_buff); return ERR_OK;

STM32入门开发: 制作红外线遥控器(智能居家-万能遥控器)

一、环境介绍MCU: STM32F103ZET6编程软件环境: keil5红外线传输协议:  NEC协议---38KHZ载波:。NEC协议是红外遥控协议中常见的一种。编码发送思路:  延时函数模拟38KHZ +  PWM产生38KHZ两种方式代码风格:   模块化编程,寄存器直接操作方式完整keil工程源码下载(解压即可编译运行测试):   https://download.csdn.net/download/xiaolong1126626497/19863305二、NEC协议与相关硬件介绍2.1 NEC协议介绍NEC协议在上篇<STM32入门开发: NEC红外线协议解码(超低成本无线传输方案)>文章里已经详细介绍过了。这篇文章和上篇文章内容是连贯的,上篇文章完成NEC红外线协议解码,这篇文章就当做遥控器发送端,发送自定义数据给接收端,完成自定义的数据传输;也可以模拟家电遥控器,对电视机、投影仪、空调等设备进行遥控操作。红外线协议有很多,本章节主要是针对NEC协议讲解,只要把NEC协议原理搞懂了,其他协议都是一样的使用;如果想要模拟空调遥控器,去控制美的空调、格力空调这些设备,就需要按照美的、格力空调的协议发送;如果不知道协议长什么样,可以将逻辑分析仪插在红外线接收头的引脚上,拿个正常的空调遥控器对着接收头按一下,然后采集数据分析,即可得到协议规律,然后网络上也有空调按键值功能的说明文档,调试一下即可。2. 2 使用的相关硬件因为要模拟红外线遥控器,就需要一个红外线发射管;在学习阶段,如果不想自己搭建电路,可以买现成的模块。 买模块连接也是比较稳定,接线也比较简单,VCC和GND接好之后,把DAT引脚接到STM32任意一个IO口上即可,如果想用硬件PWM控制发送,那么引脚接到STM32的PWM输出脚即可。2.3 完成NEC协议编码发送先看一段红外线接收头引脚上采集的NEC协议的电平: 这是接收端采集的。红外线接收头的硬件特性: (注意: 这里是针对NEC遥控器协议来说明),下图就是当前使用的红外线接收头。收到38KHZ的红外光,IN引脚就输出低电平;没有收到IN引脚就输出高电平。NEC红外线协议说明:(这是站在接收端解码的角度分析的)一段独立的NEC协议数据包由引导码+32位数据组成。引导码:  9ms的高电平  +  4.5ms 低电平组成。32位数据就是:  8位用户码+ 8位用户反码+ 8位按键码+8位按键反码每个数据位之间的间隔时间是0.56ms(低电平)NEC协议是依靠收到的高电平持续时间来判断数据0和数据1;高电平持续时间是0.56ms表示数据0,高电平持续时间是1.68ms表示数据1。只要明白上面说的两个特点,就可以写程序,按照NEC协议驱动红外线发射管,发送数据了。编写发送程序之前,得先明白这个38KHZ的红外光如何产生?STM32支持硬件PWM功能,可以配置38KHZ方波输出;如果没有硬件PWM功能的单片机,也可以使用延时的方式产生38KHZ方波,差那么一点点问题也不到,解码端适当调整一下时间范围即可。采用延时函数实现方法如下:/* 函数功能: 发送38KHZ的载波 函数参数: u32 time_us 持续的时间 u8 flag 1表示发送38KHZ载波,0表示不发送 void InfraredSend38KHZ(u32 time_us,u8 flag) u32 i; if(flag) //发送38KHZ载波 for(i=0;i<time_us/13;i++) INFRARED_OUTPUT=!INFRARED_OUTPUT; DelayUs(13); INFRARED_OUTPUT=1;//关闭红外线发射管 DelayUs(time_us); }为了方便发送指定的用户码和按键码,可以封装成一个函数调用。/* 函数功能: NEC协议编码发送 函数参数: u8 user 用户码 u8 key 按键码 按键反码+按键码+用户反码+用户码 void InfraredNECSend(u8 user,u8 key) u32 i; /*1. 组合发送的数据*/ u32 data=((~key&0xFF)<<24)|((key&0xFF)<<16)|((~user&0xFF)<<8)|((user&0xFF)<<0); /*2. 发送引导码*/ InfraredSend38KHZ(9000,1);//发送38KHZ载波 InfraredSend38KHZ(4500,0);//不发送 /*3. 发送32位数据*/ for(i=0;i<32;i++) InfraredSend38KHZ(560,1); //间隔时间 if(data&0x01)InfraredSend38KHZ(1685,0); //发送1 else InfraredSend38KHZ(560,0); //发送0 data>>=1; InfraredSend38KHZ(560,1); //间隔时间 }这是使用逻辑分析仪采集的发送端波形: 和协议对应了一下,没有问题。对比一下解码端采集的波形图:三、核心代码3.1 main.c#include "stm32f10x.h" #include "beep.h" #include "delay.h" #include "led.h" #include "key.h" #include "sys.h" #include "usart.h" #include <string.h> #include <stdio.h> #include "exti.h" #include "timer.h" #include "rtc.h" #include "adc.h" #include "ds18b20.h" #include "ble.h" #include "esp8266.h" #include "wdg.h" #include "oled.h" #include "rfid_rc522.h" #include "infrared.h" int main() LED_Init(); KEY_Init(); BEEP_Init(); TIM1_Init(72,20000); //辅助串口1接收,超时时间为20ms USART_X_Init(USART1,72,115200); //InfraredDecodeInit(); //红外线解码初始化 InfraredCodingInit(); //红外线编码初始化 printf("UART1 OK.....\n"); while(1) InfraredNECSend(13,14); //发送红外线数据 DelayMs(500); LED0=!LED0; }3.2 红外线.c#include "infrared.h" 函数功能: 红外线编码初始化 硬件连接: PG11 编码思路: 采用延时函数实现38KHZ void InfraredCodingInit(void) RCC->APB2ENR|=1<<8; //PG GPIOG->CRH&=0xFFFF0FFF; GPIOG->CRH|=0x00003000; GPIOG->ODR|=1<<11; 函数功能: 发送38KHZ的载波 函数参数: u32 time_us 持续的时间 u8 flag 1表示发送38KHZ载波,0表示不发送 void InfraredSend38KHZ(u32 time_us,u8 flag) u32 i; if(flag) //发送38KHZ载波 for(i=0;i<time_us/13;i++) INFRARED_OUTPUT=!INFRARED_OUTPUT; DelayUs(13); INFRARED_OUTPUT=1;//关闭红外线发射管 DelayUs(time_us); 函数功能: NEC协议编码发送 函数参数: u8 user 用户码 u8 key 按键码 按键反码+按键码+用户反码+用户码 void InfraredNECSend(u8 user,u8 key) u32 i; /*1. 组合发送的数据*/ u32 data=((~key&0xFF)<<24)|((key&0xFF)<<16)|((~user&0xFF)<<8)|((user&0xFF)<<0); /*2. 发送引导码*/ InfraredSend38KHZ(9000,1);//发送38KHZ载波 InfraredSend38KHZ(4500,0);//不发送 /*3. 发送32位数据*/ for(i=0;i<32;i++) InfraredSend38KHZ(560,1); //间隔时间 if(data&0x01)InfraredSend38KHZ(1685,0); //发送1 else InfraredSend38KHZ(560,0); //发送0 data>>=1; InfraredSend38KHZ(560,1); //间隔时间 }四、格力空调遥控协议介绍4.1 协议解析报头脉冲:9ms报头间距:4.5ms载波频率:37.9KHz(38KHz)码段1与码段2间距:20ms“1”:脉宽,656us。间距,1640us。“0”:脉宽,656us。间距,544us。4.2 编码定义1-3位:模式送风:图标:风扇。代码:110。自动:图标:循环箭头。代码:000。除湿:图标:水滴。代码:010。制冷:图标:雪花。代码:100。制热:图标:太阳。代码:001。4位(加68位):开机关机开机:1。关机:0。第68位取反。5-6位:风速一级:10二级:01三级:11自动:007、37、41位(加65位):扫风上下扫风:110。第65位取反左右扫风:101。上下左右:111无扫风:0008位:睡眠睡眠:1不睡眠:09-12位与65-68位:温度制冷模式下:制热模式:​​吸湿模式:送风模式:13-20位:睡眠定时21位:超强超强:1普通:022位:灯光亮:1灭:023位与25位:健康,换气健康:10换气:01健康+换气:11普通:0024位:制冷模式下-干燥;制热模式下-辅热;干燥:1普通:045-46位:显示温度不显示:00显示:10显示室内温度:01显示室外温度:11其他位:除了29、31、34位为“1”外,均为“0”。其他位功能不详(遥控器无对应项)。第36位和69位分别是码段1和码段2的最后一位,无所谓“0”“1”。4.3 其他说明在自动模式下只可以设置的项目有:风速1、2、3级、自动;上上下左右扫风;显示温度;灯光;睡眠定时(非睡眠)。其他项均不可以设置。此时温度不可设置,温度段的代码为:10011101。在关机状态下,可以设置定时开机,代码与睡眠定时关机一样。也可以设置灯光。在制冷模式下,可以设置的项有:温度;扫风;健康换气,节能(仅在此状态下可以设置);风速;定时;超强;睡眠;灯光;温度显示。在除湿模式下,可以设置的项有:温度;扫风;健康换气;干燥;温度显示;定时;睡眠;灯光。在送风模式下,可以设置的项有:温度;风速;健康换气;扫风;温度显示;定时;灯光。在制热模式下,可以设置的项有:温度;风速;扫风;辅热;温度显示;定时;超强;睡眠;灯光。MGQ 2012-04-141、 格力YB0F2红外信号命令格式红外信号主要包括CMD1和CMD2两部分,其中CMD1包括35 位的命令 和一位停止位,CMD2包括32位的命令和一位停止位。五、美的空调协议介绍L为引导码,S为分隔码,A为认别码(A=10110010=B2,预留方案时A=10110111=B7),A'为A的反码,B'为B的反码,C'为C的反码遥控器发射红外信号之时,通过“560微秒低电平+1680微秒高电平”代表“1”,通过“560微秒低电平+560微秒低电平”代表“0”。美的的红外采用NEC格式的R05d该协议的红外信号编码格式为:引导码+客户码+客户反码+数据码+数据反码+结束位,其中引导码和结束码都是固定的,数据反码由数据码按位取反得来,真正变化的只有用户码和数据码。

STM32入门开发: NEC红外线协议解码(超低成本无线传输方案)

一、环境介绍MCU: STM32F103ZET6编程软件环境: keil5红外线传输协议:  NEC协议---38KHZ载波:。NEC协议是红外遥控协议中常见的一种。解码思路:  外部中断 + 定时器方式代码风格:   模块化编程,寄存器直接操作方式完整keil工程源码下载(解压即可编译运行测试):   https://download.csdn.net/download/xiaolong1126626497/19863275二、NEC协议与解码思路介绍 2.1 采用的相关硬件  图1:  这是NEC协议的红外线遥控器:  如果自己手机没有红外线遥控器的功能,可以淘宝上买一个小遥控器来学习测试,成本不高,这个遥控器也可以自己做,能解码当然也可以编码发送,只需要一个红外光发射管即可。     图2: 这是红外线接收头模块。如果自己的开发板没有自带这个接收头,那就单独买一个接收头模块,使用杜邦线接到开发板的IO口上即可用来测试学习,接线很方便。图3: 这是红外线发射管,如果自己想做遥控器的发射端,自己做遥控器,那么就可以直接购买这种模块即可。2.2 红外线协议介绍在光谱中波长自760nm至400um的电磁波称为红外线,它是一种不可见光。红外线通信的例子我们每个人应该都很熟悉,目前常用的家电设备几乎都可以通过红外遥控的方式进行遥控,比如电视机、空调、投影仪等,都可以见到红外遥控的影子。这种技术应用广泛,相应的应用器件都十分廉价,因此红外遥控是我们日常设备控制的理想方式。红外线的通讯原理: 红外光是以特定的频率脉冲形式发射,接收端收到到信号后,按照约定的协议进行解码,完成数据传输,在消费类电子产品里,脉冲频率普遍采用 30KHz 到 60KHz 这个频段,NEC协议的频率就是38KHZ。 这个以特定的频率发射其实就可以理解为点灯,不要被复杂的词汇难住了,就是控制灯的闪烁频率(亮灭),和刚学单片机完成闪光灯一样的意思,只不过是灯换了一种类型,都是灯。 接收端的原理:  接收端的芯片对这个红外光比较敏感,可以根据有没有光输出高低电平,如果发送端的闪烁频率是有规律的,接收端收到后输出的高电平和低电平也是有规律对应的,这样发送端和接收端只要约定好,那就可以做数据传输了。红外线传输协议可以说是所有无线传输协议里成本最低,最方便的传输协议了,但是也有缺点,距离不够长,速度不够快;当然,每个传输协议应用的环境不一样,定位不一样,好坏没法比较,具体要看自己的实际场景选择合适的通信方式。   2.3 NEC协议介绍NEC协议是众多红外线协议中的一种(这里说的协议就是他们数据帧格式定义不一样,数据传输原理都是一样的),我们购买的外能遥控器、淘宝买的mini遥控器、电视机、投影仪几乎都是NEC协议。  像格力空调、美的空调这些设备使用的就是其他协议格式,不是NEC协议,但是只要学会一种协议解析方式,明白了红外线传输原理,其他遥控器协议都可以解出来。下图是NEC协议传输一次数据的完整格式:NEC协议一次完整的传输包含:    引导码、8位用户码、8位用户反码、8位数据码、8位数据反码。(注意:下面的解释都是站在红外线接收端的角度来进行说明的,就是解码端的角度)引导码:  由9ms的高电平+4.5ms的低电平组成。4个字节的数据:  用户码+用户反码+数据码+数据反码。  这里的反码可以用来校验数据是否传输正确,有没有丢包。重点:  NEC协议传输数据位的时候,0和1的区分是依靠收到的高、低电平的持续时间来进行区分的---这是解码关键。标准间隔时间:0.56ms收到数据位0:  0.56ms收到位1:  1.68ms所以,收到一个数据位的完整时间表示方法是这样的:收到数据位0:   0.56m低电平+ 0.56ms的高电平收到数据位1:  0.56ms低电平+1.68ms的高电平红外线接收头模块输出电平的原理: 红外线接收头感应到有红外光就输出低电平,没有感应到红外光就输出高电平。这是使用逻辑分析采集红外线接收头输出的信号:​这是采集红外线遥控器上的LED灯输出电平时序图,刚好和接收端相反:​​单片机编写解码程序的时候,常见的方式就是采用外部中断+定时器的方式进行解析,中断可以设置为低电平触发,因为接收头没有感应到红外光默认是输出高电平,如果收到NEC引导码,就会输出低电平,进入到中断服务函数,完成解码,解码过程中开启定时器记录每一段的高电平、低电平的持续时间,按照NEC协议进行判断,完成最终解码。STM32可以使用输入捕获方式完成解码,其实输入捕获就是外部中断+定时器的组合,只不过是STM32内部封装了一层。外部中断服务器里的解码程序如下(这个在其他单片机上思路是一样的):/* 函数功能: 外部中断线9_5服务函数 void EXTI9_5_IRQHandler(void) u32 time; u8 i,j,data=0; //清除中断线9上的中断请求 EXTI->PR|=1<<9; time=Infrared_GetTime_L(); //得到低电平时间 if(time<7000||time>10000)return; //标准时间: 9000us time=Infrared_GetTime_H(); //得到高电平时间 if(time<3000||time>5500)return; //标准时间4500us //正式解码NEC协议 for(i=0;i<4;i++) for(j=0;j<8;j++) time=Infrared_GetTime_L(); //得到低电平时间 if(time<400||time>700)return; //标准时间: 560us time=Infrared_GetTime_H(); //得到高电平时间 if(time>1400&&time<1800) //数据1 1680us data>>=1; data|=0x80; else if(time>400&&time<700) //数据0 560us data>>=1; else return; InfraredRecvData[i]=data; //存放解码成功的值 //解码成功 InfraredRecvState=1; }三、核心完整代码本程序的解码思路是: 将红外线接收模块的输出脚接到STM32的PB9上,配置STM32的PB9为外部中断模式,下降沿电平触发;如果收到红外线信号就进入到中断服务函数里解码,如果解码过程中发现数据不符合要求就终止解码,如果数据全部符合要求就按照协议接收,直到解码完成,设置标志位,在main函数里打印解码得到的数据。代码都是模块化编程,阅读起来也很方便。3.1  红外线解码.c#include "nec_Infrared.h" u8 InfraredRecvData[4]; //存放红外线解码接收的数据 u8 InfraredRecvState=0; //0表示未接收到数据,1表示接收到数据 函数功能: 红外线解码初始化(接收) void Infrared_RecvInit(void) Infrared_Time6_Init(); //定时器初始化 /*1. 配置GPIO口*/ RCC->APB2ENR|=1<<3; //PB GPIOB->CRH&=0xFFFFFF0F; GPIOB->CRH|=0x00000080; GPIOB->ODR|=1<<9; /*2. 配置外部中断*/ EXTI->IMR|=1<<9; //外部中断线9,开放中断线的中断请求功能 EXTI->FTSR|=1<<9; //中断线9_下降沿 RCC->APB2ENR|=1<<0; //开启AFIO时钟 AFIO->EXTICR[2]&=~(0xF<<1*4); AFIO->EXTICR[2]|=0x1<<1*4; STM32_NVIC_SetPriority(EXTI9_5_IRQn,1,1); 函数功能: 初始化定时器,用于红外线解码 void Infrared_Time6_Init(void) RCC->APB1ENR|=1<<4; RCC->APB1RSTR|=1<<4; RCC->APB1RSTR&=~(1<<4); TIM6->PSC=72-1; //预分频器 TIM6->ARR=65535; //重装载寄存器 TIM6->CR1|=1<<7; //开启缓存功能 //TIMx->CR1|=1<<0; //开启定时器 函数功能: 测量高电平持续的时间 u32 Infrared_GetTime_H(void) TIM6->CNT=0; TIM6->CR1|=1<<0; //开启定时器 while(NEC_IR){} //等待高电平结束 TIM6->CR1&=~(1<<0); //关闭定时器 return TIM6->CNT; 函数功能: 测量低电平持续的时间 u32 Infrared_GetTime_L(void) TIM6->CNT=0; TIM6->CR1|=1<<0; //开启定时器 while(!NEC_IR){} //等待低电平结束 TIM6->CR1&=~(1<<0); //关闭定时器 return TIM6->CNT; 函数功能: 外部中断线9_5服务函数 void EXTI9_5_IRQHandler(void) u32 time; u8 i,j,data=0; //清除中断线9上的中断请求 EXTI->PR|=1<<9; time=Infrared_GetTime_L(); //得到低电平时间 if(time<7000||time>10000)return; //标准时间: 9000us time=Infrared_GetTime_H(); //得到高电平时间 if(time<3000||time>5500)return; //标准时间4500us //正式解码NEC协议 for(i=0;i<4;i++) for(j=0;j<8;j++) time=Infrared_GetTime_L(); //得到低电平时间 if(time<400||time>700)return; //标准时间: 560us time=Infrared_GetTime_H(); //得到高电平时间 if(time>1400&&time<1800) //数据1 1680us data>>=1; data|=0x80; else if(time>400&&time<700) //数据0 560us data>>=1; else return; InfraredRecvData[i]=data; //存放解码成功的值 //解码成功 InfraredRecvState=1; }3.2  主函数.c#include "stm32f10x.h" #include "led.h" #include "delay.h" #include "key.h" #include "usart.h" #include "at24c02.h" #include "W25Q64.h" #include "spi.h" #include "nec_Infrared.h" int main() LED_Init(); BEEP_Init(); KeyInit(); USARTx_Init(USART1,72,115200); IIC_Init(); W25Q64_Init(); printf("芯片ID号:0x%X\n",W25Q64_ReadID()); Infrared_RecvInit(); while(1) if(InfraredRecvState) InfraredRecvState=0; printf("用户码:%d,按键码:%d\n",InfraredRecvData[0],InfraredRecvData[2]); printf("user反码:%d,key反码:%d\n",(~InfraredRecvData[1])&0xFF,(~InfraredRecvData[3])&0xFF); BEEP=!BEEP; LED0=!LED0; 四、扩展提高如果上面的NEC的解码思路已经看到,程序已经可以自己编写,就可以试着使用STM32的输入捕获+定时器方式写一版解码代码,既能更加熟悉NEC协议、也可以学习STM32定时器捕获捕获的用法;也可以做一些小东西来锻炼,比如:红外线遥控小车、音乐播放器支持红外线遥控器切歌,电机的开关、灯的开关等等。搞定协议解码之后,我们下一步就是完成自定义的NEC协议红外线制作,采用STM32模拟一个万能红外线遥控器。

51单片机入门指南-基于STC89C52(持续更新)

一、环境介绍编程IDE: keil5单片机型号: STC89C51/52开发板: 普中科技的开发板完整PDF资源下载地址: https://download.csdn.net/download/xiaolong1126626497/19785856二、51单片机开发手册介绍2.1 前言51单片机是对所有兼容Intel 8031指令系统的单片机的统称。该系列单片机的始祖是Intel的8004单片机,后来随着Flash rom技术的发展,8004单片机取得了长足的进展,成为应用最广泛的8位单片机之一,其代表型号是ATMEL公司的AT89系列,它广泛应用于工业测控系统之中。很多公司都有51系列的兼容机型推出, 51单片机是基础入门的一个单片机,还是应用最广泛的一种。主要产品代表:(1)、Intel(英特尔)的:80C31、80C51、87C51,80C32、80C52、87C52等;(2)、ATMEL(爱特梅尔)的:89C51、89C52、89C2051,89S51(RC),89S52(RC)等;(3)、Philips(飞利浦)、华邦、Dallas(达拉斯)、Siemens(西门子)等公司;(4)、STC(国产宏晶)单片机:89c51、89c52、89c516、90c516等。宏晶科技是新一代增强型8位单片微型计算机标准的制定者和领导厂商。市场上的主流单片机种类(1)、8051单片机8051单片机最早由Intel公司推出,随后Intel公司将80C51内核使用权,以专利互换或出让给世界许多著名IC制造厂商,这样80C51单片机就变成了众多芯片制造厂商支持的大家族,统称为80C51系列单片机。客观事实表明,80C51已成为8位单片机的主流。(2)、AVR单片机AVR单片机是1997年由ATMEL(爱特梅尔)公司研发出的增强型内置Flash的RISC(Reduced Instruction Set CPU) 精简指令集高速8位单片机。可以广泛应用于计算机外部设备、工业实时控制、仪器仪表、通讯设备、家用电器等各个领域。AVR单片机最大的特点是精简指令型单片机,执行速度,在相同的振荡频率下是8位MCU中最快的一种单片机。(3)、PIC单片机PIC单片机是Microchip(美国微芯半导体)公司的产品,它也是一种精简指令型的单片机,指令数量比较少,中档的PIC系列仅仅有35条指令而已,低档的仅有33条指令。适用于用量大,档次低,价格敏感的产品,在办公自动化设备,消费电子产品,电讯通信,智能仪器仪表,汽车电子,金融电子,工业控制不同领域都有广泛的应用。PIC最大的特点是不搞单纯的功能堆积,而是从实际出发,重视产品的性能与价格比,靠发展多种型号来满足不同层次的应用要求。PIC系列从低到高有几十个型号,可以满足各种需要。其中,PIC12C508单片机仅有8个引脚,是世界上最小的单片机。(4)、MSP430MSP430系列单片机是美国德州仪器(TI)1996年开始推向市场的一种16位超低功耗、具有精简指令集(RISC)的混合信号处理器(Mixed Signal Processor)。MSP430单片机称之为混合信号处理器,是由于其针对实际应用需求,将多个不同功能的模拟电路、数字电路模块和微处理器集成在一个芯片上,以提供“单片机”解决方案。该系列单片机多应用于需要电池供电的便携式仪器仪表中。MSP430系列单片机是一个16位的单片机,运算速度快,超低功耗,MSP430 系列单片机的电源电压采用的是1.8-3.6V电压。(5)、ARM处理器ARM即以英国ARM(Advanced RISC Machines)公司的内核芯片作为CPU,同时附加其他外围功能的嵌入式开发板,用以评估内核芯片的功能和研发各科技类企业的产品。ARM是一个32位元精简指令集(RISC)处理器架构,ARM处理器广泛地使用在许多嵌入式系统设计。ARM处理器的特点有指令长度固定,执行效率高,低成本等。ARM微处理器,已遍及工业控制、消费类电子产品、通信系统、网络系统、无线系统等各类产品市场,基于ARM技术的微处理器应用约占据了32位RISC微处理器75%以上的市场份额,ARM技术正在逐步渗入到我们生活的各方面。ARM 微处理器目前包括下面几个系列,以及其它厂商基于 ARM 体系结构的处理器,除了具有ARM 体系结构的共同特点以外,每一个系列的 ARM 微处理器都有各自的特点和应用领域。- ARM7 系列- ARM9 系列- ARM9E 系列- ARM10E 系列- ARM11系列- Cortex 系列 : Cortex系列处理器是基于ARMv7架构的,分为Cortex-M、Cortex-R和Cortex-A三类。由于应用领域的不同,基于v7架构的Cortex处理器系列所采用的技术也不相同。基于v7A的称为“Cortex-A系列。- SecurCore 系列- OptimoDE Data Engines- Intel的Xscale- Intel的StrongARM ARM11系列2.2  目录介绍51单片机开发手册... 1一、       单片机开发入门知识介绍... 11.1 51单片机介绍... 11.2 市场上的主流单片机种类... 11.3 FPGA与单片机区别... 21.4 DSP和单片机区别... 3二、搭建开发环境... 42.1 STC单片机命名介绍... 42.2 安装keil软件... 42.3 新建工程... 52.4 下载程序... 82.5 使用辅助工具计算延时时间... 122.6 STC90C51系列单片机引脚图... 13三、       基础入门学习... 143.1 LED灯模块... 143.2 蜂鸣器模块... 173.5 独立按键... 183.6 矩阵键盘... 223.7 独立数码管(静态数码管) 263.8 动态数码管... 293.9 LED 16*16点阵... 333.10 采用38译码器驱动8位数码管... 48四、单片机提高篇... 534.1 定时器... 534.2 中断... 604.3 串口通信... 714.4  NEC红外线遥控器解码... 804.5 DS18B20温度传感器... 874.6 EEPROM存储芯片(AT24C02) 964.7 DS1302 实时时钟芯片... 1074.8 PCF8591(ADC/DAC)转换芯片... 1194.9 (HC-SR04)超声波测距模块... 1304.10 使用计数器测量NE555脉冲频率... 1344.11 LCD1602显示屏... 139四、       单片机项目篇... 1465.1 16x16点阵滚动显示... 1462.3   书籍内容介绍书籍内容从基础的keil软件安装、环境搭建、程序编译、下载、运行、LED、按键、数码管、中断、定时器、.....开始讲解51单片机的开发。 文中代码注释完整、代码完整、配图清晰有顺序、都是以模块化编程的风格写代码,代码都有着色,很方便阅读;文中的代码都可以直接复制粘贴出来编译运行测试,非常适合当做平时开发参考的工具书、入门学习的工具书使用。资料包的里的PDF会持续更新,后续有新版本会直接替换资源包里的内容,再次下载即可。2.3 内容详情

C++入门指南(持续更新)

一、环境介绍编程IDE:  VS Code文章中的代码编译测试的运行环境:  windows完整PDF下载地址(持续更新): https://download.csdn.net/download/xiaolong1126626497/19785777来至知乎的说明:C++是一种面向对象的计算机程序设计语言,由美国AT&T贝尔实验室的本贾尼·斯特劳斯特卢普博士在20世纪80年代初期发明并实现(最初这种语言被称作“C with Classes”带类的C)。它是一种静态数据类型检查的、支持多重编程范式的通用程序设计语言。它支持过程化程序设计、数据抽象、面向对象程序设计、泛型程序设计等多种程序设计风格。 C++是C语言的继承,进一步扩充和完善了C语言,成为一种面向对象的程序设计语言。C++这个词在中国大陆的程序员圈子中通常被读做“C加加”,而西方的程序员通常读做“C plus plus”,“CPP”。C++是 C 语言的升级版本,它既可以进行 C 语言的过程化程序设计,又可以进行抽象数据类型程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。C++由 C 语言发展而来, 完全兼容 C 语言, 编写的 C 语言代码可以不加修改地用于 C++。C 语言是面向过程的语言, C++ 在此基础上增加了面向对象以及泛型编程机制,因此 C++ 更适合大中型程序的开发。面向对象编程在代码执行效率上与面向过程相比没有任何优势, 主要是针对开发中大规模的程序而提出来的,目的是提高软件开发的效率。二、C++入门指南介绍2.1 内容介绍C++入门指南主要讲解C++的基础内容,适合学完C语言的童鞋进阶学习;文章内容直接从C++部分讲起,C语言部分没有提及,所以需要提前学习C语言,再继续进阶。文章里从最开始的环境搭建、程序调试运行、到C++类、对象、继承、重载、多态、模板开始讲解,文章里代码清晰规范,配图完整有顺序,完全按照标准出版书籍风格编写,代码都有着色,看起来清晰易懂,可以直接复制出来测试运行验证。非常适合当做平时开发,学习的工具书使用,后续会持续更新(更新会直接更新替换资源PDF)。2.2 目录介绍C++入门指南... 1一、       C++语言基本介绍与开发环境搭建... 11.1 C++简介... 11.2 面向对象编程... 11.3 Windows系统下搭建C++学习环境... 2二、C++基础入门... 162.1 C++类和对象... 172.2 C++命名空间... 182.3 std标准命名空间... 202.4 C++新增的标准输入输出方法(cin和cout) 222.5 C++规定的变量定义位置... 242.6 C++新增的布尔类型(bool)... 242.7 C++ 新增的new和delete运算符... 252.8 C++函数的默认参数(缺省参数) 262.9 C++函数重载详解... 282.10 C++新增的引用语法... 30三、       C++面向对象:类和对象... 343.1 类的定义和对象的创建... 343.2 类的成员变量和成员函数... 363.3 类成员的访问权限以及类的封装... 383.4 C++类的构造函数与析构函数... 393.5 对象数组... 473.6  this指针... 503.7 static静态成员变量... 523.8 static静态成员函数... 533.9 const成员变量和成员函数... 553.10 const对象(常对象) 563.11 友元函数和友元类... 583.11.3 友元类... 613.12 C++字符串... 62四、C++面向对象:继承与派生... 754.1 继承与派生概念介绍... 754.2 继承的语法介绍... 754.3 继承方式介绍(继承的权限) 764.4 继承时变量与函数名字遮蔽问题... 794.5 基类和派生类的构造函数... 824.6 基类和派生类的析构函数... 834.7 多继承... 854.8 虚继承和虚基类... 88五、C++多态与抽象类... 915.1 多态概念介绍... 915.2 虚函数... 925.3 纯虚函数和抽象类... 95六、C++运算符重载... 976.1 运算符重载语法介绍... 976.2 可重载运算符与不可重载运算符... 986.3 一元运算符重载... 996.4 二元运算符重载... 1026.5 关系运算符重载... 1046.6 输入/输出运算符重载(>>、<<) 1056.7 函数调用运算符 () 重载... 1066.8 重载[ ](下标运算符)... 107七、C++模板和泛型程序设计... 1087.1 函数模板... 1087.2 类模板    1102.3  内容详情

Python3入门指南(持续更新)

一、环境介绍编程IDE:  VS Code运行环境:  windows 、UbuntuPython版本:  3.8.0完整PDF下载地址(持续更新资源包): https://download.csdn.net/download/xiaolong1126626497/19785720 二、Python3入门指南介绍Python 是一种跨平台的计算机程序设计语言, 属于解释型、面向对象、动态数据类型的高级程序设计语言,在许多领域都有应用,包括 Web 编程,脚本编写,科学计算和人工智能。Python 最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越来越多被用于独立的、大型项目的开发。语言没有高低之分, 各有自己存在的价值,每种编程语言各有千秋; C 语言是可以用来编写操作系统的贴近硬件的语言,所以, C 语言适合开发那些追求运行速度、充分发挥硬件性能的程序。而 Python 是用来编写应用程序的高级编程语言。2.1  内容介绍Python3入门指南主要讲了python基础内容,从语言介绍、环境搭建、程序运行、基础语法、变量.....开始讲解,目前一共写了10个大章节,后续会持续更新(会直接更新资源包里的PDF),文中截图清晰,代码详细,都有着色,方便阅读,也方便直接复制粘贴运行测试。  2.2 目录介绍下面是PDF里的详细内容:Python3入门指南... 1一、       Python编程语言入门介绍... 11.1 什么是Python?... 11.2 Python与其他编程语言的比较... 1二、搭建Python开发环境... 22.1 Python版本介绍... 22.2 在windows下安装Python环境... 22.3 在ubuntu18.04环境下安装Python环境... 52.4 运行Python代码方式... 82.4 编写Python推荐的编辑器... 92.5 windows下安装VSCode代码编辑器... 112.6 ubuntu下安装VSCode代码编辑器... 22三、Python基础语法学习... 303.1 编写第一个Python程序... 303.2 Python中单行与多行注释语法... 313.3 python输出功能基本语法:print() 323.4 python输入功能基本语法:input() 343.5 Python标识符与关键字... 343.6 Python代码缩进规则... 353.7 文本编码... 36四、Python变量、数据类型... 384.1 Python数据类型介绍... 384.2 变量的概念与赋值方法... 384.3 Python数字数据类型... 414.4 Python字符串类型... 424.5 Python字符串处理... 444.6 Python列表数据类型... 484.7 Python元组数据类型... 564.8 Python字典数据类型... 584.9 Python集合数据类型... 66五、Python运算符... 735.1 运算符介绍... 735.2 算术运算符... 745.3 比较运算符... 755.4 赋值运算符... 775.5 位运算符... 795.6 逻辑运算符... 815.7 成员运算符... 825.8 身份运算符... 845.9 运算符优先级... 86六、Python判断语句与循环语句... 876.1 if判断语句... 876.2 while循环语句... 896.3 for循环语句... 926.4 break循环控制语句... 986.5 continue循环控制语句... 996.6 assert语句... 100七、Python函数... 1007.1 调用函数... 1017.2 定义一个函数... 1027.3 函数参数传递... 1037.4 匿名函数... 1067.5 return语句... 1077.6 关于函数形参可更改与不可更改对象... 1087.7 局部变量与全局变量... 1097.8 局部函数... 111八、Python模块... 1118.1 python中的模块是什么?. 1128.2 导入模块: 学习import语句... 1128.3 编写自定义模块... 1158.4 模块的搜索路径... 1178.5 Python的包... 1238.6  Python第三方库(模块)下载... 128九、文件IO编程... 1309.1 Python文件操作函数介绍... 1309.2 操作文件的一般顺序... 1309.3 Python内置的open函数... 1319.4 操作文件常用的方法... 1339.5 调用操作系统提供的接口函数操作文件... 1409.6 os.path模块常见函数用法... 1449.7 fnmatch模块:用于文件名的匹配... 1459.8 tempfile模块: 生成临时文件和临时目录... 146十、面向对象编程... 14710.1 面向对象程序设计思想... 14710.2 面向对象相关术语... 14810.3 类与对象的基本使用... 14810.4 类封装机制... 15610.5 类继承机制... 15710.6 父类方法重写... 1592.3  内容详情

STM32入门开发:编写XPT2046电阻触摸屏驱动(模拟SPI)

一、环境介绍单片机采用: STM32F103ZET6编程软件: keil5编程语言: C语言编程风格:  寄存器开发. 目标芯片: XPT2046---标准SPI接口时序二、XPT2046芯片介绍2.1  功能XPT2046是一颗12位的ADC芯片,可以当做普通的ADC芯片使用,但是一般都是用在电阻触摸屏上,方便定位触摸屏坐标。图1: XPT2046内部原理图图2:电阻触摸屏---引出的4条线就接在XPT2046的YN\XN\YP\XP上(XPT2046支持笔中断输出--低电平有效,这个引脚可以配置到单片机的中断脚上,或者轮询判断这个引脚状态,判断触摸屏是否已经按下) 可以单独买一个触摸屏+一个XPT2046就可以自己做手画板、触摸按键(自己用一张纸在下面画个模型就行)、等等很多小玩意。图3:采用的电阻触摸屏的LCD屏(上面盖的哪一层薄膜就是触摸用的)2.2 特性1. 工作电压范围为 2.2V~5.25V2. 支持 1.5V~5.25V 的数字 I/O 口3. 内建 2.5V 参考电压源4. 电源电压测量(0V~6)5. 内建温度测量功能6. 触摸压力测量7. 采用 SPI 3线控制通信接口8. 具有自动 power-down 功能9.  封装:QFN-16、 TSSOP-16 和 VFBGA-48与 TSC2046、 AK4182A 完全兼容10.  XPT2046 在 125KHz 转换速率和 2.7V 电压下的功耗仅为750 µW。 XPT2046 11. 以其低功耗和高速率等特性,被广泛应用在采用电池供电的小型手持设备上,比如 PDA、手机等。12. XPT2046 有 TSSOP-16、 QFN-16 和 VFBGA 三种封装形式,温度范围是 - 40 ~ + 85℃ 。2.3  工作原理XPT2046 是一种典型的逐次逼近型模数转换器(SAR ADC),包含了采样/保持、模数转换、串口数据输出等功能。同时芯片集成有一个 2.5V的内部参考电压源、温度检测电路,工作时使用外部时钟。 XPT2046 可以单电源供电,电源电压范围为 2.7V~5.5V。参考电压值直接决定ADC的输入范围,参考电压可以使用内部参考电压,也可以从外部直接输入1V~VCC范围内的参考电压(要求外部参考电压源输出阻抗低)。 X、 Y、 Z、 VBAT、 Temp和AUX模拟信号经过片内的控制寄存器选择后进入ADC, ADC可以配置为单端或差分模式。选择VBAT、 Temp和AUX时可以配置为单端模式;作为触摸屏应用时,可以配置为差分模式,这可有效消除由于驱动开关的寄生电阻及外部的干扰带来的测量误差,提高转换准确度。典型的应用:单端工作模式SER/DFR置为高电平时, XPT2046 工作在为单端模式,单端工作模式的应用原理如下图所示。单端模式简单,在采样过程完成后,转换过程中可以关闭驱动开关,降低功耗。但这种模式的缺点是精度直接受参考电压源的精度限制,同时由于内部驱动开关的导通电阻存在,导通电阻与触摸屏电阻的分压作用,也会带来测量误差。(图片里的A2 A1 A0 ,还有上面说的SER/DFR就是XPT2046的配置命令,具体使用方法在后面会讲到)​差分工作模式SER/DFR置为低电平时, XPT2046 为差分工作模式.差分模式的优点是: +REF 和-REF 的输入分别直接接到 YP、 YN 上,可消除由于驱动开关的导通电阻引入的坐标测量误差。缺点是:无论是采样还是转换过程中,驱动开关都需要接通,相对单端模式而言,功耗增加了。如果不考虑功耗的话,当前就选择差分工作模式了。(图片里的A2 A1 A0 ,还有上面说的SER/DFR就是XPT2046的配置命令,具体使用方法在后面会讲到)2.3  XPT2046采集并转换一次数据的时序介绍XPT2046 数据接口是串行接口,处理器和转换器之间的通信需要 8 个时钟周期,可采用 SPI、 SSI 和 Microwire 等同步串行接口。一次完整的转换需要 24 个串行同步时钟(DCLK)来完成。前 8 个时钟用来通过DIN引脚输入控制字节。当转换器获取有关下一次转换的足够信息后,接着根据获得的信息设置输入多路选择器和参考源输入,并进入采样模式,如果需要,将启动触摸面板驱动器。 3 个多时钟周期后,控制字节设置完成,转换器进入转换状态。这时,输入采样-保持器进入保持状态,触摸面板驱动器停止工作(单端工作模式)。接着的12 个时钟周期将完成真正的模数转换。如果是度量比率转换方式(SER/DFR ——=0),驱动器在转换过程中将一直工作,第13 个时钟将输出转换结果的最后一位。剩下的 3 个多时钟周期将用来完成被转换器忽略的最后字节(DOUT置低)。时序图如下:时序图里的控制命令字节:控制字节每个位的含义如下:​注意: 差分模式仅用于 X 坐标、 Y 坐标和触摸压力的测量,其它测量要求采用单端模式。根据上面表格的介绍,可以得到在差分模式下,选择12位分辨率,测量X和Y坐标的两个命令:0xD0 和 0x90XPT2046还有其他模式,可以测量温度,笔中断的开关(默认是开着的),16时钟周期转换,15时钟周期转换,这些就不再介绍。 根据前面的介绍用在触摸屏上测量XY坐标的功能已经满足了。2.4 SPI时序介绍这里的XPT2046支持标准3线SPI接口,关于SPI时序的介绍,在前面文章里有介绍过。参考这里: https://blog.csdn.net/xiaolong1126626497/article/details/1176485392.5 物理坐标与屏幕坐标的转换正常在LCD屏上使用触摸屏,肯定是需要将采集的原始X、Y值转为LCD屏的屏幕坐标才好使用。转换的方法有很多,这里采用最简单的角系数计算方法转换。比如,我使用的LCD屏是3.5寸的,分辨率是320*480。1. 得到触摸屏左上角和右下角的坐标XY极限值  x=3831,y=3934  x=155,y=1682. 转换坐标值  x坐标:3831~155 -->  3676~0  y坐标:3934~168 -->  3766~03. 计算斜率  x坐标的斜率: 3676/320=11.4875  y坐标的斜率: 3766/480=7.84583 4. 得到实际的像素坐标  x坐标:  320-(实时采集的当前X模拟量-155)/11.4875  y坐标:  480-(实时采集的当前Y模拟量-168)/7.84583这里相减的原因: 因为我测试用的触摸屏采集出来的X、Y值大小和LCD屏的屏幕坐标值大小是反过来的。三、示例代码采用SPI模拟时序驱动,其他平台都可以移植。3.1 xpt2046.c#include "xpt2046_touch.h" struct XPT2046_TOUCH xpt2046_touch; 函数功能: 初始化 硬件连接: T_MOSI--PF9 T_MISO--PB2 T_SCK---PB1 T_PEN---PF10 T_CS----PF11 void XPT2046_TouchInit(void) /*1. 时钟初始化*/ RCC->APB2ENR|=1<<3; //PB RCC->APB2ENR|=1<<7; //PF /*2. 初始化GPIO口*/ GPIOB->CRL&=0xFFFFF00F; GPIOB->CRL|=0x00000830; GPIOF->CRH&=0xFFFF000F; GPIOF->CRH|=0x00003830; /*3. 上拉*/ GPIOB->ODR|=0x3<<1; GPIOF->ODR|=0x7<<9; 函数功能: SPI底层写一个字节 void XPT2046_SPI_WriteOneByte(u8 cmd) u8 i; for(i=0;i<8;i++) XPT2046_SCK=0; //低电平写 if(cmd&0x80)XPT2046_MOSI=1; else XPT2046_MOSI=0; cmd<<=1; XPT2046_SCK=1; //高电平读,保证数据线稳定 函数功能: 读2个字节 说明: 读取16位数据,最低4位数据无效,有效数据是高12位 u16 XPT2046_ReadData(u8 cmd) u16 data; u8 i; XPT2046_CS=0; //选中XPT2046 XPT2046_MOSI=0; XPT2046_SCK=0; XPT2046_SPI_WriteOneByte(cmd); DelayUs(8); //0.008ms ,等待XPT2046转换完成。 //消除忙信号 XPT2046_SCK=0; DelayUs(1); XPT2046_SCK=1; //连续读取16位的数据 for(i=0;i<16;i++) XPT2046_SCK=0; //通知XPT2046,主机需要数据 XPT2046_SCK=1; data<<=1; if(XPT2046_MISO)data|=0x01; data>>=4; //丢弃最低4位 XPT2046_CS=1; //取消选中 return data; XPT2046的命令: 10010000 :测试Y的坐标 0x90 11010000 :测试X的坐标 0xD0 返回值: 0表示没有读取到坐标,1表示读取到当前坐标 //1. 得到左上角和右下角的坐标XY极限值 x=3831,y=3934 x=155,y=168 //2. 转换坐标值 x坐标:3831~155 --> 3676~0 y坐标:3934~168 --> 3766~0 //3. 计算斜率 x坐标的斜率: 3676/320=11.4875 y坐标的斜率: 3766/480=7.84583 //4. 得到实际的像素坐标 x坐标: 320-(模拟量-155)/11.4875 y坐标: 480-(模拟量-168)/7.84583 u8 XPT2046_ReadXY(void) if(XPT2046_PEN==0) //判断触摸屏是否按下 /*1. 得到物理坐标*/ xpt2046_touch.x0=XPT2046_ReadData(0xD0); xpt2046_touch.y0=XPT2046_ReadData(0x90); /*2. 得到像素坐标*/ xpt2046_touch.x=320-(xpt2046_touch.x0-155)/11.4875; xpt2046_touch.y=480-(xpt2046_touch.y0-168)/7.84583; return 1; return 0; }3.2 xpt2046.h#ifndef XPT2046_TOUCH_H #define XPT2046_TOUCH_H #include "stm32f10x.h" #include "sys.h" #include "delay.h" //触摸屏引脚定义 #define XPT2046_MOSI PFout(9) #define XPT2046_MISO PBin(2) #define XPT2046_SCK PBout(1) #define XPT2046_CS PFout(11) #define XPT2046_PEN PFin(10) //函数声明 void XPT2046_TouchInit(void); void XPT2046_SPI_WriteOneByte(u8 cmd); u8 XPT2046_ReadXY(void); //存放触摸屏信息的结构体 struct XPT2046_TOUCH u16 x0; //物理坐标x u16 y0; //物理坐标y u16 x; //像素坐标x u16 y; //像素坐标y extern struct XPT2046_TOUCH xpt2046_touch; #endif

基于STM32设计的智能家居系统(采用ESP8266+OneNet云平台)

一、环境介绍单片机采用:STM32F103C8T6上网方式:采用ESP8266,也可以使用其他设备代替,只要支持TCP协议即可。比如:GSM模块、有线网卡等。云平台: 采用中国移动OneNet.  也可以采用腾讯、阿里云、华为云、百度天工物接入、机智云等等。 前面文章有讲解。协议: 采用MQTT协议开发软件:keil5完整项目源码下载:  https://download.csdn.net/download/xiaolong1126626497/19766925二、包含的硬件与实物图介绍1. 一个光敏电阻传感器2. 一个DHT11温湿度传感器3. 一个MQ-5 液化气/天然气/煤气监测传感器4. 一个MQ-2 烟雾传感器5. 3盏LED灯表示窗帘开关、空调开关、电视开关6. 联网WIFI采用: ESP82667. 最小系统板: STM32F103C8T68. 物联网服务器: OneNet平台9. 物联网手机APP: 设备云设备云APP下载地址(Android):  https://download.csdn.net/download/xiaolong1126626497/18697132三、功能介绍这是基于STM32设计的智能家居控制系统,采用ESP8266连接OneNet云平台。设备端可以实时采集烟雾浓度、温湿度、煤气天然气浓度到云平台,在云平台网页端或者手机APP上可以远程查看数据,还可以点击云平台界面上的按钮,完成对家里的电器设备控制:窗帘开关、空调开关、电视开关(采用LED灯模拟)等。 四、OneNet创建设备从0开始创建OneNet设备,参考这里:https://xiaolong.blog.csdn.net/article/details/107385118首地址:https://open.iot.10086.cn/这是STM32设备端连接OneNet串口打印的提示信息:登录成功的效果:数据流收到物联网终端上传的信息。查看设计的网页界面:五、程序下载介绍点击开始编程之后,按下开发板上的RST按钮,即可启动下载。六、STM32设备端代码6.1 esp8266.c#include "esp8266.h" struct ESP8266_WIFI esp8266_wifi; 函数功能: 配置ESP8266WIFI为AP模式+TCP服务器模式 函数参数: char *ssid :将要创建的WIFI热点名称(英文字母) char *password :将要创建的WIFI热点密码(最短长度8位) u16 port :TCP服务器的端口号(0~65535) 返 回 值:0表示成功,其他值表示失败 u8 ESP8266_AP_TCP_ServerMode(const char *ssid,const char *password,u16 port) u8 i; char *find_str=NULL; char cmd_buffer[100]; /*1. 发送测试指令,检测WIFI是否正常*/ printf("发送测试指令,检测WIFI是否正常...\n"); if(ESP8266_SendCmd("AT\r\n"))return 1; /*2. 关闭回显*/ printf("关闭回显...\n"); if(ESP8266_SendCmd("ATE0\r\n"))return 2; /*3. 设置当前WIFI的模式为AP模式*/ printf("设置当前WIFI的模式为AP模式...\n"); if(ESP8266_SendCmd("AT+CWMODE=2\r\n"))return 3; /*4. 复位模块,设置的模式必须复位之后才会生效*/ printf("复位模块...\n"); if(ESP8266_SendCmd("AT+RST\r\n"))return 4; DelayMs_72M(1000); DelayMs_72M(1000); DelayMs_72M(1000); DelayMs_72M(1000); /*5. 关闭回显*/ printf("关闭回显...\n"); if(ESP8266_SendCmd("ATE0\r\n"))return 5; /*6. 设置创建的热点信息*/ printf("设置创建的热点信息...\n"); snprintf(cmd_buffer,100,"AT+CWSAP=\"%s\",\"%s\",1,4\r\n",ssid,password); if(ESP8266_SendCmd(cmd_buffer))return 6; /*7. 开启多连接,在服务器模式下才可开启*/ printf("开启多连接...\n"); if(ESP8266_SendCmd("AT+CIPMUX=1\r\n"))return 7; /*8. 设置端口号(0~65535)*/ printf("设置端口号...\n"); snprintf(cmd_buffer,100,"AT+CIPSERVER=1,%d\r\n",port); if(ESP8266_SendCmd(cmd_buffer))return 8; /*9.查询本地IP地址*/ printf("查询本地IP地址...\n"); if(ESP8266_SendCmd("AT+CIFSR\r\n"))return 9; /*10. 保存WIFI的信息*/ esp8266_wifi.port=port; strcpy(esp8266_wifi.mode,"AP"); //提取IP地址 IP地址=+CIFSR:APIP,"192.168.4.1" +CIFSR:APMAC,"86:f3:eb:17:e6:86" find_str=strstr((char*)USART3_RX_BUFF,"APIP"); if(find_str) //判断是否查找成功 find_str+=6; for(i=0;*find_str!='"';i++) esp8266_wifi.ip[i]=*find_str; find_str++; esp8266_wifi.ip[i]='\0'; USART3_RX_FLAG=0; USART3_RX_CNT=0; printf("当前WIFI模式:%s\n",esp8266_wifi.mode); printf("当前网络协议类型:%s\n","TCP"); printf("当前网络通信模式:%s\n","server"); printf("当前网络端口号:%d\n",esp8266_wifi.port); printf("本地网络IP地址:%s\n",esp8266_wifi.ip); return 0; 函数功能:向ESP8266wifi发送指令 说明:该函数只是适用于成功后返回OK的指令 返回值: 0表示成功 1表示失败 u8 ESP8266_SendCmd(char *cmd) u8 i,j; for(i=0;i<5;i++) //测试的总次数 USART3_RX_FLAG=0; USART3_RX_CNT=0; USART_X_SendString(USART3,cmd); for(j=0;j<200;j++) //等待的时间(ms单位) if(USART3_RX_FLAG) USART3_RX_BUFF[USART3_RX_CNT]='\0'; printf("USART3_RX_BUFF=%s\r\n",USART3_RX_BUFF); if(strstr((char*)USART3_RX_BUFF,"OK\r\n")) return 0; else break; DelayMs_72M(2); //一次的时间 return 1; 函数功能:ESP8266wifi处于TCP服务器模式下的数据发送函数 函数参数: u8 id :已经连接服务器的客户端ID号 char *data :将要发送数据(字符串) 返回值: 0表示成功 1表示失败 说明:只有在TCP服务器模式下才可使用该函数 Recv 12 bytes SEND OK u8 ESP8266_ServerSendData(u8 id,char *data) u32 i,j,n; char cmd[100]; snprintf(cmd,1024,"AT+CIPSEND=%d,%d\r\n",id,strlen(data)); for(i=0;i<5;i++) //测试的总次数 USART3_RX_FLAG=0; USART3_RX_CNT=0; USART_X_SendString(USART3,cmd); for(j=0;j<200;j++) //等待的时间(ms单位) if(USART3_RX_FLAG) USART3_RX_BUFF[USART3_RX_CNT]='\0'; if(strstr((char*)USART3_RX_BUFF,">")) //开始发送数据 USART3_RX_FLAG=0; USART3_RX_CNT=0; USART_X_SendString(USART3,data); //发送数据 for(n=0;n<10000;n++) if(USART3_RX_FLAG) USART3_RX_BUFF[USART3_RX_CNT]='\0'; if(strstr((char*)USART3_RX_BUFF,"SEND OK")) //开始发送数据 USART3_RX_FLAG=0; USART3_RX_CNT=0; return 0; else return 1; DelayMs_72M(1); //一次的时间 else break; DelayMs_72M(5); //一次的时间 return 1; 函数功能: 设置STA模式下连接的热点信息 返 回 值: 0表示成功,1表示失败 连接成功的提示符: WIFI DISCONNECT WIFI CONNECTED WIFI GOT IP u8 ESP8266_STA_ModeConnectWIFI(char *cmd) u32 i,j; for(i=0;i<3;i++) //测试的总次数 USART3_RX_FLAG=0; USART3_RX_CNT=0; USART_X_SendString(USART3,cmd); for(j=0;j<30000;j++) //等待的时间(ms单位) if(USART3_RX_FLAG) USART3_RX_BUFF[USART3_RX_CNT]='\0'; if(strstr((char*)USART3_RX_BUFF,"OK")) return 0; memset(USART3_RX_BUFF,0,sizeof(USART3_RX_BUFF)); USART3_RX_FLAG=0; USART3_RX_CNT=0; DelayMs_72M(1); //一次的时间 return 1; 函数功能: 用于在客户端模式下,连接服务器 返 回 值: 0表示成功,1表示失败 连接成功的提示符:CONNECT u8 ESP8266_ConnectServer(char *cmd) u32 i,j; for(i=0;i<5;i++) //测试的总次数 USART3_RX_FLAG=0; USART3_RX_CNT=0; USART_X_SendString(USART3,cmd); for(j=0;j<10000;j++) //等待的时间(ms单位) if(USART3_RX_FLAG) USART3_RX_BUFF[USART3_RX_CNT]='\0'; if(strstr((char*)USART3_RX_BUFF,"CONNECT")) return 0; memset(USART3_RX_BUFF,0,sizeof(USART3_RX_BUFF)); USART3_RX_FLAG=0; USART3_RX_CNT=0; DelayMs_72M(1); //一次的时间 return 1; 函数功能: 配置ESP8266WIFI为STA模式+TCP客户端模式 函数参数: char *ssid :将要连接的WIFI热点名称(英文字母) char *password :将要连接的WIFI热点密码(最短长度8位) u16 port :TCP服务器的端口号(0~65535) 返 回 值:0表示成功,其他值表示失败 u8 ESP8266_STA_TCP_ClientMode(const char *ssid,const char *password,char *server_ip,u16 port) u8 i; char *find_str=NULL; char cmd_buffer[100]; /*1. 发送测试指令,检测WIFI是否正常*/ printf("发送测试指令,检测WIFI是否正常...\n"); for(i=0;i<10;i++) if(ESP8266_SendCmd("AT\r\n")) //可能WIFI正处于透传模式,无法接收指令 USART_X_SendString(USART3,"+++"); //退出透传模式 DelayMs_72M(80); //等待退出透传模式 else break; if(i==10)return 1; //检测失败 /*2. 关闭回显*/ printf("关闭回显...\n"); if(ESP8266_SendCmd("ATE0\r\n"))return 2; /*3. 设置当前WIFI的模式为STA模式*/ printf("设置当前WIFI的模式为STA模式...\n"); if(ESP8266_SendCmd("AT+CWMODE=1\r\n"))return 3; /*4. 复位模块,设置的模式必须复位之后才会生效*/ printf("复位模块...\n"); if(ESP8266_SendCmd("AT+RST\r\n"))return 4; DelayMs_72M(1000); DelayMs_72M(1000); DelayMs_72M(1000); DelayMs_72M(1000); /*5. 关闭回显*/ printf("关闭回显...\n"); if(ESP8266_SendCmd("ATE0\r\n"))return 5; /*6. 设置连接的热点信息*/ printf("设置连接的热点信息...\r\n"); snprintf(cmd_buffer,100,"AT+CWJAP=\"%s\",\"%s\"\r\n",ssid,password); printf("cmd_buffer=%s",cmd_buffer); if(ESP8266_STA_ModeConnectWIFI(cmd_buffer))return 6; /*7. 开启单连接,在客户端模式下才可开启*/ printf("开启单连接...\n"); if(ESP8266_SendCmd("AT+CIPMUX=0\r\n"))return 8; /*8.查询本地IP地址*/ printf("查询本地IP地址...\n"); if(ESP8266_SendCmd("AT+CIFSR\r\n"))return 7; /*9. 保存WIFI的信息*/ esp8266_wifi.port=port; strcpy(esp8266_wifi.mode,"STA"); //提取IP地址 +CIFSR:STAIP,"192.168.1.115" +CIFSR:STAMAC,"84:f3:eb:17:e6:86" find_str=strstr((char*)USART3_RX_BUFF,"STAIP"); if(find_str) //判断是否查找成功 find_str+=7; for(i=0;*find_str!='"';i++) esp8266_wifi.ip[i]=*find_str; find_str++; esp8266_wifi.ip[i]='\0'; /*10. 连接服务器*/ printf("开始连接服务器...\n"); snprintf(cmd_buffer,100,"AT+CIPSTART=\"TCP\",\"%s\",%d\r\n",server_ip,port); if(ESP8266_ConnectServer(cmd_buffer))return 9; /*11. 开启透传模式*/ printf("开启透传模式...\n"); if(ESP8266_SendCmd("AT+CIPMODE=1\r\n"))return 10; /*12. 开始透传*/ printf("开始透传...\n"); if(ESP8266_SendCmd("AT+CIPSEND\r\n"))return 11; printf("当前WIFI模式:%s\r\n",esp8266_wifi.mode); printf("当前网络协议类型:%s\r\n","TCP"); printf("当前网络通信模式:%s\r\n","Client"); printf("连接的服务器端口号:%d\r\n",esp8266_wifi.port); printf("连接的服务器IP地址:%s\r\n",server_ip); printf("本地的IP地址:%s\r\n",esp8266_wifi.ip); return 0; }6.2 esp8266.h#ifndef ESP8266_H #define ESP8266_H #include "stm32f10x.h" #include "usart.h" #include "key.h" #include "led.h" #include <string.h> #include <stdlib.h> u8 ESP8266_SendCmd(char *cmd); u8 ESP8266_AP_TCP_ServerMode(const char *ssid,const char *password,u16 port); u8 ESP8266_ServerSendData(u8 id,char *data); u8 ESP8266_STA_TCP_ClientMode(const char *ssid,const char *password,char *server_ip,u16 port); struct ESP8266_WIFI u16 port; char mode[10]; //sta/ap char ip[10]; //ip地址 extern struct ESP8266_WIFI esp8266_wifi; #endif3.3 main.c#include "stm32f10x.h" #include "led.h" #include "key.h" #include "usart.h" #include <string.h> #include "adc.h" #include "esp8266.h" #include "timer.h" #include "rtc.h" #include "dht11.h" //网络协议层 #include "onenet.h" //协议封装文件 #include "dStream.h" /*WIFI信息配置---将要连接的WIFI信息*/ #define WIFI_NAME "Xiaomi_meizi6" #define WIFI_PASSWORD "12170307yu" /*Onenet服务器地址:固定的*/ #define SERVER_IP "183.230.40.39" #define SERVER_PORT 6002 //产品ID const char OneNetPROID[]="332761"; //鉴权信息 access_key const char OneNetAUTH_INFO[]="1234567890"; //设备ID char OneNetDEVID[]="590592359"; //ApiKey char OneNetAPI_KEY[]="fTgT3L9k3gyalPDMeojEEPrwzlo="; //onenet数据点定义 DATA_STREAM data_stream[]= {"DHT11_T","66",TYPE_STRING,1}, //温度 {"DHT11","66",TYPE_STRING,1}, //湿度 {"Light","66",TYPE_STRING,1}, //光照强度 {"MQ2","66",TYPE_STRING,1}, //烟雾浓度 {"MQ5","66",TYPE_STRING,1}, //液化气浓度 char *WIFI_STAT; //WIFI状态指针 //u8 temp,humi; char DisplayDataBuffer[20]; u8 GL5637_Info[10]; //光敏传感器信息 u8 TEMP_Info[10]; u8 HUMI_Info[10]; u8 MQ2_Info[10]; u8 MQ5_Info[10]; 工程内容介绍: 物联网开发平台全功能测试代码! 支持上位机控制 编写日期 : 20200403 版本 : v4.0 int main(void) int cnt_OneNet_time=0; u32 time_cnt=0; //记录时间 u16 GL5637_temp=0; //光敏信息 u8 ESP8266_Stat; u8 temp,humi; u16 mq2_temp,mq5_temp; LedInit(); KeyInit(); USART_X_Init(USART1,72,115200); USART_X_Init(USART3,36,115200); //WIFI的波特率为115200 ADC1_Init(); //ADC初始化 Timer2Init(72,10000); //10ms中断一次,辅助串口3接收数据--WIFI数据 printf("DHT11=%d\r\n",DHT11_Init()); //连接至指定的WIFI ESP8266_Stat=ESP8266_STA_TCP_ClientMode(WIFI_NAME,WIFI_PASSWORD,SERVER_IP,SERVER_PORT); if(ESP8266_Stat) printf("ESP8266_Stat=%d\r\n",ESP8266_Stat); WIFI_STAT="WIFIConnectERROR"; WIFI_STAT="WIFI Connect OK"; //接入OneNET while(OneNet_DevLink()) printf("Connect OneNet..\r\n"); DelayMs_72M(500); LED=!LED; while(1) cnt_OneNet_time++;//用于控制向服务器上传数据点的频率 /*轮询扫描数据*/ if(USART3_RX_FLAG) USART3_RX_BUFF[USART3_RX_CNT]='\0'; //解析平台返回的数据 OneNet_RevPro(USART3_RX_BUFF); USART3_RX_CNT=0; USART3_RX_FLAG=0; memset(USART3_RX_BUFF,0,sizeof(USART3_RX_BUFF)); /*获取光敏数据*/ GL5637_temp=ADC1_GetCHxVal(8); sprintf((char*)GL5637_Info,"%d",GL5637_temp); //保存光敏传感器信息,上传给上位机 /*获取烟雾浓度数据*/ mq2_temp=ADC1_GetCHxVal(1); sprintf((char*)MQ2_Info,"%d",mq2_temp); //保存MQ2传感器信息,上传给上位机 /*获取液化气浓度数据*/ mq5_temp=ADC1_GetCHxVal(2); sprintf((char*)MQ5_Info,"%d",mq5_temp); //保存MQ5传感器信息,上传给上位机 /*获取温湿度*/ if(DHT11_Read_Data(&temp,&humi)==0) sprintf((char*)TEMP_Info,"%d",temp); //保存温度传感器信息,上传给上位机 sprintf((char*)HUMI_Info,"%d",humi); //保存温度传感器信息,上传给上位机 DelayMs_72M(1); //延时1ms time_cnt++; if(cnt_OneNet_time>=200 &&ESP8266_Stat==0) cnt_OneNet_time=0; //清0 data_stream[0].dataPoint=TEMP_Info; data_stream[1].dataPoint=HUMI_Info; data_stream[2].dataPoint=GL5637_Info; data_stream[3].dataPoint=MQ2_Info; data_stream[4].dataPoint=MQ5_Info; printf("GL5637_Info=%s\r\n",GL5637_Info); printf("temp=%s,humi=%s\r\n",TEMP_Info,HUMI_Info); printf("MQ2_Info=%s\r\n",MQ2_Info); printf("MQ5_Info=%s\r\n",MQ5_Info); //向OneNet云端服务器上传数据点 OneNet_SendData(FORMAT_TYPE1,OneNetDEVID,OneNetAPI_KEY,data_stream,5); printf("发送成功\r\n"); /*轮询扫描数据*/ if(USART3_RX_FLAG) USART3_RX_BUFF[USART3_RX_CNT]='\0'; //解析平台返回的数据 OneNet_RevPro(USART3_RX_BUFF); USART3_RX_CNT=0; USART3_RX_FLAG=0; memset(USART3_RX_BUFF,0,sizeof(USART3_RX_BUFF));

基于CC2530(ZigBee)设计的景观照明控制系统+配套手机APP

一、环境介绍编译集成开发环境: IAR(安装包下载:https://download.csdn.net/download/xiaolong1126626497/19732120)MCU:CC2530(ZigBee)编程语言:  C语言手机APP:  采用QT设计,程序支持跨平台编译运行(Android、IOS、Windows、Linux都可以编译运行,对应平台上QT的环境搭建,之前博客已经发了文章讲解)硬件包含:  3套CC2530开发板(1个协调器、2个节点)、1个ESP8266串口WIFI、1个DHT11温湿度传感器、1个RGB多彩灯、1个BH1750光强度检测传感器.完整项目源码下载地址: https://download.csdn.net/download/xiaolong1126626497/19730948资料包里包含了:  CC2530协调器源码、2个节点源码、手机APP源码、Windows上位机源码、手机APP可执行文件、IAR下载器驱动、IAR资料等等。二、功能介绍这是基于CC2530设计的景观照明控制系统,一共包含了3个CC2530节点(就是3块CC2530开发板)。下面将这个3个CC2530开发板称为A、B、C节点。A节点: 当做协调器、可以接收BC节点上传的数据;A模块配了一个ESP8266 WIFI模块,可以连接手机APP,将BC节点上传温湿度数据再上传给手机APP显示。如果A节点在一定时间内没有收到B、C节点的数据,就会通知手机APP,告诉用户,B、C节点已经掉线。B节点:  作为RGB多彩灯+温湿度检测节点,会根据当前温湿度调整当前RGB灯的颜色,用于告诉景区的游客,当前景区的温湿度情况,采集的温湿度也会通过CC2530传递给A节点。C节点: 光照强度检测+LED灯节点。 这里的LED灯就是模拟景区的路灯,手机APP可以控制LED灯的开关,如果是白天的时候,LED灯会自动关掉,天气变暗,自动打开。也可以设计时间,定时关灯。三、相关的硬件介绍3.1 DTH11 温湿度传感器DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器,它应用专用的数字模块采集技术和温湿度传感技术,确保产品具有极高的可靠性和卓越的长期稳定性。传感器包括一个电阻式感湿元件和一个NTC测温元件,并与一个高性能8位单片机相连接。因此该产品具有品质卓越、超快响应、抗干扰能力强、性价比极高等优点。每个DHT11传感器都在极为精确的湿度校验室中进行校准。校准系数以程序的形式存在OTP内存中,传感器内部在检测信号的处理过程中要调用这些校准系数。单线制串行接口,使系统集成变得简易快捷。超小的体积、极低的功耗,使其成为该类应用中,在苛刻应用场合的最佳选择。产品为4针单排引脚封装,连接方便。3.2 ESP8266 WIFIESP8266系列无线模块是高性价比WIFI SOC模组,该系列模块支持标准的IEEE802.11b/g/n协议,内置完整的TCP/IP协议栈。用户可以使用该系列模块为现有的设备添加联网功能,也可以构建独立的网络控制器。能卓越ESP8266EX 芯片内置超低功耗 Tensilica L106 32 位 RISC 处理器,CPU 时钟速度最⾼可达 160 MHz,⽀持实时操作系统 (RTOS) 和 Wi-Fi 协议栈,可将⾼达 80% 的处理能⼒应用于编程和开发。高度集成ESP8266 芯片高度集成天线开关、射频巴伦、功率放大器、低噪声接收放大器、滤波器等射频模块。模组尺寸小巧,尤其适用于空间受限的产品设计。认证齐全RF 认证:SRRC、FCC、CE-RED、KCC、TELEC/MIC、IC 和 NCC 认证;环保认证:RoHS、REACH;可靠性认证:HTOL、HTSL、μHAST、TCT、ESD。丰富的产品应用ESP8266 模组既可以通过 ESP-AT 指令固件,为外部主机 MCU 提供 Wi-Fi 连接功能;也可以作为独立 Wi-Fi MCU 运行,用户通过基于 RTOS 的 SDK 开发带 Wi-Fi 连接功能的产品。用户可以轻松实现开箱即用的云连接、低功耗运行模式,以及包括 WPA3 在内的 Wi-Fi 安全支持等功能。3.3 CC2530CC2530 是用于2.4-GHz IEEE 802.15.4、ZigBee 和RF4CE 应用的一个真正的片上系统(SoC)解决方案。它能够以非常低的总的材料成本建立强大的网络节点。CC2530 结合了领先的RF 收发器的优良性能,业界标准的增强型8051 CPU,系统内可编程闪存,8-KB RAM 和许多其它强大的功能。CC2530 有四种不同的闪存版本:CC2530F32/64/128/256,分别具有32/64/128/256KB 的闪存。CC2530 具有不同的运行模式,使得它尤其适应超低功耗要求的系统。运行模式之间的转换时间短进一步确保了低能源消耗。CC2530F256 结合了德州仪器的业界领先的黄金单元ZigBee 协议栈(Z-Stack™),提供了一个强大和完整的ZigBee 解决方案。CC2530F64 结合了德州仪器的黄金单元RemoTI,更好地提供了一个强大和完整的ZigBee RF4CE 远程控制解决方案。3.4 RGB多彩灯颜色:全彩 红绿蓝三基色亮度:高亮电压:5V输入:数字电平三基色原理显示多重颜色通过PWM端口控制实现全彩显示3.5 BH1750bh1750是16位数字输出型,环境光强度传感器。  四、节点硬件+手机APP介绍4.1 C节点: 光敏传感器+LED灯控制4.2  B节点: 多彩灯+温湿度4.3 A节点: WIFI + APP4.4  手机APP界面效果五、手机APP上位机源码六、CC2530节点源码节点源码较多,下面就贴一些关键代码。6.1 uart.c #include "uart.h" 函数功能:串口0初始化 void Init_Uart0(void) PERCFG&=~(1<<0); //串口0的引脚映射到位置1,即P0_2和P0_3 P0SEL|=0x3<<2; //将P0_2和P0_3端口设置成外设功能 U0BAUD = 216; //32MHz的系统时钟产生115200BPS的波特率 U0GCR&=~(0x1F<<0);//清空波特率指数 U0GCR|=11<<0; //32MHz的系统时钟产生115200BPS的波特率 U0UCR |= 0x80; //禁止流控,8位数据,清除缓冲器 U0CSR |= 0x3<<6; //选择UART模式,使能接收器 函数功能:UART0发送字符串函数 void UR0SendString(u8 *str) while(*str!='\0') U0DBUF = *str; //将要发送的1字节数据写入U0DBUF while(UTX0IF == 0);//等待数据发送完成 UTX0IF = 0; //清除发送完成标志,准备下一次发送 str++; 函数功能: 模仿printf风格的格式化打印功能 char USART0_PRINT_BUFF[200]; //格式化数据缓存数据 void USART0_Printf(const char *format,...) char *str=NULL; /*1. 格式化转换*/ va_list ap; // va_list---->char * va_start(ap,format); //初始化参数列表 vsprintf(USART0_PRINT_BUFF, format, ap); //格式化打印 va_end(ap); //结束参数获取 /*2. 串口打印*/ str=USART0_PRINT_BUFF;//指针赋值 while(*str!='\0') U0DBUF=*str; //发送一个字节的数据 str++; //指针自增,指向下一个数据 while(UTX0IF == 0);//等待数据发送完成 UTX0IF = 0; //清除发送完成标志,准备下一次发送 }6.2 DHT11.c #include "dht11.h" #include "delay.h" #define DATA_PIN P0_7 //温湿度定义 uchar ucharFLAG,uchartemp; uchar shidu_shi,shidu_ge,wendu_shi,wendu_ge=4; uchar ucharT_data_H,ucharT_data_L,ucharRH_data_H,ucharRH_data_L,ucharcheckdata; uchar ucharT_data_H_temp,ucharT_data_L_temp,ucharRH_data_H_temp,ucharRH_data_L_temp,ucharcheckdata_temp; uchar ucharcomdata; //延时函数 void Delay_us() //1 us延时 asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); void Delay_10us() //10 us延时 #if 0 Delay_us(); Delay_us(); Delay_us(); Delay_us(); Delay_us(); Delay_us(); Delay_us(); Delay_us(); Delay_us(); Delay_us(); #else int i = 10; while(i--); #endif void Delay_ms(unsigned int Time)//n ms延时 unsigned char i; while(Time--) for(i=0;i<100;i++) Delay_10us(); //温湿度传感 void COM(void) // 温湿写入 uchar i; for(i=0;i<8;i++) ucharFLAG=2; while((!DATA_PIN)&&ucharFLAG++); Delay_10us(); Delay_10us(); Delay_10us(); uchartemp=0; if(DATA_PIN)uchartemp=1; ucharFLAG=2; while((DATA_PIN)&&ucharFLAG++); if(ucharFLAG==1)break; ucharcomdata<<=1; ucharcomdata|=uchartemp; void DHT11(void) //温湿传感启动 DATA_PIN=0; Delay_ms(19); //>18MS DATA_PIN=1; P0DIR &= ~0x80; //重新配置IO口方向 Delay_10us(); Delay_10us(); Delay_10us(); Delay_10us(); if(!DATA_PIN) ucharFLAG=2; while((!DATA_PIN)&&ucharFLAG++); ucharFLAG=2; while((DATA_PIN)&&ucharFLAG++); COM(); ucharRH_data_H_temp=ucharcomdata; COM(); ucharRH_data_L_temp=ucharcomdata; COM(); ucharT_data_H_temp=ucharcomdata; COM(); ucharT_data_L_temp=ucharcomdata; COM(); ucharcheckdata_temp=ucharcomdata; DATA_PIN=1; uchartemp=(ucharT_data_H_temp+ucharT_data_L_temp+ucharRH_data_H_temp+ucharRH_data_L_temp); if(uchartemp==ucharcheckdata_temp) ucharRH_data_H=ucharRH_data_H_temp; ucharRH_data_L=ucharRH_data_L_temp; ucharT_data_H=ucharT_data_H_temp; ucharT_data_L=ucharT_data_L_temp; ucharcheckdata=ucharcheckdata_temp; wendu_shi=ucharT_data_H/10; wendu_ge=ucharT_data_H%10; shidu_shi=ucharRH_data_H/10; shidu_ge=ucharRH_data_H%10; else //没用成功读取,返回0 wendu_shi=0; wendu_ge=0; shidu_shi=0; shidu_ge=0; P0DIR |= 0x80; //IO口需要重新配置 }6.3 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)USART0_Printf("error:1\r\n"); IIC_WriteOneByteData(0x01); r_s=IIC_GetACK();//获取应答 if(r_s)USART0_Printf("error:2\r\n"); IIC_Stop(); //停止信号 IIC_Start(); //发送起始信号 IIC_WriteOneByteData(0x46); r_s=IIC_GetACK();//获取应答 if(r_s)USART0_Printf("error:3\r\n"); IIC_WriteOneByteData(0x01); r_s=IIC_GetACK();//获取应答 if(r_s)USART0_Printf("error:4\r\n"); IIC_Stop(); //停止信号 IIC_Start(); //发送起始信号 IIC_WriteOneByteData(0x46); r_s=IIC_GetACK();//获取应答 if(r_s)USART0_Printf("error:5\r\n"); IIC_WriteOneByteData(0x10); r_s=IIC_GetACK();//获取应答 if(r_s)USART0_Printf("error:6\r\n"); IIC_Stop(); //停止信号 DelayMs(300); //等待 IIC_Start(); //发送起始信号 IIC_WriteOneByteData(0x47); r_s=IIC_GetACK();//获取应答 if(r_s)USART0_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; 五、IAR环境搭建--搭建CC2530开发环境

基于STM32设计的智能插座+人体感应灯(ESP8266+人体感应+手机APP)

一、环境介绍MCU: STM32F103C8T6程序开发IDE: keil5STM32程序风格:  采用寄存器方式开发,注释齐全,执行效率高,方便移植手机APP:  采用QT设计,程序支持跨平台编译运行(Android、IOS、Windows、Linux都可以编译运行,对应平台上QT的环境搭建,之前博客已经发了文章讲解)硬件包含:  SRM32F103C8T6最小系统板、红外热释电人体感应模块、DHT11温湿度传感器、0.96寸单色OLED显示屏、ESP8266、继电器、RGB大功率白灯.完整工程源码下载地址(包含手机APP源码、Windows系统上位机源码、STM32工程、下载工具、原理图):  https://download.csdn.net/download/xiaolong1126626497/19702853二、功能介绍这是基于STM32设计的智能插座+人体感应灯。硬件包含:  1. SRM32F103C8T6最小系统板:  基础的系统板,引出了所有IO口2. 红外热释电人体感应模块: 用来检测人体3. DHT11温湿度传感器: 检测环境的温度、湿度4. 0.96寸单色OLED显示屏 : 显示状态信息。比如: WIFI状态、RTC时钟、插座状态、温湿度值5. ESP8266: 用来与手机APP之间通信6. 继电器:  模拟插座开关 7. RGB大功率白灯: 模拟正常的灯泡 支持的功能如下:1. 使用热释电人体感应模块检测人体,检测到人体自动开灯,30秒(时间可以根据要求调整)没有检测到人体就自动关灯。2. 检测环境温湿度,使用OLED显示屏在界面上实时显示出来。 如果环境温度高于阀值,强制关闭插座、如果湿度高于阀值,也会强制关闭插座;防止火灾隐患。  温度最高阀值设置为: 30°,湿度阀值为80%, 这些都可以根据设计要求调整。   并且RGB灯也会根据不同的温度阀值亮不同颜色的灯。 比如: 温度高于30°亮红色、温度20°黄色 、温度10°青色3. 设置ESP8266WIFI模块为AP模式(路由器模式),手机或者电脑可以连接到ESP8266.搭建局域网。4. 设计手机APP和电脑客户端软件,可以实时显示收到的温湿度数据(3秒上传一次).可以显示历史. 点击手机APP上的按钮,可以用来控制插座开关。5. OLED一共有4个页面。 RTC实时时钟显示页面、温湿度显示页面、智能插座开关状态页面、WIFI热点信息页面6. OLED显示屏的第一页是实时时钟页面,时间可以通过手机APP来校准。 在手机APP上有一个RTC校准按钮,点击一下就可以校准设备上的时间。三、使用的相关硬件介绍3.1 DTH11 温湿度传感器 DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器,它应用专用的数字模块采集技术和温湿度传感技术,确保产品具有极高的可靠性和卓越的长期稳定性。传感器包括一个电阻式感湿元件和一个NTC测温元件,并与一个高性能8位单片机相连接。因此该产品具有品质卓越、超快响应、抗干扰能力强、性价比极高等优点。每个DHT11传感器都在极为精确的湿度校验室中进行校准。校准系数以程序的形式存在OTP内存中,传感器内部在检测信号的处理过程中要调用这些校准系数。单线制串行接口,使系统集成变得简易快捷。超小的体积、极低的功耗,使其成为该类应用中,在苛刻应用场合的最佳选择。产品为4针单排引脚封装,连接方便。3.2  热释电传感器热释电红外传感器在结构上引入场效应管,其目的在于完成阻抗变换。由于热释电元输出的是电荷信号,并不能直接使用,因而需要用电阻将其转换为电压形式。故引入的N沟道结型场效应管应接成共漏形式来完成阻抗变换。热释电红外传感器由传感探测元、干涉滤光片和场效应管匹配器三部分组成。设计时应将高热电材料制成一定厚度的薄片,并在它的两面镀上金属电极,然后加电对其进行极化,这样便制成了热释电探测元。热释电红外传感器的外形如上图所示。其可以检测人体发出的红外线信号,并将其转换成电信号输出。传感器顶部的长方形窗口加有滤光片,可以使人体发出的9~10μm波长的红外线通过,而其它波长的红外线被滤除,这样便提高了抗干扰能。热释电红外传感器由滤光片、热释电探测元和前置放大器组成,补偿型热释电传感器还带有温度补偿元件,图所示为热释电传感器的内部结构。为防止外部环境对传感器输出信号的干扰,上述元件被真空封装在一个金属营内。热释电传感器的滤光片为带通滤光片,它封装在传感器壳体的顶端,使特定波长的红外辐射选择性地通过,到达热释电探测元+在其截止范围外的红外辐射则不能通过。    热释电探测元是热释电传感器的核心元件,它是在热释电晶体的两面镀上金属电极后,加电极化制成,相当于一个以热释电晶体为电介质的平板电容器。当它受到非恒定强度的红外光照射时,产生的温度变化导致其表面电极的电荷密度发生改变,从而产生热释电电流。前置放大器由一个高内阻的场效应管源极跟随器构成,通过阻抗变换,将热释电探测元微弱的电流信号转换为有用的电压信号输出。3.3 ESP8266串口WIFI模块ESP8266系列无线模块是高性价比WIFI SOC模组,该系列模块支持标准的IEEE802.11b/g/n协议,内置完整的TCP/IP协议栈。用户可以使用该系列模块为现有的设备添加联网功能,也可以构建独立的网络控制器。能卓越ESP8266EX 芯片内置超低功耗 Tensilica L106 32 位 RISC 处理器,CPU 时钟速度最⾼可达 160 MHz,⽀持实时操作系统 (RTOS) 和 Wi-Fi 协议栈,可将⾼达 80% 的处理能⼒应用于编程和开发。高度集成ESP8266 芯片高度集成天线开关、射频巴伦、功率放大器、低噪声接收放大器、滤波器等射频模块。模组尺寸小巧,尤其适用于空间受限的产品设计。认证齐全RF 认证:SRRC、FCC、CE-RED、KCC、TELEC/MIC、IC 和 NCC 认证;环保认证:RoHS、REACH;可靠性认证:HTOL、HTSL、μHAST、TCT、ESD。丰富的产品应用ESP8266 模组既可以通过 ESP-AT 指令固件,为外部主机 MCU 提供 Wi-Fi 连接功能;也可以作为独立 Wi-Fi MCU 运行,用户通过基于 RTOS 的 SDK 开发带 Wi-Fi 连接功能的产品。用户可以轻松实现开箱即用的云连接、低功耗运行模式,以及包括 WPA3 在内的 Wi-Fi 安全支持等功能。3.4 OLED显示屏OLED显示屏是利用有机电自发光二极管制成的显示屏。由于同时具备自发光有机电激发光二极管,不需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优异之特性,被认为是下一代的平面显示器新兴应用技术。有机发光二极管 (OLED)显示器越来越普遍,在手机、媒体播放器及小型入门级电视等产品中最为显著。不同于标准的液晶显示器,OLED 像素是由电流源所驱动。若要了解 OLED 电源供应如何及为何会影响显示器画质,必须先了解 OLED 显示器技术及电源供应需求。本文将说明最新的 OLED 显示器技术,并探讨主要的电源供应需求及解决方案,另外也介绍专为 OLED 电源供应需求而提出的创新性电源供应架构。背板技术造就软性显示器  高分辨率彩色主动式矩阵有机发光二极管 (AMOLED) 显示器需要采用主动式矩阵背板,此背板使用主动式开关进行各像素的开关。液晶 (LC) 显示器非晶硅制程已臻成熟,可供应低成本的主动式矩阵背板,并且可用于 OLED。许多公司正针对软性显示器开发有机薄膜晶体管 (OTFT) 背板制程,此一制程也可用于 OLED 显示器,以实现全彩软性显示器的推出。不论是标准或软性 OLED,都需要运用相同的电源供应及驱动技术。若要了解 OLED 技术、功能及其与电源供应之间的互动,必须深入剖析这项技术本身。OLED 显示器是一种自体发光显示器技术,完全不需要任何背光。OLED 采用的材质属于化学结构适用的有机材质。  OLED 技术需要电流控制驱动方法  OLED 具有与标准发光二极管 (LED) 相当类似的电气特性,亮度均取决于 LED 电流。若要开启和关闭 OLED 并控制 OLED 电流,需要使用薄膜晶体管 (TFT)的控制电路。OLED为自发光材料,不需用到背光板,同时视角广、画质均匀、反应速度快、较易彩色化、用简单驱动电路即可达到发光、制程简单、可制作成挠曲式面板,符合轻薄短小的原则,应用范围属于中小尺寸面板。显示方面:主动发光、视角范围大;响应速度快,图像稳定;亮度高、色彩丰富、分辨率高。工作条件:驱动电压低、能耗低,可与太阳能电池、集成电路等相匹配。适应性广:采用玻璃衬底可实现大面积平板显示;如用柔性材料做衬底,能制成可折叠的显示器。由于OLED是全固态、非真空器件,具有抗震荡、耐低温(-40℃)等特性,在军事方面也有十分重要的应用,如用作坦克、飞机等现代化武器的显示终端。3.5 LED大功率灯模块LED灯是一块电致发光的半导体材料芯片,用银胶或白胶固化到支架上,然后用银线或金线连接芯片和电路板,四周用环氧树脂密封,起到保护内部芯线的作用,最后安装外壳,所以 LED 灯的抗震性能好。LED(Light Emitting Diode),发光二极管,是一种能够将电能转化为可见光的固态的半导体器件,它可以直接把电转化为光。LED的心脏是一个半导体的晶片,晶片的一端附在一个支架上,一端是负极,另一端连接电源的正极,使整个晶片被环氧树脂封装起来。半导体晶片由两部分组成,一部分是P型半导体,在它里面空穴占主导地位,另一端是N型半导体,在这边主要是电子。但这两种半导体连接起来的时候,它们之间就形成一个P-N结。当电流通过导线作用于这个晶片的时候,电子就会被推向P区,在P区里电子跟空穴复合,然后就会以光子的形式发出能量,这就是LED灯发光的原理。而光的波长也就是光的颜色,是由形成P-N结的材料决定的。LED可以直接发出红、黄、蓝、绿、青、橙、紫、白色的光。3.6 STM32F103C8T6最小系统板STM32F系列属于中低端的32位ARM微控制器,该系列芯片是意法半导体(ST)公司出品,其内核是Cortex-M3。该系列芯片按片内Flash的大小可分为三大类:小容量(16K和32K)、中容量(64K和128K)、大容量(256K、384K和512K)。芯片集成定时器Timer,CAN,ADC,SPI,I2C,USB,UART等多种外设功能。内核--ARM 32位的Cortex-M3--最高72MHz工作频率,在存储器的0等待周期访问时可达1.25DMips/MHZ(DhrystONe2.1)--单周期乘法和硬件除法存储器--从16K到512K字节的闪存程序存储器(STM32F103XXXX中的第二个X表示FLASH容量,其中:“4”=16K,“6”=32K,“8”=64K,B=128K,C=256K,D=384K,E=512K)--最大64K字节的SRAM电源管理--2.0-3.6V供电和I/O引脚--上电/断电复位(POR/PDR)、可编程电压监测器(PVD)--4-16MHZ晶振--内嵌经出厂调校的8MHz的RC振荡器--内嵌带校准的40KHz的RC振荡器--产生CPU时钟的PLL--带校准的32KHz的RC振荡器低功耗--睡眠、停机和待机模式--Vbat为RTC和后备寄存器供电模数转换器--2个12位模数转换器,1us转换时间(多达16个输入通道)--转换范围:0至3.6V--双采样和保持功能--温度传感器DMA--2个DMA控制器,共12个DMA通道:DMA1有7个通道,DMA2有5个通道--支持的外设:定时器、ADC、SPI、USB、IIC和UART--多达112个快速I/O端口(仅Z系列有超过100个引脚)--26/37/51/80/112个I/O口,所有I/O口一块映像到16个外部中断;几乎所有的端口均可容忍5V信号调试模式--串行单线调试(SWD)和JTAG接口--多达8个定时器--3个16位定时器,每个定时器有多达4个用于输入捕获/输出比较/PWM或脉冲计数的通道和增量编码器输入--1个16位带死区控制和紧急刹车,用于电机控制的PWM高级控制定时器--2个看门狗定时器(独立的和窗口型的)--系统时间定时器:24位自减型计数器--多达9个通信接口:2个I2C接口(支持SMBus/PMBus)3个USART接口(支持ISO7816接口,LIN,IrDA接口和调制解调控制)2个SPI接口(18M位/秒)CAN接口(2.0B主动)USB 2.0全速接口计算单元CRC计算单元,96位的新批唯一代码封装ECOPACK封装3.7 杜邦线3.8 继电器四、STM32核心代码4.1  STM32:  main.c #include "stm32f10x.h" #include "beep.h" #include "delay.h" #include "led.h" #include "key.h" #include "sys.h" #include "usart.h" #include <string.h> #include <stdlib.h> #include "exti.h" #include "timer.h" #include "rtc.h" #include "wdg.h" #include "oled.h" #include "fontdata.h" #include "adc.h" #include "FunctionConfig.h" #include "dht11.h" #include "HumanDetection.h" #include "esp8266.h" 函数功能: 绘制时钟表盘框架 void DrawTimeFrame(void) u8 i; OLED_Circle(32,32,31);//画外圆 OLED_Circle(32,32,1); //画中心圆 //画刻度 for(i=0;i<60;i++) if(i%5==0)OLED_DrawAngleLine(32,32,6*i,31,3,1); OLED_RefreshGRAM(); //刷新数据到OLED屏幕 函数功能: 更新时间框架显示,在RTC中断里调用 char TimeBuff[20]; void Update_FrameShow(void) /*1. 绘制秒针、分针、时针*/ OLED_DrawAngleLine2(32,32,rtc_clock.sec*6-6-90,27,0);//清除之前的秒针 OLED_DrawAngleLine2(32,32,rtc_clock.sec*6-90,27,1); //画秒针 OLED_DrawAngleLine2(32,32,rtc_clock.min*6-6-90,24,0); OLED_DrawAngleLine2(32,32,rtc_clock.min*6-90,24,1); OLED_DrawAngleLine2(32,32,rtc_clock.hour*30-6-90,21,0); OLED_DrawAngleLine2(32,32,rtc_clock.hour*30-90,21,1); //绘制电子钟时间 sprintf(TimeBuff,"%d",rtc_clock.year); OLED_ShowString(65,16*0,16,TimeBuff); //年份字符串 OLED_ShowChineseFont(66+32,16*0,16,4); //显示年 sprintf(TimeBuff,"%d/%d",rtc_clock.mon,rtc_clock.day); OLED_ShowString(75,16*1,16,TimeBuff); //月 if(rtc_clock.sec==0)OLED_ShowString(65,16*2,16," "); //清除多余的数据 sprintf(TimeBuff,"%d:%d:%d",rtc_clock.hour,rtc_clock.min,rtc_clock.sec); OLED_ShowString(65,16*2,16,TimeBuff); //秒 //显示星期 OLED_ShowChineseFont(70,16*3,16,5); //星 OLED_ShowChineseFont(70+16,16*3,16,6); //期 OLED_ShowChineseFont(70+32,16*3,16,rtc_clock.week+7); //具体的值 函数功能: 温湿度显示 void ShowTemperatureAndHumidity(u8 temp,u8 humi) sprintf(TimeBuff,"T: %d",temp); OLED_ShowString(40,16*1,16,TimeBuff); sprintf(TimeBuff,"H: %d%%",humi); OLED_ShowString(40,16*2,16,TimeBuff); 函数功能: OLED所有显示页面信息初始化 u8 ESP8266_Stat=0; //存放ESP8266状态 1 OK ,0 error u8 ESP8266_WIFI_AP_SSID[10]; //存放WIFI的名称 #define STM32_96BIT_UID (0x1FFFF7E8) //STM32内部96位唯一芯片标识符寄存器地址 函数功能: ESP8266 WIFI 显示页面 char ESP8266_PwdShow[20]; char ESP8266_IP_PortAddr[30]; //存放ESP8266 IP地址与端口号地址 u8 Save_ESP8266_SendCmd[30]; //保存WIFI发送的命令 u8 Save_ESP8266_SendData[50]; //保存WIFI发送的数据 void ESP8266_ShowPageTable(void) if(ESP8266_Stat)OLED_ShowString(0,16*0,16,"WIFI STAT:ERROR"); else OLED_ShowString(0,16*0,16,"WIFI STAT:OK"); //显示字符串 //OLED_ShowString(0,2,(u8*)" "); //清除一行的显示 memset((u8*)ESP8266_PwdShow,0,20); //清空内存 sprintf((char*)ESP8266_PwdShow,"WIFI:%s",ESP8266_WIFI_AP_SSID); ESP8266_PwdShow[15]='\0'; OLED_ShowString(0,16*1,16,ESP8266_PwdShow); memset((u8*)ESP8266_PwdShow,0,20); //清空内存 sprintf((char*)ESP8266_PwdShow,"PWD:%s",wifiap_password); OLED_ShowString(0,16*2,16,ESP8266_PwdShow); OLED_ShowString(0,16*3,16,"192.168.4.1:8089"); void OledShowPageTableInit(void) u8 data[100],i; u32 stm32_uid=0; u8 *uid; /*1. ESP8266 WIFI相关信息初始化*/ OLED_ShowString(0,2,16," "); //清空一行的显示 OLED_ShowString(0,2,16,"WifiInit..."); //显示页面提示信息 /*1.1 设置WIFI AP模式 */ if(ESP8266_SendCmd("AT+CWMODE=2\r\n","OK",50)) ESP8266_Stat=1; //OK ESP8266_Stat=0; //ERROR /*1.2 重启模块 */ ESP8266_SendCmd("AT+RST\r\n","OK",20); /*1.3 延时3S等待重启成功*/ DelayMs(1000); DelayMs(1000); DelayMs(1000); //得到WIFI的名称 uid=(u8*)STM32_96BIT_UID; //转为指针为1个字节 for(i=0;i<96;i++) stm32_uid+=*uid++; //计算校验和,得到WIFI数字编号 printf("stm32_uid=%d\r\n",stm32_uid); sprintf((char*)data,"%d",stm32_uid); strcpy((char*)ESP8266_WIFI_AP_SSID,"wbyq_"); strcat((char*)ESP8266_WIFI_AP_SSID,(char*)data); printf("请用设备连接WIFI热点:%s,%s,%s\r\n",(u8*)ESP8266_WIFI_AP_SSID,(u8*)wifiap_encryption,(u8*)wifiap_password); /*1.4 配置模块AP模式无线参数*/ memset(data,0,100); //清空数组 sprintf((char*)data,"AT+CWSAP=\"%s\",\"%s\",1,4\r\n",ESP8266_WIFI_AP_SSID,wifiap_password); ESP8266_SendCmd(data,"OK",1000); /*1.5 设置多连接模式:0单连接,1多连接(服务器模式必须开启)*/ ESP8266_SendCmd("AT+CIPMUX=1\r\n","OK",20); /*1.6 开启Server模式(0,关闭;1,打开),端口号为portnum */ memset(data,0,100); //清空数组 sprintf((char*)data,"AT+CIPSERVER=1,%s\r\n",(u8*)portnum); ESP8266_SendCmd(data,"OK",50); /*1.7 获取当前模块的IP*/ ESP8266_GetWanip((u8*)ESP8266_IP_PortAddr); strcat(ESP8266_IP_PortAddr,":"); strcat(ESP8266_IP_PortAddr,portnum); printf("IP地址:%s\r\n",ESP8266_IP_PortAddr); OLED_ShowString(0,2,16," "); //清空一行的显示 OLED_ShowString(0,2,16,"WifiInitOk"); //显示页面提示信息 函数功能: 显示开关状态 void Show_Switch(int state) OLED_Clear(0); if(state) sprintf(TimeBuff,"Socket:ON"); OLED_ShowString(20,16*2,16,TimeBuff); sprintf(TimeBuff,"Socket:OFF"); OLED_ShowString(20,16*2,16,TimeBuff); //int main1(void) // HumanDetection_Init(); //热释电模块初始化 // UsartInit(USART1,72,115200);//串口1的初始化 // LED_Init(); //初始化LED // while(1) // { // //热释电状态 为真表示有人 // if(HumanState) // { // LED1=0; //开灯 // } // else //为假 // { // LED1=1; //关灯 // } // printf("%d\n",HumanState); // } int main(void) u8 rlen; char *p; u8 temp; //温度 u8 humi; //湿度 u8 stat; u8 key_val; u32 TimeCnt=0; u32 wifi_TimeCnt=0; u16 temp_data; //温度数据 u8 page_cnt=0; //显示的页面 char *time; u8 socket_state=0; UsartInit(USART1,72,115200);//串口1的初始化 BEEP_Init(); //初始化蜂鸣器 LED_Init(); //初始化LED KEY_Init(); //按键初始化 printf("正在初始化OLED...\r\n"); OLED_Init(0xc8,0xa1); //OLED显示屏初始化--正常显示 //OLED_Init(0xc0,0xa0); //OLED显示屏初始化--翻转显示 OLED_Clear(0x00); //清屏 UsartInit(USART3,36,115200); //WIFI的波特率为115200 Timer2Init(72,10000); //10ms中断一次,辅助串口3接收数据--WIFI数据 printf("正在初始化ESP8266..\r\n"); //ESP8266初始化 OledShowPageTableInit(); OLED_Clear(0); printf("正在初始化RTC...\r\n"); RTC_Init(); //RTC初始化 DrawTimeFrame(); //画时钟框架 USART3_RX_STA=0; //清空串口的接收标志位 USART3_RX_CNT=0; //清空计数器 printf("初始化DHT11...\r\n"); DHT11_Init(); //初始化DHT11 printf("热释电模块初始化...\r\n"); HumanDetection_Init(); //热释电模块初始化 //OLED_Clear(0); printf("开始进入while(1)\r\n"); while(1) key_val=KEY_GetValue(); if(key_val) page_cnt++; printf("page_cnt:%d\r\n",page_cnt); OLED_Clear(0); //时钟页面 if(page_cnt==0) DrawTimeFrame(); //画时钟框架 RTC->CRH|=1<<0; //开启秒中断 //温湿度页面 else if(page_cnt==1) RTC->CRH&=~(1<<0); //关闭秒中断 //温湿度 ShowTemperatureAndHumidity(temp,humi); //ESP8266显示 else if(page_cnt==2) ESP8266_ShowPageTable(); else if(page_cnt==3) Show_Switch(socket_state); DrawTimeFrame(); //画时钟框架 RTC->CRH|=1<<0; //开启秒中断 page_cnt=0; //时间记录 DelayMs(10); TimeCnt++; wifi_TimeCnt++; if(TimeCnt>=100) //1000毫秒一次 TimeCnt=0; //读取温湿度数据 DHT11_Read_Data(&temp,&humi); //湿度大于90就关闭开关 if(humi>90) socket_state=0; if(page_cnt==3) Show_Switch(socket_state); //温湿度页面 if(page_cnt==1) //温湿度 ShowTemperatureAndHumidity(temp,humi); if(wifi_TimeCnt>=300) //3000毫秒一次 wifi_TimeCnt=0; //温湿度1秒上传一次 sprintf((char*)Save_ESP8266_SendData,"#%d,%d",temp,humi); //拼接数据 sprintf((char*)Save_ESP8266_SendCmd,"AT+CIPSEND=0,%d\r\n",strlen((char*)Save_ESP8266_SendData)); ESP8266_SendCmd(Save_ESP8266_SendCmd,(u8*)"OK",200); //WIFI设置发送数据长度 ESP8266_SendData(Save_ESP8266_SendData,(u8*)"OK",100); //WIFI发送数据 //热释电状态 为真表示有人 if(HumanState) LED1=0; //开灯 else //为假 LED1=1; //关灯 /*轮询扫描数据*/ if(USART3_RX_STA) //WIFI 接收到一次数据了 rlen=USART3_RX_CNT; //得到本次接收到的数据长度 USART3_RX_BUF[rlen]='\0'; //添加结束符 // printf("接收的数据: %s\r\n",USART3_RX_BUF); //发送到串口 /*判断是否收到客户端发来的数据 */ p=strstr((char*)USART3_RX_BUF,"+IPD"); if(p!=NULL) //正常数据格式: +IPD,0,7:LED1_ON +IPD,0表示第0个客户端 7:LED1_ON表示数据长度与数据 /*解析上位机发来的数据*/ p=strstr((char*)USART3_RX_BUF,":"); if(p!=NULL) p+=1; //向后偏移1个字节 if(*p=='*') //设置RTC时间 p+=1; //向后偏移,指向正确的时间 time=p; rtc_clock.year=(time[0]-48)*1000+(time[1]-48)*100+(time[2]-48)*10+(time[3]-48)*1; rtc_clock.mon=(time[4]-48)*10+(time[5]-48)*1; rtc_clock.day=(time[6]-48)*10+(time[7]-48)*1; rtc_clock.hour=(time[8]-48)*10+(time[9]-48)*1; rtc_clock.min=(time[10]-48)*10+(time[11]-48)*1; rtc_clock.sec=(time[12]-48)*10+(time[13]-48)*1; RTC_SetTime(rtc_clock.year,rtc_clock.mon,rtc_clock.day,rtc_clock.hour,rtc_clock.min,rtc_clock.sec); if(page_cnt==0) OLED_Clear(0); //OLED清屏 DrawTimeFrame();//画时钟框架 else if(strcmp(p,"LED1_ON")==0) socket_state=1; else if(strcmp(p,"LED1_OFF")==0) socket_state=0; if(page_cnt==3) Show_Switch(socket_state); USART3_RX_STA=0; USART3_RX_CNT=0; 4.2  STM32:  rtc.c#include "rtc.h" //定义RTC标准结构体 struct RTC_CLOCK rtc_clock; 函数功能: RTC初始化函数 void RTC_Init(void) //检查是不是第一次配置时钟 u8 temp=0; if(BKP->DR1!=0X5051)//之前使用的不是LSE RCC->APB1ENR|=1<<28; //使能电源时钟 RCC->APB1ENR|=1<<27; //使能备份时钟 PWR->CR|=1<<8; //取消备份区写保护 RCC->BDCR|=1<<16; //备份区域软复位 RCC->BDCR&=~(1<<16); //备份区域软复位结束 RCC->BDCR|=1<<0; //开启外部低速振荡器 while((!(RCC->BDCR&0X02))&&temp<250)//等待外部时钟就绪 temp++; DelayMs(10); if(temp>=250) //return 1;//初始化时钟失败,晶振有问题 RCC->CSR|=1<<0; //开启外部低速振荡器 while(!(RCC->CSR&(1<<1)));//外部低速振荡器 RCC->BDCR|=2<<8; //LSI作为RTC时钟 BKP->DR1=0X5050; //标记使用LSI作为RTC时钟 RCC->BDCR|=1<<8; //LSE作为RTC时钟 BKP->DR1=0X5051; //标记使用LSE作为RTC时钟 RCC->BDCR|=1<<15;//RTC时钟使能 while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成 while(!(RTC->CRL&(1<<3)));//等待RTC寄存器同步 RTC->CRH|=0X01; //允许秒中断 while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成 RTC->CRL|=1<<4; //允许配置 RTC->PRLH=0X0000; RTC->PRLL=32767; //时钟周期设置(有待观察,看是否跑慢了?)理论值:32767 RTC_SetTime(2021,4,25,20,36,20); //设置时间 RTC->CRL&=~(1<<4); //配置更新 while(!(RTC->CRL&(1<<5))); //等待RTC寄存器操作完成 printf("FIRST TIME\n"); }else//系统继续计时 while(!(RTC->CRL&(1<<3)));//等待RTC寄存器同步 RTC->CRH|=0X01; //允许秒中断 while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成 printf("OK\n"); STM32_NVIC_SetPriority(RTC_IRQn,2,2); //优先级 extern void Update_FrameShow(void); 函数功能: RTC闹钟中断服务函数 void RTC_IRQHandler(void) u32 SecCnt; if(RTC->CRL&1<<0) SecCnt=RTC->CNTH<<16;//获取高位 SecCnt|=RTC->CNTL; //获取低位 RTC_GetTime(SecCnt); //转换标准时间 RTC_GetWeek(SecCnt); // printf("%d-%d-%d %d:%d:%d week:%d\n",rtc_clock.year,rtc_clock.mon,rtc_clock.day,rtc_clock.hour,rtc_clock.min,rtc_clock.sec,rtc_clock.week); Update_FrameShow(); //更新显示 RTC->CRL&=~(1<<0); //清除秒中断标志位 if(RTC->CRL&1<<1) // printf("闹钟时间到达!....\n"); // BEEP=1; // DelayMs(500); // BEEP=0; RTC->CRL&=~(1<<1); //清除闹钟中断标志位 //闰年的月份 static int mon_r[12]={31,29,31,30,31,30,31,31,30,31,30,31}; //平年的月份 static int mon_p[12]={31,28,31,30,31,30,31,31,30,31,30,31}; 函数功能: 设置RTC时间 函数形参: u32 year; 2018 u32 mon; 8 u32 day; u32 hour; u32 min; u32 sec; void RTC_SetTime(u32 year,u32 mon,u32 day,u32 hour,u32 min,u32 sec) u32 i; u32 SecCnt=0; //总秒数 /*1. 累加已经过去的年份*/ for(i=2017;i<year;i++) //基准年份:20170101000000 if(RTC_GetYearState(i)) SecCnt+=366*24*60*60; //闰年一年的秒数 SecCnt+=365*24*60*60; //平年一年的秒数 /*2. 累加过去的月份*/ for(i=0;i<mon-1;i++) if(RTC_GetYearState(year)) SecCnt+=mon_r[i]*24*60*60; //闰年一月的秒数 SecCnt+=mon_p[i]*24*60*60; //平年一月的秒数 /*3. 累加过去的天数*/ SecCnt+=(day-1)*24*60*60; /*4. 累加过去小时*/ SecCnt+=hour*60*60; /*5. 累加过去的分钟*/ SecCnt+=min*60; /*6. 累加过去的秒*/ SecCnt+=sec; /*7. 设置RTC时间*/ //设置时钟 RCC->APB1ENR|=1<<28;//使能电源时钟 RCC->APB1ENR|=1<<27;//使能备份时钟 PWR->CR|=1<<8; //取消备份区写保护 //上面三步是必须的! RTC->CRL|=1<<4; //允许配置 RTC->CNTL=SecCnt&0xffff; RTC->CNTH=SecCnt>>16; RTC->CRL&=~(1<<4);//配置更新 while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成 函数功能: 获取RTC时间 函数参数: u32 sec 秒单位时间 void RTC_GetTime(u32 sec) u32 i; rtc_clock.year=2017; //基准年份 /*1. 计算当前的年份*/ while(1) if(RTC_GetYearState(rtc_clock.year)) if(sec>=366*24*60*60) //够一年 sec-=366*24*60*60; rtc_clock.year++; else break; if(sec>=365*24*60*60) //够一年 sec-=365*24*60*60; rtc_clock.year++; else break; /*2. 计算当前的月份*/ rtc_clock.mon=1; for(i=0;i<12;i++) if(RTC_GetYearState(rtc_clock.year)) if(sec>=mon_r[i]*24*60*60) sec-=mon_r[i]*24*60*60; rtc_clock.mon++; else break; if(sec>=mon_p[i]*24*60*60) sec-=mon_p[i]*24*60*60; rtc_clock.mon++; else break; /*3. 计算当前的天数*/ rtc_clock.day=1; while(1) if(sec>=24*60*60) sec-=24*60*60; rtc_clock.day++; else break; /*4. 计算当前的小时*/ rtc_clock.hour=0; while(1) if(sec>=60*60) sec-=60*60; rtc_clock.hour++; else break; /*5. 计算当前的分钟*/ rtc_clock.min=0; while(1) if(sec>=60) sec-=60; rtc_clock.min++; else break; /*6. 计算当前的秒*/ rtc_clock.sec=sec; 函数功能: 判断年份是否是平年、闰年 返回值 : 0表示平年 1表示闰年 u8 RTC_GetYearState(u32 year) if((year%4==0&&year%100!=0)||year%400==0) return 1; return 0; 函数功能: 获取星期 void RTC_GetWeek(u32 sec) u32 day1=sec/(60*60*24); //将秒单位时间转为天数 switch(day1%7) case 0: rtc_clock.week=0; break; case 1: rtc_clock.week=1; break; case 2: rtc_clock.week=2; break; case 3: rtc_clock.week=3; break; case 4: rtc_clock.week=4; break; case 5: rtc_clock.week=5; break; case 6: rtc_clock.week=6; break; 4.3 ESP8266.c#include "esp8266.h" 函数功能:向ESP82668266发送命令 函数参数: cmd:发送的命令字符串 ack:期待的应答结果,如果为空,则表示不需要等待应答 waittime:等待时间(单位:10ms) 返 回 值: 0,发送成功(得到了期待的应答结果) 1,发送失败 u8 ESP8266_SendCmd(u8 *cmd,u8 *ack,u16 waittime) u8 res=0; USART3_RX_STA=0; USART3_RX_CNT=0; UsartStringSend(USART3,cmd);//发送命令 if(ack&&waittime) //需要等待应答 while(--waittime) //等待倒计时 DelayMs(10); if(USART3_RX_STA)//接收到期待的应答结果 if(ESP8266_CheckCmd(ack)) res=0; //printf("cmd->ack:%s,%s\r\n",cmd,(u8*)ack); break;//得到有效数据 USART3_RX_STA=0; USART3_RX_CNT=0; if(waittime==0)res=1; return res; 函数功能:ESP8266发送命令后,检测接收到的应答 函数参数:str:期待的应答结果 返 回 值:0,没有得到期待的应答结果 其他,期待应答结果的位置(str的位置) u8* ESP8266_CheckCmd(u8 *str) char *strx=0; if(USART3_RX_STA) //接收到一次数据了 USART3_RX_BUF[USART3_RX_CNT]=0;//添加结束符 strx=strstr((const char*)USART3_RX_BUF,(const char*)str); //查找是否应答成功 //printf("RX=%s",USART3_RX_BUF); return (u8*)strx; 函数功能:向ESP8266发送指定数据 函数参数: data:发送的数据(不需要添加回车) ack:期待的应答结果,如果为空,则表示不需要等待应答 waittime:等待时间(单位:10ms) 返 回 值:0,发送成功(得到了期待的应答结果)luojian u8 ESP8266_SendData(u8 *data,u8 *ack,u16 waittime) u8 res=0; USART3_RX_STA=0; UsartStringSend(USART3,data);//发送数据 if(ack&&waittime) //需要等待应答 while(--waittime) //等待倒计时 DelayMs(10); if(USART3_RX_STA)//接收到期待的应答结果 if(ESP8266_CheckCmd(ack))break;//得到有效数据 USART3_RX_STA=0; USART3_RX_CNT=0; if(waittime==0)res=1; return res; 函数功能:ESP8266退出透传模式 返 回 值:0,退出成功; 1,退出失败 u8 ESP8266_QuitTrans(void) while((USART3->SR&0X40)==0); //等待发送空 USART3->DR='+'; DelayMs(15); //大于串口组帧时间(10ms) while((USART3->SR&0X40)==0); //等待发送空 USART3->DR='+'; DelayMs(15); //大于串口组帧时间(10ms) while((USART3->SR&0X40)==0); //等待发送空 USART3->DR='+'; DelayMs(500); //等待500ms return ESP8266_SendCmd("AT\r\n","OK",20);//退出透传判断. 函数功能:获取ESP8266模块的连接状态 返 回 值:0,未连接;1,连接成功. u8 ESP8266_ConstaCheck(void) u8 *p; u8 res; if(ESP8266_QuitTrans())return 0; //退出透传 ESP8266_SendCmd("AT+CIPSTATUS\r\n",":",50); //发送AT+CIPSTATUS指令,查询连接状态 p=ESP8266_CheckCmd("+CIPSTATUS\r\n:"); res=*p; //得到连接状态 return res; 函数功能:获取ip地址 函数参数:ipbuf:ip地址输出缓存区 void ESP8266_GetWanip(u8* ipbuf) u8 *p,*p1; if(ESP8266_SendCmd("AT+CIFSR\r\n","OK",50))//获取WAN IP地址失败 ipbuf[0]=0; return; p=ESP8266_CheckCmd("\""); p1=(u8*)strstr((const char*)(p+1),"\""); *p1=0; sprintf((char*)ipbuf,"%s",p+1); 五、QT设计的上位机代码:  Android手机APP+Windows系统上位机

基于STM32设计的智能插座+人体感应灯(ESP8266+人体感应+手机APP)

一、环境介绍MCU: STM32F103C8T6程序开发IDE: keil5STM32程序风格:  采用寄存器方式开发,注释齐全,执行效率高,方便移植手机APP:  采用QT设计,程序支持跨平台编译运行(Android、IOS、Windows、Linux都可以编译运行,对应平台上QT的环境搭建,之前博客已经发了文章讲解)硬件包含:  SRM32F103C8T6最小系统板、红外热释电人体感应模块、DHT11温湿度传感器、0.96寸单色OLED显示屏、ESP8266、继电器、RGB大功率白灯.完整工程源码下载地址(包含手机APP源码、Windows系统上位机源码、STM32工程、下载工具、原理图):  https://download.csdn.net/download/xiaolong1126626497/19702853二、功能介绍这是基于STM32设计的智能插座+人体感应灯。硬件包含:  1. SRM32F103C8T6最小系统板:  基础的系统板,引出了所有IO口2. 红外热释电人体感应模块: 用来检测人体3. DHT11温湿度传感器: 检测环境的温度、湿度4. 0.96寸单色OLED显示屏 : 显示状态信息。比如: WIFI状态、RTC时钟、插座状态、温湿度值5. ESP8266: 用来与手机APP之间通信6. 继电器:  模拟插座开关 7. RGB大功率白灯: 模拟正常的灯泡 支持的功能如下:1. 使用热释电人体感应模块检测人体,检测到人体自动开灯,30秒(时间可以根据要求调整)没有检测到人体就自动关灯。2. 检测环境温湿度,使用OLED显示屏在界面上实时显示出来。 如果环境温度高于阀值,强制关闭插座、如果湿度高于阀值,也会强制关闭插座;防止火灾隐患。  温度最高阀值设置为: 30°,湿度阀值为80%, 这些都可以根据设计要求调整。   并且RGB灯也会根据不同的温度阀值亮不同颜色的灯。 比如: 温度高于30°亮红色、温度20°黄色 、温度10°青色3. 设置ESP8266WIFI模块为AP模式(路由器模式),手机或者电脑可以连接到ESP8266.搭建局域网。4. 设计手机APP和电脑客户端软件,可以实时显示收到的温湿度数据(3秒上传一次).可以显示历史. 点击手机APP上的按钮,可以用来控制插座开关。5. OLED一共有4个页面。 RTC实时时钟显示页面、温湿度显示页面、智能插座开关状态页面、WIFI热点信息页面6. OLED显示屏的第一页是实时时钟页面,时间可以通过手机APP来校准。 在手机APP上有一个RTC校准按钮,点击一下就可以校准设备上的时间。三、使用的相关硬件介绍3.1 DTH11 温湿度传感器 DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器,它应用专用的数字模块采集技术和温湿度传感技术,确保产品具有极高的可靠性和卓越的长期稳定性。传感器包括一个电阻式感湿元件和一个NTC测温元件,并与一个高性能8位单片机相连接。因此该产品具有品质卓越、超快响应、抗干扰能力强、性价比极高等优点。每个DHT11传感器都在极为精确的湿度校验室中进行校准。校准系数以程序的形式存在OTP内存中,传感器内部在检测信号的处理过程中要调用这些校准系数。单线制串行接口,使系统集成变得简易快捷。超小的体积、极低的功耗,使其成为该类应用中,在苛刻应用场合的最佳选择。产品为4针单排引脚封装,连接方便。3.2  热释电传感器热释电红外传感器在结构上引入场效应管,其目的在于完成阻抗变换。由于热释电元输出的是电荷信号,并不能直接使用,因而需要用电阻将其转换为电压形式。故引入的N沟道结型场效应管应接成共漏形式来完成阻抗变换。热释电红外传感器由传感探测元、干涉滤光片和场效应管匹配器三部分组成。设计时应将高热电材料制成一定厚度的薄片,并在它的两面镀上金属电极,然后加电对其进行极化,这样便制成了热释电探测元。热释电红外传感器的外形如上图所示。其可以检测人体发出的红外线信号,并将其转换成电信号输出。传感器顶部的长方形窗口加有滤光片,可以使人体发出的9~10μm波长的红外线通过,而其它波长的红外线被滤除,这样便提高了抗干扰能。热释电红外传感器由滤光片、热释电探测元和前置放大器组成,补偿型热释电传感器还带有温度补偿元件,图所示为热释电传感器的内部结构。为防止外部环境对传感器输出信号的干扰,上述元件被真空封装在一个金属营内。热释电传感器的滤光片为带通滤光片,它封装在传感器壳体的顶端,使特定波长的红外辐射选择性地通过,到达热释电探测元+在其截止范围外的红外辐射则不能通过。    热释电探测元是热释电传感器的核心元件,它是在热释电晶体的两面镀上金属电极后,加电极化制成,相当于一个以热释电晶体为电介质的平板电容器。当它受到非恒定强度的红外光照射时,产生的温度变化导致其表面电极的电荷密度发生改变,从而产生热释电电流。前置放大器由一个高内阻的场效应管源极跟随器构成,通过阻抗变换,将热释电探测元微弱的电流信号转换为有用的电压信号输出。3.3 ESP8266串口WIFI模块ESP8266系列无线模块是高性价比WIFI SOC模组,该系列模块支持标准的IEEE802.11b/g/n协议,内置完整的TCP/IP协议栈。用户可以使用该系列模块为现有的设备添加联网功能,也可以构建独立的网络控制器。能卓越ESP8266EX 芯片内置超低功耗 Tensilica L106 32 位 RISC 处理器,CPU 时钟速度最⾼可达 160 MHz,⽀持实时操作系统 (RTOS) 和 Wi-Fi 协议栈,可将⾼达 80% 的处理能⼒应用于编程和开发。高度集成ESP8266 芯片高度集成天线开关、射频巴伦、功率放大器、低噪声接收放大器、滤波器等射频模块。模组尺寸小巧,尤其适用于空间受限的产品设计。认证齐全RF 认证:SRRC、FCC、CE-RED、KCC、TELEC/MIC、IC 和 NCC 认证;环保认证:RoHS、REACH;可靠性认证:HTOL、HTSL、μHAST、TCT、ESD。丰富的产品应用ESP8266 模组既可以通过 ESP-AT 指令固件,为外部主机 MCU 提供 Wi-Fi 连接功能;也可以作为独立 Wi-Fi MCU 运行,用户通过基于 RTOS 的 SDK 开发带 Wi-Fi 连接功能的产品。用户可以轻松实现开箱即用的云连接、低功耗运行模式,以及包括 WPA3 在内的 Wi-Fi 安全支持等功能。3.4 OLED显示屏OLED显示屏是利用有机电自发光二极管制成的显示屏。由于同时具备自发光有机电激发光二极管,不需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优异之特性,被认为是下一代的平面显示器新兴应用技术。有机发光二极管 (OLED)显示器越来越普遍,在手机、媒体播放器及小型入门级电视等产品中最为显著。不同于标准的液晶显示器,OLED 像素是由电流源所驱动。若要了解 OLED 电源供应如何及为何会影响显示器画质,必须先了解 OLED 显示器技术及电源供应需求。本文将说明最新的 OLED 显示器技术,并探讨主要的电源供应需求及解决方案,另外也介绍专为 OLED 电源供应需求而提出的创新性电源供应架构。背板技术造就软性显示器  高分辨率彩色主动式矩阵有机发光二极管 (AMOLED) 显示器需要采用主动式矩阵背板,此背板使用主动式开关进行各像素的开关。液晶 (LC) 显示器非晶硅制程已臻成熟,可供应低成本的主动式矩阵背板,并且可用于 OLED。许多公司正针对软性显示器开发有机薄膜晶体管 (OTFT) 背板制程,此一制程也可用于 OLED 显示器,以实现全彩软性显示器的推出。不论是标准或软性 OLED,都需要运用相同的电源供应及驱动技术。若要了解 OLED 技术、功能及其与电源供应之间的互动,必须深入剖析这项技术本身。OLED 显示器是一种自体发光显示器技术,完全不需要任何背光。OLED 采用的材质属于化学结构适用的有机材质。  OLED 技术需要电流控制驱动方法  OLED 具有与标准发光二极管 (LED) 相当类似的电气特性,亮度均取决于 LED 电流。若要开启和关闭 OLED 并控制 OLED 电流,需要使用薄膜晶体管 (TFT)的控制电路。OLED为自发光材料,不需用到背光板,同时视角广、画质均匀、反应速度快、较易彩色化、用简单驱动电路即可达到发光、制程简单、可制作成挠曲式面板,符合轻薄短小的原则,应用范围属于中小尺寸面板。显示方面:主动发光、视角范围大;响应速度快,图像稳定;亮度高、色彩丰富、分辨率高。工作条件:驱动电压低、能耗低,可与太阳能电池、集成电路等相匹配。适应性广:采用玻璃衬底可实现大面积平板显示;如用柔性材料做衬底,能制成可折叠的显示器。由于OLED是全固态、非真空器件,具有抗震荡、耐低温(-40℃)等特性,在军事方面也有十分重要的应用,如用作坦克、飞机等现代化武器的显示终端。3.5 LED大功率灯模块LED灯是一块电致发光的半导体材料芯片,用银胶或白胶固化到支架上,然后用银线或金线连接芯片和电路板,四周用环氧树脂密封,起到保护内部芯线的作用,最后安装外壳,所以 LED 灯的抗震性能好。LED(Light Emitting Diode),发光二极管,是一种能够将电能转化为可见光的固态的半导体器件,它可以直接把电转化为光。LED的心脏是一个半导体的晶片,晶片的一端附在一个支架上,一端是负极,另一端连接电源的正极,使整个晶片被环氧树脂封装起来。半导体晶片由两部分组成,一部分是P型半导体,在它里面空穴占主导地位,另一端是N型半导体,在这边主要是电子。但这两种半导体连接起来的时候,它们之间就形成一个P-N结。当电流通过导线作用于这个晶片的时候,电子就会被推向P区,在P区里电子跟空穴复合,然后就会以光子的形式发出能量,这就是LED灯发光的原理。而光的波长也就是光的颜色,是由形成P-N结的材料决定的。LED可以直接发出红、黄、蓝、绿、青、橙、紫、白色的光。3.6 STM32F103C8T6最小系统板STM32F系列属于中低端的32位ARM微控制器,该系列芯片是意法半导体(ST)公司出品,其内核是Cortex-M3。该系列芯片按片内Flash的大小可分为三大类:小容量(16K和32K)、中容量(64K和128K)、大容量(256K、384K和512K)。芯片集成定时器Timer,CAN,ADC,SPI,I2C,USB,UART等多种外设功能。内核--ARM 32位的Cortex-M3--最高72MHz工作频率,在存储器的0等待周期访问时可达1.25DMips/MHZ(DhrystONe2.1)--单周期乘法和硬件除法存储器--从16K到512K字节的闪存程序存储器(STM32F103XXXX中的第二个X表示FLASH容量,其中:“4”=16K,“6”=32K,“8”=64K,B=128K,C=256K,D=384K,E=512K)--最大64K字节的SRAM电源管理--2.0-3.6V供电和I/O引脚--上电/断电复位(POR/PDR)、可编程电压监测器(PVD)--4-16MHZ晶振--内嵌经出厂调校的8MHz的RC振荡器--内嵌带校准的40KHz的RC振荡器--产生CPU时钟的PLL--带校准的32KHz的RC振荡器低功耗--睡眠、停机和待机模式--Vbat为RTC和后备寄存器供电模数转换器--2个12位模数转换器,1us转换时间(多达16个输入通道)--转换范围:0至3.6V--双采样和保持功能--温度传感器DMA--2个DMA控制器,共12个DMA通道:DMA1有7个通道,DMA2有5个通道--支持的外设:定时器、ADC、SPI、USB、IIC和UART--多达112个快速I/O端口(仅Z系列有超过100个引脚)--26/37/51/80/112个I/O口,所有I/O口一块映像到16个外部中断;几乎所有的端口均可容忍5V信号调试模式--串行单线调试(SWD)和JTAG接口--多达8个定时器--3个16位定时器,每个定时器有多达4个用于输入捕获/输出比较/PWM或脉冲计数的通道和增量编码器输入--1个16位带死区控制和紧急刹车,用于电机控制的PWM高级控制定时器--2个看门狗定时器(独立的和窗口型的)--系统时间定时器:24位自减型计数器--多达9个通信接口:2个I2C接口(支持SMBus/PMBus)3个USART接口(支持ISO7816接口,LIN,IrDA接口和调制解调控制)2个SPI接口(18M位/秒)CAN接口(2.0B主动)USB 2.0全速接口计算单元CRC计算单元,96位的新批唯一代码封装ECOPACK封装3.7 杜邦线3.8 继电器四、STM32核心代码4.1  STM32:  main.c #include "stm32f10x.h" #include "beep.h" #include "delay.h" #include "led.h" #include "key.h" #include "sys.h" #include "usart.h" #include <string.h> #include <stdlib.h> #include "exti.h" #include "timer.h" #include "rtc.h" #include "wdg.h" #include "oled.h" #include "fontdata.h" #include "adc.h" #include "FunctionConfig.h" #include "dht11.h" #include "HumanDetection.h" #include "esp8266.h" 函数功能: 绘制时钟表盘框架 void DrawTimeFrame(void) u8 i; OLED_Circle(32,32,31);//画外圆 OLED_Circle(32,32,1); //画中心圆 //画刻度 for(i=0;i<60;i++) if(i%5==0)OLED_DrawAngleLine(32,32,6*i,31,3,1); OLED_RefreshGRAM(); //刷新数据到OLED屏幕 函数功能: 更新时间框架显示,在RTC中断里调用 char TimeBuff[20]; void Update_FrameShow(void) /*1. 绘制秒针、分针、时针*/ OLED_DrawAngleLine2(32,32,rtc_clock.sec*6-6-90,27,0);//清除之前的秒针 OLED_DrawAngleLine2(32,32,rtc_clock.sec*6-90,27,1); //画秒针 OLED_DrawAngleLine2(32,32,rtc_clock.min*6-6-90,24,0); OLED_DrawAngleLine2(32,32,rtc_clock.min*6-90,24,1); OLED_DrawAngleLine2(32,32,rtc_clock.hour*30-6-90,21,0); OLED_DrawAngleLine2(32,32,rtc_clock.hour*30-90,21,1); //绘制电子钟时间 sprintf(TimeBuff,"%d",rtc_clock.year); OLED_ShowString(65,16*0,16,TimeBuff); //年份字符串 OLED_ShowChineseFont(66+32,16*0,16,4); //显示年 sprintf(TimeBuff,"%d/%d",rtc_clock.mon,rtc_clock.day); OLED_ShowString(75,16*1,16,TimeBuff); //月 if(rtc_clock.sec==0)OLED_ShowString(65,16*2,16," "); //清除多余的数据 sprintf(TimeBuff,"%d:%d:%d",rtc_clock.hour,rtc_clock.min,rtc_clock.sec); OLED_ShowString(65,16*2,16,TimeBuff); //秒 //显示星期 OLED_ShowChineseFont(70,16*3,16,5); //星 OLED_ShowChineseFont(70+16,16*3,16,6); //期 OLED_ShowChineseFont(70+32,16*3,16,rtc_clock.week+7); //具体的值 函数功能: 温湿度显示 void ShowTemperatureAndHumidity(u8 temp,u8 humi) sprintf(TimeBuff,"T: %d",temp); OLED_ShowString(40,16*1,16,TimeBuff); sprintf(TimeBuff,"H: %d%%",humi); OLED_ShowString(40,16*2,16,TimeBuff); 函数功能: OLED所有显示页面信息初始化 u8 ESP8266_Stat=0; //存放ESP8266状态 1 OK ,0 error u8 ESP8266_WIFI_AP_SSID[10]; //存放WIFI的名称 #define STM32_96BIT_UID (0x1FFFF7E8) //STM32内部96位唯一芯片标识符寄存器地址 函数功能: ESP8266 WIFI 显示页面 char ESP8266_PwdShow[20]; char ESP8266_IP_PortAddr[30]; //存放ESP8266 IP地址与端口号地址 u8 Save_ESP8266_SendCmd[30]; //保存WIFI发送的命令 u8 Save_ESP8266_SendData[50]; //保存WIFI发送的数据 void ESP8266_ShowPageTable(void) if(ESP8266_Stat)OLED_ShowString(0,16*0,16,"WIFI STAT:ERROR"); else OLED_ShowString(0,16*0,16,"WIFI STAT:OK"); //显示字符串 //OLED_ShowString(0,2,(u8*)" "); //清除一行的显示 memset((u8*)ESP8266_PwdShow,0,20); //清空内存 sprintf((char*)ESP8266_PwdShow,"WIFI:%s",ESP8266_WIFI_AP_SSID); ESP8266_PwdShow[15]='\0'; OLED_ShowString(0,16*1,16,ESP8266_PwdShow); memset((u8*)ESP8266_PwdShow,0,20); //清空内存 sprintf((char*)ESP8266_PwdShow,"PWD:%s",wifiap_password); OLED_ShowString(0,16*2,16,ESP8266_PwdShow); OLED_ShowString(0,16*3,16,"192.168.4.1:8089"); void OledShowPageTableInit(void) u8 data[100],i; u32 stm32_uid=0; u8 *uid; /*1. ESP8266 WIFI相关信息初始化*/ OLED_ShowString(0,2,16," "); //清空一行的显示 OLED_ShowString(0,2,16,"WifiInit..."); //显示页面提示信息 /*1.1 设置WIFI AP模式 */ if(ESP8266_SendCmd("AT+CWMODE=2\r\n","OK",50)) ESP8266_Stat=1; //OK ESP8266_Stat=0; //ERROR /*1.2 重启模块 */ ESP8266_SendCmd("AT+RST\r\n","OK",20); /*1.3 延时3S等待重启成功*/ DelayMs(1000); DelayMs(1000); DelayMs(1000); //得到WIFI的名称 uid=(u8*)STM32_96BIT_UID; //转为指针为1个字节 for(i=0;i<96;i++) stm32_uid+=*uid++; //计算校验和,得到WIFI数字编号 printf("stm32_uid=%d\r\n",stm32_uid); sprintf((char*)data,"%d",stm32_uid); strcpy((char*)ESP8266_WIFI_AP_SSID,"wbyq_"); strcat((char*)ESP8266_WIFI_AP_SSID,(char*)data); printf("请用设备连接WIFI热点:%s,%s,%s\r\n",(u8*)ESP8266_WIFI_AP_SSID,(u8*)wifiap_encryption,(u8*)wifiap_password); /*1.4 配置模块AP模式无线参数*/ memset(data,0,100); //清空数组 sprintf((char*)data,"AT+CWSAP=\"%s\",\"%s\",1,4\r\n",ESP8266_WIFI_AP_SSID,wifiap_password); ESP8266_SendCmd(data,"OK",1000); /*1.5 设置多连接模式:0单连接,1多连接(服务器模式必须开启)*/ ESP8266_SendCmd("AT+CIPMUX=1\r\n","OK",20); /*1.6 开启Server模式(0,关闭;1,打开),端口号为portnum */ memset(data,0,100); //清空数组 sprintf((char*)data,"AT+CIPSERVER=1,%s\r\n",(u8*)portnum); ESP8266_SendCmd(data,"OK",50); /*1.7 获取当前模块的IP*/ ESP8266_GetWanip((u8*)ESP8266_IP_PortAddr); strcat(ESP8266_IP_PortAddr,":"); strcat(ESP8266_IP_PortAddr,portnum); printf("IP地址:%s\r\n",ESP8266_IP_PortAddr); OLED_ShowString(0,2,16," "); //清空一行的显示 OLED_ShowString(0,2,16,"WifiInitOk"); //显示页面提示信息 函数功能: 显示开关状态 void Show_Switch(int state) OLED_Clear(0); if(state) sprintf(TimeBuff,"Socket:ON"); OLED_ShowString(20,16*2,16,TimeBuff); sprintf(TimeBuff,"Socket:OFF"); OLED_ShowString(20,16*2,16,TimeBuff); //int main1(void) // HumanDetection_Init(); //热释电模块初始化 // UsartInit(USART1,72,115200);//串口1的初始化 // LED_Init(); //初始化LED // while(1) // { // //热释电状态 为真表示有人 // if(HumanState) // { // LED1=0; //开灯 // } // else //为假 // { // LED1=1; //关灯 // } // printf("%d\n",HumanState); // } int main(void) u8 rlen; char *p; u8 temp; //温度 u8 humi; //湿度 u8 stat; u8 key_val; u32 TimeCnt=0; u32 wifi_TimeCnt=0; u16 temp_data; //温度数据 u8 page_cnt=0; //显示的页面 char *time; u8 socket_state=0; UsartInit(USART1,72,115200);//串口1的初始化 BEEP_Init(); //初始化蜂鸣器 LED_Init(); //初始化LED KEY_Init(); //按键初始化 printf("正在初始化OLED...\r\n"); OLED_Init(0xc8,0xa1); //OLED显示屏初始化--正常显示 //OLED_Init(0xc0,0xa0); //OLED显示屏初始化--翻转显示 OLED_Clear(0x00); //清屏 UsartInit(USART3,36,115200); //WIFI的波特率为115200 Timer2Init(72,10000); //10ms中断一次,辅助串口3接收数据--WIFI数据 printf("正在初始化ESP8266..\r\n"); //ESP8266初始化 OledShowPageTableInit(); OLED_Clear(0); printf("正在初始化RTC...\r\n"); RTC_Init(); //RTC初始化 DrawTimeFrame(); //画时钟框架 USART3_RX_STA=0; //清空串口的接收标志位 USART3_RX_CNT=0; //清空计数器 printf("初始化DHT11...\r\n"); DHT11_Init(); //初始化DHT11 printf("热释电模块初始化...\r\n"); HumanDetection_Init(); //热释电模块初始化 //OLED_Clear(0); printf("开始进入while(1)\r\n"); while(1) key_val=KEY_GetValue(); if(key_val) page_cnt++; printf("page_cnt:%d\r\n",page_cnt); OLED_Clear(0); //时钟页面 if(page_cnt==0) DrawTimeFrame(); //画时钟框架 RTC->CRH|=1<<0; //开启秒中断 //温湿度页面 else if(page_cnt==1) RTC->CRH&=~(1<<0); //关闭秒中断 //温湿度 ShowTemperatureAndHumidity(temp,humi); //ESP8266显示 else if(page_cnt==2) ESP8266_ShowPageTable(); else if(page_cnt==3) Show_Switch(socket_state); DrawTimeFrame(); //画时钟框架 RTC->CRH|=1<<0; //开启秒中断 page_cnt=0; //时间记录 DelayMs(10); TimeCnt++; wifi_TimeCnt++; if(TimeCnt>=100) //1000毫秒一次 TimeCnt=0; //读取温湿度数据 DHT11_Read_Data(&temp,&humi); //湿度大于90就关闭开关 if(humi>90) socket_state=0; if(page_cnt==3) Show_Switch(socket_state); //温湿度页面 if(page_cnt==1) //温湿度 ShowTemperatureAndHumidity(temp,humi); if(wifi_TimeCnt>=300) //3000毫秒一次 wifi_TimeCnt=0; //温湿度1秒上传一次 sprintf((char*)Save_ESP8266_SendData,"#%d,%d",temp,humi); //拼接数据 sprintf((char*)Save_ESP8266_SendCmd,"AT+CIPSEND=0,%d\r\n",strlen((char*)Save_ESP8266_SendData)); ESP8266_SendCmd(Save_ESP8266_SendCmd,(u8*)"OK",200); //WIFI设置发送数据长度 ESP8266_SendData(Save_ESP8266_SendData,(u8*)"OK",100); //WIFI发送数据 //热释电状态 为真表示有人 if(HumanState) LED1=0; //开灯 else //为假 LED1=1; //关灯 /*轮询扫描数据*/ if(USART3_RX_STA) //WIFI 接收到一次数据了 rlen=USART3_RX_CNT; //得到本次接收到的数据长度 USART3_RX_BUF[rlen]='\0'; //添加结束符 // printf("接收的数据: %s\r\n",USART3_RX_BUF); //发送到串口 /*判断是否收到客户端发来的数据 */ p=strstr((char*)USART3_RX_BUF,"+IPD"); if(p!=NULL) //正常数据格式: +IPD,0,7:LED1_ON +IPD,0表示第0个客户端 7:LED1_ON表示数据长度与数据 /*解析上位机发来的数据*/ p=strstr((char*)USART3_RX_BUF,":"); if(p!=NULL) p+=1; //向后偏移1个字节 if(*p=='*') //设置RTC时间 p+=1; //向后偏移,指向正确的时间 time=p; rtc_clock.year=(time[0]-48)*1000+(time[1]-48)*100+(time[2]-48)*10+(time[3]-48)*1; rtc_clock.mon=(time[4]-48)*10+(time[5]-48)*1; rtc_clock.day=(time[6]-48)*10+(time[7]-48)*1; rtc_clock.hour=(time[8]-48)*10+(time[9]-48)*1; rtc_clock.min=(time[10]-48)*10+(time[11]-48)*1; rtc_clock.sec=(time[12]-48)*10+(time[13]-48)*1; RTC_SetTime(rtc_clock.year,rtc_clock.mon,rtc_clock.day,rtc_clock.hour,rtc_clock.min,rtc_clock.sec); if(page_cnt==0) OLED_Clear(0); //OLED清屏 DrawTimeFrame();//画时钟框架 else if(strcmp(p,"LED1_ON")==0) socket_state=1; else if(strcmp(p,"LED1_OFF")==0) socket_state=0; if(page_cnt==3) Show_Switch(socket_state); USART3_RX_STA=0; USART3_RX_CNT=0; 4.2  STM32:  rtc.c#include "rtc.h" //定义RTC标准结构体 struct RTC_CLOCK rtc_clock; 函数功能: RTC初始化函数 void RTC_Init(void) //检查是不是第一次配置时钟 u8 temp=0; if(BKP->DR1!=0X5051)//之前使用的不是LSE RCC->APB1ENR|=1<<28; //使能电源时钟 RCC->APB1ENR|=1<<27; //使能备份时钟 PWR->CR|=1<<8; //取消备份区写保护 RCC->BDCR|=1<<16; //备份区域软复位 RCC->BDCR&=~(1<<16); //备份区域软复位结束 RCC->BDCR|=1<<0; //开启外部低速振荡器 while((!(RCC->BDCR&0X02))&&temp<250)//等待外部时钟就绪 temp++; DelayMs(10); if(temp>=250) //return 1;//初始化时钟失败,晶振有问题 RCC->CSR|=1<<0; //开启外部低速振荡器 while(!(RCC->CSR&(1<<1)));//外部低速振荡器 RCC->BDCR|=2<<8; //LSI作为RTC时钟 BKP->DR1=0X5050; //标记使用LSI作为RTC时钟 RCC->BDCR|=1<<8; //LSE作为RTC时钟 BKP->DR1=0X5051; //标记使用LSE作为RTC时钟 RCC->BDCR|=1<<15;//RTC时钟使能 while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成 while(!(RTC->CRL&(1<<3)));//等待RTC寄存器同步 RTC->CRH|=0X01; //允许秒中断 while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成 RTC->CRL|=1<<4; //允许配置 RTC->PRLH=0X0000; RTC->PRLL=32767; //时钟周期设置(有待观察,看是否跑慢了?)理论值:32767 RTC_SetTime(2021,4,25,20,36,20); //设置时间 RTC->CRL&=~(1<<4); //配置更新 while(!(RTC->CRL&(1<<5))); //等待RTC寄存器操作完成 printf("FIRST TIME\n"); }else//系统继续计时 while(!(RTC->CRL&(1<<3)));//等待RTC寄存器同步 RTC->CRH|=0X01; //允许秒中断 while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成 printf("OK\n"); STM32_NVIC_SetPriority(RTC_IRQn,2,2); //优先级 extern void Update_FrameShow(void); 函数功能: RTC闹钟中断服务函数 void RTC_IRQHandler(void) u32 SecCnt; if(RTC->CRL&1<<0) SecCnt=RTC->CNTH<<16;//获取高位 SecCnt|=RTC->CNTL; //获取低位 RTC_GetTime(SecCnt); //转换标准时间 RTC_GetWeek(SecCnt); // printf("%d-%d-%d %d:%d:%d week:%d\n",rtc_clock.year,rtc_clock.mon,rtc_clock.day,rtc_clock.hour,rtc_clock.min,rtc_clock.sec,rtc_clock.week); Update_FrameShow(); //更新显示 RTC->CRL&=~(1<<0); //清除秒中断标志位 if(RTC->CRL&1<<1) // printf("闹钟时间到达!....\n"); // BEEP=1; // DelayMs(500); // BEEP=0; RTC->CRL&=~(1<<1); //清除闹钟中断标志位 //闰年的月份 static int mon_r[12]={31,29,31,30,31,30,31,31,30,31,30,31}; //平年的月份 static int mon_p[12]={31,28,31,30,31,30,31,31,30,31,30,31}; 函数功能: 设置RTC时间 函数形参: u32 year; 2018 u32 mon; 8 u32 day; u32 hour; u32 min; u32 sec; void RTC_SetTime(u32 year,u32 mon,u32 day,u32 hour,u32 min,u32 sec) u32 i; u32 SecCnt=0; //总秒数 /*1. 累加已经过去的年份*/ for(i=2017;i<year;i++) //基准年份:20170101000000 if(RTC_GetYearState(i)) SecCnt+=366*24*60*60; //闰年一年的秒数 SecCnt+=365*24*60*60; //平年一年的秒数 /*2. 累加过去的月份*/ for(i=0;i<mon-1;i++) if(RTC_GetYearState(year)) SecCnt+=mon_r[i]*24*60*60; //闰年一月的秒数 SecCnt+=mon_p[i]*24*60*60; //平年一月的秒数 /*3. 累加过去的天数*/ SecCnt+=(day-1)*24*60*60; /*4. 累加过去小时*/ SecCnt+=hour*60*60; /*5. 累加过去的分钟*/ SecCnt+=min*60; /*6. 累加过去的秒*/ SecCnt+=sec; /*7. 设置RTC时间*/ //设置时钟 RCC->APB1ENR|=1<<28;//使能电源时钟 RCC->APB1ENR|=1<<27;//使能备份时钟 PWR->CR|=1<<8; //取消备份区写保护 //上面三步是必须的! RTC->CRL|=1<<4; //允许配置 RTC->CNTL=SecCnt&0xffff; RTC->CNTH=SecCnt>>16; RTC->CRL&=~(1<<4);//配置更新 while(!(RTC->CRL&(1<<5)));//等待RTC寄存器操作完成 函数功能: 获取RTC时间 函数参数: u32 sec 秒单位时间 void RTC_GetTime(u32 sec) u32 i; rtc_clock.year=2017; //基准年份 /*1. 计算当前的年份*/ while(1) if(RTC_GetYearState(rtc_clock.year)) if(sec>=366*24*60*60) //够一年 sec-=366*24*60*60; rtc_clock.year++; else break; if(sec>=365*24*60*60) //够一年 sec-=365*24*60*60; rtc_clock.year++; else break; /*2. 计算当前的月份*/ rtc_clock.mon=1; for(i=0;i<12;i++) if(RTC_GetYearState(rtc_clock.year)) if(sec>=mon_r[i]*24*60*60) sec-=mon_r[i]*24*60*60; rtc_clock.mon++; else break; if(sec>=mon_p[i]*24*60*60) sec-=mon_p[i]*24*60*60; rtc_clock.mon++; else break; /*3. 计算当前的天数*/ rtc_clock.day=1; while(1) if(sec>=24*60*60) sec-=24*60*60; rtc_clock.day++; else break; /*4. 计算当前的小时*/ rtc_clock.hour=0; while(1) if(sec>=60*60) sec-=60*60; rtc_clock.hour++; else break; /*5. 计算当前的分钟*/ rtc_clock.min=0; while(1) if(sec>=60) sec-=60; rtc_clock.min++; else break; /*6. 计算当前的秒*/ rtc_clock.sec=sec; 函数功能: 判断年份是否是平年、闰年 返回值 : 0表示平年 1表示闰年 u8 RTC_GetYearState(u32 year) if((year%4==0&&year%100!=0)||year%400==0) return 1; return 0; 函数功能: 获取星期 void RTC_GetWeek(u32 sec) u32 day1=sec/(60*60*24); //将秒单位时间转为天数 switch(day1%7) case 0: rtc_clock.week=0; break; case 1: rtc_clock.week=1; break; case 2: rtc_clock.week=2; break; case 3: rtc_clock.week=3; break; case 4: rtc_clock.week=4; break; case 5: rtc_clock.week=5; break; case 6: rtc_clock.week=6; break; 4.3 ESP8266.c#include "esp8266.h" 函数功能:向ESP82668266发送命令 函数参数: cmd:发送的命令字符串 ack:期待的应答结果,如果为空,则表示不需要等待应答 waittime:等待时间(单位:10ms) 返 回 值: 0,发送成功(得到了期待的应答结果) 1,发送失败 u8 ESP8266_SendCmd(u8 *cmd,u8 *ack,u16 waittime) u8 res=0; USART3_RX_STA=0; USART3_RX_CNT=0; UsartStringSend(USART3,cmd);//发送命令 if(ack&&waittime) //需要等待应答 while(--waittime) //等待倒计时 DelayMs(10); if(USART3_RX_STA)//接收到期待的应答结果 if(ESP8266_CheckCmd(ack)) res=0; //printf("cmd->ack:%s,%s\r\n",cmd,(u8*)ack); break;//得到有效数据 USART3_RX_STA=0; USART3_RX_CNT=0; if(waittime==0)res=1; return res; 函数功能:ESP8266发送命令后,检测接收到的应答 函数参数:str:期待的应答结果 返 回 值:0,没有得到期待的应答结果 其他,期待应答结果的位置(str的位置) u8* ESP8266_CheckCmd(u8 *str) char *strx=0; if(USART3_RX_STA) //接收到一次数据了 USART3_RX_BUF[USART3_RX_CNT]=0;//添加结束符 strx=strstr((const char*)USART3_RX_BUF,(const char*)str); //查找是否应答成功 //printf("RX=%s",USART3_RX_BUF); return (u8*)strx; 函数功能:向ESP8266发送指定数据 函数参数: data:发送的数据(不需要添加回车) ack:期待的应答结果,如果为空,则表示不需要等待应答 waittime:等待时间(单位:10ms) 返 回 值:0,发送成功(得到了期待的应答结果)luojian u8 ESP8266_SendData(u8 *data,u8 *ack,u16 waittime) u8 res=0; USART3_RX_STA=0; UsartStringSend(USART3,data);//发送数据 if(ack&&waittime) //需要等待应答 while(--waittime) //等待倒计时 DelayMs(10); if(USART3_RX_STA)//接收到期待的应答结果 if(ESP8266_CheckCmd(ack))break;//得到有效数据 USART3_RX_STA=0; USART3_RX_CNT=0; if(waittime==0)res=1; return res; 函数功能:ESP8266退出透传模式 返 回 值:0,退出成功; 1,退出失败 u8 ESP8266_QuitTrans(void) while((USART3->SR&0X40)==0); //等待发送空 USART3->DR='+'; DelayMs(15); //大于串口组帧时间(10ms) while((USART3->SR&0X40)==0); //等待发送空 USART3->DR='+'; DelayMs(15); //大于串口组帧时间(10ms) while((USART3->SR&0X40)==0); //等待发送空 USART3->DR='+'; DelayMs(500); //等待500ms return ESP8266_SendCmd("AT\r\n","OK",20);//退出透传判断. 函数功能:获取ESP8266模块的连接状态 返 回 值:0,未连接;1,连接成功. u8 ESP8266_ConstaCheck(void) u8 *p; u8 res; if(ESP8266_QuitTrans())return 0; //退出透传 ESP8266_SendCmd("AT+CIPSTATUS\r\n",":",50); //发送AT+CIPSTATUS指令,查询连接状态 p=ESP8266_CheckCmd("+CIPSTATUS\r\n:"); res=*p; //得到连接状态 return res; 函数功能:获取ip地址 函数参数:ipbuf:ip地址输出缓存区 void ESP8266_GetWanip(u8* ipbuf) u8 *p,*p1; if(ESP8266_SendCmd("AT+CIFSR\r\n","OK",50))//获取WAN IP地址失败 ipbuf[0]=0; return; p=ESP8266_CheckCmd("\""); p1=(u8*)strstr((const char*)(p+1),"\""); *p1=0; sprintf((char*)ipbuf,"%s",p+1); 五、QT设计的上位机代码:  Android手机APP+Windows系统上位机

基于STM32完成FATFS文件系统移植与运用--这是完全免费开源的FAT文件系统

一、环境介绍主控MCU: STM32F103ZET6 STM32程序开发IDE: keil5STM32程序风格:  采用寄存器方式开发,注释齐全,执行效率高,方便移植硬件包含:  一块STM32F103ZET6系统板、一个SPI接口的SD卡卡槽模块、一张SD卡工程完整源码下载地址:  https://download.csdn.net/download/xiaolong1126626497/19687693这篇文章主要演示FATFS文件系统如何移植到自己的工程,并完成文件的读写。因为SD卡采用的是SPI模拟时序,所以,其他单片机一样可以照着移植,代码都可以复制粘贴的。 二、FATFS文件系统介绍2.1 FATFS简介FatFs 是一种完全免费开源的 FAT 文件系统模块,专门为小型的嵌入式系统而设计。它完全用标准C 语言编写,所以具有良好的硬件平台独立性,可以移植到 8051、 PIC、 AVR、 SH、 Z80、 H8、 ARM 等系列单片机上而只需做简单的修改。它支持 FATl2、 FATl6 和 FAT32,支持多个存储媒介;有独立的缓冲区,可以对多个文件进行读/写,并特别对 8 位单片机和 16 位单片机做了优化。2.2 特点 Windows兼容的FAT文件系统不依赖于平台,易于移植代码和工作区占用空间非常小 多种配置选项多卷(物理驱动器和分区) 多ANSI/OEM代码页,包括DBCS在ANSI/OEM或Unicode中长文件名的支持RTOS的支持多扇区大小的支持只读,最少API,I/O缓冲区等等2.3 移植性fatfs模块是ANSI C(C89)编写的。 没有平台的依赖, 编译器只要符合ANSI C标准就可以编译。fatf模块假设大小的字符/短/长8/16/32位和int是16或32位。 这些数据类型在integer.h文件中定义。这些数据类型在大多数的编译器中定义都符合要求。 如果现有的定义与编译器有任何冲突发生时,需要自己解决。2.4 源码下载下载地址:http://elm-chan.org/fsw/ff/00index_e.htmlFATFS有两个版本,一个大版本,一个小版本。小版本主要用于8位机(内存小)使用。下载图:2.5 FATFS源码文件介绍将下载的源码解压后可以得到两个文件夹: doc 和 src。 doc 里面主要是对 FATFS 的介绍(离线文档—英文和日文),而 src 里面才是我们需要的源码。其中,与平台无关的是:与平台相关的代码:diskio.c     底层接口文件(需要用户提供)FATFS 模块在移植的时候,我们一般只需要修改 2 个文件,即 ffconf.h 和 diskio.c。FATFS模块的所有配置项都是存放在 ffconf.h 里面,我们可以通过配置里面的一些选项,来满足自己的需求。FATFS最顶层是应用层,使用者无需理会 FATFS 的内部结构和复杂的 FAT 协议,只需要调用FATFS 模块提供给用户的一系列应用接口函数,如 f_open, f_read, f_write 和 f_close 等,就可以像在 PC 上读/写文件那样简单。中间层 FATFS 模块, 实现了 FAT 文件读/写协议。 FATFS 模块提供的是 ff.c 和 ff.h。除非有必要,使用者一般不用修改,使用时将头文件直接包含进去即可。需要我们编写移植代码的是 FATFS 模块提供的底层接口,它包括存储媒介读/写接口 ( disk、I/O) 和供给文件创建修改时间的实时时钟。三、 移植FATFS文件系统移植之前,首先得准备一个能正常编译的工程,并且工程里有SD卡的驱动代码,提供了读写扇区这些函数才能进行FATFS文件系统的正常移植。关于如何编写SD卡驱动,SD卡的时序介绍、命令介绍等知识点下篇文章再讲解。这篇文章重点是FATFS文件系统的移植过程。3.1  新建工程FATFS文件系统源码下载下来,解压之后,移植修改的步骤如下:打开KEIL工程,添加FATFS文件源码:加入.h文件主要是方便配。cc936.c 用于支持中文。 3.2  修改diskio.c文件注释掉现在不需要的用到的文件,因为我们现在用的是SD卡,与USB,ATA,MMC卡没关系。并加入一个新的宏 :#define  SD  0定义SD卡的物理驱动器号为0。 修改 disk_status函数,该函数主要是用来获取磁盘状态。现在未用到,可以直接函数体内代码删除。修改截图:代码示例:修改disk_initialize函数,添加SD卡的初始化,其他不用到的代码直接删掉,该函数成功返回0,失败返回1。修改截图:代码示例:修改disk_read函数,加入SD卡读任意扇区的函数(需要用户自己提供),其他不用到的选项可以删掉。修改代码如下:/*-----------------------------------------------------------------------*/ /* 读扇区 */ /*-----------------------------------------------------------------------*/ DRESULT disk_read ( BYTE pdrv, /* 物理驱动编号 - 范围0-9*/ BYTE *buff, /* 数据缓冲区存储读取数据 */ DWORD sector, /* 扇区地址*/ UINT count /* 需要读取的扇区数*/ DRESULT res; int result; switch (pdrv) { case SD: res=SD_Read_Data((u8*)buff,sector,count); //读SD扇区函数--用户提供 return res; //在此处可以判错误 return RES_PARERR; //无效参数 修改disk_write 函数,添加写扇区函数:代码如下:/*-----------------------------------------------------------------------*/ /* 其他函数 */ /*-----------------------------------------------------------------------*/ #if _USE_IOCTL DRESULT disk_ioctl ( BYTE pdrv, /* 物理驱动号 */ BYTE cmd, /* 控制码 */ void *buff /* 发送/接收数据缓冲区地址 */ DRESULT res; int result; switch (pdrv) { case SD: switch(cmd) case CTRL_SYNC: //等待写过程 SD_CS(0); //选中SD卡 if(SD_Wait_Ready())result = RES_ERROR;/*等待卡准备好*/ else res = RES_OK; //成功 SD_CS(1); //释放SD卡 break; case GET_SECTOR_SIZE://获取扇区大小 *(DWORD*)buff = 512; res = RES_OK; //成功 break; case GET_BLOCK_SIZE: //获取块大小 *(WORD*)buff = 8; //块大小(扇区为单位),一块等于8个扇区 res = RES_OK; break; case GET_SECTOR_COUNT: //获取总扇区数量 *(DWORD*)buff = SD_Get_Sector_Count(); res = RES_OK; break; default: //命令错误 res = RES_PARERR; break; return res; return RES_PARERR; //返回状态 diskio.c 文件修改完整代码:/*-----------------------------------------------------------------------*/ /* 低级别磁盘I / O模块框架fatf(C)ChaN)2014 *存储控制模块fatf模块定义了一个API。 */ /*-----------------------------------------------------------------------*/ #include "diskio.h" /* fatf底层API */ #include "sd.h" /* SD卡驱动头文件 */ /* 定义每个驱动器的物理驱动器号*/ #define SD 0 /*-----------------------------------------------------------------------*/ /* 获取设备(磁盘)状态 */ /*-----------------------------------------------------------------------*/ DSTATUS disk_status ( BYTE pdrv /* 物理驱动识别 */ return 0; //该函数现在无需用到,直接返回0 /*-----------------------------------------------------------------------*/ /* 初始化磁盘驱动 */ /*-----------------------------------------------------------------------*/ DSTATUS disk_initialize ( BYTE pdrv /* 物理驱动识别 */ DSTATUS stat; int result; switch (pdrv) { case SD : //选择SD卡 stat=SD_Init(); //初始化SD卡-用户自己提供 if(stat)return STA_NOINIT; //磁盘未初始化 return 0; //初始化成功 /*-----------------------------------------------------------------------*/ /* 读扇区 */ /*-----------------------------------------------------------------------*/ DRESULT disk_read ( BYTE pdrv, /* 物理驱动编号 - 范围0-9*/ BYTE *buff, /* 数据缓冲区存储读取数据 */ DWORD sector, /* 扇区地址*/ UINT count /* 需要读取的扇区数*/ DRESULT res; int result; switch (pdrv) { case SD: res=SD_Read_Data((u8*)buff,sector,count); //读SD扇区函数--用户提供 return res; //在此处可以判错误 return RES_PARERR; //无效参数 /*-----------------------------------------------------------------------*/ /* 写扇区 */ /*-----------------------------------------------------------------------*/ #if _USE_WRITE DRESULT disk_write ( BYTE pdrv, /* 物理驱动号*/ const BYTE *buff, /* 要写入数据的首地址 */ DWORD sector, /* 扇区地址 */ UINT count /* 扇区数量*/ DRESULT res; int result; switch (pdrv) { case SD: res=SD_Write_Data((u8*)buff,sector,count); //写入扇区 return res; return RES_PARERR; //无效参数 #endif /*-----------------------------------------------------------------------*/ /* 其他函数 */ /*-----------------------------------------------------------------------*/ #if _USE_IOCTL DRESULT disk_ioctl ( BYTE pdrv, /* 物理驱动号 */ BYTE cmd, /* 控制码 */ void *buff /* 发送/接收数据缓冲区地址 */ DRESULT res; int result; switch (pdrv) { case SD: switch(cmd) case CTRL_SYNC: //等待写过程 SD_CS(0); //选中SD卡 if(SD_Wait_Ready())result = RES_ERROR;/*等待卡准备好*/ else res = RES_OK; //成功 SD_CS(1); //释放SD卡 break; case GET_SECTOR_SIZE://获取扇区大小 *(DWORD*)buff = 512; res = RES_OK; //成功 break; case GET_BLOCK_SIZE: //获取块大小 *(WORD*)buff = 8; //块大小--一块等于8个扇区 res = RES_OK; break; case GET_SECTOR_COUNT: //获取总扇区数量 *(DWORD*)buff = SD_Get_Sector_Count(); res = RES_OK; break; default: //命令错误 res = RES_PARERR; break; return res; return RES_PARERR; //返回状态 #endif Return Value Currnet local time is returned with packed into a DWORD value. The bit field is as follows: bit31:25 Year origin from the 1980 (0..127) bit24:21 Month (1..12) bit20:16 Day of the month(1..31) bit15:11 Hour (0..23) bit10:5 Minute (0..59) bit4:0 Second / 2 (0..29) 3.3 修改ffconf.h文件需要注意的一些宏配置:#define _CODE_PAGE  936   //采用中文GBK编码       (64行)#define    _USE_LFN     3     //动态的堆上工作             (93行)#define    _MAX_LFN   255   /*_USE_LFN选项开关LFN(长文件名)特性。#define _VOLUMES      1     /* 支持的磁盘数量(逻辑驱动器)。 */   (142行)#define    _MIN_SS                512                                  (165行)#define    _MAX_SS              512   /*这些选项配置支持扇区大小的范围。(512,1024, 4096*/ #define _FS_NORTC         0    /*启用RTC时间功能*/   (202行)#define _NORTC_MON     1#define _NORTC_MDAY     1#define _NORTC_YEAR       2015 //年 /*需要实现:get_fattime()函数*/ffconf.h 文件源码(讲解):/*---------------------------------------------------------------------------/ / FatFs - FAT文件系统模块配置文件 R0.11a (C)ChaN, 2015 /---------------------------------------------------------------------------*/ #define _FFCONF 64180 /* 版本识别*/ /*---------------------------------------------------------------------------/ / 功能配置 /---------------------------------------------------------------------------*/ #define _FS_READONLY 0 /* 这个选项开关只读配置。(0:读/写或1:只读)    /只读配置删除编写API函数,f_write(),f_sync(),    / f_unlink(),f_mkdir(),f_chmod(),f_rename(),f_truncate(),f_getfree()    /写和可选的功能. */ / 1:f_stat(),f_getfree(),f_unlink(),f_mkdir(),f_chmod(),f_utime(),    / f_truncate()和f_rename()函数删除。   / 2:f_opendir(),f_readdir()和f_closedir()中除了1。   / 3:f_lseek()函数删除除了2。*/ /*---------------------------------------------------------------------------/ / 语言环境和名称空间配置 /---------------------------------------------------------------------------*/ #define _CODE_PAGE 936 //采用中文GBK编码 /* 这个选项指定OEM代码页在目标系统上使用。   /不正确的代码页的设置会导致文件打开失败. / 1 - ASCII (没有扩展字符。Non-LFN cfg。只有) / 437 - U.S. / 720 - 阿拉伯语 / 737 - 希腊语; / 771 - 阿富汗 / 775 - 波罗的海 / 850 - 拉丁1 / 852 - 拉丁2 / 855 - 西里尔字母 / 857 - 土耳其语 / 860 - 葡萄牙语 / 861 - 冰岛语 / 862 - 希伯来人 / 863 - 加拿大法语 / 864 - 阿拉伯语 / 865 - 日耳曼民族的 / 866 - 俄语 / 869 - 希腊 2 / 932 - 日本人 (DBCS) / 936 - 简体中文(DBCS) / 949 - 韩国人 (DBCS) / 950 - 繁体中文(DBCS) / 当启用LFN(长文件名)特性,Unicode(选项/ unicode.c)必须处理功能   /被添加到项目中。LFN工作缓冲区占用(_MAX_LFN + 1)* 2字节。   /当使用堆栈缓冲区,照顾堆栈溢出。当使用堆   /工作缓冲区内存,内存管理功能,ff_memalloc()和   / ff_memfree(),必须添加到项目中。 */ /* 当_LFN(长文件名)_UNICODE是1,这个选项选择文件的字符编码   /通过字符串读取/写入I /O功能,f_gets(),f_putc(),f_puts和f_printf(). / 0: ANSI/OEM / 1: UTF-16LE / 2: UTF-16BE / 3: UTF-8 / 当_LFN_UNICODE = 0时,该选项没有影响。*/ #define _FS_RPATH 0 /* 这个选项配置相对路径的功能。  /    / 0:禁用相对路径特性和删除相关功能。   / 1:启用相对路径特性。f_chdir()和f_chdrive()是可用的。   / 2:f_getcwd()函数可用除了1。  /    /注意,目录项读通过f_readdir()这个选项。 /*---------------------------------------------------------------------------/ / 驱动/卷配置 /---------------------------------------------------------------------------*/ #define _STR_VOLUME_ID 0 #define _VOLUME_STRS "RAM","NAND","CF","SD1","SD2","USB1","USB2","USB3" /* STR_VOLUME_ID选项开关卷ID字符串功能。   /当_STR_VOLUME_ID设置为1时,也可以使用预先定义的字符串在路径名称/数量。 为每个_VOLUME_STRS定义驱动ID字符串   /逻辑驱动器。条目的数量必须等于_VOLUMES。有效字符   /驱动ID字符串:a - z和0 - 9。*/ / 1:启用re-entrancy。还提供用户同步处理程序,    / ff_req_grant(),ff_rel_grant(),ff_del_syncobj()和ff_cre_syncobj()    /函数,必须添加到项目中。样品中可用   / syscall.c。 / _FS_TIMEOUT定义超时时间单位的滴答声。   / _SYNC_t定义了O / S依赖同步对象类型。例如处理、ID、OS_EVENT *    / SemaphoreHandle_t等. .O / S的头文件定义需要   /包括在ff.c的范围。 */ / ARM7TDMI 0 *2 ColdFire 0 *1 V850E 0 *2 / Cortex-M3 0 *3 Z80 0/1 V850ES 0/1 / Cortex-M0 0 *2 x86 0/1 TLCS-870 0/1 / AVR 0/1 RX600(LE) 0/1 TLCS-900 0/1 / AVR32 0 *1 RL78 0 *2 R32C 0 *2 / PIC18 0/1 SH-2 0 *1 M16C 0/1 / PIC24 0 *2 H8S 0 *1 MSP430 0 *2 / PIC32 0 *1 H8/300H 0 *1 8051 0/1 * 1:高位优先。  / * 2:不支持不连续的内存访问。  / * 3:一些编译器生成LDM(逻辑磁盘管理器 ) / STM mem_cpy(内存拷贝)函数。 3.4 实现动态内存分配函数与时间函数ff.h文件有动态内存的释放,动态内存申请,时间获取函数接口。在diskio.c文件实现函数功能:代码实现如下://动态内存分配 void* ff_memalloc (UINT msize) /* 分配内存块 */ return (void*)malloc(msize); //分配空间 Return Value Currnet local time is returned with packed into a DWORD value. The bit field is as follows: bit31:25 Year origin from the 1980 (0..127) bit24:21 Month (1..12) bit20:16 Day of the month(1..31) bit15:11 Hour (0..23) bit10:5 Minute (0..59) bit4:0 Second / 2 (0..29) 3.5 修改堆栈空间完成了上述的修改,还需要修改堆栈空间,因为长文件支持需要占用堆空间。修改STM32启动文件如下:3.6 编译工程测试修改完毕之后,给开发板插上SD卡,调用API函数在SD卡创建一个文件,并写入数据,测试是否成功:#include "ff.h" FATFS fs; // 用户定义的文件系统结构体 FIL file; // 用户定义的文件系统结构体 u8 buff[]="123 知识!!"; int main(void) u32 data; //检测SD卡容量 u8 i,res; LED_Init(); //LED灯初始化 Delay_Init(); KEY_Init(); USART1_Init(72,115200); USART2_Init(36,115200); FLASH_Init(); Set_Font_addr(); //字库地址初始化 FSMC_SRAM_Init(); LCD_Init(); RTC_Init(); //RTC时钟初始化 while(SD_Init()) //检测不到SD卡,SD相关硬件初始化 i=!i; LCD_ShowString(60,150,200,16,16,"SD Card Error! Please Check SD Card!!",0xf800); Delay_ms(500); LED1(i)//DS0闪烁 f_mount(&fs,"0",1); // 注册工作区,驱动器号 0,初始化后其他函数可使用里面的参数 printf("注册工作区!\n"); if(f_mkfs("0",0,4096)) //格式化SD卡 printf("格式化失败!!\n"); printf("格式化成功!!\n"); res = f_open(&file, "/file.c", FA_OPEN_ALWAYS | FA_READ | FA_WRITE); if(res==0) printf("文件创建成功!!\n"); printf("文件创建失败!!\n"); res =f_write(&file,buff,strlen((const char*)buff),&data); if(res==0) printf("数据写入成功!!\n"); printf("数据写入失败!!\n"); printf("成功写入%d字节数据\n",data); f_close(&file); //关闭文件 //_FS_RPATH while(1) Delay_ms(1000); LED1(1); Delay_ms(500); LED1(0);

STemwin图形库移植与运用(基于STM32)(完成QQ界面设计、局域网聊天)

一、环境介绍主控MCU: STM32F103ZET6 STM32程序开发IDE: keil5STM32程序风格:  采用寄存器方式开发,注释齐全,执行效率高,方便移植硬件包含:  一块STM32F103ZET6开发板、一个3.5寸TFT电阻触摸显示屏(使用的是正点原子的3.5寸电阻触摸屏)工程完整源码下载地址:  https://download.csdn.net/download/xiaolong1126626497/19671518前言:STemwin图形界面库比较吃内存的,一般跑图形界面库都需要配一块SRAM,M3系列官方推荐频率是72MHZ(当前也是可以超频的),界面不是非常复杂,刷新要求不是特别高的情况下,跑起来还是不错的。 这篇文章主要讲解Stemwin的移植与基本使用。下面示例图是为了讲解STemwin基本用法,做的界面两个例子。 QQ应用的界面完成之后加上网卡就可以完成局域网之间多个设备之间聊天了。二、 STemwin介绍2.1 emWin介绍emWin是由德国SEGGER公司开发,可为图形LCD设计提供高级支持,极大简化了LCD设计。为恩智浦ARM微控制器用户免费提供的emWin图形库。在国内做嵌入式系统的大部分都使用emwin,其简单来说就是一套图形库。做电子硬件开发,常常要为设计一个良好的UI伤透脑筋,写很多的代码也不尽人意,还要不断调试,emwin正是解决这种用户界面需求的图形库,只要在你的设计中嵌入这种图形库,就能很方便使用里面的模块化设计,既能提高设计界面图形质量,还大大的减少开发时间。SEGGER公司的产品      Segger微控制器股份有限公司开发与发布软件开发工具及ANSI C软件组件(中间件)给嵌入式系统使用并应用在许多工业应用中,如通信、医疗仪器、消费性电子产品、汽车工业及工业自动化设备。EmWin是SEGGER公司设计用来提供一个有效率的、与处理器与显示控制器无关的、可应用在任何图形显示器的图形用户界面.J-Link是SEGGER公司为支持仿真ARM内核芯片推出的仿真器。emOS是SEGGER公司开发的一个实时操作系统,使用最小的资源提供一个完整的多任务系统,被设计应用在许多难处理的即时应用当中。emFile是SEGGER公司开发的嵌入式文件系统,支持FAT12、FAT16及FAT32。已经在保持最高速的前提下,优化了emFile,使之在RAM和ROM里占最小的存储器空间embos/ip是Segger开发的嵌入式TCP/IP程序驱动库。它是一个与中央处理器架构无关、且高效能的TCP/IP驱动库,在速度上、功能上及最小结构上已经做了最佳的优化。emUSB 是Segger开发的嵌入式USB协议栈。采用ANSI C的格式撰写,具有大批量通信传输和集成诸如MSD、CDC或HID设备类等特点。拓展资料:嵌入式系统无疑是当前最热门最有发展前途的IT应用领域之一。嵌入式系统用在一些特定专用设备上,通常这些设备的硬件资源(如处理器、存储器等)非常有限,并且对成本很敏感,有时对实时响应要求很高等。特别是随着消费家电的智能化,嵌入式更显重要。像我们平常见到的手机、PDA、电子字典、可视电话、VCD/DVD/MP3 Player、数字相机(DC)、数字摄像机(DV)、U-Disk、机顶盒(Set Top Box)、高清电视(HDTV)、游戏机、智能玩具、交换机、路由器、数控设备或仪表、汽车电子、家电控制系统、医疗仪器、航天航空设备等等都是典型的嵌入式系统。Keil软件Keil公司是一家业界领先的微控制器(MCU)软件开发工具的独立供应商。Keil公司由两家私人公司联合运营,分别是德国慕尼黑的Keil Elektronik GmbH和美国德克萨斯的Keil Software Inc。Keil公司制造和销售种类广泛的开发工具,包括ANSI C编译器、宏汇编程序、调试器、连接器、库管理器、固件和实时操作系统核心(real-time kernel)。Keil 官网虽然没有发布中文版本,但是Keil 系列软件却被中国80%以上的软硬件工程师使用,但凡与电子相关的专业,都会开始从单片机和计算机编程开始学习,而学习单片机自然会用到Keil 软件。国内由米尔科技、亿道电子、英倍特提供Keil 的销售和技术支持服务,他们是ARM公司合作伙伴,也是国内领先的嵌入式解决方案提供商。2.2 UCGUI与STemwin介绍说起UCGUI得先从UCOS说起,在国内做嵌入式系统的,开始入门OS的时候,大家应该都会选择uC/OS,主要是因为代码开源且资料众多。由于uC/OS的原因大家也一定接触了uC/GUI的嵌入式图形软件库。其实uC/Gui的核心代码并不是Micrium公司开发的,而是Segger公司为Micrium公司定制的图形软件库,当然也是基于Segger公司的emwin图形软件库开发的。所有说uC/GUI和emwin的使用方法没有区别。在以前较旧的版本程序中uC/Gui的源代码是开源的(可以在网上能够找到),但是新版本的程序emWin和uC/gui只对用户提供库文件,是不开源的。Segger 除了向Micrium公司提供定制的uC/GUI版本,还向其他的IC厂家提供定制服务,比如: 向ST 公司出售了emWin 的版权,从而ST公司也得到了定制版的emWin,然后改了名字叫 STemWin。当用户在 STM32 芯片上使用 emWin 软件库时,是不需要向 emWin 或 ST 公司付费的。还有NXP公司也使用了emWin的图形库,大家使用NXP芯片的时候同样也不需要支付费用。总而言之,uC/GUI和STemWin都是 Segger 公司的 emWin 产品,而且它们的版本编号是统一的,如 uC/GUI 目前最新版本命名为uC-GUI V5.24, STemWin 最新版本命名为STemWin Library V5.24,emWin 最新版本则为 emWin V5.24,所以,要比较这三个软件库功能上的区别,只需要看它们的版本号就可以了。在选择的时候,虽然功能上没有区别,但因为版权付费问题,在实际使用时就需要根据自己的平台来选择。如果我们使用的是 STM32 开发平台,自然我们选择的是STemWin;如果我们使用的是NXP的平台,我们就是用为NXP定制的emwim。在使用特定的平台,我们也需要选择定制的emWin,在STemWin里有一个检测机制确定代码所运行的平台,若是 STM32 芯片,则运行正常,若非 STM32 芯片,就不能正常使用了。同样,NXP也是一样的机制。如果使用的芯片没有授权emWin的版权,可以推荐使用UCGUI。emWin官方下载地址:https://www.segger.com/emwin.htmlSTemWin官方下载地址:https://www.st.com/en/embedded-software/stm32cubef1.htmlGCGUI官网下载地址:https://www.micrium.com/2.3 为什么要学习图形界面框架?很多产品需要人机交互,人机交互大多数是通过LCD来完成的,所以就需要我们在应用中设计LCD交互界面,简单的UI界面我们可以自己写代码完成,但是比较复杂、绚丽的界面自己来做就比较困难了。STemWin中提供了很多的控件,我们可以使用这些控件来完成复杂的界面设计。2.4 emwin下载地址emwin下载地址: https://www.segger.com/downloads/emwin/三、STemwin基本移植(不带操作系统)3.1 获取keil软件自带的emwin库在keil软件的安装目录下,自带了emwin的所有资料,适合NXP(恩智浦)单片机使用。路径: \ARM\Segger\emWin3.2 下载STemwin图形界面库STemwin适合在ST意法半导体的芯片上使用,Stemwin的资料包可直接在ST官网上进行下载。ST意法半导体官网首页地址: https://www.st.com下面是下载的步骤截图: 3.3 添加STemwin文件到工程移植之前,需要先准备一个带LCD屏驱动、触摸屏驱动的完整Keil工程。(1) 在工程目录下创建一个ST_EMWIN文件夹,用于存放STEMWIN相关的文件(2) 拷贝的目录inc文件夹从下载的包里直接拷贝过来,不做任何修改。Config文件夹里留下以下文件:Lib目录下留下以下文件: (不带OS的库文件)(3) 在keil软件里创建一个新的分组,用于存放STemwin文件。(4) 添加头文件路径3.4 屏蔽没有用到的LCDConf.h头文件3.5 修改GUIConf.h文件GUIConf.h是STemwin的核心配置文件,主要配置操作系统、触摸屏、最大窗口的支持。​3.6 修改GUIConf.c文件GUIConf.c文件配置STemwin运行时需要的内存,如果使用了SRAM外扩内存,可以将数组定义在外部的SRAM空间。​3.7 修改GUIDRV_Template.c文件GUIDRV_Template.c文件是LCD屏的驱动模板文件,需要根据自己的LCD屏驱动进行修改。主要修改的函数是:  画点函数、读点函数。3.8 修改LCDConf_FlexColor_Template.c文件GUIDRV_Template_API 变量是在GUI.h的167行定义。GUI_TOUCH_Calibrate函数是在GUI.h的3.9 增加触摸屏底层接口代码这4个函数原型在GUI.h文件的1404行声明。在GUIDRV_Template.c文件下面增加这4个触摸屏的接口函数。3.10 添加GUI_X.c文件触摸屏底层函数增加之后,再次编译。这4个函数,在GUI_X.c文件里定义,需要将GUI_X.c文件添加到工程中。GUI_X.c文件路径: \en.stemwin\STemWin_Library_V1.2.0\Libraries\STemWinLibrary532\OS\GUI_X.c将GUI_X.c文件拷贝到工程目录下的ST_EMWIN\Config目录下。3.11 定时器提供时间基准与轮询触摸屏在STemwin的GUI_X.c文件里,带有GUI_X_Delay()延时函数,该延时函数是通过OS_TimeMS变量来计算延时的时间,如果需要使用该延时函数,就需要在自己工程的硬件定时器里1ms的频率自增OS_TimeMS变量,提供时间基准。需要使用触摸屏,就需要定期调用GUI_TOUCH_Exec()函数,每秒100次的频率调用。3.13 对移植结果进行基本测试进行GUI框架初始化之前,需要先开启CRC时钟校验。运行效果如下:3.13 测试SEGGER官方DEMO代码SEGGER官方提供的DEMO代码,在ST意法半导体的官网上下载的包里没有提供,需要去SEGGER官方提供的emwin包里获取。在keil软件的安装目录下,有完整的emwin包,可以找到DEMO代码。将GUIDemo文件夹全部拷贝到工程目录下,并将里面所有的.c文件加到工程中。四、STemwin基本运用4.1 GUIBuilder软件使用GUIBulider是emwin官方出的软件,每个版本的emwin都有其对应版本的。GUIBulider软件,控件非常齐全,熟练使用 GUIBulider在使用emWin设计GUI界面的时候会起到事半功倍的效果,使用这款软件就不需要我们自己用C语言编写界面了,可以在 GUIBulider 中设计好界面,然后导出C程序,十分的方便。GUIBulider生成的代码只是一个界面框架,程序执行的逻辑代码需要用户自己填充。逻辑代码比如: 按下按键做什么,松开按键做什么等等,这些需要用户自己设计。4.2 STemwin外置中文字库设置(1) 制作GBK中文字库制作好的字库效果:字库制作好之后,可以通过文件系统+SD将字库文件烧写到板载的W25Q64 FLASH里指定位置,方便后续调用。(2) 制作ASCII 码字库上面制作了GBK中文字库,这里还需要制作尺寸一样的ASCII码字库,方便显示与中文大小相同的英文字母和标点符号。!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~注意: 最前面有个空格。  一共95个数据。​存放到程序里的只能放 12号字体、16号字体、24号字体。  太大的字体Keil软件存放不了。超出了24号的字体,可以像GBK中文字库一样存放到FLASH W25Q128里,使用的时候在去读取数据,这样就不会占用CPU本身的FLASH空间。(3) 制作好的字库文件列表​(4) 添加ASCII码字库和GBK字库的支持先将字库的必要文件添加到工程中:  (如果用不到这么多字体可以自己添加要使用的大小)​#define GUI_FONTTYPE_PROP_USER      \  GUIPROP_X_DispChar,               \  (GUI_GETCHARDISTX*)GUIPROP_X_GetCharDistX,                 \  GUIMONO_GetFontInfo,                \  GUIMONO_IsInFont,                   \  (GUI_GETCHARINFO *)0,             \  (tGUI_ENC_APIList*)0QQ登录界面(中文显示)​QQ登录框点击登录按钮之后登录成功的效果​​​​4.3 实体按键操作界面控件GUI_SendKeyMsg()函数:  向一个指定的按键发送一个状态消息。函数原型:  void GUI_SendKeyMsg(int Key, int Pressed);​示例:​​

基于STM32设计的小说阅读器(翻页、字体切换、颜色切换、语音播报)

一、环境介绍小车主控MCU: STM32F103ZET6 STM32程序开发IDE: keil5STM32程序风格:  采用寄存器方式开发,注释齐全,执行效率高,方便移植硬件包含:  一块STM32F103ZET6系统板、一个2.8寸TFT电阻触摸显示屏、一个SD卡卡槽(SPI接口)、一张SD卡(存放字库和小说文件)工程完整源码下载地址: https://download.csdn.net/download/xiaolong1126626497/19628524二、功能介绍这是基于ST32F103ZET6设计的小说阅读器,虽然对于真实的小说阅读器产品来讲,实用性和功能方面还差很多,但是对于刚入门的STM32、单片机开发工程师来讲,这里面设计到的技术才是最有价值的。 所以这篇文章的小说阅读器主要是用来作为嵌入式单片机工程师入门练手项目、大学生的课程设计等。目的不在于小说阅读器,而是以小说阅读器为例子,学习相关的技术:  SD卡、串口通信、SPI通信、8080时序、触摸屏校准原理、FATFS文件系统使用、语音播报模块使用等等。该阅读器支持常规阅小说读器具备的基本功能:1.  支持选择指定的小说进行查看阅读,可以通过触摸屏上的按钮进行切换。2.  支持切换字体大小3.  支持切换字体颜色、背景颜色4. 标题栏显示当前阅读器查看的小说文件名称5.  支持翻页、上一页、下一页6. 支持语音自动阅读,发声接近正常真人发声,非常强大。语音方案可以选择两种:  (1). 宇音SYN6658  (2). 科大讯飞SYN5152。    这两款芯片都是通过串口通信,编程十分简单。内部编程思路介绍: 小说阅读器的字体是存放在SD卡上的,SD卡采用SPI接口的卡槽与STM32相连接,STM32配合FATFS文件系统对SD卡上的文件进行操作;为了提高访问效率、在第一次上电的时候会将SD卡上的字库文件拷贝到板载W25Q64芯片内。小说文件还是存放在SD卡上,每次翻页的时候从SD卡上获取文本文件,渲染到LCD显示屏上。该显示屏是2.8寸的电阻触摸显示屏,驱动芯片是ILI9341(兼容:9325,9328),LCD的引脚接线兼容正点原子的2.8寸LCD显示屏;电阻屏的驱动芯片是XPT2046,,是很常见的组合,这个XPT2046就是个ADC芯片,最终要完成触摸屏上坐标点定位,还需要自己写校准算法进行换算。 ILI9341驱动芯片支持8080时序操作,可以采用IO模拟方式驱动、也可以采用STM32的FSMC接口驱动。 STM32增强版支持FSMC功能的,其他没有FSMC接口的芯片,可以采用模拟8080时序方式驱动,效果一样,只是效率上差点,无法实现高速刷屏,只要不进行高速刷屏,凑合使用是没什么问题的。三、所用到的硬件介绍(都是淘宝买的)3.1 STM32F103ZET6最小系统板   这是在淘宝上买的硬件详情,开发板和LCD用哪一款都可以的,编程思路都是一样。开发板的板载资源如下:CPU:STM32F103ZET6,LQFP144,FLASH:512K,SRAM:64K;外扩SPI FLASH:W25Q32,8M字节;1个电源指示灯;2个状态指示灯;一个EEPROM芯片,24C02,容量256字节(注意:不同产地标号不一,但都是24C02芯片,经测试无误)1个光敏传感器;1个无线模块接口,可接NRF24L01/RFID/CC01模块;1路CAN接口,采用TJA1050芯片;1路485接口,采用SP485芯片;1个标准的2.4/2.8/3.5/4.3/7寸LCD接口,支持触摸屏;一个USB串口,可用于程序下载和代码调试(USMART调试);1个USB SLAVE接口,用于USB通信;1个复位按键;2个独立按键;1个SD卡座,用来接SD卡;1个RTC后备电池座;1个标准的JTAG/SWD仿真下载调试接口;1路5V转3.3V电路;芯片引脚144个脚全部引出,方便外接扩展实验;1个电源开关,用来开关USB的电源;3.2 SD卡卡槽3.3 SYN6658语音合成芯片功能特点:•  芯片支持任意中文文本的合成,可以采用GB2312、GBK、BIG5 和Unicode 四种编码方式;•  芯片具有文本智能分析处理功能,对常见的数值、电话号码、时间日期、度量衡符号等格式的文本;•  芯片可以自动对文本进行分析,判别文本中多音字的读法并合成正确的读音;•  芯片可实现10级数字音量控制,音量更大,更广;•  芯片内集成了77首声音提示音和14首和弦音乐;•  提供两男、两女、一个效果器和一个女童声共6个中文发音人;•  支持多种文本控制标记,提升文本处理的正确率;•  支持多种控制命令,包括:合成、停止、暂停合成、继续合成、改变波特率等;•  支持多种方式查询芯片的工作状态;•  两种通讯模式:芯片支持UART、SPI两种通讯方式;•  芯片支持Power Down 模式。使用控制命令可以使芯片进入Power Down 模式;•  芯片支持的通讯波特率:4800bps,9600bps,57600bps、115200bps;•  芯片各项指标均满足室外严酷环境下的应用;应用范围:•  车载信息终端语音播报,车载调度,车载导航•  停车场收费系统/诱导系统•  公交报站器 ,考勤机•  手机,固定电话•  排队叫号机,收银收费机•  自动售货机,信息机, POS 机•  智能仪器仪表 ,气象预警机,智能变压器•  智能玩具,智能手表•  电动自行车•  语音电子书,彩屏故事书,语音电子词典,语音电子导游•  短消息播放 ,新闻播放•  电子地图四、操作说明 4.1 程序下载开发板支持Jlink下载、也支持串口下载。4.2 屏幕操作说明目前实现的功能:1.    小说翻页:支持点击触摸屏按钮翻下一页显示2.    换小说:点击触摸屏按钮“下一本”,可以切换小说。3.    换颜色:点击触摸屏按钮“颜色调整”,可以切换颜色,支持12种字体颜色切换。4.    换字体:点击触摸屏按钮“字体调整”,可以切换字体,目前支持两种字体(16X16  24X24)。思路说明:程序里移植了FATFS文件系统,字体文件和小说文件都是存放在SD卡,通过文件系统读取SD卡里的小说文件进行显示。 操作的过程在串口调试助手上也会同步输出信息。4.3 校准说明第一次使用,需要校准屏幕,否则触摸屏没有反应。如果发现屏幕不灵敏,可以强制进行校准,按下按键K2再按下复位键即可进行强制校准。依次点击屏幕上4个红圈。4.4 SD卡上存放的文件 SD卡上有两个目录:font目录和txt目录。font目录:存放字库文件。有两个字库字体。txt目录:存放小说文件,内置了3篇小说。五、核心代码代码采用Keil5编写,下载即可编译,测试,学习。工程完整源码下载地址: https://download.csdn.net/download/xiaolong1126626497/196285245.1  main.c  主函数代码#include "stm32f10x.h" #include "delay.h" #include "sys.h" #include "usart.h" #include <string.h> #include <stdio.h> #include "iic.h" #include "at24c08.h" #include "w25q64.h" #include "nt35310_lcd.h" #include "xpt2046.h" #include "sdcard.h" #include "ff.h" //FATFS文件系统的头文件 //更新字库---从SD卡读取字库到W25Q64 void FontUpdate_to_W25Q64(); FATFS fatfs; //文件系统注册工作区需要使用 u16 select_color[]={WHITE,BLACK,BLUE,RED,YELLOW,BROWN,BRRED,GRAY,DARKBLUE,LIGHTBLUE,GRAYBLUE,LIGHTGREEN}; u8 read_text_buf[4096+1]; int main() u32 x;u32 y;u32 size=16;u8 *p; u8 color_select_cnt=0; //12个 FIL text_file; u16 br=0; u8 r_data=10; u32 read_cnt=0; DIR dir; FRESULT res; FILINFO fno; //存放读取的文件信息 char *abs_path=NULL; char path[]="0:/txt"; u32 cnt=0; USART_X_Init(USART1,72,115200); NT35310_LcdInit(); NT35310_Clear(WHITE); IIC_Init(); //IIC总线初始化 W25Q64_Init(); //初始化W25Q64 TOUCH_Init(); //触摸屏初始化 TOUCH_CheckXY(); //触摸屏校准程序 RCC->APB2ENR|=1<<5; GPIOD->CRH&=0xFF0FFFFF; GPIOD->CRH|=0x00300000; while(SDCardDeviceInit()!=0) printf("SDCard_DeviceInit 错误.\r\n"); PDout(13)=!PDout(13); delay_ms(100); f_mount(&fatfs,"0:",0); //注册文件系统的工作区 //设计界面 LCD_color_1=RED; LCD_color_2=LIGHTBLUE; NT35310_DisplayString(16,0,16,"基于STM32的小说阅读器设计"); NT35310_DrawLine(0,16,239,16,DARKBLUE); //绘制按键 NT35310_DrawRectangle(0,319-80,239,319,RED); NT35310_DrawLine(0,319-40,239,319-40,DARKBLUE); NT35310_DrawLine(239/2,319-80,239/2,319,DARKBLUE); LCD_color_2=WHITE; NT35310_DisplayString(32,319-70,16,"下一页"); NT35310_DisplayString(239/2+32,319-70,16,"下一本"); NT35310_DisplayString(32,319-30,16,"字体调整"); NT35310_DisplayString(239/2+32,319-30,16,"颜色调整"); /*1. 打开目录*/ res=f_opendir(&dir,path); if(res!=FR_OK)return res; res=f_readdir(&dir,&fno); printf("文件名称: %s,文件大小: %ld 字节\r\n",fno.fname,fno.fsize); LCD_color_1=BLACK; NT35310_DisplayString(0,17,16,fno.fname); if(abs_path) free(abs_path); abs_path=NULL; //申请存放文件名称的长度 abs_path=malloc(strlen(path)+strlen(fno.fname)+1); strcpy(abs_path,path); strcat(abs_path,"/"); strcat(abs_path,fno.fname); printf("abs_path=%s\n",abs_path); NT35310_DisplayString(0,17+16,16,"第1卷\ 第一回 甄士隐梦幻识通灵 贾雨村风尘怀闺秀\ 此开卷第一回也。作者自云:因曾历过一番梦幻之后,故将真事隐去,\ 而借“通灵”之说,撰此<<石头记>>一书也。故曰“甄士隐”云云。\ 但书中所记何事何人?自又云:“今风尘碌碌,一事无成,忽念及当日所有之女子,\ 一一细考较去,觉其行止见识,皆出于我之上。何我堂堂须眉,诚不若彼裙钗哉?"); while(1) if(TOUCH_PEN==0) //判断触摸屏是否按下 //判断是否读取到XY坐标 if(TOUCH_ReadXY()) // printf("x=%d,y=%d\r\n",touch_info.x,touch_info.y); //判断范围 if((touch_info.x>=0 && touch_info.x<=239/2)&& (touch_info.y>=319-80 && touch_info.y<=319-40)) LCD_color_2=BLUE; //填充颜色 NT35310_Fill(0+1,319-80+1,239/2-1,319-40-1,BLUE); //显示字符串 NT35310_DisplayString(32,319-70,16,"下一页"); //等待触摸屏松开 while(TOUCH_PEN==0){} //填充颜色--清屏 NT35310_Fill(0,18+16,239,319-80-1,WHITE); LCD_color_2=WHITE; if(read_cnt>=br) read_cnt=0; if(read_cnt==0) if(br!=4096) res=f_open(&text_file,(const TCHAR*)abs_path,FA_READ);//打开文件 if(res!=0) printf("%s文件打开失败!\r\n",abs_path); return 1; //文件打开失败 printf("%s文件打开成功!\n",abs_path); //执行代码 res=f_read(&text_file,read_text_buf,4096,(UINT*)&br);//读出4096个字节 read_text_buf[br]='\0'; printf("br=%d\r\n",br); if(br!=4096) f_close(&text_file); //字体大小 x=0; //坐标起始位置 y=17+16; //坐标起始位置 p=read_text_buf+read_cnt; while(*p!='\0') if(*p>0x80) //判断是否是中文-编码规则从 0X8140 开始 read_cnt+=2; if(x+size>239) x=0; //横坐归0 y+=size; //换行 if(y+size>=319-80-1)break; NT35310_DisplayGBKData(x,y,size,p);//显示一个中文 x+=size; p+=2; //偏移两个字节 else if(*p>=' ' && *p<='~') //常用的ASCII码 read_cnt+=1; if(x+size/2>239) x=0; //横坐归0 y+=size; //换行 if(y+size>=319-80-1)break; if(size==16) //显示英文字母 NT35310_DisplayData(x,y,size/2,size,(u8*)ASCII_8_16[*p-' ']); else if(size==24) //显示英文字母 NT35310_DisplayData(x,y,size/2,size,(u8*)asc2_2412[*p-' ']); p+=1; x+=size/2; else if(*p=='\n') x=0; y+=size; p+=1; //偏移指针 read_cnt+=1; if(y+size>=319-80-1)break; read_cnt+=1; p+=1; //偏移指针 //填充颜色 NT35310_Fill(0+1,319-80+1,239/2-1,319-40-1,WHITE); LCD_color_2=WHITE; //显示字符串 NT35310_DisplayString(32,319-70,16,"下一页"); //判断范围 if((touch_info.x>=239/2 && touch_info.x<=239)&& (touch_info.y>=319-80 && touch_info.y<=319-40)) LCD_color_2=BLUE; //填充颜色 NT35310_Fill(239/2+1,319-80+1,239-1,319-40-1,BLUE); //显示字符串 NT35310_DisplayString(239/2+32,319-70,16,"下一本"); //等待触摸屏松开 while(TOUCH_PEN==0){} LCD_color_2=WHITE; //关闭原来的文件 f_close(&text_file); //触发新的页 read_cnt=0; br=0; //执行代码 res=f_readdir(&dir,&fno); if(fno.fname[0] == 0 || res!=0) /*3. 关闭目录*/ f_closedir(&dir); /*1. 打开目录*/ res=f_opendir(&dir,path); if(res!=FR_OK)return res; res=f_readdir(&dir,&fno); printf("文件名称: %s,文件大小: %ld 字节\r\n",fno.fname,fno.fsize); LCD_color_1=BLACK; NT35310_DisplayString(0,17,16,fno.fname); if(abs_path) free(abs_path); abs_path=NULL; //申请存放文件名称的长度 abs_path=malloc(strlen(path)+strlen(fno.fname)+1); strcpy(abs_path,path); strcat(abs_path,"/"); strcat(abs_path,fno.fname); printf("abs_path=%s\n",abs_path); //填充颜色 NT35310_Fill(239/2+1,319-80+1,239-1,319-40-1,WHITE); //显示字符串 NT35310_DisplayString(239/2+32,319-70,16,"下一本"); //判断范围 if((touch_info.x>=0 && touch_info.x<=239/2)&& (touch_info.y>=319-40 && touch_info.y<=319)) LCD_color_2=BLUE; //填充颜色 NT35310_Fill(0+1,319-40+1,239/2-1,319-1,BLUE); //显示字符串 NT35310_DisplayString(32,319-30,16,"字体调整"); //等待触摸屏松开 while(TOUCH_PEN==0){} if(size==16)size=24; else size=16; //执行代码 //填充颜色 NT35310_Fill(0+1,319-40+1,239/2-1,319-1,WHITE); LCD_color_2=WHITE; //显示字符串 NT35310_DisplayString(32,319-30,16,"字体调整"); //判断范围 if((touch_info.x>=239/2 && touch_info.x<=239)&& (touch_info.y>=319-40 && touch_info.y<=319)) LCD_color_2=BLUE; //填充颜色 NT35310_Fill(239/2+1,319-40+1,239-1,319-1,BLUE); //显示字符串 NT35310_DisplayString(239/2+32,319-30,16,"颜色调整"); //等待触摸屏松开 while(TOUCH_PEN==0){} //执行代码 //前景字体颜色切换 LCD_color_1=select_color[color_select_cnt++]; if(color_select_cnt>=12) color_select_cnt=0; //填充颜色 NT35310_Fill(239/2+1,319-40+1,239-1,319-1,WHITE); LCD_color_2=WHITE; //显示字符串 NT35310_DisplayString(239/2+32,319-30,16,"颜色调整"); u32 gbk32_32_addr=1024*0; u8 font_buffer[4096]; //更新字库---从SD卡读取字库到W25Q64 void FontUpdate_to_W25Q64() u32 w_cnt=0; FILINFO fno; FIL fp; UINT br,res; /*1. 打开字库*/ f_open(&fp,"0:/font/gbk16.DZK",FA_READ); /*2. 循环读取字库更新到W25Q64*/ f_stat("0:/font/gbk16.DZK",&fno); printf("文件的大小:%d\r\n",fno.fsize); while(1) /*3. 读取字库文件*/ res=f_read(&fp,font_buffer,4096,&br); /*4. 写入到W25Q64里*/ W25Q64_WriteData(gbk32_32_addr,br,font_buffer); gbk32_32_addr+=br; w_cnt+=br; printf("font16:%.f%%\r\n",(w_cnt*1.0/fno.fsize)*100); /*5. 判断文件是否结束*/ if(res!=FR_OK||br!=4096)break; /*6. 关闭字库文件*/ f_close(&fp); 5.2 sdcard.c  SD卡驱动代码#include "sdcard.h" static u8 SD_Type=0; //存放SD卡的类型 函数功能:SD卡底层接口,通过SPI时序向SD卡读写一个字节 函数参数:data是要写入的数据 返 回 值:读到的数据 说明:时序是第二个上升沿采集数据 u8 SDCardReadWriteOneByte(u8 DataTx) u8 DataRx; u8 i; for(i=0;i<8;i++) SDCardSCLK(0); if(DataTx&0x80){SDCardOut(1);} else {SDCardOut(0);} DataTx<<=1; SDCardSCLK(1);//第二个上升沿采集数据 DataRx<<=1; if(SDCardInput)DataRx|=0x01; return DataRx; 函数功能:底层SD卡接口初始化 本程序SPI接口如下: PC11 片选 SDCardCS PC12 时钟 SDCardSCLK PD2 输出 SPI_MOSI--主机输出从机输入 PC8 输入 SPI_MISO--主机输入从机输出 void SDCardSpiInit(void) RCC->APB2ENR|=1<<5; //使能PORTD时钟 RCC->APB2ENR|=1<<4; //使能PORTC时钟 GPIOD->CRL&=0XFFFFF0FF; GPIOD->CRL|=0X00000300; //PD2 GPIOD->ODR|=1<<2; //PD2 GPIOC->CRH&=0XFFF00FF0; GPIOC->CRH|=0X00033008; GPIOC->ODR|=0X3<<11; GPIOC->ODR|=1<<8; SDCardCS(1); 函数功能:取消选择,释放SPI总线 void SDCardCancelCS(void) SDCardCS(1); SDCardReadWriteOneByte(0xff);//提供额外的8个时钟 函数 功 能:选择sd卡,并且等待卡准备OK 函数返回值:0,成功;1,失败; u8 SDCardSelectCS(void) SDCardCS(0); if(SDCardWaitBusy()==0)return 0;//等待成功 SDCardCancelCS(); return 1;//等待失败 函数 功 能:等待卡准备好 函数返回值:0,准备好了;其他,错误代码 u8 SDCardWaitBusy(void) u32 t=0; if(SDCardReadWriteOneByte(0XFF)==0XFF)return 0;//OK t++; }while(t<0xFFFFFF);//等待 return 1; 函数功能:等待SD卡回应 函数参数: Response:要得到的回应值 返 回 值: 0,成功得到了该回应值 其他,得到回应值失败 u8 SDCardGetAck(u8 Response) u16 Count=0xFFFF;//等待次数 while((SDCardReadWriteOneByte(0XFF)!=Response)&&Count)Count--;//等待得到准确的回应 if(Count==0)return SDCard_RESPONSE_FAILURE;//得到回应失败 else return SDCard_RESPONSE_NO_ERROR;//正确回应 函数功能:从sd卡读取一个数据包的内容 函数参数: buf:数据缓存区 len:要读取的数据长度. 返回值: 0,成功;其他,失败; u8 SDCardRecvData(u8*buf,u16 len) if(SDCardGetAck(0xFE))return 1;//等待SD卡发回数据起始令牌0xFE while(len--)//开始接收数据 *buf=SDCardReadWriteOneByte(0xFF); buf++; //下面是2个伪CRC(dummy CRC) SDCardReadWriteOneByte(0xFF); SDCardReadWriteOneByte(0xFF); return 0;//读取成功 函数功能:向sd卡写入一个数据包的内容 512字节 函数参数: buf 数据缓存区 cmd 指令 返 回 值:0表示成功;其他值表示失败; u8 SDCardSendData(u8*buf,u8 cmd) u16 t; if(SDCardWaitBusy())return 1; //等待准备失效 SDCardReadWriteOneByte(cmd); if(cmd!=0XFD)//不是结束指令 for(t=0;t<512;t++)SDCardReadWriteOneByte(buf[t]);//提高速度,减少函数传参时间 SDCardReadWriteOneByte(0xFF); //忽略crc SDCardReadWriteOneByte(0xFF); t=SDCardReadWriteOneByte(0xFF); //接收响应 if((t&0x1F)!=0x05)return 2; //响应错误 return 0;//写入成功 函数功能:向SD卡发送一个命令 函数参数: u8 cmd 命令 u32 arg 命令参数 u8 crc crc校验值 返回值:SD卡返回的响应 u8 SendSDCardCmd(u8 cmd, u32 arg, u8 crc) u8 r1; u8 Retry=0; SDCardCancelCS(); //取消上次片选 if(SDCardSelectCS())return 0XFF;//片选失效 //发送数据 SDCardReadWriteOneByte(cmd | 0x40);//分别写入命令 SDCardReadWriteOneByte(arg >> 24); SDCardReadWriteOneByte(arg >> 16); SDCardReadWriteOneByte(arg >> 8); SDCardReadWriteOneByte(arg); SDCardReadWriteOneByte(crc); if(cmd==SDCard_CMD12)SDCardReadWriteOneByte(0xff);//Skip a stuff byte when stop reading Retry=0X1F; r1=SDCardReadWriteOneByte(0xFF); }while((r1&0X80) && Retry--); //等待响应,或超时退出 return r1; //返回状态值 函数功能:获取SD卡的CID信息,包括制造商信息 函数参数:u8 *cid_data(存放CID的内存,至少16Byte) 返 回 值: 0:成功,1:错误 u8 GetSDCardCISDCardOutnfo(u8 *cid_data) u8 r1; //发SDCard_CMD10命令,读CID r1=SendSDCardCmd(SDCard_CMD10,0,0x01); if(r1==0x00) r1=SDCardRecvData(cid_data,16);//接收16个字节的数据 SDCardCancelCS();//取消片选 if(r1)return 1; else return 0; 函数说明: 获取SD卡的CSD信息,包括容量和速度信息 函数参数: u8 *cid_data(存放CID的内存,至少16Byte) 返 回 值: 0:成功,1:错误 u8 GetSDCardCSSDCardOutnfo(u8 *csd_data) u8 r1; r1=SendSDCardCmd(SDCard_CMD9,0,0x01); //发SDCard_CMD9命令,读CSD if(r1==0) r1=SDCardRecvData(csd_data, 16);//接收16个字节的数据 SDCardCancelCS();//取消片选 if(r1)return 1; else return 0; 函数功能:获取SD卡的总扇区数(扇区数) 返 回 值: 0表示容量检测出错,其他值表示SD卡的容量(扇区数/512字节) 说 明: 每扇区的字节数必为512字节,如果不是512字节,则初始化不能通过. u32 GetSDCardSectorCount(void) u8 csd[16]; u32 Capacity; u8 n; u16 csize; if(GetSDCardCSSDCardOutnfo(csd)!=0) return 0; //取CSD信息,如果期间出错,返回0 if((csd[0]&0xC0)==0x40) //V2.00的卡,如果为SDHC卡,按照下面方式计算 csize = csd[9] + ((u16)csd[8] << 8) + 1; Capacity = (u32)csize << 10;//得到扇区数 else//V1.XX的卡 n = (csd[5] & 15) + ((csd[10] & 128) >> 7) + ((csd[9] & 3) << 1) + 2; csize = (csd[8] >> 6) + ((u16)csd[7] << 2) + ((u16)(csd[6] & 3) << 10) + 1; Capacity= (u32)csize << (n - 9);//得到扇区数 return Capacity; 函数功能: 初始化SD卡 返 回 值: 非0表示初始化失败! u8 SDCardDeviceInit(void) u8 r1; // 存放SD卡的返回值 u16 retry; // 用来进行超时计数 u8 buf[4]; u16 i; SDCardSpiInit(); //初始化底层IO口 for(i=0;i<10;i++)SDCardReadWriteOneByte(0XFF); //发送最少74个脉冲 retry=20; r1=SendSDCardCmd(SDCard_CMD0,0,0x95);//进入IDLE状态 闲置 }while((r1!=0X01) && retry--); SD_Type=0; //默认无卡 if(r1==0X01) if(SendSDCardCmd(SDCard_CMD8,0x1AA,0x87)==1) //SD V2.0 for(i=0;i<4;i++)buf[i]=SDCardReadWriteOneByte(0XFF); //Get trailing return value of R7 resp if(buf[2]==0X01&&buf[3]==0XAA) //卡是否支持2.7~3.6V retry=0XFFFE; SendSDCardCmd(SDCard_CMD55,0,0X01); //发送SDCard_CMD55 r1=SendSDCardCmd(SDCard_CMD41,0x40000000,0X01);//发送SDCard_CMD41 }while(r1&&retry--); if(retry&&SendSDCardCmd(SDCard_CMD58,0,0X01)==0)//鉴别SD2.0卡版本开始 for(i=0;i<4;i++)buf[i]=SDCardReadWriteOneByte(0XFF);//得到OCR值 if(buf[0]&0x40)SD_Type=SDCard_TYPE_V2HC; //检查CCS else SD_Type=SDCard_TYPE_V2; else//SD V1.x/ MMC V3 SendSDCardCmd(SDCard_CMD55,0,0X01); //发送SDCard_CMD55 r1=SendSDCardCmd(SDCard_CMD41,0,0X01); //发送SDCard_CMD41 if(r1<=1) SD_Type=SDCard_TYPE_V1; retry=0XFFFE; do //等待退出IDLE模式 SendSDCardCmd(SDCard_CMD55,0,0X01); //发送SDCard_CMD55 r1=SendSDCardCmd(SDCard_CMD41,0,0X01);//发送SDCard_CMD41 }while(r1&&retry--); else//MMC卡不支持SDCard_CMD55+SDCard_CMD41识别 SD_Type=SDCard_TYPE_MMC;//MMC V3 retry=0XFFFE; do //等待退出IDLE模式 r1=SendSDCardCmd(SDCard_CMD1,0,0X01);//发送SDCard_CMD1 }while(r1&&retry--); if(retry==0||SendSDCardCmd(SDCard_CMD13,512,0X01)!=0)SD_Type=SDCard_TYPE_ERR;//错误的卡 SDCardCancelCS(); //取消片选 if(SD_Type)return 0; //初始化成功返回0 else if(r1)return r1; //返回值错误值 return 0xaa; //其他错误 函数功能:读SD卡 函数参数: buf:数据缓存区 sector:扇区 cnt:扇区数 0,ok;其他,失败. 说 明: SD卡一个扇区大小512字节 u8 SDCardReadData(u8*buf,u32 sector,u32 cnt) u8 r1; if(SD_Type!=SDCard_TYPE_V2HC)sector<<=9;//转换为字节地址 if(cnt==1) r1=SendSDCardCmd(SDCard_CMD17,sector,0X01);//读命令 if(r1==0) //指令发送成功 r1=SDCardRecvData(buf,512); //接收512个字节 }else r1=SendSDCardCmd(SDCard_CMD18,sector,0X01);//连续读命令 r1=SDCardRecvData(buf,512);//接收512个字节 buf+=512; }while(--cnt && r1==0); SendSDCardCmd(SDCard_CMD12,0,0X01); //发送停止命令 SDCardCancelCS();//取消片选 return r1;// 函数功能:向SD卡写数据 函数参数: buf:数据缓存区 sector:起始扇区 cnt:扇区数 返回值: 0,ok;其他,失败. 说 明: SD卡一个扇区大小512字节 u8 SDCardWriteData(u8*buf,u32 sector,u32 cnt) u8 r1; if(SD_Type!=SDCard_TYPE_V2HC)sector *= 512;//转换为字节地址 if(cnt==1) r1=SendSDCardCmd(SDCard_CMD24,sector,0X01);//读命令 if(r1==0)//指令发送成功 r1=SDCardSendData(buf,0xFE);//写512个字节 if(SD_Type!=SDCard_TYPE_MMC) SendSDCardCmd(SDCard_CMD55,0,0X01); SendSDCardCmd(SDCard_CMD23,cnt,0X01);//发送指令 r1=SendSDCardCmd(SDCard_CMD25,sector,0X01);//连续读命令 if(r1==0) r1=SDCardSendData(buf,0xFC);//接收512个字节 buf+=512; }while(--cnt && r1==0); r1=SDCardSendData(0,0xFD);//接收512个字节 SDCardCancelCS();//取消片选 return r1;//

基于STM32设计的遥控小车(手机APP+GPS+温湿度+ESP8266)

一、环境介绍小车主控MCU: STM32F103ZET6 STM32程序开发IDE: keil5STM32程序风格:  采用寄存器方式开发,注释齐全,执行效率高,方便移植手机APP:  采用QT设计,程序支持跨平台编译运行(Android、IOS、Windows、Linux都可以编译运行,对应平台上QT的环境搭建,之前博客已经发了文章讲解)硬件包含:  淘宝购买的完整一套4轮遥控小车(采用STM32F103ZET6作为主控板)、DHT11温湿度传感器、中科微GPS模块、ESP8266小车完整源码下载地址: https://download.csdn.net/download/xiaolong1126626497/19557040APP完整源码下载地址: https://download.csdn.net/download/xiaolong1126626497/19557009二、功能介绍这是基于STM32设计的4轮遥控小车,支持通过Android手机APP、Windows上位机完成对小车遥控;支持前进、后退、左转、右转、停止等操作。小车上会实时采集温度、湿度、GPS经纬度、通过ESP8266 WIFI上传至手机APP,手机APP收到数据之后,会将温湿度实时显示出来,经纬度收到后会调用百度地图,显示小车的位置,并且数据也会存放到数据库里,方便查看历史数据;支持范围内温湿度查询、最高温湿度、最低温湿度查询。小车电机驱动模块采用L298N、WIFI模块采用ESP8266、MCU采用STM32F103C8T6、温湿度模块采用DTH11、GPS模块采用北斗GPS+BDS。三、相关硬件介绍四、程序源码硬件连接说明:  GPS接的串口1:   PA3(RX)   --5V~3.3VWIFI接的串口3:  PB10(TX)--->接ESP8266的RX    PB11(RX)--->接ESP8266的TX    --3.3VDHT11温湿度接:  PA7  4.1  STM32小车端:  main.c源码#include "stm32f10x.h" #include "led.h" #include "delay.h" #include "key.h" #include "usart.h" #include <string.h> #include "timer.h" #include "bluetooth.h" #include "esp8266.h" #include "dht11.h" #include "gps.h" #include "motor.h" 硬件连接说明: GPS接的串口1: PA3(RX) --5V~3.3V WIFI接的串口3: PB10(TX)--->接ESP8266的RX PB11(RX)--->接ESP8266的TX --3.3V DHT11温湿度接: PA7 u8 ESP8266_SendBuff[50]; char Buffer[1024]; int main() u32 time_cnt=0; double Longitude=120.086199; double latitude=30.139219; u8 temp=20; u8 humi=70; //延时2秒保证系统稳定 delay_ms(1000); delay_ms(1000); LED_Init(); BEEP_Init(); USART1_Init(115200); //串口调试 USART2_Init(9600); //接GPS模块 TIMER2_Init(72,20000); USART3_Init(115200); //串口-WIFI ESP8166_01默认波特率9600 ESP8266_12F默认波特率115200 TIMER3_Init(72,20000); //超时时间20ms printf("正在初始化请稍等.\r\n"); printf("DHT11_Init:%d\r\n",DHT11_Init());//温湿度传感器初始化 printf("准备检测WIFI硬件,请稍等...\r\n"); //初始化WIFI硬件 if(ESP8266_Init())printf("WIFI硬件错误.\r\n"); printf("WIFI设备正常....\r\n"); //配置WIFI的模式 192.168.4.1 printf("WIFI配置状态:%d\r\n",ESP8266_AP_TCP_Server_Mode("esp8266_666","12345678",8089)); MotorInit(); //电机初始化 //电机脉冲控制 TIMER4_Init(72,1000); while(1) //接收到GPS的数据 if(USART2_RX_FLAG) USART2_RX_BUFFER[USART2_RX_CNT]='\0'; //解析经纬度 GPS_GNRMC_Decoding((char*)USART2_RX_BUFFER,&Longitude,&latitude); USART2_RX_CNT=0; USART2_RX_FLAG=0; //打印到串口调试助手 printf("GPS:%f,%f\r\n",Longitude,latitude); //接收到WIFI的数据 if(USART3_RX_FLAG) USART3_RX_BUFFER[USART3_RX_CNT]='\0'; printf("WIFI:%s\r\n",USART3_RX_BUFFER); strcpy(Buffer,(char*)USART3_RX_BUFFER); USART3_RX_CNT=0; USART3_RX_FLAG=0; BEEP=1; delay_ms(50); BEEP=0; if(strstr((char*)Buffer,":a")) printf("向前...\r\n"); CarGo(); else if(strstr((char*)Buffer,":b")) printf("后退...\r\n"); CarBack(); else if(strstr((char*)Buffer,":c")) printf("向左...\r\n"); CarLeft(); else if(strstr((char*)Buffer,":d")) printf("向右...\r\n"); CarRight(); else if(strstr((char*)Buffer,":e")) printf("停止...\r\n"); CarStop(); time_cnt++; delay_ms(10); //判断轮询时间 if(time_cnt>=100*2) time_cnt=0; //读取温湿度数据 DHT11_Read_Data(&temp,&humi); sprintf((char*)ESP8266_SendBuff,"#%d,%d,%f,%f",temp,humi,Longitude,latitude); //向服务器上传数据 ESP8266_ServerSendData(0,ESP8266_SendBuff,strlen((char*)ESP8266_SendBuff)); //打印到串口调试助手 printf("ESP8266_SendBuff:%s\r\n",(char *)ESP8266_SendBuff); //运行状态 Motor_LED=!Motor_LED; 4.2 STM32小车端: 电机控制源码#include "motor.h" //全局变量定义 unsigned int speed_count=0;//占空比计数器 50次一周期 int front_left_speed_duty=SPEED_DUTY; int front_right_speed_duty=SPEED_DUTY; int behind_left_speed_duty=SPEED_DUTY; int behind_right_speed_duty=SPEED_DUTY; unsigned char continue_time=0; //根据占空比驱动电机转动 void CarMove(void) BEHIND_RIGHT_EN; //右前轮 if(front_right_speed_duty > 0)//向前 if(speed_count < front_right_speed_duty) FRONT_RIGHT_GO; }else //停止 FRONT_RIGHT_STOP; else if(front_right_speed_duty < 0)//向后 if(speed_count < (-1)*front_right_speed_duty) FRONT_RIGHT_BACK; }else //停止 FRONT_RIGHT_STOP; else //停止 FRONT_RIGHT_STOP; //左后轮 if(behind_left_speed_duty > 0)//向前 if(speed_count < behind_left_speed_duty) BEHIND_LEFT_GO; } else //停止 BEHIND_LEFT_STOP; else if(behind_left_speed_duty < 0)//向后 if(speed_count < (-1)*behind_left_speed_duty) BEHIND_LEFT_BACK; } else //停止 BEHIND_LEFT_STOP; else //停止 BEHIND_LEFT_STOP; void CarGo(void) front_left_speed_duty=SPEED_DUTY; front_right_speed_duty=SPEED_DUTY; behind_left_speed_duty=SPEED_DUTY; behind_right_speed_duty=SPEED_DUTY; void CarBack(void) front_left_speed_duty=-SPEED_DUTY; front_right_speed_duty=-SPEED_DUTY; behind_left_speed_duty=-SPEED_DUTY; behind_right_speed_duty=-SPEED_DUTY; void CarLeft(void) front_left_speed_duty=-20; front_right_speed_duty=SPEED_DUTY; behind_left_speed_duty=-20; behind_right_speed_duty=SPEED_DUTY+10;//增加后轮驱动力 void CarRight(void) front_left_speed_duty=SPEED_DUTY; front_right_speed_duty=-20; behind_left_speed_duty=SPEED_DUTY+10;//增加后轮驱动力 behind_right_speed_duty=-20; void CarStop(void) front_left_speed_duty=0; front_right_speed_duty=0; behind_left_speed_duty=0; behind_right_speed_duty=0; FRONT_LEFT_F_PIN PG13 左前前进IO FRONT_LEFT_B_PIN PG11 左前后退IO FRONT_RIGHT_F_PIN PC11 右前前进IO FRONT_RIGHT_B_PIN PD0 右前后退IO BEHIND_LEFT_F_PIN PD6 左后前进IO BEHIND_LEFT_B_PIN PG9 左后后退IO 右后电机的两个控制IO这里改为两路使能EN1、EN2,高电平有效 BEHIND_RIGHT_F_PIN PD4 右电机使能IO BEHIND_RIGHT_B_PIN PD2 左电机使能IO void MotorInit(void) RCC->APB2ENR|=1<<8; //PG RCC->APB2ENR|=1<<5; //PD RCC->APB2ENR|=1<<4; //PC GPIOG->CRH&=0xFF0F0F0F; GPIOG->CRH|=0x00303030; GPIOD->CRL&=0xF0F0F0F0; GPIOD->CRL|=0x03030303; GPIOC->CRH&=0xFFFF0FFF; GPIOC->CRH|=0x00003000; CarStop(); 4.3  STM32小车端:  ESP8266 WIFI源码#include "esp8266.h" u8 ESP8266_IP_ADDR[16]; //255.255.255.255 u8 ESP8266_MAC_ADDR[18]; //硬件地址 函数功能: ESP8266命令发送函数 函数返回值:0表示成功 1表示失败 u8 ESP8266_SendCmd(char *cmd) u8 i,j; for(i=0;i<10;i++) //检测的次数--发送指令的次数 USART3_RX_FLAG=0; USART3_RX_CNT=0; USARTx_StringSend(USART3,cmd); delay_ms(200); if(USART3_RX_FLAG) USART3_RX_BUFFER[USART3_RX_CNT]='\0'; USART3_RX_FLAG=0; USART3_RX_CNT=0; if(strstr((char*)USART3_RX_BUFFER,"OK")) return 0; return 1; 函数功能: ESP8266硬件初始化检测函数 函数返回值:0表示成功 1表示失败 u8 ESP8266_Init(void) ESP8266_SendCmd("+++"); delay_ms(200); ESP8266_SendCmd("+++"); delay_ms(200); return ESP8266_SendCmd("AT\r\n"); 函数功能: 一键配置WIFI为AP+TCP服务器模式 函数参数: char *ssid 创建的热点名称 char *pass 创建的热点密码 (最少8位) u16 port 创建的服务器端口号 函数返回值: 0表示成功 其他值表示对应错误值 u8 ESP8266_AP_TCP_Server_Mode(char *ssid,char *pass,u16 port) char *p; u8 i; char tmp_buff[100]; /*1. 测试硬件*/ if(ESP8266_SendCmd("AT\r\n"))return 1; /*2. 关闭回显*/ if(ESP8266_SendCmd("ATE0\r\n"))return 2; /*3. 设置WIFI模式*/ if(ESP8266_SendCmd("AT+CWMODE=2\r\n"))return 3; /*4. 复位*/ ESP8266_SendCmd("AT+RST\r\n"); delay_ms(1000); delay_ms(1000); delay_ms(1000); /*5. 关闭回显*/ if(ESP8266_SendCmd("ATE0\r\n"))return 5; /*6. 设置WIFI的AP模式参数*/ sprintf(tmp_buff,"AT+CWSAP=\"%s\",\"%s\",1,4\r\n",ssid,pass); if(ESP8266_SendCmd(tmp_buff))return 6; /*7. 开启多连接*/ if(ESP8266_SendCmd("AT+CIPMUX=1\r\n"))return 7; /*8. 设置服务器端口号*/ sprintf(tmp_buff,"AT+CIPSERVER=1,%d\r\n",port); if(ESP8266_SendCmd(tmp_buff))return 8; /*9. 查询本地IP地址*/ if(ESP8266_SendCmd("AT+CIFSR\r\n"))return 9; //提取IP地址 p=strstr((char*)USART3_RX_BUFFER,"APIP"); if(p) p+=6; for(i=0;*p!='"';i++) ESP8266_IP_ADDR[i]=*p++; ESP8266_IP_ADDR[i]='\0'; //提取MAC地址 p=strstr((char*)USART3_RX_BUFFER,"APMAC"); if(p) p+=7; for(i=0;*p!='"';i++) ESP8266_MAC_ADDR[i]=*p++; ESP8266_MAC_ADDR[i]='\0'; //打印总体信息 printf("当前WIFI模式:AP+TCP服务器\n"); printf("当前WIFI热点名称:%s\n",ssid); printf("当前WIFI热点密码:%s\n",pass); printf("当前TCP服务器端口号:%d\n",port); printf("当前TCP服务器IP地址:%s\n",ESP8266_IP_ADDR); printf("当前TCP服务器MAC地址:%s\n",ESP8266_MAC_ADDR); return 0; 函数功能: TCP服务器模式下的发送函数 发送指令: u8 ESP8266_ServerSendData(u8 id,u8 *data,u16 len) u8 i,j,n; char ESP8266_SendCMD[100]; //组合发送过程中的命令 for(i=0;i<10;i++) sprintf(ESP8266_SendCMD,"AT+CIPSEND=%d,%d\r\n",id,len); USARTx_StringSend(USART3,ESP8266_SendCMD); delay_ms(200); if(USART3_RX_FLAG) USART3_RX_BUFFER[USART3_RX_CNT]='\0'; USART3_RX_FLAG=0; USART3_RX_CNT=0; if(strstr((char*)USART3_RX_BUFFER,">")) //继续发送数据 USARTx_DataSend(USART3,data,len); //等待数据发送成功 delay_ms(200); if(USART3_RX_FLAG) USART3_RX_BUFFER[USART3_RX_CNT]='\0'; USART3_RX_FLAG=0; USART3_RX_CNT=0; if(strstr((char*)USART3_RX_BUFFER,"SEND OK")) return 0; return 1; 4.4  QT软件端代码布局

基于STM32的录音机设计(STM32F103+VS1053B)

一、环境介绍MCU:  STM32F103C8T6开发软件:  Keil5音频模块:  VS1053B录音文件存储设备:  SD卡,采用SPI协议驱动显示屏:  SPI接口的0.96寸OLED代码风格:  采用寄存器编程,代码简洁、执行效率高、注释到位、移植方便。项目完整源代码下载地址(下载即可编译运行测试):  https://download.csdn.net/download/xiaolong1126626497/19520781二、功能介绍这是基于STM32F103C8T6设计的录音机功能,支持的功能如下:1.  按下按键1启动自动录音,默认为5秒录音一次,录音完毕自动保存在SD指定目录下。文件名称采用当前时间命名;音频文件格式采用WAV格式存储。2.  按下按键2启动手动录音,按键按下之后开始录音,再次按下结束录音,录音完毕之后,文件也是一样的保存在SD卡里。3. SD卡文件系统采用FAT32格式,STM32移植了FATFS开源文件系统对SD卡进行读写操作。4. OLED显示屏用于显示当前录音机的状态:  空闲、录音、回放等状态。5. 按下按键3,启动自动回放功能。自动扫描目录,按顺序播放录音文件。技术介绍:1.  SD卡采用SPI协议驱动,因为对速度没有很高要求,SPI协议已经完全满足;如果要更高的速度,可以采用SDIO协议。2.  音频模块采用VS1053B,这个芯片支持IIS和SPI协议。我这里采用的是SPI协议驱动,SPI比较简单,代码也好移植,可以很方便的移植到其他单片机上运行。VS1053功能比较强大,支持录音、解码播放。3.  文件系统采用的是FATFS文件系统,这个文件系统功能比较完善,使用免费,支持FAT16、FAT32等格式。底层也比较好适配移植。当前,除了FATFS以外,还有很多其他的嵌入式文件系统可以选择,移植都大同小异。4. OLED显示屏是0.96寸的。采用SPI协议驱动,主要是显示一些状态,SPI刷屏比较快,这款OLED也支持IIC接口。5. VS1053模块上没有喇叭设备,可以适应耳机或者音箱听回放的录音。硬件与STM32的接线说明:OLED显示屏:D0----SCK-----PB14D1----MOSI----PB13RES—复位(低电平有效)—PB12DC---数据和命令控制管脚—PB1CS---片选引脚-----PA7VS1053:#define VS1053_DREQ     PAin(11)      //DREQ  数据请求#define VS1053_RESET    PAout(12)   //RST   硬件复位--低电平有效#define VS1053_XCS      PAout(13)      //XCS   片选--低电平有效#define VS1053_XDCS     PAout(14)      //XDCS  用于数据片选、字节同步#define VS1053_SCLK     PAout(15)#define VS1053_OUTPUT   PBout(3)#define VS1053_INPUT    PBin(4)SD卡接口:5V----5VGND---GNDSPI1_MOSI---PA7SPI1_MISO---PA6SPI1_CS---PA4SPI1_SCK--PA5三、使用的相关硬件STM32F103C8T6系统板:  OLED显示屏: VS1053: SD卡卡槽: 四、操作说明开发板有一个复位键和一个K0按键程序下载:程序支持三种模式:因为开发板只有一个K0按键,所以三种模式都是通过一个按键进行切换的。一个按键是通过按下的计数方式进行切换的,切换的顺序是自动录音模式、手动录音模式、回放模式。(1)自动录音模式:按下一次按键后,进入自动录音模式,自动录音模式下,录音5秒自动退出,退出后自动启动播放状态,就是播放刚才5秒录制的音频,播放过程中按下按键可以退出播放状态。(2)手动录音模式:第二次按下K0按键后,进入手动录音模式,手动录音模式下,可以长时间录音,如果要结束录音,按下K0按键即可结束;结束后自动启动播放状态,就是播放刚才录制的音频,播放过程中按下按键可以退出播放状态。(3)回放模式:第三次按下K0按键后,进入回放模式,自动扫描wav目录,进行顺序播放音频文件。    播放过程中可以按下K0按键退出回放模式。每次录音后的文件是存放在SD卡根目录下的wav目录下。每个状态都会在OLED显示屏上显示也会同时通过串口打印到串口调试助手终端。五、SD卡上存放的文件SD卡上有两个目录:font目录和wav目录。font目录下存放16x16字库文件。wav目录下存放录音的音频文件。六、部分源码6.1 VS1053.c   这是VS1053的驱动代码#include "vs1053b.h" 函数功能:移植接口--SPI时序读写一个字节 函数参数:data:要写入的数据 返 回 值:读到的数据 u8 VS1053_SPI_ReadWriteByte(u8 tx_data) u8 rx_data=0; u8 i; for(i=0;i<8;i++) VS1053_SCLK=0; if(tx_data&0x80){VS1053_OUTPUT=1;} else {VS1053_OUTPUT=0;} tx_data<<=1; VS1053_SCLK=1; rx_data<<=1; if(VS1053_INPUT)rx_data|=0x01; return rx_data; 函数功能:初始化VS1053的IO口 void VS1053_Init(void) RCC->APB2ENR|=1<<0; AFIO->MAPR&=~(0x7<<24); //释放PA13/14/15 AFIO->MAPR|=0x4<<24; RCC->APB2ENR|=1<<2; RCC->APB2ENR|=1<<3; GPIOA->CRH&=0x00000FFF; GPIOA->CRH|=0x33338000; GPIOB->CRL&=0xFFF00FFF; GPIOB->CRL|=0x00083000; VS1053_SCLK=1; VS1053_XCS=1; VS1053_RESET=1; 函数功能:软复位VS10XX void VS1053_SoftReset(void) u8 retry=0; while(VS1053_DREQ==0); //等待软件复位结束 VS1053_SPI_ReadWriteByte(0Xff); //启动传输 retry=0; while(VS1053_ReadReg(SPI_MODE)!=0x0800) // 软件复位,新模式 VS1053_WriteCmd(SPI_MODE,0x0804); // 软件复位,新模式 DelayMs(2);//等待至少1.35ms if(retry++>100)break; while(VS1053_DREQ==0);//等待软件复位结束 retry=0; while(VS1053_ReadReg(SPI_CLOCKF)!=0X9800)//设置VS10XX的时钟,3倍频 ,1.5xADD VS1053_WriteCmd(SPI_CLOCKF,0X9800); //设置VS10XX的时钟,3倍频 ,1.5xADD if(retry++>100)break; DelayMs(20); 函数 功 能:硬复位MP3 函数返回值:1:复位失败;0:复位成功 u8 VS1053_Reset(void) u8 retry=0; VS1053_RESET=0; DelayMs(20); VS1053_XDCS=1;//取消数据传输 VS1053_XCS=1; //取消数据传输 VS1053_RESET=1; while(VS1053_DREQ==0&&retry<200)//等待DREQ为高 retry++; DelayUs(50); DelayMs(20); if(retry>=200)return 1; else return 0; 函数功能:向VS10XX写命令 函数参数: address:命令地址 data :命令数据 void VS1053_WriteCmd(u8 address,u16 data) while(VS1053_DREQ==0); //等待空闲 VS1053_XDCS=1; VS1053_XCS=0; VS1053_SPI_ReadWriteByte(VS_WRITE_COMMAND);//发送VS10XX的写命令 VS1053_SPI_ReadWriteByte(address); //地址 VS1053_SPI_ReadWriteByte(data>>8); //发送高八位 VS1053_SPI_ReadWriteByte(data); //第八位 VS1053_XCS=1; 函数参数:向VS1053写数据 函数参数:data:要写入的数据 void VS1053_WriteData(u8 data) VS1053_XDCS=0; VS1053_SPI_ReadWriteByte(data); VS1053_XDCS=1; 函数功能:读VS1053的寄存器 函数参数:address:寄存器地址 返回值:读到的值 u16 VS1053_ReadReg(u8 address) u16 temp=0; while(VS1053_DREQ==0);//非等待空闲状态 VS1053_XDCS=1; VS1053_XCS=0; VS1053_SPI_ReadWriteByte(VS_READ_COMMAND);//发送VS10XX的读命令 VS1053_SPI_ReadWriteByte(address); //地址 temp=VS1053_SPI_ReadWriteByte(0xff); //读取高字节 temp=temp<<8; temp+=VS1053_SPI_ReadWriteByte(0xff); //读取低字节 VS1053_XCS=1; return temp; 函数功能:读取VS1053的RAM 函数参数:addr:RAM地址 返 回 值:读到的值 u16 VS1053_ReadRAM(u16 addr) u16 res; VS1053_WriteCmd(SPI_WRAMADDR, addr); res=VS1053_ReadReg(SPI_WRAM); return res; 函数功能:写VS1053的RAM 函数参数: addr:RAM地址 val:要写入的值 void VS1053_WriteRAM(u16 addr,u16 val) VS1053_WriteCmd(SPI_WRAMADDR,addr); //写RAM地址 while(VS1053_DREQ==0); //等待空闲 VS1053_WriteCmd(SPI_WRAM,val); //写RAM值 函数参数:发送一次音频数据,固定为32字节 返 回 值:0,发送成功 1,本次数据未成功发送 u8 VS1053_SendMusicData(u8* buf) u8 n; if(VS1053_DREQ!=0) //送数据给VS10XX VS1053_XDCS=0; for(n=0;n<32;n++) VS1053_SPI_ReadWriteByte(buf[n]); VS1053_XDCS=1; }else return 1; return 0;//成功发送了 函数参数:发送一次音频数据,固定为32字节 返 回 值:0,发送成功 1,本次数据未成功发送 void VS1053_SendMusicByte(u8 data) u8 n; while(VS1053_DREQ==0){} VS1053_XDCS=0; VS1053_SPI_ReadWriteByte(data); VS1053_XDCS=1; 函数功能:设定VS1053播放的音量 函数参数:volx:音量大小(0~254) void VS1053_SetVol(u8 volx) u16 volt=0; //暂存音量值 volt=254-volx; //取反一下,得到最大值,表示最大的表示 volt<<=8; volt+=254-volx; //得到音量设置后大小 VS1053_WriteCmd(SPI_VOL,volt);//设音量 /*--------------------------------------录音功能-----------------------------------------------------*/ 函数功能:vs10xx装载patch 函数参数: patch:patch首地址 len :patch长度 void VS1053_LoadPatch(u16 *patch,u16 len) u16 i; u16 addr, n, val; for(i=0;i<len;) addr = patch[i++]; n = patch[i++]; if(n & 0x8000U) //RLE run, replicate n samples n &= 0x7FFF; val = patch[i++]; while(n--)VS1053_WriteCmd(addr, val); else //copy run, copy n sample while(n--) val = patch[i++]; VS1053_WriteCmd(addr, val); 函数参数:初始化WAV头 void VS1053_RecoderWavInit(__WaveHeader* wavhead) //初始化WAV头 wavhead->riff.ChunkID=0X46464952; //"RIFF" wavhead->riff.ChunkSize=0; //还未确定,最后需要计算 wavhead->riff.Format=0X45564157; //"WAVE" wavhead->fmt.ChunkID=0X20746D66; //"fmt " wavhead->fmt.ChunkSize=16; //大小为16个字节 wavhead->fmt.AudioFormat=0X01; //0X01,表示PCM;0X01,表示IMA ADPCM wavhead->fmt.NumOfChannels=1; //单声道 wavhead->fmt.SampleRate=8000; //8Khz采样率 采样速率 wavhead->fmt.ByteRate=wavhead->fmt.SampleRate*2;//16位,即2个字节 wavhead->fmt.BlockAlign=2; //块大小,2个字节为一个块 wavhead->fmt.BitsPerSample=16; //16位PCM wavhead->data.ChunkID=0X61746164; //"data" wavhead->data.ChunkSize=0; //数据大小,还需要计算 //VS1053的WAV录音有bug,这个plugin可以修正这个问题 const u16 VS1053_WavPlugin[40]=/* Compressed plugin */ 0x0007, 0x0001, 0x8010, 0x0006, 0x001c, 0x3e12, 0xb817, 0x3e14, /* 0 */ 0xf812, 0x3e01, 0xb811, 0x0007, 0x9717, 0x0020, 0xffd2, 0x0030, /* 8 */ 0x11d1, 0x3111, 0x8024, 0x3704, 0xc024, 0x3b81, 0x8024, 0x3101, /* 10 */ 0x8024, 0x3b81, 0x8024, 0x3f04, 0xc024, 0x2808, 0x4800, 0x36f1, /* 18 */ 0x9811, 0x0007, 0x0001, 0x8028, 0x0006, 0x0002, 0x2a00, 0x040e, 函数功能:激活PCM 录音模式 函数参数: agc:0,自动增益 1024相当于1倍 512相当于0.5倍 最大值65535=64倍 void VS1053_RecoderInit(u16 agc) //如果是IMA ADPCM,采样率计算公式如下: //采样率=CLKI/256*d; //假设d=0,并2倍频,外部晶振为12.288M.那么Fc=(2*12288000)/256*6=16Khz //如果是线性PCM,采样率直接就写采样值 VS1053_WriteCmd(SPI_BASS,0x0000); VS1053_WriteCmd(SPI_AICTRL0,8000); //设置采样率,设置为8Khz VS1053_WriteCmd(SPI_AICTRL1,agc); //设置增益,0,自动增益.1024相当于1倍,512相当于0.5倍,最大值65535=64倍 VS1053_WriteCmd(SPI_AICTRL2,0); //设置增益最大值,0,代表最大值65536=64X VS1053_WriteCmd(SPI_AICTRL3,6); //左通道(MIC单声道输入) VS1053_WriteCmd(SPI_CLOCKF,0X2000); //设置VS10XX的时钟,MULT:2倍频;ADD:不允许;CLK:12.288Mhz VS1053_WriteCmd(SPI_MODE,0x1804); //MIC,录音激活 DelayMs(5); //等待至少1.35ms VS1053_LoadPatch((u16*)VS1053_WavPlugin,40);//VS1053的WAV录音需要patch 6.2   SD.c  这是SD卡的驱动代码#include "sdcard.h" static u8 SD_Type=0; //存放SD卡的类型 函数功能:SD卡底层接口,通过SPI时序向SD卡读写一个字节 函数参数:data是要写入的数据 返 回 值:读到的数据 u8 SDCardReadWriteOneByte(u8 DataTx) u16 cnt=0; while((SPI1->SR&1<<1)==0) //等待发送区空--等待发送缓冲为空 cnt++; if(cnt>=65530)return 0; //超时退出 u16=2个字节 SPI1->DR=DataTx; //发送一个byte cnt=0; while((SPI1->SR&1<<0)==0) //等待接收完一个byte cnt++; if(cnt>=65530)return 0; //超时退出 return SPI1->DR; //返回收到的数据 函数功能:底层SD卡接口初始化 SPI1接口---SD卡接线原理 5V----5V GND---GND SPI1_MOSI---PA7 SPI1_MISO---PA6 SPI1_CS---PA4 SPI1_SCK--PA5 void SDCardSpiInit(void) /*1. 开启时钟*/ RCC->APB2ENR|=1<<2; //使能PORTA时钟 /*2. 配置GPIO口模式*/ GPIOA->CRL&=0x0000FFFF; GPIOA->CRL|=0xB8B30000; /*3. 上拉*/ GPIOA->ODR|=1<<4; /*SPI1基本配置*/ RCC->APB2ENR|=1<<12; //开启SPI1时钟 RCC->APB2RSTR|=1<<12; RCC->APB2RSTR&=~(1<<12); SPI1->CR1=0X0; //清空寄存器 SPI1->CR1|=0<<15; //选择“双线双向”模式 SPI1->CR1|=0<<11; //使用8位数据帧格式进行发送/接收; SPI1->CR1|=0<<10; //全双工(发送和接收); SPI1->CR1|=1<<9; //启用软件从设备管理 SPI1->CR1|=1<<8; //NSS SPI1->CR1|=0<<7; //帧格式,先发送高位 SPI1->CR1|=0x0<<3;//当总线频率为36MHZ时,SPI速度为18MHZ,高速。 SPI1->CR1|=1<<2; //配置为主设备 SPI1->CR1|=1<<1; //空闲状态时, SCK保持高电平。 SPI1->CR1|=1<<0; //数据采样从第二个时钟边沿开始。 SPI1->CR1|=1<<6; //开启SPI设备。 函数功能:取消选择,释放SPI总线 void SDCardCancelCS(void) SDCARD_CS=1; SDCardReadWriteOneByte(0xff);//提供额外的8个时钟 函数 功 能:选择sd卡,并且等待卡准备OK 函数返回值:0,成功;1,失败; u8 SDCardSelectCS(void) SDCARD_CS=0; if(SDCardWaitBusy()==0)return 0;//等待成功 SDCardCancelCS(); return 1;//等待失败 函数 功 能:等待卡准备好 函数返回值:0,准备好了;其他,错误代码 u8 SDCardWaitBusy(void) u32 t=0; if(SDCardReadWriteOneByte(0XFF)==0XFF)return 0;//OK t++; }while(t<0xFFFFFF);//等待 return 1; 函数功能:等待SD卡回应 函数参数: Response:要得到的回应值 返 回 值: 0,成功得到了该回应值 其他,得到回应值失败 u8 SDCardGetAck(u8 Response) u16 Count=0xFFFF;//等待次数 while((SDCardReadWriteOneByte(0XFF)!=Response)&&Count)Count--;//等待得到准确的回应 if(Count==0)return SDCard_RESPONSE_FAILURE;//得到回应失败 else return SDCard_RESPONSE_NO_ERROR;//正确回应 函数功能:从sd卡读取一个数据包的内容 函数参数: buf:数据缓存区 len:要读取的数据长度. 返回值: 0,成功;其他,失败; u8 SDCardRecvData(u8*buf,u16 len) if(SDCardGetAck(0xFE))return 1;//等待SD卡发回数据起始令牌0xFE while(len--)//开始接收数据 *buf=SDCardReadWriteOneByte(0xFF); buf++; //下面是2个伪CRC(dummy CRC) SDCardReadWriteOneByte(0xFF); SDCardReadWriteOneByte(0xFF); return 0;//读取成功 函数功能:向sd卡写入一个数据包的内容 512字节 函数参数: buf 数据缓存区 cmd 指令 返 回 值:0表示成功;其他值表示失败; u8 SDCardSendData(u8*buf,u8 cmd) u16 t; if(SDCardWaitBusy())return 1; //等待准备失效 SDCardReadWriteOneByte(cmd); if(cmd!=0XFD)//不是结束指令 for(t=0;t<512;t++)SDCardReadWriteOneByte(buf[t]);//提高速度,减少函数传参时间 SDCardReadWriteOneByte(0xFF); //忽略crc SDCardReadWriteOneByte(0xFF); t=SDCardReadWriteOneByte(0xFF); //接收响应 if((t&0x1F)!=0x05)return 2; //响应错误 return 0;//写入成功 函数功能:向SD卡发送一个命令 函数参数: u8 cmd 命令 u32 arg 命令参数 u8 crc crc校验值 返回值:SD卡返回的响应 u8 SendSDCardCmd(u8 cmd, u32 arg, u8 crc) u8 r1; u8 Retry=0; SDCardCancelCS(); //取消上次片选 if(SDCardSelectCS())return 0XFF;//片选失效 //发送数据 SDCardReadWriteOneByte(cmd | 0x40);//分别写入命令 SDCardReadWriteOneByte(arg >> 24); SDCardReadWriteOneByte(arg >> 16); SDCardReadWriteOneByte(arg >> 8); SDCardReadWriteOneByte(arg); SDCardReadWriteOneByte(crc); if(cmd==SDCard_CMD12)SDCardReadWriteOneByte(0xff);//Skip a stuff byte when stop reading Retry=0X1F; r1=SDCardReadWriteOneByte(0xFF); }while((r1&0X80) && Retry--); //等待响应,或超时退出 return r1; //返回状态值 函数功能:获取SD卡的CID信息,包括制造商信息 函数参数:u8 *cid_data(存放CID的内存,至少16Byte) 返 回 值: 0:成功,1:错误 u8 GetSDCardCISDCardOutnfo(u8 *cid_data) u8 r1; //发SDCard_CMD10命令,读CID r1=SendSDCardCmd(SDCard_CMD10,0,0x01); if(r1==0x00) r1=SDCardRecvData(cid_data,16);//接收16个字节的数据 SDCardCancelCS();//取消片选 if(r1)return 1; else return 0; 函数说明: 获取SD卡的CSD信息,包括容量和速度信息 函数参数: u8 *cid_data(存放CID的内存,至少16Byte) 返 回 值: 0:成功,1:错误 u8 GetSDCardCSSDCardOutnfo(u8 *csd_data) u8 r1; r1=SendSDCardCmd(SDCard_CMD9,0,0x01); //发SDCard_CMD9命令,读CSD if(r1==0) r1=SDCardRecvData(csd_data, 16);//接收16个字节的数据 SDCardCancelCS();//取消片选 if(r1)return 1; else return 0; 函数功能:获取SD卡的总扇区数(扇区数) 返 回 值: 0表示容量检测出错,其他值表示SD卡的容量(扇区数/512字节) 说 明: 每扇区的字节数必为512字节,如果不是512字节,则初始化不能通过. u32 GetSDCardSectorCount(void) u8 csd[16]; u32 Capacity; u16 csize; if(GetSDCardCSSDCardOutnfo(csd)!=0) return 0; //取CSD信息,如果期间出错,返回0 if((csd[0]&0xC0)==0x40) //SDHC卡,按照下面方式计算 csize = csd[9] + ((u16)csd[8] << 8) + 1; Capacity = (u32)csize << 10;//得到扇区数 return Capacity; 函数功能: 初始化SD卡 返 回 值: 非0表示初始化失败! u8 SDCardDeviceInit(void) u8 r1; // 存放SD卡的返回值 u16 retry; // 用来进行超时计数 u8 buf[4]; u16 i; SDCardSpiInit(); //初始化底层IO口 for(i=0;i<10;i++)SDCardReadWriteOneByte(0XFF); //发送最少74个脉冲 retry=20; r1=SendSDCardCmd(SDCard_CMD0,0,0x95);//进入IDLE状态 闲置 }while((r1!=0X01) && retry--); SD_Type=0; //默认无卡 if(r1==0X01) if(SendSDCardCmd(SDCard_CMD8,0x1AA,0x87)==1) //SD V2.0 for(i=0;i<4;i++)buf[i]=SDCardReadWriteOneByte(0XFF); if(buf[2]==0X01&&buf[3]==0XAA) //卡是否支持2.7~3.6V retry=0XFFFE; SendSDCardCmd(SDCard_CMD55,0,0X01); //发送SDCard_CMD55 r1=SendSDCardCmd(SDCard_CMD41,0x40000000,0X01);//发送SDCard_CMD41 }while(r1&&retry--); if(retry&&SendSDCardCmd(SDCard_CMD58,0,0X01)==0)//鉴别SD2.0卡版本开始 for(i=0;i<4;i++)buf[i]=SDCardReadWriteOneByte(0XFF);//得到OCR值 if(buf[0]&0x40)SD_Type=SDCard_TYPE_V2HC; //检查CCS else SD_Type=SDCard_TYPE_V2; SDCardCancelCS(); //取消片选 if(SD_Type)return 0; //初始化成功返回0 else if(r1)return r1; //返回值错误值 return 0xaa; //其他错误 函数功能:读SD卡 函数参数: buf:数据缓存区 sector:扇区 cnt:扇区数 0,ok;其他,失败. 说 明: SD卡一个扇区大小512字节 u8 SDCardReadData(u8*buf,u32 sector,u32 cnt) u8 r1; if(SD_Type!=SDCard_TYPE_V2HC)sector<<=9;//转换为字节地址 if(cnt==1) r1=SendSDCardCmd(SDCard_CMD17,sector,0X01);//读命令 if(r1==0) //指令发送成功 r1=SDCardRecvData(buf,512); //接收512个字节 }else r1=SendSDCardCmd(SDCard_CMD18,sector,0X01);//连续读命令 r1=SDCardRecvData(buf,512);//接收512个字节 buf+=512; }while(--cnt && r1==0); SendSDCardCmd(SDCard_CMD12,0,0X01); //发送停止命令 SDCardCancelCS();//取消片选 return r1;// 函数功能:向SD卡写数据 函数参数: buf:数据缓存区 sector:起始扇区 cnt:扇区数 返回值: 0,ok;其他,失败. 说 明: SD卡一个扇区大小512字节 u8 SDCardWriteData(u8*buf,u32 sector,u32 cnt) u8 r1; if(SD_Type!=SDCard_TYPE_V2HC)sector *= 512;//转换为字节地址 if(cnt==1) r1=SendSDCardCmd(SDCard_CMD24,sector,0X01);//读命令 if(r1==0)//指令发送成功 r1=SDCardSendData(buf,0xFE);//写512个字节 if(SD_Type!=SDCard_TYPE_MMC) SendSDCardCmd(SDCard_CMD55,0,0X01); SendSDCardCmd(SDCard_CMD23,cnt,0X01);//发送指令 r1=SendSDCardCmd(SDCard_CMD25,sector,0X01);//连续读命令 if(r1==0) r1=SDCardSendData(buf,0xFC);//接收512个字节 buf+=512; }while(--cnt && r1==0); r1=SDCardSendData(0,0xFD);//接收512个字节 SDCardCancelCS();//取消片选 return r1;//

STM32入门开发: 介绍SPI总线、读写W25Q64(FLASH)(硬件+模拟时序)

一、环境介绍编程软件: keil5操作系统: win10MCU型号: STM32F103ZET6STM32编程方式: 寄存器开发 (方便程序移植到其他单片机)SPI总线:  STM32本身支持SPI硬件时序,本文示例代码里同时采用模拟时序和硬件时序两种方式读写W25Q64。模拟时序更加方便移植到其他单片机,更加方便学习理解SPI时序,通用性更高,不分MCU;硬件时序效率更高,每个MCU配置方法不同,依赖MCU硬件本身支持。存储器件: 采用华邦W25Q64  flash存储芯片。 W25Q64这类似的Flash存储芯片在单片机里、嵌入式系统里还是比较常见,可以用来存储图片数据、字库数据、音频数据、保存设备运行日志文件等。完整工程代码下载:https://download.csdn.net/download/xiaolong1126626497/19425042二、华邦W25Q64介绍(FLASH存储类型)2.1 W25Q64芯片功能介绍W25Q64是为系统提供一个最小空间、最少引脚,最低功耗的串行Flash存储器,25Q系列比普通的串行Flash存储器更灵活,性能更优越。W25Q64支持双倍/四倍的SPI,可以储存包括声音、文本、图片和其他数据;芯片支持的工作电压 2.7V 到 3.6V,正常工作时电流小于5mA,掉电时低于1uA,所有芯片提供标准的封装。 W25Q64的内存空间结构:  一页256字节,4K(4096 字节)为一个扇区,16个扇区为1块,容量为8M字节,共有128个块,2048 个扇区。  W25Q64每页大小由256字节组成,每页的256字节用一次页编程指令即可完成。擦除指令分别支持: 16页(1个扇区)、128页、256页、全片擦除。 W25Q64支持标准串行外围接口(SPI),和高速的双倍/四倍输出,双倍/四倍用的引脚:串行时钟、片选端、串行数据 I/O0(DI)、I/O1(DO)、I/O2(WP)和 I/O3(HOLD)。SPI 最高支持 80MHz,当用快读双倍/四倍指令时,相当于双倍输出时最高速率160MHz,四倍输出时最高速率 320MHz。这个传输速率比得上8位和16位的并行Flash存储器。W25Q64支持 JEDEC 标准,具有唯一的 64 位识别序列号,方便区别芯片型号。 2.2 W25Q64芯片特性详细介绍●SPI串行存储器系列    -W25Q64:64M 位/8M 字节    -W25Q16:16M 位/2M 字节    -W25Q32:32M 位/4M 字节    -每 256 字节可编程页       ●灵活的4KB扇区结构     -统一的扇区擦除(4K 字节)     -块擦除(32K 和 64K 字节) -一次编程 256 字节 -至少 100,000 写/擦除周期 -数据保存 20 年    ●标准、双倍和四倍SPI -标准 SPI:CLK、CS、DI、DO、WP、HOLD         -双倍 SPI:CLK、CS、IO0、IO1、WP、HOLD         -四倍 SPI:CLK、CS、IO0、IO1、IO2、IO3 ●高级的安全特点 -软件和硬件写保护 -选择扇区和块保护 -一次性编程保护(1) -每个设备具有唯一的64位ID(1) ●高性能串行Flash存储器     -比普通串行Flash性能高6倍          -80MHz时钟频率          -双倍SPI相当于160MHz         -四倍SPI相当于320MHz         -40MB/S连续传输数据     -30MB/S随机存取(每32字节)     -比得上16位并行存储器       ●低功耗、宽温度范围 -单电源 2.7V-3.6V -工作电流 4mA,掉电<1μA(典型值)-40℃~+85℃工作 2.3  引脚介绍下面只介绍W25Q64标准SPI接口,因为目前开发板上的封装使用的就是标准SPI接口。 2.2.1 SPI片选(/CS)引脚用于使能和禁止芯片操作CS引脚是W25Q64的片选引脚,用于选中芯片;当CS为高电平时,芯片未被选择,串行数据输出(DO、IO0、IO1、IO2 和 IO3)引脚为高阻态。未被选择时,芯片处于待机状态下的低功耗,除非芯片内部在擦除、编程。当/CS 变成低电平,芯片功耗将增长到正常工作,能够从芯片读写数据。上电后, 在接收新的指令前,/CS 必须由高变为低电平。上电后,/CS 必须上升到 VCC,在/CS 接上拉电阻可以完成这个操作。 2.2.2 串行数据输入、输出和 IOs(DI、DO 和 IO0、IO1、IO2、IO3)W25Q64、W25Q16 和 W25Q32 支持标准 SPI、双倍 SPI 和四倍 SPI。标准的 SPI 传输用单向的 DI(输入)引脚连续的写命令、地址或者数据在串行时钟(CLK)的上升沿时写入到芯片内。标准的SPI 用单向的 DO(输出)在 CLK 的下降沿从芯片内读出数据或状态。 2.2.3 写保护(/WP)写保护引脚(/WP)用来保护状态寄存器。和状态寄存器的块保护位(SEC、TB、BP2、BP1 和BP0)和状态寄存器保护位(SRP)对存储器进行一部分或者全部的硬件保护。/WP 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/WP 引脚(硬件写保护)的功能不可用。2.2.4  保持端(/HOLD)当/HOLD 引脚是有效时,允许芯片暂停工作。在/CS 为低电平时,当/HOLD 变为低电平,DO 引脚将变为高阻态,在 DI 和 CLK 引脚上的信号将无效。当/HOLD 变为高电平,芯片恢复工作。/HOLD 功能用在当有多个设备共享同一 SPI 总线时。/HOLD 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/ HOLD 引脚的功能不可用。2.2.5 串行时钟(CLK)串行时钟输入引脚为串行输入和输出操作提供时序。(见 SPI 操作)。设备数据传输是从高位开始,数据传输的格式为 8bit,数据采样从第二个时间边沿开始,空闲状态时,时钟线 clk 为高电平。 2.3 内部结构框架图2.4 W25Q64的标准SPI操作流程W25Q64标准SPI总线接口包含四个信号: 串行时钟(CLK)、片选端(/CS)、串行数据输入(DI)和串行数据输出(DO)。DI输入引脚在CLK的上升沿连续写命令、地址或数据到芯片内。DO输出引脚在CLK的下降沿从芯片内读出数据或状态。W25Q64分别支持SPI总线工作模式0和工作模式3。模式0和模式3的主要区别在于常态时的CLK信号不同;对于模式0来说,当SPI主机已准备好数据还没传输到串行Flash中时,CLK信号常态为低;设备数据传输是从高位开始,数据传输的格式为8bit,数据采样从第二个时间边沿开始,空闲状态时,时钟线clk为高电平。2.5 部分控制和状态寄存器介绍2.5.1 W25Q64的指令表2.5.2 读状态寄存器1状态寄存器1的内部结构如下:状态寄存器1的S0位是当前W25Q64的忙状态;为1的时候表示设备正在执行程序(可能是在擦除芯片)或写状态寄存器指令,这个时候设备将忽略传来的指令, 除了读状态寄存器和擦除暂停指令外,其他写指令或写状态指令都无效,  当 S0 为 0 状态时指示设备已经执行完毕,可以进行下一步操作。 读状态寄存器1的时序如下:读取状态寄存器的指令是 8 位的指令。发送指令之前,先将/CS 拉低,再发送指令码“05 h” 或者“35h”。设备收到读取状态寄存器的指令后,将状态信息(高位)依次移位发送出去,读出的状态信息,最低位为 1 代表忙,最低位为 0 代表可以操作,状态信息读取完毕,将片选线拉高。 读状态寄存器指令可以使用在任何时候,即使程序在擦除的过程中或者写状态寄存器周期正在进行中。这可以检测忙碌状态来确定周期是否完成,以确定设备是否可以接受另一个指令。2.5.3 读制造商ID和芯片ID 时序图如下:读取制造商/设备 ID 指令可以读取制造商 ID 和特定的设备 ID。读取之前,拉低 CS 片选信号,接着发送指令代码“90h” ,紧随其后的是一个 24 位地址(A23-A0)000000h。 设备收到指令之后,会发出华邦电子制造商 ID(EFh) 和设备ID(w25q64 为 16h)。如果 24 位地址设置为 000001h ,设备 ID 会先发出,然后跟着制造商 ID。制造商和设备ID可以连续读取。完成指令后,片选信号/ CS 拉高。2.5.4 全片擦除(C7h/60h)全芯片擦除指令,可以将整个芯片的所有内存数据擦除,恢复到 0XFF 状态。写入全芯片擦除指令之前必须执行设备写使能(发送设备写使能指令 0x06),并判断状态寄存器(状态寄存器位最低位必须等于 0 才能操作)。发送全芯片擦除指令前,先拉低/ CS,接着发送擦除指令码”C7h”或者是”60h”, 指令码发送完毕后,拉高片选线 CS/,,并判断状态位,等待擦除结束。全片擦除指令尽量少用,擦除会缩短设备的寿命。2.5.5 读数据(03h)读取数据指令允许按顺序读取一个字节的内存数据。当片选 CS/拉低之后,紧随其后是一个 24 位的地址(A23-A0)(需要发送 3 次,每次 8 个字节,先发高位)。芯片收到地址后,将要读的数据按字节大小转移出去,数据是先转移高位,对于单片机,时钟下降沿发送数据,上升沿接收数据。读数据时,地址会自动增加,允许连续的读取数据。这意味着读取整个内存的数据,只要用一个指令就可以读完。数据读取完成之后,片选信号/ CS 拉高。读取数据的指令序列,如上图所示。如果一个读数据指令而发出的时候,设备正在擦除扇区,或者(忙= 1),该读指令将被忽略,也不会对当前周期有什么影响。三、SPI时序介绍SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间。SPI是一种高速、高效率的串行接口技术,一共有4根线。通常由一个主模块和一个或多个从模块组成,主模块选择一个从模块进行同步通信,从而完成数据的交换。SPI是一个环形结构,通信时需要至少4根线(在单向传输时3根线也可以)。分别是MISO(主设备数据输入)、MOSI(主设备数据输出)、SCLK(时钟)、CS(片选)。(1)MISO– Master Input Slave Output,主设备数据输入,从设备数据输出;(2)MOSI– Master Output Slave Input,主设备数据输出,从设备数据输入;(3)SCLK – Serial Clock,时钟信号,由主设备产生;(4)CS – Chip Select,从设备使能信号,由主设备控制。其中,CS是从芯片是否被主芯片选中的控制信号,也就是说只有片选信号为预先规定的使能信号时(高电位或低电位),主芯片对此从芯片的操作才有效。这就使在同一条总线上连接多个SPI设备成为可能。接下来就负责通讯的3根线了。通讯是通过数据交换完成的,这里先要知道SPI是串行通讯协议,也就是说数据是一位一位的传输的。这就是SCLK时钟线存在的原因,由SCLK提供时钟脉冲,SDI,SDO则基于此脉冲完成数据传输。数据输出通过 SDO线,数据在时钟上升沿或下降沿时改变,在紧接着的下降沿或上升沿被读取。完成一位数据传输,输入也使用同样原理。因此,至少需要8次时钟信号的改变(上沿和下沿为一次),才能完成8位数据的传输。时钟信号线SCLK只能由主设备控制,从设备不能控制。这样的传输方式有一个优点,在数据位的传输过程中可以暂停,也就是时钟的周期可以为不等宽,因为时钟线由主设备控制,当没有时钟跳变时,从设备不采集或传送数据。SPI还是一个数据交换协议:因为SPI的数据输入和输出线独立,所以允许同时完成数据的输入和输出。芯片集成的SPI串行同步时钟极性和相位可以通过寄存器配置,IO模拟的SPI串行同步时钟需要根据从设备支持的时钟极性和相位来通讯。SPI通信原理比I2C要简单,IIC有应答机制,可以确保数据都全部发送成。SPI接口没有指定的流控制,没有应答机制确认是否接收到数据,速度上更加快。SPI总线通过时钟极性和相位可以配置成4种时序:STM32F103参考手册,SPI章节介绍的时序图:SPI时序比较简单,CPU如果没有硬件支持,可以直接写代码采用IO口模拟,下面是模拟时序的示例的代码:SPI的模式1: u8 SPI_ReadWriteOneByte(u8 tx_data) u8 i,rx_data=0; SCK=0; //空闲电平(默认初始化情况) for(i=0;i<8;i++) /*1. 主机发送一位数据*/ SCK=0;//告诉从机,主机将要发送数据 if(tx_data&0x80)MOSI=1; //发送数据 else MOSI=0; SCK=1; //告诉从机,主机数据发送完毕 tx_data<<=1; //继续发送下一位 /*2. 主机接收一位数据*/ rx_data<<=1; //默认认为接收到0 if(MISO)rx_data|=0x01; SCK=0; //恢复空闲电平 return rx_data; SPI的模式2: u8 SPI_ReadWriteOneByte(u8 tx_data) u8 i,rx_data=0; SCK=0; //空闲电平(默认初始化情况) for(i=0;i<8;i++) /*1. 主机发送一位数据*/ SCK=1;//告诉从机,主机将要发送数据 if(tx_data&0x80)MOSI=1; //发送数据 else MOSI=0; SCK=0; //告诉从机,主机数据发送完毕 tx_data<<=1; //继续发送下一位 /*2. 主机接收一位数据*/ rx_data<<=1; //默认认为接收到0 if(MISO)rx_data|=0x01; SCK=0; //恢复空闲电平 return rx_data; SPI的模式3: u8 SPI_ReadWriteOneByte(u8 tx_data) u8 i,rx_data=0; SCK=1; //空闲电平(默认初始化情况) for(i=0;i<8;i++) /*1. 主机发送一位数据*/ SCK=1;//告诉从机,主机将要发送数据 if(tx_data&0x80)MOSI=1; //发送数据 else MOSI=0; SCK=0; //告诉从机,主机数据发送完毕 tx_data<<=1; //继续发送下一位 /*2. 主机接收一位数据*/ rx_data<<=1; //默认认为接收到0 if(MISO)rx_data|=0x01; SCK=1; //恢复空闲电平 return rx_data; SPI的模式4: u8 SPI_ReadWriteOneByte(u8 tx_data) u8 i,rx_data=0; SCK=1; //空闲电平(默认初始化情况) for(i=0;i<8;i++) /*1. 主机发送一位数据*/ SCK=0;//告诉从机,主机将要发送数据 if(tx_data&0x80)MOSI=1; //发送数据 else MOSI=0; SCK=1; //告诉从机,主机数据发送完毕 tx_data<<=1; //继续发送下一位 /*2. 主机接收一位数据*/ rx_data<<=1; //默认认为接收到0 if(MISO)rx_data|=0x01; SCK=1; //恢复空闲电平 return rx_data; }四、W25Q64的示例代码4.1 STM32采用硬件SPI读写W25Q64示例代码/* 函数功能:SPI初始化(模拟SPI) 硬件连接: MISO--->PB14 MOSI--->PB15 SCLK--->PB13 void SPI_Init(void) /*开启时钟*/ RCC->APB1ENR|=1<<14; //开启SPI2时钟 RCC->APB2ENR|=1<<3; //PB GPIOB->CRH&=0X000FFFFF; //清除寄存器 GPIOB->CRH|=0XB8B00000; GPIOB->ODR|=0X7<<13; //PB13/14/15上拉--输出高电平 /*SPI2基本配置*/ SPI2->CR1=0X0; //清空寄存器 SPI2->CR1|=0<<15; //选择“双线双向”模式 SPI2->CR1|=0<<11; //使用8位数据帧格式进行发送/接收; SPI2->CR1|=0<<10; //全双工(发送和接收); SPI2->CR1|=1<<9; //启用软件从设备管理 SPI2->CR1|=1<<8; //NSS SPI2->CR1|=0<<7; //帧格式,先发送高位 SPI2->CR1|=0x0<<3;//当总线频率为36MHZ时,SPI速度为18MHZ,高速。 SPI2->CR1|=1<<2; //配置为主设备 SPI2->CR1|=1<<1; //空闲状态时, SCK保持高电平。 SPI2->CR1|=1<<0; //数据采样从第二个时钟边沿开始。 SPI2->CR1|=1<<6; //开启SPI设备。 函数功能:SPI读写一个字节 u8 SPI_ReadWriteOneByte(u8 data_tx) u16 cnt=0; while((SPI2->SR&1<<1)==0) //等待发送区空--等待发送缓冲为空 cnt++; if(cnt>=65530)return 0; //超时退出 u16=2个字节 SPI2->DR=data_tx; //发送一个byte cnt=0; while((SPI2->SR&1<<0)==0) //等待接收完一个byte cnt++; if(cnt>=65530)return 0; //超时退出 return SPI2->DR; //返回收到的数据 函数功能:W25Q64初始化 硬件连接: MOSI--->PB15 MISO--->PB14 SCLK--->PB13 CS----->PB12 void W25Q64_Init(void) /*1. 开时钟*/ RCC->APB2ENR|=1<<3; //PB /*2. 配置GPIO口模式*/ GPIOB->CRH&=0xFFF0FFFF; GPIOB->CRH|=0x00030000; W25Q64_CS=1; //未选中芯片 SPI_Init(); //SPI初始化 函数功能:读取芯片的ID号 u16 W25Q64_ReadID(void) u16 id; /*1. 拉低片选*/ W25Q64_CS=0; /*2. 发送读取ID的指令*/ SPI_ReadWriteOneByte(0x90); /*3. 发送24位的地址-0*/ SPI_ReadWriteOneByte(0); SPI_ReadWriteOneByte(0); SPI_ReadWriteOneByte(0); /*4. 读取芯片的ID*/ id=SPI_ReadWriteOneByte(0xFF)<<8; id|=SPI_ReadWriteOneByte(0xFF); /*5. 拉高片选*/ W25Q64_CS=1; return id; 函数功能:检测W25Q64状态 void W25Q64_CheckStat(void) u8 stat=1; while(stat&1<<0) W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x05); //发送读状态寄存器1指令 stat=SPI_ReadWriteOneByte(0xFF); //读取状态 W25Q64_CS=1; //取消选中芯片 函数功能:页编程 说 明:一页最多写256个字节。 写数据之前,必须保证空间是0xFF 函数参数: u32 addr:页编程起始地址 u8 *buff:写入的数据缓冲区 u16 len :写入的字节长度 void W25Q64_PageWrite(u32 addr,u8 *buff,u16 len) u16 i; W25Q64_Enabled(); //写使能 W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x02); //页编程指令 SPI_ReadWriteOneByte(addr>>16); //24~16地址 SPI_ReadWriteOneByte(addr>>8); //16~8地址 SPI_ReadWriteOneByte(addr); //8~0地址 for(i=0;i<len;i++) SPI_ReadWriteOneByte(buff[i]); //8~0地址 W25Q64_CS=1; //取消选中芯片 W25Q64_CheckStat(); //检测芯片忙状态 函数功能:连续读数据 函数参数: u32 addr:读取数据的起始地址 u8 *buff:读取数据存放的缓冲区 u32 len :读取字节的长度 void W25Q64_ReadByteData(u32 addr,u8 *buff,u32 len) u32 i; W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x03); //读数据指令 SPI_ReadWriteOneByte(addr>>16); //24~16地址 SPI_ReadWriteOneByte(addr>>8); //16~8地址 SPI_ReadWriteOneByte(addr); //8~0地址 for(i=0;i<len;i++)buff[i]=SPI_ReadWriteOneByte(0xFF); W25Q64_CS=1; //取消选中芯片 函数功能:擦除一个扇区 函数参数: u32 addr:擦除扇区的地址范围 void W25Q64_ClearSector(u32 addr) W25Q64_Enabled(); //写使能 W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x20); //扇区擦除指令 SPI_ReadWriteOneByte(addr>>16); //24~16地址 SPI_ReadWriteOneByte(addr>>8); //16~8地址 SPI_ReadWriteOneByte(addr); //8~0地址 W25Q64_CS=1; //取消选中芯片 W25Q64_CheckStat(); //检测芯片忙状态 函数功能:写使能 void W25Q64_Enabled(void) W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x06); //写使能 W25Q64_CS=1; //取消选中芯片 函数功能:指定位置写入指定个数的数据,不考虑擦除问题 注意事项:W25Q64只能将1写为,不能将0写为1。 函数参数: u32 addr---写入数据的起始地址 u8 *buff---写入的数据 u32 len---长度 void W25Q64_WriteByteDataNoCheck(u32 addr,u8 *buff,u32 len) u32 page_remain=256-addr%256; //计算当前页还可以写下多少数据 if(len<=page_remain) //如果当前写入的字节长度小于剩余的长度 page_remain=len; while(1) W25Q64_PageWrite(addr,buff,page_remain); if(page_remain==len)break; //表明数据已经写入完毕 buff+=page_remain; //buff向后偏移地址 addr+=page_remain; //起始地址向后偏移 len-=page_remain; //减去已经写入的字节数 if(len>256)page_remain=256; //如果大于一页,每次就直接写256字节 else page_remain=len; 函数功能:指定位置写入指定个数的数据,考虑擦除问题,完善代码 函数参数: u32 addr---写入数据的起始地址 u8 *buff---写入的数据 u32 len---长度 说明:擦除的最小单位扇区,4096字节 static u8 W25Q64_READ_WRITE_CHECK_BUFF[4096]; void W25Q64_WriteByteData(u32 addr,u8 *buff,u32 len) u32 i; u32 len_w; u32 sector_addr; //存放扇区的地址 u32 sector_move; //扇区向后偏移的地址 u32 sector_size; //扇区大小。(剩余的空间大小) u8 *p=W25Q64_READ_WRITE_CHECK_BUFF;//存放指针 sector_addr=addr/4096; //传入的地址是处于第几个扇区 sector_move=addr%4096; //计算传入的地址存于当前的扇区的偏移量位置 sector_size=4096-sector_move; //得到当前扇区剩余的空间 if(len<=sector_size) sector_size=len; //判断第一种可能性、一次可以写完 while(1) W25Q64_ReadByteData(addr,p,sector_size); //读取剩余扇区里的数据 for(i=0;i<sector_size;i++) if(p[i]!=0xFF)break; if(i!=sector_size) //判断是否需要擦除 W25Q64_ClearSector(sector_addr*4096); // for(i=0;i<len;i++) // { // W25Q64_READ_WRITE_CHECK_BUFF[i]=buff[len_w++]; // W25Q64_WriteByteDataNoCheck(addr,W25Q64_READ_WRITE_CHECK_BUFF,sector_size); W25Q64_WriteByteDataNoCheck(addr,buff,sector_size); if(sector_size==len)break; addr+=sector_size; //向后偏移地址 buff+=sector_size ;//向后偏移 len-=sector_size; //减去已经写入的数据 sector_addr++; //校验第下个扇区 if(len>4096) //表明还可以写一个扇区 sector_size=4096;//继续写一个扇区 sector_size=len; //剩余的空间可以写完 }4.2 STM32采用硬件SPI读写W25Q64示例代码#include "spi.h" 函数功能:SPI初始化(模拟SPI) 硬件连接: MISO--->PB14 MOSI--->PB15 SCLK--->PB13 void SPI_Init(void) /*1. 开时钟*/ RCC->APB2ENR|=1<<3; //PB /*2. 配置GPIO口模式*/ GPIOB->CRH&=0x000FFFFF; GPIOB->CRH|=0x38300000; /*3. 上拉*/ SPI_MOSI=1; SPI_MISO=1; SPI_SCLK=1; 函数功能:SPI读写一个字节 u8 SPI_ReadWriteOneByte(u8 data_tx) u8 data_rx=0; //存放读取的数据 u8 i; for(i=0;i<8;i++) SPI_SCLK=0; //准备发送数据 if(data_tx&0x80)SPI_MOSI=1; else SPI_MOSI=0; data_tx<<=1; //依次发送最高位 SPI_SCLK=1; //表示主机数据发送完成,表示从机发送完毕 data_rx<<=1; //表示默认接收的是0 if(SPI_MISO)data_rx|=0x01; return data_rx; #include "W25Q64.h" 函数功能:W25Q64初始化 硬件连接: MOSI--->PB15 MISO--->PB14 SCLK--->PB13 CS----->PB12 void W25Q64_Init(void) /*1. 开时钟*/ RCC->APB2ENR|=1<<3; //PB /*2. 配置GPIO口模式*/ GPIOB->CRH&=0xFFF0FFFF; GPIOB->CRH|=0x00030000; W25Q64_CS=1; //未选中芯片 SPI_Init(); //SPI初始化 函数功能:读取芯片的ID号 u16 W25Q64_ReadID(void) u16 id; /*1. 拉低片选*/ W25Q64_CS=0; /*2. 发送读取ID的指令*/ SPI_ReadWriteOneByte(0x90); /*3. 发送24位的地址-0*/ SPI_ReadWriteOneByte(0); SPI_ReadWriteOneByte(0); SPI_ReadWriteOneByte(0); /*4. 读取芯片的ID*/ id=SPI_ReadWriteOneByte(0xFF)<<8; id|=SPI_ReadWriteOneByte(0xFF); /*5. 拉高片选*/ W25Q64_CS=1; return id; 函数功能:检测W25Q64状态 void W25Q64_CheckStat(void) u8 stat=1; while(stat&1<<0) W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x05); //发送读状态寄存器1指令 stat=SPI_ReadWriteOneByte(0xFF); //读取状态 W25Q64_CS=1; //取消选中芯片 函数功能:页编程 说 明:一页最多写256个字节。 写数据之前,必须保证空间是0xFF 函数参数: u32 addr:页编程起始地址 u8 *buff:写入的数据缓冲区 u16 len :写入的字节长度 void W25Q64_PageWrite(u32 addr,u8 *buff,u16 len) u16 i; W25Q64_Enabled(); //写使能 W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x02); //页编程指令 SPI_ReadWriteOneByte(addr>>16); //24~16地址 SPI_ReadWriteOneByte(addr>>8); //16~8地址 SPI_ReadWriteOneByte(addr); //8~0地址 for(i=0;i<len;i++) SPI_ReadWriteOneByte(buff[i]); //8~0地址 W25Q64_CS=1; //取消选中芯片 W25Q64_CheckStat(); //检测芯片忙状态 函数功能:连续读数据 函数参数: u32 addr:读取数据的起始地址 u8 *buff:读取数据存放的缓冲区 u32 len :读取字节的长度 void W25Q64_ReadByteData(u32 addr,u8 *buff,u32 len) u32 i; W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x03); //读数据指令 SPI_ReadWriteOneByte(addr>>16); //24~16地址 SPI_ReadWriteOneByte(addr>>8); //16~8地址 SPI_ReadWriteOneByte(addr); //8~0地址 for(i=0;i<len;i++)buff[i]=SPI_ReadWriteOneByte(0xFF); W25Q64_CS=1; //取消选中芯片 函数功能:擦除一个扇区 函数参数: u32 addr:擦除扇区的地址范围 void W25Q64_ClearSector(u32 addr) W25Q64_Enabled(); //写使能 W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x20); //扇区擦除指令 SPI_ReadWriteOneByte(addr>>16); //24~16地址 SPI_ReadWriteOneByte(addr>>8); //16~8地址 SPI_ReadWriteOneByte(addr); //8~0地址 W25Q64_CS=1; //取消选中芯片 W25Q64_CheckStat(); //检测芯片忙状态 函数功能:写使能 void W25Q64_Enabled(void) W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x06); //写使能 W25Q64_CS=1; //取消选中芯片 函数功能:指定位置写入指定个数的数据,不考虑擦除问题 注意事项:W25Q64只能将1写为,不能将0写为1。 函数参数: u32 addr---写入数据的起始地址 u8 *buff---写入的数据 u32 len---长度 void W25Q64_WriteByteDataNoCheck(u32 addr,u8 *buff,u32 len) u32 page_remain=256-addr%256; //计算当前页还可以写下多少数据 if(len<=page_remain) //如果当前写入的字节长度小于剩余的长度 page_remain=len; while(1) W25Q64_PageWrite(addr,buff,page_remain); if(page_remain==len)break; //表明数据已经写入完毕 buff+=page_remain; //buff向后偏移地址 addr+=page_remain; //起始地址向后偏移 len-=page_remain; //减去已经写入的字节数 if(len>256)page_remain=256; //如果大于一页,每次就直接写256字节 else page_remain=len; 函数功能:指定位置写入指定个数的数据,考虑擦除问题,完善代码 函数参数: u32 addr---写入数据的起始地址 u8 *buff---写入的数据 u32 len---长度 说明:擦除的最小单位扇区,4096字节 static u8 W25Q64_READ_WRITE_CHECK_BUFF[4096]; void W25Q64_WriteByteData(u32 addr,u8 *buff,u32 len) u32 i; u32 sector_addr; //存放扇区的地址 u32 sector_move; //扇区向后偏移的地址 u32 sector_size; //扇区大小。(剩余的空间大小) u8 *p=W25Q64_READ_WRITE_CHECK_BUFF;//存放指针 sector_addr=addr/4096; //传入的地址是处于第几个扇区 sector_move=addr%4096; //计算传入的地址存于当前的扇区的偏移量位置 sector_size=4096-sector_move; //得到当前扇区剩余的空间 if(len<=sector_size) sector_size=len; //判断第一种可能性、一次可以写完 while(1) W25Q64_ReadByteData(addr,p,sector_size); //读取剩余扇区里的数据 for(i=0;i<sector_size;i++) if(p[i]!=0xFF)break; if(i!=sector_size) //判断是否需要擦除 W25Q64_ClearSector(sector_addr*4096); W25Q64_WriteByteDataNoCheck(addr,buff,sector_size); if(sector_size==len)break; addr+=sector_size; //向后偏移地址 buff+=sector_size ;//向后偏移 len-=sector_size; //减去已经写入的数据 sector_addr++; //校验第下个扇区 if(len>4096) //表明还可以写一个扇区 sector_size=4096;//继续写一个扇区 sector_size=len; //剩余的空间可以写完

STM32入门开发: 采用IIC硬件时序读写AT24C08(EEPROM)

一、环境介绍编程软件: keil5操作系统: win10MCU型号: STM32F103ZET6STM32编程方式: 寄存器开发 (方便程序移植到其他单片机)IIC总线:  STM32本身支持IIC硬件时序的,上篇文章已经介绍了采用IIC模拟时序读写AT24C02,这篇文章介绍STM32的硬件IIC配置方法,并读写AT24C08。文章地址: https://xiaolong.blog.csdn.net/article/details/117586108模拟时序更加方便移植到其他单片机,通用性更高,不分MCU;硬件时序效率更高,每个MCU配置方法不同,依赖硬件本身支持。器件型号: 采用AT24C08  EEPROM存储芯片完整的工程源码下载地址,下载即可编译运行测试(包含了模拟IIC时序、STM32硬件IIC时序分别驱动AT24C02和AT24C08):  https://download.csdn.net/download/xiaolong1126626497/19399945二、AT24C08存储芯片介绍2.1 芯片功能特性介绍AT24C08 是串行CMOS类型的EEPROM存储芯片,AT24C0x这个系列包含了AT24C01、AT24C02、AT24C04、AT24C08、AT24C16这些具体的芯片型号。他们容量分别是:1K (128 x 8)、2K (256 x 8)、8K (1024 x 8)、16K (2048 x 8)  ,其中的8表示8位(bit)它们的管脚功能、封装特点如下:芯片功能描述:AT24C08系列支持I2C,总线数据传送协议I2C,总线协议规定任何将数据传送到总线的器件作为发送器。任何从总线接收数据的器件为接收器;数据传送是由产生串行时钟和所有起始停止信号的主器件控制的。主器件和从器件都可以作为发送器或接收器,但由主器件控制传送数据(发送或接收)的模式。芯片特性介绍:1. 低压和标准电压运行          –2.7(VCC=2.7伏至5.5伏)          –1.8(VCC=1.8伏至5.5伏)2. 两线串行接口(SDA、SCL)3. 有用于硬件数据保护的写保护引脚4. 自定时写入周期(5毫秒~10毫秒),因为内部有页缓冲区,向AT24C0x写入数据之后,还需要等待AT24C0x将缓冲区数据写入到内部EEPROM区域.5. 数据保存可达100年6. 100万次擦写周期7. 高数据传送速率为400KHz、低速100KHZ和IIC总线兼容。 100 kHz(1.8V)和400 kHz(2.7V、5V)8. 8字节页写缓冲区       这个缓冲区大小与芯片具体型号有关: 8字节页(1K、2K)、16字节页(4K、8K、16K)     2.2 芯片设备地址介绍因为IIC协议规定,每次传递数据都是按8个字节传输的,AT24C08是1024字节,地址的选择上与AT24C02有所区别;IIC设备的标准地址位是7位。上面这个图里AT24C08的1010是芯片内部固定值,A2 是硬件引脚、由硬件决定电平;P1、P0是空间存储块选择,每个存储块大小是256字节,寻址范围是0~255,AT24C08相当于是4块AT24C02的构造;最后一位是读/写位(1是读,0是写),读写位不算在地址位里,但是根据IIC的时序顺序,在操作设备前,都需要先发送7位地址,再发送1位读写位,才能启动对芯片的操作,我们在写模拟时序为了方便统一写for循环,按字节发送,所以一般都是将7地址位与1位读写位拼在一起,组合成1个字节,方便按字节传输数据。我现在使用的开发板上AT24C08的原理图是这样的:那么这个AT24C08的标准设备地址分别是:第一块区域:  0x50(十六进制),对应的二进制就是: 1010000第二块区域:  0x51(十六进制),对应的二进制就是: 1010001第三块区域:  0x52(十六进制),对应的二进制就是: 1010010第四块区域:  0x53(十六进制),对应的二进制就是: 1010011如果将读写位组合在一起,读权限的设备地址: 第一块区域:  0xA1(十六进制),对应的二进制就是: 10100001第二块区域:  0xA3(十六进制),对应的二进制就是: 10100011第三块区域:  0xA5(十六进制),对应的二进制就是: 10100101第四块区域:  0xA7(十六进制),对应的二进制就是: 10100111如果将读写位组合在一起,写权限的设备地址: 第一块区域:  0xA0(十六进制),对应的二进制就是: 10100000第二块区域:  0xA2(十六进制),对应的二进制就是: 10100010第三块区域:  0xA4(十六进制),对应的二进制就是: 10100100第四块区域:  0xA6(十六进制),对应的二进制就是: 101001102.3  对AT24C08 按字节写数据的指令流程(时序) 详细解释:1.  先发送起始信号2.  发送设备地址(写权限)3. 等待AT24C08应答、低电平有效4. 发送存储地址、AT24C08内部一共有256个字节空间,寻址是从0开始的,范围是(0~255);发送这个存储器地址就是告诉AT24C08接下来的数据改存储到哪个地方。5. 等待AT24C08应答、低电平有效6. 发送一个字节的数据,这个数据就是想存储到AT24C08里保存的数据。7. 等待AT24C08应答、低电平有效8. 发送停止信号2.3  对AT24C08 按页写数据的指令流程(时序)详细解释:1.  先发送起始信号2.  发送设备地址(写权限)3. 等待AT24C08应答、低电平有效4. 发送存储地址、AT24C08内部一共有256个字节空间,寻址是从0开始的,范围是(0~255);发送这个存储器地址就是告诉AT24C08接下来的数据改存储到哪个地方。5. 等待AT24C08应答、低电平有效6. 可以循环发送8个字节的数据,这些数据就是想存储到AT24C08里保存的数据。    AT24C08的页缓冲区是16个字节,所有这里的循环最多也只能发送16个字节,多发送的字节会将前面的覆盖掉。   需要注意的地方:  这个页缓冲区的寻址也是从0开始,比如:  0~15算第1页,16~32算第2页......依次类推。 如果现在写数据的起始地址是3,那么这一页只剩下13个字节可以写;并不是说从哪里都可以循环写16个字节。      详细流程: 这里程序里一般使用for循环实现     (1).  发送字节1     (2). 等待AT24C08应答,低电平有效     (3). 发送字节2     (4). 等待AT24C08应答,低电平有效     .........     最多8次.   7. 等待AT24C08应答、低电平有效8. 发送停止信号2.4  从AT24C08任意地址读任意字节数据(时序)AT24C08支持当前地址读、任意地址读,最常用的还是任意地址读,因为可以指定读取数据的地址,比较灵活,上面这个指定时序图就是任意地址读。 详细解释:1.  先发送起始信号2.  发送设备地址(写权限)3. 等待AT24C08应答、低电平有效4. 发送存储地址、AT24C08内部一共有2048个字节空间,寻址是从0开始的,范围是(0~1024);发送这个存储器地址就是告诉AT24C08接下来应该返回那个地址的数据给单片机。5. 等待AT24C08应答、低电平有效6.  重新发送起始信号(切换读写模式)7. 发送设备地址(读权限)8.  等待AT24C08应答、低电平有效9. 循环读取数据:  接收AT24C08返回的数据.   读数据没有字节限制,可以第1个字节、也可以连续将整个芯片读完。10. 发送非应答(高电平有效)11. 发送停止信号三、IIC总线介绍       2.1 IIC总线简介I2C(Inter-Integrated Circuit)总线是由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备,是微电子通信控制领域广泛采用的一种总线标准。具有接口线少,控制方式简单,器件封装形式小,通信速率较高等优点。I2C规程运用主/从双向通讯。器件发送数据到总线上,则定义为发送器,器件接收数据则定义为接收器。主器件和从器件都可以工作于接收和发送状态。I2C 总线通过串行数据(SDA)线和串行时钟(SCL)线在连接到总线的器件间传递信息。每个器件都有一个唯一的地址识别,而且都可以作为一个发送器或接收器(由器件的功能决定)。I2C有四种工作模式:       1.主机发送       2.主机接收       3.从机发送       4.从机接收I2C总线只用两根线:串行数据SDA(Serial Data)、串行时钟SCL(Serial Clock)。总线必须由主机(通常为微控制器)控制,主机产生串行时钟(SCL)控制总线的传输方向,并产生起始和停止条件。SDA线上的数据状态仅在SCL为低电平的期间才能改变。2.2 IIC总线上的设备连接图I2C 总线在物理连接上非常简单,分别由SDA(串行数据线)和SCL(串行时钟线)及上拉电阻组成。通信原理是通过对SCL和SDA线高低电平时序的控制,来产生I2C总线协议所需要的信号进行数据的传递。在总线空闲状态时,这两根线一般被上面所接的上拉电阻拉高,保持着高电平。其中上拉电阻范围是4.7K~100K。2.3 I2C总线特征I2C总线上的每一个设备都可以作为主设备或者从设备,而且每一个从设备都会对应一个唯一的地址(可以从I2C器件的数据手册得知)。主从设备之间就通过这个地址来确定与哪个器件进行通信,在通常的应用中,我们把CPU带I2C总线接口的模块作为主设备,把挂接在总线上的其他设备都作为从设备。1.    总线上能挂接的器件数量     I2C总线上可挂接的设备数量受总线的最大电容400pF 限制,如果所挂接的是相同型号的器件,则还受器件地址的限制。    一般I2C设备地址是7位地址(也有10位),地址分成两部分:芯片固化地址(生产芯片时候哪些接地,哪些接电源,已经固定),可编程地址(引出IO口,由硬件设备决定)。     例如: 某一个器件是7 位地址,其中10101 xxx  高4位出厂时候固定了,低3位可以由设计者决定。则一条I2C总线上只能挂该种器件最少8个。如果7位地址都可以编程,那理论上就可以达到128个器件,但实际中不会挂载这么多。2.    总线速度传输速度:I2C总线数据传输速率在标准模式下可达100kbit/s,快速模式下可达400kbit/s,高速模式下可达3.4Mbit/s。一般通过I2C总线接口可编程时钟来实现传输速率的调整。3.    总线数据长度I2C总线上的主设备与从设备之间以字节(8位)为单位进行双向的数据传输。 2.4 I2C总线协议基本时序信号空闲状态:SCL和SDA都保持着高电平。起始条件:总线在空闲状态时,SCL和SDA都保持着高电平,当SCL为高电平期间而SDA由高到低的跳变,表示产生一个起始条件。在起始条件产生后,总线处于忙状态,由本次数据传输的主从设备独占,其他I2C器件无法访问总线。停止条件:当SCL为高而SDA由低到高的跳变,表示产生一个停止条件。答应信号:每个字节传输完成后的下一个时钟信号,在SCL高电平期间,SDA为低,则表示一个应答信号。非答应信号:每个字节传输完成后的下一个时钟信号,在SCL高电平期间,SDA为高,则表示一个应答信号。应答信号或非应答信号是由接收器发出的,发送器则是检测这个信号(发送器,接收器可以从设备也可以主设备)。注意:起始和结束信号总是由主设备产生。2.5  起始信号与停止信号起始信号就是:  时钟线SCL处于高电平的时候,数据线SDA由高电平变为低电平的过程。SCL=1;SDA=1;SDA=0;停止信号就是: 时钟线SCL处于低电平的时候,  数据线SDA由低电平变为高电平的过程。SCL=1;SDA=0;SDA=1;2.6  应答信号数据位的第9位就时应答位。 读取应答位的流程和读取数据位是一样的。示例:   SCL=0;SCL=1;ACK=SDA;       这个ACK就是读取的应答状态。2.7 数据位传输时序通过时序图了解到,SCL处于高电平的时候数据稳定,SCL处于低电平的时候数据不稳定。那么对于写一位数据(STM32--->AT24C08): SCL=0;SDA=data; SCL=1; 那么对于读一位数据(STM32<-----AT24C08): SCL=0;SCL=1;data=SDA;    2.8 总线时序四、IIC总线时序代码、AT24C08读写代码  在调试IIC模拟时序的时候,可以在淘宝上买一个24M的USB逻辑分析仪,时序出现问题,使用逻辑分析仪一分析就可以快速找到问题。4.1 iic.c  这是STM32的IIC硬件时序完整代码1./* 函数功能: 初始化IIC总线 硬件连接: SCL---PB6 SDA---PB7 void IIC_Init(void) /*1. 时钟配置*/ RCC->APB2ENR|=1<<3; //PB /*2. GPIO口模式配置*/ GPIOB->CRL&=0x00FFFFFF; GPIOB->CRL|=0xFF000000; //复用开漏输出 GPIOB->ODR|=0x3<<6; /*3. GPIO口时钟配置(顺序不能错)*/ RCC->APB1ENR|=1<<21; //I2C1时钟 RCC->APB1RSTR|=1<<21; //开启复位时钟 RCC->APB1RSTR&=~(1<<21);//关闭复位时钟 /*4. 配置IIC的核心寄存器*/ I2C1->CR2=0x24<<0; //配置主机频率为36MHZ I2C1->CCR|=0x2D<<0; //配置主机频率是400KHZ I2C1->CR1|=1<<0; //开启IIC模块 CCR=主机时钟频率/2/IIC总线的频率 45=36MHZ/2/400KHZ ---0x2D 函数功能: 发送起始信号 当时钟线为高电平的时候,数据线由高电平变为低电平的过程 void IIC_SendStart(void) I2C1->CR1|=1<<8; //产生起始信号 while(!(I2C1->SR1&1<<0)){} //等待起始信号完成 I2C1->SR1=0; //清除状态位 函数功能: 停止信号 当时钟线为高电平的时候,数据线由低电平变为高电平的过程 void IIC_SendStop(void) I2C1->CR1|=1<<9; 函数功能: 发送地址数据 void IIC_SendAddr(u8 addr) u32 s1,s2; I2C1->DR=addr; //发送数据 while(1) s1=I2C1->SR1; s2=I2C1->SR2; if(s1&1<<1) //判断地址有没有发送成功 break; 函数功能: 发送数据 void IIC_SendOneByte(u8 addr) u32 s1,s2; I2C1->DR=addr; //发送数据 while(1) s1=I2C1->SR1; s2=I2C1->SR2; if(s1&1<<2) //判断数据有没有发送成功 break; 函数功能: 接收一个字节数据 u8 IIC_RecvOneByte(void) u8 data=0; I2C1->CR1|=1<<10; //使能应答 while(!(I2C1->SR1&1<<6)){} //等待数据 data=I2C1->DR; I2C1->CR1&=~(1<<10); //关闭应答使能 return data; }4.2 AT24C08.c 这是AT24C08完整的读写代码* 函数功能: 写一个字节 函数参数: u8 addr 数据的位置(0~1023) u8 data 数据范围(0~255) void AT24C08_WriteOneByte(u16 addr,u8 data) u8 read_device_addr=AT24C08_READ_ADDR; u8 write_device_addr=AT24C08_WRITE_ADDR; if(addr<256*1) //第一个块 write_device_addr|=0x0<<1; read_device_addr|=0x0<<1; else if(addr<256*2) //第二个块 write_device_addr|=0x1<<1; read_device_addr|=0x1<<1; else if(addr<256*3) //第三个块 write_device_addr|=0x2<<1; read_device_addr|=0x2<<1; else if(addr<256*4) //第四个块 write_device_addr|=0x3<<1; read_device_addr|=0x3<<1; addr=addr%256; //得到地址范围 IIC_SendStart();//起始信号 IIC_SendAddr(write_device_addr);//发送设备地址 IIC_SendOneByte(addr); //数据存放的地址 IIC_SendOneByte(data); //发送将要存放的数据 IIC_SendStop(); //停止信号 DelayMs(10); //等待写 函数功能: 读一个字节 函数参数: u8 addr 数据的位置(0~1023) 返回值: 读到的数据 u8 AT24C08_ReadOneByte(u16 addr) u8 data=0; u8 read_device_addr=AT24C08_READ_ADDR; u8 write_device_addr=AT24C08_WRITE_ADDR; if(addr<256*1) //第一个块 write_device_addr|=0x0<<1; read_device_addr|=0x0<<1; else if(addr<256*2) //第二个块 write_device_addr|=0x1<<1; read_device_addr|=0x1<<1; else if(addr<256*3) //第三个块 write_device_addr|=0x2<<1; read_device_addr|=0x2<<1; else if(addr<256*4) //第四个块 write_device_addr|=0x3<<1; read_device_addr|=0x3<<1; addr=addr%256; //得到地址范围 IIC_SendStart();//起始信号 IIC_SendAddr(write_device_addr);//发送设备地址 IIC_SendOneByte(addr); //将要读取数据的地址 IIC_SendStart();//起始信号 IIC_SendAddr(read_device_addr);//发送设备地址 data=IIC_RecvOneByte();//读取数据 IIC_SendStop(); //停止信号 return data; 函数功能: 从指定位置读取指定长度的数据 函数参数: u16 addr 数据的位置(0~1023) u16 len 读取的长度 u8 *buffer 存放读取的数据 返回值: 读到的数据 void AT24C08_ReadByte(u16 addr,u16 len,u8 *buffer) u16 i=0; IIC_SendStart();//起始信号 IIC_SendAddr(AT24C08_WRITE_ADDR);//发送设备地址 IIC_SendOneByte(addr); //将要读取数据的地址 IIC_SendStart();//起始信号 IIC_SendAddr(AT24C08_READ_ADDR);//发送设备地址 for(i=0;i<len;i++) buffer[i]=IIC_RecvOneByte();//读取数据 IIC_SendStop(); //停止信号 函数功能: AT24C08页写函数 函数参数: u16 addr 写入的位置(0~1023) u8 len 写入的长度(每页16字节) u8 *buffer 存放读取的数据 void AT24C08_PageWrite(u16 addr,u16 len,u8 *buffer) u16 i=0; IIC_SendStart();//起始信号 IIC_SendAddr(AT24C08_WRITE_ADDR);//发送设备地址 IIC_SendOneByte(addr); //数据存放的地址 for(i=0;i<len;i++) IIC_SendOneByte(buffer[i]); //发送将要存放的数据 IIC_SendStop(); //停止信号 DelayMs(10); //等待写 函数功能: 从指定位置写入指定长度的数据 函数参数: u16 addr 数据的位置(0~1023) u16 len 写入的长度 u8 *buffer 存放即将写入的数据 返回值: 读到的数据 void AT24C08_WriteByte(u16 addr,u16 len,u8 *buffer) u8 page_byte=16-addr%16; //得到当前页剩余的字节数量 if(page_byte>len) //判断当前页剩余的字节空间是否够写 page_byte=len; //表示一次性可以写完 while(1) AT24C08_PageWrite(addr,page_byte,buffer); //写一页 if(page_byte==len)break; //写完了 buffer+=page_byte; //指针偏移 addr+=page_byte;//地址偏移 len-=page_byte;//得到剩余没有写完的长度 if(len>16)page_byte=16; else page_byte=len; //一次可以写完 }4.3 main.c 这是AT24C08测试代码#include "stm32f10x.h" #include "beep.h" #include "delay.h" #include "led.h" #include "key.h" #include "sys.h" #include "usart.h" #include <string.h> #include <stdio.h> #include "exti.h" #include "timer.h" #include "rtc.h" #include "adc.h" #include "ds18b20.h" #include "ble.h" #include "esp8266.h" #include "wdg.h" #include "oled.h" #include "rfid_rc522.h" #include "infrared.h" #include "iic.h" #include "at24c08.h" u8 buff_tx[50]="1234567890"; u8 buff_rx[50]; u8 data=88; u8 data2; int main() u8 key; LED_Init(); KEY_Init(); BEEP_Init(); TIM1_Init(72,20000); //辅助串口1接收,超时时间为20ms USART_X_Init(USART1,72,115200); IIC_Init(); //IIC总线初始化 printf("usart1 ok\n"); while(1) key=KEY_Scanf(); if(key) //AT24C08_WriteByte(100,50,buff_tx); //AT24C08_ReadByte(100,50,buff_rx); //printf("buff_rx=%s\n",buff_rx); //测试第0块 // data=AT24C08_ReadOneByte(0); // AT24C08_WriteOneByte(0,data+1); // printf("data=%d\n",data); //测试第1块 // data=AT24C08_ReadOneByte(300); // AT24C08_WriteOneByte(300,data+1); // printf("data=%d\n",data); //测试第2块 // data=AT24C08_ReadOneByte(600); // AT24C08_WriteOneByte(600,data+1); // printf("data=%d\n",data); //测试第3块 data=AT24C08_ReadOneByte(900); AT24C08_WriteOneByte(900,data+1); printf("data=%d\n",data); } 串口调试助手源码下载地址: https://blog.csdn.net/xiaolong1126626497/article/details/116040983

STM32入门开发: 介绍IIC总线、读写AT24C02(EEPROM)(采用模拟时序)

一、环境介绍编程软件: keil5操作系统: win10MCU型号: STM32F103ZET6STM32编程方式: 寄存器开发 (方便程序移植到其他单片机)IIC总线:  STM32本身支持IIC硬件时序的,本文采用的是模拟时序,下篇文章就介绍配置STM32的IIC硬件时序读写AT24C02和AT24C08。模拟时序更加方便移植到其他单片机,通用性更高,不分MCU;硬件时序效率更高,单每个MCU配置方法不同,依赖硬件本身支持。目前器件: 采用AT24C02  EEPROM存储芯片完整的工程源码下载地址,下载即可编译运行测试(包含了模拟IIC时序、STM32硬件IIC时序分别驱动AT24C02和AT24C08):  https://download.csdn.net/download/xiaolong1126626497/19399945二、AT24C02存储芯片介绍2.1 芯片功能特性介绍AT24C02 是串行CMOS类型的EEPROM存储芯片,AT24C0x这个系列包含了AT24C01、AT24C02、AT24C04、AT24C08、AT24C16这些具体的芯片型号。他们容量分别是:1K (128 x 8)、2K (256 x 8)、4K (512 x 8)、8K (1024 x 8)、16K (2048 x 8)  ,其中的8表示8位(bit)它们的管脚功能、封装特点如下:芯片功能描述:AT24C02系列支持I2C,总线数据传送协议I2C,总线协议规定任何将数据传送到总线的器件作为发送器。任何从总线接收数据的器件为接收器;数据传送是由产生串行时钟和所有起始停止信号的主器件控制的。主器件和从器件都可以作为发送器或接收器,但由主器件控制传送数据(发送或接收)的模式,由于A0、A1和A2可以组成000~111八种情况,即通过器件地址输入端A0、A1和A2可以实现将最多8个AT24C02器件连接到总线上,通过进行不同的配置进行选择器件。芯片特性介绍:1. 低压和标准电压运行          –2.7(VCC=2.7伏至5.5伏)          –1.8(VCC=1.8伏至5.5伏)2. 两线串行接口(SDA、SCL)3. 有用于硬件数据保护的写保护引脚4. 自定时写入周期(5毫秒~10毫秒),因为内部有页缓冲区,向AT24C0x写入数据之后,还需要等待AT24C0x将缓冲区数据写入到内部EEPROM区域.5. 数据保存可达100年6. 100万次擦写周期7. 高数据传送速率为400KHz、低速100KHZ和IIC总线兼容。 100 kHz(1.8V)和400 kHz(2.7V、5V)8. 8字节页写缓冲区       这个缓冲区大小与芯片具体型号有关: 8字节页(1K、2K)、16字节页(4K、8K、16K)     2.2 芯片设备地址介绍IIC设备的标准地址位是7位。上面这个图里AT24C02的1010是芯片内部固定值,A2 、A1、 A0是硬件引脚、由硬件决定电平;最后一位是读/写位(1是读,0是写),读写位不算在地址位里,但是根据IIC的时序顺序,在操作设备前,都需要先发送7位地址,再发送1位读写位,才能启动对芯片的操作,我们在写模拟时序为了方便统一写for循环,按字节发送,所以一般都是将7地址位与1位读写位拼在一起,组合成1个字节,方便按字节传输数据。我现在使用的开发板上AT24C02的原理图是这样的:那么这个AT24C02的标准设备地址就是: 0x50(十六进制),对应的二进制就是: 1010000如果将读写位组合在一起,读权限的设备地址: 0xA1 (10100001)  、写权限的设备地址: 0xA0 (10100000)2.3  对AT24C02 按字节写数据的指令流程(时序)    详细解释:1.  先发送起始信号2.  发送设备地址(写权限)3. 等待AT24C02应答、低电平有效4. 发送存储地址、AT24C02内部一共有256个字节空间,寻址是从0开始的,范围是(0~255);发送这个存储器地址就是告诉AT24C02接下来的数据改存储到哪个地方。5. 等待AT24C02应答、低电平有效6. 发送一个字节的数据,这个数据就是想存储到AT24C02里保存的数据。7. 等待AT24C02应答、低电平有效8. 发送停止信号2.3  对AT24C02 按页写数据的指令流程(时序)详细解释:1.  先发送起始信号2.  发送设备地址(写权限)3. 等待AT24C02应答、低电平有效4. 发送存储地址、AT24C02内部一共有256个字节空间,寻址是从0开始的,范围是(0~255);发送这个存储器地址就是告诉AT24C02接下来的数据改存储到哪个地方。5. 等待AT24C02应答、低电平有效6. 可以循环发送8个字节的数据,这些数据就是想存储到AT24C02里保存的数据。    AT24C02的页缓冲区是8个字节,所有这里的循环最多也只能发送8个字节,多发送的字节会将前面的覆盖掉。   需要注意的地方:  这个页缓冲区的寻址也是从0开始,比如:  0~7算第1页,8~15算第2页......依次类推。 如果现在写数据的起始地址是3,那么这一页只剩下5个字节可以写;并不是说从哪里都可以循环写8个字节。      详细流程: 这里程序里一般使用for循环实现     (1).  发送字节1     (2). 等待AT24C02应答,低电平有效     (3). 发送字节2     (4). 等待AT24C02应答,低电平有效     .........     最多8次.   7. 等待AT24C02应答、低电平有效8. 发送停止信号2.4  从AT24C02任意地址读任意字节数据(时序)AT24C02支持当前地址读、任意地址读,最常用的还是任意地址读,因为可以指定读取数据的地址,比较灵活,上面这个指定时序图就是任意地址读。 详细解释:1.  先发送起始信号2.  发送设备地址(写权限)3. 等待AT24C02应答、低电平有效4. 发送存储地址、AT24C02内部一共有256个字节空间,寻址是从0开始的,范围是(0~255);发送这个存储器地址就是告诉AT24C02接下来应该返回那个地址的数据给单片机。5. 等待AT24C02应答、低电平有效6.  重新发送起始信号(切换读写模式)7. 发送设备地址(读权限)8.  等待AT24C02应答、低电平有效9. 循环读取数据:  接收AT24C02返回的数据.   读数据没有字节限制,可以第1个字节、也可以连续将整个芯片读完。10. 发送非应答(高电平有效)11. 发送停止信号三、IIC总线介绍       2.1 IIC总线简介I2C(Inter-Integrated Circuit)总线是由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备,是微电子通信控制领域广泛采用的一种总线标准。具有接口线少,控制方式简单,器件封装形式小,通信速率较高等优点。I2C规程运用主/从双向通讯。器件发送数据到总线上,则定义为发送器,器件接收数据则定义为接收器。主器件和从器件都可以工作于接收和发送状态。I2C 总线通过串行数据(SDA)线和串行时钟(SCL)线在连接到总线的器件间传递信息。每个器件都有一个唯一的地址识别,而且都可以作为一个发送器或接收器(由器件的功能决定)。I2C有四种工作模式:       1.主机发送       2.主机接收       3.从机发送       4.从机接收I2C总线只用两根线:串行数据SDA(Serial Data)、串行时钟SCL(Serial Clock)。总线必须由主机(通常为微控制器)控制,主机产生串行时钟(SCL)控制总线的传输方向,并产生起始和停止条件。SDA线上的数据状态仅在SCL为低电平的期间才能改变。2.2 IIC总线上的设备连接图I2C 总线在物理连接上非常简单,分别由SDA(串行数据线)和SCL(串行时钟线)及上拉电阻组成。通信原理是通过对SCL和SDA线高低电平时序的控制,来产生I2C总线协议所需要的信号进行数据的传递。在总线空闲状态时,这两根线一般被上面所接的上拉电阻拉高,保持着高电平。其中上拉电阻范围是4.7K~100K。2.3 I2C总线特征I2C总线上的每一个设备都可以作为主设备或者从设备,而且每一个从设备都会对应一个唯一的地址(可以从I2C器件的数据手册得知)。主从设备之间就通过这个地址来确定与哪个器件进行通信,在通常的应用中,我们把CPU带I2C总线接口的模块作为主设备,把挂接在总线上的其他设备都作为从设备。1.    总线上能挂接的器件数量     I2C总线上可挂接的设备数量受总线的最大电容400pF 限制,如果所挂接的是相同型号的器件,则还受器件地址的限制。    一般I2C设备地址是7位地址(也有10位),地址分成两部分:芯片固化地址(生产芯片时候哪些接地,哪些接电源,已经固定),可编程地址(引出IO口,由硬件设备决定)。     例如: 某一个器件是7 位地址,其中10101 xxx  高4位出厂时候固定了,低3位可以由设计者决定。则一条I2C总线上只能挂该种器件最少8个。如果7位地址都可以编程,那理论上就可以达到128个器件,但实际中不会挂载这么多。2.    总线速度传输速度:I2C总线数据传输速率在标准模式下可达100kbit/s,快速模式下可达400kbit/s,高速模式下可达3.4Mbit/s。一般通过I2C总线接口可编程时钟来实现传输速率的调整。3.    总线数据长度I2C总线上的主设备与从设备之间以字节(8位)为单位进行双向的数据传输。 2.4 I2C总线协议基本时序信号空闲状态:SCL和SDA都保持着高电平。起始条件:总线在空闲状态时,SCL和SDA都保持着高电平,当SCL为高电平期间而SDA由高到低的跳变,表示产生一个起始条件。在起始条件产生后,总线处于忙状态,由本次数据传输的主从设备独占,其他I2C器件无法访问总线。停止条件:当SCL为高而SDA由低到高的跳变,表示产生一个停止条件。答应信号:每个字节传输完成后的下一个时钟信号,在SCL高电平期间,SDA为低,则表示一个应答信号。非答应信号:每个字节传输完成后的下一个时钟信号,在SCL高电平期间,SDA为高,则表示一个应答信号。应答信号或非应答信号是由接收器发出的,发送器则是检测这个信号(发送器,接收器可以从设备也可以主设备)。注意:起始和结束信号总是由主设备产生。2.5  起始信号与停止信号起始信号就是:  时钟线SCL处于高电平的时候,数据线SDA由高电平变为低电平的过程。SCL=1;SDA=1;SDA=0;停止信号就是: 时钟线SCL处于低电平的时候,  数据线SDA由低电平变为高电平的过程。SCL=1;SDA=0;SDA=1;2.6  应答信号数据位的第9位就时应答位。 读取应答位的流程和读取数据位是一样的。示例:   SCL=0;SCL=1;ACK=SDA;       这个ACK就是读取的应答状态。2.7 数据位传输时序通过时序图了解到,SCL处于高电平的时候数据稳定,SCL处于低电平的时候数据不稳定。那么对于写一位数据(STM32--->AT24C02): SCL=0;SDA=data; SCL=1; 那么对于读一位数据(STM32<-----AT24C02): SCL=0;SCL=1;data=SDA;    2.8 总线时序四、IIC总线时序代码、AT24C02读写代码  在调试IIC模拟时序的时候,可以在淘宝上买一个24M的USB逻辑分析仪,时序出现问题,使用逻辑分析仪一分析就可以快速找到问题。4.1 iic.c  这是IIC模拟时序完整代码#include "iic.h" 函数功能:IIC接口初始化 硬件连接: SDA:PB7 SCL: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; 4.2 AT24C02.c 这是AT24C02完整的读写代码#include "at24c02.h" 函数功能:检查AT24C02是否存在 返 回 值:1表示失败,0表示成功 u8 At24c02Check(void) u8 data; At24c02WriteOneByteData(255,0xAA); data=At24c02ReadOneByteData(255); if(data==0xAA)return 0; else return 1; 函数功能:AT24C02随机读数据 函数形参:读取的地址(0~255) 返 回 值:读出一个数据 u8 At24c02ReadOneByteData(u32 addr) u8 data; IIC_Start(); //发送起始信号 IIC_WriteOneByteData(AT24C02_WRITE_ADDR); //设置写模式 IIC_GetACK();//获取应答 IIC_WriteOneByteData(addr); //设置读取数据的位置 IIC_GetACK();//获取应答 IIC_Start(); //发送起始信号 IIC_WriteOneByteData(AT24C02_READ_ADDR); //设置读模式 IIC_GetACK();//获取应答 data=IIC_ReadOneByteData(); //接收数据 IIC_SendACK(1); //发送非应答信号 IIC_Stop(); //停止信号 return data; 函数功能:AT24C02写一个字节的数据 函数形参: addr:写入的地址(0~255) data:写入的数据 void At24c02WriteOneByteData(u32 addr,u8 data) IIC_Start(); //发送起始信号 IIC_WriteOneByteData(AT24C02_WRITE_ADDR); //设置写模式 IIC_GetACK();//获取应答 IIC_WriteOneByteData(addr); //设置写入数据的位置 IIC_GetACK();//获取应答 IIC_WriteOneByteData(data); //设置写入的数据 IIC_GetACK();//获取应答 IIC_Stop(); //停止信号 DelayMs(10); //等待写入完毕 函数 功 能:AT24C02当前位置读一个字节数据 函数返回值:读出的数据 u8 At24c02CurrentAddrReadOneByteData(void) u8 data; IIC_Start(); //发送起始信号 IIC_WriteOneByteData(AT24C02_READ_ADDR); //设置读模式 IIC_GetACK();//获取应答 data=IIC_ReadOneByteData(); //接收数据 IIC_SendACK(1); //发送非应答信号 IIC_Stop(); //停止信号 return data; 函数功能:AT24C02连续读数据 函数形参: u8 addr //读取的地址(0~255) u8 len //读取的长度 u8 *buff //读出的数据存放缓冲区 void At24c02ReadByteData(u32 addr,u8 len,u8 *buff) u8 i; IIC_Start(); //发送起始信号 IIC_WriteOneByteData(AT24C02_WRITE_ADDR); //设置写模式 IIC_GetACK();//获取应答 IIC_WriteOneByteData(addr); //设置读取数据的位置 IIC_GetACK();//获取应答 IIC_Start(); //发送起始信号 IIC_WriteOneByteData(AT24C02_READ_ADDR); //设置读模式 IIC_GetACK();//获取应答 for(i=0;i<len;i++) buff[i]=IIC_ReadOneByteData(); //接收数据 IIC_SendACK(0); //发送应答信号 IIC_SendACK(1); //发送非应答信号 IIC_Stop(); //停止信号 函数功能:AT24C02页写 函数形参: addr:写入的地址(0~255) *data:写入的数据缓冲区 len :写入的长度 1. 页写的缓冲区大小是8个字节,一次最多写8个字节进去。 2. 页写的地址是固定的。 0~7 是第一页 8~15是第二页 void At24c02PageWrite(u32 addr,u8 *data,u8 len) u8 i; IIC_Start(); //发送起始信号 IIC_WriteOneByteData(AT24C02_WRITE_ADDR); //设置写模式 IIC_GetACK();//获取应答 IIC_WriteOneByteData(addr); //设置写入数据的位置 IIC_GetACK();//获取应答 for(i=0;i<len;i++) IIC_WriteOneByteData(data[i]); //设置写入的数据 IIC_GetACK();//获取应答 IIC_Stop(); //停止信号 DelayMs(10); //等待写入完毕 void AT24C02_WriteData(u32 addr,u8 *data,u8 len) u32 page_remain=8-addr%8; //一页剩余的字节数量 if(page_remain>=len) page_remain=len; while(1) At24c02PageWrite(addr,data,page_remain); if(page_remain==len) break; addr+=page_remain; data+=page_remain; len-=page_remain; if(len>=8)page_remain=8; else page_remain=len;

QT软件开发: 基于QT设计的完整版视频播放器、多媒体播放器(mdk-sdk)

一、环境介绍操作系统: win10 64位QT版本:  QT5.12.6编译器:  MinGW 32播放器底层接口: mdk-sdk库完整工程源码下载地址(下载即可编译运行): https://download.csdn.net/download/xiaolong1126626497/19387165二、播放器功能介绍1.  支持命令行传入视频播放2.  支持图像旋转播放3.  支持查看媒体信息4.  支持选择GPU加速解码5.  支持快进、快退6.  支持预览画面(鼠标放在进度条上查看画面缩略图)7.  支持单帧播放,就是一帧一帧的点击切换画面8. 支持画面拍照、截图9. 支持复位到视频首页10. 支持音量调整11. 支持拖拽文件到窗口播放12. 默认打开视频不会自动播放。 自动显示在第一帧,视频放完停留在最后一帧。13. 支持播放音频文件。可以显示音频文件的封面。14. 鼠标左键双击放大. 全屏播放15. 鼠标右键或者空格键切换暂停与播放状态16. 鼠标放在进度条上可以实现画面预览17. 滚动条支持点击跳转或拖动.18. 支持音量调整、拖动或者点击.19. 支持静音切换.20. 支持播放列表添加,选中右下角的复选框,可以打开播放列表。播放列表里,鼠标右键可以添加、删除播放文件.21. 支持退出时保存播放列表,下次打开软件自动加载播放列表.下次打开软件时,如果播放列表文件路径存在,将自动选中第一个文件播放.三、播放器运行效果正常播放界面:播放MP3文件,可以获取封面专辑打开:可以直接拖动文件到播放器窗口播放:右下角的复选框可以打开播放列表:播放列表里,点击鼠标右键可以添加播放文件、删除文件:鼠标左键双击屏幕可以全屏播放,再次双击可以还原界面:鼠标放在滚动条上可以预览视频画面:点击工具栏的倍速按钮,选择倍速播放:点击工具栏的旋转按钮,旋转图像:点击 工具栏的拍照按钮,截图当前视频帧保存到视频播放器同级目录下:鼠标滚轮可以向前或者向后滚动,单帧播放画面:点击工具栏上的下一个和上一个按钮,可以根据播放列表切换当前播放的视频:点击复位按钮可以重头播放:点击工具栏喇叭,可以切换静音状态,拖动或者点击滑块可以调整音量:支持快进、快退:点击按钮载入视频:四、mdk-sdk介绍github 首页地址:  https://github.com/wang-bin/mdk-sdkFeaturesSimple and powerful API setCross platform: Windows, UWP, Linux, macOS, Android, iOS, Raspberry PiHardware accelerated decoding and 0-copy GPU rendering for all platformsOpenGL, D3D11, Vulkan and Metal rendering w/ or w/o user provided contextIntegrated with any gui toolkits or apps via OpenGL, D3D11, Vulkan and Metal (OBS, Qt, SDL, glfw, SFML etc.) easilySeamless/Gapless media and bitrate switch for any mediaHDR rendering in GPUOptimized Continuous seeking. As fast as mpv, but much lower cpu, memory and gpu load. Suitable for timeline previewSmart FFmpeg runtime. See https://github.com/wang-bin/mdk-sdk/wiki/FFmpeg-Runtimemdk-sdk的API使用介绍:  https://github.com/wang-bin/mdk-sdk/wiki/Player-APIsmdk-sdk的SDK包(CSDN)--截止当前是最新版本:  https://download.csdn.net/download/xiaolong1126626497/16273875mdk-sdk的使用例子(GitHub):https://github.com/wang-bin/mdk-examples五、工程源码完整工程源码下载地址(下载即可编译运行): https://download.csdn.net/download/xiaolong1126626497/19387165UI主窗口的逻辑代码:#include "widget.h" #include "ui_widget.h" Widget* Widget::pThis = nullptr; Widget::Widget(QString filename,QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) ui->setupUi(this); pThis=this; setWindowFlags(Qt::FramelessWindowHint); setAcceptDrops(true); setFocusPolicy(Qt::StrongFocus); //设置窗口的标题名称 this->setWindowTitle("视频播放器"); //获取标题栏的状态 win_flag=windowFlags(); //加载样式表 SetStyle(":/resource/QMDK_VideoPlayer.qss"); //QMDK初始化 QMDK_InitConfig(); //UI界面相关初始化 UI_InitConfig(); //如果构造函数传入的视频文件就直接加载 if(!filename.isEmpty()) load_video_file(0,filename); isPressedWidget=false; //设置获取焦点 ui->AV_player->setFocus(); //窗口显示在屏幕正中间 QDesktopWidget *desktop = QApplication::desktop(); move((desktop->width()-this->width())/2,(desktop->height()-this->height())/2); //加载配置 LoadConfig(); //设置默认选中的条目 if(ui->listWidget_videoData->count()>0) ui->listWidget_videoData->setCurrentRow(0); //加载视频文件 QTimer::singleShot(200, this, SLOT(load_updateCaption())); Widget::~Widget() delete ui; void Widget::load_updateCaption() QString text=ui->listWidget_videoData->item(0)->text(); qDebug()<<"加载视频文件:"<<text; load_video_file(false,text); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 加载样式表 void Widget::SetStyle(const QString &qssFile) QFile file(qssFile); if (file.open(QFile::ReadOnly)) QByteArray qss=file.readAll(); qApp->setStyleSheet(qss); file.close(); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: UI界面初始化 void Widget::UI_InitConfig() //音量滑块范围设置 ui->horizontalSlider_AudioValue->setMaximum(100); ui->horizontalSlider_AudioValue->setMinimum(0); //播放速度设置 ui->MediaSpeedBtn->setCheckable(true); m_TimeSpeedGrp = new QActionGroup(this); QStringList strSpeedItems; strSpeedItems << tr("0.5X") << tr("0.75X") << tr("1.0X") << tr("1.25X") << tr("1.5X"); float speeds[] = { 0.5,0.75,1.0,1.25,1.5}; for (int i = 0; i < strSpeedItems.size(); i++) QAction *pSpeedItem = m_SpeedMenu.addAction(strSpeedItems.at(i)); pSpeedItem->setData(QVariant::fromValue(speeds[i])); pSpeedItem->setCheckable(true); m_TimeSpeedGrp->addAction(pSpeedItem); if (i == 2) pSpeedItem->setChecked(true); connect(m_TimeSpeedGrp, SIGNAL(triggered(QAction *)), this, SLOT(slot_onSetTimeSpeed(QAction *))); //安装事件监听器 事件筛选器是接收发送到此对象的所有事件的对象 ui->horizontalSlider_PlayPosition->installEventFilter(this); ui->horizontalSlider_AudioValue->installEventFilter(this); //状态信息初始化 MediaInfo.state=MEDIA_NOLOAD; //工具提示信息 ui->toolButton_load->setToolTip(tr("加载视频,也可以直接将视频文件拖拽到窗口")); ui->MediaPrevBtn->setToolTip(tr("快退")); ui->MediaPlayBtn->setToolTip(tr("快进")); ui->MediaPauseBtn->setToolTip(tr("暂停/继续")); ui->MediaSpeedBtn->setToolTip(tr("倍速选择")); ui->MediaResetBtn->setToolTip(tr("复位")); ui->MediaSnapshotBtn->setToolTip(tr("拍照(保存在程序运行目录下png)")); ui->VolumeBtn->setToolTip(tr("静音切换")); ui->checkBox_video_list->setToolTip(tr("显示视频列表")); ui->toolButton_pgup->setToolTip(tr("播放上一个媒体")); ui->toolButton_pgDn->setToolTip(tr("播放下一个媒体")); ui->MediaRotateBtn->setToolTip(tr("旋转图像")); ui->toolButton_about->setToolTip(tr("关于")); //默认不显示 ui->listWidget_videoData->setVisible(false); //播放进度条滑块初始化 connect(ui->horizontalSlider_PlayPosition, SIGNAL(onLeave()), SLOT(onTimeSliderLeave())); connect(ui->horizontalSlider_PlayPosition, SIGNAL(onHover(int,int)), SLOT(onTimeSliderHover(int,int))); connect(ui->horizontalSlider_PlayPosition, SIGNAL(sliderMoved(int)), SLOT(seek(int))); connect(ui->horizontalSlider_PlayPosition, SIGNAL(sliderPressed()), SLOT(seek())); this->setMouseTracking(true); /*初始化视频列表的右键菜单*/ Grp_In_ListWidget = new QActionGroup(this); QAction *action_DeleteSelect_In_ListWidget = menu_In_ListWidget.addAction(tr("删除选中")); QAction *action_ClearAll_In_ListWidget = menu_In_ListWidget.addAction(tr("删除全部")); QAction *action_AddFile_In_ListWidget = menu_In_ListWidget.addAction(tr("添加文件")); action_DeleteSelect_In_ListWidget->setData(MENU_DEL_SELECT); //删除选中 action_ClearAll_In_ListWidget->setData(MENU_DEL_ALL); //删除全部 action_AddFile_In_ListWidget->setData(MENU_ADD_FILE); //添加文件 Grp_In_ListWidget->addAction(action_DeleteSelect_In_ListWidget); //添加到分组 Grp_In_ListWidget->addAction(action_ClearAll_In_ListWidget); //添加到分组 Grp_In_ListWidget->addAction(action_AddFile_In_ListWidget); //添加到分组 connect(Grp_In_ListWidget, SIGNAL(triggered(QAction *)), this, SLOT(slot_onListWidgetMenu(QAction *))); /*设置QListWidget的contextMenuPolicy属性,不然不能显示右键菜单*/ ui->listWidget_videoData->setProperty("contextMenuPolicy", Qt::CustomContextMenu); /*绑定右键显示菜单:在单击右键之后会执行槽函数, 槽函数中负责弹出右键菜单*/ connect(ui->listWidget_videoData, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onCustomContextMenuRequested(const QPoint &))); timer = new QTimer(this); connect(timer, SIGNAL(timeout()), this, SLOT(timeout_update())); //图像的旋转方向 m_RotateGrp = new QActionGroup(this); QStringList strDegrees; strDegrees << tr("0~") << tr("90~") << tr("180~") << tr("270~"); int Degrees[] = {0, 90, 180, 270 }; for (int i = 0; i < strDegrees.size(); i++) QAction *pItem = m_RotateMenu.addAction(strDegrees.at(i)); pItem->setData(QVariant::fromValue(Degrees[i])); pItem->setCheckable(true); m_RotateGrp->addAction(pItem); connect(m_RotateGrp, SIGNAL(triggered(QAction *)), this, SLOT(slot_onMediaRotate(QAction *))); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 事件处理函数 void Widget::onCustomContextMenuRequested(const QPoint &pos) qDebug()<<"弹出右键菜单"; QAction *pSelect = menu_In_ListWidget.exec(QCursor::pos()); if (pSelect == nullptr) return; 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: QMDK初始化配置 void Widget::QMDK_InitConfig() //SetGlobalOption("MDK_KEY", "10453B8F2140865027CEDD6FDF846D940CA738BE72FE5EE1397DF61714CAAA2A185B72EEC1F781FD5E1FA9BB0AB739E35CCC793F0EBC3FD0182D61EE56E59E08EFBAC47021408D50D8312290207B926B0CA730D91E982991551C8FD75973CAF6B1C4573E7CBF9467F3BAF34F8D9F0A8AE239503BFB1B7B02E4EB0F2121E5D408"); //关联双击事件 connect(ui->AV_player, &QMDKWidget::ss_VideoWidgetEvent, this,&Widget::slot_VideoWidgetEvent); m_preview=new QMDKWidget(ui->AV_player); m_preview->setVisible(false); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 加载视频文件 flag=0 重新加载文件 flag=1 重新开始播放 QString file_path 这参数可以传入文件名称,因为窗口支持拖放文件进来 返回值: true 成功 false 失败 bool Widget::load_video_file(bool flag,QString file_path) if(flag==false) QString filename=file_path; if(filename.isEmpty()) filename=QFileDialog::getOpenFileName(this,"选择播放的视频","D:/",tr("*.mp4 *.wmv *.*")); strncpy(video_name,filename.toUtf8().data(),sizeof(video_name)); //判断文件是否存在 if(QFileInfo::exists(video_name)==false) return false; MediaInfo.state=MEDIA_LOAD; MediaInfo.mediaName=video_name; //设置播放的媒体文件 ui->AV_player->stop(); ui->AV_player->setMedia(video_name); ui->AV_player->play(); //预览窗口 m_preview->stop(); m_preview->setMedia(video_name); m_preview->prepreForPreview(); m_preview->setMute(true); QThread::msleep(100); //设置总时间 ui->label_Total_Time->setText(QTime(0, 0, 0,0).addMSecs(int(ui->AV_player->duration())).toString(QString::fromLatin1("HH:mm:ss:zzz"))); //设置进度条滑块范围 ui->horizontalSlider_PlayPosition->setMinimum(0); ui->horizontalSlider_PlayPosition->setMaximum(ui->AV_player->duration()); //每次加载新文件设置播放进度条为0 ui->horizontalSlider_PlayPosition->setValue(0); qDebug()<<"停止原视频"; ui->AV_player->pause(); //设置当前播放的视频名称 QFileInfo info(video_name); ui->label_FileName->setText(QString("%1").arg(info.fileName())); //设置按钮状态--暂停状态 ui->MediaPauseBtn->setChecked(true); return true; 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 加载视频文件 void Widget::on_toolButton_load_clicked() qDebug()<<"加载视频文件状态:"<<load_video_file(0,""); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 暂停播放 void Widget::on_MediaPauseBtn_clicked() bool bPause=ui->MediaPauseBtn->isChecked(); if(bPause == true) ui->AV_player->pause(); timer->stop(); ui->AV_player->play(); timer->start(); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 重新开始播放 void Widget::on_MediaResetBtn_clicked() //加重新开始播放 load_video_file(true,""); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 快退播放 void Widget::on_MediaPrevBtn_clicked() ui->AV_player->seek(ui->AV_player->position()-10000); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 快进播放 void Widget::on_MediaPlayBtn_clicked() ui->AV_player->seek(ui->AV_player->position()+10000); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 音量设置 void Widget::on_VolumeBtn_clicked() bool checked=ui->VolumeBtn->isChecked(); ui->AV_player->setMute(checked); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 音量设置 void Widget::on_horizontalSlider_AudioValue_valueChanged(int value) ui->AV_player->setVolume(float(value/10.0)); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 播放速度设置菜单选择 void Widget::slot_onSetTimeSpeed(QAction *action) action->setChecked(true); ui->MediaSpeedBtn->setToolTip(action->text()); ui->MediaSpeedBtn->setText(action->text()); //设置速度 正常速度是1.0 ui->AV_player->setPlaybackRate(float(action->data().toFloat())); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 点击了速度设置按钮 void Widget::on_MediaSpeedBtn_clicked() QPoint ptWgt = ui->MediaSpeedBtn->mapToGlobal(QPoint(0, 0)); ptWgt -= QPoint(10, 180); QAction *pSelect = m_SpeedMenu.exec(ptWgt); if (pSelect == nullptr) return; 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 显示菜单 void Widget::slot_onListWidgetMenu(QAction *action) if (action == nullptr) return; //得到按下的序号 MENU_ITEM item = MENU_ITEM(action->data().toInt()); //删除选中 if (item == MENU_DEL_SELECT) /*获取当前选中的Item*/ QList<QListWidgetItem*> items = ui->listWidget_videoData->selectedItems(); if (items.count() > 0) foreach(QListWidgetItem* var, items) ui->listWidget_videoData->removeItemWidget(var); items.removeOne(var); delete var; qDebug()<<"删除选中"; //删除全部 else if(item == MENU_DEL_ALL) int counter = ui->listWidget_videoData->count(); QListWidgetItem *item; for(int index = 0;index <counter;index++) item = ui->listWidget_videoData->takeItem(0); delete item; qDebug()<<"删除全部"; //添加文件 else if(item == MENU_ADD_FILE) QStringList filenamelist=QFileDialog::getOpenFileNames(this,"选择添加的文件","D:/",tr("*.*")); if(filenamelist.count()>0) ui->listWidget_videoData->addItems(filenamelist); //添加到列表 qDebug()<<"添加文件"; 工程: QtAV_VideoPlayer 日期: 2021-03-24 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 点击了截图按钮 void Widget::on_MediaSnapshotBtn_clicked() ui->AV_player->snapshot(); void Widget:: onTimeSliderHover(int pos, int value) QPoint gpos=mapToGlobal(ui->horizontalSlider_PlayPosition->pos() + QPoint(pos, ui->AV_player->height()/5 *5)); // qDebug()<<"onTimeSliderHover(int pos, int value) value"<<value; QToolTip::showText(gpos, QTime(0, 0, 0).addMSecs(value).toString(QString::fromLatin1("HH:mm:ss"))); m_preview->seek(value); int w=ui->AV_player->width()/5; int h=ui->AV_player->height()/5; m_preview->resize(w,h); QPoint p; p.setY(ui->AV_player->height()/6 *5-30); p.setX(pos); m_preview->move(p); m_preview->show(); void Widget::onTimeSliderLeave() if (m_preview && m_preview->isVisible()) m_preview->hide(); void Widget::seek(int value) // if(!ui->AV_player->isPaused()) // { // ui->AV_player->pause(); // } ui->AV_player->seek(value); m_preview->seek(value); m_preview->resize(ui->AV_player->width()/5,ui->AV_player->height()/5); m_preview->show(); ui->label_current_Time->setText(QTime(0, 0, 0,0).addMSecs(int(value)).toString(QString::fromLatin1("HH:mm:ss:zzz"))); void Widget::seek() qDebug()<<"seek()"; seek(ui->horizontalSlider_PlayPosition->value()); void Widget::dragEnterEvent(QDragEnterEvent *e) if (e->mimeData()->hasUrls()) e->acceptProposedAction(); void Widget::dropEvent(QDropEvent *e) foreach (const QUrl &url, e->mimeData()->urls()) QString fileName = url.toLocalFile(); qDebug() << "拖入的文件名称:" << fileName; //加载视频文件 load_video_file(false,fileName); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 双击列表播放视频 void Widget::on_listWidget_videoData_itemDoubleClicked(QListWidgetItem *item) //加载视频文件 load_video_file(false,item->text()); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 显示视频列表 void Widget::on_checkBox_video_list_clicked(bool checked) if(checked) ui->listWidget_videoData->setVisible(true); //不显示 ui->listWidget_videoData->setVisible(false); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 视频事件处理 void Widget::slot_VideoWidgetEvent(int type) //type =1 双击按下 //type =2 鼠标左键按下 //type =3 鼠标滚轮向前 //type =4 鼠标滚轮向后 int pos=0; switch (type) { case 1: //双击事件 widget_state=!widget_state; if(widget_state) //全屏 showFullScreen(); ui->listWidget_videoData->setVisible(false); ui->widget_tools->setVisible(false); ui->frame_time_bar->setVisible(false); ui->widget_tite->setVisible(false); else //恢复非全屏 // setWindowFlags(win_flag); showNormal(); if(ui->checkBox_video_list->isChecked())ui->listWidget_videoData->setVisible(true); ui->widget_tools->setVisible(true); ui->frame_time_bar->setVisible(true); ui->widget_tite->setVisible(true); break; case 2: if(ui->AV_player->isPaused()) ui->AV_player->play(); timer->start(); //设置按钮状态--播放状态 ui->MediaPauseBtn->setChecked(false); ui->AV_player->pause(); timer->stop(); //设置按钮状态--暂停状态 ui->MediaPauseBtn->setChecked(true); break; case 3: //鼠标滚轮向前 pos=ui->horizontalSlider_PlayPosition->value(); pos+=100; ui->horizontalSlider_PlayPosition->setValue(pos); ui->AV_player->seek(pos); ui->label_current_Time->setText(QTime(0, 0, 0,0).addMSecs(int(pos)).toString(QString::fromLatin1("HH:mm:ss:zzz"))); break; case 4: //鼠标滚轮向后 pos=ui->horizontalSlider_PlayPosition->value(); pos-=100; ui->horizontalSlider_PlayPosition->setValue(pos); ui->AV_player->seek(pos); ui->label_current_Time->setText(QTime(0, 0, 0,0).addMSecs(int(pos)).toString(QString::fromLatin1("HH:mm:ss:zzz"))); break; 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 关闭窗口 void Widget::on_toolButton_close_clicked() this->close(); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能:mousePressEvent void Widget::mousePressEvent(QMouseEvent *event) m_lastPos = event->globalPos(); isPressedWidget = true; // 当前鼠标按下的即是QWidget而非界面上布局的其它控件 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能:mouseMoveEvent void Widget::mouseMoveEvent(QMouseEvent *event) if (isPressedWidget) { this->move(this->x() + (event->globalX() - m_lastPos.x()), this->y() + (event->globalY() - m_lastPos.y())); m_lastPos = event->globalPos(); 工程: QtAV_VideoPlayer 日期: 2021-03-25 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能:mouseReleaseEvent void Widget::mouseReleaseEvent(QMouseEvent *event) m_lastPos = event->globalPos(); isPressedWidget = false; // 鼠标松开时,置为false void Widget::keyPressEvent(QKeyEvent *event) switch(event->key()) case Qt::Key_Space: if(ui->AV_player->isPaused()) ui->AV_player->play(); timer->start(); //设置按钮状态--播放状态 ui->MediaPauseBtn->setChecked(false); ui->AV_player->pause(); timer->stop(); //设置按钮状态--暂停状态 ui->MediaPauseBtn->setChecked(true); break; //定时器超时 void Widget::timeout_update() int64_t pos=ui->AV_player->position(); //设置进度条的时间 ui->horizontalSlider_PlayPosition->setValue(int(pos)); ui->label_current_Time->setText(QTime(0, 0, 0,0).addMSecs(int(pos)).toString(QString::fromLatin1("HH:mm:ss:zzz"))); bool Widget::eventFilter(QObject *obj, QEvent *event) if(obj==ui->horizontalSlider_AudioValue) if (event->type()==QEvent::MouseButtonPress) //判断类型 QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event); if (mouseEvent->button() == Qt::LeftButton) //判断左键 int value = QStyle::sliderValueFromPosition(ui->horizontalSlider_AudioValue->minimum(), ui->horizontalSlider_AudioValue->maximum(), mouseEvent->pos().x(), ui->horizontalSlider_AudioValue->width()); ui->horizontalSlider_AudioValue->setValue(value); //设置音量 ui->AV_player->setVolume(float(value/10.0)); return QObject::eventFilter(obj,event); //播放上一个媒体 void Widget::on_toolButton_pgup_clicked() if(ui->listWidget_videoData->count()<=0)return; int row=0; row=ui->listWidget_videoData->currentRow(); //当前选中行 qDebug()<<"row:"<<row; if(row>0) row--; ui->listWidget_videoData->setCurrentRow(row); ui->listWidget_videoData->setCurrentRow(ui->listWidget_videoData->count()-1); //加载视频文件 load_video_file(false,ui->listWidget_videoData->currentItem()->text()); //播放下一个媒体 void Widget::on_toolButton_pgDn_clicked() if(ui->listWidget_videoData->count()<=0)return; int row=0; row=ui->listWidget_videoData->currentRow(); //当前选中行 qDebug()<<"row:"<<row; if(row<ui->listWidget_videoData->count()-1) row++; ui->listWidget_videoData->setCurrentRow(row); ui->listWidget_videoData->setCurrentRow(0); //加载视频文件 load_video_file(false,ui->listWidget_videoData->currentItem()->text()); void Widget::slot_onMediaRotate(QAction *action) action->setChecked(true); ui->MediaRotateBtn->setToolTip(action->text()); ui->AV_player->setrotate(action->data().toInt()); void Widget::on_MediaRotateBtn_clicked() QPoint ptWgt = ui->MediaRotateBtn->mapToGlobal(QPoint(0, 0)); ptWgt -= QPoint(10, 94); QAction *pSelect = m_RotateMenu.exec(ptWgt); if (pSelect == nullptr) return; //保存配置文件 void Widget::SaveConfig() /*保存数据到文件,方便下次加载*/ QString text; text=QCoreApplication::applicationDirPath()+"/"+ConfigFile; QFile filesrc(text); filesrc.open(QIODevice::WriteOnly); QDataStream out(&filesrc); for(int i=0;i<ui->listWidget_videoData->count();i++) QListWidgetItem *item=ui->listWidget_videoData->item(i); out << item->text(); //序列化写 filesrc.flush(); filesrc.close(); //加载配置 void Widget::LoadConfig() //读取配置文件 QString text; QString data; text=QCoreApplication::applicationDirPath()+"/"+ConfigFile; //判断文件是否存在 if(QFile::exists(text)) QFile filenew(text); filenew.open(QIODevice::ReadOnly); QDataStream in(&filenew); // 从文件读取序列化数据 while(!in.atEnd()) in>>data; //文件存在才需要添加进去 if(QFile::exists(data)) ui->listWidget_videoData->addItem(data); filenew.close(); void Widget::closeEvent(QCloseEvent *event) int ret = QMessageBox::question(this, tr("重要提示"), tr("是否需要关闭窗口?"), QMessageBox::Yes | QMessageBox::No); if(ret==QMessageBox::Yes) SaveConfig(); event->accept(); //接受事件 event->ignore(); //清除事件 工程: QMDK_VideoPlayer 日期: 2021-04-16 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 关于的提示 void Widget::on_toolButton_about_clicked() QString text= "<html>" "<body>" "<h1><center>视频播放器功能介绍</center></h1>" "<p>1. 基于MDK-SDK接口开发.</p>" "<p>这是MDK-SDK的github地址: <a href=\"https://github.com/wang-bin/mdk-sdk/\"> https://github.com/wang-bin/mdk-sdk</a></p>" "<p>2. 鼠标左键双击放大.</p>" "<p>3. 鼠标右键或者空格键切换暂停与播放.</p>" "<p>4. 鼠标放在进度条上可以实现画面预览.</p>" "<p>5. 滚动条支持点击跳转或拖动.</p>" "<p>6. 支持音量调整、拖动或者点击.</p>" "<p>7. 支持静音切换.</p>" "<p>8. 支持播放列表添加,选中右下角的复选框,可以打开播放列表。\n播放列表里,鼠标右键可以添加、删除播放文件.</p>" "<p>9. 支持退出时保存播放列表,下次打开软件自动加载播放列表.\n下次打开软件时,如果播放列表文件路径存在,将自动选中第一个文件播放.</p>" "<p>10. 每次打开视频默认是暂停状态,不会播放,只显示第一个画面.</p>" "<p>11. 支持鼠标滚轮滚动预览画面.</p>" "</body>" "</html>"; QMessageBox::about(this,"关于",text);

STM32入门开发: 编写DS18B20温度传感器驱动(读取环境温度、支持级联)

一、环境介绍编程软件: keil5操作系统: win10MCU型号: STM32F103C8T6STM32编程方式: 寄存器开发 (方便程序移植到其他单片机)温度传感器: DS1820DS18B20是一个数字温度传感器,采用的是单总线时序与主机通信,只需要一根线就可以完成温度数据读取;DS18B20内置了64位产品序列号,方便识别身份,在一根线上可以挂接多个DS18B20传感器,通过64位身份验证,可以分别读取来至不同传感器采集的温度信息。二、DS18B20介绍2.1 DS18B20 的主要特征1. 全数字温度转换及输出。2. 先进的单总线数据通信。3. 最高 12 位分辨率,精度可达土 0.5 摄氏度。4. 12 位分辨率时的最大工作周期为 750 毫秒。5. 可选择寄生工作方式。6. 检测温度范围为–55° C ~+125° C (–67° F ~+257° F)7. 内置 EEPROM,限温报警功能。8. 64位光刻 ROM,内置产品序列号,方便多机挂接。9. 多样封装形式,适应不同硬件系统。2.2 DS18B20 引脚功能GND 电压地DQ 单数据总线VDD 电源电压NC 空引脚2.3 DS18B20 工作原理及应用        DS18B20 的温度检测与数字数据输出全集成于一个芯片之上,从而抗干扰力更强。其一个工作周期可分为两个部分,即温度检测和数据处理。18B20 共有三种形态的存储器资源,它们分别是:ROM 只读存储器,用于存放 DS18B20ID 编码,其前 8 位是单线系列编码(DS18B20 的编码是19H),后面 48 位是芯片唯一的序列号,最后 8 位是以上 56 的位的 CRC 码(冗余校验)。数据在出产时设置不由用户更改,DS18B20 一共有 64 位 ROM。RAM 数据暂存器,用于内部计算和数据存取,数据在掉电后丢失, DS18B20 共 9 个字节 RAM,每个字节为 8 位。第 1、 2 个字节是温度转换后的数据值信息,第 3、 4 个字节是用户 EEPROM(常用于温度报警值储存)的镜像。在上电复位时其值将被刷新。第 5 个字节则是用户第 3 个 EEPROM的镜像。第 6、 7、 8 个字节为计数寄存器,是为了让用户得到更高的温度分辨率而设计的,同样也是内部温度转换、计算的暂存单元。第 9 个字节为前 8 个字节的 CRC 码。 EEPROM 非易失性记忆体,用于存放长期需要保存的数据,上下限温度报警值和校验数据,DS18B20 共 3 位 EEPROM,并在 RAM 都存在镜像,以方便用户操作。DS18B20默认工作在12位分辨率模式,转换后得到的12位数据,存储在DS18B20的两个8比特的RAM中(最前面的两个字节),二进制中的前面5位是符号位,如果测得的温度大于0,这5位为0,只要将测到的数值乘于0.0625即可得到实际温度;如果温度小于0,这5位为1,测到的数值需要取反加1再乘于0.0625即可得到实际温度。或者使用位运算方式提取温度:  小数位是占用的是低4位,高位是整数位(不考虑负数情况)。2.4 DS18B20 芯片 ROM 指令表1.    Read ROM(读 ROM) [33H] (方括号中的为 16 进制的命令字)这个命令允许总线控制器读到 DS18B20 的 64 位 ROM。只有当总线上只存在一个 DS18B20 的时候才可以使用此指令,如果挂接不只一个,当通信时将会发生数据冲突2.    atch ROM(指定匹配芯片) [55H]这个指令后面紧跟着由控制器发出了 64 位序列号,当总线上有多只 DS18B20 时,只有与控制发出的序列号相同的芯片才可以做出反应,其它芯片将等待下一次复位。这条指令适应单芯片和多芯片挂接。3.    Skip ROM(跳跃 ROM 指令) [CCH]这条指令使芯片不对 ROM 编码做出反应,在单总线的情况之下,为了节省时间则可以选用此指令。如果在多芯片挂接时使用此指令将会出现数据冲突,导致错误出现。4.    Search ROM(搜索芯片) [F0H]在芯片初始化后,搜索指令允许总线上挂接多芯片时用排除法识别所有器件的 64 位 ROM。5.    Alarm Search(报警芯片搜索) [ECH]在多芯片挂接的情况下,报警芯片搜索指令只对附合温度高于 TH 或小于 TL 报警条件的芯片做出反应。只要芯片不掉电,报警状态将被保持,直到再一次测得温度什达不到报警条件为止。6.    Write Scratchpad (向 RAM 中写数据) [4EH]这是向 RAM 中写入数据的指令,随后写入的两个字节的数据将会被存到地址 2(报警 RAM 之 TH)和地址 3(报警 RAM 之 TL)。写入过程中可以用复位信号中止写入。7.    Read Scratchpad (从 RAM 中读数据) [BEH]此指令将从 RAM 中读数据,读地址从地址 0 开始,一直可以读到地址 9,完成整个 RAM 数据的读出。芯片允许在读过程中用复位信号中止读取,即可以不读后面不需要的字节以减少读取时间。8.    Copy Scratchpad (将 RAM 数据复制到 EEPROM 中) [48H]此指令将 RAM 中的数据存入 EEPROM 中,以使数据掉电不丢失。此后由于芯片忙于 EEPROM 储存处理,当控制器发一个读时间隙时,总线上输出“0”,当储存工作完成时,总线将输出“1”。在寄生工作方式时必须在发出此指令后立刻超用强上拉并至少保持 10MS,来维持芯片工作。 9.    Convert T(温度转换) [44H]收到此指令后芯片将进行一次温度转换,将转换的温度值放入 RAM 的第 1、 2 地址。此后由于芯片忙于温度转换处理,当控制器发一个读时间隙时,总线上输出“0”,当储存工作完成时,总线将输出“1”。在寄生工作方式时必须在发出此指令后立刻超用强上拉并至少保持 500MS,来维持芯片工作。10.    Recall EEPROM(将 EEPROM 中的报警值复制到 RAM) [B8H]此指令将 EEPROM 中的报警值复制到 RAM 中的第 3、 4 个字节里。由于芯片忙于复制处理,当控制器发一个读时间隙时,总线上输出“0”,当储存工作完成时,总线将输出“1”。另外,此指令将在芯片上电复位时将被自动执行。这样 RAM 中的两个报警字节位将始终为 EEPROM 中数据的镜像。11.    Read Power Supply(工作方式切换) [B4H]此指令发出后发出读时间隙,芯片会返回它的电源状态字,“0”为寄生电源状态,“1”为外部电源状态。2.5 DS18B20时序图2.5.1 DS18B20 复位及应答关系示意图每一次通信之前必须进行复位,复位的时间、等待时间、回应时间应严格按时序编程。DS18B20 读写时间隙:DS18B20的数据读写是通过时间隙处理位和命令字来确认信息交换的。 2.5.2  向DS18B20写数据0和数据1在写数据时间隙的前 15uS 总线需要是被控制器拉置低电平,而后则将是芯片对总线数据的采样时间,采样时间在 15~60uS,采样时间内如果控制器将总线拉高则表示写“1”,如果控制器将总线拉低则表示写“0”。每一位的发送都应该有一个至少 15uS的低电平起始位,随后的数据“0”或“1”应该在 45uS 内完成。整个位的发送时间应该保持在 60~120uS,否则不能保证通信的正常。注意:  DS18B20读写数据都是从低位开始传输。2.5.3 从DS18B20读数据0和数据1读时间隙时控制时的采样时间应该更加的精确才行,读时间隙时也是必须先由主机产生至少1uS的低电平,表示读时间的起始。随后在总线被释放后的 15uS 中 DS18B20 会发送内部数据位,这时控制如果发现总线为高电平表示读出“1”,如果总线为低电平则表示读出数据“0”。每一位的读取之前都由控制器加一个起始信号。 注意:必须在读间隙开始的 15uS 内读取数据位才可以保证通信的正确。在通信时是以 8 位“0”或“1”为一个字节,字节的读或写是从低位开始的。2.5.4  读取一次温度的顺序(总线上只有单个DS18B20情况)1. 发送复位信号2. 检测回应信号3. 发送0xCC4. 发送0x445. 发送复位信号6. 检测回应信号7. 写0xcc8. 写0xbe9. 循环8次读取温度低字节10. 循环8次读取温度高字节11. 合成16位温度数据,处理三、驱动代码3.1 DS18B20.c#include "ds18b20.h" 函数功能: DS18B20初始化 硬件连接: PB15 void DS18B20_Init(void) RCC->APB2ENR|=1<<3; //PB GPIOB->CRH&=0x0FFFFFFF; GPIOB->CRH|=0x30000000; GPIOB->ODR|=1<<15; //上拉 函数功能: 检测DS18B20设备是否存在 返回值 : 1表示设备不存在 0表示设备正常 u8 DS18B20_CheckDevice(void) //包含了复位脉冲、检测存在脉冲 DS18B20_OUTPUT_MODE();//初始化为输出模式 DS18B20_OUT=0; //产生复位脉冲 DelayUs(750); //产生750us的低电平 DS18B20_OUT=1; //释放总线 DelayUs(15); //等待DS18B20回应 if(DS18B20_CleckAck())//检测存在脉冲 return 1; return 0; 函数功能: 检测DS18B20设备的存在脉冲 返回值 : 1表示错误 0表示正常 u8 DS18B20_CleckAck(void) u8 cnt=0; DS18B20_INPUT_MODE();//初始化为输入模式 while(DS18B20_IN&&cnt<200) //等待DS18B20响应存在脉冲 DelayUs(1); cnt++; if(cnt>=200)return 1; //错误 cnt=0; while((!DS18B20_IN)&&cnt<240) //等待DS18B20释放总线 DelayUs(1); cnt++; if(cnt>=240)return 1; //错误 return 0; 函数功能: 写一个字节 首先学会如何写一个位。 void DS18B20_WriteByte(u8 cmd) u8 i; DS18B20_OUTPUT_MODE(); //初始化为输出模式 for(i=0;i<8;i++) DS18B20_OUT=0; //产生写时间间隙(写开始) DelayUs(2); DS18B20_OUT=cmd&0x01; //发送实际的数据位 DelayUs(60); //等待写完成 DS18B20_OUT=1; //释放总线,准备下一次发送 cmd>>=1; //继续发送下一位数据 函数功能: 读一个字节 首先学会如何读一个位。 u8 DS18B20_ReadByte(void) u8 i,data=0; for(i=0;i<8;i++) DS18B20_OUTPUT_MODE(); //初始化为输出模式 DS18B20_OUT=0; //产生读时间间隙(读开始) DelayUs(2); DS18B20_OUT=1; //释放总线 DS18B20_INPUT_MODE(); //初始化为输入模式 DelayUs(8); //等待DS18B20的数据输出 data>>=1; //高位补0,默认以0为准 if(DS18B20_IN) data|=0x80; DelayUs(60); DS18B20_OUT=1; //释放总线,等待读取下一位数据 return data; 函数功能: 读取一次DS18B20的温度数据 返 回 值: 读取的温度数据 考虑的情况: 总线上只是接了一个DS18B20的情况 u16 DS18B20_ReadTemp(void) u16 temp=0; u8 temp_H,temp_L; DS18B20_CheckDevice(); //发送复位脉冲、检测存在脉冲 DS18B20_WriteByte(0xCC); //跳过ROM序列检测 DS18B20_WriteByte(0x44); //启动一次温度转换 //等待温度转换完成 while(DS18B20_ReadByte()!=0xFF){} DS18B20_CheckDevice(); //发送复位脉冲、检测存在脉冲 DS18B20_WriteByte(0xCC); //跳过ROM序列检测 DS18B20_WriteByte(0xBE); //读取温度 temp_L=DS18B20_ReadByte(); //读取的温度低位数据 temp_H=DS18B20_ReadByte(); //读取的温度高位数据 temp=temp_L|(temp_H<<8); //合成温度 return temp; 3.2 DS18B20.h#ifndef DS18B20_H #define DS18B20_H #include "stm32f10x.h" #include "sys.h" #include "delay.h" #include "ds18b20.h" #include "usart.h" /*封装接口*/ //初始化DS18B20为输入模式 #define DS18B20_INPUT_MODE() {GPIOB->CRH&=0x0FFFFFFF;GPIOB->CRH|=0x80000000;} //初始化DS18B20为输出模式 #define DS18B20_OUTPUT_MODE(){GPIOB->CRH&=0x0FFFFFFF;GPIOB->CRH|=0x30000000;} //DS18B20 IO口输出 #define DS18B20_OUT PBout(15) //DS18B20 IO口输入 #define DS18B20_IN PBin(15) //函数声明 u8 DS18B20_CleckAck(void); u8 DS18B20_CheckDevice(void); void DS18B20_Init(void); u16 DS18B20_ReadTemp(void); u8 DS18B20_ReadByte(void); void DS18B20_WriteByte(u8 cmd); #endif3.3 延时函数/* 函数功能: 延时us单位 void DelayUs(int us) #ifdef _SYSTICK_IRQ_ int i,j; for(i=0;i<us;i++) for(j=0;j<72;j++); #else u32 tmp; SysTick->VAL=0; //CNT计数器值 SysTick->LOAD=9*us; //9表示1us SysTick->CTRL|=1<<0; //开启定时器 tmp=SysTick->CTRL; //读取状态 }while((!(tmp&1<<16))&&(tmp&1<<0)); SysTick->VAL=0; //CNT计数器值 SysTick->CTRL&=~(1<<0); //关闭定时器 #endif }3.4 main.c 调用DS18B20读取温度打印到串口#include "stm32f10x.h" #include "ds18b20.h" u8 DS18B20_ROM[8]; //存放DS18B20的64为ROM编码 int main(void) u16 temp; USARTx_Init(USART1,72,115200);//串口1的初始化 DS18B20_Init(); //DS18B20初始化 /*1. 读取DS18B20的64位ROM编码*/ //发送复位脉冲、检测存在脉冲 while(DS18B20_CheckDevice()) printf("DS18B20设备不存在!\n"); DelayMs(500); //发送读取64为ROM编码的命令 DS18B20_WriteByte(0x33); //循环读取64位ROM编码 for(i=0;i<8;i++) DS18B20_ROM[i]= DS18B20_ReadByte(); printf("DS18B20_ROM[%d]=0x%X\n",i,DS18B20_ROM[i]); while(1) /*2. 同时操作总线上所有的DS18B20开始转换温度*/ DS18B20_CheckDevice(); //发送复位脉冲、检测存在脉冲 DS18B20_WriteByte(0xCC); //跳过ROM序列检测 DS18B20_WriteByte(0x44); //启动一次温度转换(让总线上所有的DS18B20都转换温度) DelayMs(500); //等待线上所有的DS18B20温度转换完成 /*3. 单个针对性读取每个DS18B20的温度*/ DS18B20_CheckDevice(); //发送复位脉冲、检测存在脉冲 DS18B20_WriteByte(0x55); //发送匹配ROM的命令 for(i=0;i<8;i++) //发送64位编码 DS18B20_WriteByte(DS18B20_ROM[i]); DS18B20_WriteByte(0xBE); //读取温度 temp=DS18B20_ReadByte(); //读取的温度低位数据 temp|=DS18B20_ReadByte()<<8; //读取的温度高位数据 printf("temp1=%d.%d\n",temp>>4,temp&0xF); printf("temp2=%f\n",temp*0.0625); DelayMs(500);

win10系统下搭建FTP服务器(完成文件上传与下载)

一、环境介绍操作系统: win10 (64位)二、FTP介绍FTP (File Transfer Protocol) 可说是最古老的协议之一了,主要是用来进行文件的传输,尤其是大型文件的传输使用 FTP 更是方便。在FTP的使用当中,用户经常遇到两个概念:"下载"(Download)和"上载"(Upload)。"下载"文件就是从远程主机拷贝文件至自己的计算机上;"上载"文件就是将文件从自己的计算机中拷贝至远程主机上。用Internet 语言来说,用户可通过客户机程序向(从)远程主机上载(下载)文件。TCP/IP 协议中,FTP 标准命令 TCP 端口号为 21,Port 方式数据端口为 20。FTP 协议的任务是从一台计算机将文件传送到另一台计算机,它与这两台计算机所处的位置、联接的方式、甚至是是否使用相同的操作系统无关。假设两台计算机通过 ftp 协议对话,并且能访问 Internet, 你可以用 ftp 命令来传输文件。每种操作系统使用上有某一些细微差别,但是每种协议基本的命令结构是相同的。三、win10系统下搭建FTP服务器3.1  开启FTP服务器鼠标放在此电脑选项上,鼠标右键选择属性:进入控制面板:进入程序更改页面\启动windows自带的功能 启动FTP服务器与客户端程序功能 安装成功 进入控制面板页面\所有控制面板选项: 选择管理工具:选择Internet管理器:鼠标右键选择添加FTP站点:设置站点名称与物理路径:设置本机IP地址:设置登录的用户权限3.2 登录FTP访问文件浏览器上直接访问FTP服务器站点:在浏览器上直接下载FTP站点的内容:电脑文件管理系统里访问FTP站点:3.3 安装FileZilla FTP客户端软件登录FTP服务器下载地址:  https://download.csdn.net/download/xiaolong1126626497/193550333.4 FTP服务器设置指定用户登录要设置FTP服务器使用指定的账户登录,需要先在windows上创建一个本地的新账户或者原来电脑的本地账户,用于FTP服务器登录。3.5 linux下登录FTP服务器站点(浏览器方式)说明: 下面Linux系统以Redhat6.3为例。如果Linux系统跑在VM虚拟机环境下,想要与windows系统进行通信,需要设置VM桥接到windows当前使用的网卡即可,可以手动设置IP地址在同一个网段。比如: windows系统当前使用的WIFI方式上网,IP地址为172.16.21.69。那么在VM虚拟机里就设置桥接模式,桥接到WIFI网卡上。在虚拟机设置里也设置成桥接模式。​然后在命令行手动设置网卡IP地址:完支持ping一下windows的IP地址,测试网络是否畅通。能ping通windowsIP地址,就可以打开浏览器,直接访问FTP站点。3.6 linux系统下安装FTP软件登录FTP服务器站点3.6.1 安装FTP客户端软件红帽 6.3 系统光盘中自带 ftp 安装包,挂载红帽 6.3 光盘,找到 ftp 安装包安装即可。软件安装之后,在命令行就多了一个可用的ftp命令,用于登录FTP服务器站点。查看命令的帮助:3.6.2 FTP命令登录FTP服务器实名用户登录首先#ftp +IP(server)输入用户名(server的用户名)输入密码(server的密码)匿名用户登录#ftp +IP(server)用户名:anonymous (匿名用户固定的名字)密码:直接回车 (不用输入密码)实例:3.6.3 查看FTP命令帮助进入FTP命令行之后,输入一个?号即可看当前命令行支持的功能命令。3.6.4 文件的上传和下载文件的上传:#put  filename(上传登录之前所在目录的内容)文件的下载:#get  filename不允许下载目录,如果想操作目录,得先打包文件在登陆之前先确保当前所在目录3.6.5 退出服务器#bye #quit#exit 3.7 linux系统下安装lftp工具登录FTP服务器3.7.1 安装lftp工具3.7.2 登录FTP服务器站点如果FTP服务器支持匿名用户登录,直接输入服务器IP地址即可登录。2.7.3 文件和目录的上传上传单个文件使用put命令,用法格式: put <本地目录路径下将要上传的文件>示例:多个文件使用mput命令,用法格式: mput <本地文件1> <本地文件2> … ….示例:整个目录使用mirror命令,加上-R参数。用法格式:mirror -R <本地目录路径>示例:3.7.4 文件和目录的下载下载单个文件使用get命令,用法格式:get <服务器上的xx文件>示例:下载多个文件使用mget命令,用法格式:mget <服务器上的xx文件1> <服务器上的xx文件1> …示例:下载目录使用mirror命令,用法格式:mirror <服务器上的xx目录路径>示例:3.7.5 输入指定的用户名和密码登录FTP服务器如果访问的FTP服务器不支持匿名登录,就需要输入指定的账号密码登录.方式1: 直接登录格式: lftp 用户名:密码@ftp地址:传送端口(默认21-可以不填)示例: lftp 1126626497@qq.com:123456@192.168.2.16方式2: 使用命令行的login命令登录[wbyq@wbyq mnt]$ lftp 192.168.2.16lftp 192.168.2.16:~> login 1126626497@qq.com 1234563.8 (关闭匿名登录)windows 下创建FTP服务器3.8.1 查看当前电脑的上的账号也可以创建新的账号专门用于FTP服务器访问。3.8.2 关闭匿名账号使用普通账号登录

QT应用编程: 域名解析(域名转IP)

一、环境介绍Qt:  5.12.6操作系统:  win10 (64位)二、软件效果与功能介绍功能:  域名解析(域名转IP)三、核心代码3.1 widget.cpp#include "widget.h" #include "ui_widget.h" Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) ui->setupUi(this); Widget::~Widget() delete ui; 工程: SmartHome 日期: 2021-04-26 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 日志显示 void Widget::Log_Text_Display(QString text) QPlainTextEdit *plainTextEdit_log=ui->plainTextEdit_log; //设置光标到文本末尾 plainTextEdit_log->moveCursor(QTextCursor::End, QTextCursor::MoveAnchor); //当文本数量超出一定范围就清除 if(plainTextEdit_log->toPlainText().size()>1024*4) plainTextEdit_log->clear(); plainTextEdit_log->insertPlainText(text); //移动滚动条到底部 QScrollBar *scrollbar = plainTextEdit_log->verticalScrollBar(); if(scrollbar) scrollbar->setSliderPosition(scrollbar->maximum()); //当解析成功域名后,会调用lookedUp槽函数 void Widget::lookedUp(const QHostInfo &host) if (host.error() != QHostInfo::NoError) { Log_Text_Display(host.errorString()+"\n"); return; foreach (QHostAddress address, host.addresses()) Log_Text_Display(address.toString()+"\n"); 工程: IP_Addr_Analysis 日期: 2021-06-01 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 域名解析 void Widget::on_pushButton_ip_get_clicked() //先调用 命令nslookup解析域名,再调用QHostInfo解析,有些域名QHostInfo解析不了。 //多此一举使用QHostInfo的原因是,懒得解析字符串,QHostInfo信号里直接读取IP地址。 QProcess process; process.start(QString("nslookup %1").arg(ui->lineEdit_ip_name->text())); process.waitForFinished(5000); Log_Text_Display(process.readAll()+"\n"); QHostInfo::lookupHost(ui->lineEdit_ip_name->text(),this, SLOT(lookedUp(QHostInfo))); 工程: IP_Addr_Analysis 日期: 2021-06-01 作者: DS小龙哥 环境: win10 QT5.12.6 MinGW32 功能: 清除数据 void Widget::on_pushButton_clean_clicked() ui->plainTextEdit_log->clear(); }3.2 widget.h#ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QHostInfo> #include <QScrollBar> #include <QPlainTextEdit> #include <QProcess> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACE class Widget : public QWidget Q_OBJECT public: Widget(QWidget *parent = nullptr); ~Widget(); private slots: void lookedUp(const QHostInfo &host); void Log_Text_Display(QString text); void on_pushButton_ip_get_clicked(); void on_pushButton_clean_clicked(); private: Ui::Widget *ui; #endif // WIDGET_H

基于STM32L431设计的云端绿化管理系统(ESP8266+阿里云物联网平台)

一、环境介绍MCU: 采用意法半导体低功耗芯片 STM32L431RCT6编译软件:  Keil5 + CubeMX云平台: 采用阿里云物联网云平台完整项目源代码下载地址(不懂可以私信问): https://download.csdn.net/download/xiaolong1126626497/19272620二、功能与硬件介绍2.1 功能介绍前面的一篇文章是同样的环境,云平台采用的是腾讯物联网云平台(https://xiaolong.blog.csdn.net/article/details/117407900)这篇文章将云平台换成了阿里云物联网平台,其他硬件功能都是一样的。再次介绍一下功能:这是采用STM32L431 + ES8266设计的云端绿化管理系统,可以通过ESP8266 WIFI连接阿里云物联网平台,使用网页和阿里云的APP远程进行绿化管理,比如:实时获取光照强度、温度、湿度、远程控制水泵进行浇水灌溉,在任何地方都可以给自己种的花花草草浇水,了解周边环境情况。2.2 硬件介绍开发板采用的是小熊开发板,包括完成绿化管理系统的所有功能都是采用小熊派开发板的配套套件完成。小熊开发板板载了一个stlink调试器(就是STM32F103C8T6实现的),程序下载非常方便。串口1用来调试打印数据,ESP8266是接在串口LPUART1上的。小熊派开发板本身自带的例子程序也比较丰富,自带例子里采用的云平台是华为的物联网云平台,工程比较庞大使用了LiteOS操作系统。本文里的工程是重新编写的代码,使用裸机完成项目功能,没有跑操作系统,云平台采用阿里云平台服务器,MQTT协议和ESP8266驱动代码都是重新编写,框架、逻辑比较清晰,代码量也较少,适合初学者入门学习。相关传感器模块型号: (采用的是小熊开发板配套的E53_IA1扩展板)WIFI采用:ESP8266温湿度检测传感器采用:SHT30光照强度检测传感器采用:BH1750电机采用:微型直流电机三、阿里云物联网云平台关于阿里云物联网平台的创建与使用之前也介绍过一篇,只不过MCU采用的是STM32F103C8T6,这篇文章MCU采用的是STM32L431RCT6,属于低功耗系列,更加适合物联网领域;如果之前没有使用过阿里云物联网云平台,先参考这里学习了解一下:https://xiaolong.blog.csdn.net/article/details/1073118973.1 在阿里云物联网平台创建产品官网地址: https://iot.console.aliyun.com/lk/summary/new创建产品: 配置产品模型参数:页面最后加密方式这些选择默认 添加设备: 设备添加之后,可以一键将设备证书复制下来保存到记事本,方便后面使用;不复制也没关系,后面也可以设备信息中查看的: { "ProductKey": "a1ukQj2EnEJ", "DeviceName": "GreeningManagement", "DeviceSecret": "a5268d71d363f1bd68e708c9097fa3d2" }设备添加完成:在设备信息的页面也可以查看设备证书:添加功能属性字段:根据自己产品交互使用的数据类型进行定义:(绿化管理系统使用了温度、湿度、电机、光照强度一共4个数据字段。其中电机是读写类型,其他都是只读类型)自定义功能属性添加完毕之后就发布上线:查看物模型数据格式:后面通过MQTT协议向服务器上报数据就是这个格式可以选择导出模型文件,导出是一个json格式文件,方便设备端开发参考。 3.2  通过IoT Studio创建web可视化界面地址: https://iot.console.aliyun.com/lk/related-services之前旧版本的IoT Studio 选项是在产品页面里,现在移到控制台首页了。新建项目:新建web应用:设计WEB页面之前 先关联产品和设备:选择对应的产品进行关联:选择对应的设备进行关联:关联成功:可以更改页面名称:添加组件,设计页面:  阿里云的web页面控件非常丰富,可以根据自己需求设计好看的页面。接下来就要给每个控件配置数据源:调整仪表盘的属性:刻度字号配置完毕:有域名的可以绑定到域名:这里可以预览页面:四、登录阿里云平台测试4.1  MQTT协议登录的域名与端口号关于MQTT协议登录所需要的参数官方说明文档: https://help.aliyun.com/document_detail/140507.html?spm=a2c4g.11186623.6.571.1e417544OGPj2yMQTT登录域名的格式:${YourProductKey}.iot-as-mqtt.${YourRegionId}.aliyuncs.com其中:${YourProductKey}:请替换为设备所属产品的ProductKey${YourRegionId}:请替换为物联网平台设备所在地域代码。下面是阿里云国内的服务器地域和可用区详情: 地域名称 所在城市 Region ID 可用区数量 华北 1 青岛 cn-qingdao 2 华北 2 北京 cn-beijing 10 华北 3 张家口 cn-zhangjiakou 3 华北 5 呼和浩特 cn-huhehaote 2 华北 6 乌兰察布 cn-wulanchabu 3 华东 1 杭州 cn-hangzhou 8 华东 2 上海 cn-shanghai 8 华南 1 深圳 cn-shenzhen 6 华南 2 河源 cn-heyuan 2 华南 3 广州 cn-guangzhou 2 西南 1 成都 cn-chengdu 2 端口号是:1883经过上面的格式解释,我的阿里云服务器登录的域名就是(选择的是上海服务器):a1ukQj2EnEJ.iot-as-mqtt.cn-shanghai.aliyuncs.com域名对应的IP地址(动态解析出来的):  106.14.207.159在线解析域名网站:https://site.ip138.com/8O76VHCU7Y.iotcloud.tencentdevices.com/4.2  MQTT协议登录的ID、用户名、密码4.2.1 MQTT_ClientID固定格式:${ClientID}|securemode=${Mode},signmethod=${SignMethod}|。参数说明:${ClientId}:  设备ID,一般填设备的硬件编号。我这里就直接填当前的设备名称,后面的密码里也要填这个ID,必须一样就行。(设备名称就是创建设备的时候复制出来3个参数里的设备名称)securemode=3:TCP直连模式,无需设置SSL/TLS信息。securemode=2:TLS直连模式,需要设置SSL/TLS信息。${SignMethod}:算法类型,支持hmacmd5和hmacsha1。示例:当前我的绿化管理系统设备名称是:GreeningManagement ,选择TCP直连模式,选择hmacsha1算法类型。那么我的ClientID就是:GreeningManagement|securemode=3,signmethod=hmacsha1| 4.2.2 MQTT_UserName固定格式:${DeviceName}&${ProductKey}参数解释:${DeviceName} 是设备的名称(就是创建设备的时候复制出来3个参数里的设备名称)${ProductKey} 是设备的ProductKey(就是创建设备的时候复制出来3个参数里的ProductKey)示例:当前我的绿化管理系统设备名称是:GreeningManagement ,我的ProductKey是:a1ukQj2EnEJ那么我的UserName就是:GreeningManagement&a1ukQj2EnEJ4.2.3 MQTT_PassWord下载密码生成小工具:https://help.aliyun.com/document_detail/140507.html?spm=a2c4g.11186623.6.571.1e417544OGPj2y#section-dai-o6u-deh下载工具,运行:根据说明填充参数:说明:productKey、deviceName、deviceSecret:是设备证书信息,可在控制台设备详情页查看。clientID在4.2.1小节里已经说过了。时间戳可以省略不填。点击Generate生成密码。经过小工具生成后的密码是:9E580B36EE7E001980AF61EA09EAF85F0211C1464.3  使用MQTT客户端工具登录阿里云服务器MQTT客户端工具下载地址:https://blog.csdn.net/xiaolong1126626497/article/details/116779490根据前面获取的参数填入,登录测试: (为了保证不会断开连接,可以勾选MQTT客户端右下角的心跳包选项,保活)如果登录成功,在阿里云控制台页面上可以看到设备已经在线:如果设备能成功上线,那么就说明MQTT所需要的参数都已经填正确了,接下来就可以正常订阅、发布主题了。 4.4  主题订阅、发布测试属性上报主题与属性设置主题格式:发布主题:/sys/a1ukQj2EnEJ/GreeningManagement/thing/event/property/post上报属性消息的格式(精简格式):  {"method":"thing.event.property.post","params":{"temperature":11.1,"humidity":12.1,"illumination":13,"machine":1}}上报属性消息的格式详细格式(可以带上ID和版本号):  {"method":"thing.event.property.post","id":"1234567890","params":{"temperature":66.1,"humidity":22.1,"illumination":88,,"machine":1},"version":"1.1.1"}订阅主题:/sys/a1ukQj2EnEJ/GreeningManagement/thing/service/property/set通过MQTT客户端订阅主题、上报属性数据:  把相关的参数填正确,然后登陆,订阅、发布测试:阿里云物联网平台云端收到的数据: 地址:https://studio.iot.aliyun.com/点击页面上的的按钮,MQTT客户端可以收到下发的消息(要先订阅才能收到消息):注意:  阿里云按钮点击下发消息之后,客户端收到后要重新上报一次按钮的状态回去,不然阿里云按钮会恢复之前的状态。五、STM32代码测试STM32的代码主要分为以下几个部分:1.  ESP8266底层驱动代码:   完成ESP8266模式配置、数据发送,应答检测等底层网络接口。2.  MQTT协议代码:这是参考标准MQTT编写C语言版本MQTT协议框架代码,实现了重要的几个接口(主题订阅、主题发布、心跳包、登录MQTT服务器),底层采用ESP8266发送数据。 这个MQTT协议不是使用ESP8266本身的SDK,是根据MQTT协议自己实现的,所以如果使用其他的网卡,移植也很方便,不挑网卡设备。3.  传感器初始化代码: 完成温湿度传感器、光照强度传感器的驱动代码编写。4.  LCD屏代码:  LCD是SPI接口的,可以显示温湿度、光照强度数据。5.  main函数:  完成整个逻辑代码编写,检测阿里云平台是否有下发的指令,进行分析,完成水泵的开关控制;当温室和湿度到达某个阀值,自动控制水泵浇水,并上报给阿里云平台;主程序里1秒检测一次温湿度、光照强度、电机状态主动上报给阿里云平台;在设备端按下按键(模拟现场实体开关)也可以控制水泵浇水或者关闭,这些状态都会实时上报给云平台。程序的模板是使用CubeMX生成的。5.1  main.c代码 ****************************************************************************** * @file : main.c * @brief : Main program body ****************************************************************************** ** This notice applies to any and all portions of this file * that are not between comment pairs USER CODE BEGIN and * USER CODE END. Other portions of this file, whether * inserted by the user or by software development tools * are owned by their respective copyright owners. * COPYRIGHT(c) 2019 STMicroelectronics * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. Neither the name of STMicroelectronics nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ****************************************************************************** /* Includes ------------------------------------------------------------------*/ #include "main.h" #include "stm32l4xx_hal.h" #include "i2c.h" #include "usart.h" #include "gpio.h" #include "E53_IA1.h" #include "lcd.h" #include "spi.h" #include "mqtt.h" #include "esp8266.h" /* USER CODE BEGIN Includes */ #include "stdio.h" /* USER CODE END Includes */ void SystemClock_Config(void); #define ESP8266_WIFI_AP_SSID "CMCC-Cqvn" //将要连接的路由器名称 --不要出现中文、空格等特殊字符 #define ESP8266_AP_PASSWORD "99pu58cb" //将要连接的路由器密码 //阿里云物联网服务器的设备信息 #define MQTT_ClientID "GreeningManagement|securemode=3,signmethod=hmacsha1|" #define MQTT_UserName "GreeningManagement&a1ukQj2EnEJ" #define MQTT_PassWord "9E580B36EE7E001980AF61EA09EAF85F0211C146" //订阅与发布的主题 #define SET_TOPIC "/sys/a1ukQj2EnEJ/GreeningManagement/thing/service/property/set" //订阅 #define POST_TOPIC "/sys/a1ukQj2EnEJ/GreeningManagement/thing/event/property/post" //发布 //保存温湿度、光照强度 E53_IA1_Data_TypeDef E53_IA1_Data; //显示文本 char lcd_text_str[50]; UART_HandleTypeDef at_usart; //低功耗串口初始化 int32_t at_usart_init(void) at_usart.Instance = LPUART1; at_usart.Init.BaudRate = 115200; at_usart.Init.WordLength = UART_WORDLENGTH_8B; at_usart.Init.StopBits = UART_STOPBITS_1; at_usart.Init.Parity = UART_PARITY_NONE; at_usart.Init.HwFlowCtl = UART_HWCONTROL_NONE; at_usart.Init.Mode = UART_MODE_RX | UART_MODE_TX; if(HAL_UART_Init(&at_usart) != HAL_OK) _Error_Handler(__FILE__, __LINE__); // __HAL_UART_CLEAR_FLAG(usart, UART_FLAG_TC); __HAL_UART_ENABLE_IT(&at_usart, UART_IT_IDLE); __HAL_UART_ENABLE_IT(&at_usart, UART_IT_RXNE); HAL_NVIC_EnableIRQ(LPUART1_IRQn); //使能USART1中断通道 HAL_NVIC_SetPriority(LPUART1_IRQn, 3, 3); //抢占优先级3,子优先级3 return 0; unsigned char ESP8266_RecvBuf[MAX_RECV_CNT]; unsigned int ESP8266_Recv_cnt=0; unsigned int ESP8266_Recv_flag=0; void LPUART1_IRQHandler() //接收到数据 if(__HAL_UART_GET_FLAG(&at_usart, UART_FLAG_RXNE) != RESET) if(ESP8266_Recv_cnt<MAX_RECV_CNT-1) ESP8266_RecvBuf[ESP8266_Recv_cnt++] = (uint8_t)(at_usart.Instance->RDR & 0x00FF); ESP8266_Recv_flag=1; else if (__HAL_UART_GET_FLAG(&at_usart, UART_FLAG_IDLE) != RESET) __HAL_UART_CLEAR_IDLEFLAG(&at_usart); ESP8266_Recv_flag=1; void AT_SendData(unsigned char *p,unsigned int len) int i=0; for(i=0;i<len;i++) while((LPUART1->ISR & 0X40) == 0); //循环发送,直到发送完毕 LPUART1->TDR = p[i]; char mqtt_message[200]; int main(void) int i=0; int cnt=0; int motor_state=0; HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); MX_SPI2_Init(); MX_USART1_UART_Init(); at_usart_init(); //初始化硬件 STM32L431RC_BearPiBH1750_I2C1\STM32L431RC_BearPiBH1750_I2C1.axf: Error: L6218E: Undefined symbol printf (referred from main.o). Init_E53_IA1(); LCD_Init(); LCD_Clear(BLACK);//清屏为黑色 LCD_ShowString(20, 00, 240, 32, 32, "Init ESP8266");//显示字符串,字体大小32*32 if(ESP8266_Init()) printf("ESP8266硬件检测错误.\n"); LCD_Clear(BLACK);//清屏为黑色 LCD_ShowString(0, 00, 240, 32, 32, "ESP8266 ERROR");//显示字符串,字体大小32*32 LCD_Clear(BLACK);//清屏为黑色 LCD_ShowString(20, 00, 240, 32, 32, "ESP8266 OK");//显示字符串,字体大小32*32 printf("准备连接到指定的服务器.\n"); //非加密端口 printf("WIFI:%d\r\n",ESP8266_STA_TCP_Client_Mode(ESP8266_WIFI_AP_SSID,ESP8266_AP_PASSWORD,"a1ukQj2EnEJ.iot-as-mqtt.cn-shanghai.aliyuncs.com",1883,1)); //2. MQTT协议初始化 MQTT_Init(); //3. 连接阿里云IOT服务器 while(MQTT_Connect(MQTT_ClientID,MQTT_UserName,MQTT_PassWord)) printf("服务器连接失败,正在重试...\n"); HAL_Delay(500); printf("服务器连接成功.\n"); //3. 订阅主题 if(MQTT_SubscribeTopic(SET_TOPIC,0,1)) printf("主题订阅失败.\n"); printf("主题订阅成功.\n"); while (1) if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin)==GPIO_PIN_RESET)//查询按键KEY1低电平 HAL_Delay(10);//消抖 if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin)==GPIO_PIN_RESET)//查询按键KEY1低电平 HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);//亮 //补光灯亮 HAL_GPIO_WritePin(IA1_Light_GPIO_Port, IA1_Light_Pin, GPIO_PIN_SET); //电机转 HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_SET); motor_state=1; if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin)==GPIO_PIN_RESET)//查询按键KEY2低电平 HAL_Delay(10);//消抖 if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin)==GPIO_PIN_RESET)//查询按键KEY2低电平 HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_RESET);//灭 //补光灯灭 HAL_GPIO_WritePin(IA1_Light_GPIO_Port, IA1_Light_Pin, GPIO_PIN_RESET); //电机停 HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_RESET); motor_state=0; cnt++; HAL_Delay(10); if(cnt>=100) cnt=0; E53_IA1_Read_Data(); printf("光照强度:%.1f %%\r\n", E53_IA1_Data.Lux); printf("湿度:%.1f %%\r\n",E53_IA1_Data.Humidity); printf("温度:%.1f ℃\r\n", E53_IA1_Data.Temperature); sprintf(lcd_text_str,"L: %0.1f %%",E53_IA1_Data.Lux); LCD_ShowString(40, 50+10+32*1, 240, 32, 32,lcd_text_str); sprintf(lcd_text_str,"H: %.1f %%",E53_IA1_Data.Humidity); LCD_ShowString(40, 50+10+32*2, 240, 32, 32,lcd_text_str); sprintf(lcd_text_str,"T: %.1f C",E53_IA1_Data.Temperature); LCD_ShowString(40, 50+10+32*3, 240, 32, 32,lcd_text_str); //切换引脚的状态 HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin); //上传数据 sprintf(mqtt_message,"{\"method\":\"thing.event.property.post\",\"id\":\"1234567890\",\"params\":{\"temperature\":%f,\"humidity\":%f,\"illumination\":%f,\"machine\":%d},\"version\":\"1.1.1\"}", E53_IA1_Data.Temperature,E53_IA1_Data.Humidity,E53_IA1_Data.Lux,motor_state); MQTT_PublishData(POST_TOPIC,mqtt_message,0); //根据湿度自动灌溉 if((int)E53_IA1_Data.Humidity<50) //小于50自动灌溉 printf("自动灌溉....\n"); motor_state=1; //电机状态更新 //电机转 HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_SET); //接收到数据 if(ESP8266_Recv_flag) //如果是下发了属性,判断是开锁还是关锁 if(ESP8266_Recv_cnt>5) ESP8266_RecvBuf[ESP8266_Recv_cnt]='\0'; //使用字符串查找函数 if(strstr((char*)&ESP8266_RecvBuf[5],"\"machine\":1")) motor_state=1; //电机状态更新 //电机转 HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_SET); printf("开启电机...\n"); else if(strstr((char*)&ESP8266_RecvBuf[5],"\"machine\":0")) //电机停 HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_RESET); motor_state=0; printf("关闭电机...\n"); for(i=0;i<ESP8266_Recv_cnt;i++)printf("%c",ESP8266_RecvBuf[i]); ESP8266_Recv_cnt=0; ESP8266_Recv_flag=0; void SystemClock_Config(void) RCC_OscInitTypeDef RCC_OscInitStruct; RCC_ClkInitTypeDef RCC_ClkInitStruct; RCC_PeriphCLKInitTypeDef PeriphClkInit; /**Initializes the CPU, AHB and APB busses clocks RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_MSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue = 16; RCC_OscInitStruct.MSIState = RCC_MSI_ON; RCC_OscInitStruct.MSICalibrationValue = 0; RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_6; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_MSI; RCC_OscInitStruct.PLL.PLLM = 1; RCC_OscInitStruct.PLL.PLLN = 40; RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) _Error_Handler(__FILE__, __LINE__); /**Initializes the CPU, AHB and APB busses clocks RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) _Error_Handler(__FILE__, __LINE__); PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_I2C1; PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; PeriphClkInit.I2c1ClockSelection = RCC_I2C1CLKSOURCE_HSI; if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) _Error_Handler(__FILE__, __LINE__); /**Configure the main internal regulator output voltage if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) _Error_Handler(__FILE__, __LINE__); /**Configure the Systick interrupt time HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000); /**Configure the Systick HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK); /* SysTick_IRQn interrupt configuration */ HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0); /* USER CODE BEGIN 4 */ /* USER CODE END 4 */ * @brief This function is executed in case of error occurrence. * @param file: The file name as string. * @param line: The line in file as a number. * @retval None void _Error_Handler(char *file, int line) /* USER CODE BEGIN Error_Handler_Debug */ /* User can add his own implementation to report the HAL error return state */ while(1) /* USER CODE END Error_Handler_Debug */ #ifdef USE_FULL_ASSERT * @brief Reports the name of the source file and the source line number * where the assert_param error has occurred. * @param file: pointer to the source file name * @param line: assert_param error line source number * @retval None void assert_failed(uint8_t* file, uint32_t line) /* USER CODE BEGIN 6 */ /* User can add his own implementation to report the file name and line number, tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */ /* USER CODE END 6 */ #endif /* USE_FULL_ASSERT */ * @} * @} /************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/5.2  mqtt.c代码#include "mqtt.h" u8 *mqtt_rxbuf; u8 *mqtt_txbuf; u16 mqtt_rxlen; u16 mqtt_txlen; u8 _mqtt_txbuf[256];//发送数据缓存区 u8 _mqtt_rxbuf[256];//接收数据缓存区 typedef enum //名字 值 报文流动方向 描述 M_RESERVED1 =0 , // 禁止 保留 M_CONNECT , // 客户端到服务端 客户端请求连接服务端 M_CONNACK , // 服务端到客户端 连接报文确认 M_PUBLISH , // 两个方向都允许 发布消息 M_PUBACK , // 两个方向都允许 QoS 1消息发布收到确认 M_PUBREC , // 两个方向都允许 发布收到(保证交付第一步) M_PUBREL , // 两个方向都允许 发布释放(保证交付第二步) M_PUBCOMP , // 两个方向都允许 QoS 2消息发布完成(保证交互第三步) M_SUBSCRIBE , // 客户端到服务端 客户端订阅请求 M_SUBACK , // 服务端到客户端 订阅请求报文确认 M_UNSUBSCRIBE , // 客户端到服务端 客户端取消订阅请求 M_UNSUBACK , // 服务端到客户端 取消订阅报文确认 M_PINGREQ , // 客户端到服务端 心跳请求 M_PINGRESP , // 服务端到客户端 心跳响应 M_DISCONNECT , // 客户端到服务端 客户端断开连接 M_RESERVED2 , // 禁止 保留 }_typdef_mqtt_message; //连接成功服务器回应 20 02 00 00 //客户端主动断开连接 e0 00 const u8 parket_connetAck[] = {0x20,0x02,0x00,0x00}; const u8 parket_disconnet[] = {0xe0,0x00}; const u8 parket_heart[] = {0xc0,0x00}; const u8 parket_heart_reply[] = {0xc0,0x00}; const u8 parket_subAck[] = {0x90,0x03}; void MQTT_Init(void) //缓冲区赋值 mqtt_rxbuf = _mqtt_rxbuf; mqtt_rxlen = sizeof(_mqtt_rxbuf); mqtt_txbuf = _mqtt_txbuf; mqtt_txlen = sizeof(_mqtt_txbuf); memset(mqtt_rxbuf,0,mqtt_rxlen); memset(mqtt_txbuf,0,mqtt_txlen); // //无条件先主动断开 // MQTT_Disconnect(); // HAL_Delay(100); // MQTT_Disconnect(); // HAL_Delay(100); 函数功能: 登录服务器 函数返回值: 0表示成功 1表示失败 u8 MQTT_Connect(char *ClientID,char *Username,char *Password) u8 i,j; int ClientIDLen = strlen(ClientID); int UsernameLen = strlen(Username); int PasswordLen = strlen(Password); int DataLen; mqtt_txlen=0; //可变报头+Payload 每个字段包含两个字节的长度标识 DataLen = 10 + (ClientIDLen+2) + (UsernameLen+2) + (PasswordLen+2); //固定报头 //控制报文类型 mqtt_txbuf[mqtt_txlen++] = 0x10; //MQTT Message Type CONNECT //剩余长度(不包括固定头部) u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); //可变报头 //协议名 mqtt_txbuf[mqtt_txlen++] = 0; // Protocol Name Length MSB mqtt_txbuf[mqtt_txlen++] = 4; // Protocol Name Length LSB mqtt_txbuf[mqtt_txlen++] = 'M'; // ASCII Code for M mqtt_txbuf[mqtt_txlen++] = 'Q'; // ASCII Code for Q mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T //协议级别 mqtt_txbuf[mqtt_txlen++] = 4; // MQTT Protocol version = 4 对于 3.1.1 版协议,协议级别字段的值是 4(0x04) //连接标志 mqtt_txbuf[mqtt_txlen++] = 0xc2; // conn flags mqtt_txbuf[mqtt_txlen++] = 0; // Keep-alive Time Length MSB mqtt_txbuf[mqtt_txlen++] = 100; // Keep-alive Time Length LSB 100S心跳包 保活时间 mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen);// Client ID length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen);// Client ID length LSB memcpy(&mqtt_txbuf[mqtt_txlen],ClientID,ClientIDLen); mqtt_txlen += ClientIDLen; if(UsernameLen > 0) mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen); //username length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen); //username length LSB memcpy(&mqtt_txbuf[mqtt_txlen],Username,UsernameLen); mqtt_txlen += UsernameLen; if(PasswordLen > 0) mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen); //password length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen); //password length LSB memcpy(&mqtt_txbuf[mqtt_txlen],Password,PasswordLen); mqtt_txlen += PasswordLen; memset(mqtt_rxbuf,0,mqtt_rxlen); ESP8266_Recv_flag=0; ESP8266_Recv_cnt=0; MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); HAL_Delay(200); memcpy((char *)mqtt_rxbuf,ESP8266_RecvBuf,ESP8266_Recv_cnt); for(i=0;i<ESP8266_Recv_cnt;i++)printf("%#x ",ESP8266_RecvBuf[i]); //CONNECT if(mqtt_rxbuf[0]==parket_connetAck[0] && mqtt_rxbuf[1]==parket_connetAck[1]) //连接成功 return 0;//连接成功 return 1; 函数功能: MQTT订阅/取消订阅数据打包函数 函数参数: topic 主题 qos 消息等级 0:最多分发一次 1: 至少分发一次 2: 仅分发一次 whether 订阅/取消订阅请求包 (1表示订阅,0表示取消订阅) 返回值: 0表示成功 1表示失败 u8 MQTT_SubscribeTopic(char *topic,u8 qos,u8 whether) u8 i,j; mqtt_txlen=0; int topiclen = strlen(topic); int DataLen = 2 + (topiclen+2) + (whether?1:0);//可变报头的长度(2字节)加上有效载荷的长度 //固定报头 //控制报文类型 if(whether)mqtt_txbuf[mqtt_txlen++] = 0x82; //消息类型和标志订阅 else mqtt_txbuf[mqtt_txlen++] = 0xA2; //取消订阅 //剩余长度 u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); //可变报头 mqtt_txbuf[mqtt_txlen++] = 0; //消息标识符 MSB mqtt_txbuf[mqtt_txlen++] = 0x0A; //消息标识符 LSB //有效载荷 mqtt_txbuf[mqtt_txlen++] = BYTE1(topiclen);//主题长度 MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(topiclen);//主题长度 LSB memcpy(&mqtt_txbuf[mqtt_txlen],topic,topiclen); mqtt_txlen += topiclen; if(whether) mqtt_txbuf[mqtt_txlen++] = qos;//QoS级别 ESP8266_Recv_flag=0; ESP8266_Recv_cnt=0; MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); HAL_Delay(200); memcpy((char *)mqtt_rxbuf,ESP8266_RecvBuf,ESP8266_Recv_cnt); for(i=0;i<ESP8266_Recv_cnt;i++)printf("%#x ",ESP8266_RecvBuf[i]); if(mqtt_rxbuf[0]==parket_subAck[0] && mqtt_rxbuf[1]==parket_subAck[1]) //订阅成功 return 0;//订阅成功 return 1; //失败 //MQTT发布数据打包函数 //topic 主题 //message 消息 //qos 消息等级 u8 MQTT_PublishData(char *topic, char *message, u8 qos) int topicLength = strlen(topic); int messageLength = strlen(message); static u16 id=0; int DataLen; mqtt_txlen=0; //有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度 //QOS为0时没有标识符 //数据长度 主题名 报文标识符 有效载荷 if(qos) DataLen = (2+topicLength) + 2 + messageLength; else DataLen = (2+topicLength) + messageLength; //固定报头 //控制报文类型 mqtt_txbuf[mqtt_txlen++] = 0x30; // MQTT Message Type PUBLISH //剩余长度 u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength);//主题长度MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength);//主题长度LSB memcpy(&mqtt_txbuf[mqtt_txlen],topic,topicLength);//拷贝主题 mqtt_txlen += topicLength; //报文标识符 if(qos) mqtt_txbuf[mqtt_txlen++] = BYTE1(id); mqtt_txbuf[mqtt_txlen++] = BYTE0(id); id++; memcpy(&mqtt_txbuf[mqtt_txlen],message,messageLength); mqtt_txlen += messageLength; MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); return mqtt_txlen; void MQTT_SentHeart(void) MQTT_SendBuf((u8 *)parket_heart,sizeof(parket_heart)); void MQTT_Disconnect(void) MQTT_SendBuf((u8 *)parket_disconnet,sizeof(parket_disconnet)); void MQTT_SendBuf(u8 *buf,u16 len) AT_SendData(buf,len); } 5.3 设备运行效果串口打印调试数据: 连接成功六、阿里云生活物联网平台官网首页:https://help.aliyun.com/product/123207.html?spm=a2c4g.11186623.6.540.5f956897WfxgIa生活物联网平台是阿里云IoT针对生活领域推出的物联网平台,以解决家电智能化的问题。生活物联网平台提供了设备接入能力,有公版APP可以直接开发使用;下篇文章再讲解生活物联网平台使用示例。

基于STM32L431设计的云端绿化管理系统(ESP8266+腾讯物联网云平台)

一、环境介绍MCU: 采用意法半导体低功耗芯片 STM32L431RCT6编译软件:  Keil5 + CubeMX云平台: 采用腾讯物联网云平台工程完整源代码与配套资料下载地址:https://download.csdn.net/download/xiaolong1126626497/19246016二、功能与硬件介绍2.1 功能介绍这是采用STM32L431 + ES8266设计的云端绿化管理系统,可以通过ESP8266 WIFI连接腾讯云物联网平台,使用微信小程序远程进行绿化管理,比如:实时获取光照强度、温度、湿度、远程控制水泵进行浇水灌溉,在任何地方都可以给自己种的花花草草浇水,了解周边环境情况。 腾讯物联网平台支持微信小程序+WIFI一键配网,想学习如何配网的请看这里:https://blog.csdn.net/xiaolong1126626497/article/details/1173394342.2 硬件介绍开发板采用的是小熊开发板,包括完成绿化管理系统的所有功能都是采用小熊派开发板的配套套件完成。小熊开发板板载了一个stlink调试器(就是STM32F103C8T6实现的),程序下载非常方便。串口1用来调试打印数据,ESP8266是接在串口LPUART1上的。小熊派开发板本身自带的例子程序也比较丰富,自带例子里采用的云平台是华为的物联网云平台,工程比较庞大使用了LiteOS操作系统。本文里的工程是重新编写的代码,使用裸机完成项目功能,没有跑操作系统,MQTT协议和ESP8266驱动代码都是重新编写,框架、逻辑比较清晰,代码量也较少,适合初学者入门学习。相关传感器模块型号: (采用的是小熊开发板配套的E53_IA1扩展板)WIFI采用:ESP8266温湿度检测传感器采用:SHT30光照强度检测传感器采用:BH1750电机采用:微型直流电机三、腾讯物联网云平台关于腾讯物联网平台的创建已经介绍很多篇了,如果之前没有使用过腾讯物联网云平台,先参考这里学习了解一下:https://blog.csdn.net/xiaolong1126626497/article/details/116902653下面就截图介绍一下云端绿化管理系统用到的产品功能。用到的功能属性:小程序面板配置:手机微信小程序运行效果:串口打印的提示:四、STM32源代码STM32的代码主要分为以下几个部分:1.  ESP8266底层驱动代码:   完成ESP8266模式配置、数据发送,应答检测等底层网络接口。2.  MQTT协议代码:这是参考标准MQTT编写C语言版本MQTT协议框架代码,实现了重要的几个接口(主题订阅、主题发布、心跳包、登录MQTT服务器),底层采用ESP8266发送数据。 这个MQTT协议不是使用ESP8266本身的SDK,是根据MQTT协议自己实现的,所以如果使用其他的网卡,移植也很方便,不挑网卡设备。3.  传感器初始化代码: 完成温湿度传感器、光照强度传感器的驱动代码编写。4.  LCD屏代码:  LCD是SPI接口的,可以显示温湿度、光照强度数据。5.  main函数:  完成整个逻辑代码编写,检测微信小程序是否有下发的指令,进行分析,完成水泵的开关控制;当温室和湿度到达某个阀值,自动控制水泵浇水,并上报给微信小程序;主程序里1秒检测一次温湿度、光照强度、电机状态主动上报给微信小程序;在设备端按下按键(模拟现场实体开关)也可以控制水泵浇水或者关闭,这些状态都会实时上报给云平台,微信小程序。程序的模板是使用CubeMX生成的。4.1  main.c代码:  逻辑代码 ****************************************************************************** * @file : main.c * @brief : Main program body ****************************************************************************** ** This notice applies to any and all portions of this file * that are not between comment pairs USER CODE BEGIN and * USER CODE END. Other portions of this file, whether * inserted by the user or by software development tools * are owned by their respective copyright owners. * COPYRIGHT(c) 2019 STMicroelectronics * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. Neither the name of STMicroelectronics nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ****************************************************************************** /* Includes ------------------------------------------------------------------*/ #include "main.h" #include "stm32l4xx_hal.h" #include "i2c.h" #include "usart.h" #include "gpio.h" #include "E53_IA1.h" #include "lcd.h" #include "spi.h" #include "mqtt.h" #include "esp8266.h" /* USER CODE BEGIN Includes */ #include "stdio.h" /* USER CODE END Includes */ void SystemClock_Config(void); #define ESP8266_WIFI_AP_SSID "CMCC-Cqvn" //将要连接的路由器名称 --不要出现中文、空格等特殊字符 #define ESP8266_AP_PASSWORD "99pu58cb" //将要连接的路由器密码 //腾讯物联网服务器的设备信息 #define MQTT_ClientID "6142CX41XESmartAgriculture" #define MQTT_UserName "6142CX41XESmartAgriculture;12010126;HUA2G;1624271589" #define MQTT_PassWord "a8aadebe9721f70e6f9e14fe56ff1d2b5cac9625fa1f96af2f0e0098597fe78b;hmacsha256" //订阅与发布的主题 #define SET_TOPIC "$thing/down/property/6142CX41XE/SmartAgriculture" //订阅 #define POST_TOPIC "$thing/up/property/6142CX41XE/SmartAgriculture" //发布 //保存温湿度、光照强度 E53_IA1_Data_TypeDef E53_IA1_Data; //显示文本 char lcd_text_str[50]; UART_HandleTypeDef at_usart; //低功耗串口初始化 int32_t at_usart_init(void) at_usart.Instance = LPUART1; at_usart.Init.BaudRate = 115200; at_usart.Init.WordLength = UART_WORDLENGTH_8B; at_usart.Init.StopBits = UART_STOPBITS_1; at_usart.Init.Parity = UART_PARITY_NONE; at_usart.Init.HwFlowCtl = UART_HWCONTROL_NONE; at_usart.Init.Mode = UART_MODE_RX | UART_MODE_TX; if(HAL_UART_Init(&at_usart) != HAL_OK) _Error_Handler(__FILE__, __LINE__); // __HAL_UART_CLEAR_FLAG(usart, UART_FLAG_TC); __HAL_UART_ENABLE_IT(&at_usart, UART_IT_IDLE); __HAL_UART_ENABLE_IT(&at_usart, UART_IT_RXNE); HAL_NVIC_EnableIRQ(LPUART1_IRQn); //使能USART1中断通道 HAL_NVIC_SetPriority(LPUART1_IRQn, 3, 3); //抢占优先级3,子优先级3 return 0; unsigned char ESP8266_RecvBuf[MAX_RECV_CNT]; unsigned int ESP8266_Recv_cnt=0; unsigned int ESP8266_Recv_flag=0; void LPUART1_IRQHandler() //接收到数据 if(__HAL_UART_GET_FLAG(&at_usart, UART_FLAG_RXNE) != RESET) if(ESP8266_Recv_cnt<MAX_RECV_CNT-1) ESP8266_RecvBuf[ESP8266_Recv_cnt++] = (uint8_t)(at_usart.Instance->RDR & 0x00FF); ESP8266_Recv_flag=1; else if (__HAL_UART_GET_FLAG(&at_usart, UART_FLAG_IDLE) != RESET) __HAL_UART_CLEAR_IDLEFLAG(&at_usart); ESP8266_Recv_flag=1; void AT_SendData(unsigned char *p,unsigned int len) int i=0; for(i=0;i<len;i++) while((LPUART1->ISR & 0X40) == 0); //循环发送,直到发送完毕 LPUART1->TDR = p[i]; char mqtt_message[200]; int main(void) int i=0; int cnt=0; int motor_state=0; HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); MX_SPI2_Init(); MX_USART1_UART_Init(); at_usart_init(); //初始化硬件 STM32L431RC_BearPiBH1750_I2C1\STM32L431RC_BearPiBH1750_I2C1.axf: Error: L6218E: Undefined symbol printf (referred from main.o). Init_E53_IA1(); LCD_Init(); LCD_Clear(BLACK);//清屏为黑色 LCD_ShowString(0, 00, 240, 32, 32, "Init ESP8266");//显示字符串,字体大小32*32 if(ESP8266_Init()) printf("ESP8266硬件检测错误.\n"); LCD_Clear(BLACK);//清屏为黑色 LCD_ShowString(0, 00, 240, 32, 32, "ESP8266 ERROR");//显示字符串,字体大小32*32 LCD_Clear(BLACK);//清屏为黑色 LCD_ShowString(0, 00, 240, 32, 32, "ESP8266 OK");//显示字符串,字体大小32*32 printf("准备连接到指定的服务器.\n"); //非加密端口 printf("WIFI:%d\r\n",ESP8266_STA_TCP_Client_Mode(ESP8266_WIFI_AP_SSID,ESP8266_AP_PASSWORD,"106.55.124.154",1883,1)); //2. MQTT协议初始化 MQTT_Init(); //3. 连接腾讯云IOT服务器 while(MQTT_Connect(MQTT_ClientID,MQTT_UserName,MQTT_PassWord)) printf("服务器连接失败,正在重试...\n"); HAL_Delay(500); printf("服务器连接成功.\n"); //3. 订阅主题 if(MQTT_SubscribeTopic(SET_TOPIC,0,1)) printf("主题订阅失败.\n"); printf("主题订阅成功.\n"); while (1) if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin)==GPIO_PIN_RESET)//查询按键KEY1低电平 HAL_Delay(10);//消抖 if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin)==GPIO_PIN_RESET)//查询按键KEY1低电平 HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);//亮 //补光灯亮 HAL_GPIO_WritePin(IA1_Light_GPIO_Port, IA1_Light_Pin, GPIO_PIN_SET); //电机转 HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_SET); motor_state=1; if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin)==GPIO_PIN_RESET)//查询按键KEY2低电平 HAL_Delay(10);//消抖 if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin)==GPIO_PIN_RESET)//查询按键KEY2低电平 HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_RESET);//灭 //补光灯灭 HAL_GPIO_WritePin(IA1_Light_GPIO_Port, IA1_Light_Pin, GPIO_PIN_RESET); //电机停 HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_RESET); motor_state=0; cnt++; HAL_Delay(10); if(cnt>=100) cnt=0; E53_IA1_Read_Data(); printf("光照强度:%d %%\r\n", (int)E53_IA1_Data.Lux); printf("湿度:%d %%\r\n",(int)E53_IA1_Data.Humidity); printf("温度:%d ℃\r\n", (int)E53_IA1_Data.Temperature); sprintf(lcd_text_str,"L: %d %%",(int)E53_IA1_Data.Lux); LCD_ShowString(40, 50+10+32*1, 240, 32, 32,lcd_text_str); sprintf(lcd_text_str,"H: %d %%",(int)E53_IA1_Data.Humidity); LCD_ShowString(40, 50+10+32*2, 240, 32, 32,lcd_text_str); sprintf(lcd_text_str,"T: %d C",(int)E53_IA1_Data.Temperature); LCD_ShowString(40, 50+10+32*3, 240, 32, 32,lcd_text_str); //切换引脚的状态 HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin); //上传数据 sprintf(mqtt_message,"{\"method\":\"report\",\"clientToken\":\"123\",\"params\":{\"temperature\":%d,\"humidity\":%d,\"machine\":%d,\"illumination\":%d}}", (int)E53_IA1_Data.Temperature,(int)E53_IA1_Data.Humidity,motor_state,(int)E53_IA1_Data.Lux); MQTT_PublishData(POST_TOPIC,mqtt_message,0); //根据湿度自动灌溉 if((int)E53_IA1_Data.Humidity<50) //小于50自动灌溉 printf("自动灌溉....\n"); motor_state=1; //电机状态更新 //电机转 HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_SET); //接收到数据 if(ESP8266_Recv_flag) //如果是下发了属性,判断是开锁还是关锁 if(ESP8266_Recv_cnt>5) ESP8266_RecvBuf[ESP8266_Recv_cnt]='\0'; //使用字符串查找函数 if(strstr((char*)&ESP8266_RecvBuf[5],"\"machine\":1")) motor_state=1; //电机状态更新 //电机转 HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_SET); printf("开启电机...\n"); else if(strstr((char*)&ESP8266_RecvBuf[5],"\"machine\":0")) //电机停 HAL_GPIO_WritePin(IA1_Motor_GPIO_Port, IA1_Motor_Pin, GPIO_PIN_RESET); motor_state=0; printf("关闭电机...\n"); for(i=0;i<ESP8266_Recv_cnt;i++)printf("%c",ESP8266_RecvBuf[i]); ESP8266_Recv_cnt=0; ESP8266_Recv_flag=0; void SystemClock_Config(void) RCC_OscInitTypeDef RCC_OscInitStruct; RCC_ClkInitTypeDef RCC_ClkInitStruct; RCC_PeriphCLKInitTypeDef PeriphClkInit; /**Initializes the CPU, AHB and APB busses clocks RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_MSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue = 16; RCC_OscInitStruct.MSIState = RCC_MSI_ON; RCC_OscInitStruct.MSICalibrationValue = 0; RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_6; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_MSI; RCC_OscInitStruct.PLL.PLLM = 1; RCC_OscInitStruct.PLL.PLLN = 40; RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) _Error_Handler(__FILE__, __LINE__); /**Initializes the CPU, AHB and APB busses clocks RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) _Error_Handler(__FILE__, __LINE__); PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_I2C1; PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; PeriphClkInit.I2c1ClockSelection = RCC_I2C1CLKSOURCE_HSI; if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) _Error_Handler(__FILE__, __LINE__); /**Configure the main internal regulator output voltage if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) _Error_Handler(__FILE__, __LINE__); /**Configure the Systick interrupt time HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000); /**Configure the Systick HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK); /* SysTick_IRQn interrupt configuration */ HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0); /* USER CODE BEGIN 4 */ /* USER CODE END 4 */ * @brief This function is executed in case of error occurrence. * @param file: The file name as string. * @param line: The line in file as a number. * @retval None void _Error_Handler(char *file, int line) /* USER CODE BEGIN Error_Handler_Debug */ /* User can add his own implementation to report the HAL error return state */ while(1) /* USER CODE END Error_Handler_Debug */ #ifdef USE_FULL_ASSERT * @brief Reports the name of the source file and the source line number * where the assert_param error has occurred. * @param file: pointer to the source file name * @param line: assert_param error line source number * @retval None void assert_failed(uint8_t* file, uint32_t line) /* USER CODE BEGIN 6 */ /* User can add his own implementation to report the file name and line number, tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */ /* USER CODE END 6 */ #endif /* USE_FULL_ASSERT */ * @} * @} /************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/4.2  mqtt.c : MQTT协议代码#include "mqtt.h" u8 *mqtt_rxbuf; u8 *mqtt_txbuf; u16 mqtt_rxlen; u16 mqtt_txlen; u8 _mqtt_txbuf[256];//发送数据缓存区 u8 _mqtt_rxbuf[256];//接收数据缓存区 typedef enum //名字 值 报文流动方向 描述 M_RESERVED1 =0 , // 禁止 保留 M_CONNECT , // 客户端到服务端 客户端请求连接服务端 M_CONNACK , // 服务端到客户端 连接报文确认 M_PUBLISH , // 两个方向都允许 发布消息 M_PUBACK , // 两个方向都允许 QoS 1消息发布收到确认 M_PUBREC , // 两个方向都允许 发布收到(保证交付第一步) M_PUBREL , // 两个方向都允许 发布释放(保证交付第二步) M_PUBCOMP , // 两个方向都允许 QoS 2消息发布完成(保证交互第三步) M_SUBSCRIBE , // 客户端到服务端 客户端订阅请求 M_SUBACK , // 服务端到客户端 订阅请求报文确认 M_UNSUBSCRIBE , // 客户端到服务端 客户端取消订阅请求 M_UNSUBACK , // 服务端到客户端 取消订阅报文确认 M_PINGREQ , // 客户端到服务端 心跳请求 M_PINGRESP , // 服务端到客户端 心跳响应 M_DISCONNECT , // 客户端到服务端 客户端断开连接 M_RESERVED2 , // 禁止 保留 }_typdef_mqtt_message; //连接成功服务器回应 20 02 00 00 //客户端主动断开连接 e0 00 const u8 parket_connetAck[] = {0x20,0x02,0x00,0x00}; const u8 parket_disconnet[] = {0xe0,0x00}; const u8 parket_heart[] = {0xc0,0x00}; const u8 parket_heart_reply[] = {0xc0,0x00}; const u8 parket_subAck[] = {0x90,0x03}; void MQTT_Init(void) //缓冲区赋值 mqtt_rxbuf = _mqtt_rxbuf; mqtt_rxlen = sizeof(_mqtt_rxbuf); mqtt_txbuf = _mqtt_txbuf; mqtt_txlen = sizeof(_mqtt_txbuf); memset(mqtt_rxbuf,0,mqtt_rxlen); memset(mqtt_txbuf,0,mqtt_txlen); // //无条件先主动断开 // MQTT_Disconnect(); // HAL_Delay(100); // MQTT_Disconnect(); // HAL_Delay(100); 函数功能: 登录服务器 函数返回值: 0表示成功 1表示失败 u8 MQTT_Connect(char *ClientID,char *Username,char *Password) u8 i,j; int ClientIDLen = strlen(ClientID); int UsernameLen = strlen(Username); int PasswordLen = strlen(Password); int DataLen; mqtt_txlen=0; //可变报头+Payload 每个字段包含两个字节的长度标识 DataLen = 10 + (ClientIDLen+2) + (UsernameLen+2) + (PasswordLen+2); //固定报头 //控制报文类型 mqtt_txbuf[mqtt_txlen++] = 0x10; //MQTT Message Type CONNECT //剩余长度(不包括固定头部) u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); //可变报头 //协议名 mqtt_txbuf[mqtt_txlen++] = 0; // Protocol Name Length MSB mqtt_txbuf[mqtt_txlen++] = 4; // Protocol Name Length LSB mqtt_txbuf[mqtt_txlen++] = 'M'; // ASCII Code for M mqtt_txbuf[mqtt_txlen++] = 'Q'; // ASCII Code for Q mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T //协议级别 mqtt_txbuf[mqtt_txlen++] = 4; // MQTT Protocol version = 4 对于 3.1.1 版协议,协议级别字段的值是 4(0x04) //连接标志 mqtt_txbuf[mqtt_txlen++] = 0xc2; // conn flags mqtt_txbuf[mqtt_txlen++] = 0; // Keep-alive Time Length MSB mqtt_txbuf[mqtt_txlen++] = 100; // Keep-alive Time Length LSB 100S心跳包 保活时间 mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen);// Client ID length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen);// Client ID length LSB memcpy(&mqtt_txbuf[mqtt_txlen],ClientID,ClientIDLen); mqtt_txlen += ClientIDLen; if(UsernameLen > 0) mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen); //username length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen); //username length LSB memcpy(&mqtt_txbuf[mqtt_txlen],Username,UsernameLen); mqtt_txlen += UsernameLen; if(PasswordLen > 0) mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen); //password length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen); //password length LSB memcpy(&mqtt_txbuf[mqtt_txlen],Password,PasswordLen); mqtt_txlen += PasswordLen; memset(mqtt_rxbuf,0,mqtt_rxlen); ESP8266_Recv_flag=0; ESP8266_Recv_cnt=0; MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); HAL_Delay(200); memcpy((char *)mqtt_rxbuf,ESP8266_RecvBuf,ESP8266_Recv_cnt); for(i=0;i<ESP8266_Recv_cnt;i++)printf("%#x ",ESP8266_RecvBuf[i]); //CONNECT if(mqtt_rxbuf[0]==parket_connetAck[0] && mqtt_rxbuf[1]==parket_connetAck[1]) //连接成功 return 0;//连接成功 return 1; 函数功能: MQTT订阅/取消订阅数据打包函数 函数参数: topic 主题 qos 消息等级 0:最多分发一次 1: 至少分发一次 2: 仅分发一次 whether 订阅/取消订阅请求包 (1表示订阅,0表示取消订阅) 返回值: 0表示成功 1表示失败 u8 MQTT_SubscribeTopic(char *topic,u8 qos,u8 whether) u8 i,j; mqtt_txlen=0; int topiclen = strlen(topic); int DataLen = 2 + (topiclen+2) + (whether?1:0);//可变报头的长度(2字节)加上有效载荷的长度 //固定报头 //控制报文类型 if(whether)mqtt_txbuf[mqtt_txlen++] = 0x82; //消息类型和标志订阅 else mqtt_txbuf[mqtt_txlen++] = 0xA2; //取消订阅 //剩余长度 u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); //可变报头 mqtt_txbuf[mqtt_txlen++] = 0; //消息标识符 MSB mqtt_txbuf[mqtt_txlen++] = 0x0A; //消息标识符 LSB //有效载荷 mqtt_txbuf[mqtt_txlen++] = BYTE1(topiclen);//主题长度 MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(topiclen);//主题长度 LSB memcpy(&mqtt_txbuf[mqtt_txlen],topic,topiclen); mqtt_txlen += topiclen; if(whether) mqtt_txbuf[mqtt_txlen++] = qos;//QoS级别 ESP8266_Recv_flag=0; ESP8266_Recv_cnt=0; MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); HAL_Delay(200); memcpy((char *)mqtt_rxbuf,ESP8266_RecvBuf,ESP8266_Recv_cnt); for(i=0;i<ESP8266_Recv_cnt;i++)printf("%#x ",ESP8266_RecvBuf[i]); if(mqtt_rxbuf[0]==parket_subAck[0] && mqtt_rxbuf[1]==parket_subAck[1]) //订阅成功 return 0;//订阅成功 return 1; //失败 //MQTT发布数据打包函数 //topic 主题 //message 消息 //qos 消息等级 u8 MQTT_PublishData(char *topic, char *message, u8 qos) int topicLength = strlen(topic); int messageLength = strlen(message); static u16 id=0; int DataLen; mqtt_txlen=0; //有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度 //QOS为0时没有标识符 //数据长度 主题名 报文标识符 有效载荷 if(qos) DataLen = (2+topicLength) + 2 + messageLength; else DataLen = (2+topicLength) + messageLength; //固定报头 //控制报文类型 mqtt_txbuf[mqtt_txlen++] = 0x30; // MQTT Message Type PUBLISH //剩余长度 u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength);//主题长度MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength);//主题长度LSB memcpy(&mqtt_txbuf[mqtt_txlen],topic,topicLength);//拷贝主题 mqtt_txlen += topicLength; //报文标识符 if(qos) mqtt_txbuf[mqtt_txlen++] = BYTE1(id); mqtt_txbuf[mqtt_txlen++] = BYTE0(id); id++; memcpy(&mqtt_txbuf[mqtt_txlen],message,messageLength); mqtt_txlen += messageLength; MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); return mqtt_txlen; void MQTT_SentHeart(void) MQTT_SendBuf((u8 *)parket_heart,sizeof(parket_heart)); void MQTT_Disconnect(void) MQTT_SendBuf((u8 *)parket_disconnet,sizeof(parket_disconnet)); void MQTT_SendBuf(u8 *buf,u16 len) AT_SendData(buf,len);

基于STM32+ESP8266设计物联网产品(重点:支持微信小程序一键配网连接腾讯云平台)

一、环境介绍编程软件: keil5主控MCU: STM32F103C8T6WIFI: ESP8266协议:  MQTT完整项目源码下载地址:https://download.csdn.net/download/xiaolong1126626497/19137788二、前言这里的 WIFI型号不重要、主控MCU不重要,连接的物联网平台也不重要。要完成本章节的内容,只要会熟悉某款单片机的编程、了解基本的网络编程,明白MQTT协议、能读懂每个物联网云平台的帮助文档都可以完成最终的效果。三、功能介绍前面有几篇内容都介绍了如何使用在腾讯物联网平台创建设备,完成微信小程序与设备进行交互;这些设备代码里的连接的WIFI名称和密码都是固定,只能通过每次修改程序、编译、下载才能更改。一个正常的物联网智能设备,这样操作肯定是不合理的,所以这篇内容就完成如何使用微信小程序一键配网,完成设备的WIFI切换、连接。现在我们购买的智能设备都有自己的配网方式,比如: 小米的很多设备,小爱音箱,摄像头,扫地机器人等。这些设备买回来之后,用户可以参考说明书,完成对设备的配置,让设备连接上家里的WIFI,完成网络连接。本次我以智能锁为产品模型,在腾讯物联网平台创建一个设备,使用STM32F103系统板+ESP8266+LED灯完成智能锁产品的模拟开发;用户设备端可以按下指定的按键进入配网模式,打开腾讯官方的微信小程序,扫描产品二维码,根据步骤完成对设备的配网操作。腾讯物联网支持了好几种配网模式,我这里选择的是“softAP”模式来完成配网操作。 softAP 模式配网的原理介绍:  正常情况下我们买回来的新设备内部是没有我们自己家WIFI的信息的,也就是说这个设备上电之后自己不知道该连接哪一个WIFI;这时我们就需要想办法把我们自己家里的WIFI名称、WIFI密码告诉这个设备,这个设备就可以去连接了。  那问题是怎么去告诉设备这些信息?  设备一般都有进入配网模式的按钮,进入配网模式之后,会将设备内部的WIFI设置成“softAP”模式,也就是设备自己会创建一个WIFI热点出来并创建UDP服务器监听连接,这时我们打开腾讯官方的微信小程序,按照指引去连接这个WIFI,连上之后,微信小程序会通过UDP协议将WIFI的配置信息传输给设备WIFI,设备WIFI收到之后,再切换模式为STA模式,去连接目标WIFI,连接成功之后,登录云平台,绑定设备,完成配网。  这其中的交互协议,后面再细说。四、在腾讯云平台上创建智能锁本章节只会展示几个关键步骤,如果之前没有使用过腾讯物联网云平台可以参考这里学习一遍:https://blog.csdn.net/xiaolong1126626497/article/details/116902653这里可以配置微信小程序的详细参数,配网的设置也这个页面上:下面进行配网设置:选择配网模式:这个页面比较重要,需要将设备进入配网的方法告诉用户,引导用户去操作,完成进入配网模式:保存之后,打开微信小程序“腾讯连连”,扫描右下角的这个二维码,进行配网,完成设备添加。(一般正常产品,会将这个二维码打印出来,贴在设备上,方便用户扫描)(提示: 做这一步,要先设计好设备端的程序,设备上电能正常的运行,才能做)下面是手机上的截图: (根据页面上的提示操作设备)按下开发板子上的S2进入配网模式:在串口上也可以看到提示信息。这时继续操作微信小程序上的步骤,选择设备的WIFI进行连接: 之后在下一个页面会自动等待配网成功(没有截图),设备配网成功之后会出现提示,这时设备就已经在线了打开控制台已经看到设备上线了:这时进入小程序页面,就可以对智能锁进行操作了:到此,配网已经完成,接下来就介绍设备端的代码。五、STM32设备端代码--这才是核心关于配网的流程,在腾讯官网有详细介绍,看这里:https://cloud.tencent.com/document/product/1081/48404由于关联代码较多,这里只提供主要的逻辑代码,其他的代码可以自己下载完整源码查看:https://download.csdn.net/download/xiaolong1126626497/19137788main.c 代码:#include "stm32f10x.h" #include "led.h" #include "delay.h" #include "key.h" #include "usart.h" #include <string.h> #include "timer.h" #include "bluetooth.h" #include "esp8266.h" #include "mqtt.h" 智能锁(自己的设备) MQTT服务器地址: 106.55.124.154 MQTT服务器端口: 1883 MQTT客户端ID: 3XM7FNOG4Llock MQTT用户名: 3XM7FNOG4Llock;12010126;F8Q4P;1624710719 MQTT登录密码: 5d87e9a5bf8ae6295493c263b91aaebc4311f3e95763efe7f31be76c8578f9ec;hmacsha256 订阅主题: $thing/down/property/3XM7FNOG4L/lock 发布主题: $thing/up/property/3XM7FNOG4L/lock 发布消息: {"method":"report","clientToken":"123","params":{"lock":1}} #define SERVER_IP "106.55.124.154"//服务器IP #define SERVER_PORT 1883 //端口号 #define CONNECT_WIFI "_CMCC-Cqvn" //将要连接的路由器名称 --不要出现中文、空格等特殊字符 #define CONNECT_PASS "_99pu58cb" //将要连接的路由器密码 //腾讯物联网服务器的设备信息 #define MQTT_ClientID "3XM7FNOG4Llock" #define MQTT_UserName "3XM7FNOG4Llock;12010126;F8Q4P;1624710719" #define MQTT_PassWord "5d87e9a5bf8ae6295493c263b91aaebc4311f3e95763efe7f31be76c8578f9ec;hmacsha256" //订阅与发布的主题 #define SET_TOPIC "$thing/down/property/3XM7FNOG4L/lock" //订阅 #define POST_TOPIC "$thing/up/property/3XM7FNOG4L/lock" //发布 //微信小程序配网数据订阅与发布 #define SET_WEIXIN_TOPIC "$thing/down/service/3XM7FNOG4L/lock"//订阅 #define POST_WEIXIN_TOPIC "$thing/up/service/3XM7FNOG4L/lock"//发布 char mqtt_message[200];//上报数据缓存区 int main() u32 time_cnt=0; u32 i; u8 key; u8 stat=0; //1.初始化需要使用的硬件 LED_Init(); BEEP_Init(); KEY_Init(); //2. 初始化串口1(打印调试信息)与串口3(与WIFI通信) USART1_Init(115200); TIMER1_Init(72,20000); //超时时间20ms USART3_Init(115200);//串口-WIFI TIMER3_Init(72,20000); //超时时间20ms USART1_Printf("正在初始化WIFI请稍等.\n"); //3. 检测WIFI硬件 while(1) //如果硬件有问题. 蜂鸣器以300ms的频率报警 if(ESP8266_Init()) delay_ms(300); BEEP=!BEEP; USART1_Printf("ESP8266硬件检测错误.\n"); BEEP=0; //关闭蜂鸣器 break; //硬件没有问题. 退出检测 //4. 上电如果检测到S2按键按下就表示需要进入配网状态 if(KEY_S2) delay_ms(100); if(KEY_S2) while(1)//连接服务器 printf("进入配网模式.....\n"); BEEP=1; delay_ms(100); BEEP=0; //清除之前的WIFI连接信息(连接个无效的WIFI),防止默认连接上 上次的WIFI,导致配网错误 ESP8266_SendCmd("AT+CWJAP=\"666\",\"12345678\"\r\n"); delay_ms(200); stat=Esp8266_STA_TCPclinet_Init((u8 *)SERVER_IP,SERVER_PORT); if(stat==0 || stat==0x80)break; delay_ms(500); printf("stat=%d\r\n",stat); stat=0xFF; stat=0xFF; //连接默认WIFI if(stat==0xFF) printf("连接默认的WIFI.\n"); //非加密端口 USART1_Printf("WIFI:%d\n",ESP8266_STA_TCP_Client_Mode(CONNECT_WIFI,CONNECT_PASS,SERVER_IP,SERVER_PORT,1)); //2. MQTT协议初始化 MQTT_Init(); //3. 连接OneNet服务器 while(MQTT_Connect(MQTT_ClientID,MQTT_UserName,MQTT_PassWord)) USART1_Printf("服务器连接失败,正在重试...\n"); delay_ms(500); USART1_Printf("服务器连接成功.\n"); //3. 订阅主题 if(MQTT_SubscribeTopic(SET_TOPIC,0,1)) USART1_Printf("主题订阅失败.\n"); USART1_Printf("主题订阅成功.\n"); if(stat==0x80)//进入配网模式需要给微信小程序返回token值 //订阅微信topic if(MQTT_SubscribeTopic(SET_WEIXIN_TOPIC,0,1))printf("订阅失败\r\n"); //返回平台数据,告知微信连连连接服务器成功 snprintf(mqtt_message,sizeof(mqtt_message),"{\"method\":\"app_bind_token\",\"clientToken\":\"client-1234\",\"params\": {\"token\":\"%s\"}}",esp8266_info.token); MQTT_PublishData(POST_WEIXIN_TOPIC,mqtt_message,0); //Smart_home{"method":"app_bind_token_reply","clientToken":"client-1234","code":0,"status":"success"} 配网成功后微信小程序返回数据 while(1) key=KEY_Scan(0); if(key==2) time_cnt=0; sprintf(mqtt_message,"{\"method\":\"report\",\"clientToken\":\"123\",\"params\":{\"lock\":1}}"); MQTT_PublishData(POST_TOPIC,mqtt_message,0); USART1_Printf("开锁.\r\n"); else if(key==3) time_cnt=0; sprintf(mqtt_message,"{\"method\":\"report\",\"clientToken\":\"123\",\"params\":{\"lock\":0}}"); MQTT_PublishData(POST_TOPIC,mqtt_message,0); USART1_Printf("关锁\r\n"); if(USART3_RX_FLAG) USART3_RX_BUFFER[USART3_RX_CNT]='\0'; for(i=0;i<USART3_RX_CNT;i++) USART1_Printf("%c",USART3_RX_BUFFER[i]); if(strstr((char*)USART3_RX_BUFFER,"\"lock\":0")) LED4=1; else if(strstr((char*)USART3_RX_BUFFER,"\"lock\":1")) LED4=0; USART3_RX_CNT=0; USART3_RX_FLAG=0; //定时发送心跳包,保持连接 delay_ms(10); time_cnt++; if(time_cnt==500) LED1=!LED1; MQTT_SentHeart();//发送心跳包 time_cnt=0; ESP8266的核心代码://存放ESP8266的详细信息 struct ESP8266 esp8266_info; /*SoftAP配网*/ u8 ESP8266_SoftAP_MOde(void) u8 token[]="{\"cmdType\":2,\"productId\":\"3XM7FNOG4L\",\"deviceName\":\"lock\",\"protoVersion\":\"2.0\"}\r\n";//连接状态信息 char *p=NULL; char data[256]; char buff[100]; u8 i=0; u32 time1=0,time2=0; USART3_RX_CNT=0; USART3_RX_FLAG=0; while(1) if(USART3_RX_FLAG) USART3_RX_BUFFER[USART3_RX_CNT]='\0'; printf("rx=%s",USART3_RX_BUFFER); //+IPD,97,192.168.4.2,52021:{"cmdType":1,"ssid":"wbyq_wifi","password":"12345678","token":"df4a4c90abee98c9a443ae8ffd8cc16b" p=strstr((char *)USART3_RX_BUFFER,"+IPD"); if(p) strcpy(data,p);//将接收到的数据拷贝一份保存 p+=strlen("+IPD"); p+=1; while(*p!=',' && *p!='\0')p++; p++;//跳过字符',',获取到IP地址起始位置 i=0; //IP地址解析 while(*p!=',' && *p!='\0') buff[i++]=*p++; buff[i]='\0'; strcpy((char *)esp8266_info.esp8266_ip,buff); //端口号解析 p++; i=0; while(*p!=':' && *p!='\0') buff[i++]=*p++; buff[i]='\0'; esp8266_info.esp8266_prot=atoi(buff);//字符串转整数 //printf("ip=%s:%d\r\n",esp8266_info.esp8266_ip,esp8266_info.esp8266_prot); printf("ret:%d\r\n",Esp8266_UDP_SendData((u8*)esp8266_info.esp8266_ip,esp8266_info.esp8266_prot,token));//上报连接状态 ESP8266_GetData(data,(char *)esp8266_info.esp8266_name,"ssid");//WIFI名 ESP8266_GetData(data,(char *)esp8266_info.esp8266_key,"password");//密码 ESP8266_GetData(data,(char *)esp8266_info.token,"token");//token数据,需要返回给平台 printf("wifi_name:%s\r\n",esp8266_info.esp8266_name); printf("wifi_key:%s\r\n",esp8266_info.esp8266_key); printf("wifi_token:%s\r\n",esp8266_info.token); LED1=1; return 0; delay_ms(1); time1++; time2++; if(time2>=100) time2=0; LED1=!LED1; if(time1>=1000*300) LED1=1; break;//超时退出 return 1; /******************************************************************************************************************* **形参: wifi_name --WIFI名 ** password --密码 ** remote_ip --远端IP地址(255.255.255.255为广播地址) ** remote_prot --远端端口号 ** localhost ---本地端口号 **返回值:0 --成功, ** 其它值 --失败 **示例:ESP8266_UDP_STA_Mode("360WIFI_123","12345678","172.20.7.2",10500,8080); *********************************************************************************************************************/ u8 ESP8266_UDP_STA_Mode(u8 *wifi_name,u8 *password,u8 *remote_ip,u16 remote_prot,u16 localprot) char buff[100]; USARTx_StringSend(USART3,"+++"); //退出透传模式 delay_ms(1000); printf("重启模块.......\r\n"); USARTx_StringSend(USART3,"AT+RST\r\n"); delay_ms(1000); delay_ms(1000); printf("关回显.......\r\n"); if(ESP8266_SendCmd("ATE0\r\n"))return 2; printf("设置为STA模式.......\r\n"); if(ESP8266_SendCmd("AT+CWMODE=1\r\n"))return 3; printf("连接WIFI.......\r\n"); snprintf(buff,sizeof(buff),"AT+CWJAP=\"%s\",\"%s\"\r\n",wifi_name,password); if(ESP8266_SendCmd(buff))return 5; printf("查询IP.......\r\n"); if(ESP8266_SendCmd("AT+CIFSR\r\n"))return 6; printf("建立UDP连接.....\r\n"); snprintf(buff,sizeof(buff),"AT+CIPSTART=\"UDP\",\"%s\",%d,%d,0\r\n",remote_ip,remote_prot,localprot); if(ESP8266_SendCmd(buff))return 7; printf("设置透传.......\r\n"); if(ESP8266_SendCmd("AT+CIPMODE=1\r\n"))return 8; printf("发送数据.......\r\n"); USARTx_StringSend(USART3,"AT+CIPSEND\r\n"); return 0; /****************STA+TCPclinet初始化************* const char *STA_TCPCLINET[]= "AT\r\n",//测试指令 "ATE0\r\n",//关回显 "AT+CWMODE=1\r\n",//设置STA模式 "AT+RST\r\n",//模块复位 "ATE0\r\n",//关回显 "AT+CWJAP=\"HUAWEIshui\",\"asdfghjkl12\"\r\n",//连接wifi "AT+CIPMUX=0\r\n",//设置单连接 "AT+CIFSR\r\n",//查询IP "AT+CIPSTART=\"TCP\",\"192.168.43.204\",8080\r\n",//连接服务器 "AT+CIPMODE=1\r\n",//设置透传模式 "AT+CIPSEND\r\n",//开始发送数据 返回值: 0x7f --退出透传模式失败 ** 0x80 --进入配网模式正常退出 ** 0 --未进入配网模式正常退出 ** 其他值 --异常退出 *****************************************************/ u8 Esp8266_STA_TCPclinet_Init(u8 *server_ip,u16 server_port) char buff[100]; /*退出透传模式*/ u8 i=0; u8 stat=0; u32 id; for(i=0;i<5;i++) USARTx_StringSend(USART3,"+++");//退出透传模式 delay_ms(100); if(Esp8266_SendCmdCheckStat("AT\r\n","OK\r\n")==0) i=0; break; if(i!=0) printf("退出透传模式失败\r\n"); return 0x7f; printf("1.模块复位\r\n"); if(Esp8266_SendCmdCheckStat("AT+RST\r\n","OK\r\n"))return 1; delay_ms(1000); delay_ms(1000); printf("2.关回显\r\n"); if(Esp8266_SendCmdCheckStat("ATE0\r\n","OK\r\n"))return 2; if(ESP8266_GetWifi_Stat())//查询WIFI连接状态,未连接成功则进入配网模式 BEEP=1; delay_ms(100); BEEP=0; delay_ms(100); BEEP=1; delay_ms(100); BEEP=0; stat=1;//进入配网模式标志位 //查询IP地址 printf("3.设置模式AP\r\n"); if(Esp8266_SendCmdCheckStat("AT+CWMODE=2\r\n","OK\r\n"))return 3; printf("4.设置IP地址\r\n"); if(Esp8266_SendCmdCheckStat("AT+CIPAP=\"192.168.4.1\",\"192.168.4.1\",\"255.255.255.0\"\r\n","OK"))return 4; printf("4.设置热点信息\r\n"); id=*(vu32*)(0x1FFFF7E8);//使用STM32的ID作为WIFI名 snprintf((char *)esp8266_info.esp8266_name,sizeof(esp8266_info.esp8266_name),"wbyq_%d",id); snprintf(buff,sizeof(buff),"AT+CWSAP=\"%s\",\"12345678\",1,4\r\n",esp8266_info.esp8266_name); printf("wif_name:%s\r\n",esp8266_info.esp8266_name); if(Esp8266_SendCmdCheckStat(buff,"OK\r\n"))return 5; printf("5.显示端口.......\r\n"); if(Esp8266_SendCmdCheckStat("AT+CIPDINFO=1\r\n","OK"))return 6; printf("6.设置要连接的UDP\r\n"); if(Esp8266_SendCmdCheckStat("AT+CIPSTART=\"UDP\",\"192.168.4.255\",8266,8266,0\r\n","OK\r\n"))return 7; printf("7.获取微信小程序传递过来的热点信息\r\n"); if(ESP8266_SoftAP_MOde())return 8; printf("8.设置模式STA\r\n"); if(Esp8266_SendCmdCheckStat("AT+CWMODE=1\r\n","OK\r\n"))return 9; printf("9.模块复位\r\n"); if(Esp8266_SendCmdCheckStat("AT+RST\r\n","OK\r\n"))return 10; delay_ms(1000); delay_ms(1000); printf("10.连接WIFI\r\n"); snprintf((char *)buff,sizeof(buff),"AT+CWJAP=\"%s\",\"%s\"\r\n",esp8266_info.esp8266_name,esp8266_info.esp8266_key);//字符串拼接 if(Esp8266_SendCmdCheckStat(buff,"WIFI GOT IP"))return 11; printf("11.设置单连接\r\n"); if(Esp8266_SendCmdCheckStat("AT+CIPMUX=0\r\n","OK"))return 12; snprintf(buff,sizeof(buff),"AT+CIPSTART=\"TCP\",\"%s\",%d\r\n",server_ip,server_port); // printf("buff:%s\r\n",buff); printf("12.连接服务器\r\n"); if(Esp8266_SendCmdCheckStat(buff,"OK"))return 13; printf("13.配置透传模式\r\n"); if(Esp8266_SendCmdCheckStat("AT+CIPMODE=1\r\n","OK\r\n"))return 14; printf("14.开始发送数据\r\n"); if(Esp8266_SendCmdCheckStat("AT+CIPSEND\r\n",">"))return 15; if(stat)return 0x80;//进入配网模式并且正常退出 else return 0;//未进入配网模式,正常退出 /**************************获取WIFI连接状态信息***************************/ u8 ESP8266_GetWifi_Stat(void) u16 i=0; u16 time=0; u16 time2=0; USART3_RX_CNT=0; USART3_RX_FLAG=0; USARTx_StringSend(USART3,"AT+CWJAP?\r\n");//查询WIFI连接状态 while(1) if(USART3_RX_FLAG) USART3_RX_BUFFER[USART3_RX_CNT]='\0'; printf("rx=%s\r\n",USART3_RX_BUFFER); if(strstr((char *)USART3_RX_BUFFER,"+CWJAP") || strstr((char *)USART3_RX_BUFFER,"WIFI GOT IP")) USART3_RX_CNT=0; USART3_RX_FLAG=0; LED1=1; return 0; USART3_RX_CNT=0; USART3_RX_FLAG=0; memset(USART3_RX_BUFFER,0,sizeof(USART3_RX_BUFFER)); delay_ms(10); i++; time++; time2++; if(time>=1000) time=0; USARTx_StringSend(USART3,"AT+CWJAP?\r\n"); if(time2>=300) LED1=!LED1; time2=0; if(i>=100*60) LED1=1; break; return 1;

基于STM32F103设计的智能门锁(支持多种开锁解锁方式)

一、环境介绍编程软件: keil5主控MCU: STM32F103ZET6射频卡读写器: RFID-RC522步进电机: 28BYJ4  4相5线蓝牙:  ATK-HC05WIFI: ATK-ESP8266物联网云平台: 采用腾讯云物联网平台,直接支持微信小程序和手机独立APP控制完整项目源码下载地址: https://download.csdn.net/download/xiaolong1126626497/19101807二、功能介绍这是基于STM32设计的智能锁模型,支持多种开锁方式,详情如下:1.  门禁卡刷卡开锁:使用RC522射频卡读卡器,读取IC卡卡号和内部数据,验证身份,开锁。2. 蓝牙自动开锁:  业主的手机蓝牙在范围内,自动开锁(通过连接蓝牙地址确认业主身份),范围是通过蓝牙断开与连接上为准。3. 微信小程序开锁、关锁:通过ESP8266连接腾讯云物联网平台,业主可以通过微信小程序“腾讯连连” 进行远程开锁和关锁。如果没有使用过腾讯云物联网平台的,可以看这里:https://blog.csdn.net/xiaolong1126626497/article/details/1169026534. 按键开锁、关锁: 可以通过开发板上的按键进行开锁和关锁(模拟物理钥匙而已)开锁和关锁使用步进电机正转一圈、反转一圈模拟。三、使用的相关硬件介绍3.1  STM32F103ZET6系统板​3.2 HC05蓝牙模块3.3 WIFI模块3.4 RFID-RC522模块3.5 步进电机四、腾讯物联网平台没有使用过物联网云平台的参考这里: https://blog.csdn.net/xiaolong1126626497/article/details/116902653五、设备核心源码#include "stm32f10x.h" #include "led.h" #include "delay.h" #include "key.h" #include "usart.h" #include <string.h> #include "timer.h" #include "esp8266.h" #include "mqtt.h" #include "hc05_Bluetooth.h" #include "RFID_RC522.h" #include "motor.h" 硬件连接方式: ATK-HC-05串口蓝牙模块与STM32的串口2相连接。 PA1--LED 配对状态输出; 配对成功输出高电平,未配对则输出低电平。 PA4---KEY 用于进入 AT 状态;高电平有效(悬空默认为低电平)。 PA2--RXD 模块接收脚 PA3--TXD 模块发送脚 GND---GND 地 VCC---VCC 电源(3.3V~5.0V) ATK-ESP8266串口WIFI模块与STM32的串口3相连接。 PB10--RXD 模块接收脚 PB11--TXD 模块发送脚 GND---GND 地 VCC---VCC 电源(3.3V~5.0V) RC522射频模块外部的接口: *1--SDA <----->PB5--片选脚 *2--SCK <----->PB4--时钟线 *3--MOSI<----->PA12--输出 *4--MISO<----->PA11--输入 *5--悬空 *6--GND <----->GND *7--RST <----->PA8--复位脚 *8--VCC <----->VCC ULN2003控制28BYJ-48步进电机接线: ULN2003接线: IN4: PC9 d IN3: PC8 c IN2: PC7 b IN1: PC6 a + : 5V - : GND //腾讯物联网服务器的设备信息 #define MQTT_ClientID "3XM7FNOG4Llock" #define MQTT_UserName "3XM7FNOG4Llock;12010126;W5WOU;1624006004" #define MQTT_PassWord "209f8dfc3079a54540aeb4263e99be24c5b0212141d8067e6348036383535941;hmacsha256" //订阅与发布的主题 #define SET_TOPIC "$thing/down/property/3XM7FNOG4L/lock" //订阅 #define POST_TOPIC "$thing/up/property/3XM7FNOG4L/lock" //发布 #define CONNECT_WIFI "CMCC-Cqvn" //将要连接的路由器名称 --不要出现中文、空格等特殊字符 #define CONNECT_PASS "99pu58cb" //将要连接的路由器密码 #define CONNECT_SERVER_IP "106.55.124.154" //服务器IP地址 #define CONNECT_SERVER_PORT 1883 //服务器端口号 char mqtt_message[200];//上报数据缓存区 unsigned char SN[4]; //存放读出的卡号 unsigned char CheckSN[4]={71,151,114,179}; //用于验证的卡号--业主的卡号用于开锁 char SendBuff[10]; 函数功能: 打印卡号 void print_info(unsigned char *p,int cnt) int i; for(i=0;i<cnt;i++) printf("%d ",p[i]); printf("\r\n"); 函数功能: 读卡号--电子标签的卡号 返回值: 1成功 0失败 int ReadCardNumber(void) unsigned char CT[2];//卡类型 u8 status=1; status=RC522_PcdRequest(PICC_REQIDL ,CT);//(寻卡模式,卡类型),成功返回0 if(status==MI_OK)//寻卡成功 status=MI_ERR; status=RC522_PcdAnticoll(SN); //防冲撞,成功返回0,SN是读到卡号的地址 printf("卡类型:"); print_info(CT,2);//打印类型 printf("卡号:"); print_info(SN,4);//打印卡号 return 1; return 0; int main() u8 esp8266_state=0; u8 key; u8 i; u32 time_cnt=0; u8 ble_connect_flag=0; u8 Motor=0; //电机状态 LED_Init(); KEY_Init(); USART1_Init(115200); RC522_Init(); //RC522 Moto_Init(); //步进电机初始化 USART3_Init(115200);//串口-WIFI TIMER3_Init(72,20000); //超时时间20ms USART1_Printf("正在初始化WIFI请稍等.\r\n"); for(i=0;i<5;i++) if(ESP8266_Init()==0) esp8266_state=1; break; esp8266_state=0; USART1_Printf("ESP8266硬件检测错误.\n"); if(esp8266_state) printf("准备连接服务器....\r\n"); //非加密端口 USART1_Printf("WIFI:%d\n",ESP8266_STA_TCP_Client_Mode(CONNECT_WIFI,CONNECT_PASS,CONNECT_SERVER_IP,CONNECT_SERVER_PORT,1)); //2. MQTT协议初始化 MQTT_Init(); //3. 连接服务器 for(i=0;i<5;i++) if(MQTT_Connect(MQTT_ClientID,MQTT_UserName,MQTT_PassWord)==0) esp8266_state=1; break; esp8266_state=0; USART1_Printf("服务器连接失败,正在重试...\n"); delay_ms(500); USART1_Printf("服务器连接成功.\n"); //3. 订阅主题 if(MQTT_SubscribeTopic(SET_TOPIC,0,1)) USART1_Printf("主题订阅失败.\n"); USART1_Printf("主题订阅成功.\n"); /*4. 初始化HC05串口蓝牙*/ printf("蓝牙正在初始化.........\r\n"); USART2_RX_FLAG=0; while(HC05_Bluetooth_Init()){} /*设置当前蓝牙为从机模式---这里设置从机的代码只需要设置一次,后面就可以屏蔽掉了*/ // HC05_Bluetooth_SetCmd((u8*)"AT+ROLE=0\r\n"); //设置为从机模式 // if(HC05_Bluetooth_GetRoleStatus()==0)printf("当前蓝牙处于从机状态!\r\n"); // else if(HC05_Bluetooth_GetRoleStatus()==1)printf("当前蓝牙处于主机状态!\r\n"); // HC05_Bluetooth_SetCmd((u8*)"AT+RESET\r\n"); //复位ATK-HC05模块 DelayMs(1000); //等待蓝牙模块稳定 /*2. 查询蓝牙主从状态*/ if(HC05_Bluetooth_GetRoleStatus()==0)printf("2 当前蓝牙处于从机状态!\r\n"); else if(HC05_Bluetooth_GetRoleStatus()==1)printf("2 当前蓝牙处于主机状态!\r\n"); else printf("2 当前蓝牙主从状态查询失败!\r\n"); /*3. 查看蓝牙连接状态*/ if(HC05_LED)printf("3 当前蓝牙连接成功!\r\n"); else printf("3 当前蓝牙未连接!\r\n"); // /*4. 设置蓝牙的名称*/ // if(HC05_Bluetooth_SetCmd((u8*)"AT+NAME=WBYQ_HC-05\r\n"))printf("4 蓝牙名称设置失败!\r\n"); // else printf("4 蓝牙名称设置为 WBYQ_HC-05! \r\n"); // /*5. 设置蓝牙配对密码*/ // if(HC05_Bluetooth_SetCmd((u8*)"AT+PSWD=1234\r\n"))printf("5 蓝牙配对密码设置失败!\r\n"); //密码必须是4位 // else printf("5 蓝牙配对密码设置为 1234 \r\n"); while(1) //按键可以测试开锁和关锁 key=KEY_Scan(0); if(key==1) LED1=0; //亮灯--表示开锁 time_cnt=0; Motorcw_ring(1,300); //电机正转1圈 Motor=1; //更新微信小程序 sprintf(mqtt_message,"{\"method\":\"report\",\"clientToken\":\"123\",\"params\":{\"Motor\":%d}}",Motor); MQTT_PublishData(POST_TOPIC,mqtt_message,0); USART1_Printf("更新门锁状态:开锁\r\n"); else if(key==2) LED1=1; //灭灯--表示关锁 time_cnt=0; Motorccw_ring(1,300); //电机反转1圈 Motor=0; //更新微信小程序 sprintf(mqtt_message,"{\"method\":\"report\",\"clientToken\":\"123\",\"params\":{\"Motor\":%d}}",Motor); MQTT_PublishData(POST_TOPIC,mqtt_message,0); USART1_Printf("更新门锁状态:关锁\r\n"); DelayMs(10); time_cnt++; if(time_cnt>=50) time_cnt=0; LED2=!LED2; //微信小程序开锁方式: 接收WIFI返回的数据 if(USART3_RX_FLAG) USART3_RX_BUFFER[USART3_RX_CNT]='\0'; printf("UART3收到数据.....\r\n"); //向串口打印微信小程序返回的数据 for(i=0;i<USART3_RX_CNT;i++) USART1_Printf("%c",USART3_RX_BUFFER[i]); //如果是下发了属性,判断是开锁还是关锁 if(USART3_RX_CNT>5) //使用字符串查找函数 if(strstr((char*)&USART3_RX_BUFFER[5],"\"Motor\":1")) LED1=0; //亮灯--表示开锁 //执行开锁代码--电机正转 Motorcw_ring(1,300); //电机正转1圈 Motor=1; else if(strstr((char*)&USART3_RX_BUFFER[5],"\"Motor\":0")) LED1=1; //灭灯--表示关锁 //执行开锁代码--电机反转 Motorccw_ring(1,300); //电机反转1圈 Motor=0; USART3_RX_CNT=0; USART3_RX_FLAG=0; //RC522开锁方式: 读取IC卡号 if(ReadCardNumber()) sprintf(SendBuff,"%x%x%x%x\r\n",SN[0],SN[1],SN[2],SN[3]); //比较卡号是否是业主的卡号,决定是否需要开锁 int i=0; for(i=0;i<4;i++) if(CheckSN[i]!=SN[i])break; //如果是业主,就开锁 if(i==4) LED1=0; //亮灯--表示开锁 //更新微信小程序 sprintf(mqtt_message,"{\"method\":\"report\",\"clientToken\":\"123\",\"params\":{\"Motor\":%d}}",Motor); MQTT_PublishData(POST_TOPIC,mqtt_message,0); USART1_Printf("更新门锁状态:开锁\r\n"); Motor=1; //执行开锁代码--电机正转 Motorcw_ring(1,300); //电机正转1圈 if(USART2_RX_FLAG) USART2_RX_BUFFER[USART2_RX_CNT]='\0'; USART1_Printf("蓝牙:%s\r\n",USART2_RX_BUFFER); USART2_RX_CNT=0; USART2_RX_FLAG=0; //蓝牙解锁方式: 离开范围就自动关锁 连接上就自动解锁 // 区分业主身份方式: 使用蓝牙的配对密码区分. if(HC05_LED==1 && Motor==0) //关锁状态才需要开始 LED1=0; //亮灯--表示开锁 ble_connect_flag=1; //表示蓝牙已经连接过 printf("蓝牙已经连接.\r\n"); Motor=1; //更新微信小程序 sprintf(mqtt_message,"{\"method\":\"report\",\"clientToken\":\"123\",\"params\":{\"Motor\":%d}}",Motor); MQTT_PublishData(POST_TOPIC,mqtt_message,0); USART1_Printf("更新门锁状态:开锁\r\n"); //执行开锁代码--电机正转 Motorcw_ring(1,300); //电机正转1圈 //之前蓝牙连接过 if(ble_connect_flag) ble_connect_flag=0; //清除标记 LED1=1; //关灯--表示上锁 printf("蓝牙已经断开.\r\n"); Motor=0; //执行开锁代码--电机反转 Motorccw_ring(1,300); //电机反转1圈 //更新微信小程序 sprintf(mqtt_message,"{\"method\":\"report\",\"clientToken\":\"123\",\"params\":{\"Motor\":%d}}",Motor); MQTT_PublishData(POST_TOPIC,mqtt_message,0); USART1_Printf("更新门锁状态:关锁\r\n");

诺奖出炉,引起广泛讨论的量子纠缠是什么?

我认为量子纠缠是超时空的作用,而且是确定物质存在的原理。

举个太阳光到地球的栗子,我们看到的是8分钟前的太阳,就是因为光子纠缠的是8分钟前的太阳,并且中途只要不退相干,我们是可以100%获取8分钟前太阳的信息的。而在太阳上,发出光的一瞬间,太阳与光子所纠缠的状态马上就坍缩了,这就确定了太阳发出光子时自身的信息,就算是在地球上看到太阳发出的光子时才退相干,也会影响到太阳发出光子时的状态(因果论在量子层面就失效了)。

另外我认为即使是这样超时空的作用,也并不违背现有的科学,任何物质(包括电磁波)只是作为了信息传输的媒介。量子纠缠实际是更加本质的,如果没有量子纠缠,信息就不会通过物质来传输,就不会有退相干,而物质本身也就不会存在了。另外物质的速度最快也只是光速,所以传统意义上的信息传输的速度最快也就只能是光速了。

万物都是由波组成,再坍缩成为在系统内可以确定的物质,这种波叫物质波,根据特性可以分为很多种波,比如电磁波、引力波等等。而每一种波都可以与发出波的物质纠缠特定的一些信息,波再将这些信息传输给其他物质,最后计算后产生现象。 这只是一种理论而已,实际是非常简单的理论,只是微观世界中信息交换的理论,不过我觉得可以衍生出万物的运作,如果可行,出现以此为基础的万物理论也没什么问题。但我也不是什么科学家,不懂高深的公式,没有严谨的语言,所以这理论其实可以当做哲学来看待。

看了一些相关阐述介绍,量子纠缠就是比如纠缠的两个量 AB 距离无限远,A发生了变数,B也会相应发生变化,不受时间空间影响,只不过等AB得知这些变数或者第三方XDEF等得知这些变数时候是需要 波函数坍缩后才能知道,也就造成了时间差.