FSR阵列入门教程
https://www.sensitronics.com/tutorials/fsr-matrix-array/index.php
教程:读取矩阵数组 Sensitronics 的 MatrixArray 产品由多个以网格形式排列的力传感电阻元件组成。MatrixArray 提供 N 输入多点触控 + 力传感功能,是乐器、PC 输入设备和其他创新设计的基础。 为了帮助您入门,这里提供了一个基本示例,演示如何使用 Arduino 和一些常用部件连接和读取我们的一个 16x10 MatrixArray。

所需零件/技能
要启动并运行此演示,您需要以下列出的部件。我们假设您具备一些基本的 Arduino 编程经验,但实际上,我们已经提供了完整的代码示例 - 只要您知道如何上传 Arduino 代码并打开串行终端窗口,您就可以根据需要将代码复制粘贴到可运行的演示中。
- 16x10 ThruMode 矩阵阵列
- Arduino(最好是Uno,但任何都可以)
- 74HC595移位寄存器(x2)
- 74HC4051 模拟多路复用器 (x2)
- 20k电阻(x1)
- 无焊面包板
- 跳线
可以用更少的硬件来实现这一点吗?
如果您查看零件清单,认为 Arduino 的 IO 引脚足够驱动 16 列并读取 10 行,且仅需一个外部多路复用器,那么您是对的。我们选择使用 2 个移位寄存器和 2 个多路复用器有两个原因,可以避免混合板载和外部 IO:
1. 它使设计和代码保持简洁。本项目仅供参考,因此我们希望最大限度地提高清晰度。2
. 它具有高度可扩展性!按照相同的连接模式,只需更改两行代码,即可扩展此示例以扫描 96 x 96(9216 个单元)矩阵。
那么,让我们来看看电路是如何连接的......
第 2 部分:硬件连接
示意图
现在您已经收集了上一节列出的所有必需零件,是时候开始动手制作了。
所有零件均采用 DIP/通孔封装,因此我们可以直接用面包板搭建整个电路,无需焊接。开门见山,以下是原理图:

