perl-based GPIO control for pine64

The pine64, like many single board computers, has a file system interface for its gpio pins.  This means that reading and writing logic levels on a particular gpio pin is a matter of reading and writing to the particular file for that pin.  Interacting with files in this manner is something that perl excels at.  That is one reason why I mostly forego using ready made tools like wiringpi and make my own gpio interface functions.  This also help me to understand the system more completely, and customize according to the way that I think, instead of using another’s canned tools.

I have written a number of perl scripts for the raspberry pi that control the gpio system interface and bit-banged drivers for several popular integrated circuits such as the MCP3004, MCP3008, MCP3208 (all analog to digital converters) , MAX7219, 74HC595, and others.  The pine64 has a (mostly) compatible 40-pin gpio header.  I only had to change one line of my gpio driver to make it work with the pine64.  Here is the perl gpio function file in it’s entirety:

 

#!/usr/bin/perl -w
use strict;

=pod
gpio utilities for pine64
enable, disable, read, write
=cut

#global vars
#array for gpio pin numbers
my @line_nums = (227,226,362,71,233,76,64,65,66,229,230,69,73,80,32,33,72,77,78,79,67,231,68,70,74,75);
my ($iox, $status);

sub gpio_enable{
	my $ind = $_[0];
	$iox = $line_nums[$ind];
	#print "IO line: $iox\n";

	my $gpiosz = @line_nums;	
	#print "gpiosz: $gpiosz\n";

	my $direction = $_[1];
	#print "direction: $direction\n";

	#err chk
	if($ind < 0 || $ind > $gpiosz){
		print "INVALID GPIO RANGE\n";
		exit;
	}#end if invalid index
	if($direction ne 'in' && $direction ne 'out'){
		print "INVALID DIRECTION\n";
		exit;
	}#end if invalid direction
	
	#write gpio pin val to export file
	open(EF, ">", "/sys/class/gpio/export") or die $!;
	print EF $iox;
	close(EF);

	#set direction of gpio pin
	open(GF, ">", "/sys/class/gpio/gpio$iox/direction") or die $!;
	print GF $direction;
	close(GF);

}#end gpio_enable

sub gpio_read{
	#gpio number as arg, returns direction in/out
	my $ind = $_[0];
	$iox = $line_nums[$ind];
	
	my $value = ''; 

	#reads state of gpio pin
	open(GS, "/sys/class/gpio/gpio$iox/value") or die $!;
	while(){ $value = $_; };
	#print "$iox val: $value";
	close(GS);

	if($value == 0 || $value == 1){
		return $value;
	}#end unless
	else{ 
		print "ERROR: Undefined value on GPIO $iox\n";
		exit;
	}#end else
}#end gpio_read

sub gpio_write{
	my $ind = $_[0];
	$iox = $line_nums[$ind];

	my $value = $_[1];

	#write value to gpio pin
	open(GV, ">", "/sys/class/gpio/gpio$iox/value") or die $!;
	print GV $value;
	close(GV);
}#end gpio_write

1;

Very simple (under 80 lines) and very effective.  This has been tested ad infinitum to work as expected.  This gpio function file is also the main building block for my bit-banged drivers.    To demonstrate,  here is an example using a MAX7219 64-LED display driver.  I wrote a custom bit-banged driver using this gpio script.

pine64 perl max7219 driver
pine64 MAX7219 perl based driver

When I write a driver like this, I get the data sheet from the manufacturer, and write functions to set all of the configuration registers and control the IO functions.  I use these 8-position, 7-segment displays frequently in my home brew projects, so I started there with my new pine64.  The driver I wrote for this particular display has some visual effects,  sets the intensity, and has some easy to use functions that can read text files word by word.  I can also handle cascaded displays and a few more novel features that I have not seen anywhere else.  Here is the display driver in its current entirety:

 

#!/usr/bin/perl 
use strict;
use Time::HiRes qw (usleep);
require "/home/debian/gpio/gpio_pine64.pl";

