1
0
mirror of https://github.com/Optiboot/optiboot.git synced 2025-08-17 21:41:03 +03:00

Better (?) example of flash write from an Arduino sketch

This commit is contained in:
WestfW
2020-11-05 00:20:24 -08:00
parent 6aa5871762
commit 2c558a3f7d
5 changed files with 858 additions and 0 deletions

View File

@@ -0,0 +1,241 @@
/*------------- Optiboot flasher example for the MiniCore ------------------|
| |
| Created May 2016 by MCUdude, https://github.com/MCUdude |
| Based on the work done by Marek Wodzinski, https://github.com/majekw |
| Released to public domain |
| |
| This is example how to use optiboot.h together with Optiboot |
| bootloader to write to FLASH memory by application code. |
| |
| IMPORTANT THINGS: |
| - All flash content gets erased after each upload cycle |
| - Buffer must be page aligned (see declaration of flash_buffer) |
| - Interrupts must be disabled during SPM |
| - Writing to EEPROM destroys temporary buffer |
| - You can write only once into one location of temporary buffer |
| - Only safe and always working sequence is erase-fill-write |
| - If you want to do fill-erase-write, you must put code in NRWW |
| and pass data!=0 for erase. It's not easy, but possible. |
| |
| WRITE SEQUENCE - OPTION 1 (used in this example) |
| 1. Erase page by optiboot_page_erase |
| 2. Write contents of page into temporary buffer by optiboot_page_fill |
| 3. Write temporary buffer to FLASH by optiboot_page_write |
| |
| WRITE SEQUENCE - OPTION 2 (works only for code in NRWW) |
| 1. Write contents of page into temporary buffer by optiboot_page_fill |
| 2. Erase page by optiboot_page_erase (set data to NOT zero) |
| 3. Write temporary buffer to FLASH by optiboot_page_write |
|-------------------------------------------------------------------------*/
// optiboot.h contains the functions that lets you read to
// and write from the flash memory
#include "optiboot.h"
#include "simpleParser.h"
// Define the number of pages you want to write to here (limited by flash size)
// these are flash pages "internal" to the sketch.
#define NUMBER_OF_PAGES 8
// Define your termination and blank character here
const char terminationChar = '@';
uint8_t charBuffer;
int8_t menuOption;
int16_t pageNumber;
char returnToMenu;
// The temporary data (data that's read or is about to get written) is stored here
uint8_t ramBuffer[SPM_PAGESIZE + 1];
// This array allocates the space you'll be able to write to
const uint8_t flashSpace[SPM_PAGESIZE * NUMBER_OF_PAGES] __attribute__ (( aligned(SPM_PAGESIZE) )) PROGMEM = {
"This some default content stored on page one"
};
void setup()
{
// Initialize serial
Serial.begin(115200);
while (!Serial)
; // possibly wait for USB Serial to enumerate.
}
simpleParser<80> ttycli(Serial);
void loop()
{
byte clicmd;
// Print main menu
Serial.println();
Serial.print(F("|------------------------------------------------|\n"
"| Welcome to the Optiboot flash writer example! |\n"
#ifdef USE_NVMCTRL
"| Running on mega0/xTiny with NVMCTRL support. |\n"
#else
"| Running on traditional AVR with SPM support. |\n"
#endif
"| Each flash page is "));
Serial.print(SPM_PAGESIZE);
Serial.print ( F(" bytes long. |\n"));
Serial.print(F("| There are "));
Serial.print(NUMBER_OF_PAGES);
Serial.print (F( " pages that can be read/written to. |\n"));
Serial.print(F("| Total assigned flash space: "));
Serial.print(NUMBER_OF_PAGES * SPM_PAGESIZE);
Serial.print(F(" bytes. |\n"
"| Change the NUMBER_OF_PAGES constant to |\n"
"| increase or decrease this number. |\n"
"| |\n"
"| What do you want to do? |\n"
"| Read - read current flash content |\n"
"| Write - Write to flash memory |\n"
"|------------------------------------------------|\n\n"));
uint16_t counter = 0;
static const char PROGMEM cmdStrings[] = "read write ?";
enum { CMD_READ = 0, CMD_WRITE, CMD_HELP };
Serial.print("Cmd: ");
ttycli.reset();
ttycli.getLineWait();
clicmd = ttycli.keyword(cmdStrings); // look for a command.
switch (clicmd) {
case CMD_READ:
// Read flash option selected
if (ttycli.eol()) {
Serial.print(F("What page number do you want to read? Page: "));
ttycli.reset();
ttycli.getLineWait();
}
pageNumber = ttycli.number();
if (pageNumber < 0 || pageNumber > NUMBER_OF_PAGES) {
Serial.print(F("\nPlease enter a valid page between 1 and "));
Serial.print(NUMBER_OF_PAGES);
Serial.println(F(".\nThe number of pages can be extended by changing NUMBER_OF_PAGES constant"));
return; // restart parsing
}
#if 0
if (pageNumber > _etext && (pageNumber & (SPM_PAGESIZE-1)) == 0) {
if (pageNumber > 0)
Serial.println(pageNumber);
#endif
// READ SELECTED PAGE AND STORE THE CONTENT IN THE ramBuffer ARRAY
// flash_buffer is where the data is stored (contains the memory addresses)
// ramBuffer is where the data gets stored after reading from flash
// pageNumber is the page the data is read from
// blankChar is the character that gets printed/stored if there are unused space (default '.')
// use optiboot_readPage(flashSpace, ramBuffer, pageNumber) if you don't want blank chars
if (pageNumber == 0) // Read all pages
{
Serial.println(F("\nAll flash content:"));
for (uint16_t page = 1; page < NUMBER_OF_PAGES + 1; page++)
{
Serial.print(F("Page "));
Serial.print(page);
Serial.println(F(": "));
optiboot_readPage(flashSpace, ramBuffer, page);
for (int i = 0; i < SPM_PAGESIZE; i += 16) {
DumpHex(&ramBuffer[i], 16);
Serial.println();
}
}
}
else // Read selected page
{
// Print page content
Serial.print(F("\nContent of page "));
Serial.print(pageNumber);
Serial.println(F(":"));
optiboot_readPage(flashSpace, ramBuffer, pageNumber);
for (int i = 0; i < SPM_PAGESIZE; i += 16) {
DumpHex(&ramBuffer[i], 16);
Serial.println();
}
}
break;
case CMD_WRITE:
// Write flash option selected
if (ttycli.eol()) {
Serial.print(F("What page number do you want to Write? Page: "));
ttycli.reset();
ttycli.getLineWait();
}
pageNumber = ttycli.number();
if (pageNumber < 1 || pageNumber > NUMBER_OF_PAGES) {
Serial.print(F("\nPlease enter a valid page between 1 and "));
Serial.print(NUMBER_OF_PAGES);
Serial.println(F(".\nThe number of pages can be extended by changing NUMBER_OF_PAGES constant"));
return; // restart parsing
}
// Print prompt to enter some new characters to write to flash
Serial.print(F("Please type the characters you want to store (max "));
Serial.print(SPM_PAGESIZE);
Serial.println(F(" characters)"));
Serial.print(F("End with a line containing only '"));
Serial.write(terminationChar);
Serial.println(F("' character:"));
// Get all characters from the serial monitor and store it to the ramBuffer
memset(ramBuffer, 0, sizeof(ramBuffer));
counter = 0;
do {
ttycli.reset();
ttycli.getLineWait();
if (ttycli.nextChar() == terminationChar) {
break;
}
char *p = ttycli.restOfLine();
byte c;
do {
c = *p++;
if (c == 0)
break;
ramBuffer[counter++] = c;
if (counter >= SPM_PAGESIZE) {
Serial.println(F("Page is full"));
break;
}
} while (1);
} while (counter < SPM_PAGESIZE);
Serial.println(F("\n\nAll chars received \nWriting to flash..."));
// WRITE RECEIVED DATA TO THE CURRENT FLASH PAGE
// flash_buffer is where the data is stored (contains the memory addresses)
// ramBuffer contains the data that's going to be stored in the flash
// pageNumber is the page the data is written to
optiboot_writePage(flashSpace, ramBuffer, pageNumber);
Serial.println(F("Writing finished.\nYou can now reset or power cycle the board and check for new contents!"));
confirm();
break;
case CMD_HELP:
return;
}
} // End of loop
void confirm()
{
while (true) {
Serial.println("\n--type newline to continue--");
ttycli.reset();
ttycli.getLineWait();
if (ttycli.eol()) {
return;
}
}
}

View File

@@ -0,0 +1,243 @@
/*------------------------ Optiboot header file ----------------------------|
| |
| June 2015 by Marek Wodzinski, https://github.com/majekw |
| Modified June 2016 by MCUdude, https://github.com/MCUdude |
| Modified Sep 2020 for mega0/xTiny (optiboot_x) |
| by Bill Westfield https://github.com/westfw |
| Modified Nov 2020 change the read() function to not change data |
| Released to public domain |
| |
| This header file gives possibility to use SPM instruction |
| from Optiboot bootloader memory. |
| |
| There are 5 convenient functions available here: |
| * optiboot_page_erase - to erase a FLASH page |
| * optiboot_page_fill - to put words into temporary buffer |
| * optiboot_page_write - to write contents of temporary buffer into FLASH |
| * optiboot_readPage - higher level function to read a flash page and |
| store it in an array |
| * optiboot_writePage - higher level function to write content to |
| a flash page |
| |
| For some hardcore users, you could use 'do_spm' as raw entry to |
| bootloader spm function. |
|-------------------------------------------------------------------------*/
#ifndef _OPTIBOOT_H_
#define _OPTIBOOT_H_ 1
#ifdef SPMCSR
// Mega0 does not have SPMCSR
#include <avr/boot.h>
#endif
#include "Arduino.h"
// figure out whether we have SPM or NVMCTRL
#ifdef FUSE_BOOTEND
#define USE_NVMCTRL 1
#define NVMCTRL_CMD_COPY_gc (NVMCTRL_CMD_gm+1) // one beyond existing commands.
#define SPM_PAGESIZE (PROGMEM_PAGE_SIZE)
#endif
/*
* Main 'magic' function - enter to bootloader do_spm function
*
* address - address to write (in bytes) but must be even number
* command - one of __BOOT_PAGE_WRITE, __BOOT_PAGE_ERASE or __BOOT_PAGE_FILL
* data - data to write in __BOOT_PAGE_FILL. In __BOOT_PAGE_ERASE or
* __BOOT_PAGE_WRITE it control if boot_rww_enable is run
* (0 = run, !0 = skip running boot_rww_enable)
*
*/
// 'typedef' (in following line) and 'const' (few lines below)
// are a way to define external function at some arbitrary address
typedef void (*do_spm_t)(uint16_t address, uint8_t command, uint16_t data);
typedef void (*do_nvmctrl_t)(uint16_t address, uint8_t command, uint8_t data);
/*
* Devices with more than 64KB of flash:
* - have larger bootloader area (1KB) (they are BIGBOOT targets)
* - have RAMPZ register :-)
* - need larger variable to hold address (pgmspace.h uses uint32_t)
*/
#ifdef RAMPZ
typedef uint32_t optiboot_addr_t;
#else
typedef uint16_t optiboot_addr_t;
#endif
#ifdef USE_NVMCTRL
// Mega0/xTiny/etc. Bootloader is in low memory.
const do_spm_t do_nvmctrl = (do_spm_t)((PROGMEM_START + 2) >> 1);
#else
#if FLASHEND > 65534
const do_spm_t do_spm = (do_spm_t)((FLASHEND - 1023 + 2) >> 1);
#else
const do_spm_t do_spm = (do_spm_t)((FLASHEND - 511 + 2) >> 1);
#endif
#endif
#ifndef USE_NVMCTRL
// SPM-based functions
/*
* The same as do_spm but with disable/restore interrupts state
* required to succesfull SPM execution
*
* On devices with more than 64kB flash, 16 bit address is not enough,
* so there is also RAMPZ used in that case.
*/
void do_spm_cli(optiboot_addr_t address, uint8_t command, uint16_t data) {
uint8_t sreg_save;
sreg_save = SREG; // save old SREG value
asm volatile("cli"); // disable interrupts
#ifdef RAMPZ
RAMPZ = (address >> 16) & 0xff; // address bits 23-16 goes to RAMPZ
#ifdef EIND
uint8_t eind = EIND;
EIND = FLASHEND / 0x20000;
#endif
do_spm((address & 0xffff), command, data); // do_spm accepts only lower 16 bits of address
#ifdef EIND
EIND = eind;
#endif
#else
do_spm(address, command, data); // 16 bit address - no problems to pass directly
#endif
SREG = sreg_save; // restore last interrupts state
}
// Erase page in FLASH
void optiboot_page_erase(optiboot_addr_t address) {
do_spm_cli(address, __BOOT_PAGE_ERASE, 0);
}
// Write word into temporary buffer
void optiboot_page_fill(optiboot_addr_t address, uint16_t data) {
do_spm_cli(address, __BOOT_PAGE_FILL, data);
}
//Write temporary buffer into FLASH
void optiboot_page_write(optiboot_addr_t address) {
do_spm_cli(address, __BOOT_PAGE_WRITE, 0);
}
/*
* Higher level functions for reading and writing from flash
* See the examples for more info on how to use these functions
*/
// Function to write data to a flash page
void optiboot_writePage(const uint8_t allocated_flash_space[],
uint8_t data_to_store[], uint16_t page)
{
uint16_t word_buffer = 0;
// Erase the flash page
optiboot_page_erase((optiboot_addr_t)(void*) &allocated_flash_space[SPM_PAGESIZE * (page - 1)]);
// Copy ram buffer to temporary flash buffer
for (uint16_t i = 0; i < SPM_PAGESIZE; i++)
{
if (i % 2 == 0) // We must write words
word_buffer = data_to_store[i];
else
{
word_buffer += (data_to_store[i] << 8);
optiboot_page_fill((optiboot_addr_t)(void*) &allocated_flash_space[i + SPM_PAGESIZE * (page - 1)], word_buffer);
}
}
// Writing temporary buffer to flash
optiboot_page_write((optiboot_addr_t)(void*) &allocated_flash_space[SPM_PAGESIZE * (page - 1)]);
}
#else // Newer Mega0/xTiny chips with NVMCTRL
/*
* The same as do_nvmctrl but with disable/restore interrupts state
* required to succesfull execution
*
* Currently, there are no mega0/xTint parts with more than 64k, and when there are
* they'll need extra effort beyond just RAMPZ :-(
*/
void do_nvmctrl_cli(optiboot_addr_t address, uint8_t command, uint16_t data)
{
uint8_t sreg_save;
sreg_save = SREG; // save old SREG value
asm volatile("cli"); // disable interrupts
do_nvmctrl(address, command, data); // 16 bit address - no problems to pass directly
SREG = sreg_save; // restore last interrupts state
}
// Erase page in FLASH
void optiboot_page_erase(optiboot_addr_t address)
{
// set page by writing to address.
do_nvmctrl(address + MAPPED_PROGMEM_START, NVMCTRL_CMD_COPY_gc, 0xFF);
do_nvmctrl_cli(0, NVMCTRL_CMD_PAGEERASE_gc, 0); // do actual erase
}
// Write word into temporary buffer
void optiboot_page_fill(optiboot_addr_t address, uint16_t data)
{
do_nvmctrl(address + MAPPED_PROGMEM_START, NVMCTRL_CMD_COPY_gc, data & 0xFF);
do_nvmctrl(address + MAPPED_PROGMEM_START, NVMCTRL_CMD_COPY_gc, data >> 8);
}
//Write temporary buffer into FLASH
void optiboot_page_write(optiboot_addr_t address)
{
do_nvmctrl_cli(address, NVMCTRL_CMD_PAGEWRITE_gc, 0);
}
/*
* Higher level functions for reading and writing from flash
* See the examples for more info on how to use these functions
*/
// Function to write data to a flash page
void optiboot_writePage(const uint8_t allocated_flash_space[],
uint8_t data_to_store[], uint16_t page)
{
const uint8_t *adjusted_address;
// Copy ram buffer to temporary flash buffer
for (uint16_t i = 0; i < SPM_PAGESIZE; i++)
{
adjusted_address = &allocated_flash_space[i + SPM_PAGESIZE * (page - 1)];
adjusted_address += MAPPED_PROGMEM_START;
do_nvmctrl((optiboot_addr_t)(void*) adjusted_address,
NVMCTRL_CMD_COPY_gc, data_to_store[i]);
}
do_nvmctrl_cli(0, NVMCTRL_CMD_PAGEERASEWRITE_gc, 0);
}
#endif // USE_NVMTRL
// Function to read a flash page and store it in an array (storage_array[])
// (these can be shared between old and new AVRs.)
void optiboot_readPage(const uint8_t allocated_flash_space[],
uint8_t storage_array[], uint16_t page)
{
uint8_t read_character;
for (uint16_t j = 0; j < SPM_PAGESIZE; j++)
{
read_character = pgm_read_byte(&allocated_flash_space[j + SPM_PAGESIZE * (page - 1)]);
storage_array[j] = read_character;
}
}
#endif /* _OPTIBOOT_H_ */

View File

@@ -0,0 +1,279 @@
/*
* simpleParser
* Implement a command-line parser.
* Written 2014 by Bill Westfield (WestfW)
* refactored 2020
* Released to the public domain.
*/
#include "Arduino.h"
#include "simpleParser.h"
#include <avr/pgmspace.h>
//#define DEBUG 1
#if defined(DEBUG) && DEBUG
//extern char *spbuffer;
char spbuffer[100];
#endif
/*
* Reset the line buffer
*/
void parserCore::reset ()
{
memset(buffer, 0, lineLen);
inptr = 0;
parsePtr = 0;
termchar = 0;
}
/*
* getLine
* Read a line of text from Serial into the internal line buffer.
* With echoing and editing!
* Non-blocking. Returns 0 until end-of-line seen.
*/
uint8_t parserCore::getLine ()
{
int c;
c = S->read();
switch (c) {
case 127:
case CTRL('H'):
/*
Destructive backspace: remove last character
*/
if (inptr > 0) {
S->print("\010 \010");
buffer[--inptr] = 0;
}
break;
case CTRL('R'):
/*
Ctrl-R retypes the line
*/
S->print("\r\n");
S->print(buffer);
break;
case CTRL('U'):
/*
Ctrl-U deletes the entire line and starts over.
*/
S->println("XXX");
reset();
break;
case CTRL('J'):
case CTRL('M'):
buffer[inptr++] = '\n';
S->println(); /* Echo newline too. */
return inptr;
case -1:
/*
No character present; don't do anything.
*/
return 0;
default:
/*
Otherwise, echo the character and put it into the buffer
*/
buffer[inptr++] = c;
S->write(c);
}
return 0;
}
/*
* getLineWait
* like getLine, but block until a complete line is read
*/
uint8_t parserCore::getLineWait (void)
{
uint8_t status;
do {
status = getLine();
} while (status == 0);
return status;
}
bool parserCore::IsWhitespace (char c)
{
return (c == ' ' || c == CTRL('I'));
}
bool parserCore::delim(char c)
{
static const char Delimiters[] PROGMEM = "\r\n ,;:=\t";
if (c == 0 || strchr_P(Delimiters, c))
return true;
return false;
}
/*
* Number
* Advance the token and parse a number. Accept decimal, hex, octal.
*/
int parserCore::number()
{
char *p = token();
if (p) {
return strtol(p, 0, 0);
}
return -1;
}
/*
* eol
* return true if we're at the end of the line.
*/
boolean parserCore::eol ()
{
while (IsWhitespace(buffer[parsePtr])) { /* skip leading whitespace */
parsePtr++;
}
return buffer[parsePtr] == '\n' || buffer[parsePtr] == 0;
}
/*
* cliTermChar
* return the termination character of the last token
*/
uint8_t parserCore::termChar ()
{
return termchar;
}
char parserCore::nextChar () {
return buffer[parsePtr];
}
char *parserCore::restOfLine() {
return &buffer[parsePtr];
}
/*
* cliCharacter
*/
/*
* token
* A token is a set of non-delimiter characters ending at a delimiter.
* As a line is parsed from the internal buffer, parsePtr is advanced, and
* the delimiters of parsed tokens are replaced with nulls.
* Note that a line always ends with the newline character AND a null.
*/
char *parserCore::token ()
{
uint8_t i;
if (eol()) { // reached the end of the line?
return NULL;
}
i = parsePtr; // save start position of token
while ((!delim(buffer[parsePtr])) && (parsePtr < lineLen)) {
parsePtr++; // advance pointer till we hit a delimiter.
}
termchar = buffer[parsePtr];
buffer[parsePtr++] = 0; // replace the delimiter with null
return &buffer[i]; // convert position to pointer for retval
}
/*
* Match the next token with a list of keywords.
* The list of keywords is in PROGMEM, separated by spaces.
* returns either the position of the found keyword (0..n),
* PARSER_NOMATCH, PARSER_AMB, or PARSER_EOL at the end of line
*/
int8_t parserCore::keyword (const char *keys)
{
char *p = token();
char *thisKey = (char *)keys;
int8_t i = 0, match, first = PARSER_NOMATCH;
if (!p) {
#if defined(DEBUG) && DEBUG
S->println("Early EOL");
#endif
return PARSER_EOL;
}
while (pgm_read_byte(thisKey)) {
match = tokcasecmp(p, thisKey);
#if defined(DEBUG) && DEBUG
sprintf(spbuffer, "key='%S', p='%s', match = %d\n", thisKey, p, match);
S->print(spbuffer);
#endif
if (match == CMP_MATCH) {
#if defined(DEBUG) && DEBUG
sprintf(spbuffer, "Exact match %d\n", i);
S->print(spbuffer);
#endif
return i;
}
byte c;
do {
c = pgm_read_byte(thisKey);
if (c == 0)
break;
thisKey++; // advance to next keyword, but not past end!
} while (c > ' ');
if (match == CMP_PARTIALMATCH) {
// There was a partial match; check for another...
if (first != PARSER_NOMATCH) { // already another match?
return (PARSER_AMB);
} else {
first = i;
continue;
}
#if defined(DEBUG) && DEBUG
sprintf(spbuffer, "Match %d\n", i);
S->print(spbuffer);
#endif
return i; // match
}
i++; // next keyword
}
#if defined(DEBUG) && DEBUG
sprintf(spbuffer, "Partial match %d\n", first);
S->print(spbuffer);
#endif
return first;
}
/*
* tokcasecmp
* tokcasecmp is like strcasecmp_P, except that the strings are terminated
* by any char < 32. Return value is 0 for match, or pointer to the delimiter
* for non-match (to expedite comparing against strings of targets.)
*/
uint8_t parserCore::tokcasecmp(const char *tok, const char *target)
{
char tokc, keyc;
const char *t = (char *)target;
do {
tokc = toupper(*tok++);
keyc = toupper(pgm_read_byte(t++));
// tok++; t++;
if (tokc == 0) {
// End of token; see if end of keyword as well
if (keyc <= ' ') {
return CMP_MATCH; // both ended - exact match
}
return CMP_PARTIALMATCH; // keyword is longer - partial
}
// Not end of token
if (keyc <= ' ') {
return CMP_NONMATCH; // key ended before tok - non match
}
} while (tokc == keyc);
return CMP_NONMATCH; // neither string ended, but non-match
}

View File

@@ -0,0 +1,64 @@
/*
* Written 2014 by Bill Westfield (WestfW)
* Refactored 2020
* Released to the public domain.
*/
#include "Arduino.h"
class parserCore {
private:
char *buffer;
char *lastToken;
byte lineLen;
Stream *S;
byte inptr; /* read character into here */
byte parsePtr;
byte termchar;
// Internal functions.
bool IsWhitespace(char c);
bool delim(char c);
char *token(void);
// int8_t KeywordPM (const char *keys);
uint8_t tokcasecmp(const char *tok, const char * target);
public:
parserCore(char *buf, byte buflen, Stream &io) {
buffer = buf;
lineLen = buflen;
S = &io;
}
uint8_t getLine(void); /* Non-blocking read line w/editing*/
uint8_t getLineWait(void); /* wait for a full line of input */
void reset(void); /* reset the parser */
int number(); /* parse a number */
int lastNumber();
boolean eol(); /* check for EOL */
char nextChar();
char *restOfLine();
uint8_t termChar(); /* return the terminating char of last token */
int8_t keyword(const char *keys); /* keyword with partial matching */
// int8_t keywordExact(const char *keys); /* keyword exact match */
};
/*
* Constants
*/
#define PARSER_NOMATCH -1
#define PARSER_EOL -2
#define PARSER_AMB -3
#define CMP_PARTIALMATCH 2
#define CMP_NONMATCH 1
#define CMP_MATCH 0
#define CTRL(x) (x-64)
template <size_t maxline=80>
class simpleParser: public parserCore {
private:
char buf[maxline];
public:
simpleParser(Stream &io) : parserCore(buf, sizeof(buf), io) {}
};

View File

@@ -0,0 +1,31 @@
// Dump a byte as two hex characters.
void hexout(uint8_t b)
{
uint8_t high, low;
high = b >> 4;
low = b & 0xF;
if (high > 9) {
high += ('A'-10) - '0';
}
Serial.write(high + '0');
if (low > 9) {
low += ('A'-10) - '0';
}
Serial.write(low + '0');
}
void DumpHex(uint8_t *p, uint8_t len) {
for (int i = 0; i < len; i++) {
Serial.write(' ');
hexout(p[i]);
}
Serial.print(" ");
for (int i = 0; i < len; i++) {
if (p[i] < ' ') {
Serial.write('.');
} else {
Serial.write(p[i]);
}
}
}