Introduction

This tutorial describes how to use the three basic sensors that come with the NXT kit - the touch, light, and sound sensors. We'll cover the ultrasound sensor in a future discussion on the I2C capabilities of the NXT.

Just because we're calling them basic does not mean that you can't do anything interesting with them. The examples in this section demonstrate how to display the sound level in a real-time graph, and how to use the light sensor to read bar codes.

Note that some of the examples we'll be going over are not the best way to do things. For instance, the busy-wait function will prevent any other code from running until the condition clears. In later tutorials, we'll be showing ways around this problem using the build-in cooperative multitasking feature of Lua.

As a reminder, there is an excellent online version of Programming in Lua that is a complete description of Lua along with many programming examples, and an even better idea is to purchase the latest Programming in Lua book .

By the end of this tutorial, you'll be able to read the touch, light and sound sensors and write simple functions that use the data. We'll be building on the knowledge gained from the tutorial on first steps with pbLua and using the LCD display.

Sensor Basics

Before using a sensor with the Input Port API you need to know the port number the sensor will be plugged into, what type the sensor is, and what direction the sensor port pins must be set to. These things are described in detail for each sensor type in the next sections.

The NXT has 4 sensor ports along the bottom face of the housing. They are numbered from 1 to 4. The sensor type is set using the nxt.InputSetType() function. Each sensor port has two I/O pins which can be independently controlled using the nxt.InputSetDir() and nxt.InputSetState() functions. The sensor port is read using the nxt.InputGetState() function which returns the raw A/D value, and the current state of the two I/O pins.

The raw A/D value will range between 0 and 1023 - which is a range of 10 bits.

The Touch Sensor

The simplest sensor of all is the touch sensor. Only the raw A/D value returned by the nxt.InputGetState() function changes as the button is pressed. The sensor type is 0 and the direction of the I/O port pins must be set to input for the sensor to work.

The following code example continuously samples the touch sensor connected to port 1 and prints out the current millsecond timer value as well as the raw A/D and I/O pin states until the orange button on the front of the NXT is pressed.

-- Read touch sensor until the orange button is pressed
function TouchRead(port)

  nxt.InputSetType(port,0)
  nxt.InputSetDir(port,0,0)

  repeat
    print( nxt.TimerRead(), nxt.InputGetStatus(port) )
  until( 8 == nxt.ButtonRead() )
end

-- And using the function - press the orange button on the NXT to stop it
TouchRead(1)

In this example, we pass the port number to the function when we call it. When you run the TouchRead() function, watch the console display as it scrolls the readings by - they are moving very quickly indeed! This function reads the sensor and prints the results about 500 times per second.

Note the values returned by the touch sensor. My NXT running on NiMH batteries gives a value of 1023 for an open touch sensor, and a value of about 182 for a closed touch sensor.

It's a simple matter to modify the above routine to write to the console only when a change of state is detected, and we'll add another parameter to the function that sets the threshold for detection.

-- Read touch sensor until the orange button is pressed
function TouchChange(port, thresh)

  nxt.InputSetType(port,0)
  nxt.InputSetDir(port,0,0)

  local oldState = 0
  local newState = 0

  repeat
    if( thresh < nxt.InputGetStatus( port ) ) then
      newState = 0
    else
      newState = 1
    end

    if newState ~= oldState then
      print( nxt.TimerRead(), newState )
      oldState = newState
    end

  until( 8 == nxt.ButtonRead() )
end

-- And using the function - press the orange button on the NXT to stop it
TouchChange(1)

Note the use of local oldState = 0 and local newState = 0. This is done to make sure that these values are indeed local to the function and don't leave garbage in the global variable namespace.

The NXT low level firmware drivers that pbLua interfaces with already debounce the readings from the touch sensor. Normally you would have to take multiple readings and eliminate the contact bounce that comes with switches.

The Sound Sensor

The next sensor we'll look at is the sound sensor. It's also an inactive (type 0) sensor, like the touch sensor but it can operate in either the dB or dBA relative modes depending on which of the bits is set.

If you want normal sensitivity (the default) then set the two I/O bits to (0,1). For high sensitivity, the the two I/O bits to (1,0) using the nxt.InputSetState() function. The second example describes how to do this.

The following sample code works just like the touch sensor example, and spits out sensor readings continuously until the orange button on the NXT is pressed.

-- Read sound sensor until the orange button is pressed
function SoundRead(port)

  nxt.InputSetType(port,0)
  nxt.InputSetState(port,1,0)
  nxt.InputSetDir(port,1,1)

  repeat
    print( nxt.TimerRead(), nxt.InputGetStatus(port) )
  until( 8 == nxt.ButtonRead() )
end

-- And using the function - press the orange button on the NXT to stop it
SoundRead(2)

Note that the test function uses port 2, so be sure to connect your sound sensor to the correct port.

You'll see that the values tend to buzz by very quickly, about 500 samples per second, and that the raw numbers go down as the volume goes up. Let's use some Lua code to process the readings using the exact same algorithm as the standard NXT firmware uses.

The following C code is extracted out of the c_input.c file from the LEGO MINDSTORMS NXT Open Source Distribution:

#define   VCC_SENSOR                    5000L
#define   AD_MAX                        1023L

#define   NEWSOUNDSENSORMIN             (650L/(VCC_SENSOR/AD_MAX))
#define   NEWSOUNDSENSORMAX             ((AD_MAX * 4980L)/VCC_SENSOR)
#define   NEWSOUNDSENSORPCTDYN          (UBYTE)(((NEWSOUNDSENSORMAX - NEWSOUNDSENSORMIN) * 100L)/AD_MAX)

There is a general function that converts the raw A/D readings of the sensor into a percentage of the full-scale range of the sensor. For the sound sensor, the formula turns out to be something like this:

  1. If the raw value is less than the minumum, set it to 0, otherwise subtract the minimum from the raw value
  2. Multiply the result of the previous step by 100, then divide by a constant
  3. If the result of the previous step is greater than the sensor resolution set it to the maximum sensor resolution
  4. If the raw value is inversely proportional to the effect being measured, then subtract the result of the previous step from the maximum sensor resolution

For the case of the sound sensor, the mimimum value works out to 162, the constant works out to 83, the maximum works out to 1018, and the raw value is inversely proportional to the effect being measured. The following sample code processes the raw A/D readings and returns the result scaled to the full range of values.

Actually, it does a little more than that! If you read the code carefully you'll see that the SoundScale() function takes an additional parameter called sensitive. This additional parameter chooses the senitivity of the sound sensor and sets up the port accordingly.

Go ahead, run the program!

-- Read sound sensor and scale the results
function SoundScale(port,sensitive)

  nxt.InputSetType(port,0)

  sensitive = sensitive or 0

  if 0 == sensitive then nxt.InputSetState(port,0,1)
                    else nxt.InputSetState(port,1,0)
  end

  nxt.InputSetDir(port,1,1)

  repeat
    local raw = nxt.InputGetStatus(port)

    -- Check if raw value is less than minimum, adjust if needed
    if raw < 162 then raw = 0
                 else raw = raw - 162
    end

    -- Scale the result
    raw = (raw*100)/83

    -- Check if raw value is greater than maximum, adjust if needed
    if raw > 1023 then raw = 1023
    end

    -- Invert the raw value
    raw = 1023 - raw

    -- Make a string that represents the volume...

    print( nxt.TimerRead(), string.rep("*", raw/16 ) )
  until( 8 == nxt.ButtonRead() )
end

-- And using the function - press the orange button on the NXT to stop it
SoundScale(2,0)

Yes, it is cool. If everything works correctly, your console will show a rolling bar graph of the volume measured by the sound sensor, where the highest volume is 64 "*" characters. This is very useful for testing the range of values that you might want to process further.

A few ideas for further development might be:

You can see that this is a pretty cool sensor, and that there a lot of you possibilities for integrating it into robots using the high speed processing capabilities of pbLua.

The Light Sensor

The last of the basic sensors that ships with the NXT set is the light sensor. It operates in a two different modes, active, and inactive.

In inactive mode, the sensor type is 0, and the two I/O pins are set to (0,0). This mode is used when you are reading ambient light conditions and you don't need to provide your own light source.

In active mode, the sensor type is also 0, and the two I/O pins are set to (1,0). This mode is used when you are trying to detecct how light or dark something is and you need to provide a stable light source. When using this mode, you may need to find a way to filter out the ambient light using some math, which we'll get into later.

If you're paying attention and have read this far, you can probably guess that a code sample is coming, and that the light sensor is plugged into port 3. The following code samples the light sensor and dumps the timestamp and sensor value out to the console at the rate of about 500 samples per second.

-- Read light sensor until the orange button is pressed
function LightRead(port,active)

  nxt.InputSetType(port,0)

  active = active or 0

  if 0 == active then nxt.InputSetState(port,0,0)
                 else nxt.InputSetState(port,1,0)
  end

  nxt.InputSetDir(port,1,1)

  repeat
    print( nxt.TimerRead(), nxt.InputGetStatus(port) )
  until( 8 == nxt.ButtonRead() )
end

-- And using the function - press the orange button on the NXT to stop it
LightRead(3,0)

We can get a bit more interstig results in the next step, which is basically making the light sensor handle transitions between black and white stripes on a piece of paper, which is the basic building block of a bar code reader.

If we have a single threshold and set all values above that level as black and all values below the level as white, we'll run into trouble when the sensor is in the "grey" area at the threshold. The result will be a mess of transitions between white and black.

To get a round this problem, the concept of hysteresis has been used for years. Think of it as increasing the size of the gray area so that we can be sure that we're outside it.

The sample program takes a threshold and a guard value. Values that are greater that the threshold plus the guard are black, and values that are less than the threshold minus the guard are white. The program will print out the new state and the time of each transition.

To determine the theshold and guard parameters, run the LightRead() function, move the sensor over the white and black areas of the barcode, and note the results.

Take the average of the white and black values and use the average as the threshold and try different guard values to see which one gives better results. If the guard is too large, you'll end up with unreliable width measurement, and if it's too small you'll get unreliable edge detection.

-- Read light sensor until the orange button is pressed
function LightRead(port,active)

  nxt.InputSetType(port,0)

  active = active or 0

  if 0 == active then nxt.InputSetState(port,0,0)
                 else nxt.InputSetState(port,1,0)
  end

  nxt.InputSetDir(port,1,1)

  repeat
    print( nxt.TimerRead(), nxt.InputGetStatus(port) )
  until( 8 == nxt.ButtonRead() )
end

-- And using the function - press the orange button on the NXT to stop it
LightRead(3,0)

This is not a complete implementation of a barcode reader. Other issues that need to be resolved are:

The barcode sample code can also be used as the core of a line follower that uses one side of the line as a guide. Experiment with it and I'm sure you'll be able to come up with all kinds of ideas.

Conclusion

The basic sensors provided in the LEGO NXT kit are really useful for all kinds of applications. The pbLua language makes it very easy to interface with the sensors, and to build on simple concepts to design robust models.

The next tutorial is for the more advanced I2C based sensors that can measure and report multiple signals on one digital interface.