树莓派Bash脚本串行控制12864液晶屏

这个项目是在树莓派上纯使用Bash脚本对12864液晶屏进行操作的,可供大家学习Bash/Linux硬件等参考使用。

PokeBox

喜欢

4971
浏览
2
喜欢

> 更多图片

项目状态:已完成
开放度:公开
所属分类:电子
发布时间:2017-09-25
最近更新:2017-11-16

标签

Maker 尚未留下关于此项目的任何描述...

文件库

12864.sh
编写完成的实例脚本 [3942 Bytes at 2017-09-25, 12 次下载]

树莓派12864接线.fzz
树莓派和12864连 [143480 Bytes at 2017-09-25, 13 次下载]



教程

前言:最近在工作室受到前辈的经验分享,感觉以前自己写的经验实在是有点不尽人意,所以这次打算写个质量尽量高一些的经验分享。当然,毕竟还在提升,有很多不足的地方还请各位多多指教~

12864是一块黑白的点阵屏,在单片机上其实很常见。树莓派也有很多大佬用它来播放视频什么的,当然,这些几乎都是用C去编写的程序。我其实比较喜欢从简,而且深爱着bash脚本,难道这些驱动脚本程序做不到么?当然不是~这次我就用bash脚本以串行的方式来驱动12864屏幕。

首先得解决电路和接线问题,我们先来看看12864的GPIO引脚。
12864英文引脚说明
上面是12864的引脚功能说明,第一脚是板子的地线,第二脚是电源,第三脚是背光亮度的调节。

第四脚是并行数据指令的选择信号,我们用的是串行,所以第四脚其实是CS片选信号,高电平使能。

第五脚为串行的数据口,串行模式下所有的数据都通过这个脚传输。

第六脚是并行使能信号,串行的同步时钟信号。

15脚是并行串行的选择信号,这一脚接高电平为并行信号,接低电平是串行信号,我们使用串行通讯,使用这一脚直接接0V。

17脚是复位用的,这一脚不接也无所谓,屏幕会内部自动复位。

19/20脚是背光的电源脚。其他的是并行用的,我们用不上,所以就不作说明了。

我们知道了引脚功能后接下来就是整理这些引脚,事实上,真正和树莓派通讯传输数据的只有第五数据脚和第六时钟脚,其他的都是要么高电平要么低电平,我们全部根据设置接到电源上即可。
把电源和相关引脚连接
树莓派和12864连接
电路连接完成,我们就可以写树莓派上的驱动程序了。这里我把数据接在树莓派的38脚(20)上,时钟接在树莓派的36脚(16)上。
然后我们开始写代码。

首先我们要定义几个变量,CS(片选),SID(数据)和CLK(时钟),分别设置为树莓派上我们连接的引脚号(BCM编号)

##引脚定义
declare -ir CS=21       ##使能线;声明CS为只读整型变量
declare -ir SID=20      ##数据线
declare -ir CLK=16      ##时钟线

declare是变量声明,-i参数表示这个变量是整数型的,-r表示只读变量(可以理解为常量),这样就可以防止我们轻易的改变它。

树莓派的引脚控制在命令行里大家最常用的估计就是用gpio命令了,但是这里我们不依赖gpio这个命令,而是直接去控制树莓派的内核对IO进行操作。大家对Linux有了解的话都知道,Linux上一切皆为文件,所以对GPIO操作其实也是对文件进行操作,每个GPIO其实都对应了一个文件,所有GPIO相关的映射文件都在/sys/class/gpio里,比如/sys/class/gpio中的export就是和系统内核交互告诉系统需要暴露哪些IO的文件,向这个文件内写入需要使用的引脚号系统便会把对应的引脚控制相关的东西扔出来。可以理解为向系统提交一个申请,然后系统会给你对应的东西一样。

同样的,如果我们不需要这些东西了,希望系统把他收回去,那么/sys/class/gpio/unexport就是接受这个请求的窗口,操作方式也是向这个文件内写入对应的引脚号即可。

所以,为了方便,我们可以把这几个文件目录设置为一个变量来调用。

