找回密码
 立即注册
查看: 49|回复: 7

8051U深度入门到32位51大型实战教学视频-学习打卡

[复制链接]
  • 打卡等级:偶尔看看II
  • 打卡总天数:23
  • 最近打卡:2026-04-07 08:56:54
已绑定手机

3

主题

54

回帖

173

积分

注册会员

积分
173
发表于 2026-3-30 00:26:11 | 显示全部楼层 |阅读模式

打卡第一集-序言

8051U强在哪里
1.屏幕显示和视频播放(flash编程器)
2.IIS录放音3.PWM_DMA
4.频谱分析仪(上位机)
5.手写计算器.
6.QSPI,PWM移相,硬件乘除,单精度浮点

Ai8051U,USB 型 1T 8051,支持32位和8位指令集•RMB2.3
管脚兼容天王级别的!89C52RC,12C5A60S2
要兼容8位8051指令集,可以用 Keil C51/IAR/SDCC 编译器
===就相当于更强大的8H8K64U
要兼容 32位8051指令集:可以用 Keil C251编译器,双核兼容设计
= =就相当于更强大的32G12K128,32G8K64
34K SRAM(2K edata, 32K xdata), 64K Flash
TFPU@120MHz,硬件浮点/硬件三角函数 运算器
DMA支持PWM, DMA支持外设直接到外设,P2P
120MHz-PWM支持硬件移相,16位PWM;真12位ADC
USB, 4组串口,12位ADC,轨到轨比较器
QSPI, SPI, I2S, I2C, TFT-18080/M6800 接口
PDIP40,LOFP44, LQFP48

回复

使用道具 举报 送花

  • 打卡等级:偶尔看看II
  • 打卡总天数:23
  • 最近打卡:2026-04-07 08:56:54
已绑定手机

3

主题

54

回帖

173

积分

注册会员

积分
173
发表于 2026-3-30 21:01:34 | 显示全部楼层

打卡第二集-硬件及工具介绍

image.png

image.png

image.png

回复

使用道具 举报 送花

  • 打卡等级:偶尔看看II
  • 打卡总天数:23
  • 最近打卡:2026-04-07 08:56:54
已绑定手机

3

主题

54

回帖

173

积分

注册会员

积分
173
发表于 2026-3-30 23:13:13 | 显示全部楼层

打卡第三集-点亮第一颗LED

新建工程

1.创建空工程

2.添加头文件

3.输入如下代码,并编译


在单片机(嵌入式)开发中,头文件(.h 文件)扮演着至关重要的角色。它就像是硬件与软件之间的“说明书”和“接线员”,告诉编译器如何正确操作硬件。

以下是关于单片机头文件的详细解析,涵盖其作用、常见分类、内容结构以及使用技巧。

1. 头文件的核心作用

  • 硬件抽象:将单片机内部复杂的寄存器地址映射为有意义的符号(如 P1ADC1)。开发者不需要记住 0xE000E010 这样的地址,直接写 GPIOA->ODR 即可。
  • 函数声明:告诉编译器库函数的存在(如 HAL_Delay()),确保在调用时参数和返回值类型正确。
  • 宏定义:定义时钟频率、位操作(如 SET_BIT)、或条件编译开关。
  • 类型定义:统一数据类型(如 uint8_t),提高代码在不同平台间的可移植性。

2. 常见的单片机头文件分类

根据开发方式和厂商不同,头文件主要分为以下三类:

A. 厂商提供的标准库头文件

这是目前最主流的方式,由芯片厂商(ST、Microchip、NXP等)提供。

厂商 芯片系列 核心头文件 说明
ST STM32 stm32f1xx.h 标准外设库 (SPL) 或 硬件抽象层库 (HAL) 的入口。
ST STM32CubeMX main.h 由CubeMX工具生成,包含引脚配置、时钟树定义。
STC 51系列 reg51.h / reg52.h 定义了8051内核的特殊功能寄存器(SFR),如 P0, TCON
Microchip AVR (Arduino) avr/io.h 根据编译器自动选择正确的芯片寄存器定义文件。
ESP ESP32/8266 esp32-hal.h ESP-IDF或Arduino框架下,包含WiFi、GPIO等外设操作。

B. 内核与编译器相关头文件

无论是什么单片机,只要使用C语言开发,通常都需要以下头文件:

  • stdint.h:标准整数类型。定义了 uint32_t(无符号32位整型)、int8_t 等。强烈建议使用这类定长类型,而不是 intunsigned int,因为不同单片机的字长不同。
  • stdbool.h:定义了布尔类型 truefalse
  • string.h:提供 memcpymemset 等内存操作函数。
  • math.h:提供数学运算(注意在单片机中启用数学库可能会增加代码体积)。