=pod
routines to control a MAX7219 8-digit LED
display driver.  
for now:
CLK: gpio 0
DS : gpio 1
LOAD:gpio 2
data is shifted into regs on rising edge of CLK, 
output displayed on rising edge of LOAD

NOTES:
This version supports cascading several 7219's.  I have
noticed that even with programs that assume only one
7219, that if there is another cascaded, the exact same
output is mirrored on subsequent displays. 

I assume that turn_on, set_intenisty, set_scanlimit, etc
all get shifted into the next chip anyway, however, 
these subroutines take the number of cascaded 7219s as
an argument and shift in and latch the necessary
controls to use the display.  
=cut

#global vars for gpio init function
#init should be called from script
#init7219 (0,1,2);
my ($clk, $ds, $load);

#SUBROUTINE PROTOTYPES
=pod

=cut

############### GLOBAL VARS #####################
#shutdown register 
my @sdreg = (0,0,0,0,1,1,0,0);
my @turn_on = (0,0,0,0,0,0,0,1);
my @turn_off = (0,0,0,0,0,0,0,0);

#set scan limit register 
my @slimreg = (0,0,0,0,1,0,1,1);
#last 3 LSBs scan all 8 digits
my @slregall = (0,0,0,0,0,1,1,1);

#display test register
my @disptstreg = (0,0,0,0,1,1,1,1);

#intensity register
my @intreg = (0,0,0,0,1,0,1,0);

#digit addrs
#first 4 bits D15-D12
#don't care bits, then addr
my @_d0 = (0,1,0,1,0,0,0,1);
my @_d1 = (0,1,0,1,0,0,1,0);
my @_d2 = (0,1,0,1,0,0,1,1);
my @_d3 = (0,1,0,1,0,1,0,0);
my @_d4 = (0,1,0,1,0,1,0,1);
my @_d5 = (0,1,0,1,0,1,1,0);
my @_d6 = (0,1,0,1,0,1,1,1);
my @_d7 = (0,1,0,1,1,0,0,0);

#0-9
my @_1 = (0,0,1,1,0,0,0,0);
my @_2 = (0,1,1,0,1,1,0,1);
my @_3 = (0,1,1,1,1,0,0,1);
my @_4 = (0,0,1,1,0,0,1,1);
my @_5 = (0,1,0,1,1,0,1,1);
my @_6 = (0,1,0,1,1,1,1,1);
my @_7 = (0,1,1,1,0,0,0,0);
my @_8 = (0,1,1,1,1,1,1,1);
my @_9 = (0,1,1,1,0,0,1,1);
my @_0 = (0,1,1,1,1,1,1,0);
my @_all = (1,1,1,1,1,1,1,1);

#alpha chars
my @_a = (0,1,1,1,0,1,1,1);
my @_b = (0,0,0,1,1,1,1,1);
my @_c = (0,1,0,0,1,1,1,0);
my @_d = (0,0,1,1,1,1,0,1);
my @_e = (0,1,0,0,1,1,1,1);
my @_f = (0,1,0,0,0,1,1,1);
my @_g = (0,1,1,1,1,0,1,1);
my @_h = (0,0,1,1,0,1,1,1);
my @_i = (0,0,0,0,0,1,0,0);
my @_j = (0,0,1,1,1,1,0,0);
my @_k = (0,0,1,1,0,1,1,0);
my @_l = (0,0,0,0,1,1,1,0);
my @_m = (1,0,0,1,0,1,0,0);
my @_n = (0,0,0,1,0,1,0,1);
my @_o = (0,0,0,1,1,1,0,1);
my @_p = (0,1,1,0,0,1,1,1);
my @_q = (0,1,1,1,0,0,1,1);
my @_r = (0,0,0,0,0,1,0,1);
my @_s = (0,1,0,1,1,0,1,1);
my @_t = (0,0,0,0,1,1,1,1);
my @_u = (0,0,0,1,1,1,0,0);
my @_v = (0,0,0,1,1,1,0,0);
my @_w = (0,0,0,1,0,1,0,0);
my @_x = (0,0,0,1,1,1,0,1);
my @_y = (0,0,1,1,1,0,1,1);
my @_z = (0,1,1,0,1,1,0,1);

