实验环境为Window10 + Keil 5.18,代码中使用标准库。
实验准备
本次实验硬件上需要七段数码管LG3641BH,温湿度传感器DHT11,点灯用的小灯一个,面包板一块加导线若干。
由于手头公母线似乎不是很够用,我把整个STM32都插到了面包板上,然后发现这么搞好像也蛮科学的样子,整体连线清晰很多。
而软件则需要标准库的支持代码以及uCOS-II源码了。
标准库代码在Lab3的准备过程中已经载入Keil。
uCOS-II的源码可以在文末下载链接中下载。下载需要注册一个账号,顺着写就好了。吐槽一下,注册的时候Phone Number竟然不需要填数字……有反馈说有的邮箱可能收不到注册回执,我使用的是163邮箱进行注册。
由于这个uCOS-II源码包用的是标准库而不是HAL,顺势而为也就改成标准库了。虽然大同小异,不过函数名字换了一遍还是有点难受的。
本文没有使用STM32CubeMX。
实验步骤
0. 点灯
还是老步骤,拿到不会用的东西,先点个灯。
点灯程序
这次的点灯准备发挥uCOS-II的特长,一次性点两个灯。有的小灯比较脆弱,接的时候连个电阻比较保险。不然有可能烧坏。
直接新建Keil工程,选择好板子型号后进入运行环境选择页面。选择需要的环境后便进入了工程。
在工程内写入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
至此,第一步准备工作完成,虽然未实现任何功能,至少编译不再报错了
按照上面说的教程步骤跑一遍之后的工程文件及部分配置如下。
但是此时可能还会有一些地方无法通过编译。然后七翻八翻找到这个位置,发现两个DEF_ENABLED都得置为0。
然后终于编译过了。
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
}
解决了中断问题之后,编译,下载…………然后我一脸懵逼的看到只有一盏灯在闪。
那还是有问题喽。_(:з」∠)_
经过百度,发现类似于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。
据此可以写出对应的显示模块。电路各种花式连线均可,只要能和程序对应的上。以下是我的连法。
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的值。
2. DHT11数据读取及显示
DHT11是一款有已校准数字信号输出的温湿度传感器。作为一种单总线设备,输入输出均为同一个引脚。
使用DHT11的时候,需要连接三个引脚,引脚1接VCC,引脚2接是stm32的某个GPIO口,引脚4接地。而引脚3悬空。
对DHT11来说,数据的传输步骤如下:
- stm32输出低电平至少18ms(只有此处为ms,其余均为μs)。
- stm32输出高电平20~40μs
- DHT11反馈低电平80μs
- DHT11反馈高电平80μs
- 以上为双方握手,以下开始准备接受数据。数据总长40个bit,输入为大端输入,即高位的bit先进行传输。每个byte表示一个数值。按照接受顺序分别表示湿度整数,湿度小数,温度整数,温度小数,校验码。校验码为前方四个byte的和。
- 对于每个传输的bit,DHT11会首先输出50μs的低电平
- 而后以输出高电平的时间决定每个bit的值。高电平持续时间为20~30μs的为bit 0,高电平持续时间为70μs的表示bit 1。
整个通信过程大约耗时4ms。
根据传输协议写成程序即可。
#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不听话,可以尝试/多次尝试/循环尝试/随机尝试/组合尝试以下几种方法:
- 换几根线
- 换一个DHT11
- 换一个STM32的引脚
- 换一块面包板
- 玄学调参
- 换一个程序
- 换一个STM32
- 去吃个饭
- 去西湖散散心
- 换一个实验
程序内使用时直接调用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”