Tutorials:
Sensors, interfaces and bus systems (SENIN, BUSSY)

Busses

last updated: 02/01/20

Introduction

Song(s) of this chapter: Bob Marley and the Wailers > Babylon by Bus (whole album)

TIA-232 has a point to point connection and is seldom used as bus system.

If more interfaces share the same medium, we call this a bus. A mainboard has a system bus to interconnect the major components of the computer system. In a Local Area Network (LAN) one of the network topologies is the Bus network) where all the nodes are connected to a single cable (called backbone).

We will look here at the Busses mostly used by IoT devices to connect to other devices or sensors and actuators, namely I²C, SPI and 1-Wire.

In a bus every device will get all the information. The time in communication must be shared, and this time must be assigned to avoid collisions. Normally this is done by a master. The other devices are called slaves. Each device has an individual address. Master/slave buses (e.g. I²C-Bus, SPI) are often synchronous and it is the the master that supplies the timing clock signal.

There exist also multi-master buses. An arbitration scheme must take care of conflicts (two masters need the bus at the same time).

In this chapter we will use a single-board computer instead of microcontroller to connect to sensors, actuators or other devices. The most known sb-computer is the Raspberry Pi. The Raspberry Pi is a very cheap computer that runs Linux (debian), but it also provides many general purpose input/output pins (GPIO) and interfaces (Serial, SPI, I²C).

All over the world, Raspberry Pi's are used in schools to learn programming skills and build hardware projects. A Raspberry Pi is cheap, but nevertheless a fully functioning computer. The Raspberry Pi is also often used in home automation, and more and more in industrial applications.

The GPIO's of the Raspberry Pi are less robust than the GPIO's of microcontroller a wrong voltage could destroy the computer. We can use additional hardware to protect the GPIO's likr the raspi buffer board, or the 8-bit I/O port-expander chip PCF8574 for I²C to create more robust GPIO's.

Using the Raspberry Pi (Raspi)

The operating system of the Raspberry Pi is running on an SD card. We will use the desktop version, because it is easier to write python code using the Thonny IDE.

Burn the image and enable ssh

For this download the newest Raspian OS image (.zip) from https://www.raspberrypi.org/downloads/raspbian/ (raspbian with desktop), unzip it and and flash the image on an SD card. More infos can be found e.g. on rapberrytips.com.

For security reasons, ssh (secured access over network) is no longer enabled by default on the Raspi, so our headless Raspi can't be accessed. To enable ssh we need to place an empty file named ssh (no extension) in the boot directory. So we insert the SD card in a computer. We can access boot easily because it has a FAT32 file system and create an empty file with the name ssh in the root folder.

Restart the Raspberry Pi and connect it to your Ethernet. For security reasons we will not use WiFi. To find the IP address of the Raspi we can use the nmap command. If not installed, install nmap (linux: sudo apt install nmap)). Look for your Raspi with a ping scan:

    sudo nmap -sP 192.168.1.0/24

Hello3

Look for the IP address of your Raspi (e.g. 192.168.1.125) and log in with ssh (putty on windows):

    sudo ssh pi@192.168.1.125

Now log in with the standard user pi and the standard password raspberry (with a German keyboard the password is raspberrz :)).

Change password and do an upgrade

As every one knows the Raspi password let's change it immediately with the passwd command. First you will be prompted for your old password, then you new password twice.

To get the latest versions of all programs use the following commands:

    sudo apt update
    sudo apt upgrade
    sudo apt dist-upgrade

It is also good idea to install the terminal file manager "midnight commander" (mc) to search and edit (F4) files as root in terminal (sudo mc) and htop to look at processes.

    sudo apt install mc htop

Add a static IP address

It is simpler to know Raspi's IP address, so we set it static. Call the midnight commander sudo mc and open the file dhcpcd.conf in the /etc folder. Press F4 to open the nano text editor and uncomment the following lines in /etc/dhcpcd.conf. Change the IP addresses to your needs.

    interface eth0
    static ip_address=192.168.1.100/24
    static routers=192.168.1.1

