在树莓派Zero上使用C#+Mono驱动TM1637四位数码管

最近闲着无聊,买了个树莓派Zero,准备在上面跑.Net Core,来驱动各种传感器

   就是上面这货。之前手上已经有一个树莓派3B+,但是介于3B+已经被我挂在路由器旁边当做服务器用,不是很方便拿来研究接口,于是就挑了一个便宜的Zero玩玩,事实证明,我想太天真了,我以为只要是Linux系统,就能安装.net Core,实际上呢,我整了一个晚上才不得不认识到一个事实:即便是.net Core也是认CPU架构的,Pi Zero用的ARMv6就是不支持,哎早知道在买之前多做做功课了,买一个树莓派4也是个不错的选择啊。

幸好苍天不负有心人,我找到了 另外一个能在Linux上面运行.net的途径,那就是在Linux上面安装一个Mono,然后.net通过Mono当做虚拟机运行,其实在原理上和.net core是差不多的,可是Mono在性能上比原生的.net core差了很多便是,不过我们只是用来跑外部模块,也不是很需要多高性能便是了。

好了,唠嗑正式结束,让我们开始正题吧

首先,我们需要在Linux上面配置Mono的程序,讲人话就是安装Mono,不过在安装之前,我们还需要更改源,毕竟树莓派自带的源别指望在国内有好的下载体验

sudo sed -i 's#://raspbian.raspberrypi.org#s://mirrors.tuna.tsinghua.edu.cn/raspbian#g' /etc/apt/sources.list
sudo sed -i 's#://archive.raspberrypi.org/debian#s://mirrors.tuna.tsinghua.edu.cn/raspberrypi#g' /etc/apt/sources.list.d/raspi.list

运行上述两条指令,把树莓派自带的源替换成清华源,这样安装Mono会快很多

sudo apt-get install mono-devel mono-complete mono-dbg

运行上面指令后,在树莓派Zero上就会自动安装配置完毕Mono环境了。

对了,为了方便调试,我们还需要配置SSH的远程root连接

sudo nano /etc/ssh/sshd_config

