Quantcast
Channel: Embedded Lab
Viewing all 713 articles
Browse latest View live

Getting Started with Nuvoton 8-bit Microcontrollers – Coding Part 3

$
0
0

This post is a continuation of the previous post on Nuvoton N76E003 microcontroller here.

s-l1600

Communication Peripheral Overview

N76E003 packs basic serial communication peripherals for I2C, SPI and UART. These interfaces enable us to interface external EEPROM and flash memories, real-time clocks (RTC), sensors, etc.

Comm types

N76E003 has two hardware UARTs, one SPI and one I2C peripheral. The UART interfaces can be further expanded to implement RS-485 and RS-422 communication protocols. With ordinary GPIOs and optionally with hardware timers we can implement software-based UART, SPI and I2C too. However, software methods are slow and resource-hungry. Software-based approach is, on the other hand, needed when we have to interface devices that don’t use any of these communication topologies. For example, the one-wire protocol used by DHT series relative humidity and temperature sensors needs to be implemented by using an ordinary GPIO pin. Same goes for typical text and graphical LCDs.

N76E003 Comms

Note that there is a conflict between UART1 and alternative I2C pins. There are also similar conflicts with other hardware peripherals like PWMs, ADCs, etc since N76E003 packs lots of stuff in such a small form-factor. Every single GPIO pin is precious. Try to keep things with default GPIO pins in order to reduce conflicts with other hardware peripherals, coding and to maximize effective use of GPIOs. Likewise bear in mind that timers have also such conflicts as they are used to implement both hardware-based delays and the UART peripherals. You have to know for sure what you are doing, what do you want and with which stuffs you wish to accomplish your task.

Serial Communication – UART

To date serial communication (UART) is perhaps the simplest and widely used form of communication in use. UART block is only block available in most microcontrollers that can be easily interfaced with a computer or a phone. Most communication modules like Bluetooth, Wi-Fi, GSM modems, GPS modules, etc are interfaced using UART. It has incredible range and is also the backbone of other communication methods like RS-485, RS-422, etc.

HMC1022

To learn more about UART visit the following link:

https://learn.mikroe.com/uart-serial-communication

 

Code

 

HMC1022.h

#define Get_Angular_Measurement                      0x31
#define Start_Calibration                            0xC0 
#define End_Calibration                              0xC1                     
#define Set_Magnetic_Declination_High_Byte           0x03  
#define Set_Magnetic_Declination_Low_Byte            0x04

 

#define no_of_data_bytes_returned                    0x08

 

#define calibration_LED                              P15

 


void read_heading(void);
void calibrate_compass(void); 
void factory_reset(void);
void set_declination_angle(unsigned long angle);

 

HMC1022.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "soft_delay.h"
#include "HMC1022.h"

unsigned char done = 0;
unsigned char data_bytes[no_of_data_bytes_returned] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};


void read_heading(void)
{                        
     unsigned char s = 0; 
     unsigned long CRC = 0;

     Send_Data_To_UART1(Get_Angular_Measurement); 

     for(s = 0; s < no_of_data_bytes_returned; s++)
     {               
        data_bytes[s] = Receive_Data_From_UART1();
        if(s < (no_of_data_bytes_returned - 1))  
        {
            CRC += data_bytes[s];
        }
     }                                                      

     CRC = (CRC & 0xFF);

     if(CRC == data_bytes[7])
     {                                    
       done = 1;
     }    
}

void calibrate_compass(void)
{
 unsigned char s = 0x00;    

 Send_Data_To_UART1(Start_Calibration);

 for(s = 0; s < 60; s++)
 {
     calibration_LED = 1;       
     delay_ms(100);
     calibration_LED = 0;
     delay_ms(900);
 } 

 for(s = 0; s < 60; s++)   
 {
     calibration_LED = 1;   
     delay_ms(400);            
     calibration_LED = 0;
     delay_ms(600);
 }             

 Send_Data_To_UART1(End_Calibration);
}

void factory_reset(void)
{
 Send_Data_To_UART1(0xA0);
 Send_Data_To_UART1(0xAA);
 Send_Data_To_UART1(0xA5); 
 Send_Data_To_UART1(0xC5);
}     

void set_declination_angle(unsigned long angle)
{
 unsigned long hb = 0;
 unsigned char lb = 0;

 lb = (angle & 0x00FF);   

 hb = (angle & 0xFF00);
 hb >>= 8; 

 Send_Data_To_UART1(Set_Magnetic_Declination_High_Byte);    
 Send_Data_To_UART1(hb);

 Send_Data_To_UART1(Set_Magnetic_Declination_Low_Byte);
 Send_Data_To_UART1(lb);
}

 

main.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "soft_delay.h"
#include "LCD_2_Wire.h"
#include "HMC1022.h"

const unsigned char symbol[8] =
{
   0x00, 0x06, 0x09, 0x09, 0x06, 0x00, 0x00, 0x00
};

extern unsigned char done;
extern unsigned char data_bytes[no_of_data_bytes_returned];

void setup(void);
void lcd_symbol(void);

void main(void)
{
  setup();

  while(1)
  {
    read_heading();

    if(done)
     {
       LCD_goto(6, 1);
       LCD_putchar(data_bytes[2]);

       LCD_goto(7, 1);
       LCD_putchar(data_bytes[3]);

       LCD_goto(8, 1);
       LCD_putchar(data_bytes[4]);

       LCD_goto(9, 1);
       LCD_putchar('.');
       LCD_goto(10, 1);
       LCD_putchar(data_bytes[6]);

       done = 0;
     }

     P15 = ~P15;
     delay_ms(200);
  };
}

void setup(void)
{
  P15_PushPull_Mode;

  LCD_init();
  LCD_clear_home();
  lcd_symbol();
  LCD_goto(3, 0);
  LCD_putstr("Heading  N");
  LCD_goto(11, 0);
  LCD_send(0, DAT);

  InitialUART1_Timer3(9600);
}

void lcd_symbol(void) 
{
  unsigned char s = 0; 

  LCD_send(0x40, CMD);

  for(s = 0; s < 8; s++)
  {
   LCD_send(symbol[s], DAT);
  }

  LCD_send(0x80, CMD);
}

 

Schematic

UART Schematic

Explanation

The following are the commands that HMC1022 acknowledges when sent via UART:

HMC1022 Commands

HMC1022 gives heading data when it is requested. The highlighted command is that command.

Once requested it sends out a string of characters that carry heading information. The followings are the response package and its details:

HMC1022 Response

From the above info, it is clear that HMC1022 will return 8 bytes when requested for heading. Out of these we need bytes 2, 3,4 and 6. The rest can be ignored for simplicity.

Data from HM1022 is received by the following function:

void read_heading(void)
{                        
     unsigned char s = 0; 
     unsigned long CRC = 0;

     Send_Data_To_UART1(Get_Angular_Measurement); 

     for(s = 0; s < no_of_data_bytes_returned; s++)
     {               
        data_bytes[s] = Receive_Data_From_UART1();
        if(s < (no_of_data_bytes_returned - 1))  
        {
            CRC += data_bytes[s];
        }
     }                                                      

     CRC = (CRC & 0xFF);

     if(CRC == data_bytes[7])
     {                                    
       done = 1;
     }    
}

Note that the function Send_Data_To_UART1 and Receive_Data_From_UART1 are BSP functions. Likewise, InitialUART1_Timer3(9600) function is also a BSP function that initiates UART1 with Timer 3. As with ADC, BSP functions for UART are enough for basic setup. Try to use Timer 3 as it remains mostly free. If you need more control over the UART, you can manually configure the registers.

In the main, the received bytes containing heading info are displayed.

 

Demo

UART

UART Interrupt

UART, as we saw in the last example, can be used in polling mode but it is wise to use it in interrupt mode. This feature becomes especially important and therefore, a must-have requirement when it come to that fact that the host micro may not always know when to receive data. Unlike SPI/I2C, UART doesn’t always follow known data transactions. Take the example of a GSM modem. We and so does our tiny host N76E003 micro don’t know for sure when a message would arrive. Thus, when an interrupt due to reception is triggered, it is known to mark the beginning of data reception process. The host micro can relax idle until data is received fully.

Here, we will see how to use UART interrupt to read a LV-Max EZ0 SONAR module.

LV-Max Ez0

Code

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "soft_delay.h"
#include "LCD_2_Wire.h"

unsigned char received = 0;
unsigned char count = 0;
unsigned char buffer[5] = {0x00, 0x00, 0x00, 0x00, 0x00};

void setup(void);

void UART0_ISR(void)    
interrupt 4 
{
    if(RI == 1)
    {                                                               
        buffer[count] = SBUF;
        count++;

        if(count >= 4)
        {
            clr_ES;
            received = 1;
        }

        clr_RI;
    }

    P15 = ~P15;
}

void main(void)
{
    unsigned char i = 0;

    setup();

    while(1)
    {
        Send_Data_To_UART0('S');
        delay_ms(40);
        set_ES;     

        if(received)
        {

            for(i = 1; i < 4; i++)
            {
              LCD_goto((12 + i), 1);
              LCD_putchar(buffer[i]);
            }

            count = 0;
            received  = 0;
            set_ES;
        }

        delay_ms(40);
    }
}

void setup(void)
{
    P15_PushPull_Mode;

    LCD_init();
    LCD_clear_home();

    LCD_goto(1, 0);
    LCD_putstr("UART Interrupt");
    LCD_goto(0, 1);
    LCD_putstr("Range/Inch:");

    InitialUART0_Timer3(9600);
    set_EA;
}

 

Schematic

UART INT Schematic

*Note the LV-Max EZ0 SONAR sensor is not directly connected with the N76E003 as shown in the schematic but rather it is connected via a 74HC04 hex inverter. Two of these inverters are used – one for the TX side and the other for the RX side.

 

Explanation

This time UART0 is used and is setup using BSP built-in function. The only exception is the interrupt part. The global interrupt is enabled but not the UART interrupt.

InitialUART0_Timer3(9600);
set_EA;

The UART interrupt is only enabled after commanding the SONAR sensor:

Send_Data_To_UART0('S');
delay_ms(40);
set_ES;  

This is done to avoid unnecessary reads.

The sensor is read inside the interrupt routine. The sensor outputs data as Rxxx<CR>. So, there are 5 bytes to read. The “R” in the readout works as a preamble or sync byte. The “xxx” part contains range info in inches. Finally, <CR> is a carriage return character. Therefore, every time a character is received an interrupt is triggered and the character is saved in an array. When all 5 bytes have been received, the display is updated. On exiting the interrupt, the interrupt flag is cleared. P15 is also toggled to visually indicate that an UART interrupt has been triggered.

void UART0_ISR(void)    
interrupt 4 
{
  if(RI == 1)
  {                                                               
     buffer[count] = SBUF;
     count++;

 

     if(count >= 4)
     {
       clr_ES;
       received = 1;
     }

 

     clr_RI;
  }

 

  P15 = ~P15;
}

Demo

Lv-Max Ez0

Inter-Integrated Circuit (I2C) – Interfacing DS1307 I2C RTC

Developed by Philips nearly three decades ago, I2C communication (Inter-Integrated Circuit) is widely used in a number of devices and is comparatively easier than SPI. For I2C communication only two wires – serial data (SDA) and serial clock (SCK) are needed and these two wires form a bus in which we can connect up to 127 devices.

I2C

Everything you need to know about I2C can be found in these pages:

Apart from these N76E003’s datasheet explains I2C communication in high detail.

Code

 

I2C.h

#define regular_I2C_pins              0
#define alternate_I2C_pins            1

#define regular_I2C_GPIOs()           do{P13_OpenDrain_Mode; P14_OpenDrain_Mode; clr_I2CPX;}while(0)

#define alternative_I2C_GPIOs()       do{P02_OpenDrain_Mode; P16_OpenDrain_Mode; set_I2CPX;}while(0)

#define I2C_GPIO_Init(mode)           do{if(mode != 0){alternative_I2C_GPIOs();}else{regular_I2C_GPIOs();}}while(0)

#define I2C_CLOCK                     0x27 //Fclk = Fsys / (4*(prescalar + 1))

#define I2C_ACK                       0
#define I2C_NACK                      1

#define timeout_count                 1000

void I2C_init(void);
void I2C_start(void);
void I2C_stop(void);
unsigned char I2C_read(unsigned char ack_mode);
void I2C_write(unsigned char value);

 

I2C.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "I2C.h"

void I2C_init(void)
{
    I2C_GPIO_Init(regular_I2C_pins);
    I2CLK = I2C_CLOCK;
    set_I2CEN;
}

void I2C_start(void)
{
    signed int t = timeout_count;

    set_STA;                               
    clr_SI;
    while((SI == 0) && (t > 0))
    {
       t--;
    };    
}

void I2C_stop(void)
{
    signed int t = timeout_count;

    clr_SI;
    set_STO;
    while((STO == 1) && (t > 0))
    {
        t--;
    };    
}

unsigned char I2C_read(unsigned char ack_mode)
{
    signed int t = timeout_count;
    unsigned char value = 0x00;

    set_AA;                            
    clr_SI;
    while((SI == 0) && (t > 0))
    {
        t--;
    };      

    value = I2DAT;

    if(ack_mode == I2C_NACK)
    {
        t = timeout_count;
        clr_AA;  
        clr_SI;
        while((SI == 0) && (t > 0))
        {
            t--;
        };          
    }

    return value;
}

void I2C_write(unsigned char value)
{
    signed int t = timeout_count;

    I2DAT = value;
    clr_STA;          
    clr_SI;
    while((SI == 0) && (t > 0))
    {
        t--;
    }; 
}

 

DS1307.h

#define I2C_W                            0
#define I2C_R                            1

#define sec_reg                          0x00
#define min_reg                          0x01
#define hr_reg                           0x02
#define day_reg                          0x03
#define date_reg                         0x04
#define month_reg                        0x05
#define year_reg                         0x06
#define control_reg                      0x07

#define DS1307_addr                      0xD0
#define DS1307_WR                        (DS1307_addr + I2C_W)
#define DS1307_RD                        (DS1307_addr + I2C_R)

void DS1307_init(void);
unsigned char DS1307_read(unsigned char address);
void DS1307_write(unsigned char address, unsigned char value);
unsigned char bcd_to_decimal(unsigned char value);
unsigned char decimal_to_bcd(unsigned char value);
void get_time(void);
void set_time(void);

 

DS1307.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "DS1307.h"
#include "I2C.h"

struct
{
   unsigned char s;
   unsigned char m;
   unsigned char h;
   unsigned char dy;
   unsigned char dt;
   unsigned char mt;
   unsigned char yr;
}time;

void DS1307_init(void)
{
    I2C_init();
  DS1307_write(control_reg, 0x00);
}

unsigned char DS1307_read(unsigned char address)
{
    unsigned char value = 0x00;

    I2C_start();
    I2C_write(DS1307_WR);
    I2C_write(address);

    I2C_start();
    I2C_write(DS1307_RD);
    value = I2C_read(I2C_NACK);
    I2C_stop();

    return value;
}

void DS1307_write(unsigned char address, unsigned char value)
{
    I2C_start();
    I2C_write(DS1307_WR);
    I2C_write(address);
    I2C_write(value);
    I2C_stop();
}

unsigned char bcd_to_decimal(unsigned char value)
{
    return ((value & 0x0F) + (((value & 0xF0) >> 0x04) * 0x0A));
}

unsigned char decimal_to_bcd(unsigned char value)
{
    return (((value / 0x0A) << 0x04) & 0xF0) | ((value % 0x0A) & 0x0F);
}

void get_time(void)
{
    time.s = DS1307_read(sec_reg);
    time.s = bcd_to_decimal(time.s);

    time.m = DS1307_read(min_reg);
    time.m = bcd_to_decimal(time.m);

    time.h = DS1307_read(hr_reg);
    time.h = bcd_to_decimal(time.h);

    time.dy = DS1307_read(day_reg);
    time.dy = bcd_to_decimal(time.dy);

    time.dt = DS1307_read(date_reg);
    time.dt = bcd_to_decimal(time.dt);

    time.mt = DS1307_read(month_reg);
    time.mt = bcd_to_decimal(time.mt);

    time.yr = DS1307_read(year_reg);
    time.yr = bcd_to_decimal(time.yr);
}

void set_time(void)
{
    time.s = decimal_to_bcd(time.s);
    DS1307_write(sec_reg, time.s);

    time.m = decimal_to_bcd(time.m);
    DS1307_write(min_reg, time.m);

    time.h = decimal_to_bcd(time.h);
    DS1307_write(hr_reg, time.h);

    time.dy = decimal_to_bcd(time.dy);
    DS1307_write(day_reg, time.dy);

    time.dt = decimal_to_bcd(time.dt);
    DS1307_write(date_reg, time.dt);

    time.mt = decimal_to_bcd(time.mt);
    DS1307_write(month_reg, time.mt);

    time.yr = decimal_to_bcd(time.yr);
    DS1307_write(year_reg, time.yr);
}

 

main.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "soft_delay.h"
#include "LCD_3_Wire.h"
#include "I2C.h"
#include "DS1307.h"

extern struct
{
   unsigned char s;
   unsigned char m;
   unsigned char h;
   unsigned char dy;
   unsigned char dt;
   unsigned char mt;
   unsigned char yr;
}time;

void show_value(unsigned char x_pos, unsigned char y_pos, unsigned char value);
void display_time(void);

void main(void)
{
    time.s = 30;
    time.m = 58;
    time.h = 23;

    P15_PushPull_Mode;

    LCD_init();
    LCD_clear_home();

    LCD_goto(0, 0);

    LCD_putstr("N76E003 I2C RTCC");

    DS1307_init();
    set_time();

    while(1)
    {
        get_time();
        display_time();
    };
}

void show_value(unsigned char x_pos, unsigned char y_pos, unsigned char value)
{
    unsigned char chr = 0;

    chr = ((value / 10) + 0x30);
    LCD_goto(x_pos, y_pos);
    LCD_putchar(chr);

    chr = ((value % 10) + 0x30);
    LCD_goto((x_pos + 1), y_pos);
    LCD_putchar(chr);
}

void display_time(void)
{
    P15 ^= 1;
    LCD_goto(6, 1);
    LCD_putchar(' ');
    LCD_goto(9, 1);
    LCD_putchar(' ');
    delay_ms(400);

    show_value(4, 1, time.h);
    show_value(7, 1, time.m);
    show_value(10, 1, time.s);

    LCD_goto(6, 1);
    LCD_putchar(':');
    LCD_goto(9, 1);
    LCD_putchar(':');
    delay_ms(400);
}

 

Schematic

I2C_Schematic

*Note that the pin naming of the chip in the schematic above is wrong. P13 and P14 are both SDA pins according to this naming when actually P13 is SCL and P14 is SDA pin. 

Explanation

I have coded two files for I2C easy and quick I2C implementation. These are I2C header and source file. These files describe I2C hardware functionality as well as provide higher level functions under the hood of which I2C hardware is manipulated. I have only coded these files for master mode only since barely we would ever need N76E003 slaves. I also didn’t implement interrupt-based I2C functionalities as this, in my experience, is also not needed in most of the cases.

void I2C_init(void);
void I2C_start(void);
void I2C_stop(void);
unsigned char I2C_read(unsigned char ack_mode);
void I2C_write(unsigned char value);

Using hardware I2C requires us to manipulate the following registers apart from clock and GPIO settings.

I2C Registers

I2C GPIOs must be set as open-drain GPIOs as per I2C standard. I2C clock must be set according to the devices connected in the bus. Typically, I2C clock speed ranges from 20 – 400kHz. This clock speed is dependent on system clock speed Fsys. Therefore, before setting I2C clock speed you have to set/know Fsys. I2C clock is decided by the value of I2CLK register. It is basically a prescalar value in master mode.

I2C

So, if Fsys = 16MHz and I2CLK = 39 (0x27), the I2C bus clock rate is 100kHz.

Note that only in master I2C clock can be setup. In slave mode, the slave(s) synchronizes automatically with bus clock.

I2CON register is they key register for using I2C. It contains all the flags and condition-makers.

For instance, consider the I2C start condition. In order to make a start condition, we have to set the STA bit and clear I2C interrupt flag, SI. Since I used polling mode, SI is polled until it is cleared. This ensures that a start condition has been successfully made. I have also added a software-based timeout feature should there be an issue with the I2C bus. In N76E003, there is a hardware-based approach for timeout too. Note that I didn’t care about the I2C status register states as again this is rarely needed. If you want more grip on the I2C you can follow the BSP examples.

void I2C_start(void)
{
    signed int t = timeout_count;

    set_STA;                               
    clr_SI;
    while((SI == 0) && (t > 0))
    {
       t--;
    };    
}

Finally, the last register that we will need is the I2C data register (I2DAT). It is through it we send and receive data from I2C bus.

The demo I have shown here is that of a DS1307 I2C-based real time clock. Perhaps this is the simplest device for learning about and understanding I2C.

Demo

I2C_pic

Serial Peripheral Interface (SPI) – Interfacing MAX7219 and MAX6675

SPI just like I2C is another highly popular form of onboard serial communication. Compared to I2C it is fast and robust. However, it requires more GPIO pins than other communication forms. These make it ideal communication interface for TFT displays, OLED displays, flash memories, DACs, etc.

SPI is best realized as a shift register that shifts data in and out with clock pulses. In a SPI bus, there is always one master device which generates clock and selects slave(s). Master sends commands to slave(s). Slave(s) responds to commands sent by the master. The number of slaves in a SPI bus is virtually unlimited. Except the chip selection pin, all SPI devices in a bus can share the same clock and data pins.

SPI Block Diagram

In general, if you wish to know more about SPI bus here are some cool links:

 

Code

 

MAX72xx.h

#define MAX72xx_SPI_GPIO_init()                             do{P00_PushPull_Mode; P10_PushPull_Mode; P11_PushPull_Mode;}while(0)

#define MAX72xx_CS_OUT_HIGH()                               set_P11
#define MAX72xx_CS_OUT_LOW()                                clr_P11

#define NOP                                                 0x00
#define DIG0                                                0x01
#define DIG1                                                0x02
#define DIG2                                                0x03
#define DIG3                                                0x04
#define DIG4                                                0x05
#define DIG5                                                0x06
#define DIG6                                                0x07
#define DIG7                                                0x08
#define decode_mode_reg                                     0x09
#define intensity_reg                                       0x0A
#define scan_limit_reg                                      0x0B
#define shutdown_reg                                        0x0C
#define display_test_reg                                    0x0F

#define shutdown_cmd                                        0x00
#define run_cmd                                             0x01

#define no_test_cmd                                         0x00
#define test_cmd                                            0x01

#define digit_0_only                                        0x00
#define digit_0_to_1                                        0x01
#define digit_0_to_2                                        0x02
#define digit_0_to_3                                        0x03
#define digit_0_to_4                                        0x04
#define digit_0_to_5                                        0x05
#define digit_0_to_6                                        0x06
#define digit_0_to_7                                        0x07

#define No_decode_for_all                                   0x00
#define Code_B_decode_digit_0                               0x01
#define Code_B_decode_digit_0_to_3                         0x0F    
#define Code_B_decode_for_all                               0xFF

void MAX72xx_SPI_HW_Init(unsigned char clk_value);
void MAX72xx_init(void);
void MAX72xx_write(unsigned char address, unsigned char value);

 

MAX72xx.c

#include "N76E003_IAR.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "MAX72xx.h"

void MAX72xx_SPI_HW_Init(unsigned char clk_value)
{
  switch(clk_value)
  {
    case 1:
    {
      clr_SPR1;
      set_SPR0;
      break;
    }
    case 2:
    {
      set_SPR1;
      clr_SPR0;
      break;
    }
    case 3:
    {
      set_SPR1;
      set_SPR0;
      break;
    }
    default:
    {
      clr_SPR1;
      clr_SPR0;
      break;
    }
  }   

  set_DISMODF;
  set_MSTR;
  clr_CPOL;
  clr_CPHA;
  set_SPIEN;
}

void MAX72xx_init(void)
{
  MAX72xx_SPI_GPIO_init();
  MAX72xx_SPI_HW_Init(0);

  MAX72xx_write(shutdown_reg, run_cmd);
  MAX72xx_write(decode_mode_reg, Code_B_decode_digit_0_to_3);
  MAX72xx_write(scan_limit_reg, digit_0_to_3);
  MAX72xx_write(intensity_reg, 0x19);
}

void MAX72xx_write(unsigned char address, unsigned char value)
{
  MAX72xx_CS_OUT_LOW(); 

  SPDR = address;
  while(!(SPSR & SET_BIT7)); 
  clr_SPIF;

  SPDR = value;
  while(!(SPSR & SET_BIT7)); 
  clr_SPIF;

  MAX72xx_CS_OUT_HIGH();
}

 

MAX6675.h

#define MAX6675_SPI_GPIO_init()     do{P01_Input_Mode; P10_PushPull_Mode; P12_PushPull_Mode;}while(0)

#define MAX6675_CS_OUT_HIGH()       set_P12
#define MAX6675_CS_OUT_LOW()        clr_P12

#define T_min                       0
#define T_max                       1024

#define count_max                   4096

#define no_of_pulses                16

#define deg_C                       0
#define deg_F                       1
#define tmp_K                       2

#define open_contact                0x04
#define close_contact               0x00

#define scalar_deg_C                0.25
#define scalar_deg_F_1              1.8
#define scalar_deg_F_2              32.0
#define scalar_tmp_K                273.0

#define no_of_samples               16

void MAX6675_SPI_HW_Init(unsigned char clk_value);
void MAX6675_init(void);
unsigned char MAX6675_get_ADC(unsigned int *ADC_data);
float MAX6675_get_T(unsigned int ADC_value, unsigned char T_unit);

 

MAX6675.c

#include "N76E003_IAR.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "MAX6675.h"

void MAX6675_SPI_HW_Init(unsigned char clk_value)
{
  switch(clk_value)
  {
    case 1:
    {
      clr_SPR1;
      set_SPR0;
      break;
    }
    case 2:
    {
      set_SPR1;
      clr_SPR0;
      break;
    }
    case 3:
    {
      set_SPR1;
      set_SPR0;
      break;
    }
    default:
    {
      clr_SPR1;
      clr_SPR0;
      break;
    }
  }   

  set_DISMODF;
  set_MSTR;
  clr_CPOL;
  set_CPHA;
  set_SPIEN;
}

void MAX6675_init(void)
{
    MAX6675_SPI_GPIO_init();
    MAX6675_SPI_HW_Init(0);
}

unsigned char MAX6675_get_ADC(unsigned int *ADC_data)
{
    unsigned char lb = 0;
    unsigned char hb = 0;
    unsigned char samples = no_of_samples;
    unsigned int temp_data = 0;
    unsigned long avg_value = 0;

    while(samples > 0)
    {
         MAX6675_CS_OUT_LOW();

         SPDR = 0x00;
         while(!(SPSR & SET_BIT7)); 
         hb = SPDR;
         clr_SPIF; 

         SPDR = 0x00;
         while(!(SPSR & SET_BIT7));         
         lb = SPDR;
         clr_SPIF;   

         MAX6675_CS_OUT_HIGH();

         temp_data = hb;
         temp_data <<= 8;
         temp_data |= lb;
         temp_data &= 0x7FFF;

         avg_value += (unsigned long)temp_data;

         samples--;
         Timer0_Delay1ms(10);
    };

    temp_data = (avg_value >> 4);

    if((temp_data & 0x04) == close_contact)
    {
        *ADC_data = (temp_data >> 3);
        return close_contact;
    }
    else
    {
        *ADC_data = (count_max + 1);
        return open_contact;
    }
}

