sebastians site
Adventures in Open Source Hard and Software

Hey nixos, where's my I2C? - 15.09.2024

The HAM radio research group at the University Kaiserslautern runs the aerospectator, a public dump1090 instance. It has been first installed by a student as part of their seminar paper and has been kept running by the group ever since. Somewhere in 2019 I inherited the maintenance of the setup, because part of my professional skill set is prodding and poking Raspberry Pis to make them run reliably (I work on an embedded linux distro for CM3 and CM4 based hardware platforms). Also, everyone else was just getting frustrated by having to climb on the roof every few months to replace the SD-card.

aerospectator hardware with the blue barrel radom opened and the cover taken off

One of my first changes was to create an ansible playbook to simply redeployment. I also added a piwatcher as hardware watchdog, to reset the old slightly underpowered Pi1 if it got stuck.

Fast-forward a couple of years: At some point in 2023 we decided to switch as much of the groups systems over to nixos and make them deployable with colmena. That setup proved less fragile than a wild mix debian versions with ansible. However, it also meant that we needed to get nix running on a Pi1 (with 23.05 that was not too complicated) and we needed to cross-compile everything all the time for that one system. However since we rarely changed the config for that system, it was workable. The piwatcher was a bit of challenge, but after learning how to wrap its userspace-tool and the necessary system configs into a flake it just worked.

And then nixos 24.05 attacked

While updating to 24.04 I was unfortunately unable to keep the cross compilation for the Pi1 working. After a lengthy, yet ultimately unsuccessfully, adventure in the engine room of nixos, it was decided to upgrade it, since Pi3 hardware is so cheap, that replacing the tired old Pi1 is probably the better option. It also enabled us to use aarch64 nixos caches, which meant a lot less cross-compiling.

I whipped up a quick config to generate a custom SD-card image with all our "usual stuff", and especially the tools to feed our watchdog, preinstalled.

