MCU

MM32W0x2xxB 的蓝牙协议栈目前以lib 形式提供用户使用,用户无需了解蓝牙底层协议,通过调用相关接口的方式即可实现蓝牙无线传输,其中BLE 协议栈lib 放置在SRC_LIB目录下,接口定义头文件在inc 目录下。应用入口文件为main.c,应用实现代码在app.c。本章节介绍了部分对应各接口,详细完整的函数定义及使用注意事项参见官网的编程指导手册。

蓝牙相关资料下载链接:http://www.mindmotion.com.cn/download.aspx?cid=2567


上表是lib中的接口函数,在编程指导手册中都有详细的说明,大部分函数的调用很简单,其中与服务(service)及特征值定义相关的函数将在后续章节详细介绍。

1、radio_initBle

函数原型: void radio_initBle(unsigned char txpwr, unsigned char**addr/*Output*/);

函数功能:用于初始化蓝牙芯片及蓝牙协议栈。需要在协议一开始调用。

输入参数:txpwr用于配置发射功率,可取的值有TXPWR_0DBM,TXPWR_3DBM 等。

输出参数: addr 该参数返回蓝牙 MAC 地址信息, 6 个字节长度。

2、ble_run

函数原型:void ble_run(unsigned short adv_interval);

函数功能:运行蓝牙协议

输入参数:adv_interval,参数的单位为0.625us,如果160 表示100ms 的广播间隔。

注:阻塞调用。中断方式运行时,在IRQ中断处理函数中调用,参数为0

3、sconn_notifydata

函数原型: unsigned char sconn_notifydata(unsigned char* data, unsigned char len);

函数功能:通过蓝牙发送数据输入。

参数: data 需要发送的数据指针 len 数据长度。

注意事项:本接口函数会根据系统缓存情况自动拆包发送数据,但不得在原地阻塞等待反复调用本接口。

4、radio_standby

函数原型:void radio_standby(void);

函数功能:在通过该函数可以使射频模块进入standby模式。

注意事项:射频模块进入standby后不能定时唤醒(射频模块进入STOP 模式可以定时唤醒自身以及控制模块),此时需要外界给IRQ 提供上升沿电平信号才能唤醒射频模块,给PA0 提供下降沿电平才能唤醒控制模块。

5、att_notFd

函数原型: void att_notFd(unsigned char pdu_type, unsigned char attOpcode, unsigned short attHd );

函数功能:对无效特征值(或没有定义的特征值)进行操作的应答函数

注意事项:凡是无效特征值的操作需要应答本函数,可将本函数作为缺省调用。

6、ser_write_rsp_pkt

函数原型: void ser_write_rsp_pkt(unsigned char pdu_type);

函数功能:对具有 Write With Response 属性特征值写操作后的应答函数。

注意事项:对需要写应答的特征值,如果不应答会导致连接的断开。

7、att_server_rdByGrTypeRspDeviceInfo

函数原型: void att_server_rdByGrTypeRspDeviceInfo(unsigned char pdu_type);

函数功能:对缺省 Device Info 内容的应答可调用本接口函数。

输入参数: pdu 类型参数,直接引用回调函数 att_server_rdByGrType 中对应参数。

注意事项:如果用户直接使用发布包代码,可直接调用本接口函数。

8、att_server_rdByGrTypeRspPrimaryService

函数原型: void att_server_rdByGrTypeRspPrimaryService(unsigned char pdu_type,

unsigned short start_hd,

unsigned short end_hd,

unsigned char*uuid,

unsigned char uuidlen);

函数功能:应答 Primary Service 的查询,用户需按特征值实际定义的句柄及 UUID 填充对应数据。

输入参数: pdu_type PDU 类型参数,直接引用回调函数 att_server_rdByGrType中对应参 start_hd, 某个Service 对应的起始句柄值 end_hd,某个 Service 对应的结束句柄值 uuid,某个 Service 对应的 UUID 字串(Hex 值),如 0x180A 表示为 0x0a, 0x18。 uuidlen,某个 Service 对应 UUID 字串的长度。

注意事项:需要严格按照特征值定义规划填充对应参数。

9、att_server_rd

函数原型: void att_server_rd(unsigned char pdu_type,

unsigned char attOpcode

unsigned short att_hd,

unsigned char* attValue,

unsigned char datalen );

函数功能:读取某特征值的值。

输入参数:pdu_type PDU 类型参数,直接引用回调函数server_rd_rsp 中对应参数 attOpcode操作对应的值,直接引用回调函数server_rd_rsp 中对应参数att_hd 特征值对应的句柄值,直接引用回调函数server_rd_rsp 中对应参数 attValue特征值对应的值字串指针 datalen特征值字串长度。

注意事项:需要按需将对应特征值内容作为应答内容,如果对应特征值内容无效可应答 att_notFd()。

回调函数

为便于蓝牙差异化功能的灵活实现,蓝牙协议内设置了若干接口并以回调函数的方式由应用层porting实现,所有回调函数不得阻塞调用,具体函数的实现可参考SDK 发布包中对应代码的实现示例。回调函数主要包括如下的一些:

1、void gatt_user_send_notify_data_callback(void);

蓝牙连接成功后协议在空闲的时侯会调用本回调函数;

2、void UsrProcCallback(void);

蓝牙协议会周期性回调本函数;

3、void ser_prepare_write(unsigned short handle,unsigned char* attValue, unsigned short attValueLen, unsigned short att_offset);

4、void ser_execute_write(void);

以上两个函数为队列写数据回调函数。

5、unsigned char* getDeviceInfoData(unsigned char* len);

本函数GATT 中设备名称获取的回调函数;

6、void att_server_rdByGrType( unsigned char pdu_type, unsigned char attOpcode,

unsigned short st_hd, unsigned short end_hd, unsigned short att_type );

蓝牙GATT 查询服务的回调函数;

7、void ser_write_rsp(unsigned char pdu_type/*reserved*/, unsigned char attOpcode/*reserved*/,unsigned short att_hd, unsigned char* attValue/*app data pointer*/,

unsigned char valueLen_w/*app data size*/);

蓝牙GATT 写操作回调函数;

8、void server_rd_rsp(unsigned char attOpcode, unsigned short attHandle, unsigned char pdu_type);

蓝牙GATT 读操作回调函数;

9、void ConnectStausUpdate(unsigned char IsConnectedFlag);

蓝牙连接状态更新回调函数;

在使用接口函数时需要注意事项:

1、所有接口函数不得阻塞调用。

2、函数att_server_rd(...)每次调用发送的数据长度不得超过20 字节。

3、函数sconn_notifydata(...)只能在协议主循环体内调用,函数不可重入,可以发送多于20 字节的数据,协议会自动分包发送,且每个分包长度最大为20 字节。推荐一次发送的数据尽量不超过3 个分包。

4、在参考例程提供了支持配对/加密的AES加密方式:unsigned char aes_encrypt_HW(unsigned char *painText128bitBE,unsigned char *key128bitBE); //是否支持硬件AES。如果不支持请返回0,蓝牙库支持软件AES。

5、UUID 支持16bit 和128bit 两种。

来源: 灵动MM32MCU

围观 7

本章我们来看一下低功耗模式下用到的休眠和时钟配置函数。

目前MM32W0系列有n4和q1两个版本,n4主要针对需要大容量的应用方案,q1针对超低功耗精简型的应用方案,两个型号的低功耗编程操作方式相同,不同的只是MCU的时钟系统控制方式,在参考程序已经提供不同的模式下功能,用户只要改变相对应的宏定义即可实现对应的低功耗功能。

休眠函数

对于低功耗应用来说,休眠是非常关键的一个功能。开启蓝牙广播时MM32W0的控制模块有三种工作模式:正常模式、睡眠模式和停机模式。在stop模式下,射频模块都会通过IRQ引脚定时触发一个外部中断,可以借此唤醒STOP模式中的控制模块。

