Package: bbb-pru-adc
Streaming ADC capture on BeagleBone (Black) with PRUProvides PRU firmware that captures up to 8 ADC channels, and userspace driver
to receive this as a stream of buffers containing voltage readings from ADC.Python is the most convenient way of using it. Lower-level API can be also accessed via
dynamic library, if needed.Features:configurable capture speed. Highest speed is around 15KHz.configurable number of samples to average over (important to get less noise)configurable set of AIN pins to capture. From just one AIN channel and up to 8 AIN channelsreports dropped readings (when userspace client is not fast enough to process incoming
buffers data is dropped to avoid buffer overflow)uses just 15-20% CPU, leaving plenty of cycles to actually deal with the datano dependenciesRequirementsHardware: BeagleBone (Black), With remoteproc (not UIO) enabled in/boot/uEnv.txtOS: Debian GNU/Linux 10 (buster), seehttps://rcn-ee.com/rootfs/bb.org/testing/2019-12-10/buster-iot/Or Debian GNU/Linux 9.8 (the latter may need to be re-configured to use remoteproc)Python 3.5 or betterRoot access rights (needed to install firmware into/lib/firmwarefolder and access sysfs)Please note that (depending on your environment) you may need root priviledges to run this code.InstallationWe recommend installing into virtual environmentpython3-mvenv.venv
..venv/bin/activate
pipinstallbbb_pru_adcRunning sample codepython3-mbbb_pru_adc.mainHere is the the code of themain.py- an example how to use this driver in Python:importtimeimportitertoolsfrombbb_pru_adc.captureimportcapturebad=0good=0withcapture([0,1,2,3,4,5,6,7],auto_install=True,clk_div=1)ascap:start=time.time()fornum_dropped,timestamps,valuesinitertools.islice(cap,0,1000):bad+=num_droppedgood+=len(timestamps)elapsed=time.time()-startprint('Elapsed:',elapsed,bad,good)print('KHz:',round((bad+good)/elapsed/1000,3))More samplesMore code examples can be found inexamplesfolder.Building from sourcesThis step isnot neededif you installed wheel from PyPI as described above. You need this only
if you plan to make changes in firmware or driver.gitclonehttps://github.com/pgmmpk/bbb_pru_adc.gitcdbbb_pru_adc/
makeclean
make
python3-mbbb_pru_adc.mainBuilding Python wheel from sources (for PyPi)python3 -m venv .venv
. .venv/bin/activate
pip install wheel twine
python3 setup.py bdist_wheel --plat linux_armv7lStream structureEach incoming buffer contains three pieces of information:num_dropped- the number of dropped readings before this buffer was filled (i.e. between
readings from previous buffer and this buffer there was a gap). Under normal conditions
this value is zero. It can not grow beyond0xffff. Thus, if you are unlucky enough to
receive0xffff, it basically means that at least that many readings were dropped (and
probably more).values- array of readings, packed in the channel-first order. It is anarray.arrayobject
with elements offloattype. Length isnum_readings * num_channels. Values vary between
0.0 and 1.8 (volts).timestamps- array of relative timestamps, corresponding to the readings. It is anarray.arrayobject with elements ofunsigned inttype.
Length of this array isnum_readings. Value is the number of PRU clock ticks
since the last reading. These values allow one to know exact timing between two readings. Time
distance between readings is fairly stable, small deviations are due to varying codepaths to
process outgoing and incoming messages. PRU clock runs at 200MHz (5ns per tick). Thus, the
timestamp value of 1000 corresponds to 5 millisecons, value of 200000 corresponds to 1kHz, etc.How many readings do we have per buffer? This depends on the number of channels we capture.
Exact answer is:num_readings=(512-16-4)//(4+2*num_readings)This formula is mandated by remoteproc IO buffer size limit (defined as 512 at kernel compile time).For a given capture session number of readings per buffer stays the same.Note that driver hasmax_numparameter that allows one to makenum_readingssmaller than the
maximum allowed by the IO buffer size. This is an advanced functionality that may be used to
achieve specific goals (e.g. lower latency or get exact number of readings per buffer). In most
applications, this parameter should not be used.Capture APIfrombbb_pru_adc.captureimportcapturewithcapture(clk_div=0,step_avg=3,channels=[3,5,7],auto_install=False)ascap:fornum_dropped,timestamps,valuesincap:# do something with this informationThis example starts capturing ADC inputs AIN3, AIN5, and AIN7 (channels=[3, 5, 7])
at full speed (speed=0). It will not attempt to
install PRU firmware (auto_install=False).
If driver detects that system firmware is missing or obsolete, an error will be thrown.Capture has to be used as a context manager. The context is a generator spitting out
the pieces of our buffer.Capture parameters:clk_div- ADC clock divider value. Fastest isclk_div=0, capturing at about 15KHz. In many applications 15KHz is just too much data (hard to process), andclk_divcan be set to other
values. For example, settingclk_div=9will capture 10 times slower (at about 1.5KHz).step_avg- How many capture steps to avegare for one sample. Default value is4, meaning
averaging over 16 steps. It produces the least amount of noise. Setting to values less than 2
is not recommended, because of the increasing noise in the values. Note that this value affects
capture speed. Higest capture speed of 15kHz is only possible without averaging. Highest
capture frequency with the recommendedstep_avgsetting of 4 is about 7kHz.channels- which AIN pins (aka channels) to capture. This is a list of 1 to 8 unique values,
representing the AIN pins to read. Note that values in the output buffer are layed out in the
same order aschannels.max_num- limits the number of readings per buffer. This is advanced functionality, see the section
below. Deafult is 0 that disables this limit.target_delay- target number of PRU cycles (5ns per cycle) per capture. This allows one to fine-tune
the capture speed. This is an advanced functionality, see the section below. Default is 0 which
disables this functionality.auto_install- if we detect that firmware is not installed, or is different, attempt to re-install by copying firmware file from python package resources to/lib/firmware. This action requires root priveleges. Once installed, you can use the driver as a non-root user.Important!timestampsandvaluesreturned by the generator are re-used and content will be
overwritten on next iteration. Do not store these buffers. If you are not processing data immediately,
copy them out.Advanced use:target_delayNormally, the time between two ADC captures is determined by the following factors:ADC capture speed (seespeedparameter)Time needed to process and send out the data
This time can slightly vary, because number of operations depends on buffering state and other
factors pertaining to PRU/CPU communication.Actual number of cycles is reported in thetimestampsarray.Thetarget_delayparameter sets the minimal number of PRU cycles. PRU will idle until the specified
number of PRU timesteps is reached. This allows one to:
a. Remove timestamp jitters
b. Fine-tune the capture speed to any desirable number (limited by the overall capture speed - around 16kHz)To target a specific capture frequence, do the following:choosespeedparameter to find the largest value that produces capture frequence just above
the desired one, thencompute the target number of PRU cycles for the desired frequency and settarget_delayto that
value. Remember that PRU runs at 200MHz clock and one cycle takes 5ns.measure the actual capture frequencyif it deviates from the desired one, changetarget_delaya bit to adjust. If actual frequency
is lower than desired, lowertarget_dealyvalue. If actual frequency is higher than desired,
increase thetarget_delayvalue.This should allow one to get very precise capture frequency.Advanced use:max_numNormally, driver will use all available space in the communication buffer (512-16 bytes). Buffer
size is determined by theremoteprockernel module. Using all available buffer space
minimizes bandwidth loss due to the control information (attached to each buffer sent), and thus
minimizes the chance of data loss. In short, if you want the most efficient data transfer, do not
change this value.Somethimes, you may want to use smaller buffers. For example, to ensure lower latency (at the cost
of getting less efficient comunication). You can do this by settingmax_num.Themax_numparameter ensures that no more than that many ADC readings will be packed per buffer.
If you set it to a high value, the real limit will be the communication buffer size and parameter
will be effectively ignored.InternalsThere are three pieces of software:firmware running on PRU sidebbb_pru_adc/resources/am335x-pru0.fw, built fromsrc/firmware.c,src/firmware.h, andsrc/common.h.CPU-side userspace driver that handles low-level details of communication with PRUbbb_pru_adc/resources/libdriver.so, built fromsrc/driver.c,src/driver.h, andsrc/common.hPython code that is responsible for installing the firmware and starting and terminating
the PRU processor.FirmwareOverall logic is this:Initializeremoteproccommunication subsystem (this creates character device/dev/rpmsg-pru30)Initialize array of 8 ring buffers that we will use for data exchange with CPUEnter main loop, where we:
a. wait for incomingSTARTcommand with parametersspeed,channels,max-num, andtarget_delay. When received,
we initialize ADC for the given channels and capture speed and start capturing.
b. ifSTOPcommand arrives from CPU side while we are capturing, we stop the ADC capture
c. ifACKcommand arrives from CPU, we release one ring buffer (CPU sends this command
to acknowledge data receipt)
d. when one ADC capture completes, we push the readings to the ring buffer. If ring buffer is
full, we send it out to the CPU side and try to get a new ring buffer. When CPU side
is slow, we may run out of buffers. Then we will drop the reading. After pushing
the readings to the ring buffer we schedule another ADC capture.DriverOn the CPU side we do this:driver_startmethod opens/dev/rpmsg-pru30device and writes a message there
withcommand=START, andspeed,channels,max_num, andtarget_delayvalues
to ask PRU to start ADC capturedriver_readmethod reads device file, blocking until a message arrives. It then sends
out theACKcommand, and unpacks the data from received buffer into the caller's buffers.driver_stopsendsSTOPcommand to the PRUPython sidePython code inbbb_pru_adc/capture.pydoes this:loads the driver libraryloads (installing if needed) the firmware into PRU, starts PRUwaits till/dev/rpmgs-pru30device is createdcallsdriver_startto initiate capturein a loop receives captured data by callingdriver_readwhen finished, callsdriver_stopand stops PRULinkshttps://github.com/MarkAYoder/PRUCookbook.githttps://markayoder.github.io/PRUCookbook/https://github.com/derekmolloy/exploringBBhttp://exploringbeaglebone.com/http://theduchy.ualr.edu/?p=289