├── 3d-models ├── Trackball14-Bottom.stl ├── Trackball14-Button.stl ├── Trackball14-Ring.stl ├── Trackball14-Top.stl └── Trackball14.step ├── README.md ├── config-tool ├── requirements.txt ├── trackball-config.py └── trackball.png ├── firmware ├── CMakeLists.txt ├── pico_sdk_import.cmake ├── src │ ├── crc.cc │ ├── crc.h │ ├── pmw3360.cc │ ├── pmw3360.h │ ├── registers.h │ ├── srom.cc │ ├── srom.h │ ├── trackball.cc │ └── tusb_config.h └── trackball.uf2 └── images ├── config-tool.png ├── exploded.png ├── internals1.jpg ├── internals2.jpg └── trackball.jpg /3d-models/Trackball14-Bottom.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfedor2/scroll-ring-trackball/bcc73b1298b87e88c36e843fc690b19b71f73898/3d-models/Trackball14-Bottom.stl -------------------------------------------------------------------------------- /3d-models/Trackball14-Button.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfedor2/scroll-ring-trackball/bcc73b1298b87e88c36e843fc690b19b71f73898/3d-models/Trackball14-Button.stl -------------------------------------------------------------------------------- /3d-models/Trackball14-Ring.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfedor2/scroll-ring-trackball/bcc73b1298b87e88c36e843fc690b19b71f73898/3d-models/Trackball14-Ring.stl -------------------------------------------------------------------------------- /3d-models/Trackball14-Top.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfedor2/scroll-ring-trackball/bcc73b1298b87e88c36e843fc690b19b71f73898/3d-models/Trackball14-Top.stl -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DIY trackball with scroll ring 2 | 3 | This repository contains design files and code that can be used to make a fully programmable four-button USB trackball with a scroll ring. 4 | 5 | ![Trackball photo](images/trackball.jpg) 6 | 7 | It uses the following components: 8 | 9 | * 57.2mm billiard ball 10 | * PMW3360 optical sensor 11 | * [RP2040+PMW3360 PCB](https://github.com/jfedor2/rp2040-pmw3360) 12 | * [button switch mount PCBs](https://github.com/jfedor2/mouse-switch-mount-pcb) 13 | * [3D printed case](3d-models) 14 | * Omron D2FC switches (or similar) 15 | * 2x [SS49E Hall effect sensors](https://sps.honeywell.com/us/en/products/sensing-and-iot/sensors/magnetic-sensors/linear-and-angle-sensor-ics/ss39et-ss49e-ss59et-linear-sensor-ics) (TO-92 package) 16 | * 72x 2x2mm neodymium cylinder magnets (axially magnetized) 17 | * 60x78x10mm bearing (6812 ZZ) 18 | * 2.5mm zirconium oxide (or silicon nitride) bearing balls 19 | * M3x4 screws for mounting the main PCB and assembling the case 20 | * M2x4 screws for mounting the buttons and the secondary PCBs 21 | * USB cable (cut off the device end and solder the wires directly to the PCB) 22 | * some hookup wire 23 | 24 | The main PCB uses the RP2040 chip from Raspberry Pi. The firmware supports remapping of ball, button and ring functions using a configuration tool without the need to recompile the source code. All the configuration is stored on device, so the configuration tool is only needed to change the settings, not during normal use. 25 | 26 | ![Configuration tool UI screenshot](images/config-tool.png) 27 | 28 | So far I only tested the configuration tool on Linux, but it should in theory run on Windows and Mac as well. I will try to provide ready-to-use packages in the future. 29 | 30 | The provided [UF2 file](firmware/trackball.uf2) can be used to flash the firmware onto the device. When first connected, a "RPI-RP2" drive will show up and you flash the chip by copying the UF2 file to that drive. If you want to flash it again, hold the BOOT button and press the RESET button on the board. 31 | 32 | I printed the case parts with 0.2mm layer height. They don't need supports. 33 | 34 | The bearing and the magnets are press-fit into the case, but depending on your particular printer you might want to use glue. The Hall effect sensors should be oriented "face-up" and will definitely need some glue to stay in place. 35 | 36 | The magnets should be installed with alternating polarity (the outside-facing pole should be N-S-N-S... etc.). 37 | 38 | The Hall sensors' + leads are connected to 3.3V. The rest of the pin numbers are defined at the top of [trackball.cc](firmware/src/trackball.cc). 39 | 40 | ![Exploded view](images/exploded.png) 41 | 42 | ![Internals of the trackball](images/internals1.jpg) 43 | 44 | ![Internals of the trackball](images/internals2.jpg) 45 | -------------------------------------------------------------------------------- /config-tool/requirements.txt: -------------------------------------------------------------------------------- 1 | hid 2 | gobject 3 | PyGObject 4 | -------------------------------------------------------------------------------- /config-tool/trackball-config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import struct 5 | import binascii 6 | import traceback 7 | import gi 8 | import hid 9 | 10 | 11 | gi.require_version("Gtk", "3.0") 12 | from gi.repository import Gtk 13 | 14 | VID = 0xCAFE 15 | PID = 0xBAFA 16 | CONFIG_SIZE = 22 17 | REPORT_ID = 3 18 | CONFIG_VERSION = 1 19 | PICTURE_FILENAME = os.path.join(os.path.dirname(__file__), "trackball.png") 20 | 21 | BALL_FUNCTIONS = ( 22 | ("None", "0"), 23 | ("Cursor X", "1"), 24 | ("Cursor Y", "2"), 25 | ("V scroll", "3"), 26 | ("H scroll", "4"), 27 | ("Cursor X (inverted)", "-1"), 28 | ("Cursor Y (inverted)", "-2"), 29 | ("V scroll (inverted)", "-3"), 30 | ("H scroll (inverted)", "-4"), 31 | ) 32 | 33 | BUTTON_FUNCTIONS = ( 34 | ("None", "0"), 35 | ("Button 1 (left)", "1"), 36 | ("Button 2 (right)", "2"), 37 | ("Button 3 (middle)", "3"), 38 | ("Button 4 (back)", "4"), 39 | ("Button 5 (forward)", "5"), 40 | ("Button 6", "6"), 41 | ("Button 7", "7"), 42 | ("Button 8", "8"), 43 | ("Click-drag", "9"), 44 | ("Shift", "10"), 45 | ) 46 | 47 | RING_FUNCTIONS = ( 48 | ("None", "0"), 49 | ("V scroll", "1"), 50 | ("H scroll", "2"), 51 | ("V scroll (inverted)", "-1"), 52 | ("H scroll (inverted)", "-2"), 53 | ) 54 | 55 | 56 | def make_model(options): 57 | model = Gtk.ListStore(str, str) 58 | for o in options: 59 | model.append(o) 60 | return model 61 | 62 | 63 | def make_dropdown(model): 64 | dropdown = Gtk.ComboBox.new_with_model(model) 65 | renderer_text = Gtk.CellRendererText() 66 | dropdown.pack_start(renderer_text, True) 67 | dropdown.add_attribute(renderer_text, "text", 0) 68 | dropdown.set_id_column(1) 69 | dropdown.set_active_id("0") 70 | return dropdown 71 | 72 | 73 | def make_scale(): 74 | scale = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, 1, 120, 1) 75 | scale.connect("format-value", lambda _, value: str(int(value) * 100)) 76 | return scale 77 | 78 | 79 | class TrackballConfigWindow(Gtk.Window): 80 | def __init__(self): 81 | ball_function_model = make_model(BALL_FUNCTIONS) 82 | button_function_model = make_model(BUTTON_FUNCTIONS) 83 | ring_function_model = make_model(RING_FUNCTIONS) 84 | self.devices_model = Gtk.ListStore(str, str) 85 | 86 | Gtk.Window.__init__(self, title="Trackball Configuration") 87 | 88 | self.set_border_width(10) 89 | 90 | hbox = Gtk.Box() 91 | 92 | vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) 93 | 94 | devices_hbox = Gtk.Box() 95 | self.devices_dropdown = make_dropdown(self.devices_model) 96 | devices_hbox.pack_start(self.devices_dropdown, True, True, 0) 97 | refresh_button = Gtk.Button.new_from_icon_name( 98 | "view-refresh", Gtk.IconSize.BUTTON 99 | ) 100 | refresh_button.set_tooltip_text("Refresh device list") 101 | refresh_button.connect("clicked", self.refresh_button_clicked) 102 | devices_hbox.pack_start(refresh_button, False, False, 0) 103 | vbox.pack_start(devices_hbox, True, True, 0) 104 | 105 | actions_hbox = Gtk.Box() 106 | self.load_button = Gtk.Button.new_with_label("Load from device") 107 | self.load_button.connect("clicked", self.load_button_clicked) 108 | actions_hbox.pack_start(self.load_button, True, True, 0) 109 | self.save_button = Gtk.Button.new_with_label("Save to device") 110 | self.save_button.connect("clicked", self.save_button_clicked) 111 | actions_hbox.pack_start(self.save_button, True, True, 0) 112 | vbox.pack_start(actions_hbox, True, True, 0) 113 | 114 | grid = Gtk.Grid(column_spacing=6, row_spacing=6) 115 | 116 | row = 0 117 | grid.attach(Gtk.Label("Normal", halign=Gtk.Align.CENTER), 1, row, 1, 1) 118 | grid.attach(Gtk.Label("Shifted", halign=Gtk.Align.CENTER), 2, row, 1, 1) 119 | row += 1 120 | grid.attach(Gtk.Label("Ball X axis", halign=Gtk.Align.END), 0, row, 1, 1) 121 | self.ball_x_dropdown = make_dropdown(ball_function_model) 122 | grid.attach(self.ball_x_dropdown, 1, row, 1, 1) 123 | self.ball_x_shifted_dropdown = make_dropdown(ball_function_model) 124 | grid.attach(self.ball_x_shifted_dropdown, 2, row, 1, 1) 125 | row += 1 126 | grid.attach(Gtk.Label("Ball Y axis", halign=Gtk.Align.END), 0, row, 1, 1) 127 | self.ball_y_dropdown = make_dropdown(ball_function_model) 128 | grid.attach(self.ball_y_dropdown, 1, row, 1, 1) 129 | self.ball_y_shifted_dropdown = make_dropdown(ball_function_model) 130 | grid.attach(self.ball_y_shifted_dropdown, 2, row, 1, 1) 131 | row += 1 132 | grid.attach(Gtk.Label("Ball CPI", halign=Gtk.Align.END), 0, row, 1, 1) 133 | self.ball_cpi = make_scale() 134 | grid.attach(self.ball_cpi, 1, row, 1, 1) 135 | self.ball_cpi_shifted = make_scale() 136 | grid.attach(self.ball_cpi_shifted, 2, row, 1, 1) 137 | row += 1 138 | grid.attach(Gtk.Label("Ring", halign=Gtk.Align.END), 0, row, 1, 1) 139 | self.ring_dropdown = make_dropdown(ring_function_model) 140 | grid.attach(self.ring_dropdown, 1, row, 1, 1) 141 | self.ring_shifted_dropdown = make_dropdown(ring_function_model) 142 | grid.attach(self.ring_shifted_dropdown, 2, row, 1, 1) 143 | row += 1 144 | grid.attach(Gtk.Label("Button 1", halign=Gtk.Align.END), 0, row, 1, 1) 145 | self.button1_dropdown = make_dropdown(button_function_model) 146 | grid.attach(self.button1_dropdown, 1, row, 1, 1) 147 | self.button1_shifted_dropdown = make_dropdown(button_function_model) 148 | grid.attach(self.button1_shifted_dropdown, 2, row, 1, 1) 149 | row += 1 150 | grid.attach(Gtk.Label("Button 2", halign=Gtk.Align.END), 0, row, 1, 1) 151 | self.button2_dropdown = make_dropdown(button_function_model) 152 | grid.attach(self.button2_dropdown, 1, row, 1, 1) 153 | self.button2_shifted_dropdown = make_dropdown(button_function_model) 154 | grid.attach(self.button2_shifted_dropdown, 2, row, 1, 1) 155 | row += 1 156 | grid.attach(Gtk.Label("Button 3", halign=Gtk.Align.END), 0, row, 1, 1) 157 | self.button3_dropdown = make_dropdown(button_function_model) 158 | grid.attach(self.button3_dropdown, 1, row, 1, 1) 159 | self.button3_shifted_dropdown = make_dropdown(button_function_model) 160 | grid.attach(self.button3_shifted_dropdown, 2, row, 1, 1) 161 | row += 1 162 | grid.attach(Gtk.Label("Button 4", halign=Gtk.Align.END), 0, row, 1, 1) 163 | self.button4_dropdown = make_dropdown(button_function_model) 164 | grid.attach(self.button4_dropdown, 1, row, 1, 1) 165 | self.button4_shifted_dropdown = make_dropdown(button_function_model) 166 | grid.attach(self.button4_shifted_dropdown, 2, row, 1, 1) 167 | 168 | vbox.pack_start(grid, True, True, 0) 169 | 170 | hbox.pack_start(vbox, True, True, 10) 171 | 172 | image = Gtk.Image.new_from_file(PICTURE_FILENAME) 173 | hbox.pack_start(image, True, True, 10) 174 | 175 | self.refresh_device_list() 176 | 177 | self.add(hbox) 178 | 179 | def wrap_exception_in_dialog(self, f): 180 | try: 181 | f() 182 | except Exception as e: 183 | dialog = Gtk.MessageDialog( 184 | transient_for=self, 185 | flags=0, 186 | message_type=Gtk.MessageType.ERROR, 187 | buttons=Gtk.ButtonsType.OK, 188 | text=traceback.format_exc(), 189 | ) 190 | dialog.run() 191 | dialog.destroy() 192 | 193 | def refresh_button_clicked(self, button): 194 | self.refresh_device_list() 195 | 196 | def refresh_device_list(self): 197 | self.devices_model.clear() 198 | devices = [ 199 | d 200 | for d in hid.enumerate() 201 | if d["vendor_id"] == VID and d["product_id"] == PID 202 | ] 203 | if devices: 204 | for d in devices: 205 | self.devices_model.append( 206 | ( 207 | f"{d['manufacturer_string']} {d['product_string']}", 208 | str(d["path"], "ascii"), 209 | ) 210 | ) 211 | self.load_button.set_sensitive(True) 212 | self.save_button.set_sensitive(True) 213 | else: 214 | self.devices_model.append(("No devices found", "NULL")) 215 | self.load_button.set_sensitive(False) 216 | self.save_button.set_sensitive(False) 217 | self.devices_dropdown.set_active(0) 218 | 219 | def load_button_clicked(self, button): 220 | self.wrap_exception_in_dialog(self.load_config_from_device) 221 | 222 | def load_config_from_device(self): 223 | path = self.devices_dropdown.get_active_id() 224 | device = hid.Device(path=bytes(path, "ascii")) 225 | data = device.get_feature_report(REPORT_ID, CONFIG_SIZE + 1) 226 | ( 227 | report_id, 228 | version, 229 | command, 230 | ball_x, 231 | ball_y, 232 | ball_x_shifted, 233 | ball_y_shifted, 234 | ball_cpi, 235 | ball_cpi_shifted, 236 | ring, 237 | ring_shifted, 238 | button1, 239 | button2, 240 | button3, 241 | button4, 242 | button1_shifted, 243 | button2_shifted, 244 | button3_shifted, 245 | button4_shifted, 246 | crc32, 247 | ) = struct.unpack("/external/pico_sdk_import.cmake 2 | 3 | # This can be dropped into an external project to help locate this SDK 4 | # It should be include()ed prior to project() 5 | 6 | if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) 7 | set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) 8 | message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") 9 | endif () 10 | 11 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) 12 | set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) 13 | message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") 14 | endif () 15 | 16 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) 17 | set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) 18 | message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") 19 | endif () 20 | 21 | set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") 22 | set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") 23 | set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") 24 | 25 | if (NOT PICO_SDK_PATH) 26 | if (PICO_SDK_FETCH_FROM_GIT) 27 | include(FetchContent) 28 | set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) 29 | if (PICO_SDK_FETCH_FROM_GIT_PATH) 30 | get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") 31 | endif () 32 | FetchContent_Declare( 33 | pico_sdk 34 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 35 | GIT_TAG master 36 | ) 37 | if (NOT pico_sdk) 38 | message("Downloading Raspberry Pi Pico SDK") 39 | FetchContent_Populate(pico_sdk) 40 | set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) 41 | endif () 42 | set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) 43 | else () 44 | message(FATAL_ERROR 45 | "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." 46 | ) 47 | endif () 48 | endif () 49 | 50 | get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") 51 | if (NOT EXISTS ${PICO_SDK_PATH}) 52 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") 53 | endif () 54 | 55 | set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) 56 | if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) 57 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") 58 | endif () 59 | 60 | set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) 61 | 62 | include(${PICO_SDK_INIT_CMAKE_FILE}) 63 | -------------------------------------------------------------------------------- /firmware/src/crc.cc: -------------------------------------------------------------------------------- 1 | #include "crc.h" 2 | 3 | uint32_t crc_table[] = { 4 | 0x0, 0x77073096, 0xEE0E612C, 0x990951BA, 0x76DC419, 0x706AF48F, 0xE963A535, 5 | 0x9E6495A3, 0xEDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x9B64C2B, 6 | 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 7 | 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 8 | 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 9 | 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 10 | 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 11 | 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 12 | 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 13 | 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 14 | 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x1DB7106, 0x98D220BC, 15 | 0xEFD5102A, 0x71B18589, 0x6B6B51F, 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 16 | 0xF00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x86D3D2D, 0x91646C97, 17 | 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 18 | 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 19 | 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 20 | 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 21 | 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 22 | 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 23 | 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, 24 | 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 25 | 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x3B6E20C, 0x74B1D29A, 0xEAD54739, 26 | 0x9DD277AF, 0x4DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0xD6D6A3E, 27 | 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0xA00AE27, 0x7D079EB1, 0xF00F9344, 28 | 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 29 | 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 30 | 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 31 | 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 32 | 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 33 | 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 34 | 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 35 | 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 36 | 0xEC63F226, 0x756AA39C, 0x26D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 37 | 0x5005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0xCB61B38, 0x92D28E9B, 38 | 0xE5D5BE0D, 0x7CDCEFB7, 0xBDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 39 | 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, 40 | 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 41 | 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 42 | 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 43 | 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 44 | 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 45 | 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 46 | 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D 47 | }; 48 | 49 | uint32_t crc32(const uint8_t* buf, int len) { 50 | uint32_t c = 0xffffffffL; 51 | int n; 52 | 53 | for (n = 0; n < len; n++) { 54 | c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8); 55 | } 56 | return c ^ 0xffffffffL; 57 | } 58 | -------------------------------------------------------------------------------- /firmware/src/crc.h: -------------------------------------------------------------------------------- 1 | #ifndef _CRC_H_ 2 | #define _CRC_H_ 3 | 4 | #include 5 | 6 | uint32_t crc32(const uint8_t* buf, int len); 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /firmware/src/pmw3360.cc: -------------------------------------------------------------------------------- 1 | // derived from https://github.com/mrjohnk/PMW3360DM-T2QU 2 | 3 | #include 4 | 5 | #include "pmw3360.h" 6 | 7 | #include "registers.h" 8 | #include "srom.h" 9 | 10 | void PMW3360::init() { 11 | set_pins_function(); 12 | 13 | gpio_init(ncs_pin); 14 | gpio_set_dir(ncs_pin, GPIO_OUT); 15 | gpio_put(ncs_pin, 1); 16 | 17 | spi_init(spi, 500000); 18 | spi_set_format(spi, 8, SPI_CPOL_1, SPI_CPHA_1, SPI_MSB_FIRST); 19 | 20 | perform_startup(); 21 | 22 | unset_pins_function(); 23 | } 24 | 25 | void PMW3360::set_cpi(unsigned int cpi) { 26 | set_pins_function(); 27 | 28 | int cpival = (cpi / 100) - 1; 29 | cs_select(); 30 | write_register(Config1, cpival); 31 | cs_deselect(); 32 | 33 | unset_pins_function(); 34 | } 35 | 36 | void PMW3360::update() { 37 | set_pins_function(); 38 | 39 | // write 0x01 to Motion register and read from it to freeze the motion values and make them available 40 | write_register(Motion, 0x01); 41 | uint8_t motion = read_register(Motion); 42 | 43 | is_on_surface = !(motion & (1 << 3)); 44 | 45 | movement[0] = read_register(Delta_X_L); 46 | movement[0] |= ((int16_t) read_register(Delta_X_H)) << 8; 47 | movement[1] = read_register(Delta_Y_L); 48 | movement[1] |= ((int16_t) read_register(Delta_Y_H)) << 8; 49 | 50 | unset_pins_function(); 51 | } 52 | 53 | void PMW3360::cs_select() { 54 | asm volatile("nop \n nop \n nop"); 55 | gpio_put(ncs_pin, 0); // Active low 56 | asm volatile("nop \n nop \n nop"); 57 | } 58 | 59 | void PMW3360::cs_deselect() { 60 | asm volatile("nop \n nop \n nop"); 61 | gpio_put(ncs_pin, 1); 62 | asm volatile("nop \n nop \n nop"); 63 | } 64 | 65 | uint8_t PMW3360::read_register(uint8_t reg_addr) { 66 | cs_select(); 67 | 68 | // send adress of the register, with MSBit = 0 to indicate it's a read 69 | uint8_t x = reg_addr & 0x7f; 70 | spi_write_blocking(spi, &x, 1); 71 | sleep_us(100); // tSRAD 72 | // read data 73 | uint8_t data; 74 | spi_read_blocking(spi, 0, &data, 1); 75 | 76 | sleep_us(1); // tSCLK-NCS for read operation is 120ns 77 | cs_deselect(); 78 | sleep_us(19); // tSRW/tSRR (=20us) minus tSCLK-NCS 79 | 80 | return data; 81 | } 82 | 83 | void PMW3360::write_register(uint8_t reg_addr, uint8_t data) { 84 | cs_select(); 85 | 86 | // send adress of the register, with MSBit = 1 to indicate it's a write 87 | uint8_t x = reg_addr | 0x80; 88 | spi_write_blocking(spi, &x, 1); 89 | // send data 90 | spi_write_blocking(spi, &data, 1); 91 | 92 | sleep_us(20); // tSCLK-NCS for write operation 93 | cs_deselect(); 94 | sleep_us(100); // tSWW/tSWR (=120us) minus tSCLK-NCS. Could be shortened, but is looks like a safe lower bound 95 | } 96 | 97 | // We do this because we have the two sensors connected to two different sets of spi0 pins. 98 | // It wouldn't be necessary if one sensor was connected to spi0 and the other to spi1. 99 | // (Or even if we had both connected to the same spi0 pins, I think.) 100 | void PMW3360::set_pins_function() { 101 | gpio_set_function(miso_pin, GPIO_FUNC_SPI); 102 | gpio_set_function(mosi_pin, GPIO_FUNC_SPI); 103 | gpio_set_function(sck_pin, GPIO_FUNC_SPI); 104 | } 105 | 106 | void PMW3360::unset_pins_function() { 107 | gpio_set_function(miso_pin, GPIO_FUNC_NULL); 108 | gpio_set_function(mosi_pin, GPIO_FUNC_NULL); 109 | gpio_set_function(sck_pin, GPIO_FUNC_NULL); 110 | } 111 | 112 | void PMW3360::perform_startup() { 113 | cs_deselect(); // ensure that the serial port is reset 114 | cs_select(); // ensure that the serial port is reset 115 | cs_deselect(); // ensure that the serial port is reset 116 | write_register(Power_Up_Reset, 0x5a); // force reset 117 | sleep_ms(50); // wait for it to reboot 118 | // read registers 0x02 to 0x06 (and discard the data) 119 | read_register(Motion); 120 | read_register(Delta_X_L); 121 | read_register(Delta_X_H); 122 | read_register(Delta_Y_L); 123 | read_register(Delta_Y_H); 124 | // upload the firmware 125 | upload_firmware(); 126 | sleep_ms(10); 127 | } 128 | 129 | void PMW3360::upload_firmware() { 130 | // send the firmware to the chip, cf p.18 of the datasheet 131 | 132 | // Write 0 to Rest_En bit of Config2 register to disable Rest mode. 133 | write_register(Config2, 0x20); 134 | 135 | // write 0x1d in SROM_enable reg for initializing 136 | write_register(SROM_Enable, 0x1d); 137 | 138 | // wait for more than one frame period 139 | sleep_ms(10); // assume that the frame rate is as low as 100fps... even if it should never be that low 140 | 141 | // write 0x18 to SROM_enable to start SROM download 142 | write_register(SROM_Enable, 0x18); 143 | 144 | // write the SROM file (=firmware data) 145 | cs_select(); 146 | uint8_t data = SROM_Load_Burst | 0x80; // write burst destination adress 147 | spi_write_blocking(spi, &data, 1); 148 | sleep_us(15); 149 | 150 | // send all bytes of the firmware 151 | for (int i = 0; i < firmware_length; i++) { 152 | spi_write_blocking(spi, &(firmware_data[i]), 1); 153 | sleep_us(15); 154 | } 155 | 156 | // Read the SROM_ID register to verify the ID before any other register reads or writes. 157 | read_register(SROM_ID); 158 | 159 | // Write 0x00 to Config2 register for wired mouse or 0x20 for wireless mouse design. 160 | write_register(Config2, 0x00); 161 | 162 | // set initial CPI resolution 163 | write_register(Config1, 0x15); 164 | 165 | cs_deselect(); 166 | } 167 | -------------------------------------------------------------------------------- /firmware/src/pmw3360.h: -------------------------------------------------------------------------------- 1 | #ifndef _PMW3360_H_ 2 | #define _PMW3360_H_ 3 | 4 | #include 5 | 6 | class PMW3360 { 7 | public: 8 | PMW3360(spi_inst_t* spi, uint miso_pin, uint mosi_pin, uint sck_pin, uint ncs_pin) 9 | : spi(spi), miso_pin(miso_pin), mosi_pin(mosi_pin), sck_pin(sck_pin), ncs_pin(ncs_pin){}; 10 | void init(); 11 | void set_cpi(unsigned int cpi); 12 | void update(); 13 | 14 | int16_t movement[2]; 15 | bool is_on_surface; 16 | 17 | private: 18 | spi_inst_t* spi; 19 | uint miso_pin; 20 | uint mosi_pin; 21 | uint sck_pin; 22 | uint ncs_pin; 23 | 24 | void cs_select(); 25 | void cs_deselect(); 26 | uint8_t read_register(uint8_t reg_addr); 27 | void write_register(uint8_t reg_addr, uint8_t data); 28 | void set_pins_function(); 29 | void unset_pins_function(); 30 | void perform_startup(); 31 | void upload_firmware(); 32 | }; 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /firmware/src/registers.h: -------------------------------------------------------------------------------- 1 | #ifndef _REGISTERS_H_ 2 | #define _REGISTERS_H_ 3 | 4 | // Registers 5 | #define Product_ID 0x00 6 | #define Revision_ID 0x01 7 | #define Motion 0x02 8 | #define Delta_X_L 0x03 9 | #define Delta_X_H 0x04 10 | #define Delta_Y_L 0x05 11 | #define Delta_Y_H 0x06 12 | #define SQUAL 0x07 13 | #define Raw_Data_Sum 0x08 14 | #define Maximum_Raw_data 0x09 15 | #define Minimum_Raw_data 0x0A 16 | #define Shutter_Lower 0x0B 17 | #define Shutter_Upper 0x0C 18 | #define Control 0x0D 19 | #define Config1 0x0F 20 | #define Config2 0x10 21 | #define Angle_Tune 0x11 22 | #define Frame_Capture 0x12 23 | #define SROM_Enable 0x13 24 | #define Run_Downshift 0x14 25 | #define Rest1_Rate_Lower 0x15 26 | #define Rest1_Rate_Upper 0x16 27 | #define Rest1_Downshift 0x17 28 | #define Rest2_Rate_Lower 0x18 29 | #define Rest2_Rate_Upper 0x19 30 | #define Rest2_Downshift 0x1A 31 | #define Rest3_Rate_Lower 0x1B 32 | #define Rest3_Rate_Upper 0x1C 33 | #define Observation 0x24 34 | #define Data_Out_Lower 0x25 35 | #define Data_Out_Upper 0x26 36 | #define Raw_Data_Dump 0x29 37 | #define SROM_ID 0x2A 38 | #define Min_SQ_Run 0x2B 39 | #define Raw_Data_Threshold 0x2C 40 | #define Config5 0x2F 41 | #define Power_Up_Reset 0x3A 42 | #define Shutdown 0x3B 43 | #define Inverse_Product_ID 0x3F 44 | #define LiftCutoff_Tune3 0x41 45 | #define Angle_Snap 0x42 46 | #define LiftCutoff_Tune1 0x4A 47 | #define Motion_Burst 0x50 48 | #define LiftCutoff_Tune_Timeout 0x58 49 | #define LiftCutoff_Tune_Min_Length 0x5A 50 | #define SROM_Load_Burst 0x62 51 | #define Lift_Config 0x63 52 | #define Raw_Data_Burst 0x64 53 | #define LiftCutoff_Tune2 0x65 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /firmware/src/srom.cc: -------------------------------------------------------------------------------- 1 | #include "srom.h" 2 | 3 | extern const unsigned short firmware_length = 4094; 4 | 5 | extern const unsigned char firmware_data[] = { 6 | 0x01, 0x04, 0x8e, 0x96, 0x6e, 0x77, 0x3e, 0xfe, 0x7e, 0x5f, 0x1d, 0xb8, 0xf2, 0x66, 0x4e, 7 | 0xff, 0x5d, 0x19, 0xb0, 0xc2, 0x04, 0x69, 0x54, 0x2a, 0xd6, 0x2e, 0xbf, 0xdd, 0x19, 0xb0, 8 | 0xc3, 0xe5, 0x29, 0xb1, 0xe0, 0x23, 0xa5, 0xa9, 0xb1, 0xc1, 0x00, 0x82, 0x67, 0x4c, 0x1a, 9 | 0x97, 0x8d, 0x79, 0x51, 0x20, 0xc7, 0x06, 0x8e, 0x7c, 0x7c, 0x7a, 0x76, 0x4f, 0xfd, 0x59, 10 | 0x30, 0xe2, 0x46, 0x0e, 0x9e, 0xbe, 0xdf, 0x1d, 0x99, 0x91, 0xa0, 0xa5, 0xa1, 0xa9, 0xd0, 11 | 0x22, 0xc6, 0xef, 0x5c, 0x1b, 0x95, 0x89, 0x90, 0xa2, 0xa7, 0xcc, 0xfb, 0x55, 0x28, 0xb3, 12 | 0xe4, 0x4a, 0xf7, 0x6c, 0x3b, 0xf4, 0x6a, 0x56, 0x2e, 0xde, 0x1f, 0x9d, 0xb8, 0xd3, 0x05, 13 | 0x88, 0x92, 0xa6, 0xce, 0x1e, 0xbe, 0xdf, 0x1d, 0x99, 0xb0, 0xe2, 0x46, 0xef, 0x5c, 0x07, 14 | 0x11, 0x5d, 0x98, 0x0b, 0x9d, 0x94, 0x97, 0xee, 0x4e, 0x45, 0x33, 0x6b, 0x44, 0xc7, 0x29, 15 | 0x56, 0x27, 0x30, 0xc6, 0xa7, 0xd5, 0xf2, 0x56, 0xdf, 0xb4, 0x38, 0x62, 0xcb, 0xa0, 0xb6, 16 | 0xe3, 0x0f, 0x84, 0x06, 0x24, 0x05, 0x65, 0x6f, 0x76, 0x89, 0xb5, 0x77, 0x41, 0x27, 0x82, 17 | 0x66, 0x65, 0x82, 0xcc, 0xd5, 0xe6, 0x20, 0xd5, 0x27, 0x17, 0xc5, 0xf8, 0x03, 0x23, 0x7c, 18 | 0x5f, 0x64, 0xa5, 0x1d, 0xc1, 0xd6, 0x36, 0xcb, 0x4c, 0xd4, 0xdb, 0x66, 0xd7, 0x8b, 0xb1, 19 | 0x99, 0x7e, 0x6f, 0x4c, 0x36, 0x40, 0x06, 0xd6, 0xeb, 0xd7, 0xa2, 0xe4, 0xf4, 0x95, 0x51, 20 | 0x5a, 0x54, 0x96, 0xd5, 0x53, 0x44, 0xd7, 0x8c, 0xe0, 0xb9, 0x40, 0x68, 0xd2, 0x18, 0xe9, 21 | 0xdd, 0x9a, 0x23, 0x92, 0x48, 0xee, 0x7f, 0x43, 0xaf, 0xea, 0x77, 0x38, 0x84, 0x8c, 0x0a, 22 | 0x72, 0xaf, 0x69, 0xf8, 0xdd, 0xf1, 0x24, 0x83, 0xa3, 0xf8, 0x4a, 0xbf, 0xf5, 0x94, 0x13, 23 | 0xdb, 0xbb, 0xd8, 0xb4, 0xb3, 0xa0, 0xfb, 0x45, 0x50, 0x60, 0x30, 0x59, 0x12, 0x31, 0x71, 24 | 0xa2, 0xd3, 0x13, 0xe7, 0xfa, 0xe7, 0xce, 0x0f, 0x63, 0x15, 0x0b, 0x6b, 0x94, 0xbb, 0x37, 25 | 0x83, 0x26, 0x05, 0x9d, 0xfb, 0x46, 0x92, 0xfc, 0x0a, 0x15, 0xd1, 0x0d, 0x73, 0x92, 0xd6, 26 | 0x8c, 0x1b, 0x8c, 0xb8, 0x55, 0x8a, 0xce, 0xbd, 0xfe, 0x8e, 0xfc, 0xed, 0x09, 0x12, 0x83, 27 | 0x91, 0x82, 0x51, 0x31, 0x23, 0xfb, 0xb4, 0x0c, 0x76, 0xad, 0x7c, 0xd9, 0xb4, 0x4b, 0xb2, 28 | 0x67, 0x14, 0x09, 0x9c, 0x7f, 0x0c, 0x18, 0xba, 0x3b, 0xd6, 0x8e, 0x14, 0x2a, 0xe4, 0x1b, 29 | 0x52, 0x9f, 0x2b, 0x7d, 0xe1, 0xfb, 0x6a, 0x33, 0x02, 0xfa, 0xac, 0x5a, 0xf2, 0x3e, 0x88, 30 | 0x7e, 0xae, 0xd1, 0xf3, 0x78, 0xe8, 0x05, 0xd1, 0xe3, 0xdc, 0x21, 0xf6, 0xe1, 0x9a, 0xbd, 31 | 0x17, 0x0e, 0xd9, 0x46, 0x9b, 0x88, 0x03, 0xea, 0xf6, 0x66, 0xbe, 0x0e, 0x1b, 0x50, 0x49, 32 | 0x96, 0x40, 0x97, 0xf1, 0xf1, 0xe4, 0x80, 0xa6, 0x6e, 0xe8, 0x77, 0x34, 0xbf, 0x29, 0x40, 33 | 0x44, 0xc2, 0xff, 0x4e, 0x98, 0xd3, 0x9c, 0xa3, 0x32, 0x2b, 0x76, 0x51, 0x04, 0x09, 0xe7, 34 | 0xa9, 0xd1, 0xa6, 0x32, 0xb1, 0x23, 0x53, 0xe2, 0x47, 0xab, 0xd6, 0xf5, 0x69, 0x5c, 0x3e, 35 | 0x5f, 0xfa, 0xae, 0x45, 0x20, 0xe5, 0xd2, 0x44, 0xff, 0x39, 0x32, 0x6d, 0xfd, 0x27, 0x57, 36 | 0x5c, 0xfd, 0xf0, 0xde, 0xc1, 0xb5, 0x99, 0xe5, 0xf5, 0x1c, 0x77, 0x01, 0x75, 0xc5, 0x6d, 37 | 0x58, 0x92, 0xf2, 0xb2, 0x47, 0x00, 0x01, 0x26, 0x96, 0x7a, 0x30, 0xff, 0xb7, 0xf0, 0xef, 38 | 0x77, 0xc1, 0x8a, 0x5d, 0xdc, 0xc0, 0xd1, 0x29, 0x30, 0x1e, 0x77, 0x38, 0x7a, 0x94, 0xf1, 39 | 0xb8, 0x7a, 0x7e, 0xef, 0xa4, 0xd1, 0xac, 0x31, 0x4a, 0xf2, 0x5d, 0x64, 0x3d, 0xb2, 0xe2, 40 | 0xf0, 0x08, 0x99, 0xfc, 0x70, 0xee, 0x24, 0xa7, 0x7e, 0xee, 0x1e, 0x20, 0x69, 0x7d, 0x44, 41 | 0xbf, 0x87, 0x42, 0xdf, 0x88, 0x3b, 0x0c, 0xda, 0x42, 0xc9, 0x04, 0xf9, 0x45, 0x50, 0xfc, 42 | 0x83, 0x8f, 0x11, 0x6a, 0x72, 0xbc, 0x99, 0x95, 0xf0, 0xac, 0x3d, 0xa7, 0x3b, 0xcd, 0x1c, 43 | 0xe2, 0x88, 0x79, 0x37, 0x11, 0x5f, 0x39, 0x89, 0x95, 0x0a, 0x16, 0x84, 0x7a, 0xf6, 0x8a, 44 | 0xa4, 0x28, 0xe4, 0xed, 0x83, 0x80, 0x3b, 0xb1, 0x23, 0xa5, 0x03, 0x10, 0xf4, 0x66, 0xea, 45 | 0xbb, 0x0c, 0x0f, 0xc5, 0xec, 0x6c, 0x69, 0xc5, 0xd3, 0x24, 0xab, 0xd4, 0x2a, 0xb7, 0x99, 46 | 0x88, 0x76, 0x08, 0xa0, 0xa8, 0x95, 0x7c, 0xd8, 0x38, 0x6d, 0xcd, 0x59, 0x02, 0x51, 0x4b, 47 | 0xf1, 0xb5, 0x2b, 0x50, 0xe3, 0xb6, 0xbd, 0xd0, 0x72, 0xcf, 0x9e, 0xfd, 0x6e, 0xbb, 0x44, 48 | 0xc8, 0x24, 0x8a, 0x77, 0x18, 0x8a, 0x13, 0x06, 0xef, 0x97, 0x7d, 0xfa, 0x81, 0xf0, 0x31, 49 | 0xe6, 0xfa, 0x77, 0xed, 0x31, 0x06, 0x31, 0x5b, 0x54, 0x8a, 0x9f, 0x30, 0x68, 0xdb, 0xe2, 50 | 0x40, 0xf8, 0x4e, 0x73, 0xfa, 0xab, 0x74, 0x8b, 0x10, 0x58, 0x13, 0xdc, 0xd2, 0xe6, 0x78, 51 | 0xd1, 0x32, 0x2e, 0x8a, 0x9f, 0x2c, 0x58, 0x06, 0x48, 0x27, 0xc5, 0xa9, 0x5e, 0x81, 0x47, 52 | 0x89, 0x46, 0x21, 0x91, 0x03, 0x70, 0xa4, 0x3e, 0x88, 0x9c, 0xda, 0x33, 0x0a, 0xce, 0xbc, 53 | 0x8b, 0x8e, 0xcf, 0x9f, 0xd3, 0x71, 0x80, 0x43, 0xcf, 0x6b, 0xa9, 0x51, 0x83, 0x76, 0x30, 54 | 0x82, 0xc5, 0x6a, 0x85, 0x39, 0x11, 0x50, 0x1a, 0x82, 0xdc, 0x1e, 0x1c, 0xd5, 0x7d, 0xa9, 55 | 0x71, 0x99, 0x33, 0x47, 0x19, 0x97, 0xb3, 0x5a, 0xb1, 0xdf, 0xed, 0xa4, 0xf2, 0xe6, 0x26, 56 | 0x84, 0xa2, 0x28, 0x9a, 0x9e, 0xdf, 0xa6, 0x6a, 0xf4, 0xd6, 0xfc, 0x2e, 0x5b, 0x9d, 0x1a, 57 | 0x2a, 0x27, 0x68, 0xfb, 0xc1, 0x83, 0x21, 0x4b, 0x90, 0xe0, 0x36, 0xdd, 0x5b, 0x31, 0x42, 58 | 0x55, 0xa0, 0x13, 0xf7, 0xd0, 0x89, 0x53, 0x71, 0x99, 0x57, 0x09, 0x29, 0xc5, 0xf3, 0x21, 59 | 0xf8, 0x37, 0x2f, 0x40, 0xf3, 0xd4, 0xaf, 0x16, 0x08, 0x36, 0x02, 0xfc, 0x77, 0xc5, 0x8b, 60 | 0x04, 0x90, 0x56, 0xb9, 0xc9, 0x67, 0x9a, 0x99, 0xe8, 0x00, 0xd3, 0x86, 0xff, 0x97, 0x2d, 61 | 0x08, 0xe9, 0xb7, 0xb3, 0x91, 0xbc, 0xdf, 0x45, 0xc6, 0xed, 0x0f, 0x8c, 0x4c, 0x1e, 0xe6, 62 | 0x5b, 0x6e, 0x38, 0x30, 0xe4, 0xaa, 0xe3, 0x95, 0xde, 0xb9, 0xe4, 0x9a, 0xf5, 0xb2, 0x55, 63 | 0x9a, 0x87, 0x9b, 0xf6, 0x6a, 0xb2, 0xf2, 0x77, 0x9a, 0x31, 0xf4, 0x7a, 0x31, 0xd1, 0x1d, 64 | 0x04, 0xc0, 0x7c, 0x32, 0xa2, 0x9e, 0x9a, 0xf5, 0x62, 0xf8, 0x27, 0x8d, 0xbf, 0x51, 0xff, 65 | 0xd3, 0xdf, 0x64, 0x37, 0x3f, 0x2a, 0x6f, 0x76, 0x3a, 0x7d, 0x77, 0x06, 0x9e, 0x77, 0x7f, 66 | 0x5e, 0xeb, 0x32, 0x51, 0xf9, 0x16, 0x66, 0x9a, 0x09, 0xf3, 0xb0, 0x08, 0xa4, 0x70, 0x96, 67 | 0x46, 0x30, 0xff, 0xda, 0x4f, 0xe9, 0x1b, 0xed, 0x8d, 0xf8, 0x74, 0x1f, 0x31, 0x92, 0xb3, 68 | 0x73, 0x17, 0x36, 0xdb, 0x91, 0x30, 0xd6, 0x88, 0x55, 0x6b, 0x34, 0x77, 0x87, 0x7a, 0xe7, 69 | 0xee, 0x06, 0xc6, 0x1c, 0x8c, 0x19, 0x0c, 0x48, 0x46, 0x23, 0x5e, 0x9c, 0x07, 0x5c, 0xbf, 70 | 0xb4, 0x7e, 0xd6, 0x4f, 0x74, 0x9c, 0xe2, 0xc5, 0x50, 0x8b, 0xc5, 0x8b, 0x15, 0x90, 0x60, 71 | 0x62, 0x57, 0x29, 0xd0, 0x13, 0x43, 0xa1, 0x80, 0x88, 0x91, 0x00, 0x44, 0xc7, 0x4d, 0x19, 72 | 0x86, 0xcc, 0x2f, 0x2a, 0x75, 0x5a, 0xfc, 0xeb, 0x97, 0x2a, 0x70, 0xe3, 0x78, 0xd8, 0x91, 73 | 0xb0, 0x4f, 0x99, 0x07, 0xa3, 0x95, 0xea, 0x24, 0x21, 0xd5, 0xde, 0x51, 0x20, 0x93, 0x27, 74 | 0x0a, 0x30, 0x73, 0xa8, 0xff, 0x8a, 0x97, 0xe9, 0xa7, 0x6a, 0x8e, 0x0d, 0xe8, 0xf0, 0xdf, 75 | 0xec, 0xea, 0xb4, 0x6c, 0x1d, 0x39, 0x2a, 0x62, 0x2d, 0x3d, 0x5a, 0x8b, 0x65, 0xf8, 0x90, 76 | 0x05, 0x2e, 0x7e, 0x91, 0x2c, 0x78, 0xef, 0x8e, 0x7a, 0xc1, 0x2f, 0xac, 0x78, 0xee, 0xaf, 77 | 0x28, 0x45, 0x06, 0x4c, 0x26, 0xaf, 0x3b, 0xa2, 0xdb, 0xa3, 0x93, 0x06, 0xb5, 0x3c, 0xa5, 78 | 0xd8, 0xee, 0x8f, 0xaf, 0x25, 0xcc, 0x3f, 0x85, 0x68, 0x48, 0xa9, 0x62, 0xcc, 0x97, 0x8f, 79 | 0x7f, 0x2a, 0xea, 0xe0, 0x15, 0x0a, 0xad, 0x62, 0x07, 0xbd, 0x45, 0xf8, 0x41, 0xd8, 0x36, 80 | 0xcb, 0x4c, 0xdb, 0x6e, 0xe6, 0x3a, 0xe7, 0xda, 0x15, 0xe9, 0x29, 0x1e, 0x12, 0x10, 0xa0, 81 | 0x14, 0x2c, 0x0e, 0x3d, 0xf4, 0xbf, 0x39, 0x41, 0x92, 0x75, 0x0b, 0x25, 0x7b, 0xa3, 0xce, 82 | 0x39, 0x9c, 0x15, 0x64, 0xc8, 0xfa, 0x3d, 0xef, 0x73, 0x27, 0xfe, 0x26, 0x2e, 0xce, 0xda, 83 | 0x6e, 0xfd, 0x71, 0x8e, 0xdd, 0xfe, 0x76, 0xee, 0xdc, 0x12, 0x5c, 0x02, 0xc5, 0x3a, 0x4e, 84 | 0x4e, 0x4f, 0xbf, 0xca, 0x40, 0x15, 0xc7, 0x6e, 0x8d, 0x41, 0xf1, 0x10, 0xe0, 0x4f, 0x7e, 85 | 0x97, 0x7f, 0x1c, 0xae, 0x47, 0x8e, 0x6b, 0xb1, 0x25, 0x31, 0xb0, 0x73, 0xc7, 0x1b, 0x97, 86 | 0x79, 0xf9, 0x80, 0xd3, 0x66, 0x22, 0x30, 0x07, 0x74, 0x1e, 0xe4, 0xd0, 0x80, 0x21, 0xd6, 87 | 0xee, 0x6b, 0x6c, 0x4f, 0xbf, 0xf5, 0xb7, 0xd9, 0x09, 0x87, 0x2f, 0xa9, 0x14, 0xbe, 0x27, 88 | 0xd9, 0x72, 0x50, 0x01, 0xd4, 0x13, 0x73, 0xa6, 0xa7, 0x51, 0x02, 0x75, 0x25, 0xe1, 0xb3, 89 | 0x45, 0x34, 0x7d, 0xa8, 0x8e, 0xeb, 0xf3, 0x16, 0x49, 0xcb, 0x4f, 0x8c, 0xa1, 0xb9, 0x36, 90 | 0x85, 0x39, 0x75, 0x5d, 0x08, 0x00, 0xae, 0xeb, 0xf6, 0xea, 0xd7, 0x13, 0x3a, 0x21, 0x5a, 91 | 0x5f, 0x30, 0x84, 0x52, 0x26, 0x95, 0xc9, 0x14, 0xf2, 0x57, 0x55, 0x6b, 0xb1, 0x10, 0xc2, 92 | 0xe1, 0xbd, 0x3b, 0x51, 0xc0, 0xb7, 0x55, 0x4c, 0x71, 0x12, 0x26, 0xc7, 0x0d, 0xf9, 0x51, 93 | 0xa4, 0x38, 0x02, 0x05, 0x7f, 0xb8, 0xf1, 0x72, 0x4b, 0xbf, 0x71, 0x89, 0x14, 0xf3, 0x77, 94 | 0x38, 0xd9, 0x71, 0x24, 0xf3, 0x00, 0x11, 0xa1, 0xd8, 0xd4, 0x69, 0x27, 0x08, 0x37, 0x35, 95 | 0xc9, 0x11, 0x9d, 0x90, 0x1c, 0x0e, 0xe7, 0x1c, 0xff, 0x2d, 0x1e, 0xe8, 0x92, 0xe1, 0x18, 96 | 0x10, 0x95, 0x7c, 0xe0, 0x80, 0xf4, 0x96, 0x43, 0x21, 0xf9, 0x75, 0x21, 0x64, 0x38, 0xdd, 97 | 0x9f, 0x1e, 0x95, 0x16, 0xda, 0x56, 0x1d, 0x4f, 0x9a, 0x53, 0xb2, 0xe2, 0xe4, 0x18, 0xcb, 98 | 0x6b, 0x1a, 0x65, 0xeb, 0x56, 0xc6, 0x3b, 0xe5, 0xfe, 0xd8, 0x26, 0x3f, 0x3a, 0x84, 0x59, 99 | 0x72, 0x66, 0xa2, 0xf3, 0x75, 0xff, 0xfb, 0x60, 0xb3, 0x22, 0xad, 0x3f, 0x2d, 0x6b, 0xf9, 100 | 0xeb, 0xea, 0x05, 0x7c, 0xd8, 0x8f, 0x6d, 0x2c, 0x98, 0x9e, 0x2b, 0x93, 0xf1, 0x5e, 0x46, 101 | 0xf0, 0x87, 0x49, 0x29, 0x73, 0x68, 0xd7, 0x7f, 0xf9, 0xf0, 0xe5, 0x7d, 0xdb, 0x1d, 0x75, 102 | 0x19, 0xf3, 0xc4, 0x58, 0x9b, 0x17, 0x88, 0xa8, 0x92, 0xe0, 0xbe, 0xbd, 0x8b, 0x1d, 0x8d, 103 | 0x9f, 0x56, 0x76, 0xad, 0xaf, 0x29, 0xe2, 0xd9, 0xd5, 0x52, 0xf6, 0xb5, 0x56, 0x35, 0x57, 104 | 0x3a, 0xc8, 0xe1, 0x56, 0x43, 0x19, 0x94, 0xd3, 0x04, 0x9b, 0x6d, 0x35, 0xd8, 0x0b, 0x5f, 105 | 0x4d, 0x19, 0x8e, 0xec, 0xfa, 0x64, 0x91, 0x0a, 0x72, 0x20, 0x2b, 0xbc, 0x1a, 0x4a, 0xfe, 106 | 0x8b, 0xfd, 0xbb, 0xed, 0x1b, 0x23, 0xea, 0xad, 0x72, 0x82, 0xa1, 0x29, 0x99, 0x71, 0xbd, 107 | 0xf0, 0x95, 0xc1, 0x03, 0xdd, 0x7b, 0xc2, 0xb2, 0x3c, 0x28, 0x54, 0xd3, 0x68, 0xa4, 0x72, 108 | 0xc8, 0x66, 0x96, 0xe0, 0xd1, 0xd8, 0x7f, 0xf8, 0xd1, 0x26, 0x2b, 0xf7, 0xad, 0xba, 0x55, 109 | 0xca, 0x15, 0xb9, 0x32, 0xc3, 0xe5, 0x88, 0x97, 0x8e, 0x5c, 0xfb, 0x92, 0x25, 0x8b, 0xbf, 110 | 0xa2, 0x45, 0x55, 0x7a, 0xa7, 0x6f, 0x8b, 0x57, 0x5b, 0xcf, 0x0e, 0xcb, 0x1d, 0xfb, 0x20, 111 | 0x82, 0x77, 0xa8, 0x8c, 0xcc, 0x16, 0xce, 0x1d, 0xfa, 0xde, 0xcc, 0x0b, 0x62, 0xfe, 0xcc, 112 | 0xe1, 0xb7, 0xf0, 0xc3, 0x81, 0x64, 0x73, 0x40, 0xa0, 0xc2, 0x4d, 0x89, 0x11, 0x75, 0x33, 113 | 0x55, 0x33, 0x8d, 0xe8, 0x4a, 0xfd, 0xea, 0x6e, 0x30, 0x0b, 0xd7, 0x31, 0x2c, 0xde, 0x47, 114 | 0xe3, 0xbf, 0xf8, 0x55, 0x42, 0xe2, 0x7f, 0x59, 0xe5, 0x17, 0xef, 0x99, 0x34, 0x69, 0x91, 115 | 0xb1, 0x23, 0x8e, 0x20, 0x87, 0x2d, 0xa8, 0xfe, 0xd5, 0x8a, 0xf3, 0x84, 0x3a, 0xf0, 0x37, 116 | 0xe4, 0x09, 0x00, 0x54, 0xee, 0x67, 0x49, 0x93, 0xe4, 0x81, 0x70, 0xe3, 0x90, 0x4d, 0xef, 117 | 0xfe, 0x41, 0xb7, 0x99, 0x7b, 0xc1, 0x83, 0xba, 0x62, 0x12, 0x6f, 0x7d, 0xde, 0x6b, 0xaf, 118 | 0xda, 0x16, 0xf9, 0x55, 0x51, 0xee, 0xa6, 0x0c, 0x2b, 0x02, 0xa3, 0xfd, 0x8d, 0xfb, 0x30, 119 | 0x17, 0xe4, 0x6f, 0xdf, 0x36, 0x71, 0xc4, 0xca, 0x87, 0x25, 0x48, 0xb0, 0x47, 0xec, 0xea, 120 | 0xb4, 0xbf, 0xa5, 0x4d, 0x9b, 0x9f, 0x02, 0x93, 0xc4, 0xe3, 0xe4, 0xe8, 0x42, 0x2d, 0x68, 121 | 0x81, 0x15, 0x0a, 0xeb, 0x84, 0x5b, 0xd6, 0xa8, 0x74, 0xfb, 0x7d, 0x1d, 0xcb, 0x2c, 0xda, 122 | 0x46, 0x2a, 0x76, 0x62, 0xce, 0xbc, 0x5c, 0x9e, 0x8b, 0xe7, 0xcf, 0xbe, 0x78, 0xf5, 0x7c, 123 | 0xeb, 0xb3, 0x3a, 0x9c, 0xaa, 0x6f, 0xcc, 0x72, 0xd1, 0x59, 0xf2, 0x11, 0x23, 0xd6, 0x3f, 124 | 0x48, 0xd1, 0xb7, 0xce, 0xb0, 0xbf, 0xcb, 0xea, 0x80, 0xde, 0x57, 0xd4, 0x5e, 0x97, 0x2f, 125 | 0x75, 0xd1, 0x50, 0x8e, 0x80, 0x2c, 0x66, 0x79, 0xbf, 0x72, 0x4b, 0xbd, 0x8a, 0x81, 0x6c, 126 | 0xd3, 0xe1, 0x01, 0xdc, 0xd2, 0x15, 0x26, 0xc5, 0x36, 0xda, 0x2c, 0x1a, 0xc0, 0x27, 0x94, 127 | 0xed, 0xb7, 0x9b, 0x85, 0x0b, 0x5e, 0x80, 0x97, 0xc5, 0xec, 0x4f, 0xec, 0x88, 0x5d, 0x50, 128 | 0x07, 0x35, 0x47, 0xdc, 0x0b, 0x3b, 0x3d, 0xdd, 0x60, 0xaf, 0xa8, 0x5d, 0x81, 0x38, 0x24, 129 | 0x25, 0x5d, 0x5c, 0x15, 0xd1, 0xde, 0xb3, 0xab, 0xec, 0x05, 0x69, 0xef, 0x83, 0xed, 0x57, 130 | 0x54, 0xb8, 0x64, 0x64, 0x11, 0x16, 0x32, 0x69, 0xda, 0x9f, 0x2d, 0x7f, 0x36, 0xbb, 0x44, 131 | 0x5a, 0x34, 0xe8, 0x7f, 0xbf, 0x03, 0xeb, 0x00, 0x7f, 0x59, 0x68, 0x22, 0x79, 0xcf, 0x73, 132 | 0x6c, 0x2c, 0x29, 0xa7, 0xa1, 0x5f, 0x38, 0xa1, 0x1d, 0xf0, 0x20, 0x53, 0xe0, 0x1a, 0x63, 133 | 0x14, 0x58, 0x71, 0x10, 0xaa, 0x08, 0x0c, 0x3e, 0x16, 0x1a, 0x60, 0x22, 0x82, 0x7f, 0xba, 134 | 0xa4, 0x43, 0xa0, 0xd0, 0xac, 0x1b, 0xd5, 0x6b, 0x64, 0xb5, 0x14, 0x93, 0x31, 0x9e, 0x53, 135 | 0x50, 0xd0, 0x57, 0x66, 0xee, 0x5a, 0x4f, 0xfb, 0x03, 0x2a, 0x69, 0x58, 0x76, 0xf1, 0x83, 136 | 0xf7, 0x4e, 0xba, 0x8c, 0x42, 0x06, 0x60, 0x5d, 0x6d, 0xce, 0x60, 0x88, 0xae, 0xa4, 0xc3, 137 | 0xf1, 0x03, 0xa5, 0x4b, 0x98, 0xa1, 0xff, 0x67, 0xe1, 0xac, 0xa2, 0xb8, 0x62, 0xd7, 0x6f, 138 | 0xa0, 0x31, 0xb4, 0xd2, 0x77, 0xaf, 0x21, 0x10, 0x06, 0xc6, 0x9a, 0xff, 0x1d, 0x09, 0x17, 139 | 0x0e, 0x5f, 0xf1, 0xaa, 0x54, 0x34, 0x4b, 0x45, 0x8a, 0x87, 0x63, 0xa6, 0xdc, 0xf9, 0x24, 140 | 0x30, 0x67, 0xc6, 0xb2, 0xd6, 0x61, 0x33, 0x69, 0xee, 0x50, 0x61, 0x57, 0x28, 0xe7, 0x7e, 141 | 0xee, 0xec, 0x3a, 0x5a, 0x73, 0x4e, 0xa8, 0x8d, 0xe4, 0x18, 0xea, 0xec, 0x41, 0x64, 0xc8, 142 | 0xe2, 0xe8, 0x66, 0xb6, 0x2d, 0xb6, 0xfb, 0x6a, 0x6c, 0x16, 0xb3, 0xdd, 0x46, 0x43, 0xb9, 143 | 0x73, 0x00, 0x6a, 0x71, 0xed, 0x4e, 0x9d, 0x25, 0x1a, 0xc3, 0x3c, 0x4a, 0x95, 0x15, 0x99, 144 | 0x35, 0x81, 0x14, 0x02, 0xd6, 0x98, 0x9b, 0xec, 0xd8, 0x23, 0x3b, 0x84, 0x29, 0xaf, 0x0c, 145 | 0x99, 0x83, 0xa6, 0x9a, 0x34, 0x4f, 0xfa, 0xe8, 0xd0, 0x3c, 0x4b, 0xd0, 0xfb, 0xb6, 0x68, 146 | 0xb8, 0x9e, 0x8f, 0xcd, 0xf7, 0x60, 0x2d, 0x7a, 0x22, 0xe5, 0x7d, 0xab, 0x65, 0x1b, 0x95, 147 | 0xa7, 0xa8, 0x7f, 0xb6, 0x77, 0x47, 0x7b, 0x5f, 0x8b, 0x12, 0x72, 0xd0, 0xd4, 0x91, 0xef, 148 | 0xde, 0x19, 0x50, 0x3c, 0xa7, 0x8b, 0xc4, 0xa9, 0xb3, 0x23, 0xcb, 0x76, 0xe6, 0x81, 0xf0, 149 | 0xc1, 0x04, 0x8f, 0xa3, 0xb8, 0x54, 0x5b, 0x97, 0xac, 0x19, 0xff, 0x3f, 0x55, 0x27, 0x2f, 150 | 0xe0, 0x1d, 0x42, 0x9b, 0x57, 0xfc, 0x4b, 0x4e, 0x0f, 0xce, 0x98, 0xa9, 0x43, 0x57, 0x03, 151 | 0xbd, 0xe7, 0xc8, 0x94, 0xdf, 0x6e, 0x36, 0x73, 0x32, 0xb4, 0xef, 0x2e, 0x85, 0x7a, 0x6e, 152 | 0xfc, 0x6c, 0x18, 0x82, 0x75, 0x35, 0x90, 0x07, 0xf3, 0xe4, 0x9f, 0x3e, 0xdc, 0x68, 0xf3, 153 | 0xb5, 0xf3, 0x19, 0x80, 0x92, 0x06, 0x99, 0xa2, 0xe8, 0x6f, 0xff, 0x2e, 0x7f, 0xae, 0x42, 154 | 0xa4, 0x5f, 0xfb, 0xd4, 0x0e, 0x81, 0x2b, 0xc3, 0x04, 0xff, 0x2b, 0xb3, 0x74, 0x4e, 0x36, 155 | 0x5b, 0x9c, 0x15, 0x00, 0xc6, 0x47, 0x2b, 0xe8, 0x8b, 0x3d, 0xf1, 0x9c, 0x03, 0x9a, 0x58, 156 | 0x7f, 0x9b, 0x9c, 0xbf, 0x85, 0x49, 0x79, 0x35, 0x2e, 0x56, 0x7b, 0x41, 0x14, 0x39, 0x47, 157 | 0x83, 0x26, 0xaa, 0x07, 0x89, 0x98, 0x11, 0x1b, 0x86, 0xe7, 0x73, 0x7a, 0xd8, 0x7d, 0x78, 158 | 0x61, 0x53, 0xe9, 0x79, 0xf5, 0x36, 0x8d, 0x44, 0x92, 0x84, 0xf9, 0x13, 0x50, 0x58, 0x3b, 159 | 0xa4, 0x6a, 0x36, 0x65, 0x49, 0x8e, 0x3c, 0x0e, 0xf1, 0x6f, 0xd2, 0x84, 0xc4, 0x7e, 0x8e, 160 | 0x3f, 0x39, 0xae, 0x7c, 0x84, 0xf1, 0x63, 0x37, 0x8e, 0x3c, 0xcc, 0x3e, 0x44, 0x81, 0x45, 161 | 0xf1, 0x4b, 0xb9, 0xed, 0x6b, 0x36, 0x5d, 0xbb, 0x20, 0x60, 0x1a, 0x0f, 0xa3, 0xaa, 0x55, 162 | 0x77, 0x3a, 0xa9, 0xae, 0x37, 0x4d, 0xba, 0xb8, 0x86, 0x6b, 0xbc, 0x08, 0x50, 0xf6, 0xcc, 163 | 0xa4, 0xbd, 0x1d, 0x40, 0x72, 0xa5, 0x86, 0xfa, 0xe2, 0x10, 0xae, 0x3d, 0x58, 0x4b, 0x97, 164 | 0xf3, 0x43, 0x74, 0xa9, 0x9e, 0xeb, 0x21, 0xb7, 0x01, 0xa4, 0x86, 0x93, 0x97, 0xee, 0x2f, 165 | 0x4f, 0x3b, 0x86, 0xa1, 0x41, 0x6f, 0x41, 0x26, 0x90, 0x78, 0x5c, 0x7f, 0x30, 0x38, 0x4b, 166 | 0x3f, 0xaa, 0xec, 0xed, 0x5c, 0x6f, 0x0e, 0xad, 0x43, 0x87, 0xfd, 0x93, 0x35, 0xe6, 0x01, 167 | 0xef, 0x41, 0x26, 0x90, 0x99, 0x9e, 0xfb, 0x19, 0x5b, 0xad, 0xd2, 0x91, 0x8a, 0xe0, 0x46, 168 | 0xaf, 0x65, 0xfa, 0x4f, 0x84, 0xc1, 0xa1, 0x2d, 0xcf, 0x45, 0x8b, 0xd3, 0x85, 0x50, 0x55, 169 | 0x7c, 0xf9, 0x67, 0x88, 0xd4, 0x4e, 0xe9, 0xd7, 0x6b, 0x61, 0x54, 0xa1, 0xa4, 0xa6, 0xa2, 170 | 0xc2, 0xbf, 0x30, 0x9c, 0x40, 0x9f, 0x5f, 0xd7, 0x69, 0x2b, 0x24, 0x82, 0x5e, 0xd9, 0xd6, 171 | 0xa7, 0x12, 0x54, 0x1a, 0xf7, 0x55, 0x9f, 0x76, 0x50, 0xa9, 0x95, 0x84, 0xe6, 0x6b, 0x6d, 172 | 0xb5, 0x96, 0x54, 0xd6, 0xcd, 0xb3, 0xa1, 0x9b, 0x46, 0xa7, 0x94, 0x4d, 0xc4, 0x94, 0xb4, 173 | 0x98, 0xe3, 0xe1, 0xe2, 0x34, 0xd5, 0x33, 0x16, 0x07, 0x54, 0xcd, 0xb7, 0x77, 0x53, 0xdb, 174 | 0x4f, 0x4d, 0x46, 0x9d, 0xe9, 0xd4, 0x9c, 0x8a, 0x36, 0xb6, 0xb8, 0x38, 0x26, 0x6c, 0x0e, 175 | 0xff, 0x9c, 0x1b, 0x43, 0x8b, 0x80, 0xcc, 0xb9, 0x3d, 0xda, 0xc7, 0xf1, 0x8a, 0xf2, 0x6d, 176 | 0xb8, 0xd7, 0x74, 0x2f, 0x7e, 0x1e, 0xb7, 0xd3, 0x4a, 0xb4, 0xac, 0xfc, 0x79, 0x48, 0x6c, 177 | 0xbc, 0x96, 0xb6, 0x94, 0x46, 0x57, 0x2d, 0xb0, 0xa3, 0xfc, 0x1e, 0xb9, 0x52, 0x60, 0x85, 178 | 0x2d, 0x41, 0xd0, 0x43, 0x01, 0x1e, 0x1c, 0xd5, 0x7d, 0xfc, 0xf3, 0x96, 0x0d, 0xc7, 0xcb, 179 | 0x2a, 0x29, 0x9a, 0x93, 0xdd, 0x88, 0x2d, 0x37, 0x5d, 0xaa, 0xfb, 0x49, 0x68, 0xa0, 0x9c, 180 | 0x50, 0x86, 0x7f, 0x68, 0x56, 0x57, 0xf9, 0x79, 0x18, 0x39, 0xd4, 0xe0, 0x01, 0x84, 0x33, 181 | 0x61, 0xca, 0xa5, 0xd2, 0xd6, 0xe4, 0xc9, 0x8a, 0x4a, 0x23, 0x44, 0x4e, 0xbc, 0xf0, 0xdc, 182 | 0x24, 0xa1, 0xa0, 0xc4, 0xe2, 0x07, 0x3c, 0x10, 0xc4, 0xb5, 0x25, 0x4b, 0x65, 0x63, 0xf4, 183 | 0x80, 0xe7, 0xcf, 0x61, 0xb1, 0x71, 0x82, 0x21, 0x87, 0x2c, 0xf5, 0x91, 0x00, 0x32, 0x0c, 184 | 0xec, 0xa9, 0xb5, 0x9a, 0x74, 0x85, 0xe3, 0x36, 0x8f, 0x76, 0x4f, 0x9c, 0x6d, 0xce, 0xbc, 185 | 0xad, 0x0a, 0x4b, 0xed, 0x76, 0x04, 0xcb, 0xc3, 0xb9, 0x33, 0x9e, 0x01, 0x93, 0x96, 0x69, 186 | 0x7d, 0xc5, 0xa2, 0x45, 0x79, 0x9b, 0x04, 0x5c, 0x84, 0x09, 0xed, 0x88, 0x43, 0xc7, 0xab, 187 | 0x93, 0x14, 0x26, 0xa1, 0x40, 0xb5, 0xce, 0x4e, 0xbf, 0x2a, 0x42, 0x85, 0x3e, 0x2c, 0x3b, 188 | 0x54, 0xe8, 0x12, 0x1f, 0x0e, 0x97, 0x59, 0xb2, 0x27, 0x89, 0xfa, 0xf2, 0xdf, 0x8e, 0x68, 189 | 0x59, 0xdc, 0x06, 0xbc, 0xb6, 0x85, 0x0d, 0x06, 0x22, 0xec, 0xb1, 0xcb, 0xe5, 0x04, 0xe6, 190 | 0x3d, 0xb3, 0xb0, 0x41, 0x73, 0x08, 0x3f, 0x3c, 0x58, 0x86, 0x63, 0xeb, 0x50, 0xee, 0x1d, 191 | 0x2c, 0x37, 0x74, 0xa9, 0xd3, 0x18, 0xa3, 0x47, 0x6e, 0x93, 0x54, 0xad, 0x0a, 0x5d, 0xb8, 192 | 0x2a, 0x55, 0x5d, 0x78, 0xf6, 0xee, 0xbe, 0x8e, 0x3c, 0x76, 0x69, 0xb9, 0x40, 0xc2, 0x34, 193 | 0xec, 0x2a, 0xb9, 0xed, 0x7e, 0x20, 0xe4, 0x8d, 0x00, 0x38, 0xc7, 0xe6, 0x8f, 0x44, 0xa8, 194 | 0x86, 0xce, 0xeb, 0x2a, 0xe9, 0x90, 0xf1, 0x4c, 0xdf, 0x32, 0xfb, 0x73, 0x1b, 0x6d, 0x92, 195 | 0x1e, 0x95, 0xfe, 0xb4, 0xdb, 0x65, 0xdf, 0x4d, 0x23, 0x54, 0x89, 0x48, 0xbf, 0x4a, 0x2e, 196 | 0x70, 0xd6, 0xd7, 0x62, 0xb4, 0x33, 0x29, 0xb1, 0x3a, 0x33, 0x4c, 0x23, 0x6d, 0xa6, 0x76, 197 | 0xa5, 0x21, 0x63, 0x48, 0xe6, 0x90, 0x5d, 0xed, 0x90, 0x95, 0x0b, 0x7a, 0x84, 0xbe, 0xb8, 198 | 0x0d, 0x5e, 0x63, 0x0c, 0x62, 0x26, 0x4c, 0x14, 0x5a, 0xb3, 0xac, 0x23, 0xa4, 0x74, 0xa7, 199 | 0x6f, 0x33, 0x30, 0x05, 0x60, 0x01, 0x42, 0xa0, 0x28, 0xb7, 0xee, 0x19, 0x38, 0xf1, 0x64, 200 | 0x80, 0x82, 0x43, 0xe1, 0x41, 0x27, 0x1f, 0x1f, 0x90, 0x54, 0x7a, 0xd5, 0x23, 0x2e, 0xd1, 201 | 0x3d, 0xcb, 0x28, 0xba, 0x58, 0x7f, 0xdc, 0x7c, 0x91, 0x24, 0xe9, 0x28, 0x51, 0x83, 0x6e, 202 | 0xc5, 0x56, 0x21, 0x42, 0xed, 0xa0, 0x56, 0x22, 0xa1, 0x40, 0x80, 0x6b, 0xa8, 0xf7, 0x94, 203 | 0xca, 0x13, 0x6b, 0x0c, 0x39, 0xd9, 0xfd, 0xe9, 0xf3, 0x6f, 0xa6, 0x9e, 0xfc, 0x70, 0x8a, 204 | 0xb3, 0xbc, 0x59, 0x3c, 0x1e, 0x1d, 0x6c, 0xf9, 0x7c, 0xaf, 0xf9, 0x88, 0x71, 0x95, 0xeb, 205 | 0x57, 0x00, 0xbd, 0x9f, 0x8c, 0x4f, 0xe1, 0x24, 0x83, 0xc5, 0x22, 0xea, 0xfd, 0xd3, 0x0c, 206 | 0xe2, 0x17, 0x18, 0x7c, 0x6a, 0x4c, 0xde, 0x77, 0xb4, 0x53, 0x9b, 0x4c, 0x81, 0xcd, 0x23, 207 | 0x60, 0xaa, 0x0e, 0x25, 0x73, 0x9c, 0x02, 0x79, 0x32, 0x30, 0xdf, 0x74, 0xdf, 0x75, 0x19, 208 | 0xf4, 0xa5, 0x14, 0x5c, 0xf7, 0x7a, 0xa8, 0xa5, 0x91, 0x84, 0x7c, 0x60, 0x03, 0x06, 0x3b, 209 | 0xcd, 0x50, 0xb6, 0x27, 0x9c, 0xfe, 0xb1, 0xdd, 0xcc, 0xd3, 0xb0, 0x59, 0x24, 0xb2, 0xca, 210 | 0xe2, 0x1c, 0x81, 0x22, 0x9d, 0x07, 0x8f, 0x8e, 0xb9, 0xbe, 0x4e, 0xfa, 0xfc, 0x39, 0x65, 211 | 0xba, 0xbf, 0x9d, 0x12, 0x37, 0x5e, 0x97, 0x7e, 0xf3, 0x89, 0xf5, 0x5d, 0xf5, 0xe3, 0x09, 212 | 0x8c, 0x62, 0xb5, 0x20, 0x9d, 0x0c, 0x53, 0x8a, 0x68, 0x1b, 0xd2, 0x8f, 0x75, 0x17, 0x5d, 213 | 0xd4, 0xe5, 0xda, 0x75, 0x62, 0x19, 0x14, 0x6a, 0x26, 0x2d, 0xeb, 0xf8, 0xaf, 0x37, 0xf0, 214 | 0x6c, 0xa4, 0x55, 0xb1, 0xbc, 0xe2, 0x33, 0xc0, 0x9a, 0xca, 0xb0, 0x11, 0x49, 0x4f, 0x68, 215 | 0x9b, 0x3b, 0x6b, 0x3c, 0xcc, 0x13, 0xf6, 0xc7, 0x85, 0x61, 0x68, 0x42, 0xae, 0xbb, 0xdd, 216 | 0xcd, 0x45, 0x16, 0x29, 0x1d, 0xea, 0xdb, 0xc8, 0x03, 0x94, 0x3c, 0xee, 0x4f, 0x82, 0x11, 217 | 0xc3, 0xec, 0x28, 0xbd, 0x97, 0x05, 0x99, 0xde, 0xd7, 0xbb, 0x5e, 0x22, 0x1f, 0xd4, 0xeb, 218 | 0x64, 0xd9, 0x92, 0xd9, 0x85, 0xb7, 0x6a, 0x05, 0x6a, 0xe4, 0x24, 0x41, 0xf1, 0xcd, 0xf0, 219 | 0xd8, 0x3f, 0xf8, 0x9e, 0x0e, 0xcd, 0x0b, 0x7a, 0x70, 0x6b, 0x5a, 0x75, 0x0a, 0x6a, 0x33, 220 | 0x88, 0xec, 0x17, 0x75, 0x08, 0x70, 0x10, 0x2f, 0x24, 0xcf, 0xc4, 0xe9, 0x42, 0x00, 0x61, 221 | 0x94, 0xca, 0x1f, 0x3a, 0x76, 0x06, 0xfa, 0xd2, 0x48, 0x81, 0xf0, 0x77, 0x60, 0x03, 0x45, 222 | 0xd9, 0x61, 0xf4, 0xa4, 0x6f, 0x3d, 0xd9, 0x30, 0xc3, 0x04, 0x6b, 0x54, 0x2a, 0xb7, 0xec, 223 | 0x3b, 0xf4, 0x4b, 0xf5, 0x68, 0x52, 0x26, 0xce, 0xff, 0x5d, 0x19, 0x91, 0xa0, 0xa3, 0xa5, 224 | 0xa9, 0xb1, 0xe0, 0x23, 0xc4, 0x0a, 0x77, 0x4d, 0xf9, 0x51, 0x20, 0xa3, 0xa5, 0xa9, 0xb1, 225 | 0xc1, 0x00, 0x82, 0x86, 0x8e, 0x7f, 0x5d, 0x19, 0x91, 0xa0, 0xa3, 0xc4, 0xeb, 0x54, 0x0b, 226 | 0x75, 0x68, 0x52, 0x07, 0x8c, 0x9a, 0x97, 0x8d, 0x79, 0x70, 0x62, 0x46, 0xef, 0x5c, 0x1b, 227 | 0x95, 0x89, 0x71, 0x41, 0xe1, 0x21, 0xa1, 0xa1, 0xa1, 0xc0, 0x02, 0x67, 0x4c, 0x1a, 0xb6, 228 | 0xcf, 0xfd, 0x78, 0x53, 0x24, 0xab, 0xb5, 0xc9, 0xf1, 0x60, 0x23, 0xa5, 0xc8, 0x12, 0x87, 229 | 0x6d, 0x58, 0x13, 0x85, 0x88, 0x92, 0x87, 0x6d, 0x58, 0x32, 0xc7, 0x0c, 0x9a, 0x97, 0xac, 230 | 0xda, 0x36, 0xee, 0x5e, 0x3e, 0xdf, 0x1d, 0xb8, 0xf2, 0x66, 0x2f, 0xbd, 0xf8, 0x72, 0x47, 231 | 0xed, 0x58, 0x13, 0x85, 0x88, 0x92, 0x87, 0x8c, 0x7b, 0x55, 0x09, 0x90, 0xa2, 0xc6, 0xef, 232 | 0x3d, 0xf8, 0x53, 0x24, 0xab, 0xd4, 0x2a, 0xb7, 0xec, 0x5a, 0x36, 0xee, 0x5e, 0x3e, 0xdf, 233 | 0x3c, 0xfa, 0x76, 0x4f, 0xfd, 0x59, 0x30, 0xe2, 0x46, 0xef, 0x3d, 0xf8, 0x53, 0x05, 0x69, 234 | 0x31, 0xc1, 0x00, 0x82, 0x86, 0x8e, 0x7f, 0x5d, 0x19, 0xb0, 0xe2, 0x27, 0xcc, 0xfb, 0x74, 235 | 0x4b, 0x14, 0x8b, 0x94, 0x8b, 0x75, 0x68, 0x33, 0xc5, 0x08, 0x92, 0x87, 0x8c, 0x9a, 0xb6, 236 | 0xcf, 0x1c, 0xba, 0xd7, 0x0d, 0x98, 0xb2, 0xe6, 0x2f, 0xdc, 0x1b, 0x95, 0x89, 0x71, 0x60, 237 | 0x23, 0xc4, 0x0a, 0x96, 0x8f, 0x9c, 0xba, 0xf6, 0x6e, 0x3f, 0xfc, 0x5b, 0x15, 0xa8, 0xd2, 238 | 0x26, 0xaf, 0xbd, 0xf8, 0x72, 0x66, 0x2f, 0xdc, 0x1b, 0xb4, 0xcb, 0x14, 0x8b, 0x94, 0xaa, 239 | 0xb7, 0xcd, 0xf9, 0x51, 0x01, 0x80, 0x82, 0x86, 0x6f, 0x3d, 0xd9, 0x30, 0xe2, 0x27, 0xcc, 240 | 0xfb, 0x74, 0x4b, 0x14, 0xaa, 0xb7, 0xcd, 0xf9, 0x70, 0x43, 0x04, 0x6b, 0x35, 0xc9, 0xf1, 241 | 0x60, 0x23, 0xa5, 0xc8, 0xf3, 0x45, 0x08, 0x92, 0x87, 0x6d, 0x58, 0x32, 0xe6, 0x2f, 0xbd, 242 | 0xf8, 0x72, 0x66, 0x4e, 0x1e, 0xbe, 0xfe, 0x7e, 0x7e, 0x7e, 0x5f, 0x1d, 0x99, 0x91, 0xa0, 243 | 0xa3, 0xc4, 0x0a, 0x77, 0x4d, 0x18, 0x93, 0xa4, 0xab, 0xd4, 0x0b, 0x75, 0x49, 0x10, 0xa2, 244 | 0xc6, 0xef, 0x3d, 0xf8, 0x53, 0x24, 0xab, 0xb5, 0xe8, 0x33, 0xe4, 0x4a, 0x16, 0xae, 0xde, 245 | 0x1f, 0xbc, 0xdb, 0x15, 0xa8, 0xb3, 0xc5, 0x08, 0x73, 0x45, 0xe9, 0x31, 0xc1, 0xe1, 0x21, 246 | 0xa1, 0xa1, 0xa1, 0xc0, 0x02, 0x86, 0x6f, 0x5c, 0x3a, 0xd7, 0x0d, 0x98, 0x93, 0xa4, 0xca, 247 | 0x16, 0xae, 0xde, 0x1f, 0x9d, 0x99, 0xb0, 0xe2, 0x46, 0xef, 0x3d, 0xf8, 0x72, 0x47, 0x0c, 248 | 0x9a, 0xb6, 0xcf, 0xfd, 0x59, 0x11, 0xa0, 0xa3, 0xa5, 0xc8, 0xf3, 0x45, 0x08, 0x92, 0x87, 249 | 0x6d, 0x39, 0xf0, 0x43, 0x04, 0x8a, 0x96, 0xae, 0xde, 0x3e, 0xdf, 0x1d, 0x99, 0x91, 0xa0, 250 | 0xc2, 0x06, 0x6f, 0x3d, 0xf8, 0x72, 0x47, 0x0c, 0x9a, 0x97, 0x8d, 0x98, 0x93, 0x85, 0x88, 251 | 0x73, 0x45, 0xe9, 0x31, 0xe0, 0x23, 0xa5, 0xa9, 0xd0, 0x03, 0x84, 0x8a, 0x96, 0xae, 0xde, 252 | 0x1f, 0xbc, 0xdb, 0x15, 0xa8, 0xd2, 0x26, 0xce, 0xff, 0x5d, 0x19, 0x91, 0x81, 0x80, 0x82, 253 | 0x67, 0x2d, 0xd8, 0x13, 0xa4, 0xab, 0xd4, 0x0b, 0x94, 0xaa, 0xb7, 0xcd, 0xf9, 0x51, 0x20, 254 | 0xa3, 0xa5, 0xc8, 0xf3, 0x45, 0xe9, 0x50, 0x22, 0xc6, 0xef, 0x5c, 0x3a, 0xd7, 0x0d, 0x98, 255 | 0x93, 0x85, 0x88, 0x73, 0x64, 0x4a, 0xf7, 0x4d, 0xf9, 0x51, 0x20, 0xa3, 0xc4, 0x0a, 0x96, 256 | 0xae, 0xde, 0x3e, 0xfe, 0x7e, 0x7e, 0x7e, 0x5f, 0x3c, 0xfa, 0x76, 0x4f, 0xfd, 0x78, 0x72, 257 | 0x66, 0x2f, 0xbd, 0xd9, 0x30, 0xc3, 0xe5, 0x48, 0x12, 0x87, 0x8c, 0x7b, 0x55, 0x28, 0xd2, 258 | 0x07, 0x8c, 0x9a, 0x97, 0xac, 0xda, 0x17, 0x8d, 0x79, 0x51, 0x20, 0xa3, 0xc4, 0xeb, 0x54, 259 | 0x0b, 0x94, 0x8b, 0x94, 0xaa, 0xd6, 0x2e, 0xbf, 0xfc, 0x5b, 0x15, 0xa8, 0xd2, 0x26, 0xaf, 260 | 0xdc, 0x1b, 0xb4, 0xea, 0x37, 0xec, 0x3b, 0xf4, 0x6a, 0x37, 0xcd, 0x18, 0x93, 0x85, 0x69, 261 | 0x31, 0xc1, 0xe1, 0x40, 0xe3, 0x25, 0xc8, 0x12, 0x87, 0x8c, 0x9a, 0xb6, 0xcf, 0xfd, 0x59, 262 | 0x11, 0xa0, 0xc2, 0x06, 0x8e, 0x7f, 0x5d, 0x38, 0xf2, 0x47, 0x0c, 0x7b, 0x74, 0x6a, 0x37, 263 | 0xec, 0x5a, 0x36, 0xee, 0x3f, 0xfc, 0x7a, 0x76, 0x4f, 0x1c, 0x9b, 0x95, 0x89, 0x71, 0x41, 264 | 0x00, 0x63, 0x44, 0xeb, 0x54, 0x2a, 0xd6, 0x0f, 0x9c, 0xba, 0xd7, 0x0d, 0x98, 0x93, 0x85, 265 | 0x69, 0x31, 0xc1, 0x00, 0x82, 0x86, 0x8e, 0x9e, 0xbe, 0xdf, 0x3c, 0xfa, 0x57, 0x2c, 0xda, 266 | 0x36, 0xee, 0x3f, 0xfc, 0x5b, 0x15, 0x89, 0x71, 0x41, 0x00, 0x82, 0x86, 0x8e, 0x7f, 0x5d, 267 | 0x38, 0xf2, 0x47, 0xed, 0x58, 0x13, 0xa4, 0xca, 0xf7, 0x4d, 0xf9, 0x51, 0x01, 0x80, 0x63, 268 | 0x44, 0xeb, 0x54, 0x2a, 0xd6, 0x2e, 0xbf, 0xdd, 0x19, 0x91, 0xa0, 0xa3, 0xa5, 0xa9, 0xb1, 269 | 0xe0, 0x42, 0x06, 0x8e, 0x7f, 0x5d, 0x19, 0x91, 0xa0, 0xa3, 0xc4, 0x0a, 0x96, 0x8f, 0x7d, 270 | 0x78, 0x72, 0x47, 0x0c, 0x7b, 0x74, 0x6a, 0x56, 0x2e, 0xde, 0x1f, 0xbc, 0xfa, 0x57, 0x0d, 271 | 0x79, 0x51, 0x01, 0x61, 0x21, 0xa1, 0xc0, 0xe3, 0x25, 0xa9, 0xb1, 0xc1, 0xe1, 0x40, 0x02, 272 | 0x67, 0x4c, 0x1a, 0x97, 0x8d, 0x98, 0x93, 0xa4, 0xab, 0xd4, 0x2a, 0xd6, 0x0f, 0x9c, 0x9b, 273 | 0xb4, 0xcb, 0x14, 0xaa, 0xb7, 0xcd, 0xf9, 0x51, 0x20, 0xa3, 0xc4, 0xeb, 0x35, 0xc9, 0xf1, 274 | 0x60, 0x42, 0x06, 0x8e, 0x7f, 0x7c, 0x7a, 0x76, 0x6e, 0x3f, 0xfc, 0x7a, 0x76, 0x6e, 0x5e, 275 | 0x3e, 0xfe, 0x7e, 0x5f, 0x3c, 0xdb, 0x15, 0x89, 0x71, 0x41, 0xe1, 0x21, 0xc0, 0xe3, 0x44, 276 | 0xeb, 0x54, 0x2a, 0xb7, 0xcd, 0xf9, 0x70, 0x62, 0x27, 0xad, 0xd8, 0x32, 0xc7, 0x0c, 0x7b, 277 | 0x74, 0x4b, 0x14, 0xaa, 0xb7, 0xec, 0x3b, 0xd5, 0x28, 0xd2, 0x07, 0x6d, 0x39, 0xd1, 0x20, 278 | 0xc2, 0xe7, 0x4c, 0x1a, 0x97, 0x8d, 0x98, 0xb2, 0xc7, 0x0c, 0x59, 0x28, 0xf3, 0x9b 279 | }; 280 | -------------------------------------------------------------------------------- /firmware/src/srom.h: -------------------------------------------------------------------------------- 1 | #ifndef _SROM_H_ 2 | #define _SROM_H_ 3 | 4 | extern const unsigned short firmware_length; 5 | extern const unsigned char firmware_data[]; 6 | 7 | #endif 8 | -------------------------------------------------------------------------------- /firmware/src/trackball.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2022 Jacek Fedorynski 5 | * Copyright (c) 2019 Ha Thach (tinyusb.org) 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy 8 | * of this software and associated documentation files (the "Software"), to deal 9 | * in the Software without restriction, including without limitation the rights 10 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | * copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | * THE SOFTWARE. 24 | * 25 | */ 26 | 27 | #include 28 | #include 29 | #include 30 | 31 | #include 32 | #include 33 | 34 | #include 35 | #include 36 | 37 | #include 38 | #include 39 | #include 40 | 41 | #include "crc.h" 42 | #include "pmw3360.h" 43 | 44 | // These IDs are bogus. If you want to distribute any hardware using this, 45 | // you will have to get real ones. 46 | #define USB_VID 0xCAFE 47 | #define USB_PID 0xBAFA 48 | 49 | #define CONFIG_VERSION 1 50 | #define CONFIG_SIZE 22 51 | 52 | #define NBUTTONS 4 53 | #define RESOLUTION_MULTIPLIER 120 54 | 55 | #define PRESUMED_FLASH_SIZE 2097152 56 | #define CONFIG_OFFSET_IN_FLASH (PRESUMED_FLASH_SIZE - FLASH_SECTOR_SIZE) 57 | #define FLASH_CONFIG_IN_MEMORY (((uint8_t*) XIP_BASE) + CONFIG_OFFSET_IN_FLASH) 58 | 59 | uint button_pins[NBUTTONS] = { 16, 19, 17, 18 }; 60 | 61 | #define SENSOR0_SPI spi0 62 | #define SENSOR0_MISO 4 63 | #define SENSOR0_MOSI 3 64 | #define SENSOR0_SCK 2 65 | #define SENSOR0_NCS 9 66 | 67 | #define HALL_SENSOR1_PIN 26 68 | #define HALL_SENSOR2_PIN 27 69 | #define HALL_SENSOR1_INPUT 0 70 | #define HALL_SENSOR2_INPUT 1 71 | 72 | #define V_RESOLUTION_BITMASK (1 << 0) 73 | #define H_RESOLUTION_BITMASK (1 << 2) 74 | 75 | PMW3360 sensor = PMW3360(SENSOR0_SPI, SENSOR0_MISO, SENSOR0_MOSI, SENSOR0_SCK, SENSOR0_NCS); 76 | 77 | tusb_desc_device_t const desc_device = { 78 | .bLength = sizeof(tusb_desc_device_t), 79 | .bDescriptorType = TUSB_DESC_DEVICE, 80 | .bcdUSB = 0x0200, 81 | .bDeviceClass = 0x00, 82 | .bDeviceSubClass = 0x00, 83 | .bDeviceProtocol = 0x00, 84 | .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, 85 | 86 | .idVendor = USB_VID, 87 | .idProduct = USB_PID, 88 | .bcdDevice = 0x0100, 89 | 90 | .iManufacturer = 0x01, 91 | .iProduct = 0x02, 92 | .iSerialNumber = 0x00, 93 | 94 | .bNumConfigurations = 0x01, 95 | }; 96 | 97 | uint8_t const desc_hid_report[] = { 98 | 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 99 | 0x09, 0x02, // Usage (Mouse) 100 | 0xA1, 0x01, // Collection (Application) 101 | 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 102 | 0x09, 0x02, // Usage (Mouse) 103 | 0xA1, 0x02, // Collection (Logical) 104 | 0x85, 0x01, // Report ID (1) 105 | 0x09, 0x01, // Usage (Pointer) 106 | 0xA1, 0x00, // Collection (Physical) 107 | 0x05, 0x09, // Usage Page (Button) 108 | 0x19, 0x01, // Usage Minimum (0x01) 109 | 0x29, 0x08, // Usage Maximum (0x08) 110 | 0x95, 0x08, // Report Count (8) 111 | 0x75, 0x01, // Report Size (1) 112 | 0x25, 0x01, // Logical Maximum (1) 113 | 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position) 114 | 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) 115 | 0x09, 0x30, // Usage (X) 116 | 0x09, 0x31, // Usage (Y) 117 | 0x95, 0x02, // Report Count (2) 118 | 0x75, 0x10, // Report Size (16) 119 | 0x16, 0x00, 0x80, // Logical Minimum (-32768) 120 | 0x26, 0xFF, 0x7F, // Logical Maximum (32767) 121 | 0x81, 0x06, // Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) 122 | 0xA1, 0x02, // Collection (Logical) 123 | 0x85, 0x02, // Report ID (2) 124 | 0x09, 0x48, // Usage (Resolution Multiplier) 125 | 0x95, 0x01, // Report Count (1) 126 | 0x75, 0x02, // Report Size (2) 127 | 0x15, 0x00, // Logical Minimum (0) 128 | 0x25, 0x01, // Logical Maximum (1) 129 | 0x35, 0x01, // Physical Minimum (1) 130 | 0x45, RESOLUTION_MULTIPLIER, // Physical Maximum (RESOLUTION_MULTIPLIER) 131 | 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 132 | 0x85, 0x01, // Report ID (1) 133 | 0x09, 0x38, // Usage (Wheel) 134 | 0x35, 0x00, // Physical Minimum (0) 135 | 0x45, 0x00, // Physical Maximum (0) 136 | 0x16, 0x00, 0x80, // Logical Minimum (-32768) 137 | 0x26, 0xFF, 0x7F, // Logical Maximum (32767) 138 | 0x75, 0x10, // Report Size (16) 139 | 0x81, 0x06, // Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) 140 | 0xC0, // End Collection 141 | 0xA1, 0x02, // Collection (Logical) 142 | 0x85, 0x02, // Report ID (2) 143 | 0x09, 0x48, // Usage (Resolution Multiplier) 144 | 0x75, 0x02, // Report Size (2) 145 | 0x15, 0x00, // Logical Minimum (0) 146 | 0x25, 0x01, // Logical Maximum (1) 147 | 0x35, 0x01, // Physical Minimum (1) 148 | 0x45, RESOLUTION_MULTIPLIER, // Physical Maximum (RESOLUTION_MULTIPLIER) 149 | 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 150 | 0x35, 0x00, // Physical Minimum (0) 151 | 0x45, 0x00, // Physical Maximum (0) 152 | 0x75, 0x04, // Report Size (4) 153 | 0xB1, 0x03, // Feature (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 154 | 0x85, 0x01, // Report ID (1) 155 | 0x05, 0x0C, // Usage Page (Consumer) 156 | 0x16, 0x00, 0x80, // Logical Minimum (-32768) 157 | 0x26, 0xFF, 0x7F, // Logical Maximum (32767) 158 | 0x75, 0x10, // Report Size (16) 159 | 0x0A, 0x38, 0x02, // Usage (AC Pan) 160 | 0x81, 0x06, // Input (Data,Var,Rel,No Wrap,Linear,Preferred State,No Null Position) 161 | 0xC0, // End Collection 162 | 0xC0, // End Collection 163 | 0xC0, // End Collection 164 | 0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00) 165 | 0x09, 0x20, // Usage (0x20) 166 | 0x85, 0x03, // Report ID (3) 167 | 0x75, 0x08, // Report Size (8) 168 | 0x95, CONFIG_SIZE, // Report Count (CONFIG_SIZE) 169 | 0xB1, 0x02, // Feature (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile) 170 | 0xC0, // End Collection 171 | }; 172 | 173 | #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_DESC_LEN) 174 | #define EPNUM_HID 0x81 175 | 176 | uint8_t const desc_configuration[] = { 177 | // Config number, interface count, string index, total length, attribute, power in mA 178 | TUD_CONFIG_DESCRIPTOR(1, 1, 0, CONFIG_TOTAL_LEN, 0, 100), 179 | 180 | // Interface number, string index, protocol, report descriptor len, EP In address, size & polling interval 181 | TUD_HID_DESCRIPTOR(0, 0, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EPNUM_HID, CFG_TUD_HID_EP_BUFSIZE, 1) 182 | }; 183 | 184 | char const* string_desc_arr[] = { 185 | (const char[]){ 0x09, 0x04 }, // 0: is supported language is English (0x0409) 186 | "RP2040+PMW3360", // 1: Manufacturer 187 | "Trackball", // 2: Product 188 | }; 189 | 190 | struct __attribute__((packed)) hid_report_t { 191 | uint8_t buttons; 192 | int16_t dx; 193 | int16_t dy; 194 | int16_t vwheel; 195 | int16_t hwheel; 196 | }; 197 | 198 | hid_report_t report; 199 | 200 | enum class ButtonFunction : int8_t { 201 | NO_FUNCTION = 0, 202 | BUTTON1 = 1, 203 | BUTTON2 = 2, 204 | BUTTON3 = 3, 205 | BUTTON4 = 4, 206 | BUTTON5 = 5, 207 | BUTTON6 = 6, 208 | BUTTON7 = 7, 209 | BUTTON8 = 8, 210 | CLICK_DRAG = 9, 211 | SHIFT = 10, 212 | }; 213 | 214 | enum class BallFunction : int8_t { 215 | NO_FUNCTION = 0, 216 | CURSOR_X = 1, 217 | CURSOR_Y = 2, 218 | VERTICAL_SCROLL = 3, 219 | HORIZONTAL_SCROLL = 4, 220 | CURSOR_X_INVERTED = -1, 221 | CURSOR_Y_INVERTED = -2, 222 | VERTICAL_SCROLL_INVERTED = -3, 223 | HORIZONTAL_SCROLL_INVERTED = -4, 224 | }; 225 | 226 | enum class RingFunction : int8_t { 227 | NO_FUNCTION = 0, 228 | VERTICAL_SCROLL = 1, 229 | HORIZONTAL_SCROLL = 2, 230 | VERTICAL_SCROLL_INVERTED = -1, 231 | HORIZONTAL_SCROLL_INVERTED = -2, 232 | }; 233 | 234 | enum class ConfigCommand : int8_t { 235 | NO_COMMAND = 0, 236 | RESET_INTO_BOOTSEL = 1, 237 | }; 238 | 239 | struct __attribute__((packed)) config_t { 240 | uint8_t version; 241 | ConfigCommand command; 242 | BallFunction ball_function[2]; 243 | BallFunction ball_shifted_function[2]; 244 | uint8_t ball_cpi; 245 | uint8_t ball_shifted_cpi; 246 | RingFunction ring_function; 247 | RingFunction ring_shifted_function; 248 | ButtonFunction button_function[NBUTTONS]; 249 | ButtonFunction button_shifted_function[NBUTTONS]; 250 | uint32_t crc32; 251 | }; 252 | 253 | config_t config = { 254 | .version = CONFIG_VERSION, 255 | .command = ConfigCommand::NO_COMMAND, 256 | .ball_function = { BallFunction::CURSOR_X, BallFunction::CURSOR_Y_INVERTED }, 257 | .ball_shifted_function = { BallFunction::CURSOR_X, BallFunction::CURSOR_Y_INVERTED }, 258 | .ball_cpi = 600 / 100, 259 | .ball_shifted_cpi = 100 / 100, 260 | .ring_function = RingFunction::VERTICAL_SCROLL, 261 | .ring_shifted_function = RingFunction::HORIZONTAL_SCROLL, 262 | .button_function = { 263 | ButtonFunction::BUTTON1, 264 | ButtonFunction::BUTTON2, 265 | ButtonFunction::BUTTON3, 266 | ButtonFunction::SHIFT, 267 | }, 268 | .button_shifted_function = { 269 | ButtonFunction::BUTTON1, 270 | ButtonFunction::BUTTON2, 271 | ButtonFunction::BUTTON3, 272 | ButtonFunction::SHIFT, 273 | }, 274 | .crc32 = 0, 275 | }; 276 | 277 | uint8_t resolution_multiplier = 0; 278 | 279 | // 0 = sensor X, 1 = sensor Y, 2 = ring 280 | int accumulated_scroll[3] = { 0 }; 281 | uint64_t last_scroll_timestamp[3] = { 0 }; 282 | 283 | uint32_t prev_pin_state = 0xffffffff; 284 | bool click_drag = false; 285 | uint8_t current_cpi = 0; 286 | 287 | int16_t handle_scroll(int axis, int16_t movement, uint8_t multiplier_mask) { 288 | int16_t ret = 0; 289 | if (resolution_multiplier & multiplier_mask) { 290 | ret = movement; 291 | } else { 292 | if (movement != 0) { 293 | last_scroll_timestamp[axis] = time_us_64(); 294 | accumulated_scroll[axis] += movement; 295 | int ticks = accumulated_scroll[axis] / RESOLUTION_MULTIPLIER; 296 | accumulated_scroll[axis] -= ticks * RESOLUTION_MULTIPLIER; 297 | ret = ticks; 298 | } else { 299 | if ((accumulated_scroll[axis] != 0) && 300 | (time_us_64() - last_scroll_timestamp[axis] > 1000000)) { 301 | accumulated_scroll[axis] = 0; 302 | } 303 | } 304 | } 305 | return ret; 306 | } 307 | 308 | int encoder_rotation = 0; 309 | int prev_state1 = 0; 310 | 311 | void encoder_task(void) { 312 | adc_select_input(HALL_SENSOR1_INPUT); 313 | uint16_t val1 = adc_read(); 314 | adc_select_input(HALL_SENSOR2_INPUT); 315 | uint16_t val2 = adc_read(); 316 | 317 | int state1 = prev_state1; 318 | if (val1 > 2670) { 319 | state1 = 1; 320 | } 321 | if (val1 < 1602) { 322 | state1 = -1; 323 | } 324 | 325 | int state2 = 0; 326 | if (val2 > 2048) { 327 | state2 = 1; 328 | } 329 | if (val2 < 2048) { 330 | state2 = -1; 331 | } 332 | 333 | if (prev_state1 != state1) { 334 | if (state1 == state2) { 335 | encoder_rotation--; 336 | } 337 | if (state1 == -state2) { 338 | encoder_rotation++; 339 | } 340 | } 341 | 342 | prev_state1 = state1; 343 | } 344 | 345 | void hid_task() { 346 | if (!tud_hid_ready()) { 347 | return; 348 | } 349 | 350 | memset(&report, 0, sizeof(report)); 351 | 352 | uint32_t pin_state = gpio_get_all(); 353 | 354 | bool shifted = false; 355 | 356 | // first pass to determine if we're in shifted state 357 | for (int i = 0; i < NBUTTONS; i++) { 358 | if (config.button_function[i] == ButtonFunction::SHIFT && 359 | !(pin_state & (1 << button_pins[i]))) { 360 | shifted = true; 361 | } 362 | } 363 | 364 | // set CPI if not already correct 365 | uint8_t wanted_cpi = shifted ? config.ball_shifted_cpi : config.ball_cpi; 366 | if (current_cpi != wanted_cpi && wanted_cpi >= 1 && wanted_cpi <= 120) { 367 | sensor.set_cpi(wanted_cpi * 100); 368 | current_cpi = wanted_cpi; 369 | } 370 | 371 | for (int i = 0; i < NBUTTONS; i++) { 372 | ButtonFunction button_function = 373 | shifted ? config.button_shifted_function[i] : config.button_function[i]; 374 | if (config.button_function[i] == ButtonFunction::SHIFT) { 375 | button_function = ButtonFunction::NO_FUNCTION; 376 | } 377 | switch (button_function) { 378 | case ButtonFunction::NO_FUNCTION: 379 | case ButtonFunction::SHIFT: 380 | break; 381 | case ButtonFunction::BUTTON1: 382 | case ButtonFunction::BUTTON2: 383 | case ButtonFunction::BUTTON3: 384 | case ButtonFunction::BUTTON4: 385 | case ButtonFunction::BUTTON5: 386 | case ButtonFunction::BUTTON6: 387 | case ButtonFunction::BUTTON7: 388 | case ButtonFunction::BUTTON8: { 389 | int button = static_cast(button_function) - 1; 390 | if (!(pin_state & (1 << button_pins[i]))) { 391 | report.buttons |= 1 << button; 392 | } 393 | break; 394 | } 395 | case ButtonFunction::CLICK_DRAG: 396 | if ((prev_pin_state & (1 << button_pins[i])) && 397 | !(pin_state & (1 << button_pins[i]))) { 398 | click_drag = !click_drag; 399 | } 400 | break; 401 | } 402 | } 403 | 404 | if (click_drag) { 405 | report.buttons |= 1 << 0; 406 | } 407 | 408 | prev_pin_state = pin_state; 409 | 410 | sensor.update(); 411 | for (int axis = 0; axis < 2; axis++) { 412 | int16_t movement = sensor.movement[axis]; 413 | BallFunction ball_function = 414 | shifted ? config.ball_shifted_function[axis] : config.ball_function[axis]; 415 | if (static_cast(ball_function) < 0) { 416 | movement *= -1; 417 | } 418 | switch (ball_function) { 419 | case BallFunction::NO_FUNCTION: 420 | break; 421 | case BallFunction::CURSOR_X: 422 | case BallFunction::CURSOR_X_INVERTED: 423 | report.dx += movement; 424 | break; 425 | case BallFunction::CURSOR_Y: 426 | case BallFunction::CURSOR_Y_INVERTED: 427 | report.dy += movement; 428 | break; 429 | case BallFunction::VERTICAL_SCROLL: 430 | case BallFunction::VERTICAL_SCROLL_INVERTED: 431 | report.vwheel += handle_scroll(axis, movement, V_RESOLUTION_BITMASK); 432 | break; 433 | case BallFunction::HORIZONTAL_SCROLL: 434 | case BallFunction::HORIZONTAL_SCROLL_INVERTED: 435 | report.hwheel += handle_scroll(axis, movement, H_RESOLUTION_BITMASK); 436 | break; 437 | } 438 | } 439 | 440 | int16_t ring_movement = encoder_rotation * RESOLUTION_MULTIPLIER; 441 | encoder_rotation = 0; 442 | RingFunction ring_function = 443 | shifted ? config.ring_shifted_function : config.ring_function; 444 | if (static_cast(ring_function) < 0) { 445 | ring_movement *= -1; 446 | } 447 | switch (ring_function) { 448 | case RingFunction::NO_FUNCTION: 449 | break; 450 | case RingFunction::VERTICAL_SCROLL: 451 | case RingFunction::VERTICAL_SCROLL_INVERTED: 452 | report.vwheel += handle_scroll(2, ring_movement, V_RESOLUTION_BITMASK); 453 | break; 454 | case RingFunction::HORIZONTAL_SCROLL: 455 | case RingFunction::HORIZONTAL_SCROLL_INVERTED: 456 | report.hwheel += handle_scroll(2, ring_movement, H_RESOLUTION_BITMASK); 457 | break; 458 | } 459 | 460 | // uncomment to have pressing all four buttons reset into BOOTSEL 461 | // (convenient during development) 462 | // if (!(pin_state & (1 << button_pins[0]) || 463 | // pin_state & (1 << button_pins[1]) || 464 | // pin_state & (1 << button_pins[2]) || 465 | // pin_state & (1 << button_pins[3]))) { 466 | // reset_usb_boot(0, 0); 467 | // } 468 | 469 | tud_hid_report(1, &report, sizeof(report)); 470 | } 471 | 472 | void pin_init(uint pin) { 473 | gpio_init(pin); 474 | gpio_set_dir(pin, GPIO_IN); 475 | gpio_pull_up(pin); 476 | } 477 | 478 | void pins_init() { 479 | adc_init(); 480 | adc_gpio_init(HALL_SENSOR1_PIN); 481 | adc_gpio_init(HALL_SENSOR2_PIN); 482 | for (int i = 0; i < NBUTTONS; i++) { 483 | pin_init(button_pins[i]); 484 | } 485 | } 486 | 487 | void sensor_init() { 488 | sensor.init(); 489 | } 490 | 491 | void run_config_command() { 492 | // we probably shouldn't do this for config read from flash 493 | // or let's just not write any non-null command to flash 494 | if (config.command == ConfigCommand::RESET_INTO_BOOTSEL) { 495 | reset_usb_boot(0, 0); 496 | } 497 | } 498 | 499 | bool checksum_ok(const uint8_t* buffer) { 500 | return crc32(buffer, CONFIG_SIZE - 4) == ((config_t*) buffer)->crc32; 501 | } 502 | 503 | bool version_ok(const uint8_t* buffer) { 504 | return ((config_t*) buffer)->version == CONFIG_VERSION; 505 | } 506 | 507 | void load_config() { 508 | if (checksum_ok(FLASH_CONFIG_IN_MEMORY) && version_ok(FLASH_CONFIG_IN_MEMORY)) { 509 | memcpy(&config, FLASH_CONFIG_IN_MEMORY, CONFIG_SIZE); 510 | } 511 | } 512 | 513 | void persist_config() { 514 | uint8_t buffer[FLASH_PAGE_SIZE]; 515 | memset(buffer, 0, sizeof(buffer)); 516 | memcpy(buffer, &config, CONFIG_SIZE); 517 | uint32_t ints = save_and_disable_interrupts(); 518 | flash_range_erase(CONFIG_OFFSET_IN_FLASH, FLASH_SECTOR_SIZE); 519 | flash_range_program(CONFIG_OFFSET_IN_FLASH, buffer, FLASH_PAGE_SIZE); 520 | restore_interrupts(ints); 521 | } 522 | 523 | int main() { 524 | stdio_init_all(); 525 | board_init(); 526 | load_config(); 527 | pins_init(); 528 | sensor_init(); 529 | tusb_init(); 530 | 531 | while (true) { 532 | tud_task(); // tinyusb device task 533 | encoder_task(); 534 | hid_task(); 535 | } 536 | 537 | return 0; 538 | } 539 | 540 | // Invoked when device is mounted 541 | void tud_mount_cb() { 542 | // reset hi-res scroll for when we reboot from Windows into Linux 543 | resolution_multiplier = 0; 544 | } 545 | 546 | // Invoked when received GET DEVICE DESCRIPTOR 547 | // Application return pointer to descriptor 548 | uint8_t const* tud_descriptor_device_cb() { 549 | return (uint8_t const*) &desc_device; 550 | } 551 | 552 | // Invoked when received GET HID REPORT DESCRIPTOR 553 | // Application return pointer to descriptor 554 | // Descriptor contents must exist long enough for transfer to complete 555 | uint8_t const* tud_hid_descriptor_report_cb(uint8_t itf) { 556 | return desc_hid_report; 557 | } 558 | 559 | // Invoked when received GET_REPORT control request 560 | // Application must fill buffer report's content and return its length. 561 | // Return zero will cause the stack to STALL request 562 | uint16_t tud_hid_get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen) { 563 | if (report_id == 2 && reqlen >= 1) { 564 | memcpy(buffer, &resolution_multiplier, 1); 565 | return 1; 566 | } 567 | if (report_id == 3 && reqlen >= CONFIG_SIZE) { 568 | config.crc32 = crc32((uint8_t*) &config, CONFIG_SIZE - 4); 569 | memcpy(buffer, &config, CONFIG_SIZE); 570 | return CONFIG_SIZE; 571 | } 572 | 573 | return 0; 574 | } 575 | 576 | // Invoked when received SET_REPORT control request or 577 | // received data on OUT endpoint ( Report ID = 0, Type = 0 ) 578 | void tud_hid_set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize) { 579 | if (report_id == 2 && bufsize >= 1) { 580 | memcpy(&resolution_multiplier, buffer, 1); 581 | } 582 | if (report_id == 3 && bufsize >= CONFIG_SIZE) { 583 | if (checksum_ok(buffer) && version_ok(buffer)) { 584 | memcpy(&config, buffer, CONFIG_SIZE); 585 | run_config_command(); 586 | persist_config(); 587 | } 588 | } 589 | } 590 | 591 | // Invoked when received GET CONFIGURATION DESCRIPTOR 592 | // Application return pointer to descriptor 593 | // Descriptor contents must exist long enough for transfer to complete 594 | uint8_t const* tud_descriptor_configuration_cb(uint8_t index) { 595 | return desc_configuration; 596 | } 597 | 598 | static uint16_t _desc_str[32]; 599 | 600 | // Invoked when received GET STRING DESCRIPTOR request 601 | // Application return pointer to descriptor, whose contents must exist long enough for transfer to complete 602 | uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) { 603 | uint8_t chr_count; 604 | 605 | if (index == 0) { 606 | memcpy(&_desc_str[1], string_desc_arr[0], 2); 607 | chr_count = 1; 608 | } else { 609 | // Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors. 610 | // https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors 611 | 612 | if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0]))) 613 | return NULL; 614 | 615 | const char* str = string_desc_arr[index]; 616 | 617 | // Cap at max char 618 | chr_count = strlen(str); 619 | if (chr_count > 31) 620 | chr_count = 31; 621 | 622 | // Convert ASCII string into UTF-16 623 | for (uint8_t i = 0; i < chr_count; i++) { 624 | _desc_str[1 + i] = str[i]; 625 | } 626 | } 627 | 628 | // first byte is length (including header), second byte is string type 629 | _desc_str[0] = (TUSB_DESC_STRING << 8) | (2 * chr_count + 2); 630 | 631 | return _desc_str; 632 | } 633 | -------------------------------------------------------------------------------- /firmware/src/tusb_config.h: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2019 Ha Thach (tinyusb.org) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | */ 25 | 26 | #ifndef _TUSB_CONFIG_H_ 27 | #define _TUSB_CONFIG_H_ 28 | 29 | #ifdef __cplusplus 30 | extern "C" { 31 | #endif 32 | 33 | //-------------------------------------------------------------------- 34 | // COMMON CONFIGURATION 35 | //-------------------------------------------------------------------- 36 | 37 | // defined by board.mk 38 | #ifndef CFG_TUSB_MCU 39 | #error CFG_TUSB_MCU must be defined 40 | #endif 41 | 42 | // RHPort number used for device can be defined by board.mk, default to port 0 43 | #ifndef BOARD_DEVICE_RHPORT_NUM 44 | #define BOARD_DEVICE_RHPORT_NUM 0 45 | #endif 46 | 47 | // RHPort max operational speed can defined by board.mk 48 | // Default to Highspeed for MCU with internal HighSpeed PHY (can be port specific), otherwise FullSpeed 49 | #ifndef BOARD_DEVICE_RHPORT_SPEED 50 | #if (CFG_TUSB_MCU == OPT_MCU_LPC18XX || CFG_TUSB_MCU == OPT_MCU_LPC43XX || CFG_TUSB_MCU == OPT_MCU_MIMXRT10XX || \ 51 | CFG_TUSB_MCU == OPT_MCU_NUC505 || CFG_TUSB_MCU == OPT_MCU_CXD56) 52 | #define BOARD_DEVICE_RHPORT_SPEED OPT_MODE_HIGH_SPEED 53 | #else 54 | #define BOARD_DEVICE_RHPORT_SPEED OPT_MODE_FULL_SPEED 55 | #endif 56 | #endif 57 | 58 | // Device mode with rhport and speed defined by board.mk 59 | #if BOARD_DEVICE_RHPORT_NUM == 0 60 | #define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | BOARD_DEVICE_RHPORT_SPEED) 61 | #elif BOARD_DEVICE_RHPORT_NUM == 1 62 | #define CFG_TUSB_RHPORT1_MODE (OPT_MODE_DEVICE | BOARD_DEVICE_RHPORT_SPEED) 63 | #else 64 | #error "Incorrect RHPort configuration" 65 | #endif 66 | 67 | #ifndef CFG_TUSB_OS 68 | #define CFG_TUSB_OS OPT_OS_NONE 69 | #endif 70 | 71 | // CFG_TUSB_DEBUG is defined by compiler in DEBUG build 72 | // #define CFG_TUSB_DEBUG 0 73 | 74 | /* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. 75 | * Tinyusb use follows macros to declare transferring memory so that they can be put 76 | * into those specific section. 77 | * e.g 78 | * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) 79 | * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) 80 | */ 81 | #ifndef CFG_TUSB_MEM_SECTION 82 | #define CFG_TUSB_MEM_SECTION 83 | #endif 84 | 85 | #ifndef CFG_TUSB_MEM_ALIGN 86 | #define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4))) 87 | #endif 88 | 89 | //-------------------------------------------------------------------- 90 | // DEVICE CONFIGURATION 91 | //-------------------------------------------------------------------- 92 | 93 | #ifndef CFG_TUD_ENDPOINT0_SIZE 94 | #define CFG_TUD_ENDPOINT0_SIZE 64 95 | #endif 96 | 97 | //------------- CLASS -------------// 98 | #define CFG_TUD_HID 1 99 | #define CFG_TUD_CDC 0 100 | #define CFG_TUD_MSC 0 101 | #define CFG_TUD_MIDI 0 102 | #define CFG_TUD_VENDOR 0 103 | 104 | // HID buffer size Should be sufficient to hold ID (if any) + Data 105 | #define CFG_TUD_HID_EP_BUFSIZE 64 106 | 107 | #ifdef __cplusplus 108 | } 109 | #endif 110 | 111 | #endif /* _TUSB_CONFIG_H_ */ 112 | -------------------------------------------------------------------------------- /firmware/trackball.uf2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfedor2/scroll-ring-trackball/bcc73b1298b87e88c36e843fc690b19b71f73898/firmware/trackball.uf2 -------------------------------------------------------------------------------- /images/config-tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfedor2/scroll-ring-trackball/bcc73b1298b87e88c36e843fc690b19b71f73898/images/config-tool.png -------------------------------------------------------------------------------- /images/exploded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfedor2/scroll-ring-trackball/bcc73b1298b87e88c36e843fc690b19b71f73898/images/exploded.png -------------------------------------------------------------------------------- /images/internals1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfedor2/scroll-ring-trackball/bcc73b1298b87e88c36e843fc690b19b71f73898/images/internals1.jpg -------------------------------------------------------------------------------- /images/internals2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfedor2/scroll-ring-trackball/bcc73b1298b87e88c36e843fc690b19b71f73898/images/internals2.jpg -------------------------------------------------------------------------------- /images/trackball.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfedor2/scroll-ring-trackball/bcc73b1298b87e88c36e843fc690b19b71f73898/images/trackball.jpg --------------------------------------------------------------------------------