實驗環(huán)境為Window10 + Keil 5.18,代碼中使用標準庫。
實驗準備
本次實驗硬件上需要七段數(shù)碼管LG3641BH,溫濕度傳感器DHT11,點燈用的小燈一個,面包板一塊加導線若干。
由于手頭公母線似乎不是很夠用,我把整個STM32都插到了面包板上,然后發(fā)現(xiàn)這么搞好像也蠻科學的樣子,整體連線清晰很多。
而軟件則需要標準庫的支持代碼以及uCOS-II源碼了。
標準庫代碼在Lab3的準備過程中已經(jīng)載入Keil。
uCOS-II的源碼可以在文末下載鏈接中下載。下載需要注冊一個賬號,順著寫就好了。吐槽一下,注冊的時候Phone Number竟然不需要填數(shù)字……有反饋說有的郵箱可能收不到注冊回執(zhí),我使用的是163郵箱進行注冊。

由于這個uCOS-II源碼包用的是標準庫而不是HAL,順勢而為也就改成標準庫了。雖然大同小異,不過函數(shù)名字換了一遍還是有點難受的。
本文沒有使用STM32CubeMX。
實驗步驟
0. 點燈
還是老步驟,拿到不會用的東西,先點個燈。
點燈程序
這次的點燈準備發(fā)揮uCOS-II的特長,一次性點兩個燈。有的小燈比較脆弱,接的時候連個電阻比較保險。不然有可能燒壞。
直接新建Keil工程,選擇好板子型號后進入運行環(huán)境選擇頁面。選擇需要的環(huán)境后便進入了工程。

在工程內(nèi)寫入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<times; j++){
for (i=0; i<0x3ffff; i++){
}
}
}
int main() {
GPIO_Configuration();
while(1){
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
GPIO_WriteBit(GPIOA, GPIO_Pin_11, Bit_SET);
Delay_long(10);
GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
GPIO_WriteBit(GPIOA, GPIO_Pin_11, Bit_RESET);
Delay_long(10);
}
}
這份代碼目前和uCOS-II一點關(guān)系都沒有,僅僅是為了測試小燈以及電路的通暢,順便再熟悉一下標準庫的GPIO操作。
初始化的函數(shù)中初始化了兩個GPIO口PA11以及PC13,分別對應接下來要點的兩個燈,其中PA11口的燈是外接的,而PC13口的燈為板子自帶。
在出了一些狀況之后,兩個小燈順利得開始了閃爍。
uCOS-II工程創(chuàng)建
工程創(chuàng)建步驟基本完全按照參考資料中一步一步移植ucos到stm32f103開發(fā)版(修訂版)所述步驟。當然,也別完全照著來,板子類型還是需要選自己的。同時,我用的是自己實驗準備中下載的源碼包,文中給出的那個并沒有使用過。
修訂主要是略微增加對keil功能的利用,提高對編譯器功能的利用可以提高建工程的速度和減少定義沖突
先到官方下載ucos源碼,比較接近的是micrium_stm32f103-sk_ucos-ii,本文就采用該文件
開發(fā)工具版本為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文件,添加內(nèi)容如下:
#include <bsp.h>
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 <stm32f10x_lib.h>和#include <lcd.h>
16.app_cfg.h文件中,修改為#define APP_OS_PROBE_EN 0
17.APP目錄下新建app.c文件,內(nèi)容為
#include <includes.h>
int main(){
OSInit();
OSStart();
return 0;
}
18.注釋掉includes.h文件中的#include <stm32f10x_lib.h>和#include <lcd.h>
至此,第一步準備工作完成,雖然未實現(xiàn)任何功能,至少編譯不再報錯了
按照上面說的教程步驟跑一遍之后的工程文件及部分配置如下。



