The goal of OpenDPS is to reflash the stock DPS5005 (and friends) with a firmware that has the same functionality, has a less cluttered user interface and is remote controllable via wifi or a serial port. The application needs to respond to user input both via buttons and the serial port and also via ADC readings telling us if the current draw is larger than what we allowed. Buttons and UART RX are handled using interrupts and obviously the ADC needs to be interrupt based too for fast response (and less smoke). Responding to user input and TFT drawing can be left to the application context.
The following modules are used in OpenDPS:
- event – uses a ring buffer for storing events from the interrupt context to be handled in the application context. Events are button presses, received bytes on the UART and over current protection triggers
- hw – the hardware abstraction (ADC, GPIO, …)
- ili9631c – the TFT driver
- opendps – main application
- past – for storing persistent parameters in flash
- protocol – helper for the serial protocol to instrument the OpenDPS device
- pwrctl – power control, the DAC and calculations to convert ADC readings to eg. current draw
- ringbuf – a ringbuffer implementation
- spi_driver – just that
- tft – TFT utility functions
- tick systick handler with 1ms resolution
- uframe – framing of serial protocol spoken
- ui – handle the user interface
The application in opendps.c sits in a busy loop waiting for events to arrive in the circular buffer. Button presses, serial RX and over current protection events are placed in this buffer. The application calls the UI module that updates the UI with fresh measurements every 250ms. User input is also handled in the UI module. Events are 16 bit integers encoding the event type and optional event data. For the UART RX event the data obviously is the received byte. For button presses we also include information telling us if the press was a long press.
ADC and DAC Management
As I did not have the schematics at hand I had to reverse engineer the formula to calculate voltage/current based on ADC readings. I did some measurements and plotted in a Google Docs spreadsheet to find out
- V_in(ADC1_IN8) – given a reading on channel 8, what is the input voltage?
- V_out(ADC1_IN9) – given a reading on channel 9, what is the output voltage?
- V_out(DAC) – what is the output voltage given a DAC setting? (and the inverse)
- I_out(ADC1_IN7) – given a reading on channel 7, what is the current draw? (and the inverse)
To reduce complexity in the ADC IRQ handler, I pre calculate the ADC value that corresponds to the dialled current limit to quickly determine if we went over the limit. Here I ran into some issues as the ADC reading of the current draw differed between the two ‘5005s I had at hand. One of the units reported the wrong current draw which was tracked down to a different value when no current was drawn at all. The remedy is to take the first 1000 samples at system startup to calculate the ADC value for 0.00mA current draw and compensate in the calculations (see adc_i_offset & friends in hw.c)
Additionally, I currently collect a number of “over current samples” before triggering the OCP. This scheme needs some investigation.
User settings need to be persisted and this is taken care of by the past (parameter storage) module. This module uses two blocks for storing data. One is the “current block” and when it gets full a garbage collection is performed and the data is copied to the other block. Counters tell which block is the most recent one. In theory, power loss should not lead to corruption and/or data loss.
An anti aliased rendering of Ubuntu Condensed is used in two different sizes for the UI. The font is found in gfx/fonts and consists of two files. The glyphs itself (eg. ubuntu_condensed_48.png) and one image describing the widths of each character (eg. ubuntu_condensed_48_width.png).
The widths are indicated by a white pixel in the first row:
The fonts are converted by ‘make fonts’ which invokes font-convert.py. The glyph PNG is converted to BGR565 (used by the TFT) and written to font-X.[h|c]. Each glyph is converted to a separate blob which speeds up blitting to the TFT. See tft_putch(…) in tft.c for usage. Blocking DMA is used to transfer the glyph data. One enhancement would be to have a list of DMA descriptors and fire the next DMA job in ISR context at the completion of the previous one. The icons displayed are handled in a similar manner (see eg. wifi.h) and are converted by ‘make graphics’. You should be able to change the font with little difficulty, in theory 🙂
The OpenDPS device can be controlled via the UART port and you can either connect an FTDI adapter or an ESP8266. The latter is the most fun. A simple serial protocol is used (see protocol.h and uframe.h). Either way you go, there is a Python script to talk to the OpenDPS device, dpsctl.py:
% dpsctl.py -h
usage: dpsctl.py [-h] [-d DEVICE] [-s] [-u VOLTAGE] [-i CURRENT] [-p POWER]
[-P] [-L] [-l] [-S] [-j] [-v]
Instrument an OpenDPS device
-h, --help show this help message and exit
-d DEVICE, --device DEVICE
OpenDPS device to connect to. Can be a /dev/tty device
or an IP number. If omitted, dpsctl.py will try the
environment variable DPSIF
-s, --scan Scan for OpenDPS wifi devices
-u VOLTAGE, --voltage VOLTAGE
Set voltage (millivolt)
-i CURRENT, --current CURRENT
Set maximum current (milliampere)
-p POWER, --power POWER
Power 'on' or 'off'
-P, --ping Ping device
-L, --lock Lock device keys
-l, --unlock Unlock device keys
-S, --status Read voltage/current settings and measurements
-j, --json Output status as JSON
-v, --verbose Verbose communications
The utility is agnostic as to how the OpenDPS device is connected. Provide an ip address and it will attempt to talk to that. Provide a TTY device and it will attempt to talk to that. For wifi connected DPS:es, you can provide the option –scan to find all OpenDPS devices on your network.
Any good old ESP8266 board with the UART exposed will work. Connect GND, RX and TX, build and flash esp8266-proxy (don’t forget to set your wifi credentials) and you should be good to go.
git clone https://github.com/kanflo/esp-open-rtos.git
git submodule init
git submodule update
git checkout -b netif remotes/origin/sdk_system_get_netif
echo '#define WIFI_SSID "my ssid"' > include/private_ssid_config.h
echo '#define WIFI_PASS "my secret password"' >> include/private_ssid_config.h
make && make flash
Note that you currently cannot use the master branch on the main ESP Open RTOS repository as my PR for a function needed for multicast has not been merged yet.
The design of esp8266-proxy is quite minimalistic. It receives UDP packets on port 5005, sends the content on the serial port and returns the answer to the UDP source address and port.
When OpenDPS starts the wifi icon is flashing at 1Hz. When it goes steady your ESP8266 has connected to your wifi and told the OpenDPS that it is connected. Try scanning for it:
% dpsctl.py --scan
1 OpenDPS device found
Next try pinging it:
% dpsctl.py -d 172.16.3.203 --ping
The TFT should flash once as a visual indication. If you get ‘Error: timeout talking to device 172.16.3.203’, check if you swapped RX and TX. You can always connect an FTDI adapter on either RX pin on the ESP8266 and OpenDPS to debug the communication.
That concludes part two, please see part three for a description of how to upgrade your DPS5005 to an OpenDPS 5005.