C. 用户自定义头文件

用于工程模块化管理。例如:

  • bsp_led.h:板级支持包,对外提供 LED_On() 等接口,隐藏底层GPIO操作。
  • protocol.h:通信协议的结构体定义和枚举。

3. 一个典型的单片机头文件结构

以 STM32 HAL 库生成的 main.h 为例:

c

/* 1. 防重复包含保护 */
#ifndef __MAIN_H
#define __MAIN_H

/* 2. 包含其他头文件 */
#include "stm32f1xx_hal.h"
#include "stdio.h"

/* 3. 宏定义 (引脚映射、时钟、常量) */
#define LED1_PIN       GPIO_PIN_5
#define LED1_PORT      GPIOA
#define BSP_LED_On()   HAL_GPIO_WritePin(LED1_PORT, LED1_PIN, GPIO_PIN_RESET)
#define BSP_LED_Off()  HAL_GPIO_WritePin(LED1_PORT, LED1_PIN, GPIO_PIN_SET)

#define SYSTEM_CLOCK   72000000  // 72MHz

/* 4. 全局变量声明 (注意使用 extern) */
extern UART_HandleTypeDef huart1;

/* 5. 函数声明 */
void SystemClock_Config(void);
void MX_GPIO_Init(void);
void Error_Handler(void);

#endif /* __MAIN_H */

4. 关键技巧与注意事项

防重复包含

在头文件开头和结尾一定要有:

c

#ifndef _MY_HEADER_H
#define _MY_HEADER_H
// ... 代码 ...
#endif

或者使用 #pragma once(大多数现代编译器支持),防止在多个 .c 文件中包含同一个头文件导致的重复定义错误。

extern 的使用

如果在 main.c 中定义了一个全局变量 int counter;,若要在 uart.c 中使用它,必须在其头文件或 uart.c 中声明:

c

extern int counter; // 告诉编译器:这个变量在其他地方定义,链接时再找

切忌在头文件中直接定义变量,否则会导致“multiple definition”链接错误。

路径设置

编译器需要知道头文件在哪里。

  • 在 Keil/IAR 中:需要在工程设置(Options)的 C/C++ 选项卡中添加包含路径(Include Paths)。
  • 在 GCC (Makefile) 中:使用 -I 参数,如 -I./Inc -I./Drivers/STM32F1xx_HAL_Driver/Inc

条件编译

利用头文件进行配置,例如在 config.h 中:

c

#define USE_FREERTOS   1   // 使能 RTOS
#define DEBUG_ENABLE   1   // 使能调试打印

#if (DEBUG_ENABLE == 1)
    #define DEBUG_PRINT(...)    printf(__VA_ARGS__)
#else
    #define DEBUG_PRINT(...)
#endif

5. 常见问题排查

  • 找不到头文件 (fatal error: xxx.h: No such file or directory):通常是因为包含路径未添加,或者头文件路径写错了。
  • 重复定义 (error: L6200E: Symbol xxx multiply defined):一般在头文件里定义了全局变量,而没有加 static 或没有放在 .c 文件中。
  • 未定义的类型名 (error: unknown type name ‘GPIO_TypeDef’):通常是因为头文件的包含顺序错误。例如,在 bsp_led.h 中使用了 GPIO_TypeDef,但没有包含 stm32f1xx.h

image.png

image.png

回复

使用道具 举报 送花

  • 打卡等级:常住居民I
  • 打卡总天数:74
  • 最近打卡:2026-04-03 08:37:07
已绑定手机

14

主题

143

回帖

986

积分

版主

积分
986
发表于 2026-3-31 08:30:33 | 显示全部楼层
要 做到 USB不停电下载
要 尝试 AiCube 图形化自动配置生成程序工具
推荐优先看的:  
printf_usb("Hello World !\r\n")
USB不停电下载, 演示视频链接:
https://www.stcaimcu.com/thread-19077-1-1.html

下载 最新的 AiCube-ISP-V6.96T 或以上版本软件 !

深圳国芯人工智能有限公司-工具软件

下载 最新的 USB库函数,永远用最新的 USB库函数 !
深圳国芯人工智能有限公司-库函数
下载 最新的 用户手册 !
下载 最新的 上机实践指导书 !

下载 最新的 Ai8051U 用户手册
https://www.stcaimcu.com/data/download/Datasheet/AI8051U.pdf

下载 最新的 Ai8051U 实验指导书,
AiCube 图形化自动配置生成程序工具使用说明
https://www.stcaimcu.com/data/do ... %AF%BC%E4%B9%A6.pdf


推荐优先看的 printf_usb("Hello World !\r\n")及usb不停电下载, 演示视频链接



回复

