In Issue 25, we built the Serial to SPI Programmer. This month, we connect the programmer to displays and go further in depth on how to program microcontrollers.
Part two of our Serial to SPI converter demystifies the SPI protocol and gives you the tools to tackle new peripherals from scratch.
SPI is a complicated protocol that usually requires specialist knowledge, high-level libraries and module familiarity with the chip being talked to. Working at it from scratch can be daunting so we spend time this month going in-depth to give you a better understanding.
THE BROAD OVERVIEW
Connecting a display to a microcontroller for the first time can be one of the most difficult activities in any project. Thirty years ago, I was handed such a task as my first assignment, fresh out of high school and into a R&D lab designing video game support hardware. On my first day, I was handed a prototype of a dot-matrix LCD display, manufactured by Seiko, which I had been asked to test and to get working in four-bit mode. My new boss had been saving up such a task for the new guy, with the specific intent of making me fail as a lesson.
I failed, of course. Then the entire department worked on the project to prove we were a team and that’s how to solve problems – as a team. There may be no I in TEAM, but when the project still didn’t work after two more weeks, the TEAM found a U, as in, “It’s your problem, YOU fix it”. It would take three more days before I finally figured out why it never worked to begin with and, after nearly a month, got it working reliably. Then I sent in all the erratum I had located in the datasheet to the manufacturer demonstrating a serious flaw in their control logic; why four-bit mode would never work reliably with those displays.
I never saw four-bit mode again in any other display, perhaps because of that. Yet, the desire for fewer pins, even thirty years later, drives another four-pin protocol; SPI. And the displays today are just as diabolically devious when starting from scratch as they were thirty years ago. When facing a new display for the first time, I remembered the lessons from the past and began by building a tool to help me test SPI devices directly from my PC.
So this article’s about using the SPIEL MCU circuit that was built in Issue 25, to encapsulate SPI data in a normal ASYNC port serial stream and control displays and other devices from a normal PC via the serial port. Last month’s circuit was an SPI programmer that let us build the SPIEL chip. This month, I’ll cover using the SPIEL chip to program other chips, write to displays and read from SPI peripherals or even operate an entire SPI bus.
WHAT IS SPIEL?
SPIEL stands for SPI Serial Peripheral Interconnect Encapsulation Link. The SPI serial standard is synchronous while the PC uses Asynchronous ports, and lacks control lines to run a SPI device. SPI also operates at much higher speeds than an asynchronous circuit.
The SPIEL chip takes an asynchronous data stream in human-readable form, and converts this to SPI data that can control a SPI bus in a way that is as easy as typing commands directly into a serial terminal. This allows us to encapsulate SPI control data in a normal asynchronous serial stream.
This project is useful if you want to work with SPI and need a good tool to assist, or you are working with new SPI devices for the first time. It is also ideal if you want to connect something like a SPI display up to a PC and run it to display things from your program outside of the main screen. For example, on a pin pad, keyboard or even the case itself to let you know things like temperature or other internal metrics.
It will also demystify SPI to the point that it no longer seems like the monster it typically is. After all, SPI is a defacto standard – no one wrote it, and every manufacturer creates their own version. Navigating the differences genuinely is a daunting challenge.
This Project is Useful For:
- Connecting an external small display to your PC to display in-program data offscreen
- Figuring out how to initialise SPI devices (Probably the best tool to do this!)
- Learning Assembly Language (Machine Code)
- Talking to SPI Peripherals
- Learning about SPI displays or to program them
- Testing your own SPI initialisation strings before putting them in embedded code
- Making your own advanced SPI Programmer
SERIAL TO SPI PROGRAMMER
Last month, we built a programmer to allow us to program the ATMEL AT89LP2052 or AT89LP4052 processor, also called a microcontroller or MCU. This microcontroller has a dedicated serial port and SPI interface included, and runs at about 11 MIPS with an 11MHz oscillator. Although I won’t include the code itself here, you can download the code and an assembled programmer .HEX file from the resources section on the DIYODE website.
Once programmed, the three jumpers in the centre of the board should be removed, and the jumper on the side of the MCU can be set either up to allow the board to be programmed externally by another programmer, or down to allow it to program another device externally. For this project, set the jumper down, so that the MCU can control the reset line directly, rather than be reset by it.
We’ll be using the 10 pin DIL (Dual In-Line) header to program other devices now, including displays, sensors and other devices, so set the RESET jumper up, so we can control it. On connecting this board to your PC, you should be able to open a serial terminal screen to it, and you’ll see the following displayed;
SPIEL Online - CC-BY-SA 2017 V1.01.11 David Kitson - ? For help...
Hit the RESET button if nothing comes up, as it will reset the MCU and cause it to send the opening welcome to the serial terminal.
If you don’t have a serial terminal program, download one from the Internet, such as PuTTY or KiTTY. Both of these are professional serial terminals that support Telnet, SSH and Serial terminal modes.
Once the SPIEL circuit fires up, you can press “?” to see a short interactive menu reminding you of all of the keys you can press or use to control the signals on the SPI Programming interface.
At this point, you can access the SPIEL circuit made last month. If you get this far, then it should be possible to control a display directly from your PC via the Serial Terminal program, with nothing more complicated than cutting and pasting a text document into the terminal window.
ASSEMBLING TO ASSEMBLY CODE
Before we get into running the SPIEL circuit though, I’ll explain a little about the program itself.
This project is written in Assembly Language. Another name for this is Machine Code, though most assembly programmers differentiate between the two by calling it Machine Code if it’s displayed as numbers and Assembly Code if it’s displayed as Mnemonics. Fundamentally though, both are the same, except one is a little easier to read than the other, and very few people program in pure machine code since it’s just numbers, although most assembly programmers can do it when necessary.
Machine code is read by the MCU one instruction at a time. It doesn’t need to be compiled, outside of assembly, and it operates directly on the logic inside of the processor. It’s literally the fastest that a program can run, and if programmed well, it’s very efficient.
Little known fact – Bill Gates was an Assembly Programmer. That’s what got him started and gave Microsoft its first contract.
Assembly code does away with concepts like variables and strings. It doesn’t know how to do complicated maths and it can’t do complex things, but it does simple things such as adding two numbers, shifting bits and moving memory around very quickly. It looks something like this.
Imagine that we want to establish a variable, give it a value, and add something to it. It might look something like this in High-level Code, along with a type declaration depending on the language.
This is a simple command in BASIC but its just like pseudocode and there’s a version of that for every high-level language. As the language complexity increases, you get a lot more data, and you start having to think about what kind of number NUMBER is. Then you need call routines to the mathematics stack of the application, and then send the addition command to the stack, and once the number is retrieved, store it in the place referenced by the variable. Also, the code itself might compile to several kilobytes – even megabytes.
In Assembly, it looks something like this;
NUMBER DATA 010H
;Let’s store number in memory location 10
*Note – This line takes no space – It’s just an assembler instruction.</p>
MOV NUMBER,5 ;set the variable NUMBER to 5
MOV A,20 ;set accumulator A to 20
MOV A,NUMBER ; add 5 to 20
MOV NUMBER,A ;set variable NUMBER to the answer.</div>
That works out to about 8 bytes of code. Just 8 bytes.
These four instructions are probably something on the order of a hundred times faster than most high-level code might perform the same task. If you want to start programming in Assembly Language, then you need to understand binary about as well as you understand decimal or hexadecimal. For complicated projects, you should use a high-level language, but for embedded controllers, Assembly is just fine.
Once upon a time, all high-speed code was programmed in Assembly, but today people just use languages like C++ and modern processors are thousands of times faster than the processors of the 80’s, but microcontrollers were often architectures of that era, so they benefit from some tight assembly code.
If you want to download the source code and read it, it is commented line-by-line. Variable names are listed for each memory location, and bits are named also, so if you read the comments, it reads like pseudocode. The source can be downloaded from the resources section on the website.
The main routine starts off by initialising and setting the serial port and baud rate up, as well as setting the output ports to inputs so as not to interfere with anything. After that, it sets a pointer to the intialisation message in memory, flags the Transmit Interrupt, and then just loops forever. Literally forever. The main routine for this application does absolutely nothing after startup – It’s only job is to set up the Interrupts for the serial port hardware inside the MCU.
That’s because it has an interrupt running from the serial ports, which drive everything. Once enabled, any activity on either the SPI or ASYNC ports will generate an interrupt, as will any conclusion of a transmission. As such, all other routines run under interrupt, and once a serial port is ready for something else to happen, it forks out of the infinite loop and into the serial communications handling routines.
Interrupts as a primary code controller are fairly common in embedded architectures. Rather than have the main program poll for a certain situation and react to it, it receives an interrupt whenever certain hardware conditions are met, such as receiving a byte via the Async port, or when a byte is sent.
This then drives a basic multitasking environment, in which each application must do what it has to and then exit immediately, so another routine can have processor time. This is a very efficient form of coding, and results in very high reliability for the application when written correctly. Imagine, for example, if you could build PC applications like that! The computer would never slow down no matter how busy an application got, or how many resources it consumed. That’s not too bad for a 1980’s era architecture.
Once an interrupt is generated, the serial handling routine determines what generated it and then drives the code through multiple logical threads that operate completely independently of each other, without any knowledge of what the other threads are doing or what state they are in, outside of state flags and variables passed between threads.
There are some state-based decisions within each routine, that will sometimes cause the thread to generate the conditions necessary to start another thread, but generally, nothing is going to happen if an interrupt is not generated, and the routine must not loop or delay at all while executing. If the thread routine is called, and it can’t do something immediately, then it exits immediately, without delay and without requiring to be called again until it’s kicked off again by another interrupt.
The four threads running on this processor are:
- The PC Input thread
- The PC Output thread
- The SPI Output thread
- The SPI Input thread
THE PC INPUT THREAD: This part of the code receives bytes from the PC via the 115200 baud serial port. If it:
- has, at any time, received a “~” then it ignores all other input until it receives a CRLF. (Carriage Return/Line Feed)
- receives a space or tab code, it ignores it
- receives a R code, it initiates the SPI Buffer Transfer to ASYNC routine
- receives a S code, it then converts any new incoming hexadecimal into binary and stores it in the SPI Buffer, starting at the beginning, then notes how many characters were loaded into the buffer
- receives any other key that it is aware of, it sets the bit or flag associated with that key
It then exits immediately.
THE PC OUTPUT THREAD:
- If there’s a ROM based message to send, send it
- Otherwise, if there’s a short message in RAM, send it
- Otherwise, if there’s some data in the SPI buffer, convert it to HEX and send it
- Only sends one character then exits. Multiple character transmissions are achieved through state controls
THE SPI OUTPUT THREAD:
- If there’s data waiting to be sent to the SPI, send it
- Only sends one byte at a time, then exits
- Saves any received data from the SPI data register back in the same place as the previous byte it sent, rather than the current one
THE SPI INPUT THREAD:
- Is only called once per SPI transfer to catch the last byte received from the slave. This is only necessary as a special state when there’s nothing more to transmit.
Each of these threads runs each and every time an interrupt occurs, checks the current state of operation, linearly executes code and then exits regardless of any other circumstances. The code is streamlined like a pachinko machine, and while it may bounce around, it’s functionally linear and without loops and nothing ever gets caught up and delays execution. There is no such thing as waiting for a state change here – if the routine isn’t ready to execute, it doesn’t execute and if it executes, it must complete everything in a single fast pass. Every single thread gets an opportunity to execute each time there is a serial interrupt.
Code like this is nearly bulletproof. It can run reliably and with high integrity as long as the processor itself runs code correctly. The activity itself is closer to digital logic than it is to traditional software, and when it is properly debugged, it becomes very predictable and any execution delays can be known precisely within a finite period.
It is possible also to include lower-priority code within the non-interrupt-driven main loop and then use a lower-priority timer interrupt to control thread operation without affecting the high priority routines that are driven by the serial interrupt.
To this extent, it somewhat mirrors how interrupt routines occur in most software, except in our case, there is no other software running – only the interrupts matter. This helps to facilitate low-power modes of operation if the processor shuts down between interrupts, although this capability is not included in this code as power management is not a requirement for this project.
TALKING SPI WITH SPIEL
To keep the code simple, keys aren’t reused, so the hex entry keys (0 to 9, A to F) are only for HEX entry. Numerical SHIFT keys are used for setting up the SPI, and other keys are then used to change modes or change signal levels.
By sending these key characters via the serial port, SPI Encapsulation occurs, and SPI data can be sent and received via the serial port. Here’s how it works.
Say you want to talk to a SPI device, whatever chip it is. The process should be something like this:
- Activate the pin for chip select or slave select
- Send the bytes you want to transmit to the buffer
- Transfer the buffer to the SPI device
- Read back the bytes received during transmission
- De-activate the chip-select or slave select
Let’s say we want to send the code “AAAC53” - three bytes – to an AT89LP4052, to set it into programming mode. Well, the manual says to hold the reset line high, then pull down chip select, then just send the bytes.
Sending a letter I sets RESET high, while sending a letter K sets RESET low. Sending a letter O sets SLAVE SELECT high while sending a letter L sets SLAVE SELECT low. We use S to send hex bytes to the SPI buffer, usually ending with a - symbol, and X to transfer the buffer to the target chip. We can also force reset low then high to ensure that we enter the state just for this transaction and we should pre-set the select signals to not selected prior to transmission to avoid spurious or errored transmissions. The V key turns the outputs of the SPI chip on. You can turn them on at the beginning and leave them on, but since you might be connected to a running SPI bus, you might not want to take it over until you are ready.
This then makes the entire transaction:
Breaking down each key section (Spaces are ignored, so can be stored and sent anyway) that’s:
O - Chip not selected
I - Force reset high to turn the processor off prior to entering programming mode
These two set the state to begin with. Chip not selected, Chip not reset. Some other chips have inverted reset lines, such as the AT89LP214, which has a low reset instead of a high reset. This just means swap those characters around wherever they appear.
V – Turn on the output of the SPI Chip.
We’re ready to take over the SPI bus now.
SAAAC53- - This sends three bytes to the SPI buffer.
L – Set the chip select low – we’re going to send data.
X – Transfer the SPI data via the SPI bus.
“--” - This double dash is simply ignored characters. Since SPI can take some time to transmit data, and we’re probably going to cut and paste this, we need a pause here. Sending two lost bytes will achieve that for us.
O – Turn off chip select
Also, once we’ve finished with any other commands, since this probably isn’t the last, we exit programming mode by setting reset LOW again, so we should have another K at the very end of the sequence.
K – Set reset to low and start normal operation
Of course, we might want to send more than this, since all we did was enter programming mode. What if we want to read the chip’s signature to work out if we are talking to the right chip?
Then we would send the code AA3800, which means “Send me your ID code”, followed by three bytes, 00 00 00 to make sure that we read three bytes back from the SPI bus, then we need to know what the processor said. The process is much the same, except this time we don’t reset until it’s complete, and we ask it to tell us the received data;
Also, we should separate our initialisation section from our communications section. Now our string would look like this;
In fact, we can just continue chaining S instructions and LX--S instructions one after another until we’ve done everything we need to. That “R” on the end will cause the SPI interface to send back the bytes it read, and we can look at that to get the signature.
HOW THE CODE WORKS
The SPIEL software is written in 8051 Assembly code and the source code can be downloaded from the resources section of our website.
It’s written to fit within 2Kb, and is written as an entirely interrupt-driven process. That is to say, the main software loop of the processor does absolutely nothing except to loop on itself forever, after it has established the interrupts.
The source of the interrupts, rather than a time which often drives multithreaded code, is the serial port interrupt that activates whenever an event occurs at the serial port. Events that can trigger the code include;
- Receiving an asynchronous byte
- Finishing sending an asynchronous byte
- The final clock of the SPI port (both send and receive)
Each time an interrupt is received, the MCU runs the following threads:
- Received ASYNC thread
- Transmit ASYNC thread
- Transmit SPI thread
Because received SPI data occurs as a function of transmission, this is covered by the SPI thread that also transmits data.
The Received ASYNC thread
This thread processes any received characters. Most characters are a single-cycle process, such as setting a flag or modifying an internal register. Because there is no reuse of any keys, there is no need to track states, outside of “comment mode” in which all received data is ignored until a line feed is received.
The exceptions to this are when in S-mode (Buffer Receive Mode) where the characters received are assumed to be hexadecimal numbers until a non-hex character is received. i.e. not either 0-9 or A-F, with the exception of spaces and tabs, which it ignores.
In S-mode, the thread simply fills a buffer with these received characters until the buffer is full, or the transfer is finished.
THE TRANSMIT ASYNC THREAD: This thread checks flags to see if there’s anything to transmit. It may transmit one of the following three strings;
- Prewritten messages in memory
- Prewritten messages in RAM
- Hexadecimal content in RAM
Once the transmission is complete, it concludes the thread until it is called again.
THE TRANSMIT SPI THREAD: This thread is activated when an X is received by the Received ASYNC thread. At this point, it looks to see if the S-Mode buffer contains any data, and then transfers this to the SPI. It continues transmitting each time an interrupt occurs, until there is no untransmitted data in the buffer. As it transmits, it stores any received bytes in the same buffer so that it can be retrieved through the Received ASYNC thread via the R command (Read Buffer).
Each of these threads is relatively simple, but interworks with the others to keep delays between processes as short as possible. There is room for optimisation around high speed SPI output, but this can be achieved by prioritising the thread in code, by processing transmission first rather than last as it ended up arbitrarily.
The code itself is open under a CC license, so you’re free to experiment with it, modify it, sell it with your own products or change it any way you like. Please contribute any improvements back to the community.
WIRING UP CIRCUITS
The previous project uses the SPI programming header in a modified form that is backwardly compatible with the original Atmel SPI programming header. Although primarily intended for in-system programming, as long as the voltages used aren’t exceeded or fall short of requirements, then the board should work well as a SPI master bus controller. If the USB supplied power doesn’t meet your requirements, then disconnect the USB source, and run the board from your own source instead. It runs on anything from 3V to 5V.
The SPIEL header follows the original Atmel header closely enough that the undefined pins can be set low, and the remaining pins can be used to program devices via the header. But when working on new chips, it’s easier to mount the chip on a breakout board with pins, and then connect it to the header via prototype jumpers.
Most prototype jumpers are made from rainbow ribbon cable, and if you place all of the jumpers on the pins in the numerically correct order, based on the resistor code order, then you will automatically find the red jumper is VCC and the black jumper is GND. This probably isn’t a coincidence either. However, it doesn’t matter if you don’t follow the colours and the wiring is simple enough either way.
P0.0, P0.1 and P0.2 are all general purpose I/O, so can be used for any application, but the conventions in this project still set P0.2 as D/C as the displays often require a data/control line to know whether they are being programmed or receiving raster information.
It is possible to connect a number of displays up in parallel, and to operate them all independently using the extra signals. Or these extra signals can be used for things such as blanking or backlight controls.
To wire up a display, simply connect MOSI on the 10-pin SPI header to MOSI on the display, and so on, until all of the display pins have a wire connecting them. Keep in mind though that some displays have a SD card slot, and these also have separate connections, so although it’s a bus, you’ll need to wire these in parallel if you want to talk to the SD card with SPI also. Finally, on those displays, sometimes only the SD card pins are named MOSI and SCLK, while the same pins on the display connector are labelled DATA and CLK – just to confuse matters.
WORKING WITH DISPLAYS
Initialisation and Sending data
If you work with embedded projects, you’ll know that displays seem to present an interesting challenge. They are a fairly complex SPI peripheral with a number of commands, often come with deeply complex and confusing datasheets that obfuscate the task and it’s never really certain what commands you need to send, or if you need to send them all. Variation in pixel topology add to the mess and finally, there’s a range of chipsets and modes.
Three chipsets you are likely to encounter for which there are text examples within this article include:
NOKIA 5110 DISPLAY: A monochrome raster display
SSD1306: An OLED display, monochrome display with a range of size configurations
ILI 9341 LCD: A full-colour 320x240 display with 18 bits per pixel and a more complicated datasheet than the Space Shuttle Operators Manual. Sometimes called ST7735.
Between the three of these, this project actually covers most of the raster displays available. ILI9341 instructions also work with the ST7735 chipset which maintains similar commands, so in many cases, a single command structure supports multiple chips. The slight differences, which play havoc with engineering, can be addressed via the SPIEL circuit, since you can test any specific codes you want to use, and understand them in advance.
This is handy, because here’s all the displays I tested with the SPIEL chip while writing this article, and there’s three to four variations on pinouts, and you might think that keeping all the pinouts for a particular chipset or featureset the same is a good idea? It probably is, but the manufacturers clearly didn’t think so. Instead, they tend to choose a standard and then implement it across multiple chipsets, so for each common pinout, you can get just about any chipset configuration, as long as you’re not too fussed about display characteristics, such as size and resolution. If that seems like a bad idea to you also, you’re not alone in that thinking.
Nokia 5110 Display
Note: I find that there are two to three common pinouts for this display, so wire it up according to the pin label as these seem consistent between displays.
The Nokia display is the easiest to initialise and send data to. It’s also relatively simple, meaning you can have 8 pixels per byte, and can update the display graphically in less than a second through the serial port. As such, you can even use the SPIEL to control the display straight from your programs as an out-of-band information display for your PC.
To initialise the display, we have to send at least four bytes. These are detailed in the datasheet, and no matter where you get your information, whenever working with an IC, download the datasheet from the manufacturer or a datasheet site. The Nokia 5110 display is a Philips PCD8544 display, and has 48 x 84 pixels. It’s also the ONLY display I’ve worked with where it’s close to the datasheet, with the exception being how the backlight is controlled, and a wide range of contrast, which sometimes has to be adjusted for the first time.
The command structure is on Page 14 of the datasheet.
Prior to operating the display, we need to initialise the SPIEL chip at least once.
GHUKO - Set the initial signal levels (K = RESET ON. O=Chip Not Selected)
U - Display Backlight On (if it’s wired up to P0.3) D/C = Low
! - Set clock divider to /4 (fastest)
% - Least Significant Bit first
& - Data on First Clock Edge
< - Negative Clock
Q - Query the current settings (Should display :LN1/04:)
I - Turn the display reset line OFF
V - Turn the SPI interface outputs ON
In practice, we can shorten that entire string to;
This is actually the default configuration for the SPIEL chip, so we could omit it, but explicitly setting these in our notes lets us remember how it should have been configured. We could just use the second short-hand method and the SPIEL will still recognise it, but detailing each step and documenting it is useful when we have to use that information later to write code to initialise the display for embedded applications.
Next, we want to send four COMMAND bytes to the display. As per the commands on Page 14 of the datasheet, these are;
21 - Set command mode to “Extended Instruction Set Control” before next command - H=1
C0 - Vop set to middle of the range ( Contrast ) - values should vary from 80(light) to FF(dark)
20 - Set command mode back to normal (addressing) H=0
0C - Set Display Control, D=1 and E=0 which is “Normal Mode”
Alternatives to 0C at the end would include;
08 - Display Blank
09 - All Segments ON
0D - Inverse Video (White pixels on Black Background)
So we send the following string to the SPIEL to send to the display.
“S” sends 21C0020C to the SPIEL buffer. “-” ends the string. “L” sets the Chip Select (Slave Select) line low to tell the display we’re about to talk to it, “J” sets the D/C line LOW for command-mode, and finally X causes the string in the buffer to be sent to the SPI port.
The -----O at the end is because we’re not using the SPIEL interactively. If we were using code to talk to the serial port, it will use a received “+” symbol to indicate that the buffer has been accepted. It will then, after the X is sent, receive a “#” to indicate that the SPI port has finished transferring the data. Using this, it can control the flow to the serial port.
If we’re just using a serial terminal though, we want to wait a short while for the transfer to finish before turning off the DC command. Sending null characters such as “-----” just gives us a short pause and “O” turns off the chip select, as we don’t want to turn off Chip Select via a serial command before the SPI transfer has completed. Because of the delay, this lets us cut and paste the command string into a terminal without worrying about the timing.
You may need more null commands to longer strings, or if running a very slow clock for the SPI. As a rule, you won’t need more null characters than there are bytes of data send, as the SPI is always faster than the serial asynchronous port.
Finally, we want to make sure we’re going to start drawing on the display in the right place.
This ensures we’re in Addressing command mode and sets Horizontal and Vertical addressing to Zero (top left of screen).
If all goes well, we can now send video data straight to the screen.
The above is a 4x8 character set, allowing you to send a complete letter or number to the display with four characters.
In the first line, S1F111F00121F10001915120011150A00, sends video data to the display, and then rather than LJX which sends command data, the U character sets D/C to 1, allowing you to send display data. Other than this, the SPI display data is the same as the command data.
If we break down the line, and map each of the bytes directly, we can see that string looks like this;
1F111F00 – Forms the shape for a "0"
232F2000 – Forms the shape for a "1"
19151200 – Forms the shape for a "2"
11150A00 – Forms the shape for a "3"
Put together, the entire string can be sent from a text file to the SPIEL, and looks like this;
If we cut and paste this to the SPIEL in the serial terminal, we can initialise the display and send the characters 0123 to the display.
As such, we can see on the that after SPI Initialisation (the first line) and Display Initialisation (the second line), we can send display information with a location command (the third line) and BITMAP raster data with LUX rather than LJX (The fourth line).
If the data runs off the end of a line, it automatically runs onto the next line, however, when working with displays through SPI, it is always a good strategy to send location data at least once per line.
With enough repeats of the fourth line, an entire image can be sent to the display. To simplify the process of creating a full-screen image, a calculator has been provided with the downloads on the project page at DIYODE.
The spreadsheet below shows an 84x48 display area marked by grids. Placing a “1” in any of these locations will result in that location showing a pixel. The text at the bottom contains a SPIEL initialisation string, a display initialisation string, and 12 strings that each convert the display area into equivalent hexadecimal coding, and this can be copy-and-pasted directly into the SPIEL to generate a display image.
Finally, to understand how to best program this display, the location information should be understood.
The code S204080-LJX- doesn’t seem to correlate directly to 0,0 (top left) at all. Understanding its encoding, however, does provide the correct number.
The first number, 20, puts the display into standard command mode – so will accept coordinates in commands.
The second number 40, means ROW 0. There are 48 rows, as the display can be shifted around a pixel at a time, allowing for some very smooth scrolling. The display primary locations are mapped as:
40 - ROW 0
41 - ROW 1
42 - ROW 2
43 - ROW 3
44 - ROW 4
45 - ROW 5
You’ll probably have noted there that, because we started counting at zero, there are six lines if you take 8 pixels per line. You could fit 8 lines in, but the mathematics for the bitmap calculations becomes complicated.
Columns start at 80, which is equal to Column 0. Because we’re choosing 4 bits per pixel horizontally, that means the next character starts at 84, then at 88, 8C, 90, 94, 98, 9C... this continues until D3, which is the last column of the display before it wraps back around.
Because the display uses Bit 7 and Bit 6 of the position byte, it’s possible to just send a single control character to set either the horizontal or vertical position being written to.
If you want to create your own character set, that’s easy too. Just use the spreadsheet to convert bitmaps of whatever size you choose horizontally, try to maintain 8 pixels vertically, and then use the calculator to determine the hexadecimal codes for that character map from the calculations section at the bottom. Or otherwise, just draw the images how you want them, and if you like, just store these as strings in your program and send them to the serial port to update the display. Because this display is so simple, and the SPIEL takes any serial input, it’s even possible to send images to the display through the command line in many operating systems.
If you’re working with other displays, chipsets you’ll encounter a lot are the ILI9341/ST7735 (Same commands, different manufacturers) and the SSD1306. Both will vary significantly depending on the display you select, but the base commands tend to follow the chipsets to some extent, and it only takes slight variations for other displays in that family.
These are a OLED based display, and were one of my favourites after I got past their quirks. Like the Nokia, they are one bit per pixel, but some have different coloured zones. These are relatively fast displays that can be set up for complicated applications and are very small in size. They also consume very little power when not illuminated, and not too much when illuminated either. They make excellent wearable displays if you want to make your own Google-glass type glasses. These displays range in size from thumbnail to finger sized, and had good illumination and visibility characteristics.
They were a little more complicated than the Nokia display to set up and initialise, but not as complicated as the full-colour displays. Because they take the same bitmap raster data as the Nokia displays, they are a good choice if a very small display is desired.
Mapping varies from display to display, and the generic datasheet doesn’t show display-specific information so you’ll need to work out how a display is wired internally to get anything other than a 128x64 or 128x128 display running.
Commands are either SINGLE, DOUBLE or TRIPLE bytes, with all bytes sent consecutively after the first, and within the same transfer. Commands can be stacked, so you can send all commands one after another, and generally, order doesn’t matter as you’re only setting registers. D/C should remain low for all commands, no matter how many bytes are sent.
Selecting the address prior to writing raster information is accomplished through three bytes of command, although it comprises three individual commands rather than a triple, or even a single and a double. This is surprising since the horizontal addressing is sent via two commands, but they don’t have to be consecutive.
Vertical positions are prefixed with B in the upper nibble, so B0 is Row 0, B1 is Row 1, B2 is Row 2 etc., up to BF in a 128x128 display or B7 in a 128x64 display.
Horizontal position is bitwise, and the chipset itself seems to be able to support up to 256 bits across and can be as large as 5-inches, so this is sent by two 4-bit nibble commands, with the lower-order 4-bit nibble prefixed by 0 and the high-order 4-bit nibble prefixed by 1. As a result, if you wanted to write to Line 4, Column 20, you’d send three commands - B4 05 11 – And you could send those bytes in any order as long as the D/C line was low.
SDI1306 initialisation strings:
~ Send under SPI settings :LN1/04:
~ And initialise SPI state.
%<&! ~ /04 clock, LSB First, Negative Clock, 1st Edge, /04 CLOCK. ie; :LN1/04:
J ~ DC Low
O ~ SS high
K ~ Set reset Low
V ~ Activate SPI outputs
I ~ Set reset high.
~ Now we can initialise the display through the SPI interface.
S 81FF 2000 21007F 220007 40 8D14AF-LJX----O- ~ Initialise 128x64 OLED display.
~ Command string breaks down like this;
~ 81FF - Set contrast to FF ( Max Bright - Post reset it’s 7F - Mid bright )
~ 2000 - Set memory addressing mode, bits 0,1, where;
~ 00 (00H) - Horizontal Addressing Mode.
~ 01 (01H) - Vertical Addressing Mode
~ 10 (02H) - Page Address Mode (Default)
~ 11 (03H) - Invalid.
~ 21007F - Set column address start and end. 00=start, 7F=end (Pos 99)
~ This command establishes where the display is in memory.
~ 220007 - Setup page start and end address. 8 Rows - 00=start, 07=end.
~ 40 - Set display ram start to 0, Bit 6 always set, Bits 0-5 data, so 00..3F
~ 8D14 - Set charge pump ON...
~ AF - Display ON.
~ Note - Datasheet says throughout that 8D14AF is the MINIMUM initialisation bytes.
Some notes I made while connecting up several different brands of display using this chipset:
- Some displays can take up to 15 seconds to activate once the pump is started
- Screen position and start location varies from display to display – 0,0 is not a common origin
- Writing to an off-screen location can result in anything from failed image to a corrupted display, and I even managed a snow-crash once or twice
- To operate correctly, you MUST set up the boundaries for the display in the initialisation string. Never write a string longer than the display line
- Actual screen resolution varied significantly from the specifications in many cases. Even when the supposed resolution was etched into the PCB
- Some units were quite sensitive to the timing signals, and were not very tolerant of leaving signals selected
- Some supported brightness adjustment, others not. Some pseudo-supported brightness adjustment via charge pump manipulation, others not. When a feature wasn’t supported, changing the value of the register had no impact on the display
These displays range in size from around 0.5”x1” to around 4.3” diagonally and are full-colour.
They have 18 bits per pixel, which is three full bytes for every colour in full RGB mode, although you can switch them down to a two-byte 16bpp mode. The largest display in this family are 320x240 resolution, which is equal to MCGA graphics, also known as the initial VGA 256 colour mode, although this display has 262144 colours as it supports six bits per colour.
The effect of this, with respect to this project, is that the display is far too slow for use as a SPIEL output display, as even at 115200 baud, it takes quite a while to send a full display of information, however in slow-scan applications, it would still be suitable, though a more complex protocol than that SPIEL provides should be used, along with the capability to send metadata.
As a debugging tool, however, the SPIEL still works well with this display. Below is an example string to initialise the display, after which RGB information can be sent to the display as a raster.
An interesting application for such a display might be to show Video over SPI. With a secondary clocking master at the correct speed, a small chip like the 89LP4052 could still send initialisation information to the display, following which a simple clock could be supplied to transfer the raster image from the SPI slave to the display slave. However, this is a fairly complicated application and would be best left to a faster processor, such as 80MIPS 8052s or newer chips that can easily process real-time video data from VoSPI applications.
These displays were the most difficult of the three I programmed for this article. This is because most displays bought cheaply aren’t well labelled, and it’s not certain which chipset they contain. Slight differences in addressing and other factors such as image inversion round out the challenges, and it was necessary to adjust the code for each variety I was able to find and test.
Because they can provide photographic images though, there are still applications for which these displays should be used, beyond simple text imaging, although from the perspective of just text alone, these displays would make useful console loggers to attach to Linux servers, Cisco routers and other devices that send debugging information to the serial port. With 320 pixels x 240 pixels, it is possible to build an 80x30 character display in monochrome that would be suited to this application. Also, being relatively cheap and low-powered, such a display would actually be quite practical – but would still be relatively slow compared to a notebook or a phone-based application with a serial input. Of note, the command structure of the ILI9341 is such that commands are sent as both a command and data – that is, the command is sent, followed by a data string relevant to that command, so a string for this display might look like this;
~ pixel format set ~18Bit/pixel
This combines the LJX and LUX strings into a single line to send Command 3A, with instruction 66 to set the display to 18 bits per pixel.
The longest strings then for initialisation are Gamma strings which set gamma curves for display function. Be warned though that, out of a half-dozen of these displays, I hit the following differences;
- Advertised resolution often was not the actual resolution – which was frequently more
- Some displays don’t wrap immediately, but write some kind of null space for which there are no pixels
- Gamma correction didn’t work on all displays, and on some, gave an image that resembled a film negative
- X and Y scan characteristics vary from display to display regardless of the setting command – so some displays are like a mirror image of others with the same configuration
- Some displays have different commands, or support/drop commands that other displays might drop/support
- Default power-on-reset, line-reset and soft-reset have different default values
- Different delays might exist between models also. See datasheets, but don’t assume they are correct until you test the commands directly with the SPIEL
- Many of these displays come with a MISO input, so you can read the display status and memory contents also. These were the only cheap display boards which had integrated the MISO pin
ILI9341 Initialisation Strings
I've included a “Recommended” initialisation string from the manufacturer. It contains a lot of redundant information, but is fairly comprehensive, and while results were not perfect on all displays due to register differences, it did work nonetheless.
The “Recommended” string from the manufacturer is too long to show here but can be downloaded from the resources section of our website. It contains a lot of redundant information, but is fairly comprehensive, and while results were not perfect on all displays due to register differences, it did work nonetheless.
Once the initialisation strings are sent to the SPI, the display can be written, and each three byte combination will form RGB or BGR information per-pixel. This allows for photographic images to be written to the display.
Note: Some of the initialisation strings are commented out by a tilde (~) character.
Although the original circuit operated at 5V, well around 4V after going through a USB cable, it does operate down to 3.3V, which is important as many SPI peripherals, including the non-Nokia displays, all operate at 3.3V. If you want to run them, then you need to disconnect the 5V line from the USB circuit prior to connecting, so that the circuit can be powered by the display’s power, via the SPI header.
If you want to run a 3.3V display, however, from the USB power, you’ll need to modify the board with a 3.3V regulator. If you do choose to do this, it will still function correctly at 3.3V, and I have tested it as such, but it will change your voltage supplied through the SPI header also.
To change the board, remove the diode next to the 4pin USB header, at the end of the board.
One side of this diode goes to the USB 5V line. The other to the board’s supply rail. If you buy a 3-pin 3.3V regulator, you can drill the holes a little larger, and an extra hole for ground in the middle, and install a TO220 or TO120 packaged regulator right here, though be careful not to draw more than about 50mA total, as the regulator won’t handle it without a heatsink.
If I ever rebuild this board, I’ll design in a switchable 5V/4V/3.3V regulator to handle a greater range of supported circuits while using the power from the USB.
TALKING TO SENSORS
Perhaps you want to connect something like an accelerometer to your PC and do some testing with it prior to trying to embed it in a project, or you’re making your own prototype VR sensor that wires up to a USB port. Then you might choose something like the LIS302DL, which senses acceleration of between 2 to 8 G’s in X, Y and Z axis.
These chips are fairly cheap, are available on PCBs with header pins and you only read them at about 100Hz, so SPIEL is quite suited to the application.
Wire up your SPI 10-pin header like this, then run the chip. The chip I had ran OK at 5V, but it’s intended for 3.3V applications, so your mileage may vary. Once connected, you need to turn this device on, then read the three acceleration sensors inside of it.
This process is a little different from the displays, as we need to READ the data back from the chip. Because slave peripherals can’t send data without a clock signal, they use the generated signal from the master to return the data. As such, a FF byte should be transmitted where a received character would exist. It’s worth remembering that some SPI architectures use resistors to tie the MOSI and MISO lines together so that only one line is needed for transmit and receive, while others both transmit and receive at the same time.
Regardless, when the content to be read is on the bus, FF should be sent as a matter of habit to avoid pulling down the input line on some architectures. It doesn’t matter with normal SPI, but good habits will help you work with a greater number of architectures in the future.
So we want to initialise the SPI (I’ve just used the short default initialisation here of VO) and then write three strings to the accelerometer chip to read it. For this, I just need to paste in the following text into the terminal.
The first line turns on the SPI outputs, then Line2 sends 47 to register 20, which turns the chip on. Many SPI devices have to be enabled like this before they will start to talk back to you. The number 47 is specified in the datasheet, but there are other options there too.
Then line 3 sends the code 8FFF will read back the chip ID – which should return a string onscreen ending in 3B when we end with a R or READ character. According to the datasheet, 3B is the chip ID for the “who am I” command. This line is optional, and is a good way to check if the chip is the one expected, or to differentiate between accelerometer chips.
Then the final string reads in the status register, the X, Y and Z accelerations, and spaces them out with 00’s, which is once again read back by a R character sent to the SPIEL.
If viewed on a serial terminal, the response looks like this;
On the first line, the 3B at the end is the device ID.
On the second line:
- E7 is reflected by the initial string (it’s the register read command)
- FF is the Status Register at 27H
- DA is the X axis acceleration (This changes depending on acceleration)
- 12 is the Y axis acceleration (This changes depending on acceleration)
- 1E is the Z axis acceleration (This changes depending on acceleration)
The 00 reads are null-registers. They have no function if read, though most likely, they are reserved for a model that also has rotational measurement as well. At this point, you can quickly tell that your acceleration chip is working, just in case you were using the SPI as an in-circuit master. If you have a program now, you can decode the return string to see what the board’s orientation is.
PROGRAMMING OTHER CHIPS
Four examples so far have a very clear commonality. They all start with initialisation, then data is sent to, and in some cases read from, the SPI bus. When tackling a new chip for the first time, this is an excellent place to start.
This project wasn’t just something I thought up as a good idea. When I started working with SPI displays, I knew it was going to be challenging, and my first coding attempts failed, with nothing coming up on the display at all. So I wrote some debugging routines that let me control the data I was sending. Then I added some control lines via the serial port. Finally, I set it up so I could read and debug what was going on.
The end result is that the time I spent writing the original skeleton assembly code was returned when I embedded the strings, because by that time I knew exactly how to reliably talk to SPI peripherals. So the SPIEL circuit and code is something I personally use quite often.
You can either take over an existing circuit’s SPI but with the SPIEL, or you can download SPIEL to the on-board processor and control the SPI bus directly. It’s small, compact code and will work on a lot of ATMEL chips – Even the ones without upper memory blocks.
SPI & Pin Standards:
Common pin aliases for SPI devices found in the wild
SPI devices are named with the same standards mostly, so you just connect pins of the same name, eg, MOSI goes to MOSI. There is no crossover. However, below is a list of variations on pin names for you to consider.
WHERE TO FROM HERE?
If you work with SPI, I hope you find this tool useful and helpful. There are even applications where this circuit is the end-intention circuit too, such as controlling SPI devices from your PC. Whichever way you want to use it, if you liked it or have any questions, or if you’ve come across a different common SPI chipset for displays that I missed, please comment in the comments section at the bottom of this project’s website page.
PART 1: BUILD AN SPI ENCAPSULATION LINK