运算放大器之比例放大器的设计
运算放大器是电子设计中非常重要的一个器件, 在实际电路中,通常结合反馈网络共同组成某种功能模块。由于早期应用于模拟计算机中,用以实现数学运算,故得名“运算放大器”,此名称一直延续。 如图为运算放大器作为跟随器,其目的是使输入端的电压能够延续到输出端的电压并起到隔离电路的作用。 理想运算放大器有两个非常重要的基本性质: 虚短:所谓的虚短是认为运算放大器的正输入端和负输入端的电压总是保持一致,类似于短路,但并不是真正的短路。 虚断:虚断则是认为正输入端和负输入端的输入电流为0,可以认为正输入端和负输入端和电路是处于断路的状态。 根据这两条性质,我们可以利用运算放大器实现电压的四则运算。 我来做一个例子:同相比例放大器 同向比例放大器的目的是将电压放大一定的倍数并保持方向的一致。 例如实现Uo = 5 * Ui,其中Uo是输出电压,Ui是输入电压,5是放大倍数A。 以下是一个经典的同相比例放大电路。 我们可以看到输入电压是输入电压的5倍,并且二者的方向保持一致。我们将这种电路称作同相比例放大器。 下面我来推导一下这个电路。 可以看到,根据虚断的原则,流入负输入端的电流为0,因此电阻R1和R2的电流相同。 也可以得到: 而后根据虚短的原则,U- = U+,因此,Uo = (R1+R2)/R1 * Ui 这就是同相比例放大器。 同样的,我们也可以设计出反向比例放大器,实现Uo = -AUi 可以看到,我们改变了放大倍数同样的,我们也改变了输出波形的方向,实现了反向的操作。 这里的Uo = -Ui * (R2/R1)这里不做细推,读者可以自行推理,注意电流方向。 我们也可以使用比例运算放大器的级联放大的方式(因为不推荐单独一个运放放大太大的倍数)实现放大几十/百倍的需求。 在使用中,我们常用分贝来表示放大倍数。我们用dB = 20lg(A)其中,A是电路的放大倍数,Uo/Ui。例如20dB的放大倍数就是20 = 20lgA,A = 10,就是放大十倍,记住这里的Lg是以十为底的对数。
实在太懒于是不想取名
1 6 嘉立创PCB
如何自制一个简单的血氧仪
 这几天爷爷因病住院,总是因为有痰的原因导致血氧指标忽高忽低。血氧这个指标非常重要,血氧饱和度用来判断机体是否缺氧。过低的血氧会导致氧气供应不足出现很多症状。  但是家里又无法进行血氧测量而一台医用血氧仪的价格又是昂贵的,因此我就在想,能不能利用血氧模块自制一个简易的血氧仪,并且使用ESP32设计一个APP来实现远程监控血氧数据。    说干就干,目前常用的血氧测量芯片为MAX30100/30102 利用不同含氧量的血红蛋白对不同波长红外光的吸收率不一样,根据反射回来的波进行处理这种反射的方法来测量血氧(还有一种投射法不一一介绍) 我的目的是:锂电池供电(锂电池充放电路),锂电池升压电路(3.7V转5V),ESP32主控可以远程上传数据,MAX30100模块测量血氧,1.54寸OLED显示。 原理图绘制 为了方便制作,开发板选择ESP32C3开发板,升压和MAX30100均使用现成的模块,充电电路使用TP4057,本来应该是做一个TYPEC供电和锂电池供电电路的,但是嫌麻烦就算了,就单纯的锂电池供电(后续改进,后续改进) PCB布局 布线方面也没有细究,看官老爷凑合看看。由于实在医院熬夜设计的,一次第一版出现了很多问题,望海涵 将开发板锂电池置于反面,MAX30100和升压模块置于正面,充分利用空间,使得我们的设备大小控制在29*54非常小巧,因为两个排针之间的间距是24mm,因此选用宽度为20mm长度为40mm的锂电池,1200mAH的电池。 这个设备还是有非常多的缺点的,例如那俩充放电灯珠会被锂电池挡住,然后ESP32可以选择使用芯片而不是开发板(主要是手上有懒得焊接)。 电路不能使用TypeC直接供电,而是必须将电充上再供电(虽然我觉得没有直接用TypeC供电的必要)。 效果展示 ​只能说非常抽象,因为我的Type-C后面换了封装,于是导致更新PCB的时候电池的电源线和地线居然短接到一起了,害得我拿了把菜刀在磨PCB。但是所幸开发版的Typec是可以直接供电的。 但是电池的设计十分到位,整个装置小巧可用。电池正好塞进了底部的预留空间中。 目前简单的制作了屏幕显示和血氧心率显示以及Wifi连接的功能,后续将升级Wifi的数据远传功能,将数据上传到服务器。 改进缺点,针对电池供电显示位置,电池电路设计,TypeC和锂电池合并供电电路的问题,电量检测的加入(主要是电池的线整个被我切开了,充电电路是可以实现的),升压电路和开发板的分立化。
