STM32开发入门系列之七:SPI通信

SPI通信是一种高效的同步通信协议,通过4根引脚,实现了数据的高速双向通信。这4根针分工特别简单,简要说明一下:

  • NSS,使能信号,可定义高电平使能或者低电平使能
  • SCK,时钟信号,由主机发起。上升沿之前,主机设置好需要写给从机的值,供从机在高电平的时候读取;下降沿之前,从机设置好需要写给主机的值,供主机在下降沿读取。
  • MISO,主机输入,从机输出引脚。(M:master 主机,S:slaver 从机)
  • MOSI,主机输出,从机输入引脚。

根据STM32的引脚定义手册,我们可以清晰地看到PA4,PA5,PA6,PA7分别对应上面的NSSS,SCK,MISO,MOSI,截图如下:

下面是我自己用的STM32作为下位机使用SPI跟上位机通信的DEMO,秉承一贯的风格,注释详细到令人发指,就不多解释了。

#include "stm32f10x.h"

volatile u16 data[16];
volatile u8 index = 0;
volatile u8 flag  = 0;  //通知主函数处理IO事件的信号量,1:需要处理,0:不需要处理

volatile u32 time = 0;  //SPI信号中的时间偏移量,过大的话,认为是第二组SPI数据

void SPIInit(void) {
    /* 中断向量配置 */
    NVIC_InitTypeDef nvicInitTypeDef;
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

    nvicInitTypeDef.NVIC_IRQChannel = SPI1_IRQn;
    nvicInitTypeDef.NVIC_IRQChannelPreemptionPriority = 0;
    nvicInitTypeDef.NVIC_IRQChannelSubPriority = 2;
    nvicInitTypeDef.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&nvicInitTypeDef);

    /* GPIO 设置 */

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1 | RCC_APB2Periph_AFIO, ENABLE);

    GPIO_InitTypeDef  GPIO_InitStructure;

    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_Init(GPIOA , &GPIO_InitStructure);

    /*
     *  SPI 管脚配置
     *  PA4 ---- NSS
     *  PA5 ---- SCK
     *  PA6 ---- MISO
     *  PA7 ---- MOSI
     */
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA , &GPIO_InitStructure);

    /* spi config */
    SPI_InitTypeDef spiInitStructure;

    /* spi 初始化定义 */
    spiInitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双线读写
    spiInitStructure.SPI_Mode      = SPI_Mode_Slave;                  //从模式
    spiInitStructure.SPI_DataSize  = SPI_DataSize_8b;                 //8位每帧
    spiInitStructure.SPI_CPOL      = SPI_CPOL_Low;                    //时钟空闲为低
    spiInitStructure.SPI_CPHA      = SPI_CPHA_1Edge;                  //数据捕获于第一个时钟沿
    spiInitStructure.SPI_NSS       = SPI_NSS_Hard;                    //硬件控制NSS信号
    spiInitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //波特率预分频
    spiInitStructure.SPI_FirstBit  = SPI_FirstBit_MSB;                //数据传输从MSB位开始
    spiInitStructure.SPI_CRCPolynomial = 7;                           //CRC设置,不启用
    SPI_Init(SPI1, &spiInitStructure);

    SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_RXNE, ENABLE);                  //使能接收中断

    SPI_Cmd(SPI1, ENABLE);                                            //使能SPI1
}

/* SPI 中断控制函数 */
void SPI1_IRQHandler(void) {
    if(SPI_I2S_GetITStatus(SPI1, SPI_I2S_IT_RXNE) == SET){
        u16 buff = 0;
        buff = SPI_I2S_ReceiveData(SPI1);
        if(time > 9000 || index == 0) {
            //时间过大或者index本身为0,认为新数据
            index = 0;
            if(buff == 0xAA){
                //包头            
                data[index] = buff;
                index++;
            }            
        }else if(index == 10){
            //checksum校验

            //校验通过的话,通知主函数处理IO
            index = 0;
            flag  = 1;
        }else if(index < 9 && index >= 1){
            //IO数据中的字节
            data[index] = buff;
            index++;
        }
    }
    time = 0;
}

int main(void) {
    SystemInit();

    index = 0;
    flag  = 0;

    SPIInit();
    GPIO_SetBits(GPIOA, GPIO_Pin_0);

    while(1) {
        time++;
        if(flag){
            flag = 0;
            //处理IO数据
            if(data[1] & 0x0080)
                GPIO_SetBits(GPIOA, GPIO_Pin_0);
            else
                GPIO_ResetBits(GPIOA, GPIO_Pin_0);
        }
    }
}

这个系列写到第七章,STM32相关的知识也就差不多了,以后我接触到更好的知识点,在自己弄明白的基础上,再陆陆续续更新这个系列。

STM32开发入门系列之六:外部中断

STM32几乎每一个GPIO都可以用来做为外部中断使用,但是按照GPIO引脚名称的数字编号进行分组,每组GPIO只能用一个作为外部中断。比如PA0, PB0,PD0…PG0,这一组共6个GPIO,但只可以选择其中一个作为外部中断。你可以同时使用PA0,PB1,PC2,PD3…这些引脚作为外部中断。

