http://qs321.pair.com?node_id=1118676

Recently I bought a Real Time Clock module to attach to a Raspberry Pi using i2c. Getting the RTC up and running was pretty straight forward, but then I noticed that the board included an Atmel AT24C32 eeprom.

Despite extensive searches I could not find information on accessing the eeprom. I already had the HiPi::BCM2835::I2C module installed, but again the available documentation was limited, but in the end I produced two scripts for reading and writing to the eeprom, using its sequential 32 byte read and write function. This allows sequential reads or writes for up to 32 bytes from a page address.

As a 'less than Perl Monks novice' I humbly place the following two scripts here for others who want to use the eeproms on their RTC modules. Any advice on improving my code would be welcome

Write to eeprom

The script must be called as root, but permissions are put back to a normal user in the script. The two variables $user and $group need to be entered, and if necessary the eeprom address on the i2c bus should be changed - it is currently 0x57, and is on the i2c-1 bus (change to i2c-0 for rev. 1 Raspberry Pi's)

The write program requires a start address after the -a parameter (there is no default) and input can be piped to the script, entered as text in quotes after a -t parameter, or put as a path/filename after -t for a text file to be used as source

Example calls

To send text from a file to the eeprom starting at address decimal 128, call it like this

writeI2cEeprom.pl -a128 -t"/path/to/file/text.txt"

This pipes the date string to address hex aa0

date | writeI2cEeprom.pl -a0xaa0

Write to eeprom code (improved commenting - thanks to roboticus)

#!/usr/bin/perl # # writeI2cEeprom.pl # Version 1.01 # # 05 March 2015 # # A script to write to the AT24C32 eeprom often found on # real time clock boards such as the DS3231. # The RTC and eeprom are attached to the i2c bus # For newer Raspberry Pi's this is bus #1 # and the eeprom is at address 0x57 (change these as required) # # Data for the first page to be written is tested for a 32 byte bounda +ry # if the data does not start on or end on a 32 byte boundary, the pag +e # is first read, then required sections are replaced before # writing the page back to the eeprom. # Blocks of data starting and ending on 32 byte boundaries are written + 'as is', # and the last page is just written up to the end of the source data. # # The AT24C32 uses 12 address bits - with max address 0xFFF # The datasheet states that when used at 3.3 volts, # the i2c bus maximum speed is 100kHz # # Parameters: # -a the start address for the write (mandatory) # in decimal or hex in the format 0xFFF # -t the text to write or a filename for a file containing the text (o +ptional) # if -t is not used (or is empty), standard input is used as the da +ta source # this allows data to be piped to this script and written to eeprom # e.g. date | writeI2cEeprom.pl -a 0 will write the standard date/ +time string # to the eeprom starting at address 0 # # The script must be called as root, but lowers permissions once # the i2c object has been created # use Getopt::Std; use HiPi::Utils; use HiPi::BCM2835::I2C qw( :all ); use File::Slurp; # use strict; # # get the address (-a) and text(-t) parameters getopt('at:'); our ( $opt_a, $opt_t ); # # setup regular user & group id's for lowering permissions my $user = '<username>'; my $group = '<group>'; # #setup bus number, EEPROM address on i2c bus and max eeprom address my $i2cBus = BB_I2C_PERI_1; my $i2cAddr = 0x57; my $eeMax = 0xFFF; # # delay required after each write (milliseconds) my $delay = 3; # # create i2c device object my $objI2c = HiPi::BCM2835::I2C->new( peripheral => $i2cBus, address => $i2cAddr ); # HiPi::Utils::drop_permissions_name( $user, $group ); # $objI2c->set_baudrate( $i2cBus, BB_I2C_CLOCK_100_KHZ ); # # Test & handle parameters # -a address data - must be present - does not default to 0! if ( $opt_a eq "" ) { # error out if no address exit 1; } elsif ( $opt_a > $eeMax ) { # start address exceeds size of eeprom exit 2; } # # -t is text or name of file with text or if neither it can be # standard input from a pipe my @Data; # array to hold input data if ( $opt_t eq "" ) { # need to timeout in case there is no data from standard input # ( and no -t parameter or no text attached to -t) $SIG{ALRM} = sub { die "timeout" }; eval { alarm(3); # get any input from a pipe (<> is standard input) my @dataIn = <>; # split each string of data into bytes foreach (@dataIn) { push( @Data, split( '', $_ ) ); } alarm(0); }; } # # if no piped data, test for file, or use data passed as text if ( !scalar(@Data) ) { if ( -f $opt_t ) { # file exists - read it into array @Data = split( "", read_file($opt_t) ); } else { # use -t as the text to be written and put it into array @Data = split( "", $opt_t ); } } # # make sure we have got some data - from somewhere! if ( !scalar(@Data) ) { exit 3; } # # convert data bytes to numeric values (decimal) my @DataCodes; foreach (@Data) { push @DataCodes, ord($_); } # # length of data my $dataLen = scalar(@DataCodes); # # chop data if it would exceed the last eeprom address # and potentially wrap round to 0x000 if ( $opt_a + $dataLen > $eeMax ) { $dataLen = $dataLen - ( $opt_a + $dataLen - $eeMax ); } # # convert start address if hex value used if ( substr( $opt_a, 0, 2 ) == "0x" ) { $opt_a = hex($opt_a); } # # create the first page start address and the offset from the page bou +ndary # addresses are 12 bits and pages are at 32 byte boundaries my $pageStAddr = $opt_a & 0b0000111111100000; my $dataOfst = $opt_a - $pageStAddr; # # calculate number of 32 byte pages that have to be written # (have to account for addresses that start after a 32 byte boundary) my $pages = int( ( $dataOfst + $dataLen ) / 33 ) + 1; # # set initial data pointer my $dataPtrSt = 0; # # test if start of first block of data is on a 32 byte boundary my $fbSSt; if ( $dataOfst != 0 ) { $fbSSt = 0; } else { $fbSSt = 1; } # # test if end of first block of data is before the next 32 byte bounda +ry my $fbESt; if ( ( $dataOfst + $dataLen ) >= 32 ) { $fbESt = 1; } else { $fbESt = 0; } # # loop through the blocks of data, put data into array for write sub. my @WriteEE; for ( my $n = 1 ; $n <= $pages ; $n++ ) { # handle first and last blocks differently if ( $n == 1 ) { # first page - test status (complete 32 bytes of data or not) if ( $fbSSt && $fbESt ) { # a complete page, so write page 'as is' # use first 32 bytes of data @WriteEE = @DataCodes[ $dataPtrSt .. 31 ]; # write data &WriteEE( $pageStAddr, @WriteEE ); # increment data pointer $dataPtrSt = $dataPtrSt + 32; } else { # an incomplete page calculate length of data my $dataEnd; if ( $dataLen + $dataOfst >= 31 ) { # data extends to end of page or beyond $dataEnd = 32; } else { # data does not extend to end of page $dataEnd = $dataLen + $dataOfst; } # read in existing 32 bytes of data my @ReadEE = &ReadEE($pageStAddr); # merge new data into existing data for ( my $i = 0 ; $i < 32 ; $i++ ) { if ( ( $i < $dataOfst ) || ( $i >= $dataEnd ) ) { # haven't reached start of new data # or beyond end of it, so use existing data @WriteEE[ $i ] = @ReadEE[ $i ]; } else { # use new data @WriteEE[ $i ] = @DataCodes[ $i - $dataOfst ]; } } # write merged data &WriteEE( $pageStAddr, @WriteEE ); # increment data pointer $dataPtrSt = 32 - $dataOfst; } } elsif ( $n == $pages ) { # last page - use remaining data (it can't be more than 32 byt +es) @WriteEE = @DataCodes[ $dataPtrSt .. ( $dataLen - 1 ) ]; # write data &WriteEE( $pageStAddr, @WriteEE ); } else { # all other pages # just use next 32 bytes of data and write 'as is' @WriteEE = @DataCodes[ $dataPtrSt .. ( $dataPtrSt + 31 ) ]; # write data &WriteEE( $pageStAddr, @WriteEE ); # increment data pointer $dataPtrSt = $dataPtrSt + 32; } # increment page address after every write $pageStAddr = $pageStAddr + 32; # delay between writes to allow eeprom to complete the write $objI2c->delay($delay); } # exit 0; # ###################################################################### +### # + # # Subroutines & Functions + # # + # ###################################################################### +### # ###################################################################### +### # + # # Read a 32 byte block + # # + # ###################################################################### +### # sub ReadEE { # get page address (my $readAddr) = @_; my $addrMSB = int( $readAddr / 256 ); my $addrLSB = $readAddr - ( $addrMSB * 256 ); # write page address $objI2c->bus_write( $addrMSB, $addrLSB ); # Read 32 byte page my @Read = $objI2c->i2c_read(32); return @Read; } # ###################################################################### +### # + # # Write a 32 byte block + # # + # ###################################################################### +### # sub WriteEE { # get page address and data ( my $writeAddr, my @Write ) = @_; # my $addrMSB = int( $writeAddr / 256 ); my $addrLSB = $writeAddr - ( $addrMSB * 256 ); # # write-> page address followed by data # any more than 32 bytes are wrapped round to start address # @Write should never be more than 32 bytes anyway $objI2c->bus_write( $addrMSB, $addrLSB, @Write ); } #

Read from eeprom

The script to read from the eeprom is simpler. It requires a start address after the -a parameter (defaults to 0) and a length of data to read after the -l parameter (defaults to 32). Optionally a -o parameter takes a path/filename to receive output. A valid path is required, but if the file is not present it is created. Existing files are appended to. With no -o option the output is to screen, formatted with the eeprom hex addresses, if a -h parameter is included the data is displayed in hex, rather than the default character display.

Revised Read from eeprom code based on feedback from roboticus

#!/usr/bin/perl # # readI2cEeprom.pl # Version 1.01 # 04 March 2015 # # A script to read from the AT24C32 eeprom (4K bytes) # attached to the Raspberry Pi's i2c bus #1 at address 0x57 # The AT24C32 uses 12 address bits - with max address 0xFFF # The datasheet states that when used at 3.3 volts, # the i2c bus maximum speed is 100kHz # # Two parameters must be passed to this script: # -a the start address for the read (decimal or hex in the format 0xFF +F) # -l the number of bytes to read # -o the output filename can be omitted. If present and contains a val +id path # with a filename, the output will go to the named file, creating i +t # if necessary. # If -o is not present or is empty, the output is displayed on screen # with address information. # A fourth parameter '-h' is optional # If included, -h causes the screen output to display as # hex characters instead of 'ascii' characters # # The script must be called as root, but lowers permissions once # the i2c object has been created # use Getopt::Std; use HiPi::Utils; use File::Basename; use HiPi::BCM2835::I2C qw( :all ); # use strict; # # get the parameters (-a, -l, -o) getopt('alo:'); # our ( $opt_a, $opt_l, $opt_o, $opt_h, $opt_v ); # # setup regular user & group id's for lowering permissions my $user = '<username>'; my $group = '<group>'; # #setup bus number, eeprom address on i2c bus and max eeprom address my $i2cBus = BB_I2C_PERI_1; my $i2cAddr = 0x57; my $eeMax = 0xFFF; # # character to print on screen in place of codes <32 decimal my $repl = "~"; # # Test & handle parameters if ( $opt_a > $eeMax ) { # address exceeds last eeprom address so exit exit 1; } elsif ( $opt_a eq "" ) { # no address information - defaults to 0 $opt_a = 0; } # if ( $opt_l eq "" ) { # no length information - defaults to 32 (1 page) $opt_l = 32; } # # create i2c device object my $objI2c = HiPi::BCM2835::I2C->new( peripheral => $i2cBus, address => $i2cAddr ); # HiPi::Utils::drop_permissions_name( $user, $group ); # $objI2c->set_baudrate( $i2cBus, BB_I2C_CLOCK_100_KHZ ); # # test -o parameter # first set flag for printing formatted output with hex addresses my $outFlag = 1; if ( !$opt_o == undef ) { # -o is present # set flag to stop address formatted output $outFlag = 0; # get path and filename my $path = dirname $opt_o; my $file = basename $opt_o; # test for existence of path if ( -d $path ) { # path exists - so test for output file if ( !-f $file ) { # file does not exist, so create it open( OFILE, ">", $opt_o ) || die "Could not open file $fi +le"; } else { # file does exist, so open it and append data open( OFILE, ">>", $opt_o ) || die "Could not open file $f +ile"; } } else { # path does not exist - exit with error 2 exit 2; } } # # convert start address if hex value used if ( substr( $opt_a, 0, 2 ) == "0x" ) { $opt_a = hex($opt_a); } # # address manipulations - opt_a contains requested start address my $addrPB = ( $opt_a & 0b0000111111100000 ); # 32 byte page + boundary my $addrMSB = ( $addrPB & 0b0000111100000000 ) >> 8; # MSB (upper n +ibble always zero) my $addrLSB = ( $addrPB & 0b0000000011111111 ); # LSB (at page + boundary) my $addrRA = ( $opt_a & 0b0000111111100000 ) >> 4; # row address +for output table # # calculate offset from start of first page boundary to start address +requested my $offst = $opt_a - $addrPB; # # calculate number of bytes to read # length requested + offset from page boundary to first byte requeste +d my $numbBytes = $opt_l + $offst; # # crop number of bytes to read if read exceeds max eeprom address if ( $opt_a + $opt_l > $eeMax ) { $numbBytes = $numbBytes - ( $opt_a + $opt_l - $eeMax -1 ); } # # write base address for read (at page boundary) $objI2c->bus_write( $addrMSB, $addrLSB ); # # read number of bytes into array my @RdData = $objI2c->i2c_read($numbBytes); # if ($outFlag) { # print formatted output with hex addresses if ( !$opt_h ) { # display data as characters - 32 bytes per row print "\n 32 bytes of data per row\n"; print " 0--------------f0--------------f\n"; for ( my $i = 0 ; $i < $numbBytes ; $i++ ) { if ( $i % 32 == 0 ) { # print row start address and increment it printf( "%02x ", $addrRA ); $addrRA = $addrRA + 2; } if ( $i >= $offst ) { # print data if at or after start address (uses offset + value) # characters <32 replaced by a printable character if ( $RdData[ $i ] < 32 ) { print $repl; } else { printf "%s", chr( $RdData[ $i ] ); } } else { # haven't reached start address - so print blanks print " "; } if ( ( $i + 1 ) % 32 == 0 ) { # every 32 bytes start new row print "\n"; } } print "\n\n"; } else { # display data as hex pairs, 16 bytes per line # print hex column headers print "\n 0 1 2 3 4 5 6 7 8 9 a b c d e f\n" +; for (my $i = 0 ; $i < $numbBytes ; $i++ ) { if ( $i % 16 == 0 ) { # print row start address and increment it printf( "%02x ", $addrRA ); $addrRA = $addrRA + 1; } if ( $i >= $offst ) { # print data if at or after start address (uses offset +) printf "%02x ", $RdData[ $i ]; } else { # haven't reached start address - so print position ma +rks print ".. "; } if ( ( $i + 1 ) % 16 == 0 ) { # every 16 bytes start new row print "\n"; } } print "\n\n"; } } else { # print to file for ( my $i = 0 ; $i < $numbBytes ; $i++ ) { if ( $i >= $offst ) { # print data if at or after start address (uses offset) print OFILE chr( $RdData[ $i ] ); } } } # exit 0;

anita2R

Replies are listed 'Best First'.
Re: EEPROM on i2c Real Time Clock Modules
by roboticus (Chancellor) on Mar 04, 2015 at 01:29 UTC

    anita2R:

    Your code looks pretty nice, I'll have to try it out and check the E2PROM on some boards I have. I took a quick look at your code and have a couple suggestions (in no particular order):

    • You can simplify this statement: if ( ( $i + 1 ) / 16 == int( ( $i + 1 ) / 16 ) ) by using the modulo operator (%)--it does an integer division and gives you the remainder. Using it, you could write your statement as:
      if ( ($i+1)%16 == 0).
    • Generally you should declare your variables close to where you use them, in the innermost scope that makes sense. That limits the amount of code you need to review if you're looking for how the variable is used. This is causing you to double-comment certain things, kinda like:
      Line 38:
      my ($i2cBus, $i2cAddr);        # bus ID number & device address
      Line 54:
      #setup bus number, EEPROM address and max eeprom size in bytes $i2cBus = BB_I2C_PERI_1;

      If you declare the variable at the second point, you need only comment it once.

    • You're also commenting on things that need no comment. In the code below the comment we can see that it's setting the baud rate to 100kHz (due to the well-named value). In fact, with well-named variables, many comments are superfluous.
      # set baudrate to 100kHz (AT24C32 at 3.3v, max speed = 100kHz)) $objI2c->set_baudrate( $i2cBus, BB_I2C_CLOCK_100_KHZ );

      The comment about the part's limitations is something I'd put in the comment. If you decide later to use 50kHz because some flakey parts, you don't have to update your comment.

    Keep in mind that these are just some trivial suggestions. If one of my colleagues gave me this code, I'd be perfectly happy with it going into production as it is (assuming that it does what it's supposed to). If you review some of my posts here, you'll notice that I frequently post code that could do with improvement as well.

    ...roboticus

    When your only tool is a hammer, all problems look like your thumb.

      roboticus

      Thank you for taking the time to comment on my script, particularly your 'comments on comments'. I certainly take you point about declaring variables close to where they are first used. It would make it easier to understand and remove my double commenting.

      Thanks also for pointing out an alternative to my clunky code for defining when the output reached a boundary point. I should have worked on that a bit more ... the structure I used occurs four times in that script!

      Regards

      anita2R

