This is an old revision of the document!


Building a Battery Powered Smart Lamp with the Analog Discovery Pro (ADP3450/ADP3250)

In this guide, the processes of designing and building a battery-powered, Bluetooth-connected RGB lamp will be presented. The Analog Discovery Pro in Linux mode will be used to control the final project and to debug the external circuits during the testing stage. To make sure that you have the latest Linux image running on your device, follow this guide: How to Update or Recover Linux Mode on the Analog Discovery Pro (ADP3450/ADP3250)


Planning

Before starting designing the lamp, the goals of the project must be established. The central component of the lamp is an RGB LED which should be switched on/off via an application running on a phone. Communication with the phone can be resolved using Bluetooth or Bluetooth Low Energy, but the Low Energy variant (BLE) can be easier to use in some cases, due to the custom service characteristics (more on this later). The application should also display the ambient light conditions to notify the user to turn off the lamp if it isn't needed (automatic switching may be implemented if needed). To power the lamp, a battery can be used which can be charged from the Analog Discovery Pro (the lamp can't be directly powered from the ADP, as it doesn't provide enough current).

In this prototype, a 5 mm RGB LED will be used as the lamp, which is powered by an old phone battery, but the circuit can be scaled for higher power lamps if needed. Communication with the phone is resolved by the Pmod BLE (Bluetooth Low Energy) module and ambient light measurements are performed by the Pmod ALS (Ambient Light Sensor). The Pmod DA1 (Digital to Analog Converter) and the Pmod OD1 (Open Drain MOSFETs) will be used in the control circuit. A full inventory of the components needed is listed below.


Inventory

Hardware
Software

Note: WaveForms can be installed by following the WaveForms Getting Started Guide.


Creating a Hardware Abstraction Layer (HAL) for the Analog Discovery Pro

A Hardware Abstraction Layer (HAL) is a layer in programming that takes the functions controlling the hardware and makes them more abstract, by generalizing them. In this case, the hardware, the Analog Discovery Pro, is controlled using WaveForms SDK, from a Python script. WaveForms SDK requires a fair amount of stuff to happen to start using an instrument. The SDK uses the ctypes module and most instruments need several initialization functions (like dummy read/write functions in the case of some digital communication protocols) before using them. As the current project uses several instruments, some sort of module is needed to make the interaction with the hardware more straightforward. In the following, a Python module will be created, which is based on WaveForms SDK and works like an instrument driver for the Analog Discovery Pro. If you are not interested in the details of this module, skip to the next section. The library containing the modules can be downloaded here: waveforms_hal.zip.

In the attached package, not all the functions were tested, so errors might appear in some cases. Use the package responsibly and feel free to modify it.

Create a new folder and copy the dwfconstants.py file into it from WaveForms' installation path (usually C:\Program Files (x86)\Digilent\WaveFormsSDK\samples\py\dwfconstants.py). This file is needed as it contains all the constants needed for every instrument. Now create an initializer file and separate files for every instrument you want to use.

Wrapper: __init__.py

This file imports all the other modules and also gives a name to each one of them. Global initialization and cleanup functions are also implemented here.

""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
 
""" WRAPPER """
 
# import every submodule
from WaveForms_HAL import WaveForms_HAL_Device as device
 
from WaveForms_HAL import WaveForms_HAL_Supply as supply
 
from WaveForms_HAL import WaveForms_HAL_Scope as scope
from WaveForms_HAL import WaveForms_HAL_Wavegen as wavegen
 
from WaveForms_HAL import WaveForms_HAL_Logic as logic
from WaveForms_HAL import WaveForms_HAL_Pattern as pattern
from WaveForms_HAL import WaveForms_HAL_Pattern_static as static_pattern
from WaveForms_HAL import WaveForms_HAL_Static as digital
 
from WaveForms_HAL import WaveForms_HAL_SPI as spi
from WaveForms_HAL import WaveForms_HAL_UART as uart
 
 
"""-------------------------------------------------------------------"""
""" INITIALIZATION """
"""-------------------------------------------------------------------"""
 
 
# synchronize submodules
def initialize():
    # connect to the device
    if device.connected != True:
        device.open()
        # copy device handles
        supply.hdwf = device.hdwf
        scope.hdwf = device.hdwf
        wavegen.hdwf = device.hdwf
        logic.hdwf = device.hdwf
        pattern.hdwf = device.hdwf
        digital.hdwf = device.hdwf
        spi.hdwf = device.hdwf
        uart.hdwf = device.hdwf
    return
 
 
"""-------------------------------------------------------------------"""
""" CLEANUP """
"""-------------------------------------------------------------------"""
 
 
def close():
    # close the device
    if device.connected:
        # reset all instruments
        supply.reset()
        scope.reset()
        wavegen.reset()
        logic.reset()
        pattern.reset()
        static_pattern.reset()
        digital.reset()
        spi.reset()
        uart.reset()
 
        device.close()
    return
 
 
"""-------------------------------------------------------------------"""
General: WaveForms_HAL_Device.py

This file contains functions used for opening and closing the device and error checking. A flag showing the current connection status is also included here.

""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
 
""" MAIN FUNCTIONS """
 
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import sys
if sys.platform.startswith("win"):
    dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
# global variables
hdwf = c_int()  # device handle
connected = False   # the current state of the device
 
 
"""-------------------------------------------------------------------"""
""" ERROR CHECKING """
"""-------------------------------------------------------------------"""
 
 
def check_error():
    # check for errors
    errno = c_int()  # variable for error number
    dwf.FDwfGetLastError(errno)  # get error number
 
    # if there is an error
    if errno != dwfercNoErc:
        szerr = create_string_buffer(512)   # variable for the error message
        dwf.FDwfGetLastErrorMsg(szerr)  # get the error message
        print(str(szerr.value))  # print the error message
        quit()  # terminate the program
    return
 
 
"""-------------------------------------------------------------------"""
""" INITIALIZATION """
"""-------------------------------------------------------------------"""
 
 
def open():
    # open the first connected device
    dwf.FDwfDeviceOpen(c_int(-1), byref(hdwf))
    # if there is a problem
    if hdwf.value == hdwfNone.value:
        # check for errors
        check_error()
    connected = True    # set connection flag
    return
 
 
"""-------------------------------------------------------------------"""
""" CLEANUP """
"""-------------------------------------------------------------------"""
 
 
def close():
    dwf.FDwfDeviceClose(hdwf)   # close the device
    connected = False   # set connection flag
    return
 
 
"""-------------------------------------------------------------------"""
Logic Analyzer: WaveForms_HAL_Logic.py

This file contains functions controlling the logic analyzer. Besides the initialization and cleanup functions, the most important one is the receive_data() function, which reads the states of the digital I/O lines in a buffer according to the initialization parameters. The function accepts a parameter that specifies whether data specific to a digital I/O line should be selected from the dataset or not.

""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
 
""" DIGITAL INPUT FUNCTIONS """
 
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import numpy
import sys
if sys.platform.startswith("win"):
    dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
# global variables
hdwf = c_int()  # device handle
data_count = 8192   # buffer size
 
 
"""-------------------------------------------------------------------"""
""" INITIALIZATION """
"""-------------------------------------------------------------------"""
 
 
def initialize(base_frequency=100e06, buffer_size=8192):
    global data_count
    data_count = buffer_size
    # set internal clock frequency
    internal_frequency = c_double()
    dwf.FDwfDigitalInInternalClockInfo(hdwf, byref(internal_frequency))
    dwf.FDwfDigitalInDividerSet(hdwf, c_int(
        int(internal_frequency.value / base_frequency)))
    # set 16-bit sample format
    dwf.FDwfDigitalInSampleFormatSet(hdwf, c_int(16))
    # set buffer size
    dwf.FDwfDigitalInBufferSizeSet(hdwf, c_int(buffer_size))
    return
 
 
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
 
 
def reset():
    # reset the instrument
    dwf.FDwfDigitalInReset(hdwf)    # reset the logic analyzer
    return
 
 
"""-------------------------------------------------------------------"""
""" RECEIVE ALL DATA """
"""-------------------------------------------------------------------"""
 
 
def receive_data(channel=None):
    # begin acquisition
    dwf.FDwfDigitalInConfigure(hdwf, False, True)
    while True:
        status = c_byte()
        dwf.FDwfDigitalInStatus(hdwf, True, byref(status))
        if status.value == stsDone.value:
            # exit loop when finished
            break
 
    # get samples
    buffer = (c_uint16 * data_count)()
    dwf.FDwfDigitalInStatusData(hdwf, buffer, 2 * data_count)
    buffer = numpy.fromiter(buffer, dtype=numpy.uint16)
    buffer = buffer.tolist()
 
    # break out for every pin
    result = [[] for _ in range(16)]
    for data in buffer:
        for index in range(16):
            result[index].append(data & (1 << index))
 
    # return only what is necessary
    if channel != None:
        return result[channel]
    else:
        return result
 
 
"""-------------------------------------------------------------------"""
Pattern Generator: WaveForms_HAL_Pattern.py

The most important part of this file is the function that generates the required signal on the required channel according to the input parameters.

""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
 
""" DIGITAL OUTPUT FUNCTIONS """
 
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import WaveForms_HAL.WaveForms_HAL_Static as digital
import math
import sys
if sys.platform.startswith("win"):
    dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
# global variables
hdwf = c_int()  # device handle
 
# function types
 
 
class type:
    pulse = DwfDigitalOutTypePulse
    custom = DwfDigitalOutTypeCustom
    random = DwfDigitalOutTypeRandom
 
 
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
 
 
def reset():
    # reset the instrument
    dwf.FDwfDigitalOutReset(hdwf)    # reset the pattern generator
    return
 
 
