找回密码
 立即注册
楼主: wdxzaxem

STC32G系列,备战2025全国大学生智能车竞赛学习贴

[复制链接]
  • 打卡等级:初来乍到
  • 打卡总天数:5
  • 最近打卡:2026-01-20 17:19:10
已绑定手机

3

主题

32

回帖

91

积分

注册会员

积分
91
发表于 2026-1-20 17:59:30 | 显示全部楼层

第八集:蜂鸣器应用与电磁炉仿真项目实战


1. 蜂鸣器类型与区别

  • 有源蜂鸣器
    • 内部自带振荡源
    • 通电即响(一端接高电平,一端接低电平)
    • 控制简单,成本较高
  • 无源蜂鸣器
    • 无内部振荡源
    • 需输入频率信号(高低电平交替)才能发声
    • 成本低,适合播放音乐等复杂发声场景
  • 开发板使用:有源蜂鸣器(P54引脚控制)

2. 蜂鸣器控制原理

  • 电路结构:蜂鸣器 → 三极管(8550) → P54引脚
  • 低电平打开蜂鸣器(P54 = 0)
  • 高电平关闭蜂鸣器(P54 = 1)
  • 控制代码示例:
    BEEP = 0;  // 打开蜂鸣器
    delay_ms(10);
    BEEP = 1;  // 关闭蜂鸣器
    

3. 电磁炉项目需求分析

组件 对应功能 控制引脚
按键 K1 开关机键 P32
按键 K2 功能选择键 P33
LED1~LED8 8个功能图标 P6(P60~P67)
蜂鸣器 操作提示音 P54

功能流程

  1. 开机(K1按下):
    • 蜂鸣器响10ms
    • 8个LED全亮200ms后熄灭
  2. 功能选择(K2按下,仅开机后有效):
    • 蜂鸣器响10ms
    • LED从右至左轮流点亮(1→2→3→…→8→1)
  3. 关机(K1再次按下):
    • 蜂鸣器响10ms
    • 所有LED熄灭

1. 变量定义

bit power_flag = 0;  // 开关机状态:0-关机,1-开机
u8 mode = 0;         // 功能模式:0-无模式,1~8对应LED1~LED8

2. 按键K1:开关机控制

if (K1 == 0) {
    delay_ms(10);
    if (K1 == 0) {
        while (K1 == 0);  // 等待按键松开
  
        if (power_flag == 0) {  // 当前为关机状态
            power_flag = 1;     // 开机
            // 蜂鸣提示
            BEEP = 0;
            delay_ms(10);
            BEEP = 1;
            // LED全亮200ms
            P6 = 0x00;          // 全部点亮(低电平点亮)
            delay_ms(200);
            P6 = 0xFF;          // 全部熄灭
        } else {                // 当前为开机状态
            power_flag = 0;     // 关机
            BEEP = 0;
            delay_ms(10);
            BEEP = 1;
            P6 = 0xFF;          // 确保所有LED熄灭
            mode = 0;           // 模式清零
        }
    }
}

3. 按键K2:功能选择(仅开机有效)

if (power_flag == 1 && K2 == 0) {  // 开机状态下检测K2
    delay_ms(10);
    if (K2 == 0) {
        while (K2 == 0);
  
        // 蜂鸣提示
        BEEP = 0;
        delay_ms(10);
        BEEP = 1;
  
        // 模式切换
        mode++;
        if (mode > 8) mode = 1;  // 循环1~8
  
        // 根据模式点亮对应LED(低电平点亮)
        P6 = ~(1 << (mode - 1));  // 将1左移(mode-1)位后取反
    }
}

  • 左移操作1 << (mode - 1)例如 mode=100000001mode=200000010

  • 按位取反~(将每一位取反)

    • 例如 ~(00000001) = 11111110(低电平点亮LED1)
      延时10ms消除机械抖动
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:5
  • 最近打卡:2026-01-20 17:19:10
已绑定手机

3

主题

32

回帖

91

积分

注册会员

积分
91
发表于 2026-1-20 18:01:40 | 显示全部楼层

第八集作业:电磁炉启动/停止功能扩展


一、作业要求

在第八集“电磁炉仿真项目”基础上,增加按键K3(P3.4引脚)实现以下功能:

  1. 启动/停止控制:按下K3切换“工作”与“停止”状态
  2. 工作状态指示:工作状态下,当前模式对应的LED持续闪烁
  3. 功能锁定:工作状态下,禁止通过K2切换模式
  4. 状态恢复:停止工作后,LED恢复常亮,模式切换恢复可用

二、新增变量与引脚定义

1. 引脚定义

sbit K3 = P3^4;  // 新增按键K3,用于启动/停止

2. 状态变量

bit run_flag = 0;  // 工作状态标志位:0-停止,1-运行
// 已存在变量:
// bit power_flag = 0;  // 开关机状态
// u8 mode = 0;         // 当前功能模式(0-无模式,1~8对应LED)

三、按键K3功能实现

1. 按键检测框架

if (K3 == 0) {               // 检测K3是否按下
    delay_ms(10);            // 消抖
    if (K3 == 0) {           // 确认按下
        // 功能执行区域
        while (K3 == 0);     // 等待按键松开
    }
}

2. 启动/停止逻辑

if (K3 == 0) {
    delay_ms(10);
    if (K3 == 0) {
        // 蜂鸣提示
        BEEP = 0;
        delay_ms(10);
        BEEP = 1;
  
        // 只有选择了模式(mode > 0)才能启动
        if (mode > 0) {
            run_flag = ~run_flag;  // 切换运行状态
        }
  
        while (K3 == 0);  // 等待松开
    }
}

