Arduin和LED或如何升级儿童设计师



我儿子坚定地“勾搭”了磁性建筑商Magformers 。 在浏览了一系列具有相同构造函数的Fixik之后,孩子问:“爸爸,为什么固定技术的细节让人眼前一亮,但我们却没有?”

事实证明,确实有一个“ Magformers Neon LED Set”,其中除了通常的构造块外,还带有一个带有LED的元件。 由于到那时我们已经收集了一整箱各种形状和尺寸的磁铁(对我而言, 中国的变形金刚根本不逊于原始的磁铁),我不知何故不想为了灯泡而购买另一组磁铁。 此外,该套件的成本比没有背光的同类套件要高得多。

估计组件中只有几美元,而我已经拥有了其中的大部分,所以我决定收集我的morgulka。 是的,并具有原始图像所没有的效果。

在猫下,您可以在ATTiny85上找到一个指示灯闪烁,并在WS8212 LED上找到LED面板。 我将讨论电路,如何通过电池为整个设备供电以及过程中遇到的显而易见的问题。 我还将详细讨论该项目的软件组件。

第一步


在我看来,普通LED(甚至是RGB)上的辉光无聊而平淡。 但是感觉像WS8212似乎很有趣。 在ebee上,提供了单个LED和尺寸最大为16x16的矩阵。 购买了几个不同的模块后,我决定使用4x4矩阵。 其中有许多LED可以沉迷于各种视觉效果,而该模块的尺寸可与设计者的方形方框的窗口相媲美。



为了控制LED矩阵,仅微控制器的一个引脚就足够了,因此,即使arduino nano也看起来像半身像(此外,它也不适合装入外壳中)。 但是事实证明,ATTiny85控制器上的digispark克隆是正确的-它没有大量的内存和引脚,但足以容纳LED闪烁器。 该模块与Arduino IDE完美集成,并且板上带有USB Bootloader,因此对该模块进行编程非常简单和舒适。 我很想尝试一下。

从最简单的方案开始。



通过这种形式,可以快速调试所有发光/闪烁算法(下面将对它们进行调试)。 但是,并非只有有线玩具-您需要考虑电池电量。 此外,为了不使手指电池破产(而且电池不适合装在外壳中),决定使用锂。 并且由于有锂电池,因此您需要考虑如何充电。 在垃圾箱中,我们刚刚在TP4056芯片上找到了一个“受欢迎的”充电控制器,这是当时购买的。

但是它并没有马上生效。 Digispark ATTiny85模块的电路不是为此专门设计的-有USB电源,但是电源直接通过+5总线(通过+5总线)或从VIN输入提供给微控制器,但电源通过7805线性稳压器供电。未提供插入USB连接器和微控制器之间间隙的电缆。 我不得不稍微修改一下电路并删除其他细节。



因此,现在将USB电源提供给VIN引脚,然后再提供给充电器输入。 充电器的输出(实际上是电池直接连接)通过5V脚回到板上。 尽管实际上会有3至4.2V(电池电压),这是很正常的-微控制器的工作电压范围为1.8-5.5V。 即使LED模块在2.7V电压下也能正常工作,尽管在3.2V以下,蓝色的LED还是有点缺乏,而颜色则以黄色“浮动”。

为了节省能源,常亮的D2 LED也消失了。 现在的总体方案如下所示



可以通过充电器中的USB连接器为电路供电,但随后将失去通过控制板上USB连接器上传固件的能力。 可以留出两个USB连接器以用于各种目的-一个用于充电,另一个用于固件,但这在某种程度上是错误的。

我在ebay上购买了6x25x35尺寸的电池,但事实证明它是有缺陷的,或者是由于短路或较大的充电电流将其杀死(默认情况下,充电电流设置为1A,您需要焊接一个电阻器以减小电流)。 无论如何,当连接负载时,即使在10 mA时,电池上的电压也会降至1V。 在测试时,我从小型直升机上换了一块半没电的锂电池。 过了一会儿,我从另一个卖家那里订购了电池,结果证明它很好。