使用道具 举报 送花

  • 打卡等级:偶尔看看II
  • 打卡总天数:23
  • 最近打卡:2026-04-07 08:56:54
已绑定手机

3

主题

54

回帖

173

积分

注册会员

积分
173
发表于 2026-3-31 23:57:38 | 显示全部楼层

打卡第四集-USB不停电下载

USB不停电下载

1.实验对比演示

2.下载所需文件(STC官网-软件工具-库函数-USB库文件)

3.移植关键部分到工程:

3.1 添加头文件

3.2 USB初始化函数(lib+.h库实现)

3.3 命令参数

3.4 打开P_SW2寄存器和IE2寄存器(只打开一个位!)

image.png

image.png

image.png

回复

使用道具 举报 送花

  • 打卡等级:偶尔看看II
  • 打卡总天数:23
  • 最近打卡:2026-04-07 08:56:54
已绑定手机

3

主题

54

回帖

173

积分

注册会员

积分
173
发表于 2026-4-1 09:15:11 | 显示全部楼层

打卡第五集-C语言基础

C语言 USB-CDC串口之printf函数的实现

1.打开USB库中的PRINTF_HID宏定义(去掉//)

2.理解PRINTF的函数原型的定义

#define printf printf_hid

int printf_hid (const char *fmt, ...);

参数fmt -- 是格式控制字符串,包含了两种类型的对象:普通字符和转换说明 。

普通字符:在输出时,普通字符将原样不动地复制到标准输出。

printf("8051U深度入门到32位51大型实战视频\r\n");

转换说明:不直接输出,用于控制 printf 中参数的转换和打印。每个转换说明都由一个百分号字符(%)开始,以转换说明符结束,从而说明输出数据的类型、宽度、精度等。

printf("8051U深度入门到32位51大型实战视频,%s\\r\\n","加油");

转换说明简介:

1.类型:根据不同的 fmt 字符串,函数可能需要一系列的附加参数,每个

参数包含了一个要被插入的值,替换了 fmt 参数中指定的每个 % 标签。

关于附加参数,既可以是变量,也可以是常量。

2.位置:printf()函数的普通字符和转换说明放在" "双引号内,附加参数

放在双引号外,每个附加参数之间用逗号隔开。

3.数量:printf() 的附加参数与转换说明符是⼀⼀对应关系,如果有 n 个转

换说明符, printf() 的参数就应该有 n + 1 个。如果参数个数少于对应的转

换说明符,printf() 可能会输出内存中的任意值。


image.png

image.png


C语言的基本数据类型(也称为内置类型)是构建程序的基础。它们分为整型浮点型字符型void 型四类。下面分别介绍。


1. 整型(Integer Types)

整型用于存储整数,分为有符号(signed)和无符号(unsigned)两种。默认情况下 intshortlong 都是有符号的。

类型 常用大小(字节) 取值范围(典型) 格式说明符
char 1 -128~ 127 或 0 ~ 255(取决于符号性) %c, %hhd
signed char 1 -128~ 127 %hhd
unsigned char 1 0~ 255 %hhu
short(或 short int 2 -32,768~ 32,767 %hd
unsigned short 2 0~ 65,535 %hu
int 4(常用) -2,147,483,648~ 2,147,483,647 %d
unsigned int 4 0~ 4,294,967,295 %u
long(或 long int 4 或 8(视平台) 至少 -2,147,483,647~ 2,147,483,647 %ld
unsigned long 4 或 8 至少 0~ 4,294,967,295 %lu
long long(C99起) 8 -9.22×10¹⁸~ 9.22×10¹⁸ %lld
unsigned long long 8 0~ 1.84×10¹⁹ %llu

注意:实际字节数由编译器和平台决定。可以用 sizeof(类型) 获取,或用 <limits.h> 中的宏(如 INT_MAX)得到准确范围。


2. 浮点型(Floating-Point Types)

用于存储带小数部分的数值。

类型 常用大小(字节) 精度 格式说明符
float 4 约 7 位十进制 %f
double 8 约 15 位十进制 %lf(输入)/ %f(输出)
long double 8、12 或 16 依平台而定 %Lf
  • float 通常用于节省内存,但精度较低。
  • double 是默认的浮点类型,足够大多数场景。
  • long double 提供更高精度,但可能因平台不同而表现不同。

3. 字符型(Character Type)

char 本质上是整型,用于存储单个字符(ASCII 或 UTF-8 等编码)。字符常量用单引号括起,如 'A'

  • 字符在内存中以整数形式存储(例如 'A' 对应 65)。
  • 可以用 %c 输出字符,或用 %d 输出其 ASCII 码。

c

char ch = 'A';
printf("%c = %d\n", ch, ch);  // 输出: A = 65

4. void 类型

void 表示“无类型”,主要用于:

  • 函数无返回值:void func(void);
  • 通用指针:void * 可指向任意类型的数据。

5. 类型修饰符(Modifiers)

  • 符号修饰signed / unsigned 决定是否有符号。
  • 长度修饰shortlonglong long 改变数据长度。

可以组合使用:unsigned long long intint 可省略)。


6. 如何选择数据类型

  • 整数:优先用 int(通常是最快的自然宽度)。若需大范围,用 long long;若需节省内存或用于数组索引,用 shortunsigned
  • 浮点数:一般用 double,除非内存紧张或确定不需要高精度才用 float
  • 字符:用 char 处理文本,用 unsigned char 处理原始字节(如二进制数据)。

7. 示例:使用基本数据类型

c

#include <stdio.h>
#include <limits.h>

int main() {
    // 整型
    int a = 42;
    unsigned int b = 100u;
    long long big = 123456789012345LL;

    // 浮点
    double pi = 3.1415926535;

    // 字符
    char grade = 'A';

    // 打印大小
    printf("int 大小: %zu 字节\n", sizeof(int));
    printf("long long 大小: %zu 字节\n", sizeof(long long));
    printf("double 大小: %zu 字节\n", sizeof(double));

    // 使用 limits.h 中的常量
    printf("int 最大值: %d\n", INT_MAX);

    return 0;
}

8. 进制与数据类型的关联(承前)

  • 整型常量可以用十进制、十六进制(0x 前缀)或八进制(0 前缀)书写。
  • 在格式化输出时,printf%x 以十六进制显示整数的内存表示(对负数显示补码)。

c

int num = 0x2A;   // 十六进制字面量,赋值给 int
printf("%d\n", num);  // 输出 42
printf("%x\n", num);  // 输出 2a

C语言的运算符


1. 算术运算符

用于数值计算。

运算符 含义 示例
+ 加法 a + b
- 减法 a - b
* 乘法 a * b
/ 除法 a / b
% 取模(求余) a % b

注意

  • 两个整数相除,结果为整数(向零截断)。例如 5 / 22
  • 取模运算要求操作数均为整数,结果的符号与被除数相同:-5 % 2-1

2. 关系运算符

用于比较大小,结果为 1(真)或 0(假)。

运算符 含义 示例
== 等于 a == b
!= 不等于 a != b
< 小于 a < b
> 大于 a > b
<= 小于等于 a <= b
>= 大于等于 a >= b

常见错误:将 == 误写为 =(赋值),例如 if (x = 5) 会永远为真(因为赋值表达式结果为 5,非零)。


3. 逻辑运算符

用于组合多个条件,结果也为 1(真)或 0(假)。

运算符 含义 示例
&& 逻辑与 a && b
|| 逻辑或 a || b
! 逻辑非 !a

短路求值

  • && 若左操作数为假,不再计算右操作数。
  • || 若左操作数为真,不再计算右操作数。

c

int a = 0;
if (a && (++a > 0)) { }   // 右侧不会执行,a 保持 0

4. 位运算符

直接操作整数在内存中的二进制位。

运算符 含义 示例
& 按位与 a & b
| 按位或 a | b
^ 按位异或 a ^ b
~ 按位取反 ~a
<< 左移 a << n
>> 右移 a >> n

注意

  • 移位运算符对负数进行右移时,行为由编译器实现决定(通常为算术右移,即补符号位)。
  • 左移 n 位等价于乘以 2^n(不溢出时),右移等价于除以 2^n 的整数部分。

5. 赋值运算符

运算符 含义 示例
= 简单赋值 a = b
+= 加后赋值 a += ba = a + b
-= 减后赋值 类似
*= 乘后赋值
/= 除后赋值
%= 取模后赋值
&= 按位与后赋值
|= 按位或后赋值
^= 按位异或后赋值
<<= 左移后赋值
>>= 右移后赋值

赋值表达式的结果是赋值后左操作数的值,且具有右结合性,因此可以连续赋值:a = b = c = 0;


6. 自增自减运算符

运算符 含义 示例
++ 自增(加1) i++(后缀)、++i(前缀)
-- 自减(减1) i----i

前缀与后缀的区别

  • 前缀:先运算,后使用值。
  • 后缀:先使用值,后运算。

c

int i = 5, j;
j = ++i;   // i 先变为 6,再赋值给 j,所以 i=6, j=6
i = 5;
j = i++;   // 先赋值 j=5,再 i 自增为 6,所以 i=6, j=5

7. 条件(三目)运算符

表达式1 ? 表达式2 : 表达式3

  • 先计算 表达式1,若为真,则执行 表达式2 并返回其值,否则执行 表达式3 并返回其值。

c

int max = (a > b) ? a : b;

8. 逗号运算符

依次计算多个表达式,整个表达式的值是最后一个表达式的值。

c

int a = (1, 2, 3);   // a 得到 3

常用于 for 循环中:

c

for (i=0, j=10; i<j; i++, j--) { ... }

9. sizeof 运算符

以字节为单位返回类型或对象的大小。结果为 size_t 类型(无符号整型)。

c

size_t size = sizeof(int);    // 通常是 4
int arr[10];
size_t arr_size = sizeof(arr); // 整个数组占用的字节数

注意sizeof 是运算符,不是函数,括号仅对类型必需。


10. 指针运算符

运算符 含义 示例
& 取地址 &var
* 解引用(间接访问) *ptr

c

int x = 10;
int *p = &x;   // p 指向 x
*p = 20;       // 通过指针修改 x

11. 类型转换运算符

C 语言支持隐式类型转换(自动提升)和显式强制转换

  • 隐式转换:混合运算时,小类型向大类型提升;有符号和无符号混合时可能产生意外结果。
  • 显式转换:(类型)表达式

c

double d = 3.14;
int i = (int)d;   // i = 3

12. 运算符优先级与结合性

  • 优先级:例如 */ 高于 +-() 最高。
  • 结合性:多数二元运算符为左结合(从左到右计算),赋值、条件、单目运算符为右结合(从右到左)。

常见陷阱

c

if (x & 1 == 0)   // 错误:`==` 优先级高于 `&`,实际为 x & (1==0) → x & 0 → 0

正确写法:if ((x & 1) == 0)

为避免错误,建议不确定优先级时使用括号


13. 总结表(常用运算符优先级,从高到低)

优先级 运算符 结合性
1 () [] -> . 左→右
2 ++ -- ! ~ + - * & (类型) sizeof 右→左
3 * / % 左→右
4 + - 左→右
5 << >> 左→右
6 < <= > >= 左→右
7 == != 左→右
8 & 左→右
9 ^ 左→右
10 | 左→右
11 && 左→右
12 || 左→右
13 ?: 右→左
14 = += -= *= 右→左
15 , 左→右

三种常用进制的表示法

在代码中,通过前缀来区分:

  • 二进制:前缀 0b0B
    • 例如:0b1010 (等于十进制的10)
    • 注:部分老旧编译器可能不支持,但在主流单片机IDE(如Keil、Arduino、STM32CubeIDE)中均支持。
  • 十六进制:前缀 0x
    • 例如:0x0A (等于十进制的10)
    • 这是单片机中最常用的写法,因为它和内存地址、寄存器位一一对应。
  • 十进制:无前缀,直接写数字
    • 例如:10

2. 为什么要用十六进制(最实用)

在单片机中,一个字节由8位二进制组成。如果用二进制写 11001011,看着很累;用十进制写 203,看不出每一位是0还是1。

十六进制的优势: 1位十六进制数 = 4位二进制数
因此,1个字节(8位) 可以用 2位十六进制数 完美表示,非常直观:

  • 0b10101010 (二进制,看着眼花) → 0xAA (十六进制,简洁)
  • 0x37 拆开:高4位是 30011),低4位是 70111),组合成 00110111