在阻塞模式中,休眠是蓝牙服务通过调用void McuGotoSleepAndWakeup(void) 函数实现的,对于中断模式,则是用户在代码中主动调用IrqMcuGotoSleepAndWakeup()函数来实现。

void McuGotoSleepAndWakeup(void) // auto goto sleep AND wakeup, porting api

{

if ((SleepStop)&&                                            //开启休眠功能

 (TxTimeout SCR &= 0xfb;

__WFE();

}else{                                         //STOP

SysClk48to8();                          //HSI 6分频

SCB->SCR |= 0x4;

__WFI();                                 //进入STOP模式

                                         

RCC->CR|=RCC_CR_HSION;   //从STOP模式唤醒,使能时钟

SysClk8to48();                                      //PLL倍频至48MHz

}

}

}

            

void IrqMcuGotoSleepAndWakeup(void) // auto goto sleep AND wakeup, porting api

{

if(ble_run_interrupt_McuCanSleep() == 0) return;

#ifdef USE_UART

if ((SleepStop)&&

 (TxTimeout SCR &= 0xfb;

__WFE();                                  //控制模块进入睡眠模式

}else{                                        //stop

SleepStatus = 2;

SysClk8M();

SCB->SCR |= 0x4; 

SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; 

__WFI();

}

}

#endif

}

在中断方式中,需要在中断处理函数中重新配置时钟:

void EXTI4_15_IRQHandler(void)

{

EXTI_ClearITPendingBit(EXTI_Line8); 

if(2 == SleepStatus){                //stop

RCC->CR|=RCC_CR_HSION;               //HSI使能

RCC->CR |= RCC_CR_PLLON;            //PLL使能

RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;

SysTick_Config(48000);

}

SleepStatus = 0; 

ble_run(0); 

}

时钟配置

进入低功耗前后需要配置时钟,从低功耗模式恢复时,时钟默认设置为HSI6分频,需要重新配置系统时钟。

注意:下面函数属于蓝牙库接口,没有用到也不要删除。

相关时钟配置:

void SysClk48to8(void)1

{

RCC_SYSCLKConfig(RCC_SYSCLKSource_HSI);//selecting PLL clock as sys clock

    

while (RCC_GetSYSCLKSource() != 0x0)

{}

    

RCC->CR &=~(RCC_CR_PLLON);          //clear PLL

SysTick_Config(8000);

}

 

void SysClk8to48(void)         //从STOP模式中恢复

{

SetSysClock_HSI(4);//HSI:12*4=48M

 

RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
//selecting PLL clock as sys clock

while (RCC_GetSYSCLKSource() != 0x08)

{}

SysTick_Config(48000);

}

void SetSysClock_HSI(u8 PLL)      //重新配置HSI和PLL

{  

unsigned char temp=0;   

  

RCC->CR|=RCC_CR_HSION;  

while(!(RCC->CR&RCC_CR_HSIRDY));

RCC->CFGR=RCC_CFGR_PPRE1_2; //APB1=DIV2;APB2=DIV1;AHB=DIV1;

  

RCC->CFGR&=~RCC_CFGR_PLLSRC;              //PLLSRC ON 

  

RCC->CR &=~(RCC_CR_PLLON);                    

  

RCC->CR &=~(0x1fCR|=(PLL - 1) ACR=FLASH_ACR_LATENCY_1|FLASH_ACR_PRFTBE;       
//FLASH 2 delay clk cycles

  

RCC->CR|=RCC_CR_PLLON; //PLLON

while(!(RCC->CR&RCC_CR_PLLRDY));//waiting for PLL locked

RCC->CFGR&=~RCC_CFGR_SW;

RCC->CFGR|=RCC_CFGR_SW_PLL;//PLL to be the sys clock

while(temp!=0x02)    //waiting PLL become the sys clock

{    

temp=RCC->CFGR>>2;

temp&=0x03;

} 

}

以上时钟配置只是针对MM32W0系列的n4版本,在q1版不需要以上时钟操作。MM32W051PFB(q1)蓝牙功耗参数:

在SleepStop设置成0x02,MCU将会进入STOP模式,在保持 SRAM 和寄存器内容不丢失的情况下,停机模式可以达到最低的电能消耗。在停机模式下,HSI 的振荡器和 HSE 晶体振荡器被关闭。可以通过任一配置成 EXTI 的信号或者看门狗不复位方式把微控制器从停机模式中唤醒,EXTI 信号可以是 16 个外部 I/O 口之一、 PVD 的输出的唤醒信号。。STOP模式下无法下载调试程序。为了方便调试,可以在程序开始时加入一个延时,这样每次复位都有一段时间可以下载程序。

来源:灵动MM32MCU

围观 4

本章我们将对软件架构进行简单的讲解。

阻塞式例程介绍

对于大部分的低功耗设备来说,CPU都是处在休眠模式中,只在接收到特定数据的时候被唤醒处理少量数据,这种应用场景比较适合阻塞方式运行,这种方式配置简单,CPU大部分时间都被蓝牙服务占用,在收到来自射频模块的IRQ信号时需要及时处理,因此,用户的代码不允许出现阻塞。

我们先看一下例程中的main函数:

int main(void)

{

unsigned long temp=0x800000;

SystemClk_HSEInit();               //系统时钟配置为48MHz

            

#ifdef USE_UART                    //目前只支持UART接收与发送AT指令

#ifdef USE_AT_CMD    //开启AT指令功能

SleepStop = 0x01;       //空闲时低功耗,0x00不休眠,0x01睡眠,0x02停机模式

#endif

#endif

            

#ifdef USE_UART                    //开启UART功能,修改全局宏定义改变UART和对应引脚

uart_initwBaudRate();               //波特率默认9600,可以修改uart.c中的BaudRate变量

#endif

            

#ifdef USE_I2C                        //开启I2C功能

IIC_Init(I2C1);               // I2C1,标准模式,SCL PB6 ,SDA PB7 ,SendDataFlag PA10

#endif

 

#ifdef USE_USB                      //开启USB功能

usb_test();                    //使用PA11、PA12,枚举为USB HID设备

#endif

 

//SysTick_Count每1ms加一,系统时钟改变时应调用SysTick_Config()函数

SysTick_Configuration();           

            

//启用SPI2,在芯片内部与射频模块通信,速度应不低于6Mhz

SPIM_Init(SPI2,/*0x06*/0x06); 

            

IRQ_RF();         //配置PB8的IRQ 功能处理射频模块信号,用于低功耗唤醒

            

while(temp--);                //延时,方便烧录程序

radio_initBle(0x48, &ble_mac_addr);   //初始化射频模块(3dBm),并获取MAC地址

printf("\r\nMAC:%02x-%02x-%02x-%02x-%02x-%02x\r\n", ble_mac_addr[5],
ble_mac_addr[4],ble_mac_addr[3],
ble_mac_addr[2],ble_mac_addr[1],
ble_mac_addr[0]);

            

ble_run(160*2); //广播间隔320*0.625=200 ms,

}

对于蓝牙,必要的系统资源有:用于计时的Systick和与射频模块通信的SPI2,Systick使用SysTick_Count变量计时,也可以使用这个变量主动避开与IRQ处理任务的冲突;

目前程序支持RUN MODE,SLEEP MODE,STOP MODE模式,其中,在stop模式下MCU可以通过任意一个外部中断线唤醒,比如MM32W073NTB封装,在芯片设计上将IRQ与PB8共用一个GPIO口,在MM32W073PFB封装上,IRQ是独立的GPIO,需要用户在设计时将IRQ连接到任意一个GPIO口修改外部中断唤醒源即可实现stop唤醒模式;

例程中通过修改全局宏定义,可以启用UART、IIC和USB,目前都支持AT指令方式;

蓝牙的广播间隔由ble_run()中的参数决定,单位为0.625ms;

与中断式例程不同的地方:

① 蓝牙服务会定时调用接口函数McuGotoSleepAndWakeup();

② ble_run()是一个阻塞函数,后续程序将不会执行,中断模式中ble_run参数为0,且不是阻塞函数;

③ 中断式例程中蓝牙广播间隔由ble_run_interrupt_start()函数的参数决定,阻塞式例程中蓝牙广播间隔由ble_run()的参数决定;


图2 阻塞式程序流程

阻塞式程序流程如上图。实际使用中,蓝牙服务将定时调用UsrProcCallback()函数,在连接后允许时调用gatt_user_send_notify_data_callback()。两个函数的处理时间应尽可能短,不能影响IRQ信号的处理,否则可能出现蓝牙连接断开、无蓝牙广播等问题。

我们可以将用户程序放在callback.c的UsrProcCallback()函数中定时执行,如例程中的AT指令的处理CheckAtCmdInfo()函数,注意不要阻塞。可以将发送自身状态的程序放在gatt_user_send_notify_data_callback()函数中,同样不得阻塞。

来源:灵动MM32MCU

围观 4

本章我们将对软件架构进行简单的讲解。


图1 蓝牙通信框图

MM32W0控制模块通过SPI通信对射频模块进行控制,MM32W0的蓝牙程序提供以库的形式提供给大家使用,用户无需了解蓝牙协议栈,只需要对MCU进行控制即可实现蓝牙控制。在协议栈中为方便用户使用预留接口函数,用户通过调用相关接口的方式实现对应功能。

以下几点需要注意:

1)控制模块SPI2 仅且只能用于与射频模块的通信。