下面就是stm32使用PD2作为下降沿外部中断的示例:

exinterrupt.h

/**
 * 初始化外部中断
 */
#include "stm32f10x.h"

void EXTIX_Init(void);

/* 使用PD2,对应中断是EXTI2,定义引脚 */
#define EXTI2_IOU_Line GPIO_PortSourceGPIOD
#define EXTI2_IOP_Line GPIO_PinSource2
#define EXTI2_IOU_RCC  RCC_APB2Periph_GPIOD
#define EXTI2_IOP_RCC  GPIO_Pin_2
#define EXTI2_IOU_GPIO GPIOD

exinterrupt.c

#include "exinterrupt.h"

/**
 * 整个系统的中断向量表:外部中断 > UART中断 > RTC中断
 * 外部中断优先级最高,可以在任何时间嵌套进其他中断执行
 * 使用Group1,抢占优先级1,响应优先级最高7(三位)
 */
void EXTI2_NVIC_Configuration(void) {
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

    NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x07;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

/**
 * 设置外部中断2引脚位EXTI2_IOU EXTI2_IOP
 * 触发方式下降沿中断
 */
void EXTIX_Init(void) {
    /* GPIO初始化 */
    RCC_APB2PeriphClockCmd(EXTI2_IOU_RCC, ENABLE);

    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin  = EXTI2_IOP_RCC;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;             /* 上拉输入 */
    GPIO_Init(EXTI2_IOU_GPIO, &GPIO_InitStructure);

    /* 中断结构初始化 */
    GPIO_EXTILineConfig(EXTI2_IOU_Line, EXTI2_IOP_Line);
    EXTI_InitTypeDef EXTI_InitStructure;
    EXTI_InitStructure.EXTI_Line = EXTI_Line2;                /* 外部中断2 */
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;       /* 中断模式,另一种模式是事件模式 */
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;   /* 下降沿 */
    EXTI_Init(&EXTI_InitStructure);

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);

    /* 中断向量初始化 */
    EXTI2_NVIC_Configuration();
}

main.c

/* 外部中断2服务程序,最后释放中断标志,让下次中断可以进来 */
void EXTI2_IRQHandler(void){    
    //...your logic code

    EXTI_ClearITPendingBit(EXTI_Line2);     //清除LINE2上的中断标志位  
}

说实话,STM32的外部中断还是相当清晰的。代码的注释很足,不再赘述。下一节我们来介绍一些STM32中的SPI通信。

STM32开发入门系列之五:RTC时钟

RTC,实时时钟(Real-Time Clock),用来让嵌入式的产品获取当前时分秒,年月日的功能,具备主控芯片断电后继续跑时的功能,一般情况下都需要后备电池,以及独立的32.768K晶振的支撑。

在很多嵌入式应用中,都需要RTC的支撑,比如万年历,比如定时锁机等等,现在我们就来一起学习和巩固一下STM32上使用RTC的方法。

首先是硬件连接,PC14和PC15两个引脚需要占用,作为外部32.768K晶振的引脚。注意这两个管脚接到晶振上时线路应该尽可能短,更不可以将引脚通过排线暴露出来,否则相当于天线,极易受到干扰,导致RTC走时严重不准。网上很多帖子说STM32的RTC不准,大致可以归纳为三种情况: 1. 自身代码问题;2. 晶振选择错误; 3. 晶振与PC14,PC15连接的线路过长,导致干扰严重。注意这三点后,内部RTC时钟是完全能够取代DS1302或者8025T这类的外部时钟芯片,丝毫不逊色。

如果还需要基于时钟做一个定时中断出来,比如每隔5分钟执行一段状态检测逻辑,除了使用之前介绍过的滴答定时器以外,还可以开启RTC中断,也是非常方便的,只要对nvic简单的配置即可。

#include <time.h>

void RTC_Configuration(void);
void RTC_NVIC_Configuration(void);

/* 秒中断标志,进入秒中断时置1,当时间被刷新之后清0 */
__IO uint32_t TimeDisplay;

/**
 * 整个系统的中断向量表:外部中断 > UART中断 > RTC中断
 * RTC秒中断优先级最低
 * 使用Group1,抢占优先级0,响应优先级0
 */
void RTC_NVIC_Configuration(void) {
    NVIC_InitTypeDef NVIC_InitStructure;

    /* Configure one bit for preemption priority */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

    /* Enable the RTC Interrupt */
    NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

/* 暴露给外部的RTC初始化函数 */
void RTC_Init(void) {
    /* Enable PWR and BKP clocks */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);

    /* Allow access to BKP Domain */
    PWR_BackupAccessCmd(ENABLE);

    RTC_NVIC_Configuration();

    if (BKP_ReadBackupRegister(BKP_DR2) != 0xA6A6) {
        /* RTC Configuration */
        RTC_Configuration();

        BKP_WriteBackupRegister(BKP_DR2, 0xA6A6);
    } else {
        /* Wait for RTC registers synchronization */
        RTC_WaitForSynchro();

        /* Enable the RTC Second */
        RTC_ITConfig(RTC_IT_SEC, ENABLE);

        /* Wait until last write operation on RTC registers has finished */
        RTC_WaitForLastTask();
    }

    RCC_ClearFlag();

    /* Allow access to BKP Domain */
    PWR_BackupAccessCmd(DISABLE);
}

