搜索
bottom↓
回复: 242

百为STM32开发板教程——从LED流水灯到UCGUI手机界面

  [复制链接]

出0入0汤圆

发表于 2013-7-27 05:29:36 | 显示全部楼层 |阅读模式
本帖最后由 xi_liang 于 2013-7-27 06:09 编辑

首先介绍下百为STM3210E-EVAL开发板,
百为STM3210E-EVAL开发板兼容官方STM3210E-EVAL开发板,
几乎所有官方程序,包括2.0固件库里的86个例程,3.5固件库里的94个例程都可以直接运行。
另外有官方提供的ST_GUI_LIB,USB库,Application notes里的十几个程序都可以直接运行。
除此之外,我们另外编写和移植了几十个重量级程序,每个程序投入了无数个通宵时间和精力。这其中包括STM32+WIFI驱动,网络收音机,UCGUI手机界面。

虽然我认为最好的资料是官方的数据手册和例程,但是大众喜欢的是白话文式的教程。所以接着下来会一直更新这个教程,从LED流水灯到UCGUI手机界面。
建议学习先从《一个内幕者对STM32的介绍》开始,然后再到这里的教程。

先把已经编写好的部分PDF教程上传,后面再继续更新













后面更新的PDF教程将放在以下网盘地址:
http://pan.baidu.com/share/link? ... 3&uk=4063753675

最新更新程序网盘下载地址:
http://pan.baidu.com/share/link? ... 6&uk=4063753675

网盘截图:



配套视频教程网盘地址:
http://pan.baidu.com/share/link?shareid=396282&uk=4063753675


开发板其他资源汇总:
http://www.amobbs.com/thread-5539467-1-1.html

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

阿莫论坛20周年了!感谢大家的支持与爱护!!

曾经有一段真挚的爱情摆在我的面前,我没有珍惜,现在想起来,还好我没有珍惜……

出0入0汤圆

发表于 2013-7-27 07:10:17 | 显示全部楼层
开源到这个程度,实属不易了,支持

出0入0汤圆

发表于 2013-7-27 07:16:10 | 显示全部楼层
程序已经可以使用,不过设置DEMO 程序多了会死机而已,触摸可以用但不稳定,正在调试中,先传上来,给大家做个参考,哈哈

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

发表于 2013-7-27 07:17:25 | 显示全部楼层
本帖最后由 ersha4877 于 2013-7-27 07:19 编辑

上面这个在百为的板子上直接使用,无问题 是EMWIN5.20

出0入0汤圆

发表于 2013-7-27 08:11:52 来自手机 | 显示全部楼层
顶起,赞一个!

出0入0汤圆

发表于 2013-7-27 08:14:17 | 显示全部楼层
谢谢楼主分享~~

出0入0汤圆

发表于 2013-7-27 08:16:37 | 显示全部楼层
tinwy_zhang 发表于 2013-7-27 08:14
谢谢楼主分享~~

+1 谢谢马上下载。

出0入0汤圆

发表于 2013-7-27 10:14:19 | 显示全部楼层
谢谢楼主分享!

出0入0汤圆

发表于 2013-7-27 10:17:28 | 显示全部楼层
支持下,

出0入0汤圆

发表于 2013-7-27 11:03:02 来自手机 | 显示全部楼层
楼主不易 狂顶一下

出0入0汤圆

发表于 2013-7-27 11:30:26 | 显示全部楼层
正下载 到板子试试

出0入0汤圆

发表于 2013-7-27 12:06:20 | 显示全部楼层
感觉还可以啊.

出0入0汤圆

发表于 2013-7-27 12:26:04 | 显示全部楼层
楼主很给力啊,支持

出0入0汤圆

 楼主| 发表于 2013-7-28 09:50:47 | 显示全部楼层
ersha4877 发表于 2013-7-27 07:16
程序已经可以使用,不过设置DEMO 程序多了会死机而已,触摸可以用但不稳定,正在调试中,先传上来,给大家 ...

有个很多开发板都存在的光标残影问题我找到解决办法了,把FSMC时序时间延长下就可以了:

把LCD_FSMCConfig()函数里面的
  p.FSMC_AddressSetupTime = 0;
  p.FSMC_AddressHoldTime = 0;
  p.FSMC_DataSetupTime = 1;
改为
  p.FSMC_AddressSetupTime = 1;
  p.FSMC_AddressHoldTime = 0;
  p.FSMC_DataSetupTime = 2;

出0入0汤圆

 楼主| 发表于 2013-7-28 09:51:50 | 显示全部楼层
airwolf09921 发表于 2013-7-27 11:03
楼主不易 狂顶一下

多谢帮顶,发上来大家顶下才有动力继续写下去啊

出0入0汤圆

发表于 2013-7-28 10:26:50 来自手机 | 显示全部楼层
辛苦了
板子不错
一定要顶

出0入0汤圆

发表于 2013-7-28 10:38:47 | 显示全部楼层
更新了这么多,搞完比赛再回来搞吧!

出0入0汤圆

发表于 2013-7-28 10:40:29 | 显示全部楼层
立马下载,谢谢楼主!

出0入0汤圆

发表于 2013-7-28 10:52:31 | 显示全部楼层
楼主太敬业了  五点半发帖 是还没睡  还是已经起来了 谢谢楼主

出0入0汤圆

发表于 2013-7-28 11:09:44 | 显示全部楼层
我就用的这个板,非常好,楼主加油。

出0入0汤圆

发表于 2013-7-28 11:19:30 | 显示全部楼层
持续关注之。。。。。。

出50入0汤圆

发表于 2013-7-28 11:42:46 来自手机 | 显示全部楼层
看得出楼主用心在做百为这款开发板,支持了!

出0入0汤圆

发表于 2013-7-28 13:03:34 | 显示全部楼层
一直关注楼主,支持了。

出0入0汤圆

 楼主| 发表于 2013-7-28 14:49:33 | 显示全部楼层
omityoung 发表于 2013-7-28 10:52
楼主太敬业了  五点半发帖 是还没睡  还是已经起来了 谢谢楼主

呵呵,好多程序都是通宵写的,不过晚上写程序感觉挺好

出0入0汤圆

发表于 2013-7-28 15:19:52 | 显示全部楼层
楼主很给力

出0入0汤圆

发表于 2013-7-28 16:59:52 | 显示全部楼层
xi_liang 发表于 2013-7-28 14:49
呵呵,好多程序都是通宵写的,不过晚上写程序感觉挺好

注意身体啊 虽然夜里加班安静 有感觉 但长期下去身体吃不消的 感觉lz也不年轻了吧 多注意注意身体

出0入0汤圆

 楼主| 发表于 2013-7-29 10:15:00 | 显示全部楼层
贴上网页版的程序,后面继续更新

百为STM32开发板教程之一——LED流水灯

LED流水灯是单片机世界里的hello world,我们学习一门新的MCU都是从它开始。下面就介绍一下怎么点亮百为STM3210E-EVAL开发板的LED灯

其实这个LED流水灯并不简单,要理解整个程序,建议大家看下这个:我翻译的书《一个内幕者对STM32的介绍》

但刚开始学,书也不一定看得明白,所以在这里我以实例给大家演示一下

看下百为STM3210E-EVAL开发板上LED的电路图:








开发板上4个LED是接到STM32的4个IO口上的,要点亮LED,只要在这4个IO上输出对应的高电平

那如何让STM32在PF6~PF9这4个IO上输出高点平呢
首先要看下《STM32F10xxx参考手册CD00171190.pdf》里关于IO端口的介绍,第8章 通用和复用功能I/O(GPIO和AFIO)

百为STM3210E-EVAL开发板上的CPU是STM32F103ZET6,有GPIOA~GPIOG七组IO,每组IO有16个引脚GPIO_Pin_0~GPIO_Pin_15,如板上的PF0~PF15

其中每个IO端口有2个32位的寄存器(GPIOx_CRL和GPIOx_CRH)配置,每个引脚由其中4位进行配置,
4位字段是由一个两位的配置字段和一个两位的模式字段组成




要设置这4位,要看下GPIOx_CRL,GPIOx_CRH的寄存器定义





由上图可以看出GPIOF_CRL的31~28位是设置PF7的,GPIOF_CRL的27~24位是设置PF6的
GPIOF_CRH的3~0位是设置PF8的,GPIOF_CRH的7~4位是设置PF6的

这里我们把每个引脚设置为
CNF[1:0]    = 00:通用推挽输出模式
MODE[1:0] = 11 : 输出模式,最大速度50MH

GPIOF_CRL,GPIOF_CRH的寄存器地址分别是0x40011C00,0x40011C04
这个寄存器是怎么得来的,STM32数据手册里并没有直接给出,在《一个内幕者对STM32的介绍》4.1.1 寄存器地址查阅 这里有介绍怎么通过上面的偏移地址算出。

配置好,我们就可以直接在端口输出数据寄存器GPIOF_ODR输出高电平控制LED了,GPIOF_ODR有16位,每个位对应GPF的一个引脚,PF0~PF15




下面就可以编写LED流水灯程序了,激动啊,等了好久终于等到今天

/* main.c */
#define GPIOF_CRL  (*(volatile unsigned int *)0x40011C00)        //端口配置低寄存器
#define GPIOF_CRH  (*(volatile unsigned int *)0x40011C04)        //端口配置高寄存器
#define GPIOF_ODR  (*(volatile unsigned int *)0x40011C0C)        //端口输出数据寄存器
#define RCC_APB2ENR  (*(volatile unsigned int *)0x40021018)        //

void delay(void)
{
        unsigned int i;
        for(i=0;i<500000;i++);
}
int main()
{
        int i;
        RCC_APB2ENR |= 1<<7; //打开GPIOF的时钟
        GPIOF_CRL = ( GPIOF_CRL & 0x00ffffff ) | 0x33000000; //配置PF6,PF7为通用推挽输出模式,最大速度50MH
        GPIOF_CRH = ( GPIOF_CRH & 0xffffff00 ) | 0x00000033; //配置PF8,PF9为通用推挽输出模式,最大速度50MH

        while(1)
        {
                for(i=6;i<=9;i++)
                {
                        GPIOF_ODR = ( GPIOF_ODR & 0xfffffc3f ) | ( 1<<i );  //在PF6~PF9引脚轮流输出高电平
                        delay();
                }
        }
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

发表于 2013-7-29 10:40:21 | 显示全部楼层
支持一下!!

出0入0汤圆

 楼主| 发表于 2013-7-29 10:43:42 | 显示全部楼层
百为STM32开发板教程之二——RCC时钟系统

STM32主要有5个时钟源:
HSI:内部高速时钟,8Mhz左右
HSE:外部高速时钟,即百为STM32开发板上的X1,8Mhz晶振
PLL:PLL时钟,可以是HSI或HSE的倍频,倍频后可达到72Mhz

LSI:内部低速时钟,40Khz左右
LSE:外部低速时钟,即百为STM32开发板上的X2,32.768Khz晶振,用来驱动RTC时钟

因为内部时钟不太准确,所以我们一般都是用外部时钟,系统时钟一般是通过HSE倍频后得到72Mhz。
选择哪个时钟,设置什么频率是由程序来决定的。




上面是STM32的时钟源框图,可以大致看出STM32各模块的时钟源
这个图其实就是STM32F10xxx参考手册CD00171190.pdf里第56页的图8 时钟树的简化版,如下图,图8描述得更详细一点,多了FSMC,SDIO,SPI/I2S等的时钟源。




上面的图8还不够仔细,因为还有APB1,APB2对应的其他外设还没列出来。
完整的APB1,APB2对应的外设模块在STM32F10xxx参考手册CD00171190.pdf的第二章 2.1 系统构架里有列了出来,如图1 系统结构:




STM32的各个模块都有对应的时钟源,当要使用模块时,要把对应的时钟打开。
上面的图一下子看觉得很复杂,没关系,暂时不需要全部了解的,等要学习其他模块的时候再回来看这些图,会有不同理解的。
我们现在的目的主要是把系统时钟设置为72Mhz。具体设置看下面程序和注释。

寄存器版本,仅作为学习理解用,一般编写程序是用后面的库函数版本:
/* main.c */

void delay(int time)
{
        int i,j;
        for(i=0; i<time; i++)
                for(j=0; j<1000; j++);
}
int main()
{
        int i;

        RCC_CR |= (u32)0x00010000;  //系统上电默认是用HSI内部高速时钟作为系统时钟,我们需要置位RCC控制寄存器的HSEON位打开HSE外部高速时钟
        while((RCC_CR & ( 1<<17 )) == 0);  //判断HSERDY位,若为1,表示HSE外部高速时钟已打开
        FLASH_ACR |= 0x00000010;           //使能预取指缓冲
        FLASH_ACR = ( FLASH_ACR & ((u32)0xfffffff8) ) | 0x00000002;  //设置FLASH为2个等待状态
        RCC_CFGR &= (u32)0xFFFFFF0F;        //设置HCLK时钟为SYSCLK   
        RCC_CFGR &= (u32)0xFFFFC7FF;        //设置PCLK2时钟为HCLK  
        RCC_CFGR = ( RCC_CFGR & ((u32)0xFFFFF8FF) ) | (u32)0x00000400;  //设置PCLK1时钟为HCLK/2
        RCC_CFGR |= 0x00010000 | 0x001C0000;      //设置PLL倍频为9,PLLCLK = 8MHz * 9 = 72 MHz
        RCC_CR |= 1<<24;                              //打开PLL时钟
        while((RCC_CR & (1<<25)) == 0);        //等待PLL时钟打开
        RCC_CFGR = ( RCC_CFGR & ((u32)0xFFFFFFFC) ) | 0x00000002;  //设置PLL时钟作为系统时钟源,取代上面设置的HSE时钟。
        while((RCC_CFGR & 0x0000000C) != 0x08);      //等待PLL设置为系统时钟,此时系统已经成功跑在72MHz了

        RCC_APB2ENR |= 1<<7; //打开GPIOF的时钟
        GPIOF_CRL = ( GPIOF_CRL & 0x00ffffff ) | 0xcc000000; //配置PF6,PF7为通用推挽输出模式,最大速度50MH
        GPIOF_CRH = ( GPIOF_CRH & 0xffffff00 ) | 0x000000cc; //配置PF8,PF9为通用推挽输出模式,最大速度50MH
        while(1)
        {
                for(i=6;i<=9;i++)
                {
                        GPIOF_ODR = ( GPIOF_ODR & 0xfffffc3f ) | ( 1<<i );  //在PF6~PF9引脚轮流输出高电平
                        delay(1000);
                }
        }

}

库函数版本:
/* main.c */

#include "stm32f10x_lib.h"

void Delay(vu32 nCount)
{
  for(; nCount != 0; nCount--);
}

void RCC_Configuration(void)
{
        RCC_DeInit();  //RCC系统复位,仅在调试时用(如JLINK调试)
        RCC_HSEConfig(RCC_HSE_ON);  //打开HSE外部8Mhz高速时钟
        HSEStartUpStatus = RCC_WaitForHSEStartUp();  //等待HSE时钟打开
        if(HSEStartUpStatus == SUCCESS)   //若HSE时钟打开成功
        {
                FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);  //使能FLASH缓冲
                FLASH_SetLatency(FLASH_Latency_2);   //设置FLASH为2个等待状态  
                RCC_HCLKConfig(RCC_SYSCLK_Div1);   //设置HCLK时钟为SYSCLK
                RCC_PCLK2Config(RCC_HCLK_Div1);     //设置PCLK2时钟为HCLK
                RCC_PCLK1Config(RCC_HCLK_Div2);    //设置PCLK1时钟为HCLK/2
                RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9);  //设置PLL倍频为9,PLLCLK = 8MHz * 9 = 72 MHz
                RCC_PLLCmd(ENABLE);                        //打开PLL时钟
                while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET)  //等待PLL时钟打开
                {
                }
                RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);  //设置PLL时钟作为系统时钟源,取代上面设置的HSE时钟
                while(RCC_GetSYSCLKSource() != 0x08)  //等待PLL设置为系统时钟,此时系统已经成功跑在72MHz了
                {
                }
        }
}