##引脚控制相关路径
PINDIR=/sys/class/gpio      ##树莓派引脚控制相关映射目录
PIN_INIT=$PINDIR/export     ##树莓派引脚控制映射文件
PIN_UNIT=$PINDIR/unexport   ##撤销引脚控制映射的文件

大体的环境搞定后,我们就可以开始写功能了。

首先我们控制IO得先让系统把IO控制的接口给我们,上面说到了引脚控制相关的请求,所以我们只要用echo 引脚号 > /sys/class/gpio/export里即可。

自己一个命令一个命令的去写比较麻烦,特别是如果东西多的话,手写效率就太低了,那么我们用一个for循环来把我们需要的几个IO直接写出来。

在bash里,for可以这样用

for 变量 in 值1 值2 值3 ...
do
执行语句
done

所以我们申请的代码可以这样写:

for pin in $CS $SID $CLK
do
 echo "$pin" > $PIN_INIT
done

这样,我们就可以在gpio文件夹下看到一个新生成的对应GPIO口的文件夹,里面都有啥呢~我们ls看看

active_low device direction edge power subsystem uevent value

这里我们只需要注意两个文件,其中一个是direction,这个是GPIO的模式,IO方向是输入还是输出(in或out)。另一个是value,这个是当前IO的电平值(1或0)。
这两个文件是可读写的。我们设置这个io的模式直接向direction内写入值in或out即可完成设置,电平值也一样,写入1或0即可改变这个IO的状态。查询可以用cat命令来查看当前的状态。

默认把IO暴露出来后是输入模式,我们需要控制这个引脚来给12864传输数据,所以我们要把这个引脚设置成输出模式。

于是我们把上面的代码改一下,添加一条写入out的指令:

    for pin in $CS $SID $CLK            ##分别初始化三个引脚
    do
        if [ ! -e $PINDIR/gpio${pin}/direction ];then   ##判断这个引脚是否已经初始化过
            echo "初始化$pin"
            echo "$pin" > $PIN_INIT     ##如果没有初始化则把对应的引脚号暴露出来
        fi
        echo "out" > $PINDIR/gpio${pin}/direction   ##把初始化好的引脚设置为输出模式
        echo "$pin输出模式"
    done
上面的代码就完成了对引脚的初始化设置,同时为了防止重复初始化,这里加入了一个判断,如果没有产生对应的gpio文件夹,说明没有启用这个IO口,那么就对它进行初始化设置,否则就忽略掉它。在if语句里-e是判断文件或文件夹是否存在的。感叹号表示取反,就是如果原来是条件为真时执行变成条件为假时执行。

IO的初始化完成后,我们就要写数据控制屏幕了,要进行这一步我们需要对12864的驱动有一定了解,

我们首先要明白12864的串行是怎么工作的,这就得学会看时序图。
12864时序图
根据这个时序图,我们可以看到,要和屏幕通讯,每发送一组数据,CS都需要拉高使能,CS就相当于开关一样,只有把开关打开才能使用。这里打开它的方式是给这个引脚一个高电平信号。

同时,时钟脚要给一个脉冲信号,时钟每拉高一次,SID数据脚就发送一个位,每8个位是一个字节数据。每组数据由3个字节组成,第一个字节是功能数据,用来告诉屏幕我现在要发送还是接收,操作的是命令还是数据,这两个参数由第6和第七个位来控制,第八个位固定为0,前5位固定为1。

第二字节是8位数据的高位,即如果我们要发送一个0xFA,那么0xF0就是高位数据,0x0A就是低位数据。空白部分用0填充。

那么我们怎么把这些数据转换成高低电平信号呢?事实上如果你了解2进制和16进制转换的话就明白,0xF代表的是二进制的1111,0x1对应的就是二进制的1,0x2对应的就是二进制的10。这个大家只要打开计算器到程序员模式查看二进制和十六进制的对应关系即可明白。
计数器上16进制和2进制转换
我们搞懂二进制和16进制的转换后就可以编写代码去控制树莓派的GPIO给屏幕发数据了。