1{
2 description = "Build raspi 3 bootstrap images";
3 inputs = {
4 nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05";
5 piwatcher.url = "git+ssh://forgejo@ourserver:/piwatcher.git";
6 };
7 outputs = { self, nixpkgs }:
8 let
9 pkgs = nixpkgs.legacyPackages.x86_64-linux;
10 pkgs_aarch64 = nixpkgs.legacyPackages.aarch64-linux;
11 in
12 {
13 formatter.x86_64-linux = pkgs.nixpkgs-fmt;
14 nixosConfigurations.rpi3 = nixpkgs.lib.nixosSystem {
15 modules = [
16 "${nixpkgs}/nixos/modules/installer/sd-card/sd-image-aarch64.nix"
17 {
18 nixpkgs.config.allowUnsupportedSystem = true;
19 nixpkgs.hostPlatform.system = "aarch64-linux";
20
21 imports = [
22 piwatcher.nixosModules.default
23 ];
24
25 # Stolen from nixos-hardware/blob/master/raspberry-pi/3/default.nix
26 boot.kernelPackages = pkgs_aarch64.linuxPackages_rpi3;
27 nixpkgs.overlays = [
28 (final: super: {
29 makeModulesClosure = x:
30 super.makeModulesClosure (x // { allowMissing = true; });
31 })
32 ];
33
34 services.openssh.enable = true;
35 users.users.root.openssh.authorizedKeys.keys = [
36 # Add keys here...
37 ];
38
39 # Talk to watchdog, to disable it
40 services.piwatcher = {
41 enable = true;
42 default-timeout = 0;
43 wakeup-timer = 0;
44 timeout = 0;
45 package = piwatcher.packages.x86_64-linux.cross-aarch64-linux;
46 };
47
48 environment.systemPackages = with pkgs_aarch64; [
49 i2c-tools
50 dtc
51 ];
52
53 system.stateVersion = "24.05";
54 }
55 ];
56 };
57 images.bootstrap-rpi3 =
58 self.nixosConfigurations.rpi3.config.system.build.sdImage;
59 };
60}

And that's when I hit a really annoying roadblock. The piwatcher flake wasn't working any more. The Pi kept getting power cycled by the watchdog. However after some investigation I noticed the piwatcher tool was working fine. It just couldn't talk to the hardware. There was no I2C device under /dev. dmesg and lsmod both show i2c-dev is loaded and doing something. So it must be a device tree issue. On a regular Pi I'd set dtparam=i2c_arm=on in /boot/config.txt and the bus should magically appear after a reboot.

So ... what's the nixos way of doing that? There's got be an option for that! Turns out there is boot.loader.raspberryPi.firmwareConfig. It's description states These options are deprecated, unsupported, and may not work like expected., which is probably true as it does not change my config.txt at all.

The config.txt is generated by nixos is actually pretty generic and bare-bones:

1[pi3]
2kernel=u-boot-rpi3.bin
3
4[pi02]
5kernel=u-boot-rpi3.bin
6
7[pi4]
8kernel=u-boot-rpi4.bin
9enable_gic=1
10armstub=armstub8-gic.bin
11
12# Otherwise the resolution will be weird in most cases, compared to
13# what the pi3 firmware does by default.
14disable_overscan=1
15
16# Supported in newer board revisions
17arm_boost=1
18
19[cm4]
20# Enable host mode on the 2711 built-in XHCI USB controller.
21# This line should be removed,
22# if the legacy DWC2 controller is required
23# (e.g. for USB device mode) or if USB support is not required.
24otg_mode=1
25
26[all]
27# Boot in 64-bit mode.
28arm_64bit=1
29
30# U-Boot needs this to work,
31# regardless of whether UART is actually used or not.
32# Look in arch/arm/mach-bcm283x/Kconfig
33# in the U-Boot tree to see if this is still
34# a requirement in the future.
35enable_uart=1
36
37# Prevent the firmware from smashing the framebuffer
38# setup done by the mainline kernel
39# when attempting to show low-voltage or
40# overtemperature warnings.
41avoid_warnings=1

Just editing config.txt to bypass nix also didn't help. I added dtparam=i2c_arm=on and it didn't make a difference.

But since we are already looking at FIRMWARE partition, do you notice something? There are a lot of dbt files, but they all have pi4 and cm4 in their name. Where's the devicetree file for the Pi3?

$ ls
armstub8-gic.bin      bootcode.bin  fixup4db.dat  start.elf start_cd.elf
bcm2711-rpi-4-b.dtb   config.txt    fixup4x.dat   start4.elf  start_db.elf
bcm2711-rpi-400.dtb   fixup.dat     fixup_cd.dat  start4cd.elf  start_x.elf
bcm2711-rpi-cm4.dtb   fixup4.dat    fixup_db.dat  start4db.elf  u-boot-rpi3.bin
bcm2711-rpi-cm4s.dtb  fixup4cd.dat  fixup_x.dat   start4x.elf u-boot-rpi4.bin

This, dear reader, is the point where my professionally experience in messing around with raspis comes into play. Seems like you've just won a little sidebar rant about devicetrees and how to get them into the linux kernel.

Traditionally on x86 and similar platforms the linux kernel would use BIOS, or these days UEFI interfaces to discover important hardware such as storage devices, network cards, videos cards, buses like USB. Your typical ARM SoC don't have a BIOS or UEFI (unless you are lucky enough to get to enjoy UEFI on aarch64). Instead, a devicetree is provided to the kernel as binary blob, somewhere in the RAM. The tree basically is a binary serialized representation of all the buses and attached peripherals, as well as some necessary setup parameters for your SoC. Usually, the devicetree is loaded by the bootloader along with the kernel and the initial ramdisk.

However, on the Raspberry Pi things can get a bit complicated, especially if there's a second stage bootloader like u-boot involved. Normally without a second stage bootloader, the first stage bootloader embedded in the Pis ROM looks for the first FAT32 formatted partition on the SD-Card, or in case of a compute module the onboard eMMC, and load the file bootcode.bin from there. There are also (presumably) newer versions of the bootloader that can boot from FAT16 as well, but you never know which version you end up with on a Pi3. The bootcode.bin is then execute by the "VideoCore GPU". Yes that's right the GPU bootstraps the CPU. Somebody at Broadcom thought this was a sensible way of doing things. This has some funny lesser known side effects, e.g. the overclocking the GPU, but not the CPU can speed up boot times, in a headless setup. The Pi4 and Pi5 appear to load this code from an EEPROM instead. After that continues to load start.elf, which brings up the actual ARM core, reads config.txt, prepares a device tree and finally hands the execution over to the kernel.
See the official documentation for more info.

If u-boot is used a second stage bootloader, then u-boot.bin pretends to be the linux kernel, so that it can be executed by start.elf. U-boot much like the linux kernel, needs to know to access the SoC peripheral, e.g. mass storage to load the actual OS, a UART to display the display its console, or a network interface to perform a network boot. For that it also needs a device tree. On Pi3 and older u-boot has two possible sources for that: It can use the device tree conveniently provided by the Raspberry Pi bootloader. Alternatively, it can have its own devicetree, which is embedded into it during compilation. After that it can forward one of those two trees to the kernel, or it can load a third tree from storage forward that one to the kernel.

On Pi4 and Pi5 things are little more complicated. Parameters like memory parameters and video timings are read from a ROM by the Raspberry Pi bootloader and added to the initial devicetree. This means all stage following the first stage bootloader have to reuse that devicetree, otherwise the linux kernel in the last stage will fail boot properly in some cases. Fortunately, you can just instruct u-boot to use the tree provided in RAM by the Raspberry Pi bootloader and then have pass that tree on to the linux kernel.

And that's almost all that you need to know about devicetrees. There is one more thing though: device tree overlays. Overlays are fragments of a device trees that can be added on top of a full tree, overwriting some of its properties. They can be applied either by a bootloader, e.g. the Raspberry Pi bootloader using dtoverlay in your config.txt or by u-boot using the ftd apply command. Alternatively, if you are feeling extra adventurous you can also apply an overlay to the device tree currently in use, after the kernel has booted. If for some reason you can not use overlays in your setup, you can also just merge a devicetree blob with some overlays at compile time using a command line utility.

Back to the problem at hand

As I found out, there is no device tree file for the Pi3 in the FIRMWARE partition. However, the booted Pi3 knows about its network card, USB works and there even a console on the HDMI port, so evidently it gets a device tree from somewhere. Using dtc we can even decompile it back into a text and look at it:

$ dtc -I fs /sys/firmware/devicetree/base

[ ... a bunch of warnings that I removed ]

/dts-v1/;

/ {
  #address-cells = <0x01>;
  model = "Raspberry Pi 3 Model B+";
  serial-number = "000000001d7c907a";
  #size-cells = <0x01>;
  interrupt-parent = <0x01>;
  compatible = "raspberrypi,3-model-b-plus\0brcm,bcm2837";

  fixedregulator_3v3 {
    regulator-max-microvolt = <0x325aa0>;
    regulator-always-on;
    regulator-min-microvolt = <0x325aa0>;
    regulator-name = "3v3";
    compatible = "regulator-fixed";
    phandle = <0x97>;
  };

  fixedregulator_5v0 {
    regulator-max-microvolt = <0x4c4b40>;
    regulator-always-on;
    regulator-min-microvolt = <0x4c4b40>;
    regulator-name = "5v0";
    compatible = "regulator-fixed";
    phandle = <0x98>;
  };

  memory@0 {
    device_type = "memory";
    reg = <0x00 0x3b400000>;
  };

  arm-pmu {
    interrupts = <0x09 0x04>;
    interrupt-parent = <0x18>;
    compatible = "arm,cortex-a53-pmu\0arm,cortex-a7-pmu";
  };

  thermal-zones {

    cpu-thermal {
      polling-delay = <0x3e8>;
      polling-delay-passive = <0x00>;
      thermal-sensors = <0x02>;
      phandle = <0x3c>;
      coefficients = <0xfffffde6 0x64960>;

      trips {
        phandle = <0x3d>;

        cpu-crit {
          temperature = <0x1adb0>;
          hysteresis = <0x00>;
          type = "critical";
        };
      };

      cooling-maps {
        phandle = <0x3e>;
      };
    };
  };

  soc {
    dma-ranges = <0xc0000000 0x00 0x3f000000 0x7e000000 0x3f000000 0x1000000>;
    #address-cells = <0x01>;
    #size-cells = <0x01>;
    compatible = "simple-bus";
    ranges = <0x7e000000 0x3f000000 0x1000000 0x40000000 0x40000000 0x1000>;
    phandle = <0x3f>;

  [...]

    i2c@7e804000 {
      pinctrl-names = "default";
      #address-cells = <0x01>;
      pinctrl-0 = <0x15>;
      interrupts = <0x02 0x15>;
      clocks = <0x08 0x14>;
      #size-cells = <0x00>;
      clock-frequency = <0x186a0>;
      compatible = "brcm,bcm2835-i2c";
      status = "disabled";
      reg = <0x7e804000 0x1000>;
      phandle = <0x2b>;
    };

  [...]

  };
};

There is a i2c@7e804000 in it, but messing around with config.txt didn't change anything about it, therefore it can't be the tree loaded by the Raspberry Pi bootloader. Similarly, if it were loaded by u-boot I'd expect a dtb file somewhere on the FIRMWARE partition. At that point I was totally stuck and did something that I should have done in the beginning: I looked at the u-boot article in the nixos wiki.

As it turns out u-boot has a feature called Generic Distro Configuration Concept, which replace the imperative u-boot bootscripts with a simpler config file. The way this is set up in nixos, u-boot looks for a file called extlinux/extlinux.conf on all partitions on all storage devices it can find. The file is located in /boot/extlinux/extlinux.conf when looking at the bootet OS on the pi.

$ cat /boot/extlinux/extlinux.conf
# Generated file, all changes will be lost on nixos-rebuild!

# Change this to e.g. nixos-42 to temporarily boot to an older configuration.
DEFAULT nixos-default

MENU TITLE ------------------------------------------------------------
TIMEOUT 50

LABEL nixos-default
  MENU LABEL NixOS - Default
  LINUX ../nixos/1w09bbijmmcw0hw2m9w0vycnzpkswrmi-linux-6.1.63-stable_20231123-Image
  INITRD ../nixos/bcfkyw1mgp4ykpajw1cz6pbh1vag1ygv-initrd-linux-6.1.63-stable_20231123-initrd
  APPEND init=/nix/store/r1cbbp0kdzi3k8apcfnfsx7rqxgn923y-nixos-system-aerospectator-24.05pre-git/init loglevel=4
  FDTDIR ../nixos/ldxaavrzzc60izvc9q2mfqdfph76b8dw-device-tree-overlays

This tells u-boot where to find the kernel, the initrd and most importantly the devicetree. And that's where I finally found the elusive bcm2837-rpi-3-b-plus.dtb that gets loaded for my Pi3.

$ ls -alh /boot/nixos/ldxaavrzzc*-device-tree-overlays/broadcom/
total 772K
dr-xr-xr-x 2 root root 4.0K Aug 10 11:47 .
dr-xr-xr-x 4 root root 4.0K Aug 10 11:47 ..
-r--r--r-- 1 root root  31K Aug 10 11:47 bcm2710-rpi-2-b.dtb
-r--r--r-- 1 root root  34K Aug 10 11:47 bcm2710-rpi-3-b-plus.dtb
-r--r--r-- 1 root root  33K Aug 10 11:47 bcm2710-rpi-3-b.dtb
-r--r--r-- 1 root root  31K Aug 10 11:47 bcm2710-rpi-cm3.dtb
-r--r--r-- 1 root root  32K Aug 10 11:47 bcm2710-rpi-zero-2-w.dtb
-r--r--r-- 1 root root  32K Aug 10 11:47 bcm2710-rpi-zero-2.dtb
-r--r--r-- 1 root root  54K Aug 10 11:47 bcm2711-rpi-4-b.dtb
-r--r--r-- 1 root root  54K Aug 10 11:47 bcm2711-rpi-400.dtb
-r--r--r-- 1 root root  38K Aug 10 11:47 bcm2711-rpi-cm4-io.dtb
-r--r--r-- 1 root root  55K Aug 10 11:47 bcm2711-rpi-cm4.dtb
-r--r--r-- 1 root root  51K Aug 10 11:47 bcm2711-rpi-cm4s.dtb
-r--r--r-- 1 root root  74K Aug 10 11:47 bcm2712-rpi-5-b.dtb
-r--r--r-- 1 root root  34K Aug 10 11:47 bcm2837-rpi-3-a-plus.dtb
-r--r--r-- 1 root root  34K Aug 10 11:47 bcm2837-rpi-3-b-plus.dtb
-r--r--r-- 1 root root  33K Aug 10 11:47 bcm2837-rpi-3-b.dtb
-r--r--r-- 1 root root  31K Aug 10 11:47 bcm2837-rpi-cm3.dtb
-r--r--r-- 1 root root  32K Aug 10 11:47 bcm2837-rpi-zero-2.dtb
-r--r--r-- 1 root root  54K Aug 10 11:47 bcm2838-rpi-4-b.dtb

Now I know that the devicetree is generated by nix somewhere and that it lives in the nix store, along with the kernel. That's pretty cool, because the FIRMWARE partition is usually not mounted and it's contents exist out of the immutable world of the nix store. It also means that there is no automatic way to update the FIRMWARE partition using nixos-rebuild or in my case using colmena apply (at least none that I found, please do correct me on this). However, with this setup only the bootloader lives in the firmware partition, so I can update the kernel and the devicetree by just changing our configuration and reapplying it.

Moreover, it also means that the hardware.devicetree should just allow us to setup a overlay.

1{ config, pkgs, ... }: {
2 hardware.deviceTree.overlays = [
3 {
4 name = "enable-i2c";
5 dtsText = ''
6 /dts-v1/;
7 /plugin/;
8 / {
9 compatible = "brcm,bcm2837";
10 fragment@0 {
11 target = <&i2c1>;
12 __overlay__ {
13 status = "okay";
14 };
15 };
16 };
17 '';
18 }
19 ];
20}

I don't want to go too much into the details of how overlays work, but this snippet targets i2c1 which is just an alias for i2c@7e804000 from the devicetree snippet above and flips the status property from disabled to okay. That's all that dtparam=i2c_arm=on would do on a regular pi.

After applying the new config and rebooting, I suddenly have the missing device file. The piwatcher starts to work, and I could just do what any sane person would do, that is stop poking at it and enjoy that it finally works. Maybe touch some grass instead of computers for a change. Then again, a sane person would have not ended up in this situation in the first place. Let's see just how deep this rabbit hole actually is.

The missing bits

I've successfully set up an overlay. So the changes I made must be somewhere™ on that pi.

$ ls -R /boot/nixos/iclw8i0iwj7i0cdq34f48c29s1ianp4i-device-tree-overlays
/boot/nixos/iclw8i0iwj7i0cdq34f48c29s1ianp4i-device-tree-overlays:
broadcom  overlays

/boot/nixos/iclw8i0iwj7i0cdq34f48c29s1ianp4i-device-tree-overlays/broadcom:
bcm2710-rpi-2-b.dtb       bcm2710-rpi-zero-2-w.dtb  bcm2711-rpi-cm4-io.dtb  
bcm2837-rpi-3-a-plus.dtb  bcm2837-rpi-zero-2.dtb    bcm2710-rpi-3-b-plus.dtb
bcm2710-rpi-zero-2.dtb    bcm2711-rpi-cm4.dtb       bcm2837-rpi-3-b-plus.dtb
bcm2838-rpi-4-b.dtb       bcm2710-rpi-3-b.dtb       bcm2711-rpi-4-b.dtb
bcm2711-rpi-cm4s.dtb      bcm2837-rpi-3-b.dtb       bcm2710-rpi-cm3.dtb  
bcm2711-rpi-400.dtb       bcm2712-rpi-5-b.dtb       bcm2837-rpi-cm3.dtb

/boot/nixos/iclw8i0iwj7i0cdq34f48c29s1ianp4i-device-tree-overlays/overlays:
hat_map.dtb  overlay_map.dtb

That's ... odd. There is no enable-i2c.dto. There are no overlay files at all. I could also trawl trough /nix using something like find /nix -iname *.dto ... or I could just take advantage of how nix works. Looking the path it's clear there has to be a derivation called device-tree-overlays somewhere in the nixpkgs repo. I can just grep the entire repo for that string. And bingo: pkgs/os-specific/linux/device-tree/default.nix#L28

1{ lib, stdenv, stdenvNoCC, dtc }: {
2 applyOverlays = (base: overlays': stdenvNoCC.mkDerivation {
3 name = "device-tree-overlays";
4 nativeBuildInputs = [ dtc ];
5 buildCommand = let
6 overlays = lib.toList overlays';
7 in ''
8 mkdir -p $out
9 cd "${base}"
10 find -L . -type f -name '*.dtb' -print0 \
11 | xargs -0 cp -v --no-preserve=mode --target-directory "$out" --parents
12
13 for dtb in $(find "$out" -type f -name '*.dtb'); do
14 dtbCompat=$(fdtget -t s "$dtb" / compatible 2>/dev/null || true)
15 # skip files without `compatible` string
16 test -z "$dtbCompat" && continue
17
18 ${lib.flip (lib.concatMapStringsSep "\n") overlays (o: ''
19 overlayCompat="$(fdtget -t s "${o.dtboFile}" / compatible)"
20
21 # skip incompatible and non-matching overlays
22 if [[ ! "$dtbCompat" =~ "$overlayCompat" ]]; then
23 echo "Skipping overlay ${o.name}: incompatible with $(basename "$dtb")"
24 elif ${if (o.filter == null) then "false" else ''
25 [[ "''${dtb//${o.filter}/}" == "$dtb" ]]
26 ''}
27 then
28 echo "Skipping overlay ${o.name}: filter does not match $(basename "$dtb")"
29 else
30 echo -n "Applying overlay ${o.name} to $(basename "$dtb")... "
31 mv "$dtb"{,.in}
32 fdtoverlay -o "$dtb" -i "$dtb.in" "${o.dtboFile}"
33 echo "ok"
34 rm "$dtb.in"
35 fi
36 '')}
37
38 done
39 '';
40 });
41}

