互联网不景气了那就玩玩嵌入式吧,用纯.NET开辟并制作一个智能桌面机器人( ...

打印 上一主题 下一主题

主题 916|帖子 916|积分 2748

前言

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

问题解答

上一篇文章里有人问外壳模子的问题,这个我是自己设计的模子,后面我会把设计文件都开源出来,大家可以通过自己的3D打印机打印,也可以去一些在线平台下单打印都可以操纵,这个不消担心。
关于电路板,这个桌面机器人我为了简化线路,绘制了一个ups板子,外加把显示屏的线路也整合到一起了,但是这篇文章还用不到这个电路板子,我们可以通过屏幕模块和杜邦线之类的进行验证测试。

上篇文章还有人推荐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进行接线和代码编写。

3. 什么是SPI

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

1. 硬件预备

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

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

假如模块调试都ok了,胸中有数了,后面直接买裸屏就可以很自制了。

杜邦线如下:

由于两款屏幕的驱动芯片都是同一系列的,以是驱动编写起来基本上没太大的区别。
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的实现。

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

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

在Linux体系中,有一句经典的话:“一切皆文件”(Everything is a file)。以是SPI设备也不破例,在树莓派中也是看作文件来处置惩罚。
首先我们通过下面的指令进入到树莓派配置页面。
  1. sudo raspi-config
复制代码

选择第三个的接口配置项里,然后启用SPI接口,这样SPI设备就算是可以使用了。

通过树莓派的指令 ls -l /dev/spi* 列出spi设备我们能看到以下设备,就算正常了。

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

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

驱动编写

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口,同时通讯是速度也会比较快

LcdConfig类的话实现基本的SPI的数据写入,包含一些引脚的输出的操纵,用来复位屏幕等。代码有点粗糙,大家轻喷。
  1. using System.Device.Gpio;
  2. using System.Device.Pwm.Drivers;
  3. using System.Device.Spi;
  4. namespace Verdure.Iot.Device;
  5. public class LcdConfig : IDisposable
  6. {
  7.     protected GpioController _gpio;
  8.     protected SpiDevice _spi;
  9.     protected SoftwarePwmChannel _pwmBacklight;
  10.     protected int RST_PIN;
  11.     protected int DC_PIN;
  12.     protected int BL_PIN;
  13.     protected int BL_freq;
  14.     public LcdConfig(SpiDevice spi, SoftwarePwmChannel pwmBacklight, int spiFreq = 40000000, int rst = 27, int dc = 25, int bl = 18, int blFreq = 1000)
  15.     {
  16.         _gpio = new GpioController();
  17.         this._spi = spi;
  18.         this.RST_PIN = rst;
  19.         this.DC_PIN = dc;
  20.         this.BL_PIN = bl;
  21.         this.BL_freq = blFreq;
  22.         _gpio.OpenPin(RST_PIN, PinMode.Output);
  23.         _gpio.OpenPin(DC_PIN, PinMode.Output);
  24.         _gpio.OpenPin(BL_PIN, PinMode.Output);
  25.         DigitalWrite(BL_PIN, false);
  26.         if (spi != null)
  27.         {
  28.             spi.ConnectionSettings.ClockFrequency = spiFreq;
  29.             spi.ConnectionSettings.Mode = SpiMode.Mode0;
  30.         }
  31.         _pwmBacklight = pwmBacklight;
  32.     }
  33.     public void DigitalWrite(int pin, bool value)
  34.     {
  35.         _gpio.Write(pin, value ? PinValue.High : PinValue.Low);
  36.     }
  37.     public bool DigitalRead(int pin)
  38.     {
  39.         return _gpio.Read(pin) == PinValue.High;
  40.     }
  41.     public void DelayMs(int delaytime)
  42.     {
  43.         Thread.Sleep(delaytime);
  44.     }
  45.     public void SpiWriteByte(byte[] data)
  46.     {
  47.         _spi.Write(data);
  48.     }
  49.     public void BlDutyCycle(double duty)
  50.     {
  51.         _pwmBacklight.DutyCycle = duty / 100;
  52.         // Implement PWM control for backlight if needed
  53.     }
  54.     public void BlFrequency(int freq)
  55.     {
  56.         _pwmBacklight.Frequency = freq;
  57.         // Implement frequency control for backlight if needed
  58.     }
  59.     public void Dispose()
  60.     {
  61.         Console.WriteLine("spi end");
  62.         if (_spi != null)
  63.         {
  64.             _spi.Dispose();
  65.         }
  66.         Console.WriteLine("gpio cleanup...");
  67.         DigitalWrite(RST_PIN, true);
  68.         DigitalWrite(DC_PIN, false);
  69.         _gpio.ClosePin(BL_PIN);
  70.         Thread.Sleep(1);
  71.         _gpio?.Dispose();
  72.     }
  73. }
复制代码
LCD1inch47这个就是具体的屏幕的驱动了,包含屏幕的初始化指令,和设置屏幕尺寸的指令。
  1. using SixLabors.ImageSharp;
  2. using SixLabors.ImageSharp.PixelFormats;
  3. using System.Device.Pwm.Drivers;
  4. using System.Device.Spi;
  5. namespace Verdure.Iot.Device;
  6. public class LCD1inch47 : LcdConfig
  7. {
  8.     public const int Width = 172;
  9.     public const int Height = 320;
  10.     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)
  11.     {
  12.     }
  13.     public void Command(byte cmd)
  14.     {
  15.         DigitalWrite(DC_PIN, false);
  16.         SpiWriteByte([cmd]);
  17.     }
  18.     public void Data(byte val)
  19.     {
  20.         DigitalWrite(DC_PIN, true);
  21.         SpiWriteByte([val]);
  22.     }
  23.     public void Reset()
  24.     {
  25.         DigitalWrite(RST_PIN, true);
  26.         Thread.Sleep(10);
  27.         DigitalWrite(RST_PIN, false);
  28.         Thread.Sleep(10);
  29.         DigitalWrite(RST_PIN, true);
  30.         Thread.Sleep(10);
  31.     }
  32.     public void Init()
  33.     {
  34.         Command(0x36);
  35.         Data(0x00);
  36.         Command(0x3A);
  37.         Data(0x05);
  38.         Command(0xB2);
  39.         Data(0x0C);
  40.         Data(0x0C);
  41.         Data(0x00);
  42.         Data(0x33);
  43.         Data(0x33);
  44.         Command(0xB7);
  45.         Data(0x35);
  46.         Command(0xBB);
  47.         Data(0x35);
  48.         Command(0xC0);
  49.         Data(0x2C);
  50.         Command(0xC2);
  51.         Data(0x01);
  52.         Command(0xC3);
  53.         Data(0x13);
  54.         Command(0xC4);
  55.         Data(0x20);
  56.         Command(0xC6);
  57.         Data(0x0F);
  58.         Command(0xD0);
  59.         Data(0xA4);
  60.         Data(0xA1);
  61.         Command(0xE0);
  62.         Data(0xF0);
  63.         Data(0xF0);
  64.         Data(0x00);
  65.         Data(0x04);
  66.         Data(0x04);
  67.         Data(0x04);
  68.         Data(0x05);
  69.         Data(0x29);
  70.         Data(0x33);
  71.         Data(0x3E);
  72.         Data(0x38);
  73.         Data(0x12);
  74.         Data(0x12);
  75.         Data(0x28);
  76.         Data(0x30);
  77.         Command(0xE1);
  78.         Data(0xF0);
  79.         Data(0x07);
  80.         Data(0x0A);
  81.         Data(0x0D);
  82.         Data(0x0B);
  83.         Data(0x07);
  84.         Data(0x28);
  85.         Data(0x33);
  86.         Data(0x3E);
  87.         Data(0x36);
  88.         Data(0x14);
  89.         Data(0x14);
  90.         Data(0x29);
  91.         Data(0x32);
  92.         Command(0x21);
  93.         Command(0x11);
  94.         Command(0x29);
  95.     }
  96.     public void SetWindows(int xStart, int yStart, int xEnd, int yEnd)
  97.     {
  98.         Command(0x2A);
  99.         Data((byte)(((xStart) >> 8) & 0xff));
  100.         Data((byte)((xStart + 34) & 0xff));
  101.         Data((byte)((xEnd - 1 + 34) >> 8 & 0xff));
  102.         Data((byte)((xEnd - 1 + 34) & 0xff));
  103.         Command(0x2B);
  104.         Data((byte)((yStart) >> 8 & 0xff));
  105.         Data((byte)((yStart) & 0xff));
  106.         Data((byte)((yEnd - 1) >> 8 & 0xff));
  107.         Data((byte)((yEnd - 1) & 0xff));
  108.         Command(0x2C);
  109.     }
  110.     public void ShowImage(Image<Bgr24> image, int xStart = 0, int yStart = 0)
  111.     {
  112.         int imwidth = image.Width;
  113.         int imheight = image.Height;
  114.         var pix = new byte[imheight * imwidth * 2];
  115.         for (int y = 0; y < imheight; y++)
  116.         {
  117.             for (int x = 0; x < imwidth; x++)
  118.             {
  119.                 var color = image[x, y];
  120.                 pix[(y * imwidth + x) * 2] = (byte)((color.R & 0xF8) | (color.G >> 5));
  121.                 pix[(y * imwidth + x) * 2 + 1] = (byte)(((color.G << 3) & 0xE0) | (color.B >> 3));
  122.             }
  123.         }
  124.         SetWindows(0, 0, Width, Height);
  125.         DigitalWrite(DC_PIN, true);
  126.         for (int i = 0; i < pix.Length; i += 4096)
  127.         {
  128.             SpiWriteByte(pix.AsSpan(i, Math.Min(4096, pix.Length - i)).ToArray());
  129.         }
  130.     }
  131.     public void ShowImageBytes(byte[] pix)
  132.     {
  133.         SetWindows(0, 0, Width, Height);
  134.         DigitalWrite(DC_PIN, true);
  135.         for (int i = 0; i < pix.Length; i += 4096)
  136.         {
  137.             SpiWriteByte(pix.AsSpan(i, Math.Min(4096, pix.Length - i)).ToArray());
  138.         }
  139.     }
  140.     public void Clear()
  141.     {
  142.         var buffer = new byte[Width * Height * 2];
  143.         Array.Fill(buffer, (byte)0xff);
  144.         Thread.Sleep(20);
  145.         SetWindows(0, 0, Width, Height);
  146.         DigitalWrite(DC_PIN, true);
  147.         for (int i = 0; i < buffer.Length; i += 4096)
  148.         {
  149.             SpiWriteByte(buffer.AsSpan(i, Math.Min(4096, buffer.Length - i)).ToArray());
  150.         }
  151.     }
  152. }
复制代码
4. 图片处置惩罚的核心逻辑

我是接纳开源的ImageSharp这个库进行的图片处置惩罚,这个库可以解析图片或者直接绘制图形之类的,是个比较火的库。
使用它将普通的Bgra32转成Bgr24然后通过驱动里的ShowImage方法,将图片转成RGB565的数据在屏幕初始化之后,直接传输到SPI就可以了,留意事项,SPI一次最多传输4096字节,以是要分段传输。
图片处置惩罚核心代码如下:
  1. public void ShowImage(Image<Bgr24> image, int xStart = 0, int yStart = 0)
  2.     {
  3.         int imwidth = image.Width;
  4.         int imheight = image.Height;
  5.         var pix = new byte[imheight * imwidth * 2];
  6.         for (int y = 0; y < imheight; y++)
  7.         {
  8.             for (int x = 0; x < imwidth; x++)
  9.             {
  10.                 var color = image[x, y];
  11.                 pix[(y * imwidth + x) * 2] = (byte)((color.R & 0xF8) | (color.G >> 5));
  12.                 pix[(y * imwidth + x) * 2 + 1] = (byte)(((color.G << 3) & 0xE0) | (color.B >> 3));
  13.             }
  14.         }
  15.         SetWindows(0, 0, Width, Height);
  16.         DigitalWrite(DC_PIN, true);
  17.         for (int i = 0; i < pix.Length; i += 4096)
  18.         {
  19.             SpiWriteByte(pix.AsSpan(i, Math.Min(4096, pix.Length - i)).ToArray());
  20.         }
  21.     }
复制代码
下面是主步伐的代码内容,主步伐是针对两个屏幕循环操纵。
  1. using SixLabors.ImageSharp;
  2. using SixLabors.ImageSharp.PixelFormats;
  3. using SixLabors.ImageSharp.Processing;
  4. using System.Device.Pwm.Drivers;
  5. using System.Device.Spi;
  6. using Verdure.Iot.Device;
  7. using var pwmBacklight = new SoftwarePwmChannel(pinNumber: 18, frequency: 1000);
  8. pwmBacklight.Start();
  9. string input2inch4Path = "LCD_2inch4.jpg";
  10. string input1inch47Path = "LCD_1inch47.jpg";
  11. using SpiDevice sender2inch4Device = SpiDevice.Create(new SpiConnectionSettings(0, 0)
  12. {
  13.     ClockFrequency = 40000000,
  14.     Mode = SpiMode.Mode0
  15. });
  16. using SpiDevice sender1inch47Device = SpiDevice.Create(new SpiConnectionSettings(0, 1)
  17. {
  18.     ClockFrequency = 40000000,
  19.     Mode = SpiMode.Mode0
  20. });
  21. using var inch24 = new LCD2inch4(sender2inch4Device, pwmBacklight);
  22. inch24.Reset();
  23. inch24.Init();
  24. inch24.Clear();
  25. inch24.BlDutyCycle(50);
  26. using var inch147 = new LCD1inch47(sender1inch47Device, pwmBacklight);
  27. //inch147.Reset();
  28. inch147.Init();
  29. inch147.Clear();
  30. inch147.BlDutyCycle(50);
  31. while (true)
  32. {
  33.     using (Image<Bgra32> image2inch4 = Image.Load<Bgra32>("LCD_2inch.jpg"))
  34.     {
  35.         image2inch4.Mutate(x => x.Rotate(90));
  36.         using Image<Bgr24> converted2inch4Image = image2inch4.CloneAs<Bgr24>();
  37.         inch24.ShowImage(converted2inch4Image);
  38.     }
  39.     Console.WriteLine("2inch4 Done");
  40.     using (Image<Bgra32> image1inch47 = Image.Load<Bgra32>(input1inch47Path))
  41.     {
  42.         using Image<Bgr24> converted1inch47Image = image1inch47.CloneAs<Bgr24>();
  43.         inch147.ShowImage(converted1inch47Image);
  44.     }
  45.     Console.WriteLine("1inch47 Done");
  46.     using (Image<Bgra32> image2inch41 = Image.Load<Bgra32>(input2inch4Path))
  47.     {
  48.         using Image<Bgr24> converted2inch4Image1 = image2inch41.CloneAs<Bgr24>();
  49.         inch24.ShowImage(converted2inch4Image1);
  50.     }
  51.     Console.WriteLine("2inch41 Done");
  52.     using (Image<Bgra32> image1inch471 = Image.Load<Bgra32>("excited.png"))
  53.     {
  54.         using Image<Bgr24> converted1inch47Image1 = image1inch471.CloneAs<Bgr24>();
  55.         inch147.ShowImage(converted1inch47Image1);
  56.     }
  57.     Console.WriteLine("1inch471 Done");
  58. }
  59. //Console.ReadLine();
复制代码
5. 同时控制两个屏幕的原理

首先我们需要将两个屏幕的除去CS的引脚进行并联,然后接到树莓派对应的引脚上,然后cs引脚分别接到树莓派的CE0,CE1引脚上,CEO对应BCM的8,CE1对应BCM的7。
2.4寸是CE0,1.47寸是CE1,大家根据代码检查接线。
这样我们通过操纵不同的CS引脚选中对应的屏幕,速度足够快就像是操纵两个屏幕一样。显示动画都没问题。
驱动源码地址
驱动验证

1. 硬件接线

微雪的文档里都有接线图,大家可以细致对照。
留意事项 由于是两个屏幕,CS引脚分别接到上面说的引脚上面
2.4寸接线图如下:

1.47接线图如下:

2. 树莓派运行步伐

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

总结感悟

写这篇文章我也翻了下之前的一些概念之类的,也算是温习了一遍,感觉也跟着刷新了一些知识,用文档记载下一些东西,对于我们查找是很方便的事情,好记性不如烂笔头。
这篇文章的篇幅有点长,渴望大家能够细致的阅读,省的遗漏了一些内容导致大家操纵失败,我很渴望大家能够成功,并且能够做出一些有趣的东西。假如能够帮助到大家,我照旧很开心的。
参考推荐文档项目如下:


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

道家人

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表