运行上述指令后

 找到这一条,然后改成上图这样子后(其实也就去掉#,后面的参数改成yes罢了)

完事以后,按Ctrl+X,退出编辑并覆盖保存就行。

sudo service ssh --full-restart

最后我们运行上述指令重启SSH服务以后就能够以root权限登录树莓派了。

以上是树莓派的系统的配置过程。

接下来我们需要配置Visual Studio

首先我们新建一个项目,由于最新的Mono支持.net core,所以我们直接建立.net core 3框架的项目就行,而且甚至不需要拖家带口带上.net core那么多运行库就能直接在Mono虚拟机下跑,简直了...

然后,我们需要有一个扩展能够直接在PC上远程调试树莓派上的程序,因此

搜索Mono的调试插件,有很多个,功能都差不多,挑一个顺手的就行

安装好Mono调试插件以后

 

需要配置下Mono调试插件的设置

 其实主要的无非就是这么几个,新建一个配置,输入IP、端口、用户名和密码,避免麻烦最好直接上root权限,反正自己用

然后每次调试的时候,点击通过SSH生成和调试

 

 就能获得和本地调试一样的体验,不得不说,这个体验实在是太好了。

接下来是项目的

其实也就一点,在Nuget上面找一个第三方的库来调用GPIO接口就行,没别的了

Nuget下搜索Raspberry,下面的库基本上都是关于调用树莓派gpio的,随便挑一个便是

我这边选择了文档最为齐全的Unosquare.Raspberry.IO

 下面两个是依赖项,尤其是WiringPi,是直接管理接口的主要库

好,以上是准备工作,下面的是具体实现

 上面这张图,对应的就是树莓派Zero上,一共40个针脚的定义,其中,两个5V的接口可以直接当做电源输入或者输出用,GND是接地这个没啥好说的,我们主要看GPIO,这里有很多很多GPIO接口,这些接口才是负责信号输入以及输出使用,我们控制的主要也是这些接口。

 然后我们这次的主角也上场了

 

 注意看接线的颜色,其中CLK和DIO代表时钟信号和数据信号,虽说是时钟信号,其实是类似于发送命令的接口,因此都接GPIO,VCC是电源,这个没啥好说的,就是输入电源(注意看传感器的电压,如果电压过高会烧毁传感器,所以树莓派预留了两个3.3V的电压接口),GND是接地,随便找个接地的接口插上去就行。

 根据照片所示,我使用了4,14,16,18号接口,其中16口接了时钟信号,18口接了数据信号

好了,线也接好了,环境也配置好了,我们正式开始编程阶段

Pi.Init<BootstrapWiringPi>();//初始化通信接口,分配内存空间等
var clkPin = Pi.Gpio[BcmPin.Gpio23];//引用16接口
var dataPin = Pi.Gpio[BcmPin.Gpio24];//引用18接口
clkPin.PinMode = GpioPinDriveMode.Output;//设置16接口模式为输出
dataPin.PinMode = GpioPinDriveMode.Output;//设置18接口模式为输出

上面代码是初始化阶段,反正刚开头照这个姿势填就行了,值得注意的是接口引用部分

你看,我明明CLK接口插的是树莓派16号物理接口,为啥这里引用的却是GPIO23呢,其实这个是编码方式的不同导致的,主要有以下两种

  • BCM

编号侧重CPU寄存器,根据BCM2835的GPIO寄存器编号。

  • wiringPi

编号侧重实现逻辑,把扩展GPIO端口从0开始编号,这种编号方便编程。

 具体使用哪一种,需要看调用库用的哪一套,因为我用的这个库使用的是Bcm(引用的时候已经写明了BcmPin)所以查表得知,16接口对应的GPIO23,18接口对应的GPIO24

接口配置完毕以后我们就可以正式开始驱动四位数码管了

驱动数码管实际上是操控TM1637芯片,我们的操作规程需要满足TM1637芯片的特性,

其中最主要的特性是

//数据输入开始
void startDisp()
{
clkPin.Write(GpioPinValue.High);//CLK拉为高电平 dataPin.Write(GpioPinValue.High);//DIO拉为高电平 dataPin.Write(GpioPinValue.Low);//和上面那句指令一起就是DIO由高变低 clkPin.Write(GpioPinValue.Low);//然后CLK上的时钟信号拉低,DIO接口的数据允许改变,代表开始写入数据
}

上面这四句执行完后,就表示告诉TM1637芯片,我要开始写数据了,后面DIO接口的任何电位变化,都是我要写的数据,下面就是写数据的过程

//开始写入数据
void writeByte(byte input)
{ for (int i = 0; i < 8; i++)//每次写入一个byte,一共8bit {   clkPin.Write(GpioPinValue.Low);//确保无误输入前再拉低一次时钟 ,代表开始写入数据   if ((input & 0x01) == 1)//判断每一位的高低电平   {     dataPin.Write(GpioPinValue.High);   }   else   {     dataPin.Write(GpioPinValue.Low);   }   input >>= 1;//每写入完毕一次,移动一位   clkPin.Write(GpioPinValue.High);//每次输入完一位,就拉高一次时钟 } //应答信号ACK,这里本来是用来判断DIO脚是否被自动拉低,代表上面写入的数据TM1637已经接受到了
//但是我这里闲麻烦,直接将CLK信号低高低的拉,让芯片直接执行下一步操作
clkPin.Write(GpioPinValue.Low);//先拉低 clkPin.Write(GpioPinValue.High);//需要判断D是否为低电平此期间C一直拉高 clkPin.Write(GpioPinValue.Low);//应答完毕以后拉低C
}

 上面执行完以后,我们就已经向芯片发送了一个字节,也就是8位的数据,写完以后,我们还需要告知芯片,数据传输完毕了

//结束条件是CLK为高时,DIO由低电平变为高电平
void stopDisp()
{
clkPin.Write(GpioPinValue.Low);//先拉低CLK,代表允许DIO改变数据 dataPin.Write(GpioPinValue.Low);//拉低DIO clkPin.Write(GpioPinValue.High);//CLK拉高,满足结束条件前半部分 dataPin.Write(GpioPinValue.High);//DIO由低变高,代表数据输入结束
}

好了,上面三段代码一起执行完毕,就表示一个完整的,信号从准备输入,开始输入,结束输入的过程,每次要输入1字节,都需要经过以上三个过程,因此我们将上面三个过程分别写成各自的方法,毕竟是需要经常调用的东西,而且基本上都不会变。以上的代码实现,都是基于TM1637规格书,就是上面的接口说明实现的。

下面开始介绍该怎么在数码管上显示出东西来

在开始之前,我们先仔细看看数码管是怎么一个样子的

  //      A
  //     ---
  //  F |   | B
  //     -G-
  //  E |   | C
  //     ---
  //      D
   XGFEDCBA
  00111111,    // 0 //0x3f
  00000110,    // 1 //0x06
  01011011,    // 2 //0x5b
  01001111,    // 3 //0x4f
  01100110,    // 4 //0x66
  01101101,    // 5 //0x6d
  01111101,    // 6 //0x7d
  00000111,    // 7 //0x07
  01111111,    // 8 //0x7f
  01101111,    // 9 //0x6f
  01110111,    // A //0x77
  01111100,    // b //0x7C
  00111001,    // C //0X39
  01011110,    // d //0X5E
  01111001,    // E //0X79
  01110001     // F //0X71

上图展示了数码管,一个字样的显示方式,跟我们写汉字一样,一共准备了7个笔画,我们想让哪个笔画亮起来,就让那个笔画的电平拉高就行,总的来说还是挺直观的,因为我们只有7个笔画,但是一个比特有8位,所有还有一位空置为低电平,如果有其他用处的话,可以补上()。于是我们把这些二进制,通过计算器换算成16进制的话就变成了0x3F样式的字节码

static byte[] Characters = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};//0~9,A,b,C,d,E,F            

