The Missing Bit

Programming STM32l011 as I2C slave with STOP mode using rust

2021-03-27

In one of my project, I want to use a small microcontroller. Because of it's low cost, low power use and simplicity, I decided to use a STM32l011. With it's TSSOP footprint, I can solder it manually.

The MCU will be sleeping most of the time, and waken up by another MCU via I2C. So I have to put it in STOP mode with wake up from I2C.

Rust HAL do not have support for I2C slave. And anyway for fine tuning, it is easier to use PAC crates and access registers direcly. As all official documentation uses register names, it also makes documentation navigation easier.

The main issue I had is with STOP mode, I could never wake the device on I2C.

The solution while simple, took me hours to find. Instead of going in STOP mode with WIF we must go in STOP mode with SLEEPONEXIT and the device will go to sleep when there is no more interrupt waiting.

Below is some example working code for an STM32L011K4Tx.

#![no_main]
#![no_std]

// Use RTT for log and panic
use panic_rtt_target as _;
use rtt_target::rtt_init_print;
use rtt_target::rprintln;
use cortex_m::asm;
use rtic::app;
use stm32l0xx_hal::pac;

pub struct Buffer {
    data: [u8; 4],
    data_idx: usize,
}

pub struct Power {
    pwr: pac::PWR,
    rcc: pac::RCC,
    scb: pac::SCB,
}

#[app(device = stm32l0xx_hal::pac, peripherals = true)]
const APP: () = {
    struct Resources {
        i2c: pac::I2C1,
        buffer: Buffer,
        power: Power,
    }
    #[init]
    fn init(cx: init::Context) -> init::LateResources {
        let rcc = cx.device.RCC;
        let scb = cx.core.SCB;
        let pwr = cx.device.PWR;
        let gpio = cx.device.GPIOA;
        let i2c = cx.device.I2C1;

        // Enable HSI16 clock
        rcc.cr.modify(|_, w| w.hsi16on().set_bit());

        // Enable GPIOA clock
        rcc.iopenr.modify(|_, w| w.iopaen().enabled());
        // Enable peripheral clock
        rcc.apb1enr.modify(|_, w| w.pwren().enabled());
        // Enable I2C clock
        rcc.apb1enr.modify(|_, w| w.i2c1en().enabled());
        // Set I2C clock source as hsi16
        rcc.ccipr.modify(|_, w| w.i2c1sel().hsi16());

        // Reset I2C
        rcc.apb1rstr.modify(|_, w| w.i2c1rst().set_bit());
        rcc.apb1rstr.modify(|_, w| w.i2c1rst().clear_bit());

        // Disable RTC register protection
        pwr.cr.modify(|_, w| w.dbp().set_bit());
        // Enable LSE (external crystal) and set it as RTC source
        rcc.csr.modify(|_, w| {
            w.lseon().set_bit();
            w.rtcsel().lse();
            w.rtcen().set_bit()
        });
        // Restore RTC register protection
        pwr.cr.modify(|_, w| w.dbp().clear_bit());
        // Use HSI16 as wakeup clock and system clock
        rcc.cfgr.modify(|_, w| {
            w.stopwuck().set_bit();
            w.sw().hsi16()
        });


        // SCL - PA9  AF1
        // SDA - PA10 AF1

        // Configure SDA/SCL as open drain
        gpio.otyper.modify(|_, w| {
            w.ot9().open_drain(); // Open drain on SCL
            w.ot10().open_drain() // Open drain on SDA
        });

        // Alternate function 1
        gpio.afrh.modify(|_, w| {
            w.afsel9().af1(); // Alternate function 1 on SCL
            w.afsel10().af1() // Alternate function 1 on SDA
        });

        // Configure SDA/SCL as alternate function
        gpio.moder.modify(|_, w| {
            w.mode9().alternate();
            w.mode10().alternate()
        });

        // Timing register, per AN4235
        i2c.timingr.modify(|_, w| {
            w.presc().bits(0x06);
            w.scll().bits(0x13);
            w.sclh().bits(0x0f);
            w.sdadel().bits(0x02);
            w.scldel().bits(0x04)
        });

        // I2C configuration
        i2c.cr1.modify(|_, w| {
            w.addrie().enabled(); // Address match interrupt
            w.txie().enabled(); // Transmit interrupt
            w.stopie().enabled(); // Stop interrupt
            w.wupen().enabled(); // Wakeup from STOP
            // Both are required for WUPEN
            w.dnf().no_filter(); // Filter cannot be on with wake up
            w.nostretch().clear_bit(); // Enable stretch on SCL
            w.pe().enabled() // Peripheral enable
        });

        // I2C own address
        i2c.oar1.modify(|_, w| {
            w.oa1().bits(0b0011001000); // Set address
            w.oa1mode().bit7(); // Address is 7 bits
            w.oa1en().enabled() // Enable address 1
        });

        rtt_init_print!();

        rprintln!("I2C configured");
        rprintln!("I2C {:?}", i2c.txdr.read().bits());
        rprintln!("I2C {:?}", i2c.cr1.read().bits());


        init::LateResources {
            power: Power { scb, pwr, rcc },
            i2c,
            buffer: Buffer {
                data: [1, 2, 3, 4],
                data_idx: 0,
            },
        }
    }

    #[idle()]
    fn idle(cx: idle::Context) -> ! {
        loop {}
    }

    #[task(resources=[power])]
    fn sleep(cx: sleep::Context) {
        let power = cx.resources.power;

        power.scb.set_sleepdeep();
        power.scb.set_sleeponexit();

        power.pwr.cr.modify(|_, w| {
            // Clear wakeup flag
            w.cwuf().set_bit();
            // Ultra low power off
            w.ulp().clear_bit();
            // STOP mode when deepsleep
            w.pdds().stop_mode();
            // Regulator in MAIN mode in STOP mode
            w.lprun().clear_bit();
            // Regulator ON in SLEEP
            w.lpsdsr().clear_bit();
            // Regulator in MAIN mode in STOP mode
            w.lpds().clear_bit()
        });
    }
    #[task(binds=I2C1,spawn=[sleep], resources=[i2c, buffer, power])]
    fn i2c(cx: i2c::Context) {
        let power = cx.resources.power;
        let i2c = cx.resources.i2c;
        let buf = cx.resources.buffer;
        if i2c.isr.read().addr().is_match_() {
            power.scb.clear_sleeponexit();
            power.scb.clear_sleepdeep();
            i2c.icr.write(|w| w.addrcf().clear());
            buf.data_idx = 0;
            let b = buf.data.get(buf.data_idx).unwrap_or(&0);
            i2c.isr.modify(|_, w| w.txe().set_bit());
            i2c.txdr.write(|w| w.txdata().bits(*b));
            buf.data_idx += 1;
        }
        if i2c.isr.read().txis().is_empty() {
            let b = buf.data.get(buf.data_idx).unwrap_or(&0);
            i2c.txdr.write(|w| w.txdata().bits(*b));
            buf.data_idx += 1;
        }
        if i2c.isr.read().stopf().is_stop() {
            i2c.icr.write(|w| w.stopcf().clear());
            cx.spawn.sleep().unwrap();
        }
    }
    extern "C" {
        fn TIM3();
    }
};
If you wish to comment or discuss this post, just mention me on Bluesky or email me.