#special chars
my @_dp = (1,0,0,0,0,0,0,0);
my @_sp = (0,0,0,0,0,0,0,0);
my @_dash = (0,0,0,0,0,0,0,1);
my @_comma = (1,0,0,0,0,0,0,0);
my @_period = (1,0,0,0,0,0,0,0);
my @_question = (1,1,1,0,0,0,1,0);
my @_exclaimation = (1,0,1,1,0,0,0,0);

#individual segments
my @_seg_a = (0,1,0,0,0,0,0,0);
my @_seg_b = (0,0,1,0,0,0,0,0);
my @_seg_c = (0,0,0,1,0,0,0,0);
my @_seg_d = (0,0,0,0,1,0,0,0);
my @_seg_e = (0,0,0,0,0,1,0,0);
my @_seg_f = (0,0,0,0,0,0,1,0);
my @_seg_g = (0,0,0,0,0,0,0,1);

#@all_digits is an arra of array references
#representing each digit used to operate
#on each digit 
my @all_digits = (\@_d0,\@_d1,\@_d2,\@_d3,\@_d4,\@_d5,\@_d6,\@_d7);

#@all_segs is an array of array references
#representing each segment of a 7-seg LED array
my @all_segs = (\@_seg_a,\@_seg_b,\@_seg_c,\@_seg_d,\@_seg_e,\@_seg_f,\@_seg_g);

#Hash that maps alphanumeric chars to array ref
my %alphanums = (
	'A' => \@_a,
	'B' => \@_b,
	'C' => \@_c,
	'D' => \@_d,
	'E' => \@_e,
	'F' => \@_f,
	'G' => \@_g,
	'H' => \@_h,
	'I' => \@_i,
	'J' => \@_j,
	'K' => \@_k,
	'L' => \@_l,
	'M' => \@_m,
	'N' => \@_n,
	'O' => \@_o,
	'P' => \@_p,
	'Q' => \@_q,
	'R' => \@_r,
	'S' => \@_s,
	'T' => \@_t,
	'U' => \@_u,
	'V' => \@_v,
	'W' => \@_w,
	'X' => \@_x,
	'Y' => \@_y,
	'Z' => \@_z, 
	'0', => \@_0, 
	'1', => \@_1, 
	'2', => \@_2, 
	'3', => \@_3, 
	'4', => \@_4, 
	'5', => \@_5, 
	'6', => \@_6, 
	'7', => \@_7, 
	'8', => \@_8, 
	'9', => \@_9, 
	'.' => \@_period, 
	'-' => \@_dash, 
	',' => \@_comma, 
	'?' => \@_question, 
	'!' => \@_exclaimation
);#end %alphanums declaration

############### SUBROUTINES #####################
sub shift_in{
=pod
data is shifted in with 16 bit packets: 8-bit segment,
4-bit digit address, 4-bit dont care bits
=cut
	#array ref seg data
	my $leds = $_[0];
	#array ref seg addr
	my $addr = $_[1];
	#number cascaded 7219s
	my $ncas = $_[2];
	#delay in milliseconds
	my $delay = $_[3];
	#latch flag
	my $lf = $_[4];

	#high flag for data gpio line
	my $hf = 0;

	#my $ncp = $ncas * 32;	#min number 32 for 16 clock pulses
	my $ncp = 32;
	##print "ncp: $ncp\n"; 

	#main clock loop
	my $i=0;
	#$state toggles from 1 to 0 needed for clock pulse
	my $state = 0;					
	#ensures first pulse goes from low to high
	my $seed = 3;					

	while($i<$ncp){#correct num clock pulses $state = $seed%2; $seed++; #load data in last 8 clock pulses #make DS high before clock pulse; only on even num #so index array is whole number #MSB read first, then addr, last data if(($i%2) eq 0){ #address D8-D11, and don't care bits D12-D15 if($addr->[$i/2] eq 1 && $i<=14){ gpio_write($ds,1); $hf = 1;#set high flag }#end addr high bit #7-seg data, D0-D7 if($leds->[($i-16)/2] eq 1 && $i > 14){#array ref, light this led up
				#test; set q1 high
				##print "inside data loop, i: $i\tindex: " .(($i-16)/2) . "\n";
				gpio_write($ds, 1);
				$hf = 1;#set high flag
			}#end if build D0-D7

		}#end if even $i 

		#TEST
		#print "i:\t$i\nD:\t$hf\n\n";
	
		#toggle clock pulse
		gpio_write($clk, $state);
		usleep($delay);#sleep .001 sec

		#lower data if high flag set
		if($hf eq 1){
			gpio_write($ds,0);
			$hf = 0;#reset high flag
		}#end if

		$i++;
	}#end while

	#latch output lines
	if($lf eq 1){	
		load();
	}#end latch flag

	gpio_write($clk,0);#set clock pulse low

}#end shift_in