/**
 * RTC配置
 */
void RTC_Configuration(void) {
    /* Enable PWR and BKP clocks */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);

    /* Allow access to BKP Domain */
    PWR_BackupAccessCmd(ENABLE);

    /* Reset Backup Domain */
    BKP_DeInit();

    /* Enable LSE */
    RCC_LSEConfig(RCC_LSE_ON);

    /* Wait till LSE is ready */
    while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);

    /* Select LSE as RTC Clock Source */
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);

    /* Enable RTC Clock */
    RCC_RTCCLKCmd(ENABLE);

    /* Wait for RTC registers synchronization */
    RTC_WaitForSynchro();

    /* Wait until last write operation on RTC registers has finished */
    RTC_WaitForLastTask();

    /* Enable the RTC Second */
    RTC_ITConfig(RTC_IT_SEC, ENABLE);

    /* Wait until last write operation on RTC registers has finished */
    RTC_WaitForLastTask();

    /* Set RTC prescaler: set RTC period to 1sec */
    RTC_SetPrescaler(32767); /* RTC period = RTCCLK/RTC_PR = (32.768 KHz)/(32767+1) */

    /* Wait until last write operation on RTC registers has finished */
    RTC_WaitForLastTask();

    /* 设置初始时间 */
    Date initTime;
    initTime.tm_year = 2017 - 1900;    /* years since 1900 */
    initTime.tm_mon  = 4;
    initTime.tm_mday = 26;
    initTime.tm_hour = 14;
    initTime.tm_min  = 16;
    initTime.tm_sec  = 0;

    RTC_SetCounter(mktime(&initTime));
    RTC_WaitForLastTask();
}

/* 修改RTC时间 */
void ChangeRTCDateTime(Date changeDate){
    /* Enable PWR and BKP clocks */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);

    /* Allow access to BKP Domain */
    PWR_BackupAccessCmd(ENABLE);

    RTC_WaitForLastTask();
    RTC_SetCounter(mktime(&changeDate));
    RTC_WaitForLastTask();

    PWR_BackupAccessCmd(DISABLE);
}

/**
 * 获取当前日期
 * @return  [6位整数,当前日期,yymmdd]
 */
u32  GetToday(void){
    u32 theTime = RTC_GetCounter();
    Date *theTimeDate = localtime(&theTime);

    return (theTimeDate->tm_year % 100) * 10000 + theTimeDate->tm_mon * 100 + theTimeDate->tm_mday;
}

/**
 * 获取当前时间
 * @return  [6位整数,当前时间,hhmmss]
 */
u32  GetNow(void){
    u32 theTime = RTC_GetCounter();
    Date *theTimeDate = localtime(&theTime);

    return theTimeDate->tm_hour * 10000 + theTimeDate->tm_min * 100 + theTimeDate->tm_sec;
}

/* RTC 秒中断 */
void RTC_IRQHandler(void) {
    if (RTC_GetITStatus(RTC_IT_SEC) != RESET) {
        /* Clear the RTC Second interrupt */
        RTC_ClearITPendingBit(RTC_IT_SEC);

        /* Customer Code */
        FiveMiniteCounter++;
        if(FiveMiniteCounter >= 300){
            FiveMiniteCounter = 0;
            FiveMiniteFunction(NO_NEED_WRITE);
        }
    }
}

代码注释很详细,其中BKP功能是为了不要每次启动STM32都重置时间,而是在后备存储区写一个特定的数值,这个数值由后备电源供电维护,一旦断开电源,这个数值就被抹去了,开机检测这个数据,从而判断是否需要更新RTC时间。另外,值得一提的是time.h文件提供的时间处理功能,localtime函数将一个从1900年1月1日距离当前时间相差的长整型数据,转换成一个年月日,时分秒的结构体Date;同时,mktime函数,又可以做为localtime的逆运算,将一个Date类型的结构体,转化成RTC需要的长整型秒数。

以上,就是RTC的用法,应为代码中注释非常丰满,实在不想用文字在赘述太多。如果读者您在使用STM32的RTC时,遇到困难,可以联系我,微信168138332。

下一章我们来学习一下STM32中的外部中断。

丁丁生于 1987.07.01 ,30岁,英文ID:newflydd

  • 现居住地 江苏 ● 泰州 ● 姜堰
  • 创建了 Jblog 开源博客系统
  • 坚持十余年的 独立博客 作者
  • 大学本科毕业后就职于 中国电信江苏泰州分公司,前两年从事Oracle数据库DBA工作,两年后公司精简技术人员,被安排到农村担任支局长(其本质是搞销售),于2016年因志向不合从国企辞职,在小城镇找了一份程序员的工作。
  • Git OSChina 上积极参与开源社区