基于MSP430G2553的打铃系统开发手记
前言
我萌萌哒的妹纸是一个代码苦手,完全无法理解 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。这个问题调试了很久,拿着原来的代码逐行比对之后发现,出了这样的问题:
1234- UCA0BR0 = 130;- UCA0BR1 = 6;+ UCA0BR0 = 104;+ UCA0BR1 = 0;Google 一下才明白,原来 UCA0BR0 和 UCA0BR1 是由系统的时钟速度和波特率决定的值,如果设置错误就会导致串口发送失败。具体的值可以参考用户手册, Ctrl+F 搜索 Table 15-4. Commonly Used Baud Rates 即可。
串口输出异常
前面提到我们直接使用三个变量保存当前时间,在输出时做进一步处理,转为字符串的过程中,我们进行了这样的操作:
1Time[0] = hour / 10 + '0';但有趣的事情是,在初始化之后,我们得到的输出是这样的: 0/:0/:0/ 。随手输出了一下/ 的 ASCII 码,发现它刚好比 0 小一。
难道说,存储器中的默认值不是 0 吗? Google 一下之后发现,还真的不是 0 。 MSP430G2553 中的 Flash 存储器在默认状态下的值全为 1 ,然后写入时只能将 1 置为 0 ,所以每一次写入数据都需要先清空再写入。那么问题来了,为什么全为 1 会导致最后输出的结果小 1 呢?我来简单的阐述一下我的理解:
假设这个存储器只有 8 位,也就是说,现在的值为 11111111 ,然后我加上一个 1 ,于是我们得到:
12311111111+ 1100000000显然,我们最后的结果已经移除了,此时会产生截断,也就是说,存储器现在的数据变成了00000000 ,也就是 0,跟我们期望的结果 1 刚好相差一。
当然,实际的情况要比我上面的举例要复杂的多,不过我想已经足够我们认识到这个 BUG 的本质,就不再多说啦。
Flash 存储器未清空
在测试中,我们发现每一次烧录程序之后, Flash 存储器不会清空,依然会从上一次我们保存的时间开始计时。我觉得这是正确的行为,没有在意,但是我妹纸和她的队友告诉我她们在完成上一个作业的时候每次都是会清空的。我对着这次和上次的代码研究了很久,认为代码里面根本就没有清空 Flash 存储器的操作,如果有的话,掉电保存这项功能根本无从谈起。我妹纸她们也同意我的分析,但是她们的实践确实证明了每次都会清空 Flash 存储区。
这个问题也困扰了很久,直到第二天,用别人的电脑重新烧录了一遍程序,发现他们的是会正常清空的。所以说,问题在于 CCS 的版本:我妹纸使用的 CCS 版本是 6.1 ,而
他们用的版本是 5.1.1 ,也就是说,不同版本的 CCS 在烧录程序期间的不同行为导致了这次错误。我们换用了 5.1.1 之后,成功解决了这个问题。
总结
对嵌入式开发有了初步的了解,向着真·全栈开发工程师又近了一步。
这一次的开发经历遇到了很多因缺思艇的问题,因为嵌入式开发本身比较偏向底层,这次开发甚至还遇到了存储器的存储原理。也有一点将自己看的 CSAPP 融会贯通的感觉,还是很有意思的。
「硬件小百科」单片机定时器与计数器
计数概念的引入
从选票的统计谈起:画“正”。这就是计数,生活中计数的例程处处可见。例:录音机上的计数器、家里面用的电度表、汽车上的里程表等等,再举一个工业生产中的例程,线缆行业在电线生产出来之后要计米,也就是测量长度,怎么测法呢?用尺量?不现实,太长不说,要一边做一边量呢,怎么办呢?行业中有很巧妙的办法,用一个周长是1米的轮子,将电缆绕在上面一周,由线带轮转,这样轮转一周不就是线长1米嘛,所以只要记下轮转了多少圈,就能知道走过的线有多长了。
计数器的容量
从一个生活中的例程看起:一个水盆在水龙头下,水龙没关紧,水一滴滴地滴入盆中。水滴持续落下,盆的容量是有限的,过一段时间之后,水就会逐渐变满。录音机上的计数器最多只计到999….那么单片机中的计数器有多大的容量呢?8031单片机中有两个计数器,分别称之为T0和T1,这两个计数器分别是由两个8位的RAM单元组成的,即每个计数器都是16位的计数器,最大的计数量是65536。
定时
8031中的计数器除了能作为计数之用外,还能用作时钟,时钟的用途当然很大,如打铃器,电视机定时关机,空调定时开关等等,那么计数器是如何作为定时器来用的呢?
一个闹钟,我将它定时在1个小时后闹响,换言之,也能说是秒针走了(3600)次,所以时间就转化为秒针走的次数的,也就是计数的次数了,可见,计数的次数和时间之间的确十分相关。那么它们的关系是什么呢?那就是秒针每一次走动的时间正好是1秒。
<单片机定时器记数器结构>
结论:只要计数脉冲的间隔相等,则计数值就代表了时间的流逝。由此,单片机中的定时器和计数器是一个东西,只不过计数器是记录的外界发生的事情,而定时器则是由单片机供给一个非常稳定的计数源。那么供给组定时器的是计数源是什么呢?看图1,原来就是由单片机的晶体震荡器经过12分频后获得的一个脉冲源。晶体震荡器的频率当然很准,所以这个计数脉冲的时间间隔也很准。问题:一个12M的晶体震荡器,它供给给计数器的脉冲时间间隔是多少呢?当然这很不难,就是12M/12等于1M,也就是1个微秒。结论:计数脉冲的间隔与晶体震荡器有关,12M的晶体震荡器,计数脉冲的间隔是1微秒。
溢出
让我们再来看水滴的例程,当水持续落下,盆中的水持续变满,最终有一滴水使得盆中的水满了。这个时候如果再有一滴水落下,就会发生什么现象?水会漫出来,用个术语来讲就是“溢出”。
水溢出是流到地上,而计数器溢出后将使得TF0变为“1”。至于TF0是什么我们稍后再谈。一旦TF0由0变成1,就是产生了变化,产生了变化就会引发事件,就象定时的时间一到,闹钟就会响一样。至于会引发什么事件,我们下次课再介绍,现在我们来研究另一个问题:要有多少个计数脉冲才会使TF0由0变为1。
任意定时及计数的办法
刚才已研究过,计数器的容量是16位,也就是最大的计数值到65536,因此计数计到65536就会产生溢出。这个没有问题,问题是我们现实生活中,经常会有少于65536个计数值的要求,如包装线上,一打为12瓶,一瓶药片为100粒,怎么样来满足这个要求呢?
提示:如果是一个空的盆要1万滴水滴进去才会满,我在开始滴水之前就先放入一勺水,还需要10000滴嘛?对了,我们采用预置数的办法,我要计100,那我就先放进65436,再来100个脉冲,不就到了65536了吗。定时也是如此,每个脉冲是1微秒,则计满65536个脉冲需时65.536毫秒,但现在我只要10毫秒就能了,怎么办?10个毫秒为10000个微秒,所以,只要在计数器里面放进55536就能了。
以上所有信息仅作为学习交流使用,不作为任何学习和商业标准。若您对文中任何信息有异议,欢迎随时提出,谢谢!
关于云创硬见
云创硬见是国内最具特色的电子工程师社区,融合了行业资讯、社群互动、培训学习、活动交流、设计与制造分包等服务,以开放式硬件创新技术交流和培训服务为核心,连接了超过30万工程师和产业链上下游企业,聚焦电子行业的科技创新,聚合最值得关注的产业链资源, 致力于为百万工程师和创新创业型企业打造一站式公共设计与制造服务平台。
相关问答
求一张声控开关电路图- 一起装修网[回答]朋友的目的是为了学习吧我知有几种做法,前三十几年因为当时的条件关系有厂家把现成数字钟芯片进行倒译码以矩阵接点引出由用户设*间供学校打铃用。...