My first robot using pbLua involved Power Function motors, so I had to devise Lua code to communicate with the Mindsensors NRLink device. The pbLua homepage has a tutorial on how to communicate with I2C devices which provided a starting point for my code. Ralph has also written a tutorial to control the HiTechnic IRLink, the NRLink .

After a lot of trial-and-error I’m happy to share my pbLua library to communicate with the NRLink. It provides functions for the following tasks:

  • Initialise the NRLink
  • Send commands to the NRLink
  • Tell the NRLink to run macros from the EEPROM
  • Print the contents of the NRLink EEPROM on the console (handy for debugging macros)
  • Install new macro bytes into the NRLink EEPROM
  • Drive a dual-motor robot forwards, backwards, left and right

You can download NRLink.lua which is the pbLua source file for the examples below.

NRLink constants

The NRLink appears as a set of registers in the I2C address space. The key registers are at addresses 0x41 and 0x42 which are the command register (0x41) and command byte (0x42) when written to. I place the NRLink constants into a table to avoid cluttering the global Lua namespace – a good programming practice.


-- Constants for talking to the NRLink
NRLinkport = 3 -- change this depending on the port you connect the NRLink to
NRLinkID = 0x02

-- Put the various constants into an NRLink table to avoid polluting the
-- global namespace
NRLink = {}
NRLink.NRLinkDataBytes = 0x40
NRLink.NRLinkCommandReg = 0x41
NRLink.NRLinkReadResult = 0x42
NRLink.NRLinkWriteData = 0x42
NRLink.NRLinkDefault = 0x44
NRLink.NRLinkFlush = 0x46
NRLink.NRLinkHighSpeed = 0x48
NRLink.NRLinkLongRange = 0x4C
NRLink.NRLinkShortRange = 0x53
NRLink.NRLinkSetADPAON = 0x4E
NRLink.NRLinkSETADPAOFF = 0x4F
NRLink.NRLinkTxUnassembled = 0x55
NRLink.NRLinkSelectRCX = 0x58
NRLink.NRLinkSelectTRAIN = 0x54
NRLink.NRLinkSelectPF = 0x50
NRLink.NRLinkMacroCommand = 0x52
NRLink.Macro_Short_range = 0x01
NRLink.Macro_Long_Range = 0x04

Initialise the NRLink

The NRLink is initialised by connecting to it on the I2C port and then instructing it to enter Power Functions mode:


-- setupI2C() - sets up the specified port to handle an I2C sensor
-- From Ralph Hempel's tutorial
function setupI2C(port)
nxt.InputSetType(port,2)
nxt.InputSetDir(port,1,1)
nxt.InputSetState(port,1,1)

nxt.I2CInitPins(port)
end

-- Initialise the NRLink for communication with the PF IR receiver
function initNRLink(port, address)

setupI2C(port)
NRLinkCommand(port, address, NRLink.NRLinkFlush);
NRLinkCommand(port, address, NRLink.NRLinkDefault);
NRLinkCommand(port, address, NRLink.NRLinkLongRange);
NRLinkCommand(port, address, NRLink.NRLinkSelectPF);
end

Send command to the NRLink

The NRLinkCommand is responsible for sending basic one byte commands to the NRLink. It uses the pbLua string.char() function to build the correct byte string to send via the I2C interface. To send a command to the NRLink (e.g. to select Power Functions mode) we write it into address 0x41. Details of the commands supported by the NRLink are provided in the manual available on the Mindsensors website.


-- waitI2C() - sits in a tight loop until the I2C system goes idle
-- From Ralph Hempel's I2C tutorial
function waitI2C( port )
while( 0 ~= nxt.I2CGetStatus( port ) ) do
end
end

-- Send a command byte to the NRLink
function NRLinkCommand(port, address, command)

local NRLinkMsg = string.char(address, NRLink.NRLinkCommandReg, command);

waitI2C(port)
nxt.I2CSendData(port, NRLinkMsg, 0)
end

 

Run a macro on the NRLink

The NRLink can store macros which are previously defined commands in the EEPROM. The macros are preserved when the power is removed. Macros are handy for two reasons: once you load them they do not disappear and they reduce the amount of I2C bus traffic and hence IR command to make your PF motors work.

The NRLinkRunMacro function takes the address of a previously loaded macro and tells the NRLink to run it by sending the “R” command followed by the address of the macro:


-- Instruct the NRLink to run a macro at a given address
function NRLinkRunMacro(port, address, macro)

local NRLinkMsg = string.char(address, NRLink.NRLinkCommandReg, NRLink.NRLinkMacroCommand, macro)

waitI2C(port)
nxt.I2CSendData(port, NRLinkMsg, 0)
end

Dump the contents of the NRLink EEPROM

It is useful to have a function that will print the contents of the NRLink EEPROM so you can see what macros have been loaded (or to verify that your macros were loaded correctly). The dumpNRLinkEEPROM function will print the bytes stored in the EEPROM in lines of 8 hex on the console:


-- Print the contents of the NRLink EEPROM onto the console.
-- Each address is printed in a line of 8 characters as hex
function dumpNRLinkEEPROM(port, address)

local addr
local bytes
local s0 = string.char(0x02);

print("--------------------------------------------------")
for addr=0,255,8 do
local cmd = s0 .. string.char(addr)
nxt.I2CSendData(port, cmd, 8)
waitI2C(port)
bytes = nxt.I2CRecvData(port, 8)
print(string.format("0x%2x: 0x%2x 0x%2x 0x%2x 0x%2x 0x%2x 0x%2x 0x%2x 0x%2x", addr,
string.byte(bytes, 1), string.byte(bytes, 2), string.byte(bytes, 3),
string.byte(bytes, 4), string.byte(bytes, 5), string.byte(bytes, 6),
string.byte(bytes, 7), string.byte(bytes, 8) ) )
end
print("--------------------------------------------------")
end

A few notes on this function; to read a string of bytes from an I2C device you send the address and number of bytes requested in an I2C message (the nxt.I2CSendData you see above). Then you wait for the I2C bus to be ready and attempt to read the bytes back from the I2C device. The for loop iterates 8 bytes at a time across all 256 bytes stored in the NRLink EEPROM.

The output from this function looks something like this:

dumpNRLinkEEPROM(3)
--------------------------------------------------
0x 0: 0x56 0x32 0x2e 0x30  0x30 0x 0 0x 0 0x 0
0x 8: 0x6d 0x6e 0x64 0x73  0x6e 0x73 0x72 0x73
0x10: 0x4e 0x52 0x4c 0x69  0x6e 0x6b 0x 0 0x 0
0x18: 0xff 0xff 0xff 0xff  0xff 0xff 0xff 0xff
0x20: 0xff 0xff 0xff 0xff  0xff 0xff 0xff 0xff
0x28: 0xff 0xff 0xff 0xff  0xff 0xff 0xff 0xff
0x30: 0xff 0xff 0xff 0xff  0xff 0xff 0xff 0xff
0x38: 0xff 0xff 0xff 0xff  0xff 0xff 0xff 0xff
0x40: 0x 0 0x 0 0x 0 0x 0  0x 0 0x 0 0x 0 0x 0
0x48: 0xff 0xff 0xff 0xff  0xff 0xff 0xff 0xff
0x50: 0x 2 0x 1 0x 0 0x 2  0x 1 0x10 0x 2 0x 1
0x58: 0x20 0x 2 0x 1 0x30  0x 2 0x 1 0x 0 0x 2
0x60: 0x 1 0x40 0x 2 0x 1  0x80 0x 2 0x 1 0xc0
0x68: 0x 2 0x11 0x 0 0x 2  0x11 0x10 0x 2 0x11
0x70: 0x20 0x 2 0x11 0x30  0x 2 0x11 0x 0 0x 2
0x78: 0x11 0x40 0x 2 0x11  0x80 0x 2 0x11 0xc0
0x80: 0x 2 0x21 0x 0 0x 2  0x21 0x10 0x 2 0x21
0x88: 0x20 0x 2 0x21 0x30  0x 2 0x21 0x 0 0x 2
0x90: 0x21 0x40 0x 2 0x21  0x80 0x 2 0x21 0xc0
0x98: 0x 2 0x31 0x 0 0x 2  0x31 0x10 0x 2 0x31
0xa0: 0x20 0x 2 0x31 0x30  0x 2 0x31 0x 0 0x 2
0xa8: 0x31 0x40 0x 2 0x31  0x80 0x 2 0x31 0xc0
0xb0: 0x 2 0x 1 0x50 0x 2  0x 1 0x90 0x 2 0x 1
0xb8: 0x60 0x 2 0x 1 0xa0  0x 2 0x11 0x50 0x 2
0xc0: 0x11 0x90 0x 2 0x11  0x60 0x 2 0x11 0xa0
0xc8: 0x 2 0x21 0x50 0x 2  0x21 0x90 0x 2 0x21
0xd0: 0x60 0x 2 0x21 0xa0  0x 2 0x31 0x50 0x 2
0xd8: 0x31 0x90 0x 2 0x31  0x60 0x 2 0x31 0xa0
0xe0: 0x 2 0x47 0x70 0x 2  0x43 0x30 0x 2 0x45
0xe8: 0x50 0x 2 0x4b 0x50  0x 4 0x31 0x10 0x31
0xf0: 0x40 0x 4 0x31 0x10  0x 0 0x80 0x 4 0x31
0xf8: 0x20 0x31 0x40 0x 4  0x31 0x20 0x31 0x80
--------------------------------------------------

You can see that I have loaded my macros starting at address 0x50.

Install macros in the NRLink EEPROM

To install a new macro you must write the macro bytes to an address past 0x50 in the NRLink EEPROM. I use a table to store each macro; the first entry in the table is the address to write the macro into, followed by the number of bytes in the macro and finally the macro bytes themselves.


