STM32 ARM Cortex-M0+ 入门指南

介绍

文档目的

本指南旨在帮助您开始使用STM32及其外设。特别说明,本指南重点介绍用于Nucleo STM32L0评估板的STM32L053R8T6。本指南面向对微控制器及其操作有基本了解的用户。

警告:代码和电路图仅供参考

注意:最新代码将在GitHub上发布。完整代码请参见此处,所有代码片段均引用自GitHub版本。

器件描述

ST公司通过其STM32系列微控制器,提供了一种经济灵活的开发与原型设计方式。与竞争对手相比,ST的Nucleo系列评估板极具成本效益。您只需花费Arduino Uno约一半的价格,就能使用ST的一款具备DSP功能的32位微控制器!Nucleo板还通过采用Arduino外形尺寸及其独有的"Morpho连接器",提供了灵活的原型设计方式。这意味着您可以轻松使用为Arduino设计的扩展板进行原型开发,同时仍可通过Morpho连接器完全访问所有微控制器引脚。Nucleo板上还集成了ST-LINK/V2-1调试器和编程器。

使用的硬件

开发过程中使用的硬件如下所示。如需订购部件,只需点击名称即可跳转至DigiKey部件页面。

设备 部件编号 描述 图片
STMicroelectronics NUCLEO-L053R8 497-14710-ND STM32微控制器评估板
STMicroelectronics X-NUCLEO-IKS01A1 497-15062-ND ST传感器评估板
Adafruit Ultimate GPS扩展板 1528-1045-ND Arduino用SD卡及GPS扩展板
XBee Pro S1无线模块 XBP24-AWI-001-ND 无线收发器

文档资料:

以下列出我在微控制器开发过程中使用过的最实用数据手册。提供这些资料是为了快速查阅,希望能减少您寻找所需信息对应数据手册的时间。

STM32L053xx Nucleo 开发板文档

这是STM32L053R8开发相关的实用数据手册与文档清单。

文档标题 描述
NUCLEO L053R8 原理图 Nucleo STM32L0评估板电路图
STM32L053xx技术数据 基础信息、特性参数及功能描述
STM32L053xx参考手册 寄存器描述与功能说明
STM32L053xx入门指南 开发板原理图、引脚定位及描述
I2C时序配置 时序配置工具说明

STM32L053xx 运动 MEMS 与环境传感器扩展板文档

这是使用ST生产的部分传感器时的实用数据手册与文档清单。

文档标题 描述
X-Nucleo-ISK01A1用户手册 开发板用户手册
X-Nucleo-ISK01A1原理图 开发板原理图
温湿度传感器 温湿度传感器数据手册
压力传感器 压力传感器数据手册
磁力计 磁力计数据手册
加速度计与陀螺仪 加速度计与陀螺仪数据手册

Adafruit 终极 GPS 记录扩展板文档

以下是使用Adafruit终极GPS扩展板时实用的数据手册和文档列表。

文档标题 描述
Adafruit GPS扩展板原理图 终极GPS扩展板原理图
FGPMMOPA6B GPS模块数据手册 包含GPS基础数据及NMEA报文格式
格式与协议 详细说明NMEA语句格式
PMTK指令集 包含发送至GPS模块的PMTK指令

XBee 文档

文档标题 描述
XBee Pro使用手册 关于XBee您需要了解的一切!

设置:

开发板设置

警告:若NUCLEO及其扩展板的总电流消耗超过300mA,必须通过E5V或VIN接口连接外部电源供电。

应首先检查Nucleo开发板上的跳线帽是否满足运行需求。该信息也可在《STM32L053xx入门指南》第8页找到。关于JP1和JP5的电源选项设置,请参阅第16页。下表描述了跳线配置。

跳线名称 跳线位置
JP1 无跳线
JP5 连接PWR与U5V
JP6 添加跳线

接下来连接USB线缆,LD1和LD3将亮红灯,表示已准备好编程。若LD3未亮红灯,请参照上方警告并阅读第16页。

程序

与STM32L0 Nucleo开发板交互的程序选择众多,本指南将使用Keil提供的工具。进入Keil页面后选择Keil MDK-ARM。Keil提供的IDE名为µVision,是本指南编程环节的核心工具。本指南编写时使用的是Keil MDK-ARM 5.15版本。

驱动与库

还需提及ST公司开发的STM32CubeMX软件。该软件是STM32系列微控制器C代码初始化的图形界面工具。对配置时钟、GPIO、ADC、UART等外设极为便利。未采用该软件的原因是生成的驱动过于臃肿。其HAL(硬件抽象)驱动提供开发板编程所需的全套功能。若希望继续使用Keil µVision免费版,建议避免使用HAL库。

熟悉STM32微控制器的用户可能会疑惑:L0系列的标准外设库去哪了?很遗憾地告知您——根本没有。生成HAL库的STM32CubeMX是唯一选择。

配置 Keil MDK-ARM ST-Link

在Keil官网注册并完成下载安装后,还需完成几个步骤。

  1. microVision 环境中:更新 STM32L0 系列的软件包安装(通过菜单路径:项目 管理 包安装器( Project → Manage → Pack installer ))
    a. ARM::CMSIS版本:4.3.0及4.2.0
    b. KEIL::MDK中间件版本:6.4.0及6.2.0
    c. KEIL:STM32L0xx_DFP版本:1.3.0
    d. KEIL::STM32NUCLEO板级支持包版本:1.3.0
  2. ST-Link 驱动安装
    a. 若ST-Link未自动安装,可手动运行以下.bat文件
    b. 进入C:\keil_v5\ARM\STLink\USBDriver目录,运行名为stlink_winusb_install.bat的批处理文件
  3. 目标选项配置
    a. 前往项目→目标选项
    b. 选择设备型号[设备选项卡]

    c. 确保时钟设置为32.0 MHz [目标选项卡]

    d. 勾选"使用目标对话框的内存布局"[链接器选项卡]

    e. 使用ST-Link调试器,并按图示修改调试设置[调试选项卡]

  4. 至此所有准备工作已完成,现在可以开始编程。除了本页示例代码外,您还可在µVision的包安装器中下载更多示例程序。

复位与时钟控制 (RCC) 示例

系统核心时钟初始化流程详解

所有寄存器及设置的详细说明可参考STM32L053R8参考手册。本部分指南将介绍系统时钟与外设时钟的配置方法。为直观展示最终系统配置,我使用STM32CubeMx软件呈现相关设置。再次强调,这仅是可视化辅助工具,并未用于生成时钟配置代码。

警告:USART1CLK I2C1CLK 的配置可能不准确。仅 HSI RC 到蓝色外设时钟框的路径是确认正确的

如上图所示,我们将使用16MHz内部时钟并将其倍频至32MHz。为此,首先需将系统时钟设置为内部高速时钟(HSI16),接着配置闪存与电源设置,最后通过PLL利用HSIRC时钟实现32MHz倍频。

步骤 1 :启用 HSI16

只需设置RCC_CR 寄存器中的HSI16ON 位。随后通过监测RCC_CR 寄存器的HSI16RDYF位,等待内部高速时钟就绪。

注意:**设备头文件未采用数据手册的命名方式,但代码逻辑正确。例如,**HSI16ON 对应的寄存器位实际命名为 RCC_CR_HSION 而非预期的 RCC_CR_HSI16ON

/* Enable HSI */
  RCC->CR |= ((uint32_t)RCC_CR_HSION);
   
  /* Wait for HSI to be ready */
while ((RCC->CR & RCC_CR_HSIRDY) == 0){
      // Nop
  }

步骤 2 :设置 HSI 为系统时钟

启用HSI后,通过配置RCC_CFGR 寄存器的SW[1:0] 位将其设为系统时钟。等待系统时钟就绪。

  /* Set HSI as the System Clock */
RCC->CFGR = RCC_CFGR_SW_HSI;
   
  /* Wait for HSI to be used for teh system clock */
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_HSI){
      // Nop
  }

步骤 3 :配置 FLASH PWR

FLASH->ACR |= FLASH_ACR_PRFTEN;                          // Enable Prefetch Buffer
FLASH->ACR |= FLASH_ACR_LATENCY;                         // Flash 1 wait state
RCC->APB1ENR |= RCC_APB1ENR_PWREN;                       // Enable the PWR APB1 Clock
PWR->CR = PWR_CR_VOS_0;                                  // Select the Voltage Range 1 (1.8V)
while((PWR->CSR & PWR_CSR_VOSF) != 0);                   // Wait for Voltage Regulator Ready

步骤 4 :配置 PLL

现在需要配置PLL。首先选择RCC_CFGR 中的PLLSRC 为HSI,设置PLLMUL[3:0] 实现4倍频,最后配置PLLDIV[1:0] 进行2分频。

/* PLLCLK = (HSI * 4)/2 = 32 MHz */
RCC->CFGR &= ~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLMUL | RCC_CFGR_PLLDIV);                /* Clear */
RCC->CFGR |=  (RCC_CFGR_PLLSRC_HSI | RCC_CFGR_PLLMUL4 | RCC_CFGR_PLLDIV2);         /* Set   */

步骤 5 :设置外设时钟分频器

所有外设分频系数可保持为1。

/* Peripheral Clock divisors */
RCC->CFGR |= RCC_CFGR_HPRE_DIV1;                         // HCLK = SYSCLK
RCC->CFGR |= RCC_CFGR_PPRE1_DIV1;                        // PCLK1 = HCLK
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;                        // PCLK2 = HCLK

步骤 6 :设置 PLL 为系统时钟

通过设置RCC_CR 寄存器的PLLON 位启用PLL。检测RCC_CRPLLRDY 位确认就绪后,通过SW[1:0] 位将其设为系统时钟。

/* Enable PLL */
 RCC->CR &= ~RCC_CR_PLLON;       /* Disable PLL */
 RCC->CR |= RCC_CR_PLLON;        /* Enable PLL     */
    
   /* Wait until the PLL is ready */
 while((RCC->CR & RCC_CR_PLLRDY) == 0){
       //Nop
   }
    
   /* Select PLL as system Clock */
 RCC->CFGR &= ~RCC_CFGR_SW;            /* Clear */
 RCC->CFGR |=  RCC_CFGR_SW_PLL;    /* Set   */
    
   /* Wait for PLL to become system core clock */
 while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL){
       //Nop
   }

系统核心时钟初始化代码:

完整代码详见GitHub。以下示例展示关键操作流程。

Timing.c
/* Enable PLL */
 RCC->CR &= ~RCC_CR_PLLON;       /* Disable PLL */
 RCC->CR |= RCC_CR_PLLON;        /* Enable PLL     */
    
   /* Wait until the PLL is ready */
 while((RCC->CR & RCC_CR_PLLRDY) == 0){
       //Nop
   }
    
   /* Select PLL as system Clock */
 RCC->CFGR &= ~RCC_CFGR_SW;            /* Clear */
 RCC->CFGR |=  RCC_CFGR_SW_PLL;    /* Set   */
    
   /* Wait for PLL to become system core clock */
 while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL){
       //Nop
   }

