понедельник, 14 декабря 2015 г.

Калибровка резистивного тачскрина.

Сегодня поговорим о такой коварной штуке, как тачскрин. Конструктивно это две пленки на которых нанесен резистивный состав, в точке нажатия рабочие поверхности соприкасаются и получается некое подобие делителя напряжения по каждой из осей X и Y. Напряжение, снятое с этих делителей, измеряется либо любым доступным АЦП, либо специализированным контроллером, в котором, помимо двух каналов АЦП, обычно есть дополнительные плюшки. Например, в контроллере XPT2046 (китайский функциональный аналог ADS7843) есть встроенный термодатчик, внешний канал для измерения напряжения, допустим, батареи, и, что самое главное, сигнал запроса на прерывание при нажатии. Размер тачскрина всегда больше размера видимой области дисплея на которую он устанавливается, из-за чего координаты полученные в точке соответствующей угловому пикселю экрана, не будут равны нулю.

 Кроме того, разрешение координатной сетки тача пропорционально разрядности АЦП и явно больше разрешения экрана. Из 12 бит АЦП используется 11 реальных, то есть 2048 отсчетов. В моем случае реальные значения координат лежали в диапазоне от 150-200 до 1800. Еще одна интересная особенность тачскрина, с которым пришлось работать - начало координат расположено в правом верхнем углу, а не в левом как мы привыкли. На рисунке выше я это обозначил.
Ну хорошо, а что тут сложного, спросите вы? Снимаем координаты угловых точек (достаточно даже двух по диагонали), вычисляем рабочий диапазон, считаем коэффициент, на который нужно поделить координаты тачскрина, чтобы получить привязку к пикселям дисплея. Не забываем про отступы от начала координат и о том что координаты по оси X надо пересчитать в обратном порядке, то есть слева направо. Т.е. в результате имеем такой набор формул:

x_coord = 239 - ((x - x_offset)/x_slope),
y_coord = (y - y_offset)/y_slope,

где x_coord и y_coord - полученные координаты в масштабе дисплея (у меня 240*320), x и y - исходные координаты полученные с контроллера, x_offset и y_offset отступы от начала координат, x_slope и y_slope - коэффициенты пропорциональности координатных сеток дисплея и тачскрина. Для калибровки по двум диагональным точкам расчет этих коэффициентов выглядит так:

x_slope = (x_top_left - x_bot_right)/240,
y_slope = (y_top_left - y_bot_right)/320.

Таким образом, для работы нам нужно всего 4 константы, вычисленные на этапе калибровки - два значения отступа по осям, и два коэффициента пересчета. Все это прекрасно работает до тех пор, пока сопротивление резистивного слоя изменяется линейно в зависимости от положения точки нажатия. А если нет? Старение или износ резистивного слоя, попадание жидкостей, нарушение технологии производства, да мало ли какие могут быть причины. 
Вышеприведенный алгоритм у меня отлично работал, пока я не протер залапаный экран специальной влажной салфеткой для очистки мониторов. Уж не знаю, сильно давил или пары формальдегида просочились, но откалиброванный экран таковым быть перестал. Отклик по оси Y пострадал не особо, а вот коэффициент x_slope стал сильно отличатся в верхней и в нижней частях экрана. Кроме того, изменился и отступ координат. С этим нужно было что то делать, погрешность местами достигала 60 пикселей. Некоторое время спустя родилась функция калибровки версии 2.0. Теперь измерение координат проводится по 4-м точкам в углах и на выходе имеем больший набор констант. К основным добавляются еще 4 коэффициента, которые учитывают изменения отступа по обоим осям относительно начального, и изменения наклона характеристик (x_slope и y_slope).  Итак:

