In Castlevania II, I made each save block 256 bytes long, so to generate a pointer to save number X you would use X+$60 in the high byte of the offset and $00 in the low byte of the offset.
For the checksum I used CRC16 with small tweaks.
The save-blocks do not really fully populate the 256 bytes though; this fact is used for other purposes.
; CRC16 by Greg Cook.
EOR Checksum+1 ; A contained the data
STA Checksum+1 ; XOR it into high byte
LSR ; right shift A 4 bits
LSR ; to make top of x^12 term
LSR ; ($1...)
TAX ; save it
ASL ; then make top of x^5 term
EOR Checksum+0 ; and XOR that with low byte
STA Checksum+0 ; and save
TXA ; restore partial term
EOR Checksum+1 ; and update high byte
STA Checksum+1 ; and save
ASL ; left shift three
ASL ; the rest of the terms
ASL ; have feedback from x^12
TAX ; save bottom of x^12
ASL ; left shift two more
ASL ; watch the carry flag
EOR Checksum+1 ; bottom of x^5 ($..2.)
STA Checksum+1 ; save high byte
TXA ; fetch temp value
ROL ; bottom of x^12, middle of x^5!
EOR Checksum+0 ; finally update low byte
LDX Checksum+1 ; then swap high and low bytes
.macro QuickPermutateChecksum memword
; Checksum += int16
The save-data itself consists of a stream of memory-address + data commands. This allows for both backward and future compatibility between saves and patch versions, as long as variables are never relocated to another memory address (which is almost always the case when you're hacking a cooked game, i.e. a compiled & released game).
; Save data format:
; 16 bytes password data
; Followed by checksum (16-bit)
; If 0, record ends here
; Followed by records:
; nnnnnaaa aaaaaaaa ...
; n = number of bytes-1 (0-31, coding for 1-32 bytes)
; a = address to save ($000-$7FF)
; word $0000 = end of records
; For up to 256 bytes.
The reason for the odd order of checksum + records + terminator is backward compatibility with Matrixz's patch that only saved the 16-byte password data and nothing else.
Certain security checks ensure that even a carefully crafted malicious savestate will not cause too much mayhem, such as by writing into the stack.
To report on screen what a save contains, the save is simply extracted and the running game state is then inspected. To ensure that the running game state is not permanently damaged when the save is inspected, the current game state is first "saved" into a backup RAM location (rather than SRAM) when the save/load screen is entered. The backup is restored right before when "save" is selected, and when exiting the screen without using "load".
For checking the amount of installed SRAM, I used a sequence of reads & writes of four consecutive bytes at $60FF, $63FF, $67FF, $6FFF and $7FFF if I recall correctly; writing a sequence of random bytes and verifying whether they read back correctly and that they are not mirrored in a previously checked location. I probably also restored the bytes to their original values after testing them.
I'm aware that there would usually be either none or the full 8 kilobytes, but I wrote the checking of installed amount with homebrew cartridges in mind where you might make a working cart even if you only had a 2-kilobyte SRAM chip at your disposal, as I do.