GPIO 配置示例

GPIO 初始化流程详解

所有寄存器及设置的详细说明可参考STM32L053R8参考手册。让我们以绿色板载LED和蓝色用户按钮为例,将PORTA引脚5设为输出,PORTC引脚13设为输入。要确认LED和蓝色用户按钮的位置,请参考NUCLEO L053R8原理图

步骤 1 :设置时钟

必须首先完成这一步,否则寄存器不会受到所做更改的影响!RCC_IOPENR 用于启用不同的I/O时钟,因此我们可以写入IOPAE N位来启用PORTA的时钟。

RCC->IOPENR |=  (1UL << 0);        // Enable GPIOA clock

步骤 2 :设置模式(输入 / 输出等)

由于我们以绿色LED为例,需要将PORTA引脚5设为输出引脚。下图中标记的MODE0…MODE15可视为Pin0…Pin15。我们可以通过向GPIOx_MODER 寄存器的MODE5 位写入适当的位序列,将引脚5设为输出。

GPIOA->MODER   &= ~((3ul << 2*pin));       // write 00 to pin location
GPIOA->MODER   |=  ((mode << 2*pin));      // Choose mode

步骤 3 :设置输出类型(推挽 / 开漏)

如果愿意,可以不修改此寄存器,因为其复位状态为推挽模式,这正是我们LED所需的。

  1. 开漏模式: 输出寄存器中的“0”激活N-MOS,而“1”使端口处于高阻态(P-MOS从不激活)。
  2. 推挽模式: 输出寄存器中的“0”激活N-MOS,而“1”激活P-MOS。

GPIOA->OTYPER  |= (0 << 5);        //Output type Push/Pull

步骤 4 :设置速度

我为绿色LED选择了中速。

  1. 极低速: 400kHz
  2. 低速: 2MHz
  3. 中速: 10MHz
  4. 高速: 40MHz
GPIOC->OSPEEDR |=  ((3 << 2*5));   //medium speed

步骤 5 :设置上拉 / 下拉寄存器

如果愿意,可以不修改此寄存器,因为其复位状态为推挽模式,这正是我们LED所需的。请参考步骤3中的图25,查看GPIO引脚及上拉和下拉电阻的示意图。

GPIOC->PUPDR   |= (pupd << 2*5);   //No pull up pull down

同样的流程可用于设置蓝色用户按钮。

初始化 GPIO 代码

完整代码详见GitHub

GPIO.c
/**
  \fn             void GPIO_Init(GPIO_TypeDef* GPIOx, struct GPIO_Parameters GPIO)
  \brief        Initialize GPIO
    \param        GPIO_TypeDef* GPIOx: Which port to initialize, i.e. GPIOA,GPIOB
    \param        struct GPIO_Parameters GPIO: Structure containing all GPIO parameters:
                        * Pin
                        *    Mode
                        *    Output Type
                        * Output Speed
                        * Pull up / Pull down
*/
void GPIO_Init(GPIO_TypeDef* GPIOx, struct GPIO_Parameters GPIO){
     
    /* Enable GPIO Clock depending on port */
    if(GPIOx == GPIOA) RCC->IOPENR |= RCC_IOPENR_GPIOAEN;        // Enable GPIOA clock
    if(GPIOx == GPIOB) RCC->IOPENR |= RCC_IOPENR_GPIOBEN;        // Enable GPIOB clock
    if(GPIOx == GPIOC) RCC->IOPENR |= RCC_IOPENR_GPIOCEN;        // Enable GPIOC clock
    if(GPIOx == GPIOD) RCC->IOPENR |= RCC_IOPENR_GPIODEN;        // Enbale GPIOD clock
     
    /* GPIO Mode Init */
  GPIOx->MODER   &= ~((3ul << 2*GPIO.Pin));               // write 00 to pin location
  GPIOx->MODER   |=  ((GPIO.Mode << 2*GPIO.Pin));        // Choose mode
     
    /* Output Type Init */
  GPIOx->OTYPER  &= ~((~(GPIO.OType) << GPIO.Pin));
     
    /* GPIO Speed Init */
  GPIOx->OSPEEDR |=  ((GPIO.Speed << 2*GPIO.Pin));
     
    /* GPIO PULLUP/PULLDOWN Init */
  GPIOx->PUPDR   |= (GPIO.PuPd << 2*GPIO.Pin);
}
 
/**
  \fn          void Button_Initialize (void)
  \brief       Initialize User Button
*/
void Button_Initialize(void){
     
    /* Set port parameters */
    struct GPIO_Parameters GPIO;
    GPIO.Pin    = Blue_Button;
    GPIO.Mode = Input;
    GPIO.OType = Push_Pull;
    GPIO.PuPd = No_PuPd;
    GPIO.Speed = Low_Speed;
     
    /* Initialize button */
    GPIO_Init(GPIOC,GPIO);
}
 
/**
  \fn          void LED_Init(void)
  \brief       Initialize LD2
*/
void LED_Init(void){
     
    /* Set port parameters */
    struct GPIO_Parameters GPIO;
    GPIO.Pin = Green_LED;
    GPIO.Mode = Output;
    GPIO.OType = Push_Pull;
    GPIO.PuPd = No_PuPd;
    GPIO.Speed = High_Speed;
     
    /* Initialize the LED */
    GPIO_Init(GPIOA,GPIO);
}
GPIO.h
struct GPIO_Parameters
{
    int Pin;
    int Mode;
    int Speed;
    int OType;
    int PuPd;
};
typedef enum Mode_Choices
{
    Input                     = 0,
    Output                    = 1,
    Alternate_Function        = 2,
    Analog_Mode               = 3
}Mode_Choices;
typedef enum OType_Choices
{
    Push_Pull     = 0,
    Open_Drain    = 1
}OType_Choices;
typedef enum Speed_Choices
{
    Very_Low_Speed        = 0,
    Low_Speed             = 1,
    Medium_Speed          = 2,
    High_Speed            = 3
}Speed_Choices;
typedef enum PuPd_Choices
{
    No_PuPd          = 0,
    Pull_Up          = 1,
    Pull_Down        = 2,
}PuPd_Choices;

设置复用功能分步指南

许多情况下您可能需要使用GPIO复用功能。本指南将演示如何在PORTB引脚8和引脚9上分别配置I2C1_SCL和I2C1_SDA复用功能。

步骤 1 :确定正确的复用功能

查看下方可知,我们需要使用AF4将PB8和PB9分别设置为I2C1_SCL和I2C1_SDA

步骤 2 :向 AFRL AFRH 寄存器写入数值

int SCL = 8;                            //SCL pin on PORTB alt fnc 4
int SDA = 9;                            //SDA pin on PORTB alt fnc 4
 
/*Enable Clock for I2C*/
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
 
/* Enable GPIO Clock */
RCC->IOPENR |=  (1UL << 1);
 
/** GPIOB Setup
    *    Alternate Mode PORTB pin 8 and pin 9.............(1)
    *    Set alternate function 4 for both pin 8 an 9.....(2)
    */
    GPIOB->MODER = ~((~GPIOB->MODER) | ((1 << 2*SCL) + (1 << 2*SDA)));    /*(1)*/
    GPIOB->AFR[1] = 0x00000044;                                           /*(2)*/

关于写入AFR寄存器的注意事项:

/**
  * Set AFRL to AF_Value on Pin...(1)
  * Set AFRH to AF_Value on Pin...(2)
*/
 
GPIOB->AFR[0] |= (AF_Value << 4*Pin)       /* (1) */
GPIOB->AFR[1] |= (AF_Value << 4*(Pin - 8)) /* (2) */

ADC 示例

所有代码均可在GitHub上获取。

ADC 初始化分步指南

步骤 1 :设置时钟

RCC->APB2ENR |= (1UL << 9);

步骤 2 :启用内置校准

必须确保ADC尚未启用,若已启用则需先禁用。随后设置ADCAL位启动校准过程。等待校准完成,然后通过向ISR寄存器的EOCAL位写入1来清除标志。

/* Calibration Setup */
if((ADC1->CR & ADC_CR_ADEN) != 0){
    ADC1->CR &= (uint32_t)(~ADC_CR_ADEN);
}
 
ADC1->CR |= ADC_CR_ADCAL;
while((ADC1->ISR & ADC_ISR_EOCAL) == 0);
ADC1->ISR |= ADC_ISR_EOCAL;

步骤 3 :启用 ADC

通过CR寄存器的ADEN位启用ADC。随后应检查ISR寄存器的ADRDY位确认ADC就绪。

/* Enable ADC */
ADC1->CR |= (1UL << 0);
 
/* Wait for ISR bit to set */
while((ADC1->ISR & 1) == 0);

步骤 4 :初始化 ADC 引脚

ADC1->CHSELR |= (1UL << pin);

步骤 5 :配置分辨率、对齐方式和模式

/* Enable Continuous mode */
ADC1->CFGR1 |= (1UL << 13);
 
/* Right Aligned data in DR register */
ADC1->CFGR1 |= (0UL << 5);
 
/* 12-Bit Resolution */
ADC1->CFGR1 |= (0Ul << 3);

步骤 6 :启动转换并获取数据

/* Start Conversion */
ADC1->CR |= (1UL << 2);
 
/* Grab the last 12-Bit Data part */
ADC_Conversion = ADC1->DR & 0x00000FFF;

I2C 示例

I2C 初始化分步指南

所有寄存器及设置的详细说明可参考STM32L053R8参考手册。查阅STM32L053xx技术资料也有助于查找引脚的复用功能。本教程假设您已设置好备用功能。若未完成,请参考《设置备用功能教程》。本教程将分别在PB8引脚配置I2C1_SCL,在PB9引脚配置I2C1_SDA。

步骤 1 :设置时钟

必须配置两个时钟:1.启用GPIO时钟;2.启用I2C时钟

/*Enable Clock for I2C*/
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
 
/* Enable GPIO Clock */
RCC->IOPENR |=  (1UL << 1);

步骤二:配置 I2C 时序参数

最简单的配置方法是使用ST公司提供的I2C时序配置工具。该工具基于Excel运行,可根据您的设置自动生成时钟配置。

/** GPIOB Setup
    *    Standard Mode @100kHz with I2CCLK = 16MHz, rise time = 100ns, fall time = 10ns.(1)
    */
    I2C1->TIMINGR = (uint32_t)0x00503D5A;    /*(1)*/

步骤三:启用 I2C

最后一步是启用I2C功能。这必须是I2C初始化的最后步骤。

I2C1->CR1 |= I2C_CR1_PE;     //Enable I2C1 peripheral

I2C 初始化代码

完整代码详见GitHub

注意我已注释掉I2C中断使能,选择采用轮询方式而非中断方式与各类传感器通信。