因为一组数据事实上是通过3组一个字节(8位)数据发送的,所以我们可以直接写一个发送一个字节的脚本。

我们首先要把一个字节的数据转换成一个位,从高位开始发送,那么我们就需要对数据进行逻辑运算。比如我们有一个0x3A的数据要发送,那么我们先把它变成二进制看看本质是什么样的,一个0x3A的二进制是这样的:

0011 1010

前面的0011就是3的二进制,1010就是十六进制A的二进制形式。我们需要把这个数据的高位先扔出去,那么我们就把这个数与1000 0000进行与运算,即

0x3A & 0x80

得到的就是0,这就是第一位数据,然后我们把这个数据直接传输给IO就可以从树莓派的引脚上得到一个低电平信号。
接着我们要得到第二位,第三位数据,那么怎么办呢?是的,进行数据移位,把发送过的数据扔掉,只要对数据进行左移即可。

我们可以在bash脚本里用双括号进行像在C语言里一样的逻辑运算操作。((0x3a<<=1))

这样,我们得到的就是0111 0100的数据了。和原来的0011 1010相比较不难看出,整个数据被向左推了一格。

以此类推,我们就可以把八个位的数据发送出去。

写成脚本大概就是这样:

data=0x3a
for ((i=0;i<8;i++))
do
    (( DATABIT=(data&0x80) ))
    echo "${DATABIT}" > $PINDIR/gpio${PIN}/value    ##发数据
    echo "1" >  $PINDIR/gpio${PIN}/value    ##拉时钟线
    echo "0" >  $PINDIR/gpio${PIN}/value
    (( data<<=1 ))          ##SPI数据左移一个字节
done

我们想发送什么数据只要把data改成我们要发送的数据即可~
这一步搞定,我们的算法已经完成了一半了,接着我们利用这个脚本把屏幕的初始化命令发出去就可以初始化屏幕了。

那么我们怎么把文字发送给屏幕呢?别着急,首先我们看屏幕是怎么组成的。根据数据手册上的寄存器地址可以看到屏幕的RAM是这样的:

# 90H 91H 92H 93H 94H 95H 96H 97H | 98H 99H 9AH 9BH 0CH 9DH 9EH 9FH
# A0H A1H A2H A3H A4H A5H A6H A7H | A8H A9H AAH ABH ACH ADH AEH AFH
# B0H B1H B2H B3H B4H B5H B6H B7H | B8H B9H BAH BBH BCH BDH BEH BFH

每一个位其实是对应着控制屏幕的一个块,因为屏幕是支持汉字的,一个汉字需要16个像素来显示,那么一行能显示128个像素点,就是8个汉字,每列有64个像素点,也就是可以显示4个汉字,刚好是上面DDRAM的一半,所以其实12864是可以缓存2个屏的数据,这样我们其实可以很容易的完成例如滚屏之类的操作。事实上,12864使用的前0x80到0x9F是当前屏幕的寄存器,所以0x80对应的就是左上角,第一行第一个字,0x90对应的其实是第二行第一个字,那么0x88就是第三行第一个字,0x98就是第四行第一个字。所以可以说其实寄存器是折过来用的。
那么我们都知道ascii码其实也是一个个二进制数据组成的,只是我们人为的给他编码,比如数字1对应的就是十六进制的0x31,中文的GB2312编码其实算是ascii的扩充,因为我们都常规的认为一个汉字占2个字节,其实说的就是GB2312编码的一个中文汉字。而如果我们用gb2312编码来发给树莓派,你会发现其实会出现乱码, 因为树莓派默认使用的是UTF8的编码(其实大部分linux都是),UTF8的一个中文可就不见得只占2个字节了。所以我们如果要在树莓派上发中文给12864的话得进行编码转换,把UTF8的编码数据转成GB2312的数据发给屏幕。这样才能正确的显示中文。那么问题又来了,我们怎么在命令行把中文转换成电平数据呢?

