一、所选任务介绍
任务名称: 无线 Touch 测试平台(自命题)
本项目希望像手机、平板那样,从一块 Touch Pad 原始的手指感应信号中,用 MCU 自己的算力实时解算出触摸坐标,再通过无线方式(BLE)把坐标传到主机显示。换句话说,就是做一个"脱离手机主机、由 MCU 端独立完成触控坐标解算"的无线触摸测试工具。
任务要求
- 通过 SPI 读取 Touch Pad 的原始手指信号数据(Delta 信号)
- 在 MCU 端实时完成坐标解算,支持多指报点
- 通过 BLE 或 USB 串口把坐标数据传到 PC 端 GUI
- PC 端 GUI 实时显示触摸位置,并可查看原始信号热力图,用于调试与浮空对比
B站演示视频: https://b23.tv/hCIa8L6
二、项目描述
本项目基于 MAX32655FTHR 开发板和一块 Synaptics 触摸屏(Touch Pad),搭建了一套无线 Touch 测试平台。系统以 MAX32655 为主控,通过 SPI 接口按 TouchComm 协议读取触摸屏的原始感应数据,在 MCU 内部完成手指信号到坐标的换算,再通过 BLE 4.2 或 USB 串口发送给 PC 端的 Python GUI 进行可视化。
系统的主要功能包括:
- Touch Pad 原始信号采集 通过 SPI1 以 TouchComm 协议读取触摸屏的手指 Delta 信号,每帧 1369 字节,对应 17 条 TX × 37 条 RX 的感应矩阵。
- 实时坐标解算 在 MCU 固件中根据原始信号矩阵计算手指触摸坐标,定点数运算,单指约 60Hz、双指约 40Hz。
- 多指报点 支持多指同时检测与报点,GUI 端用不同颜色区分每个触点。
- 无线传输(BLE) 利用 MAX32655 板载的 BLE 4.2,把坐标数据无线发送到 PC,实现"浮空测试"——类似手机上不接触屏幕也能感应手指位置的场景。
- PC 端可视化 GUI 使用 Python(tkinter + pyserial + bleak)编写,提供坐标显示、Delta 信号热力图、日志等多个界面,用于实时观察触摸报点和原始信号。
因此,本项目的核心设计目标是:
利用 MAX32655 的算力和板载 BLE,在 MCU 端独立完成 Touch Pad 原始信号到触摸坐标的解算,并无线传输到 PC 可视化,构建一套可用于触控调试与浮空测试的无线 Touch 测试平台。
图1:项目实物连接(左:MAX32655FTHR 开发板;右:Synaptics Touch Pad 触摸屏,杜邦线连接)

三、硬件介绍
3.1 MAX32655FTHR 主控模块
本项目采用 MAX32655FTHR 作为主控。该开发板搭载 Cortex-M4 @ 100MHz 主频,外加一颗 RISC-V 核,配备 128KB SRAM / 512KB Flash,板载 BLE 4.2 无线模块,并板载 DAPLink 调试器(在 PC 上枚举为 COM29 串口)。
在本项目中,MAX32655 承担以下功能:
- 通过 SPI1 接口按 TouchComm 协议读取 Touch Pad 的原始信号帧;
- 通过外部中断(INT 脚)与 Touch Pad 同步抓帧;
- 在固件中完成手指信号到触摸坐标的解算;
- 通过 BLE 4.2 或 USB 串口把坐标数据发送给 PC;
- 把手指信号矩阵做 90° 转置,使其方向符合手机正常使用习惯。
3.2 Synaptics Touch Pad 触摸屏
项目使用一块 Synaptics 触摸屏(Touch Pad),面板分辨率为 1200×2560px,感应矩阵为 17 条 TX × 37 条 RX。触摸屏采用 TouchComm 协议通信,接在 MAX32655 的 SPI1 上,配置为 8-bit、SPI Mode 0、4MHz。
触摸屏需要两路供电:3.3V 和 1.8V,另有一根中断脚(INT)用于通知主控有新数据。
四、方案框图与设计思路
图2:系统方案框图