2)IRQ 信号引脚用于射频模块与控制模块的唤醒,且PB8 引脚只能用于控制模块唤醒。

3)AVDD 供电电压为2.2V ~ 3.6V

目前蓝牙控制程序有两种类型:中断式和阻塞式,中断方式是是以中断服务的方式运行,适合于实现用户某功能需要占用较长CPU 时间但可以被任意打断的应用场景;阻塞方式是蓝牙协议运行的入口函数为ble_run(),该函数不会返回,两种方式调用的接口函数都相同。

中断式例程介绍

中断服务程序方式运行的软件架构如下图所示。


图2 中断方式软件构架
main()函数:

int main(void)

{

unsigned long temp=0x800000;

unsigned long i=0; 

while(temp--);

SystemClk_HSEInit();

PWM_Init();



#ifdef USE_UART

#ifdef USE_AT_CMD

SleepStop = 0x02;

#endif

#endif



#ifdef USE_UART

uart_initwBaudRate();

#endif



#ifdef USE_I2C

IIC_Init(I2C1);

#endif    

SysTick_Configuration();



SPIM_Init(SPI2,/*0x06*/0x06); //6Mhz

 

IRQ_RF();

 

SetBleIntRunningMode(); 

radio_initBle(TXPWR_0DBM, &ble_mac_addr); 

SysTick_Count = 0;

while(SysTick_Count CR|=RCC_CR_HSION;

RCC->CR |= RCC_CR_PLLON;

RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;

SysTick_Config(48000);

}

SleepStatus = 0;         //设置当前状态为唤醒

ble_run(0); 

}

中断式例程需要用到两个中断服务程序,一个是蓝牙IRQ 中断PB8对应的外部中断线,一个是实现SysTick 对应的中断。IRQ 对应的中断服务程序用以运行蓝牙协议,需要有较高的中断优先级(针对所有系统中断来说)。

UART,SPI,IRQ,USB等控制模块上的配置同阻塞方式。

SPIM_Init(SPI2,0x06)是控制模块和射频模块间通信的初始化,SPI2只能用于与射频模块的通信。

IRQ_RF将PB8设置为外部中断,用于实现IRQ外部唤醒功能,通过一个下降沿唤醒MCU。PB8 引脚只能用于控制模块唤醒。

uart_initwBaudRate()是UART的初始化,对于两种封装对应的UART和GPIO接口不同。

不同点:

① 初始化蓝牙配置函数radio_initBle()之前,需要先调用SetBleIntRunningMode()函数。

②启动蓝牙调用ble_run_interrupt_start()而不是ble_run(),后面需要一个while(1)循环,可以将用户程序放在这里。

③ 进入休眠模式的函数需要主动调用IrqMcuGotoSleepAndWakeup()函数,函数McuGotoSleepAndWakeup()不再被调用。根据启动蓝牙时的参数,射频模块将定时触发IRQ的外部中断唤醒MCU。

来源: 灵动MM32MCU

围观 5

在文章中使用到了MM32 BLE_TestBoard和蓝牙模组,该蓝牙开发板是为了用户快速上手、了解、学习及评估MM32无线系列MCU性能的一块入门级开发板,本章节将会针对开发板及两款模组差异做详细介绍。

MM32W0系列主要有QFN32和LQFP48两种封装,支持32k\64k\128k flash容量,支持UART、I2C、SPI、USB device、CAN等通信接口。目前都是提供模组和开发板的方式供大家评估、测试。

MM32W0系列蓝牙模块是灵动微电子专为智能低功耗蓝牙数据传输而打造,遵循BLEV4.1蓝牙规范。支持蓝牙SPP 协议,可与所有版本安卓手机收发数据,可与支持BLE 的IOS 设备配对连接,无需额外授权费用,支持后台程序常驻运行;支持AT 指令,用户可根据需要更改串口波特率、设备名称、配对密码等参数,使用灵活。

本模块支持UART 接口、I2C 接口和USB 接口,具有成本低、体积小、功耗低、收发灵敏性高等优点,只需配备少许的外围元件就能实现其强大无线数据传输功能。

模组资源介绍


图1 QFN32封装模组资源


图2 LQFP48封装模组资源


图3 模组尺寸封装

开发板介绍

对于两种模块,大小规格是一样的,所以官方的开发板兼容这两种模块,只需要配置好对应的引脚即可正常使用。


图4 BLE开发板

如上图,这是一个官方的开发板,模块上的芯片是一颗LQFP48封装的MM32W0芯片。开发板的左边画框的部分需要连接跳帽,从上到下分别是UART,USB,和供电。

① UART部分连接有LED指示灯。

配置时需要注意的是,

LQFP48:UART2、PA2、PA3;

QFN32:UART1、PA9、PA10。

②USB部分使用的是PA11和PA12,可以向上连接使用USB功能,也可以向下连接使用UART的硬件流控功能。

③供电部分工作电压为2.3V ~ 3.6V

④在开发板下方是标准的SWD烧录接口

⑤在模块的周围,可以看到用丝印将对应的引脚名称或常用功能标注出来了。其中右上角标注了两种模块的不同,从上到下依次是:

LQFP48:PB9(Pin34)、PB0、PB1、PA0、PA1、IRQ

QFN32:PA8(Pin34)、PB0、PB1、PB2、PA0、IRQ

⑥在开发板最右边(以LQFP48为例)

LED2:PB9,低电平点亮

LED2:PB1,低电平点亮

变阻器:PB0,10K

Key2:Reset,按下为低电平

Key1:PA0,按下为高电平

来源:灵动MM32MCU

围观 3

在物联网的大趋势下,智慧城市和智能家居也随之兴起。而物联网的发展离不开无线技术,众所周知蓝牙是目前物联网产业中使用最广泛的无线通讯技术,特别是像蓝牙这种低功耗技术,更是众所瞩目的焦点。

BLE的优点主要包括:高可靠性、高安全性、低成本、低功耗。