四、工作状态下的LED控制

1. 停止状态(run_flag = 0)

if (run_flag == 0) {
    // 根据当前模式点亮对应LED
    P6 = ~(1 << (mode - 1));
}

2. 运行状态(run_flag = 1)

if (run_flag == 1) {
    // LED闪烁:200ms亮,200ms灭
    P6 = 0xFF;           // 全部熄灭
    delay_ms(200);
    P6 = ~(1 << (mode - 1));  // 点亮当前模式LED
    delay_ms(200);
}

五、功能锁定实现

修改K2检测逻辑

// 只有在开机状态且不在运行状态时,才能切换模式
if (power_flag == 1 && run_flag == 0 && K2 == 0) {
    delay_ms(10);
    if (K2 == 0) {
        // ... 原有模式切换代码 ...
        while (K2 == 0);  // 等待松开
    }
}

1. 常见错误:赋值与判断混淆

// 错误写法(赋值):
if (run_flag = 1)  // 这会将run_flag赋值为1,永远为真

// 正确写法(判断):
if (run_flag == 1)  // 判断run_flag是否等于1

2. 状态初始化

在关机函数中,需要重置运行状态:

// 关机时清零
run_flag = 0;  // 停止运行
mode = 0;      // 清除模式

3. 按键响应时机优化

// 原逻辑:等待松开后执行
while (K3 == 0);  // 先等待松开
// 再执行功能代码...

// 优化后:按下即执行,然后等待松开
// 先执行功能代码...
while (K3 == 0);  // 再等待松开

操作顺序

  1. 开机:K1按下 → 蜂鸣 → LED全亮200ms → 熄灭
  2. 选择模式:K2按下 → 蜂鸣 → LED轮流点亮
  3. 启动工作:K3按下 → 蜂鸣 → 当前模式LED开始闪烁
  4. 功能锁定:闪烁期间,K2按键无效
  5. 停止工作:K3再次按下 → 蜂鸣 → LED恢复常亮
  6. 关机:K1按下 → 蜂鸣 → 所有LED熄灭,状态重置
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:5
  • 最近打卡:2026-01-20 17:19:10
已绑定手机

3

主题

32

回帖

91

积分

注册会员

积分
91
发表于 2026-1-20 18:04:39 | 显示全部楼层

第九集:数码管的静态使用

1. 数码管

  • 数码管(LED数码管)是由多个**发光二极管(LED)**组成的显示器件。
  • 常见形态:一位、两位、四位、八位数码管,以及特殊符号(如电池、蓝牙、单位符号等)。

2. 常见数码管类型

  • 一位数码管:单个“8”字形。
  • 多位数码管:如四位数码管可显示“9999”。
  • 符号数码管:可显示特定图标或单位。

3. 数码管结构

  • 每位数码管由8个LED段组成(7段数字 + 1个小数点),分别标记为:a, b, c, d, e, f, g, dp

数码管控制原理

1. 共阴与共阳

  • 共阴(Common Cathode):所有LED的负极接在一起,正极分别控制。给对应段高电平点亮。
  • 共阳(Common Anode):所有LED的正极接在一起,负极分别控制。给对应段低电平点亮。
  • 开发板上使用的是共阴数码管

2. 位选与段选

  • 段选:控制显示哪些段(a~g, dp)。
  • 位选:选择点亮哪一个数码管(在多位数码管中)。

3. 引脚连接(以开发板为例)

  • 段选引脚:a~g, dp 分别接在 P6.0 ~ P6.7
  • 位选引脚(四位数码管):K1~K4 对应四个数码管,控制信号来自 P7.0 ~ P7.3

数码管内码(段码)生成方法

1. 段码对应关系(共阴)

数字 a b c d e f g dp 十六进制
0 1 1 1 1 1 1 0 0 0xC0
1 0 1 1 0 0 0 0 0 0xF9
2 1 1 0 1 1 0 1 0 0xA4
3 1 1 1 1 0 0 1 0 0xB0
4 0 1 1 0 0 1 1 0 0x99
5 1 0 1 1 0 1 1 0 0x92
6 1 0 1 1 1 1 1 0 0x82
7 1 1 1 0 0 0 0 0 0xF8
8 1 1 1 1 1 1 1 0 0x80
9 1 1 1 1 0 1 1 0 0x90

2. 如何计算内码?

  • 按段顺序(dp, g, f, e, d, c, b, a)排列二进制值,再转为十六进制。
  • 数字“0” → 二进制 1100 0000 → 十六进制 0xC0

1. 定义段码数组

unsigned char code segmentCode[] = {
    0xC0, // 0
    0xF9, // 1
    0xA4, // 2
    0xB0, // 3
    0x99, // 4
    0x92, // 5
    0x82, // 6
    0xF8, // 7
    0x80, // 8
    0x90  // 9
};

2. 初始化与显示

  • 设置P6为输出(段选)。
  • 设置P7.0为低电平(选中最后一个数码管)。
  • 将段码赋值给P6即可显示对应数字。

3. 实现0~9循环显示

unsigned char num = 0;
while (1) {
    P6 = segmentCode[num];
    num++;
    if (num > 9) num = 0;
    delay_ms(1000);
}

4. 按键控制数字加减

  • 使用两个按键分别控制 num++num--
  • 限制范围在0~9之间。
  • 可配合蜂鸣器(接P5.4)做按键提示音。
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:5
  • 最近打卡:2026-01-20 17:19:10
已绑定手机

3

主题

32

回帖

91

积分

注册会员

积分
91
发表于 2026-1-20 18:08:10 | 显示全部楼层

