B站,小鱼创意
控制两个不同周期的LED灯闪烁
思路:
在loop中循环检测运行时长(loop执行时间非常短)
定义的int类型是占了4字节所以是232 (-2,147,483,648 到2,147,483,647)
unsigned int 是0 到4,294,967,295
1d=24h=1,440m=86,400s
1h=60m=3600s
1m=60s
mills()
功能:获取本程序已经进行多少毫秒
返回值:本程序已经运行的时间(毫秒)
micros()返回的微秒
#include <Arduino.h>
int ledPin1 = 12;
int ledPin2 = 13;
int led1Status = 0;
int led2Status = 0;
unsigned int prev1Time = 0;
unsigned int prev2Time = 0;
// put function declarations here:
void setup() {
// put your setup code here, to run once:
pinMode(ledPin1,OUTPUT);
pinMode(ledPin2,OUTPUT);
digitalWrite(ledPin1,HIGH);
digitalWrite(ledPin2,HIGH);
led1Status = HIGH;
led2Status = HIGH;
prev1Time = millis();
prev2Time = millis();
}
void loop() {
// put your main code here, to run repeatedly:
unsigned int now1 = millis();
if(now1 -prev1Time > 3000)
{
int status1 = led1Status == HIGH ? LOW : HIGH;
digitalWrite(ledPin1,status1);
led1Status = status1;
prev1Time = now1;
}
unsigned int now2 = millis();
if(now2 -prev2Time > 2000)
{
int status2 = led2Status == HIGH ? LOW : HIGH;
digitalWrite(ledPin2,status2);
led2Status = status2;
prev2Time = now2;
}
}
// put function definitions here:
使用RBD库函数读取按键状态
#include <Arduino.h>
#include<RBD_Timer.h>
#include<RBD_Button.h>
int pin = 2;
int Button = 5;
int ledStstus =0 ;
RBD::Button button(Button,INPUT_PULLUP);
//创建一个消除按键抖动的对象
// put function declarations here:
void setup() {
// put your setup code here, to run once:
pinMode(pin,OUTPUT);
button.setDebounceTimeout(20);
digitalWrite(pin,HIGH);
ledStstus = HIGH;
}
void loop() {
// put your main code here, to run repeatedly:
if(button.onPressed())
{
ledStstus = !ledStstus;
digitalWrite(pin,ledStstus);
}
}
// put function definitions here:
LEDC/PWM/led呼吸灯(有单独的硬件电路)
ledc是指led pwm控制器,实际上是pwm信号产生器
ledc通道绑定gpio控制led亮度
PWM分辨率:讲一个周期的PWM分成若干份
最大分辨率公式:
f
l
o
o
r
(
l
o
g
2
??
M
H
z
P
W
M
频率
)
floor(log _{2} \frac{??MHz}{PWM频率})
floor(log2PWM频率??MHz)
其中floor是向下取整
频率先设置,然后精度(分辨率)根据公式算出,也向下取整
int led=0;//设置通道
int gpio=4;//设置要绑定的gpio管脚
ledcSetup(ch0,5000,12);//设置通道0的频率和分辨率
ledcAttachPin(gpio4,ch0);
ledcWrite(ch0,pow(2,11));
精度(分辨率)设置的是12(212)一个周期共分成4096个格子
pow(2,11) 高电平占用2048个格子
所以占空比是50%
led灯呼吸灯效果(ledc/pwm实现)
思路
- 每秒固定调整占空比50次
- 假设T为呼吸周期,则光从关闭到最亮经过的时间就是T/2.
- 半个周期就要进行50*T/2次调整占空比
- count表示的是占空比为100%时等分的格子
- step是每次调整要加上的步进值(增量)
s t e p = c o u n t 50 ∗ T 2 = 2 ∗ c o u n t 50 ∗ T step = \frac{count}{50*\frac{T}{2}}=\frac{2*count}{50*T} step=50∗2Tcount=50∗T2∗count - 占空比超过100%时就要以step值递减到0
使用阻塞代码(使用delay)
#include <Arduino.h>
int gpio = 16;
int ch = 0;//leds通道
int duty = 0;
int count = 0;
int step = 0;
int breathTime = 3;//呼吸灯的周期,单位为s
// put function declarations here:
void setup() {
// put your setup code here, to run once:
ledcSetup(ch,1000,12);//建立ledc通道
count = pow(2,12);//计算占空比为100时所占的格子数量
step = 2 * count /(50 * breathTime);//计算一次增加多少占空比格子
ledcAttachPin(gpio,ch);
}
void loop() {
// put your main code here, to run repeatedly:
ledcWrite(ch,duty);
duty += step;
if(duty > count)
{
duty = count;
step = -step;
}
else if(duty <0)
{
duty = 0;
step = -step;
}
delay(20);
}
// put function definitions here:
不使用阻塞代码(使用millis)
int prevTime = 0;//记录上一次调整占空比的时间
……
void loop() {
// put your main code here, to run repeatedly:
int now = millis();
if(now - prevTime >= 20)
{
ledcWrite(ch,duty);
duty += step;
if(duty > count)
{
duty = count;
step = -step;
}
else if(duty <0)
{
duty = 0;
step = -step;
}
}
prevTime = now;
}
AsyncTimer(定时器库)
AsynTimer使用基础 :
- handle函数:所有操作都在此函数中完成,每一次loop时定时器都在handle中执行一次所有和定时器有关的操作。
t.handle();//要在loop中写一次
#include <AsyncTimer.h>
AsyncTimer timer;//定义一个定时器
void setup() {
}
void loop() {
timer.handle();//loop中必须写上这句
//这里开始处理其它事情
}
创建单次运行的定时任务
setTimeout(callbackFunction,delaylnMs)
- 返回值:成功返回任务编号,失败返回0
回调函数:
- 可以使用普通的函数,形式为void func(){}
- 也可以使用lamda表达式,无参无返回值
- 使用lamda [](){}
- []参数:=传值,&传址
- ()返回值
- {}业务代码
AsyncTimer t;
t.setTimeout([](){
Serial.println("hello");
},2000);
- 使用普通函数
AsyncTimer t;
void functionCall()
{
Serial.println("hello");
}
t.setTimeout(functionCall,2000);
创建周期运行的定时任务
setInterval()
和单次运行的定时任务类似
停止单个定时任务
cancel()
- cancel an interval
AsyncTimer t;
unsigned short intervalId = t.setInterval([](){
Serial.println("hello");
},2000);
//cancel the interval after 7 seconds:
t.setTimeout([=](){t.cancel(intervalId);
},7000);
- cancel a timeout
AsyncTimer t;
//this timeout will never run
unsigned short timeoutId = t.setTimeout([](){
Serial.println("hello");
},3000);
//cancel the timeout before its executed
t.cancel(timeoutId);
停止多个定时任务
cancelAll(includeIntervals = true)
如果includeIntervals为true ,是取消所有的定时任务,包括周期性和一次性,如果是false,则只取消单次的定时任务
t.cancelAll();//括号内不填是true:取消所有的定时任务
t.cancelAll(false);//取消单次定时任务
I2C(Wire库)
两线的串行半双工通信方式
地址有7位和10位的
速度快于串口,慢于SPI。标准模式(100kbps)快速模式是(400kbps)
Wire库
- 包含头文件
- 库里面已经定义好两个I2C对象,可以直接拿来使用wire,wire1
-默认的时钟频率是100khz
初始化
wire.begin();//成功返回true,失败返回false
重载:
- 作为主机加入时:
1.1 无参数传入:即都使用默认sda:21,scl:22,频率100Khz
1.2只传递sda,scl
1.3都自定义3个参数- 作为从机加入时
2.1一个参数:指定从机地址
2.2个参数:地址,sda,scl,频率都自定义
主机向从机发送数据(1)
beginTransmission(address);//开始传输
- address是从机地址
uint8_t endTransmission(bool sendStop = true);//结束传输
- 如果sendStop为true,则释放总线,false不释放总线
- 返回值:
- 0:成功
- 1:数据过长,超出发送缓冲区
- 2:在地址发送时接收到NACK信号
- 3:在数据发送时接收到NACK信号
- 4:其它错误
- 5:超时
- 如果sendStop为true,则释放总线,false不释放总线
所有的写数据过程都要包含在两个函数之间
主机向从机发送数据(2)
size_t write(uint8_t);//只能传一个字节数据
size_t write(const uint8_t * ,size_t);//传一个Byte的数组
inline size_t write(const char * s)
{
return write((uint8_t*)s,strlen(s));
//返回值:成功传送的字节数
}
主机向从机发送数据使用write命令
第一个函数的重载版本,它的形参类型是uint8_t,是Byte类型,只能传递一个字节,如果用int long类型的数据传递进去,数据会被截断!(数据丢失)
如过传递多个字节,使用第二个函数重载
I2C设备地址的查找
原理:使用endTransmission函数返回0,说明该地址的设备链接在I2C总线上。枚举出设备地址
wire.begin();
for(int address = 0x01; address < 0x7f; address++)
{
Wire.beginTransmission(address);
auto error = Wire.endTransmission();
if (error == 0)
{
Serial.printf("I2C device found at address 0x%02X\n", address);
nDevices++;//有n个device
}
{
主机向从机读取数据(1)
requestFrom(address,quantity,<stop>)
- 请求后数据放在接收缓冲区
参数
- address:从机地址
- quantity:请求多少字节数量
- stop:请求过后是否释放总线,true:释放(默认值)
返回值
- 收到了从机发来的数据量大小(字节)
主机向从机读取数据(2)
int available(void);
判断接收缓冲区中有多少字节数据可读
int read(void);
接收一个字节的数据
size_t readBytes(uint8_t *buffer, size_t length)
一次读取多个字节
size t readBytesUntil(char terminator, uint8_t*buffer, size_t length)
一次最读取多个字节或碰到结束符terminator为止
从机注册相关的事件
void onReceive(void (*)(int));
注册接收到主机数据事件的回调函数,此回调函数在接收到主机发来的数据时触发
void onRequest(void(*)(void));
注册发送到主机数据事件的回调函数,此回调函数在主机向从机请求数据时触发
void setup()
{
Wire.onReceive(onReceive);//注册接收事件
Wire.onRequest(onRequest);//注册发送事件
Wire.begin(27);//设置完成后将#27号从机加入到总线
}
- 在onReceive的注册的回调函数中处理
void onReceive(int len)
{
//有len字节数据可以接收
while(wire.available())
{
int val = Wire.read();
//其它操作
}
}
==============或者================
void onReceive(int len)
{
//有len字节数据可以接收
if(len == 0)
return;
byte* buf = new byte[len];
Wire.readBytes(buf, len);
//处理其它事情
delete []buf;
}
- 收到主机请求发送数据时,在onRequest中注册的回调函数就会被调用,此时就可以在此函数中向主机发送数据。
void onRequest()
{
char val ='a';
Wire.write(val);
}
ADC
void analogReadResolution(uint8_t bits);//设置ADC精度
void analogSetWidth(uint8_t bits);//设置位宽
void __analogReadResolution(uint8_t bits)
{
if(!bits || bits > 16){
return;
}
__analogReturnedWidth = bits;
#if CONFIG_IDF_TARGET_ESP32
__analogSetWidth(bits); // hadware from 9 to 12
#endif
}
设置衰减值
//所有的通道都设置
/*
* Set the attenuation for all channels
* Default is 11db
* */
void analogSetAttenuation(adc_attenuation_t attenuation);
//设置指定的GPIO口
/*
* Set the attenuation for particular pin
* Default is 11db
* */
void analogSetPinAttenuation(uint8_t pin, adc_attenuation_t attenuation);
typedef enum {
ADC_0db,
ADC_2_5db,
ADC_6db,
ADC_11db,
ADC_ATTENDB_MAX
} adc_attenuation_t;
读取电压函数
/*
* Get ADC value for pin,获取指定GPIO口的ADC值
* */
uint16_t analogRead(uint8_t pin);
/*
* Get MilliVolts value for pin,获取指定GPIO口的电压值,(毫伏)150-2450之间的精度可以使用
* */
uint32_t analogReadMilliVolts(uint8_t pin);
中断
中断服务程序ISR (Interrupt Service Routines)
中断函数要求:
1.尽量短,减小执行时间
2.不要使用delay函数
3.不要使用serial、print等打印函数
4.和主程序共享的变量要加上volatile(多变,易失性)关键字
5.不要使用millis函数,它的值将不会增长
6.可以使用micros函数来获取程序运行时间
将中断附加到GPIO口
attachInterrupt(digitalPinToGPIONumber(pin), fcn, mode)
中断服务程序接收外界数据,共享给主程序,这时涉及到数据共享的问题
临界资源和临界区
- 临界资源:一次仅允许一个进程使用的共享资源(打印机)
- 临界区:每个进程中访问临界资源的那段程序称之为临界区
解决临界资源访问方法1:
在准备读取临界资源时暂时禁用中断,快速完成临界资源的访问后再开启中断
interrupts() //启用中断
noInterrupts()//禁用中断
ESP32有两颗CPU,禁用了这颗CPU的中断,另一颗CPU的线程还可能对临界资源访问,所以要对临界资源上锁,同时在处理临界资源时禁用中断.
自旋锁
解决临界资源访问方法2:
自旋锁加禁用中断
portMUX_INITIALIZER_UNLOCKED
typedef struct {
uint32_t owner;
uint32_t count;
} portMUX_TYPE;
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
//在中断中使用的版本:
portENTER_CRITICAL_ISR(&mux);
···//原子操作,既上锁又禁用中断
···
portEXIT_CRITICAL_ISR(&mux);
//在主函数中使用的版本:
portENTER_CRITICAL(&mux);
···//原子操作,既上锁又禁用中断
···
portEXIT_CRITICAL(&mux);
二值信号量
在主函数中频繁使用
portENTER_CRITICAL(&mux);
会阻塞正常中断,所以引入二值信号量
(在中断中处理完成发送信号给主函数,主函数接收到信号后使用自旋锁后处理临界资源)
- 信号量:semaphore是操作系统用来解决并发中的互斥和同步问题的一种方法
- 信号量可以简单的理解为一个队列,我们只关注这个队列里元素的个数,也可以理解成一个整型全局变量,记录着信号个数
- 二值信号量可以理解为一个队列,要么是满的(有信号),要么是空的(无信号),我们在任务中检查或等待信号的到来,再处理相关事宜
- 二值信号量与全局变量相比,它可以保证原子性,可以等待信号(设置等待信号时间)等优点
- 二值信号量常用于ISR和任务之间的同步
相关函数:
- 创建二值信号量
xSemaphoreCreateBinary()
成功返回信号量句柄,失败返回null - 在普通任务中获取信号量
xSemaphoreTake( xSemaphore, xBlockTime )
xSemaphore:信号量句柄
xBlockTime :等待的节拍数,0立即返回,portMAX_DELAY是一直等待,数字(ESP32默认1节拍是1ms)
返回值:pdTRUE获取成功,pdFALSE获取失败
#if (configUSE_16_BIT_TICKS == 1)
typedef uint16_t TickType_t;
#define portMAX_DELAY (TickType_t) 0xffff
#else
typedef uint32_t TickType_t;
#define portMAX_DELAY (TickType_t) 0xffffffffUL
#endif
- 在ISR中获取信号量
xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken )
- 在普通任务中释放信号量(也就是将信号量设置为有信号的状态)
xSemaphoreGive( xSemaphore )
- 在ISR中释放信号量
xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken )
- 创建句柄
SemaphoreHandle_t (加volatile修饰)
硬件定时器
精确定时
预分频器:ESP中的预分频器是16bit的,所以最大的分频数是65535
系统时钟通过预分频得到计数周期,给定时器使用
硬件定时器的使用流程:
初始化(确定时钟频率)->绑定ISR->设置触发ISR的计数值->启动定时器
1.void IRAM_ATTR onTimer(){······}//ISR中断服务函数
2.hw_timer_t * timer = NULL;//创建一个定时器的指针
3.timer = timerBegin(0,80,true);//初始化定时器,设置周期
4.timerAttachInterrupt(timer,onTimer,true);//将定时器与ISR绑定
5.timerAlarmWrite(tiemr,1000000,true);//在计数到1M时触发中断
6.timerAlarmEnable(timer);//开启定时器
函数功能,参数讲解
- 定时器初始化和结束
1.1 定时器初始化
hw_timer_t * timerBegin(uint8_t num, uint16_t divider, bool countUp);
num:定时器编号,ESP32有4个可用定时器,编号0-3
driver:分频数
countUp: true为向上计数 , false为向下
返回值:返回定时器指针,初始化失败返回null
1.2定时器结束
void timerEnd(hw_timer_t *timer);
- 定时器中断绑定与分离
2.1定时器中断绑定
void timerAttachInterrupt(hw_timer_t *timer, void (*fn)(void), bool edge);
timer:定时器指针
fn:ISR函数,函数原型为无参无返回
edge:是否为边沿触发,true边沿,false电平
2.2定时器中断分离
void timerDetachInterrupt(hw_timer_t *timer);
- 定时器报警(中断)的设置(设置和获取参数)
3.1 设定定时器计数器报警的值及周期模式
void timerAlarmWrite(hw_timer_t *timer, uint64_t alarm_value, bool autoreload);
alarm_value:计数器的值,计数到此值触发中断
autoreload:是否重新加载定时器?true是 false一次性的
3.1.1 设定定时器计数器报警的值
void timerWrite(hw_timer_t *timer, uint64_t val);
3.1.2设定定时器是否自动重新加载
void timerSetAutoReload(hw_timer_t *timer, bool autoreload);
3.2获取定时器是否重新加载
bool timerGetAutoReload(hw_timer_t *timer);
3.3获取定时器计数器报警的值,时间值
uint64_t timerAlarmRead(hw_timer_t *timer);
uint64_t timerAlarmReadMicros(hw_timer_t *timer);
double timerAlarmReadSeconds(hw_timer_t *timer);
- 报警事件的启动与停止
4.1 设置启动定时器报警
void timerAlarmEnable(hw_timer_t *timer);
4.2 设置禁用定时器报警
void timerAlarmDisable(hw_timer_t *timer);
4.3 获取定时器目前的报警状态是可用还是禁止
bool timerAlarmEnabled(hw_timer_t *timer);
- 定时器计数器的运行控制
5.1 计数器开始计数
void timerStart(hw_timer_t *timer);
超声波测距模块(HC-SR04)
距离 = 声速 ∗ 返回时间 2 距离=声速*\frac{返回时间}{2} 距离=声速∗2返回时间
pulseIn函数链接,此函数是阻塞式的
使用中断方式来测量距离
ESP32超声波测距代码查看链接,可编辑链接
舵机
扭矩
失速力矩(堵转力矩):电机轴被外力锁定的情况下,可连续输出力矩的最高值
SPI
- 高速、全双工、同步的通信总线
- 强调控制权在主机
- CS片选选出从机,下拉CS信号即为开始信号
- 在SCK时钟线上升沿或下降沿时变换数据或者采集数据
- 有4种工作模式:通过“时钟极性CPOL”和“时钟相位CPHA”来进行选择
ESP32API中都是以传输事务为基础的
片选线拉低到释放为一次传输事务,一次传输事务数据量最大是64字节,大于64字节要使用DMA
SPI主机库
首先引入头文件
#include"SPI.h"
创建对象:SPIClass::SPIClass(uint8_t spi_bus)
SPIClass vspi(VSPI);
SPIClass hspi(HSPI);另在源文件里定义了
SPIClass SPI(VSPI);
可以直接使用SPI对象
SPI主机通讯流程
SPIClass * vspi = new SPIClass(VSPI);//创建对象
vspi->begin();//SPI控制器绑定引脚,并占用总线(使用默认参数)
pinMode(vspi->pinSS(),OUTPUT);//设置片选引脚为输出模式
vspi->beginTransaction(SPISettings(SPI_CLK_DIV,MSBFIRST,SPI_MODE0));//设置SPI的传输参数
digitalWrite(vspi->pinSS(),LOW);//片选
vspi->transfer(data);
digitalWrite(vspi->pinSS(),HIGH);
vspi->endTransaction();
vspi->end();
delete vspi;
其中一些API参数说明
- 指定SPI相关引脚,并占用总线,不填为默认,速度最快
void SPIClass::begin(int8_t sck, int8_t miso, int8_t mosi, int8_t ss)
- 释放总线
void SPIClass::end()
- 设置SPI传输的相关参数,(时钟,极性,相位,比特顺序等)
void SPIClass::beginTransaction(SPISettings settings)
- 结束本次传输,但不释放总线
void SPIClass::endTransaction()
- SPI默认配置的时钟是1MHz,比特顺序是高位先行,使用MOD0的模式
class SPISettings
{
public:
SPISettings() :_clock(1000000), _bitOrder(SPI_MSBFIRST), _dataMode(SPI_MODE0) {}
SPISettings(uint32_t clock, uint8_t bitOrder, uint8_t dataMode) :_clock(clock), _bitOrder(bitOrder), _dataMode(dataMode) {}
uint32_t _clock;
uint8_t _bitOrder;
uint8_t _dataMode;
};
SPI主机库数据传输方法
void transfer(void * data, uint32_t size);
发送指定数量的字节数
下面的方法同时接收相同字节数的数据
uint8_t transfer(uint8_t data);
发送一个字节
uint16_t transfer16(uint16_t data);
2个字节
uint32_t transfer32(uint32_t data);
4个字节
void transferBytes(const uint8_t * data, uint8_t * out, uint32_t size);
void transferBits(uint32_t data, uint32_t * out, uint8_t bits);
SPI从机库
需要包含第三方库#include"ESP32SPISlave.h"
一般不使用从机模式
ESP32使用DMA的方式进行SPI的传输
SD卡/TF卡模块
使用基于SPI协议的传输方式
Arduino中自带的一个读写SD卡的类:fs::SDFS不支持中文
使用第三方的SD库SdFat:SdFat库链接
下载后可放在Arduino第三方库文件夹中解压.
plantform中当前工程库存放路径:C:\Users\Xue_s\Documents\PlatformIO\Projects\SDcard\.pio\libdeps\airm2m_core_esp32c3\SdFat - Adafruit Fork
在SdFat-2.2.2\SdFat-2.2.2\src中的SdFatConfig.h修改配置USE_UTF8_LONG_NAMES(0改为1)
#ifndef USE_UTF8_LONG_NAMES
#define USE_UTF8_LONG_NAMES 0
#endif // USE_UTF8_LONG_NAMES
------------------------------------
#ifndef USE_UTF8_LONG_NAMES
#define USE_UTF8_LONG_NAMES 1
#endif // USE_UTF8_LONG_NAMES
将USE_UTF8_LONG_NAMES定义为1 ,这样字符串编码就使用utf8了,不然是ascii.
- 功能
– 支持UTF-8
– 支持FAT16,FAT32,exFAT - 头文件
– #include<SPI.h>
– #include"SdFat.h" - 相关的类
– SdFs 代表SD卡文件系统
– FsFile 代表的是文件
DSPI(Dual SPI)
多任务
进程->线程(任务)
时间片轮转调度,任务优先级高的分配的时间长一些
ESP32多任务相关函数
- 首先包含头文件
#include"freertos/FreeRTOS.h"
#include"freertos/task.h"
- 创建任务
BaseType_t xTaskCreatePinnedToCore( TaskFunction_t pvTaskCode,
const char * const pcName,
const uint32_t usStackDepth,
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pvCreatedTask,
const BaseType_t xCoreID);
pvTaskCode:指向任务输入函数的指针,任务函数原型:void task(void* param);
pcName:任务名称,最多16字节
usStackDepth:指定任务堆栈大小
pvParameters:用作所创建任务的参数的指针,在创建任务的时候可以向任务传递参数
uxPriority:任务优先级[0-25]级,数字越大,优先级越高
pvCreatedTask:指向任务句柄的指针,接收任务句柄
xCoreID:传入CPU核心,指明使用哪一颗CPU核心(ESP32有两个核心)
- 删除任务,如果在任务体中参数传入null结束任务
void vTaskDelete( TaskHandle_t xTaskToDelete ) PRIVILEGED_FUNCTION;
- 获取任务优先级,本任务传入null
UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask ) PRIVILEGED_FUNCTION;
- 获取本任务在哪个CPU上运行
static inline BaseType_t IRAM_ATTR xPortGetCoreID(void)
多任务及互斥量代码:可编辑链接,查看链接
互斥量(xSemaphoreHandle)
互斥量是一种特殊的二值信号量,它用于实现对临界资源的独占式处理(它不会屏蔽CPU的中断处理)
任意时刻的互斥量只有两种状态:开锁和闭锁.当互斥量被任务持有时,该互斥量处于闭锁状态,这个任务获得互斥量的所有权.
xSemaphoreHandle//互斥锁,也算是一种信号量
使用:
- 创建一个互斥锁
xSemaphoreHandle xMutex = xSemaphoreCreateMutex();
- 获取互斥锁(在普通任务中获取信号量)
xSemaphoreTake( xSemaphore, xBlockTime )
xSemaphore:信号量句柄
xBlockTime :等待的节拍数,0是立即返回
返回值:pdTRUE成功,pdFALSE失败 - 释放互斥锁
xSemaphoreGive( xSemaphore )
U8g2操作OLED屏幕
U8g2类名解析
- 第一部分:U8g2
- 第二部分:驱动芯片名
- 第三部分:显示大小
- 第四部分:OLED型号名
- 第五部分:
5.1 数字壹:有一个的显存,使用firstPage()/nextPage()
循环来更新屏幕,使用128字节的内存空间
5.2 数字二:有两页的显存,可以使用所有的函数,使用256字节的内存空间
5.3 字母F:完整的显示缓存,使用1024字节的内存空间 - 第六部分:OLED的通讯类型
常用的函数
- 创建对象
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2 //硬件I2C
(U8G2_R0,
/* clock=*/ SCL,
/* data=*/ SDA,
/* reset=*/ U8X8_PIN_NONE);
- 光标回到原点
home()
- 光标设置在(x,y)处,设置的是文字左下角的点
setCurser()
- 清除屏幕,清除缓冲区,光标回到左上角原点位置
clear()
- 清除内存中数据缓冲区
clearBuffer()
- 在屏幕上显示缓冲区的内容
sendBuffer()
- 修改的内容处于firstPage和nextPage之间,每次都重新渲染所有内容
firstPage()
,nextPage()
- 获取指定字体里最高/宽字体的高/宽度
u8g2.GetMaxCharHeight()
,u8g2_GetMaxCharWidth()
- 透明模式/实显模式
u8g2.SetDrawColor(0)
0:透明模式(字是透明的,背景是亮的)
1:实显模式(字是亮的,背景是透明的,默认)
刷屏方法:
//方法1:
u8g2.ClearBuffer();
//文字输出,图像输出
u8g2.SendBuffer();
//方法2:
u8g2.FirstPage();
do{
//文字输出,图像输出
}while(u8g2.NextPage);
输出文本函数:
- 绘制文本字符,编码小于等于255.需提前设置字体,
u8g2.DrawStr(x,y,"文本")
- 绘制编码为UTF-8的字符串,可以打印中文
u8g2.drawUTF8(x,y,"字符")
- 绘制特殊文本图标,(需联合特殊字体)
u8g2.DrawGlyph(x,y,"HEX")
- 打印文本字符,可以打印中文(需联合光标位置
setCursor,setFont
)支持变量
u8g2.print()
输出图片
- 画图函数
DrawXBM(&u8g2, x, y, w, h, bitmap);
DrawXBMP(&u8g2, x, y, w, h, bitmap);
DrawXBM和DrawXBMP区别在于DrawXBMP支持PROGMEM(但是实测都支持)
PROGMEM是放在Flash中,不加放在RAM中
DAC及midi音乐
简谱
认识简谱链接
数字1-7对应do,re,mi,fa,sol,la,si
音的高低:
低音,中音,高音:分别对应数字下面加点,不加点,数字上面加点
音的长短
全音符:X—
二分音符:X-
四分音符:X
八分音符:X加1个下划线
十六分音符:X加2个下划线
调号:1=C,1=D,1=F
拍号:2/4,3/4,4/4
例:2/4是以四分音符为一拍,每小节唱两拍的时长
相关函数
- 输出电压
dacWrite(pin, value)
value:[0-255],对应的电压是0-3.3V
V o l t a g e = v a l u e 255 ∗ 3.3 V Voltage=\frac{value}{255}*3.3V Voltage=255value∗3.3V - 取消引脚的DAC功能
dacDisable(pin)
可以使用ADC输出正弦波
f
(
t
)
=
A
s
i
n
(
ω
t
+
ψ
)
+
c
f(t)=Asin(ωt+ψ)+c
f(t)=Asin(ωt+ψ)+c
ω
=
2
π
f
ω=2πf
ω=2πf
其中: A是振幅,c是直流分量,要控制这两个值让f(t)的值域在[0,3.3]中
如: 让A为128,c为127.这样就可以输出主流分量为1.65V,振幅为1.65V,最小值为0,最大值为3.3v不失真的正弦波
对于t: 可以固定每隔固定的时间计算一次,如100us计算一次,每次将计算结果用dacWrite输出,就得到了正弦波信息
采样频率大于输出正弦波频率的两倍(内奎斯特定理)
I2S
I2S:Inter-IC Sound总线,又称集成电路内置音频总线,飞利浦公司为数字音频设备之间传输而制定的一种总线标准.
I2S是一种串行总线,传输的是PCM,DPM音频数据(未经压缩的音频采样数据裸流)
I2S特点:支持全双工/半双工.支持主从模式
PCM的参数:声道数,采样位数,采样频率
采样的数据量=采样频率x量化位数x声道数(bps)
底层API方式
首先包含头文件#include"driver/i2s.h"
通信的流程:
i2s_driver_install
加载i2s的驱动
i2s_set_pin
设置i2s使用的引脚
i2s_driver_uninstall
卸载i2s驱动
ADC通道设置
esp_err_t i2s_set_adc_mode(adc_unit_t adc_unit, adc1_channel_t adc_channel);
设置衰减值
使能ADC测量功能
esp_err_t i2s_adc_enable(i2s_port_t i2s_num);
DAC通道设置
esp_err_t i2s_set_dac_mode(i2s_dac_mode_t dac_mode);
数据传输:读取
esp_err_t i2s_read(i2s_port_t i2s_num, void *dest, size_t size, size_t *bytes_read, TickType_t ticks_to_wait);
数据传输:发送
esp_err_t i2s_write(i2s_port_t i2s_num, const void *src, size_t size, size_t *bytes_written, TickType_t ticks_to_wait);
开始和结束
esp_err_t i2s_start(i2s_port_t i2s_num);
esp_err_t i2s_stop(i2s_port_t i2s_num);
字节序:大小端模式
大端模式:重要数据保存在内存的低地址中,和我们的阅读顺序一样(big endian)
小端模式:
X86计算机是小端模式
WAV