任务介绍
本项目实现了Funpack4-3 板卡二的任务2,使用TMC4361A+TMC2160控制方案,基于CrowPanel ESP32 4.3英寸HMI开发板与ESP32 XIAO开发板,构建了完整的步进电机控制系统。系统采用BLE GATT无线通信协议,实现上位机(CrowPanel HMI)与下位机(XIAO)的实时指令传输,实现对步进电机的速度与位置精确控制。
硬件平台
任务板卡
首先介绍本次用到的板卡:TMC4361A-BOB & TMC2160-BOB 评估套件。
顾名思义,TMC4361A-BOB & TMC2160-BOB 评估套件分别是用来评估TMC4361A和TMC2160芯片的,其中BOB是分线板(Breakout Board)的缩写。其中TMC4361A是步进电机运动控制器,它有两套SPI接口,分别与微控制器和下层驱动器通信,它支持我们通过它“透传”SPI信息给下层驱动器,这样可以简化连接,给微控制器节省一个SPI接口。TMC2160是栅极驱动器,它通过控制外接的四个晶体管来控制电流,最终实现对步进电机的控制。

主控板
为了控制上述的板卡,我们还需要两块开发板:CrowPanel ESP32 Display和ESP32 XIAO。
CrowPanel是一块基于ESP32 S3微控制器的HMI触摸屏开发板,因此它的主体就是一块4.3寸的LCD屏,并且集成了比较丰富的外设接口。
- 上位机设备:CrowPanel ESP32 Display 4.3英寸HMI开发板
- 搭载双核Xtensa® 32位LX6微处理器
- 集成WiFi/蓝牙无线通信模块
- 480x272分辨率RGB显示屏
- 电容式触摸屏交互界面
ESP32 XIAO同样是一块基于ESP32 S3微控制器的开发板,它的规格非常小巧,但麻雀虽小,五脏俱全。强劲的ESP32 S3核心以及外接的引脚足以支撑本次任务。
- 下位机设备:ESP32 XIAO开发板
- 用于驱动TMC4361A+TMC2160步进电机控制芯片
- 控制步进电机的运动参数
- 执行上位机发送的控制指令