灵动微电子有基于ARM® CortexTM-M3 和ARM® CortexTM-M0为内核的无线连接功能的MM32W系列(集成MCU+RF射频芯片的单芯片),MM32W0xxB 产品提供LQFP48 和QFN32 封装形式;根据不同的封装形式,器件中的外设配置不尽相同。这些丰富的外设配置,使得MM32W0xxB 产品微控制器适合于多种应用场合。

下面是MM32W0系列的选型表,可以根据所需的外设来选择合适的型号。


图1 MM32W0系列选型表


图2 MM32W3系列选型表

为了方便客户的使用,灵动微电子设计了几种蓝牙模块及配套的蓝牙开发板,只需配备少许的外围元件就能实现其强大无线数据传输功能,,搭载了MM32W0xxB是超低功耗的单模蓝牙芯片,工作电压为2.3V ~ 3.6V,多种省电工作模式保证低功耗应用的要求。支持多种数据通信接口(UART\USB\IIC\SPI等),支持AT 指令,用户可根据需要更改串口波特率、设备名称、配对密码等参数,使用灵活。该模块主要用于短距离的数据无线传输领域,可以实现与手机间一对一互连。


图3 两种常用的模块


图4 模组物理特性

模组主要应用领域:

1)蓝牙无线数据采集、传输;
2)工业遥控、遥测;
3)无线键盘、鼠标;
4)汽车检测设备、交通、井下定位、报警;
5)智能家居、楼宇自动化、安防、机房设备无线监控、门禁系统;
6)蓝牙操纵杆、蓝牙游戏手柄;
7)蓝牙遥控玩具;
8)防丢器、LED 灯控、iBeacon

使用AT指令控制模块

下面我们使用蓝牙开发板来演示AT指令控制蓝牙模块,通过Micro-USB线连接电脑,打开串口助手,手机端使用蓝牙APP软件与模块连接。


图5 蓝牙开发板


图6 部分AT指令列表

如图4在程序运行之后,首先在串口输出的是模块的MAC地址,手机连接之后输出已连接信息,在第三行,收到手机端发来的Hello字符串。接着,我们通过串口向模块发送AT指令,AT+BLESEND=10,0x4D696E644D6F74696F6E,在手机端收到发来的字符串”Mindmotion”如图5,最后断开连接,串口输出状态。


图7 模块串口输出


图8 手机端收发数据

对于简单的应用,连接上VDD、GND、TX和RX这四根线之后就可以为项目增加蓝牙通信功能。


图9 QFN32典型应用电路

用户如果有需要重新设计PCB,可以参照上面的原理图,自行设计合适的通信模块,更好地应用在不同的项目中。

来源: 灵动MM32MCU

围观 3

本章节将与大家一起使用CRC模块进行数据校验。

在数据传输过程中,无论传输系统的设计再怎么完美,差错总会存在,这种差错可能会导致在链路上传输的一个或者多个帧被破坏(出现比特差错,0变为1,或者1变为0),从而接受方接收到错误的数据。为尽量提高接受方收到数据的正确率,在接受方接收数据之前需要对数据进行差错检测,当且仅当检测的结果为正确时接收方才真正收下数据。检测的方式有多种,常见的有奇偶校验、因特网校验和循环冗余校验等。

其中循环冗余校验(CRC)原理实际上就是在一个p位二进制数据序列之后附加一个r位二进制检验码(序列),从而构成一个总长为n=p+r位的二进制序列;附加在数据序列之后的这个检验码与数据序列的内容之间存在着某种特定的关系。如果因干扰等原因使数据序列中的某一位或某些位发生错误,这种特定关系就会被破坏。因此,通过检查这一关系,就可以实现对数据正确性的检验。只要经过严格的挑选,并使用位数足够多的除数 P,那么出现检测不到的差错的概率就很小很小。CRC是一种常用的检错码,无法检测出错误在哪里,因此并不能用于自动纠错,一般的做法是丢弃接收的数据。

从上面的程序框图,我们可以发现,多项式的幂次越高,校验效果越好,但所花费的时间也越长。因此常用查表法或专用的硬件CRC模块来提高效率。

其中查表法,是事先根据特定的校验多项式,算出1字节数据范围所对应的256个余数,将其作为表格,编程写到程序存储器中查询而避免在线运算,已是非常通用的做法。但如果是CRC16校验,存储表格要占512字节(CRC32则需要1 KB),对于有限的单片机ROM资源来说所占比例不小,往往只因为多装了此表,就不得不升级单片机的型号。

在SPIN2x系列MCU中,加入了一个CRC计算单元,使用1个32位数据寄存器作为输入和输出,在执行写操作时输入CRC计算的新数据,在执行读操作时返回上一次CRC计算的结果。每一次写入数据寄存器,都会对整个32位字进行CRC计算,而不是逐字节地计算,从而节省大量的时间。

• 使用CRC-32(以太网)多项式: 0x4C11DB7
• 每次CRC计算需要4个AHB时钟周期
• 在 CRC 计算期间会暂停写操作,因此可以对寄存器 CRC_DR 进行背靠背写入或者连续地写-读操作。
• 可以通过设置寄存器CRC_CTRL的RESET位来重置寄存器 CRC_DR 为 0xFFFFFFFF,该操作不影响8位独立数据寄存器CRC_IDR内的数据。


图2 CRC计算单元框图

下面我们来看一下在程序中软件CRC与硬件CRC的配置。

CRC模块的配置步骤如下:

• CRC模块时钟使能
• CRC_CR的第一位RESET位复位(可选)
• 将数据写入CRC_DR寄存器
• 从CRC_DR寄存器中读出计算结果

程序中配置如下:

uint32_t Hardware_CRC(u32*addr, int num) 

{

CRC->CR|=1; //复位

for (; num > 0; num--)             

CRC->DR = (*addr++);

return CRC->DR;

}

我们可以使用软件算法来检验计算结果,对比两种方式花费的时间。

软件算法如下:

u32 Software_CRC (u32 *ptr,u32 len) 

{ 

u32 xbit; 

u32 data; 

u32 CRC32 = 0xFFFFFFFF; 

u32 bits; 

const u32 dwPolynomial =0x04C11DB7 ; 

u32 i; 

             

for(i = 0;i >= 1; 

} 

} 

return CRC32; 

}

下面我们用两个简单的数组,一个数组从0x00递增到0x7F,另一个数组从0x7F递减到0x00,分别使用硬件CRC和软件CRC计算,同时使用TIM1进行计时,最后通过UART输出得到的校验码和花费的时间。

计算和输出程序:

void CRCTest()

