This is my LoRaWAN enabled business card project.

It’s a four-channel 2KHz impulse counter based on an STM32L051K8.

assembled board

3d model

3d model

in case

profile

Hardware features include:

  • STM32L051K8 (64K flash, 8K RAM, Cortex M0+)
  • RFM95W (SX1276 based radio module from HopeRF)
  • dual SMA/UFL footprint
  • status button and LED indicator
  • UART with level converter
  • reverse cell protection
  • power supply for accessories (via “V+”)
  • surge protection
  • four inputs suitable for open-collector or dry-contact

The firmware is currently occupying half of available flash. Features include:

  • low power operation (~2uA in stop mode)
  • bespoke LoRaWAN stack
  • device failure alarms
  • tickless application timers
  • XTAL fail-safe
  • 32 bit counters stored in no-init volatile memory
  • count on rising/falling/both
  • alert on rising/falling/both/none
  • double-buffer configuration store with fail-safe defaults
  • multi-function status button with LED indicator
  • UART protocol
  • local and remote configuration/control via a common register system
  • hard fault diagnostics

Configuration items include:

  • (per input)
    • pull-up/pull-down
    • polarity
    • rise-time (250us to 10s)
    • counter trigger (rising/falling/both)
    • alert trigger (none/rising/falling/both)
    • publish enable/disable
  • publish interval
  • LoRaWAN parameters
  • radio enabled/disabled
  • user defined serial number (device name)
  • low-battery level

Pin Map

Inputs

pin signal comment
1 V+ weak power supply / reference
2 1 channel 1
3 2 channel 2
4 GND  
5 V+  
6 3 channel 3
7 4 channel 4
8 GND  

UART

The pin layout is compatible with a large number of commercial USB-to-serial adapters:

pin signal comment
1 GND mandatory
2 CTS connected to GND
3 VCC mandatory 3V3 or 5V (this powers the level converter)
4 TXD transmit line
5 RXD receive line
6 DTR not connected

Status Button and LED

The button has two actions depending on how it is pressed:

  • A short press will cause the status LED to blink the device status code.
  • A long press (>5s) will enable or disable joining mode (i.e. toggle the setting)

The status codes are as follows:

number of flashes status
0 dead
1 joined and downlink received in interval
2 joined but no downlink received in interval
3 joining
4 not-joined

LoRaWAN Message Format

direction port name
up 1 power-up
up 2 config-publish
up 3 input-publish
up 4 input-alert
down 1 remote-write-register

Power-Up

Sent once immediately after the first join-accept is received after system start. If MCU state is available from the time of reset, this is included in the message.

Counter-Publish ::= SEQUENCE
{
    device-reset-reason BIT STRING (SIZE(8)) {
        wdt,
        hard-fault,
        power,
        user,
        reserved-5,
        reserved-6,
        reserved-7,
        reserved-8,
    },
    device-alarms BIT STRING (SIZE(8)) {
        clock-failure,
        low-battery,
        over-temperature,
        reserved-4,
        reserved-5,
        reserved-6,
        reserved-7,
        reserved-8
    },
    mcu-state SEQUENCE
    {
        r0  INTEGER (0..4294967295),
        r1  INTEGER (0..4294967295),
        r2  INTEGER (0..4294967295),
        r3  INTEGER (0..4294967295),
        r12 INTEGER (0..4294967295),
        lr  INTEGER (0..4294967295),
        pc  INTEGER (0..4294967295),
        sr  INTEGER (0..4294967295)

    } OPTIONAL
}

The encoding is Octet Encoding Rules X.696 with the following exceptions:

  • OPTIONAL mcu-state presence is detected by size

Config-Publish

TBD.

Input-Publish

Sent every publish-interval. The abstract structure is as follows:

Counter-Publish ::= SEQUENCE
{
    device-alarms BIT STRING (SIZE(8)) {
        clock-failure,
        low-battery,
        over-temperature,
        reserved-4,
        reserved-5,
        reserved-6,
        reserved-7,
        reserved-8
    },
    input-alarms BIT STRING (SIZE(8)) {
        input-1-active,
        input-2-active,
        input-3-active,
        input-4-active,
        reserved1,
        reserved2,
        reserved3,
        reserved4
    },
    counter-presence BIT STRING (SIZE(8)) {
        input-1-present,
        input-2-present,
        input-3-present,
        input-4-present,
        reserved-5,
        reserved-6,
        reserved-7,
        reserved-8,
    },
    counter1 INTEGER (0..4294967295) OPTIONAL,
    counter2 INTEGER (0..4294967295) OPTIONAL,
    counter3 INTEGER (0..4294967295) OPTIONAL,
    counter4 INTEGER (0..4294967295) OPTIONAL
}

The encoding is Octet Encoding Rules X.696 with the following exceptions:

  • OPTIONAL counter fields are included according to corresponding counter-presence bit being set

Input-Alert

TBD.

Remote-Write-Register

TBD.

UART Protocol

UART Settings

  • 38400 baud
  • 8 data bits, 1 stop bit, no parity

Framing

The SLIP framing format is used to transport the CRC Layer.

CRC Layer

The CRC Layer consists of n bytes of payload and two bytes of CRC:

<payload 0><payload 1>...<payload n><CRC(MSB)><CRC(LSB)>

The CRC is ordered most significant byte first and is calculated over all payload bytes.

CRC parameters are as follows:

name CRC-16-CCITT
polynomial 0x1021
initial value 0xffff
reflect input no
reflect output no
xor output no

Message Format

Messages are encoded according to the Aloof protocol.

The key feature of this format is that information and control points are exposed as “objects” which may be read and/or written. The objects have human readable names which map to numerical codes.

Aloof Objects

Aloof objects are described in a JSON document that maps object name to a numerical OID and type.