Save the file with Ctrl-O, exit with Ctrl-X , exit mcwith F10 and reboot (sudo reboot). Now we can log in with the new IP.

Use VNC Viewer

It is very convenient to work on the Raspberry Pi by remote control. A VNC (graphical desktop sharing system) server is already installed on the Raspi. It can be enabled by using the following command:

    sudo raspi-config

Go to 5 Interfacing options and enable P3 VNC, P4 SPI, P5 I²C and P7 1-Wire. Reboot the Raspi.

On our computer or mobile device we need to download and run VNC Viewer from realvnc.com. VNC Viewer transmits the keyboard, mouse or touch events to the VNC Server on the Raspi, and receives updates to the screen in return.

Start VNC Viewer and type the IP address from the Raspi. In a window you are now able to control the Raspi as though you were working on itself. More infos on raspberrypi.org.

Further infos about the Raspberry Pi under http://weigu.lu/sb-computer/raspi_tips_tricks.

Learning Python

Let's write a little Python Hello World program for the Raspi. If you don't know Python, you will learn it in a blink.

Python is a powerful, high-level, object-oriented, open source programming language that's easy to use, because it has a very clean and readable syntax. Python is very easy to learn and has user-friendly data structures. Python also runs on all operating systems and is so powerful because of thousands of libraries (modules).

The Pi in Raspberry Pi stands for Python (the Raspberry is a reference to a fruit naming tradition in the old days of microcomputers).

Python is an interpreted language, meaning the interpreter executes instructions directly one by one.

"Just do it" Busses 1:

All the structuring in Python is done with Indentation. In Arduino (C,C++) we use curly braces { and } to structure the code. In python we indent by 4 spaces. Don't use tabs! Python code can be written with any editor. The file is saved in "text only" with the extension .py.

The first line of if statements (if...elif...else), for and while loops, functions and try..except statements end with a colon :.

Using the GPIO's

Now to our first hardware Hello World program, the blinking LED.

"Just do it" Busses 2:

Raspberry Pi GPIO blink

      #!/usr/bin/env python3
      # -*- coding: utf-8 -*-
      # gpio_blink.py

      import RPi.GPIO as GPIO  # sudo apt-get install rpi.gpio
      from time import sleep
      PIN_LED = 17
      GPIO.setmode(GPIO.BCM)   # Broadcom SOC channel number (numbers after GPIO)
      GPIO.setup(PIN_LED, GPIO.OUT)
      try:
          for i in range(0,10):
              GPIO.output(PIN_LED, GPIO.HIGH)
              sleep(0.5)
              GPIO.output(PIN_LED, GPIO.LOW)
              sleep(0.5)
      except KeyboardInterrupt:
          print("Keyboard interrupt by user")
      GPIO.cleanup()
      #!/usr/bin/env python3
      # -*- coding: utf-8 -*-
      # gpio_read.py

      import RPi.GPIO as GPIO  # sudo apt-get install rpi.gpio
      from time import sleep

      PIN_LED = 17
      PIN_SWITCH = 27
      GPIO.setmode(GPIO.BCM)   # Broadcom SOC channel number (numbers after GPIO)
      GPIO.setup(PIN_SWITCH, GPIO.IN)
      GPIO.setup(PIN_LED, GPIO.OUT)

      try:
          while (True):
              state_sw = GPIO.input(PIN_SWITCH)
              if state_sw == 0:
                  GPIO.output(PIN_LED, GPIO.HIGH)
              else:
                  GPIO.output(PIN_LED, GPIO.LOW)
              sleep(0.5)
      except KeyboardInterrupt:
          print("Keyboard interrupt by user")
          GPIO.cleanup()

1-Wire (wiki)

1-Wire is a device communications bus system designed by Dallas Semiconductor. It is possible to use only 1 wire (2 wires including ground) because power can be delivered over the data line when inactive. The voltage may range from 2.8 V to 6 V and the sensors have an internal capacitor to store the energy over a short time period.