float MAX6675_get_T(unsigned int ADC_value, unsigned char T_unit)
{
    float tmp = 0.0;

    tmp = (((float)ADC_value) * scalar_deg_C);

    switch(T_unit)
    {
        case deg_F:
        {
             tmp *= scalar_deg_F_1;
             tmp += scalar_deg_F_2;
             break;
        }
        case tmp_K:
        {
            tmp += scalar_tmp_K;
            break;
        }
        default:
        {
            break;
        }
    }

    return tmp;
}

 

main.c

#include "N76E003_IAR.h"
#include "Common.h"
#include "Delay.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "soft_delay.h"
#include "MAX72xx.h"
#include "MAX6675.h"

void main(void)
{
  unsigned int ti = 0;
  unsigned int t = 0;

  P15_PushPull_Mode;
  MAX6675_init();
  MAX72xx_init(); 

  while(1)
  {
      P15 = 1;
      clr_CPOL;
      set_CPHA;
      MAX6675_get_ADC(&ti);
      t = ((unsigned int)MAX6675_get_T(ti, tmp_K));
      delay_ms(100);

      P15 = 0;
      clr_CPOL;
      clr_CPHA;
      MAX72xx_write(DIG3, ((t / 1000) % 10));
      MAX72xx_write(DIG2, ((t / 100) % 10));
      MAX72xx_write(DIG1, ((t / 10) % 10));
      MAX72xx_write(DIG0, (t % 10));
      delay_ms(100);
  };
}

 

Schematic

SPI Schematic

Explanation

With SPI we have to deal with two things – first, the initialization and second, the data transfer process. Configuring SPI needs a few things to be set up. These are:

  • SPI communication bus speed or simply clock speed
  • Clock polarity
  • Clock phase
  • Orientation of data, i.e. MSB first or LSB first
  • Master/slave status
  • Optionally hardware slave selection pin use

Most of these can be done by manipulating the following registers:

SPI Registers

SPCR configures the hardware SPI peripheral of N76E003 while SPDR is used for transferring data.

To aid in all these I have coded the following functions. Though I didn’t use these code snippets in the actual demo code for demonstration purposes, these will most certainly work and reduce coding effort.

void SPI_init(unsigned char clk_speed, unsigned char mode)
{
  set_SPI_clock_rate(clk_speed);
  set_SPI_mode(mode);
  set_DISMODF;
  set_MSTR;
  set_SPIEN;
}



unsigned char SPI_transfer(unsigned char write_value)
{
  signed int t = timeout_count;
  register unsigned char read_value = 0x00;

 

  SPDR = write_value;
  while((!(SPSR & SET_BIT7)) && (t > 0))
  {
    t--;
  };        
  read_value = SPDR;
  clr_SPIF;   

 

  return read_value;
}

SPI_init function sets up the SPI peripheral using two information – the bus clock speed and SPI data transfer mode. It initializes the SPI hardware as a SPI master device and without hardware salve selection pin. This configuration is mostly used in common interfacings. Only mode and clock speed are the variables. In the demo, it is visible that MAX7219 and MAX6675 work with different SPI modes. I also didn’t use the hardware slave selection because there are two devices and one slave pin. Certainly, both external devices are not driven at the same time although both share the same bus lines.

SPI_transfer reads and writes on the SPI bus. Here the master writes to the slave first, waits for successful transmission and reads from the slave later. It is best realized by the following figure:

SPI-Loop

To demo SPI, MAX6675 and MAX7219 were used because one lacked read feature while the other lacked write feature. In the demo, MAX6675 is read to measure the temperature sensed by the K-type thermocouple attached with it and display the measured temperature in Kelvin on MAX7219-based seven segment display arrays.

Demo

SPI

One Wire (OW) Communication – Interfacing DHT11

One protocol is not a standard protocol like I2C or SPI. It is just a mere method of communicating with a device that uses just one wire. There are a few devices that uses such principle for communication. DHT series sensors, DS18B20s, digital serial number chips, etc. are a few to mention. As I stated before, since one wire communication doesn’t belong to a dedicated format of communication, there is no dedicated hardware for it. With just a bidirectional GPIO pin we can implement one wire communication. Optionally we can also use a timer for measuring response pulse widths since the method of sending ones and zeroes over one wire requires the measurement of pulse durations. This way of encoding ones and zeros as a function of pulse width is called time-slotting.

DHT11 Timing Diagram

Shown above is the timing diagram for DHT11’s time slots. Check the length of pulse high time for one and zero. This is the technique applied in time-slotting mechanism. We have to time pulse lengths in order to identify the logic states.

Code

 

DHT11.h

#define HIGH                    1
#define LOW                     0

#define DHT11_pin_init()        P05_Quasi_Mode      

#define DHT11_pin_HIGH()        set_P05
#define DHT11_pin_LOW()         clr_P05

#define DHT11_pin_IN()          P05             

void DHT11_init(void);
unsigned char get_byte(void);
unsigned char get_data(void);

 

DHT11.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "soft_delay.h"
#include "DHT11.h"

extern unsigned char values[5];

void DHT11_init(void)
{
   DHT11_pin_init();
   delay_ms(1000);
}

unsigned char get_byte(void)
{
   unsigned char s = 0;
   unsigned char value = 0;

   for(s = 0; s < 8; s++)
   {
      value <<= 1;
      while(DHT11_pin_IN() == LOW);
      delay_us(30);

      if(DHT11_pin_IN() == HIGH)
      {
          value |= 1;
      }

      while(DHT11_pin_IN() == HIGH);
   }
   return value;
}

unsigned char get_data(void)
{
   short chk = 0;
   unsigned char s = 0;
   unsigned char check_sum = 0;

   DHT11_pin_HIGH();
   DHT11_pin_LOW();
   delay_ms(18);
   DHT11_pin_HIGH();
   delay_us(26);

   chk = DHT11_pin_IN();

   if(chk)
   {
      return 1;
   }
   delay_us(80);

   chk = DHT11_pin_IN();

   if(!chk)
   {
      return 2;
   }
   delay_us(80);

   for(s = 0; s <= 4; s += 1)
   {
       values[s] = get_byte();
   }

   DHT11_pin_HIGH();

   for(s = 0; s < 4; s += 1)
   {
       check_sum += values[s];
   }

   if(check_sum != values[4])
   {
      return 3;
   }
   else
   {
      return 0;
   }
}

 

main.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "soft_delay.h"
#include "LCD_2_Wire.h"
#include "DHT11.h"

unsigned char values[5];

const unsigned char symbol[8] =
{
   0x00, 0x06, 0x09, 0x09, 0x06, 0x00, 0x00, 0x00
};

void setup(void);
void lcd_symbol(void);
void lcd_print(unsigned char x_pos, unsigned char y_pos, unsigned char value);

void main(void)
{
  unsigned char state = 0;

  setup();

  while(1)
  {
    state = get_data();

    switch(state)
    {
            case 1:
            {
            }
            case 2:
            {
                 LCD_clear_home();
                 LCD_putstr("No Sensor Found!");
                 break;
            }
            case 3:
            {
                 LCD_clear_home();
                 LCD_putstr("Checksum Error!");
                 break;
            }
            default:
            {
                 LCD_goto(0, 0);
                 LCD_putstr("R.H/ %:       ");

                 lcd_print(14, 0, values[0]);

                 LCD_goto(0, 1);
                 LCD_putstr("Tmp/");
                 LCD_goto(4, 1);
                 LCD_send(0, DAT);
                 LCD_goto(5, 1);
                 LCD_putstr("C:");

                 if((values[2] & 0x80) == 1)
                 {
                     LCD_goto(13, 1);
                     LCD_putstr("-");
                 }
                 else
                 {
                     LCD_goto(13, 1);
                     LCD_putstr(" ");
                 }

                 lcd_print(14, 1, values[2]);
                 break;
            }
    }

    delay_ms(1000);
  };
}

void setup(void)
{
  DHT11_init();

  LCD_init(); 
  LCD_clear_home();
  lcd_symbol();
}

void lcd_symbol(void) 
{
  unsigned char s = 0; 

  LCD_send(0x40, CMD);

  for(s = 0; s < 8; s++)
  {
   LCD_send(symbol[s], DAT);
  }

  LCD_send(0x80, CMD);
}

void lcd_print(unsigned char x_pos, unsigned char y_pos, unsigned char value)
{
 unsigned char chr = 0x00;

 chr = ((value / 10) + 0x30);
 LCD_goto(x_pos, y_pos);
 LCD_putchar(chr);

 chr = ((value % 10) + 0x30);
 LCD_goto((x_pos + 1), y_pos);
 LCD_putchar(chr);
}

 

Schematic

DH11 Schematic

Explanation

As stated earlier, OW basically needs two things – first, the control of a single GPIO pin and second, the measurement of time. I would like to highlight the most important part of the whole code here:

unsigned char get_byte(void)
{
   unsigned char s = 0;
   unsigned char value = 0;

 

   for(s = 0; s < 8; s++)
   {
      value <<= 1;
      while(DHT11_pin_IN() == LOW);
      delay_us(30);

 

      if(DHT11_pin_IN() == HIGH)
      {
          value |= 1;
      }

 

      while(DHT11_pin_IN() == HIGH);
   }
   return value;
}

From DHT11’s timing diagram, we know that a zero is define by a pulse of 26 – 28μs and a logic one is defined by a pulse of 70μs. It is here in the get_byte function we check the pulse coming from DHT11. The DHT11’s pin is polled as an input and after 30μs if the result of this polling is high then it is considered as a logic one or else it is considered otherwise. Once triggered, DHT11 will provide a 5-byte output. All of the bits in these 5 bytes are checked this way. Though it is not the best method to do so, it is easy and simple.

Demo

DHT11

One Wire (OW) Communication – Interfacing DS18B20

One wire communication, as stated previously, doesn’t have a defined protocol. Except for the time-slotting mechanism part, there is nothing in common across the devices using this form of communication. We have seen how to interface a DHT11 relative humidity and temperature sensor previously. In this segment, we will see how to interface a DS18B20 one-wire digital temperature sensor with N76E003. DS18B20 follows and uses a completely different set of rules for communication.

Code

 

one_wire.h

#define DS18B20_GPIO_init()             P06_OpenDrain_Mode

#define DS18B20_IN()                    P06

#define DS18B20_OUT_LOW()               clr_P06
#define DS18B20_OUT_HIGH()              set_P06

#define TRUE                            1
#define FALSE                           0

unsigned char onewire_reset(void);
void onewire_write_bit(unsigned char bit_value);
unsigned char onewire_read_bit(void);
void onewire_write(unsigned char value);   
unsigned char onewire_read(void);

 

one_wire.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "soft_delay.h"
#include "one_wire.h"
unsigned char onewire_reset(void) 
{                                        
     unsigned char res = FALSE;
     DS18B20_GPIO_init();
     DS18B20_OUT_LOW();
     delay_us(480);       
     DS18B20_OUT_HIGH();
     delay_us(60);       
     res = DS18B20_IN();
     delay_us(480);      
     return res;
}
void onewire_write_bit(unsigned char bit_value)
{
    DS18B20_OUT_LOW();
    if(bit_value)
    {       
        delay_us(104);
        DS18B20_OUT_HIGH();  
    }             
}    
unsigned char onewire_read_bit(void)       
{    
    DS18B20_OUT_LOW(); 
    DS18B20_OUT_HIGH(); 
  delay_us(15);
    return(DS18B20_IN());   
}
void onewire_write(unsigned char value)
{                   
     unsigned char s = 0;
     while(s < 8)   
     {                             
          if((value & (1 << s)))
          {
              DS18B20_OUT_LOW();
                nop;
              DS18B20_OUT_HIGH(); 
              delay_us(60);  
          }      
          else
          {
            DS18B20_OUT_LOW();           
              delay_us(60);          
              DS18B20_OUT_HIGH();  
              nop;
          }
          s++;
     }
}                                     
unsigned char onewire_read(void)
{
     unsigned char s = 0x00;
     unsigned char value = 0x00;
     while(s < 8)
     {
          DS18B20_OUT_LOW();
          nop;
          DS18B20_OUT_HIGH(); 
          if(DS18B20_IN()) 
          {                                     
              value |=  (1 << s);                         
          }       
          delay_us(60);
          s++;
     }    
     return value;
}

 

DS18B20.h

#include "one_wire.h" 

#define convert_T                       0x44
#define read_scratchpad                 0xBE           
#define write_scratchpad                0x4E
#define copy_scratchpad                 0x48  
#define recall_E2                       0xB8
#define read_power_supply               0xB4   
#define skip_ROM                        0xCC

#define resolution                      12

void DS18B20_init(void);
float DS18B20_get_temperature(void); 

 

DS18B20.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "soft_delay.h"
#include "DS18B20.h"

void DS18B20_init(void)                            
{                                      
    onewire_reset();
    delay_ms(100);
}             

float DS18B20_get_temperature(void)
{                                              
    unsigned char msb = 0x00;
    unsigned char lsb = 0x00;
    register float temp = 0.0; 

    onewire_reset();    
    onewire_write(skip_ROM);       
    onewire_write(convert_T);

    switch(resolution)  
    {                                                 
        case 12:
        {                                           
            delay_ms(750);
            break;
        }               
        case 11:                                    
        {             
            delay_ms(375);
            break;
        }          
        case 10:                            
        {                                
            delay_ms(188);  
            break;
        }                                       
        case 9:                                  
        {                                               
            delay_ms(94);                
            break;                           
        }                       
    }                 

    onewire_reset();

    onewire_write(skip_ROM);                
    onewire_write(read_scratchpad);

    lsb = onewire_read();
    msb = onewire_read();

    temp = msb;                          
    temp *= 256.0;
    temp += lsb;


    switch(resolution)  
    {                                 
        case 12:           
        {                                               
            temp *= 0.0625;                
            break;                           
        }       
        case 11:
        {          
            temp *= 0.125;      
            break;
        }               
        case 10:
        {           
            temp *= 0.25;       
            break;
        } 
        case 9:                                
        {                                
            temp *= 0.5;        
            break;     
        }                         
    }  

    delay_ms(40);       

    return (temp);      
}

 

main.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "soft_delay.h"
#include "DS18B20.h"
#include "LCD_3_Wire.h"

const unsigned char symbol[8] =
{
   0x00, 0x06, 0x09, 0x09, 0x06, 0x00, 0x00, 0x00
};

void lcd_symbol(void);
void print_C(unsigned char x_pos, unsigned char y_pos, signed int value);
void print_I(unsigned char x_pos, unsigned char y_pos, signed long value);
void print_D(unsigned char x_pos, unsigned char y_pos, signed int value, unsigned char points);
void print_F(unsigned char x_pos, unsigned char y_pos, float value, unsigned char points);

void main(void)
{
    float t = 0.0;

    DS18B20_init();
    LCD_init();
    lcd_symbol();

    LCD_goto(0, 0);
    LCD_putstr("N76E003 DS18B20");

    LCD_goto(0, 1);
    LCD_putstr("T/ C");
    LCD_goto(2, 1);
    LCD_send(0, DAT);

    while(1)
    {
       t = DS18B20_get_temperature();
       print_F(9, 1, t, 3);
       delay_ms(100);
    };
}

void lcd_symbol(void)
{
    unsigned char s = 0;

   LCD_send(0x40, CMD);

   for(s = 0; s < 8; s++)
   {
        LCD_send(symbol[s], DAT);
   }

   LCD_send(0x80, CMD);
}

void print_C(unsigned char x_pos, unsigned char y_pos, signed int value)
{
     char ch[5] = {0x20, 0x20, 0x20, 0x20, '\0'};

     if(value < 0x00)
     {
        ch[0] = 0x2D;
        value = -value;
     }
     else
     {
        ch[0] = 0x20;
     }

     if((value > 99) && (value <= 999))
     {
         ch[1] = ((value / 100) + 0x30);
         ch[2] = (((value % 100) / 10) + 0x30);
         ch[3] = ((value % 10) + 0x30);
     }
     else if((value > 9) && (value <= 99))
     {
         ch[1] = (((value % 100) / 10) + 0x30);
         ch[2] = ((value % 10) + 0x30);
         ch[3] = 0x20;
     }
     else if((value >= 0) && (value <= 9))
     {
         ch[1] = ((value % 10) + 0x30);
         ch[2] = 0x20;
         ch[3] = 0x20;
     }

     LCD_goto(x_pos, y_pos);
     LCD_putstr(ch);
}

void print_I(unsigned char x_pos, unsigned char y_pos, signed long value)
{
    char ch[7] = {0x20, 0x20, 0x20, 0x20, 0x20, 0x20, '\0'};

    if(value < 0)
    {
        ch[0] = 0x2D;
        value = -value;
    }
    else
    {
        ch[0] = 0x20;
    }

    if(value > 9999)
    {
        ch[1] = ((value / 10000) + 0x30);
        ch[2] = (((value % 10000)/ 1000) + 0x30);
        ch[3] = (((value % 1000) / 100) + 0x30);
        ch[4] = (((value % 100) / 10) + 0x30);
        ch[5] = ((value % 10) + 0x30);
    }

    else if((value > 999) && (value <= 9999))
    {
        ch[1] = (((value % 10000)/ 1000) + 0x30);
        ch[2] = (((value % 1000) / 100) + 0x30);
        ch[3] = (((value % 100) / 10) + 0x30);
        ch[4] = ((value % 10) + 0x30);
        ch[5] = 0x20;
    }
    else if((value > 99) && (value <= 999))
    {
        ch[1] = (((value % 1000) / 100) + 0x30);
        ch[2] = (((value % 100) / 10) + 0x30);
        ch[3] = ((value % 10) + 0x30);
        ch[4] = 0x20;
        ch[5] = 0x20;
    }
    else if((value > 9) && (value <= 99))
    {
        ch[1] = (((value % 100) / 10) + 0x30);
        ch[2] = ((value % 10) + 0x30);
        ch[3] = 0x20;
        ch[4] = 0x20;
        ch[5] = 0x20;
    }
    else
    {
        ch[1] = ((value % 10) + 0x30);
        ch[2] = 0x20;
        ch[3] = 0x20;
        ch[4] = 0x20;
        ch[5] = 0x20;
    }

    LCD_goto(x_pos, y_pos);
    LCD_putstr(ch);
}

void print_D(unsigned char x_pos, unsigned char y_pos, signed int value, unsigned char points)
{
    char ch[5] = {0x2E, 0x20, 0x20, '\0'};

    ch[1] = ((value / 100) + 0x30);

    if(points > 1)
    {
        ch[2] = (((value / 10) % 10) + 0x30);

        if(points > 1)
        {
            ch[3] = ((value % 10) + 0x30);
        }
    }

    LCD_goto(x_pos, y_pos);
    LCD_putstr(ch);
}

void print_F(unsigned char x_pos, unsigned char y_pos, float value, unsigned char points)
{
    signed long tmp = 0x0000;

    tmp = value;
    print_I(x_pos, y_pos, tmp);
    tmp = ((value - tmp) * 1000);

    if(tmp < 0)
    {
       tmp = -tmp;
    }

    if(value < 0)
    {
        value = -value;
        LCD_goto(x_pos, y_pos);
        LCD_putchar(0x2D);
    }
    else
    {
        LCD_goto(x_pos, y_pos);
        LCD_putchar(0x20);
    }

    if((value >= 10000) && (value < 100000))
    {
        print_D((x_pos + 6), y_pos, tmp, points);
    }
    else if((value >= 1000) && (value < 10000))
    {
        print_D((x_pos + 5), y_pos, tmp, points);
    }
    else if((value >= 100) && (value < 1000))
    {
        print_D((x_pos + 4), y_pos, tmp, points);
    }
    else if((value >= 10) && (value < 100))
    {
        print_D((x_pos + 3), y_pos, tmp, points);
    }
    else if(value < 10)
    {
        print_D((x_pos + 2), y_pos, tmp, points);
    }
}

 

Schematic

DS18B20 Schematic

Explanation

One wire communication is detailed in these application notes from Maxim:

https://www.maximintegrated.com/en/app-notes/index.mvp/id/126

https://www.maximintegrated.com/en/app-notes/index.mvp/id/162

These notes are all that are needed for implementing the one wire communication interface for DS18B20. Please go through these notes. The codes are self-explanatory and are implemented from the code examples in these app notes.

ds18b20


Decoding NEC IR Remote Protocol

IR remote controllers are used in a number of devices for remotely controlling them. Examples of these devices include TV, stereo, smart switches, projectors, etc. IR communication as such is unidirectional and in a way one wire communication. Ideally there is one transmitter and one receiver only.

IR Remote

Data to be sent from a remote transmitter is sent by modulating it with a carrier wave (usually between 36kHz to 40kHz). Modulation ensure safety from data corruption and long-distance transmission. An IR receiver at the receiving end receives and demodulates the sent out modulated data and outputs a stream of pulses. These pulses which vary in pulse widths/timing/position/phase carry the data information that was originally sent by the remote transmitter. How the pulses should behave is governed closely by a protocol or defined set of rules. In most micros, there is no dedicated hardware for decoding IR remote protocols. A micro that needs to decode IR remote data also doesn’t know when it will be receiving an IR data stream. Thus, a combination of external interrupt and timer is needed for decoding IR data.

NEC

In this segment, we will see how we can use an external interrupt and a timer to easily decode a NEC IR remote. This same method can be applied to any IR remote controller. At this stage, I recommend that you do a study of NEC IR protocol from here. SB-Project’s website is an excellent page for info as such.

Code

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "soft_delay.h"
#include "LCD_2_Wire.h"

#define sync_high          22000
#define sync_low           14000
#define one_high            3600
#define one_low             2400
#define zero_high           1800
#define zero_low            1200

bit received;
unsigned char bits = 0;
unsigned int frames[33];

void setup(void);
void set_Timer_0(unsigned int value);
unsigned int get_Timer_0(void);
void erase_frames(void);
unsigned char decode(unsigned char start_pos, unsigned char end_pos);
void decode_NEC(unsigned char *addr, unsigned char *cmd);
void lcd_print(unsigned char x_pos, unsigned char y_pos, unsigned char value);

void EXTI0_ISR(void)
interrupt 0
{
  frames[bits] = get_Timer_0();
  bits++;
  set_TR0;

  if(bits >= 33)
  {
     received = 1;
     clr_EA;
     clr_TR0;
  }
  set_Timer_0(0x0000);
  P15 = ~P15;
}

void main(void)
{
  unsigned char address = 0x00;
  unsigned char command = 0x00;

  setup();

  while(1)
  {   
     if(received)
     {
       decode_NEC(&address, &command);
       lcd_print(13, 0, address); 
       lcd_print(13, 1, command);
       delay_ms(100);
       erase_frames();
       set_EA;
     }
  };
}

void setup(void)
  erase_frames();
  P15_PushPull_Mode;  
  TIMER0_MODE1_ENABLE;          
  set_Timer_0(0x0000);
  set_IT0;
  set_EX0;  
  set_EA;

  LCD_init();
  LCD_clear_home();
  LCD_goto(0, 0);
  LCD_putstr("Address:");
  LCD_goto(0, 1);
  LCD_putstr("Command:");
}

void set_Timer_0(unsigned int value)
{
  TH0 = ((value && 0xFF00) >> 8);
  TL0 = (value & 0x00FF);
}

unsigned int get_Timer_0(void)
{
  unsigned int value = 0x0000;

  value = TH0;
  value <<= 8;
  value |= TL0;

  return value;
}

void erase_frames(void)
{
  for(bits = 0; bits < 33; bits++)
  {
    frames[bits] = 0x0000;
  }

  set_Timer_0(0x0000);
  received = 0;
  bits = 0;
}

unsigned char decode(unsigned char start_pos, unsigned char end_pos)
{
  unsigned char value = 0;

  for(bits = start_pos; bits <= end_pos; bits++)
  {
    value <<= 1;

    if((frames[bits] >= one_low) && (frames[bits] <= one_high))
    {
      value |= 1;
    }

    else if((frames[bits] >= zero_low) && (frames[bits] <= zero_high))
    {
      value |= 0;
    }

    else if((frames[bits] >= sync_low) && (frames[bits] <= sync_high))
    {
      return 0xFF;
    }
  }

  return value;
}

void decode_NEC(unsigned char *addr, unsigned char *cmd)
{
  *addr = decode(2, 9);
  *cmd = decode(18, 25);
}

void lcd_print(unsigned char x_pos, unsigned char y_pos, unsigned char value)
  LCD_goto(x_pos, y_pos);
  LCD_putchar((value / 100) + 0x30);
  LCD_goto((x_pos + 1), y_pos);
  LCD_putchar(((value % 10) / 10) + 0x30);
  LCD_goto((x_pos + 2), y_pos);
  LCD_putchar((value % 10) + 0x30);
}

 

Schematic

IR Schematic

Explanation

A NEC transmission sends out 33 pulses. The first one is a sync bit and the rest 32 contain address and command info. These 32 address and command bits can be divided into 4 groups – address, command, inverted address and inverted command. The first 16 bits contain address and inverted address while the rest contain command and inverted command. The inverted signals can be used against the non-inverted ones for checking signal integrity.

We already know that IR data is received as a steam of pulses. Pulses represent sync bit or other info, ones and zeros. In case of NEC IR protocol, the pulses have variable lengths. Sync bit represented by a pulse of 9ms high and 4.5ms low – total pulse time is 13.5ms. Check the timing diagram below. Please note that in this timing diagram the blue pulses are from the transmitter’s output and the yellow ones are those received by the receiver. Clearly the pulses are inverted.

NEC Sync

Logic one is represented by a pulse of 560µs high time and 2.25ms of low time – total pulse time is 2.81ms. Likewise, logic zero is represented by a pulse of 560µs high time and 1.12ms of low time – total pulse time is 1.68ms.

NEC High Low

These timings can vary slightly about 15 – 25% due to a number of factors like medium, temperature, etc. Therefore, we can assume the following timings:

IR

Now as per timing info and timing diagram we have to detect falling edges and time how long it takes to detect another falling edge. In this way, we can time the received pulses and use the pulse time info to decode a received signal.

In the demo, I used external interrupt channel EXTI0 and Timer 0. The system clock frequency is set to 16MHz with HIRC. Timer 0 is set in Mode 1 and its counter is rest to 0 count. EXTI0 is set to detect falling edges. Note that the timer is set but not started.

TIMER0_MODE1_ENABLE;          
set_Timer_0(0x0000);
set_IT0;
set_EX0;  
set_EA;

Also note that each tick of Timer 0 here is about:

NEC

