智能门锁(CH32V307)
第一个入门完整的实物项目
Author:崔竣硕(Cc)
Time:2025.3.9
嵌入书设计大赛参赛规划
- 5.1 全面开工
- 6.1 雏形 明确得失
- 7.1 完成初赛复赛作品
第一讲 零基础标准库扫课
认识单片机与开发方式
单片机:集成在一起的CPU内存硬盘USB接口
嵌入式设备:除了x86(高功耗全指令集架构)架构以外的其他架构,因为指令机不一样。
存储结构:寄存器文件(高速存储,CPU直接访问)缓存(介于寄存器和内存间)内存(临时存储数据和程序)——实现的目的都是存储,造成差距的是存储的大小,读取的速度,单位的造价。L0——L5从属结构
寄存器与库的开发方式:
-
寄存器开发(直接操作硬件寄存器,查手册,直接给数据)
-
标准库开发(使用封装好的标准库函数,把地址的操作帮你操作完了,我们用的时候只需要用宏定义就可以了)厂商在底层给了你封装,你只需要传参数进去。
结构体与配置参数:
- 结构体(数组/结构体)是内存段:数组元素(数组名是他的首地址)是一样的,通过偏移元素+首地址来确定,结构体通过变量名来访问的config->…
- 配置参数(配置GPIO需设定参数)
-
HAL库开发(硬件抽象层库,简化开发)
GPIO与LED操作
GPIO的8种模式:
浮空输入(无内阻上下拉电阻)
上拉输入(内部上拉电阻使信号为高)
下拉输入(内部下拉电阻使信号为低)
…….
推挽输出
(补充)软件遇到的问题
字体放大:快捷键“Ctrl +”
直接拿别人的程序来用,烧录的时候需要修改。
这就是底层
要会改历程,不是说跑通了就可以。
GPIO初始化
GPIO_InitTypeDef GPIO_InitStructure = {0};//定义结构体,用来传递参数。
前面的是一种数据类型
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//打开GPIOA的时钟;RCC就是reset clock control 复位时钟控制
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;//Pin就是一个引脚,配置引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//Out_PP推挽输出,配置模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//翻转速度,配置速度额
GPIO_Init(GPIOA, &GPIO_InitStructure);//调用函数
就是——定义他的结构体,开启他的时钟,用结构体里面的三种元素赋一个值,然后通过传参的方式传给初始化函数(中间3个不知道怎么设置的话,就跳转过去)
底层是怎么运转的你不要管,你就知道SetBits是设置高电平就行了,然后再知道你要配置GPIO哪个引脚就可以了。
定时器与中断配置
- 配置参数关键点(定时器周期与频率)
- 工作模式选择(普通/计数/定时模式)
- 计数范围(最大值与溢出处理)
- 中断触发条件(到达设定时间/计数值)
- 中断优先级(配置中断优先级)
- 中断服务函数
报红线,说明它没有
void TIM3_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));//定时器2快速中断
/*时钟初始化函数*/
void Tim3_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
TIM_TimeBaseInitStruct.TIM_ClockDivision=0;//时钟分割
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//设置为向上计数
TIM_TimeBaseInitStruct.TIM_Period=arr;//自动重装载值,5000为500ms
TIM_TimeBaseInitStruct.TIM_Prescaler=psc;//设置分频值
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct);
TIM_ITConfig(TIM3, TIM_IT_Update|TIM_IT_Trigger, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM3, ENABLE);
}
/*中断服务函数*/
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3, TIM_IT_Update)!=RESET)
{
}
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
}
用到什么去查什么就可以。
单片机的时钟是96兆
96 000——K 96 000 000——兆 一秒钟进行96 000 000次的加
对他进行96分频,就变成1 000 000(1兆)
变成一毫秒,除以3个0就可以了 (加到1 000重置就可以了)
Tim3_Init(1000,96-1);//中断初始化
串口配置
电脑是USB信号,单片机给出的是TTL信号,不能直接进行通信。单独找一个USB转TTL模块与电脑进行连接——CH341
然后查引脚
移植代码
- 初始化函数
- 中断函数(在Main函数里面)
- 函数本体
void Usart2_Init()//串口2初始化
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2 , ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx|USART_Mode_Rx;
USART_Init(USART2, &USART_InitStructure);
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART2, ENABLE);
}
void USART2_IRQHandler(void) __attribute__((interrupt("WCH-Interrupt-fast")));//串口2快速中断
void USART2_IRQHandler(void)//串口2函数本体
{
u8 temp;
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)
{
temp=USART_ReceiveData(USART2);
}
USART_ClearITPendingBit(USART2, USART_IT_RXNE);
}
改成串口3就可以
不知道函数去哪里找?
中断(发送中断+接收中断)发送或者接受数据都会进入中断,然后要再在里面判断一下,是发送还是接收中断。才能够对数据进行处理。如果没有引用中断会找不到中断函数就会指向不确定的地方,代码就跑崩溃了。
发送标志位在上电初始化就会是1,会跳过第一次发送。初始化的时候清空标志位。
第二讲 舵机和屏幕
舵机原理
本质就是一个闭环控制的电机,用PWM信号去给他发送数据,控制它转动相应的角度。
一、什么是PWM
PWM,即脉冲宽度调制(Pulse Width Modulation),是一种利用数字信号来控制模拟电路的有效技术。其基本原理是通过改变一系列固定频率脉冲的宽度,从而调节这些脉冲的占空比(高电平时间与整个周期时间的比例),以此来模拟连续的模拟信号。
在PWM中,尽管脉冲本身是矩形波,但通过调整占空比,接收PWM信号的负载(如电机、LED等)所感受到的有效电压或功率,可以被模拟成期望的连续变化值。这种技术使得使用数字硬件(如微控制器)能够高效且精确地控制模拟电路,特别是在需要灵活调整输出电平的场合,比如调光、调速控制等。
二、PWM有哪些优势
效率高:由于大部分时间要么完全导通要么完全截止,减少了功率损耗。
成本低廉:只需简单的电路即可实现复杂的控制功能。
灵活性高:通过软件即可改变脉冲宽度,易于实现动态控制。
稳定性好:对于负载变化,通过调整PWM信号即可维持输出稳定
三、PWM有哪些应用场景
电机控制:PWM是控制电机速度和转矩的常用方法,适用于直流电机、步进电机和交流电机(如BLDC电机)。通过调整PWM信号的占空比,可以平滑地调节电机的平均供电电压,从而控制电机转速和输出力矩。
灯光控制:在LED照明系统中,PWM常用来调节灯光的亮度。通过改变脉冲的占空比,可以控制流经LED的平均电流,实现从全暗到全亮的平滑调节,同时保持颜色的一致性,减少能耗并延长LED寿命。
电力电子设备:在变频器、逆变器和其他电力转换系统中,PWM用于生成所需频率和电压的输出波形,以高效地控制电力系统的运行。这在太阳能逆变器、不间断电源(UPS)和各种电源管理应用中尤为重要。
温度控制:PWM可用于加热元件(如PTC加热器)的温度调节,通过改变脉冲的占空比来控制加热元件的平均功率,从而达到温度控制的目的。
音频信号处理:在某些音频放大器和扬声器系统中,PWM技术被用来生成模拟音频信号,通过高速切换来模拟音频波形,实现音频信号的放大和播放。
电池充电:在一些电池充电器设计中,PWM用于调节充电电流,通过改变脉冲的宽度来控制充电速率,确保电池安全高效地充电。
传感器信号调理:PWM也用于将传感器的模拟输出转换为数字信号,便于微控制器处理,例如在压力传感器或位置传感器的信号传输中。
风扇速度控制:在电脑和其他电子设备的冷却系统中,PWM可以用来调节散热风扇的转速,根据系统温度自动调整风量,降低噪音并节约能源。
四、PWM中的周期是什么
PWM(脉冲宽度调制)中的“周期”是指一个完整PWM脉冲序列中重复出现的时间间隔,包括一个高电平时间(脉宽)和紧接着的一个低电平时间。简单来说,周期是从一个脉冲的开始点到下一个相同状态(通常是下一个高电平的开始点)之间的时间长度。周期的倒数即为PWM信号的频率,单位通常是赫兹(Hz)。
例如,如果一个PWM信号的频率为50Hz,这意味着每秒钟会有50个完整的PWM周期,每个周期的时间长度为1秒除以50,即20毫秒。
五、PWM中的脉宽(占空比是什么)
PWM(脉冲宽度调制)中的“脉宽”是指单个脉冲周期内高电平状态持续的时间长度。在PWM信号中,一个完整的周期包括高电平时间和低电平时间,脉宽即是指高电平持续的时间占比。这个时间长度通常以占空比的形式来表达,占空比是指高电平时间与整个周期时间的比例,通常以百分比表示。
例如,如果一个PWM信号的周期是10毫秒,而高电平时间为2毫秒,那么脉宽就是2毫秒,占空比为2毫秒除以10毫秒,即20%。通过改变这个脉宽或占空比,可以调节输出信号的平均电压值或功率,从而实现对连接设备(如电机、LED灯等)的控制。在不同的应用场景中,通过精细调整脉宽,PWM能够高效且精确地控制各种电气设备的工作状态。
把单片机的GND和降压模块的GND连接到一起实现共地
用已经学会的知识,去验证新的东西。
Delay验证成功,证明你的接线没有问题。然后去生成PWM,不然的话,你一下子调试这么多东西,你又没有示波器,你不知道是舵机坏了还是线没接好,还是你的代码生成不了PWM。所以先手动用Delay去生成一个最简单的PWM波形。
下一步把他变成用定时器去控制,因为定时器是可以用硬件去生成PWM的,不需要阻塞我们的程序,而且更精准。
PWM使用GPIO去输出的,
屏幕
要先添加路径才能引用
在一步一步移植的过程中才知道缺少了什么,而不是一开始直接告诉你什么都有,直接一个移植,在调试的过程中,慢慢补充完全。
屏幕就是128*128个点,控制每一个点的颜色。
修改字库显示汉字
但是字库里面不能重复添加
显示图片首先要添加头文件
void LCD_ShowPicture(u16 x,u16 y,u16 length,u16 width,const u8 pic[]);//显示图片
包含图像头数据不要勾选。
整个流程:开机,显示开机过程,文字提示,进度条,开机成功,文字提示,Logo,图片,舵机动,屏幕显示上锁,等待5秒,舵机动90°,显示门开启。
不要一直刷新,只有变化之后我们再刷新,一直刷新屏幕会一直闪烁。
第三讲 代码框架与按键密码
代码模块化与任务调度
这样就不是按照绝对路径来寻找的,这样直接复制文件夹也可以找到正确的路径。
#include debug.h 是比较省事的,它包含了很多头文件。
.c .h的区别
一、是什么 ?
1 .c文件包含了程序的实现部分,其中包含了函数的实现和变量的定义等内容。.c文件是可以被编译成可执行文件的。
2 .h文件包含了程序的接口部分,其中包含了函数的声明和结构体的定义等内容。这些代码不是可执行代码,而是提供给其他模块使用的接口。其他模块可以导入这些头文件,并通过调用头文件中声明的函数和定义的结构体来与该模块进行交互。
因此,.c文件和.h文件是相互关联的。通常,每个.c文件都对应一个.h文件。
二、怎么做 ?
- 先编写 led.h :
// 当一个头文件被多次包含时,预处理器会将该头文件的内容复制到每个包含它的源文件中。
// 如果一个头文件被重复包含多次,就会导致重复定义的问题。
// 当第一次包含头文件时,头文件保护宏被定义,后续再包含头文件时,头文件保护宏已经被定义,预处理器会直接跳过头文件的内容。
// 头文件保护宏可以确保头文件只被包含一次,避免重复定义问题,同时也提高了编译速度
#ifndef LED_H
#define LED_H
// 定义 LED 状态
typedef enum {
LED_OFF = 0,
LED_ON // 不赋值,会根据第一个值计算为 1
} LedStatus;
// 打开 LED
void led_open(void);
// 关闭 LED
void led_close(void);
#endif
- 在 led.c 实现 led.h 中定义的接口,或引用定义好的结构体,宏,枚举等:
#include "led.h"
// 定义 LED 状态变量
static LedStatus led_status = LED_OFF;
// 打开 LED
void led_open(void)
{
led_status = LED_ON;
}
// 关闭 LED
void led_close(void)
{
led_status = LED_OFF;
}
- 在 main.c 中导入 led.h,使用定义好的接口:
#include "led.h"
int main(void)
{
// 打开 LED
led_open();
return 0;
}
总结——先编写 led.h;再编写 led.c;在mian.c 或其他文件中导入 led.h 使用定义好的函数。
一.函数指针
1.1函数的定义
在讨论函数指针的定义之前,我们同样先看函数是怎么样定义的:
如上图所示,函数是由三个部分组成,分别为
- 函数名 + ( ) (括号证明这是一个函数)
- 函数参数 int x和int y
- 函数的返回类型 int
而调用函数时需要函数名+ (对应的参数)即可,如上图的Add(a,b)
1.2函数指针的定义
函数指针说白了也是一个指针,指针中所保存的地址中的内容是一个函数,同之前说过的数组指针相似,函数指针的定义便是
返回类型 (* 指针名) (函数参数) //例如: int (*pa) (intx,iny)
同数组指针一样,当定义函数指针的时候,* 需要和指针名打括号相结合,( )的优先级高于 * ,不打括号编译器自动会将 指针名 与( )相结合,如 int * pa (int x,int y) ,这样的话便是一个名为pa的函数,函数参数为 int x,int y,函数的返回类型时 int *
1.3函数指针的调用
我们先来看一个有意思的情况:
如上图,当尝试打印Add函数的地址和Add这个函数变量名时,会发现Add变量名就是函数的地址,相当于我们平时在调用函数时就是在通过类似指针调用
所有我们的函数调用就可以用如下方式来进行:
如上图:
Add(a,b)的效果等同于(*p)(a,b)
那么有一个新的问题:如果函数名就等同于函数指针,那直接用函数名不就可以了吗,为什么还需要函数指针?
二.回调函数
回调函数就是函数指针最佳的使用方式
1.1回调函数的定义
所谓的回调函数,就是利用函数指针调⽤的函数,通俗来讲,只要是一个函数参数里面有函数指针,都可以被称为回调函数
例如:完成一个简易的计算器,要求输入1代表计算加法,2代表计算减法,3代表计算乘法,4代表计算除法,0代表退出计算器,选择1 2 3 4其中一个后输入要计算的两个数字,返回计算结果
可以看到有多个重复的步骤,增加了无意义的工作量,这是我们可以注意到这四个函数(add,sub,mul,div)的函数参数都是两个int类型的变量,返回值也都是int,那么我们就可以使用回调函数来简化代码
运用回调函数之后,整个代码会简洁许多,并且方便阅读
现在我们来仔细看看这个回调函数是怎么实现的:
void Callback(int (*pfun)(int,int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入两个数字:");
scanf("%d %d",&x,&y);
ret = pfun(x,y);
printf("结果为:%d\n",ret);
}
可以看到该回调函数的函数名为Callback,函数参数是一个名为pfun的函数指针,返回值为void,假设用户输入1时,便将add的地址传入Callback中,从而不需要在主函数中提示用户输入和调用add函数(主函数调用的全是Callback函数)
Callback(add) //该语句将add地址传入
回调函数类似于一个中间商,再调用其他有用的函数
c语言中u8,u16,u32和int区别为符号不同、数据范围不同、内存占用的空间不同。
一、符号不同
1、u8:u8表示无符号char字符类型。
2、u16:u16表示无符号short短整数类型。
3、u32:u32表示无符号int基本整数类型。
4、int:int表示带符号int基本整数类型。
二、数据范围不同
1、u8:u8的数据范围为0~+127[0~2^8-1]。
2、u16:u16的数据范围为0~+65535[0~2^16-1]。
3、u32:u32的数据范围为0+2147483647[02^32-1]。
4、int:int的数据范围为-2147483648~+2147483647[-2^31~2^31-1]。
三、内存占用空间不同
1、u8:u8的内存占用空间大小为只占一个字节。
2、u16:u16的内存占用空间大小为占用两个字节。
3、u32:u32的内存占用空间大小为占用四个字节。
4、int:int的内存占用空间大小为占用八个字节。
汉字是占两个字节的。
矩阵键盘与按键密码
R4——PD11
R3——PD9
R2——PE15
R1——PE13
C1——PE11
C2——PE9
C3——PE7
C4——PC5
51单片机默认弱上拉,而我们2的CH32是可以更改配置的,可以更改里面的硬件电路
矩阵键盘的逐行逐列扫描方法可以先进行行扫描,也可以先进行列扫描。这取决于具体的实现方式和需求。在行扫描中,逐行检测按键状态,而在列扫描中,逐列检测按键状态。无论是先进行行扫描还是列扫描,最终都可以获取到按键的状态信息。 在逐行逐列扫描方法中,键盘的按键布局被组织成多行和多列的矩阵。每一行的按键都连接到一个行线上,而每一列的按键则连接到一个列线上。
扫描过程中可以从第一行开始,逐行扫描每一行。在扫描过程中,将当前行的行线置为低电平(相当于是接GND),然后检测该行上每一列的列线上是否有按键按下。如果有按键按下,相应的列线会被置低电平,就可以确定按下的按键是哪一个。
类似地,在逐列扫描方法中,扫描过程从第一列开始,逐列扫描每一列。将当前列的列线置为低电平(相当于接GND),然后检测每一行的行线上是否有按键按下。如果有按键按下,相应的行线会被置低电平,就可以确定按下的按键是哪一个。
首先进行初始化,排列不用去管,之后再矫正就可以。
初始化都是二位数字这样不会出现bug,后面为0的情况 填充memset。
VCC:C=circuit 表示电路的意思, 即接入电路的电压
VDD:D=device 表示器件的意思, 即器件内部的工作电压;
VSS:S=series 表示公共连接的意思,通常指电路公共接地端电压
VEE:负电压供电;场效应管的源极(S)
VBAT:当使用电池或其他电源连接到VBAT脚上时,当VDD 断电时,可以保存备份寄存器的内容和维持RTC的功能。如果应用中没有使用外部电池,VBAT引脚应接到VDD引脚上。
1.上、下拉电阻定义
上拉电阻是把一个信号通过一个电阻接到电源(Vcc),下拉电阻是一个信号通过一个电阻接到地(GND)。
2.强上拉、弱上拉
强上拉、弱上拉的强弱只是上拉电阻的阻值不同,没有什么严格区分。例如:50Ω上拉,一般成为强上拉;100kΩ上拉则称为弱上拉。下拉也是一样的。强拉电阻的极端就是0Ω电阻,即将信号线直接与电源或低相连接。
3.上、下拉电阻的作用
因为上下拉电阻的作用概念很宽泛,不用领域的使用方法也不同,常见使用方法整理如下:
3.1.维持输入管脚是一个稳态
芯片的管脚有三个类型,输出(Output,简称O)、输入(Input,简称I)和输入输出(Input/Output,简称I/O)。芯片的输入管脚,输入的状态有三个:高电平、低电平和高阻状态。高阻状态,即管脚悬空,很可能造成输入的结果是不定状态,引起输出震荡。有些应用场合不希望出现高阻状态,可以通过上拉电阻或下拉电阻使管脚稳定状态。
3.2.三极管实现电平转换电路的外围电路
三极管属于电流控制电流型元件,于MOS管不同,MOS管属于电压控制电压型元件。三极管有三个工作区:截至区、放大区和饱和区。以NPN型三极管为例,BE之间那个跟箭头很像一个二极管,其实BE之间就是一个二极管,BE的压差(Ube)约为0.6V(实际大小与元器件型号有关。很多都说是0.7V,0.7V只是为例工程计算方便选取的一个比较接近范围中心得电压值,在铃木雅臣《晶体管电路设计》中基射电压是按照0.6V计算,在工程上并不会有太大差异)。当Ube<0.6V时,BE间得等效二级管没有导通,此时三极管处于截至状态;随着BE之间的电压差上升,三极管进入放大区,三极管处于放大区或饱和区时Ube=0.6V。这时BE之间的压差不会随着输入的电压变高而继续增加,体现出二极管的特性,保持导通电压。
如上图所示,输入信号如果为3.3V电压信号,三极管的BE电路等效于一个二极管。我们并不会把二极管两端之接到电压和GND之间,一般会串联电阻,对电流进行控制。
-
R1电阻输入限流电阻,因为三极管属于电流控制元件,当三极管属于放大或饱和状态时,Ube的电压为0.6V,可以根据输入电压U计算基极Ib的电流,计算公示为Ib=(U-0.6)/R1,从公示可以看出,若不接限流单组R1,当输入电压大于0.6V时,基极电流会非常大,从而烧毁三极管。需根据输入电压、三极管的特性进行计算。如果该三极管的放大倍数为50(三极管的固有特性,在放大状态集电极电流Ic的大小是基极电流Ib的倍)。
-
输出电压 Vout=Vcc-Ic*R2。通过这个公式,我们可以看出:Vcc 确定,上图中 Vcc 为12V,Vout 在 Ic 为0时达到最大值12V(等于Vcc),由于是数字电路,Vout需要达到0V附近,实现低电平的效果。如果R2选定为1KΩ,很容易计算出 Ic 让三极管达到饱和状态的值,
-
三极管的导流能力有限,如果选定的三极管集电极的额定电流为500mA,那么 Ic 的最大值 Ic(max)=500mA 所以,R2的选值不能太小,避免Ic太大导致三极管烧毁。通过公式可以看出,集电极电阻越大越容易饱和,饱和区的现象是两个PN结均正偏,Ic 不受 Ib的控制,因为 Vout 已经接近 GND 了,不可能凭空产生负电压。
-
如果,输入电压为3.3V,若要求设计时三极管处于饱和状态,则 Ic(饱和)=12mA,那么Ib(min)=Ic(饱和)/=12mA/50=0.24mA,则基极限流电阻R1(max)=(3.3V-0.6V)/Ib(min)=11.25kΩ。若要求输入3.3V时,三极管饱和,并且要求考虑三极管的放大系数、电阻、Vcc电压的离散型、精度、波动等因素,需要留够足够的余量。于是,此时我们可能选择R1为 1kΩ 的电阻让三极管足够饱和,另外 R1 的阻值也不能太小,需要考虑 Ib 的额定电流。R1、R2都不能太小的另一个原因是我们需要考虑功耗和节能。
3.3.OC、OD电路
对于 OC(Open Collector,集电极开路)、OD(Open Drain,漏极开路)电路上拉电阻的功能主要是为集电极开路输出型电路提供输出电流通道。有些芯片的输出管脚,形成了三极管或MOSFET,电商没有继承上拉电阻到Vcc。
3.4.总线I/O接口上、下拉电阻
一些总线有输入输出接口,本质就是OC或OD的接口。I2C(Inter Intergrated Circuit,内部集成电路)总线就是典型的OD输出结构的应用,典型的I2C电路都有上拉电阻。
3.5.增加输出管脚的驱动能力
芯片的输出挂角本身并不是OC、OD,但是有时也会增加一个上拉或下拉电阻,通过上拉或下拉来增加或减小驱动电流。
3.6.电平标准匹配
改变电平的电位,常用在TTL-CMOS匹配。当TTL电路驱动CMOS电路时,若TTL电路输出的高电平低于CMOS电路的最低电平(一般为3.5V),这时就需要在TTL的输出端接上拉电阻,以提高输出高电平值。注意:上拉电阻的电阻值应不低于CMOS电路的最低高电压,同时要考虑TTL电路电流(如某端口最大输入或输出电流)的影响。
3.7.增强电路抗干扰能力
芯片的管脚加上拉电阻来提高输出电平,从而提高芯片输入信号的噪声容限,增强抗干扰能力。长线传输中,电阻不匹配容易引起反射波干扰,加上、下拉电阻的电阻值匹配,能有小抑制反射波干扰。提高总线的抗电磁干扰能力,管脚悬空就比较容易受外界的电磁干扰。
4.吸电流、拉电流、灌电流定义
拉电流:主动输出电流,是从输出口输出电流。
灌电流:被动输入电流,是从输出端口流入吸电流。
吸电流:吸是主动吸入电流,是从输入端口流入吸电流和灌电流就是从芯片外电路通过引脚流入芯片内的电流,区别在于吸收电流是主动的,从芯片输入端流入的叫吸收电流(即吸电流)。
拉电流是数字电路输出高电平给负载提供的输出电流,灌电流时输出低电平时外部给数字电路的输入电流,它们实际就是输入、输出电流能力;吸电流是对输入端(输入端吸入)而言的,而拉电流(输出点流出)和灌电流(输出端被灌入)是相对输出端而言的。
5.上拉电阻阻值选择原则
1)从节约功耗及芯片的灌电流能力考虑,电阻值应当足够大。电阻越大,电流越小。
2)从确保足够的驱动电流考虑,电阻值需要足够小。电阻值越小,电流越大。
3)对于高速电路,过大的上拉电阻可能边沿变平缓。需要电阻与电容形成RC滤波电路,影响信号的高频分量的传输。
4)驱动能力与功耗的平衡。以上拉电阻为例,一般来说,上拉电阻越小,驱动能力越强,但功耗越大,设计时应注意二者之间的平衡。
5)下级电路的驱动需求。同样以上拉电阻为例,当输出高电平时,开关管断开,上拉电阻应适当选择以能够向下级电路提供足够的电流。
6)高低电平的设定。不同电路的高低电平的门槛电平会有所不同,电阻应适当设定以确保能输出正确的电平。以上拉电阻为例,当输出低电平时开关管导通,上拉电阻和开关管导通电阻分压值应确保在零电平门槛之下。
7)频率特性。以上拉电阻为例,上拉电阻和开关管漏源极之间的电容和下级电路之间的输入电容会形成RC延迟,电阻越大,延迟越大。上拉电阻的设定应考虑电路在频率方面的需求。
**定点读取,达到按键消抖。**每隔10ms(机械抖动)读取一次。
利用边沿触发解决中间会执行多次的问题。
异或
相同为0,不同为1,即
1 ^ 1 = 0
0 ^ 0 = 0
1 ^ 0 = 1
由运算规则可知,任何二进制数与零异或,都会等于其本身,即 A ^ 0 = A。
如果你不想让一个事情一直做,你只想在那一下去触发他,那你就每次在采样前记住他上一次的状态,只有他发生变化的时候,才会进行这次操作,并且吧变化之后的再赋给上一次。
第四讲语音模块刷卡模块解锁完善
语音模块
- 语音模块简介
相当于MP3播放音乐
喇叭接SPK1、2不区分正负极
可以通过IO口来驱动的,可以用最简单的,但明显IO口是不够用的,用串口发送,因为有很多功能,通过数据包的形式来控制。
校验码——防止有跳变发生,数据发生异常。
- 语音生成
- 1按键音效
- 2门已上锁,请刷卡或输入密码解锁
- 3密码正确,门已打开,欢迎回家
- 4密码错误,请重新输入
- 5连续三次输入密码错误,已锁定,请等待30秒后重新输入
- 6修改开锁密码,请输入管理员密码
- 7管理员密码验证成功,请输入新密码
- 8新密码设置成功
- 9管理员密码错误,请重新输入
- 10连续三次输入管理员密码错误,已锁定,请等待30秒后重新输入
- 11刷卡成功,门已开启,欢迎回家
- 12刷卡失败,请重试
- 13录入卡片,请输入管理员密码
- 14管理员密码验证成功,请将卡片平放在传感器上
- 15卡片添加成功
- 16指纹验证成功,门已开启,欢迎回家
- 17指纹验证失败,请重试
- 18录入指纹,请输入管理员密码
- 19管理员密码验证成功,请将手指平放在传感器上
- 20指纹正在录入
- 21指纹录入成功
- 语音模块手册,通信协议
- 串口助手测试
- 串口3测试
- 校验设置
- 密码相关代码完善
- 按键音效
- 解锁音效
- 输入错误3次锁定
- 修改密码
- 管理员密码输入错误3次锁定
- 回退
- 后面为0的情况 填充memset
设置音量
刷卡模块
- 基本原理
- 串口助手调试
很多时候不需要看数据手册,刷不同的卡分析发出来的数据包就可以了。
- 手册阅读
- 代码编写
波特率不一样,串口不能重复利用。
- 现象调试
最简单的功能,刷卡,发送卡号
把卡片放上去,模块会发卡号出来
第五讲指纹模块
首先看的看的不应该是数据包和通讯手册,首先看怎么接线和电压规格
上位机检测模块
-
利用串口监控精灵监控上位机和模块之间的通信,把收发都检测到
-
也可以用面包板用硬件直接截取数据监控
send_string 为什么不让他自动检测长度,因为数据是可能出现0的。后面的就丢了,所以手动给长度是比较稳妥的。
USART_ClearFlag(UART7,USART_FLAG_TC);//清空串口7的发送标志位
void uart7_send_string(u8* string,u8 len)
{
u8 i;
for(i=0;i<len;i++)
{
USART_SendData(UART7,string[i]);
while( USART_GetFlagStatus(UART7, USART_FLAG_TC)==0 );
}
usart1_send_string(string,len);
}
//给串口7发数据发给指纹模块,给串口1也发送一个,方便观测我发的是什么。
//不用printf的原因也是遇到0会丢失后面的数据
接受数据包,但是每一包数据不定长,如何确定一包数据接受完,用tick来确定,超过10ms索引就归零——超时解析
if(uart7_rec_tick>10)uart7_rec_index=0;
temp=USART_ReceiveData(UART7);
uart7_rec_string[uart7_rec_index]=temp;
uart7_rec_index++;
uart7_rec_tick=0;
具体的指令首先应该通读手册理解实现指令的过程。
- BUF,是英文buffer的缩写,意思是缓冲区,指在工厂中生产出来产品的临时存放位置,在达到一定数量后会搬运到其它的地方。
- Flash 是一种内存存储器,可在系统中进行电擦写,掉电后信息不丢失。
指纹录入
- 传感器得到图像数据
- 变成数字量存到暂存区(PS_GetImage)
- 数字量变成指纹特征存到缓存区1或2(PS_Genchar)
- 融合
- 调用函数把缓存区的数据存到Flash库中
指纹检测
/*从传感器上读入图像存于图像缓冲区*/
void PS_GetImage()
{
u8 string[]={0xef,0x01,0xff,0xff,0xff,0xff,0x01,0x00,0x03,0x01,0x00,0x05};
uart7_send_string(string,12);
}
void PS_GenChar(u8 buffer)
{
u8 string[]={0xef,0x01,0xff,0xff,0xff,0xff,0x01,0x00,0x04,0x02,buffer,0x00,0x01+0x00+0x04+0x02+buffer};
uart7_send_string(string,13);
}
void PS_RegModel()
{
u8 string[]={0xef,0x01,0xff,0xff,0xff,0xff,0x01,0x00,0x03,0x05,0x00,0x09};
uart7_send_string(string,12);
}
void PS_StoreChar(u8 addr)
{
u8 string[]={0xef,0x01,0xff,0xff,0xff,0xff,0x01,0x00,0x06,0x06,0x02,0x00,addr,0x00,0x01+0x00+0x06+0x06+0x02+0x00+addr};
uart7_send_string(string,15);
}
void PS_Search()
{
u8 string[]={0xef,0x01,0xff,0xff,0xff,0xff,0x01,0x00,0x08,0x04,0x02,0x00,0x00,0x00,0xff,0x01,0x0e};
uart7_send_string(string,17);
}
void PS_Empty()
{
u8 string[]={0xef,0x01,0xff,0xff,0xff,0xff,0x01,0x00,0x03,0x0d,0x00,0x11};
uart7_send_string(string,12);
}
void as608_proc()
{
as608_proc_falg=GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1);
if(as608_proc_falg==as608_proc_falg_old)return;
as608_proc_falg_old=as608_proc_falg;
if(as608_proc_falg==0)return;
//类似键盘,跳变的时候才会触发
if(mode==6)
{
PS_GetImage();//从指纹传感器获取图像到暂存区
Ps_Wait();//等待接收到数据包
PS_GenChar(1);
Ps_Wait();//等待接收到数据包
PS_GetImage();//从指纹传感器获取图像到暂存区
Ps_Wait();//等待接收到数据包
PS_GenChar(2);
Ps_Wait();//等待接收到数据包
PS_RegModel();
Ps_Wait();//等待接收到数据包
PS_StoreChar(as608_store_index);
as608_store_index++;
Ps_Wait();//等待接收到数据包
audio_play(21);
mode=0;
}
}
if(mode==0)
{
PS_GetImage();//从指纹传感器获取图像到暂存区
Ps_Wait();//等待接收到数据包
PS_GenChar(2);
Ps_Wait();//等待接收到数据包
PS_Search();
Ps_Wait();//等待接收到数据包
if(uart7_rec_string[13]>50)//匹配
{
audio_play(16);
lock_flag=0;//开门
show_flag=1;//显示汉字们已开启
}
else
{
audio_play(17);
}
}
void Ps_Wait()
{
ps_wait_flag=1;
do
{
Delay_Ms(200);
}
while(ps_wait_flag);
}
/*串口7中断*/
void UART7_IRQHandler(void)
{
u8 temp=0;
if(USART_GetITStatus(UART7, USART_IT_RXNE) != RESET)
{
ps_wait_flag=0;
if(uart7_rec_tick>10)uart7_rec_index=0;
temp=USART_ReceiveData(UART7);
uart7_rec_string[uart7_rec_index]=temp;
uart7_rec_index++;
uart7_rec_tick=0;
}
USART_ClearITPendingBit(UART7, USART_IT_RXNE);
}
如何调用去数据手册查
检测问题调式:
- 首先看能不能进串口中断(如果进入不了那就是波特率的问题)
第六讲ESP8266连接OneNET云平台 & 手机APP远程控制
什么是物联网服务器
为什么要有物联网服务器
- 1. 数据集中管理
聚合海量设备数据,提供统一存储与分析能力 - 2. 远程交互控制
突破局域网限制,实现跨地域设备操控(如通过4G/5G远程开锁) - 3. 多端协同
支持手机APP/Web/小程序等多平台接入,提供标准化API接口 - 4. 安全防护
提供设备认证、数据加密、访问权限管理等安全机制
常见的物联网服务器及通信协议
主流云平台
- 阿里云IoT:规则引擎强大,支持边缘计算
- OneNET:中国移动打造,协议支持全面
- 华为云IoT:深度整合LiteOS系统
- 腾讯云IoT:无缝对接微信生态
- 自建方案:
- 开源MQTT服务器:EMQX/Mosquitto
- 私有化部署,数据自主可控
核心通信协议
协议 | 传输层 | 特点 | 典型场景 |
---|---|---|---|
MQTT | TCP | 低功耗/发布订阅模式 | 物联网设备通信 |
CoAP | UDP | 报文精简/支持多播 | 资源受限设备 |
HTTP | TCP | 通用性强/高开销 | 数据上报 |
LwM2M | UDP | 设备管理专用协议 | 固件升级/状态监控 |
MQTT及OneNET
MQTT协议三大核心
-
主题(Topic)机制
- 分层结构设计(例:
CH32V307/lock/status
) - 支持通配符
+
和#
进行消息过滤
- 分层结构设计(例:
-
服务质量(QoS)
QoS等级 传输保证 资源消耗 0 最多一次(可能丢失) 最低 1 至少一次(可能重复) 中等 2 精确一次(可靠传输) 最高 -
遗嘱消息(Last Will)
- 设备异常离线时自动发布预设消息
OneNET平台接入要点
- 三元组鉴权
// 设备唯一标识
#define PRODUCT_ID "123456"
#define DEVICE_ID "CH32V307_Lock"
#define API_KEY "abcDEF123=="
onenet云平台用户环境搭建
注册及创建产品
- 注册登录官网https://open.iot.10086.cn/
- 登录成功以后,右上角>文档中心 右上角>开发者中心
- 左上角>产品开发>创建产品 >其他行业>其他类别 名称:米醋智能门锁
- 选择智能化方式为设备接入 节点类型:直连设备 接入协议:mqtt 数据协议:onejson 联网方式:wifi 开发方案:自定义方案
- 产品开发>设置物模型>添加自定义功能点 功能类型:属性类型 功能名称:测试 标识符:text 数据类型:int 范围:0-9999999
- 添加自定义功能点 功能类型:属性类型 功能名称:锁 标识符:lock 数据类型:bool 范围:1 -ture 0-false 保存
- 左上角>产品开发>设备管理>添加设备>所属产品:米醋智能门锁 >设备名称:ch32
mqtt上位机模拟设备登录
- MQTT调试助手v3.1.1
- 服务器域名或者IP地址填入:183.230.40.96
- 服务器端口填入:1883
- ClienId填入:ch32 (填入设备名,这里以ch32为例)
- Username填入:物联网平台>设备接入管理>设备管理>ch32详情>复制产品id粘贴 (例:W6A6j0yRnH)
- password: token
- res填入:products/产品ID/devices/设备名称
- 有效时间,单位秒,填入10个9 (时间超过上下限将导致错误)
- key填入:物联网平台ch32详情页,复制设备密钥 (例:Z05JcmpyNVRwVVg4Qmt4STJzNERSMElrcFFpMHZxNFY=)
- 点击Generate生成token
- 将复制的token填入MQTT调试助手V3.1.1的password (注意检查末尾是否有空格,MQTT调试助手所有参数末尾空格要删除,否则将导致错误)
- 点击connect连接服务器,如连接失败请检查前面的步骤并重试
- 物联网平台>设备接入管理>设备管理>设备列表>可以看到ch32已经在线
- 订阅主题:MQTT调试助手>订阅主题填入:$sys/产品ID/设备名/#
- 物联网平台>运维监控>API调试>物模型使用>设置属性期望值设置
- product_id:产品ID
- device_name:ch32
- params {“lock”:true}
MQTT调试助手将看到订阅消息
- MQTT调试助手>发布主题: $sys/产品ID/设备名/thing/property/post
- 主题消息:{“id”:“123”,“version”:“1.0”,“params”:{“password”:{“value”:567},“lock”:{“value”:true}}}
- 物联网平台>设备管理>ch32详情>属性 可以看到属性上传成功
- MQTT调试助手点击disconnect,断开连接
- 物联网平台>设备接入管理>设备管理>设备列表>刷新可以看到ch32已经离线
esp8266连接Onenet
-
flash_download
-
进入下载工具,在上方选择固件包(ESP01选择1M,ESP12F选择4M) 填入起始地址:@0x000000
-
选择com口。点击START开始下载,等待FINISH下载完成
设置PC机波特率
设置PC机上的端口波特率和flash下载工具中的波特率一致,否则flash下载工具会一直提示串口连接失败。我这里将PC机上的串口波特率设置为115200,然后flash下载工具波特率也设置为115200
-
打开sscom,连接esp8266,波特率选择115200,发送字符 AT ,应答 OK 则代表烧录成功,正常应答
-
点击sscom的扩展,添加常用指令
-
AT+RST 重启
-
AT+CWMODE=1 设置为station模式
-
AT+CWDHCP=1,1 启动DHCP
-
AT+CWJAP=“may”,“01kd01kd” 连接wifi,may替换成wifi名称,01kd01kd替换成wifi密码,必须为2.4GHz
-
AT+MQTTUSERCFG=0,1,“ch32”,“W6A6j0yRnH”,“version=2018-10-31&res=products%2FW6A6j0yRnH%2Fdevices%2Fch32&et=9999999999&method=md5&sign=fsy5FYosF3qpDMMhOgP0Xw%3D%3D”,0,0,“”
将产品ID和password替换成自己的
-
AT+MQTTCONN=0,“mqtts.heclouds.com”,1883,1 连接onenet
-
物联网平台>设备接入管理>设备管理>设备列表>刷新可以看到ch32已经在线 ,如果还是离线,检查前面的步骤并重复,主要是产品ID和password的替换,建议用AI完成
-
AT+MQTTSUB=0,“$sys/W6A6j0yRnH/ch32/thing/property/set”,1 订阅物模型主题,将产品ID和设备名称换成自己的
-
物联网平台>运维监控>API调试>物模型使用>设置属性期望值设置
- product_id:产品ID
- device_name:ch32
- params {“lock”:true}
sscom串口助手将看到esp8266接收的消息
- AT+MQTTPUB=0,“$sys/W6A6j0yRnH/ch32/thing/property/post”,“{“id”:“123”,“params”:{“text”:{“value”:32}}}”,0,0 发布消息
- 物联网平台>设备管理>ch32详情>属性 可以看到属性上传成功
ch32连接esp8266
- 创建第六讲工程,由第五讲复制而来
- 配置串口6为115200
- 定义串口6数组发送函数
- 测试发送和接受功能
- 连接ch32和esp8266,RX接PC0,TX接PC1,GND接GND,3V3接3V3
- 封装onenet_init函数 使用AI转义
- 下载代码 串口助手监控esp8266和ch32通信
- 解析lock变量 远程控制开门和锁门
- 官方可视化工具
制作手机app
- app inventor
- AI2Offline安装
- 打开AI2Offline,点击AII AI2Offline Server,点击 AIStarter,点击Start Invent,允许浏览器打开 ,点击log in
- 项目>新建项目>micu>确认
- 手机安装AI伴侣 (后续调试需手机和电脑在同一网络环境下)
- 用户布局,水平布局,拉入三条水平布局,宽度拉满,给居中
- 用户界面拖入标签,拖入按钮,拖入显现标签
- 通信连接 拖入web客户端,然后进入逻辑设计
- 点击按钮1,拖入“当按钮1被点击执行…”
- 进入物联网平台查看API
- 拖入web客户端,执行get请求
- 设置请求头为
- 设置网址为
- 完善请求头,Accept ,authorization,注意别复制到双引号
- 完善网址:https://iot-api.heclouds.com/thingmodel/query-device-property
?product_id=W6A6j0yRnH&device_name=ch32 - 当web客户端获得文本,将相应内容复制给标签,设置标签的文本为响应内容
- 电脑点击连接>AI伴侣,手机打开AI伴侣扫码,电脑弹出版本过期,点确认无视就行,等待连接成功,在手机点击按钮测试,看到反馈的json数据包
- web解析json文本,判断是否为列表
- 列表,选择列表中索引值为…
- 索引选数字
- app下发命令
- 测试API,组件设计界面拖入按钮
- 逻辑设计,POST (区别于GET)
- 填充请求头,填充POST文本请求
- 请求头过期问题,文档中心,物联网平台,api,安全鉴权
后记&致谢
本文仅用来记录学习过程,很多笔记摘抄了很多大佬的笔记,如有侵权立马删除。学习过程中得到了很多米醋电子工作室大佬们的帮助,在此感谢。
继续沉淀,希望将来的某一天也能产出属于自己的成果。帮助到像我一开始那样迷茫的人。