第十集:数码管的动态显示

动态显示原理

1. 动态刷新概念

  • 静态显示:数码管持续点亮,不熄灭。
  • 动态显示:逐位快速轮流显示,利用视觉暂留(约20ms内)产生“同时点亮”的错觉。

2. 动态刷新机制

  • 每个数码管显示一个短时间(如1ms),然后切换到下一个。
  • 循环周期需小于20ms,刷新率高于50Hz,避免肉眼看到闪烁。
  • 原理类似电影帧切换,通过快速切换形成连续画面。

3. 实现步骤(以八位数码管为例)

  1. 选择第一位(位选)
  2. 输出该位对应的段码(段选)
  3. 延时1ms
  4. 关闭当前位,选择下一位,重复步骤2-3
  5. 循环8位后回到第一步,形成连续显示

硬件连接与码表

1. 开发板连接(共阴数码管)

  • 段选(a~g, dp):接P6.0~P6.7
  • 位选(8位数码管):接P7.0P7.7(K1K8)

2. 段码数组(共阴)

unsigned char code segmentCode[] = {
    0xC0, // 0
    0xF9, // 1
    0xA4, // 2
    0xB0, // 3
    0x99, // 4
    0x92, // 5
    0x82, // 6
    0xF8, // 7
    0x80, // 8
    0x90  // 9
};

3. 位码数组(开发板对应P7.0~P7.7)

unsigned char code bitCode[] = {
    0x7F, // 第一位(P7.0=0)
    0xBF, // 第二位(P7.1=0)
    0xDF, // 第三位(P7.2=0)
    0xEF, // 第四位(P7.3=0)
    0xF7, // 第五位(P7.4=0)
    0xFB, // 第六位(P7.5=0)
    0xFD, // 第七位(P7.6=0)
    0xFE  // 第八位(P7.7=0)
};

4. 带小数点的段码

  • 在原段码基础上减去0x80(点亮dp段)
  • 例如:数字0的段码0xC0,带小数点则为0x40

程序实现

1. 基础动态显示程序

unsigned char num = 0;  // 当前显示数字

while(1) {
    for(int i = 0; i < 8; i++) {
        P7 = bitCode[i];                // 选择第i位
        P6 = segmentCode[num];          // 显示数字num
        delay_ms(1);                    // 延时1ms
    }
    num++;
    if(num > 9) num = 0;
}

2. 使用显示数组管理各位置

// 定义每个数码管要显示的数字(0-9)
unsigned char displayBuff[8] = {1, 2, 3, 4, 5, 6, 7, 8};

void displayScan() {
    static unsigned char i = 0;
  
    P7 = bitCode[i];                                // 选择第i位
    P6 = segmentCode[displayBuff[i]];               // 显示对应数字
    i++;
    if(i >= 8) i = 0;
  
    delay_ms(1);  // 每位移显示时间
}

3. 带小数点的显示

// 段码数组扩展,10-19为带小数点的0-9
unsigned char code segmentCodeWithDot[20] = {
    0xC0, 0xF9, 0xA4, 0xB0, 0x99,  // 0-4
    0x92, 0x82, 0xF8, 0x80, 0x90,  // 5-9
    0x40, 0x79, 0x24, 0x30, 0x19,  // 0.-4.(带小数点)
    0x12, 0x02, 0x78, 0x00, 0x10   // 5.-9.(带小数点)
};

// 显示带小数点的数字(如显示"3.")
P6 = segmentCodeWithDot[13];  // 3在数组下标3,3.在下标13

五、十秒免单计数器

1. 项目功能

  • 前四位显示目标时间:"10.00"
  • 后四位显示当前计时(10ms单位)
  • 按键控制开始/停止
  • 目标:停在"10.00"时"免单"

2. 关键代码实现

// 显示缓冲区
unsigned char displayBuff[8] = {1, 10, 0, 0, 0, 0, 0, 0};  // 显示"10.00 0000"

// 计时变量
unsigned long timerCount = 0;      // 计时变量(10ms单位)
unsigned char runFlag = 0;         // 运行标志

// 主循环
while(1) {
    // 扫描显示
    for(int i = 0; i < 8; i++) {
        P7 = bitCode[i];
        P6 = segmentCodeWithDot[displayBuff[i]];
        delay_ms(1);
    }
  
    // 按键检测
    if(KEY1 == 0) {
        delay_ms(10);
        if(KEY1 == 0) {
            BEEP = 0;  // 蜂鸣器响
            delay_ms(10);
            BEEP = 1;
        
            runFlag = !runFlag;  // 切换运行状态
            if(runFlag == 1) {
                timerCount = 0;  // 重新开始计时
            }
        
            while(KEY1 == 0) {
                // 等待按键释放,同时保持显示刷新
                displayScan();
            }
        }
    }
  
    // 计时更新
    if(runFlag == 1) {
        timerCount++;
    
        // 更新显示缓冲区后4位
        displayBuff[4] = timerCount / 10000 % 10;      // 万位
        displayBuff[5] = timerCount / 1000 % 10;       // 千位
        displayBuff[6] = timerCount / 100 % 10;        // 百位
        displayBuff[7] = timerCount / 10 % 10 + 10;    // 十位(带小数点)
    }
}

3. 数字提取技巧

// 提取数值的各位数字
unsigned long num = 12345;

// 方法1:除法和取余
ge = num % 10;          // 个位:5
shi = num / 10 % 10;    // 十位:4
bai = num / 100 % 10;   // 百位:3

// 方法2:使用循环
while(num > 0) {
    digit = num % 10;   // 从个位开始取
    num = num / 10;
}