And there is it is on line 32. The shell script that builds this derivation uses fdtoverlay to apply all the overlays to the devicetree files at build time. This means all overlays specified in my config will just be merged into bcm2837-rpi-3-b-plus.dtb while the derivation is build.

TL;DR

Finally, here's the short version:

  1. NixOS on the Pi3 (and older) uses u-boot to load the devicetree and discards the tree provided by the Raspberry Pi bootloader. So nothing you do to the devicetree using config.txt will have any effect.
  2. The devicetree and the kernel are loaded from the nix store, so they can just be updated along with everything else, without ever touching the FIRMWARE partition.
  3. Device tree overlays are applied at build time. So there is only one bcm2837-rpi-3-b-plus.dtb and no *.dtbo files at all.
  4. All the regular options to interact with the devicetree should just work™.

Also check out aerospectator.amateurfunk.uni-kl.de if you want to know what is flying in and out of the biggest American air force base on foreign soil. (Of course you'll only see planes with an active ADS-B transponder, so it's mostly just the boring stuff).

Published: 15.09.2024 16:17 Updated: 15.09.2024 17:17

RingLock for AVR - 18.10.2011

How it started

After reading about PIClock4 a inter-microcontroller mutex on dangerousprototypes.com, I got curious about the actual implementation. It uses shift registers to pass a token in form of just a single bit around. Once the token arrives at a mcu that needs access to the resource, the mcu can stop the shifting. After having done whatever it needs the resource for, it can start the shifting again. Unfortunately the schematic files were down, so I had to ask for them. The marker of piclock told me that his project was just for fun and never intended for actual usage. Because of the huge amount of logic chips needed he suggested rather using a arduino/atmega328 to emulate the hardware in software. That's what got me thinking initially. Using a 32k flash mcu to emulate 9 logic ics, sounded terribly wasteful to me.