但是此時可能還會有一些地方無法通過編譯。然后七翻八翻找到這個位置,發(fā)現(xiàn)兩個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創(chuàng)建任務。該函數(shù)接收4個參數(shù),第一個參數(shù)是任務執(zhí)行的函數(shù),第二個參數(shù)是附帶的參數(shù),只有1個指針的位置,第三個參數(shù)是該任務使用的棧的位置,第四個參數(shù)是任務的優(yōu)先級。
而在執(zhí)行了OSStart后,uCOS-II會對任務進行調(diào)度。
看起來一切正常,編譯通過之后下板子本以為會看到燈閃爍起來的,然后程序就死掉了。
程序調(diào)錯
經(jīng)過單步調(diào)試,發(fā)現(xiàn)OSStart最終會進入os_cpu_a.asm的OSStartHighRdy函數(shù)中。在順序執(zhí)行之后,死在了OSStartHang上。
……
CPSIE I ; Enable interrupts at processor level
OSStartHang
B OSStartHang ; Should never get here
……
感覺到了來自系統(tǒng)深深的惡意。不過肯定是哪里有問題。使用這個函數(shù)名進行百度之后發(fā)現(xiàn)——大家的程序原來都死在這了。
而在這個地方死循環(huán)的原因是uCOS-II所需的兩個中斷函數(shù)沒有調(diào)用到。
所以,根據(jù)各種教程,把兩個缺失的中斷函數(shù)補上即可。兩個中斷為startup_stm32f10x_md.s中的PendSV Handler 以及 SysTick Handler。
有兩種方法,第一種是自己寫一個函數(shù)接收中斷并向uCOS-II內(nèi)核傳遞消息,第二種方法是直接使用uCOS-II自帶的中斷函數(shù)替換原本的函數(shù)。
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
}

解決了中斷問題之后,編譯,下載…………然后我一臉懵逼的看到只有一盞燈在閃。
那還是有問題嘍。_(:з」∠)_
經(jīng)過百度,發(fā)現(xiàn)類似于while(i--);之類的延時函數(shù)在uCOS-II中是屬于阻塞的延遲,即任務不會在這個時間點進行切換。而任務切換的方式需要調(diào)用uCOS-II中自帶的延時函數(shù)。
此時,調(diào)整Delay_long函數(shù)即可。
void Delay_long(int times){
/*unsigned int i, j;
for (j=0; j<times; j++){
for (i=0; i<0x3ffff; i++){
}
}*/
OSTimeDly(OS_TICKS_PER_SEC/10 * times);
}
至此,點燈成功。

1. 七段數(shù)碼管
我們用的型號為LG3641BH的七段數(shù)碼管是一款共陽極的數(shù)碼管。從LG3641BH Datasheet中可以找到該款元件的基本信息。

而為了使七段數(shù)碼管的四個數(shù)字同時亮起,使用時分復用的方式,高頻率點亮每個數(shù)碼管,使人眼看起來所有的數(shù)字都是同時亮起的。
咳咳,下面這倆ppt是邏輯課上的,所以別吐槽那個Spartan III。


據(jù)此可以寫出對應的顯示模塊。電路各種花式連線均可,只要能和程序?qū)纳稀R韵率俏业倪B法。

12, 9, 8, 6引腳分別是從左到右4個數(shù)字的陽極,屬于位選引腳,即輸出高電平意為選擇。這四個引腳分別對應stm32上的PA11, PA12, PC13, PC14。
而剩下的引腳為控制具體單段數(shù)碼管的引腳。標號0到7的引腳分別與PA0至PA7相連。
據(jù)此,可以寫出點亮七段數(shù)碼管的程序。
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){
// 通過輸出高電平選擇點亮某個數(shù)字
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){
// 通過對引腳輸出低電平點亮數(shù)碼管段
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; // 小數(shù)點為最低位
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){
// 時分復用的方式輸出數(shù)字,每次調(diào)用led_show只輸出一位數(shù)字
static int index = -1;
int i;
int base = 1000;
index = (index + 1) % 4;
for (i=0; i<index; i++){
base /= 10;
}
digit = (digit / base) % 10;
digit_select(index);
digit_show(digit, 0);
}
int ledValue = 0; // 這個值由task0寫入,task1讀取,都是單向的,所以可以不考慮線程之間的沖突
void LED0_task(void* pdata){
while (1){
ledValue++;
Delay_ms(200);
}
}
void LED1_task(void* pdata){
while(1){
led_show(ledValue);
Delay_ms(7);
}
}
程序使用兩個task完成全部功能。首先是LED0_task,該任務每隔200ms修改一次ledValue的值,即修改需要顯示的值。而LED1_task每次顯示一位ledValue的值。

