系列文章目录
项目一 基于 SIM800 天气语音广播服务机器人的时间 等待后续添加……
文章目录
- 系列文章目录
- 前言
- 一、总体设计方案
-
- 1. 硬件
- 2. 软件
- 二、硬件设计
- 三、软件设计
-
- 1 时间天气服务API
- 2 SIM800 HTTP 服务
- 3 使用 cJSON 分析服务器返回的数据
- 4 使用数据的坑
- 5 显示天气和时间
- 6 SYN2688 语言播报
- 四、总结
前言
移动蜂窝网络已经渗透到我们生活的方方面面,无论是即时聊天、视频电视还是移动支付,都深深依赖于移动蜂窝网络。
经过几十年的发展,移动蜂窝网从 1G 到 5G,人们的生活方式发生了翻天覆地的变化。
该项目使用 2G 实现语言播报天气和时间的服务机器人。
一、总体设计方案
1. 硬件
- 主控:STM32F103C8T6;
- 通信模块:SIM800模块;
- 显示模块:0.96 寸 OLED12864 显示屏幕;
- 语音模块:SYN6288 串口文字转语音模块;
- 独立按键模块。
2. 软件
- SIM800 的 HTTP 服务器获取天气和时间数据;
- 单片机串口通过文字发送天气和时间数据 SYN6288 模块;
- 按键设置闹钟和定时器功能;
- 显示时间和天气。
二、硬件设计
设计中使用的所有模块均采用模块组装,方便省事。
1. STM32F103C8T6 最小系统
资金紧张,STM32F103C8T6 可考虑使用国产芯片代替。 STM32 PIN to PIN 有很多国产芯片可以自己玩。
2. SIM800 模块
SIM800 是一款四频 GSM/GPRS 为城堡孔包装的模块。性能稳定,外观小巧,性价比高,能满足客户的各种需求。SIM800 工作频率为GSM/GPRS850/900/1800/1900MHz,语音可以低功耗实现,SMS传输数据信息。SIM800 尺寸为17.615.72.3mm,可适用于各种紧凑型产品的设计需求。
该模块需要配合 SIM 使用卡片。不同的制造商是对的 SIM800 模块设计略有不同,有的自动复位,有的需要手动复位;SIM 大小也不一样:有的用小卡,有的用大卡。购买时注意这些参数。
现在我们日常使用的卡大多数已经是 5G 网络,至少也是 4G 是的,但基本上 2G,所以你可以用你的手机卡来测试。然而,互联网上有很多纯粹的 2G 卖卡很方便。
3. SYN6288 语音模块
这个模块没什么好说的。可以直接用串口发文字转换成语音。中英文可以直接转换。
唯一需要注意的是中文编码。该模块支持 GB2312,GBK,Unicode 等待不同的中文编码,您使用的编码与编译器有关,需要作为参数设置。
详情请参考本文:SYN6288语音合成模块介绍
数据手册:SYN6288 数据手册
4. OLED 显示模块
屏幕模块的尺寸约为 0.96 英寸主要由裸屏和底板组成PCB裸屏由组成 SSD1306驱动也被广泛使用 LED 驱动芯片。
驱动接口有 SPI 和 IIC 本设计采用两种 IIC 驱动。
5. 按键模块
直接购买四合一的独立按键模块。不知道为什么这个模块没有上拉电阻,所以使用要配置成 。
三、软件设计
1 时间天气服务API
网上有很多免费的天气和时间服务界面,可以自己去找。我在这里用。 NowAPI。使用方法也很简单,去官方网站注册账户,然后申请免费使用。使用时间只有三个月,我只需要做测试。高德天气服务似乎有一个长期的免费接口。 申请天气预报接口和标准北京时间接口。
附上我申请的接口(过期无效): 天气预报 北京时间 如果无效,注册一个账户并申请 API 自动生成后 AppKey 和 Sign,替换上面连接的内部 AppKey 和 Sign 就行了。
2 SIM800 HTTP 服务
使用天气预报和北京时间服务 HTTP 因此,我们需要获取服务器数据并启动需求 SIM800 的 HTTP 服务。
SIM800 使用 串口 AT 实际上,我们只需要发送几个指令控制 AT 您可以获得服务器返回给我们的数据。
/* 获取服务器数据 url 是服务 API 获得的数据存在于串口接收中 BUFF 里面 */ u8 sim900a_get_http(u8 *url) {
static u8 http_not_init = 1; u8 cmd[200]; sprintf((char*)cmd, "AT HTTPPARA=\"URL\","); strcat((char*)cmd, (char*)url);
//if(sim900a_send_cmd((u8 *)"ATE0",(u8 *)"OK",100)) return SIM_CSQ_ERR;
if(http_not_init)
{
http_not_init = 0;
if(sim900a_send_cmd((u8 *)"AT+CSQ",(u8 *)"OK",100)) return SIM_CSQ_ERR;
if(sim900a_send_cmd((u8 *)"AT+CREG?",(u8 *)"OK",100)) return SIM_CREQ_ERR;
if(sim900a_send_cmd((u8 *)"AT+CSCA?",(u8 *)"OK",100)) return SIM_CSCA_ERR;
if(sim900a_send_cmd((u8 *)"AT+CGATT?",(u8 *)"OK",100)) return SIM_CGATT_ERR;
if(sim900a_send_cmd((u8 *)"AT+SAPBR=3,1,\"APN\",\"CMNET\"",(u8 *)"OK",100)) return SIM_SAPBR_ERR;
if(sim900a_send_cmd((u8 *)"AT+SAPBR=1,1",(u8 *)"OK",1000)) return SIM_SAPBR_ERR;
if(sim900a_send_cmd((u8 *)"AT+HTTPINIT",(u8 *)"OK",300)) return SIM_HTTPINIT_ERR;
}
if(sim900a_send_cmd(cmd,(u8 *)"OK",300));// return SIM_CMGS_ERR;
if(sim900a_send_cmd((u8 *)"AT+HTTPACTION=0",(u8 *)"+HTTPACTION: 0,200",2000)) return SIM_HTTPACTION_ERR;
sim900a_send_cmd((u8 *)"AT+HTTPREAD",(u8 *)"OK",100);
return SIM_OK;
}
//向sim900a发送命令
//cmd:发送的命令字符串(不需要添加回车了),当cmd<0XFF的时候,发送数字(比如发送0X1A),大于的时候发送字符串.
//ack:期待的应答结果,如果为空,则表示不需要等待应答
//waittime:等待时间(单位:10ms)
//返回值:0,发送成功(得到了期待的应答结果)
// 1,发送失败
u8 sim900a_send_cmd(u8 *cmd,u8 *ack,u16 waittime)
{
u8 res=0;
USART2_RX_STA=0;USART2_RX_REC_ATCOMMAD=1;
if((u32)cmd<=0XFF)
{
while(DMA1_Channel7->CNDTR!=0); //等待通道7传输完成
USART2->DR=(u32)cmd;
}else u2_printf("%s\r\n",cmd);//发送命令
if(ack&&waittime) //需要等待应答
{
while(--waittime) //等待倒计时
{
delay_ms(10);
if(USART2_RX_STA&0X8000)//接收到期待的应答结果
{
if(((USART2_RX_STA & 0x7FFF) > 200) && (strstr((const char*)USART2_RX_BUF,"{")))
{
memset(http_buff, 0, USART2_MAX_RECV_LEN);
strcpy((char*)http_buff, (char*)USART2_RX_BUF); //大于200字节认为是http数据
}
if(sim900a_check_cmd(ack))break;//得到有效数据
USART2_RX_STA=0;
}
}
if(waittime==0)res=1;
}
USART2_RX_STA=0;USART2_RX_REC_ATCOMMAD=0;
return res;
}
///
//usmart支持部分
//将收到的AT指令应答数据返回给电脑串口
//mode:0,不清零USART2_RX_STA;
// 1,清零USART2_RX_STA;
void sim_at_response(u8 mode)
{
if(USART2_RX_STA&0X8000) //接收到一次数据了
{
USART2_RX_BUF[USART2_RX_STA&0X7FFF]=0;//添加结束符
printf("%s",USART2_RX_BUF); //发送到串口
if(mode)USART2_RX_STA=0;
}
}
///
//ATK-SIM900A 各项测试(拨号测试、短信测试、GPRS测试)共用代码
//sim900a发送命令后,检测接收到的应答
//str:期待的应答结果
//返回值:0,没有得到期待的应答结果
// 其他,期待应答结果的位置(str的位置)
u8* sim900a_check_cmd(u8 *str)
{
char *strx=0;
if(USART2_RX_STA&0X8000) //接收到一次数据了
{
USART2_RX_BUF[USART2_RX_STA&0X7FFF]=0;//添加结束符
strx=strstr((const char*)USART2_RX_BUF,(const char*)str);
}
return (u8*)strx;
}
3 使用 cJSON 解析服务器返回的数据
上述接口返回的数据都是 JSON 格式的,我们的单片机没法直接使用,要先把有用的数据解析出来。
已时间服务为例,获取到的数据如下:
{
"success":"1","result":
{
"timestamp":"1656234962","datetime_1":"2022-06-26
17:16:02","datetime_2":"2022年06月26日 17时16分02
秒","week_1":"0","week_2":"星期日","week_3":"周
日","week_4":"Sunday"}}
这是标准的 JSON 格式数据。开头的“success”:“1” 表示获取成功,“result” 里面包含了返回的数据。
JSON 是一种很直观的数据表示方式,上面的数据不用解释想必大家也能看懂。
我们能看懂,但是单片机可看不懂。JSON 格式本身就不是给 C 语言准备的,所以解析的话相对麻烦。这里使用大神写的 cJSON 库,底层已经帮我们做好了,只要简单调用几个函数就可以解析。
cJSON 使用参考一下大佬的文章 cJSON使用详细教程。
u8 http_weather_data_parser(char *dat)
{
cJSON* cjson_root = NULL;
cJSON* cjson_success = NULL;
cJSON* cjson_result = NULL;
cJSON* cjson_weather = NULL;
/* 解析整段JSO数据 */
printf("%s", dat);
cjson_root = cJSON_Parse(dat);
if(cjson_root == NULL)
{
printf("Parse fail.\n");
return PARSE_FAILED;
}
/* 依次根据名称提取JSON数据(键值对) */
cjson_success = cJSON_GetObjectItem(cjson_root, "success");
if(*cjson_success->valuestring != '1')
{
printf("GET_DATA_FAILED\r\n");
cJSON_Delete(cjson_root);
return PARSE_FAILED;
}
/* 解析嵌套json数据 */
cjson_result = cJSON_GetObjectItem(cjson_root, "result");
cjson_weather = cJSON_GetObjectItem(cjson_result, "weather_curr");
strcpy((char*)weather_buf, cjson_weather->valuestring);
for(weather_index = 0; weather_index < 10; weather_index++)
{
if(strcmp((char*)&weather_matrix[weather_index][0], (char*)weather_buf) == 0)
{
break;
}
}
if(weather_index > 10)
{
weather_index = 0;
}
cJSON_Delete(cjson_root);
return GET_DATA_SUCCESS;
}
4 数据使用的坑
这里有个大坑:服务器返回的天气数据只有 UTF-8 的中文,没有英文;而使用 cJSON 没办法把这些中文解析出来。
尝试改编辑器的编码也无济于事,最后没办法,只好采用曲线救国的方式,手动把中文天气和十六机制编码匹配。
英文天气是给 OLED 显示用的,懒得给每个天气做中文转换,就全用英文显示了。
u8 weather_matrix[][7] = {
{
0xE6, 0x99, 0xB4, 0x00}, //sunny
{
0xE5, 0xA4, 0x9A, 0xE4, 0xBA, 0x91, 0x00}, //cloudy
{
0xE9, 0x98, 0xB4, 0x00}, //overcast
{
0xE5, 0xB0, 0x8F, 0xE9, 0x9B, 0xA8, 0x00}, // light rain
{
0xE4, 0xB8, 0xAD, 0xE9, 0x9B, 0xA8, 0x00}, // moderate rain
{
0xE5, 0xA4, 0xA7, 0xE9, 0x9B, 0xA8, 0x00}, // heavy rain
{
0xE6, 0x9A, 0xB4, 0xE9, 0x9B, 0xA8, 0x00}, // rainstorm
{
0xE5, 0xB0, 0x8F, 0xE9, 0x9B, 0xAA, 0x00}, // light snow
{
0xE4, 0xB8, 0xAD, 0xE9, 0x9B, 0xAA, 0x00}, // moderate snow
{
0xE5, 0xA4, 0xA7, 0xE9, 0x9B, 0xAA, 0x00}, // heavy snow
{
0xE6, 0x99, 0xB4, 0x00}}; //sunny
u8 weather_str[][15] = {
"Sunny", "Cloudy", "Overcast", "Light rain", "Moderate rain",
"Heavy_rain", "Rainstorm", "Light snow", "Moderate snow", "Heavy snow"};
u8 weather_voice[][30] = {
"天气晴", "天气多云", "天气阴", "天气小雨", "天气中雨",
"天气大雨", "天气暴雨", "天气小雪", "天气中雪", "天气大雪"};
北京时间我这里是直接拿获取到的时间戳转换出来,不存在上述的问题。
5 显示天气和时间到显示屏
首先贴一下 SSD1306 驱动代码,这是我自己测试过能使用的。
#include "OLED_I2C.h"
#include "delay.h"
#include "codetab.h"
void I2C_Configuration(void)
{
I2C_InitTypeDef I2C_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
/*STM32F103C8T6芯片的硬件I2C: PB6 -- SCL; PB7 -- SDA */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;//I2C必须开漏输出
GPIO_Init(GPIOB, &GPIO_InitStructure);
I2C_DeInit(I2C1);//使用I2C1
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = 0x30;//主机的I2C地址,随便写的
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = 400000;//400K
I2C_Cmd(I2C1, ENABLE);
I2C_Init(I2C1, &I2C_InitStructure);
}
void I2C_WriteByte(uint8_t addr,uint8_t data)
{
u16 cnt = 0xFFFF;
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY) && cnt--);
I2C_GenerateSTART(I2C1, ENABLE);//开启I2C1
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));/*EV5,主模式*/
I2C_Send7bitAddress(I2C1, OLED_ADDRESS, I2C_Direction_Transmitter);//器件地址 -- 默认0x78
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, addr);//寄存器地址
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_SendData(I2C1, data);//发送数据
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTOP(I2C1, ENABLE);//关闭I2C1总线
}
void WriteCmd(unsigned char I2C_Command)//写命令
{
I2C_WriteByte(0x00, I2C_Command);
}
void WriteDat(unsigned char I2C_Data)//写数据
{
I2C_WriteByte(0x40, I2C_Data);
}
void OLED_Init(void)
{
delay_ms(100); //这里的延时很重要
WriteCmd(0xAE); //display off
WriteCmd(0x20); //Set Memory Addressing Mode
WriteCmd(0x10); //00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid
WriteCmd(0xb0); //Set Page Start Address for Page Addressing Mode,0-7
WriteCmd(0xc8); //Set COM Output Scan Direction
WriteCmd(0x00); //---set low column address
WriteCmd(0x10); //---set high column address
WriteCmd(0x40); //--set start line address
WriteCmd(0x81); //--set contrast control register
WriteCmd(0xff); //亮度调节 0x00~0xff
WriteCmd(0xa1); //--set segment re-map 0 to 127
WriteCmd(0xa6); //--set normal display
WriteCmd(0xa8); //--set multiplex ratio(1 to 64)
WriteCmd(0x3F); //
WriteCmd(0xa4); //0xa4,Output follows RAM content;0xa5,Output ignores RAM content
WriteCmd(0xd3); //-set display offset
WriteCmd(0x00); //-not offset
WriteCmd(0xd5); //--set display clock divide ratio/oscillator frequency
WriteCmd(0xf0); //--set divide ratio
WriteCmd(0xd9); //--set pre-charge period
WriteCmd(0x22); //
WriteCmd(0xda); //--set com pins hardware configuration
WriteCmd(0x12)