3. 常用换算与位运算

在单片机编程中,你很少需要手动计算“十进制转二进制”的数学题,更多的是通过位运算直接操作寄存器。

  • 设置某一位为1REG |= (1 << 2); (将第2位设为1,其余不变)
  • 清除某一位为0REG &= ~(1 << 3); (将第3位清0)
  • 取反某一位REG ^= (1 << 5);

记住常见的十六进制掩码:

  • 0x01 = 0b0000 0001
  • 0x0F = 0b0000 1111
  • 0xFF = 0b1111 1111 (即255,常用于清除一个字节或全设置为高电平)

4. 总结

  1. 看前缀识进制0x 是十六进,0b 是二进,没前缀是十进。
  2. 写寄存器用十六进:因为 0xFF255 更能直接反映8个引脚的高低电平状态。
  3. 移位操作代替乘法(1 << n)pow(2, n) 快得多,且直接对应位位置。
回复

使用道具 举报 送花

  • 打卡等级:偶尔看看II
  • 打卡总天数:23
  • 最近打卡:2026-04-07 08:56:54
已绑定手机

3

主题

54

回帖

173

积分

注册会员

积分
173
发表于 2026-4-1 14:04:53 | 显示全部楼层

打卡第六集-I/O输入输出

GPIO(General Purpose I/O Ports)意思为通用输入/输出端口,通俗地说,就是一些引脚,可以通过它们输出高低电平或者通过它们读入引脚的状态-是高电平或是低电平。

