Lab5 : uC/OS室温计 - 快来再点个灯

实验环境为Window10 + Keil 5.18,代码中使用标准库。

实验准备

本次实验硬件上需要七段数码管LG3641BH,温湿度传感器DHT11,点灯用的小灯一个,面包板一块加导线若干。

由于手头公母线似乎不是很够用,我把整个STM32都插到了面包板上,然后发现这么搞好像也蛮科学的样子,整体连线清晰很多。

而软件则需要标准库的支持代码以及uCOS-II源码了。

标准库代码在Lab3的准备过程中已经载入Keil。

uCOS-II的源码可以在文末下载链接中下载。下载需要注册一个账号,顺着写就好了。吐槽一下,注册的时候Phone Number竟然不需要填数字……有反馈说有的邮箱可能收不到注册回执,我使用的是163邮箱进行注册。

Lab5 : uC/OS室温计 - 快来再点个灯_第1张图片
解压

由于这个uCOS-II源码包用的是标准库而不是HAL,顺势而为也就改成标准库了。虽然大同小异,不过函数名字换了一遍还是有点难受的。

本文没有使用STM32CubeMX。

实验步骤

0. 点灯

还是老步骤,拿到不会用的东西,先点个灯。

点灯程序

这次的点灯准备发挥uCOS-II的特长,一次性点两个灯。有的小灯比较脆弱,接的时候连个电阻比较保险。不然有可能烧坏。

直接新建Keil工程,选择好板子型号后进入运行环境选择页面。选择需要的环境后便进入了工程。

Lab5 : uC/OS室温计 - 快来再点个灯_第2张图片
Project Run-Time Environment

在工程内写入app.c文件即可。

#include "stm32f10x.h"
#include "stm32f10x_conf.h"

void GPIO_Configuration(void){
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_DeInit();
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOA, ENABLE);
    
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