I2C.c
/**
  \fn         void I2C_Init(void)
  \brief            I2C initialization
                            *PORTB-8: SCL
                            *PORTB-9: SDA
                            *Digital Noise filter with supression of 1 I2Cclk
                            *fast Mode @400kHz with I2CCLK = 16MHz, rise time = 100ns, fall time = 10ns
*/
void I2C_Init(void){
     
    int SCL = 8;                            //SCL pin on PORTB alt fnc 4
    int SDA = 9;                            //SDA pin on PORTB alt fnc 4
     
    /*Enable Clock for I2C*/
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
     
    /* Enable GPIO Clock */
    RCC->IOPENR |=  (1UL << 1);
     
    /* Don't forget to also enable which interrupts you want in CR1 */
    //interrupt init
//    NVIC_EnableIRQ(I2C1_IRQn);
//    NVIC_SetPriority(I2C1_IRQn,0);
     
/** GPIOB Setup
    *    Digital Noise filter with supression of 1 I2Cclk.(1)
    *    Alternate Mode PORTB pin 8 and pin 9.............(2)
    *    Set alternate function 4 for both pin 8 an 9.....(3)
    */
    I2C1->CR1 |= (1<<8);                                                  /*(1)*/
    GPIOB->MODER = ~((~GPIOB->MODER) | ((1 << 2*SCL) + (1 << 2*SDA)));    /*(2)*/
    GPIOB->AFR[1] = 0x00000044;                                           /*(3)*/
     
/** GPIOB Setup
    *    Standard Mode @100kHz with I2CCLK = 16MHz, rise time = 100ns, fall time = 10ns.(1)
    *    Enable I2C1 peripheral.........................................................(2)
    */
    I2C1->TIMINGR = (uint32_t)0x00503D5A;    /*(1)*/
    I2C1->CR1 |= I2C_CR1_PE;                 /*(2)*/
}

HTS221 温湿度传感器通信教程

所有寄存器及设置的详细说明可参考《温湿度传感器数据手册》。本节将介绍如何与HTS221温湿度传感器通信、配置传感器,并读取温湿度数据。

步骤一:通信时序

35_00

36_00

以下I2C通信描述均以主设备(本案例中为STM32L053R8)视角展开。

写入时序可分解为:

  1. 发送起始信号
  2. 发送从机地址+读写位(读=1,写=0)
  3. 接收从机应答(应为0)
  4. 发送子地址(即目标寄存器地址)
  5. 接收从机应答(应为0)
  6. 发送待写入寄存器的数据
  7. 接收从机应答(应为0)
  8. 发送停止序列

读取序列可更清晰地描述为:

  1. 发送起始信号
  2. 发送从机地址+读写位(读=1,写=0)
  3. 接收从机应答(应为0)
  4. 发送子地址,指定要写入的寄存器
  5. 接收从机应答(应为0)
  6. 再次发送起始序列
  7. 发送从机地址+读写位(读=1,写=0)
  8. 接收从机应答(应为0)
  9. 从子地址接收数据
  10. 发送无主设备确认信号
  11. 发送停止序列

以下通信序列展示了以STM32L053R8作为主设备、HTS221作为从设备通过I2C进行读取的过程。

以下是读取寄存器时步骤1-3 的示例。

该序列可分解为:

起始条件 从设备地址 / 从设备确认
高电平到低电平转换 1011111 0 0

注:从设备发送的是NACK,实际上表示"非未确认"状态。

以下是步骤4 5 的示例。

该序列可分解为:

子地址 从设备确认
00001111 0

以下是从设备步骤6-10 接收数据的示例。

该序列可分解为:

重启条件 从设备地址 / 从设备确认 子地址 Nack (主设备)
高电平到低电平转换 1011111 1 0 10111100 1

以下是停止序列步骤****11 的示例。

步骤 2 :通信序列代码

完整代码详见GitHub

I2C.c
/**
  \fn                    uint32_t I2C_Read_Reg(uint32_t Register)
  \brief            Reads a register, the entire sequence to read (at least for HTS221)
    \param            uint32_t Device: The slave address of the device
    \param            uint32_t Register: The Register to read from
    \returns        uint32_t I2C1_RX_Data: The data read
*/
uint32_t I2C_Read_Reg(uint32_t Device,uint32_t Register){
 
    //Reset CR2 Register
    I2C1->CR2 = 0x00000000;
 
    //Check to see if the bus is busy
    while((I2C1->ISR & I2C_ISR_BUSY) == I2C_ISR_BUSY);
 
    //Set CR2 for 1-byte transfer for Device
    I2C1->CR2 |=(1UL<<16) | (Device<<1);
 
    //Start communication
    I2C1->CR2 |= I2C_CR2_START;
 
    //Check Tx empty before writing to it
    if((I2C1->ISR & I2C_ISR_TXE) == (I2C_ISR_TXE)){
        I2C1->TXDR = Register;
    }
 
    //Wait for transfer to complete
    while((I2C1->ISR & I2C_ISR_TC) == 0);
    //Clear CR2 for new configuration
    I2C1->CR2 = 0x00000000;
 
    //Set CR2 for 1-byte transfer, in read mode for Device
    I2C1->CR2 |= (1UL<<16) | I2C_CR2_RD_WRN | (Device<<1);
 
    //Start communication
    I2C1->CR2 |= I2C_CR2_START;
 
    //Wait for transfer to complete
    while((I2C1->ISR & I2C_ISR_TC) == 0);
 
    //Send Stop Condition
    I2C1->CR2 |= I2C_CR2_STOP;
 
    //Check to see if the bus is busy
    while((I2C1->ISR & I2C_ISR_BUSY) == I2C_ISR_BUSY);
    //Clear Stop bit flag
    I2C1->ICR |= I2C_ICR_STOPCF;
 
    return(I2C1_RX_Data);
}
/**
  \fn                    uint32_t I2C_Read_Reg(uint32_t Register)
  \brief            Reads a register, the entire sequence to read (at least for HTS221)
    \param            uint32_t Device: The slave address to written to
    \param            uint32_t Register: The register that you would like to write to
    \param            uint32_t Data: The data that you would like to write to the register
*/
void I2C_Write_Reg(uint32_t Device,uint32_t Register, uint32_t Data){
 
    //Reset CR2 Register
    I2C1->CR2 = 0x00000000;
 
    //Check to see if the bus is busy
    while((I2C1->ISR & I2C_ISR_BUSY) == I2C_ISR_BUSY);
 
    //Set CR2 for 2-Byte Transfer, for Device
    I2C1->CR2 |= (2UL<<16) | (Device<<1);
 
    //Start communication
    I2C1->CR2 |= I2C_CR2_START;
 
    //Check Tx empty before writing to it
    if((I2C1->ISR & I2C_ISR_TXE) == (I2C_ISR_TXE)){
        I2C1->TXDR = Register;
    }
 
    //Wait for TX Register to clear
    while((I2C1->ISR & I2C_ISR_TXE) == 0);
 
    //Check Tx empty before writing to it
    if((I2C1->ISR & I2C_ISR_TXE) == I2C_ISR_TXE){
        I2C1->TXDR = Data;
    }
 
    //Wait for transfer to complete
    while((I2C1->ISR & I2C_ISR_TC) == 0);
 
    //Send Stop Condition
    I2C1->CR2 |= I2C_CR2_STOP;   
 
    //Check to see if the bus is busy
    while((I2C1->ISR & I2C_ISR_BUSY) == I2C_ISR_BUSY);
 
    //Clear Stop bit flag
    I2C1->ICR |= I2C_ICR_STOPCF;
}

步骤 3 :配置 HTS221

在此配置中,我将HTS221设置为单次模式,这意味着每次仅读取一组数据。这种情况下我们需要关注两个寄存器。


AV_Conf:

该寄存器在精度和功耗之间进行权衡。精度越高,功耗越大。

  • 温度平均次数设置为16
  • 湿度平均次数设置为32

CTRL_REG1:

务必记住先开启设备,再阻止数据更新。阻止数据更新的原因是数据长度为16位,但存储在8位寄存器中。通过阻止数据更新,您必须在设备更新寄存器中的值之前读取低数据和高数据寄存器。这可以防止您读取低位数据后数据被更新,然后再读取高位数据,导致在一次读取中得到两个不同的数值。

  • 向PD写入1
  • 向BDU写入1

请在GitHub上查看完整代码。以下是您应执行的操作要点。

HTS221.c
/**
  \fn                    void HTS221_Init(void)
  \brief            Initialize the HTS221 and check device signature
    \returns        uint8_t Device_Found - Determines if the device was detected
*/
uint8_t HTS221_Init(void){
     
    //Local variables
    uint8_t Device_Found = 0;
    uint32_t AV_CONF_Init = 0x1B;        /*16 Temp (AVGT) and 32 Hum (AVGT)*/
 
    //Read data from register and check signature   
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_WHO_AM_I);
     
    //Check if device signature is correct
    if (I2C1->RXDR == HTS221_DEVICE_ID){
        Device_Found = 1;
    }
    else Device_Found = 0;
     
    /* Setup HTS221_AV_CONF Register */
    if(Device_Found){
        //Set to Default Configuration
        I2C_Write_Reg(HTS221_ADDRESS,HTS221_AV_CONF,AV_CONF_Init);
         
        //Activate and Block Data Update, this will ensure that both the higher and lower bits are read
        I2C_Write_Reg(HTS221_ADDRESS,HTS221_CTRL_REG1,(HTS221_CTRL_REG1_PD | HTS221_CTRL_REG1_BDU));
    }
     
    return(Device_Found);
}

步骤 4 :读取温度和湿度数据

这无疑是最奇怪且最困难的部分,主要是因为数据手册并未真正描述您需要做什么。但别担心,我已经根据为ISK01A1板编写的HAL驱动程序全部搞清楚了。有两个关键方程用于计算温度和湿度。

温度插值:

Temperature(^\circ{}C) = T0\_DegC + \frac{(T\_OUT - T0\_OUT) * (T1\_DegC - T0\_DegC)}{T1\_OUT - T0\_OUT}

湿度插值:

Humidity(rH\%) = H0\_rH + \frac{(H\_OUT - H0\_T0\_OUT) * (H1\_rH - H0\_rH)}{H1\_T0\_OUT - H0\_T0\_OUT}

查看校准寄存器和输出寄存器以找到上述方程中的所有值。

请在GitHub上查看完整代码。以下是您应执行的操作要点。

