STM32开发入门系列之四:UART通信及NVIC_InitStructure中断向量详解

UART通信属于入门嵌入式的必学通信方式之一,为什么?因为UART一般作为早期输入输出、调试、事件响应的重要手段。

在51单片机上使用UART相对来说比较简单:设置一下定时器T1使其实现固定波特率,使能串口中断,使用interrupt关键字定义中断函数就可以了,此处不表。STM32的串口通信就显得有点麻烦,当然也不是烧脑的那种困难,而是步骤有些繁琐,因为有一个重要的中断向量的概念,就是我们在他人代码里,初始化阶段经常会看到的NVIC_InitStructure配置。

先来讲讲NVIC_InitStructure配置到底是干什么的,它就是中断优先级的概念,中断优先级分为响应优先级和抢占优先级:

  • 响应优先级:同时到达响应谁;
  • 抢占优先级:响应B的过程中是否允许响应A;

在51中,响应优先级一般固定了:外部中断0 > 定时器中断0 > 外部中断1 > 定时器中断1 > 串口中断;而抢占优先级是通过IP寄存器来设置的,这里就不展开讲了。

对于STM32来说,配置中断的响应优先级和抢占优先级更加灵活(繁琐,捂脸,逃…),这种灵活但繁琐的特性我们在第二节配置GPIO的过程中就见识过了,那现在我们就正式来会一会STM32配置UART的流程吧。

/**
 * 整个系统的中断向量表:外部中断 > UART中断 > RTC中断
 * UART中断次高,但是UART中断不允许抢占RTC中断
 * 使用Group1,抢占优先级0,响应优先级1
 */
void UART_NVIC_Configuration(void) {
    NVIC_InitTypeDef NVIC_InitStructure;
    /* UART中断使用抢占式 */
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);     

    /* 使能 USART1 中断 */
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

/*
 * 函数名:USART1_Config
 * 描述  :USART1 GPIO 配置,工作模式配置。115200 8-N-1
 * 输入  :无
 * 输出  : 无
 * 调用  :外部调用
 */
void USART1_Init(void) {
    //配置中断向量函数
    UART_NVIC_Configuration();

    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;

    /* config USART1 clock */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);

    /* USART1 GPIO config */
    /* Configure USART1 Tx (PA.09) as alternate function push-pull */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    /* Configure USART1 Rx (PA.10) as input floating */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* USART1 mode config */
    USART_InitStructure.USART_BaudRate = 9600;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No ;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Rx;        /* 只接收,不需要发送 */
    /**
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;      //需要发送和接收
    */
    USART_Init(USART1, &USART_InitStructure);

    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);        /* 使能接受中断 */
    USART_ITConfig(USART1, USART_IT_TXE, DISABLE);        /* 失能发送中断 */
    USART_Cmd(USART1, ENABLE);                            /* 串口使能 */
}

/* 串口中断,系统会自动调用此函数,函数名固定。这里只接收数据 */
void USART1_IRQHandler(void) {
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { //如果寄存器中有数据
        /* Read one byte from the receive data register */
        /* 接收到UART数据,执行客户代码分析解码 */
    }
}

/**
 * 以下所有函数实现了 USART1_printf 功能,将数据输出打印到串口
 * 如果只需实现串口中断响应输入,而无需输出,可不必使用
 */

/*
 * 函数名:fputc
 * 描述  :重定向c库函数printf到USART1
 * 输入  :无
 * 输出  :无
 * 调用  :由printf调用
 */
int fputc(int ch, FILE *f)
{
    /* 将Printf内容发往串口 */
    USART_SendData(USART1, (unsigned char) ch);
    while (!(USART1->SR & USART_FLAG_TXE));

    return (ch);
}

/*
 * 函数名:itoa
 * 描述  :将整形数据转换成字符串
 * 输入  :-radix =10 表示10进制,其他结果为0
 *         -value 要转换的整形数
 *         -buf 转换后的字符串
 *         -radix = 10
 * 输出  :无
 * 返回  :无
 * 调用  :被USART1_printf()调用
 */