原则上,可以停下来,焊接连接线,然后将所有物体轻轻推入某种类型的外壳中,但是我决定测量电路的功耗。 然后我哭了。 好吧,在工作状态下(灯泡完全发光时),这东西消耗的电流高达130mA,因此在静止状态下,消耗电流超过25mA! 即 这个信号灯在不到一天的时间内吃了我的600mAh电池!

原来,消耗约10 mA的LED。 即使它们不点亮,微控制器仍可在​​其中的每一个中工作并等待命令。 即 您需要为LED提供一个掉电电路。

其余的15 mA由微控制器消耗。 是的,它可以放在床上,根据数据表,功耗将以微安为单位进行测量,但实际上不可能获得小于1 mA的电流。 我关闭了ADC并将引脚转换为输入。 似乎电路中的某处存在某种泄漏,但是我对电子学的有限了解不足以找到并理解它。

我们使计划复杂化


然后我想起我购买了PT1502芯片进行测试。 该芯片是锂电池充电控制器,带有带几个控制输入的电源。 唯一的困难是微电路采用4x4 mm QFN20封装,并且需要绑扎。 在家焊接很难,但有可能。 对于常规的LUT来说,这笔费用很难,而且必须从中文订购。 但是我们不怕困难,对吗?

在几个方框中,该方案可以描述如下。



在关闭状态下,不会为控制器和LED供电。 该设备具有一个“电源”按钮,该按钮可以打开闪光灯(也可以切换模式)。 LED会闪烁一分钟,如果没有用户活动(没有人按下按钮),则设备将关闭。 即 它不仅会进入睡眠状态,而且还会通过Power Hold信号关闭自身的电源。 而且,它可以立即关闭所有内容-微控制器和LED指示灯。 电源开启和关闭功能在PT1502芯片内部实现

剩下的就是绘制电路图并制作电路板。 大部分电路都与PT1502数据表以及Digispark ATTiny85模块捆绑在一起。 PT1502电源控制器的微电路在功能上分为几个部分,因此,它在电路中分为几个块。



实际上,这是具有自己的线束的锂电池充电控制器。 LED1指示充电状态-亮起,然后充电开始。 电阻R6将充电电流设置为470mA。 因为我有600mAh的电池,原则上您可以增加电流,并在780-800 Ohms的电阻上放置一个最大600mA的电流。 但是,我不确定电池的特殊品质-充电速度最好慢一些,但持续时间更长。

考虑电源计划



SW1按钮可启动整个系统-PT1502芯片会自行唤醒,然后启动所有电源(其中有3个)。 接通电源后,微电路将通过释放RESET信号来启动微控制器。 为了便于调试,我还添加了一个单独的“重置”按钮。

保持信号用于关闭整个系统。 当微控制器启动时,应在此线上设置单元。 当需要舍入时,微控制器在HOLD线上置零,并且PT1502电源芯片将停止所有电源。

可以借助BAT_LOW输出跟踪电池电量不足的情况,但是在本文中,我对它进行了评分-您无需保存任何数据,并且如果您没有及时注意到电池没电,则不会爆炸。 死了就死了。 但以防万一,董事会为此事务提供了联系。

让我们回到SW1按钮一秒钟。 我决定不制作2个用于打开和控制的单独按钮。 因此,同一按钮也连接到ATTiny85,并且在操作过程中切换闪烁模式。 选择分频器R7-R8的值,以免烧坏PB2微控制器的端口。 对于所有电池电压范围(3.3-4.2V),将在指定的数据表限制(0.7 * VCC-VCC + 0.5V)内向控制器的脚提供电压。

考虑电源



这是一个脉冲式DC-DC转换器。 输出电压由电阻R10-R11设置,并根据数据表中的公式设置为3.3V。 其他一切都是简单的捆扎。

永远来说,实际上并不需要这种欺骗性的电源-通常有可能直接从电池为微控制器供电。 只是此源已经在PT1502芯片中实现,并且可以在需要时打开/关闭-为什么不使用它?



该芯片还具有2个线性稳定器,但我不会使用它们。 不幸的是,事实证明,仍然有必要向该电源提供输入电压,否则微电路会认为电源仍然不够稳定并且无法启动微控制器(这一知识是通过一周来回焊接测试板而获得的-我无法理解为什么它不起作用)