The serial asynchronous half-duplex interface is similar to I²C but slower and it can cover bigger distances (up to 750 m). The bus takes one master and up to 100! slaves. Each slave has an unique 64-bit address (8-bit family-Code, 48-bit serial number, 8 bit CRC checksum), that is burned in the chip during production.

A popular IC using this bus is the DS18B20 temperature sensor. The temperature range is from -55 °C to +125 °C. The accuracy is high with less than ±0.5°C error between -10°C and +85°C). The internal ADC has 12 bit and the conversion needs about 750 ms.

The data wire has to be on high potential when inactive, because the master and the slaves pull the wire to ground when transmitting data. So we need a pull-up resistor. The current through this resistor should be about 1 mA, so 4.7 kΩ is a good value if we use 5 V, and 2.7 kΩ is a good value for 3.3 V. The driver of the Raspi is configured to use pin 4 for 1-Wire. We use here the 3 wire variant, because data communication is more robust with a supplementary 3.3 V power wire.

Raspberry Pi GPIO blink

Take care not to cause short circuits when wiring. The Raspi's GPIO pins are not as robust as those of various microcontrollers! If the rpibuffboard adapter (http://www.weigu.lu/sb-computer/rpibufferboard) is used, set the jumper to 3.3 V.

The drivers for the Raspi's 1-Wire bus are not hard-coded into the kernel, but must be loaded via kernel modules at startup. If a Raspi image with graphical user interface is used, the 1-Wire interface can be enabled under Menu > Preferences > Raspberry Pi Configuration > Interfaces. For a Raspi without graphical interface this is done with the command sudo raspi-config (5 Interfacing options, P7 1-Wire). Both commands enable a line in the file boot/config.txt. With the command lsmod you can check which kernel modules are loaded. For 1-Wire the modules are called w1-therm, w1_gpio and wire).

In Linux everything is a file! This is also true for our 1-Wire sensor.

If the sensor is wired correctly, the driver takes care of requesting the unique address and creates a directory for the sensor with this address in the following subdirectory: /sys/bus/w1/devices. The name of the directory is the unique address (without 8 bit CRC checksum) of the sensor. 0x28 is the 8 bit family code. The hyphen is followed by the 48-bit serial number (6 bytes). As the sensor is treated like a file the device file has the name w1-slave and contains two text lines. The content of the file can be displayed with the cat command (or in midnight commander with F3).

1-Wire device

Our Python program must only read the file and parse the temperature:

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    # busses_w1_ds18b20_1.py

    device_file = "/sys/bus/w1/devices/28-000006b684e1/w1_slave"

    with open(device_file, 'r') as f:
        line1 = f.readline()
        print(line1, end='')
        line2 = f.readline()
        print(line2, end='')

To access the file we use the with statement because it automatically closes files after our operations and thus facilitates the code. After opening the file (r for read), we read the lines and use the print()-method with the supplementary argument end='' to suppress the newline character of the method.

1-Wire device

The required information is located in the 2nd line after the string t=. With the find() method the temperature string can be extracted and converted into a number. All the temperature readings are packed into a function, that returns a number. A possible version of the program could look like the following:

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    # busses_w1_ds18b20_2.py

    from time import sleep

    device_file = "/sys/bus/w1/devices/28-000006b684e1/w1_slave"

    def read_temp():
        with open(device_file, 'r') as f:
            line1 = f.readline()
            line2 = f.readline()
        pos = line2.find("t=")
        if pos != -1:
            temp_string = line2[pos + 2:]
            temp = round(float(temp_string) / 1000.0, 1)
        else:
            print("error: temperature not found")
        return temp

    try:
        while (True):
            print(str(read_temp())+" °C")
            sleep(1)
    except KeyboardInterrupt:
        print("Keyboard interrupt by user")

The program can be stopped with CTRL+C.

"Just do it" Busses 3:
      from glob import glob

      try:
          device_folder = glob("/sys/bus/w1/devices/28*")
      except:
          print("Error: Sensor not found")
      device_file = device_folder[0] + "/w1_slave"
