STM32运行机器学习模型
一个训练好的模型可以在 STM32 "蓝板" 上运行!
AI编程/Vibe Coding 遇到问题需要帮助的,联系微信 ezpoda,免费咨询。
在本文中,使用了一个 STM32F103C8T6 "蓝板" 微控制器板来收集输入数据,以便在机器学习过程中训练模型。数据代表了一对简单的 "敲击" 模式,作为敲击之间的时间间隔。训练是使用 Tensor Flow 进行的。
最初,模型在桌面上运行,因为 STM32 家族中相对较小的成员无法方便地运行 Tensor Flow。在随后的文章中,模型在另一个微控制器上运行 — 更强大的 ESP32-C6。这个设备的限制比 F103 少得多,它拥有 20KB 的 RAM。
在本文中,我们将使用完全不同类型的机器学习模型,以更少的资源实现相同的效果。有一种更古老和更简单的 ML 技术称为线性 SVM。一旦训练完成,只要问题本身有一些约束,它产生的模型几乎可以应用于任何地方。对于这里使用的两个模式中的 7 或 8 次敲击(6 或 7 个间隔),线性 SVM 非常适合。一旦训练完成,它只需要少数简单的代数运算就可以使用输出进行预测。
在本文的其余部分中,我们将微控制器称为 "STM32"、"蓝板" 或 "MCU",为了简洁。
1、敲击模式
这些是以前文章中显示的相同的两个敲击模式。一个是 "Shave and a Haircut — two Bits"。另一个是四个敲击的两个序列。后一种模式来自英国科幻/奇幻特许经营 "Doctor Who"。如果你看过 2000 年代中后期的剧集,你可能会记得角色 Wilfred Mott,由已故且深受怀念的演员 Bernard Cribbens 扮演。四个中的两个模式以他的名字命名。
2、SVM 背景
SVM 代表 "支持向量机",在某些圈子中已经失宠,因为它在维度增加(越来越多输入字段,如果你愿意的话)时扩展得不太好。几十年前,它更为常见,并开发超出下面介绍的线性模型。
用于分类,SVM 是机器学习的监督形式。监督仅仅意味着输入模型的数据已经有一个关于它代表什么的真实来源。如果每个点的值或 "维度" 被取为 x-sub-i 值,那么它们的结果可以取为单个 y 值,通常称为该点的 "标签"。因此在监督机器学习中,数据是标记的。
点集合 — 正如 ML 常见的情况 — 被分为训练集和测试集。训练集用于训练一组 "支持向量"。如下图所示,支持向量定义了 "超平面" 或一对边界的放置。边界位置由向量支持,这些向量在训练期间计算。然后针对之前分离的测试集测量性能。