六、代码优化技巧

1. 使用宏定义延时时间

#define DISPLAY_DELAY 1  // 每位移显示时间

// 使用时
delay_ms(DISPLAY_DELAY);

2. 封装显示函数

void displayRefresh() {
    static unsigned char i = 0;
  
    P7 = bitCode[i];
    P6 = segmentCodeWithDot[displayBuff[i]];
  
    i++;
    if(i >= 8) i = 0;
}

// 主循环中调用
while(1) {
    displayRefresh();
    delay_ms(1);
}

3. 全局变量管理

  • displayBuff[8]:显示缓冲区
  • timerCount:计时变量
  • runFlag:运行标志
  • 使用全局变量便于多个函数访问
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:5
  • 最近打卡:2026-01-20 17:19:10
已绑定手机

3

主题

32

回帖

91

积分

注册会员

积分
91
发表于 2026-1-20 18:11:08 | 显示全部楼层

第十一课:定时器的使用

一、

1. 为什么要使用定时器?

  • 在循环中使用软件延时(如 Delay(1ms))时,若遇到按键等待、长延时等阻塞操作,会导致系统计时不准确。
  • 定时器可以独立于主程序运行,实现硬件计时,确保时间基准不受主程序阻塞影响。

2. 什么是定时器中断?

  • 中断机制:主程序执行过程中,若定时器时间到达,暂停主程序,执行中断函数,完成后返回主程序继续执行。
  • 比喻理解:你在背书(主程序),妈妈让你5分钟后去看锅(中断),你看完后回来继续背书,不影响之前进度。

二、STC32G定时器的基本结构

1. 定时器数量与类型

  • STC32G有 5个定时器:T0、T1、T2、T3、T4。
  • 每个定时器既可用作定时器,也可用作计数器

2. 核心寄存器介绍

寄存器 功能说明
TMOD 设置定时器模式(定时/计数)与工作模式
TH0 / TL0 定时器初值寄存器(高8位/低8位)
TCON 控制定时器启动、中断标志等
AUXR 控制分频系数(12分频/不分频)
IE 中断使能寄存器(EA总中断,ET0定时器0中断)

三、定时器配置步骤(以T0为例)

1. 设置为定时器模式

TMOD = 0x00; // 设置T0为16位自动重载定时器模式
  • C/T = 0:定时器模式;C/T = 1:计数器模式。

2. 设置分频系数

  • 通过 AUXR 中的 T0x12 位控制:
    • T0x12 = 0:12分频(默认)
    • T0x12 = 1:不分频
  • 分频比喻:脚踏板转12圈后轮转一圈(12分频),转1圈后轮转一圈(不分频)。

3. 选择工作模式

  • 模式0:16位自动重载(常用)
  • 模式1:16位不自动重载
  • 模式2:8位自动重载
  • 模式3:不可屏蔽中断模式

4. 计算并设置定时初值

  • TH0 = 0xF8;
    TL0 = 0x30;
    

5. 启动定时器与使能中断

TR0 = 1;   // 启动定时器0
ET0 = 1;   // 允许定时器0中断
EA = 1;    // 开启总中断

6. 编写中断服务函数

void Timer0_ISR() interrupt 1
{
    // 每1ms执行一次
    time_count++;  // 计时变量
    // 其他周期性任务
}

四、实际应用:修复数码管闪烁问题

原问题:

  • 按键按下时,数码管刷新被阻塞,出现闪烁。

解决方案:

  • 将数码管刷新逻辑移至定时器中断中,每1ms执行一次。
  • 按键检测仍放在主循环中,但不再影响时间基准。

关键代码修改:

void Timer0_ISR() interrupt 1
{
    // 刷新数码管
    Display_Refresh();
    // 计时
    time_count++;
}

void main()
{
    Timer0_Init();  // 初始化定时器
    while(1)
    {
        Key_Scan(); // 按键检测(不阻塞)
        // 其他任务
    }
}

五、快捷配置工具:STC-ISP软件

  • 使用STC-ISP的“定时器计算器”功能:
    1. 选择系统频率(如24MHz)
    2. 选择定时器(T0~T4)
    3. 输入定时时间(如1ms)
    4. 选择分频与模式
    5. 自动生成初始化代码与中断函数模板
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:5
  • 最近打卡:2026-01-20 17:19:10
已绑定手机

3

主题

32

回帖

91

积分

注册会员

积分
91
发表于 2026-1-20 18:40:24 | 显示全部楼层

第十二课:计数器的使用


一、计数器

1. 什么是计数器?

  • 计数器用于对外部脉冲信号进行计数,每检测到一个脉冲(如上升沿或下降沿),计数值加一。
  • 与定时器共用硬件资源,通过配置选择为“定时模式”或“计数模式”。

2. 典型应用场景

场景 说明
电机转速测量 通过光电码盘输出脉冲,计算电机转数
流量检测 编码器随液体流动输出脉冲,计算流量
按键计数 模拟脉冲输入,用于测试与调试
位置检测 通过脉冲数计算位移或角度

3. 举例说明

  • 电机测速:电机后加装光电码盘,码盘有60个孔,电机转一圈输出60个脉冲。
  • 编码器:用于测量液体流量,转一圈输出一个脉冲。

二、计数器配置(以T1为例)

1. 设置计数器模式

TMOD = 0x40;  // 设置T1为计数器模式,16位自动重载
  • C/T = 1:计数器模式;C/T = 0:定时器模式。
  • T1M1T1M0 设为 00,表示16位自动重载模式。

2. 设置计数初值