"""-------------------------------------------------------------------"""
""" GENERATION """
"""-------------------------------------------------------------------"""
 
 
def generate(channel, frequency, function, duty_cycle=50, data=None):
    # reset pin
    digital.stop(channel)
 
    # get internal clock frequency
    internal_frequency = c_double()
    dwf.FDwfDigitalOutInternalClockInfo(hdwf, byref(internal_frequency))
    # get counter value range
    counter_limit = c_uint()
    dwf.FDwfDigitalOutCounterInfo(hdwf, c_int(0), 0, byref(counter_limit))
    # calculate the divider for the given frequency
    divider = int(math.ceil(internal_frequency.value /
                  frequency / counter_limit.value))
    # calculate counter steps to get the required frequency
    steps = int(round(internal_frequency.value / frequency / divider))
    # calculate steps for low and high parts of the period
    high_steps = int(steps * duty_cycle / 100)
    low_steps = int(steps - high_steps)
 
    # enable the respective channel
    dwf.FDwfDigitalOutEnableSet(hdwf, c_int(channel), c_int(1))
    # set output type
    dwf.FDwfDigitalOutTypeSet(hdwf, c_int(channel), function)
    # set frequency
    dwf.FDwfDigitalOutDividerSet(hdwf, c_int(channel), c_int(divider))
    if function == type.pulse:
        # set duty cycle
        dwf.FDwfDigitalOutCounterSet(hdwf, c_int(
            channel), c_int(low_steps), c_int(high_steps))
    elif function == type.custom:
        # format data
        buffer = (c_ubyte * ((len(data) + 7) >> 3))(0)
        for index in range(len(data)):
            if data[index] != 0:
                buffer[index >> 3] |= 1 << (index & 7)
        # load data
        dwf.FDwfDigitalOutDataSet(hdwf, c_int(
            channel), byref(buffer), c_int(len(data)))
 
    # start generating the signal
    dwf.FDwfDigitalOutConfigure(hdwf, c_int(1))
    return
 
 
"""-------------------------------------------------------------------"""
Static Pattern Generator: WaveForms_HAL_Pattern_static.py

Due to hardware limitations, if one instrument is initialized which uses a certain hardware part (like the digital I/O lines), all the other instruments which use the same hardware lose control over it, so, for example, the pattern generator, the UART master, and the SPI master instruments can't be used at the same time. However, the digital I/O lines still can be controlled in a static way, by using bitmasks.

To make use of this possibility, this library realizes the pattern generator functions using the static I/O instrument instead of the pattern generator. While this module can be used at the same time as other digital instruments, it has a very limited speed, which is not enough for the scope of this project.

The current version of this module uses multithreading to realize several operations in parallel (actually the processor just switches very fast between the tasks).

""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
 
""" DIGITAL OUTPUT FUNCTIONS """
 
# import necessary modules
import WaveForms_HAL.WaveForms_HAL_Static as digital
import time
import threading
import random
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
# list for processes
processes = [None, None, None, None,
             None, None, None, None,
             None, None, None, None,
             None, None, None, None]
 
 
# class containing all channel parameters
class channel_data:
    frequency = None
    duty_cycle = None
    signal_data = None
    function_type = None
    thread = None
    stop = None
 
 
# function types
class type:
    pulse = 0
    custom = 1
    random = 2
 
 
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
 
 
def reset():
    # stop all threads
    for index in range(16):
        stop(index)
 
    # reset the instrument
    digital.reset()    # reset the pattern generator
    return
 
 
"""-------------------------------------------------------------------"""
""" STOP GENERATION """
"""-------------------------------------------------------------------"""
 
 
def stop(channel):
    global processes
 
    # stop the generation
    if processes[channel] != None:
        processes[channel].stop = True
        processes[channel].thread.join()
    processes[channel] = None
 
    # reset the pin
    digital.stop(channel)
    return
 
 
"""-------------------------------------------------------------------"""
""" SET GENERATION DATA """
"""-------------------------------------------------------------------"""
 
 
def generate(channel, frequency, function, duty_cycle=None, data=None):
    global processes
 
    # stop running thread
    if processes[channel] != None:
        stop(channel)
 
    # set pin as output
    digital.set_output(channel)
 
    # save current channel data
    current_ch = channel_data()
    current_ch.frequency = frequency
    current_ch.duty_cycle = duty_cycle
    current_ch.signal_data = data
    current_ch.function_type = function
    current_ch.stop = False
    current_ch.thread = threading.Thread(target=channel_handler, args=(channel,))
 
    # update data table
    processes[channel] = current_ch
 
    # start thread
    processes[channel].thread.start()
 
    return
 
 
"""-------------------------------------------------------------------"""
""" CHANNEL HANDLER FUNCTION """
"""-------------------------------------------------------------------"""
 
 
def channel_handler(channel):
    global processes
 
    loop_index = 0
 
    # start looping
    while processes[channel].stop != True:
 
        # measure start time
        start_time = time.time()
 
        if processes[channel].function_type == type.pulse:
            # calculate waiting times
            period = 1 / processes[channel].frequency
            high_period = period * processes[channel].duty_cycle / 100
            low_period = period - high_period
            # generate low cycle
            digital.write(channel, False)
            # wait
            duration = time.time() - start_time
            if duration < low_period:
                time.sleep(low_period - duration)
            # record starting time
            start_time = time.time()
            # generate high cycle
            digital.write(channel, True)
            # wait
            duration = time.time() - start_time
            if duration < high_period:
                time.sleep(high_period - duration)
 
        elif processes[channel].function_type == type.custom:
            # generate custom signal
            data = processes[channel].signal_data
            digital.write(channel, data[loop_index % len(data)])
            # wait
            duration = time.time() - start_time
            period = 1 / processes[channel].frequency
            if duration < period:
                time.sleep(period - duration)
 
        else:
            # generate random signal
            digital.write(channel, random.choice([True, False]))
            # wait
            duration = time.time() - start_time
            period = 1 / processes[channel].frequency
            if duration < period:
                time.sleep(period - duration)
 
        # increment loop index
        loop_index += 1
    return
 
 
"""-------------------------------------------------------------------"""
Oscilloscope: WaveForms_HAL_Scope.py

This file contains functions that can command the oscilloscope to measure voltages on a specified channel or to fill a buffer with the recorded data point according to the settings with which the instrument was initialized.

""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
 
""" ANALOG INPUT FUNCTIONS """
 
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import numpy
import sys
if sys.platform.startswith("win"):
    dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
# global variables
hdwf = c_int()  # device handle
 
# triggering flag
trigger = False
# buffer size
data_count = 8192
 
# possible trigger options
 
 
class trig:
    # sources
    class src:
        no = (trigsrcNone, )
        scope_ch = [(trigsrcDetectorAnalogIn, 0), (trigsrcDetectorAnalogIn, 1),
                    (trigsrcDetectorAnalogIn, 2), (trigsrcDetectorAnalogIn, 3)]
        digital_ch = [(trigsrcDetectorDigitalIn, 0), (trigsrcDetectorDigitalIn, 1),
                      (trigsrcDetectorDigitalIn, 2), (trigsrcDetectorDigitalIn, 3),
                      (trigsrcDetectorDigitalIn, 4), (trigsrcDetectorDigitalIn, 5),
                      (trigsrcDetectorDigitalIn, 6), (trigsrcDetectorDigitalIn, 7),
                      (trigsrcDetectorDigitalIn, 8), (trigsrcDetectorDigitalIn, 9),
                      (trigsrcDetectorDigitalIn, 10), (trigsrcDetectorDigitalIn, 11),
                      (trigsrcDetectorDigitalIn, 12), (trigsrcDetectorDigitalIn, 13),
                      (trigsrcDetectorDigitalIn, 14), (trigsrcDetectorDigitalIn, 15)]
        external_ch = [(trigsrcExternal1, ), (trigsrcExternal2, ),
                       (trigsrcExternal3, ), (trigsrcExternal4, )]
    # types
 
    class type:
        edge = trigtypeEdge
        pulse = trigtypePulse
        transition = trigtypeTransition
    # edges
 
    class edge:
        rising = trigcondRisingPositive
        falling = trigcondFallingNegative
 
 
"""-------------------------------------------------------------------"""
""" INITIALIZATION """
"""-------------------------------------------------------------------"""
 
 
def initialize(offset=0, range=10, buffer_size=8192, frequency=20e06, trigger=False, trigger_src=trig.src.no, trigger_type=trig.type.edge, trigger_timeout=0, trigger_lvl=0, trigger_edge=trig.edge.rising):
    global data_count
    data_count = buffer_size
    # enable all channels
    dwf.FDwfAnalogInChannelEnableSet(hdwf, c_int(0), c_bool(True))
    # set offset voltage
    dwf.FDwfAnalogInChannelOffsetSet(hdwf, c_int(0), c_double(offset))
    # set range
    dwf.FDwfAnalogInChannelRangeSet(hdwf, c_int(0), c_double(range / 2))
    # set the buffer size
    dwf.FDwfAnalogInBufferSizeSet(hdwf, c_int(buffer_size))
    # set the acquisition frequency
    dwf.FDwfAnalogInFrequencySet(hdwf, c_double(frequency))
    # disable averaging
    dwf.FDwfAnalogInChannelFilterSet(hdwf, c_int(-1), filterDecimate)
 
    # set up triggering
    if trigger:
        # enable/disable auto triggering
        dwf.FDwfAnalogInTriggerAutoTimeoutSet(hdwf, c_double(trigger_timeout))
        # set trigger source
        dwf.FDwfAnalogInTriggerSourceSet(hdwf, trigger_src[0])
        if trigger_src[1] != None:
            dwf.FDwfAnalogInTriggerChannelSet(hdwf, c_int(trigger_src[1]))
        # set trigger type
        dwf.FDwfAnalogInTriggerTypeSet(hdwf, trigger_type)
        # set trigger level
        dwf.FDwfAnalogInTriggerLevelSet(hdwf, c_double(trigger_lvl))
        # set trigger edge
        dwf.FDwfAnalogInTriggerConditionSet(hdwf, trigger_edge)
    return
 
 
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
 
 
def reset():
    # reset the instrument
    dwf.FDwfAnalogInReset(hdwf)  # reset the oscilloscope
    return
 
 