void XPT_Calibrate(void)
{
uint32_t  x_top_left, y_top_left, x_bot_right, y_bot_right;
uint32_t  x_top_right, y_top_right, x_bot_left, y_bot_left;
float  x_slope_top, x_slope_bot, y_slope_left, y_slope_right;
uint32_t  x_offset_top, x_offset_bot, y_offset_left, y_offset_right;

        while(x_top_left == 0) // Top left corner
{
LCD_clr();
TFT_DrawString("Press red circle", 35, 150, 1, RED);
TFT_FillCircle(9, 9, 8, RED);
while(XPT_isPressed() == 0);
XPT_GetPosition(&x_top_left, &y_top_left);
while (XPT_isPressed()) delay(1);
};

while(x_top_right == 0) // Top right corner
{
LCD_clr();
TFT_DrawString("Press red circle", 35, 150, 1, RED);
TFT_FillCircle(230, 9, 8, RED);
while(XPT_isPressed() == 0);
XPT_GetPosition(&x_top_right, &y_top_right);
while (XPT_isPressed()) delay(1);
};

while(x_bot_left == 0) // Bottom left corner
{
LCD_clr();
TFT_DrawString("Press red circle", 35, 150, 1, RED);
TFT_FillCircle(9, 310, 8, RED);
while(XPT_isPressed() == 0);
XPT_GetPosition(&x_bot_left, &y_bot_left);
while (XPT_isPressed()) delay(1);
};

while(x_bot_right == 0) // Bottom right corner
{
LCD_clr();
TFT_DrawString("Press red circle", 35, 150, 1, RED);
TFT_FillCircle(230, 310, 8, RED);
while(XPT_isPressed() == 0);
XPT_GetPosition(&x_bot_right, &y_bot_right);
while (XPT_isPressed()) delay(1);
};

/*********** TOP ***************/
if(x_top_left > x_top_right)
{
x_slope_top = ((float)x_top_left - (float)x_top_right)/220;
x_offset_top = x_top_right - 10*x_slope_top;
x_offset = x_offset_top;
x_slope = x_slope_top;
};

/*************** LEFT **************/
if(y_top_left > y_bot_left)
{
y_slope_left = ((float)y_top_left - (float)y_bot_left)/300;
y_offset_left = y_bot_left - 10*y_slope_left;
y_offset = y_offset_left;
y_slope = y_slope_left;
};

/************* BOTTOM ***************/
if(x_bot_left > x_bot_right)
{
x_slope_bot = ((float)x_bot_left - (float)x_bot_right)/220;
x_offset_bot = x_bot_right - 10*x_slope_bot;
};

/************** RIGHT ***************/
if(y_top_right > y_bot_right)
{
y_slope_right = ((float)y_top_right - (float)y_bot_right)/300;
y_offset_right = y_bot_right - 10*y_slope_right;
};

/************** COEFFICIENTS ***************/
kxs = ((float)x_slope_top - (float)x_slope_bot)/320;   //Изменение x_slope
kys = ((float)y_slope_left - (float)y_slope_right)/240;  //Изменение y_slope
kxo = ((float)x_offset_top - (float)x_offset_bot)/320;   //Изменение x_offset
kyo = ((float)y_offset_left - (float)y_offset_right)/240;  //Изменение y_offset

       //Далее, вычисленные константы сохраняются во FLASH-память микроконтроллера
}

Внимательный читатель заметит, что при расчете коэффициентов привязки к разрешению дисплея константы не равны 240 и 320. Для удобства нажатия пальцем или стилусом, я сделал отступ контрольных точек от углов на 10 пикселей, следовательно расстояние между ними уменьшилось на 20 пикселей по каждой из осей.
Расчет координат:

x_coord = 239 - ( (x - (x_offset - (kxo*((y - y_offset)/y_slope)))) / (x_slope - (kxs*((y - y_offset)/y_slope))) );

y_coord = (y - (y_offset - (kyo*((x - x_offset)/x_slope)))) / (y_slope - (kys*((x - x_offset)/x_slope)));

Конечно же, такой способ не сможет исправить сильные нелинейные отклонения, и, возможно, было бы лучше применить алгоритм табличной коррекции, но данная реализация меня полностью устраивает. Сейчас погрешность в худшем случае достигает 5-7 пикселей, что при нажатии пальцем не заметно.