Based on this tick info, we can deduce the maximum and minimum pulse durations as like:

#define sync_high          22000 // 22000 × 0.75ms = 16.5ms
#define sync_low           14000 // 14000 × 0.75ms = 10.5ms
#define one_high            3600 // 3600 × 0.75ms = 2.7ms
#define one_low             2400 // 2400 × 0.75ms = 1.8ms
#define zero_high           1800 // 1800 × 0.75ms = 1.35ms
#define zero_low            1200 // 1200 × 0.75ms = 0.9ms

Now when an IR transmission is received, an interrupt will be triggered. Therefore, in the ISR, we immediately capture the timer’s tick count and reset it. When all 33 pulses have been received this way, we temporarily halt all interrupts by disabling the global interrupt and stop the timer in order to decode the received signal. P15 is also toggled to demonstrate reception.

void EXTI0_ISR(void)
interrupt 0
{
  frames[bits] = get_Timer_0();
  bits++;
  set_TR0;

  if(bits >= 33)
  {
     received = 1;
     clr_EA;
     clr_TR0;
  }
  set_Timer_0(0x0000);
  P15 = ~P15;
}

Now the signal is decoded in the decode function. This function scans a fixed length of time frames for sync, ones and zeros and return the value obtained by scanning the time info of the pulses. In this way, a received signal is decoded.

unsigned char decode(unsigned char start_pos, unsigned char end_pos)
{
  unsigned char value = 0;

  for(bits = start_pos; bits <= end_pos; bits++)
  {
    value <<= 1;

    if((frames[bits] >= one_low) && (frames[bits] <= one_high))
    {
      value |= 1;
    }

    else if((frames[bits] >= zero_low) && (frames[bits] <= zero_high))
    {
      value |= 0;
    }

    else if((frames[bits] >= sync_low) && (frames[bits] <= sync_high))
    {
      return 0xFF;
    }
  }

 

  return value;
}

In the main loop, the decode info are displayed on a text LCD.

Demo

NEC

Epilogue

After going through all these topics and exploring the N76E003 to the finest details, I have to say that just like flagship killer cell phones, this is a flagship killer 8-bit microcontroller. In terms of price vs feature ratio, it is a winner. I’m truly impressed by its performance and hardware features. It offers something like an 8051 but with more advanced modern features. Old-school 8051 users will surely love to play with it.

Every day we talk about software piracy and hacking but little effort is done to prevent them. Students, hobbyists and low-profile business houses can’t afford to use the expensive Keil/IAR compiler. They often go for pirated versions of these software and this is a very wrong path to follow. Like I said in my first post on N76E003, Nuvoton has rooms for making its devices more popular by investing on a compiler of its own or at least start doing something with free Eclipse-IDE.

PDF version of this post can be found here.

All files and code examples related to Keil compiler can be downloaded from here.

All files and code examples related to IAR compiler can be downloaded from here.

Youtube playlist.

Happy coding.

 

Author: Shawon M. Shahryiar

https://www.youtube.com/user/sshahryiar

https://www.facebook.com/groups/microarena

https://www.facebook.com/MicroArena                                                                         

06.08.2018

The post Getting Started with Nuvoton 8-bit Microcontrollers – Coding Part 3 appeared first on Embedded Lab.


Exploring STC 8051 Microcontrollers

$
0
0

8051 microcontrollers are the first-generation microcontrollers that sparked the modern embedded-system era and established the basic concepts for almost all microcontrollers. In the early 1980s, 8051 microcontrollers were first introduced by Intel. Later other manufacturers like Philips (NXP), Atmel (now Microchip), Silicon Labs, Maxim, etc took the 8051 architecture and introduced their variants of 8051s. Today there are hundreds of such companies which still manufactures this old school legendary micro. of them have even added more features like ADCs, communication peripherals like SPI and I2C, etc that were not originally incepted or integrated. There are even some manufacturers who produce micros under their naming convention/branding while maintaining the basic architecture. Recently I covered an article about Nuvoton N76E003 here. It is based on such ideas. STC (not to be confused with STMicroelectronics) is a Chinese semiconductor manufacturer that operates in the same way as Nuvoton. STC took the model of 8051 just like other manufacturers and upgraded it to new levels by implementing some vital upgrades, enhancements and additions. It also manufactures standard 8051s which are designed to fit in place of any other 8051s from any manufacturer. At present STC has several different variants of 8051s, ranging from standard 40 pin regular DIP 8051s to tiny 8-pin variants. Some are shown below.

STC8051s

STC 8051s vs Other 8051s

STC 8051s, as stated, offers additional hardware peripherals when compared to standard 8051s. There are some STC microcontrollers like STC89C52RC that are same as the standard ones while some others like STC8A8K64S4A12 are more robust with many advanced features. Some key differences between standard 8051s and STC micros are discussed below:

  • Packages / Sizes
    STC offers microcontrollers in various DIP and SMD IC packages. Thus, instead of using a 40-pin DIP package microcontroller to solve a problem that can be solved with an 8-pin SMD low cost microcontroller, we can avoid using a big microcontroller and thereby save valuable PCB space. Most STC 8051s are, by the way, 100% pin-compatible with other 8051s. This feature makes STC micros easy and viable replacements for devices that use standard 8051s.
  • Speed / Operating Frequency
    STC microcontrollers are relatively faster than common 8051s as they can operate at higher clock frequencies. For example, AT89S52 has a maximum operating frequency of 33MHz while STC89C52RC can be clocked with an 80MHz source.
  • Additional Hardware Peripherals
    Some STC microcontrollers have in-built ADC, EEPROM, watchdog timer, external interrupt pins and other peripherals. Some are even equipped with higher storage capacities. These are not available in typical 8051s.
  • Operating Voltage
    Most 8051 micros need 4.0 – 5.5V DC supply voltage. Some can operate with 3.3V supplies too. Same goes for STC micros. However, there are some STC micros that are designed to operate at yet lower voltage levels. STC offers low power MCUs that operate between 2.0 – 3.6V and general-purpose micros that can operate between 3.6 – 5.5V. The operating voltage ranges and low power consumption figures of STC micros make them well-suited for battery and solar-powered devices.
  • Programming Interface
    Most 8051s require a parallel port programmer while some require serial port programmer or separate dedicated programmer hardware. STC micros on the other hand can be programmer with a serial port programmer and so there is no need to buy a dedicated programmer. A simple USB-TTL serial converter can be used to load codes into STC micros.
  • Other Minor Differences
    Other areas of differences include added/reduced functionalities/features. In some STC micros, there additional options for GPIOs, timers, etc while in some other devices these extras are not observed. For example, in STC89C52RC, there is 13-bit timer mode for timers 0 and 1 but this feature is absent in STC15L204EA. Likewise in STC15L204EA, there are many ways for setting up GPIOs which are not present in STC89C52RC.

 

Documentations and Websites

STC microcontrollers are popular in China and Chinese-speaking countries. Owing to this fact, most of the documentation and even the websites are in Chinese. It is hard to get English documentations. Fortunately, we will not be needing anything else other than device datasheets which are luckily available both in Chinese and English.

Unlike other manufacturers who maintain one website dedicated to their products and themselves, STC maintains several websites. Most are in Chinese. This creates lot of confusion about STC. Some common STC websites are listed below:

http://www.stcmicro.com

http://www.stcmcu.com

http://www.stcisp.com

Hardware Tools

From AliExpress, DX, Alibaba and other similar websites/stores, you can buy any STC development board of your choice. Alternatively, you can buy common STC chips and use them with your existing development board or setup a bread-board arrangement.

stc

One such board is shown above. These boards have lot of hardware devices like external 24 series EEPROM, I2C ADC-DAC, communication and display interfaces, etc already embedded and ready for go. Such boards are, thus easy to use and need less wiring. However, boards as such are relatively expensive and big than the one shown below:

nrf

This board is designed to bridge a serial interface between a host micro and a nRF24L01 2.4GHz wireless communication module. However, that doesn’t restrict us from using the STC15F(L)204EA micro embedded in it. If you just want to give STC micros a shot with very little investment then this sort of board is all that you can ever expect.

In my tutorials, I’ll be using both kinds of boards but the main focus will be towards STC15L204EA or similar slightly non-standard 8051s since they are not like playing with typical 8051s.

We will also need an USB-serial converter for uploading code.

USB-Serial Converter

Apart from these, some regularly used hardware items like LCDs, sensors, wires, etc will be needed. These can easily be found in any starter kit and most are available in any hobbyist’s collection.

new_starter_kit_waspmote_big

Software Tools

Only two software tools will be needed. The first is Keil C51 compiler and second STC ISP tool.

STC-ISP

STC ISP tool can be downloaded from here. This is one helluva tool that has many useful features. It a programmer interface, a code generator, serial port monitor, code bank and many other stuffs. It sure does make coding STC micros lot easier than you can possibly imagine.

Keil

Keil C51 C compiler will be needed to code STC micros. At present, Keil is the only C compiler that can be used reliably to code STC micros. STC documentations speak of Keil mostly. If you want to use some other compiler like IAR Embedded Workbench, MikroC for 8051, etc other than Keil, your have to add the SFR definitions of your target STC micros and do other stuffs to familiarize it with the STC micro target. Alternatively, you can use models of other similar 8051 model. For example, STC89C52RC is similar to AT89S52. You can use codes for such interchangeably. However, this method won’t work in cases where we have more hardware peripheral than an ordinary 8051 micro. STC15L204EA, for instance, can’t be used like ordinary 8051 or like STC89/90 series micros.

Programming STC Microcontrollers

By default, STC microcontroller database is absent in Keil. It is imperative that this database is added to Keil when using it for STC micros for the very first time. Though this database is not complete in the sense that not chips are enlisted in it, it is still a must or else we will have to use unconventional coding methods by using models of similar microcontrollers of different manufacturers. Personally, I hate unconventional tactics because why use such methods when we can add the database easily. We only have to add this database once.

First run the STC-ISP tool.

STC-ISP Icon

After clicking the STC-ISP tool icon, the application starts and the following window appears:

Garbage

Don’t worry. It is not an error or garbage text window. It appears so if you don’t have Chinese font database installed in your PC and so just click OK to continue.

Once the application starts, navigate to Keil ICE Settings tab and locate the highlighted button as shown below:

Keil-ICE

Now just navigate to Keil installation folder and hit OK as shown below:

Keil Folder

Selecting wrong folder will end up with an error and the database won’t be installed.

If the database addition is a success, you’ll get the following message:

Add Successful

Now run Keil C51 compiler.

Keil Icon

Go to Project >> New µVision Project… as shown below:

Keil New Project 1

Give your project a folder and a name as shown below:

Keil New Project 2

Select STC Database and appropriate chip or similar part number.

Keil Chip Selection

Please note that your target chip may not be in the list. For example, the L-series chips are not enlisted in the database and so they are absent in the list. STC15L204, for example, is not shown in the list. You can use STC15F204EA instead of it as they are similar stuffs. The only difference is their power consumptions. You have to use such tricks when your target chip is not listed. Make sure that the model you selected matches with the target chip or else things may not work properly.

After chip selection, add the startup assembler file to your project.

Keil Startup File

By default, Keil doesn’t add/create any file and so you’ll see that the project folder has no main source file. We’ll have to create one main source file and add additional files if needed.

Keil Main File

Still we are not ready to start coding. This is because we have not yet added SFR definition header file and other custom optional files.

Again, we need to take the help of STC-ISP tool.

 

In STC-ISP tool, locate the Header File tab and select appropriate MCU series.

Header File

Save or copy the file to your desired location.

In Keil, go to target options by right click the folder icon and set target options as shown in the following screenshots:

Keil Target Settings

Keil Target Settings 1

From this window select clock frequency and memory model.

Keil Target Settings 2

Select Create HEX File from this window as this file will be uploaded in the target chip.

Keil Target Settings 3

This section above is highly important because here we have to show the compiler the locations of the header or include files. Check the step numbering carefully.

Keil Target Settings 4

Lastly disable Warning Number 16. Now, we are good to code.

 

I made a small Youtube video on all the above discussed processes. If you didn’t understand some part or if you are confused at some point, you can watch the video here.

Now let us discuss about uploading codes to STC micros. The most advantageous part is the fact that we don’t need to invest on a dedicated programmer as with other microcontrollers as a simple USB-serial converter will do the job. However, on first go things may look confusing.

STC-ISP Steps

Shown above is the STC-ISP tool’s screenshot with numbers. We have to follow them one-by-one and in incremental order. 1 denotes that we must select the right COM port and part number. You can use Windows Device Manager to find out which COM port is being used for uploading code. Step 2 is to select the target HEX code file. Optionally in step 3, we can set some additional internal MCU parameters. When everything has been set properly, we can hit the Download Program button shown in step 4. If the target microcontroller or board is already powered then the code won’t be uploaded. This is the confusing part because it should have been the other way.

Notice the red arrow in the schematic below. The code is not uploaded by holding and releasing the reset button for some time or by pulling high or low some special pin or by some other means. We need to create a handshake between the PC and the target MCU and this is done when the MCU is powered off and then powered on, i.e. when the micro is powered up.

ISP Circuit

This is why the red arrow highlights the power switch in the schematic. Though the schematic shows a MAX232-based converter, we can use a USB-serial converter instead.

All of these steps are demoed in this video. Note that no external USB-serial converter/cable can be seen in the video as it is embedded in the board. The following schematic and photo will make this fact clearer. This is why most STC development boards come with such arrangement and without any programmer.

Cut Away Schematic board

The post Exploring STC 8051 Microcontrollers appeared first on Embedded Lab.

Getting Started with Nuvoton 8-bit Microcontrollers

$
0
0

Many of us who are involved in the embedded system industry like 8-bit microcontrollers. They are cheap, easy to use and solve most of the common automation problems. 32-bit micros are in that contrast expensive and are mainly intended for advance-level works that cannot be solved with 8-bit micros. Thus, 32-bit micros are not much demanding as the 8-bit ones. In most places, 8051s, AVRs and PICs are the most used 8-bit micros. The popular Arduino platform is mainly based on 8-bit AVR micros. However, these are not the only 8-bit micros of the whole embedded world. Nuvoton – a Taiwan-based semiconductor manufacturer, is one such company that has its own flavour of 8-bit and 32-bit micros. The 8-bit micros from Nuvoton are based on the popular 8051 architectures. In this series of articles, we will be discovering Nuvoton N76E003 1T-8051-based microcontroller.

SDK

N76E003 vs STM8S003

Hardware Internals

In terms of outlook, part numbering, pin layout and other basic features, N76E003 looks like a cheap Chinese copy of STMicroelectronics’ STM8S003. However, as we all know, looks can be deceptive. Though several similarities, N76E003 is not a replica of STM8S003 in any way. In fact, in terms of architecture and some hardware features, N76E003 varies a lot from STM8S003. For instance, the processor core of these chips is not same. Even after having same pin layouts, N76E003 has several additional hardware than STM8S003, for instance 8 channel – 12-bit ADC.

ST Vs Nu

The table below summarizes an actual comparison of these chips.

Features

It is a good idea to consider N76E003 as an advanced 8051 micro in STM8S003 form-factor and having several similarities with the STM8S003.

Hardware Tools

To get started only two things are needed – a prototyping/development board and a programmer called NuLink.  There are two options either you can buy the expensive official NuTiny SDK boards with built-in NULink programmer/debugger or you can buy the cheap unofficial ones as shown below.

programmer + board

My personal choice is the unofficial one.

 

Software Tools

Despite Nuvoton being a giant in manufacturing some of the industry’s coolest chips, it greatly lags in terms of software tools. Unlike other manufacturers like STMicroelectronics and Microchip, Nuvoton doesn’t have a free C compiler of its own. To use Nuvoton 8-bit micros, we have to use either Keil Micro Vision or IAR Embedded Workbench. Both industry-standard tools but are not free. However, there are trial and evaluation versions of these tools.

keil vs iar

The next stuffs that we will be needing are device datasheet, drivers, GUI for NuLink and sample codes with Nuvoton’s official header and source files. These are available here.

 

We will also be needing a pin map because there are few pins in N76E003 and they have multiple functions.

N76E003

How to get started?

I have made two videos – one for Keil and the other for IAR. These videos show in details how to start building projects with N76E003 using these compilers.

 

Nuvoton Files

No matter which compiler you use ultimately, you’ll need the following header (.h) and source files (.c) to get things done properly. These are available in Keil BSP and IAR BSP zip files.

Files

Now what these files do? These files make up something like a standard peripheral library. They define registers and functions that are needed to simplify coding. These files replace tradition register-level access to meaningful codes. For instance, the Delay files dictate the software delay functions by using hardware timers. Likewise, SFR_Macro and Function_Define files define hardware-based functions and SFR uses. I highly recommend going through the files.

The post Getting Started with Nuvoton 8-bit Microcontrollers appeared first on Embedded Lab.

Acme traffic light restoration

$
0
0

The Southern California Transportation Museum is one of the largest private transportation museums in the United States. We are privileged to have among our artifacts a set of Acme traffic lights. This type of traffic signal was deployed in the Los Angeles area in the 1920s and 1930s. This was the time when every city was experimenting with different types of traffic signals. Later the Automobile Club convinced everyone to adopt the three-light signal they use today. During the brief time they were in use, the ACME traffic light became the favorite of the Hollywood cartoonist so, that’s why you see them all over the movies. It’s the one with the arm and lights. The arms were used during the day since lights used electricity and electricity cost money. Then, at night the lights were used because, logically, you couldn’t see the arm.

The museum has one of the original ACME signal controllers – but it has a fatal design flaw, it thinks that it’s running a traffic signal. That means it will run it all day and night, wearing out the motors. So, the museum decided to replace the old controller with a new one consisting of a Raspberry Pi and a 16-channel relay module. The relay module was chosen because in the past it had provided reliable service in the museum’s signal garden. Also, because it is incredibly easy to program.

Acme controller

Acme traffic light controller

The Raspberry Pi and the relay board must work in a somewhat harsh environment. The museum moved to Perris, CA because the hot dry climate is good for the  artifacts. However, the climate is not good for the electronic parts. Most of the components sit in a cast iron box, painted black, with no ventilation, in a semi-desert. We are not certain of how hot it gets inside, but the 3d printed hinges we implemented in the box melted. Fortunately, all of the other parts are still working properly.

Now, you might be wondering why it takes so many relays to control one signal. The ACME traffic light is not a simple signal. There are arms, lights, and a bell. So, the relays are for:

  1. Red Light
  2. Green Light
  3. Yellow Light
  4. Motor power
  5. Motor direction
  6. Bell
  7. Fold (Keeps the arms from extending when returning them to the housing.)

And everything must be doubled because we have two signal heads.

Some additional relays were needed since the arm motor works by shorting two wires and applying power to a third. To change direction, you change which wires are shorted and which gets power. This would have been possible with two relays on the 16-channel board, but in this configuration, it would be possible for a software glitch to damage the motor. To ensure that would not happen we used a DPDT relay which provided a hardware interlock. Also, the motor, fold magnet, and bell all use big A/C coils. When these are turned off they generate a huge electric pulse. This does nasty things to the Raspberry Pi. (Funny enough the board has no problem.) To get around this problem three zero crossing solid state relays were added. By turning off the coils when no voltage is present you avoid the pulse problem.

Railroad equipment is designed to last a long time. The last railroad relay I used had an inspection sticker on it of 1910 (that’s when it was taken out of service, not when it was made). I don’t know if the new parts will last as long, but so far, the new parts have been very reliable in a challenging environment.

Watch the demo video below:

Author of the project: Eduardo Martinez

The post Acme traffic light restoration appeared first on Embedded Lab.

Getting Started with Nuvoton 8-bit Microcontrollers – Coding Part 1

$
0
0

This post is a continuation of the first post on Nuvoton N76E003 microcontroller here.

s-l1600

 

About the N76E006 Test Board

Shown below are the actual photo, the PCB layout and the schematic of the unofficial cheap N76E003 test/development board. This is the board I will be using in this tutorial series.

board

There is nothing much about the 30mm x 43.5mm board. Everything is visible and neat. However, having the schematic alongside the board layout in possession is a great advantage. There are two sidewise header that bring out the GPIO pins and positive supply rails. There is another header opposite to the USB port. This header is for connecting a Nulink programmer-debugger interface and it also has a serial port interface brought out straight from the N76E003 chip. This serial port is useful for quickly debugging/testing stuffs with a serial port monitor. There is an LED connected with P15 pin via a computer jumper. The only thing that is wrong in this board is the crystal resonator part. N76E003 has an external clock input pin but it is meant to be used with active oscillators/crystal modules. In the official, SDK there is no such points to connect an external crystal resonator.  The internal high frequency oscillator is accurate enough for most cases.

At this point, I would like to thank Electro Dragon for these images because they are the only ones who shared these resources online.

2018-02-12_235823

Shown below is the official SDK board’s schematic:

Official SDK Schematic

Coding Nuvoton N76E003

When entering a new environment, things are not very easy at first. It takes times to get acquainted with new the new environment, new tools and new stuffs. New means everything different from the ones that you have been using prior to its introduction. In many occasions, I had trouble playing with new microcontrollers due to this. However, after some decent play offs things unfolded themselves.

President JFK’s speech on lunar landing should be an inspirational note for everyone who is trying to do something new or something that he/she has never done before:

We choose to go to the Moon in this decade and do the other things, not because they are easy, but because they are hard; because that goal will serve to organize and measure the best of our energies and skills, because that challenge is one that we are willing to accept, one we are unwilling to postpone, and one we intend to win, and the others, too.

Both Keil and IAR are excellent tools for coding Nuvoton MCUs. I have used both and everything is same in both cases. Literally there is no difference at all. I won’t recommend which one to use and I leave the choice to the readers. However, there are some areas where you may find difficulty porting codes of one compiler to the other. The table below summarizes some of these differences.

keil vs iar

Two more things I would like to highlight here. Firstly, both Keil and IAR compiler can throw some errors during code compilations. Most of these errors are due to BSP definitions. One such error is in the line below:

error

If you try to use set_P0S_6 definition, IAR sometimes throws an error because it can’t find BIT_TMP. However, there are other similar definitions that don’t throw such error and in Keil you won’t notice something like this. Such things are nasty illogical surprises. We have to understand that the BSPs are still in development. Always remember that the datasheet is your friend. I suggest that when you try out the examples I have shown here, you read the relevant parts of the datasheet to enhance learning and understanding.

The other thing to note is the fact that not always we have the luxury to avoid register-level coding and so when needed we must have the right knowledge to use them. We can also use bit-level manipulations as shown below:

Coding

Sometimes but not always, we have to code things the old ways. Sometimes mixing assembly code with C code becomes a necessity. For instance, the software delay library uses this concept.

There are other aspects to consider too like case sensitivity and coding conventions. It is wise to choose interrupt-driven methods over polling-based ones. Codes should be included in hierarchical orders. Like such there are tons of stuffs to make your code smart and error-free. The best source of knowledge of such things and much more are app notes of various manufacturers.

Whenever making a new library, add the followings in your library’s source code along with other header files of your choice to avoid errors and nasty surprises:

Common Headers

Additionally, I have made a set of files called “Extended_Functions”. Here I added all the functions that we will need almost every time when we deal with common internal hardware like timers, ADC, etc. These files are like repositories of all the additional functions that I made for some internal hardware – something that Nuvoton didn’t provide and something that makes coding lot easier. I’ll share these and all the codes with the PDF of the tutorials after Part 2 of this tutorial series.

Here I’m sharing two videos to demonstrate how to code and add custom-made library files in both Keil C51 and IAR Embedded-Workbench compliers.


General Purpose Input-Output (GPIO)

GPIOs are the most common hardware that we use in a microcontroller. Since N76E003 is based on 8051 architecture, we should be getting some similarities with the old school 8051s. Shown below is the hardware schematic of N76E003’s GPIO block:

GPIO Structure

On close inspection, we can realize that this structure has striking resemblance with the GPIO structure of a typical 8051 microcontroller as shown below:

8051 GPIO

Thus, we can expect similar behaviour.

There are four GPIO modes and these are as follows:

GPIO types

PxM1.n and PxM2.n bits decide these modes. For most cases, we can stick to push-pull and input modes as they are the most commonly used ones.

Code

#include "N76E003_iar.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"

void setup(void);

void main(void)
  setup();

  while(1)
  {
    if(P05 != 0x00)
    {
      Timer0_Delay1ms(900);
    }

    set_P15;
    Timer0_Delay1ms(100);
    clr_P15;
    Timer0_Delay1ms(100);
  };
}

void setup(void)
  P15_PushPull_Mode;
  P05_Input_Mode;
}

Schematic

GPIO_Schematic

Explanation

The Function_define BSP header file states GPIO modes as follows:

GPIO_Def

Similarly, SFR_Macro BSP header file defines the bit-level setting of all N76E003 registers. To set the logic level of GPIO pins we can use the following definitions:

GPIO_Def2

However, these don’t restrict us from using classical register-level coding. N76E003 header file states all the registers present in it.

For port/pin reading I didn’t see any function definition as like one I already discussed. Thus, there are two ways to do it on your own. The following as two examples of such:

Coding 2

The demo here is a simple one. The onboard LED connected to P15 pin is toggled at a fixed interval. When a button connected to P05 is pressed the off time of the LED is increased, affecting toggle rate.

Demo

GPIO

Driving 2×16 LCD

Driving alphanumeric/text LCDs requires no special hardware as simple manipulation of GPIO pins and understanding of their working principle are all that are needed.

LCD

Code

 

lcd.h

#define LCD_GPIO_init()                    do{P00_PushPull_Mode; P01_PushPull_Mode; P10_PushPull_Mode; P11_PushPull_Mode; P12_PushPull_Mode; P13_PushPull_Mode;}while(0)

#define LCD_RS_HIGH                        set_P00
#define LCD_RS_LOW                         clr_P00

#define LCD_EN_HIGH                        set_P01
#define LCD_EN_LOW                         clr_P01

#define LCD_DB4_HIGH                       set_P10
#define LCD_DB4_LOW                        clr_P10

#define LCD_DB5_HIGH                       set_P11
#define LCD_DB5_LOW                        clr_P11

#define LCD_DB6_HIGH                       set_P12
#define LCD_DB6_LOW                        clr_P12

#define LCD_DB7_HIGH                       set_P13
#define LCD_DB7_LOW                        clr_P13

#define clear_display                      0x01
#define goto_home                          0x02

#define cursor_direction_inc               (0x04 | 0x02)
#define cursor_direction_dec               (0x04 | 0x00)
#define display_shift                      (0x04 | 0x01)
#define display_no_shift                   (0x04 | 0x00)

#define display_on                         (0x08 | 0x04)
#define display_off                        (0x08 | 0x02)
#define cursor_on                          (0x08 | 0x02)
#define cursor_off                         (0x08 | 0x00)
#define blink_on                           (0x08 | 0x01)
#define blink_off                          (0x08 | 0x00)

#define _8_pin_interface                   (0x20 | 0x10)
#define _4_pin_interface                   (0x20 | 0x00)
#define _2_row_display                     (0x20 | 0x08)
#define _1_row_display                     (0x20 | 0x00)
#define _5x10_dots                         (0x20 | 0x40)
#define _5x7_dots                          (0x20 | 0x00)

