"失败不总是很常见的,放弃才是 "

开头在这

  大二学了单片机,本来想趁着课程设计的机会好好玩一把ESP32,但被老师告知课设就两周乖乖用51吧。一想到必须回去用屎一样的8051,我就只能像和超进化批元帅打游戏时候一样无语。于是想着干脆白兰的我开始思考8051能干些什么,几百字节的RAM,最好的加强型也只有60KB的扩展FLASH,不仅IO口资源稀缺,串口也只有一两个。硬件资源稀缺,加上它的各种库和指令集也只能用简陋来形容。没有专用的串口通信库,没有并行的串口。除了能拿来控制电机,做一些传感器的小项目好像也没什么用。看了周围的同学,果不其然绝大多数不是上淘宝买课设,就是上网抄现成的贪吃蛇和俄罗斯方块。课设每个学期都有,虽然我自知做不到牛哥那样一转专业就把整个大一课设做完那样的壮举。毕竟不是每个人能被评价为“班上学的最好的”,但我还是想要做点什么。看了一圈STC的所有系列,只有15和12集成了SPI和I2C总线控制器,下位机烂怎么办,那只能搞上位机控制了。要不整个HC05搞个天气预报吧,我觉得行。虽然STC的8051属于是烂中烂,但至少便宜。我选的STC15F2KS60只要4,5块,60KBFLASH,2048RAM,两个异步通信串口,内部集成SPI和I2C,至少现在看上去还不错。

硬件

  感觉没什么好说的,因为大量采用模块的缘故,电路相当简易,甚至可以去掉外部晶振和复位电路,只依靠MCU内部的晶振来提高集成度。关于显示室温的功能,我选择了DHT11传感器。然而软硬双修的牛哥对我的方案嗤之以鼻,精通数模电和软件的他提出了采用湿敏电阻,运算放大电路和AD转换芯片的方案,让才学浅薄的我甘拜下风。想必牛哥一定是自己设计一款温湿度IC,但显然对我来说这有些超纲了。我还是用简单易懂的DHT11吧。
原理图
其实打板的时候才发现CH340G的Rxd和Txd连反了,这下不仅浪费了一个串口,原来打算的下载电路只能拿来供电了

软件

Andriod SDK

  既然做了那就做全套,正好试试Andriod编程。实际上手感觉…还行?不追求额外功能的我就放了两个button在上面,反正只是作为课设演示用,能把请求天气api得到的天气编码发给HC05就行了。本来这部分是打算交给牛哥做的,只能说他到现在还是没有把程序给我,可能是打算在在上面集成什么先进技术吧,我暂且蒙古。

graph TD A[向天气API请求天气数据] --> B{api是否响应} B --> |否| A B --> |是| C[将JSON字符串转换为String类型的Java实体] C --> D["截取其中的天气状况,体感温度,气压,风速和云量"] D --> E[编码天气数据] E --> F[开始与HC05连接] F --> G[接收单片机发送的开始标志] G --> H{是否接收到0X43} H --> |是| I[开始传输天气数据] H --> |否| G

Andirod Bluetooth Api

  Andriod的蓝牙api提供了对蓝牙丰富的支持和功能,具体可以阅读Andriod的中文文档Andirod Bluetooth 。而实际情况是我并不需要用到如此多且复杂的功能,由于我只需要连接HC05一个设备,Mac的地址就直接写死了。在编写时我用到的只有BluetoothAdapter BluetoothDevice BluetoothSocket这三个类,分别用于蓝牙的连接和信息的收发。丢人的是我一直把Mac地址小写导致总是抛错误,再次体现写之前仔细阅读文档的重要性
官方文档的蓝牙读写例程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;

public ConnectedThread(BluetoothSocket socket) {
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;

// Get the input and output streams, using temp objects because
// member streams are final
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) { }

mmInStream = tmpIn;
mmOutStream = tmpOut;
}

