简介:面向嵌入式开发者与智能硬件爱好者的完整参考资料,围绕STM32F103VET6与2.0寸TFT屏、蓝牙模块,提供从ALTIUM原理图到软件源码的全套设计。资源为ZIP压缩包,共1075个文件、约36.99MB,其中C/H源代码覆盖引导加载、实时操作系统驱动、图形用户界面库、蓝牙通信库以及时间显示、通知处理、健康监测等应用层逻辑,ALTIUM相关文件则呈现原理图与PCB布局,另有BMP图片、PDF与DOC文档辅助理解整体结构。这颗基于Cortex-M3内核的芯片主频72MHz,内置512KB闪存和64KB SRAM,资源中针对其丰富外设接口的扩展设计值得深入学习。已有292人学习下载,适合项目移植、毕业设计和产品原型验证。通过分析原理图与源码,可掌握GPIO、SPI/UART等外设的配置方法,理解电池管理、TFT显示驱动、蓝牙数据收发和系统任务调度的实现路径,对提升嵌入式开发和低功耗穿戴应用能力有明显价值。
1. 从一块开发板到一个能戴在手腕上的 TFT 彩屏手表
拿到这个 zip 的工程师,通常不是为了看一款商品级手表的拆解,而是想用 STM32F103VET6、2.0 寸 TFT 彩屏和蓝牙模块自己拼出一个能跑通的最小智能手表系统。标题里真正要拆的是三件事:ALTIUM 原理图决定硬件怎么连,软件源码决定屏幕亮不亮、蓝牙稳不稳,而“2.0寸TFT”和“蓝牙”这两项决定了大部分引脚分配和通信协议。下面按硬件设计、固件驱动、蓝牙实战、排错进阶的顺序把整套方案讲透;给的可执行步骤能直接照抄,也会把引脚映射、电平转换、HC-05 连接不上这类常见问题提前说破。适合做毕业设计、开源硬件复刻,以及想把别人工程移植到自己板子上的嵌入式开发。
2. STM32F103VET6 与 2.0 寸 TFT、蓝牙的硬件链路设计
2.1 为什么是 VET6:引脚余量和双通信通道
STM32F103VET6 是 LQFP100 封装,内置 512KB Flash 和 64KB SRAM,对比最常见的 C8T6(48 引脚 / 64KB Flash),多出来的引脚和存储空间在手表场景非常关键。手表固件里通常要放中文字库和图标资源,64KB Flash 会很紧张,512KB 可以把它们直接放进内部 Flash,省掉外部 Flash 的成本和布线。更重要的是通信资源:一个 SPI1 接 TFT 彩屏,一个 USART2 接蓝牙,还能留 USART1 做调试日志,三路互不影响。实践中我更倾向于把 TFT 挂在 SPI1、蓝牙挂在 USART2、调试日志放 USART1,这样即使屏幕刷新占满 CPU,蓝牙数据也有独立的中断入口。
以一块 2.0 寸、240×320、主控为 ST7789V 的 SPI 接口 TFT 液晶屏为例,常见引脚映射如下:
| 功能 | 信号 | STM32F103VET6 引脚 | 说明 |
|---|---|---|---|
| SPI1 时钟 | TFT_SCLK | PA5 | 分频后约 18MHz |
| SPI1 主出从入 | TFT_MOSI | PA7 | 屏幕单输入,MISO 可不接 |
| SPI 片选 | TFT_CS | PA4 | 软件控制,低电平有效 |
| 数据/命令选择 | TFT_DC | PB0 | 低电平发命令,高电平发数据 |
| 复位 | TFT_RST | PB1 | 低电平复位 |
| 背光 | TFT_BL | PB10 | 也可以用 PWM 调亮度 |
2.2 蓝牙模块选型与 ALTIUM 原理图的电平关系
标题只写了“蓝牙”,没有说是 BLE 还是经典蓝牙。带 ALTIUM 原理图和源码的手表项目,绝大多数用的是 HC-05/HC-06 这类基于 CSR 芯片的经典蓝牙 3.0 模块,走 SPP 串口协议。原因很现实:STM32F103 原生没有 BLE 协议栈,外挂 SPP 模块在固件里只需要处理串口收发,代码量最小。HC-05 支持主从一体,比 HC-06 更适合做手表:它能被手机连接,也能在另一块板子上做主设备主动回连,STATE 引脚还能直接指示连接状态,硬件上只需一根 GPIO 线。
原理图设计时要注意电平。很多 HC-05 模块板载了 3.3V LDO,VCC 可以接 5V,但串口逻辑电平仍然是 3.3V。如果参考设计里单片机是 5V,必须在 TX 到蓝牙 RXD 之间串联 1kΩ 电阻限流。STM32F103 的 GPIO 是 3.3V,这里没有 5V 耐受问题,但我仍会在模块 RXD 上串一个 100Ω 电阻,用来抑制反射。供电方面,外部电源先接到 AMS1117-3.3 或 RT9013,输出 3.3V 再分给 MCU、蓝牙模块和 TFT 背光。蓝牙模块在广播或回连时峰值电流会到 30~50mA,TFT 背光 20~40mA,整机稳态电流约 100mA,选择 LDO 时不要只看额定电流,还要看压差。
2.3 ALTIUM 原理图绘制:去耦、guard ring 和射频处理
用 ALTIUM 画这块板子,最容易出问题的地方不是连线,而是电源完整性和地平面。每个电源引脚旁边放 100nF 去耦电容是基本要求,位置尽量靠近引脚,过孔到电容再到电源脚的回路要短。蓝牙射频部分如果和 TFT 数据线、背光 PWM 线长距离并行,容易把辐射耦合进参考时钟。常见做法是在模块下方铺连续地,不要走太细的地线;射频电源加一个 LC 滤波。晶振和模拟信号附近可以画一圈接地环,也就是很多人说的 guard ring:在 Altium Designer 里用 Polygon Pour 或 Isolate 区域实现,设置 0.3mm 以上间距,并打上一圈过孔,能明显降低来自周围走线的耦合干扰。
2.4 固件侧的引脚初始化要和原理图保持一致
原理图连线正确只是第一步,屏能亮还必须让固件里的 GPIO 复用和原理图一一对应。下面这段是初始化 TFT 的 SPI1 和蓝牙串口 USART2 的代码,对应 2.1 小节的引脚表:
static void lcd_pin_init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_SPI1, ENABLE); GPIO_StructInit(&gpio); gpio.GPIO_Mode = GPIO_Mode_AF_PP; // SPI1_SCK/MOSI 复用推挽 gpio.GPIO_Speed = GPIO_Speed_50MHz; gpio.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Mode = GPIO_Mode_Out_PP; // 软件控制 CS/DC/RST/BL gpio.GPIO_Speed = GPIO_Speed_2MHz; gpio.GPIO_Pin = GPIO_Pin_4; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_10; GPIO_Init(GPIOB, &gpio); }注意 PA4 是 SPI1 的硬件 NSS 引脚,如果直接配置成复用功能,SPI 主机可能自动产生片选动作;这里把它改成普通推挽输出,由软件拉低选中屏幕,是最稳妥的做法。SCK 和 MOSI 为什么用AF_PP而不是Out_PP:SPI 外设接管引脚后,数据由外设移位寄存器驱动,普通推挽输出无法产生 SPI 时序。
蓝牙串口初始化对应 USART2 的 PA2/PA3:
void bt_uart_init(uint32_t baud) { USART_InitTypeDef usart; GPIO_InitTypeDef gpio; RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin = GPIO_Pin_2; // PA2 = USART2_TX gpio.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_3; // PA3 = USART2_RX gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); USART_StructInit(&usart); usart.USART_BaudRate = baud; usart.USART_WordLength = USART_WordLength_8b; usart.USART_Parity = USART_Parity_No; USART_Init(USART2, &usart); USART_Cmd(USART2, ENABLE); }HC-05/06 出厂默认波特率常见为 9600 或 38400,第一次上电时先看模块背面丝印。之后用 AT 指令把波特率改成 115200,这里的baud参数也要同步改,否则出现配对成功但数据乱码。参数说明:USART_WordLength_8b和USART_Parity_No是 8N1,这是 HC-05 的默认帧格式;如果模块被配置成了偶校验,这里要一起改,否则进入 AT 模式也会一直 ERROR。
3. TFT 彩屏驱动与蓝牙串口数据通道的软件源码实现
3.1 2.0 寸 TFT 彩屏的初始化序列和最小显示代码
2.0 寸模块常见主控是 ST7789V 或 ILI9341,驱动差别只在初始化序列。以 ST7789V 为例,上电后先复位,再发 Sleep Out 和 Display On,然后设置像素格式。下面是一段能直接放进工程的初始化代码:
static void lcd_write_cmd(uint8_t cmd) { LCD_DC_LOW(); // DC=0,传输命令 lcd_spi_write(cmd); LCD_DC_HIGH(); // 恢复为数据模式 } static void lcd_write_data(uint8_t data) { LCD_DC_HIGH(); // DC=1,传输数据 lcd_spi_write(data); } void lcd_init(void) { lcd_reset(); delay_ms(120); lcd_write_cmd(0x11); // SLPOUT:退出睡眠 delay_ms(120); lcd_write_cmd(0x3A); lcd_write_data(0x05); // 16bit/pixel,RGB565 lcd_write_cmd(0x36); lcd_write_data(0x00); // MADCTL:扫描方向和 RGB 顺序 lcd_write_cmd(0x29); // DISPON:开显示 lcd_fill_screen(0x0000); // 全屏清黑 }关键参数在0x3A和0x36。0x3A后面的0x05表示 16 位色深,对应固件里的uint16_t颜色值;如果写成0x06(18 位),大部分 SPI 屏幕在 RGB565 模式下颜色会变得很怪。0x36的 MADCTL 值直接控制扫描方向和 RGB 顺序,屏幕上下反时把0x00改成0xC0,左右反改成0xA0,颜色偏红偏蓝时尝试0x08这类带 RGB 交换位的值。这个参数不用每次都重烧固件,可以做成编译宏,在不同批次屏幕间切换。
区域填充是显示的核心:
void lcd_fill_rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t i; lcd_write_cmd(0x2A); // 设置列地址 lcd_write_data(x0 >> 8); lcd_write_data(x0 & 0xFF); lcd_write_data(x1 >> 8); lcd_write_data(x1 & 0xFF); lcd_write_cmd(0x2B); // 设置行地址 lcd_write_data(y0 >> 8); lcd_write_data(y0 & 0xFF); lcd_write_data(y1 >> 8); lcd_write_data(y1 & 0xFF); lcd_write_cmd(0x2C); // RAMWR:开始写显存 LCD_DC_HIGH(); for (i = 0; i <= (uint32_t)((x1 - x0 + 1) * (y1 - y0 + 1)); i++) lcd_spi_write16(color); // 先高字节后低字节 }lcd_spi_write16要按大端序先发高字节再发低字节,ST7789V 的显存写时序就是这样要求的。如果用lcd_spi_write(color & 0xFF)先发低字节,全屏会出现水平条纹或颜色错位。
3.2 蓝牙串口接收的环形缓冲区设计
蓝牙模块把手机发来的数据通过串口逐字节送给 STM32,如果主循环里用轮询接收,屏幕刷新时会丢帧。手表源码里最常见的方案是环形缓冲区,让 USART2 中断把字节放进去,主循环在空闲时取出来解析。
#define BT_RX_BUF_SIZE 256 static uint8_t bt_rx_buf[BT_RX_BUF_SIZE]; static volatile uint16_t bt_rx_head = 0; static volatile uint16_t bt_rx_tail = 0; void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE)) { bt_rx_buf[bt_rx_head] = USART_ReceiveData(USART2); bt_rx_head = (bt_rx_head + 1) % BT_RX_BUF_SIZE; USART_ClearITPendingBit(USART2, USART_IT_RXNE); } } uint8_t bt_poll_byte(void) { uint8_t ch; if (bt_rx_head == bt_rx_tail) return 0; ch = bt_rx_buf[bt_rx_tail]; bt_rx_tail = (bt_rx_tail + 1) % BT_RX_BUF_SIZE; return ch; }中断里只写bt_rx_head,主循环只读bt_rx_tail,两个指针不会同时被修改,所以不需要临界区保护。缓冲区取 256 字节是因为手表控制指令和状态上报都很短;如果以后要支持 OTA 或大批量数据,这个方案会被高速填满,应改成 DMA 环形队列,让 USART2 的 DMA 搬运直接写内存。
3.3 命令解析与 UI 联动
光有字节流还不够,还要定义协议。简单做法:手机 App 发送以\r\n结尾的文本命令,STM32 收到完整一行再解析,而不逐字节判断。
#define CMD_TIME "TIME" #define CMD_NOTIFY "NOTICE" void bt_handle_packet(uint8_t *pkt, uint16_t len) { if (len < 6) return; pkt[len] = '\0'; if (strncmp((char *)pkt, CMD_TIME, 4) == 0 && pkt[4] == ':') { lcd_draw_string(0, 16, (char *)pkt + 5, COLOR_BLACK); // 刷新时间区 } else if (strncmp((char *)pkt, CMD_NOTIFY, 6) == 0) { lcd_draw_notify((char *)pkt + 7); // 通知条 } }这一段要说明:先用\r\n分帧,再把命令名和参数分开。直接对USART2收到的每个字节调用lcd_draw_string会让屏幕闪烁,因为一个字符串要多少帧才能收完。注意pkt[len] = '\0'前要确保len小于缓冲区容量,否则会越界写。
4. HC-05 蓝牙配对、自动回连与测距参数的实战设置
4.1 用 AT 指令把 HC-05 配置成手表端从机
HC-05 上电前按住模块上的按钮再上电,会进入 AT 模式,此时模块的 RXD 接 USB-TTL 的 TX,VCC 接 5V,GND 共地。默认波特率常见是 38400。用 Linux 串口终端逐条发送:
stty -F /dev/ttyUSB0 38400 cs8 -cstopb -parenb echo -e "AT+NAME=Watch\r\n" > /dev/ttyUSB0 echo -e "AT+PSWD=8888\r\n" > /dev/ttyUSB0 echo -e "AT+ROLE=0\r\n" > /dev/ttyUSB0 echo -e "AT+UART=115200,0,0\r\n" > /dev/ttyUSB0完成下面几个关键配置:
| 目标 | AT 指令 | 说明 |
|---|---|---|
| 设备名称 | AT+NAME=Watch | 手机扫描时显示的名称 |
| 配对码 | AT+PSWD=8888 | 手机配对时输入的 PIN |
| 角色 | AT+ROLE=0 | 0=从机,1=主机 |
| 配对模式 | AT+CMODE=1 | 1=允许任意设备连接,0=仅绑定的地址 |
| 串口参数 | AT+UART=115200,0,0 | 波特率、停止位、校验位 |
参数说明:AT+CMODE=1适合开发阶段,任意手机都能连;量产建议改成AT+CMODE=0并配合AT+BIND=<手机蓝牙地址>,防止附近其他手机抢占连接。AT+UART里的后两位是停止位和校验位,0,0表示 1 位停止位、无校验,必须与固件里的 8N1 保持一致。
提示:HC-05 对 AT 指令的响应是
OK\r\n或ERROR(0)\r\n。如果发送后没反应,先确认 KEY 或 EN 引脚是否已经拉到 3.3V;部分模块只有在上电时检测该引脚才会进入 AT 模式,运行中再拉高无效。
4.2 自动连接和连接状态监测
手表从机模式是被手机连接的,所以“自动连接”通常由 App 侧完成。固件需要做的,是准确知道当前是否已连接,以便 UI 显示连接状态。HC-05 的 STATE 引脚空闲时为低电平,配对后为高电平,可以把它接到 STM32 的一个 GPIO 上:
void bt_monitor_task(void) { static uint8_t last_state = 0xFF; uint8_t s = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_12); if (s != last_state) { if (s == Bit_SET) lcd_draw_string(0, 8, "Connected", COLOR_GREEN); else lcd_draw_string(0, 8, "Disconnected", COLOR_RED); last_state = s; } }如果板子上没接 STATE 引脚,也可以在应用层定时发AT+STATE?查询,但查询指令会占用串口,并且 HC-05 在透传模式下不一定会响应 AT 指令(取决于模块固件版本)。所以最可靠的还是硬件状态引脚,这也是标题里那份 ALTIUM 原理图通常会把蓝牙模块状态脚引出来的原因。
4.3 蓝牙测距的 RSSI 模型和信号阈值
蓝牙测距在手表里的典型用途是靠近解锁或离开锁屏。HC-05 模块本身不暴露 RSSI,常见做法是让手机 App 读取蓝牙 RSSI,再把等级通过串口回传给 STM32F103VET6。Android 的BluetoothDevice接口可以直接拿到extraRSSI;iOS 对经典蓝牙 SPP 的 RSSI 支持有限,所以这一功能通常只在 Android App 端做。常见信号衰减模型是:
d = 10^((A - RSSI) / (10 * n))
其中 A 是 1 米处平均 RSSI,一般在 -55dBm~-62dBm;n 是环境衰减指数,空旷场地取 2.0,室内有遮挡取 2.5~3.5。这个公式算出的距离波动很大,手表上不适合显示精确米数,更适合映射成“近/中/远”三档:
- RSSI 大于 -50dBm:靠近,点亮屏幕。
- RSSI 在 -65dBm 到 -50dBm:正常距离,保持显示。
- RSSI 小于 -65dBm:认为离开,停显并进入低功耗。
实际调试时,可以在 App 里持续打印 RSSI 原始值,再调阈值。注意 RSSI 受蓝牙模块天线和外壳影响,每版硬件都要重新标定,不要照抄网上的阈值。
5. HC-05 连不上、Altium 异形封装和网盘源码落地的三个技巧
5.1 HC-05 蓝牙模块连接不上的排查顺序
如果手机搜不到设备,先看模块 LED:慢闪表示未连接,快闪通常是进入 AT 模式或正在配对;常亮表示已连接。搜得到但连不上,优先查AT+PSWD和AT+CMODE。连上但发数据没反应,检查 RX/TX 是否交叉、模块波特率是否与固件一致。用 USB-TTL 单独测模块之前,把模块的 KEY 引脚悬空或接 GND,确保它不在 AT 模式。
5.2 Altium Designer 异形封装与拼板
手表外壳多是圆形,PCB 板框不能是默认矩形。在 Altium Designer 里用 Design > Board Shape 重新绘制,画弧线时按 Shift+Space 切换走线模式。异形封装(比如焊盘非矩形)可以在 PCB Library 里放置圆弧和自定义焊盘,记得在 Rules 里把 Solder Mask Expansion 设置到封装属性一致。拼板时用 V-cut 适合直线边,圆弧边用邮票孔拼板,发布 Gerber 前把拼板文件单独输出。
5.3 让网盘下载的源码在本地跑起来
从网盘下载的 ALTIUM 工程和固件源码经常遇到三个问题:原理图库文件缺失、编译器版本不一致、端口配置对不上自己板子。打开 Altium 工程后如果卡顿或经常崩溃,先清理缓存:在 Preferences 里找到缓存管理并清除,或者直接删掉用户目录下 Altium 的 Cache 文件夹,注意删除前备份工程。固件源码如果是用老版本标准外设库写的,先确认system_stm32f10x.c里的时钟配置和你的晶振一致,常见的 8MHz 外部晶振对应HSE_VALUE=8000000。最后检查芯片型号和引脚映射,再做第一次下载。
本文还有配套的精品资源,点击获取