#define DAT                                1
#define CMD                                0

void LCD_init(void);
void LCD_send(unsigned char value, unsigned char mode);
void LCD_4bit_send(unsigned char lcd_data);
void LCD_putstr(char *lcd_string);
void LCD_putchar(char char_data);
void LCD_clear_home(void);
void LCD_goto(unsigned char x_pos, unsigned char y_pos);
void toggle_EN_pin(void);

 

lcd.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "lcd.h"

void LCD_init(void)
{
    Timer0_Delay1ms(10);

    LCD_GPIO_init();

    Timer0_Delay1ms(100);

    toggle_EN_pin();

    LCD_RS_LOW;

    LCD_DB7_LOW;
    LCD_DB6_LOW;
    LCD_DB5_HIGH;
    LCD_DB4_HIGH;

    toggle_EN_pin();

    LCD_DB7_LOW;
    LCD_DB6_LOW;
    LCD_DB5_HIGH;
    LCD_DB4_HIGH;

    toggle_EN_pin();

    LCD_DB7_LOW;
    LCD_DB6_LOW;
    LCD_DB5_HIGH;
    LCD_DB4_HIGH;

    toggle_EN_pin();

    LCD_DB7_LOW;
    LCD_DB6_LOW;
    LCD_DB5_HIGH;
    LCD_DB4_LOW;

    toggle_EN_pin();

    LCD_send((_4_pin_interface | _2_row_display | _5x7_dots), CMD);
    LCD_send((display_on | cursor_off | blink_off), CMD);
    LCD_send(clear_display, CMD);
    LCD_send((cursor_direction_inc | display_no_shift), CMD);
}

void LCD_send(unsigned char value, unsigned char mode)
{
    switch(mode)
    {
        case DAT:
        {
            LCD_RS_HIGH;
            break;
        }
        case CMD:
        {
            LCD_RS_LOW;
            break;
        }
    }

    LCD_4bit_send(value);
}

void LCD_4bit_send(unsigned char lcd_data)
{
    unsigned char temp = 0;

    temp = ((lcd_data & 0x80) >> 7);

    switch(temp)
    {
        case 1:
        {
            LCD_DB7_HIGH;
            break;
        }
        default:
        {
            LCD_DB7_LOW;
            break;
        }
    }

    temp = ((lcd_data & 0x40) >> 6);

    switch(temp)
    {
        case 1:
        {
            LCD_DB6_HIGH;
            break;
        }
        default:
        {
            LCD_DB6_LOW;
            break;
        }
    }

    temp = ((lcd_data & 0x20) >> 5);

    switch(temp)
    {
        case 1:
        {
            LCD_DB5_HIGH;
            break;
        }
        default:
        {
            LCD_DB5_LOW;
            break;
        }
    }

    temp = ((lcd_data & 0x10) >> 4);

    switch(temp)
    {
        case 1:
        {
            LCD_DB4_HIGH;
            break;
        }
        default:
        {
            LCD_DB4_LOW;
            break;
        }
    }

    toggle_EN_pin();

    temp = ((lcd_data & 0x08) >> 3);

    switch(temp)
    {
        case 1:
        {
            LCD_DB7_HIGH;
            break;
        }
        default:
        {
            LCD_DB7_LOW;
            break;
        }
    }

    temp = ((lcd_data & 0x04) >> 2);

    switch(temp)
    {
        case 1:
        {
            LCD_DB6_HIGH;
            break;
        }
        default:
        {
            LCD_DB6_LOW;
            break;
        }
    }

    temp = ((lcd_data & 0x02) >> 1);

    switch(temp)
    {
        case 1:
        {
            LCD_DB5_HIGH;
            break;
        }
        default:
        {
            LCD_DB5_LOW;
            break;
        }
    }

    temp = ((lcd_data & 0x01));

    switch(temp)
    {
        case 1:
        {
            LCD_DB4_HIGH;
            break;
        }
        default:
        {
            LCD_DB4_LOW;
            break;
        }
    }

    toggle_EN_pin();
}

void LCD_putstr(char *lcd_string)
{
    do
    {
        LCD_send(*lcd_string++, DAT);
    }while(*lcd_string != '\0');
}

void LCD_putchar(char char_data)
{
    LCD_send(char_data, DAT);
}

void LCD_clear_home(void)
{
    LCD_send(clear_display, CMD);
    LCD_send(goto_home, CMD);
}

void LCD_goto(unsigned char x_pos, unsigned char y_pos)
{
    if(y_pos == 0)
    {
        LCD_send((0x80 | x_pos), CMD);
    }
    else
    {
        LCD_send((0x80 | 0x40 | x_pos), CMD);
    }
}

void toggle_EN_pin(void)
{
    LCD_EN_HIGH;
    Timer0_Delay1ms(4);
    LCD_EN_LOW;
    Timer0_Delay1ms(4);
}

 

main.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "lcd.h"

void show_value(unsigned char value);

void main(void)
{   
    unsigned char s = 0;

    const char txt1[] = {"MICROARENA"};
    const char txt2[] = {"SShahryiar"};
    const char txt3[] = {"Nuvoton 8-bit uC"};
    const char txt4[] = {"N76E003"};

    LCD_init();

    LCD_clear_home();

    LCD_goto(3, 0);
    LCD_putstr(txt1);
    LCD_goto(3, 1);
    LCD_putstr(txt2);
    Timer3_Delay100ms(30);

    LCD_clear_home();

    for(s = 0; s < 16; s++)
    {
        LCD_goto(s, 0);
        LCD_putchar(txt3[s]);
        Timer0_Delay1ms(90);
    }

    Timer3_Delay100ms(20);

    for(s = 0; s < 7; s++)
    {
        LCD_goto((4 + s), 1);
        LCD_putchar(txt4[s]);
        Timer0_Delay1ms(90);
    }

    Timer3_Delay100ms(30);

    s = 0;
    LCD_clear_home();

    LCD_goto(3, 0);
    LCD_putstr(txt1);

    while(1)
    {
        show_value(s);
        s++;
        Timer3_Delay100ms(4);
    };
}

void show_value(unsigned char value)
{
   unsigned char ch = 0x00;

   ch = ((value / 100) + 0x30);
   LCD_goto(6, 1);
   LCD_putchar(ch);

   ch = (((value / 10) % 10) + 0x30);
   LCD_goto(7, 1);
   LCD_putchar(ch);

   ch = ((value % 10) + 0x30);
   LCD_goto(8, 1);
   LCD_putchar(ch);
}

 

Schematic

LCD Schematic

Explanation

There is nothing to explain here. The LCD driver is based on simple manipulation of GPIO pins. The codes for the LCD are coded using all available info on LCD datasheet – just initialization and working principle. If you need to change GPIO pins just edit the following lines in the LCD header file:

LCD Coding

Demo

2x16 LCD

Driving 2×16 LCD with Software SPI

One problem with alphanumeric LCDs and GLCDs is the number of GPIOs needed to connect so with host micros. For a small micro like N76E003, each GPIO pin is like a gem and we can’t afford to use too many GPIO pins for an LCD. The solution to this problem is to use SPI/I2C-based LCD drivers that significantly reduce GPIO pin requirement. Implementing software-based SPI/I2C for such LCD drivers is also both easy and universal since these solutions don’t need hardware SPI/I2C ports. Since the SPI/I2C functionality is software emulated, any set of GPIO pins can be used – another advantage.

SPI-LCD

In this segment, we will be driving a 2×16 LCD with CD4094B Serial-In-Parallel-Out (SIPO) shift register using software SPI. The same idea can be used for other similar shift registers like 74HC595. There are other ways of using SPI-based LCDs but the aforementioned are the cheapest ways.

Code

 

LCD_3_Wire.h

#define LCD_GPIO_init()                        do{P02_PushPull_Mode; P03_PushPull_Mode; P04_PushPull_Mode;}while(0)

#define LCD_SDO_HIGH()                          set_P00
#define LCD_SDO_LOW()                           clr_P00

#define LCD_SCK_HIGH()                          set_P01
#define LCD_SCK_LOW()                           clr_P01

#define LCD_STB_HIGH()                          set_P02
#define LCD_STB_LOW()                           clr_P02

#define DAT                                     1
#define CMD                                     0

#define clear_display                           0x01
#define goto_home                               0x02

#define cursor_direction_inc                    (0x04 | 0x02)
#define cursor_direction_dec                    (0x04 | 0x00)
#define display_shift                           (0x04 | 0x01)
#define display_no_shift                        (0x04 | 0x00)

#define display_on                              (0x08 | 0x04)
#define display_off                             (0x08 | 0x02)
#define cursor_on                               (0x08 | 0x02)
#define cursor_off                              (0x08 | 0x00)
#define blink_on                                (0x08 | 0x01)
#define blink_off                               (0x08 | 0x00)

#define _8_pin_interface                        (0x20 | 0x10)
#define _4_pin_interface                        (0x20 | 0x00)
#define _2_row_display                          (0x20 | 0x08)
#define _1_row_display                          (0x20 | 0x00)
#define _5x10_dots                              (0x20 | 0x40)
#define _5x7_dots                               (0x20 | 0x00)

extern unsigned char data_value;


void SIPO(void);
void LCD_init(void);
void LCD_toggle_EN(void);
void LCD_send(unsigned char value, unsigned char mode);
void LCD_4bit_send(unsigned char lcd_data);           
void LCD_putstr(char *lcd_string);
void LCD_putchar(char char_data);
void LCD_clear_home(void);
void LCD_goto(unsigned char x_pos, unsigned char y_pos);

 

LCD_3_Wire.c

#include "N76E003_iar.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "LCD_3_Wire.h"

unsigned char data_value;

void SIPO(void)
{
    unsigned char bit_value = 0x00;
    unsigned char clk = 0x08;
    unsigned char temp = 0x00;

    temp = data_value;
    LCD_STB_LOW();

    while(clk > 0)
    {
        bit_value = ((temp & 0x80) >> 0x07);
        bit_value &= 0x01;

        switch(bit_value)
        {
            case 0:
            {
                LCD_SDO_LOW();
                break;
            }
            default:
            {
                LCD_SDO_HIGH();
                break;
            }
        }

        LCD_SCK_HIGH();

        temp <<= 0x01;
        clk--;

        LCD_SCK_LOW();
    };

    LCD_STB_HIGH();
}

void LCD_init(void)
{                                     
    Timer0_Delay1ms(10);

    LCD_GPIO_init();

    Timer0_Delay1ms(10);

    data_value = 0x08;
    SIPO();
    Timer0_Delay1ms(10);

    LCD_send(0x33, CMD);
    LCD_send(0x32, CMD);

    LCD_send((_4_pin_interface | _2_row_display | _5x7_dots), CMD);        
    LCD_send((display_on | cursor_off | blink_off), CMD);    
    LCD_send((clear_display), CMD);        
    LCD_send((cursor_direction_inc | display_no_shift), CMD);       
}  

void LCD_toggle_EN(void)
{
    data_value |= 0x08;
    SIPO();
    Timer0_Delay1ms(2);
    data_value &= 0xF7;
    SIPO();
    Timer0_Delay1ms(2);
}


void LCD_send(unsigned char value, unsigned char mode)
{                              
    switch(mode)
    {
        case DAT:
        {
            data_value |= 0x04;
            break;
        }
        default:
        {
            data_value &= 0xFB;
            break;
        }
    }

    SIPO();
    LCD_4bit_send(value);


void LCD_4bit_send(unsigned char lcd_data)       
{
    unsigned char temp = 0x00;

    temp = (lcd_data & 0xF0);
    data_value &= 0x0F;
    data_value |= temp;
    SIPO();
    LCD_toggle_EN();

    temp = (lcd_data & 0x0F);
    temp <<= 0x04;
    data_value &= 0x0F;
    data_value |= temp;
    SIPO();
    LCD_toggle_EN();

void LCD_putstr(char *lcd_string)
{
    while(*lcd_string != '\0') 
    {
        LCD_putchar(*lcd_string++);
    }
}

void LCD_putchar(char char_data)
{
    if((char_data >= 0x20) && (char_data <= 0x7F))
    {
        LCD_send(char_data, DAT);
    }
}

void LCD_clear_home(void)
{
    LCD_send(clear_display, CMD);
    LCD_send(goto_home, CMD);
}

void LCD_goto(unsigned char x_pos,unsigned char y_pos)
{                                                  
    if(y_pos == 0)   
    {                             
        LCD_send((0x80 | x_pos), CMD);
    }
    else
    {                                             
        LCD_send((0x80 | 0x40 | x_pos), CMD);
    }
}

 

main.c

#include "N76E003_iar.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "LCD_3_Wire.h"

void show_value(unsigned char value);

void main(void)
  unsigned char s = 0;

  static char txt1[] = {"MICROARENA"};
  static char txt2[] = {"SShahryiar"};
  static char txt3[] = {"Nuvoton 8-bit uC"};
  static char txt4[] = {"N76E003"};

  LCD_init();

  LCD_clear_home();

  LCD_goto(3, 0);
  LCD_putstr(txt1);
  LCD_goto(3, 1);
  LCD_putstr(txt2);
  Timer3_Delay100ms(30);

  LCD_clear_home();

  for(s = 0; s < 16; s++)
  {
    LCD_goto(s, 0);
    LCD_putchar(txt3[s]);
    Timer0_Delay1ms(90);
  }

  Timer3_Delay100ms(20);

  for(s = 0; s < 7; s++)
  {
    LCD_goto((4 + s), 1);
    LCD_putchar(txt4[s]);
    Timer0_Delay1ms(90);
  }

  Timer3_Delay100ms(30);

  s = 0;
  LCD_clear_home();

  LCD_goto(3, 0);
  LCD_putstr(txt1);

  while(1)
  {
    show_value(s);
    s++;
    Timer3_Delay100ms(4);
  };
}

void show_value(unsigned char value)
{
   unsigned char ch = 0x00;

   ch = ((value / 100) + 0x30);
   LCD_goto(6, 1);
   LCD_putchar(ch);

   ch = (((value / 10) % 10) + 0x30);
   LCD_goto(7, 1);
   LCD_putchar(ch);

   ch = ((value % 10) + 0x30);
   LCD_goto(8, 1);
   LCD_putchar(ch);
}

 

Schematic

LCD Schematic

Explanation

The code demoed here is same as the last LCD code and so there is not much to explain. The GPIO operations of the LCD are handled using a CD4094B Serial-In-Parallel-Out (SIPO) shift register. This shift register here acts like an output expander. With just three GPIOs we are able to interface a 4-bit LCD that needs at least six GPIOs to work.

The SIPO function shown below simulates software-based SPI:

void SIPO(void)
{
    unsigned char bit_value = 0x00;
    unsigned char clk = 0x08;
    unsigned char temp = 0x00;

    temp = data_value;
    LCD_STB_LOW();

    while(clk > 0)
    {
        bit_value = ((temp & 0x80) >> 0x07);
        bit_value &= 0x01;

        switch(bit_value)
        {
            case 0:
            {
                LCD_SDO_LOW();
                break;
            }
            default:
            {
                LCD_SDO_HIGH();
                break;
            }
        }

        LCD_SCK_HIGH();

        temp <<= 0x01;
        clk--;

        LCD_SCK_LOW();
    };

    LCD_STB_HIGH();
}

To change pins, change the following the lines in the LCD_3_Wire header file:

#define LCD_GPIO_init()                        do{P02_PushPull_Mode; P03_PushPull_Mode; P04_PushPull_Mode;}while(0)

#define LCD_SDO_HIGH()                          set_P00
#define LCD_SDO_LOW()                           clr_P00

#define LCD_SCK_HIGH()                          set_P01
#define LCD_SCK_LOW()                           clr_P01

#define LCD_STB_HIGH()                          set_P02
#define LCD_STB_LOW()                           clr_P02

Lastly, I have code two versions of this LCD library – one with BSP-based delays and the other with software delays. Technically there’s no big change. The software-based one frees up a hardware timer or two.

Demo

SPI LCD

Driving 2×16 LCD with Software I2C

We have already seen in the last segment how to use software SPI with a shift register to drive a 2×16 LCD. In this segment, we will explore the same concept with software I2C and PCF8574 I2C port expander IC. There is a popular readymade module for such task and I used it here. The advantage of I2C-based LCD over SPI-based LCD driver is the lesser number of GPIOs required compared to SPI-based LCD. However, it is slower than SPI-based drivers.

2-Wire LCD

Code

 

SW_I2C.h

#define SDA_DIR_OUT()   P03_PushPull_Mode
#define SDA_DIR_IN()    P03_Input_Mode

#define SCL_DIR_OUT()   P04_PushPull_Mode
#define SCL_DIR_IN()    P04_Input_Mode

#define SDA_HIGH()      set_P03
#define SDA_LOW()       clr_P03

#define SCL_HIGH()      set_P04
#define SCL_LOW()       clr_P04

#define SDA_IN()        P03

#define I2C_ACK         0xFF
#define I2C_NACK        0x00

#define I2C_timeout     1000

void SW_I2C_init(void);
void SW_I2C_start(void);
void SW_I2C_stop(void);
unsigned char SW_I2C_read(unsigned char ack);
void SW_I2C_write(unsigned char value);
void SW_I2C_ACK_NACK(unsigned char mode);
unsigned char SW_I2C_wait_ACK(void);

 

SW_I2C.c

#include "N76E003_iar.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "SW_I2C.h"

void SW_I2C_init(void)
{
    SDA_DIR_OUT();
    SCL_DIR_OUT();
    Timer0_Delay100us(1);
    SDA_HIGH();
    SCL_HIGH();
}

void SW_I2C_start(void)
{
    SDA_DIR_OUT();
    SDA_HIGH();
    SCL_HIGH();
    Timer3_Delay10us(4);
    SDA_LOW();
    Timer3_Delay10us(4);
    SCL_LOW();
}

void SW_I2C_stop(void)
{
    SDA_DIR_OUT();
    SDA_LOW();
    SCL_LOW();
    Timer3_Delay10us(4);
    SDA_HIGH();
    SCL_HIGH();
    Timer3_Delay10us(4);
}

unsigned char SW_I2C_read(unsigned char ack)
{
    unsigned char i = 8;
    unsigned char j = 0;

    SDA_DIR_IN();

    while(i > 0)
    {
        SCL_LOW();
        Timer3_Delay10us(2);
        SCL_HIGH();
        Timer3_Delay10us(2);
        j <<= 1;

        if(SDA_IN() != 0x00)
        {
            j++;
        }

        Timer3_Delay10us(1);
        i--;
    };

    switch(ack)
    {
        case I2C_ACK:
        {
            SW_I2C_ACK_NACK(I2C_ACK);;
            break;
        }
        default:
        {
            SW_I2C_ACK_NACK(I2C_NACK);;
            break;
        }
    }

    return j;
}

void SW_I2C_write(unsigned char value)
{
    unsigned char i = 8;

    SDA_DIR_OUT();
    SCL_LOW();

    while(i > 0)
    {
        if(((value & 0x80) >> 7) != 0x00)
        {
            SDA_HIGH();
        }
        else
        {
            SDA_LOW();
        }

        value <<= 1;
        Timer3_Delay10us(2);
        SCL_HIGH();
        Timer3_Delay10us(2);
        SCL_LOW();
        Timer3_Delay10us(2);
        i--;
    };
}

void SW_I2C_ACK_NACK(unsigned char mode)
{
    SCL_LOW();
    SDA_DIR_OUT();

    switch(mode)
    {
        case I2C_ACK:
        {
            SDA_LOW();
            break;
        }
        default:
        {
            SDA_HIGH();
            break;
        }
    }

    Timer3_Delay10us(2);
    SCL_HIGH();
    Timer3_Delay10us(2);
    SCL_LOW();
}

unsigned char SW_I2C_wait_ACK(void)
{
    signed int timeout = 0;

    SDA_DIR_IN();

    SDA_HIGH();
    Timer3_Delay10us(1);
    SCL_HIGH();
    Timer3_Delay10us(1);

    while(SDA_IN() != 0x00)
    {
        timeout++;

        if(timeout > I2C_timeout)
        {
            SW_I2C_stop();
            return 1;
        }
    };

    SCL_LOW();
    return 0;
}

 

PCF8574.h

#include "SW_I2C.h"

#define PCF8574_address                 0x4E

#define PCF8574_write_cmd               PCF8574_address
#define PCF8574_read_cmd                (PCF8574_address | 1)

void PCF8574_init(void);
unsigned char PCF8574_read(void);
void PCF8574_write(unsigned char data_byte);

 

PCF8574.c

#include "N76E003_iar.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "PCF8574.h"

void PCF8574_init(void)
{
    SW_I2C_init();
    Timer0_Delay1ms(20);
}

unsigned char PCF8574_read(void)
{
    unsigned char port_byte = 0;

    SW_I2C_start();
    SW_I2C_write(PCF8574_read_cmd);
    port_byte = SW_I2C_read(I2C_NACK);
    SW_I2C_stop();

    return port_byte;
}

void PCF8574_write(unsigned char data_byte)
{
    SW_I2C_start();
    SW_I2C_write(PCF8574_write_cmd);
    SW_I2C_ACK_NACK(I2C_ACK);
    SW_I2C_write(data_byte);
    SW_I2C_ACK_NACK(I2C_ACK);
    SW_I2C_stop();
}

 

LCD_2_Wire.h

#include "PCF8574.h"

#define clear_display                                0x01
#define goto_home                                    0x02

#define cursor_direction_inc                         (0x04 | 0x02)
#define cursor_direction_dec                         (0x04 | 0x00)
#define display_shift                                (0x04 | 0x01)
#define display_no_shift                             (0x04 | 0x00)

#define display_on                                   (0x08 | 0x04)
#define display_off                                  (0x08 | 0x02)
#define cursor_on                                    (0x08 | 0x02)
#define cursor_off                                   (0x08 | 0x00)
#define blink_on                                     (0x08 | 0x01)
#define blink_off                                    (0x08 | 0x00)

#define _8_pin_interface                             (0x20 | 0x10)
#define _4_pin_interface                             (0x20 | 0x00)
#define _2_row_display                               (0x20 | 0x08)
#define _1_row_display                               (0x20 | 0x00)
#define _5x10_dots                                   (0x20 | 0x40)
#define _5x7_dots                                    (0x20 | 0x00)

#define BL_ON                                        1
#define BL_OFF                                       0

#define dly                                          2

#define DAT                                          1
#define CMD                                          0

void LCD_init(void);
void LCD_toggle_EN(void);
void LCD_send(unsigned char value, unsigned char mode);
void LCD_4bit_send(unsigned char lcd_data);           
void LCD_putstr(char *lcd_string);
void LCD_putchar(char char_data);
void LCD_clear_home(void);
void LCD_goto(unsigned char x_pos, unsigned char y_pos);

 

LCD_2_Wire.c

#include "N76E003_iar.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "LCD_2_Wire.h"

static unsigned char bl_state;
static unsigned char data_value;

void LCD_init(void)
{                       
  PCF8574_init();
  Timer0_Delay1ms(10);

  bl_state = BL_ON;
  data_value = 0x04;
  PCF8574_write(data_value);

  Timer0_Delay1ms(10);

  LCD_send(0x33, CMD);
  LCD_send(0x32, CMD);

  LCD_send((_4_pin_interface | _2_row_display | _5x7_dots), CMD);        
  LCD_send((display_on | cursor_off | blink_off), CMD);    
  LCD_send((clear_display), CMD);        
  LCD_send((cursor_direction_inc | display_no_shift), CMD);       
}  

void LCD_toggle_EN(void)
{
  data_value |= 0x04;
  PCF8574_write(data_value);
  Timer0_Delay1ms(1);
  data_value &= 0xF9;
  PCF8574_write(data_value);
  Timer0_Delay1ms(1);
}


void LCD_send(unsigned char value, unsigned char mode)
{
  switch(mode)
  {
     case CMD:
     {
        data_value &= 0xF4;
        break;
     }
     case DAT:
     {
        data_value |= 0x01;
        break;
     }
  }

  switch(bl_state)
  {
     case BL_ON:
     {
        data_value |= 0x08;
        break;
     }
     case BL_OFF:
     {
        data_value &= 0xF7;
        break;
     }
  }

  PCF8574_write(data_value);
  LCD_4bit_send(value);
  Timer0_Delay1ms(1);
}


void LCD_4bit_send(unsigned char lcd_data)      
{
  unsigned char temp = 0x00;

  temp = (lcd_data & 0xF0);
  data_value &= 0x0F;
  data_value |= temp;
  PCF8574_write(data_value);
  LCD_toggle_EN();

  temp = (lcd_data & 0x0F);
  temp <<= 0x04;
  data_value &= 0x0F;
  data_value |= temp;
  PCF8574_write(data_value);
  LCD_toggle_EN();

void LCD_putstr(char *lcd_string)
{
  do
  {
    LCD_putchar(*lcd_string++);
  }while(*lcd_string != '\0') ;
}

void LCD_putchar(char char_data)
{
  if((char_data >= 0x20) && (char_data <= 0x7F))
  {
    LCD_send(char_data, DAT);
  }
}

void LCD_clear_home(void)
{
  LCD_send(clear_display, CMD);
  LCD_send(goto_home, CMD);
}

void LCD_goto(unsigned char x_pos,unsigned char y_pos)
{                                                  
  if(y_pos == 0)   
  {                             
    LCD_send((0x80 | x_pos), CMD);
  }
  else
  {                                              
    LCD_send((0x80 | 0x40 | x_pos), CMD);
  }
}

 

main.c

#include "N76E003_iar.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "LCD_2_Wire.h"

void show_value(unsigned char value);

void main(void)
  unsigned char s = 0;

  static char txt1[] = {"MICROARENA"};
  static char txt2[] = {"SShahryiar"};
  static char txt3[] = {"Nuvoton 8-bit uC"};
  static char txt4[] = {"N76E003"};

  LCD_init();

  LCD_clear_home();

  LCD_goto(3, 0);
  LCD_putstr(txt1);
  LCD_goto(3, 1);
  LCD_putstr(txt2);
  Timer3_Delay100ms(30);

  LCD_clear_home();

  for(s = 0; s < 16; s++)
  {
    LCD_goto(s, 0);
    LCD_putchar(txt3[s]);
    Timer0_Delay1ms(90);
  }

  Timer3_Delay100ms(20);

  for(s = 0; s < 7; s++)
  {
    LCD_goto((4 + s), 1);
    LCD_putchar(txt4[s]);
    Timer0_Delay1ms(90);
  }

  Timer3_Delay100ms(30);

  s = 0;
  LCD_clear_home();

  LCD_goto(3, 0);
  LCD_putstr(txt1);

  while(1)
  {
    show_value(s);
    s++;
    Timer3_Delay100ms(4);
  };
}

void show_value(unsigned char value)
{
  unsigned char ch = 0x00;

  ch = ((value / 100) + 0x30);
  LCD_goto(6, 1);
  LCD_putchar(ch);

  ch = (((value / 10) % 10) + 0x30);
  LCD_goto(7, 1);
  LCD_putchar(ch);

  ch = ((value % 10) + 0x30);
  LCD_goto(8, 1);
  LCD_putchar(ch);
}

 

Schematic

I2C LCD Driver Schematic 2 Wire LCD_Schematic

Explanation

Just like the last example, software method is used to emulate I2C protocol using ordinary GPIOs. There are three parts of the code – first the software I2C driver, second the driver library for PCF8574 I2C 8-bit port expander and lastly the LCD driver itself. The LCD driver is same as the other LCD drivers in this document. I kept the code modular so that it is easy to understand the role of each piece of code. The I2C driver (SW_I2C) implements software I2C which is used by the PCF8574 driver. Thus, the port expander driver is dependent on the SW_I2C driver and the LCD driver is dependent on the port expander driver, and in cases like such we must find add libraries according to the order of dependency.

The advantage of keeping things modular is to easily modify things in a fast and trouble-free manner while keeping things ready for other deployments. In my codes I try to avoid repetitive and meaningless stuffs with meaningful definitions. For instance, just change the following lines to change pin configurations without going through the whole code:

#define SDA_DIR_OUT()   P03_PushPull_Mode
#define SDA_DIR_IN()    P03_Input_Mode

#define SCL_DIR_OUT()   P04_PushPull_Mode
#define SCL_DIR_IN()    P04_Input_Mode

#define SDA_HIGH()      set_P03
#define SDA_LOW()       clr_P03

#define SCL_HIGH()      set_P04
#define SCL_LOW()       clr_P04

#define SDA_IN()        P03

Likewise, the SW_I2C functions are not implemented inside the LCD or port expander driver files so that they can be used for other I2C devices.

I have code two versions of this LCD library just like the SPI-based ones – one with BSP-based delays and the other with software delays.

Demo

I2C LCD

Driving seven Segments by Bit-banging TM1640

Seven segment displays take up lot of GPIO pins when they are required to be interfaced with a host micro. There are several driver ICs like MAX7219, TM1640, 74HC594, etc to overcome this issue. TM1640 from Titan Micro Electronics does not support standard I2C or SPI communication protocol unlike most other driver ICs. Thus, to interface it with our host N76E003 micro, we need to apply bit-banging method just like the LCD examples.

TM1640

Code

 

fonts.h

const unsigned char fonts[11] =
{                       
  0x00, // (32)   <space>
  0x3F, // (48)   0
  0x06, // (49)   1
  0x5B, // (50)   2
  0x4F, // (51)   3
  0x66, // (52)   4 
  0x6D, // (53)   5 
  0x7D, // (54)   6 
  0x27, // (55)   7
  0x7F, // (56)   8 
  0x6F, // (57)   9 
};  

 

TM1640.h

#define TM1640_GPIO_init()                               do{P03_PushPull_Mode; P04_PushPull_Mode;}while(0)

#define DIN_pin_HIGH()                                   set_P03
#define DIN_pin_LOW()                                    clr_P03

#define SCLK_pin_HIGH()                                  set_P04
#define SCLK_pin_LOW()                                   clr_P04

#define no_of_segments                                   16

#define auto_address                                     0x40
#define fixed_address                                    0x44
#define normal_mode                                      0x40
#define test_mode                                        0x48

#define start_address                                    0xC0

#define brightness_5_pc                                  0x88
#define brightness_10_pc                                 0x89
#define brightness_25_pc                                 0x8A
#define brightness_60_pc                                 0x8B
#define brightness_70_pc                                 0x8C
#define brightness_75_pc                                 0x8D
#define brightness_80_pc                                 0x8E
#define brightness_100_pc                                0x8F
#define display_off                                      0x80
#define display_on                                       0x8F


void TM1640_init(unsigned char brightness_level);  
void TM1640_start(void);
void TM1640_stop(void);
void TM1640_write(unsigned char value);      
void TM1640_send_command(unsigned char value);
void TM1640_send_data(unsigned char address, unsigned char value);
void TM1640_clear_display(void);

 

TM1640.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "TM1640.h"


void TM1640_init(unsigned char brightness_level)
{               
  TM1640_GPIO_init();

  Timer0_Delay1ms(10); 

  DIN_pin_HIGH();
  SCLK_pin_HIGH();

  TM1640_send_command(auto_address);
  TM1640_send_command(brightness_level);
  TM1640_clear_display();
}  

void TM1640_start(void)
{
  DIN_pin_HIGH();
  SCLK_pin_HIGH();
  Timer3_Delay10us(1);
  DIN_pin_LOW();
  Timer3_Delay10us(1);
  SCLK_pin_LOW();
}

void TM1640_stop(void)
{
  DIN_pin_LOW();
  SCLK_pin_LOW();
  Timer3_Delay10us(1);
  SCLK_pin_HIGH();
  Timer3_Delay10us(1);
  DIN_pin_HIGH();
}


void TM1640_write(unsigned char value) 
{                                                      
  unsigned char s = 0x08;

  while(s > 0)
  {
    SCLK_pin_LOW();

    if((value & 0x01) == 0x01)
    {
     DIN_pin_HIGH();
    }
    else
    {
     DIN_pin_LOW();
    }

    SCLK_pin_HIGH();

    value >>= 0x01;
    s--;
  };
}                                

void TM1640_send_command(unsigned char value)   
{                           
  TM1640_start();
  TM1640_write(value);
  TM1640_stop();
}              

void TM1640_send_data(unsigned char address, unsigned char value)
{                 
  TM1640_send_command(fixed_address);

  TM1640_start();

  TM1640_write((0xC0 | (0x0F & address)));
  TM1640_write(value);

  TM1640_stop();

void TM1640_clear_display(void)
{
  unsigned char s = 0x00;

  for(s = 0x00; s < no_of_segments; s++)
  {
    TM1640_send_data(s, 0);
  };
}

 

main.c

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "font.h"
#include "TM1640.h"

void display_data(unsigned char segment, signed int value);

void main(void)
  unsigned int i = 0;
  unsigned int j = 999;

  TM1640_init(brightness_75_pc);;

  while(1)
  {
    display_data(0, i++);
    display_data(4, j--);
    Timer3_Delay100ms(4);
  };
}

void display_data(unsigned char segment, signed int value)
{
    unsigned char ch = 0;

    if((value > 99) && (value <= 999))
    {
      ch = (value / 100);
      TM1640_send_data((2 + segment), fonts[1 + ch]);

      ch = ((value / 10) % 10);
      TM1640_send_data((1 + segment), fonts[1 + ch]);

      ch = (value % 10);
      TM1640_send_data(segment, fonts[1 + ch]);
    }

    else if((value > 9) && (value <= 99))
    {
      TM1640_send_data((2 + segment), 0);

      ch = (value / 10);
      TM1640_send_data((1 + segment), fonts[1 + ch]);

      ch = (value % 10);
      TM1640_send_data(segment, fonts[1 + ch]);
    }

    else
    {
      TM1640_send_data((2 + segment), 0);

      TM1640_send_data((1 + segment), 0);

      ch = (value % 10);
      TM1640_send_data(segment, fonts[1 + ch]);
    }
}

 

Schematic

TM1640_Schematic

Explanation

Like the LCD libraries demoed previously, TM1640 is driven with GPIO bit-banging. Please read the datasheet of TM1640 to fully understand how the codes are implemented. It uses two pins just like I2C but don’t be fooled as it doesn’t support I2C protocol. It uses a protocol of its own. To change pin configuration, just change the following lines of code:

#define TM1640_GPIO_init()                               do{P03_PushPull_Mode; P04_PushPull_Mode;}while(0)

#define DIN_pin_HIGH()                                   set_P03
#define DIN_pin_LOW()                                    clr_P03

#define SCLK_pin_HIGH()                                  set_P04
#define SCLK_pin_LOW()                                   clr_P04

 

Demo

TM1640

External Interrupt (EXTI)

External interrupt is a key GPIO feature in input mode. It momentarily interrupts regular program flow just like other interrupts and does some tasks before resuming interrupted task. In traditional 8051s, there are two external interrupts with dedicated and separate interrupt vector addresses. The same applies to N76E003. Highlighted below in the N76E003’s interrupt vector table are the interrupt vector addresses/numbers of these two external interrupts:

Interrupt Vector

Code

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"

void setup(void);

void EXT_INT0(void)
interrupt 0
{
  set_P00;
}

void EXT_INT1(void)
interrupt 2
{
  set_P01;
}

void main(void)
{
  setup();

  while(1)
  {
    Timer0_Delay1ms(1000);
    clr_P00;
    clr_P01;
  };
}

void setup(void)
{
  P00_PushPull_Mode;
  P01_PushPull_Mode;
  P17_Input_Mode;
  P30_Input_Mode; 
  set_P1S_7;
  set_P3S_0;
  set_IT0;
  set_IT1;
  set_EX0;
  set_EX1;
  set_EA;   
}

 

Schematic

EXTI schematic

Explanation

The setup for this demo is simple. There are two LEDs and two buttons connected with a N76E003 chip as per schematic. The buttons are connected with external interrupt pins. Obviously, these pins are declared as input pins. Additionally, internal input Schmitt triggers of these pins are used to ensure noise cancellation. Both interrupts are enabled along with their respective interrupt hardware. Finally, global interrupt is set. Optionally interrupt priority can be applied.

P17_Input_Mode;
P30_Input_Mode; 

set_P1S_7;
set_P3S_0;

set_IT0;
set_IT1;

set_EX0;
set_EX1;
set_EA;   

Since we enabled two interrupts with different interrupt vectors, there will be two interrupt subroutine functions. Each of these functions will briefly turn on LEDs assigned to them. The LEDs are turned off in the main function. Thus, the LEDs mark which interrupt occurred.

void EXT_INT0(void)
interrupt 0
{
  set_P00;
}

void EXT_INT1(void)
interrupt 2
{
  set_P01;
}

 

Demo

EXTI

Pin Interrupt – Interfacing Rotary Encoder

Apart from dedicated external interrupts, N76E003 is equipped with pin interrupt facility – a feature that can be found in almost every microcontroller of modern times. With pin interrupt, any GPIO can be made to behave like external interrupt. However, unlike external interrupts, a single hardware interrupt channel and therefore one vector address is used for mapping a maximum of eight different GPIO pins. These pins need not to be on the same GPIO port. When interrupt occurs, we need to assert from which pin it originated. This feature becomes very useful when interfacing keypads and buttons.

Pin Interrupt Structure

Code

#include "N76E003_IAR.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "soft_delay.h"
#include "LCD_2_Wire.h"

signed char encoder_value = 0;

void setup(void);
void lcd_print(unsigned char x_pos, unsigned char y_pos, unsigned int value);

#pragma vector = 0x3B
__interrupt void PIN_INT(void)
{
  clr_EA;

  if(PIF == 0x01)
  {
    if((P1 & 0x03) == 0x02)
    {
      encoder_value++;
    }

    if(encoder_value > 99)
    {
      encoder_value = 0;
    }
  }

  if(PIF == 0x02)
  {       
    if((P1 & 0x03) == 0x01)
    {
      encoder_value--;
    }

    if(encoder_value < 0)
    {
      encoder_value = 99;
    }
  }

  PIF = 0x00;

  P15 = ~P15;
}

void main(void)
{
  setup();

  while(1)
  {
    set_EA;
    lcd_print(14, 0, encoder_value);
    delay_ms(40);
  }
}

void setup(void)
{   
  P10_Input_Mode;
  P11_Input_Mode; 

  P15_PushPull_Mode;

  Enable_BIT0_LowLevel_Trig;
  Enable_BIT1_LowLevel_Trig;

  Enable_INT_Port1;

  set_EPI;  

  LCD_init();
  LCD_clear_home();
  LCD_goto(0, 0);
  LCD_putstr("ENC Count:");
}

void lcd_print(unsigned char x_pos, unsigned char y_pos, unsigned int value)
  LCD_goto(x_pos, y_pos);
  LCD_putchar((value / 10) + 0x30);
  LCD_goto((x_pos + 1), y_pos);
  LCD_putchar((value % 10) + 0x30);
}

 