{
    "log_severity_level":   {"oid":"0x0000", "type":"u8"},
    "boot_version":         {"oid":"0x0001", "type":"string"},
    "app_version":          {"oid":"0x0002", "type":"string"},
    "factory_id":           {"oid":"0x0003", "type":"blob"},
    "vbat":                 {"oid":"0x0004", "type":"u32", "scale":-3, "unit":"V"},
    "ambient":              {"oid":"0x0005", "type":"i32", "unit":"degC"},
    "vbat_low_threshold":   {"oid":"0x0006", "type":"u16", "scale":-3},
    "device_name":          {"oid":"0x0007", "type":"string"},
    "device_alarm":         {"oid":"0x0008", "type":"u8"},
    "ambient_offset":       {"oid":"0x0009", "type":"i16", "unit":"degC"},
    "reset":                {"oid":"0x000a", "type":"u8"},
    "reset_reason":         {"oid":"0x000b", "type":"u8"},

    "dev_eui":              {"oid":"0x0100", "type":"blob", "size":8},
    "app_eui":              {"oid":"0x0101", "type":"blob", "size":8},
    "app_key":              {"oid":"0x0102", "type":"blob", "size":16},
    "enc_app_key":          {"oid":"0x0103", "type":"blob", "size":16},
    "join_enabled":         {"oid":"0x0104", "type":"bool"},
    "rate":                 {"oid":"0x0105", "type":"u8"},
    "power":                {"oid":"0x0106", "type":"u8"},
    "adr_enabled":          {"oid":"0x0107", "type":"bool"},
    "joined":               {"oid":"0x0108", "type":"bool"},
    "dev_nonce":            {"oid":"0x0109", "type":"u32"},

    "counter1":             {"oid":"0x0200", "type":"u32"},
    "counter2":             {"oid":"0x0201", "type":"u32"},
    "counter3":             {"oid":"0x0202", "type":"u32"},
    "counter4":             {"oid":"0x0203", "type":"u32"},

    "input1_polarity":      {"oid":"0x0204", "type":"u8", "enum":{"low":0,"high":1}},
    "input2_polarity":      {"oid":"0x0205", "type":"u8", "enum":{"low":0,"high":1}},
    "input3_polarity":      {"oid":"0x0206", "type":"u8", "enum":{"low":0,"high":1}},
    "input4_polarity":      {"oid":"0x0207", "type":"u8", "enum":{"low":0,"high":1}},

    "input1_pull":          {"oid":"0x0208", "type":"bool", "enum":{"down":0,"up":1}},
    "input2_pull":          {"oid":"0x0209", "type":"bool", "enum":{"down":0,"up":1}},
    "input3_pull":          {"oid":"0x020a", "type":"bool", "enum":{"down":0,"up":1}},
    "input4_pull":          {"oid":"0x020b", "type":"bool", "enum":{"down":0,"up":1}},

    "input1_rise_time":     {"oid":"0x020c", "type":"u8", "enum":{"none":0,"25ms":1,"250ms":2,"1000ms":3,"5000ms":4,"10000ms":5}},
    "input2_rise_time":     {"oid":"0x020d", "type":"u8", "enum":{"none":0,"25ms":1,"250ms":2,"1000ms":3,"5000ms":4,"10000ms":5}},
    "input3_rise_time":     {"oid":"0x020e", "type":"u8", "enum":{"none":0,"25ms":1,"250ms":2,"1000ms":3,"5000ms":4,"10000ms":5}},
    "input4_rise_time":     {"oid":"0x020f", "type":"u8", "enum":{"none":0,"25ms":1,"250ms":2,"1000ms":3,"5000ms":4,"10000ms":5}},

    "input1_alert_trigger": {"oid":"0x0210", "type":"u8", "enum":{"never":0, "rising":1, "falling":2, "rising_falling":3}},
    "input2_alert_trigger": {"oid":"0x0211", "type":"u8", "enum":{"never":0, "rising":1, "falling":2, "rising_falling":3}},
    "input3_alert_trigger": {"oid":"0x0212", "type":"u8", "enum":{"never":0, "rising":1, "falling":2, "rising_falling":3}},
    "input4_alert_trigger": {"oid":"0x0213", "type":"u8", "enum":{"never":0, "rising":1, "falling":2, "rising_falling":3}},

    "input1_lockout":       {"oid":"0x0214", "type":"u8", "unit":"min"},
    "input2_lockout":       {"oid":"0x0215", "type":"u8", "unit":"min"},
    "input3_lockout":       {"oid":"0x0216", "type":"u8", "unit":"min"},
    "input4_lockout":       {"oid":"0x0217", "type":"u8", "unit":"min"},

    "counter1_publish":     {"oid":"0x0218", "type":"bool"},
    "counter2_publish":     {"oid":"0x0219", "type":"bool"},
    "counter3_publish":     {"oid":"0x021a", "type":"bool"},
    "counter4_publish":     {"oid":"0x021b", "type":"bool"},

    "publish_interval":     {"oid":"0x021c", "type":"u16", "unit":"min"},
    "alert_interval":       {"oid":"0x021d", "type":"u8", "unit":"min"},

    "input1_state":         {"oid":"0x021e", "type":"bool"},
    "input2_state":         {"oid":"0x021f", "type":"bool"},
    "input3_state":         {"oid":"0x0220", "type":"bool"},
    "input4_state":         {"oid":"0x0221", "type":"bool"},

    "counter1_trigger":     {"oid":"0x0222", "type":"u8", "enum":{"rising":0,"falling":1,"rising_falling":2}},
    "counter2_trigger":     {"oid":"0x0223", "type":"u8", "enum":{"rising":0,"falling":1,"rising_falling":2}},
    "counter3_trigger":     {"oid":"0x0224", "type":"u8", "enum":{"rising":0,"falling":1,"rising_falling":2}},
    "counter4_trigger":     {"oid":"0x0225", "type":"u8", "enum":{"rising":0,"falling":1,"rising_falling":2}}
}

Aloof Alerts

TBD.