Introduction
This is my experiment of using Azure Sphere kit for the Sensing the World Phase 2.
Hardware
To make it simple, i am choose two snap type sensor card so we don't need working on solder, however we need solder a connector to Amphenol PM and use jumper wire to the board second Mikrobus slot.
1. Azure Sphere MT3620 Starter Free Kit
Which is a control centre for everything, the onboard RGB LED served as a air quality indicator, two button used for control FAN speed and switch between manual/auto mode (will be described below), it also served as a basic web http server for user web access, and wifi connect to outside.
2. Speed Bosch BME680 Environment Sensor Grove board
Which include precision temperature, humidity, barometer and gas resistance (for Air quality) sensor, the Grove interface make connection can not be more easy, however the instruction from Seeed is based on Arduino, we need some afford for working in the software.
3. Amphenol SM-UART-04L PM2.5 sensor
Connected directly to MT3620, it use UART to send data to host, because it will continue send reading to host, in most case only one data wire plus 5V and ground is need. A small fan movement the air and internal laser dust sensor will count the number of small particulars as three size PM1 PM2.5 and PM10. Because it is very sensitive of direct dirty, i packed in a recycled transparent blue plastic box for protection.
4. Mikroe FAN 4 Click with small 5V fan and air filter cloth
Mikroe FAN 4 Click use LTC1695 for speed control fan by voltage, it may not precision as PWM control but enough for this purpose, the fan is normal brushless two wire without speed detect, it may not big enough for real use, but it enough to demonstration purpose.
Let's Begin
Microsoft Azure Sphere is new platform running at specifically ARM security OS based Linux.
The BME680 software which include two parts, an open sourcing driver for basic sensor setting and reading, and the BSEC, a closed sources library for Advancing control the sensor parameter, timing, get reading from sensor, produce the Indoor air quality (IAQ) score (1-500), we will describe it more detail in later.
The start point is ADC demo project provided by Avnet: https://github.com/CloudConnectKits/Azure_Sphere_SK_ADC_RTApp
We need work for two Components for this project, one for main App, one for sub-real core for BSEC.
C:\Users\IEUser\Documents>azsphere dev sideload show-status ec6232b9-26d5-a49f-9980-0bec91dd3385: App state: running 66961e20-9574-4d1d-a766-8ac740ea7a0a: App state: running
- Main Core (Cortex A7) - build a connection driver for data transfer between BME680 open source driver and I2C interface, communicated intercore to BSEC to get IQC result. Build driver for PM2.5 sensor, and build driver for Fan IC (LTC1695) and a basic web-interface.
- First M4F Core - Bare Metal program for BSEC
The BSEC libraries which is integrated at M4F core because the libraries (libalgobsec.a) supplied by Bosch
https://www.bosch-sensortec.com/bst/products/all_products/bsecis
only support A7 without NEON, as main core component need complied the software with NEON compatibility, the linker is not able to link up the libraries, we try to work both M4 and M4F (with hardware FPU) GCC complied libraries running on M4F core, success but only M4 version (without FPU) work correctly.
PM2.5 sensor Driver
Prepare: Connect the sensor TX to mikrobus 2 TX pin, ground to Ground pin and 5V to 5V pin.
1. under main project (AzSphereWheatherStation) app_manifest.json added UART under "Capabilities"
//// Uart for PM 2.5 sensor "Uart": [ "ISU0" ], "I2cMaster": [ "ISU2" ],
2. Under main.c InitPeripheralsAndHandlers added UART config, the PM2.5 sensor use 9600 baudrate and 8N1.
UART_Config uartConfig; UART_InitConfig(&uartConfig); uartConfig.baudRate = 9600; uartConfig.flowControl = UART_FlowControl_None; uartFd = UART_Open(AVT_SK_CM1_ISU0_UART, &uartConfig);
3. registed to a handler which received UART data
if (RegisterEventHandlerToEpoll(epollFd, uartFd, &uartEventData, EPOLLIN) != 0) { return -1; }
4. UartEventHandler received the data and transfer to smuart04l_update under smuart04l.c for decode data.
if (bytesRead > 0) { // Null terminate the buffer to make it a valid string, and print it receiveBuffer[bytesRead] = 0; if (smuart04l_update(receiveBuffer, bytesRead) != SMUART04L_STATUS_UPDATE) return; }
5. Datasheet:
https://www.amphenol-sensors.com/en/telaire/dust-sensors/3393-sm-uart-04l page.4 is the protocol of sensor output, which is very similar other brand of PM sensor. smuart04l.c worked for decoding.
- Monitoring the message and check the header 0x42 0x4D is observed.
- Check the Length value is some as datasheet said 2*13+2.
- Decode the PM values to the temporary buffer.
- Check the CRC code, which is all sum value from header to error code.
If all correctly, flip the temp_buffer to data for available program access.
uint8_t smuart04l_update(uint8_t* receiveBuffer, size_t length) { ..... for (int i = 0;i < length;i++) { switch (read_pos) { case 0: data_high = receiveBuffer[i]; if (data_high != 0x42) { continue; ....... temp_buffer.id = currentid++; data = temp_buffer; .....
Intercore message facilities
Between real and high level core, we need socket for share memory to communicated between them, the real_core.c support that.
uint8_t real_core_init(int *epollFd, sig_atomic_t termination) { sockFd = Application_Socket(ledrtAppComponentId);
First, create the Application_Socket and setup the registers for listening real-core income. In our app, the intercore data first uint8_t is "type", which defined at real_core.h, used for identify the type of data, for example LOG mean the log message send from real-core, ADC_LIGHT mean this is light sensor value...
uint8_t send_message_to_rtcore(uint8_t type, const uint8_t* message, uint16_t len);
send_message_to_rtcore used for send data to real-core as sender need provide the data type.
In real-core main.c the main loop dequeue the message pool and identify the data type and process it.
for (;;) { .... int8_t result = dequeue(&type, &data, &datasize); if (result || !datasize) continue; if (type == BME680_RECV) {
BME680 and BSEC
As mention before libalgobsec.a for BSEC unable working at main core but this can be work at M4 real core, so download the BSEC software, uncompress and copy the .a and .h files (algo>normal_version>bin>gcc>Cortex_M4>) to directory of real-core project,
Modify CMakeLists.txt TARGET_LINK_LIBRARIES to
TARGET_LINK_LIBRARIES(${PROJECT_NAME} -lm ${CMAKE_SOURCE_DIR}/libalgobsec.a)
-lm is C math library, we need this make BSEC work.
The real-core c math library missing xxxf float function, so we need to redirect xxxf to use math double functions.
... float cosf(float x) { return cos(x); } float sinf(float x) { return sin(x); } ...
Next, we need initialise the library
bsec_library_return_t ret = bsec_init();
and config it, because our board is 3.3v and we make it run every 3s LP mode, so copy .h and .c from BSEC software pack (config>generic_33v_3s_4d) to real-core project directory.
ret = bsec_set_configuration(bsec_config_iaq, BSEC_MAX_PROPERTY_BLOB_SIZE, work_buffer,
Third, we need build subscription function, this telling BSEC which type sensor data we requested, for this project we request 10 sensor values.
static bsec_library_return_t bme680_bsec_update_subscription(float sample_rate)
{ bsec_sensor_configuration_t requested_virtual_sensors[NUM_USED_OUTPUTS]; ... requested_virtual_sensors[0].sensor_id = BSEC_OUTPUT_IAQ; requested_virtual_sensors[0].sample_rate = sample_rate; requested_virtual_sensors[1].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED _TEMPERATURE; requested_virtual_sensors[1].sample_rate = sample_rate; ... requested_virtual_sensors[9].sensor_id = BSEC_OUTPUT_BREATH_VOC_EQUIVALENT; ...
The expression is sampling frequency, which is 0.33333 for update every 3s
ret = bme680_bsec_update_subscription(BSEC_SAMPLE_RATE_LP);
Now finished the init, next step is forever loop.
In order to feed BSEC, we gain the raw sensor data from Main core, turn back to main core bme680_sensor.c working for that.
uint8_t bme680_sensor_init()
This function setup of relative config struct, call bme680 open-source driver and initialise the sensor. Please don't conflict, the real-core work for initialise the BSEC software, and here we work for the hardware.
The Bosch driver need provide three custom class, read, write and delay_ms which is define how read, write the I2C bus, and the delay timer which is specially for each platform.
Next we do setting for the first measurement, which include the gas sensor heater and heat duration, the oversampling setting, turn on the GAS sensor and trigger the measurement.
After that, we retrieve the library state. We get the library state every hour, send to main core and save to permanent storage, so after loss power, we can retrieve the state, the library require that to restore the parameter as the history calibration as the BME680 need few days data for make it accuracy.
Now, we are finished all initialise, call
bme680_sensor_force();
for start sensor measurement and this is the start function of the loop.
Inside the function, we config the dev parameter by reading bsec_bme_settings_t struct, which is normally feedback from BSEC, the BSEC library require us adjust the device parameters sensor every measurement, this is because the gas sensor is mixture type, a batch of chemical type can be sense by one detector with one resistance output, each type of chemical have different sensitive under different heating temperature, therefore the BSEC config the heating temperature and duration of heater every single measurement to gain the data in multi aspect, thus the Bosch secret equation can calculate the result more precistion.
After that, we set the power_mode to FORCED
bme680dev.power_mode = BME680_FORCED_MODE;
set the settings
int8_t result = bme680_set_sensor_settings(set_required_settings, &bme680dev);
get the measurement waiting time
bme680_get_profile_dur(&meas_period, &bme680dev);
force the sensor mode, the measure start now
result = bme680_set_sensor_mode(&bme680dev);
waiting the measurement waiting time
bme680_sensor_delay_ms(meas_period);
and we go to next step: bme680_sensor_get.
The bme680_sensor_get function, worked for get the sensor raw data
result = bme680_get_sensor_data(&bme680_sensor_data, &bme680dev);
serialising it and send to Real-core BSEC software via intercore messenger,
send_message_to_rtcore(BME680_SEND, send, len);
Now, we go back to real core, the real core received the message and transfer the data to bsec_input_t struct for BSEC library, and finally we are really to feed the physical sensor data to BSEC.
if (type == BME680_RECV) { .... bsec_input_t inputs[5]; bsec_output_t outputs[BSEC_NUMBER_OUTPUTS]; uint8_t num_bsec_inputs = 0; .... uint8_t num_bsec_outputs = BSEC_NUMBER_OUTPUTS; ret = bsec_do_steps(inputs, num_bsec_inputs, outputs, &num_bsec_outputs);
We call bsec_do_steps, the Main signal processing function for BSEC, after that we get the output, which is value of virtual sensor.
next we prepare next measurement by call
ret = bsec_sensor_control(timestamp, &sensor_settings);
timestamp is last sample starting time, which get from main core data.
At the end, we serialising the BSEC output and the next measurement setting, send it by type BME680_SEND to main core.
enqueue(BME680_SEND, sendback, 21 + NUM_USED_OUTPUTS * 5);
We come back to main Core, the main core received the message and call bme680_sensor_bsec_feedback
The function reformatted the sequence data to bme680_virtual_sensor_data array.
Now , we got the useful data such as the IAQ, the eCO2 and eVOC level, it also provide the sensor accuracy level.
We need to prepare next sampling correctly, which is important for overall accuracy.
In additional to result data , we also received the "setting", very simple, just replace our original setting to new one.
memcpy(&setting, data + virtual_sensor_size * 5, 21);
and then we build up new epoll timer call of bme680_sensor_force for start another loop in right time.
bme680PollTimerFd = CreateTimerFdAndAddToEpoll(epollFd, &bme680period, &bme680EventData, EPOLLIN);
Be notice, we set the epoll timer call of bme680_sensor_force a little early and use nanosleep wait finally to improve the precision of timing.
LTC1695
LTC1695 is a fan controller use voltage to control the fan speed, it only require two wire fan, but is not as precision as PWM, but this enough for this application. we connect a small 5V fan powered by MT3620 power bus.
The LTC1695 use I2C for voltage setting, we need to build the driver, very simple, we only need write the data to the I2C address for setting voltage, and read the I2C address for check the error. ltc1695.c working for that.
uint8_t ltc1695_set(uint16_t mV, _Bool start_boost) { if (mV > LTC_1695_MAX_VOLTAGE || mV < LTC_1695_MIN_VOLTAGE) return LTC1695_ERR_VOLTAGE_OUTOFRANGE; uint8_t command = 0x40 * start_boost; command += mV / 78; ltc1695_write(&command); ltc1695_value = mV; return command; }
we transfer the value mV (1/1000V) to i2c command by divided 78, which is the first 6 bits of command, the 7 bit for start up boost, which provide full voltage 250ms when start.
Although the IC no voltage value feedback, ltc1695.c work for tracking the value.
This also provide AUTO mode for setting the fan speed based on PM and IQA class values
uint8_t ltc1695_auto(uint8_t pm, uint8_t iaq) { const speed[7] = { 0,3000,4200,4500,4922,4922,4922 };
above array is the corresponding voltage of the max value for IAQ and PM, for example IAQ is 2 and PM is 3, the voltage is 4500mv.
The board A B button used for choose of AUTO/Manual speed, push B switch to manual mode and increase the fan speed, push A reduce fan speed, stop and switch to AUTO mode, the APP LED indicate the mode of FAN, blinking mean manual set, and long lighting is AUTO mode.
LED
This project, without external display, the only output is the inboard RGB LED which indicate the PM class when blinking, long for IAQ, and both flip every three seconds.
When i started this project, no software level support of hardware PWM, so we implement a softPWM at the second real core, after that, the new beta 3+1909 support hardware PWM, i rewrite this part to use hardware PWM in Main Core (A7), as i prevent the second core SoftPWM code for reference.
We use PWM for dimming, instead controlled by voltage, the PWM use switch on/off duty cycle which is the scale of on/off time, for example 100ns switch on 200ns switch off mean only 1/3 full brightness. When this switch on/off very quick (high frequency) user percept this is sustain.
The MT3620 with three PWM controller and each controller control 4 output, the LED APP (PWM4) and LED WIFI(PWM5) under controller 1 and RGB LED(PWM8-10) under controller 2, modify app_manifest.json "Capabilities"
"Pwm": [ "PWM-CONTROLLER-1", "PWM-CONTROLLER-2" ],
under led.c we need to enable of two LED controller
uint8_t led_init() {
pwmFd_1 = PWM_Open(PWMCONTROLLER_1);
...
pwmFd_2 = PWM_Open(PWMCONTROLLER_2);
each controller with 4 control channel, led_apply_force transfer Luminosity value to PWM state, which is value * full_cycle / 255.
value *= fullcycleNs; value /= 255; value *= LEDMAX; value /= 255;
The full cycle ns which is one full cycle time should need careful determine, as too long will introduced flicking, too short will not precision enough for dimming, we choose 131071ns, which around 7000 hz.
Use PWM_Apply to force hardware
int result = PWM_Apply(controller, channel, &ledPwmState);
Next, we build a timer every 0.4s for blinking effect, a uint32_t used for define the blinking pattern,
total ON is 0xFFFFFFFF (11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 )
total OFF is 0x0 (000000 000000 000000 000000 000000 000000 000000 000000 )
after 32 cycles will comeback to begining of pattern, if we want bright at first cycle and 3 cycle and 13 cycle our pattern is:
0xA0080000 (10100000 00001000 000000 000000 000000 000000 000000 000000).
So, the RGB can be indicated the IAQ from BME680 when long light, and show PM level when blinking, and the display mode change about every 3s.
Storage
The BSEC require save of the state and retrieve it after loss of power for sensor continue long-team accuracy adjustment, every hour the M4 core will send latest BSEC state data to Main core (A7), the Main core save the state data to mutable storage, after lose power, the Main core load data from mutable storage and send to M4 core for initiation.
We also save the fan config value on mutable storage, so after loose power the fan setting is remain.
Because the mutable storage is a serial block, we apply a basic header and checksum to support multiple data read and write, file_retrieve_mutable and file_set_mutable read and write based on the offset of block, for detail , reference of file.c.
Web Interface
In order to support user direct view of sensor information via wifi, we implement a basic web server inside MT3620.
Modify the app_manifest.json "Capabilities"
"AllowedConnections": [ "192.168.1.101","iotc-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.azure-devices.net" ], "AllowedTcpServerPorts": [8080],
added 192.168.1.101 under AllowedConnections for client IP (PC side) access right, and added AllowedTcpServerPorts for 8080 so the Az Sphere OS allow TCP income.
The web_tcp_server.c support the web inteface, which is reference by Azure PrivateNetworkServices
https://github.com/Azure/azure-sphere-samples/tree/master/Samples/PrivateNetworkServices
Inside web_tcp_server.h ,set the networkinteface and port, wlan0 is MT3620 internal wifi
static const uint16_t LocalTcpServerPort = 8080; static int serverBacklogSize = 3;
// WIFI
static const char NetworkInterface[] = "wlan0";
To start webserver:
webServer_ServerState *webServer_Start(int epollFd, in_addr_t ipAddr, uint16_t port, int backlogSize, void (*shutdownCallback)(webServer_StopReason));
To stop webserver:
void webServer_ShutDown(webServer_ServerState *serverState);
I am not go though the detail of webserver, briefly it listen the TCP Income, identify this is required by http clients, get the path and the GET query, send the web content to client and close it, the html itself make require update every 3s.
The webserver also support basic interactive between browser and device, such as
http://192.168.1.134:8080/?debug=&sea=1014.6
This enhance the log message showed and set the sea level pressure to 1014.6hPa for calculate the height of device.
You can modify the content and the "GET" query under LaunchWrite
static void LaunchWrite(webServer_ServerState *serverState)
Due to restrict of resource of those small iOT device, the web-server only allow one client at a time, also without implement secure (https) communication, the basic security feature on Azure Sphere OS
"Capabilities" is only allow whitelist, for outside of local network we recommend use intermediate server connect by curl, or use Azure IoT Central for fully security, but this good for quick enquire or first time setup wizard.
Connect IOS/Android though http server
{"temperature":28.22,"humidity":57.78,"pressure":1013.41,"altitude":0.75,"pm1":15,"pm25":15,"pm10":15,"light":11.71,"iaq":233,"eco2":2494,"evoc":14,"accuracy":0,"gas":352768,"fan":0,"time":1574952456}
Azure IoT Central
At the end, we connected the APP to Microsoft Cloud service, which for logging, monitoring and even make control of the devices though security online platform. As our project foundation is the Avnet ADC demo, we have basic framework of Azure.
First, we need build a create a new application under Azure IoT Central, create a new Device Templates, added the necessary telemetry, which is the sensor data, i am not go though the detail, the elements14 with a very good starting tutorial.
Under Measurement, we build 8 telemetry, the field name is corresponding of the json string name inside the i2c.c AccelTimerEventHandler which is the timer loop for update the sensor data.
snprintf(pjsonBuffer, JSON_BUFFER_SIZE, "{\"gX\":\"%.2lf\", \"gY\":\"%.2lf\", \"gZ\":\"%.2lf\", \"aX\": \"%.2f\", \"aY\": \"%.2f\", \"aZ\": \"%.2f\", \"pressure\": \"%.2f\", \"light_intensity\": \"%.2f\", \"altitude\": \"%.2f\", \"temp\": \"%.2f\", \"humidity\": \"%.2f\",\"iaq\": \"%d\", \"pm1\": \"%d\",\"pm2_5\": \"%d\",\"pm10\": \"%d\",\"fan\": \"%d\",\"fanspeed\": \"%d\",\"fanmodes\": \"%d\" }",angular_rate_dps[0], angular_rate_dps[1], angular_rate_dps[2], acceleration_mg[0], acceleration_mg[1], acceleration_mg[2], bme680_pressure/100.0f, light_sensor, altitude, bme680_temperature,bme680_humidity,(int)bme680_iaq,smuart04l_getPM1(),smuart04l_getPM2_5(),smuart04l_getPM10(),ltc1695_value,ltc1695_manual_value,ltc1695_mode);
for example, to gain the PM1 value, we modify the snprintf, added ,\"pm1\":\"%d"\ in the long string, and added smuart04l_getPM1() arguments.
After that use the AzureIoT_SendMessage send the formatted output pjsonBuffer to Azure IoT centre.
AzureIoT_SendMessage(pjsonBuffer); free(pjsonBuffer); Very simple.
Future more, we create two item under Setting, which for set of FAN speed (Number) and the FAN mode (Toggle).
Under device_twin.c we added two item on twinArray[]
{.twinKey = "fanspeed",.twinVar = <c1695_manual_value,.twinFd = NULL,.twinGPIO = NO_GPIO_ASSOCIATED_WITH_TWIN,.twinType = TYPE_INT,.active_high = true}, {.twinKey = "fanmode",.twinVar = <c1695_mode,.twinFd = NULL,.twinGPIO = NO_GPIO_ASSOCIATED_WITH_TWIN,.twinType = TYPE_BOOL,.active_high = true} };
When the user change the settings under Azure IoT centre and click update, the requirement will send to device, and triggering the deviceTwinChangedHandler, therefore we update the fan condition under this handler.
void deviceTwinChangedHandler(JSON_Object * desiredProperties) .... if (!strcmp(twinArray[i].twinKey,"fanmode")) { ltc1695_setmode( *(bool*)twinArray[i].twinVar); } .... if (!strcmp(twinArray[i].twinKey, "fanspeed")) { ltc1695_manual_value = *(int*)twinArray[i].twinVar; if (!ltc1695_mode) ltc1695_set(*(int*)twinArray[i].twinVar,false); }
We also need to inform Azure IoT centre when the Fan status change
void updateFanDevice() { for (int i=4;i<=5;i++) checkAndUpdateDeviceTwin(twinArray[i].twinKey, twinArray[i].twinVar, twinArray[i].twinType, true); }
Let's come back to Azure IoT centre, we need to create a new real Devices under Devices > Plus > Real, use default Device ID,
After that, click the Device created, and click upper right "Connect" item.
Copy the Scope ID, Device ID and Primary Key, run dps-keygen to generate the connection string
#define MY_CONNECTION_STRING "HostName=iotc-xxxxxxxx-xxxx-xxxx-xxxx-6ca12a71a134.azure-devices.net;DeviceId=xxxxxxxx-2616-4cfe-ac6a-35c9fff2e2bbb0;SharedAccessKey=4Uxxxxxxxxxxx/6OraNMh4bIOUxxxxxxxxMhefA+"
and change "AllowedConnections" string in app_manifest.json.
"AllowedConnections": [ "192.168.1.101", "iotc-xxxxxxxxx-8707-4aeb-a938-33xxxxxxx.azure-devices.net" ],
Reference of element14 tutorial for more detail.
Now, the Azure IoT centre is able to logging the device data,
Under setting, we are able to change the Fan setting:
Simple change the value and click "Update", the fan speed will change immediately.
If you push the hardware button to change the setting of fan, Azure will inform you the value do not match, it normal, and you can edit the value and send update usually.
Summary
In this project, we are use two sensor for monitoring the air quality, the laser dust detector use laser and camera to count the very small particulate matter <2.5um which impact our respiratory system, the GAS sensor which can detect mixture of toxic chemical to estimate the VOC and CO2 level, both is not good for our body. Those budget sensor is not good for very precision measurement or critical place such as observatory, but this work for monitoring, alerting and evaluating the working place or home environment air quality level as well as used in iOT project.
We also added a FAN to demonstration of air purifier working, although it is too small for real useful, but we can make it simple without external power supply.
The RGB LED used for indicate the air quality by colour, we use hardware PWM for dimming purpose. Our device also included a small web interface as local wifi network user can view the sensor data in the web browser directly. Finally, the Azure IoT central SaaS supported by official provide easy and security method for remote logging, monitoring, andd control, for example, a scheduling send simple json string to server is enough for a security telemetry process.
As the device, i hope Microsoft can open source more parts of OS, also provide more detail of hardware so we can work for the real-core better such as implement hardware GPIO interrupt.
Source Code
source code available in https://github.com/sicreative/azweather