"Just do it" Busses 4:

1-Wire jdi4

I²C (wiki)

I²C (Inter-Integrated-Circuit) was developed by Philipps (now NXP) intended for communication between IC's in Audio and Video devices. It is a synchronous, half-duplex interface on a (multi)master/slave bus. Many special IC's can be used directly on this bus. The IC's have an own selectable address. More than one master can be used on the bus (multi-master), but normally the master (in our case the Raspi) transmits and a slave responds. I²C is a superset of the Intel's SMBus. On AVR controller the interface is called TWI (two-wire-interface).

Advantages of the I²C bus are the low wiring effort and the low costs in the development of a device. A microcontroller can control a whole network of IC's with only three wires and simple software. This lowers the costs of the device to be developed. During operation, chips can be added to or removed from the bus (hot-plugging).

Disadvantages of the I²C bus are the low speed and the small bridgeable distances. Data can only be sent alternately over the data line (half duplex), and in addition to the data, the addresses of the chips must be sent. It is not suitable for longer distances (low interference immunity).

Application:
The I²C bus is mostly used for the transmission of control and configuration data, where speed is not so important. It is used e.g. for real-time clocks, volume controls, sensors, A/D and D/A converters with low sampling rates, EEPROM memory chips or bidirectional switches and multiplexers.

Four speeds can be used depending on the IC's:

The higher speeds (1 Mbit/s, 3.4 Mbit/s, 5 Mbit/s) are possible but reduce the length of the connecting wires.

i2c

In the picture one master and three slaves are shown. The synchronous I²C bus requires a Serial Clock Line (SCL), a Serial Data Line (SDA) and ground (GND). Each data bit on the SDA line is synchronized with the clock of the SCL line. The pull-up resistors on the clock and data lines pull both lines to high level in idle state. As for 1-Wire all devices connected to the bus have an open collector (bipolar transistor) or open drain (FET) output (the collector or drain of a transistor is open (not connected) and gets connected to VCC by the common pull-up resistor of the bus). If the bipolar transistor or FET is activated, the bus is pulled to ground. Such a circuit is called a []wired AND connection](https://en.wikipedia.org/wiki/Wiredlogicconnection), because the circuit acts like an AND gate.

The I²C protocol and addressing

With a falling edge on SDA (SCL = High) the master starts the communication. After the start bit, the master first sends the address byte to the slave. The address byte consists of a 7-bit slave address and a read-write bit, which determines the direction of communication. The slave confirms the correct reception with an ACK confirmation bit (ACKnowledgement). The master generates the 9 clock pulses and then reads the clock line. Here a slow slave with a low level can then force a waiting time (clock stretching).

i2c

Depending on the direction of the communication, the master or slave now sends any number of data bytes (8 bits, MSB first). Each data byte is confirmed by an ACK bit (low level). The transmission is aborted by the master or slave sending a NACK bit (Not ACKnowledge, HIGH). With a rising edge on SDA (SCL = HIGH) the master releases the bus again (stop bit).

i2c

To save time the master can continue without releasing the bus (no stop bit) and start a new communication with another start bit (Repeated Start). The communication direction can of course be changed at will. The address byte sent by the master consists, as described, of seven bits that represent the actual address of the slave and an eighth bit that determines the read or write direction. The I²C interface uses an address space of 7 bits, which means that 112 devices can be addressed simultaneously on one bus (16 of the 128 possible addresses are reserved for special purposes). Each I²C-capable component (IC) has a fixed address. Some IC's, have the possibility to change a part of the address with control pins. This allows e.g. to use up to eight similar IC's on one I²C bus. More and more often the address can also be reprogrammed by software (e.g. for digital sensors). There is also a newer alternative 10 bit addressing (1136 blocks). It is downward compatible with the 7-bit standard (uses 4 of the 16 reserved addresses additionally).

I²C with the Raspberry Pi

As for 1-Wire, the I²C interface can be switched on with raspi-config in terminal or via the menu if a Raspi image with a graphical user interface is used (menu > Preferences > Raspberry Pi Configuration).

The new Raspbian automatically takes care of several steps here. To use the I²C bus the appropriate kernel modules are loaded. With the command lsmod you can check which kernel modules are loaded. For I²C the modules are called i2c-dev and i2c-bcm2708. The packages i2c-tools and python3-smbus are already installed (if they are not installed, they can be installed with sudo apt install i2c-tools python3-smbus). To avoid having to run the programs with root privileges, the user pi now belongs to the group i2c. This can be verified with the command groups pi (if it is not the case you can use the command sudo adduser pi i2c).

The Real Time Clock (RTC) DS3231

The Raspi has no RTC. So if we start a Raspberry Pi the Raspi date and time is the 01/01/70 00:00:00. If a network is available the Raspberry Pi gets the time and date from the Internet using the Network Time Protocol (NTP). If the network is down, we have no accurate time for our IoT device (the Raspi :)). So lets use an RTC breakout board to test sending and receiving data on the I²C bus.