让我们继续进行逻辑部分。



USB电缆未从Digispark板上搭接。 这对于协调USB电压(运行3.3V)和微控制器的信号(最初由5V供电)是必要的。 由于在我的情况下,微控制器也由3.3V供电,因此可以简化电路,但以防万一,我与板上的原始电路离婚了。



微控制器的绑定没有什么有趣的。

最后一点就是连接器



实际上,我在ATTiny85上为自己准备了这样的调试板,带有USB支持和带有锂电池的电源控制器。 因此,我不仅限于将线路输出到LED。 取而代之的是,我将微控制器的所有线路都带到了梳子上,同时又方便了与编程器的连接。

将来,让几乎所有线路都牢固地绑定到某种功能(PB1-保持线,PB2-电源按钮,PB3 / PB4-USB,PB5-重置),将有可能绕过某些限制。 例如,请勿焊接USB电缆并释放PB3 / PB4线。 或者,例如,拒绝重置并释放PB5。 同时,只有PB0保持空闲-并将我们的LED连接到它。

我们进入董事会。 考虑到40x40mm的电路板尺寸,PT1502芯片的组件数量和QFN20外壳的限制,我什至没有考虑在家中制造该电路板。 因此,我立即开始选育最紧凑的两层板。 那就是我得到的



为了易于使用,我在背面签名了所有可能的输出功能(我从Digispark板上获得了灵感)



我在JLCPCB上订购了该 。 老实说,我对质量不太满意-如果您多次焊接芯片,则PT1502小触点附近的掩模会有点混浊。 好吧,小的铭文浮了一下。 但是,如果所有东西都是第一次焊接,则符合规范。

要焊接QFN20,您需要一个烙铁,其他所有东西都可以使用具有一定技能的烙铁进行焊接。 这就是焊接板的样子



房屋


现在该继续船体了。 我将其打印在3D打印机上。 简洁的设计-框和按钮。 包装盒上提供了特殊的挂钩,用于将萤火虫安装在设计人员的标准方形模块中。



主板和电池都放在箱子里。





LED面板安装在盖子上,然后用螺丝将其固定在主机箱上

起初,我想到了用螺钉将LED面板固定到盖板上,但最后我只是将其粘贴在双面胶带上。 原来是这样的



以这种形式,该设备已经可以使用,但看起来仍然很丑-扩散器不足。

我试图使用带有构造吹风机的PET瓶收缩技术制作扩散器的第一个版本(在飞机模型中可见)。

因此,首先需要空白。 我是用石膏制成的,然后倒入在3D打印机上打印的表格中。 在第一个版本中,该表单是一个整体,而我却永远无法将其从中拉出。 因此,我不得不做一个两部分的形式。



该方法的思想如下。 您将一瓶婴儿酸奶放在空白处,然后用建筑吹风机坐下。 这里只是从不同的牛奶中重新装满20个不同的容器,我从来没有设法把它放好,没有褶皱和气泡。 显然,您需要围住某种真空安装和塑料垫板。 总的来说,对于这样的手艺来说,这太难了。

穿过地鼠之后,我发现了两米的Verbatim PET透明塑料探针。 我决定尝试扩散器只是为了打印。 尽管在打印机的入口处,塑料看上去像是透明的,但真正的零件却很钝。 这可能是由于内部结构,如 层不能完全填充体积,但会与间隙和间隙重叠。 此外,如果您尝试用砂纸加工零件以获得更光滑的表面,我们将得到更多的无光泽。 但是,这正是我所需要的。

我懒得打扰扩散器的安装架,所以我将其添加到热胶中。 因此,我的设计现在可以有条件地折叠了。 我可能会对某种闩锁的发明感到困惑,但是我已经用光了透明的塑料探针。 因此,使其热熔。





韧体


对于LED闪烁器,您无需特别深入微控制器的外围-使用GPIO的几个功能就足够了。 但是,由于该模块与Arduino平台对接,那么为什么不利用它呢?

首先,一些定义和常量

