In my reporta project, I used a PyQt program to drive an FTDI adapter, producing a graphical display of the CPU’s internals: a real-time animation showing the I/O states, that doesn’t require any additional programming on the target system.
This post aims to produce a more powerful version, namely:
- Use a Raspberry Pi as the interface to the CPU (SWD or JTAG)
- Allow remote diagnosis, by using network communications
- Use standard web browser graphics in place of PyQt
There are many advantages to using the Web browser as a display tool; most importantly, there is no need to install extra software on the display system; you can even use your smartphone as a display device. Wireless communication between the data acquisition & display can be really useful when working on real-world industrial systems, which are often in cramped and inaccessible locations.
First we have to create the display graphic, and I’m using Scalable Vector Graphics (SVG). Since everything is drawn on-the-fly from 2-dimensional x,y positions, it automatically resizes from large to small screens, which is important for mobile devices.
Scalable Vector Graphics
In a previous post, I created some simple graphics in SVG; now I need to draw something that looks like my demonstration target system, with a pushbutton, seven-segment display and ‘blue pill’ STM32F103 CPU module:
My previous PyQt display looked like this:
..but we can do better than that! My first idea was to create the SVG graphics in Inkscape, then add Javascript code to animate them. The problem with this approach is the very large number of tools & settings in Inkscape; it is easy to create something that looks really good visually, but is extremely difficult (or impossible) to animate. So it is much easier to create the SVG from scratch using the Python ‘svgwrite’ library; the display elements can be structured so as to make animation easy.
Background
The background component is a solderless breadboard, with holes at 0.1 inch pitch. This can be created in SVG using a ‘pattern’:
import svgwrite PIN_PITCH = 10 PIN_SIZE = 2 BB_SIZE = PIN_PITCH*31, PIN_PITCH*11 TILE_SIZE = PIN_PITCH, PIN_PITCH TILE_CENTRE = PIN_PITCH/2.0,PIN_PITCH/2.0 # Create maximised SVG drawing def create_svg(fname, size): return svgwrite.Drawing(fname, width="100%", height="100%", viewBox=("0 0 %u %u" % size), debug=False) # Add a breadboard background pattern def add_breadboard(dwg, pos=(XPAD,TPAD), size=BB_SIZE): dots = svgwrite.pattern.Pattern(width=TILE_SIZE[0], height=TILE_SIZE[1], id="dots", patternUnits="userSpaceOnUse") dots.add(dwg.rect((0,0), TILE_SIZE, fill="#f0f0f0")) dots.add(dwg.circle(TILE_CENTRE, 1, fill="white")) dwg.defs.add(dots) dwg.add(dwg.rect(pos, size, stroke="darkgray", fill="url(#dots)", filter="url(#shadow)")) dwg = create_svg(FNAME, DWG_SIZE) add_breadboard(dwg)
A single light grey tile of 10 x 10 units is defined, with a small white dot in the middle. This is used to fill a full-size rectangle; the SVG interpreter automatically duplicates the tile to fill the area.
You may wonder why I have chosen to make the holes 10 units apart, instead of redefining the SVG coordinate system so they are 0.1 units apart, to match the real-world value. The reason is that I’ve found the 10-unit convention to be much more convenient, as it allows positioning to be done with integer values, and the default line width of 1 unit looks fine, so doesn’t need to be modified.
CPU module
A blue rectangle is created, with red ‘pins’ that can be animated to show the I/O on/off status. A list is used to define the pins and their I/O functions:
# STM32F103 'blue pill' board pinout, starting top left BOARD_PINS=("GND", "GND", "3V3", "NRST","PB11","PB10","PB1", "PB0", "PA7", "PA6", "PA5", "PA4", "PA3", "PA2", "PA1", "PA0", "PC15","PC14","PC13","VBAT", "PB12","PB13","PB14","PB15","PA8", "PA9", "PA10","PA11","PA12","PA15", "PB3", "PB4", "PB5", "PB6", "PB7", "PB8", "PB9", "5V", "GND", "3V3")
CSS styles are used to define the box colour and pin size. The pin text is also defined, using a ‘writing mode’ of top-to-bottom, which produces the vertical labels.
STYLES = """ .cpu_style {stroke:darkblue; stroke-width:1; fill:#b0c0e0} .pin_style {stroke:red; stroke-width:1; fill:red} .pin_text {font-size:6px; writing-mode:tb; font-family:Arial} """ dwg.defs.add(dwg.style(STYLES))
It is then just a question of iterating across the pins, drawing them and the optional text labels; these are optional so the same code can be used to draw the (unlabeled) seven-segment display pins.
# Add a dual-in-line part def add_dil_part(dwg, pos, row_pitch, idents, label=False, style="part_style"): g = Group(transform="translate"+str(pos), class_="pin_text") row_pins = len(idents) / 2 g.add(dwg.rect((0,0), (row_pins*PIN_PITCH, row_pitch), class_=style)) for n, ident in enumerate(idents): pos = pin_pos(n, row_pins, PIN_PITCH/2, (PIN_PITCH/2, row_pitch-PIN_PITCH/2)) g.add(dwg.circle(pos, PIN_SIZE/2, class_="pin_style", id=ident)) if label: pos = pin_pos(n, row_pins, PIN_PITCH/2, (PIN_PITCH, row_pitch-PIN_PITCH*2.5)) g.add(svgwrite.text.Text(ident, pos)) dwg.add(g) add_dil_part(dwg, (100,30), PIN_PITCH*7, BOARD_PINS, True, "cpu_style")
A ‘group’ is used to house the complete part, so it can be styled and positioned as a single item.
Seven-segment display
The same dual-in-line code is used to draw the component base; the pins aren’t actually visible in the real part, but have been included as a handy on/off status indication.
The display segments are drawn using a list of points, arranged so that the line drawing is sequential; this is transformed by the ‘zip’ function into a list of start & end points for each line:
# Dimensions of 7-seg display D7W,D7H,D7L = 20,20,2 # X and Y seg length, and X-direction lean # Segment endpoints in the order FABCDEG (for continous drawing) SEG_LINES = ((D7L, D7H), (D7L*2,0),(D7L*2+D7W,0),(D7L+D7W,D7H), (D7W,D7H*2),(0,D7H*2),(D7L,D7H), (D7L+D7W,D7H)) # Idents for the display pins, starting top left DISP_PINS = ("PB11","PB10","GND", "PB1", "PB0", "PB12","PB13","GND", "PB14","PB15") # Idents for the segments, in order ABCDEFGH SEG_PINS = ("PB1", "PB0", "PB14","PB13", "PB12","PB10","PB11","PB15") STYLES = """ .seg_stroke {stroke:#00a000; stroke-width:5; stroke-linecap:round} """ # Add 7 display segments def add_disp_segs(dwg, pos): g = Group(transform="translate"+str(pos), class_="seg_stroke") lines = zip(SEG_LINES[:-1], SEG_LINES[1:]) for n, line in enumerate(lines): g.add(dwg.line(*line, id=SEG_PINS[n])) dwg.add(g)
Pushbutton
A simple square-plus circle gives an approximation to the real button. The square has slightly rounded corners, using the ‘rx’ parameter.
PB_SIZE = 20 # Add a pushbutton def add_pb(dwg, pos, ident, size=PB_SIZE, fill="darkred"): g = Group(transform="translate"+str(pos)) g.add(dwg.rect((0,0), (size,size), rx=2, fill=fill, opacity=0.8)) g.add(dwg.circle((size/2,size/2), size/2, fill=fill, id=ident)) dwg.add(g)
Drop shadow
Adding a drop shadow to a component is a simple way of creating a 3-dimensional effect.
Confusingly, there are two ways this effect can be achieved; using a CSS definition, or an SVG filter. The CSS method is simpler (since the CSS functionality is a subset of the SVG functionality) but doesn’t work on all browsers, so I’ve used the SVG method instead.
The filter definition consists of a series of steps, with an input and an output; the steps I’ve used are:
- Get the alpha (i.e. the monochrome) values of the image, and offset by 2 units
- Add Gaussian blur to the offset image
- Combine the original image with the offset & blurred image
# Define a shadow filter def define_shadow(dwg): f = dwg.defs.add(dwg.filter(id="shadow", x=0, y=0, width="150%", height="150%")) f.feOffset(in_="SourceAlpha", result="AlphaOset", dx="2", dy="2") f.feGaussianBlur(in_="AlphaOset", result="AlphaBlur", stdDeviation=2) f.feBlend(in_="SourceGraphic", in2="AlphaBlur", mode="normal") # Add filter to a rectangle, e.g. for the breadboard: dwg.add(dwg.rect(pos, size, stroke="darkgray", fill="url(#dots)", filter="url(#shadow)"))
Note the appending of an underscore to ‘in’. This is necessary to avoid a Python syntax error; it is stripped off when the SVG output file is written
Control button, and text display
We need some method of controlling the data connection between the browser and Web server, also displaying the current status. This is achieved by adding an area at the bottom of the graphic.
The ‘connect’ button is drawn as a group, containing a rounded-corner rectangle, and text.
CTRL_SIZE = 60,19 STYLES = """ .ctrl_style {stroke:black; stroke-width:0.5; font-size:9px; font-family:Arial; text-anchor:middle} """ # Add a pushbutton control def add_ctrl_button(dwg, pos, ident, text, onclick, size=CTRL_SIZE, fill="palegreen"): g = Group(transform="translate"+str(pos), onclick=onclick, class_="ctrl_style") g.add(dwg.rect((0,0), size, rx=5, fill=fill)) g.add(svgwrite.text.Text(text, (size[0]/2,12), fill="black", id=ident)) dwg.add(g) add_ctrl_button(dwg, (20,133), "button1", "Connect", "click_handler()")
The ‘onclick’ parameter will trigger the given JavaScript function when the button is clicked, e.g.
var connected=0; function click_handler() { if (connected) disconnect(); else connect(); }
The status display consists of 2 lines of text; there is no need for scrolling, so the lines are tagged individually:
TEXTBOX_SIZE = 200,20 STYLES = """ .textbox_style {stroke-width:1.0; stroke:lightgray; fill:none} .text_style {font-size:8px; font-family:Courier} """ # Add a text area def add_textbox(dwg, pos, size=TEXTBOX_SIZE): g = Group(transform="translate"+str(pos)) g.add(dwg.rect((0,0), size, class_="textbox_style")) g.add(svgwrite.text.Text("Line1", (5,8), class_="text_style", id="text1")) g.add(svgwrite.text.Text("Line2", (5,17), class_="text_style", id="text2")) dwg.add(g)
Updating the text in Javascript just requires the ‘textContent’ to be set, e.g.:
// Connect to host function connect() { text2.textContent = "Connected"; button1.textContent = "Disconnect"; connected = 1; } // Disconnect from host function disconnect() { text2.textContent = "Disconnected"; button1.textContent = "Connect"; connected = 0; }
To be concluded…
The next blog will describe how Raspberry Pi OpenOCD data is used to animate the graphics. It will include a link to the full source code.
Copyright (c) Jeremy P Bentham 2019. Please credit this blog if you use the information or software in it.