A real time clock continues to run even without external power supply because it uses a battery (usually a lithium button cell with 3 V). Maxim's DS1307 I²C device was often used as the real-time clock. Today it is mostly replaced with the DS3231, an RTC that is more accurate, faster (400 kHz), doesn't request an external clock crystal of 32.768 kHz, has 2 alarms and even a measures the temperature.

We will use an RTC breakout board populated with this chip.

Raspberry Pi GPIO blink

"Just do it" Busses 5:

The Raspi has 2 I²C ports (i2c-0 and i2c-1). On the first Raspis port 0 was used. On newer Raspis port 1 (SDA1 (pin3) and SCL1 (pin 5)) is used. It is also possible to use port 0 but by default is is disabled.

After the RTC board has been connected we can use the i2c-tools to test if our bus works out. To do this, we enter the following command (1 stands for port 1):

    i2cdetect -y 1

i2cdetect

The output shows us all addresses of the connected I²C devices. In in our case the address '0x68' of the RTC.

"Just do it" Busses 6:

The clock module has 19 registers (RAM memory cells), which can be accessed via a register address. The first seven registers contain the data of the clock (time (3), weekday (1) and date (3)). As soon as the seconds register has been written (bit 7 (CH) = 0) the clock is running. The data is stored in BCD code (Binary Coded Decimal). BCD is a code with dual coded decimal digits. 4 bits (nibble) represent one decimal digit (0-9, 0b0000-0b1001).