// Number of total LEDs on the board. Mine has 4x4 LEDs #define NUM_HW_PIXELS 16 // Pin number where LED data pin is attached #define DATA_PIN 0 // Pin number where mode switch button is attached #define BUTTON_PIN 2 // Power Enabled pin #define POWER_EN_PIN 1 // Max brightness (dimming the light for debugging) #define MAX_VAL 255 

这确定了矩阵中的像素数,引脚数和LED的最大亮度(在调试过程中,将其设置为50十分方便,这样它就不会让我看不见)

我矩阵中的LED以一种非显而易见的方式排列-之字形。 因此,为了获得不同的效果,我不得不重新编号。

 // LED indexes for different patterns uint8_t circleLEDIndexes[] = {0, 1, 2, 3, 4, 11, 12, 13, 14, 15, 8, 7}; uint8_t beaconLEDIndexes[] = {6, 5, 10, 9}; uint8_t policeLEDIndexes[] = {7, 6, 10, 11, 4, 5, 9, 8}; 

为了控制LED,我没有重新发明轮子,而是准备了一个现成的库来处理WS8211 LED 。 库界面已被粉刷​​一遍。 一些辅助功能(例如,将HSV转换为RGB)也卡在其中。

首先,需要初始化开发板和WS8211库。

 // Driver Ai_WS2811 ws2811; void setup() { // Set up power pinMode(POWER_EN_PIN, OUTPUT); digitalWrite(POWER_EN_PIN, HIGH); // initialize LED data pin pinMode(LED_PIN, OUTPUT); // Initialize button pin pinMode(BUTTON_PIN, INPUT); // Initialize WS8211 library static CRGB ledsBuf[NUM_HW_PIXELS]; ws2811.init(DATA_PIN, NUM_HW_PIXELS, ledsBuf); // Set the watchdog timer to 2 sec wdt_enable(WDTO_2S); } 

首先,您需要将POWER HOLD信号设置为1,这将是向PT1502芯片发出的信号,表明微控制器已经闭合并且工作正常。 反过来,只要将HOLD信号设置为1,微电路将定期为微控制器和LED供电。

接下来,配置用于控制输出上的LED和输入上的按钮的支脚。 之后,您可以初始化WS8211库。

由于这是一个相当自治的设备,因此不可能让微控制器陷入无法理解的状态并吞噬整个电池。 为此,我启动看门狗定时器2秒钟。 计时器将在主程序循环中重新启动。

现在,您需要定义几个辅助功能。 WS8211库存储一个缓冲区,其中包含每个LED的颜色值。 直接使用缓冲区不是很方便,因为我编写了一个简单的函数将RGB值写入特定的LED

 void setRgb(uint8_t led_idx, uint8_t r, uint8_t g, uint8_t b) { CRGB * leds = ws2811.getRGBData(); leds[led_idx].r = r; leds[led_idx].g = g; leds[led_idx].b = b; } 

