xref: /linux/tools/gpio/gpio-sloppy-logic-analyzer.sh (revision c34e9ab9a612ee8b18273398ef75c207b01f516d)
1#!/bin/sh -eu
2# SPDX-License-Identifier: GPL-2.0
3#
4# Helper script for the Linux Kernel GPIO sloppy logic analyzer
5#
6# Copyright (C) Wolfram Sang <wsa@sang-engineering.com>
7# Copyright (C) Renesas Electronics Corporation
8
9samplefreq=1000000
10numsamples=250000
11cpusetdefaultdir='/sys/fs/cgroup'
12cpusetprefix='cpuset.'
13debugdir='/sys/kernel/debug'
14ladirname='gpio-sloppy-logic-analyzer'
15outputdir="$PWD"
16neededcmds='taskset zip'
17max_chans=8
18duration=
19initcpu=
20listinstances=0
21lainstance=
22lasysfsdir=
23triggerdat=
24trigger_bindat=
25progname="${0##*/}"
26print_help()
27{
28	cat << EOF
29$progname - helper script for the Linux Kernel Sloppy GPIO Logic Analyzer
30Available options:
31	-c|--cpu <n>: which CPU to isolate for sampling. Only needed once. Default <1>.
32		      Remember that a more powerful CPU gives you higher sampling speeds.
33		      Also CPU0 is not recommended as it usually does extra bookkeeping.
34	-d|--duration-us <SI-n>: number of microseconds to sample. Overrides -n, no default value.
35	-h|--help: print this help
36	-i|--instance <str>: name of the logic analyzer in case you have multiple instances. Default
37			     to first instance found
38	-k|--kernel-debug-dir <str>: path to the kernel debugfs mountpoint. Default: <$debugdir>
39	-l|--list-instances: list all available instances
40	-n|--num_samples <SI-n>: number of samples to acquire. Default <$numsamples>
41	-o|--output-dir <str>: directory to put the result files. Default: current dir
42	-s|--sample_freq <SI-n>: desired sampling frequency. Might be capped if too large.
43				 Default: <1000000>
44	-t|--trigger <str>: pattern to use as trigger. <str> consists of two-char pairs. First
45			    char is channel number starting at "1". Second char is trigger level:
46			    "L" - low; "H" - high; "R" - rising; "F" - falling
47			    These pairs can be combined with "+", so "1H+2F" triggers when probe 1
48			    is high while probe 2 has a falling edge. You can have multiple triggers
49			    combined with ",". So, "1H+2F,1H+2R" is like the example before but it
50			    waits for a rising edge on probe 2 while probe 1 is still high after the
51			    first trigger has been met.
52			    Trigger data will only be used for the next capture and then be erased.
53
54<SI-n> is an integer value where SI units "T", "G", "M", "K" are recognized, e.g. '1M500K' is 1500000.
55
56Examples:
57Samples $numsamples values at 1MHz with an already prepared CPU or automatically prepares CPU1 if needed,
58use the first logic analyzer instance found:
59	'$progname'
60Samples 50us at 2MHz waiting for a falling edge on channel 2. CPU and instance as above:
61	'$progname -d 50 -s 2M -t "2F"'
62
63Note that the process exits after checking all parameters but a sub-process still works in
64the background. The result is only available once the sub-process finishes.
65
66Result is a .sr file to be consumed with PulseView from the free Sigrok project. It is
67a zip file which also contains the binary sample data which may be consumed by others.
68The filename is the logic analyzer instance name plus a since-epoch timestamp.
69EOF
70}
71
72fail()
73{
74	echo "$1"
75	exit 1
76}
77
78parse_si()
79{
80	conv_si="$(printf $1 | sed 's/[tT]+\?/*1000G+/g; s/[gG]+\?/*1000M+/g; s/[mM]+\?/*1000K+/g; s/[kK]+\?/*1000+/g; s/+$//')"
81	si_val="$((conv_si))"
82}
83set_newmask()
84{
85	for f in $(find "$1" -iname "$2"); do echo "$newmask" > "$f" 2>/dev/null || true; done
86}
87
88init_cpu()
89{
90	isol_cpu="$1"
91
92	[ -d "$lacpusetdir" ] || mkdir "$lacpusetdir"
93
94	cur_cpu=$(cat "${lacpusetfile}cpus")
95	[ "$cur_cpu" = "$isol_cpu" ] && return
96	[ -z "$cur_cpu" ] || fail "CPU$isol_cpu requested but CPU$cur_cpu already isolated"
97
98	echo "$isol_cpu" > "${lacpusetfile}cpus" || fail "Could not isolate CPU$isol_cpu. Does it exist?"
99	echo 1 > "${lacpusetfile}cpu_exclusive"
100	echo 0 > "${lacpusetfile}mems"
101
102	oldmask=$(cat /proc/irq/default_smp_affinity)
103	newmask=$(printf "%x" $((0x$oldmask & ~(1 << isol_cpu))))
104
105	set_newmask '/proc/irq' '*smp_affinity'
106	set_newmask '/sys/devices/virtual/workqueue/' 'cpumask'
107
108	# Move tasks away from isolated CPU
109	for p in $(ps -o pid | tail -n +2); do
110		mask=$(taskset -p "$p") || continue
111		# Ignore tasks with a custom mask, i.e. not equal $oldmask
112		[ "${mask##*: }" = "$oldmask" ] || continue
113		taskset -p "$newmask" "$p" || continue
114	done 2>/dev/null >/dev/null
115
116	# Big hammer! Working with 'rcu_momentary_eqs()' for a more fine-grained solution
117	# still printed warnings. Same for re-enabling the stall detector after sampling.
118	echo 1 > /sys/module/rcupdate/parameters/rcu_cpu_stall_suppress
119
120	cpufreqgov="/sys/devices/system/cpu/cpu$isol_cpu/cpufreq/scaling_governor"
121	[ -w "$cpufreqgov" ] && echo 'performance' > "$cpufreqgov" || true
122}
123
124parse_triggerdat()
125{
126	oldifs="$IFS"
127	IFS=','; for trig in $1; do
128		mask=0; val1=0; val2=0
129		IFS='+'; for elem in $trig; do
130			chan=${elem%[lhfrLHFR]}
131			mode=${elem#$chan}
132			# Check if we could parse something and the channel number fits
133			[ "$chan" != "$elem" ] && [ "$chan" -le $max_chans ] || fail "Trigger syntax error: $elem"
134			bit=$((1 << (chan - 1)))
135			mask=$((mask | bit))
136			case $mode in
137				[hH]) val1=$((val1 | bit)); val2=$((val2 | bit));;
138				[fF]) val1=$((val1 | bit));;
139				[rR]) val2=$((val2 | bit));;
140			esac
141		done
142		trigger_bindat="$trigger_bindat$(printf '\\%o\\%o' $mask $val1)"
143		[ $val1 -ne $val2 ] && trigger_bindat="$trigger_bindat$(printf '\\%o\\%o' $mask $val2)"
144	done
145	IFS="$oldifs"
146}
147
148do_capture()
149{
150	taskset "$1" echo 1 > "$lasysfsdir"/capture || fail "Capture error! Check kernel log"
151
152	srtmp=$(mktemp -d)
153	echo 1 > "$srtmp"/version
154	cp "$lasysfsdir"/sample_data "$srtmp"/logic-1-1
155	cat > "$srtmp"/metadata << EOF
156[global]
157sigrok version=0.2.0
158
159[device 1]
160capturefile=logic-1
161total probes=$(wc -l < "$lasysfsdir"/meta_data)
162samplerate=${samplefreq}Hz
163unitsize=1
164EOF
165	cat "$lasysfsdir"/meta_data >> "$srtmp"/metadata
166
167	zipname="$outputdir/${lasysfsdir##*/}-$(date +%s).sr"
168	zip -jq "$zipname" "$srtmp"/*
169	rm -rf "$srtmp"
170	delay_ack=$(cat "$lasysfsdir"/delay_ns_acquisition)
171	[ "$delay_ack" -eq 0 ] && delay_ack=1
172	echo "Logic analyzer done. Saved '$zipname'"
173	echo "Max sample frequency this time: $((1000000000 / delay_ack))Hz."
174}
175
176rep=$(getopt -a -l cpu:,duration-us:,help,instance:,list-instances,kernel-debug-dir:,num_samples:,output-dir:,sample_freq:,trigger: -o c:d:hi:k:ln:o:s:t: -- "$@") || exit 1
177eval set -- "$rep"
178while true; do
179	case "$1" in
180	-c|--cpu) initcpu="$2"; shift;;
181	-d|--duration-us) parse_si $2; duration=$si_val; shift;;
182	-h|--help) print_help; exit 0;;
183	-i|--instance) lainstance="$2"; shift;;
184	-k|--kernel-debug-dir) debugdir="$2"; shift;;
185	-l|--list-instances) listinstances=1;;
186	-n|--num_samples) parse_si $2; numsamples=$si_val; shift;;
187	-o|--output-dir) outputdir="$2"; shift;;
188	-s|--sample_freq) parse_si $2; samplefreq=$si_val; shift;;
189	-t|--trigger) triggerdat="$2"; shift;;
190	--) break;;
191	*) fail "error parsing command line: $*";;
192	esac
193	shift
194done
195
196for f in $neededcmds; do
197	command -v "$f" >/dev/null || fail "Command '$f' not found"
198done
199
200# print cpuset mountpoint if any, errorcode > 0 if noprefix option was found
201cpusetdir=$(awk '$3 == "cgroup" && $4 ~ /cpuset/ { print $2; exit (match($4, /noprefix/) > 0) }' /proc/self/mounts) || cpusetprefix=''
202if [ -z "$cpusetdir" ]; then
203	cpusetdir="$cpusetdefaultdir"
204	[ -d $cpusetdir ] || mkdir $cpusetdir
205	mount -t cgroup -o cpuset none $cpusetdir || fail "Couldn't mount cpusets. Not in kernel or already in use?"
206fi
207
208lacpusetdir="$cpusetdir/$ladirname"
209lacpusetfile="$lacpusetdir/$cpusetprefix"
210sysfsdir="$debugdir/$ladirname"
211
212[ "$samplefreq" -ne 0 ] || fail "Invalid sample frequency"
213
214[ -d "$sysfsdir" ] || fail "Could not find logic analyzer root dir '$sysfsdir'. Module loaded?"
215[ -x "$sysfsdir" ] || fail "Could not access logic analyzer root dir '$sysfsdir'. Need root?"
216
217[ $listinstances -gt 0 ] && find "$sysfsdir" -mindepth 1 -type d | sed 's|.*/||' && exit 0
218
219if [ -n "$lainstance" ]; then
220	lasysfsdir="$sysfsdir/$lainstance"
221else
222	lasysfsdir=$(find "$sysfsdir" -mindepth 1 -type d -print -quit)
223fi
224[ -d "$lasysfsdir" ] || fail "Logic analyzer directory '$lasysfsdir' not found!"
225[ -d "$outputdir" ] || fail "Output directory '$outputdir' not found!"
226
227[ -n "$initcpu" ] && init_cpu "$initcpu"
228[ -d "$lacpusetdir" ] || { echo "Auto-Isolating CPU1"; init_cpu 1; }
229
230ndelay=$((1000000000 / samplefreq))
231echo "$ndelay" > "$lasysfsdir"/delay_ns
232
233[ -n "$duration" ] && numsamples=$((samplefreq * duration / 1000000))
234echo $numsamples > "$lasysfsdir"/buf_size
235
236if [ -n "$triggerdat" ]; then
237	parse_triggerdat "$triggerdat"
238	printf "$trigger_bindat" > "$lasysfsdir"/trigger 2>/dev/null || fail "Trigger data '$triggerdat' rejected"
239fi
240
241workcpu=$(cat "${lacpusetfile}effective_cpus")
242[ -n "$workcpu" ] || fail "No isolated CPU found"
243cpumask=$(printf '%x' $((1 << workcpu)))
244instance=${lasysfsdir##*/}
245echo "Setting up '$instance': $numsamples samples at ${samplefreq}Hz with ${triggerdat:-no} trigger using CPU$workcpu"
246do_capture "$cpumask" &
247