在树莓派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.listsudo 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);//拉低DIOclkPin.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的方式

相关文章