系统以 MAX32655 为中转核心:Touch Pad 通过 SPI 把原始手指信号量传给 MCU,MCU 在内部实时完成坐标点计算,再把计算后的坐标点通过 USB 串口或 BLE 发送给 Host 端的 GUI。
整体工作逻辑如下:
- Touch Pad 检测到手指触碰后,通过 INT 脚通知 MCU;
- MCU 通过 SPI1 以 TouchComm 协议读取一帧 1369 字节的原始 Delta 信号;
- MCU 在固件中把信号矩阵解算为触摸坐标,并做 90° 转置;
- MCU 通过 BLE 或串口把坐标和信号数据发给 PC;
- PC 端 GUI 实时绘制触摸点位置和原始信号热力图。
五、原理图与引脚连接
图3:硬件连接原理图

本项目硬件非常简单,因为只涉及电源和 SPI 两类连接,直接用杜邦线把 MAX32655FTHR 和 Touch Pad 连起来即可,没有额外画板。
5.1 MAX32655 与 Touch Pad 引脚连接表
MAX32655 引脚 | GPIO / 功能 | 连接对象 | 功能说明 |
|---|---|---|---|
CS0 | P0.20 | Touch Pad | SPI 片选 |
MISO | P0.22 | Touch Pad | SPI 主入从出 |
MOSI | P0.21 | Touch Pad | SPI 主出从入 |
SCK | P0.23 | Touch Pad | SPI 时钟 |
INT | P2.0 | Touch Pad | 触摸中断信号 |
VDD | 3.3V | Touch Pad | 3.3V 供电 |
VDDIO | 1.8V | Touch Pad | 1.8V 供电 |
GND | GND | Touch Pad | 共地 |
六、软件流程图与关键代码
本项目软件分为两部分:MCU 固件(运行在 MAX32655 的 M4 核上)和 Host GUI(运行在 PC 上的 Python 脚本)。固件利用 ADI 提供的 CodeFusion Studio 生成初步工程,配好 SPI1 和中断脚后再手写业务逻辑;Host GUI 使用 Python,依赖 tkinter、pyserial(串口)和 bleak(BLE)三个库。
6.0 MCU 开发软件
主题框架代码有ADI提供的VScode的CodeFusion Stuio自动生成的。可以新建工程/打开例子工程,可以编译+下载+调试一套龙服务,保持用户开发的习惯。

6.1 MCU 固件整体流程
图4:MCU 固件流程图(下面是配套的 GUI 界面)


固件上电后的运行顺序为:
- Power ON,完成各外设初始化(除 TouchComm_Init 外,其余基本由 CodeFusion Studio 自动生成);
- 初始化 Touch Pad;
- 启动 BLE 广播并等待建立连接;
- 进入大循环,以中断脚为同步信号抓取 Touch Pad 数据,解算为坐标后传给 GUI。
6.2 SPI 接口配置
图5:SPI 接口配置代码

按照 TouchComm 协议要求,把 SPI1 配置为 8-bit、Mode 0,时钟初始设为 4MHz。需要注意的是,实测 SPI 实际频率与设定值存在偏差(详见"难点"章节)。

6.3 中断脚配置
图6:中断脚配置代码

配置 P2.0 为外部中断输入,用于接收 Touch Pad 的数据就绪信号,实现"有数据才读"的同步抓帧模式。
6.4 代码结构
图7:固件代码结构

固件主要文件如下,其中除 touchcomm 相关业务文件外,其余基本为 CodeFusion Studio 自动生成:
- main.c / soc_init.c:固件入口与系统初始化;
- touchcomm.c / touchcomm.h:Touch Pad 通信驱动(对应 SDK 工程 BLE4_CTL);
- calc.c / calc.h:手指信号量计算坐标的模块;
- host_protocol.c / host_protocol.h:把坐标和手指信号量打包传给 Host;
- src/ble_touch.c / ble_touch.h:BLE 4.2 收发处理。
6.5 主模块初始化与大循环
图8:主模块初始化代码
各个模块初始化,都是系统生成除了,TouchComm_Init外。其中SPI的频率,目前应该还有一些问题,比如上图的8M设定和下面MXC_SPI_GetFrequency获得的频率值有一些出入。
设定8M打印的是16.6M,设定大于12M,显示25Mhz,大于18Mzh显示50Mhz。

result = MXC_SPI_Init(MXC_SPI1,
1, /* 0 for target, 1 for controller */
0, /* 1 for quad mode, 0 otherwise */
1, /* number of targets */
0U, /* polarity mask */
8000000U, /* frequency */
spi1_pins_dummy);