HTS221.c
/**
  \fn                    HTS221_Temp_Read(void)
  \brief            Reads the temperature from HTS221 in one-shot mode
    \returns        float Temperature_In_F: The temperature in Fahrenheit
*/
float HTS221_Temp_Read(void){
     
    /* Local Variables */
    uint8_t STATUS_REG = 0;
     
    //T0_degC and T1_degC
    uint16_t T0_degC_x8 = 0;
    uint16_t T1_degC_x8 = 0;
    uint16_t Msb_TO_T1_degC = 0;
    float T0_DegC = 0;
    float T1_DegC = 0;
     
    //T_OUT
    uint16_t T_OUT_L = 0;
    uint16_t T_OUT_H = 0;
    float T_OUT = 0;
     
    //T0_OUT and T1_OUT
    int16_t T0_OUT_L = 0;
    int16_t T0_OUT_H = 0;
    int16_t T1_OUT_L = 0;
    int16_t T1_OUT_H = 0;
    float T0_OUT = 0;
    float T1_OUT = 0;
     
    //Temperature Variables
    float Temperature_In_C = 0;
    float Temperature_In_F = 0;
     
    //Start a temperature conversion
    I2C_Write_Reg(HTS221_ADDRESS,HTS221_CTRL_REG2,HTS221_CTRL_REG2_ONE_SHOT);
     
    //Wait for Temperature data to be ready
    do{
        I2C_Read_Reg(HTS221_ADDRESS,HTS221_STATUS_REG);
        STATUS_REG = I2C1->RXDR;
    }while((STATUS_REG & HTS221_STATUS_REG_TDA) == 0);
     
    //Read Temperature Data and Calibration
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_TEMP_OUT_L);
    T_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_TEMP_OUT_H);
    T_OUT_H = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_TO_OUT_L);
    T0_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_T0_OUT_H);
    T0_OUT_H = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_T1_OUT_L);
    T1_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_T1_OUT_H);
    T1_OUT_H = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_T0_degC_x8);
    T0_degC_x8 = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_T1_degC_x8);
    T1_degC_x8 = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_T1_T0_Msb);
    Msb_TO_T1_degC = I2C1->RXDR;
     
    //Process Calibration Registers
    T0_DegC = ((float)(((Msb_TO_T1_degC & 0x3) << 8) | (T0_degC_x8))/8.0);
    T1_DegC = ((float)(((Msb_TO_T1_degC & 0xC) << 6) | (T1_degC_x8))/8.0); //Value in 3rd and 4th bit so only shift 6
    T0_OUT = (float)((T0_OUT_H << 8) | T0_OUT_L);
    T1_OUT = (float)((T1_OUT_H << 8) | T1_OUT_L);
    T_OUT = (float)((T_OUT_H << 8) | T_OUT_L);
     
    //Calculate Temperatuer using linear interpolation and convert to Fahrenheit
    Temperature_In_C = (float)(T0_DegC + ((T_OUT - T0_OUT)*(T1_DegC - T0_DegC))/(T1_OUT - T0_OUT));
    Temperature_In_F = (Temperature_In_C*(9.0/5.0)) +32.0;
     
    return(Temperature_In_F);
}
/**
  \fn                    float HTS221_Humidity_Read(void)
  \brief            Reads the Humidity from HTS221 in one-shot mode
    \returns        float Humidity_rH: The relative humidity %
*/
float HTS221_Humidity_Read(void){
     
    /* Local Variables */
    uint8_t STATUS_REG = 0;
     
    //H0_rH and H1_rH
    uint8_t H0_rH_x2 = 0;
    float H0_rH = 0;
    uint8_t H1_rH_x2 = 0;
    float H1_rH = 0;
    //H_OUT
    float H_OUT = 0;
    uint16_t H_OUT_L = 0;
    uint16_t H_OUT_H = 0;
    //H0_TO_OUT and H1_TO_OUT
    float H0_T0_OUT = 0;
    float H1_T0_OUT = 0;
    uint16_t H0_T0_OUT_L = 0;
    uint16_t H0_T0_OUT_H = 0;
    uint16_t H1_T0_OUT_L = 0;
    uint16_t H1_T0_OUT_H = 0;
    //Humidity Variables
    float Humidity_rH = 0;
     
    //Start a humidity conversion
    I2C_Write_Reg(HTS221_ADDRESS,HTS221_CTRL_REG2,HTS221_CTRL_REG2_ONE_SHOT);
     
    //Wait for Humidity data to be ready
    do{
        I2C_Read_Reg(HTS221_ADDRESS,HTS221_STATUS_REG);
        STATUS_REG = I2C1->RXDR;
    }while((STATUS_REG & HTS221_STATUS_REG_HDA) == 0);
     
    //Read Humidity data and Calibration
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_H0_rH_x2);
    H0_rH_x2 = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_H1_rH_x2);
    H1_rH_x2 = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_HUMIDITY_OUT_L);
    H_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_HUMIDITY_OUT_H);
    H_OUT_H = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_H0_T0_OUT_L);
    H0_T0_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_H0_T0_OUT_H);
    H0_T0_OUT_H = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_H1_T0_OUT_L);
    H1_T0_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(HTS221_ADDRESS,HTS221_H1_T0_OUT_H);
    H1_T0_OUT_H = I2C1->RXDR;
     
    //Process Calibration Registers
    H0_rH = (float)H0_rH_x2/2.0;
    H1_rH = (float)H1_rH_x2/2.0;
    H_OUT = (float)((H_OUT_H << 8) | H_OUT_L);
    H0_T0_OUT = (float)((H0_T0_OUT_H << 8) | H0_T0_OUT_L);
    H1_T0_OUT = (float)((H1_T0_OUT_H << 8) | H1_T0_OUT_L);
     
    //Calculate the relative Humidity using linear interpolation
    Humidity_rH = ( float )(((( H_OUT - H0_T0_OUT ) * ( H1_rH - H0_rH )) / ( H1_T0_OUT - H0_T0_OUT )) + H0_rH );
     
    return(Humidity_rH);
}

LPS25HB 压力传感器通信的逐步指南

所有寄存器和设置的详细参考可在压力传感器数据手册中找到。在本指南的这一部分,我将描述如何与LPS25HB压力传感器通信、设置并读取其数据。

步骤 1 :通信序列

通信序列与HTS221相同。如果您尚未阅读此部分,请点击此处

步骤 2 :配置 LPS25HB

与HTS221类似,我们将使用单次模式,每次读取一组数据。我们还需要为设备上电,并阻止数据更新以避免获取冲突数据(因为数据是24位,分散在8位寄存器中)。所有代码均可在GitHub上找到,但以下是您需要执行的操作要点。

/**
  \fn                    uint8_t LPS25HB_Init(void))
  \brief            Initializes the LPS25HB Pressure sensor
    \returns         uint8_t Device_Found:  1 - Device found, 0 - Device not found
*/
uint8_t LPS25HB_Init(void){
     
    uint8_t Device_Found = 0;
     
    //Read data from register and check signature   
    I2C_Read_Reg(LPS25HB_ADDRESS,LPS25HB_WHO_AM_I);
     
    //Check if device signature is correct
    if (I2C1->RXDR == LPS25HB_DEVICE_ID){
        Device_Found = 1;
    }
    else{
        Device_Found = 0;
    }
     
    if(Device_Found){
        //Power on the device and Block Data Update
        I2C_Write_Reg(LPS25HB_ADDRESS,LPS25HB_CTRL_REG1,(LPS25HB_CTRL_REG1_PD | LPS25HB_CTRL_REG1_BDU));
         
        //Configure the resolution for pressure for 16 internal averages
        I2C_Write_Reg(LPS25HB_ADDRESS,LPS25HB_RES_CONF,LPS25HB_RES_CONF_AVGP0);
    }
     
    return(Device_Found);
}

步骤 3 :读取压力数据

幸运的是,读取压力数据时无需进行线性插值。但在获取压力读数前仍需完成一项操作。

根据数据手册,压力灵敏度为1位/百帕,因此必须对24位值应用以下公式。

LPS25HB\_Pressure(hPa) = \frac{Raw\_Pressure}{4096}

另一个必须完成的技巧是将24位二进制补码值转换为32位二进制补码值。可通过检查24位值的最高有效位,然后用适当值扩展剩余8位来实现。

//convert the 2's complement 24 bit to 2's complement 32 bit
    if (Raw_Pressure & 0x00800000){
            Raw_Pressure |= 0xFF000000;
    }

所有代码均可在GitHub上找到,但以下是应执行操作的要点。

LPS25HB.c
/**
  \fn                    void LPS25HB_Configuration(void)
  \brief            Prints important Configuration registers
    \returns        float LPS25HB_Pressure: pressure measured in mbar
*/
float LPS25HB_Pressure_Read(void){
     
    //Local Variables
    uint8_t PRESS_OUT_XL = 0;
    uint8_t PRESS_OUT_L = 0;
    uint8_t PRESS_OUT_H = 0;
    float Pressure = 0;
    int32_t Raw_Pressure = 0;
    uint8_t LPS25HB_STATUS = 0;
     
    //Start a temperature conversion
    I2C_Write_Reg(LPS25HB_ADDRESS,LPS25HB_CTRL_REG2,LPS25HB_CTRL_REG2_ONE_SHOT);
     
    //Wait for Temperature data to be ready
    do{
        I2C_Read_Reg(LPS25HB_ADDRESS,LPS25HB_STATUS_REG);
        LPS25HB_STATUS = I2C1->RXDR;
    }while((LPS25HB_STATUS & LPS25HB_STATUS_REG_PDA) == 0);
     
    //Read the pressure output registers
    I2C_Read_Reg(LPS25HB_ADDRESS,LPS25HB_PRESS_OUT_XL);
    PRESS_OUT_XL = I2C1->RXDR;
     
    I2C_Read_Reg(LPS25HB_ADDRESS,LPS25HB_PRESS_OUT_L);
    PRESS_OUT_L = I2C1->RXDR;
     
    I2C_Read_Reg(LPS25HB_ADDRESS,LPS25HB_PRESS_OUT_H);
    PRESS_OUT_H = I2C1->RXDR;
     
    //Read the reference Register
     
    /*    Combine pressure into 24 bit value
            PRESS_OUT_H is the High bits     23 - 16
            PRESS_OUT_L is the mid bits     15 - 8
            PRESS_OUT_XL is the lsb                7 - 0
    */
    Raw_Pressure = ((PRESS_OUT_H << 16) | (PRESS_OUT_L << 8) | (PRESS_OUT_XL));
     
    //convert the 2's complement 24 bit to 2's complement 32 bit
    if (Raw_Pressure & 0x00800000){
            Raw_Pressure |= 0xFF000000;
    }
     
    //Calculate Pressure in mbar
    Pressure = (float)Raw_Pressure/4096.0f;
     
    return(Pressure);
}

将压力转换为海拔高度

压力传感器的用途之一是计算海拔高度。我发现一篇关于海拔与压力关系推导的实用文章。您可以在下载该文章,若只需获取海拔计算公式,请参考以下内容。

从上述公式可见,大多数数值都是常量。计算海拔时唯一的未知量就是您的压力读数。

/**
  \fn                    ISK01A1_Get_Altitude(void)
  \brief            Calculates the altitude based on the pressure
    \returns        float Z: Altitude calculation in meters
*/
float ISK01A1_Get_Altitude(void){
     
    /* Calculation should be good up to 11km */
    /* Local Variables */
    const float T0 = 288.15;                            /* Temperatuer at zero altitude, ISA */
    float P = 0.0;                                                /* Measured Pressure                 */
    const float P0 = 101325.0;                        /* Pressure at zero altitude, ISA    */
    const float g = 9.80655;                            /* Acceleration due to gravity       */
    const float L = -0.0065;                            /* Lapse Rate, ISA                   */
    const float R = 287.053;                            /* Gas constant for air              */
     
    /* Read Pressure */
    P = LPS25HB_Pressure_Read()*100.0;            /* Convert mbar to Pa */
     
    /* Calculate Altitude in meters */
    ISK01A1.Altitude = (T0/L)*(pow((P/P0),((-L*R)/g))-1);
     
    return(ISK01A1.Altitude);
}