该模型的维度是敲击振动之间的间隔。所以有七个 x 值。当训练完成时,将提供一组权重,每个维度一个。
3、在我们的数据上训练
为 Tensor Flow 文章进行的 训练 无法在这里重用。但是,数据 可以重用,并且已经重用。与其回到以前文章中使用的 Jupyter 笔记本,这个依赖于一些自定义代码,这些代码仍然进一步定制。我们非常感谢 Github 上的一个项目SVMLibC,这是一个用 C 编写的 SVM 库,并针对我们数据中的列数进行了修改。几乎不需要任何工作就能让它运行。数据格式不同,点计数也不同。但除此之外,它直接从仓库运行完美。
Model Weights:
w for Feature 1 = -0.5317
w for Feature 2 = -0.1439
w for Feature 3 = 0.5865
w for Feature 4 = 0.4011
w for Feature 5 = -0.6478
w for Feature 6 = -0.5286
w for Feature 7 = 0.7674
Bias (b) = 0.0000
Test Accuracy: 100.00%以上是针对输入数据运行 SVMLibC 代码的结果。它为每一列产生一个权重。两个敲击包含 "Shave and a Haircut" 的 6 个间隔和 "Wilf Knock" 的 7 个间隔。在前一种情况下,需要零填充。但由于表示了七个,所以七个有权重。"bias" 参数幸运地变成了 0。但它将作为移动值,或者可以被认为是轴截距。
如何应用这些非常简单。记住名称 "Linear SVM" 中的术语 "linear"?虽然这是一个简化,但模型可以线性代数的方式应用。上面的权重告诉我们每列中的值如何影响绘制一个特殊分割边界的放置,称为(如上所述)超平面,该超平面分离标记数据(在这种情况下标记为 Wilf 或 ShaveHaircut)。'x' 值是间隔。'y' 值是标签。如果你试图 使用 这个模型,'y' 值是你的预期信号。
敲击之间的间隔简单地乘以各自的权重并求和。然后将偏置值加到这些乘法的总和上。结果是估计的 'y' 值。这是用户可能标记模式的估计或猜测;或者对用户意图的猜测。你可以这样想。
y = w1x1 + w2x2 + w3x3 + .... + b在面包板上敲击时产生的输入值将成为 x1、x2、x3 等。
如前所述,这是一个相当简单的计算。即使像 STM32F103 这样没有浮点处理器的微控制器也可以通过仿真快速完成并返回结果。你想以非常高的吞吐量来做吗?可能不会,但只是像人类那样敲出一个信号并不是高量。
4、演示和使用
正如在初始训练期间所做的那样,输入敲击模式需要首先按下瞬时开关,之后 STM32 的板载 LED 将亮起,通过敲击面包板进入序列,然后再次按下按钮以信号完成。此时,板载 LED 将再次熄灭。此外,传感器有自己的绿色发光 LED,当它感应到振动时会闪烁。这是一个很好的视觉反馈。
STM32 蓝板有一个板载 LED,可以与 UART 一起工作。为了展示敲击预测的工作效果(或无效),"wilf" 的判断通过点亮绿色 LED 来显示,而 "Shave and a Haircut" 敲击则通过蓝色 LED 显示。如果估计分数不够强,板载 LED 会短暂闪烁,两个外部 LED 都不亮。间隔也会推送到 UART,但可能会被忽略。你甚至不需要连接 UART 就可以看到预测。
这是一个展示敲击决策的视频。
5、微控制器背景
如其他地方指出的那样,微控制器是离大数据中心最远的可能事物。几乎按任何标准,它消耗的功率都非常少。它非常紧凑,只有引脚与世界的其余部分互动,MCU 通常只有少量内存。
这里描述的 STM32F103C8T6 是表面贴装焊接到开发板上。众所周知的 "蓝板" 取名于 Matrix 电影中给出的选择。大约口香糖棒的大小,这个完整开发板也大约是几十年前你可能购买(或仍然可以购买)的一些微芯片的大小,这些芯片可能需要几个外部支持芯片的 CPU。但这个板不仅有 MCU(它本身是片上系统),还有复位按钮、两个独立的计时晶体、指示 LED,甚至还有一个 USB 连接器。以 72MHz 运行,具有 20K 的 RAM 和 128KB 或 256KB 的闪存,它不会让你的安卓手机运转起来。但如果明智使用并配合良好的电源供应,它可能会在没有充电的情况下运行更长时间。在这些设备上,闪存是二级或永久存储的替代品。直到你重新刷写(或除非出现严重错误),闪存将无限期地保持你存储的程序。相比之下,RAM 用于动态事物,如情况计算。程序和数据内存空间之间的分离也称为哈佛架构,与不分离它们的类似 CPU 的冯·诺伊曼架构相反。
6、代码
如上所述,有一些从 github 仓库改编的 C 代码。那是在桌面上运行的,以便在 Eclipse CDT 的帮助下训练权重和偏置。板载 STM32 C 代码是使用 ST Micro 的 STM32CubeIDE 构建的。该工具和他们的 STM32CubeProgrammer 都是免费的。可以在 IDE 中编辑代码,然后从命令行构建(在 Windows 上使用了 WSL 命令行)。任何运行 "make" 的方法都可以正常工作 — 当然包括使用 Linux。一旦构建,生成的 "elf" 二进制文件然后使用 STM32CubeProgrammer 刷写到 STM32 上。
这是代码,包括在训练期间发现的硬编码训练权重,以及结果偏置值。
/*
* Knock Classifier
* Author: Les Foster
* Date: 2026/02/16
* Purpose: STM32F103C8T6 code to recieve knock sequences through a vibration sensor and
* indicate one or another pattern was entered, using LEDs
*/
#include <FreeRTOS.h>
#include <task.h>
#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>
#include <libopencm3/stm32/usart.h>
#include <libopencm3/stm32/timer.h>
#include <libopencm3/stm32/exti.h>
#include <libopencm3/stm32/f1/nvic.h>
#include "miniprintf.h"
#define MAX_INTERVALS 9
// Formula: 72,000,000 / 5760 = 1,250. 1 tick is 0.00008s.
// 625 ticks is 0.05s. Therefore, min ticks is 1/20 of a second
#define MIN_TICKS 1200
// Avoid button-release stopping capture.
#define MIN_RELEASE 180
#define BLUE_LED GPIO7
#define GREEN_LED GPIO5
#define WEIGHTS_LEN = 7
static volatile uint8_t SENSOR_READ = 0;
static volatile uint16_t v_intervals[MAX_INTERVALS];
static volatile uint8_t v_in_capture = 0;
static volatile uint8_t v_intvl_num = 0;
static volatile uint8_t v_capture_complete = 0;
// Pre-trained values
// w for Feature 1 = -0.5317
// w for Feature 2 = -0.1439
// w for Feature 3 = 0.5865
// w for Feature 4 = 0.4011
// w for Feature 5 = -0.6478
// w for Feature 6 = -0.5286
// w for Feature 7 = 0.7674
// Bias (b) = 0.0000
static const float svm_weights[] = {
-0.5317, -0.1439, 0.5865, 0.4011, -0.6478, -0.5286, 0.7674
};
static const float svm_bias = 0.00;
static const float svm_alpha = 0.690684;
// Settling on a pair of "tau" values, because this is how the outcomes seem to
// be. When positive, the values can be closer to the hyperplane than when negative,
// and still be 'correct' for those judgement calls
static const float svm_tau_positive = 95.0;
static const float svm_tau_negative = 250.0;
static const int8_t svm_positive = (int8_t)1;
static const int8_t svm_negative = (int8_t)-1;
static const int8_t svm_unknown = (int8_t)0;
static int uart_printf(const char *format,...) __attribute((format(printf,1,2)));
/*********************************************************************
* UART
*********************************************************************/
static inline void
uart_putc(char ch) {
usart_send_blocking(USART1,ch);
}
static int
uart_printf(const char *format,...) {
va_list args;
int rc;
va_start(args,format);
rc = mini_vprintf_cooked(uart_putc,format,args);
va_end(args);
return rc;
}
static void
init_usart(void) {
//////////////////////////////////////////////////////////////
// STM32F103C8T6:
// RX: A9
// TX: A10
// CTS: A11 (not used)
// RTS: A12 (not used)
// Baud: 38400
//////////////////////////////////////////////////////////////
// GPIO_USART1_TX/GPIO13 on GPIO port A for tx
gpio_set_mode(GPIOA,GPIO_MODE_OUTPUT_50_MHZ,GPIO_CNF_OUTPUT_ALTFN_PUSHPULL,GPIO_USART1_TX);
usart_set_baudrate(USART1,38400);
usart_set_databits(USART1,8);
usart_set_stopbits(USART1,USART_STOPBITS_1);
usart_set_mode(USART1,USART_MODE_TX);
usart_set_parity(USART1,USART_PARITY_NONE);
usart_set_flow_control(USART1,USART_FLOWCONTROL_NONE);
usart_enable(USART1);
}
/*********************************************************************
* TIMER
*********************************************************************/
static void
setup_timer1(void) {
// TIM1:
timer_disable_counter(TIM1);
rcc_periph_reset_pulse(RST_TIM1);
timer_set_mode(TIM1,
TIM_CR1_CKD_CK_INT,
TIM_CR1_CMS_EDGE,
TIM_CR1_DIR_UP);
timer_set_prescaler(TIM1, 5760 - 1); // Needing 20x / second; this gives 5m before reset
// Only needed for advanced timers:
// timer_set_repetition_counter(TIM1, 1);
timer_continuous_mode(TIM1);
// timer_set_period(TIM1, 0); // resets to top
// timer_disable_break_main_output(TIM1); // No output is needed
}
static void
reset_timer1(void) {
timer_set_counter(TIM1, (uint16_t)0);
}
static void
start_timer1(void) {
setup_timer1(); //?
timer_enable_counter(TIM1);
reset_timer1();
}
static void
stop_timer1(void) {
timer_disable_counter(TIM1);
}
/*********************************************************************
* SENSOR
*********************************************************************/
static void
setup_sensor(void) {
// Enable IRQ for External interrupt 1
nvic_enable_irq(NVIC_EXTI1_IRQ);
// Setup the pin mode
gpio_set_mode(
GPIOB,
GPIO_MODE_INPUT,
// GPIO_CNF_INPUT_FLOAT, GPIO_CNF_INPUT_ANALOG, GPIO_CNF_INPUT_PULL_UPDOWN
GPIO_CNF_INPUT_FLOAT,
GPIO1);
/* Configure the EXTI subsystem. */
exti_select_source(EXTI1, GPIOB);
exti_set_trigger(EXTI1, EXTI_TRIGGER_FALLING);
exti_enable_request(EXTI1);
}
/*********************************************************************
* BUTTON
*********************************************************************/
static void
setup_button(void) {
// Enable IRQ for External interrupt 14
nvic_enable_irq(NVIC_EXTI15_10_IRQ);
// Setup the pin mode
gpio_set_mode(
GPIOB,
GPIO_MODE_INPUT,
// GPIO_CNF_INPUT_FLOAT, GPIO_CNF_INPUT_ANALOG, GPIO_CNF_INPUT_PULL_UPDOWN
GPIO_CNF_INPUT_FLOAT,
GPIO14);
/* Configure the EXTI subsystem. */
exti_select_source(EXTI14, GPIOB);
exti_set_trigger(EXTI14, EXTI_TRIGGER_FALLING);
exti_enable_request(EXTI14);
}
/*********************************************************************
* Pause
*********************************************************************/
static void
delay_ms(uint16_t ms) {
// Delay ~1ms with 72MHz clock
for (uint32_t i = 0; i < 7800 * ms; i++) { // 7200
__asm__("nop");
}
}
/*********************************************************************
* LED
*********************************************************************/
static void
brief_led(void) {
gpio_clear(GPIOC, GPIO13);
delay_ms(5);
gpio_set(GPIOC, GPIO13);
}
static void
choice_led(uint8_t led) {
gpio_set(GPIOB, led);
delay_ms(750);
gpio_clear(GPIOB, led);
}
/*********************************************************************
* Interpretation: using a dot product of weights, plus the bias
*********************************************************************/
static int8_t
get_prediction(void)
{
float total = 0.0;
for (int i = 0; i < 7; i++) {
float val = (0.08 * (float)v_intervals[i+2]);
uart_printf("%d, ", (int)val);
total = total + (svm_weights[i] * val);
}
float estimator = (total + svm_bias) * svm_alpha;
uart_printf(" with estimator %d\n", (int)estimator);
if (estimator < 0.0) {
if (-estimator < svm_tau_negative) {
return svm_unknown;
} else {
return svm_negative;
}
} else if (estimator < svm_tau_positive){
return svm_unknown;
} else {
return svm_positive;
}
}
/*********************************************************************
* Interrupt service routine
*********************************************************************/
void
exti1_isr(void) {
exti_reset_request(EXTI1); // Reset cause of ISR
if (v_in_capture == 1 && v_intvl_num < MAX_INTERVALS) {
uint32_t elapsed = timer_get_counter(TIM1);
if (elapsed > MIN_TICKS) {
// What was the elapsed interval? In STM32F103C8T6, TIM1 is only 16 bit
// but this library accounts for 32 bit timers as well.
v_intervals[v_intvl_num++] = (uint16_t)(elapsed & 0xFFFF);
reset_timer1();
}
}
}
void
exti15_10_isr(void) {
exti_reset_request(EXTI14); // Reset cause of ISR
if (v_in_capture == 1) {
uint32_t elapsed = timer_get_counter(TIM1);
if (elapsed >= MIN_RELEASE) {
v_in_capture = 0;
v_capture_complete = 1;
stop_timer1();
}
// set --> turns off
gpio_set(GPIOC, GPIO13);
}
else {
v_capture_complete = 0;
v_intvl_num = 0;
for (int i = 0; i < MAX_INTERVALS; i++) {
v_intervals[i] = 0;
}
start_timer1();
// clear --> turns on
gpio_clear(GPIOC, GPIO13);
v_in_capture = 1;
}
}
/*********************************************************************
* Main program
*********************************************************************/
int
main(void) {
rcc_clock_setup_pll(&rcc_hse_configs[RCC_CLOCK_HSE8_72MHZ]);
rcc_periph_clock_enable(RCC_GPIOA);
rcc_periph_clock_enable(RCC_GPIOB);
rcc_periph_clock_enable(RCC_GPIOC);
// This clock is for interrupts.
rcc_periph_clock_enable(RCC_AFIO);
rcc_periph_clock_enable(RCC_TIM1); // Need TIM1 clock
rcc_periph_clock_enable(RCC_USART1);
// Setup onboard LED
gpio_set_mode(
GPIOC,
GPIO_MODE_OUTPUT_2_MHZ,
GPIO_CNF_OUTPUT_PUSHPULL,
GPIO13);
gpio_set(GPIOC, GPIO13);
// Setup blue LED
gpio_set_mode(
GPIOB,
GPIO_MODE_OUTPUT_2_MHZ,
GPIO_CNF_OUTPUT_PUSHPULL,
BLUE_LED);
gpio_clear(GPIOB, BLUE_LED);
// Setup green LED
gpio_set_mode(
GPIOB,
GPIO_MODE_OUTPUT_2_MHZ,
GPIO_CNF_OUTPUT_PUSHPULL,
GREEN_LED);
gpio_clear(GPIOB, GREEN_LED);
setup_timer1();
setup_button();
setup_sensor();
init_usart();
for (;;) {
delay_ms(300);
if (v_capture_complete) {
v_capture_complete = 0;
for (int i = 0; i < v_intvl_num; i++) {
if (i > 0) {
uart_printf(",");
}
// Converting to ms
// There is no Floating Point processor.
uart_printf("%d", (int)(0.08 * v_intervals[i]));
}
uart_printf("\n");
// Now: doing some kind of test
int8_t prediction = get_prediction();
if (prediction == 1) {
choice_led(GREEN_LED);
} else if (prediction == -1) {
choice_led(BLUE_LED);
} else {
brief_led(); // flash on board means not callable
}
}
}
return 0;
}
// End这段代码利用了 "libopencm3" 库,以及作者 Warren Gay 的一些代码,他关于 STM32 的优秀书籍是开始不仅使用这些设备,而且使用 Free RTOS 的好方法。
7、电路
以下是一些电路布局的图像,带有描述。这个布局相当繁忙,即使在串联面包板上。因此,它可能看起来实际上更复杂。但它将详细描述。