它是如何工作的?
如果您曾经通过查看分压器两端的电压来测量单元件FSR,那么我们这里也采用同样的方法。
但在本例中,我们通过为单个驱动列供电来选择测量点,启用一个多路复用通道,以便ADC检测到单行数据。在下一节中,Arduino代码演示了一种扫描模式,可以按顺序读取所有160个单元。
它会是Ghost吗?
“重影”是指传感器或按键矩阵中常见的问题。我们不再赘述,因为网络上已经有其他文章详细讨论过了。
这里有一篇很好的文章,解释了重影(与电脑键盘电路相关)。
简而言之,问题在于,当多个单元被按下时,行和列之间可能会创建意外的路径,导致未按下单元的读数错误。这是我们设计的问题吗?嗯,部分原因。如果我们使用带有三态输出的移位寄存器来驱动列,并将未驱动的列设置为高阻态,我们预计会看到像键盘矩阵一样的“鬼影”点。但是,我们打算将未驱动的列接地。这些意外的路径实际上与传感电阻 (R1) 并联,当同一列或同一行中的其他单元被按下时,这会降低某些单元的视在灵敏度。因此,我们可以说这个例子演示了“反向鬼影”。
最终结果是,这种设计在多点触摸时无法提供准确的绝对读数。然而,它能够提供每个单元相对力的良好测量结果,相对于传感器片上的其他力。
观察电路,另一个问题是“移位寄存器的高/低输出会相互短路吗?”。在这种情况下,答案是“不会”,因为在这个特定的ThruMode矩阵中,每个单元的最小电阻(满载时)在2.5k-5.0k之间,这将任何移位寄存器引脚的最大拉/吸电流限制在1-2 mA。如果我们使用的传感器矩阵中,单元的电阻可能会被强制接近零欧姆,那么是的,我们很可能会熔化传感器膜并炸毁移位寄存器。
有解决这一限制的办法吗?
是的,可以设计放大器级来取代简单的分压器输出,以提高单元独立性并滤除不需要的信号成分。然而,事情会变得有点复杂,而且具体应用也有所不同。因此,为了本教程的基础,这方面的研究留给读者自己去完成。
第 3 部分:Arduino 代码
现在硬件已经连接好了,是时候编写一些代码了。
要读取 MatrixArray 的单个交点(或“单元”),我们需要执行以下步骤:
- 使用 +5V 驱动电池行
- 启用单元的列(多路复用器通道)
- 在引脚 A0 上读取 ADC 读数
- 通过串口输出读数
很简单,所以从这里开始,我们将编写一个扫描模式来高效地驱动和读取矩阵的每个单元。回想一下,在本例中,我们将移位寄存器的驱动引脚称为“行”,将复用的读取引脚称为“列”。我们将按顺序启用每一列,当每一列处于活动状态时,我们将每一行驱动为高电平并进行读取。
下面的代码示例实现了一个扫描模式,并将输出连续地转储到串行终端。
/********************************************************************************************************** * Project: MatrixArray.ino * By: Chris Wittmier @ Sensitronics LLC * LastRev: 09/09/2015 * Description: FSR MatrixArray Demonstration of Sensitronics' 16x10 element resistive ThruMode. Scanning * electronics consist of 2 HCT595 shift registers, and 2 4051 multiplexers connected to Arduino Uno. **********************************************************************************************************/ /********************************************************************************************************** * MACROS / PIN DEFS **********************************************************************************************************/ #define BAUD_RATE 115200 #define ROW_COUNT 10 #define COLUMN_COUNT 16 #define PIN_ADC_INPUT A0 #define PIN_SHIFT_REGISTER_DATA 2 #define PIN_SHIFT_REGISTER_CLOCK 3 #define PIN_MUX_CHANNEL_0 4 //channel pins 0, 1, 2, etc must be wired to consecutive Arduino pins #define PIN_MUX_CHANNEL_1 5 #define PIN_MUX_CHANNEL_2 6 #define PIN_MUX_INHIBIT_0 7 //inhibit = active low enable. All mux IC enables must be wired to consecutive Arduino pins #define PIN_MUX_INHIBIT_1 8 #define ROWS_PER_MUX 8 #define MUX_COUNT 2 #define CHANNEL_PINS_PER_MUX 3 /********************************************************************************************************** * GLOBALS **********************************************************************************************************/ int current_enabled_mux = MUX_COUNT - 1; //init to number of last mux so enabled mux increments to first mux on first scan. /********************************************************************************************************** * setup() **********************************************************************************************************/ void setup() { Serial.begin(BAUD_RATE); pinMode(PIN_ADC_INPUT, INPUT); pinMode(PIN_SHIFT_REGISTER_DATA, OUTPUT); pinMode(PIN_SHIFT_REGISTER_CLOCK, OUTPUT); pinMode(PIN_MUX_CHANNEL_0, OUTPUT); pinMode(PIN_MUX_CHANNEL_1, OUTPUT); pinMode(PIN_MUX_CHANNEL_2, OUTPUT); pinMode(PIN_MUX_INHIBIT_0, OUTPUT); pinMode(PIN_MUX_INHIBIT_1, OUTPUT); } /********************************************************************************************************** * loop() **********************************************************************************************************/ void loop() { for(int i = 0; i < ROW_COUNT; i ++) { setRow(i); shiftColumn(true); shiftColumn(false); //with SR clks tied, latched outputs are one clock behind for(int j = 0; j < COLUMN_COUNT; j ++) { int raw_reading = analogRead(PIN_ADC_INPUT); byte send_reading = (byte) (lowByte(raw_reading >> 2)); shiftColumn(false); printFixed(send_reading); Serial.print(" "); } Serial.println(); } Serial.println(); delay(200); } /********************************************************************************************************** * setRow() - Enable single mux IC and channel to read specified matrix row. **********************************************************************************************************/ void setRow(int row_number) { if((row_number % ROWS_PER_MUX) == 0) //We've reached channel 0 of a mux IC, so disable the previous mux IC, and enable the next mux IC { digitalWrite(PIN_MUX_INHIBIT_0 + current_enabled_mux, HIGH); //Muxes are enabled using offset from MUX_INHIBIT_0. This is why mux inhibits MUST be wired to consecutive Arduino pins! current_enabled_mux ++; if(current_enabled_mux >= MUX_COUNT) { current_enabled_mux = 0; } digitalWrite(PIN_MUX_INHIBIT_0 + current_enabled_mux, LOW); //enable the next mux, active low } for(int i = 0; i < CHANNEL_PINS_PER_MUX; i ++) { if(bitRead(row_number, i)) { digitalWrite(PIN_MUX_CHANNEL_0 + i, HIGH); } else { digitalWrite(PIN_MUX_CHANNEL_0 + i, LOW); } } } /********************************************************************************************************** * shiftColumn() - Shift out a high bit to drive first column, or increment column by shifting out a low * bit to roll high bit through cascaded shift register outputs. **********************************************************************************************************/ void shiftColumn(boolean is_first) { if(is_first) { digitalWrite(PIN_SHIFT_REGISTER_DATA, HIGH); } digitalWrite(PIN_SHIFT_REGISTER_CLOCK, HIGH); digitalWrite(PIN_SHIFT_REGISTER_CLOCK, LOW); if(is_first) { digitalWrite(PIN_SHIFT_REGISTER_DATA, LOW); } } /********************************************************************************************************** * printFixed() - print a value padded with leading spaces such that the value always occupies a fixed * number of characters / space in the output terminal. **********************************************************************************************************/ void printFixed(byte value) { if(value < 10) { Serial.print(" "); } else if(value < 100) { Serial.print(" "); } Serial.print(value); }
第四部分:初步结果

