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

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

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.

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.

Shown below is the official SDK board’s 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.

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:

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:

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:

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:

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

Thus, we can expect similar behaviour.

There are four GPIO modes and these are as follows:

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

Explanation

The Function_define BSP header file states GPIO modes as follows:

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:

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:

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

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.

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:

Demo

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.

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.

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:

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

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.

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

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

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.

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:

Demo

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:

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

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

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.

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

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:

The three sources are as follows:

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

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.

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

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.

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.

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:

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 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.

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 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.

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.

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.

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

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.

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.

Very nice and detailed tutorial. I haven’t seen much MCU tutorials as good as this.

Regarding the PCF8574 header file, I’d like to know how you arrived with the address 0x4E. By default ( no jumpers or switches enabled), my I2C board (backpack) has an address of 0x3F. Verified with I2C scanner and resulted 0x3F. Tried to modify the address at the PCF8574.h but all I get is a dotted pixels on first line of the lcd and blank on the second line.

Without the backpack, your example with the lcd library is ok.

Tried Arduino to check the backpack and lcd and both are fine.

Tried it with a different N76E003AT20 dev board, but has same problem. Also tried 4.7k pullups on sda and scl lines.

The 7-bit I2C address for PCF8574A is 0b0111xxx(R/W) while that for PCF8574 is 0x0100xxx(R/W)…. Both you and I are correct and are using different chips …. You considered only the 7 bits (0b0011111 = 0x3F) without the R/W bit and I considered all (0b01001110 = 0x4E)…. The I2C-LCD board I used has all address selectors (A0 – A1) pins pulled externally up and not soldered, resulting in 0x27 consider only 7-bits without R/W bit…. When left shifted by one bit 0x27 becomes 0x4E…. Have a look for yourself regarding addressing here….