我萌萌哒的妹纸是一个代码苦手,完全无法理解 C 语言,所以每一次到单片机上机需要交作业的时候都是愁眉苦脸的样子。而我又总是因为自己确实不懂单片机里面的种种奇怪定义(中断,串口,P1.x 之类),所以也一直没有什么好办法去帮她。这一次的作业对编码能力要求较高,但是涉及到的硬件比较少,于是决定以此为契机,开始我的嵌入式开发之旅。

需求

这次的如下:

基本要求

  • 基本计时和显示功能(用 12 小时制显示)。 包括上下午标志,时、分的数字显示,秒信号指示。
  • 能设置当前时间(含上、下午,时,分)
  • 能实现基本打铃功能,规定: 上午 6:00 起床铃;打铃 5 秒、停 2 秒、再打铃 5 秒。 下午 10:30 熄灯铃;打铃 5 秒、停 2 秒、再打铃 5 秒。

铃声可用 LED 灯光显示,如果实验装置没有 LED 发光管,可以用七段显示管的小数点显示,也可以用显示小时的十位数码管的多余段显示。凡是用到铃声功能的均可如此处理。

发挥部分

  • 增加整点报时功能,整点时响铃 5 秒,要求有控制启动和关闭功能。
  • 增加调整起床铃、熄灯铃时间的功能。
  • 增加调整打铃时间长短和间歇时间长短的功能。
  • 增设上午 4 节课的上、下课打铃功能,规定: 7:30 上课,8:20 下课;8:30 上课,9:20 下课;9:40 上课,10:30 下课;10:40 上课,11:30 下课;每次铃声 5 秒。
  • 利用板上按键做一个 12 小时/24 小时的显示格式切换

分析

既然我都出动了,肯定不能满足于只完成基本要求,决定把所有功能全都完整的实现。

简单的来说,整体需求可以分为三个部分:显示,打铃,修改。

需要用到的东西有:串口,指示灯和一个按键。

显示

遵循简单的前后端分离的思想,我们可以使用三个全局变量 hour , minute , second 来存储当前的时间,只需要在显示的时候区分上下午和 12 小时/ 24 小时即可。这两个部分解耦之后会发现,我们后面的利用板上按键修改显示格式也变得容易了很多。 通过串口显示也就是需要向指定变量发送字符,将这个功能抽象并封装之后,对于我后续的编程来说,也就是调用一下 Send_Str(str) 的过程。

打铃

打铃是这套系统的重头戏,因为学校方面的资源限制,所以使用指示灯示意的方法来代替打铃。 指示灯的亮灭是通过控制一个变量的值来确定的,于是我只要在正确时候设置正确的值,打铃系统就能按照我期望的方式工作。

修改

修改同样是通过串口进行的。

在最开始的设计文档中,本来是要求使用4个按键来进行设计,也就是说跟一个普通的电子表差不多。但是非常因缺思艇的事情是学校的按键不够了,所以老师要求所有功能都用串口实现。

跟显示有些不同的地方是,通过串口向芯片发送数据需要正确使用串口中断。

综上,这个系统所需要的全部内容就已经实现了。可以看到我做了很多将对硬件的操作抽象化的处理,其实这一点非常重要。因为对于我来说,嵌入式开发最大的难题在于,我不知道里面种种变量的含义,不知道如何操作具体的硬件。将硬件操作抽象化处理之后,我就可以很方便地开展我的后续开发。

问题

实现就不再赘述了,想必读者一定都比我强,下面聊一聊遇到的问题以及 debug 的经历。

串口配置

串口的收和发其实是分开的,这里用到了两个变量: UCA0TXBUF , UCA0RXBUF 。从字面意思上可以看出,前一个用于发送,后一个用于接收(相对于开发者来说)。发送和接收其实就是给这两个值赋值的过程,看起来这两个变量在接受到值之后会将这个值传给别的变量,所以只要不断的将值赋给它就行,我们写了这样的函数:

#pragma vector=USCIAB0RX_VECTOR //中断服务函数

__interrupt void uart() {
    rec = UCA0RXBUF;
    //读取到缓冲区
    strtmp[strlen++] = rec;
    strtmp[strlen] = '\0';
    //切换模式
    mode = strtmp[0];
}

