Skip to content

Commit 28b817e

Browse files
committed
[ot] scripts/opentitan: add a new script to demonstrate GPIO chardev
Signed-off-by: Emmanuel Blot <eblot@rivosinc.com>
1 parent e0cc6a9 commit 28b817e

File tree

3 files changed

+192
-0
lines changed

3 files changed

+192
-0
lines changed

docs/opentitan/gpio.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,20 @@ a `I` frame whenever its own input lines change.
7777
`Q` and `R` are only emitted when a host connects to QEMU or when one side resets its internal
7878
state.
7979

80+
### Example
81+
82+
The `scripts/opentitan/trellis` directory contains two Python files that may be copied to an
83+
an Adafruit NeoTrellis M4 Express card initialized with Circuit Python 8.0+
84+
85+
These scripts provide a physical, visual interface to the virtual GPIO pins, which is connected to
86+
the QEMU machine over a serial port (a USB CDC VCP in this case).
87+
88+
To connect to the NeoTrellis board, use a configuration such as:
89+
90+
```
91+
-chardev serial,id=gpio,path=/dev/ttyACM1 -global ot-gpio.chardev=gpio
92+
```
93+
94+
where /dev/ttyACM1 is the data serial port of the Neotreillis board.
95+
96+
Note: the first serial port of the board is reserved to its debug console.

scripts/opentitan/treillis/boot.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import usb_cdc
2+
import usb_midi
3+
4+
usb_midi.disable()
5+
usb_cdc.enable(console=True, data=True)

scripts/opentitan/treillis/code.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
"""Simple CircuitPython script (which is stuck with Python 3.4) that maps
2+
QEMU GPIO chardev backend device onto a NeoTreillis M4 Express device.
3+
"""
4+
5+
#pylint: disable=import-error
6+
#pylint: disable=invalid-name
7+
#pylint: disable=missing-function-docstring
8+
#pylint: disable=consider-using-f-string
9+
#pylint: disable=missing-class-docstring
10+
#pylint: disable=too-few-public-methods
11+
#pylint: disable=too-many-branches
12+
#pylint: disable=too-many-instance-attributes
13+
14+
15+
try:
16+
from time import monotonic_ns as now
17+
import usb_cdc
18+
from adafruit_trellism4 import TrellisM4Express
19+
except ImportError:
20+
print('This code should run on Adafruit NeoTrellis M4 Express with '
21+
'CircuitPython')
22+
raise
23+
24+
class OtGPIO:
25+
"""OpenTitan GPIO interface with an Adafruit NeoTrellis M4 Express.
26+
27+
Demonstrate the GPIO protocol over a QEMU CharDev.
28+
"""
29+
30+
GPO_ON = (20, 0, 0) # red
31+
GPO_OFF = (0, 20, 0) # green
32+
GPI_ON = (0, 0, 80) # bright blue
33+
GPI_OFF = (2, 2, 2) # greyish
34+
35+
LOCK_TIME_MS = 300 # key depressed time to lock/unlock an input key
36+
37+
def __init__(self, serial):
38+
self._serial = serial
39+
self._trellis = TrellisM4Express()
40+
# 32-bit bitmaps
41+
self._oe = 0 # output enable (1: out, 0: in)
42+
self._out = 0 # output value (from peer)
43+
self._in = 0 # input value (to peer)
44+
self._kin = 0 # keyboard input
45+
self._lock_in = 0 # locked keys
46+
self._lock_time = {} # when key has been first pressed (ns timestamp)
47+
48+
def _update_input(self, newval, force=False):
49+
ts = now()
50+
change = self._kin ^ newval
51+
self._kin = newval
52+
for pos in range(32):
53+
bit = 1 << pos
54+
# only consider keys that change
55+
if not bit & change:
56+
continue
57+
# is the key down?
58+
down = bool(bit & newval)
59+
if down:
60+
self._lock_time[pos] = ts
61+
continue
62+
# key is released
63+
dtime = self._lock_time.get(pos)
64+
if dtime is None:
65+
continue
66+
delay_ms = (ts - dtime) // 1_000_000
67+
if delay_ms > self.LOCK_TIME_MS:
68+
# Lock action
69+
on = bool(self._lock_in & bit)
70+
if on:
71+
self._lock_in &= ~bit
72+
else:
73+
self._lock_in |= bit
74+
self._lock_time.pop(pos)
75+
self._in = self._lock_in ^ newval
76+
return change
77+
78+
def _update_output(self, newval):
79+
change = (self._out ^ newval) & self._oe
80+
self._out = newval
81+
return change
82+
83+
def _refresh_input(self, change):
84+
for pos in range(32):
85+
bit = 1 << pos
86+
if not bit & change:
87+
continue
88+
y = pos >> 3
89+
x = pos & 0x7
90+
if self._in & bit:
91+
color = self.GPI_ON
92+
else:
93+
color = self.GPI_OFF
94+
self._trellis.pixels[7-x, 3-y] = color
95+
96+
def _refresh_output(self, change):
97+
for pos in range(32):
98+
bit = 1 << pos
99+
if not change & bit:
100+
continue
101+
y = pos >> 3
102+
x = pos & 0x7
103+
if self._out & bit:
104+
color = self.GPO_ON
105+
else:
106+
color = self.GPO_OFF
107+
self._trellis.pixels[7-x, 3-y] = color
108+
109+
def run(self):
110+
self._serial.timeout = 0.1
111+
self._serial.write_timeout = 0.5
112+
# query QEMU to repeat I/O config on startup.
113+
self._serial.write(b'R:00000000\r\n')
114+
buf = bytearray()
115+
last_kin = 0
116+
force = False
117+
while True:
118+
kin = 0
119+
for x, y in self._trellis.pressed_keys:
120+
kin |= 1 << (31 - (8 * y + x))
121+
if last_kin != kin:
122+
change = self._update_input(kin)
123+
if not force:
124+
change &= ~self._oe
125+
self._refresh_input(change)
126+
self._serial.write(b'I:%08x\r\n' % self._in)
127+
last_kin = kin
128+
data = self._serial.read()
129+
if not data:
130+
continue
131+
for b in data:
132+
if b != 0x0d:
133+
buf.append(b)
134+
pos = buf.find(b'\n')
135+
if pos < 0:
136+
continue
137+
line = bytes(buf[:pos])
138+
buf = buf[pos+1:]
139+
if not line:
140+
continue
141+
if len(line) < 2 or line[1] != ord(':'):
142+
continue
143+
cmd = line[0]
144+
try:
145+
val = int(line[2:], 16)
146+
except ValueError:
147+
continue
148+
if cmd == ord('D'):
149+
# update I/O direction
150+
self._oe = val
151+
self._refresh_output(self._oe)
152+
self._refresh_input(~self._oe)
153+
elif cmd == ord('O'):
154+
# update output
155+
change = self._update_output(val)
156+
self._refresh_output(change)
157+
elif cmd == ord('Q'):
158+
# QEMU query for current Input state
159+
self._serial.write(b'I:%08x\r\n' % self._in)
160+
else:
161+
print('Unknown command %s' % cmd)
162+
163+
164+
if __name__ == '__main__':
165+
if not usb_cdc.data:
166+
# boot.py should be used to enable the secondary, data CDC serial-over-USB
167+
# device. The first port is reserved for the Console.
168+
raise RuntimeError('No serial port available')
169+
gpio = OtGPIO(usb_cdc.data)
170+
gpio.run()

0 commit comments

Comments
 (0)