#!/usr/bin/perl

use strict;
use warnings;
use 5.010;

use FindBin;
use Getopt::Long;
use List::Util qw( first );

use Device::AVR::Info;

my %current;

# TODO: Find a reliable way to get this dir
my $SHAREDIR = "$FindBin::Bin/../share";
my $ATDFDIR = "$SHAREDIR/Device-AVR-Info/packs/atdf";
undef $ATDFDIR unless -d $ATDFDIR;

sub usage
{
   my ( $err ) = @_;

   my ( $basename ) = $0 =~ m{/([^/]+)$};
   ( $err ? \*STDERR : \*STDOUT )->print( <<"EOF" );
Usage: $basename [ATDF-FILE] FUSES...

Options:
   -h, -help              - display this help

   -v, --verbose          - print more verbose messages

   -p, --part NAME        - specify AVR part name as an alternative to giving
                            the ATDF-FILE path

   -f, --fuse FUSE=VALUE  - preset the given fuse value
                            FUSE:  lfuse | hfuse | efuse
                            VALUE: 123 | 0456 | 0x78
EOF

   exit $err;
}

my $atdfpath;

GetOptions(
   'h|help' => sub { usage(0) },

   'v|verbose' => \my $VERBOSE,

   'p|part=s' => sub {
      unless( defined $ATDFDIR ) {
         print STDERR "Cannot specify part by name without a defined ATDFDIR\n";
         exit 1;
      }
      my $partname = $_[1];
      $partname = "ATmega\U$1" if $partname =~ m/^(?:atmega|m)(.*)$/i;
      $partname = "ATtiny\U$1" if $partname =~ m/^(?:attiny|t)(.*)$/i;

      $atdfpath = "$ATDFDIR/$partname.atdf";
      unless( -f $atdfpath ) {
         print STDERR "No ATDF file found at $atdfpath\n";
         exit 1;
      }
   },

   'f|fuse=s' => sub {
      my ( $fuse, $val ) = $_[1] =~ m/^(.*?)=(.*)$/ or die "Unable to parse --fuse\n";
      $val = oct $val if $val =~ m/^0/;
      $current{lc substr $fuse, 0, 1} = $val;
   },
) or usage(1);

$atdfpath //= shift @ARGV;

defined $atdfpath
   or usage(1);

my $avr = Device::AVR::Info->new_from_file( $atdfpath )
   or die "Can't load\n";

my %want;
foreach my $arg ( @ARGV ) {
   my ( $name, $val ) = $arg =~ m/^(\w+)=(.*)$/ or
      die "Unable to parse '$arg'\n";
   $want{ uc $name } = $val;
}

my $fuses = $avr->peripheral( 'FUSE' );
$fuses->regspace->name eq 'fuses' or
   die "Expected FUSES peripheral to exist in the 'fuses' address space\n";

my @output;

foreach my $reg ( $fuses->registers ) {
   my $regname = lc substr( $reg->name, 0, 1 );

   my $mask = 0;
   my $value = 0;

   my $current = $current{$regname};
   $current //= $reg->initval;

   foreach my $field ( $reg->bitfields ) {
      my $fusename = $field->name;
      my @values = $field->values;

      if( defined( my $want = delete $want{$fusename} ) ) {
         goto list_values if $want eq "?";

         if( @values ) {
            my $chosen = first { $_->name eq $want } @values;
            defined $chosen or
               die "Unrecognised value for $fusename\n";

            $value |= $chosen->value;
         }
         else {
            $value |= $field->mask if $want;
         }
      }
      elsif( defined( $current ) ) {
         my $currentval = $current & $field->mask;

         if( @values ) {
            my $chosen = first { $_->value == $currentval } @values;
            unless( defined $chosen ) {
               warn "Unrecognised value for $fusename\n";
               next;
            }

            print "using $fusename=${\ $chosen->name } - ${\ $chosen->caption }\n" if $VERBOSE;
         }
         else {
            printf "using %s=%s\n", $fusename, $currentval ? 1 : 0  if $VERBOSE;
         }

         $value |= $currentval;
      }
      else {
list_values:
         print STDERR "Unspecified value for fuse $fusename\n";
         print STDERR "Possible values are:\n";

         if( @values ) {
            print STDERR "  ${\ $_->name } - ${\ $_->caption }\n" for @values;
         }
         else {
            print STDERR "  1\n";
            print STDERR "  0\n";
         }

         exit 1;
      }

      $mask |= $field->mask;
   }

   # Set unused bits to 1
   $value |= ( 255 ^ $mask );

   # avrdude format -U lfuse:w:0x62:m 
   push @output, sprintf "-U %sfuse:w:0x%02X:m", $regname, $value;
}

print join( " ", @output ) . "\n";

__END__

=head1 NAME

F<avr-fuses> - a commandline fuse value calculator for F<AVR> microcontrollers

=head1 SYNOPSIS

   $ avr-fuses --part m328
   -U efuse:w:0xFF:m -U hfuse:w:0xD9:m -U lfuse:w:0x62:m

   $ avr-fuses --part m328 CKDIV8=1
   -U efuse:w:0xFF:m -U hfuse:w:0xD9:m -U lfuse:w:0xE2:m

=head1 DESCRIPTION

This program interprets the contents of definition files ("ATDF files")
provided by Microchip (formerly Atmel) that describe the configuration fuses
of AVR microcontrollers.

Individual fuses may be named on the commandline, each giving a value in the
form C<NAME=VALUE>. These will be applied on top of the default values for the
chosen part.

The program ends by printing new values for the fuse configuration registers,
in a form suitable to paste directly into an F<avrdude> commandline. This may
be useful in a shell fragment, such as

   $ avrdude -c avrisp -p m328 $(avr-fuses -p m328 CKDIV8=1)

=head1 OPTIONS

=head2 -h, --help

Displays a summary of the commandline and options

=head2 -v, --verbose

Describe the meaning of each individual named fuse while parsing it

=head2 -p, --part NAME

Gives the part name of the chosen microcontroller. Parts may be specified
in the following ways:

   ATmega328PB
   atmega328pb
   m328pb

   ATtiny84A
   attiny84a
   t84a

Specfically, these are the same forms as recognised by F<avr-gcc>'s C<-mmcu>
option and F<avrdude>'s C<-p> option, for convenience in Makefiles and build
scripts.

=head2 -f, --fuse FUSE=VALUE

Supplies a value from one of the fuse configuration registers. This is useful
combined with the C<--verbose> option to decode values read from an AVR chip.

=head1 AUTHOR

Paul Evans <leonerd@leonerd.org.uk>
