Mindsensors NRLink

in pbLua

April 2009

 

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:

  1. Initialise the NRLink

  2. Send commands to the NRLink

  3. Tell the NRLink to run macros from the EEPROM

  4. Print the contents of the NRLink EEPROM on the console (handy for debugging macros)

  5. Install new macro bytes into the NRLink EEPROM

  6. 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.


-- 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.


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



Stay tuned for more tutorials in pbLua!

 

Last updated 10 April, 2009


All content © 2008, 2009 Mark Crosbie  mark@mastincrosbie.com


LEGO® is a trademark of the LEGO Group of companies which does not sponsor, authorize or endorse this site. This site, its owner and contents are in no way affiliated with or endorsed by the LEGO Group. For more please read the LEGO Fair Play policy.