互联网不景气了那就玩玩嵌入式吧,用纯.NET开发并制作一个智能桌面机器人(二):用.NET IoT库编写驱动控制两个屏幕

news/2025/3/11 2:57:54/文章来源:https://www.cnblogs.com/GreenShade/p/18671407

前言

从.NET IoT入门开始这篇文章想必大家应该都看过了,也有很多人都该着手购买树莓派Zero 2W进行上手体验了,那么我们这篇文章就开始真正的实践了,玩硬件肯定是要亲自操作得出成果才会开心,由于牵扯到硬件,所以有的时候软件没问题,但是硬件接线错误或者接触不良都会结果不正常,这个时候就需要我们有个强大的内心了,不能被困难打倒,不能半途而废,图上的为我画的PCB板子最终脱离数据线的效果。
img

问题解答

上一篇文章里有人问外壳模型的问题,这个我是自己设计的模型,后面我会把设计文件都开源出来,大家可以通过自己的3D打印机打印,也可以去一些在线平台下单打印都可以操作,这个不用担心。

关于电路板,这个桌面机器人我为了简化线路,绘制了一个ups板子,外加把显示屏的线路也整合到一起了,但是这篇文章还用不到这个电路板子,我们可以通过屏幕模块和杜邦线之类的进行验证测试。
img

上篇文章还有人推荐nanoframework的,这个框架是针对esp32和stm32的单片机提供的库,不是完整的.NET,好多东西都是定制的,所以和我文章里的做法是有一些区别的,这个大家有兴趣可以玩玩看。

还有个小问题,就是为什么我在发布项目的时候不选择专有的Arm64版本,这个主要是简化大家的操作,因为有的小白用户,让他多操作一个肯定是要多记住一步,这样也不好,而且有时候一些项目我们想在电脑测试完之后直接复制到树莓派上,这样可移植版本也不用在重新发布了。

名词解释

1. 什么是GPIO

GPIO(General-purpose input/output)即通用输入输出端口,是嵌入式设备中非常基础的一部分。它们允许嵌入式系统与外界环境交互,可以被配置为输入或输出模式。在输入模式下,GPIO可以读取来自传感器、开关等外部设备的信号;在输出模式下,它可以控制LED灯、电机等外部设备。GPIO是硬件和软件之间通信的桥梁,通过编程可以灵活地控制它们进行各种操作。

2. wiringPi,BCM,BOARD编码关系和区分

BOARD编码中的37号引脚,在wiringPi 中的编码就是25号引脚,在BCM中的编码就是26号引脚,他们有的功能都是GPIO.25(通用输入输出管脚25),BOARD编码和BCM一般都在python库中使用,而wiringPi一般用于C++等平台。注意,.NET IoT默认使用的BCM所以大家接线注意对着BCM进行接线和代码编写。
img

3. 什么是SPI

SPI,是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议,比如MSP430单片机系列处理器。

准备工作

1. 硬件准备

首先购买屏幕模块1.47寸和2.4寸总共两个(用于学习测试),裸屏两块用于最后机器人复刻(如果只是学习可以只买模块),外加杜邦线公对公,母对母和公对母。建议大家准备风枪烙铁和镊子之类的工具。我使用的屏幕如下,大家可以根据需要购买下。

头部屏幕是2.4寸的屏幕,屏幕驱动芯片为ST7789V2。
img

胸部的显示屏为1.47寸的小屏幕,微雪的这个屏幕模块有点贵,不过资料很全,我的屏幕驱动代码就是参考他们的资料实现的,屏幕驱动芯片为ST7789V3。
img

如果模块调试都ok了,胸有成竹了,后面直接买裸屏就可以很便宜了。
img

杜邦线如下:
img

由于两款屏幕的驱动芯片都是同一系列的,所以驱动编写起来基本上没太大的区别。

2. 软件环境准备

能够正常运行Visual Studio的电脑,和安装了.NET环境的树莓派,并且能够ssh登录和使用filezilla上传文件,上面文章有说怎么操作,这里就不展开了。