{

unsigned int i;      

u32 CRCtime,CRCresault;

u32 crc1[128];

for(i=0;iCR|=1; //复位

printf("\r\nCRC_DR=%x\t\r\n",CRC->DR);//输出复位值

      

TIM1->CNT &= 0;

CRCresault=Hardware_CRC(crc1,128);

CRCtime =TIM1->CNT;

printf("Hardware_CRC1:resault=%08x\ttime=%d\r\n",CRCresault,CRCtime);

             

TIM1->CNT &= 0;

CRCresault=Software_CRC(crc1,128);

CRCtime =TIM1->CNT;

printf("Software_CRC1:resault=%08x\ttime=%d\r\n",CRCresault,CRCtime);

             

for(i=0;iCNT &= 0;

CRCresault=Hardware_CRC(crc1,128);

CRCtime =TIM1->CNT;

printf("Hardware_CRC2:resault=%08x\ttime=%d\r\n",CRCresault,CRCtime);

             

TIM1->CNT &= 0;

CRCresault=Software_CRC(crc1,128);

CRCtime =TIM1->CNT;

printf("Software_CRC2:resault=%08x\ttime=%d\r\n",CRCresault,CRCtime);

}

定时器配置程序:

void Tim1_UPCount_test(u16Prescaler,u16 Period)

{

TIM_TimeBaseInitTypeDefTIM_StructInit;

 

/*使能TIM1时钟,默认时钟源为PCLK2(PCLK2未分频时不倍频,否则由PCLK2倍频输出),可选其它时钟源*/

RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE);

 

TIM_StructInit.TIM_Period=Period;                                         //ARR寄存器值

TIM_StructInit.TIM_Prescaler=Prescaler;                                 //预分频值

/*数字滤波器采样频率,不影响定时器时钟*/

TIM_StructInit.TIM_ClockDivision=TIM_CKD_DIV1;                 //采样分频值

TIM_StructInit.TIM_CounterMode=TIM_CounterMode_Up;       //计数模式

TIM_StructInit.TIM_RepetitionCounter=0;

TIM_TimeBaseInit(TIM1,&TIM_StructInit);

 

TIM_Cmd(TIM1, ENABLE);  

/*更新定时器时会产生更新时间,清除标志位*/

TIM_ClearFlag(TIM1,TIM_FLAG_Update);

}

Main函数:

int main(void)

{ 

Tim1_UPCount_test(48-1,0xffff); //APB2时钟为48M,48分频后TIM1时钟为1MHz

delay_init();

uart_initwBaudRate(9600);    //初始化UART

CRCTest();

while(1) { }

}


图3 软件CRC与硬件CRC计算结果

从结果中,我们可以看到,对于这两个数组,硬件CRC与软件CRC所得到的结果一致,但是硬件CRC每个数组只花费了52us,远小于软件CRC的2.5ms,由此可见,在进行大量数据处理的时候,使用硬件CRC模块可以节省大量的时间,同时保证了计算结果的正确。

来源:灵动MM32MCU

围观 6

本章节将通过使用采集红外测距模块测量阻挡物的距离与大家一起学习配置ADC和DMA模块。

ADC介绍与配置

ADC,Analog-to-Digital Converter的缩写,指模数转换器,是一种将连续变化的模拟信号转换为离散的数字信号的器件。真实世界的模拟信号,例如温度、压力、声音或者图像等,需要转换成更容易储存、处理和发射的数字形式。ADC可以实现这个功能,不同的应用对ADC的精度有着不同的要求。

在MM32SPIN27系列芯片中,内嵌 2 个 12 位的模拟/数字转换器 (ADC),每个 ADC 可用多达 16 个外部通道,可以满足大部分产品需求。


图1 ADC框图

MM32SPIN27中的12 位 ADC 是逐次逼近式的模拟-数字转换器 (SAR A/D 转换器),拥有高达 1Msps 转换速率,可以软件配置通道采样时间和分辨率。每个通道拥有独立的数据寄存器和一个共用的数据寄存器。

MM32SPIN27的ADC支持多种工作模式,包括单次转换模式、单周期扫描模式和连续扫描模式。扫描模式中可以选择通道扫描的顺序,选择采样顺序从高到低或从低到高。

A/D转换的启动方式有软件设定、外部引脚触发以及各个定时器启动。在触发信号产生后,ADC最多可以延时512个PLCK2时钟周期再开始采样。

在比较模式下提供了上限和下限两个比较寄存器。可通过软件设定 CMPCH 位选择监控通道。

ADC 的输入时钟由 PCLK2 经分频产生,通过正确配置ADC_ADCFG寄存器的ADCPRE位和ADCPRE位,可以自由地配置2分频到17分频。注意,输入时钟不得超过15MHz。

下面是一个简单的ADC配置函数:

void ADCInit(void)

{

ADC_InitTypeDef ADC_InitStructure;      

ADC_StructInit(&ADC_InitStructure);

GPIO_InitTypeDef GPIO_InitStructure;

 

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA,ENABLE);                         

RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);         

 

GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_2;                                

GPIO_InitStructure.GPIO_Speed =GPIO_Speed_50MHz;                          

GPIO_InitStructure.GPIO_Mode =GPIO_Mode_AIN;                              

GPIO_Init(GPIOA, &GPIO_InitStructure);

                                 

ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;   //12位分辨率

ADC_InitStructure.ADC_PRESCARE =ADC_PCLK2_PRESCARE_8;//96M时钟8分频

ADC_InitStructure.ADC_Mode =ADC_Mode_Continuous_Scan;//连续扫描模式

ADC_InitStructure.ADC_DataAlign =ADC_DataAlign_Right;   //数据对齐方式,右对齐     

ADC_InitStructure.ADC_ExternalTrigConv =ADC_ExternalTrigConv_T1_CC1;

//选择触发源,需要ADC_ExternalTrigConvCmd函数开启外部触发模式        

ADC_Init(ADC1, &ADC_InitStructure);

   

ADC_RegularChannelConfig(ADC1,ADC_Channel_DisableAll, 0, ADC_SampleTime_13_5Cycles);

ADC_RegularChannelConfig(ADC1, ADC_Channel_2,0, ADC_SampleTime_239_5Cycles);

//仅开启ADC1的通道2,采样时间为239.5个周期

ADC_DMACmd(ADC1,ENABLE);   //启用DMA传输功能                                                  

ADC_Cmd(ADC1, ENABLE);                                                        

}

这样就把PA2配置成了ADC1通道2的输入,每次转换完成后,数据将会保存在独立寄存器ADC1->ADDR2和共用寄存器ADC1->ADDATA中,同时ADC将会发出DMA传输请求。

下面我们来配置DMA传输。

DMA介绍与配置

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

MM32SPIN27的DMA 控制器有 5个通道,每个通道都直接连接专用的硬件 DMA 请求(具体可以在用户手册中有描述),每个通道都同样支持软件触发。每个通道都有 3个事件标志:DMA 半传输, DMA 传输完成和 DMA 传输出错。

在5个通道的请求之间的优先权可以通过软件编程设置 (共有四级:很高、高、中等和低),假如在相等优先权时由硬件决定 (请求 0 优先于请求 1,依此类推)。


图2 DMA功能框图

下面我们来配置DMA将ADC采集的数据传输到数组中:

void DMAInit(void)

{

DMA_InitTypeDef DMA_InitStructure;

NVIC_InitTypeDef NVIC_InitStruct;

   

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);

DMA_DeInit(DMA1_Channel1);

      

DMA_InitStructure.DMA_PeripheralBaseAddr =(u32)&(ADC1->ADDATA);//外设地址

DMA_InitStructure.DMA_MemoryBaseAddr =(u32)&ADCValue;     //内存地址

DMA_InitStructure.DMA_DIR =DMA_DIR_PeripheralSRC;             //方向

DMA_InitStructure.DMA_BufferSize = 20;                                     //传输数量

DMA_InitStructure.DMA_PeripheralInc =DMA_PeripheralInc_Disable;//外设地址固定

DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  //内存地址自增

DMA_InitStructure.DMA_PeripheralDataSize =DMA_PeripheralDataSize_HalfWord;

DMA_InitStructure.DMA_MemoryDataSize =DMA_MemoryDataSize_HalfWord;

DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;                 //循环传输

DMA_InitStructure.DMA_Priority =DMA_Priority_High;                 //传输优先级

DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;                   //外设与内存交互

DMA_Init(DMA1_Channel1,&DMA_InitStructure);

DMA_Cmd(DMA1_Channel1, ENABLE);

   

//NVIC中断设定

DMA_ITConfig(DMA1_Channel1,DMA_IT_TC,ENABLE);                              

NVIC_InitStruct.NVIC_IRQChannel =DMA1_Channel1_IRQn;

NVIC_InitStruct.NVIC_IRQChannelPriority = 0;

NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;

NVIC_Init(&NVIC_InitStruct);

}

void DMA1_Channel1_IRQHandler(void)