First idea: I could use a Attiny instead.

Advantages: I would not waste that much flash space.

Disadvantage: Attinys have only few IO-pins and therefore the amount of mcus that can be attached to the lock is limited.

So I was looking for a solution that allows a huge number of mcus, while needing no/not much additional hardware. At some point I got reminded of token ring networks, something I've never seen myself, because it's a rather old technique and at least for computer networks totally replaced by Ethernet. It's basic ideas is that the hosts pass a token from one to the next in a ring like topology. Once a host gets a token he is allowed to attach data to it while passing it on. This is very similar to the bit that gets shifted around in the piclock system, but it does not require a central piece of hardware controlling the whole process. That sounded just like the thing I wanted.

After googling for a while I found a few implementations for token passing on microcontrollers, but the majority of them were integrated in other projects and designed for a single usage scenario. At that point I started experimenting with an own implementation. It had easily configurable for different AVR mcus and applicable to various scenarios where multiple mcus access the same resource.

It's important to notice that I don't claim this to be a full featured library. Let's be honest, there not even enough lines of code to call this a program. I rather wanted to present the idea behind it, as well as providing a little reference implementation. So just think of this as a request for comments.

How it works

The basic idea is to implement a decentralized token passing mechanism, allowing multiple mcus to share access to a resource like external memory, a sensor or a communication interface of some kind. Each mcu has one of its IO Pins connected to the external interrupt pin e.g. INT0 of the next mcu. The last mcu in this chain has one of its IOs connected to the interrupt pin of the first one. This results in a ring topology as shown below.