.NET IoT库实现原理解释

1. 库介介绍

适用于IoT的 .NET,使用在 Raspberry Pi、HummingBoard、BeagleBoard、Spring A64 等上运行的 C# 和 .NET 生成 IoT 应用。
利用开源库和框架与专用硬件(如传感器、模拟到数字转换器、LCD 设备)交互。

2. 整体项目结构分析

.NET IoT是开源的,点击打开开源地址,项目主要包含两部分。

第一部分System.Device.Gpio目录,主要是一些系统级别的SPI,I2C,GPIO和PWM的实现。
img

第二部分,是一些针对外设封装好的开箱即用的轮子,如果我们将屏幕抽象出来之后,也可以针对具体的芯片贡献这些代码。

img

3. 基于SPI的实现进行源码分析

在Linux系统中,有一句经典的话:“一切皆文件”(Everything is a file)。所以SPI设备也不例外,在树莓派中也是看作文件来处理。

首先我们通过下面的指令进入到树莓派配置页面。

sudo raspi-config

img
选择第三个的接口配置项里,然后启用SPI接口,这样SPI设备就算是可以使用了。
img
通过树莓派的指令 ls -l /dev/spi* 列出spi设备我们能看到以下设备,就算正常了。

img

再结合.NET IoT源码,我们会发现,其实.NET IoT针对linux上的SPI设备的通讯就是通过操作这个SPI设备文件实现的。

img

创建SPI设备的时候,是根据不同系统创建不同的实例,然后进行一些数据的读写操作。
img

驱动编写

1. 驱动和屏幕驱动芯片的关系

我们编写的屏幕驱动其实是根据不同的驱动芯片的芯片手册,进行数据的封装,比如芯片的初始化数据,芯片的复位,以及屏幕尺寸的初始化,完成了一些的初始化之后,就是开始屏幕数据的写入了,根据屏幕的特性不同,需要处理不同的图片格式,进行转换到屏幕能够显示的格式,比如色彩构成是RGB565,还是RGB888之类的,这样根据像素的RGB值排列的不同,最终的数据也就不同了,需要根据屏幕定制像素处理的代码。

2. 驱动主要是做哪些事情

主要就是简化一些调用逻辑,有了驱动,我们在使用屏幕的时候就不用关注具体的指令格式了,只需要调用Init()或者reset()方法就可以使用屏幕了。如果有人实现了一些设备的驱动,那我们作为使用者其实就可以拿来实现业务逻辑了。

3. 驱动的具体实现

我们以1.47寸的屏幕为例,首先先看屏幕的一些资料,2.4寸的微雪也有对应的资料,虽然屏幕不是微雪的,但是资料是通用的。

我用的2.4寸屏幕资料和微雪的2寸的一致

1.47寸的屏幕资料链接

本款LCD使用的内置控制器为ST7789V3,是一款240 x RGB x 320像素的LCD控制器,而本LCD本身的像素为172(H)RGB x 320(V),同时由于初始化控制可以初始化为横屏和竖屏两种,因此LCD的内部RAM并未完全使用。
该LCD支持12位,16位以及18位每像素的输入颜色格式,即RGB444,RGB565,RGB666三种颜色格式,本例程使用RGB565的颜色格式,这也是常用的RGB格式
LCD使用四线SPI通信接口,这样可以大大的节省GPIO口,同时通信是速度也会比较快
img

LcdConfig类的话实现基本的SPI的数据写入,包含一些引脚的输出的操作,用来复位屏幕等。代码有点粗糙,大家轻喷。

