Thursday, July 30, 2009

PIC 18f14k50 USB Interface Board: Part 3

Introduction
As promised, a blog about USB-HID devices. This blog will demonstrate how to create a USB-HID keyboard device running on the PIC 18f14k50 USB Interface Board. Making a HID device is a bit more complicated that getting the USB serial communication, becuase there are a wide range of HID devices, it is very hard to make a generic library. However, HID devices are nice, because you don't need a special drivers on the Host PC. There are many HID devices that can directly interact with the host operating systems, like keyboards, mice, joysticks etc, it is even possible to make a generic HID device, which can be controlled from a host application (but I will save that for another blog). Today were making a keyboard device, once you press the boot button it will generate a character. It is an educational example and does not have any practical use, but you can use it as a starting point to make for example a IR remote receiver that generate keystrokes.

The Code
I've splitted up the code description in a couple of section, and it will explain it step by step. We start of with the regular stuff, not very existing and I won't explain it in more detail

-- chip setup
include 18f14k50
pragma target clock 48_000_000

include delay

Before creating the USB descriptors, we need to include the USB constant definitions by

include usb_defs

Ok, that was the easy stuff. USB works with enpoints, and we have to indicate which endpoints will be enabled, where there are located and specify their size, and endpoint can be considered more or less a communcation channel between the Host PC and the PIC device. An endpoint can consist of an input endpoint (seen from the Host computer, so output for the PIC device) and an output endpoints (again, seen from the Host computer, so input for the PIC device). An USB device should always have input & output enpoint zero, which is used for e.g. exchanging device configuration settings. In this example we will have and additional enpoint, which is used for the exchange of HID reports (i.e. the keyboard information). Furthermore, the an enpoint consist of two parts, the Endpoint Descriptor and the Enpoint Buffers, the setup of the Endpoint Descriptors are taken care of by the USB library, however, the Enpoint buffers have to be setup by the user of the library. The following code shows how to enable the endpoints and Endpoints Buffers:

const bit USB_EP0 = 1
const byte USB_EP0_OUT_SIZE = 8
const word USB_EP0_OUT_ADDR = ( USB_BASE_ADDRESS + 0x0010 )
const byte USB_EP0_IN_SIZE = 8
const word USB_EP0_IN_ADDR = ( USB_EP0_OUT_ADDR + USB_EP0_OUT_SIZE )

const bit USB_EP1 = 1
const byte USB_EP1_OUT_SIZE = 8
const word USB_EP1_OUT_ADDR = ( USB_EP0_IN_ADDR + USB_EP0_IN_SIZE )
const byte USB_EP1_IN_SIZE = 8
const word USB_EP1_IN_ADDR = ( USB_EP1_OUT_ADDR + USB_EP1_OUT_SIZE )

const bit USB_EP2 = 0
const bit USB_EP3 = 0

var volatile byte usb_ep1in_buf[ 8 ] at USB_EP1_IN_ADDR


const bit USB_EPx = 1 indicates that the endpoint is enabled, if the endpoint is enabled the address of the Endpoint buffer must be specified, in addition, it should map within a special memory region. The USB hardware "talks" with the microcontroller via a dual access memory, this memory location is dependent on the PIC type, however its base addres USB_BASE_ADDRESS constant is defined usb_defs, so we can use this constant to setup the memory regions for the usb buffers independent of the PIC type. You might wonder why the enpoint buffers are not mapped at the beginning of the memory regions (USB_BASE_ADDRESS), this is because the USB library needs to setup the Endpoint descriptors, they shall be located starting at the beginning of the dual access memory. To calculate the start of the memory region that can be used for the Endpoint buffers, take the highest enpoint number, add one, and multiply it by 8. So in this example we have endpoint 0 and 1, so 2 x 8 = 16 (0x10 hex).

