切换到宽版
爱科技/爱创意/爱折腾;电子/数码爱好者的家!欢迎访问新版数码之家网站
  • 26953阅读
  • 51回复

[STM]超级单片机万年历|STM32核心|完整二级中文字库|自定义铃声|自定义节日提醒【参赛】 [复制链接]

上一主题 下一主题
离线easterndigi
 

发帖
415
M币
4398
专家
14
粉丝
133
只看楼主 倒序阅读 我要置顶 楼主  发表于: 2012-10-06
我之前预告过我要做个单片机万年历,这个十一,我把它实现了,不过硬件方面缩水还是蛮大的。别看它的功能并不算非常强大,但是代码可一点都不简单。

【本帖目的】
由于我原材料的特殊性,导致它并不适合仿制。所以本帖重点在于能让您快速地了解它的原理和一些技巧,并运用到自己的制作中去。

【多图展示】

屏幕是二手的,所以有点划痕

背面为了好看(飞线太多),盖了一块PCB,按键处有开口

机身顶部的USB接口

开机Logo

主屏幕(背光是有按键按下时打开,5s后自动关闭)

设置界面,很简单

闹钟设置,可以设置在星期几响

时间设置,大家一看就明白

闹钟提示,铃声是可以从PC上下载的哦(WAV格式)

【硬件配置】
CPU:STM32F103C8
RAM:20KB
ROM:64KB
外部FLASH:2MB
LCD:无控制器320240

【软件功能】
* 1970-2100年公历计时
* 星期、农历自动计算
* 月历显示
* 节气自动提醒
* 可自定义节日提醒
* PC闹钟铃声下载
* 可选择星期的闹钟
* 可更换任意字体


【成本计算】
CPU 8.5RMB
FLASH 1.5RMB
LCD 10RMB
KEY 1RMB
XTAL 1RMB
PCB 5RMB
其它 2RMB
合计 29元

【设计说明】
    这次设计充分地考虑到了低成本。
    从处理器说起,这次选用的主处理器是STM32F103C8,32位ARM-Cortex M3架构,主频高达72MHz,片内资源也十分丰富,包括64KB ROM,20kB RAM,4个定时器,3个串口,3个SPI,1个RTC,1个USB,还有1个DMA。配置上完全符合要求,更重要的是,这么给力的芯片只卖8.5元/片(零售),比我们常用的STC12C5A60S2只贵了0.5元哦!顺便告诉你个秘密,STM32F101C8的晶片和STM32F103C8的晶片完全相同,两者可以直接代换,而STM32F101C8的价格只有6.5元/片!是不是很超值呢?
    再说说RTC。一般的做法都是使用DS1302加上32.768K晶振。得益于STM32处理器内置的RTC,这下我可以省去专用的RTC芯片(DS1302最便宜都要6毛钱呢,STM32比STC贵的5毛钱补回来了吧),甚至我可以把晶振给省了(内置带温度补偿的低速RC振荡器,可做为RTC时钟源)。
    屏幕方面,我用了320240的液晶,绝对面子够大。不过要注意,我用的是二手的无控制器屏,优点是价格便宜(我买来只花了10元,比12864都便宜哦)而且可以配合软件实现FRC 16级灰度,缺点就是需要专门的控制器,而控制器一点都不便宜,至少20元。但是如果为了这屏而去加控制器,这很明显是不划算的。所以怎么办呢?我们神奇的STM32又要发挥它的作用了。它的IO在72MHz主屏下可以达到36MHz的速度,而且片内20KB的RAM足够用来放LCD的图形缓冲,所以,用软件模拟时序把LCD给搞定了。
    我在标题里提到了完整二级中文字库,这是干啥子的叻?为了自定义节日提醒。所有的节日信息可以连接电脑更新,包括名称和日期。如果没有中文字库,这是不可能实现的。很多人在这方面选择了带字库的LCD(一般是ST7920控制的),但是我说了,低成本,所以选带字库的LCD肯定是不可能的。怎么办捏,64KB的ROM肯定是放不下的,所以就得增加片外FLASH了。我选择的是华邦的SPI FLASH,型号W25X16,容量2MByte,价格1.5元左右。SPI FLASH,顾名思义,就是用SPI通信的FLASH,SPI只有四根线,省IO,好焊接,而且配合STM32的SPI控制器,可以达到18Mbps的传输速度。事实上,二级中文字库大小只有212KB,那么这么大的FLASH还能实现什么呢?自定义WAV铃声,这是个很吃空间的玩意,不过有了2MB的FLASH,通通能实现!
    在连接PC的方式上,我本来是想用USB的,不过第一次用STM32,经验不足,USB方面硬件设计失误,只能用传统的串口了。不过串口通信STM32也是充分体现出了它NB的实力,配合DMA控制器,从电脑上往SPI FLASH上下载数据,速度高达15KB/s(STC的串口ISP下载速度仅为1.2KB/s)!传输完整的二级字库只需15秒,而传输铃声也可以在1分钟之内完成。