Re: EEPROM on i2c Real Time Clock Modules
by choroba (Cardinal) on Aug 13, 2023 at 08:10 UTC
    if ( substr( $opt_a, 0, 2 ) == "0x" ) { $opt_a = hex($opt_a); }

    Are you sure you want to use == here instead of eq? The former compares numbers, the latter compares strings. As written, setting $opt_a to 'b0011' would turn it into 720913. I don't think that's expected.

    map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
Re: EEPROM on i2c Real Time Clock Modules
by jwkrahn (Abbot) on Aug 13, 2023 at 01:27 UTC

    A problem with writeI2cEeprom.pl

    45 getopt('at:'); 46 our ( $opt_a, $opt_t ); ... 75 } elsif ( $opt_a > $eeMax ) { ... 126 if ( $opt_a + $dataLen > $eeMax ) { 127 $dataLen = $dataLen - ( $opt_a + $dataLen - $eeMax ); 128 } 129 # 130 # convert start address if hex value used 131 if ( substr( $opt_a, 0, 2 ) == "0x" ) { 132 $opt_a = hex($opt_a); 133 }

    If $opt_a contains, for example '0xAB', then lines 75, 126 and 127 will not work correctly.

    Lines 130 to 133 should be moved before $opt_a is used in a mathematical context.

    Some problems with readI2cEeprom.pl

    36 getopt('alo:'); 37 # 38 our ( $opt_a, $opt_l, $opt_o, $opt_h, $opt_v ); ... 53 if ( $opt_a > $eeMax ) { ... 102 # convert start address if hex value used 103 if ( substr( $opt_a, 0, 2 ) == "0x" ) { 104 $opt_a = hex($opt_a); 105 }

    Same problem as with the other file. Lines 102 to 105 should be moved before $opt_a is used in a mathematical context.

    79 if ( !$opt_o == undef ) {

    Incorrect use of undef. That should be if ( !defined $opt_o ) {

    Naked blocks are fun! -- Randal L. Schwartz, Perl hacker
      That should be if ( !defined $opt_o ) {

      You have negated the logic. It should actually be the even simpler if (defined $opt_o) {


      🦛

Re: EEPROM on i2c Real Time Clock Modules
by stevieb (Canon) on Aug 13, 2023 at 17:31 UTC