Schematic

pin int schematic

Explanation

Pin interrupt is not same as dedicated external interrupt but still it is very useful in a number of cases. In this demo, two pin interrupts are used to decode a rotary encoder. Probably this is the simplest method of decoding a rotary encoder.

Setting up pin interrupt is very easy. We need to set the pin interrupt pins are inputs. We can optionally use the internal Schmitt triggers. Then we decide the edge to detect and which ports to check for pin interrupt. Finally, we set the pin interrupt hardware.

P10_Input_Mode;
P11_Input_Mode;

Enable_BIT0_LowLevel_Trig;
Enable_BIT1_LowLevel_Trig;

Enable_INT_Port1;

set_EPI;  

Inside the pin interrupt function, we need to check which pin shot the interrupt by checking respective flags. Encoder count is incremented/decremented based on which flag got shot first and the logic state of the other pin. Since here a rotary encoder is interfaced with pin interrupt facility of N76E003, we have to ensure that the micro doesn’t detect any further or false interrupts while already processing one interrupt condition. This is why the global interrupt is disabled every time the code enters the pin interrupt function. This is restarted in the main. Similarly, to ensure proper working we have clear the interrupt flags before exiting the function. P15 is toggled with interrupt to visually indicate the rotation of the encoder.

#pragma vector = 0x3B
__interrupt void PIN_INT(void)
{
  clr_EA;

  if(PIF == 0x01)
  {
    if((P1 & 0x03) == 0x02)
    {
      encoder_value++;
    }

    if(encoder_value > 99)
    {
      encoder_value = 0;
    }
  }

  if(PIF == 0x02)
  {       
    if((P1 & 0x03) == 0x01)
    {
      encoder_value--;
    }

    if(encoder_value < 0)
    {
      encoder_value = 99;
    }
  }

  PIF = 0x00;

  P15 = ~P15;
}

The main code just shows the encoder count. When the encoder is rotated in one direction, the count increases while rotating it in the opposite direction causes the encoder count to decrease.

Demo

Pin Int

Clock System

The clock system of N76E003 is very straight forward and very flexible. To begin with, there are three clock sources, a clock selector and a common clock divider block apart from other blocks. Shown below is the block diagram of N76E003’s clock system:

Clock System

The three sources are as follows:

Clock Sources

Once a given clock source is set, it becomes the clock for all systems. The only exception here is the watchdog timer and the self-wake-up timer which are only run by the LIRC.

 

Code

#include "N76E003_iar.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"

#define HIRC    0
#define LIRC    1
#define ECLK    2

void set_clock_source(unsigned char clock_source);
void disable_clock_source(unsigned char clock_source);
void set_clock_division_factor(unsigned char value);

void main(void)
{
  signed char i = 30;     

  P11_PushPull_Mode; 
  P15_PushPull_Mode;

  set_clock_division_factor(0);
  set_clock_source(HIRC);

  set_CLOEN;

  while(i > 0)                                  
  {  
    clr_P15;                            
    Timer0_Delay1ms(100);
    set_P15;
    Timer0_Delay1ms(100);
    i--;
  }

  set_clock_source(ECLK);
  disable_clock_source(HIRC);

  i = 30;

  while(i > 0)                                  
  {  
    clr_P15;                            
    Timer0_Delay1ms(100);
    set_P15;
    Timer0_Delay1ms(100);
    i--;
  }

  set_clock_source(LIRC);
  disable_clock_source(HIRC);

  while(1)
  {
    clr_P15;                            
    Timer0_Delay1ms(1);
    set_P15;
    Timer0_Delay1ms(1);
  };
}

void set_clock_source(unsigned char clock_source)
{
  switch(clock_source)
  {
    case LIRC:
    {
      set_OSC1;                     
      clr_OSC0; 

      break;
    }

    case ECLK:
    {
      set_EXTEN1;
      set_EXTEN0;

      while((CKSWT & SET_BIT3) == 0); 

      clr_OSC1;                     
      set_OSC0;

      break;
    }

    default:
    {
      set_HIRCEN;         

      while((CKSWT & SET_BIT5) == 0);   

      clr_OSC1;                       
      clr_OSC0;

      break;
    }
  }

  while((CKEN & SET_BIT0) == 1);  
}

void disable_clock_source(unsigned char clock_source)
{
  switch(clock_source)
  {
    case HIRC:
    {
      clr_HIRCEN;
      break;
    }

    default:
    {
      clr_EXTEN1;
      clr_EXTEN0;
      break;
    }
  }
}

void set_clock_division_factor(unsigned char value)
{
  CKDIV = value;
}

 

Schematic

CLK_Schematic

Explanation

The very first thing to note is the absence of two OSC pins unlike other micros and yet a crystal resonator is connected with P30 and P17. This configuration is not correct. There is only OSCIN pin. This is because N76E003 can only be driven with active clock sources like crystal modules, external electronic circuitry, etc. The HIRC clock source is accurate enough for most purposes and there is literally no need for external clock. I have done most of the experiments with HIRC and I’m satisfied with it.

Many people don’t understand the difference between a crystal oscillator module and a crystal resonator. Both are based on quartz crystals but the oscillator one has internal electronics to generate clock pulses precisely while the resonator just contains the quartz crystal. Crystal modules are accurate compared to resonators because the internal electronics in them take care of the effects of temperature. Resonators are therefore called passive clock crystals while the clock modules are termed active clocks.

crystal

Here to test all three clock sources, I used two things – first the onboard LED and second the clock output pin. Different clock sources are enabled briefly one after another and the onboard LED is blinked. The blinking rate of the LED is an indirect indicator of clock speed. The clock output too is monitored with an oscilloscope/signal analyser for clock speeds. HIRC is turned on first, then ECLK and finally LIRC. By default, both HIRC and LIRC are turned on during power on. When switching between clock sources, we should poll if the new clock source is stable prior to using it and disable the one that we don’t need.

I have coded the following three for setting up the clock system. Their names suggest their purposes.

void set_clock_source(unsigned char clock_source);
void disable_clock_source(unsigned char clock_source);
void set_clock_division_factor(unsigned char value);

These three functions will be all that you’ll ever need to configure the clock system without any hassle. The first two are most important as they select clock source and disabled the one that is not need. If you are still confused about setting the system clock then you can avoid the clock division function and straight use the following function:

void set_clock_frequency(unsigned long F_osc, unsigned long F_sys)
{
  F_osc = (F_osc / (2 * F_sys));

  if((F_osc >= 0x00) && (F_osc <= 0xFF))
  {
    CKDIV = ((unsigned char)F_osc);
  }
}

This function takes two parameters – the frequency of the clock source and the frequency of the system after clock division.

Demo

Clock System

12-Bit ADC – LM35 Thermometer

Most 8051s don’t have any embedded ADC but N76E003 comes with a 12-bit SAR ADC. This is also one area where N76E003 differs a lot from STM8S003. The 12-bit resolution is the factor. N76E003 has eight single-ended ADC inputs along with a bandgap voltage generator and a built-in comparator. The ADC can be triggered internally with software or by external hardware pins/PWM. Everything is same as the ADCs of other microcontrollers and there’s not much difference.

ADC_Structure

LM35

Code

#include "N76E003_IAR.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "LCD_3_Wire.h"

#define scalar          0.12412

void setup(void);
unsigned int ADC_read(void);
void lcd_print_i(unsigned char x_pos, unsigned char y_pos, unsigned int value);
void lcd_print_f(unsigned char x_pos, unsigned char y_pos, unsigned int value);

void main(void)
{
  unsigned int temp = 0;
  unsigned int adc_count = 0;

  setup();

  while(1)
  {
    adc_count = ADC_read();
    temp = ((unsigned int)(((float)adc_count) / scalar));
    lcd_print_i(12, 0, adc_count);
    lcd_print_f(11, 1, temp);
    Timer0_Delay1ms(600);
  }
}

void setup(void)
{
  LCD_init();
  LCD_clear_home();
  LCD_goto(0, 0);
  LCD_putstr("ADC Count:");
  LCD_goto(0, 1);
  LCD_putstr("Tmp/deg C:");

  Enable_ADC_AIN0;
}

unsigned int ADC_read(void)
{
  register unsigned int value = 0x0000;

  clr_ADCF;
  set_ADCS;                 
  while(ADCF == 0);

  value = ADCRH;
  value <<= 4;
  value |= ADCRL;

  return value;
}

void lcd_print_i(unsigned char x_pos, unsigned char y_pos, unsigned int value)
{  
  LCD_goto(x_pos, y_pos);
  LCD_putchar((value / 1000) + 0x30);
  LCD_goto((x_pos + 1), y_pos);
  LCD_putchar(((value % 1000) / 100) + 0x30); 
  LCD_goto((x_pos + 2), y_pos);
  LCD_putchar(((value % 100) / 10) + 0x30);
  LCD_goto((x_pos + 3), y_pos);
  LCD_putchar((value % 10) + 0x30);
}

void lcd_print_f(unsigned char x_pos, unsigned char y_pos, unsigned int value)
  LCD_goto(x_pos, y_pos);
  LCD_putchar((value / 1000) + 0x30);
  LCD_goto((x_pos + 1), y_pos);
  LCD_putchar(((value % 1000) / 100) + 0x30); 
  LCD_goto((x_pos + 2), y_pos);
  LCD_putchar('.');
  LCD_goto((x_pos + 3), y_pos);
  LCD_putchar(((value % 100) / 10) + 0x30);
  LCD_goto((x_pos + 4), y_pos);
  LCD_putchar((value % 10) + 0x30);
}

 

Schematic

ADC_Schematic

Explanation

In this demo, one ADC channel (AIN0) is used to read a LM35 temperature sensor. Polling method is used to read the ADC.

Enabling the ADC is simply done by coding the following line:

Enable_ADC_AIN0;

In the background of this, ADC channel selection and other parameters are set. If you want more control over the ADC then you must set the ADC registers on your own. Most of the times that can be avoided.

Reading the ADC needs some attention because the ADC data registers are not aligned like other registers and we just need 12-bits, not 8/16-bits.

ADC_data_register

Notice that the we must extract ADC from ADCCRH and from the low four bits of ADCCRL. To handle this issue the follow function is devised:

unsigned int ADC_read(void)
{
  register unsigned int value = 0x0000;

 

  clr_ADCF;
  set_ADCS;                 
  while(ADCF == 0);

 

  value = ADCRH;
  value <<= 4;
  value |= ADCRL;

 

  return value;
}

When this function is called, the ADC conversion completion flag is cleared and the ADC is software-triggered. The conversion completion flag is polled. When this flag status is changed, the ADC data registers are read. Finally, the value is returned.

LM35 gives 10mV output for each degree of temperature. Therefore, for 26°C the sensor is supposed to give 260mV output. At 3.3V the ADC count will 4095 while at 0V the ADC count will be 0 count. Thus, 0.806mV equals one count and so 260mV should give:

Counts

 

In the display, we have to show 26.00°C i.e. 2600 considering no decimal point and fully integer value. Thus, to transform 322 to 2600, we have to divide the result with a scalar (0.12412). The main code just does that after reading the ADC. The ADC count and temperature are then both shown in the LCD.

Demo

ADC

ADC Interrupt – LDR-based Light Sensor

Like any other interrupts, ADC interrupt is a very interrupt. In the last example we saw polling-based ADC readout. In this segment, we will see how to use interrupt-based method to extract ADC data. The concept of ADC interrupt is simply to notify that an ADC data has been made ready for reading once triggered.

LDR

Code

#include "N76E003_IAR.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "soft_delay.h"
#include "LCD_2_Wire.h"

//LDR Definitions//

#define LDR_constant                  100000.0
#define R_fixed                        10000.0            
#define VDD                               4095

unsigned int adc_value = 0x0000;

void setup(void);
unsigned int ADC_read(void);
void lcd_print_i(unsigned char x_pos, unsigned char y_pos, unsigned int value);
unsigned int measure_light_intensity(void);

#pragma vector = 0x5B
__interrupt void ADC_ISR(void)
{
  adc_value = ADC_read();
  clr_ADCF;
}

void main(void)
{
  unsigned int lux = 0;

  setup();

  while(1)
  {
    set_ADCS;
    lux = measure_light_intensity();
    lcd_print_i(12, 0, adc_value);
    lcd_print_i(12, 1, lux);
    delay_ms(400);
  }
}

void setup(void)
{
  LCD_init();
  LCD_clear_home();
  LCD_goto(0, 0);
  LCD_putstr("ADC Count:");
  LCD_goto(0, 1);
  LCD_putstr("Lux Value:");

  Enable_ADC_AIN4;
  set_EADC;
  set_EA;
}

unsigned int ADC_read(void)
{
  register unsigned int value = 0x0000; 

  value = ADCRH;
  value <<= 4;
  value |= ADCRL;

  return value;
}

void lcd_print_i(unsigned char x_pos, unsigned char y_pos, unsigned int value)
  LCD_goto(x_pos, y_pos);
  LCD_putchar((value / 1000) + 0x30);
  LCD_goto((x_pos + 1), y_pos);
  LCD_putchar(((value % 1000) / 100) + 0x30); 
  LCD_goto((x_pos + 2), y_pos);
  LCD_putchar(((value % 100) / 10) + 0x30);
  LCD_goto((x_pos + 3), y_pos);
  LCD_putchar((value % 10) + 0x30);
}

unsigned int measure_light_intensity(void)
{
  float lux = 0;

  lux = adc_value;

  lux = (LDR_constant * ((VDD / (R_fixed * lux))) - 0.1);

  if((lux >= 0) && (lux <= 9999))
  {
    return ((unsigned int)lux);
  }
  else
  {
    return 0;
  }
}

 

Schematic

ADC_INT_Schematic

Explanation

Setting the ADC in interrupt is not much different from the previous example except for the interrupt parts.

Enable_ADC_AIN4;
set_EADC;
set_EA;

We have to enable both the ADC and global interrupts.

The reading process is also same:

unsigned int ADC_read(void)
{
  register unsigned int value = 0x0000; 

 

  value = ADCRH;
  value <<= 4;
  value |= ADCRL;

 

  return value;
}

The ADC is triggered in the main with the following line of code since we are using software-based triggering:

set_ADCS;

Now instead of reading the ADC in the main by polling, the ADC is read in the interrupt.

#pragma vector = 0x5B
__interrupt void ADC_ISR(void)
{
  adc_value = ADC_read();
  clr_ADCF;
}

When ADC interrupt occurs, we must clear ADC interrupt flag and read the ADC data registers. In this way, the main code is made free from polling and free for other tasks.

The demo here is a rudimentary LDR-based light intensity meter. Light falling on the LDR changes its resistance. Using voltage divider method, we can back calculate the resistance of the LDR and use this info to measure light intensity.

Demo

ADC Int

ADC Comparator

Many micros are equipped with on-chip analogue comparator. N76E003 has an embedded ADC comparator. The main feature of this comparator is the range it offers. Unlike comparators of other micros in which only a few selectable set points can set, the ADC comparator of N76E003 can be set in any range from 0 count to the max ADC count of 4095. This allows us to easily implement it for many applications like low battery alarm, SMPSs, over voltage sense, overload detection, etc. It must be noted however, it is not a true comparator because a true comparator has nothing to do with ADC.