【上位机展示】

未连接时

写入铃声和字库的界面

自定义节日的界面

【难度所在】
    如果这个东西一点难度都没有,我也不可能拿来参赛。这个东西我遇到的最大难度就是如何在LCD稳定显示的同时完成该完成的工作。别看主频有72MHz,单色液晶可是需要大约80Hz的刷新率才能实现稳定显示,导致实际的CPU空闲只有20%左右。而且受限于ROM和RAM,不能使用操作系统(没玩过STM32的可能认为64KB ROM和20KB RAM相当大,毕竟51通常只有1KB的RAM。但是事实上STM32是32位RISC处理器,而51是8位CISC处理器,其代码密度相差甚远,STM32的64KB ROM只能相当于51的16KB ROM,20KB RAM也只能相当于51的6KB RAM,更别说我还要带个320240的液晶,瞬间10KB RAM就没了),导致在部分任务时,如何快速响应而且显示稳定就成了一个问题。不过可喜的是,因为LCD的图形缓冲在单片机内部,所以绘图相当快。

【完整代码】

上位机代码

下位机代码

【电路原理】







【制作过程】
1、我用的是某STM32F103C8应用板,和万年历无关,因为没有转接板,就拿它当最小系统用了



STM32F103C8T6是LQFP48封装,引脚间距0.5mm,焊接要快一点,不能用杜洋那种方法,他那种方法单片机肯定烧坏(STC:你看我质量比你好)
焊接好主芯片的样子(其实本来是要用右边那块转接板的,但是事实证明,我没对齐,把它弄短路了):

2、把它弄到屏幕后面去,屏幕我以前发过图的,这次再发一遍。

3、焊上SPI FLASH

4、完整背面

4、把背盖装上
背盖就是一PCB,顺便把喇叭粘一下




【重点代码解析】
尼玛,以为是中考题啊,还解析……为什么解析呢,因为代码很多,也很混乱,注释不多,阅读起来有一定难度。我把一些关键的代码挑出来,做了点解释。玩惯STM32的老鸟就不必仔细看了,对于STM32入门者还是有一定帮助的。
为了清晰,我们先来把源代码文件来梳理一下。
  1. CMSIS //Cortex-M单片机标准文件
  2. Startup //启动文件
  3. StdPeriph_Driver //库文件
  4. User //用户代码
启动文件和用户代码都好理解,那么CMSIS是什么呢?这是ARM规定的一个Cortex-M标准,所有的厂商都要遵循,其中对单片机的寄存器之类的东西做了一些定义,相当于51的AT89X52.H。而库文件是什么呢?它是用来降低你编程难度的。在51里,什么都得和寄存器打交道。而寄存器的操作实在不好记,所以就有了单片机小精灵之类的计算软件。库可以算是单片机上的计算软件,在51上因为硬件配置不够强大,这种做法一定会被认为是在浪费资源。而在STM32上,大多数情况下可以忽略库带来的硬件开支。

