1. 项目概述
作为一名嵌入式开发工程师,我最近使用CH32V307单片机完成了一个智能门锁的原型开发。这个项目涉及串口通信、舵机控制和TFT屏幕显示三个核心模块的整合。本文将详细记录这三个模块的实现过程,特别适合刚接触CH32系列单片机或嵌入式开发的新手参考。
在开发过程中,我发现很多教程要么过于理论化,要么缺乏关键细节。因此,我会重点分享实际开发中遇到的坑和解决方案,包括:
- 串口3的特殊配置要点
- 如何用PWM精准控制SG90舵机
- TFT屏幕的驱动移植技巧
- 汉字和图片显示的实现方法
2. 串口3的移植与使用
2.1 硬件连接与引脚配置
CH32V307的串口3与串口1/2有些不同,需要特别注意总线挂载问题。通过查阅数据手册,我确认串口3挂载在APB1总线上,使用PB10(TX)和PB11(RX)引脚。
重要提示:CH32系列不同型号的引脚分配可能不同,务必查阅对应型号的参考手册。我使用的是CH32V307VCT6型号。
2.2 初始化代码详解
以下是完整的串口3初始化代码,关键点已添加注释:
c复制void Usart3_Init()
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 特别注意:串口3挂载在APB1总线
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// PB10(TX)和PB11(RX)配置为复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 串口参数配置
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx|USART_Mode_Rx;
USART_Init(USART3, &USART_InitStructure);
// 使能接收中断
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);
// 中断优先级配置
NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART3, ENABLE);
}
2.3 中断处理实现
串口3的中断服务函数需要单独实现,这里我添加了一个简单的回显功能:
c复制void USART3_IRQHandler(void)
{
u8 temp;
if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
{
temp = USART_ReceiveData(USART3);
USART_SendData(USART3, temp+1); // 回显接收到的字符+1
}
USART_ClearITPendingBit(USART3, USART_IT_RXNE);
}
2.4 快速中断配置技巧
CH32支持快速中断,可以显著降低中断延迟。在包含头文件后添加以下声明:
c复制void TIM3_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
2.5 测试与调试
主函数中初始化串口3并发送测试数据:
c复制int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SystemCoreClockUpdate();
Delay_Init();
Usart3_Init();
while(1)
{
USART_SendData(USART3, 'a');
Delay_Ms(1000);
}
}
测试时使用USB-TTL模块连接PB10和PB11,注意共地。正常情况应该:
- 每秒收到字符'a'
- 发送'1'会收到'2'(因为中断函数做了+1处理)
3. SG90舵机控制实现
3.1 舵机工作原理
SG90舵机通过PWM信号控制,关键参数:
- 周期:20ms (50Hz)
- 脉宽范围:0.5ms-2.5ms
- 0.5ms → 0度
- 1.5ms → 90度
- 2.5ms → 180度
3.2 GPIO模拟PWM
最简单的控制方式是用GPIO模拟PWM,适合快速验证:
c复制while(1)
{
GPIO_ResetBits(GPIOC, GPIO_Pin_2); // 低电平
Delay_Ms(20); // 周期20ms
GPIO_SetBits(GPIOC, GPIO_Pin_2); // 高电平
Delay_Us(1500); // 1.5ms脉宽(90度)
}
这种方法简单但精度不高,适合临时测试舵机是否正常工作。
3.3 定时器生成精确PWM
使用TIM2的PWM模式可以生成更精确的控制信号:
3.3.1 定时器初始化
c复制#define PWM_PERIOD 20000 // 20ms周期(20000us)
#define PWM_MIN 500 // 0.5ms最小脉宽
#define PWM_MAX 2500 // 2.5ms最大脉宽
void TIM2_PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 定时器基础配置
TIM_TimeBaseStructure.TIM_Period = PWM_PERIOD - 1;
TIM_TimeBaseStructure.TIM_Prescaler = 96 - 1; // 96MHz/96=1MHz(1us)
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// PWM模式配置
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = PWM_MIN;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
// 通道1配置(PA0)
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM2, ENABLE);
TIM_Cmd(TIM2, ENABLE);
}
3.3.2 GPIO初始化
c复制void GPIO_Pwm_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
3.4 舵机控制函数封装
为方便使用,我将舵机控制封装成函数:
c复制void lock(unsigned char mode) // 0-上锁 1-开锁
{
if(mode == 0) // 0度位置(上锁)
{
TIM_SetCompare1(TIM2, 500); // 0.5ms脉宽
}
else if(mode == 1) // 90度位置(开锁)
{
TIM_SetCompare1(TIM2, 1500); // 1.5ms脉宽
}
}
4. TFT屏幕移植与显示
4.1 硬件连接
我使用的是1.44寸128x128 SPI TFT屏幕,接线如下:
| 屏幕引脚 | 单片机引脚 | 备注 |
|---|---|---|
| VCC | 3.3V | 电源 |
| GND | GND | 地线 |
| SCL | PB3 | SPI时钟 |
| SDA | PB5 | SPI数据 |
| RES | 3.3V | 复位(常高) |
| DC | PD3 | 数据/命令选择 |
| CS | PD4 | 片选 |
| BLK | 3.3V | 背光 |
注意:确保电源稳定,屏幕工作时电流可能达到100mA以上。
4.2 驱动移植步骤
- 从官方例程中复制SPI_LCD相关代码
- 创建Driver文件夹存放LCD驱动
- 添加lcd.c和lcd.h文件
- 复制字库文件font_ascii_16x8.h
- 配置工程包含路径
4.3 基本显示功能
初始化后可以进行基本绘图:
c复制LCD_Init();
LCD_Fill(0, 0, 127, 127, WHITE); // 清屏
LCD_Fill(10, 10, 20, 20, RED); // 绘制红色方块
4.4 进度条动画实现
通过逐行绘制实现进度条效果:
c复制unsigned char i = 0;
while(i < 128)
{
LCD_DrawLine(i, 100, i, 127, RED);
i++;
Delay_Ms(20);
}
4.5 汉字显示方法
使用PCtoLCD2002软件生成字模:
- 设置字模参数:阴码、逐行、顺向、16x16
- 输入要显示的汉字
- 生成字模并复制到font_ascii_16x8.h
显示示例:
c复制LCD_Show_Chinese(0, 0, "门锁状态:上锁", RED, WHITE, 16, 0);
LCD_Show_Chinese(0, 30, "输入密码", RED, WHITE, 16, 0);
4.6 图片显示实现
使用Img2Lcd工具将图片转换为数组:
- 设置输出格式:C语言数组、16位真彩色
- 调整图片大小为128x128
- 保存为pic.h文件
显示图片:
c复制#include "pic.h"
LCD_ShowPicture(0, 0, 128, 128, gImage_1);
5. 系统整合与优化
5.1 主程序流程设计
c复制int main(void)
{
// 初始化各模块
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SystemCoreClockUpdate();
Delay_Init();
Usart3_Init();
TIM2_PWM_Init();
GPIO_Pwm_Init();
LCD_Init();
// 开机动画
LCD_Fill(0, 0, 127, 127, WHITE);
LCD_ShowPicture(0, 0, 128, 128, gImage_1);
// 进度条动画
unsigned char i = 0;
while(i < 128)
{
LCD_DrawLine(i, 0, i, 10, RED);
i++;
Delay_Ms(20);
}
// 主界面
LCD_ShowPicture(0, 0, 128, 128, gImage_2);
LCD_Show_Chinese(0, 0, "门锁状态:上锁", RED, WHITE, 16, 0);
LCD_Show_Chinese(0, 30, "输入密码", RED, WHITE, 16, 0);
// 主循环
while(1)
{
// 处理串口命令等
}
}
5.2 常见问题解决
-
屏幕无显示:
- 检查背光是否点亮
- 测量电源电压是否稳定
- 确认SPI时钟极性设置正确
-
舵机不转动:
- 检查PWM信号是否输出
- 确认脉宽在有效范围内
- 确保供电充足(单独供电测试)
-
串口通信失败:
- 确认TX/RX线序正确
- 检查波特率设置一致
- 测量信号波形是否正常
5.3 性能优化建议
- 使用DMA传输SPI数据提高刷新率
- 添加双缓冲机制减少屏幕闪烁
- 对频繁调用的显示函数进行优化
- 使用硬件定时器生成更精确的PWM
在实际开发中,我遇到最棘手的问题是屏幕驱动移植时的SPI时序问题。通过逻辑分析仪捕获波形,最终发现是时钟极性设置错误。这个经验告诉我,硬件调试时仪器设备的重要性不亚于代码能力。