但是在大多数情况下,在RGB颜色模型中,对颜色进行计数不是很方便,甚至是不可能的。 例如,绘制任何类型的彩虹时,使用HSV颜色模型会更加方便。 每个像素的颜色由色调和亮度的值设置。 为了简单起见,省略了饱和(使用最大值)。 色调值减小到0-255的范围(而不是标准的0-359)。

 /** * HVS to RGB conversion (simplified to the range 0-255) **/ void setHue(uint8_t led_idx, int hue, int brightness) { //this is the algorithm to convert from RGB to HSV double r = 0; double g = 0; double b = 0; double hf = hue/42.6; // Not /60 as range is _not_ 0-360 int i=(int)floor(hue/42.6); double f = hue/42.6 - i; double qv = 1 - f; double tv = f; switch (i) { case 0: r = 1; g = tv; break; case 1: r = qv; g = 1; break; case 2: g = 1; b = tv; break; case 3: g = qv; b = 1; break; case 4: r = tv; b = 1; break; case 5: r = 1; b = qv; break; } brightness = constrain(brightness, 0, MAX_VAL); setRgb(led_idx, constrain(brightness*r, 0, MAX_VAL), constrain(brightness*g, 0, MAX_VAL), constrain(brightness*b, 0, MAX_VAL) ); } 

该函数取自Ai_WS8211库,并作了简要介绍。 在库中此函数的原始版本中,存在一些错误,由于这些错误,彩虹上的颜色带有抽搐。

让我们继续实现各种效果。 从主循环调用每个函数以绘制一个“框架”。 由于每个效果在调用之间使用不同的参数进行操作,因此它们存储在静态变量中。

这是最简单的效果-所有LED都填充有一种颜色,颜色会平滑变化。

 void rainbow() { static uint8_t hue = 0; hue++; for (int led = 0; led < NUM_HW_PIXELS; led++) setHue(led, hue, MAX_VAL); ws2811.sendLedData(); delay(80); } 

下一个效果更有趣-它沿着矩阵的轮廓显示一条彩虹,并且彩虹中的颜色逐渐移动一个圆圈。

 void slidingRainbow() { static uint8_t pos = 0; pos++; for (int led = 0; led < ARRAY_SIZE(circleLEDIndexes); led++) { int hue = (pos + led*256/ARRAY_SIZE(circleLEDIndexes)) % 256; setHue(circleLEDIndexes[led], hue, MAX_VAL); } ws2811.sendLedData(); delay(10); } 

这种效果会以随机的颜色填充整个矩阵,该颜色先平滑点亮,然后又平滑熄灭。

 void randomColorsFadeInOut() { static uint8_t color = 0; static bool goesUp = false; static uint8_t curLevel = 0; if(curLevel == 0 && !goesUp) { color = rand() % 256; goesUp = true; } if(curLevel == MAX_VAL && goesUp) { goesUp = false; } for(int led = 0; led < NUM_HW_PIXELS; led++) setHue(led, color, curLevel); if(goesUp) curLevel++; else curLevel--; ws2811.sendLedData(); delay(10); } 

下一组效果将绘制不同的闪烁信标。 因此,例如,一个孩子喜欢用磁铁建造推土机,而橙色闪光器在那里将非常有用。

 void orangeBeacon() { const int ORANGE_HUE = 17; static uint8_t pos = 0; pos+=3; for (int led = 0; led < ARRAY_SIZE(circleLEDIndexes); led++) { int brightness = brightnessByPos(pos, led*255/ARRAY_SIZE(circleLEDIndexes), 70); setHue(circleLEDIndexes[led], ORANGE_HUE, brightness); } ws2811.sendLedData(); delay(1); } 

从技术上讲,效果看起来像沿着矩阵移动的亮点。 但是为了使它看起来很漂亮,当您远离主要点时,相邻的LED逐渐消失。 因此,我需要一个计算该亮度的函数。

 int brightnessByPos(int pos, int ledPos, int delta) { int diff = abs(pos - ledPos); if(diff > 127) diff = abs(256-diff); int brightness = MAX_VAL - constrain(MAX_VAL*diff/delta, 0, MAX_VAL); return brightness; } 

Pos是亮度发光点的特定条件位置,映射到0-255的环回范围。 ledPos是需要计算其亮度的LED的位置(在相同范围内显示)。 如果位置差大于delta,则LED不会点亮,并且距离位置越近,它的发光越亮。

或者,例如,警察的红蓝色闪烁灯

 void policeBeacon() { const int RED_HUE = 0; const int BLUE_HUE = 170; static uint8_t pos = 0; pos += 2; for (int led = 0; led < ARRAY_SIZE(policeLEDIndexes); led++) { int ledPos = led*255/ARRAY_SIZE(policeLEDIndexes); int brightness = brightnessByPos(pos, ledPos, 50); setHue(policeLEDIndexes[led], RED_HUE, brightness); if(brightness == 0) { brightness = brightnessByPos((pos+100) % 256, ledPos, 50); setHue(policeLEDIndexes[led], BLUE_HUE, brightness); } } ws2811.sendLedData(); delay(1); } 

既然我们在谈论汽车,那么这里的交通信号灯就不成问题。

这些功能包括在各个位置的各种交通信号灯。

 void clearPixels() { for(int i=0; i<NUM_HW_PIXELS; i++) { setRgb(i, 0, 0, 0); } } void redTrafficLights() { for(int i=0; i<4; i++) setRgb(i, MAX_VAL, 0, 0); ws2811.sendLedData(); } void yellowTrafficLights() { for(int i=4; i<8; i++) setRgb(i, MAX_VAL, MAX_VAL, 0); ws2811.sendLedData(); } void greenTrafficLights() { for(int i=8; i<16; i++) setRgb(i, 0, MAX_VAL, 0); ws2811.sendLedData(); } 

现在该复兴它了。 交通信号灯根据以某种字节码定义的特殊程序进行操作。 铭牌描述了该模式以及必须打开该模式的时间。

 enum TRAFFIC_LIGHTS { NONE, RED, YELLOW, GREEN }; struct trafficLightState { uint8_t state; uint16_t duration; }; const trafficLightState trafficLightStates[] = { {NONE, 1}, // clear yellow {RED, 7000}, // red {YELLOW, 2000}, // red + yellow {NONE, 1}, // clear red+yellow {GREEN, 7000}, // green {NONE, 300}, // Blinking green {GREEN, 300}, // Blinking green {NONE, 300}, // Blinking green {GREEN, 300}, // Blinking green {NONE, 300}, // Blinking green {GREEN, 300}, // Blinking green {NONE, 1}, // clear green {YELLOW, 2000}, // yellow }; 

实际上处理所有功能

 void trafficLights() { static uint8_t curStateIdx = 0; static unsigned long curStateTimeStamp = 0; // Switch to a new state when time comes if(millis() - curStateTimeStamp > (unsigned long)trafficLightStates[curStateIdx].duration) { curStateIdx++; curStateIdx %= ARRAY_SIZE(trafficLightStates); curStateTimeStamp = millis(); } switch(trafficLightStates[curStateIdx].state) { case NONE: clearPixels(); ws2811.sendLedData(); break; case RED: redTrafficLights(); break; case YELLOW: yellowTrafficLights(); break; case GREEN: greenTrafficLights(); break; default: break; } // Just waiting delay(10); } 

达到指定的时间间隔后,下一个交通信号灯模式将打开,并且倒数计时再次开始。

我的想象力足够发挥的最后一个效果是星星。 5个随机的LED以随机的亮度点亮,然后平稳关闭。 如果一颗星星熄灭,则随机位置的另一颗星星会亮起。

 void stars() { const uint8_t numleds = 5; static uint8_t ledIndexes[numleds] = {0}; static uint8_t curVal[numleds] = {0}; static uint8_t maxVal[numleds] = {0}; for(int i=0; i<numleds; i++) { if(ledIndexes[i] == 0) { uint8_t led = rand() % (NUM_HW_PIXELS+1); CRGB * leds = ws2811.getRGBData(); if(leds[led].r == 0) { ledIndexes[i] = led; maxVal[i] = rand() % (MAX_VAL-1) + 1; curVal[i] = 0; } } else { uint8_t led = ledIndexes[i]; if(curVal[i] < maxVal[i]) curVal[i]++; else if(curVal[i] == maxVal[i]) maxVal[i] = 0; else if(curVal[i] == 0 || --curVal[i] == 0) ledIndexes[i] = 0; setRgb(led-1, curVal[i], curVal[i], curVal[i]); } } ws2811.sendLedData(); delay(80); } 

一个邪恶的虫子潜入这里。 有时星星会突然亮起,反之亦然。 但老实说,我懒得弄清楚-看起来很正常。

现在该考虑节省电池了。 我已经给出了整个东西的消耗值。 如果您不打算关闭电源,LED将在数小时内耗尽电池。 此功能负责在闲置90秒后关闭电源。 最初是60秒,但是对于一个真正的游戏来说,这还不够,而且2分钟还很长。

 void shutdownOnTimeOut(bool resetTimer = false) { static unsigned long periodStartTime = 0; if(periodStartTime == 0 || resetTimer) { periodStartTime = millis(); return; } if(millis() - periodStartTime >= 90000UL) { periodStartTime = 0; shutDown(); } } 

实际断电发生如下。

 void shutDown() { clearPixels(); ws2811.sendLedData(); wdt_disable(); digitalWrite(POWER_EN_PIN, LOW); // No power after this point while(true) ; } 

如果用户按下按钮,计时器将重置。 经过设置的时间后,该功能将HOLD信号设置为零,这是PT1502命令关闭电源。 顺便说一句,看门狗也需要停止,否则2秒钟后它将唤醒系统并再次打开电源。

最后,开始一切的主循环

 // List of pointers to functions that serve different modes void (*Modes[])() = { rainbow, slidingRainbow, orangeBeacon, policeBeacon, trafficLights, stars, randomColorsFadeInOut }; void loop() { static uint8_t mode = eeprom_read_byte( (uint8_t*) 10 ); static bool waitingForBtnUp = false; static long btnPressTimeStamp; // Button switches mode if(digitalRead(BUTTON_PIN) == HIGH && !waitingForBtnUp) { delay(20); if(digitalRead(BUTTON_PIN) == HIGH) { mode++; mode %= ARRAY_SIZE(Modes); // num modes clearPixels(); ws2811.sendLedData(); delay(1); eeprom_write_byte( (uint8_t*) 10, mode ); waitingForBtnUp = true; btnPressTimeStamp = millis(); shutdownOnTimeOut(true); } } // Shut down on long press over 5s if(digitalRead(BUTTON_PIN) == HIGH && waitingForBtnUp && millis() - btnPressTimeStamp > 5000) shutDown(); // Detect button release if(digitalRead(BUTTON_PIN) == LOW && waitingForBtnUp) waitingForBtnUp = false; // display LEDs according to current mode Modes[mode](); // pong shutdown timer shutdownOnTimeOut(); // Yes, we still alive wdt_reset(); } 

按下按钮可切换模式并重置自动关闭计时器。 根据当前模式,将启动“模式”列表中的一种效果功能。 在每个周期,看门狗也会复位。

例如,如果一个孩子在玩警车,并且1.5分钟后应急灯熄灭,那么儿子第二次打开后,很可能会想继续玩警车。 为此,将选定的模式保存在EEPROM中(从推土机中选择单元编号10)。

这是一个视频,显示了它们如何工作。


引导程序


几乎一切都准备就绪。 但是还需要归档另一件事-引导加载程序。 事实是标准的引导加载程序不适合我们。

首先,当您打开电源时,它会等待长达6秒的时间-也许固件会开始涌入其中。 仅在此控制权转移到主固件之后。 这在开发阶段很方便,但在成品设备中会很烦人。

其次,标准的引导加载程序对PT1502芯片一无所知,这对于发出HOLD信号将是很好的。 如果没有该信号,则微电路认为微控制器没有启动,或者相反,希望关闭。 如果是这样,那么几毫秒后PT1502将切断整个电路的电源。

解决这两个问题的好处并不难。 digispark ATTiny85使用微核引导程序 。 该引导程序很容易满足我们的需求。 仅需要更正配置文件中的相应定义。

首先,我将标准固件\ configuration \ t85_default配置复制到我自己的目录中,并已在其中进行了所有更改。 因此,很容易回滚到原始引导加载程序。

在bootloaderconfig.h文件中,可以选择如何输入引导程序。 从开箱即用的内容来看,没有什么适合我们的,但是最接近的选项是ENTRY_JUMPER。 在此选项中,仅当特定电平出现在特定引脚上(跳线在板上已闭合)时,才可以访问引导加载程序。

 #define ENTRYMODE ENTRY_JUMPER 

我们没有跳线,但是PB2的脚上有一个按钮。 如果在打开电源时按住按钮5-7秒钟,则让引导加载程序进入。 但是,如果按下并释放,则将立即过渡到主固件。

我们需要定义3个功能-初始化,取消初始化,以及实际检查是否该进入引导加载程序。 最初,它们都很简单,并使用宏实现。 只有前2个会很简单

 #define HOLD_PIN PB1 #define JUMPER_PIN PB2 #define JUMPER_PORT PORTB #define JUMPER_DDR DDRB #define JUMPER_INP PINB #define bootLoaderInit() {JUMPER_DDR &= ~_BV(JUMPER_PIN); JUMPER_DDR |= _BV(HOLD_PIN); JUMPER_PORT &= ~_BV(JUMPER_PIN); JUMPER_PORT |= _BV(HOLD_PIN); _delay_ms(1);} #define bootLoaderExit() {;} 

bootLoaderInit()将按钮引脚(JUMPER_PIN)配置为输入,并关闭其上的悬挂器。 我们已经在板上和地面上安装了一个上拉电路,相反,当您按下图钉上的按钮时,就会出现一个上拉电路。 同时,您可以立即配置HOLD信号以输出并将其设置为...

例如,有关位运算的说明,请转到此处 ,例如, 从此处可以了解AVR控制器中的GPIO设置寄存器。

bootLoaderExit()函数为空,因为 公开的配置非常适合随后过渡到主固件

bootLoaderStartCondition()函数无法以宏格式输入Bootloader,因此该函数不适合使用,因此已成为功能全面的函数

 #ifndef __ASSEMBLER__ // Bootloader condition is to hold the button for 5 seconds inline unsigned char bootLoaderStartCondition() { long int i; for(i=0; i<10000000; i++) if( !(JUMPER_INP & _BV(JUMPER_PIN))) return 0; return 1; } #endif 

该功能在几秒钟内(实际上大约为6-7)检查按钮的状态。 如果该按钮是较早释放的,则我们无需输入引导程序。 耐心和持久允许进入引导加载程序。

事实证明,bootloaderconfig.h文件涉及汇编程序文件的编译,并且该文件中的代码会导致错误。 我不得不将函数放在#ifndef __ASSEMBLER__块中

我调整的另一个参数告诉引导加载程序如果未连接到USB怎么办-一秒钟后退出。 事实是,在闯入过程中,儿子经常按下按钮,不小心进入了引导程序。 我不知道这有多神奇,但是如果引导加载程序没有看到USB连接,它可能会意外覆盖一些内存页面。 因此,如果没有连接,我们将直接退出主程序。

 /* * Define bootloader timeout value. * * The bootloader will only time out if a user program was loaded. * * AUTO_EXIT_NO_USB_MS The bootloader will exit after this delay if no USB is connected. * Set to 0 to disable * Adds ~6 bytes. * (This will wait for an USB SE0 reset from the host) * * All values are approx. in milliseconds */ #define AUTO_EXIT_NO_USB_MS 1000 

我们编译...并得到一个错误,指出代码不适合分配给它的引导程序空间。 由于控制器中的闪存非常小,因此将引导加载程序压缩到最大,以便为主程序留出更多空间。 但这可以按照说明在Makefile.inc文件中轻松修复。

 # hexadecimal address for bootloader section to begin. To calculate the best value: # - make clean; make main.hex; ### output will list data: 2124 (or something like that) # - for the size of your device (8kb = 1024 * 8 = 8192) subtract above value 2124... = 6068 # - How many pages in is that? 6068 / 64 (tiny85 page size in bytes) = 94.8125 # - round that down to 94 - our new bootloader address is 94 * 64 = 6016, in hex = 1780 BOOTLOADER_ADDRESS = 1940 

然后,我只是将引导加载程序的起始地址减少到一页(64字节),从而增加了引导加载程序的空间。

否则,使用USBAsp编程器编译和上传引导加载程序就不是问题。

结论


从面包板上的原型到成品设备,这是一种非常有趣的方式。 从arduino课程上看,这似乎是普通的眨眼功夫,但实际上,在工作过程中,我不得不解决许多有趣的问题-这是消耗方面的挣扎,元件底座的选择以及外壳的设计,以及带有引导加载程序的固件。 我衷心希望我的经验对某人有用。

会更容易吗? 当然可以 我认为一切都可以用晶体管来完成。 不幸的是我在焊接电路板后阅读了本文 。 我会更早地看到该文章-我将使用相同的流行TP4056进行所有操作-焊接起来更容易。 无论如何,永远不需要此设备中PT1502内部的DC-DC转换器。 但是,对于我的其他项目,对PT1502微电路的实践研究对我很有用,而且对于QFN20封装中的微电路焊接能力也非常有用。

最后,这是我的项目的链接:

固件代码
电路板
外壳和扩散器型号
准备就绪的STL模型用于打印

Source: https://habr.com/ru/post/zh-CN435198/


All Articles