我们按照功能来分别讲解一下。
主要有以下几方面:
1、串口
2、液晶
3、RTC
4、WAV播放

串口方面的东西是在usart.c中完成的,我们可以来看下串口的初始化过程
  1. void USART1_Config(void)
  2. {
  3.   GPIO_InitTypeDef GPIO_InitStructure;
  4.   USART_InitTypeDef USART_InitStructure;
  5.   NVIC_InitTypeDef NVIC_InitStructure;
  6.   
  7.   /* config USART1 clock */
  8.   RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
  9.   /* USART1 GPIO config */
  10.   /* Configure USART1 Tx (PA.09) as alternate function push-pull */
  11.   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
  12.   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  13.   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  14.   GPIO_Init(GPIOA, &GPIO_InitStructure);    
  15.   /* Configure USART1 Rx (PA.10) as input floating */
  16.   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
  17.   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  18.   GPIO_Init(GPIOA, &GPIO_InitStructure);
  19.           
  20.   /* USART1 mode config */
  21.   USART_InitStructure.USART_BaudRate = 460800;
  22.   USART_InitStructure.USART_WordLength = USART_WordLength_8b;
  23.   USART_InitStructure.USART_StopBits = USART_StopBits_1;
  24.   USART_InitStructure.USART_Parity = USART_Parity_No;
  25.   USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  26.   USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  27.   USART_Init(USART1, &USART_InitStructure);
  28.   /*使能中断*/
  29.   USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
  30.   USART_Cmd(USART1, ENABLE);
  31.   
  32.   NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
  33.   NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  34.   NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  35.   NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  36.   NVIC_Init(&NVIC_InitStructure);
  37. }

    串口初始化代码难道也算重点么?是的,这不单单是串口的初始化方法,这基本上是STM32所有外设的初始化方法!
    看代码可以发现,虽然名义上是初始化串口,但是代码干了三件事情,初始化IO、初始化串口、初始化NVIC。为啥有这么多事呢?首先,串口输入输出要通过IO,那么如果IO不工作也是徒劳,所以要先初始化IO口。NVIC的全称是Nested Vector Interrupt Controller,中文嵌套向量中断控制器。初始化NVIC其实在这里就是决定串口的优先级,顺便允许串口接收中断(这在51里也是一样的)。
初始化这三样东西的方法大同小异,我们就以最主要的串口为例来说明好了。
    首先,通过RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);这句话来使能时钟。为啥要使能时钟呢?STM32内部外设很多,如果全部打开,那么会十分的费电。所以就有了RCC来管理各个外设的时钟。就像单片机没有时钟不能工作一样,外设没有时钟也不能工作。在上电复位后,所有外设的时钟都是切断的,必须要手动把它的时钟打开,才能工作。
之后,我们开始对一个叫GPIO_InitStructure的东西开始赋值。它的定义就在函数开头,仔细看下,你会发现他们的定义是如此地相似,知道为什么我说这就是STM32所有外设的初始化方法了吧?而下面赋值的内容更是充分地体现了库的优势,无需翻阅寄存器说明,无需使用计算器,只要按照你的想法赋值就OK了。最后用USART_Init(USART1, &USART_InitStructure); 把设置写入到寄存器,再用USART_Cmd(USART1, ENABLE);启用串口,搞定。一开始可能会觉得这麻烦,但是慢慢地,你会爱上这种编程方式的。
    串口初始化讲解结束。

    接下来我来说说为啥串口的传输能够如此之快。一方面,波特率达到了460800,是115200的4倍,速度自然快。另一方面,我使用了DMA控制器来传输数据,数据传输无需CPU介入,使得在传输时,CPU能同时擦除FLASH,为写入做准备。可能有人还不了解DMA,我来简单介绍下。DMA的就是一个独立于CPU核心的搬运工,它可以在内存之间搬运数据而无需CPU介入。说内存其实不准确,其实是整个STM32寻址空间,STM32能直接寻址的都能直接搬运。比如RAM和寄存器就是可以直接寻址的,而SPI FLASH必须要软件来发送地址,属于间接寻址,所以不能使用DMA。我这里,就是串口接收寄存器到RAM的搬运。我在RAM里开辟了一个接收缓冲区来接收数据。为了节约内存和提高速度,我在串口传输时关闭了液晶。这样不但可以腾出CPU,而且可以把LCD的图形缓冲区利用起来,变成一个超大的接收缓冲区。