高电平就是指接近于电源正极电压的电平;也叫逻辑“1”;

单片机输出高电平就是输出VCC电压,输出低电平就是输出GND的电压。

image.png


GPIOGeneral-Purpose Input/Output(通用输入输出)的缩写。它是嵌入式系统、微控制器(如Arduino、ESP32、树莓派)以及各种芯片上最常见的一种接口。

简单来说,GPIO 就是芯片上的通用引脚,你可以通过软件将它们动态配置为不同的功能。它就像设备的“触角”,负责与外部世界(传感器、开关、LED、电机等)进行交互。

以下是 GPIO 的核心特性和工作模式:

1. 核心工作模式

GPIO 引脚通常可以配置为以下几种状态:

A. 输入模式

用于读取外部信号(如读取按键是否按下、传感器的电平)。

  • 浮空输入:引脚未连接外部电路时,电平状态不确定(易受电磁干扰),常用于带外部上拉/下拉电阻的场景。
  • 上拉输入:芯片内部连接一个电阻到高电平(VCC)。默认状态下引脚为高电平(1),当外部接地时,引脚变为低电平(0)。
  • 下拉输入:芯片内部连接一个电阻到地(GND)。默认状态下引脚为低电平(0),当外部接高电平时,引脚变为高电平(1)。
  • 模拟输入:用于读取连续的电压值(0V ~ 参考电压),通常连接传感器(如光敏电阻、电位器)时使用。此时 GPIO 不再是单纯的数字“0/1”,而是通过模数转换器(ADC)读取具体数值。