其实也不是很难,首先我们要把字符转换成十六进制数据,然后再利用我们刚才写的那段代码就可以把字符串变成电平数据发给屏幕了。命令行有一个od命令可以用来处理十六进制数据。

我们可以试着把数据传递给od看一下输出结果:

这是个例子:

echo -n "3" | od

我们可以看到输出的结果是这样的:

0000000 000063
0000001

这是什么意思呢?左边的0000000 0000001是地址偏移量,我们不需要这些数据。右边的 是以10进制数输出的3的数据,这其实也不是我们想要的16进制数。(虽然也能计算,但是用起来不方便)

我们先查看一下od命令的参数,使用od --help来查看帮助说明。

用法:od [选项]... [文件]...
 或:od [-abcdfilosx]... [文件] [[+]偏移量[.][b]]
 或:od --traditional [选项]... [文件] [[+]偏移量[.][b] [+][标签][.][b]]

Write an unambiguous representation, octal bytes by default,
of FILE to standard output.  With more than one FILE argument,
concatenate them in the listed order to form the input.

如果没有指定文件,或者文件为"-",则从标准输入读取。

If first and second call formats both apply, the second format is assumed
if the last operand begins with + or (if there are 2 operands) a digit.
An OFFSET operand means -j OFFSET.  LABEL is the pseudo-address
at first byte printed, incremented when dump is progressing.
For OFFSET and LABEL, a 0x or 0X prefix indicates hexadecimal;
suffixes may be . for octal and b for multiply by 512.

必选参数对长短选项同时适用。
  -A, --address-radix=RADIX   output format for file offsets; RADIX is one
                                of [doxn], for Decimal, Octal, Hex or None
      --endian={big|little}   swap input bytes according the specified order
  -j, --skip-bytes=BYTES      skip BYTES input bytes first
  -N, --read-bytes=BYTES      limit dump to BYTES input bytes
  -S BYTES, --strings[=BYTES]  output strings of at least BYTES graphic chars;
                                3 is implied when BYTES is not specified
  -t, --format=TYPE           select output format or formats
  -v, --output-duplicates     do not use * to mark line suppression
  -w[BYTES], --width[=BYTES]  output BYTES bytes per output line;
                                32 is implied when BYTES is not specified
      --traditional           accept arguments in third form above
      --help        显示此帮助信息并退出
      --version     显示版本信息并退出


Traditional format specifications may be intermixed; they accumulate:
  -a   same as -t a,  select named characters, ignoring high-order bit
  -b   same as -t o1, select octal bytes
  -c   same as -t c,  select printable characters or backslash escapes
  -d   same as -t u2, select unsigned decimal 2-byte units
  -f    即 -t fF,指定浮点数对照输出格式
  -i    即 -t dl,指定十进制整数对照输出格式
  -l    即 -t dL,指定十进制长整数对照输出格式
  -o    即 -t o2,指定双字节单位八进制数的对照输出格式
  -s    即 -t d2,指定双字节单位十进制数的对照输出格式
  -x    即 -t x2,指定双字节单位十六进制数的对照输出格式


TYPE is made up of one or more of these specifications:
  a          named character, ignoring high-order bit
  c          printable character or backslash escape
  d[尺寸] 有符号十进制数,每个整形数占指定尺寸的字节
  f[尺寸] 浮点数,每个整形数占指定尺寸的字节
  o[尺寸] 八进制数,每个整形数占指定尺寸的字节
  u[尺寸] 无符号十进制数,每个整形数占指定尺寸的字节
  x[尺寸] 十六进制数,每个整形数占指定尺寸的字节

SIZE is a number.  For TYPE in [doux], SIZE may also be C for
sizeof(char), S for sizeof(short), I for sizeof(int) or L for
sizeof(long).  If TYPE is f, SIZE may also be F for sizeof(float), D
for sizeof(double) or L for sizeof(long double).

Adding a z suffix to any type displays printable characters at the end of
each output line.


BYTES is hex with 0x or 0X prefix, and may have a multiplier suffix:
  b    512
  KB   1000
  K    1024
  MB   1000*1000
  M    1024*1024