int main()
{
        RCC_Configuration();  //配置系统时钟跑在72Mhz等
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF, ENABLE);  //打开LED时钟,即GPIOF时钟
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init(GPIOF, &GPIO_InitStructure);  //配置PF6~PF9为通用推挽输出模式,最大速度50MH
        while (1)
        {
                GPIO_SetBits(GPIO_LED, GPIO_Pin_6);  //点亮LED1
                Delay(0xAFFFF);
                GPIO_ResetBits(GPIO_LED, GPIO_Pin_6);//灭掉LED1

                GPIO_SetBits(GPIO_LED, GPIO_Pin_7);  //点亮LED2
                Delay(0xAFFFF);
                GPIO_ResetBits(GPIO_LED, GPIO_Pin_7);//灭掉LED2

                GPIO_SetBits(GPIO_LED, GPIO_Pin_8);  //点亮LED3
                Delay(0xAFFFF);
                GPIO_ResetBits(GPIO_LED, GPIO_Pin_8);//灭掉LED3

                GPIO_SetBits(GPIO_LED, GPIO_Pin_9);  //点亮LED4
                Delay(0xAFFFF);
                GPIO_ResetBits(GPIO_LED, GPIO_Pin_9);//灭掉LED4
        }
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

发表于 2013-7-29 10:51:17 | 显示全部楼层
看帖积极回帖,增加莫元

出0入0汤圆

 楼主| 发表于 2013-7-29 14:15:13 | 显示全部楼层
百为STM32开发板教程之三——USART串口通信

一、简介
STM32F103ZET6有3个USART(通用同步和异步收发器) + 2个UART(通用异步收发器)
分别是USART1,USART2,USART3
和UART4,UART5

二、USART和UART有什么区别呢?
当进行异步通信时,这两者是没有区别的。区别在于USART比UART多了同步通信功能,同步通信需要STM32提供时钟来同步的,
这个同步通信功能可以把USART当做SPI来用,比如用USART来驱动SPI设备。同步通信的连接示例图:




其中RX,TX,SCLK引脚的定义,在数据手册上都可以找到:百为stm32开发板光盘\芯片数据手册\数据手册STM32F103xC STM32F103xD STM32F103xE.pdf

这个区别在初学STM32的时候我们不需要去深入研究,只要知道USART有很多功能,除了全双工异步通信之外,还包括支持同步通信和单线半双工通信,支持LIN(局部互连网),智能卡协议和IrDA红外通信,以及调制解调器(CTS/RTS)等操作。

三、数据通信格式
我们用得最多的是全双工异步通信功能,下面我们来研究下怎么通过串口1(USART1)来收发信息,和printf功能的实现。

通常串口通信的数据格式如下图:




我们需要设置的数据有通信速率,数据字长,奇偶检验位,停止位。一个典型的设置是115200波特率,8位数据,无奇偶校验,1位停止位。
这个设置在固件函数库里面,我们是通过设置USART_InitStructure结构体,然后调用USART_Init函数来实现的:

  USART_InitStructure.USART_BaudRate = 115200;                                    //设置通信波特率为115200
  USART_InitStructure.USART_WordLength = USART_WordLength_8b;     //设置通信数据格式为8位数据
  USART_InitStructure.USART_StopBits = USART_StopBits_1;                     //设置停止位为1位
  USART_InitStructure.USART_Parity = USART_Parity_No ;                         //设置为无奇偶校验
  USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;  //设置为无硬件流控制,即无CTS/RTS控制
  USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;  //设置发送使能,接收使能
  
  USART_Init(USART1, &USART_InitStructure);  //调用USART_Init,把上面的参数分别设置进USART的控制寄存器USART1->CR1,USART1->CR2,USART1->CR3

  USART_Cmd(USART1, ENABLE);  //使能串口

大家发现,在2.0固件库的USART例程里并没有看到USART1,因为是它用USARTx宏代替的,
其中USARTx是platform_config.h里定义的,大概是这个样子:
#ifdef USE_USART1
  #define  USARTx                                USART1
  #define  GPIOx                                   GPIOA
  #define  RCC_APB2Periph_GPIOx       RCC_APB2Periph_GPIOA
  #define  GPIO_RxPin                          GPIO_Pin_10
  #define  GPIO_TxPin                          GPIO_Pin_9
#endif

只有定义了USE_USART1,上面的#ifdef USE_USART1和#endif之间的内容才会被编译,所以在platform_config.h里也需要定义USE_USART1:
#define USE_USART1

这里GPIO_Pin_9是串口1的发送引脚,GPIO_Pin_10是串口1的接收引脚,也可以从百为STM32开发板的电路图上看出来:




四、串口引脚配置
上面USART_Init函数配置了USART1的数据通信格式,但串口能工作的前提是需要配置相应的TX,RX的引脚,这个是通过GPIO_Configuration函数来配置的:
void GPIO_Configuration(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);    //打开USART1时钟
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);  //打开AFIO时钟
  /* 配置 USARTx_Tx 为复用推挽输出 */
  GPIO_InitStructure.GPIO_Pin = GPIO_TxPin;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_Init(GPIOx, &GPIO_InitStructure);
  /* 配置 USARTx_Rx 为输入悬空 */
  GPIO_InitStructure.GPIO_Pin = GPIO_RxPin;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  GPIO_Init(GPIOx, &GPIO_InitStructure);
}

五、收发数据
配置好USART1使用的引脚,数据通信格式,下面就可以收发数据了,
USART_GetFlagStatus函数可以读取收发状态等,读取状态标志可以是以下几个:




发送数据示例:
USART_SendData(USART1, 'a');  //发送一个字符a

接收数据示例:
u16 RxData;
RxData = USART_ReceiveData(USART1);  //从USART1接收数据到RxData变量

下面是串口通信printf程序里的主要功能,上电打印一串信息,把接收到的数据回显到PC上:
/* 用printf打印一串信息到PC的超级终端或串口调试软件上 */
  printf("\n\rUSART Printf Example: retarget the C library printf function to the USART\n\r");
  while (1)
  {
       if(USART_GetFlagStatus(USARTx,USART_FLAG_RXNE)==SET)  //判断是否有数据要接收
      {  
       i = USART_ReceiveData(USARTx);  //接收数据
       printf("%c\n\r",i&0xff);  //回显到PC的超级终端或串口调试软件上
      }
  }

六、printf的实现
上面的printf是怎么实现的呢,这个是C标准库里定义的函数,我们是怎样把它的输出重定向到串口的呢?

我们知道printf是调用fputc函数来打印的,所以我们只要把fputc函数重定义就可以了:
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)

PUTCHAR_PROTOTYPE
{
  /* 调用USARTx发送一个字符*/
  USART_SendData(USARTx, (u8) ch);
  /* 等待发送完成 */
  while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET)
  {
  }
  return ch;
}

另外还要加上头文件
#include "stdio.h"

还要注意的是,在工程里要勾上USE MicroLIB

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 2013-7-29 21:33:33 | 显示全部楼层
百为STM32开发板教程之四——9320 LCD显示

以下内容都是来自STM32数据手册,9320数据手册,和百为STM3210E-EVAL电路图,和测试程序,大家可以翻阅看下。

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

1、先了解STM32 FSMC接口连接LCD的电路图:
LCD部分




STM32数据线部分,FSMC_NE4、FSMC_NWE和FSMC_NOE部分未画出,可参看百为STM32开发板原理图




暂时撇开触摸电路不讲,这里LCD和STM32连接的主要有A0(寄存器或数据选择),D0~D15(数据线),FSMC_NE4(FSMC片选),FSMC_NWE(FSMC读使能),FSMC_NOE(FSMC写使能)

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

二、FSMC是什么?
FSMC可以理解为一个自动控制的接口,它可以自动输出读、写信号等。  
如果不用FSMC,我们控制一个LCD时,需要手动输出RS,RO,RW这些信号
以读LCD数据为例,传统的控制方法:
RS  = 1;
RO  = 0;
RW = 1;
lcd_data = Pxxx;
若采用FSMC则很简单,只要读一个地址的值读出即可,FSMC系统会自动输出RO,RW这些控制信号:
lcd_data = *(U32)0x60000002;

这个0x60000002是怎么来的呢,这个要看下数据手册里FSMC的功能模块了




FSMC支持多种储存器件,我们把LCD当作NOR或PSRAM类型来控制,即属于块1,地址在0x60000000~0x6FFFFFFF范围内
这个范围又可以分为4个小范围 ,每个小范围对应STM32的一个管脚/片选,FSMC_NEx(x=1,2,3,4)




我们百为STM3210E-EVAL开发板的LCD用的是FSMC_NE4
所以地址为0x60000000 | 0x3<<26 = 0x6C000000, 其中0x3就是内部的地址线HADDR[27:26]的值。




这里采用的是FSMC的16位接口,RS接的是A0地址线,所以当要输出A0为1时,FSMC_A[0] = 1,即,HADDR[1] = 1
所以得出HADDR[27:0] = 0x6C000002

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

三、9320 LCD是怎么控制的?
9320的LCD控制分为寄存器控制和数据读写,所以首先定义9320的寄存器地址和数据地址
typedef struct
{
  vu16 LCD_REG;
  vu16 LCD_RAM;
} LCD_TypeDef;

/* Note: LCD /CS is CE4 - Bank 4 of NOR/SRAM Bank 1~4 */
#define LCD_BASE        ((u32)(0x60000000 | 0x0C000000))
#define LCD             ((LCD_TypeDef *) LCD_BASE)

经过上面这样定义之后,LCD->LCD_REG 就相当于 *(u32)0x6c000000,LCD->LCD_RAM相当于*(u32)0x6c000002

/************************************************************************/

然后可以通过下面的函数写寄存器:
void LCD_WriteReg(u8 LCD_Reg, u16 LCD_RegValue)
{
  LCD->LCD_REG = LCD_Reg;        //写命令寄存器
  LCD->LCD_RAM = LCD_RegValue;        //写GRAM数据

}
例如我们写寄存器R3,可以调用上面的函数
LCD_WriteReg(R3,  0x1030);

通过下面的函数读寄存器:
u16 LCD_ReadReg(u8 LCD_Reg)
{
  LCD->LCD_REG = LCD_Reg;        //写命令寄存器
  return (LCD->LCD_RAM);//读GRAM数据
}

/************************************************************************/

写LCD GARM数据之前,可以设置一下坐标位置:
void LCD_SetCursor(u16 Xpos, u16 Ypos)
{
  LCD_WriteReg(R32, Xpos);        //写命令寄存器R32
  LCD_WriteReg(R33, Ypos);        //写命令寄存器R33
}

设置R34寄存器准备写GRAM数据,(写多个数据时只需设置一次)
void LCD_WriteRAM_Prepare(void)
{
  LCD->LCD_REG = R34;        //准备读写GRAM数据
}

然后通过下面的函数写LCD GARM数据:
void LCD_WriteRAM(u16 RGB_Code)
{
  LCD->LCD_RAM = RGB_Code;//写GRAM数据到当前地址
}

/************************************************************************/

然后通过下面的函数读LCD GARM数据,根据9320数据手册,读GRAM数据需要读两次,第一次为无效数据,第二次才是实际数据
u16 LCD_ReadRAM(void)
{
  vu16 dummy;
  LCD->LCD_REG = R34;//准备读GRAM数据
  dummy = LCD->LCD_RAM;//第一次返回无效数据,丢弃
  return LCD->LCD_RAM; //第二次返回真实的数据
}