adc_comparator_block

The comparator block is situated at the output of the ADC and so it is just a single block like the ADC but all eight channels share it. Thus, when it is needed to compare multiple channels, it should be reset and reconfigured.

Code

#include "N76E003_IAR.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "soft_delay.h"
#include "LCD_2_Wire.h"

unsigned int adc_value = 0x0000;

void setup(void);
unsigned int ADC_read(void);
void set_ADC_comparator_value(unsigned int value);
void lcd_print_i(unsigned char x_pos, unsigned char y_pos, unsigned int value);

#pragma vector = 0x5B
__interrupt void ADC_ISR(void)
{
  adc_value = ADC_read();
  clr_ADCF;
}

void main(void)
{
  setup();

  while(1)
  {
    set_ADCS;
    lcd_print_i(12, 0, adc_value);

    LCD_goto(12, 1);

    if((ADCCON2 & 0x10) != 0x00)
    {
      LCD_putstr("HIGH");
      set_P15;
    }
    else
    {
      LCD_putstr(" LOW");
      clr_P15;
    }

    delay_ms(400);
  }
}

void setup(void)
{
  P15_PushPull_Mode;

  LCD_init();
  LCD_clear_home();
  LCD_goto(0, 0);
  LCD_putstr("ADC Count:");
  LCD_goto(0, 1);
  LCD_putstr("Cmp State:");

  Enable_ADC_BandGap;
  Enable_ADC_AIN4;

  set_ADC_comparator_value(1023); 
  set_ADCMPEN;

  set_EADC;
  set_EA;
}

unsigned int ADC_read(void)
{
  register unsigned int value = 0x0000; 

  value = ADCRH;
  value <<= 4;
  value |= ADCRL;

  return value;
}

void set_ADC_comparator_value(unsigned int value)
{
  ADCMPH = ((value & 0x0FF0) >> 4);
  ADCMPL = (value & 0x000F);
}

void lcd_print_i(unsigned char x_pos, unsigned char y_pos, unsigned int value)
  LCD_goto(x_pos, y_pos);
  LCD_putchar((value / 1000) + 0x30);
  LCD_goto((x_pos + 1), y_pos);
  LCD_putchar(((value % 1000) / 100) + 0x30); 
  LCD_goto((x_pos + 2), y_pos);
  LCD_putchar(((value % 100) / 10) + 0x30);
  LCD_goto((x_pos + 3), y_pos);
  LCD_putchar((value % 10) + 0x30);
}

 

Schematic

ADC_comparator_Schematic

Explanation

To configure the ADC comparator block, we just need to specify two things – the reference value with which the comparison should to be done and the polarity of comparison. After setting these up, we have to enable the comparator block. These are done as follows:

set_ADC_comparator_value(1023); 
set_ADCMPEN;

Note in the code snippet above, I didn’t code anything regarding polarity because by default the polarity is set as such that the comparator’s output will change state when the input voltage is greater than or equal to the set ADC count level of 1023.

ADC Comparator

Just like ADC reading, we have to take care of the bit positions for the comparator set point. It is coded as follows.

void set_ADC_comparator_value(unsigned int value)
{
  ADCMPH = ((value & 0x0FF0) >> 4);
  ADCMPL = (value & 0x000F);
}

In this demo, I also used the bandgap voltage as reference source:

Enable_ADC_BandGap;

The rest of the code is similar to the ADC interrupt code.

Demo

ADC Comparator (1) ADC Comparator (2)

Data Flash – Using APROM as EEPROM

In most standard 8051s, there is no dedicated memory space as EEPROM. This is unlike other microcontrollers of modern era. EEPROM memory is needed for the storage of critical data that need to be retained even in power down state. In N76E003, there are two types of ROM memory. EEPROM-like storage can be achieved by using APROM (Application ROM). APROM is the actual flash memory where store our application codes. User Code Loader ROM or LDROM of N76E003 microcontroller, is another ROM where we can keep a bootloader and configuration codes. Sizes of these ROMs can be varied according to configuration bits.

Memory Block

Code

 

Flash.h

#define  CID_READ            0x0B
#define  DID_READ            0x0C

#define  PAGE_ERASE_AP       0x22
#define  BYTE_READ_AP        0x00
#define  BYTE_PROGRAM_AP     0x21
#define  PAGE_SIZE           128u

#define  ERASE_FAIL          0x70
#define  PROGRAM_FAIL        0x71
#define  IAPFF_FAIL          0x72
#define  IAP_PASS            0x00

void enable_IAP_mode(void);
void disable_IAP_mode(void);
void trigger_IAP(void);
unsigned char write_data_to_one_page(unsigned int u16_addr, const unsigned char *pDat, unsigned char num);
void write_data_flash(unsigned int u16_addr, unsigned char *pDat, unsigned int num);
void read_data_flash(unsigned int u16_addr, unsigned char *pDat, unsigned int num);

 

Flash.c

#include "N76E003_iar.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "soft_delay.h"
#include "Flash.h"

static unsigned char EA_Save_bit;

void enable_IAP_mode(void)
{
    EA_Save_bit = EA;
    clr_EA;
    TA = 0xAA;
    TA = 0x55;
    CHPCON |= 0x01 ;
    TA = 0xAA;
    TA = 0x55;
    IAPUEN |= 0x01;
    EA = EA_Save_bit;
}

void disable_IAP_mode(void)
{
    EA_Save_bit = EA;
    clr_EA;
    TA = 0xAA;
    TA = 0x55;
    IAPUEN &= ~0x01;
    TA = 0xAA;
    TA = 0x55;
    CHPCON &=~ 0x01;
    EA = EA_Save_bit;
}

void trigger_IAP(void)
{
    EA_Save_bit = EA;
    clr_EA;
    TA = 0xAA;
    TA = 0x55;
    IAPTRG |= 0x01;
    EA = EA_Save_bit;
}


unsigned char write_data_to_one_page(unsigned int u16_addr, const unsigned char *pDat, unsigned char num)
{
    unsigned char i = 0;
    unsigned char offset = 0;
    unsigned char __code *pCode;
    unsigned char __xdata *xd_tmp;

    enable_IAP_mode();
    offset = (u16_addr & 0x007F);
    i = (PAGE_SIZE - offset);

    if(num > i)
    {
      num = i;
    }

    pCode = (unsigned char __code *)u16_addr;

    for(i = 0; i < num; i++)
    {
        if(pCode[i] != 0xFF)
        {
          break;
        }
    }

    if(i == num)
    {
        IAPCN = BYTE_PROGRAM_AP;
        IAPAL = u16_addr;
        IAPAH = (u16_addr >> 8);

        for(i = 0; i < num; i++)
        {
          IAPFD = pDat[i];
          trigger_IAP();
          IAPAL++;
        }

        for(i = 0; i < num; i++)
        {
          if(pCode[i] != pDat[i])
          {
                   break;   
          } 
        }

        if(i != num)
        {
          goto WriteDataToPage20;
        }
    }

    else
    {
      WriteDataToPage20:
      pCode = (unsigned char __code *)(u16_addr & 0xFF80);
      for(i = 0; i < 128; i++)
      {
           xd_tmp[i] = pCode[i];
      }

      for(i = 0; i < num; i++)
      {
           xd_tmp[offset + i] = pDat[i];
      }

      do
      {
           IAPAL = (u16_addr & 0xFF80);
           IAPAH = (u16_addr >> 8);
           IAPCN = PAGE_ERASE_AP;
           IAPFD = 0xFF;    
           trigger_IAP();
           IAPCN =BYTE_PROGRAM_AP;

           for(i = 0; i < 128; i++)
           {
                IAPFD = xd_tmp[i];
                trigger_IAP();
                IAPAL++;
           }

           for(i = 0; i < 128; i++)
           {
                if(pCode[i] != xd_tmp[i])
                {
                     break;
                }
           }
      }while(i != 128);

    }

    disable_IAP_mode();

    return num;
}   


void write_data_flash(unsigned int u16_addr, unsigned char *pDat,unsigned int num)
{
    unsigned int CPageAddr = 0;
    unsigned int EPageAddr = 0;
    unsigned int cnt = 0;

    CPageAddr = (u16_addr >> 7);
    EPageAddr = ((u16_addr + num) >> 7);

    while(CPageAddr != EPageAddr)
    {
      cnt = write_data_to_one_page(u16_addr, pDat, 128);
      u16_addr += cnt;
      pDat += cnt;
      num -= cnt;
      CPageAddr = (u16_addr >> 7);
    }

    if(num)
    {
      write_data_to_one_page(u16_addr, pDat, num);
    }
}

void read_data_flash(unsigned int u16_addr, unsigned char *pDat, unsigned int num)
{
    unsigned int i = 0;

    for(i = 0; i < num; i++)
    {
        pDat[i] = *(unsigned char __code *)(u16_addr+i);
    }
}

 

main.c

#include "N76E003_IAR.h"
#include "Common.h"
#include "Delay.h"
#include "soft_delay.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "LCD_2_Wire.h"
#include "Flash.h"

#define BASE_ADDRESS        3700

void lcd_print_c(unsigned char x_pos, unsigned char y_pos, unsigned char value);
void lcd_print_i(unsigned char x_pos, unsigned char y_pos, unsigned int value);

void main (void)
{
  unsigned char s = 0;
  unsigned char val[1] = {0};
  unsigned char ret_val[1] = {0};

  P15_PushPull_Mode;

  LCD_init();
  LCD_clear_home();

  clr_P15;  
  LCD_goto(0, 0);
  LCD_putstr("R Addr:");
  LCD_goto(0, 1);
  LCD_putstr("R Data:");

  for(s = 0; s <= 9; s++)
  {
    read_data_flash((s + BASE_ADDRESS), ret_val, 1);
    delay_ms(10);
    lcd_print_i(11, 0, (s + BASE_ADDRESS));
    lcd_print_c(13, 1, ret_val[0]);
    delay_ms(600);
  }

  delay_ms(2000);

  set_P15;  
  LCD_goto(0, 0);
  LCD_putstr("W Addr:");
  LCD_goto(0, 1);
  LCD_putstr("W Data:");

  for(s = 0; s <= 9; s++)
  {
    val[0] = s;   
    write_data_flash((s + BASE_ADDRESS), val, 1);
    delay_ms(10);
    lcd_print_i(11, 0, (s + BASE_ADDRESS));
    lcd_print_c(13, 1, val[0]);
    delay_ms(600);
  }

  while(1)
  {
  };
}

void lcd_print_c(unsigned char x_pos, unsigned char y_pos, unsigned char value)
  LCD_goto(x_pos, y_pos);
  LCD_putchar((value / 100) + 0x30);
  LCD_goto((x_pos + 1), y_pos);
  LCD_putchar(((value % 10) / 10) + 0x30);
  LCD_goto((x_pos + 2), y_pos);
  LCD_putchar((value % 10) + 0x30);
}

void lcd_print_i(unsigned char x_pos, unsigned char y_pos, unsigned int value)
  LCD_goto(x_pos, y_pos);
  LCD_putchar((value / 10000) + 0x30);
  LCD_goto((x_pos + 1), y_pos);
  LCD_putchar(((value % 10000) / 1000) + 0x30); 
  LCD_goto((x_pos + 2), y_pos);
  LCD_putchar(((value % 1000) / 100) + 0x30);
  LCD_goto((x_pos + 3), y_pos);
  LCD_putchar(((value % 100) / 10) + 0x30);
  LCD_goto((x_pos + 4), y_pos);
  LCD_putchar((value % 10) + 0x30);
}

 

Schematic

EEPROM Schematic

Explanation

To write and read data from flash we can use the following functions:

void write_data_flash(unsigned int u16_addr, unsigned char *pDat, unsigned int num);
void read_data_flash(unsigned int u16_addr, unsigned char *pDat, unsigned int num);

Both of these functions are pointer-based functions. The first parameter of these function is the physical location of data, the second argument is the data pointer itself and the last argument is the number of bytes to read/write. Try to use the upper addresses of the flash where application code usually doesn’t reside. Reading the flash doesn’t require any involvement of IAP while writing does require IAP. The functions are self-explanatory and can be used readily without any changes since they are provided in the BSP examples.

In the demo, ten data bytes are saved in ten separate locations starting from location 3700 to 3709. Prior to that these locations are read. On first start up, these locations have no saved data and so they show up garbage values (205/255 usually). When a valid user data is saved, the location is updated with it. When reset or powered down, the newly written data bytes are retained and reread when powered up again.

Try not to frequently write on the flash since flash memories wear on frequent writes. Use a RAM-based buffer and try to write one page of flash when the buffer is full. In this way, flash memory wear-and-tear is slightly reduced.

Be careful about saving locations. Don’t use locations that hold application code. Try to use unoccupied empty flash locations or upper flash addresses. Use the NuMicro ISP Programming Tool for finding empty locations.

Demo

EEPROM

The post Getting Started with Nuvoton 8-bit Microcontrollers – Coding Part 1 appeared first on Embedded Lab.

Making a SPL dB Meter

$
0
0

In the 1980s, there was no internet as like today and so the sources of entertainment were televisions, radios and cassette players. When I was a kid, we had an audio cassette player. We used it to play songs but my imagination was always fixed to its VU meter display with its fancy readings as shown below. It changed with the volume of the speakers and matched rhythmically with the sound coming out of it. During my engineering career, I got to know about the Decibel scale and sound pressure measurement. It soon became a goal for me to design an audio dB meter and recreate my child memories. In this tutorial, I will show how to make a dB meter that is accurate enough for general uses.

Background and Application

Sound needs a medium for propagation or travel. It can’t travel in vacuum. Normally air is that medium but sound can also propagate in liquids and other states of matter. I am not going to lecture on how sound travels and its properties as Wikipedia details everything well here. Everything we see around us has a measurement and a unit. In case of sound pressure, the unit is decibel. Our basic requirement is to be able to measure Sound Pressure Level (SPL) in decibel scale with a typical 8-bit microcontroller, an ordinary microphone and without involving complex algorithms.

Measurement of sound has a number of uses. For instance, monitoring sound pollution, security system, monitoring the quality of an amplifier, detecting sound profile of an environment, etc.

Selecting Microphone

For the ease of work and for known parameters, I selected Seeedstudio’s Grove Sound Sensor which happens to have the following specs:

Seeedstudio – a Shenzhen, China-based component manufacturer and global component supplier, has a good reputation in the hobby-electronics community, particularly amongst Arduino and Raspberry Pi users. It is one of the most reliable electronics partners that make quality items at reasonable price tags.

The Grove microphone module that Seeedstudio sells under the Grove product family banner consists of an electret microphone and a pair of general-purpose op-amps in non-inverting configuration that increase the gain roughly by 100 times. The sensor I used was an old version one and the main difference is an additional potentiometer to alter gain. Shown below is the schematic of the current version of the sensor:

The microphone module is itself very small. The Grove header and connector are simple and all products of Grove family share the same pin/header layout. The headers bring out power pins and signal pin(s). Grove connectors connect with these headers in one way only and the connectors have properly colored-wires. Thus, there is literally no chance of accidental wrong connections. The same type of header brings out the needed connections from the microphone module. We just have to connect them to our host microcontroller and power supply.

The Math

The specs of the microphone in the Grove Sound Sensor suggests that it has a sensitivity of 52 – 48dB at 1kHz spectrum. Therefore, it is best to consider an average sensitivity of 50dB. 

The formula for sensitivity is as follows:

where Output AREF is typically 1000 mV/Pa (1 V/Pa) reference output ratio.

Thus, the sensitivity of the microphone in mv/Pa is found by going on the other side of the formula as shown in the following steps:

Going back to the schematic of the microphone module, we see that the original signal is amplified by about 100 times and so the actual sensitivity of the microphone is calculated to be:

Microphone sensitivity is typically measured with a 1kHz sine wave at a 94dB sound pressure level (SPL), or with 1Pa pressure. This is a reference value. Thus, the SPL value is given by the following formula:

where 94 is the base value.

A dB reading of 40 represents near quietness while a dB reading of 95 represents the while of a train.

Schematic

As can be seen, the above schematic was designed with Proteus VSM. There is no model for Grove Sound Sensor and so to mimic it, an interactive potentiometer was used in its place. Additionally, power and additional GLCD pins have been ignored in the schematic. However, those pins are present physically.

Coding

The code was written with CCS PIC C compiler and a PIC18F242 microcontroller was used for the project due to its large memories. CCS PIC C compiler’s coding style is similar to that of Arduino’s and so I guess nobody would be having any issue understanding the code.

The coding has two major parts. First is data collection and processing, and secondly, the graphical presentation of the collected data on a Graphical Liquid Crystal Display (GLCD).

#include 
                
                
#device *= 16 
#device ADC=10


#fuses HS, PUT, NOWDT, PROTECT, CPD, BROWNOUT, BORV45
#fuses NOSTVREN, NOLVP, NODEBUG, NOCPB, NOWRT, NOWRTC
#fuses NOWRTB,  NOWRTD, NOEBTR, NOEBTRB, NOOSCSEN       


#use delay (clock = 10MHz)      
                      

#define LARGE_LCD        
                     
#define ref_SPL			 94 
#define sensitivity    3.16
                    
                                                        
#include 
#include "HDM64GS192.c"             
#include "graphics.c"       
#include "background_art.c"           
                                             
                                                                     
unsigned char txt_dB_msg[30] = {"SPL Meter"};
unsigned char txt_dB_current[30];
unsigned char txt_dB_max[30];   
unsigned char txt_dB_min[30];    
unsigned char txt_dB_avg[30];    
                                
unsigned char x = 17;             

float dB_max = 0.0;
float dB_min = 100.0;
float dB_avg = 0.0;             
float dB_current = 0.0;        
                              
                                                                                                 
void setup();
void draw_background();                                                        
float adc_rms();    
void read_SPL();
void display_redings();            
void plot_data(); 
float map(float value, float x_min, float x_max, float y_min, float y_max);
                             

void main()                                                                                
{                        
    setup(); 
    draw_background();
    
    while(TRUE)            
    {
        read_SPL();
        display_redings();
        plot_data();     
        delay_ms(400);
    }                             
}                                                                                
                                                         
                                                  
void setup()                                                                   
{                                           
       disable_interrupts(global);  
       setup_WDT(WDT_off);
       setup_spi(spi_ss_disabled|spi_disabled);
       setup_timer_0(T0_internal);                              
       setup_timer_1(T1_disabled);                                                                         
       setup_timer_2(T2_disabled, 255, 1);     
       set_timer0(0);
       set_timer1(0);     
       set_timer2(0);
       setup_ccp1(ccp_off);       
       setup_ccp2(ccp_off);
       setup_ADC_ports(AN0);                        
       setup_ADC(ADC_clock_div_32);  
       set_ADC_channel(0);
       glcd_init(on);   
       glcd_fillscreen(0);            
       memset(txt_dB_current, 0, 30);     
       memset(txt_dB_max, 0, 30); 
       memset(txt_dB_min, 0, 30);    
       memset(txt_dB_avg, 0, 30);    
                                                     
       glcd_text57(130, 4, txt_dB_msg, 1, ON); 
}                               