"""-------------------------------------------------------------------"""
""" MEASURE VOLTAGE """
"""-------------------------------------------------------------------"""
 
 
def measure(channel):
    # set up the instrument
    dwf.FDwfAnalogInConfigure(hdwf, c_bool(False), c_bool(False))
    # read data to buffer
    dwf.FDwfAnalogInStatus(hdwf, False, None)
    # extract data from buffer
    voltage = c_double()
    dwf.FDwfAnalogInStatusSample(hdwf, c_int(channel - 1), byref(voltage))
    return voltage.value
 
 
"""-------------------------------------------------------------------"""
""" RECEIVE DATA """
"""-------------------------------------------------------------------"""
 
 
def receive(channel):
    # set up the instrument
    dwf.FDwfAnalogInConfigure(hdwf, c_bool(False), c_bool(True))
    # read data to buffer
    while True:
        status = c_byte()
        dwf.FDwfAnalogInStatus(hdwf, True, byref(status))
        if status.value == DwfStateDone.value:
            # exit loop when ready
            break
 
    # copy buffer
    buffer = (c_double * data_count)()
    dwf.FDwfAnalogInStatusData(hdwf, c_int(channel - 1), buffer, data_count)
 
    # convert list
    buffer = numpy.fromiter(buffer, dtype=numpy.float)
    buffer = buffer.tolist()
    return buffer
 
 
"""-------------------------------------------------------------------"""
Serial Peripheral Interface (SPI) Master: WaveForms_HAL_SPI.py

The Analog Discovery Pro can initiate SPI communication on any of the digital pins, after which it can exchange data with the connected slave device.

""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
 
""" SPI FUNCTIONS """
 
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import sys
if sys.platform.startswith("win"):
    dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
# global variables
hdwf = c_int()  # device handle
 
# endianness
 
 
class bit_order:
    MSB_first = 1
    LSB_first = 0
 
# pins
 
 
class pins:
    cs = None
    sck = None
    mosi = None
    miso = None
 
 
"""-------------------------------------------------------------------"""
""" SPI INITIALIZATION """
"""-------------------------------------------------------------------"""
 
 
def initialize(cs_pin, sck_pin, miso_pin=None, mosi_pin=None, frequency=1e06, mode=0, order=bit_order.MSB_first):
    # save pin numbers globally
    pins.cs = cs_pin
    pins.sck = sck_pin
    pins.mosi = mosi_pin
    pins.miso = miso_pin
 
    dwf.FDwfDigitalSpiFrequencySet(hdwf, c_double(
        frequency))   # set the clock frequency
    dwf.FDwfDigitalSpiClockSet(hdwf, c_int(sck_pin))    # set the clock pin
 
    if mosi_pin != None:
        dwf.FDwfDigitalSpiDataSet(hdwf, c_int(
            0), c_int(mosi_pin))  # set the mosi pin
        dwf.FDwfDigitalSpiIdleSet(hdwf, c_int(
            0), DwfDigitalOutIdleZet)  # set the initial state
    if miso_pin != None:
        dwf.FDwfDigitalSpiDataSet(hdwf, c_int(
            1), c_int(miso_pin))  # set the miso pin
        dwf.FDwfDigitalSpiIdleSet(hdwf, c_int(
            1), DwfDigitalOutIdleZet)  # set the initial state
 
    dwf.FDwfDigitalSpiModeSet(hdwf, c_int(mode))    # set the SPI mode
    dwf.FDwfDigitalSpiOrderSet(hdwf, c_int(order))  # set endianness
    dwf.FDwfDigitalSpiSelect(hdwf, c_int(
        cs_pin), c_int(1))  # set the cs pin HIGH
 
    dwf.FDwfDigitalSpiWriteOne(hdwf, c_int(
        1), c_int(0), c_int(0))  # dummy write
    return
 
 
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
 
 
def reset():
    # reset the instrument
    dwf.FDwfDigitalSpiReset(hdwf)   # reset the SPI interface
    return
 
 
"""-------------------------------------------------------------------"""
""" SPI SENDING """
"""-------------------------------------------------------------------"""
 
 
def send(data):
    # cast data
    if type(data) == int:
        data = "".join(chr(data))
    elif type(data) == list:
        data = "".join(chr(element) for element in data)
 
    # enable the chip select line
    dwf.FDwfDigitalSpiSelect(hdwf, c_int(pins.cs), c_int(0))
 
    # create buffer to write
    data = (c_ubyte * len(data))(*[c_ubyte(ord(character)) for character in data])
    dwf.FDwfDigitalSpiWrite(hdwf, c_int(1), c_int(8), data, c_int(
        len(data)))  # write array of 8 bit elements
 
    # disable the chip select line
    dwf.FDwfDigitalSpiSelect(hdwf, c_int(pins.cs), c_int(1))
    return
 
 
"""-------------------------------------------------------------------"""
""" SPI RECEIVING """
"""-------------------------------------------------------------------"""
 
 
def receive(count):
    # enable the chip select line
    dwf.FDwfDigitalSpiSelect(hdwf, c_int(pins.cs), c_int(0))
 
    # create buffer to store data
    buffer = (c_ubyte*count)()
    dwf.FDwfDigitalSpiRead(hdwf, c_int(1), c_int(8), buffer, c_int(
        len(buffer)))  # read array of 8 bit elements
 
    # disable the chip select line
    dwf.FDwfDigitalSpiSelect(hdwf, c_int(pins.cs), c_int(1))
 
    # decode data
    data = [str(bin(element))[2:] for element in buffer]
    data = [int(element, 2) for element in data]
    data = "".join(chr(element) for element in data)
 
    return data
 
 
"""-------------------------------------------------------------------"""
""" SPI EXCHANGE """
"""-------------------------------------------------------------------"""
 
 
def exchange(data, count):
    # cast data
    if type(data) == int:
        data = "".join(chr(data))
    elif type(data) == list:
        data = "".join(chr(element) for element in data)
 
    # enable the chip select line
    dwf.FDwfDigitalSpiSelect(hdwf, c_int(pins.cs), c_int(0))
 
    # create buffer to write
    tx_buff = (c_ubyte * len(data)).from_buffer_copy(data)
    # create buffer to store data
    rx_buff = (c_ubyte*count)()
 
    dwf.FDwfDigitalSpiWriteRead(hdwf, c_int(1), c_int(8), tx_buff, c_int(
        len(tx_buff)), rx_buff, c_int(len(rx_buff)))  # write to MOSI and read from MISO
 
    # decode data
    data = [str(bin(element))[2:] for element in rx_buff]
    data = [int(element, 2) for element in data]
    data = "".join(chr(element) for element in data)
 
    # disable the chip select line
    dwf.FDwfDigitalSpiSelect(hdwf, c_int(pins.cs), c_int(1))
    return data
 
 
"""-------------------------------------------------------------------"""
Static I/O: WaveForms_HAL_Static.py

This module can read/write the state of a digital I/O line and can set the line as input, or output.

""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
 
""" DIGITAL I/O FUNCTIONS """
 
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import sys
if sys.platform.startswith("win"):
    dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
# global variables
hdwf = c_int()  # device handle
 
 
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
 
 
def reset():
    # reset the instrument
    dwf.FDwfDigitalIOReset(hdwf)    # reset the digital I/O
    return
 
 
"""-------------------------------------------------------------------"""
""" RESET PIN """
"""-------------------------------------------------------------------"""
 
 
def stop(pin):
    # set pin to LOW
    write(pin, False)
    # disable static I/O
    set_output(pin, False)
    return
 
 
"""-------------------------------------------------------------------"""
""" READING A DIGITAL PIN """
"""-------------------------------------------------------------------"""
 
 
def read(pin):
    # read a digital pin
    # load internal buffer with current state of the pins
    dwf.FDwfDigitalIOStatus(hdwf)
    data = c_uint32()  # variable for this current state
    # get the current state of the pins
    dwf.FDwfDigitalIOInputStatus(hdwf, byref(data))
    # convert the state to a 16 character binary string
    data = list(bin(data.value)[2:].zfill(16))
 
    # check the required bit
    if data[15 - pin] != "0":
        # if it is one, return True (HIGH)
        return True
    else:
        # else return False (LOW)
        return False
 
 
"""-------------------------------------------------------------------"""
""" SETTING A DIGITAL PIN AS OUTPUT """
"""-------------------------------------------------------------------"""
 
 
def set_output(pin, state=True):
    # set a pin as output
    # load current state of the output enable buffer
    mask = c_uint16()
    dwf.FDwfDigitalIOOutputEnableGet(hdwf, byref(mask))
 
    # set bit mask
    mask = set_mask(pin, mask, state)
 
    # set the pin to output
    dwf.FDwfDigitalIOOutputEnableSet(hdwf, c_int(mask))
    return
 
 
"""-------------------------------------------------------------------"""
""" WRITING A DIGITAL PIN """
"""-------------------------------------------------------------------"""
 
 
def write(pin, state):
    # set a pin as output
    # load current state of the output state buffer
    mask = c_uint16()
    dwf.FDwfDigitalIOOutputGet(hdwf, byref(mask))
 
    # set bit mask
    mask = set_mask(pin, mask, state)
 
    # set the pin state
    dwf.FDwfDigitalIOOutputSet(hdwf, c_int(mask))
    return
 
 