/************************************************************************/

四、程序是怎么控制LCD的
先用下面的函数初始化
  STM3210E_LCD_Init();
  LCD_Clear(White);

然后就可以调用刚才介绍的函数读写LCD GRAM了
如画一个蓝色的点:
  LCD_SetCursor(50, 100);         
  LCD_WriteRAM_Prepare();  
  LCD_WriteRAM(Blue);

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

发表于 2013-7-29 22:14:29 来自手机 | 显示全部楼层
睡前顶贴

出0入0汤圆

发表于 2013-7-29 23:35:07 | 显示全部楼层
支持LZ。

出0入0汤圆

发表于 2013-7-30 00:57:04 | 显示全部楼层
支持开源例程

出0入0汤圆

发表于 2013-7-30 07:50:51 | 显示全部楼层
百为的板子确实不错。

出0入0汤圆

发表于 2013-7-30 09:41:55 | 显示全部楼层
支持楼主,入手一个开发板好好的学学

出0入0汤圆

发表于 2013-7-30 09:46:39 | 显示全部楼层
对初学者很有用.

出0入0汤圆

发表于 2013-7-30 09:55:20 | 显示全部楼层
mark,新手来学习,希望能多弄些入门的,我现在连如何写一个程序都没搞懂呢,按照51的步骤搞一编译全是错

出0入0汤圆

发表于 2013-7-30 10:11:44 | 显示全部楼层
如此开源,怎能不支持

出0入0汤圆

发表于 2013-7-30 10:34:08 | 显示全部楼层
支持

出0入0汤圆

 楼主| 发表于 2013-7-30 17:33:17 | 显示全部楼层
百为STM32开发板教程之五——9320 LCD横屏和竖屏显示

参考资料:
百为stm32开发板光盘\芯片数据手册\tft屏资料\ILI9320.pdf

修改横竖屏显示,主要设置寄存器有R1,R96,R3
其中R1的SS位和R96的GS位是决定坐标原点的,即R32(X坐标)= 0,R33(Y坐标)= 0时的位置。
共有以下4个原点位置,具体位置以实验为准:
SS = 0,GS = 0
SS = 0,GS = 1
SS = 1,GS = 0
SS = 1,GS = 1

其中R3是设置扫描方向的,与上面设置的坐标原点结合起来用,下面图中的B即是坐标原点:




下面给出横屏设置和竖屏设置代码的区别,左边是竖屏的配置,右边是横屏的设置:




那么设置好之后,如何看坐标原点和扫描的效果呢,这里可以在LCD_Clear里加个延时,上电时调用这个函数就可以看到放慢镜头的扫描过程了:
void LCD_Clear(uint16_t Color)
{
  uint32_t index = 0;
  
  LCD_SetCursor(0x00, 0x00);
  LCD_WriteRAM_Prepare(); /* Prepare to write GRAM */
  for(index = 0; index < 76800; index++)
  {
    LCD->LCD_RAM = Color;
   _delay_(1);  //加延时,观察实际扫描过程
  }  
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 2013-7-31 09:00:28 | 显示全部楼层
百为STM32开发板教程之六——触摸画板程序

一、四线电阻触摸屏的结构与工作原理
二、ADS7843触摸屏控制器与触摸坐标读取
三、触摸校准原理与算法


一、四线电阻触摸屏的结构与工作原理
1、四线电阻触摸屏主要由三部分构成:两层透明导体层、隔离层和电极。





2、触摸的检测。当在电极Y-和Y+上加电压时,从未加电压的X+或X-上可以读出触摸点的电压。同样在X-和X+上加电压时,可以从Y+或Y-上读出另一个方向的电压。




3、触摸屏的排线引出有4个信号:XL,YD,XR,YU:




这四个信号是接到ADS7843(ADS7843和TSC2046、XPT2046都是兼容的)上的,看百为stm32开发板光盘\原理图\tft_2.8_lcd_v3.0.pdf就知道了:









这里为什么YU接的是Y-而不是Y+呢,我的理解是这样的,因为LCD的坐标系是这样的:



所以默认把Y-和Y+倒过来了。


二、ADS7843触摸屏控制器与触摸坐标读取

1、ADS7843引脚定义




2、ADS7843典型应用电路



3、ADS7843寄存器操作。ADS7843的控制字结构如下:



其中S为数据传输起始标志位,该位必为"1"。
A2~A0进行信道选择(见表2和3)。
MODE用来选择A/D转换的精度,"1"选择8位,"0"选择12位。
SER/选择参考电压的输入模式(见表2和3)。
PD1、PD0选择省电模式:"00"省电模式允许,在两次A/D转换之间掉电,且中断允许;"01"同"00",只是不允许中断;"10"保留;"11"禁止省电模式。



我们这里用的是电压差动输入模式,12位精度,省电模式。所以要读取X坐标,S = 1,A2 A1 A0 = 0 0 1,SER/DFR = 0,PD1 PD0 = 00
即读X坐标时控制字要为0x90,同样得出读Y坐标时控制字要为0xD0(A2 A1 A0 = 1 0 1)。

4、读取触摸坐标程序设计
ADS7843控制器的PENIRQ引脚,当有触摸按下时,该引脚会输出低电平。所以程序中可以采用中断的方式检测触摸,也可以用查询方式检测触摸,
我们这里采用第二种方式。



PENIRQ引脚在百为STM32开发板上是接到STM32的PB10的,所以检测并读取坐标程序如下:
void getxy(int *x, int *y)
{
int i;
*x=0;
*y=0;
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_10)==Bit_SET);  //检测低电平,即检测是否有触摸按下
for(i=0; i<10; i++)
{
   *y += SPI_TOUCH_Read_X();
   *x += SPI_TOUCH_Read_Y();
}  
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_10)==Bit_RESET);
*x=*x/10;
*y=*y/10;
}

读X坐标程序:
u16 SPI_TOUCH_Read_X(void)
{
  u16 xPos = 0, Temp = 0, Temp0 = 0, Temp1 = 0;
  /* Select the TP: Chip Select low */
  SPI_TOUCH_CS_LOW();
SPI_Delay(10);
  /* Send Read xPos command */
  SPI_TOUCH_SendByte(0x90);
SPI_Delay(10);
  /* Read a byte from the TP */
  Temp0 = SPI_TOUCH_ReadByte();
SPI_Delay(10);  
  /* Read a byte from the TP */
  Temp1 = SPI_TOUCH_ReadByte();  
SPI_Delay(10);
  /* Deselect the TP: Chip Select high */
  SPI_TOUCH_CS_HIGH();

  Temp = (Temp0 << 8) | Temp1;
  
  xPos = Temp>>3;
  return xPos;
}

读Y坐标程序:
u16 SPI_TOUCH_Read_Y(void)
{
  u16 yPos = 0, Temp = 0, Temp0 = 0, Temp1 = 0;
  /* Select the TP: Chip Select low */
  SPI_TOUCH_CS_LOW();
SPI_Delay(10);
  /* Send Read yPos command */
  SPI_TOUCH_SendByte(0xD0);
SPI_Delay(10);
  /* Read a byte from the TP */
  Temp0 = SPI_TOUCH_ReadByte();
  SPI_Delay(10);
  /* Read a byte from the TP */
  Temp1 = SPI_TOUCH_ReadByte();  
SPI_Delay(10);
  /* Deselect the TP: Chip Select high */
  SPI_TOUCH_CS_HIGH();
  Temp = (Temp0 << 8) | Temp1;
  
  yPos = Temp>>3;
  return yPos;
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 2013-7-31 09:02:09 | 显示全部楼层
三、触摸校准原理与算法

1、为什么需要触摸校准呢?
因为触摸屏和LCD显示屏坐标之间存在误差,这误差包括比例系数、机械不同轴性等。LCD显示屏与触摸屏之间的机械不同轴性又包括移动与旋转误差。

缩放:



平移:



旋转:




因此,从上面的缩放,平移,旋转,可以得出LCD坐标和触摸屏坐标的计算公式

XL=XT*A+XT*B+C
YL=YT*D+YT*E+F

所以,只要计算出参数A,B,C,D,E,F,
我们就可以将从触摸芯片(ADS7843/TSC2046/XPT2046)上读出的触摸屏坐标转换成LCD坐标

2、我们采用tslib的五点校准算法,
其中触摸采样采用tslib里的排序取中间值,另外加上阈值判断的滤波算法。

typedef struct {
int x[5], xfb[5];
int y[5], yfb[5];
int a[7];
} calibration;

xfb[5],yfb[5]存放预先设定的5个LCD坐标值
x[5],y[5]存放从触摸芯片读回来的触摸坐标值

函数get_sample调用put_cross输出田字形光标,并通过getxy采样触摸坐标值
static void get_sample (calibration *cal, int index, int x, int y, char *name)
{

put_cross(x, y, 2 | XORMODE);
while(!getxy (&cal->x [index], &cal->y [index]));         //调用getxy将采样到的触摸坐标值存放在x[],y[]数组里
put_cross(x, y, 2 | XORMODE);

//将预先设定的LCD坐标值存放在xfb[],yfb[]数组里
cal->xfb [index] = x;
cal->yfb [index] = y;
}

getxy就是调用底层函数读取20组触摸坐标值,进行排序取中值平均,并进行阈值检查的函数

#define MAX_SAMPLES 20
struct ts_sample samp[MAX_SAMPLES];
int getxy(int *x, int *y)
{
int i;
int index, middle;

while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_10)==Bit_SET);  //检测是否有触摸按下

index = 0;

  while((index < MAX_SAMPLES)&&(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_10)==Bit_RESET))  //采样MAX_SAMPLES个点,直到触摸松开
  {
   index++;  
   samp[index].x = SPI_TOUCH_Read_X();
   samp[index].y = SPI_TOUCH_Read_Y();  
  }

middle = index/2;
if (x) {
  qsort(samp, index, sizeof(struct ts_sample), sort_by_x);  //对采样到的数据进行排序
  if(samp[middle].x - samp[middle-1].x > 5)
   return 0;
  if (index & 1)  //若为奇数个点,则取中间3个点平均
   *x = (samp[middle-1].x + samp[middle].x + samp[middle+1].x)/ 3;
  else               //否则取中间4个点平均
   *x = (samp[middle-1].x + samp[middle].x + samp[middle-2].x + samp[middle+1].x) / 4;
}
if (y) {
  qsort(samp, index, sizeof(struct ts_sample), sort_by_y); //对采样到的数据进行排序
  if(samp[middle].y - samp[middle-1].y > 5)
   return 0;
  if (index & 1) //若为奇数个点,则取中间3个点平均
   *y = (samp[middle-1].y + samp[middle].y + samp[middle+1].y) / 3;
  else              //否则取中间4个点平均
   *y = (samp[middle-1].y + samp[middle].y + samp[middle-2].y + samp[middle+1].y) / 4;
}
return 1;
}

3、得到5个点的采样数据之后,下面就可以通过perform_calibration计算上面的参数A,B,C,D,E,F了

求解方法采用了克拉姆法则,这个是线性代数的内容了,若看不懂的话要复习下线性代数的行列式和矩阵的内容。

5点校准这部分内容也可以参考下《Tslib中触摸屏校准原理及其实现.pdf》

计算的到上面的参数后,存放在cal.a[]数组中。
然后程序就可以利用上面参数,计算出LCD坐标了,其中a[6]是放大倍数(因为部分参数是小数,所以把全部参数放大倍数,方便存储)
   xtemp = cal.x[0];
   ytemp = cal.y[0];
   x =  (int)(( cal.a[0] + cal.a[1]*xtemp + cal.a[2]*ytemp ) / cal.a[6]);
   y =  (int)(( cal.a[3] + cal.a[4]*xtemp + cal.a[5]*ytemp ) / cal.a[6]);

得到触摸按下时对应LCD的x,y坐标之后,我们就利用它来做触摸画板,GUI触摸输入等应用了

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

发表于 2013-7-31 09:10:38 | 显示全部楼层
谢谢楼主分享,先标记一下

出0入0汤圆

发表于 2013-7-31 09:20:24 | 显示全部楼层
好资料,学习STM32更顺利了。。。

出0入0汤圆

发表于 2013-7-31 09:39:36 | 显示全部楼层
很不错的资料,顶一下

出0入0汤圆

发表于 2013-7-31 09:44:59 | 显示全部楼层
支持开源例程

出0入0汤圆

发表于 2013-7-31 11:06:07 | 显示全部楼层
LZ辛苦了,不容易啊

出0入0汤圆

发表于 2013-7-31 11:48:34 | 显示全部楼层
楼主太辛苦,顶一下

出0入0汤圆

发表于 2013-7-31 11:55:25 | 显示全部楼层
支持LZ。

出0入0汤圆

发表于 2013-7-31 11:56:31 | 显示全部楼层
好好!谢谢咯

出0入0汤圆

发表于 2013-7-31 11:56:43 | 显示全部楼层
先用不到,mark下。
感谢楼主的无私开源。

出0入0汤圆

发表于 2013-7-31 13:07:04 | 显示全部楼层
支持开源,帮顶!

出0入0汤圆

发表于 2013-7-31 13:28:15 | 显示全部楼层
不错  ,呵呵

出0入0汤圆

发表于 2013-7-31 13:57:16 | 显示全部楼层
不错,收藏了

出0入0汤圆

 楼主| 发表于 2013-7-31 16:56:00 | 显示全部楼层
百为STM32开发板教程之七——SysTick定时器

实验目的:用SysTick定时器进行定时

关于SysTick定时器,相关资料可以查阅 百为stm32开发板光盘\用户手册\The Insider's Guide to Stm32.pdf ——2.4.3 系统定时器
或参考百为stm32开发板光盘\stm32电子书\Cortex-M3 技术参考手册.pdf——8.2.2 NVIC 寄存器描述——系统时钟节拍(SysTick)控制与状态寄存器