串行终端输出
使用第三部分的代码对 Arduino 进行编程后,我们打开一个串行终端窗口,看到一系列以网格形式排列的数据转储。当 MatrixArray 为空/未加载时,我们应该看到全零或非常低的读数。
如果所有连接都正确,用食指按下 MatrixArray 的中心,我们应该会看到一些读数……
成功!在右侧的屏幕截图中,我们看到 Matrix 中心附近的读数,这些读数与施加的力成比例变化。
范围视图
现在我们已经验证了项目基本可以正常工作,我们将拿出探针,在数字示波器上查看系统的运行情况。这将清晰地展示系统运行/时序,或许还能提供一些关于哪些方面可以进行优化的线索。
将水平窗口设置为显示完整的 MatrixArray 扫描周期,并根据 Arduino 引脚名称标记信号,我们观察到的结果如下:

从图像中,我们可以看到在多行/列上检测到了一次手指按压(信号 A0)。我们还注意到,单个扫描周期大约需要 57 毫秒才能完成。如果这对于我们的设计来说足够快,那么我们可以就此打住。但对于许多设计来说,这太慢了——你可能不会把 17Hz 刷新率的触摸板称为“响应迅速”。
所以,让我们开始优化吧。
第 5 部分:优化性能
在第四部分中,我们观察到 MatrixArray 上的力度输入在正确的位置被检测到。读数与力度成正比,并且相当接近 ADC 的满量程范围。模拟性能看起来令人满意,所以让我们关注速度。
我们之前观察到,一个完整的扫描周期大约需要 57 毫秒才能完成(17Hz 更新率),这对于大多数项目来说太慢了,尤其是在测量用户触摸输入时。
您可能已经发现了初始 Arduino 代码示例中的一些(明显的)效率低下问题。再次参考右侧的示波器视图,让我们看看一些可能大幅提升速度的关键领域。

