前言
(1)本系列是基于STM32的项目笔记,内容涵盖了STM32各种外设的使用,由浅入深。
(2)小编使用的单片机是STM32F105RCT6,项目笔记基于小编的实际项目,但是博客中的内容适用于各种单片机开发的同学学习和使用。
学习目标
本章有三个任务:
- 关于语言芯片外设电路的设计
- 语言芯片驱动的逻辑分析
- 驱动芯片程序的开发
- 语音芯片功能的测试
学习内容
语音芯片的主要作用是让主机有语音提示功能。 这个是本产品最主要的功能之一。我们选择的是广州唯创电子的一颗定制芯片。
1.硬件原理图纸的分析:
从上面的原理图很难看的出,WTN06的通讯的方式的。
但是可以看出,接单片机引脚的三个口,是单片机输出来驱动语音芯片,输出是PWM,PWM再进入功放电路,功放就是声音放大的作用,不加功放的话声音会小一点。
WTN06支持多种通讯方式,刚开始原理图是按照单线通讯的方式设计的,但和厂家沟通后,单线通讯不支持我们的功能,所有我们有修改成了2线串口方式。
PC1 – WTN6_DATA – CLK 时钟线
PC2 – WTN6_NC – DAT
PC3 – WTN6_BUSY – NC (PC3没用到,预留在这,我们只用到了PC1和PC2,CLK和DAT,他俩是什么,看下面的逻辑分析)
功放芯片:
PA0 — 8002_EN 功放控制脚 8002_EN写入高电平,将三极管导通,SH为低电平,功放工作
2.语言芯片驱动的逻辑分析
96H:1001 0110 先发送低位,再高位,0110 1001
96H发送完后,就播放96H地址对应的语音,在表里面可以找。
例如要发在家布防: 0000 0001,则DATA发送1000 0000,如下
50, 3,3, 3,3, 3,3, 3,3, 3,3, 3,3, 3,3, 3,3, 3,3, 3(多一个,是最后数据发送完了,要对CLK和DATA拉高,这里多一位相当于把这位提前初始化了,是0是1并不重要)
CLK-L 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1
DATA-L 1 0 0 0 0 0 0 0 0
①时钟脚CLK.
● 空闲:高电平
● 唤醒:5ms的低电平
● 输出数据:40-3.2ms周期的时钟 推荐高低电平300uS
● 结束:高电平
5ms 300us 300us…(16个高低电平,16个300us)最后持续高电平
②数据脚DATA
○ 空闲:高电平
○ 唤醒: 和CLK同步5ms的低电平
○ 数据传输:低位在前 数据1:高电平 数据0:低电平
CLK和DATA空闲的时候都是高电平。
数据的就和我们的语音对应
程序语音的排序:sentence句子
3.驱动芯片程序的开发
上面的代码我们用定时器矩阵来完成。如果没有定时器矩阵功能的话,这个功能如果做起来还是有很大的困难的。裸机去跑的话还是有很多办法去实现的,但是如果是放在系统(Freertos)里面,那做起来还是有难度的。
首先我们用定时器矩阵依次定时5ms + 16个300us,控制CLK 管脚
其次我们根据CLK的变化,来控制DATA的高低电平
步骤
①程序版本修改为V1.14 我们在V1.14版本上完成WTN6驱动的开发
②新建hal_wtn6.c和hal_wtn6.h文件。并加载到V1.14工程里
③相关IO的初始化
端口定义:
#include "hal_wTn6.h"
#include "stm32F10x.h"
#include "hal_timer.h"
//只用了PC1和PC2
#define WTN6_CLK_PORT GPIOC
#define WTN6_CLK_PIN GPIO_Pin_1
#define WTN6_DAT_PORT GPIOC
#define WTN6_DAT_PIN GPIO_Pin_2
#define SC8002_SH_PORT GPIOA
#define SC8002_SH_PIN GPIO_Pin_0
//时钟低电平,时钟高电平
#define WTN6_CLK_LOW GPIO_ResetBits(WTN6_CLK_PORT,WTN6_CLK_PIN)
#define WTN6_CLK_HIG GPIO_SetBits(WTN6_CLK_PORT,WTN6_CLK_PIN)
//数据高低电平
#define WTN6_DAT_LOW GPIO_ResetBits(WTN6_DAT_PORT,WTN6_DAT_PIN)
#define WTN6_DAT_HIG GPIO_SetBits(WTN6_DAT_PORT,WTN6_DAT_PIN)
//控制功放引脚
#define SC8002_SH_LOW GPIO_ResetBits(SC8002_SH_PORT,SC8002_SH_PIN)
#define SC8002_SH_HIG GPIO_SetBits(SC8002_SH_PORT,SC8002_SH_PIN)
GPIO的初始化:
初始化相关的GPIO口。配置CLK DAT 为高电平(空闲为高电平)
static void hal_Wtn6Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC , ENABLE); //注意时钟端口一定都要打开
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE);
GPIO_InitStructure.GPIO_Pin = WTN6_CLK_PIN | WTN6_DAT_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = SC8002_SH_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ;
GPIO_Init(SC8002_SH_PORT, &GPIO_InitStructure);
GPIO_SetBits(GPIOC,GPIO_Pin_1);
GPIO_SetBits(GPIOC,GPIO_Pin_2);
GPIO_SetBits(SC8002_SH_PORT,SC8002_SH_PIN);
}
④播放语音的初始化
在hal_timer.h文件中增加T_WTN6定时器矩阵,用来驱动WTN6语音播报。
typedef enum
{
T_LED, //LED定时器
T_WTN6,
T_SUM,
}TIMER_ID_TYPEDEF;
利用定时器矩阵依次定时5ms 300us 300us…300us(17个),并初始化相关参数。
定义三个变量:
unsigned short wtn6[] = {50,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}; //5ms 300us 300us…300us(17个),
unsigned char Wtn6Playflag;// CLK的序号
unsigned char Wtn6VolueNum;// 当前播放的语音
unsigned char Wtn6NextNum; // 下一个播放的语音
unsigned short wtn6[] = {50,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}; //50*100us=5ms;3*100us=300us
unsigned char Wtn6Playflag;// CLK的序号的计数个数
unsigned char Wtn6VolueNum;// 当前播放的语音
unsigned char Wtn6NextNum; // 下一个播放的语音
volatile unsigned short Wtn6Timer; //当前WTN计时次数
static void hal_Wtn6_PlayHandle(void);
void hal_wtn6(void)
{
hal_Wtn6Config();
WTN6_DAT_HIG;
WTN6_CLK_HIG;
Wtn6Playflag = 0;
Wtn6VolueNum = 0;
Wtn6NextNum = 0xff;
hal_CreatTimer(T_WTN6,hal_Wtn6_PlayHandle,2,T_STA_STOP);//100us
}
开始语音播报程序:
void hal_Wtn6_PlayVolue(unsigned char VolNum) //VolNum是我们程序头文件枚举中的值,例如,播放在家布防就是1
{
if(Wtn6Playflag == 0) //时序等于0,表示当前是空闲状态
{ //空闲 ,这次直接发
hal_CreatTimer(T_WTN6,hal_Wtn6_PlayHandle,2,T_STA_START); //启动定时器矩阵,时间周期为100us
WTN6_CLK_LOW;
WTN6_DAT_LOW; //这两个引脚拉低,去唤醒芯片
Wtn6VolueNum = VolNum; //把当前要播放的语音复制给Wtn6VolueNum
Wtn6Playflag = 0; //清零
Wtn6Timer = wtn6[0]; //用来计数,50 3 3 3 ……,每进一次定时器矩阵处理函数,Wtn6Timer会减减
Wtn6NextNum = 0xff; //清零,这里的清零是初始化为定值0xff
}
else //时序不等于0 ,则表示正在发送时序
{ //忙碌,则保存为Next值,下一次发
Wtn6NextNum = VolNum;
}
}
语音播报处理函数程序:
/********
*CLK:
*开始时,CLK是50ms高电平,后面数组的第1,3,5,7,9……15都是低电平,WTN6_CLK_LOW,2,4,6,*8……16都是高电平,WTN6_CLK_HIG
*DATA:VolueNum & 0x01,每次只判断VolueNum(0000 0001)(VolueNum = Wtn6VolueNum)的最低位(因为DATA每次都是从最低位开始发),每次判断完后,将VolueNum右移一位,VolueNum >>= 1;每次判断最低位是1还是0,1则发高电平,0则发低电平
***************/
static void hal_Wtn6_PlayHandle(void)
{
static unsigned char VolueNum;
Wtn6Timer --;
if(Wtn6Timer == 0)
{
Wtn6Playflag ++; //对数组的元素,一个一个位来;比如,数组第一个数是50,则50--,到0的时候,Wtn6Playflag ++,后面Wtn6Timer= wtn6[Wtn6Playflag],即要3--直到0,才到下一次
Wtn6Timer= wtn6[Wtn6Playflag];
switch(Wtn6Playflag)
{
case 1:
VolueNum = Wtn6VolueNum;
WTN6_CLK_LOW;
if(VolueNum & 0x01)
{
WTN6_DAT_HIG;
}
else
{
WTN6_DAT_LOW;
}
break;
case 2:
case 4:
case 6:
case 8:
case 10:
case 12:
case 14:
case 16:
{
WTN6_CLK_HIG;
VolueNum >>= 1;
}
break;
case 3:
case 5:
case 7:
case 9:
case 11:
case 13:
case 15:
{
WTN6_CLK_LOW;
if(VolueNum & 0x01)
{
WTN6_DAT_HIG;
}
else
{
WTN6_DAT_LOW;
}
}
break;
}
if(Wtn6Playflag == 17) //发完了,对要清零的数据清零
{
WTN6_DAT_HIG;
WTN6_CLK_HIG;
Wtn6Playflag = 0;
if(Wtn6NextNum != 0xff) //若下一次用播放,即Wtn6NextNum!=0,则继续播放
{
hal_Wtn6_PlayVolue(Wtn6NextNum);
Wtn6NextNum = 0xff;
}
return;
}
}
hal_ResetTimer(T_WTN6,T_STA_START);
}
分析(及时理解):
(1)语音播报程序void hal_Wtn6_PlayVolue(unsigned char VolNum) 其实就包含了语音播报处理函数static void hal_Wtn6_PlayHandle(void),
(2)因为语音播报处理函数是语音播报程序的回调函数
(3)语音播报程序void hal_Wtn6_PlayVolue(unsigned char VolNum) 相当于播报前的一个预处理,语音播报处理函数static void hal_Wtn6_PlayHandle(void)是正式处理播报功能
(4)这就体现出来,程序和做事一样,做得很周到,有预处理和对预处理的结果做正式处理。
⑤完善hal_wtn6.h代码
#ifndef ____HAL_WTN6_H_
#define ____HAL_WTN6_H_
//通过枚举的形式,把要播放的语音都写在这里,要播放哪个直接取值就行
enum
{
WTN6_HOMEARM = 1, ///在家布防
WTN6_AWAYARM, ///离家布防
WTN6_DISARM, ///撤防
WTN6_STUDY_START, ///开始成功,请出发探测器
WTN6_STUDY_SUC, ///配对成功
WTN6_HAVED_DETEC, ///探测器已存在
WTN6_STUDY_FAIL, ///配对失败
WTN6_GET_WIFI_PASSWORD, ///开始配网,请在APP上输入WIFI密码,点击链接按钮
WTN6_GET_WIFI_OK, ///配网成功
WTN6_GET_WIFI_FAIL, ///配网失败
WTN6_WIFI_TIMEOUT, ///配网超时
WTN6_AC_DOWN, ///主机掉电
WTN6_AC_RECOVER, ///外电恢复
WTN6_TO_FACTORY, ///恢复出厂设置OK
WTN6_UPGRADE_NEWFIREWARE,//new fireware
WTN6_UPGRADE_DOWN_START,//
WTN6_UPGRADE_DOWN_FAIL,
WTN6_UPGRADE_DOWN_SUC, ///升级成功,
WTN6_VOLUE_SUC, ///音效 成功音效
WTN6_VOLUE_DI, ///音效 DI DI
WTN6_VOLUE_DINGDONG, ///音效 叮咚
WTN6_VOLUE_VOLT_LOW, ///音效 电池低压
WTN6_VOLUE_110, ///音效 110报警声音
WTN6_VOLUE_110_12, ///音效 110报警声音
WTN6_VOLUE_110_15, ///音效 110报警声音
WTN6_VOLUE_110_18, ///音效 110报警声音
};
void hal_wtn6(void); //初始化的函数
void hal_Wtn6_PlayVolue(unsigned char VolNum); //播放语音的函数
#endif
⑥在hal_task.c中初始化wtn6.代码如下。
#include "hal_task.h"
#include "hal_timer.h"
#include "hal_led.h"
#include "hal_gpio.h"
#include "hal_wtn6.h"
void hal_task_init(void)
{
hal_timerInit();
hal_GpioConfig_init();
hal_LedInit();
hal_wtn6(); //初始化语音播报芯片
}
⑦在hal_task.c 函数void hal_task(void)中增加wtn6.测试代码:
void hal_task(void)
{
static unsigned short wtn6testDelay = 0;
static unsigned char wtn6PlayNum = 1;
wtn6testDelay ++;
if(wtn6testDelay > 500) //10ms*500 = 5s,每5s执行一次播报,把枚举的几个语句全部读完又从头读
{
wtn6testDelay = 0;
hal_Wtn6_PlayVolue(wtn6PlayNum);
wtn6PlayNum ++;
if(wtn6PlayNum >= WTN6_VOLUE_110_18)
{
wtn6PlayNum = 1;
}
}
}
4.语音芯片功能的测试
tips
(1)理解代码,先去找到最终实现功能的函数,
(2)根据目的,去研究相关的各个函数,每个函数的研究可以从函数里面的变量入手,去理解一个一个有什么作用。
(3)理解每个变量的作用,而后理解整个函数的思路,最后可以理解整个系统的逻辑思路。
(4)上面的驱动代码还是比较灵活的运用了定时器矩阵,不过我感觉这代码里面更值得欣赏的还是发送CLK和DATA数据的思路,巧妙的配合,灵活的程序编写,很优秀,值得深入研究学习。
(5)有的时候程序写完了却没有实现目的,可能的代码问题是,功能所用到的端口的时钟,没有全部打开。