Close
0%
0%

Adafruit Macropad Modding

I am using this project to document the changes and additions I make to my Adafruit Macropad firmware. Currently written in CircuitPython

Public Chat
Similar projects worth following
There are surprisingly few examples and guides for the Macropad despite always being sold out, so I am using this project to document my firmware changes and peripheral additions. My Macropad stuff started bleeding over into my other RP2040 project logs so I figured I'd put them into their own project.
Code can be found at https://github.com/neatloaf7/Adafruit-Macropad-FW
This is meant to be an example of some of the things you can do with the Macropad and CircuitPython in general. If you are trying to use my firmware directly and are running into issues, feel free to drop a message or comment and I can try to help. The OLED animation script is extremely specific to my use case, so if you try to use your own sprites it will likely not work.
The most in depth discussion will be found in my project logs.
I will try to add better videos and pictures but my phone camera scan rate matches the OLED refresh rate pretty closely so it's hard to get good footage.

Some features I have implemented:

  • Supports multiple profiles, currently using 2
  • profile keymaps and neopixel colors are set in config.json
  • Looping animation that indicates currently selected profile
    • After a set number of loops, play a fast effect animation
  • Simple cascade animation for updating neopixels
  • OLED and neopixel timeout
  • I2c gamepad added for mouse control
  • Asyncio used for task scheduling

PXL_20250314_150004703.mp4

Video of neopixel animation

MPEG-4 Video - 46.15 MB - 03/14/2025 at 15:05

