Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

necessary to use old rpi kernel ? #64

Closed
beadon opened this issue Sep 28, 2022 · 20 comments
Closed

necessary to use old rpi kernel ? #64

beadon opened this issue Sep 28, 2022 · 20 comments
Labels
question Further information is requested

Comments

@beadon
Copy link

beadon commented Sep 28, 2022

In https://github.com/David00/rpi-power-monitor/wiki/Software-0.-Installation there is reference to use an old kernel for raspi. While this is all well and good to get things working, this soon becomes a security problem since this cannot be upgraded.

Over time, this will likely mean a lot of headache since the setup appears to be quite brittle.

I've given the latest kernel a go now -- v5.10 Have you tested this kernel before to see if it exhibits the SPI problem ? If not - has this SPI problem been reported to the kernel dev team yet ? If not, I can help champion it with you!

@David00
Copy link
Owner

David00 commented Sep 28, 2022

Hi @beadon, thanks for your input. Do you know of any open kernel-level security vulnerabilities? The kernel version I've recommended is a rather late release in the 4.19 chain and I can see that it has one minor open vulnerability:
https://www.cvedetails.com/vulnerability-list/vendor_id-33/product_id-47/version_id-643443/Linux-Linux-Kernel-4.19.118-2.html

Running an old kernel does not mean you have to run old software packages on top of it, but it does make upgrading packages a little more difficult. However, I want to be sure we're on the same page about any potential vulnerabilities, and I take security very seriously, so if you have any more insight on how the old kernel would become a security problem, I would like to dive into it here so we don't scare off other folks. :)


The underlying SPI issue has been reported to the Raspberry Pi team and you can see the issue here: raspberrypi/linux#3381

Just over a week ago, I did some in-depth testing with 5.15. The results of that test indicate it's not the kernel at fault, because using the low-level C driver produces a sample rate that meets expectations IFF the Pi clock is forced to high 24/7 with force_turbo=1. It seems to be limited down to the Python SPI library that I'm using to read the SPI bus.

While I've narrowed it down to the spidev library, it doesn't make sense that this would be the root cause of the issue. It's easy to point at the kernel, because that's the only thing that changes when the problem appears. And, as you know, falling back to v4.19.118 resolves the issue, even when everything else stays the same.

I would love to collaborate on fixing the issue for this project, and I see two paths forward:

  1. Rewrite the sampling part of the code in C, or
  2. Dig deep into the spidev library and determine what is causing the sample rate to be cut in half on the newer kernels.

Rewriting the SPI sampling in C would be a significant change to the project, whereas hypothetically identifying the issue in spidev and patching it would essentially be a simple dependency version bump.

@David00 David00 added the question Further information is requested label Sep 28, 2022
@beadon
Copy link
Author

beadon commented Sep 29, 2022

at the moment there are no known security vulnerabilities that I am aware of for the linux kernels in question here. I mention security because it's a broad topic that also covers the concept of "code rot" as various tools, requirements, and code dependencies change.

Regarding the investigation into why SPI is not clocking properly, this unfortunately makes a lot of sense. The clock rate of the ARM influences the SPI clock rate.

I believe the best, fastest, simplest path forward is to force the ARM core(s) to the correct frequency. Either:

  1. all the time
  2. for the duration of sampling (to ensure that the results are correct )

To achieve this, we have to get into C-states. ACPI defines these well. Intel has done a great job of implementing this for well over 15 years with the speedstep technologies and 'pcm' tools that both allow changing the CPU core frequencies, and monitoring the C-states, but it appears ARM is a little looser in implementation - which is where we will need to drive. Here's an antiquated guide for ARM dating from 2013 : https://www.linaro.org/app/resources/Connect%20Events/ACPI_and_Idle_States.pdf

In the intel CPUs the Linux kernel reports the CPU frequency right in /proc/cpuinfo , you can 'cat' this to see which frequency it is, know that it's populated by a daemon -- anyway it appears that the latest and greatest tool is cpufreq-info, which appears to gather lots of very useful information from across the system displaying it in one go.

install it:
$ sudo apt install cpufrequtils

run it:

$ cpufreq-info

cpufrequtils 008: cpufreq-info (C) Dominik Brodowski 2004-2009
Report errors and bugs to cpufreq@vger.kernel.org, please.
analyzing CPU 0:
  driver: cpufreq-dt
  CPUs which run at the same hardware frequency: 0 1 2 3
  CPUs which need to have their frequency coordinated by software: 0 1 2 3
  maximum transition latency: 0.00 ms.
  hardware limits: 600 MHz - 1.20 GHz
  available frequency steps: 600 MHz, 700 MHz, 800 MHz, 900 MHz, 1000 MHz, 1.10 GHz, 1.20 GHz
  available cpufreq governors: conservative, ondemand, userspace, powersave, performance, schedutil
  current policy: frequency should be within 1.20 GHz and 1.20 GHz.
                  The governor "ondemand" may decide which speed to use
                  within this range.
  current CPU frequency is 1.20 GHz.
  cpufreq stats: 600 MHz:53.41%, 700 MHz:1.34%, 800 MHz:10.82%, 900 MHz:3.29%, 1000 MHz:0.00%, 1.10 GHz:0.00%, 1.20 GHz:31.13%  (2871)
analyzing CPU 1:
  driver: cpufreq-dt
  CPUs which run at the same hardware frequency: 0 1 2 3
  CPUs which need to have their frequency coordinated by software: 0 1 2 3
  maximum transition latency: 0.00 ms.
  hardware limits: 600 MHz - 1.20 GHz
  available frequency steps: 600 MHz, 700 MHz, 800 MHz, 900 MHz, 1000 MHz, 1.10 GHz, 1.20 GHz
  available cpufreq governors: conservative, ondemand, userspace, powersave, performance, schedutil
  current policy: frequency should be within 1.20 GHz and 1.20 GHz.
                  The governor "ondemand" may decide which speed to use
                  within this range.
  current CPU frequency is 1.20 GHz.
  cpufreq stats: 600 MHz:53.41%, 700 MHz:1.34%, 800 MHz:10.82%, 900 MHz:3.29%, 1000 MHz:0.00%, 1.10 GHz:0.00%, 1.20 GHz:31.13%  (2871)
analyzing CPU 2:
  driver: cpufreq-dt
  CPUs which run at the same hardware frequency: 0 1 2 3
  CPUs which need to have their frequency coordinated by software: 0 1 2 3
  maximum transition latency: 0.00 ms.
  hardware limits: 600 MHz - 1.20 GHz
  available frequency steps: 600 MHz, 700 MHz, 800 MHz, 900 MHz, 1000 MHz, 1.10 GHz, 1.20 GHz
  available cpufreq governors: conservative, ondemand, userspace, powersave, performance, schedutil
  current policy: frequency should be within 1.20 GHz and 1.20 GHz.
                  The governor "ondemand" may decide which speed to use
                  within this range.
  current CPU frequency is 1.20 GHz.
  cpufreq stats: 600 MHz:53.41%, 700 MHz:1.34%, 800 MHz:10.82%, 900 MHz:3.29%, 1000 MHz:0.00%, 1.10 GHz:0.00%, 1.20 GHz:31.13%  (2871)
analyzing CPU 3:
  driver: cpufreq-dt
  CPUs which run at the same hardware frequency: 0 1 2 3
  CPUs which need to have their frequency coordinated by software: 0 1 2 3
  maximum transition latency: 0.00 ms.
  hardware limits: 600 MHz - 1.20 GHz
  available frequency steps: 600 MHz, 700 MHz, 800 MHz, 900 MHz, 1000 MHz, 1.10 GHz, 1.20 GHz
  available cpufreq governors: conservative, ondemand, userspace, powersave, performance, schedutil
  current policy: frequency should be within 1.20 GHz and 1.20 GHz.
                  The governor "ondemand" may decide which speed to use
                  within this range.
  current CPU frequency is 1.20 GHz.
  cpufreq stats: 600 MHz:53.41%, 700 MHz:1.34%, 800 MHz:10.82%, 900 MHz:3.29%, 1000 MHz:0.00%, 1.10 GHz:0.00%, 1.20 GHz:31.13%  (2871)

Notably the amount of time each of my cores has spent in each frequency is interesting - to save power the system is correctly choosing the lowest available frequency.

So, the next step here is exaclty what this tools hints to us : the cpu governor - this is a set of rules that code / profiles follow to set the frequencies of the CPU cores. The downside of this is that it takes a few clock cycles to get to the right frequency, so for low latency applications, there is a need to disable the governor. So, let's do that.

Here's some good reading material on what you're stepping into: https://www.kernel.org/doc/Documentation/cpu-freq/governors.txt

To set the frequency so there is no variance we need to set it to "performance", and set the max and min frequencies the same.

First, let's check into where the governor stores it's secrets:

[22:13:51] pi@raspi-flasher:/sys/bus/cpu/devices/cpu0/cpufreq $ ls
affected_cpus     cpuinfo_min_freq            scaling_available_frequencies  scaling_driver    scaling_min_freq
cpuinfo_cur_freq  cpuinfo_transition_latency  scaling_available_governors    scaling_governor  scaling_setspeed
cpuinfo_max_freq  related_cpus                scaling_cur_freq               scaling_max_freq  stats

So, as root cat any of these "files" , or echo > into them to set the parameters.

Here are a few handy aliases for the shell:

alias cpugetavail='cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_available_governors'
alias cpushowcurrent='cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor'
alias cpusethigh='echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor'

In use:

[22:21:30] pi@raspi-flasher:/sys/bus/cpu/devices/cpu0/cpufreq $ cpugetavail
conservative ondemand userspace powersave performance schedutil
conservative ondemand userspace powersave performance schedutil
conservative ondemand userspace powersave performance schedutil
conservative ondemand userspace powersave performance schedutil
[22:21:42] pi@raspi-flasher:/sys/bus/cpu/devices/cpu0/cpufreq $ cpushowcurrent
ondemand
ondemand
ondemand
ondemand
[22:21:46] pi@raspi-flasher:/sys/bus/cpu/devices/cpu0/cpufreq $ cpusethigh
performance
[22:21:51] pi@raspi-flasher:/sys/bus/cpu/devices/cpu0/cpufreq $ cpushowcurrent
performance
performance
performance
performance

Ok, so this is all well and good, but we DO need to check that max and min frequencies are the same:

pi@raspi-flasher:/sys/bus/cpu/devices/cpu0/cpufreq $ cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_max_freq
1200000
1200000
1200000
1200000
[22:24:49] pi@raspi-flasher:/sys/bus/cpu/devices/cpu0/cpufreq $ cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_min_freq
1200000
1200000
1200000
1200000

And they are set now ! Beautiful.

So, let's run out SPI test now ... and I'll drop some results into the next comment -

@beadon
Copy link
Author

beadon commented Sep 29, 2022

Ok, Success !

Old - not setting the core speed to be static, I was seeing poor results, as you did.

New - Set the core speeds to be static:

[22:28:42] pi@raspi-flasher:~/build $ time { ./spitest -S 16 -I 5000 -D /dev/spidev0.0 -s 100000; }
spi mode: 0x4
bits per word: 8
max speed: 100000 Hz (100 KHz)
rate: tx 96.9kbps, rx 96.9kbps
total: tx 78.1KB, rx 78.1KB

real    0m7.461s
user    0m0.019s
sys     0m0.195s

Is this the proper rate now?

Since it appears that timing is super critical here to measure things properly, then I would also advise using a deadline scheduler selected by kernel parameters to get very nearly realtime kernel behavior. Documentation : https://docs.kernel.org/scheduler/sched-deadline.html

Can you test these steps to see if the performance governor sets the speeds properly for you, and gives proper results? If we're a little bit off, then I think the deadline task scheduling is the next necessary step. I would also pin the process to the correct core so there is no cross-cpu talking to mess with timing, and then into sticking the process into the correct, closest memory addresses too (numa).

Tools required:

  1. update grub.conf kernel params for deadline
  2. cpu pinning done with taskset
  3. numa control, install the right tools:
    sudo apt install numa*

then use : numactl

  1. if we really do want realtime, then you'll also want to set the priority to near-realtime, I don't recommend all the way to -20, , but as long as you get to -10 , you get past a lot of the processes which would otherwise preempt you. tools - use : nice / renice read instructions carefully -- "-20" is realtime and will preempt even very important OS processes, and 20 is the other end of the scale -- basically last to be done.

I am open to ideas. What's next ?

My kernel:
[22:30:28] pi@raspi-flasher:~/build $ uname -na
Linux raspi-flasher 5.15.70-v7+ #1590 SMP Tue Sep 27 15:56:24 BST 2022 armv7l GNU/Linux

@David00
Copy link
Owner

David00 commented Sep 30, 2022

@beadon I'm on the same page with regards to the security concerns. Thank you for your input.

The information you've shared on CPU frequency and scheduling is awesome!! Thank you so much.

I believe Raspberry Pi's support of force_turbo=1 in /boot/config.txt accomplishes the same task of setting the min_freq equal to the max_freq. I just installed kernel 5.15.70 on my Pi 4 and ran cpufreq-info:

$ sudo cpufreq-info
cpufrequtils 008: cpufreq-info (C) Dominik Brodowski 2004-2009
Report errors and bugs to cpufreq@vger.kernel.org, please.
analyzing CPU 0:
  driver: cpufreq-dt
  CPUs which run at the same hardware frequency: 0 1 2 3
  CPUs which need to have their frequency coordinated by software: 0 1 2 3
  maximum transition latency: 0.00 ms.
  hardware limits: 1.80 GHz - 1.80 GHz
  available frequency steps: 1.80 GHz
  available cpufreq governors: conservative, ondemand, userspace, powersave, performance, schedutil
  current policy: frequency should be within 1.80 GHz and 1.80 GHz.
                  The governor "ondemand" may decide which speed to use
                  within this range.
  current CPU frequency is 1.80 GHz (asserted by call to hardware).
  cpufreq stats: 1.80 GHz:100.00%
analyzing CPU 1:
  driver: cpufreq-dt
  CPUs which run at the same hardware frequency: 0 1 2 3
  CPUs which need to have their frequency coordinated by software: 0 1 2 3
  maximum transition latency: 0.00 ms.
  hardware limits: 1.80 GHz - 1.80 GHz
  available frequency steps: 1.80 GHz
  available cpufreq governors: conservative, ondemand, userspace, powersave, performance, schedutil
  current policy: frequency should be within 1.80 GHz and 1.80 GHz.
                  The governor "ondemand" may decide which speed to use
                  within this range.
  current CPU frequency is 1.80 GHz (asserted by call to hardware).
  cpufreq stats: 1.80 GHz:100.00%
analyzing CPU 2:
  driver: cpufreq-dt
  CPUs which run at the same hardware frequency: 0 1 2 3
  CPUs which need to have their frequency coordinated by software: 0 1 2 3
  maximum transition latency: 0.00 ms.
  hardware limits: 1.80 GHz - 1.80 GHz
  available frequency steps: 1.80 GHz
  available cpufreq governors: conservative, ondemand, userspace, powersave, performance, schedutil
  current policy: frequency should be within 1.80 GHz and 1.80 GHz.
                  The governor "ondemand" may decide which speed to use
                  within this range.
  current CPU frequency is 1.80 GHz (asserted by call to hardware).
  cpufreq stats: 1.80 GHz:100.00%
analyzing CPU 3:
  driver: cpufreq-dt
  CPUs which run at the same hardware frequency: 0 1 2 3
  CPUs which need to have their frequency coordinated by software: 0 1 2 3
  maximum transition latency: 0.00 ms.
  hardware limits: 1.80 GHz - 1.80 GHz
  available frequency steps: 1.80 GHz
  available cpufreq governors: conservative, ondemand, userspace, powersave, performance, schedutil
  current policy: frequency should be within 1.80 GHz and 1.80 GHz.
                  The governor "ondemand" may decide which speed to use
                  within this range.
  current CPU frequency is 1.80 GHz (asserted by call to hardware).
  cpufreq stats: 1.80 GHz:100.00%

So my Pi 4's cores are running at 1.8GHz constantly. My tests from spidev_test.c yield expected results:

$ sudo ./spidev_test -S 16 -I 5000 -D /dev/spidev0.0 -s 100000
spi mode: 0x4
bits per word: 8
max speed: 100000 Hz (100 KHz)
rate: tx 101.0kbps, rx 101.0kbps
total: tx 78.1KB, rx 78.1KB

So the kernel is working fine with the C driver. However, my project doesn't use the C driver - it uses the py-spidev library, which is a C Extension for Python.

The problem is that v5 kernels get half the sample rate compared to v4 kernels when using the py-spidev module. Here's a quick test using py-spidev's xfer function, which is a bidirectional r/w SPI transaction:

sudo python3
from time import time
import spidev

spi = spidev.SpiDev()
spi.open(0, 0)
spi.max_speed_hz = 1750000

def get_timed_samples():	
	num_samples = 10000
	start = time()
	for _ in range(num_samples):
		spi.xfer([1, 8 << 4, 0])
		
		
	end = time()
	duration = (end - start)
	sample_rate = num_samples / duration 
	return sample_rate
	

if __name__ == '__main__':
	for _ in range(10):
		sample_rate = get_timed_samples()
		print(f"Sample Rate: {round(sample_rate, 2)} SPS")

You'll have to pip install spidev (and run as root) for the above code. Here's output from the same Pi 4 demonstrated above, using Linux kerneltesting 5.15.70-v7l+ #1589 SMP Mon Sep 26 13:00:21 BST 2022 armv7l GNU/Linux:

Sample Rate: 18274.64 SPS
Sample Rate: 18300.91 SPS
Sample Rate: 17955.48 SPS
Sample Rate: 17997.5 SPS
Sample Rate: 18053.72 SPS
Sample Rate: 18220.79 SPS
Sample Rate: 18534.71 SPS
Sample Rate: 18552.01 SPS
Sample Rate: 18279.41 SPS
Sample Rate: 18154.35 SPS

On a v4 kernel, with the same test code, the sample rate is over 30K SPS. So, it seems like a kernel issue, as that's the only variable (besides the major Python version, but I've already tested Python 3.9 on an a v4 kernel and Python 3.7 on a v5 kernel). The sample rate test results of spidev do not change depending on the Python version - only on the kernel version.

But, as we've both tested above using the C driver, the v5 kernels are capable of reaching the intended bandwidth.

So, I am left wondering, why is the performance of spidev changing between v4 and v5 kernels? Here is the output of the script when ran on Linux raspberrypi 4.19.97-v7l+ #1294 SMP Thu Jan 30 13:21:14 GMT 2020 armv7l GNU/Linux:

$ sudo python3 test.py
Sample Rate: 31730.9 SPS
Sample Rate: 33752.95 SPS
Sample Rate: 33745.0 SPS
Sample Rate: 33743.53 SPS
Sample Rate: 33547.83 SPS
Sample Rate: 33543.75 SPS
Sample Rate: 33537.29 SPS
Sample Rate: 33118.72 SPS
Sample Rate: 33074.61 SPS
Sample Rate: 33078.73 SPS

Note: To get this old Pi OS Lite build to run on a recent Pi 4, I had to upgrade the GPU firmware files manually as described in my comment here: #53 (comment)

@beadon
Copy link
Author

beadon commented Oct 1, 2022

Ok, thanks, nice results, good test code.

Since we are talking about spidev, then we have to remember that python is ultimately 'single-threaded' - or at least it is using GIL in this example. So that is sitting in the back of my head when looking at the implementation, the blocking, I/O transfers, ring buffers, and the OS too - which is no longer using jiffies (tickless kernels, will re-confirm below) -- these are some of the variables working against us here.

[21:41:13] pi@raspi-flasher:~/build/rpi-power-monitor $ sudo pip3 install spidev
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting spidev
  Downloading https://www.piwheels.org/simple/spidev/spidev-3.5-cp39-cp39-linux_armv7l.whl (39 kB)
Installing collected packages: spidev
Successfully installed spidev-3.5
[21:41:49] pi@raspi-flasher:~/build/rpi-power-monitor $ sudo python3
Python 3.9.2 (default, Mar 12 2021, 04:06:34)
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from time import time
>>> import spidev
        end = time()
        duration = (end - start)
        sample_rate = num_samples / duration
        return sample_rate

if __name__ == '__main__':
        for _ in range(10):
                sample_rate = get_timed_samples()
                print(f"Sample Rate: {round(sample_rate, 2)} SPS")
>>>
>>> spi = spidev.SpiDev()
>>> spi.open(0, 0)
>>> spi.max_speed_hz = 1750000
>>>
>>> def get_timed_samples():
...     num_samples = 10000
...     start = time()
...     for _ in range(num_samples):
...             spi.xfer([1, 8 << 4, 0])
...
...
...     end = time()
...     duration = (end - start)
...     sample_rate = num_samples / duration
...     return sample_rate
...
...
 >>> if __name__ == '__main__':
...     for _ in range(10):
...             sample_rate = get_timed_samples()
...             print(f"Sample Rate: {round(sample_rate, 2)} SPS")
...

Sample Rate: 9841.81 SPS
Sample Rate: 8304.12 SPS
Sample Rate: 8318.74 SPS
Sample Rate: 10904.61 SPS
Sample Rate: 15929.76 SPS
Sample Rate: 15963.23 SPS
Sample Rate: 15977.75 SPS
Sample Rate: 14096.43 SPS
Sample Rate: 8303.19 SPS
Sample Rate: 8265.57 SPS

So this is going to start me down a path of "Why is this so incredibly variable ?!"

Possible options to chase;

  1. strace the results, see if there are any timestamp irregularities in the report -- unlikely, but possible, this would incriminate the GIL.
  2. pin everything down , taskset, numa, priority and deadline.
  3. change behavior with ticks in the kernel, because this is a key portion of a RT kernel, and I believe the most likely culprit.
  4. interrupts could be getting spread across a bunch of cores, resulting in strange or delayed behavior. We have to look at irqbalance.

Notes for later:

strace on the python run with test code. This is quite verbose, and will take some time to get a clear picture of what's happening -
fileout.3374.gz

moving closer to a realtime kernel is going to be chasing the ticks. quick notes - /proc/timer_list has lots of detail to chew on.

check this out -

[22:06:06] pi@raspi-flasher:~/build/rpi-power-monitor $ sudo cat /proc/timer_list
Timer List Version: v0.9
HRTIMER_MAX_CLOCK_BASES: 8
now at 9351303491151 nsecs

cpu: 0
 clock 0:
  .base:       6018c719
  .index:      0
  .resolution: 1 nsecs
  .get_time:   ktime_get
  .offset:     0 nsecs
active timers:
 #0: <73f725fa>, tick_sched_timer, S:01
 # expires at 9351340000000-9351340000000 nsecs [in 36508849 to 36508849 nsecs]
 #1: <4d09504b>, pm_suspend_timer_fn, S:01
 # expires at 9350969640144-9351719640144 nsecs [in -333851007 to 416148993 nsecs]
 #2: <5369abdf>, hrtimer_wakeup, S:01
 # expires at 9352110237646-9352140237645 nsecs [in 806746495 to 836746494 nsecs]
 #3: <7f3bec8b>, it_real_fn, S:01
 # expires at 9352213424888-9352213424888 nsecs [in 909933737 to 909933737 nsecs]
 #4: <6e5ef742>, hrtimer_wakeup, S:01
 # expires at 9362709989366-9362710039366 nsecs [in 11406498215 to 11406548215 nsecs]
 #5: <9c91b678>, hrtimer_wakeup, S:01
 # expires at 9370737975538-9370738025538 nsecs [in 19434484387 to 19434534387 nsecs]
 #6: <6fa09350>, hrtimer_wakeup, S:01
 # expires at 9378410802352-9378440772351 nsecs [in 27107311201 to 27137281200 nsecs]
 #7: <6fcc27c5>, hrtimer_wakeup, S:01
 # expires at 9394439183011-9394490911991 nsecs [in 43135691860 to 43187420840 nsecs]
 #8: <6b659e8d>, hrtimer_wakeup, S:01
 # expires at 9613692348695-9613792348695 nsecs [in 262388857544 to 262488857544 nsecs]
 #9: <f4e3b975>, hrtimer_wakeup, S:01
 # expires at 10651164240137-10651164290137 nsecs [in 1299860748986 to 1299860798986 nsecs]
 #10: <8819d60e>, hrtimer_wakeup, S:01
 # expires at 10816590579841-10816590629841 nsecs [in 1465287088690 to 1465287138690 nsecs]
 #11: <2c5906e6>, sched_clock_poll, S:01
 # expires at 13194139533234-13194139533234 nsecs [in 3842836042083 to 3842836042083 nsecs]
 clock 1:
  .base:       eb9a13f4
  .index:      1
  .resolution: 1 nsecs
  .get_time:   ktime_get_real
  .offset:     1664580622321878087 nsecs
active timers:
 #0: <7ca17cb6>, timerfd_tmrproc, S:01
 # expires at 2147483647000000000-2147483647000000000 nsecs [in 482893673374630762 to 482893673374630762 nsecs]
 clock 2:
  .base:       cf3b2890
  .index:      2
  .resolution: 1 nsecs
  .get_time:   ktime_get_boottime
  .offset:     0 nsecs
active timers:
 clock 3:
  .base:       37256c3b
  .index:      3
  .resolution: 1 nsecs
  .get_time:   ktime_get_clocktai
  .offset:     1664580622321878087 nsecs
active timers:
 clock 4:
  .base:       0a42fd42
  .index:      4
  .resolution: 1 nsecs
  .get_time:   ktime_get
  .offset:     0 nsecs
active timers:
 clock 5:
  .base:       e1e315d6
  .index:      5
  .resolution: 1 nsecs
  .get_time:   ktime_get_real
  .offset:     1664580622321878087 nsecs
active timers:
 clock 6:
  .base:       8b321613
  .index:      6
  .resolution: 1 nsecs
  .get_time:   ktime_get_boottime
  .offset:     0 nsecs
active timers:
 clock 7:
  .base:       428fc49f
  .index:      7
  .resolution: 1 nsecs
  .get_time:   ktime_get_clocktai
  .offset:     1664580622321878087 nsecs
active timers:
  .expires_next   : 9351340000000 nsecs
  .hres_active    : 1
  .nr_events      : 160910
  .nr_retries     : 915
  .nr_hangs       : 0
  .max_hang_time  : 0
  .nohz_mode      : 2
  .last_tick      : 9351310000000 nsecs
  .tick_stopped   : 1
  .idle_jiffies   : 905131
  .idle_calls     : 2972332
  .idle_sleeps    : 2761282
  .idle_entrytime : 9351303620734 nsecs
  .idle_waketime  : 9351303532870 nsecs
  .idle_exittime  : 9351303598807 nsecs
  .idle_sleeptime : 9072113393199 nsecs
  .iowait_sleeptime: 17341580528 nsecs
  .last_jiffies   : 905131
  .next_timer     : 9351340000000
  .idle_expires   : 9351340000000 nsecs
jiffies: 905131

cpu: 1
 clock 0:
  .base:       2282c734
  .index:      0
  .resolution: 1 nsecs
  .get_time:   ktime_get
  .offset:     0 nsecs
active timers:
 #0: <eb16b943>, tick_sched_timer, S:01
 # expires at 9351320000000-9351320000000 nsecs [in 16508849 to 16508849 nsecs]
 #1: <12443b4c>, hrtimer_wakeup, S:01
 # expires at 9351563968055-9351564018055 nsecs [in 260476904 to 260526904 nsecs]
 #2: <edce5292>, hrtimer_wakeup, S:01
 # expires at 9351667580394-9351667630394 nsecs [in 364089243 to 364139243 nsecs]
 #3: <a2b97cab>, hrtimer_wakeup, S:01
 # expires at 9351667387706-9351677387702 nsecs [in 363896555 to 373896551 nsecs]
 #4: <3aba7925>, hrtimer_wakeup, S:01
 # expires at 9354191538419-9354191588419 nsecs [in 2888047268 to 2888097268 nsecs]
 #5: <000341d8>, hrtimer_wakeup, S:01
 # expires at 9378410508499-9378440478497 nsecs [in 27107017348 to 27136987346 nsecs]
 #6: <88fed497>, hrtimer_wakeup, S:01
 # expires at 9378740035392-9378740085392 nsecs [in 27436544241 to 27436594241 nsecs]
 #7: <614db611>, hrtimer_wakeup, S:01
 # expires at 9381200486671-9381230462670 nsecs [in 29896995520 to 29926971519 nsecs]
 #8: <ec3e74d4>, hrtimer_wakeup, S:01
 # expires at 9384182846628-9384242846626 nsecs [in 32879355477 to 32939355475 nsecs]
 #9: <0f26a9cc>, hrtimer_wakeup, S:01
 # expires at 9411292971238-9411293021238 nsecs [in 59989480087 to 59989530087 nsecs]
 #10: <2dd60fbb>, timerfd_tmrproc, S:01
 # expires at 9427887207000-9427887207000 nsecs [in 76583715849 to 76583715849 nsecs]
 #11: <99001d09>, timerfd_tmrproc, S:01
 # expires at 9428137207000-9428137207000 nsecs [in 76833715849 to 76833715849 nsecs]
 #12: <f8d181c7>, hrtimer_wakeup, S:01
 # expires at 9615000623811-9615100623811 nsecs [in 263697132660 to 263797132660 nsecs]
 #13: <32bcf811>, it_real_fn, S:01
 # expires at 10822712786138-10822712786138 nsecs [in 1471409294987 to 1471409294987 nsecs]
 #14: <4d5942cf>, hrtimer_wakeup, S:01
 # expires at 28848330639358-28848330689358 nsecs [in 19497027148207 to 19497027198207 nsecs]
 clock 1:
  .base:       b98a5e95
  .index:      1
  .resolution: 1 nsecs
  .get_time:   ktime_get_real
  .offset:     1664580622321878087 nsecs
active timers:
 clock 2:
  .base:       6388661d
  .index:      2
  .resolution: 1 nsecs
  .get_time:   ktime_get_boottime
  .offset:     0 nsecs
active timers:
 clock 3:
  .base:       95a804d9
  .index:      3
  .resolution: 1 nsecs
  .get_time:   ktime_get_clocktai
  .offset:     1664580622321878087 nsecs
active timers:
 clock 4:
  .base:       38b5573e
  .index:      4
  .resolution: 1 nsecs
  .get_time:   ktime_get
  .offset:     0 nsecs
active timers:
 clock 5:
  .base:       c118cb43
  .index:      5
  .resolution: 1 nsecs
  .get_time:   ktime_get_real
  .offset:     1664580622321878087 nsecs
active timers:
 clock 6:
  .base:       10f54dd0
  .index:      6
  .resolution: 1 nsecs
  .get_time:   ktime_get_boottime
  .offset:     0 nsecs
active timers:
 clock 7:
  .base:       d261c7b2
  .index:      7
  .resolution: 1 nsecs
  .get_time:   ktime_get_clocktai
  .offset:     1664580622321878087 nsecs
active timers:
  .expires_next   : 9351320000000 nsecs
  .hres_active    : 1
  .nr_events      : 169603
  .nr_retries     : 518
  .nr_hangs       : 0
  .max_hang_time  : 0
  .nohz_mode      : 2
  .last_tick      : 9351300000000 nsecs
  .tick_stopped   : 0
  .idle_jiffies   : 905130
  .idle_calls     : 73700031
  .idle_sleeps    : 72757137
  .idle_entrytime : 9351296813517 nsecs
  .idle_waketime  : 9351292045876 nsecs
  .idle_exittime  : 9351296813517 nsecs
  .idle_sleeptime : 9119679409052 nsecs
  .iowait_sleeptime: 12990811528 nsecs
  .last_jiffies   : 905130
  .next_timer     : 9351410000000
  .idle_expires   : 9351410000000 nsecs
jiffies: 905132

cpu: 2
 clock 0:
  .base:       0fda3cbe
  .index:      0
  .resolution: 1 nsecs
  .get_time:   ktime_get
  .offset:     0 nsecs
active timers:
 #0: <c5ec112f>, tick_sched_timer, S:01
 # expires at 9351320000000-9351320000000 nsecs [in 16508849 to 16508849 nsecs]
 #1: <159d1e3d>, hrtimer_wakeup, S:01
 # expires at 9351345250559-9351345413557 nsecs [in 41759408 to 41922406 nsecs]
 #2: <cbfe204e>, hrtimer_wakeup, S:01
 # expires at 9351346195472-9351346245472 nsecs [in 42704321 to 42754321 nsecs]
 #3: <77cfc479>, timerfd_tmrproc, S:01
 # expires at 9351637207000-9351637207000 nsecs [in 333715849 to 333715849 nsecs]
 #4: <c85dd6d6>, hrtimer_wakeup, S:01
 # expires at 9352032824995-9352033598993 nsecs [in 729333844 to 730107842 nsecs]
 #5: <63ee649d>, hrtimer_wakeup, S:01
 # expires at 9354244044995-9354247474993 nsecs [in 2940553844 to 2943983842 nsecs]
 #6: <db9a6f5f>, hrtimer_wakeup, S:01
 # expires at 9354540342582-9354540392582 nsecs [in 3236851431 to 3236901431 nsecs]
 #7: <8f7be5a5>, hrtimer_wakeup, S:01
 # expires at 9354539594534-9354549593532 nsecs [in 3236103383 to 3246102381 nsecs]
 #8: <a6c5fbaf>, timerfd_tmrproc, S:01
 # expires at 9367887207000-9367887207000 nsecs [in 16583715849 to 16583715849 nsecs]
 #9: <e8e9eb5f>, hrtimer_wakeup, S:01
 # expires at 9378410818344-9378410868344 nsecs [in 27107327193 to 27107377193 nsecs]
 #10: <7bbb0d1a>, hrtimer_wakeup, S:01
 # expires at 9378410825426-9378410875426 nsecs [in 27107334275 to 27107384275 nsecs]
 #11: <2209aa14>, timerfd_tmrproc, S:01
 # expires at 9427887207000-9427887207000 nsecs [in 76583715849 to 76583715849 nsecs]
 #12: <8ddbcb2f>, hrtimer_wakeup, S:01
 # expires at 9979571035076-9979571085076 nsecs [in 628267543925 to 628267593925 nsecs]
 #13: <8962d1d9>, hrtimer_wakeup, S:01
 # expires at 10816225728826-10816225778826 nsecs [in 1464922237675 to 1464922287675 nsecs]
 clock 1:
  .base:       deb44c36
  .index:      1
  .resolution: 1 nsecs
  .get_time:   ktime_get_real
  .offset:     1664580622321878087 nsecs
active timers:
 #0: <83b7faee>, timerfd_tmrproc, S:01
 # expires at 1664590567887207000-1664590567887207000 nsecs [in 594261837762 to 594261837762 nsecs]
 #1: <389ecf2b>, timerfd_tmrproc, S:01
 # expires at 2147483647000000000-2147483647000000000 nsecs [in 482893673374630762 to 482893673374630762 nsecs]
 clock 2:
  .base:       d74e8912
  .index:      2
  .resolution: 1 nsecs
  .get_time:   ktime_get_boottime
  .offset:     0 nsecs
active timers:
 #0: <abf179bd>, timerfd_tmrproc, S:01
 # expires at 10252637207000-10252637207000 nsecs [in 901333715849 to 901333715849 nsecs]
 clock 3:
  .base:       4bad3b2c
  .index:      3
  .resolution: 1 nsecs
  .get_time:   ktime_get_clocktai
  .offset:     1664580622321878087 nsecs
active timers:
 clock 4:
  .base:       b32eff94
  .index:      4
  .resolution: 1 nsecs
  .get_time:   ktime_get
  .offset:     0 nsecs
active timers:
 clock 5:
  .base:       2ee21205
  .index:      5
  .resolution: 1 nsecs
  .get_time:   ktime_get_real
  .offset:     1664580622321878087 nsecs
active timers:
 clock 6:
  .base:       b3adb3b7
  .index:      6
  .resolution: 1 nsecs
  .get_time:   ktime_get_boottime
  .offset:     0 nsecs
active timers:
 clock 7:
  .base:       6f3f0f6c
  .index:      7
  .resolution: 1 nsecs
  .get_time:   ktime_get_clocktai
  .offset:     1664580622321878087 nsecs
active timers:
  .expires_next   : 9351320000000 nsecs
  .hres_active    : 1
  .nr_events      : 157326
  .nr_retries     : 645
  .nr_hangs       : 0
  .max_hang_time  : 0
  .nohz_mode      : 2
  .last_tick      : 9351270000000 nsecs
  .tick_stopped   : 0
  .idle_jiffies   : 905127
  .idle_calls     : 390792
  .idle_sleeps    : 328085
  .idle_entrytime : 9351310171494 nsecs
  .idle_waketime  : 9351292127959 nsecs
  .idle_exittime  : 9351292148584 nsecs
  .idle_sleeptime : 9138816985329 nsecs
  .iowait_sleeptime: 16436753683 nsecs
  .last_jiffies   : 905132
  .next_timer     : 9351590000000
  .idle_expires   : 9351590000000 nsecs
jiffies: 905132

cpu: 3
 clock 0:
  .base:       82ef5135
  .index:      0
  .resolution: 1 nsecs
  .get_time:   ktime_get
  .offset:     0 nsecs
active timers:
 #0: <f07b6026>, tick_sched_timer, S:01
 # expires at 9351330000000-9351330000000 nsecs [in 26508849 to 26508849 nsecs]
 #1: <9a30ce0c>, hrtimer_wakeup, S:01
 # expires at 9351745105297-9351750105294 nsecs [in 441614146 to 446614143 nsecs]
 #2: <c0a056d7>, hrtimer_wakeup, S:01
 # expires at 9352033746233-9352033796233 nsecs [in 730255082 to 730305082 nsecs]
 #3: <466ebdc4>, hrtimer_wakeup, S:01
 # expires at 9352120571612-9352130571606 nsecs [in 817080461 to 827080455 nsecs]
 #4: <41930f5b>, hrtimer_wakeup, S:01
 # expires at 9378454488656-9378454538656 nsecs [in 27150997505 to 27151047505 nsecs]
 #5: <24dea29f>, hrtimer_wakeup, S:01
 # expires at 9378650607243-9378650657243 nsecs [in 27347116092 to 27347166092 nsecs]
 #6: <8adb6ff8>, hrtimer_wakeup, S:01
 # expires at 9398515855072-9398515905072 nsecs [in 47212363921 to 47212413921 nsecs]
 #7: <0161b5b9>, hrtimer_wakeup, S:01
 # expires at 9398689273101-9398689323101 nsecs [in 47385781950 to 47385831950 nsecs]
 #8: <a4c9a492>, hrtimer_wakeup, S:01
 # expires at 86449329610136-86449329660136 nsecs [in 77098026118985 to 77098026168985 nsecs]
 #9: <922f3695>, hrtimer_wakeup, S:01
 # expires at 2147483663453001763-2147483663453051763 nsecs [in 2147474312149510612 to 2147474312149560612 nsecs]
 clock 1:
  .base:       f041e099
  .index:      1
  .resolution: 1 nsecs
  .get_time:   ktime_get_real
  .offset:     1664580622321878087 nsecs
active timers:
 #0: <ff1fa5da>, timerfd_tmrproc, S:01
 # expires at 2147483647000000000-2147483647000000000 nsecs [in 482893673374630762 to 482893673374630762 nsecs]
 clock 2:
  .base:       3d2ffce1
  .index:      2
  .resolution: 1 nsecs
  .get_time:   ktime_get_boottime
  .offset:     0 nsecs
active timers:
 clock 3:
  .base:       dd14c51c
  .index:      3
  .resolution: 1 nsecs
  .get_time:   ktime_get_clocktai
  .offset:     1664580622321878087 nsecs
active timers:
 clock 4:
  .base:       deec78c6
  .index:      4
  .resolution: 1 nsecs
  .get_time:   ktime_get
  .offset:     0 nsecs
active timers:
 clock 5:
  .base:       2683733e
  .index:      5
  .resolution: 1 nsecs
  .get_time:   ktime_get_real
  .offset:     1664580622321878087 nsecs
active timers:
 clock 6:
  .base:       c106a571
  .index:      6
  .resolution: 1 nsecs
  .get_time:   ktime_get_boottime
  .offset:     0 nsecs
active timers:
 clock 7:
  .base:       63e7a7bb
  .index:      7
  .resolution: 1 nsecs
  .get_time:   ktime_get_clocktai
  .offset:     1664580622321878087 nsecs
active timers:
  .expires_next   : 9351330000000 nsecs
  .hres_active    : 1
  .nr_events      : 213475
  .nr_retries     : 831
  .nr_hangs       : 0
  .max_hang_time  : 0
  .nohz_mode      : 2
  .last_tick      : 9351300000000 nsecs
  .tick_stopped   : 0
  .idle_jiffies   : 905130
  .idle_calls     : 546410
  .idle_sleeps    : 452958
  .idle_entrytime : 9351320114951 nsecs
  .idle_waketime  : 9351291879731 nsecs
  .idle_exittime  : 9351291896397 nsecs
  .idle_sleeptime : 8682525491565 nsecs
  .iowait_sleeptime: 8121844281 nsecs
  .last_jiffies   : 905133
  .next_timer     : 9351400000000
  .idle_expires   : 9351400000000 nsecs
jiffies: 905133

Tick Device: mode:     1
Broadcast device
Clock Event Device: bc_hrtimer
 max_delta_ns:   9223372036854775807
 min_delta_ns:   1
 mult:           1
 shift:          0
 mode:           1
 next_event:     9223372036854775807 nsecs
 set_next_event: 0x0
 shutdown:       bc_shutdown
 event_handler:  tick_handle_oneshot_broadcast

 retries:        0

tick_broadcast_mask: 0
tick_broadcast_oneshot_mask: 0

Tick Device: mode:     1
Per CPU device: 0
Clock Event Device: arch_sys_timer
 max_delta_ns:   111848106728
 min_delta_ns:   1000
 mult:           82463372
 shift:          32
 mode:           3
 next_event:     9351340000000 nsecs
 set_next_event: arch_timer_set_next_event_phys
 shutdown:       arch_timer_shutdown_phys
 oneshot stopped: arch_timer_shutdown_phys
 event_handler:  hrtimer_interrupt

 retries:        355
Wakeup Device: <NULL>

Tick Device: mode:     1
Per CPU device: 1
Clock Event Device: arch_sys_timer
 max_delta_ns:   111848106728
 min_delta_ns:   1000
 mult:           82463372
 shift:          32
 mode:           3
 next_event:     9351340000000 nsecs
 set_next_event: arch_timer_set_next_event_phys
 shutdown:       arch_timer_shutdown_phys
 oneshot stopped: arch_timer_shutdown_phys
 event_handler:  hrtimer_interrupt

 retries:        648
Wakeup Device: <NULL>

Tick Device: mode:     1
Per CPU device: 2
Clock Event Device: arch_sys_timer
 max_delta_ns:   111848106728
 min_delta_ns:   1000
 mult:           82463372
 shift:          32
 mode:           3
 next_event:     9351340000000 nsecs
 set_next_event: arch_timer_set_next_event_phys
 shutdown:       arch_timer_shutdown_phys
 oneshot stopped: arch_timer_shutdown_phys
 event_handler:  hrtimer_interrupt

 retries:        118
Wakeup Device: <NULL>

Tick Device: mode:     1
Per CPU device: 3
Clock Event Device: arch_sys_timer
 max_delta_ns:   111848106728
 min_delta_ns:   1000
 mult:           82463372
 shift:          32
 mode:           3
 next_event:     9351340000000 nsecs
 set_next_event: arch_timer_set_next_event_phys
 shutdown:       arch_timer_shutdown_phys
 oneshot stopped: arch_timer_shutdown_phys
 event_handler:  hrtimer_interrupt

 retries:        5126
Wakeup Device: <NULL>

There is a lot to absorb here. I believe reading through the strace will show more information. Is it possible that there's a different interface (file handle class / type , etc) that python is using to access SPI, rather than the C module. Since the code for spidev has not changed since 2020, there may be some kind of legacy interface in this kernel jump kept around for spidev, or others to hit...

More tools to extract behavior --

  1. Flame Charts -- https://github.com/brendangregg/FlameGraph -- because I think the code in python might be blocking where it shouldn't , and this will find the time each call to the OS or other system filehandles takes.
  2. deep instrumentation of the spidev modules : https://pypi.org/project/spidev/ , https://www.sigmdel.ca/michel/ha/rpi/spi_on_pi_en.html
  3. somehow getting 'perf' to run on the raspi - just need to sped some time with the documentation, this is also very handy.

I do have a question - why the hard set speed "spi.max_speed_hz = 1750000" ? This raises red flags for me because it looks like a magic number. This is 1.750 MHz , shouldn't we be sampling somewhere in the khz range ?

@beadon
Copy link
Author

beadon commented Oct 1, 2022

On re-reading your comments, I think we are dealing with at least 2 different problems.

  1. The CPU speed variance, which you have worked around by fixing the turbo speed parameter. The speed governor I have set appears to be providing the same results in the C implementation, but since our setups differ this could cause confusion in the future. I will set the force_turbo=1 in /boot/config.txt to align and re-run tests to confirm the same setup.

  2. The spidev driver/module and the python implementation for the monitor :: While this is 2 different things, I'd like to keep driving a wedge between these and the OS. Currently suspicious of the spidev implementation, not the Linux OS kernel. This is just a starting point. strace results to read ...

@beadon
Copy link
Author

beadon commented Oct 1, 2022

For later reference - perf for the 5.15 kernel is not available yet in the apt package manager. linux-perf-5.10 is the latest. A reminder we may want to ping the package maintainer about this.

@David00
Copy link
Owner

David00 commented Oct 1, 2022

Wow, this is going to get pretty deep, but I'm all for it. It seems there is lots for me to learn here.

I don't think the GIL is the culprit because everything is running in a single thread. If the spidev SPI driver was being passed between threads, the GIL would manage the control of the driver which would impact the timings on when transfers are initiated.

In the driver's xfer2 function, which I'm using in my code, there are several uses of the macro Py_BEGIN_ALLOW_THREADS. This would allow the GIL to transfer ownership of an object, but without any other Python threads running, I don't think this is going on. Also, this wouldn't explain why, in the same test code, we see differences between v4 and v5 kernels.

There is a lot going on in the SPI xfer2 function where delays could be introduced. I think increasing the priority of the process like you've suggested might be a good place to start. I will also setup some tests to capture a few sequences of the SPI clock over several SPI transactions and measure the time between when the transactions start. (I'm not suggesting that the clock itself is varying - just that the time between the start of each transaction is varying)

I do have a question - why the hard set speed "spi.max_speed_hz = 1750000" ? This raises red flags for me because it looks like a magic number. This is 1.750 MHz , shouldn't we be sampling somewhere in the khz range ?

1.75 MHz is the SPI clock speed only - and to do a single 10 bit transfer, it takes ~ 18 clock cycles (there's also some overhead in pulling the CS pin high to prepare for the transfer). I selected 1.75 MHz by interpolating (and then testing) between the datasheet clock frequency boundaries, i.e. 1.35MHz at 2.7V and 3.6 MHz at 5V. Since I'm running the ADC at 3.3V, the clock upper limit is somewhere on the curve from 1.35MHz to 3.6MHz. When we see 30K SPS in the test code, that's split between 7 channels, which gives an effective per-channel sampling rate of 4.28 kSPS/kHz.

I'll see if I can think of any other tests to do with my scope. Also, I'm thinking of altering the Python test code above to track the sample rate over an increasing range of samples to capture in each batch. For instance, find the average sample time when capturing only 1 sample, then 10 samples, 50, 100, 200, etc, and see if the sample rate stays the same, or if there's a linear/logarithmic relationship. This would help us understand if the batch size has much to do with the effective sample rate.

@beadon
Copy link
Author

beadon commented Oct 2, 2022

Ok it looks like the perf tool is not available with the 5.15 kernel yet on raspbian. I've pinged the rasbian channel on liberia (IRC). There is a debian package for it updated as of Oct 2nd (yes, the future! Because the server is in a timezone ahead of EST), but we may need to compile it ourselves. Here's the code - https://github.com/torvalds/linux/tree/master/tools/perf -- snagging the tree -- mostly for kicks :) . Reference : https://linux-packages.com/debian/package/linux-perf-515

@beadon
Copy link
Author

beadon commented Oct 2, 2022

It appears that the C driver will only use DMA mode to communicate with SPI when a communication is over 96 bytes -- reference: https://github.com/raspberrypi/linux/blob/rpi-4.19.y/drivers/spi/spi-bcm2835.c#L77

Additional reading - https://iosoft.blog/2020/06/11/fast-data-capture-raspberry-pi/

If the first is true, then there might be something that spidev is doing to stay under that limit (assuming it's stacked on top of the driver at a lower level). If you have the scope up, I assume that the behavior looks very different between the C and python versions - does anything stand out ?

taking a close look at spidev, bits_per_word .

@beadon
Copy link
Author

beadon commented Oct 2, 2022

Interesting -- delay_usecs is used inside spidev, but it's been removed from the kernel ( https://www.spinics.net/lists/kernel/msg3864354.html ). Maybe relevant.

@David00
Copy link
Owner

David00 commented Oct 4, 2022

Hi @beadon, great find on delay_usecs being removed from the kernel. I think a PR for py-spidev is in order (eventually!).

For DMA, that's an interesting note, however it shouldn't apply here just because the size of the transfers is, I believe, a 3 byte write followed by a 10 bit read. (I haven't validated that on the serial lines with my scope though).

I modified my test script shared above to pull pin 16 high when a batch of sampling starts, and to set pin 16 low when sampling is done. I've also set num_samples = 2000 to match what I've set in this project for each sample cycle (2000 per iteration). I pushed the modified test script to a new branch so you can see what it's doing. If you want to run it, you'll need the RPi.GPIO library.

Updated test script: https://github.com/David00/rpi-power-monitor/blob/kernel-testing/test.py

Here is an annotated capture of the scope screen which explains how I've setup this test.

Annotated Sequence Start

Yellow: Channel 1 - attached to header pin 16 (not GPIO 16 - the actual pin number 16 which is GPIO 23). Referred to as SCOPE_PIN in the annotation above.
Purple: Channel 2 - attached to SPI clock

So, channel 1 stays high while 2k samples are captured. My intent with this test is to measure both the variance of, and the duration the time that pin 16 is pulled high, which would give us the jitter between calls to get_timed_samples(). Then, if I repeat the test on the working v4 kernel, we can see if the jitter and/or the duration that pin 16 stays high is shorter duration/less jitter. The pin 16 being pulled high is just a flag for the start of the process - the actual delay I'm interested in is the time between the start of each group of clock cycles because I think any variance or noticeable differences here will have the biggest impact on the effective sample rate.

Speaking of the clock, it looks odd in the photo above, so I'm going to look into this more too. The clock should not be broken up into three groups of 8 AFAIK. But then again, I haven't actually dug that deep into the Linux SPI driver to see how the transfer is done with ioctl... yet. I'm stopping here for now, which is the creation of the initial TX buffer that gets sent to the ADC for channel selection and mode. This code is casting my provided values to 8 bit unsigned ints upon adding them to the tx buffer. Here's my call to xfer2, so perhaps that's why we see three groups of 8 clocks. I am optimistic this test will provide direction on where to continue troubleshooting.

Here is a close up of the clocks, just FYI.
Strange Clocks

The screenshots above are from readings taken on my v5.15 board. I'll do the same test with my v4.19 board.

I have exported the raw scope data to CSV (7M lines, 215MB) but I'll hold off the analysis until I repeat this test on the v4.19 kernel tomorrow, just in case there's any obvious sign of the issue after that test. Plus, it's late.

@David00
Copy link
Owner

David00 commented Oct 5, 2022

Ok, the comparison on the scope between the two kernels uncovered where the sample rate is differing. Now we just need to figure out why.

In 5.15.70-v7l+, the time between a single collection is ~ 59 µs. The freq of SCOPE_PIN is 9.49Hz. (So 9.49 times per second, get_timed_samples() is called to collect 2k samples, giving us an effective sampling rate of 9.49 * 2000 = ~18.9 kSPS).

In 4.19.97-v7l+, the time between a single collection is ~ 37 µs. The freq of SCOPE_PIN is 16.69 Hz, so... ~ 33 kSPS, which is pretty close to what we see printed in the test.

By time between single collection, I mean the time between each call to xfer here:
https://github.com/David00/rpi-power-monitor/blob/kernel-testing/test.py#L30

V5.15:

image

V4.19

Note how in the following photo, the distance between each group of clocks seems to vary. The one I selected to measure seems to have a larger gap than the batch of clocks before and after. I think the small variance is not related to the issue and could possibly be improved with the process scheduling that you suggested.

image

Any idea why Python might be taking longer to execute the calls in the v5 kernel? My suspicion leads me to the inner workings of ioctl.c but I'm not too sure how to debug beyond this point.

@David00
Copy link
Owner

David00 commented Oct 17, 2022

Hi @beadon, are you available to continue troubleshooting this? I keep thinking about it and it's one of those problems that's going to continue being a thorn in my side.

I've narrowed it down to a difference in execution time in Python between v4 and v5 kernels. I don't know if it'd be valuable, but we could disassemble the Python instructions and compare between v4 and v5 calls. I wouldn't expect to see a change though since the CPU arch isn't changing...

@beadon
Copy link
Author

beadon commented Oct 24, 2022

long delay - apologies- working on some life stuff concurrently. --

We might have to grab kernel sources and debug at that level. I believe that the timing issue you are seeing is related only to the python implementation, correct ? If so, then this means we need to be on either side of python -- in the OS, comparing what happens when the C driver is pushing things around and , in/above python where it is executing.

The insight between kernels and the visual capture of the timing and grouping of messages is really interesting! You can clearly see the difference.

There might be a shortcut here, a shot in the dark troubleshooting wise -- if there is a difference in the low level driver, this could also explain the behavior difference. At the moment I am unsure what the interface is between the kernel and the driver, but a good starting point are the loaded kernel modules, or the filehandles that each process has open (identify with lsof ).

We should see the process(es) grab different C-level handles , associated with different libraries or modules. One more thing we need to check into is ldd, and LD_LIBRARY_PATH, in case any library is stepping in front of another library and getting used when it should not be. A reminder to keep a close eye on versioning.

... have to dig in more ...

@David00
Copy link
Owner

David00 commented Nov 2, 2022

Hi @beadon, no worries at all - same for me.

Yes, I've tested the sampling rate in C and it does not vary between kernel versions (at least, not so significantly - it might have a little bit, don't remember).

In the case of py-spidev, it's importing <sys/ioctl.h> and <linux/ioctl.h>. My understanding is that these are the kernel drivers for Linux and that the py-spidev library is interfacing with them directly. I don't know much about kernel drivers, though. I'd like to see if we can have @doceme review this discussion and see if they have any hunches. I'll link a new issue in the py-spidev repo to this one.

@David00
Copy link
Owner

David00 commented Nov 2, 2022

You know what, I think the short break that I took from this issue was beneficial overall. We might be barking up the wrong tree here.

I went back through my test script and re-analyzed the data I collected. While my decorator trigger function was beneficial to spot the issue from a birds eye view, I think it needs to go one level deeper and measure the time it takes to make a single call to spi.xfer(), and compare this measurement between kernel the two versions.

If the time is the same, which I have a good feeling it will be, the issue is not with SPI, but rather with the Python implementation (and therefore, CPython). It could even be something as simple as the specific Python build (like whether or not it was compiled with optimizations, or something else I'm not currently aware of). I did test the same Python version, but I didn't test the same Python build. I'm going to run the 'scope test case for individual calls to spidev.xfer() and report back shortly.

@David00
Copy link
Owner

David00 commented Nov 2, 2022

Ok, so I adjusted the test script to pull the scope pin high immediately before calling spi.xfer(), and then set it low on the very next call:

for _ in range(num_samples):
	GPIO.output(SCOPE_PIN, 1) # pin set high
	spi.xfer([1, 8 << 4, 0])
	GPIO.output(SCOPE_PIN, 0) # pin set low

So, the pin is high immediately before, during, and immediately after the call to spi.xfer() until it is set low agin.
The pin is low only in between iterations of the for loop.

I have measured the following time deltas on both kernels, and have uncovered a bit more detail about the source of the delays noted in my previous comment.

  • time it takes for the SPI clock to start after the scope pin is pulled high
  • time it takes for the scope pin to get pulled low after the SPI clock stops
  • total duration that the pin is high

On both kernels, it takes about 10µs for the SPI clock to begin after the scope pin is pulled high. (13.2µs on v5, and 9.7µs on v4... not much of a difference at all).

Example (note the "13.20µs" near the bottom left, on the X2 - X1 box):

image

However...

When the SPI clock stops, it takes significantly longer on the v5 kernel for the scope pin to be pulled low again - about 5x longer!!

On v5, it takes ~30µs for the scope pin to be pulled low after the SPI clock stops.

image

On v4, it takes ~6µs for the scope pin to be pulled low after the SPI clock stops. (Note that the scale of the following image is twice as large as the scale above, yet the delay between the clock ending and the scope pin going low still appears smaller!)

image

So, the time between # pin set high and spi.xfer(...) is about the same on both kernels, and this testing shows that spi.xfer(...) is indeed taking longer to run on v5.15.

So, when I get back to this again, I'll take another look through spi.xfer() and look at what it's doing to clean up prior to returning. If we need to go one step deeper with the scope (controlling the scope pin at various places inside the spi.xfer() function), I can look at that too.

@David00
Copy link
Owner

David00 commented Nov 9, 2022

@beadon, it looks we're good now after @doceme's patch and bump to py-spidev v3.6.

I'm working on a new custom OS build based off the latest Raspberry Pi OS Lite image, dated 2022-09-22, which ships with a v5.15.61 kernel. I'm also merging PR #62 into this release which @kizniche was so kind to work on and submit.

I'm so glad to have this SPI issue resolved now - thank you so much for lighting the fire that drove us to get to the bottom of it! I should have the release out by this weekend. In addition to the new custom OS release, I will be tagging this update as v0.2.0.

Future Readers

You should be safe to sudo apt update && sudo apt upgrade your systems now, followed by a pip install -U spidev, to get spidev version 3.6. You'll also need to add force_turbo=1 to /boot/config.txt and reboot your Pi since v5 kernels handle the SPI clock speed differently:

pip install -U spidev
sudo su
echo "force_turbo=1" >> /boot/config.txt
reboot 0

@David00 David00 closed this as completed Nov 9, 2022
@beadon
Copy link
Author

beadon commented Nov 9, 2022

You rock!

I have had a storm of life events. Thank you for bringing this to conclusion!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants