We recently had cause to build a simple serial/UART "network" of slave devices. We had a single "controller" device (which receives data from a PC and broadcasts it along the bus) and a number of similar "slave" type devices.
Normally, when it comes to multiple devices along a bus, we'd be thinking of either SPI (broadcast the message to all devices with an identifier in the message to which the appropriate devices repond) or I2C (each device could have its own unique hardware ID to which we address the messages).
But for a recent project we were asked if we could create a serial/UART bus. At first it seemed quite straight forward - simply tie all the TX lines of the "slaves" together and connect to the "RX" of the "master" and invert; tie all the RX lines of the "slaves" to each other and connect them to the "TX" of the master device.
The basic idea is that the master would broadcast a message to all devices, including a device ID in the message. When any device receives an end-of-message marker, it looks at the device ID. If the message is not intended for that device, it simply ignores it.
The theory works great.
Sometimes in hardware it works just fine.
But sometimes it goes horribly wrong.
Now of course if two devices try talking at once, you just get garbled nonsense (so at the end of each message we include a simple XOR sum to check if a message is valid). So this set-up only works if you can be sure that only one device is going to try to use the bus at any one time.
But sometimes we were getting devices resetting. Not all of them, and not all at the same time. Just some devices, sometimes. Which in turn indicates that one device is trying to drive a line high, while another is trying to drive it low. When this happens, we're effectively creating a dead-short between power and ground; so it's no wonder that the devices are resetting!
By simply putting a diode on each of the TX lines and a pull-up resistor on the "master" RX line we can overcome this problem easily. Now, when a device tries to drive a TX line high, the current can't get through the diode. But the pull-up resistor lets the TX line (connected to the RX of the master) float high. So the end result is the same.
But if another device drives the TX line low, it's enough to overcome the pull-up resistor, so the entire TX bus goes low (and the master RX line goes low). If one device tries to drive the TX line high and another low, the TX line goes low. The data at the other end might get garbled, but the important thing is that we don't get slave devices resetting.
It's basically the same idea used with SPI communications - drive a line low, release it to let it float high. But if we can't guarantee that our slave devices aren't going to try to drive the TX line high, the diode simply blocks that behaviour. When no devices are pulling the TX line low, it floats high anyway (which is the idle state of a UART transmitter anyway).
Simple.
But a trick worth knowing!
Sunday, 16 April 2017
Saturday, 15 April 2017
Creating primitives and textures in Unity
I love Unity. I love that you can write code and compile it to multiple platforms. I love that you can "hit up" the Asset Store and have a game working in a couple of hours. At least, a simple game.
But one of the things I've always fancied doing with Unity was have it load levels (from a web server perhaps) and create rooms and playing areas dynamically. We've played about with doing just that using pre-bought assets (it's not as easy as you think, if you're working on a grid-based system, since most assets have their origin in the dead centre, not on one corner!)
So as a bit of an experiment, we played about with creating a map "plane" from primitives, onto which we'll dynamically load textures. So at the start of the "game" there's nothing on screen - then a few script calls and we'll create some primitive shapes (after all, most floors and walls are not much more than simple rectangles) and apply some textures.
It's worth noting that we're creating a 2D top-down type map, even though we're using 3D shapes (the 3d shapes allow us to work with complex principles such as rotation and line-of-sight later on down the line).
We've set up our camera as orthographic and have it pointing straight down. We also added a directional light and made this a child of the camera - effectively following it as it moves over the map. We also created a "gameWorld" empty gameobject just to hold all our dynamically generated content, in case we need to turn the global world on/off for some reason in the future.
Now a couple of scripts to actually generate our primitive shapes and to apply textures to them. We're working on a grid-based map and each object we create in our game-world will be placed from the bottom-left corner:
But when you create a gameobject in Unity, the origin of the object is smack-bang in the centre. Which makes getting everything to line up in a grid a bit of a pain (especially if the objects are not perfectly square).
So whenever we create an object that we want to align on our grid, we "wrap it up" inside an empty gameobject and set the local x/y co-ordinates to half the height/width of the object. This way we can place our floors and walls without having to keep applying an offset to get the origin somewhere near the bottom-left corner.
With the gameobject in worldspace, placed at 0,0 half of the floor tile is beyond our 0,0 position (ok, it's only a quarter section, but you get the idea)
By placing the tile inside an empty game object, we can place the parent at 0,0 and offset the child by half the height/width and get our tile to appear where we want it "in world space".
Our "object creator" script is referenced by our "game controller" script.
When any primitive is created, it needs to be given a material to apply to it; so we create a global material, based on the "sprites/default" shader. This same material can be applied to all our primitive shapes. With a material applied, we can then change the texture property of each shape, with a newly-downloaded image, if necessary.
This script creates two "map tiles" each 8x8 units in size. It places the first at 0,0 and the second at 8,0 (immediately to the right of the first one). The script downloads the image board1.png and applies it to the first tile, and downloads the png image board2.png and applies it to the second tile.
The end result looks something like this:
When we place an object at 0,0 (in world space) it appears in the first square, from the bottom-left corner of the map. If we change the co-ordinates of the object to 3,4 in world space, it appears four squares in and five squares up from our "board origin" in the bottom-left corner of the map (remember our map starts at zero, so at x3, the object should appear on the fourth square in).
A liberal sprinkling of iTween functions and a simple download-map-data-via-xml and we're on the way to creating a top-down game which can load map layout data (and sprites/images) from a website - online map editing here we come!
But one of the things I've always fancied doing with Unity was have it load levels (from a web server perhaps) and create rooms and playing areas dynamically. We've played about with doing just that using pre-bought assets (it's not as easy as you think, if you're working on a grid-based system, since most assets have their origin in the dead centre, not on one corner!)
So as a bit of an experiment, we played about with creating a map "plane" from primitives, onto which we'll dynamically load textures. So at the start of the "game" there's nothing on screen - then a few script calls and we'll create some primitive shapes (after all, most floors and walls are not much more than simple rectangles) and apply some textures.
It's worth noting that we're creating a 2D top-down type map, even though we're using 3D shapes (the 3d shapes allow us to work with complex principles such as rotation and line-of-sight later on down the line).
We've set up our camera as orthographic and have it pointing straight down. We also added a directional light and made this a child of the camera - effectively following it as it moves over the map. We also created a "gameWorld" empty gameobject just to hold all our dynamically generated content, in case we need to turn the global world on/off for some reason in the future.
Now a couple of scripts to actually generate our primitive shapes and to apply textures to them. We're working on a grid-based map and each object we create in our game-world will be placed from the bottom-left corner:
But when you create a gameobject in Unity, the origin of the object is smack-bang in the centre. Which makes getting everything to line up in a grid a bit of a pain (especially if the objects are not perfectly square).
So whenever we create an object that we want to align on our grid, we "wrap it up" inside an empty gameobject and set the local x/y co-ordinates to half the height/width of the object. This way we can place our floors and walls without having to keep applying an offset to get the origin somewhere near the bottom-left corner.
With the gameobject in worldspace, placed at 0,0 half of the floor tile is beyond our 0,0 position (ok, it's only a quarter section, but you get the idea)
By placing the tile inside an empty game object, we can place the parent at 0,0 and offset the child by half the height/width and get our tile to appear where we want it "in world space".
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class object_creator : MonoBehaviour {
Material mat;
Shader shdr;
// if you're using one-square to one-unity-unit keep track of it here
// (in earlier versions, a 0.5 scaled plane - 5Uunits - represented a board
// of 8x8 grid, in which case square size would be 5/8 = 0.625)
private float square_size = 1f;
// Use this for initialization
void Start () {
}
void Awake(){
shdr = Shader.Find ("Sprites/Default");
if (shdr) {
mat = new Material (shdr);
} else {
Debug.Log ("wtf");
}
}
// Update is called once per frame
void Update () {
}
public GameObject createObject(string objName, GameObject objParent, float x, float y, float z, float size_x, float size_y, float size_height){
// creates a primitive (cube) wrapped inside an empty game object
// which is placed at the gameworld position x,y
// the position of the (empty) game object is such that the origin is in the
// bottom-left corner (not the centre as is usual with gameobjects)
GameObject piece = new GameObject();
piece.name = objName;
piece.transform.parent = objParent.transform;
piece.transform.localPosition = new Vector3 (x, z, y);
piece.transform.Translate(new Vector3(-square_size/2, 0, -square_size/2));
GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube.transform.parent = piece.transform;
cube.transform.localPosition = new Vector3 (size_x/2, 0, size_y/2);
cube.transform.localScale = new Vector3 (size_x, size_height, size_y);
return(piece);
}
public void setTexture(GameObject o, string imageName){
// get the child object in "o" with the name "Cube"
// (this is the actual shape, the game object is the container)
GameObject p = o.transform.FindChild("Cube").gameObject;
// download the texture for this object
string url="http://your_url/" + imageName + ".png";
StartCoroutine (downloadImage(url, p));
}
IEnumerator downloadImage(string url, GameObject o){
if (url.Length > 0) {
Debug.Log ("loading from " + url);
WWW www = new WWW (url);
yield return www;
Texture2D tex = new Texture2D (www.texture.width, www.texture.height);
www.LoadImageIntoTexture(tex);
o.GetComponent<Renderer> ().material = mat;
o.GetComponent<Renderer> ().material.mainTexture = tex;
o.GetComponent<Renderer> ().material.shader = shdr;
Debug.Log ("Texture set");
}
}
}
using System.Collections.Generic;
using UnityEngine;
public class object_creator : MonoBehaviour {
Material mat;
Shader shdr;
// if you're using one-square to one-unity-unit keep track of it here
// (in earlier versions, a 0.5 scaled plane - 5Uunits - represented a board
// of 8x8 grid, in which case square size would be 5/8 = 0.625)
private float square_size = 1f;
// Use this for initialization
void Start () {
}
void Awake(){
shdr = Shader.Find ("Sprites/Default");
if (shdr) {
mat = new Material (shdr);
} else {
Debug.Log ("wtf");
}
}
// Update is called once per frame
void Update () {
}
public GameObject createObject(string objName, GameObject objParent, float x, float y, float z, float size_x, float size_y, float size_height){
// creates a primitive (cube) wrapped inside an empty game object
// which is placed at the gameworld position x,y
// the position of the (empty) game object is such that the origin is in the
// bottom-left corner (not the centre as is usual with gameobjects)
GameObject piece = new GameObject();
piece.name = objName;
piece.transform.parent = objParent.transform;
piece.transform.localPosition = new Vector3 (x, z, y);
piece.transform.Translate(new Vector3(-square_size/2, 0, -square_size/2));
GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube.transform.parent = piece.transform;
cube.transform.localPosition = new Vector3 (size_x/2, 0, size_y/2);
cube.transform.localScale = new Vector3 (size_x, size_height, size_y);
return(piece);
}
public void setTexture(GameObject o, string imageName){
// get the child object in "o" with the name "Cube"
// (this is the actual shape, the game object is the container)
GameObject p = o.transform.FindChild("Cube").gameObject;
// download the texture for this object
string url="http://your_url/" + imageName + ".png";
StartCoroutine (downloadImage(url, p));
}
IEnumerator downloadImage(string url, GameObject o){
if (url.Length > 0) {
Debug.Log ("loading from " + url);
WWW www = new WWW (url);
yield return www;
Texture2D tex = new Texture2D (www.texture.width, www.texture.height);
www.LoadImageIntoTexture(tex);
o.GetComponent<Renderer> ().material = mat;
o.GetComponent<Renderer> ().material.mainTexture = tex;
o.GetComponent<Renderer> ().material.shader = shdr;
Debug.Log ("Texture set");
}
}
}
Our "object creator" script is referenced by our "game controller" script.
When any primitive is created, it needs to be given a material to apply to it; so we create a global material, based on the "sprites/default" shader. This same material can be applied to all our primitive shapes. With a material applied, we can then change the texture property of each shape, with a newly-downloaded image, if necessary.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class game_controller : MonoBehaviour {
public GameObject world;
public object_creator oc;
// Use this for initialization
void Start () {
GameObject o;
o = oc.createObject ("b1", world, 0f, 0f, 0f, 8f, 8f, 0.05f);
oc.setTexture(o,"board1");
GameObject o2 = oc.createObject ("b2", world, 8f, 0f, 0f, 8f, 8f, 0.05f);
oc.setTexture(o2,"board2");
}
// Update is called once per frame
void Update () {
}
}
using System.Collections.Generic;
using UnityEngine;
public class game_controller : MonoBehaviour {
public GameObject world;
public object_creator oc;
// Use this for initialization
void Start () {
GameObject o;
o = oc.createObject ("b1", world, 0f, 0f, 0f, 8f, 8f, 0.05f);
oc.setTexture(o,"board1");
GameObject o2 = oc.createObject ("b2", world, 8f, 0f, 0f, 8f, 8f, 0.05f);
oc.setTexture(o2,"board2");
}
// Update is called once per frame
void Update () {
}
}
This script creates two "map tiles" each 8x8 units in size. It places the first at 0,0 and the second at 8,0 (immediately to the right of the first one). The script downloads the image board1.png and applies it to the first tile, and downloads the png image board2.png and applies it to the second tile.
The end result looks something like this:
When we place an object at 0,0 (in world space) it appears in the first square, from the bottom-left corner of the map. If we change the co-ordinates of the object to 3,4 in world space, it appears four squares in and five squares up from our "board origin" in the bottom-left corner of the map (remember our map starts at zero, so at x3, the object should appear on the fourth square in).
A liberal sprinkling of iTween functions and a simple download-map-data-via-xml and we're on the way to creating a top-down game which can load map layout data (and sprites/images) from a website - online map editing here we come!
Sunday, 9 April 2017
AVR atmega328 PORTC not working AVCC
One of the things I've personally struggled with, switching between Arduino and PIC is the way the Arduino IDE/language deals with digital pins. I like to use terms like PORTB.5 (the sixth pin on portB) rather than the Arduino-specific "pin 13". Of course you can use direct port access with Arduino, but the convention is to address each individual pin using the crazy sequential numbering system.
I've been working with a couple of guys on a custom "Arduino" board - in actual fact, it's little more than an ATMega328P AVR chip on a custom PCB; necessary only because we wanted to use 8 inputs, 8 outputs, SPI and a single, reversible pin for serial communication. At first we wanted to use an Arduino Pro Mini but no matter which way we tried to route things, we always ended up with pins 10-17 (yep, digital 17) as inputs with pull-up resistors enabled.
As most Arduino users know, on most Arduino boards, pin 13 has an onboard LED. Which means we can't use it as an input (since the inline resistor on the LED is pulling the input pin low despite the internal pull-up).
We also wanted to use a full-bridge rectifier to protect our little delicate AVR chips (they really don't like being powered up in reverse and can easily let out the blue magic smoke if you get the power and ground pins the wrong way around!)
So we figured that the best idea would be a custom board with an AVR atmega328, with connectors for our inputs and outputs (routed to the nearest pins on the mcu, not necessarily in the digital pin number sequence) and multiple connectors for power and ground connected not to the AVR chip, but to pins 2 and 3 of the rectifier. The output of the rectifier is then connected to the AVR chip (pin1 to ground, pin 4 to power). This gives us power sockets which can be connected without worrying about the polarity of the power source.
So everything appeared to be working just fine - the chip booted up and sent data over serial, irrespective of the polarity of the power supply. We tested all the inputs and could see that they were all working. But we were surprised to see that some of our outputs simply didn't work; the serial debug log indicated that the inputs were being read correctly, but the outputs simply failed to go high.
We'd moved some pins around, putting our inputs onto the lower numbered pins with outputs on pins 10-17 (in case we ever wanted to return to the Arduino pre-made boards and needed to use the i/o pin with an LED connected to it). But it turned out that every one of our output pins numbered above 13 was not working. That's A0 (digital pin 14) A1 (pin 15) A2 (pin 16) and A3 (pin 17).
We've used pins numbered 14 - 19 as digital i/o in the past; pins 20-21 can be set to digital inputs but not outputs, but we've had no trouble in the past making A2 light up an LED, for example. But there was something not right with our isolated AVR chip on our custom board....
It took some desoldering and a while testing for continuity before we discovered a hairline fracture in the trace connecting Vcc to the AVcc pins. It turns out that you need power connected to the AVcc pin for any of PORTC to work as digital outputs.
And it also turns out that PORTC happen to include the Arduino digital pins 14 (C0) through to 19 (C5). So without power on our AVcc pin, pins 14-19 fail to work as outputs.
A quick bit of tack-soldering and short length of wire and everything worked perfectly! So there you have it - if your digital pins 14-19 fail to work as outputs, double-check your connection between Vcc and AVcc; it's not just some useless "alternative" connection, it does actually serve a purpose!
I've been working with a couple of guys on a custom "Arduino" board - in actual fact, it's little more than an ATMega328P AVR chip on a custom PCB; necessary only because we wanted to use 8 inputs, 8 outputs, SPI and a single, reversible pin for serial communication. At first we wanted to use an Arduino Pro Mini but no matter which way we tried to route things, we always ended up with pins 10-17 (yep, digital 17) as inputs with pull-up resistors enabled.
As most Arduino users know, on most Arduino boards, pin 13 has an onboard LED. Which means we can't use it as an input (since the inline resistor on the LED is pulling the input pin low despite the internal pull-up).
We also wanted to use a full-bridge rectifier to protect our little delicate AVR chips (they really don't like being powered up in reverse and can easily let out the blue magic smoke if you get the power and ground pins the wrong way around!)
So we figured that the best idea would be a custom board with an AVR atmega328, with connectors for our inputs and outputs (routed to the nearest pins on the mcu, not necessarily in the digital pin number sequence) and multiple connectors for power and ground connected not to the AVR chip, but to pins 2 and 3 of the rectifier. The output of the rectifier is then connected to the AVR chip (pin1 to ground, pin 4 to power). This gives us power sockets which can be connected without worrying about the polarity of the power source.
So everything appeared to be working just fine - the chip booted up and sent data over serial, irrespective of the polarity of the power supply. We tested all the inputs and could see that they were all working. But we were surprised to see that some of our outputs simply didn't work; the serial debug log indicated that the inputs were being read correctly, but the outputs simply failed to go high.
We'd moved some pins around, putting our inputs onto the lower numbered pins with outputs on pins 10-17 (in case we ever wanted to return to the Arduino pre-made boards and needed to use the i/o pin with an LED connected to it). But it turned out that every one of our output pins numbered above 13 was not working. That's A0 (digital pin 14) A1 (pin 15) A2 (pin 16) and A3 (pin 17).
We've used pins numbered 14 - 19 as digital i/o in the past; pins 20-21 can be set to digital inputs but not outputs, but we've had no trouble in the past making A2 light up an LED, for example. But there was something not right with our isolated AVR chip on our custom board....
It took some desoldering and a while testing for continuity before we discovered a hairline fracture in the trace connecting Vcc to the AVcc pins. It turns out that you need power connected to the AVcc pin for any of PORTC to work as digital outputs.
And it also turns out that PORTC happen to include the Arduino digital pins 14 (C0) through to 19 (C5). So without power on our AVcc pin, pins 14-19 fail to work as outputs.
A quick bit of tack-soldering and short length of wire and everything worked perfectly! So there you have it - if your digital pins 14-19 fail to work as outputs, double-check your connection between Vcc and AVcc; it's not just some useless "alternative" connection, it does actually serve a purpose!
Wednesday, 5 April 2017
Not all A3114 hall sensors are the same - who knew?
We were playing about with hall sensors again this week. We've used hall sensors a lot in the past, and had a bunch of A3114 sensors left over from previous projects. But there were only a couple left and the massive bag of left-overs was somewhere in a box in the black hole that the bungalow workshop has quickly become.
A few clicks on AliExpress later and we had some more A3114 sensors sent within just five days. We threw the lot together in a little component drawer and got on with making our project.
Hall sensors are often used as limit switches, but don't suffer from the problems that mechanical switches often do in dusty environments - namely there are no moving parts to get gunked up with dust, and no way the switch can get jammed. But when we tried using them, we got some weird results.
Some hall sensors plain simply didn't work.
Some triggered from about three inches away!
Some worked as we expected, triggering when a neodymium magnet approached from about 5mm away. And some acted less like switches and more like variable/analogue devices, with the output increasing in intensity as the magnet was moved closer.
To get to the bottom of things we created a simple hall sensor tester from a battery, an LED and a socket (into which we plugged our different hall sensors to try them out).
The first sensor in the video demonstrates how we expected the hall sensors to work; introduce a magnet and at a certain distance, the sensor acts like a switch and the LED lights up (in the video it appears to fade up quickly, but that's the camera auto-light-adjustment; in real life it switches almost instantly).
The last sensor in the video - although not immediately obvious in the film - appeared to work a tiny, tiny amount; if you looked right inside the LED, a tiny little dot of light was just about perceptible, when the magnet was right up against the sensor.
The second sensor in the video had us puzzled.
Not because it triggers from a long way away, but because it appears to have an almost-analogue-like behaviour - the intensity of the light increases/decreases as the magnet is moved towards/away from the sensor. The reason this was particularly puzzling is because A3114 sensors are supposed to have an inbuilt hysteresis.
The A3114 is supposed to have a "trigger" and "release" magnetic flux density with a "dead band" which reduces any "chatter" that might occur just at the point where the switch would normally activate (similar to the bounce in a mechanical switch).
Yet the second sensor in the video doesn't display either a trigger or a release threshold - the intensity of the LED changes in relation to the distance from the sensor. Which makes us wonder - what on earth kind of sensor is it?!
On closer inspection, we found that the sensors that worked as we expected them to were labelled 3114/515 and 3114/OH15.
The newer sensors are labelled 3114/402.
Which suggests that not all 3114 hall sensors are the same.
Who knew?
A few clicks on AliExpress later and we had some more A3114 sensors sent within just five days. We threw the lot together in a little component drawer and got on with making our project.
Hall sensors are often used as limit switches, but don't suffer from the problems that mechanical switches often do in dusty environments - namely there are no moving parts to get gunked up with dust, and no way the switch can get jammed. But when we tried using them, we got some weird results.
Some hall sensors plain simply didn't work.
Some triggered from about three inches away!
Some worked as we expected, triggering when a neodymium magnet approached from about 5mm away. And some acted less like switches and more like variable/analogue devices, with the output increasing in intensity as the magnet was moved closer.
To get to the bottom of things we created a simple hall sensor tester from a battery, an LED and a socket (into which we plugged our different hall sensors to try them out).
The first sensor in the video demonstrates how we expected the hall sensors to work; introduce a magnet and at a certain distance, the sensor acts like a switch and the LED lights up (in the video it appears to fade up quickly, but that's the camera auto-light-adjustment; in real life it switches almost instantly).
The last sensor in the video - although not immediately obvious in the film - appeared to work a tiny, tiny amount; if you looked right inside the LED, a tiny little dot of light was just about perceptible, when the magnet was right up against the sensor.
The second sensor in the video had us puzzled.
Not because it triggers from a long way away, but because it appears to have an almost-analogue-like behaviour - the intensity of the light increases/decreases as the magnet is moved towards/away from the sensor. The reason this was particularly puzzling is because A3114 sensors are supposed to have an inbuilt hysteresis.
The A3114 is supposed to have a "trigger" and "release" magnetic flux density with a "dead band" which reduces any "chatter" that might occur just at the point where the switch would normally activate (similar to the bounce in a mechanical switch).
Yet the second sensor in the video doesn't display either a trigger or a release threshold - the intensity of the LED changes in relation to the distance from the sensor. Which makes us wonder - what on earth kind of sensor is it?!
On closer inspection, we found that the sensors that worked as we expected them to were labelled 3114/515 and 3114/OH15.
The newer sensors are labelled 3114/402.
Which suggests that not all 3114 hall sensors are the same.
Who knew?
Friday, 31 March 2017
Resurrecting more old posts - word clock with Arduino and TinyRTC DS1307 module
I've always loved the idea of a word clock; a few of us have even built a couple over the years (though I don't actually manage to ever keep hold of the ones I've made). Mostly they're a fun way to prove our multiplexing/charlie-plexing is working properly. Sometimes just to demonstrate how to use a MAX7219 chip.
Recently we had to convert some old code over to Arduino, to use a MAX7219 LED driver. We'd had some trouble getting a 4-way 7-segment LED display to work properly with the MAX7219 chips - mostly because we were using common anode displays and the driver chips are best suited to common cathode (or was it the other way around...?). So we thought it'd be a good idea to strip the bigger project back to individual pieces; and for this task we focussed on just making the MAX7219 chips work.
A few months back we'd done some playing around with Arduino and MAX7219. This time we cascaded two chips and connected each MAX7219 to a single LED matrix. A bit of copy-n-paste coding later and we had a working example using the Arduino MAX Library
Having just moved a load of stuff into the new workshop bungalow, we had boxes and boxes of components hanging around - to hand were some RTC modules from a few years back; this gave us a perfect opportunity to put one to use
The RTC Library had us reading the current date/time off the TinyRTC module in just minutes!
After putting the two matrix boards side-by-side all we needed to do was read the current time over I2C and decide which LEDs needed to light up when. Here's the pattern of letters/words we came up with:
As we're effectively using a larger 16x8 matrix, we had plenty of extra letters after writing out the hours and minutes, so went to town with improving the precision of our clock.
Normally a word clock might say something like "it is ten past two" and display this until the time had changed to the next five-minute segment (in this case, "it is quarter past two"). We decided that we'd improve the precision by adding in "nearly" and "just gone" elements to each five-minute segment.
So, within two minutes of a time, we'd describe it as "nearly". For example at 14:04 we'd say "it is nearly five past two". Similarly, for up to two minutes after a time, we'd describe it is "just gone". So at 14:31 we'd say "it has just gone half past two".
This gives us the ability to tell the time to within two minutes, using language that many of us commonly use every day (although, as Nick pointed out, a margin of error of two minutes still leaves plenty time to miss your bus if you're not careful!)
Here's the code we came up with.
It's written to be readable/understandable rather than particularly "clever" (for example, we'd normally use an array for multi-line variables, like R[1][3] rather than multiple "single" variables, like R1C3 but as we've been getting a few questions of late on things we'd consider to be pretty simple, we tried to keep the code as understandable as possible!)
#include "Wire.h"
#define DS1307_I2C_ADDRESS 0x68
int dataIn = 2;
int load = 3;
int clk = 4;
int maxInUse = 2; //change this variable to set how many MAX7219's you'll use
// define max7219 registers
byte max7219_reg_noop = 0x00;
byte max7219_reg_digit0 = 0x01;
byte max7219_reg_digit1 = 0x02;
byte max7219_reg_digit2 = 0x03;
byte max7219_reg_digit3 = 0x04;
byte max7219_reg_digit4 = 0x05;
byte max7219_reg_digit5 = 0x06;
byte max7219_reg_digit6 = 0x07;
byte max7219_reg_digit7 = 0x08;
byte max7219_reg_decodeMode = 0x09;
byte max7219_reg_intensity = 0x0a;
byte max7219_reg_scanLimit = 0x0b;
byte max7219_reg_shutdown = 0x0c;
byte max7219_reg_displayTest = 0x0f;
int e = 0; // just a variable
// these are used to run in test mode
bool test_mode = false;
int curr_min = 0;
int curr_hr = 0;
// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val){
return ( (val/10*16) + (val%10) );
}
// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val){
return ( (val/16*10) + (val%16) );
}
void setDateDs1307(byte second, byte minute, byte hour, byte dayOfWeek, byte dayOfMonth, byte month, byte year){
Wire.beginTransmission(DS1307_I2C_ADDRESS);
Wire.write(0);
Wire.write(decToBcd(second)); // 0 to bit 7 starts the clock
Wire.write(decToBcd(minute));
Wire.write(decToBcd(hour)); // If you want 12 hour am/pm you need to set
// bit 6 (also need to change readDateDs1307)
Wire.write(decToBcd(dayOfWeek));
Wire.write(decToBcd(dayOfMonth));
Wire.write(decToBcd(month));
Wire.write(decToBcd(year));
Wire.endTransmission();
}
// Gets the date and time from the ds1307
void getDateDs1307(byte *second, byte *minute, byte *hour, byte *dayOfWeek, byte *dayOfMonth, byte *month, byte *year){
// Reset the register pointer
Wire.beginTransmission(DS1307_I2C_ADDRESS);
Wire.write(0);
Wire.endTransmission();
Wire.requestFrom(DS1307_I2C_ADDRESS, 7);
// A few of these need masks because certain bits are control bits
byte b;
b=Wire.read();
Serial.print(b,HEX);
*second = bcdToDec(b & 0x7f);
b=Wire.read();
Serial.print(b,HEX);
*minute = bcdToDec(b);
b=Wire.read();
Serial.print(b,HEX);
*hour = bcdToDec(b & 0x3f); // Need to change this if 12 hour am/pm
b=Wire.read();
Serial.print(b,HEX);
*dayOfWeek = bcdToDec(b);
b=Wire.read();
Serial.print(b,HEX);
*dayOfMonth = bcdToDec(b);
b=Wire.read();
Serial.print(b,HEX);
*month = bcdToDec(b);
b=Wire.read();
Serial.print(b,HEX);
*year = bcdToDec(b);
Serial.print(" ");
}
void putByte(byte data) {
byte i = 8;
byte mask;
while(i > 0) {
mask = 0x01 << (i - 1);
digitalWrite( clk, LOW);
if (data & mask){
digitalWrite(dataIn, HIGH);
}else{
digitalWrite(dataIn, LOW);
}
digitalWrite(clk, HIGH);
--i;
}
}
void maxAll (byte reg, byte col) { // initialize all MAX7219's in the system
int c = 0;
digitalWrite(load, LOW); // begin
for ( c =1; c<= maxInUse; c++) {
putByte(reg); // specify register
putByte(col);//((data & 0x01) * 256) + data >> 1); // put data
}
//digitalWrite(load, LOW);
digitalWrite(load,HIGH);
}
void maxOne(byte maxNr, byte reg, byte col) {
//maxOne is for addressing different MAX7219's,
//while having a couple of them cascaded
int c = 0;
digitalWrite(load, LOW); // begin
for ( c = maxInUse; c > maxNr; c--) {
putByte(0); // means no operation
putByte(0); // means no operation
}
putByte(reg); // specify register
putByte(col);//((data & 0x01) * 256) + data >> 1); // put data
for ( c =maxNr-1; c >= 1; c--) {
putByte(0); // means no operation
putByte(0); // means no operation
}
//digitalWrite(load, LOW); // and load da stuff
digitalWrite(load,HIGH);
}
void showClockOutput(byte hour, byte minute, byte second, byte dayOfMonth, byte month, byte year){
Serial.print(hour, DEC);
Serial.print(":");
Serial.print(minute, DEC);
Serial.print(":");
Serial.print(second, DEC);
Serial.print(" ");
Serial.print(dayOfMonth, DEC);
Serial.print("/");
Serial.print(month, DEC);
Serial.print("/");
Serial.print(year, DEC);
Serial.println();
}
void setup () {
byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
pinMode(dataIn, OUTPUT);
pinMode(clk, OUTPUT);
pinMode(load, OUTPUT);
//initiation of the max 7219
maxAll(max7219_reg_scanLimit, 0x07);
maxAll(max7219_reg_decodeMode, 0x00); // using an led matrix (not digits)
maxAll(max7219_reg_shutdown, 0x01); // not in shutdown mode
maxAll(max7219_reg_displayTest, 0x00); // no display test
for (e=1; e<=8; e++) { // empty registers, turn all LEDs off
maxAll(e,0);
}
maxAll(max7219_reg_intensity, 0x0f & 0x0f);
// initialise the RTC module
// (inc setting the time if necessary)
Wire.begin();
bool set_date=true;
if(set_date==false){
// change these values as necessary
second = 10;
minute = 46;
hour = 16;
dayOfWeek = 4;
dayOfMonth = 30;
month = 3;
year = 17;
setDateDs1307(second, minute, hour, dayOfWeek, dayOfMonth, month, year);
}
Serial.begin(9600);
Serial.println("Let's go");
}
void loop () {
byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
getDateDs1307(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year);
showClockOutput( hour, minute, second, dayOfMonth, month, year);
int r;
int r1c1, r1c2;
int r2c1, r2c2;
int r3c1, r3c2;
int r4c1, r4c2;
int r5c1, r5c2;
int r6c1, r6c2;
int r7c1, r7c2;
int r8c1, r8c2;
if(test_mode==true){
curr_min++;
if(curr_min > 59){ curr_hr++; curr_min=0;}
if(curr_hr > 23 ){ curr_hr=0; }
hour = curr_hr;
minute = curr_min;
}
// from the time, work out whether we start with its or it has
// (its nearly or it has just past)
r=0;
r1c1 = B11100000; r1c2 = 0x00;
r2c1 = 0x00; r2c2 = 0x00;
r3c1 = 0x00; r3c2 = 0x00;
r4c1 = 0x00; r4c2 = 0x00;
r5c1 = 0x00; r5c2 = 0x00;
r6c1 = 0x00; r6c2 = 0x00;
r7c1 = 0x00; r7c2 = 0x00;
r8c1 = 0x00; r8c2 = 0x00;
if(minute==1 || minute==2 || minute==6 || minute==7 || minute==11 || minute==12 || minute==16 || minute==17){ r=1; }
if(minute==3 || minute==4 || minute==8 || minute==9 || minute==13 || minute==14 || minute==18 || minute==19){ r=2; }
if(minute==21 || minute==22 || minute==26 || minute==27 || minute==31 || minute==32 || minute==36 || minute==37){ r=1; }
if(minute==23 || minute==24 || minute==28 || minute==29 || minute==33 || minute==34 || minute==38 || minute==39){ r=2; }
if(minute==41 || minute==42 || minute==46 || minute==47 || minute==51 || minute==52 || minute==56 || minute==57){ r=1; }
if(minute==43 || minute==44 || minute==48 || minute==49 || minute==53 || minute==54 || minute==58 || minute==59){ r=2; }
// just before or just after major clock minutes, light up the
// word "nearly" or "just past"
if(r==1){ r1c1 = B11011101; r1c2 = B11101111; }
if(r==2){ r1c1 = B11100000; r1c2 = B00000000; }
if(r==2){ r2c1 = B11111100; }
// maybe we could use the word "just gone" for one minute past and drop the "just"
// for two minutes past?
r=0;
if(minute==2 || minute==7 || minute==12 || minute==17){ r=1; }
if(minute==22 || minute==27 || minute==32 || minute==37){ r=1; }
if(minute==42 || minute==47 || minute==52 || minute==57){ r=1; }
if(r==1){
// mask out the word "just"
r1c1 = r1c1 & B11111110;
r1c2 = r1c2 & B00011111;
}
// at quarter past and quarter to the hour, light up the word quarter
if(minute >=13 && minute<=17){ r2c1+=1; r2c2 = B11111100; }
if(minute >=43 && minute<=47){ r2c1+=1; r2c2 = B11111100; }
// at twenty to and twenty past the hour, light up the word "twenty"
// (also at twenty-five to and twenty-five past)
if(minute >=18 && minute<=27){ r3c1 = B11111100; }
if(minute >=33 && minute<=42){ r3c1 = B11111100; }
// light up the word "five" at not only five to/past but also
// twenty-five to and twenty-five past
if(minute >=3 && minute<=7) { r3c1 +=1; r3c2 = B11100000;}
if(minute >=53 && minute<=57){ r3c1 +=1; r3c2 = B11100000;}
if(minute >=23 && minute<=27){ r3c1 +=1; r3c2 = B11100000;}
if(minute >=33 && minute<=37){ r3c1 +=1; r3c2 = B11100000;}
// at ten past/to the hour, light up the word ten
if(minute >=8 && minute <=12){ r3c2 = B00001110;}
if(minute >=48 && minute <=52){ r3c2 = B00001110;}
// at around half past...
if(minute >=28 && minute <=32){ r4c1 = B11110000;}
// display the either the word "past" or "two"
if(minute > 2 && minute <= 32){ r4c1 += B00000011; r4c2 = B11000000; }
if(minute > 32 && minute < 58) { r4c1 += B00001100; }
// this is for on the hour, o'clock
if(minute >=58 || minute <=2) { r8c1 = B11111110; }
// show the correct hour(s)
// (why compare to 32 and not 30 for half past the hour?
// because at 11:32 we want it still to read "it has just
// gone half past 11" - not half past twelve!
// just before/after one o'clock
if((hour==0 && minute > 32) || (hour == 1 && minute <=32)){ r7c2 = B00000111; }
if((hour==12 && minute > 32) || (hour == 13 && minute <=32)){ r7c2 = B00000111; }
// just before/after two o'clock
if((hour== 1 && minute > 32) || (hour == 2 && minute <=32)) { r5c1 = B11100000; }
if((hour==13 && minute > 32) || (hour == 14 && minute <=32)) { r5c1 = B11100000; }
// just before/after three o'clock
if((hour== 2 && minute > 32) || (hour == 3 && minute <=32)) { r4c2 += B00011111; }
if((hour==14 && minute > 32) || (hour == 15 && minute <=32)) { r4c2 += B00011111; }
// just before/after four o'clock
if((hour== 3 && minute > 32) || (hour == 4 && minute <=32)) { r5c1 = B00011110; }
if((hour==15 && minute > 32) || (hour == 16 && minute <=32)) { r5c1 = B00011110; }
// just before/after five o'clock
if((hour==4 && minute > 32) || (hour == 5 && minute <=32)){ r5c1 += 1; r5c2 = B11100000; }
if((hour==16 && minute > 32) || (hour == 17 && minute <=32)){ r5c1 += 1; r5c2 = B11100000; }
// just before/after six o'clock
if((hour==5 && minute > 32) || (hour == 6 && minute <=32)){ r6c1 = B11100000; }
if((hour==17 && minute > 32) || (hour == 18 && minute <=32)){ r6c1 = B11100000; }
// just before/after seven o'clock
if((hour==6 && minute > 32) || (hour == 7 && minute <=32)){ r5c2 = B00011111; }
if((hour==18 && minute > 32) || (hour == 19 && minute <=32)){ r5c2 = B00011111; }
// just before/after eight o'clock
if((hour==7 && minute > 32) || (hour == 8 && minute <=32)){ r6c1 = B00011111; }
if((hour==19 && minute > 32) || (hour == 20 && minute <=32)){ r6c1 = B00011111; }
// just before/after nine o'clock
if((hour==8 && minute > 32) || (hour == 9 && minute <=32)){ r6c2 = B11110000; }
if((hour==20 && minute > 32) || (hour == 21 && minute <=32)){ r6c2 = B11110000; }
// just before/after ten o'clock
if((hour==9 && minute > 32) || (hour == 10 && minute <=32)){ r6c2 = B00001110; }
if((hour==21 && minute > 32) || (hour == 22 && minute <=32)){ r6c2 = B00001110; }
// just before/after eleven o'clock
if((hour==10 && minute > 32) || (hour == 11 && minute <=32)){ r7c1 = B11111100; }
if((hour==22 && minute > 32) || (hour == 23 && minute <=32)){ r7c1 = B11111100; }
// this is just before/after twelve (noon)
if((hour==11 && minute > 32) || (hour == 12 && minute <=32)){ r7c1 = B00000011; r7c2 = B11110000; }
// this is just before/after midnight
if((hour==23 && minute > 32) || (hour == 0 && minute <=32)){ r8c1 = B00000001; r8c2 = B11111110; }
// now light up the LEDs
maxOne(1,1,r1c1);
maxOne(1,2,r2c1);
maxOne(1,3,r3c1);
maxOne(1,4,r4c1);
maxOne(1,5,r5c1);
maxOne(1,6,r6c1);
maxOne(1,7,r7c1);
maxOne(1,8,r8c1);
maxOne(2,1,r1c2);
maxOne(2,2,r2c2);
maxOne(2,3,r3c2);
maxOne(2,4,r4c2);
maxOne(2,5,r5c2);
maxOne(2,6,r6c2);
maxOne(2,7,r7c2);
maxOne(2,8,r8c2);
delay(2000);
}
Normally, when telling the time, anything before 30 minutes is described as "past the hour" and anything after is "to the following hour". But because we're allowing for "a few minutes past a specific time point" we had to allow for up to 32 minutes to be described as "past" (so 15:32 would be written as just gone half past three). Similarly, up to two minutes before the o'clock position would be described as "nearly x o'clock" so any checks against minutes are 0-32 and 33-58 (not against 30 and 59 as might be expected).
With the LEDs wired up and the code working, we fired up the laser cutter to create a fascia. We cut the protective film from one side of some clear perspex and sprayed it with black paint....
....then laser-etched the letters onto the paint. As this was to be the back of the display, the lettering had to be mirrored.
Amazingly, the clock booted up and worked first time.
So we added a test mode to increase the minutes every couple of seconds, so we can see it cycle through all available times in just a few minutes instead of having to stay awake (and gawp at the clock face without a break) for 24 hours or more!
Maybe if we build another one we might try smoked acrylic so the unlit letters are less prominent when they're not in use?
Recently we had to convert some old code over to Arduino, to use a MAX7219 LED driver. We'd had some trouble getting a 4-way 7-segment LED display to work properly with the MAX7219 chips - mostly because we were using common anode displays and the driver chips are best suited to common cathode (or was it the other way around...?). So we thought it'd be a good idea to strip the bigger project back to individual pieces; and for this task we focussed on just making the MAX7219 chips work.
A few months back we'd done some playing around with Arduino and MAX7219. This time we cascaded two chips and connected each MAX7219 to a single LED matrix. A bit of copy-n-paste coding later and we had a working example using the Arduino MAX Library
Having just moved a load of stuff into the new workshop bungalow, we had boxes and boxes of components hanging around - to hand were some RTC modules from a few years back; this gave us a perfect opportunity to put one to use
The RTC Library had us reading the current date/time off the TinyRTC module in just minutes!
#include <Wire.h>
#include "RTClib.h"
RTC_DS1307 RTC;
void setup () {
Serial.begin(9600);
Wire.begin();
RTC.begin();
if (! RTC.isrunning()) {
Serial.println("RTC is NOT running!");
// following line sets the RTC to the date & time this sketch was compiled
RTC.adjust(DateTime(__DATE__, __TIME__));
}
}
void loop () {
DateTime now = RTC.now();
Serial.print(now.year(), DEC);
Serial.print('/');
Serial.print(now.month(), DEC);
Serial.print('/');
Serial.print(now.day(), DEC);
Serial.print(' ');
Serial.print(now.hour(), DEC);
Serial.print(':');
Serial.print(now.minute(), DEC);
Serial.print(':');
Serial.print(now.second(), DEC);
Serial.println();
delay(1000);
}
#include "RTClib.h"
RTC_DS1307 RTC;
void setup () {
Serial.begin(9600);
Wire.begin();
RTC.begin();
if (! RTC.isrunning()) {
Serial.println("RTC is NOT running!");
// following line sets the RTC to the date & time this sketch was compiled
RTC.adjust(DateTime(__DATE__, __TIME__));
}
}
void loop () {
DateTime now = RTC.now();
Serial.print(now.year(), DEC);
Serial.print('/');
Serial.print(now.month(), DEC);
Serial.print('/');
Serial.print(now.day(), DEC);
Serial.print(' ');
Serial.print(now.hour(), DEC);
Serial.print(':');
Serial.print(now.minute(), DEC);
Serial.print(':');
Serial.print(now.second(), DEC);
Serial.println();
delay(1000);
}
After putting the two matrix boards side-by-side all we needed to do was read the current time over I2C and decide which LEDs needed to light up when. Here's the pattern of letters/words we came up with:
As we're effectively using a larger 16x8 matrix, we had plenty of extra letters after writing out the hours and minutes, so went to town with improving the precision of our clock.
Normally a word clock might say something like "it is ten past two" and display this until the time had changed to the next five-minute segment (in this case, "it is quarter past two"). We decided that we'd improve the precision by adding in "nearly" and "just gone" elements to each five-minute segment.
So, within two minutes of a time, we'd describe it as "nearly". For example at 14:04 we'd say "it is nearly five past two". Similarly, for up to two minutes after a time, we'd describe it is "just gone". So at 14:31 we'd say "it has just gone half past two".
This gives us the ability to tell the time to within two minutes, using language that many of us commonly use every day (although, as Nick pointed out, a margin of error of two minutes still leaves plenty time to miss your bus if you're not careful!)
Here's the code we came up with.
It's written to be readable/understandable rather than particularly "clever" (for example, we'd normally use an array for multi-line variables, like R[1][3] rather than multiple "single" variables, like R1C3 but as we've been getting a few questions of late on things we'd consider to be pretty simple, we tried to keep the code as understandable as possible!)
#include "Wire.h"
#define DS1307_I2C_ADDRESS 0x68
int dataIn = 2;
int load = 3;
int clk = 4;
int maxInUse = 2; //change this variable to set how many MAX7219's you'll use
// define max7219 registers
byte max7219_reg_noop = 0x00;
byte max7219_reg_digit0 = 0x01;
byte max7219_reg_digit1 = 0x02;
byte max7219_reg_digit2 = 0x03;
byte max7219_reg_digit3 = 0x04;
byte max7219_reg_digit4 = 0x05;
byte max7219_reg_digit5 = 0x06;
byte max7219_reg_digit6 = 0x07;
byte max7219_reg_digit7 = 0x08;
byte max7219_reg_decodeMode = 0x09;
byte max7219_reg_intensity = 0x0a;
byte max7219_reg_scanLimit = 0x0b;
byte max7219_reg_shutdown = 0x0c;
byte max7219_reg_displayTest = 0x0f;
int e = 0; // just a variable
// these are used to run in test mode
bool test_mode = false;
int curr_min = 0;
int curr_hr = 0;
// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val){
return ( (val/10*16) + (val%10) );
}
// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val){
return ( (val/16*10) + (val%16) );
}
void setDateDs1307(byte second, byte minute, byte hour, byte dayOfWeek, byte dayOfMonth, byte month, byte year){
Wire.beginTransmission(DS1307_I2C_ADDRESS);
Wire.write(0);
Wire.write(decToBcd(second)); // 0 to bit 7 starts the clock
Wire.write(decToBcd(minute));
Wire.write(decToBcd(hour)); // If you want 12 hour am/pm you need to set
// bit 6 (also need to change readDateDs1307)
Wire.write(decToBcd(dayOfWeek));
Wire.write(decToBcd(dayOfMonth));
Wire.write(decToBcd(month));
Wire.write(decToBcd(year));
Wire.endTransmission();
}
// Gets the date and time from the ds1307
void getDateDs1307(byte *second, byte *minute, byte *hour, byte *dayOfWeek, byte *dayOfMonth, byte *month, byte *year){
// Reset the register pointer
Wire.beginTransmission(DS1307_I2C_ADDRESS);
Wire.write(0);
Wire.endTransmission();
Wire.requestFrom(DS1307_I2C_ADDRESS, 7);
// A few of these need masks because certain bits are control bits
byte b;
b=Wire.read();
Serial.print(b,HEX);
*second = bcdToDec(b & 0x7f);
b=Wire.read();
Serial.print(b,HEX);
*minute = bcdToDec(b);
b=Wire.read();
Serial.print(b,HEX);
*hour = bcdToDec(b & 0x3f); // Need to change this if 12 hour am/pm
b=Wire.read();
Serial.print(b,HEX);
*dayOfWeek = bcdToDec(b);
b=Wire.read();
Serial.print(b,HEX);
*dayOfMonth = bcdToDec(b);
b=Wire.read();
Serial.print(b,HEX);
*month = bcdToDec(b);
b=Wire.read();
Serial.print(b,HEX);
*year = bcdToDec(b);
Serial.print(" ");
}
void putByte(byte data) {
byte i = 8;
byte mask;
while(i > 0) {
mask = 0x01 << (i - 1);
digitalWrite( clk, LOW);
if (data & mask){
digitalWrite(dataIn, HIGH);
}else{
digitalWrite(dataIn, LOW);
}
digitalWrite(clk, HIGH);
--i;
}
}
void maxAll (byte reg, byte col) { // initialize all MAX7219's in the system
int c = 0;
digitalWrite(load, LOW); // begin
for ( c =1; c<= maxInUse; c++) {
putByte(reg); // specify register
putByte(col);//((data & 0x01) * 256) + data >> 1); // put data
}
//digitalWrite(load, LOW);
digitalWrite(load,HIGH);
}
void maxOne(byte maxNr, byte reg, byte col) {
//maxOne is for addressing different MAX7219's,
//while having a couple of them cascaded
int c = 0;
digitalWrite(load, LOW); // begin
for ( c = maxInUse; c > maxNr; c--) {
putByte(0); // means no operation
putByte(0); // means no operation
}
putByte(reg); // specify register
putByte(col);//((data & 0x01) * 256) + data >> 1); // put data
for ( c =maxNr-1; c >= 1; c--) {
putByte(0); // means no operation
putByte(0); // means no operation
}
//digitalWrite(load, LOW); // and load da stuff
digitalWrite(load,HIGH);
}
void showClockOutput(byte hour, byte minute, byte second, byte dayOfMonth, byte month, byte year){
Serial.print(hour, DEC);
Serial.print(":");
Serial.print(minute, DEC);
Serial.print(":");
Serial.print(second, DEC);
Serial.print(" ");
Serial.print(dayOfMonth, DEC);
Serial.print("/");
Serial.print(month, DEC);
Serial.print("/");
Serial.print(year, DEC);
Serial.println();
}
void setup () {
byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
pinMode(dataIn, OUTPUT);
pinMode(clk, OUTPUT);
pinMode(load, OUTPUT);
//initiation of the max 7219
maxAll(max7219_reg_scanLimit, 0x07);
maxAll(max7219_reg_decodeMode, 0x00); // using an led matrix (not digits)
maxAll(max7219_reg_shutdown, 0x01); // not in shutdown mode
maxAll(max7219_reg_displayTest, 0x00); // no display test
for (e=1; e<=8; e++) { // empty registers, turn all LEDs off
maxAll(e,0);
}
maxAll(max7219_reg_intensity, 0x0f & 0x0f);
// initialise the RTC module
// (inc setting the time if necessary)
Wire.begin();
bool set_date=true;
if(set_date==false){
// change these values as necessary
second = 10;
minute = 46;
hour = 16;
dayOfWeek = 4;
dayOfMonth = 30;
month = 3;
year = 17;
setDateDs1307(second, minute, hour, dayOfWeek, dayOfMonth, month, year);
}
Serial.begin(9600);
Serial.println("Let's go");
}
void loop () {
byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
getDateDs1307(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year);
showClockOutput( hour, minute, second, dayOfMonth, month, year);
int r;
int r1c1, r1c2;
int r2c1, r2c2;
int r3c1, r3c2;
int r4c1, r4c2;
int r5c1, r5c2;
int r6c1, r6c2;
int r7c1, r7c2;
int r8c1, r8c2;
if(test_mode==true){
curr_min++;
if(curr_min > 59){ curr_hr++; curr_min=0;}
if(curr_hr > 23 ){ curr_hr=0; }
hour = curr_hr;
minute = curr_min;
}
// from the time, work out whether we start with its or it has
// (its nearly or it has just past)
r=0;
r1c1 = B11100000; r1c2 = 0x00;
r2c1 = 0x00; r2c2 = 0x00;
r3c1 = 0x00; r3c2 = 0x00;
r4c1 = 0x00; r4c2 = 0x00;
r5c1 = 0x00; r5c2 = 0x00;
r6c1 = 0x00; r6c2 = 0x00;
r7c1 = 0x00; r7c2 = 0x00;
r8c1 = 0x00; r8c2 = 0x00;
if(minute==1 || minute==2 || minute==6 || minute==7 || minute==11 || minute==12 || minute==16 || minute==17){ r=1; }
if(minute==3 || minute==4 || minute==8 || minute==9 || minute==13 || minute==14 || minute==18 || minute==19){ r=2; }
if(minute==21 || minute==22 || minute==26 || minute==27 || minute==31 || minute==32 || minute==36 || minute==37){ r=1; }
if(minute==23 || minute==24 || minute==28 || minute==29 || minute==33 || minute==34 || minute==38 || minute==39){ r=2; }
if(minute==41 || minute==42 || minute==46 || minute==47 || minute==51 || minute==52 || minute==56 || minute==57){ r=1; }
if(minute==43 || minute==44 || minute==48 || minute==49 || minute==53 || minute==54 || minute==58 || minute==59){ r=2; }
// just before or just after major clock minutes, light up the
// word "nearly" or "just past"
if(r==1){ r1c1 = B11011101; r1c2 = B11101111; }
if(r==2){ r1c1 = B11100000; r1c2 = B00000000; }
if(r==2){ r2c1 = B11111100; }
// maybe we could use the word "just gone" for one minute past and drop the "just"
// for two minutes past?
r=0;
if(minute==2 || minute==7 || minute==12 || minute==17){ r=1; }
if(minute==22 || minute==27 || minute==32 || minute==37){ r=1; }
if(minute==42 || minute==47 || minute==52 || minute==57){ r=1; }
if(r==1){
// mask out the word "just"
r1c1 = r1c1 & B11111110;
r1c2 = r1c2 & B00011111;
}
// at quarter past and quarter to the hour, light up the word quarter
if(minute >=13 && minute<=17){ r2c1+=1; r2c2 = B11111100; }
if(minute >=43 && minute<=47){ r2c1+=1; r2c2 = B11111100; }
// at twenty to and twenty past the hour, light up the word "twenty"
// (also at twenty-five to and twenty-five past)
if(minute >=18 && minute<=27){ r3c1 = B11111100; }
if(minute >=33 && minute<=42){ r3c1 = B11111100; }
// light up the word "five" at not only five to/past but also
// twenty-five to and twenty-five past
if(minute >=3 && minute<=7) { r3c1 +=1; r3c2 = B11100000;}
if(minute >=53 && minute<=57){ r3c1 +=1; r3c2 = B11100000;}
if(minute >=23 && minute<=27){ r3c1 +=1; r3c2 = B11100000;}
if(minute >=33 && minute<=37){ r3c1 +=1; r3c2 = B11100000;}
// at ten past/to the hour, light up the word ten
if(minute >=8 && minute <=12){ r3c2 = B00001110;}
if(minute >=48 && minute <=52){ r3c2 = B00001110;}
// at around half past...
if(minute >=28 && minute <=32){ r4c1 = B11110000;}
// display the either the word "past" or "two"
if(minute > 2 && minute <= 32){ r4c1 += B00000011; r4c2 = B11000000; }
if(minute > 32 && minute < 58) { r4c1 += B00001100; }
// this is for on the hour, o'clock
if(minute >=58 || minute <=2) { r8c1 = B11111110; }
// show the correct hour(s)
// (why compare to 32 and not 30 for half past the hour?
// because at 11:32 we want it still to read "it has just
// gone half past 11" - not half past twelve!
// just before/after one o'clock
if((hour==0 && minute > 32) || (hour == 1 && minute <=32)){ r7c2 = B00000111; }
if((hour==12 && minute > 32) || (hour == 13 && minute <=32)){ r7c2 = B00000111; }
// just before/after two o'clock
if((hour== 1 && minute > 32) || (hour == 2 && minute <=32)) { r5c1 = B11100000; }
if((hour==13 && minute > 32) || (hour == 14 && minute <=32)) { r5c1 = B11100000; }
// just before/after three o'clock
if((hour== 2 && minute > 32) || (hour == 3 && minute <=32)) { r4c2 += B00011111; }
if((hour==14 && minute > 32) || (hour == 15 && minute <=32)) { r4c2 += B00011111; }
// just before/after four o'clock
if((hour== 3 && minute > 32) || (hour == 4 && minute <=32)) { r5c1 = B00011110; }
if((hour==15 && minute > 32) || (hour == 16 && minute <=32)) { r5c1 = B00011110; }
// just before/after five o'clock
if((hour==4 && minute > 32) || (hour == 5 && minute <=32)){ r5c1 += 1; r5c2 = B11100000; }
if((hour==16 && minute > 32) || (hour == 17 && minute <=32)){ r5c1 += 1; r5c2 = B11100000; }
// just before/after six o'clock
if((hour==5 && minute > 32) || (hour == 6 && minute <=32)){ r6c1 = B11100000; }
if((hour==17 && minute > 32) || (hour == 18 && minute <=32)){ r6c1 = B11100000; }
// just before/after seven o'clock
if((hour==6 && minute > 32) || (hour == 7 && minute <=32)){ r5c2 = B00011111; }
if((hour==18 && minute > 32) || (hour == 19 && minute <=32)){ r5c2 = B00011111; }
// just before/after eight o'clock
if((hour==7 && minute > 32) || (hour == 8 && minute <=32)){ r6c1 = B00011111; }
if((hour==19 && minute > 32) || (hour == 20 && minute <=32)){ r6c1 = B00011111; }
// just before/after nine o'clock
if((hour==8 && minute > 32) || (hour == 9 && minute <=32)){ r6c2 = B11110000; }
if((hour==20 && minute > 32) || (hour == 21 && minute <=32)){ r6c2 = B11110000; }
// just before/after ten o'clock
if((hour==9 && minute > 32) || (hour == 10 && minute <=32)){ r6c2 = B00001110; }
if((hour==21 && minute > 32) || (hour == 22 && minute <=32)){ r6c2 = B00001110; }
// just before/after eleven o'clock
if((hour==10 && minute > 32) || (hour == 11 && minute <=32)){ r7c1 = B11111100; }
if((hour==22 && minute > 32) || (hour == 23 && minute <=32)){ r7c1 = B11111100; }
// this is just before/after twelve (noon)
if((hour==11 && minute > 32) || (hour == 12 && minute <=32)){ r7c1 = B00000011; r7c2 = B11110000; }
// this is just before/after midnight
if((hour==23 && minute > 32) || (hour == 0 && minute <=32)){ r8c1 = B00000001; r8c2 = B11111110; }
// now light up the LEDs
maxOne(1,1,r1c1);
maxOne(1,2,r2c1);
maxOne(1,3,r3c1);
maxOne(1,4,r4c1);
maxOne(1,5,r5c1);
maxOne(1,6,r6c1);
maxOne(1,7,r7c1);
maxOne(1,8,r8c1);
maxOne(2,1,r1c2);
maxOne(2,2,r2c2);
maxOne(2,3,r3c2);
maxOne(2,4,r4c2);
maxOne(2,5,r5c2);
maxOne(2,6,r6c2);
maxOne(2,7,r7c2);
maxOne(2,8,r8c2);
delay(2000);
}
Normally, when telling the time, anything before 30 minutes is described as "past the hour" and anything after is "to the following hour". But because we're allowing for "a few minutes past a specific time point" we had to allow for up to 32 minutes to be described as "past" (so 15:32 would be written as just gone half past three). Similarly, up to two minutes before the o'clock position would be described as "nearly x o'clock" so any checks against minutes are 0-32 and 33-58 (not against 30 and 59 as might be expected).
With the LEDs wired up and the code working, we fired up the laser cutter to create a fascia. We cut the protective film from one side of some clear perspex and sprayed it with black paint....
....then laser-etched the letters onto the paint. As this was to be the back of the display, the lettering had to be mirrored.
Amazingly, the clock booted up and worked first time.
So we added a test mode to increase the minutes every couple of seconds, so we can see it cycle through all available times in just a few minutes instead of having to stay awake (and gawp at the clock face without a break) for 24 hours or more!
Maybe if we build another one we might try smoked acrylic so the unlit letters are less prominent when they're not in use?
Monday, 27 March 2017
Recreating MAX7219 functionality with an Arduino
In recent weeks a few of our older projects have been resurrected and we've had a few emailed questions about them. Number one tends to be "you built this with a PIC can you send me the Arduino code?"
The short answer is "no". The longer answer is "well, maybe, one day, when one of us needs something similar for some project".
The latest project to garner interest is our "dartsboard scorer" using some massive 7-segment LEDs. It's been in a box for the last six months, but having recently got the workshop bungalow into some kind of useable state, I thought I might set up the dartboard and get it out again.
One thing that always bugged me (and everyone else who used it if I'm honest) is that the brightness of the LEDs fluctuates depending on the number of segments lit up. It's all because we couldn't use our MAX7219 chips as the supply voltage needs to be in the region of 9v-12v and the LEDs are common anode types (the max7219 chips work best with common cathode displays).
So I figured it's time we put the display right. And in doing so, maybe answering a couple of questions about the darts scorer - mostly "can you do it on an Arduino?" So here goes - we're going to be multiplexing the segments of the display(s) so that each segment draws the same amount of current and stays lit for the same duration; in theory that should make each segment appear with the same brightness.
As before, we're using a ULN2803A sink array. Each 7-segment display will get it's own (Arduino) controller chip (bare ATMega328 chips) and we'll tell it which number to display by sending data to it over a simple three-wire SPI/I2C connector.
We'll store the value we want to display in a variable. Then create a "pattern" to display on the 7-segment LED. So if we wanted to show the value 4:
We'd want to light up segments 2, 3, 6 and 7.
So our pattern (reading right-to-left) would be 01100110.
Similarly to display the number 7 we'd light up 1,2,3 and 6.
The pattern for number seven would be 00100111.
So in the main loop of our code, we'll look at bits 0-7 of our pattern variable and illuminate the appropriate LED segment (or not as necessary). All other segments will be turned off - so only one segment is lit at any one time and all active segments are illuminated for the same duration.
int k=2;
int last_k=2;
int mask_pattern = B00001101;
int value_to_display=0;
int byte_received=0;
int spi_cs=10;
int spi_data=11;
int spi_clk=12;
int last_cs=1;
int last_data=1;
int last_clk=1;
int byte_buffer=0;
int bits_received=0;
int a;
int b;
int c;
int d;
void setMaskPattern(int p){
switch(p){
case 0:
mask_pattern = B00111111; break;
case 1:
mask_pattern = B00000110; break;
case 2:
mask_pattern = B01011011; break;
case 3:
mask_pattern = B01001111; break;
case 4:
mask_pattern = B01100110; break;
case 5:
mask_pattern = B01101101; break;
case 6:
mask_pattern = B01111101; break;
case 7:
mask_pattern = B00100111; break;
case 8:
mask_pattern = B01111111; break;
case 9:
mask_pattern = B01101111; break;
case 99:
mask_pattern = 0x00; break;
}
}
void clearBuffer(){
byte_buffer=0;
bits_received=0;
}
void setup() {
for(int i=2; i<=8; i++){
pinMode(i,OUTPUT);
digitalWrite(i,LOW);
}
pinMode(spi_cs,INPUT_PULLUP);
pinMode(spi_data,INPUT_PULLUP);
pinMode(spi_clk,INPUT_PULLUP);
clearBuffer();
setMaskPattern(99);
}
void loop() {
// --------------------------------------------------
// multiplex the LEDs as fast as we can
// (up to 3ms is ok, 5ms creates a visible flicker)
// --------------------------------------------------
digitalWrite(last_k,LOW);
k=k+1;
if(k>8){ k=2;}
b=1 << (k-2);
if(mask_pattern & b){
digitalWrite(k,HIGH);
}
last_k=k;
// ---------------------------------------------
// if we've received any data on the SPI bus,
// update the number to display
// ---------------------------------------------
b=digitalRead(spi_cs);
if(b==LOW){
if(last_cs!=LOW){
// this is a falling edge - prepare the data values
clearBuffer();
}else{
// monitor the CLK line
a=digitalRead(spi_clk);
if(a==LOW){
if(last_clk!=LOW){
// this is a falling edge, get the data
c=digitalRead(spi_data);
if(c==HIGH){
d = 1;
d = d << bits_received;
byte_buffer = byte_buffer + d;
}
bits_received++;
}
}
last_clk=a;
}
}else{
if(last_cs==LOW){
// this is releasing the CS line, so put the value
// from the buffer onto the display
if(bits_received > 3){
value_to_display = byte_buffer;
if(value_to_display > 9){ value_to_display=99;}
setMaskPattern(value_to_display);
}
}
}
last_cs=b;
}
We've also got a bit of "pin polling" going on, looking for data coming in over I2C on pins 8,9 and 10. So when our CS line goes low, we reset everything, read some incoming data and when CS drifts high, choose a new pattern to make different segments of the LED display to light up. Of course this could (should?) be put onto an interrupt, but as the controller has nothing else to do, it won't hurt to poll the pins.
Which means we need a "controller" to send data to the display - the following code simply increments a counter from zero through to ten and displays the appropriate digit on the 7-segment LED.
int byte_to_send;
int spi_cs=10;
int spi_data=11;
int spi_clk=12;
int mask=0;
int k;
void sendByte(int byte_value){
// we assert the CS line (pull it low) to tell
// the target to start listening - but first need
// to make sure that the clock line is also idle
digitalWrite(spi_clk,HIGH);
digitalWrite(spi_cs,LOW);
// give it a moment
delay(1);
// now we set the data pin to indicate each bit
// in the value we want to send
for(int i=0; i<8; i++){
mask = 1;
mask = mask << i;
k = byte_value & mask;
if(k==0){
digitalWrite(spi_data,LOW);
}else{
digitalWrite(spi_data,HIGH);
}
// give it a moment
delay(1);
// drive the clock line low
digitalWrite(spi_clk,LOW);
// give it a moment
delay(2);
// return the clock line to idle
digitalWrite(spi_clk,HIGH);
}
// release the CS line (send it high)
digitalWrite(spi_cs,HIGH);
}
void setup() {
// put your setup code here, to run once:
pinMode(spi_cs,OUTPUT);
pinMode(spi_data,OUTPUT);
pinMode(spi_clk,OUTPUT);
// we're using pull-ups on the other end
// so everything should idle high
digitalWrite(spi_cs,HIGH);
digitalWrite(spi_data,HIGH);
digitalWrite(spi_clk,HIGH);
}
void loop() {
// put your main code here, to run repeatedly:
byte_to_send++;
if(byte_to_send > 10){ byte_to_send=0;}
sendByte(byte_to_send);
delay(2000);
}
The result looks something like this:
Now obviously this isn't the full code for our updated Darts Scorer, but should be enough to get your own project off the ground - swapping out a MAX7219 with an atmega328 and a ULN2803A transistor array (if you don't use the decimal point you can get away with a ULN2003).
The short answer is "no". The longer answer is "well, maybe, one day, when one of us needs something similar for some project".
The latest project to garner interest is our "dartsboard scorer" using some massive 7-segment LEDs. It's been in a box for the last six months, but having recently got the workshop bungalow into some kind of useable state, I thought I might set up the dartboard and get it out again.
One thing that always bugged me (and everyone else who used it if I'm honest) is that the brightness of the LEDs fluctuates depending on the number of segments lit up. It's all because we couldn't use our MAX7219 chips as the supply voltage needs to be in the region of 9v-12v and the LEDs are common anode types (the max7219 chips work best with common cathode displays).
So I figured it's time we put the display right. And in doing so, maybe answering a couple of questions about the darts scorer - mostly "can you do it on an Arduino?" So here goes - we're going to be multiplexing the segments of the display(s) so that each segment draws the same amount of current and stays lit for the same duration; in theory that should make each segment appear with the same brightness.
As before, we're using a ULN2803A sink array. Each 7-segment display will get it's own (Arduino) controller chip (bare ATMega328 chips) and we'll tell it which number to display by sending data to it over a simple three-wire SPI/I2C connector.
We'll store the value we want to display in a variable. Then create a "pattern" to display on the 7-segment LED. So if we wanted to show the value 4:
We'd want to light up segments 2, 3, 6 and 7.
So our pattern (reading right-to-left) would be 01100110.
Similarly to display the number 7 we'd light up 1,2,3 and 6.
The pattern for number seven would be 00100111.
So in the main loop of our code, we'll look at bits 0-7 of our pattern variable and illuminate the appropriate LED segment (or not as necessary). All other segments will be turned off - so only one segment is lit at any one time and all active segments are illuminated for the same duration.
int k=2;
int last_k=2;
int mask_pattern = B00001101;
int value_to_display=0;
int byte_received=0;
int spi_cs=10;
int spi_data=11;
int spi_clk=12;
int last_cs=1;
int last_data=1;
int last_clk=1;
int byte_buffer=0;
int bits_received=0;
int a;
int b;
int c;
int d;
void setMaskPattern(int p){
switch(p){
case 0:
mask_pattern = B00111111; break;
case 1:
mask_pattern = B00000110; break;
case 2:
mask_pattern = B01011011; break;
case 3:
mask_pattern = B01001111; break;
case 4:
mask_pattern = B01100110; break;
case 5:
mask_pattern = B01101101; break;
case 6:
mask_pattern = B01111101; break;
case 7:
mask_pattern = B00100111; break;
case 8:
mask_pattern = B01111111; break;
case 9:
mask_pattern = B01101111; break;
case 99:
mask_pattern = 0x00; break;
}
}
void clearBuffer(){
byte_buffer=0;
bits_received=0;
}
void setup() {
for(int i=2; i<=8; i++){
pinMode(i,OUTPUT);
digitalWrite(i,LOW);
}
pinMode(spi_cs,INPUT_PULLUP);
pinMode(spi_data,INPUT_PULLUP);
pinMode(spi_clk,INPUT_PULLUP);
clearBuffer();
setMaskPattern(99);
}
void loop() {
// --------------------------------------------------
// multiplex the LEDs as fast as we can
// (up to 3ms is ok, 5ms creates a visible flicker)
// --------------------------------------------------
digitalWrite(last_k,LOW);
k=k+1;
if(k>8){ k=2;}
b=1 << (k-2);
if(mask_pattern & b){
digitalWrite(k,HIGH);
}
last_k=k;
// ---------------------------------------------
// if we've received any data on the SPI bus,
// update the number to display
// ---------------------------------------------
b=digitalRead(spi_cs);
if(b==LOW){
if(last_cs!=LOW){
// this is a falling edge - prepare the data values
clearBuffer();
}else{
// monitor the CLK line
a=digitalRead(spi_clk);
if(a==LOW){
if(last_clk!=LOW){
// this is a falling edge, get the data
c=digitalRead(spi_data);
if(c==HIGH){
d = 1;
d = d << bits_received;
byte_buffer = byte_buffer + d;
}
bits_received++;
}
}
last_clk=a;
}
}else{
if(last_cs==LOW){
// this is releasing the CS line, so put the value
// from the buffer onto the display
if(bits_received > 3){
value_to_display = byte_buffer;
if(value_to_display > 9){ value_to_display=99;}
setMaskPattern(value_to_display);
}
}
}
last_cs=b;
}
We've also got a bit of "pin polling" going on, looking for data coming in over I2C on pins 8,9 and 10. So when our CS line goes low, we reset everything, read some incoming data and when CS drifts high, choose a new pattern to make different segments of the LED display to light up. Of course this could (should?) be put onto an interrupt, but as the controller has nothing else to do, it won't hurt to poll the pins.
7-seg-spi by chris_holden2495 on Scribd
Which means we need a "controller" to send data to the display - the following code simply increments a counter from zero through to ten and displays the appropriate digit on the 7-segment LED.
int byte_to_send;
int spi_cs=10;
int spi_data=11;
int spi_clk=12;
int mask=0;
int k;
void sendByte(int byte_value){
// we assert the CS line (pull it low) to tell
// the target to start listening - but first need
// to make sure that the clock line is also idle
digitalWrite(spi_clk,HIGH);
digitalWrite(spi_cs,LOW);
// give it a moment
delay(1);
// now we set the data pin to indicate each bit
// in the value we want to send
for(int i=0; i<8; i++){
mask = 1;
mask = mask << i;
k = byte_value & mask;
if(k==0){
digitalWrite(spi_data,LOW);
}else{
digitalWrite(spi_data,HIGH);
}
// give it a moment
delay(1);
// drive the clock line low
digitalWrite(spi_clk,LOW);
// give it a moment
delay(2);
// return the clock line to idle
digitalWrite(spi_clk,HIGH);
}
// release the CS line (send it high)
digitalWrite(spi_cs,HIGH);
}
void setup() {
// put your setup code here, to run once:
pinMode(spi_cs,OUTPUT);
pinMode(spi_data,OUTPUT);
pinMode(spi_clk,OUTPUT);
// we're using pull-ups on the other end
// so everything should idle high
digitalWrite(spi_cs,HIGH);
digitalWrite(spi_data,HIGH);
digitalWrite(spi_clk,HIGH);
}
void loop() {
// put your main code here, to run repeatedly:
byte_to_send++;
if(byte_to_send > 10){ byte_to_send=0;}
sendByte(byte_to_send);
delay(2000);
}
The result looks something like this:
Now obviously this isn't the full code for our updated Darts Scorer, but should be enough to get your own project off the ground - swapping out a MAX7219 with an atmega328 and a ULN2803A transistor array (if you don't use the decimal point you can get away with a ULN2003).
Wednesday, 22 March 2017
Bluetooth with Unity for iOS and Android
In recent weeks we've had quite a bit of activity on an old "how to do bluetooth" page on the blog. A lot of people have asked for a zip file or git link so they can just clone it into their projects.
Firstly, that's not quite how this blog works; stuff posted here is a learning aid - probably just an aide-memoire for the few contributors who actually make the stuff in the first place - rather than a repository of open source code. In fact, I get quite resentful of ignorant people who post (often abusive) comments demanding the entire source code project, instead of reading the post and learning how to do it for themselves. It's not a view shared by everyone at Nerd Towers, but for me, any comment beginning "TL;DR" is meaningless drivel.
With that rant out of the way, we recently had need to create a simple bluetooth-enabled app (for a Unity Meetup event next week, demonstrating how to connect Unity/software to hardware/real-world devices). So it seemed like a good time to review the bluetooth Unity library and hopefully clarify a few things that seem to have tripped a few readers up...
The idea here is to simplify using the bluetooth library as much as possible. As the "list all devices and pick one" part of the last post caused such trouble, we've cut it out completely and tried to keep everything together in one place/script. This means we're looking for a specific named device and we'll automatically connect to it, then send and receive data via a couple of text boxes.
Once you've got this working, you should be able to modify the script as necessary to pass values to/from your own functions to make it work in your own Unity project.
Right, first up, import the BTLE library from the Asset Store.
And create a panel in the Unity IDE
Create a new, blank game object then create a script btle_controller.cs. Drag and drop the script onto the blank game object
Create a text object in the panel (this will be our debug window)
Copy and paste this code into the script:
... and hook up all the controls to the public variables. Starting with the debug text object, drag and drop this into the public variable slot in the Unity IDE.
Now create a second panel (we made ours dark so you can see it clearly) and link this up to the script variable by dragging and dropping it in the Unity IDE.
Create an input object (which we'll type in to send data) and a text object, as children of the second panel (this panel gets disabled until the Bluetooth device has connected). Plop a button on there too while you're about it. As before, hook up all the on-screen game objects to the script by dragging and dropping.
We tested our project using Unity Remote on an LG G3 phone and a simple Arduino/BLE-CC41a combo (pushing data onto the serial port to send/receive over bluetooth).
Now fire up your app (we found it only worked on an actual device, running it in test-mode on the PC did nothing- even with bluetooth enabled on the laptop) and send sending/receiving data over bluetooth!
Note:
If you're getting nothing, and no errors during compile, it's quite likely that the UUID patterns are incorrect. Use some software such as BLEGattList (for Android) on your device to query not only which bluetooth devices are available, but also which UUIDs they are using.
We found that the bottom device, listed as "unknown service" beginning 0000ffe0 with a single read/write characteristic with the leading digits 0000ffe1 gave us the values to "plug in" to our code: the service was FFE0, the read characteristic FFE1 and the write characteristic was also FFE1.
This seems pretty standard across almost every one of these cheap bluetooth modules that mimic the BLE-CC41a devices. We did find an old RedBear bluetooth module which we got working, but the FullUID string had to be changed to something completely unrecognisable, and the read and write characteristics had different values (from memory something like 0002 for reading and 0001 for writing).
Anyway, there it is - very much a cut-down, quick and dirty way of getting your device to talk to bluetooth modules, all from a single script. Please don't ask for zip files or full source code - the BTLE plugin is available on the asset store, it costs about a tenner and is well worth spending a few quid to support a fellow programmer. With that installed you can copy and paste a single script, assign some variables and off you go!
Firstly, that's not quite how this blog works; stuff posted here is a learning aid - probably just an aide-memoire for the few contributors who actually make the stuff in the first place - rather than a repository of open source code. In fact, I get quite resentful of ignorant people who post (often abusive) comments demanding the entire source code project, instead of reading the post and learning how to do it for themselves. It's not a view shared by everyone at Nerd Towers, but for me, any comment beginning "TL;DR" is meaningless drivel.
With that rant out of the way, we recently had need to create a simple bluetooth-enabled app (for a Unity Meetup event next week, demonstrating how to connect Unity/software to hardware/real-world devices). So it seemed like a good time to review the bluetooth Unity library and hopefully clarify a few things that seem to have tripped a few readers up...
The idea here is to simplify using the bluetooth library as much as possible. As the "list all devices and pick one" part of the last post caused such trouble, we've cut it out completely and tried to keep everything together in one place/script. This means we're looking for a specific named device and we'll automatically connect to it, then send and receive data via a couple of text boxes.
Once you've got this working, you should be able to modify the script as necessary to pass values to/from your own functions to make it work in your own Unity project.
Right, first up, import the BTLE library from the Asset Store.
And create a panel in the Unity IDE
Create a new, blank game object then create a script btle_controller.cs. Drag and drop the script onto the blank game object
Create a text object in the panel (this will be our debug window)
Copy and paste this code into the script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Text;
public class btle_controller : MonoBehaviour {
// -----------------------------------------------------------------
// change these to match the bluetooth device you're connecting to:
// -----------------------------------------------------------------
// private string _FullUID = "713d****-503e-4c75-ba94-3148f18d941e"; // redbear module pattern
private string _FullUID = "0000****-0000-1000-8000-00805f9b34fb"; // BLE-CC41a module pattern
private string _serviceUUID = "ffe0";
private string _readCharacteristicUUID = "ffe1";
private string _writeCharacteristicUUID = "ffe1";
private string deviceToConnectTo = "ChrisBLE";
public bool isConnected=false;
private bool _readFound=false;
private bool _writeFound=false;
private string _connectedID = null;
private Dictionary<string, string> _peripheralList;
private float _subscribingTimeout = 0f;
public Text txtDebug;
public GameObject uiPanel;
public Text txtSend;
public Text txtReceive;
public Button btnSend;
// Use this for initialization
void Start () {
btnSend.onClick.AddListener (sendData);
uiPanel.SetActive (false);
txtDebug.text+="\nInitialising bluetooth \n";
BluetoothLEHardwareInterface.Initialize (true, false, () => {},
(error) => {}
);
Invoke ("scan", 1f);
}
// Update is called once per frame
void Update () {
if (_readFound && _writeFound) {
_readFound = false;
_writeFound = false;
_subscribingTimeout = 1.0f;
}
if (_subscribingTimeout > 0f) {
_subscribingTimeout -= Time.deltaTime;
if (_subscribingTimeout <= 0f) {
_subscribingTimeout = 0f;
BluetoothLEHardwareInterface.SubscribeCharacteristicWithDeviceAddress (
_connectedID, FullUUID (_serviceUUID), FullUUID (_readCharacteristicUUID),
(deviceAddress, notification) => {
},
(deviceAddress2, characteristic, data) => {
BluetoothLEHardwareInterface.Log ("id: " + _connectedID);
if (deviceAddress2.CompareTo (_connectedID) == 0) {
BluetoothLEHardwareInterface.Log (string.Format ("data length: {0}", data.Length));
if (data.Length == 0) {
// do nothing
} else {
string s = ASCIIEncoding.UTF8.GetString (data);
BluetoothLEHardwareInterface.Log ("data: " + s);
receiveText (s);
}
}
});
}
}
}
void receiveText(string s){
txtDebug.text += "Received: " + s + " \n";
txtReceive.text = s;
}
void sendDataBluetooth(string sData){
if (sData.Length > 0) {
byte[] bytes = ASCIIEncoding.UTF8.GetBytes (sData);
if (bytes.Length > 0) {
sendBytesBluetooth (bytes);
}
}
}
void sendBytesBluetooth(byte[] data){
BluetoothLEHardwareInterface.Log (string.Format ("data length: {0} uuid {1}", data.Length.ToString (), FullUUID (_writeCharacteristicUUID)));
BluetoothLEHardwareInterface.WriteCharacteristic (_connectedID, FullUUID(_serviceUUID), FullUUID(_writeCharacteristicUUID),
data, data.Length, true, (characteristicUUID)=> {
BluetoothLEHardwareInterface.Log("Write succeeded");
}
);
}
void scan(){
// the first callback will only get called the first time this device is seen
// this is because it gets added to a list in the BluetoothDeviceScript
// after that only the second callback will get called and only if there is
// advertising data available
txtDebug.text+=("Starting scan \r\n");
BluetoothLEHardwareInterface.ScanForPeripheralsWithServices (null, (address, name) => {
AddPeripheral (name, address);
}, (address, name, rssi, advertisingInfo) => {});
}
void AddPeripheral (string name, string address){
txtDebug.text+=("Found "+name+" \r\n");
if (_peripheralList == null) {
_peripheralList = new Dictionary<string, string> ();
}
if (!_peripheralList.ContainsKey (address)) {
_peripheralList [address] = name;
if (name.Trim().ToLower() == deviceToConnectTo.Trim().ToLower()) {
//txtDebug.text += "Found our device, stop scanning \n";
//BluetoothLEHardwareInterface.StopScan ();
txtDebug.text += "Connecting to " + address + "\n";
connectBluetooth (address);
} else {
txtDebug.text += "Not what we're looking for \n";
}
} else {
txtDebug.text += "No address found \n";
}
}
void connectBluetooth(string addr){
BluetoothLEHardwareInterface.ConnectToPeripheral (addr, (address) => {
},
(address, serviceUUID) => {
},
(address, serviceUUID, characteristicUUID) => {
// discovered characteristic
if (IsEqual (serviceUUID, _serviceUUID)) {
_connectedID = address;
isConnected = true;
if (IsEqual (characteristicUUID, _readCharacteristicUUID)) {
_readFound = true;
}
if (IsEqual (characteristicUUID, _writeCharacteristicUUID)) {
_writeFound = true;
}
txtDebug.text += "Connected";
BluetoothLEHardwareInterface.StopScan ();
uiPanel.SetActive (true);
}
}, (address) => {
// this will get called when the device disconnects
// be aware that this will also get called when the disconnect
// is called above. both methods get call for the same action
// this is for backwards compatibility
isConnected = false;
});
}
void sendData(){
string s = txtSend.text.ToString ();
sendDataBluetooth (s);
}
// -------------------------------------------------------
// some helper functions for handling connection strings
// -------------------------------------------------------
string FullUUID (string uuid) {
return _FullUID.Replace ("****", uuid);
}
bool IsEqual(string uuid1, string uuid2){
if (uuid1.Length == 4) {
uuid1 = FullUUID (uuid1);
}
if (uuid2.Length == 4) {
uuid2 = FullUUID (uuid2);
}
return (uuid1.ToUpper().CompareTo(uuid2.ToUpper()) == 0);
}
}
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Text;
public class btle_controller : MonoBehaviour {
// -----------------------------------------------------------------
// change these to match the bluetooth device you're connecting to:
// -----------------------------------------------------------------
// private string _FullUID = "713d****-503e-4c75-ba94-3148f18d941e"; // redbear module pattern
private string _FullUID = "0000****-0000-1000-8000-00805f9b34fb"; // BLE-CC41a module pattern
private string _serviceUUID = "ffe0";
private string _readCharacteristicUUID = "ffe1";
private string _writeCharacteristicUUID = "ffe1";
private string deviceToConnectTo = "ChrisBLE";
public bool isConnected=false;
private bool _readFound=false;
private bool _writeFound=false;
private string _connectedID = null;
private Dictionary<string, string> _peripheralList;
private float _subscribingTimeout = 0f;
public Text txtDebug;
public GameObject uiPanel;
public Text txtSend;
public Text txtReceive;
public Button btnSend;
// Use this for initialization
void Start () {
btnSend.onClick.AddListener (sendData);
uiPanel.SetActive (false);
txtDebug.text+="\nInitialising bluetooth \n";
BluetoothLEHardwareInterface.Initialize (true, false, () => {},
(error) => {}
);
Invoke ("scan", 1f);
}
// Update is called once per frame
void Update () {
if (_readFound && _writeFound) {
_readFound = false;
_writeFound = false;
_subscribingTimeout = 1.0f;
}
if (_subscribingTimeout > 0f) {
_subscribingTimeout -= Time.deltaTime;
if (_subscribingTimeout <= 0f) {
_subscribingTimeout = 0f;
BluetoothLEHardwareInterface.SubscribeCharacteristicWithDeviceAddress (
_connectedID, FullUUID (_serviceUUID), FullUUID (_readCharacteristicUUID),
(deviceAddress, notification) => {
},
(deviceAddress2, characteristic, data) => {
BluetoothLEHardwareInterface.Log ("id: " + _connectedID);
if (deviceAddress2.CompareTo (_connectedID) == 0) {
BluetoothLEHardwareInterface.Log (string.Format ("data length: {0}", data.Length));
if (data.Length == 0) {
// do nothing
} else {
string s = ASCIIEncoding.UTF8.GetString (data);
BluetoothLEHardwareInterface.Log ("data: " + s);
receiveText (s);
}
}
});
}
}
}
void receiveText(string s){
txtDebug.text += "Received: " + s + " \n";
txtReceive.text = s;
}
void sendDataBluetooth(string sData){
if (sData.Length > 0) {
byte[] bytes = ASCIIEncoding.UTF8.GetBytes (sData);
if (bytes.Length > 0) {
sendBytesBluetooth (bytes);
}
}
}
void sendBytesBluetooth(byte[] data){
BluetoothLEHardwareInterface.Log (string.Format ("data length: {0} uuid {1}", data.Length.ToString (), FullUUID (_writeCharacteristicUUID)));
BluetoothLEHardwareInterface.WriteCharacteristic (_connectedID, FullUUID(_serviceUUID), FullUUID(_writeCharacteristicUUID),
data, data.Length, true, (characteristicUUID)=> {
BluetoothLEHardwareInterface.Log("Write succeeded");
}
);
}
void scan(){
// the first callback will only get called the first time this device is seen
// this is because it gets added to a list in the BluetoothDeviceScript
// after that only the second callback will get called and only if there is
// advertising data available
txtDebug.text+=("Starting scan \r\n");
BluetoothLEHardwareInterface.ScanForPeripheralsWithServices (null, (address, name) => {
AddPeripheral (name, address);
}, (address, name, rssi, advertisingInfo) => {});
}
void AddPeripheral (string name, string address){
txtDebug.text+=("Found "+name+" \r\n");
if (_peripheralList == null) {
_peripheralList = new Dictionary<string, string> ();
}
if (!_peripheralList.ContainsKey (address)) {
_peripheralList [address] = name;
if (name.Trim().ToLower() == deviceToConnectTo.Trim().ToLower()) {
//txtDebug.text += "Found our device, stop scanning \n";
//BluetoothLEHardwareInterface.StopScan ();
txtDebug.text += "Connecting to " + address + "\n";
connectBluetooth (address);
} else {
txtDebug.text += "Not what we're looking for \n";
}
} else {
txtDebug.text += "No address found \n";
}
}
void connectBluetooth(string addr){
BluetoothLEHardwareInterface.ConnectToPeripheral (addr, (address) => {
},
(address, serviceUUID) => {
},
(address, serviceUUID, characteristicUUID) => {
// discovered characteristic
if (IsEqual (serviceUUID, _serviceUUID)) {
_connectedID = address;
isConnected = true;
if (IsEqual (characteristicUUID, _readCharacteristicUUID)) {
_readFound = true;
}
if (IsEqual (characteristicUUID, _writeCharacteristicUUID)) {
_writeFound = true;
}
txtDebug.text += "Connected";
BluetoothLEHardwareInterface.StopScan ();
uiPanel.SetActive (true);
}
}, (address) => {
// this will get called when the device disconnects
// be aware that this will also get called when the disconnect
// is called above. both methods get call for the same action
// this is for backwards compatibility
isConnected = false;
});
}
void sendData(){
string s = txtSend.text.ToString ();
sendDataBluetooth (s);
}
// -------------------------------------------------------
// some helper functions for handling connection strings
// -------------------------------------------------------
string FullUUID (string uuid) {
return _FullUID.Replace ("****", uuid);
}
bool IsEqual(string uuid1, string uuid2){
if (uuid1.Length == 4) {
uuid1 = FullUUID (uuid1);
}
if (uuid2.Length == 4) {
uuid2 = FullUUID (uuid2);
}
return (uuid1.ToUpper().CompareTo(uuid2.ToUpper()) == 0);
}
}
... and hook up all the controls to the public variables. Starting with the debug text object, drag and drop this into the public variable slot in the Unity IDE.
Now create a second panel (we made ours dark so you can see it clearly) and link this up to the script variable by dragging and dropping it in the Unity IDE.
Create an input object (which we'll type in to send data) and a text object, as children of the second panel (this panel gets disabled until the Bluetooth device has connected). Plop a button on there too while you're about it. As before, hook up all the on-screen game objects to the script by dragging and dropping.
We tested our project using Unity Remote on an LG G3 phone and a simple Arduino/BLE-CC41a combo (pushing data onto the serial port to send/receive over bluetooth).
IMPORTANT:
If you're running the BTLE4 library on a device running Android 6.0 or later, you'll need to add an extra line to the AndroidManifest.xml file
If you're running the BTLE4 library on a device running Android 6.0 or later, you'll need to add an extra line to the AndroidManifest.xml file
<uses-permission: android:name="android.permission.ACCESS_COARSE_LOCATION" />
Now fire up your app (we found it only worked on an actual device, running it in test-mode on the PC did nothing- even with bluetooth enabled on the laptop) and send sending/receiving data over bluetooth!
Note:
If you're getting nothing, and no errors during compile, it's quite likely that the UUID patterns are incorrect. Use some software such as BLEGattList (for Android) on your device to query not only which bluetooth devices are available, but also which UUIDs they are using.
We found that the bottom device, listed as "unknown service" beginning 0000ffe0 with a single read/write characteristic with the leading digits 0000ffe1 gave us the values to "plug in" to our code: the service was FFE0, the read characteristic FFE1 and the write characteristic was also FFE1.
This seems pretty standard across almost every one of these cheap bluetooth modules that mimic the BLE-CC41a devices. We did find an old RedBear bluetooth module which we got working, but the FullUID string had to be changed to something completely unrecognisable, and the read and write characteristics had different values (from memory something like 0002 for reading and 0001 for writing).
Anyway, there it is - very much a cut-down, quick and dirty way of getting your device to talk to bluetooth modules, all from a single script. Please don't ask for zip files or full source code - the BTLE plugin is available on the asset store, it costs about a tenner and is well worth spending a few quid to support a fellow programmer. With that installed you can copy and paste a single script, assign some variables and off you go!
Subscribe to:
Posts (Atom)





