public void run() {
byte[] buffer = new byte[1024]; // buffer store for the stream
int bytes; // bytes returned from read()

// Keep listening to the InputStream until an exception occurs
while (true) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer);
// Send the obtained bytes to the UI activity
mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
break;
}
}
}

/* Call this from the main activity to send data to the remote device */
public void write(byte[] bytes) {
try {
mmOutStream.write(bytes);
} catch (IOException e) { }
}

/* Call this from the main activity to shutdown the connection */
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}

由于read和write方法都为阻塞调用,所以为了防止线程阻塞将write方法单独写成一个公共方法来向输出流中写入数据。

天气Api请求与处理

  在网上找了一圈没有找到什么好用免费的天气api,原来气象局提供的免费api早在六年前就停运了,除非你想请求到六年前的天气数据。最后我用了一款天气api的免费开发版,关注公众号一天可以免费请求2000次。
原来打算处理天气JSON的方式

1
2
3
4
5
6
7
8
StringBuffer weathertu=new StringBuffer(ind.substring(ind.indexOf("text")+7,ind.indexOf("\"",ind.indexOf("text")+7)));
printLog(weathertu.toString());
StringBuffer feelsLike=new StringBuffer(ind.substring(ind.indexOf("feelsLike")+12,ind.indexOf("\"",ind.indexOf("feelsLike")+12)));
printLog(feelsLike.toString());
StringBuffer windSpeed=new StringBuffer(ind.substring(ind.indexOf("windSpeed")+12,ind.indexOf("\"",ind.indexOf("windSpeed")+12)));
printLog(windSpeed.toString());
StringBuffer pressure=new StringBuffer(ind.substring(ind.indexOf("pressure")+11,ind.indexOf("\"",ind.indexOf("pressure")+11)));
printLog(windSpeed.toString());

哈哈原来JSON可以调库来将JSON字符串转换为JAVA实体类来处理。Java自带的JSON处理库实属不太行,而net.sf.json的依赖包Andriod用不了,网上冲浪之后发现能用Google的gson库。
1
2
Gson gson = new Gson();
Bean beanOne = gson.fromJson(ind, Bean.class);

驱动

  只有在MCU上写程序时才明白大内存的好处,每定义一个变量都要精打细算,担惊受怕会不会超出内存。

串口1硬件管脚选择

1
2
3
4
ACC = P_SW1;             //外部硬件引腳選擇
ACC &= ~(S1_S0 | S1_S1); //S1_S0=1 S1_S1=0
ACC |= S1_S0; //(P3.6/RxD_2, P3.7/TxD_2)
P_SW1 = ACC;

令人欣慰的是STC15F2K60S的两个串口可以在五组硬件管脚中选择来实现多个串口的分时复用,至少STC已经把8051的性能榨的够干了,想必五十年前的intel也无法想到自己的8051能用到五十年后。
串口地址
AUXR1(ACC)寄存器中的S1_S0S1_S1两位表示串口1的硬件地址选择S1_S1=0,S1_S0=1时串口选择到我定义的P3^6和P3^7口。

串口中断

  所谓的蓝牙收发其实就是蓝牙模块与MCU的串口通信罢了。辅助寄存器AUXR中的UART_M0x6用于设置串口1在方式0时数据传输的波特率,置1时波特率为fsys/2,置0时波特率为fsys/12,也就是选择51单片机的12T和6T时钟模式。然后通过初始化定时器1的初始值TH1TL1来定义串口与HC05通信时的波特率,打开串口1的总中断和接收中断,中断号4。(波特率计算可以通过STC的烧录软件生成)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void UsartConfiguration()
{
SCON = 0x50; //8位数据,可变波特率
AUXR |= 0x40; //定时器时钟1T模式
AUXR &= 0xFE; //串口1选择定时器1为波特率发生器
TMOD &= 0x0F; //设置定时器模式
TL1 = 0x41; //设置定时初始值
TH1 = 0xFD; //设置定时初始值
//ET1 = 0; //禁止定时器%d中断
//TR1 = 1; //定时器1开始计时
ES = 1; //打开接收中断
EA = 1; //打开总中断
TR1 = 1; //打开计数器1
}