STM32的Cortex核心内部包含有一个24 位的递减计数器,含有自动装载和计数结束中断功能。



SysTick定时器有三个寄存器。分别是SysTick控制和状态寄存器,SysTick重装载值寄存器和SysTick当前值寄存器。
当前的计数值和重载值需要用计数周期来进行初始化。在控制和状态寄存器中包含了一个ENABLE位来开启定时器运行和一个TICKINT的位来启用它的中断。


SysTick控制和状态寄存器,地址为0xE000E010:



其中SysTick定时器的时钟源是可选择的,通过设置SysTick控制和状态寄存器的CLKSOURCE位,
可以设置SysTick的时钟源为系统时钟(CLKSOURCE =1),或为系统时钟的八分之一(CLKSOURCE =0)

SysTick重装载值寄存器,地址为0xE000E014:



SysTick重装载值寄存器位[23:0] RELOAD表示当计数器到达0时需要重装载到“当前值寄存器”的值。


SysTick当前值寄存器,地址0xE000E018:




了解了SysTick寄存器之后,下面就可以使用SysTick了,使用SysTick主要有三个步骤:
1、设置重装载值
2、打开SysTick计数使能
3、打开SysTick中断使能

2.0固件库相关代码:

/* 设置重装载值 */
SysTick_SetReload(9000);
/* 使能SysTick计数 */
SysTick_CounterCmd(SysTick_Counter_Enable);
/* 使能SysTick中断 */
SysTick_ITConfig(ENABLE);

3.5固件库相关代码:
  if (SysTick_Config(SystemCoreClock / 1000))  //这里相关于上面2.0固件库的3个步骤了
  {
    /* Capture error */
    while (1);
  }

这时每1ms就会产生SysTick中断一次,在SysTick的中断服务程序SysTickHandler()里调用我们的延时计数程序即可
如:
/* stm32f10x_it.c */
void SysTickHandler(void)
{
  TimingDelay_Decrement();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/* main.c */
void Delay(__IO uint32_t nTime)
{
  TimingDelay = nTime;
  while(TimingDelay != 0);
}

void TimingDelay_Decrement(void)
{
  if (TimingDelay != 0x00)
  {
    TimingDelay--;
  }
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

然后我们就可以通过调用Delay()进行毫秒级延时了。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 2013-8-2 11:31:19 | 显示全部楼层
百为STM32开发板教程之八——汉字显示

1、了解汉字编码
2、了解汉字点阵数据及显示过程
3、利用.c字库文件在程序中显示汉字

1、国标码:
国标码(GB2312-80):中国标准总局1981年制定了中华人民共和国国家标准GB2312--80《信息交换用汉字编码字符集--基本集》,即国标码。
是从0x2121开始的编码,具体可以参考下面的网页
http://examples.oreilly.com/cjkvinfo/unicode/gb2312-80.txt

3、机内码
根据国标码的规定,每一个汉字都有确定的二进制编码,在微机内部汉字代码都用机内码,在磁盘上记录汉字代码也使用机内码。

2、汉字区位码:
区位码是国标码的另一种表现形式,把国标GB2312--80中的汉字、图形符号组成一个94×94的方阵,分为94个“区”,每区包含94个“位”,其中“区”的序号由01至94,“位”的序号也是从01至94。94个区中位置总数=94×94=8836个,其中7445个汉字和图形字符中的每一个占一个位置后,还剩下1391个空位,这1391个位置空下来保留备用。

汉字区位码也叫汉字机内码,汉字机内码由国标码(GB2312-80)演化而来,把表示国际码的两个字节的最高位分别加1(加0x8080),就变成了汉字机内码。
是从0xA1A1开始的编码,具体可以参考下面的网页
http://www.knowsky.com/resource/gb2312tbl.htm

因此在程序中定义了汉字字库表之后,就可以通过汉字在表中的偏移,取得它的点阵数据,然后我们把点阵数据按一定高度和宽度扫描到LCD上,就可以显示汉字了

定义汉字字库表:
const uint8_t GB2312_16x16_Table [] =
{
……
}

程序中调用显示汉字函数:
LCD_DisplayString(80,100,"啊");  //这里“啊”是用汉字机内码表示,即0xB0A2

然后在LCD_DisplayString里判断编码是否大于等于0xA1,如果是表示当前要显示的是汉字:

void LCD_DisplayString(uint16_t xPos, uint16_t yPos, uint8_t *ptr)
{
  uint32_t index = 0;

  while (*ptr != 0)  //显示一串字符串
  {
  if(*ptr < 0xA1)  //当前字符是否小于0xA1,如果是,表示是非汉字显示
  {
     LCD_DisplayChar(xPos, yPos, *ptr);  //显示ASCIII字符   
     if ( *ptr == 'A' || *ptr == 'G' || *ptr == 'M' || *ptr == 'O' || *ptr == 'Q' || *ptr == 'X' || *ptr == 'm')  //宽字符显示
       xPos += (LCD_Currentfonts->Width);
     else
       xPos += (LCD_Currentfonts->Width - 1);
     ptr++;  //指向下一个字符,ASCII字符只占一个字节
     index++;
  }
  else
  {
     LCD_DisplayChineseChar(xPos, yPos, ptr);  //显示一个汉字
   xPos += (LCD_Currentfonts->Width - 1);
     ptr++;  //指向下一个字符,一个汉字占用两个字节
   ptr++;    //
     index++;
  }
  }
}

调用函数LCD_DisplayChineseChar计算出汉字点阵数据在字库表中的偏移,显示一个汉字点阵数据
void LCD_DisplayChineseChar(uint16_t Line, uint16_t Column, uint8_t *Char_GB)
{
uint32_t offset;
uint16_t byteOfDots;
byteOfDots = LCD_Currentfonts->Height*(LCD_Currentfonts->Width/16);
offset = 94 * (*Char_GB - 0xA1) * byteOfDots + (*(Char_GB+1) - 0xA1) * byteOfDots;
  LCD_DrawChineseChar(Line, Column, &GB2312_16x16_Table[offset]);
}

最终把点阵数据取出,画到LCD上的函数:
void LCD_DrawChineseChar(uint16_t Xpos, uint16_t Ypos, const uint16_t *c) /* 16bit char */
{
  uint32_t line_index = 0, pixel_index = 0;
  uint8_t Xaddress = 0;
  uint16_t Yaddress = 0;
  __IO uint16_t tmp_color = 0;
  Xaddress = Xpos;
  Yaddress = Ypos;
LCD_SetCursor(Xaddress, Ypos);
  for (line_index = 0; line_index < LCD_Currentfonts->Height; line_index++)
  {
  LCD_WriteRAM_Prepare(); /* Prepare to write GRAM */
    for (pixel_index = 0; pixel_index < LCD_Currentfonts->Width/2; pixel_index++)
    {
      /* SmallFonts have bytes in reverse order */
   if ((((const uint16_t*)c)[line_index] & (0x80 >> pixel_index)) == 0x00)
      {
        LCD_WriteRAM(BackColor);
      }
      else
      {
        LCD_WriteRAM(TextColor);
      }
    }
  for (pixel_index = 0; pixel_index < LCD_Currentfonts->Width/2; pixel_index++)
    {
      /* SmallFonts have bytes in reverse order */
   if ((((const uint16_t*)c)[line_index] & (0x8000 >> pixel_index)) == 0x00)
      {
        LCD_WriteRAM(BackColor);
      }
      else
      {
        LCD_WriteRAM(TextColor);
      }
    }
//    Xaddress++;
//    Yaddress = Ypos;
  Ypos++;
  LCD_SetCursor(Xaddress, Ypos);
  }
}

出0入0汤圆

 楼主| 发表于 2013-8-3 09:51:43 | 显示全部楼层
百为STM32开发板教程之九——SPI FLASH

实验目的:把字库写到SPI FLASH中,然后在程序里利用SPI FLASH中的字库进行显示

主要内容:
一、了解SPI通信协议
二、了解STM32的SPI接口及编程
三、了解SPI FLASH的工作原理及读写方法
四、了解如何把字库写到SPI FLASH及把字库读出显示

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

一、SPI通信协议
SPI总线规定了4个逻辑信号:
(1)MOSI – SPI总线主机输出/ 从机输入(Master Output/Slave Input)
(2)MISO – SPI总线主机输入/ 从机输出(Master Input/Slave Output)
(3)SCLK – 时钟信号,由主设备产生
(4)CS – 从设备使能信号,由主设备控制(Chip select)

其中SCLK的极性和相位是可设置的:





CPOL表示时钟极性,即空闲状态时的电平,低电平(0)或高电平(1)
CPOH表示数据采样的时钟沿,第一个沿(0)或第二个沿(1)

CPOL和CPOH组成了SPI的四种工作模式:(0)  CPOL = 0 CPOH = 0  (1)  CPOL = 0 CPOH = 1  (2)  CPOL = 1, CPOH = 0  (3)  CPOL = 1, CPOH = 1


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

二、STM32的SPI接口及编程

STM32F103ZET6有3个SPI,分别为SPI1、SPI2和SPI3。
其中SP2和I2S2,SPI3和I2S3共用管脚,采用SPI功能还是I2S功能是由SPI_I2S_CFGR寄存器的第11位I2SMOD来决定的。

SPI_I2S配置寄存器(SPI_I2S_CFGR)



其中I2SMODE位为1时表示选择I2S模式,为0时表示选择SPI模式。

在我们百为STM32开发板上,是采用STM32的SPI1连接SPI FLASH的



IO对应关系为:
STM32                                                   M25P80
PB2(CS)----------------------------------  S(片选信号)
PA5(SPI1_SCK)-------------------------  C(串行时钟)
PA6(SPI1_MISO)------------------------  Q(串行数据输出)
PA7(SPI1_MOSI)------------------------  D(串行数据输入)

程序中要SPI,首先要调用下面的函数进行初始化
void SPI_FLASH_Init(void)
{
  SPI_InitTypeDef  SPI_InitStructure;
  GPIO_InitTypeDef GPIO_InitStructure;
  /* 打开SPI1和GPIO时钟 */
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIO_CS, ENABLE);
  /* 配置SPI1管脚: SCK, MISO and MOSI */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
  /* 配置SPI FLASH 片选IO */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_CS;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_Init(GPIO_CS, &GPIO_InitStructure);
  /* 初始化SPI FLASH片选为高电平 */
  SPI_FLASH_CS_HIGH();
  /* SPI1参数配置 */
  SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //设置SPI为全双工通信
  SPI_InitStructure.SPI_Mode = SPI_Mode_Master;  //设置为SPI主模式
  SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;  //设置为8位数据格式
  SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;  //设置为SPI工作模式4,即CPOL = 1,
  SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //CPOH = 1
  SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;  //设置NSS为软件管理
  SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;  //设置SPI波特率为fpclk/4
  SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;  //设置数据高位在先
  SPI_InitStructure.SPI_CRCPolynomial = 7;
  SPI_Init(SPI1, &SPI_InitStructure);
  /* 使能SPI1  */
  SPI_Cmd(SPI1, ENABLE);
}

三、了解SPI FLASH的工作原理及读写方法
1、简介
M25P80是一个8Mbit(1Mbitx8)的串行FLASH,具有高级的写保护机制,可以通过SPI兼容的总线进行读写。
M25P80一共有16个扇区,每个扇区有256个页,每个页有256 byte。
通过页写入指令,可以一次写入1-256 byte数据。
通过扇区擦除指令,可以以扇区为单位进行擦除。

2、M25P80的SPI工作模式
M25P80支持两种SPI模式:(0)  CPOL = 0 CPOH = 0   (3)  CPOL = 1, CPOH = 1




3、操作指令
(1)写使能指令(0x06)
部分指令如页写入指令,扇区擦除指令,批量擦除指令操作之前都需要发送写使能指令。
写使能指令时序:




(2)页写入指令(0x02)
页写入需要两个指令,一个是写使能指令(1个字节),另一个是页写入指令(4个字节以上)。




(3)扇区擦除指令(0xD8)
在页写入之前,我们必须擦除相应的扇区。扇区擦除也需要两个指令,一个是写使能指令(1个字节),另一个是扇区擦除指令(4个字节)。
扇区擦除指令时序:




(4)批量擦除指令(0xC7)
批量擦除是指擦除整个芯片内容。批量擦除需要两个指令,一个是写使能指令(1个字节),另一个是批量擦除指令(1个字节)
批量擦除指令时序:




(5)读ID指令(0x9F)
发送读ID指令时数据输出序列:



读ID指令时序:




(6)读数据指令(0x03)
写入读数据指令(1个字节)和地址(3个字节),可以读回1个字节以上的数据。
读数据指令时序:




(7)读状态指令(0x05)
状态寄存器格式:



读状态寄存器指令时序:




读SPI FLASH ID程序:
u32 SPI_FLASH_ReadID(void)
{
  u32 Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;
  /* 设置片选为低电平,选中SPI FLASH*/
  SPI_FLASH_CS_LOW();
  /* 发送读ID指令0x9F */
  SPI_FLASH_SendByte(0x9F);
  /* 从SPI FLASH读出第一个字节(0x20) */
  Temp0 = SPI_FLASH_SendByte(Dummy_Byte);
  /* 从SPI FLASH读出第二个字节(0x20) */
  Temp1 = SPI_FLASH_SendByte(Dummy_Byte);
  /* 从SPI FLASH读出第三个字节(0x14) */
  Temp2 = SPI_FLASH_SendByte(Dummy_Byte);
  /* 设置片选为高电平,取消选择SPI FLASH */
  SPI_FLASH_CS_HIGH();
  Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;
  return Temp;
}

SPI FLASH扇区擦除函数:
void SPI_FLASH_SectorErase(u32 SectorAddr)
{
  /* 发送写使能指令 */
  SPI_FLASH_WriteEnable();
  /* 扇区擦除 */
  /* 设置片选为低电平,选中SPI FLASH */
  SPI_FLASH_CS_LOW();
  /* 发送扇区擦除指令 */
  SPI_FLASH_SendByte(SE);
  /* 发送扇区地址a23~a16 */
  SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);
  /* 发送扇区地址a15~a8 */
  SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);
  /* 发送扇区地址a7~a0 */
  SPI_FLASH_SendByte(SectorAddr & 0xFF);
  /* 设置片选为高电平,取消选择SPI FLASH */
  SPI_FLASH_CS_HIGH();
  /* 等待写操作完成 */
  SPI_FLASH_WaitForWriteEnd();
}

写入数据到SPI FLASH,SPI FLASH只提供了页写入指令对SPI FLASH进行写操作,一次最多能写256 Byte数据,所以如果超过256 Byte,我们要多次调用页写入指令。
void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
  u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
  Addr = WriteAddr % SPI_FLASH_PageSize;  //检测地址WriteAddr是否是页(256 Byte)对齐的
  count = SPI_FLASH_PageSize - Addr;  //Addr 是最后不足一页的地址偏移,count是补齐一页的地址长度
  NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;  //计算出总页数
  NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;  //计算出最后不足一页的数据长度
  if (Addr == 0) /* 判断地址WriteAddr是否是页对齐的*/
  {
    if (NumOfPage == 0) /* 要写入的字节数NumByteToWrite 小于 页大小SPI_FLASH_PageSize,则只调用一次页写入指令即可,否则要调用多次*/
    {
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
    }
    else /* 要写入的字节数NumByteToWrite 小于 页大小SPI_FLASH_PageSize */
    {
      while (NumOfPage--) //写入NumOfPage个页
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
        WriteAddr +=  SPI_FLASH_PageSize;  //地址指向下一页
        pBuffer += SPI_FLASH_PageSize; //pBuffer指针指向要写入下一页的数据
      }
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle); //写入最后不足一页的字节数
    }
  }
  else /* 地址WriteAddr不是页对齐的  */
  {
    if (NumOfPage == 0) /* 要写入的字节数NumByteToWrite 小于页大小SPI_FLASH_PageSize */
    {
      if (NumOfSingle > count) /* 要写入的数据超出了当前地址WriteAddr到页尾的距离,(NumByteToWrite + WriteAddr) > SPI_FLASH_PageSize */
      {
        temp = NumOfSingle - count;  //超出部分写到下一页,temp表示写到下一页的字节数
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);  //写当前页
        WriteAddr +=  count;  //地址指向下一页
        pBuffer += count; //pBuffer指针指向要写入下一页的数据
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);  //写下一页
      }
      else
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);  //要写入的数据没有超出当前地址WriteAddr到页尾的距离,直接写入当前页
      }
    }
    else /* 要写入的字节数NumByteToWrite 大于页大小SPI_FLASH_PageSize */
    {
      NumByteToWrite -= count;  //NumByteToWrite -count表示补齐当前页地址长度后,剩余的数据长度
      NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;  //剩余数据长度所占的页数
      NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize; //剩余数据长度最后不足一页的数据长度
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);  //补齐当前页
      WriteAddr +=  count;  //地址指向下一页
      pBuffer += count; //pBuffer指针指向要写入下一页的数据
      while (NumOfPage--) //写入NumOfPage个页
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
        WriteAddr +=  SPI_FLASH_PageSize;  //地址指向下一页
        pBuffer += SPI_FLASH_PageSize; //pBuffer指针指向要写入下一页的数据
      }
      if (NumOfSingle != 0)
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle); //写入最后不足一页的字节数
      }
    }
  }
}

SPI FLASH页写入函数:
void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
  /* 发送写使能指令 */
  SPI_FLASH_WriteEnable();
  /* 片选设为低电平,选中SPI FLASH */
  SPI_FLASH_CS_LOW();
  /* 发送页写入指令 */
  SPI_FLASH_SendByte(WRITE);
  /* 发送地址a23~a16 */
  SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
  /* 发送地址a15~a8 */
  SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
  /* 发送地址a7~a0 */
  SPI_FLASH_SendByte(WriteAddr & 0xFF);
  /* 循环写入NumByteToWrite个字节数据 */
  while (NumByteToWrite--)
  {
    /* 发送一字节数据 */
    SPI_FLASH_SendByte(*pBuffer);
    /* pBuffer指向下一个要写入的数据 */
    pBuffer++;
  }
  /* 片选设为高电平,取消选中SPI FLASH*/
  SPI_FLASH_CS_HIGH();
  /* 等待写操作完成 */
  SPI_FLASH_WaitForWriteEnd();
}

从SPI FLASH读取数据,发送读指令后,可以读取无限个数据:
void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
{
  /* 片选设为低电平,选中SPI FLASH */
  SPI_FLASH_CS_LOW();
  /* 发送读数据指令 */
  SPI_FLASH_SendByte(READ);
  /* 发送地址a23~a16 */
  SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
  /* 发送地址a15~a8 */
  SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);
  /* 发送地址a7~a0 */
  SPI_FLASH_SendByte(ReadAddr & 0xFF);
  while (NumByteToRead--) /* 读取NumByteToRead个字节数据 */
  {
    /* 从SPI FLASH读一字节数据*/
    *pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
    /* pBuffer指针指向读出数据被存放到的下一个位置 */
    pBuffer++;
  }
  /* 设置片选为高电平,取消选择SPI FLASH */
  SPI_FLASH_CS_HIGH();
}


四、了解如何把字库写到SPI FLASH及把字库读出显示
把hzk16.c文件里的字库数据写到SPI FLASH里,首先要擦除要写入的扇区
相关代码:
/* main.c */
sectorCnt = sizeof(GB2312_16x16_Table)/SPI_FLASH_SectorSize;  //计算字库数据需要写入的扇区数
if(sizeof(GB2312_16x16_Table)%SPI_FLASH_SectorSize != 0)
  sectorCnt++;   //如果不满一个扇区,擦除一个扇区
for(i = 0; i < sectorCnt; i++ )  //擦除sectorCnt个扇区
{
  SPI_FLASH_SectorErase(i*SPI_FLASH_SectorSize);  //擦除一个扇区
}

SPI_FLASH_BufferWrite((u8*)GB2312_16x16_Table, 0, sizeof(GB2312_16x16_Table)); //把字库数据写入到SPI FLASH,从地址0开始写。

/* stm3210e_eval_lcd.c */
void LCD_DisplayChineseChar(uint16_t Line, uint16_t Column, uint8_t *Char_GB)
{
uint32_t offset;
uint16_t byteOfDots;
uint8_t pData[24*24];  
byteOfDots = LCD_Currentfonts->Height*(LCD_Currentfonts->Width/8);
offset = 94 * (*Char_GB - 0xA1) * byteOfDots + (*(Char_GB+1) - 0xA1) * byteOfDots;  //计算当前要显示的字符的点阵数据在SPI FLASH中的偏移地址
SPI_FLASH_BufferRead(pData, offset, byteOfDots);  //从SPI FLASH中读取字库点阵数据,存放在pData数组中
//  LCD_DrawChineseChar(Line, Column, &LCD_Currentfonts->table[offset]);
LCD_DrawChineseChar(Line, Column, (uint16_t*)pData);  //调用函数把点阵数据画到LCD上
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 2013-8-4 09:28:45 | 显示全部楼层
自己顶下,顶顶顶

出0入0汤圆

发表于 2013-8-4 09:41:11 | 显示全部楼层
楼主的板子不错。

出0入0汤圆

发表于 2013-8-5 06:16:08 来自手机 | 显示全部楼层
精华贴啊,手机怎么收藏?

出0入0汤圆

 楼主| 发表于 2013-8-5 10:58:19 | 显示全部楼层
百为STM32开发板教程之十——I2C数字温度传感器

参考资料:
百为stm32开发板光盘\其他参考资料\I2C协议-周立功.pdf
百为stm32开发板光盘\芯片数据手册\LM75A_2_cn.pdf

实验目的:读取I2C数字温度传感器LM75A的温度,并打印到串口上

主要内容:
一、了解I2C协议
二、了解STM32的I2C接口
三、研究LM75A的数据手册,了解LM75A工作原理及相关操作
四、通过STM32的I2C接口读取LM75A的温度并打印串口上

一、了解I2C协议
I2C(Inter-Integrated Circuit)总线是由PHILIPS公司开发的两线式串行总线。
1、这两线分别是串行数据线SDA和串行时钟线SCL。
2、I2C 总线上数据的传输速率在标准模式下可达100kbit/s, 在快速模式下可达400kbit/s。
3、任何连接到总线上的设备都有一个唯一的地址,主机访问从机设备之前要先发送地址选中从机。
4、I2C进行通信时由主机发送起始位开始,直到主机发送停止位时结束。
SCL 线是高电平时,SDA 线从高电平向低电平切换,这个情况表示起始条件。
SCL 线是高电平时,SDA 线由低电平向高电平切换,这个情况表示停止条件。




5、发送到SDA 线上的每个字节必须为8 位,每次传输可以发送的字节数量不受限制。每个字节后必须跟一个响应位。首先传输的是数据的最高位(MSB)。
6、I2C地址有7位地址和10位地址两种,我们这里只介绍7位地址的情况。主机发送完停止位后,发送的第一个字节包括7位地址,和1位读写标志位。



最后一位R/W为0时表示写,为1时表示读

二、了解STM32的I2C接口
STM32的I2C既可做主机也可做从机,我们这里只讨论STM32的I2C作为主机的情况。
STM32的I2C功能:
1、硬件产生起始位,停止位
2、状态寄存器中有“起始条件已发送”等状态标志,这些标志组合起来就成了I2C通信过程中的事件EV5,EV6,EV7,EV8等

STM32的I2C通信过程:

STM32的I2C主机发送数据过程(发送数据到LM75A):




STM32的I2C主机接收数据过程(从LM75A接收数据):




通信过程中的EV事件(仅用于STM32):
EV5:检查状态寄存器的这三位:SB:起始位(主模式),MSL:主从模式,BUSY:总线忙
EV6:主机发送时:检查状态寄存器的:BUSY:总线忙,MSL:主从模式,ADDR:地址已被发送,TxE:数据寄存器为空,TRA:发送/接收
         主机接收时:检查状态寄存器的:BUSY:总线忙,MSL:主从模式,ADDR:地址已被发送
EV8:检查状态寄存器的:TRA:发送/接收,BUSY:总线忙,MSL:主从模式,TxE:数据寄存器为空,BTF:字节发送结束
EV7:检查状态寄存器的:BUSY:总线忙,MSL:主从模式,RxNE:数据寄存器非空

要用I2C接口,首先要初始化相关的GPIO及I2C的参数:
/* tsensor.c */
void I2C_LM75_Init(void)
{
  GPIO_InitTypeDef  GPIO_InitStructure;
  I2C_InitTypeDef   I2C_InitStructure;
  /* GPIOB Periph clock enable */
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOB, ENABLE);
  /* 打开I2C1时钟*/
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
  /* 配置I2C1管脚: SCL and SDA */
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_6 | GPIO_Pin_7;  //设置I2C1管脚PB6,PB7
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;  //设置为复用开漏输出
  GPIO_Init(GPIOB, &GPIO_InitStructure);
  /* 配置PB5为上拉输入, 连接LM75A的INT管脚 */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置为输入上拉
  GPIO_Init(GPIOB, &GPIO_InitStructure);
  I2C_DeInit(I2C1);  //xx_DeInit仅DEBUG时用
  /* I2C1 参数初始化 */
  I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;   //设置为I2C模式,区别于SMBus模式
  I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; //设置I2C快速模式Tlow / Thigh = 2
  I2C_InitStructure.I2C_OwnAddress1 = 0x00;  //设置I2C自身设备地址,我们这里STM32的I2C做主机用,设为0
  I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;  //使能ACK应答位
  I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;  //响应7位地址,仅作从机时用
  I2C_InitStructure.I2C_ClockSpeed = 200000;  //设置I2C时钟频率
  I2C_Init(I2C1, &I2C_InitStructure);
  /*打开I2C1 */
  I2C_Cmd(I2C1, ENABLE);
}

三、了解LM75A工作原理及相关操作
1、概述
LM75A是NXP半导体公司推出的I2C接口的数字温度传感器,它也是一个温度检测器,提供过热检测输出功能。
LM75A 可设置成工作在两种模式:正常工作模式或关断模式。我们这里只讨论正常模式。

2、LM75A管脚电路图:



百为STM32开发板上连接LM75A的电路图(STLM75M2E是另一个器件的名称,我们只要采用的是LM75A):




3、LM75A地址及寄存器
LM75A的7位地址:其中高4位固定为1001,低3位由管脚A2,A1,A0决定




LM75A 包含许多寄存器:
(1)指针寄存器,用来选择要操作的寄存器



其中最低两位B1,B0表示要操作的寄存器




(2)配置寄存器:8位寄存器,用来配置器件的工作模式



(3)温度寄存器:16位寄存器,只有其中高11位有效,存放以0.125℃为单位的温度值补码



(4)滞后寄存器(Thyst):当环境温度低于此温度的时候,LM75A将根据当前模式(比较、中断)控制OS引脚作出相应反应。
(5)过热关断阈值寄存器(Tos):当环境温度高于此温度的时候,LM75A将根据当前模式(比较、中断)控制OS引脚作出相应反应。
这两个寄存器的作用是控制OS管脚的输出。具体可以参考LM75A数据手册。

4、LM75A相关操作时序
(这里只介绍含指针字节的情况,预置指针的情况请参考LM75A数据手册)
写配置寄存器



读配置寄存器




写Tos 或Thyst 寄存器