{

ADC_SoftwareStartConvCmd(ADC1, DISABLE);

//DMA传输20个数据完成,标志置1

DMA_ClearITPendingBit(DMA1_IT_TC1);                                        

ADCflag = 1;                                                                

}

配置完成后,DMA控制器收到ADC产生的请求后,将会传输一次数据到ADCValue数组中,并执行一次 DMA_CNDTRx 寄存器的递减操作和内存地址自动增量操作,直到传输完成20个数据,关闭ADC转换,并将标志ADCflag置1。

接下来就需要根据不同的应用来处理数据了。

简单应用:2Y0A21YK0F红外测距模块

2Y0A21YK0F是一个有效距离为10cm~80cm的红外测距模块,由PSD(位置敏感探测器)、IRED(红外发射二极管)和信号处理电路组成。采用三角测距法,目标反射率、环境温度和工作时间的变化对距离检测影响较小,在V0输出与检测距离相对应的电压。


图3 2Y0A21YK0F红外测距模块


图4 输出电压与距离倒数的关系

根据图4中给出的关系,将ADC中的数据进行滤波和计算,就可以得到所需的距离信息了。

程序配置:

void ADCFilter(void)                                                           

{

static u16 cntFilter;

static u32 lADCFilterValue = 0;

for(cntFilter = 0; cntFilter 

程序运行结果:


图5 程序运行结果

连接实验板与红外测距模块,编译下载运行程序,按照模块说明,使用一本白色封面的书本,接近再远离模块,通过不断变化距离,我们可以从UART串口看到程序将计算得到的结果(图5)。在这个过程中,ADC将模拟信号转换成数字信号,通过DMA传输到内存中,经过MCU的处理,最终得到了一个相对准确的距离数据。

来源: 灵动MM32MCU

围观 4

本章节将与大家一起配置I2C接口来控制一个OLED模块。

I2C介绍

IIC 即Inter-Integrated Circuit(集成电路总线),是由飞利浦半导体公司在八十年代初设计出来的,主要是用来连接整体电路(ICS) ,IIC是一种多向控制总线,也就是说多个芯片可以连接到同一总线结构下,同时每个芯片都可以作为实时数据传输的控制源。这种方式简化了信号传输总线接口。


图1 I2C主设备与从设备

I2C总线采用一条数据线(SDA),加一条时钟线(SCL)来完成数据的传输及外围器件的扩展。每个器件都有一个唯一的地址识别,而且都可以作为一个发送或接收器。除了发送器和接收器外,器件在执行数据传输时也可以被看做是主机或者从机。主机是初始化总线的数据传输并产生允许传输的时钟信号的器件。此时,任何被寻址的器件都被认为是从机。

在SPIN27上的I2C拥有如下特性:

• 半双工同步操作
• 支持主从模式
• 支持 7 位地址和 10位地址
• 支持标准模式 100Kbps,快速模式 400Kbps
• 产生 Start、 Stop、重新发 Start、应答 Acknowledge信号检测
• 在主模式下只支持一个主机
• 分别有 2字节的发送和接收缓冲
• 在 SCL和 SDA上增加了无毛刺电路
• 支持 DMA 操作
• 支持中断和查询操作

I2C协议

在I2C中,当总线处于空闲状态时,SCL和SDA同时被外部上拉电阻拉为高电平。在SCL线是高电平时,SDA线从高电平向低电平切换表示起始条件。当主机结束传输时要发送停止条件。在SCL线是高电平,SDA线由低电平向高电平切换表示停止条件。下图显示了起始和停止条件的时序图。


图2 起始和停止条件

I2C总线的数据都是以字节(8位)的方式传送的,每个数据字节在传送时都是高位(MSB)在前。数据传输过程中,当SCL为1时,SDA必须保持稳定。发送器件每发送一个字节之后,在时钟的第9个脉冲期间释放数据总线,由接收器发送一个ACK(把数据总线的电平拉低)来表示数据成功接收;接收器不拉低数据总线表示一个NACK,NACK有两种用途:在主机发送从机接收时表示未成功接收数据字节,在从机发送主机接收时表示传送数据结束。

下面是主机对从机的读写过程:

写通讯过程:

1、主机在检测到总线空闲的状况下,首先发送一个START信号掌管总线;
2、发送一个地址字节(包括7位地址码和一位R/W);
3、当从机检测到主机发送的地址与自己的地址相同时发送一个应答信号(ACK);
4、主机收到ACK后开始发送第一个数据字节;
5、从机收到数据字节后发送一个ACK表示继续传送数据,发送NACK表示传送数据结束
6、主机发送完全部数据后,发送一个停止位STOP,结束整个通讯并且释放总线;

读通讯过程:

1、主机在检测到总线空闲的状况下,首先发送一个START信号掌管总线;
2、发送一个地址字节(包括7位地址码和一位R/W);
3、当从机检测到主控发送的地址与自己的地址相同时发送一个应答信号(ACK);
4、主机收到ACK后释放数据总线,开始接收第一个数据字节;
5、主机收到数据后发送ACK表示继续传送数据,发送NACK表示传送数据结束;
6、主机发送完全部数据后,发送一个停止位STOP,结束整个通讯并且释放总线;

I2C有两种地址格式: 7位的地址格式和 10位的地址格式。

7位的地址格式:

在起始条件(S)后发送的一个字节的前7位(bit 7:1)为从机地址,最低位(bit 0)是数据方向位,当bit 0为0,表示主机写数据到从机,1表示主机从从机读数据。


图3 7位的地址格式

10 位的地址格式:

在 10 位的地址格式中,发送2个字节来传输 10 位地址。发送的第一个字节的位的描述如下:第一个5位(bit 7:3)用于告示从机接下来是10位的传输。第一个字节的后两个字节(bit 2:1)位从机地址的bit 9:8,最低位(bit 0)是数据方向位(R/W)。传输的第二个字节为10位地址的低八位。


图4 10位的地址格式

下面我们一起来配置MM32SPIN27的I2C模块进行OLED屏的显示功能:

I2C配置:

void I2CInitMasterMode()

{

I2C_InitTypeDef I2C_InitStructure;

GPIO_InitTypeDef  GPIO_InitStructure;

NVIC_InitTypeDef NVIC_InitStructure;

 

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB,ENABLE); //开启GPIO时钟

RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE); //开启I2C1时钟

 

GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_8 | GPIO_Pin_9;  //I2C1重映射IO口

GPIO_InitStructure.GPIO_Speed =GPIO_Speed_2MHz;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//保持总线空闲即CLK&DATA为高

GPIO_Init(GPIOB, &GPIO_InitStructure);   

 

I2C_InitStructure.I2C_Mode = I2C_Mode_MASTER; //主模式

I2C_InitStructure.I2C_OwnAddress =FLASH_DEVICE_ADDR; //从机地址

I2C_InitStructure.I2C_Speed =I2C_Speed_STANDARD;  //标准速率

I2C_InitStructure.I2C_ClockSpeed = 100000; //100K

I2C_Init(I2C1, &I2C_InitStructure);

 

NVIC_InitStructure.NVIC_IRQChannel = I2C1_IRQn;//I2C中断设置

NVIC_InitStructure.NVIC_IRQChannelPriority = 1;

NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

NVIC_Init(&NVIC_InitStructure);

 

I2C_ClearITPendingBit(I2C1, I2C_IT_RX_FULL |I2C_IT_TX_EMPTY);//请中断标志位

I2C_ITConfig(I2C1,I2C_IT_RX_FULL|I2C_IT_TX_EMPTY, ENABLE);//开启I2C中断

 

I2C_Cmd(I2C1, ENABLE);    //使能I2C

 

GPIO_InitStructure.GPIO_Speed =GPIO_Speed_2MHz;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;  //需要外加上拉

GPIO_Init(GPIOB, &GPIO_InitStructure);

 

GPIO_PinAFConfig(GPIOB,GPIO_PinSource8,GPIO_AF_1);//设置GPIO的复用功能

GPIO_PinAFConfig(GPIOB,GPIO_PinSource9,GPIO_AF_1);

}

