实验4-RTOS多任务与温度采集
文章目录
前言
本文记录嵌入式系统实验的两个综合任务: 任务一 —— 把 uC/OS-II 移植到 STM32F103C8 上,构建 3 个不同周期的任务; 任务二 —— 学习 I2C 总线协议,用 AHT20 传感器采集温湿度并通过串口上报,最后把采集任务"塞进"任务一的多任务框架里。
全文基于真实工程(Keil MDK5 + STM32CubeMX + STM32F103C8T6 最小系统板 + ST-Link V2),所有代码均可直接编译通过。
一、实验环境与硬件准备
1.1 软硬件清单
| 类别 | 型号 / 版本 |
|---|---|
| 主控 | STM32F103C8T6 最小系统板(“蓝板”/Blue Pill,64 KB Flash / 20 KB RAM) |
| 调试器 | ST-Link V2(SWD 接口) |
| 传感器 | AHT20 / DHT20 温湿度模块(I2C 接口) |
| 通信 | USB 转 TTL 模块(CH340 / CP2102) |
| 开发环境 | Keil MDK 5.38 + ARMCC V5.06 update 7 |
| 配置工具 | STM32CubeMX 6.18.1 + STM32Cube FW_F1 V1.8.7 |
| RTOS | uC/OS-II V2.93.01(Micrium / Silicon Labs,Apache-2.0) |
| 上位机 | Win11 + 串口助手(XCOM) |
1.2 硬件接线总表
整个实验只用到 4 个外设引脚组,接线如下:
| 功能 | STM32 引脚 | 说明 |
|---|---|---|
| LED1 | PA0 | 低电平点亮,串 330 Ω 到 3.3 V |
| LED2 | PA1 | 低电平点亮,串 330 Ω 到 3.3 V |
| 串口 TX | PB6 | USART1 重映射 |
| 串口 RX | PB7 | USART1 重映射 |
| I2C SCL | PB8 | I2C1 重映射 |
| I2C SDA | PB9 | I2C1 重映射 |
| SWD | PA13 / PA14 | ST-Link 下载调试 |
下面是实物接线图
1.3 三条供电铁律
只用一个电源:板子由 ST-Link 的 3.3 V 供电,USB-TTL 的 VCC 不要接,否则两路电源互相反灌。
必须共地:ST-Link、USB-TTL、AHT20 模块的 GND 必须全部连到一起,否则串口一定收不到数据。
AHT20 用 3.3 V,不要接 5 V。
二、任务一:移植 uC/OS-II 并构建三个任务
2.1 先搞懂:什么是 RTOS
RTOS(Real-Time Operating System,实时操作系统) 的核心不是"快",而是"确定性"——系统对外部事件的响应时间有可预测的上界。
它和我们熟悉的裸机循环最大的区别在于任务调度模型:
| 对比项 | 裸机前后台(while(1) 轮询) | RTOS 多任务 |
|---|---|---|
| 程序结构 | 一个超级循环,用状态机拆 | 多个独立任务,每个任务写得像"独占 CPU" |
| 实时性 | 一个函数卡住,全系统卡住 | 高优先级任务可随时抢占 CPU |
| 延时代码 | HAL_Delay() 死等,CPU 空转 | OSTimeDly() 主动让出 CPU,立刻切走 |
| 扩展性 | 加功能要改主循环 | 加功能只需新建任务 |
RTOS 的内核本质就是一个"调度器",它负责在合适的时候保存当前任务的寄存器现场、恢复下一个任务的现场。这个动作叫上下文切换(Context Switch)。
我们常听到的 RTOS 有:uC/OS-II / uC/OS-III、FreeRTOS、RT-Thread、ThreadX、Zephyr 等。其中 uC/OS-II 代码量小(约 6000 行)、注释详尽、教科书式的源码结构,是学习 RTOS 原理最合适的样本——这也是本次实验选它的原因。
2.2 uC/OS-II 是什么,为什么要移植
uC/OS-II 是 Micrium 公司(现属 Silicon Labs)推出的抢占式、可裁剪、基于优先级的实时内核,通过了 DO-178B 和 IEC 61508 SIL3 认证,工业界应用广泛。
所谓-移植(Porting),就是针对目标 CPU 重新实现 Ports/ 目录下的这 3 个文件,让内核能在该 CPU 上跑起来。
2.3 详细操作步骤
步骤 1:用 CubeMX 建一个干净的裸机工程
新建工程 → 选 STM32F103C8Tx → 按下表配置:
| 配置项 | 设置内容 |
|---|---|
| RCC | HSE = Disable;时钟树使用 HSI:PLL Source = HSI/2,PLLMul = ×16 → SYSCLK = 64 MHz;AHB /1,APB1 /2,APB2 /1 |
| SYSDebug | Serial Wire(只留 SWD,省出 PA15/PB3/PB4) |
| GPIO | PA0 → GPIO_Output,Label = LED1;PA1 → GPIO_Output,Label = LED2 |
| USART1 | Mode = Asynchronous,115200,8-N-1 |
| NVIC | Priority Group = 4 bits(NVIC_PRIORITYGROUP_4) |
| Project | Toolchain = MDK-ARM V5,Min Heap = 0x200,Min Stack = 0x400 |
任务一用不到 I2C,可以只配 GPIO + USART1。等做任务二时再加 I2C1。
步骤 2:放入 uC/OS-II 源码
在工程根目录(与 Core、Drivers 同级)新建 uC-OS2,拷入 Source/ 和 Ports/ 两部分。
Ports/ 下需要自己实现的 4 个文件
步骤 3:Keil 工程配置
Project → Options for Target:
(1) C/C++ → Define 里加:
USE_HAL_DRIVER,STM32F103xB,__TARGET_FPU_SOFTVFP
(2) C/C++ → Include Paths 加两条:
../uC-OS2/Source
../uC-OS2/Ports
(3) 新建分组 uC-OS2,只加这 4 个文件:
| 文件类型 | 路径 |
|---|---|
| C Source | …/uC-OS2/Source/ucos_ii.c |
| C Source | …/uC-OS2/Ports/os_cpu_c.c |
| Assembler Source | …/uC-OS2/Ports/os_cpu_a.asm |
| Assembler Source | …/uC-OS2/Ports/os_vectors.asm |
步骤 4:配置内核 os_cfg.h
只需要改这几处,其余保持 0u
/* App/os_cfg.h */
#define OS_MAX_TASKS 5u /* 最多 5 个任务:本实验 3 个 + 空闲 + 余量 */
#define OS_LOWEST_PRIO 9u /* 优先级 0~9,数字越小优先级越高 */
#define OS_TICKS_PER_SEC 1000u /* 每秒 1000 个节拍 → 1 节拍 = 1 ms */
#define OS_TASK_STAT_EN 0u /* 关掉统计任务,省 512 字节 RAM */
#define OS_TASK_IDLE_STK_SIZE 128u /* 空闲任务栈 */
/* 本实验用不到 IPC,全部关掉省 Flash */
#define OS_SEM_EN 0u
#define OS_MUTEX_EN 0u
#define OS_Q_EN 0u
#define OS_MBOX_EN 0u
#define OS_FLAG_EN 0u
#define OS_MEM_EN 0u
#define OS_TMR_EN 0u
步骤 5:SysTick
(A) 让 HAL 和 uC/OS 共用一个 SysTick
STM32F1 只有一个 SysTick,HAL 用它做 HAL_Delay(),uC/OS 用它做心跳。做法是在原来的中断里追加一行:
/* Core/Src/stm32f1xx_it.c */
#include "ucos_ii.h" /* 记得在 USER CODE BEGIN Includes 里加 */
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick(); /* 保留:HAL 的毫秒计数,HAL_Delay 还靠它 */
/* USER CODE BEGIN SysTick_IRQn 1 */
OS_CPU_SysTickHandler(); /* 【新增】uC/OS-II 的节拍 */
/* USER CODE END SysTick_IRQn 1 */
}
然后在第一个任务里(不能放 main())初始化 SysTick 频率:
OS_CPU_SysTickInitFreq(SystemCoreClock); /* 64 MHz / 1000 = 64000 → 1 ms 一个节拍 */
步骤 6:写三个任务(App/app.c)
(1) App/os_cfg.h(关键片段)
/* 任务与优先级 */
#define OS_LOWEST_PRIO 9u
#define OS_MAX_TASKS 5u
#define OS_TICKS_PER_SEC 1000u
/* 任务栈尺寸(单位:OS_STK 个字) */
#define OS_TASK_IDLE_STK_SIZE 128u
#define OS_TASK_STAT_STK_SIZE 128u
#define OS_TASK_STAT_EN 0u
/* 钩子函数:放在 os_cpu_c.c 里 */
#define OS_CPU_HOOKS_EN 1u
#define OS_DEBUG_EN 0u
#define OS_ARG_CHK_EN 1u
(2) App/app_cfg.h
#ifndef _APP_CFG_H_
#define _APP_CFG_H_
/* Cortex-M 移植层需要的内核配置 */
#define CPU_CFG_NVIC_PRIO_BITS 4u /* STM32F1 的 NVIC 优先级位数 */
#define CPU_CFG_KA_IPL_BOUNDARY 4u /* 优先级 >= 4 的中断为"内核感知" */
/* 三个任务的优先级(数字越小越高) */
#define APP_PRIO_LED1 4u
#define APP_PRIO_LED2 5u
#define APP_PRIO_MSG 6u
/* 任务栈(单位:字;64 字 = 256 字节) */
#define APP_STK_LED 64u
#define APP_STK_MSG 128u
/* 任务三的形态:1 = 采集 AHT20 温湿度(任务二);0 = 发送 hello(任务一) */
#define APP_TASK3_AHT20 0u
#endif
- App/app.h
#ifndef __APP_H
#define __APP_H
#include "main.h"
#include "ucos_ii.h"
void App_Start(void); /* 初始化 uC/OS-II、创建 3 个任务、启动调度(不返回) */
void App_UartSend(const char *str); /* 通过 USART1 发送字符串 */
#endif
(4) App/app.c —— 三个任务
/**
* 三个任务:
* Task_Led1 优先级 4:每 1 秒翻转 LED1(PA0,低电平点亮)
* Task_Led2 优先级 5:每 3 秒翻转 LED2(PA1)
* Task_Msg 优先级 6:每 2 秒发送一句 hello(或采集 AHT20)
*/
#include "app.h"
#include "aht20.h"
#include <string.h>
/* ---------------------------- 任务栈 ---------------------------- */
static OS_STK Led1Stk[APP_STK_LED];
static OS_STK Led2Stk[APP_STK_LED];
static OS_STK MsgStk[APP_STK_MSG];
/**
* @brief 串口发送(轮询阻塞;本任务优先级最低,不会挡住 LED 任务)
*/
void App_UartSend(const char *str)
{
HAL_UART_Transmit(&huart1, (uint8_t *)str, (uint16_t)strlen(str), 100U);
}
#if APP_TASK3_AHT20 > 0u
/**
* @brief 把浮点数格式化为十进制字符串,不依赖 printf 的浮点支持
* @note 先整体四舍五入到 dec 位小数,再拆分整数/小数部分,
* 这样 25.96 会正确进位成 "26.0",而不会被截断成 "25.0"
*/
static char *App_FmtFloat(char *buf, float val, uint8_t dec)
{
static const int32_t pow10[4] = {1, 10, 100, 1000};
int32_t scale, scaled, ip, fp;
int8_t i, j;
char *p = buf;
char tmp[12];
uint8_t neg;
scale = pow10[dec];
neg = (val < 0.0f) ? 1U : 0U;
if (neg != 0U) { val = -val; }
scaled = (int32_t)(val * (float)scale + 0.5f); /* 放大后四舍五入 */
if (scaled == 0) { neg = 0U; } /* 不输出 "-0.0" */
if (neg != 0U) { *p++ = '-'; }
fp = scaled % scale;
ip = scaled / scale;
i = 0;
if (ip == 0) { tmp[i++] = '0'; }
while (ip > 0) { tmp[i++] = (char)('0' + (ip % 10)); ip /= 10; }
while (i > 0) { *p++ = tmp[--i]; }
if (dec > 0U)
{
int32_t d = scale;
*p++ = '.';
for (j = (int8_t)dec - 1; j >= 0; j--)
{
d /= 10;
*p++ = (char)('0' + (fp / d) % 10);
}
}
*p = '\0';
return buf;
}
#endif
/* ---------------------------- 任务 1:LED1 每 1 秒 ---------------------------- */
static void Task_Led1(void *p_arg)
{
(void)p_arg;
/* 移植层规定:OS 节拍定时器必须在 OSStart() 之后初始化,且只做一次 */
OS_CPU_SysTickInitFreq(SystemCoreClock);
for (;;)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
OSTimeDly(OS_TICKS_PER_SEC); /* 1000 节拍 = 1 秒 */
}
}
/* ---------------------------- 任务 2:LED2 每 3 秒 ---------------------------- */
static void Task_Led2(void *p_arg)
{
(void)p_arg;
for (;;)
{
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
OSTimeDly(3 * OS_TICKS_PER_SEC); /* 3000 节拍 = 3 秒 */
}
}
/* ---------------------------- 任务 3 ---------------------------- */
#if APP_TASK3_AHT20 > 0u
/* 【任务二】每 2 秒采集一次 AHT20 温湿度并通过串口上报 */
static void Task_Msg(void *p_arg)
{
uint8_t ok;
float t, h;
char msg[64];
char num[16];
(void)p_arg;
ok = (AHT20_Init() == 0U) ? 1U : 0U; /* 探测传感器是否在线 */
App_UartSend("hello uc/OS! 欢迎来到RTOS多任务环境!\r\n");
for (;;)
{
if (ok != 0U)
{
if (AHT20_Read(&t, &h) == 0U)
{
strcpy(msg, "T=");
strcat(msg, App_FmtFloat(num, t, 1));
strcat(msg, "C H=");
strcat(msg, App_FmtFloat(num, h, 1));
strcat(msg, "%\r\n");
App_UartSend(msg);
}
else
{
/* 读取失败:复位 I2C 外设,防止 F1 的 BUSY 卡死,下周期重试 */
(void)HAL_I2C_DeInit(&AHT20_I2C);
(void)HAL_I2C_Init(&AHT20_I2C);
ok = 0U;
App_UartSend("AHT20 offline\r\n");
}
}
else
{
App_UartSend("AHT20 not found\r\n");
ok = (AHT20_Init() == 0U) ? 1U : 0U;
}
OSTimeDly(2 * OS_TICKS_PER_SEC); /* 2000 节拍 = 2 秒 */
}
}
#else
/* 【任务一】每 2 秒发送一句 hello */
static void Task_Msg(void *p_arg)
{
(void)p_arg;
for (;;)
{
App_UartSend("hello uc/OS! 欢迎来到RTOS多任务环境!\r\n");
OSTimeDly(2 * OS_TICKS_PER_SEC); /* 2000 节拍 = 2 秒 */
}
}
#endif
/* ---------------------------- 启动 uC/OS-II ---------------------------- */
void App_Start(void)
{
INT8U err;
OSInit(); /* 1. 初始化内核 */
/* 2. 创建任务:栈顶 = &Stk[SIZE-1](Cortex-M 栈向下生长) */
err = OSTaskCreate(Task_Led1, (void *)0, &Led1Stk[APP_STK_LED - 1U], APP_PRIO_LED1);
if (err == OS_ERR_NONE)
{
err = OSTaskCreate(Task_Led2, (void *)0, &Led2Stk[APP_STK_LED - 1U], APP_PRIO_LED2);
}
if (err == OS_ERR_NONE)
{
err = OSTaskCreate(Task_Msg, (void *)0, &MsgStk[APP_STK_MSG - 1U], APP_PRIO_MSG);
}
if (err != OS_ERR_NONE)
{
Error_Handler(); /* 任务创建失败 */
}
OSStart(); /* 3. 启动调度,永不返回 */
}
(5) Core/Src/main.c(修改处)
/* USER CODE BEGIN Includes */
#include "app.h"
/* USER CODE END Includes */
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
App_Start(); /* 初始化 OS、创建任务、启动调度(不返回) */
/* USER CODE END 2 */
while (1) { } /* 永远走不到这里 */
}
2.4 实验现象
| 时刻 | LED1 (PA0, 1 s) | LED2 (PA1, 3 s) | 串口 (2 s) |
|---|---|---|---|
| 0 s | — | — | hello uc/OS! 欢迎来到RTOS多任务环境! |
| 1 s | 翻转 | — | — |
| 2 s | 翻转 | — | hello uc/OS! 欢迎来到RTOS多任务环境! |
| 3 s | 翻转 | 翻转 | — |
| 4 s | 翻转 | — | hello uc/OS! 欢迎来到RTOS多任务环境! |
| 6 s | 翻转 | 翻转 | hello uc/OS! 欢迎来到RTOS多任务环境! |