LSM6DS0 加速度计 / 陀螺仪传感器通信指南

所有寄存器设置参考均可在加速度计和陀螺仪数据手册中找到。本指南章节将介绍如何与LSM6DS0加速度计/陀螺仪传感器通信、设置设备,并读取横滚角、偏航角、俯仰角以及X/Y/Z轴加速度。

步骤 1 :通信序列

通信序列与HTS221相同。如果您尚未阅读此部分,请点击此处

步骤 2 :配置 LSM6DS0

该设备与其他传感器略有不同,因其没有单次触发模式,且上电方式也与其他传感器不同。如下图所示,该设备可能处于几种不同状态。

我决定让加速度计和陀螺仪都处于工作状态,因此必须向CTRL_REG1_G的ODR_G[2:0]位写入大于000(复位)的值。通过此操作可退出省电模式并设置输出数据速率。我决定选择 238Hz 作为输出数据速率 (ODR) ,这意味着设备处于正常模式(非低功耗),消耗电流为 4.3 毫安。

与其他传感器类似,我们仍需向CTRL_REG8寄存器中的BDU位写入数据(因为16位数据被拆分为8位寄存器)。所有代码都可以在GitHub上找到,但核心操作要点如下。

/**
  \fn                    uint8_t LSM6DS0_Init(void)
  \brief            Initialize LSM6DS0 Gyro and accelerometer
    \returns        uint8_t Device_Found: 1 - Device found, 0 - Device not found   
*/
uint8_t LSM6DS0_Init(void){
     
    //Global Variables
    uint8_t Device_Found = 0;
     
    //Read data from register and check signature   
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_WHO_AM_I);
     
    //Check if device signature is correct
    if (I2C1->RXDR == LSM6DS0_DEVICE_ID){
        Device_Found = 1;
    }
    else Device_Found = 0;
     
    if(Device_Found){
         
        //Enable Block Data Update until MSB and LSB read
        I2C_Write_Reg(LSM6DS0_ADDRESS,LSM6DS0_CTRL_REG8,LSM6DS0_CTRL_REG8_BDU);
         
        //Activate both the gyro and the accelerometer at the same ODR of 238 Hz
        I2C_Write_Reg(LSM6DS0_ADDRESS,LSM6DS0_CTRL_REG1_G,LSM6DS0_CTRL_REG1_G_ODR_G2);
    }
     
    return(Device_Found);
}

步骤三:读取传感器数据

这与LPS25HB压力传感器非常相似,我们只需通过简单公式转换原始读数即可。

默认情况下,设备线性加速度量程为FS=±2g,角速度量程为FS=±245度/秒,因此计算公式如下。

LSM6DS0\_Acceleration(miliG - Force) = Raw\_Acceleration * 0.61
LSM6DS0\_Gyroscope(mdps) = Raw\_Gyro\_Reading * 8.75

所有代码均可在GitHub上找到,但以下是您需要执行的操作要点。

LSM6DS0.c
/**
  \fn                    float LSM6DS0_X_Acceleration_Read(void)
  \brief            Retrieves X-Direction Acceleration
    \returns        float Acceleration_X: Acceleration in mg
*/
float LSM6DS0_X_Acceleration_Read(void){
     
    //Local Variables
    uint8_t LSM6DS0_STATUS = 0;
    uint8_t Out_X_XL_L = 0;
    uint8_t Out_X_XL_H = 0;
    int16_t Raw_X = 0;
    float Acceleration_X = 0;
     
    //Wait for acceleration data to be ready
    do{
        I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_STATUS_REG);
        LSM6DS0_STATUS = I2C1->RXDR;
    }while((LSM6DS0_STATUS & LSM6DS0_STATUS_REG_XLDA) == 0);
     
    //Read acceleration output registers
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_X_XL_L);
    Out_X_XL_L = I2C1->RXDR;
     
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_X_XL_H);
    Out_X_XL_H = I2C1->RXDR;
     
    //Combine Lower and upper bits
    Raw_X = ((Out_X_XL_H << 8) | Out_X_XL_L);
     
    //Calculate acceleration based on FS configuration, see datasheet
    Acceleration_X = (float)Raw_X*0.061f;
     
    return(Acceleration_X);
}
/**
  \fn                    float LSM6DS0_Y_Acceleration_Read(void)
  \brief            Retrieves Y-Direction Acceleration
    \returns        float Acceleration_Y: Acceleration in mg
*/
float LSM6DS0_Y_Acceleration_Read(void){
     
    //Local Variables
    uint8_t LSM6DS0_STATUS = 0;
    uint8_t Out_Y_XL_L = 0;
    uint8_t Out_Y_XL_H = 0;
    int16_t Raw_Y = 0;
    float Acceleration_Y = 0;
     
    //Wait for acceleration data to be ready
    do{
        I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_STATUS_REG);
        LSM6DS0_STATUS = I2C1->RXDR;
    }while((LSM6DS0_STATUS & LSM6DS0_STATUS_REG_XLDA) == 0);
     
    //Read acceleration output registers
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Y_XL_L);
    Out_Y_XL_L = I2C1->RXDR;
     
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Y_XL_H);
    Out_Y_XL_H = I2C1->RXDR;
     
    //Combine Lower and upper bits
    Raw_Y = ((Out_Y_XL_H << 8) | Out_Y_XL_L);
     
    //Calculate acceleration based on FS configuration, see datasheet
    Acceleration_Y = (float)Raw_Y*0.061f;
     
    return(Acceleration_Y);
}
/**
  \fn                    float LSM6DS0_Z_Acceleration_Read(void)
  \brief            Retrieves Z-Direction Acceleration
    \returns        float Acceleration_Z: Acceleration in mg
*/
float LSM6DS0_Z_Acceleration_Read(void){
     
    //Local Variables
    uint8_t LSM6DS0_STATUS = 0;
    uint8_t Out_Z_XL_L = 0;
    uint8_t Out_Z_XL_H = 0;
    int16_t Raw_Z = 0;
    float Acceleration_Z = 0;
     
    //Wait for acceleration data to be ready
    do{
        I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_STATUS_REG);
        LSM6DS0_STATUS = I2C1->RXDR;
    }while((LSM6DS0_STATUS & LSM6DS0_STATUS_REG_XLDA) == 0);
     
    //Read acceleration output registers
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Z_XL_L);
    Out_Z_XL_L = I2C1->RXDR;
     
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Z_XL_H);
    Out_Z_XL_H = I2C1->RXDR;
     
    //Combine the lower and upper bits
    Raw_Z = ((Out_Z_XL_H << 8) | Out_Z_XL_L);
     
    //Calculate acceleration based on FS configuration, see datasheet
    Acceleration_Z = (float)Raw_Z*0.061f;
     
    return(Acceleration_Z);
}
/**
  \fn                    float LSM6DS0_Gyroscope_Roll_Read(void)
  \brief            Retrieves X-Direction(Roll)
    \returns        float Roll: Roll in mdps
*/
float LSM6DS0_Gyroscope_Roll_Read(void){
     
    //Local Variables
    uint8_t LSM6DS0_STATUS = 0;
    uint8_t Roll_L = 0;
    uint8_t Roll_H = 0;
    int16_t Raw_Roll = 0;
    float Roll = 0;
     
    //Wait for roll data to be ready
    do{
        I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_STATUS_REG);
        LSM6DS0_STATUS = I2C1->RXDR;
    }while((LSM6DS0_STATUS & LSM6DS0_STATUS_REG_GDA) == 0);
     
    //Read Gyroscope output registers
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_X_G_L);
    Roll_L = I2C1->RXDR;
     
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_X_G_H);
    Roll_H = I2C1->RXDR;
     
    //Combine lower and upper bits
    Raw_Roll = ((Roll_H << 8) | Roll_L);
     
    //Calculate Roll based on FS configuration,see datasheet
    Roll = (float)Raw_Roll*8.75f;
     
    return(Roll);
}
/**
  \fn                    float LSM6DS0_Gyroscope_Pitch_Read(void)
  \brief            Retrieves Y-Direction(Pitch)
    \returns        float Pitch: Pitch in mdps
*/
float LSM6DS0_Gyroscope_Pitch_Read(void){
     
    //Local Variables
    uint8_t LSM6DS0_STATUS = 0;
    uint8_t Pitch_L = 0;
    uint8_t Pitch_H = 0;
    int16_t Raw_Pitch = 0;
    float Pitch = 0;
     
    //Wait for pitch data to be ready
    do{
        I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_STATUS_REG);
        LSM6DS0_STATUS = I2C1->RXDR;
    }while((LSM6DS0_STATUS & LSM6DS0_STATUS_REG_GDA) == 0);
     
    //Read gyroscope output registers
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Y_G_L);
    Pitch_L = I2C1->RXDR;
     
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Y_G_H);
    Pitch_H = I2C1->RXDR;
     
    //Combine lower and upper bits
    Raw_Pitch = ((Pitch_H << 8) | Pitch_L);
     
    //Calculate Pitch based on FS configuration,see datasheet
    Pitch = (float)Raw_Pitch*8.75f;
     
    return(Pitch);
}
/**
  \fn                    float LSM6DS0_Gyroscope_Yaw_Read(void)
  \brief            Retrieves Z-Direction(Yaw)
    \returns        float Yaw: Yaw in mdps
*/
float LSM6DS0_Gyroscope_Yaw_Read(void){
     
    //Local Variables
    uint8_t LSM6DS0_STATUS = 0;
    uint8_t Yaw_L = 0;
    uint8_t Yaw_H = 0;
    int16_t Raw_Yaw = 0;
    float Yaw = 0;
     
    //Wait for Yaw data to be ready
    do{
        I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_STATUS_REG);
        LSM6DS0_STATUS = I2C1->RXDR;
    }while((LSM6DS0_STATUS & LSM6DS0_STATUS_REG_GDA) == 0);
     
    //Read gyroscope output registers
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Z_G_L);
    Yaw_L = I2C1->RXDR;
     
    I2C_Read_Reg(LSM6DS0_ADDRESS,LSM6DS0_OUT_Z_G_H);
    Yaw_H = I2C1->RXDR;
     
    //Combine lower and upper bits
    Raw_Yaw = ((Yaw_H << 8) | Yaw_L);
     
    //Calculate Yaw based on FS configuration,see datasheet
    Yaw = (float)Raw_Yaw*8.75f;
     
    return(Yaw);
}

LIS3MDL 磁力计传感器通信指南

所有寄存器设置参考详见磁力计数据手册。本节将介绍如何与LIS3MDL磁力计传感器通信、配置该传感器,并读取X、Y、Z三个方向的磁场数据。

步骤 1 :通信序列

通信序列与HTS221相同。如果您尚未阅读此部分,请点击此处

步骤二:配置 LIS3MDL