Here a basic code in python to set the RTC and run it.

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    # busses_i2c_ds3231_1.py

    from smbus import SMBus
    from time import sleep

    port = 1                # (0 for i2c-0, 1 for i2c-1)
    Bus = SMBus(port)
    rtcAddr = 0x68

    def bcd2str(d):         # // for integer division; % for modulo
        if (d <= 9):
            return '0' + str(d)
        else:
            return str(d // 16) + str(d % 16)

    def set_clock(): #set clock (BCD: sec,min,hour,weekday,day,mon,year)
        rtc_address_map = [0x00, 0x00, 0x08, 0, 0x01, 0x01, 0x20]
        Bus.write_i2c_block_data(rtcAddr, 0, rtc_address_map)

    set_clock()
    try:
        while (True):
            am = Bus.read_i2c_block_data(rtcAddr, 0, 7)
            time_str = bcd2str(am[2]) + ':' + bcd2str(am[1]) + ':' + bcd2str(am[0])
            date_str = bcd2str(am[4]) + '/' + bcd2str(am[5]) + '/' + bcd2str(am[6])
            print(date_str + ' ' + time_str)
            sleep(1)
    except KeyboardInterrupt:
        print("Keyboard interrupt by user")

The class SMBus from the smbus library is used to access I²C. After creating the object Bus for port 1 we can access the bus with the methods write_i2c_block_data() and read_i2c_block_data() to send and receive a whole block of data to the bus. For our RTC we will send or read a memory block of 7 bytes, The BCD data is contained in a python list (called here rtcaddressmap).

The clock should of course be set only when the program is called up for the first time. This is done with the function set_clock(). After that the command should be deactivated with a comment character.

The write command inside the set_clock() function takes three parameters. The first parameter is the I²C address. The second parameter is a command from the master. In our case we can specify the start address of the RTC address pointer to the address map here (it is automatically incremented when the data is transferred). We pass zero as the starting address, so that the memory is written from the second address (with a one we would start at the minutes.) The third parameter is the list with the data to be written. !

In the following infinite main loop the clock is read every second and date and time are output with print(). The reading of the RTC is done with the method read_i2c_block_data(). The first two parameters are the same as for the write method. The third parameter specifies the number of bytes to be read. Since the data is in BCD, it must be converted into a string. This is done by the function bcd2str(), using the integer division and the modulo operation.

"Just do it" Busses 7:
      from datetime import datetime

      def int2bcd(d):         # // for integer division; % for modulo
           return (((d // 10) << 4) + (d % 10))

      def set_clock(): #set clock (BCD: sec,min,hour,weekday,day,mon,year)
          s = int2bcd(datetime.now().second)
          mi = int2bcd(datetime.now().minute)
          h = int2bcd(datetime.now().hour)
          d = int2bcd(datetime.now().day)
          mo = int2bcd(datetime.now().month)
          y = int2bcd(datetime.now().year-2000)
          wd = datetime.now().weekday()
          td = [s, mi, h, wd, d, mo, y]
          bus.write_i2c_block_data(rtcAddr, 0, td)
      weekday = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
"Just do it" Busses 8:
      weekday_lcd = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
      time_temp_str_lcd = time_str + ' ' + temp_str + "ßC"

Adding MQTT

Next we add MQTT communication to our python scripts. First we need the paho MQTT python client from Eclipse. We install it with the following coammand:

    sudo apt install python3-paho-mqtt

Here a basic example to use the library for publishing:

    import paho.mqtt.client as mqtt

    client_id   = "pi_iot_1"
    mqtt_server_ip   = "192.168.1.84"
    mqtt_server_port = 1883
    mqtt_topic = "pi_iot_1/test"

    # Callback if CONNACK response from the server.
    def onConnect(client, userdata, flags, rc):
        print("Connected with result code " + str(rc))
        mqttc.subscribe(topic, 0)  # Subscribe (topic name, QoS)
    # Callback that is executed when we disconnect from the broker.
    def onDisconnect(client, userdata, message):
        print("Disconnected from the broker.")
    # Callback that is executed when subscribing to a topic
    def onSubscribe(client, userdata, mid, granted_qos):
        print('Subscribed on topic.')
    # Callback that is executed when unsubscribing to a topic
    def onUnsubscribe(client, userdata, mid, granted_qos):
        print('Unsubscribed on topic.')
    # Callback that is executed when a message is received.
    def onMessage(client, userdata, message):
        print("message received " ,str(message.payload.decode("utf-8")))
        print("message topic=",message.topic)
        print("message qos=",message.qos)
        print("message retain flag=",message.retain)

    mqttc = mqtt.Client(client_id=client_id, clean_session=True)
    mqttc.on_connect      = onConnect   # define the callback functions
    mqttc.on_disconnect   = onDisconnect
    mqttc.on_subscribe    = onSubscribe
    mqttc.on_unsubscribe  = onUnsubscribe
    mqttc.on_message      = onMessage
    mqttc.connect(mqtt_server_ip, mqtt_server_port, keepalive=60, bind_address="")
    mqttc.loop_start() # start loop to process callbacks! (new thread!)

    try:
        while (True):
            mqtt_message = "{\"say it\": \"Hello:\"}"
            mqttc.publish(mqtt_topic,mqtt_message)
            sleep(1)
    except KeyboardInterrupt:
        print("Keyboard interrupt by user")
"Just do it" Busses 9:

jdi_busses 9 json

Interesting links: