在上一篇中,老周用 .NET Nano Framework 给大伙伴们演示了 WS2812 灯带的控制,包括用 SPI 和 红外RMT 的方式。利用 RMT 是一个很机灵的方案,不过,可能很多大伙伴对 ESP32 的 RMT 不是很熟悉。除了乐鑫自己的文档,没几个人写过相关的水文,可见这里头空白的水市场很充足,老周一时手痒,就决定再水一篇博文。
不管你有没有做过物联网项目,只要你有关注,你就会发现,当今时尚流行忽悠不擦嘴巴。许多教程就拿个 MicroPython 或者 Arduino,贴几行代码,然后叫你烧录进去看效果。可是,效果看完了,你知道了啥?你学到了啥?你知道这里头做了啥?全 TM 不知。做教程的人只管忽悠,然后就没下文了。这就是它们老喜欢用 Python 的原因。基于脚本语言的特性,很多库都是高度封装的,拿来直接敲几行代码就完事了。写教程的是这样,做培训的也是这样。
用 Arduino 好不好?好,开柜即用;用 MicroPython 好不好?好,开桶即用。这就是现在为什么 Py 流行的原因,做培训的演示起来多起劲,几行代码(估计他们为这几行代码都练了无数次,都背下来了)天天敲,而且这么简单的代码,现场演示也不怕出错,然后告诉你:看看,做 AI,做 Iot 多简单!但是,老周是很 BS 这些人的,只告诉你吃鱼很香,却不告诉你怎么捕的鱼。
为什么很多人学物联网就三分热度,一开始好玩轻松,玩几下就没意思了,直接 6 了。这就是学 Python 的害人之处。封装得太好了,入门前快乐满满,学起来简简单单。让菜鸟们对他们自己很烂的技术有了100%的信心,甚至天真地以为这就是编程。而等到真正入门了就没啥好玩了,也不知道自己学了啥,更不知道在干啥,很难再深入。于是弃坑了。Python 不是不能用,而是你不能指望凭它来学编程。脚本语言本来就是做辅助用的。
如果你一开始用的是 C 语言,就算你没在做项目,你反而可以坚持玩几年,甚至十几年。哪怕业余玩玩,也能一层一层地挖掘出很多有趣的东西。
还有一种更离谱的观点:Py 适合科研人员,可以快速验证结果。C语言留给开发的苦逼去干。老周认为:做科研的人在底层和基础知识方面更应该比开发的人强,不然你研究个鸵鸟蛋!连基本的原理和细节都搞不清楚,那就是纸上谈兵,骗经费吃喝P赌罢了。就像现在某些建筑,某些服装,为什么会出现许多反人类设计;很多产品也是反人类设计?正是因为做设计的人对生产、对技术、对基础原理不了解,闭上眼睛无脑瞎编乱涂。有些设计人员对自己、对产品、对他人也是不负责的,自己设计的东西做出来,也不去试用一下,看看你设想的东西多么不靠谱。
所以,老周写的东西,一直以来都是立足于实际使用的,而不是立足炒作和无脑吹。吹得天花乱坠,如果用起来很难用的东西,老周是不会推荐的。
好了,不小心扯了一堆没用的了。有大伙伴可能说老周这么批别人不会得罪人吗?得不得罪有啥关系呢,老周跟他们又不是一伙的,没有利益关系,他们敢拿导弹轰老周吗?
OK,不扯蛋了。说回 RMT,ESP32 中,一个周期 RMT 消息共 32 位,分两段,每段16位。然后老周给你画个图。
别以为 32 个位这么多能描述一整条消息,不是的,它只描述了一个脉冲周期罢了。你看,这个脉冲是不是被分成了两段?为什么要分成两段?因为这样就能说清楚了:高电平占了多长时间,低电平占了多长时间。也就是说,这一帧的数据包含了两个电平的参数。
1-16 位是第一个电平,前15位表示该电平持续的时间,最后一位(图中的 L)表示电平,1表示高电平,0表示低电平;
17-32 位是第二个电平,前15位表示该电平的时长,最后一位表示电平,1是高,0是低。
举个例子:
0000 1101 0011 0111 0011 1111 0101 1000
先看第一行,最后一位是1,说明是低电平,时长就是 0000 1101 0011 011,不含最后一位。
第二行呢,最后一位是0,说明是低电平,时长就是 0011 1111 0101 100,不含最后一位。
如果整个脉冲全是低电平呢,那就这样:
0000 0000 1111 0110 0000 0000 0110 1010
最后一位都是0,就表明这个周期没有高电平。于是,你能想到,如果一个周期内全是高电平呢,是不是这样?
0000 1111 0101 0011 0000 1000 0110 0111
至于电平的时间长度是单位,这个要看定时器的频率的。还记得吗?上一篇水文中,老周说默认用的是 APB 时钟,80 MHz,假设我们分频后让定时器的频率变成 1 MHz,即 1 000 000 Hz,然后 1s / 1000000 = 0.000001 秒,即 1 微秒(us)/ Tick。那么,这个15位的整数就和微秒数一致。
现在,你明白了 RMT 是怎么描述一个脉冲的了,于是,IDF 中有这么个类型:
typedef union { struct { uint16_t duration0 : 15; uint16_t level0 : 1; uint16_t duration1 : 15; uint16_t level1 : 1; }; uint32_t val; } rmt_symbol_word_t;
咦,这个类型咋这么怪啊?不怪,这种货叫做内联,说人话就是:里面的结构体和 val 的值共用内存。
前面的 struct 有四个字段:
duration0:第一个电平的时长,后面的冒号和15表示它占 15 位;
level0:表示第一个电平值,占一位;
duration1:第二个电平的时长,占 15 位;
level1:第二个电平的值,占一位。
那么,我问你,这四个字段加起来多位,是不是 32 ?val 的类型是 uint32 ,无符号32位整数。前面的结构体和 val 是不是大小相同?都是4个字节?是吧,于是,它们用同一块内存,也就是说,这个rmt_symbol_word_t 你可以用四个字段去设置它,也可以直接用一个整数去设置。C 语言是直接操作内存的,可以强制转换,在后面调用相关函数时,可以取地址直接赋值给 void* (指针)。
请你记住这个类型,你可以字面翻译为”符号字“,或者叫 RMT 描述符号。记好了,一个符号字只描述一个周期的脉冲哦。要是向 WS2812 发数据,RGB共 24 位,一个灯珠你就要发 24 个 符号字,点亮两个灯就发 48 个符号字。我要点100个灯呢,那就 24*100 呗。你不妨理解为:一个符号字就是代表一个二进制位。有几个二进制位就得发送几个符号。
这里要说明一点:.NET Nano Framework 用的 IDF 是 4xx 的,而目前新的版本是 5xx 的,新旧版本之间在 RMT 操作上有很大区别,函数也不同。不过,原理差不多,说直白一点就是:把内存中的 rmt_symbol_word_t 队列发送出去。
由于版本更新,.NET Nano Framework 后面肯定要适配新版 IDF 的,所以,老周决定用新的版本的方式演示。在新版本 API 中,不需要分频设定了。其实直接设置频率更好,尤其是对初学者,总觉得分频很难懂。不过老周可以把分频总结为:把总线/或CPU/或其他振荡源的频率除以某个数,得到更低的频率。即原来的频率太高了,要降一降。比较,原频 120 MHz,分频系数为 4,那就调整为 120/4 = 30 MHz。树莓派(Raspberry Pi Pico)Pico 的官方SDK中,PWM的频率也是用到了分频。不过小草莓先分频,再计数。先把频率降一下,然后周期性地数 256(0-255),如果计数满 255 重新回到 0,再计数。所以,RPI Pico 的 PWM 频率其实算起来挺麻烦,要考虑分频,还要考虑计数次数。
ESP 32 新的 IDF 直接让你配置频率了,这样更方便更直观。
下面老周说说 RMT API 怎么用。不要听别人造谣,说 IDF 很难用,其实不难用的。毕竟是官方的,功能很全,官方服务器托管团队直接维护。老周安装 IDF 就没失败过,这里再次强调用两点,保证你能成功安装:1、装好 Python 后,pip 改国内源;2、在 VS Code 的 Esp 插件中下载 IDF时,选乐鑫的服务器,不要选 github。
然后,其他选项你随意。其实它无非就用到两个目录,一个放 IDF 的源码,一个放编译的 tools。然后会设置环境变量 IDF_PATH 等。
下面请记住一个万能规律,不管你用的什么开发板,什么芯片,什么平台,所有外部设备的通信都是这样的流程:
1、配置参数;
2、init(初始化);
3、加载驱动(一般在 init 时就完成,这一步许多平台可省略);
4、读/写数据;
5、清理资源。
一、配置阶段
RMT API 定义专门的结构体,用于配置参数。
typedef struct { gpio_num_t gpio_num; rmt_clock_source_t clk_src; uint32_t resolution_hz; size_t mem_block_symbols; size_t trans_queue_depth; int intr_priority; struct { uint32_t invert_out: 1; uint32_t with_dma: 1; uint32_t io_loop_back: 1; uint32_t io_od_mode: 1; } flags; } rmt_tx_channel_config_t;
这是配置发送的,如果接收数据,要用rmt_rx_channel_config_t,用起来一样,搞懂一个,另一个就懂了。注意,接收和发送的函数是分布在两个头文件中的,发送是 rmt_tx.h,接收是 rmt_rx.h。因为驱动 WS2812 是输出,属于发送模式,咱们只用rmt_tx_channel_config_t 结构体。
不要看它那么多成员,其实,在实际使用时,咱们不需要全都用,不用的保持默认(不赋值就是了)。
gpio_num:用来发信号的引脚,GPIO 号。这个可用枚举值(在 gpio_num.h 头文件中),如GPIO_NUM_0 表示 GPIO0,GPIO_NUM_33 表示 GPIO33,也可以直接用整数,如 33、25、8 等。
clk_src:振动的时钟源,可以用RMT_CLK_SRC_DEFAULT 表示默认值,即用 APB 时钟,80兆那个。一般不用选其他,毕竟不是每个板子都通用,默认是比较通用。
resolution_hz:这个就是直接设置频率了,不用思考分频的事了。
mem_block_symbols:分配内存量,常用 64。注意它的大小不是字节,而是 符号字(rmt_symbol_word_t),就是最开始咱们介绍那个,32位两个阶段那个,描述两个电平时长的。比如,设置64就是分配的内存可以放 64 个符号字,字节是 64 * 4,32位嘛,是吧,前面反复说了。
trans_queue_depth:队列深度,一般不要太大,4 或 8 均可。数据在传输时,不是马上就发出去的,而是放进一个队列中,然后驱动层会调度这个队列,慢慢发(其实很快发完)。设置为4表示队列中可以放(挂起)4条等待传输的符号字。
intr_priority:中断的优先值,非特殊情况保持默认。
另外,此结构体内嵌了一个 flags 结构体。
invert_out:是否电平反向,1表示开启。就是反转电平,比如,本来高的变低,低的变高。这个一般不用;
with_dma:是否走 DAM 通道,不占用CPU运算资源;
io_loop_back:就跟在电脑上 ping 127.0.0.1 一样,“我发给我自己”,即自发自收(在同一引脚上)。这个一般没啥用。
io_od_mode:是否设置为开漏模式。
二、初始化阶段
配置完相关参数后,调用rmt_new_tx_channel 函数,用已配置的参数创建通信通道。
esp_err_t rmt_new_tx_channel(const rmt_tx_channel_config_t *config, rmt_channel_handle_t *ret_chan);
config 引用配置结构体实例,ret_chan 接收创建的通道句柄,后面在发送数据时要用。所以,在调用此函数前,先声明一个rmt_channel_handle_t 类型的变量,最后是全局的。
新版 API 虽然精简了许多,但也有缺点:在配置好参数创建通道后,就不能再修改参数了,除非重新初始化。而旧版 API 是可以修改的。
三、启用通道
调用rmt_enable 函数启用通道。
esp_err_t rmt_enable(rmt_channel_handle_t channel);
channel 就是刚刚创建的通道。这一步很关键,也很容易遗忘。不启用通道的话,是无法接收和发送数据的。如果忘了,你测试来测试去,死活不能工作,你甚至会怀疑自己写错了协议。如果要禁用通道,可以调用rmt_disable 函数。
esp_err_t rmt_disable(rmt_channel_handle_t channel);
这两个函数都声明在 rmt_common.h 头文件中。
四、创建编码器
创建编码器可以在启用通道之前完成,第【三】、【四】阶段顺序不重要。IDF 内置两个编码器:
1、bytes encoder:就是把你给它的字节数组转换为符号字(前面说过的 rmt_symbol_word);
2、copy encoder:这玩意儿很玄,如果你看官方文档介绍可能会怀疑人生,不知道说啥。老周用一句话概括:这货就是不处理不转换,你直接把符号字传给它,然后它复制到驱动层的内存中,放入队列准备发送。数据只是被复制,不会修改。这是防止让驱动空间的代码跨空间引用用户代码,那样有内存泄漏的风险,复制数据就不存在跨空间长距离引用,发完就清理。用户代码可能长期保持数据的生命周期。
当然,你可以写自己的编码器(组合使用内置的编码器)。若要自定义,请认识一下rmt_encoder_t 结构体。
struct rmt_encoder_t { /* 编码时用 */ size_t (*encode)(rmt_encoder_t *encoder, rmt_channel_handle_t tx_channel, const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state); /* 重置编码器参数时用 */ esp_err_t (*reset)(rmt_encoder_t *encoder); /* 清理编码器时用 */ esp_err_t (*del)(rmt_encoder_t *encoder); };
这个结构体的成员都是函数指针,你让它们分别指向你定义的函数,就实现了自定义编码了。这个东西你可能看得很绕,为什么函数的输入参数还要 rmt_encoder_t ?这是因为 C 结构体不能继承,要想实现类开继承的功能,就得定义一个更大的结构体,然后大结构体中引用 rmt_encoder_t,模拟调用基类成员。由于 IDF 支持 C++,为了好用,你不妨用 C++ 类去封装。
看看官方的源码是怎么封装的。
typedef struct rmt_bytes_encoder_t { rmt_encoder_t base; // encoder base class size_t last_bit_index; // index of the encoding bit position in the encoding byte size_t last_byte_index; // index of the encoding byte in the primary stream rmt_symbol_word_t bit0; // bit zero representing rmt_symbol_word_t bit1; // bit one representing struct { uint32_t msb_first: 1; // encode MSB firstly } flags; } rmt_bytes_encoder_t; typedef struct rmt_copy_encoder_t { rmt_encoder_t base; // encoder base class size_t last_symbol_index; // index of symbol position in the primary stream } rmt_copy_encoder_t;
就是定义一个结构体,然后里面有个 base,base 就是 rmt_encoder_t 类型,这就等于从抽象基类派生出 rmt_bytes_encoder和rmt_copy_encoder类型,其他成员则用于参数配置。访问 encode、reset、del 函数指针时就通过 S.base.encode(….) 来调用。当然,你自己写的话不一定要搞那么复杂,就是按 rmt_encoder_t 结构的三个函数指针成员,引其引用你写的函数就行了。
初始化 bytes encoder 使用rmt_new_bytes_encoder 函数,初始化 copy encoder 使用rmt_new_copy_encoder 函数。调用函数前,先声明rmt_encoder_handle_t 类型的变量,该变量会引用创建的编码器,由函数的 ret_encoder 参数赋值。
esp_err_t rmt_new_bytes_encoder(const rmt_bytes_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder); esp_err_t rmt_new_copy_encoder(const rmt_copy_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder);
创建编码器后用变量保存引用,不需要我们手动调用,传输数据时会自动调用。
五、发送数据
发送数据调用rmt_transmit 函数,参数包括:刚创建的通道、编码器,以及要发送的符号字数组(多个符号字一同推入队列,不必一个一个推)。调用此函数只是把消息放进传输队列,至于是否立即发送,那看队列里面拥不拥挤了,由驱动层自行处理,我们不用管。
如果你不使用中断,但希望等到数据发出去了再执行后面的程序代码,那可以调用rmt_tx_wait_all_done 函数,它会等待指定的时间,直到数据发送出去才返回。等待时间可以用最大值——portMAX_DELAY。
六、清理
如果你的程序不是一直发数据,或只是特定时候发送。那传输完数据后应当清理相应的对象。
rmt_del_encoder:清除刚创建的编码器。
rmt_disable:禁用通道。
rmt_del_channel:清除通道。
如果程序一直发数据,可以不清理。
官方有一个示例是用 RMT 驱动灯带的,但那个用了混合编码器,弄得有点复杂,老周这里直接用 copy encoder 复制符号字。符号字咱们自己生成。
先做好初始化工作。
1、声明相关参数。
// 声明区 #define GPIO_NUM 6 // 引脚号 #define TICK_FREQ 10 * 1000000 // 频率 #define LED_NUM 24 // 灯珠数目
这里我把频率设置为 10 MHz,即一 tick 为 0.1 us。因为 WS2812 的电平时长有 0.2-0.8 us,所以要把 Tick 精确到 0.1 us,这样好控制。
2、声明全局变量。
static rmt_channel_handle_t txChannel; /* 编码器 */ static rmt_encoder_handle_t rfEncoder; /* 消息符号 */ static rmt_symbol_word_t zeroSymbol, oneSymbol, resetSymbol; /* 要传输的颜色数据 */ static rmt_symbol_word_t rgbSymbols[24 * LED_NUM] = {0};
注意符号字数组,大小是灯珠数 * 24。为什么24呢?因为 RGB 数据加起来24位,一个符号字只能描述一个位。
zeroSymbol 表示发送 0 时的电平,表示发送 1 时的电平,resetSymbol 是复位电平,每发完一次数据都要一个复位电平,告诉 WS2812 我这儿发送完了。这几个电平信息的初始化代码:
void init_symbols() { // 0码高电平 zeroSymbol.duration0 = 0.4 * (TICK_FREQ / 1000000); zeroSymbol.level0 = 1; // 0码低电平 zeroSymbol.duration1 = 0.8 * (TICK_FREQ / 1000000); zeroSymbol.level1 = 0; // 1码高电平 oneSymbol.duration0 = 0.8 * (TICK_FREQ / 1000000); oneSymbol.level0 = 1; // 1码低电平 oneSymbol.duration1 = 0.4 * (TICK_FREQ / 1000000); oneSymbol.level1 = 0; // 复位信号全为低电平 resetSymbol.duration0 = 25 * (TICK_FREQ / 1000000); resetSymbol.level0 = 0; resetSymbol.duration1 = 25 * (TICK_FREQ / 1000000); resetSymbol.level1 = 0; }
0 码这里设置的是 高电平持续 0.4 us,低电平持续 0.8 us;1 码相反。这里0.3-0.4,0.7-0.8都可以,老周这里设置大一点的值,不容易抽风。如果设置0.3 和 0.7,在 ESP32 Pico 上有时候会抽风(有的灯珠不亮或颜色不对)。
这个时间算的是 tick 周期计数,我们设的频率是每周期 0.1 us,除以1000000 就是一微秒内会 tick 多少次,这里就是 1 us tick 10 次,那么,0.4 us 就是 tick 0.4 * 10 = 4 次。就是这么算出来的。复位信号全是低电平,按数据手册是最少 50us,这里把50分两段,即电平1=25us,电平2=25us,电平值全为0。
那么,RGB 怎么转为符号字呢?WS 2812c 中是 GRB 排列的,其他的芯片可以查资料,或者多次试验来验证顺序。颜色值总共就 24 位,更简洁的方法是用一个 32 位整数来表示一个颜色。发送时从高位开始处理,每处理一位,就向左移一位。直接看代码。
void set_rgb(int index, uint32_t grb) { if (index 0 || index > LED_NUM - 1) { return; // 索引无效 } // 循环的开始和结束索引 int startIdx = index * 24; int endIdx = startIdx + 24; for (int i = startIdx; i ) { if (grb & 0x00800000) { // 1 rgbSymbols[i] = oneSymbol; } else { // 0 rgbSymbols[i] = zeroSymbol; } // 左移一位 grb 1; } }
index 是某个灯珠的索引,每一次处理都跟 0x00800000 进行“与”运算,就是确定第 24 位(最高)位是否为1,若为1就用oneSymbol 变量的值,若为0就用zeroSymbol 变量的值,赋值一轮后,让颜色值左移一位,就能实现从高位到低位发送了。
下面代码初始化发送通道和编码器。
void init_tx_channel() { rmt_tx_channel_config_t cfg = { // GPIO .gpio_num = GPIO_NUM, // 时钟源:默认是APB .clk_src = RMT_CLK_SRC_DEFAULT, // 分辨率,即频率 .resolution_hz = TICK_FREQ, // 内存大小,指的是符号个数,不是字节个数 .mem_block_symbols = 64, // 传输队列深度,不要设得太大 .trans_queue_depth = 4 // 禁用回环(自己发给自己) //.flags.io_loop_back=0 }; // 调用函数初始化 ESP_ERROR_CHECK(rmt_new_tx_channel(&cfg, &txChannel)); } void init_encoder() { // 目前配置不需要参数 rmt_copy_encoder_config_t cfg = {}; // 创建拷贝编码器 ESP_ERROR_CHECK(rmt_new_copy_encoder(&cfg, &rfEncoder)); }
调用 API 时,可以嵌套在ESP_ERROR_CHECK 宏中,它会自动检查调用是否成功,不成功就输出错误。
下面代码发送数据。
void send_data() { // 配置 rmt_transmit_config_t cfg = { // 不要循环发送 .loop_count = 0}; // 发送 ESP_ERROR_CHECK(rmt_transmit(txChannel, rfEncoder, rgbSymbols, sizeof(rgbSymbols), &cfg)); // 等待发送完毕 // ESP_ERROR_CHECK(rmt_tx_wait_all_done(txChannel, portMAX_DELAY)); // 发送复位信号 ESP_ERROR_CHECK(rmt_transmit(txChannel, rfEncoder, &resetSymbol, 1, &cfg)); // 等待完成 ESP_ERROR_CHECK(rmt_tx_wait_all_done(txChannel, portMAX_DELAY)); }
在 app_main 函数中,先显示红色,一秒后显示蓝色,再过一秒显示绿色。
while (1) { // 红色 for (i = 0; i ) { set_rgb(i, COLOR_U32(0xff, 0x0, 0x0)); } send_data(); // 延时 vTaskDelay(1000 / portTICK_PERIOD_MS); // 蓝色 for (i = 0; i ) { set_rgb(i, COLOR_U32(0x0, 0x0, 0xff)); } send_data(); // 延时 vTaskDelay(1000 / portTICK_PERIOD_MS); // 绿色 for (i = 0; i ) { set_rgb(i, COLOR_U32(0x0, 0xff, 0x0)); } send_data(); // 延时 vTaskDelay(1000 / portTICK_PERIOD_MS); }
vTaskDelay 是 RTOS 系统移植函数,表示当前任务延时。注意这个延时函数的参数不是秒或毫秒,而是“跑多少圈” Tick。portTICK_PERIOD_MS 表示一毫秒 Tick 的步数。为什么是相除,不是相乘?这个,老周举一个不太恰当的例子:假如你跑一圈有 2000 步,现在我要你跑 8000 步,问你要跑几圈 ?答案就是 8000 / 2000 = 4 圈。就是这样。
这些 RTOS 函数在包含头文件时得小心,你得先包含FreeRTOS.h,然后再包含其他头文件,否则容易报错。
下面是完整代码:
#include #include string.h> #include "driver/rmt_common.h" #include "driver/rmt_encoder.h" #include "driver/rmt_types.h" #include "driver/rmt_tx.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" // 声明区 #define GPIO_NUM 6 // 引脚号 #define TICK_FREQ 10 * 1000000 // 频率 #define LED_NUM 24 // 灯珠数目 // #define DELAY_MS 20 // 延时 // 将RGB转为GRB整数 #define COLOR_U32(r, g, b) ( (uint32_t)g 16 | (uint32_t)r 8 | (uint32_t)b) // 变量区 /* 发送通道 */ static rmt_channel_handle_t txChannel; /* 编码器 */ static rmt_encoder_handle_t rfEncoder; /* 消息符号 */ static rmt_symbol_word_t zeroSymbol, oneSymbol, resetSymbol; /* 要传输的颜色数据 */ static rmt_symbol_word_t rgbSymbols[24 * LED_NUM] = {0}; /************* 自定义函数 ******************/ void init_tx_channel() { rmt_tx_channel_config_t cfg = { // GPIO .gpio_num = GPIO_NUM, // 时钟源:默认是APB .clk_src = RMT_CLK_SRC_DEFAULT, // 分辨率,即频率 .resolution_hz = TICK_FREQ, // 内存大小,指的是符号个数,不是字节个数 .mem_block_symbols = 64, // 传输队列深度,不要设得太大 .trans_queue_depth = 4 // 禁用回环(自己发给自己) //.flags.io_loop_back=0 }; // 调用函数初始化 ESP_ERROR_CHECK(rmt_new_tx_channel(&cfg, &txChannel)); } /* 初始化符号 */ void init_symbols() { // 0码高电平 zeroSymbol.duration0 = 0.4 * (TICK_FREQ / 1000000); zeroSymbol.level0 = 1; // 0码低电平 zeroSymbol.duration1 = 0.8 * (TICK_FREQ / 1000000); zeroSymbol.level1 = 0; // 1码高电平 oneSymbol.duration0 = 0.8 * (TICK_FREQ / 1000000); oneSymbol.level0 = 1; // 1码低电平 oneSymbol.duration1 = 0.4 * (TICK_FREQ / 1000000); oneSymbol.level1 = 0; // 复位信号全为低电平 resetSymbol.duration0 = 25 * (TICK_FREQ / 1000000); resetSymbol.level0 = 0; resetSymbol.duration1 = 25 * (TICK_FREQ / 1000000); resetSymbol.level1 = 0; } /* 初始化编码器 */ void init_encoder() { // 目前配置不需要参数 rmt_copy_encoder_config_t cfg = {}; // 创建拷贝编码器 ESP_ERROR_CHECK(rmt_new_copy_encoder(&cfg, &rfEncoder)); } /* 设置颜色 */ void set_rgb(int index, uint32_t grb) { if (index 0 || index > LED_NUM - 1) { return; // 索引无效 } // 循环的开始和结束索引 int startIdx = index * 24; int endIdx = startIdx + 24; for (int i = startIdx; i ) { if (grb & 0x00800000) { // 1 rgbSymbols[i] = oneSymbol; } else { // 0 rgbSymbols[i] = zeroSymbol; } // 左移一位 grb 1; } } /* 发送数据 */ void send_data() { // 配置 rmt_transmit_config_t cfg = { // 不要循环发送 .loop_count = 0}; // 发送 ESP_ERROR_CHECK(rmt_transmit(txChannel, rfEncoder, rgbSymbols, sizeof(rgbSymbols), &cfg)); // 等待发送完毕 // ESP_ERROR_CHECK(rmt_tx_wait_all_done(txChannel, portMAX_DELAY)); // 发送复位信号 ESP_ERROR_CHECK(rmt_transmit(txChannel, rfEncoder, &resetSymbol, 1, &cfg)); // 等待完成 ESP_ERROR_CHECK(rmt_tx_wait_all_done(txChannel, portMAX_DELAY)); } void app_main(void) { // 1、初始化通道 init_tx_channel(); // 2、初始化符号 init_symbols(); // 3、初始化编码器 init_encoder(); // 4、使能通道 ESP_ERROR_CHECK(rmt_enable(txChannel)); int i; /* 进入循环 */ while (1) { // 红色 for (i = 0; i ) { set_rgb(i, COLOR_U32(0xff, 0x0, 0x0)); } send_data(); // 延时 vTaskDelay(1000 / portTICK_PERIOD_MS); // 蓝色 for (i = 0; i ) { set_rgb(i, COLOR_U32(0x0, 0x0, 0xff)); } send_data(); // 延时 vTaskDelay(1000 / portTICK_PERIOD_MS); // 绿色 for (i = 0; i ) { set_rgb(i, COLOR_U32(0x0, 0xff, 0x0)); } send_data(); // 延时 vTaskDelay(1000 / portTICK_PERIOD_MS); } }
下面是效果:
好了,今天就水到这里了。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net
相关推荐: jupyter快速实现单标签及多标签多分类的文本分类BERT模型
jupyter实现pytorch版BERT(单标签分类版) nlp-notebooks/Text classification with BERT in PyTorch.ipynb 通过改写上述代码,实现多标签分类 参考解决方案 ,我选择的解决方案是继承Ber…