Download

  • Log 3: Asyncio and the Adafruit Macropad Library

    neatloaf76 days ago 0 comments

    After reading the source code, I learned the macropad.display shares the same attributes as a display initialized with board or displayio. I have rewritten my code to use the macropad library as it takes care of initializing the macropad keys, neopixels, and encoder. This is a good reminder to myself to read the source code.

    One of the issues I ran into was code latency, which was especially noticeable during neopixel or OLED animations. One thing I did to mitigate this was to split up the sprite sheet I was initially using. The original sprite sheet had an entire 128x64 tile for every unique frame, which resulted in a 241kb sprite image, which was too large to load into memory. By splitting up the animation into several smaller sprite sheets, the final size for images comes out to 13kb. I switched to using adafruit_imageload over displayio's OnDiskBitmap to load the images into memory. I didn't do any testing but the code definitely initializes faster now. 

    The main bottleneck for speed was actually the QT Gamepad. The analog and digital button reads took up alot of processing time; almost a factor of 30 compared to running the macropad without the gamepad. I figured out it is the way the seesaw module reads and writes; by default there is a 0.008s delay after each read, unless you specify otherwise. I set the delay to 0 which might cause issues if many inputs are received in a short time, but it seems alright for now. The input checks still slow down the code, but the mouse is significantly more usable now. 

    Another thing that helped was refactoring my code to use asyncio. It simplifies the timing and checks for neopixel and OLED animations, and everything runs smoother as a whole now. There is a slight learning curve for those as inexperienced as I am, but I think it was worth it to refactor everything into asyncio functions. 

    Some other minor changes I made was moving the profile configs into a JSON file for easier editing, and bumping up the clock speed in the boot.py to 250MHz. I'm not sure if it made much of a difference but I'll keep it for now.

    One thing I will note is that I changed a lot of variables concurrently, so I'm not sure how much each change contributed to speeding up the program. Everything runs pretty smooth now so I probably won't go back and check.

    I have uploaded the new code to github here for reference. Now that the Macropad is an acceptable state, I can finally play games again.

    One issue I have left to solve is when switching between profiles quickly, the neopixel cascade animation sometimes bugs out and you can end up with the wrong neopixel colors for the profile. I could add a failsafe check to make sure animations match with the profile state but that might be overkill.

    I'm not sure what I should use the encoder buttons for either; my keyboard encoder already does previous/next track and pause. With all the free space I have now I could add some more animations but I'd have to be pretty bored to try making more pixel art.

  • Log 2: QT Gamepad, lag

    neatloaf703/04/2025 at 18:21 0 comments

    I printed out a new enclosure that includes a spot for the Adafruit QT gamepad module. I have the mouse controls setup, but mouse movement is very choppy, especially during the 8FPS animations. I believe displayio is taking up too many system resources while changing the default tile and refreshing the display, resulting in the script being bound up until displayio is finished. There are a few things that could remedy this problem.

    The easiest would be to split up the display group so that the whole display doesn't refresh. Only the top two quadrants need to be animated, so I can try appending groups only for animations, and have a main static group as the background. My current sprite.bmp is far too large to fit into memory using adafruit_imageload. I can either downsize the one sprite image and use adafruit_imageload, or have a couple separate images that hold only animation frames.

    A more complicated approach would be to refactor my code to use asyncio, which I should do anyway. I suspect a full display refresh task would still bind up the program but I'll have to check and see.

    The last and possibly most complicated approach would be to port everything to MicroPython. CircuitPython does not support threading but MicroPython does, and the Blinka library allows MicroPython to use CircuitPython libraries. Putting all display effects into their own thread could possibly be the best solution, and would make full use of the dual core mcu. I haven't done much research on MicroPython out of fear, but it could turn out to be easier to get everything working than with using asyncio.

    The true solution would be to implement all these things but I have other projects that need attention right now, and my MacroPad is technically in a working state. I won't be using the mouse control constantly so it won't be a huge issue, but it's definitely noticeable. One thing I haven't considered heavily is changing the display bus and CPU clock speeds. I believe the RP2040 can be safely overclocked to around 200Mhz. I just read that the OLED display uses an SPI bus which is faster than I2C, so I'm not sure if this will change anything. I will have to read over the displayio documentation and see if there are any settings or features I can change. 

  • Log 1

    neatloaf703/03/2025 at 19:36 0 comments

    First log will be copy and paste from my other hackaday project and github. 

    I started out using the Macropad with QMK. Once learning how to configure QMK it was very simple to use. One issue for me was that I had to compile and load the new firmware with each change I made. Another issue I had was with displaying animations; I believe QMK only supports importing images as byte arrays, so I had to use an online image converting utility to do this. 

    I recently ported all my settings to CircuitPython. CircuitPython takes care of the first issue, and the displayio library helps alot with importing .bmp images directly and animations. Adafruit supplies their own helper library for the Macropad which is a good place to start, but I found that more complex display features were inaccessible when using the library's Macropad class. Creating my firmware from scratch was very time consuming but allows for finer control over the Macropad features. Some things I have setup:

    • Supports multiple profiles, currently using 2
    • keycode and Neopixel profiles are set in keycodes.py and rgbs.py
    • Looping animation that indicates currently selected profile
    • After a set number of loops, play a fast effect animation
    • OLED and neopixel timeout
    • On profile change, play Neopixel animation that cascades new Neopixel profile downwards

    My way of configuring keycode and rgb profiles is not as user friendly as QMK's. Another thing I should setup eventually is scheduling tasks with asyncio. My whole firmware loop runs synchronously which isn't a big problem, but some lag can be introduced when an effect animation plays while the rgb profile is updating.

    My Macropad switches between sitting on my desk and attached to my sim rig. I recently bought this mini gamepad module from adafruit to be used for mouse controls. It can plug into the qwiic port on the Macropad, and I believe there is a CircuitPython library available. I am imagining something like those left-hand gaming keyboards with the analog stick on the thumb. Making the gamepad work will be my next task, and then I will need to print either a new housing, or an attachment that clamps onto my current housing.


    Gamepad

    I have been looking into KMK but I am not sure if I will make the switch. The only advantage would be simplifying configuring keycode and rgb profiles, and taking care of asynchronous tasks for watching button state changes. I think I would still have to port all my animation and neopixel code into coroutine functions with asyncio. As I probably won't be adding another profile anytime soon, KMK might be something to look at again when I'm bored.

View all 3 project logs

Enjoy this project?

Share

Discussions

John Citizen wrote 3 days ago point

Enjoying your updates! Keep it up

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates