Tuesday, October 20, 2020

Using asyncio with libgpiod on an OrangePI Zero

 Python is a great tool to create simple/fast test code, combine that with libgpiod, now you can toggle and monitor gpio pins.  

In recent Linux kernels libgpiod is a library to write C/C++ and Python to toggle and monitor general purpose pins.   There are plenty examples for python.  I use buildroot to create a Linux distro, one option under hardware is to install libgpiod and if you have python3 selected now you can write python scripts. 

Here is two basic classes gpiowrapper.py:

import gpiod
import threading
import asyncio
import logging

class OutputPin:

def __init__(self, chip ="gpiochip0", pin = 0):

logging.debug("args chip={0} pin={1}".format(chip, pin))
self.chip = gpiod.Chip(chip)

if self.chip is None:
logging.error("chip not found")
else:
logging.debug("type {0} {1}".format(type(self.chip), type(pin)))

self.line = self.chip.get_line(pin)
self.line.request(consumer=chip, type=gpiod.LINE_REQ_DIR_OUT)

def set_level(self, level=0):
if level:
self.line.set_value(1)
else:
self.line.set_value(0)

class EventPin:

def __init__(self, chip = "gpiochip0", pin=0 ):
try:
self._pin = pin
self.chip = gpiod.Chip(chip)
if self.chip is None:
logging.error("chip not found")
else:
logging.debug("type {0} {1}".format(type(self.chip), type(pin)))

self.line = self.chip.get_line(pin)

self.line.request(consumer=chip, type=gpiod.LINE_REQ_EV_RISING_EDGE)
self.on_wait = asyncio.Queue()

asyncio.get_running_loop().add_reader( self.line.event_get_fd(), self.event_cb)

except Exception as ex:
logging.error("Error: {0}".format(str(ex)))

def event_cb(self):
event = self.line.event_read()
if event.type == gpiod.LineEvent.RISING_EDGE:
evstr = ' RISING EDGE'
elif event.type == gpiod.LineEvent.FALLING_EDGE:
evstr = 'FALLING EDGE'
else:
raise TypeError('Invalid event type')

logging.debug(" event {0}".format(evstr))
self.on_wait.put_nowait(event.type)
async def wait(self):
await self.on_wait.get()




 

The EventPin class can be used with asyncio to wait for Edge Triggered GPIO.  The key line is adding file descriptor of the event pin to asyncio and perform a callback when the FD is changes and push the event into a queue so await can be used. 

Here is a simple blink.py:

import gpiod
import sys
import time
import argparse
import gpiowrapper
import asyncio
import logging

async def blink(io_chip, o_pin):

logging.debug("blink")
pin = gpiowrapper.OutputPin(chip=io_chip, pin=int(o_pin))

while True:
pin.set_level(0)
await asyncio.sleep(0.1)
pin.set_level(1)
await asyncio.sleep(0.1)


async def toggleTask( o_pin, timeout):
try:
while True:
o_pin.set_level(0)
await asyncio.sleep(timeout)
o_pin.set_level(1)
await asyncio.sleep(timeout)
except Exception as ex:
logging.error("error {0}".format(str(ex)))

async def monitor_event(io_chip, output_pin, event_pin):

o_pin = gpiowrapper.OutputPin(io_chip, output_pin)
evt_pin = gpiowrapper.EventPin(io_chip, event_pin)

toggle_task = asyncio.create_task(toggleTask(o_pin, .5))

while True:
val = await evt_pin.wait()
if val == gpiod.LineEvent.RISING_EDGE:
logging.debug("Rising edge event")
elif val == gpiod.LineEvent.FALLING_EDGE:
logging.debug("Falling edge event")

if __name__ == "__main__":
logging.basicConfig(format='%(asctime)s %(levelname)s %(filename)s:%(lineno)s - %(funcName)20s() %(message)s', level=logging.DEBUG)
# execute only if run as a script
# Create the parser
my_parser = argparse.ArgumentParser(description='List the content of a folder')

# Add the arguments
my_parser.add_argument('-chip', help='chip ')
my_parser.add_argument('-opin', type=int, help='output pin')
my_parser.add_argument('--epin', type=int, help='event pin')
my_parser.add_argument('--blink', help='event pin')
my_parser.add_argument('--event', help='event pin')

# Execute the parse_args() method
args = my_parser.parse_args()
logging.debug("main")

if args.blink:
logging.debug("run blink")
asyncio.run(blink( args.chip, args.opin))
if args.event:
logging.debug("monitor")
asyncio.run(monitor_event(args.chip, args.opin, args.epin))

 

Load both files onto the Orange PI Zero. 

python blink.py -chip gpiochip0 -opin 1 --epin 0 --event True

This will use  PIN11 (GPIO1) for output and toggle at 1 second.  Pin13 will wait for Rising Edge trigger.  If you connect both pins together then you will get:

# python blink.py -chip gpiochip0 -opin 1 --epin 0 --event True
1970-01-01 00:47:21,419 DEBUG blink.py:60 -             <module>() main
1970-01-01 00:47:21,420 DEBUG blink.py:66 -             <module>() monitor
1970-01-01 00:47:21,421 DEBUG selector_events.py:59 -             __init__() Using selector: EpollSelector
1970-01-01 00:47:21,424 DEBUG gpiowrapper.py:10 -             __init__() args chip=gpiochip0 pin=1
1970-01-01 00:47:21,558 DEBUG gpiowrapper.py:16 -             __init__() type <class 'gpiod.Chip'> <class 'int'>
1970-01-01 00:47:21,648 DEBUG gpiowrapper.py:36 -             __init__() type <class 'gpiod.Chip'> <class 'int'>
1970-01-01 00:47:22,153 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:23,156 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:24,159 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:25,161 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:26,164 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:27,167 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:28,170 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:29,172 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:30,175 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:31,178 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:32,180 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:33,183 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:34,186 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:35,188 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:36,191 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:37,194 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:38,197 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:39,199 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:40,202 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:41,205 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1970-01-01 00:47:42,207 DEBUG gpiowrapper.py:57 -             event_cb()  event  RISING EDGE
1


Sunday, August 30, 2020

Using FPGA to accelerate serial protocol handling adding two AXI Stream blocks.

 I have been working on communication protocol to the CMOD-S7 and CYC1000 via the FTDI over USB.  One of the issues is round-trip-time.  Currently the packet handling is done all by the MicroBlaze.  There are to basic packet handling duties the Microblaze performs in the RX/TX data path:

  • Stripping or adding ESC formatting.  This loosely follows SLIP/PPP serial encoding.  
  • Calculating CCITT CRC-16
 So, to lower the round trip time, I have been working on two independent AXI Stream blocks:
  1. 16 bit CRC, will calculate the CRC per byte when TLAST is raised append the two bytes.
  2. Packet format: add SOP (start of packet byte), ESC any bytes that are SOP,EOP, or ESC and add EOP when TLAST is raised. 

Using just the MicroBlaze, the ping rate is 1.7Mpbs, adding the two AXI stream pushes the rate to 2.7Mbps.

 

Here is a block diagram from vivado.


 The next goal is to eliminate the MicroBlaze processor and add a AXI switch.  The serial protocol has a ToPort, this will be used for the AXI Stream ID.  

This project take some time, the goal is to push the data rate to 12Mbps, maybe add the ADC and temperature sensor, to also push data via AXI stream.

Sunday, August 23, 2020

Using AXI stream with DMA on a CMOD-S7 FPGA board

After a several weeks of tweaking, DMA driver and Verilog, AXI Stream serial TX/RX using DMA with the MicroBlaze soft core is working.

Over the past several months I have been using Fast Serial mode with FTDI, but it is only half duplex, if the PC and FPGA try to send messages at the same time, the FTDI chip does some weird things depending on the state if the FDSI and FSDO, worked with tech support, "should only be used with a half duplex protocol".  

So, back to standard serial, the FTDI serial interface can support 12Mbps in full duplex, so, it is possible to have the FPGA  and PC send async messages.  So, how to get 12 Mpbs to the Spartan 7?


Created a axis_serial RTL (In verilog) that can stream TX/RX bytes out to the FTDI chip at 12Mpbs.   This is a real simple block, used serial code from Nandland with a AXI stream wrapper. Also this wrapper looks at the incoming bytes from the serial and will raise TLAST for EOP (End of packet) byte.  TLAST is connected to the DMA block and this will terminate the DMA and the RX buffer is now filled with a packet.  

The DMA driver is packet based, i.e. like Ethernet LWIP example, but in this case it is using a serial stream. This way the microbalze is not processing serial interrupts per byte, but, interrupt per packet.  

Configured the clock to be 120Mhz, the current "ping rate" is 1.7Mpbs in one direction, or 3.4Mpbs full duplex.  I'm working on offloading the CRC and packet ESC packing to the FPGA.  CRC is done, Packing is taking some time.  This will be in the TX direction for now, so, the rate should increase to 3.4Mbps in TX direction.

All of the code is in gitub.




Sunday, May 24, 2020

Using SERV RISCV soft processor on CYC1000 FPGA board

I've been working on using a FPGA for I/O acceleration for low cost ARM boards for a while.  The first pass was done using standard Avalon bus master over Fast Serial interface using the FTDI chip.  Fast serial can use up to 50Mhz clock, but it was scale back to around 20Mhz for debugging with my old logic analyzer.  

The goal is to use HighSpeed USB between a low end ARM SBC and off load tasks like:

Recently, I moved to using fusesoc tool and the SERV RISV processor for offloading some of the verilog coding effort.  So, far the project SERV RISCV is running, fast serial wishbone interface has been debugged.  Next is to update the protocol.  The project is on github.  Soon to follow a HacksterIO article

Using the SERV processor is SMALL, and can use several in this design, will use one for the accelerometer control.


Sunday, March 15, 2020

PWM input is working (Betaflight and CYC1000)

Made some good progress on integrating CYC1000 FPGA into betaflight running on Linux.  The CYC1000 is a FPGA performing PWM decode (from cheap RC transmitter), PWM encode (oneshot125 or DSHOT), APA102 led strip and on board LED.  Updated betaflight to send requests to CYC1000 to read PWM registers via libftdi1 over USB.

Here is a youtube video of betaflight showing PWM decode and IMU.

Tuesday, February 25, 2020

Using Fast Opto-Isolated Serial Interface Mode in the FT2232H with CYC1000 FPGA and libftdi1

After learning more about FPGA programming i.e verilog, nmigen, migen, and generating test benches finally got back on finishing the Avalon bus for the quad-copter.

The core gateware is Avalon bus with slave PWM (oneshot125), DSHOT150, slave LED APA102, slave on board LED, slave PWM decoder, and general timer.  The master is using Avalon bus master  bytes to packet interface over Fast Opt-Ioslated Serial interface.  One of the issues I was having was how to communicate from Linux to the CYC1000 board, could do standard serial, the top speed is 2Mbps, so, took the time and implemented Opt-Isolated serial interface at 25Mbps.  Reading more about the interface it is half duplex, but, it is still faster than serial.

So, use the standard FTDI driver with Linux with standard tty termios, but after trying to get 1ms round trip packet processing, the CPU usage was high and still slow.  I tried setting  setserial /dev/ttyUSB0 low_latency, that helped but still high CPU usage.

Next was trying libftdi1, with a latency timer of 0.  After a few few mod's to libftdi1, using a 100us timer to generate packets and uses 40% CPU usage.  Validated the data stream using LA2016 Logic Analyzer and the rrt is 180us.  So, not bad. Next is to move everything on to the orangePI plus2 H5.




Saturday, February 8, 2020

First project on Hackster.io

Hacksterio hosted webinar "Mini but Mighty: Motor control Live Build with MiniZed"  also Avnet/Hacksterio/Xilinix created a design challenge.  After several days of work, entered with a project on Hacksterio.    Will find out on 2/18 on the results.

Wednesday, January 8, 2020

PWM encode/decode is working using a FPGA

Tested FPGA based pwm encode/decode.  Using Hobby King HK-16A V2 6 channel transmitter and decoding 6 channels of PWM from the receiver..  Encoding 4 PWM with a 1-2ms pulse width to 4 ESCs.  The biggest issue generating PWM is to complete the last cycle before apply the next update.  

Next step is to integrate into BetaFlight Linux Port, the goal is to be flying in a month.

All the code is on github.  


Sunday, January 5, 2020

New logic analyzer

It was time to update to a better logic analyzer, LA2016. I have been using 8 channel original saleae. The LA2016 is a lot faster, 200Mhz and has internal storage with advance triggering.

One of the biggest issues I've been running into is tracing fast digital signals when developing FPGA code.   CYC1000 uses Cyclone 10LP FPGA, have used  Quartus internal logic analyzer a couple of times to trace/debug designs.  But, I still like to have one around, so, the LA2016, so far it has been very handle, with a 1Mhz signal with a pulse width of 15ns was not an issue.  But, also still getting up to speed on Verilog, have been working on test benches and verilator C++ test code.  This way modeling and formal verification should cover most of the issues I've having debugging designs.

While debugging PWM state machine, the power supply driving my ESC tripped on overload.  No, it does not power one, so, have another one on order, 12v 10amp.  So, will be a little more careful.

Looks, like the PWM output should be working now, there was a glitch when setting a new PWM value, the duty cycle was going to 80% causing some ESC to stop.