Coupling STM32 And Linux? Consider HID Over I2C

screenshot of the code defining a hid descriptor by using essentially macros for common descriptor types

If you’re pairing a tiny Linux computer to a few peripherals — perhaps you’re building a reasonably custom Pi-powered device — it’s rightfully tempting to use something like an STM32 for all your low-level tasks, from power management to reading keyboard events.

Now, in case you were wondering how to tie the two together, consider HID over I2C, it’s a standardized protocol with wide software and peripheral support, easily implementable and low-power. What’s more, [benedekkupper] gives you an example STM32 project with a detailed explanation on how you too can benefit from the protocol.

There are several cool things about this project. For a start, its code is generic enough that it will port across the entire STM32 lineup nicely. Just change the pin definitions as needed, compile it, flash it onto your devboard and experiment away. Need to change the descriptors? The hid-rdf library used lets you define a custom descriptor super easily, none of that building a descriptor from scratch stuff, and it even does compile-time verification of the descriptor!

The project has been tested with a Raspberry Pi 400, and [benedekkupper] links a tutorial on quickly adding your I2C-HID device on an Linux platform; all you need is DeviceTree support. Wondering what’s possible with HID? We’ve seen hackers play with HID aplenty here, and hacking on the HID standard isn’t just for building keyboards. It can let you automate your smartphone, reuse a laptop touchpad or even a sizeable Wacom input surface, liberate extra buttons on gamepads, or build your own touchscreen display.

25 thoughts on “Coupling STM32 And Linux? Consider HID Over I2C

    1. 2 weeks? That’s rookie numbers. More like 2 months to write anything that’s actually usable or maintainable. At least linux devicetrees are useful.

      Zephyr “devicetrees” look like their linux counterparts at a glance yet are anything but

      1. Have you even looked at the link? It’s a regular Linux dts file.

        Don’t assume everyone shares your irrational hate for a tool you couldn’t learn (and which is not even used here).

    1. I had to learn Zephyr because of work a few months ago and the most baffling thing about the entire experience was the fact that not more people disliked Zephyr.

      Its complex, bloated, provides questionable “features” which exist to get in your way, devicetrees which don’t work half the time. And when you get over all of that, you find that its actually very difficult to do anything that’s Arduino tier.

      Its just the very easy stuff that is made…slightly more easier. The harder stuff is arguably even more harder. Its driver model is tolerable but again, it simply fails if you want to do anything more than the most obvious and easiest interaction.

      I actually miss the times when FreeRTOS was the go-to RTOS, if you needed any RTOS at all! I don’t care for drivers from manufacturers because I can write better drivers 99% of the time for my suitable application.

      I’m sorry I don’t usually rant about software, and FOSS of all, but its just very frustrating.

    2. How does that even relate? Should we say that people push Arduino because they are too lazy to learn how to use avr-libc, or that they push Python because they are too lazy to learn C or Rust?

      Zephyr is still embedded, and it’s a preference. There are obvious advantages to using an RTOS for anything more complex than a blinking LED.

      Or you can keep reinventing the wheel and shaking your first at “lazy” people that are actually doing something productive.

      1. Yes we should say that because a lot of the time it is the case.

        Whether or not these people are doing anything productive is arguable and depends heavily on what it is they are trying to do.

  1. An alternative approach, particularly suited for Arduino boards communicating with Linux devices via I2C (including STM32s), involves employing a register-based I2C peripheral format.

    From the perspective of the master device, this method allows for reading and writing data to an emulated set of registers, akin to interfacing with an I2C EEPROM, for instance.

    On the peripheral side, incoming data can be immediately acted upon, or stored in a register for later processing. Outgoing data may be generated on demand, or pre-generated, stored in a register, and then retrieved upon request.

    For a detailed example of Arduino code implementing this peripheral-side functionality, which has been peer-reviewed and refined, along with comprehensive setup instructions, please refer to: https://green.bug-eyed.monster/arduino-i2c-slave/

    Exercise caution when using this approach alongside other libraries that disable interrupts for extended periods. The Arduino Wire library is sensitive to such interruptions, potentially causing the I2C peripheral to malfunction under certain conditions.

  2. Maybe call be old and outdated, but I usually prefer to solve this with UART. Usually I build some kind of small serial shell into my firmware. If it really needs to be more complicated then I use USB with multiple virtual CDCs talking to different modules within my firmware.

    Why? Because developing and debugging is so much easier this way. You can directly interact with it in a terminal, test it, sniff it, splice it, emulate it, tunnel it over tcp around the world, connect it to a virtual machine and so on. And it makes a very good interface for modular design: you can update either side and keep the other side compatible across multiple generations of designs.

      1. Usually UART allows a max. clock deviation between two parties of about 2%. This is because they can resynchronize every frame (=usually 8 bit).

        Current microcontrollers usually can achieve that with their integrated RC oscillator. For example all the STM32 do, but also cheaper alternatives like CH32V or PY32.

        So usually the integrated RC is all you need.

        1. 2% error margin should work if there aren’t other error sources, like sampling jitter, other side’s clock accuracy, etc.. Considering all error sources RC is not good enough. If you want a reliable system with uart then you have to use xtal or share clock soures.

          1. If the 2% aren’t reliable enough for your application, there are a few ways to still make it work with RC oscillators:

            – The easiest method: reduce baud rate. This will reduce the proportion of each bit time that is set apart for voltage swing and capacitive load on the wire and allow it to be used for clock drift instead. The UARTs usually oversample 16 times and often you can control the strategy used for determining the read bit out of the 16 samples.

            – Declare one side as the timing controller and regularly send a special command from it to the other devices. This command should include for example the char 0x7F, that results in one bit low at the start and one bit low at the end, high in between. This in-between time can then be precisely measured by the device with a timer. The result can then be used to trim the RC oscillator of the device.

            – Add some CRC mechanism to your data transfers and resend in case of an error.

            But in my experience just not overdoing it with the baud rate (like staying at 115200) is usually enough for a reliable transmission.

  3. Did not know this exists. Even though i do recall encountering I2C in stuff like keyboard docks for Tablet-PC so i have no clue how i didn’t consider this being a thing…

    Well. This is great to know now as i had just recently starting looking into making a “Cyberdeck” specifically for ROS2 applications with controller wings and such. HID-over-I2C could be a great boon to avoid occupying any of the USB ports and pull off stuff like having one of the Joysticks double as a improvised mouse.

    Assuming i can manage to figure out DeviceTree. That looks like one of those “Looks easy enough till you start messing with it” things.

  4. This seems like a very good idea, and potentially very useful to me in particular. A few weeks ago, I was working on programming an MCU (initially an STM32F103) to integrate a touch sensor mouse and keyboard into a single piece of hardware, and I’m not ashamed to admit that the brainwidth required to acquire the understanding required to build the descriptors for both in one device was more than I could afford, and the project is now in a box on the shelf. Yes, that shelf. In fact, I scoured the web for an example that would do mouse and keyboard together, but found nothing that would work. I was almost to the point of incorporating a USB hub into the project just to avoid building a custom descriptor, even though that would cost another MCU. The hid-rdf library (or [benedekkupper]’s example, anyway) looks like the example I couldn’t find, and the idea of using this over I2C so I don’t need to use USB within my device is a bonus.

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.