Device Identification
By design, the device should come up functional. To help ensure the device being communicated with is the expected device, the following initialization occurs:
First read one byte from STATUS_BASE
(0x00
) /
STATUS_HW_ID
(0x01) to get an identifier of the chip on the far side of the I2C connection. In the below example, the chip responds 0x55
, indicating that the controller on the target I2C device is a SAMD09
chip.
I2C> [ 0x6c 0x00 0x01 ] d:100 [ 0x6d r:1 ]
I2C START
TX: 0x6C ACK 0x00 ACK 0x01 ACK
I2C STOP
Delay: 100us
I2C START
TX: 0x6D ACK
RX: 0x55 NACK
I2C STOP
Next get the product ID (PID) by reading int32_t
(encoded big-endian) by reading STATUS_BASE
(0x00
) / STATUS_VERSION
(0x02
). The PID is the 16 most significant bits of that int32_t
value.
In the below example, the target I2C device is reporting a device ID of 4991 (0x137F
):
I2C> [ 0x6c 0x00 0x02 ] d:100 [ 0x6d r:4 ]
I2C START
TX: 0x6C ACK 0x00 ACK 0x02 ACK
I2C STOP
Delay: 100us
I2C START
TX: 0x6D ACK
RX: 0x13 ACK 0x7F ACK 0x2A ACK 0xA3 NACK
I2C STOP
I2C> = 4991
=0x137F.16 =4991.16 =0b0001001101111111.16
The PID is not one of the originally released SeeSaw devices, so the chip ID can be used determine which pins support capacitive touch, analog reads, PWM outputs, etc. In this case, we know the details of the board with PID 4991 (0x137F
), which are also confirmed by reading STATUS_BASE
(0x00
) / STATUS_OPTIONS
(0x04
):
I2C> [ 0x6C 0x00 0x03 ] d:1000 [ 0x6D r:4 ]
I2C START
TX: 0x6C ACK 0x00 ACK 0x03 ACK
I2C STOP
Delay: 1000us
I2C START
TX: 0x6D ACK
RX: 0x00 ACK 0x02 ACK 0x48 ACK 0x03 NACK
I2C STOP
I2C> # 0x0002'4803 was the returned data
I2C> # Each set bit corresponds to a supported register base:
I2C> # 0x0000'0001 == Bit 0x00 == STATUS_BASE
I2C> # 0x0000'0002 == Bit 0x01 == GPIO_BASE
I2C> # 0x0000'0800 == Bit 0x0B == INTERRUPT_BASE
I2C> # 0x0000'4000 == Bit 0x0E == NEOPIXEL_BASE
I2C> # 0x0002'0000 == Bit 0x11 == ENCODER_BASE
Details for PID 4991
Here, we know that PID 4991 (0x137F
) is a board with SeeSaw firmware having the following features:
- a single neopixel attached to pin 6 (SeeSaw firmware exposing defined register
SEESAW_NEOPIXEL_BASE
)
- a single rotary encoder (SeeSaw firmware exposing defined register
SEESAW_ENCODER_BASE
)
- a rotary encoder button connected to pin 24 (setup and read via SeeSaw GPIO register / edge modules)
`SEESAW_NEOPIXEL_BASE` (`0x0E`) defines six edge modules / functions:
Symbol |
Value |
Type |
Purpose |
NEOPIXEL_STATUS |
0x00 |
N/A |
Not used |
NEOPIXEL_PIN |
0x01 |
W |
Configure a neopixel on one of the I2C device’s pins |
NEOPIXEL_SPEED |
0x02 |
W |
0x01 is 800kHz and default, 0x00 for 400kHz |
NEOPIXEL_BUF_LENGTH |
0x03 |
W |
Send big-endian 16-bit value indicating length of buffer for the neopixels |
NEOPIXEL_BUF |
0x04 |
W |
Send color data to on-I2C-device buffer; First two bytes is start address, followed by |
NEOPIXEL_SHOW |
0x05 |
W |
Update neopixel using buffered color data |
`SEESAW_ENCODER_BASE` (`0x11`) defines four edge modules / functions:
Symbol |
Value |
Type |
Purpose |
SEESAW_ENCODER_STATUS |
0x00 |
N/A |
Not used |
SEESAW_ENCODER_INTENSET |
0x10 |
W |
Interrupt line will go high for rotation, until position (or delta) is read |
SEESAW_ENCODER_INTENCLR |
0x20 |
W |
Disable interrupt line due to rotation |
SEESAW_ENCODER_POSITION |
0x30 |
R/W |
Encoder Current Position |
SEESAW_ENCODER_DELTA |
0x40 |
R |
Encoder difference from last read position |
`SEESAW_GPIO_BASE` (`0x01`) defines eleven edge modules / functions:
All functions take a 32-bit input, encoded big-endian, with each bit corresponding to one of the pins of the mcu of the SeeSaw firmware-enabled device. For SAMD09, bits 0..31 correspond to PA00..PA31. (The mapping is not as straightforward for some of the ATTiny chips.)
With the exception of SEESAW_GPIO_INTFLAG
and SEESAW_GPIO_GPIO
, the functions will only affect pins whose corresponding bit is set to one. This allows the host to perform bulk updates of pin state with fewer I2C transactions.
Symbol |
Value |
Type |
Purpose |
SEESAW_GPIO_DIRSET |
0x02 |
W |
Configure pins as OUTPUT |
SEESAW_GPIO_DIRCLR |
0x03 |
W |
Configure pins as INPUT |
SEESAW_GPIO_GPIO |
0x04 |
R/W |
Reads all pins (and clears INTFLAG ); Writes all pins to high or low based on corresponding bit. THIS AFFECTS ALL PINS WITH EACH CALL. |
SEESAW_GPIO_SET |
0x05 |
W |
Set pins output to HIGH |
SEESAW_GPIO_CLR |
0x06 |
W |
Set pins output to LOW |
SEESAW_GPIO_TOGGLE |
0x07 |
W |
Toggles pins output |
SEESAW_GPIO_INTENSET |
0x08 |
W |
Enable reporting pins’ value changes in INTFLAG register. |
SEESAW_GPIO_INTENCLR |
0x09 |
W |
Disable reporting pins’ value changes in INTFLAG register |
SEESAW_GPIO_INTFLAG |
0x0A |
R |
Read status of all GPIO interrupts. Reading this register will clear the value to zero. |
SEESAW_GPIO_PULLENSET |
0x0B |
W |
Enable the pins internal pullup/pulldown. Direction is determined by the GPIO value: LOW for pulldown, HIGH for pullup. |
SEESAW_GPIO_PULLENCLR |
0x0C |
W |
Disable the pins internal pullup/pulldown. |
Here, the button is connected to the SAMD09 pin 24, which encoded as a pin bitmask (BE) is 0x01 0x00 0x00 0x00
.
Thus, to configure that pin as input with pullup:
` NOTE: data is big-endian encoded
` NOTE: Mask for pin 24 is 0x01 0x00 0x00 0x00
` Set the pin to INPUT with DIRCLR
[ 0x6C 0x01 0x03 0x01 0x00 0x00 0x00 ]
` Enable the internal pullup/pulldown
[ 0x6C 0x01 0x0B 0x01 0x00 0x00 0x00 ]
` Make it a pullup by setting pin output HIGH
[ 0x6C 0x01 0x05 0x01 0x00 0x00 0x00 ]
` Read the button state
[ 0x6C 0x01 0x04 ] d:250 [ 0x6D r:4 ]
Known Bugs
-
SeeSaw firmware (at least on SAMD09 chips) does NOT support REPEATED START
(i.e., sending a new START frame without sending a STOP frame). A STOP frame is required before the next START frame, or the data received will be (at best) unreliable.
-
Setting the current position of the encode has resulted in heisenbugs. Recommending to avoid writing a current position to the SeeSaw encoder.
-
Timing window can leave interrupt line high. Here’s an example of the order in which operations might happen:
- Interrupt line is enabled
- Rotation occurs, sets interrupt line (high)
- Host reads rotation, which clears interrupt line (low)
- Rotation occurs, sets interrupt line (high)
- Host sends I2C command to disable interrupt line
- Note: this does NOT clear the interrupt line … it remains high
- Host reads rotation
- NOTE: this does NOT clear the interrupt line … it remains high
- Therefore, there is a timing window where rotations occurs between the last host read of the encoder, and the host disabling interrupts, which leaves the interrupt line set high. There is no obvious way to clear the interrupt line (yet). Open Question: Can this be “fixed” by writing to the I2C (SeeSaw) GPIO registers?
Initialization Script
NOTE: For old BP5 firmware (where #
is a “reset the buspirate” command), simply remove the commented lines, or replace with “`” character (which will report an invalid command, but not do anything unexpected).
# ---- Verify the device's product ID is 4991 (`0x137F`) ----
[ 0x6c 0x00 0x02 ] d:100 [ 0x6d r:4 ]
= 0x137F
# ---- Configure Neopixel
# Set neopixel to use pin 6
[ 0x6C 0x0E 0x01 0x06 ]
# Set neopixel buffer length to 3 bytes (GRB)
[ 0x6C 0x0E 0x03 0x00 0x03 ]
# Update neopixel buffer at address 0x0000
# with RGB 0x22 0x11 0x33 --> GRB 0x11 0x22 0x33
[ 0x6C 0x0E 0x04 0x00 0x00 0x11 0x22 0x33 ]
# Send neopixel data to the pixel(s)
[ 0x6C 0x0E 0x05 ]
# ---- Configure Rotary Encoder
# Optionally, enable an interrupt when encoder rotates
[ 0x6C 0x11 0x10 0x01 ]
# Read the current position
[ 0x6C 0x11 0x30 ] [ 0x6D r:4 ]
# If enabled interrupts, read position when that line goes high
# Otherwise, poll the current position periodically
# ---- Configure the Encoder button ----
# Set the encoder button's pin to INPUT with DIRCLR
[ 0x6C 0x01 0x03 0x01 0x00 0x00 0x00 ]
# Enable the internal pullup/pulldown for encoder button
[ 0x6C 0x01 0x0B 0x01 0x00 0x00 0x00 ]
# Make encoder button pin a pullup by setting pin output HIGH
[ 0x6C 0x01 0x05 0x01 0x00 0x00 0x00 ]
# With button released, read the button state
[ 0x6C 0x01 0x04 ] d:250 [ 0x6D r:4 ]
# Note: returned data & 0x01000000 == 0x01000000
# and therefore the button is NOT pressed
# Now hold the encoder button down and read again
[ 0x6C 0x01 0x04 ] d:250 [ 0x6D r:4 ]
# Note: returned data & 0x01000000 == 0x00000000
# and therefore the button IS pressed
Actual initialization output
Example output of running the above initialization commands:
I2C> # ---- Verify the device's product ID is 4991 (`0x137F`) ----
I2C> [ 0x6c 0x00 0x02 ] d:100 [ 0x6d r:4 ]
I2C START
TX: 0x6C ACK 0x00 ACK 0x02 ACK
I2C STOP
Delay: 100us
I2C START
TX: 0x6D ACK
RX: 0x13 ACK 0x7F ACK 0x2A ACK 0xA3 NACK
I2C STOP
I2C> = 0x137F
=0x137F.16 =4991.16 =0b0001001101111111.16
I2C> # ---- Configure Neopixel
I2C> # Set neopixel to use pin 6
I2C> [ 0x6C 0x0E 0x01 0x06 ]
I2C START
TX: 0x6C ACK 0x0E ACK 0x01 ACK 0x06 ACK
I2C STOP
I2C> # Set neopixel buffer length to 3 bytes (GRB)
I2C> [ 0x6C 0x0E 0x03 0x00 0x03 ]
I2C START
TX: 0x6C ACK 0x0E ACK 0x03 ACK 0x00 ACK 0x03 ACK
I2C STOP
I2C> # Update neopixel buffer at address 0x0000
I2C> # with RGB 0x22 0x11 0x33 --> GRB 0x11 0x22 0x33
I2C> [ 0x6C 0x0E 0x04 0x00 0x00 0x11 0x22 0x33 ]
I2C START
TX: 0x6C ACK 0x0E ACK 0x04 ACK 0x00 ACK 0x00 ACK 0x11 ACK 0x22 ACK 0x33 ACK
I2C STOP
I2C> # Send neopixel data to the pixel(s)
Invalid command: `. Type ? for help.
I2C> [ 0x6C 0x0E 0x05 ]
I2C START
TX: 0x6C ACK 0x0E ACK 0x05 ACK
I2C STOP
I2C> # ---- Configure Rotary Encoder
I2C> # Optionally, enable an interrupt when encoder rotates
I2C> [ 0x6C 0x11 0x10 0x01 ]
I2C START
TX: 0x6C ACK 0x11 ACK 0x10 ACK 0x01 ACK
I2C STOP
I2C> # Read the current position
I2C> [ 0x6C 0x11 0x30 ] [ 0x6D r:4 ]
I2C START
TX: 0x6C ACK 0x11 ACK 0x30 ACK
I2C STOP
I2C START
TX: 0x6D ACK
RX: 0x00 ACK 0x00 ACK 0x00 ACK 0x00 NACK
I2C STOP
I2C> # ---- Configure the Encoder button ----
I2C> # Set the encoder button's pin to INPUT with DIRCLR
Invalid command: `. Type ? for help.
I2C> [ 0x6C 0x01 0x03 0x01 0x00 0x00 0x00 ]
I2C START
TX: 0x6C ACK 0x01 ACK 0x03 ACK 0x01 ACK 0x00 ACK 0x00 ACK 0x00 ACK
I2C STOP
I2C> # Enable the internal pullup/pulldown for encoder button
I2C> [ 0x6C 0x01 0x0B 0x01 0x00 0x00 0x00 ]
I2C START
TX: 0x6C ACK 0x01 ACK 0x0B ACK 0x01 ACK 0x00 ACK 0x00 ACK 0x00 ACK
I2C STOP
I2C> # Make encoder button pin a pullup by setting pin output HIGH
I2C> [ 0x6C 0x01 0x05 0x01 0x00 0x00 0x00 ]
I2C START
TX: 0x6C ACK 0x01 ACK 0x05 ACK 0x01 ACK 0x00 ACK 0x00 ACK 0x00 ACK
I2C STOP
I2C> # Read the button state
I2C> [ 0x6C 0x01 0x04 ] d:250 [ 0x6D r:4 ]
I2C START
TX: 0x6C ACK 0x01 ACK 0x04 ACK
I2C STOP
Delay: 250us
I2C START
TX: 0x6D ACK
RX: 0x53 ACK 0xC0 ACK 0x85 ACK 0x30 NACK
I2C STOP
I2C> # Note: 0x53C08530 & 0x01000000 == 0x01000000
I2C> # Now hold the encoder button down and read again
I2C> # and therefore the button is NOT pressed
I2C> [ 0x6C 0x01 0x04 ] d:250 [ 0x6D r:4 ]
I2C START
TX: 0x6C ACK 0x01 ACK 0x04 ACK
I2C STOP
Delay: 250us
I2C START
TX: 0x6D ACK
RX: 0x52 ACK 0xC0 ACK 0x85 ACK 0x30 NACK
I2C STOP
I2C> # Note: 0x52C08530 & 0x01000000 == 0x00000000
I2C> # and therefore the button IS pressed