2026 ADI CodeFusion 竞赛 - 基于MAX32655FTHR 开发板实现无线Touch测试平台
该项目使用了Codefusion,实现了MAX32655FTHR 开发板应用的设计,它的主要功能为:利用蓝牙/Uart的Touch 数据采集测试系统。
标签
嵌入式系统
边缘智能
CodeFusion
tonyz
更新2026-10-08
67


一、所选任务介绍

任务名称: 无线 Touch 测试平台(自命题)

本项目希望像手机、平板那样,从一块 Touch Pad 原始的手指感应信号中,用 MCU 自己的算力实时解算出触摸坐标,再通过无线方式(BLE)把坐标传到主机显示。换句话说,就是做一个"脱离手机主机、由 MCU 端独立完成触控坐标解算"的无线触摸测试工具。

任务要求

  1. 通过 SPI 读取 Touch Pad 的原始手指信号数据(Delta 信号)
  2. 在 MCU 端实时完成坐标解算,支持多指报点
  3. 通过 BLE 或 USB 串口把坐标数据传到 PC 端 GUI
  4. PC 端 GUI 实时显示触摸位置,并可查看原始信号热力图,用于调试与浮空对比

B站演示视频: https://b23.tv/hCIa8L6

二、项目描述

本项目基于 MAX32655FTHR 开发板和一块 Synaptics 触摸屏(Touch Pad),搭建了一套无线 Touch 测试平台。系统以 MAX32655 为主控,通过 SPI 接口按 TouchComm 协议读取触摸屏的原始感应数据,在 MCU 内部完成手指信号到坐标的换算,再通过 BLE 4.2 或 USB 串口发送给 PC 端的 Python GUI 进行可视化。

系统的主要功能包括:

  1. Touch Pad 原始信号采集 通过 SPI1 以 TouchComm 协议读取触摸屏的手指 Delta 信号,每帧 1369 字节,对应 17 条 TX × 37 条 RX 的感应矩阵。
  2. 实时坐标解算 在 MCU 固件中根据原始信号矩阵计算手指触摸坐标,定点数运算,单指约 60Hz、双指约 40Hz。
  3. 多指报点 支持多指同时检测与报点,GUI 端用不同颜色区分每个触点。
  4. 无线传输(BLE) 利用 MAX32655 板载的 BLE 4.2,把坐标数据无线发送到 PC,实现"浮空测试"——类似手机上不接触屏幕也能感应手指位置的场景。
  5. 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 承担以下功能:

  1. 通过 SPI1 接口按 TouchComm 协议读取 Touch Pad 的原始信号帧;
  2. 通过外部中断(INT 脚)与 Touch Pad 同步抓帧;
  3. 在固件中完成手指信号到触摸坐标的解算;
  4. 通过 BLE 4.2 或 USB 串口把坐标数据发送给 PC;
  5. 把手指信号矩阵做 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:系统方案框图

image.png

系统以 MAX32655 为中转核心:Touch Pad 通过 SPI 把原始手指信号量传给 MCU,MCU 在内部实时完成坐标点计算,再把计算后的坐标点通过 USB 串口或 BLE 发送给 Host 端的 GUI。

整体工作逻辑如下:

  1. Touch Pad 检测到手指触碰后,通过 INT 脚通知 MCU;
  2. MCU 通过 SPI1 以 TouchComm 协议读取一帧 1369 字节的原始 Delta 信号;
  3. MCU 在固件中把信号矩阵解算为触摸坐标,并做 90° 转置;
  4. MCU 通过 BLE 或串口把坐标和信号数据发给 PC;
  5. PC 端 GUI 实时绘制触摸点位置和原始信号热力图。

五、原理图与引脚连接

图3:硬件连接原理图

image.png

本项目硬件非常简单,因为只涉及电源和 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自动生成的。可以新建工程/打开例子工程,可以编译+下载+调试一套龙服务,保持用户开发的习惯。

clipboard_2026-09-24_14-42.png

6.1 MCU 固件整体流程

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

image.png

固件上电后的运行顺序为:

  1. Power ON,完成各外设初始化(除 TouchComm_Init 外,其余基本由 CodeFusion Studio 自动生成);
  2. 初始化 Touch Pad;
  3. 启动 BLE 广播并等待建立连接;
  4. 进入大循环,以中断脚为同步信号抓取 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:固件代码结构

image.png

固件主要文件如下,其中除 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会比较清爽

image.png

        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 设备并再次连接。后续会继续关注其他参赛者和官方的情况,进一步确认和优化。

九、复刻注意事项

  1. 依赖安装: PC 端需 Python 环境,并安装 pyserial 和 bleak 两个库;
  2. 供电: Touch Pad 需要 3.3V 和 1.8V 两路供电,注意不要接错电平;
  3. 连接方式: 调试阶段优先用 USB 串口(DAPLink 枚举的 COM 口)连接,稳定性远好于 BLE;BLE 适合演示无线浮空效果;
  4. SPI 频率: 不要盲目拉高 SPI 时钟,建议先用 4MHz 跑通,再根据示波器波形逐步调整;
  5. 固件入口: 工程基于 CodeFusion Studio 生成,业务代码主要在 m4 目录下,libtouchcomm 以静态库形式提供;
  6. 代码工程: 根目录下 touch_gui.py 可直接运行。

十、心得体会

  1. AI 目前已经能在嵌入式开发中大显身手,如果不继续跟上,很快就会被淘汰;
  2. 虽然 AI 很强,但 ADI 的 CodeFusion Studio 插件做得确实不错,能让初学者容易上手,比 STM32 的 Cube 工具链更简单易懂一些;
  3. 这次活动感觉参与人数偏少,虽然有微信支持群,但还是有些单薄。建议后续活动同步在网站上开辟一个讨论区,把常见问题和解决办法集中放上去,大家更容易看到,也不容易错漏。

附件

源代码请见工程目录:根目录下 touch_gui.py 可直接运行;m4/ 目录下是固件,其中 libtouchcomm 由于NDA原因以静态库形式提供。

附件下载
spi_touch_release.7z
团队介绍
纯个人项目
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号