TH1 = 0xFF;
TL1 = 0xFF;  // 初值为65535,再来一个脉冲即溢出
  • 初值决定计满溢出所需的脉冲数。

3. 启动计数器与使能中断

TR1 = 1;    // 启动计数器T1
ET1 = 1;    // 允许T1中断
EA  = 1;    // 开启总中断

4. 设置外部引脚与上拉电阻

  • 计数器T1使用 P3.5 引脚(T1引脚)作为脉冲输入。
  • 需启用内部上拉电阻:
    P3PU = 0x20;  // 使能P3.5上拉电阻
    

5. 编写中断服务函数

void Timer1_ISR() interrupt 3  // T1中断号为3
{
    P60 = !P60;  // 每次脉冲取反LED,用于测试
}

三、计数器工作原理:自动重载模式

  • 16位自动重载:计数器从初值开始加一,到达65536后溢出,自动重新装载初值。
  • 计数溢出:溢出时触发中断,可执行相应操作(如累加溢出次数)。

示意图:

初值500 → 计数加1 → ... → 65535 → 溢出 → 重新从500开始
          ↑                       ↑
      每个脉冲                触发中断

四、实际应用:电机测速装置

1. 题目要求(大学生电子设计竞赛)

  • 测量电机转速,范围:600 ~ 5000 转/分钟。
  • 显示:4位十进制数码管。
  • 测量周期:2秒。

2. 实现思路:M法测速(频率测量法)

  • 在固定时间(2秒)内统计脉冲数。

3. 系统框架

模块 功能
定时器T0 定时2秒,用于测量周期
计数器T1 统计2秒内的脉冲数
数码管 显示转速值

4. 关键代码片段

// 变量定义
unsigned int pulse_count = 0;
unsigned int time_count = 0;

// 定时器T0中断:1ms定时
void Timer0_ISR() interrupt 1
{
    time_count++;
    if(time_count >= 2000)  // 2秒到
    {
        // 读取脉冲数
        pulse_count = TH1 * 256 + TL1;
        // 清空计数器,重新计数
        TH1 = 0;
        TL1 = 0;
        // 刷新数码管显示
        Display_Refresh(pulse_count);
        time_count = 0;
    }
}

// 主函数
void main()
{
    Timer0_Init();  // 1ms定时
    Timer1_Init();  // 计数器模式
    while(1);
}

五、注意事项与常见问题

1. 文件名与变量名

  • 避免使用含 0xFD编码的汉字(如“数”、“过”、“三”等),否则编译报错。
  • 建议使用英文或拼音命名。

2. 按键抖动问题

  • 机械按键会产生抖动,导致误计数。
  • 实际应用中应使用硬件消抖或软件滤波。

3. 溢出处理

  • 若脉冲数超过65535,需在中断中累加“溢出次数”。
  • 总脉冲数 = 溢出次数 × 65536 + 当前计数值。

4. 单位换算

  • 若显示“转/分钟”,需将脉冲数乘以60:
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:5
  • 最近打卡:2026-01-20 17:19:10
已绑定手机

3

主题

32

回帖

91

积分

注册会员

积分
91
发表于 2026-1-20 19:12:34 | 显示全部楼层

第13集:简易多任务处理(上)课程笔记

将前12课内容划分为四个功能模块:

  • LED和数码管模块 (led_seg.c / led_seg.h)
    • LED和数码管共用P6端口,便于统一控制
  • 按键模块 (key.c / key.h)
  • 蜂鸣器模块 (beep.c / beep.h)
  • 定时器模块 (timer.c / timer.h)
    • 定时器与计数器功能类似,可合并

3. 编程思路梳理

程序设计的核心是理清思路:

  • 何时打开/关闭LED?打开多久?
  • 何时切换/显示数码管?如何显示?
  • 按键按下后触发什么功能?
  • 理清思路后,编写程序将变得简单

1. 三步骤法

  1. 新建文件并保存
  2. 添加到工程
  3. 添加引用路径
1. 创建led_seg.c和led_seg.h文件
2. 在工程中添加led_seg.c文件
3. 在Include Paths中添加hardware/led_seg路径
4. 在主文件中引用头文件:#include "led_seg.h"
#ifndef __LED_SEG_H__
#define __LED_SEG_H__

// 函数声明
// 变量声明

#endif

1. extern修饰符

  • 作用:声明变量或函数是在别处定义的,此处引用
  • 两种用法
    1. 在a.c中声明:extern int v;(引用b.c中的变量v)
    2. 在b.h中声明:extern int v;,然后在a.c中包含b.h
  • 重要规则
    • extern声明的变量不能直接赋初值
    • 赋初值必须在变量定义的地方进行

2. bitdata位寻址变量

  • 特点:可以单独访问一个字节中的每一位
  • 应用场景:LED控制、状态标志位等
  • 示例
    u8 led;  // 定义一个8位变量
    sbit led0 = led^0;  // 定义第0位
    sbit led1 = led^1;  // 定义第1位
    // ...以此类推
    

3. static静态变量

  • 特点:只在第一次进入函数时初始化一次
  • 应用场景:需要保持状态计数的函数
  • 示例
    void func() {
        static u8 count = 0;  // 只初始化一次
        count++;  // 每次调用加1
    }
    

LED与数码管刷新

1. 引脚定义规范

#define SEG_PORT P6  // 数码管段选引脚
#define COM_PORT P7  // 数码管位选引脚
#define LED_POWER P40 // LED电源控制引脚

2. 数码管显示数据结构

  • 0-9数字段码(10字节)
  • 0-9带小数点段码(10字节)
  • 全部隐藏(1字节)
  • 总计21字节的查找表

3. 刷新函数设计

  • 思路:在定时器中断中轮流刷新8位数码管和LED
  • 实现方法
    void refresh_display() {
        static u8 nb = 0;  // 静态变量,保持状态
    
        if(nb <= 7) {
            // 刷新数码管
            // 1. 关闭LED电源
            // 2. 关闭所有数码管
            // 3. 选择当前数码管位
            // 4. 输出对应段码
        }
        else if(nb == 8) {
            // 刷新LED
            // 1. 关闭数码管
            // 2. 打开LED电源
            // 3. 输出LED状态
        }
        else {
            // 关闭所有显示
        }
    
        nb++;
        if(nb >= 10) nb = 0;  // 循环计数
    }
    

4. 变量定义

// 在led_seg.c中定义
u8 seg_data[8];  // 数码管显示数据
u8 led_data;     // LED显示数据(8位对应8个LED)

// 在led_seg.h中声明为外部变量
extern u8 seg_data[8];
extern u8 led_data;
  1. 模块化组织:一个功能对应一个.c/.h文件对
  2. 详细注释:每个函数添加完整注释头
  3. 引脚宏定义:硬件相关参数集中定义,便于移植
  4. 变量外部声明:跨文件使用的变量用extern声明
  5. 代码层次清晰:合理使用if-else if-else结构
  6. 状态保持:适当使用static变量保持函数状态
  7. 显示刷新:在定时中断中统一处理显示刷新
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:5
  • 最近打卡:2026-01-20 17:19:10
已绑定手机

3

主题

32

回帖

91

积分

注册会员

积分
91
发表于 2026-1-20 19:17:28 | 显示全部楼层

第13集:简易多任务处理下课程笔记

LED与数码管模块深入解析

1. 位寻址变量 (bitdata)

  • 作用:允许单独访问一个字节中的每一位
  • 应用场景:LED状态控制、标志位管理
  • 定义方法
    bdata u8 led_data;  // 定义位寻址变量
    sbit led0 = led_data ^ 0;  // 定义第0位
    sbit led1 = led_data ^ 1;  // 定义第1位
    // ... 以此类推,直到第7位
    

2. 批量编辑技巧

  • 变量重命名:选中变量名 → 右键 → Replace → 选择整个工程范围
  • 批量操作:Alt+Shift + 鼠标拖动,可实现多行同时编辑
  • 应用场景:批量添加extern修饰符、添加分号等

3. 引脚定义规范

// 硬件引脚定义(便于移植)
#define SEG_PORT    P6  // 数码管段选引脚
#define COM_PORT    P7  // 数码管位选引脚  
#define LED_POWER   P40 // LED电源控制引脚

// 位寻址变量定义
bdata u8 led_data;      // LED控制变量(8位对应8个LED)
sbit led0 = led_data ^ 0;  // LED0控制位
sbit led1 = led_data ^ 1;  // LED1控制位
// ... 共8个LED

bdata u8 seg_data[8];   // 数码管显示数据(8个数码管)
sbit seg0 = seg_data[0] ^ 0;  // 数码管0控制位
// ... 每个数码管可单独控制

4. 刷新函数设计优化

/**
 * 函数名称:seg_led_show
 * 函数功能:循环刷新数码管和LED
 * 入口参数:无
 * 返回参数:无
 * 当前版本:1.0
 * 作者:
 * 修改日期:2023-01-01
 * 其他备注:前8ms刷新8位数码管,第9ms刷新LED,第10ms全部关闭
 *           该函数需1ms调用一次
 */
void seg_led_show(void) {
    static u8 nb = 0;  // 静态变量,保持状态
  
    if (nb <= 7) {
        // 刷新数码管(前8ms)
        LED_POWER = 1;          // 关闭LED电源
        COM_PORT = 0xFF;        // 关闭所有数码管
        COM_PORT = ~(1 << nb);  // 选中当前数码管位
        SEG_PORT = seg_code[seg_data[nb]]; // 显示对应数字
    }
    else if (nb == 8) {
        // 刷新LED(第9ms)
        COM_PORT = 0xFF;        // 关闭所有数码管
        LED_POWER = 0;          // 打开LED电源
        SEG_PORT = ~led_data;   // 显示LED状态
    }
    else {
        // 全部关闭(第10ms)
        LED_POWER = 1;          // 关闭LED电源
        COM_PORT = 0xFF;        // 关闭所有数码管
    }
  
    nb++;
    if (nb >= 10) nb = 0;  // 循环计数
}

5. 静态变量 (static) 的重要性

  • 特点:只在第一次进入函数时初始化
  • 作用:保持函数内部状态,实现轮流刷新
  • 注意事项:去掉static会导致每次调用都从0开始

三、按键模块完整实现

1. 按键状态定义

// 按键状态枚举(k.h中定义)
typedef enum {
    KEY_NO_PRESS = 0,   // 按键未按下
    KEY_SHAKE    = 1,   // 按键消抖中(<30ms)
    KEY_PRESS    = 2,   // 按键按下(=30ms)
    KEY_OVER     = 3,   // 单击结束(30-3000ms)
    KEY_LONG     = 4,   // 长按3秒(=3000ms)
    KEY_LONG_OVER= 5,   // 长按结束(>3000ms)
    KEY_RELEASE  = 6    // 按键松开
} KEY_STATE;

2. 按键引脚定义

// 按键引脚定义(k.h中)
#define KEY_PORT   P3      // 按键所在端口
#define KEY0_PIN   P32     // 按键0引脚
#define KEY1_PIN   P33     // 按键1引脚  
#define KEY2_PIN   P34     // 按键2引脚
#define KEY3_PIN   P35     // 按键3引脚

3. 核心函数设计

3.1 按键状态检测函数

/**
 * 函数名称:key_scan
 * 函数功能:检测所有按键状态(10ms调用一次)
 * 入口参数:无
 * 返回参数:无
 */
void key_scan(void) {
    u8 i;
    static u16 key_count[8] = {0};  // 8个按键的计数数组
    static u8 key_last_state = 0;   // 上一次按键状态
  
    for (i = 0; i < 8; i++) {
        // 检查当前按键是否按下(取反:按下为1,松开为0)
        if (~KEY_PORT & (1 << i)) {
            // 按键按下,计数增加(不超过60000)
            if (key_count[i] < 60000) {
                key_count[i]++;
            }
            // 设置上一次按下标志
            key_last_state |= (1 << i);
        } else {
            // 按键松开,计数清零
            key_count[i] = 0;
            // 清除上一次按下标志
            key_last_state &= ~(1 << i);
        }
    }
}

3.2 按键状态查询函数

/**
 * 函数名称:key_get_state
 * 函数功能:获取指定按键的当前状态
 * 入口参数:key - 按键编号(0-7)
 * 返回参数:按键状态(KEY_STATE枚举值)
 */
KEY_STATE key_get_state(u8 key) {
    static u16 key_count[8] = {0};  // 需要与key_scan共用
  
    // 判断按键是否按下
    if (~KEY_PORT & (1 << key)) {
        // 按键按下状态判断
        if (key_count[key] < 3)        // <30ms(10ms检测一次)
            return KEY_SHAKE;
        else if (key_count[key] == 3)   // =30ms
            return KEY_PRESS;
        else if (key_count[key] < 300)  // <3000ms
            return KEY_OVER;
        else if (key_count[key] == 300) // =3000ms
            return KEY_LONG;
        else                            // >3000ms
            return KEY_LONG_OVER;
    } else {
        // 按键松开状态判断
        if (key_last_state & (1 << key))  // 之前按下过
            return KEY_RELEASE;
        else                               // 一直没按下
            return KEY_NO_PRESS;
    }
}

4. 循环语句 (for) 详解

// for循环语法
for (初始化; 条件判断; 更新表达式) {
    // 循环体
}

// 示例:循环8次检测8个按键
for (u8 i = 0; i < 8; i++) {
    // i从0到7依次执行
    // 第1次:i=0,判断0<8成立,执行循环体,然后i++变成1
    // 第2次:i=1,判断1<8成立,执行循环体,然后i++变成2
    // ...
    // 第8次:i=7,判断7<8成立,执行循环体,然后i++变成8
    // 第9次:i=8,判断8<8不成立,退出循环
}

5. 位运算应用

// 1. 左移运算:1 << i
// i=0 → 0x01 (0000 0001)
// i=1 → 0x02 (0000 0010)
// i=2 → 0x04 (0000 0100)
// i=3 → 0x08 (0000 1000)

// 2. 按位与:判断特定位是否为1
if (KEY_PORT & (1 << i)) {
    // 第i位为1(高电平)
}

// 3. 按位取反:~KEY_PORT
// 按键按下为低电平(0),取反后变成1,便于判断

// 4. 按位或:设置特定位为1
key_last_state |= (1 << i);  // 将第i位设为1

// 5. 按位与取反:清除特定位
key_last_state &= ~(1 << i); // 将第i位清0

主程序调用示例

#include "led_seg.h"
#include "key.h"

void main() {
    // 初始化
    timer_init();           // 定时器初始化
    led_data = 0xFF;        // LED初始全灭
    seg_data[0] = 0;        // 数码管0显示0
    seg_data[1] = 1;        // 数码管1显示1
    // ... 其他数码管初始化
  
    // 定义LED控制位(使用位寻址变量)
    led0 = 0;  // LED0点亮
    led7 = 1;  // LED7熄灭
  
    while(1) {
        // 每10ms检测一次按键
        delay_ms(10);
        key_scan();  // 检测按键状态
    
        // 检查按键1状态
        KEY_STATE state = key_get_state(1);
        if (state == KEY_PRESS) {
            // 按键1单击,切换LED0状态
            led0 = ~led0;
        }
        else if (state == KEY_LONG) {
            // 按键1长按3秒,点亮LED7
            led7 = 0;
        }
        else if (state == KEY_RELEASE) {
            // 按键1松开,熄灭LED7
            led7 = 1;
        }
    }
}

// 定时器中断服务函数(1ms一次)
void timer_isr() interrupt 1 {
    seg_led_show();  // 刷新数码管和LED
}
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:5
  • 最近打卡:2026-01-20 17:19:10
已绑定手机

3

主题

32

回帖

91

积分

注册会员

积分
91
发表于 2026-1-20 19:20:16 | 显示全部楼层

第十三集:简易多任务处理终


1. 状态机替代忙等待

  • 传统按键检测使用 while循环等待松开,会阻塞程序运行。
  • 改进方案:使用状态机实时读取按键状态,不阻塞其他任务。
  • 优点:程序响应快,可同时处理多个任务。

2. 模块化编程实践

  • 将不同功能拆分为独立模块(如按键、蜂鸣器、定时器)。
  • 每个模块包含:
    • 头文件(.h):函数声明、宏定义
    • 源文件(.c):函数实现
  • 便于代码复用、维护和协作开发。

3. 蜂鸣器模块化设计

  • 函数设计:
    • Buzzer_On(time):设置蜂鸣器响铃时长(单位:10ms)
    • Buzzer_Off():立即关闭蜂鸣器
    • Buzzer_Run():在定时中断中调用,控制蜂鸣器计时
  • 使用一个计时变量(如 tb)控制蜂鸣器状态,实现非阻塞式响铃