"""-------------------------------------------------------------------"""
""" SET MASK """
"""-------------------------------------------------------------------"""
 
 
def set_mask(pin, mask, value):
    # convert to list
    mask = list(bin(mask.value)[2:].zfill(16))
 
    # set bit
    if value:
        mask[15 - pin] = "1"
    else:
        mask[15 - pin] = "0"
 
    # convert to number
    mask = "".join(element for element in mask)
    mask = int(mask, 2)
    return mask
 
 
"""-------------------------------------------------------------------"""
Supplies: WaveForms_HAL_Supply.py

This module can turn on/off the 3.3V power supply

""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
 
""" POWER SUPPLY FUNCTIONS """
 
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import sys
if sys.platform.startswith("win"):
    dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
# global variables
hdwf = c_int()  # device handle
 
 
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
 
 
def reset():
    # reset the instrument
    dwf.FDwfAnalogIOReset(hdwf)  # reset the power supplies
    return
 
 
"""-------------------------------------------------------------------"""
""" COMMANDING THE POWER SUPPLIES """
"""-------------------------------------------------------------------"""
 
 
def switch(state):
    # start/stop the power supplies
    # set the output voltage to 3.3V
    dwf.FDwfAnalogIOChannelNodeSet(hdwf, c_int(0), c_int(0), c_double(3.3))
    # start/stop the supplies
    dwf.FDwfAnalogIOEnableSet(hdwf, c_int(state))
    return
 
 
"""-------------------------------------------------------------------"""
Universal Asynchronous Receiver-Transmitter (UART) Master: WaveForms_HAL_UART.py

This module can be used to initialize UART communication on any of the digital I/O lines, then send/receive data on those lines.

""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
 
""" UART FUNCTIONS """
 
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import sys
if sys.platform.startswith("win"):
    dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
# global variables
hdwf = c_int()  # device handle
 
# possible parity settings
 
 
class parity:
    none = 0
    odd = 1
    even = 2
 
 
"""-------------------------------------------------------------------"""
""" UART INITIALIZATION """
"""-------------------------------------------------------------------"""
 
 
def initialize(rx_pin, tx_pin, baud_rate=9600, parity=parity.none, data_bits=8, stop_bits=1):
    dwf.FDwfDigitalUartRateSet(hdwf, c_double(baud_rate))  # set baud rate
    dwf.FDwfDigitalUartTxSet(hdwf, c_int(tx_pin))  # set tx pin
    dwf.FDwfDigitalUartRxSet(hdwf, c_int(rx_pin))  # set rx pin
    dwf.FDwfDigitalUartBitsSet(hdwf, c_int(data_bits))  # set data bit count
    # set parity bit requirements
    dwf.FDwfDigitalUartParitySet(hdwf, c_int(parity))
    dwf.FDwfDigitalUartStopSet(hdwf, c_double(stop_bits))  # set stop bit count
 
    # initialize tx with idle levels
    reset_tx()
    reset_rx()
    return
 
 
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
 
 
def reset():
    # reset the instrument
    dwf.FDwfDigitalUartReset(hdwf)   # reset the UART interface
    return
 
 
"""-------------------------------------------------------------------"""
""" RESET RX """
"""-------------------------------------------------------------------"""
 
 
def reset_rx():
    # initialize rx (dummy receive)
    dummy_buffer = c_int(0)
    dummy_parity_flag = c_int(0)
    dwf.FDwfDigitalUartRx(hdwf, None, c_int(0), byref(dummy_buffer), byref(
        dummy_parity_flag))
    return
 
 
"""-------------------------------------------------------------------"""
""" RESET TX """
"""-------------------------------------------------------------------"""
 
 
def reset_tx():
    # initialize tx with idle level (dummy send)
    dwf.FDwfDigitalUartTx(hdwf, None, c_int(0))
    return
 
 
"""-------------------------------------------------------------------"""
""" UART SENDING """
"""-------------------------------------------------------------------"""
 
 
def send(data):
    # cast data
    if type(data) == int:
        data = "".join(chr(data))
    elif type(data) == list:
        data = "".join(chr(element) for element in data)
 
    # encode the string into a string buffer
    data = create_string_buffer(data.encode("UTF-8"))
 
    # send text, trim zero ending
    dwf.FDwfDigitalUartTx(hdwf, data, c_int(sizeof(data)-1))
    return
 
 
"""-------------------------------------------------------------------"""
""" UART RECEIVING """
"""-------------------------------------------------------------------"""
 
 
def receive(chunk="", error="no error"):
    data = create_string_buffer(8193)   # create empty string buffer
    count = c_int(0)    # character counter
    parity_flag = c_int(0)    # parity check result
    dwf.FDwfDigitalUartRx(hdwf, data, c_int(
        sizeof(data)-1), byref(count), byref(parity_flag))  # read up to 8k characters
 
    if count.value > 0:
        data[count.value] = 0  # add zero ending
        # make a string from the string buffer
        data = list(data.value)
        data = "".join(chr(element) for element in data)
 
        # attach previous data
        data = chunk + data
 
        # decode parity check results
        if parity_flag.value == 0 and error == "no error":
            parity_flag = "no error"
        elif parity_flag.value < 0:
            parity_flag = "buffer overflow"
        elif parity_flag.value > 0:
            parity_flag = "parity error: {}".format(parity_flag.value)
        else:
            parity_flag = error
 
        # propagate previous results
        data, parity_flag = receive(chunk=data, error=parity_flag)
 
    else:
        data = chunk
        parity_flag = error
 
    return data, parity_flag
 
 
"""-------------------------------------------------------------------"""
Waveform Generator: WaveForms_HAL_Wavegen.py

This module can be used to generate analog signals with the required parameters. Possible functions are also present in the module as class members.

""" This module realizes communication with the Analog Discovery Pro using the WaveForms SDK"""
 
""" ANALOG OUTPUT FUNCTIONS """
 
# import necessary modules
from ctypes import *
from WaveForms_HAL.dwfconstants import *
import sys
if sys.platform.startswith("win"):
    dwf = cdll.dwf
elif sys.platform.startswith("darwin"):
    dwf = cdll.LoadLibrary("/Library/Frameworks/dwf.framework/dwf")
else:
    dwf = cdll.LoadLibrary("libdwf.so")
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
# global variables
hdwf = c_int()  # device handle
 
# function types
 
 
class type:
    custom = funcCustom
    sine = funcSine
    square = funcSquare
    triangle = funcTriangle
    noise = funcNoise
    dc = funcDC
    pulse = funcPulse
    trapezium = funcTrapezium
    sine_power = funcSinePower
 
    class ramp:
        up = funcRampUp
        down = funcRampDown
 
 
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
 
 
def reset():
    # reset the instrument
    dwf.FDwfAnalogOutReset(hdwf)  # reset the waveform generator
    return
 
 
"""-------------------------------------------------------------------"""
""" GENERATION """
"""-------------------------------------------------------------------"""
 
 
def generate(channel, function, amplitude, frequency=1e03, symmetry=50, offset=0, data=None, run_time=0, wait_time=0, repeat_time=0):
    # enable channel
    channel -= 1
    dwf.FDwfAnalogOutNodeEnableSet(
        hdwf, channel, AnalogOutNodeCarrier, c_bool(True))
    # set function type
    dwf.FDwfAnalogOutNodeFunctionSet(
        hdwf, channel, AnalogOutNodeCarrier, function)
    if function == type.custom:
        dwf.FDwfAnalogOutNodeDataSet(
            hdwf, channel, AnalogOutNodeCarrier, data, len(data))
    # set frequency
    dwf.FDwfAnalogOutNodeFrequencySet(
        hdwf, channel, AnalogOutNodeCarrier, c_double(frequency))
    # set amplitude
    dwf.FDwfAnalogOutNodeAmplitudeSet(
        hdwf, channel, AnalogOutNodeCarrier, c_double(amplitude))
    # set offset
    if function == type.dc:
        dwf.FDwfAnalogOutNodeOffsetSet(
            hdwf, channel, AnalogOutNodeCarrier, c_double(amplitude))
    else:
        dwf.FDwfAnalogOutNodeOffsetSet(
            hdwf, channel, AnalogOutNodeCarrier, c_double(offset))
    # set symmetry
    dwf.FDwfAnalogOutNodeSymmetrySet(
        hdwf, channel, AnalogOutNodeCarrier, c_double(symmetry))
 
    # set running time limit
    dwf.FDwfAnalogOutRunSet(hdwf, channel, c_double(run_time))
    # set wait time before start
    dwf.FDwfAnalogOutWaitSet(hdwf, channel, c_double(wait_time))
    # set number of repeating cycles
    dwf.FDwfAnalogOutRepeatSet(hdwf, channel, c_int(repeat_time))
 
    # start
    dwf.FDwfAnalogOutConfigure(hdwf, channel, c_bool(True))
    return
 
"""-------------------------------------------------------------------"""

Creating a Library for the Pmods

Now a library should be created to control the Pmods easily. These Pmod drivers will be created in a similar way to the HAL, but will use it to control the hardware. If you are not interested in the details of this module, skip to the next section. The library containing the modules can be downloaded here.

In the attached package, not all the functions were tested, so errors might appear in some cases. Use the package on your own responsibility and feel free to modify it.

Create a new folder and inside create an initializer file and separate files for every Pmod you want to use.

Wrapper: __init__.py

This module imports all submodules and names them

""" This module realizes communication with multiple PMODs """
 
""" WRAPPER """
 
from PMOD import PMOD_ALS as ALS
from PMOD import PMOD_BLE as BLE
from PMOD import PMOD_DA1 as DA1
from PMOD import PMOD_KYPD as KYPD
from PMOD import PMOD_OD1 as OD1
Pmod ALS: PMOD_ALS.py