完整电路:STM32 蓝板在左上角;振动传感器在 STM32 的左下角;按钮在底部;USB-to-TTL 在右上角;可以在 STM32 和 TTL 设备之间看到蓝色和绿色 LED。连接到 STM32 末端的是其编程器硬件
7.1 材料
以下所有价格均为 2026 年初期的美元价格。你在上图中看到的是:
- 面包板、电线、一些电阻、两个 LED 和一个按钮。这些通常包含在便宜的电子套件中
- STM32 蓝板开发板在左上角。有各种购买选择。大约 15 美元可以购买六个* 连接到蓝板顶部四个引脚的对象是 STM32 的编程设备。可以单独购买约 6 美元,或者可以与蓝板一起购买
- 振动传感器刚好插在开发板下方。SW-420 型号可以购买约 5 个,6.50 美元,或者在爱好传感器套件中
- 有一个 USB-to-TTL 站在左上角。这是可选的。如果使用,你可以监控输出消息,甚至添加一些你自己的
7.2 接线
所有这些都在面包板上显示。像往常一样,仔细考虑尝试以任何长期 可靠 的方式使用面包板项目。它们旨在使电路原型化。
首先,这些是从蓝板发出的连接。
- 四根电线到其编程设备。在运行时,它可以提供电源 — 或者可以通过 USB-to-TTL 提供。电线是白色用于电源/VCC,灰色用于接地,蓝色用于数据,紫色用于时钟。你的电线可能有所不同,但请确保它们在 MCU 和编程器之间正确匹配。
- 沿左侧,深绿色电线从引脚 B1 运行到 SW-420 传感器的数据输出。
- 沿左侧更远的是向面包板红色轨的输出电源电线。
- 有一根输出接地电线(用于所有组件的公共接地)到面包板的蓝色轨。
- 蓝板的右侧更忙。它还向面包板的轨提供电源,这些连接更靠近 "编程端" 的顶部。电源输出标记为 3.3,接地标记为 "gnd"。
- 引脚 B7 和 B5 运行到 LED。可以选择任何约定,但在这里,B7(带有蓝色电线)是到蓝色,B5(带有灰棕色电线)是到绿色 LED。
- 有一根白色电线运行到 USB-to-TTL 设备(如果你选择拥有一个用于调试目的)
- 最后,在 "usb 连接器" 端附近有一根黄色 dupont 电线,运行到按钮。按钮被放置在振动传感器远处,以尝试避免添加意外的振动。你仍然应该将拇指滚动到按钮上和关闭,以便传感器不会将按钮弹跳与敲击混淆。
当然,元件放置和电线类型的选择在一定程度上是任意的。如果在右侧使用 dupont 电线,可以在单个面包板上完成。