Rough idea for the ringlock system

When everything is powered up somebody has to have the token and the access initially. I called this special mcu master. It does whatever it needs to do with the resource and then pulls his IO-pin (called PYX in the drawing) high for 1us. Slave 1 will get interrupted because of the rising signal at his INT0. In his ISR he will check whether the rest of his program has requested access to the resource. This is done by simply checking a flag. If the flag is not set it will pass the token to slave 2 immediately in his ISR. Otherwise it will set a flag, signalling the program that it can now access the resource and leave the ISR. It's now up to the program to work with the shared resource for a while and to pass on the token. The next mcus will do exactly the same thing and the token will circulate in the ring infinitely.

At least theory. In praxis you'll sooner or later lose the token, simply because of murphys law. Imagine a mcu crashes, there is a power failure or a bad connection. Having lost the token, the system will get stuck since no one is allowed to access the shared resource. Every mcu waits for the token, that will never arrive. That's the point where a timeout might come handy. Using a timer of the master mcu it's easy to implement one. The timer gets reset every time the master gets the token back from the last mcu in the ring. If the token doesn't arrive in time, the master assumes that it was lost and pretends to have the token. The new token will be passed on and everything is hopefully working again. Still there is one pitfall in this approach, the timeout has to be chosen carefully. If the master sends a new token while another is still on the ring, to mcu will try to work with the resource, which is in fact the situation we are trying to avoid using lock. The best idea is to use a timeout that is at least 1,5 times as long, as the longest time a token need back to the master.