4. 定时器中断与多任务调度

  • 利用定时器中断(如1ms中断一次)生成时间基准。
  • 在中断中设置标志位(如 flag_10ms),表示10ms时间到。
  • 在主循环中检测标志位,执行需要周期运行的任务(如按键扫描、蜂鸣器计时、LED闪烁)。

5. 代码注释与规范

  • 函数头注释应包含:
    • 函数名称、功能
    • 入口参数、返回值
    • 作者、日期
  • 注释对齐、清晰,便于他人阅读和维护。
回复

使用道具 举报 送花

  • 打卡等级:初来乍到
  • 打卡总天数:5
  • 最近打卡:2026-01-20 17:19:10
已绑定手机

3

主题

32

回帖

91

积分

注册会员

积分
91
发表于 2026-1-20 19:22:32 | 显示全部楼层

第十四集:矩阵按键


1. 矩阵按键 vs 独立按键

类型 IO口占用 按键数量 特点
独立按键 1个IO对应1个按键 N个按键需N个IO 编程简单,IO占用多
矩阵按键 M行 + N列 → M+N个IO 最多 M×N 个按键 节省IO,编程复杂
  • 例如:4行 + 4列 = 8个IO → 最多16个按键。

2. 矩阵按键扫描原理(行扫描法)

  • 第一步:所有行输出高电平,所有列输出低电平。若有按键按下,对应行会被拉低,读取行值即可知道按键所在行。
  • 第二步:所有列输出高电平,所有行输出低电平。若有按键按下,对应列会被拉低,读取列值即可知道按键所在列。
  • 第三步:结合行值与列值,确定具体按键位置。

3. 矩阵按键编程关键技巧

  • 去抖动:按键按下后延时一小段时间再读取,避免抖动干扰。
  • 状态记忆:使用静态变量记录上一次按键状态,只有状态变化时才返回有效键值,防止重复触发。
  • 键值映射:通过 switch语句或查找表,将扫描得到的原始键值(如 0x41)映射为逻辑键值(如“按键1”)。

矩阵按键读取函数示例(简化版)

// 矩阵按键读取函数,返回0表示无按键,1~8表示按键1~8
uint8_t MatrixKey_Read(void) {
    static uint8_t last_key = 0;  // 上一次按键状态
    uint8_t key_value = 0;
    uint8_t row_val, col_val;

    // 第一步:行输出高电平,列输出低电平
    P0 = 0xC0;  // P0.6、P0.7高,其余低
    Delay_us(10);  // 短延时去抖动
    row_val = P0 & 0xC0;  // 读取行状态(P0.6、P0.7)

    // 第二步:列输出高电平,行输出低电平
    P0 = 0x0F;  // P0.0~P0.3高,其余低
    Delay_us(10);
    col_val = P0 & 0x0F;  // 读取列状态(P0.0~P0.3)

    // 第三步:合并行列值,得到原始键值
    key_value = row_val | col_val;

    // 第四步:键值转换为逻辑键值(1~8)
    switch (key_value) {
        case 0x41: return 1;  // 按键1
        case 0x42: return 2;  // 按键2
        case 0x44: return 3;  // 按键3
        case 0x48: return 4;  // 按键4
        case 0x81: return 5;  // 按键5
        case 0x82: return 6;  // 按键6
        case 0x84: return 7;  // 按键7
        case 0x88: return 8;  // 按键8
        default:   return 0;  // 无按键
    }

    // 状态记忆:只有按键变化时才返回有效值
    if (key_value != last_key) {
        last_key = key_value;
        return key_value;
    } else {
        return 0;
    }
}

实战项目:简易密码锁

项目要求

  1. 使用 LED0模拟门锁状态:亮=开锁,灭=上锁。
  2. 八位数码管初始显示“--------”(8个横杠)。
  3. 通过矩阵按键输入1~8数字作为密码,并在数码管上依次显示。
  4. 每输入一个数字,蜂鸣器响20ms作为反馈。
  5. 输入8位密码后自动验证:
    • 密码正确:LED0点亮(开锁)。
    • 密码错误:蜂鸣器响2秒,数码管清空并重新显示“--------”。

关键代码逻辑(主循环中)

uint8_t key = MatrixKey_Read();  // 读取按键
if (key > 0 && key <= 8) {      // 有有效按键
    Buzzer_On(2);               // 蜂鸣20ms(2×10ms)
    password[input_index] = key; // 保存密码
    Seg_Display[input_index] = key; // 数码管显示
    input_index++;

    if (input_index >= 8) {     // 已输入8位密码
        if (CheckPassword(password)) {
            LED0 = 0;           // 开锁(LED亮)
        } else {
            Buzzer_On(200);     // 蜂鸣2秒(200×10ms)
            ResetDisplay();     // 清空数码管
        }
        input_index = 0;        // 重置输入位置
    }
}

密码验证函数(可优化为for循环)

uint8_t CheckPassword(uint8_t pwd[]) {
    if (pwd[0]==1 && pwd[1]==1 && pwd[2]==1 && pwd[3]==1 &&
        pwd[4]==1 && pwd[5]==1 && pwd[6]==1 && pwd[7]==1) {
        return 1;  // 密码正确(示例密码:8个1)
    }
    return 0;  // 密码错误
}
回复

使用道具 举报 送花

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|手机版|深圳国芯人工智能有限公司 ( 粤ICP备2022108929号-2 )

GMT+8, 2026-2-12 18:02 , Processed in 0.115852 second(s), 83 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表