Skip to content

Commit

Permalink
[ot] scripts/opentitan: add a new script to demonstrate GPIO chardev
Browse files Browse the repository at this point in the history
Signed-off-by: Emmanuel Blot <[email protected]>
  • Loading branch information
rivos-eblot committed Sep 5, 2023
1 parent e0cc6a9 commit 28b817e
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 0 deletions.
17 changes: 17 additions & 0 deletions docs/opentitan/gpio.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,20 @@ a `I` frame whenever its own input lines change.
`Q` and `R` are only emitted when a host connects to QEMU or when one side resets its internal
state.

### Example

The `scripts/opentitan/trellis` directory contains two Python files that may be copied to an
an Adafruit NeoTrellis M4 Express card initialized with Circuit Python 8.0+

These scripts provide a physical, visual interface to the virtual GPIO pins, which is connected to
the QEMU machine over a serial port (a USB CDC VCP in this case).

To connect to the NeoTrellis board, use a configuration such as:

```
-chardev serial,id=gpio,path=/dev/ttyACM1 -global ot-gpio.chardev=gpio
```

where /dev/ttyACM1 is the data serial port of the Neotreillis board.

Note: the first serial port of the board is reserved to its debug console.
5 changes: 5 additions & 0 deletions scripts/opentitan/treillis/boot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import usb_cdc
import usb_midi

usb_midi.disable()
usb_cdc.enable(console=True, data=True)
170 changes: 170 additions & 0 deletions scripts/opentitan/treillis/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""Simple CircuitPython script (which is stuck with Python 3.4) that maps
QEMU GPIO chardev backend device onto a NeoTreillis M4 Express device.
"""

#pylint: disable=import-error
#pylint: disable=invalid-name
#pylint: disable=missing-function-docstring
#pylint: disable=consider-using-f-string
#pylint: disable=missing-class-docstring
#pylint: disable=too-few-public-methods
#pylint: disable=too-many-branches
#pylint: disable=too-many-instance-attributes


try:
from time import monotonic_ns as now
import usb_cdc
from adafruit_trellism4 import TrellisM4Express
except ImportError:
print('This code should run on Adafruit NeoTrellis M4 Express with '
'CircuitPython')
raise

class OtGPIO:
"""OpenTitan GPIO interface with an Adafruit NeoTrellis M4 Express.
Demonstrate the GPIO protocol over a QEMU CharDev.
"""

GPO_ON = (20, 0, 0) # red
GPO_OFF = (0, 20, 0) # green
GPI_ON = (0, 0, 80) # bright blue
GPI_OFF = (2, 2, 2) # greyish

LOCK_TIME_MS = 300 # key depressed time to lock/unlock an input key

def __init__(self, serial):
self._serial = serial
self._trellis = TrellisM4Express()
# 32-bit bitmaps
self._oe = 0 # output enable (1: out, 0: in)
self._out = 0 # output value (from peer)
self._in = 0 # input value (to peer)
self._kin = 0 # keyboard input
self._lock_in = 0 # locked keys
self._lock_time = {} # when key has been first pressed (ns timestamp)

def _update_input(self, newval, force=False):
ts = now()
change = self._kin ^ newval
self._kin = newval
for pos in range(32):
bit = 1 << pos
# only consider keys that change
if not bit & change:
continue
# is the key down?
down = bool(bit & newval)
if down:
self._lock_time[pos] = ts
continue
# key is released
dtime = self._lock_time.get(pos)
if dtime is None:
continue
delay_ms = (ts - dtime) // 1_000_000
if delay_ms > self.LOCK_TIME_MS:
# Lock action
on = bool(self._lock_in & bit)
if on:
self._lock_in &= ~bit
else:
self._lock_in |= bit
self._lock_time.pop(pos)
self._in = self._lock_in ^ newval
return change

def _update_output(self, newval):
change = (self._out ^ newval) & self._oe
self._out = newval
return change

def _refresh_input(self, change):
for pos in range(32):
bit = 1 << pos
if not bit & change:
continue
y = pos >> 3
x = pos & 0x7
if self._in & bit:
color = self.GPI_ON
else:
color = self.GPI_OFF
self._trellis.pixels[7-x, 3-y] = color

def _refresh_output(self, change):
for pos in range(32):
bit = 1 << pos
if not change & bit:
continue
y = pos >> 3
x = pos & 0x7
if self._out & bit:
color = self.GPO_ON
else:
color = self.GPO_OFF
self._trellis.pixels[7-x, 3-y] = color

def run(self):
self._serial.timeout = 0.1
self._serial.write_timeout = 0.5
# query QEMU to repeat I/O config on startup.
self._serial.write(b'R:00000000\r\n')
buf = bytearray()
last_kin = 0
force = False
while True:
kin = 0
for x, y in self._trellis.pressed_keys:
kin |= 1 << (31 - (8 * y + x))
if last_kin != kin:
change = self._update_input(kin)
if not force:
change &= ~self._oe
self._refresh_input(change)
self._serial.write(b'I:%08x\r\n' % self._in)
last_kin = kin
data = self._serial.read()
if not data:
continue
for b in data:
if b != 0x0d:
buf.append(b)
pos = buf.find(b'\n')
if pos < 0:
continue
line = bytes(buf[:pos])
buf = buf[pos+1:]
if not line:
continue
if len(line) < 2 or line[1] != ord(':'):
continue
cmd = line[0]
try:
val = int(line[2:], 16)
except ValueError:
continue
if cmd == ord('D'):
# update I/O direction
self._oe = val
self._refresh_output(self._oe)
self._refresh_input(~self._oe)
elif cmd == ord('O'):
# update output
change = self._update_output(val)
self._refresh_output(change)
elif cmd == ord('Q'):
# QEMU query for current Input state
self._serial.write(b'I:%08x\r\n' % self._in)
else:
print('Unknown command %s' % cmd)


if __name__ == '__main__':
if not usb_cdc.data:
# boot.py should be used to enable the secondary, data CDC serial-over-USB
# device. The first port is reserved for the Console.
raise RuntimeError('No serial port available')
gpio = OtGPIO(usb_cdc.data)
gpio.run()

0 comments on commit 28b817e

Please sign in to comment.