static char *itoa(int value, char *string, int radix)
{
    int     i, d;
    int     flag = 0;
    char    *ptr = string;

    /* This implementation only works for decimal numbers. */
    if (radix != 10)
    {
        *ptr = 0;
        return string;
    }

    if (!value)
    {
        *ptr++ = 0x30;
        *ptr = 0;
        return string;
    }

    /* if this is a negative value insert the minus sign. */
    if (value < 0)
    {
        *ptr++ = '-';

        /* Make the value positive. */
        value *= -1;
    }

    for (i = 10000; i > 0; i /= 10)
    {
        d = value / i;

        if (d || flag)
        {
            *ptr++ = (char)(d + 0x30);
            value -= (d * i);
            flag = 1;
        }
    }

    /* Null terminate the string. */
    *ptr = 0;

    return string;

} /* NCL_Itoa */

/*
 * 函数名:USART1_printf
 * 描述  :格式化输出,类似于C库中的printf,但这里没有用到C库
 * 输入  :-USARTx 串口通道,这里只用到了串口1,即USART1
 *           -Data   要发送到串口的内容的指针
 *             -...    其他参数
 * 输出  :无
 * 返回  :无 
 * 调用  :外部调用
 *         典型应用USART1_printf( USART1, "\r\n this is a demo \r\n" );
 *                   USART1_printf( USART1, "\r\n %d \r\n", i );
 *                   USART1_printf( USART1, "\r\n %s \r\n", j );
 */
void USART1_printf(USART_TypeDef* USARTx, uint8_t *Data,...)
{
    const char *s;
    int d;   
    char buf[16];

    va_list ap;
    va_start(ap, Data);

    while ( *Data != 0)     // 判断是否到达字符串结束符
    {                                         
        if ( *Data == 0x5c )  //'\'
    {                                     
    switch ( *++Data )
    {
        case 'r':                                     //回车符
            USART_SendData(USARTx, 0x0d);
            Data ++;
        break;

        case 'n':                                     //换行符
            USART_SendData(USARTx, 0x0a);   
            Data ++;
        break;

        default:
            Data ++;
        break;
    }            
    }
    else if ( *Data == '%')
    {                                     //
    switch ( *++Data )
    {               
        case 's':                                         //字符串
            s = va_arg(ap, const char *);
    for ( ; *s; s++) 
    {
        USART_SendData(USARTx,*s);
        while( USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET );
    }
        Data++;
        break;

    case 'd':                                       //十进制
    d = va_arg(ap, int);
    itoa(d, buf, 10);
    for (s = buf; *s; s++) 
    {
        USART_SendData(USARTx,*s);
        while( USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET );
    }
    Data++;
    break;
         default:
                Data++;
            break;
    }        
    } /* end of else if */
    else USART_SendData(USARTx, *Data++);
    while( USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET );
    }
}
/****************************/

关于上述代码,注释已经非常详细了,但是关于NVIC_InitStructure的一些基础知识还是得再讲清楚些。

一般我们在编写STM32程序时,需要先从整体上分析一下,我们需要使用到几个中断、优先级怎样、中断之间是否允许嵌套、嵌套关系怎样的。
比如我现在的一个DEMO程序,一共使用了3种中断,分别是外部中断、串口中断、RTC时钟中断(每隔一段时间触发状态检测),我希望将它们的响应优先级配置成外部中断 > 串口中断 > RTC时钟中断,并且,外部中断可以嵌入串口中断和RTC时钟中断执行。因此我需要2个抢占优先级实现嵌套(优先级1 > 优先级0,配置外部中断为抢占优先级1,串口和RTC中断为抢占优先级0)。

  • 抢占优先级1:外部中断(在下面两种中断执行过程中可以立即响应,而不必等下面两个中断执行完毕,优先级最高,立即响应)
  • 抢占优先级0:串口中断、RTC时钟中断(它们之间不可以互相互相嵌套,在其他中断执行过程中触发时,必须等待其他中断执行完毕)

对于NVIC_InitStructure来说,我这里仅仅需要2个抢占优先级,因此可以使用Group1,Group0-4是一个4bit寄存器,这4个bit分配了抢占优先级和响应优先级,对于所有Group,有如下分配定义:

  • 第0组:寄存器所有4位用于指定响应优先级(16种)该组所有中断无法嵌套抢占
  • 第1组:寄存器最高1位用于指定抢占式优先级,最低3位用于指定响应优先级(8种)
  • 第2组:寄存器最高2位用于指定抢占式优先级,最低2位用于指定响应优先级(4种)
  • 第3组:寄存器最高3位用于指定抢占式优先级,最低1位用于指定响应优先级(2种)
  • 第4组:寄存器所有4位用于指定抢占式优先级