1s和3s周期对LED等进行点亮-熄灭的控制
三、任务二:I2C 协议与 AHT20 温湿度采集
3.1 I2C 总线协议入门
I2C(Inter-Integrated Circuit) 是 Philips(现 NXP)在 1982 年提出的一种两线制、半双工、同步、多主多从串行总线
| 信号线 | 全称 | 作用 |
|---|---|---|
| SCL | Serial Clock | 时钟线,由主机产生 |
| SDA | Serial Data | 数据线,双向 |
两根线都必须接上拉电阻(典型 4.7 kΩ)到 VCC,并且所有器件的引脚都是开漏输出
三个特殊电平状态:
起始条件 START:SCL 保持高电平时,SDA 由高变低
停止条件 STOP:SCL 保持高电平时,SDA 由低变高
- 起始条件 S:SCL 为高时,SDA 由高 → 低
- 停止条件 P:SCL 为高时,SDA 由低 → 高
- 数据有效性:SCL 为高时 SDA 必须稳定(数据采样点);SCL 为低时 SDA 才允许变化。
3.2 软件 I2C 与硬件 I2C 的区别
一、硬件 I2C(Hardware I2C)
用 STM32 片上的 I2C 外设(I2C1 / I2C2),由硬件状态机自动产生时序。
/* 你只需要把数据丢进寄存器,剩下的起始/时钟/ACK 全由硬件完成 */
HAL_I2C_Master_Transmit(&hi2c1, 0x70, cmd, 3, 20); /* 发 3 个字节 */
HAL_I2C_Master_Receive(&hi2c1, 0x71, buf, 7, 20); /* 读 7 个字节 */
内部工作流程:
- 置 CR1.START=1 → 硬件发起始条件
- 等待 SR1.SB=1 → 起始位已发送
- 写 DR → 硬件自动移位输出 8 位 + 收 ACK
- 等待 SR1.TXE/BTF → 字节发送完成
- 置 CR1.STOP=1 → 硬件发停止条件
它的优点是cpu占用极低,速率高,抖动小且代码量小。
二、软件 I2C(Software / Bit-Banging I2C)
不用任何 I2C 外设,只用两个普通 GPIO:
/* 软件 I2C 的原子操作 */
#define SCL_H() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET)
#define SCL_L() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET)
#define SDA_H() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET)
#define SDA_L() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET)
#define SDA_READ() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_9)
/* 起始条件:SCL 高时 SDA 由高变低 */
static void I2C_Start(void)
{
SDA_H(); delay_us(2);
SCL_H(); delay_us(2); /* 满足 t_SU;STA ≥ 4.7us */
SDA_L(); delay_us(2);
SCL_L(); delay_us(2);
}
/* 发送一个字节,返回从机 ACK(0 = 收到应答) */
static uint8_t I2C_SendByte(uint8_t byte)
{
uint8_t i, ack;
for (i = 0; i < 8; i++)
{
if (byte & 0x80) SDA_H(); else SDA_L(); /* 高位先行 */
byte <<= 1;
delay_us(2);
SCL_H(); delay_us(2); /* 从机在这时采样 */
SCL_L(); delay_us(2);
}
/* 第 9 个时钟:释放 SDA,读从机的应答 */
SDA_H(); delay_us(2);
SCL_H(); delay_us(2);
ack = SDA_READ(); /* 0 = ACK,1 = NACK */
SCL_L(); delay_us(2);
return ack;
}
它的优点是任意两个gpio都能用,不受外设引脚限制,比较稳定,时许可任意调整且便于示波器观察时序。
在本次实验中我们用到的是硬件I2C(HAL_I2C_* + I2C1 重映射到 PB8/PB9)。
3.3 硬件 I2C 实现
CubeMX 里勾选 I2C1 后,生成的代码默认在 PB6/PB7。要放到 PB8/PB9。
代码实现:
App/aht20.h
#ifndef __AHT20_H
#define __AHT20_H
#include "main.h"
#define AHT20_I2C_ADDR 0x70 /* 7 位地址 0x38 左移 1 位(写) */
#define AHT20_I2C hi2c1 /* 使用 I2C1:PB8=SCL,PB9=SDA */
uint8_t AHT20_Init(void);
uint8_t AHT20_Read(float *temp, float *humi);
#endif
App/aht20.c:
#include "aht20.h"
/* ---------------------------- 内部工具函数 ---------------------------- */
/**
* @brief I2C 总线恢复
* STM32F1 的 I2C 外设存在 errata:从机在一次传输中途被中止时,
* 可能一直拉低 SDA,导致硬件 BUSY 位永远清不掉。
* 这里先用最多 9 个 SCL 时钟让它把数据移完并释放 SDA,
* 再补一个 STOP 条件,最后重新初始化 I2C 外设。
*/
static void AHT20_I2cRecover(void)
{
GPIO_InitTypeDef gpio = {0};
uint8_t i;
(void)HAL_I2C_DeInit(&AHT20_I2C); /* 释放 I2C 外设和引脚 */
__HAL_RCC_GPIOB_CLK_ENABLE();
/* PB8 = SCL,配成开漏输出,默认拉高 */
gpio.Pin = GPIO_PIN_8;
gpio.Mode = GPIO_MODE_OUTPUT_OD;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &gpio);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
/* PB9 = SDA,先当输入,监听从机是否还占着总线 */
gpio.Pin = GPIO_PIN_9;
gpio.Mode = GPIO_MODE_INPUT;
gpio.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOB, &gpio);
/* 最多打 9 个时钟,直到 SDA 被从机释放为高电平 */
for (i = 0U; i < 9U; i++)
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
HAL_Delay(1);
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_9) == GPIO_PIN_SET)
{
break; /* SDA 已释放 */
}
}
/* 补一个 STOP 条件:SCL 高电平期间 SDA 由低变高 */
gpio.Pin = GPIO_PIN_9;
gpio.Mode = GPIO_MODE_OUTPUT_OD;
HAL_GPIO_Init(GPIOB, &gpio);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET);
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET);
HAL_Delay(1);
/* 重新初始化 I2C 外设(MspInit 会把 PB8/PB9 配回复用开漏) */
(void)HAL_I2C_Init(&AHT20_I2C);
}
static uint8_t AHT20_Write(const uint8_t *buf, uint16_t len)
{
return (HAL_I2C_Master_Transmit(&AHT20_I2C, AHT20_I2C_ADDR,
(uint8_t *)buf, len, 20) == HAL_OK) ? 0U : 1U;
}
static uint8_t AHT20_ReadBytes(uint8_t *buf, uint16_t len)
{
/* 注意:HAL 内部会自己置/清最低位(读操作 RD_WRN=1),
所以这里传 0x70 或 0x71 效果一样,工程里统一用 0x70 */
return (HAL_I2C_Master_Receive(&AHT20_I2C, AHT20_I2C_ADDR,
buf, len, 20) == HAL_OK) ? 0U : 1U;
}
/* ---------------------------- 对外接口 ---------------------------- */
/**
* @brief AHT20 初始化:检查校准状态,未校准则发送初始化命令
* @retval 0 成功;1 失败(通常说明没有接模块)
*/
uint8_t AHT20_Init(void)
{
static const uint8_t init_cmd[3] = {0xBE, 0x08, 0x00}; /* 初始化/校准命令 */
uint8_t status = 0;
uint8_t i;
HAL_Delay(50); /* 上电后至少等 40 ms */
if (AHT20_ReadBytes(&status, 1) != 0U) /* 读状态字,无应答 = 没接模块 */
{
AHT20_I2cRecover();
return 1;
}
if ((status & 0x08U) == 0U) /* bit3 = 0 表示尚未校准 */
{
if (AHT20_Write(init_cmd, 3) != 0U)
{
AHT20_I2cRecover();
return 1;
}
for (i = 0U; i < 10U; i++) /* 校准最多再等 100 ms */
{
HAL_Delay(10);
if ((AHT20_ReadBytes(&status, 1) == 0U) && ((status & 0x08U) != 0U))
{
break; /* 校准完成 */
}
}
if ((status & 0x08U) == 0U)
{
return 1; /* 校准超时 */
}
}
return 0;
}
/**
* @brief 启动一次测量并读取温湿度
* @param temp 温度(摄氏度,-40 ~ +85)
* @param humi 湿度(相对湿度 %,0 ~ 100)
* @retval 0 成功;1 失败
*/
uint8_t AHT20_Read(float *temp, float *humi)
{
static const uint8_t trig_cmd[3] = {0xAC, 0x33, 0x00}; /* 触发测量命令 */
uint8_t buf[7];
uint8_t i;
uint8_t done = 0;
uint32_t h, t;
if (AHT20_Write(trig_cmd, 3) != 0U)
{
AHT20_I2cRecover();
return 1;
}
/* 轮询等待转换完成:一次转换约 75~80 ms,每 10 ms 读一次状态,
bit7 = 0 表示数据就绪;最多等 20 次(200 ms)。
注意:不能用死等固定 80 ms —— 刚好卡在边界上会时不时读到"忙"。 */
for (i = 0U; i < 20U; i++)
{
HAL_Delay(10);
if ((AHT20_ReadBytes(buf, 7) == 0U) && ((buf[0] & 0x80U) == 0U))
{
done = 1U;
break;
}
}
if (done == 0U) /* 超时仍忙或读数一直失败 */
{
AHT20_I2cRecover();
return 1;
}
/* 湿度:20 位 */
h = ((uint32_t)buf[1] << 12) | ((uint32_t)buf[2] << 4) | ((uint32_t)buf[3] >> 4);
/* 温度:20 位 */
t = (((uint32_t)(buf[3] & 0x0FU)) << 16) | ((uint32_t)buf[4] << 8) | (uint32_t)buf[5];
*humi = (float)h * 100.0f / 1048576.0f; /* 2^20 = 1048576 */
*temp = (float)t * 200.0f / 1048576.0f - 50.0f;
return 0;
}
Core/Src/main.c:
/* 把浮点数格式化为字符串(不依赖 printf 的浮点支持,省 2 KB Flash) */
static char *FmtFloat(char *buf, float val, uint8_t dec)
{
static const int32_t pow10[4] = {1, 10, 100, 1000};
int32_t scale, scaled, ip, fp;
int8_t i, j;
char *p = buf;
char tmp[12];
uint8_t neg;
scale = pow10[dec];
neg = (val < 0.0f) ? 1U : 0U;
if (neg != 0U) { val = -val; }
/* 先整体四舍五入再拆分,避免 25.96 被显示成 25.0 */
scaled = (int32_t)(val * (float)scale + 0.5f);
if (scaled == 0) { neg = 0U; }
if (neg != 0U) { *p++ = '-'; }
fp = scaled % scale;
ip = scaled / scale;
i = 0;
if (ip == 0) { tmp[i++] = '0'; }
while (ip > 0) { tmp[i++] = (char)('0' + (ip % 10)); ip /= 10; }
while (i > 0) { *p++ = tmp[--i]; }
if (dec > 0U)
{
int32_t d = scale;
*p++ = '.';
for (j = (int8_t)dec - 1; j >= 0; j--) { d /= 10; *p++ = (char)('0' + (fp / d) % 10); }
}
*p = '\0';
return buf;
}
static void UartSend(const char *str)
{
HAL_UART_Transmit(&huart1, (uint8_t *)str, (uint16_t)strlen(str), 100U);
}
int main(void)
{
char msg[64], num[16];
float t, h;
uint8_t ok;
uint32_t tick;
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();
UartSend("AHT20 demo start\r\n");
ok = (AHT20_Init() == 0U) ? 1U : 0U;
if (ok == 0U) { UartSend("AHT20 NOT FOUND\r\n"); }
tick = HAL_GetTick();
while (1)
{
/* 每 2 秒采集一次 */
if ((HAL_GetTick() - tick) >= 2000U)
{
tick = HAL_GetTick();
if ((ok != 0U) && (AHT20_Read(&t, &h) == 0U))
{
strcpy(msg, "T=");
strcat(msg, FmtFloat(num, t, 1));
strcat(msg, "C H=");
strcat(msg, FmtFloat(num, h, 1));
strcat(msg, "%\r\n");
UartSend(msg);
}
else if (ok == 0U)
{
ok = (AHT20_Init() == 0U) ? 1U : 0U; /* 定时重试探测 */
UartSend("AHT20 not found, retrying\r\n");
}
}
}
}
DHT20
四、总结
4.1 任务一:RTOS 移植的核心结论
1.uC/OS-II 的移植工作量集中在 Ports/ 目录的 3~4 个文件,内核 Source/ 一行都不用改——这就是"可移植性"的实际含义。
2.Cortex-M3 的双栈机制(MSP/PSP) 是移植的基础:任务跑在 PSP 上(每个任务一个栈),中断和内核跑在 MSP 上(OS_CPU_ExceptStk 独立 1 KB 栈),两者互不覆盖。
3.上下文切换挂接在 PendSV(优先级 0xFF 最低),保证切换只在所有中断处理完毕、寄存器状态确定时发生。
4.临界区使用 BASEPRI(优先级 4 为内核感知边界)而非关总中断,使优先级 0~3 的高实时中断不受内核临界区影响——这是 RTOS 实时性的关键设计。
5.三个任务以 1 s / 3 s / 2 s 独立周期运行且互不干扰,验证了抢占式优先级调度 + 时间延迟机制的正确性。
4.2 任务二:I2C 与 AHT20 的核心结论
1.I2C 是开漏 + 上拉的线与总线,只需要 SCL/SDA 两根线就能挂多个从机;START/STOP 是 SCL 高电平期间 SDA 的跳变,数据在 SCL 高电平时有效。
2.软件 I2C 与硬件 I2C 的本质区别:一个用 GPIO 手工"打拍子",一个用片上外设状态机。软件 I2C 灵活稳定但费 CPU,硬件 I2C 高效但 F1 上有硅 bug,需要额外做总线恢复。
3.AHT20 的 7 位地址是 0x38,HAL 库里传 8 位左移地址:写 0x70、读 0x71。
4.初始化命令 0xBE 0x08 0x00(校准),触发测量命令 0xAC 0x33 0x00;读回 7 字节 = 状态 + 5 字节数据 + CRC。
5.状态字节 bit7 = 忙标志、bit3 = 校准标志,必须轮询 bit7 而不能死等固定延时。
6.20 bit 原始数据 → 物理量的换算:湿度 raw/2^20100 %,温度 raw/2^20200 - 50 ℃。
参考资料
- 《嵌入式实时操作系统 uC/OS-II(第2版)》—— Jean J. Labrosse
- uC/OS-II V2.93.01 源码及 Ports/ARM-Cortex-M3/Generic 移植说明(Micrium / Silicon Labs,Apache-2.0)
- 野火《STM32F103 开发实战指南》第 23 章「I2C–读写 EEPROM」
- AHT20 数据手册(ASAIR,2019)
- STM32F103xx 参考手册 RM0008 —— AFIO 重映射、I2C 章节
- STM32F10xxx 勘误手册 ES096 —— I2C 外设 errata
- ARM Cortex-M3 权威指南(Joseph Yiu)—— 异常模型、双栈、PendSV
更多推荐
所有评论(0)