1. 串行通信
查看TX引脚,我们可以看到该系统的主要瓶颈。即使硬件扫描速度更快,我们仍然需要等待串行传输完成才能进行下一次扫描。
在最初的Arduino代码示例中,我们每次读取都发送3个字节,加上换行符,以保持终端窗口中文本间距的一致性/可读性。
如果我们将每次读取的数据减少到单字节并删除换行符,就可以立即使串行通信速度提高3倍以上。当然,这会弄乱终端窗口,因此我们将在第8部分中创建自己的接收应用程序。
我们还注意到长串的零,因此我们将应用简单的压缩,将连续的零值作为单个零 + 计数发送。例如,假设有一串连续 100 个零,我们将发送“0”,然后发送“100”——2 个字节而不是 100 个字节。
2. ADC 转换速率:
使用默认 ADC 配置,每次 ADC 转换大约需要 100 微秒。我们每个扫描周期读取 160 个读数,因此总共需要 16 毫秒。为了缩短转换时间,我们将 ADC 预分频器降低到能够提供良好读数的最小值。
3. IO 速度 / digitalWrite()
使用外部 IO(移位寄存器和多路复用器)意味着需要翻转大量位来扫描 MatrixArray——每个扫描周期我们调用近 400 次 digitalWrite()。
我们将用直接端口操作替换 digitalWrite。 直接端口操作已在其他网站上进行了广泛讨论,因此我们在此省略完整解释,但我们应该能够将每个扫描周期缩短一到两毫秒。
/********************************************************************************************************** * Project: FastMatrixArray.ino * By: Chris Wittmier @ Sensitronics LLC * LastRev: 04/22/2014 * Description: Demonstration of Sensitronics' 16x10 element resistive ThruMode MatrixArray. Scanning * electronics consist of 2 HCT595 shift registers, and 2 4051 multiplexers connected to an Arduino Uno. * This sketch is Uno-specific and optimized for faster rate. **********************************************************************************************************/ /********************************************************************************************************** * MACROS / PIN DEFS **********************************************************************************************************/ #define BAUD_RATE 115200 #define ROW_COUNT 10 #define COLUMN_COUNT 16 #define PIN_ADC_INPUT A0 #define PIN_SHIFT_REGISTER_DATA 2 #define PIN_SHIFT_REGISTER_CLOCK 3 #define PIN_MUX_CHANNEL_0 4 //channel pins 0, 1, 2, etc must be wired to consecutive Arduino pins #define PIN_MUX_CHANNEL_1 5 #define PIN_MUX_CHANNEL_2 6 #define PIN_MUX_INHIBIT_0 7 //inhibit = active low enable. All mux IC enables must be wired to consecutive Arduino pins #define PIN_MUX_INHIBIT_1 8 #define SET_SR_DATA_HIGH() PORTD|=B00000100 #define SET_SR_DATA_LOW() PORTD&=~B00000100 #define SET_SR_CLK_HIGH() PORTD|=B00001000 #define SET_SR_CLK_LOW() PORTD&=~B00001000 #define ROWS_PER_MUX 8 #define MUX_COUNT 2 #define CHANNEL_PINS_PER_MUX 3 #define PACKET_END_BYTE 0xFF #define MAX_SEND_VALUE 254 //reserve 255 (0xFF) to mark end of packet #define COMPRESSED_ZERO_LIMIT 254 #define MIN_SEND_VALUE 1 //values below this threshold will be treated and sent as zeros #ifndef cbi #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) #endif #ifndef sbi #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) #endif /********************************************************************************************************** * GLOBALS **********************************************************************************************************/ int current_enabled_mux = MUX_COUNT - 1; //init to number of last mux so enabled mux increments to first mux on first scan. int compressed_zero_count = 0; /********************************************************************************************************** * setup() **********************************************************************************************************/ void setup() { Serial.begin(BAUD_RATE); pinMode(PIN_ADC_INPUT, INPUT); pinMode(PIN_SHIFT_REGISTER_DATA, OUTPUT); pinMode(PIN_SHIFT_REGISTER_CLOCK, OUTPUT); pinMode(PIN_MUX_CHANNEL_0, OUTPUT); pinMode(PIN_MUX_CHANNEL_1, OUTPUT); pinMode(PIN_MUX_CHANNEL_2, OUTPUT); pinMode(PIN_MUX_INHIBIT_0, OUTPUT); pinMode(PIN_MUX_INHIBIT_1, OUTPUT); sbi(ADCSRA,ADPS2); //set ADC prescaler to CLK/16 cbi(ADCSRA,ADPS1); cbi(ADCSRA,ADPS0); } /********************************************************************************************************** * loop() **********************************************************************************************************/ void loop() { compressed_zero_count = 0; for(int i = 0; i < ROW_COUNT; i ++) { setRow(i); shiftColumn(true); for(int j = 0; j < COLUMN_COUNT; j ++) { int raw_reading = analogRead(PIN_ADC_INPUT); byte send_reading = (byte) (lowByte(raw_reading >> 2)); shiftColumn(false); sendCompressed(send_reading); } } if(compressed_zero_count > 0) { Serial.write((byte) 0); Serial.write((byte) compressed_zero_count); } Serial.write((byte) PACKET_END_BYTE); } /********************************************************************************************************** * setRow() - Enable single mux IC and channel to read specified matrix row. digitalWrite() have not been * replaced in this function, as mux changes are relatively infrequent. **********************************************************************************************************/ void setRow(int row_number) { if((row_number % ROWS_PER_MUX) == 0) //We've reached channel 0 of a mux IC, so disable the previous mux IC, and enable the next mux IC { digitalWrite(PIN_MUX_INHIBIT_0 + current_enabled_mux, HIGH); //This offset is why mux inhibits must be wired to consecutive Arduino pins current_enabled_mux ++; if(current_enabled_mux >= MUX_COUNT) { current_enabled_mux = 0; } digitalWrite(PIN_MUX_INHIBIT_0 + current_enabled_mux, LOW); //enable the next mux, active low } for(int i = 0; i < CHANNEL_PINS_PER_MUX; i ++) { if(bitRead(row_number, i)) { digitalWrite(PIN_MUX_CHANNEL_0 + i, HIGH); } else { digitalWrite(PIN_MUX_CHANNEL_0 + i, LOW); } } } /********************************************************************************************************** * shiftColumn() - Shift out a high bit to drive first column, or increment column by shifting out a low * bit to roll high bit through cascaded shift register outputs. digitalWrite() has been replaced with direct * port manipulation macros, as this function performs the vast majority of pin writes **********************************************************************************************************/ void shiftColumn(boolean is_first) { if(is_first) { SET_SR_DATA_HIGH(); } SET_SR_CLK_HIGH(); SET_SR_CLK_LOW(); if(is_first) { SET_SR_DATA_LOW(); } } /********************************************************************************************************** * sendCompressed() - If value is nonzero, send it via serial terminal as a single byte. If value is zero, * increment zero count. The current zero count is sent and cleared before the next nonzero value **********************************************************************************************************/ void sendCompressed(byte value) { if(value < MIN_SEND_VALUE) { if(compressed_zero_count < (COMPRESSED_ZERO_LIMIT - 1)) { compressed_zero_count ++; } else { Serial.write((byte) 0); Serial.write((byte) COMPRESSED_ZERO_LIMIT); compressed_zero_count = 0; } } else { if(compressed_zero_count > 0) { Serial.write((byte) 0); Serial.write((byte) compressed_zero_count); compressed_zero_count = 0; } if(value > MAX_SEND_VALUE) { Serial.write((byte) MAX_SEND_VALUE); } else { Serial.write((byte) value); } } }
第七部分:优化结果
关键时刻到了……我们能提高那平淡无奇的 17Hz 扫描率吗?
让我们再看看示波器,使用与上次相同的时间/格设置:

成功!
使用光标绘制单个扫描周期,我们观察到一次典型的扫描现在只需 3.5 毫秒即可完成——更新率为 285Hz。还不错!
现在我们已经达到了更实用的速度,让我们在 PC 端添加一些图形反馈来结束这次扫描。
浙公网安备 33010602011771号