根据我们DEMO所需要的场景,这里我们选择使用Group1,1位抢占,3位响应。外部中断配置在抢占优先级1,响应优先级7上;串口中断配置在抢占优先级0,响应优先级1上;RTC时钟中断配置在抢占优先级0,响应优先级0上。PS:对于整个项目,建议规划好使用同一组Group,如果项目非常复杂,中断非常多导致一个Group不够用的情况发生,还需要注意,Gropu本身还是有优先级的:Group0 > Group1 > Group2 > Group3 > Group4。 一个Group不够用时可以再仔细规划一下,分配多个Group使用,但那种情况太极端了,适用的场景也太复杂了,一般不会碰到的。

具体的定义方法,上面的代码和注释都写得比较详细,这里就不再赘述了。

这一节提到的知识点比较多,UART本身的配置并不算非常复杂,复杂的是中断的管理。并且本节提到了另外两个中断,RTC时钟中断和外部中断,下一节,我们来认识一下RTC实时时钟功能,这个神器内置在STM32中几乎取代了DS1302外部时钟芯片,是一个非常方便并且节省硬件成本的重要功能。

STM32开发入门系列之三:滴答定时器SysTick的使用

上一篇博客介绍了STM32中 如何使用GPIO,用LED做了一个闪烁的小灯,其中延时函数使用了步进延时:

delay = 65535;
while(delay)
    delay--;

这一章我们来认识一下STM32特有的、非常简单易用的滴答定时器,来制作一个精确可控的延时函数。

滴答定时器,也叫SysTick,是一个隐藏在STM32固件中专为操作系统提供定时功能的定时器,使用它并不会影响片内正常的定时器T0,T1这些资源,在没有使用操作系统的情况下,相当于STM32附带为你赠送了一个额外的定时资源。

下面我们来看一下SysTick的初始化和使用方法:

void SysTickInit(void) {
    /**
     * 对于72M的Clock 1ms一次中断,计算方法:参数值 = 系统时钟频率 / 每秒期望触发的次数
     * 举例:系统时钟频率72M,期望每秒触发1000次,也就是1ms一个周期
     * 则参数 = 72,000,000 / 1000 = 72000
     */
    if (SysTick_Config(72000)) { 
        while (1);               // 如果设置不成功,假死
    }

    // 关闭滴答定时器
    SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk;
}

//延时函数
void delay(__IO u32 nTime) {
    sysTimeDelay = nTime;

    // 使能滴答定时器,开始每1ms一次中断,系统将自动调用SysTick_Handler函数
    SysTick->CTRL |=  SysTick_CTRL_ENABLE_Msk;

    //等待nTime次的中断,将全局变量sysTimeDelay降至0
    while(sysTimeDelay != 0);
}

//中断中调用的降值函数
void SysTimeDelayDecrement(void) {
    if(sysTimeDelay != 0x00) {
        sysTimeDelay--;
    }
}

//中断函数
void SysTick_Handler(void) {
    // Customer Code
    SysTimeDelayDecrement();
}

int main(void){
    SysTickInit();

    //do something...

    delay(500);        //延时500毫秒

    //do something...
}

上面的代码注释写得很详细,就不需要在文字上赘述了。

下一章我们来认识一下STM32中如何进行UART通信。

ISSUE:Keil5新建STM32项目,编译时报错

  • “stm32f10x_conf.h”: No such file or directory

    C:\Keil_v5\ARM\PACK\Keil\STM32F1xx_DFP\2.1.0\Device\Include\stm32f10x.h(8302): error: #5: cannot open source input file “stm32f10x_conf.h”: No such file or directory

  • Could not open file .\objects\demo.o

    .\Objects\demo.axf: error: L6002U: Could not open file .\objects\demo.o: No such file or directory

  • no section to be FIRST/LAST.

    .\Objects\demo.sct(7): error: L6236E: No section matches selector - no section to be FIRST/LAST.

  • Cannot find argument ‘Reset_Handler’.

    .\Objects\demo.axf: Error: L6320W: Ignoring —entry command. Cannot find argument ‘Reset_Handler’.

以上所有的问题,都是因为新建项目时选择组件库时忘记添加Device下的Startup组件了,如图:
Keil5中添加Startup组件库

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

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