SBUF是MCU的串口缓存寄存器,用来缓存串口接收和准备发送的数据内容。
串口中断服务程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Usart() interrupt 4 //串口1接收中断
{
if (RI)
{
statser = 0;
receiveData = SBUF; //出去接收到的数据
RI = 0; //清除接收中断标志位
SBUF = receiveData; //将接收到的数据放入到发送寄存器
if (receiveData != 'X')
{
getinfo[times] = receiveData;
times++;
}
else
{
ifgetcompete = 1;
ifget = 1;
}
while (!TI)
; //等待发送数据完成
TI = 0; //清除发送完成标志位
}
}

串口数据发送
1
2
3
4
5
void PostChar(unsigned char character)  
{
SBUF=character; //发送单个字符
while(!TI);TI=0; //发送完成标志
}

DHT11

  DHT11是通过单总线协议(1-wire)来向MCU发送数据的传感器,也就是说它有着严格的时序控制。通过一定的延时来处理传感器的初始化与数据的读写。通过单总线协议,DHT11一共会向总线发送一串40bit的数据。包括了元件的温湿度整数及小数位和校验位,分别是8bit湿度整数+8bit湿度小数+8bit温度整数+8bit温度小数+8bit校验位。

单总线协议

初始化

单总线初始化
MCU在总线上发送480 ~ 960 us的低电平来产生16-60us的高电平复位脉冲给从机。从机再将总线拉低60-240us来应答MCU,最后从机发送高电平应答脉冲来表示初始化完成。
  对于DHT11来说主机的起始信号应该在18ms左右,再次高电平是开始发送数据。

1
2
3
4
5
6
7
8
void Start()
{
DATA=0;//将总线拉低
Delay20ms();//延时20ms这样DHT11可以检测到主机发送的起始信号,然后就可以开始发出响应信号
DATA=1;//将总线拉高
Delay30us();
DATA=1;//二次拉高总线开始传输数据
}

写间隙

单总线写间隙

读间隙

单总线读间隙
初始化之后总线低电平12-14us表示开始传输1bit,也就是传输每一bit之间的间隔信号。开始信号后26-28us高电平表示bit值为0,116-118us高电平则表示1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int readdht11_byte() //读取DHT11发送的每一个bit
{
int i,dht;
for(i=8;i>0;i--)//读取8bit数据
{
dht=dht<<1;//每次读取前,将数据的高位移位向左移位
while(!DATA);//等待0us分隔信号
Delay35us();//等待35us
if(DATA==1) //延时35us后依然是高电平说明bit值是1
{
dht=dht|0x01;//dht的最低位置1
Delay55us();
}
else;//bit值为0跳过此次循环仅将dht最低位左移
}
return(dht);//将这个dht返回
}

总结

  这个课设本来是打算三天做完的然后我就能去复习六级了,然而因为各种原因和不可抗力海猫真是太好玩辣我硬是做了一个礼拜才搞完。本来想着最简单的硬件部分也出了不少差错。牛哥在Andriod编程上确实是给了我不少帮助,如果他能在星期二把程序给我那我必定狠狠的夸他,在文章末尾还是感谢他。多做做项目确实能提升一些自己的能力,看见成品是也挺有成就感的。这个项目的驱动手机程序已经全部打包上传github,虽然两个东西都完全没做优化,驱动的按键中断有不少问题,传感器接收的湿度在第一次按按键时是完全不准的。手机程序有可能会因为找不到蓝牙设备而线程溢出,蓝牙连接的关闭也没写。以后有缘会把这些优化都搞完或者直接用牛哥给我的程序。旅途在第十晚结束,我也终于能进入我的黄金之乡。

更新2021.12.12

  妹想到刚写完博客牛哥就把程序给我了,这下我成小丑了哈哈哈。

蓝发女孩

能给可爱的蓝发妹妹点个关注吗