sub load{

=pod
toggles LOAD pin on 7219 to send to output pins.  When the pin goes from low to high, sets output. 
=cut

	#enable XIO-p2
	gpio_enable($load, 'out');

	#ensure it is low
	gpio_write($load, 0);#low

	#go from low to high
	gpio_write($load, 1);#high
	#print "LATCH HIGH\n";
	usleep(500);#pause 0.0005 second

	#reset to low
	gpio_write($load, 0);#low
	#print "LATCH LOW\n";
	
}#end load

sub init7219{
=pod
initializes gpio lines to low
=cut
	$clk = $_[0];
	$ds = $_[1];
	$load = $_[2];

	gpio_enable($clk, 'out');
	gpio_write($clk, 0);
	gpio_enable($ds, 'out');
	gpio_write($ds, 0);
	gpio_enable($load, 'out');
	gpio_write($load, 0);
}#end init

sub print_sentence{
	#currently for a single 7219 array
	my $sentence = $_[0];
	#set to all uppercase letters
	$sentence = uc $sentence;

	#delay between words in microseconds
	my $delay = $_[1];

	#all off flag, set if reading, unset if
	#you want the text to remain on the array
	#if empty, clears the text
	my $cleartxt_flag = $_[2];

	my @words = split / /, $sentence;
	my $numwords = @words;
=pod
	foreach my $word (@words){
		print $word."\n";
	}#end foreach
=cut
	
	#print "sentence: $sentence\nNumwords: $numwords\n";
	
	#loop through words
	for(my $i=0;$i<$numwords;$i++){
		#split word into an array of chars
		my @letters = split //, $words[$i];

		#number of chars in word
		my $numalphanums = @letters;
		#print "num letters: $numalphanums\n";
			
		#first letter
		my $ln = 0;
		
		#reverse @all_digits to display words
		#left to right
		my @rev_segs = reverse @all_digits;

		foreach my $digit (@rev_segs){
		
			#limited to 8 digits for now
			shift_in($alphanums{$letters[$ln]}, $digit, 1, 250, 1 );
			#print "letter: $letters[$ln]\n";
			$ln++;#go to next letter
		}#end letters inner for loop
		
		usleep($delay);
		unless($cleartxt_flag == 1){
			all_off();
		}#end if
	}#end for numwords
}#end print_sentence

sub print_interleaved{
	#takes separate strings for each
	#line of displays. The strings are
	#assumed to fit into the 8-digit
	#line of an led array for a 7219
	my $str1 = $_[0];
	my $str2 = $_[1];

	#upper case
	$str1 = uc $str1;
	$str2 = uc $str2;

	#convert strings to array of chars
	my @s1 = split //, $str1;
	my @s2 = split //, $str2;

	#pad string arrays with spaces
	#if less than 8 chars
	my $s1_len = @s1;
	my $s2_len = @s2;
	#print "s1len: $s1_len\ts2len: $s2_len\n";

	if($s1_len < 8){
		my $nsp = 8-$s1_len;
		for(my $n=$s1_len;$n<8;$n++){
			$s1[$n] = " ";
		}#end for pad w/ spaces
	}#end pad str1 with spaces

	if($s2_len < 8){
		my $nsp = 8-$s2_len;
		for(my $n=$s2_len;$n<8;$n++){
			$s2[$n] = " ";
		}#end for pad w/ spaces
	}#end pad str1 with spaces

	$s1_len = @s1;
	$s2_len = @s2;
	#print "s1len: $s1_len\ts2len: $s2_len\n";

	#reverse @all_digits to display words
	#left to right
	my @rev_segs = reverse @all_digits;

	#init letter number to 0
	my $ln = 0;

	foreach my $digit (@rev_segs){
		shift_in($alphanums{$s2[$ln]}, $digit, 1, 250, 0 );
		shift_in($alphanums{$s1[$ln]}, $digit, 1, 250, 0 );
		load();
		$ln++;
	}#end for
}#end print_interleaved