2. DHT11數(shù)據(jù)讀取及顯示
DHT11是一款有已校準數(shù)字信號輸出的溫濕度傳感器。作為一種單總線設備,輸入輸出均為同一個引腳。
使用DHT11的時候,需要連接三個引腳,引腳1接VCC,引腳2接是stm32的某個GPIO口,引腳4接地。而引腳3懸空。
對DHT11來說,數(shù)據(jù)的傳輸步驟如下:
- stm32輸出低電平至少18ms(只有此處為ms,其余均為μs)。
- stm32輸出高電平20~40μs
- DHT11反饋低電平80μs
- DHT11反饋高電平80μs
- 以上為雙方握手,以下開始準備接受數(shù)據(jù)。數(shù)據(jù)總長40個bit,輸入為大端輸入,即高位的bit先進行傳輸。每個byte表示一個數(shù)值。按照接受順序分別表示濕度整數(shù),濕度小數(shù),溫度整數(shù),溫度小數(shù),校驗碼。校驗碼為前方四個byte的和。
- 對于每個傳輸?shù)腷it,DHT11會首先輸出50μs的低電平
- 而后以輸出高電平的時間決定每個bit的值。高電平持續(xù)時間為20~30μs的為bit 0,高電平持續(xù)時間為70μs的表示bit 1。
整個通信過程大約耗時4ms。

根據(jù)傳輸協(xié)議寫成程序即可。
#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函數(shù)需要的時間是1.4us * @times
unsigned int i;
for (i=0; i<times; i++){
// 外層循環(huán)一次差不多1us,不過不是特別精確,所以不需要內(nèi)層的循環(huán)了
/*
unsigned int j;
for (j=0; j<0x3fff; j++){
}
*/
}
}
void ErrorState(int state){
// 使用死循環(huán)自殺,并顯示錯誤碼
while (1){
led_show(state);
Delay_us(4000);
}
}
void DHT11_Set(int state){
// 設置DHT11 GPIO口的值
BitAction s;
if (state){
s = Bit_SET;
}else{
s = Bit_RESET;
}
GPIO_WriteBit(DHT11_PORT, DHT11_PIN, s);
}
void DHT11_Pin_OUT(){
// 調(diào)整DHT11 GPIO口為輸出模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = DHT11_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DHT11_PORT, &GPIO_InitStructure);
DHT11_Set(1);
}
void DHT11_Pin_IN(){
// 調(diào)整DHT11_GPIO口為輸入模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = DHT11_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DHT11_PORT, &GPIO_InitStructure);
DHT11_Set(1);
}
uint8_t DHT11_Check(){
// 獲取DHT11 GPIO口的數(shù)據(jù),下方算式與調(diào)用函數(shù)等價
return (DHT11_PORT->IDR & DHT11_PIN)> 0;
//return GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN);
}
void DHT11_Wait(int state, int place){
// 等待GPIO口變?yōu)閟tate的值,超時自動自殺
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);
// 轉(zhuǎn)為接收模式準備讀入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);
// 計算高電平持續(xù)的時間
while (DHT11_Check() > 0){
Delay_us(1);
cnt++;
}
// 持續(xù)的足夠久則為bit 1
data |= cnt > 5;
}
return data;
}
uint8_t DHT11_Read_Data(uint8_t *buf){
// 從DHT11內(nèi)讀取數(shù)據(jù)的函數(shù)
int i;
unsigned int cpu_sr;
// 為了關(guān)閉中斷進入臨界區(qū)
OS_ENTER_CRITICAL();
val = 10;
// 發(fā)送握手消息
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];
}
值得一提的是,上述代碼在出現(xiàn)異常情況的時候會直接進入ErrorState自殺,此時會在連入的LED上顯示錯誤號,根據(jù)錯誤號判斷進入ErrorState的位置進行debug即可。
在實踐過程中,如果DHT11不聽話,可以嘗試/多次嘗試/循環(huán)嘗試/隨機嘗試/組合嘗試以下幾種方法:
- 換幾根線
- 換一個DHT11
- 換一個STM32的引腳
- 換一塊面包板
- 玄學調(diào)參
- 換一個程序
- 換一個STM32
- 去吃個飯
- 去西湖散散心
- 換一個實驗
程序內(nèi)使用時直接調(diào)用DHT11_Read_Data函數(shù)進行讀取即可。
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開發(fā)版(修訂版)
- 移植ucosii遇到的問題 B OSStartHang
- 【教程貼】【原創(chuàng)】stm32 的ucosii移植全過程 詳細教程
- 學ucos的心得
- ucos ii的任務在何時切換
- LG3641BH Datasheet
- Stm32程序控制DHT11
- Arduino教程——DHT11數(shù)字溫濕度傳感器(Ⅱ)
- Github: Humidity-stm32-I / dht11.c