从零开始:基于STM32F103C8T6的丹青识画边缘端硬件原型设计
本文介绍了如何利用星图GPU平台,一键自动化部署🖼️ 丹青识画 · 智能影像雅鉴系统镜像,为嵌入式硬件原型提供强大的云端AI分析能力。该镜像的核心应用场景是接收来自STM32等边缘设备采集的图像,并进行智能化的画作内容、风格鉴定与分析,实现边缘与云的高效协同。
从零开始:基于STM32F103C8T6的丹青识画边缘端硬件原型设计
最近在捣鼓一个挺有意思的小项目:用一块几十块钱的STM32单片机,加上摄像头和Wi-Fi模块,做一个能联网鉴定画作的小设备。听起来是不是有点“小马拉大车”的感觉?毕竟STM32F103C8T6这块“蓝色小药丸”性能有限,跑不动复杂的AI模型。但我们的思路是,让它干它擅长的事——采集图像、简单处理、然后通过网络把“作业”交给云端更强大的“丹青识画”服务去分析。这样一来,一个低成本、低功耗的边缘硬件原型就诞生了,特别适合用在一些对实时性要求不高,但需要移动或离线部署的初步鉴定、数据采集场景里。
今天,我就来手把手带你把这个想法变成现实。我们会从硬件怎么连、程序怎么写,一直聊到怎么让这个小设备更省电、更稳定。如果你手头正好有一块STM32F103C8T6最小系统板,不妨跟着一起试试。
1. 项目整体思路与硬件选型
首先得搞清楚我们要做什么。这个设备的终极目标是:拍一张画作或图片的照片,然后告诉用户它可能是什么内容、属于什么风格。显然,让STM32自己识别是不现实的。所以,我们的架构是“边缘采集+云端分析”。
边缘端(STM32)负责:
- 控制摄像头模块拍照。
- 对拍到的原始图片进行压缩(为了节省流量和存储)。
- 通过Wi-Fi模块连接到互联网。
- 将压缩后的图片数据,按照云端API的要求,打包成HTTP请求发送出去。
- 接收云端返回的JSON格式的分析结果,并做简单解析和展示(比如通过串口打印)。
云端服务(假设的“丹青识画”服务)负责: 接收图片,运行大型图像识别模型进行分析,并将结构化的识别结果(如作者、风格、年代、内容描述等)返回。
基于这个思路,我们的硬件清单就出来了:
- 核心控制器:STM32F103C8T6最小系统板。这是我们的“大脑”,选择它是因为性价比极高,资源足够(72MHz主频,64KB Flash,20KB RAM),社区资料丰富。其内置的SPI、I2C、USART等外设正好用来连接其他模块。
- 图像采集:OV7670摄像头模块(带FIFO)。这是一个经典的30万像素摄像头模块。选择带FIFO(AL422B芯片)的版本至关重要,因为FIFO可以暂存一帧图像数据,让STM32有充足的时间慢慢读取,而不需要极高的实时性。
- 网络连接:ESP8266系列Wi-Fi模块(如ESP-01S)。这几乎是单片机联网的标配。它本身就是一个MCU,能处理复杂的TCP/IP协议栈,我们只需要通过串口(AT指令)命令它去连接Wi-Fi、发送HTTP请求即可,大大减轻了STM32的负担。
- 其他: 一些杜邦线、一个USB转TTL串口模块(用于调试和供电)、以及为各模块提供稳定3.3V电压的电源(STM32板载的LDO可能功率不足,需要外接或从USB转TTL取电)。
硬件连接关系如下图所示,你可以先有个直观印象:
+-------------------+
| |
| OV7670摄像头 |
| (带FIFO) |
| |
+--------+----------+
| 并行数据口+DVS/HREF等控制线
+--------v----------+
| |
| STM32F103C8T6 |
| 最小系统板 |
| |
+---+------------+--+
| |
USART2(TX/RX) | | USART1(TX/RX)
+---v------------v---+
| |
| ESP8266 Wi-Fi |
| 模块 |
| |
+-------------------+
|
+--------v----------+
| |
| 互联网 |
| (云端AI服务) |
| |
+-------------------+
2. 硬件连接详解
接下来,我们得把这几块板子用杜邦线正确地“缝合”起来。请对照你的模块引脚定义进行操作。
2.1 STM32与OV7670(带FIFO)连接
OV7670的FIFO模块让我们避开了直接驱动摄像头的时序难题。我们主要连接控制线和数据线。
| STM32F103C8T6引脚 | OV7670 FIFO模块引脚 | 功能说明 |
|---|---|---|
| PA8 | XCLK | 为摄像头提供主时钟(可由STM32的MCO输出) |
| PC13 | RESET | 复位摄像头(低电平有效) |
| PB5 | PWDN | 电源控制(高电平为掉电模式,通常接低) |
| PB6 | VSYNC | 垂直同步信号,接外部中断,用于检测一帧开始 |
| PA0 | WRST / RRST | 写指针复位/读指针复位(通常短接,同一信号控制) |
| PA1 | OE | FIFO输出使能(低电平有效) |
| PA2 | RCLK | 读FIFO时钟(上升沿读取数据) |
| PA3 | WE | FIFO写使能(由FIFO模块自动控制,通常仅监测) |
| PB12~PB15, PA4~PA7 | D0~D7 | 8位并行数据总线(具体连接顺序需与程序一致) |
关键点:
- 数据总线:需要占用STM32的8个连续的IO口(最好属于同一GPIO端口)以提高读取速度。这里示例使用了PB12-15和PA4-7,你也可以规划其他连续的8个引脚。
- VSYNC:连接到具有外部中断功能的引脚(如PB6),用于精确捕获一帧图像的起始时刻。
- 电源:确保OV7670模块和FIFO模块都稳定工作在3.3V。
2.2 STM32与ESP8266连接
ESP8266通过AT指令与STM32通信,使用串口连接最为简单。
| STM32F103C8T6引脚 | ESP8266 (ESP-01S)引脚 | 功能说明 |
|---|---|---|
| PA2 (USART2_TX) | RX | STM32发送指令给ESP8266 |
| PA3 (USART2_RX) | TX | STM32接收ESP8266的响应 |
| 3.3V | VCC | 电源(确保电流足够,最好单独供电) |
| GND | GND | 共地 |
| PC14 | RST | 复位引脚(可选,低电平复位) |
| PC15 | IO0 | 工作模式选择(上拉为正常模式,下拉为烧录模式) |
关键点:
- 电源是重中之重:ESP8266在发射Wi-Fi信号时峰值电流可能超过200mA,STM32板载的3.3V LDO通常无法承受。强烈建议使用外接的3.3V稳压电源模块,或者使用能提供500mA以上电流的USB转TTL模块的3.3V输出口为其单独供电。所有模块的GND必须连接在一起。
- 电平匹配:ESP8266和STM32都是3.3V电平,可以直接连接。
- 串口选择:这里使用了USART2,你也可以用其他可用的USART。
3. 嵌入式软件程序框架
硬件连好了,接下来就是让STM32“活”起来的代码。我们使用Keil MDK或STM32CubeIDE进行开发。程序主要分为几个模块。
3.1 图像采集模块 (OV7670 FIFO Driver)
这个模块的核心任务是:在VSYNC信号指示新的一帧到来时,启动读取流程,将FIFO里的一整帧图像数据搬运到STM32的内存缓冲区中。
// 省略了头文件和引脚定义部分
#define IMAGE_WIDTH 160 // 为了节省内存和传输时间,我们采集QVGA或更小的分辨率
#define IMAGE_HEIGHT 120
#define IMAGE_BUFFER_SIZE (IMAGE_WIDTH * IMAGE_HEIGHT * 2) // RGB565格式
uint8_t image_buffer[IMAGE_BUFFER_SIZE];
// VSYNC外部中断服务函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if(GPIO_Pin == VSYNC_Pin) {
if(HAL_GPIO_ReadPin(VSYNC_GPIO_Port, VSYNC_Pin) == 0) {
// VSYNC变低,表示一帧开始,可以准备读取
fifo_start_read_frame();
}
}
}
void fifo_start_read_frame(void) {
// 1. 复位FIFO读指针 (拉低再拉高RRST引脚)
HAL_GPIO_WritePin(RRST_GPIO_Port, RRST_Pin, GPIO_PIN_RESET);
delay_us(1);
HAL_GPIO_WritePin(RRST_GPIO_Port, RRST_Pin, GPIO_PIN_SET);
// 2. 使能FIFO输出 (拉低OE)
HAL_GPIO_WritePin(OE_GPIO_Port, OE_Pin, GPIO_PIN_RESET);
// 3. 循环读取像素数据
for(uint32_t i = 0; i < IMAGE_BUFFER_SIZE; i++) {
// 产生一个RCLK上升沿
HAL_GPIO_WritePin(RCLK_GPIO_Port, RCLK_Pin, GPIO_PIN_RESET);
delay_us(1);
// 在上升沿期间读取8位数据总线
image_buffer[i] = read_data_bus(); // 自定义函数,从GPIO口读取8位数据
HAL_GPIO_WritePin(RCLK_GPIO_Port, RCLK_Pin, GPIO_PIN_SET);
delay_us(1);
// 可以在这里加入行同步(HREF)判断来精确控制行宽,简化版可省略
}
// 4. 禁止FIFO输出
HAL_GPIO_WritePin(OE_GPIO_Port, OE_Pin, GPIO_PIN_SET);
// 设置标志位,通知主循环图像已就绪
image_ready_flag = 1;
}
3.2 图像压缩模块
直接从摄像头读出的RGB565数据量对于网络传输来说还是太大。我们需要压缩。在STM32上实现JPEG编码比较吃力,一个更轻量的选择是使用行程编码(RLE) 或转换为灰度图后再压缩。这里以转换为灰度图并简单裁剪为例,实际项目中可以考虑移植微型JPEG编码库(如TinyJPEG)或发送原始的小尺寸RGB数据。
// 将RGB565缓冲区转换为灰度图并缩小尺寸(简单平均)
void compress_to_grayscale(uint8_t *rgb565_buf, uint8_t *gray_buf, uint16_t orig_w, uint16_t orig_h, uint16_t new_w, uint16_t new_h) {
uint16_t block_w = orig_w / new_w;
uint16_t block_h = orig_h / new_h;
for(uint16_t y = 0; y < new_h; y++) {
for(uint16_t x = 0; x < new_w; x++) {
uint32_t sum_r = 0, sum_g = 0, sum_b = 0;
// 对原始图像的一个小方块取平均
for(uint16_t by = 0; by < block_h; by++) {
for(uint16_t bx = 0; bx < block_w; bx++) {
uint16_t orig_idx = ((y*block_h + by) * orig_w + (x*block_w + bx)) * 2;
uint16_t pixel = (rgb565_buf[orig_idx+1] << 8) | rgb565_buf[orig_idx];
// 提取RGB分量 (RGB565)
uint8_t r = (pixel >> 11) & 0x1F;
uint8_t g = (pixel >> 5) & 0x3F;
uint8_t b = pixel & 0x1F;
// 转换为灰度 (简化公式)
sum_r += r;
sum_g += g;
sum_b += b;
}
}
uint16_t avg_gray = ( (sum_r/block_w/block_h) * 77 + (sum_g/block_w/block_h) * 150 + (sum_b/block_w/block_h) * 29 ) >> 8;
gray_buf[y * new_w + x] = (uint8_t)avg_gray;
}
}
}
3.3 Wi-Fi通信与HTTP客户端模块
这是连接云端的关键。我们通过串口向ESP8266发送AT指令。
// 向ESP8266发送指令并等待响应
uint8_t esp8266_send_cmd(char *cmd, char *expected_ack, uint32_t timeout) {
uart_clear_buffer(); // 清空接收缓冲区
uart_send_string(cmd); // 通过USART2发送AT指令字符串
return uart_wait_for_string(expected_ack, timeout); // 等待期待的回显
}
// 配置ESP8266并连接Wi-Fi和服务器
uint8_t init_network_connection(void) {
// 1. 测试AT指令
if(!esp8266_send_cmd("AT\r\n", "OK", 1000)) return 0;
// 2. 设置Wi-Fi模式为Station
if(!esp8266_send_cmd("AT+CWMODE=1\r\n", "OK", 1000)) return 0;
// 3. 连接路由器
char connect_cmd[128];
sprintf(connect_cmd, "AT+CWJAP=\"%s\",\"%s\"\r\n", WIFI_SSID, WIFI_PASSWORD);
if(!esp8266_send_cmd(connect_cmd, "OK", 10000)) return 0; // 连接可能较慢
// 4. 获取本地IP地址(可选,用于调试)
uart_send_string("AT+CIFSR\r\n");
delay_ms(500);
// 5. 建立TCP连接(假设云端服务IP为192.168.1.100,端口80)
sprintf(connect_cmd, "AT+CIPSTART=\"TCP\",\"%s\",%d\r\n", SERVER_IP, SERVER_PORT);
if(!esp8266_send_cmd(connect_cmd, "OK", 5000)) return 0;
return 1;
}
// 发送HTTP POST请求(携带图片数据)
uint8_t send_image_via_http(uint8_t *image_data, uint32_t data_len) {
// 1. 准备HTTP报文头
char header[512];
sprintf(header,
"POST /api/identify HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Content-Type: application/octet-stream\r\n"
"Content-Length: %lu\r\n"
"Connection: close\r\n"
"\r\n",
SERVER_IP, SERVER_PORT, data_len);
// 2. 计算总发送长度
uint32_t header_len = strlen(header);
uint32_t total_len = header_len + data_len;
// 3. 设置ESP8266为透传模式,并告知数据长度
char cipsend_cmd[64];
sprintf(cipsend_cmd, "AT+CIPSEND=%lu\r\n", total_len);
if(!esp8266_send_cmd(cipsend_cmd, ">", 2000)) return 0; // 等待‘>’提示符
// 4. 发送HTTP头
uart_send_string(header);
// 5. 发送图片数据(需要以字节流形式发送)
uart_send_bytes(image_data, data_len);
// 6. 等待响应(这里需要根据实际云端API的响应格式进行解析)
// 例如,等待包含结果关键词的JSON字符串
if(uart_wait_for_string("\"success\":true", 10000)) {
// 从接收缓冲区中解析出具体的识别结果...
parse_json_response();
return 1;
}
return 0;
}
3.4 主程序逻辑
最后,我们把所有模块像拼图一样组合起来。
int main(void) {
// HAL初始化、时钟配置、GPIO初始化、串口初始化、外部中断初始化...
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART2_UART_Init();
MX_USART1_UART_Init(); // 用于调试打印
// ... 其他初始化
printf("System Boot OK.\r\n");
// 初始化摄像头(配置OV7670寄存器,需要根据具体模块编写)
ov7670_init();
// 连接网络
printf("Connecting to Wi-Fi...\r\n");
while(!init_network_connection()) {
printf("Wi-Fi Connect Failed, retry...\r\n");
HAL_Delay(2000);
}
printf("Wi-Fi Connected.\r\n");
while(1) {
// 等待一帧图像采集完成
if(image_ready_flag) {
image_ready_flag = 0;
printf("Image captured.\r\n");
// 图像压缩处理
uint8_t compressed_buf[COMPRESSED_SIZE];
compress_to_grayscale(image_buffer, compressed_buf, IMAGE_WIDTH, IMAGE_HEIGHT, 80, 60);
// 通过HTTP发送图像数据
printf("Sending to cloud...\r\n");
if(send_image_via_http(compressed_buf, COMPRESSED_SIZE)) {
printf("Identification successful!\r\n");
// 在这里处理并显示结果,比如通过串口打印
} else {
printf("Identification failed.\r\n");
}
// 一次任务完成,进入低功耗模式或等待下一次触发(如按键)
printf("Task finished. Entering low-power mode.\r\n");
enter_low_power_mode();
// 等待外部唤醒(如另一个按键)
wait_for_wakeup();
}
HAL_Delay(10); // 主循环短暂延迟
}
}
4. 低功耗与稳定性设计考量
一个实用的原型不能一直满功率运行。我们需要考虑省电和抗干扰。
1. 工作模式循环:
- 活跃模式:拍照、压缩、发送数据、接收结果时,全速运行。
- 睡眠模式:完成任务后,让STM32进入
Stop或Sleep模式,关闭外设时钟(如摄像头时钟、串口),仅保留唤醒源(如外部中断按键)有效。ESP8266也可以通过AT指令(AT+GSLP)进入深度睡眠。 - 定时唤醒:如果需要定期工作,可以配置STM32的RTC(实时时钟)闹钟,从低功耗模式定时唤醒。
2. 电源管理:
- 使用MOSFET或负载开关电路,在睡眠时彻底切断摄像头、ESP8266等大功耗模块的电源,仅保留STM32的待机电路。
- 确保3.3V电源网络在ESP8266发射瞬间的电压跌落不会导致STM32复位,可增加大容量(如100uF)的钽电容缓冲。
3. 通信可靠性:
- AT指令超时与重试:每个AT指令交互都必须设置超时机制,失败后应有重试逻辑(如最多3次)。
- TCP连接保活:长时间待机后,TCP连接可能断开。每次唤醒后需要检查连接状态,必要时重新执行
AT+CIPSTART。 - 数据完整性:对于重要的HTTP响应,可以计算CRC或使用更健壮的协议(如MQTT over SSL,但STM32F103实现较复杂)。
4. 图像采集优化:
- 如果光照条件差,图像噪声会严重影响云端识别效果。可以在采集多帧后做简单的软件滤波(如中值滤波)。
- 根据场景固定对焦(如果摄像头支持),避免拍糊。
5. 总结与展望
折腾完这一套,你会发现,用STM32F103这样的经典单片机做边缘AI原型,核心思路就是“扬长避短”。它不擅长计算密集型任务,但胜在接口丰富、控制精准、功耗管理灵活。我们把复杂的识别任务卸载到云端,它只负责当好一个可靠的数据采集和传输节点。
这个原型虽然简单,但已经具备了边缘智能设备的几个关键要素:传感器数据采集、前端轻量处理、无线数据传输、云端协同、低功耗设计。你可以基于它进行很多扩展,比如换用更高像素的摄像头、增加LCD屏来本地显示结果、使用电池供电使其完全便携,甚至集成多个传感器(温湿度、光照)来丰富上传的数据上下文。
当然,它也有局限,比如依赖网络、识别速度受网络延迟影响。但对于艺术教育辅助、画廊展品信息采集、甚至是智能家居中的简单图像触发场景,它提供了一个非常经济且可行的起点。下一步,如果你手头的项目对实时性要求更高,可以考虑性能更强的STM32H7系列,或者尝试在STM32上直接部署裁剪后的TinyML模型,那又是另一个充满挑战和乐趣的世界了。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐

所有评论(0)