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 of S) (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 of S)
  • 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

  1. 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).
  2. Wait a full tick to skip past the start bit.
  3. Right-shift got to move the bit we set in step 3. The first time this step is called the right-shift does nothing as got is 0.
  4. Set the current bit as the 7th bit of got and wait a full tick.
  5. While we have not yet read 8 bits, go to step 3.
  6. 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:

  1. 2 instructions from call waiti
  2. 1 instruction from movwf wait1
  3. 1 instruction from decfsz wait1, 1 (while wait1 is greater than 1)
  4. 2 instructions from bra $-1
  5. 2 instructions from decfsz wait1, 1 (when wait1 = 1)
  6. 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

  1. Set pin to low to transmit start bit.
  2. Wait 1 tick.
  3. If the first bit of the input is 0, set pin low.
  4. If the first bit of the input is 1, set pin high.
  5. Right-shift the input to ready the next bit to transmit.
  6. Go to step 2 until all 8 bits are transmitted.
  7. Wait 1 tick.
  8. Set pin high to transmit stop bit.
  9. 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");
  }
}

Author: Dan Amlund Thomsen

Created: 2019-05-09 Thu 19:53

Validate