JQ8900-16P语音模块串口驱动移植与天空星STM32F407实战应用
JQ8900-16P语音模块串口驱动移植与天空星STM32F407实战应用
最近在做一个智能设备的项目,需要加入语音提示功能,比如“欢迎光临”、“操作成功”之类的。找了一圈,发现JQ8900-16P这个语音模块挺合适,价格便宜,控制简单,最关键的是它内置了SPI Flash,更新语音就像往U盘里拷贝文件一样方便,不用折腾什么专用上位机软件。正好手头有块天空星的STM32F407开发板,就决定用它来驱动这个模块。
今天这篇文章,我就来手把手带你把这个JQ8900语音模块,通过串口接到咱们的天空星开发板上,从硬件连接到驱动代码编写,再到最后播放测试,走一遍完整的流程。就算你之前没玩过语音模块,跟着做下来也能搞定。
1. 认识JQ8900-16P语音模块
在动手写代码之前,咱们先得搞清楚要控制的“对象”是个啥。JQ8900-16P模块,你可以把它理解成一个自带小喇叭和存储卡的MP3播放器,但它比MP3更简单,专门为嵌入式设备设计。
模块核心特点:
- 供电简单:工作电压范围是2.8V到5.5V,咱们的开发板上的3.3V或者5V都能直接用。
- 功耗低:额定电流在500微安到10毫安之间,非常省电。
- 存储方便:这是它最大的亮点!模块上的SPI Flash芯片在电脑上会被识别成一个U盘。你想换语音,直接把新的音频文件(比如MP3格式)拖进去,按规则重命名就行,完全不需要任何烧录工具。
- 控制灵活:支持三种控制模式,适合不同场景:
- 单独IO控制:每个语音文件对应一个引脚,拉低引脚就播放,适合按键直接触发。
- 一线串行控制:用一根数据线,按照特定的时序脉冲发送指令,节省IO口。
- 两线串口控制:就是我们今天要用的方式,通过串口发送指令,功能最全,也最常用。
注意:模块出厂时,SPI Flash里已经预存了10首测试语音。你自己添加的语音文件不能太大,否则存储空间可能不够。
串口通信参数(务必记准):我们选择“两线串口控制”模式,它的通信格式是固定的:
- 波特率:9600
- 数据位:8位
- 停止位:1位
- 校验位:无
这个参数就像两个人对话的语速和规则,单片机和模块必须设置成一样的才能正常交流。
2. 硬件连接:把模块“插”到开发板上
硬件连接是第一步,错了后面全白搭。我们用的是串口控制,所以只需要连接4根线。
| 模块引脚 | 天空星STM32F407引脚 | 连接说明 |
|---|---|---|
| VCC | 3.3V 或 5V | 电源正极,开发板上任意的3.3V或5V引脚 |
| GND | GND | 电源地,和开发板共地 |
| TX | PA3 (USART2_RX) | 模块的TX接单片机的RX,模块发送数据给单片机听 |
| RX | PA2 (USART2_TX) | 模块的RX接单片机的TX,单片机发送指令给模块 |
重要提示:串口连接时,一定要交叉!即发送端(TX)接接收端(RX)。很多新手在这里栽跟头,如果接反了,通信肯定失败。
为什么选PA2和PA3?因为这两个引脚是STM32F407的USART2串口默认功能引脚,用起来最方便,不需要重映射。当然,你也可以用其他串口,但代码里的引脚定义就得跟着改。
3. 驱动代码移植与解析
硬件连好了,接下来就是软件部分。我们需要创建两个文件:bsp_jq8900.c(驱动源文件)和bsp_jq8900.h(驱动头文件)。下面我逐段解释关键代码。
3.1 头文件配置 (bsp_jq8900.h)
头文件主要做两件事:定义硬件连接和声明函数。
#ifndef _BSP_JQ8900_H_ #define _BSP_JQ8900_H_ #include "stm32f4xx.h" #include "string.h" #include "board.h" // 你的开发板基础头文件 // 是否开启调试,开启后会在串口0打印接收到的数据 #define DEBUG 1 #define JQ8900_RX_LEN_MAX 250 // 串口接收缓冲区最大长度 /**************************** 串口配置 ****************************/ // 以下宏定义将代码与具体的硬件引脚绑定 #define BSP_JQ8900_TX_RCC RCC_AHB1Periph_GPIOA // TX引脚所在GPIO端口时钟 #define BSP_JQ8900_RX_RCC RCC_AHB1Periph_GPIOA // RX引脚所在GPIO端口时钟 #define BSP_JQ8900_RCC RCC_APB1Periph_USART2 // 使用USART2 #define BSP_JQ8900_TX_PORT GPIOA // TX引脚端口 #define BSP_JQ8900_RX_PORT GPIOA // RX引脚端口 #define BSP_JQ8900_AF GPIO_AF_USART2 // 复用功能为USART2 #define BSP_JQ8900_TX_PIN GPIO_Pin_2 // TX对应PA2 #define BSP_JQ8900_TX_SOURCE GPIO_PinSource2 #define BSP_JQ8900_RX_PIN GPIO_Pin_3 // RX对应PA3 #define BSP_JQ8900_RX_SOURCE GPIO_PinSource3 #define BSP_JQ8900 USART2 // 使用的串口 #define BSP_JQ8900_IRQ USART2_IRQn // 串口中断号 #define BSP_JQ8900_IRQHandler USART2_IRQHandler // 中断服务函数名 // 如果你要用一线串行控制模式,才需要定义这个APP引脚(这里用PC2举例) #define RCC_JQ8900_APP RCC_AHB1Periph_GPIOC #define PORT_JQ8900_APP GPIOC #define GPIO_JQ8900_APP GPIO_Pin_2 #define SET_JQ8900_APP(x) GPIO_WriteBit(PORT_JQ8900_APP, GPIO_JQ8900_APP, x?Bit_SET:Bit_RESET); // 函数声明 void JQ8900_Init(void); void JQ8900_USART_send_String(unsigned char *str, unsigned int len); void SendData ( unsigned char addr ); // 一线串行控制函数 #endif这里把串口2(USART2)和引脚PA2、PA3绑定好了。如果你想换到其他串口(比如USART1),只需要修改这些宏定义即可。
3.2 串口初始化与发送函数 (bsp_jq8900.c)
驱动文件里函数不少,我们挑最核心的几个讲。
首先是串口初始化函数JQ8900_USART_Init。这个函数负责把STM32的USART2配置成9600波特率、8N1格式,并开启接收中断。
void JQ8900_USART_Init(unsigned int bund) { GPIO_InitTypeDef GPIO_InitStructure; // 1. 使能GPIO时钟 RCC_AHB1PeriphClockCmd(BSP_JQ8900_TX_RCC, ENABLE); RCC_AHB1PeriphClockCmd(BSP_JQ8900_RX_RCC, ENABLE); // 2. 配置GPIO为串口复用功能 GPIO_PinAFConfig(BSP_JQ8900_TX_PORT, BSP_JQ8900_TX_SOURCE, BSP_JQ8900_AF); GPIO_PinAFConfig(BSP_JQ8900_RX_PORT, BSP_JQ8900_RX_SOURCE, BSP_JQ8900_AF); GPIO_InitStructure.GPIO_Pin = BSP_JQ8900_TX_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; // 复用模式 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽输出 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; // 上拉 GPIO_Init(BSP_JQ8900_TX_PORT, &GPIO_InitStructure); // RX引脚的配置类似,略... // 3. 使能USART2时钟并配置参数 RCC_APB1PeriphClockCmd(BSP_JQ8900_RCC, ENABLE); USART_InitTypeDef USART_InitStructure; USART_StructInit(&USART_InitStructure); USART_InitStructure.USART_BaudRate = bund; // 波特率,我们传9600 USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; // 收发模式 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_Init(BSP_JQ8900, &USART_InitStructure); // 4. 使能接收中断和空闲中断 USART_ITConfig(BSP_JQ8900, USART_IT_RXNE, ENABLE); // 接收缓冲区非空中断 USART_ITConfig(BSP_JQ8900, USART_IT_IDLE, ENABLE); // 空闲中断,用于判断一帧数据接收完成 USART_Cmd(BSP_JQ8900, ENABLE); // 使能串口 // 5. 配置NVIC(嵌套向量中断控制器),设置中断优先级 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = BSP_JQ8900_IRQ; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }这里用到了空闲中断(IDLE),这是个好东西。当串口总线上一段时间没有新数据时,就会产生这个中断。我们可以用它来判断一包数据是否接收完毕,比单纯用接收中断更可靠。
然后是数据发送函数,很简单,就是循环发送字符串里的每个字节。
void JQ8900_USART_send_String(unsigned char *str, unsigned int len) { while( len-- ) { // 调用发送单字节函数 USART_SendData(BSP_JQ8900, (uint8_t)(*str++)); while( RESET == USART_GetFlagStatus(BSP_JQ8900, USART_FLAG_TXE) ){} // 等待发送完成 } }3.3 中断服务函数:接收模块的回复
我们发送指令后,模块有时会回复状态(比如播放完成信号),所以需要接收数据。这里用中断方式接收,不占用主程序时间。
// 定义接收缓冲区和标志 unsigned char JQ8900_RX_BUFF[JQ8900_RX_LEN_MAX]; unsigned char JQ8900_RX_FLAG = 0; unsigned char JQ8900_RX_LEN = 0; void JQ8900_USART_IRQHandler(void) { // 1. 处理接收中断(RXNE):来了一个新字节 if(USART_GetITStatus(BSP_JQ8900, USART_IT_RXNE) != RESET) { // 读取这个字节,存到缓冲区 JQ8900_RX_BUFF[ JQ8900_RX_LEN ] = USART_ReceiveData(BSP_JQ8900); // 如果定义了DEBUG,可以通过printf打印出来看看 #if DEBUG printf("%c", JQ8900_RX_BUFF[ JQ8900_RX_LEN ]); #endif // 缓冲区索引增加,防止溢出 JQ8900_RX_LEN = ( JQ8900_RX_LEN + 1 ) % JQ8900_RX_LEN_MAX; USART_ClearITPendingBit(BSP_JQ8900, USART_IT_RXNE); // 清除中断标志 } // 2. 处理空闲中断(IDLE):一帧数据收完了 if(USART_GetITStatus(BSP_JQ8900, USART_IT_IDLE) == SET) { // 读SR和DR寄存器是为了清除IDLE标志位,这是STM32库函数的要求 volatile uint32_t temp; temp = BSP_JQ8900->SR; temp = BSP_JQ8900->DR; // 给接收到的字符串加上结束符'\0',方便后续用字符串函数处理 JQ8900_RX_BUFF[JQ8900_RX_LEN] = '\0'; JQ8900_RX_FLAG = 1; // 设置接收完成标志,主函数里可以查询这个标志来处理数据 USART_ClearITPendingBit(BSP_JQ8900, USART_IT_IDLE); } }这个中断服务函数是驱动接收部分的核心。它实现了“来一个字节存一个,数据流停了就通知主程序”的机制。
3.4 模块初始化函数
最后,我们把所有初始化工作封装成一个函数,方便主程序调用。
void JQ8900_Init(void) { JQ8900_USART_Init(9600); // 初始化串口,波特率9600 // 以下是一线串行控制模式所需的GPIO初始化(如果只用串口模式,这部分可以不要) GPIO_InitTypeDef GPIO_InitStructure; RCC_AHB1PeriphClockCmd(RCC_JQ8900_APP, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_JQ8900_APP; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(PORT_JQ8900_APP, &GPIO_InitStructure); GPIO_SetBits(PORT_JQ8900_APP, GPIO_JQ8900_APP); // 默认拉高 }4. 主程序测试:让模块“开口说话”
驱动写好了,现在来写个主程序测试一下。我们让模块循环播放“下一曲”。
#include "board.h" #include "bsp_uart.h" // 用于printf调试 #include <stdio.h> #include "bsp_jq8900.h" int main(void) { // JQ8900串口控制指令:下一曲 // 指令格式:0xAA (帧头), 0x06 (命令), 0x00 (数据高字节), 0xB0 (数据低字节+校验) uint8_t send_buff[4] = {0xAA, 0x06, 0x00, 0xB0}; board_init(); // 开发板基础初始化(系统时钟、滴答定时器等) uart1_init(115200U); // 初始化调试串口,用于打印信息 printf("JQ8900 Test Start...\r\n"); JQ8900_Init(); // 初始化JQ8900模块(串口、GPIO等) while(1) { // 每隔2秒发送一次“下一曲”指令 JQ8900_USART_send_String(send_buff, 4); printf("Send 'Next' command.\r\n"); delay_ms(2000); // 等待2秒 } }这段代码干了啥?
- 定义了一个指令数组
send_buff,里面是让模块播放“下一曲”的串口指令码。 - 初始化系统和调试串口。
- 调用
JQ8900_Init()初始化我们刚写好的驱动。 - 在主循环里,每隔2秒就通过串口发送一次这个指令。
上电后的现象应该是:模块会从第一首语音开始播放,播放完后等2秒(我们延迟的时间),自动播放第二首,依次类推,直到播放完存储的所有语音文件,然后又会从第一首开始循环。
5. 进阶与排错
如何播放指定曲目?JQ8900有完整的串口指令集,在它的数据手册里。比如播放第5首语音,指令可能是{0xAA, 0x07, 0x00, 0x05, 0xB2}(具体以手册为准)。你需要把指令码替换掉上面测试代码里的send_buff即可。
如果没声音,怎么排查?
- 查硬件:首先确认VCC和GND没接反、接牢。重点检查TX和RX是否交叉连接。可以用万用表测一下电压。
- 查代码:确认波特率是不是9600。确认发送的指令码是否正确。可以尝试在
JQ8900_USART_send_String函数里加个printf,把发送的每个字节的十六进制打印出来看看。 - 听提示音:有些模块上电或收到错误指令时会有一个“嘀”的提示音。如果有提示音但播放没声音,可能是语音文件格式或命名不对。
- 用USB-TTL调试:如果你有一个USB转TTL模块,可以把它连接到电脑,用串口助手直接发送指令给JQ8900,这样可以排除单片机代码的问题。
关于语音文件: 把模块用USB线连接到电脑,它会弹出一个U盘。把你要的MP3或WAV文件放进去,并重命名为5位数字,例如00001.mp3、00002.mp3。模块就是根据这个文件名编号来播放对应曲目的。
好了,整个移植和应用的流程就是这样。代码我已经在实际的天空星开发板上跑通了。你只要跟着步骤,注意硬件连接和指令格式,让JQ8900在STM32上工作起来并不难。遇到问题,多利用串口打印调试信息,慢慢分析,总能解决的。