sub turn_on{
	my $ncas = $_[0];
	if(defined($ncas)){
		#print "tu ncas defined\n";
		for(my $ni=0;$ni<$ncas;$ni++){
			shift_in(\@turn_on, \@sdreg, $ncas, 250, 0);
			if($ni ==($ncas-1)){
				#print "tu load\n";
				load();
			}#end if
		}#end for
	}#end if multiple 7219's
	else{#just one
		#set shutdown register to normal operation
		shift_in(\@turn_on, \@sdreg, 1, 250, 1);
	}#end else
}#end turn_on

sub turn_off{
	my $ncas = $_[0];
	if(defined($ncas)){
		for(my $ni=0;$ni<$ncas;$ni++){
			shift_in(\@turn_off, \@sdreg, $ncas, 250, 0);
			if($ni ==($ncas-1)){
				load();
			}#end if
		}#end for
	}#end if multiple 7219's
	else{#just one
		#set shutdown register to off
		shift_in(\@turn_off, \@sdreg, 1, 250, 1);
	}#end else
}#end turn_off

sub set_scanlimit{
	my $ncas = $_[0];
	if(defined($ncas)){
		#print "sl ncas defined\n";
		for(my $ni=0;$ni<$ncas;$ni++){
			shift_in(\@slregall, \@slimreg, $ncas, 250, 0);
			if($ni ==($ncas-1)){
				load();
				#print "sl load\n";
			}#end if
		}#end for
	}#end if multiple 7219's
	else{#just one
		#set scan limit register
		shift_in(\@slregall, \@slimreg, 1, 500, 1);
		#print "sl 1 chip\n";
	}#end else
}#end set_scanlimit

sub set_intensity{
	#takes string as arg: min, dim, mid, bright, max
	my $intensity = $_[0];
	
	#default to max
	my @intregdata = (0,0,0,0,1,1,1,1);
	if($intensity eq 'min'){
		@intregdata = (0,0,0,0,0,0,0,0);
	}#end if
	if($intensity eq 'dim'){
		@intregdata = (0,0,0,0,0,0,1,1);
	}#end if
	if($intensity eq 'mid'){
		@intregdata = (0,0,0,0,0,1,1,1);
	}#end if
	if($intensity eq 'bright'){
		@intregdata = (0,0,0,0,1,0,1,1);
	}#end if
	if($intensity eq 'max'){
		@intregdata = (0,0,0,0,1,1,1,1);
	}#end if

	#print "Intensity: $intensity\n";
	shift_in(\@intregdata, \@intreg, 1, 250, 1);
}#end set_intensity

sub all_off{
	#clear display
	#call after turned on, and
	#scan reg set to all digits
	my $ncas = $_[0];
	if(defined($ncas)){
		foreach my $digit(@all_digits){
			for(my $ni=0;$ni<$ncas;$ni++){
				shift_in(\@turn_off, $digit, $ncas, 100, 0);
				if($ni ==($ncas-1)){
					load();
				}#end if
			}#end for
		}#end outer for
	}#end if multiple 7219's
	else{#just one
		#@all_digits is an array of array references
		#representing each digit
		foreach my $digit (@all_digits){
			shift_in(\@turn_off, $digit, 1, 100, 1);
		}#end foreach
	}#end else
}#end all_off