代码我把关键的贴一下
  1. PCCon_EnableDMA();//初始化DMA
  2.       for (i=0;i<53;i++)
  3.       {
  4.         DMA1_Channel5->CCR &= (uint16_t)(~DMA_CCR1_EN);//设置前先关闭DMA
  5.         DMA1_Channel5->CNDTR=4096;//设置传输长度4096字节
  6.         DMA1_Channel5->CCR |= DMA_CCR1_EN;//重开DMA
  7.         USART1->DR = (0x52 & (uint16_t)0x01FF);
  8.         CurrAddr=i*4096;//1个Page 4096字节      
  9.      SPI_FLASH_SectorErase(CurrAddr);//擦除Flash
  10.         while((DMA1_Channel5->CNDTR)!=0);//等待DMA传输完成
  11.         SPI_FLASH_BufferWrite(LCD_Framebuffer,CurrAddr,4096);//写入SPI Flash
  12.       }
  13.       USART1_PutChar(0x53);//告诉PC传输结束
  14.       PCCon_DisableDMA();//关闭DMA
    其中速度敏感区域我直接操作的寄存器,寄存器操作代码也是直接从库函数里复制来的。

    LCD的驱动其实还是比较简单的,就是模拟时序来刷新屏幕。可能有人不理解为什么要刷新。其实LCD的电路结构类似于我们的LED点阵,是分行列线的,只有不断地刷新才能显示图像。要驱动LCD,需要有控制器和驱动器来配合。驱动器就好比LED点阵上用的锁存器,而控制器就是控制锁存器的单片机。一般LCD的点阵较大,直接用单片机来控制太费资源,速度也不够,所以通常都有专门的控制器来控制液晶。我的液晶没有控制器,只有驱动器,控制这部分就交给单片机了。原理都说到这份上了,应该明白要怎么控制了吧,把图形数据不断地送到锁存器里就OK了。代码不复杂,可以参考我以前的帖子,也可以下载完整的代码看。其实凭自己理解写写也不是很困难的事吧。

    而RTC则比较有意思,我利用RTC的备份寄存器(这些寄存器的值和时间是生死与共的,如果备份电池没电了,它们的数据是一起丢失的)来确定需不需要重新设置时间。
  1. NVIC_RTC_Configuration();
  2.   if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
  3.   {
  4.     printf("\r\n\n RTC not yet configured....");
  5.     /* RTC Configuration */
  6.     RTC_Configuration();
  7.     printf("\r\n RTC configured....");
  8.     /* Adjust time by users typed on the hyperterminal */
  9.     Guide_FirstUse();
  10.     BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
  11.     /*等待寄存器同步*/
  12.     RTC_WaitForSynchro();
  13.     /*等待上次RTC寄存器写操作完成*/
  14.     RTC_WaitForLastTask();
  15.     Delayms(200);
  16.     //NVIC_SystemReset();
  17.   }
  18.   else
  19.   {
  20.     /*启动无需设置新时钟*/
  21.     /*等待寄存器同步*/
  22.     RTC_WaitForSynchro();
  23.     /*允许RTC秒中断*/
  24.     RTC_ITConfig(RTC_IT_SEC, ENABLE);
  25.     /*等待上次RTC寄存器写操作完成*/
  26.     RTC_WaitForLastTask();
  27.   }