void draw_background() 
{                            
   unsigned long n = 0;                      
   unsigned char i = 0;
   unsigned char j = 0;
   unsigned char cs = 0;        
   
   for(i = 0; i < 8; ++i)       
   {       
          output_low(GLCD_DI);                      
          glcd_writeByte(GLCD_LEFT, 0x40);   
          glcd_writeByte(GLCD_RIGHT, 0x40);       
          glcd_writeByte(GLCD_MID, 0x40);                          
          glcd_writeByte(GLCD_LEFT,(i | 0xB8));
          glcd_writeByte(GLCD_RIGHT,(i | 0xB8)); 
          glcd_writeByte(GLCD_MID,(i | 0xB8));     
          output_high(GLCD_DI);                         
          for(j = 0; j = 0) && (j  0)
    {
        read_adc(adc_start_only);
        while(!adc_done());
        rms = read_adc(adc_read_only);  
        tmp += (rms * rms);      
        samples--;      
    }                     
    tmp >>= 4;                           
    rms = (sqrt(tmp));     
    rms *= 0.004875; 
    
    if(rms <= 0)
    {
    	rms = 0.004875;
    }
    
    return rms;                    
}                                         


void read_SPL() 
{
   dB_current = adc_rms();
   db_current = (ref_SPL + 20 * log10(db_current / sensitivity)); 
   
   if(db_current = 99)
   {
       dB_current = 99;
   }
                                                 
   if(x > 125) 
   {
		db_max = 0.0;
		db_min = 100.0;    
		x = 17;            
   }
   
   if(dB_current > dB_max)      
   {     
		db_max = dB_current;  
   }              
   
   if(dB_current < dB_min)      
   {                    
		db_min = dB_current;
   }                   
   
   dB_avg = ((db_max + dB_min) * 0.5);   
}                                                                 
                     

void display_redings() 
{                                                          
    glcd_text57(130, 20, txt_dB_current, 1, OFF);  
    sprintf(txt_dB_current, "Cr:%2.1g dB", db_current);    
    glcd_text57(130, 20 , txt_dB_current, 1, ON);     
                                          
    glcd_text57(130, 30, txt_dB_max, 1, OFF);         
    sprintf(txt_dB_max, "Mx:%2.1g dB", dB_max);
    glcd_text57(130, 30 , txt_dB_max, 1, ON);     
    
    glcd_text57(130, 40, txt_dB_min, 1, OFF);         
    sprintf(txt_dB_min, "Mn:%2.1g dB", dB_min);
    glcd_text57(130, 40 , txt_dB_min, 1, ON); 
                      
    glcd_text57(130, 50, txt_dB_avg, 1, OFF);         
    sprintf(txt_dB_avg, "Av:%2.1g dB", dB_avg);
    glcd_text57(130, 50, txt_dB_avg, 1, ON); 
}                                
                            
                                                    
void plot_data()                                
{                                    
   unsigned char l = 0;
                           
   l = map(dB_current, 40, 99, 61, 2); 
   
   glcd_line(x, 2, x, 61, YES);   
   glcd_line(x, l, x, 61, NO); 
   
   x += 2;
}     

                                  
float map(float value, float x_min, float x_max, float y_min, float y_max)   
{                         
    return (y_min + (((y_max - y_min) / (x_max - x_min)) * (value - x_min)));
}          

Explaining the Code

To efficiently collect data, the Root-Mean-Square (RMS) value of 16 raw output samples from the Grove Sound Sensor is taken and the value is converted from ADC counts to voltage. This way of sampling data ensures cancellation of unnecessary noise and glitches.

float adc_rms()
{                         
    unsigned char samples = 16;
    register unsigned long long tmp = 0;
    register float rms = 0.0;          
                      
    while(samples > 0)
    {
        read_adc(adc_start_only);
        while(!adc_done());
        rms = read_adc(adc_read_only);  
        tmp += (rms * rms);      
        samples--;      
    }                     
    tmp >>= 4;                           
    rms = (sqrt(tmp));     
    rms *= 0.004875; 
    
    if(rms <= 0)
    {
        rms = 0.004875;
    }
    
    return rms;                    
}

The RMS voltage value from the sensor is then put to the derived formula discussed earlier. However, a microcontroller is not your ordinary calculator and it will behave erratically rather than showing an error symbol as in your calculator when it is forced to do a wrong calculation like dividing a value by zero. To avoid such incidents, the readings are checked for upper and lower hardware limits and confined if necessary, before processing and graphical representation.  

Further data processing is done to determine the maximum, minimum and average SPL values.

void read_SPL() 
{
   dB_current = adc_rms();
   db_current = (ref_SPL + 20 * log10(db_current / sensitivity)); 
   
   if(db_current = 99)
   {
       dB_current = 99;
   }
                                                 
   if(x > 125) 
   {
        db_max = 0.0;
        db_min = 100.0;    
        x = 17;            
   }
   
   if(dB_current > dB_max)      
   {     
        db_max = dB_current;  
   }              
   
   if(dB_current < dB_min)      
   {                    
        db_min = dB_current;
   }                   
   
   dB_avg = ((db_max + dB_min) * 0.5);   
}      

Now with the data processed, the data is ready for graphical presentation. The GLCD used here had a resolution of 192 x 64 pixels. It had three regions represented by three chip select (CS) pins. The first two regions were used for graphics while the third or the last region was used for text data.

Every graphical presentation has two components – one is the static background part and the other is the dynamic foreground part. In this code, the static part is the dB scale and the enclosure where the SPL bars graph is to be shown. This part as shown below is loaded only once right after initialization. The dynamic part changes with SPL values in the form of thin vertical bar graphs.  

void display_redings() 
{                                                          
    glcd_text57(130, 20, txt_dB_current, 1, OFF);  
    sprintf(txt_dB_current, "Cr:%2.1g dB", db_current);    
    glcd_text57(130, 20 , txt_dB_current, 1, ON);     
                                          
    glcd_text57(130, 30, txt_dB_max, 1, OFF);         
    sprintf(txt_dB_max, "Mx:%2.1g dB", dB_max);
    glcd_text57(130, 30 , txt_dB_max, 1, ON);     
    
    glcd_text57(130, 40, txt_dB_min, 1, OFF);         
    sprintf(txt_dB_min, "Mn:%2.1g dB", dB_min);
    glcd_text57(130, 40 , txt_dB_min, 1, ON); 
                      
    glcd_text57(130, 50, txt_dB_avg, 1, OFF);         
    sprintf(txt_dB_avg, "Av:%2.1g dB", dB_avg);
    glcd_text57(130, 50, txt_dB_avg, 1, ON); 
}                                
                                                                             
void plot_data()                                
{                                    
   unsigned char l = 0;
                           
   l = map(dB_current, 40, 99, 61, 2); 
   
   glcd_line(x, 2, x, 61, YES);   
   glcd_line(x, l, x, 61, NO); 
   
   x += 2;
}     

                                  
float map(float value, float x_min, float x_max, float y_min, float y_max)   
{                         
    return (y_min + (((y_max - y_min) / (x_max - x_min)) * (value - x_min)));
}      

As shown above the first function is responsible for displaying text data. The second function is responsible for plotting the bar graph. The region where a bar is to be plotted is cleared before plotting. This allows us not to fully refresh the screen. The map function is a straight-line equation solver and it comes useful in quickly translating from one range to another. For instance, in this case, the map function translates dB readings from 40 – 99 dB to 61 – 2 Y-coordinate position of the GLCD. Over 100 bars (X-coordinate points) together show the dB trend plot.

Improvements

A number of improvements can be made to the project. Some of these include:

  • Using a more sensitive and professional microphone.
  • Using a larger GLCD/TFT display.
  • Using a faster chip for faster data processing.
  • Adding a computer or mobile-phone application.
  • Adding features like FFT data processing, spectrum display, etc.
  • Adding a temperature and pressure sensor to take care of changes in medium with temperature and air pressure variations.

Demo

Resources:
http://www.libstock.com/projects/view/1690/graphical-spl-db-meter.

Author: Shawon M. Shahryiar

https://www.youtube.com/user/sshahryiar

https://www.facebook.com/MicroArena                                                               

26.05.2015

The post Making a SPL dB Meter appeared first on Embedded Lab.

Tinkering TI MSP430F5529

$
0
0

In my past tutorials on MSP430s, I demonstrated how to get started with MSP430 general purpose microcontrollers from Texas Instruments (TI). Those tutorials covered most aspects of low and mid-end MSP430G2xxx series microcontrollers. For those tutorials, TI’s official software suite – Code Composer Studio (CCS) – an Eclipse-based IDE and GRACE – a graphical peripheral initialization and configuration tool similar to STM32CubeMX were used. To me, those low and mid-end TIs chips are cool and offer best resources one can expect at affordable prices and small physical form-factors. I also briefly discussed about advanced MSP430 microcontrollers and the software resources needed to use them effectively. Given these factors, now it is high time that we start exploring an advanced 16-bit TI MSP430 microcontroller using a combination of past experiences and advanced tools. MSP430F5529 is such a robust high-end device and luckily it also comes with an affordable Launchpad board dedicated for it.

First of all, MSP430F5529 is a monster microcontroller because it offers large memory spaces– 128kB of flash and 8kB of RAM. Secondly, it is a 16-bit RISC microcontroller that host several advanced features like USB, DMA, 12-bit SAR ADC, analogue comparator, a real time clock (RTC), several sophisticated timers with multiple capture-compare I/O channels, etc. There is an on-chip hardware multiplier apart from several bidirectional general-purpose digital input-output (DIO/GPIO) pins, multipurpose communication peripherals (USCIs) and a highly complex clock and power system that can be tweaked to optimize the speed-power performances. The MSP430F5529LP microcontroller featured on-board MSP430F5529 Launchpad is a low power energy-efficient variant of MSP430F5529 and hence the LP designation at the end of the part naming. It can run at low voltage levels like 1.8V. In short, it is a hobbyist wildest dream come true.

MSP-EXP430F5529LP Launchpad Board

The MSP-EXP430F5529LP Launchpad board is just like other Launchpad boards, having similar form-factor and layout. Unlike the previously seen MSP-EXP430G2 Launchpads, its header pins are brought out using dual-sided male- female rail-connectors. These allow us to stack BoosterPacks on both sides.

Shown above and below are the pin maps of this Launchpad board.

The first pin map is useful for Energia users and the second one is useful for CCS users. I use a combination of both. Like other Launchpad boards, this Launchpad comes with a separable MSP-FET USB programmer and debugger interface. Since MSP430F5529 micro has USB hardware embedded in it, the on-board micro USB port can act as a physical USB port when developing USB-based hardware. The micro USB port isn’t, however, directly connected with the chip. Instead of that, the same USB port is connected to the on-board FET programmer and the MSP430F5529 chip via an on-board USB hub. The micro USB port also provides power to the board. The on-board MSP430 chip is powered by 3.3V LDO regulator. This LDO is not meant for driving large loads. There are sufficient power rail pins to hook-up low-power external devices like sensors, LCDs, etc without the need of power rail extenders.   

MSP430F5529 is not a low pin count micro and it has a sophisticated clock system that can be feed with both internal and external clock sources. Thus, unlike MSP430G2xxx Launchpads, this Launchpad comes with two external crystals – a 4MHz and a 32.768kHz crystal that are physically connected with GPIO pins. There are two user buttons and two LEDs also. Additionally, there are dedicated buttons for USB bootstrap loader and hardware reset. There are few jumpers that can be for measurements, debugging, programming, etc.

Shown below is the schematic of MSP430F5529LP Launchpad board:

TI MSP430Ware Driver Library and Other Stuffs

Like with other advanced microcontrollers from TI, there is no GRACE-like support that can be found for MSP430F5529 and similar devices. In fact, GRACE only supports few low-end and mid-end MSP430 microcontrollers and for some reason TI stopped updating this great tool since 2013. One probably reason may be the difficulty in developing a GUI-based application that can be used across all computer platforms (Windows, Linux, Mac OS, etc) while maintaining support for all ever-growing families of MSP430 devices.

Instead of upgrading GRACE, TI invested on something called MSP430Ware Driver Library or simply Driverlib. Driverlib replaces traditional register-level coding with a set of dedicated hardware libraries that reduces learning and implementation curves. These libraries consist of functions, variables and constants that have meaningful names. A sample code is shown below:

Timer_A_outputPWMParam outputPWMParam = {0}; 
outputPWMParam.clockSource = TIMER_A_CLOCKSOURCE_SMCLK; outputPWMParam.clockSourceDivider = TIMER_A_CLOCKSOURCE_DIVIDER_2; outputPWMParam.timerPeriod = 20000; 
outputPWMParam.compareRegister = TIMER_A_CAPTURECOMPARE_REGISTER_1; outputPWMParam.compareOutputMode = TIMER_A_OUTPUTMODE_RESET_SET; outputPWMParam.dutyCycle = 0; 
Timer_A_outputPWM(TIMER_A2_BASE, &outputPWMParam);

Note that there is no register-level coding involved and that’s the beauty. Here all the settings of a timer in PWM mode are being set. Each parameter is named with a meaningful name instead of some meaningless magic numbers and coding jargon.

Initially, I was doubtful and hesitant about using Driverlib because it hides away all the good-old register-level coding and at the same time it lacks good documentations. Comparing Driverlib documentation with the documentation of other manufacturers like STMicroelectronics or Microchip, I would say that Driverlib documentation is somewhat incomplete, clumsy and difficult to perceive. Adding to this is the fact that TI while developing and updating Driverlib made some changes to some function and definition names. This creates further confusion unless you are using the right version of documentation alongside the right Driverlib version.

Here’s one example. Both are correct and the compiler won’t throw any unexpected error but the first one is seen in older documentations while the second is used more often now.

Timer_A_startCounter(__MSP430_BASEADDRESS_T0A5__, TIMER_A_CONTINUOUS_MODE);

or

Timer_A_startCounter(TIMER_A0_BASE, TIMER_A_CONTINUOUS_MODE);

Issues like this one discussed above may not always be that simple and when it comes to older codes/examples, thing may become very ugly.

Another disadvantage that comes along with stuffs like Driverlib is resource management. Indeed, Driverlib reduces coding efforts a lot but at the expense of valuable and limited memory resources. Performance is also a bit compromised as execution speed is reduced due to additional hidden coding.

Despite these facts, I decided to go along with Driverlib because like other coders I didn’t want to spend time going through register-by-registers. In present day’s embedded system arena, people like the easy, effective and quick paths. It happened to me that after few trials I was able to master Driverlib and could also pinpoint issues with documentation and even practically get rid of the issues. As of this moment, I am enjoying it a lot and this whole tutorial is based on it.

Use Resource Explorer of Code Composer Studio (CCS) IDE or visit the following link:

http://www.ti.com/tool/MSPWARE

to get access to MSP430Ware. MSP430Ware contains lot example codes, libraries and other stuffs.

The rest of the software suite and hardware tools will be same as the ones used before. Grace support is unavailable and so it won’t be used.

Please also download MSP430F5529 documentations and Launchpad board resources.

Meet CrowPi2: An all-in-one portable Raspberry Pi laptop for STEAM learning and rapid prototyping

$
0
0

When it comes to STEAM (Science, technology, engineering, art and math) education, the Raspberry Pi is perhaps the most affordable, powerful, and versatile learning tool for all ages and skill levels. The versatility of this little computer comes from its incredible interconnectivity feature that offers direct connection of sensors and electromechanical components, thereby allowing the users to learn embedded hardware and programming in a very simple and engaging way. Since it first came out in 2012, the Raspberry Pi has been the most adventurous tool of teachers in the classroom to foster learning, awareness of computer programming, and extend the computer capabilities to multiple domains of education. The best way to learn Raspberry Pi is by doing projects. There are tons of starter kits available in the market for Raspberry Pi to satisfy both the programming and hardware needs. Elecrow’s has just announced a kickstarter campaign for their latest CrowPi2, which is an unique development platform combining the power of the Raspberry Pi computer with a bunch of sensors, displays, and software applications in a form of portable laptop suitable for easy learning and rapid prototyping.

CrowPi2: An all-in-one laptop and prototyping device based on Raspberry Pi

For more details on the introduction pricing and onboard features of CrowPi2, visit the Kickstarter page.


Elecrow launches Crowbits: Electronic Blocks for STEM Education

$
0
0

With the grand success of CROWPI2, an all-in-one portable Raspberry Pi Laptop development platform, Elecrow has now come up with Crowbits: easy-to-use electronic building blocks for young inventors. Crowbits include more than 80 modules that can be easily snapped through built-in magnets to create projects. It also incorporates a graphical programming software -Letsocde, where users can simply drag and drop blocks to develop applications.

Crowbits: easy-to-use electronic building blocks for young inventors

Exploring STC 8051 Microcontrollers – Coding

$
0
0

About STC8A8K64S4A12 Microcontroller and its Development Board

This is the continuation of my first post on STC 8051 Microcontrollers here.

Many Chinese microcontroller manufacturers develop awesome and cheap general-purpose MCUs using the popular 8051 architecture. There are many reasons for that but most importantly the 8051 architecture is a very common one that has been around for quite a long time. Secondly, manufacturing MCUs with 8051 DNA allows manufacturers to focus less on developing their own proprietary core and to give more effort in adding features. Holtek, Nuvoton, STC, etc are a few manufacturers to name.

Rather than mastering a good old 8051-based microcontroller like the AT89C52 or similar, it is better to learn something new that has many similarities with that architecture. As mentioned earlier, STC has various flavour of microcontrollers based on 8051 cores. STC8A8K64S4A12 of the STC8 family is one such example. Here for this documentation, I will be using this MCU specifically. In short, it is a beast as it offers lot of additional hardware that are usually not seen in regular 8051s. Some key features are listed below. The red box highlights the STC8A8K64S4A12 micro in particular.

I chose STC8A8K64S4A12 for this tutorial for all of its rich features. My favourite features include 12-bit ADC, multiple timers, reduced EMI feature and PCA module.

Now let’s see the naming convention of STC microcontrollers. The figure below shows us what the name of a STC8 micro means:

The name STC8A8K64S4A12 is quite a mouthful and given the nomenclature info, STC8A8K64SA412 is actually a STC8 series micro with on-chip 12-bit ADC, 8kB SRAM, 64kB code space and 4 hardware serial (UART) ports. Apart from these hardware thingies, the naming convention does not reveal other cool features, for if it had been so, the device name would be even longer.   

Initially I wanted to make this tutorial with the STC15F(L)204EA microcontroller but later I changed my mind because STC8A8K64S4A12 is much richer in hardware peripheral terms than STC15F(L)204EA, not to mention several similar hardware are present in both models of microcontroller. Since I planned to use STC8A8K64S4A12, I waited for the arrival of the board shown below before completing this work. Although this development board is an official board, it has been smartly designed for fast learning and rapid deployment of projects. As a matter of fact, if someone learns about this micro with this board, he/she will rule over all of STC’s 8051-based line-up.

This board has the following schematic and it will be needed throughout this tutorial.

On board, we have connectors for OLED display, GLCD, LCD, TFT Display, nRF24L01 transceiver, ESP8266 Wi-Fi module, etc. We also have on board W25x16 flash, 24C04 EEPROM, RS485 communication bridge and a CH340G USB-serial converter that doubles as an on-board programmer.

A Simple Solar Irradiation Measurement Technique

$
0
0

A pyranometer or solar irradiation tester is measurement tool that is a must-have for every professional in renewable energy sector. However, owing one is not easy because it is both expensive and rare. It is expensive because it uses highly calibrated components and it is rare because it is not an ordinary multi-meter that is available in common hardware shops.


Personally, I have a long professional career in the field of LED lighting, renewable energy (mainly solar), Lithium and industrial battery systems, electronics and embedded-systems. I have been at the core of designing, testing, commissioning and analyzing some of the largest solar power projects of my country – a privilege that only a few enjoyed. In many of my solar-energy endeavors, I encountered several occasions that required me to estimate solar isolation or solar irradiation. Such situations included testing solar PV module efficiencies, tracking solar system performance with seasons, weather, sky conditions, dust, Maximum Power Point Tracking (MPPT) algorithms of solar inverters, chargers, etc.

I wanted a simply way with which I can measure solar irradiance with some degree of accuracy. Having the advantage of working with raw solar cells, tools that test cells and complete PV modules combined with theoretical studies, I found out a rudimentary method of measuring solar irradiation.

In simple terms, solar insolation is defined as the amount of solar radiation incident on the surface of the earth in a day and is measured in kilowatt-hour per square meter per day (kWh/m²/day). Solar irradiation, on the other hand, is the power per unit area received from the Sun in the form of electromagnetic radiation as measured in the wavelength range of the measuring instrument. It is measured in watt per square meter (W/m²). The solar insolation is, therefore, an aggregate of solar irradiance over a day.   Sun energy collection by solar photovoltaic (PV) modules is depended on solar irradiance and this in turn has impact on devices using this energy. For example, in a cloudy day a solar battery charger may not be able to fully charge a battery attached to it. Likewise, in a sunny day the opposite may come true.

Solar PV Cells

For making a solar irradiation meter we would obviously need a solar cell. In terms of technology, there are three kinds of cells commonly available in the market and these are shown below.

Basic Solar Math

Typically, Earth-bound solar irradiation is about 1350 W/m². About 70 – 80% of this irradiation makes to Earth’s surface and rest is absorbed and reflected. Thus, the average irradiation at surface is about 1000 W/m². Typical cell surface area of a poly crystalline cell having dimensions 156 mm x 156 mm x 1mm is:

The theoretical or ideal wattage of such a cell should be:

However, the present-day cell power with current technology somewhere between 4 – 4.7 Wp. It is safe to assume an average wattage of a typical poly crystalline cell to be 4.3 Wp. Therefore, cell efficiency is calculated to be:

72 cells of 4.3 Wp capacity will form a complete solar PV module of 310Wp.

Likewise, the approximate area of a 310 Wp solar panel having dimensions (1960 mm x 991 mm x 40 mm) is:

The theoretical wattage that we should be getting with 1.942 m² area panel is found to be:

Therefore, module/panel efficiency is calculated to be:

Cell efficiency is always higher than module efficiency because in a complete PV module there are many areas where energy is not harvested. These areas include bus-bar links, cell-to-cell gaps, guard spaces, aluminum frame, etc. As technology improves, these efficiencies also improve.

Monocrystalline cells have same surface are but more wattage, typically between 5.0 to 5.5 Wp. We can assume an average cell wattage of 5.2 Wp.

Therefore, cell efficiency is calculated to be:

Typically, monocrystalline PV modules consist of 60 cells and so 60 such cells are arranged then the power of a complete PV module is calculated to be:

The area of that PV module having dimensions 1650 mm x 991 mm x 38 mm would be:

The theoretical wattage that we should be getting with 1.635 m² area panel is found to be:

Therefore, module/panel efficiency is calculated to be:

These calculations prove the following points:

  • Monocrystalline PV modules can harvest the same amount of energy as their polycrystalline counterparts with lesser area.
  • Monocrystalline cells are more efficient than poly crystalline cells.
  • Monocrystalline PV modules are more efficient than polycrystalline PV modules.
  • 156 mm x 156 mm cell dimension is typical but there are also cells of other dimensions like 125 mm x 125 mm x 1 mm.
  • Though I compared monocrystalline and polycrystalline cells and modules here the same points are true for thin film or amorphous cells.
  • Currently, there are more efficient cells and advanced technologies, and thus higher power PV modules with relatively smaller area. For instance, at present there are PV modules from Tier 1 manufacturers that have powers well above 600W but their physical dimensions are similar to 300Wp modules.

Solar PV modules can be generally categorized in four categories based on the number of solar cells in series and they usually have the following characteristics:

36 Cells

  • 36 x 0.6 V = 21.6V (i.e., 12V Panel)
  • VOC = 21.6V
  • VMPP = 16 – 18V
  • For modules from 10 – 120 Wp

60 Cells

  • 60 x 0.6 V = 36V
  • VOC = 36V
  • VMPP = 25 – 29V
  • For modules from 50 – 300+ Wp

72 Cells

  • 72 x 0.6 V = 43.2V (i.e., 24V Panel)
  • VOC = 43.2V
  • VMPP = 34 – 38V
  • For modules from 150 – 300+ Wp

96 Cells

  • 96 x 0.6 V = 57.6V (i.e., 24V Panel)
  • VOC = 57.6V
  • VMPP = 42 – 48V
  • For modules from 250 – 300+ Wp
  • Rare

One cell may not be fully used and could be cut with laser scribing machines to meet voltage and power requirements. Usually, the above-mentioned series connections are chosen because they are the standard ones. Once a cell series arrangement is chosen, the cells are cut accordingly to match required power. A by-cut cell has lesser area and therefore lesser power. Cutting a cell reduces its current and not its voltage. Thus, power is reduced. 36 cell modules are also sometimes referred as 12V panels although there is no 12V rating in them and this is because such modules can directly charge a 12V lead-acid battery but cannot charge 24V battery systems without the aid of battery chargers. Similarly, 72 cell modules are often referred as 24V panels.

Important Parameters of a Panel/Cell

Open Circuit Voltage (Voc) –no load/open-circuit voltage of cell/panel. 

Short Circuit Current (Isc) – current that will flow when the panel/cell is shorted in full sunlight.

Maximum Power Point Voltage (VMPP) – output voltage of a panel at maximum power point or at rated power point of the cell’s/panel’s characteristics curve.

Maximum Power Point Current (IMPP) – output current of a panel at maximum power point or at rated power point of the cell’s/panel’s characteristics curve.

Peak Power (Pmax) – the product of VMPP and IMPP. It is the rated power of a cell’s/PV module at STC. Its unit is watt-peak (WP) and not watts because this is the maximum or peak power of a cell/module.

Power Tolerance – percentage deviation of power.

STC stands for Standard Temperature Condition or simply lab conditions (25°C).

AM stands for Air Mass – This can be used to help characterize the solar spectrum after solar radiation has traveled through the atmosphere.

E stands for Standard Irradiation of 1000 W/m².

Shown below is the technical specification sticker of a 320 Wp JA Solar PV Module:

According to the sticker the PV module has the following specs at STC and irradiation, E = 1000 W/m²:

VOC = 46.12 V

VMPP = 37.28 V

Isc = 9.09 A

IMPP = 8.58 A

Pmax = 320 WP

From these specs we can estimate and deduce the followings:

VMPP-by-VOC ratio:

This value is always between 76 – 86%.

Similarly, IMPP-by-ISC ratio:

This value is always between 90 – 96%.

The product of these ratios should be as high as possible. Typical values range between 0.74 – 0.79. This figure represents fill-factor (FF) of a PV module or a PV cell. 

An ideal PV cell/module should have a characteristics curve represented by the green line. However, the actual line is represented by the blue one. Fill-Factor is best described as the percentage of ideality. Thus, the greater it is or the closer it is to 100 % the better is the cell/module.  

In this case, the FF is calculated to be 0.94 x 0.81 = 0.76 or 76%.

Number of cells is calculated to be:

Therefore, each cell has an open circuit voltage (VOC) of:

IMPP is 8.58 A and ISC is 9.09 A.

Cell power is calculated as follows:

Efficiencies are deduced as follows:

The I-V curves of the PV module shown below show the effect of solar irradiation on performance. It is clear that with variations in solar irradiation power production varies. The same is shown by the power curve. Voltage and current characteristics follow a similar shape.

The I-V curve shown below demonstrates the effect of temperature on performance. We can see that power production performance deteriorates with rise in temperature.

The characteristics curves of the 320 Wp JA Solar PV module are shown above. From the curves above we can clearly see the following points:

  • Current changes with irradiation but voltage changes slightly and so power generation depends on irradiation and current.
  • Current changes slightly with temperature but voltage changes significantly. At high temperatures, PV voltage decreases and this leads to change in Maximum Power Point (MPP) point. Thus, at high temperatures power generation decreases and vice-versa.
  • Vmpp-by-Voc ratio remains pretty much the same at all irradiation levels.
  • The curves are similar to that of a silicon diode and this is so because a cell is essentially a light sensitive silicon diode.

Impacts of Environment on PV

Effect of Temperature

Let us now see how temperature affects solar module performance. Remember that a solar cell is just like a silicon diode and we know that a silicon diode’s voltage changes with temperature. This change is about -2 mV/°C. Thus, the formula below demonstrates the effect of temperature:

Suppose the ambient temperature is 35°C and let us consider the same 320 Wp PV module having:

VOC at STC = 46.12V

VMPP at STC = 37.28V

Therefore, the temperature difference from STC is:

Under such circumstance the VMPP should be about 35V but it gets decreased more and fill-factor is ultimately affected.

So clearly there is a shift in VMPP which ultimately affects output power. This is also shown in the following graph:

Effect of Shading, Pollution and Dust

Power generation is also affected by shades, i.e., obstruction of sunlight. Obstacles can be anything from a thin stick to a large tree. Dust and pollution also contribute to shading on microscopic level. However, broadly speaking, shading can be categorized in two basic categories – Uniform Shading and Non-uniform Shading.

Uniform shading is best described as complete shading of all PV modules or cells of a solar system. This form of shading is mainly caused by dust, clouds and pollution. In such shading conditions, PV current is affected proportionately with the amount of shading, yielding to decreased power collection. However, fill-factor and efficiencies are literally unaffected.

Non-uniform shading, as its name suggests, is partial shading of some PV modules or cells of a solar system. Causes of such shading include buildings, trees, electric poles, etc. Non-uniform shading must be avoided because cells that receive less radiation than the others behave like loads, leading to formation of hotspots in long-run. Shadings as such result in decreased efficiency, current and power generation because of multiple wrong MPP points.

Effect of Other Variables

There are several other variables that have effect on solar energy collection. Variables and weather conditions like rain, humidity, snow, clouds, etc. affect energy harvest while cool, windy and sunny conditions have positive impact on energy harvest. These factors are region and weather dependent and cannot be generalized.

Basic Solar Geometry

Some basics of solar geometry is needed to be realized to efficiently design solar systems and how sun rays are reach earth. Nature follows a mathematical pattern that it never suddenly or abruptly alters. The movement of the sun and its position on the sky at any instance can be described by some simple trigonometric functions. The Earth not just orbits the Sun but it tilts and revolves about its axis. The daily rotation of the Earth about the axis through its North and South poles (also known as celestial poles) is perpendicular to the equator point, however it is not perpendicular to the plane of the orbit of the Earth. In fact, the measure of tilt or obliquity of the axis of the Earth to a line perpendicular to the plane of its orbit is currently about 23.5°.

The plane of the Sun is the plane parallel to the Earth’s celestial equator and through the center of the sun. The Earth passes alternately above and below this plane making one complete elliptic cycle every year.

In summer solstice, the Sun shines down most directly on the Tropic of Cancer in the northern hemisphere, making an angle δ = 23.5° with the equatorial plane. Likewise, in winter solstice, it shines on the Tropic of Capricorn, making an angle δ = -23.5° with the equatorial plane. In equinoxes, this angle δ is 0°. Here δ is called the angle of declination. In simple terms the angle of declination represents the amount of Earth’s tilt or obliquity.

The angle of declination, δ, for Nth day of a year can be deduced using the following formula:

Here, N = 1 represents 1st January.

The angle of declination has effect on day duration, sun travel path and thus it dictates how we should tilt PV modules with respect to ground with seasonal changes. 

The next most important thing to note is the geographical location in terms of GPS coordinates (longitude, λ East and latitude, ϕ North). This is because geo location also has impact on the aforementioned. Northern and Southern Hemispheres have several differences.

Shown below is a graphical representation of some other important solar angles. Here:

  • The angle of solar elevation, ά, is the angular measure of the Sun’s rays above the horizon.
  • The solar zenith angle, Z, is the angle between an imaginary point directly above a given location, on the imaginary celestial sphere. and the center of the Sun‘s disc. This is similar to tilt angle.

The azimuth angle, A, is a local angle between the direction of due North and that of the perpendicular projection of the Sun down onto the horizon line measured clockwise.

 

The angle of solar elevation, ά, at noon for location on Northern Hemisphere can be deduced as follows:

Similarly, for location on Southern Hemisphere, the angle of solar elevation at noon can be deduced as follows:

Here ϕ represents latitude and δ represents angle of declination. Zenith/Tilt angle is found to be:

Determining azimuth angle is not simple as additional info are needed. The first thing that we will need is to the sunrise equation. This is used to determine the local time of sunrise and sunset at a given latitude, ϕ at a given solar declination angle, δ. The sunrise equation is given by the formula below:

Here ω is the hour angle. ω is between -180° to 0° at sunrise and between 0° to 180° at sunset.

If [tan(ϕ).tan(δ)] ≥ 1, there is no sunset on that day. Likewise, if [tan(ϕ).tan(δ)] ≤ -1, there is no sunrise on that day.

The hour angle, ω is the angular distance between the meridian of the observer and the meridian whose

plane contains the sun. When the sun reaches its highest point in the sky at noon, the hour angle is zero. At this time the Sun is said to be ‘due south’ (or ‘due north’, in the Southern Hemisphere) since the meridian plane of the observer contains the Sun. On every hour the hour angle increases by 15°.

From hour angle, we can determine the local sunrise and sunset times as follows:

For a given location, hour angle and date the angle of solar elevation can be expressed as follows:

Since we have the hour angle info along with sunrise and sunset times, we can now determine the azimuth angle. It is expressed as follows:

Solving the above equation for A will yield in azimuth angle.

Knowing azimuth and solar elevation angles help us determine the length and location of the shadow of an object. These are important for solar installations.

The length of the shadow is as in the figure above is found to be:

From the geometric analysis, it can be shown that solar energy harvest will increase if PV modules can be arranged as such to follow the Sun. This is the concept of solar tracking system.

If the Sun can be tracked according to these math models, the maximum possible harvest can be obtained. However, some form of solar-tracking structures will be needed. An example of sun tracking system is shown above. This was designed by a friend of mine and myself back in 2012. He designed the mechanical section while I added the intelligence in form of embedded-system coding and tracking controller design.

Here’s a photo of the sun tracking controller that I designed. It used a sophisticate algorithm to track the sun.

Building the Device

In order to build the solar irradiance meter, we will need a solar cell or a PV module of known characteristics. Between a single cell and module, I would recommend for a cell because:

  • Cells have lower power than complete modules.
  • Cells have almost no framing structure.
  • Cells are small and light-weight.
  • Cells are individual and so there is no need to take account of series-parallel combination.

We would see why these are important as we move forward.

For the project, I used a cheap amorphous solar cell as shown in the photo below. It looks like a crystalline cell under my table lamp but actually it is an amorphous one. It can be purchased from AliExpress, Amazon or similar online platform.

With a cell test machine, it gave a characteristic curve and data as shown below:

From this I-V characteristics curve, we can see its electrical parameters. The cell has a physical dimension of 86mm x 56mm and so it has an area of 0.004816m².

From the calculation which I already discussed, irradiation measurement needs two known components – the total cell area and the maximum power it can generate. We know both of these data. Now we just have to formulate how we can use these data to measure irradiation.

Going back to a theory taught in the first year of electrical engineering, the maximum power transfer theorem, we know that to obtain maximum external power from a source with a finite internal resistance, the resistance of the load must equal the resistance of the source as viewed from its output terminals. This is the theory behind my irradiation measurement.

From the cell electrical characteristics data, we can find the ideal resistance that is needed to make this theory work.

It is important to note that we would just be focusing on maximum power point data only and that is because this is the maximum possible output that the cell will provide at the maximum irradiation level of 1000W/m². The ideal resistance is calculated as follows:

and so does the electric current. The voltage that would be induced across RMPP is proportional to this current because according to Ohms law:

The equivalent circuit is as shown below:

The boxed region is the electrical equivalent of a solar cell. At this point it may look complicated but we are not digging inside the box and so it can be considered as a mystery black box.

We know that:

We know the value of resistance and all we have to do is to measure the voltage (V) across the cell. We also know the area of the cell and so we can deduce the value of irradiation, E incident on the cell according to the formula:

Schematic

Code

#include "N76E003.h"
#include "SFR_Macro.h"
#include "Function_define.h"
#include "Common.h"
#include "Delay.h"
#include "soft_delay.h"
#include "LCD_2_Wire.h"

#define Vmpp                    5.162F
#define Impp                    0.056F
#define R_actual                (Vmpp / Impp)
#define R_fixed                 100.0F //    
#define R_Error                 (R_fixed / R_actual)
#define ADC_Max                 4095.0F
#define VDD                     3.3F
#define scale_factor            2.0F

#define cell_efficiency         0.065F // 6.5% (Typical Amorphous Cell Efficiency)
#define cell_length             0.0854F // 85.4mm as per inscription on the cell
#define cell_width              0.0563F // 56.3mm as per inscription on the cell
#define effective_area_factor   0.90F // Ignoring areas without cell, i.e. boundaries, frames, links, etc
#define cell_area               (cell_length * cell_width) // 0.004816 sq.m  
#define effective_cell_area     (cell_area * effective_area_factor * cell_efficiency) // 0.000281736 sq.m

void setup(void);
unsigned int ADC_read(void);
unsigned int ADC_average(void);
void lcd_print(unsigned char x_pos, unsigned char y_pos, unsigned int value);

void main(void)
{
  unsigned int ADC = 0;
  float v = 0;
  float V = 0;
  float P = 0;
  float E = 0;
 
  setup();
 
  while(1)
  {
      ADC = ADC_average();
      v = ((VDD * ADC) / ADC_Max);        
      V = (v * scale_factor);
      P = (((V * V) / R_fixed) * R_Error);
      E = (P / effective_cell_area);
       
      lcd_print(12, 0, (unsigned int)(P * 1000.0));
      lcd_print(12, 1, (unsigned int)E);
       
      delay_ms(100);
  };
}

void setup(void)
{
  LCD_init();
  LCD_clear_home();
  LCD_goto(0, 0);
  LCD_putstr("PV PWR mW:");
  LCD_goto(0, 1);
  LCD_putstr("E. W/sq.m:");
 
  Enable_ADC_AIN0;
}

unsigned int ADC_read(void)
{
  register unsigned int value = 0x0000;
 
  clr_ADCF;
  set_ADCS;                                
  while(ADCF == 0);
 
  value = ADCRH;
  value <<= 4;
  value |= ADCRL;
 
  return value;
}

unsigned int ADC_average(void)
{
    signed char samples = 16;
    unsigned long value = 0;
   
    while(samples > 0)
    {
        value += ((unsigned long)ADC_read());
        samples--;
    };
   
    value >>= 4;
   
    return ((unsigned int)value);
}

void lcd_print(unsigned char x_pos, unsigned char y_pos, unsigned int value)
{  
  unsigned char ch = 0;

    if((value > 999) && (value <= 9999))
    {
        ch = (((value % 10000) / 1000) + 0x30);
        LCD_goto(x_pos, y_pos);
        LCD_putchar(ch);
       
        ch = (((value % 1000) / 100) + 0x30);
        LCD_goto((x_pos + 1), y_pos);
        LCD_putchar(ch);
       
        ch = (((value % 100) / 10) + 0x30);
        LCD_goto((x_pos + 2), y_pos);
        LCD_putchar(ch);
       
        ch = ((value % 10) + 0x30);
        LCD_goto((x_pos + 3), y_pos);
        LCD_putchar(ch);
    }
   
    else if((value > 99) && (value <= 999))
    {
        ch = 0x20;
        LCD_goto(x_pos, y_pos);
        LCD_putchar(ch);
       
        ch = (((value % 1000) / 100) + 0x30);
        LCD_goto((x_pos + 1), y_pos);
        LCD_putchar(ch);
       
        ch = (((value % 100) / 10) + 0x30);
        LCD_goto((x_pos + 2), y_pos);
        LCD_putchar(ch);
       
        ch = ((value % 10) + 0x30);
        LCD_goto((x_pos + 3), y_pos);
        LCD_putchar(ch);
    }
   
    else if((value > 9) && (value <= 99))
    {
        ch = 0x20;
        LCD_goto(x_pos, y_pos);
        LCD_putchar(ch);
       
        ch = 0x20;
        LCD_goto((x_pos + 1), y_pos);
        LCD_putchar(ch);
       
        ch = (((value % 100) / 10) + 0x30);
        LCD_goto((x_pos + 2), y_pos);
        LCD_putchar(ch);
       
        ch = ((value % 10) + 0x30);
        LCD_goto((x_pos + 3), y_pos);
        LCD_putchar(ch);
    }
   
    else
    {
        ch = 0x20;
        LCD_goto(x_pos, y_pos);
        LCD_putchar(ch);
       
        ch = 0x20;
        LCD_goto((x_pos + 1), y_pos);
        LCD_putchar(ch);
       
        ch = 0x20;
        LCD_goto((x_pos + 2), y_pos);
        LCD_putchar(ch);
       
        ch = ((value % 10) + 0x30);
        LCD_goto((x_pos + 3), y_pos);
        LCD_putchar(ch);
    }  
}

Explanation

This project is completed with a Nuvoton N76E003 microcontroller. I chose this microcontroller because it is cheap and features a 12-bit ADC. The high-resolution ADC is the main reason for using it because we are dealing with low power and thus low voltage and current. The solar cell that I used is only about 300mW in terms of power. If the reader is new to Nuvoton N76E003 microcontroller, I strongly suggest going through my tutorials on this microcontroller here.

Let us first see what definitions have been used in the code. Most are self-explanatory.

#define Vmpp                    5.162F
#define Impp                    0.056F
#define R_actual                (Vmpp / Impp)
#define R_fixed                 100.0F //    
#define R_Error                 (R_fixed / R_actual)
#define ADC_Max                 4095.0F
#define VDD                     3.3F
#define scale_factor            2.0F

#define cell_efficiency         0.065F // 6.5% (Typical Amorphous Cell Efficiency)
#define cell_length             0.0854F // 85.4mm as per inscription on the cell
#define cell_width              0.0563F // 56.3mm as per inscription on the cell
#define effective_area_factor   0.90F // Ignoring areas without cell, i.e. boundaries, frames, links, etc
#define cell_area               (cell_length * cell_width) // 0.004816 sq.m  
#define effective_cell_area     (cell_area * effective_area_factor * cell_efficiency) // 0.000281736 sq.m

Obviously VMPP and IMPP are needed to calculate RMPP and this is called here as R_actual. R_fixed is the load resistor that is put in parallel to the solar cell and this is the load resistor needed to fulfill maximum power transfer theorem. Practically, it is not possible to get 92.18Ω easily and so instead of 92.18Ω ,a 100Ω (1% tolerance) resistor is placed in its place. The values are close enough and the difference between these values is about 8%. This difference is also taken into account in the calculations via the definition R_Error.

Thus, during calculation this amount of difference should be compensated for.

ADC_Max and VDD are maximum ADC count and supply voltage values respectively. These would be needed to find the voltage resolution that the ADC can measure.

Since 806µV is a pretty small figure, we can rest assure that very minute changes in solar irradiance will be taken into account during measurement.

scale_factor is the voltage divider ratio. The cell gives a maximum voltage output of 5.691V but N76E003 can measure up to 3.3V. Thus, a voltage divider is needed.

This is the maximum voltage that N76E003 will see when the cell reaches open-circuit voltage. Thus, the ADC input voltage needs to be scaled by a factor of 2 in order to back-calculate the cell voltage, hence the name scale_factor.

The next definitions are related to the cell that is used in this project. Cell length and width are defined and with these the area of the cell is calculated.

The physical cell area does not represent the total area that is sensitive to solar irradiation. There are spaces within this physical area that contain electrical links, bezels or frame, etc. Thus, a good estimate of effective solar sensitive cell area is about 90% of the physical cell area. This is defined as the effective_area_factor.    

Lastly, we have to take account of cell efficiency because we have seen that in a given area not all solar irradiation is absorbed and so the cell_efficiency is also defined. Typically, a thin film cell like the one I used in this project has an efficiency of 6 – 7% and so a good guess is 6.5%. All of the above parameters lead to effective cell area and it is calculated as follows:

So now the aforementioned equation becomes as follows:

The equations suggest that only by reading ADC input voltage we can compute both the power and solar irradiance.

The code initializes by initializing the I2C LCD, printing some fixed messages and enabling ADC pin 0.

void setup(void)
{
  LCD_init();
  LCD_clear_home();
  LCD_goto(0, 0);
  LCD_putstr("PV PWR mW:");
  LCD_goto(0, 1);
  LCD_putstr("E. W/sq.m:");
 
  Enable_ADC_AIN0;
}

The core components of the code are the functions related to ADC reading and averaging. The code below is responsible for ADC reading.

unsigned int ADC_read(void)
{
  register unsigned int value = 0x0000;
 
  clr_ADCF;
  set_ADCS;                                
  while(ADCF == 0);
 
  value = ADCRH;
  value <<= 4;
  value |= ADCRL;
 
  return value;
}

The following code does the ADC averaging part. Sixteen samples of ADC reading are taken and averaged. Signal averaging allows for the elimination of false and noisy data. The larger the number of samples, the more is the accuracy but having a larger sample collection lead to slower performance.

unsigned int ADC_average(void)
{
    signed char samples = 16;
    unsigned long value = 0;
   
    while(samples > 0)
    {
        value += ((unsigned long)ADC_read());
        samples--;
    };
   
    value >>= 4;
   
    return ((unsigned int)value);
}  

In the main, the ADC average is read and this is converted to voltage. The converted voltage is then upscaled by applying the scale_factor to get the actual cell voltage. With the actual voltage, power and irradiation are computed basing on the math aforementioned. The power and irradiation values are displayed on the I2C LCD and the process is repeated every 100ms.

ADC = ADC_average();
v = ((VDD * ADC) / ADC_Max);      
V = (v * scale_factor);
P = (((V * V) / R_fixed) * R_Error);
E = (P / effective_cell_area);
       
lcd_print(12, 0, (unsigned int)(P * 1000.0));
lcd_print(12, 1, (unsigned int)E);
       
delay_ms(100);

I have tested the device against a SM206-Solar solar irradiation meter and the results are fairly accurate enough for simple measurements.

Demo

Demo video links:

Improvements and Suggestions

Though this device is not a professional solar irradiation measurement meter, it is fair enough for simple estimations. In fact, SMA – a German manufacturer of solar inverter, solar charger and other smart solar solution uses a similar method with their SMA Sunny Weather Station. This integration allows them to monitor their power generation devices such as on-grid inverters, multi-cluster systems, etc. and optimize performances.  

There are rooms for lot of improvements and these are as follows:

  • A temperature sensor can be mounted on the backside of the solar cell. This sensor can take account of temperature variations and thereby compensate readings. We have seen that temperature affects solar cell performance and so temperature is a major influencing factor.
  • A smaller cell would have performed better but it would have needed additional amplifiers and higher resolution ADCs. A smaller cell will have lesser useless areas and weight. This would make the device more portable.
  • Using filters to filter out unnecessary components of solar spectrum.
  • Adding a tracking system to scan and align with the sun would result in determining maximum irradiation.

Project Code can be found here.

Happy coding.

Author: Shawon M. Shahryiar

https://www.facebook.com/groups/microarena

https://www.facebook.com/MicroArena                                                                     

28.02.2022

Inductance-Capacitance Measurement using PIC18 Microcontroller

$
0
0

When designing or debugging an electrical or electronics device, it is very important to know the values of the components that have been used on board. With a multimeter most of the components can be easily measured and identified but most ordinary multimeters do not have options to measure inductors and capacitors as this is rarely needed. However, without capacitors there are literally no circuits while complex circuits may have inductors in them. A LCR (inductor-capacitor-resistor) measurement meter can be used to determine the aforementioned components but usually such meters are pretty expensive.

Here, I will share a method with which we can measure inductors and capacitors with some degree of accuracy. For the purpose, we can use a common 8-bit microcontroller. I have used a PIC18F452. Of course, any other microcontroller will be equally suitable for the purpose as long as the calculations and procedures are maintained. We will also need a small external analogue circuit based on LM393 dual analogue comparator. This circuit is a simple Hatley oscillator. The idea is to measure the frequency or time period of the oscillator. When a new component such an inductor introduced to the oscillator, its frequency and therefore time period changes. We can then back-calculate component value.  

PIC18F452 is an 8-bit PIC18F series microcontroller. In terms of hardware is much more capable than the popular PIC16F877A or PIC16F887. However, we do not need any other hardware apart from a capture compare module (CCP) and a timer. The main reason to use a PIC18F is its operating clock speed which is 40MHz.

Schematic

Components

As shown in the circuit diagram all the components would be needed. Components L2 to L6 and C4 to C8 are test components. These are present only in simulation and not physically. Rotary switches SW1 and SW2 are only use in simulation schematic and are physically not present. A 2×16 alphanumerical LCD is used for displaying info. And a button connected to pin C0 is used for mode selection.

Code

 #include <18F452.h>  
 
#device *=16
 
                                                                       
#fuses NOWDT, PUT, H4, BROWNOUT, BORV42
#fuses NOLVP, NODEBUG, NOCPB, STVREN, CPD
#fuses PROTECT, NOWRT, NOWRTB, NOWRTC
#fuses NOWRTD, NOEBTR, NOEBTRB, NOOSCSEN
 
#use delay(clock = 40M)    

                                                                   
#include "lcd.c"    
 
                                       
#define mode_button      input(pin_C0)  
                                             
#define C_cal_in_nF         100.0
#define L_cal_in_uH         100.0              
#define pi                            3.142                                                  
#define cal_delay               1000  

#define scaling_factor_c    ((10.0 / (4.0 * pi * pi * L_cal_in_uH)))    
#define scaling_factor_l     ((10.0 / (4.0 * pi * pi * C_cal_in_nF)))          
                                                             
unsigned int32 overflow_count = 0;                                                      
unsigned int32 pulse_ticks = 0;                                                  
unsigned int16 start_time = 0;  
unsigned int16 end_time = 0;    
                            
                                                                                             
void setup(void);                                                                            
 
#int_TIMER1                                                                                            
void TMR_ISR(void)                
{                                                                                        
   overflow_count++;      
}              


#int_CCP1                                                
                                                 
void CCP_ISR(void)
{
   end_time = CCP_1;                                                                                        
   pulse_ticks = ((65536 * overflow_count) + end_time - start_time);
   start_time = end_time;
   overflow_count = 0;
}
 
void main(void)  
{
    short calibration_done = 0;                                                                                          
    unsigned char mode = 0;        
    unsigned long t = 0;      
    double ref = 0.0;
    double value = 0.0;
             
    setup();                                                                            
    while(TRUE)                          
    {                                                                    
        t = (pulse_ticks);  
        value = ((double)t * (double)t);
         
        if(mode_button == FALSE)
        {
           delay_ms(60);                                            
           while(mode_button == FALSE);    
           calibration_done = 0;
           mode++;
           
           if(mode > 1)
           {
               mode = 0;
           }
        }  
                             
        if(calibration_done == 0)
        {
            lcd_putc("\f");              
            lcd_gotoxy(1, 1);
            lcd_putc("Calibrating....");                                                              
            lcd_gotoxy(1, 2);              
            lcd_putc("Place no part.");                                                                
            delay_ms(cal_delay);        
            lcd_putc("\f");    
           
            if(mode == 0)
            {  
                ref = (value * scaling_factor_c);  
                lcd_gotoxy(1, 1);  
                lcd_putc("C.ref/nF:");        
                lcd_gotoxy(1, 2);
                printf(lcd_putc, "%3.1g   ", ref);  
                 
            }
           
            if(mode == 1)            
            {                                            
                ref = (value * scaling_factor_l);
                lcd_gotoxy(1, 1);  
                lcd_putc("L.ref/uH:");        
                lcd_gotoxy(1, 2);
                printf(lcd_putc, "%3.1g   ", ref);  
            }  
           
            delay_ms(cal_delay);        
            lcd_putc("\f");
                                                 
            calibration_done = 1;
        }
       
        else
        {  
            lcd_gotoxy(1, 1);  
           
            switch(mode)
            {
                case 1:
                {
                    value = (value * scaling_factor_c);  
                    lcd_putc("Ind./uH:");      
                                               
                    break;
                }  
                                         
                default:
                {  
                    value = (value * scaling_factor_l);  
                    lcd_putc("Cap./nF:");        
                             
                    break;
                }
            }
           
            value -= ref;
                     
            if((value < 0) || (value > 1000))            
            {              
               value = 0;
            }  
                         
            lcd_gotoxy(1, 2);                              
            printf(lcd_putc, "%3.1g         ", value);  
        }
     
        delay_ms(100);                  
    };
}
 
void setup(void)                                     
{                        
    setup_wdt(WDT_OFF);  
    setup_adc(ADC_OFF);
    setup_adc_ports(NO_ANALOGS);                                                                                    
    setup_spi(SPI_DISABLED);    
    setup_psp(PSP_DISABLED);                  
    setup_ccp1(CCP_CAPTURE_RE);
    setup_ccp2(CCP_OFF);
    setup_low_volt_detect(LVD_43);                                                
    setup_timer_0(T0_OFF | T0_8_BIT);
    setup_timer_1(T1_INTERNAL);  
    setup_timer_2(T2_DISABLED, T2_DIV_BY_1, 16);
    setup_timer_3(T3_DISABLED);  
    set_timer0(0);
    set_timer1(0);  
    set_timer2(0);                                            
    set_timer3(0);
    enable_interrupts(INT_CCP1);      
    enable_interrupts(INT_TIMER1);
    enable_interrupts(global);  
    lcd_init();
    lcd_putc("\f");                              
}        

Theory

We know that:

For a known set of L and C the equation above becomes:

We also know that the inductance of inductors adds when connected in series:

Similarly, the total capacitance of capacitors sums up when connected in parallel:

The above equation can be rearranged as follows:

We also know that:

Thus, the above equation for unknown capacitor becomes:

The same is equation also holds true for unknown inductor too:

Thus, by knowing two different frequencies or time periods, the value of any unknown capacitor or inductor can be determined.

A PIC18F452’s CCP1 module along with Timer 1 is used to capture the oscillations coming out of a Hartley oscillator. With nothing unknown connected except the 100nF and 100µH reference capacitor and inductor respectively, the reference oscillation frequency is about 50kHz (about 20µs time period). Whenever a new component is added this frequency changes.

Explanation

Basing on the theory discussed, we would need PIC18’s high processing speed along with a timer and capture module. We would also need an LCD to display results. The setup function shown below highlights these modules and their settings.

 void setup(void)                                             
{                        

    setup_ccp1(CCP_CAPTURE_RE);

    setup_timer_1(T1_INTERNAL);  

    set_timer1(0);  

    enable_interrupts(INT_CCP1);      
    enable_interrupts(INT_TIMER1);
    enable_interrupts(global);  
    lcd_init();
    lcd_putc("\f");      
}                        

CCP1 module is set for rising edge capture. This means that CCP1 will capture Timer 1’s count whenever it senses rising edges. CCP modules in PIC microcontrollers usually work Timer 1 module and so its count is captured. Capturing two consecutive rising edges result in period measurement of incoming waveform. This is what we would need the most.

To further make thing work apparently in concurrent manner, interrupts are used. CCP1 interrupt is triggered when there is a rising edge capture and Timer 1 interrupt is used to keep track of timer overflows. The current and previous capture counts are stored while taking care of Timer 1 overflow. These would be used to determine time period. Timer 1 will rarely overflow because it would only overflow if the incoming waveform has very low frequency and this would literally never happen.  

 #int_TIMER1                                                                                                                               
void TMR_ISR(void)                
{        
   overflow_count++;      
}              
                                                                                                       

#int_CCP1                                                
                                                 
void CCP_ISR(void)
{
   end_time = CCP_1;                                                                                        
   pulse_ticks = ((65536 * overflow_count) + end_time - start_time);
   start_time = end_time;
   overflow_count = 0;
}

PIC18’s PLL is used to upscale an external 10 MHz crystal oscillator clock to 40 MHz. This is reflected in the fuse bit and clock settings.

 #fuses H4 ….
 
#use delay(clock = 40M)    

However, PICs usually take 4 clock cycles per instruction and so the effective system clock frequency is 10 MHz.

Thus, one tick of Timer 1 is:

Thus, at the base frequency of 50 kHz (20 µs), Timer 1 would count:

Since the reference inductor and capacitor values are know the following equations simply as:

The same applies for inductor measurement too and the formula simplifies as shown below:

The fixed constants are defined in definitions on top of the code

 #define C_cal_in_nF         100.0 
#define L_cal_in_uH         100.0              
#define pi                  3.142                                                                                                                                            
#define cal_delay           1000  
                                                                             
#define scaling_factor_c    ((10.0 / (4.0 * pi * pi * L_cal_in_uH)))    
#define scaling_factor_l    ((10.0 / (4.0 * pi * pi * C_cal_in_nF)))      

Now let’s say we want to measure 220nF. So, for this amount of capacitance, the oscillator frequency would be:

Timer 1 would count 355 counts while capturing this frequency from the oscillator.

Now if we back-calculate using the formulae shown previously, the capacitor is found to be:

Yes, the reading is a bit off from actual value but close enough to actual value. The error occurs because timer counts cannot be anything other than integers. This error can be smartly compensated in code.

The push button connected to pin C0 is used to select either inductance or capacitance measurement mode. Every time the mode is changed, calibration would be needed. The calibration procedure is simple. We just have to leave our meter with nothing connected except the reference capacitor and inductor as shown the following circuit diagram. This is the Hartley oscillator and the core part of the entire device.

During this time, the reference component value is measured and saved. This value would be deducted during unknown component measurement. During calibration, the meter’s display would show instruction for not placing any external component.

After the calibration is completed, unknown components can be placed and measured accordingly. These are all what the program’s main loop does.

     while(TRUE)                          
    {                                                              
        t = (pulse_ticks);  
        value = ((double)t * (double)t);
         
        if(mode_button == FALSE)
        {
           delay_ms(60);                                            
           while(mode_button == FALSE);    
           calibration_done = 0;
           mode++;
           
           if(mode > 1)
           {
               mode = 0;
           }
        }  
                             
        if(calibration_done == 0)
        {
            lcd_putc("\f");              
            lcd_gotoxy(1, 1);
            lcd_putc("Calibrating....");                                                              
            lcd_gotoxy(1, 2);              
            lcd_putc("Place no part.");                                                                
            delay_ms(cal_delay);        
            lcd_putc("\f");    
           
            if(mode == 0)
            {  
                ref = (value * scaling_factor_c);  
                lcd_gotoxy(1, 1);  
                lcd_putc("C.ref/nF:");        
                lcd_gotoxy(1, 2);
                printf(lcd_putc, "%3.1g   ", ref);  
                 
            }
           
            if(mode == 1)            
            {                                            
                ref = (value * scaling_factor_l);
                lcd_gotoxy(1, 1);  
                lcd_putc("L.ref/uH:");        
                lcd_gotoxy(1, 2);
                printf(lcd_putc, "%3.1g   ", ref);  
            }  
           
            delay_ms(cal_delay);        
            lcd_putc("\f");
                                                 
            calibration_done = 1;
        }
       
        else
        {  
            lcd_gotoxy(1, 1);  
           
            switch(mode)
            {
                case 1:
                {
                    value = (value * scaling_factor_c);  
                    lcd_putc("Ind./uH:");      
                                               
                    break;
                }  
                                         
                default:
                {  
                    value = (value * scaling_factor_l);  
                    lcd_putc("Cap./nF:");        
           
                    break;
                }
            }
           
            value -= ref;
                     
            if((value < 0) || (value > 1000))            
            {              
               value = 0;
            }  
                         
            lcd_gotoxy(1, 2);                              
            printf(lcd_putc, "%3.1g         ", value);  
        }
     
        delay_ms(100);                  
    };

Some time-proven good-old tricks have been applied in this code:

  1. Instead of measuring frequency, time period is measured. This saved one calculation step because frequency is inverse of time period.
  • Rather than using math library, some values that need to raised by some power, is simply repeated multiplied. For example, the time period needs to be squared but instead it is multiplied by itself.
  • Definitions have been used in order to identify some important constant values and reduce calculation.
  • The use of global variables has been kept to a minimum. Although the code is nothing compared to the memory resources of the PIC18 microcontroller.
  • Interrupt methods have been employed to make the program efficient and as less blocking as possible.
  • Unused hardware peripherals have been reinitialized to reduce power consumption and other issues.

Demo

https://youtu.be/R6ZhQxcOhSo

Happy coding.

Author: Shawon M. Shahryiar

https://www.facebook.com/groups/microarena

https://www.facebook.com/MicroArena                                     

23.08.2022

Mastering the SiLabs C8051 Microcontroller

$
0
0

In a world where upgrades and advancements are constant, it is easy to overlook older technology in favour of newer, more advanced options. However, the case of 8051 microcontrollers defies this trend. Despite being considered relics of the past, there is still a significant demand for these microcontrollers. Manufacturers have revitalized the proven 8051 architecture by incorporating modern features such as ADCs and communication modules, transforming them into powerful, reliable and versatile devices.

Silicon Laboratories (SiLabs) is an American semiconductor-manufacturing company, similar to Microchip and STMicroelectronics. They are renowned for producing a wide range of semiconductor components, including both 8 and 32-bit microcontrollers. Notably, SiLabs is highly regarded for its RF chips and USB-Serial converters such as CP2102.

In terms of their 8-bit MCU product line-up, SiLabs offers microcontrollers based on the well-established 8051 architecture. However, their MCUs go beyond being simple, traditional 8051 devices. Like other manufacturers like Nuvoton and STC, SiLabs enhances their MCUs with additional modern hardware components such as DACs and communication peripherals.

SiLabs C8051 microcontrollers are recognized for their good performance, reliability, and scalability. They cater to the evolving needs of the embedded systems industry, whether it’s in the realm of consumer electronics, industrial automation, or smart home applications. These microcontrollers serve as a solid foundation for various projects, providing developers with a dependable and flexible platform.

Viewing all 713 articles
Browse latest View live