目标地址设置函数:

void I2CSetDeviceAddr(unsigned chardeviceaddr) 

{

GPIO_InitTypeDef  GPIO_InitStructure;

   

GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_8 | GPIO_Pin_9;  //I2C1重映射IO口

GPIO_InitStructure.GPIO_Speed =GPIO_Speed_2MHz;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//保持总线空闲即CLK&DATA为高

GPIO_Init(GPIOB, &GPIO_InitStructure);   

   

I2C_Cmd(I2C1,DISABLE);

I2C_Send7bitAddress(I2C1, deviceaddr ,I2C_Direction_Transmitter);//设置从机地址

I2C_Cmd(I2C1, ENABLE);

   

GPIO_InitStructure.GPIO_Speed =GPIO_Speed_2MHz;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; 

GPIO_Init(GPIOB, &GPIO_InitStructure);

}

发送与接收检查:

void I2CTXEmptyCheck(I2C_TypeDef *I2Cx)

{

while(1)

if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_TX_EMPTY))

break;

}

void I2CRXFullCheck(I2C_TypeDef *I2Cx)

{

while(1)

if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_RX_FULL))

break;

}

库函数中还有一些常用的操作函数,如发送函数I2C_SendData()和停止函数I2C_GenerateSTOP()等,下面我们连接上一个I2C从机,使用上面几个函数发送几个数据,从逻辑分析仪,可以得到下图所示波形


图5 数据发送

可以看到,图中,从机的地址为0x78(包含读/写位),发送的几个数据也得到了ACK回应。

I2C的简单配置就完成了。在实际应用中我们需要根据不同器件,对数据进行处理,以实现所需的功能。

下面,我们以市面上常见的基于SSD1303的OLED显示模块为例,介绍一个SPIN27的I2C接口应用。

OLED应用

SSD1303是一个带有单芯片的单色OLED显示模块,支持最大显示分辨率132x64。

模块内嵌了对比度控制、显存和振荡器,减少了外部组件的数量和功耗。它适用于许多小型便携式应用,例如手机子显示屏、计算器、MP3播放器等显示类应用。

I2C接口应用代码:

void WriteCmdStart()

{

I2C_SendData(I2C1,0x80);//寄存器地址

I2CTXEmptyCheck(I2C1);

}

 

void WriteDataStart()

{

I2C_SendData(I2C1,0x40);//寄存器地址

I2CTXEmptyCheck(I2C1);

}

 

void WriteEnd()

{

delay_us(10);

I2C_GenerateSTOP( I2C1, ENABLE );

}

 

void WriteCmd(u8 command)

{

I2C_SendData(I2C1,command);

I2CTXEmptyCheck(I2C1);

}

 

void WriteData(u8 data)

{

I2C_SendData(I2C1,data);

I2CTXEmptyCheck(I2C1);

}

 

void OLED_Init(void)

{

delay_ms(100); //延时

 

WriteCmdStart();

WriteCmd(0xAE); //display off

WriteCmd(0x20); //Set Memory AddressingMode   

WriteCmd(0x10); //00,Horizontal AddressingMode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid

WriteCmd(0xb0); //Set Page Start Address forPage Addressing Mode,0-7

WriteCmd(0xc8); //Set COM Output Scan Direction

WriteCmd(0x00); //---set low column address

WriteCmd(0x10); //---set high column address

WriteCmd(0x40); //--set start line address

WriteCmd(0x81); //--set contrast controlregister

WriteCmd(0xff); //亮度调节 0x00~0xff

WriteCmd(0xa1); //--set segment re-map 0 to 127

WriteCmd(0xa6); //--set normal display

WriteCmd(0xa8); //--set multiplex ratio(1 to64)

WriteCmd(0x3F); //

WriteCmd(0xa4); //0xa4,Output follows RAMcontent;0xa5,Output ignores RAM content

WriteCmd(0xd3); //-set display offset

WriteCmd(0x00); //-not offset

WriteCmd(0xd5); //--set display clock divideratio/oscillator frequency

WriteCmd(0xf0); //--set divide ratio

WriteCmd(0xd9); //--set pre-charge period

WriteCmd(0x22); //

WriteCmd(0xda); //--set com pins hardwareconfiguration

WriteCmd(0x12);

WriteCmd(0xdb); //--set vcomh

WriteCmd(0x20); //0x20,0.77xVcc

WriteCmd(0x8d); //--set DC-DC enable

WriteCmd(0x14); //

WriteCmd(0xaf); //--turn on oled panel

WriteEnd();

}

 

void OLED_ON(void)

{

WriteCmdStart();

WriteCmd(0X8D); //设置电荷泵

WriteCmd(0X14); //开启电荷泵

WriteCmd(0XAF); //OLED唤醒

WriteEnd();

}

void OLED_Fill(unsigned char fill_Data)//全屏填充

{

unsigned char m,n;

for(m=0;m>4)|0x10);

WriteEnd();    

WriteCmdStart();

WriteCmd((x&0x0f)|0x01);

WriteEnd();    

}

//Main函数:

int main(void)

{

unsigned char str1[]="Hello!";

unsigned char str2[]="MM32";

unsigned char str3[]="SPIN27";

delay_init();//启动外部晶振

uart_initwBaudRate(115200);

printf("uart ok\r\n");

 

I2CInitMasterMode() ;

I2CSetDeviceAddr(OLED_DEVICE_ADDR);

OLED_Init();//关闭显示并初始化

OLED_ON();//OLED唤醒

OLED_Fill(0xFF);//点亮所有像素点

OLED_CLS();//清屏

OLED_ShowStr(5,6,str1,3);

OLED_ShowStr(21,5,str2,3);

OLED_ShowStr(37,6,str3,3);

while(1)             

{

}

}

由于篇幅限制,在本篇文章中字库部分没有提供对应的代码。连接好线路(VCC\GND\SCL\SDA4线),编译程序下载,我们就可以看到屏幕亮起来了,显示“Hello! MM32 SPIN27”。


图6 OLED显示效果

同样,我们也可以通过逻辑分析仪,分析芯片与模块之间的数据交互,进一步了解I2C通信协议,进一步熟悉I2C的使用。

来源:灵动MM32MCU

围观 7

本章节将与大家一起配置SPI使用GY-63气压传感器模块。

SPI介绍

SPI(Serial Peripheral Interface, 同步外设接口)是由摩托罗拉公司开发的全双工同步串行总线,该总线大量用在与EEPROM、ADC、FRAM和显示驱动器之类的慢速外设器件通信。

SPI 通信原理比 I2C要简单,它主要是主从方式通信,这种模式通常只有一个主机和一个或者多个从机,如图1,标准的 SPI 是 4 根线,分别是 SSEL(片选,也写作 SCS)、SCLK(时钟,也写作 SCK)、MOSI(主机输出从机输入Master Output/Slave Input)和 MISO(主机输入从机输出 Master Input/Slave Output)。


图1 SPI单主单从应用


图2 SPI时序图

SPI接口有四种不同的数据传输时序,取决于CPOL和CPHL这两位的组合。图2中表现了这四种时序,时序与CPOL、CPHA的关系也可以从图中看出。在通信时,主机和从机必须要配置成相同的时序模式。

CPOL是用来决定SCK时钟信号空闲时的电平,CPOL=0,空闲电平为低电平,CPOL=1时,空闲电平为高电平。CPHA是用来决定采样时刻的,CPHA=0,在每个周期的第一个时钟沿采样,CPHA=1,在每个周期的第二个时钟沿采样。

在SPIN27上的SPI拥有如下特性:
• 完全兼容 Motorola 的 SPI 规格

