从零开始学习一块单片机(3)——绘制51单片机的常用外设
上期我们介绍完51单片机最小系统板的组成和原理图绘制之后我们本期介绍51单片机常用外设的绘制与使用。根据冯诺伊曼的计算机结构,优秀的计算机系统由五个部分组成(运算器、控制器、存储器、输入设备、输出设备),对于我们的系统来说还缺乏输入设备和输出设备。输入设备输入设备顾名思义就可以可以向单片机传入信息的例如常用的温湿度传感器,按键,麦克风等等。本期我们将选择按键以及温湿度传感器作为我们设备的输入设备。输出设备同样的,输出设备是单片机向外部环境传递信号的设备,常见的输出设备有LED灯,蜂鸣器,屏幕。而屏幕根据类型不同也可以分为LCD屏,OLED屏,LED点阵,数码管等等。本期我们选择常用的LCD1602作为我们的屏幕使用。LCD1602LCD1602属于常用的LCD屏幕,可以显示16*2共32个字符。可以很好的显示我们的信息。在立创EDA中在元件库中搜索LCD1602,即可选出我们的相应元件。我们可以按照这样子的接线来接我们的LCD屏幕,我们使用P1八个引脚来控制LCD,P3.2,P3.3,P3.4来控制LCD(P3.0和P3.1被用来下载口)矩阵键盘矩阵键盘可以作为我们单片机的输入之一。采用如图的4*4矩阵,我们可以使用逐行逐列扫描的方式来判断是哪个按键按下(不支持多个按钮按下)我们依次设置P2.0\P2.1\P2.2\P2.3为高,检测P2.4\P2.5\P2.6\P2.7这样子就可以判断是哪一行按下,同样的方法反过来来判断是哪一列按下。蜂鸣器蜂鸣器可以发出声音,但是需要注意的是,蜂鸣器工作时工作电流较大,我们的单片机输出能力不够,因此我们选择使用三极管放大电流。 我们在元件库中选择无源蜂鸣器,注意大小选择9* 我们使用三极管放大电路,用P3.5来控制蜂鸣器,在常用库中选择三极管放入,注意的是TO-92才是直插封装。 并且加上R2限流电阻,大小是1K 流水灯 我们在单片机上加上流水灯,但是为了节省IO口,我们选择使用一个排针来控制LED是否接入使用。4的蜂鸣器也是直插器件。
实在太懒于是不想取名
0 2 嘉立创PCB
从零开始学习一块单片机(2)——绘制51单片机最小系统板原理图
复位电路 前面说到,复位电路部分我们需要在工作的时候保持低电平,当我们需要复位的时候使其接高电平一定时间。 我们使用这个电路来实现按下按钮来复位,电阻R1作为下拉电阻用于在平时按键不按下的时候保持RST引脚低电平。 当我们按下按键的时候,RST通过按键接到电源,这样子我们就可以实现按下按钮高电平复位了。 复位电容C3的目的是为了让按键更加平滑,由于按键的机械结构,我们按下按键的时候会产生很多方波信号,为了使信号更加正常,于是我们引入了这个电容这样子可以很好的消除RST引脚上的杂波。 时钟电路 最后是我们的时钟电路,时钟是单片机必不可少的一部分电路,我们阅读芯片手册后知道,该单片机工作的最高时钟频率是80MHZ,我们选用常见的12MHZ晶振为单片机提供时钟。 并且根据手册提供的最小系统图了解到,晶振应该连接到XTAL2和XTAL1引脚,并且通过两个[removed]
实在太懒于是不想取名
1 7 嘉立创PCB
从零开始学习一块单片机(1)——立创EDA的下载与使用
在单片机中,51单片机指的是兼容英特尔8051指令系统的单片机的统称。51单片机广泛应用于家用电器、汽车、工业测控、通信设备中。因为51单片机的指令系统、内部结构相对简单,所以国内许多高校用其进行单片机入门教学。 其价格低廉,学习简单,市面上有很多卖51单片机开发板的视频,但是却没有系统性的教学如何制作51单片机的资料,因此本系列指在从0开始制作51单片机以及后续使用的教程。 国内所使用的51单片机多为宏晶公司所开发的STC89C52系列,其芯片手册与使用我们可以去宏晶官网查询stcmcudata.com 本期我们将学习如何使用立创EDA软件 首先是下载我们的立创EDA,百度搜索立创EDA,前往其官网。 我们下载并安装对应版本的标准版(暂时用不到专业版)即可。安装完之后打开我们的立创EDA,注册我们的账户。进入工程页面。 了解立创EDA 我们点击新建工程->输入工程名字 之后出现的界面就是我们的原理图了,我们需要在原理图上面绘制我们的51单片机原理图。 右边的常用库中有我们常用的需要的器件,包括电阻电容电感等等。 值得注意的是,我们选择器件需要注意其封装, 例如电阻中R_AXIAL系列就是直插器件,0603这些就是贴片系列 ,后面的数字则代表着不同的大小。对于新手而言直插电阻的焊接难度远远低于贴片电阻,因此我们都选择使用直插电阻进行焊接。 元件库中用于选择常用库中没有的器件,例如运算放大器,各种各样的芯片等等。 我们的51单片机芯片和其外部晶振也要在元件库中选取。 在元件库中输入我们需要的器件名称,点击出来的搜索选项就可以看见右侧的原理图以及芯片的封装。 封装:就是芯片真实的样子,常见的封装有DIP就是直插的。LQFP就是常见的贴片的封装。对于一些三极管而讲,TIP也是常见的封装,TOP23等就是常见的贴片封装,我们使用器件的时候一定要注意其封装。 单击我们选中的元件,我们就可以在原理图中布置我们的器件。 在右上角工具栏中,我们需要着重注意的是第一个导线,用于连接器件,网络标签,拥有相同的网络标签的导线被视作连在一起以及特殊的网络标签例如VCC,+5V,GND等等 等我们绘制好我们的原理图之后,就可以在上方工具栏设置中将我们的原理图转PCB,PCB就是我们的电路板,我们要在PCB中绘制相对应的导线,使我们的原理图中的电路能够联系在一起。 在原理图中我们也可以通过视图中的3D视图来查看我们的PCB板子。
实在太懒于是不想取名
2 4 嘉立创PCB
不用开发版也能畅玩单片机?Protues安装与使用
众所周知,嵌入式编程通常与硬件离不开关系。而单片机开发版对于程序员而言正如将军的配剑一般重要。但是,开发版的价格少则数十多则上板,并且我们似乎也没有办法随身携带。那么有没有什么方法能够利用电脑仿真实现单片机开发呢?本期我们要介绍的软件是Protues,一款应用广泛的单片机仿真软件。1.软件下载安装与破解首先我们可以去Protues的官网下载安装包也可以后台私信我  Protues  使用百度网盘进行安装与破解。导入我们的汉化包和破解补丁,我们就可以享受Protues专版啦,需要注意的是这里我们推荐使用Protues8.0以上版本的,相较于7,Protues8具有更加丰富的元件库以及更好的用户体验。例如我们常用的单总线温湿度传感器DHT11传感器并没有包含在Protues7的元件库中,而Protues8的元件库中则是包含着这款元器件。2.添加元器件创建工程应该不用教吧4.编写示例代码 我们打开我们的Keil5(没使用过的同学可以上网搜一下教程后续也会出相关的文章),选择我们的芯片AT89C52。 右键添加51单片机的头文件,即可开始我们的编程。 首先第一步我们需要写上我们的主函数main,需要在main函数中加入while(1)使其构成一个死循环,否则单片机运行一遍程序后将不再运行。之后我们需要定义我们的延时函数,延时函数的原理是利用单片机执行一次指令需要一定周期的时间,因此我们让程序执行一定量的次数,即可实现循环。#include [removed] void delayms(unsigned int x) { unsigned char i; while (x--) { for (i = 0; i < 120; i++); } } void main() { } 51单片机有许多引脚,我们使用51单片机特有的类型sbit来定义我们最开始仿真图中连接LED灯所使用的引脚。sbit LED = P1 ^ 1 ; 之后我们只需要控制该变量的0或者1即可实现单片机io的高低电平状态从而实现控制LED灯的亮灭。 具体代码如下:#include [removed] sbit LED = P1 ^ 1 ; void delayms(unsigned int x) { unsigned char i; while (x--) { for (i = 0; i [removed]
实在太懒于是不想取名
0 7 嘉立创PCB
C语言——如何给变量类型起别名
在有些嵌入式例程中,我们经常能看到一些变量类型比如说u8,u16 而它的真正含义其实是unsigned char (u8),unsigned short(u16),开发人员利用这些 “别名”   减少写代码的工作量。 那么这个效果是如何实现的呢? 我们要介绍的C语言关键字是Typedef Typedef在计算机编程语言中用来为已有的类型声明和定义简单的别名,区别于宏定义他本身是一种存储类的关键字,而不是像宏定义一样属于预编译语句。 用法也非常的简单typedef 类型名称 新类型名称 ,这样子就可以为已有的类型名称起别名使用。 用这种方式,我们就可以给我们的变量起别名,即便他的名字叫做阿猫阿狗也没有关系。 既然typedef是对已经存在的变量类型进行别名定义,那么我们之前有介绍过结构体C语言——结构体,讲过结构体可以算作我们认为定义的一种变量,因此,typedef自然可以应用到结构体之上,讲我们的结构体进行简单的取别名操作。 在我们使用回调函数的时候,通常会写如下语句,定义一个函数类型的指针,Function,其内容指向函数T1,之后可以直接调用Function来调用T1void T1(int a) { printf("%d", a); } int main() { void(*Function)(int) = &T1; Function(100); } 但是这样子的定义就会有点冗长,我们可以利用typedef来简化这种定义typedef void(*FunctionHandler)(int); void T1(int a) { printf("%d", a); } int main() { FunctionHandler Function; Function = &T1; Function(100); } 我们利用typedef定义创建出我们的"变量类型"FunctionHandler,之后我们创建相关类型的变量就可以使用FunctionHandler来定义我们的变量。
实在太懒于是不想取名
3 9 嘉立创PCB
C语言——extern外部变量以及函数声明和定义
C语言中有许多关键字,例如static,const等等,每个关键字都有其不同的用处,本期介绍一个常用的关键字——externextern字面意思为外部,像static,const一样,用以修饰变量。被extern修饰的变量则称为外部变量。就如static修饰的变量则为静态变量,const修饰的变量则为常量一样。为什么要用extern?我们举个例子:假设我有三个文件,首先是main.c这个是用来存放我们mian函数的文件。其次是test.h是我们用来测试的头文件,最后是test.c则是对于test.h的.c文件。假如我们有一个函数并不会在main.c文件中执行而只会在test.c文件中执行(例如嵌入式的中断服务函数)或者我们举个例子我们在test.c文件中,我们首先定义了一个变量a ,再定义了一个TestF函数,其内容是a递增,那么我在main函数中如何获得这个a的值呢?我们通常的做法是将TestF的函数类型修改成int 型,使其每次返回我们对应的a值,或者重新定义一个函数,其作用就是返回a的值。那还有没有其他的方式呢?或者说我能不能直接获得这个a的值,直接调用a ,修改并使用呢?在这之前,我们首先要介绍一下声明和定义的区别。首先,我们通常定义一个变量通常的写法是 int a = 0 ;这样子的写法,事实上,我个人更倾向于将这种称为 声明并定义 ,但是又是不对的,因为即便是int a 或者int a = 0; 变量a都已经被分配了空间。而我认为,真正的声明则是如下图这样子,并且如果我们不去定义函数(补全)则会提醒我们未定义。在这种情况下,TestFF这个函数并没有被分配空间,但是计算机知道可能会有这样子的一个函数,但是没找到它的定义。 实际上这句话很重要,知道有这么个东西但是没找到定义顶多是警告,但是不知道有这么个东西那可是不仅仅是警告这么简单了。 我们观察下面两份代码 仅仅只是交换一下位置程序就运行错误。 其实我们可以简单的认为C语言的运行逻辑是从上至下的,main函数为入口,找到main函数为止。 既然是从上到下的,第一份代码中先经过了TT()函数,在找到的main函数,其实这时候系统已经“认识”了TT()函数,当我们需要调用TT函数的时候,系统知道该做什么事情。 而第二份代码中,TT函数在main函数之后,因此事实上系统压根并不认识TT函数(你谁啊) 因此我们需要有一个步骤来让系统认识这个函数,即函数声明。 当我们声明这个函数,相当于先告诉编译器有这个函数,之后如果我们遇到了这个函数,那么肯定是有这个函数的,全局里面慢慢找找。 因此声明是C语言必不可少的一项功能。 言归正传,声明和extern到底有什么关系呢? 其实,大家不觉得这二者很像嘛,我调用其他文件中的变量,和我调用其他文件中的函数。本质上报错的原因,不都是:编译器不认识这个变量或者函数。 那能不能通过一种类似声明的方式,使得系统能够提前认识这个变量呢? 没错,那就是extern 我们首先在需要调用的函数里面声明我们的extern int a ,注意这时候不要给a赋值,因为我们是单纯的来声明a的,告诉系统,我的整个文件里面是由a的,但是具体在哪你自己找找,这个a 是定义在其他文件中的。 如果我们写成extern int a = 30 ;实际上这个语句是声明并且定义了变量a,而test文件中也有int a 的定义,就会出现重复定义的报错。 因此在使用extern的时候尤其要注意不同文件之间的使用千万不要造成重复定义。
实在太懒于是不想取名
1 3 嘉立创PCB
C语言——链表的实现
什么是链表? 链表是一种基础的数据结构类型,一种能够动态维护数据的线性数据表。链表的数据以结点形式存储信息,并通过结点之间的指针实现结点之间的衔接。在学习完动态内存分配之后,我们就可以尝试着去使用链表。 单链表存放着两个数据,第一个是链表本身存储的数据,第二个是下一个链表的地址,通过指针的方式去链接下一个链表~ 为什么要用链表? 链表和数组类似,但是数组的长度固定并且不能修改,而链表的长度可以由我们自己创建并且数组插入元素时异常麻烦,而链表插入、删除元素就相对简单。 如何实现链表? 在C语言中我们可以使用结构体创建一个链表单位,结构体中包含着两个元素——链表数据、下一个链表的地址。#include[removed]#include [removed] struct node { int data; node* address; }; 包含标准C语言头文件以及动态内存分配头文件[removed] 初始化 创建结构体node ,包含我们要存储的数据int 以及node * 下一个链表的地址。node * NodeInit() { node* head = (node*)malloc(sizeof(node)); head->address = NULL; head->data = 0; return head; } 初始化链表函数,创建一个链表单位。 尾插 我们接下来写一个尾部添加元素的函数。 node* Node_add( node * head , int Data) { node* old = head; int number = 0; node* new_node = (node*)malloc(sizeof(node)); new_node->data = Data; new_node->address = NULL; while((head)->address!=NULL) { head = head->address; number++; } head->address = new_node; return head; } 当末尾不是NULL时,向下迭代,直到搜索到末尾链表,该链表的address指向NULL。 我们创建一个新的链表,让最后一个链表的address指向新链表。这样子就实现在末尾插入链表。 我们可以利用上述的方法实现,这种一个一个向下搜寻的方式叫做迭代 int Node_Index(node* head,int i) { int number = 0; node* adc = (node*)malloc(sizeof(node)); adc = head; while (number [removed]address == NULL) { return NULL;//溢出 } adc = adc->address; number++; } return adc->data; } 插入 和上述同理,利用同一种迭代的方法,我们可以在链表的任意位置插入我们想要的数据。 我们只需要断开其中的某个链接,将我们的新链接插入其中,void Node_Insert(node* head, int index,int Data) { int number = 0; node* nenode = (node*)malloc(sizeof(node)); while (number [removed]address == NULL) { return;//溢出 } head = head->address; number++; } nenode->data = Data; nenode->address = head->address; head->address = nenode; } 相较于数组,链表的插入更节省逻辑,也从侧面强调了,链表是逻辑上的连续,并不是空间上的连续 这样子一个具备插入、索引、尾插、显示的链表就创建完成啦、之后我们可以封装入更多的功能,例如排序、首插等等。也可以扩大数据类型,增加数据内容实现更多的功能!
实在太懒于是不想取名
3 4 嘉立创PCB
C语言——常量修饰符const(嵌入式常客)
常量的介绍以及作用 const修饰符的使用 指针常量和常量指针的区别 C和C++中const的区别 常量的介绍以及作用 我们在学习C语言中介绍过变量,也知道变量的定义与初始化的过程。常量则是和变量相对,顾名思义变量即为可改变的量,常量即为保持不变的量。专业点说即为“是否只读”,例如数字就是最常见的常量,比如20,20代表的含义就是20,20不能代表30 也不能代表40(别杠宏定义) 常量在C语言中又分为三种类型:整型常量、实型常量和字符常量。其中整形常量又可分为长整型常量、短整型常量等等(本文不主要将这些所以略过)。例如1000,3.1415,‘a’,这些都是常量。 那么,常量有什么作用呢? 首先常量表现为只读,就如同宇宙中光速恒定一般,想让一个数作为规则般不能被修改的话,即可以引入常量,可以将一个变量变成常量(使用const修饰符)这样子可以大幅度的提高代码的可读性和维护性,防止在使用过程中被修改导致意外发生。 那为什么不用#define? #define作为预处理语句虽然也可以实现常量的效果,但是及其容易造成边界效应的出现。 例如在如下计算时会出现实际运算变为3+3*2,实际计算结果出错的问题,因此在使用#define时需要使用小括号提高运算优先级。 常量还有什么用途? 常量除了只读的特性之外,常量是会单独开辟一段内存空间存放,这段空间被称为常量区,是内存五区之一。对于学习嵌入式来说,常量区的数据读取速度更快,可以有效的减少CPU的负荷。尤其是例如正弦表、寄存器配置等内容庞大的数据可以选择放在常量区以便于读取时更加快速,以及保证不会被意外修改。 常量在嵌入式编程中大范围的使用。 const修饰符的使用 const修饰符的作用是将变量修饰为常量,使用方法如下,在定义变量时加上前缀const,改变量即变为常量,只读不能修改其值。 值得注意的是,由于常量的值不可修改,因此我们在定义的时候就必须确定其初始值,否则编译器会报错,其次我们在使用过程中也不能修改其值。 我们可以给所有的变量加上const修饰,包括字符型,浮点型,整型,亦或者自己定义的结构体等等。#include[removed]#include [removed] int main() { const int m = 30; const char p = 'a'; const double s = 3.14159; } 指针常量和常量指针 如图所示,p1和p2均报错,但是二者报错的位置和原因并不一样。当我们使用const int * 定义的p1时,我们认为我们是创建了一个指针变量,这个指针变量指向了一个常量,因此我们不能通过解引用的方式去修改指针的值,可以理解成是 (const int )类型的指针。但是由于指针本身是一个变量,因此我们修改指针指向的值,例如可以修改p1从原来的m地址为s地址。这被称为指针常量。 而int * const 则认为:我们创建了一个常量,这个常量的类型是int *型,因此我们不能修改p2从m的地址到s,但是我们可以通过解引用的方式来修改p2所指向的内容,但是p2本身(指向m的地址)并不能被修改,这称作常量指针。 C和C++中const的区别 由于const最早出现在C++中,为了替代#define预编译的作用,后来被移植到了C语言中,实际上C++中的const真正的做到了只读的效果,即通过其他方式(我没找到)并不能修改由const修饰后的变量。 而C语言中,我们虽然也不能直接的修改const修饰后的变量,但是我们可以通过其他的方式,例如利用指针的间接内存访问的方式来修改我们的变量。 所以在C语言中使用const一定要注意也可以通过一些间接内存访问来修改const所修饰的变量。
实在太懒于是不想取名
1 6 嘉立创PCB
C语言——可变长度数组(VLA)
前段时间实验室开始招收新生,在新生群里总是出现一个情况,他们在写C语言的时候会使用 变量作为数组长度。 系统性的学过C语言的同学都明白,变量作为数组长度的做法是非法的,而且在如VS等编译器中均会报错。 但是学弟学妹们在DEV中这样子却可以的,使用变量作为数组编译和运行均没有问题,所以我不经思考这个问题。就如这篇公众号所说,栈区数据是由系统分配的,而我们的数组属于栈区数据,会在代码运行前就申请好空间,所以不应该会出现这种情况 后来查阅许多资料后得知,这种现象叫做可变数组长度(Variable Length Arrays) 有些编译器支持VLC,而有些编译器则不支持VLC,查阅资料得知从C99开始支持VLC,C90是不支持VLC的,并且VLC并不属于C语言标准的。 VLC是将栈区申请空间延迟到了代码运行后,本质还是在开辟栈区空间,并且在代码结束后(如函数运行结束后被释放)。 因此使用VLC和前文提到的动态内存分配有利有弊。 虽然使用VLC可以节省时间,但是VLC的生命周期短,在代码结束后即被释放,并且由于空间位于栈区会占用栈区空间可能会导致栈溢出等错误。 因此真正的需要使用动态内存分配应尽量使用malloc函数来实现动态内存分配如下:#include[removed]#include [removed] int main() { int m; printf("请输入长度:"); scanf("%d", &m); if (m [removed]
实在太懒于是不想取名
0 1 嘉立创PCB
C语言——动态内存分配
在介绍动态内存分配之前首先要介绍堆区、栈区,堆区栈区是C语言内存五大区(堆区、栈区、全局区、代码区、常量区)的两区。本篇主要介绍堆区和栈区,以及利用堆区进行动态内存分配。 栈区按内存地址由高到低方向生长,其最大大小由编译时确定,速度快,但自由性差,最大空间不大。我们代码中的临时变量,局部变量包括调用函数时的传入参数以及返回值都存放在栈区中,其特点是由编译器决定其大小,程序员并不能直接控制栈区。其数据在使用完(例如函数结束)时由系统释放。 而堆区的数据则是由程序员开辟空间存放数据,若程序员不主动释放该空间,则程序结束后可能由OS回收。值得注意的是,我们创建堆区数据时是开辟了一系列的连续内存空间,而这个连续内存空间的首地址是存放在栈区的。 首先是由于栈区的内存普遍较少,如果我们需要很大的空间的话,栈区是不符合需要的。而堆区的内存庞大,完全可以符合我们的要求。 其次,如果我们需要根据需求定义数组长度,而使用变量当作数组长度的操作是非法的,那么我们则需要我们人为开辟指定大小的内存空间,因此我们需要利用堆区来进行动态内存分配。 我们需要了解两个函数:malloc 和 free (C++中为new 和 delete) 使用这两个函数需要#include [removed] malloc 的函数原型如下,void*类型则是未确定类型的指针,c,c++规定void*可以强转为任何其他类型的指针。我们需要使用一个指定类型的指针变量来接收malloc的返回值——开辟的动态内存空间的首地址。该地址存放在栈区,其余空间则是存放在堆区。extern void *malloc(unsigned int num_bytes); free的函数原型如下,用以释放我们开辟的内存空间。extern void free (void* ptr) 实际使用 我们使用如下代码即可开辟我们需要的空间。#include[removed]#include [removed] #include [removed] int main() { int* p = NULL; p = (int*)malloc(sizeof(int) * 100000000); } 首先定义一个我们需要的指针类型变量,比方说int * p; 之后使用malloc函数,变量为我们需要的空间,那么就可以使用sizeof函数再乘长度,之后将void * 强制转化为 我们需要的数据类型。 可以看到,即使我们开辟了一千万个长度的空间也没有发生报错。 当然一个好习惯的程序员一定要有一个及时释放的好习惯free(p);
实在太懒于是不想取名
2 5 嘉立创PCB
C语言——宏定义#define
近日在某一技术群又水群时某一群友将这个称之为“常量”,事实上在C语言中#define 正确的叫法叫做“宏定义”属于预处理指令中的一种,在C语言中应用极其广泛。 预处理指令则是指在程序编译前的操作,例如#include #define等带“#”的指令,其本身并不是C语言语句。 宏定义的用法主要有两个:定义值和定义算式 定义值:将某一值宏定义为一些符号 例如下述演示中将3.1415 定义为 Pi ,这样子我们就可以使用Pi来代替3.1415进行计算 注意上述所说的替代是真正字面意义上的替代,可以理解为就是把Pi的地方给替换成了3.1415,而开头我们说过预处理指令不是C语言所谓的语句,因此不需要加上分号进行结尾。 结合这两个特性,在使用#define宏定义的时候就要尤其注意分号的存在,因为#define是替代,分号也会被替代进去,因此如果携带分号的时候如下语句就会报错,原因就是这句话等效于在算式中插入了一个分号。 什么时候我们会使用到宏定义呢? 例如代码中存在大量的重复数字,且这些数字都一起改变时,比方说一次采样的长度,我们就可以利用宏定义#define Lenth 1000 这样子我们就可以只改变替代Lenth的值来改变全部代码中Lenth的大小,节省了很多修改代码的时间。 许多同学经常因为使用中文字符而苦恼报错嘛~经常注释时进行中英文切换而头疼嘛~~没错,使用#define ,走报错的路,让报错无路可走!! 好好好,中文编译器是吧~~  请勿模仿 第二个用法就是定义算式 如下图所示利用三目运算符 将最小值宏定义为min(a,b),宏定义中出现的变量a ,b 会代入后续式子中进行运算,这种类型很像是函数但实际上并非函数。 利用这种方法可以快速的定义一些算式,例如下面的三个数最小值两个联立的三目运算符即可比较出三个数的最小值,可以较为方便的编写部分代码,节省我们所需要的空间。
实在太懒于是不想取名
1 9 嘉立创PCB
为什么要用高速运算放大器——压摆率
这段时间在某一技术群水群时,群友提出了一个问题是:他的单片机引脚翻转后需要采集翻转后的电压为什么采集不准,需要等待500us之后才可以采集成功。\  对此,另有群友提出可能是ADC转换时间的原因,但是这个原因不太可能,理由是:一般ADC采集已经考虑了其ADC采集时间,因此在调用ADC读取函数的同时已经对其进行了延时。其次是500us的时间对于ADC的转换速度来说已经是绰绰有余了,因此不太可能是ADC的转换时间原因。 对此,我提出的两个观点是:1.可能是由于配置的上下拉电阻太大,导致电阻与寄生电容构成的充放电回路的时间常数过大,导致方波的下降沿时间过长。但是这一可能性也不是很大,因为从示波器上看出这个波形还是非常工整漂亮的,可以认为是理想方波。2.第二个可能是由于一般ADC前总是要接一个跟随器,其硬件电路上可能使用的跟随器性能不够,无法快速的适应电压的变换从而导致当IO口翻转后,跟随器无法及时的翻转电压,导致需要延时一段时间后等待跟随器稳定,进而可以准确采集电压。 后来群友去检测跟随器输出的信号之后,发现当IO口翻转后,运算放大器输出的电压并没有及时的变换,而是需要等待一段时间后才能进行稳定。 那么如何解决这个问题呢? 这里就不得不提运算放大器的两个重要指标:压摆率 压摆率(SR slew-rate-limit) 顾名思义:压~摆~率,就是能“压住电压的速率”,单位通常是V/us,其定义为输入为阶跃信号时闭环放大器的输出电压时间变化率的平均值。简而言之就是运算放大器的响应速度。 压摆率高的运算放大器,其响应速度也很快,所以许多高频电路也需要选用压摆率高的运算放大器,也就是我们所说的高速运放,而前面群友遇到的问题也就是运算放大器的压摆率不够导致的。 例如LM358的压摆率为0.5V/us 而高速运算放大器AD8066则高达180V/us 因此如果电路涉及到高频或者需要高频跟随器等更快的响应速度,即需要选择合适的高速运算放大器来搭建电路。 仿真 可以看到在Multisim中,LM358的压摆率为5V/16.8us =0.297V/us (具体应以实际测量为主) 如何去选择压摆率呢? 有一个非常重要的公式:SR = 2pi * f * Vplc 其中f是工作频率,Vplc是信号的幅值。例如我们的工作频率是100kHZ,信号的幅度最终是100mV,那么压摆率就应该满足SR>2*3.14*100000*0.1 = 62800V/s ,也就是0.062V/us.
实在太懒于是不想取名
1 12 嘉立创PCB
C语言——结构体
在C语言中有一块极容易被忽略,但是对于嵌入式编程来说用处特别大的内容——结构体 我发现在上课的教学中包括许多授课视频中经常会忽略结构体,也许是因为结构体的作用在C++中被完美的替代了,但是在嵌入式的学习中适当的使用好结构体可以搞笑的提高代码的效率。 当我们的代码中需要表示不同类型的变量时,如字符型,浮点型,整型等类型,我们可以使用结构体将这些代码作为一个整体使用。 如下图所示,我们使用struct 创建一个结构体,这个结构体中有我们想要整合的变量及其类型,我们将这些变量和类型作为一个整体。 并且将这个结构体命名为 a  这里的 a 可以理解成变量类型,我们创建了一个新的变量类型a 这个变量类型a是char* ,int .....这些的整体。 我们在使用的时候也是一样,使用变量类型 + 变量名; 的方法创建新的变量,但是要注意的是,在使用结构体时再变量类型前加上struct,让系统可以判断这是一个结构体。 这样子,我们就可以创建出一个结构体变量啦。 接着我们使用 点引用 来使用这个结构体的数据。使用点引用来修改和调用结构体变量的数据。 既然结构体变量作为一种变量,那么结构体变量必定是向内存申请了一部分空间用以存放数据,我们也可以通过变量类型+*的方式创建其对应的指针变量,如下图所示,我们创建了Son的指针变量用来指向一个结构体变量,但是在使用结构体的指针变量时,我们调用数据的方式就不能是点引用,而是需要使用 ->  地址引用的方式修改和调用数据。 我们观察结构体的成员地址时,我们发现:在我们创建成员变量时,这些成员的地址是连续的。所以结构体也就是在数组的基础上为不同的成员申请空间不同的连续内从空间。 我们也可以利用这个特性,直接对地址进行操作(当然这个操作很没有必要) 我们也可以创建结构体函数,将结构体作为返回类型,提高我们代码的可读性以及更好的优化我们代码的结构。
实在太懒于是不想取名
1 4 嘉立创PCB
2023年全国大学生电子设计大赛B题共轴电缆检测装置(2)
上一篇我们介绍了本题我们的使用方法以及原理,这期介绍我们在使用过程中遭遇的坑点以及解决方案。 长度的不准确与漂移 我们在测试过程中发现我们可以利用扫频的办法准确的获得长度的与频率的关系,但是测到的长度总是在飘,并不能很好的稳定。 长度的不稳定源自在频点扫描的不稳定上。我将DDS峰值检波的信号打印出来后,发现采集的信号中会有一大串“0”的存在,这些0的原因是因为由于信号太小,ADC的精度有限。STM32上板载的ADC是12位精度,因此低于精度的数据会读不出来值。然而我们判断频点第一个谷值的方法是寻找最小值,相当于在谷值附近会有数据丢失的情况。 解决办法:对于这个情况我们有两个解决办法,第一个办法是我们读取到第一个0的时候,并不立刻退出,而是等待最后一个0的出现,将我们的索引值定为这两个0的中间点。 第二个方法就是传统的放大峰值检波的电压。 PS:不同信号的谷值和线长有关,这个好像叫做信号的抑制深度(没学过传输线理论,之后学习一下) 2.高速跟随器与高速放大器 由于单片机的自身特性以及该电路的特殊性,峰值检波的输出必须加上跟随器,ADC才可以正常的读取电压值。但是由于我们是扫频信号,峰值检波的输出信号虽然是一个直流信号,但是这个直流信号的变化是一个高频的,是和工作频率有关的。因此我们选择的跟随器和运算放大器不仅仅是能够起到跟随以及放大的效果,更重要的是其速度(压摆率)必须满足我们电路的要求。 由于比赛的最后一天我们才想到这茬事情,而我们手上并没有适合的高速运算放大器(本来买了AD8066,但是发烫严重来不及研究了)。我们使用NE5532进行替代。但是其工作带宽只有10MHZ,而且其压摆率只有9V/us,根本无法满足我们的速度要求。但是事已至此,我们只得采用转换信号时延时,人为的为5532提供响应时间。 最终,我们的信号放大倍数是103倍,在这个放大倍数下,我们可以精确的确定谷值,并且也解决了之前尚未解决的电阻曲线的问题。 3.ADC微小信号误差     我们通常认为在3.3V的参考电平情况下,12位ADC与实际电压的关系是:Val = ADC_Val*3.3/4096     事实上,在微小信号测量时,ADC总会有自身的误差使其并非工作在线性区域。 放大电压不仅仅可以准确的寻找到谷值,也可以进一步的降低ADC的误差,使其工作在线性区域。 并且我们惊讶的发现,事实上电阻和电压是一条三次函数的关系,但是由于之前的信号受干扰较大,并且信号被压缩的尺度太小,导致我们也无法认出这是什么曲线!!! 因此这道题的关键点之一就是能否有效的放大信号。 但是放大信号有利有弊,下一期介绍平滑滤波算法在这道题上的关键作用。
实在太懒于是不想取名
1 7 嘉立创PCB
2023年全国大学生电子设计大赛B题同轴电缆检测装置(1)
 前几日参加了全国大学电子设计大赛,选题方面我们选择了B题——同轴电缆长度和终端负载检测装置。 选题方面我们今年的题目在B题同轴电感测量,C题电感电容测量以及H题信号分离装置中选择了B题,原因是C题要求使用MCU为Ti公司单片机,并且其要求需要20MHZ以上的高频Q表,由于我们找不到高频Q表,于是并没有选择这题。 而信号分离装置由于其硬件工作量较多,而且更具我们先前的教训,大量的硬件设计会导致作品的高度不稳定性以及我们自身能力受限于是也选择了放弃。 在多重因素的考量之下,我们选择了同轴电缆的测量。 1.一条同轴电缆可以看作一条阻抗为50欧的电阻,我们想要测量其长度可以在其输入端发送一个信号,信号经过这条信号线的传输,由于其中终端是开路,我们可以看作阻抗无穷大,因此根据传输线理论其信号会完完整整的反射回来,因此这个我们可以计算返回来的时间除以传输速度这样子出来的值就是传输线长度的两倍了。 但是这种方法实现起来的难度很大,电信号在传输线中传输的速度约是0.7倍的光速,我们要测量其纳秒级的时间,相当复杂。 2.另一种方法就是使用驻波叠加法,我们发射的信号经过信号的反射传输回来时会在发射点上进行叠加,如果我们调整好我们发射的信号频率,就可以改变叠加信号幅度,当发射的频率所对应的波长为线长的四倍时,信号经过传输线反射回来正好是信号的波峰与反射信号的波谷,其叠加后信号大小为0,因此我们可以采用这种方式测量电缆的长度。 那么思路就非常的简单,我们需要使用一个DDS设置频率步进对同轴电缆进行扫频,将每一个频点的最大值有效值求出,找出有效值最小的一个频点既可完成长度的测量。 电阻的测量可以使用电阻分压的原理,当我们的信号工作在低频时,信号的有效值和大小有关,不同的电阻使得负载(加上导线的电阻)得到的分压受到了影响,因此我们可以测试不同电阻的分压情况,得到电路的分压模型。如下图也可以看到该电路模型极像一个带偏置的反比例函数模型,推断其分压状况为(50)/(x+50+50),其中的50分别为DDS输出阻抗与同轴电缆的线阻。 电容的测量则可以利用传输线的特性,由于传输线其信号线由绝缘层包裹,绝缘层外有一层屏蔽层连接电缆的外壳。 因此我们可以将同轴电缆的开路状态看作一个电容,而电容的容值则和电缆的长度有关。 不同的电缆具有不同的电气特性,我们查阅同轴电缆的电气参数表,发现其传输速率为0.7,等效电容量为99pf/m。因此测量其电容量大小时,我们只需要提前测量其长度,连接上电容之后再次测量其长度,就可以获得一个差值,这个差值和实际的电容大小是一个线性关系。所以我们就可以利用这种方法测量电容的大小了。 最后在我们的制作中,实际测量的电缆长度精度在1%左右,而电容电阻测量其精度更是在5%左右,完全符合题目的要求。