需要重点关注4个不同的寄存器。

通过CTRL_REG1 寄存器中的OM[1:0] 位序列设置,可调整X、Y方向的功耗模式。下方代码将其设置为中等性能模式。此外,我们还可以通过DO[2:0] 序列设置输出数据速率。以下示例中我选择了10Hz的输出数据速率。

//Performance Vs. Power consumption XY (medium), and set data rate to 10Hz
I2C_Write_Reg(LIS3MDL_ADDRESS,LIS3MDL_CTRL_REG1,(LIS3MDL_CTRL_REG1_OM0 | LIS3MDL_CTRL_REG1_DO2));
         
//Full scale = +/- 4 gauss (Default value) - just in case
I2C_Write_Reg(LIS3MDL_ADDRESS,LIS3MDL_CTRL_REG2,0x0);
         
//Performance Vs. Power consumption Z (medium)
I2C_Write_Reg(LIS3MDL_ADDRESS,LTS3MDL_CTRL_REG4,LIS3MDL_CTRL_REG4_OMZ0);
         
//Enable BDU so you ensure MSB and LSB have been read
I2C_Write_Reg(LIS3MDL_ADDRESS,LTS3MDL_CTRL_REG5,LIS3MDL_CTRL_REG5_BDU);

步骤三:读取传感器数据

与其他传感器类似,我们需要对读数进行微小调整。本例中FS=±4,意味着需要将原始读数除以6842。

Magnetometer\ Reading(mili - Gauss) = \frac{Raw\_Reading}{6842}

所有代码均可在GitHub上找到,但以下是您需要执行的操作要点。

LIS3MDL.c
/**
  \fn                    float LIS3MDL_X_Read(void)
  \brief            Reads the magnetic field from the X axis
    \returns         float OUT_X: the magnetic field in mG
*/
float LIS3MDL_X_Read(void){
     
    //Local variables
    uint8_t LIS3MDL_STATUS = 0;
    uint8_t OUT_X_L = 0;
    uint8_t OUT_X_H = 0;
    float OUT_X = 0;
    int16_t Raw_X = 0;
     
    //Set device to continuous conversion mode
    I2C_Write_Reg(LIS3MDL_ADDRESS,LTS3MDL_CTRL_REG3,LIS3MDL_CTRL_REG3_MD0);
     
    //Wait for X coordinate data to be ready
    do{
        I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_STATUS_REG);
        LIS3MDL_STATUS = I2C1->RXDR;
    }while((LIS3MDL_STATUS & LIS3MDL_STATUS_REG_XDA) == 0);
     
    //Read X Axis magnetic field
    I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_OUT_X_L);
    OUT_X_L = I2C1->RXDR;
     
    I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_OUT_X_H);
    OUT_X_H = I2C1->RXDR;
     
    //Process X coordinates
    Raw_X = ((OUT_X_H << 8) | OUT_X_L);
     
    /*
     *1/6842 = ~0.146 mG/LSB according to datasheet
     */
    OUT_X = (float)Raw_X * 0.146f;        // when using +/- 4 gauss
    return(OUT_X);
}
/**
  \fn                    float LIS3MDL_Y_Read(void)
  \brief            Reads the magnetic field from the Y axis
    \returns         float OUT_Y: the magnetic field in mG
*/
float LIS3MDL_Y_Read(void){
     
    //Local Variables
    uint8_t LIS3MDL_STATUS = 0;
    uint8_t OUT_Y_L = 0;
    uint8_t OUT_Y_H = 0;
    float OUT_Y = 0;
    int16_t Raw_Y = 0;
     
    //Set device to continuous conversion mode
    I2C_Write_Reg(LIS3MDL_ADDRESS,LTS3MDL_CTRL_REG3,LIS3MDL_CTRL_REG3_MD0);
     
    //Wait for X coordinate data to be ready
    do{
        I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_STATUS_REG);
        LIS3MDL_STATUS = I2C1->RXDR;
    }while((LIS3MDL_STATUS & LIS3MDL_STATUS_REG_YDA) == 0);
     
    //Read Y Axis magnetic field
    I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_OUT_Y_L);
    OUT_Y_L = I2C1->RXDR;
     
    I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_OUT_Y_H);
    OUT_Y_H = I2C1->RXDR;
     
    /*
    1/6842 = ~0.146 mG/LSB according to datasheet
    */
    Raw_Y = ((OUT_Y_H << 8) | OUT_Y_L);
    OUT_Y = (float)Raw_Y * 0.146f;        // when using +/- 4 gauss
    return(OUT_Y);
}
/**
  \fn                    float LIS3MDL_Z_Read(void)
  \brief            Reads the magnetic field from the Z axis
    \returns         float OUT_Z: the magnetic field in mG
*/
float LIS3MDL_Z_Read(void){
     
    //Local Variables
    uint8_t LIS3MDL_STATUS = 0;
    uint8_t OUT_Z_L = 0;
    uint8_t OUT_Z_H = 0;
    float OUT_Z = 0;
    int16_t Raw_Z = 0;
     
    //Set device to continuous conversion mode
    I2C_Write_Reg(LIS3MDL_ADDRESS,LTS3MDL_CTRL_REG3,LIS3MDL_CTRL_REG3_MD0);
     
    //Wait for X coordinate data to be ready
    do{
        I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_STATUS_REG);
        LIS3MDL_STATUS = I2C1->RXDR;
    }while((LIS3MDL_STATUS & LIS3MDL_STATUS_REG_ZDA) == 0);
     
    //Read Z Axis magnetic field
    I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_OUT_Z_L);
    OUT_Z_L = I2C1->RXDR;
     
    I2C_Read_Reg(LIS3MDL_ADDRESS,LIS3MDL_OUT_Z_H);
    OUT_Z_H = I2C1->RXDR;
     
    Raw_Z = ((OUT_Z_H << 8) | OUT_Z_L);
     
    /*
    1/6842 = ~0.146 mG/LSB according to datasheet
    */
    OUT_Z = (float)Raw_Z * 0.146f;        // when using +/- 4 gauss
    return(OUT_Z);
}

USART/LPUART 应用示例

USART/LPUART 初始化指南

所有寄存器及设置的详细说明可参考STM32L053R8参考手册。查阅STM32L053xx技术资料也有助于查找引脚的复用功能。本教程假设您已设置好备用功能。若未完成,请参考《设置备用功能教程》。在本教程中,我们将设置几个UART接口。STM32L053R8拥有多种不同的UART类型。第一种是标准USART(通用同步异步收发器),第二种是低功耗UART(LPUART)。LPUART的设置与USART几乎相同,但为了全面起见我仍会详述。

步骤 1 :配置时钟

只需在RCC_IOPENR寄存器中写入IOPAEN,并通过在RCC_APB2ENR寄存器中写入USART1EN来启用USART时钟。默认时钟源为PCLK。

RCC->IOPENR   |=   RCC_IOPENR_GPIOAEN;      /* Enable GPIOA clock */
RCC->APB2ENR  |=   RCC_APB2ENR_USART1EN;    /* Enable USART#1 clock */

步骤 2 :配置 USART 波特率寄存器

计算波特率时需注意几个公式。这完全取决于你采用的过采样倍数或是否使用LPUART。什么是过采样?即对UART接收的每个比特位进行多次采样。采样完成后,将选取多数值(0或1)作为最终值。

警告:8 倍过采样时不可直接写入计算出的 USARTDIV

16 倍过采样时(默认)

LPUARTDIV = \frac{F_{ck}}{Baud\ Rate}

可见每个比特时间被采样16次以确定数值。

8 倍过采样时

LPUARTDIV = \frac{2 * F_{ck}}{Baud\ Rate}

可见每个比特时间被采样8次以确定数值。

本质上需要遵循特定规则才能向寄存器写入正确值。请参阅STM32L053xx参考手册第759页验证我所写的步骤。

  1. BRR[2:0] = 计算所得USARTDIV[3:0]右移一位后的值
  2. BRR[3] = 0,始终如此!
  3. BRR[15:4] = USARTDIV[15:4]。

当使用 LPUART

LPUARTDIV = \frac{256 * F_{ck}}{Baud\ Rate}

过采样无选择余地,因此这是LPUART唯一可用的计算公式。

USART BRR 代码:

/* Define statements outside function */
#define PCLK    32000000                                    // Peripheral Clock
#define BAUD    9600                                            // Baud rate
 
/* Local variables */
uint16_t USARTDIV = 0;
uint16_t USART_FRACTION = 0;
uint16_t USART_MANTISSA = 0;
 
/* Check to see if oversampling by 8 or 16 to properly set baud rate*/
  if((USART2->CR1 & USART_CR1_OVER8) == 1){
      USARTDIV = PCLK/BAUD;
      USART_FRACTION = ((USARTDIV & 0x0F) >> 1) & (0xB);
      USART_MANTISSA = ((USARTDIV & 0xFFF0) << 4);
      USARTDIV = USART_MANTISSA | USART_FRACTION;
      USART2->BRR = USARTDIV;                                                            /* 9600 Baud with 32MHz peripheral clock 8bit oversampling */
  }
  else{
      USART2->BRR = PCLK/BAUD;                                                        /* 9600 Baud with 32MHz peripheral clock 16bit oversampling */   
  }

LPUART BRR 代码:

由于我们的MCU是32位,最大值仅为2^32-1 = 4294967295,无法处理像256*32,000,000这样的数值。但我们可以用个小技巧,先除后乘,具体实现如下所示。

/* Define statements outside function */
#define PCLK    32000000                                    // Peripheral Clock
#define BAUD    9600                                            // Baud rate
 
LPUART1->BRR      = (unsigned long)((256.0f/BAUD)*PCLK);         /* 9600 baud @ 32MHz */

步骤 3 :配置 CR1 寄存器

我们需要通过该寄存器实现几个不同的功能。需启用RX和TX,因此同时设置RETE 。还需指定字长,通过向M[1:0] 写入相应值实现。请注意M0位于第12位,M1位于第28位。

USART2->CR3    = 0x0000;                                                      /* no flow control */
USART2->CR2    = 0x0000;                                                      /* 1 stop bit */
 
/* 1 stop bit, 8 data bits */
  USART2->CR1    = ((USART_CR1_RE) |                                          /* enable RX  */
                    (USART_CR1_TE) |                                          /* enable TX  */
                    (USART_CR1_UE) |                                          /* enable USART */
                    (USART_CR1_RXNEIE));                                      /* Enable Interrupt */

USART/LPUART 初始化代码(含读写功能!)

所有代码均可在GitHub上找到,但以下是您需要执行的操作要点。

Serial.c
/*------------------------------------------------------------------------------------------------------
 * Name:    Serial.c
 * Purpose: Used to communicat with your computer using USART2
 * Date:         7/14/15
 * Author:    Christopher Jordan - Denny
 *------------------------------------------------------------------------------------------------------
 * Note(s):    SER_PutChar are used to redefine printf function
 *----------------------------------------------------------------------------------------------------*/
