Compiling MicroPython and installing on the Discovery board was very easy after setting up for the PyBoard. The STM32F407 is nearly the same as the PyBoard with a few extra features.
- 168 MHz ARM M4 that does 210 DMIPS.
- Three 12 bit ADC that can convert at over 2MHz each or be combined for nearly 7 MHz.
- Many more GPIO pins exposed. 5V and 3.3V (3V3) available on the pins.
Use a jumper from PA9 to 5V to provide power from the Micro-USB connector CN5 (or connect a second USB to the other mini-USB connector). Connect your Discovery board to the Micro-USB from your PC and ignore Debian’s request to open it with Files. Be sure you have a jumper for DFU mode – connect pins Boot0 and VDD. They are next to each other. To compile and program the board, get in the STMHAL directory (see the PyBoard page) and use this make command:
cd stmhal make BOARD=STM32F4DISC deploy
The download of MicroPython to the Discovery Board Flash will proceed automatically. When finished, remove power and the Boot0 jumper. Launch a terminal program like Minicom, and reconnect the Discover Board. Minicom will find Micropython on /dev/ttyACM0 and you will see the boot message and the Python prompt >>> My Minicom found it automatically. You can use arguments when starting Minicom if you have to
minicom -D /dev/ttyACM0
Debian will (should) prompt you to open the device with files. Go ahead. Wait for it to read and sync, and then open main.py with a text editor. I just use gedit for simple tasks. You can put any test code here. Blink LEDs or send text in an infinite loop, or execute lines as if you typed on a terminal.
while TRUE: print("Test")
Save in the editor and switch to the Mincom terminal. ^C to stop any running main.py and get the >>> interactive Python prompt. ^D will launch the edited main.py. Repeat as needed. You can also use reset if something goes really wrong. Save, unmount the board, reset (the black button on the Discovery).
This is the development cycle and it goes pretty fast. Each time you reset to run new code, Debian will also offer to open the device in Files. You can go ahead and open and edit while the board is running your current code.
My real test task is to use an LTC2400 24 bit ADC on a break-out from an eBay seller. The device uses a 3-wire interface compatible with SPI and this means figuring out SPI with MicroPython and printing and calibrated voltage from 0 to 4.096 volts. (The eBay beak-outs are also available with a voltage reference for 3.3 volts).
The SPI interface uses 3 or 4 lines.
- MOSI Master Out Slave In
- SCK Data clock
- MISO Master In Slave Out
- NSS A Chip Select and/or Data Mode line.
And the LTC2400 only uses MISO, SCK, and an optional NSS. The Python interface to SPI does not have NSS or a chip select (CS) signal and CS will have to be provided by a GPIO pin and manipulated in the data collection code.
From the pinout and alternate functions list for the Discovery Board, SPI1 is used by one of the USB. SPI2 is available and can be configured to use some GPIO pins. PB7 is not used on Port B and is OK for a CS output. Here is function name, port name and pin, and header connector-pin number.
- MISO PB14 P1-38
- MOSI PB15 P1-39
- SCK PB13 P1-37
- CS PB7 P2-24
This extra GPIO for CS is forced by the nature of the LTC2400. In order to run in external clock mode where we control the data conversions, SCK must be low when CS goes low, otherwise it goes into an internally clocked mode. Rather than take the time to understand the complicated port timing, I chose to just make my own CS with GPIO and experiment for lowest noise clock and select rates. The LTC2400 data is clocked out on the falling edge of SCK and latched by SPI on the rising edge. There are some other simplifications here that I will explain later.
I’ll try posting the code then explaining it in detail line by line. I usually hate that, but in this case I’ll include the SPI hardware setup and description so there is more than just code talk. The format for the LTC2400 output is most significant byte first with upper 4 bits of the first byte beings status and the last 4 bits on the last byte being extra conversions bits that may have value in long averages but are normally discarded.
# main.py -- put your code here! from pyb import Pin, SPI spi = SPI(2, SPI.MASTER) spiCS = pyb.Pin(pyb.Pin.cpu.B7, Pin.OUT_PP, Pin.PULL_NONE) print("Init CS") spiCS.init(Pin.OUT_PP, pull=Pin.PULL_NONE) print("Init SPI") spiCS.low() spi.init(SPI.MASTER, prescaler=64, polarity=0, phase=1, bits=8) print(spi) buf = bytearray(4) signBit = 0 rangeExtended = 0 # Clear last conversion spiCS.low() spi.recv(buf, timeout = 1000) py.delay(500) spi.recv(buf, timeout = 1000) spiCS.high() py.delay(500) while True: sum=0 for i in range(0,64,1): spiCS.low() spi.recv(buf, timeout = 1000) spiCS.high() pyb.delay(250) signBit = (buf & 0b00100000) >> 5 # Find sign bit rangeExtended = (buf & 0b00010000) >> 4 # Find extended range bit adcval = (buf + (buf<<8) + (buf<<16) + ((buf&0xF)<<24))>>4 # Shift the bytes to get 24 bit data. if signBit<1: adcval = -adcval if rangeExtended > 0: print("Extended Range") sum += adcval raw = sum>>6 print("sum: %12d, Raw: %9d, Volts: %8.7f" % (sum,raw,(raw*4.096)/0xFFFFFF))
Only two PyBoard resources are needed. The Pin and SPI libraries are imported. Then an SPI device is created spi = SPI(2, SPI.MASTER) with augments that choose SPI channel 2 and define the SPI as a master device. Then a GPIO pin is given a name, spiCS, and initialized as a push-pull output (most like a CMOS output that toggles high/low). spiCS = pyb.Pin(pyb.Pin.cpu.B7, Pin.OUT_PP, Pin.PULL_NONE)
The initialization can take place when the device is created or with an init method later. Here I have done both for the GPIO pin. The SPI init call shows some of the options for SPI. In this case, it is a Master device, speed of the SPI SCK clock is set by the prescaler, polarity is 0 which means the SCK idles low (as needed by LTC2400), phase is 1 which means data is latched in SCK rising edge, and the data will be read in 8 bit hunks. There us a 16 bit mode which I have not tried. If I was doing this in assembler, I would use 16 bit mode. The ARM has a barrel shifter which means any size shift of bits left or right has the same cost – free. It is part of any instruction since barrel shift logic is basically instant. I hope the MicroPython internals take advantage of this.
There are other parameters like baud rate that can be entered optionally but as far as I can see, prescaller over-rides any value in baudrate. (I will be looking at the core C code soon). Now print(spi) will print the parameter list and values for this SPI channel, SPI(2)
spi.init(SPI.MASTER, prescaler=64, polarity=0, phase=1, bits=8) print(spi)
Set up some variables:
buf = bytearray(4) signBit = 0 rangeExtended = 0 meanCount=64
buf is a place to put the 4 bytes that will be read from the LTC2400 each time a reading is taken. There are two status bits of any use, sign, and a bit that says the device is in extended range. meanCount is for averaging readings to reduce noise. Assuming noise is Gaussian, noise is reduced by the square root of the number of readings added up. 64 readings is an 8 times reduction in noise.
Then there is a double reading of the LTC2400 to clear it out before starting data collection. Note: The LTC2400 is always holding the data from the last time it did a conversion. When you read data, it then starts a new conversion and holds the value until you read again. spi.recv(buf, timeout = 1000) does four readings of one byte each in order to fill buf, which is 4 bytes long.
Viewing the main data taking loop again, while true: starts an infinite loop
while True: sum=0 for i in range(0,64,1): spiCS.low() spi.recv(buf, timeout = 1000) spiCS.high() pyb.delay(250) signBit = (buf & 0b00100000) >> 5 # Find sign bit rangeExtended = (buf & 0b00010000) >> 4 # Find extended range bit adcval = (buf + (buf<<8) + (buf<<16) + ((buf&0xF)<<24))>>4 # Shift the bytes to get 24 bit data. if signBit<1: adcval = -adcval if rangeExtended > 0: print("Extended Range") sum += adcval raw = sum>>6 print("sum: %12d, Raw: %9d, Volts: %8.7f" % (sum,raw,(raw*4.096)/0xFFFFFF))
In this infinite loop, the sum for the average is set to zero. A reading is made by setting CS low, reading four bytes into buf, and setting CS high followed by a delay of 250ms.
The bye order is highest byte first, which has status in the 4 highest order data bits. The sign bit is isolated and shifted to the lowest order bit, which makes it a 1. Then the range extension bit is isolated. I like to use binary representation of masks in order to picture the bit positions. Yes, I can do math in hex in my sleep, but the binary is a better representation of the physical device.
Then the ADC data has to be extracted and formed into a 32 bit number with the low 24 bits containing the data and the upper 8 set to zero. The least significant byte is in buf and it can be used as-is. Add buf to the buf value with its binary value shifted 8 places to the left. Add buf that has been shifted 16 places left. Finally for buf, wipe the upper 4 bits with the binary AND operation and shift left 24 places and add. If you like to see the bits like I do, the line can be written this way.
adcval = (buf + (buf<<8) + (buf<<16) + ((buf&0b00001111)<<24))>>4
If MicroPython is using the ARM barrel shifter for integer shifts, then this is very fast. In assembly code on the Discovery board this whole line will execute in about 10 instructions or 16 to 21 million times a second. [The ARM M4 on the Discovery does more than one instruction per clock due to look-ahead and pipe-lining. I would have to test to see the real speed.]
The readings are totaled up and the result is divided by meanCount, in this case 64, which is the same as a right shift of 6 bits. To generalize, divide the floating value of the total by meanCount as a regular floating point math calculation.
I print three values to inspect the process. The sum: is the total integer sum of the readings. The Raw: is the integer value of the average of the readings without scaling to a voltage. Volts: is the raw value scaled to a voltage. The voltage reference on this board is 4.096 volts. To scale, simply multiply by 4.096 and divide by the maximum reading of the LTC2400. To minimize round-off and truncation errors, multiply first, then divide. The max reading has all bits on. In binary this (24 bit number) is 111111111111111111111111 and in hex it is FFFFFF. No need to figure what that is in decimal (16,777,215). [If you remove the 4 bit shift >>4 for adcval, you can use all 28 data bits. Divide by hex FFFFFFF.]
Here is some output from a partially discharged Li-Ion battery.
sum: 1010304193, Raw: 15786003, Volts: 3.8540051 sum: 1010303548, Raw: 15785992, Volts: 3.8540025 sum: 1010307666, Raw: 15786057, Volts: 3.8540182 sum: 1010310359, Raw: 15786099, Volts: 3.8540285 sum: 1010312272, Raw: 15786129, Volts: 3.8540359 sum: 1010313298, Raw: 15786145, Volts: 3.8540397 sum: 1010315626, Raw: 15786181, Volts: 3.8540485 sum: 1010316534, Raw: 15786195, Volts: 3.8540518 sum: 1010320641, Raw: 15786260, Volts: 3.8540678 sum: 1010316923, Raw: 15786201, Volts: 3.8540535 sum: 1010320659, Raw: 15786260, Volts: 3.8540678 sum: 1010320845, Raw: 15786263, Volts: 3.8540686 sum: 1010322418, Raw: 15786287, Volts: 3.8540745
This is using all 24 bits and averaging 1024 samples:
sum: 258696696256, Raw: 15789593, Volts: 3.8548815 sum: 258697241294, Raw: 15789626, Volts: 3.8548897 sum: 258697294828, Raw: 15789629, Volts: 3.8548903 sum: 258697876649, Raw: 15789665, Volts: 3.8548992 sum: 258697751112, Raw: 15789657, Volts: 3.8548973 sum: 258697695754, Raw: 15789654, Volts: 3.8548964 sum: 258697819403, Raw: 15789661, Volts: 3.8548983 sum: 258697499242, Raw: 15789642, Volts: 3.8548935 sum: 258697389996, Raw: 15789635, Volts: 3.8548918 sum: 258697665284, Raw: 15789652, Volts: 3.8548958 sum: 258697085561, Raw: 15789617, Volts: 3.8548875 sum: 258696741473, Raw: 15789596, Volts: 3.8548822 sum: 258696909367, Raw: 15789606, Volts: 3.8548846 sum: 258696821528, Raw: 15789600, Volts: 3.8548832 sum: 258697302451, Raw: 15789630, Volts: 3.8548906 sum: 258697008351, Raw: 15789612, Volts: 3.8548861 sum: 258696827087, Raw: 15789601, Volts: 3.8548834 sum: 258696894048, Raw: 15789605, Volts: 3.8548843 sum: 258697492970, Raw: 15789641, Volts: 3.8548932 sum: 258697605363, Raw: 15789648, Volts: 3.8548948 sum: 258697647702, Raw: 15789651, Volts: 3.8548956 sum: 258697203213, Raw: 15789624, Volts: 3.8548891
There are some nice improvements that can be made to this setup, including powering the LCT2440 from a GPIO pin and reading the data out without clocking to find when conversion is done. For a serious meter or precision measurement, a better layout, better reference chip, and good shielding/grounding practice can bring it up to the full potential of the LTC2400. The chip can also be used with the MicroPython SPI interface by connecting the LTC2400 CS to ground and either reading at a rate a little slower than the chip, or monitoring the data pin. It goes low when data conversion is complete. This can also be used as an interrupt.
Now to clean this up a little, find out why grounding the Vin causes Extended Range, do an example with dynamic checking of the conversion done bit by switching pin mode on the MISO pin to GPIO and back to SPI, do an interrupt driven version, then finish up with interrupt driven DMA for data collection! That should be a good start for MicroPython in real–world data collection. After that, do it on the ESP8266 and send the data to a PC with WiFi. And just one more thing. Mesh network a dozen ESP8266 all collecting data and see what the data collection bandwidth is. Can I get audio from a dozen microphones in an array and track animals on my property?