B. 输出模式

用于控制外部设备。

  • 推挽输出:具备较强的驱动能力,能直接输出高电平(灌电流)或低电平(拉电流),适合驱动 LED、数字逻辑电路等。结构简单,但不能将多个输出并联(除非使用外部电路),否则可能导致短路。
  • 开漏输出:只能输出低电平或高阻态。要输出高电平,必须在外部接上拉电阻。这种模式常用于需要电平转换(例如 3.3V 设备控制 5V 设备)或多设备共用总线(如 I2C 总线)的场景,避免多个设备同时输出时产生冲突。

C. 复用功能

现代芯片的引脚往往具有“多功能”。GPIO 引脚除了作为普通输入输出外,还可以配置为特定的硬件外设接口,如:

  • UART(串口通信)
  • I2C(总线通信)
  • SPI(高速串行通信)
  • PWM(脉宽调制,用于控制电机转速、LED 调光等)

2. 关键电气特性(使用时需注意)

  • 耐压值:大多数微控制器(如 ESP32、STM32)的 GPIO 是 3.3V 逻辑。如果直接接入 5V 信号,可能会烧毁芯片(除非标注“5V tolerant”即兼容5V)。
  • 驱动电流:单个 GPIO 引脚能输出的电流有限(通常为 20mA~40mA)。不能直接用 GPIO 驱动大功率电机或大功率 LED,需要配合三极管或继电器使用。
  • 中断:支持中断的 GPIO 可以在外部信号变化(如上升沿、下降沿)时,立即打断当前程序执行,适合处理需要快速响应的场景(如编码器、急停按钮)。

3. 常见应用场景

  • 数字输入:读取按键状态、红外传感器触发信号、霍尔传感器信号。
  • 数字输出:点亮 LED、控制继电器开关、发出蜂鸣器报警音。
  • 模拟输入:读取电位器旋转角度、土壤湿度传感器值。
  • 通信总线:通过复用功能连接显示屏、SD卡、传感器模块。
  • PWM 输出:调节 LED 亮度、控制舵机角度、调节直流电机转速。

按键输入检测

1. 硬件连接

1.1 常用电路:上拉电阻或下拉电阻

微控制器的GPIO引脚在输入模式下,必须有一个确定的电平状态(不能悬空)。通常通过外接或内部的上拉/下拉电阻来实现。

  • 上拉电阻方式(按键按下接地)
    引脚通过电阻连接到VCC(如3.3V或5V)。按键一端接引脚,另一端接地(GND)。

    • 未按下:引脚被上拉为高电平(逻辑1)。
    • 按下:引脚直接接地,变为低电平(逻辑0)。
    • 这种接法下,检测按键是否按下就是检测引脚是否为低电平。

    text

            VCC
             |
             R (上拉电阻,如10kΩ)
             |
          GPIO_Pin
             |
          /   \   (按键)
          —————
             |
            GND
    
  • 下拉电阻方式(按键按下接高电平)
    引脚通过电阻接地。按键一端接引脚,另一端接VCC。

    • 未按下:引脚被下拉为低电平(逻辑0)。
    • 按下:引脚接到VCC,变为高电平(逻辑1)。
    • 这种接法下,检测按键是否按下就是检测引脚是否为高电平。