/*-------------------------------------------------Include Statements---------------------------------*/
#include "stm32l0xx.h"                  // Specific Device header
#include "Serial.h"
/*-------------------------------------------------Define Statements----------------------------------*/
#define PCLK    32000000                                    // Peripheral Clock
#define BAUD    9600                                            // Baud rate
/*-------------------------------------------------Functions------------------------------------------*/
/**
  \fn          void SER_Initialize (void)
  \brief       Initializes USART2 to be used with the serial monitor
*/
     
void SER_Initialize (void){
  /* Local variables */
  uint16_t USARTDIV = 0;
  uint16_t USART_FRACTION = 0;
  uint16_t USART_MANTISSA = 0;
  RCC->IOPENR   |=   ( 1ul <<  0);         /* Enable GPIOA clock   */
  RCC->APB1ENR  |=   ( 1ul << 17);         /* Enable USART#2 clock */
  /* Configure PA3 to USART2_RX, PA2 to USART2_TX */
  GPIOA->AFR[0] &= ~((15ul << 4* 3) | (15ul << 4* 2) );
  GPIOA->AFR[0] |=  (( 4ul << 4* 3) | ( 4ul << 4* 2) );
  GPIOA->MODER  &= ~(( 3ul << 2* 3) | ( 3ul << 2* 2) );
  GPIOA->MODER  |=  (( 2ul << 2* 3) | ( 2ul << 2* 2) );
  /* Check to see if oversampling by 8 or 16 to properly set baud rate*/
  if((USART2->CR1 & USART_CR1_OVER8) == 1){
      USARTDIV = PCLK/BAUD;
      USART_FRACTION = ((USARTDIV & 0x0F) >> 1) & (0xB);
      USART_MANTISSA = ((USARTDIV & 0xFFF0) << 4);
      USARTDIV = USART_MANTISSA | USART_FRACTION;
      USART2->BRR = USARTDIV;                                                            /* 9600 Baud with 32MHz peripheral clock 8bit oversampling */
  }
  else{
      USART2->BRR = PCLK/BAUD;                                                        /* 9600 Baud with 32MHz peripheral clock 16bit oversampling */   
  }
  USART2->CR3   = 0x0000;                 /* no flow control */
  USART2->CR2   = 0x0000;                 /* 1 stop bit */
     
    /* 1 stop bit, 8 data bits */
  USART2->CR1    = ((USART_CR1_RE) |                                                /* enable RX  */
                     (USART_CR1_TE) |                                                /* enable TX  */
                     (USART_CR1_UE) |                                          /* enable USART */
                                         (USART_CR1_RXNEIE));                                        /* Enable Interrupt */
}
/**
  \fn                    char SER_PutChar(char ch)
  \brief            Put character to the serial monitor
    \param            char ch: Character to send to the serial monitor
    \returns        char ch: The character that was sent to the serial monitor
*/
char SER_PutChar(char ch){
    //Wait for buffer to be empty
  while ((USART2->ISR & USART_ISR_TXE) == 0){
            //Nop
    }
     
    //Send character
  USART2->TDR = (ch);
  return (ch);
}
/**
  \fn                    int SER_GetChar(void)
  \brief            Get character from the serial monitor
    \returns        int USART2->RDR: The value of character from the serial monitor
*/
int SER_GetChar(void){
  if (USART2->ISR & USART_ISR_RXNE)
    return (USART2->RDR);
  return (-1);
}

重定向 Printf

有个巧妙方法可在C语言中通过USART使用printf函数。一如既往,所有代码均可在GitHub上找到。

/*------------------------------------------------------------------------------------------------------
 * Name:    Retarget.c
 * Purpose: Retargets printf C function to be used with USART2
 * Date:         7/14/15
 * Author:    Christopher Jordan - Denny
 *------------------------------------------------------------------------------------------------------
 * Note(s):    This is really just magic
 *----------------------------------------------------------------------------------------------------*/
/*--------------------------------------Include Statements--------------------------------------------*/
#include <stdio.h>
#include <rt_misc.h>
#include "Serial.h"
/*--------------------------------------Additional Compiler Info--------------------------------------*/
#pragma import(__use_no_semihosting_swi)
/*--------------------------------------Structure Definitions-----------------------------------------*/
struct __FILE { int handle;};
FILE __stdout;
FILE __stdin;
/*--------------------------------------Functions-----------------------------------------------------*/
int fputc(int c, FILE *f){
  return (SER_PutChar(c));
}
int fgetc(FILE *f){
  return (SER_GetChar());
}
void _sys_exit(int return_code){
label:  goto label;  /* endless loop */
}

FGPMMOPA6H GPS 模块通信实战指南

FGPMMOPA6B GPS模块数据手册提供了所有设置和NMEA语句的完整参考。完整命令列表请参阅PMTK命令集。本节将介绍如何与FGPMMOPA6H GPS模块建立通信并进行配置。同时还将讲解接收数据的解析方法。强烈建议结合GitHub上的完整代码学习本节内容。若未配置USART,请先阅读《USART/LPUART初始化指南》

步骤 1 :设置刷新率

首先需声明位置回显时间,其次声明更新时间。必须同时设置这两项才能更改刷新时间。

#define PMTK_SET_NMEA_UPDATE_200_MILLIHERTZ        "$PMTK220,5000*1B\r\n"  // Once every 5 seconds, 200 millihertz.
#define PMTK_API_SET_FIX_CTL_200_MILLIHERTZ        "$PMTK300,5000,0,0,0,0*18\r\n"  // Once every 5 seconds, 200 millihertz.
 
USART1_Send(PMTK_API_SET_FIX_CTL_200_MILLIHERTZ);        /* 5s Position echo time   */
USART1_Send(PMTK_SET_NMEA_UPDATE_200_MILLIHERTZ);        /* 5s update time                */

步骤 2 :设置接收的 GPS 消息类型

默认接收5种消息:$GPGGA、$GPGSA、$GPGSV、$GPRMC和$GPVTG。其中$GPRMC和$GPGGA最为实用。选择这两条消息只需一条简单指令。

// turn on GPRMC and GGA
#define PMTK_SET_NMEA_OUTPUT_RMCGGA                        "$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n"
 
USART1_Send(PMTK_SET_NMEA_OUTPUT_RMCGGA);                      /* Output RMC Data and GGA */

步骤 3 :整理接收的 GPS 数据

我制作了流程图来帮助理解如何分类接收到的消息。此操作仅将消息归入对应标签,不解析数据内容。本例中我采用中断机制处理数据接收。

FGPMMOPA6H.c
/*---------------------------------NMEA Output Sentences----------------------------------------------*/
static const char GGA_Tag[] = "$GPGGA";
static const char GSA_Tag[] = "$GPGSA";
static const char GSV_Tag[] = "$GPGSV";
static const char RMC_Tag[] = "$GPRMC";
static const char VTG_Tag[] = "$GPVTG";
/*---------------------------------Globals------------------------------------------------------------*/
volatile int                 CharIndex = 0;                                                /* Character index of the char array */
const int                     NMEA_LENGTH = 128;                                        /* The max length of one NMEA line */
char                                 Rx_Data[NMEA_LENGTH] = "0";                        /* Rx Sring */
volatile uint8_t         Transmission_In_Progress = FALSE;            /* Are we in between a $ and \n */
char                                 GGA_Message[128];                                            /* Original GGA message */
char                                 GSA_Message[128];                                            /* Original GSA message */
char                                 GSV_Message[128];                                            /* Original GSV message */
char                                 RMC_Message[128];                                            /* Original RMC message */
char                                 VTG_Message[128];                                            /* Original VTG message */
 
/**
  \fn          void USART1_IRQHandler(void)
  \brief       Global interrupt handler for USART1, handles Rx interrupt
*/
void USART1_IRQHandler(void){
     
    if(USART1->ISR & USART_ISR_RXNE){
         
        /* Reads and CLEARS RXNE Flag */
    Rx_Data[CharIndex] = USART1->RDR;
         
        /* Data Not Ready */
        RMC.New_Data_Ready = FALSE;
        GGA.New_Data_Ready = FALSE;
         
        /* If Rx_Data = $, then we are in transmission */
        if(Rx_Data[CharIndex] == '$'){
            Transmission_In_Progress = TRUE;
        }
         
        /* If we are transmitting then save to proper message once complete */
        if(Transmission_In_Progress == TRUE){
            if(Rx_Data[CharIndex] == '\n'){
                if(strncmp(GGA_Tag,Rx_Data,(sizeof(GGA_Tag)-1)) == 0){
                    strcpy(GGA_Message,Rx_Data);
                    GGA.New_Data_Ready = TRUE;
                }
                if(strncmp(GSA_Tag,Rx_Data,(sizeof(GSA_Tag)-1)) == 0){
                    strcpy(GSA_Message,Rx_Data);
                }
                if(strncmp(GSV_Tag,Rx_Data,(sizeof(GSV_Tag)-1)) == 0){
                    strcpy(GSV_Message,Rx_Data);
                }
                if(strncmp(RMC_Tag,Rx_Data,(sizeof(RMC_Tag)-1)) == 0){
                    strcpy(RMC_Message,Rx_Data);
                    RMC.New_Data_Ready = TRUE;
                }
                if(strncmp(VTG_Tag,Rx_Data,(sizeof(VTG_Tag)-1)) == 0){
                    strcpy(VTG_Message,Rx_Data);
                }
                Transmission_In_Progress = FALSE;
                CharIndex = 0;
                memset(Rx_Data,0,sizeof(Rx_Data));
            }
            else{
                CharIndex++;
            }
        }
    }
}

步骤 4 :解析特定消息数据

GPS数据以逗号分隔,结尾为回车符或换行符。例如$GPRMC消息格式如下所示:

$GPRMC,064951.000,A,2307.1256,N,12016.4438,E,0.03,165.48,260406,3.05,W,A*2C

这意味着我们只需按逗号分隔符进行解析。C语言的strtok函数非常适合此操作,其用法稍显特殊,建议深入研究。强烈推荐查看GitHub上的相关代码。核心操作逻辑可参考下方示例。

FGPMMOPA6H.c
/**
  \fn          void FGPMMOPA6H_Parse_RMC_Data(void)
  \brief       Parses the RMC Data based on the , delimeter                   
*/
void FGPMMOPA6H_Parse_RMC_Data(void){
     
    //Local Variables
    char RMC_Message_Copy[128] = "";
    const char delimeter[2] = ",";
    char *token = "";
    int i = 0;
    char temp[11][12];            /* [11][12]: 11 strings, of length 12 */
     
    //Copy original RMC to a copy in order to not destroy message
    strcpy(RMC_Message_Copy,RMC_Message);
     
    //Seperated Message
    /* get the first token */
   token = strtok(RMC_Message_Copy, delimeter);
    
   /* walk through other tokens */
   while( token != NULL )
   {
         strcpy(temp[i],token);
         i++;
     token = strtok(NULL, delimeter);
   }
      
     //Copy the message into its individual components
    strcpy(RMC.Message_ID,temp[0]);
    strcpy(RMC.UTC_Time,temp[1]);
    strcpy(RMC.Status,temp[2]);
    strcpy(RMC.Latitude,temp[3]);
    strcpy(RMC.N_S_Indicator,temp[4]);
    strcpy(RMC.Longitude,temp[5]);
    strcpy(RMC.E_W_Indicator,temp[6]);
    strcpy(RMC.Speed_Over_Ground,temp[7]);
    strcpy(RMC.Course_Over_Ground,temp[8]);
    strcpy(RMC.Date,temp[9]);
    strcpy(RMC.Mode,temp[10]);
}