void Delay_long(int times){
    unsigned int i, j;
    for (j=0; j

这份代码目前和uCOS-II一点关系都没有,仅仅是为了测试小灯以及电路的通畅,顺便再熟悉一下标准库的GPIO操作。

初始化的函数中初始化了两个GPIO口PA11以及PC13,分别对应接下来要点的两个灯,其中PA11口的灯是外接的,而PC13口的灯为板子自带。

在出了一些状况之后,两个小灯顺利得开始了闪烁。

uCOS-II工程创建

工程创建步骤基本完全按照参考资料中一步一步移植ucos到stm32f103开发版(修订版)所述步骤。当然,也别完全照着来,板子类型还是需要选自己的。同时,我用的是自己实验准备中下载的源码包,文中给出的那个并没有使用过。

修订主要是略微增加对keil功能的利用,提高对编译器功能的利用可以提高建工程的速度和减少定义冲突

先到官方下载ucos源码,比较接近的是micrium_stm32f103-sk_ucos-ii,本文就采用该文件

开发工具版本为MDK511

1.新建ucos工程,选择STM32F103VE,选择CMSIS下的CORE和Device下的Startup,以及Device下的StdPeriph Drivers下的Framework,RCC,和GPIO

2.工程中和实际目录中都新建几个目录,APP,UCOS,BSP,LIB,CPU,Output

3.工程上右键,Options,Output页签,Select Foldeer for Objects,进入Output目录,点击OK

4.把Micrium\Software\uCOS-II\Source目录中的文件拷贝到UCOS目录下,并添加到工程中

5.工程Options中,C/C++页签,Include Paths,点击后面省略号可选择include目录,添加UCOS路径

6.复制Micrium\Software\EvalBoards\ST\STM3210B-EVAL\RVMDK\OS-Probe目录下的文件app_cfg.h,os_cfg.h和includes.h到APP目录中,并在Include Paths中添加APP

7.复制Micrium\Software\uCOS-II\Ports\arm-cortex-m3\Generic\RealView目录下的所有文件到CPU目录,添加到工程和Include Path中

8.工程Options中,C/C++页签,Defines中添加 USE_STDPERIPH_DRIVER

9.把RTE和RTE\Device\STM32F103VE添加进Include Paths中

10.修改os_cfg.h文件,#define OS_APP_HOOKS_EN 1为0

11.BSP目录下新建BSP.c文件,添加内容如下:

#include 
CPU_INT32U  BSP_CPU_ClkFreq (void) {
    RCC_ClocksTypeDef  rcc_clocks;
    RCC_GetClocksFreq(&rcc_clocks);
    return ((CPU_INT32U)rcc_clocks.HCLK_Frequency);
}
INT32U  OS_CPU_SysTickClkFreq (void) {
    INT32U  freq;
    freq = BSP_CPU_ClkFreq();
    return (freq);
}

12.复制Micrium\Software\EvalBoards\ST\STM3210B-EVAL\RVMDK\BSP目录下的bsp.h到 BSP目录中

13.复制Micrium\Software\uC-CPU\ARM-Cortex-M3\RealView目录和Micrium\Software\uC-CPU目录下的所有文件到CPU目录下,并添加到工程和Include Path中

14.复制Micrium\Software\uC-LIB目录下的所有.h文件到LIB目录下,并添加到Include Path中

15.注释掉bsp.h中的#include 和#include

16.app_cfg.h文件中,修改为#define APP_OS_PROBE_EN 0

17.APP目录下新建app.c文件,内容为

#include 
int main(){
  OSInit();
  OSStart();
  return 0;
}

18.注释掉includes.h文件中的#include 和#include

至此,第一步准备工作完成,虽然未实现任何功能,至少编译不再报错了

按照上面说的教程步骤跑一遍之后的工程文件及部分配置如下。

Lab5 : uC/OS室温计 - 快来再点个灯_第3张图片
代码清单
Lab5 : uC/OS室温计 - 快来再点个灯_第4张图片
代码清单
Lab5 : uC/OS室温计 - 快来再点个灯_第5张图片
options - C/C++

但是此时可能还会有一些地方无法通过编译。然后七翻八翻找到这个位置,发现两个DEF_ENABLED都得置为0。

define置零

然后终于编译过了。

uCOS-II多任务点灯

小灯的闪烁实际上可以看成是两个独立的事件,即使用两个任务分别点灯即可。

void LED0_task(void* pdata){
    while(1){
        GPIO_WriteBit(GPIOA, GPIO_Pin_11, Bit_SET);
        Delay_long(5);
        GPIO_WriteBit(GPIOA, GPIO_Pin_11, Bit_RESET);
        Delay_long(5);
    }
}

void LED1_task(void* pdata){
    while(1){
        GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
        Delay_long(10);
        GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
        Delay_long(10);
    }
}

#define STK_Size 100
int LED0_Task_STK[STK_Size];
int LED1_Task_STK[STK_Size];
int Task_STK[STK_Size];

int main(){
    OS_CPU_SR cpu_sr=0;
    GPIO_Configuration();
    OSInit();
    OS_CPU_SysTickInit();
    OSTaskCreate(
        LED0_task,
        (void *)0,
        (OS_STK *)&LED0_Task_STK[STK_Size-1],
        1
    );
    OSTaskCreate(
        LED1_task,
        (void *)0,
        (OS_STK *)&LED1_Task_STK[STK_Size-1],
        2
    );
    OSStart();
    return 0;
}

uCOS-II中使用OSTaskCreate创建任务。该函数接收4个参数,第一个参数是任务执行的函数,第二个参数是附带的参数,只有1个指针的位置,第三个参数是该任务使用的栈的位置,第四个参数是任务的优先级。

而在执行了OSStart后,uCOS-II会对任务进行调度。

看起来一切正常,编译通过之后下板子本以为会看到灯闪烁起来的,然后程序就死掉了。

程序调错

经过单步调试,发现OSStart最终会进入os_cpu_a.asm的OSStartHighRdy函数中。在顺序执行之后,死在了OSStartHang上。

……
    CPSIE   I                      ; Enable interrupts at processor level

OSStartHang
    B       OSStartHang            ; Should never get here
……

感觉到了来自系统深深的恶意。不过肯定是哪里有问题。使用这个函数名进行百度之后发现——大家的程序原来都死在这了。

而在这个地方死循环的原因是uCOS-II所需的两个中断函数没有调用到。

所以,根据各种教程,把两个缺失的中断函数补上即可。两个中断为startup_stm32f10x_md.s中的PendSV Handler 以及 SysTick Handler。

有两种方法,第一种是自己写一个函数接收中断并向uCOS-II内核传递消息,第二种方法是直接使用uCOS-II自带的中断函数替换原本的函数。

void SysTick_Handler(void){
    OS_CPU_SR  cpu_sr;

    OS_ENTER_CRITICAL();    // Tell uC/OS-II that we are starting an ISR
    OSIntNesting++;
    OS_EXIT_CRITICAL();
    
    OSTimeTick();  // Call uC/OS-II's OSTimeTick()  
    OSIntExit();   // Tell uC/OS-II that we are leaving the ISR
}
Lab5 : uC/OS室温计 - 快来再点个灯_第6张图片
使用uCOS-II的OS_CPU_PendSVHandler替换中断函数PendSV_Handler

解决了中断问题之后,编译,下载…………然后我一脸懵逼的看到只有一盏灯在闪。

那还是有问题喽。_(:з」∠)_

经过百度,发现类似于while(i--);之类的延时函数在uCOS-II中是属于阻塞的延迟,即任务不会在这个时间点进行切换。而任务切换的方式需要调用uCOS-II中自带的延时函数。

此时,调整Delay_long函数即可。

void Delay_long(int times){
    /*unsigned int i, j;
    for (j=0; j

至此,点灯成功。

小灯各自闪烁中……

1. 七段数码管

我们用的型号为LG3641BH的七段数码管是一款共阳极的数码管。从LG3641BH Datasheet中可以找到该款元件的基本信息。

手动点亮数码管

而为了使七段数码管的四个数字同时亮起,使用时分复用的方式,高频率点亮每个数码管,使人眼看起来所有的数字都是同时亮起的。

咳咳,下面这俩ppt是逻辑课上的,所以别吐槽那个Spartan III。

Lab5 : uC/OS室温计 - 快来再点个灯_第7张图片
七段数码管显示原理
Lab5 : uC/OS室温计 - 快来再点个灯_第8张图片
七段数码管显示原理

据此可以写出对应的显示模块。电路各种花式连线均可,只要能和程序对应的上。以下是我的连法。

Lab5 : uC/OS室温计 - 快来再点个灯_第9张图片
引脚对应关系

12, 9, 8, 6引脚分别是从左到右4个数字的阳极,属于位选引脚,即输出高电平意为选择。这四个引脚分别对应stm32上的PA11, PA12, PC13, PC14。

而剩下的引脚为控制具体单段数码管的引脚。标号0到7的引脚分别与PA0至PA7相连。

据此,可以写出点亮七段数码管的程序。

void GPIO_Configuration(void) {
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_DeInit();
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOA, ENABLE);
    
    
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13
                                | GPIO_Pin_14;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
    
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11
                                | GPIO_Pin_12;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0
                                | GPIO_Pin_1
                                | GPIO_Pin_2
                                | GPIO_Pin_3
                                | GPIO_Pin_4
                                | GPIO_Pin_5
                                | GPIO_Pin_6
                                | GPIO_Pin_7;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
}

void Delay_ms(int times){
    OSTimeDly(OS_TICKS_PER_SEC / 1000 * times);
}

void digit_select(int index){
    // 通过输出高电平选择点亮某个数字
    BitAction v[4];
    int i;
    for (i=0; i<4; i++){
        if (index == i){
            v[i] = Bit_SET;
        }else{
            v[i] = Bit_RESET;
        }
    }
    GPIO_WriteBit(GPIOA, GPIO_Pin_11, v[0]);
    GPIO_WriteBit(GPIOA, GPIO_Pin_12, v[1]);
    GPIO_WriteBit(GPIOC, GPIO_Pin_13, v[2]);
    GPIO_WriteBit(GPIOC, GPIO_Pin_14, v[3]);
}

void digit_show(int dight, int point){
    // 通过对引脚输出低电平点亮数码管段
    int segment, i, base;
    BitAction v[8];
    switch (dight){
        case    0   :   segment = 0xee; break; // 0b11101110  0 -> 7
        case    1   :   segment = 0x24; break; // 0b00100100
        case    2   :   segment = 0xba; break; // 0b10111010
        case    3   :   segment = 0xb6; break; // 0b10110110
        case    4   :   segment = 0x74; break; // 0b01110100
        case    5   :   segment = 0xd6; break; // 0b11010110
        case    6   :   segment = 0xde; break; // 0b11011110
        case    7   :   segment = 0xa4; break; // 0b10100100
        case    8   :   segment = 0xfe; break; // 0b11111110
        case    9   :   segment = 0xf6; break; // 0b11110110
        default     :   segment = 0xda; break; // 0b11011010 error state
    }
    segment |= point != 0; // 小数点为最低位
    base = 1 << 8;
    for (i=0; i<8; i++){
        base >>= 1;
        // segment中某位的1表示点亮,而输出低电平为点亮
        if ((segment & base )== 0){
            v[i] = Bit_SET;
        }else{
            v[i] = Bit_RESET;
        }
    }
    GPIO_WriteBit(GPIOA, GPIO_Pin_0, v[0]);
    GPIO_WriteBit(GPIOA, GPIO_Pin_1, v[1]);
    GPIO_WriteBit(GPIOA, GPIO_Pin_2, v[2]);
    GPIO_WriteBit(GPIOA, GPIO_Pin_3, v[3]);
    GPIO_WriteBit(GPIOA, GPIO_Pin_4, v[4]);
    GPIO_WriteBit(GPIOA, GPIO_Pin_5, v[5]);
    GPIO_WriteBit(GPIOA, GPIO_Pin_6, v[6]);
    GPIO_WriteBit(GPIOA, GPIO_Pin_7, v[7]);
}

void led_show(int digit){
    // 时分复用的方式输出数字,每次调用led_show只输出一位数字
    static int index = -1;
    int i;
    int base = 1000;
    index = (index + 1) % 4;
    for (i=0; i

程序使用两个task完成全部功能。首先是LED0_task,该任务每隔200ms修改一次ledValue的值,即修改需要显示的值。而LED1_task每次显示一位ledValue的值。

Lab5 : uC/OS室温计 - 快来再点个灯_第10张图片
程序运行结果

2. DHT11数据读取及显示

DHT11是一款有已校准数字信号输出的温湿度传感器。作为一种单总线设备,输入输出均为同一个引脚。

使用DHT11的时候,需要连接三个引脚,引脚1接VCC,引脚2接是stm32的某个GPIO口,引脚4接地。而引脚3悬空。

对DHT11来说,数据的传输步骤如下:

  1. stm32输出低电平至少18ms(只有此处为ms,其余均为μs)。
  2. stm32输出高电平20~40μs
  3. DHT11反馈低电平80μs
  4. DHT11反馈高电平80μs
  5. 以上为双方握手,以下开始准备接受数据。数据总长40个bit,输入为大端输入,即高位的bit先进行传输。每个byte表示一个数值。按照接受顺序分别表示湿度整数,湿度小数,温度整数,温度小数,校验码。校验码为前方四个byte的和。
  6. 对于每个传输的bit,DHT11会首先输出50μs的低电平
  7. 而后以输出高电平的时间决定每个bit的值。高电平持续时间为20~30μs的为bit 0,高电平持续时间为70μs的表示bit 1。

整个通信过程大约耗时4ms。

Lab5 : uC/OS室温计 - 快来再点个灯_第11张图片
图来自[Stm32程序控制DHT11](http://wenku.baidu.com/link?url=q45Du2opi0SljsMJGpJYJ8zrlNIjyEeLyYH-1aIrJG-s2PQAVEBpfGYKAtw5ChELNGy4s5YAIGhA470sRDyTWnZm6XwpKFWS2heoAJHjmWi)

根据传输协议写成程序即可。

#define MAX_TICS 100000
#define DHT11_OK 0
#define DHT11_NO_CONN 1
#define DHT11_CS_ERROR 2
#define DHT11_PORT GPIOB
#define DHT11_PIN GPIO_Pin_0

void Delay_us(int times){
    // 我自己测大约这个delay函数需要的时间是1.4us * @times
    unsigned int i;
    for (i=0; iIDR & DHT11_PIN)> 0;
    //return GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN);
}

void DHT11_Wait(int state, int place){
    // 等待GPIO口变为state的值,超时自动自杀
    int loopCnt = MAX_TICS;
    while (DHT11_Check() != state){
        if (loopCnt -- == 0){
            ErrorState(1000 + state * 1000 + place);
        }
    }
}

void DHT11_Rst(){
    // stm32端输出握手信号
    DHT11_Pin_OUT();
    DHT11_Set(0);
    Delay_us(25000);
    DHT11_Set(1);
    Delay_us(40);
    DHT11_Set(0);
    // 转为接收模式准备读入DHT11的握手信号
    DHT11_Pin_IN();
}

int val = 10;

uint8_t DHT11_Read_Byte(){
    // 读入一个Byte
    int i, cnt;
    uint8_t data = 0;
    for (i=0; i<8; i++){
        cnt = 0;
        data <<= 1;
        // 当前为低电平,等待高电平
        DHT11_Wait(1, ++val);
        
        // 计算高电平持续的时间
        while (DHT11_Check() > 0){
            Delay_us(1);
            cnt++;
        }
        // 持续的足够久则为bit 1
        data |= cnt > 5;
    }
    return data;
}

uint8_t DHT11_Read_Data(uint8_t *buf){
    // 从DHT11内读取数据的函数
    int i;
    unsigned int cpu_sr;
    // 为了关闭中断进入临界区
    OS_ENTER_CRITICAL();
    val = 10;
    // 发送握手消息
    DHT11_Rst();
    // 如果给予了回复
    if (DHT11_Check() == 0){
        // 等待低电平过去
        DHT11_Wait(1, 2);
        // 等待高电平过去
        DHT11_Wait(0, 3);
        // 握手完成,开始读取40个bit
        for (i=0; i<5; i++){
            buf[i] = DHT11_Read_Byte();
        }
        
        // 重新将GPIO口置为输出模式
        DHT11_Pin_OUT();
        OS_EXIT_CRITICAL(); 
        
        // 判断校验和是否满足要求
        if (buf[0] + buf[1] + buf[2] + buf[3] == buf[4]){
            return DHT11_OK;
        }else{
            return DHT11_CS_ERROR;
        }
    }else{
        // 该分支表示没有收到回复
        OS_EXIT_CRITICAL(); 
        return DHT11_NO_CONN;
    }
}

uint8_t DHT11_Humidity(uint8_t *buf){
    // 返回湿度
    return buf[0];
}

uint8_t DHT11_Temperature(uint8_t *buf){
    // 返回温度
    return buf[2];
}

值得一提的是,上述代码在出现异常情况的时候会直接进入ErrorState自杀,此时会在连入的LED上显示错误号,根据错误号判断进入ErrorState的位置进行debug即可。

在实践过程中,如果DHT11不听话,可以尝试/多次尝试/循环尝试/随机尝试/组合尝试以下几种方法:

  1. 换几根线
  2. 换一个DHT11
  3. 换一个STM32的引脚
  4. 换一块面包板
  5. 玄学调参
  6. 换一个程序
  7. 换一个STM32
  8. 去吃个饭
  9. 去西湖散散心
  10. 换一个实验

程序内使用时直接调用DHT11_Read_Data函数进行读取即可。

void LED0_task(void* pdata){
    uint8_t buf[5];
    int state;
    memset(buf, 0, sizeof(buf));
    
    while (1){
        state = DHT11_Read_Data(buf);
        switch(state){
            case DHT11_CS_ERROR:
                ledValue = 9002;
                break;
            case DHT11_NO_CONN:
                ledValue = 9001;
                break;
            case DHT11_OK:
                ledValue = DHT11_Temperature(buf);
                break;
        }
        Delay_ms(1000);
    }
}
正常显示
使用电吹风后

参考资料:

  • 一步一步移植ucos到stm32f103开发版(修订版)
  • 移植ucosii遇到的问题 B OSStartHang
  • 【教程贴】【原创】stm32 的ucosii移植全过程 详细教程
  • 学ucos的心得
  • ucos ii的任务在何时切换
  • LG3641BH Datasheet
  • Stm32程序控制DHT11
  • Arduino教程——DHT11数字温湿度传感器(Ⅱ)
  • Github: Humidity-stm32-I / dht11.c

下载链接:

  • Download “Micrium_STM32xxx_uCOS-II”

你可能感兴趣的:(Lab5 : uC/OS室温计 - 快来再点个灯)