注意:很多现代MCU(如Arduino、ESP32、STM32)的GPIO内部集成了可编程的上拉或下拉电阻,可以直接启用,无需外接电阻,简化电路。

1.2 硬件去抖

在按键两端并联一个小电容(如0.1μF)可以吸收部分抖动,是一种简单的硬件去抖方法。但完全依靠电容去抖效果有限,通常还需软件配合。


2. 软件去抖动原理

机械按键的抖动时间通常为 5~20ms。在这段时间内,引脚电平会多次跳变。软件去抖的核心思想是:检测到电平变化后,延迟一段时间(通常10~30ms),再次确认电平状态是否一致,如果一致则认为是有效按键,否则视为抖动。

2.1 简单延时去抖

cpp

int readButton(int pin) {
  if (digitalRead(pin) == LOW) {      // 检测到按下(假设按下为低)
    delay(20);                        // 等待抖动稳定
    if (digitalRead(pin) == LOW) {    // 再次确认还是按下
      return 1;                       // 有效按下
    }
  }
  return 0;
}

这种方式的缺点是 delay()会阻塞程序,不适合在需要同时处理其他任务的场合使用。

2.2 非阻塞状态机去抖

更常用的方法是使用状态机配合计时器,不阻塞主循环。通过记录上一次稳定的电平状态和电平变化的时间,来判断按键是否被按下或释放。

以下是一个典型的状态机逻辑(以按键按下接地为例):

  • 状态IDLE(空闲)、PRESSED(已按下待确认)、RELEASED(已释放待确认)
  • 在每个循环中读取当前电平。
  • 如果当前电平与上次稳定状态不同,记录当前时间(millis())。
  • 如果电平变化时间超过去抖阈值(如20ms),则更新稳定状态,并触发相应的事件。
回复

使用道具 举报 送花

  • 打卡等级:偶尔看看II
  • 打卡总天数:23
  • 最近打卡:2026-04-07 08:56:54
已绑定手机

3

主题

54

回帖

173

积分

注册会员

积分
173
发表于 2026-4-1 15:16:12 | 显示全部楼层

打卡第七集-定时器中断

定时器中断简单来说,它是硬件定时器根据预设的时间间隔,自动向CPU触发的中断请求

1. 工作原理(底层逻辑)

定时器本质是一个计数器,它连接着系统时钟(或外部晶振)。

  • 计数过程:每来一个时钟脉冲,计数器就加1(或减1)。
  • 产生中断:当计数器的值溢出(从设定值数到最大值)或达到比较值(匹配)时,硬件会自动将中断标志位拉高。
  • CPU响应:CPU在执行完当前指令后,检测到中断信号,会暂停当前正在执行的主程序,保存现场(压栈),然后跳转到预先设定好的中断服务函数中执行你的代码。

2. 关键参数:溢出与比较

定时器中断通常有两种触发模式,决定了它的用途:

  • 溢出中断:计数器从0开始加,加到最大值(如65535)溢出归零时触发。
    • 特点:实现固定的周期性中断,例如“每隔1ms进一次中断”。
  • 比较中断:计数器从0开始加,当计数值等于你设定的某个数值(如1000)时立即触发。
    • 特点:可以在一个计数周期内产生多个中断点,常用于生成精确的PWM波(脉宽调制波形)或控制步进电机的精确相位。

3. 定时器中断 vs. 软件延时

为什么我们要用定时器中断,而不是用 delay()while 循环?

  • 非阻塞性:这是最大的区别。
    • 软件延时:CPU在空转,无法做任何其他事情(比如响应按键、刷新屏幕)。
    • 定时器中断:CPU大部分时间在处理主逻辑,只有到了“时间到了”的那一刻,才抽空去处理一下中断任务,处理完立即回去继续干活。
  • 精确性:定时器依赖硬件晶振,精度极高(通常ppm级别),不受编译器优化或C语言指令执行时间波动的影响。

4. 典型应用场景

在单片机开发中,定时器中断几乎无处不在:

  • 操作系统节拍:RTOS(实时操作系统,如FreeRTOS)的心跳,用于任务切换和时间管理。
  • 精准调度:比如“每10ms采集一次传感器数据,每500ms刷新一次屏幕”。
  • 消抖与长按检测:利用定时器中断每隔几毫秒扫描一次按键状态,避免软件延时阻塞CPU。
  • 生成波形:输出特定频率的方波或PWM信号(如控制舵机、电机调速)。
  • 超时监控:实现看门狗或串口接收超时判断。

5. 开发中的注意事项(避坑指南)