读包含指针字节的Temp、Tos 或Thyst 寄存器




四、通过STM32的I2C接口读取LM75A的温度并打印到串口上
/* main.c */

  TempReadValue = I2C_LM75_Temp_Read();  //读取LM75A温度寄存器值
  temperature = TempReadValue*0.125;  //转换成实际的温度值
  printf("TempReadValue = %d\n\r", TempReadValue);
  printf("temperature = %.3f\n\r", temperature);

/* tsensor.c */

u16 I2C_LM75_Temp_Read(void)
{
  u32 RegValue = 0;
  /* 使能I2C1 ACK应答位(如果ACK被其他功能模块取消的话)*/
  I2C_AcknowledgeConfig(I2C1, ENABLE);
  /*----- 发送阶段-----*/
  /* 发送I2C起始条件 */
  I2C_GenerateSTART(I2C1, ENABLE);
  
  /* 等待EV5事件 */
  while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))  /* EV5 */
  {
  }
  
  /* 发送LM75A从机地址,写操作 */
  I2C_Send7bitAddress(I2C1, LM75_Addr, I2C_Direction_Transmitter);
  
  /* 等待EV6事件 */
  while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) /* EV6 */
  {
  }
  /* 发送温度寄存器指针值 */
  I2C_SendData(I2C1, LM75_TEMP_Reg);
  
  /* 等待EV8事件*/
  while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) /* EV8 */
  {
  }
  /*----- 接收阶段 -----*/
  /* 发送重复起始条件 */
  I2C_GenerateSTART(I2C1, ENABLE);
  
  /* 等待EV5事件 */
  while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))  /* EV5 */
  {
  }
  
  /* 发送LM75A从机地址,读操作 */
  I2C_Send7bitAddress(I2C1, LM75_Addr, I2C_Direction_Receiver);
  
  /* 等待EV6事件 */
  while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))  /* EV6 */
  {
  }
  /* 等待EV7事件 */
  while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED))  /* EV7 */
  {
  }
  
  /* 接收LM75A温度值高8位 */
  RegValue = I2C_ReceiveData(I2C1) << 8;
  
  /* 取消应答位,LM75A读温度的第二个字节不需要应答位,参考LM75A的时序图 */
  I2C_AcknowledgeConfig(I2C1, DISABLE);
  
  /* 发送停止条件 */
  I2C_GenerateSTOP(I2C1, ENABLE);
  
  /* 等待EV7事件 */
  while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED))  /* EV7 */
  {
  }
  
  /* 接收LM75A温度值低8位 */
  RegValue |= I2C_ReceiveData(I2C1);
  /* 返回温度值高11位 */
  return (RegValue >> 5);
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

发表于 2013-8-5 11:00:55 | 显示全部楼层
收藏了,多谢

出0入0汤圆

 楼主| 发表于 2013-8-5 11:46:24 | 显示全部楼层
注意:STM32的FSMC和I2C1有冲突,使用I2C1的时候还要把FSMC关掉,使用完后再打开:
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, DISABLE);
……I2C1程序……
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE);

出0入0汤圆

 楼主| 发表于 2013-8-10 11:07:41 | 显示全部楼层
百为STM32开发板教程之十一——NOR FLASH

参考文档:
百为stm32开发板光盘\st官方参考资料\Application notes\AN2784 Using the high-density STM32F10xxx FSMC peripheral to drive external memories.pdf
百为stm32开发板光盘\芯片数据手册\M29W128G.pdf
百为stm32开发板光盘\芯片数据手册\STM32F10xxx参考手册CD00171190.pdf

实验目的: 实现擦除和读写M29W128GH NOR FLASH的第二个块

主要内容:
一、了解STM32的FSMC接口
二、了解M29W128GH NOR FLASH的相关操作
三、编程实现擦除并读写M29W128GH NOR FLASH的第二个块

一、了解STM32的FSMC接口
1、STM32 FSMC功能框图:
FSMC主要包括有:AHB接口(包含FSMC配置寄存器) , NOR闪存和PSRAM控制器,NAND闪存和PC卡控制器,外部设备接口



2、STM32 FSMC外部设备地址映射:
从FSMC的角度看,可以把外部存储器划分为固定大小为256M字节的四个存储块,见下图
● 存储块1用于访问最多4个NOR闪存或PSRAM存储设备。这个存储区又被划分为4个NOR/PSRAM区,并有4个专用的片选。
● 存储块2和3用于访问NAND闪存设备,每个存储块连接一个NAND闪存。
● 存储块4用于访问PC卡设备
每一个存储块上的存储器类型是由用户在配置寄存器中定义的。



其中存储块1用于NOR/PSRAM存储设备。这个范围又可以分为4个小范围 ,每个小范围对应STM32的一个管脚/片选,FSMC_NEx(x=1,2,3,4)


(1) HADDR是需要转换到外部存储器的内部AHB地址线。

我们百为STM3210E-EVAL开发板的M29W128 NOR FLASH用的是FSMC_NE2
所以地址为0x60000000 | 0x1<<26 = 0x64000000, 其中0x1就是内部的地址线HADDR[27:26]的值01。

HADDR[25:0]包含外部存储器地址。HADDR是字节地址,而存储器访问不都是按字节访问,因此接到存储器的地址线依存储器的数据宽度有所不同,如下表:



所以在输出NOR FLASH地址时要左移动1位:
NOR_WRITE(ADDR_SHIFT(0x0555), 0x00AA);
#define ADDR_SHIFT(A) (Bank1_NOR2_ADDR + (2 * (A)))

3、STM32的FSMC与M29W128GH的硬件连接
百为STM3210E-EVAL开发板STM32和M29W128GH的连接电路图如下:



STM32和M29W128GH管脚连接对应如下:



STM32配置NOR FLASH的IO端口及对应FSMC模块初始化代码:
void FSMC_NOR_Init(void)
{
  FSMC_NORSRAMInitTypeDef  FSMC_NORSRAMInitStructure;
  FSMC_NORSRAMTimingInitTypeDef  p;
  GPIO_InitTypeDef GPIO_InitStructure;
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE |
                         RCC_APB2Periph_GPIOF | RCC_APB2Periph_GPIOG, ENABLE);
  /*-- 配置GPIO  ------------------------------------------------------*/
  /* 配置NOR 数据线D[15:0] */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_8 | GPIO_Pin_9 |
                                GPIO_Pin_10 | GPIO_Pin_14 | GPIO_Pin_15;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOD, &GPIO_InitStructure);
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 |
                                GPIO_Pin_11 | GPIO_Pin_12 | GPIO_Pin_13 |
                                GPIO_Pin_14 | GPIO_Pin_15;
  GPIO_Init(GPIOE, &GPIO_InitStructure);
  /* 配置NOR 地址线A[22:0] */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 |
                                GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_12 | GPIO_Pin_13 |
                                GPIO_Pin_14 | GPIO_Pin_15;
  GPIO_Init(GPIOF, &GPIO_InitStructure);
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 |
                                GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5;                           
  GPIO_Init(GPIOG, &GPIO_InitStructure);
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_12 | GPIO_Pin_13;
  GPIO_Init(GPIOD, &GPIO_InitStructure);
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6;
  GPIO_Init(GPIOE, &GPIO_InitStructure);
  /* 配置NOE 和NWE  */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
  GPIO_Init(GPIOD, &GPIO_InitStructure);
  /* 配置NE2  */
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
  GPIO_Init(GPIOG, &GPIO_InitStructure);
  /*-- 配置FSMC ----------------------------------------------------*/
  p.FSMC_AddressSetupTime = 0x05;
  p.FSMC_AddressHoldTime = 0x00;
  p.FSMC_DataSetupTime = 0x07;
  p.FSMC_BusTurnAroundDuration = 0x00;
  p.FSMC_CLKDivision = 0x00;
  p.FSMC_DataLatency = 0x00;
  p.FSMC_AccessMode = FSMC_AccessMode_B;
  FSMC_NORSRAMInitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM2;
  FSMC_NORSRAMInitStructure.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable;
  FSMC_NORSRAMInitStructure.FSMC_MemoryType = FSMC_MemoryType_NOR;
  FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b;
  FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode = FSMC_BurstAccessMode_Disable;
  FSMC_NORSRAMInitStructure.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low;
  FSMC_NORSRAMInitStructure.FSMC_WrapMode = FSMC_WrapMode_Disable;
  FSMC_NORSRAMInitStructure.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState;
  FSMC_NORSRAMInitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Enable;
  FSMC_NORSRAMInitStructure.FSMC_WaitSignal = FSMC_WaitSignal_Disable;
  FSMC_NORSRAMInitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Disable;
  FSMC_NORSRAMInitStructure.FSMC_AsyncWait = FSMC_AsyncWait_Disable;
  FSMC_NORSRAMInitStructure.FSMC_WriteBurst = FSMC_WriteBurst_Disable;
  FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct = &p;
  FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct = &p;
  FSMC_NORSRAMInit(&FSMC_NORSRAMInitStructure);
  /* 使能FSMC Bank1_NOR Bank */
  FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM2, ENABLE);
}

4、STM32 FSMC的NOR读写时序:
读操作时序:



写操作时序:



这里就是FSMC的核心内容了,从时序图可以看到:
读操作时输出A[25:0]信号和NEx,NOE等信号,输出完这些信号后,器件就会在D[15:0]总线上返回我们要读的数据了
写操作时输出A[25:0]信号和NEx,NWE等信号,输出完这些信号后,我们就可以在D[15:0]总线上发送要写的数据了

而这些信号的控制是由FSMC来自动控制的,不需要我们手动去控制每个管脚,我们只要在程序里像操作内存一样操作就可以了
例如:
读数据:
Data = *(vu16 *) ADDR_SHIFT(0x0000);
写数据:
*(vu16 *)(Address) = Data;


二、M29W128GH NOR FLASH的相关操作
1、M29W128GH简介
M29W128GH一共有128个块,每个块有128K个字节(64K字)。其中最后一个块可以有VPP/WP管脚设定写保护,当VPP/WP为低电平时写保护。

2、操作命令表



(1)读ID命令
从命令表可以看出读ID命令,需要输出3个地址和数据序列。
其中输出3个序列后,再输出指定的地址就可以得到对应的数据:


具体如下:
void FSMC_NOR_ReadID(NOR_IDTypeDef* NOR_ID)
{
  NOR_WRITE(ADDR_SHIFT(0x0555), 0x00AA);  //输出地址0x0555,数据0x00AA
  NOR_WRITE(ADDR_SHIFT(0x02AA), 0x0055);  //输出地址0x02AA,数据0x0055
  NOR_WRITE(ADDR_SHIFT(0x0555), 0x0090);  //输出地址0x0555,数据0x0090
  NOR_ID->Manufacturer_Code = *(vu16 *) ADDR_SHIFT(0x0000);  //上面Table7中A3,A2,A1,A0 = 0x0000,即可读出数据0020h
  NOR_ID->Device_Code1 = *(vu16 *) ADDR_SHIFT(0x0001); //上面Table7中A3,A2,A1,A0 = 0x0001,即可读出数据227Eh
  NOR_ID->Device_Code2 = *(vu16 *) ADDR_SHIFT(0x000E); //上面Table7中A3,A2,A1,A0 = 0x000E,即可读出数据2221h
  NOR_ID->Device_Code3 = *(vu16 *) ADDR_SHIFT(0x000F); //上面Table7中A3,A2,A1,A0 = 0x000F,即可读出数据2201h
}

(2)复位/返回读模式命令
NOR_Status FSMC_NOR_ReturnToReadMode(void)
{
  NOR_WRITE(Bank1_NOR2_ADDR, 0x00F0);  //输出任意地址,数据0x00F0
  return (NOR_SUCCESS);
}

(3)擦除块命令(Block Erase)
NOR_Status FSMC_NOR_EraseBlock(u32 BlockAddr)
{
  NOR_WRITE(ADDR_SHIFT(0x0555), 0x00AA);  //输出地址0x0555,数据0x00AA
  NOR_WRITE(ADDR_SHIFT(0x02AA), 0x0055);  //输出地址0x02AA,数据0x0055
  NOR_WRITE(ADDR_SHIFT(0x0555), 0x0080);  //输出地址0x0555,数据0x0080
  NOR_WRITE(ADDR_SHIFT(0x0555), 0x00AA);  //输出地址0x0555,数据0x00AA
  NOR_WRITE(ADDR_SHIFT(0x02AA), 0x0055);  //输出地址0x02AA,数据0x0055
  NOR_WRITE((Bank1_NOR2_ADDR + BlockAddr), 0x30);  //擦除指定的地址BlockAddr的内容
  return (FSMC_NOR_GetStatus(BlockErase_Timeout));
}

(4)写命令
NOR_Status FSMC_NOR_WriteHalfWord(u32 WriteAddr, u16 Data)
{
  NOR_WRITE(ADDR_SHIFT(0x0555), 0x00AA);  //输出地址0x0555,数据0x00AA
  NOR_WRITE(ADDR_SHIFT(0x02AA), 0x0055);  //输出地址0x02AA,数据0x0055
  NOR_WRITE(ADDR_SHIFT(0x0555), 0x00A0);  //输出地址0x0555,数据0x00A0
  NOR_WRITE((Bank1_NOR2_ADDR + WriteAddr), Data);  //写16 bit数据到指定地址WriteAddr
  return (FSMC_NOR_GetStatus(Program_Timeout));
}

(5)读命令,不存在读命令了,直接读指定地址数据就可以了
*pBuffer++ = *(vu16 *)((Bank1_NOR2_ADDR + ReadAddr));

三、编程实现擦除并读写M29W128GH NOR FLASH的第二个块