实在太懒于是不想取名
0 1 嘉立创PCB
ESP32物联网教程之MQTT
 在前面介绍利用百度智能云实现MQTT设备创建并且获取设备信息后,我们介绍了如何使用C++实现一个简单的MQTT服务器,可以实现发送与接收MQTT消息的功能。 这期我们介绍如何使用Ardunio IDE实现ESP32上云。 步骤也非常简单: 导入MQTT相关库 配置MQTT连接信息 连接MQTT 注册响应回调函数 实现响应回调函数 ESP32 是一款低成本、低功耗的微控制器,集成了 Wi-Fi 和蓝牙。 它是 ESP8266 的后继产品,ESP8266 也是一款低成本 Wi-Fi 微芯片,尽管功能非常有限。它是一个集成天线和射频巴伦、功率放大器、低噪声放大器、滤波器和电源管理模块。整个解决方案占用的印刷电路板面积最少。该板采用台积电40nm低功耗技术的2.4GHz双模Wi-Fi和蓝牙芯片,功率和射频性能最佳,安全可靠,可扩展到各种应用。 首先,打开Ardunio IDE,导入相关MQTT库(关于如何安装Ardunio 以及ESP32库的下载不做介绍请自行搜索) 在IDE右侧,输入MQTT,选择MQTT进行安装,此时我们就可以导入WiFI以及服务器连接库(WiFi不需要进行安装) 接着我们配置Wifi连接密码以及MQTT连接信息。const char* ssid = "1cm"; const char* password = "a1234555"; const char* mqtt_server = "altnlnn.iot.gz.baidubce.com"; const int mqtt_port = 1883; const char* mqtt_user = "thingidp@altnlnn|ESP32|0|MD5"; const char* mqtt_password = "9aeec4289c816ecfdb0de7ed3b164bf6"; const char* mqtt_topic = "TEST"; 配置好MQTT及Wifi信息后,我们在setup函数中配置启动信息,连接到Wifi之后,ESP32会自动尝试连接MQTT服务器,连接完成后进入循环函数,执行我们的主循环。void setup() { Serial.begin(115200); WiFi.begin(ssid, password);//连接Wifi while (WiFi.status() != WL_CONNECTED) { delay(1000);//等待WIfi连接成功 } client.setServer(mqtt_server, mqtt_port);//连接MQTT服务器 client.setCallback(callback);//注册接收回调函数 while (!client.connected()) {//等待连接成功 if (client.connect("ESP32Client", mqtt_user, mqtt_password )) { client.subscribe(mqtt_topic);//订阅需要的主题 } else { Serial.println("Failed to connect to MQTT Broker"); delay(5000); } } } 主循环的内容非常简单,主要是检查MQTT连接是否正常以及向云端发送心跳报文和处理消息。 void loop() { if (!client.connected()) { reconnect();//检查MQTT是否连接断开,如果连接超时,则尝试重新连接 } client.loop();//MQTT发送响应报文(还活着) } 在相应回调函数中,我们判断接收的字符是否为LED_ON,如果接收到的字符等于LED_ON的话,我们则让ESP32板载LED灯亮起。void callback(char* topic, byte* payload, unsigned int length) { Serial.print("Message received in topic: "); Serial.println(topic); Serial.print("Message:"); char * ss = (char*)payload; if(strcmp(ss,"LED_ON")==1) { LED_ON(); } }
实在太懒于是不想取名
2 7 嘉立创PCB
基于C++的MQTT服务器实现
之前有一篇文章介绍了如何在百度智能云上面创建MQTT设备,这期我们讲一讲如何利用VS使用C++制作一个简单的MQTT服务器。 首先,我们需要下载一个VS,VS全称visual studio,是微软发布的一款功能强大的编译器,从我的学习之初也一直在使用这款。这里就不赘述如何安装Visual Studio了,不会安装的同学可以自行搜索。 我们打开VS,选择创建新项目,类型选择C++的空项目。 在开始之前,我们需要一个可以实现MQTT的库,我这里的是paho.mqtt的库(链接贴不上来,如需私信),我们需要把库链接入编译器。 选择 项目-xxxx属性-配置属性-链接器-常规,在附加库依赖项中添加我们的库文件 之后添加“MQTTClient.h”头文件即可使用我们的MQTT库。 1.在主函数中我们首先创建MQTT连接变量以及存储MQTT连接信息的结构体。 MQTTClient client; MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer; 2.获取MQTT设备信息以及设置MQTT连接。 在百度智能云上面创建MQTT设备中提到获取MQTT服务器连接信息,我们在连接信息生成器中获得这些信息。并将这些信息保存下来作为等会连接使用。 3.配置MQTT连接信息,设置等待时间,以及设置消息回调函数。   connlost, msgarrvd, delivered是三个回调函数,分别当MQTT断开连接,MQTT接收消息,MQTT发送确认之后调用。#define ADDRESS "agcqeqz.iot.gz.baidubce.com" #defineCLIENTIDTEST1#define TOPIC "One" #definePAYLOADTEST1#define QOS 0 #define USERNAME "thingidp@agcqeqz|Test1|0|MD5" #define PASSWORD "b0fcc0100018a7b8db847f916fbedf7f" MQTTClient_create(&client, ADDRESS, CLIENTID, MQTTCLIENT_PERSISTENCE_NONE, NULL); conn_opts.keepAliveInterval = 20; conn_opts.cleansession = 1; conn_opts.username = USERNAME; conn_opts.password = PASSWORD; MQTTClient_setCallbacks(client, NULL, connlost, msgarrvd, delivered); if ((rc = MQTTClient_connect(client, &conn_opts)) != MQTTCLIENT_SUCCESS) { printf("Failed to connect, return code %d\n", rc); exit(EXIT_FAILURE); } printf("Subscribing to topic %s\nfor client %s using QoS%d\n\n", TOPIC, CLIENTID, QOS); MQTTClient_subscribe(client, TOPIC, QOS); while (1); 运行上述代码,打开用量统计可以看见MQTT连接成功,我们接下来来编辑消息响应函数。我们将消息变量中的值存入payloadptr变量中,之后打印这个变量的值,这样子当有一个设备发送消息的时候,就可以触发这个函数打印消息了。int msgarrvd(void* context, char* topicName, int topicLen, MQTTClient_message* message) { printf("Message arrived\r\n"); printf(" topic: %s\r\n", topicName); printf(" message: "); char* payloadptr = static_cast[removed](message->payload); cout<< payloadptr<<endl; MQTTClient_freeMessage(&message); MQTTClient_free(topicName); return 1; } 在设备中,我们选择模拟设备,点击开始模拟。订阅主题选择之前模板中(上一篇公众号提过)设置的主题,我这里是One,注意的是程序中的Topic 宏定义要一样 接下来我们选择我们需要的主题,发送我们的信息,我们发现我们的程序控制台上也成功接收到了我们发送的信息。 4.配置发送函数。我们在主函数中设置输入,当我们有输入时我们即向云端发送数据。 while (1) { char cinn[256]; cout <[removed]> cinn; if (strcmp(cinn, "exit") == 1) { exit(0); } MQTTClient_publish(client, TOPIC, strlen(cinn), cinn, QOS, 0, NULL); } MQTTClient_disconnect(client, 10000); MQTTClient_destroy(&client); 不知道为什么我发送空格就会分成几段(我在研究一下),可以看出我们设备发送的消息也会被设备本身所接受,所以我们发送信息的时候应当包含更多的信息,比方说时间戳、设备ID等等信息,但是由于本文章只起演示作用就不多展示。
实在太懒于是不想取名
1 5 嘉立创PCB
STM32串口中断之缓存区溢出卡死
近日,一朋友给我讲述了一个关于STM32串口的问题。他想实现接收无限制字符数量的串口信息。 但是他遇到了一个问题,他在主函数中发送循环发送内容(500ms的延时)串口一中断回调函数中如果收到了串口内容,就利用串口发送。 但是他的串口一旦接收到了信息就会导致主循环中的串口发送极快。他的主代码如下unsigned char RecieveBuffer; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { UNUSED(huart); HAL_UART_Transmit(&huart1,&RecieveBuffer,8 ,100); // 将接收到的数据再通过串口发送出去   HAL_UART_Receive_IT(&huart1,&RecieveBuffer,8); //使能接收中断,RecieveBuffer的类型是uint8_t } 看了他的代码,就这么短短的7行代码,让我汗流浃背。 他的发送和接收中有一个参数是8,这意味着当串口接收的寄存器中有8个数据时就会触发中断回调。 我们需要注意的是,串口寄存器的数据会被存储到缓存区中,这里的缓存区相当于一个队列先进先出,当我们调用HAL_UART_Receive_IT(&huart1,&RecieveBuffer,8); 会将RecieveBuffer作为缓存区,将串口接收的信息存入缓存区。 那么让我们来试一下这段代码。 可以看到,代码死住了。其实不难理解,我们发送数据的时候,由于缓存区只有一个字符的空间,我们发送八个字符的时候会导致缓存区溢出。所以我朋友的代码可以“正常运行”也是一件非常奇怪的事情。 所以正确的做法应该是:设置和缓存区一样长的读取字符。unsigned char RecieveBuffer[8]; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { UNUSED(huart); HAL_UART_Transmit(&huart1,&RecieveBuffer,8 ,100); // 将接收到的数据再通过串口发送出去 HAL_UART_Receive_IT(&huart1,&RecieveBuffer,8); //使能接收中断,RecieveBuffer的类型是uint8_t } 这样子就可以实现每接收8个字符就调用中断一次 可以看到,我们正常的发送了8个字符。 但是这样子有一个非常非常致命的缺点! 由于我们的代码每8个才会接收一次,所以当我们的发送的数量小于8时,必须发送多次才能会触发一次调用。 注意,这里的每次我们都是先发送再接收,因为初始化的时候是启用了接收。 当我们发送3次“123”时才能让接收的字符>=8,我们把前八个发送了出去,此时缓存区剩下了一个字符是“3” 第二次我们发送了三次“123123123”,再次让字符数量>=8,这时候把缓存区的八个“31231231”发送出去再接收后面的“23”,这时候缓存区的数据为:"23 "。 最后我们第三次发送“123”的时候,由于23 + 123 + 123 这时候,虽然这里按理来说正好八个,但是实际测下来,当这时候接收的时候系统就会卡死。 所以,实际上这种方法必须保证发送端的数据是完完整整的八个八个发送,多一个少一个都不行。 解法 事实上最标准的做法是单个字符单个字符处理。定义一个大的缓存区,用来存储接收到的字符并且确立一个结束符来确定数据流的结束。if((USART_RX_STA&0x8000)==0)//接收未完成 { if(USART_RX_STA&0x4000)//接收到了0x0d { if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始 else USART_RX_STA|=0x8000; //接收完成了 } else //还没收到0X0D { if(Res==0x0d)USART_RX_STA|=0x4000; else { USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ; USART_RX_STA++; if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收 } } } 上述是正点原子的官方例程,其中USART_RX_STA是接收到的字符,即是确定一个结束符\r\n来作为一串字符的结尾并且检测长度是否超出长度。 这样子的代码容错率非常高,也确定了一个规范,并且避免了缓存区溢出的情况。
实在太懒于是不想取名
3 10 嘉立创PCB
STM32基于CubeMX的ADC实现
什么是ADC?ADC是模数转换器(Analog-to-Digital Converter)的缩写,它是一种将模拟信号转换为数字信号的设备。在嵌入式系统中,ADC常用于将传感器产生的模拟信号转换为数字信号,以便微控制器进行处理。ADC是非常常用的器件,所以应该学会如何使用。STM32中的ADCSTM32微控制器系列是由STMicroelectronics推出的一系列32位ARM Cortex-M处理器的嵌入式系统。STM32系列通常配备了内置的ADC单元,使其能够轻松地进行模拟信号的数字化转换。STM32 ADC的主要特点多通道: STM32 ADC通常具有多个通道,允许同时转换多个模拟信号。每个通道可以连接到不同的传感器或电压源。分辨率: 分辨率表示ADC能够将模拟信号分成多少个离散的步骤。常见的分辨率有12位和16位,分辨率越高,转换精度越高。采样速率: 采样速率是ADC每秒对模拟信号进行转换的次数。STM32 ADC通常具有可调节的采样速率,允许根据应用的要求进行优化。触发模式: ADC可以通过软件或外部触发启动转换。这使得可以根据需要灵活地控制转换的时机。其实STM32的ADC可以分为单通道与多通道两种。单通道即使用一个IO来实现ADC,多通道也顾名思义,使用多个通道ADC时如何处理。在CubeMX中选择好对应的芯片,配置好时钟,开启串口方便调试。选择具有ADC功能的IO,点击开启ADC。设置ADC的模式,单通道的话大部分都不用改。然后就可以生成我们的工程了。代码配置#include [removed] int fputc(int ch, FILE *f) { // 发送单个字符 HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY); // 返回发送的字符 return ch; } 在Uart.c中重定向我们的串口,方便使用Printf函数。 while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ HAL_ADC_Start(&hadc1);//轮询模式开启ADC HAL_ADC_PollForConversion(&hadc1, 50);//等待ADC转换结束 int Value = HAL_ADC_GetValue(&hadc1);//获取ADC转换的结果 printf("ADC:%d\r\n",Value);//打印ADC } 可以看到我们实现了单通道ADC。 多通道ADC,我们使用多通道间断模式 开启多个ADC通道。 这里必 须使能扫描模式和间断模式 通道数设置为3,顺序为12,13,14. while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ HAL_ADC_Start(&hadc1);//轮询模式开启ADC HAL_ADC_PollForConversion(&hadc1, 50);//等待ADC转换结束 for(int i = 1;i[removed]
实在太懒于是不想取名
0 2 嘉立创PCB
STM32F407基于Cubemx的ADC采集+DMA传输实现简易示波器
示波器,厚礼协好贵,但是有时候还是很想看波形怎么办?我们可以使用STM32的ADC配合DMA连续采集波形数据之后利用串口示波器来显示波形。本期教大伙如何使用STM32 关于ADC配置以及DMA配置。1. 什么是DMA?Direct Memory Access(DMA): 直接内存操作!是一种允许外设之间或外设和内存之间直接进行数据传输的技术,相当于直接把数据搬到存储区,无需CPU的干预。这提高了数据传输的效率,同时释放了CPU用于其他任务。2. STM32中的DMA特性:多通道支持: STM32的DMA控制器通常支持多个通道,每个通道可以连接到不同的外设或内存区域。内存到内存传输: DMA可以在两个内存区域之间传输数据,而不涉及外设。内存到外设传输: DMA可以从内存传输数据到外设,例如将数据发送到USART或SPI外设。外设到内存传输: DMA可以从外设接收数据并将其存储到内存中,例如从ADC获取数据。循环模式: DMA支持循环模式,即在传输完成后重新开始,无需重新配置。传输方向和数据宽度: DMA支持不同的传输方向(内存到外设、外设到内存等)和数据宽度(8位、16位、32位)。3. DMA的工作原理:配置: 在DMA传输之前,需要配置DMA控制寄存器,包括源地址、目的地址、数据宽度、传输方向等。触发: 一旦配置完成,DMA可以由硬件或软件触发开始传输。(本期我们选择软件触发)中断: DMA传输完成时,可以触发中断以通知CPU。循环传输: DMA可以配置为在传输完成后自动重新开始,形成循环传输。4. 应用场景:DMA在需要大量数据传输的应用中特别有用,例如音频处理、图像处理、通信协议等。 时钟配置首先在CubeMX中选择我们的芯片,配置好时钟树以及时钟。将PA1(或者其他IO)配置为ADC_IN以及模拟模式。 在ADC配置中,开启DMA传输,模式为循环模式! 开启ADC连续转换以及DMA请求~ 防止ADC只采样一次以及DMA只工作一次。 接下来就是创建工程了。 定义一个全局变量来存放DMA读取的内容HAL_ADC_Start_DMA(&hadc1,(uint32_t *)ADC_Value,100); 开启DMA,传入相关参数和存储区。for(int i = 0;i[removed]
实在太懒于是不想取名
0 1 嘉立创PCB
C++中的类和对象:面向对象编程的基石
在计算机编程的世界中,C++是一门备受推崇的编程语言,它的强大之处之一就是支持面向对象编程(Object-Oriented Programming,OOP)。在C++中,类和对象是面向对象编程的核心概念,它们为程序员提供了一种结构化的方式来组织和管理代码。1. 类(Class):抽象的蓝图在C++中,类是一种用户定义的数据类型,用于封装数据和方法。可以将类看作是对象的抽象蓝图,其中包含了描述对象属性和行为的成员变量和成员函数。通过定义类,程序员可以创建自己的数据类型,从而更好地组织和管理代码。 例如我们可以把人作为一个对象,他有基本的属性:性别、身高、年龄等等 也有基本的行为:吃饭、睡觉、学习、玩游戏等等。1.1 类的基本结构一个简单的类通常包含以下几个要素:class MyClass { private: // 成员变量(属性) int myInt; double myDouble; public: // 构造函数 MyClass() { myInt = 0; myDouble = 0.0; } // 成员函数(方法) void setValues(int i, double d) { myInt = i; myDouble = d; }     ~MyClass() { } }; class + 类名 的方法创建我们的类。 类中的变量可以称作成员,他们有一些访问属性:Protect:保护,private:私密,public:公开 故名思意除了public其他的变量都不可以在类外被访问。 类中有两种特殊的函数 与类名相同的名称被称作构造函数,我们在创建类的同时会调用构造函数! 同样的还有一个特殊的函数:析构函数,和析构函数差不多,但是需要多一个~符号,这个函数会在类被释放的时候调用。1.2 类的实例化通过定义类,我们可以创建类的实例,也称为对象。对象是类的具体实体,可以调用类中定义的方法,访问和修改成员变量。int main() { // 创建类的实例 MyClass myObject; // 调用成员函数     myObject.setValues(42, 3.14); return 0; } 2. 对象(Object):实体化的具体实例对象是类的实例,是具体存在的数据单元。在C++中,通过创建对象,我们可以利用类定义的结构来存储和操作数据。2.1 对象的特性对象具有以下几个重要的特性:封装性(Encapsulation): 类的内部实现对外部是不可见的,只有通过公共接口(成员函数)才能访问类的属性和方法,确保了数据的安全性和代码的模块化。继承性(Inheritance): 类可以通过继承从其他类中获取属性和方法,实现代码的重用和扩展。多态性(Polymorphism): 不同的类可以具有相同的接口,但表现出不同的行为,提高了代码的灵活性和可维护性。2.2 对象的生命周期对象的生命周期从创建到销毁,包括对象的构造、使用和析构阶段。在C++中,构造函数和析构函数负责对象的初始化和清理工作。int main() { // 创建对象,调用构造函数 MyClass obj; // 对象在main函数结束时销毁,调用析构函数 return 0; C++中的类和对象为程序提供了一种灵活而强大的编程范式,使得代码更易理解、维护和扩展。通过合理利用类和对象,程序员可以更好地组织和管理代码,提高代码的可读性和可维护性。
实在太懒于是不想取名
3 5 嘉立创PCB
ESP32使用Arduino IDE开启Wifi AP模式并获取所连接设备地址
前面几期利用.NET MAUI我们开发了一个Android应用用来接收ESP32的图片数据以及制作了一个摇杆方便我们操控。 但是有一点,我们发送图片以及交流的IP都是固定的。 可是,IP地址会随着网络以及设备发生变换,那么我们怎么知道每次的IP地址呢。 本期我们将介绍ESP32如何开启AP模式来让手机进行连接并且获取所连接的设备的IP地址。 在ESP的AP模式下,设备像一个Wi-Fi网络中的路由器一样,允许其他设备通过Wi-Fi连接到它,从而建立本地网络。这种模式通常用于创建一个局域网(Local Area Network,LAN),其中ESP设备充当中心节点,其他设备可以通过该节点相互通信。 通过AP模式,ESP设备可以提供网络连接、数据传输和通信服务,使其他设备能够连接到互联网或者在局域网内进行数据交换。这对于构建物联网应用和连接智能设备非常有用。代码编写#includeesp_wifi.h#include [removed] 首先需要导入这两个库,分别用来连接WiFi和获取设备IP  WiFi.softAP(ssid, password);//AP模式开启Wifi ip = WiFi.softAPIP();//获取本机IP   Serial.println(ip);//打印IP   WiFi.onEvent(WiFiEvent);//创建一个事件用来监听事件 使用上述语句开启WiFi AP模式,ssid为Wifi的名字,password为Wifi的密码,注意这个密码至少八个字符!! 可以看到可以发现一个ESP32的Wifi.void WiFiEvent(WiFiEvent_t event) {   if(event == 13)   {    wifi_sta_list_t wifi_sta_list; tcpip_adapter_sta_list_t adapter_sta_list; memset(&wifi_sta_list, 0, sizeof(wifi_sta_list)); memset(&adapter_sta_list, 0, sizeof(adapter_sta_list)); esp_wifi_ap_get_sta_list(&wifi_sta_list); tcpip_adapter_get_sta_list(&wifi_sta_list, &adapter_sta_list); for (int i = 0; i < adapter_sta_list.num; i++) { tcpip_adapter_sta_info_t station = adapter_sta_list.sta[i]; Serial.print("station nr "); Serial.println(i + 1); Serial.print("MAC: "); for (int i = 0; i < 6; i++) { Serial.printf("%02X", station.mac[i]); if (i [removed]
实在太懒于是不想取名
0 2 嘉立创PCB
.NET MAUI的Android WiFi图传开发(6)——利用SkiaSharp制作一个摇杆
上期我们利用FFImageLoad实现了图片流的显示,之前也有一期简单介绍了一下利用SkiaSharp实现绘图。 本期我们来实现一个摇杆的实现。public class DrawAble {     private readonly Rocker view; public DrawAble(Rocker view) {         this.view = view; } private void Draw_Circle(SKSurface surface, SKRect bounds) { float centerX = bounds.MidX; float centerY = bounds.MidY; Midx = centerX; Midy = centerY; // 计算圆的半径(使用ChargingRingDrawable类中的rad属性) float radius = CirCleRad; // 使用SKPaint对象定义圆的样式(颜色、线条宽度等) using (SKPaint paint = new SKPaint()) { paint.Color = SKColors.Blue; paint.Style = SKPaintStyle.Stroke; paint.StrokeWidth = 20; // 画圆 surface.Canvas.DrawCircle(centerX, centerY, radius, paint); }     } public void Draw(SKSurface surface, SKRect bounds) { surface.Canvas.Clear();         Draw_Circle(surface, bounds);//画圆轮廓     } } 首先定义两个类,一个是画板类,他必须有最基本的Draw函数用来给Sharp实现接口。 其构造函数传入我们的Rocker类,这个类我们在下面定义。 public partial class Rocker : SKCanvasView {         private readonly DrawAble drawable; public Rocker() {             this.EnableTouchEvents = true;// this.drawable = new ChargingRingDrawable(this);         } protected override void OnPaintSurface(SKPaintSurfaceEventArgs e) { this.drawable.Draw(e.Surface, e.Info.Rect); this.InvalidateSurface(); } protected override void OnTouch(SKTouchEventArgs e) {             base.OnTouch(e);         } } } 接着定义一个类,命名为Rocker,这个类抽象自SKCanvasView(SkiaSharp的控件) 定义一个画板,构造的时候传入自身。 我们要保留两个处理函数,一个是OnPaintSurface,我们的控件刷新就会调用这个函数,还有一个是OnTouch函数,这个函数用来处理我们的触摸事件。private void Draw_Circle(SKSurface surface, SKRect bounds) {     float centerX = bounds.MidX;//获得画板中心 float centerY = bounds.MidY;     Midx = centerX;//用一个参数保存画板中心 Midy = centerY; // 计算圆的半径(使用ChargingRingDrawable类中的rad属性) float radius = CirCleRad; // 使用SKPaint对象定义圆的样式(颜色、线条宽度等) using (SKPaint paint = new SKPaint()) { paint.Color = SKColors.Blue; paint.Style = SKPaintStyle.Stroke; paint.StrokeWidth = 20; // 画圆 surface.Canvas.DrawCircle(centerX, centerY, radius, paint); } } 在DrawAble类中有这样子一个函数,其作用是画一个基本的圆,我设置的大小是400像素。private void Draw_InsertCircle(SKCanvas canvas, SKPoint touchLocation, float radius) { // 控件的中心点 float centerX = Midx; float centerY = Midy; // 计算手的位置与圆的交点 SKPoint intersectionPoint = CalculateIntersectionPoint(centerX, centerY, 300, touchLocation); // 使用SKPaint对象定义圆的样式(颜色、线条宽度等) using (SKPaint paint = new SKPaint()) { paint.Color = SKColors.Red; paint.Style = SKPaintStyle.Fill; // 在交点位置绘制圆 canvas.DrawCircle(intersectionPoint.X, intersectionPoint.Y, radius, paint); LocationPoint = intersectionPoint; } } private SKPoint CalculateIntersectionPoint(float centerX, float centerY, float radius, SKPoint touchLocation) { // 计算手的位置与圆的交点 float dx = touchLocation.X - centerX; float dy = touchLocation.Y - centerY; float distance = (float)Math.Sqrt(dx * dx + dy * dy); // 如果距离超出阈值,将交点移动到圆上最近的点 if (distance > radius) { float scale = radius / distance; float intersectionX = centerX + dx * scale; float intersectionY = centerY + dy * scale; return new SKPoint(intersectionX, intersectionY); } // 如果距离未超出阈值,则返回手的位置作为交点 return touchLocation; } 接着画内部的摇杆内容,我们计算这个圆和控件中心的位置,设置一个阈值,保证我们画的圆在这个阈值之内,如果超过了阈值,就计算手的位置和圆的交点,再进行画圆。 实现这样子的效果。 接着,我们补全Rocker中触摸事件protected override void OnTouch(SKTouchEventArgs e) { base.OnTouch(e); switch (e.ActionType) { case SKTouchAction.Pressed: // 处理按下事件 break; case SKTouchAction.Moved: // 处理移动事件 SKPoint touchLocation = e.Location; drawable.InsertCirclePoint = touchLocation; OnPositionChanged(new SKPoint(drawable.LocationPoint.X-drawable.Midx,drawable.LocationPoint.Y-drawable.Midy)); break; case SKTouchAction.Released: SKPoint InsertCirclePoint = new SKPoint(drawable.Midx,drawable.Midy); drawable.InsertCirclePoint = InsertCirclePoint; // 使得画布无效,触发重绘 InvalidateSurface(); OnPositionChanged(new SKPoint(0,0)); break; case SKTouchAction.Cancelled: // 处理取消事件 break; } // 标记事件已处理 e.Handled = true; } 当移动时,将位置传给DrawAble中,DrawAble会根据手的位置绘制摇杆位置。 并且在放下的时候重新归位。 同时我们定义一个事件,向MainPage中传入位置信息。private void Rocker_PositionChanged(object sender, SKPoint newPosition) {    Label.Text = $"Position: ({newPosition.X}, {newPosition.Y})"; } MainPage中打印我们的位置信息。
实在太懒于是不想取名
0 1 嘉立创PCB
图片流的传递以及显示优化
在第三期中,我们成功的利用TCP传输图片。 本期我们将图片连贯起来,利用ESP32_Cam传输图片,关于ESP32传输图片可以参考前面的公众号 库安装 因为使用原来的Image控件的话会导致图片刷新的时候一闪一闪的很抽象。 FFImageLoading是一个用于Xamarin和.NET应用程序的强大的图片加载库,它支持缓存、异步加载和其他图像处理功能 在Nuget中获取FFImageLoading.Maui 在Xaml文件中导入命名空间。 添加一个CachedImage控件。 将之前代码中的文件路径更新替换成我们的新控件。ESP32源码 #includeesp_camera.h#include [removed] #include [removed] // // WARNING!!! PSRAM IC required for UXGA resolution and high JPEG quality // Ensure ESP32 Wrover Module or other board with PSRAM is selected // Partial images will be transmitted if image exceeds buffer size // // You must select partition scheme from the board menu that has at least 3MB APP space. // Face Recognition is DISABLED for ESP32 and ESP32-S2, because it takes up from 15 // seconds to process single frame. Face Detection is ENABLED if PSRAM is enabled as well // =================== // Select camera model // =================== //#define CAMERA_MODEL_WROVER_KIT // Has PSRAM //#define CAMERA_MODEL_ESP_EYE // Has PSRAM //#define CAMERA_MODEL_ESP32S3_EYE // Has PSRAM //#define CAMERA_MODEL_M5STACK_PSRAM // Has PSRAM //#define CAMERA_MODEL_M5STACK_V2_PSRAM // M5Camera version B Has PSRAM //#define CAMERA_MODEL_M5STACK_WIDE // Has PSRAM //#define CAMERA_MODEL_M5STACK_ESP32CAM // No PSRAM //#define CAMERA_MODEL_M5STACK_UNITCAM // No PSRAM #define CAMERA_MODEL_AI_THINKER // Has PSRAM //#define CAMERA_MODEL_TTGO_T_JOURNAL // No PSRAM //#define CAMERA_MODEL_XIAO_ESP32S3 // Has PSRAM // ** Espressif Internal Boards ** //#define CAMERA_MODEL_ESP32_CAM_BOARD //#define CAMERA_MODEL_ESP32S2_CAM_BOARD //#define CAMERA_MODEL_ESP32S3_CAM_LCD //#define CAMERA_MODEL_DFRobot_FireBeetle2_ESP32S3 // Has PSRAM //#define CAMERA_MODEL_DFRobot_Romeo_ESP32S3 // Has PSRAM #include "camera_pins.h" // =========================== // Enter your WiFi credentials // =========================== const char* ssid = "yiyang"; const char* password = "lly20030601"; const char* serverUrl = "http://192.168.137.1:8081/"; // 替换为你的服务器地址 void setupLedFlash(int pin); void setup() { Serial.begin(115200); Serial.setDebugOutput(true); Serial.println(); //igitalWrite(4, HIGH); camera_config_t config; config.ledc_channel = LEDC_CHANNEL_0; config.ledc_timer = LEDC_TIMER_0; config.pin_d0 = Y2_GPIO_NUM; config.pin_d1 = Y3_GPIO_NUM; config.pin_d2 = Y4_GPIO_NUM; config.pin_d3 = Y5_GPIO_NUM; config.pin_d4 = Y6_GPIO_NUM; config.pin_d5 = Y7_GPIO_NUM; config.pin_d6 = Y8_GPIO_NUM; config.pin_d7 = Y9_GPIO_NUM; config.pin_xclk = XCLK_GPIO_NUM; config.pin_pclk = PCLK_GPIO_NUM; config.pin_vsync = VSYNC_GPIO_NUM; config.pin_href = HREF_GPIO_NUM; config.pin_sccb_sda = SIOD_GPIO_NUM; config.pin_sccb_scl = SIOC_GPIO_NUM; config.pin_pwdn = PWDN_GPIO_NUM; config.pin_reset = RESET_GPIO_NUM; config.xclk_freq_hz = 20000000; config.frame_size = FRAMESIZE_UXGA; config.pixel_format = PIXFORMAT_JPEG; // for streaming //config.pixel_format = PIXFORMAT_RGB565; // for face detection/recognition config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; config.fb_location = CAMERA_FB_IN_PSRAM; config.jpeg_quality = 12; config.fb_count = 1; // if PSRAM IC present, init with UXGA resolution and higher JPEG quality // for larger pre-allocated frame buffer. if(config.pixel_format == PIXFORMAT_JPEG){ if(psramFound()){ config.jpeg_quality = 10; config.fb_count = 2; config.grab_mode = CAMERA_GRAB_LATEST; } else { // Limit the frame size when PSRAM is not available config.frame_size = FRAMESIZE_SVGA; config.fb_location = CAMERA_FB_IN_DRAM; } } else { // Best option for face detection/recognition config.frame_size = FRAMESIZE_240X240; #if CONFIG_IDF_TARGET_ESP32S3 config.fb_count = 2; #endif}#if defined(CAMERA_MODEL_ESP_EYE) pinMode(13, INPUT_PULLUP); pinMode(14, INPUT_PULLUP); #endif // camera init esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("Camera init failed with error 0x%x", err); return; } sensor_t * s = esp_camera_sensor_get(); // initial sensors are flipped vertically and colors are a bit saturated if (s->id.PID == OV3660_PID) { s->set_vflip(s, 1); // flip it back s->set_brightness(s, 1); // up the brightness just a bit s->set_saturation(s, -2); // lower the saturation } // drop down frame size for higher initial frame rate if(config.pixel_format == PIXFORMAT_JPEG){ s->set_framesize(s, FRAMESIZE_QVGA); } #if defined(CAMERA_MODEL_M5STACK_WIDE) || defined(CAMERA_MODEL_M5STACK_ESP32CAM) s->set_vflip(s, 1); s->set_hmirror(s, 1); #endif#if defined(CAMERA_MODEL_ESP32S3_EYE) s->set_vflip(s, 1); #endif // Setup LED FLash if LED pin is defined in camera_pins.h #if defined(LED_GPIO_NUM) setupLedFlash(LED_GPIO_NUM); #endif WiFi.begin(ssid, password); WiFi.setSleep(false); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected"); Serial.print("Camera Ready! Use 'http://"); Serial.print(WiFi.localIP()); Serial.println("' to connect"); } void loop() { camera_fb_t *fb = esp_camera_fb_get(); if (fb == nullptr) { Serial.println("Camera capture failed"); } else { Serial.println("Camera capture OK"); // 将图像帧转换为 Stream WiFiClient client; client.connect("192.168.137.226", 8081); client.write(fb->buf, fb->len); } esp_camera_fb_return(fb); }
实在太懒于是不想取名
0 3 嘉立创PCB
.NET MAUI的Android WiFi图传开发(4)——Android设备利用官方库画图和Sk
这期究极折磨,因为资料太少了,然后出了问题真不知道怎么解决。(已解决)好歹彻夜研究了好几天,总算是起步了。 本来我是想用Skias.Maui.Controls;的SKasView来当画板的,但是一点一点资料都没有,官方的API文档又找不到相关的设置。然后退而求其次用SKCanvasView的基类,官方的 Microsoft.aphics;来画图,用SkiaSharp来快速渲染,但是就这个工作都折磨死我了。然后仔细的阅读了官方的启动手册(这个库我没有找到API文档)终于解决了画图的问题。首先我们要在App.xaml.cs中定义我们的Draw函数。public class Graphiwabawable { public void Draw(ICanvas, ReRect) { using (var paint = new SKPaint()) { paint.Color = SKColors.Red; canvas.DrawRecle(0,0,50,50); } } } 可以看到,我们是成功的画了个框。但是我们可以导入更加便利的画图库:SkiaSharp导入SkiaStrols,这样子我们就可以使用其控件。 添加一个画板控件,可以命名,绑定一个PaintSurface刷新,Touch触摸处理。然后,最最重要的,在MauiProgram.cs中,添加初始化语句,这句话卡了我四五天!!protected override void OnPurface(SKPaintSuntArgs e) { this.drawable.Draw(e.Surface, e.Info.Rect); this.Inurface(); } 补全这些函数,我们就可以使用我们的控件啦。可以看到画了个框框 这个是我们的基础画图库,更新UI就会调用这里。 接着在MainPage.xaml中注册我们的画板,并且为我们的控件绑定画板。[removed]
实在太懒于是不想取名
2 3 嘉立创PCB
看看大家的毕设做什么
想问一下大家的毕设都在做什么题目[微笑]
实在太懒于是不想取名
1 4 硬创社
.NET MAUI的Android WiFi图传开发(3)——Android设备接收TCP信息并显示
这几天折磨死我了,因为这玩意我也是自己在琢磨,然后资料也没有多少,简直就是自己慢慢摸索。 本期介绍如何利用TCP接收图片信息并显示。 首先我们先新建一个类,将我们想要的内容全部写到这个类上(类的代码会放在最后面) cpListener用来创建监听,tcpClient用以发送。public AndroidWifiService() { } ~AndroidWifiService() { tcpClient?.Close(); tcpClient = null; tcpListener = null; MessageReceived = null; } public AndroidWifiService(IPAddress address,int Port) { StartListening(address, Port); } public AndroidWifiService(string address, int Port) { StartListening(IPAddress.Parse(address), Port); } 提供三种类型的构造函数和析构函数。protected virtual void OnMessageReceived(byte[] buffer) { // 触发事件 MessageReceived?.Invoke(this, buffer); } 设置一个事件,用以传递监听到的图片信息。public async void StartListening(IPAddress IP,int Port) { tcpListener = new TcpListener(IP, Port); tcpListener.Start(); while (true) { tcpClient = await tcpListener.AcceptTcpClientAsync(); Task.Run(() => HandleClient(tcpClient)); } } private void HandleClient(TcpClient client) { try { NetworkStream stream = client.GetStream(); byte[] buffer = new byte[1024]; // 使用固定大小的缓冲区 using (MemoryStream ms = new MemoryStream()) { int bytesRead; while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) { ms.Write(buffer, 0, bytesRead); } if (ms.Length > 0) { // 在主线程上更新 UI MainThread.BeginInvokeOnMainThread(() => { OnMessageReceived(ms.ToArray()); }); } } } catch (Exception ex) { Console.WriteLine($"Exception in HandleClient: {ex.Message}"); } } 创建一个监听函数,注意的是,我们是接受完一次完整的流再传递信息,而不是边接收边传递。private void OnMessageReceived(object sender, byte[] message) { MainThread.BeginInvokeOnMainThread(async () => { try { using (MemoryStream ms = new MemoryStream(message)) { SKBitmap bitmap = SKBitmap.Decode(ms); AndroidSaveClass save = new AndroidSaveClass(); string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "MAUI_Picture_Save"); if (!save.DoesFolderExist(folderPath)) { Directory.CreateDirectory(folderPath); } // 生成文件路径 string uniqueFileName = $"sample_{DateTime.Now:yyyyMMddHHmmssfff}.png"; string filePath = Path.Combine(folderPath, uniqueFileName); // 将 SKBitmap 编码并保存为 PNG 文件 using (var image = SKImage.FromBitmap(bitmap)) using (var data = image.Encode(SKEncodedImageFormat.Png, 100)) using (var stream = File.OpenWrite(filePath)) { data.SaveTo(stream); } Picture.Source = filePath; } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } }); } 再MainPage.xmal.cs中,我们新建一个这个类的变量,监听我们的窗口。并且设置回调函数用来处理事件。 事件中,我们将图片的字节数组转为图片,之后保存到程序的文件目录中,并且使用时间戳防止图片重复。 之后用Image控件来绑定图片源显示图片。
实在太懒于是不想取名
3 9 嘉立创PCB
.NET MAUI的Android WiFi图传开发(2)——Android设备利用TCP传输信息
本期我们实现TCP的数据接收和发送。 public interface IWifiService { string GetIpAddress(); bool TCP_Send(string message, IPAddress iP, int Port); } 在上一期的IWifiService类中,我们添加TCP_Send包括App.xaml.cs和MainPage.xaml.cs中,其中参数为message消息,IPAddress IP地址,还有Port端口号。   接着在Android中实现具体Android功能的实现。 public bool TCP_Send(string message,string IP ,int Port) { try { if (tcpClient == null || !tcpClient.Connected) { // 创建并连接到服务器 tcpClient = new TcpClient(IP,Port); } // 发送消息 using (NetworkStream stream = tcpClient.GetStream()) { byte[] buffer = Encoding.UTF8.GetBytes(message); stream.Write(buffer, 0, buffer.Length); } return true; } catch (Exception ex) { Console.WriteLine($"TCP_Send Exception: {ex.Message}"); return false; } } 在功能的实现中,使用网络流来将message发送出去。 接着在MainPage.xaml.cs中调用这个函数。AndroidWifiService wifiService = new AndroidWifiService(); wifiService.TCP_Send("Hello", IPAddress.Parse("192.168.137.1"), 8081); 向我们的主机地址:192.168.137.1的8081端口发送数据(这里应该要用cmd窗口ipconfig命令来查看主机的IP地址) 接着打开我们的TCP助手,这里用我自己写的TCP助手 可以看到发送数据成功。 接收数据 接收数据比较麻烦,因为我想给他封装成一个类,我们可以异步开启一个监听用来监听收到的消息,当我们收到消息时我们订阅一个事件,利用这个事件向主UI发送已经接收到了消息的指令提醒主函数开始处理接收到的消息。public event EventHandler[removed] MessageReceived; 订阅一个事件。 public async void StartListening(IPAddress IP,int Port) { tcpListener = new TcpListener(IP, Port); tcpListener.Start(); while (true) { tcpClient = await tcpListener.AcceptTcpClientAsync(); Task.Run(() => HandleClient(tcpClient)); } } private void HandleClient(TcpClient client) { try { NetworkStream stream = client.GetStream(); byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) { string receivedMessage = Encoding.UTF8.GetString(buffer, 0, bytesRead); // 在主线程上更新UI Device.InvokeOnMainThreadAsync(() => { // 在UI上显示接收到的消息 OnMessageReceived(receivedMessage); }); } } catch (Exception ex) { Console.WriteLine($"Exception in HandleClient: {ex.Message}"); } } 异步开启监听,接着如果收到了消息,就利用事件将消息传递出去。public MainPage() { InitializeComponent(); #if ANDROID AndroidWifi.MessageReceived += OnMessageReceived; AndroidWifi.StartListening(IPAddress.Parse("192.168.137.200"),8099); #endif } 初始化函数中,订阅这个事件并且开启特定端口的监听。 并且为订阅的函数进行定义。 private void OnMessageReceived(object sender, string message) { // 处理接收到的消息     Label.Text = message; } 发送端下面展示主函数的所有代码using System; using System.Collections.Generic; using System.IO.Ports; using System.Net; using System.Net.Sockets; using Microsoft.Maui.Controls; using Microsoft.Maui.Controls.PlatformConfiguration; #if __ANDROID__ using System.Text; using MAUIapp.Platforms; using MAUIapp.Platforms.Android.Servers; #endif namespace MAUIapp { public partial class MainPage : ContentPage { #if __ANDROID__ AndroidWifiService AndroidWifi = new AndroidWifiService(); private void OnMessageReceived(object sender, string message) { // 处理接收到的消息 Label.Text = message; // 在这里添加其他处理逻辑 } #endif string ReciveBuff; public MainPage() { InitializeComponent(); #if ANDROID AndroidWifi.MessageReceived += OnMessageReceived; AndroidWifi.StartListening(IPAddress.Parse("192.168.137.200"),8099); #endif } private void OnButtonClick(object sender, EventArgs e) { #if __ANDROID__ try { AndroidWifi.TCP_Send("Hello","192.168.137.1",8099); //Label.Text = "OK"; } catch (Exception ex) { // 捕捉异常并在调试器中查看 Label.Text = ($"Exception: {ex.Message}"); } #endif } } }
实在太懒于是不想取名
1 4 嘉立创PCB
求嵌入式大赛选题推荐
嵌入式大赛的ST赛道大家有没有什么好的选题方向呀!
实在太懒于是不想取名
0 3 嘉立创PCB
.NET MAUI的Android WiFi图传开发(1)——Android设备获取Wifi的IP地
上期不是介绍了一下怎么使用.NET MAUI来进行自己手机的Android代码调试嘛。本期介绍如何在Android平台上来实现Wifi IP地址的获取。首先打开我们的项目,在MainPage.xaml中加入我们的页面布局 我们放置一个容器StackLayout,他的Orientation属性设置为Vertical,代表这个容器的内容是垂直分布,控件间距为10分别添加按钮,标签,图片。按钮设置回调函数OnButtonClickprivate void OnButtonClick(object sender, EventArgs e) {        } 接着就可以在MainPage.xaml.cs中添加我们的回调函数。Wifi地址获取实现之后我们需要实现Android设备获取IP地址的函数。namespace MAUIapp { public partial class App : Application { public App() { InitializeComponent(); MainPage = new AppShell(); } } public interface IWifiService { string GetIpAddress(); } } 首先在App.cs中添加一个Wifi的类(名字随意),注意和上面的App的类分开,这个类我们先添加一个内容,获取IP地址的函数。 我们放置一个容器StackLayout,他的Orientation属性设置为Vertical,代表这个容器的内容是垂直分布,控件间距为10 分别添加按钮,标签,图片。按钮设置回调函数OnButtonClickprivate void OnButtonClick(object sender, EventArgs e) {        } 接着就可以在 MainPage.xaml.cs中添加我们的回调函数。 Wifi地址获取实现 之后我们需要实现Android设备获取IP地址的函数。namespace MAUIapp { public partial class App : Application { public App() { InitializeComponent(); MainPage = new AppShell(); } } public interface IWifiService { string GetIpAddress(); } } 首先在App.cs中添加一个Wifi的类(名字随意),注意和上面的App的类分开,这个类我们先添加一个内容,获取IP地址的函数。 接着在Playforms分平台中,添加文件夹分类我们的文件,添加Wifi的类,我们在其中实现我们的代码。namespace MAUIapp.Platforms.Android.Servers { public class AndroidWifiService : IWifiService { public string GetIpAddress() { // 获取当前应用程序的上下文 Context context = MauiApplication.Current.ApplicationContext; // 使用上下文获取 WifiManager WifiManager wifiManager = (WifiManager)context.GetSystemService(Context.WifiService); // 检查 WiFi 是否启用 if (wifiManager != null && wifiManager.IsWifiEnabled) { // 获取连接的 WiFi 信息 WifiInfo wifiInfo = wifiManager.ConnectionInfo; // 获取 IP 地址 int ipAddress = wifiInfo.IpAddress; string ipAddressString = FormatIpAddress(ipAddress); return ipAddressString; } // 如果 WiFi 未启用或未连接,则返回 "No IpAddress" return "No IpAddress"; } private string FormatIpAddress(int ipAddress) { byte[] byteAddress = BitConverter.GetBytes(ipAddress); IPAddress ip = new IPAddress(byteAddress); return ip.ToString(); } } } 这里我们重新创建了一个类,用来继承父类,为Android提供特定接口。 我们使用Context类来获取Wifi的信息,在 Android 开发中,Context 是一个关键类,它是一个抽象类,继承自 Object 类。Context 类提供了应用程序的全局信息和访问应用程序资源的方法。它是 Android 应用程序中的关键组件,几乎在每个 Android 应用程序中都会用到。 利用WifiManage来获取Wifi设备信息。 之后写一个重新写一个函数FormatIpAddress来获取IP地址。 在MainPage.xaml.cs中调用我们的函数。 private void OnButtonClick(object sender, EventArgs e) { #if __ANDROID__ try { AndroidWifiService wifiService = new AndroidWifiService(); string ipAddress = wifiService.GetIpAddress(); Label.Text = ipAddress; } catch (Exception ex) { // 捕捉异常并在调试器中查看 Label.Text = ($"Exception: {ex.Message}"); } #endif #if WINDOWS10_0_17763_0_OR_GREATER Label.Text = "这里是Windows专属"; #endif } public interface IWifiService { string GetIpAddress(); } 记得加上#if __ANDROID__来区分不同的设备,以及这里重新声明一下IWifiServerce。 运行我们的代码。
实在太懒于是不想取名
1 6 嘉立创PCB