1. 项目概述
作为一名嵌入式开发者,我深知模块化设计在项目开发中的重要性。今天要分享的是基于CH32单片机开发智能门锁的第三讲内容,重点讲解如何将代码进行模块化处理,以及矩阵键盘的移植方法。这个教程特别适合刚接触嵌入式开发的朋友,我会尽量用通俗易懂的方式讲解每个步骤。
在之前的开发中,我们可能习惯把所有代码都写在main.c里,但随着功能增加,代码会变得难以维护。模块化设计就像把工具箱里的工具分类存放,用的时候直接拿取,既整洁又高效。本次教程会手把手教你如何将定时器、PWM、LCD显示等功能模块化,并实现矩阵键盘的输入功能。
2. 模块化设计原理与实现
2.1 为什么需要模块化
在嵌入式开发中,模块化设计有三大优势:
- 代码复用性:封装好的模块可以直接移植到其他项目
- 可维护性:功能独立,修改时不会影响其他部分
- 可读性:代码结构清晰,便于团队协作
想象一下,如果你的衣柜里所有衣服都堆在一起,找一件T恤有多困难?模块化就是给代码"分类收纳"的过程。
2.2 定时器模块封装
2.2.1 创建定时器模块
首先我们创建timer.c和timer.h两个文件:
- 在工程上右键 -> New -> Header File/Source File
- 分别命名为timer.h和timer.c
- 将之前的定时器相关代码迁移过来
注意:如果提示找不到文件,记得按照上一讲的方法添加Driver文件夹到包含路径
2.2.2 定时器初始化函数
下面是TIM3初始化的核心代码解析:
c复制void Tim3_Init(u16 arr, u16 psc) {
// 1. 定义结构体
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
NVIC_InitTypeDef NVIC_InitStructure;
// 2. 使能TIM3时钟(注意TIM3挂在APB1总线上)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// 3. 配置定时器参数
TIM_TimeBaseInitStruct.TIM_Period = arr; // 自动重装载值
TIM_TimeBaseInitStruct.TIM_Prescaler = psc; // 分频系数
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
// 4. 使能定时器中断
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
// 5. 配置NVIC
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 6. 启动定时器
TIM_Cmd(TIM3, ENABLE);
}
参数说明:
arr:自动重装载值,决定定时周期psc:预分频值,与主频共同决定计数频率
计算公式:
定时时间 = (arr + 1) * (psc + 1) / 系统时钟频率
例如系统时钟72MHz,arr=7199,psc=9999:
定时时间 = (7199+1)*(9999+1)/72000000 = 1秒
2.2.3 PWM生成与舵机控制
舵机控制需要精确的PWM信号,我们使用TIM2生成PWM:
c复制#define PWM_PERIOD 20000 // 20ms周期(标准舵机信号周期)
#define PWM_MIN 500 // 0.5ms脉宽(0度位置)
#define PWM_MAX 2500 // 2.5ms脉宽(180度位置)
void TIM2_PWM_Init(void) {
// 1. 使能TIM2时钟(APB1总线)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
// 2. 基础定时器配置
TIM_TimeBaseStructure.TIM_Period = PWM_PERIOD - 1;
TIM_TimeBaseStructure.TIM_Prescaler = 96 - 1; // 96MHz/96=1MHz(1us分辨率)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 3. PWM通道配置(通道1)
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_Pulse = PWM_MIN; // 初始占空比
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
// 4. 使能预装载
TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM2, ENABLE);
// 5. 初始化GPIO(PA0作为TIM2_CH1输出)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 6. 启动定时器
TIM_Cmd(TIM2, ENABLE);
}
舵机控制函数:
c复制void lock(unsigned char mode) {
if(mode == 0) { // 上锁
TIM_SetCompare1(TIM2, 500); // 0.5ms脉宽
} else { // 开锁
TIM_SetCompare1(TIM2, 1500); // 1.5ms脉宽
}
}
实操技巧:不同舵机的脉宽范围可能略有差异,建议先用示波器观察输出波形,微调PWM_MIN和PWM_MAX值
2.3 LCD显示优化
原生的LCD_ShowChinese函数一次只能显示一个汉字,我们封装一个可以显示字符串的函数:
c复制void LCD_Show_Chinese(u16 x, u16 y, u8 *s, u16 fc, u16 bc, u8 sizey, u8 mode) {
while(*s) {
LCD_ShowChinese(x, y, s, fc, bc, sizey, mode);
x += 16; // 每个汉字占16像素宽度
if(x > 127) { // 超出屏幕宽度换行
x = 0;
y += 16;
}
s += 2; // 跳过2字节(一个汉字占2字节)
}
}
这样调用时就可以直接显示整句中文:
c复制LCD_Show_Chinese(0, 0, "门锁状态:上锁", RED, WHITE, 16, 0);
3. 矩阵键盘的实现
3.1 矩阵键盘工作原理
4x4矩阵键盘通过行列扫描方式检测按键,原理如下:
- 将4个行线设置为输出,4个列线设置为输入
- 依次将每行拉低,检测各列线状态
- 当某列检测到低电平时,结合当前行号即可确定按键位置
这种设计只用8个IO口就能实现16个按键检测,大大节省IO资源。
3.2 硬件连接
根据我的开发板,键盘连接如下:
- 行线:R4-PD11, R3-PD9, R2-PE15, R1-PE13
- 列线:C1-PE11, C2-PE9, C3-PE7, C4-PC5
注意:不同键盘引脚可能不同,使用前务必确认原理图
3.3 GPIO初始化
c复制void key_init() {
// 1. 使能各端口时钟(所有GPIO都挂在APB2总线)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE |
RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
// 2. 配置行线为推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_9; // PD11,PD9
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOD, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15 | GPIO_Pin_13; // PE15,PE13
GPIO_Init(GPIOE, &GPIO_InitStructure);
// 3. 配置列线为上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_9 | GPIO_Pin_7; // PE11,PE9,PE7
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(GPIOE, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; // PC5
GPIO_Init(GPIOC, &GPIO_InitStructure);
}
3.4 按键扫描函数
c复制u8 key_read() {
u8 temp = 0;
// 扫描第一行(R4)
GPIO_ResetBits(GPIOD, GPIO_Pin_11); // R4低电平
GPIO_SetBits(GPIOD, GPIO_Pin_9); // 其他行高电平
GPIO_SetBits(GPIOE, GPIO_Pin_15);
GPIO_SetBits(GPIOE, GPIO_Pin_13);
if(!GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_11)) temp = 4; // C1
if(!GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_9)) temp = 3; // C2
if(!GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_7)) temp = 2; // C3
if(!GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_5)) temp = 1; // C4
// 扫描第二行(R3)
GPIO_SetBits(GPIOD, GPIO_Pin_11);
GPIO_ResetBits(GPIOD, GPIO_Pin_9);
// ...省略相似代码...
return temp; // 返回按键编号(1-16)
}
避坑指南:实际使用时要添加去抖动处理,可以用延时或定时器扫描方式
4. 主函数集成
将各模块集成到主函数中:
c复制int main(void) {
// 1. 系统初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SystemCoreClockUpdate();
Delay_Init();
// 2. 外设初始化
TIM2_PWM_Init(); // PWM初始化
lock(1); // 初始状态上锁
LCD_Init(); // LCD初始化
// 3. 启动界面
LCD_Fill(0, 0, 127, 127, WHITE);
LCD_ShowPicture(0, 0, 128, 128, gImage_1);
// 4. 进度条动画
for(u8 i=0; i<128; i++) {
LCD_DrawLine(i, 0, i, 10, RED);
Delay_Ms(20);
}
// 5. 主界面显示
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);
// 6. 键盘初始化
key_init();
// 7. 主循环
while(1) {
u8 key = key_read();
if(key) {
LCD_ShowIntNum(0, 50, key, 2, RED, WHITE, 16);
Delay_Ms(200); // 简单去抖
}
}
}
5. 常见问题与解决方案
5.1 模块编译错误
问题:提示找不到头文件
解决:
- 检查头文件路径是否添加到工程设置
- 确认头文件guard宏定义正确(如#ifndef DRIVER_TIMER_H_)
5.2 按键响应不稳定
问题:按键有时检测不到或误触发
解决:
- 添加硬件去抖电路(RC滤波)
- 软件去抖:检测到按键后延时10-20ms再次确认
- 改为定时扫描(如每50ms扫描一次)
5.3 PWM控制不精确
问题:舵机转动角度不准确
解决:
- 用示波器测量实际输出波形
- 调整PWM_PERIOD确保周期为20ms
- 微调PWM_MIN和PWM_MAX值
5.4 显示乱码
问题:LCD显示中文不正常
解决:
- 确认字库编码格式正确
- 检查字符串指针操作(汉字占2字节)
- 确保显示位置不会超出屏幕范围
6. 项目优化建议
- 状态机设计:将门锁状态(待机、输入密码、验证中等)用状态机管理
- 密码存储:添加EEPROM存储密码功能
- 背光控制:LCD背光自动熄灭节省功耗
- 声音反馈:添加蜂鸣器提示音
- 低功耗模式:无操作时进入睡眠模式
通过这期教程,我们实现了智能门锁的模块化设计和矩阵键盘输入。模块化不仅使代码更整洁,也大大提高了开发效率。在实际项目中,建议从一开始就采用模块化思维,这会让你后期的开发工作事半功倍。