using System.Device.Gpio;
using System.Device.Pwm.Drivers;
using System.Device.Spi;namespace Verdure.Iot.Device;public class LcdConfig : IDisposable
{protected GpioController _gpio;protected SpiDevice _spi;protected SoftwarePwmChannel _pwmBacklight;protected int RST_PIN;protected int DC_PIN;protected int BL_PIN;protected int BL_freq;public LcdConfig(SpiDevice spi, SoftwarePwmChannel pwmBacklight, int spiFreq = 40000000, int rst = 27, int dc = 25, int bl = 18, int blFreq = 1000){_gpio = new GpioController();this._spi = spi;this.RST_PIN = rst;this.DC_PIN = dc;this.BL_PIN = bl;this.BL_freq = blFreq;_gpio.OpenPin(RST_PIN, PinMode.Output);_gpio.OpenPin(DC_PIN, PinMode.Output);_gpio.OpenPin(BL_PIN, PinMode.Output);DigitalWrite(BL_PIN, false);if (spi != null){spi.ConnectionSettings.ClockFrequency = spiFreq;spi.ConnectionSettings.Mode = SpiMode.Mode0;}_pwmBacklight = pwmBacklight;}public void DigitalWrite(int pin, bool value){_gpio.Write(pin, value ? PinValue.High : PinValue.Low);}public bool DigitalRead(int pin){return _gpio.Read(pin) == PinValue.High;}public void DelayMs(int delaytime){Thread.Sleep(delaytime);}public void SpiWriteByte(byte[] data){_spi.Write(data);}public void BlDutyCycle(double duty){_pwmBacklight.DutyCycle = duty / 100;// Implement PWM control for backlight if needed}public void BlFrequency(int freq){_pwmBacklight.Frequency = freq;// Implement frequency control for backlight if needed}public void Dispose(){Console.WriteLine("spi end");if (_spi != null){_spi.Dispose();}Console.WriteLine("gpio cleanup...");DigitalWrite(RST_PIN, true);DigitalWrite(DC_PIN, false);_gpio.ClosePin(BL_PIN);Thread.Sleep(1);_gpio?.Dispose();}
}

LCD1inch47这个就是具体的屏幕的驱动了,包含屏幕的初始化指令,和设置屏幕尺寸的指令。

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System.Device.Pwm.Drivers;
using System.Device.Spi;namespace Verdure.Iot.Device;
public class LCD1inch47 : LcdConfig
{public const int Width = 172;public const int Height = 320;public LCD1inch47(SpiDevice spi, SoftwarePwmChannel pwmBacklight, int spiFreq = 40000000, int rst = 27, int dc = 25, int bl = 18, int blFreq = 1000) : base(spi, pwmBacklight, spiFreq, rst, dc, bl, blFreq){}public void Command(byte cmd){DigitalWrite(DC_PIN, false);SpiWriteByte([cmd]);}public void Data(byte val){DigitalWrite(DC_PIN, true);SpiWriteByte([val]);}public void Reset(){DigitalWrite(RST_PIN, true);Thread.Sleep(10);DigitalWrite(RST_PIN, false);Thread.Sleep(10);DigitalWrite(RST_PIN, true);Thread.Sleep(10);}public void Init(){Command(0x36);Data(0x00);Command(0x3A);Data(0x05);Command(0xB2);Data(0x0C);Data(0x0C);Data(0x00);Data(0x33);Data(0x33);Command(0xB7);Data(0x35);Command(0xBB);Data(0x35);Command(0xC0);Data(0x2C);Command(0xC2);Data(0x01);Command(0xC3);Data(0x13);Command(0xC4);Data(0x20);Command(0xC6);Data(0x0F);Command(0xD0);Data(0xA4);Data(0xA1);Command(0xE0);Data(0xF0);Data(0xF0);Data(0x00);Data(0x04);Data(0x04);Data(0x04);Data(0x05);Data(0x29);Data(0x33);Data(0x3E);Data(0x38);Data(0x12);Data(0x12);Data(0x28);Data(0x30);Command(0xE1);Data(0xF0);Data(0x07);Data(0x0A);Data(0x0D);Data(0x0B);Data(0x07);Data(0x28);Data(0x33);Data(0x3E);Data(0x36);Data(0x14);Data(0x14);Data(0x29);Data(0x32);Command(0x21);Command(0x11);Command(0x29);}public void SetWindows(int xStart, int yStart, int xEnd, int yEnd){Command(0x2A);Data((byte)(((xStart) >> 8) & 0xff));Data((byte)((xStart + 34) & 0xff));Data((byte)((xEnd - 1 + 34) >> 8 & 0xff));Data((byte)((xEnd - 1 + 34) & 0xff));Command(0x2B);Data((byte)((yStart) >> 8 & 0xff));Data((byte)((yStart) & 0xff));Data((byte)((yEnd - 1) >> 8 & 0xff));Data((byte)((yEnd - 1) & 0xff));Command(0x2C);}public void ShowImage(Image<Bgr24> image, int xStart = 0, int yStart = 0){int imwidth = image.Width;int imheight = image.Height;var pix = new byte[imheight * imwidth * 2];for (int y = 0; y < imheight; y++){for (int x = 0; x < imwidth; x++){var color = image[x, y];pix[(y * imwidth + x) * 2] = (byte)((color.R & 0xF8) | (color.G >> 5));pix[(y * imwidth + x) * 2 + 1] = (byte)(((color.G << 3) & 0xE0) | (color.B >> 3));}}SetWindows(0, 0, Width, Height);DigitalWrite(DC_PIN, true);for (int i = 0; i < pix.Length; i += 4096){SpiWriteByte(pix.AsSpan(i, Math.Min(4096, pix.Length - i)).ToArray());}}public void ShowImageBytes(byte[] pix){SetWindows(0, 0, Width, Height);DigitalWrite(DC_PIN, true);for (int i = 0; i < pix.Length; i += 4096){SpiWriteByte(pix.AsSpan(i, Math.Min(4096, pix.Length - i)).ToArray());}}public void Clear(){var buffer = new byte[Width * Height * 2];Array.Fill(buffer, (byte)0xff);Thread.Sleep(20);SetWindows(0, 0, Width, Height);DigitalWrite(DC_PIN, true);for (int i = 0; i < buffer.Length; i += 4096){SpiWriteByte(buffer.AsSpan(i, Math.Min(4096, buffer.Length - i)).ToArray());}}
}

4. 图片处理的核心逻辑

我是采用开源的ImageSharp这个库进行的图片处理,这个库可以解析图片或者直接绘制图形之类的,是个比较火的库。

使用它将普通的Bgra32转成Bgr24然后通过驱动里的ShowImage方法,将图片转成RGB565的数据在屏幕初始化之后,直接传输到SPI就可以了,注意事项,SPI一次最多传输4096字节,所以要分段传输。

图片处理核心代码如下:

public void ShowImage(Image<Bgr24> image, int xStart = 0, int yStart = 0){int imwidth = image.Width;int imheight = image.Height;var pix = new byte[imheight * imwidth * 2];for (int y = 0; y < imheight; y++){for (int x = 0; x < imwidth; x++){var color = image[x, y];pix[(y * imwidth + x) * 2] = (byte)((color.R & 0xF8) | (color.G >> 5));pix[(y * imwidth + x) * 2 + 1] = (byte)(((color.G << 3) & 0xE0) | (color.B >> 3));}}SetWindows(0, 0, Width, Height);DigitalWrite(DC_PIN, true);for (int i = 0; i < pix.Length; i += 4096){SpiWriteByte(pix.AsSpan(i, Math.Min(4096, pix.Length - i)).ToArray());}}

下面是主程序的代码内容,主程序是针对两个屏幕循环操作。

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System.Device.Pwm.Drivers;
using System.Device.Spi;
using Verdure.Iot.Device;using var pwmBacklight = new SoftwarePwmChannel(pinNumber: 18, frequency: 1000);
pwmBacklight.Start();string input2inch4Path = "LCD_2inch4.jpg";string input1inch47Path = "LCD_1inch47.jpg";using SpiDevice sender2inch4Device = SpiDevice.Create(new SpiConnectionSettings(0, 0)
{ClockFrequency = 40000000,Mode = SpiMode.Mode0
});
using SpiDevice sender1inch47Device = SpiDevice.Create(new SpiConnectionSettings(0, 1)
{ClockFrequency = 40000000,Mode = SpiMode.Mode0
});using var inch24 = new LCD2inch4(sender2inch4Device, pwmBacklight);
inch24.Reset();
inch24.Init();
inch24.Clear();
inch24.BlDutyCycle(50);using var inch147 = new LCD1inch47(sender1inch47Device, pwmBacklight);//inch147.Reset();
inch147.Init();
inch147.Clear();
inch147.BlDutyCycle(50);while (true)
{using (Image<Bgra32> image2inch4 = Image.Load<Bgra32>("LCD_2inch.jpg")){image2inch4.Mutate(x => x.Rotate(90));using Image<Bgr24> converted2inch4Image = image2inch4.CloneAs<Bgr24>();inch24.ShowImage(converted2inch4Image);}Console.WriteLine("2inch4 Done");using (Image<Bgra32> image1inch47 = Image.Load<Bgra32>(input1inch47Path)){using Image<Bgr24> converted1inch47Image = image1inch47.CloneAs<Bgr24>();inch147.ShowImage(converted1inch47Image);}Console.WriteLine("1inch47 Done");using (Image<Bgra32> image2inch41 = Image.Load<Bgra32>(input2inch4Path)){using Image<Bgr24> converted2inch4Image1 = image2inch41.CloneAs<Bgr24>();inch24.ShowImage(converted2inch4Image1);}Console.WriteLine("2inch41 Done");using (Image<Bgra32> image1inch471 = Image.Load<Bgra32>("excited.png")){using Image<Bgr24> converted1inch47Image1 = image1inch471.CloneAs<Bgr24>();inch147.ShowImage(converted1inch47Image1);}Console.WriteLine("1inch471 Done");
}
//Console.ReadLine();

5. 同时控制两个屏幕的原理

首先我们需要将两个屏幕的除去CS的引脚进行并联,然后接到树莓派对应的引脚上,然后cs引脚分别接到树莓派的CE0,CE1引脚上,CEO对应BCM的8,CE1对应BCM的7。

2.4寸是CE0,1.47寸是CE1,大家根据代码检查接线。

这样我们通过操作不同的CS引脚选中对应的屏幕,速度足够快就像是操作两个屏幕一样。显示动画都没问题。

驱动源码地址

驱动验证

1. 硬件接线

微雪的文档里都有接线图,大家可以仔细对照。

注意事项 由于是两个屏幕,CS引脚分别接到上面说的引脚上面

2.4寸接线图如下:
img

1.47接线图如下:
img

2. 树莓派运行程序

根据上一篇文章说的发布程序的方法,将程序发布上传到树莓派执行程序。
正常情况下,就可以看到屏幕交替刷新的画面了,如果大家做到这里,就基本上算是驱动测试完成了。
img

总结感悟

写这篇文章我也翻了下之前的一些概念之类的,也算是温习了一遍,感觉也跟着刷新了一些知识,用文档记录下一些东西,对于我们查找是很方便的事情,好记性不如烂笔头。

这篇文章的篇幅有点长,希望大家能够仔细的阅读,省的遗漏了一些内容导致大家操作失败,我很希望大家能够成功,并且能够做出一些有趣的东西。如果能够帮助到大家,我还是很开心的。

参考推荐文档项目如下:

  • 创客社区地址

  • 桌面桌面机器人仓库地址

  • .NET IoT库源码地址

  • .NET IoT官方地址

  • 驱动源码地址

  • 树莓派 40Pin 引脚对照表

  • 什么是GPIO

  • 什么是串行外设接口 (SPI)?

  • wiringPi,BCM,BOARD编码关系和区分

  • 2.4寸屏幕资料和微雪的2寸的一致

  • 1.47寸的屏幕资料链接

  • .NET IoT社区人员张高兴推荐

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/869340.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

openGauss训练营第二期结营!一百个QA和PPT合辑大放送

2021年9月11-12日,由openGauss内核项目研发经理、openGauss社区Maintainer、openGauss布道师朱金伟老师领衔,联合openGauss社区、Gauss松鼠会、云和恩墨的专家们组织的第二期“8小时玩转openGauss训练营”活动通过线上直播的方式举办,获得圆满成功。本次参与学员超千人,最终…

我们一起聊聊数据库的可观测性

我们一起聊聊数据库的可观测性作者:白鳝2022-08-16 07:49:48 数据库其他数据库 云原生应用来是更为复杂和无序的,而对于数据库来说,相对来说要简单一些。因为数据库系统是按照某种客观规律组织起来的,其内在规律可以被数字化。因此也有一些运维专家认为数据库不需要搞什么可…

Ansible模块使用指南

作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任。 目录一.ansible模块概述1.ansible模块数量井喷式增长2.模块分类二.Ansible常用模块1.command模块1.1 command模块概述1.2 command模块示例2.shell模块2.1 shell模块概述2.2 shell模块示例3.script模块3.1 scr…

【Linux搭建教程】Linux 安装多个jdk版本并进行快速切换、以jdk8和jdk17为例【测试成功】

一、问题背景 由于项目需要,环境分别使用到jdk 8版本和jdk 17版本,故需要共存并配置快速切换。 二、具体实现 1、正常配置俩个jdk环境变量; 2、修改环境变量alias配置快速切换 vim /etc/profile #编辑以下内容信息alias java17=export JAVA_HOME=/usr/java/jdk-17.0…

【Redis源码】轻松看懂 rdb 文件

一、数据存储格式二、查看rdb文件 查看文件16进制编码 #od -A x -t x1c -v dump.rdbRDB文件格式如下: 0000000 52 45 44 49 53 30 30 30 38 fa 09 72 65 64 69 73R E D I S 0 0 0 8 372 \t r e d i s 0000010 2d 76 65 72…

【供应链管理系统】你了解供应链管理的五大系统(ERP、WMS、TMS、CRM和OMS)吗?

供应链管理听起来可能很复杂,但它其实是现代企业运营中不可或缺的一部分。 想象一下,从生产原材料到产品配送到客户手中,这整个过程是如何协调运作的。 每个环节需要无缝对接,确保产品的及时生产、运输和交付。 而要想高效地完成这些任务,企业就需要依赖一系列强大的系统工…

【C++安全】C++ 直接编写 Shellcode 和使用常量字符串

免责声明: 该公众号分享的安全工具和项目均来源于网络,仅供安全研究与学习之用,如用于其他用途,由使用者承担全部法律及连带责任,与工具作者和本公众号无关。一、基础知识1. PE 文件的基本结构和作用2. PE 文件加载流程3. 章节总结 二、编写 MessageBox Shellcode1. 开始2…

Easysearch Rollup 使用指南

背景 在现代数据驱动的世界中,时序数据的处理变得越来越重要。无论是监控系统、日志分析,还是物联网设备的数据收集,时序数据都占据了大量的存储空间。随着时间的推移,这些数据的存储成本和管理复杂度也在不断增加。 为了解决这一问题,Rollup 技术应运而生。本文将带你深入…

.NET 数据拷贝方案选择

应用中我们经常使用到数据的复制,在.NET中有多种方式可以实现复制数据或对象。选择哪种方式通、是浅拷贝还是深拷贝,取决于对象的复杂性、数据量以及具体需求场景。1. MemberwiseClone拷贝 浅拷贝 Object.MemberwiseClone 方法 (System) | Microsoft Learn,指针对对象执行非…

1.14 eclipse配置spring

今天完成了eclipse配置springboot eclipse本身并没有spring项目,需要在eclipse市场下载插件选择tool4安装安装完成等待eclipse加载,全部安装完成后即可创建spring项目

英语语法(标点符号:逗号和撇号)

结束句子的三种方法 认识逗号

深入浅出:Agent如何调用工具——从OpenAI Function Call到CrewAI框架

深入浅出:Agent如何调用工具——从OpenAI Function Call到CrewAI框架 嗨,大家好!作为一个喜欢折腾AI新技术的算法攻城狮,最近又学习了一些Agent工作流调用工具的文章,学完之后,我真的是“啊这”,一边感慨AI技术的强大,一边觉得自己打开了新世界的大门。于是,我决定写这…