灰色是接地,白色是电源。紫色是时钟。蓝色是数据

灰色是接地。白色是电源。紫色是时钟。蓝色是数据

蓝板的所有电线

左侧视图:B1 到振动传感器。电源和接地到面包板轨

蓝板的右侧接线。更多电源和接地输出;连接到两个 LED;白色电线到 USB-to-TTL;黄色 dupont 电线用于按钮输入
现在,外设接线已描述。这将基于电线颜色。
- 每个 LED 从 MCU 通过 330Ω 电阻接线到其较长腿(或更精确地,其阳极接触)。不同的电阻值可以工作,但总是使用一个以避免烧毁 LED。较短(或阴极接触)腿必须连接到接地。
- USB-to-TTL 设备的中心引脚是 RXD。它从 MCU 的引脚 A9 接收数据。其电源(最右侧)和接地(从左侧第二)引脚连接到板电源轨
- MCU 的引脚 B14 通过 Dupont 电线连接到按钮输出。如下图所示,按钮有一个上拉电阻到电源。这里的值是 5.1kΩ,但其他值应该可以正常工作。该项目的代码不假设(也不建立)按钮输入上的内部上拉。如果你更愿意,可以更改代码并消除外部电阻。按钮使用上拉值,因此它在下降沿激活。当按下此瞬时开关时,将上拉侧连接到接地,导致电压电平变为零。

