我之前预告过我要做个单片机万年历,这个十一,我把它实现了,不过硬件方面缩水还是蛮大的。别看它的功能并不算非常强大,但是代码可一点都不简单。
【本帖目的】
由于我原材料的特殊性,导致它并不适合仿制。所以本帖重点在于能让您快速地了解它的原理和一些技巧,并运用到自己的制作中去。
【多图展示】
屏幕是二手的,所以有点划痕
背面为了好看(飞线太多),盖了一块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入门者还是有一定帮助的。
为了清晰,我们先来把源代码文件来梳理一下。
- CMSIS //Cortex-M单片机标准文件
- Startup //启动文件
- StdPeriph_Driver //库文件
- User //用户代码
启动文件和用户代码都好理解,那么CMSIS是什么呢?这是ARM规定的一个Cortex-M标准,所有的厂商都要遵循,其中对单片机的寄存器之类的东西做了一些定义,相当于51的AT89X52.H。而库文件是什么呢?它是用来降低你编程难度的。在51里,什么都得和寄存器打交道。而寄存器的操作实在不好记,所以就有了单片机小精灵之类的计算软件。库可以算是单片机上的计算软件,在51上因为硬件配置不够强大,这种做法一定会被认为是在浪费资源。而在STM32上,大多数情况下可以忽略库带来的硬件开支。
我们按照功能来分别讲解一下。
主要有以下几方面:
1、串口
2、液晶
3、RTC
4、WAV播放
串口方面的东西是在usart.c中完成的,我们可以来看下串口的初始化过程
- void USART1_Config(void)
- {
- GPIO_InitTypeDef GPIO_InitStructure;
- USART_InitTypeDef USART_InitStructure;
- NVIC_InitTypeDef NVIC_InitStructure;
-
- /* config USART1 clock */
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
- /* USART1 GPIO config */
- /* Configure USART1 Tx (PA.09) as alternate function push-pull */
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
- GPIO_Init(GPIOA, &GPIO_InitStructure);
- /* Configure USART1 Rx (PA.10) as input floating */
- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
- GPIO_Init(GPIOA, &GPIO_InitStructure);
-
- /* USART1 mode config */
- USART_InitStructure.USART_BaudRate = 460800;
- USART_InitStructure.USART_WordLength = USART_WordLength_8b;
- USART_InitStructure.USART_StopBits = USART_StopBits_1;
- USART_InitStructure.USART_Parity = USART_Parity_No;
- USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
- USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
- USART_Init(USART1, &USART_InitStructure);
- /*使能中断*/
- USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
- USART_Cmd(USART1, ENABLE);
-
- NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
- NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
- NVIC_Init(&NVIC_InitStructure);
- }
串口初始化代码难道也算重点么?是的,这不单单是串口的初始化方法,这基本上是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的图形缓冲区利用起来,变成一个超大的接收缓冲区。
代码我把关键的贴一下
- PCCon_EnableDMA();//初始化DMA
- for (i=0;i<53;i++)
- {
- DMA1_Channel5->CCR &= (uint16_t)(~DMA_CCR1_EN);//设置前先关闭DMA
- DMA1_Channel5->CNDTR=4096;//设置传输长度4096字节
- DMA1_Channel5->CCR |= DMA_CCR1_EN;//重开DMA
- USART1->DR = (0x52 & (uint16_t)0x01FF);
- CurrAddr=i*4096;//1个Page 4096字节
- SPI_FLASH_SectorErase(CurrAddr);//擦除Flash
- while((DMA1_Channel5->CNDTR)!=0);//等待DMA传输完成
- SPI_FLASH_BufferWrite(LCD_Framebuffer,CurrAddr,4096);//写入SPI Flash
- }
- USART1_PutChar(0x53);//告诉PC传输结束
- PCCon_DisableDMA();//关闭DMA
其中速度敏感区域我直接操作的寄存器,寄存器操作代码也是直接从库函数里复制来的。
LCD的驱动其实还是比较简单的,就是模拟时序来刷新屏幕。可能有人不理解为什么要刷新。其实LCD的电路结构类似于我们的LED点阵,是分行列线的,只有不断地刷新才能显示图像。要驱动LCD,需要有控制器和驱动器来配合。驱动器就好比LED点阵上用的锁存器,而控制器就是控制锁存器的单片机。一般LCD的点阵较大,直接用单片机来控制太费资源,速度也不够,所以通常都有专门的控制器来控制液晶。我的液晶没有控制器,只有驱动器,控制这部分就交给单片机了。原理都说到这份上了,应该明白要怎么控制了吧,把图形数据不断地送到锁存器里就OK了。代码不复杂,可以参考我以前的帖子,也可以下载完整的代码看。其实凭自己理解写写也不是很困难的事吧。
而RTC则比较有意思,我利用RTC的备份寄存器(这些寄存器的值和时间是生死与共的,如果备份电池没电了,它们的数据是一起丢失的)来确定需不需要重新设置时间。
- NVIC_RTC_Configuration();
- if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
- {
- printf("\r\n\n RTC not yet configured....");
- /* RTC Configuration */
- RTC_Configuration();
- printf("\r\n RTC configured....");
- /* Adjust time by users typed on the hyperterminal */
- Guide_FirstUse();
- BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
- /*等待寄存器同步*/
- RTC_WaitForSynchro();
- /*等待上次RTC寄存器写操作完成*/
- RTC_WaitForLastTask();
- Delayms(200);
- //NVIC_SystemReset();
- }
- else
- {
- /*启动无需设置新时钟*/
- /*等待寄存器同步*/
- RTC_WaitForSynchro();
- /*允许RTC秒中断*/
- RTC_ITConfig(RTC_IT_SEC, ENABLE);
- /*等待上次RTC寄存器写操作完成*/
- RTC_WaitForLastTask();
- }
代码还是很好理解的,就是读取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重新编辑 ]