Created
January 24, 2015 20:25
-
-
Save maleadt/ef26a7d567b8a9d27534 to your computer and use it in GitHub Desktop.
Accessing nRF24LE1 PROG commands using the Bus Pirate's binary SPI mode.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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