Reading/writing Arduino pins over I2C with Perl
Today, loosely inspired by this thread over on Perlmonks, I'm going to show how to set up an Arduino (Uno in this test case) with a pseudo-register that allows toggling one if its digital pins on and off, and another pseudo-register to read an analog pin that the digital pin is connected to, over I2C. Because it's digital to analog, the only possible values of the analog read will be 0 (off) or 1023 (full on, ie. 5v). This is an exceptionally basic example, but with some thought, one can imagine the possibilities (read/write EEPROM, set PWM etc etc).
We'll then use RPi::I2C to toggle the digital pin and read the analog pin over the I2C bus. Note I'm not using the encompassing RPi::WiringPi distribution in this case. The benefit to using that is to clean up Raspberry Pi's GPIO pins, which we aren't using any. In fact, any Linux device with I2C can be used for this example, I just so happen to be using one of my Pi 3 boards.
First, the simple Arduino sketch. All I2C devices require putting themselves on the "wire" with a unique address. I'm using 0x04
, which is how the Pi will identify the Arduino on the I2C bus.
#include <Wire.h>
// Arduino I2C address
#define SLAVE_ADDR 0x04
// pseudo register addresses
#define READ_A0 0x05
#define WRITE_D2 0x0A
uint8_t reg = 0;
void read_analog_pin (){
switch (reg){
case READ_A0: {
__read_analog(A0);
break;
}
}
}
void __read_analog (int pin){
int val = analogRead(pin);
uint8_t buf[2];
// reverse endian so we're little endian going out
buf[0] = (val >> 8) & 0xFF;
buf[1] = val & 0xFF;
Wire.write(buf, 2);
}
void write_digital_pin (int num_bytes){
reg = Wire.read(); // global register addr
while(Wire.available()){
uint8_t state = Wire.read();
switch (reg){
case WRITE_D2: {
digitalWrite(2, state);
break;
}
}
}
}
void setup(){
Wire.begin(SLAVE_ADDR);
// set up the I2C callbacks
Wire.onReceive(write_digital_pin);
Wire.onRequest(read_analog_pin);
// set up the pins
pinMode(2, OUTPUT);
pinMode(A0, INPUT);
}
void loop(){
delay(10000);
}
Now, I'll show a simple script that loops 10 times, toggling the digital pin then displaying the value from the analog pin. Arudino's Wire
library sends data a byte at a time, so we have to do some bit manipulation to turn the two bytes returned in the read_block()
call back together into a single 16-bit integer (I wrote the merge()
sub to handle this job).
use warnings;
use strict;
use RPi::Const qw(:all);
use RPi::I2C;
use constant {
ARDUINO_ADDR => 0x04,
READ_REGISTER => 0x05,
WRITE_REGISTER => 0x0A,
};
my $device = RPi::I2C->new(ARDUINO_ADDR);
for (0..9){
my (@bytes_read, $value);
$device->write_byte(HIGH, WRITE_REGISTER);
@bytes_read = $device->read_block(2, READ_REGISTER);
$value = merge(@bytes_read);
print "$value\n"; # 1023
$device->write_byte(LOW, WRITE_REGISTER);
@bytes_read = $device->read_block(2, READ_REGISTER);
$value = merge(@bytes_read);
print "$value\n"; # 0
}
sub merge {
return ($_[0] << 8) & 0xFF00 | ($_[1] & 0xFF);
}
Output:
1023
0
1023
0
1023
0
1023
0
1023
0
1023
0
1023
0
1023
0
1023
0
1023
0
I must acknowledge Slava Volkov (SVOLKOV) for the actual XS code. Most of the low-level hardware code I've been working on over the last year has been wrapping C/C++ libraries, a decent chunk of it has had me following datasheets to write my own, but in this case, I bit the whole XS file from Device::I2C and just presented a new Perl face to it so it fit in under the RPi::WiringPi
umbrella. It just worked.
Leave a comment