Basic bitbanging of a PIC microcontroller
Table of Contents
1 Description
This page describes how to transmit and receive serial communications. We use a PIC12f1840 and a USB to UART gadget. We describe a pic program that echos inputs, and a c program that transmits user input and prints the resulting output.
This article is wrong.
1200 baud rate is not 1.2 ms/bit, its 1.2 bits/ms. 1200 baud rate is 0.83 ms/bit. That means that the 8th bit is (1.2 - 0.83) * 8 = 2.96 ms out of sync. My code should not work, yet it does, I do not know why.
2 News
- 4 Apr 2015: 1200 baud rate is not 1.2 ms/bit, its 1.2 bits/ms. 1200 baud rate is 0.83 ms/bit. My code should not work, yet it seems to does.
- 26 Jan 2014: initial
3 Serial communications
The protocol handles 1 byte at a time, the byte is transmitted by setting a pin high or low for a predetermined length of time (that the transmitter and receiver must agree on) 10 times (2 control bits, 8 data bits).
Transmitting the character S
(decimal 83, binary 01010011
) using
baud 1200 causes the pin to look like this:
- -0.1 ms: high (the pin is high when not transmitting)
- 0 ms: low (start bit)
- 1.2 ms: high (
0101001 1
) (first (least significant) bit ofS
) (we wait 1.2ms because baud 1200 means we transmit 1200 bits a second (1200 / 1000 = 1.2) - 2.4 ms: high (
010100 1 1
) (second bit ofS
) - 3.6 ms: low (
01010 0 11
) - 4.8 ms: low (
0101 0 011
) - 6.0 ms: high (
010 1 0011
) - 7.2 ms: low (
01 0 10011
) - 8.4 ms: high (
0 1 010011
) - 9.6 ms: low (
0 1010011
) - 10.8 ms: high (stop bit)
- 12.1 ms: high (idle)
4 Receiving data
Here we show and describe the part of the pic echo program that
receives a byte and stores it in the got
variable.
4.1 Procedure summary
- Wait half a tick (1.2 / 2 = 0.6 ms (for 1200 baud)). This wait aligns us such that we checks the state of the pin in the middle of the tick (and not near the edges where minor fluctuations in timing can cause a misread).
- Wait a full tick to skip past the start bit.
- Right-shift
got
to move the bit we set in step 3. The first time this step is called the right-shift does nothing asgot
is 0. - Set the current bit as the 7th bit of
got
and wait a full tick. - While we have not yet read 8 bits, go to step 3.
- Return. We do not wait for the stop bit because we can have a new byte being sent a tick after the stop bit. And the new bytes start bit is detected outside this procedure.
4.2 Waiting
To wait 1.2 ms we have to know the clock rate of the pic (500 kHz), the number of clocks it takes to execute an instruction (4), and how many instructions each opcode takes to execute (1 or 2). To wait 1.2 ms for this pic running 500 kHz we have to execute 104.17 instructions (500.000 Hz / 4 cycles pr. instruction / 1200 baud = 104.17).
To wait a tick (1.2 ms or 104 instructions), we use the waiti
procedure. waiti
waits a variable number of instructions depending
on the value of the W
register. Calling waiti
with W
= 33 waits
103 instructions, which we calculate as follows:
- 2 instructions from
call waiti
- 1 instruction from
movwf wait1
- 1 instruction from
decfsz wait1, 1
(whilewait1
is greater than 1) - 2 instructions from
bra $-1
- 2 instructions from
decfsz wait1, 1
(whenwait1
= 1) - 2 instructions from
return
We execute step 3. and 4. 32 times, and we execute step 5 once. This gives us: 2 + 1 + (1 + 2) * 32 + 2 + 2 = 103 instructions.
To wait a tick we can call waiti
with W
= 33 (103 instructions)
and execute a single nop
(1 + 103 = 104 instructions = 1 tick).
waiti: ;; W - 0x1E: wait 94 instructions ;; W = 0x1F: wait 97 instructions ;; W = 0x20: wait 100 instructions movwf wait1 decfsz wait1, 1 bra $-1 return
4.3 Waiting imprecision
The wait and the other instructions does not have to exactly wait 104 instructions between each read of the pin. We just have to be within plus or minus 52 instructions (half a tick) for the last bit. That means we cannot be consistently be off by more than 6 instructions per bit read (52 / 8 = 6.5).
As seen in the code, we call waiti
with W
set to less
than 33. These W
values are lesser to account for the instructions
that prepares the received bit and handles looping. We try to execute
104.17 instructions between each execution of "btfsc 0x0C, RXPIN
"
(which is the opcode that reads the pin).
4.4 The get
procedure
variables: got
, iter
, wait1
.
... get: ;; get contents of W over 1200 baud ;; wait 1200 baud (1.2ms) ;; wait instructions 500 kHz / 4 c/i / 1200 ms = 104.17 clrf got ; ready output movlw 0x08 movwf iter ; set up iterator ;; wait half a tick movlw 0x0E call waiti movlw 0x1F ; wait past start bit call waiti loop1: lsrf got, 1 ; right shift got btfsc 0x0C, RXPIN ; if current bit = 1 bsf got, 7 ; set got:0 = 1 ;; wait a tick (100) movlw 0x20 call waiti decfsz iter, 1 goto loop1 return ...
5 transmitting data
Here we show and describe the part of the pic echo program that
transmits the byte defined in the W
register.
5.1 Procedure summary
- Set pin to low to transmit start bit.
- Wait 1 tick.
- If the first bit of the input is 0, set pin low.
- If the first bit of the input is 1, set pin high.
- Right-shift the input to ready the next bit to transmit.
- Go to step 2 until all 8 bits are transmitted.
- Wait 1 tick.
- Set pin high to transmit stop bit.
- Wait 1 tick.
5.2 Waiting imprecision
A difference in the send
procedure (compared to the get
procedure), is that we change the pin in 2 locations ("bcf 0x0C,
TXPIN
" and "bsf 0x0C, TXPIN
"). This change means we wait 2
instructions less than normally when transmitting a 1 followed by a 0,
and 2 instructions more for a 0 followed by a 1. This imprecision does
not accumulate and, if it did, it is still less than the 6
instructions we were allowed to be consistently wrong for each bit
handled.
5.3 The send
procedure
variables: input
, iter
, wait1
... send: ;; send contents of W over 1200 baud ;; wait 1200 baud (1.2ms) ;; wait instructions 500 kHz / 4 c/i / 1200 ms = 104.17 movwf input ; store W in input movlw 0x08 movwf iter ; set up iterator movlb 0 ; select bank 0 (PORTA) bcf 0x0C, TXPIN ; transmit start bit loop2: ;; wait 104 - 9 = 95 movlw 0x1E call waiti btfss input, 0 ; if bit = 0 bcf 0x0C, TXPIN ; transmit a 0 btfsc input, 0 ; if bit = 1 bsf 0x0C, TXPIN ; transmit a 1 lsrf input, 1 ; left shift 0x20 by 1 decfsz iter, 1 ; while bits left to transmit goto loop2 ;; wait 102 - 2 = 100 movlw 0x20 call waiti bsf 0x0C, TXPIN ; transmit stop bit movlw 0x24 ; wait > 104 instructions call waiti return ...
6 Problems
The asm program does not support transmitting and receiving at the same time.
This is very evident when sending more than 1 character using the c terminal program. What happens when sending 2 bytes is that the asm program receives the first byte, then starts transmitting that byte back. Only when the asm program is done transmitting do we return to waiting to receive another byte. Since transmitting and receiving takes the same amount of time, we see that only every second byte is echoed. Even when writing a single character, we also send the newline. But since the newline character is the second byte, it is ignored.
Ignoring every second byte is one problem, another is reading bad data. We can read bad data if someone transmits a byte, then waits half a tick, then transmits another byte. The first half of the second transmitted byte is ignored because the asm program is busy transmitting the first byte. Once the asm program is done transmitting the first byte, it starts receiving the second byte (but it have missed the first half of it). This means that we echo back the most-significant half of the second byte (or less depending on the byte) as the least-significant bits (and the remaining bits as 0 (or the first few least-significant bits of the 3rd byte being sent).
These are severe problems, we could avoid both problems by receiving and transmitting bits using an interrupt timer instead of a busy wait, or by using the built-in EUSART module in the pic (but where's the fun in that?).
7 Pic asm program
I was able to compile this program using gpasm version "gpasm-0.14.1 (Jun 19 2012)". Uploading the resulting .hex file to the pic is explained in PIC microcontroller blinking LED without cheating.
list p=p12f1840 include <p12f1840.inc> __CONFIG _CONFIG1, _MCLRE_OFF & _WDTE_OFF __CONFIG _CONFIG2, _LVP_OFF #define TXPIN 2 #define RXPIN 4 cblock 0x20 wait1 iter input got tmp endc org 0 ;;; Define pin 5 (RA2) as output movlb 0 ; select bank 0 (PORTA) clrf 0x0C ; clear PORTA movlb 2 ; select bank 2 (LATA) clrf 0x0C ; clear LATA movlb 3 ; select bank 3 (ANSELA) clrf 0x0C ; clear ANSELA movlb 1 ; select bank 1 (TRISA) ;; TRISA (0: output, 1: input) bcf 0x0C, TXPIN bsf 0x0C, RXPIN ;;; program movlb 0 ; select bank 0 (PORTA) bsf 0x0C, TXPIN ; init tx loop0: btfsc 0x0C, RXPIN ; loop while input is high bra $-1 call get movf got, 0 call send goto loop0 get: ;; get contents of W over 1200 baud ;; wait 1200 baud (1.2ms) ;; wait instructions 500 khz / 4 c/i / 1200 ms = 104.17 clrf got ; ready output movlw 0x08 movwf iter ; set up iterator ;; wait half a tick movlw 0x0E call waiti movlw 0x1F ; wait past start bit call waiti goto loop1 shift: lsrf got, 1 ; right shift got loop1: btfsc 0x0C, RXPIN ; if current bit = 1 bsf got, 7 ; set got:0 = 1 ;; wait a tick (100) movlw 0x20 call waiti decfsz iter, 1 goto shift return send: ;; send contents of W over 1200 baud ;; wait 1200 baud (1.2ms) ;; wait instructions 500 khz / 4 c/i / 1200 ms = 104.17 movwf input ; store W in input movlw 0x08 movwf iter ; set up iterator movlb 0 ; select bank 0 (PORTA) bcf 0x0C, TXPIN ; transmit start bit loop2: ;; wait 104 - 9 = 95 movlw 0x1E call waiti btfss input, 0 ; if bit = 0 bcf 0x0C, TXPIN ; transmit a 0 btfsc input, 0 ; if bit = 1 bsf 0x0C, TXPIN ; transmit a 1 lsrf input, 1 ; left shift 0x20 by 1 decfsz iter, 1 ; while bits left to transmit goto loop2 ;; wait 102 - 2 = 100 movlw 0x20 call waiti bsf 0x0C, TXPIN ; transmit stop bit movlw 0x24 ; wait > 104 instructions call waiti return waiti: ;; W - 0x1E: wait 94 instructions ;; W = 0x1F: wait 97 instructions ;; W = 0x20: wait 100 instructions ;; wait 95 instructions ;; 4 + 1 + x * (1 + 2) - 1 = 4 + 3x movwf wait1 decfsz wait1, 1 bra $-1 return end
8 C serial communications terminal program
// gcc -Wall -lm -O2 -pipe -std=c99 bitbang_terminal.c -o bitbang_terminal #define _BSD_SOURCE 1 #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <termios.h> #include <unistd.h> #include <string.h> int main(int argc, char *argv[]) { struct termios term_opt; int fd = open("/dev/ttyUSB0", O_RDWR); if(fd < 0) { perror("argh"); return -1; } tcgetattr(fd, &term_opt); cfmakeraw(&term_opt); cfsetispeed(&term_opt, B1200); cfsetospeed(&term_opt, B1200); tcsetattr(fd, TCSAFLUSH, &term_opt); while (1) { char line[256]; if (NULL == fgets(line, sizeof(line), stdin)) { perror("input"); return -3; } if (write(fd, line, strlen(line)) < 0) { perror("send error"); return -4; } for (int i = 0; i < sizeof(line); i++) { line[i] = 0; } sleep(1); int read_bytes = read(fd, line, sizeof(line)); if (read_bytes <= 0) { perror("read error"); return -5; } line[read_bytes] = '\0'; printf("received (%d): %s", read_bytes, line); printf(" ("); for (int i = 0; i < read_bytes; i++) { if (i > 0) { printf(" "); } printf("0x%X", line[i]); } printf(")\n"); } }