The ambient light sensor uses the SPI interface for communication, with three lines (chip select, serial data out and serial clock) and a clock frequency between 1MHz and 4MHz. The on-board analog to digital converter returns 2 bytes of data, most significant bit first, which contain 3 leading and 4 trailing zeros. The sensor saturates at the output value 127. The module also converts the raw data into percentage.

""" This module realizes communication with the Pmod ALS """
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
class pins:
    cs = 0  # CS pin of the Pmod
    sdo = 1  # SDO pin of the Pmod
    sck = 2  # SCK pin of the Pmod
 
 
# WaveForms SDK HAL object
WF = None
 
 
"""-------------------------------------------------------------------"""
""" INITIALIZATION """
"""-------------------------------------------------------------------"""
 
 
def initialize(WaveForms_HAL):
    global WF
    WF = WaveForms_HAL
 
    # start the power supply
    WF.supply.switch(True)
 
    # initialize the SPI interface
    WF.spi.initialize(cs_pin=pins.cs, sck_pin=pins.sck,
                      miso_pin=pins.sdo, frequency=1e06)
    return
 
 
"""-------------------------------------------------------------------"""
""" CLEANUP """
"""-------------------------------------------------------------------"""
 
 
def close():
    pass
    return
 
 
"""-------------------------------------------------------------------"""
""" RECEIVE DATA """
"""-------------------------------------------------------------------"""
 
 
def receive_data():
    data = WF.spi.receive(2)   # read 2 bytes
    msb = ord(data[0]) & 0xFF
    lsb = ord(data[1]) & 0xFF
    # concatenate bytes without trailing and leading zeros
    result = ((msb << 3) | (lsb >> 4)) & 0xFF
    return result
 
 
"""-------------------------------------------------------------------"""
""" RECEIVE PERCENTAGE """
"""-------------------------------------------------------------------"""
 
 
def receive_percent():
    # receive and convert raw data
    data = receive_data() * 100 / 127
    return round(data, 2)
 
 
"""-------------------------------------------------------------------"""
Pmod BLE: PMOD_BLE.py

The Pmod BLE is a Bluetooth Low Energy module, which communicates on UART interface, using a 115200bps baud rate. The controlling module contains functions to factory reset the device by putting it into command mode and sending the necessary commands. Besides that, it also can send and receive data, decoding the received bytes and separating the buffer into a list which contains only data and one which contains only system messages (starting and ending with “%”).

""" This module realizes communication with the Pmod BLE """
 
import time
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
class pins:
    rx = 0  # TX pin of the Pmod
    tx = 1  # RX pin of the Pmod
    rst = 2  # RESET pin of the Pmod
 
 
class commands:
    command_mode = "$$$"    # enter command mode
    data_mode = "---\r"  # enter data mode
    rename = "S-,PmodBLE\r"  # set name to PmodBLE_XXXX
    factory_reset = "SF,1\r"    # factory reset
    high_power = "SGA,0\r"  # set high power output
    code = "SP,123456\r"    # set pin code to "123456"
    mode = "SS,C0\r"    # support device info + UART Transparent
    reboot = "R,1\r"    # reboots the device
 
 
# WaveForms SDK HAL object
WF = None
 
# variables for detecting system messages
currently_sys = False
previous_msg = ""
 
 
"""-------------------------------------------------------------------"""
""" INITIALIZATION """
"""-------------------------------------------------------------------"""
 
 
def initialize(WaveForms_HAL, first_run=False):
    global WF
    WF = WaveForms_HAL
 
    # start the power supply
    WF.supply.switch(True)
 
    # initialize the UART interface
    WF.uart.initialize(rx_pin=pins.tx, tx_pin=pins.rx, baud_rate=115200)
 
    # factory reset the Pmod
    if first_run:
        reset()
    return
 
 
"""-------------------------------------------------------------------"""
""" REBOOT """
"""-------------------------------------------------------------------"""
 
 
def reboot():
    # hard reset the device
    # pull down the reset line
    WF.digital.write(pins.rst, False)
    # wait
    time.sleep(1)
    # pull up the reset line
    WF.digital.write(pins.rst, True)
    return
 
 
"""-------------------------------------------------------------------"""
""" RESET """
"""-------------------------------------------------------------------"""
 
 
def reset():
    # enter command mode
    send_command(commands.command_mode)
    time.sleep(3)
 
    # factory reset the Pmod
    success = send_command(commands.factory_reset)
    while success != True:
        success = send_command(commands.factory_reset)
        time.sleep(1)
 
    # enter command mode
    send_command(commands.command_mode)
    time.sleep(3)
 
    # rename device
    send_command(commands.rename)
    time.sleep(1)
 
    # set high power mode
    send_command(commands.high_power)
    time.sleep(1)
 
    # set communication mode
    send_command(commands.mode)
    time.sleep(1)
 
    # exit command mode
    send_command(commands.data_mode)
    time.sleep(3)
    return
 
 
"""-------------------------------------------------------------------"""
""" CLEANUP """
"""-------------------------------------------------------------------"""
 
 
def close():
    # restart the module
    reboot()
    return
 
 
"""-------------------------------------------------------------------"""
""" SEND COMMAND """
"""-------------------------------------------------------------------"""
 
 
def send_command(command):
    # send the command
    send_data(command)
    # record response
    response, _, error = receive_data
    # analyze response
    response = response[0:3]
    if response == "ERR" or response == "Err" or error != "no error":
        return False
    return True
 
 
"""-------------------------------------------------------------------"""
""" SEND DATA """
"""-------------------------------------------------------------------"""
 
 
def send_data(data):
    # send data
    WF.uart.send(data)
    return
 
 
"""-------------------------------------------------------------------"""
""" RECEIVE DATA """
"""-------------------------------------------------------------------"""
 
 
def receive_data():
    global previous_msg, currently_sys
 
    # record incoming message
    data, error = WF.uart.receive()
    sys_msg = ""
 
    # check for system messages
    special = data.count("%")
 
    """---------------------------------"""
 
    if special == 0:
        if currently_sys:
            # the middle of a system message
            previous_msg = previous_msg + data
            data = ""
        else:
            # not a system message
            pass
 
        """---------------------------------"""
 
    elif special == 2:
        # clear system message
        sys_msg = data
        data = ""
        currently_sys = False
        previous_msg = ""
 
        """---------------------------------"""
 
    else:
        # fragmented system message
        data_list = data.split("%")
 
        if currently_sys:
            # the end of the message
            sys_msg = previous_msg + data_list[0] + "%"
            currently_sys = False
            previous_msg = ""
            data = data_list[1]
 
        else:
            # the start of the message
            currently_sys = True
            previous_msg = "%" + data_list[1]
            data = data_list[0]
 
    return data, sys_msg, error
 
 
"""-------------------------------------------------------------------"""
Pmod DA1: PMOD_DA1.py

The digital to analog converter also communicates via the SPI interface. The Pmod DA1 includes two DAC chips that have common chip select and serial clock pins. Both chips contain two 8-bit DAC channels, so in total, the Pmod features four different conversion channels. The program computes the 8-bit data word from the voltage, then appends it to the command word specific to the DAC channel. To use chip 1, or chip 2, the command and data words must be sent on the data 0, or data 1 lines.

""" This module realizes communication with the Pmod DA1 """
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
class pins:
    sync = 0  # CS pin of the Pmod
    d0 = 1  # data 0 pin of the Pmod
    d1 = 2  # data 0 pin of the Pmod
    sck = 3  # SCK pin of the Pmod
 
 
class output_channel:
    # channels of the digital to analog converter
    A1 = 0
    B1 = 1
    A2 = 2
    B2 = 3
 
 
# voltage limits in Volts
max_voltage = 3.3
min_voltage = 0
 
# WaveForms SDK HAL object
WF = None
 
 
"""-------------------------------------------------------------------"""
""" INITIALIZATION """
"""-------------------------------------------------------------------"""
 
 
def initialize(WaveForms_HAL):
    global WF
    WF = WaveForms_HAL
 
    # start the power supply
    WF.supply.switch(True)
 
    return
 
 
"""-------------------------------------------------------------------"""
""" CLEANUP """
"""-------------------------------------------------------------------"""
 
 
def close():
    pass
    return
 
 
"""-------------------------------------------------------------------"""
""" SEND VOLTAGE """
"""-------------------------------------------------------------------"""
 
 
def output_voltage(channel, voltage):
    # select DAC chip
    mosi = pins.d0
    if channel == output_channel.A2 or channel == output_channel.B2:
        mosi = pins.d1
 
    # initialize the SPI interface
    WF.spi.initialize(cs_pin=pins.sync, sck_pin=pins.sck,
                      mosi_pin=mosi, frequency=1e06)
 
    # limit and encode voltage
    voltage = max(min(voltage, max_voltage), min_voltage)
    data = round(voltage * 255 / 3.3) & 0xFF
 
    # select channel
    command = 3
    if channel == output_channel.B1 or channel == output_channel.B2:
        command = 7
 
    # send both words
    WF.spi.send([command, data])
    return
 
 
"""-------------------------------------------------------------------"""
Pmod KYPD: PMOD_KYPD.py

This module uses the static I/O instrument to read the current state of the keypad and returns the pressed keys.

""" This module realizes communication with the Pmod KYPD """
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
class pins:
    rows = [7, 6, 5, 4]    # pins connected to keypad rows
    columns = [3, 2, 1, 0]  # pins connected to keypad columns
 
 
# the layout of the keypad
keymap = [["1", "2", "3", "A"],
          ["4", "5", "6", "B"],
          ["7", "8", "9", "C"],
          ["0", "F", "E", "D"]]
 
 
