In Part 3, we designed and implemented the hardware side of the project, using Vivado. After placing and connecting all the IP blocks, we synthesised, implemented and generated the bitstream for the Arty S7 board. A quick test gave us a good indication the build was good, so now that the system is ready, we need to write the software for our application, which will run on the MicroBlaze and use all the modules created in the FPGA.
As mentioned already in the previous part, the great thing about using MicroBlaze as soft processor is the tight integration between Vivado (hardware development tool) and Xilinx SDK (software development tool), which allows a streamlined end-to-end development. Once finished the bitstream generation, integrating it with the SDK simply means we need to export the hardware, including the bitstream, and launch the SDK: Vivado will create the hardware platform descriptor file (system.hdf) for the bitstream, which will be ready to use when you create your new project in the SDK.
The hardware descriptor contains all the information about what IP blocks have been used in the design, which allows the SDK to automatically set up the right bare-metal drivers' libraries to support the hardware. For each of the blocks, the descriptor provides also the addressing information (base and high address), used by the MicroBlaze to access memory mapped device registers or storage memory (internal BRAM and external DDR3 RAM/QUAD SPI Flash).
The actual mapping between the board and the hardware supporting libraries is provided by the Board Support Package. This package is automatically generated upon creation of a new project in the Xilinx SDK. Another important file, generated as well upon project creation, is the linker script (lscript.ld). This file is used to control where the executable file for our project will be loaded in memory, and it is particularly important because, modifying this file, you can fine tune the size of the stack and the heap, which can make the difference between having yor software running smoothly and experience inexplicable random failures.
The Requirements
Now that we have created the empty project, it is time to design the software. The basic requirements of our detector have been already identified in the previous blog, and I will expand them below, for easier reference:
- manage monitored zones: the field of view of the Grid-Eye sensor will be split in 4 regions, and each region can be enabled/disabled independently;
- manage the temperature monitoring mode: the temperature reading can be used either for absolute or differential check. In case of absolute check, the reading is compared against a preset threshold value, and if it is equal or greater than the reference, the alarm will set off. For the differential temperature, you need to have a background reference temperature, which will be subtracted from the reading, and such difference will be compared to the set threshold, setting off the alarm if equal or greater;
- set temperature threshold: depending on the monitoring mode, the threshold will be an absolute temperature or a differential. Absolute temperature could be either negative or positive, and will need to vary between a wider range (lets make it symmetric, from -47C to 47C). In differential mode, we are really trying to identify objects whose temperature is higher than the background temperature. Although the differential could be wide, the differential mode is most useful to identify humans, so in that case the differential is not very wide (lets start by assuming just 0-7C threshold range);
- acquire the background frame: this is the reference frame used for temperature differential check;
- reset the alarm: well, I think this is self-explanatory! :-)
The Design
As for the hardware, lets try to use a simple design for the software as well. On the right there is a first sketch of the basic flow chart.
Once the system is powered-up (or reset), it goes through the initialisation procedure, when all the system’s modules are brought up and configured.
The core of the action happens inside is the main loop that follows: the system will accept commands from the buttons, changing its operating mode accordingly, and then will read the thermal data from the Grid-Eye, and will use it to determine if an object has appeared in the field of view.
The main sticking points for the design are:
- the user interface design: need to devise an easy way to capture the user input, using only the 4 push buttons and the 4 switches;
- the reading of the sensor: the GridEye samples the thermopile sensors and refresh the data only every 1/10th of a second (10 Hz) at the fastest, therefore accessing the sensor via I2C at higher frequency would be only a waste of resources. As designed in the flow chart, the system would loop on the reading constantly, and considering the system runs with an 81 MHz clock, even considering the overhead for the processing, it is still way too fast, so that needs addressing
Reading the sensor data: delay vs timer
Let's address the last point first. As the problem is basically to "slow down" the loop, the first thought would be to add a delay. As the system is not particularly demanding for its timing, it would be easy to think such solution would solve our problems. But delays are blocking calls, and since our system would be spending most of the time waiting, beside the waste of resources, there is the potential problem of the "lack of responsiveness" caused by the blocking delays.
Considering that our system already uses interrupts for responding to user input, a better approach would be to follow the same route for reading the GridEye data as well: set up a timer that generates an interrupt every tenth of a second.
We don't have any timer amongst the resources we created earlier during the hardware design, this means we need to go back to our design and add one.
Fortunately, Vivado offers a Fixed Interval Timer IP block, where you can define exactly the time interval you want the block to fire an interrupt. Obviously, together with this block, we need to increase also the number of interrupt ports available on the Concat block, which will take care of bringing the line up to the AXI Interrupt Controller. The picture below shows the modified design.
Hopefully, there won't be the need to rework the design anymore, so lets fire up the Sythesis/Implementation/Bitstream generation build again, and make sure we export the hardware as well, so that our SDK project will be synchronised with the new hardware, and the new interrupt will be available to use.
Incorporating this change, we can now think the design in terms of interrupts. Working with main loop and interrupts, rather than flow chart, it makes more sense to describe the system using a state diagram. The application main loop, which will take care of the user interface and alarm (actioning the LEDs) will be the PROCESS_IDLE state.For the two interrupts, we need to write the interrupt service routines, which will be invoked by the system when the interrupt is triggered. Such routines will be very "light", as they will only set a flag which will cause the state to change.
The timer interrupt will cause the state to transition to PROCESS_READ_TEMPERATURE. In this state, the system reads the frame temperature from the GridEye and checks if the threshold has been hit.Once done, returns the state to PROCESS_IDLE. The user interface interrupt, generated by the buttons on the board, will cause the PROCESS_IDLE state to change to PROCESS_UI_INTERRUPT. In this state, the GPIO ports for both switches and buttons are read, and the data interpreted, according to the rules specified later on in the UI design section. Once the processing is done, the state moves back to PROCESS_IDLE.
The UI design
Lets address the other point we left suspended: the user interface. As simple as a system can be, it is only as simple as its user interface, and this is why it is important to find an easy (or, should I say, "easy enough") solution to the problem.
The system has 4 push button, which we can use for the main functions, and 4 switches, that can be used to "enrich" the push button, and provide the extras information needed. It seems natural to use the switches as a nibble (4-bit word), which we can use to let the user input data in binary form (either for on/off-enable/disable settings or for actual numbers). We can also use the LEDs to help, for example by flashing them to confirm the input of some switches configuration. The image below shows the mapping I have chosen for the system.
Let me explain the choices. The system needs to support the following functions:
- F1: Reset Alarm
- F2: Set Zones
- F3: Set Mode
- F4: Set Threshold
- F5: Acquire Background (only for differential mode)
For function F4, if the switches SW0-SW3 are used to input the temperature in binary form, that would only allow for values from 0 to 15C, which is not enough. We need to extend the range by using some fixed offset. Moreover, for the absolute threshold, we also need to define the sign of the threshold, as it could be either positive or negative.
Ideally, each button should be used for one function only , but unfortunately the board has only 4 buttons, so one of them will need to be "overloaded".This is the mapping:
- BTN0: F1 (switches not used)
- BTN1: SW0-SW3 will be used for F2 (each switch represents one zone: enabled=1, disabled=0)
- BTN2: Switch SW0 will set the mode (F3) - OR - SW1-SW3 will be used for F4 (if in differential mode), or SW1 will set the sign and SW2-SW3 set the offset (in absolute mode)
- BTN3: F5 (if in differential mode -switches non used) - OR - SW0-SW3 will be used for F4, and the total threshold will be calculated as this value plus the offset previously set by SW2-SW3 with BTN2, all with the sign assigned by SW1 with BTN2
It sound very convoluted, but actually it is not as bad as it seems. The video will show how I have implemented the UI, and also how the LEDs are used for confirming the user input.
I have mentioned detection zones before, without explaining what they are exactly. The GridEye sensor consists of an 8x8 array of thermopiles, for a total of 64 pixels. The field of view of the sensor can be approximated as a square area, with an opening angle of 60 degree, both vertically and horizontally. For the detector, this area is split in 4 detection areas (zones) of the same size, 4x4 pixels. The zones are indexed from 1 to 4, as shown in the image below.
One final note about the user interface: to help improve the experience, the 4 green LEDs of the board will be used to give confirmation of the action to the user. The logic for the flashing of the LEDs will be part of the PROCESS_IDLE state processing, this way we will have good response time to the user action. Same applies for the alarm signal: when one or more zone detect an object, the alarm will be triggered. This will be shown on the board by switching on the 2 RGB LEDs (using only the red colour), and by turning on the green LEDs corresponding to the zones that sensed the object. To make sure the user interaction is not influenced by the alarm switched on (and viceversa), the status of the alarm light is preserved, so while providing input, the user will still have the usual response (confirmation flashing lights), but at the end of that, the alarm status is restored, so the information is not lost.
The Code
Lets take a look at the more important parts of the code. The complete source code for the detector can be found on Github .The first bit of code needed is the one used to query the I2C device, and make sure we can interact with it. As mentioned earlier, all the information needed to interact with the hardware can be found in the Board Support Package. Specifically, if you drill down into the BSP folder, created by the SDK for your project (the folder name will be <your_project_name>_bsp), you will find a microblaze_0/include folder, which contains all the drivers files. The one to look for is the xparameters.h, which contains all the defines for the constants we will need to be able to use the I2C master device.
The GridEye sensor address for the I2C communication is 0x68, and from the datasheet, we know that the pixel data can be read from the registers, starting from the address 0x80 (pixel 1) and ending at 0xFF. Each pixel needs 2 bytes for the temperature (actually 11 bit + 1 bit for the sign), and they are stored with the LSB first (ie: register at 0x80 contains the LSB for pixel 1, register at 0x81 contains the MSB for pixel 1, and so on...). The SDK provides some low level APIs for the I2C communication: XIic_Send() and XIic_Recv(). The code that takes care of the basic communication (reading the value from the GridEye registers) is part of the file grideye.c, and an excerpt is publised below (the XPAR_IIC_0_BASEADDR is defined in xparameters.h, and it is the base address for the master I2C controller device) :
inline int read_iic_register(u8* reg, u8 *value){ if(XIic_Send(XPAR_IIC_0_BASEADDR,GRIDEYE_ADDRESS,reg,1,XIIC_REPEATED_START) != 1){ return XST_FAILURE; } if(XIic_Recv(XPAR_IIC_0_BASEADDR, GRIDEYE_ADDRESS, value, 1, XIIC_STOP) != 1){ return XST_FAILURE; } return XST_SUCCESS; }
The other import part of the code is, again, the one that initialise the hardware. Besides the input and output ports, we need to set up the Interrupt Controller, add the interrupt service routines for the timer and gpio interrupts and enable the interrupts. This is all taken care by the in the file detector.c (functions setup_system(), gpio_int_handler() and timer_int_handler()). Below is the excerpt:
void gpio_int_handler(void *ctrl){ XGpio_InterruptGlobalDisable(&gpio1); u32 ch = XGpio_InterruptGetStatus(&gpio1) & BTNS_SW_INT_MASK; if(ch==XGPIO_IR_CH1_MASK){ // Buttons uiInterruptInfo.buttons=XGpio_DiscreteRead(&gpio1, ch); uiInterruptInfo.switches=XGpio_DiscreteRead(&gpio1, XGPIO_IR_CH2_MASK); flags |= UI_INTERRUPT; } //Clear the interrupt both in the Gpio instance as well as the interrupt controller XGpio_InterruptClear(&gpio1, BTNS_SW_INT_MASK); XIntc_Acknowledge(&intc,INTC_GPIO_INTERRUPT_ID); XGpio_InterruptGlobalEnable(&gpio1); } void timer_int_handler(void *ctrl){ if(current_state == PROCESS_IDLE){ flags |= TIMER_INTERRUPT; } XIntc_Acknowledge(&intc,INTC_TIMER_INTERRUPT_ID); } /* * Setup GPIO controller and Interrupt controller * * There are 5 interrupts defined for the system:* * int0 - I2C controller * int1 - UART controller * int2 - GPIO1 (switches & buttons) * int3 - QSPI Flash controller * int4 - Timer (0.1s interrupt) */ int setup_system(void){ int Status; /* * Setup GPIO controller for buttons and switches * set the GPIO ports and their interrupts */ XGpio_Initialize(&gpio1, BTNS_SW_DEVICE_ID); XGpio_SetDataDirection(&gpio1,XGPIO_IR_CH1_MASK,1); //set push buttons as input port XGpio_SetDataDirection(&gpio1,XGPIO_IR_CH2_MASK,1); //set switches as input port XGpio_InterruptEnable(&gpio1, BTNS_SW_INT_MASK); XGpio_InterruptGlobalEnable(&gpio1); /* * Setup GPIO controller for leds * set the GPIO ports */ XGpio_Initialize(&gpio0, LEDS_DEVICE_ID); XGpio_SetDataDirection(&gpio0,XGPIO_IR_CH1_MASK,0); //set rgb leds as output port XGpio_SetDataDirection(&gpio0,XGPIO_IR_CH2_MASK,0); //set leds as output port //test leds on init for(u8 i=1;i<64;){ XGpio_DiscreteWrite(&gpio0,XGPIO_IR_CH1_MASK,i); XGpio_DiscreteWrite(&gpio0,XGPIO_IR_CH2_MASK,i); usleep(100000); XGpio_DiscreteWrite(&gpio0,XGPIO_IR_CH1_MASK,0); XGpio_DiscreteWrite(&gpio0,XGPIO_IR_CH2_MASK,0); usleep(100000); i*=2; } //switch leds off XGpio_DiscreteWrite(&gpio0,XGPIO_IR_CH1_MASK,0); XGpio_DiscreteWrite(&gpio0,XGPIO_IR_CH2_MASK,0); /* Initialize the Interrupt controller */ Status = XIntc_Initialize(&intc, XPAR_INTC_0_DEVICE_ID); if (Status != XST_SUCCESS) { return XST_FAILURE; } /* Perform a self-test to ensure that the hardware was built correctly.*/ Status = XIntc_SelfTest(&intc); if (Status != XST_SUCCESS) { return XST_FAILURE; } /* Connect the interrupt handler routines */ Status = XIntc_Connect(&intc, INTC_GPIO_INTERRUPT_ID, // interrupt line from xconcat to intc (XInterruptHandler)gpio_int_handler,(void *)&intc); if (Status != XST_SUCCESS) { return XST_FAILURE; } Status = XIntc_Connect(&intc, INTC_TIMER_INTERRUPT_ID, // interrupt line from xconcat to intc (XInterruptHandler)timer_int_handler,(void *)&intc); if (Status != XST_SUCCESS) { return XST_FAILURE; } /* Enable all interrupts (5 interrupt lines -> 5 bits width -> 0x1f mask) on the Interrrupt controller */ XIntc_EnableIntr(INTC_BASE_ADDRESS,0x1f); /* Start the Interrupt controller */ XIntc_Start(&intc, XIN_REAL_MODE); /* Setting up exceptions handler */ Xil_ExceptionInit(); Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_M_AXI_I_EXCEPTION, (XExceptionHandler)XIntc_InterruptHandler, &intc); Xil_ExceptionEnable(); current_state = PROCESS_IDLE; return XST_SUCCESS; }
The rest of the code, made up mostly by the main loop, contains the logic for the states transitions and the code for the UI leds management.I will not add it here, as it is quite long, but it can be viewed on Github.
Conclusions
We finally arrived at the end of this journey. It has been quite demanding, as the amount of documentation to digest was (is) rather large, but all in all has been a great experience so far. Sure there have been a few hiccups on the way, and I think having chosen to develop on Linux didn't help, as I do believe that the Windows version is more stable. Anyway, I think I have only scratched the surface of what this board is capable of and I'm now looking forward to experimenting with the board using RTL design. To the next journey...
Top Comments