In this post, I'm using the ADC and I2C features to finalise the display and show the board/MCU interfacing with the external world. I'm particularly interested in how much more complex this is than, say, something like an Arduino which is well-provisioned with libraries to help with this.
WWTL;DR:
I’m aiming these posts at those who are not familiar with MCU development to give them a feel of what they will go through. I know not everyone will be interested in reading this in detail so I’ll provide a brief summary of the points:
- I wanted to hook this into my PSU but the voltages are incompatible so I have to test on the breadboard. This is what I initially did with the 4Duino during development of the PSU.
- The ADC was easy to set up, configure and get running.
- I2C, similarly, was easy to set up, configure and get running.
- The documentation for both is comprehensive and useful. The hardware manual comes in handy as well for these.
- In particular, I’d have to praise the FIT modules and Smart Configuration for the heavy lifting and the ease in which this can be done. It would seem that after a short while, you ‘get your eye in’ so to speak and have a better understanding of how this hangs together.
- Not really part of the Envision board per-se, but emWin is proving a nice tool for developing with as well. It’s great that Renesas offer this with the board.
- I'm very impressed: this was no more complex or difficult than doing this with an Arduino.
Limitations for my Roadtest
The RX65N MCU operates at a voltage from 2.7V to 3.6V. The 12-bit ADC analog input pins' voltage is AVss <= Vpin <= AVcc. AVss and AVcc do not need to be tied to Vcc but on the Envision board they are and VCC = AVcc = 3.3V; Vss = AVss = 0V. There are two 12-bit conversion units: 0 and 1; thus, AVSS0; AVCC0; AVSS1; AVCC1. The Arduino pins are tied across both groups.
Therefore, voltage on the Analog pins must lie between 0V and 3.3V. I was going to connect this to my PSU, but its Vout for analog conversion on the thermistors is set to 0V to 5V; the pull-ups on the I2C lines are to 5V as well. The maximum temperature the thermistors could read without blowing the voltage limit would be 40C and they would do that easily: soak testing whilst I was building the unit indicates that. I could do this with a voltage divider I guess but it’s just going to affect the results. Instead, I will do it via a breadboard: I have an INA260 IC that I can plug in to read Voltage and Current (via I2C) plus three thermistors for temperature readings. I can simulate other temperature readings and current (which are driven off the Linear Regulators with a small current) with trimmers. The Arduino pins on the Envision board provide for a 3.3V output which can be used to drive the test circuit.
So, although not quite what I wanted to do, the principle is exactly the same. Indeed, this is highly representative of the circuit inside the PSU except:
- I’m forced to use 1x10K and 2x100K thermistors because of the parts I have, rather than 3x10K
- I will drive everything at 3.3V rather than 5V, or 0-15V for the load.
- The Linear regulators generate a small current for their temp pins and iMon current setting; I will use three trimmers.
I think this is still a perfectly valid test as the underlying approach is the same and it will reach a conclusion on how easy this is to program and set up as a HMI interface to any hobbyist project.
Here’s the test circuit:
Implementing the Analog to Digital Conversions
If you haven’t already, it is worth reading the brief note 4E which gives an overview of the ADC capability of the RX65N. If there are any terms in the description below which you don’t know, I’ve explained them in that post.
The Envision board will use the Arduino headers to drive the ADC units as below, specifically CN10 and CN8:
From the images above it’s possible to see that the power supply for the test circuit can be pulled directly off the board. The analog pins, A0 to A5, are tied to ‘channels’ on the RX65N ADC units:
- AN000 to AN003 are Unit 0 and channels 0 to 3
- AN114 and AN116 are Unit 1 and channels 14 and 16
These are important elements in the API which are heavily unit and channel driven, as you’d expect. There is a FIT module for the ADC functionality, r_s12ad_rx, and it is specific to the RX65N. The application note that comes with the FIT module to explain the API is comprehensive and to be fair against what I’ve posted in the past, quite understandable. It does describe the APIs in terms of ALL functions irrespective of the capability of the RX MCU you have: that is, it is up to you to work out what is/is not available for your particular MCU. It could do with a summary chapter to cover this really as I ended up having to implement the FIT module, configure it, generate the code, then inspect the header files to determine what I could/could not do. I also found it necessary to also consult the RX65N hardware manual as well, which is a 2,729 page monster packed full of detail (as manuals go, large but extremely comprehensive.) Not a major problem really.
Ok, so we know the channels that will be used. Configuring the ADC module to use these is extremely simple:
Basically, install the module then select both units and the 6 channels. In the image above, channels 14 and 16 are off the display but are ticked. This also automatically sets the right Ports and Pins on the RX65N and generating the code creates the necessary headers and setup code. There’s nothing dramatic here, it really is as simple as ticking some checkboxes; it is worth pointing out that the hardware manual warns about various combinations of ADC pins in conjunction with nearby output pins which will affect the results - basically, avoid those combinations if at all possible, otherwise use averaging and discard highest/lowest readings and/or other techniques to improve the accuracy.
Application Logic
The high-level logic flow for ADC operation in an application is:
- Initialise and open the ADC units
- Initialise Ports and Pins for the ADC and application
- Perform application Initialisation if any, e.g. the GUI
- Initialise and enable the channels
- Initialise the Timer unit for triggering
- Enable triggers
- Main application loop runs
Here’s main() showing this:
voidmain(void) { initialiseADC(); // Initialise all ports, pins and the GUI, displaying the main interface R_Pins_Create(); GUI_Init(); CreatewndMainInterface(); // Enable the channels for each unit enableUnit0Channels(); enableUnit1Channels(); // Initialise trigger source (MTU0) and enable triggers init_mtu0(); enableTriggers(); // Main application loop while(1) { GUI_Delay(100); } } // End of main()
I’m not posting all the application code here but I have zipped up the project and attached it. I will go through the ADC relevant elements though.
Initialising the ADC Units
The ADC units need to be configured and opened before any pins are configured. The generated code creates a raft of definitions to make this easy:
// Initialise ADC config.resolution = ADC_RESOLUTION_12_BIT; // 12 bit resolution config.alignment = ADC_ALIGN_RIGHT; // conversions are right-aligned config.add_cnt = ADC_ADD_AVG_4_SAMPLES; // Average of 4 conversions used config.clearing = ADC_CLEAR_AFTER_READ_OFF; // data remains in register after reading config.trigger = ADC_TRIG_SYNC_TRG0AN; // trigger from mtu config.trigger_groupb = ADC_TRIG_NONE; // group mode not used config.trigger_groupb = ADC_TRIG_NONE; // group mode not used config.priority = 3; // interrupt priority 3 (1=low, 15=high) config.priority_groupb = 0; // group mode not used config.priority_groupc = 0; // group mode not used config.temp_sensor = ADC_TEMP_SENSOR_NOT_AD_CONVERTED; // sensor not used config.add_temp_sensor = ADC_TEMP_SENSOR_ADD_OFF; // sensor not used // Configure and open both ADC units adc_err = R_ADC_Open(UNIT0, ADC_MODE_SS_MULTI_CH, &config, MyCallback); // Open for multiple channels while (ADC_SUCCESS != adc_err) { // Error - loop here for debug R_BSP_NOP(); } adc_err = R_ADC_Open(UNIT1, ADC_MODE_SS_MULTI_CH, &config, MyCallback); // Open for multiple channels while (ADC_SUCCESS != adc_err) { // Error - loop here for debug R_BSP_NOP(); }
Some explanations:
- The ADC units can be configured for 12-, 10- or 8-bit operation, the trade-off being speed vs accuracy of conversion
- The units can work in what is called addition mode - which is an integration of multiple readings (two, three, four or sixteen) - or averaging; here it is using the average of 4 samples.
- Clearing after reading can be used to detect failures in writing to registers. After reading, the value would be set to 0x0000 which would be an indicator, after the next reading if still 0x0000, that writing failed.
- The readings can be triggered through timer units or by software (i.e. manually.) In this implementation, the channels are not being grouped but I am using a timer to trigger the reads so that updates can be undertaken through interrupts.
- Unit 1 is able to perform a conversion on the internal temperature sensor for the RX65N. I'm choosing not to do so.
- Each unit is configured and opened for multi-channel readings - in this case, that setting is for Single-Scan mode, rather than Continuous-Scan mode. When conversion is triggered and completed, the callback function is executed.
Initialising the Channels
Configuring the channels is similarly simple:
ch_cfg.chan_mask = ADC_MASK_CH0 | ADC_MASK_CH1 \ | ADC_MASK_CH2 | ADC_MASK_CH3; // select channels 0 to 3 ch_cfg.priority_groupa = ADC_GRPA_PRIORITY_OFF; // group mode not used ch_cfg.chan_mask_groupb = ADC_MASK_GROUPB_OFF; // group mode not used ch_cfg.chan_mask_groupc = ADC_MASK_GROUPC_OFF; // group mode not used ch_cfg.add_mask = ADC_MASK_ADD_OFF; // no channels using addition ch_cfg.diag_method = ADC_DIAG_OFF; // self-diagnosis off ch_cfg.anex_enable = false; // no external amplifier ch_cfg.sample_hold_mask = ADC_MASK_SAMPLE_HOLD_OFF; // Bypass chnl-dedicated S&H circuits ch_cfg.sample_hold_states = ADC_SST_SH_CNT_DEFAULT; // default sample & hold states adc_err = R_ADC_Control(UNIT0, ADC_CMD_ENABLE_CHANS, &ch_cfg); while (ADC_SUCCESS != adc_err) { // Error - loop here for debug R_BSP_NOP(); } ch_cfg.chan_mask = ADC_MASK_CH14 | ADC_MASK_CH16; // select channels 14 and 16 adc_err = R_ADC_Control(UNIT1, ADC_CMD_ENABLE_CHANS, &ch_cfg); while (ADC_SUCCESS != adc_err) { // Error - loop here for debug R_BSP_NOP(); }
Some explanations:
- Channels to be activated are masked together; each Unit deals with different channels
- No groups are being used
- The ADC units can undertake self-diagnosis before each scan by converting an internal reference voltage
- It is possible to use an external Operational Amplifier as part of the conversion of multiple channels (only 0 to 7). The incoming signal reaching a channel is fed out into an operational ampler before feeding back in to the ADC unit. This is done for each channel that is active, one at a time.
- It is possible to set up scanning to sample and hold across channels and then undertake the conversion. I.e., each channel used for sample-and-hold is scanned first, and once sampling is completed, each is converted.
- In the above code, you can see that the majority of configuration options are the same for each unit. However, it is possible for these to be different although I suspect it becomes more useful to do so when groups are used.
Application Callback
Once the application is running, the ADC is operating under trigger control of a timer unit (in this case.) When triggered, it will read the configured channels, convert and store the value in registers for each channel. It will then execute the callback function passing callback arguments. In this function, the values can be read and processed, and it is, again, a simple process - first, we can check the unit and that the scan is completed before reading the value:
if ((UNIT0 == p_args->unit) && (ADC_EVT_SCAN_COMPLETE == p_args->event)) { adc_err = R_ADC_Read(unit, channel, result); while (ADC_SUCCESS != adc_err) { // Error - loop here for debug R_BSP_NOP(); } }
Some explanation:
- p_args is a structure defining the unit and completion event (nothing else)
- channel is the channel number.
- Result is the value between 0 and 4095 (when in 12-bit resolution)
Doesn’t get any easier than that. The result can then be turned into an actual voltage between 0 and 3.3V, or a temperature using the Steinhart-Hart equation and the display updated.
At the bottom of the post there is a video showing the display updating, but next I'm looking at the I2C interface.
Implementing the I2C Readings
Volts, Amps and Power are going to be read using the I2C interface from an INA260 IC. On the Envision board these SDA and SCL are in the standard Arduino position:
On this board though, ports 50 and 52, Pins 56 and 54, are shared with the PMOD connector and the board is set up, by default, for connections through than connector. Some re-configuration is required to use the SDA/SCL lines then:
The resistors are, thankfully, easy to get at and look like 0805 size which I can manage:
However, do I need to swap them over? Can’t I just plug the leads into the PMOD connector and have ports 50/52, pins 54/56 configured as SDA/SCL? I don’t see why not - there are 200R resistors in series with PMOD pins 2 and 3 but that won't interfere with the SDA/SCL signals. So I'm not going to actually change these over unless it does cause a problem - I have no wish to damage anything accidentally and whilst not difficult to get to they are only about 1mm away from the MCU.
Renesas provides two FIT modules for I2C: Simple I2C and I2C Bus Interface. A comparison:
Simple I2C | I2C Bus Interface |
---|---|
Features | |
Single master mode (no slave transmission/reception | Master and slave transmission and reception |
Bus Condition Waveform generation | Multi-master configuration |
Standard or fast communication, max 384kbps | Standard or fast communication, max 400kbps. On RX65N, Channel 0 supports 1Mbps |
Simple I2C will suit the needs here: the I2C bus operates in a very similar manner (from scanning the application note) but clearly has the potential for better communication speeds as well as multiple masters and one slave. I can’t use the 1Mbps speed setting as my I2C connection is not on Channel 0, and I have only one master and one slave.
There are some demo apps for the RX64M on the starter kit board which is really all I have to work from with examples. For some reason, the Smart Configurator won’t show any configuration for this so I have to figure it out from code and the Application Note. Fortunately I have a reasonable grasp of what to do with the SC now. As can be seen in the image above of the SDA/SCL lines, these are SDA2 and SCL2 which is channel 2. Therefore, not unreasonable to select SCI2 and enable these connections, set their pins to Port 52 and 54 and generate the code.
Sending data
First approach will be to just create a simple app that sends some data to the INA260 and make sure there are no errors. It doesn’t seem too complicated looking at the demo code - I will amend that to send the configuration data to the INA260 that I want. This would consist of (values from my old code/INA260 data sheet):
- Send the slave address, 0x40 (this is configured by wiring two of the INA260 to ground)
- Send the config register to update, 0x00
- Send a high byte to tell it to take 16 averages, 0x65
- Send a low byte to tell it the conversion time 8.244ms, 0xFF
Note that the INA260 treats the first data byte received as high, the second as low. There’s no specific flag or other indicator to determine that.
The code to configure the Simple I2C operation, for the above:
sci_iic_return_t ret; /* For verifying the return value of the API function */ sci_iic_info_t iic_info_device; /* Information structure for slave device*/ uint8_t slave_addr_device[1] = {0x40}; /* Slave address of device */ uint8_t access_addr_device[1] = {0x00}; /* Address to be accessed in device */ uint8_t send_data[2] = {0x65,0xff}; /* Transmit data, high byte, low byte */ /* Set up the configuration data structure for Simple IIC operation. */ iic_info_device.p_slv_adr = slave_addr_device; /* Pointer to the buffer to store the slave address */ iic_info_device.p_data1st = access_addr_device; /* Address to be accessed in device (to be updated) */ iic_info_device.p_data2nd = send_data; /* Pointer to the buffer to store the second data (to be updated) */ iic_info_device.dev_sts = SCI_IIC_NO_INIT; /* Device state flag (to be updated) */ iic_info_device.cnt1st = sizeof(access_addr_device); /* First data counter (to be updated) */ iic_info_device.cnt2nd = sizeof(send_data); /* Second data counter (to be updated) */ iic_info_device.callbackfunc = &callback_sci_iic_send; /* Callback function */ iic_info_device.ch_no = 2; /* Channel number */
Some explanation:
- Most of the above is self explanatory given the data values I want to send
- The I2C connection is initially not initialised; this setting will ensure the module does this
- The callback function is executed once a NACK is received from the Slave;
- As noted above, the Arduino headers are operating on Channel 2.
The basic operational logic is to:
- Initialise and open the connection to the slave
- Send the address data (data1st)
- Loop until a Finish status is received…
- …terminating if a timeout is reached (in my demo, a simple counter up to 100)
- …on a NACK response, send the data (data2nd) OR terminate on 200 NACK signals (arbitrary, but set to 200 in my demo)
I’m just outputting some status strings to the display as the app goes through that process. And the result:
Success.
Next, is to read some data from the INA260. During its operation, the IC is storing volts, amps and power in their own registers. To read these values, I need to send the register to read to the IC (no additional data), then receive the data, high byte followed by low byte. Volts will do and that value is stored in register 0x02.
The process to set register 0x02 and receive two bytes is pretty much the same as above:
Initialise and open the connection to the slave
- Send the address data (data1st) - 0x02
- Loop until a Finish status is received…
- …terminating if a timeout is reached (in my demo, a simple counter up to 100)
- …ask for the data (data2nd)
Here’s the configuration code for receiving data:
sci_iic_return_t ret; /* For verifying the return value of the API function */ sci_iic_info_t iic_info_device; /* Information structure for slave device*/ uint8_t slave_addr_device[1] = {0x40}; /* Slave address of device */ uint8_t access_addr_device[1] = {0x02}; /* Address to be accessed in device */ uint8_t receive_data[2] = {0x00, 0x00}; /* Receive data */ /* Set up the configuration data structure for Simple IIC operation. */ iic_info_device.p_slv_adr = slave_addr_device; /* Pointer to the buffer to store the slave address */ iic_info_device.p_data1st = access_addr_device; /* Address to be accessed in device (to be updated) */ iic_info_device.p_data2nd = receive_data; /* Pointer to the buffer to store the second data (to be updated) */ iic_info_device.dev_sts = SCI_IIC_IDLE; /* Device state flag (to be updated) */ iic_info_device.cnt1st = sizeof(access_addr_device); /* First data counter (to be updated) */ iic_info_device.cnt2nd = 2; /* Second data counter (to be updated) */ iic_info_device.callbackfunc = &callback_sci_iic_receive; /* Callback function */ iic_info_device.ch_no = 2; /* Channel number */
Some explanation:
- Receive data is two bytes; high in position 0, low in position 1
- The device status at the time of receiving is now idle and shouldn’t be re-initialised
- The rest is pretty much the same as explained for Sending.
Again, it works and it was incredibly simple to get working. When I did this for my PSU with a 4Duino, I had access to the “wire” library and commands were “simpler” because the configuration was hidden in the library, but what you see above is easily obtained from the Application Note and sample program, adjusted for my purpose. Even setting up the base configuration through Smart Configurator was intuitive. Just to be sure that it is talking, I’ve tested error conditions, e.g. by removing pull up resistors, and the MCU is reporting error conditions (in this case, bus busy condition.)
Display in Operation
The (shorth and dull) video is showing the operation of the GUI when hooked up and using both the ADC conversion and I2C. This is running purely on a loop/trigger basis with no attempt to control timings, hysteresis or interrupts of interrupts: a better approach would be to run it purely from a Compare and Match timer with software triggered readings of the ADC. This would give much better control over ‘when’ and ‘how often’ readings are taken, however for the road test, it wouldn’t really test the more complex aspects of the MCU (i.e. the triggers), only my ability to write simple software. As an example of what I mean, in my PSU, the Volts, Amps and Power are read every 500mS but the temperatures are read only every 30 seconds.
The point is though, it does show the ADC and I2C features working and as I report above, that was a simple, easily achieved activity on this board.
Additional Notes
Multi-Buffering
When I first had the GUI updating values, the screen was flickering during the update. This is due to the way that emWin works and writes to the driver of the display: essentially, a whole ‘rectangle’ of the display from the top-left point of the first change, to the bottom right point of the last change in that rectangle.
For example, with reference to the above image, if I change the value of the iMon current and the value of the Mosfet temperature, you can envisage a rectangle covering the area marked by the blue rectangle being marked invalid, thus encompassing more than a quarter of the screen. This whole area being invalid is redrawn. As this is done frequently, it results in flickering which is very annoying. The answer is to enable a feature called ‘Multi-buffering’. When it is, the window manager creates two buffers: a front buffer and a back buffer. Screen updates are made into the back buffer, then the LCD memory is pointed at that buffer. Thus, the back buffer becomes the front buffer and the previous front buffer becomes the back buffer! I hope that makes sense. In turn, it means the whole screen is redrawn by the LCD driver at once because it's a simple memory address pointer change. It’s actually possible to triple-buffer, the benefit being related to being able to callback on a VSYNC interrupt which is the ideal point to flip buffers to prevent tearing (VSYNC, as I understand it, is the point at which the LCD has just been refreshed (which it is 50/60 times per second.) If the app is waiting to swap buffers but needs to draw, it can do so on the third buffer.
It all gets a bit complicated but fortunately, the window manager is able to handle this automatically just by calling the command WM_MULTIBUF_Enable(1).
It works. Clearly there needs to be enough RAM to be able to do this: (X-size * Y-size * bitsperpixel / 8) for each buffer. For the Envision board that is (480 * 272 * 16) / 8 = 255K, which sounds like a lot but here’s a memory captured during the application execution; it’s certainly useful having that extra ram on board the RX65N.
Timing
The MCU doesn’t have a concept of ‘current time’ - in fact, the standard C Library time.h isn’t included in the list of standard libraries with the compiler. This makes sense of course. So, in order to calculate the elapsed time I used the Compare and Match Timer to interrupt every 1000mS to increment the seconds time. Surprisingly perhaps, given the way the Smart Configurator and Code Generation modules works this is actually easier to do than working from a standard library call “millis()” because, although not at all complex, that requires keeping tracking and testing for 1000mS elapsed which is actually more work.
Summary Thoughts
Getting the ADC and I2C configured and running was actually very simple and I feel after writing all the above there should be more to say. I’ve been used to using libraries that hide some of the complexity, but with the FIT modules doing a lot of the heavy-lifting, and the demos which were readily adaptable, I was able to get it up and running very easily. The documentation is comprehensive and useful. It was also handy to not have to reconfigure the jumpers to get I2C running but they looked like they wouldn’t be too much problem - at least the LCD would not need removing again. I’m impressed with this: I expected it to be a lot harder than accomplishing the same thing with an Arduino, but it wasn’t.
Incidentally, it is possible with the Arduino headers in place to actually remove a jumper and have the board powered from an external 5V supply whilst the debugger takes its power from the USB connection. Thinking about this further: it means that mounting the board into a case, it can be powered from a regulated 5V supply (I assume it has to be regulated, it doesn't say.) The USB connection can be extended to a connector on the case for re-programming in the future. This is exactly how the 4Duino that I use in my PSU works.
I've added a zip file of the project below. If you want to get it working though you will need to build the schematic at the start of this post.
Further Entries
I will update this entry to provide links to further instalments as I create them.
Part Three: Development Software
Part Four: Brief notes, first part
Part Five: Static Setup of the GUI
Part Six: Getting the Display Working (this post.)
Part Seven: USB/Serial Interface
Also, worth reading Jan Crump's road test as he's taking a more technical approach.