Download the UF2 file which has Wi-Fi and Bluetooth LE support.
Step 2: Put the UF2 file in your raspberry pi pico w
Push and hold the BOOTSEL button and plug your Pico into the USB port of your Raspberry Pi or other computer. Release the BOOTSEL button after your Pico is connected.
It will mount as a Mass Storage Device called RPI-RP2.
Drag and drop the MicroPython UF2 file onto the RPI-RP2 volume. Your Pico will reboot. You are now running MicroPython.
You can access the REPL via USB Serial.
Step 3: Save the following files in your raspberry pi pico w
As you can see in the image, these files should be saved on the raspberry pi pico w. As they will be imported as modules when you write your application code.
[ Click on the file names to see the complete code ]
ble_advertising.py
# Helpers for generating BLE advertising payloads.
from micropython import const
import struct
import bluetooth
# Advertising payloads are repeated packets of the following form:
# 1 byte data length (N + 1)
# 1 byte type (see constants below)
# N bytes type-specific data
_ADV_TYPE_FLAGS = const(0x01)
_ADV_TYPE_NAME = const(0x09)
_ADV_TYPE_UUID16_COMPLETE = const(0x3)
_ADV_TYPE_UUID32_COMPLETE = const(0x5)
_ADV_TYPE_UUID128_COMPLETE = const(0x7)
_ADV_TYPE_UUID16_MORE = const(0x2)
_ADV_TYPE_UUID32_MORE = const(0x4)
_ADV_TYPE_UUID128_MORE = const(0x6)
_ADV_TYPE_APPEARANCE = const(0x19)
# Generate a payload to be passed to gap_advertise(adv_data=...).
def advertising_payload(limited_disc=False, br_edr=False, name=None, services=None, appearance=0):
payload = bytearray()
def _append(adv_type, value):
nonlocal payload
payload += struct.pack("BB", len(value) + 1, adv_type) + value
_append(
_ADV_TYPE_FLAGS,
struct.pack("B", (0x01 if limited_disc else 0x02) + (0x18 if br_edr else 0x04)),
)
if name:
_append(_ADV_TYPE_NAME, name)
if services:
for uuid in services:
b = bytes(uuid)
if len(b) == 2:
_append(_ADV_TYPE_UUID16_COMPLETE, b)
elif len(b) == 4:
_append(_ADV_TYPE_UUID32_COMPLETE, b)
elif len(b) == 16:
_append(_ADV_TYPE_UUID128_COMPLETE, b)
# See org.bluetooth.characteristic.gap.appearance.xml
if appearance:
_append(_ADV_TYPE_APPEARANCE, struct.pack("<h", appearance))
return payload
def decode_field(payload, adv_type):
i = 0
result = []
while i + 1 < len(payload):
if payload[i + 1] == adv_type:
result.append(payload[i + 2 : i + payload[i] + 1])
i += 1 + payload[i]
return result
def decode_name(payload):
n = decode_field(payload, _ADV_TYPE_NAME)
return str(n[0], "utf-8") if n else ""
def decode_services(payload):
services = []
for u in decode_field(payload, _ADV_TYPE_UUID16_COMPLETE):
services.append(bluetooth.UUID(struct.unpack("<h", u)[0]))
for u in decode_field(payload, _ADV_TYPE_UUID32_COMPLETE):
services.append(bluetooth.UUID(struct.unpack("<d", u)[0]))
for u in decode_field(payload, _ADV_TYPE_UUID128_COMPLETE):
services.append(bluetooth.UUID(u))
return services
def demo():
payload = advertising_payload(
name="micropython",
services=[bluetooth.UUID(0x181A), bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")],
)
print(payload)
print(decode_name(payload))
print(decode_services(payload))
if __name__ == "__main__":
demo()
ble_simple_peripheral.py
# This example demonstrates a UART periperhal.
import bluetooth
import random
import struct
import time
from ble_advertising import advertising_payload
from micropython import const
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)
_FLAG_READ = const(0x0002)
_FLAG_WRITE_NO_RESPONSE = const(0x0004)
_FLAG_WRITE = const(0x0008)
_FLAG_NOTIFY = const(0x0010)
_UART_UUID = bluetooth.UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
_UART_TX = (
bluetooth.UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"),
_FLAG_READ | _FLAG_NOTIFY,
)
_UART_RX = (
bluetooth.UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"),
_FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE,
)
_UART_SERVICE = (
_UART_UUID,
(_UART_TX, _UART_RX),
)
class BLESimplePeripheral:
def __init__(self, ble, name="mpy-uart"):
self._ble = ble
self._ble.active(True)
self._ble.irq(self._irq)
((self._handle_tx, self._handle_rx),) = self._ble.gatts_register_services((_UART_SERVICE,))
self._connections = set()
self._write_callback = None
self._payload = advertising_payload(name=name, services=[_UART_UUID])
self._advertise()
def _irq(self, event, data):
# Track connections so we can send notifications.
if event == _IRQ_CENTRAL_CONNECT:
conn_handle, _, _ = data
print("New connection", conn_handle)
self._connections.add(conn_handle)
elif event == _IRQ_CENTRAL_DISCONNECT:
conn_handle, _, _ = data
print("Disconnected", conn_handle)
self._connections.remove(conn_handle)
# Start advertising again to allow a new connection.
self._advertise()
elif event == _IRQ_GATTS_WRITE:
conn_handle, value_handle = data
value = self._ble.gatts_read(value_handle)
if value_handle == self._handle_rx and self._write_callback:
self._write_callback(value)
def send(self, data):
for conn_handle in self._connections:
self._ble.gatts_notify(conn_handle, self._handle_tx, data)
def is_connected(self):
return len(self._connections) > 0
def _advertise(self, interval_us=500000):
print("Starting advertising")
self._ble.gap_advertise(interval_us, adv_data=self._payload)
def on_write(self, callback):
self._write_callback = callback
def demo():
ble = bluetooth.BLE()
p = BLESimplePeripheral(ble)
def on_rx(v):
print("RX", v)
p.on_write(on_rx)
i = 0
while True:
if p.is_connected():
# Short burst of queued notifications.
for _ in range(3):
data = str(i) + "_"
print("TX", data)
p.send(data)
i += 1
time.sleep_ms(100)
if __name__ == "__main__":
demo()
Step 4: Create a simple Bluetooth UART Code
which receives the message “Toggle\r\n” and Toggle the onboard LED and sends the state of the led back to the user.
# Import necessary modules
from machine import Pin
import bluetooth
from ble_simple_peripheral import BLESimplePeripheral
# Create a Bluetooth Low Energy (BLE) object
ble = bluetooth.BLE()
# Create an instance of the BLESimplePeripheral class with the BLE object
sp = BLESimplePeripheral(ble)
# Create a Pin object for the onboard LED, configure it as an output
led = Pin("LED", Pin.OUT)
# Initialize the LED state to 0 (off)
led_state = 0
xWR_flag = 0
# Define a callback function to handle received data
def on_rx(data):
print("Data received: ", data) # Print the received data
global led_state,xWR_flag # Access the global variable led_state
if data == b'toggle\r\n': # Check if the received data is "toggle"
led.value(not led_state) # Toggle the LED state (on/off)
led_state = 1 - led_state # Update the LED state
xWR_flag = 1
# Start an infinite loop
while True:
if sp.is_connected(): # Check if a BLE connection is established
sp.on_write(on_rx) # Set the callback function for data reception
if xWR_flag == 1:
# Create a message string
msg="LED STATE: "
# Send the message via BLE
sp.send(msg)
sp.send(str(led_state))
sp.send(str("\r\n"))
xWR_flag = 0
I have this MG90S small servo motor. It is supposed to go from 0° to 180°.
Because of their low cost, they have a short range which is less than 180°. Some go to 135°, 100° or 150°.
PWM signal required by servo to move.
Frequency = 50Hz Time Period = 0.02 Second or 20 mili second Pulse width range: 500us to 2500us
Properties of pulse width At 500us pulse width the servo position will be 0° At 2500us pulse width the servo position will be 180°
It is a good habit to check the servo controls before putting it in a project and make the adjustments in code or in hardware.
Hardware Connections
Raspberry Pi Pico
Servo Motor
GND
GND (Brown color wire)
Vsys
VCC (Red color wire)
GP9
Signal (Orange color wire)
Most small and micro Servo operate from 5V to 6V. If a servo is designed to operate at 3.3V always check it’s datasheet carefully.
Calculations
Raspberry pi pico has a 16 bit PWM controller. This means we set its frequency to 50Hz by pwm.freq(50) This means that one oscillation has 65535 steps
65535 steps = 20 ms
First we convert angles to pulse width time
pulse width = angle/90 + 0.5 then we convert pulse width to step count
steps count = ( 65535 * pulse width ) / 20
Code
import machine
import utime
Led_pin = 25
LED = machine.Pin(Led_pin, machine.Pin.OUT)
# Configure the GPIO pin for PWM
pwm_pin = machine.Pin(9)
pwm = machine.PWM(pwm_pin)
# Set the PWM frequency to 50 kHz
pwm.freq(50)
# Debug Message Print
debug = 0
time_delay = 1/10
increment_step = 100
decrement_step = -200
'''
# Function: deg_to_time
# Parameters: deg : degree (0 - 180)
# Description: This function takes the degree and translates them
to Pulse Width time.
For a servo motor we have to use a pulse width of 500us to 2500us
'''
def deg_to_time(deg):
temp = ((deg/90)+0.5)
if debug:print("deg ",deg," to timems: ",temp)
return temp
'''
# Function: timems_to_duty
# Parameters: timems : pulse width time in milli second
# Description: This function takes pulse width duration of 500us to 2500us.
and generates a duty cycle value.
'''
def timems_to_duty(timems):
temp = 20/timems
temp1 = 65535/temp
if debug:print("timems to duty: ",temp1)
return temp1
'''
# Function: set_pwm
# Parameters: duty : duty cycle value (0 - 65535)
# Description: This function takes a duty cycle value and set it for the PWM.
'''
def set_pwm(duty):
pwm.duty_u16(int(duty))
if debug:print("duty cycle: ",duty)
def angle_to_pwm(angle):
set_pwm(int(timems_to_duty(deg_to_time(angle))))
while(1):
# sweep from 0 to 180
for _ in range(0,180,1):
angle_to_pwm(_)
utime.sleep(time_delay)
# sweep from 180 to 0
for _ in range(180,0,-1):
angle_to_pwm(_)
utime.sleep(time_delay)
The PIC16F877A has a single ISR function which is called every time some things generate an interrupt. Inside the ISR you have to check for individual flag bits to know which peripheral has generated that interrupt.
/*
* File: main.c
* Author: abhay
*
* Created on July 15, 2023, 11:48 PM
*/
// PIC16F877A Configuration Bit Settings
// 'C' source line config statements
// CONFIG
#pragma config FOSC = HS // Oscillator Selection bits (HS oscillator)
#pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = OFF // Power-up Timer Enable bit (PWRT disabled)
#pragma config BOREN = OFF // Brown-out Reset Enable bit (BOR disabled)
#pragma config LVP = OFF // Low-Voltage (Single-Supply) In-Circuit Serial Programming Enable bit (RB3 is digital I/O, HV on MCLR must be used for programming)
#pragma config CPD = OFF // Data EEPROM Memory Code Protection bit (Data EEPROM code protection off)
#pragma config WRT = OFF // Flash Program Memory Write Enable bits (Write protection off; all program memory may be written to by EECON control)
#pragma config CP = OFF // Flash Program Memory Code Protection bit (Code protection off)
// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.
#define _XTAL_FREQ 16000000
#include <xc.h>
#include <pic16f877a.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include<string.h>
void GPIO_init(void);
void UART_init(void) ;
void tx(unsigned char a);
unsigned char rx();
void txstr(unsigned char *s);
uint8_t RX_chr;
void clear_flag(void);
unsigned char msgACK[16] = "Received";
void __interrupt() myISR() {
if (RCIF == 1) {
if (RCSTAbits.OERR) {
CREN = 0;
NOP();
CREN = 1;
}
RX_chr = RCREG;
RCIF = 0;
}
}
/*
*
*/
int main(int argc, char** argv) {
GPIO_init();
UART_init();
/*
* Interrupt Setup START
*/
INTCONbits.GIE = 1;
INTCONbits.PEIE = 1;
PIE1bits.TXIE = 0;
PIE1bits.RCIE = 1;
/*
* Interrupt Setup END
*/
while (1) {
if (RX_chr == '0') {
tx(RX_chr);
RD1 = 0 ; // LED OFF
txstr(msgACK);
clear_flag();
}
if (RX_chr == 'a') {
RD1 = 1; // LED ON
txstr(msgACK);
clear_flag();
}
}
return (EXIT_SUCCESS);
}
void clear_flag(void) {
RX_chr = 0;
}
void GPIO_init(void) {
TRISD &= ~(1 << 1);
RD1 = 0;
/*
* UART Pins Initialize
*/
TRISCbits.TRISC6 = 0; // TX
TRISCbits.TRISC7 = 1; //RX
}
void UART_init() {
TXSTAbits.CSRC = 0;
TXSTAbits.TX9 = 0;
TXSTAbits.TXEN = 1;
TXSTAbits.SYNC = 0;
/* BRGH - High Baud Rate Select Bit
* 1: High Speed
* 0: Low Speed
*/
TXSTAbits.BRGH = 1;
TXSTAbits.TRMT = 0;
TXSTAbits.TX9D = 0;
RCSTAbits.SPEN = 1;
RCSTAbits.RX9 = 0;
RCSTAbits.SREN = 0;
RCSTAbits.CREN = 1;
RCSTAbits.ADDEN = 0;
RCSTAbits.FERR = 0;
RCSTAbits.OERR = 0;
RCSTAbits.RX9D = 0;
/*
* Baud Rate Formula
* Asynchronous
* Baud Rate = Fosc / (64 (x + 1))
* Baud Rate x (64 (x+1)) = FOSC
* SPBRG= (x) = ( Fosc/( Baud_Rate * 64 ) ) - 1
*/
SPBRG = 103; // Baud Rate 9600
TXIF = RCIF = 0;
}
void tx(unsigned char a) {
while (TXIF == 0); // Wait till the transmitter register becomes empty
TXIF = 0; // Clear transmitter flag
TXREG = a; // load the char to be transmitted into transmit reg
}
unsigned char rx() {
while (!RCIF);
RCIF = 0;
return RCREG;
}
void txstr(unsigned char *s) {
while (*s) {
tx(*s++);
__delay_us(10);
}
}
If we were to use this LCD in 8-bit mode we have to use 8 GPIO pins for the DATA (D0-D7) and three GPIO Pins for control(R/S, R/W, E).
But in 4-bit mode, we use three control pins and only four data pins D4-D7 .
Schematics
In the above schematic, I have interfaced the LCD in 4-bit mode.
LCD BACKLIGHT I have connected a 220 ohm resistor to the K pin; since my lcd module has a 100 ohm resistor on board. It is labeled R8. So 100 + 220 = 320 ohm resistance total. If the LED takes approx. 3V forward volage then the current will be given by equation Current = (VCC – Forward Voltage) / Total resistance => (5v-3v)/320ohm = 2v / 320 ohm = 0.00625 A = 6.25 mA(approx.)
LCD Command
You can make your own commands using this table shown below.
CODE
main.hLCD_16x2.hboard.h
/*
* File: main.c
* Author: abhay
*
* Created on July 14, 2023, 12:05 AM
*/
// PIC16F877A Configuration Bit Settings
// 'C' source line config statements
// CONFIG
#pragma config FOSC = HS // Oscillator Selection bits (HS oscillator)
#pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = OFF // Power-up Timer Enable bit (PWRT disabled)
#pragma config BOREN = OFF // Brown-out Reset Enable bit (BOR disabled)
#pragma config LVP = OFF // Low-Voltage (Single-Supply) In-Circuit Serial Programming Enable bit (RB3 is digital I/O, HV on MCLR must be used for programming)
#pragma config CPD = OFF // Data EEPROM Memory Code Protection bit (Data EEPROM code protection off)
#pragma config WRT = OFF // Flash Program Memory Write Enable bits (Write protection off; all program memory may be written to by EECON control)
#pragma config CP = OFF // Flash Program Memory Code Protection bit (Code protection off)
// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.
#define _XTAL_FREQ 16000000
#include <xc.h>
#include <pic16f877a.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include "board.h"
#include "LCD_16x2.h"
#include<string.h>
char GPIO_init(void);
/*
*
*/
int main(int argc, char** argv) {
GPIO_init();
InitLCD(); // Initialize LCD in 8bit mode
//const char *msg = "Hello World!";
// const char *msg1 = "Abhay";
//WriteStringToLCD(msg1);
char lcd_buff1[16] = "Abhay Kant";
char lcd_buff2[16] = "Kant";
lcd_set_cursor(1, 1);
WriteDataToLCD('E');
WriteDataToLCD('X');
WriteDataToLCD('A');
WriteDataToLCD('S');
WriteDataToLCD('U');
WriteDataToLCD('B');
WriteDataToLCD('.');
WriteDataToLCD('C');
WriteDataToLCD('O');
WriteDataToLCD('M');
lcd_set_cursor(2, 1);
WriteStringToLCD(lcd_buff1);
while (1) {
}
return (EXIT_SUCCESS);
}
char GPIO_init(void) {
/*
* TRIS = Data Direction Register
* 0 = OUTPUT
* 1 = INPUT
* TRISD &= ~(1 << 1); // LED RD1 as OUTPUT
* TRISD1 = 0; // RD1 as OUTPUT
*/
LED_PORT_DIR &= ~(1 << 1);
LED_PIN = 0;
Relay_1a_DIR &= ~(1 << 0);
Relay_1a_PIN = 0;
Relay_1b_DIR &= ~(1 << 2);
Relay_1b_PIN = 0;
Relay_2a_DIR &= ~(1 << 4);
Relay_2a_PIN = 0;
Relay_2b_DIR &= ~(1 << 5);
Relay_2b_PIN = 0;
/*
* LCD Pins Initialize
*/
TRISB = 0;
return 0;
}
/*
* File: LCD_16x2.h
* Author: abhay
*
* Created on July 16, 2023, 6:21 PM
*/
#ifndef LCD_16X2_H
#define LCD_16X2_H
#include "board.h"
#ifdef __cplusplus
extern "C" {
#endif
/*
*Sr.No. Hex Code Command to LCD instruction Register
1 01 Clear display screen
2 02 Return home
3 04 Decrement cursor (shift cursor to left)
4 06 Increment cursor (shift cursor to right)
5 05 Shift display right
6 07 Shift display left
7 08 Display off, cursor off
8 0A Display off, cursor on
9 0C Display on, cursor off
10 0E Display on, cursor blinking off
11 0F Display on, cursor blinking on
12 10 Shift cursor position to left
13 14 Shift the cursor position to the right
14 18 Shift the entire display to the left
15 1C Shift the entire display to the right
16 80 Force cursor to the beginning ( 1st line)
17 C0 Force cursor to the beginning ( 2nd line)
18 38 2 lines and 5×7 matrix
*/
// Define Pins
#define LCD_RS RB1 // RS pin for LCD
#define LCD_RW RB2 // RS pin for LCD
#define LCD_E RB3 // Enable pin for LCD
#define LCD_Data_Bus_D4 RB4 // Data bus bit 4
#define LCD_Data_Bus_D5 RB5 // Data bus bit 5
#define LCD_Data_Bus_D6 RB6 // Data bus bit 6
#define LCD_Data_Bus_D7 RB7 // Data bus bit 7
// Define Pins direction registrers
#define LCD_E_Dir TRISB3
#define LCD_RS_Dir TRISB1
#define LCD_RW_Dir TRISB2
#define LCD_Data_Bus_Dir_D4 TRISB4
#define LCD_Data_Bus_Dir_D5 TRISB5
#define LCD_Data_Bus_Dir_D6 TRISB6
#define LCD_Data_Bus_Dir_D7 TRISB7
// Constants
#define E_Delay 1000
// Function Declarations
void WriteCommandToLCD(unsigned char);
void WriteDataToLCD(char);
void InitLCD(void);
void WriteStringToLCD(const char*);
void ClearLCDScreen(void);
void ToggleEpinOfLCD(void) {
LCD_E = 1; // Give a pulse on E pin
__delay_us(E_Delay); // so that LCD can latch the
LCD_E = 0; // data from data bus
__delay_us(E_Delay);
}
void WriteCommandToLCD(unsigned char Command) {
LCD_RS = 0; // It is a command
PORTB &= 0x0F; // Make Data pins zero
PORTB |= (Command & 0xF0); // Write Upper nibble of data
ToggleEpinOfLCD(); // Give pulse on E pin
PORTB &= 0x0F; // Make Data pins zero
PORTB |= ((Command << 4)&0xF0); // Write Lower nibble of data
ToggleEpinOfLCD(); // Give pulse on E pin
}
void WriteDataToLCD(char LCDChar) {
LCD_RS = 1; // It is data
PORTB &= 0x0F; // Make Data pins zero
PORTB |= (LCDChar & 0xF0); // Write Upper nibble of data
ToggleEpinOfLCD(); // Give pulse on E pin
PORTB &= 0x0F; // Make Data pins zero
PORTB |= ((LCDChar << 4)&0xF0); // Write Lower nibble of data
ToggleEpinOfLCD(); // Give pulse on E pin
}
void InitLCD(void) {
// Firstly make all pins output
LCD_E = 0; // E = 0
LCD_RS = 0; // RS = 0
LCD_Data_Bus_D4 = 0; // Data bus = 0
LCD_Data_Bus_D5 = 0; // Data bus = 0
LCD_Data_Bus_D6 = 0; // Data bus = 0
LCD_Data_Bus_D7 = 0; // Data bus = 0
LCD_E_Dir = 0; // Make Output
LCD_RS_Dir = 0; // Make Output
LCD_RW_Dir = 0;
LCD_RW = 0;
LCD_Data_Bus_Dir_D4 = 0; // Make Output
LCD_Data_Bus_Dir_D5 = 0; // Make Output
LCD_Data_Bus_Dir_D6 = 0; // Make Output
LCD_Data_Bus_Dir_D7 = 0; // Make Output
///////////////// Reset process from datasheet //////////////
__delay_ms(40);
PORTB &= 0x0F; // Make Data pins zero
PORTB |= 0x30; // Write 0x3 value on data bus
ToggleEpinOfLCD(); // Give pulse on E pin
__delay_ms(6);
PORTB &= 0x0F; // Make Data pins zero
PORTB |= 0x30; // Write 0x3 value on data bus
ToggleEpinOfLCD(); // Give pulse on E pin
__delay_us(300);
PORTB &= 0x0F; // Make Data pins zero
PORTB |= 0x30; // Write 0x3 value on data bus
ToggleEpinOfLCD(); // Give pulse on E pin
__delay_ms(2);
PORTB &= 0x0F; // Make Data pins zero
PORTB |= 0x20; // Write 0x2 value on data bus
ToggleEpinOfLCD(); // Give pulse on E pin
__delay_ms(2);
/////////////// Reset Process End ////////////////
WriteCommandToLCD(0x28); //function set
WriteCommandToLCD(0x0c); //display on,cursor off,blink off
WriteCommandToLCD(0x01); //clear display
WriteCommandToLCD(0x06); //entry mode, set increment
WriteCommandToLCD(0x0e); //display on,cursor on,blink off
WriteCommandToLCD(0x0f); //display on,cursor on,blink on
}
void WriteStringToLCD(const char *s) {
while (*s) {
WriteDataToLCD(*s++); // print first character on LCD
}
}
void ClearLCDScreen(void) // Clear the Screen and return cursor to zero position
{
WriteCommandToLCD(0x01); // Clear the screen
__delay_ms(2); // Delay for cursor to return at zero position
}
void lcd_set_cursor(uint8_t row, uint8_t col) {
if (row == 1) {
WriteCommandToLCD(0x80 | (col - 1));
} else if (row == 2) {
WriteCommandToLCD(0xC0 | (col - 1));
}
}
#ifdef __cplusplus
}
#endif
#endif /* LCD_16X2_H */
The PIC16F877A microcontroller is a popular choice for embedded systems development due to its versatility and ease of use. One of the essential aspects of working with microcontrollers is controlling General Purpose Input/Output (GPIO) pins. In this blog post, we will explore how to control the GPIO pins of the PIC16F877A using MPLAB X IDE, a powerful Integrated Development Environment.
Prerequisites: To follow along with this tutorial, you will need the following:
PIC16F877A microcontroller.
MPLAB X IDE installed on your computer.
MPLAB XC8 Compiler.
Step 1: Create a New Project Launch MPLAB X IDE and create a new project by navigating to File -> New Project. Select “Microchip Embedded” under “Categories” and “Standalone Project” under “Projects”. Choose the PIC16F877A as the device and specify a name and location for your project. Click “Finish” to create the project.
Step 2: Configure the GPIO Pins To control the GPIO pins, we need to configure them as inputs or outputs. In the project window, open the “main.c” source file. Locate the main function and add the necessary code to configure the GPIO pins.
To set a pin as an output, use the TRISx register, where x represents the port name (A, B, C, etc.). For example, to set RB0 as an output pin, use the following code:
TRISBbits.TRISB0 = 0; // Set RB0 as an output pin
To set a pin as an input, use the same TRISx register and set the corresponding bit to 1. For example, to set RA2 as an input pin, use the following code:
TRISAbits.TRISA2 = 1; // Set RA2 as an input pin
Step 3: Control the GPIO Pins Once the GPIO pins are configured, we can control their state by manipulating the corresponding PORT registers. To set an output pin high (logic level 1), write 1 to the corresponding bit in the PORT register. For example, to set RB0 high, use the following code:
PORTBbits.RB0 = 1; // Set RB0 high
To set an output pin low (logic level 0), write 0 to the corresponding bit in the PORT register. For example, to set RB0 low, use the following code:
PORTBbits.RB0 = 0; // Set RB0 low
To read the state of an input pin, you can directly access the corresponding PORT register. For example, to read the state of RD2, use the following code:
if (PORTAbits.RD2 == 1) {
// RD2 is high
} else {
// RD2 is low
}
PORTA: It is configured as an analog port by default. When you want to use it as a Digital I/O port, you have to configure the ADCON1 register.
Demo Code
/*
* File: main.c
* Author: abhay
*
* Created on July 14, 2023, 12:05 AM
*/
// PIC16F877A Configuration Bit Settings
// 'C' source line config statements
// CONFIG
#pragma config FOSC = HS // Oscillator Selection bits (HS oscillator)
#pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = OFF // Power-up Timer Enable bit (PWRT disabled)
#pragma config BOREN = OFF // Brown-out Reset Enable bit (BOR disabled)
#pragma config LVP = OFF // Low-Voltage (Single-Supply) In-Circuit Serial Programming Enable bit (RB3 is digital I/O, HV on MCLR must be used for programming)
#pragma config CPD = OFF // Data EEPROM Memory Code Protection bit (Data EEPROM code protection off)
#pragma config WRT = OFF // Flash Program Memory Write Enable bits (Write protection off; all program memory may be written to by EECON control)
#pragma config CP = OFF // Flash Program Memory Code Protection bit (Code protection off)
// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.
#define _XTAL_FREQ 16000000
#include <xc.h>
#include <pic16f877a.h>
#include <stdio.h>
#include <stdlib.h>
#include "board.h"
/*
*
*/
int main(int argc, char** argv) {
/*
* TRIS = Data Direction Register
* 0 = OUTPUT
* 1 = INPUT
*/
// Make the Pin 1 of PORT D as output
TRISD &= ~(1 << 1); // LED RD1 as OUTPUT
TRISD1 = 0; // RD1 as OUTPUT
// Make the Pin 0 of PORT A as digital input
ADCON1bits.PCFG0 = 0;
ADCON1bits.PCFG1 = 1;
ADCON1bits.PCFG2 = 1;
ADCON1bits.PCFG3 = 0;
TRISA0 = 1; // button
while (1) {
if((RA0 ) == 1)
{
RD1 = 1;
}
else {
PORTD &= ~(1<<1);
}
}
return (EXIT_SUCCESS);
}
Step 4: Build and Program the Microcontroller Now that we have written the code, it’s time to build and program the microcontroller. Connect your PIC16F877A microcontroller to your computer via a suitable programmer/debugger. Ensure that the proper hardware connections are made.
Step 5: Test the GPIO Control Once the programming is complete, disconnect the programming cable and power the microcontroller using an appropriate power supply. Connect LEDs, switches, or other devices to the configured GPIO pins. Execute the code on the microcontroller and observe the desired behavior of the GPIO pins based on the control logic implemented in your code.
Conclusion: Controlling the GPIO pins of the PIC16F877A microcontroller using MPLAB X IDE is a fundamental skill in embedded systems development. By following this step-by-step guide, you have learned how to configure the GPIO pins as inputs or outputs and manipulate their states using the appropriate registers. With this knowledge, you can now start building a wide range of projects that involve interacting with the external world through GPIO pins.
Remember to refer to the PIC16F877A datasheet for detailed information on register names, bit assignments, and other specific details related to the microcontroller.
Happy coding and exploring the world of embedded systems!
I have used this FAST PWM mode to trigger two interrupt service routines. The timer compares register A sets the frequency. I have not enabled the output compare pins. since it was used by some other peripheral. So by doing this, I generate a PWM signal by issuing a command in the ISR.
/*
* main.c
*
* Created: 10 July 2023 10:47:23 PM
* Author: abhay
*/
#define F_CPU 16000000
#include <xc.h>
#include <stdio.h>
#include "util/delay.h"
#include <avr/interrupt.h>
#include "uart.h"
#define LED_ON PORTB |= (1<<5)
#define LED_OFF PORTB &= ~(1<<5)
// Function to send a character via UART
int UART_putchar(char c, FILE *stream) {
if (c == '\n')
UART_putchar('\r', stream); // Add carriage return before newline
while (!(UCSR0A & (1 << UDRE0))); // Wait for the transmit buffer to be empty
UDR0 = c; // Transmit the character
return 0;
}
// Create a FILE structure to redirect the printf stream to UART
FILE uart_output = FDEV_SETUP_STREAM(UART_putchar, NULL, _FDEV_SETUP_WRITE);
ISR(TIMER3_COMPA_vect){
PORTD |= (1<<6);
}
ISR(TIMER3_COMPB_vect){
PORTD &= ~(1<<6);
}
int main(void)
{
USART_Init();
// Redirect stdout stream to UART
stdout = &uart_output;
DDRB |= (1<<5); // set Data direction to output for PB5
LED_OFF; // set output to high
DDRD |= (1 << 6); //set PD6 as output
/*
F_CPU = 16000000
Prescaler = 64
Frequency = 50Hz
Period = 0.020 s
step time = 1/(F_CPU/Prescaler) = 0.000004 s
number of steps = 0.020/0.000004 = 5000
*/
TCNT3 = 0; // Timer counter initial value = 0
// Output Compare A value = 5000 or 20 Milli second
OCR3A = 5000;
// Output Compare B value = 500 or 2 Milli second
OCR3B = 500;
// Fast PWM
TCCR1A |= (1 << WGM31)|(1 << WGM30);
// Prescaler: 64
TCCR3B |= (1 << WGM32)|(1<<WGM32)|(1 << CS31)|(1 << CS30);
// Enable Timer Interrupt for Overflow, Compare match A and Compare Match B
TIMSK3 |= (1 << OCIE3B)|(1 << OCIE3A)|(1<<TOIE3);
// Enable Global Interrupt
sei();
while(1)
{
OCR3B = 250;//5% 1ms
_delay_ms(500);
OCR3B = 375;//7.5% 1.5ms
_delay_ms(100);
OCR3B = 500;//10% 2ms
_delay_ms(500);
}
}
Timers are essential components in microcontrollers that allow precise timing and synchronization for various applications. The ATmega328PB microcontroller offers several timer/counters, including Timer/Counter 1 (TC1), which is a 16-bit timer with advanced features. In this blog post, I will explore how to utilize Timer 1 in CTC (Clear Timer on Compare Match) mode on the ATmega328PB microcontroller.
Hardware Setup
Before we proceed with the code, ensure you have the necessary hardware setup. You will need an ATmega328PB microcontroller, a 16MHz crystal oscillator, and any additional components required for your specific application. Connect the crystal oscillator to the XTAL1 and XTAL2 pins of the microcontroller to provide a stable clock signal.
UART Communication Initialization
In this example, we will utilize UART communication for debugging or output purposes. Make sure you have already implemented the necessary UART functions or library. The UART initialization code should include setting the baud rate, enabling the transmitter and receiver, and configuring the data format (e.g., number of data bits, parity, and stop bits). We will also redirect the stdout stream to the UART using the stdio.h library, allowing us to use the printf function for UART output.
Timer 1 Configuration in CTC Mode
Let’s dive into the code and configure Timer 1 in CTC mode. Here’s an example code snippet:
/*
* main.c
*
* Created: 7/9/2023 12:47:23 AM
* Author: abhay
*/
#define F_CPU 16000000
#include <xc.h>
#include <stdio.h>
#include "util/delay.h"
#include <avr/interrupt.h>
#include "uart.h"
// Function to send a character via UART
int UART_putchar(char c, FILE *stream) {
if (c == '\n')
UART_putchar('\r', stream); // Add carriage return before newline
while (!(UCSR0A & (1 << UDRE0))); // Wait for the transmit buffer to be empty
UDR0 = c; // Transmit the character
return 0;
}
// Create a FILE structure to redirect the printf stream to UART
FILE uart_output = FDEV_SETUP_STREAM(UART_putchar, NULL, _FDEV_SETUP_WRITE);
ISR(TIMER1_COMPA_vect){
printf("2. compare match A\n");
}
ISR(TIMER1_COMPB_vect){
printf("1. compare match B\n");
}
int main(void)
{
USART_Init();
// Redirect stdout stream to UART
stdout = &uart_output;
DDRB |= (1<<5); // set Data direction to output for PB5
PORTB |= (1<<5); // set output to high
/*
* Timer 1
* Mode of operation : CTC
* When Output Compare A register value equals the
* Timer Counter register (TCNT1) it resets the Timer-Counter-register value
* and generates a interrupt.
* Only OCR1A will reset the timer counter.
* OCR1B can be used to generate a compare match between TCNT1 = 0 and OCR1A
*
*/
TCNT1 = 0; // Timer counter initial value = 0
OCR1BH = 0x3D; // Output Compare B value = 0x3d09 or 1 second
OCR1BL = 0x09;
OCR1AH = 0x7a; // Output Compare A value = 0x7a12 or 2 second
OCR1AL = 0x12;
TCCR1B |= (1<<WGM02)|(1 << CS12)|(1 << CS10); // CTC Prescaler: 1024
TIMSK1 |= (1 << OCIE1B)|(1 << OCIE1A)|(1<<TOIE1);
sei(); // Enable Global Interrupt
while(1)
{
printf(" This is Main:\n");
_delay_ms(2500);
//TODO:: Please write your application code
}
}
In this code snippet, we first initialize the UART communication and redirect the stdout stream to the UART output using the FDEV_SETUP_STREAM macro. The UART_putchar function is used to send a character via UART, ensuring that newline characters (\n) are preceded by a carriage return character (\r) for proper line endings.
Next, we configure Timer/Counter 1 (TC1) for CTC mode and set the prescaler to 1024, which divides the clock frequency to generate a suitable timebase. The TCCR1A and TCCR1B registers are set accordingly.
We then set the compare values (OCR1A and OCR1B) to determine the time intervals at which we want to generate interrupts. In this example, OCR1A is set for 2 second delay, and OCR1B is set for approximately 1 seconds delay.
Finally, we enable the Timer/Counter TC1 compare match interrupts (OCIE1A and OCIE1B) using the TIMSK1 register, and we enable global interrupts with the sei() function.
Interrupt Service Routines (ISRs)
The code snippet defines two Interrupt Service Routines (ISRs): TIMER1_COMPA_vect and TIMER1_COMPB_vect. These ISRs will be executed when a compare match occurs for Output Compare A and Output Compare B, respectively. In this example, we use these ISRs to print messages to the UART output. You can modify these ISRs to perform any desired actions based on your specific application requirements.
Putting It All Together
Once you have set up the UART communication, configured Timer 1 in CTC mode, and defined the necessary ISRs, you can utilize the precise timing capabilities of Timer 1 in your main program loop. Use the printf function to output information via UART, and the compare match interrupts will handle the precise timing events.
while (1) {
printf(" This is Main:\n");
_delay_ms(2500);
// Additional code and operations
// ...
}
In the above example, the main program loop will execute continuously, printing “This is the main program loop” every 1 second using the printf function. The _delay_ms function provides a delay of 2500 milliseconds (2.5 second) between each iteration of the loop.
Conclusion
Utilizing Timer 1 in CTC mode on the ATmega328PB microcontroller provides precise timing capabilities for various applications. By configuring Timer 1, setting compare match values, and utilizing compare match interrupts, you can achieve accurate timing control in your embedded systems. When combined with UART communication, you can easily monitor and debug your code by printing relevant information via the UART interface.
Remember to consult the ATmega328PB datasheet and relevant documentation for more details on Timer 1, CTC mode, and other timer features. Ensure that you have correctly configured your hardware setup, including the crystal oscillator and UART connection, to match the code requirements.
Using Timer 1 in CTC mode
with UART communication opens up a range of possibilities for precise timing and debugging capabilities in your projects. Experiment with different compare match values and integrate this functionality into your applications to enhance timing accuracy and control.
The simplest mode of operation is the Normal mode (WGM0[2:0] = 0x0). In this mode, the counting direction is always up (incrementing), and no counter clear is performed. The counter simply overruns when it passes its maximum 8-bit value (TOP=0xFF) and then restarts from the bottom (0x00). In Normal mode operation, the Timer/Counter Overflow flag (TOV0) will be set in the same clock cycle in which the TCNT0 becomes zero. In this case, the TOV0 flag behaves like a ninth bit, except that it is only set, not cleared. However, combined with the timer overflow interrupt that automatically clears the TOV0 flag, the timer resolution can be increased by software. There are no special cases to consider in the Normal mode, a new counter value can be written any time.
The output compare unit can be used to generate interrupts at some given time. Using the output compare to generate waveforms in Normal mode is not recommended since this will occupy too much of the CPU time.
The counter will always count from 0 to 255 and then after 255 it will set the overflow bit and start the counting from 0.
Without Interrupt
We can use this to generate delays. But This is a type of blocking code.
/*
* main.c
*
* Created: 7/9/2023 12:47:23 AM
* Author: abhay
*/
#define F_CPU 16000000UL
#include <xc.h>
#include <util/delay.h>
void T0delay();
int main(void)
{
DDRB |= (1<<5);
PORTB |= (1<<5);
while(1)
{
PORTB |= (1<<PINB5);
T0delay();
PORTB &= ~(1<<PINB5);
T0delay();
//TODO:: Please write your application code
}
}
void T0delay()
{
/*
F_CPU/prescaler = clock for timer
1/clock for timer = timer step time
for 1 second the timer will take : 1/timer step time
16000000/1024 = 15625 clock
1/15625 = 0.000064 = 64us (counter step size)
64us x 256 = 0.016384 sec (0verflow time)
64us x 255 - tcnt +1 = 0.016384 sec (0verflow time)
64us x (255 - tcnt +1 )= x
tcnt = x / 64us ) -256
for 1 sec Delay
=> 1/0.016384 sec = 61.03515625
Taking only integer
so 61 overflows needs to occur for 0.999424 Seconds
1-0.999424 = 0.000576 seconds
0.000576/ (counter step size) = required steps
0.000576/0.00064 = 9 steps
Note: this will be close to 1 second. but it will be longer due to overhead added by the instructions.
*/
for(int i = 0; i< 61;i++){
TCNT0 = 0;
TCCR0A = 0;
TCCR0B |= (1<<CS00)|(1<<CS02); //prescaler 1024
while( (TIFR0 & 0x1) == 0); // Wait for overflow flag
TCCR0B = 0;
TIFR0 = 0x1;
}
//9 steps timer code
TCNT0 = 255-9;
TCCR0A = 0;
TCCR0B |= (1<<CS00)|(1<<CS02); //prescaler 1024
while( (TIFR0 & 0x1) == 0); // Wait for overflow flag
TCCR0B = 0;
TIFR0 = 0x1;
}
Normal Mode with Interrupt
#define F_CPU 16000000UL
#include <xc.h>
#include <avr/interrupt.h>
ISR(TIMER0_OVF_vect){
PORTB ^= (1<<PINB5); // Toggle the GPIO
}
int main(void)
{
DDRB |= (1<<5); // set Data direction to output for PB5
PORTB |= (1<<5); // set output to high
TCNT0 = 0;
TCCR0A = 0; // Normal Mode
TCCR0B |= (1<<CS00)|(1<<CS02); //prescaler 1024
TIMSK0 |= (1 << TOIE0); // Overflow Interrupt Enable Bit
sei(); // Enable Global Interrupt
while(1)
{
//TODO:: Please write your application code
}
}