# WaveForms SDK HAL object
WF = None
 
 
"""-------------------------------------------------------------------"""
""" INITIALIZATION """
"""-------------------------------------------------------------------"""
 
 
def initialize(WaveForms_HAL):
    global WF
    WF = WaveForms_HAL
 
    # enable the power supply
    WF.supply.switch(True)
 
    # for every pin connected to a row
    for pin in pins.rows:
        WF.digital.set_output(pin)  # set the pin as output
        WF.digital.write(pin, False)    # set a starting state of LOW
    return
 
 
"""-------------------------------------------------------------------"""
""" CLEANUP """
"""-------------------------------------------------------------------"""
 
 
def close():
    pass
    return
 
 
"""-------------------------------------------------------------------"""
""" GETTING THE KEYPAD STATE """
"""-------------------------------------------------------------------"""
 
 
def receive_data():
    # get the current state of the Pmod KYPD
    result = []  # buffer to store the results
 
    # go through all the rows
    for index_1 in range(len(pins.rows)):
        # set the state of every row
        for index_2 in range(len(pins.rows)):
            # only one row is HIGH at every moment
            if index_1 == index_2:
                # set the current row HIGH
                WF.digital.write(pins.rows[index_2], True)
            else:
                # set the current row LOW
                WF.digital.write(pins.rows[index_2], False)
 
        # check every column
        for index_2 in range(len(pins.columns)):
            # if the current column is HIGH
            if WF.digital.read(pins.columns[index_2]):
                # append the respective key to the results
                result.append(keymap[index_1][index_2])
 
    # return the results
    return result
 
 
"""-------------------------------------------------------------------"""
Pmod OD1: PMOD_OD1.py

The Pmod OD1 contains four open drain MOSFETs, which act like switches: if a gate pin is set HIGH (True), the respective MOSFET grounds the connected line. If the gate pin is set to LOW (False), the MOSFET input (the drain) has high impedance. The module realizes functions for turning the switches on/off, toggling and driving them with a pulse width modulated (PWM) signal. When driving the MOSFETs with the PWM signal, the type of signal generation (static, or pattern generator) can be defined as a parameter.

""" This module realizes communication with the Pmod ALS """
 
"""-------------------------------------------------------------------"""
""" VARIABLES """
"""-------------------------------------------------------------------"""
 
 
class pins:
    g1 = 0  # G1 pin of the Pmod
    g2 = 1  # G2 pin of the Pmod
    g3 = 2  # G3 pin of the Pmod
    g4 = 3  # G4 pin of the Pmod
 
 
# WaveForms SDK HAL object
WF = None
 
 
"""-------------------------------------------------------------------"""
""" INITIALIZATION """
"""-------------------------------------------------------------------"""
 
 
def initialize(WaveForms_HAL):
    global WF
    WF = WaveForms_HAL
 
    # start the power supply
    WF.supply.switch(True)
    return
 
 
"""-------------------------------------------------------------------"""
""" CLEANUP """
"""-------------------------------------------------------------------"""
 
 
def close():
    WF.static_pattern.reset()
    return
 
 
"""-------------------------------------------------------------------"""
""" SWITCH ON """
"""-------------------------------------------------------------------"""
 
 
def on(channel):
    pin = None
    if channel == 1:
        pin = pins.g1
    elif channel == 2:
        pin = pins.g2
    elif channel == 3:
        pin = pins.g3
    elif channel == 4:
        pin = pins.g4
 
    if pin != None:
        # initialize the pins
        WF.static_pattern.stop(pin)
        WF.digital.set_output(pin)
        # switch on MOSFET
        WF.digital.write(pin, True)
    return
 
 
"""-------------------------------------------------------------------"""
""" SWITCH OFF """
"""-------------------------------------------------------------------"""
 
 
def off(channel):
    pin = None
    if channel == 1:
        pin = pins.g1
    elif channel == 2:
        pin = pins.g2
    elif channel == 3:
        pin = pins.g3
    elif channel == 4:
        pin = pins.g4
 
    if pin != None:
        # initialize the pins
        WF.static_pattern.stop(pin)
        WF.digital.set_output(pin)
        # switch off MOSFET 4
        WF.digital.write(pin, False)
    return
 
 
"""-------------------------------------------------------------------"""
""" SWITCH """
"""-------------------------------------------------------------------"""
 
 
def switch(channel):
    pin = None
    if channel == 1:
        pin = pins.g1
    elif channel == 2:
        pin = pins.g2
    elif channel == 3:
        pin = pins.g3
    elif channel == 4:
        pin = pins.g4
 
    if pin != None:
        # initialize the pins
        WF.static_pattern.stop(pin)
        WF.digital.set_output(pin)
        # switch off MOSFET 4
        state = WF.digital.read(pin)
        WF.digital.write(pin, not state)
    return
 
 
"""-------------------------------------------------------------------"""
""" DRIVE MOSFET WITH PWM SIGNAL """
"""-------------------------------------------------------------------"""
 
 
def drive(channel, frequency, duty_cycle, static=False):
    # enable/disable running in the background
    instrument = WF.pattern
    if static:
        instrument = WF.static_pattern
 
    if channel == 1:
        # drive MOSFET 1
        instrument.generate(pins.g1, frequency,
                            instrument.type.pulse, duty_cycle=duty_cycle)
    elif channel == 2:
        # drive MOSFET 2
        instrument.generate(pins.g2, frequency,
                            instrument.type.pulse, duty_cycle=duty_cycle)
    elif channel == 3:
        # drive MOSFET 3
        instrument.generate(pins.g3, frequency,
                            instrument.type.pulse, duty_cycle=duty_cycle)
    elif channel == 4:
        # drive MOSFET 4
        instrument.generate(pins.g4, frequency,
                            instrument.type.pulse, duty_cycle=duty_cycle)
    return
 
 
"""-------------------------------------------------------------------"""

Testing the Modules

To make sure that the previously created Python modules work (and to provide examples for later usage), they should be tested. If you are not interested in the details of this testing, skip to the next section. The test scripts can be downloaded here.

Create test files for all Pmods and the Analog and Digital input-output instruments in the same folder, where the WaveForms_HAL and PMOD folders are.

Testing the Oscilloscope and the Waveform Generator: Test_HAL_Analog.py

To test the functionality of the analog input-output modules, first connect an oscilloscope channel to one of the waveform generator's channels, then output a DC voltage on the waveform generator and measure it with the scope. An AC signal should also be outputted and read back with the scope, then plotted using the matplotlib module.

# import modules
import WaveForms_HAL as WF
import matplotlib.pyplot as plt
 
# define used channels
scope_ch = 1
wavegen_ch = 1
 
# define signal parameters
dc_voltage = 2.25
sin_amplitude = 2.0
sin_frequency = 50e03
 
try:
    # initialize the interface
    WF.initialize()
 
    # initialize the scope with default settings
    WF.scope.initialize()
 
    # output a DC voltage
    WF.wavegen.generate(wavegen_ch, WF.wavegen.type.dc, dc_voltage)
    # display measured voltage level
    print(WF.scope.measure(scope_ch))
 
    # output a sine signal
    WF.wavegen.generate(wavegen_ch, WF.wavegen.type.sine,
                        amplitude=sin_amplitude, frequency=sin_frequency)
 
    while True:
        plt.plot(WF.scope.receive(scope_ch))    # plot recorded signal
        plt.show()
 
except KeyboardInterrupt:
    pass
finally:
    # close device
    WF.close()
Testing the Logic Analyzer and the Pattern Generator: Test_HAL_Digital.py

To test the functionality of the digital input-output modules, a PWM signal is generated on one of the digital input-output channels. The signal is read back on the same pin, then plotted using the matplotlib module.

# import modules
import WaveForms_HAL as WF
import matplotlib.pyplot as plt
 
# define used digital pin
pin = 1
 
# define signal parameters
frequency = 50e03
duty_cycle = 30
 
try:
    # initialize the interface
    WF.initialize()
 
    # initialize the logic analyzer with default settings
    WF.logic.initialize()
 
    # generate a signal
    WF.pattern.generate(
        pin, frequency, WF.pattern.type.pulse, duty_cycle=duty_cycle)
 
    while True:
        plt.plot(WF.logic.receive_data(pin))    # plot recorded signal
        plt.show()
 
except KeyboardInterrupt:
    pass
finally:
    # close the device
    WF.close()
Testing the Pmod ALS: Test_PMOD_ALS.py

To test the Pmod ALS, the ambient light intensity is read and displayed continuously.

# import modules
from PMOD import ALS
import WaveForms_HAL as WF
 
# define pins
ALS.pins.cs = 10
ALS.pins.sdo = 9
ALS.pins.sck = 8
 
try:
    # initialize the interface
    WF.initialize()
    ALS.initialize(WF)
 
    while True:
        # display measurements
        print(ALS.receive_percent())
 
except KeyboardInterrupt:
    pass
finally:
    # close the device
    ALS.close()
    WF.close()
Testing the Pmod BLE: Test_PMOD_BLE.py

To test the Pmod BLE, all messages received on Bluetooth are displayed, the string “got it” is sent as a response.

To be able to test the Pmod, download the BLE Scanner application to your phone. Run the Python script, then start the application. It will ask you to turn on Bluetooth and location. After some time, you should see the Pmod BLE appearing in the scanner.

Note the long code (MAC address) below the name of the device. It will be important later.

Connect to it, then open Custom Service and tap the N (notify) icon in the first custom characteristic. You should see system messages appearing in the output stream of the Python script. Also, the “got it” message should appear at the custom characteristic. To send data, tap the W icon and type in your message.

Note the long code (UUID) of the service and the characteristic you are using. It will be important later.

# import modules
from PMOD import BLE
import WaveForms_HAL as WF
 
# define pins
BLE.pins.tx = 6
BLE.pins.rx = 5
BLE.pins.rst = 4
 
