Add HF2 spec
This commit is contained in:
parent
40f9609b81
commit
7d03ee469f
4 changed files with 313 additions and 25 deletions
4
.clang-format
Normal file
4
.clang-format
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
BasedOnStyle: LLVM
|
||||
IndentWidth: 4
|
||||
UseTab: Never
|
||||
ColumnLimit: 100
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,3 +4,4 @@ flash.uf2
|
|||
flash.bin
|
||||
|
||||
built/
|
||||
uf2tool/uf2hid.h
|
||||
|
|
|
|||
273
hf2.md
Normal file
273
hf2.md
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
# HID Flashing Format
|
||||
|
||||
HF2 (HID Flashing Format) is a protocol and message format intended for
|
||||
communication with embedded devices. Basic functions supported include:
|
||||
|
||||
* serial communication for `printf()` debugging etc.
|
||||
* flashing (updating device firmware)
|
||||
* debugger interfaces
|
||||
|
||||
It is optimized for packet formats, where packets are around 64 bytes long.
|
||||
It will work for smaller packets, of at least a few bytes, or bigger
|
||||
ones, but be less efficient.
|
||||
|
||||
In particular, it is suitable for running over USB HID (Human Interface Device),
|
||||
which is widely supported in various operating systems without the need for kernel-space
|
||||
drivers. It is also possible to run the protocol over a WebUSB link with either a single
|
||||
interrupt endpoint or two bulk endpoints, allowing direct access from supported
|
||||
browsers.
|
||||
|
||||
## Raw message format
|
||||
|
||||
HF2 messages are composed of packets. Packets are up to 64 bytes long.
|
||||
Messages sent from host to device and vice versa have the same basic format.
|
||||
|
||||
The first byte of each packet indicates:
|
||||
* length of the remaining data (payload) in the packet, in the lower 6 bits, i.e., between 0 and 63 inclusive
|
||||
* the type of the packet, in the two high bits.
|
||||
|
||||
| Bit 7 | Bit 6 | Hex | Meaning
|
||||
|-------|-------|------|----------------------------------------------
|
||||
| 0 | 0 | 0x00 | Inner packet of a command message
|
||||
| 0 | 1 | 0x40 | Final packet of a command message
|
||||
| 1 | 0 | 0x80 | Serial `stdout`
|
||||
| 1 | 1 | 0xC0 | Serial `stderr`
|
||||
|
||||
Serial messages are thus between 0 and 63 bytes in length.
|
||||
|
||||
Command messages can have any length (though devices will typically limit
|
||||
it to the native flash page size + 64 bytes). They consist of a zero or more
|
||||
inner packets, followed by a single final packet. Any of these packets
|
||||
can carry between 0 and 63 bytes of payload.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
Packet 0: 83 01 02 03 AB FF FF FF
|
||||
Packet 1: 85 04 05 06 07 08
|
||||
Packet 2: 80 DE 42 42 42 42 FF FF
|
||||
Packet 3: D0 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 FF FF FF
|
||||
--->
|
||||
Decoded: 01 02 03 04 05 06 07 08 D0 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17
|
||||
```
|
||||
|
||||
Note that packets can be longer than the first byte requires (packets 0, 2, and 3 are).
|
||||
The additional data should be discarded.
|
||||
This is due to various HID implementations imposing an exact 64 byte packet size.
|
||||
|
||||
Different command messages cannot be interleaved.
|
||||
|
||||
Serial messages should not be interleaved with command messages (though it would be
|
||||
technically possible).
|
||||
|
||||
## Higher-level message format
|
||||
|
||||
### Serial messages
|
||||
|
||||
Serial messages are meant for `printf()` style debugging
|
||||
and general simple data output from the device.
|
||||
If the device supports only one serial channel, it should support
|
||||
the `stdout` channel. Otherwise, `stdout` is meant
|
||||
for data output (eg., logging measurements), and `stderr`
|
||||
is meant for `printf()` debugging.
|
||||
|
||||
The logging application on the host may choose to send these
|
||||
two channels into a single output stream.
|
||||
|
||||
Serial messages contain between `0` and `63` bytes of payload data.
|
||||
Size of `0` can be used as a keep-alive packet if needed.
|
||||
|
||||
### Command messages
|
||||
|
||||
Command structure:
|
||||
|
||||
```c
|
||||
struct Command {
|
||||
uint32_t command_id;
|
||||
uint16_t tag;
|
||||
uint8_t reserved0;
|
||||
uint8_t reserved1;
|
||||
uint8_t data[...];
|
||||
};
|
||||
|
||||
struct Response {
|
||||
uint16_t tag;
|
||||
uint8_t status;
|
||||
uint8_t status_info;
|
||||
uint8_t data[...];
|
||||
};
|
||||
```
|
||||
|
||||
All words in HF2 are little endian.
|
||||
|
||||
The `tag` is an arbitrary number set by the host, for example as sequence
|
||||
number. The response should repeat the `tag`.
|
||||
|
||||
The two reserved bytes in the command should be sent as zero and ignored by the device.
|
||||
|
||||
The response status is one of the following:
|
||||
* `0x00` - command understood and executed correctly
|
||||
* `0x01` - command not understood
|
||||
* `0x02` - command execution error
|
||||
|
||||
Note, that embedded devices might crash on invalid arguments, instead
|
||||
of returning errors. OTOH, the devices should always handle invalid commands with
|
||||
`0x01` status.
|
||||
|
||||
In case of non-zero status, the `status_info` field can contain additional information.
|
||||
|
||||
The host shall not send a new command, until the previous one was responded to.
|
||||
TODO does this make sense? maybe just let USB flow control handle this?
|
||||
|
||||
## Standard commands
|
||||
|
||||
Below we list standard commands. Not all commands have to be supported by all
|
||||
devices.
|
||||
|
||||
When the C fragment states `no results`, it means just a response
|
||||
with zero status and no additional data should be expected.
|
||||
|
||||
### BININFO (0x0001)
|
||||
|
||||
This command states the current mode of the device:
|
||||
* ``mode == 0x01`` - bootloader, and thus flashing of user-space programs is allowed
|
||||
* ``mode == 0x02`` - user-space mode.
|
||||
It also returns the size of flash page size (flashing needs to be done on page-by-page basis),
|
||||
and the maximum size of message. It is always the case that
|
||||
``max_message_size >= flash_page_size + 64``.
|
||||
|
||||
```c
|
||||
struct HF2_BININFO_Result {
|
||||
uint32_t mode;
|
||||
uint32_t flash_page_size;
|
||||
uint32_t flash_num_pages;
|
||||
uint32_t max_message_size;
|
||||
};
|
||||
```
|
||||
|
||||
### INFO (0x0002)
|
||||
|
||||
Various device information.
|
||||
The result is a character array. See `INFO_UF2.TXT` in UF2 format for details.
|
||||
|
||||
```c
|
||||
// no arguments
|
||||
struct HF2_INFO_Result {
|
||||
uint8_t info[...];
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
### RESET INTO APP (0x0003)
|
||||
|
||||
Reset the device into user-space app. Usually, no response at all will arrive for this command.
|
||||
|
||||
```c
|
||||
// no arguments, no result
|
||||
```
|
||||
|
||||
|
||||
### RESET INTO (0x0004)
|
||||
|
||||
Reset the device into bootloader, usually for flashing. Usually, no response at all will arrive for this command.
|
||||
|
||||
```c
|
||||
// no arguments, no result
|
||||
```
|
||||
|
||||
### START FLASH (0x0005)
|
||||
|
||||
When issued in bootloader mode, it has no effect.
|
||||
In user-space mode it causes handover to bootloader.
|
||||
A `BININFO` command can be issued to verify that.
|
||||
|
||||
```c
|
||||
// no arguments, no result
|
||||
```
|
||||
|
||||
### WRITE FLASH PAGE (0x0006)
|
||||
|
||||
Write a single page of flash memory.
|
||||
|
||||
```c
|
||||
struct HF2_WRITE_FLASH_PAGE_Command {
|
||||
uint32_t target_addr;
|
||||
uint8_t data[flash_page_size];
|
||||
};
|
||||
// no result
|
||||
```
|
||||
|
||||
### CHKSUM PAGES (0x0007)
|
||||
|
||||
Compute checksum of a number of pages.
|
||||
Maximum value for ``num_pages`` is ``max_message_size / 2 - 2``.
|
||||
The checksum algorithm used is CRC-16-CCITT.
|
||||
|
||||
```c
|
||||
struct HF2_CHKSUM_PAGES_Command {
|
||||
uint32_t target_addr;
|
||||
uint32_t num_pages;
|
||||
};
|
||||
struct HF2_CHKSUM_PAGES_Result {
|
||||
uint16_t chksums[0 /* num_pages */];
|
||||
};
|
||||
```
|
||||
|
||||
### READ WORDS (0x0008)
|
||||
|
||||
Read a number of words from memory.
|
||||
Memory is read word by word (and not byte by byte), and ``target_addr`` must
|
||||
be suitably aligned. This is to support reading of special IO regions.
|
||||
|
||||
```c
|
||||
struct HF2_READ_WORDS_Command {
|
||||
uint32_t target_addr;
|
||||
uint32_t num_words;
|
||||
};
|
||||
struct HF2_READ_WORDS_Result {
|
||||
uint32_t words[num_words];
|
||||
};
|
||||
```
|
||||
|
||||
### WRITE WORDS (0x0009)
|
||||
|
||||
Dual of READ WORDS, with the same constraints.
|
||||
|
||||
```c
|
||||
struct HF2_WRITE_WORDS_Command {
|
||||
uint32_t target_addr;
|
||||
uint32_t num_words;
|
||||
uint32_t words[num_words];
|
||||
};
|
||||
// no result
|
||||
```
|
||||
|
||||
## Extensibility
|
||||
|
||||
The HF2 protocol is easy to extend with new command messages. The command ids
|
||||
you introduce should be chosen at random. This ensures very low probability of
|
||||
conflict between different extensions.
|
||||
|
||||
While numbers like `0x42420123`, `0x10001`, or `0xdeaff00d` may look random,
|
||||
others are likely to use them as well and a conflict might occur.
|
||||
Please also do not use numbers below `0xffff`, as these are standardized here.
|
||||
Ideally, use one of the following commands (or similar) to generate a random number:
|
||||
|
||||
```bash
|
||||
node -p "require('crypto').randomBytes(4).toString('hex')"
|
||||
# or slightly worse:
|
||||
printf "%04x%04x\n" $RANDOM $RANDOM
|
||||
```
|
||||
|
||||
If you change the behavior of a command, even just extend what it can do, and
|
||||
there is even a remote possibility of devices or interface applications using
|
||||
it in the wild, it's best to introduce a new command, and possibly have the
|
||||
device handle both (fall-through `switch` cases are useful here).
|
||||
|
||||
## Detection of HF2 devices
|
||||
|
||||
## Notes
|
||||
|
||||
If the device exposes both HID and WebUSB, it has to use two separate
|
||||
interfaces.
|
||||
|
||||
|
|
@ -18,7 +18,12 @@ typedef struct {
|
|||
uint8_t serial;
|
||||
uint16_t seqNo;
|
||||
uint16_t pageSize;
|
||||
uint8_t buf[FLASH_ROW_SIZE + 64];
|
||||
uint32_t flashSize;
|
||||
uint32_t msgSize;
|
||||
union {
|
||||
uint8_t buf[4096];
|
||||
HF2_Response resp;
|
||||
};
|
||||
} HID_Dev;
|
||||
|
||||
uint64_t millis() {
|
||||
|
|
@ -67,17 +72,21 @@ int recv_hid(HID_Dev *pkt, int timeout) {
|
|||
buf++; // skip report number if passed
|
||||
|
||||
uint8_t tag = buf[0];
|
||||
if (pkt->size && !(tag & 0x80))
|
||||
if (pkt->size && !(tag & HF2_FLAG_SERIAL_OUT))
|
||||
fatal("invalid serial transfer");
|
||||
uint32_t newsize = pkt->size + (tag & HF2_SIZE_MASK);
|
||||
if (newsize > sizeof(pkt->buf))
|
||||
fatal("too large packet");
|
||||
memcpy(pkt->buf + pkt->size, buf + 1, tag & HF2_SIZE_MASK);
|
||||
pkt->size = newsize;
|
||||
if (tag & 0x40) {
|
||||
pkt->serial = (tag & HF2_FLAG_MASK) == HF2_FLAG_SERIAL;
|
||||
|
||||
tag &= HF2_FLAG_MASK;
|
||||
if (tag != HF2_FLAG_CMDPKT_BODY) {
|
||||
pkt->serial = //
|
||||
tag == HF2_FLAG_SERIAL_OUT ? 1 : tag == HF2_FLAG_SERIAL_ERR ? 2 : 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
timeout = -1; // next read is blocking
|
||||
}
|
||||
}
|
||||
|
|
@ -90,10 +99,10 @@ void send_hid(hid_device *dev, const void *data, int size) {
|
|||
int s;
|
||||
if (size <= 63) {
|
||||
s = size;
|
||||
buf[1] = HF2_FLAG_PKT_LAST | size;
|
||||
buf[1] = HF2_FLAG_CMDPKT_LAST | size;
|
||||
} else {
|
||||
s = 63;
|
||||
buf[1] = HF2_FLAG_PKT_BODY | 63;
|
||||
buf[1] = HF2_FLAG_CMDPKT_BODY | 63;
|
||||
}
|
||||
memcpy(buf + 2, ptr, s);
|
||||
int sz = hid_write(dev, buf, 65);
|
||||
|
|
@ -107,18 +116,18 @@ void send_hid(hid_device *dev, const void *data, int size) {
|
|||
}
|
||||
|
||||
void talk_hid(HID_Dev *pkt, int cmd, const void *data, uint32_t len) {
|
||||
if (len >= sizeof(pkt->buf) - 4)
|
||||
if (len >= sizeof(pkt->buf) - 8)
|
||||
fatal("buffer overflow");
|
||||
if (data)
|
||||
memcpy(pkt->buf + 4, data, len);
|
||||
write16(pkt->buf, cmd);
|
||||
write16(pkt->buf + 2, ++pkt->seqNo);
|
||||
uint32_t saved = read32(pkt->buf);
|
||||
send_hid(pkt->dev, pkt->buf, 4 + len);
|
||||
memcpy(pkt->buf + 8, data, len);
|
||||
write32(pkt->buf, cmd);
|
||||
write16(pkt->buf + 4, ++pkt->seqNo);
|
||||
write16(pkt->buf + 6, 0);
|
||||
send_hid(pkt->dev, pkt->buf, 8 + len);
|
||||
recv_hid(pkt, -1);
|
||||
if ((read32(pkt->buf) & 0x7fffffff) != saved)
|
||||
if (read16(pkt->buf) != pkt->seqNo)
|
||||
fatal("invalid sequence number");
|
||||
if (read32(pkt->buf) & 0x80000000)
|
||||
if (read16(pkt->buf + 2))
|
||||
fatal("invalid status");
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +146,7 @@ unsigned short add_crc(char ptr, unsigned short crc) {
|
|||
}
|
||||
|
||||
void verify(HID_Dev *cmd, uint8_t *buf, int size, int offset) {
|
||||
int maxSize = (cmd->pageSize / 2 - 8) * cmd->pageSize;
|
||||
int maxSize = (cmd->pageSize / 2 - 12) * cmd->pageSize;
|
||||
while (size > maxSize) {
|
||||
verify(cmd, buf, maxSize, offset);
|
||||
buf += maxSize;
|
||||
|
|
@ -145,8 +154,8 @@ void verify(HID_Dev *cmd, uint8_t *buf, int size, int offset) {
|
|||
size -= maxSize;
|
||||
}
|
||||
int numpages = size / cmd->pageSize;
|
||||
write32(cmd->buf + 4, offset);
|
||||
write32(cmd->buf + 8, numpages);
|
||||
write32(cmd->buf + 8, offset);
|
||||
write32(cmd->buf + 12, numpages);
|
||||
talk_hid(cmd, HF2_CMD_CHKSUM_PAGES, 0, 8);
|
||||
for (int i = 0; i < numpages; ++i) {
|
||||
int sum = read16(cmd->buf + 4 + i * 2);
|
||||
|
|
@ -175,7 +184,7 @@ void serial(HID_Dev *cmd) {
|
|||
memset(buf, 0, 65);
|
||||
int sz = read(0, buf + 2, 63);
|
||||
if (sz > 0) {
|
||||
buf[1] = HF2_FLAG_SERIAL | sz;
|
||||
buf[1] = HF2_FLAG_SERIAL_OUT | sz;
|
||||
hid_write(cmd->dev, buf, 65);
|
||||
}
|
||||
}
|
||||
|
|
@ -209,14 +218,16 @@ int main(int argc, char *argv[]) {
|
|||
talk_hid(&cmd, HF2_CMD_INFO, 0, 0);
|
||||
printf("INFO: %s\n", cmd.buf + 4);
|
||||
|
||||
//serial(&cmd);
|
||||
// serial(&cmd);
|
||||
|
||||
talk_hid(&cmd, HF2_CMD_BININFO, 0, 0);
|
||||
if (cmd.buf[4] != HF2_MODE_BOOTLOADER)
|
||||
fatal("not bootloader");
|
||||
|
||||
cmd.pageSize = read32(cmd.buf + 8);
|
||||
printf("page size: %d\n", cmd.pageSize);
|
||||
cmd.flashSize = read32(cmd.buf + 12) * cmd.pageSize;
|
||||
cmd.msgSize = read32(cmd.buf + 16);
|
||||
printf("page size: %d, total: %dkB\n", cmd.pageSize, cmd.flashSize / 1024);
|
||||
|
||||
srand(millis());
|
||||
int i;
|
||||
|
|
@ -226,8 +237,8 @@ int main(int argc, char *argv[]) {
|
|||
uint64_t start = millis();
|
||||
|
||||
for (i = 0; i < sizeof(flashbuf); i += cmd.pageSize) {
|
||||
write32(cmd.buf + 4, i + 0x2000);
|
||||
memcpy(cmd.buf + 8, flashbuf + i, cmd.pageSize);
|
||||
write32(cmd.buf + 8, i + 0x2000);
|
||||
memcpy(cmd.buf + 12, flashbuf + i, cmd.pageSize);
|
||||
talk_hid(&cmd, HF2_CMD_WRITE_FLASH_PAGE, 0, cmd.pageSize + 4);
|
||||
}
|
||||
|
||||
|
|
@ -235,10 +246,9 @@ int main(int argc, char *argv[]) {
|
|||
start = millis();
|
||||
|
||||
#if 0
|
||||
|
||||
for (i = 0; i < sizeof(flashbuf); i += cmd.pageSize) {
|
||||
write32(cmd.buf + 4, i + 0x2000);
|
||||
write32(cmd.buf + 8, cmd.pageSize / 4);
|
||||
write32(cmd.buf + 8, i + 0x2000);
|
||||
write32(cmd.buf + 12, cmd.pageSize / 4);
|
||||
talk_hid(&cmd, HF2_CMD_MEM_READ_WORDS, 0, 8);
|
||||
if (memcmp(cmd.buf + 4, flashbuf + i, cmd.pageSize)) {
|
||||
printf("%d,%d,%d != %d?\n", cmd.buf[8], cmd.buf[9], cmd.buf[10], flashbuf[i]);
|
||||
|
|
|
|||
Loading…
Reference in a new issue