当然,也可以自己任意根据上面的描述,编写想要的走线图案,不一定非要按照0到9的数字或者字母定式来写。 

//设置基本参数 
startDisp();//开始写入指令
writeByte(0x40);//指定功能参数
stopDisp();//结束写入指令

//设置显示地址以及显示内容
startDisp();
writeByte(0xC0);//设置首地址,指向第一个字符
var Date = DateTime.Now.ToString("hhmm").ToCharArray();//获得当前日期,并表示为小时分钟
byte[] Characters = { 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71 };//0~9,A,b,C,d,E,F            
for (int i = 0; i <4; i++)//循环更改四个字符的显示,想更改数码管的显示,只要更改循环体内的操作就行
{
  if (i != 1) writeByte(Characters[Date[i] - 48]);//从Characters数组根据索引获得字符显示的编码
  else writeByte((byte)(Characters[Date[1] - 48] + 0x80));//第二个字符带有冒号,因此将第一位空置拉高
}
stopDisp();

//开始写入亮度
startDisp();
writeByte(0x8f);
stopDisp(); 

 以上代码便是驱动数码管显示的完整代码,循环运行上述代码就能不断驱动数码管显示当前的时间,同时更改循环体内的writeByte()方法参数,就能实现不同字符的显示。startDisp();stopDisp(); writeByte();方法体,都在上面有完全的展示。

除去上述三个方法,

writeByte(0x40);//指定功能参数
writeByte(0xC0);//设置首地址,指向第一个字符
writeByte((byte)(Characters[Date[1] - 48] + 0x80));//显示 :符号,这三个参数需要单独讲一下。
writeByte(0x8f);//指定亮度

首先,上述代码的先后顺序不能变,一定是先指定功能参数,后指定显示位置,然后指定显示内容,最后指定显示亮度

而功能参数0x40写入进去有啥用呢

我们查阅TM1637的规格书可知

 0x40翻译成二进制便是

 0 | 1  | 0 | 0 |  0 | 0 | 0 |  0
B7|B6|B5|B4|B3|B2|B1|B0

根据上述表格我们可以知道01000000(0X40)所代表的的意思就是

1:数据写到显示寄存器,也就是功能是显示

2:地址的增加模式是自+1

3:测试模式为普通模式

在此介绍一下前两种的区别

第一条的意思就是,这个芯片是支持按键响应和屏幕输出的,也就是说,如果B1置1则芯片功能是读取按钮 (虽然数位管上并没有任何按键),B1置0就是显示输出模式

第二条的意思就是,

for (int i = 0; i <4; i++)//循环更改四个字符的显示,想更改数码管的显示,只要更改循环体内的操作就行
{
  writeByte(Characters[Date[i] - 48]);//从Characters数组根据索引获得字符显示的编码
}

如果B2置0,功能为自动地址增加模式,那么循环体内每次循环写入一个字符以后,下一次循环光标位置就会移到下一个字符的位置,就和我们打字类似

那么如果B2置1,功能为固定地址模式的话,顾名思义,就是哪个位置显示什么字符串由我们决定。

那么显示代码就变成了

  startDisp();
  writeByte(0xC0);//第一个字符
  writeByte(Characters[Date[0] - 48]);
  stopDisp();

  startDisp();
  writeByte(0xC1);//第二个字符
  writeByte(Characters[Date[1] - 48]);
  stopDisp();

  startDisp();
  writeByte(0xC2);//第三个字符
  writeByte(Characters[Date[2] - 48]);
  stopDisp();

  startDisp();
  writeByte(0xC3);//第四个字符
  writeByte(Characters[Date[3] - 48]);
  stopDisp();

那么这个0xC0,0xC1,0xC2,0xC3哪里来的呢,同样查阅规格书可知

 就是11000000,11000001,11000010,11000011,上述几个二进制转换成16进制便是C0,C1,C2,C3,当然,该芯片最多可支持显示6个字符

  对了,中间这个  : 的符号,并不占用一个字符显示,这个符号归类到0xC1地址内,被当成了一个标点使用

   XGFEDCBA
  00111111,    // 0 //0x3f

