Input / Output¶
Learning Objectives: Using I/O pins programmatically for various purposes in embedded devices.
Input / Output, commonly referred to as I/O, refers to the transfer of information between a computer's components and peripherals. Inputs are signals/data received by any component, such as a button press or a message from a peripheral. Outputs are signals or information sent by electronic components, such as a control signals to a peripheral device (e.g PWM signal to control a motor).
Embedded devices typically come with built-in physical components and control logic on the microcontroller and circuit board to handle I/O. Below is a list of common components, some of which will be used in this course:
- Buttons, slide switches, touch screens (i.e., the device's "keyboard"),
- LEDs,
- Timer circuits,
- Analog-to-digital converter (ADC),
- PWM circuits (Pulse Width Modulation, Pulse Width Modulation),
- Serial communication circuits such as UART, USART, SPI, and I2C,
- Non-volatile memory such as EPROM,
- USB and/or Ethernet controller chips,
- LCD display.
Peripherals are either digital or analog. Digital components are often more complex, using different data buses, and are managed using bit values (i.e., agreed-upon voltage levels).
Analog components provide voltage values that can range between ground and the operating voltage (e.g., 0-5V). For example, many simple sensors represent their measurements as analog voltages. Of course, analog signals must first be converted to digital values (numbers) before a computer can process them. Microcontrollers have built-in analog-to-digital converters (ADCs) that convert voltages into (rounded) numerical values with a set precision, for example, using an 8-bit scale (0-255). The programmer's task is then to convert the ADC's value to correspond to a real-world measurement, which can then be processed in the program.
Examples of such sensors include various temperature, pressure, and light intensity sensors, as well as analog microphones. For instance, if a temperature sensor has a measurement range of 0-100°C, the programmer would need to create a conversion function that converts the ADC value (0-255) to a temperature value (0-100°C). This can then be displayed to the user, such as showing that the room temperature is +21°C.
In the SensorTag, the integrated sensors are primarily digital (more on this later), but the analog components include LEDs, whose brightness can be adjusted using PWM.
Pins¶
Okay, let’s start with the basics. Pin refers to the physical lead or connector of a microchip or component. The purpose of pins is to provide both electrical and mechanical connections to the circuit board. Each pin or lead has a specific purpose. The pinout defines the functions of each pin. As you’ll notice, sometimes a pin has multiple purposes, depending on the device's functionalities. This means that a device’s operation is limited to using only one of these functions at a time.
For example, below is a description of the pins on the Intel 4004 (the world’s first commercial microprocessor from 1971).
Pins and Bits¶
Let’s review a bit :-p from the previous Bit Operations lecture material. The general principle is that in the program code, each pin of a peripheral or microcontroller corresponds to a single bit, which we manipulate using C language bitwise operations.
- By setting this bit to either a logical zero or one, the voltage level of the physical pin changes, and thus we can programmatically control the pin and the connected component or peripheral.
- Similarly, by reading the state (i.e., voltage) of the physical pin, the corresponding bit is set to the appropriate logical value.
- Logical one (High) usually corresponds to the operating voltage (typically 3.3V or 5V), or a voltage within certain agreed-upon limits. Often, values higher than 2.0-2.7V are interpreted as a logical one.
- Logical zero (Low) corresponds to ground level (0V) or typically a voltage lower than 0.7V.
To simplify the programmer’s (and electronics designer’s) work, each pin is given a logical name associated with its function, such as
RESET
or IO_16
. These names usually align with the peripheral library names, and they represent constants that we use to perform bitwise operations on the corresponding bit.Example: Resetting a device by clearing (setting to 0) the RESET bit in the device’s control register. (Normally, you can’t reset a device directly from the program, but it makes for a dramatic example...)
control_register = control_register & ~(1 << RESET);
Some pins are designated as general-purpose (General Purpose I/O, GPIO), and the programmer is free to define their purpose, depending on what peripherals are connected to them. Logically, in microcontrollers, GPIO pins are sometimes grouped into I/O ports, so that, for example, 8 pins are treated as a single port. This is convenient if the device requires multiple I/O lines, as they can be logically handled as a single unit and managed easily as binary values. However, SensorTag treats all I/O pins as individual unless the programmer defines logical ports for the desired pins. This is not needed for the course project.
Memory-Mapped I/O¶
Memory-mapped I/O means that we reserve memory locations in the device's central memory as (device) registers connected to the peripheral's pins. By manipulating the bits in these registers through variables in the program, we interact with the peripheral. The memory locations can be seen as representing the data, address, and control buses required by the peripheral, based on the device’s needs:
- We use pointer variables by setting them to point to the memory locations allocated for the peripheral. These memory addresses are established during the device's design phase, and when the peripheral library is created, the addresses are configured correctly.
- We can control the peripheral through code by modifying the corresponding bits in these memory locations. This allows us to both set control bits and read the device’s status from them.
- As part of the control bus, there may be an interrupt pin that sends a signal (i.e., interrupt), which our program detects and responds to accordingly. More on interrupts shortly...
- In more complex devices, specific memory locations are mapped to the peripheral’s data bus (i.e., pins), enabling the transmission of information to the device in the form of binary values.
- In some more complex peripherals, like displays, there may be a separate address bus whose memory locations are internal to the device. Interesting.
Registers come in three types: address, control, and data registers. A peripheral may offer several registers of each type, or any combination of these types depending on the device’s implementation. For example, with SensorTag, we might need dozens of registers to operate a more complex peripheral. It’s quite a relief to have pre-made libraries for working with peripherals!
For each of these registers, we need to know:
- The general purpose, functionalities, and how to use them.
- The memory addresses where the registers are located, and whether the compiler environment provides constants and (pointer) variables for these.
- The purpose of each individual bit in the register.
As an example, here’s one register description from SensorTag. This peripheral (a circuit monitoring the battery voltage) has a data register that contains the battery voltage. A very useful register, indeed!
The image shows that the register size is 32 bits (
31-0
, the yellow field in the image). It’s noted that bits 31-11
are reserved for internal use by the device, but the other bits contain information relevant to us:- Bits
10-8
hold the integer part of the battery voltage. - Bits
7-0
hold the decimal part of the battery voltage, encoded in a specific way.
Now, if we want to know the battery voltage, we would query this memory-mapped data register and convert its value into a floating-point number, for example. SensorTag’s RTOS provides a macro for reading and writing register values called
HWREG
.uint32_t battery_voltage = HWREG(AON_BATMON_BASE + AON_BATMON_O_BAT);
In this case, we obtain the register’s memory address by adding the base address reserved for battery voltage monitoring (
AON_BATMON_BASE
) to the register’s offset within that memory area (AON_BATMON_O_BAT
).Datasheet¶
The manual for a component or microcontroller, known as the datasheet, provides an extremely detailed description of the internal workings and every integrated circuit. The datasheet serves as a reference when programming the hardware.
Datasheets are typically written in English, use highly specialized terminology, and are hundreds or even thousands of pages long for more complex devices. Even the datasheet for the relatively simple 8-bit Arduino microcontroller is almost 300 pages! So, while controlling pins on Arduino seems simple, there are dozens of pages in the datasheet that the programmer may not need to know about. For more complex devices, there may be additional manuals along with datasheets, such as the SensorTag Technical Reference Manual. This handbook has 1743 pages!
Fortunately, instead of combing through hundreds of pages of datasheets, development environments provide libraries, functions, macros, and constants that implement peripheral control and higher-level functions for us.
A Look into SensorTag¶
The image shows a block diagram of the functionalities of SensorTag’s microcontroller (TI Simplelink CC2650 Wireless MCU). As seen, the CC2650 is quite a versatile and complex device. It even integrates two separate ARM Cortex cores, each with its own program and RAM memory. The more powerful one (Main CPU, Cortex M3) is used for running user programs, while the other (Rf Core, Cortex M0) is dedicated solely to wireless communication. SensorTag is designed so that both cores are programmed separately and independently of each other. This has the advantage that the device's wireless communication protocol stack (wireless technology) can be swapped without affecting the user’s program.
Below is a description of the pinout for SensorTag’s microcontroller (TI CC2650). Pins are given logical names by the programmer (and electronics designer) to simplify their use, such as
RESET_N
or DIO_16
.Some pins are designated for general purpose use (General Purpose I/O, GPIO), and the programmer can freely define their purpose, depending on the peripherals connected to them. In SensorTag, during the design phase of the device, some GPIO pins have already been reserved for connecting peripherals to the microcontroller, which, of course, dictates their usage.
In SensorTag, the RTOS libraries provide us with these constants and pre-made function calls to programmatically use the pins.
Below is an example of constants defined for SensorTag’s I/O pins in header files. We can use the logical name of the pin in the code through these constants provided in the header files. For example, we can use the constant
IOID_18
to refer to the physical pin DIO_18, and then change the logical value of the corresponding bit in the code using bitwise operations. Convenient!#define IOID_18 0x00000012 // IO Id 18
#define IOID_19 0x00000013 // IO Id 19
#define IOID_20 0x00000014 // IO Id 20
Using I/O Pins¶
Next, we will explore using I/O pins in SensorTag with a code example. The available SensorTag-specific constants can easily be found in the header files that automatically appear in our software project Board.h and CC2650STK.h.
In the example below, we use one of SensorTag’s two buttons as an on/off switch for one of the device’s LEDs. This means we need to define two pins for the program to use: one for the button and one for the LED.
We will use the pre-built
Pin
library provided by the compiler environment. Since certain I/O pins in SensorTag are pre-wired to the buttons and LEDs, we can include their configurations in the code by using the PINCC26XX.h
header file. To activate the button in the program, four things must be done:- Declare global RTOS variables for initializing and handling the button.
- In this example,
buttonHandle
,buttonState
, and the arraybuttonConfig[]
. - Initialize the physical buttons as desired.
- In this example, the array
buttonConfig[]
. - Write a handler function for the button press.
- In this example, the interrupt handler function
buttonFxn
. - Finally, use the library functions in the
main
function to activate the I/O pins corresponding to the buttons.
Note! The SensorTag has two buttons and two LEDs. These definitions must be made separately for each pin. So, if we want to use all of them, we need to write four declarations and initializations in the code.
In this example program, each time a button is pressed, the function
buttonFxn
is executed, which toggles the state of the pin corresponding to the LED, thus turning the device’s LED on or off. The example is broken down below.#include <ti/drivers/PIN.h>
#include <ti/drivers/pin/PINCC26XX.h>
...
// RTOS global variables for handling the pins
static PIN_Handle buttonHandle;
static PIN_State buttonState;
static PIN_Handle ledHandle;
static PIN_State ledState;
// Pin configurations, with separate configuration for each pin
// The constant BOARD_BUTTON_0 corresponds to one of the buttons
PIN_Config buttonConfig[] = {
Board_BUTTON0 | PIN_INPUT_EN | PIN_PULLUP | PIN_IRQ_NEGEDGE,
PIN_TERMINATE // The configuration table is always terminated with this constant
};
// The constant Board_LED0 corresponds to one of the LEDs
PIN_Config ledConfig[] = {
Board_LED0 | PIN_GPIO_OUTPUT_EN | PIN_GPIO_LOW | PIN_PUSHPULL | PIN_DRVSTR_MAX,
PIN_TERMINATE // The configuration table is always terminated with this constant
};
// Button press interrupt handler function
void buttonFxn(PIN_Handle handle, PIN_Id pinId) {
// Toggle the state of the LED pin using negation
uint_t pinValue = PIN_getOutputValue(Board_LED0);
pinValue = !pinValue;
PIN_setOutputValue(ledHandle, Board_LED0, pinValue);
}
int main(void) {
Board_initGeneral();
// Enable the pins for use in the program
buttonHandle = PIN_open(&buttonState, buttonConfig);
if(!buttonHandle) {
System_abort("Error initializing button pins\n");
}
ledHandle = PIN_open(&ledState, ledConfig);
if(!ledHandle) {
System_abort("Error initializing LED pins\n");
}
// Set the button pin’s interrupt handler to function buttonFxn
if (PIN_registerIntCb(buttonHandle, &buttonFxn) != 0) {
System_abort("Error registering button callback function");
}
BIOS_start();
return (0);
}
Let’s break down the example program step by step.
RTOS Variables for Pin Usage¶
First, we introduce a set of variables for each pin we are using, which the RTOS requires. Again, we need handles for the pins, which are declared using the
Pin_Handle
variable. Another variable that the RTOS needs is the pin state, which is declared using the Pin_State
variable. We don’t need these variables in our own code, but we’ll keep the RTOS happy by including them.// RTOS variables for pin usage
static PIN_Handle buttonHandle;
static PIN_State buttonState;
static PIN_Handle ledHandle;
static PIN_State ledState;
Here we declare variables for two pins, one for the button and one for the LED. For each pin, we need the two variables above:
buttonHandle
and buttonState
for the button pin, and the same for the LED. Piece of cake.Pin Initialization¶
Next, we initialize each pin as either input or output in its respective configuration table. All the constants and their purposes used here can be found in the Pin library documentation, but the following configurations are sufficient for the course.
PIN_Config buttonConfig[] = {
Board_BUTTON0 | PIN_INPUT_EN | PIN_PULLUP | PIN_IRQ_NEGEDGE,
PIN_TERMINATE
};
PIN_Config ledConfig[] = {
Board_LED0 | PIN_GPIO_OUTPUT_EN | PIN_GPIO_LOW | PIN_PUSHPULL | PIN_DRVSTR_MAX,
PIN_TERMINATE
};
Note that the variables
buttonConfig
and ledConfig
are arrays with two elements. In the first element, we use a bitwise OR operation on four constants, and the second element is the constant PIN_TERMINATE
.In the first element, the constants are interpreted as follows. The first constant (configuration bit) is the identifier for the corresponding button or LED on the SensorTag, either
Board_BUTTON0
or Board_BUTTON1
for the buttons, and Board_LED0
or Board_LED1
for the LEDs.The second constant defines the pin’s purpose, i.e., whether it is used as input or output. For an input pin, we read its state (for example, whether the button is pressed or not), and for an output pin, we set its state (for example, turning the LED on or off). After all this explanation, in the example, we set the button pin as input and the LED pin as output.
- A pin is set as input using the constant
PIN_INPUT_EN
. - A pin is set as output using the constant
PIN_GPIO_OUTPUT_EN
.
The third configuration bit tells us the initial state of the pin:
- The constant
PIN_GPIO_LOW
sets the pin voltage to ground level (0V, GND), meaning the LED is off in this code. - The constant
PIN_GPIO_HIGH
would set the voltage to the operating voltage (3
- Additionally, the RTOS allows for other configuration parameters related to the electrical behavior of the pins, such as
PIN_PULLUP
,PIN_PUSHPULL
, andPIN_DRVSTR_MAX
. But as promised, no deep knowledge of electronics is needed for this course.
In the button configuration, we also see the constant
PIN_IRQ_NEGEDGE
, which sets the pin to trigger an interrupt in the program whenever its state changes on the falling edge. That is, when the button is pressed, the voltage drops to ground level. Or, when the button is released, the voltage rises to the operating voltage (rising edge), and an interrupt for that can be caught using the constant PIN_IRQ_POSEDGE
. More about pin interrupts will be covered in the next material.The configuration table always ends with the constant
PIN_TERMINATE
.Pin Interrupt Handler Function¶
For pins set as inputs, we generally need a handler function that specifies what action to take when the button is pressed, causing an interrupt. In this case, we have implemented the handler function
buttonFxn
.void buttonFxn(PIN_Handle handle, PIN_Id pinId) {
// Toggle the state of the LED pin using negation
// Not using pointers but direct values!!!
uint_t pinValue = PIN_getOutputValue(Board_LED0);
pinValue = !pinValue;
PIN_setOutputValue(ledHandle, Board_LED0, pinValue);
}
Here’s how the function works. First, we read the current state of the LED pin (on "1" / off "0") using the
PIN_getOutputValue
function, storing the value in the pinValue
variable. This function takes the constant corresponding to the pin (Board_LED0
) as its argument. Then, we negate the value, meaning we toggle the LED state between on and off. The new state is then set using the PIN_setOutputValue
function.Initializing the Pins in the Program¶
Next, we move on to the
main
function. Pins are reserved for use in our program using the Pin_open
function call, which takes the RTOS pin variables and the configuration we introduced earlier as parameters.// Initialize the LED in the program
ledHandle = PIN_open(&ledState, ledConfig);
if (!ledHandle) {
System_abort("Error initializing LED pin\n");
}
// Initialize the button in the program
buttonHandle = PIN_open(&buttonState, buttonConfig);
if (!buttonHandle) {
System_abort("Error initializing button pin\n");
}
// Register the button interrupt handler
if (PIN_registerIntCb(buttonHandle, &buttonFxn) != 0) {
System_abort("Error registering button callback function");
}
The RTOS handle tasks and interrupt handler functions as equivalent functionalities. We’ll talk more about interrupts shortly, but for now, when the button’s state changes (on the falling edge, as defined by the constant
PIN_IRQ_NEGEDGE
), an interrupt is triggered. The function PIN_registerIntCb
sets the function to be executed in response to the interrupt, i.e., the handler. In this program, the function buttonFxn
is the handler for this interrupt.Note! We could have implemented the same button state check using a superloop, continuously polling the button pin state in each iteration, and taking action if it changed. That’s another example of superloop-based design, but as we can see, the handler makes things easier.
Conclusion¶
Aside from memory-mapped I/O, another option is port-mapped I/O, where registers are managed through separate in and out instructions. However, this mechanism is not used by SensorTag.
But hey… based on this material, you now know how to write an embedded program that blinks an LED when it detects a button press! Is it time for cake and coffee?
Give feedback on this content
Comments about this material