//发送字符
void Send_Char(char ch) {
    while (!(IFG2 & UCA0TXIFG));
    UCA0TXBUF = ch;
}

//发送字符串
void Send_Str(char *p) {
    unsigned char i;
    i = 0;
    while (*(p + i) != '\0') {
        while (!(IFG2 & UCA0TXIFG));
        UCA0TXBUF = *(p + i);
        i++;
    }
    Send_Char(0x0d);
    Send_Char(0x0a);
}

uart 貌似是一个内置的中断函数,用来处理串口的接收,只要将变量 UCA0RXBUF 的值存储起来即可;后面的 Send_Str 就非常好理解了,将值发送给 UCA0TXBUF ,从而实现串口的输出。

思路如此清晰,但是测试的时候却遇到了问题,我们的输出是空的,转为16进制显示后,全都是0x00。这个问题调试了很久,拿着原来的代码逐行比对之后发现,出了这样的问题:

- UCA0BR0 = 130;
- UCA0BR1 = 6;
+ UCA0BR0 = 104;
+ UCA0BR1 = 0;

Google 一下才明白,原来 UCA0BR0 和 UCA0BR1 是由系统的时钟速度和波特率决定的值,如果设置错误就会导致串口发送失败。具体的值可以参考用户手册, Ctrl+F 搜索 Table 15-4. Commonly Used Baud Rates 即可。

串口输出异常

前面提到我们直接使用三个变量保存当前时间,在输出时做进一步处理,转为字符串的过程中,我们进行了这样的操作:

Time[0] = hour / 10 + '0';

但有趣的事情是,在初始化之后,我们得到的输出是这样的: 0/:0/:0/ 。随手输出了一下 / 的 ASCII 码,发现它刚好比 0 小一。

难道说,存储器中的默认值不是 0 吗? Google 一下之后发现,还真的不是 0 。 MSP430G2553 中的 Flash 存储器在默认状态下的值全为 1 ,然后写入时只能将 1 置为 0 ,所以每一次写入数据都需要先清空再写入。那么问题来了,为什么全为 1 会导致最后输出的结果小 1 呢?我来简单的阐述一下我的理解:

假设这个存储器只有 8 位,也就是说,现在的值为 11111111 ,然后我加上一个 1 ,于是我们得到:

  11111111
+        1
 100000000

显然,我们最后的结果已经移除了,此时会产生截断,也就是说,存储器现在的数据变成了 00000000 ,也就是 0,跟我们期望的结果 1 刚好相差一。

当然,实际的情况要比我上面的举例要复杂的多,不过我想已经足够我们认识到这个 BUG 的本质,就不再多说啦。

Flash 存储器未清空

在测试中,我们发现每一次烧录程序之后, Flash 存储器不会清空,依然会从上一次我们保存的时间开始计时。我觉得这是正确的行为,没有在意,但是我妹纸和她的队友告诉我她们在完成上一个作业的时候每次都是会清空的。我对着这次和上次的代码研究了很久,认为代码里面根本就没有清空 Flash 存储器的操作,如果有的话,掉电保存这项功能根本无从谈起。我妹纸她们也同意我的分析,但是她们的实践确实证明了每次都会清空 Flash 存储区。

这个问题也困扰了很久,直到第二天,用别人的电脑重新烧录了一遍程序,发现他们的是会正常清空的。所以说,问题在于 CCS 的版本:我妹纸使用的 CCS 版本是 6.1 ,而 他们用的版本是 5.1.1 ,也就是说,不同版本的 CCS 在烧录程序期间的不同行为导致了这次错误。我们换用了 5.1.1 之后,成功解决了这个问题。

总结

对嵌入式开发有了初步的了解,向着真·全栈开发工程师又近了一步。

这一次的开发经历遇到了很多因缺思艇的问题,因为嵌入式开发本身比较偏向底层,这次开发甚至还遇到了存储器的存储原理。也有一点将自己看的 CSAPP 融会贯通的感觉,还是很有意思的。

更新日志

  • 2016年05月09日 首次发布