从零开始:基于STM32F103C8T6的丹青识画边缘端硬件原型设计

最近在捣鼓一个挺有意思的小项目:用一块几十块钱的STM32单片机,加上摄像头和Wi-Fi模块,做一个能联网鉴定画作的小设备。听起来是不是有点“小马拉大车”的感觉?毕竟STM32F103C8T6这块“蓝色小药丸”性能有限,跑不动复杂的AI模型。但我们的思路是,让它干它擅长的事——采集图像、简单处理、然后通过网络把“作业”交给云端更强大的“丹青识画”服务去分析。这样一来,一个低成本、低功耗的边缘硬件原型就诞生了,特别适合用在一些对实时性要求不高,但需要移动或离线部署的初步鉴定、数据采集场景里。

今天,我就来手把手带你把这个想法变成现实。我们会从硬件怎么连、程序怎么写,一直聊到怎么让这个小设备更省电、更稳定。如果你手头正好有一块STM32F103C8T6最小系统板,不妨跟着一起试试。

1. 项目整体思路与硬件选型

首先得搞清楚我们要做什么。这个设备的终极目标是:拍一张画作或图片的照片,然后告诉用户它可能是什么内容、属于什么风格。显然,让STM32自己识别是不现实的。所以,我们的架构是“边缘采集+云端分析”。

边缘端(STM32)负责:

  1. 控制摄像头模块拍照。
  2. 对拍到的原始图片进行压缩(为了节省流量和存储)。
  3. 通过Wi-Fi模块连接到互联网。
  4. 将压缩后的图片数据,按照云端API的要求,打包成HTTP请求发送出去。
  5. 接收云端返回的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位并行数据总线(具体连接顺序需与程序一致)

关键点:

  1. 数据总线:需要占用STM32的8个连续的IO口(最好属于同一GPIO端口)以提高读取速度。这里示例使用了PB12-15和PA4-7,你也可以规划其他连续的8个引脚。
  2. VSYNC:连接到具有外部中断功能的引脚(如PB6),用于精确捕获一帧图像的起始时刻。
  3. 电源:确保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 工作模式选择(上拉为正常模式,下拉为烧录模式)

关键点:

  1. 电源是重中之重:ESP8266在发射Wi-Fi信号时峰值电流可能超过200mA,STM32板载的3.3V LDO通常无法承受。强烈建议使用外接的3.3V稳压电源模块,或者使用能提供500mA以上电流的USB转TTL模块的3.3V输出口为其单独供电。所有模块的GND必须连接在一起。
  2. 电平匹配:ESP8266和STM32都是3.3V电平,可以直接连接。
  3. 串口选择:这里使用了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进入StopSleep模式,关闭外设时钟(如摄像头时钟、串口),仅保留唤醒源(如外部中断按键)有效。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星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