How to use it

There are two ways of getting the source code :

  1. Get the tarball here
  2. Clone the git repository: git clone https://github.com/LongHairedHacker/avr-ringlock

Once you got the source, you'll want to adjust the configuration in include/ringlock.h. The config is documented there and changing it should be pretty straight forward.

Using the makefile is a bit more complex then the usual make all. There are some variables that need to be set :

So if you want to build a slave for an Atmega8 with 8MHz call make ROLE=slave AVRMCU=atmega8 F_CPU=8000000 all. Currently the source only supports Atmega32 as master or slave and Atmega8 as slave. This can be changed by extending the configs in include/ringlock.h. Once you have added a new mcu, it would be nice to send me patch so I can add it to the source.

Using the ringlock in your source code is simple, it boils down to 4 functions :

Example

Since this was a solution to which I didn't have a Problem I made one up: One ATmega32 (Master) and one Atmega8 (Slave) share the TXD line of a RS232 connection. I used the OpenBench LogicSniffer to measure the signals.

Schematic of the test setup for the ringlock system

The and gate was in a HCF4081. If you set the TXD of the passive mcu to high-Z you won't need it, but I wanted the unmixed signals as well. The program used in this test is bundled with the sourcecode as main.c.

Here are all the signals in the jawis OLS client:

Signal traces in the OLS client

and a photo of my setup :

Actual hardware test-setup

So that's all so far.

Have fun with this stuff and please don't waste an entire Atmega32 on locks.

Published: 18.10.2011 20:36

AVR library for lc7981 - 12.04.2010

The information on this page is only for the DataVision DG-16080-11 lcd. It may apply to your lcd as well, but this also may not be the case. Be sure your read this page completely. It contains information about setting up the hardware which are not covered in the sourcecode documentation.

Idea

After discovering a quite cheap touchscreen lcd on the Roboternetz website I decided that such a display might be a useful addition to some of my projects. Therefore I bought some of them and started to write a C-library to use them with an ATmega32.

All the other librarys available missed some of the features I needed or they had some inefficient code.

Result

it has touch

it really has touch

The result was a small and quite flexible library for the lc7981-chip which is used by this display.

Features

I can hear you say now : 'That's just like any other library out there. So where's the point in using yours ?'

The lc7981 has to ways of setting Pixels. The first is using it's set pixel command. The advantage of plotting a bitmap with this command is that you get a simple short sourcecode, and it is easy to place a bitmap anywhere you like on the screen. A big disadvantage of doing it this way is that you'll need to transfer 2byte per pixel. Plotting a 10x10px bitmap will result in 200 bytes of data being send. The other approach is to transfer a byte directly into the display memory. One byte represents 8 pixel of a display row. Using this approach, transferring the 10x10 bitmap will require 4 bytes per row, which results in 40 bytes in total. The major disadvantage is that it is difficult to place a bitmap on an X-coordinate that isn't a multiple of 8. My library uses the second approach in combination with some tricky bit shift operations to be able to plot a bitmap fast and at every X-ccordinate. It needs some more computing time, but I thinks it's a good trade-off.

