This tutorial shows how to create a touch-controlled LED light strip to place it over the kitchen counter, for example. For this tutorial, the LED strip is abstracted by the onboard LED instead of using MOSFET (with a proper MOSFET driver circuit) that would control a higher voltage LED strip. The touch control must be encoded due to the presence of only a single touch pad for multiple actions.
Capacitive touch (CAPT) control allows you to place the button under the product cover for keeping the design of the product consistent. You can cover touch pads with dielectric materials and hide them inside the product.
The CAPT peripheral in YH mode works by counting cycles until the charge is transferred from the small charging capacitor to the larger measuring one. A human finger or capacitive stylus causes partial grounding of the charge that causes an increase or decrease of the cycles needed to fully charge the measuring capacitor.
A counter triggers a toggle of the output pin’s logic level that creates a PWM signal on that pin. The peripheral increases the counter with each peripheral clock cycle and when the counter matches the required number the output logic level is toggled.
A counter triggers interrupts after a defined number of peripheral clock cycles. This peripheral can be programmed to create an interrupt repetitively or only after a specified number of cycles.
Figure 1 Block diagram
As mentioned previously, the board has only a single touch pad, and there are 3 different actions in this tutorial that must be controlled by it.
The touch pad is periodically measured for touch or no touch events. Such events can store the state of the touch pad in a ring buffer. The ring buffer represents the history of the touches over a short time and can be analyzed for a long uninterrupted touch or double tap.
Figure 2 Encoding state diagram
Figure 3 Creating a project in MCUXpresso IDE
In the project setup wizard, click the Drivers tab in the Components section in the lower half of the wizard. Select CAPT, CTIMER, and MRT in this table.
Figure 4 Adding required peripheral drivers to the project
To make active the newly created project (if it is not active yet), select it and open the Pins tool either in the context menu or by clicking the Config tools button.
Figure 5 The Config tools button
Open the Pins tool and create a functional group for our application. Set it to be called by the default initialization function and set other functional groups not to be called by the initialization function.
Figure 6 Properties of Pins tool functional groups
Figure 7 Routing details
Steps:
NOTE:
The LED is connected to the MCU in a way that 90 % of the duty cycle would create only 10 % of the on-time. To achieve the expected behavior, invert the signal. The CAPT peripheral requires the mode of the pins to be inactive to work properly. Activating a pull-up, pull-down resistor, or any other integrated circuit causes false measurements, and touch control does not work or causes false touch events.
Open the functional group's dialog and select BOARD_BootClockFRO30M as the initialized functional group.
Figure 8 Properties of Clocks tool functional groups
Steps:
Figure 9 System clock divider setting
Figure 10 Selection of clock source for CAPT peripheral
NOTE:
The system clock is slowed down to 10 MHz because it is used for the MRT peripheral as a source and the period with 30 MHz is too short for the purposes of this tutorial.
This peripheral is used to delay the execution of events.
Steps:
Figure 11 Configuration of the MRT peripheral
NOTE:
For this application, the delay must be 400 ms. You can achieve it only by using a 10 MHz clock or by creating a counter in memory that is increased every time the interrupt handler is executed. Only when the counter has the required value, the delayed event executes, and the counter is reset. You need a counter because the MRT register, which has only 24 bits and overflows before reaching the number of ticks calculated from 400 ms.
For the PWM output, configure the CTIMER peripheral in the following way.
Figure 12 Configuration of the CTIMER peripheral
Steps:
NOTE:
The prescaler set to 1 (no prescaling) allows the most precise duty cycle (for example, if the resolution must be more than one percent). The frequency of the output must be in the range between not noticing the flickering of the LED and potential unintentional overheating of the MOSFET that could be connected to the output due to high frequency. The frequency of 100 Hz is selected because unless the board is moving the LED looks dim and does not flicker, and the potential MOSFET does not overheat. The duty cycle is set in ticks, and at runtime is computed as a percentage.
Steps:
Figure 13 Configuration of the CAPT peripheral
NOTE:
The CAPT peripheral requires the input clock to be maximally 4 MHz. Therefore, a frequency of 2 MHz was chosen as it was easily achievable and easy to calculate the resulting period.
On the breakout board, only signal 0 is routed to the touch pad, and other signals can be routed to external connectors on the board.
For the CAPT to function properly, calibrate touch and no touch events to obtain the number of cycles required to transfer the charge. Enable the usage of the calibration method and either use the default name or provide your name of the function. The other method is to use a constant that was measured during development. However, keep in mind that the measured value might differ when the board is inside the final product and covered by a layer of material.
A touch event can happen either when the counter is higher or lower than the threshold. The selection depends on the final product design. A value higher than the threshold option was used in this tutorial because the finder/stylus grounds a part of the charge that is transferred and causes a higher cycles count required to fully charge the measured capacitor.
The timeout count is a counter of the number of ticks when a timeout interrupt is created. It is not used in this example, so set the highest value to avoid timeout instead of touch or no touch events.
The measurement delay setting represents the number of ticks until the measuring capacitor is sampled. When the YH method is used, it must be measured immediately.
The reset delay is a wait after a touch or no touch event and waiting discharges the capacitors before the next measurement cycle. The poll delay is a delay between repetitive measurement cycles and should be high since the physical movement of the finger/stylus is slow relative to the speed of the MCU and unnecessary measurements can be avoided.
The changes you made must be written on the disk before using them in the application code. Open the Update code dialog and confirm the changes.
Figure 14 Update code action on the main panel
Figure 15 Update code dialog
For the convenience of working with the code, create the following defines:
#define WAIT_MS 400U // How long to wait before making decisions
#define POLL_WAIT_MS 100U // How long it takes before next CAPT measurement
#define TOUCH_DATA_SIZE 32U // Size of ring buffer
Create the ring buffer, index of the current position, and functions that fill and analyze it. The ring buffer must hold 2-3 seconds of data and since the poll delay of the CAPT is 100 ms then 32 is sufficient.
Functions for analysis scan the ring buffer and let the application know whether a double tap was performed, for how many measurements the touch pad was held, and how many measurements ago the touch pad was touched. Create functions for storing and clearing data in the ring buffer.
unsigned int g_touchDataIndex = 0;
bool g_touchData[TOUCH_DATA_SIZE] = {false};
/**
* Checks whether double tap was performed
*/
bool wasDoubleTap() {
bool untouched = false;
for (int i = 0; i < TOUCH_DATA_SIZE; i++) {
if (!g_touchData[(g_touchDataIndex - i) % TOUCH_DATA_SIZE]) {
untouched = true;
} else if (untouched) {
return true;
}
}
return false;
}
/**
* Returns number of data writes that represent continuous touch.
*/
int getContinuousTouchCount() {
int count = 0;
for (int i = 0; i < TOUCH_DATA_SIZE; i++) {
if (g_touchData[(g_touchDataIndex - i) % TOUCH_DATA_SIZE]) {
count++;
} else {
return count;
}
}
return -1;
}
/**
* Returns number that represents count of data writes until tap is found in history.
* Returns -1 in case nothing is found.
*/
int getPreviousTouch() {
for (int i = 0; i < TOUCH_DATA_SIZE; i++) {
if (g_touchData[(g_touchDataIndex - i) % TOUCH_DATA_SIZE]) {
return i;
}
}
return -1;
}
/**
* Clears touch data in ring buffer
*/
void clearTouchData() {
for (int i = 0; i < TOUCH_DATA_SIZE; i++) {
g_touchData[(g_touchDataIndex - i) % TOUCH_DATA_SIZE] = false;
}
}
/**
* Stores touch data in ring buffer
*/
void storeTouchData(bool data) {
++g_touchDataIndex;
g_touchDataIndex = g_touchDataIndex % TOUCH_DATA_SIZE;
g_touchData[g_touchDataIndex] = data;
}
Create storage for the current state of the PWM signal and the state machine. Create a function to change the PWM output of the CTIMER channel. The function must check the wanted duty cycle to be in a reasonable range.
int currentPWMDuty = 50;
bool PWMOutputEnabled = true;
enum STATE {
IDLE = 0,
WAITING_FOR_TIMER = 100,
TOGGLE = 200,
INCREASE = 300,
DECREASE = 400
};
enum STATE state = IDLE;
/**
* Sets PWM output according to currently wanted duty cycle if output is enabled
*/
void setPWMOutput() {
if (currentPWMDuty < 5) {
currentPWMDuty = 5;
}
if (currentPWMDuty > 100) {
currentPWMDuty = 100;
}
int ticks = PWMOutputEnabled ? ((CTIMER0_PWM_PERIOD / 100) * currentPWMDuty) : 0;
CTIMER_SetupPwmPeriod(CTIMER0_PERIPHERAL, CTIMER0_PWM_0_CHANNEL, CTIMER0_PWM_PERIOD, ticks, false);
}
Calibration of the CAPT peripheral must be done in the CAPT_doCalibration function as it was set in the Peripherals tool.
/*
* @brief Calibration of the CAPT peripheral. Returns number of charge cycles in not activated state
*/
uint32_t CAPT_doCalibration(CAPT_Type *base) {
// Here can be placed code that uses one time polling multiple times and the results would be averaged
// or a constant returned
return 70;
}
When a 400 ms delay passes, the MRT peripheral runs this interrupt and after clearing the status flag the logic follows. After obtaining information on whether the touch pad is touched and for how many measurement cycles it is not held, the decision of what state follows is made.
/* MRT0_IRQn interrupt handler */
void MRT0_IRQHANDLER(void) {
/* Add for ARM errata 838869, affects Cortex-M4, Cortex-M4F
Store immediate overlapping exception return operation might vector to incorrect
interrupt. */
#if defined __CORTEX_M && (__CORTEX_M == 4U)
__DSB();
#endif
uint32_t mrtWaitStatus = MRT_GetStatusFlags(MRT0, MRT0_CHANNEL_WAIT);
MRT_ClearStatusFlags(MRT0, MRT0_CHANNEL_WAIT, mrtWaitStatus);
if (mrtWaitStatus != 0) {
int previousTouch = getPreviousTouch();
bool currentlyTouched = g_touchData[g_touchDataIndex];
if (!currentlyTouched && previousTouch >= (WAIT_MS / POLL_WAIT_MS) - 1)
{ // Tap, currently not pressed and no taps after the initial one
PWMOutputEnabled = !PWMOutputEnabled;
setPWMOutput();
clearTouchData();
state = IDLE;
} else if (currentlyTouched && getContinuousTouchCount() >= (WAIT_MS /
POLL_WAIT_MS) && PWMOutputEnabled) { // Tap, currently pressed and touch pad held
until now
state = INCREASE;
} else if (currentlyTouched && wasDoubleTap() && PWMOutputEnabled) { // Tap,
currently pressed and double tap was performed
state = DECREASE;
} else {
state = IDLE; // Error -> reset
}
}
}
The interrupt from CAPT is performed every 100 ms and the data with interrupt status must be read and status cleared. When the touch pad is touched, the current state is checked and one of the following actions happens:
When the touch pad is not touched, the INCREASE or DECREASE state is changed to IDLE state and ring buffer is cleared. In other states nothing happens.
/* CMP_CAPT_IRQn interrupt handler */
void CAPT_IRQHANDLER(void) {
/* Add for ARM errata 838869, affects Cortex-M4, Cortex-M4F
Store immediate overlapping exception return operation might vector to incorrect
interrupt. */
#if defined __CORTEX_M && (__CORTEX_M == 4U)
__DSB();
#endif
uint32_t mask;
capt_touch_data_t g_data;
CAPT_GetTouchData(CAPT, &g_data);
mask = CAPT_GetInterruptStatusFlags(CAPT);
CAPT_ClearInterruptStatusFlags(CAPT, mask);
if (mask & kCAPT_InterruptOfYesTouchEnable && g_data.yesTouch)
{
PRINTF("Touch\n");
storeTouchData(true);
switch(state) {
case IDLE: {
MRT_StartTimer(MRT0, MRT0_CHANNEL_WAIT,
MRT0_CHANNEL_WAIT_TICKS);
state = WAITING_FOR_TIMER;
break;
}
case INCREASE: {
currentPWMDuty += 5;
setPWMOutput();
break;
}
case DECREASE: {
currentPWMDuty -= 5;
setPWMOutput();
break;
}
default: {
// Do not react - unexpected state
}
}
}
if (mask & kCAPT_InterruptOfNoTouchEnable) {
PRINTF("No touch\n");
storeTouchData(false);
switch(state) {
case INCREASE: {
state = IDLE;
clearTouchData();
break;
}
case DECREASE: {
state = IDLE;
clearTouchData();
break;
}
default: {
// Do not react - unexpected state
}
}
}
}
/*
* @brief Application entry point.
*/
int main(void) {
/* Init board hardware. */
BOARD_InitBootPins();
BOARD_InitBootClocks();
BOARD_InitBootPeripherals();
#ifndef BOARD_INIT_DEBUG_CONSOLE_PERIPHERAL
/* Init FSL debug console. */
BOARD_InitDebugConsole();
#endif
while(1) {
__asm__("nop");
}
return 0;
}
Now you can compile the application and upload it to the board. You do not require additional steps to make the application work.