任务分析与实现
这次我选择了任务二来实现。
方案框图:
一、上位机控制层(HMI)
- 人机交互界面
- 基于LVGL9图形库构建的触摸界面
- 速度控制滑动条(-100~100 RPM范围)
- 角度控制滑动条(-180~180度范围)
- RUN/STOP控制按钮
- BLE客户端模块
- 连接下位机BLE服务器
- 将用户操作转换为控制指令
- 通过GATT特性值写入传输指令
二、下位机执行层(ESP32 XIAO)
- BLE服务端模块
- 提供BLE服务与特性值
- 接收上位机指令
- 解析速度与角度控制参数
- 电机驱动核心
- TMC4361A运动控制器配置
- TMC2160步进电机驱动器配置
- 速度模式与位置模式切换
- 微步控制与加速度规划
- 电机控制算法
- RPM到步进脉冲的转换
- 角度到步进位置的计算
- 梯形加速度曲线实现
代码详解
下位机软件流程图:
上位机软件流程图:
本次项目涉及到三个关键部分:电机控制、数据传输与显示,接下来结合相关代码来进行讲解:
一、电机控制模块(下位机)
首先是初始化,电机控制的初始化分为TMC4361A和TMC2160两部分。需要注意的是,我们通过TMC4361A来透传SPI数据给TMC2160,需要严格按照手册中的通信步骤和包定义来发出SPI信息。
void setup()
{
...
// Init TMC4361 clock
// --- 初始化 8MHz PWM on D8 ---
ledcSetup(PWM_CHANNEL, TMC4361_CLK_FREQ, PWM_RESOLUTION); // 设置通道
ledcAttachPin(TMC4361_CLK_PIN, PWM_CHANNEL); // 将通道与对应的引脚连接
ledcWrite(PWM_CHANNEL, 2);
tmc.begin(TMC4361_CLK_FREQ, TMC4361_CS_PIN);
tmc.writeRegister(TMC4361_SPIOUT_CONF_REGISTER, 0x4440128D);
delay(5);
tmc.writeRegister(TMC4361_COVER_HIGH_REGISTER, 0xec);
tmc.writeRegister(TMC4361_COVER_LOW_REGISTER, 0x004140C5);
delay(5);
tmc.writeRegister(TMC4361_COVER_HIGH_REGISTER, 0x90);
tmc.writeRegister(TMC4361_COVER_LOW_REGISTER, 0x00070803);
delay(5);
tmc.writeRegister(TMC4361_COVER_HIGH_REGISTER, 0x91);
tmc.writeRegister(TMC4361_COVER_LOW_REGISTER, 0x0000000A);
delay(5);
tmc.writeRegister(TMC4361_COVER_HIGH_REGISTER, 0x80);
tmc.writeRegister(TMC4361_COVER_LOW_REGISTER, 0x00000004);
delay(5);
tmc.writeRegister(TMC4361_COVER_HIGH_REGISTER, 0x93);
tmc.writeRegister(TMC4361_COVER_LOW_REGISTER, 0x00000000);
delay(5);
tmc.writeRegister(0X24, 0x00000000);
tmc.writeRegister(0X21, 0x00000000);
tmc.writeRegister(0X37, 0x00000000);
delay(5);
...
}
下位机监听到数据变更后,在回调函数中实施控制:
// BLE characteristic write callbacks
class BLEWriteCallbacks : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *characteristic) {
std::string value = characteristic->getValue();
if (value.length() > 0) {
// Convert string to integer
int32_t number = atoi(value.c_str());
if (characteristic == pCharSpeed) {
Serial.print("BLE: Setting speed to ");
Serial.print(number);
Serial.println(" RPM");
setSpeedInRPM(number);
} else if (characteristic == pCharAngle) {
Serial.print("BLE: Rotating by ");
Serial.print(number);
Serial.println(" degrees");
rotateByDegrees(number);
}
}
}
};
二、数据传输(上位机&下位机)
1. BLE GATT服务定义
我们使用了2个特征来分别代表速度、角度信息,在上、下位机都可以看到相同的UUID:
// BLE Service and Characteristics UUIDs
#define BLE_SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define BLE_CHAR_UUID_SPEED "beb5483e-36e1-4688-b7f5-ea07361b26a8" // Speed control
#define BLE_CHAR_UUID_ANGLE "c1f0a8a5-5d0d-4f1c-9a1b-7a7c7a7d7e7f" // Angle control
2. 下位机 GATT Server
我们在下位机初始化一个名为 "ESP32_S3_Server" 的GATT Server,使用公约的UUID创建一个服务以及两个特征,特征均定义为可读写。
// Initialize BLE server
BLEDevice::init("ESP32_S3_Server");
pBLEServer = BLEDevice::createServer();
pBLEServer->setCallbacks(new BLEConnectionCallbacks());
// Create BLE service
BLEService *pService = pBLEServer->createService(BLE_SERVICE_UUID);
// Create speed control characteristic
pCharSpeed = pService->createCharacteristic(
BLE_CHAR_UUID_SPEED,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
pCharSpeed->addDescriptor(new BLE2902());
pCharSpeed->setCallbacks(new BLEWriteCallbacks());
// Create angle control characteristic
pCharAngle = pService->createCharacteristic(
BLE_CHAR_UUID_ANGLE,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
pCharAngle->addDescriptor(new BLE2902());
pCharAngle->setCallbacks(new BLEWriteCallbacks());
// Start service and advertising
pService->start();
pBLEServer->getAdvertising()->start();
Serial.println("BLE Server started. Waiting for clients...");
3. 上位机 GATT Client
相应地,上位机要连接到Server端对应的Service和Characteristic。
static bool connectToServer() {
Serial.print("Connecting to: ");
Serial.println(myDevice->getAddress().toString().c_str());
pClient = BLEDevice::createClient();
pClient->setClientCallbacks(new MyClientCallback());
// Connect to the server
if (!pClient->connect(myDevice)) {
Serial.println("Connection failed");
return false;
}
Serial.println("Connected to server");
// Get remote service
pRemoteService = pClient->getService(SERVICE_UUID);
if (pRemoteService == nullptr) {
Serial.print("Service not found with UUID: ");
Serial.println(SERVICE_UUID);
pClient->disconnect();
return false;
}
// Get speed characteristic
pRemoteCharacteristicSpeed = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID_SPEED);
if (pRemoteCharacteristicSpeed == nullptr) {
Serial.print("Characteristic not found with UUID: ");
Serial.println(CHARACTERISTIC_UUID_SPEED);
pClient->disconnect();
return false;
}
// Get angle characteristic
pRemoteCharacteristicAngle = pRemoteService->getCharacteristic(CHARACTERISTIC_UUID_ANGLE);
if (pRemoteCharacteristicAngle == nullptr) {
Serial.print("Characteristic not found with UUID: ");
Serial.println(CHARACTERISTIC_UUID_ANGLE);
pClient->disconnect();
return false;
}
Serial.println("Connected and all characteristics found");
connected = true;
return true;
}
三、显示模块设计(上位机)
这一次我们使用LVGL 9作为UI框架,相比以前CrowPanel提供的 LVGL 8有较大的代码变更,需要进行一系列修改,但主要思路不变。经过改造之后
1. LCD与图形框架初始化
void setup() {
...
// lvgl初始化
lv_init();
// Initialize LCD and touch screen
init_lcd_touch();
// GUI界面初始化和事件初始化
setup_ui(&guider_ui);
events_init(&guider_ui);
...
}
2. UI设计
使用NXP的GUI Guider来进行UI设计,这里我使用了两个滑条来控制速度与角度,两个按钮分别控制电机运转与停止。

3. 实时渲染优化
xTaskCreatePinnedToCore(lvgl_task, "LVGL Task", 40960, NULL, 1, NULL, 0); // 核心0,增加栈大小到40960
void lvgl_task(void *pvParameters) {
printf("Task Core id:%d\n", xPortGetCoreID());
printf("Task CPU Freq:%d\n", getCpuFrequencyMhz());
while (1) {
lv_task_handler(); /* 让 GUI 完成其工作 */
lv_tick_inc(33); /* 告诉 LVGL 经过了多少时间 */
vTaskDelay(pdMS_TO_TICKS(33));
}
}
- 多核架构:显示任务运行在核心0,数据处理在核心1
- 刷新周期:33ms(约30FPS,符合人眼视觉暂留特性)
效果展示
整体效果

下位机串口输出信息
遇到的问题与解决办法
上位机重启后,无法重新连接下位机.
原因分析:下位机连接断开后,没有再次进入广告模式。
解决办法:下位机监听连接断开事件,手动进入广告模式。
// BLE connection callbacks
class BLEConnectionCallbacks : public BLEServerCallbacks {
void onConnect(BLEServer* server) {
Serial.println("BLE Client connected");
}
void onDisconnect(BLEServer* server) {
Serial.println("BLE Client disconnected, restarting advertising...");
server->getAdvertising()->start();
}
};
活动感想
本项目是我第一次接触目前产业正在广泛使用的步进电机精确控制方案,让我深入实践了电机控制技术,进一步丰富了技能树。同时,这也是我第一次基于LVGL 9进行电机控制界面开发,对LVGL的开发也得到了进一步的锻炼。特别感谢硬禾科技提供的高质量开发平台,这次任务使用到的一系列板卡都是参与硬禾往期活动时收获的,能借Funpack4-3再次让它们发光发热也是一件非常开心的事情。
感谢硬禾学堂举办的寒假练活动,祝硬禾的活动越办越好!