1. 合泰BS67F350微控制器开发实战指南
合泰BS67F350是一款集成了丰富外设功能的8位微控制器,特别适合需要触摸按键、串口通信和LCD显示的应用场景。作为一名嵌入式开发工程师,我在多个项目中都使用过这款芯片,今天就来分享一个完整的参考实现方案。
这个参考程序包含了三个核心功能模块:触摸按键检测、串口通信和LCD驱动。这三个功能在实际项目中非常常见,比如智能家居控制面板、工业控制界面等场景都会用到。程序采用模块化设计,每个功能都有清晰的接口定义,方便移植和扩展。
2. 硬件架构与初始化配置
2.1 系统时钟配置
BS67F350支持多种时钟源选择,在这个参考实现中我们使用内部RC振荡器作为系统时钟源。内部RC振荡器的优点是无需外部晶振,节省BOM成本和PCB空间。
c复制void System_Init(void) {
// 设置系统时钟
CLK->CKCON = 0x00; // 设置时钟分频
CLK->CLKSEL = 0x01; // 选择内部RC振荡器
CLK->PLLCON = 0x00; // 关闭PLL
}
选择内部RC振荡器时需要注意,它的精度相比外部晶振要低一些(通常±2%),对于串口通信等对时钟精度要求较高的外设,建议在代码中加入自动波特率校准功能。
2.2 GPIO初始化
GPIO的配置需要根据具体硬件连接来设置。我们的参考设计中:
- P0口用于LCD控制(RS、RW、EN)和数据线(D4-D7)
- P1口用于触摸按键输入(CH0-CH3)
- P2口用于串口通信(TX、RX)
c复制void GPIO_Init(void) {
// 设置触摸通道引脚为输入
P1M0 &= ~(1<<TOUCH_CH0); P1M1 |= (1<<TOUCH_CH0); // CH0
P1M0 &= ~(1<<TOUCH_CH1); P1M1 |= (1<<TOUCH_CH1); // CH1
P1M0 &= ~(1<<TOUCH_CH2); P1M1 |= (1<<TOUCH_CH2); // CH2
P1M0 &= ~(1<<TOUCH_CH3); P1M1 |= (1<<TOUCH_CH3); // CH3
// 设置LCD控制引脚为输出
P0M0 |= (1<<0) | (1<<1) | (1<<2); P0M1 &= ~((1<<0)|(1<<1)|(1<<2)); // RS, RW, EN
P0M0 |= (1<<4) | (1<<5) | (1<<6) | (1<<7); P0M1 &= ~((1<<4)|(1<<5)|(1<<6)|(1<<7)); // D4-D7
// 设置串口引脚
P2M0 |= (1<<0) | (1<<1); P2M1 &= ~((1<<0)|(1<<1)); // TX, RX
}
注意:BS67F350的GPIO模式寄存器分为M0和M1两个,需要组合配置才能设置正确的输入/输出模式。具体配置方式请参考芯片数据手册。
3. 触摸按键检测实现
3.1 触摸模块初始化
BS67F350内置了电容式触摸检测模块,支持最多8个触摸通道。我们的参考程序使用了4个通道(CH0-CH3)。
c复制void Touch_Init(void) {
// 使能触摸模块时钟
CLK->APBCLK |= CLK_APBCLK_TOUCH_EN;
// 配置触摸控制寄存器
TOUCH->CTRL = TOUCH_CTRL_ENABLE | TOUCH_CTRL_CLK_DIV_16;
// 配置触摸通道
TOUCH->CH0_CR = TOUCH_CH_CR_ENABLE | TOUCH_CH_CR_PRECHARGE_8 | TOUCH_CH_CR_SENSE_8;
TOUCH->CH1_CR = TOUCH_CH_CR_ENABLE | TOUCH_CH_CR_PRECHARGE_8 | TOUCH_CH_CR_SENSE_8;
TOUCH->CH2_CR = TOUCH_CH_CR_ENABLE | TOUCH_CH_CR_PRECHARGE_8 | TOUCH_CH_CR_SENSE_8;
TOUCH->CH3_CR = TOUCH_CH_CR_ENABLE | TOUCH_CH_CR_PRECHARGE_8 | TOUCH_CH_CR_SENSE_8;
// 设置初始阈值
for(int i = 0; i < 4; i++) {
touchThreshold[i] = 100;
}
}
触摸模块的几个关键参数:
- CLK_DIV_16:设置触摸检测时钟分频,影响检测灵敏度和功耗
- PRECHARGE_8:预充电时间设置,影响检测稳定性
- SENSE_8:感应时间设置,影响检测灵敏度
3.2 触摸扫描与检测
触摸检测采用轮询方式,在主循环中定期调用Touch_Scan()函数进行检测。
c复制void Touch_Scan(void) {
static uint8_t prescaler = 0;
if(++prescaler < 10) return; // 每10次循环扫描一次
prescaler = 0;
// 读取触摸值
touchValues[0] = TOUCH->CH0_DATA;
touchValues[1] = TOUCH->CH1_DATA;
touchValues[2] = TOUCH->CH2_DATA;
touchValues[3] = TOUCH->CH3_DATA;
// 检测触摸按下
for(int i = 0; i < 4; i++) {
if(touchValues[i] < touchThreshold[i]) {
if(!keyPressed[i]) {
keyPressed[i] = 1;
}
} else {
keyPressed[i] = 0;
}
}
}
触摸检测的关键点:
- 采用10次循环扫描一次的频率,平衡响应速度和CPU占用率
- 当检测到的触摸值低于阈值时判定为触摸按下
- 使用keyPressed数组记录按键状态,避免重复触发
实际项目中,建议增加去抖处理:连续多次检测到触摸才判定为有效触摸,避免误触发。
4. 串口通信实现
4.1 UART初始化
BS67F350的UART模块支持标准异步串行通信,我们的参考程序配置为9600波特率、8位数据、无校验位、1位停止位。
c复制void UART_Init(uint32_t baudrate) {
// 计算波特率分频
uint32_t div = (SystemCoreClock / baudrate) - 1;
// 使能UART模块时钟
CLK->APBCLK |= CLK_APBCLK_UART_EN;
// 配置UART
UART->BAUD = div;
UART->CTRL = UART_CTRL_TXEN | UART_CTRL_RXEN | UART_CTRL_UARTEN;
// 使能UART中断
UART->IE = UART_IE_RXIE;
NVIC_EnableIRQ(UART_IRQn);
}
波特率计算公式:
code复制波特率分频值 = (系统时钟频率 / 目标波特率) - 1
例如系统时钟8MHz,目标波特率9600:
code复制分频值 = (8000000 / 9600) - 1 = 832.33 ≈ 832
4.2 数据发送与接收
数据发送采用查询方式,接收采用中断方式,提高系统响应效率。
c复制// UART发送字节
void UART_SendByte(uint8_t byte) {
while(!(UART->STAT & UART_STAT_TXEMPTY)); // 等待发送缓冲区空
UART->TXBUF = byte;
}
// UART发送字符串
void UART_SendString(char *str) {
while(*str) {
UART_SendByte(*str++);
}
}
// UART中断服务函数
void UART_IRQHandler(void) {
if(UART->IF & UART_IF_RXIF) {
// 接收中断
uint8_t data = UART->RXBUF;
if(uartRxIndex < sizeof(uartRxBuffer) - 1) {
uartRxBuffer[uartRxIndex++] = data;
// 检测回车换行
if(data == '\r' || data == '\n') {
uartRxFlag = 1;
}
}
UART->IF = UART_IF_RXIF; // 清除中断标志
}
}
串口通信的几个实用技巧:
- 发送字符串时,使用指针遍历直到遇到字符串结束符'\0'
- 接收缓冲区设置合理的长度限制,防止溢出
- 以回车换行作为帧结束标志,简化协议处理
- 在主循环中检查uartRxFlag处理完整帧数据
5. LCD驱动实现
5.1 LCD1602初始化
参考程序使用4位数据总线方式驱动标准的1602字符型LCD,相比8位方式可以节省4个IO口。
c复制void LCD_Init(void) {
Delay_ms(50); // 等待LCD上电稳定
// 4位模式初始化
LCD_WriteCommand(0x33);
Delay_ms(5);
LCD_WriteCommand(0x32);
Delay_ms(5);
LCD_WriteCommand(0x28); // 4位总线,2行显示,5x8点阵
Delay_ms(5);
LCD_WriteCommand(0x0C); // 开显示,关光标
Delay_ms(5);
LCD_WriteCommand(0x06); // 地址递增,文字不动
Delay_ms(5);
LCD_WriteCommand(0x01); // 清屏
Delay_ms(5);
}
LCD初始化的关键步骤:
- 上电后等待足够时间让LCD完成内部初始化(通常15-50ms)
- 发送特殊的初始化序列切换到4位模式
- 设置显示模式、光标模式等参数
- 最后清屏完成初始化
5.2 LCD基本操作
4位模式下,每个字节需要分两次发送,先高4位后低4位。
c复制void LCD_WriteCommand(uint8_t cmd) {
LCD_RS = 0; // 命令模式
LCD_RW = 0; // 写操作
// 高4位
LCD_D4 = (cmd >> 4) & 0x01;
LCD_D5 = (cmd >> 5) & 0x01;
LCD_D6 = (cmd >> 6) & 0x01;
LCD_D7 = (cmd >> 7) & 0x01;
LCD_EN = 1;
Delay_ms(1);
LCD_EN = 0;
Delay_ms(1);
// 低4位
LCD_D4 = cmd & 0x01;
LCD_D5 = (cmd >> 1) & 0x01;
LCD_D6 = (cmd >> 2) & 0x01;
LCD_D7 = (cmd >> 3) & 0x01;
LCD_EN = 1;
Delay_ms(1);
LCD_EN = 0;
Delay_ms(1);
}
void LCD_WriteData(uint8_t data) {
LCD_RS = 1; // 数据模式
LCD_RW = 0; // 写操作
// 高4位
LCD_D4 = (data >> 4) & 0x01;
LCD_D5 = (data >> 5) & 0x01;
LCD_D6 = (data >> 6) & 0x01;
LCD_D7 = (data >> 7) & 0x01;
LCD_EN = 1;
Delay_ms(1);
LCD_EN = 0;
Delay_ms(1);
// 低4位
LCD_D4 = data & 0x01;
LCD_D5 = (data >> 1) & 0x01;
LCD_D6 = (data >> 2) & 0x01;
LCD_D7 = (data >> 3) & 0x01;
LCD_EN = 1;
Delay_ms(1);
LCD_EN = 0;
Delay_ms(1);
}
LCD操作注意事项:
- 每次操作后需要足够的延时确保LCD完成内部处理
- RS信号区分命令和数据模式
- EN信号需要产生完整的脉冲(高-低跳变)才能锁存数据
- 4位模式下,数据线D0-D3不需要连接
6. 系统集成与主程序流程
6.1 主程序结构
主程序采用经典的超级循环结构,初始化后进入无限循环,轮流处理各个功能模块。
c复制void main(void) {
System_Init();
GPIO_Init();
Touch_Init();
UART_Init(UART_BAUD);
LCD_Init();
LCD_Clear();
LCD_SetCursor(0, 0);
LCD_PrintString("Touch & UART Demo");
LCD_SetCursor(1, 0);
LCD_PrintString("Press any key...");
while(1) {
// 触摸扫描
Touch_Scan();
// 处理触摸事件
for(int i = 0; i < 4; i++) {
if(keyPressed[i]) {
char msg[20];
sprintf(msg, "Touch CH%d Pressed", i);
LCD_SetCursor(1, 0);
LCD_PrintString(msg);
// 通过串口发送消息
UART_SendString("Touch event: Channel ");
UART_SendByte('0' + i);
UART_SendString("\r\n");
keyPressed[i] = 0;
}
}
// 处理串口接收
if(uartRxFlag) {
uartRxBuffer[uartRxIndex] = '\0'; // 字符串结束符
LCD_SetCursor(1, 0);
LCD_PrintString(" "); // 清除行
LCD_SetCursor(1, 0);
LCD_PrintString((char*)uartRxBuffer);
// 回显接收到的数据
UART_SendString("Echo: ");
UART_SendString((char*)uartRxBuffer);
UART_SendString("\r\n");
uartRxIndex = 0;
uartRxFlag = 0;
}
Delay_ms(50);
}
}
主循环的关键点:
- 触摸扫描频率通过Delay_ms(50)控制,约20Hz的扫描频率
- 触摸事件和串口接收分开处理,互不干扰
- LCD显示更新采用"先清行再写入"的方式,避免残留字符
- 串口接收到的数据会同时显示在LCD和回传到串口
6.2 延时函数实现
由于BS67F350没有内置SysTick定时器,我们使用简单的软件延时实现。
c复制void Delay_ms(uint16_t ms) {
uint16_t i, j;
for(i = 0; i < ms; i++) {
for(j = 0; j < 1000; j++) {
__asm__("nop");
}
}
}
软件延时的注意事项:
- 延时精度受编译器优化和系统时钟影响
- 在延时期间CPU处于忙等待状态,无法执行其他任务
- 实际项目中建议使用定时器中断实现更精确的延时
7. 功能扩展与高级应用
7.1 触摸校准功能
为了提高触摸检测的准确性,可以增加触摸校准功能,自动计算每个通道的最佳阈值。
c复制void Touch_Calibrate(void) {
char buffer[20];
LCD_Clear();
LCD_SetCursor(0, 0);
LCD_PrintString("Calibrate Ch0");
LCD_SetCursor(1, 0);
LCD_PrintString("Touch and hold...");
uint32_t sum = 0;
uint8_t count = 0;
while(count < 10) {
Touch_Scan();
if(keyPressed[0]) {
sum += touchValues[0];
count++;
Delay_ms(200);
}
}
touchThreshold[0] = (sum / 10) - 20; // 设置阈值为平均值的80%
// 显示结果
LCD_Clear();
LCD_SetCursor(0, 0);
LCD_PrintString("Calibration Done");
sprintf(buffer, "Thresh: %d", touchThreshold[0]);
LCD_SetCursor(1, 0);
LCD_PrintString(buffer);
Delay_ms(2000);
}
校准过程:
- 提示用户触摸并保持按住指定通道
- 采集10次触摸值并计算平均值
- 将阈值设置为平均值的80%(减去20%作为容差)
- 显示校准结果
7.2 LCD自定义字符
1602 LCD支持8个5x8点阵的自定义字符,可以用来显示简单的图标或特殊符号。
c复制// 自定义字符
const uint8_t customChar[8][8] = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F}, // 电池图标
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x1F},
{0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x1F, 0x1F},
{0x00, 0x00, 0x00, 0x00, 0x1F, 0x1F, 0x1F, 0x1F},
{0x00, 0x00, 0x00, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F},
{0x00, 0x00, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F},
{0x00, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F},
{0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F}
};
void LCD_CreateCustomChar(uint8_t location, uint8_t charmap[]) {
location &= 0x07; // 只能使用0-7的位置
LCD_WriteCommand(0x40 | (location << 3));
for(int i = 0; i < 8; i++) {
LCD_WriteData(charmap[i]);
}
}
void LCD_DisplayBatteryLevel(uint8_t level) {
LCD_CreateCustomChar(0, customChar[level]);
LCD_SetCursor(0, 15);
LCD_WriteData(0); // 显示自定义字符
}
自定义字符使用步骤:
- 定义字符点阵数据(8字节表示5x8点阵)
- 使用LCD_CreateCustomChar将字符写入CGRAM
- 通过LCD_WriteData(0-7)显示自定义字符
7.3 串口协议解析
对于更复杂的应用,可以扩展串口通信协议,实现命令解析功能。
c复制typedef enum {
CMD_NONE,
CMD_LED_ON,
CMD_LED_OFF,
CMD_GET_TEMP,
CMD_SET_THRESHOLD
} CommandType;
CommandType ParseUARTCommand(char *cmd) {
if(strcmp(cmd, "LED ON") == 0) return CMD_LED_ON;
if(strcmp(cmd, "LED OFF") == 0) return CMD_LED_OFF;
if(strncmp(cmd, "GET TEMP", 8) == 0) return CMD_GET_TEMP;
if(strncmp(cmd, "SET THRESH", 10) == 0) return CMD_SET_THRESHOLD;
return CMD_NONE;
}
void ProcessCommand(CommandType cmd, char *params) {
switch(cmd) {
case CMD_LED_ON:
// 点亮LED
break;
case CMD_LED_OFF:
// 熄灭LED
break;
case CMD_GET_TEMP:
// 读取温度并通过串口发送
break;
case CMD_SET_THRESHOLD:
// 解析并设置阈值
sscanf(params, "%d", &touchThreshold[0]);
break;
default:
UART_SendString("Unknown command\r\n");
break;
}
}
协议解析实现要点:
- 定义命令枚举类型,方便代码维护
- 使用字符串比较函数识别不同命令
- 对于带参数的命令,使用sscanf解析参数
- 每个命令对应特定的处理函数
8. 常见问题与解决方案
8.1 触摸检测不灵敏
可能原因:
- 触摸阈值设置过高
- 触摸面板材料或厚度不合适
- 环境电磁干扰
- 电源噪声
解决方案:
- 重新校准触摸阈值,使用Touch_Calibrate函数
- 检查触摸面板材质,建议使用PCB铜箔或导电膜
- 增加软件滤波算法,如多次采样取平均值
- 在触摸通道上增加0.1uF滤波电容
- 确保电源稳定,必要时增加LC滤波
8.2 LCD显示乱码
可能原因:
- 初始化时序不正确
- 对比度调节不合适
- 数据线接触不良
- 电源电压不稳定
解决方案:
- 检查初始化序列,确保每个命令后有足够延时
- 调节VO引脚上的电位器,找到最佳对比度
- 用万用表检查各数据线连接
- 确保LCD供电电压在4.5-5.5V范围内
- 检查背光电路,确保A/K引脚正确连接
8.3 串口通信失败
可能原因:
- 波特率不匹配
- TX/RX线接反
- 地线未连接
- 信号电平不兼容
解决方案:
- 检查双方波特率设置,确保一致
- 确认TX接RX,RX接TX的交叉连接方式
- 必须连接GND作为参考地
- BS67F350是TTL电平,如需连接RS232设备需要电平转换
- 使用逻辑分析仪或示波器观察实际通信波形
8.4 程序运行不稳定
可能原因:
- 堆栈溢出
- 中断冲突
- 看门狗未处理
- 电源波动
解决方案:
- 在IDE中增加堆栈大小设置
- 检查中断优先级,确保关键中断优先
- 定期喂狗或禁用看门狗
- 增加电源滤波电容,确保供电稳定
- 检查复位电路,确保复位信号干净
9. 项目优化建议
9.1 功耗优化
对于电池供电的应用,可以采取以下措施降低功耗:
- 使用睡眠模式:在空闲时进入低功耗模式,通过触摸或串口唤醒
- 动态时钟调节:根据任务需求动态切换系统时钟频率
- 外设电源管理:不使用时关闭触摸、LCD背光等外设电源
- 降低扫描频率:适当降低触摸和LCD刷新频率
9.2 可靠性增强
提高系统可靠性的建议:
- 增加看门狗定时器,防止程序跑飞
- 实现软件CRC校验,确保数据传输正确性
- 增加EEPROM存储配置参数,掉电不丢失
- 实现固件升级功能,通过串口更新程序
9.3 功能扩展
可以考虑的扩展功能:
- 增加更多的触摸通道,支持更复杂的控制面板
- 实现触摸手势识别,如滑动、长按等
- 连接温湿度传感器,实现环境监测
- 增加无线通信模块,如蓝牙或Wi-Fi
- 设计更复杂的用户界面,支持多级菜单
10. 开发工具与调试技巧
10.1 开发环境配置
合泰官方提供HT-IDE3000集成开发环境,支持BS67F350的全功能开发:
- 下载安装HT-IDE3000
- 创建新项目,选择BS67F350器件
- 配置编译器选项和头文件路径
- 连接仿真器(如HT-ICE)进行在线调试
- 使用内置的Flash编程工具烧录程序
10.2 调试技巧
- 利用串口打印调试信息,实时监控程序状态
- 使用GPIO引脚作为调试信号,用示波器观察时序
- 在关键代码段前后切换调试引脚电平,测量执行时间
- 使用变量监视功能,观察关键变量变化
- 设置断点,单步执行分析程序流程
10.3 性能优化
- 使用-O2优化等级编译,提高代码执行效率
- 将频繁调用的函数声明为inline
- 使用查表法替代复杂计算
- 优化中断服务函数,减少执行时间
- 合理使用寄存器变量提高访问速度