绿色 LED 和 USB-to-TTL。引脚 B5 运行到电阻的一个腿。相对的腿运行到 LED 的阳极(按照约定:较长腿),较短腿直接插入接地。USB-to-TTL 中间引脚从引脚 A9 接收(RXD),而其电源和接地引线连接到电源轨。注意 B14 / 从末端第三根的黄色 Dupont 电线

Dupont 电线的另一端连接到上拉按钮电阻。按钮的相对侧连接到接地。
8、编程和其他设置
构建此项目的全部细节不会在此处涵盖。然而,如果你追求它,原始数据采集文章 有更多信息。Warren Gay 的书籍对此涵盖得非常好。
9、效果如何?
基于 SVMLibC github 库的训练过程,使用此数据集运行得非常好。训练/测试比较显示 100% 准确率。可能是输入这些敲击的过程讽刺地训练了输入它们的人,但发现模型部署后,一些问题被解决,它运行得相当好。
此项目需要特别注意的当然是常规软件相关事情。这是 C 代码,不是 Python 或 Java,因此在处理内存使用等事情时需要小心。此外,如果你选择外设的不同 STM32 引脚分配,你可能必须获得一些关于哪些是通用引脚和哪些需要额外设置才能自由使用的信息深度。最后,为了避免接受 "任何旧序列" 作为任一模式或另一个,代码中引入了 alpha 和 tau 值。这些值部分通过调优,部分通过计算到达。权重的 "L2 范数" 可以计算为权重平方和的平方根。其倒数可以乘以传感器捕获间隔数组与权重的点积结果。然后此乘法将告诉点离决策边界有多近。如果它太近,则可以拒绝。这非常有效地消除了明显的事物,如给出了两次敲击。
tau 值(容忍度,如果你愿意)作为两个单独的值 95 和 250 效果最好。这些理想情况下应该缩放以防止重新训练的问题,但输入几个重复的敲击模式需要时间。
原文链接: Machine Learning in 20KB or Less
汇智网翻译整理,转载请标明出处