实在太懒于是不想取名
2 5 嘉立创PCB
555定时器之无稳态电路详解及占空比可调电路的实现
上期我们介绍了如何使用555定时器实现电子门铃,按下开关后门铃发出1.2KHZ的方波持续十秒钟。 其核心是使用无稳态振荡电路来产生方波以及RC电路来实现10秒时间的控制。 本期我们详细讲解一下无稳态振荡电路以及利用无稳态振荡电路的一些拓展。 细说555定时器 555定时器,其内部的主要结构是由两个比较器C1,C2以及一个RS触发器,RS触发器的输出Q经过一个与非门和非门作为输出。并且当OUT输出低电平时,非门前是高电平,NPN三极管导通,7脚导通到地,所以7脚状态和3脚相同,并且其低电平是导通到地。 比较器C1的正输入端电压为2/3VCC,也可以由5脚(CO)决定,比较器C2的正输入端的电压为1/3VCC。当正输入端的电压大于负输入端时输出高电平,反之输出低电平。 RS触发器的真值表如上所示,当RS均为高电平时,Q输出状态不变。否则为对角输出,即Q对应R(这里的R可以看作Reset,S看作Set,分别为高电平有效,低电平有效)。 结合比较器的正输入端电压,我们可以得到555定时器的功能表。 我们结合波形图来进行阐述。 初始状态下电容C上无电荷,电压为0,所以THR,TRI引脚均为低电平,OUT输出高电平。因此DIS(7)三极管不导通。电流经电源->R1->R2->C构成充电路线。 电容器C上电荷量增加,电压上升,上升到1/3VCC的时候,此时RS触发器属于RS均为高电平状态,保持不变,电容器继续充电。 电容上电压到达2/3VCC时,状态改变,OUT输出为低电平,此时DIS(7)经三极管导通到地。此时电容通过电阻R2向DIS放电。 电容电压降低,但是未降到1/3VCC以下,RS触发器均处于高电平状态,Q输出保持不变,因此OUT输出为低电平,DIS依旧导通到地。电容继续放电。 当电容降到1/3VCC时,状态改变,OUT输出高电平,DIS三极管截止,电源向电容充电,之后重复3~4~5的过程形成振荡。 改变相关参数 经典无稳态振荡的两大参数为频率和占空比,我们已经知道了充电回路是R1、R2、C,放电回路是R2、C,因此频率F=1.44/((R1+2R2)*C)其中电阻的单位是kΩ,电容单位为uF,频率单位为kHZ,占空比D=R1/(R1+R2)所以我们可以看到,想要在频率不变的情况下改变占空比显然是不可能的,但是我们可以另辟蹊径,利用2、6脚波形是锯齿波的特性,利用比较器设置一阈值将锯齿波转换为方波 如图,利用运放作为比较器,将锯齿波转为方波,调节R4(R3)改变比较的阈值从而实现占空比的调节
实在太懒于是不想取名
0 7 嘉立创PCB
555定时器应用——门铃
前面几期介绍有关于NE555的一些经典电路,本期我们使用555定时器来实现电子门铃。 利用轻触开关、555定时器制作电子门铃,当按下开关时,可使门铃以1.2kHz响10s; 555定时器由其内部三个5K电阻而得名,由两个比较器和RS触发器构成主要结构,其中的2,6作为触发引脚将其连接在一起,利用7脚和3脚状态相同以及RC充放电路即可构成无稳态振荡器。 #RST引脚作为复位脚在0~3.5时将禁用555定时器,在3.5V以上则可以正常使用定时器。 关于555的原理原理不多赘述,我们使用555定时器的无稳态振荡器来实现1.2KHZ方波的产生,以及利用RC充放电路控制555定时器的使能端来实现10秒的工作时间。 如图所示这是一个经典无稳态振荡。 接着我们利用RC充放电电路来 控制其RST复位引脚 ,正常情况下RST引脚处于低电平不使用状态。 我们利用如图所示电路,当开关S1闭合时电容直连电源,认为充电时间无限短,当开关断开的时候,电容通过电阻R3进行放电。 RST引脚是低电平有效,而对于5V电平来说,3.5V以上才会认为是高电平,因此当我们的电压低于3.5V就会失效。 我们可以利用 RC充放电电路的公式 来计算 当我R3 取28K,C取10uF,V0初始电压为5V,V1为0V,Vt为3.5V计算得t = 99.86ms,仿真得100.624ms非常接近。 所以只需要R = 280K,C = 100uF(或者二者乘积相同即可)由于延迟十秒仿真软件测不出来。 因此使用这种RC电路来控制RST的方式来控制使用时间是非常好用的一种方式来实现单次定时。
实在太懒于是不想取名
0 6 嘉立创PCB
基于C#的软件大杂烩(6.1)——简单的蓝牙助手
在很久之前有一期公众号介绍了如何使用HC05来实现蓝牙通信,但是事实上并没有很好的蓝牙通讯助手(手机上调试的是一个广告很多的软件),电脑端是利用另一个蓝牙模块接上CH340转TTL模块,将HC05/06的蓝牙转为串口进行通信。因此本期旨在开发一个蓝牙助手可以直接连接蓝牙模块进行通讯。 框架 利用ESP32来模拟BT蓝牙(BLE低功耗蓝牙后续也会进行测试) C#利用32feet.NET库来实现蓝牙的使用。 准备工作 界面布局 这个界面基本也是搬运至前面的串口助手界面,目前制作了发送和接收界面。并且代码逻辑也比较简单,还需要有很大的修改。 软件流程 (1)初始化扫描蓝牙设备写入选择框 (2)连接蓝牙设备,连接成功则开启监听线程 (3)监听线程接收到数据设置回调函数将接收到的内容显示在文本框上。 (4)点击发送按钮后将文本框的内容发送给接收端。 核心代码 初始化private async void Booth_Init() { try { client = new BluetoothClient(); // 使用 Task.Run 来在另一个线程中执行 DiscoverDevices devices = await Task.Run(() => client.DiscoverDevices()); BoothChoose.Items.Clear(); foreach (BluetoothDeviceInfo device in devices) { // 确保 UI 更新在 UI 线程上执行 this.Invoke((MethodInvoker)delegate { BoothChoose.Items.Add(device.DeviceName); }); Console.WriteLine(device.DeviceName); } // 确保在 UI 线程上修改 SelectedIndex this.Invoke((MethodInvoker)delegate { if (BoothChoose.Items.Count > 0) { BoothChoose.SelectedIndex = 0; } }); } catch (Exception ex) { Console.WriteLine("错误: " + ex.Mege); } } 初始化的代码是使用异步的方式扫描设备,并且将设备名称显示在选择控件上,之所以是采用异步的方式原因是不采用异步会占用UI线程导致UI线程无法使用。所以需要单独开线程来初始化,同时该函数也可以用于重新扫描设备。 蓝牙连接private async void BoothConnect() { try { if (BoothChoose.Items.Count != 0) { string selectedDeviceName = BoothChoose.SelectedItem.ToString(); // 寻找匹配的设备 BluetoothDeviceInfo deviceToConnect = devices.FirstOrDefault(d => d.DeviceName == selectedDeviceName); if (deviceToConnect != null) { // 连接到设备 await Task.Run(() => client.Connect(deviceToConnect.DeviceAddress, BluetoothService.SerialPort)); MessageBox.Show("连接成功", "信息"); if (client.Connected) { stream = client.GetStream(); StartListening(OnDataReceived); // 开始监听数据并指定回调 } } else { MeBox.Show("未找到选定的设备", "警告"); } } else { Messox.Show("未检测到设备", "警告"); } } catch (Exception ex) { Messox.Show("蓝牙连接失败: " + ex.Mesge, "错误"); } } 连接蓝牙的代码,根据选择控件选择的蓝牙名称匹配对应的蓝牙设别,接着使用异步的方式来连接蓝牙,防止连接蓝牙的过程中导致UI线程卡顿无法使用。 并且使用一个Stream流变量来接收蓝牙传输的信息,调用StartListening函数来实现蓝牙设备的监听 监听函数private void StartListening(Action[removed] callback) { Task.Run(() => { byte[] buffer = new byte[1024]; // 数据缓冲区 int bytes; try { while (client.Connected) { bytes = stream.Read(buffer, 0, buffer.Length); if (bytes > 0) { string receivedData = Encoding.UTF8.GetString(buffer, 0, bytes); this.Invoke((MethodInvoker)delegate { callback(receivedData); // 在UI线程上调用回调 }); } } } catch (IOException ex) { // 连接丢失或读取错误 this.Invoke((MethodInvoker)delegate { MesBox.Show("读取数据错误: " + ex.Mesge, "错误"); }); } }); } private void OnDataReceived(string data) { if (Receive.InvokeRequired) { // 如果调用线程不是创建Receive控件的线程,则使用Invoke调用 Receive.Invoke(new MethodInvoker(delegate { Receive.AppendText(data); })); } else { // 如果已经在UI线程,则直接更新 Receive.AppendText(data); } } 监听函数中我们异步监听数据,并且设置OnDataReceived回调函数处理接收到的数据。 发送数据private void TransButton_Click(object sender, EventArgs e) { try { // 获取要发送的文本 string dataToSend = TransText.Text; if (NewLine.Checked) { dataToSend += Environment.NewLine; } if (client != null && client.Connected) { Stream stream = client.GetStream(); if (stream.CanWrite) { // 将文本转换为字节数据 byte[] buffer = Encoding.UTF8.GetBytes(dataToSend); // 发送数据 stream.Write(buffer, 0, buffer.Length); } } else { Messox.Show("Bluetooth is not connected."); } } catch (Exception ex) { Messox.Show("Error sending data: " + ex.Mese); } } 效果展示 ESP32中的代码是利用蓝牙传输接收到的信息。 因此我们发送信息会接收到发送的信息。 缺点还有很多,后续将会进行迭代升级
实在太懒于是不想取名
0 4 嘉立创PCB
基于C#的软件大杂烩(4.3)——TCP服务端的图像接收和处理
在这个科技日新月异的时代,图像处理和人脸识别技术已经广泛应用于各个领域。。本文将介绍如何使用C#结合ESP32_Cam来实现图像接收和人脸识别功能。因为后面的代码无论是逻辑还是细节都有太多太多需要注意的,所以文章内不具体讲解,只做关键部分说明。感兴趣的朋友可以加群拿源码或者来交流。1.概述    本项目旨在通过ESP32_Cam模块捕获图像,并使用C#编写的软件进行处理和人脸识别。我们将使用OpenCvSharp库来处理图像,初步实现灰度化、二值化的功能并利用Haar级联分类器进行人脸检测。2.准备工作硬件:ESP32_Cam模块。软件:Visual Studio(用于C#开发)。OpenCvSharp库(用于图像处理)。Haar级联分类器(用于人脸识别)。3. 界面布局核心代码解析监听端口接收图像private void StopListening() { // 停止TCP监听 isListening = false; if (cancellationTokenSource != null) { cancellationTokenSource.Cancel(); cancellationTokenSource.Dispose(); cancellationTokenSource = null; } if (tcpListener != null) { tcpListener.Stop(); } } private void ListenForText(CancellationToken cancellationToken) { try { while (!cancellationToken.IsCancellationRequested) { using (TcpClient tcpClient = tcpListener.AcceptTcpClient()) { using (NetworkStream networkStream = tcpClient.GetStream()) { using (MemoryStream memoryStream = new MemoryStream()) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = networkStream.Read(buffer, 0, buffer.Length)) > 0) { memoryStream.Write(buffer, 0, bytesRead); } // 处理和显示接收到的数据 string receivedText = Encoding.UTF8.GetString(memoryStream.ToArray()); ReciveText.AppendText(receivedText); } } // 关闭TcpClient tcpClient.Close(); } } } catch (Exception ex) { Console.WriteLine($"错误:{ex.Mege}"); } } // 在UI线程上显示图像的方法 private void Dispmage(Bitmap image) { if (InvokeRequired) { Invoke(new Action(() => DisplayImage(image))); } else { pictox.Image = image; } } private void Stten_Click(object sender, EventArgs e) { if (Staten.Text == "创建监听") { DisableAll(); StaeningText(); PicTurSta.Enabled = false; StartListen.Text = "关闭监听"; } else { EnableAll(); Stening(); PicTurSta.Enabled=true; StartListen.Text = "创建监听"; } } 图像处理 以下是图像处理的核心代码段: Bitmap bitmap = new Bitmap(image); Mat inputMat = OpenCvSharp.Extensions.Bitverter.ToMat(bitmap); Mat OutMat = new Mat(); OutMat = inputMat.Clone(); // 灰度化 if (HuiDuHua.Checked) { Cv2.CvtColor(inputMat, OutMat, ColonCodes.BGR2GRAY); } // 二值化 if (Erzhihua.Checked) { int thresholdValue = Throid.Value; Cv2.Threshold(OutMat, OutMat, thresholdValue, 255, ThresholdTypes.Binary); } 人脸识别人脸识别部分的代码如下:string classifierPath = @"rcascade_frontalfac.xml"; CascadeClassifier faceCascade = new Cassifier(classifierPath); Mat grayImage = new Mat(); Cv2.CvtColor(OutMat, grayImage, ColoonCodes.BGR2GRAY); Rect[] faces = faceCascade.DetectMultiScale(grayImage); foreach (Rect face in faces) { Cv2.Rectangle(OutMat, face, Scalar.Red, 2); } 注意点我们使用NuGet管理包来安装我们需要的OpenCV的包,需要注意的是,这四个包都是需要的,并且我们需要在代码开始的时候导入我们的库。using OpenCvSharp; using OpenCvSharp.Extensions; 5. 效果展示
实在太懒于是不想取名
0 4 嘉立创PCB
基于C#的软件大杂烩(4.2)——TCP服务端的文本接收和发送
上一期使用TCP协议接收图片数据,本期我们使用TCP协议来实现文本的接收和发送。关于TCP协议的具体内容本文不做赘述,如果有感兴趣的伙伴可以去CSDN或者B站查看相关视频。。简单的布置一下界面, 将数据接收和数据发送分开来。接收数据的部分监听我们的端口,发送数据的部分根据我们输出的端口地址来向目标地点发送数据。并且由于逻辑的问题,使用这些内容的时候会把其他内容禁用,防止端口被占用或者重复监听。接收数据 private void StartListeningText() { try { // 获取选定的IP地址和端口 IPAddress ipAddress = IPAess.Parse(IPAdssChoose.Selem.ToString()); int port = int.Parse(PortChoose.Text); // 启动TCP监听 tcpListener = new TcpListener(ipAddress, port); tcpListener.Start(); cancellationTokenSource = new CancellationTokenSource(); isListening = true; listenerThread = new Thread(() => ListenForText(cancellationTokenSource.Token)); listenerThread.Start(); } catch (Exception ex) { Mex.Show($"错误:{ex.Message}", "错误", Messttons.OK, .Error); } } private void StopListening() { // 停止TCP监听 isListening = false; if (cancellationTokenSource != null) { cancellationTokenSource.Cancel(); cancellationTokenSource.Dispose(); cancellationTokenSource = null; } if (tcpListener != null) { tcpListener.Stop(); } } private void ListenForText(CancellationToken cancellationToken) { try { while (!cancellationToken.IsCancellationRequested) { using (TcpClient tcpClient = tcpListener.AcceptTcpClient()) { using (NetworkStream networkStream = tcpClient.GetStream()) { using (MemoryStream memoryStream = new MemoryStream()) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = networkStream.Read(buffer, 0, buffer.Length)) > 0) { memoryStream.Write(buffer, 0, bytesRead); } // 处理和显示接收到的数据 string receivedText = Encoding.UTF8.GetString(memoryStream.ToArray()); ReciveText.AppendText(receivedText); } } // 关闭TcpClient tcpClient.Close(); } } } catch (Exception ex) { Console.WriteLine($"错误:{ex.Message}"); } } 接收数据的逻辑和上一期接收图像差不多,但是更为简单,我们不需要把数据转为图像,只需要直接把内容显示出来。发送数据private void TransButton_Click(object sender, EventArgs e) { int port = int.Parse(PortChooseTran.Text); IPAddress TransAddress = IPAddress.Parse(AddressText.Text.ToString()); if (TransAddress == null) { MessageBox.Show("错误:无可用地址"); return; } //使用TCP发送数据 string TranText = TransFormText.Text; using (TcpClient tcpClient = new TcpClient()) { tcpClient.Connect(TransAddress, port); using (NetworkStream networkStream = tcpClient.GetStream()) { // 将字符串转换为字节数组 byte[] data = Encoding.UTF8.GetBytes(TranText); // 发送数据 networkStream.Write(data, 0, data.Length); } } } 发送数据的代码中,我们需要将我们的文本内容转换成字节数组,之后根据文本框中的目标地址,将数据发送出去。
实在太懒于是不想取名
0 2 嘉立创PCB
如何利用正弦波制造方波以及利用方波提取正弦波
俗话说万物皆可傅里叶,傅里叶级数可以说是非常伟大的一个工具,傅里叶提出:任何周期函数都可以用正弦函数和余弦函数的无穷级数来表示。f(x)=a0+∑(ancos(nx)+bnsin(nx));当然我们可以简化其中的运算只观察其中的正弦函数部分。 那么我们的方波是否也可以用傅里叶级数来表示? 当然可以,方波的傅里叶级数表达式为: 我们可以用简单的程序来实现 for(float j = 0;j<100*3.14;j+=0.01) { double t = 0; for(int k = 0;k[removed]
实在太懒于是不想取名
3 11 嘉立创PCB
基于C#的软件大杂烩(5.1)——简介HTTP协议以及编写HTTP服务端
上一期我们介绍了使用TCP协议来建立流式传输实现实时的图像显示。我们也简单的提到了HTTP与TCP协议的优缺点。本期我们简单的介绍一下HTTP并且使用C#制作一个HTTP助手来接收我们的HTTP消息。HTTP简介HTTP是超文本传输协议(Hypertext Transfer Protocol)的简称,既然是一种协议,HTTP分为客户端和服务端,顾名思义服务端是为客户端服务的。服务端不会主动发送请求,但是服务端可以响应客服端发出的请求。客户端通过请求报文向服务端发送消息,服务端接收到请求报文后会使用响应报文对客户端进行响应,HTTP规定了请求报文和响应报文的格式。HTTP请求报文如下所示例如这个例子,请求方法是GET,请求路劲是http://192.168.137.1,版本是HTTP/1.1 200通常代表着响应成功。 其实关于HTTP还有好多好多内容,我们就简单的介绍一下HTTP的请求和响应机制,接着使用C#编写测试代码。 界面布局 还是简单的布置一下我们的界面,使用一个Teox来接收客户端发送的请求报文,下面设置一个我们的自定义响应文本。 右边使用Comox控件来存放电脑的可用IP地址,并且可以选择输入端口号。 设置波形显示的勾选框,方便后期可以从HTTP协议中解析波形。 设置详细信息的勾选框,可以选择显示详细信息还是显示简短的信息。 主要代码 private void LoadLocalIPv4Addresses() { try { // 获取本地主机名 string hostName = Dns.GetHostName(); // 获取主机名对应的IP地址列表 IPAddress[] ipAddresses = Dns.GetHostAddresses(hostName); // 筛选IPv4地址并添加到ComboBox foreach (IPAddress ipAddress in ipAddresses.Where(ip => ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)) { IPAddressChoose.Items.Add(ipAddress.ToString()); } // 设置默认选择第一个IPv4地址 if (IPAddressChoose.Items.Count > 0) { IPAddressChoose.SelectedIndex = 0; } } catch (Exception ex) { Messaox.Show($"错误: {ex.Message}", "错误", MessaoxButtons.OK, MessaxIcon.Error); } } 利用Dns.GetHostName的方法获得主机的可用IP地址,并将地址写入名为IPAddressChoose的Coox控件中 需要注意的是传入格式的问题IPAddresses是IPAddress类型的数据,我们需要将其转换为IPAddress类型传入IPAddressChoose控件的Text属性中。 private HttpListener httpListener = null; private void CreateHttpListener() { try { httpListener = new HttpListener(); // 获取选择的 IP 地址和端口号 string selectedIpAddress = IPAddressChoose.SelectedItem.ToString(); string selectedPort = PortChoose.Text; // 创建监听地址 string listenUrl = $"http://{selectedIpAddress}:{selectedPort}/";                 // 创建 HttpListener 实例 httpListener.Prefixes.Add(listenUrl); // 启动监听器 try { // 启动监听器 httpListener.Start(); IPAddressChoose.Enabled = true; PortChoose.Enabled = true; // 异步处理接收请求 Task.Run(async () => { while (true) { // 等待请求连接 HttpListenerContext context = await httpListener.GetContextAsync(); // 处理请求 HandleHttpRequest(context); } }); } catch (HttpListenerException ex) { // 输出错误信息或采取适当的处理措施 MessageBox.Show("访问端口失败\r\n如未启动管理员模式请使用管理员模式启动\r\n检查端口是否被占用可以尝试更换端口"); } } catch (Exception ex) { Messaox.Show($"Error: {ex.Mese}", "Error", MessxButtons.OK, Messacon.Error); } } private void HandleHttpRequest(HttpListenerContext context) { // 获取请求对象 HttpListenerRequest request = context.Request; // 获取请求的 URL string url = request.Url.AbsoluteUri; // 获取请求的 HTTP 方法(GET、POST 等) string httpMethod = request.HttpMethod; // 获取请求头 WebHeaderCollection headers = request.Headers as WebHeaderCollection; // 获取请求的 IP 地址 string ipAddress = context.Request.RemoteEndPoint?.Address?.ToString(); // 读取请求正文 string requestBody; using (System.IO.Stream body = request.InputStream) using (System.IO.StreamReader reader = new System.IO.StreamReader(body, request.ContentEncoding)) { requestBody = reader.ReadToEnd(); } // 在这里处理请求正文 this.BeginInvoke(new Action(() => { // 显示 IP 地址 if (!ChartShow.Checked) { Recive.AppendText("New Message:\r\n"); Recive.AppendText($"IP Address: {ipAddress}\r\n"); // 显示 URL Recive.AppendText($"URL: {url}\r\n"); } if (MoreDetile.Checked&&!ChartShow.Checked) { // 显示 HTTP 方法 Recive.AppendText($"HTTP Method: {httpMethod}\r\n"); // 显示请求头 Recive.AppendText($"Request Headers:\n{string.Join("\n", headers.AllKeys.Select(key => $"{key}: {headers[key]}"))}\r\n"); // 显示请求正文 Recive.AppendText($"Request Body:\n{requestBody}\r\n"); } else { Dictionary[removed] keyValuePairs = ParseJsonRequestBody(requestBody); // 在这里处理请求正文 this.BeginInvoke(new Action(() => { // 打印键值对 foreach (var kvp in keyValuePairs) { if (double.TryParse(kvp.Value, out double doubleValue)) { if (ChartShow.Checked) { // 在 Chart 中检查是否包含对应键名的 Serial if (chart.Series.Any(series => series.Name == kvp.Key)) { // 如果已经包含,将数据添加到对应的 Serial 中 chart.Series[kvp.Key].Points.AddXY(chart.Series[kvp.Key].Points.Count+1, double.Parse(kvp.Value)); } else { // 如果不存在,新建对应键名的 Serial,并添加数据 Series newSeries = new Series(kvp.Key); Random random = new Random(); Color randomColor = Color.FromArgb(random.Next(256), random.Next(256), random.Next(256)); // 新建 Series,并设置线条的属性 newSeries.Color = randomColor; // 设置颜色 newSeries.BorderWidth = 3; // 设置宽度 newSeries.ChartType = SeriesChartType.Spline; newSeries.Points.AddY(double.Parse(kvp.Value)); chart.Series.Add(newSeries); } } } Recive.AppendText($"{kvp.Key}: {kvp.Value}\r\n"); } })); } })); // 构建响应内容 string responseString = Recall.Text; byte[] buffer = Encoding.UTF8.GetBytes(responseString); // 发送响应 HttpListenerResponse response = context.Response; response.ContentLength64 = buffer.Length; System.IO.Stream output = response.OutputStream; output.Write(buffer, 0, buffer.Length); output.Close(); } 接着初始化一个监听实例,根据我们选择的IP地址以及端口号创建HTTP监听,并通过异步的方式处理接收到的数据,将数据打印或者从中解析出波形显示在外面的页面上。
实在太懒于是不想取名
1 4 嘉立创PCB
基于Python和opencv图像处理(3)——图片的膨胀与腐蚀
图片的膨胀与腐蚀是非常常见的图像处理技巧(现在就见到了)需要注意的是,这里的腐蚀和膨胀时对于白色部分而言的,而不是黑色部分!!字体的大小就是常见的图像的腐蚀和膨胀。腐蚀和膨胀可以很好的去除掉图像中的噪声点,消除物体边界附近的像素。代码编写首先是导入相关的库import cv2 import numpy as np import matplotlib.pyplot as plt 之后利用opencv的函数来读取我们的图像。# 读取图像 img = cv2.imread('C:/Users/13256/Desktop/11.tif', cv2.IMREAD_GRAYSCALE) 定义结构元素来确定我们的卷积运算大小# 定义结构元素(核) kernel = np.ones((5, 5), np.uint8) 利用opencv的内置函数来进行膨胀运算# 膨胀操作 dilated_img = cv2.dilate(img, kernel, iterations=1) 最后显示出我们的图像# 显示原始图像、膨胀后的图像 plt.subplot(1, 2, 1), plt.imshow(img, cmap='gray'), plt.title('原始图像') plt.subplot(1, 2, 2), plt.imshow(dilated_img, cmap='gray'), plt.title('膨胀操作后的图像') plt.show()可以看到膨胀操作更凸显出图片的轮廓和边界将膨胀函数方法换成腐蚀,之后再运行我们的代码。# 腐蚀操作 dilated_img = cv2.erode(img, kernel, iterations=1) 以下附上全部代码import cv2 import numpy as np import matplotlib.pyplot as plt # 读取图像 img = cv2.imread('C:/Users/13256/Desktop/11.tif', cv2.IMREAD_GRAYSCALE) # 定义结构元素(核) kernel = np.ones((5, 5), np.uint8) # 膨胀操作 dilated_img = cv2.erode(img, kernel, iterations=1) # 显示原始图像、膨胀后的图像 plt.subplot(1, 2, 1), plt.imshow(img, cmap='gray'), plt.title('原始图像') plt.subplot(1, 2, 2), plt.imshow(dilated_img, cmap='gray'), plt.title('膨胀操作后的图像') plt.show()
实在太懒于是不想取名
2 9 嘉立创PCB
基于C#的软件大杂烩(4.1)—— 和ESP32_cam搭配的TCP协议解析图像
前几期公众号(上一期)中有介绍如何使用ESP32_Cam获取图像利用TCP协议上传,之后利用利用Python解析收到的代码并且显示出来。那么本期我们在之前的C#软件大合集中添加TCP服务器端并完成在C#中的图像显示的功能。在上一期说过,由于HTTP协议需要等待响应,因此可以做到非常好的数据传输校验。而TCP协议不需要服务器响应,因此虽然在数据传输的可靠性上得到了下降,但是在数据传输的速度上却得到了质的飞跃。首先简简单单地做个界面,右边是选择地址和端口的界面,左边是分开文本接收模式和图片显示模式。有一个件方便我们选择IP地址,由端口号来选择我们的端口号,需要注意的是通常端口都是开放的,如果我们遇到了打开端口报错的问题,我们需要及时调节我们的端口或者开启管理员模式。虽然说是1024及以下的低序列端口才需要开启管理员模式才能打开,但是在实际的使用中发现如果需要使用端口还是需要打开管理员模式的。IPV6地址由四组组成,我们获得的地址包括我们的公网地址以及映射地址。值得一提的事,如果设置地址是即默认监听所有可用地址。private void LoadLocalIPv4Addresses() { try { // 获取本地主机名 string hostName = Dns.GetHostName(); // 获取主机名对应的IP地址列表 IPAddress[] ipAddresses = Dns.GetHostAddresses(hostName); // 筛选IPv4地址并添加到 foreach (IPAddress ipAddress in ipAddresses.Where(ip => ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)) { IPAddressChoose.Items.Add(ipAddress.ToString()); } // 设置默认选择第一个IPv4地址 if (IPAddressChoose.Items.Count > 0) { IPAddressChoose.SelectedIndex = 0; } } catch (Exception ex) { MessageB.Show($"错误:{ex.Message}", "错误", MessageBButtons.OK, MessageBIcon.Error); } } 首先是加载的时候筛选出本机可以用的IP地址,把IP地址加入我们的控件中,方便我们使用时候的选择。主要代码之后编写创建监听的代码。创建监听的步骤主要是建立一个TCP监听句柄,之后使用异步的方法创建一个回调函数来处理我们接收到的图像。需要调节关闭时候的任务状态,防止调用过程中关闭监听,导致监听继续从而发生代码中断。 private void Connect_Click(object sender, EventArgs e) {        if (Connect.Text == "创建监听")        {             DisableAll();             StartListening();             Connect.Text = "结束监听";        }        else        {            EnableAll();            StopListening();            Connect.Text = "创建监听";        } } 创建监听的按钮按下后首先是禁用其他控件,之后是创建监听。private void StartListening() { try { // 获取选定的IP地址和端口 IPAddress ipAddress = IPAddress.Parse(IPAddressChoose.SelectedItem.ToString()); int port = int.Parse(PortChoose.Text); // 启动TCP监听 tcpListener = new TcpListener(ipAddress, port); tcpListener.Start(); cancellationTokenSource = new CancellationTokenSource(); isListening = true;                 //开辟线程实现异步监听 listenerThread = new Thread(() => ListenForImages(cancellationTokenSource.Token)); listenerThread.Start(); } catch (Exception ex) { MessageB.Show($"错误:{ex.Message}", "错误", MessageBButtons.OK, MessageBIcon.Error); } } private void StopListening() { // 停止TCP监听 isListening = false; if (cancellationTokenSource != null) {                 cancellationTokenSource.Cancel();//取消进程防止出错 cancellationTokenSource.Dispose(); cancellationTokenSource = null; } if (tcpListener != null) { tcpListener.Stop(); } } 创建监听的时候,我们根据所选择的IP地址和端口开启TCP监听,并且使用一个isListening标志位来判断是否处于监听状态。需要注意的是这里的cancellationTokenSource.Dispose()是不可少的,如果缺少这行代码就会导致异步程序进行会导致程序错误。 并且开始一个线程来实现异步监听。 之后分别是异步监听的函数,将我们收到的TCP数据转化为图片信息进行保存,并且把图片显示到PictureBox控件上。需要注意的是,在异步监听的函数中,我们使用Console.WriteLine来打印错误信息,防止阻塞UI进程。 效果展示
实在太懒于是不想取名
0 1 嘉立创PCB
基于ESP32_Cam和Python的实时图像显示
很早一段时间之前入手了一个ESP32_Cam即Camera摄像头。但是一直没有时间去研究这个模块,难得最近空下来了所以本期决定使用ESP32_Cam去实现一个实时视频传输的效果。ESP32_Cam如上图所示,淘宝上加上烧录器(一个串口转CH340)总计30+,使用的是经典的OV2640摄像头。官方Demo使用组装好之后,我们打开Arduino IDE,烧入ESP32的摄像头例程。并且修改相应的设置。使用该实例的代码,需要注意的是,我们需要注释掉第17行,解开第24行的注释,之后烧入我们的代码。修改WIFI连接的账号还有密码,之后开启电脑热点(或者自己家的WIFI)保证处于同一个局域网中。我们打开Start Stream来开启我们的流式传输,效果如下打开串口,输入这个浏览器地址就可以使用ESP32的官方Demo啦。代码优化为了更好的帧率以及拓展,我们决定使用Python来实现数据的接收以及显示。首先我们需要修改我们的代码。将官方例程中的这两个函数注释掉,我们在Loop循环中添加我们的逻辑。void loop() {    camera_fb_t *fb = esp_camera_fb_get();//获取图像 if (fb == nullptr) { Serial.println("Camera capture failed"); } else { Serial.println("Camera capture OK");     //使用TCP协议上传 WiFiClient client;     client.connect("192.168.137.1", 8081);//上传地址的端口号     client.write(fb->buf, fb->len);//上传字节数组和长度 }   esp_camera_fb_return(fb);//返回帧 } 为此ESP32的代码编写完成。之后我们需要编辑Python部分的代码,关于Python的环境安装可以看往期的公众号。 需要导入Opencv库来显示我们的图像。#该线程用于图像显示 def display_image(): global shared_image while True:         if shared_image is not None: for (x, y, w, h) in faces: cv2.rectangle(shared_image, (x, y), (x + w, y + h), (0, 255, 0), 2) cv2.imshow('Received Image with Face Detection', shared_image) cv2.waitKey(1) # 服务器地址和端口 host = '0.0.0.0' #0.0.0.0则是设置监听所有可用地址 port = 8081 # 创建一个TCP套接字 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 将套接字绑定到指定地址和端口 server_socket.bind((host, port)) # 开始监听连接,允许最大连接数为1 server_socket.listen(1) print(f"Listening on {host}:{port}...") # 创建一个线程用于图像显示 display_thread = threading.Thread(target=display_image) display_thread.daemon = True # 设置为守护线程,主程序退出时线程也会退出 display_thread.start() while True: # 接受客户端连接 client_socket, client_address = server_socket.accept() print(f"Accepted connection from {client_address}") # 接收数据并保存到内存中 data = b"" while True: packet = client_socket.recv(4096) if not packet: break data += packet # 将接收到的二进制数据转换为NumPy数组 image_array = np.frombuffer(data, dtype=np.uint8) # 解码图像 with image_lock: shared_image = cv2.imdecode(image_array, cv2.IMREAD_COLOR) print("Image received and sent to display.") # 关闭连接 client_socket.close() 之后运行以下我们的代码,可以看到虽然运行起来还不是很流畅,但是也可以勉强接受,相比官方的几秒一帧已经有很大的改善了。
实在太懒于是不想取名
2 16 嘉立创PCB
基于Python和opencv图像处理(1)——相关库导入完成简单的图像处理
关于Python的环境安装还有库导入可以参考这篇公众号 人工智能导论(1)——Python环境安装与基础编程 本期我们要实现的内容是: (1)使用canny边缘检测算法提取花朵轮廓; (2)计算图像直方图,使用阈值分割法对花朵区域进行分割,并提取花朵轮廓; (3)计算出花朵轮廓图的Hu矩; 打开PyCharm在安装库的界面搜索opencv-python,安装Python的Opencv库。import cv2 import numpy as np 导入opencv库以及运算工具库。 我们利用opencv的自带库来进行Canny算法。import cv2 import numpy as np # 读取图像 img = cv2.imread('C:/Users/13256/Desktop/1.bmp', 0) #读取灰度图像# 使用Canny算法进行边缘检测 edges = cv2.Canny(img, 50, 150) #50和150是Canny算法的低阈值和高阈值# 显示原始图像和边缘检测结果 cv2.imshow('原始 Image', img) cv2.imshow('Canny Edges', edges) cv2.waitKey(0) #任意键推出 cv2.destroyAllWindows() 运行代码可以看效果 可以看到效果显著。 (2)计算图像直方图,使用阈值分割法对花朵区域进行分割,并提取花朵轮廓; import cv2 import numpy as np # 读取图像 img = cv2.imread('C:/Users/13256/Desktop/5.jpg') # 转换为灰度图像 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 计算直方图 hist = cv2.calcHist([gray], [0], None, [256], [0, 256]) # 选择适当的阈值进行分割 _, thresholded = cv2.threshold(gray, 70, 256, cv2.THRESH_BINARY) # 查找轮廓 contours, _ = cv2.findContours(thresholded, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 绘制轮廓 result = img.copy() cv2.drawContours(result, contours, -1, (0, 255, 0), 2) # 显示图像和结果 cv2.imshow('yuanshi Image', img) cv2.imshow('yuzhi Image', thresholded) cv2.imshow('Contours', result) cv2.waitKey(0) cv2.destroyAllWindows() (3)计算出花朵轮廓图的Hu矩; Hu矩(Hu Moments)是一组用于描述图像形状的特征矩。这些矩是从图像的原始矩(Moments)计算而来的,通过对图像的几何形状进行建模,可以用于表征图像的整体形状特征。 Hu矩的主要优点是它们对于图像的旋转、缩放和平移具有不变性,这使得它们在图像识别和模式匹配中非常有用。 共有七个Hu矩,称为Hu1至Hu7。这些矩可以通过计算图像的中心矩(central moments)和规范化中心矩(normalized central moments)得到。Hu矩的计算公式较为复杂,但它们的主要思想是通过对图像的几何形状特征进行标准化,使得这些特征对于缩放、旋转和平移不敏感。 在OpenCV中,可以使用cv2.HuMoments()函数来计算图像的Hu矩。这些矩以一维数组的形式返回,其中包含Hu1至Hu7的值。 在绘制Hu矩的时候 需要我们多导入一个库: matplotlib来绘制我们的Hu图像。import cv2 import numpy as np import matplotlib.pyplot as plt # 读取图像 img = cv2.imread('C:/Users/13256/Desktop/5.jpg', cv2.IMREAD_GRAYSCALE) # 选择适当的阈值进行分割 _, thresholded = cv2.threshold(img, 70, 255, cv2.THRESH_BINARY) # 查找轮廓 contours, _ = cv2.findContours(thresholded, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 筛选出面积较大的轮廓 filtered_contours = [cnt for cnt in contours if cv2.contourArea(cnt) > 100] # 初始化一个数组来保存Hu矩的值 hu_values = [] # 遍历筛选后的轮廓并计算Hu矩 for contour in filtered_contours: hu_moments = cv2.HuMoments(cv2.moments(contour)).flatten() hu_values.append(hu_moments) # 将Hu值打印成直方图 hu_values = np.array(hu_values) num_hu_moments = hu_values.shape[1] plt.figure(figsize=(10, 6)) for i in range(num_hu_moments): plt.subplot(2, 4, i + 1) plt.hist(hu_values[:, i], bins=20, color='blue', alpha=0.7) plt.title(f'Hu{i + 1}') plt.grid(True) plt.tight_layout() plt.show()
实在太懒于是不想取名
2 5 嘉立创PCB
电容测量之NE555
电容传感器在我们的生活中用处广泛,如何测量电容,电容测量电路也是一个讨论甚广的电路。本期我们分享一种利用NE555测量电路的方法。NE555有着电子小甲虫之称,其使用非常非常的广泛,并且稳定性经过了时间的考验。在NE555的基础上有着许多的电路设计例如施密特触发器、单稳态触发器等等,本期我们使用NE555构成无稳态振荡器来实现电容的测量。上图这种利用NE555的无稳态电路被广泛的使用,利用R1和R2以及C1构成的充电放电电路在这个特定的电路之下使得3脚输出一个频率的方波利用这个公式我们可以推算出频率和电容电阻的关系我们利用Multisim进行仿真,可以看到理论频率为4800HZ,我们利用频谱分析仪测出的频率则是在4877HZ,1.6%的误差(应该可以纠正, 而且是频谱分析仪,而且是仿真。)02测量电路与数据处理本期我们想要测量10pf到100pf的电容,事实上在测量这种小电容的时候会受到电路分布电容的影响。由于两个引脚相距很近,所以事实上这两个IO自己就会构成一个电容,因此当我们测微小电容的时候引脚之间的电容是不能忽视的。简单的用洞洞板做了个电路,R1和R2分别选用328K电阻(可能也有点误差)。我们发现,电路空载的时候存在一个168.4KHZ的方波,这个信号就是由电路的分布电容引起的振荡。接着我们将168.4KHZ代入公式。1.44/(328+2*328)/168.4 = 0.0000086uF,即约等于8pf的电容。之后我们测量多组数据,将实际电容值和理论电容值记录下来利用拟合工具进行拟合。、程序运行程序部分本来打算使用51单片机的(主打一个丐版),因为频率在168KHZ,51单片机完全可以采集到168KHZ的信号,因此但是手上并没有合适的51单片机和屏幕,于是就打算使用STM32单片机来进行测试,我们使用外部中断来获取频率数,利用定时器定时触发来计算频率数量,之后转换成电容值进行数据处理后打印在LCD屏幕上。使用PB2作为外部中断,定时器2作为时间计数。定义一个数字变量,在外部中断的中断回调函数中进行频率计数,定时器每秒统计频率计数用一个变量来存储。 之后再把我们获得的频率利用公式计算出理论电容,之后把理论电容通过拟合曲线转换为实际电容。 效果展示
实在太懒于是不想取名
3 7 嘉立创PCB
基于C#和ESP32的远程示波器(7)——外部ADC设计提高采样率和数据精度
上期我们暂时的解决了数据连续性问题,但是我们发现我们的ADC采样似乎有非常大的噪声,因此我决定利用外部ADC芯片来替换到噪声非常大的板载ADC的噪声非常大。 我们对ADC信号进行傅里叶变换,可以看到ADC的噪声还是非常大的。 因此我们决定使用外部ADC来提高精度。 ADC选型 MAX11606 是美信 公司推出的低噪声累加型ADC芯片,并且其四通道的设计符合我们的需求,87KHZ的采样率也是满足我们的使用要求,支持IIC通讯。 所以我决定使用这块ADC芯片来制作我的外部ADC模块。我们参考芯片手册可以了解这块芯片的相关 查看芯片手册,其中MAX11606的供电电源可以到达5V,参考电平也可以到达5V(纠正,这个是外部参考,我们使用内部参考为4.096V)满足我们的0~3.3V的使用需求。 DAC选型 DAC芯片我们选择微芯公司的MCP4822,MCP4822具有12位精度,SPI通讯双通道芯片 MCP4822的快速沉降时间为4.5us,压摆率为0.55V/us,足够满足我们的要求(我的前置运放LM324是0.5V/us的压摆率二者差不多) 并且,其信噪比也很低,非常适合于我们的项目。 (这两块芯片是我找了好久好久的电路比较简单,速度快,分辨率高以及最最最最最重要的价格便宜并且嘉立创上面有库存的器件) 电路设计 绘制我们的原理图部分,其中红色方框为数字地与模拟地,数字电源和模拟电源的隔离。 蓝色方框为IIC和SPI的上拉电阻。 我们利用ESP32的SPI,CS连D5,SCK连D18,DIN连D23,LDNC选择一个(我选择D17)。 IIC部分,SCL连D22,SDA连D21。 MCP4822驱动编写 我们查看MCP4822的手册。 MCP4822的命令由:4个配置位+12位数据组成。 从高到低分别是:通道选择,无关位,增益控制位以及输出控制。 例如我们想要使用通道A,增益为2,并且可输出,那么设置为0001,对应的十六进制是0x1;void SPI_Init() { // 初始化对应VSPI接口,得到SPI对象 vspi = new SPIClass(VSPI); vspi->begin(VSPI_SCLK, VSPI_MISO, VSPI_MOSI, VSPI_SS); //SCLK, MISO, MOSI, SS pinMode(vspi->pinSS(), OUTPUT); //VSPI SS pinMode(LDAC, OUTPUT); } 初始化我们的SPI,begin参数中传入我们的SPI引脚。 接着我们编写我们的写命令函数。void spiCommand(SPIClass *spi, uint8_t cmd,uint16_t value) { // 设置SPI对应模式 uint32_t data = cmd<[removed]beginTransaction(SPISettings(spiClk, MSBFIRST, SPI_MODE0)); digitalWrite(spi->pinSS(), LOW); // NSS低电平,数据开始传输 spi->write16(data); digitalWrite(spi->pinSS(), HIGH); // NSS高电平,数据截止传输 digitalWrite(LDAC,LOW); digitalWrite(LDAC,HIGH); spi->endTransaction(); } 我们将cmd命令左移12位,接着加上我们的值就构成了我们要发送的命令,接着我们开始SPI的传输,并且拉低SS片选信号。接着传入我们拼接好的数据,接着拉低LDAC使的数据写入寄存器开始工作,关闭SPI传输。 //use the SPI buses for(float i = 0;i<2*3.14;i+=0.01) {     spiCommand(vspi, 0x1,((sin(i))+1)*2096/4); // junk data to illustrate usage } 在主函数中我们设置一个For循环让DAC输出一个正弦波。 可以看到DAC可以正常地工作。 MAX11606驱动编写 MAX11606是使用IIC驱动,首先我们需要在头文件中引入ESP32 Arduino的IIC库:#include [removed] 之后启动IIC,由于MPU作为主机,因此begin函数不需要传入参数,如果作为从机和其他主机通讯就需要输入地址。Wire.begin(); 查阅MAX11606手册得知,该芯片的从机地址是0x34 上述的两张表用来配置启动设置,我们需要的是:选择内部Vref,外部时钟,单极输入,不重置。扫描方式为连续扫描,通道选择1,单端输入。Wire.beginTransmission(0x34);   Wire.write(0xC);//一个字节一个字节的发 Wire.write(0xA); Wire.endTransmission(true); int result = Wire.endTransmission(); Wire.beginTransmission(0x34); Wire.write(0x6); Wire.write(0x7); result = Wire.endTransmission(); if (result == 0) { Serial.println("命令发送成功"); } else { Serial.println("命令发送失败"); } 之后定义读取的函数。void IIC_Read() { int deviceAddress = 0x34; uint16_t result = 0; // 请求读取数据,读取2个字节 Wire.requestFrom(deviceAddress, 2); // 等待数据可用 while (Wire.available() < 2) { // 等待数据 } // 读取数据并输出到串口 int TT = Wire.read(); //result = result<<8; result = Wire.read(); TT=(TT-252); result = (TT<[removed]
实在太懒于是不想取名
7 13 嘉立创PCB
人工智能导论(3.1)—— 决策树和信息增益
决策树是一种基于树状结构来进行决策的模型,被广泛用于分类和回归问题。它通过对输入数据集进行逐步的划分,从而在叶节点上做出决策。每个内部节点表示一个属性或特征,每个分支代表该属性或特征的一个可能的取值,而每个叶节点表示一个类别标签或回归值。 决策树的优点是简单易懂,可以直观地表示决策过程。缺点是决策树可能过于复杂,难以解释。 信息增益(information gain)是决策树算法中的一个重要概念,它用于选择最能划分数据集的属性。信息增益是指划分数据集后,数据集的熵(不确定性)减少的程度。 熵是信息论中的一个概念,它用来度量随机变量的不确定性。熵越高,表示随机变量的不确定性越大。 信息增益的公式如下: 信息增益 = 原始熵 - 条件熵 其中,原始熵是指未划分数据集的熵,条件熵是指根据属性划分数据集后,每个子集的熵。 例如,假设我们有以下数据集: 该数据集共有两种属性:年龄和类别。年龄有两个值:小于30岁和大于30岁。类别有两种值:感冒和流感。 原始熵可以计算为: 原始熵 = -(1/2 * log2(1/2) + 1/2 * log2(1/2)) = 1 根据年龄将数据集划分为两个子集: 其中,小于30岁子集包含一个数据点,类别均为感冒。大于30岁子集包含一个数据点,类别均为流感。 条件熵可以计算为: 条件熵 = -(1 * log2(1) + 1 * log2(1)) = 0 因此,信息增益可以计算为: 信息增益 = 原始熵 - 条件熵 = 1 - 0 = 1 信息增益越大,表示属性对数据集的划分越有效。因此,在决策树算法中,我们会选择信息增益最大的属性作为划分数据集的依据。 信息增益的优点是简单易懂,计算方便。缺点是容易受到数据集分布的影响。例如,如果数据集中的某个类别占比过高,那么信息增益可能会被夸大。 ID3算法是决策树算法的一种,它使用信息增益来选择最能划分数据集的属性。信息增益是指划分数据集后数据集的熵(不确定性)减少的程度。 信息增益的公式如下: 信息增益 = 原始熵 - 条件熵 其中,原始熵是指未划分数据集的熵,条件熵是指根据属性划分数据集后,每个子集的熵。 在本例中,原始熵可以计算为: 原始熵 = -(11/14 * log2(11/14) + 3/14 * log2(3/14)) = 0.940 根据天气将数据集划分为两个子集:晴天和非晴天。 晴天子集包含 5 个数据点,其中 3 个数据点的类别是“否”,2 个数据点的类别是“是”。因此,晴天子集的条件熵可以计算为: 条件熵 = -(3/5 * log2(3/5) + 2/5 * log2(2/5)) = 0.609 因此,根据天气划分数据集的信息增益可以计算为 信息增益 = 0.940  - 0.609  = 0.331 同样,我们可以计算出其他属性的信息增益。 因此,我们选择天气作为第一个分类属性,因为它的信息增益最大。属性信息增益天气0.6931471806温度0.4138067639湿度0.3258092328是否有风0.1921568627 根据信息增益的值顺序,我们可以列出我们的分类图 天气-->温度-->湿度-->风力 例如: 非晴   凉爽   正常   无风  去打球
实在太懒于是不想取名
1 4 嘉立创PCB
基于C#的软件大杂烩(3.1)—— 线性拟合工具
前面人工智能导论(2)——基本算法程序编写——回归介绍了Python中使用线性拟合工具,我就想在C#中能不能做一个线性拟合工具,方便我们进行线性拟合。界面布局和基础准备我们简单的绘制一个窗体,其中可以进行选择数据,显示原始数据以及进行数据拟合。其中的存放数据的容器使用的是DataGripView,其他的应该都很熟悉。之前在设计串口示波器的时候,我们引入了MathNet数学库,本期我们也是利用MathNet库中的方法进行数据拟合。using MathNet.Numerics.LinearAlgebra; using MathNet.Numerics.LinearAlgebra.Double; using MathNet.Numerics.LinearRegression; 导入我们的MathNet相关库(在NuGet中获取)除此之外我们还需要安装其他的几个库,也在NuGet管理库中添加。CsvHelper,EPPlus这两个库可以帮助我们解析CSV文件和EXCEL文件。功能编写private void DataChoose_Click(object sender, EventArgs e) {   using (OpenFileDialog openFileDialog = new OpenFileDialog())   {      openFileDialog.Filter = "CSV Files (*.csv)|*.csv|Excel Files (*.xls;*.xlsx)|*.xls;*.xlsx|All Files (*.*)|*.*"; DialogResult result = openFileDialog.ShowDialog(); if (result == DialogResult.OK)      {          string selectedFilePath = openFileDialog.FileName; LoadDataFromSelectedFile(selectedFilePath); } } } 当我们点击按钮的时候,唤醒资源管理器,选择我们的CSV文件或者Excel文件,并且将文件路劲作为变量传递给LoadDataFromSelectedFile函数。 当我们点击数据显示按钮的时候,先检查我们的XY轴是否已经选择,如果没有选择我们则进行报错,并退出函数。如果选择没有问题的话,我们就先检测数据是否有异常(这里应该添加更多的判断逻辑)之后根据我们选择的XY轴来将数据打印到Chart上作为原始数据。当我们点击拟合按钮,我们从波形图中获取各个点的数据保存到列表中,之后调用数学库中的线性拟合函数,将XY作为参数传入。效果展示regression 的两个成员item1和item2分别是截距和斜率,根据我们的截距和斜率来画出我们的线性拟合曲线。
实在太懒于是不想取名
1 2 嘉立创PCB
人工智能导论(2)——基本算法程序编写——回归
回归是统计学和机器学习中的一种分析方法,用于研究变量之间的关系。具体而言,回归分析用于建立一个模型来描述自变量(特征)与因变量(目标)之间的关系,并且该关系通常用一个函数来表示。在回归中,我们通常考虑的是连续型的因变量。回归分析的目标是找到一个适当的模型,以最好地拟合已知数据,并用于对未知数据的预测。回归模型的形式可以是线性的或非线性的,具体选择取决于问题的性质和数据的特点。以下是一些常见的回归模型:线性回归(Linear Regression):假设自变量与因变量之间的关系是线性的。多项式回归(Polynomial Regression):考虑自变量的高次项,使得模型可以拟合非线性关系。岭回归(Ridge Regression)和Lasso回归(Lasso Regression):用于处理多重共线性问题,同时对参数进行正则化。逻辑回归(Logistic Regression):用于处理二分类问题,输出是概率值,并通过一个逻辑函数将概率映射到0和1之间。支持向量回归(Support Vector Regression,SVR):适用于处理非线性关系,利用支持向量机的思想。..............本期我们使用线性回归实现对数据的预测(数据来源:文件 (lanzoui.com))我们将我们的样本复制到我们的工程目录接着我们导入我们的相关库包括:pandas库用来读取数据,线性回归模型以及画图库。import pandas as pd from sklearn.linear_model import LinearRegression import matplotlib.pyplot as plt 读取csv文件的第一列和第二列,用X和Y存储。df = pd.read_csv('data.csv') X = df.iloc[:, 0:1]  # 第一列y=df.iloc:,1# 第二列 接着创建一个线性回归模型,对我们的X和Y进行线性回归拟合model = LinearRegression() #训练模型model.fit(X, y)# 获取模型参数 slope = model.coef_[0] intercept = model.intercept_ 绘制出我们的线性回归模型:# 绘制原始数据和线性回归模型的拟合曲线 plt.scatter(X, y, color='black') plt.plot(X, model.predict(X), color='blue', linewidth=3, label='函数: y = {:.2f}X + {:.2f}'.format(slope, intercept)) plt.xlabel('X') plt.ylabel('y') plt.title('xian xing hui guiw') plt.legend() plt.show() 可以看到我们的线性回归曲线被打印了出来、我们也可以用R-sqared来评估我们的线性回归曲线。r_squared = model.score(X, y) print('R-squared:', r_squared) 可以看到我们的R-squared为0.9618,非常的接近1,说明我们的模型非常的成功。 最后我们也可以调用我们的模型进行预测predicted_y = model.predict([[0.5]])#因为是二维 print('Predicted y:', predicted_y)
实在太懒于是不想取名
0 3 嘉立创PCB
人工智能导论(1.2)——ROC曲线介绍以及绘制ROC曲线
上期我们介绍了Python的环境配置,本期我们介绍ROC曲线以及如何绘制ROC曲线。 ROC(Receiver Operating Characteristic)曲线是用于评估二分类模型性能的一种工具。它显示了在不同阈值下,真正例率(True Positive Rate,也称为灵敏度或召回率)与假正例率(False Positive Rate)之间的权衡关系。当我们讨论ROC曲线时,可以想象一个二分类模型,例如医学诊断中的癌症检测。我们用“正例”表示病患患有癌症,而“负例”表示患者没有癌症。真正例率 (TPR,也叫灵敏度或召回率): 假设我们的模型通过某个阈值,成功地找出了患有癌症的病患。 这就是一个真正例。 TPR告诉我们在所有实际患有癌症的病患中,有多少被成功地找出来了。 假正例率 (FPR): 然而,模型可能会犯错误,将一些实际上是健康的人错误地判定为患有癌症。 这就是一个假正例。 FPR告诉我们在所有实际健康的人中,有多少被错误地认为患有癌症。 现在 ,我们可以考虑不同的 阈值。 如果我们把阈值设置得很低,模型可能会把很多人都判定为患有癌症,这会增加TPR,但同时也增加了FPR。 反之,如果我们把阈值设置得很高,模型只会把那些非常确信患有癌症的人判定为患有癌症,这样会减少FPR,但可能也减少了TPR。ROC曲线就是在不同阈值下,TPR和FPR之间的权衡。如果你能画出ROC曲线,你可以看到在整个权衡范围内的模型表现情况。比如,一条理想的ROC曲线可能如下图所示: 这个图形展示了在不同阈值下,TPR和FPR的变化。曲线越靠近左上角,说明模型在保持高召回率的同时,控制假正例率的能力越好。ROC曲线正是通过不断移动分类器的“阈值”来生成曲线上的一组关键点 (也可以参考老师给的这篇博客:机器学习基础(1)- ROC曲线理解 - 简书 (jianshu.com)可能讲的更清晰一点) import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import roc_curve, auc 我们导入我们需要使用的库,我们使用import导入特定的库,使用from从特定关键字导入,使用as作为库的别称。y_true = np.random.randint(2, size=100) y_scores = np.random.rand(100) 我们使用random生成随机的一百组数据。fpr, tpr, thresholds = roc_curve(y_true, y_scores) 接着调用roc_curve代入我们生成的数 据 计算 ROC曲线的参数。roc_auc = auc(fpr, tpr) 计算ROC曲线的面积。plt.figure(figsize=(8, 8)) plt.plot(fpr, tpr, label='ROC曲线(AUC = {:.2f})'.format(roc_auc)) plt.plot([0, 1], [0, 1], linestyle='--', label='数据') plt.xlabel('FPR') plt.ylabel('TPR') plt.legend(loc='lower right') plt.show() 最后利用matplotlib生成我们的图像并显示就完成了我们的ROC曲线的绘制。
实在太懒于是不想取名
2 5 嘉立创PCB
人工智能导论(1)——Python环境安装与基础编程
人工智能是科技领域冉冉升起的一颗新星,年初的ChatGPT,智能驾驶等无不预示着AI时代的到来,而人工智能导论正是开始学习AI的开篇。本期我们以Python为工具之后逐步实现人工智能的四个实验(主要这门课就给我布置了四个实验)我们的语言使用Python,原因在于Python拥有丰富的深度学习框架,大量的开源工具以及库,丰富的拓展性以及使用人群,最重要的是简单易上手。环境安装首先是Python的安装与相关编程工具的下载。Python环境的安装有两个方式,一种方式是使用Anaconda安装Python全家桶第二种方法则是直接前往Python的官网下载Python环境。我推荐是使用第二种方法,前往Python的官网(因为那个Anaconda可能是我使用的原因,我之前在使用Anaconda的时候下了好几个版本,包括Mini Anaconda反正啰嗦的要死,我之前去Python官网的配置环境倒是简单的不要再简单了)某度搜索Python官网,进入官网之后,在下载界面找到我们的Python最新稳定版 现在最新的稳定版是3.12,我当时下的比较早是3.7(后来更新到3.10)不过只要大版本相同,区别都不大。接着我们下滑,找到我们的下载链接,点击下载源文件。在最下面找到对应我们系统的版本,我们的系统一般是Win 64(Win32 和 Linux的自行切换)不知是因为我在绿皮火车上的原因还是网站本身的原因(因为一般下载这种资源需要在镜像网站下载否则会非常非常慢)当然我这里提供懒人链接啦,公众号私信(Python.exe)千万别傻乎乎的把括号也加上去。有时候作者看到你们的私信都有那种想顺着网线过去打你们的冲动,记住就是Python.exe   10个字母,Python.exe !!!下载好Python.exe之后,我们运行这个文件。安装的时候这边的五个选项全部打勾(总没人傻乎乎的安装到C盘吧)(我为此还把正版的卸了装了盗版)安装完PyChram,我们的环境配置也就完成了,接下来介绍如何在PyCharm中创建我们相关的工程以及添加我们的各种各样的库。新建工程首先打开我们的PyCharm,点击File下拉框,点击New Project创建我们的新工程、红色方框处选择我们的工程位置,蓝色方框处选择我们的版本(我这里比较乱,大家选择能选的就行)点击This Window就不打开新的窗体了,在本窗体。main.py就是我们的主要文件,右键空白(黑)处,Run "main.py"就可以运行我们的程序。库安装接着我们为我们工程安装我们相关的工具包。在File中选择Settings选择Python Interpreter点击加号,添加我们的库。我们分别添加numpy、scipy、pandas、matplotlib、scikit-learn、sklearn等工具包;其中Numpy库可以用来处理多维数组和矩阵,神经网络的本质就是复杂的矩阵运算(不亏我线代考了五次)Scipy则是在Numpy的基础上提供了许多模块,例如方程求解,图像处理,插值算法,信号处理,统计等等。Pandas提供了两种额外的数据结构,可以帮助我们更好的处理数据。Matplotlib学过Matlab的朋友可能觉得很熟悉,因为在Matlab中就是使用Plot函数进行画图的,所以这个库顾名思义就是用来画二维图像的库。scikit-learn,这个直译,机器学习~~sklearn 这个,额奇怪,Python里面好像没有这个库,这个应该是scikit-learn的简称,我怀疑是我老师写错了。from sklearn.datasets import load_iris iris_dataset = load_iris() print("Keys of iris_dataset: \n{}".format(iris_dataset.keys())) print("Shape of data: {}".format(iris_dataset['data'].shape)) 我们运行我老师给的代码,在下方的终端可以看见运行结果(右键空白处点击Run "main")上述代码其实是导入模块:从这里也可以看出sklearn其实是简称from sklearn.datasets import load_iris 加载某个数据集:iris_dataset = load_iris() 打印其中的iris_dataset对应的值print("Keys of iris_dataset: \n{}".format(iris_dataset.keys())) 打印其中的样本数和特征数print("Shape of data: {}".format(iris_dataset['data'].shape)) 下期预告下期介绍Python的语法规范以及如何绘制ROC曲线
实在太懒于是不想取名
0 2 嘉立创PCB
基于C#的软件大杂烩(2.1)——MQTT助手
简单介绍 MQTT(消息队列遥测传输)是ISO 标准(ISO/IEC PRF 20922)下基于发布/订阅范式的消息协议。在之前的文章中我们也介绍过好几次使用MQTT:ESP32物联网教程之MQTT(Ardunio IDE)      百度云物联网之MQTT协议创建设备及获取信息....... 本期我们使用C#制作一个MQTT助手,可以实现指定MQTT设备的连接,主题的订阅与取消,消息的接收与发布。 我们简单的设置一下我们的界面,设置了四个文本框用来获取我们的连接信息。 接着我们在NuGet管理包中下载我们的M2MQTT库。 基础部分 在我们的类的顶部使用using 包含我们的库。using uPLibrary.Networking.M2Mqtt; using uPLibrary.Networking.M2Mqtt.Messages; 接着我们在点击连接的按钮中使用Client的Connect方法连接我们的MQTT服务端 private void Conect_Click(object sender, EventArgs e) { if (Conect.Text == "连接") { string username = UserText.Text; string password = PassText.Text; string clientName = NameText.Text; string address = AddressText.Text; client = new MqttClient(address); client.Connect(clientName, username, password); Console.WriteLine("Connected to MQTT as " + clientName); Conect_Click_Connect_Change(); } else { Conect_Click_Closed_Change(); } } private void Conect_Click_Connect_Change() { UserText.Enabled = false; NameText.Enabled = false; AddressText.Enabled = false; PassText.Enabled = false; Conect.Text = "断开连接"; } private void Conect_Click_Closed_Change() { UserText.Enabled = true; NameText.Enabled = true; AddressText.Enabled = true; PassText.Enabled = true; Conect.Text = "连接"; client.Disconnect(); } 并且点击连接之后禁用我们的其他控件。 关于MQTT连接信息可以参考公众号的这篇文章: 百度云物联网之MQTT协议创建设备及获取信 接着我们运行一下我们的程序: 我们可以看到我们的MQTT成功进行了连接。 优化 接着我们优化完善一下我们的布局: 我们添加几个文本框还有按钮用来设置我们的订阅信息包括主题还有安全性等等(实际上我们这个库比较简单,这个安全性没啥子用)并且我们在成功连接之前我们禁用这些控件。 private void AddTopic_Click(object sender, EventArgs e) { //弹出输入框输入主题 var userInput = GetUserTopicInput(); string topic = userInput.topic; byte qos = userInput.qos; // 首先检查用户输入是否为空 if (string.IsNullOrEmpty(topic)) { MessageBox.Show("主题不能为空"); return; } // 判断TopicBox中是否已包含该主题 if (TopicBoxtems.Contains(topic)) { MessageBox.Show("主题 " + topic + " 已经存在"); return; } // 如果不存在就订阅并添加到TopicBox中 //client.Subscribe(new[] { topic }); byte[] byteArray = Encoding.UTF8.GetBytes(SubQOS.Text); try { string[] topics = { topic }; client.Subscribe(topics, byteArray); client.MqttMsgPublishReceived += Client_MqttMsgPublishReceived; MessageBox.Show("订阅成功"); } catch (Exception ex) { MessageBox.Show("订阅失败"); } TopicBox.tems.Add(topic); TopicBox.SelectedIndex = 0; TransTopic.Items.Add(topic); } private void UpdateReciveText(string message) { if (ReciveText.InvokeRequired) { ReciveText.Invoke(new Action[removed](UpdateReciveText), message); } else { // 在 UI 线程上更新 ReciveText 控件 ReciveText.AppendText(message + "\r\n"); } } // 在其他地方调用这个方法,传递要显示的消息 private void Client_MqttMsgPublishReceived(object sender, MqttMsgPublishEventArgs e) { // 收到发布的消息时执行 string topic = e.Topic; // 主题 byte[] payload = e.Message; // 消息负载 // 处理消息... UpdateReciveText("收到主题: " + topic + "\r\n" + "消息: " + Encoding.UTF8.GetString(payload)); Console.WriteLine("收到主题: " + topic); Console.WriteLine("消息: " + Encoding.UTF8.GetString(payload)); } private (string topic, byte qos) GetUserTopicInput() { string topic = Interaction.InputBox("请输入Topic:", "输入", ""); byte qos = 0; return (topic, qos); } 这部分代码包括弹出提示框用来输入我们的主题,判断这个主题是否已经被订阅以及订阅我们的主题。 之后设置我们的回调函数用来处理MQTT接收到的信息。 效果展示 运行程序,输入用户名以及密码,之后使用百度云的MQTT服务器端我们是可以接收到数据(图片欠着)。
实在太懒于是不想取名
0 4 嘉立创PCB
基于C#的软件大杂烩(1.3)——串口助手的优化
前端时间做的串口示波器做了许多优化,其代码量也到达了将近2000行 在原本的基础上做了很多内容的增加,私信回复《串口助手》即可获取发布链接(还有很多内容尚未完善),欢迎朋友们提出改进意见以及点赞分享。 包括但不限于: 控件提示:将鼠标悬停于控件上可以提示相关控件信息。 接收设置优化: 利用定时器更新UI控件防止当串口收发速度过快的时候导致UI进程被占用无法使用。 串口详细信息显示: 显示串口的详细信息包括蓝牙,CH340等详细内容的显示,方便我们在连接蓝牙设备,CH340设备的多串口选择。 热拔插刷新:在使用串口的过程中拔插串口或者在软件启动后加入新的串口时会刷新串口列表以及对状态进行修改。 布局优化:使用Panel容器优化界面布局,是的波形显示和接收显示界面分开可以自由来回切换。 波形显示逻辑: 修改波形显示逻辑,我们可以在使用串口的同时选择是否开启波形显示(之前是在打开串口前进行选择) 滚轮控制显示区域:增添了利用滚轮控制波形的显示范围,之前只能利用滑条来控制区域。 傅里叶变换:增加了对指定区域的傅里叶变换功能,将时域图转换成频域图。 保存图表: 可以保存我们的波形图到我们的文件夹中。 数据统计: 增加了统计波形显示区域内的最大值与最小值(后续可能更新更多的统计数据) 此外还有许多地方尚未完成,例如自定义数据帧解析,多行发送,高速模式下时间戳的使用等等~欢迎大家提出建议,会多多吸取意见并修改。 以下展示部分源码: private void Chart1_MouseEnter(object sender, EventArgs e) { chart.MouseWheel += Chart1_MouseWheel; } private void Chart1_MouseLeave(object sender, EventArgs e) { chart.MouseWheel -= Chart1_MouseWheel; } private void Chart1_MouseWheel(object sender, MouseEventArgs e) { try { if (e.Delta [removed] 0) // 向上滚动 { // 减小 chartLength 的值,可以根据需要调整减小的步长 ChartLenth -= 100; if (ChartLenth [removed] 0) { using (SaveFileDialog saveFileDialog = new SaveFileDialog()) { saveFileDialog.Filter = "PNG Files (*.png)|*.png|All Files (*.*)|*.*"; saveFileDialog.DefaultExt = "png"; saveFileDialog.Title = "Save Chart Image"; if (saveFileDialog.ShowDialog() == DialogResult.OK) { SaveChartImage(chart, saveFileDialog.FileName); MessageBox.Show("图表已成功保存为图片!", "成功", MessageBoxButtons.OK, MessageBoxcon.Information); } } } } private void SaveChartImage(Chart chart, string filePath) { try { // 使用 SaveImage 方法保存图表为图片 chart.SaveImage(filePath, ChartImageFormat.Png); } catch (Exception ex) { // 处理保存异常 MessageBox.Show("保存图片时发生错误:" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxcon.Error); } } 图片保存部分的代码,启用资源管理器来获取我们的保存信息,之后利用chart控件自带保存控件的方法来保存我们的控件。 private void FuChange_Click(object sender, EventArgs e) { int Xmax = int.Parse(MAXArea.Text); int Xmin = int.Parse(MinArea.Text); if (Xmax < Xmin || Xmin < 0) { MessageBox.Show("请输入正确的范围"); return; } if (chart.Series.Count == 0) { MessageBox.Show("图表中没有可用的系列"); return; } // 获取选定范围内的数据点 List[removed] selectedDataPoints = new List[removed](); foreach (DataPoint dataPoint in chart.Series[0].Points) { if (dataPoint.XValue >= Xmin && dataPoint.XValue <= Xmax) { selectedDataPoints.Add(dataPoint); } } // 将数据点转换为复数数组 Complex[] complexData = new Complex[selectedDataPoints.Count]; for (int i = 0; i < selectedDataPoints.Count; i++) { complexData[i] = new Complex(selectedDataPoints[i].YValues[0], 0); } // 执行傅里叶变换 Fourier.Forward(complexData, FourierOptions.Matlab); // 将结果显示在 FuChart 上 FuChart.Series.Clear(); WindowsFormsSeries fuSerial = new WindowsFormsSeries("傅里叶变换结果"); fuSerial.ChartType = SeriesChartType.FastLine; fuSerial.BorderWidth = 3; FuChart.Series.Add(fuSerial); // 计算频率间隔 double frequencyInterval = 1.0 / (selectedDataPoints[1].XValue - selectedDataPoints[0].XValue); // 显示正频率部分 for (int i = 0; i [removed]
实在太懒于是不想取名
1 13 嘉立创PCB
基于C#和ESP32的远程示波器(6)—— 合理的利用数据缓存区来消除数据丢失
啊~我的天!好久没更新这个系列了,主要是在琢磨怎么彻底的消除数据不连续的问题。 这个星期从硬件到软件到通讯协议,试过了很多方法来解决数据,不连续的问题。光通讯协议就用了HTTP,UDP,WebSockerts以及TCP还为每一个通讯协议专门写了数据处理的代码和示波器(每天写到吐了、无数的BUG和Error) 最后我们解决的方法还是:定时器控制采样率、HTTP将数据打包上传、利用标志位来交替写入缓存区。 上期问题阐述与解决 我简单的阐述一下上一期的代码出现的问题: 首先是我们在任务中运行ADC采集,这样子会导致我们无法控制我们的采样率,这样子对于信号采集来说是不应该出现的。 其次我们的数据在Data = PostData中会有一个数据传递的过程,又可以造成不必要的耗时。 因此我们选择的方法是使用定时器来定时触发我们的ADC采样。 并且使用两个数据缓存区PostData1和PostData2,定时器交替向这两个缓存区写入数据。 主要代码 我们首先加入定时器的头文件(利用的也是FreeRTOS)以及声明其句柄。#include [removed] TimerHandle_t timer; 接着在主函数中加入定时器的配置timer = xTimerCreate("DataTimer", pdMS_TO_TICKS(2), pdTRUE, NULL, onTimer); xTimerStart(timer, 0); 该定时器的名字命名为:DataTimer,2ms触发一次,自动装填,回调函数为onTimer 接着我们定义我们的回调函数的内容。int QueueFlag = 1; void IRAM_ATTR onTimer(TimerHandle_t xTimer) { static int j = 0;   int ADCVlaue[4]; for (int i = 0; i [removed]
实在太懒于是不想取名
0 2 嘉立创PCB
基于C#的软件大杂烩(1.2)——串口示波器
上一期我们实现了串口助手收发数据之后,这期我们为我们的串口助手加上我们的波形图功能。 我们在前面有一期中介绍如何在C#中使用chart画图库 本期我们利用我们上一期制作的串口助手来实现我们的串口示波器。 准备工作 首先我们在上一期的基础上加上我们的chart库using System.Windows.Forms.DataVisualization.Charting; 其次,简单的布置一下我们的界面。 其中图表和按钮的默认状态是隐藏的,当我们勾选波形显示时显示。 private void ChartShow_CheckedChanged(object sender, EventArgs e) { if (ChartShow.Checked == true) { Receive.Height = 680/2; chart.Show(); ClearChart.Show(); } else { Receive.Height = 680; chart.Hide(); ClearChart.Hide(); } } 我们勾选按钮的时候,将接收区的高度减少一半,让按键和图表显形。 数据解析与存放 我们希望我们的数据格式是类似“a:xxx,b:xxx”等等。因此,我们需要解析我们的数据private Dictionary[removed] ParseData(string input) {             Dictionary[removed] result = new Dictionary[removed](); // 按逗号分割字符串             string[] pairs = input.Split(','); foreach (string pair in pairs) { // 再按冒号分割键值对                 string[] keyValue = pair.Split(':'); if (keyValue.Length == 2) { string key = keyValue[0].Trim();                     string value = keyValue[1].Trim(); result[key] = value; } } return result; } 我们定义一个解析键值对函数,利用逗号分隔字符串,冒号分隔将解析的结果返回,这样子我们就可以获取我们的键值对。 // 添加解析数据的代码 string[] lines = data.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); lines = lines.Skip(1).Take(lines.Length - 2).ToArray(); 由于我们的数据可能有很多行,因此我们在更新图表的代码中需要将数据按照换行符分开,并且由于第一行和最后一行的数据可能不完整,我们需要删除第一行和最后一行的数据。foreach (string line in lines) {     // 对每一行进行解析     Dictionary[removed] parsedData = ParseData(line);     foreach (var kvp in parsedData) {        // 继续处理解析后的数据       int existingIndex = chart.Series.IndexOf(kvp.Key);       if (existingIndex != -1)       {          //如果该系列的图表存在           chart.Series[existingIndex].Points.AddXY(chart.Series[existingIndex].Points.Count, double.Parse(kvp.Value));      } else { //如果该系列的图表不存在 string seriesName = new string(kvp.Key.TakeWhile(char.IsLetter).ToArray()); // 获取字符串中的所有字母前缀 Series newSeries = new Series(seriesName); newSeries.ChartType = SeriesChartType.Spline; // 设置图表类型为 Spline newSeries.Points.AddXY(0, double.Parse(kvp.Value)); chart.Series.Add(newSeries); } } } 我们将我们解析的数据放入图表,按照数据的前缀来划分数据。 判断图表是否存在,如果不存在就添加相应的系列。 效果展示 接着让我们看看效果
实在太懒于是不想取名
1 9 嘉立创PCB
基于C#的软件大杂烩——手搓串口助手
作为一个有着伟大理想,崇高信念21青年,在我平时用着串口助手、计算器的时候。我总是寻思着,我能不能自己去实现做一个软件,能实现我日常想要做的功能,例如串口助手啦,图片助手啦,文件转换啦等等。 于是打算从0.5开始手搓一个大杂烩软件。 首先的工具我就想到了串口助手,那么本期我们就介绍如何使用C#制作串口助手。相关库安装 首先,我们需要在NuGet包管理中,找到我们的关于端口的包 我们搜索这个包并且安装这个包,这样子我们就可以在C#中调用我们的端口类了。 页面布局 通常我们的串口助手最基本的数据接收和发送,打开和关闭串口以及选择端口设置波特率等等,所以我们需要若干个TextBox文本框,Label标签以及Button按钮来实现我们的基本功能最后还需要有一个下拉式的ComboBo来选择我们的串口 我们在左边的空间库中寻找我们需要的控件,放置到我们的设计器中。 基础编写private void LoadAvailablePorts() { // 获取可用端口 string[] ports = SerialPort.GetPortNames(); // 将端口添加到ComboBox中 COMChoose.Items.AddRange(ports); // 如果需要,默认选择第一个端口 if (COMChoose.Items.Count > 0) { COMChoose.SelectedIndex = 0; } } 我们首先编写获取端口的代码,并且在FromLoad函数中调用这个代码,这样子我们就可以加载端口显示在下拉框中CheckBit.SelectedIndex = 0; 我们在这些选项框中使用默认第一个数据。private void Connect_Click(object sender, EventArgs e) { if (serialPort.IsOpen) { // 串口已经打开,关闭串口 serialPort.Close();         Connect.Text = "打开串口"; // 启用控件 COMChoose.Enabled = true; PortText.Enabled = true; DataBit.Enabled = true; StopBit.Enabled = true; CheckBit.Enabled = true; } else { // 串口未打开,尝试打开串口 try { // 配置串口 serialPort.PortName = COMChoose.SelectedItem.ToString(); serialPort.BaudRate = int.Parse(PortText.Text); // 获取波特率 serialPort.DataBits = int.Parse(DataBit.SelectedItem.ToString()); // 获取数据位 // 设置停止位 switch (StopBit.SelectedItem.ToString()) { case "1": serialPort.StopBits = StopBits.One; break; case "1.5": serialPort.StopBits = StopBits.OnePointFive; break; case "2": serialPort.StopBits = StopBits.Two; break; default: serialPort.StopBits = StopBits.One;              break;      } // 设置校验位 switch (CheckBit.SelectedItem.ToString()) {      case "None": serialPort.Parity = Parity.None;             break; case "Odd": serialPort.Parity = Parity.Odd; break; case "Even": serialPort.Parity = Parity.Even; break; default: serialPort.Parity = Parity.None; break;      }      // 打开串口 serialPort.Open();      Connect.Text = "关闭串口";      // 禁用控件 COMChoose.Enabled = false; PortText.Enabled = false; DataBit.Enabled = false; StopBit.Enabled = false;      CheckBit.Enabled = false; serialPort.DataReceived += DataReceivedHandler; } catch (Exception ex) {        // 打开串口失败,显示错误消息 MessageBox.Show("串口打开失败" + ex.Message); } } } 我们在点击按钮的时候设置的代码是:点击按钮改变按钮的文本,并且配置按钮的各项数据,禁用/开启端口选择..等等的使用以及创建新的回调函数(DataReceivedHandler)在这个函数中编写我们收到的代码信息。private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e) { try   { // 从串口读取数据 string data = serialPort.ReadExisting(); // 将数据追加到 Receive 文本框中 UpdateReceiveTextBox(data); } catch (Exception ex) { MessageBox.Show("接收失败" + ex.Message); } } private void UpdateReceiveTextBox(string data) { if (Receive.InvokeRequired) { // 如果不在 UI 线程上,通过 Invoke 在 UI 线程上更新文本框 Receive.Invoke(new Action[removed](UpdateReceiveTextBox), data); } else { // 在 UI 线程上更新文本框 Receive.AppendText(data); } } 我们在串口回调函数中简单的添加一些内容,我们将收到的串口打印到我们的TextBox上(UpdateReceiveTextBox这个函数也是我们自己定义的)作用是用来区分当前程序是不是在UI线程上,如果不在UI线程上,我们需要用另一种方式来更新我们的控件。 接收串口数据成功,我们也添加了时间戳的功能,用来为我们的代码接收添加时间。 // 在 UI 线程上更新文本框  if (TimeTik.Checked)  {   // 如果 TimeTik 被勾选,加上时间戳 data = $"{DateTime.Now:HH:mm:ss} -> {data}"; } Receive.AppendText(data); 接着我们来编写发送部分的代码private void TransBotton_Click(object sender, EventArgs e) { try    { // 获取要发送的文本    string dataToSend = TransText.Text;    // 判断是否勾选了 NewLine 复选框 if (NewLine.Checked) { // 添加新行      dataToSend += Environment.NewLine;  ‍‍          ‍ ‍ ‍‍‍‍‍ }‍‍‍ ‍ // 发送数据 serialPort.Write(dataToSend); } catch (Exception ex) {     MessageBox.Show("串口发送失败");     } } 其实这个东西运行起来还是有不少BUG的,需要我们不断的优化,感兴趣的朋友可以后台私信联系。
实在太懒于是不想取名
3 8 嘉立创PCB
基于C#和ESP32的远程示波器(5)—— 利用FreeRTOS双核运行减少数据丢失
在我们上一期代码中,我们利用HTTP将我们的数据上传到局域网,但是由于我们的程序是串行程序,即ADC采集完需要等待数据上传成功才能继续运行我们的代码,因此我们的数据就会出现很严重的数据丢失。 而我们在之前学习过STM32中的FreeRTOS即实时操作系统,将不同的任务按照不同的优先级分别运行。因此我们尝试使用FreeRTOS将ADC采集和数据上传的任务按照不同级别的优先级进行划分。 关于FreeRTOS的介绍可以查看前几期的内容基于STM32F407的FreeRTOS学习笔记(13)—— 阶段性的总结与后续 FreeRTOS库准备 FreeRTOS在ESP32中的环境配置比在STM32中简单许多,我们只需要在库管理中搜索FreeRTOS即可安装我们的FreeRTOS 我们直接安装最新版本的FreeRTOS#include[removed]#include [removed] #include[removed]#include [removed]//FreeRTOS头文件 #include [removed]//任务头文件 #include [removed]//信号量头文件 之后我们放入相关头文件包含信号量和任务头文件还有FreeRTOS的基本头文件。 任务创建 任务创建可以参考我们的公众号文章基于STM32F407的FreeRTOS学习笔记(2)——任务的创建与删除TaskHandle_t Task1, Task2; xTaskCreatePinnedToCore( Task1Code, // 任务函数 "Task1", // 任务名称 10000, // 任务堆栈大小 NULL, // 任务参数 1, // 任务优先级 &Task1, // 任务句柄 0); // 核心编号 // 创建任务2,绑定到核心1 xTaskCreatePinnedToCore( Task2Code, // 任务函数 "Task2", // 任务名称 10000, // 任务堆栈大小 NULL, // 任务参数 2, // 任务优先级 &Task2, // 任务句柄 1); // 核心编号 我们创建任务1和任务2 的句柄,并且由于ESP32是双核的MPU,比起STM32我们可以将不同的任务绑定到不同的核心进行使用,我们设置两个任务,并且任务1的优先级大于任务2的优先级。 我们将任务1用于ADC采集,任务2用于数据上传。String postData = ""; String Data; int dataArray[1000]; // 任务1的函 void Task1Code(void *parameter) { for (;;) { for (int i = 0; i < 1000; i++) { dataArray[i] = analogRead(34); postData += String(dataArray[i]); if (i [removed] 0) { Serial.print("HTTP Response code: "); Serial.println(httpResponseCode); String response = http.getString(); Serial.println(response); } else { Serial.print("HTTP Request failed. Error code: "); Serial.println(httpResponseCode); } } delay(1000); } } 接着我们定义我们的任务函数,关于HTTP的部分参考上一期公众号内容。 我们每次采集好数据之后释放信号量,任务2等待信号量的释放,当任务1释放信号量之后我们上传我们的数据,需要注意的是,此时任务2上传数据时postData依旧在进行改变,所以我们需要用一个新的变量来接受PostData。 效果展示 我们来看看我们的运行效果
实在太懒于是不想取名
1 5 嘉立创PCB
基于C#和ESP32的远程示波器(4)—— ESP32在局域网中利用HTTP协议代替MQTT协议
上一期我们使用MQTT协议连接到百度云并且向C#的客户端上传了我们的数据。 我之后也成功使用ESP32上传数据到MQTT服务器,在C#中显现折线图,但是随之而来的我们发现了新的问题:这消息量好像有点庞大了 短短的测试环节我就发送了将近20w条消息(幸亏我关的早),于是当机立断选择放弃使用MQTT协议,改用其他协议来传输数据。 MQTT协议适合小型数据量的传输,那么对于我们这么庞大的数据量来说确实不太适合,于是我们选择常用的超文本传输协议:HTTP来传输我们的数据。 HTTP简单介绍 HTTP由三个部分组成: 起始行(start line):描述请求或响应的基本信息; 头部字段集合(header):使用 key-value 形式更详细地说报文; 消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。 起始行中我们需要确定我们的HTTP是进行请求还是发送。 这里不细细赘述如何具体使用HTTP报文的详细,我们简单的介绍一下在ESP32中如何使用HTTP。 HTTP发送 首先,我们在Arduino IDE中安装我们的HTTP相关的库,我们在库管理中搜索HttpClient安装相关库。 其次按win+R调出命令窗口,输入cmd并运行调出终端面板,之后输入ipconfig查询我们的电脑IP 在IPv4的地址中(任意一个)查看我们的局域网IP地址 我们在ESP32中保存我们的Wifi用户名,Wifi密码还有我们刚才的IP地址,注意加上http://还有确定我们使用的端口号,我们使用8081端口,最后一定一定记得加上斜杠void WIFIConnect() { WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(1000); Serial.println("Connecting to WiFi..."); } Serial.print("IP Address: "); Serial.println(WiFi.localIP()); Serial.println("Connected to WiFi"); } 我们定义一个函数来连接我们的Wifi,因为我们使用的ESP32的定时器2,使用Wifi的时候不能 使用ESP32的ADC2,因此我们使用ADC2的时候我们需要关闭我们的Wifi。 String postData = ""; for (int i = 0; i < 1000; i++) { dataArray[i] = analogRead(13); postData += String(dataArray[i]); if (i [removed] ListenForHttpRequests()); } 接着我们定义我们的监听函数,我们在监听函数中解析我们的数据。 private void HandleHttpRequest(HttpListenerContext context) { try { // 从请求中获取数据 Stream body = context.Request.InputStream; Encoding encoding = context.Request.ContentEncoding; StreamReader reader = new StreamReader(body, encoding); if (context.Request.HasEntityBody) { string postData = reader.ReadToEnd(); Console.WriteLine($"Received data from ESP32: {postData}"); // 将以逗号分隔的多个数值解析为数组 string[] values = postData.Split(','); Invoke((MethodInvoker)delegate { for (int i = 0; i [removed]
实在太懒于是不想取名
3 7 嘉立创PCB
基于C#和ESP32的远程示波器(3)—— 使用C#连接MQTT百度云服务器
上期我们介绍了C#中的折线工具,本期我们介绍C#中利用MQTT协议连接baidu服务器。在使用MQTT之前,我们需要获得我们的MQTT服务器地址和相关信息,往期有介绍关于如何使用百度云创建MQTT服务器并获取连接信息的文章,可以参考百度云物联网之MQTT协议创建设备及获取信息。关于ESP32连接MQTT的部分可以参考下的(哥们能不水就不水)ESP32物联网教程之MQTT(Ardunio IDE)但是有一点要注意的是,WIFI的使用会占用ADC2导致ADC2不能用,所以不能使用ADC2,或者先采集数据,再连接WIFI,再上传数据,之后再断开连接·相关库在C#中有许多库是可以使用MQTT的,使用较多的有MQTTnet,M2Mqtt等,我们选择的是M2Mqtt库(MQTTnet我没用明白)我们打开NuGet程序包管理。在其中搜索M2Mqtt安装最新版即可。连接MQTT服务器接着我们在代码中连接我们的MQTT服务器。首在全局中创建我们的MQTT句柄,用以连接我们的MQTT服务器。接着加入如下代码,连接MQTT服务器并且配置我们的回调函数 private void InitializeMqttClient() { // 服务器地址 string brokerAddress = "你的服务器地址"; // 你的用户名和密码 string username = "你的用户名"; string password = "你的密码"; // 创建MqttClient实例 mqttClient = new MqttClient(brokerAddress); // 设置用户名和密码 mqttClient.Connect(Guid.NewGuid().ToString(), username, password); // 订阅主题 string topic = "你的主题"; mqttClient.Subscribe(new string[] { topic }, new byte[] { MqttMsgBase.QOS_LEVEL_AT_MOST_ONCE }); // 设置接收消息的回调函数 //这个Client_MqttMesgPublishReceived是我自己定义的 mqttClient.MqttMsgPublishReceived += Client_MqttMsgPublishReceived; Console.WriteLine($"Connected to {brokerAddress} with username {username}"); } 服务器的地址用户名和密码,参考开头的那期有讲解如何获取相关信息的。接着我们在Form函数中调用这个函数,这样子我们在窗口加载的时候就可以连接到我们的MQTT服务器 public Form1() { InitializeComponent(); InitializeMqttClient(); } 接着我们点击运行,前往百度云的后台查看是否有设备连接并运行。首先我们可以看到我们的控制台提示:我们已经连接到了MQTT服务器,并且打印了服务器地址以及用户名。之后在运维管理中,我们也可以看到我们当前有一个设备正在使用。接收MQTT消息并显示连接好设备之后,我们需要编写我们的相关回调函数,我们之前定义了我们的回调函数这里要对其做具体定义。我们在上一期的时候介绍了如何使用chart控件显示折线图,我们在消息回调函数中,利用chart控件,将我们的信打印出来。 private void Client_MqttMsgPublishReceived(object sender, MqttMsgPublishEventArgs e) { // 处理接收到的消息 string message = Encoding.UTF8.GetString(e.Message); number++; Console.WriteLine(message); // 在图表中添加新数据点 this.Invoke((MethodInvoker)delegate { Series series = chart1.Series[0]; series.Points.AddXY(number,int.Parse(message)); // 如果超过一定数量的数据点,删除最旧的数据点,保持图表显示的数据量有限 if (series.Points.Count > 50) { series.Points.RemoveAt(0); } }); } 这里我还定义了一个number变量,用以确定折线图的下标。需要注意的是,我们收到的数据应该使用Encoding.UTF8.GetString方法转化成字符串否则会报错。接着我们在百度云中,创建一个新的实例,然后模拟设备,进入MQTT服务端我们需要订阅和我们程序中连接的服务端一样的主题,之后设置我们的消息,点击发布(Publish)可以看到我们成功的发布了消息。同样的我们C#中的服务器也接收到了消息并且在折线图中打印了出来。这样子我们就成功的让我们的C#服务器连接到MQTT云平台啦。
实在太懒于是不想取名
0 6 嘉立创PCB
基于STM32系列的气压传感器MS5611——获取温度数据和气压数据
 MS5611是一款由瑞士公司MEAS推出的高分辨率气压传感器。它具有以下特点:接口:支持SPI和I²C总线接口。(通过PS脚来选择)内部结构:包括高线性度的压力传感器和超低功耗的24位累加模数转换器(工厂校准系数)。功能:提供精确的24位数字压力值和温度值,适用于高度计、温度计等功能。尺寸:仅有5.0毫米×3.0毫米×1.0毫米的小尺寸,适合集成在移动设备中。    这款传感器采用领先的MEMS技术,具有高稳定性和非常低的压力信号滞后。它在移动高度计、气压计系统、自行车电脑、智能手机等领域有广泛的应用准备工作由于芯片上支持IIC通讯,因此我们打算使用STM32的硬件IIC,我的STM32为STM32F407。我们打开CUBEMX,选择好我们的时钟还有其他设置之后,我们打开我们的硬件IIC并且我们打开串口来进行调试,我们选择IIC1,可以发现是PB6/PB7。并且我们在USART.C文件中加入我们的串口重定向代码,方便我们使用printf函数struct __FILE { int handle; }; struct __FILE __stdout; //定义_sys_exit()以避免使用半主机模式 void _sys_exit(int x) { x = x; } //重定义fputc函数 int fputc(int ch, struct __FILE *f) { while((USART1->SR&0X40)==0);//循环发送,直到发送完毕 USART1->DR = (unsigned char) ch; return ch; } 利用这串代码,我们就可以把我们的串口发送函数重定向到我们的printf函数上。之后我们测试一下,我们的串口发送。我们在MS5611的官方手册中可以看到,MS5611的数据主要由C1~C6这六个校准系数以及D1,D2两个数据组成。我们在向MS5611发送数据转换命令的时候,模块会把对应的信号转换好之后存入PROM寄存器中,我们需要读取PROM的数据来读取这些数据,PROM的地址从0XA0~0XAE 代码编写#define MS5611_I2C_ADDR 0xEE //PS拉高的地址 #define CMD_ADC_READ    0x00 //ADC转换开始 #define CMD_ADC_CONV_D1 0x40 //D1读取命令 #define CMD_ADC_CONV_D2 0x50 //D2读取命令 #define CMD_PROM_RD 0xA0 //PROM基地址 typedef struct {     double temperature;//温度     double pressure;//气压 uint32_t D1; uint32_t D2; uint16_t C1; uint16_t C2; uint16_t C3; uint16_t C5; uint16_t C6; } MS5611Data; 首先我们定义模块的命令宏定义以及我们的结构体用来存放我们的数据。extern I2C_HandleTypeDef hi2c1; void ReadMS5611Data(MS5611Data *ms5611Data); 其次定义我们的hi2c1句柄,由于这个句柄的实际定义是在i2c.hd文件中,因此我们需要加上extern来修饰我们的变量接着我们来定义我们的ReadMS5611Data函数,我们分步讲解。 HAL_I2C_Mem_Write(&hi2c1, MS5611_I2C_ADDR, CMD_ADC_CONV_D1, I2C_MEMADD_SIZE_8BIT, NULL, 0, 100); HAL_Delay(10); uint8_t buffer[3]; HAL_I2C_Mem_Read(&hi2c1, MS5611_I2C_ADDR, CMD_ADC_READ, I2C_MEMADD_SIZE_8BIT, buffer, 3, 100); ms5611Data->D1 = ((uint32_t)buffer[0] << 16) | ((uint32_t)buffer[1] <[removed]D2 = ((uint32_t)buffer[0] << 16) | ((uint32_t)buffer[1] << 8) | (uint32_t)buffer[2]; 这部分也是同理,我们用这个方式获取D2的值。uint8_t coef_data[2]; for (int i = 1; i [removed]C1 = (coef_data[0] <[removed]C2 = (coef_data[0] <[removed]C3 = (coef_data[0] <[removed]C5 = (coef_data[0] <[removed]C6 = (coef_data[0] <[removed]D2 - ((int32_t)ms5611Data->C5 *256); double temp = 2000+((double)dt/ 8388608)*ms5611Data->C6; 接着我们按照手册中的方法计算我们的数据,需要注意的是,temp的类型最好选择是double型,否则dt/8388608(1<[removed]C2 * 65536) + ((int64_t)ms5611Data-> * dt) / 128; int64_t sens = ((int64_t)ms5611Data->C1 <[removed]C3 * dt) / (1 <[removed]D1 * sens/2097152 - off) / (1 <[removed]
实在太懒于是不想取名
2 11 嘉立创PCB
基于C#和ESP32的远程示波器(1)—— C#的简单介绍与使用
最近在做一个项目,关于远程采集数据并上传的一套装置, 为此也决定做一期合集用以记录制作流程。 关于硬件方面我选择了ESP32,ESP32有双核240MHZ的主频,并且自带ADC以及DAC,非常方便我们的数据采集与云端上传。 关于上位机方面,我选择了C#,C#作为基于C语言的图形化编程语言,可以有效的帮助我们编写相关的上位机。我也是曾经使用C#完成过一个远程LED点阵控制系统的制作。 本期我们的主要内容是介绍一下C#的简单使用。C#环境 我所使用的C#是在Visual Studio中安装的, 在创建项目中,我们在右边选项中拉到底,点击安装多个工具和功能既可安装我们的C#环境 选择通用Windows平台开发,安装即可。 简单使用 我们创建工程时,选择Windows窗体应用。 进入工程界面,有我们的窗体,左边的工具栏,右边的项目资源管理器。 我们可以从左边的工具栏选择常用的工具到我们的窗体上。 右下角则是关于这些控件的属性,例如大小,位置,字体大小,名称等等。 我们放置一个标签和一个按钮,并修改他们的字体大小。 接着我们双击我们的控件就可以进入相应的编写代码,例如我们双击我们的按钮。 我们可以看到这时候我们创建了一个函数,函数的名称是button1_Click,我们可以理解为这个函数是我们按下按钮后调用的函数,我们在这个函数中编写,标签的值改变。 再点击上方的运行按钮。 这时候就会跳出我们的窗口了。 然后我们点击Hello按钮。 我们可以发现标签的文本发生了改变。 这样子就简单的使用了C#,C#中有很多强力的工具和控件,我们合理的使用这些控件可以很好的帮助我们实现上位机软件的编程。
实在太懒于是不想取名
2 8 嘉立创PCB