/* main.c */

  /* 打开FSMC时钟 */
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE);
  /* 初始化FSMC Bank1 NOR/SRAM2 */
  FSMC_NOR_Init();

  /* 读M29W128GH ID */
  FSMC_NOR_ReadID(&NOR_ID);
  /* 复位/返回读模式 */
  FSMC_NOR_ReturnToReadMode();
  /* 擦除要写入的块 */
  FSMC_NOR_EraseBlock(WRITE_READ_ADDR);
  /* 填充要写入的数据到TxBuffer */
  Fill_Buffer(TxBuffer, BUFFER_SIZE, 0x3210);
  /* 写数据到NOR FLASH中 */
  FSMC_NOR_WriteBuffer(TxBuffer, WRITE_READ_ADDR, BUFFER_SIZE);
  /* 从NOR FLASH中读出数据 */
  FSMC_NOR_ReadBuffer(RxBuffer, WRITE_READ_ADDR, BUFFER_SIZE);  
  /* 比较写入的数据和读出的数据看是否相等*/   
  for (Index = 0x00; (Index < BUFFER_SIZE) && (WriteReadStatus == 0); Index++)
  {
    if (RxBuffer[Index] != TxBuffer[Index])
    {
      WriteReadStatus = Index + 1;
    }
  }
  if (WriteReadStatus == 0)
  {
    /*  如果相等,点亮LED1 */
    GPIO_SetBits(GPIOF, GPIO_Pin_6);
  }
  else
  {
    /* 否则,点亮LED2 */
    GPIO_SetBits(GPIOF, GPIO_Pin_7);     
  }

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入34汤圆

发表于 2013-8-10 12:25:36 | 显示全部楼层
我顶 xi_liang 兄,现在这开发板的教程与例程是越来越丰富了 ...

出0入0汤圆

发表于 2013-8-11 17:29:08 来自手机 | 显示全部楼层
mark……
顶一个…

出0入0汤圆

发表于 2013-8-12 19:36:59 | 显示全部楼层
能开源到这个地步,真的做得很好了,我已经有学习板了,所以不打算再要了,但也下载来看看,强力支持。

出0入0汤圆

发表于 2013-8-13 10:50:32 | 显示全部楼层
哈哈,STM32怎么能不火呢

出0入0汤圆

发表于 2013-8-13 15:04:58 | 显示全部楼层
嗯。不错!下载了!要是有关于MODBUS-RTU通讯的例子就完美了

出0入0汤圆

 楼主| 发表于 2013-8-21 16:11:26 | 显示全部楼层
xyz543 发表于 2013-8-10 12:25
我顶 xi_liang 兄,现在这开发板的教程与例程是越来越丰富了 ...

多谢xyz543兄帮顶

出0入0汤圆

 楼主| 发表于 2013-8-21 16:13:05 | 显示全部楼层
zhepingli 发表于 2013-8-13 15:04
嗯。不错!下载了!要是有关于MODBUS-RTU通讯的例子就完美了

MODBUS要是用得多,后面也可以考虑加入的。
最近在弄VS1003,ENC28J60,MMA7455模块的驱动,很快会弄好了

出0入53汤圆

发表于 2013-8-21 16:29:17 | 显示全部楼层
强烈支持LZ的精神,LZ顶你,先收藏了

出0入0汤圆

发表于 2013-8-21 17:27:26 | 显示全部楼层
怎么木有看手机界面的gui

出0入0汤圆

发表于 2013-8-21 18:17:38 | 显示全部楼层
感谢楼主的无私

出0入0汤圆

 楼主| 发表于 2013-8-22 13:55:54 | 显示全部楼层
百为STM32开发板教程之十二——NAND FLASH

参考资料:
百为stm32开发板光盘V3\百为stm32开发板光盘\芯片数据手册\K9F1208.pdf
百为stm32开发板光盘\st官方参考资料\Application notes\AN2784 Using the high-density STM32F10xxx FSMC peripheral to drive external memories.pdf

实验目的:实现擦除NAND FLASH的第一个块,并读写NAND FLASH的开头两页

主要内容:
一、了解STM32 FSMC NAND控制器
二、了解K9F1208 NAND FLASH的原理与操作
三、编程实现擦除K9F1208 NAND FLASH的第一个块,并读写K9F1208 NAND FLASH的开头两页

一、STM32 FSMC NAND控制器
1、1、STM32 FSMC功能框图:
FSMC主要包括有:AHB接口(包含FSMC配置寄存器) , NOR闪存和PSRAM控制器,NAND闪存和PC卡控制器,外部设备接口


2、STM32 FSMC外部设备地址映射:
从FSMC的角度看,可以把外部存储器划分为固定大小为256M字节的四个存储块,见下图
● 存储块1用于访问最多4个NOR闪存或PSRAM存储设备。这个存储区又被划分为4个NOR/PSRAM区,并有4个专用的片选。
● 存储块2和3用于访问NAND闪存设备,每个存储块连接一个NAND闪存。
● 存储块4用于访问PC卡设备
每一个存储块上的存储器类型是由用户在配置寄存器中定义的。



其中块2和块3属于NAND存储块,每个存储块又可以划分为通用和属性空间



通用和属性空间又可以在低256K字节部分划分为3个区
● 数据区(通用/属性空间的前64K字节区域)
● 命令区(通用/属性空间的第2个64K字节区域)
● 地址区(通用/属性空间的第2个128K字节区域)


应用软件使用这3个区访问NAND闪存存储器:
● 发送命令到NAND闪存存储器:软件只需对命令区的任意一个地址写入命令即可。
● 指定操作NAND闪存存储器的地址:软件只需对地址区的任意一个地址写入命令即可。因为一个NAND地址可以有4或5个字节(依实际的存储器容量而定),需要连续地执行对地址区的写才能输出完整的操作地址。
● 读写数据:软件只需对数据区的任意一个地址写入或读出数据即可。 因为NAND闪存存储器自动地累加其内部的操作地址,读写数据时没有必要变换数据区的地址,即不必对连续的地址区操作。

3、STM32的NAND控制信号
(1)STM32 FSMC NAND控制信号描述:



(2)STM32 FSMC信号连接K9F1208 NAND FLASH:



(3)百为STM3210E-EVAL开发板上STM32和K9F1208的连接电路图(这里电路图上画的是NAND512,实际焊接的硬件是K9F1208):




二、K9F1208 NAND FLASH的原理与操作
1、K9F1208 的硬件结构组织
K9F1208是容量为512M bit,即64M byte的存储器,它是由4096个块(block)组成,其中每个块又是由32个页(page)组成,每个页由512byte+16byte组成。
K9F1208的读写都是以页为单位,而擦除则是以块为单位。



程序中相关定义:
/* FSMC NAND memory parameters */
#define NAND_PAGE_SIZE             ((u16)0x0200) /* 512 bytes per page w/o Spare Area */
#define NAND_BLOCK_SIZE            ((u16)0x0020) /* 32x512 bytes pages per block */
#define NAND_ZONE_SIZE             ((u16)0x0400) /* 1024 Block per zone */
#define NAND_SPARE_AREA_SIZE       ((u16)0x0010) /* last 16 bytes as spare area */
#define NAND_MAX_ZONE              ((u16)0x0004) /* 4 zones of 1024 block */

2、K9F1208引脚定义



3、K9F1208的操作命令集



(1)读ID命令:


先输出命令90H,再输出地址00H,然后读回4个字节的数据即是K9F1208的ID,ECH,76H,5AH,3FH
代码如下:
void FSMC_NAND_ReadID(NAND_IDTypeDef* NAND_ID)
{
  u32 data = 0;
  /* 发送命令到命令区0x70010000 */  
  *(vu8 *)(Bank_NAND_ADDR | CMD_AREA) = 0x90;
  *(vu8 *)(Bank_NAND_ADDR | ADDR_AREA) = 0x00;
   /* 从K9F1208 NAND FLASH读回ID序列 */
   data = *(vu32 *)(Bank_NAND_ADDR | DATA_AREA);  //从数据区0x70000000读回数据
   NAND_ID->Maker_ID   = ADDR_1st_CYCLE (data);
   NAND_ID->Device_ID  = ADDR_2nd_CYCLE (data);
   NAND_ID->Third_ID   = ADDR_3rd_CYCLE (data);
   NAND_ID->Fourth_ID  = ADDR_4th_CYCLE (data);  
}

(2)块擦除命令


块擦除是先输出60H,再输出块地址,然后输出D0H,用70H读回状态,等待操作完成即可
u32 FSMC_NAND_EraseBlock(NAND_ADDRESS Address)
{
  *(vu8 *)(Bank_NAND_ADDR | CMD_AREA) = NAND_CMD_ERASE0;  //发送命令60H到命令区0x70010000
  *(vu8 *)(Bank_NAND_ADDR | ADDR_AREA) = ADDR_1st_CYCLE(ROW_ADDRESS);  //发送地址A9~A16到地址区0x70020000
  *(vu8 *)(Bank_NAND_ADDR | ADDR_AREA) = ADDR_2nd_CYCLE(ROW_ADDRESS); //发送地址A17~A24到地址区0x70020000
  *(vu8 *)(Bank_NAND_ADDR | ADDR_AREA) = ADDR_3rd_CYCLE(ROW_ADDRESS);  //发送地址A25到地址区0x70020000
   
  *(vu8 *)(Bank_NAND_ADDR | CMD_AREA) = NAND_CMD_ERASE1;  //发送命令D0H到命令区0x70010000
  return (FSMC_NAND_GetStatus());  //读回操作结果
}

(4)页写入命令
因为每个页(page)可分为A,B,C三个区



所以页写入也分为三种方式:



具体时序:



我们这里采用的第一种方式,可以写入0~528byte的数据。K9F1208属于小页的NAND(512byte+16byte),区别于大页的NAND(2048byte+64byte)
u32 FSMC_NAND_WriteSmallPage(u8 *pBuffer, NAND_ADDRESS Address, u32 NumPageToWrite)
{
  u32 index = 0x00, numpagewritten = 0x00, addressstatus = NAND_VALID_ADDRESS;
  u32 status = NAND_READY, size = 0x00;
  while((NumPageToWrite != 0x00) && (addressstatus == NAND_VALID_ADDRESS) && (status == NAND_READY))
  {
    /* 页写命令和地址 */
    *(vu8 *)(Bank_NAND_ADDR | CMD_AREA) = NAND_CMD_AREA_A;  //发送命令00H到命令区0x70010000,从A区开始写入
    *(vu8 *)(Bank_NAND_ADDR | CMD_AREA) = NAND_CMD_WRITE0;  //发送命令80H到命令区0x70010000
    *(vu8 *)(Bank_NAND_ADDR | ADDR_AREA) = 0x00;  //发送地址A0~A7到地址区0x70020000,从地址0开始写入  
    *(vu8 *)(Bank_NAND_ADDR | ADDR_AREA) = ADDR_1st_CYCLE(ROW_ADDRESS); //发送地址A9~A16到地址区0x70020000  
    *(vu8 *)(Bank_NAND_ADDR | ADDR_AREA) = ADDR_2nd_CYCLE(ROW_ADDRESS);  //发送地址A17~A24到地址区0x70020000  
    *(vu8 *)(Bank_NAND_ADDR | ADDR_AREA) = ADDR_3rd_CYCLE(ROW_ADDRESS);  //发送地址A25到地址区0x70020000
    /* 计算写入数据的大小 */
    size = NAND_PAGE_SIZE + (NAND_PAGE_SIZE * numpagewritten);
    /* 写入数据 */
    for(; index < size; index++)
    {
      *(vu8 *)(Bank_NAND_ADDR | DATA_AREA) = pBuffer[index];  //发送数据到数据区0x70000000
    }
   
    /* 检查状态看是否操作成功 */
    status = FSMC_NAND_GetStatus();
   
    if(status == NAND_READY)  //如果操作完成
    {
      numpagewritten++;  //已写入页数加1
      NumPageToWrite--;  //待写入页数减1
      /* 计算要写入的下一个小页的地址 */
      addressstatus = FSMC_NAND_AddressIncrement(&Address);   
    }   
  }
  
  return (status | addressstatus);
}

(5)read 1页读命令


页读命令是先输出00H,再输出要读入的页地址,然后就可以读回最多528byte的数据了。
u32 FSMC_NAND_ReadSmallPage(u8 *pBuffer, NAND_ADDRESS Address, u32 NumPageToRead)
{
  u32 index = 0x00, numpageread = 0x00, addressstatus = NAND_VALID_ADDRESS;
  u32 status = NAND_READY, size = 0x00;
  while((NumPageToRead != 0x0) && (addressstatus == NAND_VALID_ADDRESS))
  {   
    /* 页读命令和页地址*/
    *(vu8 *)(Bank_NAND_ADDR | CMD_AREA) = NAND_CMD_AREA_A;   //发送命令00H到命令区0x70010000
   
    *(vu8 *)(Bank_NAND_ADDR | ADDR_AREA) = 0x00;  ////发送地址A0~A7(00H)到地址区0x70020000
    *(vu8 *)(Bank_NAND_ADDR | ADDR_AREA) = ADDR_1st_CYCLE(ROW_ADDRESS); //发送地址A9~A16到地址区0x70020000
    *(vu8 *)(Bank_NAND_ADDR | ADDR_AREA) = ADDR_2nd_CYCLE(ROW_ADDRESS);  //发送地址A17~A24到地址区0x70020000
    *(vu8 *)(Bank_NAND_ADDR | ADDR_AREA) = ADDR_3rd_CYCLE(ROW_ADDRESS);   //发送地址A25到地址区0x70020000
     
    /* 计算要读的数据大小 */
    size = NAND_PAGE_SIZE + (NAND_PAGE_SIZE * numpageread);
   
    /* 读数据到pBuffer */   
    for(; index < size; index++)
    {
      pBuffer[index]= *(vu8 *)(Bank_NAND_ADDR | DATA_AREA);  //从数据区0x70000000读回数据
    }
    numpageread++;  //已读的页数加1
   
    NumPageToRead--;  //待读的页数减1
    /* 计算下一个要读的页地址 */               
    addressstatus = FSMC_NAND_AddressIncrement(&Address);
  }
  /* 检查状态看是否操作成功 */
  status = FSMC_NAND_GetStatus();
  
  return (status | addressstatus);
}