sub disp_teston{
	#turn on display test, 
	#all digits and dp
	shift_in(\@turn_on, \@disptstreg, 1, 200, 1);
}#end disp_test

sub disp_testoff{
	#turn off display test, 
	#all digits and dp
	shift_in(\@turn_off, \@disptstreg, 1, 200, 1);
}#end disp_test

#################### EFFECTS######################
sub clockwise_circles{
	#number of iterations
	my $i = $_[0];
	my $k = 0;
	
	while($k<$i){
		for(my $n=0;$n<6;$n++){
			#outer for, each segment
			for(my $x=0; $x<8; $x++){
				#inner for, each digit
				shift_in(@all_segs[$n], @all_digits[$x], 1, 100, 1);
			}#end inner for
		}#end outer for

		$k++;

	}#end while
	all_off();
}#end clockwise_circles

sub countercw_circles{
	#number of iterations
	my $i = $_[0];
	my $k = 0;
	my @revall_segs = reverse @all_segs;
	while($k<$i){
		for(my $n=1;$n<7;$n++){
			#outer for, each segment
			for(my $x=0; $x<8; $x++){
				#inner for, each digit
				shift_in(@revall_segs[$n], @all_digits[$x], 1, 100, 1);
			}#end inner for
		}#end outer for

		$k++;

	}#end while
	all_off();
}#end countercw_circles

sub bullets_lrtop {
	#number of iterations
	my $ni = $_[0];
	for(my $n=0;$n<$ni;$n++){ for(my $i=8;$i>=0;$i--){
			shift_in(\@_seg_a, @all_digits[$i], 1, 100, 1);
			all_off();
		}#end inner for
	}#end outer for	
	all_off();
}#end bullets_lrtop

sub bullets_rltop {
	#number of iterations
	my $ni = $_[0];
	for(my $n=0;$n<$ni;$n++){
		for(my $i=0;$i<=8;$i++){
			shift_in(\@_seg_a, @all_digits[$i], 1, 100, 1);
			all_off();
		}#end inner for
	}#end outer for	
	all_off();
}#end bullets_lrtop

sub bullets_lrmid {
	#number of iterations
	my $ni = $_[0];
	for(my $n=0;$n<$ni;$n++){ for(my $i=8;$i>=0;$i--){
			shift_in(\@_seg_g, @all_digits[$i], 1, 100, 1);
			all_off();
		}#end inner for
	}#end outer for	
	all_off();
}#end bullets_lrtop

sub bullets_rlmid {
	#number of iterations
	my $ni = $_[0];
	for(my $n=0;$n<$ni;$n++){
		for(my $i=0;$i<=8;$i++){
			shift_in(\@_seg_g, @all_digits[$i], 1, 100, 1);
			all_off();
		}#end inner for
	}#end outer for	
	all_off();
}#end bullets_lrtop

sub bullets_lrbot {
	#number of iterations
	my $ni = $_[0];
	for(my $n=0;$n<$ni;$n++){ for(my $i=8;$i>=0;$i--){
			shift_in(\@_seg_d, @all_digits[$i], 1, 100, 1);
			all_off();
		}#end inner for
	}#end outer for	
	all_off();
}#end bullets_lrtop

sub bullets_rlbot {
	#number of iterations
	my $ni = $_[0];
	for(my $n=0;$n<$ni;$n++){
		for(my $i=0;$i<=8;$i++){
			shift_in(\@_seg_d, @all_digits[$i], 1, 100, 1);
			all_off();
		}#end inner for
	}#end outer for	
	all_off();
}#end bullets_lrtop
1;

Here is an example of how simple it is to use one of these LED displays using this driver.

 

#!/usr/bin/perl
use strict;
require 'MAX7219.pl';        #required driver for LED array

init7219(0,1,2);             #set the GPIO lines on the pine64 for clock, data in, and latch
turn_on(1);                  #turn on only 1 MAX7219 chip
set_scanlimit(1);            #tell it to use all 8 digits for 1 LED array
set_intensity('max');        #maximum intensity

print_sentence("pine64 rules the world", 500000);

Leave a Reply

Your email address will not be published. Required fields are marked *