Keyboard firmware written in Haskell and C

root

Haskell Keyboard

This uses the pico-sdk, which is targeting specific microcontrollers like the rp2040 (my test device has 264kB SRAM and 2MB flash). Pico-sdk provides some examples, especially those relating to usb communication were used: dev_hid_composite and dev_multi_cdc were great help.

The MicroHs compiler is used to make Haskell programs run or the microcontroller. It compiles Haskell to a variant of SKI combinators, which are then interpreted by a c runtime. See the wiki.

Nix is a package manager that is used for external dependency handling. The nix part is based on pico-sdk-nixos-template, using nixpkgs' MicroHs wrapper.


How to Use

I added a Makefile where most of the instructions below are included. You stil will need to nix develop first to fetch all dependencies.

Be sure to make mountpoint refer to where you mounted your microcontroller.

# build and flash
make flash
# connect via usb serial
make tty

Detailed explanation

# Get all dependencies
nix develop

# Build library (haskellkeyboard-0.1.0.0.pkg)
mcabal build

# Run compiler
# -o provides output file
# -p provides pkg to load. Make sure it matches output of mcabal
# -C enables caching ; do not use when changing compiler versions and overwrite cache using -CW (cache write only)
# if encountering issues, omit -C
mhs Main.hs -oMain.c -phaskellkeyboard-0.1.0.0.pkg -C

# Configure cmake and create build scripts
cmake -S . -B build

# Build
cd build

    # Build
    make

    # Connect microcontroller when in boot mode
    # Mount it somewhere (likely done automatically)
    # The `mountpoint` symbolic link is expected to refer to it

    # Flash
    cp main.uf2 /mnt/your-device

# Leave build
cd ..

# Check for success
# May need to use different tty, look for changes on connecting
nix-shell -p screen --run "sudo screen /dev/ttyACM0 19200"
# should print "Hello World!" repeatedly

Files

tree
.
│
├──!flake.nix                   Dependencies; sets environment variables
├── flake.lock                  Versions of the flake.nix dependencies
│
├──!Makefile                    Helper script
├── mountpoint                  symbolic link to mount point of microcontroller
│
├── mhs-derivation.nix          Wraps MicroHs
├── mhs-release.nix             Wraps mhs-derivation.nix
│
├── CMakeLists.txt              Build instructions for pico sdk and MicroHs runtime
├── include
│   ├──!config.h                MicroHs runtime options
│   ├── tusb_config.h           tinyusb configuration
│   ├── usb_descriptors.h       tinyusb configuration
│   └──!extra.c                 MicroHs runtime options
├── usb_descriptors.c           tinyusb callbackis
│
├── haskellkeyboard.cabal       Cabal project for src directory
├── Main.hs                     Haskell program wrapper
├── src
│   ├──!Lib.hs                  Haskell main function
│   ├── Hid.hs                  Usb Hid Key list
│   │   ├── German.hs           pattern synonyms for german iso keyboard layout
│   │   └── Us.hs               pattern synonyms for us ansi keyboard layout
│   ├── Usb.hs                  Usb abstraction
│   └── Gpio.hs                 Gpio abstraction
│
├── LICENSE
└── README.md

You will need to interact with files marked with ! to compile and work. It should go like this:

rm mountpoint && ln -s /path/to/your/device mountpoint # point to your device
nix develop # get dependencies from `flake.nix` in current shell session
$EDITOR src/Lib.hs # edit program
$EDITOR include/extra.c # add c helper functions
$EDITOR include/config.h # add c helper function declarations
make flash # build and flash
make tty # listen to device output using usb communication

Without nix

You need: - environment variables to point pico-sdk and microhs runtime - mhs and mcabal in path

Reference

Pinpointing issues

In case you get a message staring with *** PANIC *** then this is generated by pico-sdk panic function. The message afterwards is therefore not related in any way to MicroHs. If e.g. Out of memory is printed afterwards, that means the malloc version in pico-sdk failed because it could not procure any more memory.

Current feature and wishlist

My current test device is an anavi macropad 12.

  • [x] Send keyboard reports via usb
    • [x] Send up to 6 pressed keys
    • [x] Send modifier keys (Ctrl, Shift, Alt, Gui) × (Left, Right)
    • [ ] Send any number of keys
  • [x] Send debug data via usb serial
    • [ ] Receive data via usb serial (not sure why you would want that)
  • [x] Key matrix scanning
  • [ ] Use oled display via I²C
  • [x] Use leds
  • [ ] Use WS2812B led strip
  • [ ] Hide tudTask from main loop
  • [ ] Extract event information from key state
  • [ ] Make a nice keyboard layer abstraction
  • [x] Make a usb hid keycode list
    • [ ] for many languages and layouts
  • [ ] Use the microcontroller's PIO module
    • [ ] make a DSL
  • [ ] Use the microcontroller's second core
  • [ ] Read rotary encoder (requires other test device)
  • [ ] Support split communication (requires other test device)
  • [ ] Make the windows operating system detect it as keyboard