XBee Pro S1 通信配置指南

所有参数设置可参考XBee Pro用户手册。本节将说明如何配置并实现XBee Pro S1的通信。强烈建议查看GitHub上的完整代码。若未配置USART,请先阅读《USART/LPUART初始化指南》。若对串口监视程序不熟悉,请参阅《使用CoolTerm与XBee通信》快速入门教程。

第1步:设置地址

有几个地址需要您关注。

地址类型 指令发出 目的:
MY ATMY 读取/写入MY地址,即XBee的源地址
PAN ATID 个人区域网络 - 读取/写入XBee的16位ID只有同一PAN内的XBee才能通信
DH ATDH 目标高位 - 读取 / 写入 XBee 的高 32 位目标地址
DL ATDL 目标低位 - 读取 / 写入 XBee 的低 32 位目标地址
CH ATCH 读取/写入XBee的工作频道(1)

(1) XBee频道由以下符合802.15.4标准的公式决定

Center\ Frequency(MHz) = 2.405 + (CH - 11.0) * 5

默认CH = 0x0C或12,表示中心频率=7.405MHz

修改这些值的指令序列如下

接下来我将展示如何在两个XBee之间建立点对点网络的示例重申,如果您不熟悉串行监视程序,请参阅《使用CoolTerm与XBee通信》

您会注意到,每次寄存器成功写入后,XBee都会返回一个OK响应。我们可以利用这一点来确保寄存器已正确设置,并通过中断来处理接收到的数据,如下所示。

XBeePro24.c
/**
    \fn            void RNG_LPUART1_IRQHandler(void)
    \brief    Global interrupt handler for LPUART, Currently only handles RX
*/
void RNG_LPUART1_IRQHandler(void){
    if((LPUART1->ISR & USART_ISR_RXNE) == USART_ISR_RXNE){
         
        /* Read RX Data */
        RX_Data[ChIndex] = LPUART1->RDR;
         
        /* Check for end of recieved data */
        if(RX_Data[ChIndex] == '\r'){
             
            /* Compare string to OK to see if Acknowledged */
            if(strncmp(OK,RX_Data,(sizeof(OK)-1)) == 0){
                Device_Ack_Flag = TRUE;
            }
             
            /* Copy XBee message */
            strcpy(XBee_Message,RX_Data);
             
            /* set data ready to read flag */
            XBee_Ready_To_Read = TRUE;
             
            /* Clear RX_Data */
            ChIndex = 0;
            memset(RX_Data,0,sizeof(RX_Data));
             
        }else ChIndex++;
    }
}

要在TeraTerm中应用我们示例中的设置,可以按照以下步骤操作。

XBeePro24.c
/**
    \fn                void XBee_Init(void)
    \brief        Initializes the XBEE
*/
void XBee_Init(void){
     
    /* Enter AT command mode */
    Delay(1000);
    LPUART1_Send(ENTER_AT_COMMAND_MODE);
    Wait_For_OK();
     
    /* MY address = 2 */
    LPUART1_Send(SET_ATMY);
    Wait_For_OK();
     
    /* PAN = 3001 */
    LPUART1_Send(SET_ATID);
    Wait_For_OK();
     
    /* Set Destination address high */
    LPUART1_Send(SET_ATDH);
    Wait_For_OK();
     
    /* Set Destination Address low */
    LPUART1_Send(SET_ATDL);
    Wait_For_OK();
     
    /* Set Channel */
    LPUART1_Send(SET_ATCH);
    Wait_For_OK();
     
    /* End AT command mode */
    LPUART1_Send(EXIT_AT_COMMAND_MODE);
    Wait_For_OK();
     
    printf("#####  XBee            Initialized  #####\r\n");
}
 
/**
    \fn                void Wait_For_OK(void)
    \brief        Waits until the XBEE has sent the OK message
                        This is done when it has changed its settings
*/
void Wait_For_OK(void){
     
    /* Wait for XBee Acknowledge */
    while(Device_Ack_Flag == 0){
        //Nop
    }
     
    /* Reset Flags */
    Device_Ack_Flag = FALSE;
    XBee_Ready_To_Read = FALSE;
     
}
/**
    \fn                void Wait_For_Data(void)
    \brief        This waits for the data the XBEE writes to the bus
*/
void Wait_For_Data(void){
     
    /* Wait for data to be copied */
    while(XBee_Ready_To_Read == 0){
        //Nop
    }
     
    /* Reset Flags */
    Device_Ack_Flag = FALSE;
    XBee_Ready_To_Read = FALSE;
}

第2步:发送数据

正确设置地址后,发送数据就很简单了,只需向XBee写入即可。

/* Send data over the XBEE */
LPUART1_Send(Data);

使用 CoolTerm XBee 通信

这是一份关于如何设置CoolTerm作为串行监视器的简短指南。您可以从此处下载CoolTerm。

第1步:设置串行端口

点击齿轮和扳手图标(称为“选项 ”),您可以使用以下设置与默认配置下的XBee Pro s1进行通信。

第2步:设置终端

在左侧菜单中点击“终端”,然后勾选“本地回显”框。

就这样,您已经准备好通过串行端口监控和与XBee或其他设备通信了,只需点击连接按钮即可。

脉宽调制示例?

初始化 PWM 逐步指南

所有设置的详细参考可以在STM32L053xx参考手册中找到。在本指南的这一部分,我将描述如何使用通用定时器(TIM21/22)启用脉宽调制(PWM)。该信号将用于控制伺服电机。我强烈建议查看GitHub上的完整代码。

第1步:启用时钟

 RCC->APB2ENR |= RCC_APB2ENR_TIM22EN;

第2步:设置周期

这段代码将设置PWM的时钟速度、PWM周期以及脉冲长度。在我的示例中,时钟设置为32MHz,因此要获得一个良好的1MHz时钟,我们可以将TIM22_PSC 的值设置为31。这来自于公式:

CK\_CNT = \frac{FCK\_PSC}{PSC[15:0] + 1}

其中FCK_PSC 为时钟频率(32MHz),PSC[15:0] 是写入TIM22_PSC 寄存器的值(31),CK_CNT 为定时器时钟。下图能最直观地说明这一点。

现在可以设置所需周期,由于我使用PWM控制伺服电机,故选择20ms周期。既然现在时钟为1MHz(周期1µs),向TIM22_ARR 寄存器写入2000即可获得20ms周期。最后需要计算的是脉冲宽度。可通过从TIM22_ARR 寄存器值减去脉冲持续时间,并将结果写入TIM22_CCR1 寄存器实现。

TIM22->PSC = 31;                            //CK_CNT=Fck_psc/(PSC[15:0]+1), so 32MHz clock becomes 1MHz
TIM22->ARR = 20000;                        //Clock is 1MHz so period becomes 20ms
  
/*Pulse Width Calculation*/
TIM22->CCR1 = (TIM22->ARR)-Pulse_Duration;

第3步:更多设置

还需完成几项设置才能完成配置。本案例中我使用了输出比较模式1的PWM模式2。这将使通道1在向上计数时,只要TIMx_CNT<TIMx_CCR1 就保持无效状态。向上计数是默认设置。我还启用了输出比较1预装载功能。接着通过向TIMx_CCER 寄存器写入TIM_CCER_CC1E 位,将OC1设为高电平有效。最后通过向TIMx_CR1 寄存器写入TIM_CR1_CEN 位启用PWM,并通过向TIMx_EGR 写入TIM_EGR_UG 位重新初始化计数器和寄存器。

/*Select PWM 2 on OC1 and enable preload register*/
TIMx->CCMR1 |= TIM_CCMR1_OC1M_2|TIM_CCMR1_OC1M_1|TIM_CCMR1_OC1M_0
                               |TIM_CCMR1_OC1PE
#if PULSE_WITHOUT_DELAY > 0
                               |TIM_CCMR1_OC1FE
#endif
;
 
/*Select active high polarity on OC1*/
TIMx->CCER |= TIM_CCER_CC1E;
 
/*Enable PWM*/
TIMx->CR1 = TIM_CR1_CEN;
TIMx->EGR = TIM_EGR_UG;

第4步:选择 GPIO

最后一步是选择要使用的引脚。这涉及检查复用功能并配置GPIO。若未阅读《GPIO示例》《复用功能配置指南》章节,请立即查阅。下文展示了我用于PWM的GPIO配置代码。

/*Initialize GPIO*/
struct GPIO_Parameters GPIO;
GPIO.Pin     = Pin;
GPIO.Mode     = Alternate_Function;
GPIO.Speed = High_Speed;
GPIO.PuPd     = Pull_Down;
GPIO_Init(GPIOx,GPIO);

PWM 初始化代码

完整代码详见GitHub

PWM.c
void PWM(TIM_TypeDef* TIMx, int Pulse_Duration,GPIO_TypeDef* GPIOx, int Pin){
  
 /*Initialize GPIO*/
 struct GPIO_Parameters GPIO;
 GPIO.Pin     = Pin;
 GPIO.Mode     = Alternate_Function;
 GPIO.Speed = High_Speed;
 GPIO.PuPd     = Pull_Down;
 GPIO_Init(GPIOx,GPIO);
  
 /*Enable TIM clock*/
 if(TIMx == TIM22){
        RCC->APB2ENR |= RCC_APB2ENR_TIM22EN;
 }
 if(TIMx == TIM21){
     RCC->APB2ENR |= RCC_APB2ENR_TIM21EN;
 }
  
 TIMx->PSC = 31;                            //CK_CNT=Fck_psc/(PSC[15:0]+1), so 32MHz clock becomes 1MHz
 TIMx->ARR = 20000;                        //Clock is 1MHz so period becomes 20ms
  
 /*Pulse Width Calculation*/
 TIMx->CCR1 = (TIMx->ARR)-Pulse_Duration;
  
 /*Select PWM 2 on OC1 and enable preload register*/
 TIMx->CCMR1 |= TIM_CCMR1_OC1M_2|TIM_CCMR1_OC1M_1|TIM_CCMR1_OC1M_0
                                |TIM_CCMR1_OC1PE
 #if PULSE_WITHOUT_DELAY > 0
                                |TIM_CCMR1_OC1FE
 #endif
 ;
  
 /*Select active high polarity on OC1*/
 TIMx->CCER |= TIM_CCER_CC1E;
  
 /*Enable PWM*/
 TIMx->CR1 = TIM_CR1_CEN;
 TIMx->EGR = TIM_EGR_UG;
}

欢迎大家在GitHub上使用、修改并优化我的代码。