Wiring stuff up

My primary aim was the use the DataVision DG-16080-11 lcd, therefore I'll provide some info how I wired it up here. Using a plain lc7981 with some other lcd would be beyond the scope of this document. See the datasheet here for the display pinout. Connect each one of RS, RW and EN to a free pin of your controller. You'll have to specify the pins used in the lc7981.h. RES (Reset) has to be high and CS has to be low, so they have to be connected to Vcc and Gnd. The DB0-DB7 pins must be connect to one port of the AVR. Like for example PortB. Connect DB0 to PB0, DB1 to PB1 and so on. The Display supply voltage (pin 3) has to be connected to pin 17 and Gnd using a potentiometer. The potentiometer will be used to adjust the contrast.

License

The Versions below 0.6 beta were released under GPL. All newer versions including 0.6 and above are/will be realeased under MPL. I won't support the old GPLed versions anymore.

You may ask yourself why I changed my license. The answer is rather simple: I used GPL since it wanted my library to profit from bugs that get fixed and features that get written by other people who use it.

What I did not think about was the following scenario : Somebody uses my lib in an automated garden irrigation for his carrots. He does not change anything or adds any feature. He just uses my work, but he has to publish his source under GPL. I don't see any advantages in that. (Not that I don't like gardening or carrots, but I can't see a relation to my work here.) So I cause additional work for this poor guy and take away his freedom to chose an own license without any advantages for me, my work or him and his work. This is simply not what I intended to do in the first place

Okay, you might complain that I could have used LGPL instead of MPL. But it's terms are rather unclear to me and the rule of thumb with dynamic and static linking stuff is ridiculous when you use a mircocontroller where you have to use static linking. Therefore I use MPL so I still can profit from changes made to my code, but other can use it how they want as long as they don't change it.

Downloads

All the files can be obtained here : https://github.com/LongHairedHacker/avr-lc7981/

If you need the (not supported and not maintained) GPL Version or the latest development version use git to clone the repo and go back. I would not recommend using the GPL version, it has some ugly bugs.

If you have any questions feel free to ask using my mail adress, that can be found in the source code.

Have fun with your display!

Comments

Edmundo: (29.8.2011 9:44:48)

Thanks, I´m professor in the "INSTITUO POLITECNICO > NACIONAL" at MEXICO, just all the info my students needs to start. Very grateful.

Good information , very complete. Thanks again

Jesus Palomo: (11.9.2011 2:15:54)

Hi, I am student in the "Instituto Tecnologico de Toluca" at Mexico, you program and library very well, I compile in AVR Studio and worked well, Thanks for you info and post...Greetings from México

Published: 12.04.2010 16:47