-- Define some macros to load into the NRLink
-- Macros are defined as
-- Address, Number of bytes in macro, Macrobytes...
NRLink.powerFunctionsMacros = {
[1] = {0xB0, 0x02, 0x01, 0x50}, -- Motor Ch1 A Forw B Forw
[2] = {0xB3, 0x02, 0x01, 0x90}, -- Motor Ch1 A Forw B Rev
[3] = {0xB6, 0x02, 0x01, 0x60}, -- Motor Ch1 A Rev B Forw
[4] = {0xB9, 0x02, 0x01, 0xa0}, -- Motor Ch1 A Rev B Rev
[5] = {0xBC, 0x02, 0x11, 0x50}, -- Motor Ch2 A Forw B Forw
[6] = {0xBF, 0x02, 0x11, 0x90}, -- Motor Ch2 A Forw B Rev
[7] = {0xC2, 0x02, 0x11, 0x60}, -- Motor Ch2 A Rev B Forw
[8] = {0xC5, 0x02, 0x11, 0xa0}, -- Motor Ch2 A Rev B Rev
[9] = {0xC8, 0x02, 0x21, 0x50}, -- Motor Ch3 A Forw B Forw
[10] = {0xCB, 0x02, 0x21, 0x90}, -- Motor Ch3 A Forw B Rev
[11] = {0xCE, 0x02, 0x21, 0x60}, -- Motor Ch3 A Rev B Forw
[12] = {0xD1, 0x02, 0x21, 0xa0}, -- Motor Ch3 A Rev B Rev
[13] = {0xD4, 0x02, 0x31, 0x50}, -- Motor Ch4 A Forw B Forw
[14] = {0xD7, 0x02, 0x31, 0x90}, -- Motor Ch4 A Forw B Rev
[15] = {0xDA, 0x02, 0x31, 0x60}, -- Motor Ch4 A Rev B Forw
[16] = {0xDD, 0x02, 0x31, 0xa0} -- Motor Ch4 A Rev B Rev
}

-- Install new macros into the NRLink
-- You can modify the macro definitions to load whatever is convenient for your robot
function installNRLinkMacros(port, address, macroBytes)

local msg
-- iterate over the byte array provided and send via the I2C link
for i,v in ipairs(macroBytes) do
msg = string.char(address)
waitI2C(port)
for j,byte in ipairs(v) do
msg = msg .. string.char(byte)
end
nxt.I2CSendData(port, msg, 0)
delayms(10)
end
end

Drive a robot

Finally, we can put all of these macros to work and write some helper functions to drive our robot via the PF motors. The functions are very straightforward, and simply tell the NRLink to run a certain macro. You have to continue to send the macro to the IR receiver until you want the motor to stop. I will be exploring the PF “Continuous” and “PWM” modes in a future tutorial.


-- Stop both motors
function brakeMotors(port)
waitI2C(port)
nxt.I2CSendData(port, NRLink.Ch1_A_Brake, 0)
delayms(10)
waitI2C(port)
nxt.I2CSendData(port, NRLink.Ch1_B_Brake, 0)
end

-- Drive robot forward
-- Note that the motors are mirrored relative to each other, so one must
-- be driven forward, and the other backwards
-- Drive for the specified number of milliseconds
function driveForwards(port, duration)
local start
start = nxt.TimerRead()
repeat
waitI2C(port)
nxt.I2CSendData(port, NRLink.Ch1_A_Fwd_B_Rev, 0)
delayms(10)
until( nxt.TimerRead() >= (start+duration) )
brakeMotors(port)
end

-- Drive robot backwards
-- Note that the motors are mirrored relative to each other, so one must
-- be driven forward, and the other backwards
-- Drive for the specified number of milliseconds
function driveBackwards(port, duration)
local start
start = nxt.TimerRead()
repeat
waitI2C(port)
nxt.I2CSendData(port, NRLink.Ch1_A_Rev_B_Fwd, 0)
delayms(10)
until( nxt.TimerRead() >= (start+duration) )
brakeMotors(port)
end

-- Turn robot to the right
-- This pivots the robot on the right wheel
function turnRight(port, duration)
local start
start = nxt.TimerRead()
repeat
waitI2C(port)
nxt.I2CSendData(port, NRLink.Ch1_A_Fwd, 0)
delayms(10)
until( nxt.TimerRead() >= (start+duration) )
brakeMotors(port)
end

-- Turn robot to the left
-- This pivots the robot on the left wheel
function turnLeft(port, duration)
local start
start = nxt.TimerRead()
repeat
waitI2C(port)
nxt.I2CSendData(port, NRLink.Ch1_B_Rev, 0)
delayms(10)
until( nxt.TimerRead() >= (start+duration) )
brakeMotors(port)
end

initNRLink(NRLinkport, NRLinkID)

Stay tuned for more tutorials in pbLua!