三、编程实现擦除K9F1208 NAND FLASH的第一个块,并读写K9F1208 NAND FLASH的开头两页

/* main.c */

  /* FSMC NAND初始化 */
  FSMC_NAND_Init();
  /* 读NAND ID操作 */
  FSMC_NAND_ReadID(&NAND_ID);
  /* 检查ID是否正确 */
  if((NAND_ID.Maker_ID == NAND_K9F1208_MakerID) && (NAND_ID.Device_ID == NAND_K9F1208_DeviceID))
  {
    /* 初始化要写入NAND的页地址 */
    WriteReadAddr.Zone = 0x00;
    WriteReadAddr.Block = 0x00;
    WriteReadAddr.Page = 0x00;
    /* 擦除NAND FLASH的第一个块(第1和第2页所在的块) */
    status = FSMC_NAND_EraseBlock(WriteReadAddr);
    /* 写数据到NAND FLASH的第1和第2页 */
    /* 填充要发送的数据到buffer */
    Fill_Buffer(TxBuffer, BUFFER_SIZE , 0x66);
    status = FSMC_NAND_WriteSmallPage(TxBuffer, WriteReadAddr, PageNumber);  //PageNumber=2,表示要写入第1和第2页
    /* 从NAND FLASH读回数据 */
    status = FSMC_NAND_ReadSmallPage (RxBuffer, WriteReadAddr, PageNumber);
   
    /* 比较写入的数据和读回的数据是否相等 */
    for(j = 0; j < BUFFER_SIZE; j++)
    {
      if(TxBuffer[j] != RxBuffer[j])
      {     
        WriteReadStatus++;
      }
    }
    if (WriteReadStatus == 0)
    {
      /* 如果相等,则点亮LED1 */
      GPIO_SetBits(GPIOF, GPIO_Pin_6);
    }
    else
    {
      /* 否则,点亮LED2 */
      GPIO_SetBits(GPIOF, GPIO_Pin_7);     
    }
  }
  else //ID不正确
  {
    /* 点亮LED3 */
    GPIO_SetBits(GPIOF, GPIO_Pin_8);  
  }

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 2013-8-27 18:01:11 | 显示全部楼层
最近在调试MMA7455重力加速度,VS1003 MP3模块,ENC28J60网络模块,NRF24L01,
等调试完,后面继续SDIO教程,UCOS教程,UCGUI教程

出0入0汤圆

发表于 2013-8-27 18:42:32 | 显示全部楼层
楼主无私啊,只下不顶太不人道了

出0入0汤圆

发表于 2013-8-27 18:53:42 | 显示全部楼层
mark           

出0入0汤圆

发表于 2013-8-28 09:10:06 | 显示全部楼层
谢谢楼主

出0入0汤圆

发表于 2013-8-30 16:31:09 | 显示全部楼层
正在用百位的板子,狂顶ing

出0入0汤圆

发表于 2013-8-30 18:19:44 | 显示全部楼层
顶一个,资料已经下载了,慢慢消化了

出0入0汤圆

 楼主| 发表于 2013-9-1 16:58:33 | 显示全部楼层
caizhiwei 发表于 2013-8-30 16:31
正在用百位的板子,狂顶ing

多谢支持啊

出0入0汤圆

 楼主| 发表于 2013-9-4 10:43:18 | 显示全部楼层
更新源码
百为MMA7455重力加速度程序.rar
百为VS1003_MP3程序.rar
百为_NRF24L01程序.rar
已上传到网盘

出0入0汤圆

 楼主| 发表于 2013-9-6 12:57:25 | 显示全部楼层
更新模块合集图

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

发表于 2013-9-6 13:57:38 | 显示全部楼层
谢谢楼主分享。 收藏。。

出0入0汤圆

发表于 2013-9-7 00:35:34 来自手机 | 显示全部楼层
早点看到就好了

出0入0汤圆

发表于 2013-9-7 14:02:05 | 显示全部楼层
灰常感谢

出0入0汤圆

发表于 2013-9-7 17:07:45 | 显示全部楼层
呵呵,还不错啊

出100入101汤圆

发表于 2013-9-7 17:13:43 | 显示全部楼层
手机界面demo程序还没好

出0入0汤圆

 楼主| 发表于 2013-9-8 18:50:55 | 显示全部楼层
本帖最后由 xi_liang 于 2013-9-9 07:03 编辑

百为STM32开发板教程之十四——百为STM32开发板使用外扩SRAM步骤(用3.5固件库)

1、在工程选项里定义外部SRAM的起始地址和容量大小,百为STM3210E-EVAL的外部SRAM起始地址是0x68000000(FSMC_NE3),容量大小是0x100000(1M)


2、开启宏定义DATA_IN_ExtSRAM

在启动代码startup_stm32f10x_hd.s里有如下几行代码,其中会调用SystemInit这个函数,
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0               
                LDR     R0, =__main
                BX      R0

SystemInit这个函数在system_stm32f10x.c里定义,这个函数又包含如下几行代码
#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
  #ifdef DATA_IN_ExtSRAM
    SystemInit_ExtMemCtl();
  #endif /* DATA_IN_ExtSRAM */
#endif

这里就是重点了,SystemInit_ExtMemCtl() 就是初始化FSMC接口外部SRAM的代码
也就是说只要在系统启动的时候调用这个函数,启动完后就能用外部SRAM了
但这里有个#ifdef DATA_IN_ExtSRAM,所以我们要定义这个宏:
#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
#define DATA_IN_ExtSRAM
#endif

其中#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)这行代码,
在工程设置里是有定义了的STM32F10X_HD的,所以上面#if和#endif中间的内容是会执行的。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 2013-9-8 18:58:21 | 显示全部楼层
最近接了个项目,会把项目做的东西体现在开发板上。会在UCGUI界面加入USB拷贝图片、字库到NOR FLASH功能,采用FATFS文件系统。

出0入0汤圆

 楼主| 发表于 2013-9-9 07:05:07 | 显示全部楼层
百为STM32开发板教程之十五——TIM通用定时器

使用定时器的步骤:
1、打开定时器时钟
  /* TIM2 时钟使能 */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

2、使能定时器NVIC中断
  /*使能定时器2中断*/
  NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQChannel;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

3、配置定时器参数
  /* 定时器基本配置 */
  TIM_TimeBaseStructure.TIM_Prescaler = 2;  //定时器计数频率12Mhz
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
  TIM_TimeBaseStructure.TIM_Period = 12000;  //1ms溢出中断
  TIM_TimeBaseStructure.TIM_ClockDivision = 0x0;
  TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

4、打开定时器,开启中断
    /* 使能TIM2向上计数中断 */
    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
    /* TIM2计数使能 */
    TIM_Cmd(TIM2, ENABLE);


其中
TIM2的时钟 = PCLK1/(TIM_Prescaler+1)=36Mhz/(TIM_Prescaler+1)= 12Mhz

TIM_CounterMode_Up表示向上计数模式

TIM_Period:
在向上计数模式中,计数器从0计数到自动加载值TIM_Period(TIMx_ARR计数器的内容),然后重新从0开始计数并且产生一个计数器溢出事件。

5、中断服务程序处理
void TIM2_IRQHandler(void)
{
  if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) //是否有溢出中断
  {
    TIM_ClearITPendingBit(TIM2, TIM_IT_Update );  //清除溢出中断
  cnt++;
  if(cnt>=1000)
  {
   cnt = 0;
   GPIO_WriteBit(GPIOF, GPIO_Pin_6, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOF, GPIO_Pin_6))); //1秒取反一次LED状态
  }
}
}

出0入0汤圆

发表于 2013-9-9 07:40:05 | 显示全部楼层
真心谢谢楼主

出0入0汤圆

 楼主| 发表于 2013-9-9 20:49:59 | 显示全部楼层
百为STM32_3.5固件库_uCOS2.92源码.rar
已更新到网盘

出0入0汤圆

发表于 2013-9-9 21:12:31 | 显示全部楼层
很详细,确实不容易,支持

出0入0汤圆

 楼主| 发表于 2013-9-10 00:15:50 | 显示全部楼层
后面会继续更新SDIO教程,USB教程,顶顶

出0入0汤圆

 楼主| 发表于 2013-9-10 03:08:56 | 显示全部楼层
本帖最后由 xi_liang 于 2013-9-10 03:11 编辑

百为STM32开发板教程之十六——DMA

实验目的:
使用DMA1的通道6,把存放在FLASH里面的一段数据用DMA传输到RAM里面。
传送完成后把数据打印到串口上。

STM32两个DMA控制器共有12个独立可配置的通道,其中DMA1有7个通道,DMA2有5个通道。
主要特性有:
1、12个独立的可配置的通道(请求):DMA1有7个通道,DMA2有5个通道
2、独立的源数据和目的数据传输宽度(字节、半字、全字),源数据和目的数据地址必须按数据传输宽度对齐。
3、每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错)
4、同一个DMA控制器上,多个DMA请求有优先级之分,共有四级:很高、高、中等和低。
5、支持存储器和存储器间的传输
6、外设和存储器、存储器和外设之间的传输

DMA的功能:
直接存储器存取(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。
无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。

DMA功能框图,如下图所示:



每次DMA的传输包括三个步骤:
1、从外设数据寄存器或者从当前外设/存储器地址寄存器指示的存储器地址取数据,第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器指定的外设基地址或存储器单元。
2、存数据到外设数据寄存器或者当前外设/存储器地址寄存器指示的存储器地址,第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器指定的外设基地址或存储器单元。
3、执行一次DMA_CNDTRx寄存器的递减操作,该寄存器包含未完成的操作数目。

DMA1各通道请求分配:


从外设(TIMx[x=1、2、3、4]、ADC1、SPI1、SPI/I2S2、I2Cx[x=1、2]和USARTx[x=1、2、3])产生的7个请求,通过逻辑或输入到DMA1控制器,这意味着同时只能有一个请求有效。

DMA2各通道请求分配:


从外设(TIMx[5、6、7、8]、ADC3、SPI/I2S3、UART4、DAC通道1、2和SDIO)产生的5个请求,经逻辑或输入到DMA2控制器,这意味着同时只能有一个请求有效。

DMA通道配置过程:
下面是配置DMA通道x的过程(x代表通道号):
1、在DMA_CPARx寄存器中设置外设寄存器的地址。发生外设数据传输请求时,这个地址将是数据传输的源或目标。
2、在DMA_CMARx寄存器中设置数据存储器的地址。发生外设数据传输请求时,传输的数据将从这个地址读出或写入这个地址。
3、在DMA_CNDTRx寄存器中设置要传输的数据量。在每个数据传输后,这个数值递减。
4、在DMA_CCRx寄存器的PL[1:0]位中设置通道的优先级。
5、在DMA_CCRx寄存器中设置数据传输的方向、循环模式、外设和存储器的增量模式、外设和存储器的数据宽度、传输一半产生中断或传输完成产生中断。
6、设置DMA_CCRx寄存器的ENABLE位,启动该通道。
一旦启动了DMA通道,它既可响应连到该通道上的外设的DMA请求。 当传输一半的数据后,半传输标志(HTIF)被置1,当设置了允许半传输中断位(HTIE)时,将产生一个中断请求。在数据传输结束后,传输完成标志(TCIF)被置1,当设置了允许传输完成中断位(TCIE)时,将产生一个中断请求。

  /* 配置DMA1 通道6 */
  DMA_DeInit(DMA1_Channel6);
  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)SRC_Const_Buffer;  //设置外设地址,把这个地址作为传送的数据源地址或目的地址
  DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)DST_Buffer;  //设置存储器地址,把这个地址作为传送的源地址或目的地址
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;  //设置数据传输的方向
  DMA_InitStructure.DMA_BufferSize = BufferSize;  //设置数据传输的大小
  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;  //设置外设的增量模式
  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  //设置存储器的增量模式
  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;  //设置外设的数据宽度
  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;  //设置存储器的数据宽度
  DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;  //设置DMA的循环模式
  DMA_InitStructure.DMA_Priority = DMA_Priority_High;  //设置DMA通道的优先级
  DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //设置存储器到存储器模式
  DMA_Init(DMA1_Channel6, &DMA_InitStructure);  //把上面的配置(DMA_InitStructure)通过DMA_Init函数写入到相应的寄存器

  /* 使能DMA1通道6传输完成中断 */
  DMA_ITConfig(DMA1_Channel6, DMA_IT_TC, ENABLE);

  /* 获得传送之前当前数据计数值 */
  CurrDataCounterBegin = DMA_GetCurrDataCounter(DMA1_Channel6);

  /* 使能DMA1通道6传输 */
  DMA_Cmd(DMA1_Channel6, ENABLE);

  /* 等待传输完成 */
  while (CurrDataCounterEnd != 0)
  {
  }
printf("Transfer complete.\n\r");

  /* 打印源数据内容 */
for(count = 0; count < BufferSize; count++)
{
  printf("0x%x ", SRC_Const_Buffer[count]);
}

  /* 打印目的数据内容 */
for(count = 0; count < BufferSize; count++)
{
  printf("0x%x ", DST_Buffer[count]);
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 2013-9-13 16:24:52 | 显示全部楼层
百为STM32开发板_摇杆USB鼠标程序.rar
百为STM32_USB_Mass_Storage.rar
百为STM32_Virtual_COM_Port.rar

USB相关程序更新到网盘
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-6-2 11:01

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表