try:
    # initialize the interface
    WF.initialize()
    BLE.initialize(WF)
    BLE.reboot()
 
    while True:
        # receive data
        data, sys_msg, error = BLE.receive_data()
 
        # merge data and system messages
        buf = ""
        if len(data) > 0:
            buf = data
        elif len(sys_msg) > 0:
            buf = sys_msg
 
        # if the data is valid
        if len(buf) > 0:
            print(buf)  # display it
            BLE.send_data(" got it")  # and send response
 
        # if the data isn't valid
        elif error != "no error":
            print(error)    # display the error
 
except KeyboardInterrupt:
    pass
finally:
    # close the device
    BLE.reboot()
    BLE.close()
    WF.close()
Testing the Pmod DA1: Test_PMOD_DA1.py

Connect the oscilloscope channels of the ADP to the output channels of the Pmod DA1, then run the script. Enter voltage levels for each DAC channel, then compare them with the ones measured by the oscilloscope. Enter an “x” to finish the program.

# import modules
from PMOD import DA1
import WaveForms_HAL as WF
 
# define used pins
DA1.pins.sync = 3
DA1.pins.d0 = 2
DA1.pins.d1 = 1
DA1.pins.sck = 0
 
try:
    # initialize the interface
    WF.initialize()
    DA1.initialize(WF)
 
    # initialize the scope with default settings
    WF.scope.initialize()
 
    while True:
        # read voltage from keyboard
        voltage = input("Set voltage for A1: ")
        # check exit condition
        if voltage == "x":
            break
        # set voltage
        DA1.output_voltage(DA1.output_channel.A1, float(voltage))
        # display measured voltage levels
        print([WF.scope.measure(1), WF.scope.measure(2), WF.scope.measure(3), WF.scope.measure(4)])
 
        # read voltage from keyboard
        voltage = input("Set voltage for B1: ")
        # check exit condition
        if voltage == "x":
            break
        # set voltage
        DA1.output_voltage(DA1.output_channel.B1, float(voltage))
        # display measured voltage levels
        print([WF.scope.measure(1), WF.scope.measure(2), WF.scope.measure(3), WF.scope.measure(4)])        
 
        # read voltage from keyboard
        voltage = input("Set voltage for A2: ")
        # check exit condition
        if voltage == "x":
            break
        # set voltage
        DA1.output_voltage(DA1.output_channel.A2, float(voltage))
        # display measured voltage levels
        print([WF.scope.measure(1), WF.scope.measure(2), WF.scope.measure(3), WF.scope.measure(4)])
 
        # read voltage from keyboard
        voltage = input("Set voltage for B2: ")
        # check exit condition
        if voltage == "x":
            break
        # set voltage
        DA1.output_voltage(DA1.output_channel.B2, float(voltage))
        # display measured voltage levels
        print([WF.scope.measure(1), WF.scope.measure(2), WF.scope.measure(3), WF.scope.measure(4)])
 
except KeyboardInterrupt:
    pass
finally:
    # close the device
    DA1.close()
    WF.close()
Testing the Pmod KYPD: Test_PMOD_KYPD.py

To test the keypad, read and display the pressed keys continuously.

# import modules
from PMOD import KYPD
import WaveForms_HAL as WF
 
# define pins
KYPD.pins.rows = [7, 6, 5, 4]
KYPD.pins.columns = [3, 2, 1, 0]
 
try:
    # initialize the interface
    WF.initialize()
    KYPD.initialize(WF)
 
    while True:
        # display pressed keys
        print(KYPD.receive_data())
 
except KeyboardInterrupt:
    pass
finally:
    # close the device
    KYPD.close()
    WF.close()
Testing the Pmod OD1: Test_PMOD_OD1.py

To test the open drain MOSFETs, connect the cathode (shorter lead) of some LEDs to the MOSFET drains. Connect one and of 470Ω resistors to the anodes (longer lead) of the LEDs. Connect the other lead of the resistors to the power supply output (red cable) on ADP.

The script turns on and off every LED one after the other, then repeats this step, but with the toggling function. When finished, enter duty cycles for every PWM signal commanding the MOSFETs to set the brightness of the LEDs. Finish the script by entering “x” instead of a duty cycle.

# import modules
from PMOD import OD1
import WaveForms_HAL as WF
import time
 
# define used pins
OD1.pins.g1 = 3
OD1.pins.g2 = 2
OD1.pins.g3 = 1
OD1.pins.g4 = 0
 
# define signal frequency
frequency = 1
 
# define PWM type
static_pwm = True
 
# require static testing
static_required = False
 
# define delay after operations
delay = 1   # in seconds
 
try:
    # initialize the interface
    WF.initialize()
    OD1.initialize(WF)
 
    if static_required:
        # try the on/off functions
        for index in range(1, 5):
            OD1.on(index)   # turn on MOSFET
            print("on")  # display message
            time.sleep(delay)   # wait
            OD1.off(index)  # turn off MOSFET
            print("off")  # display message
            time.sleep(delay)   # wait
 
        # try the toggling function
        for index in range(1, 5):
            OD1.switch(index)   # toggle MOSFET
            print("toggle")  # display message
            time.sleep(delay)   # wait
            OD1.switch(index)   # toggle MOSFET
            print("toggle")  # display message
            time.sleep(delay)   # wait
 
    while True:
        # read duty cycles from keyboard
        duty = input("First MOSFET duty cycle: ")
        # check exit condition
        if duty == "x":
            break
        # test the pwm drive functions
        OD1.drive(1, frequency, int(duty), static=static_pwm)
 
        duty = input("Second MOSFET duty cycle: ")
        if duty == "x":
            break
        OD1.drive(2, frequency, int(duty), static=static_pwm)
 
        duty = input("Third MOSFET duty cycle: ")
        if duty == "x":
            break
        OD1.drive(3, frequency, int(duty), static=static_pwm)
 
        duty = input("Fourth MOSFET duty cycle: ")
        if duty == "x":
            break
        OD1.drive(4, frequency, int(duty), static=static_pwm)
 
except KeyboardInterrupt:
    pass
finally:
    # turn off everything
    OD1.off(1)
    OD1.off(2)
    OD1.off(3)
    OD1.off(4)
 
    # close the device
    OD1.close()
    WF.close()

Building the Application

To build an Android application, the MIT App Inventor web application will be used. Open the site, create a new user, then start a new project. First, create the user interface of the application by drag and dropping user interface elements onto the virtual phone. Use the companion app on your real phone to see how the user interface will look. Alternatively, you can download and import the already created project file, or just download and install on your phone the final application.

For this application, a switch, three sliders, and several labels are needed. Use the Horizontal Arrangement and Vertical Arrangement blocks from the Layout menu to arrange everything on the screen.

When you are ready, find the Extension menu and import the Bluetooth Low Energy by MIT extension. Drag and drop a BLE component on the screen of the virtual phone.

When you are finished with the user interface, enter Block view. Here use the puzzle pieces to create the logic backbone of your application. Define what happens when the user touches a slider/switch/label, when the Bluetooth module connects/disconnects/receives a message. Define what data do you want to send and how to decode the received information. If you have never created an application before, this is a great way to start.

Use comments on the pieces (small blue circles with a question mark) to make your “code” easy to understand.

In this editor, every separate puzzle piece runs as an interrupt. Use this to your advantage.

Hiding a component and invisible components can be useful. You can use an invisible error message, which is made visible only if an error appears, then hide it again using a timer interrupt, to display connection problems - this can be extremely useful during debugging.

To easily access the BLE device, service and characteristic you need, use the MAC address, Service UUID and Characteristic UUID obtained previously. If you don't have those, you can find the addresses by following this step: Testing the Pmod BLE.

When you are ready, build the application and install it on your phone. To install it, you must enable installation from unknown sources. After installing the app, you might encounter warning messages from Google Play Protect, but just ignore them.


Hardware Setup

Connecting the PMODs

Connect the Pmod ALS, the Pmod BLE, the Pmod DA1, and one channel of the Pmod OD1 to the Analog Discovery Pro.

Building the PWM Generator

Connect three output channels of the Pmod DA1 to the inverting input of the comparators realized with the OP484 operational amplifier. To the non-inverting input, connect the second channel of the waveform generator. This circuit will create the PWM signals from the numerical data: the waveform generator outputs a sawtooth signal with 0.5V amplitude and 0.5V offset and this signal is compared to the voltage levels provided by the DAC so the rail-to-rail comparator generates logic levels according to the comparison result. The duty cycle of the resulting signal can be easily set: 0V on the DAC means 100% duty cycle, while 1V on the DAC means 0% duty cycle.

Connect the outputs of the comparators to the remaining MOSFET gates on the Pmod OD1.

To test the PWM generator circuit, you can connect an oscilloscope channel to the output of a comparator, then use WaveForms to generate the signal: first provide power to the Pmods by enabling the Supplies instrument, then generate the reference signal (sawtooth signal with 500mV offset and 500mV amplitude) on the second Wavegen channel. Use the SPI Master tool in the Protocol instrument to command the DAC and the Scope to display the generated signal. To generate the control voltages, use the command 0x03 to select channel A, or 0x07 to select channel B (the data line selects the DAC chip), then send the data word (a number between 0 - 0V and 77 - 0.99V). The duty cycle is set by the sent data word.

Building the Charger Circuit

Use the Micro-USB plug to provide power to the battery charger. You can connect the charger to the back panel of the ADP. The charger consists of a programmable current sink, realized with the LT3092. The reference voltage will be provided by the first waveform generator channel of the ADP. The current sink can charge up the Li-Po cell until it reaches 4.2V. From here the cell would need a constant voltage source with decreasing current to get the topping charge, but to keep the circuit simple, this part won't be implemented.