在实际编程中,有几个容易踩坑的地方:

  • 中断服务函数要“短小精悍”:不要在ISR(中断服务例程)里写耗时的循环、延时或复杂的打印。中断的原则是“快进快出”。耗时任务应该通过标志位传递给主循环处理。
  • 临界区保护:如果在主循环和中断中都会访问同一个全局变量,在主循环访问前需要暂时关闭中断(或使用原子操作),防止变量在读写过程中被中断修改导致数据错乱。
  • 重入问题:如果你的中断优先级被设置得允许嵌套,要小心函数重入导致的栈溢出或逻辑错误。
  • 清除标志位:绝大多数MCU进入中断后,必须手动清除中断标志位,否则CPU会认为中断一直在发生,导致程序死锁在中断里。

定时器作用:

(1) 用于计时系统,可实现软件计时,或者使程序每隔一固定时间完成一项操作

(2) 替代长时间的Delay,提高程序的运行效率和处理速度(可以打断主循环)


函数的定义、声明、调用

1. 函数声明(Function Declaration)

作用:告诉编译器“存在这样一个函数”,让编译器知道它的返回类型、函数名以及参数列表(即函数原型)。这样,在函数调用出现时,编译器就能正确检查参数类型并生成调用代码。

形式:以分号结尾,没有函数体。

c

返回类型 函数名(参数类型列表);

示例

c

int add(int a, int b);   // 声明了 add 函数,接收两个 int,返回 int
void printMessage(void); // 声明了无参数无返回值的函数
  • 声明通常放在 头文件(.h) 中,或者放在源文件的开头(如果函数定义在后面)。
  • 声明可以出现多次,但必须保持一致。

2. 函数定义(Function Definition)

作用:提供函数的具体实现,即函数体。定义会为函数分配内存空间,并包含要执行的代码。

形式:包含函数头 + 函数体(花括号内的语句)。

c

返回类型 函数名(参数列表) {
    // 函数体
    // ...
    return 返回值;   // 如果返回类型是 void,可省略
}

示例

c

// 定义 add 函数
int add(int a, int b) {
    return a + b;
}

// 定义 printMessage 函数
void printMessage(void) {
    printf("Hello, World!\n");
}
  • 一个函数在整个程序中 只能被定义一次(否则会产生重复定义错误)。
  • 如果定义出现在调用之前,那么可以省略单独的声明(因为定义本身已经包含了声明信息)。

3. 函数调用(Function Call)

作用:执行函数。程序会跳转到函数定义处,执行函数体,然后返回调用点继续执行。

形式:使用函数名,并传递实参(若需要)。

c

函数名(实参列表);

示例

c

int result = add(3, 5);     // 调用 add,结果赋给 result
printMessage();             // 调用无参函数
  • 调用时,实参的类型、数量必须与声明/定义中的形参匹配。
  • 调用可以出现在任何函数内部(包括 main 函数,以及其他函数)。

4. 三者的关系与示例

假设我们有一个简单的程序,展示声明、定义和调用的位置:

c

#include <stdio.h>

// 声明:告诉编译器后面会有这个函数
int multiply(int x, int y);

int main() {
    int a = 4, b = 5;
    // 调用:使用函数
    int result = multiply(a, b);
    printf("Result: %d\n", result);
    return 0;
}

// 定义:函数的实际实现
int multiply(int x, int y) {
    return x * y;
}
  • 如果把 multiply 的定义放在 main 之前,那么可以省略声明,因为定义本身已经起到了声明作用。
  • 但在大型项目中,通常把声明集中放在头文件,定义放在单独的源文件,这样便于模块化编译。

5. 进阶补充

5.1 头文件与源文件分离

  • 头文件(.h):存放函数声明、宏定义、类型定义等。
  • 源文件(.c / .cpp):存放函数定义、全局变量定义等。
  • 其他源文件只需 #include 头文件,即可安全调用该函数,而无需关心实现细节。

5.2 静态函数

  • static 修饰的函数,其作用域被限制在定义它的源文件内,其他文件无法通过声明调用。此时,函数定义本身也是一种声明(不需要额外声明)。

5.3 内联函数(C99 / C++)

  • inline 关键字提示编译器将函数体嵌入到调用点,以减少函数调用开销。内联函数通常定义在头文件中(因为编译器需要看到定义才能内联),但它的定义仍需遵循单一定义规则(或使用 static inline 避免重复定义错误)。

6. 总结

概念 作用 形式 出现次数
声明 告诉编译器函数的存在 函数原型 + 分号 可以多次(必须一致)
定义 提供函数的具体代码 函数头 + 函数体 只能一次
调用 实际执行函数 函数名 + 实参 任意多次
回复

使用道具 举报 送花

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

本版积分规则

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

GMT+8, 2026-4-9 18:07 , Processed in 0.121375 second(s), 81 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

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