Skip to content

Instantly share code, notes, and snippets.

@maleadt
Created January 24, 2015 20:25
Show Gist options
  • Save maleadt/ef26a7d567b8a9d27534 to your computer and use it in GitHub Desktop.
Save maleadt/ef26a7d567b8a9d27534 to your computer and use it in GitHub Desktop.
Accessing nRF24LE1 PROG commands using the Bus Pirate's binary SPI mode.
#!/usr/bin/perl
#
# Initialization
#
use strict;
use warnings;
use Time::HiRes qw/usleep/;
use Getopt::Std;
my %opts;
getopts( 'h', \%opts );
sub HELP_MESSAGE { usage(); exit 0; }
sub VERSION_MESSAGE { usage(); exit 0; }
if ( $opts{h} ) {
usage();
exit 0;
}
sub usage {
print <<EOD
Usage: $0 [-h] [PORT]
EOD
;
}
my $port_path = shift || "/dev/buspirate";
die("inaccessible serial port") unless (-r $port_path);
#
# Main
#
use Device::SerialPort;
my $port = Device::SerialPort->new($port_path);
die("could not open serial port") unless $port;
# port configuration 115200/8/N/1
$port->databits(8);
$port->baudrate(115200);
$port->parity("none");
$port->stopbits(1);
$port->buffers(1, 1); # 1 byte or it buffers everything forever
$port->write_settings or die("could not configure serial port");
test();
setup();
main();
#
# Auxiliary
#
# Pack a binary string
sub bytestr {
my $string = shift || die;
$string =~ s/[a-z]/0/g;
$string =~ m/^[01]{8}$/ || die;
return pack('B8', $string);
}
# Pack something else into a single byte
sub byte {
pack("C", shift);
}
sub byte2str {
unpack("B8", shift);
}
# Flush all output on the serial port
sub flush {
usleep(20*1000);
my $data = "";
while (my $buf = $port->read(255)) {
$data .= $buf;
usleep(20*1000);
};
return $data;
}
# TODO: use vec?
sub bit_set {
my ($value, $bit) = @_;
return (($value & $bit) eq "\x00") ? 0 : 1;
}
sub hdump {
my $offset = 0;
my(@array,$format);
foreach my $data (unpack("a16"x(length($_[0])/16)."a*",$_[0])) {
my($len)=length($data);
if ($len == 16) {
@array = unpack('N4', $data);
$format="0x%08x (%05d) %08x %08x %08x %08x %s\n";
} else {
@array = unpack('C*', $data);
$_ = sprintf "%2.2x", $_ for @array;
push(@array, ' ') while $len++ < 16;
$format="0x%08x (%05d)" .
" %s%s%s%s %s%s%s%s %s%s%s%s %s%s%s%s %s\n";
}
$data =~ tr/\0-\37\177-\377/./;
printf $format,$offset,$offset,@array,$data;
$offset += 16;
}
}
#
# Bus Pirate handling
#
my $MODE = "unknown";
use constant {
ENTER_BINMODE => bytestr('00000000'),
ENTER_SPI => bytestr('00000001'),
RESET => bytestr('00001111'),
MODE_USER => "user",
MODE_BIN => "binary",
MODE_SPI => "spi"
};
sub bp_command {
# Send all command bytes
for my $cmd (@_) {
print "-> ", byte2str($cmd), "\n";
$port->write($cmd);
usleep(20*1000);
}
# Look for command acknowledgement (0x01)
my $char = $port->read(1);
print "<- ", byte2str($char), "\n";
if ($char eq "\x01") {
return 1;
}
return 0;
}
# Prepare the Bus Pirate for entering binary mode (escape any user terminal
# menu, ...).
sub prepareBinMode {
$port->write("\n" x 10); # exit any user terminal menu
$port->write("#"); # reset the user terminal
flush();
}
# Put the Bus Pirate in binary (raw bit-bang) mode.
#
# Returns protocol version number on success, or 0 on failure.
sub enterBinMode {
# We may need to send up to 20 commands to finally enter binary mode
for (1..20) {
$port->write(ENTER_BINMODE);
usleep(20*1000);
# Look for BBIOx (x = protocol version)
my $char = $port->read(4);
if ($char eq "BBIO") {
$MODE = MODE_BIN;
return $port->read(1);
}
}
return 0;
}
# Returns to user terminal mode from raw binary mode.
#
# Returns 1 on success, or 0 on failure.
sub enterUserMode {
die unless $MODE eq MODE_BIN;
if (bp_command(RESET)) {
$MODE = MODE_USER;
return 1;
}
return 0;
}
# Put the Bus Pirate in SPI mode.
#
# Returns protocol version number on success, or 0 on failure.
sub enterSPIMode {
die unless $MODE eq MODE_BIN;
$port->write(ENTER_SPI);
usleep(20*1000);
# Look for SPIn (x = protocol version)
my $char = $port->read(3);
if ($char eq "SPI") {
$MODE = MODE_SPI;
return $port->read(1);
}
}
sub test {
prepareBinMode();
enterBinMode() or die;
enterUserMode() or die;
print "Version info:\n";
usleep(200*1000);
my $version = $port->read(1000);
my @values = split('\x0d\x0a', $version);
print " Hardware: " . $values[1] . "\n";
print " Firmware: " . $values[2] . "\n";
print " PIC chip: " . $values[3] . "\n";
print " Updates URL: " . $values[4] . "\n";
}
#
# nRF24LE1 handling
#
# HARDWARE WIRING:
# - SPI lines to proper pins of the MCU when in programming mode
# - AUX to the MCU RESET pin
# - MCU PROG always high (we don't have an additional GPIO on the BP...)
my $speed;
my $config;
my $peripherals;
use constant {
# Command opcodes
NRF_WREN => "\x06",
NRF_WRDIS => "\x04",
NRF_RDSR => "\x05",
NRF_WRSR => "\x01",
NRF_READ => "\x03",
NRF_PROGRAM => "\x02",
NRF_ERASE_PAGE => "\x52",
NRF_ERASE_ALL => "\x62",
NRF_RDFPCR => "\x89",
NRF_RDISMB => "\x85",
NRF_ENDEBUG => "\x86",
# FSR register masks
FSR_ENDEBUG => byte(1 << 7),
FSR_STP => byte(1 << 6),
FSR_WEN => byte(1 << 5),
FSR_RDYN => byte(1 << 4),
FSR_INFEN => byte(1 << 3),
FSR_RDISMB => byte(1 << 2)
};
sub nrf_command {
my ($command, $args, $retlen) = @_;
my $arglen = scalar(@{$args}) + 1;
die if $arglen > 4096;
my $arglen_low = $arglen % 256;
my $arglen_high = int($arglen / 256);
die if $retlen > 4096;
my $retlen_low = $retlen % 256;
my $retlen_high = int($retlen / 256);
my @data = (
bytestr('00000100'), # Write then read BP SPI command
byte($arglen_high), # #bytes to write: high8
byte($arglen_low), # low8
byte($retlen_high), # #bytes to read: high8
byte($retlen_low) # low8
);
push(@data, $command); # bytes to write: command
push(@data, @{$args}); # args
bp_command(@data) || die;
my $output = flush();
warn("not enough data returned") if (length($output) != $retlen);
return $output;
}
# Read the flash status register
sub read_fsr {
return nrf_command(NRF_RDSR, [], 1);
}
sub write_fsr {
my $fsr = shift || die;
return nrf_command(NRF_WRSR, [$fsr], 0);
}
sub disable_infen {
my $fsr = read_fsr();
# IF the info page is enabled, try to disable it
if (bit_set($fsr, FSR_INFEN)) {
write_fsr($fsr & ~FSR_INFEN);
$fsr = read_fsr();
die("failed to unset INFEN bit in FSR") if (bit_set($fsr, FSR_INFEN));
}
}
sub enable_infen {
my $fsr = read_fsr();
# IF the info page is disabled, try to enable it
if (!bit_set($fsr, FSR_INFEN)) {
write_fsr($fsr | FSR_INFEN);
$fsr = read_fsr();
die("failed to set INFEN bit in FSR") if (!bit_set($fsr, FSR_INFEN));
}
}
sub read_page {
my ($addr, $len) = @_;
# If the FCSN line is kept active after the first data byte is read out the
# read command can be extended, the address is auto incremented and data
# continues to shift out. The internal address counter rolls over when the
# highest address is reached, allowing the complete memory to be read in one
# continuous read command.
bp_command(
bytestr('00000010') # pull CS low
);
bp_command(
bytestr('00000101'), # Write then read without CS BP SPI command
byte(0), byte(3), # #bytes to write: 3 (high8, low8)
byte(int($len/256)), # #bytes to read: $len (high8,
byte($len%256), # low8)
NRF_READ, # input: command
byte(int($addr/256)), # input: start address (high8,
byte($addr%256) # low8)
);
bp_command(
bytestr('00000011') # pull CS high
);
my $page = flush();
# TODO: the len+1 seems to be coming from a leftover 0x01 confirmation
# message the bus pirate sends out (maybe from the CS toggle?)
warn("not enough data returned") if (length($page) != $len+1);
warn("did not get message confirmation") unless substr($page, $len, 1) eq "\x01";
return substr($page, 0, $len);
}
sub read_infopage {
enable_infen();
my $infopage = read_page(0, 512);
hdump($infopage);
}
sub read_flash {
disable_infen();
die("flash readback protection enabled") if (bit_set(read_fsr(), FSR_RDISMB));
my $page = read_page(0, 512);
hdump($page);
}
sub setup {
print "Configuring SPI mode\n";
enterBinMode() or die;
enterSPIMode() or die;
# Initial SPI configuration
bp_command($speed =
bytestr('01100xxx') | bytestr('xxxxx001')); # 125 KHz
bp_command($config =
bytestr('1000wxyz') | bytestr('xxxx1xxx') # high pin = 3.3v
| bytestr('xxxxx0xx') # low clock idle phase
| bytestr('xxxxxx1x') # clock edge active to idle
| bytestr('xxxxxxx0')); # middle sample time
bp_command($peripherals =
bytestr('0100wxyz') | bytestr('xxxx1xxx') # power on
| bytestr('xxxxx0xx') # pull-ups off
| bytestr('xxxxxx1x') # AUX on
| bytestr('xxxxxxx1')); # CS high
# Trigger programming mode
bp_command($peripherals | bytestr('xxxxxx0x')); # activate RESET
usleep(1); # sleep at least 0.2 us
bp_command($peripherals | bytestr('xxxxxx1x')); # deactivate RESET
usleep(2*1000); # sleep at least 1.5 ms
}
sub main() {
print "Fetching info page:\n";
read_infopage();
## HACK ATTEMPT 1: overwrite RDISMB in the IP
#
# doesn't work
# print "Fetching info page:\n";
# read_infopage();
# my $fsr = read_fsr();
# print "FSR: ", byte2str($fsr), "\n";
# print "Setting WEN...\n";
# write_fsr($fsr | FSR_WEN);
# $fsr = read_fsr();
# print " FSR: ", byte2str($fsr), "\n";
# print "Trying to modify...\n";
# bp_command(
# bytestr('00000100'), # Write then read without SPI command
# byte(0), byte(4), # #bytes to write: 3 (high8, low8)
# byte(0), byte(0), # #bytes to read: 0 (high8, low8)
# NRF_PROGRAM, # input: command
# byte(0), byte(0x23), # input: start address (high8, low8)
# byte(0xFF)
# ) || die;
# print "Fetching info page:\n";
# read_infopage();
## HACK ATTEMPT 2: overwriting RDISMB does something funky with the FSR
#
# doesn't work
# $fsr = read_fsr();
# print "FSR: ", byte2str($fsr), "\n";
# print "Setting RDISMB...\n";
# write_fsr($fsr | ~FSR_RDISMB);
# $fsr = read_fsr();
# print " FSR: ", byte2str($fsr), "\n";
# # INFEN and WEN are on now?
# print "Trying to modify...\n";
# bp_command(
# bytestr('00000100'), # Write then read without SPI command
# byte(0), byte(4), # #bytes to write: 3 (high8, low8)
# byte(0), byte(0), # #bytes to read: 0 (high8, low8)
# NRF_PROGRAM, # input: command
# byte(0), byte(0x23), # input: start address (high8, low8)
# byte(0xFF)
# ) || die;
# $fsr = read_fsr();
# print " FSR: ", byte2str($fsr), "\n";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment