[2021-11-25]
Lately, I had a lot of fun - and frustation - playing with the I2C bus/protocol between a Rapsberry Pi and an Arduino-like expansion hat - namely, the Sleepy Pi - as part as my Pi Station project.
There are numerous examples on the Internet on how to achieve that - starting by the simple and
excellent examples of the Arduino Wire library - and as many reports of
frustation revolving around the (sporadic) i2cget -> Error: Read failed
error message.
Recently, that frustration nearly turned into suicidal will, following the latest (2011-10-30, 64-bit)
update of the Raspberry Pi firmware - aka. RaspiOS - and the dreaded i2cget -> Error: Read failed
error message becoming systematic.
TL;DR, we Raspberry Pi owners - especially those of the BCM283x-powered versions (1, 2 and 3) - are bound to experience frustration - even contemplating death - given:
a rather obscure I2C feature dubbed Clock Stretching (huh?!?) …
is nowadays a well-known broken feature of BCM283x SoCs (haa!) …
resulting in its being disabled in the Linux Kernel (no!!!)…
made even worse by the disappearance of the I2C Clock Frequency - aka. baudrate
- setting
parameter from the i2c-bcm2xxx
Linux driver (what???):
modinfo i2c-bcm2835
# [output]
#parm: debug:1=err, 2=isr, 3=xfer (uint)
maybe because, as some point, setting that baudrate was considered misleading, given the BCM283x I2C Clock Frequency is internally configured with a Clock Divider in respect with the Core Clock Frequency (that of the SoC itself), which is varying along what the CPU frequency governor tells it (say again…)
while all the Internet claims you should set some (poorly documented) dtparam=i2c_arm_baudrate=<freq>
parameter in your Raspberry Pi hardware configuration (WTF!?!)
In parallel, the Internet is full of - more or less patroning - comments about a few rules one must observe when doing I2C, interrupts in general and Arduino specifically:
Interrupt Service Routines (ISRs) - the things wired in Wire.onReceive
and Wire.onRequest
-
MUST NOT hold the interrupt up - and mask other interrupts - and be done as quickly as
possible
whatever variable is modified in a ISR MUST be declared volatile
when doing i2cget y <bus> <address> <command> ...
, you actually end up with two interrupts:
one that send the <command>
to your onReceive
handler
(with <command>
passed as a single byte data to be read with Wire.read()
)
one that request data from your onRequest
handler
(which ought to send those data back with Wire.write()
)
Even though one might has taught oneself all the basic things one ought to know, bam!,
frustation hits with i2cget -> Error: Read failed
.
It’s unclear - to me at least - what the default I2C Clock Divider might be. And for some reason, it seems the situation is made worse by the latest RaspiOS release.
Without detours, here is how I initially solved it:
On the Arduino:
make the onReceive
handler do nothing when receiving a dedicated Request <command>
make the onRequest
handler do nothing but spew a previously-prepared memory buffer
On the Raspberry Pi:
# Use the lowest and fixed ARM/Core Clock Frequency (yeck...)
echo powersave | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
# Display the actual BCM283x Core Clock Frequency
vcgencmd measure_clock core
# [output]
frequency(1)=250000000 # vs 400000000 in "performance" mode
# Prepare the data to read
i2cset -y <bus> <address> <read-some-metric>
# Actually read the data
i2cget -y <bus> <address> <read> w
More specifically, on the Arduino, and very shortly put:
// INTERRUPT SERVICE ROUTINES
// I2C Receive
void isrI2cReceive(int iBytes) {
yI2cOpCode = iBytes-- > 0 ? Wire.read() : I2C_OPCODE_NONE;
if(yI2cOpCode != I2C_OPCODE_REQUEST) {
for(int8_t i=0; i<I2C_BUFFER_RECEIVE_SIZE; i++) {
pyI2cReceiveBuffer[i] = iBytes-- > 0 ? Wire.read() : I2C_VALUE_UNSET;
}
}
}
// I2C Request
void isrI2cRequest() {
if(yI2cRequestSize != 0) {
Wire.write(pyI2cRequestBuffer, yI2cRequestSize);
}
}
// ARDUINO MAIN SETUP/LOOP
// Setup
void setup() {
Wire.begin(I2C_ADDRESS);
Wire.onReceive(isrI2cReceive);
Wire.onRequest(isrI2cRequest);
}
// Loop
void loop() {
switch(yI2cOpCode) {
case I2C_OPCODE_REQUEST:
// I2C is LSB first
for(int8_t i=yMetricSize-1; i>=0; i--) {
pyI2cRequestBuffer[i] = (uint8_t)(uiMetricValue >> 8*i);
}
yI2cRequestSize = yMetricSize;
break;
case I2C_OPCODE_RECEIVE:
for(int8_t i=0; i<I2C_BUFFER_RECEIVE_SIZE; i++) {
Serial.println(pyI2cReceiveBuffer[i]);
}
break;
}
}
yI2cOpCode = I2C_OPCODE_NONE;
}
And very lengthily put: sleepy-pi.ino
My own feable attempt to explain the problem at hand, given how I managed to quirk my way around it, is:
the (default) I2C Clock Frequency on the Raspberry Pi is set to a value that is too high …
especially in the absence of Clock Stretching …
and only lowering this clock frequency, indirectly via the powersave
CPU Scaling Governor …
along the leanest possible ISRs …
allows to reduce one’s frustration, even maybe contemplate life again
Still, no euphoria…
So I went on to alternatively try that dtparam=i2c_arm_baudrate=<freq>
parameter, starting with
a very reasonable 100kHz (100000
) value. No change. Frustation. Again.
PS: Remember we’re talking the Sleepy Pi expansion hat, a 3.3V-running GPIO-compatible 8MHz ATmega328P, with no lengthy wires, no missing pull-down/up resistances, no alien goo spat on it, etc.
Out of despair, I lowered that frequency to 10kHz… and… bingo! Each and every i2cget
calls
succeeding! Life! Hope!!! FUTURE!!!
dtparam=i2c_arm_baudrate=10000
But then, 10kHz… WTF?!?…
Digging further, I face-palmed myself realizing that one may change the I2C Clock Frequency of the Arduino, using the Wire.setClock function.
But don’t follow me there (unless you wanna die)! After further reading on I2C Clock
Stretching, the basics of I2C 2-wire signaling eventually
imprinted my slow brain, in particular the fact that the Clock signal/wire (SCL) is controlled
solely by the I2C Master and Wire.setClock
does not apply to I2C Slaves (the exception
being… Clock Stretching! which merely indicates to the Master that it should hold).
However, bear with me, should you ever want to Wire.setClock
when using the Arduino as the
I2C Master.
I stumbled on a few reports claiming the resulting frequency might be wrong given some non-standard setups (like modding the Arduino frequency). So I verified:
the actual setClock function …
itself leveraging the twi_setFrequency primitive …
and relying on the F_CPU macro …
itself depending on the AVR_FREQ variable…
which actually matched the expected 8Mhz (and my Sleepy Pi board definition)
… enough …