diff --git a/docs/manual/src/applets/index.rst b/docs/manual/src/applets/index.rst index 423181b0f..c9278036d 100644 --- a/docs/manual/src/applets/index.rst +++ b/docs/manual/src/applets/index.rst @@ -13,4 +13,5 @@ Applet index program/index debug/index control/index + sensor/index internal/index diff --git a/docs/manual/src/applets/interface/index.rst b/docs/manual/src/applets/interface/index.rst index c5ffb3f98..44d38fc3d 100644 --- a/docs/manual/src/applets/interface/index.rst +++ b/docs/manual/src/applets/interface/index.rst @@ -17,3 +17,4 @@ I/O interfaces jtag_xvc swd_probe probe_rs + ps2_host diff --git a/docs/manual/src/applets/interface/ps2_host.rst b/docs/manual/src/applets/interface/ps2_host.rst new file mode 100644 index 000000000..402b21b8e --- /dev/null +++ b/docs/manual/src/applets/interface/ps2_host.rst @@ -0,0 +1,7 @@ +``ps2-host`` +============ + +.. _applet.interface.ps2_host: + +.. autoprogram:: glasgow.applet.interface.ps2_host:PS2HostApplet._get_argparser_for_sphinx("ps2-host") + :prog: glasgow run ps2-host diff --git a/docs/manual/src/applets/sensor/index.rst b/docs/manual/src/applets/sensor/index.rst new file mode 100644 index 000000000..a69a5e675 --- /dev/null +++ b/docs/manual/src/applets/sensor/index.rst @@ -0,0 +1,11 @@ +.. _applet.sensor: + +Sensor data collection +====================== + +.. automodule:: glasgow.applet.sensor + +.. toctree:: + :maxdepth: 2 + + mouse_ps2 diff --git a/docs/manual/src/applets/sensor/mouse_ps2.rst b/docs/manual/src/applets/sensor/mouse_ps2.rst new file mode 100644 index 000000000..fdb71da88 --- /dev/null +++ b/docs/manual/src/applets/sensor/mouse_ps2.rst @@ -0,0 +1,7 @@ +``sensor-mouse-ps2`` +==================== + +.. _applet.sensor.mouse_ps2: + +.. autoprogram:: glasgow.applet.sensor.mouse_ps2:SensorMousePS2Applet._get_argparser_for_sphinx("sensor-mouse-ps2") + :prog: glasgow run sensor-mouse-ps2 diff --git a/software/glasgow/applet/interface/ps2_host/__init__.py b/software/glasgow/applet/interface/ps2_host/__init__.py index 3967fb09b..6fbefca84 100644 --- a/software/glasgow/applet/interface/ps2_host/__init__.py +++ b/software/glasgow/applet/interface/ps2_host/__init__.py @@ -35,7 +35,7 @@ # The Intel 8042 controller fixes, or rather works around, this problem by completely encapsulating # the handling of PS/2. In fact many (if not most) PS/2 commands that are sent to i8042 result in # no PS/2 traffic at all, with their response being either synthesized on the fly on the i8042, or, -# even worse, them being an in-band commands to i8042 itself. This lead mice developers to a quite +# even worse, them being in-band commands to i8042 itself. This lead mice developers to a quite # horrifying response: any advanced settings are conveyed to the mouse and back by abusing # the sensitivity adjustment commands as 2-bit at a time a communication channel; keyboards use # similar hacks. @@ -61,11 +61,15 @@ import logging import asyncio + from amaranth import * -from amaranth.lib import data, io -from amaranth.lib.cdc import FFSynchronizer +from amaranth.lib import data, io, wiring, stream, cdc +from amaranth.lib.wiring import In, Out + +from glasgow.applet import GlasgowAppletV2, GlasgowAppletError -from ... import * + +__all__ = ["PS2HostComponent", "PS2HostInterface", "PS2HostError"] _frame_layout = data.StructLayout({ @@ -91,16 +95,18 @@ def _prepare_frame(frame, data): ] -class PS2Bus(Elaboratable): +class PS2Bus(wiring.Component): + falling : Out(1) + rising : Out(1) + clock_i : Out(1, init=1) + clock_o : In(1, init=1) + data_i : Out(1, init=1) + data_o : In(1, init=1) + def __init__(self, ports): self.ports = ports - self.falling = Signal() - self.rising = Signal() - self.clock_i = Signal(init=1) - self.clock_o = Signal(init=1) - self.data_i = Signal(init=1) - self.data_o = Signal(init=1) + super().__init__() def elaborate(self, platform): m = Module() @@ -115,8 +121,8 @@ def elaborate(self, platform): ] m.submodules += [ - FFSynchronizer(clock_buffer.i, self.clock_i, init=1), - FFSynchronizer(data_buffer.i, self.data_i, init=1), + cdc.FFSynchronizer(clock_buffer.i, self.clock_i, init=1), + cdc.FFSynchronizer(data_buffer.i, self.data_i, init=1), ] clock_s = Signal(init=1) @@ -135,19 +141,21 @@ def elaborate(self, platform): return m -class PS2HostController(Elaboratable): - def __init__(self, bus): - self.bus = bus +class PS2HostController(wiring.Component): + en : In(1) # whether communication should be allowed or inhibited + stb : Out(1) # strobed for 1 cycle after each stop bit to indicate an update - self.en = Signal() # whether communication should be allowed or inhibited - self.stb = Signal() # strobed for 1 cycle after each stop bit to indicate an update + i_valid : In(1) # whether i_data is to be transmitted + i_data : In(8) # data byte written to device + i_ack : Out(1) # whether the device acked the data ("line control" bit) - self.i_valid = Signal() # whether i_data is to be transmitted - self.i_data = Signal(8) # data byte written to device - self.i_ack = Signal() # whether the device acked the data ("line control" bit) + o_valid : Out(1) # whether o_data has been received correctly + o_data : Out(8) # data byte read from device + + def __init__(self, bus): + self.bus = bus - self.o_valid = Signal() # whether o_data has been received correctly - self.o_data = Signal(8) # data byte read from device + super().__init__() def elaborate(self, platform): m = Module() @@ -224,32 +232,35 @@ def elaborate(self, platform): return m -class PS2HostSubtarget(Elaboratable): - def __init__(self, ports, in_fifo, out_fifo, inhibit_cyc): - self.ports = ports - self.in_fifo = in_fifo - self.out_fifo = out_fifo - self.inhibit_cyc = inhibit_cyc +class PS2HostComponent(wiring.Component): + i_stream: In(stream.Signature(8)) + o_stream: Out(stream.Signature(8)) + + def __init__(self, ports, *, inhibit_cyc): + self._ports = ports + self._inhibit_cyc = inhibit_cyc + + super().__init__() def elaborate(self, platform): m = Module() - m.submodules.bus = bus = PS2Bus(self.ports) + m.submodules.bus = bus = PS2Bus(self._ports) m.submodules.ctrl = ctrl = PS2HostController(bus) - timer = Signal(range(self.inhibit_cyc)) + timer = Signal(range(self._inhibit_cyc)) count = Signal(7) error = Signal() with m.FSM(): with m.State("RECV-COMMAND"): - m.d.comb += self.out_fifo.r_en.eq(1) - with m.If(self.out_fifo.r_rdy): + m.d.comb += self.i_stream.ready.eq(1) + with m.If(self.i_stream.valid): m.d.sync += [ - count.eq(self.out_fifo.r_data[:7]), + count.eq(self.i_stream.payload[:7]), error.eq(0), ] - with m.If(self.out_fifo.r_data[7]): + with m.If(self.i_stream.payload[7]): m.d.sync += ctrl.i_valid.eq(1) m.next = "WRITE-BYTE" with m.Else(): @@ -260,10 +271,10 @@ def elaborate(self, platform): m.next = "READ-WAIT" with m.State("WRITE-BYTE"): - m.d.comb += self.out_fifo.r_en.eq(1) - with m.If(self.out_fifo.r_rdy): + m.d.comb += self.i_stream.ready.eq(1) + with m.If(self.i_stream.valid): m.d.sync += [ - ctrl.i_data.eq(self.out_fifo.r_data), + ctrl.i_data.eq(self.i_stream.payload), ctrl.en.eq(1), ] m.next = "WRITE-WAIT" @@ -275,8 +286,8 @@ def elaborate(self, platform): # You better be reading from that FIFO. (But the FIFO is 4× larger than the largest # possible command response, so it's never a race.) m.d.comb += [ - self.in_fifo.w_en.eq(1), - self.in_fifo.w_data.eq(ctrl.i_ack), + self.o_stream.valid.eq(1), + self.o_stream.payload.eq(ctrl.i_ack), ] with m.If((count == 0) | ~ctrl.i_ack): m.next = "INHIBIT" @@ -290,8 +301,8 @@ def elaborate(self, platform): m.next = "READ-BYTE" with m.State("READ-BYTE"): m.d.comb += [ - self.in_fifo.w_en.eq(1), - self.in_fifo.w_data.eq(ctrl.o_data), + self.o_stream.valid.eq(1), + self.o_stream.payload.eq(ctrl.o_data), ] with m.If(~ctrl.o_valid & (error == 0)): m.d.sync += error.eq(count) @@ -302,8 +313,8 @@ def elaborate(self, platform): with m.State("SEND-ERROR"): m.d.comb += [ - self.in_fifo.w_en.eq(1), - self.in_fifo.w_data.eq(error), + self.o_stream.valid.eq(1), + self.o_stream.payload.eq(error), ] m.next = "INHIBIT" @@ -312,7 +323,7 @@ def elaborate(self, platform): # sending two back-to-back commands (or a command and a read, etc). m.d.sync += [ ctrl.en.eq(0), - timer.eq(self.inhibit_cyc), + timer.eq(self._inhibit_cyc - 1), ] m.next = "INHIBIT-WAIT" @@ -330,10 +341,17 @@ class PS2HostError(GlasgowAppletError): class PS2HostInterface: - def __init__(self, interface, logger): - self._lower = interface + def __init__(self, logger, assembly, *, clock, data, reset): self._logger = logger self._level = logging.DEBUG if self._logger.name == __name__ else logging.TRACE + + inhibit_cyc = round(60e-6 / assembly.sys_clk_period) + + ports = assembly.add_port_group(clock=clock, data=data, reset=reset) + assembly.use_pulls({clock: "high", data: "high"}) + component = assembly.add_submodule(PS2HostComponent(ports, inhibit_cyc=inhibit_cyc)) + self._pipe = assembly.add_inout_pipe(component.o_stream, component.i_stream) + self._streaming = False def _log(self, message, *args): @@ -342,13 +360,14 @@ def _log(self, message, *args): async def send_command(self, cmd, ret=0): assert ret < 0x7f assert not self._streaming - await self._lower.write([0x80|(ret + 1), cmd]) - line_ack, = await self._lower.read(1) + await self._pipe.send([0x80|(ret + 1), cmd]) + await self._pipe.flush() + line_ack, = await self._pipe.recv(1) if not line_ack: self._log("cmd=%02x nak", cmd) raise PS2HostError("peripheral did not acknowledge command {:#04x}" .format(cmd)) - cmd_ack, *result, error = await self._lower.read(1 + ret + 1) + cmd_ack, *result, error = await self._pipe.recv(1 + ret + 1) result = bytes(result) self._log("cmd=%02x ack=%02x ret=<%s>", cmd, cmd_ack, result.hex()) if error > 0: @@ -373,8 +392,9 @@ async def send_command(self, cmd, ret=0): async def recv_packet(self, ret): assert ret < 0x7f assert not self._streaming - await self._lower.write([ret]) - *result, error = await self._lower.read(ret + 1) + await self._pipe.send([ret]) + await self._pipe.flush() + *result, error = await self._pipe.recv(ret + 1) result = bytes(result) self._log("ret=<%s>", result.hex()) if error > 0: @@ -385,12 +405,13 @@ async def recv_packet(self, ret): async def stream(self, callback): assert not self._streaming self._streaming = True - await self._lower.write([0x7f]) + await self._pipe.send([0x7f]) + await self._pipe.flush() while True: - await callback(*await self._lower.read(1)) + await callback(*await self._pipe.recv(1)) -class PS2HostApplet(GlasgowApplet): +class PS2HostApplet(GlasgowAppletV2): logger = logging.getLogger(__name__) help = "communicate with IBM PS/2 peripherals" description = """ @@ -404,44 +425,31 @@ class PS2HostApplet(GlasgowApplet): @classmethod def add_build_arguments(cls, parser, access): - super().add_build_arguments(parser, access) - + access.add_voltage_argument(parser) access.add_pins_argument(parser, "clock", default=True) access.add_pins_argument(parser, "data", default=True) access.add_pins_argument(parser, "reset") - def build(self, target, args): - self.mux_interface = iface = target.multiplexer.claim_interface(self, args) - subtarget = iface.add_subtarget(PS2HostSubtarget( - ports =iface.get_port_group( - clock = args.clock, - data = args.data, - reset = args.reset - ), - in_fifo=iface.get_in_fifo(), - out_fifo=iface.get_out_fifo(), - inhibit_cyc=int(target.sys_clk_freq * 60e-6), - )) - - async def run(self, device, args): - iface = await device.demultiplexer.claim_interface(self, self.mux_interface, args, - pull_high={args.clock, args.data}) - return PS2HostInterface(iface, self.logger) + def build(self, args): + with self.assembly.add_applet(self): + self.assembly.use_voltage(args.voltage) + self.ps2_iface = PS2HostInterface(self.logger, self.assembly, + clock=args.clock, data=args.data, reset=args.reset) @classmethod - def add_interact_arguments(cls, parser): + def add_run_arguments(cls, parser): def hex_bytes(arg): return bytes.fromhex(arg) parser.add_argument( "init", metavar="INIT", type=hex_bytes, nargs="?", default=b"", help="send each byte from INIT as an initialization command") - async def interact(self, device, args, iface): + async def run(self, args): for init_byte in args.init: - await iface.send_command(init_byte) + await self.ps2_iface.send_command(init_byte) async def print_byte(byte): print(f"{byte:02x}", end=" ", flush=True) - await iface.stream(print_byte) + await self.ps2_iface.stream(print_byte) @classmethod def tests(cls): diff --git a/software/glasgow/applet/interface/ps2_host/test.py b/software/glasgow/applet/interface/ps2_host/test.py index 40c6520b8..d3e0e18fc 100644 --- a/software/glasgow/applet/interface/ps2_host/test.py +++ b/software/glasgow/applet/interface/ps2_host/test.py @@ -1,8 +1,8 @@ -from ... import * +from glasgow.applet import GlasgowAppletV2TestCase, synthesis_test from . import PS2HostApplet -class PS2HostAppletTestCase(GlasgowAppletTestCase, applet=PS2HostApplet): +class PS2HostAppletTestCase(GlasgowAppletV2TestCase, applet=PS2HostApplet): @synthesis_test def test_build(self): self.assertBuilds() diff --git a/software/glasgow/applet/sensor/mouse_ps2/__init__.py b/software/glasgow/applet/sensor/mouse_ps2/__init__.py index 078ccbd08..3cf86bb88 100644 --- a/software/glasgow/applet/sensor/mouse_ps2/__init__.py +++ b/software/glasgow/applet/sensor/mouse_ps2/__init__.py @@ -19,12 +19,15 @@ # # See also the note on the i8042 controller in the ps2-host applet. -from collections import namedtuple +from dataclasses import dataclass import logging import asyncio -from ... import * -from ...interface.ps2_host import PS2HostApplet +from glasgow.applet import GlasgowAppletV2, GlasgowAppletError +from ...interface.ps2_host import PS2HostApplet, PS2HostInterface + + +__all__ = ["SensorMousePS2Interface", "SensorMousePS2Report", "SensorMousePS2Error"] CMD_RESET = 0xff @@ -65,9 +68,18 @@ REP_5TH_BUTTON = 0b10_0000 -SensorMousePS2Report = namedtuple("SensorMousePS2Report", - ("left", "right", "middle", "button_4", "button_5", - "offset_x", "offset_y", "offset_z", "overflow_x", "overflow_y")) +@dataclass +class SensorMousePS2Report: + left: bool = False + right: bool = False + middle: bool = False + button_4: bool = False + button_5: bool = False + offset_x: int = 0 + offset_y: int = 0 + offset_z: int = 0 + overflow_x: bool = False + overflow_y: bool = False class SensorMousePS2Error(GlasgowAppletError): @@ -75,16 +87,17 @@ class SensorMousePS2Error(GlasgowAppletError): class SensorMousePS2Interface: - def __init__(self, interface, logger): - self.lower = interface + def __init__(self, logger, assembly, *, clock, data, reset): self._logger = logger self._level = logging.DEBUG if self._logger.name == __name__ else logging.TRACE + self.ps2 = PS2HostInterface(logger, assembly, clock=clock, data=data, reset=reset) + def _log(self, message, *args): self._logger.log(self._level, "PS/2 Mouse: " + message, *args) async def reset(self): - bat_result, = await self.lower.send_command(CMD_RESET, ret=1) + bat_result, = await self.ps2.send_command(CMD_RESET, ret=1) self._log("reset bat-result=%02x", bat_result) if bat_result == 0xaa: pass # passed @@ -95,7 +108,7 @@ async def reset(self): .format(bat_result)) async def identify(self): - ident, = await self.lower.send_command(CMD_GET_DEVICE_ID, ret=1) + ident, = await self.ps2.send_command(CMD_GET_DEVICE_ID, ret=1) self._log("ident=%02x", ident) return ident @@ -114,36 +127,36 @@ async def probe(self): async def set_reporting(self, enabled=True): self._log("reporting=%s", "on" if enabled else "off") if enabled: - await self.lower.send_command(CMD_ENABLE_REPORTING) + await self.ps2.send_command(CMD_ENABLE_REPORTING) else: - await self.lower.send_command(CMD_DISABLE_REPORTING) + await self.ps2.send_command(CMD_DISABLE_REPORTING) async def set_sample_rate(self, rate): assert rate in ARG_SAMPLE_RATES self._log("sample-rate=%d [report/s]", rate) - await self.lower.send_command(CMD_SET_SAMPLE_RATE) - await self.lower.send_command(rate) + await self.ps2.send_command(CMD_SET_SAMPLE_RATE) + await self.ps2.send_command(rate) async def set_remote_mode(self): self._log("mode=remote") - await self.lower.send_command(CMD_SET_REMOTE_MODE) + await self.ps2.send_command(CMD_SET_REMOTE_MODE) async def set_stream_mode(self): self._log("mode=stream") - await self.lower.send_command(CMD_SET_STREAM_MODE) + await self.ps2.send_command(CMD_SET_STREAM_MODE) async def set_resolution(self, resolution): assert resolution in ARG_RESOLUTIONS self._log("resolution=%d [count/mm]", resolution) - await self.lower.send_command(CMD_SET_RESOLUTION) - await self.lower.send_command(ARG_RESOLUTIONS.index(resolution)) + await self.ps2.send_command(CMD_SET_RESOLUTION) + await self.ps2.send_command(ARG_RESOLUTIONS.index(resolution)) async def set_autospeed(self, enabled): self._log("autospeed=%s", "on" if enabled else "off") if enabled: - await self.lower.send_command(CMD_ENABLE_AUTOSPEED) + await self.ps2.send_command(CMD_ENABLE_AUTOSPEED) else: - await self.lower.send_command(CMD_DISABLE_AUTOSPEED) + await self.ps2.send_command(CMD_DISABLE_AUTOSPEED) def _size_report(self, ident): if ident == ID_MOUSE_STANDARD: @@ -193,14 +206,14 @@ def _decode_report(self, ident, packet): async def request_report(self, ident=None): if ident is None: ident = await self.identify() - packet = await self.lower.send_command(CMD_READ_DATA, ret=self._size_report(ident)) + packet = await self.ps2.send_command(CMD_READ_DATA, ret=self._size_report(ident)) return self._decode_report(ident, packet) async def request_report(self, ident=None): if ident is None: ident = await self.identify() size = self._size_report(ident) - packet = await self.lower.send_command(CMD_READ_DATA, ret=size) + packet = await self.ps2.send_command(CMD_READ_DATA, ret=size) return self._decode_report(ident, packet) async def stream_reports(self, ident=None): @@ -211,11 +224,11 @@ async def stream_reports(self, ident=None): size = self._size_report(ident) more = True while more or more is None: - packet = await self.lower.recv_packet(size) + packet = await self.ps2.recv_packet(size) more = (yield self._decode_report(ident, packet)) -class SensorMousePS2Applet(PS2HostApplet): +class SensorMousePS2Applet(GlasgowAppletV2): logger = logging.getLogger(__name__) help = "receive axis and button information from PS/2 mice" description = """ @@ -223,16 +236,26 @@ class SensorMousePS2Applet(PS2HostApplet): may be logged or forwarded to the desktop on Linux. This applet has additional Python dependencies: - * uinput (optional, required for Linux desktop forwarding) + + * uinput (optional, required for Linux desktop forwarding) """ + required_revision = PS2HostApplet.required_revision - async def run(self, device, args): - ps2_iface = await self.run_lower(SensorMousePS2Applet, device, args) - mouse_iface = SensorMousePS2Interface(ps2_iface, self.logger) - return mouse_iface + @classmethod + def add_build_arguments(cls, parser, access): + access.add_voltage_argument(parser) + access.add_pins_argument(parser, "clock", default=True) + access.add_pins_argument(parser, "data", default=True) + access.add_pins_argument(parser, "reset") + + def build(self, args): + with self.assembly.add_applet(self): + self.assembly.use_voltage(args.voltage) + self.mouse_iface = SensorMousePS2Interface(self.logger, self.assembly, + clock=args.clock, data=args.data, reset=args.reset) @classmethod - def add_interact_arguments(cls, parser): + def add_run_arguments(cls, parser): parser.add_argument( "--no-reset", dest="reset", default=True, action="store_false", help="do not send the reset command before initialization (does not affect reset pin)") @@ -242,10 +265,12 @@ def add_interact_arguments(cls, parser): parser.add_argument( "-r", "--resolution", metavar="RES", type=int, choices=ARG_RESOLUTIONS, - help="set resolution to RES counts/mm (one of: %(choices)s)") + help="set resolution to RES counts/mm " + f"(one of: {', '.join(str(v) for v in ARG_RESOLUTIONS)})") parser.add_argument( "-s", "--sample-rate", metavar="RATE", type=int, choices=ARG_SAMPLE_RATES, - help="set sample rate to RATE reports/s (one of: %(choices)s)") + help="set sample rate to RATE reports/s " + f"(one of: {', '.join(str(v) for v in ARG_SAMPLE_RATES)})") parser.add_argument( "-a", "--acceleration", dest="acceleration", default=None, action="store_true", help="enable acceleration (also known as autospeed and scaling)") @@ -267,14 +292,14 @@ def add_interact_arguments(cls, parser): "-y", "--invert-y", default=1, action="store_const", const=-1, help="invert Y axis offsets") - async def interact(self, device, args, mouse_iface): + async def run(self, args): async def initialize(): if args.reset: - await mouse_iface.reset() + await self.mouse_iface.reset() if args.probe: - return await mouse_iface.probe() + return await self.mouse_iface.probe() else: - return await mouse_iface.identify() + return await self.mouse_iface.identify() try: ident = await asyncio.wait_for(initialize(), timeout=1) @@ -291,14 +316,14 @@ async def initialize(): self.logger.warning("found unknown mouse with ID %#04x", ident) if args.resolution is not None: - await mouse_iface.set_resolution(args.resolution) + await self.mouse_iface.set_resolution(args.resolution) if args.sample_rate is not None: - await mouse_iface.set_sample_rate(args.sample_rate) + await self.mouse_iface.set_sample_rate(args.sample_rate) if args.acceleration is not None: - await mouse_iface.set_autospeed(args.acceleration) + await self.mouse_iface.set_autospeed(args.acceleration) if args.operation == "stream-log": - async for report in mouse_iface.stream_reports(ident): + async for report in self.mouse_iface.stream_reports(ident): overflow = report.overflow_x or report.overflow_y self.logger.log(logging.WARN if overflow else logging.INFO, "btn=%s%s%s%s%s x=%+4d%s y=%+4d%s z=%+2d", @@ -327,7 +352,7 @@ async def initialize(): uinput.REL_WHEEL, ]) - async for report in mouse_iface.stream_reports(ident): + async for report in self.mouse_iface.stream_reports(ident): device.emit(uinput.BTN_LEFT, report.left, syn=False) device.emit(uinput.BTN_MIDDLE, report.middle, syn=False) device.emit(uinput.BTN_RIGHT, report.right, syn=False) diff --git a/software/pyproject.toml b/software/pyproject.toml index a807b1045..dd9ba4374 100644 --- a/software/pyproject.toml +++ b/software/pyproject.toml @@ -78,6 +78,10 @@ numpy = [ "matplotlib~=3.10" ] +uinput = [ + "python-uinput>=1.0.1", +] + [project.scripts] glasgow = "glasgow.cli:run_main"