To prevent the negative lead of the battery from becoming floating, the first MOSFET is used in the Pmod OD1 to couple it to the ground, when the charger isn't plugged in. A charge signaling LED is also connected to the ADP to provide feedback.

To be able to measure the voltage of the battery cell, connect the first oscilloscope channel to the negative lead of the battery and the second channel to the positive lead. As the scope channels have a common reference, the difference between the two measurements will give the battery voltage.

Connecting the RGB LED

Finally, connect the RGB LED (and the current limiting resistors calculated to your LED) between the positive lead of the battery and the remaining three MOSFETs of the Pmod OD1. In this circuit, only RGB LEDs with separate anodes and cathodes, or with a common anode and separate cathodes can be used.


Software Setup

In the following, the structure of the main program file will be detailed. You can follow this guide to write your own control program based on the instructions given here, or you can download the source code here.

Importing the Modules and Defining Connections

To be able to use all the previously created modules, you must import them into your script.

To make your code more readable and easier to debug, it is good practice to name your connections (so you don't have to keep in mind which digital, or analog channel is used for what).

# import modules
from PMOD import DA1, BLE, ALS, OD1
import WaveForms_HAL as WF
import time
 
"""-------------------------------------------------------------------"""
 
# define connections
 
# PMOD DA1 pins
DA1.pins.sync = 3
DA1.pins.d0 = 2
DA1.pins.d1 = 1
DA1.pins.sck = 0
 
# Pmod OD1 pins
OD1.pins.g1 = 7
 
# PMOD BLE pins
BLE.pins.rst = 4
BLE.pins.rx = 5
BLE.pins.tx = 6
 
# PMOD ALS pins
ALS.pins.cs = 10
ALS.pins.sdo = 9
ALS.pins.sck = 8
 
# digital I/O
signal_LED = 11
 
# scope channels
battery_neg_ch = 1
battery_pos_ch = 2
 
# wavegen channels
current_reference = 1
pwm_reference = 2
 
# lamp color references
lamp_red = DA1.output_channel.A1
lamp_green = DA1.output_channel.B1
lamp_blue = DA1.output_channel.A2
 
# define low side switch (OD1 channel)
low_switch = 1

Global Variables and Auxiliary Functions

Define the global parameters of your project. If you initialize these variables at the start of your code, it will be easier to modify them during tuning the finished project.

Some auxiliary functions might be needed as well, which will be used later in the script.

# other parameters
light_referesh = 2  # delay between refreshing the light intensity (in seconds)
scope_average = 10  # how many measurements to average with the scope
light_average = 10  # how many measurements to average with the light sensor
battery_charged = 4.2   # battery charge level in volts
battery_discharged = 3.7    # battery discharge level in volts
max_charge_current = 0.1    # maximum charge current of the battery (in Amperes)
 
"""-------------------------------------------------------------------"""
 
 
def scale(value, source, destination):
    # scale a number from a range to another
    return ((value - source[0]) / (source[1] - source[0])) * (destination[1] - destination[0]) + destination[0]

The “body” of the script is inserted in a try-except structure. This structure runs the code sequence in the “try” block, and if an error (exception) occurs, handles it as defined in the “except” block. In this case the “except” block is empty (the script just finishes when the Ctrl+C combination is pressed), but it is followed by a “finally” block, which runs only once, when the try-except structure is exited. Here, the cleanup procedure can be executed, which will be discussed later.

Initialization

To be able to use the instruments, you must initialize the HAL created previously, as well as the modules controlling the PMODs. After everything is initialized, start generating the reference signals on the Wavegen channels, but turn off the lamp and the signaling LED. You can also make the battery ground floating, by turning off the MOSFET used as a low-side switch.

    # initialize the interface
    WF.initialize()
 
    # initialize the scope with default settings
    WF.scope.initialize()
 
    # initialize the PMODs
    DA1.initialize(WF)
    OD1.initialize(WF)
    ALS.initialize(WF)
    BLE.initialize(WF)
    BLE.reboot()
 
    """---------------------------------"""
 
    # start generating the reference signals
    WF.wavegen.generate(current_reference, WF.wavegen.type.dc, 0)
    WF.wavegen.generate(pwm_reference, WF.wavegen.type.ramp.up,
                        0.5, frequency=1e03, offset=0.5, symmetry=100)
 
    """---------------------------------"""
 
    # turn off the lamp
    DA1.output_voltage(lamp_red, 1)
    DA1.output_voltage(lamp_green, 1)
    DA1.output_voltage(lamp_blue, 1)
 
    # turn off the switch and the LED
    WF.digital.set_output(signal_LED)
    WF.digital.write(signal_LED, False)
    OD1.off(low_switch)

Endless Loop

The main part of the script is run in an endless loop, which can be exited only by a keyboard interrupt. However, there are some states, which must be kept unchanged between iterations, so certain flags and variables must be initialized before the loop.

    # variables to remember states
    start_time = time.time()
    lamp_on = False
    off_flag = True
    charger_connected = False

All other variables are temporal, they change in every iterations, so they can be defined in the loop.

        # temporal variables
        BT_flag = False
        battery_negative = 0
        battery_positive = 0
        light = 0

Receive Data on Bluetooth Low Energy

As a first step, read the messages from the PMOD BLE and save the connection status (also turn off the lamp if the module is disconnected). If the data is received without error, convert the received bytes to voltages, then set the lamp color, by setting the output values of the DAC channels.

        # read data from the Bluetooth module
        data, sys_msg, error = BLE.receive_data()
        data = list(data)
 
        # turn the lamp on/off
        if sys_msg.startswith("%CONNECT"):
            lamp_on = True
        elif sys_msg == "%DISCONNECT%":
            lamp_on = False
 
        # process the data
        if error == "no error" and len(data) >= 3:
            BT_flag = True
            data = [ord(element) for element in data]
            duty_red = scale(data[-3], [1, 255], [1, 0])
            duty_green = scale(data[-2], [1, 255], [1, 0])
            duty_blue = scale(data[-1], [1, 255], [1, 0])
 
        # set lamp color according to received data
        if lamp_on:
            if BT_flag:
                BT_flag = False
                off_flag = False
                DA1.output_voltage(lamp_red, duty_red)
                DA1.output_voltage(lamp_green, duty_green)
                DA1.output_voltage(lamp_blue, duty_blue)
        else:
            if not off_flag:
                off_flag = True
                DA1.output_voltage(lamp_red, 1)
                DA1.output_voltage(lamp_green, 1)
                DA1.output_voltage(lamp_blue, 1)

Measuring the Light Intensity

Light intensity is measured only after a predefined time interval. When it is needed, the PMOD ALS is used to measure the light intensity, then the result is encoded to a byte and sent using the Pmod BLE.

        # if the light intensity has to be measured
        if time.time() - start_time > light_referesh:
            # reinitialize the SPI interface
            ALS.initialize(WF)
 
            # measure the light intensity
            for _ in range(light_average):
                light += ALS.receive_percent()
            light /= light_average
 
            # encode the light intensity
            encoded_light = round(light * 2.55) & 0xFF
 
            # reinitialize the UART interface
            BLE.initialize(WF)
 
            # send light intensity
            BLE.send_data(encoded_light)
 
            start_time = time.time()

Charging the Battery

In every iteration of the main loop, measure the battery voltage. Also check, whether the charger is connected, or not and turn on the low-side switch, if the charger is disconnected, to ground the battery properly, but turn off the switch, when the charger is connected, to prevent any overvoltage on the battery cell. Set the charging current and turn the signaling LED on, or off according to the state of the charger circuit.

        # read battery voltage
        for _ in range(scope_average):
            battery_negative += WF.scope.measure(battery_neg_ch)
        battery_negative /= scope_average
        for _ in range(scope_average):
            battery_positive += WF.scope.measure(battery_pos_ch)
        battery_positive /= scope_average
        battery_voltage = battery_positive - battery_negative
 
        # decide if the charger is connected or not
        if battery_positive >= 4.3:
            charger_connected = True
            OD1.off(low_switch)
        else:
            charger_connected = False
            OD1.on(low_switch)
 
        # calculate charge current reference
        charge_current = max_charge_current * scale(battery_voltage, [battery_discharged, battery_charged], [0.5, 1])
        charge_current_ref = scale(charge_current, [0, max_charge_current], [0, 1])
 
        # start/stop charging
        if charger_connected and battery_voltage < battery_charged:
            WF.wavegen.generate(current_reference, WF.wavegen.type.dc, charge_current_ref)
            WF.digital.write(signal_LED, True)
        else:
            WF.wavegen.generate(current_reference, WF.wavegen.type.dc, 0)
            WF.digital.write(signal_LED, False)

Cleanup

When the script is finished, turn off the lamp and the charger, then close reset the used instruments and disconnect from the device.

    # turn off the lamp
    DA1.output_voltage(lamp_red, 1)
    DA1.output_voltage(lamp_green, 1)
    DA1.output_voltage(lamp_blue, 1)
 
    # turn off the LED and the switch
    WF.digital.write(signal_LED, False)
    OD1.off(low_switch)
 
    # close PMODs
    ALS.close()
    BLE.close()
    DA1.close()
    OD1.close()
 
    # close device
    WF.close()

Tuning and Testing

Start the script, then start the application on your phone. Wait until the phone discovers the PMOD BLE, then connect to it. Set the lamp color and luminosity on the sliders of the application.

If you want to modify the parameters of the script, stop it with Ctrl+C, modify the parameters, then restart the script. Averaging more measurements leads to a more stable result, with a slower update time.

Take care not to overcharge, or over-discharge the battery, or overload the power supply.


Next Steps

For more information on WaveForms SDK, see its Resource Center.

For technical support, please visit the Test and Measurement section of the Digilent Forums.