用纯.NET开发并制作一个智能桌面机器人(二):用.NET IoT库编写驱动控制两个屏幕

目录

前言

问题解答

名词解释

1. 什么是GPIO

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

3. 什么是SPI

准备工作

1. 硬件准备

2. 软件环境准备

.NET IoT库实现原理解释

1. 库介介绍

2. 整体项目结构分析

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

驱动编写

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

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

3. 驱动的具体实现

4. 图片处理的核心逻辑

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

驱动验证

1. 硬件接线

2. 树莓派运行程序

总结感悟


 

前言

从.NET IoT入门开始这篇文章《用纯.NET开发并制作一个智能桌面机器人(一):从.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设备也不例外,在树莓派中也是看作文件来处理。

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

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的数据写入,包含一些引脚的输出的操作,用来复位屏幕等。代码有点粗糙,大家轻喷。

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寸接线图如下:

图片

1.47接线图如下:

图片

2. 树莓派运行程序

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

图片

总结感悟

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

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

引入地址 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值