Well, that just beginning, next "record" we have to specify is the USB_DEVICE_DESCRIPTOR, it consists of 18 bytes and describes the device properties. Take a look at the usb specifiction (see http://www.usb.org/) for the details.



const byte USB_DEVICE_DESCRIPTOR[USB_DEVICE_DESCRIPTOR_SIZE] = {
USB_DEVICE_DESCRIPTOR_SIZE, -- 18 bytes long
USB_DT_DEVICE, -- DEVICE 01h
0x00,
0x02, -- usb version 2.00
0x00, -- class
0x00, -- subclass
0x00, -- protocol
USB_EP0_OUT_SIZE, -- max packet size for end point 0
0xd8,
0x04, -- Microchip's vendor
0x55,
0x00, -- Microchip keyboard demo
0x01,
0x00, -- version 1.0 of the product
0x01, -- string 1 for manufacturer
0x02, -- string 2 for product
0x00, -- string 3 for serial number
0x01 -- number of configurations
}

const byte USB_STRING0[] =
{
0x04, -- bLength
USB_DT_STRING, -- bDescriptorType
0x09, -- wLANGID[0] (low byte)
0x04 -- wLANGID[0] (high byte)
}

const byte USB_STRING1[0x36] =
{
0x36, -- bLength
USB_DT_STRING, -- bDescriptorType
"M", 0x00,
"i", 0x00,
"c", 0x00,
"r", 0x00,
"o", 0x00,
"c", 0x00,
"h", 0x00,
"i", 0x00,
"p", 0x00,
" ", 0x00,
"T", 0x00,
"e", 0x00,
"c", 0x00,
"h", 0x00,
"n", 0x00,
"o", 0x00,
"l", 0x00,
"o", 0x00,
"g", 0x00,
"y", 0x00,
",", 0x00,
" ", 0x00,
"I", 0x00,
"n", 0x00,
"c", 0x00,
".", 0x00
}

const byte USB_STRING2[42] =
{
42, -- bLength
USB_DT_STRING, -- bDescriptorType
"J", 0x00,
"A", 0x00,
"L", 0x00,
" ", 0x00,
"H", 0x00,
"I", 0x00,
"D", 0x00,
" ", 0x00,
"K", 0x00,
"e", 0x00,
"b", 0x00,
"o", 0x00,
"a", 0x00,
"r", 0x00,
" ", 0x00,
" ", 0x00,
"D", 0x00,
"e", 0x00,
"m", 0x00,
"o", 0x00
}


I'll not go into all the details of the USB descriptors, however, we need to specify USB_STRING0..3 because we filled in the USB string identifiers in the USB_DEVICE_DESCRIPTOR record. These string have to conform a predfined format, i.e. the first byte should specify the total record length, and the second byte indicates that it is a string USB_DT_STRING, furthermore, it has to be a unicode string, therefore after each character a 0x00 value needs to be defined. USB_String0 is a special string, since it specifies the language code, which is set to US English. Furthermore we specified there is one configuration record (last byte in the USB_DEVICE_DESCRIPTOR record.

The USB_CONFIGURATION_DESCRIPTOR record specifies the properties of the USB device, again the details are available the USB specification at http://www.usb.org/. High level description is that were setting up one configuration record for a HID device, which specifies the HID interface by pointing to the HID descriptor and it will have one endpoint (is number of enpoint exluding enpoint 0 which should always be present).










const byte USB_HID_REPORT1[]=
{
0x05, 0x01, -- USAGE_PAGE (Generic Desktop)
0x09, 0x06, -- USAGE (Keyboard)
0xa1, 0x01, -- COLLECTION (Application)
0x05, 0x07, -- USAGE_PAGE (Keyboard)
0x19, 0xe0, -- USAGE_MINIMUM (Keyboard LeftControl)
0x29, 0xe7, -- USAGE_MAXIMUM (Keyboard Right GUI)
0x15, 0x00, -- LOGICAL_MINIMUM (0)
0x25, 0x01, -- LOGICAL_MAXIMUM (1)
0x75, 0x01, -- REPORT_SIZE (1)
0x95, 0x08, -- REPORT_COUNT (8)
0x81, 0x02, -- INPUT (Data,Var,Abs)
0x95, 0x01, -- REPORT_COUNT (1)
0x75, 0x08, -- REPORT_SIZE (8)
0x81, 0x03, -- INPUT (Cnst,Var,Abs)
0x95, 0x05, -- REPORT_COUNT (5)
0x75, 0x01, -- REPORT_SIZE (1)
0x05, 0x08, -- USAGE_PAGE (LEDs)
0x19, 0x01, -- USAGE_MINIMUM (Num Lock)
0x29, 0x05, -- USAGE_MAXIMUM (Kana)
0x91, 0x02, -- OUTPUT (Data,Var,Abs)
0x95, 0x01, -- REPORT_COUNT (1)
0x75, 0x03, -- REPORT_SIZE (3)
0x91, 0x03, -- OUTPUT (Cnst,Var,Abs)
0x95, 0x06, -- REPORT_COUNT (6)
0x75, 0x08, -- REPORT_SIZE (8)
0x15, 0x00, -- LOGICAL_MINIMUM (0)
0x25, 0x65, -- LOGICAL_MAXIMUM (101)
0x05, 0x07, -- USAGE_PAGE (Keyboard)
0x19, 0x00, -- USAGE_MINIMUM (Reserved (no event indicated))
0x29, 0x65, -- USAGE_MAXIMUM (Keyboard Application)
0x81, 0x00, -- INPUT (Data,Ary,Abs)
0xc0
}
const USB_CONFIGURATION_DESCRIPTOR_SIZE = 0x09 + 0x09 + 0x09 + 0x07

const byte USB_CONFIGURATION_DESCRIPTOR[ USB_CONFIGURATION_DESCRIPTOR_SIZE ]=
{
-- configuration descriptor - - - - - - - - - -
0x09, -- length,
USB_DT_CONFIGURATION, -- descriptor_type

USB_CONFIGURATION_DESCRIPTOR_SIZE,
0x00, -- total_length;

0x01, -- num_interfaces,
0x01, -- configuration_value,
0x00, -- configuration_string_id,
0b10000000, -- attributes (bus powered, no remote wake up)
100, -- max_power; (200ma)

-- interface descriptor - - - - - - - - - - - -
0x09, -- length,
USB_DT_INTERFACE, -- descriptor_type,
0x00, -- interface_number, (starts at zero)
0x00, -- alternate_setting, (no alternatives)
0x01, -- num_endpoints,
USB_HID_INTF, -- interface_class, (HID)
USB_BOOT_INTF_SUBCLASS, -- interface_subclass, (boot)
USB_HID_PROTOCOL_KEYBOARD, -- interface_protocol, (keyboard)
0x00, -- interface_string_id;

-- hid descriptor - - - - - - - - - - - - - - -
0x09, -- length,
USB_DT_HID, -- descriptor_type;
0x11,
0x01, -- hid_spec in BCD (1.11)
0x00, -- country_code, (0=not country specific)
0x01, -- num_class_descriptors, (1)
USB_DT_HID_REPORT, -- class_descriptor_type; (0x22 = report)
(count( USB_HID_REPORT1 ) & 0xFF ),
(count( USB_HID_REPORT1 ) >> 8 ),

0x07, -- length,
USB_DT_ENDPOINT, -- descriptor_type,
0b10000001, -- endpoint_address, (Endpoint 1, IN)
USB_EPT_INT, -- attributes; (Interrupt)
USB_EP1_IN_SIZE,
0x00, -- max_packet_size
0x01 -- interval (1ms)
}










The HID Report specifies which information can be exchanged between the Host PC and the
USB device. There are seperate specification available which describes the details of a HID report (again, see http://www.usb.org/).
Making a HID report can be quite cumbersome, and there are even tools available to make HID report. Anyhow, for now assume that the specified HID report can exchange keyboard related information between the Host PC and the USB device


OK that was the toughest part, now some real JAL code.
Tekengrootte


-- include remaining USB libraries
include usb_drv_core
include usb_drv

-- HID report layout (see USB specification for more details)
-- 0 Modifier byte
-- bit
-- 0 LEFT CTRL
-- 1 LEFT SHIFT
-- 2 LEFT ALT
-- 3 LEFT GUI
-- 4 RIGHT CTRL
-- 5 RIGHT SHIFT
-- 6 RIGHT ALT
-- 7 RIGHT GUI
-- 1 reserved
-- 2 keycode array (0)
-- 3 keycode array (1)
-- 4 keycode array (2)
-- 5 keycode array (3)
-- 6 keycode array (4)
-- 7 keycode array (5)
var byte hid_report_in[8] = { 0,0,0,0,0,0,0,0 }

var byte key_value = 4
var bit sw3 is pin_c2
var bit latched_sw3 = sw3


-- disable analog unit, all ports set to digital
enable_digital_io()

-- setup the USB device
usb_setup()

-- enable USB device
usb_enable_module()

-- set input where switch is attached to input
pin_c2_direction = input

-- main loop
forever loop
-- poll the usb ISR function on a regular base, in order to
-- serve the USB requests
usb_handle_isr()

-- check if USB device has been configured by the HOST
if usb_is_configured() then

-- prepare the HID buffer
if ( sw3 != latched_sw3 ) then

latched_sw3 = sw3

if ( sw3 == low )then
hid_report_in[2] = key_value
key_value = key_value + 1
if ( key_value == 40 ) then
key_value = 4
end if
else
hid_report_in[2] = 0
end if

-- Send the 8 byte packet over USB to the host.
usb_send_data(USB_HID_ENDPOINT, hid_report_in, count( hid_report_in ) , low )

-- debounce
delay_1ms(50)
end if
end if
end loop





First the remaing USB libraries have to be included, since the library need to have access
to the device and configuration records it has to be included after the definition of these recods. Next an array of 8 bytes is defined for the HID report were sending out from the device towards the HOST. The first byte is a modifier key, while the remaing bytes will contain the key character(s). More details can be found in the USB HID keyboard specification (see also www.usb.org). Keep in mind though that the characters you send are not ASCII character, for example the "a" character has a byte value of 4.
Next the switch is initialized, and all inputs are set to digital io. Then the USB will be initialized by calling the usb_setup() and usb_enable_module() procedures. Then the main loop, first it has to call the usb_handle_isr() procedure on a regular base, in order to service the USB hardware. Next it will check if the USB interface is already configure by the host, via the usb_is_configured() function. If the USB device is recognized by the Host OS, it will check for state changes of the program switch of the PIC 18f14k50 USB Interface Board. If the value has changed and the button is pressed, it will send a HID report (i.e. it will send a character to the Host PC). The value of the character starts at 4 (the "a" character) and will increase till the "z" character, when reached it will start over again with the "a" character. It will fill in the character in the HID report and then it sends the HID report via USB towards the Host PC by calling the usb_send_data() procedure. Which will send the data via endpoint 1.

Executing the demo
Compile the code (available at the Jallib SVN repository projects\pic14k50_usb_io\examples\blog_part3.jal) using the -loader18 flag. Reboot the PIC 18f14k50 USB Interface Board, while holding down the program and reset switch together and release the reset switch while keep holding down the program switch. After a couple of seconds you can release the program switch. Start the PDFSUSB.exe application to download the hex file. Once downloaded, press the reset button once again. After a couple of seconds the Host PC should recgonize the a new "Keyboard". Wait a couple of seconds, start an editor, for example notepad on the Host PC, if you now press & release the program button on the board, it should type a character within the editor, next time you press & release the program button it will show you the next character of the alphabet.

OK, a long story this time, I know not everything has been explained in detail yet about the Jallib USB libraries, but maybe in a future blog I can tell more about the inner working of the Jallib USB libraries. The next episode will likely building a Generic HID device.

Wednesday, July 29, 2009

PIC 18f14k50 USB Interface Board: Part 4

Introduction
This is a small follow up regarding the Jallib USB Serial library. Today's blog I'm showing how to make a more serious application with the PIC 18f14k50 USB Interface Board, namely a USB-RS-232 converter. I will go trough the source and explain the JAL code step by step.

Source code
The beginning is the same as Part 2, namely defining the target clockand including the device file.

1:  -- chip setup
2: include 18f14k50
3:
4: -- even though the external crystal is 20 MHz, the configuration is such that
5: -- the CPU clock is derived from the 96 Mhz PLL clock (div2), therefore set
6: -- target frequency to 48 MHz
7: pragma target clock 48_000_000
8:

Serial interrupt handling
Then we need to declare a forward procedure in order to receive the line setting change events, which will be discussed a bit later on. In addition the usb_serial library has to be included as well as the print library so for now we have to add the following code (line 1-7)

1:  -- forward procedure declaration
2: procedure usb_cdc_line_coding_changed_callback()
3:
4: -- include standard libraries
5: include delay
6: include usb_serial
7: include print
8:
9: procedure _serial_receive_interrupt_handler() is
10: pragma interrupt
11:
12:
13: if (PIR1_RCIF == TRUE) then -- UART receive interrupt
14:
15: if ((RCSTA_OERR == TRUE) | (RCSTA_FERR == TRUE)) then -- frame/overr error
16: var byte x = RCREG -- flush hardware buffer
17: while RCSTA_OERR == TRUE loop -- overrun state
18: RCSTA_CREN = FALSE -- disable UART
19: RCSTA_CREN = TRUE -- re-enable UART
20: x = RCREG -- \ flush hardware buffers
21: x = RCREG -- /
22: end loop -- until no more overrun
23:
24: else -- data without errors
25: var byte ch = RCREG
26: -- usb_serial_data = RCREG
27: usb_cdc_putc( ch )
28: end if
29: end if
30: end procedure
31:
32:
From line 9-30, the interrupt routine to handle the receiving of the UART characters. As discussed in the previous blog, the USB library is not interrupt driven, therefore the USB service procedure must be called on a regular base to service the USB hardware. Servicing can be time consuming therefore I've decided to make the RS-232 serial interface (UART) interrupt driven, so were sure that were not lossing incoming data on the UART. The incoming data will be placed directly in the USB transmit ring buffer by calling the usb_cdc_putc( ch ) procedure. The USB-Serial serving procedure checks if there is data present in the ring buffer.


Handling baudrate settings
Next code section is the handling of the baudrate settings. The USB serial library has a callback function usb_cdc_line_coding_changed_callback() , when defined, it will be called each time the USB Host will request the line coding setting (which contains the baudrate, parity, stopbits etc). The procedure entry is on line 45 in the code below, the procedure checks/limits the baudrate settings and will change the baudrate (line 2-23) and by re-initializing the UART hardware (line 27-42).

1:  -- procedure to change the baudrate settings of the UART
2: procedure change_baudrate( dword in baud_rate) is
3:
4: -- compiler issue with -const-detect
5: if true then
6: var dword fosc_div4 = dword( target_clock ) / 4
7: else
8: var dword fosc_div4
9: fosc_div4 = dword( target_clock )
10: fosc_div4 = fosc_div4 / 4
11: end if
12:
13: var dword div_factor = fosc_div4 / baud_rate - 1
14: var word div_wfactor = word( div_factor )
15: var byte div_btfactor[2] at div_wfactor
16:
17: TXSTA_BRGH = true
18: BAUDCON_BRG16 = true
19:
20: SPBRGH = div_btfactor[1]
21: SPBRG = div_btfactor[0]
22:
23: end procedure
24:
25:
26: -- Initializes the serial port, calculates baudrate registers.
27: procedure serial_hw_init() is
28: RCSTA = 0b0000_0000 -- reset
29: RCSTA_SPEN = enabled -- serial port enable
30: RCSTA_CREN = enabled -- continuous receive enable
31:
32: TXSTA = 0b0000_0100 -- reset (16 bit, asyn)
33: TXSTA_TXEN = enabled -- UART transmit enabled
34: -- TXSTA_SYNC = true
35: TXSTA_BRGH = true
36: BAUDCON_BRG16 = true
37:
38: PIE1_RCIE = enabled -- UART receive int. enable
39: -- (PIE1_TXIE dynamically)
40: INTCON_PEIE = enabled -- periferal
41: INTCON_GIE = enabled -- general
42: end procedure
43:
44: -- callback procedure, is called if the USB Host changes the line settings
45: procedure usb_cdc_line_coding_changed_callback() is
46:
47: if ( cdc_line_coding_dte_rate > 115200 ) then
48: cdc_line_coding_dte_rate = 115200
49: end if
50: change_baudrate( cdc_line_coding_dte_rate )
51: serial_hw_init()
52: end procedure
53:

The main loop
Finally we need to initalize the hardware and define the main loop. First all IO ports are set to digital (line 3) and need to initialize the USB serial library (line 6). Next a character is defined which holds the receiving character.

1:  
2: -- disable analog
3: enable_digital_io()
4:
5: -- setup the USB serial library
6: usb_serial_init()
7:
8: var byte ch
9:
10: -- main loop
11: forever loop
12: -- poll the usb ISR function on a regular base, in order to
13: -- serve the USB requests
14: usb_serial_flush()
15:
16: -- check if USB device has been configured by the HOST
17: if ( usb_cdc_line_status() != 0x00 ) then
18:
19: -- check for input character
20: while usb_serial_read( ch ) loop
21: -- echo input character
22: TXREG = ch
23: while ! PIR1_TXIF loop end loop
24: end loop
25:
26: end if
27: end loop
Finally the main loop, as said before, the usb_serial_flush() procedure has to be called on a regular base. This function takes care of transmitting the character to the USB Host that have been received by the UART, and it also takes care of putting the character that are sent from USB Host into a receive buffer.

A rough calculation on the maximum interval time between two USB Serial flush:
By default the USB-Serial library will create a ring buffer of 32 bytes, which is for this application more than sufficient (max UART speed is ~10.000 characters/second, so USB Serial flush must be called at least every 10.000 / 32 = 3.2 ms. So there is enough time left to perform additional taks

On line 17 it checks if the USB Host is connected, if so, it will put all character that were sent by the USB Host in the UART Transmit register (line 19-24).

That's about it, this blog showed how to create a simple USB-RS-232 convert, using the JALLB USB serial librray and the PIC 18f14k50 USB Interface Board. Possible extentions are to include RS-232 handshaking and to take other RS-232 line setting into account.



Sunday, July 26, 2009

PIC 18f14k50 USB Interface Board: Part 2

This is the second episode about the PIC 18f14k50 USB Interface Board. This time I want to take about the usage of the JALLIB USB-Serial library and how it can be used in conjuction with the interface board.

Introduction
USB is a fairly complex interface, besides the basic interface, there are defined a couple of higher level "protocols" which are target for specific "device" functions. Commonly used interfaces are the Human Interface Device(s) types, to support devices like keyboards, joysticks and other devices, another commenly used interace is the Communication Device Class (CDC), which is developped for a broad range of (serial) communication devices, like modems, RS-232 converters etc. HID devices will be discussed in another blog, today we're looking a bit closer on the CDC interface and how to talk to the PIC device with a terminal application via the USB interface.

In order to reduce the USB learning curve, we've devevlopped a special library (USB-Serial) to easily create firmware that behaves like a RS-232 port on the Host side (virtual COM port). The big advantage is that you can still use the good old terminal application(s) to talk with your PIC device via the USB interface (both on Windows and Linux and probably other Host operating systems)

As an example we use the 18f14k50_board_test.jal, we start of simple and show step by step how to use the usb_serial library.

USB Serial example code
We start of with a basic application, the code is listed below:

include 18f14k50                    -- target PICmicro

--
-- This program assumes a 12 MHz resonator or crystal
-- is connected to pins OSC1 and OSC2.
-- Configuration bits may cause a different frequency!
pragma target clock 48_000_000 -- oscillator frequency

-- include libraries
include usb_serial
include print

-- initialize the USB serial library
usb_serial_init()


-- main loop
forever loop

var byte ch

-- Service USB, call on a regular base to keep communcaiton going
usb_serial_flush()

-- check for input character
if usb_serial_read( ch ) then
-- nothing spectecular, echo ch + 1
if ch == "?" then
const byte str1[] = "Hi there \r\n"
print_string( usb_serial_data, str1 )
else
usb_serial_data = ch
end if
end if
end loop
--




The code is pretty straight forward, first the device file is included and the target clock speed is set. Then the usb_serial and print JALLIB libraries are included. The next step is to initalize the USB serial library by calling the usb_serial_init() procedure. Within the main loop, the usb_serial_flush() is called, so the USB serial Interface Enginge (SIE) is serviced on a regular base (the USB libraries are not interrupt driven, therefore this procedure must be called regulary.
The call towards usb_serial_read will check if there are character recieved from the USB Host, if a character has been received it will be echoed, unless the "?" character is send, then it will use print the "Hi there" string. Notice that the usb_serial library can be used in conjuction with the print and serial libraries, if the usb_serial_data argument is passed along (as the first argument) with the print related procedure calls.

Get it running
The code is compiled with jalv2 using the -loader18 -no-fuse. After compilation one can reset the PIC 18f14k50 USB Interface Board and holding down the program button. Now the PDFSUSB application can be used to download the HEX file file, after download reset the board. If everything went OK, it should recognize the USB-CDC device and ask for drivers (Windows only), select the driver (which can be downloaded from
http://groups.google.com/group/jallib/files, file usb_cdc_drivers.zip), by selecting the win2k_xp_vista32_64 directory. After installtion of the drivers, the host operating system will create a new (virtual) serial port.
After the (virtual) serial port is created, open the serial port with your favorite terminal appliction. The actual serial port settings does not really matter, once the serial port is opened, you can any send character, the character will be echoed unless the "?" mark character is sent, the it will respond by the "Hi There" sentence.

Wrap Up
So far about the usb_serial library, it shows how easy it is to create an PIC firmware application using which can comminicate with a Host PC via the USB interface. Upcoming blog will address more advanced usage of the USB interface by creating a HID device,.

Thursday, July 23, 2009

Jaluino: an Arduino-like board, power by jal


Jaluino, with user guide, schematics, PCB, shields, etc... can now be reached on the following sites:

Main site: http://jaluino.org
Repository: http://jaluino.googlecode.com
Group: http://groups.google.com/group/jaluino


It's been a while I'd like to build and provide the same kind of board as Arduino's, but PIC-based. And powered by Jalv2 and jallib, of course. The reasons are pretty clear: as we share our jal code (software), why don't we also share our boards & PCB (hardware) ? Jaluino to the rescue !

There can be many benefits having such a board, such a common hardware. For instance, everybody would be able to get a board, and experiment with it (educational benefits). Keeping hardware problems away helps focusing on the real experiment's content. Jaluino provides a base, a context on which anyone can build extra boards. These can then be shared with everyone, easily, since Jaluino is the common base. This is what Arduino's shields are about, and what would be nice to reproduce.


Which PIC should be used for Jaluino ?

The size and the features are what matter. A USB capable PIC is somewhat mandatory, it opens oportunities for nice experimentation. 18F PICs seem to be the candidates. What about these three PICs:

  • 18F14K50 : 20-pin, cheap, lots of features
  • 18F2550 : 28-pin
  • 18F4550 : big fat 40-pin, the max for a PDIP package.
Following Arduino's ideas, several Jaluino boards could be provided:

  • Jaluino Mini : 18F14K50 based, breadboard layout (no shield connectors, just plug your Jaluino board on a breadboard). Closed to Pinguino, but with a smaller PIC
  • Jaluino Medium : 18F2550 based. Board contains connectors on which we can plug shields. A breadboard version can also be built (same as Pinguino)
  • Jaluino Maxi : 18F4550 based. Board is "shieldable" too, a breadboard version may not be a good idea, as the PIC is quite big a would require a rather large breadboard.

(we'd definitively need to work on marketing names for "Mini", "Medium", "Maxi"...)


Connectors for shields

Despite several Jaluino boards would be available, shield connectors have to be compatible. Ex: a shield for 18F2550 can be used with a 18F4550 (the reverse isn't true...). As Arduino Diecilima's shields are compatible with Arduino Mega. This can be done by extanding the 18F2550 layout by other connectors.

Comparing 18F2550 and 18F4550 pins, we can see one is perfectly included into the other. In green, pins from 18F2550 which be found in 18F4550, in blue, 18F4550 only pins. Circle-red pins are important features: serial, I²C, USB and OSC. Fortunatly, these are carried by the exact same pins between 18F2550 and 18F4550.

PIC 18F2550


PIC 18F4550

(also note 18F877 could be used and plugged into a Jaluino Maxi as OSC, RX/TX are carried by the same pins as in 18F4550. Only I²C -- and USB of course -- pins aren't compatible).

The general layout for Jaluino Medium and Jaluino Maxi would look like this (not scaled...). Orange connector is dedicated to power supply. Green connectors are exactly on the place between Maxi and Medium Layout, these are the compatible pins. Blue connectors export 18F4550 pins only.


Other connectors...

Several other connectors would be needed:

  • USB, of course...
  • I²C : following Joep's suggestion, a 5-pin header (+9V,+5V, GND, SCL, SDA) can be used a dedicated connector, making easier connection between boards & other parts using this protocol
  • Serial : again, following Joep's suggestion, a 4-pin header (+5V, GND, TX, RX) would allow to plug any serial module (see later)
  • ICSP : to easily program the chip, even if no bootloader is present

Serial : different options

In addition to dedicated USB connector, a serial connector would allow easy comms. from Jaluino to the external world. But what type of connector to provide ? DB9 ? USB via USB-to-serial conversion ?

Here are the different alternatives, depending on budget limits, easiness to make, and parts used to build this module:

  • USB-to-Serial via FDTI chip: widely parts, but FTDI chip only exist on SSOP package, hard to solder (but not impossible). Small. USB can be used to power the board.
  • USB-to-Serial, using a 18F14K50 : easy to make (though 18F14K50 may be quite hard to find...). Size to define. USB can be used to power the board.
  • DB9 connector, using MAX232 chip : classical, quite big. External power supply will be required
  • what-you-want option: just plug a module which honor the connector's interface. For instance, I have a bluetooth-to-serial module, this gives the opportunity to easily integrate it to Jaluino.

Few words about bootloader

ICSP connector make the develop-program-test cycle faster, preventing unplug/plug the chip to program it. But still it requires a programmer... A bootloader is required here.

There are many different ones out there, but Tiny Bootloader is widely used, very (very) small, many different programs exits for many different platforms. Bootloader should be able to be triggered without having to manually reset the device (see here for more, it seems to require some hardware parts).


A first schematic

I started to build a schematic. Please keep in mind this is very alpha, and this is not even supposed to work...




License

Following Arduino, I'd like to release Jaluino schematics & PCB under Creative Commons Attribution-Share Alike 3.0.


Basically, it says:

  • AttributionYou must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work).
  • Share AlikeIf you alter, transform, or build upon this work, you may distribute the resulting work only under the same, similar or a compatible license.
See here for more details.


Any suggestions ? Would you like to join the effort ?


PS: see this thread on jallib group for a discussion. Also search the group more...



Sébastien Lelong

Friday, July 10, 2009

Using new ADC libraries

Since several weeks or months, quite a lot of effort has been put into the development and update of ADC libraries. Why ? Because when Guru Stef Mientki first developed this library, he designed it for quite a few PICs (but most used ones at this time). Since jallib is to support all (or as much as) possible PICs, it was time to have a look on this huge piece of code...


Many different cases...

As usual, Microchip PICs offers a wide choice configuring ADC:
  • Not all PICs have ADC module (...)
  • Analog pins are dispatched differently amongst PICs, still for user's sake, they have to be automatically configured as input. We thus need to know, for each PIC, where analog pins are...
  • Some PICs have their analog pins dependent from each other, and some are independent (more on this later)
  • Clock configuration can be different
  • Some PICs have 8-bits low resolution ADC module, some have 10-bits high resolution ADC module (and some have 12-bits, those aren't currently handled, that's a restriction...)
  • Some PICs can have two voltage references (VRef+ and VRef-), only one voltage reference (Vref+) and some can't handle voltage references at all
  • (and probably other differences I can't remember :)...
Luckily most of these differences are transparent to users (hey, that was the goal of this refactoring/rewriting...).


Dependent and independent analog pins

OK, let's write some code ! But before this, you have to understand one very important point: some PICs have their analog pins dependent from each other, some PICs have their analog pins independent from each other. "What is this suppose to mean ?" I can hear... Let's consider two famous PICs: 16F877 and 16F88. 16F877 datasheet explains how to configure the number of analog pins, and vref, setting PCFG bits:


Want 6 analog pins, no Vref ? Then PCFG bits must be set to 0b1001. What will then be the analog pins ? RA0, RA1, RA2, RA3, RA5 and RE0. "What if I want 7 analog pins, no Vref ?" You can't because you'll get a Vref pin, no choice. "What if I want 2 analog pins being RE1 and RE2 ?" You can't, because there's no such combination. So, for this PIC, analog pins are dependent from each other, driven by a combination. In this case, you'll have to specify:
  • the number of ADC channels you want,
  • and amongst them, the number of Vref channels
Note: this is the behavior of the original Stef's lib adc_hardware.jal

Now, let's consider 16F88. In this case, there's no such table:


Mmmh... OK, there are ANS bits, one for each analog pins. Setting an ANS bit to 1 sets the corresponding pin to analog. This means I can set whatever pin I want to be analog. "I can have 3 analog pins, configured on RA0, RA4 and RB6. Freedom !"

Analog pins are
independent from each other in this case, you can do what you want. As a consequence, since it's not driven by a combination, you won't be able to specify the number of ADC channels here. Instead, you'll use set_analog_pin() procedure, and if needed, the reverse set_digital_pin() procedure. These procedures takes a analog pin number as argument. Say analog pin AN5 is on pin RB6. To turn this pin as analog, you just have to write set_analog_pin(5), because this is about analog pin AN5, and not RB6.

Note: As a consequence, these procedures don't exist when analog pins are dependent as in our first case.

Careful:
it's not because there are PCFG bits that PICs have dependent analog pins. Some have PCFG bits which act exactly the same as ANS bits (like some of recent 18F)

"Stop it, show me some code !"

Basically, all the logic accessing ADC module has been kept. Most of the work was about configuring the ADC module to handle all PICs. You'll find adc_read() and adc_read_low_res() functions, as usual. Changes only occur while configuring the ADC module.

16F877 : dependent analog pins

The following examples briefly explains how to setup ADC module when analog pins are dependent from each other, using PIC 16F877.

The following diagram is here to help knowing where analog pins are and where Vref pins are:



Example 1: 16F877, with only one analog pin, no voltage reference

-- beginning is about configuring the chip
-- this is the same for all examples for about 18F877
include 16f877
-- setup clock running @20MHz
pragma target OSC HS
pragma target clock 20_000_000
-- no watchdog
pragma target WDT disabled
pragma target LVP disabled
enable_digital_io()
include delay

-- ok, now setup serial, we'll use this
-- to get ADC measures
const serial_hw_baudrate = 19_200
include serial_hardware
serial_hw_init()


-- ok, now let's configure ADC
-- we want to measure using low resolution
-- (that's our choice, we could use high resolution as well)
const bit ADC_HIGH_RESOLUTION = false
-- we said we want 1 analog channel...
const byte ADC_NCHANNEL = 1
-- and no voltage reference
const byte ADC_NVREF = 0
-- now we can include the library
-- note it's now named "adc", not "adc_hardware" anymore
include adc
-- and run the initialization step
adc_init()


-- will periodically send those chars
var byte measure
forever loop
-- get ADC result, on channel 0
-- this means we're currently reading on pin RA0/AN0 !
measure = adc_read_low_res(0)
-- send it back through serial
serial_hw_write(measure)

-- and sleep a litte to prevent flooding serial...
delay_1ms(200)
end loop

Example 2: 16F877, with 5 analog pins, 1 voltage reference, that is, Vref+

This is almost the same as before, except we now want 5 (analog pins) + 1 (Vref) = 6 ADC channels (yes, I consider Vref+ pin as an ADC channel).

The beginning is the same, here's just the part about ADC configuration and readings:

const bit ADC_HIGH_RESOLUTION = false
-- our 6 ADC channel
const byte ADC_NCHANNEL = 6
-- and one Vref pin
const byte ADC_NVREF = 1
-- the two parameters could be read as:
-- "I want 6 ADC channels, amongst which 1 will be
-- reserved for Vref, and the 5 remaining ones will be
-- analog pins"
include adc
adc_init()

-- will periodically send those chars
var byte measure
forever loop
-- get ADC result, on channel 0
-- this means we're currently reading on pin RA0/AN0 !
measure = adc_read_low_res(0)
-- send it back through serial
serial_hw_write(measure)

-- same for pin RA1/AN1
measure = adc_read_low_res(1)
serial_hw_write(measure)

-- same for pin RA2/AN2
measure = adc_read_low_res(2)
serial_hw_write(measure)

-- pin RA3/AN3 can't be read, since it's Vref+

-- same for pin RA5/AN4
-- 4 is from from "AN4" !
measure = adc_read_low_res(4)
serial_hw_write(measure)

-- same for pin RE10/AN5
measure = adc_read_low_res(5)
serial_hw_write(measure)

-- and sleep a litte to prevent flooding serial...
delay_1ms(200)
end loop


16F88 : independent analog pins

The following example is about setting up ADC module with PIC 16F88, where analog pins are independent from each other.

The following diagram is here to help knowing where analog pins are and where Vref pins are:


Example 1: 16F88, analog pins on RA0/AN0, RA4/AN4 and RB6/AN5. No voltage reference.

-- beginning is about configuring the chip
include 16f88
-- We'll use internal oscillator. It work @ 8MHz
pragma target CLOCK 8_000_000
pragma target OSC INTOSC_NOCLKOUT
OSCCON_IRCF = 0b_111
pragma target WDT disabled
enable_digital_io()

-- ok, now setup serial, we'll use this
-- to get ADC measures
const serial_hw_baudrate = 19_200
include serial_hardware
serial_hw_init()

-- now configure ADC
const bit ADC_HIGH_RESOLUTION = false
const byte ADC_NVREF = 0
-- we can't specify a number of ADC channel here,
-- or we'll get an error !
include adc
adc_init()
-- now we declare the pin we want as analog !
set_analog_pin(0) -- RA0/AN0
set_analog_pin(4) -- RA4/AN4
set_analog_pin(5) -- RB6/AN5

-- reading is then the same
var byte measure
forever loop

measure = adc_read_low_res(0)
serial_hw_write(measure)

measure = adc_read_low_res(4)
serial_hw_write(measure)

measure = adc_read_low_res(5)
serial_hw_write(measure)

end loop



Whether you would want to turn RB6/AN5 into a digital pin again, you'd just call:

set_digital_pin(5)



OK, enough for now. Next time, we'll see how it works for real, with a "Step by Step" tutorial. We'll probably use a 16F88 for this, using serial.

As of July 19th, 2009, this ADC libraries aren't release yet (but expected to be soon). If you want to have fun with them, you'll have to get them from jallib SVN repository. See instructions here:

http://code.google.com/p/jallib/source/checkout


You can also get files one by one from SVN:

Anyway, browse SVN as needed :)



Sébastien Lelong