• 支持各 8 个对应配置数据位(Data size)的发送缓冲器和接收缓冲器
• 支持 DMA 请求
• 在 3 根线上支持全双工同步传输
• 16 位的可编程波特率生成器
• 支持主机模式和从机模式

• 支持一个主机多个从机操作
• 主机模式下SPI的时钟最快可高达36M,从机模式下SPI的时钟最快可高达18M
• 可编程的时钟极性和相位
• 可编程的数据顺序, MSB在前或者LSB在前
• 支持 1 ∼ 32 位的数据位长度同时发送和接收注:除了 8 位数据收发,其余 1 ∼ 32 位数据收发只支持 LSB 模式,不支持 MSB 模式。

下面我们简单的配置一下MM32SPIN27的SPI主机模式:

SPI主机模式配置:

void SPIM_Init(SPI_TypeDef* SPIx,unsigned short spi_baud_div)

{

SPI_InitTypeDef SPI_InitStructure;

GPIO_InitTypeDef  GPIO_InitStructure;

      

if(SPIx==SPI1)

{

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE);

RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);

 

GPIO_PinAFConfig(GPIOA, GPIO_PinSource4,GPIO_AF_0);  

GPIO_PinAFConfig(GPIOA, GPIO_PinSource5,GPIO_AF_0);

GPIO_PinAFConfig(GPIOA, GPIO_PinSource6,GPIO_AF_0); 

GPIO_PinAFConfig(GPIOA, GPIO_PinSource7,GPIO_AF_0);

               

GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_4;               //spi1_cs  pa4

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //推挽输出

GPIO_Init(GPIOA, &GPIO_InitStructure);

             

GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_5;               //spi1_sck  pa5

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  // 推免复用输出

GPIO_Init(GPIOA, &GPIO_InitStructure);

      

GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_7;               //spi1_mosi  pa7

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  // 推免复用输出

GPIO_Init(GPIOA, &GPIO_InitStructure);

             

GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_6;               //spi1_miso  pa6

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;              //上拉输入  

GPIO_Init(GPIOA, &GPIO_InitStructure);

}

if(SPIx==SPI2)

{

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOB, ENABLE);

RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);

 

GPIO_PinAFConfig(GPIOB, GPIO_PinSource12,GPIO_AF_0);  

GPIO_PinAFConfig(GPIOB, GPIO_PinSource13,GPIO_AF_0);

GPIO_PinAFConfig(GPIOB, GPIO_PinSource14,GPIO_AF_0);  

GPIO_PinAFConfig(GPIOB, GPIO_PinSource15,GPIO_AF_0);

             

GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_12;            

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  // 推免复用输出

GPIO_Init(GPIOB, &GPIO_InitStructure);

             

GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_13;             //spi2_sck  pb13

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  // 推免复用输出

GPIO_Init(GPIOB, &GPIO_InitStructure);

             

GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_15;             //spi2_mosi  pb15

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  // 推免复用输出

GPIO_Init(GPIOB, &GPIO_InitStructure);

                    

GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_14;              //spi2_miso  pb14

GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;              //上拉输入  

GPIO_Init(GPIOB, &GPIO_InitStructure);

}

SPI_InitStructure.SPI_Mode = SPI_Mode_Master;

SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;

SPI_InitStructure.SPI_DataWidth = SPI_DataWidth_8b;  

SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;              //空闲时钟电平

SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;          //采样时钟沿

SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;                  //软件控制片选线

SPI_InitStructure.SPI_BaudRatePrescaler = spi_baud_div;     //时钟分频

SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;         

SPI_Init(SPIx, &SPI_InitStructure);

      

SPI_Cmd(SPIx, ENABLE);         

SPI_BiDirectionalLineConfig(SPIx, SPI_Direction_Tx);            //启用发送功能

SPI_BiDirectionalLineConfig(SPIx, SPI_Direction_Rx);           //启用接收功能

}

下面是一些常用操作函数:

void SPIM_CSLow(SPI_TypeDef* SPIx)

{

SPI_CSInternalSelected(SPIx, SPI_CS_BIT0,ENABLE);

}

void SPIM_CSHigh(SPI_TypeDef* SPIx)

{

SPI_CSInternalSelected(SPIx, SPI_CS_BIT0,DISABLE);

}

unsigned int SPIMReadWriteByte(SPI_TypeDef* SPIx,unsigned char tx_data)

{

SPI_SendData(SPIx, tx_data);   

while(!SPI_GetFlagStatus(SPIx, SPI_FLAG_RXAVL));

return SPI_ReceiveData(SPIx);

}

接下来,调用上面的SPIM_Init()函数,就可以初始化SPI主机模式了

按照SPI标准,发送和接收数据的时候,需要先用SPIM_CSLow()或SPIM_CSHigh()设置片选信号,再用SPIMReadWriteByte()函数同时收发数据,完成操作后再设置片选信号为空闲,以此完成一次通信。


图3 数据收发

可以看到,图中,CS输出低电平,开始一次通信,主机发送的数据为0xA2,0x00,0x00,从机发送的数据为0xFE,0xB9,0xB9,CS输出高电平,此次通信结束。

下面,我们以市面上常见的基于MS5611的GY-63气压传感器为例,介绍一个SPIN27的SPI接口应用。

气压传感器应用

MS5611气压传感器是由MEAS(瑞士)推出的一款SPI和I²C总线接口的新一代高分辨率气压传感器,分辨率可达到10cm。MS5611提供了一个精确的24位数字压力值和温度值以及不同的操作模式,可以提高转换速度并优化电流消耗。5.0毫米×3.0毫米×1.0毫米的小尺寸可以集成在移动设备中。

可以从下面的网址获取详细数据手册:

https://www.alldatasheetcn.com/datasheet-pdf/pdf/880802/TEC/MS5611-01BA0...


图4 MS5611和GY-63模块

GY-63程序代码:

u16 PROMData[8]={0};  //实际使用6个u16

uint32_t D1_Pres,D2_Temp; // 存放数字压力和温度

float Pressure; //温度补偿大气压

float dT,Temperature,Temperature2;//实际和参考温度之间的差异,实际温度,中间值

double OFF,SENS;  //实际温度抵消,实际温度灵敏度

float Aux,OFF2,SENS2;  //温度校验值

 

void SPIM_Test(SPI_TypeDef* SPIx)

{

int i =0;

SPIM_Init(SPIx,0x4);//12MHz

      

Reset_GY_63(SPIx);     //复位,0x1E

ReadPROM(SPIx,PROMData);

GetTemperature(SPIx,Convert_D2_OSR_4096); //0x58

GetPressure(SPIx,Convert_D1_OSR_4096);             //0x48

      

printf("\nPROM Data: \r\n");

for (i =0;i 2000)

{

Temperature2 = 0;

OFF2 = 0;

SENS2 = 0;

}

Temperature = Temperature - Temperature2;

OFF = OFF - OFF2;

SENS = SENS - SENS2;    

 

Pressure=(D1_Pres*SENS/2097152.0-OFF)/32768.0;

}

int main(void)

{

Uart_ConfigInit(9600);

delay_init();

      

SPIM_Test(SPI1);        

while(1)

{

}

}

程序运行结果:

按照数据手册,连接MCU和模块对应的引脚,模块的PS引脚接地选择SPI模式,编译下载运行程序,在UART输出如下结果:


图5 程序运行结果

从上图,我们可以看到MCU成功读取到模块的PROM和两种模式下ADC的数据,并且通过初步计算得到了温度和气压。

同样,我们也可以通过逻辑分析仪,分析芯片与模块之间的数据交互,进一步了解SPI通信,进一步熟悉芯片的使用。

来源:灵动MM32MCU

围观 7

页面

订阅 RSS - MCU