还记得上面那张图吧,A~G,分别表示7个笔画,但是多了一位闲置的在这里就派上用场了,只要把0xC1,也就是第二个字符的位置最高位置1,变成10111111,那么这个符号变会显示出来

writeByte((byte)(Characters[Date[1] - 48] + 0x80));

代码上就是在原先的基础上加上0x80就可以了。

上面的步奏全部完成以后,其实只是把需要显示的数据存到芯片里面而已,芯片还没有输出任何数据给数码管,因为我们还什么都看不到

所以我们还需要再输入一次命令

writeByte(0x8f);//指定亮度并显示

那这个 0x8f又是哪里来的呢

这里同样有一张表格,B3表示开关,B0~B2表示脉冲宽带pwm,脉冲宽度越长,代表输出给数码管的时间越长,也就越亮,参照之前的方法,把对应的8位二进制转换成16进制填入进去就行,我想都看到这里了,应该没啥疑问的。

好,上述就是完整的教程,下面贴完整代码

        private static void Main(string[] args)
        {
            Pi.Init<BootstrapWiringPi>();//初始化通信接口,分配内存空间等
            var clkPin = Pi.Gpio[BcmPin.Gpio23];//引用16接口
            var dataPin = Pi.Gpio[BcmPin.Gpio24];//引用18接口
            clkPin.PinMode = GpioPinDriveMode.Output;//设置16接口模式为输出
            dataPin.PinMode = GpioPinDriveMode.Output;//设置18接口模式为输出
            clkPin.Write(GpioPinValue.Low);//初始化电平为低,可不加
            dataPin.Write(GpioPinValue.Low);//初始化电平为低,可不加

            void startDisp()
            {
                //数据输入开始
                clkPin.Write(GpioPinValue.High);//CLK拉为高电平
                dataPin.Write(GpioPinValue.High);//DIO拉为高电平
                dataPin.Write(GpioPinValue.Low);//和上面那句指令一起就是DIO由高变低
                clkPin.Write(GpioPinValue.Low);//然后CLK上的时钟信号拉低,DIO接口的数据允许改变,代表开始写入数据
            }
            void stopDisp()
            {
                //结束条件是CLK为高时,DIO由低电平变为高电平
                clkPin.Write(GpioPinValue.Low);//先拉低CLK,代表允许DIO改变数据
                dataPin.Write(GpioPinValue.Low);//拉低DIO
                clkPin.Write(GpioPinValue.High);//CLK拉高,满足结束条件前半部分
                dataPin.Write(GpioPinValue.High);//DIO由低变高,代表数据输入结束
            }
            void writeByte(byte input)
            {
                //开始写入数据
                for (int i = 0; i < 8; i++)//每次写入一个byte,一共8bit
                {
                    clkPin.Write(GpioPinValue.Low);//确保无误输入前再拉低一次时钟 ,代表开始写入数据
                    if ((input & 0x01) == 1)//判断每一位的高低电平
                    {
                        dataPin.Write(GpioPinValue.High);
                    }
                    else
                    {
                        dataPin.Write(GpioPinValue.Low);
                    }
                    input >>= 1;//每写入完毕一次,移动一位
                    clkPin.Write(GpioPinValue.High);//每次输入完一位,就拉高一次时钟
                }
                //应答信号ACK,这里本来是用来判断DIO脚是否被自动拉低,代表上面写入的数据TM1637已经接受到了,
                //但是我们这里闲麻烦,直接将CLK信号低高低的拉,让芯片直接执行下一步操作
                clkPin.Write(GpioPinValue.Low);//先拉低
                clkPin.Write(GpioPinValue.High);//需要判断D是否为低电平此期间C一直拉高
                clkPin.Write(GpioPinValue.Low);//应答完毕以后拉低C
            }
            void Show()
            {
                //设置基本参数
                startDisp();//开始写入指令
                writeByte(0x40);//指定功能参数为自动增加
                stopDisp();//结束写入指令

                //设置显示地址以及显示内容
                startDisp();
                writeByte(0xC0);//设置首地址,指向第一个字符
                var Date = DateTime.Now.ToString("hhmm").ToCharArray();//获得当前日期,并表示为小时分钟
                byte[] Characters = { 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71 };//0~9,A,b,C,d,E,F
                for (int i = 0; i < Date.Length; i++)
                {
                    if (i != 1) writeByte(Characters[Date[i] - 48]);//从Characters数组根据索引获得字符显示的编码
                    else writeByte((byte)(Characters[Date[1] - 48] + 0x80));//第二个字符带有冒号,因此将第一位空置拉高
                }

                //开始写入亮度
                startDisp();
                writeByte(0x8f);
                stopDisp();
            }
            while (true)
            {
                Show();
            } 
        }