and so on for G, T, P, E, Z, Y.

这里我们只要关注两个参数,一个是-A参数,一个是-t参数和-x参数。根据帮助说明,-A是偏移量的参数设置,可以设置成十进制/八进制/十六进制或者是不输出。我们这里不需要这个偏移量的信息,所以直接设置为n【即none】。然后来看看-t参数,这里只简单的说了它代表输出的格式,但没有详细说格式是怎么样的。我们再来看看下面的-x,它相当于-t x2,输出双字节的16进制数,不过我们需要的是单字节输出,所以这里还得想办法变成单字节。其实可以通过-t x2是双字节16进制推测出单字节输出应该是-t x1,但是敲代码需要谨慎一些,更详细的文档在man od里,我们可以执行这个命令查看详细的说明文档。在这个文档里我们就可以查阅到更详细的命令说明。其中就有对于type的详细说明

x[SIZE]

         hexadecimal, SIZE bytes per integer

所以我们的推测其实是没错的,-t x1就是输出单字节的16进制数。

我们可以试试看,

echo -n "hello" | od -A n -t x1

看到结果68 65 6c 6c 6f正好就是hello的ascii码。但是还有一些瑕疵,就是在进行运算时系统是根据数据的前导符来识别数值类型的,比如0x01表示十六进制的1,默认不带前导符的话会被识别为10进制数,所以我们还需要在每一个数据前添加0x前导符。这里我们使用sed命令去添加即可。

echo -n "I love You" | od -A n -t x1 | sed 's/ / 0x/g'

最后一个管道后的sed命令表示把数据中的空格(" ")变成空格+0x(" 0x"),这样我们就完成在数据前添加前导符了。
这一切完成后,我们就可以把数据发给屏幕了。结合前面的显示寄存器地址,我们把代码完善,然后写成一个函数的形式。

最后,这个脚本的代码是这样的:

#!/bin/bash
################################
#   树莓派 bash 12864驱动
#   By  PokeBox
################################

##引脚定义
declare -ir CS=21       ##使能线;声明CS为只读整型变量
declare -ir SID=20      ##数据线
declare -ir CLK=16      ##时钟线

##引脚控制相关路径
PINDIR=/sys/class/gpio      ##树莓派引脚控制相关映射目录
PIN_INIT=$PINDIR/export     ##树莓派引脚控制映射文件
PIN_UNIT=$PINDIR/unexport   ##撤销引脚控制映射的文件

##脚本程序参数
declare -i EY=$1       ##列控制参数
declare -i EX=$2       ##行控制参数
declare CHR=$3      ##要显示的字符串参数
declare OT=$4       ##其他参数

##子函数体
init_pin()        ##引脚初始化函数
{
    for pin in $CS $SID $CLK            ##分别初始化三个引脚
    do
        if [ ! -e $PINDIR/gpio${pin}/direction ];then    ##判断这个引脚是否已经初始化过
            ##echo "初始化$pin"
            echo "$pin" > $PIN_INIT        ##如果没有初始化则把对应的引脚号暴露出来
        fi
        echo "out" > $PINDIR/gpio${pin}/direction    ##把初始化好的引脚设置为输出模式
        ##echo "$pin输出模式"
    done
}

unpin()        ##撤销引脚的控制访问
{
    for pin in $CS $SID $CLK
    do
        if [ -e $PINDIR/gpio${pin}/direction ];then
            echo "卸载$pin"
            echo "$pin" > $PIN_UNIT
        fi
    done
}

outpin()        ##引脚输出电平控制;用法:outpin <引脚号> <值>
{
    local PIN="$1"
    local VALUE="$2"
    echo "$VALUE" > $PINDIR/gpio${PIN}/value
}

sendbyte()        ##发送数据给屏幕;用法:sendbyte <值>
{
    local data="$1"
    for ((i=0;i<8;i++))
    do
        (( DATABIT=(data&0x80) ))
        outpin ${SID} ${DATABIT}    ##发数据
        outpin ${CLK} 1            ##拉时钟线
        outpin ${CLK} 0
        (( data<<=1 ))            ##SPI数据左移一个字节
    done
}