int main(void)
{
int retVal;
uint8_t identify[64];
uint16_t idLen = 0;
tc_app_info_t appInfo;
/* CFS-generated SoC init: UART0 (console), SPI1, GPIO pins (incl. P0.30 CS, P2.0 INT). */
retVal = PinInit();
if (retVal != E_NO_ERROR) {
return retVal;
}
retVal = PeripheralInit();
if (retVal != E_NO_ERROR) {
return retVal;
}
/* Initialize the TouchComm protocol driver (SPI1, DMA IRQs, CS). */
retVal = TouchComm_Init();
if (retVal != E_NO_ERROR) {
return retVal;
}
/* Initialize BLE: Cordio host + controller on this M4, advertises as
* "TOUCH-M4". Set the RX callback AFTER BleTouch_Init(): BleTouch_Init()
* clears bleCb.rxCb to NULL, so registering before it would be wiped and
* BLE writes would never reach the host-protocol parser. */
BleTouch_Init();
BleTouch_SetRxCallback(ble_rx_into_host);
图9:主模块大循环代码
目前所有的都合在一起,有些杂乱,可能需要用RTOS会比较清爽

while (1) {
/* Answer any host commands (non-blocking). */
Host_Process();
/* Route host-protocol responses to BLE when a host is connected. */
ble_update_host_output();
/* Service the BLE stack (HCI over the mailbox to the RISC-V
* controller, GATT events, timers). Non-blocking. */
BleTouch_Process();
if ((uint32_t)(PalRtcCounterGet() - last_beat) >= 32768u) {
last_beat = PalRtcCounterGet();
/* UART-independent liveness: blink the red LED each heartbeat
* so we can tell the main loop is running even if the serial
* console is stuck. */
LED_Toggle(LED_RED);
/* Heartbeat text is DBGPRINT-gated: at TOUCH_DEBUG_PRINT=0 the
* console UART must carry ONLY host-protocol packets. Any
* periodic newlib printf on the same UART as the raw-write
* protocol path corrupts printf's internal state and floods the
* line with 0x2A ('*'), drowning the AA55 packets (see notes). */
DBGPRINT("[M] hb frame=%lu ble=%s\r\n",
(unsigned long)frame_count, BleTouch_StateStr());
}
/* Wait for the panel to assert INT (data ready). */
int_status=TouchComm_IntState();
if (!int_status) {
continue;
}
frame_count++;
retVal = TouchComm_ReadFrame(rx_data, TC_FRAME_LEN);
if (retVal != E_NO_ERROR) {
//printf("\r\nFrame %lu : SPI read error %d\r\n", (unsigned long)frame_count, retVal);
/* Wait for the panel to release INT before retrying. */
while (TouchComm_IntState()) {
Host_Process();
BleTouch_Process();
}
continue;
}
else{
/* Transpose the frame's [col][row] image into signal_grid[row][col]. */
for(int i=0;i<GRID_ROWS;i++){
for( int j=0;j<GRID_COLS;j++){
signal_grid[i][j]=rx_buf[trx_idx[j]+i];
}
}
for(int i=0;i<GRID_ROWS;i++){
rx_self[i]=rx_buf[trx_idx[GRID_COLS]+i];
}
for(int i=0;i<GRID_COLS;i++){
tx_self[i]=rx_buf[trx_idx[GRID_COLS+1]+i];
}
int n_now = Touch_Report();
}
}
主模块大循环里完成了数据抓取、坐标解算和上报。其中做了一个 手指信号量转置 90° 的处理,目的是让坐标方向符合手机正常使用习惯。解算出的报点坐标会通过串口或 BLE 传给 Host。目前所有逻辑合在一个大循环里,略显杂乱,后续如果维护可以考虑引入 RTOS 让结构更清晰。
6.6 Calc 坐标解算模块
图10:Calc 模块代码

/**
* touch_convert_get_combined - 获取完整输出缓冲区
*
* 数据排布: 互容(GRID_ROWS*GRID_COLS) + RX自容(GRID_ROWS) + TX自容(GRID_COLS)
* 总共: GRID_ROWS*GRID_COLS + GRID_ROWS + GRID_COLS 个 uint16_t
*
* @param buf 输出缓冲区 (至少 N 个 uint16_t)
* @param maxlen 缓冲区最大长度
* @return 实际写入的元素数量
*/
int touch_convert_get_combined(uint16_t *buf, int maxlen)
{
int total = GRID_ROWS * GRID_COLS + GRID_ROWS + GRID_COLS;
int n = (maxlen < total) ? maxlen : total;
int idx = 0;
/* 互容: rows x cols */
for (int r = 0; r < GRID_ROWS && idx < n; r++)
for (int c = 0; c < GRID_COLS && idx < n; c++)
buf[idx++] = (uint16_t)signal_grid[r][c];
/* RX 自容 */
for (int r = 0; r < GRID_ROWS && idx < n; r++)
buf[idx++] = (uint16_t)rx_self[r];
/* TX 自容 */
for (int c = 0; c < GRID_COLS && idx < n; c++)
buf[idx++] = (uint16_t)tx_self[c];
return idx;
}
/* ========================================================================== */
/* 报点 (Finger Reporting) */
/* 输入: signal_grid (互容), tx_self / rx_self (自容) */
/* 输出: touch_points[] + 返回有效手指数 (0..MAX_FINGERS) */
/* ========================================================================== */
touch_point_t touch_points[MAX_FINGERS];
/* 1D 自容质心: 在 peak 附近自容信号 > THRESHOLD/2 的连续窗口内加权, 返回像素 */
static int _self_centroid_px(const int16_t *prof, int n, int peak,
const int16_t *centers)
{
int lo = peak, hi = peak;
while (lo > 0 && prof[lo - 1] > (THRESHOLD >> 1)) lo--;
while (hi < n - 1 && prof[hi + 1] > (THRESHOLD >> 1)) hi++;
int32_t tw = 0, wc = 0;
for (int i = lo; i <= hi; i++) {
int v = prof[i];
if (v <= (THRESHOLD >> 1)) continue;
int w = v - (THRESHOLD >> 1);
tw += w;
wc += (int32_t)w * (int)centers[i];
}
if (tw <= 0) return (int)centers[peak];
return (int)((wc + (tw >> 1)) / tw);
}
/* 手指尺寸: 沿 X (width) / Y (length) 统计 >THRESHOLD 的连续格数 x 块像素 */
static void _finger_extent_px(int row, int col, int *width, int *length)
{
int cl = col, cr = col;
while (cl > 0 && signal_grid[row][cl - 1] > THRESHOLD) cl--;
while (cr < GRID_COLS - 1 && signal_grid[row][cr + 1] > THRESHOLD) cr++;
*width = (cr - cl + 1) * BLOCK_W_PX;
int rt = row, rb = row;
while (rt > 0 && signal_grid[rt - 1][col] > THRESHOLD) rt--;
while (rb < GRID_ROWS - 1 && signal_grid[rb + 1][col] > THRESHOLD) rb++;
*length = (rb - rt + 1) * BLOCK_H_PX;
}
/* 后处理: 强制任意两指间距 >= MIN_FINGER_SEP_PX, 过近保留信号强者 */
static int _enforce_min_sep(void)
{
int n = 0;
for (int i = 0; i < MAX_FINGERS; i++) {
if (!touch_points[i].valid) continue;
int keep = 1;
for (int j = 0; j < MAX_FINGERS; j++) {
if (i == j || !touch_points[j].valid) continue;
int dx = touch_points[i].x - touch_points[j].x;
int dy = touch_points[i].y - touch_points[j].y;
if (dx * dx + dy * dy < MIN_FINGER_SEP_PX * MIN_FINGER_SEP_PX) {
/* 与更强 (或同强但序号更小) 的手指太近 -> 丢弃 */
if (touch_points[i].peak < touch_points[j].peak ||
(touch_points[i].peak == touch_points[j].peak && j < i)) {
keep = 0;
break;
}
}
}
if (keep) touch_points[n++] = touch_points[i];
}
for (int i = n; i < MAX_FINGERS; i++) touch_points[i].valid = 0;
return n;
}
int Touch_Report(void)
Calc 模块负责把 Touch Pad 传来的原始信号矩阵转换为触摸坐标。代码里有明确注释,小数运算统一使用 Q12.4 定点数表示。这种做法精度会稍差一些,但能省去浮点运算开销,换取速度上的优化。实际效果:一个手指报点约 60Hz,两个手指约 40Hz。
6.7 Host GUI 软件
PC 端 GUI 用 Python 编写,依赖三个库:
pip install pyserial
pip install bleak
GUI 包含两个主要界面:
界面一:坐标显示(报点界面)
图11:GUI 坐标显示界面

左侧黑色区域按面板 1200×2560 的比例绘制触摸点,多个手指用不同颜色(红/蓝/绿)区分;右侧坐标列表实时显示每个触点的 x/y 坐标以及 width、length、peak 等参数。其中 Peak这个数据在 BLE 无线连接时尤其重要——因为无线浮空测试时,需要把报点数据和正常使用时做对比,这个就是这个项目核心意义所在。
界面二:Delta 手指信号量(热力图)
图12:GUI Delta 信号热力图

这个界面把 17×37 的原始信号矩阵绘制成热力图,手指按压处显示为红黄暖色峰值。它和日志界面主要用于辅助调试。由于数据量较大,BLE 连接下不太稳定,不推荐长时间使用;如果需要长时间观察,改用 USB 串口连接会稳定很多。
后续还要优化的话,可以用手机的BLE来连接它,手机应该比host更加稳定。同时也可以改为数字的方式更直观
七、实物演示
图13:实物运行效果一
运行如下的脚本,就可以打开Gui界面


在项目根目录下安装依赖后,直接运行 Python 脚本即可启动 GUI:
pip install pyserial
pip install bleak
python touch_gui.py
坐标显示和 Delta 数据的效果见上文坐标显示界面和热力图界面,详细运行效果可参考 B 站视频。
图14:实物运行效果二


图15:BLE 连接运行效果(与串口方式效果一致)
图16:电池供电连接方式

蓝牙部分的效果和串口方式一致。实际使用中,用 USB 串口连接最稳定;BLE 无线连接方便做浮空测试,但受 Windows 蓝牙栈影响可能会定时断开(解决方法见下一章)。
八、遇到的难点及解决方法
8.1 用 RISC-V 核跑 BLE 协议栈卡死
问题: 最初尝试用 RISC-V 核跑蓝牙协议栈,但 M4 核始终收不到 R 核初始化蓝牙成功的消息,系统直接卡死。由于目前没有 BLE 双核的官方示例,这个问题暂时无解。
解决方法: 绕过去——直接用 M4 核跑 BLE,不再让 RISC-V 核参与蓝牙。虽然没有用上双核,但功能是跑通了。
8.2 SPI 实际速度"成谜"
问题: SPI 时钟设定和实测对不上。代码里设的是 8MHz,但用 MXC_SPI_GetFrequency 读回来打印的是 16.6MHz;设定值大于 12MHz 时显示 25MHz,大于 18MHz 时显示 50MHz。设到 10MHz 以上后,手边的示波器一接上去 SPI 通信就异常,波形也很难看,所以至今不能完全确定高速档下 SPI 的实际速率。
解决方法: 保守起见把 SPI 频率控制在较低档位(4MHz),保证通信稳定优先。后续需要更仔细地对照数据手册和示波器波形确认时钟树分频。
8.3 Windows 下 BLE 定时断连
问题: Windows 下的 BLE 连接会在运行一段时间后自动断开,经确认这是 Windows 自身蓝牙栈的问题,和固件无关。
解决方法: 让固件在检测到 BLE 异常时自动重新初始化 BLE,这样 Windows 端应用就能重新枚举到 BLE 设备并再次连接。后续会继续关注其他参赛者和官方的情况,进一步确认和优化。
九、复刻注意事项
- 依赖安装: PC 端需 Python 环境,并安装
pyserial和bleak两个库; - 供电: Touch Pad 需要 3.3V 和 1.8V 两路供电,注意不要接错电平;
- 连接方式: 调试阶段优先用 USB 串口(DAPLink 枚举的 COM 口)连接,稳定性远好于 BLE;BLE 适合演示无线浮空效果;
- SPI 频率: 不要盲目拉高 SPI 时钟,建议先用 4MHz 跑通,再根据示波器波形逐步调整;
- 固件入口: 工程基于 CodeFusion Studio 生成,业务代码主要在 m4 目录下,
libtouchcomm以静态库形式提供; - 代码工程: 根目录下
touch_gui.py可直接运行。
十、心得体会
- AI 目前已经能在嵌入式开发中大显身手,如果不继续跟上,很快就会被淘汰;
- 虽然 AI 很强,但 ADI 的 CodeFusion Studio 插件做得确实不错,能让初学者容易上手,比 STM32 的 Cube 工具链更简单易懂一些;
- 这次活动感觉参与人数偏少,虽然有微信支持群,但还是有些单薄。建议后续活动同步在网站上开辟一个讨论区,把常见问题和解决办法集中放上去,大家更容易看到,也不容易错漏。
附件
源代码请见工程目录:根目录下 touch_gui.py 可直接运行;m4/ 目录下是固件,其中 libtouchcomm 由于NDA原因以静态库形式提供。