以上是字符地址自增加的代码

        private static void Main(string[] args)
        {
            Pi.Init<BootstrapWiringPi>();//初始化通信接口,分配内存空间等
            var clkPin = Pi.Gpio[BcmPin.Gpio23];//引用16接口
            var dataPin = Pi.Gpio[BcmPin.Gpio24];//引用18接口
            clkPin.PinMode = GpioPinDriveMode.Output;//设置16接口模式为输出
            dataPin.PinMode = GpioPinDriveMode.Output;//设置18接口模式为输出
            clkPin.Write(GpioPinValue.Low);//初始化电平为低,可不加
            dataPin.Write(GpioPinValue.Low);//初始化电平为低,可不加

            void startDisp()
            {
                //数据输入开始
                clkPin.Write(GpioPinValue.High);//CLK拉为高电平
                dataPin.Write(GpioPinValue.High);//DIO拉为高电平
                dataPin.Write(GpioPinValue.Low);//和上面那句指令一起就是DIO由高变低
                clkPin.Write(GpioPinValue.Low);//然后CLK上的时钟信号拉低,DIO接口的数据允许改变,代表开始写入数据
            }
            void stopDisp()
            {
                //结束条件是CLK为高时,DIO由低电平变为高电平
                clkPin.Write(GpioPinValue.Low);//先拉低CLK,代表允许DIO改变数据
                dataPin.Write(GpioPinValue.Low);//拉低DIO
                clkPin.Write(GpioPinValue.High);//CLK拉高,满足结束条件前半部分
                dataPin.Write(GpioPinValue.High);//DIO由低变高,代表数据输入结束
            }
            void writeByte(byte input)
            {
                //开始写入数据
                for (int i = 0; i < 8; i++)//每次写入一个byte,一共8bit
                {
                    clkPin.Write(GpioPinValue.Low);//确保无误输入前再拉低一次时钟 ,代表开始写入数据
                    if ((input & 0x01) == 1)//判断每一位的高低电平
                    {
                        dataPin.Write(GpioPinValue.High);
                    }
                    else
                    {
                        dataPin.Write(GpioPinValue.Low);
                    }
                    input >>= 1;//每写入完毕一次,移动一位
                    clkPin.Write(GpioPinValue.High);//每次输入完一位,就拉高一次时钟
                }
                //应答信号ACK,这里本来是用来判断DIO脚是否被自动拉低,代表上面写入的数据TM1637已经接受到了,
                //但是我们这里闲麻烦,直接将CLK信号低高低的拉,让芯片直接执行下一步操作
                clkPin.Write(GpioPinValue.Low);//先拉低
                clkPin.Write(GpioPinValue.High);//需要判断D是否为低电平此期间C一直拉高
                clkPin.Write(GpioPinValue.Low);//应答完毕以后拉低C
            }
            void Show()
            {
                //设置基本参数
                startDisp();//开始写入指令
                writeByte(0x44);//指定功能参数为固定地址显示
                stopDisp();//结束写入指令

                var Date = DateTime.Now.ToString("hhmm").ToCharArray();//获得当前日期,并表示为小时分钟
                byte[] Characters = { 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71 };//0~9,A,b,C,d,E,F
                //设置显示地址以及显示内容
                startDisp();
                writeByte(0xC0);//第一个字符
                writeByte(Characters[Date[0] - 48]);
                stopDisp();

                startDisp();
                writeByte(0xC1);//第二个字符
                writeByte((byte)(Characters[Date[1] - 48] + 0x80));//第二个字符带有冒号,因此将第一位空置拉高
                stopDisp();

                startDisp();
                writeByte(0xC2);//第三个字符
                writeByte(Characters[Date[2] - 48]);
                stopDisp();

                startDisp();
                writeByte(0xC3);//第四个字符
                writeByte(Characters[Date[3] - 48]);
                stopDisp();

                //开始写入亮度
                startDisp();
                writeByte(0x8f);
                stopDisp();
            }
            while (true)
            {
                Show();
            } 
        }

以上是固定字符显示代码

对于.net core的项目来说,如果想使用Mono运行,那么命令是

 执行mono xxxx.dll的方式,如果是普通的.net4.0框架的程序才是mono xxxx.exe的方式

猜你喜欢

转载自www.cnblogs.com/ACDIV/p/11625977.html