代码还是很好理解的,就是读取BKP_DR1寄存器的值(这里用库来读取,当然你也可以手动定位到BKP_DR1的地址去读取),如果是A5A5,那么说明时间已经设置过了,如果是其它值,说明时间需要设置。

WAV播放方面,原理我也不仔细讲了,可以参考我另外一个参赛帖子:http://bbs.mydigit.cn/read.php?tid=461709
实现方面,我用了两个定时器,一个定时器用来输出PWM,另外一个每秒中断11052次,将WAV数据从SPI FLASH中读出,送至PWM。

这样我个人认为的一些重点代码就讲解完了,如果有什么不足,可以在下面评论中指出。


【总结】
事实上这个东西并不完善,在软件上还有很多可扩展的地方。由于时间问题,我并没有加上。希望从这个作品中,大家可以了解STM32,学习STM32,用它做出更给力的东西




[ 此帖被nbzwt在2012-10-07 07:50重新编辑 ]
本文内容包含图片或附件,获取更多资讯,请 登录 后查看;或者 注册 成为会员获得更多权限
本帖最近打赏记录:共14条打赏M币+100
12
离线pangjineng

发帖
2895
M币
6807
专家
5
粉丝
51
只看该作者 1楼 发表于: 2012-10-06
Re:超级单片机万年历|STM32核心|完整二级中文字库|自定义铃声|自定义节日提醒【参 ..
屏幕显示的内容视乎少了些……
离线easterndigi

发帖
415
M币
4398
专家
14
粉丝
133
只看该作者 2楼 发表于: 2012-10-06
回 1楼(pangjineng) 的帖子
功能尚不完善,有进一步扩展空间
离线sdf15937

发帖
1747
M币
14
专家
8
粉丝
61
只看该作者 3楼 发表于: 2012-10-06
Re:超级单片机万年历|STM32核心|完整二级中文字库|自定义铃声|自定义节日提醒【参 ..
好奢侈的电子钟……
玩不来STM
51比较适合我……
离线a931948882

发帖
1772
M币
1898
专家
1
粉丝
88
只看该作者 4楼 发表于: 2012-10-06
Re:超级单片机万年历|STM32核心|完整二级中文字库|自定义铃声|自定义节日提醒【参 ..
是DELPHI写的程序吧
离线zz0215

发帖
22285
M币
10216
专家
2
粉丝
57
只看该作者 5楼 发表于: 2012-10-06
Re:超级单片机万年历|STM32核心|完整二级中文字库|自定义铃声|自定义节日提醒【参 ..
既然打板了,按键怎么不一起做呢,这样不美观啊。

发帖
168
M币
2832
专家
0
粉丝
12
只看该作者 6楼 发表于: 2012-10-06
Re:超级单片机万年历|STM32核心|完整二级中文字库|自定义铃声|自定义节日提醒【参 ..
我只练过单片机上的万年历,这样做出实物,很少见到,所以,多谢楼主
离线easterndigi

发帖
415
M币
4398
专家
14
粉丝
133
只看该作者 7楼 发表于: 2012-10-06
回 5楼(zz0215) 的帖子
板子不是给万年历做的,所以很多东西没有考虑到。难看确实那难看了点
离线easterndigi

发帖
415
M币
4398
专家
14
粉丝
133
只看该作者 8楼 发表于: 2012-10-06
回 4楼(a931948882) 的帖子
是的,图标暴露了一切
离线easterndigi

发帖
415
M币
4398
专家
14
粉丝
133
只看该作者 9楼 发表于: 2012-10-06
回 3楼(sdf15937) 的帖子
看起来奢侈,实际上就花了我30,比大多数12864时钟都便宜
快速回复
限80 字节
“新手上路”发帖需审核后才能显示(请认真发帖),达到数码9级后取消此限制
 
上一个 下一个