write()        ##写数据或写指令;用法:write <[dat/cmd]写数据/指令> <值>
{
    local ddata="${2}"
    if [ "$1" == "cmd" ];then    ##判断是发命令还是数据
        sendDATA='0xf8'        ##写指令参数
    else    sendDATA='0xfa'        ##写数据参数
    fi
    (( Hdata=(ddata&0xf0) ))    ##高位数据
    (( Ldata=((ddata<<4)&0xf0) ))    ##低位数据
    sendbyte sendDATA
    sendbyte Hdata
    sendbyte Ldata
}

clGRAM()    ##清屏
{
    write cmd 0x30
    write cmd 0x01
}

lcdinit()    ##屏幕初始化
{
    outpin ${CS} 1    ##片选(其实可以直接接到电源上)
    write cmd 0x30    ##初始化屏幕指令
    write cmd 0x0c
    write cmd 0x01
    clGRAM
}

lcdprint()    ##在屏幕上显示字符串;用法:<X坐标> <Y坐标> <UTF8字符串>
{
    local X=$1        ##函数体参数;X坐标
    local Y=$2        ##函数体参数;Y坐标
    local OUTCHAR=${3}    ##函数体参数;输出的字符串
    
    ##DDRAM 相关资料:http://www.360doc.com/content/14/0411/10/10868223_367962970.shtml
    ##
    # 80H 81H 82H 83H 84H 85H 86H 87H | 88H 89H 8AH 8BH 8CH 8DH 8EH 8FH
    # 90H 91H 92H 93H 94H 95H 96H 97H | 98H 99H 9AH 9BH 0CH 9DH 9EH 9FH
    # A0H A1H A2H A3H A4H A5H A6H A7H | A8H A9H AAH ABH ACH ADH AEH AFH
    # B0H B1H B2H B3H B4H B5H B6H B7H | B8H B9H BAH BBH BCH BDH BEH BFH
    
    case ${Y} in        ##根据Y坐标值设置地址值
    0)    (( address=0x80+X )) ;;        #初始地址+坐标偏移量
    1)    (( address=0x90+X )) ;;
    2)    (( address=0x88+X )) ;;
    3)    (( address=0x98+X )) ;;
    esac
    echo "在$X行$Y列显示$OUTCHAR"
    write cmd ${address}    ##写指令

    ##把要输出到屏幕上的字符串转换成GB2312编码后再转成16进制数后发送
    for hex in $(echo -n "$OUTCHAR" | iconv -f utf8 -t gb2312 | od -A n -t x1 | sed 's/ / 0x/g')
    do
        write dat ${hex}    ##发送一个16进制数据给屏幕
    done
}

##其他参数选项
case $OT in
reset | '-1')    lcdinit;lcdprint ${EX} ${EY} "$CHR" ;;            ##重新初始化屏幕
clear | 0)    clGRAM;lcdprint ${EX} ${EY} "$CHR" ;;        ##清屏
init | 1)    init_pin;lcdinit;lcdprint ${EX} ${EY} "$CHR" ;;    ##全部初始化
unit | 2)    unpin;sleep 0.1 ;;                    ##关闭控制引脚
help)        echo "<reset> <clear> <init> <unit>" ;;        ##显示帮助
*)    lcdprint ${EX} ${EY} "$CHR" ;;                ##其他内容
esac

我们把每一块功能单独写成一个函数体,这样我们调用起来就方便许多,可以省去很多重复的代码工作。需要什么数据只要传参就OK了。

把代码保存成12864.sh脚本,赋予文件可执行权限【chmod 755 ./12864.sh】,然后使用sudo ./12864.sh就可以运行这个脚本对屏幕进行操作了。(建议大家把直接的用户添加到gpio组里,这样操作gpio就不需要sudo提权了)

这个脚本的参数怎么用?我就写个简单的例子吧
添加图片描述
执行后效果如下~
添加图片描述
其实写完后感觉还是很棒的~